mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Add mb firmware slots
This commit is contained in:
1 parent
6692b5191b
commit
a25d2fc75e
14 files changed
+1905
-15
No files matched your search
@@ -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
@@ -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);
|
||||
|
||||
@@ -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) */
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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 — bench test</h1>
|
||||
<div class="sub">UV-K1 / UV-K5 V3 · F4HWN · 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 <small>0x0710</small></button>
|
||||
<button id="btnRestore" class="danger" disabled>Restore + reset <small>0x0712</small></button>
|
||||
</div>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="btnCorrupt" class="warnbtn" disabled>Corrompre le slot (random) <small>0x0714</small></button>
|
||||
<button id="btnDiag" class="warnbtn" disabled>Valider slot 0 <small>0x0716</small></button>
|
||||
<button id="btnDump" class="warnbtn" disabled>Dump slot 0 <small>0x0718</small></button>
|
||||
<button id="btnSpi" class="warnbtn" disabled>Diag SPI <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>
|
||||
Reference in new issue
Block a user