mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Add SetCfg to pick a config bank independently of the slot
This commit is contained in:
1 parent
5b6174085b
commit
e4a645a5c7
12 files changed
+492
-233
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
+158
-82
@@ -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,
|
||||
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(¤t, ¤t_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(¤t, ¤t_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, ¤t, 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(¤t, ¤t_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, ¤t, 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;
|
||||
|
||||
+63
-51
@@ -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,19 +111,21 @@ 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 */
|
||||
@@ -132,75 +133,86 @@ uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_
|
||||
/* 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) ] */
|
||||
/* 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 */
|
||||
/* */
|
||||
/* 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 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 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 */
|
||||
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_profile_state_t;
|
||||
} 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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
+68
-30
@@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
GUI_DisplaySmallest(text, tx, (uint8_t)(line * 8u + 1u), false, true);
|
||||
|
||||
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!";
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user