Add mb firmware slots

This commit is contained in:
Armel FAUVEAU committed 2026-08-11 22:33:33 +02:00
1 parent 6692b5191b
commit a25d2fc75e
14 files changed
+1905 -15

No files matched your search

+4
View File
@@ -230,6 +230,10 @@ endif()
if(ENABLE_FEAT_F4HWN_BEAM AND NOT ENABLE_AIRCOPY)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_BEAM requires ENABLE_AIRCOPY (it reuses g_FSK_Buffer, AIRCOPY_Obfuscate and the FSK packet plumbing).")
endif()
enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT
driver/mb_flash.c
ui/multiboot.c
)
enable_feature(ENABLE_FEAT_F4HWN_QRCODE)
enable_feature(ENABLE_FEAT_F4HWN_LOGO)
enable_feature(ENABLE_FEAT_F4HWN_LOGO_SAV)
+250 -11
View File
@@ -45,6 +45,10 @@
#include "settings.h"
#include "version.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#endif
#if defined(ENABLE_OVERLAY)
#include "sram-overlay.h"
#endif
@@ -201,11 +205,11 @@ static void SendReply_VCP(void *pReply, uint16_t Size)
return;
}
memcpy(VCP_ReplyBuf + sizeof(Header_t), pReply, Size);
uint8_t *pBody = VCP_ReplyBuf + sizeof(Header_t);
uint8_t *pFooter = pBody + Size;
Header_t *pHeader = (Header_t *)VCP_ReplyBuf;
Footer_t *pFooter = (Footer_t *)(VCP_ReplyBuf + sizeof(Header_t) + Size);
pReply = VCP_ReplyBuf + sizeof(Header_t);
memcpy(pBody, pReply, Size);
pReply = pBody;
if (bIsEncrypted)
{
@@ -215,23 +219,29 @@ static void SendReply_VCP(void *pReply, uint16_t Size)
pBytes[i] ^= Obfuscation[i % 16];
}
pHeader->ID = 0xCDAB;
pHeader->Size = Size;
/* Build the transport header/footer byte by byte. The reply body may have
* an odd size, so pFooter is not necessarily half-word aligned; casting it
* to Footer_t and storing ID as uint16_t can HardFault on Cortex-M0+. */
VCP_ReplyBuf[0] = 0xAB;
VCP_ReplyBuf[1] = 0xCD;
VCP_ReplyBuf[2] = (uint8_t)(Size & 0xFFu);
VCP_ReplyBuf[3] = (uint8_t)(Size >> 8);
// VCP_Send((uint8_t *)&Header, sizeof(Header));
// VCP_Send(pReply, Size);
if (bIsEncrypted)
{
pFooter->Padding[0] = Obfuscation[(Size + 0) % 16] ^ 0xFF;
pFooter->Padding[1] = Obfuscation[(Size + 1) % 16] ^ 0xFF;
pFooter[0] = Obfuscation[(Size + 0) % 16] ^ 0xFF;
pFooter[1] = Obfuscation[(Size + 1) % 16] ^ 0xFF;
}
else
{
pFooter->Padding[0] = 0xFF;
pFooter->Padding[1] = 0xFF;
pFooter[0] = 0xFF;
pFooter[1] = 0xFF;
}
pFooter->ID = 0xBADC;
pFooter[2] = 0xDC;
pFooter[3] = 0xBA;
// VCP_Send((uint8_t *)&Footer, sizeof(Footer));
@@ -782,6 +792,24 @@ bool UART_IsCommandAvailable(uint32_t Port)
return CRC_Calculate(pUART_Command->Buffer, Size) == Crc;
}
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Timestamp latched by the device-info handshake (0x0514) for this port. Slot
* writes/erases require it to match, like the EEPROM write command (CMD_051D). */
static uint32_t mb_port_timestamp(uint32_t Port)
{
#if defined(ENABLE_UART)
if (Port == UART_PORT_UART)
return UART_Timestamp;
#endif
#if defined(ENABLE_USB)
if (Port == UART_PORT_VCP)
return VCP_Timestamp;
#endif
(void)Port;
return 0;
}
#endif
void UART_HandleCommand(uint32_t Port)
{
UART_Command_t *pUART_Command;
@@ -852,6 +880,217 @@ void UART_HandleCommand(uint32_t Port)
#endif
break;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
// Multiboot self-test (developer only, gated build).
case 0x0710: // backup: internal application -> external flash slot 0
{
uint32_t size = 0, crc = 0;
MB_BackupToSlot0(&size, &crc);
struct __attribute__((packed)) {
Header_t Header;
uint32_t Size;
uint32_t Crc32;
} Reply;
Reply.Header.ID = 0x0711;
Reply.Header.Size = 8;
Reply.Size = size;
Reply.Crc32 = crc;
SendReply(Port, &Reply, sizeof(Reply)); // ack once the backup completed
break;
}
case 0x0712: // restore: external flash slot 0 -> internal application + reset
{
// Returns only if validation failed (internal flash untouched);
// on success it reflashes and resets, so control never comes back.
uint8_t err = MB_RestoreSlot0();
struct __attribute__((packed)) {
Header_t Header;
uint8_t Code;
} Reply;
Reply.Header.ID = 0x0713;
Reply.Header.Size = 1;
Reply.Code = err;
SendReply(Port, &Reply, sizeof(Reply)); // restore refused, report why
break;
}
case 0x0714: // test: corrupt slot 0 image (exercise the CRC-refusal path)
{
uint16_t n = pUART_Command->Header.Size;
if (n > 128)
n = 128;
MB_CorruptSlot0(pUART_Command->Data, n);
struct __attribute__((packed)) {
Header_t Header;
uint16_t Count;
} Reply;
Reply.Header.ID = 0x0715;
Reply.Header.Size = 2;
Reply.Count = n;
SendReply(Port, &Reply, sizeof(Reply)); // ack: bytes corrupted
break;
}
case 0x0716: // validate slot 0 (no reflash): report result code + computed CRC
{
uint32_t crc = 0;
uint8_t code = MB_ValidateSlot0(&crc);
struct __attribute__((packed)) {
Header_t Header;
uint32_t Crc32; // 4-byte aligned (offset 4): no unaligned write
uint8_t Code;
} Reply;
Reply.Header.ID = 0x0717;
Reply.Header.Size = 5;
Reply.Crc32 = crc;
Reply.Code = code;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0718: // ground-truth dump of external flash via the DMA driver
{
uint32_t addr = (uint32_t)pUART_Command->Data[0]
| ((uint32_t)pUART_Command->Data[1] << 8)
| ((uint32_t)pUART_Command->Data[2] << 16)
| ((uint32_t)pUART_Command->Data[3] << 24);
uint8_t len = pUART_Command->Data[4];
if (len > 64)
len = 64;
struct __attribute__((packed)) {
Header_t Header;
uint32_t Addr;
uint8_t Len;
uint8_t Data[64];
} Reply;
MB_DumpExt(addr, Reply.Data, len);
Reply.Header.ID = 0x0719;
Reply.Header.Size = 5 + len; // Addr(4) + Len(1) + data(len)
Reply.Addr = addr;
Reply.Len = len;
SendReply(Port, &Reply, sizeof(Header_t) + 5 + len);
break;
}
case 0x071C: // diagnostic: snapshot SPI2 / DMA state (no flash access)
{
uint32_t st[4];
MB_SpiState(st);
struct __attribute__((packed)) {
Header_t Header;
uint32_t V[4];
} Reply;
Reply.Header.ID = 0x071D;
Reply.Header.Size = 16;
Reply.V[0] = st[0]; Reply.V[1] = st[1];
Reply.V[2] = st[2]; Reply.V[3] = st[3];
SendReply(Port, &Reply, sizeof(Reply));
break;
}
// ---- M4 slot management ("Firmware Slots") ------------------------
case 0x0720: // slot info: read the 64-byte header only (fast, no CRC)
{
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
mb_slot_header_t hdr;
memset(&hdr, 0, sizeof(hdr));
uint8_t status = MB_SlotInfo(slot, &hdr);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
uint8_t Hdr[sizeof(mb_slot_header_t)];
} Reply;
Reply.Header.ID = 0x0721;
Reply.Header.Size = 2 + sizeof(mb_slot_header_t);
Reply.Slot = slot;
Reply.Status = status;
memcpy(Reply.Hdr, &hdr, sizeof(hdr));
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0722: // slot erase: wipe the whole 128 KiB slot region
{
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint32_t ts = (uint32_t)pUART_Command->Data[2]
| ((uint32_t)pUART_Command->Data[3] << 8)
| ((uint32_t)pUART_Command->Data[4] << 16)
| ((uint32_t)pUART_Command->Data[5] << 24);
uint8_t status = (ts != mb_port_timestamp(Port))
? MB_ERR_AUTH : MB_SlotErase(slot);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0723;
Reply.Header.Size = 2;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0724: // slot write: program bytes at slot+offset (slot pre-erased)
{
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint32_t offset = (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);
uint16_t len = (uint16_t)(pUART_Command->Data[6]
| ((uint16_t)pUART_Command->Data[7] << 8));
uint32_t ts = (uint32_t)pUART_Command->Data[8]
| ((uint32_t)pUART_Command->Data[9] << 8)
| ((uint32_t)pUART_Command->Data[10] << 16)
| ((uint32_t)pUART_Command->Data[11] << 24);
uint8_t status;
if (ts != mb_port_timestamp(Port))
status = MB_ERR_AUTH;
else if (len > 240u) // 12-byte prefix + data must fit Data[252]
status = MB_ERR_SIZE;
else
status = MB_SlotWrite(slot, offset, &pUART_Command->Data[12], len);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0725;
Reply.Header.Size = 2;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0726: // slot validate: full image CRC-32, no reflash
{
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint32_t crc = 0;
uint8_t status = MB_ValidateSlot(slot, NULL, &crc);
struct __attribute__((packed)) {
Header_t Header;
uint32_t Crc32; // offset 4: 4-byte aligned, no unaligned store
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0727;
Reply.Header.Size = 6;
Reply.Crc32 = crc;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
#endif
#ifdef ENABLE_UART_RW_BK_REGS
case 0x0601:
CMD_0601_ReadBK4819Reg(Port, pUART_Command->Buffer);
+798
View File
@@ -0,0 +1,798 @@
/* Copyright 2026 F4HWN
*
* 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>
#include "driver/mb_flash.h"
#include "py32f0xx.h"
#include "driver/py25q16.h"
#include "driver/st7565.h"
#include "ui/helper.h"
#include "version.h"
/* Internal-flash program/erase keys (FLASH_KEY1 / FLASH_KEY2). */
#define MB_FLASH_KEY1 0x45670123u
#define MB_FLASH_KEY2 0xCDEF89ABu
/* Internal flash granularity (PY32F071xB): program & page-erase = 256 bytes. */
#define MB_FLASH_PAGE 256u
/* External SPI flash chip-select is on PA3 (see driver/py25q16.c). */
#define MB_CS_PIN (1u << 3)
/* LCD control pins used only for RAM-resident progress updates. */
#define MB_LCD_CS_PIN (1u << 2) /* PB2 */
#define MB_LCD_A0_PIN (1u << 6) /* PA6 */
/* Rounded progress gauge geometry, matching ScanProgress_DrawGaugeLine(). */
#define MB_PROGRESS_COLS 118u
#define MB_PROGRESS_FIRST_COL 5u
#define MB_PROGRESS_FILLED 0x2Du
/* Number of erase/program retries per page before giving up (and resetting
* anyway - the region is already erased, so USB recovery is the only option). */
#define MB_PAGE_RETRIES 3u
/* Bounded waits used by the RAM-only copier. A timeout forces an immediate
* reset instead of hanging forever with IRQs disabled. */
#define MB_RAM_SPI_TIMEOUT 100000u
#define MB_RAM_FLASH_TIMEOUT 10000000u
/*
* Factory flash-timing parameter records, held in Puya system memory.
* Mirror of the HAL's _FlashTimmingParam[] table (the HAL module is not built
* in this project). Indexed by the HSI frequency setting (RCC->ICSCR HSI_FS).
* Each entry is the address of a 5-word record read at +0/+8/+16/+24/+32.
*/
static const uint32_t mb_flash_timing[8] = {
0x1FFF3238, 0x1FFF3260, 0x1FFF3288, 0x1FFF32B0,
0x1FFF32D8, 0x1FFF3238, 0x1FFF3238, 0x1FFF3238
};
/* -------------------------------------------------------------------------- */
/* Helpers (flash-resident). */
/* -------------------------------------------------------------------------- */
/* zlib/PNG CRC-32 (poly 0xEDB88320), streaming. Seed 'crc' with 0xFFFFFFFF and
* XOR the final result with 0xFFFFFFFF. No lookup table (saves flash). */
static uint32_t mb_crc32_update(uint32_t crc, const uint8_t *data, uint32_t len)
{
while (len--)
{
crc ^= *data++;
for (int k = 0; k < 8; k++)
crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
}
return crc;
}
static void mb_copy_str(char *dst, uint32_t cap, const char *src)
{
uint32_t i = 0;
if (src)
for (; i + 1 < cap && src[i]; i++)
dst[i] = src[i];
for (; i < cap; i++)
dst[i] = 0;
}
/* -------------------------------------------------------------------------- */
/* Flash-resident preparation (runs while the flash is still readable). */
/* -------------------------------------------------------------------------- */
static void MB_PrepareInternalFlash(void)
{
/* Unlock the internal flash control register. */
if (FLASH->CR & FLASH_CR_LOCK)
{
FLASH->KEYR = MB_FLASH_KEY1;
FLASH->KEYR = MB_FLASH_KEY2;
}
/* Program/erase timing sequence (factory calibrated), replicating
* __HAL_FLASH_TIMMING_SEQUENCE_CONFIG(). These registers persist, so it is
* enough to set them once here, before the RAM copier starts erasing. */
uint32_t base = mb_flash_timing[(RCC->ICSCR & RCC_ICSCR_HSI_FS) >> RCC_ICSCR_HSI_FS_Pos];
uint32_t p0 = *(volatile uint32_t *)(base + 0);
uint32_t p1 = *(volatile uint32_t *)(base + 8);
uint32_t p2 = *(volatile uint32_t *)(base + 16);
uint32_t p3 = *(volatile uint32_t *)(base + 24);
uint32_t p4 = *(volatile uint32_t *)(base + 32);
FLASH->TS0 = p0 & 0xFFu;
FLASH->TS1 = (p0 >> 16) & 0x1FFu;
FLASH->TS3 = (p0 >> 8) & 0xFFu;
FLASH->TS2P = p1 & 0xFFu;
FLASH->TPS3 = (p1 >> 16) & 0x7FFu;
FLASH->PERTPE = p2 & 0x1FFFFu;
FLASH->SMERTPE = p3 & 0x1FFFFu;
FLASH->PRGTPE = p4 & 0xFFFFu;
FLASH->PRETPE = (p4 >> 16) & 0x3FFFu;
}
/* -------------------------------------------------------------------------- */
/* RAM-resident copier. */
/* */
/* This runs while the internal application flash is being erased/programmed, */
/* during which the flash bus is unavailable. It must therefore NOT fetch any */
/* code from flash nor read any flash data: it uses raw register access only */
/* (no external calls), reads the source from the external SPI flash in */
/* polled mode, and resets the MCU when done. It is placed in .RamFunc, which */
/* the linker stores in flash and the startup copies to RAM alongside .data. */
/* -------------------------------------------------------------------------- */
/* Polled single-byte SPI2 transfer. always_inline keeps all code in .RamFunc.
* The result is returned through out so timeout and received 0xFF remain
* distinguishable. */
__attribute__((always_inline)) static inline bool mb_ram_spi(uint8_t v, uint8_t *out)
{
uint32_t timeout = MB_RAM_SPI_TIMEOUT;
while (!(SPI2->SR & SPI_SR_TXE))
if (!--timeout)
return false;
*(volatile uint8_t *)&SPI2->DR = v;
timeout = MB_RAM_SPI_TIMEOUT;
while (!(SPI2->SR & SPI_SR_RXNE))
if (!--timeout)
return false;
*out = *(volatile uint8_t *)&SPI2->DR;
return true;
}
__attribute__((always_inline)) static inline bool mb_ram_flash_idle(void)
{
uint32_t timeout = MB_RAM_FLASH_TIMEOUT;
while (FLASH->SR & FLASH_SR_BSY)
if (!--timeout)
return false;
return true;
}
__attribute__((always_inline, noreturn)) static inline void mb_ram_reset(void)
{
__DSB();
SCB->AIRCR = (0x5FAu << SCB_AIRCR_VECTKEY_Pos) | SCB_AIRCR_SYSRESETREQ_Msk;
__DSB();
for (;;) { }
}
/* Minimal SPI1 LCD writer. A display timeout merely disables progress updates:
* it must never abort or delay the safety-critical flash copy. */
__attribute__((always_inline)) static inline bool mb_ram_lcd_spi(uint8_t v)
{
uint32_t timeout = MB_RAM_SPI_TIMEOUT;
while (!(SPI1->SR & SPI_SR_TXE))
if (!--timeout)
return false;
*(volatile uint8_t *)&SPI1->DR = v;
timeout = MB_RAM_SPI_TIMEOUT;
while (!(SPI1->SR & SPI_SR_RXNE))
if (!--timeout)
return false;
(void)*(volatile uint8_t *)&SPI1->DR;
return true;
}
__attribute__((always_inline)) static inline bool mb_ram_progress_blit(const uint8_t *line)
{
uint32_t ok = 1u;
GPIOB->BRR = MB_LCD_CS_PIN;
GPIOA->BRR = MB_LCD_A0_PIN; /* command */
if (!mb_ram_lcd_spi(0xB7u) || /* LCD page 7 */
!mb_ram_lcd_spi(0x10u) || /* column high nibble */
!mb_ram_lcd_spi(0x04u)) /* visible RAM starts at column 4 */
ok = 0u;
GPIOA->BSRR = MB_LCD_A0_PIN; /* data */
if (ok)
for (uint32_t i = 0; i < 128u; i++)
if (!mb_ram_lcd_spi(line[i]))
{
ok = 0u;
break;
}
GPIOB->BSRR = MB_LCD_CS_PIN;
return ok != 0u;
}
__attribute__((section(".RamFunc"), noinline, used))
static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize,
uint8_t *progressLine)
{
/* 4-byte aligned so the 64-word page program can read it as uint32_t
* (Cortex-M0+ cannot do unaligned word accesses). */
uint8_t buf[MB_FLASH_PAGE] __attribute__((aligned(4)));
uint32_t remaining = imageSize;
uint32_t regionRemaining = MB_INT_APP_SIZE;
uint32_t pagesDone = 0;
uint32_t progressAccumulator = 0;
uint32_t progressFilled = 0;
uint32_t lcdEnabled = progressLine != NULL;
__disable_irq();
if (FLASH->CR & FLASH_CR_LOCK)
{
FLASH->KEYR = MB_FLASH_KEY1;
FLASH->KEYR = MB_FLASH_KEY2;
}
FLASH->SR = FLASH_SR_EOP | FLASH_SR_WRPERR | FLASH_SR_OPTVERR;
/* Rebuild the complete application region. Bytes past imageSize are never
* read from the external slot: they are forced to erased 0xFF, preventing
* unvalidated padding or remnants of an older, longer firmware. */
while (regionRemaining >= MB_FLASH_PAGE)
{
uint32_t readSize = remaining < MB_FLASH_PAGE ? remaining : MB_FLASH_PAGE;
uint32_t needProgram = 0;
uint32_t success = 0;
uint8_t ignored;
/* Volatile stores prevent GCC from replacing this loop with a call
* to flash-resident memset while the application flash is unavailable. */
volatile uint8_t *fill = buf;
for (uint32_t i = 0; i < MB_FLASH_PAGE; i++)
fill[i] = 0xFFu;
if (readSize)
{
/* Read only CRC-validated image bytes. The rest of the page stays
* 0xFF when imageSize is not page-aligned. */
GPIOA->BRR = MB_CS_PIN; /* CS low */
if (!mb_ram_spi(0x03u, &ignored) ||
!mb_ram_spi((extAddr >> 16) & 0xFFu, &ignored) ||
!mb_ram_spi((extAddr >> 8) & 0xFFu, &ignored) ||
!mb_ram_spi(extAddr & 0xFFu, &ignored))
goto fatal_reset;
for (uint32_t i = 0; i < readSize; i++)
if (!mb_ram_spi(0xFFu, &buf[i]))
goto fatal_reset;
GPIOA->BSRR = MB_CS_PIN; /* CS high */
}
for (uint32_t i = 0; i < MB_FLASH_PAGE; i++)
{
if (buf[i] != 0xFFu)
{
needProgram = 1u;
break;
}
}
for (uint32_t retry = 0; retry < MB_PAGE_RETRIES; retry++)
{
const uint32_t *src = (const uint32_t *)(const void *)buf;
volatile uint32_t *dst = (volatile uint32_t *)intAddr;
uint32_t i;
uint32_t ok = 1u;
/* --- page erase (256 bytes) --- */
if (!mb_ram_flash_idle())
goto fatal_reset;
FLASH->CR |= FLASH_CR_PER;
*(volatile uint32_t *)intAddr = 0xFFFFFFFFu;
if (!mb_ram_flash_idle())
goto fatal_reset;
FLASH->CR &= ~FLASH_CR_PER;
FLASH->SR = FLASH_SR_EOP | FLASH_SR_WRPERR | FLASH_SR_OPTVERR;
if (needProgram)
{
/* Page program: 64 words, PGSTRT before the last word. */
FLASH->CR |= FLASH_CR_PG;
for (i = 0; i < 64u; i++)
{
dst[i] = src[i];
if (i == 62u)
FLASH->CR |= FLASH_CR_PGSTRT;
}
if (!mb_ram_flash_idle())
goto fatal_reset;
FLASH->CR &= ~FLASH_CR_PG;
FLASH->SR = FLASH_SR_EOP | FLASH_SR_WRPERR | FLASH_SR_OPTVERR;
}
/* --- verify (read-back compare) --- */
for (i = 0; i < 64u; i++)
{
if (dst[i] != src[i])
{
ok = 0u;
break;
}
}
if (ok)
{
success = 1u;
break;
}
}
/* Never silently continue after an unprogrammable page. Returning to
* flash-resident code is unsafe once the application has been erased. */
if (!success)
goto fatal_reset;
/* Advance the gauge without division (which could call a helper
* in erased flash). Refresh once per 8 KiB internal sector. */
if (lcdEnabled)
{
pagesDone++;
progressAccumulator += MB_PROGRESS_COLS;
while (progressAccumulator >= (MB_INT_APP_SIZE / MB_FLASH_PAGE))
{
progressAccumulator -= (MB_INT_APP_SIZE / MB_FLASH_PAGE);
if (progressFilled < MB_PROGRESS_COLS)
{
progressLine[MB_PROGRESS_FIRST_COL + progressFilled] = MB_PROGRESS_FILLED;
progressFilled++;
}
}
if ((pagesDone & 31u) == 0u || regionRemaining == MB_FLASH_PAGE)
lcdEnabled = mb_ram_progress_blit(progressLine);
}
intAddr += MB_FLASH_PAGE;
extAddr += readSize;
remaining -= readSize;
regionRemaining -= MB_FLASH_PAGE;
}
FLASH->CR |= FLASH_CR_LOCK;
mb_ram_reset();
fatal_reset:
/* Release the external flash and reset immediately. If failure happened
* after an erase, the factory USB/DFU bootloader remains the recovery path. */
GPIOA->BSRR = MB_CS_PIN;
FLASH->CR &= ~(FLASH_CR_PER | FLASH_CR_PG | FLASH_CR_PGSTRT);
FLASH->CR |= FLASH_CR_LOCK;
mb_ram_reset();
}
/* -------------------------------------------------------------------------- */
/* Public API. */
/* -------------------------------------------------------------------------- */
void MB_BackupToSlot0(uint32_t *out_size, uint32_t *out_crc32)
{
const uint32_t imgBase = MB_SLOT0_EXT_BASE + MB_SLOT_IMG_OFFSET;
const uint32_t size = MB_INT_APP_SIZE; /* full region (self-test) */
/* CRC-32 of the internal image (memory-mapped, contiguous read). */
uint32_t crc = mb_crc32_update(0xFFFFFFFFu, (const uint8_t *)MB_INT_APP_BASE, size)
^ 0xFFFFFFFFu;
/* Write the image first ... */
PY25Q16_WriteBuffer(imgBase, (const void *)MB_INT_APP_BASE, size, false);
/* ... then a valid header (COMMITTED) last, so a committed header always
* implies a fully written image. */
mb_slot_header_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.magic = MB_SLOT_MAGIC;
hdr.hdr_version = MB_HDR_VERSION;
hdr.flags = MB_FLAG_COMMITTED;
hdr.image_size = size;
hdr.image_crc32 = crc;
#ifdef ENABLE_FEAT_F4HWN
mb_copy_str(hdr.name, MB_NAME_LEN, Edition);
#else
mb_copy_str(hdr.name, MB_NAME_LEN, "slot0");
#endif
mb_copy_str(hdr.fw_version, MB_VERSION_LEN, Version);
PY25Q16_WriteBuffer(MB_SLOT0_EXT_BASE, &hdr, sizeof(hdr), false);
if (out_size) *out_size = size;
if (out_crc32) *out_crc32 = crc;
}
/* Set when a polled SPI wait below times out (external flash unresponsive). */
static volatile int mb_spi_err;
/* Polled single-byte SPI2 transfer (flash-resident; runs in normal context).
* Bounded so a wedged SPI can never freeze the firmware: on timeout it sets
* mb_spi_err and returns 0xFF, letting the caller fail gracefully. */
#define MB_SPI_TIMEOUT 20000u /* ~a few ms max per byte; a healthy transfer
* completes in well under a microsecond */
static uint8_t mb_spi_byte(uint8_t v)
{
uint32_t to = MB_SPI_TIMEOUT;
while (!(SPI2->SR & SPI_SR_TXE)) { if (!--to) { mb_spi_err = 1; return 0xFFu; } }
*(volatile uint8_t *)&SPI2->DR = v;
to = MB_SPI_TIMEOUT;
while (!(SPI2->SR & SPI_SR_RXNE)) { if (!--to) { mb_spi_err = 1; return 0xFFu; } }
return *(volatile uint8_t *)&SPI2->DR;
}
/*
* Put SPI2 into clean polled mode before a manual read. The flash driver leaves
* SPI2 in "DMA mode": the RX/TX DMA requests stay on and the DMA channels stay
* armed (confirmed by the SPI-state diagnostic: RD.EN=1 WR.EN=1). A polled read
* then loses every received byte to the still-armed DMA, so RXNE never sets and
* the read hangs forever. Disabling the DMA requests + channels and draining the
* RX FIFO restores plain polled behaviour. The next driver operation re-arms DMA
* on its own, so this is safe.
*/
static void mb_spi_polled_mode(void)
{
SPI2->CR2 &= ~(SPI_CR2_RXDMAEN | SPI_CR2_TXDMAEN);
DMA1_Channel4->CCR &= ~DMA_CCR_EN;
DMA1_Channel5->CCR &= ~DMA_CCR_EN;
/* Drain any pending RX. Bounded: the RX FIFO is only a few bytes deep, so a
* stuck/overrun RXNE (which would otherwise loop forever) can't hang here. */
for (uint32_t guard = 64; (SPI2->SR & SPI_SR_RXNE) && guard; guard--)
(void)*(volatile uint8_t *)&SPI2->DR;
}
/*
* CRC-32 of `len` bytes of external flash starting at `addr`, read in ONE
* continuous polled transfer (command 0x03, CS held low, auto-incrementing
* address). This avoids issuing hundreds of tiny back-to-back DMA reads through
* PY25Q16_ReadBuffer(), which is not reliable at that rate.
*/
#define MB_READ_CHUNK 2048u
static uint32_t mb_ext_image_crc32(uint32_t addr, uint32_t len)
{
uint32_t crc = 0xFFFFFFFFu;
mb_spi_polled_mode();
/* Read in chunks. IRQs are masked during each chunk's continuous transfer
* (an interrupt mid-transfer desyncs the polled SPI - the same reason the
* RAM copier masks them), but re-enabled between chunks so the USB stack
* keeps being serviced and the reply can go out afterwards. */
while (len && !mb_spi_err)
{
uint32_t chunk = (len < MB_READ_CHUNK) ? len : MB_READ_CHUNK;
uint32_t primask = __get_PRIMASK();
__disable_irq();
GPIOA->BRR = MB_CS_PIN; /* CS low */
mb_spi_byte(0x03u);
mb_spi_byte((addr >> 16) & 0xFFu);
mb_spi_byte((addr >> 8) & 0xFFu);
mb_spi_byte(addr & 0xFFu);
for (uint32_t i = 0; i < chunk && !mb_spi_err; i++)
{
crc ^= mb_spi_byte(0xFFu);
for (int k = 0; k < 8; k++)
crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
}
GPIOA->BSRR = MB_CS_PIN; /* CS high */
__set_PRIMASK(primask); /* let IRQs / USB breathe */
addr += chunk;
len -= chunk;
}
return crc ^ 0xFFFFFFFFu;
}
/* Polled read of `len` bytes from external flash into `buf` (no DMA), matching
* the technique the RAM copier uses. Sets mb_spi_err on a stuck SPI. */
static void mb_ext_read(uint32_t addr, uint8_t *buf, uint32_t len)
{
mb_spi_polled_mode();
/* IRQs off during the transfer (see mb_ext_image_crc32); break on timeout. */
uint32_t primask = __get_PRIMASK();
__disable_irq();
GPIOA->BRR = MB_CS_PIN;
mb_spi_byte(0x03u);
mb_spi_byte((addr >> 16) & 0xFFu);
mb_spi_byte((addr >> 8) & 0xFFu);
mb_spi_byte(addr & 0xFFu);
while (len-- && !mb_spi_err)
*buf++ = mb_spi_byte(0xFFu);
GPIOA->BSRR = MB_CS_PIN;
__set_PRIMASK(primask);
}
/* -------------------------------------------------------------------------- */
/* External flash raw erase/program (polled, flash-resident). */
/* */
/* Used only by the M4 slot-management commands. These bypass the stateful */
/* PY25Q16 driver (its sector cache would desync when we erase and program a */
/* slot behind its back, and its per-chunk read-modify-write would erase a */
/* sector on every small chunk). Each CS-framed transaction masks IRQs for */
/* its own burst only - an interrupt mid-transfer desyncs the polled SPI, the */
/* same reason mb_ext_image_crc32 masks them - and re-enables them between */
/* transactions so USB keeps being serviced (notably across the long erase). */
/* -------------------------------------------------------------------------- */
#define MB_EXT_CMD_WREN 0x06u /* write enable */
#define MB_EXT_CMD_PP 0x02u /* page program (<=256 B) */
#define MB_EXT_CMD_SE 0x20u /* 4 KiB sector erase */
#define MB_EXT_CMD_RDSR 0x05u /* read status register 1 */
#define MB_EXT_SECTOR 0x1000u /* PY25Q16 erase granularity */
#define MB_EXT_PAGE 0x100u /* PY25Q16 program granularity */
#define MB_EXT_WIP_TIMEOUT 5000000u /* status polls before giving up (~seconds) */
static void mb_ext_wren(void)
{
uint32_t primask = __get_PRIMASK();
__disable_irq();
GPIOA->BRR = MB_CS_PIN;
mb_spi_byte(MB_EXT_CMD_WREN);
GPIOA->BSRR = MB_CS_PIN;
__set_PRIMASK(primask);
}
/* Poll WIP until the erase/program finishes (or mb_spi_err / timeout). IRQs are
* masked only for each 2-byte status read, not the whole wait, so a ~300 ms
* erase does not starve USB. */
static bool mb_ext_wait_wip(void)
{
for (uint32_t i = 0; i < MB_EXT_WIP_TIMEOUT; i++)
{
uint32_t primask = __get_PRIMASK();
__disable_irq();
GPIOA->BRR = MB_CS_PIN;
mb_spi_byte(MB_EXT_CMD_RDSR);
uint8_t status = mb_spi_byte(0xFFu);
GPIOA->BSRR = MB_CS_PIN;
__set_PRIMASK(primask);
if (mb_spi_err)
return false;
if (!(status & 1u)) /* WIP clear */
return true;
}
return false;
}
static bool mb_ext_sector_erase(uint32_t addr)
{
mb_ext_wren();
if (mb_spi_err)
return false;
uint32_t primask = __get_PRIMASK();
__disable_irq();
GPIOA->BRR = MB_CS_PIN;
mb_spi_byte(MB_EXT_CMD_SE);
mb_spi_byte((addr >> 16) & 0xFFu);
mb_spi_byte((addr >> 8) & 0xFFu);
mb_spi_byte(addr & 0xFFu);
GPIOA->BSRR = MB_CS_PIN;
__set_PRIMASK(primask);
if (mb_spi_err)
return false;
return mb_ext_wait_wip();
}
/* Program up to one 256-byte page; the caller must not cross a page boundary. */
static bool mb_ext_page_program(uint32_t addr, const uint8_t *data, uint32_t len)
{
mb_ext_wren();
if (mb_spi_err)
return false;
uint32_t primask = __get_PRIMASK();
__disable_irq();
GPIOA->BRR = MB_CS_PIN;
mb_spi_byte(MB_EXT_CMD_PP);
mb_spi_byte((addr >> 16) & 0xFFu);
mb_spi_byte((addr >> 8) & 0xFFu);
mb_spi_byte(addr & 0xFFu);
for (uint32_t i = 0; i < len && !mb_spi_err; i++)
mb_spi_byte(data[i]);
GPIOA->BSRR = MB_CS_PIN;
__set_PRIMASK(primask);
if (mb_spi_err)
return false;
return mb_ext_wait_wip();
}
/* Program an arbitrary range, split on 256-byte page boundaries (a page program
* that crosses a page boundary wraps within the page instead of advancing). */
static bool mb_ext_program(uint32_t addr, const uint8_t *data, uint32_t len)
{
while (len)
{
uint32_t pageRem = MB_EXT_PAGE - (addr & (MB_EXT_PAGE - 1u));
uint32_t n = (len < pageRem) ? len : pageRem;
if (!mb_ext_page_program(addr, data, n))
return false;
addr += n;
data += n;
len -= n;
}
return true;
}
/* Read + validate a slot header only (no CRC recompute), via polled reads. */
static uint8_t mb_read_header(uint8_t slot, mb_slot_header_t *hdr)
{
if (slot >= MB_SLOT_COUNT)
return MB_ERR_SLOT;
const uint32_t slotBase = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE;
mb_spi_err = 0;
mb_ext_read(slotBase, (uint8_t *)hdr, sizeof(*hdr));
if (mb_spi_err) return MB_ERR_SPI;
if (hdr->magic != MB_SLOT_MAGIC) return MB_ERR_MAGIC;
if (hdr->hdr_version > MB_HDR_VERSION) return MB_ERR_VERSION;
if (!(hdr->flags & MB_FLAG_COMMITTED)) return MB_ERR_NOT_COMMITTED;
if (hdr->image_size == 0 || hdr->image_size > MB_INT_APP_SIZE) return MB_ERR_SIZE;
return MB_OK;
}
/* Validate one slot (header + image CRC-32), entirely via polled reads.
* Never touches the internal flash. */
static uint8_t mb_validate(uint8_t slot, mb_slot_header_t *hdr, uint32_t *crcOut)
{
uint8_t err = mb_read_header(slot, hdr);
if (err != MB_OK)
return err;
const uint32_t slotBase = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE;
mb_spi_err = 0;
uint32_t crc = mb_ext_image_crc32(slotBase + MB_SLOT_IMG_OFFSET, hdr->image_size);
if (crcOut)
*crcOut = crc;
if (mb_spi_err) return MB_ERR_SPI;
if (crc != hdr->image_crc32) return MB_ERR_CRC;
return MB_OK;
}
uint8_t MB_ValidateSlot(uint8_t slot, mb_slot_header_t *out_header, uint32_t *out_crc)
{
mb_slot_header_t local;
return mb_validate(slot, out_header ? out_header : &local, out_crc);
}
uint8_t MB_ValidateSlot0(uint32_t *out_crc)
{
return MB_ValidateSlot(0, NULL, out_crc);
}
uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line)
{
mb_slot_header_t hdr;
uint8_t err = mb_validate(slot, &hdr, NULL);
if (err != MB_OK)
return err;
const uint32_t slotBase = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE;
/* Valid: the RAM stub copies exactly image_size bytes, pads the partial
* page with 0xFF and erases the remainder of the application region. */
MB_PrepareInternalFlash();
/* Call through a volatile pointer so the compiler emits an absolute 'blx'
* (the RAM copy sits far beyond a Cortex-M0+ 'bl' reach from flash). */
void (*volatile ramReflash)(uint32_t, uint32_t, uint32_t, uint8_t *) = MB_RamReflash;
ramReflash(MB_INT_APP_BASE, slotBase + MB_SLOT_IMG_OFFSET,
hdr.image_size, progress_line);
return MB_OK; /* not reached */
}
uint8_t MB_RestoreSlot0(void)
{
return MB_RestoreSlot(0, NULL);
}
/* -------------------------------------------------------------------------- */
/* M4 slot management (host tool). External flash only - never brick-critical.*/
/* -------------------------------------------------------------------------- */
uint8_t MB_SlotInfo(uint8_t slot, mb_slot_header_t *out_header)
{
mb_slot_header_t local;
return mb_read_header(slot, out_header ? out_header : &local);
}
uint8_t MB_SlotErase(uint8_t slot)
{
if (slot >= MB_SLOT_COUNT)
return MB_ERR_SLOT;
const uint32_t base = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE;
mb_spi_err = 0;
mb_spi_polled_mode();
for (uint32_t off = 0; off < MB_SLOT_STRIDE; off += MB_EXT_SECTOR)
if (!mb_ext_sector_erase(base + off))
return MB_ERR_SPI;
return MB_OK;
}
uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len)
{
if (slot >= MB_SLOT_COUNT)
return MB_ERR_SLOT;
if (len == 0)
return MB_OK;
if (offset > MB_SLOT_STRIDE || len > MB_SLOT_STRIDE - offset)
return MB_ERR_SIZE;
const uint32_t base = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE;
mb_spi_err = 0;
mb_spi_polled_mode();
if (!mb_ext_program(base + offset, data, len))
return MB_ERR_SPI;
return MB_OK;
}
/* Middle of slot 0's image (32 KiB in, well within the 118 KiB image body). */
#define MB_CORRUPT_OFFSET 0x8000u
#define MB_CORRUPT_MAX 128u
void MB_CorruptSlot0(const uint8_t *data, uint32_t len)
{
if (!data || len == 0)
return;
if (len > MB_CORRUPT_MAX)
len = MB_CORRUPT_MAX;
/* Constrained to slot 0's image body: this address range cannot reach the
* slot header, nor calibration / EEPROM / RF log / voice regions. */
PY25Q16_WriteBuffer(MB_SLOT0_EXT_BASE + MB_SLOT_IMG_OFFSET + MB_CORRUPT_OFFSET,
data, len, false);
}
volatile uint8_t mb_mark_on = 0;
void MB_Mark(const char *s)
{
if (!mb_mark_on)
return;
memset(gFrameBuffer, 0, sizeof(gFrameBuffer));
UI_PrintStringSmallNormal(s, 10, 0, 3);
ST7565_BlitFullScreen();
}
void MB_DumpExt(uint32_t addr, uint8_t *buf, uint32_t len)
{
/* Trace the driver read step by step on the LCD so a freeze reveals where. */
mb_mark_on = 1;
MB_Mark("DUMP begin");
PY25Q16_ReadBufferSafe(addr, buf, len);
MB_Mark("DUMP done");
mb_mark_on = 0;
}
void MB_SpiState(uint32_t out[4])
{
out[0] = SPI2->CR1; /* bit 6 (SPE) = SPI enabled */
out[1] = SPI2->CR2; /* bit0 RXDMAEN, bit1 TXDMAEN */
out[2] = SPI2->SR; /* bit0 RXNE, bit1 TXE, bit7 BSY */
out[3] = DMA1_Channel4->CCR; /* SPI2 RX DMA channel (bit0 EN) */
}
+168
View File
@@ -0,0 +1,168 @@
/* Copyright 2026 F4HWN
*
* 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.
*
* See docs/multiboot-design.md for the overall design.
*/
#ifndef DRIVER_MB_FLASH_H
#define DRIVER_MB_FLASH_H
#include <stdint.h>
#include <stdbool.h>
/* 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 (see docs/multiboot-design.md).
* Each 128 KiB slot = one header sector (4 KiB) followed by the image. */
#define MB_SLOT_STRIDE 0x00020000u /* 128 KiB per slot */
#define MB_SLOT_IMG_OFFSET 0x00001000u /* image starts after header sector */
#define MB_SLOT0_EXT_BASE 0x00020000u /* slot 0 header base */
#define MB_SLOT_COUNT 4u
/* 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;
/* Restore validation result (MB_OK never returns - the radio resets). */
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 index out of range */
MB_ERR_AUTH /* write refused: timestamp mismatch */
};
/*
* Copy the live internal application image into external flash slot 0, writing
* the image first and then a valid header (COMMITTED) last. Runs entirely from
* flash and only writes the external SPI flash, so it is safe (no brick risk).
* Reports the stored image size and CRC-32 through the (optional) out params.
*/
void MB_BackupToSlot0(uint32_t *out_size, uint32_t *out_crc32);
/*
* Validate slot 0 and, if valid, reflash the internal application from it and
* reset. Validation (magic, version, COMMITTED flag, size, image CRC-32) runs
* from flash *before* any erase: on failure it returns an MB_ERR_* code and the
* internal flash is left untouched. On success it NEVER RETURNS (the RAM-resident
* copier reflashes the internal application and triggers a system reset). The
* factory bootloader is never touched, so an interrupted copy is recoverable
* over USB/DFU.
*/
uint8_t MB_RestoreSlot0(void);
/* 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. */
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);
/*
* Test helper: overwrite up to `len` (capped) bytes in the MIDDLE of slot 0's
* image with the supplied data, so the next MB_RestoreSlot0() fails its CRC-32
* check and refuses. It writes ONLY inside slot 0's image body - never the
* header, and never anything outside the slot (calibration, EEPROM, RF log...).
* Developer aid to exercise the safe-refusal path without external tooling.
*/
void MB_CorruptSlot0(const uint8_t *data, uint32_t len);
/*
* Validate slot 0 (header + image CRC-32) WITHOUT reflashing. Same checks as the
* restore path, entirely via polled reads (cannot hang). Returns MB_OK or an
* MB_ERR_* code, and reports the computed image CRC-32 through out_crc.
* Useful as a safe diagnostic from the host tool.
*/
uint8_t MB_ValidateSlot0(uint32_t *out_crc);
/*
* Read `len` bytes of external flash at `addr` into `buf`, via the DMA driver
* (PY25Q16_ReadBuffer) - the same proven read path the backup uses internally.
* Ground-truth diagnostic to check what is actually stored in a slot.
*/
void MB_DumpExt(uint32_t addr, uint8_t *buf, uint32_t len);
/*
* Diagnostic: snapshot the SPI2 / DMA hardware state WITHOUT any flash access
* (so it cannot hang). out[0]=SPI2->CR1, [1]=SPI2->SR, [2]=DMA RD chan CCR,
* [3]=DMA WR chan CCR. Reveals whether SPI2 is disabled/busy or a DMA channel
* is left armed when a read command runs.
*/
void MB_SpiState(uint32_t out[4]);
/*
* On-screen trace marker (debug). Draws `s` on the LCD (SPI1, independent of the
* flash SPI2) so that when a flash read freezes the CPU, the frozen screen shows
* the last step reached. Only draws while mb_mark_on is set (i.e. during a Dump),
* so it does not flash the screen during normal writes.
*/
extern volatile uint8_t mb_mark_on;
void MB_Mark(const char *s);
#endif /* DRIVER_MB_FLASH_H */
+24
View File
@@ -27,6 +27,13 @@
#include "external/printf/printf.h"
#include "misc.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#define MBMARK(s) MB_Mark(s)
#else
#define MBMARK(s)
#endif
// #define DEBUG
#define SPIx SPI2
@@ -227,17 +234,21 @@ void PY25Q16_Init()
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
{
MBMARK("RD cmd"); // about to assert CS + send read command
CS_Assert();
SPI_WriteByte(0x03); // Send read command
MBMARK("RD addr"); // command sent, about to send address
WriteAddr(Address); // Send address (3 bytes)
MBMARK("RD flush"); // address sent, about to flush RX FIFO
// CRITICAL: Flush RX FIFO before DMA to remove residual data
while (LL_SPI_RX_FIFO_EMPTY != LL_SPI_GetRxFIFOLevel(SPIx))
{
LL_SPI_ReceiveData8(SPIx); // Read and discard
}
MBMARK("RD data"); // FIFO flushed, about to read the data
if (Size >= 16) {
SPI_ReadBuf((uint8_t *)pBuffer, Size);
} else {
@@ -247,9 +258,22 @@ void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
}
}
MBMARK("RD end"); // data read, about to release CS
CS_Release();
}
// Like PY25Q16_ReadBuffer, but waits for the flash to be idle first (WIP=0),
// exactly as PY25Q16_WriteBuffer does before its internal reads. A standalone
// read issued while the chip is still busy from a prior program/erase never
// returns the expected data.
void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size)
{
MBMARK("SAFE wip"); // about to WaitWIP()
WaitWIP();
MBMARK("SAFE rb"); // WaitWIP done, about to ReadBuffer
PY25Q16_ReadBuffer(Address, pBuffer, Size);
}
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append)
{
#ifdef DEBUG
+1
View File
@@ -22,6 +22,7 @@
void PY25Q16_Init();
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append);
void PY25Q16_SectorErase(uint32_t Address);
+27 -2
View File
@@ -29,20 +29,40 @@
#include "settings.h"
#include "ui/menu.h"
#include "ui/ui.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "ui/multiboot.h"
#endif
BOOT_Mode_t BOOT_GetMode(void)
{
unsigned int i;
KEY_Code_t Keys[2];
bool PttPressed[2];
/* Poll the keypad even without PTT: holding MENU alone enters multiboot.
* The two samples keep the same debounce rule as the legacy boot modes. */
for (i = 0; i < 2; i++)
{
if (!GPIO_IsPttPressed())
return BOOT_MODE_NORMAL; // PTT not pressed
PttPressed[i] = GPIO_IsPttPressed();
Keys[i] = KEYBOARD_Poll();
SYSTEM_DelayMs(20);
}
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
if (!PttPressed[0] && !PttPressed[1] &&
Keys[0] == KEY_MENU && Keys[1] == KEY_MENU)
{
gKeyReading0 = Keys[0];
gKeyReading1 = Keys[0];
gDebounceCounter = 2;
return BOOT_MODE_MULTIBOOT;
}
#endif
/* All historical special modes still require PTT for both samples. */
if (!PttPressed[0] || !PttPressed[1])
return BOOT_MODE_NORMAL;
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
if (Keys[0] == (10 + gEeprom.SET_KEY))
{
@@ -125,5 +145,10 @@ void BOOT_ProcessMode(BOOT_Mode_t Mode)
}
#endif
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
else if (Mode == BOOT_MODE_MULTIBOOT)
UI_MultibootSelector();
#endif
GUI_SelectNextDisplay(display);
}
+4 -1
View File
@@ -28,7 +28,10 @@ enum BOOT_Mode_t
BOOT_MODE_RESCUE_OPS,
#endif
#ifdef ENABLE_AIRCOPY
BOOT_MODE_AIRCOPY
BOOT_MODE_AIRCOPY,
#endif
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
BOOT_MODE_MULTIBOOT,
#endif
};
+10
View File
@@ -132,6 +132,16 @@ void Main(void)
BOOT_Mode_t BootMode = BOOT_GetMode();
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Run before the welcome screen and the normal application UI. EXIT from
* the selector simply resumes this boot as if no special mode was held. */
if (BootMode == BOOT_MODE_MULTIBOOT)
{
BOOT_ProcessMode(BootMode);
BootMode = BOOT_MODE_NORMAL;
}
#endif
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
if (BootMode == BOOT_MODE_RESCUE_OPS)
{
+266
View File
@@ -0,0 +1,266 @@
/* Copyright 2026 F4HWN
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "driver/backlight.h"
#include "driver/gpio.h"
#include "driver/keyboard.h"
#include "driver/mb_flash.h"
#include "driver/st7565.h"
#include "driver/system.h"
#include "ui/helper.h"
#include "ui/multiboot.h"
static const char *mb_error_text(uint8_t err)
{
switch (err)
{
case MB_OK: return "OK";
case MB_ERR_MAGIC: return "empty";
case MB_ERR_VERSION: return "new header";
case MB_ERR_NOT_COMMITTED: return "incomplete";
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_AUTH: return "auth";
default: return "error";
}
}
static void mb_copy_label(char *dst, uint8_t cap, const char *src, uint8_t src_cap)
{
uint8_t n = 0;
while (n + 1u < cap && n < src_cap && src[n])
{
dst[n] = src[n];
n++;
}
dst[n] = 0;
}
static void mb_format_slot_label(char *dst, uint8_t cap, const mb_slot_header_t *header)
{
const char *version = NULL;
uint8_t version_cap = 0;
uint8_t version_len = 0;
uint8_t name_limit = cap - 1u;
uint8_t n = 0;
if (!header->name[0])
{
mb_copy_label(dst, cap, header->fw_version, MB_VERSION_LEN);
return;
}
for (uint8_t i = 0; i + 1u < MB_VERSION_LEN && header->fw_version[i]; i++)
{
if (header->fw_version[i] == 'v' &&
header->fw_version[i + 1u] >= '0' &&
header->fw_version[i + 1u] <= '9')
{
version = &header->fw_version[i + 1u];
version_cap = MB_VERSION_LEN - i - 1u;
break;
}
}
if (version)
{
while (version_len < version_cap && version[version_len])
version_len++;
if (version_len + 1u < cap)
name_limit = cap - version_len - 2u;
}
while (n < name_limit && n < MB_NAME_LEN && header->name[n])
{
dst[n] = header->name[n];
n++;
}
if (version && n + version_len + 1u < cap)
{
dst[n++] = ' ';
for (uint8_t i = 0; i < version_len; i++)
dst[n++] = version[i];
}
dst[n] = 0;
}
static void mb_show_message(const char *line1, const char *line2, const char *line3)
{
UI_DisplayClear();
UI_StatusClear();
UI_PrintStringSmallNormal("MultiBoot", 2, 126, 0);
if (line1) UI_PrintStringSmallNormal(line1, 2, 126, 2);
if (line2) UI_PrintStringSmallNormal(line2, 2, 126, 4);
if (line3) UI_PrintStringSmallNormal(line3, 2, 126, 6);
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
}
static void mb_wait_release(void)
{
uint8_t stable = 0;
while (stable < 10u)
{
if (!GPIO_IsPttPressed() && KEYBOARD_Poll() == KEY_INVALID)
stable++;
else
stable = 0;
SYSTEM_DelayMs(10);
}
}
static KEY_Code_t mb_get_key(void)
{
for (;;)
{
KEY_Code_t key = KEYBOARD_Poll();
if (key != KEY_INVALID)
{
SYSTEM_DelayMs(30);
if (KEYBOARD_Poll() == key)
{
while (KEYBOARD_Poll() != KEY_INVALID)
SYSTEM_DelayMs(10);
return key;
}
}
SYSTEM_DelayMs(10);
}
}
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");
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
status[slot] = MB_ValidateSlot(slot, &headers[slot], NULL);
}
static void mb_render_slots(uint8_t selected,
const mb_slot_header_t headers[MB_SLOT_COUNT],
const uint8_t status[MB_SLOT_COUNT])
{
char line[19]; /* 18 glyphs max: 18 * 7 px fits from x=2 to x=126. */
UI_DisplayClear();
UI_StatusClear();
UI_PrintStringSmallNormal("MultiBoot", 2, 126, 0);
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
memset(line, 0, sizeof(line));
line[0] = (slot == selected) ? '>' : ' ';
line[1] = (char)('0' + slot);
line[2] = ' ';
if (status[slot] == MB_OK)
mb_format_slot_label(&line[3], sizeof(line) - 3u, &headers[slot]);
else
mb_copy_label(&line[3], sizeof(line) - 3u, mb_error_text(status[slot]), 20u);
UI_PrintStringSmallNormal(line, 2, 0, (uint8_t)(slot + 1u));
}
UI_PrintStringSmallNormal("MENU to select", 2, 126, 5);
UI_PrintStringSmallNormal("EXIT to go back", 2, 126, 6);
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
}
static void mb_prepare_progress(uint8_t slot)
{
char title[] = "RESTORE SLOT 0";
title[13] = (char)('0' + slot);
UI_DisplayClear();
UI_StatusClear();
UI_PrintStringSmallNormal(title, 2, 126, 0);
UI_PrintStringSmallNormal("DO NOT POWER OFF", 2, 126, 2);
UI_PrintStringSmallNormal("Writing & Verify", 2, 126, 4);
/* Same rounded outline and hatch pattern as the scan progress gauge. */
gFrameBuffer[6][3] = 0x0Cu;
gFrameBuffer[6][4] = 0x12u;
gFrameBuffer[6][123] = 0x12u;
gFrameBuffer[6][124] = 0x0Cu;
for (uint8_t x = 5; x < 123u; x++)
gFrameBuffer[6][x] = 0x21u;
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
}
void UI_MultibootSelector(void)
{
mb_slot_header_t headers[MB_SLOT_COUNT];
uint8_t status[MB_SLOT_COUNT];
uint8_t selected = 0;
BACKLIGHT_TurnOn();
mb_show_message("Release keys", NULL, NULL);
mb_wait_release();
mb_scan_slots(headers, status);
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
if (status[slot] == MB_OK)
{
selected = slot;
break;
}
}
for (;;)
{
mb_render_slots(selected, headers, status);
KEY_Code_t key = mb_get_key();
if (key == KEY_EXIT)
{
/* MENU was latched by BOOT_GetMode(). Do not let that stale boot
* key reach the normal application after leaving the selector. */
gKeyReading0 = KEY_INVALID;
gKeyReading1 = KEY_INVALID;
gDebounceCounter = 0;
return;
}
if (key == KEY_UP)
{
selected = (uint8_t)((selected + MB_SLOT_COUNT - 1u) % MB_SLOT_COUNT);
continue;
}
if (key == KEY_DOWN)
{
selected = (uint8_t)((selected + 1u) % MB_SLOT_COUNT);
continue;
}
if (key != KEY_MENU)
continue;
if (status[selected] != MB_OK)
{
mb_show_message("SLOT NOT VALID", mb_error_text(status[selected]), "Press any key");
(void)mb_get_key();
continue;
}
char confirm[] = "Restore slot 0?";
confirm[13] = (char)('0' + selected);
mb_show_message(confirm, "MENU to confirm", "EXIT to cancel");
key = mb_get_key();
if (key != KEY_MENU)
continue;
mb_prepare_progress(selected);
uint8_t err = MB_RestoreSlot(selected, gFrameBuffer[6]);
/* Only reached when the final pre-erase validation refused the slot. */
status[selected] = err;
mb_show_message("RESTORE REFUSED", mb_error_text(err), "Press any key");
(void)mb_get_key();
mb_scan_slots(headers, status);
}
}
+12
View File
@@ -0,0 +1,12 @@
/* Copyright 2026 F4HWN
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef UI_MULTIBOOT_H
#define UI_MULTIBOOT_H
/* Blocking boot-time slot selector. Returns only when the user chooses EXIT;
* a successful restore resets the radio from the RAM-resident copier. */
void UI_MultibootSelector(void);
#endif
+4 -1
View File
@@ -81,17 +81,20 @@
"ENABLE_FEAT_F4HWN_LOGO": false,
"ENABLE_FEAT_F4HWN_LOGO_SAV": false,
"ENABLE_FEAT_F4HWN_MENU_CAT": false,
"ENABLE_FEAT_F4HWN_MULTIBOOT": false,
"ENABLE_AGC_SHOW_DATA": false,
"ENABLE_UART_RW_BK_REGS": false,
"ENABLE_SWD": false,
"VERSION_STRING_1": "v0.22",
"VERSION_STRING_2": "v5.8.1"
"VERSION_STRING_2": "v5.9.0"
}
},
{
"name": "Custom",
"inherits": "default",
"cacheVariables": {
"ENABLE_FEAT_F4HWN_QRCODE": true,
"ENABLE_FEAT_F4HWN_MEM": true,
"EDITION_STRING": "Custom",
"TARGET": "f4hwn.custom"
}
+4
View File
@@ -138,6 +138,10 @@ SECTIONS
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.RamFunc) /* functions that must execute from RAM (e.g. internal */
*(.RamFunc*) /* flash reprogramming); copied to RAM with .data */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
+333
View File
@@ -0,0 +1,333 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Multiboot M1 - bench test</title>
<style>
:root {
--bg:#0f1216; --panel:#171b21; --line:#272d36; --fg:#e6e9ee; --muted:#9aa4b2;
--accent:#3aa0ff; --ok:#3fbf6b; --danger:#e5484d; --warnbg:#2a2114; --warnline:#6b5424;
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
* { box-sizing:border-box; }
body {
margin:0; background:var(--bg); color:var(--fg);
font:15px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
display:flex; justify-content:center; padding:24px;
}
.wrap { width:100%; max-width:760px; }
h1 { font-size:20px; margin:0 0 2px; }
.sub { color:var(--muted); font-size:13px; margin-bottom:18px; }
.panel { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:16px; margin-bottom:14px; }
.warn { background:var(--warnbg); border-color:var(--warnline); font-size:13px; color:#e8d9b0; }
.warn b { color:#f4e6c0; }
.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
button {
font:inherit; font-weight:600; color:var(--fg); background:#222831;
border:1px solid var(--line); border-radius:8px; padding:10px 16px; cursor:pointer;
}
button:hover:not(:disabled) { border-color:var(--accent); }
button:disabled { opacity:.4; cursor:not-allowed; }
button.primary { background:var(--accent); border-color:var(--accent); color:#04121f; }
button.danger { background:var(--danger); border-color:var(--danger); color:#1a0405; }
button.warnbtn { background:transparent; border-color:var(--warnline); color:#e5b84d; }
button.warnbtn:hover:not(:disabled) { border-color:#e5b84d; }
.status { display:flex; align-items:center; gap:8px; margin-left:auto; font-size:13px; color:var(--muted); }
.dot { width:9px; height:9px; border-radius:50%; background:#555; }
.dot.on { background:var(--ok); box-shadow:0 0 8px var(--ok); }
.dot.err { background:var(--danger); }
label.baud { font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px; }
input { font:inherit; width:80px; background:#0d1015; color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:6px 8px; }
.steps { font-size:13px; color:var(--muted); margin:0 0 14px; padding-left:18px; }
.steps li { margin:3px 0; }
.steps code { color:var(--fg); }
#log {
font:12.5px/1.5 var(--mono); background:#0b0e12; border:1px solid var(--line);
border-radius:8px; padding:12px; height:280px; overflow:auto; white-space:pre-wrap; word-break:break-word;
}
.l-tx { color:var(--accent); } .l-rx { color:var(--ok); }
.l-warn { color:#e5b84d; } .l-err { color:var(--danger); } .l-info { color:var(--muted); }
.logbar { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
.logbar button { padding:5px 10px; font-size:12px; font-weight:500; }
.unsupported { display:none; }
</style>
</head>
<body>
<div class="wrap">
<h1>Multiboot M1 &mdash; bench test</h1>
<div class="sub">UV-K1 / UV-K5 V3 &middot; F4HWN &middot; commandes cachées <code>0x0710</code> / <code>0x0712</code> via Web Serial</div>
<div class="panel warn">
<b>Radio sacrifiable uniquement.</b> Nécessite un firmware compilé avec
<code>-DENABLE_FEAT_F4HWN_MULTIBOOT=ON</code>. Le bootloader d'usine n'est jamais touché :
en cas d'échec du <i>restore</i>, re-flasher via USB/DFU dans UV Studio.
Ferme UV Studio / K5Viewer avant de te connecter (le port série ne peut être ouvert qu'une fois).
</div>
<div id="unsupported" class="panel warn unsupported">
<b>Web Serial indisponible.</b> Utilise Chrome, Edge, Brave ou Opera sur ordinateur.
Si tu as ouvert ce fichier en <code>file://</code> et que ça ne marche pas, sers-le en local :
<code>python -m http.server</code> puis ouvre <code>http://localhost:8000/tools/mb_test.html</code>.
</div>
<div class="panel">
<div class="row">
<button id="btnConnect" class="primary">Connecter</button>
<button id="btnDisconnect" disabled>Déconnecter</button>
<label class="baud">baud <input id="baud" type="number" value="38400" title="ignoré sur le port USB-C virtuel"></label>
<span class="status"><span id="dot" class="dot"></span><span id="statusText">déconnecté</span></span>
</div>
</div>
<div class="panel">
<ol class="steps">
<li><b>Backup</b> — copie l'app interne vers le slot 0 externe (sans danger). Attends l'ack.</li>
<li>Optionnel : dumpe le slot 0 (<code>0x020000</code>) dans UV Studio pour vérifier.</li>
<li><b>Restore + reset</b> — reprogramme l'interne depuis le slot 0. Succès = la radio reboote sur le même firmware.</li>
</ol>
<div class="row">
<button id="btnBackup" disabled>Backup &nbsp;<small>0x0710</small></button>
<button id="btnRestore" class="danger" disabled>Restore + reset &nbsp;<small>0x0712</small></button>
</div>
<div class="row" style="margin-top:10px">
<button id="btnCorrupt" class="warnbtn" disabled>Corrompre le slot (random) &nbsp;<small>0x0714</small></button>
<button id="btnDiag" class="warnbtn" disabled>Valider slot 0 &nbsp;<small>0x0716</small></button>
<button id="btnDump" class="warnbtn" disabled>Dump slot 0 &nbsp;<small>0x0718</small></button>
<button id="btnSpi" class="warnbtn" disabled>Diag SPI &nbsp;<small>0x071C</small></button>
</div>
<div class="row" style="margin-top:6px">
<span class="sub" style="margin:0"><b>Corrompre</b> → le prochain Restore doit être refusé (CRC32). <b>Valider</b> → contrôle header + CRC32 sans rien reflasher, renvoie le verdict et le CRC calculé (ne peut pas se bloquer).</span>
</div>
</div>
<div class="panel">
<div class="logbar">
<span class="sub" style="margin:0">Journal</span>
<button id="btnClear">Effacer</button>
</div>
<div id="log"></div>
</div>
</div>
<script>
"use strict";
// Mirror of App/app/uart.c (Obfuscation[]) and App/driver/crc.c (CRC-16/XMODEM).
const OBF = [0x16,0x6C,0x14,0xE6,0x2E,0x91,0x0D,0x40,0x21,0x35,0xD5,0x40,0x13,0x03,0xE9,0x80];
const CMD_BACKUP = 0x0710, CMD_RESTORE = 0x0712;
// Restore refusal codes (mb_flash.h). 0 = OK (never reported: the radio resets).
const MB_ERR = {
1: "pas de header de slot valide (magic)",
2: "version de header trop récente",
3: "image non marquée complète (COMMITTED)",
4: "taille d'image invalide",
5: "CRC32 de l'image incorrect",
6: "timeout lecture flash externe (SPI)",
};
let port = null, writer = null, keepReading = false;
const rx = []; // rolling received-byte buffer for reply parsing
const $ = id => document.getElementById(id);
const logEl = $("log");
function log(msg, cls="l-info") {
const t = new Date().toLocaleTimeString();
const div = document.createElement("div");
div.className = cls;
div.textContent = `[${t}] ${msg}`;
logEl.appendChild(div);
logEl.scrollTop = logEl.scrollHeight;
}
const hex = arr => Array.from(arr, b => b.toString(16).padStart(2,"0")).join(" ");
function crc16(data) {
let crc = 0;
for (const b of data) {
crc ^= b << 8;
for (let i = 0; i < 8; i++)
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xFFFF : (crc << 1) & 0xFFFF;
}
return crc & 0xFFFF;
}
// Build one obfuscated, CRC'd command frame the firmware will accept.
function buildFrame(id, data = new Uint8Array(0)) {
const payload = new Uint8Array(4 + data.length);
payload[0] = id & 0xFF; payload[1] = (id >> 8) & 0xFF;
payload[2] = data.length & 0xFF; payload[3] = (data.length >> 8) & 0xFF;
payload.set(data, 4);
const size = payload.length;
const crc = crc16(payload);
const body = new Uint8Array(size + 2);
body.set(payload, 0); body[size] = crc & 0xFF; body[size + 1] = (crc >> 8) & 0xFF;
for (let i = 0; i < body.length; i++) body[i] ^= OBF[i % 16]; // obfuscate payload+CRC
const frame = new Uint8Array(4 + body.length + 2);
frame[0] = 0xAB; frame[1] = 0xCD; frame[2] = size & 0xFF; frame[3] = (size >> 8) & 0xFF;
frame.set(body, 4);
frame[frame.length - 2] = 0xDC; frame[frame.length - 1] = 0xBA;
return frame;
}
// Scan the rolling RX buffer for a complete reply frame:
// 0xAB 0xCD | size(2 LE) | body(size, obfuscated) | pad(2) | 0xDC 0xBA
function scanReplies() {
for (let i = 0; i + 4 <= rx.length; i++) {
if (rx[i] !== 0xAB || rx[i+1] !== 0xCD) continue;
const size = rx[i+2] | (rx[i+3] << 8);
const total = 4 + size + 2 + 2; // header + body + pad + footer
if (i + total > rx.length) return; // wait for more bytes
if (rx[i + total - 2] !== 0xDC || rx[i + total - 1] !== 0xBA) continue;
const body = rx.slice(i + 4, i + 4 + size).map((b, k) => b ^ OBF[k % 16]);
const replyId = body[0] | (body[1] << 8);
const u32 = o => (body[o] | (body[o+1] << 8) | (body[o+2] << 16) | (body[o+3] << 24)) >>> 0;
if (replyId === 0x0711 && size >= 12) {
log(`✓ BACKUP OK — image ${u32(4)} o, CRC32 0x${u32(8).toString(16).padStart(8,"0")}`, "l-rx");
} else if (replyId === 0x0713 && size >= 5) {
const code = body[4];
log(`✗ RESTORE refusé — ${MB_ERR[code] || "erreur inconnue"} (code ${code}) → flash interne intacte`, "l-err");
} else if (replyId === 0x0715 && size >= 6) {
const cnt = body[4] | (body[5] << 8);
log(`✓ slot 0 corrompu (${cnt} octets) → le prochain Restore doit être REFUSÉ (CRC32)`, "l-warn");
} else if (replyId === 0x0717 && size >= 9) {
const crc = u32(4), code = body[8];
if (code === 0)
log(`✓ VALIDATION OK — slot 0 sain, CRC32 0x${crc.toString(16).padStart(8,"0")}`, "l-rx");
else
log(`✗ VALIDATION KO — ${MB_ERR[code] || "erreur inconnue"} (code ${code}), CRC calculé 0x${crc.toString(16).padStart(8,"0")}`, "l-err");
} else if (replyId === 0x0719 && size >= 9) {
const addr = u32(4), n = body[8], data = body.slice(9, 9 + n);
const hx = data.map(b => b.toString(16).padStart(2,"0")).join(" ");
log(`DUMP 0x${addr.toString(16).padStart(6,"0")} (${n} o): ${hx}`, "l-info");
const isFMB1 = data[0] === 0x46 && data[1] === 0x4d && data[2] === 0x42 && data[3] === 0x31;
const allFF = data.length > 0 && data.every(b => b === 0xff);
if (isFMB1) log("✓ magic « FMB1 » présent → le backup ÉCRIT bien la flash externe", "l-rx");
else if (allFF) log("✗ zone vierge (tout à FF) → le backup n'écrit PAS la flash externe", "l-err");
else log("⚠ ni FMB1 ni vierge → contenu inattendu (voir hex ci-dessus)", "l-warn");
} else if (replyId === 0x071d && size >= 20) {
const cr1 = u32(4), cr2 = u32(8), sr = u32(12), ccr4 = u32(16);
const spe = (cr1 >> 6) & 1, rxdma = cr2 & 1, txdma = (cr2 >> 1) & 1;
const txe = (sr >> 1) & 1, rxne = sr & 1, bsy = (sr >> 7) & 1;
const dmaOn = rxdma || txdma;
log(`SPI2 CR1=0x${cr1.toString(16).padStart(4,"0")} (SPE=${spe}) · CR2=0x${cr2.toString(16).padStart(4,"0")} (RXDMAEN=${rxdma} TXDMAEN=${txdma}) · SR=0x${sr.toString(16).padStart(4,"0")} (TXE=${txe} BSY=${bsy}) · DMA RD.EN=${ccr4 & 1}`, (spe && !dmaOn) ? "l-rx" : "l-err");
if (!spe) log("→ SPI2 DÉSACTIVÉ : c'est ça qui fige les lectures.", "l-err");
else if (rxdma) log("→ RXDMAEN=1 : l'octet reçu part vers le DMA au lieu de RXNE → lecture polled bloque. (c'est ma théorie)", "l-err");
else if ((ccr4 & 1) && !rxdma && !txdma) log("-> canal DMA arme mais requetes SPI coupees : etat inactif normal.", "l-rx");
else if (ccr4 & 1) log("→ canal DMA RX armé : peut consommer les octets. Le fix le désarme.", "l-warn");
else log("→ SPI2 propre (pas de DMA). Si la lecture bloque encore, c'est ailleurs.", "l-warn");
} else {
log(`reply 0x${replyId.toString(16).padStart(4,"0")} reçu`, "l-rx");
}
rx.splice(0, i + total); // consume up to end of frame
return scanReplies();
}
if (rx.length > 512) rx.splice(0, rx.length - 512);
}
async function readLoop() {
while (port && port.readable && keepReading) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (value && value.length) {
log("RX " + hex(value), "l-rx");
for (const b of value) rx.push(b);
scanReplies();
}
}
} catch (e) {
if (keepReading) log("lecture interrompue: " + e.message + " (reset radio ?)", "l-warn");
} finally {
reader.releaseLock();
}
}
}
async function connect() {
if (!navigator.serial) return;
try {
port = await navigator.serial.requestPort();
await port.open({ baudRate: parseInt($("baud").value, 10) || 38400 });
writer = port.writable.getWriter();
keepReading = true;
rx.length = 0;
readLoop();
setConnected(true);
log("connecté.", "l-info");
} catch (e) {
log("connexion échouée: " + e.message, "l-err");
setConnected(false);
}
}
async function disconnect() {
keepReading = false;
try { if (writer) { writer.releaseLock(); writer = null; } } catch {}
try { if (port) await port.close(); } catch {}
port = null;
setConnected(false);
log("déconnecté.", "l-info");
}
async function send(name, frame) {
if (!writer) { log("non connecté.", "l-err"); return; }
log(`TX ${name}: ` + hex(frame), "l-tx");
await writer.write(frame);
}
function setConnected(on) {
$("dot").className = "dot" + (on ? " on" : "");
$("statusText").textContent = on ? "connecté" : "déconnecté";
$("btnConnect").disabled = on;
$("btnDisconnect").disabled = !on;
$("btnBackup").disabled = !on;
$("btnRestore").disabled = !on;
$("btnCorrupt").disabled = !on;
$("btnDiag").disabled = !on;
$("btnDump").disabled = !on;
$("btnSpi").disabled = !on;
}
$("btnConnect").onclick = connect;
$("btnDisconnect").onclick = disconnect;
$("btnClear").onclick = () => { logEl.textContent = ""; };
$("btnBackup").onclick = () => {
log("BACKUP: la radio est figée quelques secondes pendant l'écriture (~118 Ko)...", "l-info");
send("BACKUP 0x0710", buildFrame(CMD_BACKUP));
};
$("btnRestore").onclick = () => {
log("RESTORE: validation slot 0 puis reflash. Succès = reboot (device lost). Refus = reply 0x0713 (flash intacte).", "l-warn");
send("RESTORE 0x0712", buildFrame(CMD_RESTORE));
};
$("btnCorrupt").onclick = () => {
const rnd = new Uint8Array(32);
crypto.getRandomValues(rnd);
log("CORRUPT: écrit 32 octets aléatoires dans l'image du slot 0 (header intact)...", "l-warn");
send("CORRUPT 0x0714", buildFrame(0x0714, rnd));
};
$("btnDiag").onclick = () => {
log("VALIDER: contrôle header + CRC32 du slot 0 (sans reflash)...", "l-info");
send("VALIDATE 0x0716", buildFrame(0x0716));
};
$("btnDump").onclick = () => {
const addr = 0x020000, len = 8; // < 16 -> forces the driver's POLLED read path
const d = new Uint8Array([addr & 0xFF, (addr >> 8) & 0xFF, (addr >> 16) & 0xFF, (addr >> 24) & 0xFF, len]);
log(`DUMP: lecture de ${len} octets à 0x${addr.toString(16).padStart(6,"0")} via le driver polled (< 16 o)...`, "l-info");
send("DUMP 0x0718", buildFrame(0x0718, d));
};
$("btnSpi").onclick = () => {
log("DIAG SPI: lecture des registres SPI2 / DMA (aucun accès flash, ne peut pas bloquer)...", "l-info");
send("SPI STATE 0x071C", buildFrame(0x071C));
};
if (!navigator.serial) {
$("unsupported").style.display = "block";
["btnConnect","btnDisconnect","btnBackup","btnRestore"].forEach(id => $(id).disabled = true);
}
</script>
</body>
</html>