Skip to content

0016. Field extraction: the first external-flash loader settles the loader runtime

Date: 2026-09-30

Recorded when AT25XE_PresTagv3 was built, the first SRAM-resident loader for reading a returned tag's external flash without touching internal flash. Cut verbatim from Field Data Extraction, "What the first loader settled".

Context

Building AT25XE_PresTagv3 answered the questions this section used to leave open, and overturned three of its expectations. The detail is in Loader Runtime Design.

Decision

The HAL route works, without interrupts. The loader uses ChibiOS's register headers, the committed board.h and PAL, with the os-less OSAL and osalconf.h in place of chconf.h. It needs no vector table and no ISR path, because the PresTag family already runs SPI as polled register access (HAL_USE_SPI FALSE). ST's own loaders disable interrupts in every entry point, and so does this one.

Moving the drivers to the OSAL would not have made them loader-ready. This document expected the 18 chThdSleep* call sites to be the only obstacle. But the os-less osalThreadSleep waits on a SysTick-driven virtual timer, which never advances with interrupts off, and osalSysPolledDelayX is an empty stub. The dependency web is also wider than listed: storage_spi.h reaches ch.h and writes idlePowerMode, spi_bus.c waits on a semaphore, and at25xe.c calls stopMilliseconds.

The firmware driver is unsuitable for forensic reads in any case. Its wake hook clears block protection with a Write Status Register whenever the protect bits are set, which mutates the part, and the bits are evidence. The loader has its own small driver whose probe never writes, and it shares the command set header with the firmware. Moving the AT25XE constants there left all seven AT25XE firmware targets byte-identical.

The calling convention, from -vb 3 traces: the programmer leaves interrupts as the tag had them (the loader disables them), sets a BKPT return trap at 0x20000000, MSP just past the image (about 1 KB of stack), the transfer buffer after that, and Init before every operation. Nothing survives between calls that the loader relies on. The SRAM cost is about 1.5 KB for the read-only image and 2.3 KB for the RW image.

StorageInfo must sit at address 0 in its own segment. A loader with its descriptor in SRAM is ignored without any message, and reads then silently bypass it. This was not in any of the material this document drew on.

The clock is HSI16, set by hand. ChibiOS's stm32_clock_init() can reset the whole backup domain -- RTC and pState -- and must never be called from a loader.

Bring-up from cold on PresTagv3 needs no power sequencing: the flash is on the main supply.

board.h is committed for PresTagv3, so the loader builds from a checkout without fmpp, as the reproducibility work intended.

Provenance is handled as proposed: loaders are built by distributed_firmware, installed beside the firmware, carry a build manifest, and put the variant and commit in the device name CubeProgrammer displays.

Alternatives considered

ST's External Memory Manager was considered and does not fit. Its custom-driver configuration is XSPI vocabulary -- dummy cycles, single/dual/quad modes, DQS, instruction and address widths -- while every external flash in this tree hangs off SPI1 (TAG_SPI1_DEVICE_DEFAULTS in spi_bus.h). Its parameter list is still a useful checklist of what a loader's Init must establish -- JEDEC ID, capacity, reset method, read opcode, dummy cycles, chip-select timing -- and if a future board puts flash on OCTOSPI it becomes worth revisiting, since a generated loader would beat a written one. Note that ST's generated loaders are read/write by design, so the read-only stubbing above would have to be applied deliberately.

Consequences

STM32CubeProgrammer is a poor driver for this. Its contract is undocumented, it rejects a malformed loader silently, and its sector-number erase is ambiguous between internal and external memory when a loader is loaded. It also knows nothing of capture-first or external-before-internal ordering. A host tool on tagcore, using the same loader images, is the next step; see Where to start.