esp32_flash
Raw SPI flash access, driving SPI1 directly.
This is what lib/kvstore needs underneath it, and it is the one
peripheral on this chip you cannot drive casually: the code is executing
out of the thing being written.
Not through the ROM
Section titled “Not through the ROM”The obvious route is the ROM’s own flash driver, and it is a dead end.
Espressif’s linker script comments out esp_rom_spiflash_wait_idle and
esp_rom_spiflash_unlock with the note “always using patched versions of
these functions” – unlock’s address is not even recorded, written
0x400????? – and spi_flash/esp32/spi_flash_rom_patch.c exists to
replace them. esp_rom_spiflash_read waits for the chip to go idle before
returning, so a broken wait_idle is a read that never returns, which is
exactly what this driver did for a week. Zephyr’s flash_esp32.c and
Arduino both reach flash through esp-idf’s esp_flash layer rather than
calling the ROM, for the same reason: there is no supported way to call it
directly.
So spi1 sends the commands itself. See its module docs for the part
that is genuinely surprising – this chip needs two different transaction
conventions, and using the wrong one for a read is silent rather than
loud.
Why every one of these functions is in IRAM
Section titled “Why every one of these functions is in IRAM”Instructions come from flash through the cache. Programming the flash means
taking the SPI1 controller away from the cache, so for the duration of the
operation a cache miss has nowhere to go and the CPU stops — permanently,
with no fault. Everything on the path therefore has to already be in RAM
before the cache goes away: this crate’s functions and anything they call,
constant data included — an array literal lives in .rodata, which is
flash, and reading it here is fatal in exactly the way calling into flash
is, while looking nothing like a call.
Hence .iram1.flash on every one of them — and #[inline(never)]
beside it. A link_section says where a function body goes; it says
nothing about a copy the optimiser folded into a caller. The first version
of this file had the attribute and no inline(never), every function was
inlined into .text, and the board wedged the moment the cache went off.
The give-away was that the ELF contained no symbols for this crate at all.
The other core
Section titled “The other core”It is executing from flash too, and disabling the cache stops it just as
dead. So it has to be got out of the way, and it is asked rather than
frozen — see the note above [park_this_core]. Core 0 raises
FROM_CPU_2, core 1’s handler turns off its own cache, says it is parked
and spins until released.
This used to be a hardware stall, and the argument for that being safe was
that nothing ran between the stall and the release, so core 1 could be
frozen holding a lock and nobody on this side would ask for it. #69 made
IRAM-safe interrupts run in exactly that window, which retired the
argument. esp-idf
(spi_flash_disable_interrupts_caches_and_other_cpu) and NuttX
(esp32_spiflash_opstart) both ask, and neither stalls.
The stall survives as a bounded fallback: if core 1 does not answer
within PARK_SPINS, the old path runs and [PARK_FELL_BACK] records it.
A board whose park signal was never routed then still writes flash, badly
rather than not at all.
Structs
Section titled “Structs”ChipInfo
Section titled “ChipInfo”struct ChipInfo { pub device_id: u32, pub chip_size: u32, pub block_size: u32, pub sector_size: u32, pub page_size: u32, pub status_mask: u32,}What the ROM thinks the flash is.
Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq
FlashRegion
Section titled “FlashRegion”struct FlashRegion { /* private fields */ }A region of flash, owned exclusively.
Offsets are relative to base, so a caller cannot reach past the partition
it was given by arithmetic alone.
Implements: NorFlash
FlashError
Section titled “FlashError”enum FlashError { Timeout, OutOfRange, CacheBusy, Misaligned, UnknownChip,}Why a flash operation failed.
Implements: Clone, Copy, Debug, Eq, From, PartialEq, StructuralPartialEq
Functions
Section titled “Functions”jedec_id
Section titled “jedec_id”unsafe fn jedec_id() -> Result<u32, FlashError>The chip’s JEDEC id, straight from the chip: manufacturer in bits [7:0],
memory type in [15:8], capacity in [23:16].
This is what spi1::unlock gates on, so it is the number to look at when
a board reports UnknownChip — and the reason it exists separately from
[ChipInfo::read], which returns the ROM’s cached view in a different
byte order ((mfr << 16) | (type << 8) | capacity). Two representations of
one fact is exactly the sort of thing to read off the hardware rather than
derive.
Safety
Section titled “Safety”Runs a flash transaction with the cache disabled, so the same rules as any other operation in this crate apply.
References: FlashError
park_this_core
Section titled “park_this_core”unsafe fn park_this_core()The handler core 1 runs when core 0 asks it to park. IRAM throughout.
Register it on the second core with interrupt::connect_iram_safe against
crosscore::Signal::FromCpu2, routed in the APP table. The kernel does
that in join_scheduler; this crate cannot, being Layer 1.
Acknowledging comes first and matters: the from-CPU signal is level-triggered, so a handler that spins without clearing it re-enters the moment it returns.
Safety
Section titled “Safety”Trap context on the APP CPU only. Disables that core’s cache, so everything from here until it is restored must be in IRAM — which is why this does so little.
set_interrupt_hooks
Section titled “set_interrupt_hooks”unsafe fn set_interrupt_hooks(mask: fn(..), restore: fn(..))Tell the driver how to mask interrupts that cannot survive the cache going away, and how to put them back.
Call once, from the kernel, before any flash operation. Without it flash still works and simply masks everything for the duration.
Safety
Section titled “Safety”mask must return a value restore accepts, and neither may touch flash
— they are called with the cache on, but the second is called immediately
before it comes back, so keeping both in IRAM is the safe habit.
Constants
Section titled “Constants”ROM_SPIFLASH_CHIP
Section titled “ROM_SPIFLASH_CHIP”const ROM_SPIFLASH_CHIP: u32;The ROM’s description of the flash chip, at 0x3FFAE270.
esp_rom_spiflash_chip_t: device id, chip size, block size, sector size,
page size, status mask. Every ROM routine reads its geometry and its read
command from here, so if it is not populated they poll for a status that
never arrives.
SECTOR_SIZE
Section titled “SECTOR_SIZE”const SECTOR_SIZE: u32;Erase granularity, and the alignment every erase must respect.