Multiboot: per-slot config profiles + restorable Main backup

This commit is contained in:
Armel FAUVEAU committed 2026-08-18 03:57:29 +02:00
1 parent 295d4a2c05
commit 78f5b76a71
8 files changed
+763 -26

No files matched your search

+23
View File
@@ -981,6 +981,29 @@ void UART_HandleCommand(uint32_t Port)
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0728: // profile config reset: wipe the 64 KiB config bank of a slot
{
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint32_t ts = (uint32_t)pUART_Command->Data[2]
| ((uint32_t)pUART_Command->Data[3] << 8)
| ((uint32_t)pUART_Command->Data[4] << 16)
| ((uint32_t)pUART_Command->Data[5] << 24);
uint8_t status = (ts != mb_port_timestamp(Port))
? MB_ERR_AUTH : MB_ProfileErase(slot);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0729;
Reply.Header.Size = 2;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
#endif
#ifdef ENABLE_UART_RW_BK_REGS
+368 -9
View File
@@ -14,8 +14,10 @@
*/
#include <stddef.h>
#include <string.h>
#include "driver/mb_flash.h"
#include "driver/py25q16.h"
#include "py32f0xx.h"
@@ -103,10 +105,14 @@ static void MB_PrepareInternalFlash(void)
/* the linker stores in flash and the startup copies to RAM alongside .data. */
/* -------------------------------------------------------------------------- */
/* Polled single-byte SPI2 transfer. always_inline keeps all code in .RamFunc.
* The result is returned through out so timeout and received 0xFF remain
* distinguishable. */
__attribute__((always_inline)) static inline bool mb_ram_spi(uint8_t v, uint8_t *out)
/* 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))
/* Polled single-byte SPI2 transfer. The result is returned through out so
* timeout and received 0xFF remain distinguishable. */
MB_RAM_HELPER static bool mb_ram_spi(uint8_t v, uint8_t *out)
{
uint32_t timeout = MB_RAM_SPI_TIMEOUT;
while (!(SPI2->SR & SPI_SR_TXE))
@@ -124,7 +130,7 @@ __attribute__((always_inline)) static inline bool mb_ram_spi(uint8_t v, uint8_t
return true;
}
__attribute__((always_inline)) static inline bool mb_ram_flash_idle(void)
MB_RAM_HELPER static bool mb_ram_flash_idle(void)
{
uint32_t timeout = MB_RAM_FLASH_TIMEOUT;
while (FLASH->SR & FLASH_SR_BSY)
@@ -133,7 +139,7 @@ __attribute__((always_inline)) static inline bool mb_ram_flash_idle(void)
return true;
}
__attribute__((always_inline, noreturn)) static inline void mb_ram_reset(void)
MB_RAM_HELPER __attribute__((noreturn)) static void mb_ram_reset(void)
{
__DSB();
@@ -144,7 +150,7 @@ __attribute__((always_inline, noreturn)) static inline void mb_ram_reset(void)
/* Minimal SPI1 LCD writer. A display timeout merely disables progress updates:
* it must never abort or delay the safety-critical flash copy. */
__attribute__((always_inline)) static inline bool mb_ram_lcd_spi(uint8_t v)
MB_RAM_HELPER static bool mb_ram_lcd_spi(uint8_t v)
{
uint32_t timeout = MB_RAM_SPI_TIMEOUT;
while (!(SPI1->SR & SPI_SR_TXE))
@@ -183,6 +189,35 @@ __attribute__((always_inline)) static inline bool mb_ram_progress_blit(const uin
return ok != 0u;
}
/* Blank the whole LCD RAM (all 8 pages) right before the reset. The MCU reset
* leaves the display controller powered and still showing the "Restore slot N"
* screen; it stays visible through the next boot until ST7565_Init re-inits the
* panel. Wiping it here means the reboot window shows nothing instead of a
* stale restore screen. Best-effort: a display timeout just leaves it as-is. */
__attribute__((always_inline)) static inline void mb_ram_lcd_clear(void)
{
for (uint8_t page = 0; page < 8u; page++)
{
GPIOB->BRR = MB_LCD_CS_PIN;
GPIOA->BRR = MB_LCD_A0_PIN; /* command */
if (!mb_ram_lcd_spi((uint8_t)(0xB0u | page)) || /* set page 0..7 */
!mb_ram_lcd_spi(0x10u) || /* column high nibble */
!mb_ram_lcd_spi(0x04u)) /* visible RAM starts at column 4 */
{
GPIOB->BSRR = MB_LCD_CS_PIN;
return;
}
GPIOA->BSRR = MB_LCD_A0_PIN; /* data */
for (uint32_t i = 0; i < 128u; i++)
if (!mb_ram_lcd_spi(0x00u))
{
GPIOB->BSRR = MB_LCD_CS_PIN;
return;
}
GPIOB->BSRR = MB_LCD_CS_PIN;
}
}
__attribute__((section(".RamFunc"), noinline, used))
static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize,
uint8_t *progressLine)
@@ -332,6 +367,8 @@ static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize
}
FLASH->CR |= FLASH_CR_LOCK;
if (lcdEnabled)
mb_ram_lcd_clear();
mb_ram_reset();
fatal_reset:
@@ -644,7 +681,8 @@ uint8_t MB_SlotInfo(uint8_t slot, mb_slot_header_t *out_header)
uint8_t MB_SlotErase(uint8_t slot)
{
if (slot >= MB_SLOT_COUNT)
/* Slot 0 is the firmware-managed base backup: never erasable from the host. */
if (slot >= MB_SLOT_COUNT || slot == MB_SLOT_BACKUP)
return MB_ERR_SLOT;
const uint32_t base = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE;
@@ -660,7 +698,8 @@ uint8_t MB_SlotErase(uint8_t slot)
uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len)
{
if (slot >= MB_SLOT_COUNT)
/* Slot 0 is the firmware-managed base backup: never writable from the host. */
if (slot >= MB_SLOT_COUNT || slot == MB_SLOT_BACKUP)
return MB_ERR_SLOT;
if (len == 0)
return MB_OK;
@@ -676,3 +715,323 @@ uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_
return MB_OK;
}
/* -------------------------------------------------------------------------- */
/* Per-profile settings banks. */
/* */
/* The banking offset itself lives in the flash driver (PY25Q16_SetProfileBase);*/
/* here we only own the active-profile marker and the profile->base mapping. */
/* All external-flash only, never brick-critical. */
/* -------------------------------------------------------------------------- */
static uint32_t mb_crc32_bytes(const uint8_t *p, uint32_t len)
{
uint32_t crc = 0xFFFFFFFFu;
for (uint32_t i = 0; i < len; i++)
{
crc ^= p[i];
for (int k = 0; k < 8; k++)
crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
}
return crc ^ 0xFFFFFFFFu;
}
uint32_t MB_ProfileBase(uint8_t profile)
{
if (profile == 0)
return 0; /* profile 0 = historical config region */
if (profile < MB_PROFILE_COUNT)
return MB_PROFILE1_EXT_BASE + (uint32_t)(profile - 1u) * MB_PROFILE_BANK_SIZE;
return 0; /* out of range -> safe default */
}
static mb_mark_status_t mb_read_profile_copy(uint32_t base, mb_profile_state_t *st)
{
mb_spi_err = 0;
mb_ext_read(base, (uint8_t *)st, sizeof(*st));
if (mb_spi_err) return MB_MARK_IO;
if (st->magic == 0xFFFFFFFFu) return MB_MARK_MISSING;
if (st->magic == MB_PROFILE_LEGACY_MAGIC)
{
/* FMP1 was { magic, index, ~index, reserved[2] }. Preserve its slot
* choice long enough for boot resolution to migrate it to FMP2. */
const uint8_t legacy_index = ((const uint8_t *)st)[4];
const uint8_t legacy_inv = ((const uint8_t *)st)[5];
if ((uint8_t)~legacy_inv != legacy_index || legacy_index >= MB_PROFILE_COUNT)
return MB_MARK_CORRUPT;
memset(st, 0, sizeof(*st));
st->magic = MB_PROFILE_LEGACY_MAGIC;
st->index = legacy_index;
st->index_inv = legacy_inv;
return MB_MARK_LEGACY;
}
if (st->magic != MB_PROFILE_MAGIC) return MB_MARK_CORRUPT;
if ((uint8_t)~st->index_inv != st->index) return MB_MARK_CORRUPT;
if (st->index >= MB_PROFILE_COUNT) return MB_MARK_CORRUPT;
if (st->image_size == 0u ||
st->image_size > MB_INT_APP_SIZE) return MB_MARK_CORRUPT;
if (mb_crc32_bytes((const uint8_t *)st,
sizeof(*st) - sizeof(st->state_crc32)) != st->state_crc32)
return MB_MARK_CORRUPT;
return MB_MARK_VALID;
}
static bool mb_generation_newer(uint32_t a, uint32_t b)
{
return (int32_t)(a - b) > 0;
}
static mb_mark_status_t mb_read_active_profile(mb_profile_state_t *state,
uint32_t *state_base)
{
mb_profile_state_t a;
mb_profile_state_t b;
mb_mark_status_t sa = mb_read_profile_copy(MB_PROFILE_STATE_A_BASE, &a);
mb_mark_status_t sb = mb_read_profile_copy(MB_PROFILE_STATE_B_BASE, &b);
/* If either sector could not be read, it might contain the newest record.
* Do not silently select an older state and risk loading the wrong bank. */
if (sa == MB_MARK_IO || sb == MB_MARK_IO)
return MB_MARK_IO;
const mb_profile_state_t *chosen = NULL;
uint32_t chosen_base = 0;
if (sa == MB_MARK_VALID && sb == MB_MARK_VALID)
{
if (mb_generation_newer(b.generation, a.generation))
{
chosen = &b;
chosen_base = MB_PROFILE_STATE_B_BASE;
}
else
{
chosen = &a;
chosen_base = MB_PROFILE_STATE_A_BASE;
}
}
else if (sa == MB_MARK_VALID)
{
chosen = &a;
chosen_base = MB_PROFILE_STATE_A_BASE;
}
else if (sb == MB_MARK_VALID)
{
chosen = &b;
chosen_base = MB_PROFILE_STATE_B_BASE;
}
if (chosen)
{
if (state)
*state = *chosen;
if (state_base)
*state_base = chosen_base;
return MB_MARK_VALID;
}
/* A legacy record is usable for migration but has no firmware identity.
* Prefer A if both somehow exist; the next successful repair writes FMP2. */
if (sa == MB_MARK_LEGACY || sb == MB_MARK_LEGACY)
{
const bool use_b = sa != MB_MARK_LEGACY;
if (state)
*state = use_b ? b : a;
if (state_base)
*state_base = use_b ? MB_PROFILE_STATE_B_BASE : MB_PROFILE_STATE_A_BASE;
return MB_MARK_LEGACY;
}
return (sa == MB_MARK_MISSING && sb == MB_MARK_MISSING)
? MB_MARK_MISSING : MB_MARK_CORRUPT;
}
mb_mark_status_t MB_ReadActiveProfile(mb_profile_state_t *state)
{
return mb_read_active_profile(state, NULL);
}
uint8_t MB_GetActiveProfile(void)
{
mb_profile_state_t st;
mb_mark_status_t status = MB_ReadActiveProfile(&st);
return (status == MB_MARK_VALID || status == MB_MARK_LEGACY) ? st.index : 0;
}
uint8_t MB_SetActiveProfile(uint8_t profile)
{
mb_slot_header_t hdr;
mb_profile_state_t st;
mb_profile_state_t current;
uint32_t current_base = 0;
if (profile >= MB_PROFILE_COUNT)
return MB_ERR_SLOT;
uint8_t hdr_err = mb_read_header(profile, &hdr);
if (hdr_err != MB_OK)
return hdr_err;
mb_mark_status_t current_status = mb_read_active_profile(&current, &current_base);
if (current_status == MB_MARK_IO)
return MB_ERR_SPI;
memset(&st, 0, sizeof(st));
st.magic = MB_PROFILE_MAGIC;
st.generation = (current_status == MB_MARK_VALID) ? current.generation + 1u : 0u;
st.image_size = hdr.image_size;
st.image_crc32 = hdr.image_crc32;
st.index = profile;
st.index_inv = (uint8_t)~profile;
st.state_crc32 = mb_crc32_bytes((const uint8_t *)&st,
sizeof(st) - sizeof(st.state_crc32));
const uint32_t target_base = ((current_status == MB_MARK_VALID ||
current_status == MB_MARK_LEGACY) &&
current_base == MB_PROFILE_STATE_A_BASE)
? MB_PROFILE_STATE_B_BASE
: MB_PROFILE_STATE_A_BASE;
mb_spi_err = 0;
mb_spi_polled_mode();
if (!mb_ext_sector_erase(target_base))
return MB_ERR_SPI;
if (!mb_ext_program(target_base, (const uint8_t *)&st, sizeof(st)))
return MB_ERR_SPI;
/* Verify the new copy directly. The previous valid sector has not been
* touched, so any failure here remains power-loss safe. */
mb_profile_state_t chk;
mb_mark_status_t chk_status = mb_read_profile_copy(target_base, &chk);
if (chk_status == MB_MARK_IO) return MB_ERR_SPI;
if (chk_status != MB_MARK_VALID ||
memcmp(&chk, &st, sizeof(st)) != 0) return MB_ERR_CRC;
return MB_OK;
}
uint8_t MB_ProfileErase(uint8_t profile)
{
/* Profile 0 (the base config) is not resettable from the host: reset it by
* factory-resetting the running base firmware instead. Profiles 1..N live
* in their own 64 KiB banks, clear of the shared calibration at 0x010000. */
if (profile == 0 || profile >= MB_PROFILE_COUNT)
return MB_ERR_SLOT;
const uint32_t base = MB_ProfileBase(profile);
/* Invalidate before the first raw erase so an early failure cannot leave a
* cache entry referring to a sector that was already erased. */
PY25Q16_InvalidateCache();
mb_spi_err = 0;
mb_spi_polled_mode();
for (uint32_t off = 0; off < MB_PROFILE_BANK_SIZE; off += MB_EXT_SECTOR)
if (!mb_ext_sector_erase(base + off))
return MB_ERR_SPI;
return MB_OK;
}
/* -------------------------------------------------------------------------- */
/* Slot 0 self-backup (base firmware). */
/* -------------------------------------------------------------------------- */
/* Bounded copy of a NUL-terminated string into a fixed field, zero-padded. */
static void mb_copy_str(char *dst, uint8_t cap, const char *src)
{
uint8_t n = 0;
while (n + 1u < cap && src[n])
{
dst[n] = src[n];
n++;
}
while (n < cap)
dst[n++] = 0;
}
/* CRC-32 (zlib) of the internal application flash, which is memory-mapped so
* no SPI is involved. Same polynomial as the external slot CRC. */
static uint32_t mb_int_image_crc32(uint32_t len)
{
const uint8_t *p = (const uint8_t *)MB_INT_APP_BASE;
return mb_crc32_bytes(p, len);
}
static bool mb_internal_matches(uint32_t image_size, uint32_t image_crc32)
{
return mb_int_image_crc32(image_size) == image_crc32;
}
mb_fw_match_t MB_InternalMatchesSlot(uint8_t slot)
{
mb_slot_header_t hdr;
uint8_t err = mb_read_header(slot, &hdr);
if (err == MB_ERR_SPI)
return MB_FW_IO; /* couldn't read the header: never conclude mismatch */
if (err != MB_OK)
return MB_FW_MISMATCH; /* no valid header: definitely not this firmware */
return mb_internal_matches(hdr.image_size, hdr.image_crc32)
? MB_FW_MATCH : MB_FW_MISMATCH;
}
bool MB_InternalMatchesProfile(const mb_profile_state_t *state)
{
if (!state || state->image_size == 0u || state->image_size > MB_INT_APP_SIZE)
return false;
return mb_internal_matches(state->image_size, state->image_crc32);
}
uint8_t MB_BackupInternalToSlot0(mb_progress_fn progress)
{
const uint8_t *img = (const uint8_t *)MB_INT_APP_BASE;
const uint32_t size = MB_INT_APP_SIZE;
const uint32_t base = MB_SLOT0_EXT_BASE + (uint32_t)MB_SLOT_BACKUP * MB_SLOT_STRIDE;
mb_spi_err = 0;
mb_spi_polled_mode();
/* Erase the header sector + image area (rounded up to 4 KiB sectors). */
for (uint32_t off = 0; off < MB_SLOT_IMG_OFFSET + size; off += MB_EXT_SECTOR)
if (!mb_ext_sector_erase(base + off))
return MB_ERR_SPI;
/* Program the whole internal application region verbatim, in chunks,
* reporting progress. It is an exact copy of what executes, so restoring
* it later reproduces the running firmware bit-for-bit. */
for (uint32_t done = 0; done < size; )
{
uint32_t n = (size - done < MB_EXT_SECTOR) ? (size - done) : MB_EXT_SECTOR;
if (!mb_ext_program(base + MB_SLOT_IMG_OFFSET + done, img + done, n))
return MB_ERR_SPI;
done += n;
if (progress)
progress(done, size);
}
/* Validate the stored image before committing its header. Until that final
* header write the slot remains invalid, so a failed CRC or interrupted
* backup is guaranteed to retry at the next boot. */
const uint32_t image_crc = mb_int_image_crc32(size);
uint32_t ext_crc = mb_ext_image_crc32(base + MB_SLOT_IMG_OFFSET, size);
if (mb_spi_err) return MB_ERR_SPI;
if (ext_crc != image_crc) return MB_ERR_CRC;
/* Write the COMMITTED header only after the external image verified. */
mb_slot_header_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.magic = MB_SLOT_MAGIC;
hdr.hdr_version = MB_HDR_VERSION;
hdr.flags = MB_FLAG_COMMITTED;
hdr.image_size = size;
hdr.image_crc32 = image_crc;
mb_copy_str(hdr.name, MB_NAME_LEN, EDITION_STRING);
mb_copy_str(hdr.fw_version, MB_VERSION_LEN, VERSION_STRING_2);
if (!mb_ext_program(base, (const uint8_t *)&hdr, sizeof(hdr)))
return MB_ERR_SPI;
return MB_OK;
}
+128 -3
View File
@@ -43,11 +43,15 @@
#define MB_INT_APP_SIZE 0x0001D800u /* 118 KiB */
/* External SPI flash slot layout (see docs/multiboot-design.md).
* Each 128 KiB slot = one header sector (4 KiB) followed by the image. */
* Each 128 KiB slot = one header sector (4 KiB) followed by the image.
* Slot 0 is the firmware-managed BACKUP of the normally-flashed firmware
* (written by MB_BackupInternalToSlot0, protected from host writes); slots
* 1..4 are the user slots managed from UV Studio. */
#define MB_SLOT_STRIDE 0x00020000u /* 128 KiB per slot */
#define MB_SLOT_IMG_OFFSET 0x00001000u /* image starts after header sector */
#define MB_SLOT0_EXT_BASE 0x00020000u /* slot 0 header base */
#define MB_SLOT_COUNT 4u
#define MB_SLOT0_EXT_BASE 0x00020000u /* slot 0 (backup) header base */
#define MB_SLOT_COUNT 5u /* slot 0 backup + slots 1..4 */
#define MB_SLOT_BACKUP 0u /* slot 0 = firmware base backup */
/* Slot header (stored at the slot base, first 4 KiB sector). 64 bytes. */
#define MB_SLOT_MAGIC 0x31424D46u /* "FMB1" */
@@ -104,4 +108,125 @@ uint8_t MB_SlotInfo(uint8_t slot, mb_slot_header_t *out_header);
uint8_t MB_SlotErase(uint8_t slot);
uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len);
/* -------------------------------------------------------------------------- */
/* Per-profile settings banks. */
/* */
/* Each firmware slot gets its own copy of the user configuration (memory */
/* channels, names, VFOs, settings) so switching firmware no longer shares - */
/* or silently clobbers - settings between editions. The banking itself is a */
/* single address offset applied in the flash driver (see */
/* PY25Q16_SetProfileBase); everything below PY25Q16_PROFILE_SHARED_FROM */
/* (0x010000, the calibration boundary) is per-profile, everything at/above */
/* stays shared. Slot N is bound to profile N. Profile 0 (slot 0 = base */
/* backup) reuses the historical config region at 0x000000 - no migration. */
/* */
/* External SPI flash (PY25Q16, 2 MiB) map: */
/* 0x000000 profile 0 config (channels/settings) ] per-profile */
/* 0x010000 calibration (512 B) ] shared */
/* 0x011000 boot logo (4 KiB) ] shared */
/* 0x020000 slot 0 firmware (BACKUP, 128 KiB) ] firmware-managed */
/* 0x040000 slot 1 firmware (128 KiB) ] */
/* 0x060000 slot 2 firmware ] user (UV Studio) */
/* 0x080000 slot 3 firmware ] */
/* 0x0A0000 slot 4 firmware ] */
/* 0x0C0000 profile 1 config (64 KiB) ] */
/* 0x0D0000 profile 2 config (64 KiB) ] per-profile */
/* 0x0E0000 profile 3 config (64 KiB) ] */
/* 0x0F0000 profile 4 config (64 KiB) ] */
/* 0x100000 multiboot state A (4 KiB marker) ] shared */
/* 0x101000 multiboot state B (4 KiB marker) ] redundant */
/* 0x102000 -- free ~888 KiB -- */
/* 0x1E0000 RX/TX log (32 KiB) ] shared */
/* */
/* Banks are 64 KiB for headroom; the live config footprint is ~44 KiB (max */
/* physical config address 0x00A170). Keep the profile banks past the last */
/* slot if MB_SLOT_COUNT ever grows. */
#define MB_PROFILE_COUNT MB_SLOT_COUNT /* one profile per slot (0..4) */
#define MB_PROFILE_BANK_SIZE 0x00010000u /* 64 KiB per config bank */
#define MB_PROFILE1_EXT_BASE 0x000C0000u /* profiles 1..4, right after slot 4 */
#define MB_PROFILE_STATE_A_BASE 0x00100000u /* redundant marker sector A */
#define MB_PROFILE_STATE_B_BASE 0x00101000u /* redundant marker sector B */
/* Active-profile marker: two alternating external-flash sectors, never banked,
* outside the EEPROM logical map. A new record is verified in the inactive
* sector before it supersedes the previous one, so a power loss always leaves
* at least one usable state. The expected firmware identity is stored here as
* well: boot resolution never depends on the slot header remaining readable. */
#define MB_PROFILE_LEGACY_MAGIC 0x31504D46u /* "FMP1" (single 8-byte record) */
#define MB_PROFILE_MAGIC 0x32504D46u /* "FMP2" (redundant identity record) */
typedef struct __attribute__((packed)) {
uint32_t magic; /* MB_PROFILE_MAGIC */
uint32_t generation; /* monotonically increasing record version */
uint32_t image_size; /* expected internal image size */
uint32_t image_crc32; /* expected internal image CRC-32 */
uint8_t index; /* active profile 0..MB_PROFILE_COUNT-1 */
uint8_t index_inv; /* ~index, quick integrity check */
uint8_t reserved[2]; /* fixed zero for deterministic state CRC */
uint32_t state_crc32; /* CRC-32 over all preceding fields */
} mb_profile_state_t;
/* External-flash base of a profile's config bank. Profile 0 -> 0 (historical
* region, identity map); profiles 1..N -> past the slots. Out-of-range -> 0. */
uint32_t MB_ProfileBase(uint8_t profile);
/* Result of reading the active-profile marker. A boot must treat these very
* differently: MISSING = fresh radio (adopting the running firmware as Main is
* fine); IO / CORRUPT = uncertain (must NOT overwrite Main). */
typedef enum {
MB_MARK_VALID = 0, /* read OK, well-formed; *state populated */
MB_MARK_LEGACY, /* valid FMP1 index; identity not stored */
MB_MARK_MISSING, /* read OK but both sectors erased */
MB_MARK_CORRUPT, /* read OK but magic/integrity bad */
MB_MARK_IO, /* could not be read (SPI error) */
} mb_mark_status_t;
/* Read the newest valid active-profile marker, distinguishing the states above. */
mb_mark_status_t MB_ReadActiveProfile(mb_profile_state_t *state);
/* Convenience wrapper for non-critical callers (e.g. cursor pre-selection):
* the valid index, or 0 for anything not cleanly VALID. */
uint8_t MB_GetActiveProfile(void);
/* Write the active-profile marker into the inactive redundant sector and read it
* back to confirm it landed. The previous valid record is kept intact. The slot
* header supplies the expected internal firmware identity. */
uint8_t MB_SetActiveProfile(uint8_t profile);
/* Erase a profile's whole config bank (host "Reset config" for a user slot):
* the next boot on that slot reads 0xFF and re-seeds factory defaults. Profile 0
* (the base) is refused - reset it by factory-resetting the base firmware.
* External flash only, never brick-critical. */
uint8_t MB_ProfileErase(uint8_t profile);
/* -------------------------------------------------------------------------- */
/* Slot 0 self-backup (base firmware). */
/* -------------------------------------------------------------------------- */
/* Progress callback for the (slow) slot-0 self-backup; may be NULL. */
typedef void (*mb_progress_fn)(uint32_t done, uint32_t total);
/* Does the running internal firmware carry the DECLARED identity of `slot`
* (CRC32 over image_size == the slot header's image_crc32)? This checks the
* header's claim, not the stored image itself (that is validated once, right
* after a backup). IO is reported separately so a transient SPI error is never
* mistaken for a mismatch - which would wrongly trigger a self-backup over Main. */
typedef enum {
MB_FW_MATCH = 0, /* internal carries this slot's declared identity */
MB_FW_MISMATCH, /* reliably not this slot (or slot has no header) */
MB_FW_IO, /* slot header unreadable: uncertain */
} mb_fw_match_t;
mb_fw_match_t MB_InternalMatchesSlot(uint8_t slot);
/* Compare internal flash with the identity stored in a validated marker. */
bool MB_InternalMatchesProfile(const mb_profile_state_t *state);
/* Back up the running internal firmware into slot 0 (erase + full image +
* COMMITTED header, name=edition, version) and validate the stored image by a
* full external CRC read-back. External flash only, never brick-critical; a
* failure (SPI or CRC) leaves the slot uncommitted and does not advance the
* marker, so boot resolution retries it. progress may be NULL. */
uint8_t MB_BackupInternalToSlot0(mb_progress_fn progress);
#endif /* DRIVER_MB_FLASH_H */
+44 -2
View File
@@ -47,6 +47,30 @@ static uint8_t SectorCache[SECTOR_SIZE];
static uint8_t BlackHole[4] __attribute__((aligned(4)));
static volatile bool TC_Flag;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Active settings-profile base (see py25q16.h). 0 = profile 0 / historical
* config region, i.e. an identity mapping. */
static uint32_t ProfileBase = 0;
void PY25Q16_SetProfileBase(uint32_t Base)
{
ProfileBase = Base;
}
/* Redirect config-region accesses (addr < boundary) into the active bank.
* Calibration/logo/slots/marker (addr >= boundary) are returned unchanged.
* ProfileBase is sector-aligned, so alignment done by callers is preserved. */
static inline uint32_t ProfileMap(uint32_t Address)
{
return (Address < PY25Q16_PROFILE_SHARED_FROM) ? (Address + ProfileBase) : Address;
}
#else
static inline uint32_t ProfileMap(uint32_t Address)
{
return Address;
}
#endif
static inline void CS_Assert()
{
GPIO_ResetOutputPin(CS_PIN);
@@ -222,6 +246,7 @@ static void WriteEnable();
static void SectorErase(uint32_t Addr);
static void SectorProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size);
static void PageProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size);
static void ReadBufferRaw(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_Init()
{
@@ -229,7 +254,7 @@ void PY25Q16_Init()
SPI_Init();
}
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
static void ReadBufferRaw(uint32_t Address, void *pBuffer, uint32_t Size)
{
MBMARK("RD cmd"); // about to assert CS + send read command
CS_Assert();
@@ -259,6 +284,11 @@ void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
CS_Release();
}
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
{
ReadBufferRaw(ProfileMap(Address), pBuffer, Size);
}
// Like PY25Q16_ReadBuffer, but waits for the flash to be idle first (WIP=0),
// exactly as PY25Q16_WriteBuffer does before its internal reads. A standalone
// read issued while the chip is still busy from a prior program/erase never
@@ -273,6 +303,8 @@ void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size)
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append)
{
Address = ProfileMap(Address); /* map once; internal reads use *Raw below */
#ifdef DEBUG
printf("spi flash write: %06x %ld %d\n", Address, Size, Append);
#endif
@@ -298,7 +330,9 @@ void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, b
if (SecAddr != SectorCacheAddr)
{
PY25Q16_ReadBuffer(SecAddr, SectorCache, SECTOR_SIZE);
/* SecAddr is already in mapped space (Address was mapped above), so
* read raw to avoid mapping a second time. */
ReadBufferRaw(SecAddr, SectorCache, SECTOR_SIZE);
SectorCacheAddr = SecAddr;
}
@@ -358,6 +392,7 @@ void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, b
void PY25Q16_SectorErase(uint32_t Address)
{
Address = ProfileMap(Address);
Address -= (Address % SECTOR_SIZE);
SectorErase(Address);
if (SectorCacheAddr == Address)
@@ -366,6 +401,13 @@ void PY25Q16_SectorErase(uint32_t Address)
}
}
void PY25Q16_InvalidateCache(void)
{
/* Same "no sector cached" sentinel as the initial value: the next write
* re-reads its sector from flash instead of trusting SectorCache. */
SectorCacheAddr = 0x1000000;
}
static inline void WriteAddr(uint32_t Addr)
{
SPI_WriteByte(0xff & (Addr >> 16));
+29
View File
@@ -26,4 +26,33 @@ void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append);
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. */
void PY25Q16_InvalidateCache(void);
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/*
* Multiboot per-profile config banking.
*
* Each firmware slot owns a private copy of the user configuration (memory
* channels, names, VFOs, settings). A non-zero profile base transparently
* shifts every flash access BELOW PY25Q16_PROFILE_SHARED_FROM into the active
* profile's bank; calibration, boot logo, firmware slots and the multiboot
* marker all live at/above that boundary and stay shared across every profile.
*
* This is the single choke point: both the EEPROM emulation (eeprom_compat.c)
* and the firmware's direct config reads/writes (settings.c) end up here, so
* one offset covers them all - no per-call-site patching.
*
* Set once at boot, before any settings read, from
* PY25Q16_SetProfileBase(MB_ProfileBase(MB_BootResolveProfile()));
* and never changed again during a session (the profile only changes through a
* reflash + reset), so the banking itself never needs a cache flush. Raw profile
* erases behind the driver explicitly call PY25Q16_InvalidateCache().
*/
#define PY25Q16_PROFILE_SHARED_FROM 0x00010000u /* calibration boundary (see flash map) */
void PY25Q16_SetProfileBase(uint32_t Base);
#endif
#endif
+12
View File
@@ -52,6 +52,10 @@
#include "driver/system.h"
#include "driver/systick.h"
#include "driver/py25q16.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#include "ui/multiboot.h"
#endif
#ifdef ENABLE_UART
#include "driver/uart.h"
#endif
@@ -81,6 +85,14 @@ void Main(void)
SYSTICK_Init();
BOARD_Init();
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Resolve the active settings profile BEFORE any EEPROM/settings access
* below. This also adopts a normally-flashed firmware as slot 0 (discreet
* self-backup) when the running image isn't the slot the marker points to.
* Calibration stays shared regardless of the selected profile. */
PY25Q16_SetProfileBase(MB_ProfileBase(MB_BootResolveProfile()));
#endif
boot_counter_10ms = 250; // 2.5 sec
#ifdef ENABLE_UART
+151 -12
View File
@@ -75,6 +75,13 @@ static void mb_status_bar(void)
{
UI_StatusClear();
GUI_DisplaySmallestInverse("F4HWN MULTIBOOT", 34, 0, true, true, 94);
/* Thin line dressing up the otherwise blank row between the status bar and
* the first content row. Drawn on gFrameBuffer[0] (line 0), which every
* multiboot screen leaves blank, so it shows on all of them. Bit 3 (~mid of
* the row) stays clear of the selected-slot capsule's top edge (bit 7). */
for (uint8_t x = 2u; x < LCD_WIDTH - 2u; x++)
gFrameBuffer[0][x] |= 0x08u;
}
/* Bottom key-hint line: each key name as an inverse 3x5 capsule label, its
@@ -175,7 +182,9 @@ static void mb_render_slots(uint8_t selected,
memset(line, 0, sizeof(line));
memset(version, 0, sizeof(version));
line[0] = (char)('0' + slot);
/* Slot 0 is the auto-backed-up main firmware: label it 'M' (Main) so it
* reads apart from the numbered user slots 1..4. */
line[0] = (slot == 0u) ? 'M' : (char)('0' + slot);
line[1] = ' ';
line[2] = ' ';
@@ -220,16 +229,17 @@ static void mb_render_slots(uint8_t selected,
ST7565_BlitFullScreen();
}
static void mb_prepare_progress(uint8_t slot)
/* Both restore and initial Main backup use the exact same progress frame.
* Keep it out-of-line: each caller has different text, but duplicating the
* framebuffer setup and the two LCD blits only wastes MCU flash. */
__attribute__((noinline)) static void mb_prepare_progress_screen(const char *title,
const char *detail)
{
char title[] = "Restore slot 0";
title[13] = (char)('0' + slot);
UI_DisplayClear();
mb_status_bar();
UI_PrintStringSmallNormal(title, 2, 126, 1);
UI_PrintStringSmallNormal("DO NOT POWER OFF", 2, 126, 3);
UI_PrintStringSmallNormal("Writing & Verify", 2, 126, 5);
UI_PrintStringSmallNormal(detail, 2, 126, 5);
/* Same rounded outline and hatch pattern as the scan progress gauge. */
gFrameBuffer[6][3] = 0x0Cu;
@@ -242,10 +252,48 @@ static void mb_prepare_progress(uint8_t slot)
ST7565_BlitFullScreen();
}
static void mb_prepare_progress(uint8_t slot)
{
char slot_title[] = "Restore slot 0";
slot_title[13] = (char)('0' + slot);
const char *title = (slot == 0u) ? "Restore Main" : slot_title;
mb_prepare_progress_screen(title, "Writing & Verify");
}
/* Discreet "Main backup" screen shown once, at the first boot after a normal
* Flash-Firmware install, while the running firmware is copied into slot 0. */
static void mb_backup_prepare(void)
{
mb_prepare_progress_screen("Saving Main", "Slot Main");
}
static void mb_backup_progress(uint32_t done, uint32_t total)
{
uint32_t cols = total ? (done * 118u / total) : 118u;
if (cols > 118u)
cols = 118u;
for (uint32_t i = 0; i < cols; i++)
gFrameBuffer[6][5u + i] = 0x2Du;
ST7565_BlitFullScreen();
}
/* With no trustworthy profile, continuing would let normal boot-time settings
* writes modify an arbitrary bank. Keep the radio in a read-only error state;
* a power cycle can recover from a transient SPI fault. */
__attribute__((noreturn)) static void mb_profile_error_halt(void)
{
BACKLIGHT_TurnOn();
mb_show_message("PROFILE ERROR", "Flash state unknown", "Restart radio");
for (;;)
SYSTEM_DelayMs(100);
}
static void mb_confirm_screen(uint8_t slot)
{
char title[] = "Restore slot 0?";
title[13] = (char)('0' + slot);
char slot_title[] = "Restore slot 0?";
slot_title[13] = (char)('0' + slot);
const char *title = (slot == 0u) ? "Restore Main?" : slot_title;
UI_DisplayClear();
mb_status_bar();
@@ -268,12 +316,19 @@ void UI_MultibootSelector(void)
mb_wait_release();
mb_scan_slots(headers, status);
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
/* Pre-select the firmware currently running (its slot, from the marker), so
* the cursor lands on "where you are". If that slot isn't restorable (erased,
* bad CRC...), fall back to the first valid slot. */
selected = MB_GetActiveProfile();
if (selected >= MB_SLOT_COUNT || status[selected] != MB_OK)
{
if (status[slot] == MB_OK)
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
selected = slot;
break;
if (status[slot] == MB_OK)
{
selected = slot;
break;
}
}
}
@@ -316,6 +371,17 @@ void UI_MultibootSelector(void)
if (key != KEY_MENU)
continue;
/* Bind this slot to its own settings profile BEFORE reflashing. The
* write is verified (read-back); if it can't be confirmed we must NOT
* reflash - otherwise the next boot could resolve to the wrong profile
* (e.g. when two slots hold the same firmware image). */
if (MB_SetActiveProfile(selected) != MB_OK)
{
mb_show_message("PROFILE ERROR", "Marker not saved", "Press any key");
(void)mb_get_key();
continue;
}
mb_prepare_progress(selected);
uint8_t err = MB_RestoreSlot(selected, gFrameBuffer[6]);
@@ -326,3 +392,76 @@ void UI_MultibootSelector(void)
mb_scan_slots(headers, status);
}
}
/* Adopt the running internal firmware as Main: back it up into slot 0 and point
* the marker at profile 0. Reached when the firmware was installed outside
* multiboot (fresh radio, or a plain Flash-Firmware). */
static uint8_t mb_adopt_internal_as_main(void)
{
BACKLIGHT_TurnOn();
mb_backup_prepare();
if (MB_BackupInternalToSlot0(mb_backup_progress) == MB_OK)
(void)MB_SetActiveProfile(MB_SLOT_BACKUP);
return MB_SLOT_BACKUP;
}
uint8_t MB_BootResolveProfile(void)
{
mb_profile_state_t mark;
mb_mark_status_t ms = MB_MARK_IO;
for (uint8_t retry = 0; retry < 3u && ms == MB_MARK_IO; retry++)
{
ms = MB_ReadActiveProfile(&mark);
if (ms == MB_MARK_IO)
SYSTEM_DelayMs(10);
}
/* A reliably-read marker is authoritative: it carries the expected internal
* identity, so we don't even need the slot header. */
if (ms == MB_MARK_VALID)
{
if (MB_InternalMatchesProfile(&mark))
return mark.index; /* running the slot the marker names */
/* Marker read fine but internal no longer carries its identity -> the
* firmware was replaced outside multiboot (a plain Flash-Firmware). Adopt
* it as Main. Deliberately NOT a content scan here: a build that merely
* duplicates a user slot (or a marker that already points at such a slot)
* must still refresh Main. */
return mb_adopt_internal_as_main();
}
/* Marker unreliable (MISSING / LEGACY / CORRUPT / IO): identify the running
* firmware by content, and never destroy Main on uncertainty - internal is
* adopted only when it matches no slot AND every read was clean, so a
* transient SPI error or a half-written marker can never destroy Main. */
bool had_io = false;
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
mb_fw_match_t m = MB_FW_IO;
for (uint8_t retry = 0; retry < 3u && m == MB_FW_IO; retry++)
m = MB_InternalMatchesSlot(slot);
if (m == MB_FW_MATCH)
{
(void)MB_SetActiveProfile(slot); /* record/repair the marker */
return slot;
}
if (m == MB_FW_IO)
had_io = true;
}
if (had_io || ms == MB_MARK_IO || ms == MB_MARK_CORRUPT)
{
/* Halt to protect an existing Main while the flash state is uncertain;
* but if slot 0 holds no valid backup there is nothing to protect, so
* fall through and adopt instead of bricking a first boot. */
uint8_t main_status = MB_SlotInfo(MB_SLOT_BACKUP, NULL);
bool main_exists = (main_status != MB_ERR_MAGIC &&
main_status != MB_ERR_NOT_COMMITTED);
if (main_exists)
mb_profile_error_halt();
}
return mb_adopt_internal_as_main();
}
+8
View File
@@ -5,8 +5,16 @@
#ifndef UI_MULTIBOOT_H
#define UI_MULTIBOOT_H
#include <stdint.h>
/* Blocking boot-time slot selector. Returns only when the user chooses EXIT;
* a successful restore resets the radio from the RAM-resident copier. */
void UI_MultibootSelector(void);
/* Resolve the active settings profile at boot, BEFORE any settings read.
* Detects a firmware installed outside multiboot (internal != slot[marker]) and,
* if so, discreetly self-backs it up into slot 0 and adopts profile 0. Returns
* the profile index (0..MB_SLOT_COUNT-1) to feed PY25Q16_SetProfileBase(). */
uint8_t MB_BootResolveProfile(void);
#endif