Ver Fonte

Initial commit. Basic tests successful

kbere há 3 dias atrás
commit
65e9d87ef9
9 ficheiros alterados com 734 adições e 0 exclusões
  1. 3 0
      .gitignore
  2. 51 0
      README.md
  3. 34 0
      firmware/Makefile
  4. 128 0
      firmware/i2c.c
  5. 17 0
      firmware/i2c.h
  6. 120 0
      firmware/main.c
  7. 233 0
      firmware/usb.c
  8. 16 0
      firmware/usb.h
  9. 132 0
      host/usbi2c.py

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+*.elf
+*.hex
+__pycache__

+ 51 - 0
README.md

@@ -0,0 +1,51 @@
+# USB-I2C bridge (ATmega8U2 + TCA9539)
+
+USB CDC-ACM (virtual COM port) to I2C master. No LUFA, no interrupts: ~2.5 KB flash.
+
+* Windows 10/11: binds inbox `usbser.sys` automatically (no driver/INF). Windows 7/8 need a stock CDC INF.
+* 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.**
+  Default ~400 kHz (`set_delay(4)`), `set_delay(0)` is ~1 MHz (TCA9539 supports Fm+ if your pull-ups/capacitance allow).
+* Clock stretching supported; stuck-SDA bus recovery runs automatically before a transaction.
+
+## Build / flash (USBasp)
+```
+cd firmware
+make
+make fuses     # once: external crystal, CKDIV8 off (lfuse FF hfuse D9 efuse F4 - same as Arduino Uno 8U2)
+make flash
+```
+Without the crystal fuse the chip runs from the 8 MHz/8 RC and USB will not enumerate.
+Flashing over ISP overwrites any DFU bootloader. Use `make VBUS=1` if PB6 carries a VBUS sense
+(attach to USB only while PB6 high); not needed for a bus-powered board.
+
+## Protocol (binary, 0xA5-framed; frames may be concatenated)
+| Request | Reply |
+|---|---|
+| `A5 01 addr wlen rlen wdata..` (I2C xfer, 7-bit addr, <=32 B per phase; wlen&rlen>0 = repeated START; both 0 = probe) | `status [rdata]` |
+| `A5 02 n` set SCL delay | `status` |
+| `A5 03` ping | `00 proto maxlen` |
+| `A5 04` bus recovery | `status` |
+
+Status: 0 OK, 1 NACK addr, 2 NACK data, 3 bus error, 0x10 bad op, 0x11 bad length.
+Bytes outside a frame are ignored and a stalled frame is dropped after ~100 ms.
+
+**Speed:** a USB bulk round trip is ~1 ms, so unbatched transactions are ~1000/s. Pipeline
+many frames in one write (`Bridge.batch`): the device answers a whole 64-byte OUT packet with one IN packet.
+
+## Host
+```
+pip install pyserial
+python host/usbi2c.py COM5        # or /dev/ttyACM0 : scan, blink port 0, benchmark
+```
+```python
+from usbi2c import Bridge, TCA9539
+io = TCA9539(Bridge("COM5"), 0x74)   # A1/A0 select 0x74..0x77
+io.set_direction(0xFF00); io.write(0x00AA); print(hex(io.read()))
+```
+
+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"
+```
+VID/PID 1209:0001 is the pid.codes test pair; change it in `usb.c` for anything shipped.

+ 34 - 0
firmware/Makefile

@@ -0,0 +1,34 @@
+MCU      = atmega8u2
+F_CPU    = 16000000UL
+TARGET   = usbi2c
+SRC      = main.c usb.c i2c.c
+# Set to 1 to attach to USB only while PB6 (external VBUS divider/sense) is high
+VBUS     ?= 0
+
+CFLAGS   = -mmcu=$(MCU) -DF_CPU=$(F_CPU) -DUSE_VBUS_DETECT=$(VBUS) -Os -std=gnu11 \
+           -Wall -Wextra -ffunction-sections -fdata-sections -fshort-enums
+LDFLAGS  = -mmcu=$(MCU) -Wl,--gc-sections
+
+all: $(TARGET).hex size
+
+$(TARGET).elf: $(SRC) usb.h i2c.h
+	avr-gcc $(CFLAGS) $(SRC) -o $@ $(LDFLAGS)
+
+$(TARGET).hex: $(TARGET).elf
+	avr-objcopy -O ihex -R .eeprom $< $@
+
+size: $(TARGET).elf
+	avr-size $<
+
+# Flash through USBasp
+flash: $(TARGET).hex
+	avrdude -c usbasp -p m8u2 -U flash:w:$(TARGET).hex:i
+
+# Crystal oscillator, CKDIV8 off (same values as Arduino Uno R3 8U2). Needed once.
+fuses:
+	avrdude -c usbasp -p m8u2 -U lfuse:w:0xFF:m -U hfuse:w:0xD9:m -U efuse:w:0xF4:m
+
+clean:
+	rm -f *.elf *.hex
+
+.PHONY: all size flash fuses clean

+ 128 - 0
firmware/i2c.c

@@ -0,0 +1,128 @@
+#include <avr/io.h>
+#include <util/delay_basic.h>
+#include <stdbool.h>
+#include "i2c.h"
+
+#define SCL_BIT PD3
+#define SDA_BIT PD2
+#define SCL_M   (1 << SCL_BIT)
+#define SDA_M   (1 << SDA_BIT)
+
+/* Open drain: PORT bits stay 0, DDR=1 drives low, DDR=0 releases (pull-up). */
+#define SDA_LOW()  (DDRD |=  SDA_M)
+#define SDA_HIGH() (DDRD &= ~SDA_M)
+#define SCL_LOW()  (DDRD |=  SCL_M)
+#define SCL_HIGH() (DDRD &= ~SCL_M)
+
+static uint8_t dly = 4;          /* default ~400 kHz at 16 MHz - verify with a scope */
+static bool    bus_err;
+
+static inline void d(void) { if (dly) _delay_loop_1(dly); }
+
+void i2c_set_delay(uint8_t v) { dly = v; }
+
+void i2c_init(void)
+{
+    PORTD &= ~(SCL_M | SDA_M);
+    DDRD  &= ~(SCL_M | SDA_M);
+}
+
+static void scl_release(void)    /* release SCL and honour clock stretching */
+{
+    SCL_HIGH();
+    if (!(PIND & SCL_M)) {
+        uint16_t t = 0;          /* ~16 ms max */
+        while (!(PIND & SCL_M)) if (--t == 0) { bus_err = true; break; }
+    }
+}
+
+static void start(void)
+{
+    SDA_HIGH(); d();
+    scl_release(); d();
+    SDA_LOW(); d();
+    SCL_LOW();
+}
+
+static void stop(void)
+{
+    SDA_LOW(); d();
+    scl_release(); d();
+    SDA_HIGH(); d();
+}
+
+static bool wr(uint8_t b)        /* returns true on ACK */
+{
+    for (uint8_t i = 0; i < 8; i++) {
+        if (b & 0x80) SDA_HIGH(); else SDA_LOW();
+        b <<= 1;
+        d(); scl_release(); d(); SCL_LOW();
+    }
+    SDA_HIGH();
+    d(); scl_release(); d();
+    bool ack = !(PIND & SDA_M);
+    SCL_LOW();
+    return ack;
+}
+
+static uint8_t rd(bool ack)
+{
+    uint8_t v = 0;
+    SDA_HIGH();
+    for (uint8_t i = 0; i < 8; i++) {
+        d(); scl_release(); d();
+        v = (v << 1) | ((PIND & SDA_M) ? 1 : 0);
+        SCL_LOW();
+    }
+    if (ack) SDA_LOW(); else SDA_HIGH();
+    d(); scl_release(); d(); SCL_LOW();
+    SDA_HIGH();
+    return v;
+}
+
+/* Diagnostic: optionally enable weak internal pull-ups, return bit0=SCL, bit1=SDA levels */
+uint8_t i2c_lines(uint8_t pullup)
+{
+    if (pullup) PORTD |= (SCL_M | SDA_M); else PORTD &= ~(SCL_M | SDA_M);
+    DDRD &= ~(SCL_M | SDA_M);
+    d(); d();
+    uint8_t p = PIND;
+    return ((p & SCL_M) ? 1 : 0) | ((p & SDA_M) ? 2 : 0);
+}
+
+uint8_t i2c_recover(void)
+{
+    bus_err = false;
+    for (uint8_t i = 0; i < 9 && !(PIND & SDA_M); i++) {
+        SCL_LOW(); d(); d();
+        scl_release(); d(); d();
+    }
+    stop();
+    return (bus_err || !(PIND & SDA_M) || !(PIND & SCL_M)) ? I2C_BUS_ERR : I2C_OK;
+}
+
+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;
+    bus_err = false;
+
+    if (!(PIND & SDA_M) || !(PIND & SCL_M)) {
+        if (i2c_recover() != I2C_OK) return I2C_BUS_ERR;
+    }
+
+    if (wlen || !rlen) {
+        start();
+        if (!wr(addr << 1)) { st = I2C_NACK_ADDR; goto done; }
+        for (uint8_t i = 0; i < wlen; i++)
+            if (!wr(w[i])) { st = I2C_NACK_DATA; goto done; }
+    }
+    if (rlen) {
+        start();                               /* (repeated) START */
+        if (!wr((addr << 1) | 1)) { st = I2C_NACK_ADDR; goto done; }
+        for (uint8_t i = 0; i < rlen; i++)
+            r[i] = rd(i < rlen - 1);
+    }
+done:
+    stop();
+    return bus_err ? I2C_BUS_ERR : st;
+}

+ 17 - 0
firmware/i2c.h

@@ -0,0 +1,17 @@
+/* Bit-banged open-drain I2C master: SCL=PD3, SDA=PD2 (external pull-ups required) */
+#ifndef I2C_H
+#define I2C_H
+
+#include <stdint.h>
+
+enum { I2C_OK = 0, I2C_NACK_ADDR = 1, I2C_NACK_DATA = 2, I2C_BUS_ERR = 3 };
+
+void    i2c_init(void);
+void    i2c_set_delay(uint8_t d);   /* 0 = fastest, each step adds ~0.375 us per SCL period */
+uint8_t i2c_lines(uint8_t pullup);  /* diag: bit0=SCL, bit1=SDA level; pullup=1 enables weak internal pull-ups */
+uint8_t i2c_recover(void);         /* clock out stuck slave, then STOP */
+/* wlen>0: write phase; rlen>0: read phase (repeated START if both);
+ * 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);
+
+#endif

+ 120 - 0
firmware/main.c

@@ -0,0 +1,120 @@
+/*
+ * USB (CDC-ACM) <-> I2C bridge, ATmega8U2 @ 16 MHz.
+ *
+ * Request frame (host -> device), several frames may be concatenated freely:
+ *   A5 01 addr wlen rlen wdata[wlen]   I2C transaction (7-bit addr, wlen/rlen <= 32)
+ *   A5 02 n                            set SCL delay (0 = fastest, default 4 ~ 400 kHz)
+ *   A5 03                              ping
+ *   A5 04                              bus recovery
+ * Reply (device -> host), one per request, in order:
+ *   status [payload]
+ *   status: 0 OK, 1 NACK on address, 2 NACK on data, 3 bus error/timeout,
+ *           0x10 unknown op, 0x11 bad length
+ *   payload: XFER -> rlen bytes (only if status==0); PING -> proto, maxlen
+ * Bytes outside a frame are ignored (so ModemManager "AT" probes are harmless).
+ */
+#include <avr/io.h>
+#include <avr/wdt.h>
+#include <string.h>
+#include "usb.h"
+#include "i2c.h"
+
+#define MAGIC   0xA5
+#define MAXLEN  32
+#define PROTO   1
+
+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 };
+
+static uint8_t txb[USB_PACKET];
+static uint8_t txn;
+
+static void flush(void)
+{
+    if (txn) { usb_tx(txb, txn); txn = 0; }
+}
+
+static uint8_t *reply_begin(uint8_t len)       /* reserve len bytes in the tx buffer */
+{
+    if (txn + len > USB_PACKET) flush();
+    uint8_t *p = &txb[txn];
+    txn += len;
+    return p;
+}
+
+static void reply1(uint8_t st) { *reply_begin(1) = st; }
+
+/* ---- frame parser ---- */
+static uint8_t fbuf[4 + MAXLEN];
+static uint8_t fn;
+static bool    in_frame;
+
+static void frame_done(void) { in_frame = false; fn = 0; }
+
+static void feed(uint8_t b)
+{
+    if (!in_frame) {
+        if (b == MAGIC) { in_frame = true; fn = 0; }
+        return;
+    }
+    fbuf[fn++] = b;
+
+    uint8_t need;
+    switch (fbuf[0]) {
+    case OP_XFER:
+        if (fn < 4) return;
+        if (fbuf[2] > MAXLEN || fbuf[3] > MAXLEN) { reply1(ST_BAD_LEN); frame_done(); return; }
+        need = 4 + fbuf[2];
+        break;
+    case OP_SPEED:
+    case OP_LINES:   need = 2; break;
+    case OP_PING:
+    case OP_RECOVER: need = 1; break;
+    default:         reply1(ST_BAD_OP); frame_done(); return;
+    }
+    if (fn < need) return;
+
+    switch (fbuf[0]) {
+    case OP_XFER: {
+        uint8_t rlen = fbuf[3];
+        uint8_t *p = reply_begin(1 + rlen);    /* reserve worst case, trim on error */
+        uint8_t st = i2c_xfer(fbuf[1] & 0x7F, &fbuf[4], fbuf[2], p + 1, rlen);
+        p[0] = st;
+        if (st != I2C_OK) txn -= rlen;
+        break; }
+    case OP_SPEED:   i2c_set_delay(fbuf[1]); reply1(0); 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_LINES:   { uint8_t *p = reply_begin(2); p[0] = 0; p[1] = i2c_lines(fbuf[1]); break; }
+    }
+    frame_done();
+}
+
+int main(void)
+{
+    MCUSR = 0;
+    wdt_disable();
+    CLKPR = 0x80; CLKPR = 0;                    /* full 16 MHz, no prescaler */
+
+    i2c_init();
+    usb_init();
+
+    TCCR0B = (1 << CS02) | (1 << CS00);         /* clk/1024: overflow every ~16.4 ms */
+    uint8_t idle = 0;
+    uint8_t rx[USB_PACKET];
+
+    for (;;) {
+        usb_task();
+
+        uint8_t n = usb_rx(rx);
+        if (n) {
+            idle = 0;
+            for (uint8_t i = 0; i < n; i++) feed(rx[i]);
+            flush();                            /* one IN packet for the whole OUT packet */
+        }
+        if (TIFR0 & (1 << TOV0)) {              /* resync if a frame stalls ~100 ms */
+            TIFR0 = (1 << TOV0);
+            if (in_frame && ++idle > 6) frame_done();
+        }
+    }
+}

+ 233 - 0
firmware/usb.c

@@ -0,0 +1,233 @@
+/*
+ * Minimal polled USB CDC-ACM stack for ATmega8U2 @ 16 MHz crystal.
+ *
+ * Endpoints (176 B DPRAM total on 8U2):
+ *   EP0  control        32 B
+ *   EP1  bulk OUT       64 B  (host -> device)
+ *   EP2  bulk IN        64 B  (device -> host)
+ *   EP3  interrupt IN    8 B  (CDC notification, never used)
+ */
+#include <avr/io.h>
+#include <avr/pgmspace.h>
+#include <util/delay.h>
+#include "usb.h"
+
+#define USB_VID 0x1209   /* pid.codes test VID/PID - replace for production */
+#define USB_PID 0x0001
+
+#define EP0_SIZE   32
+#define EP_OUT     1
+#define EP_IN      2
+#define EP_NOTIF   3
+
+#ifndef USE_VBUS_DETECT
+#define USE_VBUS_DETECT 0
+#endif
+
+/* ---------------------------------------------------------------- descriptors */
+static const uint8_t dev_desc[] PROGMEM = {
+    18, 1, 0x00, 0x02,          /* USB 2.0 */
+    0x02, 0x00, 0x00,           /* CDC device class */
+    EP0_SIZE,
+    USB_VID & 0xFF, USB_VID >> 8,
+    USB_PID & 0xFF, USB_PID >> 8,
+    0x00, 0x01,                 /* bcdDevice */
+    1, 2, 0,                    /* strings: mfr, product, no serial */
+    1                           /* configurations */
+};
+
+static const uint8_t cfg_desc[] PROGMEM = {
+    9, 2, 67, 0, 2, 1, 0, 0x80, 50,                 /* config, 2 ifaces, bus powered 100 mA */
+    /* Communication interface */
+    9, 4, 0, 0, 1, 0x02, 0x02, 0x00, 0,             /* CDC, ACM, no protocol */
+    5, 0x24, 0x00, 0x10, 0x01,                      /* header, CDC 1.10 */
+    5, 0x24, 0x01, 0x00, 1,                         /* call management, data iface 1 */
+    4, 0x24, 0x02, 0x02,                            /* ACM: line coding + break */
+    5, 0x24, 0x06, 0, 1,                            /* union */
+    7, 5, 0x80 | EP_NOTIF, 3, 8, 0, 0xFF,           /* notification EP */
+    /* Data interface */
+    9, 4, 1, 0, 2, 0x0A, 0x00, 0x00, 0,
+    7, 5, EP_OUT,        2, USB_PACKET, 0, 0,
+    7, 5, 0x80 | EP_IN,  2, USB_PACKET, 0, 0
+};
+
+static const char str_mfr[]  PROGMEM = "Open";
+static const char str_prod[] PROGMEM = "USB-I2C Bridge";
+
+/* ---------------------------------------------------------------- state */
+static uint8_t configured;
+static uint8_t line_coding[7] = { 0x00, 0xC2, 0x01, 0x00, 0, 0, 8 };  /* 115200 8N1 */
+
+bool usb_ready(void) { return configured; }
+
+/* ---------------------------------------------------------------- EP0 helpers */
+static bool ep0_wait(uint8_t mask)
+{
+    uint16_t t = 40000;                         /* ~100 ms */
+    while (!(UEINTX & mask)) {
+        if ((UEINTX & (1 << RXSTPI)) || (UDINT & (1 << EORSTI))) return false;
+        _delay_us(2);
+        if (!--t) return false;
+    }
+    return true;
+}
+
+static void ep0_status_in(void)                 /* zero-length IN status stage */
+{
+    if (ep0_wait(1 << TXINI)) UEINTX = ~(1 << TXINI);
+}
+
+static void ep0_stall(void) { UECONX |= (1 << STALLRQ); }
+
+static void ep0_send(const void *p, uint16_t len, uint16_t wlen, bool pgm)
+{
+    const uint8_t *s = p;
+    bool zlp = len < wlen;
+    if (len > wlen) len = wlen;
+
+    for (;;) {
+        if (!ep0_wait((1 << TXINI) | (1 << RXOUTI))) return;
+        if (UEINTX & (1 << RXOUTI)) { UEINTX = ~(1 << RXOUTI); return; }  /* host aborted */
+        uint8_t n = len > EP0_SIZE ? EP0_SIZE : (uint8_t)len;
+        for (uint8_t i = 0; i < n; i++) UEDATX = pgm ? pgm_read_byte(s++) : *s++;
+        UEINTX = ~(1 << TXINI);
+        len -= n;
+        if (n < EP0_SIZE) break;                /* short packet ends transfer */
+        if (len == 0 && !zlp) break;
+    }
+    if (ep0_wait(1 << RXOUTI)) UEINTX = ~(1 << RXOUTI);   /* status stage */
+}
+
+static uint8_t build_string(uint8_t *out, const char *pgm_s)
+{
+    uint8_t n = 0, c;
+    while (n < 24 && (c = pgm_read_byte(pgm_s++))) { out[2 + 2 * n] = c; out[3 + 2 * n] = 0; n++; }
+    out[0] = 2 + 2 * n; out[1] = 3;
+    return out[0];
+}
+
+static void eps_configure(void)
+{
+    for (uint8_t e = EP_OUT; e <= EP_NOTIF; e++) { UENUM = e; UECONX = 0; UECFG1X = 0; }
+    /* allocation must be ascending */
+    UENUM = EP_OUT;   UECONX = (1 << EPEN); UECFG0X = (2 << EPTYPE0);                  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);
+    UERST = 0x0E; UERST = 0;
+}
+
+static void ep0_setup(void)
+{
+    uint8_t  bmRT = UEDATX, bReq = UEDATX;
+    uint16_t wV = UEDATX; wV |= (uint16_t)UEDATX << 8;
+    uint16_t wI = UEDATX; wI |= (uint16_t)UEDATX << 8;
+    uint16_t wL = UEDATX; wL |= (uint16_t)UEDATX << 8;
+    (void)wI;
+    UEINTX = ~(1 << RXSTPI);
+
+    uint8_t type = bmRT & 0x60;
+
+    if (type == 0x00) {                                   /* standard */
+        switch (bReq) {
+        case 0:  /* GET_STATUS */ { static const uint8_t z[2] = {0, 0}; ep0_send(z, 2, wL, false); break; }
+        case 1: case 3: /* CLEAR/SET_FEATURE */ ep0_status_in(); break;
+        case 5:  /* SET_ADDRESS */
+            UDADDR = wV & 0x7F;
+            ep0_status_in();
+            if (ep0_wait(1 << TXINI)) UDADDR |= (1 << ADDEN);
+            break;
+        case 6:  /* GET_DESCRIPTOR */ {
+            uint8_t t = wV >> 8, idx = wV & 0xFF;
+            uint8_t buf[2 + 48];
+            if (t == 1)                  ep0_send(dev_desc, sizeof dev_desc, wL, true);
+            else if (t == 2 && idx == 0) ep0_send(cfg_desc, sizeof cfg_desc, wL, true);
+            else if (t == 3 && idx == 0) { buf[0] = 4; buf[1] = 3; buf[2] = 0x09; buf[3] = 0x04; ep0_send(buf, 4, wL, false); }
+            else if (t == 3 && idx == 1) ep0_send(buf, build_string(buf, str_mfr), wL, false);
+            else if (t == 3 && idx == 2) ep0_send(buf, build_string(buf, str_prod), wL, false);
+            else ep0_stall();
+            break; }
+        case 8:  ep0_send(&configured, 1, wL, false); break;                 /* GET_CONFIGURATION */
+        case 9:  /* SET_CONFIGURATION */
+            if ((wV & 0xFF) <= 1) {
+                configured = wV & 0xFF;
+                if (configured) eps_configure();
+                ep0_status_in();
+            } else ep0_stall();
+            break;
+        case 10: { static const uint8_t z = 0; ep0_send(&z, 1, wL, false); break; }  /* GET_INTERFACE */
+        case 11: ep0_status_in(); break;                                      /* SET_INTERFACE */
+        default: ep0_stall();
+        }
+    } else if (type == 0x20) {                            /* CDC class */
+        switch (bReq) {
+        case 0x20: /* SET_LINE_CODING */
+            if (ep0_wait(1 << RXOUTI)) {
+                for (uint8_t i = 0; i < 7; i++) line_coding[i] = UEDATX;
+                UEINTX = ~(1 << RXOUTI);
+                ep0_status_in();
+            }
+            break;
+        case 0x21: ep0_send(line_coding, 7, wL, false); break;               /* GET_LINE_CODING */
+        case 0x22: case 0x23: ep0_status_in(); break;                         /* CONTROL_LINE_STATE, SEND_BREAK */
+        default: ep0_stall();
+        }
+    } else ep0_stall();
+}
+
+/* ---------------------------------------------------------------- public API */
+void usb_init(void)
+{
+    REGCR &= ~(1 << REGDIS);                            /* enable 3.3 V USB pad regulator */
+    USBCON = (1 << USBE) | (1 << FRZCLK);
+    PLLCSR = (1 << PLLP0) | (1 << PLLE);                /* PLLP=001: 16 MHz /2 = 8 MHz -> x6 = 48 MHz (as LUFA) */
+    while (!(PLLCSR & (1 << PLOCK)));
+    USBCON = (1 << USBE);                               /* unfreeze */
+    UDCON  = (1 << DETACH);
+#if !USE_VBUS_DETECT
+    UDCON  = 0;                                         /* attach, full speed */
+#else
+    DDRB  &= ~(1 << PB6);
+#endif
+}
+
+void usb_task(void)
+{
+#if USE_VBUS_DETECT
+    if (PINB & (1 << PB6)) UDCON &= ~(1 << DETACH);
+    else { UDCON |= (1 << DETACH); configured = 0; return; }
+#endif
+    if (UDINT & (1 << EORSTI)) {
+        UDINT = ~(1 << EORSTI);
+        configured = 0;
+        UENUM = 0;
+        UECONX = (1 << EPEN);
+        UECFG0X = 0;                                    /* control */
+        UECFG1X = (2 << EPSIZE0) | (1 << ALLOC);       /* 32 B */
+    }
+    UENUM = 0;
+    if (UEINTX & (1 << RXSTPI)) ep0_setup();
+}
+
+uint8_t usb_rx(uint8_t *buf)
+{
+    if (!configured) return 0;
+    UENUM = EP_OUT;
+    if (!(UEINTX & (1 << RXOUTI))) return 0;
+    uint8_t n = UEBCLX;
+    for (uint8_t i = 0; i < n; i++) buf[i] = UEDATX;
+    UEINTX = (uint8_t)~((1 << RXOUTI) | (1 << FIFOCON));
+    return n;
+}
+
+void usb_tx(const uint8_t *buf, uint8_t n)
+{
+    if (!configured || !n) return;
+    UENUM = EP_IN;
+    uint16_t t = 20000;                                 /* ~50 ms: host not reading -> drop */
+    while (!(UEINTX & (1 << TXINI))) {
+        if (!--t) return;
+        _delay_us(2);
+    }
+    for (uint8_t i = 0; i < n; i++) UEDATX = buf[i];
+    UEINTX = (uint8_t)~((1 << TXINI) | (1 << FIFOCON));
+}

+ 16 - 0
firmware/usb.h

@@ -0,0 +1,16 @@
+/* Minimal polled USB CDC-ACM device stack for ATmega8U2 (no LUFA, no interrupts) */
+#ifndef USB_H
+#define USB_H
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#define USB_PACKET 64
+
+void    usb_init(void);
+void    usb_task(void);              /* call often: reset + EP0 handling   */
+bool    usb_ready(void);             /* true once host configured device   */
+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 */
+
+#endif

+ 132 - 0
host/usbi2c.py

@@ -0,0 +1,132 @@
+"""Host library for the ATmega8U2 USB-I2C bridge + TCA9539 helper.  pip install pyserial"""
+import struct
+import serial
+
+MAGIC = 0xA5
+OP_XFER, OP_SPEED, OP_PING, OP_RECOVER = 1, 2, 3, 4
+MAXLEN = 32
+ERRORS = {1: "NACK on address", 2: "NACK on data", 3: "bus error / timeout",
+          0x10: "unknown op", 0x11: "bad length"}
+
+
+class I2CError(IOError):
+    def __init__(self, status):
+        super().__init__(f"I2C error {status}: {ERRORS.get(status, '?')}")
+        self.status = status
+
+
+class Bridge:
+    def __init__(self, port, timeout=0.5):
+        self.s = serial.Serial(port, 115200, timeout=timeout)  # baud is ignored by CDC
+        self.s.reset_input_buffer()
+        self.ping()
+
+    def close(self):
+        self.s.close()
+
+    def _read(self, n):
+        d = self.s.read(n)
+        if len(d) != n:
+            raise TimeoutError("no reply from bridge")
+        return d
+
+    def ping(self):
+        self.s.write(bytes([MAGIC, OP_PING]))
+        st, proto, maxlen = self._read(3)
+        if st:
+            raise I2CError(st)
+        return proto, maxlen
+
+    def set_delay(self, n):
+        """SCL half-period delay; 0 = fastest (~1 MHz), 4 = default (~400 kHz)."""
+        self.s.write(bytes([MAGIC, OP_SPEED, n]))
+        st, = self._read(1)
+        if st:
+            raise I2CError(st)
+
+    def recover(self):
+        self.s.write(bytes([MAGIC, OP_RECOVER]))
+        st, = self._read(1)
+        if st:
+            raise I2CError(st)
+
+    @staticmethod
+    def _frame(addr, w, rlen):
+        w = bytes(w)
+        if len(w) > MAXLEN or rlen > MAXLEN:
+            raise ValueError("max 32 bytes per phase")
+        return bytes([MAGIC, OP_XFER, addr, len(w), rlen]) + w
+
+    def xfer(self, addr, w=b"", rlen=0):
+        """One transaction: write w, then (repeated START) read rlen bytes."""
+        return self.batch([(addr, w, rlen)])[0]
+
+    def batch(self, ops, raise_on_error=True):
+        """Pipeline many (addr, wdata, rlen) transactions in as few USB packets as possible.
+        This is the fast path: one USB round trip (~1 ms) for dozens of transactions."""
+        ops = list(ops)
+        self.s.write(b"".join(self._frame(*o) for o in ops))
+        out, first_err = [], 0
+        for _, _, rlen in ops:                    # always drain every reply to stay in sync
+            st, = self._read(1)
+            if st:
+                first_err = first_err or st
+                out.append(None)
+            else:
+                out.append(self._read(rlen) if rlen else b"")
+        if first_err and raise_on_error:
+            raise I2CError(first_err)
+        return out
+
+    def scan(self):
+        res = self.batch([(a, b"", 0) for a in range(8, 120)], raise_on_error=False)
+        return [a for a, r in zip(range(8, 120), res) if r is not None]
+
+
+class TCA9539:
+    IN0, OUT0, POL0, CFG0 = 0x00, 0x02, 0x04, 0x06
+
+    def __init__(self, bridge, addr=0x74):       # 0x74..0x77 from A1/A0
+        self.b, self.addr = bridge, addr
+
+    def _w16(self, reg, v):
+        self.b.xfer(self.addr, [reg, v & 0xFF, v >> 8])
+
+    def _r16(self, reg):
+        lo, hi = self.b.xfer(self.addr, [reg], 2)
+        return lo | hi << 8
+
+    def set_direction(self, input_mask):          # bit=1 -> input (power-on default 0xFFFF)
+        self._w16(self.CFG0, input_mask)
+
+    def write(self, value):
+        self._w16(self.OUT0, value)
+
+    def read(self):
+        return self._r16(self.IN0)
+
+    def set_polarity(self, mask):
+        self._w16(self.POL0, mask)
+
+
+if __name__ == "__main__":
+    import sys, time
+    b = Bridge(sys.argv[1])
+    print("proto/maxlen:", b.ping())
+    found = b.scan()
+    print("devices:", [hex(a) for a in found])
+    if found:
+        io = TCA9539(b, found[0])
+        io.set_direction(0xFF00)                   # port0 outputs, port1 inputs
+        for i in range(8):
+            io.write(1 << i)
+            time.sleep(0.1)
+        io.write(0)
+        print("port1 inputs: %02X" % (io.read() >> 8))
+        t = time.time()
+        for _ in range(200):
+            io.write(0x55)                          # unbatched: bound by USB 1 ms polling
+        print("single writes/s: %.0f" % (200 / (time.time() - t)))
+        t = time.time()
+        b.batch([(found[0], [TCA9539.OUT0, 0x55, 0], 0)] * 400)
+        print("batched writes/s: %.0f" % (400 / (time.time() - t)))