icE1usb fw: Expose the GPS NMEA data as a CDC ACM

Note that this is read-only. We drop all data from the host
because we can't have the host reconfigure the module ...

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Change-Id: Ieb6a653ece882c5f90ab27da1bca04c94184dc5a
diff --git a/firmware/ice40-riscv/icE1usb/Makefile b/firmware/ice40-riscv/icE1usb/Makefile
index 6725f6a..a6e9138 100644
--- a/firmware/ice40-riscv/icE1usb/Makefile
+++ b/firmware/ice40-riscv/icE1usb/Makefile
@@ -54,6 +54,7 @@
 	usb_desc_ids.h \
 	usb_dev.h \
 	usb_e1.h \
+	usb_gps.h \
 	usb_str_app.gen.h \
 	$(NULL)
 
@@ -65,6 +66,7 @@
 	usb_desc_app.c \
 	usb_dev.c \
 	usb_e1.c \
+	usb_gps.c \
 	$(NULL)
 
 
diff --git a/firmware/ice40-riscv/icE1usb/fw_app.c b/firmware/ice40-riscv/icE1usb/fw_app.c
index c5c2b7c..78da8b6 100644
--- a/firmware/ice40-riscv/icE1usb/fw_app.c
+++ b/firmware/ice40-riscv/icE1usb/fw_app.c
@@ -21,6 +21,7 @@
 #include "spi.h"
 #include "usb_dev.h"
 #include "usb_e1.h"
+#include "usb_gps.h"
 #include "utils.h"
 
 
@@ -105,6 +106,7 @@
 	usb_dev_init();
 	usb_dfu_rt_init();
 	usb_e1_init();
+	usb_gps_init();
 
 	/* Start */
 	led_state(true);
@@ -168,5 +170,6 @@
 
 		/* GPS poll */
 		gps_poll();
+		usb_gps_poll();
 	}
 }
diff --git a/firmware/ice40-riscv/icE1usb/gps.c b/firmware/ice40-riscv/icE1usb/gps.c
index 2fd6ab8..c1ad3a8 100644
--- a/firmware/ice40-riscv/icE1usb/gps.c
+++ b/firmware/ice40-riscv/icE1usb/gps.c
@@ -13,6 +13,7 @@
 #include "console.h"
 #include "gps.h"
 #include "misc.h"
+#include "usb_gps.h"
 #include "utils.h"
 
 #include "config.h"
@@ -217,6 +218,9 @@
 
 	/* If we do, process it locally to update our state */
 	_gps_parse_nmea(nmea);
+
+	/* And queue it for USB */
+	usb_gps_puts(nmea);
 }
 
 void
diff --git a/firmware/ice40-riscv/icE1usb/usb_desc_app.c b/firmware/ice40-riscv/icE1usb/usb_desc_app.c
index 22a81d3..e0edf80 100644
--- a/firmware/ice40-riscv/icE1usb/usb_desc_app.c
+++ b/firmware/ice40-riscv/icE1usb/usb_desc_app.c
@@ -42,7 +42,6 @@
 	} __attribute__ ((packed)) e1[2];
 
 	/* CDC */
-#if 0
 	struct {
 		struct usb_intf_desc intf_ctl;
 		struct usb_cdc_hdr_desc cdc_hdr;
@@ -53,7 +52,6 @@
 		struct usb_ep_desc ep_data_out;
 		struct usb_ep_desc ep_data_in;
 	} __attribute__ ((packed)) cdc;
-#endif
 
 	/* DFU Runtime */
 	struct {
@@ -207,7 +205,6 @@
 			},
 		},
 	},
-#if 0
 	.cdc = {
 		.intf_ctl = {
 			.bLength		= sizeof(struct usb_intf_desc),
@@ -230,21 +227,24 @@
 			.bLength		= sizeof(struct usb_cdc_acm_desc),
 			.bDescriptorType	= USB_CS_DT_INTF,
 			.bDescriptorsubtype	= USB_CDC_DST_ACM,
+				/* Set_Line_Coding, Set_Control_Line_State, Get_Line_Coding, */
+				/* and the notification Serial_State */
 			.bmCapabilities		= 0x02,
 		},
 		.cdc_union = {
 			.bLength		= sizeof(struct usb_cdc_union_desc) + 1,
 			.bDescriptorType	= USB_CS_DT_INTF,
 			.bDescriptorsubtype	= USB_CDC_DST_UNION,
-			.bMasterInterface	= 1,
-			.bSlaveInterface	= { 2 },
+			.bMasterInterface	= USB_INTF_GPS_CDC_CTL,
+			.bSlaveInterface	= { USB_INTF_GPS_CDC_DATA },
 		},
 		.ep_ctl = {
 			.bLength		= sizeof(struct usb_ep_desc),
 			.bDescriptorType	= USB_DT_EP,
 			.bEndpointAddress	= USB_EP_GPS_CDC_CTL,
 			.bmAttributes		= 0x03,
-			.wMaxPacketSize		= 64,
+				/* Longest notif is SERIAL_STATE with 2 data bytes */
+			.wMaxPacketSize		= sizeof(struct usb_ctrl_req) + 2,
 			.bInterval		= 0x40,
 		},
 		.intf_data = {
@@ -275,7 +275,6 @@
 			.bInterval		= 0x00,
 		},
 	},
-#endif
 	.dfu = {
 		.intf = {
 			.bLength		= sizeof(struct usb_intf_desc),
diff --git a/firmware/ice40-riscv/icE1usb/usb_desc_ids.h b/firmware/ice40-riscv/icE1usb/usb_desc_ids.h
index 00dbb34..3fbc417 100644
--- a/firmware/ice40-riscv/icE1usb/usb_desc_ids.h
+++ b/firmware/ice40-riscv/icE1usb/usb_desc_ids.h
@@ -9,9 +9,15 @@
 
 #define USB_INTF_E1(p)		(0 + (p))
 #define USB_INTF_DFU		2
-#define USB_INTF_NUM		3
+#define USB_INTF_GPS_CDC_CTL	3
+#define USB_INTF_GPS_CDC_DATA	4
+#define USB_INTF_NUM		5
 
 #define USB_EP_E1_IN(p)		(0x82 + (3 * (p)))
 #define USB_EP_E1_OUT(p)	(0x01 + (3 * (p)))
 #define USB_EP_E1_FB(p)		(0x81 + (3 * (p)))
 #define USB_EP_E1_INT(p)	(0x83 + (3 * (p)))
+
+#define USB_EP_GPS_CDC_CTL	0x88
+#define USB_EP_GPS_CDC_OUT	0x09
+#define USB_EP_GPS_CDC_IN	0x89
diff --git a/firmware/ice40-riscv/icE1usb/usb_gps.c b/firmware/ice40-riscv/icE1usb/usb_gps.c
new file mode 100644
index 0000000..1c10033
--- /dev/null
+++ b/firmware/ice40-riscv/icE1usb/usb_gps.c
@@ -0,0 +1,211 @@
+/*
+ * usb_gps.c
+ *
+ * Copyright (C) 2019-2022  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <no2usb/usb.h>
+#include <no2usb/usb_hw.h>
+#include <no2usb/usb_priv.h>
+
+#include <no2usb/usb_proto.h>
+#include <no2usb/usb_cdc_proto.h>
+
+#include "console.h"
+#include "usb_desc_ids.h"
+
+
+#define BUF_SIZE_LOG	9
+#define BUF_SIZE	(1 << BUF_SIZE_LOG)
+#define BUF_MASK	(BUF_SIZE - 1)
+
+static struct {
+	/* State */
+	bool active;
+
+	/* Buffer */
+	struct {
+		unsigned int wr;
+		unsigned int rd;
+		char data[BUF_SIZE] __attribute__((aligned(4)));
+	} buf;
+} g_usb_gps;
+
+
+static void
+_usb_gps_set_active(bool active)
+{
+	/* Save new state */
+	g_usb_gps.active = active;
+
+	/* Reset FIFO if disabled */
+	if (!active)
+		g_usb_gps.buf.wr = g_usb_gps.buf.rd = 0;
+}
+
+static int
+_usb_gps_fill_packet(unsigned int dst_ofs)
+{
+	unsigned int len;
+
+	/* Len available, limited to 64 */
+	len = (g_usb_gps.buf.wr - g_usb_gps.buf.rd) & BUF_MASK;
+
+	if (len > 64)
+		len = 64;
+
+	/* Copy block */
+	usb_data_write(dst_ofs, &g_usb_gps.buf.data[g_usb_gps.buf.rd], (len + 3) & ~3);
+
+	/* Increment read pointer */
+	g_usb_gps.buf.rd = (g_usb_gps.buf.rd + len) & BUF_MASK;
+
+	/* If length was not multiple of 4, we emptied the FIFO,
+	 * so we reset it to 0 so we're aligned again */
+	if (len & 3)
+		g_usb_gps.buf.wr = g_usb_gps.buf.rd = 0;
+
+	return len;
+}
+
+
+void
+usb_gps_puts(const char *str)
+{
+	unsigned int nxt;
+	char c;
+
+	if (!g_usb_gps.active)
+		return;
+
+	while ((c = *str++) != '\0')
+	{
+		/* Next write pointer pos and full check */
+		nxt = (g_usb_gps.buf.wr + 1) & BUF_MASK;
+		if (nxt == g_usb_gps.buf.rd)
+			/* If overflow, we keep the latest content ... */
+			g_usb_gps.buf.rd = (g_usb_gps.buf.rd + 1) & BUF_MASK;
+
+		/* Write data */
+		g_usb_gps.buf.data[g_usb_gps.buf.wr] = c;
+
+		/* Update write pointer */
+		g_usb_gps.buf.wr = nxt;
+	}
+}
+
+void
+usb_gps_poll(void)
+{
+	volatile struct usb_ep *ep_regs;
+
+	/* OUT EP: We accept data and throw it away */
+	ep_regs = &usb_ep_regs[USB_EP_GPS_CDC_OUT & 0x1f].out;
+	ep_regs->bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(64);
+
+	/* IN EP: Send whatever is queued */
+	ep_regs = &usb_ep_regs[USB_EP_GPS_CDC_OUT & 0x1f].in;
+
+	if ((ep_regs->bd[0].csr & USB_BD_STATE_MSK) != USB_BD_STATE_RDY_DATA)
+	{
+		int len = _usb_gps_fill_packet(ep_regs->bd[0].ptr);
+
+		if (len)
+			ep_regs->bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(len);
+		else
+			ep_regs->bd[0].csr = 0;
+	}
+}
+
+
+static enum usb_fnd_resp
+_cdc_set_conf(const struct usb_conf_desc *conf)
+{
+	const struct usb_intf_desc *intf;
+
+	if (!conf)
+		return USB_FND_SUCCESS;
+
+	/* Configure control interface */
+	intf = usb_desc_find_intf(conf, USB_INTF_GPS_CDC_CTL, 0, NULL);
+	if (!intf)
+		return USB_FND_ERROR;
+
+	usb_ep_boot(intf, USB_EP_GPS_CDC_CTL, false);
+
+	/* Configure data interface */
+	intf = usb_desc_find_intf(conf, USB_INTF_GPS_CDC_DATA, 0, NULL);
+	if (!intf)
+		return USB_FND_ERROR;
+
+	usb_ep_boot(intf, USB_EP_GPS_CDC_OUT, false);
+	usb_ep_boot(intf, USB_EP_GPS_CDC_IN,  false);
+
+	return USB_FND_SUCCESS;
+}
+
+static enum usb_fnd_resp
+_cdc_ctrl_req(struct usb_ctrl_req *req, struct usb_xfer *xfer)
+{
+	static const struct usb_cdc_line_coding linecoding = {
+		.dwDTERate   = 115200,
+		.bCharFormat = 2,
+		.bParityType = 0,
+		.bDataBits   = 8,
+	};
+
+	/* Check this is handled here */
+	if (USB_REQ_TYPE_RCPT(req) != (USB_REQ_TYPE_CLASS | USB_REQ_RCPT_INTF))
+		return USB_FND_CONTINUE;
+
+	if (req->wIndex != USB_INTF_GPS_CDC_CTL)
+		return USB_FND_CONTINUE;
+
+	/* Process request */
+	switch (req->bRequest) {
+	case USB_REQ_CDC_SEND_ENCAPSULATED_COMMAND:
+		/* We don't support any, so just accept and don't care */
+		return USB_FND_SUCCESS;
+
+	case USB_REQ_CDC_GET_ENCAPSULATED_RESPONSE:
+		/* Never anything to return */
+		xfer->len = 0;
+		return USB_FND_SUCCESS;
+
+	case USB_REQ_CDC_SET_LINE_CODING:
+		/* We only support 1 config, doesn't matter what the hosts sends */
+		return USB_FND_SUCCESS;
+
+	case USB_REQ_CDC_GET_LINE_CODING:
+		/* We only support 1 config, send that back */
+		xfer->data = (void*)&linecoding;
+		xfer->len  = sizeof(linecoding);
+		return USB_FND_ERROR;
+
+	case USB_REQ_CDC_SET_CONTROL_LINE_STATE:
+		/* Enable if DTR is set */
+		_usb_gps_set_active((req->wValue & 1) != 0);
+		return USB_FND_SUCCESS;
+	}
+
+	/* Anything else is not handled */
+	return USB_FND_ERROR;
+}
+
+
+static struct usb_fn_drv _cdc_drv = {
+	.set_conf	= _cdc_set_conf,
+	.ctrl_req	= _cdc_ctrl_req,
+};
+
+void
+usb_gps_init(void)
+{
+	memset(&g_usb_gps, 0x00, sizeof(g_usb_gps));
+	usb_register_function_driver(&_cdc_drv);
+}
diff --git a/firmware/ice40-riscv/icE1usb/usb_gps.h b/firmware/ice40-riscv/icE1usb/usb_gps.h
new file mode 100644
index 0000000..3324a45
--- /dev/null
+++ b/firmware/ice40-riscv/icE1usb/usb_gps.h
@@ -0,0 +1,13 @@
+/*
+ * usb_gps.h
+ *
+ * Copyright (C) 2019-2022  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+void usb_gps_puts(const char *str);
+
+void usb_gps_poll(void);
+void usb_gps_init(void);
diff --git a/gateware/cores/no2usb b/gateware/cores/no2usb
index e9d1fbb..fdf42a6 160000
--- a/gateware/cores/no2usb
+++ b/gateware/cores/no2usb
@@ -1 +1 @@
-Subproject commit e9d1fbb8a1796444091cd8a39e2a4318530b4daf
+Subproject commit fdf42a6571a4ae49556626e6fffca1582796f7e8