Optimize multiboot RAM with a safe Overlay

This commit is contained in:
Armel FAUVEAU committed 2026-08-19 17:52:47 +02:00
1 parent 4ffff5149f
commit 5b6174085b
12 files changed
+299 -53

No files matched your search

+3 -3
View File
@@ -241,10 +241,10 @@ enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT
driver/mb_flash.c
ui/multiboot.c
)
enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM)
enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY)
if(ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM requires ENABLE_FEAT_F4HWN_MULTIBOOT.")
if(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY requires ENABLE_FEAT_F4HWN_MULTIBOOT.")
endif()
enable_feature(ENABLE_FEAT_F4HWN_QRCODE)
+65 -19
View File
@@ -31,7 +31,6 @@
/* External SPI flash chip-select is on PA3 (see driver/py25q16.c). */
#define MB_CS_PIN (1u << 3)
#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM
/* LCD control pins used only for RAM-resident progress updates. */
#define MB_LCD_CS_PIN (1u << 2) /* PB2 */
#define MB_LCD_A0_PIN (1u << 6) /* PA6 */
@@ -40,7 +39,6 @@
#define MB_PROGRESS_COLS 118u
#define MB_PROGRESS_FIRST_COL 5u
#define MB_PROGRESS_FILLED 0x2Du
#endif
/* Number of erase/program retries per page before giving up (and resetting
* anyway - the region is already erased, so USB recovery is the only option). */
@@ -103,14 +101,21 @@ static void MB_PrepareInternalFlash(void)
/* during which the flash bus is unavailable. It must therefore NOT fetch any */
/* code from flash nor read any flash data: it uses raw register access only */
/* (no external calls), reads the source from the external SPI flash in */
/* polled mode, and resets the MCU when done. It is placed in .RamFunc, which */
/* the linker stores in flash and the startup copies to RAM alongside .data. */
/* polled mode, and resets the MCU when done. Normally it is placed in */
/* .RamFunc, which startup copies to RAM alongside .data. With the overlay */
/* enabled it is linked in .MBRamFunc and copied over the PY25Q16 sector cache */
/* only immediately before use. */
/* -------------------------------------------------------------------------- */
/* These primitives are called repeatedly while application flash is offline.
* Keep one copy of each in the copied RAM section: noinline/noclone prevents
* GCC from silently duplicating one back into MB_RamReflash. */
#define MB_RAM_HELPER __attribute__((section(".RamFunc"), noinline, noclone, used))
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
#define MB_RAM_SECTION ".MBRamFunc"
#else
#define MB_RAM_SECTION ".RamFunc"
#endif
#define MB_RAM_HELPER __attribute__((section(MB_RAM_SECTION), noinline, noclone, used))
/* Polled single-byte SPI2 transfer. The result is returned through out so
* timeout and received 0xFF remain distinguishable. */
@@ -150,7 +155,6 @@ MB_RAM_HELPER __attribute__((noreturn)) static void mb_ram_reset(void)
for (;;) { }
}
#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM
/* Minimal SPI1 LCD writer. A display timeout merely disables progress updates:
* it must never abort or delay the safety-critical flash copy. */
MB_RAM_HELPER static bool mb_ram_lcd_spi(uint8_t v)
@@ -220,9 +224,7 @@ __attribute__((always_inline)) static inline void mb_ram_lcd_clear(void)
GPIOB->BSRR = MB_LCD_CS_PIN;
}
}
#endif /* !ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM */
__attribute__((section(".RamFunc"), noinline, used))
__attribute__((section(MB_RAM_SECTION), noinline, used))
static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize,
uint8_t *progressLine)
{
@@ -231,14 +233,10 @@ static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize
uint8_t buf[MB_FLASH_PAGE] __attribute__((aligned(4)));
uint32_t remaining = imageSize;
uint32_t regionRemaining = MB_INT_APP_SIZE;
#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM
uint32_t pagesDone = 0;
uint32_t progressAccumulator = 0;
uint32_t progressFilled = 0;
uint32_t lcdEnabled = progressLine != NULL;
#else
(void)progressLine;
#endif
__disable_irq();
@@ -349,7 +347,6 @@ static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize
if (!success)
goto fatal_reset;
#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM
/* Advance the gauge without division (which could call a helper
* in erased flash). Refresh once per 8 KiB internal sector. */
if (lcdEnabled)
@@ -368,7 +365,6 @@ static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize
if ((pagesDone & 31u) == 0u || regionRemaining == MB_FLASH_PAGE)
lcdEnabled = mb_ram_progress_blit(progressLine);
}
#endif
intAddr += MB_FLASH_PAGE;
extAddr += readSize;
@@ -377,10 +373,8 @@ static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize
}
FLASH->CR |= FLASH_CR_LOCK;
#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM
if (lcdEnabled)
mb_ram_lcd_clear();
#endif
mb_ram_reset();
fatal_reset:
@@ -399,6 +393,39 @@ fatal_reset:
/* Set when a polled SPI wait below times out (external flash unresponsive). */
static volatile int mb_spi_err;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
/* Linker-provided load/execution bounds for the restore stub. Its execution
* address aliases the PY25Q16 sector cache; its load image remains in flash. */
extern uint8_t __mb_ramfunc_load_start;
extern uint8_t __mb_ramfunc_start;
extern uint8_t __mb_ramfunc_end;
static bool mb_load_ram_reflash_overlay(void)
{
const volatile uint8_t *src = &__mb_ramfunc_load_start;
volatile uint8_t *dst = &__mb_ramfunc_start;
volatile uint8_t *end = &__mb_ramfunc_end;
/* Volatile byte copies prevent a library memcpy call. The copy itself runs
* while internal flash is still fully available. */
while (dst < end)
*dst++ = *src++;
/* Cortex-M0+ has no instruction cache, but the barriers ensure that every
* store is visible before the following BLX starts fetching the stub. */
__DSB();
__ISB();
src = &__mb_ramfunc_load_start;
dst = &__mb_ramfunc_start;
while (dst < end)
if (*dst++ != *src++)
return false;
return true;
}
#endif
/* Polled single-byte SPI2 transfer (flash-resident; runs in normal context).
* Bounded so a wedged SPI can never freeze the firmware: on timeout it sets
* mb_spi_err and returns 0xFF, letting the caller fail gracefully. */
@@ -668,8 +695,27 @@ uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line)
const uint32_t slotBase = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE;
/* Valid: the RAM stub copies exactly image_size bytes, pads the partial
* page with 0xFF and erases the remainder of the application region. */
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
/* No PY25Q16 driver access is allowed after this point: the sector cache is
* about to become executable storage and the RAM stub always ends in reset.
* Mask IRQs first, then explicitly stop SPI2 DMA before overwriting the
* cache. Keeping both operations local avoids relying on validation's
* current polled-SPI implementation and closes any IRQ re-arm window. */
const uint32_t primask = __get_PRIMASK();
__disable_irq();
mb_spi_polled_mode();
PY25Q16_InvalidateCache();
if (!mb_load_ram_reflash_overlay())
{
__set_PRIMASK(primask);
return MB_ERR_RAM_LOAD;
}
#endif
/* Valid and, for the overlay path, safely loaded: the RAM stub copies
* exactly image_size bytes, pads the partial page with 0xFF and erases the
* remainder of the application region. Keep flash locked until this point
* so an overlay-copy failure can return without changing flash state. */
MB_PrepareInternalFlash();
/* Call through a volatile pointer so the compiler emits an absolute 'blx'
+5 -5
View File
@@ -71,7 +71,7 @@ typedef struct __attribute__((packed)) {
uint8_t reserved[16]; /* pad to 64 bytes, future use */
} mb_slot_header_t;
/* Restore validation result (MB_OK never returns - the radio resets). */
/* Multiboot operation result (a successful restore resets before returning). */
enum {
MB_OK = 0,
MB_ERR_MAGIC, /* no/invalid slot header */
@@ -81,15 +81,15 @@ enum {
MB_ERR_CRC, /* image CRC32 mismatch */
MB_ERR_SPI, /* external flash read/write timed out*/
MB_ERR_SLOT, /* slot index out of range */
MB_ERR_AUTH /* write refused: timestamp mismatch */
MB_ERR_AUTH, /* write refused: timestamp mismatch */
MB_ERR_RAM_LOAD /* restore stub RAM copy mismatch */
};
/* Multi-slot API used by the boot selector. Validation always covers the full
* image CRC before restore. progress_line may point to a 128-byte LCD page; the
* RAM copier then fills it while reflashing. Pass NULL to disable LCD updates.
* The in-copier LCD progress code is compiled out when
* ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM is defined (to reclaim RAM-resident
* .RamFunc space); in that case progress_line is ignored. */
* With ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY, the copier is loaded over the
* PY25Q16 sector cache only after validation and immediately before this call. */
uint8_t MB_ValidateSlot(uint8_t slot, mb_slot_header_t *out_header, uint32_t *out_crc);
uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line);
+8
View File
@@ -43,7 +43,15 @@
#define PAGE_SIZE 0x100
static uint32_t SectorCacheAddr = 0x1000000;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
/* The restore-only RAM stub is copied over this cache immediately before it
* erases internal flash. A reset always follows, so the cache is never needed
* again after the overlay becomes active. */
static uint8_t SectorCache[SECTOR_SIZE]
__attribute__((section(".bss.mb_workspace"), aligned(4), used));
#else
static uint8_t SectorCache[SECTOR_SIZE];
#endif
static uint8_t BlackHole[4] __attribute__((aligned(4)));
static volatile bool TC_Flag;
+2 -1
View File
@@ -28,7 +28,8 @@ void PY25Q16_SectorErase(uint32_t Address);
/* Drop the internal single-sector write cache. Call after erasing/programming
* flash behind the driver's back (e.g. the raw multiboot slot/profile ops) so a
* later write cannot skip or resurrect data based on a stale cached sector. */
* later write cannot skip or resurrect data based on a stale cached sector. It
* is also called before multiboot reuses the cache storage as a RAM overlay. */
void PY25Q16_InvalidateCache(void);
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
+2 -9
View File
@@ -39,6 +39,7 @@ static const char *mb_error_text(uint8_t err)
case MB_ERR_SPI: return "SPI ERROR";
case MB_ERR_SLOT: return "bad slot";
case MB_ERR_AUTH: return "auth";
case MB_ERR_RAM_LOAD: return "RAM LOAD ERROR";
default: return "error";
}
}
@@ -262,12 +263,8 @@ __attribute__((noinline)) static void mb_prepare_progress_screen(const char *tit
UI_PrintStringSmallNormal(title, 2, 126, 1);
UI_PrintStringSmallNormal("DO NOT POWER OFF", 2, 126, 3);
UI_PrintStringSmallNormal(detail, 2, 126, 5);
#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM
/* Empty gauge that the RAM copier fills as it reflashes. Without the
* in-copier progress code the bar would never move, so it is left out and
* the static "DO NOT POWER OFF" screen stands on its own. */
/* Empty gauge that the RAM copier fills as it reflashes. */
mb_draw_progress_outline();
#endif
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
}
@@ -409,11 +406,7 @@ void UI_MultibootSelector(void)
}
mb_prepare_progress(selected);
#ifndef ENABLE_FEAT_F4HWN_MULTIBOOT_LOW_RAM
uint8_t err = MB_RestoreSlot(selected, gFrameBuffer[6]);
#else
uint8_t err = MB_RestoreSlot(selected, NULL);
#endif
/* Only reached when the final pre-erase validation refused the slot. */
status[selected] = err;
+6 -11
View File
@@ -115,15 +115,10 @@ extern uint8_t _edata; // End of .data in RAM
extern uint8_t _sbss; // Start of .bss in RAM
extern uint8_t _ebss; // End of .bss in RAM
// _eflash_used must be defined in the linker script immediately after the last
// section with a FLASH load address (after .noncacheable). Example:
//
// .noncacheable : {
// ...
// } > RAM AT> FLASH
// _eflash_used = LOADADDR(.noncacheable) + SIZEOF(.noncacheable);
//
// This gives the exact byte count that the linker reports as FLASH used.
// _eflash_used is defined by the linker at the end of the final section with a
// FLASH load image. This is currently .mb_ramfunc (empty without the overlay),
// after the load images for .data and .noncacheable. It therefore gives the
// exact byte count that the linker reports as FLASH used.
extern uint8_t _eflash_used;
// Absolute symbols: their *address* IS the numeric size value (ARM/CMSIS convention).
@@ -152,8 +147,8 @@ static void build_usage(uint32_t* ram_used, uint32_t* flash_used)
const uint32_t stack_size = (uint32_t)(uintptr_t)&_Min_Stack_Size;
*ram_used = span(&_sdata, &_ebss) + heap_size + stack_size;
// FLASH: _eflash_used is placed by the linker script right after the last
// section copied to FLASH (.data LMA + .noncacheable LMA).
// FLASH: _eflash_used follows the final FLASH load image (.mb_ramfunc,
// after the .data and .noncacheable load images).
// Note: _etext is NOT usable here because this linker script places .rodata
// sections AFTER _etext, making it an unreliable end-of-flash marker.
*flash_used = span((void*)FLASH_BASE, &_eflash_used);