Ver Fonte

Add i2c-tiny-usb compatible interface, fix SET_CONFIGURATION

Make the device a composite: the CDC-ACM pair is grouped by an IAD and
a third, endpoint-less vendor interface implements the i2c-tiny-usb
EP0 protocol, so the mainline Linux driver registers a kernel I2C
adapter while the serial protocol keeps working.

- Use the i2c-tiny-usb VID/PID 1c40:0534 (development use only).
- Add split-phase I2C primitives for transactions spanning several
  USB requests; stream data stages of any length.
- Hold off CDC processing while a kernel transaction is open, drop an
  abandoned one after ~100 ms.
- Keep separate SCL delay settings for the kernel and CDC interfaces.
- Fix eps_configure() leaving UENUM on EP3, which made the
  SET_CONFIGURATION status stage time out (-110) so no driver bound.
- Document kernel setup and udev rules.
k4be há 3 dias atrás
pai
commit
6f9d419666
9 ficheiros alterados com 279 adições e 18 exclusões
  1. 33 3
      README.md
  2. 2 2
      firmware/Makefile
  3. 26 0
      firmware/i2c.c
  4. 10 0
      firmware/i2c.h
  5. 9 2
      firmware/main.c
  6. 130 0
      firmware/tiny.c
  7. 10 0
      firmware/tiny.h
  8. 45 11
      firmware/usb.c
  9. 14 0
      firmware/usb.h

+ 33 - 3
README.md

@@ -1,8 +1,14 @@
 # USB-I2C bridge (ATmega8U2)
 # USB-I2C bridge (ATmega8U2)
 
 
-USB CDC-ACM (virtual COM port) to I2C master. No LUFA, no interrupts: ~2.5 KB flash.
+USB to I2C master for ATmega8U2. No LUFA, no interrupts: ~3.6 KB flash. Composite device with two
+independent ways in:
+
+* **CDC-ACM virtual COM port** with a fast batched binary protocol (below, `host/usbi2c.py`).
+* **i2c-tiny-usb vendor interface** (EP0 control requests only): the mainline Linux `i2c-tiny-usb`
+  driver registers it as a normal kernel I2C adapter (`/dev/i2c-N`, i2c-tools, kernel client drivers).
 
 
 * Windows 10/11: binds inbox `usbser.sys` automatically (no driver/INF). Windows 7/8 need a stock CDC INF.
 * Windows 10/11: binds inbox `usbser.sys` automatically (no driver/INF). Windows 7/8 need a stock CDC INF.
+* Windows: the vendor (i2c-tiny-usb) interface has no driver and shows up as an unknown device; harmless.
 * Linux: `cdc_acm` -> `/dev/ttyACMx`. Add the udev rule below so ModemManager does not probe it.
 * Linux: `cdc_acm` -> `/dev/ttyACMx`. Add the udev rule below so ModemManager does not probe it.
 * I2C is bit-banged (the 8U2 has no TWI): SCL=PD3, SDA=PD2, open drain. **External pull-ups (2.2-4.7 k to 5 V) are required.**
 * I2C is bit-banged (the 8U2 has no TWI): SCL=PD3, SDA=PD2, open drain. **External pull-ups (2.2-4.7 k to 5 V) are required.**
   Default ~400 kHz (`set_delay(4)`), `set_delay(0)` is ~1 MHz (TCA9539 supports Fm+ if your pull-ups/capacitance allow).
   Default ~400 kHz (`set_delay(4)`), `set_delay(0)` is ~1 MHz (TCA9539 supports Fm+ if your pull-ups/capacitance allow).
@@ -46,6 +52,30 @@ io.set_direction(0xFF00); io.write(0x00AA); print(hex(io.read()))
 
 
 Linux udev (`/etc/udev/rules.d/99-usbi2c.rules`):
 Linux udev (`/etc/udev/rules.d/99-usbi2c.rules`):
 ```
 ```
-SUBSYSTEM=="tty", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="0001", ENV{ID_MM_DEVICE_IGNORE}="1", MODE="0666"
+SUBSYSTEM=="tty", ATTRS{idVendor}=="1c40", ATTRS{idProduct}=="0534", ENV{ID_MM_DEVICE_IGNORE}="1", MODE="0666"
+SUBSYSTEM=="i2c-dev", ATTRS{idVendor}=="1c40", ATTRS{idProduct}=="0534", MODE="0666"
+```
+VID/PID 1c40:0534 is the EZPrototypes i2c-tiny-usb ID, used so the stock kernel driver binds
+without changes. It is not ours: development use only, change it in `usb.c` for anything shipped
+(then bind with `echo VID PID > /sys/bus/usb/drivers/i2c-tiny-usb/new_id`).
+
+## Linux kernel I2C adapter (i2c-tiny-usb)
+Kernel config: `CONFIG_I2C_TINY_USB=m` (Device Drivers > I2C > I2C Hardware Bus support >
+Tiny-USB adapter) and `CONFIG_I2C_CHARDEV` for `/dev/i2c-N`.
+```
+modprobe i2c-tiny-usb            # optional: delay=N (SCL half period in us, default 10 = ~100 kHz)
+i2cdetect -l                     # "i2c-tiny-usb at bus 00X device 00Y"
+i2cdetect -y N
 ```
 ```
-VID/PID 1209:0001 is the pid.codes test pair; change it in `usb.c` for anything shipped.
+`delay`: 0 = ~1 MHz, 1 = ~570 kHz, 2 = ~250 kHz, 10 = ~100 kHz (approximate, verify with a scope).
+It is sent when the driver binds. The kernel interface and the CDC `set_delay` command keep separate settings.
+
+Kernel client drivers work too, e.g. TCA9539 (needs `CONFIG_GPIOLIB` + `CONFIG_GPIO_PCA953X`):
+`echo tca9539 0x74 > /sys/bus/i2c/devices/i2c-N/new_device`.
+
+Notes:
+* `cdc_acm` keeps interfaces 0/1 (grouped by an IAD, which `i2c-tiny-usb` refuses); `i2c-tiny-usb`
+  gets interface 2. Both can be used at the same time: while a kernel transaction is open
+  (START sent, no STOP yet) CDC requests wait; an abandoned transaction is dropped after ~100 ms.
+* Each `i2c_msg` costs two control transfers, so a register read is ~4 ms. Use the CDC path for bulk work.
+* Address NAK -> `-ENXIO`; data NAK or bus error -> `-EIO`. No 10-bit addresses, no protocol mangling.

+ 2 - 2
firmware/Makefile

@@ -1,7 +1,7 @@
 MCU      = atmega8u2
 MCU      = atmega8u2
 F_CPU    = 16000000UL
 F_CPU    = 16000000UL
 TARGET   = usbi2c
 TARGET   = usbi2c
-SRC      = main.c usb.c i2c.c
+SRC      = main.c usb.c i2c.c tiny.c
 # Set to 1 to attach to USB only while PB6 (external VBUS divider/sense) is high
 # Set to 1 to attach to USB only while PB6 (external VBUS divider/sense) is high
 VBUS     ?= 0
 VBUS     ?= 0
 
 
@@ -11,7 +11,7 @@ LDFLAGS  = -mmcu=$(MCU) -Wl,--gc-sections
 
 
 all: $(TARGET).hex size
 all: $(TARGET).hex size
 
 
-$(TARGET).elf: $(SRC) usb.h i2c.h
+$(TARGET).elf: $(SRC) usb.h i2c.h tiny.h
 	avr-gcc $(CFLAGS) $(SRC) -o $@ $(LDFLAGS)
 	avr-gcc $(CFLAGS) $(SRC) -o $@ $(LDFLAGS)
 
 
 $(TARGET).hex: $(TARGET).elf
 $(TARGET).hex: $(TARGET).elf

+ 26 - 0
firmware/i2c.c

@@ -101,6 +101,32 @@ uint8_t i2c_recover(void)
     return (bus_err || !(PIND & SDA_M) || !(PIND & SCL_M)) ? I2C_BUS_ERR : I2C_OK;
     return (bus_err || !(PIND & SDA_M) || !(PIND & SCL_M)) ? I2C_BUS_ERR : I2C_OK;
 }
 }
 
 
+/* ---- split-phase API (one transaction spread over several USB requests) ---- */
+uint8_t i2c_start(uint8_t addr_rw, bool repeated)
+{
+    if (!repeated) {
+        bus_err = false;
+        if (!(PIND & SDA_M) || !(PIND & SCL_M)) {
+            if (i2c_recover() != I2C_OK) return I2C_BUS_ERR;
+        }
+    }
+    start();
+    bool ack = wr(addr_rw);
+    if (bus_err) return I2C_BUS_ERR;
+    return ack ? I2C_OK : I2C_NACK_ADDR;
+}
+
+uint8_t i2c_write(uint8_t b)
+{
+    bool ack = wr(b);
+    if (bus_err) return I2C_BUS_ERR;
+    return ack ? I2C_OK : I2C_NACK_DATA;
+}
+
+uint8_t i2c_read(bool ack)     { return rd(ack); }
+bool    i2c_bus_error(void)    { return bus_err; }
+void    i2c_stop(void)         { stop(); }
+
 uint8_t i2c_xfer(uint8_t addr, const uint8_t *w, uint8_t wlen, uint8_t *r, uint8_t rlen)
 uint8_t i2c_xfer(uint8_t addr, const uint8_t *w, uint8_t wlen, uint8_t *r, uint8_t rlen)
 {
 {
     uint8_t st = I2C_OK;
     uint8_t st = I2C_OK;

+ 10 - 0
firmware/i2c.h

@@ -3,6 +3,7 @@
 #define I2C_H
 #define I2C_H
 
 
 #include <stdint.h>
 #include <stdint.h>
+#include <stdbool.h>
 
 
 enum { I2C_OK = 0, I2C_NACK_ADDR = 1, I2C_NACK_DATA = 2, I2C_BUS_ERR = 3 };
 enum { I2C_OK = 0, I2C_NACK_ADDR = 1, I2C_NACK_DATA = 2, I2C_BUS_ERR = 3 };
 
 
@@ -14,4 +15,13 @@ uint8_t i2c_recover(void);         /* clock out stuck slave, then STOP */
  * wlen==0 && rlen==0: address probe. addr is the 7-bit address. */
  * wlen==0 && rlen==0: address probe. addr is the 7-bit address. */
 uint8_t i2c_xfer(uint8_t addr, const uint8_t *w, uint8_t wlen, uint8_t *r, uint8_t rlen);
 uint8_t i2c_xfer(uint8_t addr, const uint8_t *w, uint8_t wlen, uint8_t *r, uint8_t rlen);
 
 
+/* Split-phase access. i2c_start(): START (or repeated START) + address byte
+ * (addr_rw = addr << 1 | R/W). A non-repeated start clears the error flag and
+ * runs bus recovery if a line is stuck. Returns I2C_OK / I2C_NACK_ADDR / I2C_BUS_ERR. */
+uint8_t i2c_start(uint8_t addr_rw, bool repeated);
+uint8_t i2c_write(uint8_t b);      /* I2C_OK / I2C_NACK_DATA / I2C_BUS_ERR */
+uint8_t i2c_read(bool ack);        /* ack=false on the last byte */
+bool    i2c_bus_error(void);       /* clock-stretch timeout since last non-repeated start */
+void    i2c_stop(void);
+
 #endif
 #endif

+ 9 - 2
firmware/main.c

@@ -12,12 +12,16 @@
  *           0x10 unknown op, 0x11 bad length
  *           0x10 unknown op, 0x11 bad length
  *   payload: XFER -> rlen bytes (only if status==0); PING -> proto, maxlen
  *   payload: XFER -> rlen bytes (only if status==0); PING -> proto, maxlen
  * Bytes outside a frame are ignored (so ModemManager "AT" probes are harmless).
  * Bytes outside a frame are ignored (so ModemManager "AT" probes are harmless).
+ *
+ * The same device also speaks the i2c-tiny-usb protocol on EP0 (tiny.c), so the
+ * Linux i2c-tiny-usb driver can register it as a kernel I2C adapter.
  */
  */
 #include <avr/io.h>
 #include <avr/io.h>
 #include <avr/wdt.h>
 #include <avr/wdt.h>
 #include <string.h>
 #include <string.h>
 #include "usb.h"
 #include "usb.h"
 #include "i2c.h"
 #include "i2c.h"
+#include "tiny.h"
 
 
 #define MAGIC   0xA5
 #define MAGIC   0xA5
 #define MAXLEN  32
 #define MAXLEN  32
@@ -26,6 +30,7 @@
 enum { OP_XFER = 1, OP_SPEED = 2, OP_PING = 3, OP_RECOVER = 4, OP_LINES = 5 };
 enum { OP_XFER = 1, OP_SPEED = 2, OP_PING = 3, OP_RECOVER = 4, OP_LINES = 5 };
 enum { ST_BAD_OP = 0x10, ST_BAD_LEN = 0x11 };
 enum { ST_BAD_OP = 0x10, ST_BAD_LEN = 0x11 };
 
 
+static uint8_t cdc_delay = 4;                  /* own SCL setting, ~400 kHz */
 static uint8_t txb[USB_PACKET];
 static uint8_t txb[USB_PACKET];
 static uint8_t txn;
 static uint8_t txn;
 
 
@@ -82,7 +87,7 @@ static void feed(uint8_t b)
         p[0] = st;
         p[0] = st;
         if (st != I2C_OK) txn -= rlen;
         if (st != I2C_OK) txn -= rlen;
         break; }
         break; }
-    case OP_SPEED:   i2c_set_delay(fbuf[1]); reply1(0); break;
+    case OP_SPEED:   cdc_delay = fbuf[1]; i2c_set_delay(cdc_delay); reply1(0); break;
     case OP_PING:    { uint8_t *p = reply_begin(3); p[0] = 0; p[1] = PROTO; p[2] = MAXLEN; break; }
     case OP_PING:    { uint8_t *p = reply_begin(3); p[0] = 0; p[1] = PROTO; p[2] = MAXLEN; break; }
     case OP_RECOVER: reply1(i2c_recover()); break;
     case OP_RECOVER: reply1(i2c_recover()); break;
     case OP_LINES:   { uint8_t *p = reply_begin(2); p[0] = 0; p[1] = i2c_lines(fbuf[1]); break; }
     case OP_LINES:   { uint8_t *p = reply_begin(2); p[0] = 0; p[1] = i2c_lines(fbuf[1]); break; }
@@ -106,14 +111,16 @@ int main(void)
     for (;;) {
     for (;;) {
         usb_task();
         usb_task();
 
 
-        uint8_t n = usb_rx(rx);
+        uint8_t n = tiny_busy() ? 0 : usb_rx(rx);   /* don't interleave with a kernel transaction */
         if (n) {
         if (n) {
             idle = 0;
             idle = 0;
+            i2c_set_delay(cdc_delay);           /* the kernel interface has its own speed */
             for (uint8_t i = 0; i < n; i++) feed(rx[i]);
             for (uint8_t i = 0; i < n; i++) feed(rx[i]);
             flush();                            /* one IN packet for the whole OUT packet */
             flush();                            /* one IN packet for the whole OUT packet */
         }
         }
         if (TIFR0 & (1 << TOV0)) {              /* resync if a frame stalls ~100 ms */
         if (TIFR0 & (1 << TOV0)) {              /* resync if a frame stalls ~100 ms */
             TIFR0 = (1 << TOV0);
             TIFR0 = (1 << TOV0);
+            tiny_tick();
             if (in_frame && ++idle > 6) frame_done();
             if (in_frame && ++idle > 6) frame_done();
         }
         }
     }
     }

+ 130 - 0
firmware/tiny.c

@@ -0,0 +1,130 @@
+/*
+ * i2c-tiny-usb protocol (Linux drivers/i2c/busses/i2c-tiny-usb.c) on EP0 vendor requests.
+ *
+ *   bReq 0  ECHO        IN   2 B: wValue echoed back
+ *   bReq 1  GET_FUNC    IN   4 B: I2C functionality bits, little endian
+ *   bReq 2  SET_DELAY   OUT  0 B: wValue = SCL half period in us (kernel default 10 = 100 kHz)
+ *   bReq 3  GET_STATUS  IN   1 B: 0 idle, 1 address ACK, 2 address NAK
+ *   bReq 4  I2C_IO (+1 BEGIN: START, +2 END: STOP; neither: repeated START)
+ *           wValue = i2c_msg flags (bit0 = read), wIndex = 7-bit address,
+ *           data stage = message payload (OUT for write, IN for read), any length.
+ *
+ * The kernel sends one I2C_IO per i2c_msg followed by GET_STATUS. Address NAK is
+ * reported via GET_STATUS (-> -ENXIO); data NAK or bus error STALLs the request (-> -EIO).
+ * While a transaction is open, the CDC side is held off (see tiny_busy()).
+ */
+#include <avr/pgmspace.h>
+#include "usb.h"
+#include "i2c.h"
+#include "tiny.h"
+
+#define CMD_ECHO        0
+#define CMD_GET_FUNC    1
+#define CMD_SET_DELAY   2
+#define CMD_GET_STATUS  3
+#define CMD_I2C_IO      4
+#define CMD_IO_BEGIN    1
+#define CMD_IO_END      2
+
+#define STATUS_IDLE     0
+#define STATUS_ACK      1
+#define STATUS_NAK      2
+
+#define I2C_M_RD        0x0001
+#define I2C_M_TEN       0x0010
+
+/* I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL */
+static const uint8_t func[4] PROGMEM = { 0x09, 0x00, 0xFF, 0x0E };
+
+static uint8_t status;
+static bool    open;          /* START sent, no STOP yet */
+static uint8_t idle_ticks;
+static uint8_t k_delay = 50; /* own SCL setting; kernel default 10 us -> 50 */
+
+bool tiny_busy(void) { return open; }
+
+static void close_bus(void)
+{
+    if (open) i2c_stop();
+    open = false;
+}
+
+void usb_reset_hook(void) { close_bus(); status = STATUS_IDLE; }
+
+void tiny_tick(void)          /* ~16 ms: drop a transaction the host abandoned */
+{
+    if (open && ++idle_ticks > 6) close_bus();
+}
+
+/* SCL half period in us -> i2c.c delay units. Period ~1 us + 0.375 us per unit. */
+static uint8_t us_to_delay(uint16_t us)
+{
+    if (us == 0) return 0;
+    if (us > 48) return 255;
+    return (uint8_t)(((2 * us - 1) * 8) / 3);
+}
+
+static void i2c_io(uint8_t cmd, bool usb_in, uint16_t flags, uint8_t addr, uint16_t len)
+{
+    bool rd = flags & I2C_M_RD;
+    if (rd != usb_in || (flags & I2C_M_TEN)) { ep0_stall(); return; }
+
+    bool repeated = open && !(cmd & CMD_IO_BEGIN);
+    if (!repeated) i2c_set_delay(k_delay);    /* the CDC side has its own speed */
+    open = true;
+    uint8_t st = i2c_start((uint8_t)(addr << 1) | rd, repeated);
+    if (st == I2C_BUS_ERR) goto fail;
+    status = (st == I2C_OK) ? STATUS_ACK : STATUS_NAK;
+    bool nak = st != I2C_OK;      /* still complete the data stage, the driver checks status */
+
+    uint8_t buf[EP0_SIZE];
+    if (!rd) {
+        while (len) {
+            uint8_t n = ep0_rx(buf);
+            if (n == 0xFF) goto abort;
+            if (n == 0 || n > len) n = (uint8_t)len;   /* ZLP / overrun: stop here */
+            len -= n;
+            if (!nak)
+                for (uint8_t i = 0; i < n; i++)
+                    if (i2c_write(buf[i]) != I2C_OK) goto fail;
+        }
+    } else {
+        while (len) {
+            uint8_t n = len > EP0_SIZE ? EP0_SIZE : (uint8_t)len;
+            for (uint8_t i = 0; i < n; i++) {
+                len--;
+                buf[i] = nak ? 0xFF : i2c_read(len != 0);     /* NACK the final byte */
+            }
+            if (!nak && i2c_bus_error()) goto fail;
+            if (!ep0_tx(buf, n)) goto abort;
+        }
+    }
+
+    if (nak || (cmd & CMD_IO_END)) close_bus();
+    if (rd) ep0_status_out(); else ep0_status_in();
+    return;
+
+fail:
+    ep0_stall();
+abort:
+    close_bus();
+    status = STATUS_IDLE;
+}
+
+void usb_vendor_request(uint8_t bmRT, uint8_t bReq, uint16_t wV, uint16_t wI, uint16_t wL)
+{
+    idle_ticks = 0;
+    switch (bReq) {
+    case CMD_ECHO:       ep0_send(&wV, 2, wL, false); break;
+    case CMD_GET_FUNC:   ep0_send(func, sizeof func, wL, true); break;
+    case CMD_SET_DELAY:  k_delay = us_to_delay(wV); ep0_status_in(); break;
+    case CMD_GET_STATUS: ep0_send(&status, 1, wL, false); break;
+    case CMD_I2C_IO:
+    case CMD_I2C_IO | CMD_IO_BEGIN:
+    case CMD_I2C_IO | CMD_IO_END:
+    case CMD_I2C_IO | CMD_IO_BEGIN | CMD_IO_END:
+        i2c_io(bReq, bmRT & 0x80, wV, wI & 0x7F, wL);
+        break;
+    default: ep0_stall();
+    }
+}

+ 10 - 0
firmware/tiny.h

@@ -0,0 +1,10 @@
+/* i2c-tiny-usb compatible vendor interface (EP0 control requests) */
+#ifndef TINY_H
+#define TINY_H
+
+#include <stdbool.h>
+
+bool tiny_busy(void);   /* kernel-side I2C transaction open: keep CDC requests waiting */
+void tiny_tick(void);   /* call every ~16 ms */
+
+#endif

+ 45 - 11
firmware/usb.c

@@ -1,6 +1,10 @@
 /*
 /*
  * Minimal polled USB CDC-ACM stack for ATmega8U2 @ 16 MHz crystal.
  * Minimal polled USB CDC-ACM stack for ATmega8U2 @ 16 MHz crystal.
  *
  *
+ * Composite device:
+ *   IAD -> iface 0 (CDC comm) + iface 1 (CDC data): virtual COM port (cdc_acm / usbser)
+ *   iface 2: vendor class, no endpoints - i2c-tiny-usb protocol on EP0 (see tiny.c)
+ *
  * Endpoints (176 B DPRAM total on 8U2):
  * Endpoints (176 B DPRAM total on 8U2):
  *   EP0  control        32 B
  *   EP0  control        32 B
  *   EP1  bulk OUT       64 B  (host -> device)
  *   EP1  bulk OUT       64 B  (host -> device)
@@ -12,10 +16,11 @@
 #include <util/delay.h>
 #include <util/delay.h>
 #include "usb.h"
 #include "usb.h"
 
 
-#define USB_VID 0x1209   /* pid.codes test VID/PID - replace for production */
-#define USB_PID 0x0001
+/* i2c-tiny-usb (EZPrototypes) VID/PID, matched by the mainline i2c-tiny-usb driver.
+ * Not ours to ship - private/development use only. */
+#define USB_VID 0x1C40
+#define USB_PID 0x0534
 
 
-#define EP0_SIZE   32
 #define EP_OUT     1
 #define EP_OUT     1
 #define EP_IN      2
 #define EP_IN      2
 #define EP_NOTIF   3
 #define EP_NOTIF   3
@@ -27,7 +32,7 @@
 /* ---------------------------------------------------------------- descriptors */
 /* ---------------------------------------------------------------- descriptors */
 static const uint8_t dev_desc[] PROGMEM = {
 static const uint8_t dev_desc[] PROGMEM = {
     18, 1, 0x00, 0x02,          /* USB 2.0 */
     18, 1, 0x00, 0x02,          /* USB 2.0 */
-    0x02, 0x00, 0x00,           /* CDC device class */
+    0xEF, 0x02, 0x01,           /* misc class, IAD composite */
     EP0_SIZE,
     EP0_SIZE,
     USB_VID & 0xFF, USB_VID >> 8,
     USB_VID & 0xFF, USB_VID >> 8,
     USB_PID & 0xFF, USB_PID >> 8,
     USB_PID & 0xFF, USB_PID >> 8,
@@ -37,7 +42,8 @@ static const uint8_t dev_desc[] PROGMEM = {
 };
 };
 
 
 static const uint8_t cfg_desc[] PROGMEM = {
 static const uint8_t cfg_desc[] PROGMEM = {
-    9, 2, 67, 0, 2, 1, 0, 0x80, 50,                 /* config, 2 ifaces, bus powered 100 mA */
+    9, 2, 84, 0, 3, 1, 0, 0x80, 50,                 /* config, 3 ifaces, bus powered 100 mA */
+    8, 0x0B, 0, 2, 0x02, 0x02, 0x00, 0,             /* IAD: ifaces 0-1 are one CDC function */
     /* Communication interface */
     /* Communication interface */
     9, 4, 0, 0, 1, 0x02, 0x02, 0x00, 0,             /* CDC, ACM, no protocol */
     9, 4, 0, 0, 1, 0x02, 0x02, 0x00, 0,             /* CDC, ACM, no protocol */
     5, 0x24, 0x00, 0x10, 0x01,                      /* header, CDC 1.10 */
     5, 0x24, 0x00, 0x10, 0x01,                      /* header, CDC 1.10 */
@@ -48,7 +54,9 @@ static const uint8_t cfg_desc[] PROGMEM = {
     /* Data interface */
     /* Data interface */
     9, 4, 1, 0, 2, 0x0A, 0x00, 0x00, 0,
     9, 4, 1, 0, 2, 0x0A, 0x00, 0x00, 0,
     7, 5, EP_OUT,        2, USB_PACKET, 0, 0,
     7, 5, EP_OUT,        2, USB_PACKET, 0, 0,
-    7, 5, 0x80 | EP_IN,  2, USB_PACKET, 0, 0
+    7, 5, 0x80 | EP_IN,  2, USB_PACKET, 0, 0,
+    /* i2c-tiny-usb interface: vendor class, control endpoint only */
+    9, 4, 2, 0, 0, 0xFF, 0x00, 0x00, 0
 };
 };
 
 
 static const char str_mfr[]  PROGMEM = "Open";
 static const char str_mfr[]  PROGMEM = "Open";
@@ -61,7 +69,7 @@ static uint8_t line_coding[7] = { 0x00, 0xC2, 0x01, 0x00, 0, 0, 8 };  /* 115200
 bool usb_ready(void) { return configured; }
 bool usb_ready(void) { return configured; }
 
 
 /* ---------------------------------------------------------------- EP0 helpers */
 /* ---------------------------------------------------------------- EP0 helpers */
-static bool ep0_wait(uint8_t mask)
+bool ep0_wait(uint8_t mask)
 {
 {
     uint16_t t = 40000;                         /* ~100 ms */
     uint16_t t = 40000;                         /* ~100 ms */
     while (!(UEINTX & mask)) {
     while (!(UEINTX & mask)) {
@@ -72,14 +80,14 @@ static bool ep0_wait(uint8_t mask)
     return true;
     return true;
 }
 }
 
 
-static void ep0_status_in(void)                 /* zero-length IN status stage */
+void ep0_status_in(void)                 /* zero-length IN status stage */
 {
 {
     if (ep0_wait(1 << TXINI)) UEINTX = ~(1 << TXINI);
     if (ep0_wait(1 << TXINI)) UEINTX = ~(1 << TXINI);
 }
 }
 
 
-static void ep0_stall(void) { UECONX |= (1 << STALLRQ); }
+void ep0_stall(void) { UECONX |= (1 << STALLRQ); }
 
 
-static void ep0_send(const void *p, uint16_t len, uint16_t wlen, bool pgm)
+void ep0_send(const void *p, uint16_t len, uint16_t wlen, bool pgm)
 {
 {
     const uint8_t *s = p;
     const uint8_t *s = p;
     bool zlp = len < wlen;
     bool zlp = len < wlen;
@@ -98,6 +106,29 @@ static void ep0_send(const void *p, uint16_t len, uint16_t wlen, bool pgm)
     if (ep0_wait(1 << RXOUTI)) UEINTX = ~(1 << RXOUTI);   /* status stage */
     if (ep0_wait(1 << RXOUTI)) UEINTX = ~(1 << RXOUTI);   /* status stage */
 }
 }
 
 
+uint8_t ep0_rx(uint8_t *buf)                   /* one OUT data-stage packet */
+{
+    if (!ep0_wait(1 << RXOUTI)) return 0xFF;
+    uint8_t n = UEBCLX;
+    for (uint8_t i = 0; i < n; i++) buf[i] = UEDATX;
+    UEINTX = ~(1 << RXOUTI);
+    return n;
+}
+
+bool ep0_tx(const uint8_t *buf, uint8_t n)     /* one IN data-stage packet */
+{
+    if (!ep0_wait((1 << TXINI) | (1 << RXOUTI))) return false;
+    if (UEINTX & (1 << RXOUTI)) { UEINTX = ~(1 << RXOUTI); return false; }  /* host aborted */
+    for (uint8_t i = 0; i < n; i++) UEDATX = buf[i];
+    UEINTX = ~(1 << TXINI);
+    return true;
+}
+
+void ep0_status_out(void)                      /* zero-length OUT status stage after IN data */
+{
+    if (ep0_wait(1 << RXOUTI)) UEINTX = ~(1 << RXOUTI);
+}
+
 static uint8_t build_string(uint8_t *out, const char *pgm_s)
 static uint8_t build_string(uint8_t *out, const char *pgm_s)
 {
 {
     uint8_t n = 0, c;
     uint8_t n = 0, c;
@@ -114,6 +145,7 @@ static void eps_configure(void)
     UENUM = EP_IN;    UECONX = (1 << EPEN); UECFG0X = (2 << EPTYPE0) | (1 << EPDIR);   UECFG1X = (3 << EPSIZE0) | (1 << ALLOC);
     UENUM = EP_IN;    UECONX = (1 << EPEN); UECFG0X = (2 << EPTYPE0) | (1 << EPDIR);   UECFG1X = (3 << EPSIZE0) | (1 << ALLOC);
     UENUM = EP_NOTIF; UECONX = (1 << EPEN); UECFG0X = (3 << EPTYPE0) | (1 << EPDIR);   UECFG1X = (0 << EPSIZE0) | (1 << ALLOC);
     UENUM = EP_NOTIF; UECONX = (1 << EPEN); UECFG0X = (3 << EPTYPE0) | (1 << EPDIR);   UECFG1X = (0 << EPSIZE0) | (1 << ALLOC);
     UERST = 0x0E; UERST = 0;
     UERST = 0x0E; UERST = 0;
+    UENUM = 0;                  /* back to EP0: the SET_CONFIGURATION status stage follows */
 }
 }
 
 
 static void ep0_setup(void)
 static void ep0_setup(void)
@@ -122,7 +154,6 @@ static void ep0_setup(void)
     uint16_t wV = UEDATX; wV |= (uint16_t)UEDATX << 8;
     uint16_t wV = UEDATX; wV |= (uint16_t)UEDATX << 8;
     uint16_t wI = UEDATX; wI |= (uint16_t)UEDATX << 8;
     uint16_t wI = UEDATX; wI |= (uint16_t)UEDATX << 8;
     uint16_t wL = UEDATX; wL |= (uint16_t)UEDATX << 8;
     uint16_t wL = UEDATX; wL |= (uint16_t)UEDATX << 8;
-    (void)wI;
     UEINTX = ~(1 << RXSTPI);
     UEINTX = ~(1 << RXSTPI);
 
 
     uint8_t type = bmRT & 0x60;
     uint8_t type = bmRT & 0x60;
@@ -171,6 +202,8 @@ static void ep0_setup(void)
         case 0x22: case 0x23: ep0_status_in(); break;                         /* CONTROL_LINE_STATE, SEND_BREAK */
         case 0x22: case 0x23: ep0_status_in(); break;                         /* CONTROL_LINE_STATE, SEND_BREAK */
         default: ep0_stall();
         default: ep0_stall();
         }
         }
+    } else if (type == 0x40) {                            /* vendor */
+        usb_vendor_request(bmRT, bReq, wV, wI, wL);
     } else ep0_stall();
     } else ep0_stall();
 }
 }
 
 
@@ -199,6 +232,7 @@ void usb_task(void)
     if (UDINT & (1 << EORSTI)) {
     if (UDINT & (1 << EORSTI)) {
         UDINT = ~(1 << EORSTI);
         UDINT = ~(1 << EORSTI);
         configured = 0;
         configured = 0;
+        usb_reset_hook();
         UENUM = 0;
         UENUM = 0;
         UECONX = (1 << EPEN);
         UECONX = (1 << EPEN);
         UECFG0X = 0;                                    /* control */
         UECFG0X = 0;                                    /* control */

+ 14 - 0
firmware/usb.h

@@ -13,4 +13,18 @@ bool    usb_ready(void);             /* true once host configured device   */
 uint8_t usb_rx(uint8_t *buf);        /* copy one OUT packet (<=64B), 0=none */
 uint8_t usb_rx(uint8_t *buf);        /* copy one OUT packet (<=64B), 0=none */
 void    usb_tx(const uint8_t *buf, uint8_t n);  /* send one IN packet (<=64B), drops on timeout */
 void    usb_tx(const uint8_t *buf, uint8_t n);  /* send one IN packet (<=64B), drops on timeout */
 
 
+/* EP0 helpers for vendor requests (data stage may span several 32 B packets) */
+#define EP0_SIZE 32
+bool    ep0_wait(uint8_t mask);
+void    ep0_stall(void);
+void    ep0_status_in(void);         /* finish an OUT/no-data request */
+void    ep0_status_out(void);        /* finish an IN request */
+void    ep0_send(const void *p, uint16_t len, uint16_t wlen, bool pgm);  /* whole IN request */
+uint8_t ep0_rx(uint8_t *buf);        /* next OUT data packet, 0xFF on abort/timeout */
+bool    ep0_tx(const uint8_t *buf, uint8_t n);  /* next IN data packet, false on abort */
+
+/* implemented by the application */
+void    usb_vendor_request(uint8_t bmRT, uint8_t bReq, uint16_t wValue, uint16_t wIndex, uint16_t wLength);
+void    usb_reset_hook(void);        /* USB bus reset seen */
+
 #endif
 #endif