Skip to content

add eeprom-write and eeprom-read for the HAT ID bus - #4

Merged
joeferner merged 1 commit into
mainfrom
i2c-id
Aug 30, 2026
Merged

joeferner merged 1 commit into
mainfrom
i2c-id

Conversation

@joeferner

Copy link
Copy Markdown
Owner

A board's identity lives in an EEPROM on ID_SD/ID_SC, and every existing way to write one assumes Linux on the target: eepflash.sh instantiates a bit-banged i2c-gpio bus and talks to it through /dev/i2c. A board that runs bare metal has none of that, and pulling the part to program it in a socket is not a workflow. The loader is already attached to such a board over serial and already reaches its hardware through rpi-hal, so it is where this belongs.

eeprom-write takes the .eep that eepmake produces and programs it; eeprom-read copies it back off. Wire commands 9 and 10, both shaped like the sd-* ones -- arguments, a status byte, then the existing CRC-checked chunk stream in whichever direction. Both take --address (0x50 by default, what the HAT specification assigns) and --offset. With no --length, eeprom-read reads the 12-byte HAT header first and takes the image length out of it, so a dump is the couple of hundred bytes of atoms rather than 32 KiB of mostly 0xff.

The device reads every page back after programming it, and this is not belt and braces. A write-protected part -- WP tied high, which on a board that puts write protect on a solder jumper is a state you will meet -- acknowledges every byte and stores none. Without the read-back this command would report a clean success and leave the EEPROM exactly as it was. That is error code 8, and it is deliberately distinct from 7 (nothing acknowledged the page write) and 10 (the page was written but the part never answered the read that checks it), because those three send you to different parts of the board.

The wait between writing a page and reading it back is a fixed delay, not the acknowledge polling the datasheet also offers. Polling was the first implementation and it failed on hardware in a way worth recording: exactly one 32-byte page landed and the rest of the image stayed erased. The poll is issued tens of microseconds after the page write's STOP, which is the same moment the part begins the cycle being polled about -- so it can answer about a cycle that has not started. Waiting out the family's 5ms tWR, rounded up to 6, has no such race, and the read-back that follows is the real evidence a page took, so the wait only has to be long enough rather than exact. The read is retried a few times for a part slower than its own datasheet.

Page size comes from the host because the device cannot know what part is fitted, and getting it wrong is not a clean failure: a page write that runs past the part's page boundary wraps to the start of that page rather than carrying, overwriting data already programmed. The default is 32, the page of a 24C32 -- the smallest part the specification allows -- which divides every larger part's page and is therefore always safe. A 24C256 takes --page-size 64 and programs in half the time.

send_chunked gains an ACK-attempt count. Every other command stores a chunk and acknowledges immediately; this one programs it a page at a time, so a 4 KiB chunk takes seconds before its ACK, and one timeout window had the host resending chunks the device was still working through. The per-chunk ACK is flow control here as everywhere else in this protocol, so the fix is to wait longer for it, not to send ahead.

Three protocol tests cover the new framing against the fake device: the image transfer, the verify failure naming write protect, and the header-driven read length.

Note the temporary [patch.crates-io] in firmware/Cargo.toml, which is the one thing in this repository that assumes anything about what sits beside it on disk. The eeprom-* commands need rpi-hal's I2c::<BSC0>::init_id -- BSC0 muxed to GPIO0/1 -- which no published rpi-hal has yet. Until the release carrying it is on crates.io the firmware builds only against a sibling checkout, and CI, which clones this repository alone, cannot build it at all. At that release: raise the version and delete the section. Nothing else depends on the path.

Verified against a Pi 2 carrying a HAT with a CAT24C256 at 0x50: an eepmake -v1 image written and reported verified, read back byte-for-byte, and round-tripped through eepdump with the vendor atom and GPIO map intact.

@joeferner
joeferner marked this pull request as ready for review August 30, 2026 14:25
A board's identity lives in an EEPROM on ID_SD/ID_SC, and every existing
way to write one assumes Linux on the target: `eepflash.sh` instantiates
a bit-banged i2c-gpio bus and talks to it through /dev/i2c. A board that
runs bare metal has none of that, and pulling the part to program it in
a socket is not a workflow. The loader is already attached to such a
board over serial and already reaches its hardware through rpi-hal, so
it is where this belongs.

`eeprom-write` takes the `.eep` that `eepmake` produces and programs it;
`eeprom-read` copies it back off. Wire commands 9 and 10, both shaped
like the `sd-*` ones -- arguments, a status byte, then the existing
CRC-checked chunk stream in whichever direction. Both take `--address`
(0x50 by default, what the HAT specification assigns) and `--offset`.
With no `--length`, `eeprom-read` reads the 12-byte HAT header first and
takes the image length out of it, so a dump is the couple of hundred
bytes of atoms rather than 32 KiB of mostly 0xff.

The device reads every page back after programming it, and this is not
belt and braces. A write-protected part -- `WP` tied high, which on a
board that puts write protect on a solder jumper is a state you will
meet -- acknowledges every byte and stores none. Without the read-back
this command would report a clean success and leave the EEPROM exactly
as it was. That is error code 8, and it is deliberately distinct from 7
(nothing acknowledged the page write) and 10 (the page was written but
the part never answered the read that checks it), because those three
send you to different parts of the board.

The wait between writing a page and reading it back is a fixed delay,
not the acknowledge polling the datasheet also offers. Polling was the
first implementation and it failed on hardware in a way worth recording:
exactly one 32-byte page landed and the rest of the image stayed erased.
The poll is issued tens of microseconds after the page write's STOP,
which is the same moment the part begins the cycle being polled about --
so it can answer about a cycle that has not started. Waiting out the
family's 5ms tWR, rounded up to 6, has no such race, and the read-back
that follows is the real evidence a page took, so the wait only has to
be long enough rather than exact. The read is retried a few times for a
part slower than its own datasheet.

Page size comes from the host because the device cannot know what part
is fitted, and getting it wrong is not a clean failure: a page write
that runs past the part's page boundary wraps to the start of that page
rather than carrying, overwriting data already programmed. The default
is 32, the page of a 24C32 -- the smallest part the specification allows
-- which divides every larger part's page and is therefore always safe.
A 24C256 takes `--page-size 64` and programs in half the time.

`send_chunked` gains an ACK-attempt count. Every other command stores a
chunk and acknowledges immediately; this one programs it a page at a
time, so a 4 KiB chunk takes seconds before its ACK, and one timeout
window had the host resending chunks the device was still working
through. The per-chunk ACK is flow control here as everywhere else in
this protocol, so the fix is to wait longer for it, not to send ahead.

Three protocol tests cover the new framing against the fake device: the
image transfer, the verify failure naming write protect, and the
header-driven read length.

The firmware's rpi-hal floor moves to 0.3.0, which is where
`I2c::<BSC0>::init_id` -- BSC0 muxed to GPIO0/1 -- first appears. There
is no way to reach that routing in an earlier version, so this is a real
floor rather than whatever happened to be current.

Verified against a Pi 2 carrying a HAT with a CAT24C256 at 0x50: an
`eepmake -v1` image written and reported verified, read back
byte-for-byte, and round-tripped through `eepdump` with the vendor atom
and GPIO map intact.
@joeferner
joeferner merged commit 9ac1b49 into main Aug 30, 2026
4 checks passed
@joeferner
joeferner deleted the i2c-id branch August 30, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant