Raspberry Pi I²C

I was tasked recently with writing a firmware emulation of the popular AT24-series of I²C EEPROM devices, and then plumbing that through to userspace via the at24 module in the Linux kernel. The firmware was written in Rust and ended up being relatively straightforward to develop. The integration, however, proved to be unexpectedly complicated. Here’s the setup:

The firmware runs on a Cortex-M4-based microprocessor. This connects to a Raspberry Pi via one of the I²C buses, running in standard mode. The device tree is configured to take advantage of the full width of the EEPROM’s write FIFO. The at24 module is running in the kernel.

At this point, reads work perfectly fine, but writes are a little flaky. Doing a single write transaction is fine, but following it with another read or write causes problems (e.g. echo -n 01 | dd of=/sys/bus/nvmem/devices/1-00500/nvmem bs=1, which writes two bytes, each in a separate transaction).

This is a capture of that scenario (done with a Saleae Logic Pro 8):

Screenshot of a logic probe showing the target (microprocessor) stretching the clock signal, but the controller (Raspberry Pi) proceeding anyway after 64 microseconds.

Here’s how the I²C traffic was decoded:

write to 0x50 ack data: 0x00 0x00 0x30
write to 0x50 ack data: 0xA0 0x00 0x01 0x31

The target (microprocessor) can be seen stretching the I²C clock while it commits the previous transaction to flash (this takes about 80 milliseconds on the actual system, but I hard-coded it shorter to improve the clarity of the screenshot). Midway through this stretch, the controller (Raspberry Pi) begins to transmit data again! That’s not right, and it causes an extra 0xA0 to be read. As it turns out, the Raspberry Pi will assume a target has stalled if it stretches the clock for too long. By default, this timeout happens after 64 clock cycles, which is just 640 microseconds in standard mode. There was a change back in 2019, which increased this timeout to 35 milliseconds, as recommended by SMBus. But this is still not long enough for the flash erase cycle.

To get this working, I wrote a patch that allows the timeout to be configured using the clk_tout_ms module parameter. Adding at24.clk_tout_ms=0 to the kernel’s command line disables the timeout behavior:

Capture:

Screenshot of a logic probe showing the target (microprocessor) stretching the clock signal, and the controller (Raspberry Pi) patiently waiting.

Traffic:

write to 0x50 ack data: 0x00 0x00 0x30
write to 0x50 ack data: 0x00 0x01 0x31

Neat.