diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 374716af..28d91358 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -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) diff --git a/App/app/menu.c b/App/app/menu.c index 37c084da..b24af6f7 100644 --- a/App/app/menu.c +++ b/App/app/menu.c @@ -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; diff --git a/App/app/uart.c b/App/app/uart.c index db9b8994..e53faed7 100644 --- a/App/app/uart.c +++ b/App/app/uart.c @@ -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; diff --git a/App/driver/mb_flash.c b/App/driver/mb_flash.c index 2b226bee..9b97a1c1 100644 --- a/App/driver/mb_flash.c +++ b/App/driver/mb_flash.c @@ -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(¤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; diff --git a/App/driver/mb_flash.h b/App/driver/mb_flash.h index 14abc758..4c8e1215 100644 --- a/App/driver/mb_flash.h +++ b/App/driver/mb_flash.h @@ -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 diff --git a/App/driver/py25q16.c b/App/driver/py25q16.c index dfde7ac2..99b71f14 100644 --- a/App/driver/py25q16.c +++ b/App/driver/py25q16.c @@ -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) diff --git a/App/driver/py25q16.h b/App/driver/py25q16.h index 5c93b7e3..2b013393 100644 --- a/App/driver/py25q16.h +++ b/App/driver/py25q16.h @@ -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 diff --git a/App/main.c b/App/main.c index ad099c23..d7d1544f 100644 --- a/App/main.c +++ b/App/main.c @@ -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 diff --git a/App/ui/menu.c b/App/ui/menu.c index 6b048d28..be24d18b 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -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!"; diff --git a/App/ui/menu.h b/App/ui/menu.h index 9cd676ed..12f513c1 100644 --- a/App/ui/menu.h +++ b/App/ui/menu.h @@ -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 diff --git a/App/ui/multiboot.c b/App/ui/multiboot.c index eedb38b2..3d7f0a0a 100644 --- a/App/ui/multiboot.c +++ b/App/ui/multiboot.c @@ -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 @@ -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); } diff --git a/App/ui/multiboot.h b/App/ui/multiboot.h index d7b4d1d1..596af5b6 100644 --- a/App/ui/multiboot.h +++ b/App/ui/multiboot.h @@ -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