/* 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. */ /* * Multiboot flash programmer. * * M1 (validated): brick-critical core - reprogram the internal application flash * from an image held in the external SPI flash, running from RAM. * * M2 (this file): slot format + integrity validation. Each slot starts with a * header (magic, size, CRC32, name, version); a restore validates the header and * the image CRC32 *before* erasing anything, so an incompatible or corrupt image * 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. */ #ifndef DRIVER_MB_FLASH_H #define DRIVER_MB_FLASH_H #include #include /* Internal flash application region (see Core/py32f071xb.ld: * FLASH origin 0x08002800, length 118 KiB). 0x08002800 is 256-byte aligned, so * the whole region can be page-erased (256 B granularity) without touching the * factory bootloader that lives just below it. */ #define MB_INT_APP_BASE 0x08002800u #define MB_INT_APP_SIZE 0x0001D800u /* 118 KiB */ /* 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 * 1..4 are the user slots managed from UV Studio. */ #define MB_SLOT_STRIDE 0x00020000u /* 128 KiB per slot */ #define MB_SLOT_IMG_OFFSET 0x00001000u /* image starts after header sector */ #define MB_SLOT0_EXT_BASE 0x00020000u /* slot 0 (backup) header base */ #define MB_SLOT_COUNT 5u /* slot 0 backup + slots 1..4 */ #define MB_SLOT_BACKUP 0u /* slot 0 = firmware base backup */ /* Slot header (stored at the slot base, first 4 KiB sector). 64 bytes. */ #define MB_SLOT_MAGIC 0x31424D46u /* "FMB1" */ #define MB_HDR_VERSION 1u #define MB_FLAG_COMMITTED (1u << 0) /* image written and verified */ #define MB_NAME_LEN 16 #define MB_VERSION_LEN 16 typedef struct __attribute__((packed)) { uint32_t magic; /* MB_SLOT_MAGIC */ uint16_t hdr_version; /* MB_HDR_VERSION */ uint16_t flags; /* MB_FLAG_COMMITTED, ... */ uint32_t image_size; /* bytes, <= MB_INT_APP_SIZE */ uint32_t image_crc32; /* CRC-32 (zlib) over image_size B */ char name[MB_NAME_LEN]; /* human-readable, NUL-terminated */ char fw_version[MB_VERSION_LEN]; /* firmware version string */ uint8_t reserved[16]; /* pad to 64 bytes, future use */ } mb_slot_header_t; /* Multiboot operation result (a successful restore resets before returning). */ enum { MB_OK = 0, MB_ERR_MAGIC, /* no/invalid slot header */ MB_ERR_VERSION, /* header format too new */ MB_ERR_NOT_COMMITTED,/* image not marked complete */ 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 or bank index out of range */ MB_ERR_AUTH, /* write refused: timestamp mismatch */ MB_ERR_RAM_LOAD /* restore stub RAM copy mismatch */ }; /* CRC-32 (zlib) over a resident memory buffer. Multiboot and overlay apps * share this implementation; keeping it here avoids carrying two identical * bitwise CRC loops in the MCU flash. */ uint32_t MB_Crc32Bytes(const uint8_t *data, uint32_t len); /* Multi-slot API used by the boot selector. Validation always covers the full * image CRC before restore. progress_line may point to a 128-byte LCD page; the * RAM copier then fills it while reflashing. Pass NULL to disable LCD updates. * With ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY, the copier is loaded over the * PY25Q16 sector cache only after validation and immediately before this call. */ uint8_t MB_ValidateSlot(uint8_t slot, mb_slot_header_t *out_header, uint32_t *out_crc); uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line); /* * Host-tool slot management (M4, "Firmware Slots" in UV Studio). Everything is * bounds-checked (slot < MB_SLOT_COUNT, offset+len <= MB_SLOT_STRIDE) and writes * touch the EXTERNAL flash only, so none of this is brick-critical: a bad slot is * simply refused at restore by the CRC validation above. * * - MB_SlotInfo reads the 64-byte header only (fast, no CRC recompute) and * returns MB_OK / MB_ERR_* describing the header state. * - MB_SlotErase erases the whole 128 KiB slot region (header + image). * - MB_SlotWrite programs `len` bytes at slot_base+offset. The slot MUST have * been erased first (NOR flash only clears 1->0 bits); the host * writes the image, then the COMMITTED header last. * Use MB_ValidateSlot afterwards to confirm the full image CRC. */ uint8_t MB_SlotInfo(uint8_t slot, mb_slot_header_t *out_header); uint8_t MB_SlotErase(uint8_t slot); uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len); /* -------------------------------------------------------------------------- */ /* Config banks (one per slot by default, switchable via SetCfg). */ /* */ /* 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 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 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 config banks past the last */ /* slot if MB_SLOT_COUNT ever grows. */ #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-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_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_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 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-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, /* 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-state marker, distinguishing the states above. */ mb_mark_status_t MB_ReadActiveState(mb_state_t *state); /* 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. 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); /* 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_BankErase(uint8_t bank); /* -------------------------------------------------------------------------- */ /* Slot 0 self-backup (base firmware). */ /* -------------------------------------------------------------------------- */ /* Progress callback for the (slow) slot-0 self-backup; may be NULL. */ typedef void (*mb_progress_fn)(uint32_t done, uint32_t total); /* Does the running internal firmware carry the DECLARED identity of `slot` * (CRC32 over image_size == the slot header's image_crc32)? This checks the * header's claim, not the stored image itself (that is validated once, right * after a backup). IO is reported separately so a transient SPI error is never * mistaken for a mismatch - which would wrongly trigger a self-backup over Main. */ typedef enum { MB_FW_MATCH = 0, /* internal carries this slot's declared identity */ MB_FW_MISMATCH, /* reliably not this slot (or slot has no header) */ MB_FW_IO, /* slot header unreadable: uncertain */ } mb_fw_match_t; mb_fw_match_t MB_InternalMatchesSlot(uint8_t slot); /* Compare internal flash with the identity stored in a validated marker. */ bool MB_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 * full external CRC read-back. External flash only, never brick-critical; a * failure (SPI or CRC) leaves the slot uncommitted and does not advance the * marker, so boot resolution retries it. progress may be NULL. */ uint8_t MB_BackupInternalToSlot0(mb_progress_fn progress); #endif /* DRIVER_MB_FLASH_H */