Step 46 of 56
Block devices and wear levelling
One abstraction the filesystems talk to, thin adapters underneath it, and a filter in the middle that stops a hot metadata block from killing one sector of your flash.
A vtable, not a tagged type
type Sector is array (0 .. 511) of Interfaces.Unsigned_8;
type Sector_Index is new Interfaces.Unsigned_64;
type Read_Proc is access procedure (Ctx : System.Address; LBA : Sector_Index; Data : out Sector);
type Write_Proc is access procedure (Ctx : System.Address; LBA : Sector_Index; Data : Sector);
type Count_Func is access function (Ctx : System.Address) return Sector_Index;
ESP32S3.Block_Dev is a record of access-to-subprogram plus an
opaque context — mirroring lwext4's ext4_blockdev vtable.
No tagging, no finalization, swappable at run time. That keeps
it usable from anywhere and makes a device something you can substitute in a
test.
Behind it sit thin adapters: SD_SPI_Source and
SDMMC_Source for SD cards,
W25Q_Source for SPI NOR flash, and a
file-backed device in the host test harness — which is what lets the whole
filesystem stack be developed and tested on a PC.
The error model is Ada's: the primitive Read/Write
raise Ada.IO_Exceptions.Device_Error on a hardware
failure, with the adapters converting the SD driver's Status enum
into that raise. So a media error propagates rather than being quietly returned
and ignored.
The wear-levelling filter
Flash wears out per erase block. A filesystem writes some blocks far more often than others — an ext4 metadata block, say — so without intervention one physical sector dies long before the rest of the chip.
Block_Dev.WL is a Block_Dev over a Block_Dev: it
takes the raw medium and presents a smaller logical device whose sectors are
remapped so that, over time, every logical 4 KB block visits every physical
4 KB block. Because it is a plain filter it carries no flash-specific code
and runs unchanged on the host, where it is brute-force tested.
-- a typical stack, bottom to top
W25Q flash -> Block_Dev.W25Q_Source -> Block_Dev.WL -> Ext4
O(1) state: just a move counter — there is
no per-block map to keep in RAM or rebuild at mount. A move erases the
destination block in one shot through the lower device's optional
Erase_Sectors before copying, so each move costs exactly one
erase.
This is dynamic wear levelling only. It bounds how unevenly write activity wears the chip, but it does not actively relocate cold, never-written blocks — so data you write once and never touch keeps its physical block out of rotation. That is sufficient for a 32 MB part; it is not the same guarantee as static wear levelling.
Config blocks are rewritten once per move, which is their wear cost. Raising
Update_Rate trades levelling aggressiveness for less move and config
overhead.