I2C (M4-F)
Two-wire serial bus controller. RP2350 has two instances of the Synopsys DesignWare DW_apb_i2c (same IP block as the RP2040). Both can operate as master or slave at Standard (100 kHz), Fast (400 kHz) or Fast+ (1 MHz) speeds with 7- or 10-bit addressing. Datasheet ref: RP2350 sec 12.4 + the upstream Synopsys DW_apb_i2c databook.
The driver lives in src/i2c.S; register definitions in
include/i2c.inc; tests in tests/unicorn/test_i2c.py,
tests/unicorn/mocks_i2c.py, and the integration .resc at
tests/renode/i2c.resc.
Bases and IRQ wiring
| Inst | Base | RESETS bit | NVIC IRQ | DREQ TX | DREQ RX |
|---|---|---|---|---|---|
| I2C0 | 0x40090000 | 4 | 35 | 40 | 41 |
| I2C1 | 0x40098000 | 5 | 36 | 42 | 43 |
(DREQ codes from include/dma.inc - shared with M3-C.)
Register layout (subset; full list in include/i2c.inc)
| Off | Name | Notes |
|---|---|---|
| 0x00 | IC_CON | mode + speed + enables; R/O while enabled |
| 0x04 | IC_TAR | target address (master mode) |
| 0x08 | IC_SAR | own address (slave mode) |
| 0x10 | IC_DATA_CMD | TX byte (write) / RX byte (read); see below |
| 0x14 | IC_SS_SCL_HCNT | SCL high count, Standard speed |
| 0x18 | IC_SS_SCL_LCNT | SCL low count, Standard speed |
| 0x1C | IC_FS_SCL_HCNT | SCL high count, Fast / Fast+ speed |
| 0x20 | IC_FS_SCL_LCNT | SCL low count, Fast / Fast+ speed |
| 0x2C | IC_INTR_STAT | post-mask IRQ status |
| 0x30 | IC_INTR_MASK | per-source enables |
| 0x34 | IC_RAW_INTR_STAT | raw IRQ status |
| 0x40 | IC_CLR_INTR | read to clear all sources at once |
| 0x44.. 0x64 | IC_CLR_ |
read to clear one source at a time |
| 0x6C | IC_ENABLE | bit 0 = master switch |
| 0x70 | IC_STATUS | TFNF/TFE/RFNE/MST_ACTIVITY etc. |
| 0x7C | IC_SDA_HOLD | SDA hold time in clk_peri ticks |
| 0x80 | IC_TX_ABRT_SOURCE | non-zero -> last write/read NACKed |
| 0x88 | IC_DMA_CR | TX/RX DMA enable bits |
| 0xA0 | IC_FS_SPKLEN | input-glitch filter (clk_peri ticks) |
IC_DATA_CMD bit layout
[10] RESTART - if set, issue RESTART before this byte
[9] STOP - if set, issue STOP after this byte
[8] CMD - 0 = master write, 1 = master read
[7:0] DAT - data byte (write) or received byte (read)
Master writes use bit 8 = 0 plus bit 9 = 1 on the last byte. Master reads push N words with bit 8 = 1 each; bit 9 = 1 on the last command issues the STOP. RESTART (bit 10) is set on the first command of a new direction within the same transaction.
Initialisation sequence (gotcha: ENABLE-must-be-0)
Per the DW databook §3.2.1, most config registers (IC_CON, SCL counts, IC_SAR, IC_FS_SPKLEN, IC_SDA_HOLD, IC_RX_TL, IC_TX_TL, IC_DMA_CR) are read-only while IC_ENABLE = 1. A second-pass write to IC_CON without first dropping IC_ENABLE is silently ignored, which is a classic day-one-of-driver-bring-up bug.
The driver enforces:
RESETS_RESET CLR (1 << (4 + idx)) ; release from reset
spin on RESETS_RESET_DONE bit ; wait for hardware ack
IC_ENABLE = 0 ; unlock config
IC_CON = MASTER_MODE | SPEED_FS | RESTART_EN | SLAVE_DISABLE | TX_EMPTY_CTRL
IC_FS_SPKLEN = 2 ; ~13 ns glitch filter at 150 MHz
IC_SS/FS_SCL_HCNT = ... ; speed-dependent
IC_SS/FS_SCL_LCNT = ...
IC_SDA_HOLD = 10 (or 4 at 1 MHz)
IC_TX_TL = 0; IC_RX_TL = 0; IC_DMA_CR = 0; IC_INTR_MASK = 0
IC_ENABLE = 1 ; commit
i2c_set_baudrate (called from i2c_init and re-callable later) wraps
the disable -> program -> re-enable dance.
SCL HCNT / LCNT math
DesignWare wants HCNT / LCNT in units of clk_peri ticks. The
inter-byte SCL period is (HCNT + LCNT) ticks plus a fixed overhead
(~8 ticks per period from the IC_FS_SPKLEN-driven glitch filter and
some HW-internal pipeline cycles).
Per the I2C-bus spec §6.1, each speed has minimum bus-high / bus-low times:
| Speed | freq | tHIGH min | tLOW min | period |
|---|---|---|---|---|
| Standard | 100 kHz | 4000 ns | 4700 ns | 10 us |
| Fast | 400 kHz | 600 ns | 1300 ns | 2.5 us |
| Fast+ | 1 MHz | 260 ns | 500 ns | 1 us |
At clk_peri = 150 MHz (RP2350 post-M2 default), one tick = 6.667 ns.
The driver computes:
period_ticks = clk_peri / freq ; e.g. 1500 @ 100 kHz
HCNT = period_ticks * 4 / 10 - 8 (clamped >= 8) ; 40% high
LCNT = period_ticks * 6 / 10 - 1 (clamped >= 8) ; 60% low
The 40/60 split follows the typical DesignWare reference; the -8 / -1 offsets compensate for the controller's own pre/post-amble overhead.
| freq | period | HCNT | LCNT | HCNT+LCNT | Actual SCL |
|---|---|---|---|---|---|
| 100 kHz | 1500 | 592 | 899 | 1491 | 100.6 kHz |
| 400 kHz | 375 | 142 | 224 | 366 | 409.8 kHz |
| 1 MHz | 150 | 52 | 89 | 141 | 1064 kHz |
These satisfy the spec minima for tHIGH and tLOW at every speed.
IC_SDA_HOLD is set to 10 at SS/FS (~67 ns hold) and 4 at 1 MHz
(~27 ns hold) so tHD;DAT stays under the 70 ns Fast+ ceiling.
Which SCL register pair to use
The IC_CON.SPEED bits decide which HCNT/LCNT register pair is consulted on each transfer. The driver matches:
| baudrate | IC_CON.SPEED | HCNT/LCNT pair |
|---|---|---|
| <= 100 kHz | 1 (SS) | IC_SS_SCL_* |
| > 100 kHz | 2 (FS) | IC_FS_SCL_* |
The "FS" pair handles Fast (400 kHz) and Fast+ (1 MHz) - the controller uses the FS pair for both, with the only difference being the hold time and SCL count values you program.
RESTART vs STOP semantics
i2c_write_blocking(idx, addr, src, len, nostop=0)issues the address byte, alllendata bytes, and a STOP after the last one. The bus is released; any other master may take over.i2c_write_blocking(idx, addr, src, len, nostop=1)does not issue STOP - the master keeps the bus and the next call (typically a read) must include a RESTART, which the controller does automatically when IC_CON.RESTART_EN = 1 and the new transaction's first IC_DATA_CMD carries the address (effectively, when IC_TAR is rewritten to the same address with IC_ENABLE briefly toggled). This is the canonical "set EEPROM read-pointer + read N bytes" pattern - seeexamples/i2c_eeprom_demo.S.i2c_read_blocking(idx, addr, dst, len, nostop=0)issueslenread commands; the last one carries STOP. Withnostop=1the bus is held instead.
DMA pacing
IC_DMA_CR enables TX/RX DMA requests:
- bit 0 (RDMAE) - request a DREQ_I2Cn_RX whenever IC_RXFLR > IC_DMA_RDLR
- bit 1 (TDMAE) - request a DREQ_I2Cn_TX whenever IC_TXFLR <= IC_DMA_TDLR
i2c_set_dma_enabled(idx, tx, rx) writes the bit pattern. The DMA
channel still needs CTRL.TREQ_SEL = 40/41/42/43 (see include/dma.inc)
and READ_ADDR / WRITE_ADDR pointed at IC_DATA_CMD on the I2C side and
the user buffer on the other.
There's no integrated I2C+DMA example in M4-F (would require the M3-C DMA driver and a non-trivial wiring); the API is plumbed through so that future demos can stitch the two together.
Cycle budgets (Cortex-M33 from SRAM, no APB wait states)
| Function | Cycles |
|---|---|
_i2c_base |
3 (cmp + ldr) |
i2c_init |
~30 + DesignWare settling time |
i2c_set_baudrate |
~40 (UDIV pair + 4 stores) |
i2c_set_pins |
~40 (2 x gpio_set_function + 2 x pull_up) |
i2c_write_blocking per byte |
~12 + bus-paced wait |
i2c_clear_irq |
~6 (single bus read, side-effect IS the read) |
i2c_get_status |
4 |
Bus speed dominates any non-trivial transfer: a 1-byte write at 100 kHz takes ~100 us regardless of how fast the CPU is.
Public API
See the i2c_* symbols at the top of src/i2c.S for the full
declaration block; AAPCS calling convention throughout.
Test coverage
- T1 (Unicorn host harness):
tests/unicorn/test_i2c.py- 24 tests covering init, baudrate at 100k/400k/1M, write/read with and without STOP, IRQ mask programming, IC_CLR_INTR semantics, DMA enable bits, slave-mode entry, GPIO pin programming, and an end-to-end run through each of the three example ELFs. - T3 (Renode integration):
tests/renode/i2c.rescloadsbuild/i2c_eeprom_demo.elf, runs 500 ms simulated, asserts that the UART captured the EEPROM write banner and the four-byte read-back pattern (a5 b6 c7 d8) from the I2C peripheral stub at addr 0x50.