WIP: GPS-DO testing
diff --git a/firmware/ice40-riscv/common/console.c b/firmware/ice40-riscv/common/console.c
index c5a9136..67492e6 100644
--- a/firmware/ice40-riscv/common/console.c
+++ b/firmware/ice40-riscv/common/console.c
@@ -20,13 +20,15 @@
 
 
 static char _printf_buf[128];
+#define SYS_CLK_FREQ 30720000
+#define UART_DIV(baud) ((SYS_CLK_FREQ+(baud)/2)/(baud)-2)
 
 void console_init(void)
 {
 #ifdef BOARD_E1_TRACER
 	uart_regs->clkdiv = 22;	/* ~1 Mbaud with clk=24MHz */
 #else
-	uart_regs->clkdiv = 29;	/* ~1 Mbaud with clk=30.72MHz */
+	uart_regs->clkdiv = UART_DIV(115200);	/* ~1 Mbaud with clk=30.72MHz */
 #endif
 }
 
diff --git a/firmware/ice40-riscv/icE1usb/config.h b/firmware/ice40-riscv/icE1usb/config.h
index e10a120..6ae1b4b 100644
--- a/firmware/ice40-riscv/icE1usb/config.h
+++ b/firmware/ice40-riscv/icE1usb/config.h
@@ -16,3 +16,6 @@
 #define DMA_BASE	0x86000000
 #define E1_CORE_BASE	0x87000000
 #define MISC_BASE	0x88000000
+#define GPS_UART_BASE	0x89000000
+
+#define SYS_CLK_FREQ 30720000
diff --git a/firmware/ice40-riscv/icE1usb/fw_app.c b/firmware/ice40-riscv/icE1usb/fw_app.c
index eebc21f..f240200 100644
--- a/firmware/ice40-riscv/icE1usb/fw_app.c
+++ b/firmware/ice40-riscv/icE1usb/fw_app.c
@@ -8,10 +8,12 @@
 #include <stdint.h>
 #include <stdbool.h>
 #include <string.h>
+#include <math.h>
 
 #include <no2usb/usb.h>
 #include <no2usb/usb_dfu_rt.h>
 
+#include "config.h"
 #include "console.h"
 #include "e1.h"
 #include "led.h"
@@ -61,6 +63,309 @@
         boot_dfu();
 }
 
+// ---------------------------------------------------------------------------
+// GPS
+// ---------------------------------------------------------------------------
+
+struct wb_uart {
+	uint32_t data;
+	uint32_t clkdiv;
+} __attribute__((packed,aligned(4)));
+
+static volatile struct wb_uart * const gps_uart = (void*)(GPS_UART_BASE);
+
+static struct {
+	enum {
+		GS_WAIT   = 0,
+		GS_READ   = 1,
+		GS_CK_HI  = 2,
+		GS_CK_LO  = 3,
+		GS_END_CR = 4,
+		GS_END_LF = 5,
+	} state;
+
+	int     len;
+	uint8_t cksum;
+	char    buf[80];
+} gps;
+
+
+static void
+_gps_empty(bool wait_eol)
+{
+	bool eol = false;
+
+	uint32_t c;
+	while (1) {
+		c = gps_uart->data;
+		if (c & 0x80000000) {
+			if (!wait_eol || eol)
+				break;
+		} else {
+			eol = (c == '\n');
+		}
+	}
+}
+
+void
+gps_send(const char *s)
+{
+	char cksum = 0;
+
+	/* Start sentence */
+	gps_uart->data = '$';
+
+	/* Send payload */
+	while (*s)
+		cksum ^= (gps_uart->data = *s++);
+
+	/* Send checksum */
+	gps_uart->data = '*';
+
+	s = hexstr(&cksum, 1, false);
+	gps_uart->data = *s++;
+	gps_uart->data = *s++;
+
+	gps_uart->data = '\r';
+	gps_uart->data = '\n';
+}
+
+uint8_t
+hexval(char c)
+{
+	if (c >= '0' && c <= '9')
+		return c - '0';
+	else if (c >= 'a' && c <= 'f')
+		return 10 + (c - 'a');
+	else if (c >= 'A' && c <= 'F')
+		return 10 + (c - 'A');
+	else
+		return 0;
+}
+
+const char *
+gps_poll(void)
+{
+	uint32_t c;
+	while (1) {
+		/* Get next char */
+		c = gps_uart->data;
+		if (c & 0x80000000)
+			break;
+
+		/* State */
+		if (c == '$') {
+			/* '$' always triggers reset */
+			gps.state = GS_READ;
+			gps.len   = 0;
+			gps.cksum = 0;
+		} else {
+			switch (gps.state) {
+			case GS_READ:
+				if (c == '*') {
+					gps.state = GS_CK_HI;
+				} else if (gps.len == sizeof(gps.buf)) {
+					gps.state = GS_WAIT;
+				} else {
+					gps.buf[gps.len++] = c;
+					gps.cksum ^= c;
+				}
+				break;
+			case GS_CK_HI:
+				gps.cksum ^= hexval(c) << 4;
+				gps.state = GS_CK_LO;
+				break;
+			case GS_CK_LO:
+				gps.cksum ^= hexval(c);
+				gps.state = GS_END_CR;
+				break;
+			case GS_END_CR:
+				gps.state = (c == '\r') ? GS_END_LF : GS_WAIT;
+				break;
+			case GS_END_LF:
+				gps.state = GS_WAIT;
+				gps.buf[gps.len] = 0x00;
+				if (c == '\n')
+					return gps.buf;
+				break;
+			default:
+				gps.state = GS_WAIT;
+				break;
+			}
+		}
+	}
+	return NULL;
+}
+
+#define UART_DIV(baud) ((SYS_CLK_FREQ+(baud)/2)/(baud)-2)
+
+void
+gps_init(void)
+{
+	int i;
+
+	/* State init */
+	memset(&gps, 0x00, sizeof(gps));
+
+	/* Configure reset gpio */
+	gpio_out(3, false);
+	gpio_dir(3, true);
+
+	/* Attempt reset sequence at 9600 baud and then 115200 baud */
+	for (i=0; i<2; i++)
+	{
+		uint32_t start_time = time_now_read();
+		bool init_ok;
+
+		/* Assert reset */
+		gpio_out(3, false);
+
+		/* Configure uart and empty buffer */
+		gps_uart->clkdiv = i ? UART_DIV(115200) : UART_DIV(9600);
+		_gps_empty(false);
+
+		/* Wait 100 ms */
+		delay(100);
+
+		/* Release reset line */
+		gpio_out(3, true);
+
+		/* Wait for first line of output as sign it's ready, timeout after 1s */
+		while (!time_elapsed(start_time, SYS_CLK_FREQ))
+			if ((init_ok = (gps_poll() != NULL)))
+				break;
+
+		if (init_ok) {
+			printf("[+] GPS ok at %d baud\n", i ? 115200 : 9600);
+			break;
+		}
+	}
+
+	/* Failed ? */
+	if (i == 2) {
+		printf("[!] GPS init failed\n");
+		return;
+	}
+
+#if 1
+	/* If success was at 9600 baud, need to speed up */
+	if (i == 0) {
+		/* Configure GPS to use serial at 115200 baud */
+		gps_send("PCAS01,5");
+
+		/* Add dummy byte which will be mangled during baudrate switch ... */
+		gps_uart->data = 0x00;
+		while (!(gps_uart->clkdiv & (1<<29)));
+
+		/* Set uart to 115200 and empty uart buffer, line aligned */
+		gps_uart->clkdiv = UART_DIV(115200) ;
+		_gps_empty(true);
+	}
+
+	/* Configure GPS to be GPS-only (no GLONASS/BEIDOU) */
+	gps_send("PCAS04,1");
+#endif
+}
+
+// ---------------------------------------------------------------------------
+// GPSDO measurement
+// ---------------------------------------------------------------------------
+
+#define GPSDO_MAX_OFFSET	10000
+#define GPSDO_MAX_CHANGE	100
+
+struct {
+	uint32_t pps_last;	/* Last PPS time */
+	int      diff_last;	/* Last frequency error measurement */
+} gpsdo;
+
+#define HI_STEEPNESS	1678	/* 1.6782 counts per step */
+
+int cur_hi_val = 2048;
+int cur_lo_val = 2085;
+int total_diff = 0;
+
+
+static void
+pps_poll(void)
+{
+	uint32_t pps_now = time_pps_read();
+	static uint32_t cnt = 0;
+	int coarse = 0;
+	static int up = 0, down = 0, same = 0;
+	static int vote_cnt = 0;
+	static int cur_interval = 10;
+	static int same_cnt = 0;
+
+	/* Any change ? */
+	if (pps_now == gpsdo.pps_last)
+		return;
+
+	/* Compute frequency error */
+	int diff_cur = ((pps_now - gpsdo.pps_last) & 0x7fffffff) - 30720000;
+
+	/* Validate measurement */
+	if ((abs(diff_cur) < GPSDO_MAX_OFFSET) &&
+	    (abs(diff_cur - gpsdo.diff_last) < GPSDO_MAX_CHANGE)) {
+		cnt++;
+
+		/* Set the hi-value for a coarse correction */
+		if (cnt == 5) {
+			coarse = (diff_cur * 1000) / HI_STEEPNESS;
+			cur_hi_val -= coarse;
+			pdm_set(PDM_CLK_HI, true, cur_hi_val, false);
+		}
+
+		/* Perform fine correction */
+		if (cnt > 20) {
+			if (diff_cur == 0)
+				same++;
+			else if (diff_cur >= 1)
+				down += diff_cur;
+			else
+				up -= diff_cur;
+
+			if (up > cur_interval || down > cur_interval || same > cur_interval) {
+				if (up > down && up > same) {
+					cur_lo_val++;
+				} else if (down > up && down > same) {
+					cur_lo_val--;
+				} else {
+					if (abs(up - down) > cur_interval/8) {
+						if (up > down)
+							cur_lo_val++;
+						else
+							cur_lo_val--;
+					}
+				}
+
+				pdm_set(PDM_CLK_LO, true, cur_lo_val, false);
+
+				/* we are settled in the current state,
+				 * switch to the next higher integration intverval */
+				if (same > cur_interval/2) {
+					if (cur_interval == 10)
+						cur_interval = 100;
+					else if (cur_interval == 100)
+						cur_interval = 600;
+				}
+
+				up = same = down = 0;
+			}
+
+			total_diff += diff_cur;
+		}
+
+		printf("PPS freq diff: %d Hz, cur_hi_val: %d, cur_lo_val: %d hi_corr, "
+		       "coarse: %d, total_diff: %d | ", diff_cur, cur_hi_val,  cur_lo_val, coarse, total_diff);
+		printf("down %d, same %d, up %d\n", down, same, up);
+	}
+
+	/* Update */
+	gpsdo.pps_last = pps_now;
+	gpsdo.diff_last = diff_cur;
+}
+
 
 void main()
 {
@@ -104,6 +409,10 @@
 	/* Start */
 	e1_init(0, 0);
 	e1_active = true;
+
+	/* GPS init */
+	gps_init();
+
 	led_state(true);
 	usb_connect();
 
@@ -157,5 +466,8 @@
 			e1_poll();
 			usb_e1_run();
 		}
+
+		/* Report clock */
+		pps_poll();
 	}
 }
diff --git a/firmware/ice40-riscv/icE1usb/misc.c b/firmware/ice40-riscv/icE1usb/misc.c
index 3117be9..081fd06 100644
--- a/firmware/ice40-riscv/icE1usb/misc.c
+++ b/firmware/ice40-riscv/icE1usb/misc.c
@@ -12,10 +12,13 @@
 #include "misc.h"
 #include "e1.h"
 
-
 struct misc {
 	uint32_t warmboot;
-	uint32_t gpio;
+	struct {
+		uint16_t oe_out;
+		uint8_t  in;
+		uint8_t  _rsvd;
+	} gpio;
 	uint32_t e1_led;
 	uint32_t _rsvd;
 	struct {
@@ -81,3 +84,58 @@
 {
 	misc_regs->warmboot = (1 << 2) | (fw << 0);
 }
+
+bool
+time_elapsed(uint32_t ref, int tick)
+{
+	return ((misc_regs->time.now - ref) & 0x7fffffff) >= tick;
+}
+
+void
+delay(int ms)
+{
+	uint32_t ref = misc_regs->time.now;
+	ms *= SYS_CLK_FREQ / 1000;
+	while (!time_elapsed(ref, ms));
+}
+
+uint32_t
+time_pps_read(void)
+{
+	return misc_regs->time.pps;
+}
+
+uint32_t
+time_now_read(void)
+{
+	return misc_regs->time.now;
+}
+
+
+void
+gpio_dir(int n, bool output)
+{
+	uint16_t mask = 256 << n;
+
+	if (output)
+		misc_regs->gpio.oe_out |=  mask;
+	else
+		misc_regs->gpio.oe_out &= ~mask;
+}
+
+void
+gpio_out(int n, bool val)
+{
+	uint16_t mask = 1 << n;
+
+	if (val)
+		misc_regs->gpio.oe_out |=  mask;
+	else
+		misc_regs->gpio.oe_out &= ~mask;
+}
+
+bool
+gpio_in(int n)
+{
+	return (misc_regs->gpio.in & (1 << n)) != 0;
+}
diff --git a/firmware/ice40-riscv/icE1usb/misc.h b/firmware/ice40-riscv/icE1usb/misc.h
index 69c0da7..f429b99 100644
--- a/firmware/ice40-riscv/icE1usb/misc.h
+++ b/firmware/ice40-riscv/icE1usb/misc.h
@@ -29,4 +29,13 @@
 void e1_led_set(bool enable, uint8_t cfg);
 uint16_t e1_tick_read(void);
 
+bool time_elapsed(uint32_t ref, int tick);
+void delay(int ms);
+uint32_t time_pps_read(void);
+uint32_t time_now_read(void);
+
+void gpio_dir(int n, bool output);
+void gpio_out(int n, bool val);
+bool gpio_in(int n);
+
 void reboot(int fw);