Files
Jacob Dahl 183b3e38d5 fix(bootloader): scrub uncorrectable flash ECC errors on STM32H7 before boot (#27642)
* fix(bootloader): scrub uncorrectable flash ECC errors on STM32H7 before boot

A torn write to a flash row (power lost mid program/erase) can leave it
with inconsistent ECC. On H7 a CPU read of such a row raises an
uncorrectable double-bit ECC error -> bus fault, so the application hard
faults on every boot before printing anything, and only a mass erase
recovers it: reflashing does not, because neither the app nor the
bootloader image touches the parameter sector.

Scan the application and parameter flash with DMA right after clock_init
(a DMA read latches FLASH_SR DBECCERR instead of bus-faulting the CPU)
and erase any sector that holds an uncorrectable error. A corrupt
parameter sector then re-seeds to defaults on the next app boot; corrupt
app flash fails the image check and stays in the bootloader for reflash.
Mirrors ArduPilot's bootloader ECC scrub. H7-gated; the scan runs before
the interface DMA is brought up, so DMA1 stream 0 is uncontended.

* test(tools): add STM32H7 flash ECC fault injection tool

A bench helper that deterministically plants an uncorrectable (double-bit)
flash ECC error on an STM32H7, reproducing the torn-write parameter-store
brick. Used to validate the bootloader ECC scrub and the parameter re-seed
recovery on hardware: plant the fault, then confirm the board boots clean
instead of hard-faulting every boot.

brick_ecc.c is a freestanding stub that double-programs one flash word with
two different single-bit clears (the recipe that yields an inconsistent ECC);
brick_ecc.sh builds it, loads it into AXI SRAM over ST-Link, runs it, and
resets the board. README documents usage and how to retarget another board.

* fix(bootloader): require DMA transfer-complete for a clean ECC scan chunk

The stream self-disables on both transfer-complete and transfer-error.
A non-ECC bus error (unreachable scan buffer, rejected FIFO config)
previously read as a clean chunk, silently turning the scan into a
no-op. Require TCIF so an unread chunk takes the skip-the-scan
fallback instead of passing.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>

---------

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-06-12 19:21:01 -06:00

88 lines
2.6 KiB
C

/*
* brick_ecc.c - plant an uncorrectable flash ECC error (STM32H7)
*
* Plants an uncorrectable ECC error in the parameter flash word at ADDR,
* reproducing the torn-write brick: the firmware's find_entry() scan reads the
* word, takes an uncorrectable (double-bit) ECC error -> precise bus fault ->
* hard fault on every boot, recoverable only by a mass erase or the
* bootloader's ECC scrub. See README.md.
*
* Recipe (verified on H743, BFAR landed here in the sweep): program the flash
* word twice with no erase between, clearing a DIFFERENT single bit each time
* (0xFFFFFFFE then 0xFFFFFFFD). The stored data becomes 0xFFFFFFFC but the
* stored ECC is ecc(P1)&ecc(P2), inconsistent with it -> uncorrectable. (A
* complementary pair like 0x55/0xAA collapses to data 0 with valid ECC and
* does NOT fault - that is why the sweep was needed.)
*
* Runs from RAM with interrupts masked, no OS, driving the bank-2 flash
* controller directly. Defaults target the ARK_FPV parameter sector
* (STM32H743, bank 2, sector 7 @ 0x081E0000).
*/
#define REG(a) (*(volatile unsigned int *)(a))
#define FLASH_KEYR2 0x52002104u
#define FLASH_CR2 0x5200210Cu
#define FLASH_SR2 0x52002110u
#define FLASH_CCR2 0x52002114u
#define KEY1 0x45670123u
#define KEY2 0xCDEF89ABu
#define CR_LOCK (1u << 0)
#define CR_PG (1u << 1)
#define CR_SER (1u << 2)
#define CR_START (1u << 7)
#define CR_SNB(n) (((n) & 0x7u) << 8)
#define SR_BUSY ((1u << 0) | (1u << 2)) /* BSY | QW */
#define CCR_CLEAR_ALL 0x0FEF0000u
#define ADDR 0x081E0000u /* set to 0x081FD100 to match the original BFAR */
#define SNB 7u
#define P1 0xFFFFFFFEu /* clear bit 0 */
#define P2 0xFFFFFFFDu /* then clear bit 1 -> data 0xFFFFFFFC, bad ECC */
static void wait_idle(void)
{
while (REG(FLASH_SR2) & SR_BUSY) { }
}
/* program the 256-bit flash word at ADDR with value v (8 x 32-bit) */
static void program_word(unsigned v)
{
volatile unsigned int *w = (volatile unsigned int *)ADDR;
REG(FLASH_CR2) = CR_PG;
for (unsigned i = 0; i < 8; i++) { w[i] = v; }
wait_idle();
REG(FLASH_CR2) = 0;
}
void _start(void)
{
__asm volatile ("cpsid i");
/* unlock bank 2 and clear stale error flags */
REG(FLASH_KEYR2) = KEY1;
REG(FLASH_KEYR2) = KEY2;
REG(FLASH_CCR2) = CCR_CLEAR_ALL;
/* erase sector 7 for a known starting state */
REG(FLASH_CR2) = CR_SER | CR_SNB(SNB);
REG(FLASH_CR2) = CR_SER | CR_SNB(SNB) | CR_START;
wait_idle();
REG(FLASH_CR2) = 0;
/* double-program the word with two different single-bit clears */
program_word(P1);
program_word(P2);
/* lock and stop for the debugger */
REG(FLASH_CR2) = CR_LOCK;
__asm volatile ("bkpt #0");
for (;;) { }
}