From 107a40a7ffcc843a0abd445a2e050fb9bc35bd28 Mon Sep 17 00:00:00 2001 From: richardclli Date: Mon, 1 Jun 2026 08:39:57 +0800 Subject: [PATCH 01/11] fix(stm32): disable interrupts during flash erase/program to prevent hangs on F4 --- radio/src/targets/common/arm/stm32/flash_driver.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index cc41ad11ed9..6f11c678e32 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -145,12 +145,14 @@ static int stm32_flash_erase_sector(uint32_t address) int ret = 0; uint32_t sector_errors = 0; + __disable_irq(); stm32_flash_unlock(); if (HAL_FLASHEx_Erase(&eraseInit, §or_errors) != HAL_OK) { ret = -1; } stm32_flash_lock(); + __enable_irq(); return ret; } @@ -174,6 +176,7 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) uint32_t end_addr = address + len; int ret = 0; + __disable_irq(); stm32_flash_unlock(); while (address < end_addr) { if (_FLASH_PROGRAM(address, p_data) != HAL_OK) { @@ -186,6 +189,7 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) } stm32_flash_lock(); + __enable_irq(); return ret; } From 8dcba4dee247bcf289c4420005fdd81b99c5cc00 Mon Sep 17 00:00:00 2001 From: richardclli Date: Thu, 4 Jun 2026 19:43:25 +0800 Subject: [PATCH 02/11] fix(stm32): replace broken HAL timeouts with DWT cycle counter during flash erase/program Replace HAL_WaitForLastOperation (dead when IRQs off) with DWT cycle counter for real timeout. Re-enable IRQs before cache flush in erase to prevent deadlock from FLASH_FlushCaches running with I-cache temporarily disabled and IRQs masked. Add DSB barriers for pipeline synchronization. --- .../targets/common/arm/stm32/flash_driver.cpp | 130 +++++++++++++++++- 1 file changed, 124 insertions(+), 6 deletions(-) diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index 6f11c678e32..482f6f97df9 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -85,6 +85,55 @@ static uint32_t stm32_flash_get_sector_size(uint32_t sector) return 128 * 1024; } +#define FLASH_TIMEOUT_MS 15000 + +static bool flash_drv_wait_last_op() +{ + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + + uint32_t start = DWT->CYCCNT; + uint32_t timeout_cycles = + (uint32_t)(FLASH_TIMEOUT_MS * (SystemCoreClock / 1000UL)); + + while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)) { + if ((DWT->CYCCNT - start) > timeout_cycles) { + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR | + FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | + FLASH_FLAG_PGSERR); + return false; + } + } + + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP); + + if (__HAL_FLASH_GET_FLAG(FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | + FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR)) { + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | + FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR | + FLASH_FLAG_EOP); + return false; + } + + return true; +} + +static void flash_drv_flush_caches() +{ + if (FLASH->ACR & FLASH_ACR_ICEN) { + FLASH->ACR &= ~FLASH_ACR_ICEN; + FLASH->ACR |= FLASH_ACR_ICRST; + FLASH->ACR &= ~FLASH_ACR_ICRST; + FLASH->ACR |= FLASH_ACR_ICEN; + } + if (FLASH->ACR & FLASH_ACR_DCEN) { + FLASH->ACR &= ~FLASH_ACR_DCEN; + FLASH->ACR |= FLASH_ACR_DCRST; + FLASH->ACR &= ~FLASH_ACR_DCRST; + FLASH->ACR |= FLASH_ACR_DCEN; + } +} + #elif defined(STM32H7) || defined(STM32H7RS) static uint32_t stm32_flash_get_sector(uint32_t address) @@ -129,6 +178,40 @@ static inline void stm32_flash_lock() { HAL_FLASH_Lock(); } static int stm32_flash_erase_sector(uint32_t address) { + int ret = 0; + +#if defined(STM32F2) || defined(STM32F4) + + uint32_t sector = stm32_flash_get_sector(address); + + __disable_irq(); + __DSB(); + + stm32_flash_unlock(); + + if (sector > 11) sector += 4; + + CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE); + FLASH->CR |= FLASH_PSIZE_WORD; + CLEAR_BIT(FLASH->CR, FLASH_CR_SNB); + FLASH->CR |= FLASH_CR_SER | (sector << FLASH_CR_SNB_Pos); + FLASH->CR |= FLASH_CR_STRT; + + if (!flash_drv_wait_last_op()) { + ret = -1; + } + + CLEAR_BIT(FLASH->CR, FLASH_CR_SER | FLASH_CR_SNB); + + __DSB(); + __enable_irq(); + + flash_drv_flush_caches(); + + stm32_flash_lock(); + +#else + FLASH_EraseInitTypeDef eraseInit; eraseInit.TypeErase = FLASH_TYPEERASE_SECTORS; eraseInit.Sector = stm32_flash_get_sector(address); @@ -142,7 +225,6 @@ static int stm32_flash_erase_sector(uint32_t address) eraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3; #endif - int ret = 0; uint32_t sector_errors = 0; __disable_irq(); @@ -153,6 +235,9 @@ static int stm32_flash_erase_sector(uint32_t address) stm32_flash_lock(); __enable_irq(); + +#endif + return ret; } @@ -164,18 +249,48 @@ static int stm32_flash_erase_sector(uint32_t address) #define FLASH_PROG_WORDS 4UL #define _FLASH_PROGRAM(address, p_data) \ HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, address, (uintptr_t)p_data) -#else - #define FLASH_PROG_WORDS 1UL - #define _FLASH_PROGRAM(address, p_data) \ - HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, *p_data) #endif static int stm32_flash_program(uint32_t address, void* data, uint32_t len) { + int ret = 0; + +#if defined(STM32F2) || defined(STM32F4) + + uint32_t* p_data = (uint32_t*)data; + uint32_t end_addr = address + len; + + __disable_irq(); + __DSB(); + stm32_flash_unlock(); + + while (address < end_addr) { + CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE); + FLASH->CR |= FLASH_PSIZE_WORD; + FLASH->CR |= FLASH_CR_PG; + + *(__IO uint32_t*)address = *p_data; + + if (!flash_drv_wait_last_op()) { + ret = -1; + break; + } + + CLEAR_BIT(FLASH->CR, FLASH_CR_PG); + + address += sizeof(uint32_t); + p_data++; + } + + __DSB(); + __enable_irq(); + stm32_flash_lock(); + +#else + uint32_t* p_data = (uint32_t*)data; uint32_t end_addr = address + len; - int ret = 0; __disable_irq(); stm32_flash_unlock(); while (address < end_addr) { @@ -190,6 +305,9 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) stm32_flash_lock(); __enable_irq(); + +#endif + return ret; } From de7c1cff3a1ed28949f9cdbbf7694248cbf78bdd Mon Sep 17 00:00:00 2001 From: 3djc <3djc@gh.com> Date: Wed, 9 Sep 2026 16:39:10 +0200 Subject: [PATCH 03/11] fix(stm32f4): clear stale FLASH error flags before erase/program FLASH_SR error flags (WRPERR/PGAERR/PGPERR/PGSERR/OPERR) are sticky and survive a reset. One left behind by whatever wrote the flash before us - a DFU session, the previous firmware - makes the very next erase or program report a failure it did not cause, which the caller then treats as a genuine write error. Clear them once after unlocking, before starting the operation, so the check that follows only ever sees flags this driver produced. Also clear FLASH_CR_PG when a word write fails. Leaving it set meant the next erase ran with PG and SER both set, turning one failed word into a sequence error for the rest of the session. --- .../targets/common/arm/stm32/flash_driver.cpp | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index 482f6f97df9..0ff54410c84 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -87,6 +87,22 @@ static uint32_t stm32_flash_get_sector_size(uint32_t sector) #define FLASH_TIMEOUT_MS 15000 +#if defined(FLASH_FLAG_RDERR) + #define _FLASH_FLAG_RDERR FLASH_FLAG_RDERR +#else + #define _FLASH_FLAG_RDERR 0U +#endif + +// Error flags are sticky and survive a reset, so one left over by whatever +// wrote the flash before us (DFU, a previous firmware) would abort the very +// next erase/program. Clear them before starting an operation. +static void flash_drv_clear_errors() +{ + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | + FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | + FLASH_FLAG_PGSERR | _FLASH_FLAG_RDERR); +} + static bool flash_drv_wait_last_op() { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; @@ -188,6 +204,7 @@ static int stm32_flash_erase_sector(uint32_t address) __DSB(); stm32_flash_unlock(); + flash_drv_clear_errors(); if (sector > 11) sector += 4; @@ -263,6 +280,7 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) __disable_irq(); __DSB(); stm32_flash_unlock(); + flash_drv_clear_errors(); while (address < end_addr) { CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE); @@ -271,13 +289,15 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) *(__IO uint32_t*)address = *p_data; - if (!flash_drv_wait_last_op()) { + // PG must be cleared even on failure, or the next erase sees PG+SER + bool ok = flash_drv_wait_last_op(); + CLEAR_BIT(FLASH->CR, FLASH_CR_PG); + + if (!ok) { ret = -1; break; } - CLEAR_BIT(FLASH->CR, FLASH_CR_PG); - address += sizeof(uint32_t); p_data++; } From 9b9e59d7a35b5ae6c464635a841287f3d340db4b Mon Sep 17 00:00:00 2001 From: 3djc <3djc@gh.com> Date: Wed, 9 Sep 2026 16:39:17 +0200 Subject: [PATCH 04/11] fix(boot): report firmware write failures instead of showing success flashWrite() returned void and silently gave up when the sector erase failed, so the bootloader kept advancing the progress bar and finished on "Writing complete" over an image whose first 128KB was never written. The radio then would not boot, which reads to users as a bricked radio rather than a failed flash (#7726, #7748, #7758). Make flashWrite() return whether the page was written and propagate that up: firmwareWriteBlock() now reports FW_IN_PROGRESS/FW_DONE/FW_ERROR, and the bootloader stops on ST_FLASH_ERROR, leaving the bar where the write gave up and pointing at a DFU flash as the way out. The in-firmware bootloader update had the same shape - it showed the success popup unconditionally, even after an SD read error or an incompatible file - so gate that on the writes having actually worked. --- radio/src/boards/helloradio-h750/board.h | 2 +- radio/src/boards/jumper-h750/board.h | 2 +- radio/src/boards/rm-h750/board.h | 2 +- radio/src/bootloader/bin_fw_files.cpp | 15 +++++++++------ radio/src/bootloader/boot.h | 1 + radio/src/bootloader/boot_menu.cpp | 18 ++++++++++++------ radio/src/bootloader/firmware_files.h | 10 +++++++++- radio/src/gui/colorlcd/boot_menu.cpp | 11 +++++++++-- radio/src/gui/common/stdlcd/boot_menu.cpp | 4 ++++ radio/src/io/bootloader_flash.cpp | 14 +++++++++++--- radio/src/targets/c14/board.h | 2 +- .../targets/common/arm/stm32/flash_driver.cpp | 7 ++++--- .../targets/common/arm/stm32/flash_driver.h | 2 +- radio/src/targets/pa01/board.h | 2 +- radio/src/targets/simu/simulib.cpp | 3 ++- radio/src/targets/st16/board.h | 2 +- radio/src/translations/bl_translations.h | 16 ++++++++++++++++ 17 files changed, 84 insertions(+), 29 deletions(-) diff --git a/radio/src/boards/helloradio-h750/board.h b/radio/src/boards/helloradio-h750/board.h index 5c3f986318c..3105ae6bac0 100644 --- a/radio/src/boards/helloradio-h750/board.h +++ b/radio/src/boards/helloradio-h750/board.h @@ -61,7 +61,7 @@ void getCPUUniqueID(char * s); #define FLASH_PAGESIZE 256 void unlockFlash(); void lockFlash(); -void flashWrite(uint32_t * address, const uint32_t * buffer); +bool flashWrite(uint32_t * address, const uint32_t * buffer); uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); diff --git a/radio/src/boards/jumper-h750/board.h b/radio/src/boards/jumper-h750/board.h index 354d4bbd9b6..556dcda7f35 100644 --- a/radio/src/boards/jumper-h750/board.h +++ b/radio/src/boards/jumper-h750/board.h @@ -60,7 +60,7 @@ void getCPUUniqueID(char * s); #define FLASH_PAGESIZE 256 void unlockFlash(); void lockFlash(); -void flashWrite(uint32_t * address, const uint32_t * buffer); +bool flashWrite(uint32_t * address, const uint32_t * buffer); uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); diff --git a/radio/src/boards/rm-h750/board.h b/radio/src/boards/rm-h750/board.h index a987c59600c..86e49d82aff 100644 --- a/radio/src/boards/rm-h750/board.h +++ b/radio/src/boards/rm-h750/board.h @@ -60,7 +60,7 @@ void getCPUUniqueID(char * s); #define FLASH_PAGESIZE 256 void unlockFlash(); void lockFlash(); -void flashWrite(uint32_t * address, const uint32_t * buffer); +bool flashWrite(uint32_t * address, const uint32_t * buffer); uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); diff --git a/radio/src/bootloader/bin_fw_files.cpp b/radio/src/bootloader/bin_fw_files.cpp index 7580bcab741..c2b2d900e78 100644 --- a/radio/src/bootloader/bin_fw_files.cpp +++ b/radio/src/bootloader/bin_fw_files.cpp @@ -53,13 +53,14 @@ static FWFileInfo fwFiles[MAX_FW_FILES]; static uint8_t Block_buffer[BLOCK_LEN]; static UINT BlockCount; -static void flashWriteBlock() +static bool flashWriteBlock() { // TODO: use some board provided driver instead uint32_t blockOffset = 0; #if !defined(SIMU) while (BlockCount) { - flashWrite((uint32_t *)firmwareAddress, (uint32_t *)&Block_buffer[blockOffset]); + if (!flashWrite((uint32_t *)firmwareAddress, (uint32_t *)&Block_buffer[blockOffset])) + return false; blockOffset += FLASH_PAGESIZE; firmwareAddress += FLASH_PAGESIZE; if (BlockCount > FLASH_PAGESIZE) { @@ -70,6 +71,7 @@ static void flashWriteBlock() } } #endif // SIMU + return true; } void sdInit(void) @@ -267,17 +269,18 @@ void firmwareInitWrite(uint32_t index) firmwareWritten = 0; } -bool firmwareWriteBlock(uint32_t* progress) +FlashWriteRes firmwareWriteBlock(uint32_t* progress) { - flashWriteBlock(); + if (!flashWriteBlock()) return FW_ERROR; + firmwareWritten += sizeof(Block_buffer); *progress = (100 * firmwareWritten) / firmwareSize; readFirmwareFile(); if (BlockCount == 0 || firmwareWritten >= FLASHSIZE - BOOTLOADER_SIZE) { - return true; + return FW_DONE; } - return false; + return FW_IN_PROGRESS; } diff --git a/radio/src/bootloader/boot.h b/radio/src/bootloader/boot.h index 5c740a5a8ab..8918956c251 100644 --- a/radio/src/bootloader/boot.h +++ b/radio/src/bootloader/boot.h @@ -53,6 +53,7 @@ enum BootloaderState { ST_FLASH_CHECK, ST_FLASHING, ST_FLASH_DONE, + ST_FLASH_ERROR, ST_RESTORE_MENU, ST_USB, #if defined(SPI_FLASH) diff --git a/radio/src/bootloader/boot_menu.cpp b/radio/src/bootloader/boot_menu.cpp index 74896b5a4e9..ecdb2ce361d 100644 --- a/radio/src/bootloader/boot_menu.cpp +++ b/radio/src/bootloader/boot_menu.cpp @@ -96,6 +96,7 @@ void bootloaderMenu() uint8_t index = 0; FRESULT fr; uint32_t nameCount = 0; + uint32_t progress = 0; sdInit(); @@ -107,7 +108,8 @@ void bootloaderMenu() next_frame += FRAME_INTERVAL_MS; if (state != ST_USB && state != ST_FLASHING - && state != ST_FLASH_DONE && state != ST_RADIO_MENU) { + && state != ST_FLASH_DONE && state != ST_FLASH_ERROR + && state != ST_RADIO_MENU) { if (usbPlugged()) { state = ST_USB; #if !defined(SIMU) @@ -244,14 +246,16 @@ void bootloaderMenu() } else if (result == 1) { // confirmed firmwareInitWrite(vpos); + progress = 0; state = ST_FLASHING; } } else if (state == ST_FLASHING) { - uint32_t progress = 0; - bool done = firmwareWriteBlock(&progress); + FlashWriteRes res = firmwareWriteBlock(&progress); bootloaderDrawScreen(state, progress); - if(done) { + if (res == FW_DONE) { state = ST_FLASH_DONE; + } else if (res == FW_ERROR) { + state = ST_FLASH_ERROR; } #if defined(SPI_FLASH) } else if (state == ST_CLEAR_FLASH_CHECK) { @@ -282,13 +286,15 @@ void bootloaderMenu() } } - if (state == ST_FLASH_DONE) { + if (state == ST_FLASH_DONE || state == ST_FLASH_ERROR) { + BootloaderState drawn = state; + if (event == EVT_KEY_BREAK(KEY_EXIT) || event == EVT_KEY_BREAK(KEY_ENTER)) { state = ST_START; vpos = 0; } - bootloaderDrawScreen(state, 100); + bootloaderDrawScreen(drawn, drawn == ST_FLASH_DONE ? 100 : progress); } if (event == EVT_KEY_LONG(KEY_EXIT)) { diff --git a/radio/src/bootloader/firmware_files.h b/radio/src/bootloader/firmware_files.h index 4475b13d3d4..560e5135b11 100644 --- a/radio/src/bootloader/firmware_files.h +++ b/radio/src/bootloader/firmware_files.h @@ -69,7 +69,15 @@ enum FlashCheckRes { }; FlashCheckRes checkFirmwareFile(unsigned int index, FlashCheckRes res); +enum FlashWriteRes { + FW_IN_PROGRESS=0, + FW_DONE, + FW_ERROR +}; + void firmwareInitWrite(uint32_t index); bool firmwareEraseBlock(uint32_t* progress); -bool firmwareWriteBlock(uint32_t* progress); + +// Write the block currently in the buffer, then fetch the next one +FlashWriteRes firmwareWriteBlock(uint32_t* progress); diff --git a/radio/src/gui/colorlcd/boot_menu.cpp b/radio/src/gui/colorlcd/boot_menu.cpp index ff174d41ed1..2ae8e722ce0 100644 --- a/radio/src/gui/colorlcd/boot_menu.cpp +++ b/radio/src/gui/colorlcd/boot_menu.cpp @@ -341,16 +341,20 @@ void bootloaderDrawScreen(BootloaderState st, int opt, const char* str) lcd.drawText(USB_TXT_X, y + USB_PLG_TXT_YO, TR_BL_USB_CONNECTED, USB_TXT_ALIGN | BL_FOREGROUND); } else if (st == ST_FILE_LIST || st == ST_DIR_CHECK || st == ST_FLASH_CHECK || - st == ST_FLASHING || st == ST_FLASH_DONE) { + st == ST_FLASHING || st == ST_FLASH_DONE || st == ST_FLASH_ERROR) { bootloaderDrawTitle(LV_SYMBOL_SD_CARD " /FIRMWARE"); - if (st == ST_FLASHING || st == ST_FLASH_DONE) { + if (st == ST_FLASHING || st == ST_FLASH_DONE || st == ST_FLASH_ERROR) { LcdFlags color = BL_RED; if (st == ST_FLASH_DONE) { color = BL_GREEN; opt = 100; // Completed > 100% + } else if (st == ST_FLASH_ERROR) { + // the bar stays where the write gave up + lcd.drawText(LCD_W / 2, (LCD_H - PROGRESS_H) / 2 - EdgeTxStyles::STD_FONT_HEIGHT, + LV_SYMBOL_CLOSE " " TR_BL_WRITING_FAILED, CENTERED | BL_FOREGROUND); } lcd.drawRect(PROGRESS_X, (LCD_H - PROGRESS_H) / 2, PROGRESS_W, PROGRESS_H, LINE_H, SOLID, BL_SELECTED); @@ -405,6 +409,9 @@ void bootloaderDrawScreen(BootloaderState st, int opt, const char* str) else if (st == ST_FLASH_DONE) { pos = lcd.drawText(FOOTER_X1, LCD_H - FOOTER_Y1, LV_SYMBOL_CHARGE " " TR_BL_WRITING_COMPL, FOOTER_ALIGN1 | BL_FOREGROUND); } + else if (st == ST_FLASH_ERROR) { + pos = lcd.drawText(FOOTER_X1, LCD_H - FOOTER_Y1, LV_SYMBOL_CHARGE " " TR_BL_RETRY_OR_DFU, FOOTER_ALIGN1 | BL_FOREGROUND); + } } #if LANDSCAPE pos = 0; diff --git a/radio/src/gui/common/stdlcd/boot_menu.cpp b/radio/src/gui/common/stdlcd/boot_menu.cpp index dedd6093862..853c1357a9b 100644 --- a/radio/src/gui/common/stdlcd/boot_menu.cpp +++ b/radio/src/gui/common/stdlcd/boot_menu.cpp @@ -130,6 +130,10 @@ void bootloaderDrawScreen(BootloaderState st, int opt, const char *str) else if (st == ST_FLASH_DONE) { lcdDrawCenteredText(4 * FH, TR_BL_WRITING_COMPL); } + else if (st == ST_FLASH_ERROR) { + lcdDrawCenteredText(4 * FH, TR_BL_WRITING_FAILED); + lcdDrawCenteredText(6 * FH, TR_BL_RETRY_OR_DFU); + } } uint32_t bootloaderGetMenuItemCount(int baseCount) diff --git a/radio/src/io/bootloader_flash.cpp b/radio/src/io/bootloader_flash.cpp index 18340e00dbc..1064d6edc0d 100644 --- a/radio/src/io/bootloader_flash.cpp +++ b/radio/src/io/bootloader_flash.cpp @@ -64,6 +64,7 @@ void BootloaderFirmwareUpdate::flashFirmware(const char * filename, ProgressHand FIL file; uint8_t buffer[1024]; UINT count; + bool success = true; pulsesStop(); @@ -81,22 +82,29 @@ void BootloaderFirmwareUpdate::flashFirmware(const char * filename, ProgressHand if (f_read(&file, buffer, sizeof(buffer), &count) != FR_OK) { POPUP_WARNING(STR_SDCARD_ERROR); + success = false; break; } if (count != sizeof(buffer) && !f_eof(&file)) { POPUP_WARNING(STR_SDCARD_ERROR); + success = false; break; } if (i == 0 && !isBootloaderStart(buffer)) { POPUP_WARNING(STR_INCOMPATIBLE); + success = false; break; } - for (UINT j = 0; j < count; j += FLASH_PAGESIZE) { + for (UINT j = 0; j < count && success; j += FLASH_PAGESIZE) { WDG_ENABLE(3000); - flashWrite(CONVERT_UINT_PTR(BOOTLOADER_ADDRESS + i + j), CONVERT_UINT_PTR(buffer + j)); + success = flashWrite(CONVERT_UINT_PTR(BOOTLOADER_ADDRESS + i + j), CONVERT_UINT_PTR(buffer + j)); WDG_ENABLE(WDG_DURATION); } + if (!success) { + POPUP_WARNING(STR_FIRMWARE_UPDATE_ERROR); + break; + } progressHandler("Bootloader", STR_WRITING, i, flash_size); // Reached end-of-file @@ -107,7 +115,7 @@ void BootloaderFirmwareUpdate::flashFirmware(const char * filename, ProgressHand #endif } - POPUP_INFORMATION(STR_FIRMWARE_UPDATE_SUCCESS); + if (success) POPUP_INFORMATION(STR_FIRMWARE_UPDATE_SUCCESS); watchdogSuspend(0); WDG_RESET(); diff --git a/radio/src/targets/c14/board.h b/radio/src/targets/c14/board.h index f9193a0dcaf..133a52277fc 100644 --- a/radio/src/targets/c14/board.h +++ b/radio/src/targets/c14/board.h @@ -60,7 +60,7 @@ void getCPUUniqueID(char * s); #define FLASH_PAGESIZE 256 void unlockFlash(); void lockFlash(); -void flashWrite(uint32_t * address, const uint32_t * buffer); +bool flashWrite(uint32_t * address, const uint32_t * buffer); uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index 0ff54410c84..d89999591ee 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -353,17 +353,18 @@ const etx_flash_driver_t stm32_flash_driver = { void unlockFlash() { stm32_flash_unlock(); } void lockFlash() { stm32_flash_lock(); } -void flashWrite(uint32_t* address, const uint32_t* buffer) +bool flashWrite(uint32_t* address, const uint32_t* buffer) { // check first if the address is on a sector boundary uint32_t sector = stm32_flash_get_sector((uintptr_t)address); uint32_t bank = stm32_flash_get_bank((uintptr_t)address); if ((uintptr_t)address == _flash_sector_address(sector, bank)) { - if (stm32_flash_erase_sector((uintptr_t)address) < 0) return; + if (stm32_flash_erase_sector((uintptr_t)address) < 0) return false; } - stm32_flash_program((uintptr_t)address, (uint8_t*)buffer, FLASH_PAGESIZE); + return stm32_flash_program((uintptr_t)address, (uint8_t*)buffer, + FLASH_PAGESIZE) == 0; } // TODO: move this somewhere else, as it depends on firmware layout diff --git a/radio/src/targets/common/arm/stm32/flash_driver.h b/radio/src/targets/common/arm/stm32/flash_driver.h index 86dc0b40107..3f107cf9fc3 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.h +++ b/radio/src/targets/common/arm/stm32/flash_driver.h @@ -44,5 +44,5 @@ extern const void* stm32_flash_dfu_media; uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); -void flashWrite(uint32_t* address, const uint32_t* buffer); +bool flashWrite(uint32_t* address, const uint32_t* buffer); diff --git a/radio/src/targets/pa01/board.h b/radio/src/targets/pa01/board.h index 42a2b5bc765..4ced440aa65 100644 --- a/radio/src/targets/pa01/board.h +++ b/radio/src/targets/pa01/board.h @@ -61,7 +61,7 @@ void getCPUUniqueID(char * s); #define FLASH_PAGESIZE 256 void unlockFlash(); void lockFlash(); -void flashWrite(uint32_t * address, const uint32_t * buffer); +bool flashWrite(uint32_t * address, const uint32_t * buffer); uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); diff --git a/radio/src/targets/simu/simulib.cpp b/radio/src/targets/simu/simulib.cpp index d9ced7ff7b9..315b7887d93 100644 --- a/radio/src/targets/simu/simulib.cpp +++ b/radio/src/targets/simu/simulib.cpp @@ -391,9 +391,10 @@ void setSelectedUsbMode(int mode) {} void delay_ms(uint32_t ms) { } void delay_us(uint16_t us) { } -void flashWrite(uint32_t *address, const uint32_t *buffer) +bool flashWrite(uint32_t *address, const uint32_t *buffer) { sleep_ms(10); + return true; } uint32_t isBootloaderStart(const uint8_t * block) diff --git a/radio/src/targets/st16/board.h b/radio/src/targets/st16/board.h index cc95deef494..1055bda249c 100644 --- a/radio/src/targets/st16/board.h +++ b/radio/src/targets/st16/board.h @@ -60,7 +60,7 @@ void getCPUUniqueID(char * s); #define FLASH_PAGESIZE 256 void unlockFlash(); void lockFlash(); -void flashWrite(uint32_t * address, const uint32_t * buffer); +bool flashWrite(uint32_t * address, const uint32_t * buffer); uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); diff --git a/radio/src/translations/bl_translations.h b/radio/src/translations/bl_translations.h index f919807a4df..3402ff9121e 100644 --- a/radio/src/translations/bl_translations.h +++ b/radio/src/translations/bl_translations.h @@ -64,6 +64,8 @@ #define TR_BL_DIR_EMPTY "Adresar je prazdny" #define TR_BL_WRITING_FW "Nahravani firmware ..." #define TR_BL_WRITING_COMPL "Nahravani dokonceno" + #define TR_BL_WRITING_FAILED TR("Zapis selhal!", "Zapis se nezdaril!") + #define TR_BL_RETRY_OR_DFU TR("Zkuste znovu / DFU", "Zkuste znovu, nebo pouzijte USB (DFU)") #define TR_BL_ENABLE "Povoleno" #define TR_BL_DISABLE "Zakazano" @@ -111,6 +113,8 @@ #define TR_BL_DIR_EMPTY "Katalog er tomt" #define TR_BL_WRITING_FW "Installerer..." #define TR_BL_WRITING_COMPL "Installation slut" + #define TR_BL_WRITING_FAILED TR("Skrivning fejlede!", "Skrivning mislykkedes!") + #define TR_BL_RETRY_OR_DFU TR("Prov igen / DFU", "Prov igen, eller brug USB (DFU)") #define TR_BL_ENABLE "Aktiver" #define TR_BL_DISABLE "Deaktiver" @@ -158,6 +162,8 @@ #define TR_BL_DIR_EMPTY "Verzeichnis leer" #define TR_BL_WRITING_FW "Schreibe..." #define TR_BL_WRITING_COMPL TR("Schreiben fertig","Schreiben abgeschlossen") + #define TR_BL_WRITING_FAILED TR("Schreiben Fehler!", "Schreiben fehlgeschlagen!") + #define TR_BL_RETRY_OR_DFU TR("Neu vers. / DFU", "Erneut versuchen, oder USB (DFU)") #define TR_BL_ENABLE "Aktivieren" #define TR_BL_DISABLE "Deaktivieren" @@ -206,6 +212,8 @@ #define TR_BL_DIR_EMPTY "Repertoire vide" #define TR_BL_WRITING_FW "Ecriture Firmware ..." #define TR_BL_WRITING_COMPL "Ecriture terminée" + #define TR_BL_WRITING_FAILED TR("Echec ecriture!", "Echec de l'ecriture!") + #define TR_BL_RETRY_OR_DFU TR("Reessayer / DFU", "Reessayez, ou utilisez USB (DFU)") #define TR_BL_ENABLE "Activer" #define TR_BL_DISABLE "Désactiver" @@ -253,6 +261,8 @@ #define TR_BL_DIR_EMPTY "Cartella vuota" #define TR_BL_WRITING_FW "Scrittura..." #define TR_BL_WRITING_COMPL "Scrittura completata" + #define TR_BL_WRITING_FAILED TR("Scrittura fallita!", "Scrittura fallita!") + #define TR_BL_RETRY_OR_DFU TR("Riprova / DFU", "Riprova, oppure usa USB (DFU)") #define TR_BL_ENABLE "Abilita" #define TR_BL_DISABLE "Disabilita" @@ -300,6 +310,8 @@ #define TR_BL_DIR_EMPTY "Katalog jest pusty" #define TR_BL_WRITING_FW "Zapis firmware ..." #define TR_BL_WRITING_COMPL "Zapis ukonczony" + #define TR_BL_WRITING_FAILED TR("Zapis nieudany!", "Zapis nie powiodl sie!") + #define TR_BL_RETRY_OR_DFU TR("Powtorz / DFU", "Powtorz, lub uzyj USB (DFU)") #define TR_BL_ENABLE "Enable" #define TR_BL_DISABLE "Disable" @@ -347,6 +359,8 @@ #define TR_BL_DIR_EMPTY "Katalogen aer tom" #define TR_BL_WRITING_FW "Skriver..." #define TR_BL_WRITING_COMPL "Skrivning klar" + #define TR_BL_WRITING_FAILED TR("Skrivning fel!", "Skrivningen misslyckades!") + #define TR_BL_RETRY_OR_DFU TR("Forsok igen / DFU", "Forsok igen, eller anvand USB (DFU)") #define TR_BL_ENABLE "Aktivera" #define TR_BL_DISABLE "Inaktivera" @@ -394,6 +408,8 @@ #define TR_BL_DIR_EMPTY "Directory is empty" #define TR_BL_WRITING_FW "Writing..." #define TR_BL_WRITING_COMPL "Writing complete" + #define TR_BL_WRITING_FAILED TR("Writing failed!", "Writing failed!") + #define TR_BL_RETRY_OR_DFU TR("Retry or use DFU", "Retry, or flash over USB (DFU)") #define TR_BL_ENABLE "Enable" #define TR_BL_DISABLE "Disable" From 495fb81fb6c01e685126a496a119e183becb5c88 Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:34:17 +0000 Subject: [PATCH 05/11] fix(senduwing-h750): update flashWrite() declaration to return bool The Senduwing H17 board was added after flashWrite() was changed to report failures, so its board.h still declared the old void signature and conflicted with flash_driver.h. Co-Authored-By: Claude Opus 5.5 --- radio/src/boards/senduwing-h750/board.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/src/boards/senduwing-h750/board.h b/radio/src/boards/senduwing-h750/board.h index 17a76725e25..137b63bdc5b 100644 --- a/radio/src/boards/senduwing-h750/board.h +++ b/radio/src/boards/senduwing-h750/board.h @@ -60,7 +60,7 @@ void getCPUUniqueID(char * s); #define FLASH_PAGESIZE 256 void unlockFlash(); void lockFlash(); -void flashWrite(uint32_t * address, const uint32_t * buffer); +bool flashWrite(uint32_t * address, const uint32_t * buffer); uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); From c2a9dd550cca26ea4af3f5dc3d8771b9d5bc14b7 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Wed, 2 Sep 2026 00:01:33 +0000 Subject: [PATCH 06/11] fix(bootloader): treat SD read errors as a flash failure firmwareWriteBlock() discarded the result of readFirmwareFile(), so a disk error mid-flash (after which FatFs latches fp->err and every later read returns 0 bytes) was indistinguishable from end of file. The bootloader flashed a truncated image and showed "Writing complete". Report a non-OK read, or a short file before firmwareSize, as FW_ERROR so the ST_FLASH_ERROR screen is shown instead. Co-Authored-By: Claude Fable 5.1 --- radio/src/bootloader/bin_fw_files.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/radio/src/bootloader/bin_fw_files.cpp b/radio/src/bootloader/bin_fw_files.cpp index c2b2d900e78..35a07416b6a 100644 --- a/radio/src/bootloader/bin_fw_files.cpp +++ b/radio/src/bootloader/bin_fw_files.cpp @@ -276,7 +276,13 @@ FlashWriteRes firmwareWriteBlock(uint32_t* progress) firmwareWritten += sizeof(Block_buffer); *progress = (100 * firmwareWritten) / firmwareSize; - readFirmwareFile(); + // a read error or a short file must not be reported as success + if (readFirmwareFile() != FR_OK) { + return FW_ERROR; + } + if (BlockCount == 0 && firmwareWritten < firmwareSize) { + return FW_ERROR; + } if (BlockCount == 0 || firmwareWritten >= FLASHSIZE - BOOTLOADER_SIZE) { return FW_DONE; } From 36b6e86756aa3df6ac4503ba24a9a7ce73394e96 Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:34:39 +0000 Subject: [PATCH 07/11] fix(stm32f4): disable data cache during flash erase/program (ES0206 2.2.15) On dual bank STM32F42x/43x devices a read (data access or code execution) from one bank while the other bank is being written can corrupt the ART data cache if DCEN is set; subsequent cache hits then return corrupted data. The bootloader executes from bank 1 with the data cache enabled and a TX16S image crosses into bank 2 at roughly 57% of the write, so the tail of every flash was exposed. The documented workaround is to disable the data cache before the write and reset it before enabling it again; previously the caches were only flushed after an erase, and not at all after a program. Co-Authored-By: Claude Fable 5.1 Co-Authored-By: Claude Opus 5.5 --- .../targets/common/arm/stm32/flash_driver.cpp | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index d89999591ee..65c4b4d8e68 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -134,7 +134,17 @@ static bool flash_drv_wait_last_op() return true; } -static void flash_drv_flush_caches() +// ES0206 2.2.15: on dual bank devices the data cache may be corrupted by a +// read-while-write, so it must be disabled while flash is erased/programmed +// and reset before being enabled again. +static bool flash_drv_disable_dcache() +{ + bool enabled = (FLASH->ACR & FLASH_ACR_DCEN) != 0; + if (enabled) FLASH->ACR &= ~FLASH_ACR_DCEN; + return enabled; +} + +static void flash_drv_flush_caches(bool dcache_enabled) { if (FLASH->ACR & FLASH_ACR_ICEN) { FLASH->ACR &= ~FLASH_ACR_ICEN; @@ -142,8 +152,7 @@ static void flash_drv_flush_caches() FLASH->ACR &= ~FLASH_ACR_ICRST; FLASH->ACR |= FLASH_ACR_ICEN; } - if (FLASH->ACR & FLASH_ACR_DCEN) { - FLASH->ACR &= ~FLASH_ACR_DCEN; + if (dcache_enabled) { FLASH->ACR |= FLASH_ACR_DCRST; FLASH->ACR &= ~FLASH_ACR_DCRST; FLASH->ACR |= FLASH_ACR_DCEN; @@ -206,6 +215,8 @@ static int stm32_flash_erase_sector(uint32_t address) stm32_flash_unlock(); flash_drv_clear_errors(); + bool dcache = flash_drv_disable_dcache(); + if (sector > 11) sector += 4; CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE); @@ -223,7 +234,7 @@ static int stm32_flash_erase_sector(uint32_t address) __DSB(); __enable_irq(); - flash_drv_flush_caches(); + flash_drv_flush_caches(dcache); stm32_flash_lock(); @@ -282,6 +293,8 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) stm32_flash_unlock(); flash_drv_clear_errors(); + bool dcache = flash_drv_disable_dcache(); + while (address < end_addr) { CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE); FLASH->CR |= FLASH_PSIZE_WORD; @@ -304,6 +317,9 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) __DSB(); __enable_irq(); + + flash_drv_flush_caches(dcache); + stm32_flash_lock(); #else From 99adf2ba5e203b5a43172debb7c2e777e5a15c1c Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:34:56 +0000 Subject: [PATCH 08/11] fix(stm32): verify each page after writing it in flashWrite() The erase/program status only tells us what the flash controller reported. Read the page back and compare it with the source buffer so a write that the controller wrongly reports as successful is still caught and reported as a flash failure. The cost is a 256 byte memcmp per page, which is negligible next to programming it. On cores with a data cache (H7) the page is invalidated first, as the firmware may have read that area (e.g. the bootloader version) before updating it. Co-Authored-By: Claude Opus 5.5 --- .../src/targets/common/arm/stm32/flash_driver.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index 65c4b4d8e68..6035f360840 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -379,8 +379,19 @@ bool flashWrite(uint32_t* address, const uint32_t* buffer) if (stm32_flash_erase_sector((uintptr_t)address) < 0) return false; } - return stm32_flash_program((uintptr_t)address, (uint8_t*)buffer, - FLASH_PAGESIZE) == 0; + if (stm32_flash_program((uintptr_t)address, (uint8_t*)buffer, + FLASH_PAGESIZE) < 0) + return false; + +#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U) + // drop any lines cached before the erase/program so the read back + // below comes from flash + SCB_InvalidateDCache_by_Addr(address, FLASH_PAGESIZE); +#endif + + // verify the page was actually written, independently of what the + // flash controller reported + return memcmp(address, buffer, FLASH_PAGESIZE) == 0; } // TODO: move this somewhere else, as it depends on firmware layout From 707f7346fc773e3bb67f9258610809af13e9c9e2 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Wed, 2 Sep 2026 00:31:00 +0000 Subject: [PATCH 09/11] chore(stm32): shorten flash operation timeout to twice the datasheet maximum FLASH_TIMEOUT_MS was 15 s. DS9405 Rev 13 Table 48 gives a maximum of 2 s for a 128 KB sector erase at x32 parallelism (the driver's setting) and 100 us per word, and the driver never issues a bank or mass erase, so 4 s is ample and shortens the time to the error screen when the controller does not complete an operation. Co-Authored-By: Claude Fable 5.1 --- radio/src/targets/common/arm/stm32/flash_driver.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index 6035f360840..298afd1df6c 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -85,7 +85,9 @@ static uint32_t stm32_flash_get_sector_size(uint32_t sector) return 128 * 1024; } -#define FLASH_TIMEOUT_MS 15000 +// twice the datasheet maximum for a 128 KB sector erase at x32 parallelism +// (2 s, DS9405 Table 48); the driver never issues a bank or mass erase +#define FLASH_TIMEOUT_MS 4000 #if defined(FLASH_FLAG_RDERR) #define _FLASH_FLAG_RDERR FLASH_FLAG_RDERR From 96bce7e4850adfbd630e85be7351d0522c7af36f Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Fri, 2 Oct 2026 03:02:20 +0000 Subject: [PATCH 10/11] fix(stm32h7): keep interrupts enabled during HAL flash erase/program The interrupt masking added for F4 was kept on the H7/H7RS HAL path when F2/F4 moved to the register-level driver. There it stops the HAL timeout from ever firing: FLASH_WaitForLastOperation() counts HAL_GetTick(), which only advances in the ms timer interrupt, so a flash operation that never completes becomes an infinite loop. This is the same defect the DWT timeout fixes on F4. Masking protects nothing on these targets. RM0433 Rev 8 4.3.7 only stalls reads of the bank being written, and the bootloader runs its code and vectors from ITCM/DTCM (the firmware from SDRAM), so nothing fetches from internal flash during the erase. ES0392 Rev 15 has no flash erratum that calls for it, and ST's FLASH_EraseProgram example does not mask either. Restore main's behaviour for the HAL path. Co-Authored-By: Claude Opus 5.5 --- radio/src/targets/common/arm/stm32/flash_driver.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index 298afd1df6c..86abb611812 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -257,14 +257,15 @@ static int stm32_flash_erase_sector(uint32_t address) uint32_t sector_errors = 0; - __disable_irq(); + // Interrupts must stay enabled here: the HAL timeout counts HAL_GetTick(), + // which only advances in the ms timer interrupt. Nothing needs masking, as + // code and vectors run from TCM/SDRAM, never from the bank being erased. stm32_flash_unlock(); if (HAL_FLASHEx_Erase(&eraseInit, §or_errors) != HAL_OK) { ret = -1; } stm32_flash_lock(); - __enable_irq(); #endif @@ -329,7 +330,7 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) uint32_t* p_data = (uint32_t*)data; uint32_t end_addr = address + len; - __disable_irq(); + // interrupts stay enabled, see stm32_flash_erase_sector() stm32_flash_unlock(); while (address < end_addr) { if (_FLASH_PROGRAM(address, p_data) != HAL_OK) { @@ -342,7 +343,6 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) } stm32_flash_lock(); - __enable_irq(); #endif From 202172f315b243241c2bf4c914e737bf6a11cbb3 Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Fri, 2 Oct 2026 03:02:33 +0000 Subject: [PATCH 11/11] chore(stm32f4): correct reason given for clearing stale FLASH error flags The comment claimed the error flags survive a reset. RM0090 Rev 22 3.9.6 gives FLASH_SR a reset value of 0, so they do not. They are sticky until written with 1, though, so a flag raised earlier in the same session (a stray write to flash sets PGSERR) would still abort the next operation, which is why clearing them first is worthwhile. Co-Authored-By: Claude Opus 5.5 --- radio/src/targets/common/arm/stm32/flash_driver.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index 86abb611812..776ff2e86f6 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -95,9 +95,9 @@ static uint32_t stm32_flash_get_sector_size(uint32_t sector) #define _FLASH_FLAG_RDERR 0U #endif -// Error flags are sticky and survive a reset, so one left over by whatever -// wrote the flash before us (DFU, a previous firmware) would abort the very -// next erase/program. Clear them before starting an operation. +// Error flags are sticky until written with 1, so one left over by an earlier +// access since reset (e.g. a stray write to flash, which sets PGSERR) would +// abort the very next erase/program. Clear them before starting an operation. static void flash_drv_clear_errors() { __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR |