type: proposal status: historical summary: Plan, now built, for the GD5F2GM7RE SPI-NAND loader on STM32U375: raw and ECC page reads, NAND power, shared SPI bus and work order.
Plan: an STM32U375 SPI-NAND Loader (GD5F2GM7RE_IMUTagNandv2)
Historical: built 2026-10-02. The loader as built is described in Loader Runtime Design, and the agreed decisions are in the decision record. This plan is kept for its step-by-step bench record.
Status: plan accepted, 2026-10-02. All five decisions below were agreed with the recommendations. Decision 5 moved blank detection from the loader to the host library. Decision 4's NAND power was corrected in step 2: the loader drives FLASH_PWR, as the firmware does. All five steps are built and checked (2026-10-02); see each step.
This plan covers reading an IMUTagNandBmp581's external flash (GigaDevice
GD5F2GM7RE SPI NAND, 256 MiB) over SWD without its firmware. The tag's identity
record already names the loader GD5F2GM7RE_IMUTagNandv2. It is the first
loader for a new MCU (STM32U375) and the first for a new memory type (SPI
NAND), so it is planned before it is built.
Background: - Loader Runtime Design: the contract and the runtime rules, all of which still apply. - Field Data Extraction: why NAND is read raw and through ECC. - Offline Log Reconstruction: how IMUTag pages map to physical blocks.
Facts are cited from the source. The datasheet used is
hardware/BoardDesigns/libraries/datasheets/DS_00819_GD5F2GM7RE_Rev1_3-3435814.pdf
in the sibling repository.
What the loader must read
- Raw pages including the spare area. 2048 data bytes plus 128 spare bytes (columns 0-2175) per page; 64 pages per 128 KiB block; 2048 physical blocks. The factory bad-block mark is spare byte 0 of page 0 (and 1) of a block. With on-die ECC enabled, spare bytes 0x840-0x87F hold ECC parity, readable but not writable.
- The ECC verdict per page, as the firmware sees it. The firmware reads
with ECC on and keeps a page only if ECC reports no error or corrected
(
families/IMUTag/src/datalog.c). The on-die algorithm is undocumented, so a raw page cannot be corrected on the host. Field Data Extraction therefore asks for each used page both raw and through ECC, with the ECC status (C0bits 5:4, and the extendedF0bits the firmware never reads). - Physical pages, not logical ones. Bad-block remapping is the host's job:
gd5fLogicalBlockMap(uint16[2048]at0x080FF000) and the 16-byte checkpoints at0x08021280are both in internal flash;tag-capturealready captures them, and the identity record locates them.
Decisions (agreed)
Moved to the decision record.
Work, in order
Each step ends with a check on the bench IMUTagNandBmp581. Flashes of tag
firmware, if any, go in its flash log
(captures/2026-10-02-imutag-nand-bmp581/flash-log.md).
- U3 loader build.
loaders/common/make-u375.mk, mirroringmake.mk: thestartup_stm32u3xx.mkinclude paths only, the STM32U3xxplatform.mkwith onlyhal_pal_lld.ccompiled, the os-less OSAL,MCU = cortex-m33andUSE_FPU = no.cfg/stm32u3/with PAL-onlyhalconf.hand anmcuconf.hwithSTM32_NO_INIT TRUE.STM32U375-loader.ld:StorageInfoat 0, the image in SRAM1 from0x20000004.
The first 64 bytes of SRAM1 are the firmware's monitor mailbox. The loader overwrites them only while the core is halted, and the firmware clears them at boot; confirm that.
Check: the RV3028 probe, ported first as RV3028_IMUTagNandv2,
returns this tag's factory EEOffset. Read-only, as before.
Done 2026-10-02.
- Built: common/make-u375.mk, common/cfg/stm32u3/,
common/STM32U375-loader.ld, common/src/loader_clock_u3.c and
RV3028_IMUTagNandv2/.
- The board names its I2C pins LINE_SDA (PB7) and LINE_SCL (PB6),
aliased by the target's project.mk.
- The image references only RCC, GPIOB 6/7, DWT and DEMCR.
- On the bench IMUTagNandBmp581 it read all 64 registers:
- CLKOUT C0;
- EEOffset 00/10 = 0 steps, agreeing with the stored session facts
and tag-info;
- the time as set by the firmware.
One departure from decision 3.
- The reset clock could not be established from documentation. ChibiOS
assumes MSIRC1/4 (6 MHz); the captured reset registers read MSIRC1/2
(12 MHz).
- So loader_clock_u3.c pins MSIS at MSIRC1/2 = 12 MHz through
RCC_ICSCR1 (MSIRGSEL = 1). That is a value the part may already be
running at, and it stays within voltage range 2 at the reset's 1 wait
state.
- Still untouched: PWR, the booster, FLASH_ACR and the backup domain.
2. U3 SPI and delay.
- loader_spi_u3.c, a bounded polled master following
common/core/src/spi_bus_polled.inc:57-68, 187-207: CFG1 8-bit, and
MBR chosen for the reset clock; CFG2 MASTER|SSOE; CR1
MASRX|SPE; then per byte CSTART, TXP/TXDR, RXP/RXDR,
CSUSP.
- DWT delays calibrated for the reset clock.
Check: READ_ID (9F 00) returns C8 82, and GET_FEATURE of A0,
B0 and C0 returns plausible values.
3. GD5F part driver, read-only.
- common/inc/gd5f_commands.h: move the opcodes, feature addresses and
timing from gd5f.c:19-64 into a dependency-free header that the
firmware driver also includes.
- Prove the firmware unchanged by comparing the .list of every target
that builds gd5f.c, at the same commit, as the README requires.
- common/src/gd5f_loader.c: probe without reset, wake, page read to
cache (13) with an OIP poll bounded at tRD_ECC max (120 µs) times a
margin, read from cache (03) of 2176 bytes, and status C0/F0.
- Writes are limited to B0 (decision 2). There is no program or erase
code in this image at all.
Check: page 0 of block 0 read raw matches the bad-block mark convention,
and a used page read through ECC reports ECC OK.
4. Serve() for NAND. loader_service.h version 2 adds one command:
READ_PAGE(page, mode). It returns 2176 bytes, plus the C0/F0 status
in detail[]. Page selection is the host's (decision 5).
The AT25 loaders keep version 1, and the host accepts either version.
Check: the host's blank scan, on a tag with a short run, finds exactly
the blocks the checkpoints name, plus any factory-marked blocks. A tag-xflash NAND mode
then reads those blocks raw and through ECC, and the ECC-mode data bytes
match a normal tag-dwnld of the same run.
5. Capture integration (swd-recovery step 5): external flash in
tag-capture, with the loader named by the identity record. For NAND,
write one file of raw pages, one of ECC pages and a per-page status table.
Check: the offline shim (next-release-todo D1) rebuilds the run's SQLite
file from the capture alone.
Step 2 done 2026-10-02.
- Built: common/src/loader_spi_u3.c; common/src/gd5f_loader.c,
read-only, with no reset and no feature writes; and the
GD5F2GM7RE_IMUTagNandv2 target.
- The GD5F opcodes moved to tags/common/storage/inc/gd5f_commands.h. The
.list of IMUTagNandBmp581 and IMUTagNand is identical before and after.
- NandInfo read ID C8 82, A0 00, B0 10 (ECC on), C0 00 and
F0 00.
- Through the ST entry points and Serve(), four reads of the first 64
pages, 5 s apart, were byte-identical. They held 52 non-blank pages,
matching the 52 pages a monitor download of the same run returned.
- The core clock was checked against the host: a 2000 ms delay took
2.018 s.
- Every page read is now followed by a status read. A reply of all ones
means the part was not driving MISO, so the page is retried and never
returned silently. That is cheap insurance against the failure that
the missing PA8 drive caused.
Steps 3 and 4 done 2026-10-02.
- gd5f_loader.c gained loaderFlashReadPage(): 2176 bytes, raw or
through ECC, with C0h and F0h after the read, and the no-reply check.
It also gained loaderFlashRestore().
- B0h writes go only through gd5fSetEcc(), which changes ECC_EN alone
and holds the reserved bits low. It refuses if OTP_EN or OTP_PRT was
found set: OTP_PRT is non-volatile.
- Serve() restores B0h as found before it returns.
- loader_service.h version 2 adds LOADER_CMD_READ_PAGE and widens
detail[]: page bytes, and A0/B0/C0/F0 as found. All loaders are
version 2; the host accepts 1 or 2.
- ExternalFlash::ReadPage() is the host side.
- tag-xflash nand -o DIR applies decision 5: page 0 of each block is
read raw, a blank block is skipped, and every other block is read raw
and through ECC. It writes raw.bin, ecc.bin, pages.csv and
summary.txt.
Checked on the bench IMUTagNandBmp581:
- The whole part (2048 blocks) took 74 s: 1 block read (64 pages, every
ECC verdict ok), 2047 blank, none factory-marked on page 0.
- The ECC-mode data was byte-identical to the step-2 linear dump.
- Raw data equalled the ECC data, so there were no bit errors, and the
raw spare showed the on-die parity at 0x840.
- B0h read 10 after the session, as found.
One limit: the firmware looks for the factory mark on pages 0 and 1, but the skip rule reads only page 0. A block marked only on page 1 is skipped unreported. It holds no data, so nothing is lost.
Step 5 done 2026-10-02.
- recovery/identityrecord.* parses the identity record from the
captured internal flash, at the new McuMap::identity_offset: 0x1A0 on
L432, 0x240 on U375.
- recovery/externalcapture.* holds the shared capture (linear or
paged), used by both tag-capture and tag-xflash nand.
- tag-capture now ends with the external flash, through the loader the
record names, searched for under --loader-dir, $TAG_LOADER_DIR and
build-host/embedded/loaders; --loader overrides it. It checks the
JEDEC ID against the record (manufacturer and first device byte).
- The manifest gains identity and external_flash sections: loader and
its SHA-256, Serve version, registers as found, selection rule, block
counts, and every file's SHA-256.
On the bench IMUTagNandBmp581:
- One tag-capture found GD5F2GM7RE_IMUTagNandv2 by name and scanned
the NAND in 73.7 s: 1 block read, 2047 blank, no uncorrectable pages.
- external_ecc.bin was byte-identical to the step-4 tag-xflash nand
run.
- On this breakout the registers "as found" are the NAND's power-on
values (A0 38, F0 08). The attach's reset leaves PA8 undriven, so
the switched NAND loses power until the loader drives PA8 again. On the
final tag, which has no switch, they are the real state.
The linear path, through the AT25 loaders, is the same code as
tag-xflash dump but has not yet been run through tag-capture on a
PresTag.
Risks
- The reset clock. If MSIS after reset is not 6 MHz on this part, delays
run fast or slow. Read
RCC_ICSCR1andRCC_CFGR1in step 1, and size every budget so that a factor of 4 either way still works. - The capture exit aborts a running IMUTag (next-release-todo A6). This plan does not change that. Stop a run before capturing on the bench.
- The firmware's ECC reading has a gap.
ECCS=11b("8 bits corrected") is treated as OK only by accident (gd5f.c:341-345). It is worth fixing in the firmware separately; the loader reports the raw bits.