icE1usb fw: Add GPSDO VCXO trimming loop

Rather basic and not super well tested, use with caution

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Change-Id: I5f9ce6621492be967d6a44d31f270e107f3ef686
diff --git a/firmware/ice40-riscv/icE1usb/Makefile b/firmware/ice40-riscv/icE1usb/Makefile
index a6e9138..25c65d1 100644
--- a/firmware/ice40-riscv/icE1usb/Makefile
+++ b/firmware/ice40-riscv/icE1usb/Makefile
@@ -49,6 +49,7 @@
 	config.h \
 	e1.h \
 	gps.h \
+	gpsdo.h \
 	ice1usb_proto.h \
 	misc.h \
 	usb_desc_ids.h \
@@ -62,6 +63,7 @@
 	e1.c \
 	fw_app.c \
 	gps.c \
+	gpsdo.c \
 	misc.c \
 	usb_desc_app.c \
 	usb_dev.c \
diff --git a/firmware/ice40-riscv/icE1usb/fw_app.c b/firmware/ice40-riscv/icE1usb/fw_app.c
index 78da8b6..c1fe295 100644
--- a/firmware/ice40-riscv/icE1usb/fw_app.c
+++ b/firmware/ice40-riscv/icE1usb/fw_app.c
@@ -15,6 +15,7 @@
 #include "console.h"
 #include "e1.h"
 #include "gps.h"
+#include "gpsdo.h"
 #include "led.h"
 #include "misc.h"
 #include "mini-printf.h"
@@ -94,12 +95,13 @@
 	pdm_set(PDM_E1_RX1,  true, 128 + d, false);
 #endif
 
-	/* Setup clock tuning */
-	pdm_set(PDM_CLK_HI, true, 2048, false);
-	pdm_set(PDM_CLK_LO, false,   0, false);
-
 	/* GPS init */
 	gps_init();
+#if defined(BOARD_ICE1USB_PROTO_ICEBREAKER) || defined(BOARD_ICE1USB_PROTO_BITSY)
+	gpsdo_init(VCTXO_TAITIEN_VT40);
+#else
+	gpsdo_init(VCTXO_SITIME_SIT3808_E);
+#endif
 
 	/* Enable USB directly */
 	usb_init(&app_stack_desc);
@@ -170,6 +172,7 @@
 
 		/* GPS poll */
 		gps_poll();
+		gpsdo_poll();
 		usb_gps_poll();
 	}
 }
diff --git a/firmware/ice40-riscv/icE1usb/gpsdo.c b/firmware/ice40-riscv/icE1usb/gpsdo.c
new file mode 100644
index 0000000..ca9b806
--- /dev/null
+++ b/firmware/ice40-riscv/icE1usb/gpsdo.c
@@ -0,0 +1,254 @@
+/*
+ * gpsdo.c
+ *
+ * Copyright (C) 2019-2022  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include <stdint.h>
+#include <string.h>
+
+#include "console.h"
+#include "gps.h"
+#include "gpsdo.h"
+#include "misc.h"
+
+
+struct {
+	/* Current tuning */
+	struct {
+		uint16_t coarse;
+		uint16_t fine;
+	} tune;
+
+	/* Measurements */
+	struct {
+		uint32_t tick_prev;	/* Previous tick value */
+		uint32_t last;		/* Last measurement */
+		int      skip;		/* Number of measurement left to discard */
+		int      invalid;	/* Number of consecutive invalid measurements */
+	} meas;
+
+	/* FSM */
+	enum {
+		STATE_DISABLED,		/* Forced to manual */
+		STATE_HOLD_OVER,	/* GPS invalid data, hold */
+		STATE_TUNE_COARSE,	/* Adjust coarse tuning until we're +- 3 Hz */
+		STATE_TUNE_FINE,	/* Fine tracking */
+	} state;
+
+	/* Coarse tuning */
+	struct {
+		int vctxo_sensitivity;
+		int step;
+	} coarse;
+
+	/* Fine tracking */
+	struct {
+		int acc;
+		int div;
+	} fine;
+
+} g_gpsdo;
+
+
+/* VCXTO sensitivity vs 'coarse' count for fast initial acquisition */
+	/*
+	 * Note that the spec if often a guaranteed minimum range and goes
+	 * from ~0.1V to 3.2V instead of 0-3.3V so actual sensitivity is
+	 * higher than the "theoritical value". We boost it by ~ 10% here.
+	 */
+static const int
+vctxo_sensitivity[] = {
+	/* +-  50 ppm pull range => ~ 0.75 Hz / hi-count (set to 0.85) */
+	[VCTXO_TAITIEN_VT40]     = 300,
+
+	/* +- 100 ppm pull range => ~ 1.50 Hz / hi-count (set to 1.6) */
+	[VCTXO_SITIME_SIT3808_E] = 160,
+};
+
+/* Tuning params */
+#define TARGET		30720000
+#define MAX_DEV_VALID	1000
+#define MAX_DEV_FINE	3
+#define MAX_INVALID	5
+
+
+static void
+_gpsdo_coarse_start(void)
+{
+	/* Set the state */
+	g_gpsdo.state = STATE_TUNE_COARSE;
+
+	/* Skip a few measurements to be safe */
+	g_gpsdo.meas.skip = 3;
+
+	/* Reset coarse tuning state */
+	g_gpsdo.coarse.step = 0;
+
+	/* Put the fine adjust back in the middle point */
+	g_gpsdo.tune.coarse += ((int)g_gpsdo.tune.fine - 2048) >> 6;
+	g_gpsdo.tune.fine    = 2048;
+
+	pdm_set(PDM_CLK_HI, true, g_gpsdo.tune.coarse, false);
+	pdm_set(PDM_CLK_LO, true, g_gpsdo.tune.fine,   false);
+}
+
+static void
+_gpsdo_fine_start(void)
+{
+	/* Set the state */
+	g_gpsdo.state = STATE_TUNE_FINE;
+
+	/* Reset the long term error tracking */
+	g_gpsdo.fine.acc = 0;
+	g_gpsdo.fine.div = 0;
+}
+
+static void
+_gpsdo_coarse_tune(uint32_t tick_diff)
+{
+	int freq_diff = (int)tick_diff - TARGET;
+
+	/* Is the measurement good enough to switch to fine ? */
+	if ((freq_diff > -MAX_DEV_FINE) && (freq_diff < MAX_DEV_FINE)) {
+		_gpsdo_fine_start();
+		return;
+	}
+
+	/* Estimate correction and apply it */
+	g_gpsdo.tune.coarse -= (freq_diff * g_gpsdo.coarse.vctxo_sensitivity) >> 8;
+	pdm_set(PDM_CLK_HI, true, g_gpsdo.tune.coarse, false);
+
+	/* Skip next measurement */
+	g_gpsdo.meas.skip = 1;
+
+	/* Debug */
+#ifdef GPSDO_DEBUG
+	printf("[+] GPSDO Coarse: err=%d tune=%d\n", freq_diff, g_gpsdo.tune.coarse);
+#endif
+}
+
+static void
+_gpsdo_fine_track(uint32_t tick_diff)
+{
+	int freq_diff = (int)tick_diff - TARGET;
+	uint16_t tune;
+
+	/* Did we deviate too much ? */
+	if ((freq_diff < -2*MAX_DEV_FINE) && (freq_diff > 2*MAX_DEV_FINE)) {
+		_gpsdo_coarse_start();
+		return;
+	}
+
+	/* Accumulate long term error */
+	g_gpsdo.fine.acc += freq_diff;
+
+	/* Update fine tuning value */
+	if (((g_gpsdo.fine.acc >= 0) && (freq_diff > 0)) ||
+	    ((g_gpsdo.fine.acc <= 0) && (freq_diff < 0)))
+	{
+		    g_gpsdo.tune.fine -= freq_diff;
+	}
+
+	if (g_gpsdo.fine.acc) {
+		if (++g_gpsdo.fine.div > 10) {
+			g_gpsdo.fine.div = 0;
+			if (g_gpsdo.fine.acc > 0)
+				g_gpsdo.tune.fine--;
+			else if (g_gpsdo.fine.acc < 0)
+				g_gpsdo.tune.fine++;
+		}
+	} else {
+		g_gpsdo.fine.div = 0;
+	}
+
+	/* Apply value with a bias from long term accumulator */
+	tune = g_gpsdo.tune.fine - (g_gpsdo.fine.acc / 2);
+	pdm_set(PDM_CLK_LO, true, tune, false);
+
+	/* Debug */
+#ifdef GPSDO_DEBUG
+	printf("[+] GPSDO Fine: err=%d acc=%d tune=%d\n", freq_diff, g_gpsdo.fine.acc, g_gpsdo.tune.fine);
+#endif
+}
+
+
+void
+gpsdo_poll(void)
+{
+	uint32_t tick_now, tick_diff;
+	bool valid;
+
+	/* Get current tick and check if there was a PPS and estimate frequency */
+	tick_now = time_pps_read();
+
+	if (tick_now == g_gpsdo.meas.tick_prev)
+		return;
+
+	g_gpsdo.meas.last = tick_diff = tick_now - g_gpsdo.meas.tick_prev;
+	g_gpsdo.meas.tick_prev = tick_now;
+
+	/* If we're currently discarding samples, skip it */
+	if (g_gpsdo.meas.skip) {
+		g_gpsdo.meas.skip--;
+		return;
+	}
+
+	/* If we're disabled, nothing else to do */
+	if (g_gpsdo.state == STATE_DISABLED)
+		return;
+
+	/* Check GPS state */
+	if (!gps_has_valid_fix()) {
+		/* No GPS fix, go to hold-over */
+		g_gpsdo.state = STATE_HOLD_OVER;
+		return;
+	}
+
+	/* Check measurement plausibility */
+	valid = (tick_diff > (TARGET - MAX_DEV_VALID)) && (tick_diff < (TARGET + MAX_DEV_VALID));
+
+	if (valid) {
+		/* If we're in hold-over, switch to active */
+		if (g_gpsdo.state == STATE_HOLD_OVER) {
+			_gpsdo_coarse_start();
+			return;
+		}
+	} else {
+		/* Count invalid measurements and if too many of
+		 * them, we go back to hold-over */
+		if (++g_gpsdo.meas.invalid >= MAX_INVALID) {
+			g_gpsdo.state = STATE_HOLD_OVER;
+			return;
+		}
+	}
+
+	g_gpsdo.meas.invalid = 0;
+
+	/* If we reach here, we have a valid fix, valid measurement and
+	 * we're not in hold-over or disabled. Feed the correct loop */
+	if (g_gpsdo.state == STATE_TUNE_COARSE)
+		_gpsdo_coarse_tune(tick_diff);
+	else if (g_gpsdo.state == STATE_TUNE_FINE)
+		_gpsdo_fine_track(tick_diff);
+}
+
+
+void
+gpsdo_init(enum gpsdo_vctxo_model vctxo)
+{
+	/* State */
+	memset(&g_gpsdo, 0x00, sizeof(g_gpsdo));
+
+	/* Default tune to middle range */
+	g_gpsdo.tune.coarse = 2048;
+	g_gpsdo.tune.fine   = 2048;
+
+	pdm_set(PDM_CLK_HI, true, g_gpsdo.tune.coarse, false);
+	pdm_set(PDM_CLK_LO, true, g_gpsdo.tune.fine,   false);
+
+	/* Initial state and config */
+	g_gpsdo.state = STATE_HOLD_OVER;
+	g_gpsdo.coarse.vctxo_sensitivity = vctxo_sensitivity[vctxo];
+}
diff --git a/firmware/ice40-riscv/icE1usb/gpsdo.h b/firmware/ice40-riscv/icE1usb/gpsdo.h
new file mode 100644
index 0000000..181e4b9
--- /dev/null
+++ b/firmware/ice40-riscv/icE1usb/gpsdo.h
@@ -0,0 +1,16 @@
+/*
+ * gpsdo.h
+ *
+ * Copyright (C) 2019-2022  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+enum gpsdo_vctxo_model {
+	VCTXO_TAITIEN_VT40 = 0,		/* VTEUALJANF-30.720000 */
+	VCTXO_SITIME_SIT3808_E = 1,	/* SIT3808AI-D2-33EE-30.720000T */
+};
+
+void gpsdo_poll(void);
+void gpsdo_init(enum gpsdo_vctxo_model vctxo);