ソースを参照

Make I2C bit timing cycle-exact for 400 kHz

Every half bit called d() and scl_release(), about 48 cycles of overhead
per bit, so the documented speeds were far off: delay 0 gave ~326 kHz
rather than ~1 MHz, delay 4 ~235 kHz rather than 400 kHz, and the kernel
driver's default 10 us only ~46 kHz.

Data bits are now inlined with fixed cycle pads: at delay 0 every SCL
period is at least 40 cycles (400 kHz) with tLOW >= 1.3 us and
tHIGH >= 0.6 us, and each delay unit still adds 0.375 us per period.
SCL is read back one cycle after release, since the input synchronizer
made the immediate check fail and sent every bit down the clock
stretching path.

The kernel delay now maps to the nearest speed that is not faster than
requested: 0 or 1 us give 400 kHz, 2 us 235 kHz, 10 us 50 kHz. The CDC
default is 400 kHz. Documentation follows the measured timing.
k4be 3 日 前
親
コミット
85f720394d
6 ファイル変更、89 行追加、30 行削除
  1. 5 3
      README.md
  2. 72 19
      firmware/i2c.c
  3. 1 1
      firmware/i2c.h
  4. 2 2
      firmware/main.c
  5. 8 4
      firmware/tiny.c
  6. 1 1
      host/usbi2c.py

+ 5 - 3
README.md

@@ -11,7 +11,9 @@ independent ways in:
 * 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.
 * 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(0)`, the maximum); each step adds 0.375 us per SCL period
+  (5 = 235 kHz, 10 = 163 kHz, 47 = 50 kHz). Timing is cycle-counted from the generated code:
+  data bits meet Fast-mode tLOW >= 1.3 us and tHIGH >= 0.6 us, reads run at ~314 kHz.
 * Clock stretching supported; stuck-SDA bus recovery runs automatically before a transaction.
 
 ## Build / flash (USBasp)
@@ -63,11 +65,11 @@ without changes. It is not ours: development use only, change it in `usb.c` for
 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)
+modprobe i2c-tiny-usb delay=1    # delay=N: SCL half period in us, rounded to the slower side
 i2cdetect -l                     # "i2c-tiny-usb at bus 00X device 00Y"
 i2cdetect -y N
 ```
-`delay`: 0 = ~1 MHz, 1 = ~570 kHz, 2 = ~250 kHz, 10 = ~100 kHz (approximate, verify with a scope).
+`delay`: 0 or 1 = 400 kHz (the maximum), 2 = 235 kHz, 10 (driver default) = 50 kHz.
 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`):

+ 72 - 19
firmware/i2c.c

@@ -14,10 +14,29 @@
 #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 */
+/*
+ * Bit timing (16 MHz). Data bits are inlined with cycle-exact pads so that
+ * delay 0 gives at least T_BIT cycles per SCL period (400 kHz, Fast mode) with
+ * tLOW >= 1.3 us and tHIGH >= 0.6 us. Each delay unit adds 3 cycles to each
+ * half period: the period is 40 cycles at delay 0 and 38 + 6 * delay above:
+ *   0 = 400 kHz, 5 = 235 kHz, 10 = 163 kHz, 47 = 50 kHz.
+ * Reads run somewhat slower (~314 kHz at delay 0); both stay within Fast mode.
+ * The pads below are tuned against the generated code (see wr()/rd()); the
+ * SCL edge is followed by a read-back, so slow rise or clock stretching only
+ * ever lengthens the high phase.
+ */
+static uint8_t dly = 0;
 static bool    bus_err;
 
-static inline void d(void) { if (dly) _delay_loop_1(dly); }
+/* START/STOP/recovery only: not on the per-bit path, >= 0.8 us at delay 0 */
+static void d(void) { if (dly) _delay_loop_1(dly); }
+
+/* 3 cycles per unit; n stays in a register for the whole byte */
+static inline __attribute__((always_inline)) void xdelay(uint8_t n)
+{
+    if (n)
+        __asm__ volatile ("1: dec %0\n\tbrne 1b" : "+r" (n));
+}
 
 void i2c_set_delay(uint8_t v) { dly = v; }
 
@@ -27,13 +46,49 @@ void i2c_init(void)
     DDRD  &= ~(SCL_M | SDA_M);
 }
 
-static void scl_release(void)    /* release SCL and honour clock stretching */
+static void __attribute__((noinline)) scl_wait(void)    /* stretched or slow rise */
+{
+    uint16_t t = 0;              /* ~16 ms max */
+    while (!(PIND & SCL_M)) if (--t == 0) { bus_err = true; break; }
+}
+
+/*
+ * Release SCL and honour clock stretching. The input synchronizer needs a
+ * cycle before PIND shows the released pin, hence the nop: without it the
+ * check always failed and every bit took the slow path.
+ */
+static inline __attribute__((always_inline)) void scl_up(void)
 {
     SCL_HIGH();
-    if (!(PIND & SCL_M)) {
-        uint16_t t = 0;          /* ~16 ms max */
-        while (!(PIND & SCL_M)) if (--t == 0) { bus_err = true; break; }
-    }
+    __asm__ volatile ("nop");
+    if (!(PIND & SCL_M)) scl_wait();
+}
+
+static void scl_release(void) { scl_up(); }
+
+/*
+ * One SCL pulse for a data or ACK bit, SDA already set up. Returns the SDA
+ * level sampled at the end of the high phase. PAD_LOW/PAD_HIGH make up the
+ * difference between the instruction overhead and the timing targets.
+ */
+/*
+ * Counted on avr-gcc 15 -Os at delay 0, sbi/cbi = 2 cycles, from the SCL
+ * edge to the next one. wr(): data low 13 (bit 1) / 16 (bit 0), ACK low 9 +
+ * ACK_PAD, high 10. rd(): see there. Recount after compiler or code changes.
+ */
+#define PAD_LOW   12u   /* data low 25 / 28, ACK low 24 */
+#define PAD_HIGH  6u    /* high 16 */
+#define ACK_PAD   3u
+static inline __attribute__((always_inline)) uint8_t clock_bit(uint8_t n)
+{
+    __builtin_avr_delay_cycles(PAD_LOW);
+    xdelay(n);
+    scl_up();
+    __builtin_avr_delay_cycles(PAD_HIGH);
+    xdelay(n);
+    uint8_t sda = PIND & SDA_M;
+    SCL_LOW();
+    return sda;
 }
 
 static void start(void)
@@ -53,29 +108,27 @@ static void stop(void)
 
 static bool wr(uint8_t b)        /* returns true on ACK */
 {
+    uint8_t n = dly;
+
     for (uint8_t i = 0; i < 8; i++) {
         if (b & 0x80) SDA_HIGH(); else SDA_LOW();
         b <<= 1;
-        d(); scl_release(); d(); SCL_LOW();
+        clock_bit(n);
     }
     SDA_HIGH();
-    d(); scl_release(); d();
-    bool ack = !(PIND & SDA_M);
-    SCL_LOW();
-    return ack;
+    __builtin_avr_delay_cycles(ACK_PAD);    /* this path is shorter than a data bit */
+    return !clock_bit(n);
 }
 
 static uint8_t rd(bool ack)
 {
-    uint8_t v = 0;
+    uint8_t n = dly, 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();
-    }
+    for (uint8_t i = 0; i < 8; i++)
+        v = (v << 1) | (clock_bit(n) ? 1 : 0);
     if (ack) SDA_LOW(); else SDA_HIGH();
-    d(); scl_release(); d(); SCL_LOW();
+    clock_bit(n);
     SDA_HIGH();
     return v;
 }

+ 1 - 1
firmware/i2c.h

@@ -8,7 +8,7 @@
 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 */
+void    i2c_set_delay(uint8_t d);   /* 0 = 400 kHz, 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);

+ 2 - 2
firmware/main.c

@@ -3,7 +3,7 @@
  *
  * 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 02 n                            set SCL delay (0 = 400 kHz, the default)
  *   A5 03                              ping
  *   A5 04                              bus recovery
  * Reply (device -> host), one per request, in order:
@@ -30,7 +30,7 @@
 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 cdc_delay = 4;                  /* own SCL setting, ~400 kHz */
+static uint8_t cdc_delay = 0;                  /* own SCL setting, 400 kHz */
 static uint8_t txb[USB_PACKET];
 static uint8_t txn;
 

+ 8 - 4
firmware/tiny.c

@@ -56,12 +56,16 @@ 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. */
+/*
+ * SCL half period in us -> i2c.c delay units, never faster than asked. The
+ * period is 38 + 6 * units cycles (40 at 0), so 0 and 1 us both give the
+ * 400 kHz maximum, 2 us 235 kHz and the driver's default 10 us 50 kHz.
+ */
 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);
+    if (us <= 1) return 0;
+    if (us >= 49) return 255;
+    return (uint8_t)((32 * us - 38 + 5) / 6);
 }
 
 static void i2c_io(uint8_t cmd, bool usb_in, uint16_t flags, uint8_t addr, uint16_t len)

+ 1 - 1
host/usbi2c.py

@@ -38,7 +38,7 @@ class Bridge:
         return proto, maxlen
 
     def set_delay(self, n):
-        """SCL half-period delay; 0 = fastest (~1 MHz), 4 = default (~400 kHz)."""
+        """SCL delay; 0 = 400 kHz (default), each step adds 0.375 us per period."""
         self.s.write(bytes([MAGIC, OP_SPEED, n]))
         st, = self._read(1)
         if st: