Skip to content

rp23xx: add an OTP driver on the efuse interface#19532

Merged
xiaoxiang781216 merged 1 commit into
apache:masterfrom
casaroli:rp2350-otp
Jul 27, 2026
Merged

rp23xx: add an OTP driver on the efuse interface#19532
xiaoxiang781216 merged 1 commit into
apache:masterfrom
casaroli:rp2350-otp

Conversation

@casaroli

@casaroli casaroli commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

The rp2350 holds 4096 rows of 24 bit one-time-programmable memory, which the
port did not expose at all, although hardware/rp23xx_otp.h and
hardware/rp23xx_otp_data.h have been in tree unused. This adds a driver on
the NuttX efuse interface, registered by the common board bringup as
/dev/efuse.

Addressing. The driver uses the ECC interpretation of a row, in which 16
bits carry data and the remaining 8 carry a Hamming code. The OTP therefore
appears as a flat space of 4096 * 16 bits for the efuse field descriptors to
index: a descriptor at bit offset N refers to bit N % 16 of row N / 16.
That lines up with the row numbers already listed in rp23xx_otp_data.h, so a
field can be described directly from the constants there.

Reads come from the chip's ECC-translated window and have no side effects;
single bit errors are corrected in hardware. Rows are locked in pages of 64,
and reading a page that is locked against reads raises a bus fault, so the
driver checks the lock first and reports EPERM instead of faulting.

Programming requires the separate RP23XX_OTP_WRITE option, which defaults
to off; with it off a write returns EPERM and no programming code is compiled
in at all. When enabled, a row is programmed as a whole through the bootrom
OTP_ACCESS entry point, because the ECC bits cover the whole row. For the
same reason a row that already holds data cannot be modified, and the driver
rejects such a write rather than leaving it to corrupt the row's ECC.

Impact

  • New feature, opt-in via CONFIG_RP23XX_OTP (default n, selects EFUSE). No
    change to existing behavior when disabled.
  • Arch: arm (rp23xx / RP2350). Boards: rp23xx common bringup registers
    /dev/efuse when the option is enabled.
  • No new external dependencies, no ABI/API changes beyond the new Kconfig
    symbols and the new device node.
  • Documentation: an OTP section and a peripheral-table row were added to
    Documentation/platforms/arm/rp23xx/index.rst.

Testing

Hardware: RP2350 board (Pimoroni Pico Plus 2), raspberrypi-pico-2:nsh
plus RP23XX_OTP. Programmed over SWD with a Raspberry Pi Debug Probe
(probe-rs); console on UART0.

Read path, on hardware

Exercised with a small read-only application that issues
EFUSEIOC_READ_FIELD:

  • /dev/efuse registers at boot.
  • Multi-row field: the 64 bit chip identifier in rows 0-3 reads back as
    49a98f93d781c687, which is exactly what the four individual row reads
    compose to (0xc687, 0xd781, 0x8f93, 0x49a9). The 64 bits of the
    random identifier in rows 4-7 likewise agree with their individual rows.
  • Sub-row field: bits 3:0 of row 0 read 0x7, the low nibble of the
    0xc687 held in that row, confirming the bit offset and masking.
  • Range checking: a descriptor past the end of the array is rejected rather
    than read.
  • Independent cross-check: the same eight rows were read over SWD from the
    raw OTP window (0x40134000, which exposes all 24 bits). All eight match
    the driver's ECC-decoded values, and the upper bytes hold the Hamming codes,
    confirming the ECC interpretation is the right one.

Programming path: verified in emulation, never run on silicon

The write path is not run on hardware, because programming is irreversible
and would permanently consume fuses on the test board. It is instead exercised
against a functional RP2350 OTP model in Renode (the array, both read windows,
the SW_LOCK registers and OR-only programming, with the bootrom otp_access()
entry point intercepted). 19 checks pass, covering: programming a blank row and
reading it back, re-writing the same value as a no-op, a different value refused
with EROFS, sub-row bit placement, a 32-bit field split across two rows,
out-of-range rejection, and the factory rows left untouched.

That found a real bug, now fixed: the already-programmed guard tested only for
data outside the field's bits, so a field covering a whole row bypassed it and
the driver would have asked the bootrom to program over a row whose ECC was
already fixed. The guard is now "any non-blank row is off limits".

Emulation validates this driver's logic, not the bootrom's programming sequence
on real silicon. What was also verified:

  • It compiles: a build with CONFIG_RP23XX_OTP_WRITE=y links cleanly.
  • With the option off (the default, and the configuration flashed for the test
    above) the programming code is genuinely absent, not merely unused: the OTP
    object's only external references are efuse_register and memset, the
    linked image contains no reference to the bootrom OTP entry point, and
    rp23xx_otp_write_field compiles down to mov r0, #-1; bx lr.
  • EFUSEIOC_WRITE_FIELD returns EPERM on the test board, and the row the
    test aimed at was afterwards confirmed still blank by reading it over SWD.

tools/checkpatch.sh -f (nxstyle) and -g (patch + commit message) pass.

@casaroli
casaroli requested a review from jerpelea as a code owner July 25, 2026 09:02
@github-actions github-actions Bot added Area: Documentation Improvements or additions to documentation Arch: arm Issues related to ARM (32-bit) architecture Size: L The size of the change in this PR is large Board: arm labels Jul 25, 2026
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

@casaroli

Copy link
Copy Markdown
Contributor Author

Updated the branch: the programming path is now exercised under emulation, and doing so found and fixed a real bug in it.

What was wrong. The guard that refuses to re-program an already-programmed row checked only whether the row held data outside the bits the field covers:

if ((current & ~touched) != 0) { return -EROFS; }

For a field covering the whole row, touched is 0xffff, so that term is always zero and the guard was bypassed. Writing 0x5a5a over a row already holding 0xa5a5 therefore fell through and asked the bootrom to program 0xffff into a row whose ECC was already fixed, leaving the driver relying on the bootrom to refuse. The check is now simply "any non-blank row is off limits", which is what the ECC constraint actually implies:

if ((uint16_t)(current | value) == current) { continue; }  /* already programmed */
if (current != 0) { return -EROFS; }                       /* ECC cannot be recomputed */

How it was found. I added a functional RP2350 OTP model to my Renode setup (the 4096 x 24 bit array, the ECC window at 0x40130000, the raw window at 0x40134000, the SW_LOCK registers, and OR-only programming semantics), and intercepted the bootrom otp_access() entry point so programming reaches the model. That makes the destructive path repeatable without consuming fuses on real silicon.

19 checks now pass in emulation, covering: a blank row reads zero; programming a whole row and reading it back; re-writing the same value being a no-op rather than an error; a different value being refused with EROFS (the case that failed before); a sub-row field landing at the right bit positions; a 32-bit field split across two rows and read back whole; out-of-range writes rejected; and the factory identity rows left untouched throughout.

The read path results in the PR description are unchanged and still come from real hardware — the fix touches only rp23xx_otp_write_field, which is compiled out of the hardware build. Programming has still never been run on silicon, so the caveat in the description stands: emulation validates this driver's logic, not the bootrom's programming sequence.

acassis
acassis previously approved these changes Jul 25, 2026
The rp2350 holds 4096 rows of 24 bit one-time-programmable memory, which
the port did not expose at all.  This adds a driver on the NuttX efuse
interface, registered by the common board bringup as /dev/efuse.

The driver uses the ECC interpretation of a row, in which 16 bits carry
data and the remaining 8 carry a Hamming code, so the OTP appears as a
flat space of 4096 * 16 bits for the efuse field descriptors to index: a
descriptor at bit offset N refers to bit N % 16 of row N / 16.  That
matches the row numbers already listed in hardware/rp23xx_otp_data.h.

Reads come from the chip's ECC-translated window and have no side
effects.  Rows are locked in pages of 64; a page locked against reads
would raise a bus fault, so the lock is checked first and reported as an
error instead.

Programming needs the separate RP23XX_OTP_WRITE option, which defaults to
off; without it a write returns EPERM and no programming code is built at
all.  When enabled, a row is programmed as a whole through the bootrom,
since the ECC bits cover the whole row.  For the same reason a row that
already holds data cannot be modified, and such a write is rejected
rather than left to corrupt the row's ECC.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@xiaoxiang781216
xiaoxiang781216 merged commit 7fc26fd into apache:master Jul 27, 2026
37 of 39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Area: Documentation Improvements or additions to documentation Board: arm Size: L The size of the change in this PR is large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants