Skip to content

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.

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.

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.

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

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

enum FlashError {
Timeout,
OutOfRange,
CacheBusy,
Misaligned,
UnknownChip,
}

Why a flash operation failed.

Implements: Clone, Copy, Debug, Eq, From, PartialEq, StructuralPartialEq

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.

Runs a flash transaction with the cache disabled, so the same rules as any other operation in this crate apply.

References: FlashError

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.

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.

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.

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.

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.

const SECTOR_SIZE: u32;

Erase granularity, and the alignment every erase must respect.