Add SetCfg to pick a config bank independently of the slot

This commit is contained in:
Armel FAUVEAU committed 2026-08-21 02:56:54 +02:00
1 parent 5b6174085b
commit e4a645a5c7
12 files changed
+502 -243

No files matched your search

+3
View File
@@ -241,6 +241,9 @@ enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT
driver/mb_flash.c
ui/multiboot.c
)
if(ENABLE_FEAT_F4HWN_MULTIBOOT AND NOT ENABLE_FEAT_F4HWN)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT requires ENABLE_FEAT_F4HWN (the SetCfg menu, SysInfo badge and multiboot UI all live under ENABLE_FEAT_F4HWN).")
endif()
enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY)
if(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT)
+54
View File
@@ -30,6 +30,10 @@
#include "driver/eeprom.h"
#include "driver/gpio.h"
#include "driver/keyboard.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#include "ui/multiboot.h"
#endif
#include "frequencies.h"
#include "helper/battery.h"
#include "misc.h"
@@ -216,6 +220,13 @@ int MENU_GetLimits(uint8_t menu_id, int32_t *pMin, int32_t *pMax)
*pMax = ARRAY_SIZE(gSubMenu_RESET) - 1;
break;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
case MENU_SET_CFG:
//*pMin = 0;
*pMax = MB_BANK_COUNT - 1;
break;
#endif
case MENU_COMPAND:
case MENU_ABR_ON_TX_RX:
//*pMin = 0;
@@ -1092,6 +1103,12 @@ void MENU_ShowCurrentSetting(void)
gSubMenuSelection = 0;
break;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
case MENU_SET_CFG:
gSubMenuSelection = MB_GetActiveBank();
break;
#endif
case MENU_R_DCS:
case MENU_R_CTCS:
{
@@ -2006,6 +2023,9 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
if (m == MENU_RESET ||
m == MENU_MEM_CH ||
m == MENU_DEL_CH ||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
m == MENU_SET_CFG ||
#endif
m == MENU_MEM_NAME)
{
switch (gAskForConfirmation)
@@ -2034,6 +2054,40 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
NVIC_SystemReset();
#endif
}
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
else if (m == MENU_SET_CFG)
{
/* Bind the chosen config bank, then reboot so it is mapped
* before any settings are read. Confirming the current bank
* is a no-op: do not wear a marker sector or reboot. */
if (gSubMenuSelection == MB_GetActiveBank())
{
gFlagAcceptSetting = false;
gIsInSubMenu = false;
gAskForConfirmation = 0;
SCANNER_Stop();
return;
}
const uint8_t err = MB_SetActiveBank(gSubMenuSelection);
if (err != MB_OK)
{
/* The previous redundant marker remains authoritative.
* Explain the failure and keep the selector open. */
UI_MultibootShowConfigError(err);
gAskForConfirmation = 0;
gRequestDisplayScreen = DISPLAY_MENU;
SCANNER_Stop();
return;
}
#if defined(ENABLE_OVERLAY)
overlay_FLASH_RebootToBootloader();
#else
NVIC_SystemReset();
#endif
}
#endif
gFlagAcceptSetting = true;
gIsInSubMenu = false;
+5 -5
View File
@@ -985,24 +985,24 @@ void UART_HandleCommand(uint32_t Port)
break;
}
case 0x0728: // profile config reset: wipe the 64 KiB config bank of a slot
case 0x0728: // config reset: wipe the 64 KiB of a config bank (1..4)
{
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint8_t bank = 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);
? MB_ERR_AUTH : MB_BankErase(bank);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Bank; // echoes the erased bank (same wire layout as slot replies)
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0729;
Reply.Header.Size = 2;
Reply.Slot = slot;
Reply.Bank = bank;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
+159 -83
View File
@@ -1,4 +1,5 @@
/* Copyright 2026 F4HWN
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -775,13 +776,20 @@ uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_
}
/* -------------------------------------------------------------------------- */
/* Per-profile settings banks. */
/* Config banks and the active-state marker. */
/* */
/* The banking offset itself lives in the flash driver (PY25Q16_SetProfileBase);*/
/* here we only own the active-profile marker and the profile->base mapping. */
/* The banking offset itself lives in the flash driver (PY25Q16_SetBankBase); */
/* here we only own the active-state marker and the bank -> base mapping. */
/* All external-flash only, never brick-critical. */
/* -------------------------------------------------------------------------- */
_Static_assert(sizeof(mb_state_t) == 24u,
"FMP2/FMP3 marker layout must stay 24 bytes");
_Static_assert(offsetof(mb_state_t, firmware_slot) == 16u,
"FMP2 migration reads the coupled index at byte 16");
_Static_assert(offsetof(mb_state_t, state_crc32) == 20u,
"state CRC must cover the first 20 bytes (magic..bank_inv)");
static uint32_t mb_crc32_bytes(const uint8_t *p, uint32_t len)
{
uint32_t crc = 0xFFFFFFFFu;
@@ -795,38 +803,72 @@ static uint32_t mb_crc32_bytes(const uint8_t *p, uint32_t len)
return crc ^ 0xFFFFFFFFu;
}
uint32_t MB_ProfileBase(uint8_t profile)
uint32_t MB_BankBase(uint8_t bank)
{
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;
if (bank == 0)
return 0; /* bank 0 = historical config region */
if (bank < MB_BANK_COUNT)
return MB_BANK1_EXT_BASE + (uint32_t)(bank - 1u) * MB_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)
static mb_mark_status_t mb_read_state_copy(uint32_t base, mb_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)
if (st->magic == MB_STATE_LEGACY_MAGIC)
{
/* FMP1 was { magic, index, ~index, reserved[2] }. Preserve its slot
* choice long enough for boot resolution to migrate it to FMP2. */
/* FMP1 was { magic, index, ~index, reserved[2] }, with index coupling the
* firmware slot and the config bank. Keep it as BOTH fields so boot
* resolution can honour that choice before migrating the record to FMP3. */
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)
if ((uint8_t)~legacy_inv != legacy_index || legacy_index >= MB_BANK_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;
st->magic = MB_STATE_LEGACY_MAGIC;
st->firmware_slot = legacy_index;
st->slot_inv = legacy_inv;
st->config_bank = legacy_index;
st->bank_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->magic == MB_STATE_V2_MAGIC)
{
/* FMP2 stored one index for both the firmware slot and config bank at
* byte 16, followed by its inverse and two zeroed reserved bytes.
* Validate the on-flash record before normalizing it in RAM to FMP3. */
const uint8_t legacy_index = ((const uint8_t *)st)[16];
const uint8_t legacy_inv = ((const uint8_t *)st)[17];
if ((uint8_t)~legacy_inv != legacy_index ||
legacy_index >= MB_BANK_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;
st->magic = MB_STATE_MAGIC;
st->firmware_slot = legacy_index;
st->slot_inv = (uint8_t)~legacy_index;
st->config_bank = legacy_index;
st->bank_inv = (uint8_t)~legacy_index;
st->state_crc32 = mb_crc32_bytes((const uint8_t *)st,
sizeof(*st) - sizeof(st->state_crc32));
return MB_MARK_VALID;
}
if (st->magic != MB_STATE_MAGIC) return MB_MARK_CORRUPT;
if ((uint8_t)~st->slot_inv != st->firmware_slot)
return MB_MARK_CORRUPT;
if (st->firmware_slot >= MB_SLOT_COUNT) return MB_MARK_CORRUPT;
if ((uint8_t)~st->bank_inv != st->config_bank)
return MB_MARK_CORRUPT;
if (st->config_bank >= MB_BANK_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,
@@ -840,43 +882,43 @@ 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)
static mb_mark_status_t mb_read_active_state(mb_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);
mb_state_t a;
mb_state_t b;
mb_mark_status_t sa = mb_read_state_copy(MB_STATE_A_BASE, &a);
mb_mark_status_t sb = mb_read_state_copy(MB_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;
const mb_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;
chosen_base = MB_STATE_B_BASE;
}
else
{
chosen = &a;
chosen_base = MB_PROFILE_STATE_A_BASE;
chosen_base = MB_STATE_A_BASE;
}
}
else if (sa == MB_MARK_VALID)
{
chosen = &a;
chosen_base = MB_PROFILE_STATE_A_BASE;
chosen_base = MB_STATE_A_BASE;
}
else if (sb == MB_MARK_VALID)
{
chosen = &b;
chosen_base = MB_PROFILE_STATE_B_BASE;
chosen_base = MB_STATE_B_BASE;
}
if (chosen)
@@ -889,14 +931,14 @@ static mb_mark_status_t mb_read_active_profile(mb_profile_state_t *state,
}
/* A legacy record is usable for migration but has no firmware identity.
* Prefer A if both somehow exist; the next successful repair writes FMP2. */
* Prefer A if both somehow exist; the next successful repair writes FMP3. */
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;
*state_base = use_b ? MB_STATE_B_BASE : MB_STATE_A_BASE;
return MB_MARK_LEGACY;
}
@@ -904,79 +946,113 @@ static mb_mark_status_t mb_read_active_profile(mb_profile_state_t *state,
? MB_MARK_MISSING : MB_MARK_CORRUPT;
}
mb_mark_status_t MB_ReadActiveProfile(mb_profile_state_t *state)
mb_mark_status_t MB_ReadActiveState(mb_state_t *state)
{
return mb_read_active_profile(state, NULL);
return mb_read_active_state(state, NULL);
}
uint8_t MB_GetActiveProfile(void)
/* Commit a marker whose identity, firmware_slot and config_bank are set:
* fill the volatile fields (magic, generation, inverse bytes, state CRC), pick
* the inactive redundant sector, program it and read it back to confirm. The
* previous valid record is left untouched until the new one verifies, so this is
* power-loss safe. `current`/`current_base` come from mb_read_active_state. */
static uint8_t mb_commit_state(mb_state_t *st,
mb_mark_status_t current_status,
const mb_state_t *current,
uint32_t current_base)
{
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));
st->magic = MB_STATE_MAGIC;
st->generation = (current_status == MB_MARK_VALID) ? current->generation + 1u : 0u;
st->slot_inv = (uint8_t)~st->firmware_slot;
st->bank_inv = (uint8_t)~st->config_bank;
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;
current_base == MB_STATE_A_BASE)
? MB_STATE_B_BASE
: MB_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)))
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);
mb_state_t chk;
mb_mark_status_t chk_status = mb_read_state_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;
memcmp(&chk, st, sizeof(*st)) != 0) return MB_ERR_CRC;
return MB_OK;
}
uint8_t MB_ProfileErase(uint8_t profile)
uint8_t MB_SetActiveSlot(uint8_t slot)
{
/* 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)
mb_slot_header_t hdr;
mb_state_t st;
mb_state_t current;
uint32_t current_base = 0;
if (slot >= MB_SLOT_COUNT)
return MB_ERR_SLOT;
const uint32_t base = MB_ProfileBase(profile);
uint8_t hdr_err = mb_read_header(slot, &hdr);
if (hdr_err != MB_OK)
return hdr_err;
mb_mark_status_t current_status = mb_read_active_state(&current, &current_base);
if (current_status == MB_MARK_IO)
return MB_ERR_SPI;
memset(&st, 0, sizeof(st));
st.image_size = hdr.image_size; /* expect this slot's image (reflash) */
st.image_crc32 = hdr.image_crc32;
st.firmware_slot = slot;
st.config_bank = slot;
return mb_commit_state(&st, current_status, &current, current_base);
}
uint8_t MB_SetActiveBank(uint8_t bank)
{
mb_state_t st;
mb_state_t current;
uint32_t current_base = 0;
if (bank >= MB_BANK_COUNT)
return MB_ERR_SLOT;
/* Keep the running firmware's identity from the current marker so switching a
* bank never looks like an out-of-multiboot firmware change at the next boot.
* A valid marker is required for that identity - every normal boot leaves one. */
mb_mark_status_t current_status = mb_read_active_state(&current, &current_base);
if (current_status == MB_MARK_IO)
return MB_ERR_SPI;
if (current_status != MB_MARK_VALID)
return MB_ERR_MAGIC;
memset(&st, 0, sizeof(st));
st.image_size = current.image_size; /* keep the running firmware's identity */
st.image_crc32 = current.image_crc32;
st.firmware_slot = current.firmware_slot;
st.config_bank = bank;
return mb_commit_state(&st, current_status, &current, current_base);
}
uint8_t MB_BankErase(uint8_t bank)
{
/* Bank 0 (the base config) is not resettable from the host: reset it by
* factory-resetting the running base firmware instead. Banks 1..N live
* in their own 64 KiB banks, clear of the shared calibration at 0x010000. */
if (bank == 0 || bank >= MB_BANK_COUNT)
return MB_ERR_SLOT;
const uint32_t base = MB_BankBase(bank);
/* Invalidate before the first raw erase so an early failure cannot leave a
* cache entry referring to a sector that was already erased. */
@@ -984,7 +1060,7 @@ uint8_t MB_ProfileErase(uint8_t profile)
mb_spi_err = 0;
mb_spi_polled_mode();
for (uint32_t off = 0; off < MB_PROFILE_BANK_SIZE; off += MB_EXT_SECTOR)
for (uint32_t off = 0; off < MB_BANK_SIZE; off += MB_EXT_SECTOR)
if (!mb_ext_sector_erase(base + off))
return MB_ERR_SPI;
@@ -1035,7 +1111,7 @@ mb_fw_match_t MB_InternalMatchesSlot(uint8_t slot)
? MB_FW_MATCH : MB_FW_MISMATCH;
}
bool MB_InternalMatchesProfile(const mb_profile_state_t *state)
bool MB_InternalMatchesState(const mb_state_t *state)
{
if (!state || state->image_size == 0u || state->image_size > MB_INT_APP_SIZE)
return false;
+73 -61
View File
@@ -1,4 +1,5 @@
/* Copyright 2026 F4HWN
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,8 +26,6 @@
* can never brick the radio. There is a single firmware for both the K1 and the
* K5v3 (the keypad difference is handled at runtime by the hidden SetNav menu),
* so no per-model guard is needed.
*
* See docs/multiboot-design.md for the overall design.
*/
#ifndef DRIVER_MB_FLASH_H
@@ -42,7 +41,7 @@
#define MB_INT_APP_BASE 0x08002800u
#define MB_INT_APP_SIZE 0x0001D800u /* 118 KiB */
/* External SPI flash slot layout (see docs/multiboot-design.md).
/* External SPI flash slot layout.
* 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
@@ -80,7 +79,7 @@ enum {
MB_ERR_SIZE, /* image_size out of range */
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_SLOT, /* slot or bank index out of range */
MB_ERR_AUTH, /* write refused: timestamp mismatch */
MB_ERR_RAM_LOAD /* restore stub RAM copy mismatch */
};
@@ -112,95 +111,108 @@ 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. */
/* Config banks (one per slot by default, switchable via SetCfg). */
/* */
/* 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. */
/* Each firmware slot gets its own config bank by default (memory channels, */
/* names, VFOs, settings) so switching firmware does not implicitly share or */
/* clobber settings between editions. SetCfg deliberately lets the user pick */
/* another bank after confirmation; compatibility is then the user's concern. */
/* The banking itself is a single address offset applied in the flash driver */
/* (PY25Q16_SetBankBase): everything below PY25Q16_BANK_SHARED_FROM */
/* (0x010000, the calibration boundary) is per-bank, at/above stays shared. */
/* Restoring slot N selects bank N initially, but slot and bank are tracked */
/* independently afterwards. Bank 0 reuses the historical config region at */
/* 0x000000 - no migration. */
/* */
/* External SPI flash (PY25Q16, 2 MiB) map: */
/* 0x000000 profile 0 config (channels/settings) ] per-profile */
/* 0x000000 bank 0 config (channels/settings) ] per-bank */
/* 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) ] */
/* 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 bank 1 config (64 KiB) ] */
/* 0x0D0000 bank 2 config (64 KiB) ] per-bank */
/* 0x0E0000 bank 3 config (64 KiB) ] */
/* 0x0F0000 bank 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 */
/* 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 */
/* physical config address 0x00A170). Keep the config 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 */
#define MB_BANK_COUNT MB_SLOT_COUNT /* selectable config banks (0..4) */
#define MB_BANK_SIZE 0x00010000u /* 64 KiB per config bank */
#define MB_BANK1_EXT_BASE 0x000C0000u /* banks 1..4, right after slot 4 */
#define MB_STATE_A_BASE 0x00100000u /* redundant marker sector A */
#define MB_STATE_B_BASE 0x00101000u /* redundant marker sector B */
/* Active-profile marker: two alternating external-flash sectors, never banked,
/* Active-state 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) */
#define MB_STATE_LEGACY_MAGIC 0x31504D46u /* "FMP1" (single 8-byte record) */
#define MB_STATE_V2_MAGIC 0x32504D46u /* "FMP2" (slot == config bank) */
#define MB_STATE_MAGIC 0x33504D46u /* "FMP3" (slot + bank separated) */
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;
uint32_t magic; /* MB_STATE_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 firmware_slot; /* exact source slot of the running image */
uint8_t slot_inv; /* ~firmware_slot, quick integrity check */
uint8_t config_bank; /* active config bank 0..MB_BANK_COUNT-1 */
uint8_t bank_inv; /* ~config_bank, quick integrity check */
uint32_t state_crc32; /* CRC-32 over all preceding fields */
} mb_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);
/* External-flash base of a config bank. Bank 0 -> 0 (historical
* region, identity map); banks 1..N -> past the slots. Out-of-range -> 0. */
uint32_t MB_BankBase(uint8_t bank);
/* Result of reading the active-profile marker. A boot must treat these very
/* Result of reading the active-state 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_VALID = 0, /* valid FMP2/FMP3; *state normalized to FMP3 */
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);
/* Read the newest valid active-state marker, distinguishing the states above. */
mb_mark_status_t MB_ReadActiveState(mb_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
/* Write the active-state 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);
* header supplies the expected internal firmware identity. Use this right before
* reflashing to `slot`: both the exact firmware slot and the initial config
* bank become `slot`. */
uint8_t MB_SetActiveSlot(uint8_t slot);
/* 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
/* Switch ONLY the active settings bank, keeping the firmware that is running.
* Unlike MB_SetActiveSlot (which records that slot's image as the
* expected identity, for the imminent reflash to that slot), this preserves the
* running firmware's identity taken from the current marker and changes only the
* config bank. The next boot therefore maps a different bank with no
* reflash and is never mistaken for an out-of-multiboot firmware change (which
* would self-backup + reset to bank 0). Requires a currently valid marker -
* what every normal boot leaves behind - else MB_ERR_SPI / MB_ERR_MAGIC. The
* caller resets the MCU afterwards; the new bank takes effect at the next boot. */
uint8_t MB_SetActiveBank(uint8_t bank);
/* Erase a whole config bank (host "Reset config" for a user slot):
* the next boot using that bank reads 0xFF and re-seeds factory defaults. Bank 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);
uint8_t MB_BankErase(uint8_t bank);
/* -------------------------------------------------------------------------- */
/* Slot 0 self-backup (base firmware). */
@@ -223,7 +235,7 @@ typedef enum {
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);
bool MB_InternalMatchesState(const mb_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
+11 -11
View File
@@ -56,24 +56,24 @@ 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
/* Active settings-bank base (see py25q16.h). 0 = bank 0 / historical
* config region, i.e. an identity mapping. */
static uint32_t ProfileBase = 0;
static uint32_t BankBase = 0;
void PY25Q16_SetProfileBase(uint32_t Base)
void PY25Q16_SetBankBase(uint32_t Base)
{
ProfileBase = Base;
BankBase = 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)
* BankBase is sector-aligned, so alignment done by callers is preserved. */
static inline uint32_t BankMap(uint32_t Address)
{
return (Address < PY25Q16_PROFILE_SHARED_FROM) ? (Address + ProfileBase) : Address;
return (Address < PY25Q16_BANK_SHARED_FROM) ? (Address + BankBase) : Address;
}
#else
static inline uint32_t ProfileMap(uint32_t Address)
static inline uint32_t BankMap(uint32_t Address)
{
return Address;
}
@@ -294,7 +294,7 @@ static void ReadBufferRaw(uint32_t Address, void *pBuffer, uint32_t Size)
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
{
ReadBufferRaw(ProfileMap(Address), pBuffer, Size);
ReadBufferRaw(BankMap(Address), pBuffer, Size);
}
// Like PY25Q16_ReadBuffer, but waits for the flash to be idle first (WIP=0),
@@ -311,7 +311,7 @@ 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 */
Address = BankMap(Address); /* map once; internal reads use *Raw below */
#ifdef DEBUG
printf("spi flash write: %06x %ld %d\n", Address, Size, Append);
@@ -400,7 +400,7 @@ void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, b
void PY25Q16_SectorErase(uint32_t Address)
{
Address = ProfileMap(Address);
Address = BankMap(Address);
Address -= (Address % SECTOR_SIZE);
SectorErase(Address);
if (SectorCacheAddr == Address)
+15 -13
View File
@@ -27,33 +27,35 @@ void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, b
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
* flash behind the driver's back (e.g. the raw multiboot slot/bank ops) so a
* 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
/*
* Multiboot per-profile config banking.
* Multiboot per-bank 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.
* Each firmware slot gets its own config bank by default (memory channels,
* names, VFOs, settings), though SetCfg can point the running firmware at a
* different bank. A non-zero bank base transparently shifts every flash access
* BELOW PY25Q16_BANK_SHARED_FROM into the active bank; calibration, boot logo,
* firmware slots and the multiboot marker all live at/above that boundary and
* stay shared across every bank.
*
* 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().
* PY25Q16_SetBankBase(MB_BankBase(MB_BootResolveState()));
* and never changed again during a session (a slot restore or SetCfg selection
* records the next bank and resets first), so the banking itself never needs a
* cache flush. Raw bank 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);
#define PY25Q16_BANK_SHARED_FROM 0x00010000u /* calibration boundary (see flash map) */
void PY25Q16_SetBankBase(uint32_t Base);
#endif
#endif
+3 -3
View File
@@ -86,11 +86,11 @@ void Main(void)
BOARD_Init();
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Resolve the active settings profile BEFORE any EEPROM/settings access
/* Resolve the active settings bank 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()));
* Calibration stays shared regardless of the selected bank. */
PY25Q16_SetBankBase(MB_BankBase(MB_BootResolveState()));
#endif
boot_counter_10ms = 250; // 2.5 sec
+67 -29
View File
@@ -43,6 +43,7 @@
#include "ui.h"
#include "welcome.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#include "multiboot.h"
#endif
@@ -177,6 +178,9 @@ const t_menu_item MenuList[] =
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
{"SetSav", MENU_SET_SAV },
#endif
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
{"SetCfg", MENU_SET_CFG }, // load another settings bank (reboots)
#endif
#endif
// hidden menu items from here on
// enabled if pressing both the PTT and upper side button at power-on
@@ -601,6 +605,9 @@ static const uint8_t CatChannels[] = {
#endif
MENU_BCL, MENU_COMPAND, MENU_AM, MENU_TX_LOCK, MENU_PTT_ID, MENU_LIST_CH,
MENU_MEM_CH, MENU_DEL_CH, MENU_MEM_NAME,
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
MENU_SET_CFG,
#endif
};
static const uint8_t CatScan[] = {
MENU_S_LIST, MENU_S_PRI, MENU_S_PRI_CH_1, MENU_S_PRI_CH_2, MENU_SC_REV,
@@ -839,34 +846,27 @@ static void UI_MENU_DrawTopRightRoundedBadge(const char *text, const uint8_t lin
UI_PrintStringSmallNormalInverse(text, text_x, 0, line);
}
/* Single-line variant for tight gaps: unlike UI_PrintStringSmallNormalInverse,
* the rounded edge stays entirely inside `line` and never touches line - 1. */
static void UI_MENU_DrawInlineRoundedBadge(const char *text, const uint8_t line,
const uint8_t area_x1, const uint8_t area_x2)
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Draw `text` (3x5 font) centred inside a fixed-width rounded inverse capsule:
* left edge `cap_left`, inclusive width `cap_w`, on framebuffer page `line`. Same
* capsule pattern as GUI_DisplaySmallestInverse (0x3E rounded ends, 0x7F body) but
* with the width decoupled from the text length, so two labels of different
* lengths (e.g. "SLOT 2" / "CFG 4") share one width and each stays centred. */
static void UI_MENU_DrawFixedCapsule(const char *text, uint8_t cap_left,
uint8_t cap_w, uint8_t line)
{
const size_t length = strlen(text);
const size_t char_pitch = ARRAY_SIZE(gFontSmall[0]) + 1u;
const size_t text_width = length * char_pitch;
const size_t capsule_width = text_width + 3u;
const uint8_t cap_right = (uint8_t)(cap_left + cap_w - 1u);
const uint8_t text_w = (uint8_t)(strlen(text) * 4u - 1u); /* 3x5 glyphs: 4 px/char, last one 3 px wide */
const uint8_t tx = (uint8_t)(cap_left + (cap_w - text_w) / 2u);
if (length == 0 || line >= FRAME_LINES || area_x2 <= area_x1) {
return;
}
GUI_DisplaySmallest(text, tx, (uint8_t)(line * 8u + 1u), false, true);
const size_t area_width = area_x2 - area_x1 + 1u;
if (capsule_width >= area_width)
return;
const uint8_t capsule_left = (uint8_t)(area_x1 + ((area_width - capsule_width) / 2u));
const uint8_t text_x = (uint8_t)(capsule_left + 1u);
const uint8_t x_end = (uint8_t)(text_x + text_width + 1u);
UI_PrintStringSmallNormal(text, text_x, 0, line);
gFrameBuffer[line][text_x - 1u] ^= 0x7Eu;
for (uint8_t x = text_x; x < x_end; x++)
gFrameBuffer[line][x] ^= 0xFFu;
gFrameBuffer[line][x_end] ^= 0x7Eu;
gFrameBuffer[line][cap_left] ^= 0x3Eu;
for (uint8_t x = (uint8_t)(cap_left + 1u); x < cap_right; x++)
gFrameBuffer[line][x] ^= 0x7Fu;
gFrameBuffer[line][cap_right] ^= 0x3Eu;
}
#endif
void UI_DisplayMenu(void)
{
@@ -1454,12 +1454,39 @@ void UI_DisplayMenu(void)
sprintf(String, "%s\n%s", AUTHOR_STRING_2, DISPLAY_VERSION_STRING_2);
UI_PrintStringSmallNormal(Edition, menu_item_x1 - 1, menu_item_x2, 6);
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
const uint8_t running_slot = MB_GetRunningSlot();
char slot_badge[2];
/* Two 3x5 inverse-capsule labels on one line (scan-list "label"
* style): the running firmware slot (M = Main) and the active
* config bank. They match unless SetCfg has pointed the bank at a
* different bank (e.g. SLOT 2 / CFG 4). */
const uint8_t fw_slot = MB_GetRunningSlot();
const uint8_t bank = MB_GetActiveBank();
char slot_lbl[8];
char cfg_lbl[8];
slot_badge[0] = (running_slot == 0u) ? 'M' : (char)('0' + running_slot);
slot_badge[1] = '\0';
UI_MENU_DrawInlineRoundedBadge(slot_badge, 5, menu_item_x1, menu_item_x2);
/* Only the last glyph varies (M / digit / ?), so poke it in place
* instead of pulling sprintf for a single character. */
strcpy(slot_lbl, "SLOT ?");
if (fw_slot == 0u)
slot_lbl[5] = 'M';
else if (fw_slot < MB_SLOT_COUNT)
slot_lbl[5] = (char)('0' + fw_slot);
strcpy(cfg_lbl, "CFG M"); /* bank 0 = base config, like SLOT M */
if (bank != 0u)
cfg_lbl[4] = (char)('0' + bank);
/* Both capsules share the wider label's width (6-char "SLOT x" ->
* 4*6+3 = 27 px); the shorter CFG text is centred inside its own.
* The two are drawn as one centred pair with a small gap, centred in
* the space between the separator bar (x=48) and the right screen
* edge, so they line up with the centred identity lines above. */
const uint8_t cap_w = (uint8_t)(4u * 6u + 3u); /* 27 */
const uint8_t cap_gap = 4u;
const uint8_t pair_w = (uint8_t)(2u * cap_w + cap_gap); /* 58 */
const uint8_t slot_left = (uint8_t)((48u + LCD_WIDTH - pair_w) / 2u); /* 59 */
const uint8_t cfg_left = (uint8_t)(slot_left + cap_w + cap_gap); /* 90 */
UI_MENU_DrawFixedCapsule(slot_lbl, slot_left, cap_w, 5);
UI_MENU_DrawFixedCapsule(cfg_lbl, cfg_left, cap_w, 5);
#endif
#else
sprintf(String, "%u.%02uV\n%u%%",
@@ -1582,6 +1609,14 @@ void UI_DisplayMenu(void)
strcpy(String, gSubMenu_SET_NAV[gSubMenuSelection]);
break;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
case MENU_SET_CFG:
strcpy(String, "CFG M"); /* bank 0 = base config, like SysInfo */
if (gSubMenuSelection != 0)
String[4] = (char)('0' + gSubMenuSelection);
break;
#endif
case MENU_F1SHRT:
case MENU_F1LONG:
case MENU_F2SHRT:
@@ -1837,6 +1872,9 @@ void UI_DisplayMenu(void)
if ((m == MENU_RESET ||
m == MENU_MEM_CH ||
m == MENU_MEM_NAME ||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
m == MENU_SET_CFG ||
#endif
m == MENU_DEL_CH) && gAskForConfirmation)
{ // display confirmation
char *pPrintStr = (gAskForConfirmation == 1) ? "SURE?" : "WAIT!";
+3
View File
@@ -153,6 +153,9 @@ enum
MENU_NOAA_S,
#endif
MENU_SET_NAV,
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
MENU_SET_CFG,
#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO
MENU_SET_AUD,
#endif
+82 -29
View File
@@ -1,5 +1,17 @@
/* Copyright 2026 F4HWN
* SPDX-License-Identifier: Apache-2.0
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <string.h>
@@ -13,12 +25,14 @@
#include "ui/helper.h"
#include "ui/multiboot.h"
static uint8_t gRunningSlot = MB_SLOT_BACKUP;
static uint8_t gRunningSlot = 0xFFu;
static uint8_t gActiveBank = 0u;
static uint8_t mb_remember_running_slot(uint8_t slot)
static uint8_t mb_remember_boot_state(uint8_t slot, uint8_t bank)
{
gRunningSlot = slot;
return slot;
gActiveBank = bank;
return bank;
}
uint8_t MB_GetRunningSlot(void)
@@ -26,6 +40,11 @@ uint8_t MB_GetRunningSlot(void)
return gRunningSlot;
}
uint8_t MB_GetActiveBank(void)
{
return gActiveBank;
}
static const char *mb_error_text(uint8_t err)
{
switch (err)
@@ -37,7 +56,7 @@ static const char *mb_error_text(uint8_t err)
case MB_ERR_SIZE: return "bad size";
case MB_ERR_CRC: return "CRC ERROR";
case MB_ERR_SPI: return "SPI ERROR";
case MB_ERR_SLOT: return "bad slot";
case MB_ERR_SLOT: return "bad index";
case MB_ERR_AUTH: return "auth";
case MB_ERR_RAM_LOAD: return "RAM LOAD ERROR";
default: return "error";
@@ -171,6 +190,23 @@ static KEY_Code_t mb_get_key(void)
}
}
/* Shown from the normal settings menu (SetCfg), not the boot selector, so it does
* NOT paint the "F4HWN MULTIBOOT" status banner - just a plain acknowledged message. */
void UI_MultibootShowConfigError(uint8_t err)
{
UI_DisplayClear();
UI_StatusClear();
UI_PrintStringSmallNormal("CFG ERROR", 2, 126, 2);
UI_PrintStringSmallNormal(mb_error_text(err), 2, 126, 4);
UI_PrintStringSmallNormal("Press any key", 2, 126, 6);
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
/* The MENU press that confirmed SetCfg may still be down; wait for a clean
* release first so it isn't consumed as the acknowledgement immediately. */
mb_wait_release();
(void)mb_get_key();
}
static void mb_scan_slots(mb_slot_header_t headers[MB_SLOT_COUNT], uint8_t status[MB_SLOT_COUNT])
{
mb_show_message("Scanning slots...", NULL, "Please wait");
@@ -301,13 +337,13 @@ static void mb_backup_progress(uint32_t done, uint32_t total)
ST7565_BlitFullScreen();
}
/* With no trustworthy profile, continuing would let normal boot-time settings
/* With no trustworthy bank, 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)
__attribute__((noreturn)) static void mb_state_error_halt(void)
{
BACKLIGHT_TurnOn();
mb_show_message("PROFILE ERROR", "Flash state unknown", "Restart radio");
mb_show_message("STATE ERROR", "Flash state unknown", "Restart radio");
for (;;)
SYSTEM_DelayMs(100);
}
@@ -339,10 +375,11 @@ void UI_MultibootSelector(void)
mb_wait_release();
mb_scan_slots(headers, status);
/* 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();
/* Pre-select the exact firmware slot resolved at boot, independently of the
* active config bank (SetCfg can point the bank elsewhere), so the cursor
* lands on "where you are". An unknown or now-invalid slot falls back to the
* first valid slot below. */
selected = MB_GetRunningSlot();
if (selected >= MB_SLOT_COUNT || status[selected] != MB_OK)
{
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
@@ -394,13 +431,13 @@ void UI_MultibootSelector(void)
if (key != KEY_MENU)
continue;
/* Bind this slot to its own settings profile BEFORE reflashing. The
/* Bind this slot to its own settings bank 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
* reflash - otherwise the next boot could resolve to the wrong bank
* (e.g. when two slots hold the same firmware image). */
if (MB_SetActiveProfile(selected) != MB_OK)
if (MB_SetActiveSlot(selected) != MB_OK)
{
mb_show_message("PROFILE ERROR", "Marker not saved", "Press any key");
mb_show_message("STATE ERROR", "Marker not saved", "Press any key");
(void)mb_get_key();
continue;
}
@@ -417,25 +454,25 @@ void UI_MultibootSelector(void)
}
/* 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
* the marker at bank 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)
{
mb_backup_prepare();
BACKLIGHT_TurnOn();
if (MB_BackupInternalToSlot0(mb_backup_progress) == MB_OK)
(void)MB_SetActiveProfile(MB_SLOT_BACKUP);
(void)MB_SetActiveSlot(MB_SLOT_BACKUP);
return MB_SLOT_BACKUP;
}
uint8_t MB_BootResolveProfile(void)
uint8_t MB_BootResolveState(void)
{
mb_profile_state_t mark;
mb_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);
ms = MB_ReadActiveState(&mark);
if (ms == MB_MARK_IO)
SYSTEM_DelayMs(10);
}
@@ -444,21 +481,37 @@ uint8_t MB_BootResolveProfile(void)
* identity, so we don't even need the slot header. */
if (ms == MB_MARK_VALID)
{
if (MB_InternalMatchesProfile(&mark))
return mb_remember_running_slot(mark.index); /* slot named by the marker */
if (MB_InternalMatchesState(&mark))
return mb_remember_boot_state(mark.firmware_slot, mark.config_bank);
/* 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_remember_running_slot(mb_adopt_internal_as_main());
return mb_remember_boot_state(mb_adopt_internal_as_main(), MB_SLOT_BACKUP);
}
/* 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. */
/* An FMP1 record still names a coupled slot/bank; honour it before the
* content scan so a duplicate image in a lower slot cannot hijack the
* migration. Falls through to the scan below on mismatch or IO. */
if (ms == MB_MARK_LEGACY && mark.firmware_slot < MB_SLOT_COUNT)
{
mb_fw_match_t m = MB_FW_IO;
for (uint8_t retry = 0; retry < 3u && m == MB_FW_IO; retry++)
m = MB_InternalMatchesSlot(mark.firmware_slot);
if (m == MB_FW_MATCH)
{
(void)MB_SetActiveSlot(mark.firmware_slot);
return mb_remember_boot_state(mark.firmware_slot, mark.config_bank);
}
}
bool had_io = false;
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
@@ -467,8 +520,8 @@ uint8_t MB_BootResolveProfile(void)
m = MB_InternalMatchesSlot(slot);
if (m == MB_FW_MATCH)
{
(void)MB_SetActiveProfile(slot); /* record/repair the marker */
return mb_remember_running_slot(slot);
(void)MB_SetActiveSlot(slot); /* record/repair the marker */
return mb_remember_boot_state(slot, slot);
}
if (m == MB_FW_IO)
had_io = true;
@@ -483,8 +536,8 @@ uint8_t MB_BootResolveProfile(void)
bool main_exists = (main_status != MB_ERR_MAGIC &&
main_status != MB_ERR_NOT_COMMITTED);
if (main_exists)
mb_profile_error_halt();
mb_state_error_halt();
}
return mb_remember_running_slot(mb_adopt_internal_as_main());
return mb_remember_boot_state(mb_adopt_internal_as_main(), MB_SLOT_BACKUP);
}
+27 -9
View File
@@ -1,5 +1,17 @@
/* Copyright 2026 F4HWN
* SPDX-License-Identifier: Apache-2.0
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef UI_MULTIBOOT_H
@@ -11,14 +23,20 @@
* 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);
/* Show a blocking, acknowledged error screen for a failed SetCfg operation. */
void UI_MultibootShowConfigError(uint8_t err);
/* Slot selected by MB_BootResolveProfile for the current session. This is kept
* in RAM so UI callers do not have to reread the external-flash marker. */
/* Resolve the active config bank at boot, BEFORE any settings read. Detects a
* firmware installed outside multiboot (internal identity != marker) and, if so,
* discreetly self-backs it up into slot 0 and adopts slot 0 / bank 0. Returns the
* config bank (0..MB_BANK_COUNT-1) to feed PY25Q16_SetBankBase(); the exact
* firmware slot and bank are cached too (see MB_GetRunningSlot/MB_GetActiveBank). */
uint8_t MB_BootResolveState(void);
/* Exact firmware slot and active config bank resolved for this session. Both
* are cached in RAM so UI callers never have to reread the marker or scan slot
* headers merely to render their state. */
uint8_t MB_GetRunningSlot(void);
uint8_t MB_GetActiveBank(void);
#endif