I2C and SPI slave engine¶
A device that implements the slave group of ww_i2c or
ww_spi stands in for a chip on another master's bus. What it answers, when
a host update takes effect, what happens when the host is too slow and what each event says is fixed by the API;
the ww_bus_slave crate does all of it, so a firmware only wires its peripheral's interrupts to it. It is no_std,
sans-IO and needs no allocator.
Two sides¶
I2cSlave and SpiSlave are plain structs with &mut self methods of two kinds:
- Interrupt side, called per byte from the peripheral's interrupt handler: an address matched, a byte came, a byte is wanted, a stop; CS edges for SPI.
- Task side, called from the API handlers:
configurewhen theslave_events/slave_framesstream opens,disablewhen it closes,map_write/map_read,push_answerfor each item of theslave_answerssink (free_answer_slotsis how many credits it may give),clear_answers, andpeek_event/pop_event/take_droppedto move events into the counted stream.
The firmware decides how the two share the engine, usually a critical-section mutex in a static. new is a
const fn, so the engine (map, queue and event ring, a few KB) is built in place in that static and never
passes through the stack:
type Slave = ww_bus_slave::I2cSlave<256, 64, 4, 64, 8, 64>;
static SLAVE: Mutex<CriticalSectionRawMutex, RefCell<Slave>> = Mutex::new(RefCell::new(Slave::new()));
The const parameters are the sizes: register map bytes, room for staged host updates (each takes 6 bytes more than
its data), answer slots and their length, events and the bytes kept per event. They are what the device reports in
I2cSlaveCapabilities / SpiSlaveCapabilities (map_len, queue_len), together with what the peripheral can do
(the stretch limit, Nack, 10-bit addresses, the clock).
Times are u32 microseconds of the device clock, wrapping, the unit the events carry.
I2C¶
| Peripheral event | Call |
|---|---|
| Address matched (start or repeated start) | address(read, now); it says whether to acknowledge |
| Byte written by the master | write(byte, now) |
| Master reads a byte | read(now): ReadByte::Byte(b) to send, or ReadByte::Stretch |
| Repeated start reported on its own | restart(now) |
| Stop | stop(now), then wake the task |
| Bus error | bus_error(now) |
write and read see the direction themselves when a peripheral reports no address match (DW_apb_i2c on the
RP2040/RP2350 reports bytes, RD_REQ and STOP_DET): a byte of the other direction ends the transfer going on as a
repeated start.
- Stretching. With
I2cSlaveUnderrun::Stretch,readanswersStretchwhen no answer is queued and recordsI2cSlaveEvent::ReadWaitingfor the task to send at once. The firmware leaves the TX register empty (DW_apb_i2c and STM32 hold SCL low on their own meanwhile), arms a timer forstretch_deadline(), and callsresume(now)when it fires and after eachpush_answer: it gives the byte to send once the answer is there or the limit passed. Nack.acks_address()says whether the address should be acknowledged now. A peripheral that acknowledges by itself has its slave address enabled by it afterconfigure,push_answerandstop.- A TX FIFO loaded ahead. Bytes
readgave that the master didn't clock out (it NACKed earlier) are taken back withunsent(n)beforestop, so the register pointer and the answer stay where the master left them. - Host updates of the map made during a transaction wait for its stop (
map_writestages them;Busywhen the staging room is full: try again onceis_idle()), so a master never reads half of a multi-byte value.
SPI¶
| Peripheral event | Call |
|---|---|
| TX FIFO has room (also before CS goes active) | miso(): the byte to load |
| CS asserted | cs_asserted(now) |
| Byte received | mosi(byte) |
| RX FIFO overflowed | overrun() |
| CS released | cs_released(now), then wake the task |
SPI has no clock stretching, so a frame's answer has to be in the TX FIFO before the master clocks: the frame picks
its answer at its first miso() call, usually right after the previous CS release. An answer queued after that goes
to the next frame; a firmware that can empty its TX FIFO calls rewind() before CS and loads again, and the new
answer goes out in this frame.
With the register map, the address arrives on MOSI while MISO already runs: a byte loaded before the address is
complete goes out as idle, and the master's dummy bytes (SpiRegisters::dummy_bytes, before read data only) are
the time the device has. With a FIFO d bytes deep, dummy_bytes >= d - 1 gives every data byte right; with fewer,
the first data bytes come out as idle and the map data starts that many bytes in (the record shows what was sent).
Host updates of the map wait for the CS release as on I2C; a Busy map_write is tried again after the next
release, which empties the staging room. The engine takes 8-bit words only; configure answers Unsupported for
others.
Cost per byte¶
Instructions per hook call on a Cortex-M3 under QEMU (examples_mcu/mcu_qemu, bin qemu-bus-slave, counted with
examples_mcu/qemu_icount), each including the bench loop and call, about 10 instructions:
| Case | opt-level = 3 |
opt-level = "z" |
|---|---|---|
| I2C register map, master writes, per byte | 69 | 66 |
| I2C register map, master reads, per byte | 58 | 59 |
| I2C answer queue, master reads, per byte | 76 | 82 |
| I2C address + one byte + stop | 194 | 218 |
SPI register map read frame, miso + mosi per byte |
96 | 100 |
SPI answer queue frame, miso + mosi per byte |
85 | 93 |
| SPI CS asserted + one byte + CS released | 232 | 240 |
At 150 MHz (RP2350) 100 instructions are under a microsecond: an I2C byte at 1 MHz takes 9 µs, so the engine is far from the limit there; for SPI, a FIFO and the dummy bytes are what keep up with a fast clock, not the per-byte cost alone. Instructions are not cycles: measure on the target for the exact time.