mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
1172 lines
42 KiB
C
1172 lines
42 KiB
C
/* Copyright 2026 Armel F4HWN
|
|
* https://github.com/armel
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
|
|
#include "driver/mb_flash.h"
|
|
#include "driver/py25q16.h"
|
|
|
|
#include "py32f0xx.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
|
|
};
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 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. Normally it is placed in */
|
|
/* .RamFunc, which startup copies to RAM alongside .data. With the overlay */
|
|
/* enabled it is linked in .MBRamFunc and copied over the PY25Q16 sector cache */
|
|
/* only immediately before use. */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* These primitives are called repeatedly while application flash is offline.
|
|
* Keep one copy of each in the copied RAM section: noinline/noclone prevents
|
|
* GCC from silently duplicating one back into MB_RamReflash. */
|
|
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
|
|
#define MB_RAM_SECTION ".MBRamFunc"
|
|
#else
|
|
#define MB_RAM_SECTION ".RamFunc"
|
|
#endif
|
|
#define MB_RAM_HELPER __attribute__((section(MB_RAM_SECTION), noinline, noclone, used))
|
|
|
|
/* Polled single-byte SPI2 transfer. The result is returned through out so
|
|
* timeout and received 0xFF remain distinguishable. */
|
|
MB_RAM_HELPER static 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;
|
|
}
|
|
|
|
MB_RAM_HELPER static 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;
|
|
}
|
|
|
|
MB_RAM_HELPER __attribute__((noreturn)) static 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. */
|
|
MB_RAM_HELPER static 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;
|
|
}
|
|
|
|
/* Blank the whole LCD RAM (all 8 pages) right before the reset. The MCU reset
|
|
* leaves the display controller powered and still showing the "Restore slot N"
|
|
* screen; it stays visible through the next boot until ST7565_Init re-inits the
|
|
* panel. Wiping it here means the reboot window shows nothing instead of a
|
|
* stale restore screen. Best-effort: a display timeout just leaves it as-is. */
|
|
__attribute__((always_inline)) static inline void mb_ram_lcd_clear(void)
|
|
{
|
|
for (uint8_t page = 0; page < 8u; page++)
|
|
{
|
|
GPIOB->BRR = MB_LCD_CS_PIN;
|
|
GPIOA->BRR = MB_LCD_A0_PIN; /* command */
|
|
if (!mb_ram_lcd_spi((uint8_t)(0xB0u | page)) || /* set page 0..7 */
|
|
!mb_ram_lcd_spi(0x10u) || /* column high nibble */
|
|
!mb_ram_lcd_spi(0x04u)) /* visible RAM starts at column 4 */
|
|
{
|
|
GPIOB->BSRR = MB_LCD_CS_PIN;
|
|
return;
|
|
}
|
|
GPIOA->BSRR = MB_LCD_A0_PIN; /* data */
|
|
for (uint32_t i = 0; i < 128u; i++)
|
|
if (!mb_ram_lcd_spi(0x00u))
|
|
{
|
|
GPIOB->BSRR = MB_LCD_CS_PIN;
|
|
return;
|
|
}
|
|
GPIOB->BSRR = MB_LCD_CS_PIN;
|
|
}
|
|
}
|
|
__attribute__((section(MB_RAM_SECTION), 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;
|
|
if (lcdEnabled)
|
|
mb_ram_lcd_clear();
|
|
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. */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* Set when a polled SPI wait below times out (external flash unresponsive). */
|
|
static volatile int mb_spi_err;
|
|
|
|
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
|
|
/* Linker-provided load/execution bounds for the restore stub. Its execution
|
|
* address aliases the PY25Q16 sector cache; its load image remains in flash. */
|
|
extern uint8_t __mb_ramfunc_load_start;
|
|
extern uint8_t __mb_ramfunc_start;
|
|
extern uint8_t __mb_ramfunc_end;
|
|
|
|
static bool mb_load_ram_reflash_overlay(void)
|
|
{
|
|
const volatile uint8_t *src = &__mb_ramfunc_load_start;
|
|
volatile uint8_t *dst = &__mb_ramfunc_start;
|
|
volatile uint8_t *end = &__mb_ramfunc_end;
|
|
|
|
/* Volatile byte copies prevent a library memcpy call. The copy itself runs
|
|
* while internal flash is still fully available. */
|
|
while (dst < end)
|
|
*dst++ = *src++;
|
|
|
|
/* Cortex-M0+ has no instruction cache, but the barriers ensure that every
|
|
* store is visible before the following BLX starts fetching the stub. */
|
|
__DSB();
|
|
__ISB();
|
|
|
|
src = &__mb_ramfunc_load_start;
|
|
dst = &__mb_ramfunc_start;
|
|
while (dst < end)
|
|
if (*dst++ != *src++)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
/* 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_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;
|
|
|
|
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
|
|
/* No PY25Q16 driver access is allowed after this point: the sector cache is
|
|
* about to become executable storage and the RAM stub always ends in reset.
|
|
* Mask IRQs first, then explicitly stop SPI2 DMA before overwriting the
|
|
* cache. Keeping both operations local avoids relying on validation's
|
|
* current polled-SPI implementation and closes any IRQ re-arm window. */
|
|
const uint32_t primask = __get_PRIMASK();
|
|
__disable_irq();
|
|
mb_spi_polled_mode();
|
|
PY25Q16_InvalidateCache();
|
|
if (!mb_load_ram_reflash_overlay())
|
|
{
|
|
__set_PRIMASK(primask);
|
|
return MB_ERR_RAM_LOAD;
|
|
}
|
|
#endif
|
|
|
|
/* Valid and, for the overlay path, safely loaded: the RAM stub copies
|
|
* exactly image_size bytes, pads the partial page with 0xFF and erases the
|
|
* remainder of the application region. Keep flash locked until this point
|
|
* so an overlay-copy failure can return without changing flash state. */
|
|
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 */
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* 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)
|
|
{
|
|
/* Slot 0 is the firmware-managed base backup: never erasable from the host. */
|
|
if (slot >= MB_SLOT_COUNT || slot == MB_SLOT_BACKUP)
|
|
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)
|
|
{
|
|
/* Slot 0 is the firmware-managed base backup: never writable from the host. */
|
|
if (slot >= MB_SLOT_COUNT || slot == MB_SLOT_BACKUP)
|
|
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;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Config banks and the active-state marker. */
|
|
/* */
|
|
/* The banking offset itself lives in the flash driver (PY25Q16_SetBankBase); */
|
|
/* here we only own the active-state marker and the bank -> base mapping. */
|
|
/* All external-flash only, never brick-critical. */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
_Static_assert(sizeof(mb_state_t) == 24u,
|
|
"FMP2/FMP3 marker layout must stay 24 bytes");
|
|
_Static_assert(offsetof(mb_state_t, firmware_slot) == 16u,
|
|
"FMP2 migration reads the coupled index at byte 16");
|
|
_Static_assert(offsetof(mb_state_t, state_crc32) == 20u,
|
|
"state CRC must cover the first 20 bytes (magic..bank_inv)");
|
|
|
|
uint32_t MB_Crc32Bytes(const uint8_t *p, uint32_t len)
|
|
{
|
|
uint32_t crc = 0xFFFFFFFFu;
|
|
|
|
for (uint32_t i = 0; i < len; i++)
|
|
{
|
|
crc ^= p[i];
|
|
for (int k = 0; k < 8; k++)
|
|
crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
|
|
}
|
|
return crc ^ 0xFFFFFFFFu;
|
|
}
|
|
|
|
uint32_t MB_BankBase(uint8_t bank)
|
|
{
|
|
if (bank == 0)
|
|
return 0; /* bank 0 = historical config region */
|
|
if (bank < MB_BANK_COUNT)
|
|
return MB_BANK1_EXT_BASE + (uint32_t)(bank - 1u) * MB_BANK_SIZE;
|
|
return 0; /* out of range -> safe default */
|
|
}
|
|
|
|
static mb_mark_status_t mb_read_state_copy(uint32_t base, mb_state_t *st)
|
|
{
|
|
mb_spi_err = 0;
|
|
mb_ext_read(base, (uint8_t *)st, sizeof(*st));
|
|
if (mb_spi_err) return MB_MARK_IO;
|
|
if (st->magic == 0xFFFFFFFFu) return MB_MARK_MISSING;
|
|
if (st->magic == MB_STATE_LEGACY_MAGIC)
|
|
{
|
|
/* FMP1 was { magic, index, ~index, reserved[2] }, with index coupling the
|
|
* firmware slot and the config bank. Keep it as BOTH fields so boot
|
|
* resolution can honour that choice before migrating the record to FMP3. */
|
|
const uint8_t legacy_index = ((const uint8_t *)st)[4];
|
|
const uint8_t legacy_inv = ((const uint8_t *)st)[5];
|
|
if ((uint8_t)~legacy_inv != legacy_index || legacy_index >= MB_BANK_COUNT)
|
|
return MB_MARK_CORRUPT;
|
|
memset(st, 0, sizeof(*st));
|
|
st->magic = MB_STATE_LEGACY_MAGIC;
|
|
st->firmware_slot = legacy_index;
|
|
st->slot_inv = legacy_inv;
|
|
st->config_bank = legacy_index;
|
|
st->bank_inv = legacy_inv;
|
|
return MB_MARK_LEGACY;
|
|
}
|
|
|
|
if (st->magic == MB_STATE_V2_MAGIC)
|
|
{
|
|
/* FMP2 stored one index for both the firmware slot and config bank at
|
|
* byte 16, followed by its inverse and two zeroed reserved bytes.
|
|
* Validate the on-flash record before normalizing it in RAM to FMP3. */
|
|
const uint8_t legacy_index = ((const uint8_t *)st)[16];
|
|
const uint8_t legacy_inv = ((const uint8_t *)st)[17];
|
|
if ((uint8_t)~legacy_inv != legacy_index ||
|
|
legacy_index >= MB_BANK_COUNT)
|
|
return MB_MARK_CORRUPT;
|
|
if (st->image_size == 0u || st->image_size > MB_INT_APP_SIZE)
|
|
return MB_MARK_CORRUPT;
|
|
if (MB_Crc32Bytes((const uint8_t *)st,
|
|
sizeof(*st) - sizeof(st->state_crc32)) != st->state_crc32)
|
|
return MB_MARK_CORRUPT;
|
|
|
|
st->magic = MB_STATE_MAGIC;
|
|
st->firmware_slot = legacy_index;
|
|
st->slot_inv = (uint8_t)~legacy_index;
|
|
st->config_bank = legacy_index;
|
|
st->bank_inv = (uint8_t)~legacy_index;
|
|
st->state_crc32 = MB_Crc32Bytes((const uint8_t *)st,
|
|
sizeof(*st) - sizeof(st->state_crc32));
|
|
return MB_MARK_VALID;
|
|
}
|
|
|
|
if (st->magic != MB_STATE_MAGIC) return MB_MARK_CORRUPT;
|
|
if ((uint8_t)~st->slot_inv != st->firmware_slot)
|
|
return MB_MARK_CORRUPT;
|
|
if (st->firmware_slot >= MB_SLOT_COUNT) return MB_MARK_CORRUPT;
|
|
if ((uint8_t)~st->bank_inv != st->config_bank)
|
|
return MB_MARK_CORRUPT;
|
|
if (st->config_bank >= MB_BANK_COUNT) return MB_MARK_CORRUPT;
|
|
if (st->image_size == 0u ||
|
|
st->image_size > MB_INT_APP_SIZE) return MB_MARK_CORRUPT;
|
|
if (MB_Crc32Bytes((const uint8_t *)st,
|
|
sizeof(*st) - sizeof(st->state_crc32)) != st->state_crc32)
|
|
return MB_MARK_CORRUPT;
|
|
return MB_MARK_VALID;
|
|
}
|
|
|
|
static bool mb_generation_newer(uint32_t a, uint32_t b)
|
|
{
|
|
return (int32_t)(a - b) > 0;
|
|
}
|
|
|
|
static mb_mark_status_t mb_read_active_state(mb_state_t *state,
|
|
uint32_t *state_base)
|
|
{
|
|
mb_state_t a;
|
|
mb_state_t b;
|
|
mb_mark_status_t sa = mb_read_state_copy(MB_STATE_A_BASE, &a);
|
|
mb_mark_status_t sb = mb_read_state_copy(MB_STATE_B_BASE, &b);
|
|
|
|
/* If either sector could not be read, it might contain the newest record.
|
|
* Do not silently select an older state and risk loading the wrong bank. */
|
|
if (sa == MB_MARK_IO || sb == MB_MARK_IO)
|
|
return MB_MARK_IO;
|
|
|
|
const mb_state_t *chosen = NULL;
|
|
uint32_t chosen_base = 0;
|
|
if (sa == MB_MARK_VALID && sb == MB_MARK_VALID)
|
|
{
|
|
if (mb_generation_newer(b.generation, a.generation))
|
|
{
|
|
chosen = &b;
|
|
chosen_base = MB_STATE_B_BASE;
|
|
}
|
|
else
|
|
{
|
|
chosen = &a;
|
|
chosen_base = MB_STATE_A_BASE;
|
|
}
|
|
}
|
|
else if (sa == MB_MARK_VALID)
|
|
{
|
|
chosen = &a;
|
|
chosen_base = MB_STATE_A_BASE;
|
|
}
|
|
else if (sb == MB_MARK_VALID)
|
|
{
|
|
chosen = &b;
|
|
chosen_base = MB_STATE_B_BASE;
|
|
}
|
|
|
|
if (chosen)
|
|
{
|
|
if (state)
|
|
*state = *chosen;
|
|
if (state_base)
|
|
*state_base = chosen_base;
|
|
return MB_MARK_VALID;
|
|
}
|
|
|
|
/* A legacy record is usable for migration but has no firmware identity.
|
|
* Prefer A if both somehow exist; the next successful repair writes FMP3. */
|
|
if (sa == MB_MARK_LEGACY || sb == MB_MARK_LEGACY)
|
|
{
|
|
const bool use_b = sa != MB_MARK_LEGACY;
|
|
if (state)
|
|
*state = use_b ? b : a;
|
|
if (state_base)
|
|
*state_base = use_b ? MB_STATE_B_BASE : MB_STATE_A_BASE;
|
|
return MB_MARK_LEGACY;
|
|
}
|
|
|
|
return (sa == MB_MARK_MISSING && sb == MB_MARK_MISSING)
|
|
? MB_MARK_MISSING : MB_MARK_CORRUPT;
|
|
}
|
|
|
|
mb_mark_status_t MB_ReadActiveState(mb_state_t *state)
|
|
{
|
|
return mb_read_active_state(state, NULL);
|
|
}
|
|
|
|
/* Commit a marker whose identity, firmware_slot and config_bank are set:
|
|
* fill the volatile fields (magic, generation, inverse bytes, state CRC), pick
|
|
* the inactive redundant sector, program it and read it back to confirm. The
|
|
* previous valid record is left untouched until the new one verifies, so this is
|
|
* power-loss safe. `current`/`current_base` come from mb_read_active_state. */
|
|
static uint8_t mb_commit_state(mb_state_t *st,
|
|
mb_mark_status_t current_status,
|
|
const mb_state_t *current,
|
|
uint32_t current_base)
|
|
{
|
|
st->magic = MB_STATE_MAGIC;
|
|
st->generation = (current_status == MB_MARK_VALID) ? current->generation + 1u : 0u;
|
|
st->slot_inv = (uint8_t)~st->firmware_slot;
|
|
st->bank_inv = (uint8_t)~st->config_bank;
|
|
st->state_crc32 = MB_Crc32Bytes((const uint8_t *)st,
|
|
sizeof(*st) - sizeof(st->state_crc32));
|
|
|
|
const uint32_t target_base = ((current_status == MB_MARK_VALID ||
|
|
current_status == MB_MARK_LEGACY) &&
|
|
current_base == MB_STATE_A_BASE)
|
|
? MB_STATE_B_BASE
|
|
: MB_STATE_A_BASE;
|
|
|
|
mb_spi_err = 0;
|
|
mb_spi_polled_mode();
|
|
if (!mb_ext_sector_erase(target_base))
|
|
return MB_ERR_SPI;
|
|
if (!mb_ext_program(target_base, (const uint8_t *)st, sizeof(*st)))
|
|
return MB_ERR_SPI;
|
|
|
|
/* Verify the new copy directly. The previous valid sector has not been
|
|
* touched, so any failure here remains power-loss safe. */
|
|
mb_state_t chk;
|
|
mb_mark_status_t chk_status = mb_read_state_copy(target_base, &chk);
|
|
if (chk_status == MB_MARK_IO) return MB_ERR_SPI;
|
|
if (chk_status != MB_MARK_VALID ||
|
|
memcmp(&chk, st, sizeof(*st)) != 0) return MB_ERR_CRC;
|
|
|
|
return MB_OK;
|
|
}
|
|
|
|
uint8_t MB_SetActiveSlot(uint8_t slot)
|
|
{
|
|
mb_slot_header_t hdr;
|
|
mb_state_t st;
|
|
mb_state_t current;
|
|
uint32_t current_base = 0;
|
|
|
|
if (slot >= MB_SLOT_COUNT)
|
|
return MB_ERR_SLOT;
|
|
|
|
uint8_t hdr_err = mb_read_header(slot, &hdr);
|
|
if (hdr_err != MB_OK)
|
|
return hdr_err;
|
|
|
|
mb_mark_status_t current_status = mb_read_active_state(¤t, ¤t_base);
|
|
if (current_status == MB_MARK_IO)
|
|
return MB_ERR_SPI;
|
|
|
|
memset(&st, 0, sizeof(st));
|
|
st.image_size = hdr.image_size; /* expect this slot's image (reflash) */
|
|
st.image_crc32 = hdr.image_crc32;
|
|
st.firmware_slot = slot;
|
|
st.config_bank = slot;
|
|
return mb_commit_state(&st, current_status, ¤t, current_base);
|
|
}
|
|
|
|
uint8_t MB_SetActiveBank(uint8_t bank)
|
|
{
|
|
mb_state_t st;
|
|
mb_state_t current;
|
|
uint32_t current_base = 0;
|
|
|
|
if (bank >= MB_BANK_COUNT)
|
|
return MB_ERR_SLOT;
|
|
|
|
/* Keep the running firmware's identity from the current marker so switching a
|
|
* bank never looks like an out-of-multiboot firmware change at the next boot.
|
|
* A valid marker is required for that identity - every normal boot leaves one. */
|
|
mb_mark_status_t current_status = mb_read_active_state(¤t, ¤t_base);
|
|
if (current_status == MB_MARK_IO)
|
|
return MB_ERR_SPI;
|
|
if (current_status != MB_MARK_VALID)
|
|
return MB_ERR_MAGIC;
|
|
|
|
memset(&st, 0, sizeof(st));
|
|
st.image_size = current.image_size; /* keep the running firmware's identity */
|
|
st.image_crc32 = current.image_crc32;
|
|
st.firmware_slot = current.firmware_slot;
|
|
st.config_bank = bank;
|
|
return mb_commit_state(&st, current_status, ¤t, current_base);
|
|
}
|
|
|
|
uint8_t MB_BankErase(uint8_t bank)
|
|
{
|
|
/* Bank 0 (the base config) is not resettable from the host: reset it by
|
|
* factory-resetting the running base firmware instead. Banks 1..N live
|
|
* in their own 64 KiB banks, clear of the shared calibration at 0x010000. */
|
|
if (bank == 0 || bank >= MB_BANK_COUNT)
|
|
return MB_ERR_SLOT;
|
|
|
|
const uint32_t base = MB_BankBase(bank);
|
|
|
|
/* Invalidate before the first raw erase so an early failure cannot leave a
|
|
* cache entry referring to a sector that was already erased. */
|
|
PY25Q16_InvalidateCache();
|
|
|
|
mb_spi_err = 0;
|
|
mb_spi_polled_mode();
|
|
for (uint32_t off = 0; off < MB_BANK_SIZE; off += MB_EXT_SECTOR)
|
|
if (!mb_ext_sector_erase(base + off))
|
|
return MB_ERR_SPI;
|
|
|
|
return MB_OK;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Slot 0 self-backup (base firmware). */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* Bounded copy of a NUL-terminated string into a fixed field, zero-padded. */
|
|
static void mb_copy_str(char *dst, uint8_t cap, const char *src)
|
|
{
|
|
uint8_t n = 0;
|
|
while (n + 1u < cap && src[n])
|
|
{
|
|
dst[n] = src[n];
|
|
n++;
|
|
}
|
|
while (n < cap)
|
|
dst[n++] = 0;
|
|
}
|
|
|
|
/* CRC-32 (zlib) of the internal application flash, which is memory-mapped so
|
|
* no SPI is involved. Same polynomial as the external slot CRC. */
|
|
static uint32_t mb_int_image_crc32(uint32_t len)
|
|
{
|
|
const uint8_t *p = (const uint8_t *)MB_INT_APP_BASE;
|
|
return MB_Crc32Bytes(p, len);
|
|
}
|
|
|
|
static bool mb_internal_matches(uint32_t image_size, uint32_t image_crc32)
|
|
{
|
|
return mb_int_image_crc32(image_size) == image_crc32;
|
|
}
|
|
|
|
mb_fw_match_t MB_InternalMatchesSlot(uint8_t slot)
|
|
{
|
|
mb_slot_header_t hdr;
|
|
uint8_t err = mb_read_header(slot, &hdr);
|
|
|
|
if (err == MB_ERR_SPI)
|
|
return MB_FW_IO; /* couldn't read the header: never conclude mismatch */
|
|
if (err != MB_OK)
|
|
return MB_FW_MISMATCH; /* no valid header: definitely not this firmware */
|
|
|
|
return mb_internal_matches(hdr.image_size, hdr.image_crc32)
|
|
? MB_FW_MATCH : MB_FW_MISMATCH;
|
|
}
|
|
|
|
bool MB_InternalMatchesState(const mb_state_t *state)
|
|
{
|
|
if (!state || state->image_size == 0u || state->image_size > MB_INT_APP_SIZE)
|
|
return false;
|
|
return mb_internal_matches(state->image_size, state->image_crc32);
|
|
}
|
|
|
|
uint8_t MB_BackupInternalToSlot0(mb_progress_fn progress)
|
|
{
|
|
const uint8_t *img = (const uint8_t *)MB_INT_APP_BASE;
|
|
const uint32_t size = MB_INT_APP_SIZE;
|
|
const uint32_t base = MB_SLOT0_EXT_BASE + (uint32_t)MB_SLOT_BACKUP * MB_SLOT_STRIDE;
|
|
|
|
mb_spi_err = 0;
|
|
mb_spi_polled_mode();
|
|
|
|
/* Erase the header sector + image area (rounded up to 4 KiB sectors). */
|
|
for (uint32_t off = 0; off < MB_SLOT_IMG_OFFSET + size; off += MB_EXT_SECTOR)
|
|
if (!mb_ext_sector_erase(base + off))
|
|
return MB_ERR_SPI;
|
|
|
|
/* Program the whole internal application region verbatim, in chunks,
|
|
* reporting progress. It is an exact copy of what executes, so restoring
|
|
* it later reproduces the running firmware bit-for-bit. */
|
|
for (uint32_t done = 0; done < size; )
|
|
{
|
|
uint32_t n = (size - done < MB_EXT_SECTOR) ? (size - done) : MB_EXT_SECTOR;
|
|
if (!mb_ext_program(base + MB_SLOT_IMG_OFFSET + done, img + done, n))
|
|
return MB_ERR_SPI;
|
|
done += n;
|
|
if (progress)
|
|
progress(done, size);
|
|
}
|
|
|
|
/* Validate the stored image before committing its header. Until that final
|
|
* header write the slot remains invalid, so a failed CRC or interrupted
|
|
* backup is guaranteed to retry at the next boot. */
|
|
const uint32_t image_crc = mb_int_image_crc32(size);
|
|
uint32_t ext_crc = mb_ext_image_crc32(base + MB_SLOT_IMG_OFFSET, size);
|
|
if (mb_spi_err) return MB_ERR_SPI;
|
|
if (ext_crc != image_crc) return MB_ERR_CRC;
|
|
|
|
/* Write the COMMITTED header only after the external image verified. */
|
|
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 = image_crc;
|
|
mb_copy_str(hdr.name, MB_NAME_LEN, EDITION_STRING);
|
|
mb_copy_str(hdr.fw_version, MB_VERSION_LEN, VERSION_STRING_2);
|
|
|
|
if (!mb_ext_program(base, (const uint8_t *)&hdr, sizeof(hdr)))
|
|
return MB_ERR_SPI;
|
|
|
|
return MB_OK;
|
|
}
|