From 32b690b6f91809c5dc417a81f1d4cc539c4599b5 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 29 Sep 2026 14:47:26 -0700 Subject: [PATCH 01/23] PolarFire SoC: M-mode scratchpad, DDR training, SBI and CI fixes --- .github/workflows/test-configs.yml | 7 + Makefile | 2 + arch.mk | 13 + config/examples/polarfire_mpfs250_m.config | 23 +- docs/Targets.md | 18 +- hal/mpfs250-m.ld | 30 +- hal/mpfs250.c | 10 +- hal/mpfs250.h | 35 +- hal/mpfs250_ddr.c | 431 +++++++++++---------- options.mk | 8 + src/boot_riscv.c | 18 +- src/boot_riscv_start.S | 59 ++- src/riscv_sbi.c | 214 +++++++--- 13 files changed, 563 insertions(+), 305 deletions(-) diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 29f668613f..29a9964b17 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -539,6 +539,13 @@ jobs: config-file: ./config/examples/polarfire_mpfs250_m.config pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub + microchip_mpfs250_m_mldsa_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SIGN=ML_DSA ML_DSA_LEVEL=5 IMAGE_SIGNATURE_SIZE=4627 IMAGE_HEADER_SIZE=12288 WOLFBOOT_SECTOR_SIZE=0x4000 microchip_mpfs250_m_qspi_test: uses: ./.github/workflows/test-build-riscv.yml with: diff --git a/Makefile b/Makefile index 5ebacc53f0..16c9212eeb 100644 --- a/Makefile +++ b/Makefile @@ -797,6 +797,8 @@ $(LSCRIPT): $(LSCRIPT_IN) FORCE sed -e "s/@WOLFBOOT_L2LIM_SIZE@/$(WOLFBOOT_L2LIM_SIZE)/g" | \ sed -e "s/@L2SRAM_ADDR@/$(L2SRAM_ADDR)/g" | \ sed -e "s/@STACK_SIZE_PER_HART@/$(STACK_SIZE_PER_HART)/g" | \ + sed -e "s/@WOLFBOOT_L2SCRATCH_SIZE@/$(WOLFBOOT_L2SCRATCH_SIZE)/g" | \ + sed -e "s/@STACK_SIZE@/$(STACK_SIZE)/g" | \ sed -e 's/@WOLFHAL_FLASH_EXCLUDE_TEXT@/$(WOLFHAL_FLASH_EXCLUDE_TEXT)/g' | \ sed -e 's/@WOLFHAL_FLASH_EXCLUDE_RODATA@/$(WOLFHAL_FLASH_EXCLUDE_RODATA)/g' | \ sed -e 's/@WOLFHAL_FLASH_RAM_SECTIONS@/$(WOLFHAL_FLASH_RAM_SECTIONS)/g' \ diff --git a/arch.mk b/arch.mk index 1733788ebf..a4613b8fe7 100644 --- a/arch.mk +++ b/arch.mk @@ -1340,6 +1340,19 @@ ifeq ($(ARCH),RISCV64) # MPFS DDR init pulls LIBERO_SETTING_* values from a Libero/HSS-generated # fpga_design_config.h. Setting LIBERO_FPGA_CONFIG_DIR enables DDR init # and adds the directory to the include search path. + # + # Booting an S-mode OS means running from DDR, so an empty + # LIBERO_FPGA_CONFIG_DIR here would silently produce a bootloader with no + # DDR controller init at all. Fail the build instead. + ifeq ($(LIBERO_FPGA_CONFIG_DIR),) + ifneq (,$(findstring WOLFBOOT_MMODE_SMODE_BOOT,$(CFLAGS_EXTRA) $(CFLAGS))) + ifeq ($(filter clean keysclean distclean,$(MAKECMDGOALS)),) + $(error WOLFBOOT_MMODE_SMODE_BOOT requires LIBERO_FPGA_CONFIG_DIR: \ + point it at the board's fpga_design_config directory, e.g. \ + /build/boards/mpfs-video-kit/fpga_design_config) + endif + endif + endif ifneq ($(LIBERO_FPGA_CONFIG_DIR),) CFLAGS+=-DMPFS_DDR_INIT -I$(LIBERO_FPGA_CONFIG_DIR) # Generic Cadence DDR controller driver + the MPFS PHY/PLL/training diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index bc22c5f677..79658269c5 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -77,8 +77,14 @@ CFLAGS_EXTRA+=-DWOLFBOOT_MMODE_SMODE_BOOT DISK_SDCARD?=1 DISK_EMMC?=0 -# wolfBoot in L2 SRAM (256KB available) +# wolfBoot in L2 SRAM: all 4 scratchpad ways (0x0A000000 - 0x0A07FFFF) WOLFBOOT_ORIGIN?=0x0A000000 +WOLFBOOT_L2SCRATCH_SIZE?=512k + +# Boot-hart stack. Sized for ML-DSA-87, whose verify working set lands on +# top of the two IMAGE_HEADER_SIZE buffers src/update_disk.c puts on the +# stack; ECC384 needs far less. +STACK_SIZE?=64k # 4KB sector size (SD card flow is partition-based, not flash-erase-based) WOLFBOOT_SECTOR_SIZE?=0x1000 @@ -131,17 +137,10 @@ CFLAGS_EXTRA+=-DSDHCI_SDMA_DISABLED # race on Arasan/Cadence-family controllers; single-block avoids it. CFLAGS_EXTRA+=-DSDHCI_FORCE_SINGLE_BLOCK_READ -# Disk-load via PDMA staging. On this board, CPU AXI writes to DDR -# (cached or non-cached) do NOT reliably land at the address that -# subsequent cached reads will fetch from -- empirical alias probe -# showed CPU writes via the 0xC0000000 non-cached window are silently -# dropped, and cached PIO writes appear to allocate L2 lines that are -# never written back to DDR before the integrity-check read. -# -# Workaround: SDHCI PIO into a small L2 Scratch staging buffer, then -# mpfs_pdma_memcpy() copies the block into DDR via the PDMA master. -# PDMA-via-non-cached is the only AXI write path verified to land in -# DDR (the same path used by mpfs_clear_bootup_cache_ways pre-fill). +# Disk-load via PDMA staging: SDHCI PIO into an L2 Scratch staging buffer, +# then mpfs_pdma_memcpy() lands each block in DDR through the PDMA master. +# A CPU copy into DDR reads back wrong depending on access order and code +# layout (cause not identified); the PDMA path has never failed. CFLAGS_EXTRA+=-DSDHCI_BLOCK_VIA_PDMA # Video Kit routes the SD slot's Card Detect (CD#) signal through the FPGA diff --git a/docs/Targets.md b/docs/Targets.md index 13275e6bb0..6e7fe1f05c 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -1608,7 +1608,7 @@ See the [Encrypted Partitions](encrypted_partitions.md) documentation for additi #### Configuration -Update your `.config` file with the following ML-DSA settings: +Update your `.config` file with the following ML-DSA settings, or pass them as `make` arguments on top of `config/examples/polarfire_mpfs250_m.config` (the standalone M-mode E51 target verifies ML-DSA-87 this way, with SHA-384 as the image hash): ```makefile # ML-DSA 87 (Category 5) @@ -1663,6 +1663,20 @@ Boot time measurements on PolarFire SoC (RISC-V 64-bit U54 @ 625 MHz) for a 19MB | ECC384 | SHA384 | ~800 ms | ~2900 ms | ~1500 ms | ~70 ms | ~5.3 seconds | | ML-DSA 87 | SHA256 | ~835 ms | ~2900 ms | ~2100 ms | ~22 ms | ~5.9 seconds | +Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit for the same 19.7 MB FIT, plaintext, ECC384/SHA384, with the DDR training and SD-card load included: + +| Phase | Software crypto | Athena offload | +|-----------------------------------------------|-----------------|----------------| +| DDR training, SD init, GPT read | ~1.5 s | ~1.5 s | +| SD load of the FIT (CMD17 single block + PDMA staging) | ~15.5 s | ~15.5 s | +| SHA384 integrity (reads through the non-cached alias) | ~4.1 s | ~4.8 s | +| ECC384 signature verify | ~0.7 s | ~0.7 s | +| FIT kernel copy to its load address (PDMA + read-back verify) | ~9.1 s | ~9.1 s | +| M-mode -> S-mode handoff | ~31 s | ~32 s | +| Linux login prompt | ~53-62 s | ~53 s | + +The offload does not shorten the integrity check on this path because the time is in reading the image through the non-cached DDR alias, not in the hashing; measured on a buffer in L2 scratch the Athena SHA384 is about 1.3x the software rate and AES-256-CTR about 4.4x. The SD read and the PDMA copy with its byte-wise verify are the dominant costs; both exist because of the CPU-write-to-DDR coherence workaround (`SDHCI_BLOCK_VIA_PDMA`). ML-DSA-87 on this target (`SIGN=ML_DSA` on `polarfire_mpfs250_m.config`) links to a smaller image than ECC384 because no big-number code is needed. + ### PolarFire Soc Debugging Start GDB server: @@ -1695,7 +1709,7 @@ wolfBoot Version: 2.8.0 Running on E51 (hart 0) in M-mode Boot RESET_SR: 1FF (bit0=PERIPH bit1=MSS bit2=CPU bit3=DBG bit4=FABRIC bit5=WDOG bit6=GPIO bit7=BUS bit8=SOFT) ======================================== -DDR: Training+MTC PASS after 0 retries +DDR: Training+MTC PASS DDR: Initialization COMPLETE SDHCI: platform init Reading MBR... diff --git a/hal/mpfs250-m.ld b/hal/mpfs250-m.ld index d84aace4e1..bf012e9124 100644 --- a/hal/mpfs250-m.ld +++ b/hal/mpfs250-m.ld @@ -26,19 +26,27 @@ MEMORY * This offset is added by mpfsBootmodeProgrammer (bootmode 1) */ FLASH_ENVM (rx) : ORIGIN = 0x20220100, LENGTH = 128k - 0x100 - /* L2 Scratchpad SRAM - 256 KB used (2 of 4 scratchpad ways). - * Attempted 512 KB (all 4 ways) to match HSS layout, but ways 8-9 - * are not initialized by the bootmode programmer -- stack/HLS - * placed there hit a trap immediately after DDR init. Reverted - * until we add explicit scratchpad init for ways 8-9. - * Address range: 0x0A000000 - 0x0A03FFFF */ - L2_SCRATCH (rwx) : ORIGIN = @WOLFBOOT_ORIGIN@, LENGTH = 256k + /* L2 Scratchpad SRAM. Size is bounded by the number of scratchpad ways + * the startup asm populates one way at a time (see the pin loop in + * src/boot_riscv_start.S); MPFS250 hardwires ways 8-11 as scratchpad, so + * 4 x WAY_BYTE_LENGTH = 512 KB is the ceiling. Set per target with + * WOLFBOOT_L2SCRATCH_SIZE; the ASSERT below enforces the ceiling. */ + L2_SCRATCH (rwx) : ORIGIN = @WOLFBOOT_ORIGIN@, LENGTH = @WOLFBOOT_L2SCRATCH_SIZE@ } -/* Stack size for the boot hart (E51 in M-mode) - * ECC384 + SHA384 + SPMATHALL + NO_ASM measured peak: ~6KB. - * 32KB provides 5x headroom. */ -PROVIDE(STACK_SIZE = 32k); +/* Stack size for the boot hart (E51 in M-mode). Set per target with + * STACK_SIZE: ECC384 + SHA384 needs only a few KB, but ML-DSA-87 adds its + * own working set plus two IMAGE_HEADER_SIZE buffers that src/update_disk.c + * places on the stack. */ +PROVIDE(STACK_SIZE = @STACK_SIZE@); + +/* 4 scratchpad ways x 128 KB (WAY_BYTE_LENGTH). Going past this hands out + * addresses the startup pin loop never made resident. */ +ASSERT(LENGTH(L2_SCRATCH) <= 4 * 0x20000, + "L2_SCRATCH exceeds the 4 scratchpad ways (512 KB)") +ASSERT(ORIGIN(L2_SCRATCH) >= 0x0A000000 && + ORIGIN(L2_SCRATCH) + LENGTH(L2_SCRATCH) <= 0x0A080000, + "L2_SCRATCH must lie inside the pinned scratchpad 0x0A000000..0x0A07FFFF") SECTIONS { diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 56a2c25db4..fbd6021018 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -399,7 +399,7 @@ void hal_init(void) #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) /* Clear the DTIM-resident cross-hart state (start mailboxes + SBI * shared block): DTIM content is undefined at power-on. */ - for (k = 0; k < (0x200U / sizeof(uint32_t)); k++) { + for (k = 0; k < (MPFS_DTIM_BOOT_CLEAR_SIZE / sizeof(uint32_t)); k++) { dtim[k] = 0; } __asm__ volatile("fence iorw, iorw" ::: "memory"); @@ -472,12 +472,8 @@ void hal_init(void) #ifdef MPFS_DDR_INIT /* Bring up LPDDR4 before any DDR-resident operations. * - * Outer retry loop: each call to mpfs_ddr_init() does a SYSREG DDRC - * soft-reset pulse, which clears the MTC engine state. If the - * inner retry inside mpfs_ddr_init() exhausts (typically because - * MTC wedged after the first failure), come back here for a full - * controller re-init. Empirical: per-attempt failure rate ~30%, so - * MPFS_DDR_MAX_OUTER_RETRY (6) outer attempts cover ~99.9% of boots. */ + * Each mpfs_ddr_init() call pulses the SYSREG DDRC soft reset; a rejected + * training only recovers through this full controller re-init. */ for (outer_retry = 0; outer_retry < MPFS_DDR_MAX_OUTER_RETRY; outer_retry++) { if (outer_retry > 0) { diff --git a/hal/mpfs250.h b/hal/mpfs250.h index a3de52b817..e203c13b09 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -56,7 +56,7 @@ #define MPFS_CPU_FREQ_RESET_MHZ 80U #endif -/* Full-DDRC-reinit attempts in hal_init() (per-attempt failure rate ~30%). */ +/* Full-DDRC-reinit attempts in hal_init(). */ #ifndef MPFS_DDR_MAX_OUTER_RETRY #define MPFS_DDR_MAX_OUTER_RETRY 6U #endif @@ -351,6 +351,19 @@ typedef struct { * No UL suffix: also used from assembly (boot_riscv_start.S). */ #define MPFS_DTIM_MAIN_STARTED_ADDR 0x010000F0 +/* DTIM bytes hal_init() zeroes at boot (SBI shared block + hart mailboxes). */ +#define MPFS_DTIM_BOOT_CLEAR_SIZE 0x200U +/* DTIM pair (count, ~count) just above the cleared block, so it survives an + * MSS reset: DDR training restarts by reset since the last accepted training. */ +#define MPFS_DTIM_DDR_RESET_CNT_ADDR (0x01000000UL + MPFS_DTIM_BOOT_CLEAR_SIZE) +#ifndef MPFS_DDR_EYE_RESET_MAX +#define MPFS_DDR_EYE_RESET_MAX 5U +#endif +/* Minimum per-lane DQ/DQS window, HSS DQ_DQS_NUM_TAPS. */ +#ifndef MPFS_DDR_EYE_MIN_TAPS +#define MPFS_DDR_EYE_MIN_TAPS 5U +#endif + /* Number of harts on MPFS */ #define MPFS_NUM_HARTS 5 #define MPFS_FIRST_HART 0 /* E51 is hart 0 */ @@ -509,7 +522,25 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #define PHY_TRAINING_START 0x810 #define PHY_TRAINING_STATUS 0x814 #define PHY_TRAINING_RESET 0x818 -#define PHY_TIP_CFG 0x828 +/* Per-lane TIP status, selected via PHY_LANE_SELECT. Offsets taken from + * CFG_DDR_SGMII_PHY_TypeDef in mss_ddr_sgmii_phy_defs.h. */ +#define PHY_GT_ERR_COMB 0x81C +#define PHY_GT_CLK_SEL 0x820 +#define PHY_GT_TXDLY 0x824 +#define PHY_GT_STEPS_180 0x828 +#define PHY_GT_STATE 0x82C +#define PHY_WL_DELAY_0 0x830 +#define PHY_DQ_DQS_ERR_DONE 0x834 +#define PHY_DQDQS_WINDOW 0x838 /* [7:0] left, [15:8] right edge */ +#define PHY_DQDQS_STATE 0x83C +#define PHY_DELTA0 0x840 +#define PHY_DELTA1 0x844 +#define PHY_DQDQS_STATUS1 0x84C +#define PHY_DQDQS_STATUS2 0x850 +#define PHY_ADDCMD_STATUS0 0x864 +#define PHY_ADDCMD_STATUS1 0x868 +#define PHY_ADDCMD_ANSWER 0x86C +#define PHY_IOC_REG5 0x218 /* SRO slew readback */ #define PHY_TIP_CFG_PARAMS 0x8D0 #define PHY_EXPERT_MODE_EN 0x878 #define PHY_EXPERT_DLYCNT_MOVE0 0x87C diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index 551c82f2b6..45d265a059 100644 --- a/hal/mpfs250_ddr.c +++ b/hal/mpfs250_ddr.c @@ -36,30 +36,13 @@ #ifdef MPFS_DDR_INIT -/* DQ/DQS init offset (HSS rpc_156). Default 6 (Libero Video Kit value), - * tunable 1..9 per HSS TUNE_RPC_156_DQDQS_INIT_VALUE. Bumped between outer - * retries when training verify reports dq_dqs_err_done != 8 or - * dqdqs_status2 == 0 (data eye closed). */ +/* DQ/DQS init offset (HSS rpc_156). 6 is the Libero Video Kit value; HSS + * allows 1..9 (TUNE_RPC_156_DQDQS_INIT_VALUE) but it only shifts the window. */ static uint32_t mpfs_phy_rpc156_val = 6U; #if defined(WOLFBOOT_RISCV_MMODE) && defined(MPFS_DDR_INIT) -/* DDR-init busy-loop delay. The argument is NOT a real microsecond -- - * it is whatever the legacy busy-loop produces at the current CPU - * clock. Empirically reaches train_stat=0x1D on the first attempt with - * the same per-attempt rate as forwarding to udelay(), and is much - * faster (~4 s vs ~50 s) for the TIP-wait timeout, which dominates - * retry-loop time when training fails. - * - * Do NOT replace with udelay(us) without re-timing every call site - * below: at 600 MHz the busy-loop delivers roughly us/20 of a real us, - * so udelay(us) makes every post-PLL delay ~20x longer. In addition - * to slowing retries, this can shift LPDDR4 / PHY timing windows -- - * earlier observed empirical data showed an isolated additional - * regression beyond the pre-existing ~30% per-attempt failure rate. - * - * The "5us" / "250us" / "2ms" comments at the call sites are LEGACY - * and do not reflect the actual delay; preserved for git blame, not - * as timing references. */ +/* DDR-init busy-loop delay. The argument is NOT a real microsecond: at 600 MHz + * it delivers roughly us/20. udelay() trains identically but is ~20x slower. */ static void ddr_delay(uint32_t us) { volatile uint32_t i; @@ -535,21 +518,16 @@ static void setup_segments(void) mb(); } -/* DDR Controller Configuration +/* DDR controller configuration, in HSS init_ddrc() order. * - * Phase 3.6 rewrite: full bulk import of MC_BASE2 register configuration - * matching HSS setup_ddrc() at mss_ddr.c:3940-4225. All values come from - * the Video Kit Libero header + * Values come from the Video Kit Libero header * hart-software-services/build/boards/mpfs-video-kit/fpga_design_config/ * ddr/hw_ddrc.h * - * The previous version configured only ~30 of these registers AND used - * several wrong register offsets (e.g. MC_CFG_CL was at 0x74 -- which is - * actually CFG_XP -- so the CL value never reached the CL register). - * That left the IP in an under/mis-configured state that prevented TIP - * from progressing past BCLK_SCLK during training. - * - * This function configures the full ~155 MC_BASE2 registers in HSS order. + * ddr_cadence_controller_setup() writes the table in array order, so row + * order is write order. That matters at CTRLR_SOFT_RESET_N below: the PHY + * training, MTC and AXI_IF blocks are programmed only after the controller + * is released from soft reset. */ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { 0x2400, LIBERO_SETTING_CFG_MANUAL_ADDRESS_MAP }, @@ -659,95 +637,13 @@ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { 0x3D70, LIBERO_SETTING_CFG_RRD_DLR }, { 0x3D74, LIBERO_SETTING_CFG_FAW_DLR }, { 0x3D98, LIBERO_SETTING_CFG_ADVANCE_ACTIVATE_READY }, - { 0x4C00, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P0 }, - { 0x4C04, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P1 }, - { 0x4C08, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P2 }, - { 0x4C0C, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P3 }, - { 0x4C10, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P4 }, - { 0x4C14, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P5 }, - { 0x4C18, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P6 }, - { 0x4C1C, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P7 }, - { 0x5000, LIBERO_SETTING_CFG_REORDER_EN }, - { 0x5004, LIBERO_SETTING_CFG_REORDER_QUEUE_EN }, - { 0x5008, LIBERO_SETTING_CFG_INTRAPORT_REORDER_EN }, - { 0x500C, LIBERO_SETTING_CFG_MAINTAIN_COHERENCY }, - { 0x5010, LIBERO_SETTING_CFG_Q_AGE_LIMIT }, - { 0x5018, LIBERO_SETTING_CFG_RO_CLOSED_PAGE_POLICY }, - { 0x501C, LIBERO_SETTING_CFG_REORDER_RW_ONLY }, - { 0x5020, LIBERO_SETTING_CFG_RO_PRIORITY_EN }, - { 0x5400, LIBERO_SETTING_CFG_DM_EN }, - { 0x5404, LIBERO_SETTING_CFG_RMW_EN }, - { 0x5800, LIBERO_SETTING_CFG_ECC_CORRECTION_EN }, - { 0x5840, LIBERO_SETTING_CFG_ECC_BYPASS }, - { 0x5844, LIBERO_SETTING_INIT_WRITE_DATA_1B_ECC_ERROR_GEN }, - { 0x5848, LIBERO_SETTING_INIT_WRITE_DATA_2B_ECC_ERROR_GEN }, - { 0x585C, LIBERO_SETTING_CFG_ECC_1BIT_INT_THRESH }, - { 0x5C00, LIBERO_SETTING_INIT_READ_CAPTURE_ADDR }, - { 0x6400, LIBERO_SETTING_CFG_ERROR_GROUP_SEL }, - { 0x6404, LIBERO_SETTING_CFG_DATA_SEL }, - { 0x6408, LIBERO_SETTING_CFG_TRIG_MODE }, - { 0x640C, LIBERO_SETTING_CFG_POST_TRIG_CYCS }, - { 0x6410, LIBERO_SETTING_CFG_TRIG_MASK }, - { 0x6414, LIBERO_SETTING_CFG_EN_MASK }, - { 0x6418, LIBERO_SETTING_MTC_ACQ_ADDR }, - { 0x6430, LIBERO_SETTING_CFG_TRIG_MT_ADDR_0 }, - { 0x6434, LIBERO_SETTING_CFG_TRIG_MT_ADDR_1 }, - { 0x6438, LIBERO_SETTING_CFG_TRIG_ERR_MASK_0 }, - { 0x643C, LIBERO_SETTING_CFG_TRIG_ERR_MASK_1 }, - { 0x6440, LIBERO_SETTING_CFG_TRIG_ERR_MASK_2 }, - { 0x6444, LIBERO_SETTING_CFG_TRIG_ERR_MASK_3 }, - { 0x6448, LIBERO_SETTING_CFG_TRIG_ERR_MASK_4 }, - { 0x644C, LIBERO_SETTING_MTC_ACQ_WR_DATA_0 }, - { 0x6450, LIBERO_SETTING_MTC_ACQ_WR_DATA_1 }, - { 0x6454, LIBERO_SETTING_MTC_ACQ_WR_DATA_2 }, - { 0x652C, LIBERO_SETTING_CFG_PRE_TRIG_CYCS }, - { 0x6550, LIBERO_SETTING_CFG_DATA_SEL_FIRST_ERROR }, - { 0x7C00, LIBERO_SETTING_CFG_DQ_WIDTH }, - { 0x7C04, LIBERO_SETTING_CFG_ACTIVE_DQ_SEL }, - { 0x800C, LIBERO_SETTING_INIT_CA_PARITY_ERROR_GEN_REQ }, - { 0x8010, LIBERO_SETTING_INIT_CA_PARITY_ERROR_GEN_CMD }, - { 0x10010, LIBERO_SETTING_INIT_DFI_LP_DATA_REQ }, - { 0x10014, LIBERO_SETTING_INIT_DFI_LP_CTRL_REQ }, - { 0x1001C, LIBERO_SETTING_INIT_DFI_LP_WAKEUP }, - { 0x10020, LIBERO_SETTING_INIT_DFI_DRAM_CLK_DISABLE }, - { 0x10030, LIBERO_SETTING_CFG_DFI_DATA_BYTE_DISABLE }, - { 0x1003C, LIBERO_SETTING_CFG_DFI_LVL_SEL }, - { 0x10040, LIBERO_SETTING_CFG_DFI_LVL_PERIODIC }, - { 0x10044, LIBERO_SETTING_CFG_DFI_LVL_PATTERN }, - { 0x10050, LIBERO_SETTING_PHY_DFI_INIT_START }, - { 0x12C18, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI1_0 }, - { 0x12C1C, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI1_1 }, - { 0x12C20, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI2_0 }, - { 0x12C24, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI2_1 }, - { 0x12F18, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI1_0 }, - { 0x12F1C, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI1_1 }, - { 0x12F20, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI2_0 }, - { 0x12F24, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI2_1 }, - { 0x13218, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI1_0 }, - { 0x1321C, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI1_1 }, - { 0x13220, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI2_0 }, - { 0x13224, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI2_1 }, - { 0x13514, LIBERO_SETTING_CFG_ENABLE_BUS_HOLD_AXI1 }, - { 0x13518, LIBERO_SETTING_CFG_ENABLE_BUS_HOLD_AXI2 }, - { 0x13690, LIBERO_SETTING_CFG_AXI_AUTO_PCH }, - { 0x3C000, LIBERO_SETTING_PHY_RESET_CONTROL }, - { 0x3C000, (LIBERO_SETTING_PHY_RESET_CONTROL & ~0x8000UL) }, - { 0x3C004, LIBERO_SETTING_PHY_PC_RANK }, - { 0x3C008, LIBERO_SETTING_PHY_RANKS_TO_TRAIN }, - { 0x3C00C, LIBERO_SETTING_PHY_WRITE_REQUEST }, - { 0x3C014, LIBERO_SETTING_PHY_READ_REQUEST }, - { 0x3C01C, LIBERO_SETTING_PHY_WRITE_LEVEL_DELAY }, - { 0x3C020, LIBERO_SETTING_PHY_GATE_TRAIN_DELAY }, - { 0x3C024, LIBERO_SETTING_PHY_EYE_TRAIN_DELAY }, - { 0x3C028, LIBERO_SETTING_PHY_EYE_PAT }, - { 0x3C02C, LIBERO_SETTING_PHY_START_RECAL }, - { 0x3C030, LIBERO_SETTING_PHY_CLR_DFI_LVL_PERIODIC }, - { 0x3C034, LIBERO_SETTING_PHY_TRAIN_STEP_ENABLE }, - { 0x3C038, LIBERO_SETTING_PHY_LPDDR_DQ_CAL_PAT }, - { 0x3C03C, LIBERO_SETTING_PHY_INDPNDT_TRAINING }, - { 0x3C040, LIBERO_SETTING_PHY_ENCODED_QUAD_CS }, - { 0x3C044, LIBERO_SETTING_PHY_HALF_CLK_DLY_ENABLE }, + + /* Everything above is programmed with the controller still in soft + * reset. This write releases it; HSS init_ddrc() programs the PHY + * training, MTC and AXI_IF blocks only after this point, so the order + * of the rows below is a sequencing requirement, not a preference. */ { MC_CTRLR_SOFT_RESET_N, LIBERO_SETTING_CTRLR_SOFT_RESET_N }, + { MC_CFG_LOOKAHEAD_PCH, LIBERO_SETTING_CFG_LOOKAHEAD_PCH }, { MC_CFG_LOOKAHEAD_ACT, LIBERO_SETTING_CFG_LOOKAHEAD_ACT }, { MC_INIT_AUTOINIT_DISABLE, LIBERO_SETTING_INIT_AUTOINIT_DISABLE }, @@ -917,10 +813,98 @@ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { MC_CFG_BURST_RW_REFRESH_HOLDOFF, LIBERO_SETTING_CFG_BURST_RW_REFRESH_HOLDOFF }, { MC_CFG_BG_INTERLEAVE, LIBERO_SETTING_CFG_BG_INTERLEAVE }, { MC_CFG_REFRESH_DURING_PHY_TRAINING, LIBERO_SETTING_CFG_REFRESH_DURING_PHY_TRAINING }, + { 0x4C00, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P0 }, + { 0x4C04, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P1 }, + { 0x4C08, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P2 }, + { 0x4C0C, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P3 }, + { 0x4C10, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P4 }, + { 0x4C14, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P5 }, + { 0x4C18, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P6 }, + { 0x4C1C, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P7 }, + { 0x5000, LIBERO_SETTING_CFG_REORDER_EN }, + { 0x5004, LIBERO_SETTING_CFG_REORDER_QUEUE_EN }, + { 0x5008, LIBERO_SETTING_CFG_INTRAPORT_REORDER_EN }, + { 0x500C, LIBERO_SETTING_CFG_MAINTAIN_COHERENCY }, + { 0x5010, LIBERO_SETTING_CFG_Q_AGE_LIMIT }, + { 0x5018, LIBERO_SETTING_CFG_RO_CLOSED_PAGE_POLICY }, + { 0x501C, LIBERO_SETTING_CFG_REORDER_RW_ONLY }, + { 0x5020, LIBERO_SETTING_CFG_RO_PRIORITY_EN }, + { 0x5400, LIBERO_SETTING_CFG_DM_EN }, + { 0x5404, LIBERO_SETTING_CFG_RMW_EN }, + { 0x5800, LIBERO_SETTING_CFG_ECC_CORRECTION_EN }, + { 0x5840, LIBERO_SETTING_CFG_ECC_BYPASS }, + { 0x5844, LIBERO_SETTING_INIT_WRITE_DATA_1B_ECC_ERROR_GEN }, + { 0x5848, LIBERO_SETTING_INIT_WRITE_DATA_2B_ECC_ERROR_GEN }, + { 0x585C, LIBERO_SETTING_CFG_ECC_1BIT_INT_THRESH }, + { 0x5C00, LIBERO_SETTING_INIT_READ_CAPTURE_ADDR }, + { 0x6400, LIBERO_SETTING_CFG_ERROR_GROUP_SEL }, + { 0x6404, LIBERO_SETTING_CFG_DATA_SEL }, + { 0x6408, LIBERO_SETTING_CFG_TRIG_MODE }, + { 0x640C, LIBERO_SETTING_CFG_POST_TRIG_CYCS }, + { 0x6410, LIBERO_SETTING_CFG_TRIG_MASK }, + { 0x6414, LIBERO_SETTING_CFG_EN_MASK }, + { 0x6418, LIBERO_SETTING_MTC_ACQ_ADDR }, + { 0x6430, LIBERO_SETTING_CFG_TRIG_MT_ADDR_0 }, + { 0x6434, LIBERO_SETTING_CFG_TRIG_MT_ADDR_1 }, + { 0x6438, LIBERO_SETTING_CFG_TRIG_ERR_MASK_0 }, + { 0x643C, LIBERO_SETTING_CFG_TRIG_ERR_MASK_1 }, + { 0x6440, LIBERO_SETTING_CFG_TRIG_ERR_MASK_2 }, + { 0x6444, LIBERO_SETTING_CFG_TRIG_ERR_MASK_3 }, + { 0x6448, LIBERO_SETTING_CFG_TRIG_ERR_MASK_4 }, + { 0x644C, LIBERO_SETTING_MTC_ACQ_WR_DATA_0 }, + { 0x6450, LIBERO_SETTING_MTC_ACQ_WR_DATA_1 }, + { 0x6454, LIBERO_SETTING_MTC_ACQ_WR_DATA_2 }, + { 0x652C, LIBERO_SETTING_CFG_PRE_TRIG_CYCS }, + { 0x6550, LIBERO_SETTING_CFG_DATA_SEL_FIRST_ERROR }, + { 0x7C00, LIBERO_SETTING_CFG_DQ_WIDTH }, + { 0x7C04, LIBERO_SETTING_CFG_ACTIVE_DQ_SEL }, + { 0x800C, LIBERO_SETTING_INIT_CA_PARITY_ERROR_GEN_REQ }, + { 0x8010, LIBERO_SETTING_INIT_CA_PARITY_ERROR_GEN_CMD }, { MC_DFI_RDDATA_EN, LIBERO_SETTING_CFG_DFI_T_RDDATA_EN }, { MC_DFI_PHY_RDLAT, LIBERO_SETTING_CFG_DFI_T_PHY_RDLAT }, { MC_DFI_PHY_WRLAT, LIBERO_SETTING_CFG_DFI_T_PHY_WRLAT }, { MC_DFI_PHYUPD_EN, LIBERO_SETTING_CFG_DFI_PHYUPD_EN }, + { 0x10010, LIBERO_SETTING_INIT_DFI_LP_DATA_REQ }, + { 0x10014, LIBERO_SETTING_INIT_DFI_LP_CTRL_REQ }, + { 0x1001C, LIBERO_SETTING_INIT_DFI_LP_WAKEUP }, + { 0x10020, LIBERO_SETTING_INIT_DFI_DRAM_CLK_DISABLE }, + { 0x10030, LIBERO_SETTING_CFG_DFI_DATA_BYTE_DISABLE }, + { 0x1003C, LIBERO_SETTING_CFG_DFI_LVL_SEL }, + { 0x10040, LIBERO_SETTING_CFG_DFI_LVL_PERIODIC }, + { 0x10044, LIBERO_SETTING_CFG_DFI_LVL_PATTERN }, + { 0x10050, LIBERO_SETTING_PHY_DFI_INIT_START }, + { 0x12C18, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI1_0 }, + { 0x12C1C, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI1_1 }, + { 0x12C20, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI2_0 }, + { 0x12C24, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI2_1 }, + { 0x12F18, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI1_0 }, + { 0x12F1C, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI1_1 }, + { 0x12F20, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI2_0 }, + { 0x12F24, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI2_1 }, + { 0x13218, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI1_0 }, + { 0x1321C, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI1_1 }, + { 0x13220, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI2_0 }, + { 0x13224, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI2_1 }, + { 0x13514, LIBERO_SETTING_CFG_ENABLE_BUS_HOLD_AXI1 }, + { 0x13518, LIBERO_SETTING_CFG_ENABLE_BUS_HOLD_AXI2 }, + { 0x13690, LIBERO_SETTING_CFG_AXI_AUTO_PCH }, + { 0x3C000, LIBERO_SETTING_PHY_RESET_CONTROL }, + { 0x3C000, (LIBERO_SETTING_PHY_RESET_CONTROL & ~0x8000UL) }, + { 0x3C004, LIBERO_SETTING_PHY_PC_RANK }, + { 0x3C008, LIBERO_SETTING_PHY_RANKS_TO_TRAIN }, + { 0x3C00C, LIBERO_SETTING_PHY_WRITE_REQUEST }, + { 0x3C014, LIBERO_SETTING_PHY_READ_REQUEST }, + { 0x3C01C, LIBERO_SETTING_PHY_WRITE_LEVEL_DELAY }, + { 0x3C020, LIBERO_SETTING_PHY_GATE_TRAIN_DELAY }, + { 0x3C024, LIBERO_SETTING_PHY_EYE_TRAIN_DELAY }, + { 0x3C028, LIBERO_SETTING_PHY_EYE_PAT }, + { 0x3C02C, LIBERO_SETTING_PHY_START_RECAL }, + { 0x3C030, LIBERO_SETTING_PHY_CLR_DFI_LVL_PERIODIC }, + { 0x3C034, LIBERO_SETTING_PHY_TRAIN_STEP_ENABLE }, + { 0x3C038, LIBERO_SETTING_PHY_LPDDR_DQ_CAL_PAT }, + { 0x3C03C, LIBERO_SETTING_PHY_INDPNDT_TRAINING }, + { 0x3C040, LIBERO_SETTING_PHY_ENCODED_QUAD_CS }, + { 0x3C044, LIBERO_SETTING_PHY_HALF_CLK_DLY_ENABLE }, }; /* Program the full MC_BASE2/ADDR_MAP/MC_BASE1/MPFE/.../AXI_IF controller @@ -1134,14 +1118,9 @@ static int setup_phy(void) DDRPHY_REG(PHY_RPC156) = mpfs_phy_rpc156_val; /* DQ/DQS init offset (1..9) */ DDRPHY_REG(PHY_RPC166) = 0x00000002UL; /* Trained: 0x02 */ DDRPHY_REG(PHY_RPC168) = 0x00000000UL; /* Trained: 0x00 */ - /* rpc220 (DQ load delay). Full CFG_DDR_SGMII_PHY diff vs HSS - * (2026-06-01) shows HSS runs rpc220=0x1 during WRLVL and only - * raises it to 0xC inside write_calibration (mss_ddr.c:1744). - * Tested matching HSS (0x1 here): wolfBoot's AXI reads HANG (naked - * read @ 0xC0000000 stalls, WRCALIB times out). wolfBoot needs 0xC - * for the read path to function -- another HSS value that does not - * transfer to wolfBoot's PHY operating point. Keep 0xC. */ - DDRPHY_REG(PHY_RPC220) = 0x0000000CUL; /* wolfBoot-needed (HSS=0x1 hangs reads) */ + /* rpc220 (DQ load delay). HSS leaves the reset value (0x1) until write + * calibration; either value trains the same here. */ + DDRPHY_REG(PHY_RPC220) = 0x0000000CUL; /* rpc226 at offset 0x788: HSS-captured value 0x14. The * DDRPHY_MODE-driven preload normally populates this, but on this * board wolfBoot's preload ended up with 0x01 -- write it @@ -1450,8 +1429,8 @@ static uint8_t mpfs_write_calibration_using_mtc(uint8_t num_lanes) return 0U; } -/* DDR Training. retry_count = combined outer*MAX_TRAIN_RETRY + inner - * retry count. Used for HSS-style MOVE_CK ADDCMD cycling (mss_ddr.c: +/* DDR Training. retry_count = the caller's re-init attempt number, + * used for HSS-style MOVE_CK ADDCMD cycling (mss_ddr.c: * 6101-6128) which rotates the picked refclk index (0/45/90 deg -> * k / k+1 / k+2) across retries to converge when the first pick is * marginal. */ @@ -2247,6 +2226,49 @@ static void training_tip_wait(void) } +#ifdef DEBUG_DDR +/* Post-training dump in the column order of the Microchip HAL DDR demo + * (mss_ddr_debug.c), settling 50 us after each lane_select as HSS does. */ +static void training_posttip_dump(void) +{ + uint32_t lane; + uint32_t ioc2 = DDRPHY_REG(PHY_IOC_REG2); + uint32_t ioc5 = DDRPHY_REG(PHY_IOC_REG5); + + DBG_DDR("REFDUMP train_stat=0x%x PCODE=0x%x NCODE=0x%x WRCALIB=0x%x\n", + DDRPHY_REG(PHY_TRAINING_STATUS), ioc2 & 0x7FU, + (ioc2 >> 7) & 0x7FU, DDRPHY_REG(PHY_EXPERT_WRCALIB)); + DBG_DDR("REFDUMP sro_ref_slewr=0x%x sro_ref_slewf=0x%x sro_slewr=0x%x " + "sro_slewf=0x%x\n", ioc5 & 0x3FU, (ioc5 >> 6) & 0xFFFU, + (ioc5 >> 18) & 0x3FU, (ioc5 >> 24) & 0x3FU); + + for (lane = 0; lane < 4U; lane++) { + DDRPHY_REG(PHY_LANE_SELECT) = lane; + udelay(50); + DBG_DDR("REFDUMP L%u gt_err_comb=0x%x gt_txdly=0x%x gt_steps_180=0x%x " + "gt_state=0x%x gt_clk_sel=0x%x wl_delay_0=0x%x\n", + lane, DDRPHY_REG(PHY_GT_ERR_COMB), DDRPHY_REG(PHY_GT_TXDLY), + DDRPHY_REG(PHY_GT_STEPS_180), DDRPHY_REG(PHY_GT_STATE), + DDRPHY_REG(PHY_GT_CLK_SEL), DDRPHY_REG(PHY_WL_DELAY_0)); + DBG_DDR("REFDUMP L%u dqdqs_err_done=0x%x dqdqs_state=0x%x delta0=0x%x " + "delta1=0x%x dqdqs_window=0x%x\n", lane, + DDRPHY_REG(PHY_DQ_DQS_ERR_DONE), DDRPHY_REG(PHY_DQDQS_STATE), + DDRPHY_REG(PHY_DELTA0), DDRPHY_REG(PHY_DELTA1), + DDRPHY_REG(PHY_DQDQS_WINDOW)); + DBG_DDR("REFDUMP L%u rdqdqs_status2=0x%x addcmd_status0=0x%x " + "addcmd_status1=0x%x addcmd_answer=0x%x dqdqs_status1=0x%x\n", + lane, DDRPHY_REG(PHY_DQDQS_STATUS2), + DDRPHY_REG(PHY_ADDCMD_STATUS0), DDRPHY_REG(PHY_ADDCMD_STATUS1), + DDRPHY_REG(PHY_ADDCMD_ANSWER), DDRPHY_REG(PHY_DQDQS_STATUS1)); + } + /* TIP relies on lane_select being non-zero between iterations (see + * training_pretip_lane_snapshot), so leave it where the loop ended. */ +} +#else +#define training_posttip_dump() do { } while (0) +#endif + + static int run_training(uint32_t retry_count) { uint32_t timeout, dfi_stat, train_stat; @@ -2255,20 +2277,8 @@ static int run_training(uint32_t retry_count) uint32_t l; /* per-lane eye-width index */ uint32_t eye[4]; /* per-lane data-eye widths */ - /* TRAINING_SKIP = 0x02 to skip TIP's ADDCMD phase (we run our own - * manual ADDCMD via lpddr4_manual_training above). Matches HSS - * captured value 0x02 at PHY 0x80C (2026-05-15 DEBUG HEXDUMP). - * - * Previously experimented with 0x00 (full TIP training) under the - * theory that train_stat=0x1F (vs 0x1D) and DFI training_complete - * would help. That assumption was wrong: full TIP training picks - * different per-lane wl_dly values from the HSS-trained ones, and - * those wl_dly values combined with our other PHY config left the - * write-data path mistrained for lanes 2/3. */ - /* Tested TRAINING_SKIP=0x02 twice (with and without rpc220=0xC - * rpc226=0x14 alignment) -- regresses wl_dly to 0x56-0x7F across - * lanes (vs 0x24-0x2C with skip=0). HSS's TIP-skip approach - * requires pre-WRLVL PHY state we don't yet match. Keep skip=0. */ + /* Run every TIP step. The Libero default (0x02, skip ADDCMD) collapses + * lane 0's DQ/DQS window to one tap with this driver's manual ADDCMD. */ DDRPHY_REG(PHY_TRAINING_SKIP) = 0x00U; mb(); @@ -2691,16 +2701,16 @@ static int run_training(uint32_t retry_count) /* HSS DDR_TRAINING_VERIFY checks (mss_ddr.c:1488-1522): if any of * these are non-canonical, training had problems even though - * train_stat reads 0x1D. dqdqs_status2 is per-lane (selected via + * train_stat looks complete. dqdqs_status2 is per-lane (selected via * PHY_LANE_SELECT) -- dump all 4 to see per-lane data-eye width. */ for (l = 0; l < 4U; l++) { DDRPHY_REG(PHY_LANE_SELECT) = l; udelay(2); - eye[l] = DDRPHY_REG(0x850U); + eye[l] = DDRPHY_REG(PHY_DQDQS_STATUS2); } DBG_DDR( " gt_err_comb=0x%x dq_dqs_err_done=0x%x (need 8) eye[0..3]=%u/%u/%u/%u\n", - DDRPHY_REG(0x81CU), DDRPHY_REG(0x834U), + DDRPHY_REG(PHY_GT_ERR_COMB), DDRPHY_REG(PHY_DQ_DQS_ERR_DONE), eye[0], eye[1], eye[2], eye[3]); (void)eye; @@ -2787,17 +2797,14 @@ static int run_training(uint32_t retry_count) DDRCFG_REG(MC_CFG_AUTO_REF_EN) = 0x01; mb(); - /* HSS DDR_TRAINING_VERIFY (mss_ddr.c:1488-1504) reads: - * dq_dqs_err_done (need 8): DQ/DQS phase completion flag - * dqdqs_status2 (need >= 5 taps): data eye window width - * On this board both report bad values (0x4 / 0x0) yet train_stat - * reads 0x1D and lanes 2&3 still write correctly via the lanes-2&3 - * calibration committed by set_write_calib. Returning failure - * here triggers inner retries that empirically make PHY state - * WORSE (dq_dqs_err_done -> 0x0, all lanes fail WRCALIB), because - * back-to-back training without a power cycle accumulates errors. - * Accept training as-is; the calibration committed by WRCALIB is - * what we get. */ + /* HSS DDR_TRAINING_VERIFY (mss_ddr.c:1488-1504) reads dq_dqs_err_done + * (need 8) and dqdqs_status2 (need >= DQ_DQS_NUM_TAPS, which HSS sets + * to 5). Both are satisfied here. Returning failure would trigger + * inner retries, and back-to-back training without a power cycle + * accumulates errors rather than converging, so accept training as-is + * once those two checks pass. */ + + training_posttip_dump(); return 0; } @@ -3057,13 +3064,6 @@ int mpfs_ddr_init(unsigned int outer_retry) wolfBoot_printf("\n========================================\n"); - /* rpc_156 DQ/DQS init offset. Libero default 6 leaves the data eye - * closed (dqdqs_status2=0) on the Video Kit. HSS allows 1..9 via - * TUNE_RPC_156_DQDQS_INIT_VALUE. Empirically each fresh boot's - * training state degrades on subsequent attempts within the same - * power cycle, so we use a SINGLE value (no sweep): bump to 3 to - * push past the bad starting edge. Change in code if 3 doesn't - * give dqdqs_status2 >= 5 on cold boot. */ mpfs_phy_rpc156_val = 6U; (void)outer_retry; /* TUNE sweep removed; outer_retry kept for future use */ @@ -3140,21 +3140,14 @@ int mpfs_ddr_init(unsigned int outer_retry) * the MTC engine has a separate DDRC-internal access issue and would just * burn all retries and end in a WDT reset. * - * Empirical baseline: ~30% per-attempt training failure rate; 3 inner x - * 6 outer retries (up to 18 attempts) covers it with margin. + * A failed attempt is not retried in place: after the controller's + * training-fail reset the DFI init never completes again until the + * whole controller is re-initialised, which is also where HSS goes + * (DDR_TRAINING_FAIL -> DDR_TRAINING_CHECK_FOR_OFFMODE). The caller's + * outer loop does that, and every attempt keeps the best-margin refclk + * pick rather than pushing MOVE_CK to the next one. */ { - uint32_t train_retry = 0; - /* 3 inner attempts so the MOVE_CK rotation (0deg/45deg/90deg) - * cycles through all three alternative refclk picks (k / k+1 / - * k+2) in one DDRC-init pass. Combined with the 6 outer - * retries gives 18 chances for ADDCMD + WRLVL convergence. */ - /* Keep at 3 so MOVE_CK rotation cycles through 0deg/45deg/90deg - * within one outer DDRC-init pass. We don't force WRLVL retry - * (regression made all 18 attempts MTC-timeout) so most boots - * succeed on first attempt; the inner retries only matter when - * MTC sanity actually fails. */ - const uint32_t MAX_TRAIN_RETRY = 3; uint32_t lane; uint32_t mtc_to; uint32_t train_stat; @@ -3162,30 +3155,8 @@ int mpfs_ddr_init(unsigned int outer_retry) uint32_t dfi_to; int mtc_pass = 0; - while (train_retry < MAX_TRAIN_RETRY) { - if (train_retry > 0) { - wolfBoot_printf( - "DDR: Retry %u/%u after MTC sanity FAIL\n", - (unsigned)train_retry, (unsigned)MAX_TRAIN_RETRY); - /* HSS DDR_TRAINING_FAIL reset sequence (mss_ddr.c:519-538) */ - DDRCFG_REG(MC_INIT_CS) = 0x1; - DDRCFG_REG(MC_INIT_DISABLE_CKE) = 0x1; - ddr_delay(500); - DDRCFG_REG(MC_INIT_FORCE_RESET) = 0x1; - ddr_delay(200000); - DDRCFG_REG(MC_DFI_INIT_START) = 0x0; - DDRCFG_REG(MC_CTRLR_INIT) = 0x0; - DDRPHY_REG(PHY_TRAINING_START) = 0x0; - mb(); - } - train_retry++; - - /* Combined retry count for HSS-style MOVE_CK ADDCMD - * rotation: each outer DDR re-init contributes - * MAX_TRAIN_RETRY worth of count, so retry%3 cycles - * through all three move-CK pairs across all attempts. */ - ret = run_training(outer_retry * MAX_TRAIN_RETRY + - (train_retry - 1)); + do { + ret = run_training(0); if (ret != 0) { continue; } @@ -3244,6 +3215,53 @@ int mpfs_ddr_init(unsigned int outer_retry) (unsigned)wrcal); continue; } + /* HSS DDR_TRAINING_IP_SM_VERIFY: a DQ/DQS window under + * DQ_DQS_NUM_TAPS (5) on any lane is a failed training. */ + { + volatile uint32_t *rst_cnt = + (volatile uint32_t *)MPFS_DTIM_DDR_RESET_CNT_ADDR; + uint32_t eye_min = 0xFFU; + uint32_t n = 0; + for (lane = 0; lane < 4; lane++) { + uint32_t w; + DDRPHY_REG(PHY_LANE_SELECT) = lane; + udelay(50); + w = DDRPHY_REG(PHY_DQDQS_STATUS2); + if (w < eye_min) { + eye_min = w; + } + } + if ((rst_cnt[0] ^ rst_cnt[1]) == 0xFFFFFFFFUL) { + n = rst_cnt[0]; + } + if (eye_min < MPFS_DDR_EYE_MIN_TAPS && + n < MPFS_DDR_EYE_RESET_MAX) { + /* A controller re-init tends to repeat the narrow window; + * only a full MSS reset re-rolls it. */ + wolfBoot_printf("DDR: DQ/DQS window %u taps, MSS reset " + "%u/%u\n", (unsigned)eye_min, (unsigned)(n + 1U), + (unsigned)MPFS_DDR_EYE_RESET_MAX); + rst_cnt[0] = n + 1U; + rst_cnt[1] = ~(n + 1U); + mb(); + udelay(20000); + SYSREG_MSS_RESET_CR = 0xDEAD; + while (1) { + __asm__ volatile("wfi"); + } + } + if (eye_min < MPFS_DDR_EYE_MIN_TAPS) { + /* The window never widened: reject the training rather + * than run DDR on a one-tap eye. The reset count stays + * spent, so the outer retries cannot start a new series. */ + wolfBoot_printf("DDR: DQ/DQS window %u taps after %u " + "resets, training rejected\n", (unsigned)eye_min, + (unsigned)n); + return -1; + } + rst_cnt[0] = 0; + rst_cnt[1] = 0; + } train_stat = DDRPHY_REG(PHY_TRAINING_STATUS); /* Fast path: if TIP completed full training (train_stat @@ -3325,16 +3343,13 @@ int mpfs_ddr_init(unsigned int outer_retry) train_stat); mtc_pass = 1; - break; - } + } while (0); if (!mtc_pass) { - wolfBoot_printf("DDR: Training/MTC failed after %u retries\n", - (unsigned)MAX_TRAIN_RETRY); + wolfBoot_printf("DDR: training attempt failed\n"); return -2; } - wolfBoot_printf("DDR: Training+MTC PASS after %u retries\n", - (unsigned)(train_retry - 1)); + wolfBoot_printf("DDR: Training+MTC PASS\n"); } /* HSS LPDDR4 POST_INITIALIZATION (mss_ddr.c:5597-5646). */ diff --git a/options.mk b/options.mk index 272c5d4625..e369d6520d 100644 --- a/options.mk +++ b/options.mk @@ -1643,6 +1643,14 @@ ifneq (,$(filter RISCV RISCV64,$(ARCH))) CFLAGS+=-DSTACK_SIZE_PER_HART=$(STACK_SIZE_PER_HART) endif +# L2 scratchpad region size and boot-hart stack size, substituted into the +# linker script. The scratchpad is capped by the number of scratchpad ways +# the startup asm populates (4 ways x 128 KB on MPFS250); the linker script +# asserts the limit. Defaults keep the pre-existing layout for targets that +# do not override them. +WOLFBOOT_L2SCRATCH_SIZE ?= 256k +STACK_SIZE ?= 32k + CFLAGS+=$(CFLAGS_EXTRA) LDFLAGS+=$(LDFLAGS_EXTRA) OBJS+=$(OBJS_EXTRA) diff --git a/src/boot_riscv.c b/src/boot_riscv.c index 5de8f69719..4a03fbe5f1 100644 --- a/src/boot_riscv.c +++ b/src/boot_riscv.c @@ -60,6 +60,8 @@ extern void reloc_trap_vector(const uint32_t *address); /* Minimal SBI runtime (src/riscv_sbi.c): services S-mode ecalls and the * M-mode timer/software interrupts that back the S-mode timer and IPIs. */ extern unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc); +extern unsigned long sbi_redirect_trap(unsigned long cause, unsigned long epc, + unsigned long tval); extern void sbi_timer_irq(void); extern void sbi_ipi_irq(unsigned long hartid); extern unsigned long sbi_illegal_insn(unsigned long *regs, unsigned long epc, @@ -224,6 +226,17 @@ unsigned long WEAKFUNCTION handle_trap_ex(unsigned long cause, unsigned long epc } /* not handled: fall through to the fatal dump below */ } + + /* These harts cannot delegate access faults (medeleg reads back 0xB109): + * a supervisor exception M-mode has nothing to do with is handed to the + * supervisor's own vector, so the OS gets its fault instead of a parked + * hart. Environment calls from S-mode were serviced above. */ + if ((cause & MCAUSE_INT) == 0UL && ec != 9UL) { + unsigned long nepc = sbi_redirect_trap(cause, epc, tval); + if (nepc != 0UL) { + return nepc; + } + } #endif /* Halt on synchronous exceptions to prevent infinite trap-mret loops @@ -245,8 +258,9 @@ unsigned long WEAKFUNCTION handle_trap_ex(unsigned long cause, unsigned long epc } #if defined(DEBUG_BOOT) unsigned long sp_now; - wolfBoot_printf("TRAP: cause=%lx epc=%lx tval=%lx mstatus=%lx\n", - cause, epc, tval, csr_read(mstatus)); + wolfBoot_printf("TRAP: cause=%lx epc=%lx tval=%lx mstatus=%lx " + "medeleg=%lx\n", cause, epc, tval, csr_read(mstatus), + csr_read(medeleg)); __asm__ volatile("mv %0, sp" : "=r"(sp_now)); wolfBoot_printf(" sp=%lx\n", sp_now); #if defined(WOLFBOOT_RISCV_MMODE) && defined(TARGET_mpfs250) diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 5177180389..0093aa1f98 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -67,16 +67,50 @@ _reset: sw zero, 0x174(t1) /* SYSREG_L2_SHUTDOWN_CR = 0 */ fence - /* Route E51 D-cache to scratchpad ways (8-11) for the copy. - * With the default mask (0xFF = cache ways 0-7), stores to the - * Zero Device (0x0A000000) land in cache and never reach the - * scratchpad SRAM. The I-cache later fetches from scratchpad and - * gets uninitialized data (zeros), causing illegal-instruction - * traps. Setting the mask to 0xF00 forces stores into the actual - * scratchpad SRAM. See HSS mss_l2_cache.c config_l2_cache(). */ - li t4, 0x02010828 /* L2_WAY_MASK_E51_DCACHE */ - li t5, 0xF00 /* scratchpad ways 8-11 */ - sd t5, 0(t4) + /* Populate the scratchpad ways one way at a time, so every line of + * the Zero Device (0x0A000000) is resident before anything else runs. + * + * With the default mask (0xFF = cache ways 0-7), a store to the Zero + * Device allocates in an ordinary cache way; that line is evictable and + * its writeback is discarded, so the store is silently lost. Selecting + * all four scratchpad ways at once (0xF00) is not enough either: it only + * covers the stores made while the mask is set, which used to be the + * .text copy alone. Everything after the mask was restored -- .data, + * .bss (including the SDHCI PDMA staging buffer), the heap and the E51 + * stack -- still allocated in evictable ways. + * + * Walking one way at a time is what makes the whole 512 KB resident: a + * store can only allocate in the single enabled way, so 2048 blocks x 4 + * ways fills each way exactly once. Once resident, later stores hit and + * the mask no longer matters. This is HSS config_l2_cache(), see + * mss_l2_cache.c; the marker value matches its INIT_MARKER. + * + * Constants for WAY_ENABLE=0x0B, NUM_SCRATCH_PAD_WAYS=4: + * first way = 1 << ((WAY_ENABLE + 1) - NUM_SCRATCH_PAD_WAYS) = 1 << 8 + * blocks per way = WAY_BYTE_LENGTH / CACHE_BLOCK_BYTE_LENGTH + * = 0x20000 / 64 = 2048 */ + li t0, 0x02010828 /* L2_WAY_MASK_E51_DCACHE */ + li t1, 0x100 /* scratchpad way 8 */ + li t2, 0x0A000000 /* ZERO_DEVICE_BOTTOM */ + li t3, 0xC0FFEEBEC0010000 /* INIT_MARKER */ + li t4, 4 /* NUM_SCRATCH_PAD_WAYS */ +.L_pin_way: + sd t1, 0(t0) /* evictions allowed from this way only */ + fence + li t5, 2048 /* blocks per way */ +.L_pin_block: + sd t3, 0(t2) /* first 64-bit word of each block */ + addi t2, t2, 64 + addi t5, t5, -1 + bnez t5, .L_pin_block + slli t1, t1, 1 + fence + addi t4, t4, -1 + bnez t4, .L_pin_way + + /* Prevent the E51 from evicting from the scratchpad ways. */ + li t1, 0xFF /* cache ways 0-7 */ + sd t1, 0(t0) fence #endif @@ -97,11 +131,6 @@ _reset: addi t1, t1, 8 j .L_copy_text .L_copy_text_done: -#ifdef TARGET_mpfs250 - /* Restore D-cache to cache-only ways before normal execution */ - li t5, 0xFF /* cache ways 0-7 */ - sd t5, 0(t4) /* WAY_MASK_E51_DCACHE = cache-only */ -#endif fence rw, rw fence.i /* flush icache before jumping to SRAM */ diff --git a/src/riscv_sbi.c b/src/riscv_sbi.c index 734f475937..6e9be57925 100644 --- a/src/riscv_sbi.c +++ b/src/riscv_sbi.c @@ -114,7 +114,8 @@ #define SBI_BASE_GET_MARCHID 5 #define SBI_BASE_GET_MIMPID 6 -#define SBI_SPEC_VERSION ((0UL << 24) | 2UL) /* v0.2 */ +/* v0.3: the OS only uses SRST (reboot and power-off) from 0.3 up. */ +#define SBI_SPEC_VERSION ((0UL << 24) | 3UL) /* No registry ID is assigned to wolfBoot's SBI; report a custom value that * cannot collide with the small spec-registry IDs (0=BBL, 1=OpenSBI, 3=KVM, * 8=PolarFire HSS, ...). 0x776F6C66 = ASCII "wolf". */ @@ -157,24 +158,28 @@ typedef struct { volatile uint32_t init_magic; volatile int hart_state[MPFS_NUM_HARTS]; volatile uint32_t ipi_ops[MPFS_NUM_HARTS]; - /* Fence-completion protocol: ipi_done[h] is incremented by hart h - * only after it has executed the fence ops it consumed; a requester - * snapshots it into ipi_wait_gen[h] before posting and waits until - * ipi_done[h] passes the snapshot. */ + /* Fence-completion protocol: a requester ORs its op into ipi_ops[h] + * and then takes a ticket from ipi_posted[h]; hart h reads ipi_posted + * before it consumes ipi_ops, executes the fences, and publishes what + * it read as ipi_done[h]. A ticket is complete once ipi_done[h] has + * reached it: the fence that ran consumed an ops word that already + * held the request. A later post keeps its own ticket and is served + * by the next interrupt. */ + volatile uint32_t ipi_posted[MPFS_NUM_HARTS]; volatile uint32_t ipi_done[MPFS_NUM_HARTS]; - volatile uint32_t ipi_wait_gen[MPFS_NUM_HARTS]; } sbi_shared_state_t; #define SBI_SHARED ((sbi_shared_state_t *)SBI_SHARED_DTIM_ADDR) #define sbi_hart_state (SBI_SHARED->hart_state) #define sbi_ipi_ops (SBI_SHARED->ipi_ops) +#define sbi_ipi_posted (SBI_SHARED->ipi_posted) #define sbi_ipi_done (SBI_SHARED->ipi_done) -#define sbi_ipi_wait_gen (SBI_SHARED->ipi_wait_gen) /* Per-hart IPI work flags, set by a requesting hart and consumed in the * target hart's M-mode software-interrupt handler. */ #define SBI_IPI_OP_SSIP 1U /* inject a supervisor software interrupt */ #define SBI_IPI_OP_FENCE_I 2U /* remote instruction-stream sync */ #define SBI_IPI_OP_SFENCE 4U /* remote sfence.vma (full flush) */ +#define SBI_IPI_OP_HALT 8U /* park the hart in M-mode (system shutdown) */ /* Platform hook: release a parked hart into S-mode at saddr with a1=opaque * (SBI HSM hart_start backend). Weak default: unsupported. */ @@ -197,8 +202,8 @@ void sbi_hart_mark_started(unsigned long hartid) for (k = 0; k < (unsigned int)MPFS_NUM_HARTS; k++) { sbi_hart_state[k] = SBI_HSM_STOPPED; sbi_ipi_ops[k] = 0; + sbi_ipi_posted[k] = 0; sbi_ipi_done[k] = 0; - sbi_ipi_wait_gen[k] = 0; } __asm__ volatile("fence rw, rw" ::: "memory"); SBI_SHARED->init_magic = SBI_SHARED_MAGIC; @@ -422,14 +427,43 @@ unsigned long sbi_illegal_insn(unsigned long *regs, unsigned long epc, return 0UL; } -static void sbi_putc(char c) +/* The OS owns the UART by now and may have reconfigured or stopped it, so + * never wait on it for long with M-mode interrupts masked. Returns 1 when + * the byte was written. */ +#define SBI_UART_SPIN 200000U + +static int sbi_uart_ready(void) +{ + uint32_t n = SBI_UART_SPIN; + + while ((n > 0U) && ((SBI_UART_LSR & SBI_UART_THRE) == 0U)) { + n--; + } + return (n > 0U) ? 1 : 0; +} + +static int sbi_putc(char c) { if (c == '\n') { - while ((SBI_UART_LSR & SBI_UART_THRE) == 0U) { } + if (!sbi_uart_ready()) { + return 0; + } SBI_UART_THR = (uint8_t)'\r'; } - while ((SBI_UART_LSR & SBI_UART_THRE) == 0U) { } + if (!sbi_uart_ready()) { + return 0; + } SBI_UART_THR = (uint8_t)c; + return 1; +} + +/* Park this hart in M-mode for good. */ +static void __attribute__((noreturn)) sbi_system_halt(void) +{ + csr_clr_bits(mie, MIE_MTIP); + while (1) { + __asm__ volatile("wfi"); + } } /* Set a hart's S-mode timer. M-mode owns the timer; we program the CLINT @@ -457,15 +491,22 @@ void sbi_timer_irq(void) void sbi_ipi_irq(unsigned long hartid) { uint32_t ops = 0; + uint32_t posted = 0; CLINT_MSIP(hartid) = 0U; __asm__ volatile("fence iorw, iorw" ::: "memory"); if (hartid < (unsigned long)MPFS_NUM_HARTS) { + /* Read the ticket counter before consuming the ops: every request + * posted under a ticket acknowledged below is then already in it. */ + posted = __atomic_load_n(&sbi_ipi_posted[hartid], __ATOMIC_ACQUIRE); /* Atomically read-and-clear: a remote hart may be OR-ing a new op in * (sbi_post_ipi) concurrently with this consume. amoswap.w (rv64a) * makes the read+clear a single operation so no posted op is lost. */ ops = __atomic_exchange_n(&sbi_ipi_ops[hartid], 0U, __ATOMIC_ACQ_REL); } + if ((ops & SBI_IPI_OP_HALT) != 0U) { + sbi_system_halt(); + } if ((ops & SBI_IPI_OP_FENCE_I) != 0U) { __asm__ volatile("fence.i" ::: "memory"); } @@ -475,11 +516,9 @@ void sbi_ipi_irq(unsigned long hartid) if ((ops & SBI_IPI_OP_SSIP) != 0U || ops == 0U) { csr_set_bits(mip, MIP_SSIP); } - /* Publish completion only after the requested fences have executed: - * a requester treats the increment as this hart being done. */ - if ((ops & (SBI_IPI_OP_FENCE_I | SBI_IPI_OP_SFENCE)) != 0U) { - (void)__atomic_add_fetch(&sbi_ipi_done[hartid], 1U, - __ATOMIC_ACQ_REL); + /* Publish completion only after the requested fences have executed. */ + if (hartid < (unsigned long)MPFS_NUM_HARTS) { + __atomic_store_n(&sbi_ipi_done[hartid], posted, __ATOMIC_RELEASE); } } @@ -489,10 +528,14 @@ void sbi_ipi_irq(unsigned long hartid) * that include self rely on it), while for an RFENCE the caller has already * performed the fence locally, so self is skipped there. */ static void sbi_post_ipi(unsigned long mask, unsigned long base, - uint32_t op, unsigned long self) + uint32_t op, unsigned long self, uint32_t *ticket) { unsigned long i; unsigned long h; + + for (i = 0; i < (unsigned long)MPFS_NUM_HARTS; i++) { + ticket[i] = 0; + } /* SBI v0.2: hart_mask_base == -1 selects all available harts and * hart_mask is ignored. Normalize to (base 0, all-harts mask) so the * loop below does not compute h = (-1)+i and skip every target. */ @@ -517,30 +560,47 @@ static void sbi_post_ipi(unsigned long mask, unsigned long base, if (sbi_hart_state[h] != SBI_HSM_STARTED) { continue; /* parked harts consume MSIP in their wake loop */ } - /* Snapshot the completion counter before posting: the wait below - * completes when the target increments past this value. */ - if ((op & (SBI_IPI_OP_FENCE_I | SBI_IPI_OP_SFENCE)) != 0U) { - sbi_ipi_wait_gen[h] = sbi_ipi_done[h]; - } /* Atomic OR (amoor.w, rv64a): two harts may post to the same target * concurrently, and the target may be consuming (sbi_ipi_irq) at the * same time - a plain |= read-modify-write could drop an op. */ (void)__atomic_fetch_or(&sbi_ipi_ops[h], op, __ATOMIC_ACQ_REL); + /* Take the ticket after the op is in: the target acknowledges a + * ticket only with an ops word that already held this request. */ + if ((op & (SBI_IPI_OP_FENCE_I | SBI_IPI_OP_SFENCE)) != 0U) { + ticket[h] = __atomic_add_fetch(&sbi_ipi_posted[h], 1U, + __ATOMIC_ACQ_REL); + if (ticket[h] == 0U) { /* 0 means "nothing posted" below */ + ticket[h] = __atomic_add_fetch(&sbi_ipi_posted[h], 1U, + __ATOMIC_ACQ_REL); + } + } __asm__ volatile("fence rw, rw" ::: "memory"); CLINT_MSIP(h) = 1U; } __asm__ volatile("fence iorw, iorw" ::: "memory"); } +/* Shutdown has nowhere to go on this board: park every started hart. The + * OS has stopped the others already, but they still take timer interrupts + * and would keep running the supervisor's stall detectors. */ +static void __attribute__((noreturn)) sbi_system_shutdown(unsigned long self) +{ + uint32_t ticket[MPFS_NUM_HARTS]; + + sbi_post_ipi((1UL << MPFS_NUM_HARTS) - 1UL, 0, SBI_IPI_OP_HALT, self, + ticket); + sbi_system_halt(); +} + /* Wait (bounded) for the posted fence ops to be consumed by the targets: - * the targets increment ipi_done after executing the fences, so this - * completes only when every target has finished its fence. The SBI + * a target publishes ipi_done after executing the fences, so this + * completes only when every ticket has been reached. The SBI * remote-fence calls are synchronous; the bound guards against a wedged * target turning into a wedged caller. Returns SBI_SUCCESS when every * target completed, SBI_ERR_FAILED when any target did not complete * within the bound. */ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, - unsigned long self) + unsigned long self, const uint32_t *ticket) { unsigned long i; unsigned long h; @@ -557,20 +617,64 @@ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, } h = base + i; if (h == self || h >= (unsigned long)MPFS_NUM_HARTS || - sbi_hart_state[h] != SBI_HSM_STARTED) { + ticket[h] == 0U) { continue; } - spin = 10000000U; - while (sbi_ipi_done[h] <= sbi_ipi_wait_gen[h] && spin > 0U) { + /* A target can be inside a console write or another ecall for tens + * of milliseconds; a fence that gives up early leaves the OS with + * stale TLBs on that hart, so wait well past that (about 1 s). + * M-mode interrupts are masked in here, so service any fence the + * target (or a third hart) posts to us meanwhile: two harts fencing + * each other would otherwise each wait for the other and time out. */ + spin = 400000000U; + while ((int32_t)(__atomic_load_n(&sbi_ipi_done[h], __ATOMIC_ACQUIRE) - + ticket[h]) < 0 && spin > 0U) { + if (sbi_ipi_ops[self] != 0U) { + sbi_ipi_irq(self); + } spin--; } - if (sbi_ipi_done[h] <= sbi_ipi_wait_gen[h]) { + if ((int32_t)(__atomic_load_n(&sbi_ipi_done[h], __ATOMIC_ACQUIRE) - + ticket[h]) < 0) { err = SBI_ERR_FAILED; } } return err; } +/* Deliver a trap that arrived in M-mode from S/U mode to the supervisor's + * trap vector, the way delegation would have: sepc/scause/stval describe it, + * sstatus records the interrupted privilege and interrupt enable, and the + * mret lands on stvec in S-mode. Returns the new epc, or 0 when the trap + * came from M-mode or the supervisor has no vector installed yet. */ +unsigned long sbi_redirect_trap(unsigned long cause, unsigned long epc, + unsigned long tval) +{ + unsigned long mpp = (csr_read(mstatus) & MSTATUS_MPP_MASK) >> + MSTATUS_MPP_SHIFT; + unsigned long stvec = csr_read(stvec) & ~3UL; + unsigned long sstatus; + + if ((mpp == (unsigned long)PRV_M) || (stvec == 0UL)) { + return 0UL; + } + csr_write(sepc, epc); + csr_write(scause, cause); + csr_write(stval, tval); + sstatus = csr_read(sstatus) & ~(MSTATUS_SPP | MSTATUS_SPIE); + if (mpp == (unsigned long)PRV_S) { + sstatus |= MSTATUS_SPP; + } + if ((sstatus & MSTATUS_SIE) != 0UL) { + sstatus |= MSTATUS_SPIE; + } + sstatus &= ~MSTATUS_SIE; + csr_write(sstatus, sstatus); + csr_clr_bits(mstatus, MSTATUS_MPP_MASK); + csr_set_bits(mstatus, MSTATUS_MPP_S); + return stvec; +} + /* Returns the (possibly advanced) PC to resume at. For ecall we skip the * 4-byte ecall instruction. */ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) @@ -580,8 +684,9 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) long err = SBI_SUCCESS; unsigned long val = 0; unsigned long hartid; - volatile uint32_t spin; /* UART-drain delay before SRST/SHUTDOWN reset */ + uint32_t ticket[MPFS_NUM_HARTS]; #ifdef DEBUG_SBI + volatile uint32_t spin; /* UART-drain delay before SRST/SHUTDOWN reset */ static uint32_t sbi_dbg_calls = 0; #endif @@ -643,7 +748,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) case SBI_EXT_IPI: if (fid == 0) { /* send_ipi(hart_mask, hart_mask_base) */ - sbi_post_ipi(regs[A0], regs[A1], SBI_IPI_OP_SSIP, hartid); + sbi_post_ipi(regs[A0], regs[A1], SBI_IPI_OP_SSIP, hartid, ticket); } else { err = SBI_ERR_NOT_SUPPORTED; } @@ -664,8 +769,8 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) err = SBI_ERR_NOT_SUPPORTED; break; } - sbi_post_ipi(regs[A0], regs[A1], op, hartid); - err = sbi_wait_ipi_done(regs[A0], regs[A1], hartid); + sbi_post_ipi(regs[A0], regs[A1], op, hartid, ticket); + err = sbi_wait_ipi_done(regs[A0], regs[A1], hartid, ticket); break; } @@ -717,9 +822,13 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) unsigned long c; unsigned long j; uint8_t cbuf[64]; - if (n > 4096UL) { - n = 4096UL; /* bound a single call; kernel loops on val */ + /* Bound a single call (the kernel loops on val): this runs in + * M-mode with interrupts off, so a long write holds up fence + * IPIs from the other harts. 256 bytes is ~25 ms at 115200. */ + if (n > 256UL) { + n = 256UL; } + val = 0; for (k = 0; k < n; k += c) { c = n - k; if (c > sizeof(cbuf)) { @@ -727,14 +836,17 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) } sbi_copy_from_smode(cbuf, gp + k, c); for (j = 0; j < c; j++) { - sbi_putc((char)cbuf[j]); + if (!sbi_putc((char)cbuf[j])) { + k = n; + break; + } + val++; } } - val = n; break; } case 2: /* console_write_byte(byte) */ - sbi_putc((char)regs[A0]); + (void)sbi_putc((char)regs[A0]); break; case 1: /* console_read -- no input wired yet */ val = 0; @@ -746,10 +858,21 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) break; case SBI_EXT_SRST: - /* system_reset(type, reason): announce, drain UART, then reset. */ + /* system_reset(type, reason). No console output here: by now the OS + * owns the UARTs and a blocked printf leaves the hart stuck in the + * handler with the reset never issued. */ +#ifdef DEBUG_SBI wolfBoot_printf("[SBI] SYSTEM RESET requested: type=0x%lx " "reason=0x%lx\n", regs[A0], regs[A1]); for (spin = 0; spin < 20000000UL; spin++) { } +#endif + if (regs[A0] == 0UL) { + sbi_system_shutdown(hartid); + } + if (regs[A0] > 2UL) { + err = SBI_ERR_INVALID_PARAM; + break; + } #ifdef TARGET_mpfs250 SYSREG_MSS_RESET_CR = 0xDEAD; #endif @@ -761,7 +884,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) regs[A0] = 0; return epc + 4; case SBI_EXT_0_1_CONSOLE_PUTCHAR: - sbi_putc((char)regs[A0]); + (void)sbi_putc((char)regs[A0]); regs[A0] = 0; return epc + 4; case SBI_EXT_0_1_CONSOLE_GETCHAR: @@ -777,7 +900,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) unsigned long lmask = 0; sbi_copy_from_smode((uint8_t *)&lmask, regs[A0], sizeof(unsigned long)); - sbi_post_ipi(lmask, 0, SBI_IPI_OP_SSIP, hartid); + sbi_post_ipi(lmask, 0, SBI_IPI_OP_SSIP, hartid, ticket); } regs[A0] = 0; return epc + 4; @@ -792,18 +915,17 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) sizeof(unsigned long)); sbi_post_ipi(fmask, 0, (eid == SBI_EXT_0_1_REMOTE_FENCE_I) ? - SBI_IPI_OP_FENCE_I : SBI_IPI_OP_SFENCE, hartid); - err = sbi_wait_ipi_done(fmask, 0, hartid); + SBI_IPI_OP_FENCE_I : SBI_IPI_OP_SFENCE, hartid, ticket); + err = sbi_wait_ipi_done(fmask, 0, hartid, ticket); } regs[A0] = (unsigned long)err; return epc + 4; case SBI_EXT_0_1_SHUTDOWN: +#ifdef DEBUG_SBI wolfBoot_printf("[SBI] legacy SHUTDOWN requested\n"); for (spin = 0; spin < 20000000UL; spin++) { } -#ifdef TARGET_mpfs250 - SYSREG_MSS_RESET_CR = 0xDEAD; #endif - while (1) { } + sbi_system_shutdown(hartid); default: err = SBI_ERR_NOT_SUPPORTED; From 80143b6e016c175bcfee022bb1db8709558aa91d Mon Sep 17 00:00:00 2001 From: David Garske Date: Mon, 28 Sep 2026 14:05:08 -0700 Subject: [PATCH 02/23] PolarFire SoC: SHA-384 and AES-256-CTR offload to the Athena F5200 --- arch.mk | 16 +- hal/mpfs250.c | 586 +++++++++++++++++++++++++++++++++++++++++++++++++- hal/mpfs250.h | 34 ++- options.mk | 34 +++ 4 files changed, 654 insertions(+), 16 deletions(-) diff --git a/arch.mk b/arch.mk index a4613b8fe7..181b1b06e6 100644 --- a/arch.mk +++ b/arch.mk @@ -1405,9 +1405,19 @@ ifeq ($(ARCH),RISCV64) CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany else - # U54 cores: rv64gc (with FPU) - CFLAGS+=-march=rv64imafd$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64d -mcmodel=medany - LDFLAGS+=-march=rv64imafd -mabi=lp64d -mcmodel=medany + # U54 cores: soft-float lp64, matching Microchip's published user-crypto + # (CAL) archives - both are lp64 (bare-metal-examples is rv64imafd/lp64, + # hart-software-services is rv64imac/lp64). An lp64d build cannot link + # against either. wolfBoot does no floating point, so nothing is lost. + # + # rv64imac rather than rv64imafd because this toolchain ships no + # F/D-ISA + soft-float-ABI multilib: every rv64i*f*d* variant is paired + # with lp64f/lp64d, so -march=rv64imafd -mabi=lp64 compiles but fails to + # link ("can\'t link double-float modules with soft-float modules" against + # libgcc). The U54 keeps its FPU; wolfBoot simply does not emit FP + # instructions, and CAL\'s own F/D instructions still execute fine. + CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany + LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany # FDT support for DDR S-mode (not needed for L2-LIM bare-metal boot) ifneq ($(MPFS_L2LIM),1) diff --git a/hal/mpfs250.c b/hal/mpfs250.c index fbd6021018..61d7d1d413 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -46,6 +46,24 @@ #include "gpt.h" #include "fdt.h" +#ifdef MPFS_ATHENA +#include +/* Microchip's CAL headers for the Athena F5200, referenced not vendored (they + * carry a Mercury Systems notice); MPFS_ATHENA_CAL_DIR puts them on the path. */ +#ifndef CALCONFIGH /* the HSS calpolicy.h defines both */ +#define CALCONFIGH "config_user.h" +#endif +#ifndef INC_STDINT_H +#define INC_STDINT_H /* make caltypes.h use */ +#endif +/* caltypes.h and wolfSSL's types.h both define a uint128_t, a union of words + * there and __uint128_t here; nothing below needs CAL's. */ +#define uint128_t cal_uint128_t +#include "calini.h" +#include "hash.h" +#include "sym.h" +#undef uint128_t +#endif #if defined(DISK_SDCARD) || defined(DISK_EMMC) #include "sdhci.h" @@ -120,7 +138,6 @@ __attribute__((noinline)) void udelay(uint32_t us) #endif /* WOLFBOOT_RISCV_MMODE */ - /* Multi-Hart Support */ #ifdef WOLFBOOT_RISCV_MMODE @@ -362,6 +379,566 @@ static int test_ext_flash(void); static void qspi_uart_program(void); #endif +#ifdef MPFS_ATHENA +/* Ungate and release the Athena F5200 (sequence per HSS opensbi_crypto_ecall.c; + * CRYPTO_CR_INFO.MSS_MODE is informational, software does the un-reset). + * Idempotent: ATHENA_CR_RESET after CALIni() silently kills AES, not hashing. + * The SCA stall countermeasure (Security UG Table 7-7/7-8) is always on at + * rate 0 (1 in 8); DPA resistance is what the "S" part is for. rdcycle, not + * mcycle: an mcycle read traps in the S-mode builds. */ +static void mpfs_athena_enable(void) +{ + static int athena_enabled; + uint64_t cyc; + + if (athena_enabled) { + return; + } + + SYSREG_SUBBLK_CLOCK_CR |= MSS_PERIPH_ATHENA; + SYSREG_SOFT_RESET_CR &= ~MSS_PERIPH_ATHENA; + __asm__ volatile("rdcycle %0" : "=r"(cyc)); + ATHENA_STALL_CR = (uint32_t)(cyc ^ (cyc >> 32)); + ATHENA_CR = ATHENA_CR_RESET | ATHENA_CR_RINGOSCON; + ATHENA_CR = ATHENA_CR_RINGOSCON | ATHENA_CR_STALL_EN; + athena_enabled = 1; +} + +/* CAL finds the engine through this global: the User Crypto base in the Libero + * design. The HSS build has it compiled in and ignores this symbol. */ +uint32_t g_user_crypto_base_addr = 0x22000000UL; + +/* Hash context handed to CAL; sized for SATRESCONTEXT and checked below so a + * CAL update cannot silently overflow it. */ +#define MPFS_ATHENA_CTX_SIZE 256 +typedef char athena_ctx_size_check[ + (MPFS_ATHENA_CTX_SIZE >= (int)sizeof(SATRESCONTEXT)) ? 1 : -1]; + +static int mpfs_athena_engine_init(void) +{ + /* CALIni() must follow the un-reset, and the core must not be reset after + * it: that silently kills AES (writes nothing, returns success). */ + mpfs_athena_enable(); + if (CALIni() != SATR_SUCCESS) { + return -1; + } + return 0; +} + +/* CAL status of the last call that failed, reported by the self-check. */ +static int athena_last_cal_rc; + +static int mpfs_athena_sha384_init(void *ctx) +{ + SATR rc = CALHashCtxIni((SATRESCONTEXTPTR)ctx, SATHASHTYPE_SHA384); + + if (rc != SATR_SUCCESS) { + athena_last_cal_rc = (int)rc; + return -1; + } + return 0; +} + +/* len must be a whole number of blocks; a partial non-final chunk is rejected + * by the engine with SATR_BADHASHLEN. */ +static int mpfs_athena_sha384_update(void *ctx, const void *in, uint32_t len) +{ + SATR rc = CALHashCtx((SATRESHANDLE)0, (SATRESCONTEXTPTR)ctx, in, + (SATUINT32_t)len, NULL, SAT_FALSE); + + if (rc != SATR_SUCCESS) { + athena_last_cal_rc = (int)rc; + return -1; + } + return 0; +} + +/* The final call may carry any remaining length, including zero. */ +static int mpfs_athena_sha384_final(void *ctx, const void *in, uint32_t len, + void *digest) +{ + SATR rc = CALHashCtx((SATRESHANDLE)0, (SATRESCONTEXTPTR)ctx, in, + (SATUINT32_t)len, digest, SAT_TRUE); + + if (rc != SATR_SUCCESS) { + athena_last_cal_rc = (int)rc; + return -1; + } + return 0; +} + +#ifdef MPFS_ATHENA_AES +/* AES-256-CTR. CTR is symmetric, so this serves encrypt and decrypt alike. + * iv is advanced in place by the engine, so a caller may chain calls. */ +static int mpfs_athena_aes256_ctr(const void *key, void *iv, const void *in, + void *out, uint32_t len) +{ + if (CALSymEncrypt(SATSYMTYPE_AES256, (const SATUINT32_t *)key, + SATSYMMODE_CTR, iv, SAT_TRUE, in, out, (SATUINT32_t)len) + != SATR_SUCCESS) { + return -1; + } + /* CALSymEncrypt only starts the transfer. Without this wait the output + * buffer is never written while both calls still report SATR_SUCCESS. */ + if (CALSymTrfRes(SAT_TRUE) != SATR_SUCCESS) { + return -1; + } + return 0; +} +#endif /* MPFS_ATHENA_AES */ +#endif /* MPFS_ATHENA */ + +#ifdef MPFS_ATHENA +/* wolfCrypt crypto callback backed by the Athena F5200: SHA-384 and + * AES-256-CTR, which together are most of wolfBoot's per-image crypto cost. + * + * The engine is reached only through the plain-C mpfs_athena_* API, because + * CAL's headers cannot share a translation unit with wolfSSL's. + * + * Hash contract (established empirically; CAL does not document it): a + * non-final update must be a whole number of blocks, 128 bytes for SHA-384, + * and only the final call may carry a remainder. wolfBoot feeds + * WOLFBOOT_SHA_BLOCK_SIZE (4096) chunks, already a multiple of 128, but an + * image tail and short hashes such as a public key are not - hence the + * partial-block buffer below. */ +#define ATHENA_SHA384_BLOCK 128 +#define ATHENA_HASH_SLOTS 2 + +struct athena_hash_slot { + void *owner; /* wc_Sha384* that owns this slot */ + uint8_t ctx[MPFS_ATHENA_CTX_SIZE]; /* opaque CAL hash context */ + uint8_t part[ATHENA_SHA384_BLOCK]; /* partial block awaiting more data */ + uint32_t part_len; + uint8_t failed; /* an update faulted: no digest from + * this context can be trusted */ +}; + +static struct athena_hash_slot athena_hash_slots[ATHENA_HASH_SLOTS]; + +/* Set once the self-check has passed: from then on a SHA-384 request the + * engine cannot take is a hardware error, never a silent software fallback. */ +static int athena_ready; + +/* Counts callback entries actually serviced by the engine, so a correct + * digest cannot be mistaken for hardware use when the callback never ran. */ +static uint32_t athena_cb_calls; + +static struct athena_hash_slot *athena_slot_find(void *owner) +{ + int i; + + for (i = 0; i < ATHENA_HASH_SLOTS; i++) { + if (athena_hash_slots[i].owner == owner) { + return &athena_hash_slots[i]; + } + } + return NULL; +} + +/* Feed whole blocks from the slot buffer plus the caller's data. Anything + * short of a block is retained for the next call or the final. */ +static int athena_hash_feed(struct athena_hash_slot *slot, const uint8_t *in, + uint32_t len) +{ + uint32_t take; + + if (slot->part_len > 0) { + take = ATHENA_SHA384_BLOCK - slot->part_len; + if (take > len) { + take = len; + } + memcpy(&slot->part[slot->part_len], in, take); + slot->part_len += take; + in += take; + len -= take; + if (slot->part_len < ATHENA_SHA384_BLOCK) { + return 0; /* still short of a block */ + } + if (mpfs_athena_sha384_update(slot->ctx, slot->part, + ATHENA_SHA384_BLOCK) != 0) { + return -1; + } + slot->part_len = 0; + } + + take = len - (len % ATHENA_SHA384_BLOCK); + if (take > 0) { + if (mpfs_athena_sha384_update(slot->ctx, in, take) != 0) { + return -1; + } + in += take; + len -= take; + } + + if (len > 0) { + memcpy(slot->part, in, len); + slot->part_len = len; + } + return 0; +} + +#if defined(MPFS_ATHENA_AES) && defined(WOLFSSL_AES_COUNTER) && !defined(NO_AES) +/* AES-256-CTR. CTR is symmetric, so one path serves wolfBoot's encrypt and + * decrypt (both go through wc_AesCtrEncrypt). The raw key comes from + * aes->devKey, the field wolfCrypt fills for exactly this purpose; reading it + * does not depend on the key-schedule layout, and because wolfCrypt still runs + * its own key setup afterwards, declining a call here stays safe. The + * WOLF_CRYPTO_CB_SETKEY hook is deliberately not used: returning success from + * it makes wolfCrypt skip software key setup and strands the fallback. + * + * Every bail-out happens before any data reaches the engine. Afterwards + * wolfCrypt's counter no longer matches the data, so a late + * CRYPTOCB_UNAVAILABLE would silently corrupt the stream - a failure at that + * point has to be reported as an error instead. */ +static int athena_aesctr(wc_CryptoInfo *info) +{ + Aes *aes; + word32 sz; + word32 whole; + + if (info->cipher.type != WC_CIPHER_AES_CTR) { + return CRYPTOCB_UNAVAILABLE; + } + aes = info->cipher.aesctr.aes; + if (aes == NULL) { + return CRYPTOCB_UNAVAILABLE; + } + /* AES-256 only; the engine supports 128/192 but wolfBoot's encrypted + * images are AES-256 and an untested path is worse than none. */ + if (aes->keylen != 32) { + return CRYPTOCB_UNAVAILABLE; + } + /* aes->left != 0 means wolfCrypt holds keystream from a previous partial + * block; picking up mid-keystream would desync the counter. */ + if (aes->left != 0) { + return CRYPTOCB_UNAVAILABLE; + } + sz = info->cipher.aesctr.sz; + if (sz == 0) { + return 0; + } + + /* The engine advances the counter in place, so aes->reg stays correct for + * any subsequent call on the same context. */ + whole = sz - (sz % WC_AES_BLOCK_SIZE); + if (whole > 0) { + if (mpfs_athena_aes256_ctr(aes->devKey, aes->reg, + info->cipher.aesctr.in, info->cipher.aesctr.out, + whole) != 0) { + return WC_HW_E; + } + } + if (whole < sz) { + /* Trailing partial block: take one keystream block from the engine + * and leave the unused bytes where wolfCrypt keeps its own (aes->tmp + * with aes->left counting them), so the next call on this context + * continues the stream whichever path serves it. */ + uint8_t ks[WC_AES_BLOCK_SIZE]; + word32 i; + + memset(ks, 0, sizeof(ks)); + if (mpfs_athena_aes256_ctr(aes->devKey, aes->reg, ks, ks, + WC_AES_BLOCK_SIZE) != 0) { + return WC_HW_E; + } + for (i = whole; i < sz; i++) { + info->cipher.aesctr.out[i] = info->cipher.aesctr.in[i] ^ + ks[i - whole]; + } + memcpy(aes->tmp, ks, WC_AES_BLOCK_SIZE); + aes->left = WC_AES_BLOCK_SIZE - (sz - whole); + memset(ks, 0, sizeof(ks)); + } + athena_cb_calls++; + return 0; +} +#endif /* MPFS_ATHENA_AES && WOLFSSL_AES_COUNTER && !NO_AES */ + +static int mpfs_athena_cryptocb(int devIdArg, wc_CryptoInfo *info, void *ctx) +{ + struct athena_hash_slot *slot; + + (void)devIdArg; + (void)ctx; + + if (info == NULL) { + return CRYPTOCB_UNAVAILABLE; + } + +#if defined(MPFS_ATHENA_AES) && defined(WOLFSSL_AES_COUNTER) && !defined(NO_AES) + if (info->algo_type == WC_ALGO_TYPE_CIPHER) { + return athena_aesctr(info); + } +#endif + + if (info->algo_type != WC_ALGO_TYPE_HASH || + info->hash.type != WC_HASH_TYPE_SHA384) { + return CRYPTOCB_UNAVAILABLE; + } + + slot = athena_slot_find(info->hash.sha384); + if ((slot != NULL) && (info->hash.sha384->devCtx != slot)) { + /* Same address, but a context initialized after the one that held + * the slot was abandoned without a final: start over. */ + slot->owner = NULL; + slot->failed = 0; + slot = NULL; + } + if ((slot != NULL) && (slot->failed != 0)) { + /* An earlier update on this context faulted, so the engine holds a + * partial digest. Starting a fresh one here would digest only the + * data that follows the fault; callers that ignore an update's + * return code would then act on it. */ + return WC_HW_E; + } + if (slot == NULL) { + /* First call for this context. This is the only point where falling + * back to software is still safe: once any data has gone into the + * engine, wolfCrypt's own state is incomplete and a later + * CRYPTOCB_UNAVAILABLE would silently produce a wrong digest. */ + slot = athena_slot_find(NULL); + if (slot == NULL) { + return athena_ready ? WC_HW_E : CRYPTOCB_UNAVAILABLE; + } + if (mpfs_athena_sha384_init(slot->ctx) != 0) { + return athena_ready ? WC_HW_E : CRYPTOCB_UNAVAILABLE; + } + slot->owner = info->hash.sha384; + info->hash.sha384->devCtx = slot; + slot->part_len = 0; + slot->failed = 0; + } + + if (info->hash.in != NULL && info->hash.inSz > 0) { + if (athena_hash_feed(slot, info->hash.in, info->hash.inSz) != 0) { + slot->failed = 1; + return WC_HW_E; + } + } + + athena_cb_calls++; + + if (info->hash.digest != NULL) { + int ret = mpfs_athena_sha384_final(slot->ctx, slot->part, + slot->part_len, info->hash.digest); + slot->owner = NULL; /* release for reuse */ + slot->part_len = 0; + slot->failed = 0; + if (ret != 0) { + return WC_HW_E; + } + } + + return 0; +} + +/* Known answers for the self-check below. SHA-384 of the bytes 0x00..0xFF: + * a multi-block vector, hashed in chunks that straddle the 128-byte block so + * the intermediate state has to be carried across calls. */ +static const uint8_t athena_kat_sha384_256b[48] = { + 0xff, 0xda, 0xeb, 0xff, 0x65, 0xed, 0x05, 0xcf, + 0x40, 0x0f, 0x02, 0x21, 0xc4, 0xcc, 0xfb, 0x4b, + 0x21, 0x04, 0xfb, 0x6a, 0x51, 0xf8, 0x7e, 0x40, + 0xbe, 0x6c, 0x43, 0x09, 0x38, 0x6b, 0xfd, 0xec, + 0x28, 0x92, 0xe9, 0x17, 0x9b, 0x34, 0x63, 0x23, + 0x31, 0xa5, 0x95, 0x92, 0x73, 0x7d, 0xb5, 0xc5 +}; +#ifdef MPFS_ATHENA_AES +/* AES-256-CTR of the bytes 0x00..0x1F under key 0x00..0x1F, counter 0x00..0x0F. */ +static const uint8_t athena_kat_aesctr[32] = { + 0x5a, 0x6f, 0x06, 0x54, 0x0c, 0xfe, 0x77, 0x91, + 0xf8, 0x27, 0x5f, 0x36, 0x0e, 0xce, 0xa8, 0x9d, + 0x70, 0xe2, 0x02, 0xc6, 0xd7, 0x90, 0x4e, 0x4a, + 0x4d, 0x0f, 0xe1, 0x4a, 0x6e, 0xf8, 0x3e, 0xd0 +}; +#endif + +/* Run the offload through the wolfCrypt callback path once and compare with + * the known answers. The callback entry count is part of the check: wolfCrypt + * falls back to software silently when no device serves a call, and a correct + * result alone would not show that the hardware ran. */ +/* What the self-check tripped on, for the panic message. */ +static const char *athena_fail_what; +static int athena_fail_rc; + +static int mpfs_athena_selfcheck(void) +{ + wc_Sha384 sha; + uint8_t buf[256]; + uint8_t digest[48]; + int ret; + int i; + + athena_fail_what = NULL; + athena_fail_rc = 0; + athena_last_cal_rc = 0; + + for (i = 0; i < (int)sizeof(buf); i++) { + buf[i] = (uint8_t)i; + } + athena_cb_calls = 0; + ret = wc_InitSha384_ex(&sha, NULL, WOLFBOOT_DEVID_HASH); + if (ret != 0) { + athena_fail_what = "SHA-384 init"; + } + if (ret == 0) { + ret = wc_Sha384Update(&sha, buf, 100); + } + if (ret == 0) { + ret = wc_Sha384Update(&sha, &buf[100], 100); + } + if (ret == 0) { + ret = wc_Sha384Update(&sha, &buf[200], 56); + } + if ((ret != 0) && (athena_fail_what == NULL)) { + athena_fail_what = "SHA-384 update"; + } + if (ret == 0) { + ret = wc_Sha384Final(&sha, digest); + if (ret != 0) { + athena_fail_what = "SHA-384 final"; + } + } + wc_Sha384Free(&sha); + if (ret != 0) { + athena_fail_rc = ret; + return -1; + } + if (athena_cb_calls == 0) { + athena_fail_what = "SHA-384 callback never serviced (software fallback)"; + return -1; + } + if (memcmp(digest, athena_kat_sha384_256b, sizeof(digest)) != 0) { + athena_fail_what = "SHA-384 digest mismatch"; + return -1; + } + +#ifdef MPFS_ATHENA_AES + { + Aes aes; + uint8_t key[32], ctr[16], out[32], out2[32]; + + for (i = 0; i < 32; i++) { + key[i] = (uint8_t)i; + } + for (i = 0; i < 16; i++) { + ctr[i] = (uint8_t)i; + } + athena_cb_calls = 0; + ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); + if (ret == 0) { + ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out, buf, 32); + } + wc_AesFree(&aes); + /* The disk decrypt path calls CTR once per chunk, so the counter + * must carry across calls: two 16-byte calls equal one 32-byte call. */ + if (ret == 0) { + ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); + } + if (ret == 0) { + ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out2, buf, 16); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out2 + 16, buf + 16, 16); + } + wc_AesFree(&aes); + if (ret != 0) { + athena_fail_what = "AES-256-CTR"; + athena_fail_rc = ret; + return -1; + } + if (athena_cb_calls == 0) { + athena_fail_what = "AES-256-CTR callback never serviced (software fallback)"; + return -1; + } + if (memcmp(out, athena_kat_aesctr, sizeof(out)) != 0) { + athena_fail_what = "AES-256-CTR known-answer mismatch"; + return -1; + } + if (memcmp(out, out2, sizeof(out)) != 0) { + athena_fail_what = "AES-256-CTR counter does not carry across calls"; + return -1; + } + /* A length that is not a whole number of blocks, then the rest: the + * second call starts from the keystream the first one left behind. */ + memset(out2, 0, sizeof(out2)); + ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); + if (ret == 0) { + ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out2, buf, 20); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out2 + 20, buf + 20, 12); + } + wc_AesFree(&aes); + if (ret != 0) { + athena_fail_what = "AES-256-CTR partial block"; + athena_fail_rc = ret; + return -1; + } + if (memcmp(out2, athena_kat_aesctr, sizeof(out2)) != 0) { + athena_fail_what = "AES-256-CTR partial block mismatch"; + return -1; + } + } +#endif + return 0; +} + +/* A build that asks for the hardware must not fall through to software + * crypto unnoticed: every failure here halts. */ +static void mpfs_athena_init(void) +{ + int i; + + for (i = 0; i < ATHENA_HASH_SLOTS; i++) { + athena_hash_slots[i].owner = NULL; + athena_hash_slots[i].part_len = 0; + } + + /* Free slots hold INVALID_DEVID (-2), not 0, so registration returns + * BUFFER_E until the zero-initialised device table is initialised. */ + wc_CryptoCb_Init(); + + if (mpfs_athena_engine_init() != 0) { + wolfBoot_printf("Athena: engine init failed\n"); + wolfBoot_panic(); + } + if (wc_CryptoCb_RegisterDevice(WOLFBOOT_DEVID_HASH, mpfs_athena_cryptocb, + NULL) != 0) { + wolfBoot_printf("Athena: RegisterDevice failed\n"); + wolfBoot_panic(); + } +#if defined(WOLFBOOT_DEVID_CRYPT) && (WOLFBOOT_DEVID_CRYPT != WOLFBOOT_DEVID_HASH) + if (wc_CryptoCb_RegisterDevice(WOLFBOOT_DEVID_CRYPT, mpfs_athena_cryptocb, + NULL) != 0) { + wolfBoot_printf("Athena: cipher RegisterDevice failed\n"); + wolfBoot_panic(); + } +#endif + if (mpfs_athena_selfcheck() != 0) { + wolfBoot_printf("Athena: self-check failed: %s (rc %d, CAL rc %d, " + "callbacks %u)\n", athena_fail_what, athena_fail_rc, + athena_last_cal_rc, (unsigned)athena_cb_calls); + wolfBoot_panic(); + } + athena_ready = 1; +#ifdef MPFS_ATHENA_AES + wolfBoot_printf("Athena: SHA-384 + AES-256-CTR offload active\n"); +#else + wolfBoot_printf("Athena: SHA-384 offload active\n"); +#endif +} +#endif /* MPFS_ATHENA */ + void hal_init(void) { #ifdef WOLFBOOT_RISCV_MMODE @@ -460,6 +1037,10 @@ void hal_init(void) LIBWOLFBOOT_VERSION_STRING, __DATE__, __TIME__); #endif +#ifdef MPFS_ATHENA + mpfs_athena_init(); +#endif + #ifdef WOLFBOOT_RISCV_MMODE wolfBoot_printf("Running on E51 (hart 0) in M-mode\n"); DBG_DDR("Boot WDT_E51: REFRESH=%x CTRL=%x STATUS=%x TIME=%x MVRP=%x TRIG=%x\n", @@ -938,7 +1519,6 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) return 0; } - /* Wait for SCB register bits to clear, with timeout */ static int mpfs_scb_wait_clear(uint32_t reg_offset, uint32_t mask, uint32_t timeout) @@ -1456,7 +2036,6 @@ int ext_flash_erase(uintptr_t address, int len) #define QSPI_PROG_ACK 0x06 #define QSPI_RX_TIMEOUT_MS 10000U /* 10 s per byte -- aborts if host disappears */ - /* Returns 0-255 on success, -1 on timeout (so the boot path is never deadlocked). */ static int uart_qspi_rx(void) { @@ -1879,7 +2458,6 @@ void mpfs_iomux_init(void) #if defined(DISK_SDCARD) || defined(DISK_EMMC) /* SDHCI Platform HAL */ - /* MSS MPU base + per-master offset. Each AXI master (FIC0/1/2, CRYPTO, * GEM0/1, USB, MMC, SCB, TRACE) has 16 PMPCFG entries (uint64_t each) at * 0x20005000 + (master_index << 8). HSS calls mpu_configure() during early diff --git a/hal/mpfs250.h b/hal/mpfs250.h index e203c13b09..cc1876203b 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -170,7 +170,6 @@ #define MMUART_THR(base) *((volatile uint8_t*)((base)) + 0x100) /* Transmitter holding register */ #define MMUART_FCR(base) *((volatile uint8_t*)((base)) + 0x104) /* FIFO control register */ - /* LCR (Line Control Register) */ #define MSS_UART_DATA_8_BITS ((uint8_t)0x03) #define MSS_UART_NO_PARITY ((uint8_t)0x00) @@ -268,6 +267,31 @@ int mpfs_read_serial_number(uint8_t *serial); /* Crypto Engine: Athena F5200 (200 MHz) */ #define ATHENA_BASE (SYSREG_BASE + 0x125000) +/* Athena control block. Layout and bit names match athenareg_t / + * SYSREG_ATHENACR_* in config_athena.h from the Microchip user-crypto + * (CAL) library, where BASE32_ADDR_ATHENAREG is 0x20127000. */ +#define ATHENA_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x00))) +#define ATHENA_STALL_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x04))) +#define ATHENA_UPPER_ADDRESS (*((volatile uint32_t*)(ATHENA_BASE + 0x08))) +/* Security UG Table 7-7, MSS Crypto Control Register. RESET reads 1 out of + * power-on reset; MSS_OWNER reports that the MSS, not the fabric, holds the + * core, which is set by the Libero crypto ownership mode. */ +#define ATHENA_CR_RESET (1U << 0) +#define ATHENA_CR_PURGE (1U << 1) +#define ATHENA_CR_GO (1U << 2) +#define ATHENA_CR_RINGOSCON (1U << 3) +#define ATHENA_CR_STREAM_EN (1U << 4) +#define ATHENA_CR_STALL_EN (1U << 5) +#define ATHENA_CR_STALL_RATE_SHIFT 6 +#define ATHENA_CR_STALL_RATE_MASK (3U << ATHENA_CR_STALL_RATE_SHIFT) +#define ATHENA_CR_COMPLETE (1U << 8) +#define ATHENA_CR_ALARM (1U << 9) +#define ATHENA_CR_BUSERROR (1U << 10) +#define ATHENA_CR_STREAM_ENABLED (1U << 11) +#define ATHENA_CR_BUSY (1U << 12) +#define ATHENA_CR_MSS_OWNER (1U << 28) +#define ATHENA_CR_FAB_OWNER (1U << 29) + /* L2 Cache Controller (CACHE_CTRL @ 0x02010000) */ #define L2_CACHE_BASE 0x02010000UL @@ -305,7 +329,6 @@ int mpfs_read_serial_number(uint8_t *serial); #define L2_WAY_ENABLE_WITH_SCRATCH 0x0FFF #define L2_WAY_MASK_CACHE_ONLY 0xFF - /* CLINT - Core Local Interruptor */ #ifndef CLINT_BASE #define CLINT_BASE 0x02000000UL @@ -320,7 +343,6 @@ int mpfs_read_serial_number(uint8_t *serial); #define RISCV_SMODE_TIMER_FREQ MSS_CPU_CLK #endif - /* Hart Local Storage (HLS) - per-hart communication structure, 64 bytes at top of stack */ #define HLS_DEBUG_AREA_SIZE 64 @@ -387,7 +409,6 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #endif /* __ASSEMBLER__ */ - /* PLIC - Platform-Level Interrupt Controller (base 0x0C000000, 64MB) */ #define PLIC_BASE 0x0C000000UL #define PLIC_SIZE 0x04000000UL @@ -398,7 +419,6 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #define PLIC_INT_MMC_MAIN 88 - /* ============================================================================ * DDR Controller and PHY (LPDDR4) - Video Kit MPFS250T * @@ -592,7 +612,6 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #define IOSCB_IOC_REG0 0x004 #define IOSCB_IOC_REG1 0x008 - /* DDR Segment Register Offsets. * SEG is a 256-byte-stride peripheral pair (mss_seg.h:54): seg_t has * 8 x u32 control regs + 56 x u32 fill = 256 B. SEG[0] is at base @@ -630,7 +649,6 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #define DDR_INIT_TRAINING_FAIL -2 #define DDR_INIT_MEM_TEST_FAIL -3 - /* ============================================================================ * Video Kit Clock/DDR Configuration * @@ -691,7 +709,6 @@ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); #endif #endif /* __ASSEMBLER__ */ - #ifdef EXT_FLASH /* QSPI Flash Controller * @@ -809,5 +826,4 @@ int qspi_init(void); #endif /* EXT_FLASH */ - #endif /* MPFS250_DEF_INCLUDED */ diff --git a/options.mk b/options.mk index e369d6520d..99730c64af 100644 --- a/options.mk +++ b/options.mk @@ -1707,6 +1707,40 @@ ifeq ($(WOLFBOOT_TEST_SIM_CRYPTOCB),1) endif endif +# Athena F5200 (TeraFire) offload. MPFS_ATHENA_CAL_DIR points at Microchip's +# user-crypto (CAL) directory, e.g. /services/crypto. +# Not vendored: it carries a Mercury Systems licence. +ifeq ($(MPFS_ATHENA),1) + ifeq ($(MPFS_ATHENA_CAL_DIR),) + $(error MPFS_ATHENA=1 requires MPFS_ATHENA_CAL_DIR to point at the \ + Microchip user-crypto (CAL) directory) + endif + # Both CAL archives are soft-float lp64, so callers must be too (see arch.mk). + # M-mode: hart-software-services/services/crypto. S-mode: the rv64imafd + # build from polarfire-soc-bare-metal-examples .../middleware/cal. + CFLAGS += -DMPFS_ATHENA -I$(MPFS_ATHENA_CAL_DIR) + CFLAGS += -DWOLF_CRYPTO_CB + CFLAGS += -DWOLFBOOT_DEVID_HASH=0xA7 + CFLAGS += -DWOLFBOOT_DEVID_CRYPT=0xA7 + WOLFCRYPT_OBJS += $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/cryptocb.o + # The offload and its self-check are SHA-384; an image hashed with + # something else keeps that in software and still gets the AES offload. + ifneq ($(HASH),SHA384) + AUX_HASH_ALGOS+=sha384 + endif + # Archive name differs between Microchip's two published builds. + MPFS_ATHENA_CAL_LIB ?= mpfs-rv64imac-user-crypto-lib.a + LIBS += $(MPFS_ATHENA_CAL_DIR)/$(MPFS_ATHENA_CAL_LIB) + # AES-256-CTR offload rides on the AES-CTR path an AES-256 ENCRYPT build + # compiles; other builds offload the hash only. MPFS_ATHENA_AES=0 opts out. + ifeq ($(ENCRYPT_WITH_AES256),1) + MPFS_ATHENA_AES ?= 1 + ifeq ($(MPFS_ATHENA_AES),1) + CFLAGS += -DMPFS_ATHENA_AES + endif + endif +endif + # Size of the wolfHSM comm data payload, shared by the client and server blocks # below. The default is sized for certificate chains (the whole DER chain is # shipped to the HSM in a single message) and for ML-DSA keys/signatures. From 7036bea22aa3016cbed34a9f119a7a54837ffbe3 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 29 Sep 2026 15:46:15 -0700 Subject: [PATCH 03/23] PolarFire SoC: sNVM/PUF key storage and disk image encryption --- .github/workflows/test-configs.yml | 17 + Makefile | 20 +- arch.mk | 33 +- config/examples/polarfire_mpfs250.config | 8 +- config/examples/polarfire_mpfs250_m.config | 7 +- docs/Targets.md | 157 +++++++ docs/encrypted_partitions.md | 2 + docs/keystore.md | 4 + hal/mpfs250-m.ld | 13 +- hal/mpfs250.c | 299 ++++++++++--- hal/mpfs250.h | 62 ++- hal/mpfs250_ddr.c | 20 +- hal/mpfs250_snvm.c | 486 +++++++++++++++++++++ hal/mpfs250_snvm.h | 145 ++++++ options.mk | 90 +++- src/boot_riscv.c | 18 +- src/boot_riscv_start.S | 28 +- src/libwolfboot.c | 10 +- tools/unit-tests/Makefile | 24 + tools/unit-tests/unit-snvm-kek.c | 286 ++++++++++++ tools/unit-tests/unit-snvm-keystore.c | 328 ++++++++++++++ 21 files changed, 1907 insertions(+), 150 deletions(-) create mode 100644 hal/mpfs250_snvm.c create mode 100644 hal/mpfs250_snvm.h create mode 100644 tools/unit-tests/unit-snvm-kek.c create mode 100644 tools/unit-tests/unit-snvm-keystore.c diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 29a9964b17..3984061b69 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -546,6 +546,23 @@ jobs: config-file: ./config/examples/polarfire_mpfs250_m.config pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SIGN=ML_DSA ML_DSA_LEVEL=5 IMAGE_SIGNATURE_SIZE=4627 IMAGE_HEADER_SIZE=12288 WOLFBOOT_SECTOR_SIZE=0x4000 + # Trust anchor served from sNVM with the production hardenings: ROM-flag + # enforcement and the S-mode fence around the System Controller mailbox. + microchip_mpfs250_m_snvm_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1 MPFS_SCB_SMODE_DENY=1 + # Encrypted disk image with the AES key unwrapped by the PUF-derived KEK. + microchip_mpfs250_m_enc_kek_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1 SNVM_KEK=1 microchip_mpfs250_m_qspi_test: uses: ./.github/workflows/test-build-riscv.yml with: diff --git a/Makefile b/Makefile index 16c9212eeb..5265157968 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,10 @@ else PRIVATE_KEY?=wolfboot_signing_private_key.der endif endif - ifeq ($(FLASH_OTP_KEYSTORE),1) + ifeq ($(SNVM_KEYSTORE),1) + # PolarFire SoC: trust anchor served from secure NVM at runtime. + MPFS_SNVM_OBJ=1 + else ifeq ($(FLASH_OTP_KEYSTORE),1) OBJS+=./src/flash_otp_keystore.o else ifeq ($(WOLFBOOT_NO_KEYSTORE),1) CFLAGS+=-DWOLFBOOT_NO_KEYSTORE @@ -188,9 +191,22 @@ else WOLFBOOT_SIGN_KEY_DEP=$(PRIVATE_KEY) else OBJS+=./src/keystore.o + # Provisioning build: compiled keys plus the helper that writes them to sNVM. + ifeq ($(SNVM_KEYSTORE_PROVISION),1) + MPFS_SNVM_OBJ=1 + endif endif endif +# PolarFire SoC: sNVM keystore, PUF KEK and the PUF-wrapped encryption-key +# provider live in one object. +ifeq ($(SNVM_KEK),1) + MPFS_SNVM_OBJ=1 +endif +ifeq ($(MPFS_SNVM_OBJ),1) + OBJS+=./hal/mpfs250_snvm.o +endif + WOLFCRYPT_OBJS:= SECURE_OBJS:= PUBLIC_KEY_OBJS:= @@ -757,7 +773,7 @@ wolfboot_stage1.bin: wolfboot.elf stage1/loader_stage1.bin $(Q) cp stage1/loader_stage1.bin wolfboot_stage1.bin wolfboot.elf: include/target.h $(LSCRIPT) $(OBJS) $(BINASSEMBLE) $(WOLFBOOT_SIGN_KEY_DEP) FORCE - $(Q)(test $(SIGN) = NONE) || (test $(FLASH_OTP_KEYSTORE) = 1) || (test "$(WOLFBOOT_NO_KEYSTORE)" = "1") || (grep -q $(SIGN_ALG) src/keystore.c) || \ + $(Q)(test $(SIGN) = NONE) || (test $(FLASH_OTP_KEYSTORE) = 1) || (test "$(WOLFBOOT_NO_KEYSTORE)" = "1") || (test "$(SNVM_KEYSTORE)" = "1") || (grep -q $(SIGN_ALG) src/keystore.c) || \ (echo "Key mismatch: please run 'make keysclean' to remove all keys if you want to change algorithm" && false) @echo "\t[LD] $@" @echo $(OBJS) diff --git a/arch.mk b/arch.mk index 181b1b06e6..709f78e532 100644 --- a/arch.mk +++ b/arch.mk @@ -1337,13 +1337,9 @@ ifeq ($(ARCH),RISCV64) endif # Use M-mode specific linker script LSCRIPT_IN:=hal/$(TARGET)-m.ld - # MPFS DDR init pulls LIBERO_SETTING_* values from a Libero/HSS-generated - # fpga_design_config.h. Setting LIBERO_FPGA_CONFIG_DIR enables DDR init - # and adds the directory to the include search path. - # - # Booting an S-mode OS means running from DDR, so an empty - # LIBERO_FPGA_CONFIG_DIR here would silently produce a bootloader with no - # DDR controller init at all. Fail the build instead. + # LIBERO_FPGA_CONFIG_DIR supplies fpga_design_config.h and enables DDR init. + # An S-mode OS runs from DDR, so an empty value would silently build a + # bootloader with no DDR init at all -- fail instead. ifeq ($(LIBERO_FPGA_CONFIG_DIR),) ifneq (,$(findstring WOLFBOOT_MMODE_SMODE_BOOT,$(CFLAGS_EXTRA) $(CFLAGS))) ifeq ($(filter clean keysclean distclean,$(MAKECMDGOALS)),) @@ -1405,19 +1401,16 @@ ifeq ($(ARCH),RISCV64) CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany else - # U54 cores: soft-float lp64, matching Microchip's published user-crypto - # (CAL) archives - both are lp64 (bare-metal-examples is rv64imafd/lp64, - # hart-software-services is rv64imac/lp64). An lp64d build cannot link - # against either. wolfBoot does no floating point, so nothing is lost. - # - # rv64imac rather than rv64imafd because this toolchain ships no - # F/D-ISA + soft-float-ABI multilib: every rv64i*f*d* variant is paired - # with lp64f/lp64d, so -march=rv64imafd -mabi=lp64 compiles but fails to - # link ("can\'t link double-float modules with soft-float modules" against - # libgcc). The U54 keeps its FPU; wolfBoot simply does not emit FP - # instructions, and CAL\'s own F/D instructions still execute fine. - CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany - LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany + # U54. Microchip's CAL archive is built rv64imac soft-float, so a build + # that links it has to match; rv64imac not rv64imafd because the toolchain + # ships no F/D-ISA + soft-float multilib. wolfBoot emits no FP either way. + ifeq ($(MPFS_ATHENA),1) + CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany + LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany + else + CFLAGS+=-march=rv64imafd$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64d -mcmodel=medany + LDFLAGS+=-march=rv64imafd -mabi=lp64d -mcmodel=medany + endif # FDT support for DDR S-mode (not needed for L2-LIM bare-metal boot) ifneq ($(MPFS_L2LIM),1) diff --git a/config/examples/polarfire_mpfs250.config b/config/examples/polarfire_mpfs250.config index 701e113f55..ec61ed6521 100644 --- a/config/examples/polarfire_mpfs250.config +++ b/config/examples/polarfire_mpfs250.config @@ -66,8 +66,12 @@ WOLFBOOT_LOAD_ADDRESS?=0x8E000000 # Using update_disk loader we just need to specify the partition number or A/B WOLFBOOT_NO_PARTITIONS=1 WOLFBOOT_RAMBOOT_MAX_SIZE=0x80000000 -CFLAGS_EXTRA+=-DBOOT_PART_A=1 -CFLAGS_EXTRA+=-DBOOT_PART_B=2 +# 0-based GPT index. Index 1 on the stock Microchip layout is the BIOS-boot +# partition HSS reads its own payload from, leaving one usable boot slot: A and +# B name the same one, so failover has no rollback target until a p4 is added. +# For a two-slot layout see polarfire_mpfs250_m.config (A=0, B=1). +CFLAGS_EXTRA+=-DBOOT_PART_A=0 +CFLAGS_EXTRA+=-DBOOT_PART_B=0 # ============================================================================ # Optional: read the signed image from a file on a read-only filesystem diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index 79658269c5..41a361f63a 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -36,6 +36,8 @@ SPMATHALL?=1 DUALBANK_SWAP?=0 PKA?=0 ENCRYPT=0 +# Decrypt via a staging buffer: see the SDHCI_BLOCK_VIA_PDMA note above. +DISK_DECRYPT_STAGING=1 WOLFTPM?=0 ELF?=1 #DEBUG_ELF?=1 @@ -81,9 +83,8 @@ DISK_EMMC?=0 WOLFBOOT_ORIGIN?=0x0A000000 WOLFBOOT_L2SCRATCH_SIZE?=512k -# Boot-hart stack. Sized for ML-DSA-87, whose verify working set lands on -# top of the two IMAGE_HEADER_SIZE buffers src/update_disk.c puts on the -# stack; ECC384 needs far less. +# Boot-hart stack, sized for ML-DSA-87 verify on top of the two +# IMAGE_HEADER_SIZE buffers update_disk.c puts on the stack. ECC384 needs less. STACK_SIZE?=64k # 4KB sector size (SD card flow is partition-based, not flash-erase-based) diff --git a/docs/Targets.md b/docs/Targets.md index 6e7fe1f05c..3da9f8a209 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -1321,6 +1321,10 @@ target-independent `src/ddr_cadence.c` / `include/ddr_cadence.h` (controller bas board's `LIBERO_SETTING_*` values, stay in `hal/mpfs250_ddr.c`, which builds the controller register table and composes the generic calls. Both compile only when `MPFS_DDR_INIT` is set. +### PolarFire SoC hardware root of trust (PUF KEK, sNVM keystore, wrapped encryption key) + +The System Controller SRAM-PUF, secure NVM (sNVM), and TeraFire crypto can anchor key material in hardware: serve the verification public keys from sNVM, derive a device-unique KEK from the PUF, and store the AES image-encryption key in sNVM wrapped by that KEK. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). + ### PolarFire testing This section describes how to build the test-application, create a custom uSD with required partitions and copying signed test-application to uSD partitions. @@ -1844,6 +1848,159 @@ ML-DSA 87 verify 200 ops took 1.077 sec, avg 5.385 ms, 185.704 ops/ Benchmark complete ``` +### PolarFire SoC: PUF Root of Trust, sNVM Keystore, and PUF-Wrapped Encryption Key (M-Mode) + +This document describes the PolarFire SoC MPFS250 hardware-root-of-trust features added for the wolfBoot E51 M-mode boot flow (the same flow that brings up LPDDR4 and boots a signed Yocto FIT from SD; see `Targets.md`). They let wolfBoot anchor key material in the chip's System Controller instead of compiling it into the bootloader image: + +1. A System Controller mailbox driver for secure NVM (sNVM), the SRAM-PUF, and the nonce/TRNG service. +2. An sNVM keystore backend that serves the verification public keys from sNVM at boot. +3. A device-unique Key Encryption Key (KEK) derived from the SRAM-PUF, used to wrap/unwrap individual keys. +4. A PUF-wrapped image-encryption key: the AES image key is stored in sNVM wrapped by the PUF KEK and unwrapped at boot, with the bulk image decrypted on the M-mode disk-boot path. + +All features are off by default and gated behind build flags, so a normal build is unaffected. + +#### Trust model + +The intended chain, from the immutable hardware up: + +``` +PolarFire boot ROM (secure boot of wolfBoot: not configured on the Video Kit, bootmode 1) + -> wolfBoot (M-mode, eNVM) + -> System Controller SRAM-PUF (fixed challenge) -> SHA-384 -> 256-bit KEK + -> sNVM keystore: ECC384 verification public key(s) + -> sNVM: AES-256 image key, wrapped by the PUF KEK + -> verify + decrypt the signed/encrypted Yocto FIT + -> 4-CPU SMP Linux +``` + +The KEK is re-derived on every boot and never leaves the device. The public keys are not secret; storing them in sNVM (rather than the image) lets the trust anchor be provisioned and rotated per device, and binds verification to the System Controller. + +##### Production requirement: the keystore pages must not stay writable + +The sNVM write service is reachable from anything that can drive the System Controller mailbox, including the Linux `mpfs-sys-controller` driver, so on the development flow above the trust anchor is only as protected as the OS. A production unit closes this in one of two ways, both opt-in at build time: + +- Deliver the keystore pages as ROM in the bitstream (Libero sNVM client with the keystore image as content and the ROM attribute set) and build with `SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1`: wolfBoot reads the page admin word of every keystore module and refuses to boot unless its ROM flag is set. The ROM flag is a bitstream property; the sNVM system services can neither set nor clear it, so there is no on-device step that can lock a page by accident. The on-device provisioning writer is for development only and cannot write a ROM page. +- Build with `MPFS_SCB_SMODE_DENY=1`: every hart's PMP fences S-mode off the System Controller control and mailbox window (`0x37020000`-`0x37020FFF`) before the hand-off, and the DTB fixup disables the `mailbox@37020800`, `spi@37020100` (System Controller QSPI) and `syscontroller` nodes so the OS does not probe hardware it cannot reach. A supervisor access into the window is otherwise redirected to the kernel's trap vector as an access fault (this core cannot delegate access faults, so wolfBoot's SBI forwards them). The sNVM pages stay writable by wolfBoot (M-mode) only. This removes the OS-side System Controller services (device serial number, bitstream services, sNVM access, the zeroize procedure) and anything the OS image builds on them: on the Video Kit Yocto image the `varlog-luks-key.service` unit, which fetches its LUKS key through the System Controller, fails and systemd waits out its device timeout before the login prompt. + +#### 1. System Controller mailbox services + +`hal/mpfs250.c` drives the System Controller services mailbox (`SCBCTRL` at `0x37020000`, mailbox RAM at `0x37020800`). A small generic `mpfs_scb_request()` issues a service and polls the request/busy bits; `mpfs_scb_read()` reads the response. The following services are wrapped: + +| Service | Opcode | wolfBoot wrapper | +|---------|--------|------------------| +| Device serial number | `0x00` | `mpfs_read_serial_number()` | +| sNVM write (plaintext / authenticated / ciphertext) | `0x10` / `0x11` / `0x12` | `mpfs_snvm_write()` | +| sNVM read | `0x18` | `mpfs_snvm_read()` | +| PUF emulation | `0x20` | `mpfs_puf_emulation()` | +| Nonce (TRNG) | `0x21` | `mpfs_nonce()` | + +sNVM holds 221 modules of 252 bytes (plaintext) or 236 bytes plus a 12-byte User Secret Key (authenticated). The PUF emulation service returns a 32-byte device-unique response for a 16-byte challenge; for a fixed challenge the response is stable within a boot and across cold boots, which is what makes it usable as a KEK seed. The nonce service returns 32 random bytes. + +Note: the TRNG (nonce) and PUF services take far longer than serial/sNVM-read, so the busy-completion wait uses a larger bound (`MPFS_SCB_BUSY_TIMEOUT`) than the request-accept wait (`MPFS_SCB_TIMEOUT`). + +#### 2. sNVM public-key keystore (`SNVM_KEYSTORE`) + +`hal/mpfs250_snvm.c` + `hal/mpfs250_snvm.h` implement the five-function keystore API (`keystore_num_pubkeys`, `keystore_get_buffer`, `keystore_get_size`, `keystore_get_mask`, `keystore_get_key_type`) by reading the trust anchor from sNVM. The on-sNVM layout is the same family as the OTP keystore (`src/flash_otp_keystore.c`): a `WOLFBOOT`-magic header followed by packed `struct keystore_slot` entries, read through a linear reader that spans consecutive sNVM modules from `SNVM_KEYSTORE_MODULE` (default 200, `SNVM_KEYSTORE_MAX_MODULES` default 1; an ML-DSA-87 key needs 11). For the on-device provisioning writer every module in the range must be left runtime-writable (not ROM) in the Libero sNVM configuration: on the stock Video Kit design only modules 200 and 201 are. A production bitstream delivers the same page contents as ROM instead (see the trust model above). + +Build wiring (mirrors `FLASH_OTP_KEYSTORE`): + +- `SNVM_KEYSTORE=1` links `hal/mpfs250_snvm.o` in place of the compiled-in `src/keystore.o`, so the running bootloader contains no public keys and serves them from sNVM. +- `SNVM_KEYSTORE_PROVISION=1` keeps the compiled keystore and adds `snvm_keystore_provision()`, a one-time on-device writer that copies the compiled keystore image into sNVM (called from `hal_init`). Run once on a board, then deploy the `SNVM_KEYSTORE=1` build. Both provisioning writers read the page first and skip it when it already holds the content (`snvm: module N already provisioned`), so the extra boot the programmer triggers after flashing, or a second boot of the provisioning image, does not spend another write cycle. + +A keystore page that reads back as all zeros is reported as `sNVM keystore zeroized` and halts the boot. That is the state a zeroize procedure leaves behind (the OS overwrites the keystore and encryption-key pages through the System Controller's plaintext sNVM write service), and it is distinguished from a corrupt header, which merely yields no keys, so a deliberately erased unit is identifiable on the console. Recovery is the provisioning build again. + +#### 3. PUF KEK and AES key-wrap (`SNVM_KEK`) + +The KEK and key-wrap live in `hal/mpfs250_snvm.c` as well: + +- `mpfs_puf_kek()` derives the 256-bit KEK as `SHA-384(label || PUF(fixed-challenge))` truncated to 32 bytes. SHA-384 is used because it is the build's hash (SHA-256/HMAC/HKDF are compiled out); the PUF response is already a high-entropy device-unique secret, so a labelled hash provides domain separation without pulling in HKDF. +- `snvm_kek_wrap()` / `snvm_kek_unwrap()` wrap/unwrap a key with the KEK using RFC 3394 AES key-wrap (`wc_AesKeyWrap` / `wc_AesKeyUnWrap`). + +`SNVM_KEK=1` adds RFC 3394 key-wrap to the AES that an `ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1` build already compiles; the KEK has no other consumer, so the build refuses the flag without that combination. Determinism and the wrap/unwrap round trip are covered by the host unit test `tools/unit-tests/unit-snvm-kek.c`. + +The AES key-wrap uses the wolfCrypt software implementation. The Athena F5200 offload (`MPFS_ATHENA=1`, see `Targets.md`) serves SHA-384 and the bulk AES-256-CTR image decrypt through wolfCrypt crypto callbacks; the key-wrap is a few blocks per boot and stays in software. + +#### 4. PUF-wrapped image-encryption key + +`hal/mpfs250_snvm.c` provides `wolfBoot_get_encrypt_key()` (under `CUSTOM_ENCRYPT_KEY`): it reads `[wrapped AES key (40 B)][nonce (16 B)]` from sNVM module `SNVM_ENCKEY_MODULE` (default 201, outside the keystore range), unwraps the key with the PUF KEK, and returns the plaintext key + nonce to the decrypt path. `snvm_enckey_provision()` (under `SNVM_ENCKEY_PROVISION`) wraps a key with the device KEK and writes the blob to sNVM. + +##### Disk-image decryption enablement + +wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partition model and required `EXT_FLASH` or `MMU`. The M-mode disk-FIT path uses `WOLFBOOT_NO_PARTITIONS` and is neither. Two small, target-independent changes enable encrypted disk boot generally (regression-safe; host unit tests pass): + +- `src/libwolfboot.c`: the `EXT_ENCRYPTED requires EXT_FLASH or MMU` guard now also accepts `CUSTOM_ENCRYPT_KEY` (the platform supplies the key), and `hal_set_key()` (partition-resident key storage) is folded under `#ifndef CUSTOM_ENCRYPT_KEY`. +- `src/update_disk.c`: the `DISK_ENCRYPT` path gains `ForceZero` for secret cleanup. + +##### PolarFire M-mode decrypt note + +The image is loaded into DDR by the SD controller and decrypted there. CPU stores into that region are not coherent with what the controller wrote (which is why the load uses `SDHCI_BLOCK_VIA_PDMA`), so `polarfire_mpfs250_m.config` sets `DISK_DECRYPT_STAGING=1`: the ciphertext is read through the non-cached DDR alias, decrypted a chunk at a time into a staging buffer and landed with the same PDMA copy the load used. + +#### Build flags summary + +| Flag | Effect | +|------|--------| +| `SNVM_KEYSTORE` | Serve the public-key trust anchor from sNVM (replaces compiled keystore) | +| `SNVM_KEYSTORE_PROVISION` | Add the one-time on-device keystore writer | +| `SNVM_KEK` | Enable the PUF KEK + RFC 3394 AES key-wrap (needs the AES-256 `ENCRYPT` + `CUSTOM_ENCRYPT_KEY` build) | +| `SNVM_ENCKEY_PROVISION` | Add the one-time PUF-wrapped encryption-key writer | +| `SNVM_KEYSTORE_MODULE` / `SNVM_KEYSTORE_MAX_MODULES` / `SNVM_ENCKEY_MODULE` | sNVM module numbers (defaults 200 / 1 / 201; the key module must lie outside the keystore range and be runtime-writable) | +| `WOLFBOOT_SNVM_WRITE_APPROVED` | Required by every knob that writes sNVM; the build fails without it | +| `SNVM_KEYSTORE_REQUIRE_ROM` | Refuse to boot unless every keystore page is ROM-flagged (bitstream-provisioned keystore) | +| `MPFS_SCB_SMODE_DENY` | PMP-fence S-mode off the System Controller mailbox and disable its DTB nodes | +| `SNVM_ENCKEY_INSECURE_TEST_KEY` | Acknowledges provisioning the built-in public test key | +| `SNVM_ENCKEY_PROVISION_EXTERN` | Provision a production key instead: the integrator defines `snvm_enckey_prov_key[32]` and `snvm_enckey_prov_nonce[16]` in another object | + +Example runtime encrypted build (compiled keystore for verification, PUF-wrapped key for decryption): + +```sh +make CROSS_COMPILE=riscv64-unknown-elf- LIBERO_FPGA_CONFIG_DIR= \ + ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1 SNVM_KEK=1 wolfboot.elf +``` + +#### Provisioning and validation recipe + +1. Build with `SNVM_ENCKEY_PROVISION=1` (and/or `SNVM_KEYSTORE_PROVISION=1`) plus `WOLFBOOT_SNVM_WRITE_APPROVED=1` added to the runtime build above, flash, and boot once: `hal_init` writes the PUF-wrapped AES key into sNVM[201] (and/or the keystore into sNVM[200]). +2. Sign + encrypt the FIT with a key file matching the provisioned key (`prov_aes_key` || `prov_aes_nonce` in `hal/mpfs250_snvm.c`, i.e. a 32-byte key followed by a 16-byte IV): + + ```sh + IMAGE_HEADER_SIZE=512 ./tools/keytools/sign --ecc384 --sha384 \ + --encrypt enc_key.bin --aes256 fitImage wolfboot_signing_private_key.der 1 + ``` + Note: `make keysclean` regenerates the signing key, so re-sign the image after any keysclean or the compiled keystore will not match. +3. Write `fitImage_v1_signed_and_encrypted.bin` to the SD boot partition. With the stock HSS, boot to the `>>` CLI, run `usbdmsc`, and `dd` to the first partition of the exposed disk. Do not move an SD-Wire mux while `usbdmsc` is running: HSS's MMC layer then serves one stale block for every LBA and does not recover until `usbdmsc` is restarted, which makes a populated card read as blank. +4. Flash the runtime build and cold-boot. Expected UART0: `Disk encryption enabled` -> `Decrypting image... done` -> `Firmware Valid.` -> `M->S handoff`, and UART1 reaches the Linux login with `Brought up 1 node, 4 CPUs` and no `failed to come online`. + +#### Sample validation logs (MPFS250TS Video Kit) + +The PUF response is identical for a fixed challenge across cold power cycles while the nonce service is random, and the PUF-derived KEK wraps and unwraps a test key deterministically (the same wrapped bytes on every boot); both were verified on the kit and the KEK logic is covered by `tools/unit-tests/unit-snvm-kek.c`. + +Full encrypted boot captured on the Video Kit (UART1, S-mode wolfBoot under HSS; the standalone M-mode target boots the same image through the staged PDMA decrypt): the AES key is unwrapped from sNVM with the PUF KEK, the image is loaded, decrypted and verified. + +``` +wolfBoot Version: 2.9.0 +Disk encryption enabled +... +Load address 0x8E000000 +Attempting boot from P:A +Loading image from disk...done +Decrypting image...done +Checking image integrity...done +Verifying image signature...done +Firmware Valid. +Booting at 8E000000 +PolarFire SoC MPFS250 wolfBoot demo Application +``` + +Linux (UART1): + +``` +[ 0.040726] smp: Brought up 1 node, 4 CPUs +OpenEmbedded nodistro.0 mpfs-video-kit ttyS1 +mpfs-video-kit login: +``` + +No `failed to come online`; the only error lines are the known-cosmetic eth-SGMII PHY, i2c clock-divider, and mmc-tuning messages unrelated to this feature. + ## STM32F7 The STM32-F76x and F77x offer dual-bank hardware-assisted swapping. diff --git a/docs/encrypted_partitions.md b/docs/encrypted_partitions.md index c977a95f14..3c36883f5d 100644 --- a/docs/encrypted_partitions.md +++ b/docs/encrypted_partitions.md @@ -42,6 +42,8 @@ You can use the `CUSTOM_ENCRYPT_KEY` option to implement your own functions for: `wolfBoot_get_encrypt_key`, `wolfBoot_set_encrypt_key` and `wolfBoot_erase_encrypt_key`. +For an example that stores the AES key in non-volatile memory wrapped by a device-unique hardware PUF key (Microchip PolarFire SoC), see [polarfire_snvm_puf.md](polarfire_snvm_puf.md). Note that disk/`NO_PARTITIONS` boot can use `CUSTOM_ENCRYPT_KEY` for image encryption without `EXT_FLASH` or `MMU`. + To enable: 1) Add `CUSTOM_ENCRYPT_KEY=1` to your `.config` diff --git a/docs/keystore.md b/docs/keystore.md index a61f85bcd0..d010562060 100644 --- a/docs/keystore.md +++ b/docs/keystore.md @@ -222,6 +222,10 @@ wolfBoot supports certain platforms that contain connected HSMs (Hardware Securi To support this mode of operation, the `keygen` tool supports the `--nolocalkeys` option, which instructs the tool to generate a keystore entry with a zeroed key material. It still generates the `.der` files for private and public keys, so the wolfBoot key tools can sign images, but the `keystore.c` file that is linked into wolfBoot will contain all zeros in the `pubkey` field. Because the key material isn't present in the keystore, the keypair used to sign the image and stored on the HSM for verification can be updated in the field without needing to rebuild wolfBoot against a new `keystore.c`, as long as the signature algorithm and key size does not change. Most targets that use this option will automatically add it to the key generation options or explicitly mention this step in the build documentation. +### Using KeyStore in secure NVM (PolarFire SoC sNVM) + +On Microchip PolarFire SoC the trust anchor can be served from the System Controller secure NVM (sNVM) instead of being compiled into the bootloader, using the `SNVM_KEYSTORE` backend. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). + ## Build System Integration By default, when running `make` to build the default target (`factory.bin`) for the first time, wolfBoot automatically generates a signing keypair and creates a single-key keystore as a "demonstration". This is distinct from using `keygen` directly with `-g` or `-i` options, which provides full control over keystore creation. diff --git a/hal/mpfs250-m.ld b/hal/mpfs250-m.ld index bf012e9124..b73ccc53f9 100644 --- a/hal/mpfs250-m.ld +++ b/hal/mpfs250-m.ld @@ -26,18 +26,13 @@ MEMORY * This offset is added by mpfsBootmodeProgrammer (bootmode 1) */ FLASH_ENVM (rx) : ORIGIN = 0x20220100, LENGTH = 128k - 0x100 - /* L2 Scratchpad SRAM. Size is bounded by the number of scratchpad ways - * the startup asm populates one way at a time (see the pin loop in - * src/boot_riscv_start.S); MPFS250 hardwires ways 8-11 as scratchpad, so - * 4 x WAY_BYTE_LENGTH = 512 KB is the ceiling. Set per target with - * WOLFBOOT_L2SCRATCH_SIZE; the ASSERT below enforces the ceiling. */ + /* L2 scratchpad. MPFS250 hardwires ways 8-11, so 4 x 128 KB is the + * ceiling the startup pin loop can make resident; ASSERT below enforces it. */ L2_SCRATCH (rwx) : ORIGIN = @WOLFBOOT_ORIGIN@, LENGTH = @WOLFBOOT_L2SCRATCH_SIZE@ } -/* Stack size for the boot hart (E51 in M-mode). Set per target with - * STACK_SIZE: ECC384 + SHA384 needs only a few KB, but ML-DSA-87 adds its - * own working set plus two IMAGE_HEADER_SIZE buffers that src/update_disk.c - * places on the stack. */ +/* Boot-hart stack. ECC384+SHA384 needs a few KB; ML-DSA-87 adds its working + * set plus the two IMAGE_HEADER_SIZE buffers update_disk.c puts on the stack. */ PROVIDE(STACK_SIZE = @STACK_SIZE@); /* 4 scratchpad ways x 128 KB (WAY_BYTE_LENGTH). Going past this hands out diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 61d7d1d413..eaab8dad05 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -45,6 +45,9 @@ #include "hal.h" #include "gpt.h" #include "fdt.h" +#if defined(SNVM_KEYSTORE_PROVISION) || defined(SNVM_ENCKEY_PROVISION) +#include "mpfs250_snvm.h" +#endif #ifdef MPFS_ATHENA #include @@ -138,6 +141,7 @@ __attribute__((noinline)) void udelay(uint32_t us) #endif /* WOLFBOOT_RISCV_MMODE */ + /* Multi-Hart Support */ #ifdef WOLFBOOT_RISCV_MMODE @@ -489,18 +493,9 @@ static int mpfs_athena_aes256_ctr(const void *key, void *iv, const void *in, #endif /* MPFS_ATHENA */ #ifdef MPFS_ATHENA -/* wolfCrypt crypto callback backed by the Athena F5200: SHA-384 and - * AES-256-CTR, which together are most of wolfBoot's per-image crypto cost. - * - * The engine is reached only through the plain-C mpfs_athena_* API, because - * CAL's headers cannot share a translation unit with wolfSSL's. - * - * Hash contract (established empirically; CAL does not document it): a - * non-final update must be a whole number of blocks, 128 bytes for SHA-384, - * and only the final call may carry a remainder. wolfBoot feeds - * WOLFBOOT_SHA_BLOCK_SIZE (4096) chunks, already a multiple of 128, but an - * image tail and short hashes such as a public key are not - hence the - * partial-block buffer below. */ +/* SHA-384 + AES-256-CTR via the plain-C mpfs_athena_* API (CAL headers cannot + * share a TU with wolfSSL's). Undocumented CAL contract: a non-final update + * must be whole 128-byte blocks, so short tails need the buffer below. */ #define ATHENA_SHA384_BLOCK 128 #define ATHENA_HASH_SLOTS 2 @@ -523,6 +518,9 @@ static int athena_ready; * digest cannot be mistaken for hardware use when the callback never ran. */ static uint32_t athena_cb_calls; +/* Keyed by wc_Sha384 address; the cryptocb has no free event, so a context + * freed without Final strands its slot and a later one at that address would + * resume stale CAL state (wrong digest). Only reachable via ONESHOT today. */ static struct athena_hash_slot *athena_slot_find(void *owner) { int i; @@ -578,18 +576,9 @@ static int athena_hash_feed(struct athena_hash_slot *slot, const uint8_t *in, } #if defined(MPFS_ATHENA_AES) && defined(WOLFSSL_AES_COUNTER) && !defined(NO_AES) -/* AES-256-CTR. CTR is symmetric, so one path serves wolfBoot's encrypt and - * decrypt (both go through wc_AesCtrEncrypt). The raw key comes from - * aes->devKey, the field wolfCrypt fills for exactly this purpose; reading it - * does not depend on the key-schedule layout, and because wolfCrypt still runs - * its own key setup afterwards, declining a call here stays safe. The - * WOLF_CRYPTO_CB_SETKEY hook is deliberately not used: returning success from - * it makes wolfCrypt skip software key setup and strands the fallback. - * - * Every bail-out happens before any data reaches the engine. Afterwards - * wolfCrypt's counter no longer matches the data, so a late - * CRYPTOCB_UNAVAILABLE would silently corrupt the stream - a failure at that - * point has to be reported as an error instead. */ +/* AES-256-CTR, one path for both directions. Key from aes->devKey, not the + * WOLF_CRYPTO_CB_SETKEY hook: succeeding there strands the software fallback. + * All bail-outs precede any data entering the engine (counter would desync). */ static int athena_aesctr(wc_CryptoInfo *info) { Aes *aes; @@ -692,10 +681,8 @@ static int mpfs_athena_cryptocb(int devIdArg, wc_CryptoInfo *info, void *ctx) return WC_HW_E; } if (slot == NULL) { - /* First call for this context. This is the only point where falling - * back to software is still safe: once any data has gone into the - * engine, wolfCrypt's own state is incomplete and a later - * CRYPTOCB_UNAVAILABLE would silently produce a wrong digest. */ + /* First call: the only safe point to decline, since afterwards + * wolfCrypt's own state is incomplete and the digest would be wrong. */ slot = athena_slot_find(NULL); if (slot == NULL) { return athena_ready ? WC_HW_E : CRYPTOCB_UNAVAILABLE; @@ -939,6 +926,7 @@ static void mpfs_athena_init(void) } #endif /* MPFS_ATHENA */ + void hal_init(void) { #ifdef WOLFBOOT_RISCV_MMODE @@ -1083,6 +1071,25 @@ void hal_init(void) #endif #endif + +#ifdef SNVM_KEYSTORE_PROVISION + /* One-time: write the compiled-in trust anchor into sNVM so a subsequent + * SNVM_KEYSTORE build serves its keys from sNVM. Stop on failure so a + * provisioning run cannot look successful. */ + if (snvm_keystore_provision() != 0) { + wolfBoot_printf("snvm provision: FAILED\n"); + wolfBoot_panic(); + } +#endif + +#ifdef SNVM_ENCKEY_PROVISION + /* One-time: store the PUF-wrapped image-encryption key in sNVM. */ + if (snvm_enckey_provision() != 0) { + wolfBoot_printf("enckey provision: FAILED\n"); + wolfBoot_panic(); + } +#endif + #ifdef EXT_FLASH if (qspi_init() != 0) { wolfBoot_printf("QSPI: Init failed\n"); @@ -1104,63 +1111,219 @@ static int mpfs_scb_mailbox_busy(void) return (SCBCTRL_REG(SERVICES_SR_OFFSET) & SERVICES_SR_BUSY_MASK); } -/* Read 16-byte device serial number via SCB system service (opcode 0x00). */ -int mpfs_read_serial_number(uint8_t *serial) +/* System Controller service, polling mode, mailbox word offset 0. Returns the + * 16-bit service status (0 = success) or a negative transport error. */ +static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, + uint32_t req_len) { - uint32_t cmd, status; - int i, timeout; + uint32_t cmd, words, rem, i, v; + int timeout; - if (serial == NULL) { - return -1; - } - - /* Check if mailbox is busy */ if (mpfs_scb_mailbox_busy()) { wolfBoot_printf("SCB mailbox busy\n"); return -2; } - /* Send serial number request command (opcode 0x00) - * Command format: [31:16] = opcode, [0] = request bit */ - cmd = (SYS_SERV_CMD_SERIAL_NUMBER << SERVICES_CR_COMMAND_SHIFT) | + /* Write request words into the mailbox (RMW for a non-word-aligned tail). */ + words = req_len / 4u; + for (i = 0; i < words; i++) { + v = (uint32_t)req[(i * 4u) + 0u]; + v |= ((uint32_t)req[(i * 4u) + 1u]) << 8; + v |= ((uint32_t)req[(i * 4u) + 2u]) << 16; + v |= ((uint32_t)req[(i * 4u) + 3u]) << 24; + SCBMBOX_REG(i * 4u) = v; + } + rem = req_len - (words * 4u); + if (rem > 0u) { + v = SCBMBOX_REG(words * 4u); + for (i = 0; i < rem; i++) { + v &= ~(((uint32_t)0xFFu) << (i * 8u)); + v |= ((uint32_t)req[(words * 4u) + i]) << (i * 8u); + } + SCBMBOX_REG(words * 4u) = v; + } + + /* Ensure mailbox writes land before the request is raised. */ + __asm__ volatile("fence w,w" ::: "memory"); + + /* Command: opcode in [22:16] (SERVICES_CR_COMMAND_SHIFT), REQ in bit 0; + * the mailbox word-offset field is left 0. */ + cmd = (((uint32_t)(opcode & 0x7Fu)) << SERVICES_CR_COMMAND_SHIFT) | SERVICES_CR_REQ_MASK; SCBCTRL_REG(SERVICES_CR_OFFSET) = cmd; - /* Wait for request bit to clear (command accepted) */ + /* Wait for request bit to clear (command accepted). */ timeout = MPFS_SCB_TIMEOUT; - while ((SCBCTRL_REG(SERVICES_CR_OFFSET) & SERVICES_CR_REQ_MASK) && timeout > 0) { + while ((SCBCTRL_REG(SERVICES_CR_OFFSET) & SERVICES_CR_REQ_MASK) && + (timeout > 0)) { timeout--; } if (timeout == 0) { - wolfBoot_printf("SCB mailbox request timeout\n"); + wolfBoot_printf("SCB request timeout\n"); return -3; } - /* Wait for busy bit to clear (command completed) */ - timeout = MPFS_SCB_TIMEOUT; - while (mpfs_scb_mailbox_busy() && timeout > 0) { + /* Wait for busy bit to clear (service complete). Uses the larger + * completion bound: nonce/PUF take much longer than serial/sNVM-read. */ + timeout = MPFS_SCB_BUSY_TIMEOUT; + while (mpfs_scb_mailbox_busy() && (timeout > 0)) { timeout--; } if (timeout == 0) { - wolfBoot_printf("SCB mailbox busy timeout\n"); + wolfBoot_printf("SCB busy timeout\n"); return -4; } - /* Check status (upper 16 bits of status register) */ - status = (SCBCTRL_REG(SERVICES_SR_OFFSET) >> SERVICES_SR_STATUS_SHIFT) & 0xFFFF; - if (status != 0) { - wolfBoot_printf("SCB mailbox error: 0x%x\n", status); - return -5; + return (int)((SCBCTRL_REG(SERVICES_SR_OFFSET) >> SERVICES_SR_STATUS_SHIFT) + & 0xFFFFu); +} + +/* Read len response bytes from the mailbox at byte offset off (mb word 0). */ +static void mpfs_scb_read(uint8_t *out, uint32_t off, uint32_t len) +{ + uint32_t i; + for (i = 0; i < len; i++) { + out[i] = SCBMBOX_BYTE(off + i); + } +} + +/* Read 16-byte device serial number via SCB system service (opcode 0x00). */ +int mpfs_read_serial_number(uint8_t *serial) +{ + int ret; + + if (serial == NULL) { + return -1; } + ret = mpfs_scb_request(SYS_SERV_CMD_SERIAL_NUMBER, NULL, 0); + if (ret == 0) { + mpfs_scb_read(serial, 0, DEVICE_SERIAL_NUMBER_SIZE); + } + return ret; +} - /* Read serial number from mailbox RAM (16 bytes) */ - for (i = 0; i < DEVICE_SERIAL_NUMBER_SIZE; i++) { - serial[i] = SCBMBOX_BYTE(i); +int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, + uint8_t *data, uint16_t data_len) +{ + uint8_t frame[16]; + int ret, i; + + if ((data == NULL) || (module >= MPFS_SNVM_MODULE_MAX)) { + return -1; + } + if ((data_len != MPFS_SNVM_AUTH_DATA_LEN) && + (data_len != MPFS_SNVM_PLAIN_DATA_LEN)) { + return -1; + } + if ((data_len == MPFS_SNVM_AUTH_DATA_LEN) && (usk == NULL)) { + return -1; } + for (i = 0; i < (int)sizeof(frame); i++) { + frame[i] = 0; + } + frame[0] = module; /* bytes 1..3 reserved (0) */ + if (data_len == MPFS_SNVM_AUTH_DATA_LEN) { + for (i = 0; i < MPFS_SNVM_USK_LEN; i++) { + frame[4 + i] = usk[i]; + } + } + + ret = mpfs_scb_request(SYS_SERV_CMD_SNVM_READ, frame, sizeof(frame)); + if (ret != 0) { + return ret; + } + + /* Response: 4 admin bytes then data_len data bytes at READ_RET_OFFSET. */ + if (admin != NULL) { + mpfs_scb_read(admin, MPFS_SNVM_READ_RET_OFFSET, MPFS_SNVM_ADMIN_LEN); + } + mpfs_scb_read(data, MPFS_SNVM_READ_RET_OFFSET + MPFS_SNVM_ADMIN_LEN, + data_len); return 0; } +int mpfs_snvm_write(uint8_t format, uint8_t module, const uint8_t *data, + const uint8_t *usk) +{ + uint8_t frame[256]; + uint32_t datalen, total; + int i; + + if ((data == NULL) || (module >= MPFS_SNVM_MODULE_MAX)) { + return -1; + } + if (format == SYS_SERV_CMD_SNVM_WRITE_PLAIN) { + datalen = MPFS_SNVM_PLAIN_DATA_LEN; + total = 4u + MPFS_SNVM_PLAIN_DATA_LEN; /* 256 */ + } + else if ((format == SYS_SERV_CMD_SNVM_WRITE_AUTH) || + (format == SYS_SERV_CMD_SNVM_WRITE_CIPHER)) { + if (usk == NULL) { + return -1; + } + datalen = MPFS_SNVM_AUTH_DATA_LEN; + total = 4u + MPFS_SNVM_AUTH_DATA_LEN + MPFS_SNVM_USK_LEN; /* 252 */ + } + else { + return -1; + } + + for (i = 0; i < (int)sizeof(frame); i++) { + frame[i] = 0; + } + frame[0] = module; /* bytes 1..3 reserved (0) */ + for (i = 0; i < (int)datalen; i++) { + frame[4 + i] = data[i]; + } + if (datalen == MPFS_SNVM_AUTH_DATA_LEN) { + for (i = 0; i < MPFS_SNVM_USK_LEN; i++) { + frame[4 + MPFS_SNVM_AUTH_DATA_LEN + i] = usk[i]; + } + } + + return mpfs_scb_request(format, frame, total); +} + +int mpfs_puf_emulation(const uint8_t *challenge, uint8_t op_type, + uint8_t *response) +{ + uint8_t frame[20]; + int ret, i; + + if ((challenge == NULL) || (response == NULL)) { + return -1; + } + for (i = 0; i < (int)sizeof(frame); i++) { + frame[i] = 0; + } + frame[0] = op_type; /* bytes 1..3 reserved (0) */ + for (i = 0; i < MPFS_PUF_CHALLENGE_LEN; i++) { + frame[4 + i] = challenge[i]; + } + + ret = mpfs_scb_request(SYS_SERV_CMD_PUF_EMULATION, frame, sizeof(frame)); + if (ret == 0) { + mpfs_scb_read(response, MPFS_PUF_RET_OFFSET, MPFS_PUF_RESPONSE_LEN); + } + return ret; +} + +int mpfs_nonce(uint8_t *nonce) +{ + int ret; + + if (nonce == NULL) { + return -1; + } + ret = mpfs_scb_request(SYS_SERV_CMD_NONCE, NULL, 0); + if (ret == 0) { + mpfs_scb_read(nonce, 0, MPFS_NONCE_LEN); + } + return ret; +} + + /* Linux kernel command line arguments */ /* Must stay below the fdt.h include: the LINUX_BOOTARGS_OVERRIDE default * there keys off build-supplied macros only, not this fallback. */ @@ -1195,7 +1358,13 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) * when an OS-side petting story exists. */ static const char *const cpu_off[] = { "watchdog@20001000", "watchdog@20101000", "watchdog@20103000", - "watchdog@20105000", "watchdog@20107000" }; + "watchdog@20105000", "watchdog@20107000", +#ifdef MPFS_SCB_SMODE_DENY + /* PMP fences S-mode off the System Controller mailbox: the OS + * drivers for it would only fault. */ + "mailbox@37020800", "spi@37020100", "syscontroller", +#endif + }; unsigned int i; #endif @@ -1325,6 +1494,21 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) return 0; } +#ifdef DISK_DECRYPT_STAGING +/* Ciphertext is read through the non-cached DDR alias and plaintext is landed + * with the PDMA copy below: CPU stores into DDR are not coherent with what the + * SD controller wrote (see polarfire_mpfs250_m.config). */ +uintptr_t hal_disk_decrypt_addr(uintptr_t addr) +{ + return addr | 0x40000000UL; +} + +int hal_disk_decrypt_copy(void *dst, const void *src, uint32_t len) +{ + return wolfBoot_fit_memcpy(dst, src, len); +} +#endif + #if defined(WOLFBOOT_RISCV_MMODE) && defined(MPFS_DDR_INIT) /* FIT subimage copy via PDMA (overrides the weak default in src/fdt.c). * CPU writes to DDR do not land on this board, so route kernel/dtb copies @@ -1519,6 +1703,7 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) return 0; } + /* Wait for SCB register bits to clear, with timeout */ static int mpfs_scb_wait_clear(uint32_t reg_offset, uint32_t mask, uint32_t timeout) @@ -2036,6 +2221,7 @@ int ext_flash_erase(uintptr_t address, int len) #define QSPI_PROG_ACK 0x06 #define QSPI_RX_TIMEOUT_MS 10000U /* 10 s per byte -- aborts if host disappears */ + /* Returns 0-255 on success, -1 on timeout (so the boot path is never deadlocked). */ static int uart_qspi_rx(void) { @@ -2458,6 +2644,7 @@ void mpfs_iomux_init(void) #if defined(DISK_SDCARD) || defined(DISK_EMMC) /* SDHCI Platform HAL */ + /* MSS MPU base + per-master offset. Each AXI master (FIC0/1/2, CRYPTO, * GEM0/1, USB, MMC, SCB, TRACE) has 16 PMPCFG entries (uint64_t each) at * 0x20005000 + (master_index << 8). HSS calls mpu_configure() during early diff --git a/hal/mpfs250.h b/hal/mpfs250.h index cc1876203b..48ae21598b 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -236,13 +236,39 @@ /* System Service command opcodes */ #define SYS_SERV_CMD_SERIAL_NUMBER 0x00u #define SYS_SERV_CMD_SPI_COPY 0x50u /* SCB mailbox SPI copy service */ +#define SYS_SERV_CMD_SNVM_WRITE_PLAIN 0x10u /* non-authenticated plaintext */ +#define SYS_SERV_CMD_SNVM_WRITE_AUTH 0x11u /* authenticated plaintext */ +#define SYS_SERV_CMD_SNVM_WRITE_CIPHER 0x12u /* authenticated ciphertext */ +#define SYS_SERV_CMD_SNVM_READ 0x18u +#define SYS_SERV_CMD_PUF_EMULATION 0x20u +#define SYS_SERV_CMD_NONCE 0x21u + +/* sNVM service parameters (see Microchip system services spec) */ +#define MPFS_SNVM_MODULE_MAX 221 /* modules 0..220 */ +#define MPFS_SNVM_USK_LEN 12 /* user secret key (authenticated modes) */ +#define MPFS_SNVM_AUTH_DATA_LEN 236 /* data bytes per authenticated page */ +#define MPFS_SNVM_PLAIN_DATA_LEN 252 /* data bytes per non-authenticated page */ +#define MPFS_SNVM_ADMIN_LEN 4 /* page admin bytes returned by read */ +/* Mailbox response byte offsets (mb_offset 0) */ +#define MPFS_SNVM_READ_RET_OFFSET 16 +#define MPFS_PUF_RET_OFFSET 20 + +/* PUF emulation / nonce service sizes */ +#define MPFS_PUF_CHALLENGE_LEN 16 +#define MPFS_PUF_RESPONSE_LEN 32 +#define MPFS_NONCE_LEN 32 /* Device serial number size in bytes */ #define DEVICE_SERIAL_NUMBER_SIZE 16 /* Timeout loop iteration counts (override at build time via CFLAGS) */ #ifndef MPFS_SCB_TIMEOUT -#define MPFS_SCB_TIMEOUT 10000 /* SCB mailbox polling */ +#define MPFS_SCB_TIMEOUT 10000 /* SCB request-accept polling */ +#endif +/* Completion (BUSY) wait. TRNG and PUF services are far slower than + * serial/sNVM reads; only a wedged controller reaches this bound. */ +#ifndef MPFS_SCB_BUSY_TIMEOUT +#define MPFS_SCB_BUSY_TIMEOUT 20000000 #endif #ifndef QSPI_TIMEOUT_TRIES #define QSPI_TIMEOUT_TRIES 100000 /* QSPI controller/TX polling */ @@ -256,26 +282,40 @@ #define SCBMBOX_REG(off) (*((volatile uint32_t*)(SCBMBOX_BASE + (off)))) #define SCBMBOX_BYTE(off) (*((volatile uint8_t*)(SCBMBOX_BASE + (off)))) -/* System Controller Mailbox API */ +/* System Controller mailbox, polling mode at word offset 0. Returns 0, a + * negative transport error, or a positive 16-bit service status. */ #ifndef __ASSEMBLER__ -int mpfs_scb_service_call(uint8_t opcode, const uint8_t *mb_data, - uint32_t mb_len, uint32_t timeout); -int mpfs_scb_read_mailbox(uint8_t *out, uint32_t len); int mpfs_read_serial_number(uint8_t *serial); + +/* Read one sNVM module. data_len 236 (AUTH, needs usk) or 252 (PLAIN, usk + * may be NULL); admin is 4 bytes and optional. */ +int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, + uint8_t *data, uint16_t data_len); + +/* Write one sNVM module. data is 252 bytes for PLAIN, else 236; the 12-byte + * usk is required for AUTH/CIPHER. */ +int mpfs_snvm_write(uint8_t format, uint8_t module, const uint8_t *data, + const uint8_t *usk); + +/* PUF emulation: 16-byte challenge -> 32-byte device-unique response. */ +int mpfs_puf_emulation(const uint8_t *challenge, uint8_t op_type, + uint8_t *response); + +/* Nonce service: 32-byte random value. */ +int mpfs_nonce(uint8_t *nonce); + #endif /* __ASSEMBLER__ */ /* Crypto Engine: Athena F5200 (200 MHz) */ #define ATHENA_BASE (SYSREG_BASE + 0x125000) -/* Athena control block. Layout and bit names match athenareg_t / - * SYSREG_ATHENACR_* in config_athena.h from the Microchip user-crypto - * (CAL) library, where BASE32_ADDR_ATHENAREG is 0x20127000. */ +/* Athena control block, matching athenareg_t in the CAL library's + * config_athena.h (BASE32_ADDR_ATHENAREG = 0x20127000). */ #define ATHENA_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x00))) #define ATHENA_STALL_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x04))) #define ATHENA_UPPER_ADDRESS (*((volatile uint32_t*)(ATHENA_BASE + 0x08))) -/* Security UG Table 7-7, MSS Crypto Control Register. RESET reads 1 out of - * power-on reset; MSS_OWNER reports that the MSS, not the fabric, holds the - * core, which is set by the Libero crypto ownership mode. */ +/* Security UG Table 7-7. RESET reads 1 out of power-on reset; MSS_OWNER says + * the Libero ownership mode gave the core to the MSS rather than the fabric. */ #define ATHENA_CR_RESET (1U << 0) #define ATHENA_CR_PURGE (1U << 1) #define ATHENA_CR_GO (1U << 2) diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index 45d265a059..1d5fa54063 100644 --- a/hal/mpfs250_ddr.c +++ b/hal/mpfs250_ddr.c @@ -518,17 +518,9 @@ static void setup_segments(void) mb(); } -/* DDR controller configuration, in HSS init_ddrc() order. - * - * Values come from the Video Kit Libero header - * hart-software-services/build/boards/mpfs-video-kit/fpga_design_config/ - * ddr/hw_ddrc.h - * - * ddr_cadence_controller_setup() writes the table in array order, so row - * order is write order. That matters at CTRLR_SOFT_RESET_N below: the PHY - * training, MTC and AXI_IF blocks are programmed only after the controller - * is released from soft reset. - */ +/* DDR controller configuration in HSS init_ddrc() order, values from the Libero + * ddr/hw_ddrc.h. Row order is write order, which matters at + * CTRLR_SOFT_RESET_N: PHY training and MTC must be programmed after it. */ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { 0x2400, LIBERO_SETTING_CFG_MANUAL_ADDRESS_MAP }, { 0x2404, LIBERO_SETTING_CFG_CHIPADDR_MAP }, @@ -638,10 +630,8 @@ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { 0x3D74, LIBERO_SETTING_CFG_FAW_DLR }, { 0x3D98, LIBERO_SETTING_CFG_ADVANCE_ACTIVATE_READY }, - /* Everything above is programmed with the controller still in soft - * reset. This write releases it; HSS init_ddrc() programs the PHY - * training, MTC and AXI_IF blocks only after this point, so the order - * of the rows below is a sequencing requirement, not a preference. */ + /* Releases the controller from soft reset. Rows below are the PHY + * training, MTC and AXI_IF blocks and must follow it, not precede it. */ { MC_CTRLR_SOFT_RESET_N, LIBERO_SETTING_CTRLR_SOFT_RESET_N }, { MC_CFG_LOOKAHEAD_PCH, LIBERO_SETTING_CFG_LOOKAHEAD_PCH }, diff --git a/hal/mpfs250_snvm.c b/hal/mpfs250_snvm.c new file mode 100644 index 0000000000..aaf2531350 --- /dev/null +++ b/hal/mpfs250_snvm.c @@ -0,0 +1,486 @@ +/* mpfs250_snvm.c + * + * PolarFire SoC secure NVM (sNVM) and SRAM-PUF key material for wolfBoot: a + * keystore backend that serves the trust anchor from sNVM, a PUF-derived key + * encryption key with RFC 3394 AES key-wrap, and the image-encryption key + * provider that unwraps a PUF-wrapped AES key stored in sNVM. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include "wolfboot/wolfboot.h" +#include "keystore.h" +#include "hal.h" +#include "hal/mpfs250.h" +#include "hal/mpfs250_snvm.h" +#include "printf.h" +#include "loader.h" +#include "encrypt.h" +#include +#include +#include +#include +#include /* wc_ForceZero */ + +#if defined(SNVM_KEYSTORE_PROVISION) || defined(SNVM_ENCKEY_PROVISION) +/* Provisioning runs from hal_init() on every boot of a provisioning image, + * and the programmer reboots the board once on its own, so a page that + * already holds the content is left alone: sNVM pages have a write-cycle + * budget and a brownout mid-write can lock one read-only. */ +static int snvm_page_write_if_changed(uint8_t module, const uint8_t *data) +{ + static uint8_t cur[MPFS_SNVM_PLAIN_DATA_LEN]; + + if (mpfs_snvm_read(module, NULL, NULL, cur, sizeof(cur)) != 0) { + /* Never spend a write cycle on a page that could not be read. */ + wolfBoot_printf("snvm: module %u read failed, not written\n", + (unsigned)module); + return -1; + } + if (memcmp(cur, data, sizeof(cur)) == 0) { + wolfBoot_printf("snvm: module %u already provisioned\n", + (unsigned)module); + return 0; + } + return mpfs_snvm_write(SYS_SERV_CMD_SNVM_WRITE_PLAIN, module, data, NULL); +} +#endif + +#if defined(SNVM_KEYSTORE) && !defined(WOLFBOOT_NO_SIGN) + +static uint8_t snvm_page_cache[SNVM_KEYSTORE_PAGE_DATA]; +static uint8_t snvm_slot_cache[SIZEOF_KEYSTORE_SLOT]; + +/* Read from the logical keystore image (header + slots) spanning consecutive + * sNVM modules. 0 on success, -1 on read error or out-of-range module. */ +static int snvm_keystore_read(uint32_t offset, uint8_t *out, uint32_t len) +{ + uint32_t got = 0; + uint32_t mod, moff, n; + + while (got < len) { + mod = (uint32_t)SNVM_KEYSTORE_MODULE + + ((offset + got) / SNVM_KEYSTORE_PAGE_DATA); + moff = (offset + got) % SNVM_KEYSTORE_PAGE_DATA; + if (mod >= (uint32_t)MPFS_SNVM_MODULE_MAX) { + return -1; + } + if (mpfs_snvm_read((uint8_t)mod, NULL, NULL, snvm_page_cache, + SNVM_KEYSTORE_PAGE_DATA) != 0) { + return -1; + } + n = SNVM_KEYSTORE_PAGE_DATA - moff; + if (n > (len - got)) { + n = len - got; + } + memcpy(out + got, snvm_page_cache + moff, n); + got += n; + } + return 0; +} + +#ifdef SNVM_KEYSTORE_REQUIRE_ROM +/* The ROM flag is a bitstream property of the page (sNVM client marked ROM + * in Libero): the write service cannot set or clear it, so a page that + * carries it cannot be rewritten by anything running on the device. The + * admin word is little-endian; ROMFLAG is bit 23. */ +static int snvm_keystore_require_rom(void) +{ + static uint8_t scratch[MPFS_SNVM_PLAIN_DATA_LEN]; + uint8_t admin[MPFS_SNVM_ADMIN_LEN]; + unsigned int m; + + for (m = 0; m < SNVM_KEYSTORE_MAX_MODULES; m++) { + if (mpfs_snvm_read((uint8_t)(SNVM_KEYSTORE_MODULE + m), NULL, admin, + scratch, sizeof(scratch)) != 0) { + wolfBoot_printf("sNVM keystore module %u read failed\n", + (unsigned)(SNVM_KEYSTORE_MODULE + m)); + return -1; + } + if ((admin[2] & 0x80U) == 0U) { + wolfBoot_printf("sNVM keystore module %u is not ROM " + "(admin %02x%02x%02x%02x)\n", + (unsigned)(SNVM_KEYSTORE_MODULE + m), admin[3], admin[2], + admin[1], admin[0]); + return -1; + } + } + return 0; +} +#endif + +int keystore_num_pubkeys(void) +{ + uint8_t hdr_buf[SNVM_KEYSTORE_HDR_SIZE]; + struct snvm_keystore_hdr *hdr = (struct snvm_keystore_hdr *)hdr_buf; +#ifdef SNVM_KEYSTORE_REQUIRE_ROM + static int rom_checked; + + if (!rom_checked) { + if (snvm_keystore_require_rom() != 0) { + wolfBoot_panic(); + } + rom_checked = 1; + } +#endif + + if (snvm_keystore_read(0, hdr_buf, SNVM_KEYSTORE_HDR_SIZE) != 0) { + return 0; + } + if (memcmp(hdr->magic, SNVM_KEYSTORE_MAGIC, 8) != 0) { + /* A whole first page of zeros (still in the page cache from the + * header read) is a zeroized keystore, not a provisioning mistake: + * say so and stop, there is no trust anchor to verify with. A page + * that is merely corrupt yields no keys below. */ + if (snvm_page_cache[0] == 0 && + memcmp(snvm_page_cache, snvm_page_cache + 1, + SNVM_KEYSTORE_PAGE_DATA - 1) == 0) { + wolfBoot_printf("sNVM keystore zeroized\n"); + wolfBoot_panic(); + } + return 0; + } + if (hdr->item_count > SNVM_KEYSTORE_MAX_PUBKEYS) { + return 0; + } + return (int)hdr->item_count; +} + +/* Load slot id into the static cache; returns NULL on any error. */ +static struct keystore_slot *snvm_load_slot(int id) +{ + if ((id < 0) || (id >= keystore_num_pubkeys())) { + return (struct keystore_slot *)0; + } + if (snvm_keystore_read(SNVM_KEYSTORE_HDR_SIZE + + ((uint32_t)id * SIZEOF_KEYSTORE_SLOT), snvm_slot_cache, + SIZEOF_KEYSTORE_SLOT) != 0) { + return (struct keystore_slot *)0; + } + return (struct keystore_slot *)snvm_slot_cache; +} + +uint8_t *keystore_get_buffer(int id) +{ + struct keystore_slot *slot = snvm_load_slot(id); + if (slot == NULL) { + return (uint8_t *)0; + } + return slot->pubkey; +} + +int keystore_get_size(int id) +{ + struct keystore_slot *slot = snvm_load_slot(id); + if (slot == NULL) { + return -1; + } + /* Reject an over-large size from a corrupt/mis-provisioned slot so callers + * cannot read past the fixed-size cache. */ + if (slot->pubkey_size > KEYSTORE_PUBKEY_SIZE) { + return -1; + } + return (int)slot->pubkey_size; +} + +uint32_t keystore_get_mask(int id) +{ + struct keystore_slot *slot = snvm_load_slot(id); + if (slot == NULL) { + return 0; + } + return slot->part_id_mask; +} + +uint32_t keystore_get_key_type(int id) +{ + struct keystore_slot *slot = snvm_load_slot(id); + if (slot == NULL) { + return (uint32_t)-1; + } + return slot->key_type; +} + +#endif /* SNVM_KEYSTORE && !WOLFBOOT_NO_SIGN */ + +#if defined(SNVM_KEYSTORE_PROVISION) && !defined(WOLFBOOT_NO_SIGN) + +/* Write the compiled-in keystore into sNVM page by page. Run once; an + * SNVM_KEYSTORE build then serves the trust anchor from sNVM. Public keys only. */ +int snvm_keystore_provision(void) +{ + static uint8_t img[SNVM_KEYSTORE_MAX_MODULES * SNVM_KEYSTORE_PAGE_DATA]; + struct snvm_keystore_hdr *hdr = (struct snvm_keystore_hdr *)img; + struct keystore_slot slot; + uint8_t *pub; + uint32_t total, modules, m, off; + int n, i, sz; + + n = keystore_num_pubkeys(); + if (n <= 0) { + wolfBoot_printf("snvm provision: no compiled keys\n"); + return -1; + } + total = SNVM_KEYSTORE_HDR_SIZE + ((uint32_t)n * SIZEOF_KEYSTORE_SLOT); + if (total > sizeof(img)) { + wolfBoot_printf("snvm provision: image too large (%u)\n", + (unsigned)total); + return -1; + } + + memset(img, 0, sizeof(img)); + memcpy(hdr->magic, SNVM_KEYSTORE_MAGIC, 8); + hdr->item_count = (uint16_t)n; + hdr->flags = 0; + hdr->version = 0; + + for (i = 0; i < n; i++) { + memset(&slot, 0, sizeof(slot)); + sz = keystore_get_size(i); + pub = keystore_get_buffer(i); + if ((sz < 0) || (pub == NULL)) { + return -1; + } + slot.slot_id = (uint32_t)i; + slot.key_type = keystore_get_key_type(i); + slot.part_id_mask = keystore_get_mask(i); + slot.pubkey_size = (uint32_t)sz; + memcpy(slot.pubkey, pub, (uint32_t)sz); + memcpy(&img[SNVM_KEYSTORE_HDR_SIZE + ((uint32_t)i * SIZEOF_KEYSTORE_SLOT)], + &slot, SIZEOF_KEYSTORE_SLOT); + } + + modules = (total + SNVM_KEYSTORE_PAGE_DATA - 1u) / SNVM_KEYSTORE_PAGE_DATA; + for (m = 0; m < modules; m++) { + off = m * SNVM_KEYSTORE_PAGE_DATA; + if (snvm_page_write_if_changed((uint8_t)(SNVM_KEYSTORE_MODULE + m), + &img[off]) != 0) { + wolfBoot_printf("snvm provision: write module %u failed\n", + (unsigned)(SNVM_KEYSTORE_MODULE + m)); + return -1; + } + } + wolfBoot_printf("snvm provision: wrote %d key(s) in %u module(s)\n", n, + (unsigned)modules); + return 0; +} + +#endif /* SNVM_KEYSTORE_PROVISION && !WOLFBOOT_NO_SIGN */ + +#ifdef SNVM_KEK + +/* Fixed PUF challenge. Any constant works: the same challenge returns the + * same device-unique response across cold boots, anchoring a stable KEK. */ +static const uint8_t snvm_kek_challenge[MPFS_PUF_CHALLENGE_LEN] = { + 0x77, 0x6f, 0x6c, 0x66, 0x42, 0x6f, 0x6f, 0x74, + 0x4b, 0x45, 0x4b, 0x76, 0x31, 0x00, 0x00, 0x00 +}; +/* Domain-separation label hashed with the PUF response. */ +static const char snvm_kek_label[] = "wolfBoot-sNVM-KEK-v1"; + +int mpfs_puf_kek(uint8_t *kek) +{ + uint8_t resp[MPFS_PUF_RESPONSE_LEN]; + uint8_t digest[WC_SHA384_DIGEST_SIZE]; + wc_Sha384 sha; + int ret; + + if (kek == NULL) { + return -1; + } + ret = mpfs_puf_emulation(snvm_kek_challenge, 0, resp); + if (ret != 0) { + return -1; + } + ret = wc_InitSha384(&sha); + if (ret == 0) { + ret = wc_Sha384Update(&sha, (const byte *)snvm_kek_label, + (word32)(sizeof(snvm_kek_label) - 1)); + if (ret == 0) { + ret = wc_Sha384Update(&sha, resp, (word32)sizeof(resp)); + } + if (ret == 0) { + ret = wc_Sha384Final(&sha, digest); + } + wc_Sha384Free(&sha); + /* the context buffered the whole sub-block message, PUF response included */ + wc_ForceZero(&sha, sizeof(sha)); + } + /* KEK = first 256 bits of SHA-384(label || PUF response). */ + if (ret == 0) { + memcpy(kek, digest, SNVM_KEK_LEN); + } + wc_ForceZero(resp, sizeof(resp)); + wc_ForceZero(digest, sizeof(digest)); + return (ret == 0) ? 0 : -1; +} + +int snvm_kek_wrap(const uint8_t *key, uint32_t keysz, uint8_t *out, + uint32_t outsz) +{ + uint8_t kek[SNVM_KEK_LEN]; + int ret; + + if ((key == NULL) || (out == NULL)) { + return -1; + } + if (mpfs_puf_kek(kek) != 0) { + return -1; + } + ret = wc_AesKeyWrap(kek, (word32)sizeof(kek), key, keysz, out, outsz, NULL); + wc_ForceZero(kek, sizeof(kek)); + return ret; +} + +int snvm_kek_unwrap(const uint8_t *in, uint32_t insz, uint8_t *key, + uint32_t keysz) +{ + uint8_t kek[SNVM_KEK_LEN]; + int ret; + + if ((in == NULL) || (key == NULL)) { + return -1; + } + if (mpfs_puf_kek(kek) != 0) { + return -1; + } + ret = wc_AesKeyUnWrap(kek, (word32)sizeof(kek), in, insz, key, keysz, NULL); + wc_ForceZero(kek, sizeof(kek)); + return ret; +} + + +#endif /* SNVM_KEK */ + +#if defined(CUSTOM_ENCRYPT_KEY) && defined(SNVM_KEK) + +/* sNVM module SNVM_ENCKEY_MODULE holds: [wrapped AES key (40)][nonce (N)]. */ +#define SNVM_ENCKEY_BLOB_LEN (SNVM_KEK_WRAPPED_LEN + ENCRYPT_NONCE_SIZE) +#if SNVM_ENCKEY_BLOB_LEN > MPFS_SNVM_PLAIN_DATA_LEN +#error "wrapped key + nonce does not fit one plaintext sNVM page" +#endif + +#if (ENCRYPT_KEY_SIZE != SNVM_KEK_AESKEY_LEN) +#error "SNVM PUF encrypt-key provider expects a 256-bit (AES-256) image key" +#endif + +/* Read the wrapped AES key + nonce from sNVM, unwrap the key with the PUF KEK + * and return both. Returns 0 on success. */ +int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + uint8_t page[MPFS_SNVM_PLAIN_DATA_LEN]; + int ret; + + if ((key == NULL) || (nonce == NULL)) { + return -1; + } + /* Fail closed: a caller that ignores the return must never see stale + * or candidate key material in either output. */ + wc_ForceZero(key, ENCRYPT_KEY_SIZE); + wc_ForceZero(nonce, ENCRYPT_NONCE_SIZE); + ret = mpfs_snvm_read(SNVM_ENCKEY_MODULE, NULL, NULL, page, + MPFS_SNVM_PLAIN_DATA_LEN); + if (ret != 0) { + wolfBoot_printf("encrypt-key: sNVM read failed (%d)\n", ret); + return -1; + } + ret = snvm_kek_unwrap(page, SNVM_KEK_WRAPPED_LEN, key, ENCRYPT_KEY_SIZE); + if (ret != (int)ENCRYPT_KEY_SIZE) { + wolfBoot_printf("encrypt-key: PUF unwrap failed (%d)\n", ret); + wc_ForceZero(key, ENCRYPT_KEY_SIZE); + wc_ForceZero(page, sizeof(page)); + return -1; + } + memcpy(nonce, page + SNVM_KEK_WRAPPED_LEN, ENCRYPT_NONCE_SIZE); + wc_ForceZero(page, sizeof(page)); + return 0; +} + +/* Provisioned out-of-band, so runtime set/erase are unused by the decrypt + * path. Neither can be honoured at runtime, so both fail closed: an update + * flow that relies on them must not believe a key was stored or erased. */ +int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce) +{ + (void)key; + (void)nonce; + return -1; +} + +int wolfBoot_erase_encrypt_key(void) +{ + return -1; +} + +#ifdef SNVM_ENCKEY_PROVISION +/* One-time: wrap the AES key with the device PUF KEK and store + * [wrapped key][nonce] in sNVM. It must match the sign tool's key file. + * The key comes from snvm_enckey_prov_key/nonce, defined by the integrator in + * another object (SNVM_ENCKEY_PROVISION_EXTERN), or from the built-in public + * test vector, which needs SNVM_ENCKEY_INSECURE_TEST_KEY as acknowledgement. */ +#if defined(SNVM_ENCKEY_PROVISION_EXTERN) +extern const uint8_t snvm_enckey_prov_key[SNVM_KEK_AESKEY_LEN]; +extern const uint8_t snvm_enckey_prov_nonce[ENCRYPT_NONCE_SIZE]; +#define prov_aes_key snvm_enckey_prov_key +#define prov_aes_nonce snvm_enckey_prov_nonce +#elif !defined(SNVM_ENCKEY_INSECURE_TEST_KEY) +#error "SNVM_ENCKEY_PROVISION writes a key into sNVM permanently. Define \ +SNVM_ENCKEY_PROVISION_EXTERN and provide snvm_enckey_prov_key/nonce, or define \ +SNVM_ENCKEY_INSECURE_TEST_KEY to provision the built-in public test vector." +#else +/* Publicly known test vector. A device provisioned with this has no image + * confidentiality, and sNVM cannot be rewritten to undo it. */ +static const uint8_t prov_aes_key[SNVM_KEK_AESKEY_LEN] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f +}; +static const uint8_t prov_aes_nonce[ENCRYPT_NONCE_SIZE] = { + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f +}; +#endif + +int snvm_enckey_provision(void) +{ + uint8_t page[MPFS_SNVM_PLAIN_DATA_LEN]; + int ret, i; + + for (i = 0; i < (int)sizeof(page); i++) { + page[i] = 0; + } + ret = snvm_kek_wrap(prov_aes_key, (uint32_t)sizeof(prov_aes_key), page, + SNVM_KEK_WRAPPED_LEN); + if (ret != SNVM_KEK_WRAPPED_LEN) { + wolfBoot_printf("enckey provision: wrap failed (%d)\n", ret); + wc_ForceZero(page, sizeof(page)); + return -1; + } + memcpy(page + SNVM_KEK_WRAPPED_LEN, prov_aes_nonce, ENCRYPT_NONCE_SIZE); + ret = snvm_page_write_if_changed(SNVM_ENCKEY_MODULE, page); + wolfBoot_printf("enckey provision: sNVM write[%d] ret=%d\n", + SNVM_ENCKEY_MODULE, ret); + wc_ForceZero(page, sizeof(page)); + return ret; +} +#endif /* SNVM_ENCKEY_PROVISION */ + +#endif /* CUSTOM_ENCRYPT_KEY && SNVM_KEK */ diff --git a/hal/mpfs250_snvm.h b/hal/mpfs250_snvm.h new file mode 100644 index 0000000000..3e5bb2892e --- /dev/null +++ b/hal/mpfs250_snvm.h @@ -0,0 +1,145 @@ +/* mpfs250_snvm.h + * + * sNVM keystore layout and PUF KEK interface for hal/mpfs250_snvm.c. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifndef MPFS250_SNVM_H +#define MPFS250_SNVM_H + +#if (defined(SNVM_KEYSTORE) || defined(SNVM_KEYSTORE_PROVISION)) && \ + !defined(WOLFBOOT_NO_SIGN) + +#include "keystore.h" +#include "hal/mpfs250.h" + +#define SNVM_KEYSTORE_HDR_SIZE 16 + +#if defined(__GNUC__) + #define SNVM_KEYSTORE_PACKED __attribute__((packed)) +#else + #define SNVM_KEYSTORE_PACKED +#endif + +/* On-sNVM image: this header at logical offset 0, then item_count packed + * keystore_slot entries. Same layout family as the OTP keystore. */ +struct SNVM_KEYSTORE_PACKED snvm_keystore_hdr { + char magic[8]; + uint16_t item_count; + uint16_t flags; + uint32_t version; +}; + +static const char SNVM_KEYSTORE_MAGIC[8] = + {'W', 'O', 'L', 'F', 'B', 'O', 'O', 'T'}; + +/* First sNVM module holding the keystore image (override per board/config). + * Defaults high to leave low modules for a future bootmode-2 sNVM boot image. */ +#ifndef SNVM_KEYSTORE_MODULE +#define SNVM_KEYSTORE_MODULE 200 +#endif + +/* Modules the image may span (provisioning buffer bound). One page holds two + * ECC384 keys; an ML-DSA-87 key needs 11. Every page in the range must be + * runtime-writable (not marked ROM) in the Libero design for the on-device + * provisioning writer; a production bitstream can instead ship them as ROM + * pages, which SNVM_KEYSTORE_REQUIRE_ROM then insists on at boot. */ +#ifndef SNVM_KEYSTORE_MAX_MODULES +#define SNVM_KEYSTORE_MAX_MODULES 1 +#endif +#if (SNVM_KEYSTORE_MODULE < 0) || (SNVM_KEYSTORE_MAX_MODULES < 1) || \ + ((SNVM_KEYSTORE_MODULE + SNVM_KEYSTORE_MAX_MODULES) > MPFS_SNVM_MODULE_MAX) +#error "sNVM keystore module range must lie within modules 0..220" +#endif + +/* Plaintext page payload. Public keys are not secret; their integrity rests + * on whatever protects the wolfBoot image itself. Authenticated sNVM is a + * future option. */ +#define SNVM_KEYSTORE_PAGE_DATA MPFS_SNVM_PLAIN_DATA_LEN + +/* Upper bound on a trusted item_count, from the usable keystore capacity. + * SIZEOF_KEYSTORE_SLOT comes from keystore.h, which the user includes first. */ +#define SNVM_KEYSTORE_MAX_PUBKEYS \ + ((((SNVM_KEYSTORE_MAX_MODULES) * (SNVM_KEYSTORE_PAGE_DATA)) - \ + (SNVM_KEYSTORE_HDR_SIZE)) / (SIZEOF_KEYSTORE_SLOT)) + +#ifdef SNVM_KEYSTORE_PROVISION +/* Write the compiled-in keystore (keystore.c) into sNVM. Run once. */ +int snvm_keystore_provision(void); +#endif + +#endif /* SNVM_KEYSTORE || SNVM_KEYSTORE_PROVISION */ + +#ifdef SNVM_KEK + +#include +#include "hal/mpfs250.h" + +/* Device-unique KEK: the first 256 bits of a SHA-384 digest. */ +#define SNVM_KEK_LEN 32 + +/* AES-256 key wrapped with the PUF KEK: RFC 3394 adds 8 bytes of overhead. */ +#define SNVM_KEK_AESKEY_LEN 32 +#define SNVM_KEK_WRAPPED_LEN (SNVM_KEK_AESKEY_LEN + 8) + +/* sNVM module holding the wrapped image-encryption key (override per board). + * Must not fall inside the keystore's module range, and must be runtime-writable + * in the Libero design: on the Video Kit only pages 200 and 201 are. */ +#ifndef SNVM_ENCKEY_MODULE +#define SNVM_ENCKEY_MODULE 201 +#endif +#if (SNVM_ENCKEY_MODULE < 0) || (SNVM_ENCKEY_MODULE >= MPFS_SNVM_MODULE_MAX) +#error "SNVM_ENCKEY_MODULE is outside the sNVM module range 0..220" +#endif +#if defined(SNVM_KEYSTORE) || defined(SNVM_KEYSTORE_PROVISION) +#if (SNVM_ENCKEY_MODULE >= SNVM_KEYSTORE_MODULE) && \ + (SNVM_ENCKEY_MODULE < (SNVM_KEYSTORE_MODULE + SNVM_KEYSTORE_MAX_MODULES)) +#error "SNVM_ENCKEY_MODULE overlaps the sNVM keystore module range" +#endif +#endif + +/* Derive the 256-bit device-unique KEK from the System Controller SRAM-PUF: + * KEK = SHA384(label || PUF(fixed-challenge))[0..31]. Returns 0 on success. */ +int mpfs_puf_kek(uint8_t *kek); + +/* AES key-wrap (RFC 3394) using the PUF KEK. out needs keysz+8 bytes. + * Returns the wrapped length (keysz+8) on success, < 0 on error. */ +int snvm_kek_wrap(const uint8_t *key, uint32_t keysz, uint8_t *out, + uint32_t outsz); + +/* AES key-unwrap using the PUF KEK. key needs insz-8 bytes. + * Returns the unwrapped length (insz-8) on success, < 0 on error. */ +int snvm_kek_unwrap(const uint8_t *in, uint32_t insz, uint8_t *key, + uint32_t keysz); + + +#ifdef SNVM_ENCKEY_PROVISION +/* Provided by hal/mpfs250_snvm.c: wrap the compiled-in AES key with + * the PUF KEK and store [wrapped key][nonce] in sNVM. */ +int snvm_enckey_provision(void); +#endif + +#endif /* SNVM_KEK */ + +#if defined(SNVM_ENCKEY_PROVISION) && !defined(SNVM_KEK) +#error "SNVM_ENCKEY_PROVISION needs SNVM_KEK (the key is wrapped with the PUF KEK)" +#endif + +#endif /* MPFS250_SNVM_H */ diff --git a/options.mk b/options.mk index 99730c64af..16d3c402e1 100644 --- a/options.mk +++ b/options.mk @@ -77,6 +77,89 @@ ifeq ($(FLASH_OTP_KEYSTORE),1) CFLAGS+=-D"FLASH_OTP_KEYSTORE" endif +# PolarFire SoC: trust anchor in secure NVM (sNVM). SNVM_KEYSTORE serves keys at runtime. +ifeq ($(SNVM_KEYSTORE),1) + CFLAGS+=-D"SNVM_KEYSTORE" +endif +# Shared refusal text for every knob below that writes sNVM. +SNVM_WRITE_GATE_MSG=writes sNVM: irreversible, and a brownout mid-write can lock the page \ + read-only. Re-run with WOLFBOOT_SNVM_WRITE_APPROVED=1 to confirm +# sNVM page numbers, overridable per board; the headers range-check them. +ifneq ($(SNVM_KEYSTORE_MODULE),) + CFLAGS+=-DSNVM_KEYSTORE_MODULE=$(SNVM_KEYSTORE_MODULE) +endif +ifneq ($(SNVM_KEYSTORE_MAX_MODULES),) + CFLAGS+=-DSNVM_KEYSTORE_MAX_MODULES=$(SNVM_KEYSTORE_MAX_MODULES) +endif +ifneq ($(SNVM_ENCKEY_MODULE),) + CFLAGS+=-DSNVM_ENCKEY_MODULE=$(SNVM_ENCKEY_MODULE) +endif +ifeq ($(SNVM_KEYSTORE_PROVISION),1) + ifeq ($(SNVM_KEYSTORE),1) + $(error SNVM_KEYSTORE_PROVISION writes the compiled-in keystore to sNVM \ + and cannot be combined with SNVM_KEYSTORE=1, which serves keys \ + from sNVM instead of compiling them in.) + endif + ifneq ($(WOLFBOOT_SNVM_WRITE_APPROVED),1) + $(error SNVM_KEYSTORE_PROVISION $(SNVM_WRITE_GATE_MSG)) + endif + CFLAGS+=-D"SNVM_KEYSTORE_PROVISION" +endif + +# Production hardening: REQUIRE_ROM needs every keystore page ROM-flagged (bitstream only), and +# MPFS_SCB_SMODE_DENY PMP-fences the System Controller mailbox off S-mode and drops its OS nodes. +ifeq ($(SNVM_KEYSTORE_REQUIRE_ROM),1) + ifneq ($(SNVM_KEYSTORE),1) + $(error SNVM_KEYSTORE_REQUIRE_ROM=1 needs SNVM_KEYSTORE=1) + endif + CFLAGS+=-D"SNVM_KEYSTORE_REQUIRE_ROM" +endif +ifeq ($(MPFS_SCB_SMODE_DENY),1) + # The fence is installed at the M->S hand-off, so an S-mode build under HSS cannot claim it. + ifeq ($(WOLFBOOT_TARGET_BUILD),1) + ifneq ($(RISCV_MMODE),1) + $(error MPFS_SCB_SMODE_DENY=1 needs the M-mode target (RISCV_MMODE=1)) + endif + endif + CFLAGS+=-D"MPFS_SCB_SMODE_DENY" -DWOLFBOOT_PMP_DENY_BASE=0x37020000UL \ + -DWOLFBOOT_PMP_DENY_SIZE=0x1000UL +endif + +# PUF-derived KEK + RFC 3394 AES key-wrap for sNVM-stored keys. +ifeq ($(SNVM_KEK),1) + # The KEK only wraps the image-encryption key, so it rides on the AES-256 ENCRYPT build's AES. + ifneq ($(ENCRYPT)$(ENCRYPT_WITH_AES256)$(CUSTOM_ENCRYPT_KEY),111) + $(error SNVM_KEK=1 requires ENCRYPT=1 ENCRYPT_WITH_AES256=1 \ + CUSTOM_ENCRYPT_KEY=1) + endif + CFLAGS+=-D"SNVM_KEK" + CFLAGS+=-DHAVE_AES_KEYWRAP -DWOLFSSL_AES_DIRECT -DHAVE_AES_ECB \ + -DHAVE_AES_DECRYPT + # The KEK derivation is SHA-384 whatever the image hash is. + ifneq ($(HASH),SHA384) + AUX_HASH_ALGOS+=sha384 + endif +endif +# One-time writer for the PUF-wrapped image-encryption key into sNVM. +ifeq ($(SNVM_ENCKEY_PROVISION),1) + ifneq ($(SNVM_KEK)$(ENCRYPT)$(CUSTOM_ENCRYPT_KEY),111) + $(error SNVM_ENCKEY_PROVISION requires SNVM_KEK=1 ENCRYPT=1 \ + CUSTOM_ENCRYPT_KEY=1, which build the provider it calls.) + endif + ifneq ($(WOLFBOOT_SNVM_WRITE_APPROVED),1) + $(error SNVM_ENCKEY_PROVISION $(SNVM_WRITE_GATE_MSG)) + endif + CFLAGS+=-D"SNVM_ENCKEY_PROVISION" + # Second acknowledgement: the built-in key is a public test vector, so no confidentiality. + ifeq ($(SNVM_ENCKEY_INSECURE_TEST_KEY),1) + CFLAGS+=-D"SNVM_ENCKEY_INSECURE_TEST_KEY" + endif + # Production path: the integrator's object defines snvm_enckey_prov_key/_nonce. + ifeq ($(SNVM_ENCKEY_PROVISION_EXTERN),1) + CFLAGS+=-D"SNVM_ENCKEY_PROVISION_EXTERN" + endif +endif + ifeq ($(WOLFBOOT_TEST_FILLER),1) CFLAGS+=-D"WOLFBOOT_TEST_FILLER" endif @@ -1643,11 +1726,8 @@ ifneq (,$(filter RISCV RISCV64,$(ARCH))) CFLAGS+=-DSTACK_SIZE_PER_HART=$(STACK_SIZE_PER_HART) endif -# L2 scratchpad region size and boot-hart stack size, substituted into the -# linker script. The scratchpad is capped by the number of scratchpad ways -# the startup asm populates (4 ways x 128 KB on MPFS250); the linker script -# asserts the limit. Defaults keep the pre-existing layout for targets that -# do not override them. +# Substituted into the linker script. The scratchpad is capped at the 4 x 128 KB +# of ways the startup asm populates; the linker script asserts it. WOLFBOOT_L2SCRATCH_SIZE ?= 256k STACK_SIZE ?= 32k diff --git a/src/boot_riscv.c b/src/boot_riscv.c index 4a03fbe5f1..234d0daf6c 100644 --- a/src/boot_riscv.c +++ b/src/boot_riscv.c @@ -391,11 +391,27 @@ int WEAKFUNCTION hal_dts_fixup(void* dts_addr, uint32_t capacity) #endif #ifdef WOLFBOOT_RISCV_MMODE -/* Configure PMP entry 0: NAPOT full address space, RWX, for S-mode access */ +/* Configure PMP for S-mode access: NAPOT full address space, RWX. With + * WOLFBOOT_PMP_DENY_BASE/SIZE the window is carved out first (entries match + * lowest-numbered first), so S-mode cannot touch it; M-mode is unaffected. */ static void setup_pmp_for_smode(void) { +#if defined(WOLFBOOT_PMP_DENY_BASE) && defined(WOLFBOOT_PMP_DENY_SIZE) + unsigned long lo = (unsigned long)WOLFBOOT_PMP_DENY_BASE >> 2; + unsigned long hi = ((unsigned long)WOLFBOOT_PMP_DENY_BASE + + (unsigned long)WOLFBOOT_PMP_DENY_SIZE) >> 2; + /* entry 0: TOR below the window, RWX; entry 1: TOR the window, no + * access; entry 2: NAPOT everything else, RWX */ + unsigned long cfg = (0x1FUL << 16) | (0x08UL << 8) | 0x0FUL; + + csr_write(pmpaddr0, lo); + csr_write(pmpaddr1, hi); + csr_write(pmpaddr2, -1UL); + csr_write(pmpcfg0, cfg); +#else csr_write(pmpaddr0, -1UL); /* all-ones = cover entire address space (NAPOT) */ csr_write(pmpcfg0, 0x1F); /* A=NAPOT(3), R=1, W=1, X=1 */ +#endif __asm__ volatile("sfence.vma" ::: "memory"); } diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 0093aa1f98..650cbb7032 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -67,28 +67,12 @@ _reset: sw zero, 0x174(t1) /* SYSREG_L2_SHUTDOWN_CR = 0 */ fence - /* Populate the scratchpad ways one way at a time, so every line of - * the Zero Device (0x0A000000) is resident before anything else runs. - * - * With the default mask (0xFF = cache ways 0-7), a store to the Zero - * Device allocates in an ordinary cache way; that line is evictable and - * its writeback is discarded, so the store is silently lost. Selecting - * all four scratchpad ways at once (0xF00) is not enough either: it only - * covers the stores made while the mask is set, which used to be the - * .text copy alone. Everything after the mask was restored -- .data, - * .bss (including the SDHCI PDMA staging buffer), the heap and the E51 - * stack -- still allocated in evictable ways. - * - * Walking one way at a time is what makes the whole 512 KB resident: a - * store can only allocate in the single enabled way, so 2048 blocks x 4 - * ways fills each way exactly once. Once resident, later stores hit and - * the mask no longer matters. This is HSS config_l2_cache(), see - * mss_l2_cache.c; the marker value matches its INIT_MARKER. - * - * Constants for WAY_ENABLE=0x0B, NUM_SCRATCH_PAD_WAYS=4: - * first way = 1 << ((WAY_ENABLE + 1) - NUM_SCRATCH_PAD_WAYS) = 1 << 8 - * blocks per way = WAY_BYTE_LENGTH / CACHE_BLOCK_BYTE_LENGTH - * = 0x20000 / 64 = 2048 */ + /* Make all 512 KB of the Zero Device resident by enabling one scratchpad + * way at a time and storing a marker every 64 bytes: a store can only + * allocate in the enabled way, so 2048 blocks x 4 ways fills each once. + * A mask of 0xF00 covers only the stores made while it is set, leaving + * .data/.bss/heap/stack in evictable ways whose writeback is discarded. + * This is HSS config_l2_cache(); the marker matches its INIT_MARKER. */ li t0, 0x02010828 /* L2_WAY_MASK_E51_DCACHE */ li t1, 0x100 /* scratchpad way 8 */ li t2, 0x0A000000 /* ZERO_DEVICE_BOTTOM */ diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 51e18ff245..b3d6b862c2 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -1944,8 +1944,8 @@ int wolfBoot_fallback_is_possible(void) #include "enckey_data.h" #endif -#if !defined(EXT_FLASH) && !defined(MMU) - #error option EXT_ENCRYPTED requires EXT_FLASH or MMU mode +#if !defined(EXT_FLASH) && !defined(MMU) && !defined(CUSTOM_ENCRYPT_KEY) + #error option EXT_ENCRYPTED requires EXT_FLASH, MMU, or CUSTOM_ENCRYPT_KEY #endif #ifndef WOLFBOOT_ENCRYPT_CACHE @@ -1960,7 +1960,7 @@ int wolfBoot_fallback_is_possible(void) #define ENCRYPT_CACHE (WOLFBOOT_ENCRYPT_CACHE) #endif -#if defined(EXT_ENCRYPTED) && defined(MMU) +#if defined(EXT_ENCRYPTED) && defined(MMU) && !defined(CUSTOM_ENCRYPT_KEY) static uint8_t ENCRYPT_KEY[ENCRYPT_KEY_SIZE + ENCRYPT_NONCE_SIZE]; #endif @@ -2006,6 +2006,9 @@ void RAMFUNCTION wolfBoot_crypto_set_iv(const uint8_t *nonce, uint32_t iv_counte } #endif /* EXT_ENCRYPTED && (__WOLFBOOT || UNIT_TEST || MMU) */ +#ifndef CUSTOM_ENCRYPT_KEY +/* Under CUSTOM_ENCRYPT_KEY the platform supplies the key, so the + * partition-resident storage below (needs WOLFBOOT_PARTITION_*) is dropped. */ static int RAMFUNCTION hal_set_key(const uint8_t *k, const uint8_t *nonce) { #ifdef WOLFBOOT_RENESAS_TSIP @@ -2095,7 +2098,6 @@ static int RAMFUNCTION hal_set_key(const uint8_t *k, const uint8_t *nonce) return ret; #endif } -#ifndef CUSTOM_ENCRYPT_KEY /** * @brief Set the encryption key. * diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index f0c0fbf9c1..ced8bc4dec 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -104,6 +104,8 @@ TESTS+=unit-flash-erase-u3 TESTS+=unit-flash-erase-kinetis TESTS+=unit-flash-m2354 TESTS+=unit-otp-keystore +TESTS+=unit-snvm-keystore +TESTS+=unit-snvm-kek TESTS+=unit-otp-keystore-gen-zeroize TESTS+=unit-x86-paging-oob TESTS+=unit-ahci-unlock-panic @@ -794,6 +796,28 @@ unit-flash-m2354: unit-flash-m2354.c ../../hal/m2354.c ../../hal/m2354.h unit-otp-keystore: unit-otp-keystore.c ../../src/flash_otp_keystore.c gcc -o $@ unit-otp-keystore.c $(CFLAGS) $(LDFLAGS) +# unit-snvm-keystore includes hal/mpfs250_snvm.c directly and mocks +# mpfs_snvm_read, so it is not a separate input. -I../.. resolves +# hal/mpfs250.h, which mpfs250_snvm.h includes. Four modules so the +# page-spanning reads are exercised. +unit-snvm-keystore: unit-snvm-keystore.c ../../hal/mpfs250_snvm.c + gcc -o $@ unit-snvm-keystore.c -I../.. -DSNVM_KEYSTORE_MAX_MODULES=4 \ + $(CFLAGS) $(LDFLAGS) + +# unit-snvm-kek includes hal/mpfs250_snvm.c (KEK and encryption-key provider) +# directly with the PUF and sNVM services mocked. SHA-384 and AES key-wrap +# come from wolfCrypt, built from source like the image tests. +unit-snvm-kek: unit-snvm-kek.c ../../hal/mpfs250_snvm.c + gcc -o $@ unit-snvm-kek.c -I../.. \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/aes.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha512.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/memory.c \ + -DSNVM_KEK -DWOLFBOOT_ENABLE_HASH_SHA384 -DWOLFSSL_AES_256 \ + -DHAVE_AES_KEYWRAP -DWOLFSSL_AES_DIRECT -DHAVE_AES_ECB \ + -DHAVE_AES_DECRYPT -DCUSTOM_ENCRYPT_KEY -DEXT_ENCRYPTED \ + -DENCRYPT_WITH_AES256 -DSNVM_ENCKEY_PROVISION \ + -DSNVM_ENCKEY_INSECURE_TEST_KEY $(CFLAGS) $(LDFLAGS) + # unit-otp-keystore-gen-zeroize includes the host tool # tools/keytools/otp/otp-keystore-gen.c directly (via #define main) and # interposes read()/malloc()/free() (dlsym RTLD_NEXT, hence -ldl) to observe diff --git a/tools/unit-tests/unit-snvm-kek.c b/tools/unit-tests/unit-snvm-kek.c new file mode 100644 index 0000000000..17cb8f1880 --- /dev/null +++ b/tools/unit-tests/unit-snvm-kek.c @@ -0,0 +1,286 @@ +/* unit-snvm-kek.c + * + * Host unit test for the PolarFire SoC PUF-derived KEK, the RFC 3394 key + * wrap around it, and the sNVM-backed image-encryption-key provider. The + * System Controller services are mocked: the PUF response is a deterministic + * function of the challenge and a per-"device" seed, and sNVM is an array. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +#include "hal/mpfs250.h" + +static uint8_t mock_puf_seed; +static int mock_puf_fail; +static int mock_puf_calls; +static uint8_t mock_snvm[MPFS_SNVM_MODULE_MAX][MPFS_SNVM_PLAIN_DATA_LEN]; +static int mock_snvm_read_fail; +static int mock_snvm_writes; + +int mpfs_puf_emulation(const uint8_t *challenge, uint8_t op_type, + uint8_t *response) +{ + int i; + + (void)op_type; + mock_puf_calls++; + if (mock_puf_fail) + return -1; + for (i = 0; i < MPFS_PUF_RESPONSE_LEN; i++) { + response[i] = (uint8_t)(challenge[i % MPFS_PUF_CHALLENGE_LEN] ^ + mock_puf_seed ^ (uint8_t)(i * 7)); + } + return 0; +} + +int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, + uint8_t *data, uint16_t data_len) +{ + (void)usk; (void)admin; + if (mock_snvm_read_fail || module >= MPFS_SNVM_MODULE_MAX || + data_len > MPFS_SNVM_PLAIN_DATA_LEN) + return -1; + memcpy(data, mock_snvm[module], data_len); + return 0; +} + +int mpfs_snvm_write(uint8_t format, uint8_t module, const uint8_t *data, + const uint8_t *usk) +{ + (void)format; (void)usk; + mock_snvm_writes++; + if (module >= MPFS_SNVM_MODULE_MAX) + return -1; + memcpy(mock_snvm[module], data, MPFS_SNVM_PLAIN_DATA_LEN); + return 0; +} + +#include "../../hal/mpfs250_snvm.c" + +static void reset_mocks(void) +{ + mock_puf_seed = 0x5A; + mock_puf_fail = 0; + mock_puf_calls = 0; + mock_snvm_read_fail = 0; + mock_snvm_writes = 0; + memset(mock_snvm, 0, sizeof(mock_snvm)); +} + +static void fill(uint8_t *buf, uint32_t len, uint8_t base) +{ + uint32_t i; + for (i = 0; i < len; i++) + buf[i] = (uint8_t)(base + i); +} + +START_TEST(test_kek_deterministic_per_device) +{ + uint8_t k1[SNVM_KEK_LEN], k2[SNVM_KEK_LEN], k3[SNVM_KEK_LEN]; + + reset_mocks(); + ck_assert_int_eq(mpfs_puf_kek(k1), 0); + ck_assert_int_eq(mpfs_puf_kek(k2), 0); + ck_assert_int_eq(memcmp(k1, k2, SNVM_KEK_LEN), 0); + ck_assert_int_eq(mock_puf_calls, 2); + + /* Another device (different PUF) must derive a different KEK. */ + mock_puf_seed = 0xA5; + ck_assert_int_eq(mpfs_puf_kek(k3), 0); + ck_assert_int_ne(memcmp(k1, k3, SNVM_KEK_LEN), 0); +} +END_TEST + +START_TEST(test_kek_guards) +{ + uint8_t kek[SNVM_KEK_LEN]; + uint8_t key[SNVM_KEK_AESKEY_LEN], out[SNVM_KEK_WRAPPED_LEN]; + + reset_mocks(); + ck_assert_int_eq(mpfs_puf_kek(NULL), -1); + ck_assert_int_eq(snvm_kek_wrap(NULL, sizeof(key), out, sizeof(out)), -1); + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), NULL, sizeof(out)), -1); + ck_assert_int_eq(snvm_kek_unwrap(NULL, sizeof(out), key, sizeof(key)), -1); + ck_assert_int_eq(snvm_kek_unwrap(out, sizeof(out), NULL, sizeof(key)), -1); + + mock_puf_fail = 1; + ck_assert_int_eq(mpfs_puf_kek(kek), -1); + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), out, sizeof(out)), -1); + ck_assert_int_eq(snvm_kek_unwrap(out, sizeof(out), key, sizeof(key)), -1); +} +END_TEST + +START_TEST(test_wrap_unwrap_roundtrip) +{ + uint8_t key[SNVM_KEK_AESKEY_LEN], back[SNVM_KEK_AESKEY_LEN]; + uint8_t wrapped[SNVM_KEK_WRAPPED_LEN], wrapped2[SNVM_KEK_WRAPPED_LEN]; + + reset_mocks(); + fill(key, sizeof(key), 0xA0); + memset(back, 0, sizeof(back)); + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), wrapped, sizeof(wrapped)), + SNVM_KEK_WRAPPED_LEN); + ck_assert_int_ne(memcmp(wrapped, key, sizeof(key)), 0); + /* Deterministic: same device, same key, same blob. */ + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), wrapped2, + sizeof(wrapped2)), SNVM_KEK_WRAPPED_LEN); + ck_assert_int_eq(memcmp(wrapped, wrapped2, sizeof(wrapped)), 0); + + ck_assert_int_eq(snvm_kek_unwrap(wrapped, sizeof(wrapped), back, + sizeof(back)), SNVM_KEK_AESKEY_LEN); + ck_assert_int_eq(memcmp(key, back, sizeof(key)), 0); +} +END_TEST + +START_TEST(test_unwrap_rejects_tamper_and_foreign_device) +{ + uint8_t key[SNVM_KEK_AESKEY_LEN], back[SNVM_KEK_AESKEY_LEN]; + uint8_t wrapped[SNVM_KEK_WRAPPED_LEN]; + + reset_mocks(); + fill(key, sizeof(key), 0x10); + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), wrapped, sizeof(wrapped)), + SNVM_KEK_WRAPPED_LEN); + + /* A blob wrapped on one device does not unwrap on another. */ + mock_puf_seed = 0x3C; + ck_assert_int_ne(snvm_kek_unwrap(wrapped, sizeof(wrapped), back, + sizeof(back)), SNVM_KEK_AESKEY_LEN); + mock_puf_seed = 0x5A; + + /* The RFC 3394 integrity check catches a flipped bit. */ + wrapped[SNVM_KEK_WRAPPED_LEN - 1] ^= 0x01; + ck_assert_int_ne(snvm_kek_unwrap(wrapped, sizeof(wrapped), back, + sizeof(back)), SNVM_KEK_AESKEY_LEN); + + /* Too short an output buffer is refused rather than truncated. */ + wrapped[SNVM_KEK_WRAPPED_LEN - 1] ^= 0x01; + ck_assert_int_ne(snvm_kek_unwrap(wrapped, sizeof(wrapped), back, 16), + SNVM_KEK_AESKEY_LEN); +} +END_TEST + +START_TEST(test_provision_then_get_encrypt_key) +{ + uint8_t key[ENCRYPT_KEY_SIZE], nonce[ENCRYPT_NONCE_SIZE]; + uint8_t expect_key[ENCRYPT_KEY_SIZE], expect_nonce[ENCRYPT_NONCE_SIZE]; + + reset_mocks(); + fill(expect_key, sizeof(expect_key), 0x00); + fill(expect_nonce, sizeof(expect_nonce), 0x20); + + ck_assert_int_eq(snvm_enckey_provision(), 0); + ck_assert_int_eq(mock_snvm_writes, 1); + /* A second run finds the page already holding the content and does not + * spend another write cycle. */ + ck_assert_int_eq(snvm_enckey_provision(), 0); + ck_assert_int_eq(mock_snvm_writes, 1); + /* The page holds the wrapped key, never the key itself. */ + ck_assert_int_ne(memcmp(mock_snvm[SNVM_ENCKEY_MODULE], expect_key, + sizeof(expect_key)), 0); + ck_assert_int_eq(memcmp(mock_snvm[SNVM_ENCKEY_MODULE] + + SNVM_KEK_WRAPPED_LEN, expect_nonce, sizeof(expect_nonce)), 0); + + memset(key, 0, sizeof(key)); + memset(nonce, 0, sizeof(nonce)); + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), 0); + ck_assert_int_eq(memcmp(key, expect_key, sizeof(key)), 0); + ck_assert_int_eq(memcmp(nonce, expect_nonce, sizeof(nonce)), 0); +} +END_TEST + +START_TEST(test_get_encrypt_key_failures) +{ + uint8_t key[ENCRYPT_KEY_SIZE], nonce[ENCRYPT_NONCE_SIZE]; + + reset_mocks(); + ck_assert_int_eq(snvm_enckey_provision(), 0); + + ck_assert_int_eq(wolfBoot_get_encrypt_key(NULL, nonce), -1); + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, NULL), -1); + + mock_snvm_read_fail = 1; + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), -1); + mock_snvm_read_fail = 0; + + /* Blob from another device: unwrap fails, no key is returned. */ + mock_puf_seed = 0xC3; + memset(key, 0, sizeof(key)); + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), -1); + mock_puf_seed = 0x5A; + + /* Corrupted page: integrity check fails. */ + mock_snvm[SNVM_ENCKEY_MODULE][3] ^= 0x80; + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), -1); + mock_snvm[SNVM_ENCKEY_MODULE][3] ^= 0x80; + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), 0); + + /* PUF service down: fail closed. */ + mock_puf_fail = 1; + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), -1); +} +END_TEST + +START_TEST(test_set_erase_fail_closed) +{ + uint8_t key[ENCRYPT_KEY_SIZE], nonce[ENCRYPT_NONCE_SIZE]; + + reset_mocks(); + memset(key, 0x11, sizeof(key)); + memset(nonce, 0x22, sizeof(nonce)); + ck_assert_int_eq(wolfBoot_set_encrypt_key(key, nonce), -1); + ck_assert_int_eq(wolfBoot_erase_encrypt_key(), -1); + ck_assert_int_eq(mock_snvm_writes, 0); +} +END_TEST + +Suite *snvm_kek_suite(void) +{ + Suite *s = suite_create("snvm-kek"); + TCase *tc = tcase_create("snvm-kek"); + + tcase_add_test(tc, test_kek_deterministic_per_device); + tcase_add_test(tc, test_kek_guards); + tcase_add_test(tc, test_wrap_unwrap_roundtrip); + tcase_add_test(tc, test_unwrap_rejects_tamper_and_foreign_device); + tcase_add_test(tc, test_provision_then_get_encrypt_key); + tcase_add_test(tc, test_get_encrypt_key_failures); + tcase_add_test(tc, test_set_erase_fail_closed); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = snvm_kek_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} diff --git a/tools/unit-tests/unit-snvm-keystore.c b/tools/unit-tests/unit-snvm-keystore.c new file mode 100644 index 0000000000..f915e2f853 --- /dev/null +++ b/tools/unit-tests/unit-snvm-keystore.c @@ -0,0 +1,328 @@ +/* unit-snvm-keystore.c + * + * Host unit tests for the PolarFire SoC sNVM keystore backend. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +#define SNVM_KEYSTORE +#define KEYSTORE_PUBKEY_SIZE 64 /* model an ECC-256 keystore slot */ + +#include "keystore.h" +#include "hal/mpfs250.h" +#include "hal/mpfs250_snvm.h" + +/* Mock sNVM: one page per module, plus a failure injector. */ +static uint8_t mock_snvm[MPFS_SNVM_MODULE_MAX][MPFS_SNVM_PLAIN_DATA_LEN]; +static int mock_snvm_fail_module = -1; /* module whose read returns -1 */ +static int mock_snvm_reads; + +int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, + uint8_t *data, uint16_t len) +{ + (void)usk; (void)admin; + mock_snvm_reads++; + if (module >= MPFS_SNVM_MODULE_MAX) + return -1; + if ((int)module == mock_snvm_fail_module) + return -1; + if (len > MPFS_SNVM_PLAIN_DATA_LEN) + return -1; + memcpy(data, mock_snvm[module], len); + return 0; +} + +#include "../../hal/mpfs250_snvm.c" + +#define PAGE SNVM_KEYSTORE_PAGE_DATA + +/* Fill the mock modules with a byte pattern that encodes its logical offset, + * so a mis-computed module/offset shows up as wrong data rather than luck. */ +static void setup_pattern(void) +{ + uint32_t m, i; + + memset(mock_snvm, 0, sizeof(mock_snvm)); + mock_snvm_fail_module = -1; + mock_snvm_reads = 0; + for (m = 0; m < SNVM_KEYSTORE_MAX_MODULES; m++) { + for (i = 0; i < PAGE; i++) { + mock_snvm[SNVM_KEYSTORE_MODULE + m][i] = + (uint8_t)((m * PAGE + i) & 0xFF); + } + } +} + +static void expect_pattern(const uint8_t *buf, uint32_t off, uint32_t len) +{ + uint32_t i; + for (i = 0; i < len; i++) + ck_assert_uint_eq(buf[i], (uint8_t)((off + i) & 0xFF)); +} + +/* A read wholly inside the first page touches one module. */ +START_TEST(test_read_within_one_page) +{ + uint8_t buf[16]; + setup_pattern(); + ck_assert_int_eq(snvm_keystore_read(0, buf, sizeof(buf)), 0); + expect_pattern(buf, 0, sizeof(buf)); + ck_assert_int_eq(mock_snvm_reads, 1); +} +END_TEST + +/* A read straddling the page boundary takes the tail of one module and the head + * of the next -- the offset arithmetic the OTP sibling never has to do. */ +START_TEST(test_read_spans_two_pages) +{ + uint8_t buf[8]; + setup_pattern(); + ck_assert_int_eq(snvm_keystore_read(PAGE - 2, buf, sizeof(buf)), 0); + expect_pattern(buf, PAGE - 2, sizeof(buf)); + ck_assert_int_eq(mock_snvm_reads, 2); +} +END_TEST + +/* Spanning three modules exercises the loop rather than a single carry. */ +START_TEST(test_read_spans_three_pages) +{ + uint8_t buf[PAGE + 10]; + setup_pattern(); + ck_assert_int_eq(snvm_keystore_read(PAGE - 5, buf, sizeof(buf)), 0); + expect_pattern(buf, PAGE - 5, sizeof(buf)); + ck_assert_int_eq(mock_snvm_reads, 3); +} +END_TEST + +/* Starting exactly on a boundary must land at offset 0 of the next module, + * not offset PAGE of the previous one. */ +START_TEST(test_read_at_exact_boundary) +{ + uint8_t buf[4]; + setup_pattern(); + ck_assert_int_eq(snvm_keystore_read(PAGE, buf, sizeof(buf)), 0); + expect_pattern(buf, PAGE, sizeof(buf)); + ck_assert_int_eq(mock_snvm_reads, 1); +} +END_TEST + +/* Running off the end of the sNVM module range is refused, not wrapped. */ +START_TEST(test_read_past_module_max) +{ + uint8_t buf[8]; + uint32_t off; + setup_pattern(); + off = (uint32_t)(MPFS_SNVM_MODULE_MAX - SNVM_KEYSTORE_MODULE) * PAGE; + ck_assert_int_eq(snvm_keystore_read(off, buf, sizeof(buf)), -1); +} +END_TEST + +/* An sNVM read error propagates instead of leaving stale cache contents. */ +START_TEST(test_read_error_propagates) +{ + uint8_t buf[8]; + setup_pattern(); + mock_snvm_fail_module = SNVM_KEYSTORE_MODULE; + ck_assert_int_eq(snvm_keystore_read(0, buf, sizeof(buf)), -1); +} +END_TEST + +/* Provision slot `id` in the mock image, after the header. */ +static void setup_slot(int id, uint32_t pubkey_size, uint32_t key_type, + uint32_t mask) +{ + struct keystore_slot slot; + uint32_t off = SNVM_KEYSTORE_HDR_SIZE + + ((uint32_t)id * SIZEOF_KEYSTORE_SLOT); + uint32_t m = off / PAGE, moff = off % PAGE; + uint8_t *p = (uint8_t *)&slot; + uint32_t i; + + memset(&slot, 0, sizeof(slot)); + slot.slot_id = id; + slot.key_type = key_type; + slot.part_id_mask = mask; + slot.pubkey_size = pubkey_size; + for (i = 0; i < SIZEOF_KEYSTORE_SLOT; i++) { + mock_snvm[SNVM_KEYSTORE_MODULE + m + ((moff + i) / PAGE)] + [(moff + i) % PAGE] = p[i]; + } +} + +/* Provision the mock header with n slots. */ +static void setup_hdr(int n) +{ + struct snvm_keystore_hdr hdr; + setup_pattern(); + memset(&hdr, 0, sizeof(hdr)); + memcpy(hdr.magic, SNVM_KEYSTORE_MAGIC, 8); + hdr.item_count = (uint16_t)n; + memcpy(mock_snvm[SNVM_KEYSTORE_MODULE], &hdr, sizeof(hdr)); +} + +START_TEST(test_num_pubkeys_valid) +{ + setup_hdr(3); + ck_assert_int_eq(keystore_num_pubkeys(), 3); +} +END_TEST + +START_TEST(test_num_pubkeys_bad_magic) +{ + setup_hdr(3); + mock_snvm[SNVM_KEYSTORE_MODULE][0] = 'X'; + ck_assert_int_eq(keystore_num_pubkeys(), 0); + ck_assert_int_eq(wolfBoot_panicked, 0); +} +END_TEST + +/* A zeroized page is reported and halts; a merely corrupt one just yields + * no keys. */ +START_TEST(test_num_pubkeys_zeroized_halts) +{ + setup_hdr(3); + memset(mock_snvm[SNVM_KEYSTORE_MODULE], 0, PAGE); + wolfBoot_panicked = 0; + ck_assert_int_eq(keystore_num_pubkeys(), 0); + ck_assert_int_eq(wolfBoot_panicked, 1); + wolfBoot_panicked = 0; +} +END_TEST + +/* A corrupt header must not drive slot indexing past the keystore; the OTP + * sibling bounds item_count the same way. */ +START_TEST(test_num_pubkeys_over_max) +{ + setup_hdr(SNVM_KEYSTORE_MAX_PUBKEYS + 1); + ck_assert_int_eq(keystore_num_pubkeys(), 0); +} +END_TEST + +START_TEST(test_num_pubkeys_at_max) +{ + setup_hdr(SNVM_KEYSTORE_MAX_PUBKEYS); + ck_assert_int_eq(keystore_num_pubkeys(), SNVM_KEYSTORE_MAX_PUBKEYS); +} +END_TEST + +/* A correctly provisioned slot reports its real size. */ +START_TEST(test_get_size_valid) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE, 1, 0xFFFFFFFF); + ck_assert_int_eq(keystore_get_size(0), KEYSTORE_PUBKEY_SIZE); +} +END_TEST + +/* A corrupt slot must not report a size past the fixed slot cache, which + * callers use as a hash length and coordinate offset. */ +START_TEST(test_get_size_oversize_rejected) +{ + setup_hdr(1); + setup_slot(0, 2 * KEYSTORE_PUBKEY_SIZE, 1, 0xFFFFFFFF); + ck_assert_int_eq(keystore_get_size(0), -1); +} +END_TEST + +START_TEST(test_get_size_just_over_rejected) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE + 1, 1, 0xFFFFFFFF); + ck_assert_int_eq(keystore_get_size(0), -1); +} +END_TEST + +/* An out-of-range id yields the documented not-found values, not a slot. */ +START_TEST(test_accessors_reject_bad_id) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE, 1, 0xFFFFFFFF); + ck_assert_ptr_eq(keystore_get_buffer(5), NULL); + ck_assert_int_eq(keystore_get_size(5), -1); + ck_assert_uint_eq(keystore_get_mask(5), 0); + ck_assert_uint_eq(keystore_get_key_type(5), (uint32_t)-1); +} +END_TEST + +/* key_type and mask come back as provisioned. */ +START_TEST(test_get_key_type_and_mask) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE, 7, 0x0000A5A5); + ck_assert_uint_eq(keystore_get_key_type(0), 7); + ck_assert_uint_eq(keystore_get_mask(0), 0x0000A5A5); +} +END_TEST + +/* An sNVM failure while loading the slot propagates through every accessor + * rather than handing back the previous slot left in the cache. */ +START_TEST(test_accessors_propagate_read_error) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE, 1, 0xFFFFFFFF); + mock_snvm_fail_module = SNVM_KEYSTORE_MODULE; + ck_assert_ptr_eq(keystore_get_buffer(0), NULL); + ck_assert_int_eq(keystore_get_size(0), -1); +} +END_TEST + +Suite *snvm_keystore_suite(void) +{ + Suite *s = suite_create("snvm-keystore"); + TCase *tc = tcase_create("snvm-keystore"); + + tcase_add_test(tc, test_read_within_one_page); + tcase_add_test(tc, test_read_spans_two_pages); + tcase_add_test(tc, test_read_spans_three_pages); + tcase_add_test(tc, test_read_at_exact_boundary); + tcase_add_test(tc, test_read_past_module_max); + tcase_add_test(tc, test_read_error_propagates); + tcase_add_test(tc, test_num_pubkeys_valid); + tcase_add_test(tc, test_num_pubkeys_bad_magic); + tcase_add_test(tc, test_num_pubkeys_zeroized_halts); + tcase_add_test(tc, test_num_pubkeys_over_max); + tcase_add_test(tc, test_num_pubkeys_at_max); + tcase_add_test(tc, test_get_size_valid); + tcase_add_test(tc, test_get_size_oversize_rejected); + tcase_add_test(tc, test_get_size_just_over_rejected); + tcase_add_test(tc, test_accessors_reject_bad_id); + tcase_add_test(tc, test_get_key_type_and_mask); + tcase_add_test(tc, test_accessors_propagate_read_error); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = snvm_keystore_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} From df2437f7590833b9a194cde4dd8b57670d35bda1 Mon Sep 17 00:00:00 2001 From: David Garske Date: Mon, 5 Oct 2026 16:04:51 -0700 Subject: [PATCH 04/23] PolarFire SoC: read DDR once after init so CPU writes land, drop the workarounds --- config/examples/polarfire_mpfs250_m.config | 17 -- docs/Targets.md | 28 ++- hal/mpfs250.c | 187 +-------------------- hal/mpfs250.h | 4 +- hal/mpfs250_ddr.c | 14 ++ src/fdt.c | 5 +- src/image.c | 15 -- 7 files changed, 34 insertions(+), 236 deletions(-) diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index 41a361f63a..faf4e143cb 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -36,8 +36,6 @@ SPMATHALL?=1 DUALBANK_SWAP?=0 PKA?=0 ENCRYPT=0 -# Decrypt via a staging buffer: see the SDHCI_BLOCK_VIA_PDMA note above. -DISK_DECRYPT_STAGING=1 WOLFTPM?=0 ELF?=1 #DEBUG_ELF?=1 @@ -129,21 +127,6 @@ CFLAGS_EXTRA+=-DBOOT_PART_B=1 # Speed up disk partition read (512KB chunks - max DMA size) CFLAGS_EXTRA+=-DDISK_BLOCK_SIZE=0x80000 -# Disable SDMA on the Cadence SD4HC. SDMA hangs silently at first -# multi-block read on the Video Kit (Cadence boundary-cross bug). -# Use PIO single-block reads instead. -CFLAGS_EXTRA+=-DSDHCI_SDMA_DISABLED - -# Force single-block (CMD17) reads. Multi-block PIO suffers a BRR -# race on Arasan/Cadence-family controllers; single-block avoids it. -CFLAGS_EXTRA+=-DSDHCI_FORCE_SINGLE_BLOCK_READ - -# Disk-load via PDMA staging: SDHCI PIO into an L2 Scratch staging buffer, -# then mpfs_pdma_memcpy() lands each block in DDR through the PDMA master. -# A CPU copy into DDR reads back wrong depending on access order and code -# layout (cause not identified); the PDMA path has never failed. -CFLAGS_EXTRA+=-DSDHCI_BLOCK_VIA_PDMA - # Video Kit routes the SD slot's Card Detect (CD#) signal through the FPGA # fabric rather than MSSIO, so the SDHCI controller's hardware CI/CDPL # detection always reads 'no card' in M-mode (no fabric configuration). diff --git a/docs/Targets.md b/docs/Targets.md index 3da9f8a209..c65acc0ee4 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -1667,19 +1667,19 @@ Boot time measurements on PolarFire SoC (RISC-V 64-bit U54 @ 625 MHz) for a 19MB | ECC384 | SHA384 | ~800 ms | ~2900 ms | ~1500 ms | ~70 ms | ~5.3 seconds | | ML-DSA 87 | SHA256 | ~835 ms | ~2900 ms | ~2100 ms | ~22 ms | ~5.9 seconds | -Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit for the same 19.7 MB FIT, plaintext, ECC384/SHA384, with the DDR training and SD-card load included: +Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit with host-side timestamps on the console, plaintext FIT, ECC384/SHA384, software crypto, the same 19.7 MB kernel either gzip-compressed in the FIT (5.9 MB) or stored uncompressed (19.8 MB): -| Phase | Software crypto | Athena offload | -|-----------------------------------------------|-----------------|----------------| -| DDR training, SD init, GPT read | ~1.5 s | ~1.5 s | -| SD load of the FIT (CMD17 single block + PDMA staging) | ~15.5 s | ~15.5 s | -| SHA384 integrity (reads through the non-cached alias) | ~4.1 s | ~4.8 s | -| ECC384 signature verify | ~0.7 s | ~0.7 s | -| FIT kernel copy to its load address (PDMA + read-back verify) | ~9.1 s | ~9.1 s | -| M-mode -> S-mode handoff | ~31 s | ~32 s | -| Linux login prompt | ~53-62 s | ~53 s | +| Phase | gzip FIT | uncompressed FIT | +|-----------------------------------------------|----------|------------------| +| DDR training, SD init, GPT read, FIT load, SHA384 integrity, ECC384 verify | ~2 s | ~4 s | +| gzip inflate of the kernel | ~16 s | - | +| FIT copies, DTB fixups | <1 s | <1 s | +| M-mode -> S-mode handoff | ~18 s | ~5 s | +| Linux login prompt | ~41 s | ~28 s | -The offload does not shorten the integrity check on this path because the time is in reading the image through the non-cached DDR alias, not in the hashing; measured on a buffer in L2 scratch the Athena SHA384 is about 1.3x the software rate and AES-256-CTR about 4.4x. The SD read and the PDMA copy with its byte-wise verify are the dominant costs; both exist because of the CPU-write-to-DDR coherence workaround (`SDHCI_BLOCK_VIA_PDMA`). ML-DSA-87 on this target (`SIGN=ML_DSA` on `polarfire_mpfs250_m.config`) links to a smaller image than ECC384 because no big-number code is needed. +The inflate runs on the E51 at about 1.2 MB/s of output, so a compressed kernel costs far more than the extra card read it saves; for a short boot, build the FIT with `compression = "none"` for the kernel (`FIT_KERNEL_COMP_ALG = "none"` in Yocto). The software SHA384 runs at about 11 MB/s on the E51 and the ECC384 verify is well under a second; neither is a lever. `BOOT_BENCHMARK` phase prints on this target are in real time now that the timer uses the live CPU clock. + +The LPDDR4 controller on this board needs the first CPU-side transaction after DDR initialization to be a read: when a write comes first on a cold boot, it lands in DDR displaced by several bus beats and every later cached-port write in that boot follows it. `mpfs_ddr_init()` therefore reads one block through the non-cached alias before anything is written, after which CPU, L2 write-back and SDMA writes land exactly and the SD load, the FIT copies, the DTB fixups and the image hash use DDR directly. Measured on a buffer in L2 scratch the Athena SHA384 is about 1.3x the software rate and AES-256-CTR about 4.4x. ML-DSA-87 on this target (`SIGN=ML_DSA` on `polarfire_mpfs250_m.config`) links to a smaller image than ECC384 because no big-number code is needed. ### PolarFire Soc Debugging @@ -1931,10 +1931,6 @@ wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partiti - `src/libwolfboot.c`: the `EXT_ENCRYPTED requires EXT_FLASH or MMU` guard now also accepts `CUSTOM_ENCRYPT_KEY` (the platform supplies the key), and `hal_set_key()` (partition-resident key storage) is folded under `#ifndef CUSTOM_ENCRYPT_KEY`. - `src/update_disk.c`: the `DISK_ENCRYPT` path gains `ForceZero` for secret cleanup. -##### PolarFire M-mode decrypt note - -The image is loaded into DDR by the SD controller and decrypted there. CPU stores into that region are not coherent with what the controller wrote (which is why the load uses `SDHCI_BLOCK_VIA_PDMA`), so `polarfire_mpfs250_m.config` sets `DISK_DECRYPT_STAGING=1`: the ciphertext is read through the non-cached DDR alias, decrypted a chunk at a time into a staging buffer and landed with the same PDMA copy the load used. - #### Build flags summary | Flag | Effect | @@ -1974,7 +1970,7 @@ make CROSS_COMPILE=riscv64-unknown-elf- LIBERO_FPGA_CONFIG_DIR=0xCx and flushes - * L2. Chunked + WDT-petted for kernel-sized copies. */ -int wolfBoot_fit_memcpy(void *dst, const void *src, uint32_t len) -{ - uintptr_t d = (uintptr_t)dst; - uintptr_t s = (uintptr_t)src; - volatile const uint8_t *ncd; - const uint8_t *ncs; - uint32_t off = 0; - uint32_t chunk; - uint32_t k; - int retry; - int mism; - int rc = 0; - - if ((s & 0xF0000000UL) == 0x80000000UL) { - s |= 0x40000000UL; /* non-cached source alias */ - } - while (off < len) { - chunk = len - off; - if (chunk > (1024U * 1024U)) { - chunk = 1024U * 1024U; - } - /* mpfs_pdma_memcpy always returns 0, so the read-back verify below is - * the authoritative success check for this chunk. The PDMA->DDR write - * intermittently drops a block, so re-PDMA on a mismatch (same pattern - * as sdhci_platform_block_copy). A DDR destination (0x8xxxxxxx) is - * read back through its non-cached alias (| 0x40000000) so we compare - * what actually landed in DDR, not stale L2; this makes the caller's - * fail-closed rc real for the signature-uncovered kernel/dtb copies. - * A non-DDR destination (e.g. an L2 scratch buffer) lands directly, so - * a single copy suffices. */ - mism = 1; - for (retry = 0; retry < 8 && mism != 0; retry++) { - (void)mpfs_pdma_memcpy((void *)(d + off), - (const void *)(s + off), chunk); - /* Refresh all five MSS watchdogs (they always count and reset the - * chip and cannot be disabled) during the multi-MB kernel copy - * and its read-back verify. */ - MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_1_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_2_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_3_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_4_BASE) = 0xDEADC0DEU; - if ((d & 0xF0000000UL) != 0x80000000UL) { - mism = 0; /* non-DDR dst lands on the first copy */ - break; - } - __asm__ volatile("fence iorw,iorw" ::: "memory"); - ncd = (volatile const uint8_t *)((d + off) | 0x40000000UL); - ncs = (const uint8_t *)(s + off); - mism = 0; - for (k = 0; k < chunk; k++) { - if (ncd[k] != ncs[k]) { - mism = 1; - break; - } - } - } - if (mism != 0) { - /* Copy could not be verified within the retry budget; remember the - * failure so the caller fails closed rather than boot corrupt, - * no-longer-signature-covered data. */ - rc = -1; - } - off += chunk; - } - return rc; -} - -/* L2 round-trip wrapper around mpfs_dts_fixup_inplace(). The dtb lives in DDR - * (WOLFBOOT_LOAD_DTS_ADDRESS) but CPU writes to DDR do not land here, so copy - * it (non-cached read) into an L2 scratch buffer, run the FDT fixups there - * (CPU L2 writes work), then PDMA the result back to DDR. */ -int hal_dts_fixup(void* dts_addr, uint32_t capacity) -{ - static uint8_t l2_dtb[64 * 1024] __attribute__((aligned(8))); - fdt_ctx ctx; - const uint8_t *ddr_nc; - uint32_t sz; - int ret; - - if (dts_addr == NULL) { - return -1; - } - ddr_nc = (const uint8_t *)((uintptr_t)dts_addr | 0x40000000UL); - /* The source is bounded by whichever is smaller: the caller's DDR - * window, or what the L2 scratch buffer can hold once the fixup - * headroom is set aside. fdt_open() enforces it, so the memcpy below - * cannot overrun l2_dtb however corrupt the header is. */ - sz = (uint32_t)(sizeof(l2_dtb) - WOLFBOOT_FDT_FIXUP_HEADROOM); - if (capacity < sz) { - sz = capacity; - } - if (fdt_open(&ctx, (void *)ddr_nc, sz) != 0) { - wolfBoot_printf("FDT: invalid header at %p\n", dts_addr); - return -1; - } - sz = fdt_size(&ctx); - /* DDR (non-cached) -> L2 */ - memcpy(l2_dtb, ddr_nc, sz); - /* fixup in the CPU-writable L2 buffer, which may use the whole of it */ - ret = mpfs_dts_fixup_inplace(l2_dtb, (uint32_t)sizeof(l2_dtb)); - /* L2 -> DDR via PDMA (expanded totalsize) */ - if (fdt_open(&ctx, l2_dtb, (uint32_t)sizeof(l2_dtb)) != 0) { - wolfBoot_printf("FDT: fixed-up dtb rejected\n"); - return -1; - } - if (wolfBoot_fit_memcpy(dts_addr, l2_dtb, fdt_size(&ctx)) != 0) { - wolfBoot_printf("FDT: dtb copy-back to DDR failed\n"); - return -1; - } - return ret; -} -#else -/* Without the M-mode DDR constraints the dtb buffer is CPU-writable, so - * run the fixups directly in place (the original behavior, kept so - * FDT-enabled non-DDR builds do not silently fall back to the weak - * no-op hal_dts_fixup). */ +/* Overrides the weak no-op hal_dts_fixup so FDT-enabled builds get the + * fixups above. */ int hal_dts_fixup(void* dts_addr, uint32_t capacity) { if (dts_addr == NULL) { @@ -1639,7 +1503,6 @@ int hal_dts_fixup(void* dts_addr, uint32_t capacity) } return mpfs_dts_fixup_inplace(dts_addr, capacity); } -#endif /* WOLFBOOT_RISCV_MMODE && MPFS_DDR_INIT */ void hal_prepare_boot(void) { @@ -2669,8 +2532,7 @@ static void mpfs_mpu_init_mmc(void) } #endif /* MPFS_DDR_INIT */ -#ifdef SDHCI_BLOCK_VIA_PDMA -/* Pet all five MSS watchdogs during the (long) per-block SDHCI read loop. +/* Pet all five MSS watchdogs during the SDHCI read loops. * Overrides the weak no-op in src/sdhci.c. The MSS watchdogs always count * and reset the chip at timeout and cannot be disabled, so the multi-second * load of a large image must keep refreshing them. */ @@ -2683,49 +2545,6 @@ void sdhci_platform_wdt_pet(void) MSS_WDT_REFRESH(MSS_WDT_U54_4_BASE) = 0xDEADC0DEU; } -/* Copy a staged SDHCI block to its final destination (overrides the weak - * memcpy default in src/sdhci.c). Direct CPU writes to DDR do not land on - * this board, so a DDR destination (0x8xxxxxxx) is written through the PDMA - * master and verified via its non-cached alias (| 0x40000000), re-PDMA'ing on - * a drop (the PDMA->DDR write intermittently drops a block when interleaved - * with SDHCI reads). A non-DDR destination (L2 header/GPT buffers) is a plain - * CPU copy, which lands. Returns 0 on success, -1 if a DDR write cannot be - * verified within the retry budget. */ -int sdhci_platform_block_copy(void *dst, const void *src, uint32_t len) -{ - volatile const uint8_t *ncv; - const uint8_t *s = (const uint8_t *)src; - int retry; - int mism; - uint32_t k; - - if (((uintptr_t)dst & 0xF0000000UL) != 0x80000000UL) { - memcpy(dst, src, len); - return 0; - } - ncv = (volatile const uint8_t *)((uintptr_t)dst | 0x40000000UL); - mism = 1; - for (retry = 0; retry < 8 && mism != 0; retry++) { - /* The read-back verify below is the authoritative success check, so - * a PDMA-engine error is caught there and retried like any drop. */ - (void)mpfs_pdma_memcpy(dst, src, len); - sdhci_platform_wdt_pet(); - __asm__ volatile("fence iorw,iorw" ::: "memory"); - mism = 0; - for (k = 0; k < len; k++) { - if (ncv[k] != s[k]) { - mism = 1; - break; - } - } - } - if (mism != 0) { - return -1; - } - return 0; -} -#endif /* SDHCI_BLOCK_VIA_PDMA */ - void sdhci_platform_init(void) { /* IOMUX/MSSIO routing was already programmed in nwc_init() before the diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 48ae21598b..5dd9c6869e 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -378,9 +378,11 @@ int mpfs_nonce(uint8_t *nonce); #define RTC_CLOCK_FREQ 1000000UL /* In M-mode CLINT MTIME is not running without HSS; use mcycle (CPU clock) instead. + * The E51 clock is raised from the reset rate to the Libero PLL rate during + * init, so the rate is the live value, not MSS_CPU_CLK. * In S-mode MTIME runs at 1 MHz (default RISCV_SMODE_TIMER_FREQ). */ #if defined(WOLFBOOT_RISCV_MMODE) && !defined(RISCV_SMODE_TIMER_FREQ) -#define RISCV_SMODE_TIMER_FREQ MSS_CPU_CLK +#define RISCV_SMODE_TIMER_FREQ (mpfs_cpu_freq_mhz * 1000000UL) #endif /* Hart Local Storage (HLS) - per-hart communication structure, 64 bytes at top of stack */ diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index 1d5fa54063..c25c7c9b1a 100644 --- a/hal/mpfs250_ddr.c +++ b/hal/mpfs250_ddr.c @@ -3050,6 +3050,9 @@ static int mpfs_ddr_post_initialization(void) /* Main DDR Initialization Entry Point */ int mpfs_ddr_init(unsigned int outer_retry) { + volatile uint8_t *prime_src = (volatile uint8_t *)0xC0000000UL; + uint32_t prime_sink = 0; + uint32_t prime; int ret; wolfBoot_printf("\n========================================\n"); @@ -3356,6 +3359,17 @@ int mpfs_ddr_init(unsigned int outer_retry) * underlying address-decoder issue is still affecting writes. */ mpfs_clear_bootup_cache_ways(0xC0000000UL, 1UL * 1024UL * 1024UL); + /* The first transaction the CPU side sends to the controller after init + * must be a read. On a cold boot a write first lands displaced by + * several bus beats and every later cached-port write in that boot + * follows it; a non-cached read of one block before any write leaves + * the port clean. The PDMA fill above goes through the switch port + * and does not count. */ + for (prime = 0; prime < 512U; prime++) { + prime_sink += prime_src[prime]; + } + __asm__ volatile("fence iorw,iorw" ::: "memory"); + (void)prime_sink; wolfBoot_printf("DDR: Initialization COMPLETE\n"); diff --git a/src/fdt.c b/src/fdt.c index 2ebd4da3f7..4640dd0a89 100644 --- a/src/fdt.c +++ b/src/fdt.c @@ -1748,9 +1748,8 @@ int fit_load_ramdisk(fdt_ctx* ctx, const char* ramdisk_node, fdt_ctx* dts) #endif /* WOLFBOOT_FIT_RAMDISK */ /* Weak copy hook for FIT subimages (kernel/dtb). Default is a plain memcpy; - * boards where CPU writes to the load destination do not land (e.g. the - * PolarFire MPFS250 DDR, where cached writes thrash L2 Scratch) override this - * with a DMA-based copy (see hal/mpfs250.c). Returns 0 on success or a + * boards where CPU writes to the load destination do not land override this + * with a DMA-based copy (see hal/imx95_a55.c). Returns 0 on success or a * negative value if the copy failed, so callers can fail closed rather than * run on a partially written destination. */ int __attribute__((weak)) wolfBoot_fit_memcpy(void *dst, const void *src, diff --git a/src/image.c b/src/image.c index 03056e1b41..745767abd5 100644 --- a/src/image.c +++ b/src/image.c @@ -1079,19 +1079,6 @@ static uint8_t *get_sha_block(struct wolfBoot_image *img, uint32_t offset) } #endif p = (uint8_t *)(img->fw_base + offset); -#if defined(MPFS_DDR_INIT) - /* PolarFire SoC DDR build: route in-DDR image-body reads through the - * non-cached DDR SEG window (0xC0000000 base) so cache fills don't evict - * L2 Scratch lines (where wolfBoot's own code/stack live). PDMA already - * L2-flushed the cached writes at disk-load, so the non-cached side reads - * the correct DDR contents. 0x8xxxxxxx -> 0xCxxxxxxx. Applied at this - * single point so every image-body hasher (SHA256/384/3-384) behaves - * identically; inert for the L2-Scratch QSPI M-mode build (fw_base is not - * in the 0x8xxxxxxx window). */ - if (((uintptr_t)p & 0xF0000000UL) == 0x80000000UL) { - p = (uint8_t *)((uintptr_t)p | 0x40000000UL); - } -#endif return p; } @@ -1375,8 +1362,6 @@ static int image_sha384(struct wolfBoot_image *img, uint8_t *hash) blksz = WOLFBOOT_SHA_BLOCK_SIZE; if (position + blksz > img->fw_size) blksz = img->fw_size - position; - /* p is already routed to the non-cached DDR alias by - * get_sha_block() under MPFS_DDR_INIT (see above). */ wc_Sha384Update(&sha384_ctx, p, blksz); position += blksz; wolfBoot_watchdog_feed(); From af8ae43c2df67e45b2c885703f4d888b18d375e4 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 8 Oct 2026 08:35:20 -0700 Subject: [PATCH 05/23] PolarFire SoC: a FIT ramdisk that must load, and failover actions before the golden boot --- docs/Targets.md | 5 + docs/compile.md | 2 + hal/mpfs250_snvm.c | 40 ++ include/hal.h | 14 + options.mk | 15 + src/fdt.c | 5 +- src/update_disk.c | 185 +++++++- src/update_ram.c | 7 +- tools/unit-tests/Makefile | 7 +- tools/unit-tests/unit-update-disk-failover.c | 460 +++++++++++++++++++ 10 files changed, 715 insertions(+), 25 deletions(-) create mode 100644 tools/unit-tests/unit-update-disk-failover.c diff --git a/docs/Targets.md b/docs/Targets.md index c65acc0ee4..48eaca13bd 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -1931,6 +1931,10 @@ wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partiti - `src/libwolfboot.c`: the `EXT_ENCRYPTED requires EXT_FLASH or MMU` guard now also accepts `CUSTOM_ENCRYPT_KEY` (the platform supplies the key), and `hal_set_key()` (partition-resident key storage) is folded under `#ifndef CUSTOM_ENCRYPT_KEY`. - `src/update_disk.c`: the `DISK_ENCRYPT` path gains `ForceZero` for secret cleanup. +#### Field-recoverable zeroize + +`DISK_FAILOVER_ZEROIZE=1` (with `DISK_GOLDEN_SLOT=1`, `SNVM_KEK=1` and the `WOLFBOOT_SNVM_WRITE_APPROVED=1` gate every sNVM writer carries) turns a failover to the golden slot into a zeroize: when both operational slots are given up on, wolfBoot zeroes sNVM page `SNVM_ENCKEY_MODULE` (the PUF-wrapped image key, read back and checked; a page that already reads as zeros is not written again) and the head of both operational slots, then boots the golden image. The keystore page is never touched, so the golden image still verifies; it must be a plaintext signed image, since the key that decrypted the operational images is gone. Nothing derived from the PUF is lost: the MRAM log key is re-derived on every boot. Re-provisioning the image key afterwards is the `SNVM_ENCKEY_PROVISION` service flow; a brownout during the zeroize write can leave the page read-only, in which case the device cannot be re-keyed. + #### Build flags summary | Flag | Effect | @@ -1943,6 +1947,7 @@ wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partiti | `WOLFBOOT_SNVM_WRITE_APPROVED` | Required by every knob that writes sNVM; the build fails without it | | `SNVM_KEYSTORE_REQUIRE_ROM` | Refuse to boot unless every keystore page is ROM-flagged (bitstream-provisioned keystore) | | `MPFS_SCB_SMODE_DENY` | PMP-fence S-mode off the System Controller mailbox and disable its DTB nodes | +| `DISK_FAILOVER_ZEROIZE` | Zero the image-key page and the operational slots when falling back to the golden image | | `SNVM_ENCKEY_INSECURE_TEST_KEY` | Acknowledges provisioning the built-in public test key | | `SNVM_ENCKEY_PROVISION_EXTERN` | Provision a production key instead: the integrator defines `snvm_enckey_prov_key[32]` and `snvm_enckey_prov_nonce[16]` in another object | diff --git a/docs/compile.md b/docs/compile.md index 16254790ef..5e8ca9ea56 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -386,6 +386,8 @@ The magic is load-bearing rather than decorative. A freshly imaged partition tai 3. Whatever comes up clears it to **`success`**. 4. If the next boot still finds **`testing`**, that slot did not come up. wolfBoot skips it and boots the other slot. +With `DISK_GOLDEN_SLOT=1`, `DISK_FAILOVER_ZEROIZE=1` runs `hal_disk_failover(cause)` once, just before the golden slot is booted because both operational slots were given up on (no image, verification failure, unconfirmed boots, rollback, or a missing image key). The default hook erases the image key (`wolfBoot_erase_encrypt_key`) and zeroes the head of both operational slots so neither can be elected again; a platform overrides it for its own actions. The golden image must then be a plaintext signed image: with `DISK_ENCRYPT` the golden slot is verified but not decrypted, and a missing key marks the operational slots unbootable instead of stopping the boot. + **A slot is only ever put on probation if an update was staged into it.** A slot that is `new`, or already `success`, is booted without anything being written, so a device that never stages an update is never probated and a steady-state boot performs no writes at all. This mirrors `src/update_ram.c`, which also promotes only `updating`. The consequence is deliberate: enabling this cannot strand a system whose OS does not confirm, but an integrator who writes an image with plain `dd` and never marks the slot gets no protection either. Skipping an unconfirmed slot writes nothing. Its version is dropped from the election in memory, which also removes it from the anti-rollback ceiling, and that is what lets an older confirmed slot boot. **Anti-rollback itself is not relaxed**: a slot that merely fails verification keeps its version and still blocks an older slot, exactly as before. diff --git a/hal/mpfs250_snvm.c b/hal/mpfs250_snvm.c index aaf2531350..541522fcd2 100644 --- a/hal/mpfs250_snvm.c +++ b/hal/mpfs250_snvm.c @@ -424,10 +424,50 @@ int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce) return -1; } +#ifdef DISK_FAILOVER_ZEROIZE +/* Zero the sNVM page holding the wrapped image key, read it back. A page + * that already reads as zeros is left alone: sNVM write cycles are finite + * and a brownout during a write can lock the page read-only. */ +int wolfBoot_erase_encrypt_key(void) +{ + static uint8_t page[MPFS_SNVM_PLAIN_DATA_LEN]; + static const uint8_t zeros[MPFS_SNVM_PLAIN_DATA_LEN]; + int ret; + + ret = mpfs_snvm_read(SNVM_ENCKEY_MODULE, NULL, NULL, page, sizeof(page)); + if (ret != 0) { + wolfBoot_printf("failover: image key page %u read failed (%d)\n", + (unsigned)SNVM_ENCKEY_MODULE, ret); + return -1; + } + if (memcmp(page, zeros, sizeof(page)) == 0) { + wolfBoot_printf("failover: image key page %u already zero\n", + (unsigned)SNVM_ENCKEY_MODULE); + return 0; + } + ret = mpfs_snvm_write(SYS_SERV_CMD_SNVM_WRITE_PLAIN, SNVM_ENCKEY_MODULE, + zeros, NULL); + if (ret != 0) { + wolfBoot_printf("failover: image key page %u write failed (%d)\n", + (unsigned)SNVM_ENCKEY_MODULE, ret); + return -1; + } + ret = mpfs_snvm_read(SNVM_ENCKEY_MODULE, NULL, NULL, page, sizeof(page)); + if ((ret != 0) || (memcmp(page, zeros, sizeof(page)) != 0)) { + wolfBoot_printf("failover: image key page %u did not read back as " + "zeros\n", (unsigned)SNVM_ENCKEY_MODULE); + return -1; + } + wolfBoot_printf("failover: image key page %u zeroized\n", + (unsigned)SNVM_ENCKEY_MODULE); + return 0; +} +#else int wolfBoot_erase_encrypt_key(void) { return -1; } +#endif #ifdef SNVM_ENCKEY_PROVISION /* One-time: wrap the AES key with the device PUF KEK and store diff --git a/include/hal.h b/include/hal.h index 52094a5d01..6bccb4cc06 100644 --- a/include/hal.h +++ b/include/hal.h @@ -152,8 +152,22 @@ int hal_dma_set_noncached(uintptr_t start, uintptr_t end); * cannot be silently disabled by adding an fdt sub-image. Runs while the * boot disk is still open. Weak default is a no-op returning 0. */ int hal_boot_slot_select(void); + #endif +#ifdef DISK_FAILOVER_ZEROIZE +/* Disk boot: why the operational slots are being given up on, handed to + * hal_disk_failover() just before the golden slot is booted. */ +enum disk_failover_cause { + DISK_FAILOVER_NO_IMAGE = 1, /* no readable, parseable image */ + DISK_FAILOVER_VERIFY, /* integrity or signature failure */ + DISK_FAILOVER_UNCONFIRMED, /* boot confirmation never arrived */ + DISK_FAILOVER_ROLLBACK, /* only older images left */ + DISK_FAILOVER_NO_KEY /* the image key could not be obtained */ +}; +int hal_disk_failover(int cause); +#endif /* DISK_FAILOVER_ZEROIZE */ + #if defined(MMU) || defined(WOLFBOOT_FDT) /* Apply this target's device tree fixups in place, bounded by `capacity` * (the DTS staging window). Returns 0, or negative when the tree handed diff --git a/options.mk b/options.mk index 16d3c402e1..96d962fc65 100644 --- a/options.mk +++ b/options.mk @@ -125,6 +125,21 @@ ifeq ($(MPFS_SCB_SMODE_DENY),1) -DWOLFBOOT_PMP_DENY_SIZE=0x1000UL endif +# Field recovery: before the golden slot boots, zero the sNVM image-key page and the head of both +# operational slots (hal_disk_failover). The golden image must be a plaintext signed image. +ifeq ($(DISK_FAILOVER_ZEROIZE),1) + ifneq ($(DISK_GOLDEN_SLOT)$(SNVM_KEK),11) + $(error DISK_FAILOVER_ZEROIZE=1 requires DISK_GOLDEN_SLOT=1 and SNVM_KEK=1) + endif + ifeq ($(SNVM_ENCKEY_PROVISION),1) + $(error DISK_FAILOVER_ZEROIZE=1 and SNVM_ENCKEY_PROVISION=1 undo each \ + other at every boot; build one or the other) + endif + ifneq ($(WOLFBOOT_SNVM_WRITE_APPROVED),1) + $(error DISK_FAILOVER_ZEROIZE $(SNVM_WRITE_GATE_MSG)) + endif + CFLAGS+=-D"DISK_FAILOVER_ZEROIZE" +endif # PUF-derived KEK + RFC 3394 AES key-wrap for sNVM-stored keys. ifeq ($(SNVM_KEK),1) # The KEK only wraps the image-encryption key, so it rides on the AES-256 ENCRYPT build's AES. diff --git a/src/fdt.c b/src/fdt.c index 4640dd0a89..fc6a5245d1 100644 --- a/src/fdt.c +++ b/src/fdt.c @@ -1700,9 +1700,8 @@ const char* fit_get_compatible(fdt_ctx* ctx, const char* image) * DTB context for the initrd fixup, or NULL to skip the fixup. * * Returns 0 on success, -1 if the ramdisk node was found but the - * load failed. The current callers ignore the return value - * (log-and-continue), so a missing/failed ramdisk does not abort - * the boot. */ + * load failed; the callers treat that as fatal, since a FIT that names + * a ramdisk has no other root filesystem to offer the kernel. */ int fit_load_ramdisk(fdt_ctx* ctx, const char* ramdisk_node, fdt_ctx* dts) { int rd_size = 0; diff --git a/src/update_disk.c b/src/update_disk.c index c567406d10..997d6d0419 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -154,6 +154,35 @@ static uint8_t disk_encrypt_nonce[ENCRYPT_NONCE_SIZE]; #define SLOT_IS_GOLDEN(s) 0 #endif +/* With the failover zeroize the image key may already be gone when the + * loader runs, and the golden image is a plaintext one; without it every + * slot is encrypted and a missing key is fatal as before. */ +#ifdef DISK_FAILOVER_ZEROIZE +#ifndef DISK_GOLDEN_SLOT +#error "DISK_FAILOVER_ZEROIZE needs DISK_GOLDEN_SLOT" +#endif +/* The last reason an operational slot was passed over (enum + * disk_failover_cause in hal.h), reported to hal_disk_failover(). */ +static int failover_cause; +#define SET_FAILOVER(c) do { failover_cause = (c); } while (0) +/* Keeps a cause an earlier slot pass already recorded. */ +#define SET_FAILOVER_IF_UNSET(c) do { if (failover_cause == 0) { \ + failover_cause = (c); } } while (0) +static int disk_key_ok = 1; +#define SLOT_ENCRYPTED_KEY_OK() (disk_key_ok != 0) +#define SLOT_ENCRYPTED(s) (!SLOT_IS_GOLDEN(s)) +/* Bytes zeroed at the start of each operational slot: the manifest header + * and the signature go, so the image can never be elected again. */ +#ifndef DISK_FAILOVER_ERASE_SIZE +#define DISK_FAILOVER_ERASE_SIZE IMAGE_HEADER_SIZE +#endif +#else +#define SET_FAILOVER(c) do { } while (0) +#define SET_FAILOVER_IF_UNSET(c) do { } while (0) +#define SLOT_ENCRYPTED_KEY_OK() 1 +#define SLOT_ENCRYPTED(s) 1 +#endif + #ifndef DISK_BLOCK_SIZE #define DISK_BLOCK_SIZE 512 #endif @@ -368,6 +397,8 @@ static uint8_t disk_trailer[DISK_TRAILER_SZ]; /* Last known state of each slot, filled in by disk_boot_state_reap(). */ static uint8_t slot_state[DISK_SLOTS]; +/* Slots disk_boot_state_reap() took out of the election. */ +static uint8_t slot_reaped[DISK_SLOTS]; /* Byte offset of the trailer within the slot's partition, or -1 when the slot * cannot carry one. */ @@ -434,6 +465,7 @@ static int slot_state_write(struct boot_slot *s, uint8_t state, return 0; } + /* Read both slots' states, and drop any slot still marked TESTING out of the * election. Such a slot was armed before the previous boot and nothing * confirmed it, so whatever it started did not come up. @@ -451,6 +483,7 @@ static void disk_boot_state_reap(uint32_t *pA_ver, uint32_t *pB_ver) for (i = 0; i < 2; i++) { st = DISK_STATE_NEW; slot_state[i] = DISK_STATE_NEW; + slot_reaped[i] = 0; if (slot_state_read(&boot_slots[i], &st) != 0) { continue; } @@ -460,6 +493,8 @@ static void disk_boot_state_reap(uint32_t *pA_ver, uint32_t *pB_ver) } wolfBoot_printf("Slot %c was not confirmed; skipping it\r\n", 'A' + i); + slot_reaped[i] = 1; + SET_FAILOVER(DISK_FAILOVER_UNCONFIRMED); if (i == 0) { *pA_ver = 0; } @@ -470,6 +505,59 @@ static void disk_boot_state_reap(uint32_t *pA_ver, uint32_t *pB_ver) } #endif /* DISK_BOOT_CONFIRM */ +#ifdef DISK_FAILOVER_ZEROIZE +/* Default failover actions when the operational slots are given up on: drop + * the image key (the platform's wolfBoot_erase_encrypt_key) and zero the + * head of both operational slots so neither image can be elected again. + * The golden slot is never touched. */ +static int disk_failover_default(int cause) +{ + /* One sector at a time: DISK_BLOCK_SIZE is the load chunk and can be + * hundreds of KB on targets that stage whole images. */ + static const uint8_t zeros[512]; + int i, ret = 0; + uint64_t off; + + wolfBoot_printf("Failover: operational images abandoned (cause %d)\r\n", + cause); +#ifdef DISK_ENCRYPT + if (wolfBoot_erase_encrypt_key() != 0) { + wolfBoot_printf("Failover: image key not erased\r\n"); + ret = -1; + } +#endif + for (i = 0; i < 2; i++) { + struct boot_slot *s = &boot_slots[i]; + if (s->ready == 0) { + continue; + } +#ifdef WOLFBOOT_DISK_FS + if (s->vol.type != FS_TYPE_RAW) { + continue; + } +#endif + for (off = 0; off < (uint64_t)DISK_FAILOVER_ERASE_SIZE; + off += sizeof(zeros)) { + if (disk_part_write(BOOT_DISK, s->part, off, sizeof(zeros), + zeros) != (int)sizeof(zeros)) { + wolfBoot_printf("Failover: p%d not erased\r\n", s->part); + ret = -1; + break; + } + } +#ifdef DISK_BOOT_CONFIRM + (void)slot_state_write(s, DISK_STATE_NEW, 0); +#endif + } + return ret; +} + +int WEAKFUNCTION hal_disk_failover(int cause) +{ + return disk_failover_default(cause); +} +#endif /* DISK_FAILOVER_ZEROIZE */ + /** * @brief Read from a boot slot. * @@ -668,6 +756,9 @@ void RAMFUNCTION wolfBoot_start(void) int selected; uint32_t *load_address; int failures = 0; +#ifdef DISK_FAILOVER_ZEROIZE + int failover_done = 0; +#endif uint32_t load_off; uint32_t max_ver; const uint8_t *hdr_ptr = NULL; @@ -706,16 +797,30 @@ void RAMFUNCTION wolfBoot_start(void) disk_decrypted_header_clear(dec_hdr); disk_crypto_clear(); wolfBoot_printf("Error initializing encryption\r\n"); +#ifdef DISK_FAILOVER_ZEROIZE + disk_key_ok = 0; + SET_FAILOVER(DISK_FAILOVER_NO_KEY); +#else wolfBoot_panic(); +#endif } /* Retrieve encryption key and nonce for disk decryption */ if (wolfBoot_get_encrypt_key(disk_encrypt_key, disk_encrypt_nonce) != 0) { disk_decrypted_header_clear(dec_hdr); disk_crypto_clear(); wolfBoot_printf("Error getting encryption key\r\n"); +#ifdef DISK_FAILOVER_ZEROIZE + /* The key is gone (a previous failover zeroized it): the encrypted + * operational images cannot boot, the plaintext golden one can. */ + disk_key_ok = 0; + SET_FAILOVER(DISK_FAILOVER_NO_KEY); +#else wolfBoot_panic(); +#endif + } + else { + wolfBoot_printf("Disk encryption enabled\r\n"); } - wolfBoot_printf("Disk encryption enabled\r\n"); #endif ret = disk_init(BOOT_DISK); @@ -789,7 +894,7 @@ void RAMFUNCTION wolfBoot_start(void) if (slot_read(&boot_slots[0], 0, IMAGE_HEADER_SIZE, p_hdr) == IMAGE_HEADER_SIZE) { #ifdef DISK_ENCRYPT - if (decrypt_header(p_hdr, dec_hdr) == 0) { + if (SLOT_ENCRYPTED_KEY_OK() && (decrypt_header(p_hdr, dec_hdr) == 0)) { /* Use local version parser to avoid double-decryption issue * when EXT_ENCRYPTED && MMU is also defined */ pA_ver = get_decrypted_blob_version(dec_hdr); @@ -804,7 +909,7 @@ void RAMFUNCTION wolfBoot_start(void) if (slot_read(&boot_slots[1], 0, IMAGE_HEADER_SIZE, p_hdr) == IMAGE_HEADER_SIZE) { #ifdef DISK_ENCRYPT - if (decrypt_header(p_hdr, dec_hdr) == 0) { + if (SLOT_ENCRYPTED_KEY_OK() && (decrypt_header(p_hdr, dec_hdr) == 0)) { /* Use local version parser to avoid double-decryption issue * when EXT_ENCRYPTED && MMU is also defined */ pB_ver = get_decrypted_blob_version(dec_hdr); @@ -824,6 +929,7 @@ void RAMFUNCTION wolfBoot_start(void) #ifdef DISK_GOLDEN_SLOT /* Straight to the golden slot. A version-0 header is not a bootable * A/B image here any more than it was before this option existed. */ + SET_FAILOVER_IF_UNSET(DISK_FAILOVER_NO_IMAGE); failures = MAX_FAILURES; #else #ifdef DISK_ENCRYPT @@ -871,6 +977,17 @@ void RAMFUNCTION wolfBoot_start(void) } wolfBoot_printf("Falling back to the golden image on p%d\r\n", boot_slots[SLOT_GOLDEN].part); +#ifdef DISK_FAILOVER_ZEROIZE + /* The operational images are being given up on: run the + * platform's failover actions once, before the recovery image + * is read. They may fail; the golden slot still boots. */ + if (!failover_done) { + failover_done = 1; + if (hal_disk_failover(failover_cause) != 0) { + wolfBoot_printf("Failover actions incomplete\r\n"); + } + } +#endif } #endif slot = &boot_slots[selected]; @@ -882,10 +999,10 @@ void RAMFUNCTION wolfBoot_start(void) * with ALLOW_DOWNGRADE defined the version guard that would other- * wise refuse it is compiled out. Refuse it here so the exclusion * holds on every path into the slot, not just the first choice. */ - if (!SLOT_IS_GOLDEN(selected) && - slot_state[selected] == DISK_STATE_TESTING) { + if (!SLOT_IS_GOLDEN(selected) && slot_reaped[selected]) { wolfBoot_printf("Slot %c was not confirmed; not booting it\r\n", 'A' + selected); + SET_FAILOVER(DISK_FAILOVER_UNCONFIRMED); selected ^= 1; continue; } @@ -898,6 +1015,7 @@ void RAMFUNCTION wolfBoot_start(void) wolfBoot_printf("Rollback to lower version not allowed\r\n"); #ifdef DISK_GOLDEN_SLOT /* Nothing newer can boot: only the golden slot is left. */ + SET_FAILOVER(DISK_FAILOVER_ROLLBACK); failures = MAX_FAILURES; continue; #else @@ -924,24 +1042,33 @@ void RAMFUNCTION wolfBoot_start(void) != IMAGE_HEADER_SIZE) { wolfBoot_printf("Error reading image header from disk: p%d\r\n", cur_part); + SET_FAILOVER(DISK_FAILOVER_NO_IMAGE); selected ^= 1; continue; } hdr_ptr = p_hdr; #ifdef DISK_ENCRYPT - /* Decrypt header to parse image size */ - if (decrypt_header(p_hdr, dec_hdr) != 0) { + /* Decrypt header to parse image size. A golden image is signed + * but not encrypted: it must boot after the image key is gone. */ + if (!SLOT_ENCRYPTED(selected)) { + hdr_ptr = p_hdr; + } + else if (decrypt_header(p_hdr, dec_hdr) != 0) { wolfBoot_printf("Error decrypting header for %s\r\n", part_name); + SET_FAILOVER(DISK_FAILOVER_NO_IMAGE); selected ^= 1; continue; } - hdr_ptr = dec_hdr; + else { + hdr_ptr = dec_hdr; + } #endif memset(&os_image, 0, sizeof(os_image)); ret = wolfBoot_open_image_address(&os_image, (void*)hdr_ptr); if (ret < 0) { wolfBoot_printf("Error parsing loaded image\r\n"); + SET_FAILOVER(DISK_FAILOVER_NO_IMAGE); selected ^= 1; continue; } @@ -960,6 +1087,7 @@ void RAMFUNCTION wolfBoot_start(void) (((uint64_t)os_image.fw_size + (uint64_t)IMAGE_HEADER_SIZE) > fs_size(&slot->f))) { wolfBoot_printf("Image larger than file for %s\r\n", part_name); + SET_FAILOVER(DISK_FAILOVER_NO_IMAGE); selected ^= 1; continue; } @@ -969,6 +1097,7 @@ void RAMFUNCTION wolfBoot_start(void) if (os_image.fw_size > WOLFBOOT_RAMBOOT_MAX_SIZE) { wolfBoot_printf("Image size %u exceeds max RAM load size\r\n", os_image.fw_size); + SET_FAILOVER(DISK_FAILOVER_NO_IMAGE); selected ^= 1; continue; } @@ -981,6 +1110,7 @@ void RAMFUNCTION wolfBoot_start(void) if (os_image.fw_size > slot_max) { wolfBoot_printf("Image size %u doesn't fit in low memory\r\n", os_image.fw_size); + SET_FAILOVER(DISK_FAILOVER_NO_IMAGE); selected ^= 1; continue; } @@ -1015,6 +1145,7 @@ void RAMFUNCTION wolfBoot_start(void) wolfBoot_printf("Error reading image from disk: p%d " "(%u of %u bytes)\r\n", cur_part, (unsigned int)load_off, (unsigned int)os_image.fw_size); + SET_FAILOVER(DISK_FAILOVER_NO_IMAGE); selected ^= 1; continue; } @@ -1022,24 +1153,27 @@ void RAMFUNCTION wolfBoot_start(void) #ifdef DISK_ENCRYPT /* Decrypt the payload in RAM */ - wolfBoot_printf("Decrypting image..."); - BENCHMARK_START(); - if ((IMAGE_HEADER_SIZE % ENCRYPT_BLOCK_SIZE) != 0) { - disk_decrypted_header_clear(dec_hdr); - disk_crypto_clear(); - wolfBoot_printf("Encrypted disk images require aligned header size\r\n"); - wolfBoot_panic(); + if (SLOT_ENCRYPTED(selected)) { + wolfBoot_printf("Decrypting image..."); + BENCHMARK_START(); + if ((IMAGE_HEADER_SIZE % ENCRYPT_BLOCK_SIZE) != 0) { + disk_decrypted_header_clear(dec_hdr); + disk_crypto_clear(); + wolfBoot_printf("Encrypted disk images require aligned header size\r\n"); + wolfBoot_panic(); + } + disk_crypto_set_iv(IMAGE_HEADER_SIZE / ENCRYPT_BLOCK_SIZE); + crypto_decrypt((uint8_t*)load_address, (uint8_t*)load_address, + os_image.fw_size); + BENCHMARK_END("done"); } - disk_crypto_set_iv(IMAGE_HEADER_SIZE / ENCRYPT_BLOCK_SIZE); - crypto_decrypt((uint8_t*)load_address, (uint8_t*)load_address, - os_image.fw_size); - BENCHMARK_END("done"); #endif memset(&os_image, 0, sizeof(os_image)); ret = wolfBoot_open_image_address(&os_image, (void*)hdr_ptr); if (ret < 0) { wolfBoot_printf("Error parsing loaded image\r\n"); + SET_FAILOVER(DISK_FAILOVER_NO_IMAGE); selected ^= 1; continue; } @@ -1053,6 +1187,7 @@ void RAMFUNCTION wolfBoot_start(void) BENCHMARK_START(); if (wolfBoot_verify_integrity(&os_image) != 0) { wolfBoot_printf("Error validating integrity for %s\r\n", part_name); + SET_FAILOVER(DISK_FAILOVER_VERIFY); selected ^= 1; continue; } @@ -1063,6 +1198,7 @@ void RAMFUNCTION wolfBoot_start(void) if (wolfBoot_verify_authenticity(&os_image) != 0) { wolfBoot_printf("Error validating authenticity for %s\r\n", part_name); + SET_FAILOVER(DISK_FAILOVER_VERIFY); selected ^= 1; continue; } else { @@ -1242,7 +1378,16 @@ void RAMFUNCTION wolfBoot_start(void) fdt_open(&dts_ctx, dts_addr, WOLFBOOT_DTS_MAX_SIZE) == 0) { dts_for_initrd = &dts_ctx; } - (void)fit_load_ramdisk(fit, ramdisk, dts_for_initrd); + /* A ramdisk the FIT names is part of the OS image: without it + * the kernel would come up with no root filesystem. */ + if (fit_load_ramdisk(fit, ramdisk, dts_for_initrd) != 0) { + wolfBoot_printf("Failed to load the FIT ramdisk\r\n"); +#ifdef DISK_ENCRYPT + disk_decrypted_header_clear(dec_hdr); + disk_crypto_clear(); +#endif + wolfBoot_panic(); + } } #else (void)ramdisk; diff --git a/src/update_ram.c b/src/update_ram.c index cd7cf8bcdb..dcf3c433f0 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -699,7 +699,12 @@ void RAMFUNCTION wolfBoot_start(void) fdt_open(&dts_ctx, dts_addr, WOLFBOOT_DTS_MAX_SIZE) == 0) { dts_for_initrd = &dts_ctx; } - (void)fit_load_ramdisk(fit, ramdisk, dts_for_initrd); + /* A ramdisk the FIT names is part of the OS image: without it + * the kernel would come up with no root filesystem. */ + if (fit_load_ramdisk(fit, ramdisk, dts_for_initrd) != 0) { + wolfBoot_printf("Failed to load the FIT ramdisk\r\n"); + wolfBoot_panic(); + } } #else (void)ramdisk; diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index ced8bc4dec..2920a513de 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -68,7 +68,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-update-flash-self-update \ unit-nsc-update \ unit-update-flash-enc unit-update-flash-enc-full unit-update-flash-enc-intswap unit-update-ram unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-custom-trailer-nopart unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ - unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ + unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-failover unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-image-dts \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ unit-tpm-blob unit-policy-create unit-policy-sign unit-rot-auth unit-sdhci-response-bits \ @@ -445,6 +445,8 @@ unit-update-disk-confirm:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_R -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE unit-update-disk-golden:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x400 \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +unit-update-disk-failover:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x400 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE # Non-FSP disk-boot OOB regression (CRIT-03). WOLFBOOT_RAMBOOT_MAX_SIZE is the # cap the loader applies to the unauthenticated header fw_size before loading to # RAM, the same bound update_disk.c and update_ram.c enforce. @@ -1180,6 +1182,9 @@ unit-update-disk-confirm: ../../include/target.h unit-update-disk-confirm.c unit-update-disk-golden: ../../include/target.h unit-update-disk-golden.c gcc -o $@ unit-update-disk-golden.c $(CFLAGS) $(LDFLAGS) +unit-update-disk-failover: ../../include/target.h unit-update-disk-failover.c + gcc -o $@ unit-update-disk-failover.c $(CFLAGS) $(LDFLAGS) + # WOLFBOOT_FSP (x86) boot path of update_disk.c: the low-memory (tolum) # size check must reject a slot and fall back to the other one, like every # other per-slot rejection in the retry loop. diff --git a/tools/unit-tests/unit-update-disk-failover.c b/tools/unit-tests/unit-update-disk-failover.c new file mode 100644 index 0000000000..a44f2d6e54 --- /dev/null +++ b/tools/unit-tests/unit-update-disk-failover.c @@ -0,0 +1,460 @@ +/* unit-update-disk-failover.c + * + * Unit test for the disk boot failover actions (DISK_FAILOVER_ZEROIZE). + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#define WOLFBOOT_UPDATE_DISK +#define WOLFBOOT_SELF_UPDATE_MONOLITHIC +#define RAM_CODE +#define WOLFBOOT_SELF_HEADER +#define EXT_ENCRYPTED +#define ENCRYPT_WITH_CHACHA +#define HAVE_CHACHA +#define IMAGE_HEADER_SIZE 256 +#define BOOT_PART_A 0 +#define BOOT_PART_B 1 +#define BOOT_PART_GOLDEN 2 +#define DISK_GOLDEN_SLOT +#define DISK_BOOT_CONFIRM +#define DISK_FAILOVER_ZEROIZE +#define MOCK_ADDRESS_BOOT 0xCD000000 + +#include +#include +#include +#include + +#include "hal.h" +#include "target.h" +#include "wolfboot/wolfboot.h" +#include "image.h" +#include "loader.h" +#include + +#define TEST_PAYLOAD_SIZE 64 +#define TEST_PART_SIZE (IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE + 1024) +#define FILL_A 0xA1 +#define FILL_B 0xB2 +#define FILL_G 0xC3 +#define N_PARTS 3 + +static uint8_t load_buffer[TEST_PART_SIZE]; +#define WOLFBOOT_LOAD_ADDRESS ((uintptr_t)load_buffer) + +static uint8_t part_image[N_PARTS][TEST_PART_SIZE]; +static int mock_reads[N_PARTS]; +static int mock_writes[N_PARTS]; +static int mock_bad_fill[N_PARTS]; +static int mock_do_boot_called; +static int mock_booted_fill; +static int mock_key_erased; +static int mock_key_missing; +static int mock_key_erase_fail; +static int mock_write_fail_part; +ChaCha chacha; + +static void set_u16_le(uint8_t *dst, uint16_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)(value >> 8); +} + +static void set_u32_le(uint8_t *dst, uint32_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)((value >> 8) & 0xFF); + dst[2] = (uint8_t)((value >> 16) & 0xFF); + dst[3] = (uint8_t)(value >> 24); +} + +static void build_image(uint8_t *image, uint32_t version, uint8_t fill) +{ + memset(image, 0, TEST_PART_SIZE); + set_u32_le(image, WOLFBOOT_MAGIC); + set_u32_le(image + sizeof(uint32_t), TEST_PAYLOAD_SIZE); + set_u16_le(image + IMAGE_HEADER_OFFSET, HDR_VERSION); + set_u16_le(image + IMAGE_HEADER_OFFSET + sizeof(uint16_t), 4); + set_u32_le(image + IMAGE_HEADER_OFFSET + 2 * sizeof(uint16_t), version); + memset(image + IMAGE_HEADER_SIZE, fill, TEST_PAYLOAD_SIZE); +} + +static void reset_mocks(void) +{ + memset(load_buffer, 0, sizeof(load_buffer)); + build_image(part_image[0], 3, FILL_A); + build_image(part_image[1], 2, FILL_B); + build_image(part_image[2], 1, FILL_G); + memset(mock_reads, 0, sizeof(mock_reads)); + memset(mock_writes, 0, sizeof(mock_writes)); + memset(mock_bad_fill, 0, sizeof(mock_bad_fill)); + mock_do_boot_called = 0; + mock_booted_fill = -1; + mock_key_erased = 0; + mock_key_missing = 0; + mock_key_erase_fail = 0; + mock_write_fail_part = -1; + wolfBoot_panicked = 0; +} + +static void mark_bad(uint8_t fill) +{ + int i; + for (i = 0; i < N_PARTS; i++) { + if (mock_bad_fill[i] == 0) { + mock_bad_fill[i] = fill; + return; + } + } +} + +int chacha_init(void) +{ + return 0; +} + +int wc_Chacha_SetIV(ChaCha* ctx, const byte* inIv, word32 counter) +{ + (void)ctx; (void)inIv; (void)counter; + return 0; +} + +int wc_Chacha_Process(ChaCha* ctx, byte* output, const byte* input, word32 msglen) +{ + (void)ctx; + memmove(output, input, msglen); + return 0; +} + +void wc_ForceZero(void* mem, size_t len) +{ + volatile uint8_t *p = (volatile uint8_t *)mem; + while (len-- > 0) + *p++ = 0; +} + +int wolfBoot_initialize_encryption(void) +{ + return mock_key_missing ? -1 : 0; +} + +int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + if (mock_key_missing) { + return -1; + } + memset(key, 0x5A, ENCRYPT_KEY_SIZE); + memset(nonce, 0xC3, ENCRYPT_NONCE_SIZE); + return 0; +} + +int wolfBoot_erase_encrypt_key(void) +{ + mock_key_erased++; + return mock_key_erase_fail ? -1 : 0; +} + +int disk_init(int drv) +{ + (void)drv; + return 0; +} + +int disk_open(int drv) +{ + (void)drv; + return 0; +} + +void disk_close(int drv) +{ + (void)drv; +} + +int disk_part_read(int drv, int part, uint64_t off, uint64_t sz, uint8_t *buf) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + mock_reads[part]++; + memcpy(buf, part_image[part] + off, (size_t)sz); + return (int)sz; +} + +int disk_part_write(int drv, int part, uint64_t off, uint64_t sz, + const uint8_t *buf) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + if (part == mock_write_fail_part) { + mock_writes[part]++; + return -1; + } + mock_writes[part]++; + memcpy(part_image[part] + off, buf, (size_t)sz); + return (int)sz; +} + +int disk_part_size(int drv, int part, uint64_t *size) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + *size = TEST_PART_SIZE; + return 0; +} + +int wolfBoot_open_image_address(struct wolfBoot_image* img, uint8_t* image) +{ + uint32_t magic; + uint32_t fw_size; + + memcpy(&magic, image, sizeof(magic)); + if (magic != WOLFBOOT_MAGIC) + return -1; + memset(img, 0, sizeof(*img)); + img->hdr = image; + memcpy(&fw_size, image + sizeof(uint32_t), sizeof(fw_size)); + img->fw_size = fw_size; + img->fw_base = image + IMAGE_HEADER_SIZE; + img->hdr_ok = 1; + return 0; +} + +int wolfBoot_verify_integrity(struct wolfBoot_image* img) +{ + int i; + uint8_t fill = load_buffer[0]; + (void)img; + for (i = 0; i < N_PARTS; i++) { + if (mock_bad_fill[i] != 0 && mock_bad_fill[i] == fill) + return -1; + } + img->sha_ok = 1; + return 0; +} + +int wolfBoot_verify_authenticity(struct wolfBoot_image* img) +{ + img->signature_ok = 1; + return 0; +} + +int wolfBoot_get_dts_size(void *dts_addr, uint32_t capacity) +{ + (void)capacity; (void)dts_addr; + return -1; +} + +void hal_prepare_boot(void) +{ +} + +void do_boot(const uint32_t *address) +{ + mock_do_boot_called++; + mock_booted_fill = ((const uint8_t*)address)[0]; +} + +int hal_flash_protect(haladdr_t address, int len) +{ + (void)address; (void)len; + return 0; +} + +#include "update_disk.c" + +static void set_state(int part, uint8_t state) +{ + uint64_t off = 0; + ck_assert_int_eq(disk_trailer_offset(TEST_PART_SIZE, &off), 0); + disk_trailer_encode(part_image[part] + off, state); +} + +static uint8_t get_state(int part) +{ + uint64_t off = 0; + ck_assert_int_eq(disk_trailer_offset(TEST_PART_SIZE, &off), 0); + return disk_trailer_decode(part_image[part] + off); +} + +static int head_is_zero(int part) +{ + int i; + for (i = 0; i < IMAGE_HEADER_SIZE; i++) { + if (part_image[part][i] != 0) + return 0; + } + return 1; +} + +START_TEST(test_no_failover_when_a_boots) +{ + reset_mocks(); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_A); + ck_assert_int_eq(mock_key_erased, 0); + ck_assert_int_eq(mock_writes[BOOT_PART_A], 0); + ck_assert_int_eq(mock_writes[BOOT_PART_B], 0); +} +END_TEST + +START_TEST(test_failover_erases_key_and_operational_slots) +{ + reset_mocks(); + /* B at A's version, so the flip to B is a verification failure and not + * an anti-rollback refusal. */ + build_image(part_image[1], 3, FILL_B); + mark_bad(FILL_A); + mark_bad(FILL_B); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_key_erased, 1); + ck_assert_int_eq(failover_cause, DISK_FAILOVER_VERIFY); + ck_assert(head_is_zero(BOOT_PART_A)); + ck_assert(head_is_zero(BOOT_PART_B)); + ck_assert(!head_is_zero(BOOT_PART_GOLDEN)); + ck_assert_int_eq(get_state(BOOT_PART_A), DISK_STATE_NEW); + ck_assert_int_eq(mock_writes[BOOT_PART_GOLDEN], 0); +} +END_TEST + +START_TEST(test_failover_runs_once_and_golden_still_boots_if_it_fails) +{ + /* The key erase fails, so hal_disk_failover() returns an error: the + * golden image must still boot and the actions must not be retried. */ + reset_mocks(); + mock_key_erase_fail = 1; + memset(part_image[0], 0, TEST_PART_SIZE); + memset(part_image[1], 0, TEST_PART_SIZE); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_key_erased, 1); + ck_assert_int_eq(failover_cause, DISK_FAILOVER_NO_IMAGE); +} +END_TEST + +START_TEST(test_golden_boots_when_a_slot_head_cannot_be_erased) +{ + /* Slot A rejects every write. The key is still erased, slot B is still + * zeroed, and the golden image still boots. */ + reset_mocks(); + mock_write_fail_part = BOOT_PART_A; + mark_bad(FILL_A); + mark_bad(FILL_B); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_key_erased, 1); + ck_assert(!head_is_zero(BOOT_PART_A)); + ck_assert(head_is_zero(BOOT_PART_B)); + ck_assert_int_eq(mock_writes[BOOT_PART_GOLDEN], 0); +} +END_TEST + +START_TEST(test_missing_key_boots_plaintext_golden) +{ + reset_mocks(); + mock_key_missing = 1; + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(failover_cause, DISK_FAILOVER_NO_KEY); +} +END_TEST + +START_TEST(test_staged_slot_is_armed_for_testing) +{ + reset_mocks(); + set_state(BOOT_PART_A, DISK_STATE_UPDATING); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_A); + ck_assert_int_eq(get_state(BOOT_PART_A), DISK_STATE_TESTING); +} +END_TEST + +START_TEST(test_unconfirmed_slot_is_reaped) +{ + reset_mocks(); + set_state(BOOT_PART_A, DISK_STATE_TESTING); + wolfBoot_start(); + + /* A was left unconfirmed, so it is reaped and B (older) boots with no + * failover action: one operational slot is still good. */ + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_B); + ck_assert_int_eq(mock_key_erased, 0); + ck_assert(!head_is_zero(BOOT_PART_A)); +} +END_TEST + +START_TEST(test_both_unconfirmed_fail_over_with_the_unconfirmed_cause) +{ + reset_mocks(); + set_state(BOOT_PART_A, DISK_STATE_TESTING); + set_state(BOOT_PART_B, DISK_STATE_TESTING); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_key_erased, 1); + ck_assert_int_eq(failover_cause, DISK_FAILOVER_UNCONFIRMED); + ck_assert(head_is_zero(BOOT_PART_A)); + ck_assert(head_is_zero(BOOT_PART_B)); +} +END_TEST + +Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfBoot"); + TCase *tc = tcase_create("update-disk-failover"); + + tcase_add_test(tc, test_no_failover_when_a_boots); + tcase_add_test(tc, test_failover_erases_key_and_operational_slots); + tcase_add_test(tc, test_failover_runs_once_and_golden_still_boots_if_it_fails); + tcase_add_test(tc, test_golden_boots_when_a_slot_head_cannot_be_erased); + tcase_add_test(tc, test_missing_key_boots_plaintext_golden); + tcase_add_test(tc, test_staged_slot_is_armed_for_testing); + tcase_add_test(tc, test_unconfirmed_slot_is_reaped); + tcase_add_test(tc, test_both_unconfirmed_fail_over_with_the_unconfirmed_cause); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = wolfboot_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +} From 28e01daf2682e36c9b86a3b76c5932f79ed88d20 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 8 Oct 2026 08:49:55 -0700 Subject: [PATCH 06/23] PolarFire SoC: SBI vendor extension for the OS: PUF keys, System Controller services and flash access --- .github/workflows/test-configs.yml | 8 + Makefile | 3 + docs/Targets.md | 32 ++ hal/mpfs250.c | 487 +++++++++++++++++++++++- hal/mpfs250.h | 23 +- hal/mpfs250_snvm.c | 72 +++- hal/mpfs250_snvm.h | 9 + include/riscv_sbi.h | 97 +++++ options.mk | 26 ++ src/riscv_sbi.c | 208 ++++++++-- src/vector_riscv.S | 45 ++- tools/linux/wolfboot-sbi/Makefile | 14 + tools/linux/wolfboot-sbi/wolfboot_sbi.c | 352 +++++++++++++++++ tools/linux/wolfboot-sbi/wolfboot_sbi.h | 97 +++++ 14 files changed, 1399 insertions(+), 74 deletions(-) create mode 100644 include/riscv_sbi.h create mode 100644 tools/linux/wolfboot-sbi/Makefile create mode 100644 tools/linux/wolfboot-sbi/wolfboot_sbi.c create mode 100644 tools/linux/wolfboot-sbi/wolfboot_sbi.h diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 3984061b69..da73b1287e 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -555,6 +555,14 @@ jobs: config-file: ./config/examples/polarfire_mpfs250_m.config pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1 MPFS_SCB_SMODE_DENY=1 + # SBI vendor extension (serial, PUF keys, fenced System Controller services). + microchip_mpfs250_m_sbi_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub MPFS_SBI_VENDOR=1 MPFS_SCB_SMODE_DENY=1 MPFS_SBI_SC_FLASH=1 # Encrypted disk image with the AES key unwrapped by the PUF-derived KEK. microchip_mpfs250_m_enc_kek_test: uses: ./.github/workflows/test-build-riscv.yml diff --git a/Makefile b/Makefile index 5265157968..b020eae464 100644 --- a/Makefile +++ b/Makefile @@ -203,6 +203,9 @@ endif ifeq ($(SNVM_KEK),1) MPFS_SNVM_OBJ=1 endif +ifeq ($(MPFS_SBI_VENDOR),1) + MPFS_SNVM_OBJ=1 +endif ifeq ($(MPFS_SNVM_OBJ),1) OBJS+=./hal/mpfs250_snvm.o endif diff --git a/docs/Targets.md b/docs/Targets.md index 48eaca13bd..c73ac84b63 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -2002,6 +2002,38 @@ mpfs-video-kit login: No `failed to come online`; the only error lines are the known-cosmetic eth-SGMII PHY, i2c clock-divider, and mmc-tuning messages unrelated to this feature. +### PolarFire SoC: wolfBoot SBI vendor extension + +The M-mode wolfBoot stays resident as the SBI for the OS. With `MPFS_SBI_VENDOR=1` it also serves the Microchip vendor extension, EID `0x09000029` (`SBI_EXT_VENDOR_START | mvendorid 0x029`), the same number the Hart Software Services register, so an OS probes one extension for either firmware and tells them apart with `GET_VERSION`. The HSS function IDs for inter-hart communication, remoteproc, crypto and IPC are answered with `SBI_ERR_NOT_SUPPORTED`; `HSS_REBOOT` (0x10) performs a cold MSS reset. + +The definitions live in `include/riscv_sbi.h`; the handler is `sbi_hal_vendor_ecall()` in `hal/mpfs250.c`. Arguments travel in `a0..a2`, the SBI error returns in `a0` and the value in `a1`. Buffers are addresses in the caller's address space: M-mode reads and writes them through the caller's translation, byte by byte, with a trap vector that turns a bad address into `SBI_ERR_INVALID_ADDRESS` instead of a stopped hart. A buffer is at most 4096 bytes and, while the caller runs with paging off, must lie in DDR. + +| FID | Call | Arguments | Result | +|-----|------|-----------|--------| +| 0x40 | `GET_VERSION` | - | `a1` = 1 | +| 0x41 | `GET_SERIAL` | `a0` buffer, `a1` length >= 16 | the 16-byte device serial number | +| 0x42 | `GET_KEY` | `a0` purpose, `a1` buffer, `a2` length == 32 | a PUF-derived key; each purpose is served once per boot, a second call returns `SBI_ERR_DENIED` | +| 0x43 | `DIGEST_CHECK` | `a0` options word | starts the System Controller digest check | +| 0x44 | `IAP_AUTHENTICATE` | `a0` SPI directory index | starts IAP image authentication | +| 0x45 | `IAP_VERIFY` | `a0` SPI directory index | starts IAP verify | +| 0x46 | `IAP_PROGRAM` | `a0` SPI directory index | the device restarts on success; a return is the failure status in `a1` | +| 0x47 | `AUTO_UPDATE` | - | the device restarts on success; a return is the failure status in `a1` | +| 0x48 | `SC_FLASH_ERASE` | `a0` flash offset, `a1` length == one erase block (64 KB), both block aligned | erases that block of the System Controller's SPI flash; `MPFS_SBI_SC_FLASH=1` builds only | +| 0x49 | `SC_FLASH_WRITE` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | programs the bytes; `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4A | `SC_FLASH_READ` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | reads the bytes (any offset, the directory included); `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4B | `SC_FLASH_SET_IMAGE` | `a0` directory index 1 (upgrade) or 2 (blank), `a1` image offset | rewrites the SPI directory block with that entry changed (entry 1 also zeroes entry 2); `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4F | `SC_STATUS` | - | `SBI_ERR_ALREADY_STARTED` while the started service runs, else `a1` = 16-bit status with mailbox word 1 (the digest-check `DIGESTERR` bits) in the upper half | + +`GET_KEY` purposes name a fixed label; the caller never supplies a challenge, so the OS cannot ask the PUF for wolfBoot's own key-encryption key. Purpose 1 is the MRAM log key (`SHA-384("wolfBoot-mram-log-v1" || PUF(SHA-384("wolfBoot-mram-log-v1")[0..15]))[0..31]`); purpose 2 reproduces the earlier user-space `varlog` derivation. The key is device-bound: the same whether the golden or an operational image booted. + +The System Controller services (0x43-0x4F) are served only in a build with `MPFS_SCB_SMODE_DENY=1`, where the OS cannot reach the mailbox itself; in an unfenced build they return `SBI_ERR_DENIED` and the OS uses its own mailbox drivers, which the fence would otherwise race. The long-running ones are started inside the ecall and polled through `SC_STATUS`, because an ecall holds its hart in M-mode with interrupts off. The mailbox is one resource for all harts: a call that finds it taken returns `SBI_ERR_FAILED`. + +The fence also covers the System Controller's QSPI flash controller, so a fenced OS cannot stage a bitstream the way the mainline `mpfs-auto-update` driver does (through `mtd` on that controller). `MPFS_SBI_SC_FLASH=1` adds the flash calls (0x48-0x4B) for that build: the OS erases and writes the image region directly, reads back what it wants verified, and points the SPI directory's upgrade entry at the image with `SC_FLASH_SET_IMAGE` before `IAP_AUTHENTICATE` / `IAP_PROGRAM` or `AUTO_UPDATE`. The first erase block (the directory and the start of the design-info area) is never erased or written through `SC_FLASH_ERASE` / `SC_FLASH_WRITE`; `SET_IMAGE` reads it, changes the one entry (the golden entry 0 stays what it was) and writes it back, the same read-erase-write the OS driver performs. Each erase call takes one block and each transfer at most 4 KB, so no ecall holds its hart for more than the erase time of one block; the OS side loops. The flash layout the mainline driver uses (directory in the first 1 KB, design info at 1 KB, bitstream slots from 1 MB + 1 KB) is the OS's business; wolfBoot only protects the directory block. + +The Linux side is the `wolfboot-sbi` module in `tools/linux/wolfboot-sbi/`: `/dev/wolfboot` (root only) with one ioctl per call, see `wolfboot_sbi.h`. It probes the extension and `GET_VERSION`, so it unloads cleanly under the HSS, and it never touches the mailbox registers, so the mainline `mpfs-sys-controller`, `mpfs-auto-update` and `mpfs-rng` drivers keep working in an unfenced build. A caller of `IAP_PROGRAM` or `AUTO_UPDATE` syncs its filesystems first: the device restarts inside the call. + +Build: `MPFS_SBI_VENDOR=1` on the M-mode target (`RISCV_MMODE=1`); the key derivation is SHA-384 whatever the image hash. + ## STM32F7 The STM32-F76x and F77x offer dual-bank hardware-assisted swapping. diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 28c65072b7..2bf7a6962b 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -45,9 +45,14 @@ #include "hal.h" #include "gpt.h" #include "fdt.h" -#if defined(SNVM_KEYSTORE_PROVISION) || defined(SNVM_ENCKEY_PROVISION) +#if defined(SNVM_KEYSTORE_PROVISION) || defined(SNVM_ENCKEY_PROVISION) || \ + defined(MPFS_SBI_VENDOR) #include "mpfs250_snvm.h" #endif +#if defined(MPFS_SBI_VENDOR) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +#include "riscv_sbi.h" +#include /* wc_ForceZero */ +#endif #ifdef MPFS_ATHENA #include @@ -1111,16 +1116,24 @@ static int mpfs_scb_mailbox_busy(void) return (SCBCTRL_REG(SERVICES_SR_OFFSET) & SERVICES_SR_BUSY_MASK); } -/* System Controller service, polling mode, mailbox word offset 0. Returns the - * 16-bit service status (0 = success) or a negative transport error. */ -static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, +/* Once the OS runs, the mailbox is driven from ecalls, where a console + * write could block on a UART the OS has reconfigured. */ +static int mpfs_scb_quiet; + +/* Raise a System Controller service. The request bytes go to mailbox word + * 0; index is the SERVICES_CR mailbox-offset field, which carries the SPI + * directory index for the IAP services. Returns 0 once the request is + * accepted, or a negative transport error. */ +static int mpfs_scb_start(uint8_t opcode, uint32_t index, const uint8_t *req, uint32_t req_len) { uint32_t cmd, words, rem, i, v; int timeout; if (mpfs_scb_mailbox_busy()) { - wolfBoot_printf("SCB mailbox busy\n"); + if (!mpfs_scb_quiet) { + wolfBoot_printf("SCB mailbox busy\n"); + } return -2; } @@ -1146,9 +1159,10 @@ static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, /* Ensure mailbox writes land before the request is raised. */ __asm__ volatile("fence w,w" ::: "memory"); - /* Command: opcode in [22:16] (SERVICES_CR_COMMAND_SHIFT), REQ in bit 0; - * the mailbox word-offset field is left 0. */ + /* Command: opcode in [22:16] (SERVICES_CR_COMMAND_SHIFT), the mailbox + * offset / SPI index in [15:7], REQ in bit 0. */ cmd = (((uint32_t)(opcode & 0x7Fu)) << SERVICES_CR_COMMAND_SHIFT) | + ((index & 0x1FFu) << SERVICES_CR_MBOX_SHIFT) | SERVICES_CR_REQ_MASK; SCBCTRL_REG(SERVICES_CR_OFFSET) = cmd; @@ -1159,23 +1173,51 @@ static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, timeout--; } if (timeout == 0) { - wolfBoot_printf("SCB request timeout\n"); + if (!mpfs_scb_quiet) { + wolfBoot_printf("SCB request timeout\n"); + } return -3; } + return 0; +} + +/* The 16-bit status of the service in progress. */ +static int mpfs_scb_status(void) +{ + return (int)((SCBCTRL_REG(SERVICES_SR_OFFSET) >> SERVICES_SR_STATUS_SHIFT) + & 0xFFFFu); +} + +/* Wait for the service to complete. Returns its status (0 = success) or a + * negative transport error. Uses the larger completion bound: nonce/PUF + * take much longer than serial/sNVM-read. */ +static int mpfs_scb_wait(void) +{ + int timeout = MPFS_SCB_BUSY_TIMEOUT; - /* Wait for busy bit to clear (service complete). Uses the larger - * completion bound: nonce/PUF take much longer than serial/sNVM-read. */ - timeout = MPFS_SCB_BUSY_TIMEOUT; while (mpfs_scb_mailbox_busy() && (timeout > 0)) { timeout--; } if (timeout == 0) { - wolfBoot_printf("SCB busy timeout\n"); + if (!mpfs_scb_quiet) { + wolfBoot_printf("SCB busy timeout\n"); + } return -4; } + return mpfs_scb_status(); +} - return (int)((SCBCTRL_REG(SERVICES_SR_OFFSET) >> SERVICES_SR_STATUS_SHIFT) - & 0xFFFFu); +/* System Controller service, polling mode, mailbox word offset 0. Returns the + * 16-bit service status (0 = success) or a negative transport error. */ +static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, + uint32_t req_len) +{ + int ret = mpfs_scb_start(opcode, 0, req, req_len); + + if (ret != 0) { + return ret; + } + return mpfs_scb_wait(); } /* Read len response bytes from the mailbox at byte offset off (mb word 0). */ @@ -1187,6 +1229,419 @@ static void mpfs_scb_read(uint8_t *out, uint32_t off, uint32_t len) } } +#if defined(MPFS_SBI_VENDOR) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +/* SBI vendor extension (include/riscv_sbi.h) served to the OS. Everything + * here runs inside an ecall on one hart with interrupts off: the System + * Controller services that take seconds are started here and polled by the + * OS through SC_STATUS, the short ones complete in place. */ + +/* The mailbox is one resource for all harts. Taken for the life of a + * started service, released when SC_STATUS reports it complete. */ +static volatile uint32_t sbi_scb_lock; +/* Set while a started (asynchronous) service owns the lock, so SC_STATUS + * releases only that one and never a lock a synchronous call is holding. */ +static volatile uint32_t sbi_scb_started; +/* GET_KEY purposes already handed out this boot, one bit each. */ +static uint32_t sbi_key_served; + +static int sbi_scb_trylock(void) +{ + return (__atomic_exchange_n(&sbi_scb_lock, 1U, __ATOMIC_ACQ_REL) == 0U); +} + +static void sbi_scb_unlock(void) +{ + __atomic_store_n(&sbi_scb_lock, 0U, __ATOMIC_RELEASE); +} + +/* The challenge seed and hash label behind each purpose. The OS names a + * purpose, never a challenge, so the PUF cannot be asked for the KEK. */ +static const struct { + unsigned long purpose; + const char *seed; + const char *label; +} sbi_key_purposes[] = { + { SBI_WOLF_KEY_MRAM_LOG, "wolfBoot-mram-log-v1", "wolfBoot-mram-log-v1" }, + { SBI_WOLF_KEY_VARLOG, "varlog", "wolfLUKS-key-v1:varlog" }, +}; + +/* Give a claimed serving back, so a request that never handed the key over + * does not consume it. */ +static void sbi_key_unclaim(unsigned int i) +{ + (void)__atomic_fetch_and(&sbi_key_served, ~(1U << i), __ATOMIC_ACQ_REL); +} + +static long sbi_vendor_get_key(unsigned long purpose, unsigned long buf, + unsigned long len) +{ + uint8_t key[SBI_WOLF_KEY_LEN]; + unsigned int i; + int ret; + + for (i = 0; i < sizeof(sbi_key_purposes) / sizeof(sbi_key_purposes[0]); + i++) { + if (sbi_key_purposes[i].purpose == purpose) { + break; + } + } + if ((i == sizeof(sbi_key_purposes) / sizeof(sbi_key_purposes[0])) || + (len != SBI_WOLF_KEY_LEN)) { + return SBI_ERR_INVALID_PARAM; + } + if (!sbi_smode_range_ok(buf, len)) { + return SBI_ERR_INVALID_ADDRESS; + } + /* Claim the serving before deriving anything. A plain check here and a + * set at the end would let two harts asking for the same purpose at once + * both be served; the claim is given back below if the key never reaches + * the caller. */ + if ((__atomic_fetch_or(&sbi_key_served, 1U << i, __ATOMIC_ACQ_REL) + & (1U << i)) != 0U) { + return SBI_ERR_DENIED; + } + if (!sbi_scb_trylock()) { + sbi_key_unclaim(i); + return SBI_ERR_FAILED; + } + ret = mpfs_puf_purpose_key(sbi_key_purposes[i].seed, + sbi_key_purposes[i].label, key); + sbi_scb_unlock(); + if (ret != 0) { + wc_ForceZero(key, sizeof(key)); + sbi_key_unclaim(i); + return SBI_ERR_FAILED; + } + ret = sbi_copy_to_smode(buf, key, sizeof(key)); + wc_ForceZero(key, sizeof(key)); + if (ret != 0) { + /* the caller never saw the key, so its one serving still stands */ + sbi_key_unclaim(i); + return SBI_ERR_INVALID_ADDRESS; + } + return SBI_SUCCESS; +} + +/* Services that reach the System Controller are served only when the OS + * cannot drive the mailbox itself: unfenced, its own driver would race + * these requests. */ +#ifdef MPFS_SCB_SMODE_DENY +#ifdef MPFS_SBI_SC_FLASH +/* The System Controller's SPI flash for the OS. The fence that keeps the + * OS off the mailbox also covers the flash controller (0x37020100), so under + * it this is the OS's only way to stage a bitstream. The first erase block + * holds the SPI directory the System Controller boots from: its golden entry + * is never written, and the block changes only through SET_IMAGE, which + * edits the upgrade or blank entry in a copy and writes the rest back as it + * was. Every other offset is the OS's. An erase call takes one block and a + * write or read at most SBI_SMODE_BUF_MAX bytes, so an ecall stays well + * under a second with the hart's interrupts masked. The mailbox lock is + * held throughout: the System Controller reads this flash for its own + * services. */ +int qspi_init(void); + +#define SBI_SC_FLASH_UPGRADE_INDEX 1UL +#define SBI_SC_FLASH_BLANK_INDEX 2UL + +static int sbi_sc_flash_ready; +static uint8_t sbi_sc_flash_buf[SBI_SMODE_BUF_MAX]; +static uint32_t sbi_sc_flash_block[FLASH_SECTOR_SIZE / sizeof(uint32_t)]; + +static long sbi_sc_flash_take(void) +{ + if (!sbi_scb_trylock()) { + return SBI_ERR_FAILED; + } + if (!sbi_sc_flash_ready) { + if (qspi_init() != 0) { + sbi_scb_unlock(); + return SBI_ERR_FAILED; + } + sbi_sc_flash_ready = 1; + } + return SBI_SUCCESS; +} + +/* Inside the flash and clear of the directory block. */ +static int sbi_sc_flash_range_ok(unsigned long off, unsigned long len) +{ + return (len != 0UL) && (off >= (unsigned long)FLASH_SECTOR_SIZE) && + (off <= (unsigned long)FLASH_DEVICE_SIZE) && + (len <= (unsigned long)FLASH_DEVICE_SIZE - off); +} + +static long sbi_vendor_sc_flash_erase(unsigned long off, unsigned long len) +{ + long err; + + if (!sbi_sc_flash_range_ok(off, len) || + (len != (unsigned long)FLASH_SECTOR_SIZE) || + ((off & ((unsigned long)FLASH_SECTOR_SIZE - 1UL)) != 0UL)) { + return SBI_ERR_INVALID_PARAM; + } + err = sbi_sc_flash_take(); + if (err != SBI_SUCCESS) { + return err; + } + if (ext_flash_erase(off, (int)len) != (int)len) { + err = SBI_ERR_FAILED; + } + sbi_scb_unlock(); + return err; +} + +static long sbi_vendor_sc_flash_write(unsigned long off, unsigned long buf, + unsigned long len) +{ + long err; + + if (!sbi_sc_flash_range_ok(off, len) || (len > SBI_SMODE_BUF_MAX)) { + return SBI_ERR_INVALID_PARAM; + } + if (!sbi_smode_range_ok(buf, len)) { + return SBI_ERR_INVALID_ADDRESS; + } + err = sbi_sc_flash_take(); + if (err != SBI_SUCCESS) { + return err; + } + /* the bounce buffer is shared between harts: copy under the lock */ + if (sbi_copy_from_smode(sbi_sc_flash_buf, buf, len) != 0) { + err = SBI_ERR_INVALID_ADDRESS; + } + else if (ext_flash_write(off, sbi_sc_flash_buf, (int)len) != (int)len) { + err = SBI_ERR_FAILED; + } + sbi_scb_unlock(); + return err; +} + +static long sbi_vendor_sc_flash_read(unsigned long off, unsigned long buf, + unsigned long len) +{ + long err; + + if ((len == 0UL) || (len > SBI_SMODE_BUF_MAX) || + (off > (unsigned long)FLASH_DEVICE_SIZE) || + (len > (unsigned long)FLASH_DEVICE_SIZE - off)) { + return SBI_ERR_INVALID_PARAM; + } + if (!sbi_smode_range_ok(buf, len)) { + return SBI_ERR_INVALID_ADDRESS; + } + err = sbi_sc_flash_take(); + if (err != SBI_SUCCESS) { + return err; + } + if (ext_flash_read(off, sbi_sc_flash_buf, (int)len) != (int)len) { + err = SBI_ERR_FAILED; + } + else if (sbi_copy_to_smode(buf, sbi_sc_flash_buf, len) != 0) { + err = SBI_ERR_INVALID_ADDRESS; + } + sbi_scb_unlock(); + return err; +} + +/* Point directory entry 1 (upgrade) or 2 (blank) at an image offset. + * Setting the upgrade entry also zeroes the blank one, as the OS driver + * does, so a single-image System Controller does not object. Nothing is + * written when the entries already read that way. */ +static long sbi_vendor_sc_flash_set_image(unsigned long index, + unsigned long address) +{ + uint8_t *block = (uint8_t *)sbi_sc_flash_block; + long err; + + if (((index != SBI_SC_FLASH_UPGRADE_INDEX) && + (index != SBI_SC_FLASH_BLANK_INDEX)) || + (address > (unsigned long)FLASH_DEVICE_SIZE)) { + return SBI_ERR_INVALID_PARAM; + } + err = sbi_sc_flash_take(); + if (err != SBI_SUCCESS) { + return err; + } + if (ext_flash_read(0, block, FLASH_SECTOR_SIZE) != FLASH_SECTOR_SIZE) { + sbi_scb_unlock(); + return SBI_ERR_FAILED; + } + if ((sbi_sc_flash_block[index] == (uint32_t)address) && + ((index != SBI_SC_FLASH_UPGRADE_INDEX) || + (sbi_sc_flash_block[SBI_SC_FLASH_BLANK_INDEX] == 0U))) { + sbi_scb_unlock(); + return SBI_SUCCESS; + } + sbi_sc_flash_block[index] = (uint32_t)address; + if (index == SBI_SC_FLASH_UPGRADE_INDEX) { + sbi_sc_flash_block[SBI_SC_FLASH_BLANK_INDEX] = 0U; + } + if ((ext_flash_erase(0, FLASH_SECTOR_SIZE) != FLASH_SECTOR_SIZE) || + (ext_flash_write(0, block, FLASH_SECTOR_SIZE) != FLASH_SECTOR_SIZE)) { + err = SBI_ERR_FAILED; + } + sbi_scb_unlock(); + return err; +} +#endif /* MPFS_SBI_SC_FLASH */ + +static long sbi_vendor_sc_start(uint8_t opcode, uint32_t index, + const uint8_t *req, uint32_t req_len) +{ + if (!sbi_scb_trylock()) { + return SBI_ERR_FAILED; + } + if (mpfs_scb_start(opcode, index, req, req_len) != 0) { + sbi_scb_unlock(); + return SBI_ERR_FAILED; + } + __atomic_store_n(&sbi_scb_started, 1U, __ATOMIC_RELEASE); + return SBI_SUCCESS; +} + +/* The IAP program and auto-update services restart the device on success, + * so a return from here is the failure status. */ +static long sbi_vendor_sc_run(uint8_t opcode, uint32_t index, + unsigned long *val) +{ + long err = sbi_vendor_sc_start(opcode, index, NULL, 0); + + if (err != SBI_SUCCESS) { + return err; + } + *val = (unsigned long)mpfs_scb_wait(); + __atomic_store_n(&sbi_scb_started, 0U, __ATOMIC_RELEASE); + sbi_scb_unlock(); + return SBI_ERR_FAILED; +} +#endif + +long sbi_hal_vendor_ecall(unsigned long fid, unsigned long *regs, + unsigned long *val) +{ + unsigned long a0 = regs[SBI_REG_A0]; + unsigned long a1 = regs[SBI_REG_A1]; + unsigned long a2 = regs[SBI_REG_A2]; + long err; + + mpfs_scb_quiet = 1; + switch (fid) { + case SBI_WOLF_FID_GET_VERSION: + *val = SBI_WOLF_VERSION; + err = SBI_SUCCESS; + break; + case SBI_MCHP_FID_HSS_REBOOT: + SYSREG_MSS_RESET_CR = 0xDEAD; + while (1) { } + case SBI_WOLF_FID_GET_SERIAL: { + uint8_t serial[DEVICE_SERIAL_NUMBER_SIZE]; + int ret; + + if (a1 < sizeof(serial)) { + err = SBI_ERR_INVALID_PARAM; + break; + } + if (!sbi_smode_range_ok(a0, sizeof(serial))) { + err = SBI_ERR_INVALID_ADDRESS; + break; + } + if (!sbi_scb_trylock()) { + err = SBI_ERR_FAILED; + break; + } + ret = mpfs_read_serial_number(serial); + sbi_scb_unlock(); + if (ret != 0) { + err = SBI_ERR_FAILED; + break; + } + err = (sbi_copy_to_smode(a0, serial, sizeof(serial)) == 0) ? + SBI_SUCCESS : SBI_ERR_INVALID_ADDRESS; + break; + } + case SBI_WOLF_FID_GET_KEY: + err = sbi_vendor_get_key(a0, a1, a2); + break; +#ifdef MPFS_SCB_SMODE_DENY + case SBI_WOLF_FID_DIGEST_CHECK: { + uint8_t req[4]; + + req[0] = (uint8_t)a0; + req[1] = (uint8_t)(a0 >> 8); + req[2] = (uint8_t)(a0 >> 16); + req[3] = (uint8_t)(a0 >> 24); + err = sbi_vendor_sc_start(SYS_SERV_CMD_DIGEST_CHECK, 0, req, + sizeof(req)); + break; + } + case SBI_WOLF_FID_IAP_AUTHENTICATE: + err = sbi_vendor_sc_start(SYS_SERV_CMD_IAP_IMAGE_AUTH, + (uint32_t)a0, NULL, 0); + break; + case SBI_WOLF_FID_IAP_VERIFY: + err = sbi_vendor_sc_start(SYS_SERV_CMD_IAP_VERIFY_IDX, + (uint32_t)a0, NULL, 0); + break; + case SBI_WOLF_FID_IAP_PROGRAM: + err = sbi_vendor_sc_run(SYS_SERV_CMD_IAP_PROGRAM_IDX, (uint32_t)a0, + val); + break; + case SBI_WOLF_FID_AUTO_UPDATE: + err = sbi_vendor_sc_run(SYS_SERV_CMD_AUTO_UPDATE, 0, val); + break; +#ifdef MPFS_SBI_SC_FLASH + case SBI_WOLF_FID_SC_FLASH_ERASE: + err = sbi_vendor_sc_flash_erase(a0, a1); + break; + case SBI_WOLF_FID_SC_FLASH_WRITE: + err = sbi_vendor_sc_flash_write(a0, a1, a2); + break; + case SBI_WOLF_FID_SC_FLASH_READ: + err = sbi_vendor_sc_flash_read(a0, a1, a2); + break; + case SBI_WOLF_FID_SC_FLASH_SET_IMAGE: + err = sbi_vendor_sc_flash_set_image(a0, a1); + break; +#endif + case SBI_WOLF_FID_SC_STATUS: + if (mpfs_scb_mailbox_busy()) { + err = SBI_ERR_ALREADY_STARTED; + break; + } + *val = (unsigned long)mpfs_scb_status() | + ((unsigned long)SCBMBOX_REG(4) << 32); + /* A synchronous service also leaves the mailbox idle between its + * own requests; release only the lock a started service holds. */ + if (__atomic_exchange_n(&sbi_scb_started, 0U, __ATOMIC_ACQ_REL) + != 0U) { + sbi_scb_unlock(); + } + err = SBI_SUCCESS; + break; +#else + case SBI_WOLF_FID_DIGEST_CHECK: + case SBI_WOLF_FID_IAP_AUTHENTICATE: + case SBI_WOLF_FID_IAP_VERIFY: + case SBI_WOLF_FID_IAP_PROGRAM: + case SBI_WOLF_FID_AUTO_UPDATE: + case SBI_WOLF_FID_SC_FLASH_ERASE: + case SBI_WOLF_FID_SC_FLASH_WRITE: + case SBI_WOLF_FID_SC_FLASH_READ: + case SBI_WOLF_FID_SC_FLASH_SET_IMAGE: + case SBI_WOLF_FID_SC_STATUS: + err = SBI_ERR_DENIED; + break; +#endif + default: + err = SBI_ERR_NOT_SUPPORTED; + break; + } + mpfs_scb_quiet = 0; + (void)a2; + return err; +} +#endif /* MPFS_SBI_VENDOR && WOLFBOOT_MMODE_SMODE_BOOT */ + /* Read 16-byte device serial number via SCB system service (opcode 0x00). */ int mpfs_read_serial_number(uint8_t *serial) { @@ -1576,7 +2031,7 @@ static int mpfs_scb_wait_clear(uint32_t reg_offset, uint32_t mask, return (timeout == 0) ? -1 : 0; } -#ifdef EXT_FLASH +#if defined(EXT_FLASH) || defined(MPFS_SBI_SC_FLASH) /* ========================================================================== * QSPI Flash Controller Implementation * @@ -2323,7 +2778,7 @@ static int test_ext_flash(void) } #endif /* TEST_EXT_FLASH */ -#else /* !EXT_FLASH */ +#else /* !EXT_FLASH && !MPFS_SBI_SC_FLASH */ /* Stubs for when QSPI is disabled */ void ext_flash_lock(void) diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 5dd9c6869e..379e0bb7cb 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -227,6 +227,7 @@ /* Control Register bits */ #define SERVICES_CR_REQ_MASK 0x01u +#define SERVICES_CR_MBOX_SHIFT 7 /* mailbox word offset, or the SPI index */ #define SERVICES_CR_COMMAND_SHIFT 16 /* Status Register bits */ @@ -242,6 +243,16 @@ #define SYS_SERV_CMD_SNVM_READ 0x18u #define SYS_SERV_CMD_PUF_EMULATION 0x20u #define SYS_SERV_CMD_NONCE 0x21u +/* Bitstream/IAP services. The SPI directory index of 0x22/0x42/0x44 rides + * in the SERVICES_CR mailbox-offset field, not in the mailbox. */ +#define SYS_SERV_CMD_IAP_IMAGE_AUTH 0x22u /* index in CR[14:7] */ +#define SYS_SERV_CMD_BITSTREAM_AUTH 0x23u /* SPI address in mailbox */ +#define SYS_SERV_CMD_IAP_PROGRAM_IDX 0x42u /* restarts the device on success */ +#define SYS_SERV_CMD_IAP_PROGRAM_ADDR 0x43u +#define SYS_SERV_CMD_IAP_VERIFY_IDX 0x44u +#define SYS_SERV_CMD_IAP_VERIFY_ADDR 0x45u +#define SYS_SERV_CMD_AUTO_UPDATE 0x46u /* restarts the device on success */ +#define SYS_SERV_CMD_DIGEST_CHECK 0x47u /* options word in, DIGESTERR at byte 4 */ /* sNVM service parameters (see Microchip system services spec) */ #define MPFS_SNVM_MODULE_MAX 221 /* modules 0..220 */ @@ -417,6 +428,16 @@ typedef struct { /* DTIM bytes hal_init() zeroes at boot (SBI shared block + hart mailboxes). */ #define MPFS_DTIM_BOOT_CLEAR_SIZE 0x200U + +/* Per-hart M-mode trap stacks (src/riscv_sbi.c, src/vector_riscv.S): one + * 4 KB (1 << SBI_MSTACK_SHIFT) stack per hart in the OS-invisible reserved + * DDR (hss-buffer@103fc00000, nomap in the stock dtb); hart h's top is + * SBI_MSTACK_BASE + (h + 1) << SBI_MSTACK_SHIFT. No UL suffix: used from + * assembly. */ +#ifndef SBI_MSTACK_BASE +#define SBI_MSTACK_BASE 0x103FC00000 +#endif +#define SBI_MSTACK_SHIFT 12 /* DTIM pair (count, ~count) just above the cleared block, so it survives an * MSS reset: DDR training restarts by reset since the last accepted training. */ #define MPFS_DTIM_DDR_RESET_CNT_ADDR (0x01000000UL + MPFS_DTIM_BOOT_CLEAR_SIZE) @@ -751,7 +772,7 @@ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); #endif #endif /* __ASSEMBLER__ */ -#ifdef EXT_FLASH +#if defined(EXT_FLASH) || defined(MPFS_SBI_SC_FLASH) /* QSPI Flash Controller * * Two CoreQSPI v2 controllers with identical register layouts: diff --git a/hal/mpfs250_snvm.c b/hal/mpfs250_snvm.c index 541522fcd2..d27a0b82d0 100644 --- a/hal/mpfs250_snvm.c +++ b/hal/mpfs250_snvm.c @@ -285,35 +285,28 @@ int snvm_keystore_provision(void) #endif /* SNVM_KEYSTORE_PROVISION && !WOLFBOOT_NO_SIGN */ -#ifdef SNVM_KEK - -/* Fixed PUF challenge. Any constant works: the same challenge returns the - * same device-unique response across cold boots, anchoring a stable KEK. */ -static const uint8_t snvm_kek_challenge[MPFS_PUF_CHALLENGE_LEN] = { - 0x77, 0x6f, 0x6c, 0x66, 0x42, 0x6f, 0x6f, 0x74, - 0x4b, 0x45, 0x4b, 0x76, 0x31, 0x00, 0x00, 0x00 -}; -/* Domain-separation label hashed with the PUF response. */ -static const char snvm_kek_label[] = "wolfBoot-sNVM-KEK-v1"; +#if defined(SNVM_KEK) || defined(MPFS_SBI_VENDOR) -int mpfs_puf_kek(uint8_t *kek) +/* out = SHA384(label || PUF(challenge))[0..31]: a device-unique key bound to + * the label and the challenge. The label is hashed without its terminator. */ +int mpfs_puf_derive(const char *label, const uint8_t *challenge, uint8_t *out) { uint8_t resp[MPFS_PUF_RESPONSE_LEN]; uint8_t digest[WC_SHA384_DIGEST_SIZE]; wc_Sha384 sha; int ret; - if (kek == NULL) { + if ((label == NULL) || (challenge == NULL) || (out == NULL)) { return -1; } - ret = mpfs_puf_emulation(snvm_kek_challenge, 0, resp); + ret = mpfs_puf_emulation(challenge, 0, resp); if (ret != 0) { return -1; } ret = wc_InitSha384(&sha); if (ret == 0) { - ret = wc_Sha384Update(&sha, (const byte *)snvm_kek_label, - (word32)(sizeof(snvm_kek_label) - 1)); + ret = wc_Sha384Update(&sha, (const byte *)label, + (word32)strlen(label)); if (ret == 0) { ret = wc_Sha384Update(&sha, resp, (word32)sizeof(resp)); } @@ -324,15 +317,60 @@ int mpfs_puf_kek(uint8_t *kek) /* the context buffered the whole sub-block message, PUF response included */ wc_ForceZero(&sha, sizeof(sha)); } - /* KEK = first 256 bits of SHA-384(label || PUF response). */ if (ret == 0) { - memcpy(kek, digest, SNVM_KEK_LEN); + memcpy(out, digest, MPFS_PUF_DERIVED_LEN); } wc_ForceZero(resp, sizeof(resp)); wc_ForceZero(digest, sizeof(digest)); return (ret == 0) ? 0 : -1; } +int mpfs_puf_purpose_key(const char *seed, const char *label, uint8_t *out) +{ + uint8_t digest[WC_SHA384_DIGEST_SIZE]; + uint8_t challenge[MPFS_PUF_CHALLENGE_LEN]; + wc_Sha384 sha; + int ret; + + if ((seed == NULL) || (label == NULL) || (out == NULL)) { + return -1; + } + ret = wc_InitSha384(&sha); + if (ret == 0) { + ret = wc_Sha384Update(&sha, (const byte *)seed, (word32)strlen(seed)); + if (ret == 0) { + ret = wc_Sha384Final(&sha, digest); + } + wc_Sha384Free(&sha); + } + if (ret != 0) { + return -1; + } + memcpy(challenge, digest, sizeof(challenge)); + ret = mpfs_puf_derive(label, challenge, out); + wc_ForceZero(digest, sizeof(digest)); + wc_ForceZero(challenge, sizeof(challenge)); + return ret; +} +#endif /* SNVM_KEK || MPFS_SBI_VENDOR */ + +#ifdef SNVM_KEK + +/* Fixed PUF challenge. Any constant works: the same challenge returns the + * same device-unique response across cold boots, anchoring a stable KEK. */ +static const uint8_t snvm_kek_challenge[MPFS_PUF_CHALLENGE_LEN] = { + 0x77, 0x6f, 0x6c, 0x66, 0x42, 0x6f, 0x6f, 0x74, + 0x4b, 0x45, 0x4b, 0x76, 0x31, 0x00, 0x00, 0x00 +}; +/* Domain-separation label hashed with the PUF response. */ +static const char snvm_kek_label[] = "wolfBoot-sNVM-KEK-v1"; + +int mpfs_puf_kek(uint8_t *kek) +{ + return mpfs_puf_derive(snvm_kek_label, snvm_kek_challenge, kek); +} + + int snvm_kek_wrap(const uint8_t *key, uint32_t keysz, uint8_t *out, uint32_t outsz) { diff --git a/hal/mpfs250_snvm.h b/hal/mpfs250_snvm.h index 3e5bb2892e..7b173801fa 100644 --- a/hal/mpfs250_snvm.h +++ b/hal/mpfs250_snvm.h @@ -138,6 +138,15 @@ int snvm_enckey_provision(void); #endif /* SNVM_KEK */ +#if defined(SNVM_KEK) || defined(MPFS_SBI_VENDOR) +#define MPFS_PUF_DERIVED_LEN 32 +/* out[32] = SHA384(label || PUF(challenge[16]))[0..31]; the primitive behind + * the KEK and the keys served to the OS over SBI. */ +int mpfs_puf_derive(const char *label, const uint8_t *challenge, uint8_t *out); +/* The same with challenge = SHA384(seed)[0..15]. */ +int mpfs_puf_purpose_key(const char *seed, const char *label, uint8_t *out); +#endif + #if defined(SNVM_ENCKEY_PROVISION) && !defined(SNVM_KEK) #error "SNVM_ENCKEY_PROVISION needs SNVM_KEK (the key is wrapped with the PUF KEK)" #endif diff --git a/include/riscv_sbi.h b/include/riscv_sbi.h new file mode 100644 index 0000000000..59c22683f2 --- /dev/null +++ b/include/riscv_sbi.h @@ -0,0 +1,97 @@ +/* riscv_sbi.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* Interface between the M-mode SBI runtime (src/riscv_sbi.c) and a platform + * HAL that serves the vendor extension. */ + +#ifndef RISCV_SBI_H +#define RISCV_SBI_H + +#include + +#define SBI_SUCCESS 0L +#define SBI_ERR_FAILED (-1L) +#define SBI_ERR_NOT_SUPPORTED (-2L) +#define SBI_ERR_INVALID_PARAM (-3L) +#define SBI_ERR_DENIED (-4L) +#define SBI_ERR_INVALID_ADDRESS (-5L) +#define SBI_ERR_ALREADY_AVAIL (-6L) +#define SBI_ERR_ALREADY_STARTED (-7L) + +/* Register-frame indices of the argument registers (regs[N] == xN). */ +#define SBI_REG_A0 10 +#define SBI_REG_A1 11 +#define SBI_REG_A2 12 + +/* Vendor extension: SBI_EXT_VENDOR_START | Microchip mvendorid 0x029, the + * EID the Hart Software Services use, so an OS probes one number for both + * firmwares and tells them apart with SBI_WOLF_FID_GET_VERSION. */ +#define SBI_EXT_VENDOR_MCHP 0x09000029UL + +/* HSS function IDs on that extension. Only HSS_REBOOT is served here (as a + * cold reset); the IHC, remoteproc, crypto and IPC calls are not. */ +#define SBI_MCHP_FID_HSS_REBOOT 0x10UL + +/* wolfBoot function IDs. a0..a2 carry the arguments, a0 returns the SBI + * error, a1 the value. S-mode buffers are passed as addresses in the + * caller's address space. */ +#define SBI_WOLF_FID_GET_VERSION 0x40UL /* -> a1 = SBI_WOLF_VERSION */ +#define SBI_WOLF_FID_GET_SERIAL 0x41UL /* (buf, len>=16) */ +#define SBI_WOLF_FID_GET_KEY 0x42UL /* (purpose, buf, len==32), once per boot */ +#define SBI_WOLF_FID_DIGEST_CHECK 0x43UL /* (options) start, poll SC_STATUS */ +#define SBI_WOLF_FID_IAP_AUTHENTICATE 0x44UL /* (spi index) start, poll SC_STATUS */ +#define SBI_WOLF_FID_IAP_VERIFY 0x45UL /* (spi index) start, poll SC_STATUS */ +#define SBI_WOLF_FID_IAP_PROGRAM 0x46UL /* (spi index) the device restarts on success */ +#define SBI_WOLF_FID_AUTO_UPDATE 0x47UL /* () the device restarts on success */ +/* System Controller SPI flash, fenced builds with MPFS_SBI_SC_FLASH only: + * offsets are flash offsets, the first erase block (the SPI directory) is + * reachable only through SET_IMAGE. */ +#define SBI_WOLF_FID_SC_FLASH_ERASE 0x48UL /* (offset, len == one erase block) */ +#define SBI_WOLF_FID_SC_FLASH_WRITE 0x49UL /* (offset, buf, len <= SBI_SMODE_BUF_MAX) */ +#define SBI_WOLF_FID_SC_FLASH_READ 0x4AUL /* (offset, buf, len <= SBI_SMODE_BUF_MAX) */ +#define SBI_WOLF_FID_SC_FLASH_SET_IMAGE 0x4BUL /* (directory index 1|2, image offset) */ +#define SBI_WOLF_FID_SC_STATUS 0x4FUL /* -> a1 = status | (mailbox word 1 << 32) */ + +#define SBI_WOLF_VERSION 1UL + +/* GET_KEY purposes: each names a fixed label, so the caller never chooses + * what the PUF is asked. */ +#define SBI_WOLF_KEY_MRAM_LOG 1UL /* "wolfBoot-mram-log-v1" */ +#define SBI_WOLF_KEY_VARLOG 2UL /* "wolfLUKS-key-v1:varlog" (the earlier user-space derivation) */ +#define SBI_WOLF_KEY_LEN 32UL + +/* Buffers an S-mode caller hands to the vendor calls are bounded. */ +#define SBI_SMODE_BUF_MAX 4096UL + +/* Platform hook serving SBI_EXT_VENDOR_MCHP. regs is the trap frame + * (SBI_REG_A0..A2 in, written back by the caller), *val the a1 result. + * Weak default: SBI_ERR_NOT_SUPPORTED. */ +long sbi_hal_vendor_ecall(unsigned long fid, unsigned long *regs, + unsigned long *val); + +/* Copies between an S-mode address (translated like the trapped context) + * and M-mode memory. 0 on success, -1 when the access faulted or the range + * is not acceptable (sbi_smode_range_ok). */ +int sbi_smode_range_ok(unsigned long addr, unsigned long len); +int sbi_copy_from_smode(uint8_t *dst, unsigned long src, unsigned long len); +int sbi_copy_to_smode(unsigned long dst, const uint8_t *src, unsigned long len); + +#endif /* RISCV_SBI_H */ diff --git a/options.mk b/options.mk index 96d962fc65..7183804ba0 100644 --- a/options.mk +++ b/options.mk @@ -125,6 +125,32 @@ ifeq ($(MPFS_SCB_SMODE_DENY),1) -DWOLFBOOT_PMP_DENY_SIZE=0x1000UL endif +# SBI vendor extension (EID 0x09000029, the HSS one) serving the OS: firmware version, device +# serial, PUF keys by purpose, System Controller services behind the mailbox fence. M-mode only. +ifeq ($(MPFS_SBI_VENDOR),1) + ifeq ($(WOLFBOOT_TARGET_BUILD),1) + ifneq ($(RISCV_MMODE),1) + $(error MPFS_SBI_VENDOR=1 needs the M-mode target (RISCV_MMODE=1)) + endif + endif + CFLAGS+=-D"MPFS_SBI_VENDOR" + ifneq ($(HASH),SHA384) + AUX_HASH_ALGOS+=sha384 + endif +endif + +# System Controller SPI flash erase/write/read for the OS over the vendor extension. Needs the +# mailbox fence, and compiles the SC CoreQSPI driver into a build that boots from elsewhere. +ifeq ($(MPFS_SBI_SC_FLASH),1) + ifneq ($(MPFS_SBI_VENDOR)$(MPFS_SCB_SMODE_DENY),11) + $(error MPFS_SBI_SC_FLASH=1 requires MPFS_SBI_VENDOR=1 and MPFS_SCB_SMODE_DENY=1) + endif + ifeq ($(EXT_FLASH),1) + $(error MPFS_SBI_SC_FLASH=1 is for a build that does not boot from the SC flash (EXT_FLASH=0)) + endif + CFLAGS+=-D"MPFS_SBI_SC_FLASH" -D"MPFS_SC_SPI" +endif + # Field recovery: before the golden slot boots, zero the sNVM image-key page and the head of both # operational slots (hal_disk_failover). The golden image must be a plaintext signed image. ifeq ($(DISK_FAILOVER_ZEROIZE),1) diff --git a/src/riscv_sbi.c b/src/riscv_sbi.c index 6e9be57925..e661b26d4e 100644 --- a/src/riscv_sbi.c +++ b/src/riscv_sbi.c @@ -39,6 +39,9 @@ #if defined(WOLFBOOT_RISCV_MMODE) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +/* The SBI error codes and the vendor-extension IDs are generic. */ +#include "riscv_sbi.h" + #ifdef TARGET_mpfs250 #include "hal/mpfs250.h" #endif @@ -80,13 +83,7 @@ #define MIP_SSIP (1UL << IRQ_S_SOFT) /* bit 1 */ #define MIE_MTIP (1UL << IRQ_M_TIMER) /* bit 7 */ -/* ---- SBI constants ------------------------------------------------------ */ -#define SBI_SUCCESS 0L -#define SBI_ERR_FAILED (-1L) -#define SBI_ERR_NOT_SUPPORTED (-2L) -#define SBI_ERR_INVALID_PARAM (-3L) -#define SBI_ERR_ALREADY_AVAIL (-6L) - +/* ---- SBI constants (error codes in riscv_sbi.h) ------------------------- */ /* Extension IDs (EIDs) */ #define SBI_EXT_0_1_SET_TIMER 0x00 #define SBI_EXT_0_1_CONSOLE_PUTCHAR 0x01 @@ -230,7 +227,16 @@ void sbi_hart_mark_started(unsigned long hartid) #ifndef SBI_MSTACK_BASE #define SBI_MSTACK_BASE 0x103FC00000UL #endif -#define SBI_MSTACK_SIZE 4096UL +#ifndef SBI_MSTACK_SHIFT +#define SBI_MSTACK_SHIFT 12 +#endif +#define SBI_MSTACK_SIZE (1UL << SBI_MSTACK_SHIFT) + +/* Remote-fence completion wait, in CLINT ticks (1 MHz on PolarFire). */ +#ifndef RTC_CLOCK_FREQ +#define RTC_CLOCK_FREQ 1000000UL +#endif +#define SBI_IPI_WAIT_TICKS ((uint64_t)RTC_CLOCK_FREQ) /* Arm this hart's M-mode trap stack; call just before entering S-mode. */ void sbi_mscratch_init(unsigned long hartid) @@ -241,19 +247,151 @@ void sbi_mscratch_init(unsigned long hartid) } } -/* Copy bytes from an S-mode pointer (virtual once paging is on) into an - * M-mode buffer using the MPRV trick: with mstatus.MPRV set and MPP=S - * (true inside a trap taken from S-mode), M-mode loads are translated - * exactly like S-mode accesses. */ -static void sbi_copy_from_smode(uint8_t *dst, unsigned long src, - unsigned long len) +/* Accesses to the trapped context's memory use the MPRV trick: with + * mstatus.MPRV set and MPP=S (true inside a trap taken from S-mode), M-mode + * loads and stores are translated exactly like S-mode accesses. A bad + * address then faults in M-mode, so for the byte below mtvec is pointed at + * sbi_mprv_trap, which records the fault for this hart, steps over the + * access and returns; the default vector would treat the fault as fatal and + * park the hart. */ +volatile unsigned long sbi_mprv_fault[MPFS_NUM_HARTS]; + +__asm__( + ".pushsection .text\n" + ".align 2\n" + ".type sbi_mprv_trap, @function\n" + "sbi_mprv_trap:\n" + " csrr t0, mepc\n" + " addi t0, t0, 4\n" + " csrw mepc, t0\n" + " li t0, 0x20000\n" + " csrc mstatus, t0\n" + " la t0, sbi_mprv_fault\n" + " csrr t1, mhartid\n" + " slli t1, t1, 3\n" + " add t0, t0, t1\n" + " li t1, 1\n" + " sd t1, 0(t0)\n" + " mret\n" + ".size sbi_mprv_trap, .-sbi_mprv_trap\n" + ".popsection\n" +); + +static unsigned long sbi_hartid(void) +{ + unsigned long id; + __asm__ volatile("csrr %0, mhartid" : "=r"(id)); + return id; +} + +static int sbi_mprv_lb(unsigned long a, uint8_t *v) +{ + unsigned long mtvec_save, mstatus_save, tmp, val; + unsigned long hart = sbi_hartid(); + + sbi_mprv_fault[hart] = 0; + __asm__ volatile( + "la %[tmp], sbi_mprv_trap\n" + "csrrw %[mtvec], mtvec, %[tmp]\n" + "csrrs %[mstatus], mstatus, %[mprv]\n" + ".option push\n" + ".option norvc\n" + "lbu %[val], 0(%[addr])\n" + ".option pop\n" + "csrw mstatus, %[mstatus]\n" + "csrw mtvec, %[mtvec]\n" + : [mtvec] "=&r"(mtvec_save), [mstatus] "=&r"(mstatus_save), + [tmp] "=&r"(tmp), [val] "=&r"(val) + : [addr] "r"(a), [mprv] "r"(MSTATUS_MPRV_BIT) + : "memory", "t0", "t1"); + *v = (uint8_t)val; + return (sbi_mprv_fault[hart] != 0UL) ? -1 : 0; +} + +static int sbi_mprv_sb(unsigned long a, uint8_t v) +{ + unsigned long mtvec_save, mstatus_save, tmp; + unsigned long hart = sbi_hartid(); + + sbi_mprv_fault[hart] = 0; + __asm__ volatile( + "la %[tmp], sbi_mprv_trap\n" + "csrrw %[mtvec], mtvec, %[tmp]\n" + "csrrs %[mstatus], mstatus, %[mprv]\n" + ".option push\n" + ".option norvc\n" + "sb %[val], 0(%[addr])\n" + ".option pop\n" + "csrw mstatus, %[mstatus]\n" + "csrw mtvec, %[mtvec]\n" + : [mtvec] "=&r"(mtvec_save), [mstatus] "=&r"(mstatus_save), + [tmp] "=&r"(tmp) + : [addr] "r"(a), [val] "r"((unsigned long)v), + [mprv] "r"(MSTATUS_MPRV_BIT) + : "memory", "t0", "t1"); + return (sbi_mprv_fault[hart] != 0UL) ? -1 : 0; +} + +/* A caller's buffer must be bounded and, with paging off (early boot), + * must be DDR: an M-mode access to a device window would not fault the way + * a bad translation does. */ +#if !defined(DDR_BASE_CACHED) || !defined(DDR_SIZE) +#error "sbi_smode_range_ok needs DDR_BASE_CACHED and DDR_SIZE" +#endif +int sbi_smode_range_ok(unsigned long addr, unsigned long len) +{ + unsigned long satp; + + if ((len == 0UL) || (len > SBI_SMODE_BUF_MAX) || (addr + len < addr)) { + return 0; + } + __asm__ volatile("csrr %0, satp" : "=r"(satp)); + if ((satp >> 60) == 0UL) { + if ((addr >= DDR_BASE_CACHED) && + (addr + len <= DDR_BASE_CACHED + 0x40000000UL)) { + return 1; + } + if ((addr >= 0x1000000000UL) && + (addr + len <= 0x1000000000UL + DDR_SIZE)) { + return 1; + } + return 0; + } + return 1; +} + +int sbi_copy_from_smode(uint8_t *dst, unsigned long src, unsigned long len) { unsigned long i; - csr_set_bits(mstatus, MSTATUS_MPRV_BIT); + for (i = 0; i < len; i++) { - dst[i] = ((const volatile uint8_t *)src)[i]; + if (sbi_mprv_lb(src + i, &dst[i]) != 0) { + return -1; + } } - csr_clr_bits(mstatus, MSTATUS_MPRV_BIT); + return 0; +} + +int sbi_copy_to_smode(unsigned long dst, const uint8_t *src, unsigned long len) +{ + unsigned long i; + + for (i = 0; i < len; i++) { + if (sbi_mprv_sb(dst + i, src[i]) != 0) { + return -1; + } + } + return 0; +} + +/* Platform hook for the vendor extension; weak default declines. */ +long __attribute__((weak)) sbi_hal_vendor_ecall(unsigned long fid, + unsigned long *regs, unsigned long *val) +{ + (void)fid; + (void)regs; + (void)val; + return SBI_ERR_NOT_SUPPORTED; } #define MSTATUS_MXR_BIT (1UL << 19) @@ -604,7 +742,7 @@ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, { unsigned long i; unsigned long h; - uint32_t spin; + uint64_t deadline; long err = SBI_SUCCESS; /* SBI v0.2: hart_mask_base == -1 selects all harts (see sbi_post_ipi). */ if (base == (unsigned long)-1) { @@ -622,17 +760,19 @@ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, } /* A target can be inside a console write or another ecall for tens * of milliseconds; a fence that gives up early leaves the OS with - * stale TLBs on that hart, so wait well past that (about 1 s). - * M-mode interrupts are masked in here, so service any fence the - * target (or a third hart) posts to us meanwhile: two harts fencing - * each other would otherwise each wait for the other and time out. */ - spin = 400000000U; + * stale TLBs on that hart, so wait well past that: 1 s of CLINT + * time, the one clock every hart shares. A hart that never answers + * then costs the caller a second, not the OS its interrupts for + * minutes. M-mode interrupts are masked in here, so service any + * fence the target (or a third hart) posts to us meanwhile: two + * harts fencing each other would otherwise each wait for the other + * and time out. */ + deadline = CLINT_MTIME + SBI_IPI_WAIT_TICKS; while ((int32_t)(__atomic_load_n(&sbi_ipi_done[h], __ATOMIC_ACQUIRE) - - ticket[h]) < 0 && spin > 0U) { + ticket[h]) < 0 && (int64_t)(CLINT_MTIME - deadline) < 0) { if (sbi_ipi_ops[self] != 0U) { sbi_ipi_irq(self); } - spin--; } if ((int32_t)(__atomic_load_n(&sbi_ipi_done[h], __ATOMIC_ACQUIRE) - ticket[h]) < 0) { @@ -723,6 +863,9 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) case SBI_EXT_HSM: case SBI_EXT_SRST: case SBI_EXT_DBCN: +#ifdef MPFS_SBI_VENDOR + case SBI_EXT_VENDOR_MCHP: +#endif val = 1; break; default: val = 0; break; @@ -834,7 +977,10 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) if (c > sizeof(cbuf)) { c = sizeof(cbuf); } - sbi_copy_from_smode(cbuf, gp + k, c); + if (sbi_copy_from_smode(cbuf, gp + k, c) != 0) { + err = SBI_ERR_INVALID_ADDRESS; + break; + } for (j = 0; j < c; j++) { if (!sbi_putc((char)cbuf[j])) { k = n; @@ -857,6 +1003,12 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) } break; +#ifdef MPFS_SBI_VENDOR + case SBI_EXT_VENDOR_MCHP: + err = sbi_hal_vendor_ecall(fid, regs, &val); + break; +#endif + case SBI_EXT_SRST: /* system_reset(type, reason). No console output here: by now the OS * owns the UARTs and a blocked printf leaves the hart stuck in the @@ -898,7 +1050,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) /* a0 = S-mode pointer to a hart mask; fetch via MPRV. */ if (regs[A0] != 0UL) { unsigned long lmask = 0; - sbi_copy_from_smode((uint8_t *)&lmask, regs[A0], + (void)sbi_copy_from_smode((uint8_t *)&lmask, regs[A0], sizeof(unsigned long)); sbi_post_ipi(lmask, 0, SBI_IPI_OP_SSIP, hartid, ticket); } @@ -911,7 +1063,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) __asm__ volatile("sfence.vma" ::: "memory"); if (regs[A0] != 0UL) { unsigned long fmask = 0; - sbi_copy_from_smode((uint8_t *)&fmask, regs[A0], + (void)sbi_copy_from_smode((uint8_t *)&fmask, regs[A0], sizeof(unsigned long)); sbi_post_ipi(fmask, 0, (eid == SBI_EXT_0_1_REMOTE_FENCE_I) ? diff --git a/src/vector_riscv.S b/src/vector_riscv.S index 5c45d1ec76..86ce97d190 100644 --- a/src/vector_riscv.S +++ b/src/vector_riscv.S @@ -24,6 +24,14 @@ #ifdef TARGET_mpfs250 #include "hal/mpfs250.h" #endif +/* Per-hart M-mode trap stacks (src/riscv_sbi.c); the platform header may + * place them elsewhere. */ +#ifndef SBI_MSTACK_BASE +#define SBI_MSTACK_BASE 0x103FC00000 +#endif +#ifndef SBI_MSTACK_SHIFT +#define SBI_MSTACK_SHIFT 12 +#endif /* ============================================================================ * Trap Entry/Exit Macros @@ -82,19 +90,32 @@ STORE x31, 31 * REGBYTES(sp) #ifdef WOLFBOOT_RISCV_MMODE - /* Frame slot 0 (x0's unused slot) = mscratch at entry: the trapped - * sp when we switched to the M-stack, 0 for an M-mode fallback or a - * nested trap. Zero mscratch while inside the handler so a nested - * trap keeps descending on the CURRENT stack instead of reusing the - * consumed mscratch value as a stack (which caused an infinite - * trap-in-trap recursion). Slot 2 = the trapped sp. x1 is already - * saved in slot 1, so reuse it as a scratch register. */ - csrr x1, mscratch - STORE x1, 0 * REGBYTES(sp) - csrw mscratch, x0 - bnez x1, 101f - addi x1, sp, 32 * REGBYTES + /* Slot 2 = the trapped sp: what mscratch holds after the switch above + * (any value, 0 included), or sp + frame when no switch happened. + * Slot 0 (x0's unused slot) = 1 when this frame switched to the + * M-stack, which is the case exactly when it sits at this hart's + * M-stack top; 0 for an M-mode fallback or a nested trap. The flag + * is derived from the frame address, never from the trapped sp: a + * context trapping with sp == 0 must still re-arm mscratch on exit + * (trap_exit), or the next trap from S-mode runs the entry on an + * untranslated virtual sp and faults forever. Zero mscratch while + * inside the handler so a nested trap keeps descending on the CURRENT + * stack. x1, t0 and t1 are already saved, so they serve as scratch. */ + csrr t0, mhartid + addi t0, t0, 1 + slli t0, t0, SBI_MSTACK_SHIFT + li t1, SBI_MSTACK_BASE + add t0, t0, t1 + addi t1, sp, 32 * REGBYTES + csrrw x1, mscratch, x0 + bne t1, t0, 101f + li t0, 1 + STORE t0, 0 * REGBYTES(sp) + j 102f 101: + STORE x0, 0 * REGBYTES(sp) + mv x1, t1 +102: STORE x1, 2 * REGBYTES(sp) /* The M-mode handler's C code addresses small globals gp-relative: diff --git a/tools/linux/wolfboot-sbi/Makefile b/tools/linux/wolfboot-sbi/Makefile new file mode 100644 index 0000000000..e2cfd817e6 --- /dev/null +++ b/tools/linux/wolfboot-sbi/Makefile @@ -0,0 +1,14 @@ +# Out-of-tree build of the wolfboot-sbi module against a kernel build tree: +# make KDIR=/path/to/kernel/build ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- +obj-m := wolfboot_sbi.o + +KDIR ?= /lib/modules/$(shell uname -r)/build + +all: + $(MAKE) -C $(KDIR) M=$(CURDIR) modules + +modules_install: + $(MAKE) -C $(KDIR) M=$(CURDIR) modules_install + +clean: + $(MAKE) -C $(KDIR) M=$(CURDIR) clean diff --git a/tools/linux/wolfboot-sbi/wolfboot_sbi.c b/tools/linux/wolfboot-sbi/wolfboot_sbi.c new file mode 100644 index 0000000000..7d2a55229f --- /dev/null +++ b/tools/linux/wolfboot-sbi/wolfboot_sbi.c @@ -0,0 +1,352 @@ +/* wolfboot_sbi.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * This Linux kernel module is licensed under version 2 or later of the GNU + * General Public License, rather than version 3 or later as the rest of + * wolfBoot is, so that its license matches the MODULE_LICENSE("GPL") the + * kernel it loads into requires. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* Linux front end for the wolfBoot SBI vendor extension on PolarFire SoC. + * + * /dev/wolfboot (root only) exposes what the M-mode wolfBoot serves on the + * Microchip vendor extension: the firmware version, the device serial + * number, PUF-derived keys by purpose, and the System Controller digest, + * IAP and auto-update services when the hardware mailbox is fenced off + * from S-mode. Nothing here touches the mailbox registers, so the + * mainline System Controller drivers keep working in an unfenced build. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "wolfboot_sbi.h" + +#define SBI_EXT_VENDOR_MCHP 0x09000029UL + +#define SBI_MCHP_FID_HSS_REBOOT 0x10UL +#define SBI_WOLF_FID_GET_VERSION 0x40UL +#define SBI_WOLF_FID_GET_SERIAL 0x41UL +#define SBI_WOLF_FID_GET_KEY 0x42UL +#define SBI_WOLF_FID_DIGEST_CHECK 0x43UL +#define SBI_WOLF_FID_IAP_AUTHENTICATE 0x44UL +#define SBI_WOLF_FID_IAP_VERIFY 0x45UL +#define SBI_WOLF_FID_IAP_PROGRAM 0x46UL +#define SBI_WOLF_FID_AUTO_UPDATE 0x47UL +#define SBI_WOLF_FID_SC_FLASH_ERASE 0x48UL +#define SBI_WOLF_FID_SC_FLASH_WRITE 0x49UL +#define SBI_WOLF_FID_SC_FLASH_READ 0x4AUL +#define SBI_WOLF_FID_SC_FLASH_SET_IMAGE 0x4BUL +#define SBI_WOLF_FID_SC_STATUS 0x4FUL + +#define SBI_WOLF_VERSION_MIN 1UL + +/* asm/sbi.h has these from 6.9 on */ +#ifndef SBI_ERR_DENIED +#define SBI_ERR_DENIED (-4) +#endif +#ifndef SBI_ERR_INVALID_ADDRESS +#define SBI_ERR_INVALID_ADDRESS (-5) +#endif +#ifndef SBI_ERR_ALREADY_STARTED +#define SBI_ERR_ALREADY_STARTED (-7) +#endif + +static DEFINE_MUTEX(wolfboot_sbi_lock); + +static int wolfboot_sbi_errno(long sbi_err) +{ + switch (sbi_err) { + case SBI_SUCCESS: + return 0; + case SBI_ERR_NOT_SUPPORTED: + return -EOPNOTSUPP; + case SBI_ERR_INVALID_PARAM: + return -EINVAL; + case SBI_ERR_DENIED: + return -EPERM; + case SBI_ERR_INVALID_ADDRESS: + return -EFAULT; + case SBI_ERR_ALREADY_STARTED: + return -EBUSY; + default: + return -EIO; + } +} + +static long wolfboot_sbi_call(unsigned long fid, unsigned long a0, + unsigned long a1, unsigned long a2, + unsigned long *value) +{ + struct sbiret ret; + + ret = sbi_ecall(SBI_EXT_VENDOR_MCHP, fid, a0, a1, a2, 0, 0, 0); + if (value) + *value = ret.value; + return ret.error; +} + +static long wolfboot_sbi_ioctl(struct file *file, unsigned int cmd, + unsigned long arg) +{ + void __user *uarg = (void __user *)arg; + unsigned long value = 0; + long err; + int rc; + + if (mutex_lock_interruptible(&wolfboot_sbi_lock)) + return -ERESTARTSYS; + + switch (cmd) { + case WOLFBOOT_SBI_IOC_GET_VERSION: { + u32 v; + + err = wolfboot_sbi_call(SBI_WOLF_FID_GET_VERSION, 0, 0, 0, + &value); + rc = wolfboot_sbi_errno(err); + v = (u32)value; + if (!rc && copy_to_user(uarg, &v, sizeof(v))) + rc = -EFAULT; + break; + } + case WOLFBOOT_SBI_IOC_GET_SERIAL: { + u8 *serial = kzalloc(WOLFBOOT_SBI_SERIAL_LEN, GFP_KERNEL); + + if (!serial) { + rc = -ENOMEM; + break; + } + /* M-mode writes the buffer through this hart's translation. */ + err = wolfboot_sbi_call(SBI_WOLF_FID_GET_SERIAL, + (unsigned long)serial, + WOLFBOOT_SBI_SERIAL_LEN, 0, NULL); + rc = wolfboot_sbi_errno(err); + if (!rc && copy_to_user(uarg, serial, WOLFBOOT_SBI_SERIAL_LEN)) + rc = -EFAULT; + kfree(serial); + break; + } + case WOLFBOOT_SBI_IOC_GET_KEY: { + struct wolfboot_sbi_key *req; + + req = kzalloc(sizeof(*req), GFP_KERNEL); + if (!req) { + rc = -ENOMEM; + break; + } + if (copy_from_user(req, uarg, sizeof(*req))) { + rc = -EFAULT; + } else { + err = wolfboot_sbi_call(SBI_WOLF_FID_GET_KEY, + req->purpose, + (unsigned long)req->key, + WOLFBOOT_SBI_KEY_LEN, NULL); + rc = wolfboot_sbi_errno(err); + if (!rc && copy_to_user(uarg, req, sizeof(*req))) + rc = -EFAULT; + } + kfree_sensitive(req); + break; + } + case WOLFBOOT_SBI_IOC_DIGEST_CHECK: + case WOLFBOOT_SBI_IOC_IAP_AUTH: + case WOLFBOOT_SBI_IOC_IAP_VERIFY: + case WOLFBOOT_SBI_IOC_IAP_PROGRAM: { + unsigned long fid; + u32 a0; + + if (copy_from_user(&a0, uarg, sizeof(a0))) { + rc = -EFAULT; + break; + } + switch (cmd) { + case WOLFBOOT_SBI_IOC_DIGEST_CHECK: + fid = SBI_WOLF_FID_DIGEST_CHECK; + break; + case WOLFBOOT_SBI_IOC_IAP_AUTH: + fid = SBI_WOLF_FID_IAP_AUTHENTICATE; + break; + case WOLFBOOT_SBI_IOC_IAP_VERIFY: + fid = SBI_WOLF_FID_IAP_VERIFY; + break; + default: + /* The device restarts inside the call on success; the + * caller syncs its filesystems first. */ + fid = SBI_WOLF_FID_IAP_PROGRAM; + break; + } + err = wolfboot_sbi_call(fid, a0, 0, 0, NULL); + rc = wolfboot_sbi_errno(err); + break; + } + case WOLFBOOT_SBI_IOC_AUTO_UPDATE: + err = wolfboot_sbi_call(SBI_WOLF_FID_AUTO_UPDATE, 0, 0, 0, NULL); + rc = wolfboot_sbi_errno(err); + break; + case WOLFBOOT_SBI_IOC_SC_FLASH_ERASE: { + struct wolfboot_sbi_flash_range r; + u32 off; + + if (copy_from_user(&r, uarg, sizeof(r))) { + rc = -EFAULT; + break; + } + if (!r.len || (r.offset % WOLFBOOT_SBI_FLASH_ERASE_BLOCK) || + (r.len % WOLFBOOT_SBI_FLASH_ERASE_BLOCK) || + r.offset > U32_MAX - r.len) { + rc = -EINVAL; + break; + } + rc = 0; + /* one erase block per ecall: each holds its hart for the erase */ + for (off = r.offset; off < r.offset + r.len && !rc; + off += WOLFBOOT_SBI_FLASH_ERASE_BLOCK) { + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_FLASH_ERASE, off, + WOLFBOOT_SBI_FLASH_ERASE_BLOCK, 0, + NULL); + rc = wolfboot_sbi_errno(err); + } + break; + } + case WOLFBOOT_SBI_IOC_SC_FLASH_WRITE: + case WOLFBOOT_SBI_IOC_SC_FLASH_READ: { + struct wolfboot_sbi_flash_io io; + u8 __user *ubuf; + u8 *bounce; + u32 done = 0; + + if (copy_from_user(&io, uarg, sizeof(io))) { + rc = -EFAULT; + break; + } + if (!io.len || io.offset > U32_MAX - io.len) { + rc = -EINVAL; + break; + } + ubuf = (u8 __user *)(uintptr_t)io.buf; + bounce = kzalloc(WOLFBOOT_SBI_FLASH_CHUNK, GFP_KERNEL); + if (!bounce) { + rc = -ENOMEM; + break; + } + rc = 0; + while (done < io.len && !rc) { + u32 n = min_t(u32, io.len - done, WOLFBOOT_SBI_FLASH_CHUNK); + + if (cmd == WOLFBOOT_SBI_IOC_SC_FLASH_WRITE) { + if (copy_from_user(bounce, ubuf + done, n)) { + rc = -EFAULT; + break; + } + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_FLASH_WRITE, + io.offset + done, + (unsigned long)bounce, n, + NULL); + rc = wolfboot_sbi_errno(err); + } else { + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_FLASH_READ, + io.offset + done, + (unsigned long)bounce, n, + NULL); + rc = wolfboot_sbi_errno(err); + if (!rc && copy_to_user(ubuf + done, bounce, n)) + rc = -EFAULT; + } + done += n; + } + kfree(bounce); + break; + } + case WOLFBOOT_SBI_IOC_SC_FLASH_SET_IMAGE: { + struct wolfboot_sbi_flash_image im; + + if (copy_from_user(&im, uarg, sizeof(im))) { + rc = -EFAULT; + break; + } + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_FLASH_SET_IMAGE, im.index, + im.address, 0, NULL); + rc = wolfboot_sbi_errno(err); + break; + } + case WOLFBOOT_SBI_IOC_SC_STATUS: { + struct wolfboot_sbi_sc_status st; + + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_STATUS, 0, 0, 0, &value); + rc = wolfboot_sbi_errno(err); + st.status = (u32)(value & 0xFFFFUL); + st.mbox1 = (u32)(value >> 32); + if (!rc && copy_to_user(uarg, &st, sizeof(st))) + rc = -EFAULT; + break; + } + default: + rc = -ENOTTY; + break; + } + + mutex_unlock(&wolfboot_sbi_lock); + return rc; +} + +static const struct file_operations wolfboot_sbi_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = wolfboot_sbi_ioctl, + .compat_ioctl = compat_ptr_ioctl, +}; + +static struct miscdevice wolfboot_sbi_dev = { + .minor = MISC_DYNAMIC_MINOR, + .name = "wolfboot", + .fops = &wolfboot_sbi_fops, + .mode = 0600, +}; + +static int __init wolfboot_sbi_init(void) +{ + unsigned long value = 0; + long err; + + if (sbi_probe_extension(SBI_EXT_VENDOR_MCHP) <= 0) + return -ENODEV; + /* The HSS answers the same extension with NOT_SUPPORTED here. */ + err = wolfboot_sbi_call(SBI_WOLF_FID_GET_VERSION, 0, 0, 0, &value); + if (err != SBI_SUCCESS || value < SBI_WOLF_VERSION_MIN) + return -ENODEV; + + return misc_register(&wolfboot_sbi_dev); +} + +static void __exit wolfboot_sbi_exit(void) +{ + misc_deregister(&wolfboot_sbi_dev); +} + +module_init(wolfboot_sbi_init); +module_exit(wolfboot_sbi_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("wolfSSL Inc."); +MODULE_DESCRIPTION("wolfBoot SBI vendor extension front end for PolarFire SoC"); diff --git a/tools/linux/wolfboot-sbi/wolfboot_sbi.h b/tools/linux/wolfboot-sbi/wolfboot_sbi.h new file mode 100644 index 0000000000..c055676cb9 --- /dev/null +++ b/tools/linux/wolfboot-sbi/wolfboot_sbi.h @@ -0,0 +1,97 @@ +/* wolfboot_sbi.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * This is the user-space interface of the wolfboot-sbi Linux kernel module and + * is licensed under version 2 or later of the GNU General Public License, + * rather than version 3 or later as the rest of wolfBoot is, so that it and + * the module that includes it match the MODULE_LICENSE("GPL") the kernel + * requires. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* User-space interface of the wolfboot-sbi Linux module: /dev/wolfboot + * exposes the wolfBoot SBI vendor calls (include/riscv_sbi.h). */ + +#ifndef WOLFBOOT_SBI_UAPI_H +#define WOLFBOOT_SBI_UAPI_H + +#include +#include + +#define WOLFBOOT_SBI_SERIAL_LEN 16 +#define WOLFBOOT_SBI_KEY_LEN 32 + +#define WOLFBOOT_SBI_KEY_MRAM_LOG 1 +#define WOLFBOOT_SBI_KEY_VARLOG 2 + +struct wolfboot_sbi_key { + __u32 purpose; + __u8 key[WOLFBOOT_SBI_KEY_LEN]; +}; + +/* A started System Controller service; poll with SC_STATUS until it does + * not return EBUSY. status is the 16-bit service status, mbox1 the second + * mailbox word (the DIGESTERR bits for a digest check). */ +struct wolfboot_sbi_sc_status { + __u32 status; + __u32 mbox1; +}; + +/* System Controller SPI flash (fenced builds with MPFS_SBI_SC_FLASH). Offsets + * are flash offsets; the first erase block (the SPI directory) is reachable + * only through SET_IMAGE, which points entry 1 (upgrade) or 2 (blank) at an + * image. The module splits a range into one-erase-block erases and 4 KB + * transfers, each its own ecall. */ +#define WOLFBOOT_SBI_FLASH_ERASE_BLOCK 65536 +#define WOLFBOOT_SBI_FLASH_CHUNK 4096 +#define WOLFBOOT_SBI_FLASH_UPGRADE_INDEX 1 +#define WOLFBOOT_SBI_FLASH_BLANK_INDEX 2 + +struct wolfboot_sbi_flash_range { + __u32 offset; + __u32 len; +}; + +struct wolfboot_sbi_flash_io { + __u32 offset; + __u32 len; + __u64 buf; /* user buffer */ +}; + +struct wolfboot_sbi_flash_image { + __u32 index; + __u32 address; +}; + +#define WOLFBOOT_SBI_IOC_MAGIC 'W' +#define WOLFBOOT_SBI_IOC_GET_VERSION _IOR(WOLFBOOT_SBI_IOC_MAGIC, 0, __u32) +#define WOLFBOOT_SBI_IOC_GET_SERIAL _IOR(WOLFBOOT_SBI_IOC_MAGIC, 1, __u8[WOLFBOOT_SBI_SERIAL_LEN]) +#define WOLFBOOT_SBI_IOC_GET_KEY _IOWR(WOLFBOOT_SBI_IOC_MAGIC, 2, struct wolfboot_sbi_key) +#define WOLFBOOT_SBI_IOC_DIGEST_CHECK _IOW(WOLFBOOT_SBI_IOC_MAGIC, 3, __u32) +#define WOLFBOOT_SBI_IOC_IAP_AUTH _IOW(WOLFBOOT_SBI_IOC_MAGIC, 4, __u32) +#define WOLFBOOT_SBI_IOC_IAP_VERIFY _IOW(WOLFBOOT_SBI_IOC_MAGIC, 5, __u32) +#define WOLFBOOT_SBI_IOC_IAP_PROGRAM _IOW(WOLFBOOT_SBI_IOC_MAGIC, 6, __u32) +#define WOLFBOOT_SBI_IOC_AUTO_UPDATE _IO(WOLFBOOT_SBI_IOC_MAGIC, 7) +#define WOLFBOOT_SBI_IOC_SC_STATUS _IOR(WOLFBOOT_SBI_IOC_MAGIC, 8, struct wolfboot_sbi_sc_status) +#define WOLFBOOT_SBI_IOC_SC_FLASH_ERASE _IOW(WOLFBOOT_SBI_IOC_MAGIC, 9, struct wolfboot_sbi_flash_range) +#define WOLFBOOT_SBI_IOC_SC_FLASH_WRITE _IOW(WOLFBOOT_SBI_IOC_MAGIC, 10, struct wolfboot_sbi_flash_io) +#define WOLFBOOT_SBI_IOC_SC_FLASH_READ _IOWR(WOLFBOOT_SBI_IOC_MAGIC, 11, struct wolfboot_sbi_flash_io) +#define WOLFBOOT_SBI_IOC_SC_FLASH_SET_IMAGE _IOW(WOLFBOOT_SBI_IOC_MAGIC, 12, struct wolfboot_sbi_flash_image) + +#endif /* WOLFBOOT_SBI_UAPI_H */ From eee179889de6f61b1b9dd9040ebfc51c8b62e33b Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 8 Oct 2026 19:29:33 -0700 Subject: [PATCH 07/23] PolarFire SoC: boot mode 2 User Boot Loader stub verifying the eNVM wolfBoot --- .github/workflows/test-configs.yml | 14 + Makefile | 26 ++ arch.mk | 14 + config/examples/polarfire_mpfs250_ubl.config | 79 +++++ docs/Targets.md | 84 ++++++ hal/mpfs250-m.ld | 23 +- hal/mpfs250.c | 9 + hal/mpfs250.h | 13 + hal/mpfs250_ubl.c | 297 +++++++++++++++++++ hal/mpfs250_ubl.h | 44 +++ hal/mpfs250_ubl.ld | 90 ++++++ src/boot_riscv_start.S | 89 +++++- tools/scripts/mpfs_bootmode2.sh | 148 +++++++++ 13 files changed, 926 insertions(+), 4 deletions(-) create mode 100644 config/examples/polarfire_mpfs250_ubl.config create mode 100644 hal/mpfs250_ubl.c create mode 100644 hal/mpfs250_ubl.h create mode 100644 hal/mpfs250_ubl.ld create mode 100755 tools/scripts/mpfs_bootmode2.sh diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index da73b1287e..78181396ff 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -546,6 +546,20 @@ jobs: config-file: ./config/examples/polarfire_mpfs250_m.config pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SIGN=ML_DSA ML_DSA_LEVEL=5 IMAGE_SIGNATURE_SIZE=4627 IMAGE_HEADER_SIZE=12288 WOLFBOOT_SECTOR_SIZE=0x4000 + # Boot mode 2: the eNVM wolfBoot linked above the signed manifest and + # signed as a wolfBoot image, and the sNVM UBL stub that verifies it. + microchip_mpfs250_m_signed_envm_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub MPFS_ENVM_MANIFEST=1 wolfboot_envm_signed.bin + microchip_mpfs250_ubl_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_ubl.config # Trust anchor served from sNVM with the production hardenings: ROM-flag # enforcement and the S-mode fence around the System Controller mailbox. microchip_mpfs250_m_snvm_test: diff --git a/Makefile b/Makefile index b020eae464..80830b091f 100644 --- a/Makefile +++ b/Makefile @@ -522,6 +522,11 @@ ifeq ($(TARGET),rp2350) MAIN_TARGET:=include/target.h keytools wolfboot_signing_private_key.der pico-sdk-info endif +ifeq ($(TARGET),mpfs250_ubl) + # Boot mode 2 stub: verifies the eNVM wolfBoot, there is no application + MAIN_TARGET:=wolfboot.bin +endif + ifeq ($(FLASH_OTP_KEYSTORE),1) MAIN_TARGET+=tools/keytools/otp/otp-keystore-primer.bin @@ -775,6 +780,27 @@ factory_wstage1.bin: $(BINASSEMBLE) stage1/loader_stage1.bin wolfboot.bin $(BOOT wolfboot_stage1.bin: wolfboot.elf stage1/loader_stage1.bin $(Q) cp stage1/loader_stage1.bin wolfboot_stage1.bin +# PolarFire SoC boot mode 2: sign the eNVM wolfBoot for the UBL stub to verify. The manifest +# header goes into the placeholder MPFS_ENVM_MANIFEST=1 links in the last eNVM page, so one +# mpfsBootmodeProgrammer run programs image and manifest. See docs/PolarFire.md. +MPFS_UBL_KEY?=$(PRIVATE_KEY) +MPFS_ENVM_VERSION?=1 +# The stub is ECC384/SHA384 only, so the manifest size and algorithm do not follow the image's. +MPFS_ENVM_MANIFEST_SIZE?=512 +wolfboot_envm_signed.bin: wolfboot.elf $(MPFS_UBL_KEY) keytools_check + @echo "\t[SIGN] $@" + $(Q)(test "$(MPFS_ENVM_MANIFEST)" = "1") || \ + (echo "wolfboot_envm_signed.bin needs MPFS_ENVM_MANIFEST=1" && false) + $(Q)$(OBJCOPY) -O binary --gap-fill 0x00 -R .envm_manifest wolfboot.elf \ + wolfboot_envm.bin + $(Q)$(SIGN_ENV) IMAGE_HEADER_SIZE=$(MPFS_ENVM_MANIFEST_SIZE) \ + $(SIGN_TOOL) --ecc384 --sha384 wolfboot_envm.bin \ + $(MPFS_UBL_KEY) $(MPFS_ENVM_VERSION) + $(Q)mv wolfboot_envm_v$(MPFS_ENVM_VERSION)_signed.bin $@ + $(Q)head -c $(MPFS_ENVM_MANIFEST_SIZE) $@ > wolfboot_envm_manifest.bin + $(Q)$(OBJCOPY) --update-section .envm_manifest=wolfboot_envm_manifest.bin \ + wolfboot.elf wolfboot_envm_signed.elf + wolfboot.elf: include/target.h $(LSCRIPT) $(OBJS) $(BINASSEMBLE) $(WOLFBOOT_SIGN_KEY_DEP) FORCE $(Q)(test $(SIGN) = NONE) || (test $(FLASH_OTP_KEYSTORE) = 1) || (test "$(WOLFBOOT_NO_KEYSTORE)" = "1") || (test "$(SNVM_KEYSTORE)" = "1") || (grep -q $(SIGN_ALG) src/keystore.c) || \ (echo "Key mismatch: please run 'make keysclean' to remove all keys if you want to change algorithm" && false) diff --git a/arch.mk b/arch.mk index 709f78e532..16bab15272 100644 --- a/arch.mk +++ b/arch.mk @@ -1337,6 +1337,11 @@ ifeq ($(ARCH),RISCV64) endif # Use M-mode specific linker script LSCRIPT_IN:=hal/$(TARGET)-m.ld + # Boot mode 2: reserve the last eNVM page for the signed manifest the UBL + # stub verifies (hal/mpfs250-m.ld); `make wolfboot_envm_signed.bin` fills it. + ifeq ($(MPFS_ENVM_MANIFEST),1) + CFLAGS+=-DMPFS_ENVM_MANIFEST + endif # LIBERO_FPGA_CONFIG_DIR supplies fpga_design_config.h and enables DDR init. # An S-mode OS runs from DDR, so an empty value would silently build a # bootloader with no DDR init at all -- fail instead. @@ -1388,6 +1393,15 @@ ifeq ($(ARCH),RISCV64) UPDATE_OBJS?=src/update_ram.o endif + # Boot mode 2 UBL stub: LIM-resident, verifies the eNVM wolfBoot manifest + # in place and jumps; no update path (hal/mpfs250_ubl.c). + ifeq ($(TARGET),mpfs250_ubl) + LSCRIPT_IN:=hal/mpfs250_ubl.ld + UPDATE_OBJS:= + # mcycle rate for hal_get_timer_us(): the stub runs at the reset clock + CFLAGS+=-DRISCV_SMODE_TIMER_FREQ=80000000UL + endif + # GCC 12+ extension detection (see RISCV 32-bit section above). # Extensions go in CFLAGS only; LDFLAGS uses the base march to avoid # GCC 15 multilib lookup failures from ISA string decomposition. diff --git a/config/examples/polarfire_mpfs250_ubl.config b/config/examples/polarfire_mpfs250_ubl.config new file mode 100644 index 0000000000..094f0747b2 --- /dev/null +++ b/config/examples/polarfire_mpfs250_ubl.config @@ -0,0 +1,79 @@ +# PolarFire SoC MPFS250 boot mode 2 User Boot Loader (UBL) stub +# +# A first-stage verifier for the "user secure boot from sNVM" boot mode: +# 1. The System Controller authenticates (and optionally decrypts) the +# stub out of sNVM, copies it to the LIM (0x08000000) and releases +# all five harts at its entry point. +# 2. The E51 verifies the wolfBoot manifest in eNVM (ECC384 signature, +# SHA384 digest) against the public key compiled into the stub. +# 3. All five harts enter the eNVM wolfBoot at its reset vector, exactly +# as a boot mode 1 boot would; wolfBoot then boots the system from +# there (see polarfire_mpfs250_m.config). +# +# The eNVM wolfBoot is built with MPFS_ENVM_MANIFEST=1 and signed with +# `make wolfboot_envm_signed.bin`: the image stays at 0x20220100 and its +# manifest goes in eNVM page 0x1FC (0x2023FC00); see docs/Targets.md. +# +# Program sNVM + boot mode (requires SoftConsole / Libero SoC): +# java -jar mpfsBootmodeProgrammer.jar --bootmode 2 --snvm_page 0 \ +# --usk 0 --die MPFS250T --package FCG1152 --workdir $PWD wolfboot.elf + +ARCH?=RISCV64 +TARGET?=mpfs250_ubl +SIGN?=ECC384 +HASH?=SHA384 +IMAGE_HEADER_SIZE=512 +WOLFBOOT_VERSION?=1 +ARMORED?=0 +DEBUG?=0 +DEBUG_SYMBOLS?=1 +DEBUG_UART?=1 +VTOR?=1 +EXT_FLASH?=0 +SPI_FLASH?=0 +NO_XIP?=1 +NVM_FLASH_WRITEONCE?=0 +UART_FLASH?=0 +V?=0 +NO_MPU?=1 +RAM_CODE?=0 +SPMATH?=1 +SPMATHALL?=0 +DUALBANK_SWAP?=0 +PKA?=0 +ENCRYPT=0 +WOLFTPM?=0 +ELF?=0 + +# Size first: the stub must fit the authenticated sNVM pages (see +# hal/mpfs250_ubl.ld). Portable C crypto; the E51 is rv64imac. +OPTIMIZATION_LEVEL=s +NO_ASM?=1 + +# Runs on the E51 in M-mode at the reset clock, no HSS. +RISCV_MMODE?=1 + +# No update path: the stub verifies one manifest in place and jumps. +NO_LOADER=1 +WOLFBOOT_NO_PARTITIONS=1 + +# LIM placement. The System Controller copies the image to the address in +# the UBL header, which the programmer takes from the ELF. +WOLFBOOT_ORIGIN?=0x08000000 +BOOTLOADER_PARTITION_SIZE?=0x40000 +STACK_SIZE?=32k + +# The manifest header is detached from the image it signs: a page near the +# end of eNVM, image from the reset vector (hal/mpfs250.h). +WOLFBOOT_SELF_HEADER=1 +WOLFBOOT_PARTITION_SELF_HEADER_ADDRESS=0x2023FC00 +WOLFBOOT_SECTOR_SIZE?=0x200 + +# Informational: where the verified eNVM wolfBoot lives. +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x20220100 +WOLFBOOT_PARTITION_SIZE=0x1FD00 + +# Refuse an eNVM manifest older than this version. A field update must bump +# WOLFBOOT_VERSION past the floor the shipped stub was built with. +MPFS_UBL_MIN_VERSION?=1 +CFLAGS_EXTRA+=-DMPFS_UBL_MIN_VERSION=$(MPFS_UBL_MIN_VERSION) diff --git a/docs/Targets.md b/docs/Targets.md index c73ac84b63..a0ee58884a 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -989,6 +989,7 @@ programming, Memory Test Controller, LPDDR4 mode-register protocol) `hal/mpfs250.ld` - Linker script for S-mode (HSS-based boot) `hal/mpfs250-m.ld` - Linker script for M-mode (eNVM + L2 SRAM) `hal/mpfs250-hss.ld` - Linker script for S-mode (HSS with L2-LIM) +`hal/mpfs250_ubl.c` / `hal/mpfs250_ubl.ld` - Boot mode 2 User Boot Loader stub (sNVM-authenticated verifier of the eNVM wolfBoot) `hal/mpfs.dts` - Device tree source `hal/mpfs.yaml` - HSS payload generator configuration for use of DDR `hal/mpfs-l2lim.yaml` - HSS payload generator for the use of L2-LIM @@ -1325,6 +1326,89 @@ register table and composes the generic calls. Both compile only when `MPFS_DDR_ The System Controller SRAM-PUF, secure NVM (sNVM), and TeraFire crypto can anchor key material in hardware: serve the verification public keys from sNVM, derive a device-unique KEK from the PUF, and store the AES image-encryption key in sNVM wrapped by that KEK. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). +### PolarFire SoC boot mode 2: User Boot Loader stub + +Boot mode 2 ("user secure boot from sNVM") lets the System Controller authenticate the first code the MSS runs: it copies a User Boot Loader (UBL) out of authenticated sNVM pages into the LIM and releases all five harts at its entry. wolfBoot provides that UBL as a small verifier target, `TARGET=mpfs250_ubl`: it checks an ECC384/SHA384 wolfBoot manifest over the eNVM wolfBoot image against the public key compiled into the stub, then enters the eNVM wolfBoot at its reset vector on every hart exactly as a boot mode 1 boot would. The eNVM wolfBoot itself is unchanged and stays field-updatable; what the sNVM pages pin is the key that must have signed it. + +**eNVM layout** (`hal/mpfs250-m.ld`, `hal/mpfs250.h`) + +| Address | Content | +|---------|---------| +| `0x20220000` | boot ROM meta information (written by `mpfsBootmodeProgrammer`) | +| `0x20220100` | wolfBoot image, reset vector first | +| `0x2023FC00` | wolfBoot manifest header (`MPFS_ENVM_MANIFEST=1`), signed by the stub's key | + +The manifest is detached from the image it signs (the `WOLFBOOT_SELF_HEADER` form), so the image does not move and a boot mode 1 device boots the same eNVM contents. The last two eNVM pages stay free for the programmer. + +**Relevant files:** + +| File | Description | +|------|-------------| +| `config/examples/polarfire_mpfs250_ubl.config` | UBL stub configuration | +| `hal/mpfs250_ubl.c`, `hal/mpfs250_ubl.h` | stub HAL: watchdog hold, MMUART0 at the reset clock, manifest verify, hart hand-off | +| `hal/mpfs250_ubl.ld` | LIM linker script with the sNVM size limit | +| `src/boot_riscv_start.S` | `TARGET_mpfs250_ubl` startup: no copy, U54s park on a DTIM mailbox | +| `tools/scripts/mpfs_bootmode2.sh` | sNVM + boot mode programming on the secure dies, eNVM-only update | + +**Build the stub** (its own signing key; keep it out of the tree that signs application images): +```sh +cp config/examples/polarfire_mpfs250_ubl.config .config +make clean && make +# wolfboot.elf is the UBL; wolfboot_signing_private_key.der is the UBL key +``` +The image must fit the 221 authenticated sNVM pages (52,096 bytes after the 60-byte UBL header); the linker script enforces it. The sNVM pages above the stub stay available for the sNVM keystore and wrapped keys of [polarfire_snvm_puf.md](polarfire_snvm_puf.md). + +**Build and sign the eNVM wolfBoot** for the stub (any M-mode configuration): +```sh +cp config/examples/polarfire_mpfs250_m.config .config +make clean && make LIBERO_FPGA_CONFIG_DIR=... MPFS_ENVM_MANIFEST=1 \ + MPFS_UBL_KEY=/path/to/ubl/wolfboot_signing_private_key.der \ + wolfboot_envm_signed.bin +``` +`MPFS_ENVM_MANIFEST=1` links a manifest placeholder into the last used eNVM page; the rule signs the image without it (`wolfboot_envm.bin`, the ELF's loadable bytes with alignment gaps zero-filled, which is what the programmer writes), writes the resulting header into the placeholder and produces `wolfboot_envm_signed.elf`, which programs image and manifest together. `MPFS_UBL_KEY` defaults to the tree's own key, `MPFS_ENVM_VERSION` (default 1) is the manifest version, and `-DMPFS_UBL_MIN_VERSION=N` in the stub build refuses older manifests. + +**Program** (requires `SC_INSTALL_DIR` and `FPGENPROG`, see the M-mode section): +```sh +# 1. eNVM: image + manifest. This also selects boot mode 1, so the device +# boots the new eNVM contents before the stub is in place. +java -jar $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ + --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD \ + wolfboot_envm_signed.elf + +# 2. sNVM: the stub, and boot mode 2. Non-secure dies: the programmer +# itself (--bootmode 2 --snvm_page 0 --usk 0 wolfboot.elf). Secure dies +# (MPFS250TS...), which the programmer does not list but which an +# authenticated sNVM client requires: +tools/scripts/mpfs_bootmode2.sh --die MPFS250TS --workdir $PWD /path/to/ubl/wolfboot.elf +``` +The script runs the programmer in `--dryrun` for the UBL image and the User sNVM Key (USK), replays its `fpgenprog` steps for the requested die and programs. Keep the USK file it names: the sNVM pages can only be rewritten with it. + +The UBL lives in an authenticated sNVM client, which the System Controller keys from the device's sNVM Master Key (SMK) +and the client's 12-byte USK. A device whose SMK has never been initialized cannot hold such a client, and +`mpfsBootmodeProgrammer` never asks for one, so `--regenerate-smk` passes `--regenerate_smk` to the `snvm_client` step +and the generated bitstream carries a new SMK. A new SMK invalidates every authenticated or encrypted sNVM client +already on the device; wolfBoot's own keystore and wrapped image key are plaintext clients and are not SMK-keyed. + +Updating the eNVM wolfBoot under boot mode 2 goes through the same script with `--envm`, which programs the eNVM client only and leaves the boot mode and sNVM alone: +```sh +tools/scripts/mpfs_bootmode2.sh --envm --die MPFS250TS --workdir $PWD wolfboot_envm_signed.elf +``` +`mpfsBootmodeProgrammer --bootmode 1` restores boot mode 1. + +**Expected serial output:** +``` +wolfBoot UBL 2.10.0: eNVM image 0x20220100, manifest 0x2023FC00 +UBL: eNVM wolfBoot signature OK (1051 ms), entering 0x20220100 +UBL: 5 of 5 harts released +wolfBoot Version: ... +``` +A manifest that does not verify prints which check failed (`no manifest`, `digest mismatch`, `signature invalid`) and `UBL: eNVM wolfBoot verify FAILED`; the stub panics and nothing in eNVM runs. + +**Notes:** +- The stub runs at the reset clock and changes no clock, L2 way or peripheral the eNVM wolfBoot expects to find in its reset state. It holds the five MSS watchdogs in the no-reset, maximum-window state for the verify and puts the E51's back before the jump, so the eNVM wolfBoot's own watchdog snapshot is what the boot ROM left. +- The U54s wait in the stub on a DTIM mailbox (`hal/mpfs250_ubl.h`) for the verified entry, acknowledge and jump; the E51 follows last. All five harts then run the eNVM startup as in boot mode 1. +- Immutability of the stub is a Libero decision: the ROM flag on its sNVM pages and a frozen boot configuration. wolfBoot does not program either. + ### PolarFire testing This section describes how to build the test-application, create a custom uSD with required partitions and copying signed test-application to uSD partitions. diff --git a/hal/mpfs250-m.ld b/hal/mpfs250-m.ld index b73ccc53f9..c86c08935a 100644 --- a/hal/mpfs250-m.ld +++ b/hal/mpfs250-m.ld @@ -9,10 +9,13 @@ * 3. Jumps to L2_SCRATCH for execution * * The first 0x100 bytes of eNVM are reserved for the boot ROM secure boot - * meta information added by mpfsBootmodeProgrammer. + * meta information added by mpfsBootmodeProgrammer. A page near the end + * holds the signed manifest of this image when the boot mode 2 stub verifies + * it (MPFS_ENVM_MANIFEST=1, see hal/mpfs250_ubl.c); it is filled in after + * the link and left out of the signed bytes. * * Memory regions: - * FLASH_ENVM - Embedded NVM (128KB - 0x100 for header) + * FLASH_ENVM - Embedded NVM (128KB - 0x100 for header - 1KB at the end) * L2_SCRATCH - L2 Scratchpad SRAM (256KB) - execution and data */ @@ -24,7 +27,14 @@ MEMORY { /* The first 0x100 bytes of eNVM are used for boot ROM secure boot meta information * This offset is added by mpfsBootmodeProgrammer (bootmode 1) */ - FLASH_ENVM (rx) : ORIGIN = 0x20220100, LENGTH = 128k - 0x100 + FLASH_ENVM (rx) : ORIGIN = 0x20220100, LENGTH = 128k - 0x100 - 0x400 + /* Signed manifest of the eNVM image, verified by the boot mode 2 stub. + * The last two pages stay free: mpfsBootmodeProgrammer keeps them. + * 0x200 is the stub's header size, not this image's: the stub is always + * ECC384/SHA384, so the manifest stays that size even when a larger + * algorithm signs the image it covers. Keep in step with + * MPFS_ENVM_MANIFEST_SIZE in hal/mpfs250.h. */ + ENVM_MANIFEST (r) : ORIGIN = 0x2023FC00, LENGTH = 0x200 /* L2 scratchpad. MPFS250 hardwires ways 8-11, so 4 x 128 KB is the * ceiling the startup pin loop can make resident; ASSERT below enforces it. */ @@ -110,6 +120,13 @@ SECTIONS _end_bss = .; _end = .; } > L2_SCRATCH + + /* Placeholder the signed build overwrites with the manifest header; + * empty, and absent from the ELF, unless MPFS_ENVM_MANIFEST=1. */ + .envm_manifest : + { + KEEP(*(.envm_manifest)) + } > ENVM_MANIFEST } /* Heap starts after BSS (between _end and stack) */ diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 2bf7a6962b..6becd50ac8 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -96,6 +96,15 @@ uint32_t mpfs_apb_clk_hz = MSS_APB_AHB_CLK; * A wrong value here skews timing-sensitive paths (e.g. SD power-up). */ uint32_t mpfs_cpu_freq_mhz = MPFS_CPU_FREQ_RESET_MHZ; +#ifdef MPFS_ENVM_MANIFEST +/* Last eNVM page: the signed manifest of this image, written in after the + * link by `make wolfboot_envm_signed.bin` and checked by the boot mode 2 + * stub (hal/mpfs250_ubl.c). Erased-state fill until then. */ +const uint8_t __attribute__((section(".envm_manifest"), used)) + mpfs_envm_manifest[MPFS_ENVM_MANIFEST_SIZE] = + { [0 ... MPFS_ENVM_MANIFEST_SIZE - 1] = 0xFF }; +#endif + /* Saved boot ROM watchdog values, restored in hal_prepare_boot(). */ static uint32_t mpfs_wdt_default_mvrp = 0; static uint32_t mpfs_wdt_default_ctrl = 0; diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 379e0bb7cb..6ee22b37d9 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -61,6 +61,19 @@ #define MPFS_DDR_MAX_OUTER_RETRY 6U #endif +/* eNVM: boot ROM meta information in page 0, the wolfBoot image from page 1 + * (hal/mpfs250-m.ld), and in the boot mode 2 flow its signed manifest two + * pages below the end, verified by the UBL stub (hal/mpfs250_ubl.c). */ +#define MPFS_ENVM_BASE 0x20220000UL +#define MPFS_ENVM_SIZE 0x20000UL +#define MPFS_ENVM_IMAGE_ADDR (MPFS_ENVM_BASE + 0x100UL) +#define MPFS_ENVM_MANIFEST_ADDR (MPFS_ENVM_BASE + MPFS_ENVM_SIZE - 0x400UL) +/* The stub is built ECC384/SHA384 and has no room for a PQC verifier in its + * sNVM budget, so the manifest it checks is that size whatever algorithm + * signed the image it covers. This does NOT follow IMAGE_HEADER_SIZE. Keep + * in step with the ENVM_MANIFEST region in hal/mpfs250-m.ld. */ +#define MPFS_ENVM_MANIFEST_SIZE 512 + /* Hardware Base Address */ #define SYSREG_BASE 0x20002000 diff --git a/hal/mpfs250_ubl.c b/hal/mpfs250_ubl.c new file mode 100644 index 0000000000..4888d0711e --- /dev/null +++ b/hal/mpfs250_ubl.c @@ -0,0 +1,297 @@ +/* mpfs250_ubl.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* PolarFire SoC boot mode 2 User Boot Loader (UBL) stub. + * + * Authenticated by the System Controller out of sNVM and run from the LIM, + * this verifies the signed wolfBoot manifest in eNVM and enters wolfBoot at + * its reset vector on every hart, as a boot mode 1 boot would. It touches + * no clock, cache way or peripheral the eNVM wolfBoot does not expect to + * find in its reset state. */ + +#include +#include + +#include "image.h" +#include "loader.h" +#include "hal.h" +#include "printf.h" +#include "hal/mpfs250.h" +#include "hal/mpfs250_ubl.h" +#include "wolfboot/version.h" + +#define UBL_MAGIC (*(volatile uint32_t *)(uintptr_t)MPFS_UBL_MAGIC_ADDR) +#define UBL_ENTRY (*(volatile uint64_t *)(uintptr_t)MPFS_UBL_ENTRY_ADDR) +#define UBL_ENTRY_INV (*(volatile uint64_t *)(uintptr_t)MPFS_UBL_ENTRY_INV_ADDR) +#define UBL_ACK(h) (*(volatile uint32_t *)(uintptr_t)(MPFS_UBL_ACK_BASE + \ + (h) * 4)) + +/* Wait for the parked harts to acknowledge the published entry. */ +#define UBL_ACK_WAIT_US 50000U + +static const unsigned long wdt_bases[MPFS_NUM_HARTS] = { + MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, + MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE +}; + +/* Boot ROM E51 watchdog state, restored before the jump so the eNVM + * wolfBoot's hal_init() snapshots what the ROM left. */ +static uint32_t ubl_wdt_mvrp; +static uint32_t ubl_wdt_ctrl; + +static uint64_t rdcycle(void) +{ + uint64_t c; + __asm__ volatile("rdcycle %0" : "=r"(c)); + return c; +} + +void hal_init(void) +{ + unsigned int w; + + ubl_wdt_mvrp = MSS_WDT_MVRP(MSS_WDT_E51_BASE); + ubl_wdt_ctrl = MSS_WDT_CONTROL(MSS_WDT_E51_BASE); + + /* Same sequence as the eNVM wolfBoot: no device reset on timeout and the + * widest window, so the signature check cannot be cut short. */ + for (w = 0; w < MPFS_NUM_HARTS; w++) { + MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; + MSS_WDT_TIME(wdt_bases[w]) = 0x00FFFFFFUL; + MSS_WDT_MVRP(wdt_bases[w]) = 0x00FFFFFFUL; + MSS_WDT_CONTROL(wdt_bases[w]) = 0; + MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; + } + +#ifdef DEBUG_UART + SYSREG_SUBBLK_CLOCK_CR |= (MSS_PERIPH_MMUART0 << DEBUG_UART_PORT); + SYSREG_SOFT_RESET_CR &= ~(MSS_PERIPH_MMUART0 << DEBUG_UART_PORT); + uart_init(); +#endif +} + +void hal_prepare_boot(void) +{ + MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; + MSS_WDT_MVRP(MSS_WDT_E51_BASE) = ubl_wdt_mvrp; + MSS_WDT_CONTROL(MSS_WDT_E51_BASE) = ubl_wdt_ctrl; +} + +/* The manifest is verified in place; there is no flash backend. */ +void hal_flash_unlock(void) +{ +} + +void hal_flash_lock(void) +{ +} + +int hal_flash_write(uint32_t address, const uint8_t *data, int len) +{ + (void)address; + (void)data; + (void)len; + return -1; +} + +int hal_flash_erase(uint32_t address, int len) +{ + (void)address; + (void)len; + return -1; +} + +#ifdef DEBUG_UART +/* The MMUART sequence of hal/mpfs250.c, at the compile-time reset clock: + * a fix there belongs here too. */ +static void uart_config_baud(unsigned long base, uint32_t baudrate) +{ + const uint64_t pclk = MSS_APB_AHB_CLK; + uint32_t div_x128 = (uint32_t)((8UL * pclk) / baudrate); + uint32_t div_x64 = div_x128 / 2u; + uint32_t div_int = div_x64 / 64u; + uint32_t div_frac = div_x64 - (div_int * 64u); + div_frac += (div_x128 - (div_int * 128u)) - (div_frac * 2u); + if (div_frac > 63u) + div_frac = 63u; + if (div_int > (uint32_t)UINT16_MAX) + return; + MMUART_LCR(base) |= DLAB_MASK; + MMUART_DMR(base) = (uint8_t)(div_int >> 8); + MMUART_DLR(base) = (uint8_t)div_int; + MMUART_LCR(base) &= ~DLAB_MASK; + if (div_int > 1u) { + MMUART_MM0(base) |= EFBR_MASK; + MMUART_DFR(base) = (uint8_t)div_frac; + } else { + MMUART_MM0(base) &= ~EFBR_MASK; + } +} + +void uart_init(void) +{ + const unsigned long base = DEBUG_UART_BASE; + + MMUART_MM0(base) &= ~ELIN_MASK; + MMUART_MM1(base) &= ~EIRD_MASK; + MMUART_MM2(base) &= ~EERR_MASK; + MMUART_IER(base) = 0u; + MMUART_FCR(base) = CLEAR_RX_FIFO_MASK | CLEAR_TX_FIFO_MASK | RXRDY_TXRDYN_EN_MASK; + MMUART_MCR(base) &= ~(LOOP_MASK | RLOOP_MASK); + MMUART_MCR(base) |= RTS_MASK; + MMUART_MM1(base) &= ~(E_MSB_TX_MASK | E_MSB_RX_MASK); + MMUART_MM2(base) &= ~(EAFM_MASK | ESWM_MASK); + MMUART_MM0(base) &= ~(ETTG_MASK | ERTO_MASK | EFBR_MASK); + MMUART_GFR(base) = 0u; + MMUART_TTG(base) = 0u; + MMUART_RTO(base) = 0u; + uart_config_baud(base, 115200); + MMUART_LCR(base) = MSS_UART_DATA_8_BITS | MSS_UART_NO_PARITY | MSS_UART_ONE_STOP_BIT; +} + +void uart_write(const char* buf, unsigned int sz) +{ + uint32_t pos = 0; + while (sz-- > 0) { + char c = buf[pos++]; + if (c == '\n') { + while ((MMUART_LSR(DEBUG_UART_BASE) & MSS_UART_THRE) == 0); + MMUART_THR(DEBUG_UART_BASE) = '\r'; + } + while ((MMUART_LSR(DEBUG_UART_BASE) & MSS_UART_THRE) == 0); + MMUART_THR(DEBUG_UART_BASE) = c; + } +} + +static void uart_drain(void) +{ + while ((MMUART_LSR(DEBUG_UART_BASE) & MSS_UART_TEMT) == 0); +} +#endif /* DEBUG_UART */ + +/* Hand the verified entry to the parked harts, then take it last. */ +static void __attribute__((noreturn)) ubl_enter(uintptr_t entry) +{ + uint64_t start; + unsigned int h, acked; + + hal_prepare_boot(); +#ifdef DEBUG_UART + uart_drain(); +#endif + + for (h = 1; h < MPFS_NUM_HARTS; h++) { + UBL_ACK(h) = 0; + } + UBL_ENTRY = entry; + UBL_ENTRY_INV = ~(uint64_t)entry; + __asm__ volatile("fence iorw, iorw" ::: "memory"); + UBL_MAGIC = MPFS_UBL_MAGIC; + __asm__ volatile("fence iorw, iorw" ::: "memory"); + + start = rdcycle(); + do { + acked = 0; + for (h = 1; h < MPFS_NUM_HARTS; h++) { + acked += (UBL_ACK(h) != 0); + } + } while ((acked < MPFS_NUM_HARTS - 1) && + ((rdcycle() - start) < + (uint64_t)UBL_ACK_WAIT_US * MPFS_CPU_FREQ_RESET_MHZ)); +#ifdef DEBUG_UART + wolfBoot_printf("UBL: %u of %u harts released\n", acked + 1, + (unsigned int)MPFS_NUM_HARTS); + uart_drain(); +#endif + + __asm__ volatile("fence.i" ::: "memory"); + ((void (*)(void))entry)(); + while (1) { + } +} + +void main(void) +{ + struct wolfBoot_image img; + uint8_t *manifest = (uint8_t *)(uintptr_t)MPFS_ENVM_MANIFEST_ADDR; + uint8_t *image = (uint8_t *)(uintptr_t)MPFS_ENVM_IMAGE_ADDR; +#ifdef DEBUG_UART + uint64_t start = rdcycle(); +#endif + int ret; + + /* Invalidate whatever the mailbox held before the U54s can act on it */ + UBL_MAGIC = 0; + UBL_ENTRY = 0; + UBL_ENTRY_INV = 0; + __asm__ volatile("fence iorw, iorw" ::: "memory"); + + hal_init(); + + wolfBoot_printf("wolfBoot UBL %s: eNVM image 0x%08lx, manifest 0x%08lx\n", + LIBWOLFBOOT_VERSION_STRING, + (unsigned long)(uintptr_t)image, + (unsigned long)(uintptr_t)manifest); + + /* The manifest sits in the last eNVM page, the image it signs from the + * reset vector up to it. */ + ret = wolfBoot_open_self_address(&img, manifest, image); + if (ret != 0) { + wolfBoot_printf("UBL: no manifest at 0x%08lx\n", + (unsigned long)(uintptr_t)manifest); + } + else if (img.fw_size > (uint32_t)(manifest - image)) { + wolfBoot_printf("UBL: manifest size %u exceeds eNVM\n", + (unsigned int)img.fw_size); + ret = -1; + } + if (ret == 0) { + ret = wolfBoot_verify_integrity(&img); + if (ret != 0) { + wolfBoot_printf("UBL: eNVM wolfBoot digest mismatch\n"); + } + } + if (ret == 0) { + ret = wolfBoot_verify_authenticity(&img); + if (ret != 0) { + wolfBoot_printf("UBL: eNVM wolfBoot signature invalid\n"); + } + } +#ifdef MPFS_UBL_MIN_VERSION + if ((ret == 0) && + (wolfBoot_get_blob_version(manifest) < MPFS_UBL_MIN_VERSION)) { + wolfBoot_printf("UBL: eNVM wolfBoot version 0x%x below floor 0x%x\n", + (unsigned int)wolfBoot_get_blob_version(manifest), + (unsigned int)MPFS_UBL_MIN_VERSION); + ret = -1; + } +#endif + if ((ret != 0) || (img.signature_ok != 1) || (img.sha_ok != 1)) { + wolfBoot_printf("UBL: eNVM wolfBoot verify FAILED (%d)\n", ret); + wolfBoot_panic(); + } + + wolfBoot_printf("UBL: eNVM wolfBoot signature OK (%u ms), entering 0x%08lx\n", + (unsigned int)((rdcycle() - start) / + (MPFS_CPU_FREQ_RESET_MHZ * 1000U)), + (unsigned long)(uintptr_t)img.fw_base); + ubl_enter((uintptr_t)img.fw_base); +} diff --git a/hal/mpfs250_ubl.h b/hal/mpfs250_ubl.h new file mode 100644 index 0000000000..c73a43dcb7 --- /dev/null +++ b/hal/mpfs250_ubl.h @@ -0,0 +1,44 @@ +/* mpfs250_ubl.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* PolarFire SoC boot mode 2 User Boot Loader (UBL) stub. + * + * The System Controller releases all five harts at the stub entry. The + * E51 verifies the eNVM wolfBoot manifest; the U54s park in + * src/boot_riscv_start.S on a DTIM mailbox until the E51 publishes the + * verified entry. The mailbox sits at the top of the E51 DTIM, above + * everything the eNVM wolfBoot uses (hal/mpfs250.h). No UL suffixes: + * also used from assembly. */ + +#ifndef MPFS250_UBL_H +#define MPFS250_UBL_H + +/* DTIM content is undefined at power-on and the U54s may read the mailbox + * before the E51 has cleared it, so the entry is valid only with its + * complement and the magic beside it; the E51 writes the magic last. */ +#define MPFS_UBL_MAGIC_ADDR 0x01001F00 /* u32 MPFS_UBL_MAGIC */ +#define MPFS_UBL_ENTRY_ADDR 0x01001F08 /* u64 entry */ +#define MPFS_UBL_ENTRY_INV_ADDR 0x01001F10 /* u64 ~entry */ +#define MPFS_UBL_MAGIC 0x55424C21 /* "UBL!" */ +/* One 32-bit acknowledgement word per hart, written before it jumps. */ +#define MPFS_UBL_ACK_BASE 0x01001F20 + +#endif /* MPFS250_UBL_H */ diff --git a/hal/mpfs250_ubl.ld b/hal/mpfs250_ubl.ld new file mode 100644 index 0000000000..f52414e6e9 --- /dev/null +++ b/hal/mpfs250_ubl.ld @@ -0,0 +1,90 @@ +/* PolarFire SoC MPFS250 boot mode 2 User Boot Loader (UBL) linker script + * + * The stub lives in authenticated sNVM pages. The System Controller copies + * it to the LIM address in the UBL header (taken from this ELF) and releases + * all five harts at _reset, so VMA == LMA and nothing is relocated at start. + * + * Memory region: + * LIM - L2 Loosely Integrated Memory at 0x08000000. Only the ways not + * enabled as cache back it; at reset that is almost all of it. + */ + +OUTPUT_ARCH( "riscv" ) + +ENTRY( _reset ) + +MEMORY +{ + LIM (rwx) : ORIGIN = @WOLFBOOT_ORIGIN@, LENGTH = @BOOTLOADER_PARTITION_SIZE@ +} + +PROVIDE(STACK_SIZE = @STACK_SIZE@); + +/* sNVM holds 221 pages of 236 bytes for the UBL, 60 of which go to the UBL + * header the programmer prepends, so the image copied by the System + * Controller (.init + .text + .data, .bss is NOLOAD) is capped here. */ +MPFS_UBL_IMAGE_MAX = 221 * 236 - 60; + +ASSERT(ORIGIN(LIM) >= 0x08000000 && ORIGIN(LIM) + LENGTH(LIM) <= 0x081E0000, + "UBL must lie inside the LIM 0x08000000..0x081DFFFF") + +SECTIONS +{ + .init : ALIGN(0x10) + { + _start_text = .; + KEEP(*(.init)) + . = ALIGN(0x10); + } > LIM + + .text : ALIGN(0x10) + { + _start_vector = .; + KEEP(*(.isr_vector)) + KEEP(*(.trap_vector)) + . = ALIGN(0x10); + *(.text*) + *(.rodata*) + *(.srodata*) + . = ALIGN(8); + _end_text = .; + } > LIM + + .data : ALIGN(0x10) + { + _start_data = .; + *(.data*) + . = ALIGN(4); + _global_pointer = . + 0x800; + *(.sdata*) + . = ALIGN(4); + KEEP(*(.keystore*)) + . = ALIGN(8); + _end_data = .; + } > LIM + + .bss (NOLOAD) : ALIGN(0x10) + { + _start_bss = .; + *(.bss*) + *(.sbss*) + *(COMMON) + . = ALIGN(8); + _end_bss = .; + _end = .; + } > LIM +} + +/* Nothing is copied at start: the System Controller places the whole image. */ +_stored_data = ADDR(.data); + +PROVIDE(_start_heap = _end); +PROVIDE(_end_stack = ORIGIN(LIM) + LENGTH(LIM)); +PROVIDE(__global_pointer$ = _global_pointer); + +ASSERT(_start_text == ORIGIN(LIM), + "ERROR: UBL entry must be the first byte of the image") +ASSERT(_end_data - _start_text <= MPFS_UBL_IMAGE_MAX, + "ERROR: UBL image exceeds the sNVM budget (221 pages)") +ASSERT(_end <= _end_stack - STACK_SIZE, + "ERROR: UBL .bss overlaps the stack") diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 650cbb7032..8a540728c0 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -24,6 +24,9 @@ #ifdef TARGET_mpfs250 #include "hal/mpfs250.h" #endif +#ifdef TARGET_mpfs250_ubl +#include "hal/mpfs250_ubl.h" +#endif /* RV64 M-mode: direct boot from eNVM; a0=hartid from CSR @@ -34,7 +37,91 @@ _reset: #if __riscv_xlen == 64 -#ifdef WOLFBOOT_RISCV_MMODE +#if defined(WOLFBOOT_RISCV_MMODE) && defined(TARGET_mpfs250_ubl) + /* Boot mode 2 UBL stub: the System Controller has already copied the + * whole image to the LIM and released all five harts here, so VMA == LMA + * and nothing is copied. The E51 verifies the eNVM wolfBoot manifest; + * the U54s park on the DTIM mailbox until it publishes the entry, then + * every hart enters the eNVM reset vector as in a boot mode 1 boot. */ + csrr a0, mhartid + mv tp, a0 + + li t0, MSTATUS_MIE + csrc mstatus, t0 + csrw mie, zero + csrw mip, zero + csrw mscratch, zero + csrw mcause, zero + csrw mepc, zero + csrw pmpcfg0, zero + csrw pmpcfg2, zero + + la t0, trap_vector_table + csrw mtvec, t0 + + bnez a0, .L_ubl_secondary_wait + + /* Close the mailbox gate before anything slow (.bss clear, verify): an + * MSS reset leaves the previous boot's magic and entry in DTIM, and a + * stale pair is indistinguishable from a fresh one, so the secondaries + * would jump into an eNVM image this boot has not verified. */ + li t0, MPFS_UBL_MAGIC_ADDR + sw zero, 0(t0) + fence iorw, iorw + + .option push + .option norelax + la gp, __global_pointer$ + .option pop + + la sp, _end_stack + li t0, -16 + and sp, sp, t0 + mv s0, sp + + /* LIM comes out of reset with uninitialised ECC and a doubleword store is + * what writes it, so cover everything the System Controller did not copy: + * .bss, the heap and the stack, which faults on its first read. */ + la t0, _start_bss + la t1, _end_stack +.L_ubl_lim_clear: + beq t0, t1, .L_ubl_lim_clear_done + sd zero, 0(t0) + addi t0, t0, 8 + j .L_ubl_lim_clear +.L_ubl_lim_clear_done: + + mv a0, tp + j main + +.L_ubl_secondary_wait: + li t1, MPFS_UBL_MAGIC_ADDR + li t3, MPFS_UBL_MAGIC + /* Wait for the boot hart to close the gate, so only a pair published by + * this boot can release us. */ +.L_ubl_wait_closed: + lwu t2, 0(t1) + beq t2, t3, .L_ubl_wait_closed +.L_ubl_wait_entry: + lwu t2, 0(t1) + bne t2, t3, .L_ubl_wait_entry + ld t0, (MPFS_UBL_ENTRY_ADDR - MPFS_UBL_MAGIC_ADDR)(t1) + ld t2, (MPFS_UBL_ENTRY_INV_ADDR - MPFS_UBL_MAGIC_ADDR)(t1) + not t2, t2 + bne t0, t2, .L_ubl_wait_entry + beqz t0, .L_ubl_wait_entry + + /* Acknowledge, then jump; the E51 waits for the acks before it follows */ + li t1, MPFS_UBL_ACK_BASE + slli t2, a0, 2 + add t1, t1, t2 + li t2, 1 + sw t2, 0(t1) + fence iorw, iorw + fence.i + jr t0 + +#elif defined(WOLFBOOT_RISCV_MMODE) /* RV64 M-mode: first code from eNVM at 0x20220100 */ call .L_clear_ras /* flush stale RAS prediction state */ diff --git a/tools/scripts/mpfs_bootmode2.sh b/tools/scripts/mpfs_bootmode2.sh new file mode 100755 index 0000000000..7828d66b5e --- /dev/null +++ b/tools/scripts/mpfs_bootmode2.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# +# PolarFire SoC boot mode 2 programming helper. +# +# Two jobs mpfsBootmodeProgrammer cannot do on its own: +# +# mpfs_bootmode2.sh [options] wolfboot.elf +# Program the User Boot Loader (the TARGET=mpfs250_ubl build) into sNVM +# and select boot mode 2 on a die the programmer does not list (the +# secure "S" parts, e.g. MPFS250TS, which an authenticated sNVM client +# needs). The programmer is run in --dryrun to produce the UBL image +# (its 60-byte header, SHA-256 and Intel hex) and the USK; the fpgenprog +# steps it logged are replayed in a project for the requested die. +# +# mpfs_bootmode2.sh --envm [options] wolfboot_envm_signed.elf +# Program the eNVM only (the signed wolfBoot and its manifest) and leave +# the boot mode and sNVM alone, i.e. update the eNVM wolfBoot under boot +# mode 2. The programmer always selects boot mode 1 when it programs +# eNVM, so its envm_client step is replayed without the boot-mode step. +# +# Boot mode 1 is restored with the programmer itself: +# java -jar mpfsBootmodeProgrammer.jar --bootmode 1 --die MPFS250T ... wolfboot.elf +# +# Options: +# --die DIE fpgenprog die (default MPFS250TS) +# --package PKG package (default FCG1152) +# --snvm-page N first sNVM page of the UBL (default 0) +# --usk KEY 12-byte hex USK, or 0 for a random one (default 0) +# --encrypt store the UBL as authenticated ciphertext +# --regenerate-smk also generate a new sNVM Master Key. The authenticated +# sNVM client the UBL lives in cannot be created on a device +# whose SMK has never been initialized, and the programmer +# never asks for one. A new SMK invalidates every existing +# authenticated or encrypted sNVM client on the device. +# --workdir DIR output directory (default: current directory) +# --dryrun generate everything, do not program +# +# Requires SC_INSTALL_DIR (SoftConsole) and FPGENPROG (Libero fpgenprog). + +set -e + +DIE=MPFS250TS +PACKAGE=FCG1152 +SNVM_PAGE=0 +USK=0 +ENCRYPT="" +SMK=0 +WORKDIR="$PWD" +DRYRUN=0 +ENVM=0 + +while [ $# -gt 1 ]; do + case "$1" in + --die) DIE="$2"; shift 2 ;; + --package) PACKAGE="$2"; shift 2 ;; + --snvm-page) SNVM_PAGE="$2"; shift 2 ;; + --usk) USK="$2"; shift 2 ;; + --encrypt) ENCRYPT="--encrypt"; shift ;; + --regenerate-smk) SMK=1; shift ;; + --workdir) WORKDIR="$2"; shift 2 ;; + --dryrun) DRYRUN=1; shift ;; + --envm) ENVM=1; shift ;; + *) echo "unknown option: $1" >&2; exit 1 ;; + esac +done +ELF="${1:-}" +[ -n "$ELF" ] && [ -f "$ELF" ] || { echo "usage: $0 [--envm] [options] wolfboot.elf" >&2; exit 1; } +[ -n "${SC_INSTALL_DIR:-}" ] || { echo "SC_INSTALL_DIR is not set" >&2; exit 1; } +[ -x "${FPGENPROG:-}" ] || { echo "FPGENPROG is not an executable" >&2; exit 1; } + +JAVA="$SC_INSTALL_DIR/eclipse/jre/bin/java" +JAR="$SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar" +# The programmer knows the non-secure die names only; it is just generating +# the image here, so hand it the base die. +JAR_DIE="${DIE%S}" +mkdir -p "$WORKDIR" +WORKDIR="$(cd "$WORKDIR" && pwd)" +NAME="$(basename "$ELF" .elf)" +# The programmer takes the ELF relative to its working directory. +cmp -s "$ELF" "$WORKDIR/$NAME.elf" 2>/dev/null || cp "$ELF" "$WORKDIR/$NAME.elf" + +if [ "$ENVM" = 1 ]; then + MODE=1 + STEP=envm_client + GEN="$WORKDIR/bootmode1" + JAR_ARGS="--bootmode 1" +else + MODE=2 + STEP=snvm_client + GEN="$WORKDIR/bootmode2" + JAR_ARGS="--bootmode 2 --snvm_page $SNVM_PAGE --usk $USK $ENCRYPT" +fi +PROJ="$WORKDIR/bootmode${MODE}_$DIE/fpgenprogProject" +LOG="$GEN/debugLog.txt" + +echo "[1/3] generating the image with mpfsBootmodeProgrammer (dry run)" +# Boot mode 2 on a non-secure die fails at bitstream generation; the image +# and the USK are already written by then. +# shellcheck disable=SC2086 +"$JAVA" -jar "$JAR" $JAR_ARGS --die "$JAR_DIE" --package "$PACKAGE" --dryrun \ + --workdir "$WORKDIR" "$NAME.elf" > "$WORKDIR/bootmode${MODE}_jar.log" 2>&1 || true +[ -f "$LOG" ] && grep -q "fpgenprog $STEP " "$LOG" || { + echo "mpfsBootmodeProgrammer produced no image; see $WORKDIR/bootmode${MODE}_jar.log" >&2 + exit 1 +} + +# The exact arguments the programmer used for a step, pointed at the new project. +cmd_args() { + grep -o "fpgenprog $1 [^\"]*" "$LOG" | tail -1 | sed "s/^fpgenprog $1 //" | \ + sed "s|--location [^ ]*|--location $PROJ|" +} +CLIENT_ARGS="$(cmd_args $STEP)" +if [ "$SMK" = 1 ]; then + [ "$MODE" = 2 ] || { echo "--regenerate-smk applies to boot mode 2 only" >&2; exit 1; } + echo "note: generating a new sNVM Master Key; authenticated sNVM clients" + echo " already on this device will no longer authenticate" + CLIENT_ARGS="$CLIENT_ARGS --regenerate_smk" +fi +BOOT_INFO_ARGS="" +[ "$MODE" = 2 ] && BOOT_INFO_ARGS="$(cmd_args mss_boot_info)" + +echo "[2/3] generating the bitstream for $DIE" +rm -rf "$PROJ" +mkdir -p "$(dirname "$PROJ")" +"$FPGENPROG" new_project --location "$PROJ" --target_die "$DIE" --target_package "$PACKAGE" +if [ -n "$BOOT_INFO_ARGS" ]; then + # shellcheck disable=SC2086 + "$FPGENPROG" mss_boot_info $BOOT_INFO_ARGS +fi +# shellcheck disable=SC2086 +"$FPGENPROG" $STEP $CLIENT_ARGS +"$FPGENPROG" generate_bitstream --location "$PROJ" + +if [ "$MODE" = 2 ]; then + USKFILE="$(ls "$GEN"/"$NAME"-bm2-usk.txt 2>/dev/null | head -1)" + [ -n "$USKFILE" ] && echo "USK (keep it: a UBL update needs it): $USKFILE" +fi +if [ "$DRYRUN" = 1 ]; then + echo "[3/3] dry run: not programming" + exit 0 +fi +echo "[3/3] programming" +"$FPGENPROG" run_action --location "$PROJ" --action PROGRAM +if [ "$MODE" = 2 ]; then + echo "done: boot mode 2, UBL in sNVM from page $SNVM_PAGE" +else + echo "done: eNVM programmed, boot mode unchanged" +fi From 34aef2f07a258ebca3cb7fd2de3d5bf145b3b77e Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 17 Oct 2026 10:45:55 -0700 Subject: [PATCH 08/23] PolarFire SoC: watchdog hand-off to the OS --- docs/Targets.md | 7 ++++++- hal/mpfs250.c | 12 ++++++++++-- options.mk | 6 ++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/Targets.md b/docs/Targets.md index a0ee58884a..c9f7f997e0 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -963,6 +963,7 @@ These flags apply to `polarfire_mpfs250_m_qspi.config` and are added via `CFLAGS |------|---------|-------------| | `WATCHDOG` | undefined (disabled) | When defined, the E51 watchdog timer is **kept enabled** during wolfBoot operation with a generous timeout. When undefined, the WDT is **disabled** in `hal_init()` and re-enabled with the boot ROM default in `hal_prepare_boot()` before jumping to the application. Either way, the application receives a normal WDT. | | `WATCHDOG_TIMEOUT_MS` | `30000` (30 s) | Watchdog timeout in milliseconds when `WATCHDOG` is defined. ECDSA P-384 verification on E51 with portable C math is bounded at ~5 s; the default 30 s avoids any need to refresh the WDT during the long verify call. | +| `MPFS_WDT_NODES_KEEP` (make variable, `=1`) | undefined | Linux boot only: keep the five MSS watchdog nodes enabled in the dtb and stop the parked E51 from refreshing them, so the OS owns the watchdogs. See "Watchdog policy" below. | #### Stack overflow detection @@ -1307,7 +1308,11 @@ MAC addresses from the device serial number, and all five MSS watchdog nodes are **Watchdog policy:** the MSS watchdogs always count and reset the chip on timeout (they cannot be disabled in hardware, and `CONTROL=0` does not prevent the reset). After hand-off the parked E51 acts as a monitor and refreshes all five watchdogs; the OS watchdog driver is disabled via -the dtb fixup so the two never conflict. +the dtb fixup so the two never conflict. `MPFS_WDT_NODES_KEEP=1` hands the watchdogs to the OS +instead: the dtb nodes stay enabled and the parked E51 stops refreshing, so the OS driver arms +them at probe and something on the OS side (a watchdog daemon, or the design's own fabric +scheme) has to service them from then on. A design with its own fabric watchdog needs neither +setting; wolfBoot never touches fabric logic. **Hand-off / SMP flow:** secondary harts park in eNVM until the E51 signals image-copy completion (DTIM gate flag), then park in a WFI loop. The boot hart is released with a staged diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 6becd50ac8..3c43f66b99 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -322,11 +322,13 @@ int sbi_hal_hart_start(unsigned long hartid, unsigned long saddr, void __attribute__((noreturn)) hal_smode_boot(unsigned long entry, unsigned long hartid, unsigned long dtb) { +#ifndef MPFS_WDT_NODES_KEEP static const unsigned long park_wdt_bases[5] = { MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE }; unsigned int w; +#endif (void)hartid; /* the calling E51 hart is not the kernel boot hart */ @@ -377,9 +379,11 @@ hal_smode_boot(unsigned long entry, unsigned long hartid, unsigned long dtb) * it). With MVRP at maximum a refresh is always permitted. */ __asm__ volatile("csrw mie, %0" :: "r"(0x80UL)); /* MTIE */ while (1) { +#ifndef MPFS_WDT_NODES_KEEP for (w = 0; w < 5U; w++) { MSS_WDT_REFRESH(park_wdt_bases[w]) = 0xDEADC0DEU; } +#endif CLINT_MTIMECMP_REG(MPFS_FIRST_HART) = CLINT_MTIME_REG + (5UL * RTC_CLOCK_FREQ); __asm__ volatile("wfi"); @@ -1813,7 +1817,8 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) int off, ret; uint8_t device_serial_number[DEVICE_SERIAL_NUMBER_SIZE]; uint8_t mac_addr[6]; -#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) && \ + (!defined(MPFS_WDT_NODES_KEEP) || defined(MPFS_SCB_SMODE_DENY)) /* Nodes disabled below. Watchdogs: the MSS WDTs always count; the OS * driver arms them at probe but nothing pings them (no userspace * watchdog daemon in the default image), so the system would reset @@ -1821,8 +1826,10 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) * status and the parked E51 monitor keeps them refreshed. Re-enable * when an OS-side petting story exists. */ static const char *const cpu_off[] = { +#ifndef MPFS_WDT_NODES_KEEP "watchdog@20001000", "watchdog@20101000", "watchdog@20103000", "watchdog@20105000", "watchdog@20107000", +#endif #ifdef MPFS_SCB_SMODE_DENY /* PMP fences S-mode off the System Controller mailbox: the OS * drivers for it would only fault. */ @@ -1859,7 +1866,8 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) (void)wolfBoot_disk_dts_fixup(&ctx); #endif -#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) && \ + (!defined(MPFS_WDT_NODES_KEEP) || defined(MPFS_SCB_SMODE_DENY)) /* Disable the MSS watchdog dtb nodes BEFORE the serial-number read: this * does not depend on the serial number and MUST run even if the SC serial * read below fails. Otherwise the OS watchdog driver would arm them at diff --git a/options.mk b/options.mk index 7183804ba0..a4ad0739ba 100644 --- a/options.mk +++ b/options.mk @@ -166,6 +166,12 @@ ifeq ($(DISK_FAILOVER_ZEROIZE),1) endif CFLAGS+=-D"DISK_FAILOVER_ZEROIZE" endif +# Leave the MSS watchdog nodes in the dtb and stop the parked E51 refreshing them, so the OS owns +# the watchdogs after the hand-off and must service them itself. +ifeq ($(MPFS_WDT_NODES_KEEP),1) + CFLAGS+=-D"MPFS_WDT_NODES_KEEP" +endif + # PUF-derived KEK + RFC 3394 AES key-wrap for sNVM-stored keys. ifeq ($(SNVM_KEK),1) # The KEK only wraps the image-encryption key, so it rides on the AES-256 ENCRYPT build's AES. From a2e493a916aa6d1a1c3cf6b7108d9f2d276ce0f6 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 17 Oct 2026 10:45:55 -0700 Subject: [PATCH 09/23] PolarFire SoC: QSPI clock divider knob and word-mode FIFO reads --- docs/Targets.md | 9 +++++++ hal/mpfs250.c | 67 ++++++++++++++++++++++++++++++++++++++----------- hal/mpfs250.h | 30 +++++++++++++++++++--- options.mk | 5 ++++ 4 files changed, 92 insertions(+), 19 deletions(-) diff --git a/docs/Targets.md b/docs/Targets.md index c9f7f997e0..1d1aa9edcd 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -964,6 +964,7 @@ These flags apply to `polarfire_mpfs250_m_qspi.config` and are added via `CFLAGS | `WATCHDOG` | undefined (disabled) | When defined, the E51 watchdog timer is **kept enabled** during wolfBoot operation with a generous timeout. When undefined, the WDT is **disabled** in `hal_init()` and re-enabled with the boot ROM default in `hal_prepare_boot()` before jumping to the application. Either way, the application receives a normal WDT. | | `WATCHDOG_TIMEOUT_MS` | `30000` (30 s) | Watchdog timeout in milliseconds when `WATCHDOG` is defined. ECDSA P-384 verification on E51 with portable C math is bounded at ~5 s; the default 30 s avoids any need to refresh the WDT during the long verify call. | | `MPFS_WDT_NODES_KEEP` (make variable, `=1`) | undefined | Linux boot only: keep the five MSS watchdog nodes enabled in the dtb and stop the parked E51 from refreshing them, so the OS owns the watchdogs. See "Watchdog policy" below. | +| `MPFS_QSPI_CLK_DIV` (make variable) | `QSPI_CLK_DIV_8` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h`. See "PolarFire SoC QSPI". | #### Stack overflow detection @@ -1090,6 +1091,14 @@ Example single-shot build: `cp config/examples/polarfire_mpfs250_qspi.config .co Notes: - Both modes support full read, write, and erase operations. +- SCK is the controller clock divided by the CoreQSPI `CLKRATE` field, SCK = PCLK / (2 * CLKRATE). + `MPFS_QSPI_CLK_DIV` selects the divider by its `QSPI_CLK_DIV_*` name from `hal/mpfs250.h` + (`QSPI_CLK_DIV_2` through `QSPI_CLK_DIV_30`, CLKRATE 1..15). The default `QSPI_CLK_DIV_8` is + about 18.75 MHz from the 150 MHz APB clock, where read throughput stops improving: from there on + the polled FIFO transfer rather than SCK sets the rate, so a faster SCK buys nothing. The MSS QSPI + PCLK is the MSS APB clock, which is 40 MHz at the M-mode reset clock until wolfBoot's PLL + bring-up, so an M-mode build that starts QSPI before `mss_pll_init()` runs SCK that much slower; + the SC QSPI is clocked by the System Controller and is unaffected by the MSS PLL. - For QSPI-based boot flows, disable SD/eMMC in the config (`DISK_SDCARD=0`, `DISK_EMMC=0`) unless you explicitly want wolfBoot to load from disk and the application from QSPI. - The MSS QSPI path expects external flash on the MSS QSPI pins; the SC QSPI path is for diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 3c43f66b99..26aa15fecd 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -2121,13 +2121,13 @@ int qspi_init(void) QSPI_IEN = 0; /* Configure QSPI Control Register: - * - Clock divider for ~5MHz (conservative) + * - Clock divider (MPFS_QSPI_CLK_DIV, ~5 MHz by default) * - CPOL=1 (clock idle high) for SPI Mode 3 * - Sample on SCK edge * - Enable controller */ QSPI_CONTROL = - (QSPI_CLK_DIV_30 << QSPI_CTRL_CLKRATE_OFFSET) | + (MPFS_QSPI_CLK_DIV << QSPI_CTRL_CLKRATE_OFFSET) | QSPI_CTRL_CLKIDLE | QSPI_CTRL_SAMPLE_SCK | QSPI_CTRL_EN; @@ -2174,6 +2174,8 @@ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, uint32_t i; uint32_t timeout; uint32_t frame_cmd; + uint32_t rx_word; + int word_read; /* Wait for controller to be ready before starting */ timeout = QSPI_TIMEOUT_TRIES; @@ -2210,10 +2212,18 @@ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, * command phase avoids this. The flash determines command vs data * boundaries from the opcode, not the controller's phase. */ frame_cmd = read_mode ? cmd_len : total_bytes; + /* FBYTE makes RXAVAIL/TXAVAIL count single bytes; FWORD makes them count + * 32-bit words, so a read polls STATUS once per four bytes instead of + * once per byte and pulls them through QSPI_X4_RX_DATA in one access. + * The polled loop, not SCK, is what caps the byte-mode read rate (it + * measures the same from CLKRATE /8 down to /2). Word mode needs a data + * phase that is a whole number of words; anything else stays byte-mode. */ + word_read = (read_mode && data_len >= 4u && (data_len & 3u) == 0u) ? 1 : 0; frames = ((total_bytes & 0xFFFF) << QSPI_FRAMES_TOTALBYTES_OFFSET) | ((frame_cmd & 0x1FF) << QSPI_FRAMES_CMDBYTES_OFFSET) | ((dummy_cycles & 0xF) << QSPI_FRAMES_IDLE_OFFSET) | - (1u << QSPI_FRAMES_FBYTE_OFFSET); + (word_read ? (1u << QSPI_FRAMES_FWORD_OFFSET) + : (1u << QSPI_FRAMES_FBYTE_OFFSET)); QSPI_FRAMES = frames; @@ -2237,18 +2247,42 @@ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, } if (read_mode) { - /* Read mode: poll RXAVAIL for each data byte. */ - for (i = 0; i < data_len; i++) { - timeout = QSPI_RX_TIMEOUT_TRIES; - while (!(QSPI_STATUS & QSPI_STATUS_RXAVAIL) && --timeout); - if (timeout == 0) { - #ifdef DEBUG_QSPI - wolfBoot_printf("QSPI: RX timeout at byte %d, status=0x%x\n", - i, QSPI_STATUS); - #endif - return -3; + if (word_read) { + /* One STATUS poll and one 32-bit read per four bytes. The + * destination is not necessarily word-aligned, so the word is + * stored a byte at a time; the saving is in the polling and the + * FIFO accesses, not the stores. */ + for (i = 0; i < data_len; i += 4u) { + timeout = QSPI_RX_TIMEOUT_TRIES; + while (!(QSPI_STATUS & QSPI_STATUS_RXAVAIL) && --timeout); + if (timeout == 0) { + #ifdef DEBUG_QSPI + wolfBoot_printf("QSPI: RX timeout at word %d, status=0x%x\n", + i >> 2, QSPI_STATUS); + #endif + return -3; + } + rx_word = QSPI_X4_RX_DATA; + data[i] = (uint8_t)(rx_word & 0xFFu); + data[i + 1] = (uint8_t)((rx_word >> 8) & 0xFFu); + data[i + 2] = (uint8_t)((rx_word >> 16) & 0xFFu); + data[i + 3] = (uint8_t)((rx_word >> 24) & 0xFFu); + } + } + else { + /* Byte mode: poll RXAVAIL for each data byte. */ + for (i = 0; i < data_len; i++) { + timeout = QSPI_RX_TIMEOUT_TRIES; + while (!(QSPI_STATUS & QSPI_STATUS_RXAVAIL) && --timeout); + if (timeout == 0) { + #ifdef DEBUG_QSPI + wolfBoot_printf("QSPI: RX timeout at byte %d, status=0x%x\n", + i, QSPI_STATUS); + #endif + return -3; + } + data[i] = QSPI_RX_DATA; } - data[i] = QSPI_RX_DATA; } /* Wait for receive complete */ timeout = QSPI_RX_TIMEOUT_TRIES; @@ -2347,7 +2381,10 @@ static int qspi_enter_4byte_mode(void) /* Read from QSPI flash (4-byte addressing) */ static int qspi_flash_read(uint32_t address, uint8_t *data, uint32_t len) { - const uint32_t max_chunk = 0xFFFF - 5; /* total_bytes is 16-bit, cmd is 5 */ + /* total_bytes is 16-bit and the command takes 5 of it; round down to a + * whole number of words so a bulk read stays on the word-mode FIFO path + * in qspi_transfer_block instead of falling back to byte mode. */ + const uint32_t max_chunk = (0xFFFFu - 5u) & ~3u; uint8_t cmd[5]; uint32_t remaining = len; uint32_t chunk_len; diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 6ee22b37d9..79e3728e75 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -789,8 +789,10 @@ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); /* QSPI Flash Controller * * Two CoreQSPI v2 controllers with identical register layouts: - * SC QSPI (MPFS_SC_SPI=1, default): 0x37020100 -- fabric-connected flash - * MSS QSPI (MPFS_SC_SPI=0): 0x21000000 -- MSS QSPI pins + * SC QSPI (-DMPFS_SC_SPI, every shipped QSPI config): 0x37020100, + * the System Controller's flash + * MSS QSPI (MPFS_SC_SPI undefined): 0x21000000, + * the MSS QSPI pins */ /* QSPI Controller Base Address */ @@ -851,14 +853,34 @@ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); #define QSPI_STATUS_TXAVAIL (1u << 3) #define QSPI_STATUS_READY (1u << 7) -/* QSPI Clock Configuration */ +/* QSPI Clock Configuration: SCK = PCLK / (2 * CLKRATE). The MSS QSPI PCLK + * is the APB clock, 150 MHz under HSS and 40 MHz at the M-mode reset clock + * (MSS_APB_AHB_CLK) until mss_pll_init() raises it; the SC QSPI has the + * System Controller's own clock. MPFS_QSPI_CLK_DIV picks the divider. */ #define QSPI_CLK_DIV_2 0x01u #define QSPI_CLK_DIV_4 0x02u #define QSPI_CLK_DIV_6 0x03u -#define QSPI_CLK_DIV_8 0x04u +#define QSPI_CLK_DIV_8 0x04u /* ~18.75 MHz from 150 MHz APB */ #define QSPI_CLK_DIV_10 0x05u #define QSPI_CLK_DIV_12 0x06u +#define QSPI_CLK_DIV_14 0x07u +#define QSPI_CLK_DIV_16 0x08u +#define QSPI_CLK_DIV_18 0x09u +#define QSPI_CLK_DIV_20 0x0Au +#define QSPI_CLK_DIV_22 0x0Bu +#define QSPI_CLK_DIV_24 0x0Cu +#define QSPI_CLK_DIV_26 0x0Du +#define QSPI_CLK_DIV_28 0x0Eu #define QSPI_CLK_DIV_30 0x0Fu /* ~5 MHz from 150 MHz APB */ +/* Divide by 8 is where read throughput stops improving: the polled FIFO + * transfer, not SCK, is the limit from there on. Anything slower only costs + * time, anything faster buys nothing. */ +#ifndef MPFS_QSPI_CLK_DIV +#define MPFS_QSPI_CLK_DIV QSPI_CLK_DIV_8 +#endif +#if (MPFS_QSPI_CLK_DIV < 0x01u) || (MPFS_QSPI_CLK_DIV > 0x0Fu) +#error MPFS_QSPI_CLK_DIV must be a CLKRATE of 1..15 (QSPI_CLK_DIV_2..QSPI_CLK_DIV_30) +#endif /* Micron MT25QL01G Flash Commands */ #define QSPI_CMD_READ_ID_OPCODE 0x9Fu /* JEDEC ID Read */ diff --git a/options.mk b/options.mk index a4ad0739ba..c7a278edeb 100644 --- a/options.mk +++ b/options.mk @@ -166,6 +166,11 @@ ifeq ($(DISK_FAILOVER_ZEROIZE),1) endif CFLAGS+=-D"DISK_FAILOVER_ZEROIZE" endif +# QSPI SCK divider: a QSPI_CLK_DIV_* name from hal/mpfs250.h, default QSPI_CLK_DIV_8. +ifneq ($(MPFS_QSPI_CLK_DIV),) + CFLAGS+=-DMPFS_QSPI_CLK_DIV=$(MPFS_QSPI_CLK_DIV) +endif + # Leave the MSS watchdog nodes in the dtb and stop the parked E51 refreshing them, so the OS owns # the watchdogs after the hand-off and must service them itself. ifeq ($(MPFS_WDT_NODES_KEEP),1) From 1ea70c0054738cf03553e2deee9625ddd9f5a98d Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 14 Oct 2026 14:18:31 -0700 Subject: [PATCH 10/23] PolarFire SoC: consolidate the documentation into docs/PolarFire.md with a quick start --- config/examples/polarfire_mpfs250_m.config | 2 +- docs/PolarFire.md | 1383 ++++++++++++++++++++ docs/README.md | 1 + docs/Targets.md | 1265 +----------------- docs/encrypted_partitions.md | 2 +- docs/keystore.md | 2 +- hal/mpfs250.c | 2 +- 7 files changed, 1417 insertions(+), 1240 deletions(-) create mode 100644 docs/PolarFire.md diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index faf4e143cb..33c26eb329 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -116,7 +116,7 @@ WOLFBOOT_RAMBOOT_MAX_SIZE=0x08000000 # Use update_disk loader (partition A/B numbering instead of flash addresses). # BOOT_PART_A / BOOT_PART_B are 0-indexed GPT entry numbers. GPT partitions -# in our SD card layout (see tools/scripts/program-sdcard.sh): +# in the SD card layout of docs/PolarFire.md: # index 0 (parted "boot" 1 MiB - 33 MiB) -> active boot FIT # index 1 (parted "update" 33 MiB - 65 MiB) -> inactive/update slot # index 2 (parted "rootfs" 65 MiB - end) -> Linux rootfs diff --git a/docs/PolarFire.md b/docs/PolarFire.md new file mode 100644 index 0000000000..8012c019d4 --- /dev/null +++ b/docs/PolarFire.md @@ -0,0 +1,1383 @@ +# Microchip PolarFire SoC + +Everything wolfBoot does on PolarFire SoC, in one document: the quick start, every supported boot configuration, and the device-specific features (sNVM/PUF key storage, the SBI vendor extension, boot mode 2). The PolarFire SoC is a 64-bit RISC-V SoC with a five-core CPU cluster (1x E51 monitor core and 4x U54 application cores) and FPGA fabric; wolfBoot is tested on MPFS250. + +wolfBoot runs here either as a payload of Microchip's Hart Software Services or as a full replacement for it, in which case it owns LPDDR4 bring-up, image verification and the SBI the OS runs on. [Targets.md](Targets.md#microchip-polarfire-soc) carries the summary tables; this document is the detail behind them. + +> **Filesystem boot:** this target can also read its signed image from a **file** on a read-only FAT32 or ext4 partition instead of from raw offset 0. Build with `DISK_FS=fat32|ext4|both` and set `BOOT_FILE_A` / `BOOT_FILE_B`. See [Disk boot from a read-only filesystem (FAT32 / ext4)](compile.md#disk-boot-from-a-read-only-filesystem-fat32--ext4). + +## Contents + +- [Quick start](#quick-start) +- [Features](#features) +- [Boot configurations](#boot-configurations) +- [M-mode build flags](#m-mode-build-flags) +- [Building wolfBoot](#building-wolfboot) +- [QSPI](#qspi) +- [HSS S-mode with L2-LIM (no DDR)](#hss-s-mode-with-l2-lim-no-ddr) +- [M-mode (bare-metal eNVM boot)](#m-mode-bare-metal-envm-boot) +- [M-mode + DDR: booting Linux (minimal SBI)](#m-mode--ddr-booting-linux-minimal-sbi) +- [Boot mode 2: User Boot Loader stub](#boot-mode-2-user-boot-loader-stub) +- [Encryption](#encryption) +- [PQC (ML-DSA)](#pqc-ml-dsa) +- [Writing a card](#writing-a-card) +- [Building Hart Software Services (HSS)](#building-hart-software-services-hss) +- [Building Yocto-SDK Linux](#building-yocto-sdk-linux) +- [Debugging](#debugging) +- [Example boot output](#example-boot-output) +- [Performance comparison](#performance-comparison) +- [Source files](#source-files) +- [SBI vendor extension for the OS](#sbi-vendor-extension-for-the-os) +- [Hardware root of trust: PUF KEK, sNVM keystore, wrapped image key](#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key) + +## Quick start + +Three ways to get wolfBoot running on an MPFS250T board, shortest first. Each path is self-contained: build, program, and what the console must print for the step to have worked. The sections after this one are the reference material behind them. + +| Path | What it proves | eNVM | Reversible by | +|---|---|---|---| +| [1. HSS payload](#path-1-run-wolfboot-as-an-hss-payload) | wolfBoot verifies and boots an image on this board | untouched (HSS stays) | swapping the SD card | +| [2. wolfBoot replaces HSS](#path-2-wolfboot-replaces-hss-and-boots-linux) | the full configuration: M-mode, LPDDR4, signed FIT, Linux | reprogrammed | reprogramming HSS | +| [3. Yocto](#path-3-let-yocto-build-it) | the same as path 2, built and packaged by bitbake | reprogrammed | reprogramming HSS | + +Path 1 needs no FPGA design files and never writes the device, so it is the right first contact with an unfamiliar board. Path 2 is the configuration the target is built for. + +### Before you start + +| Need | Where it comes from | +|---|---| +| RISC-V bare-metal toolchain | SoftConsole (`/opt/Microchip/SoftConsole-v2022.2-RISC-V-747/riscv-unknown-elf-gcc/bin`), or any `riscv64-unknown-elf-gcc` | +| `mpfsBootmodeProgrammer.jar` + a JRE | SoftConsole (`extras/mpfs/`, `eclipse/jre/bin/java`); paths 2 and 3 only | +| FlashPro programmer | on-board on the Video Kit, over the same USB cable as the consoles | +| `dtc`, `mkimage` | `device-tree-compiler`, `u-boot-tools` | +| `hss-payload-generator` | `tools/hss-payload-generator` in the [hart-software-services](https://github.com/polarfire-soc/hart-software-services) tree; path 1 only | +| `fpga_design_config/` for the board | an HSS build (`build/boards//fpga_design_config`) or a Libero MSS Configurator export; paths 2 and 3 only | + +```sh +git clone --recursive https://github.com/wolfSSL/wolfBoot.git +cd wolfBoot +export PATH=/opt/Microchip/SoftConsole-v2022.2-RISC-V-747/riscv-unknown-elf-gcc/bin:$PATH +``` + +Two things that cause most first-attempt failures: + +* **Keys.** The first build generates `wolfboot_signing_private_key.der` and compiles its public half into `src/keystore.c`, so the bootloader only accepts images signed with that key. Back the private key up before anything else; `make keysclean` discards both halves and the next build makes a new pair, which invalidates every image already signed. +* **`make clean` between paths.** Object files are configuration-specific. Reusing them across configs produces link errors naming symbols that are compiled out (`undefined reference to wolfBoot_get_dts_size`) or, worse, a bootloader built half from each config. + +Console: the E51 prints on MMUART0, the first of the four USB-serial interfaces the board exposes, 115200 8N1. + +### Path 1: run wolfBoot as an HSS payload + +HSS keeps the eNVM and loads wolfBoot as its S-mode payload from the SD card, so nothing about the device changes and the original card restores the board exactly. + +```sh +cp config/examples/polarfire_mpfs250.config .config +make clean +make +dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb +hss-payload-generator -c hal/mpfs.yaml wolfboot-payload.bin +``` + +`make` produces `wolfboot.elf`, the raw `wolfboot.bin`, a signed test application and `factory.bin`. The payload generator reads `wolfboot.elf` and `hal/mpfs.dtb` as named in `hal/mpfs.yaml`, so build the dtb first; the output name is yours to choose (about 63 KB for a 61 KB wolfBoot). Do not call it `wolfboot.bin` or it overwrites the raw binary. + +Write `wolfboot-payload.bin` to the card's HSS boot partition - the GPT entry with type GUID `21686148-6449-6E6F-744E-656564454649`, or sector 0 on a card with no GPT. With the card in the board, HSS's `usbdmsc` command exports it over USB; `lsblk` then names the partition: + +```sh +sudo dd if=wolfboot-payload.bin of=/dev/sdX1 bs=512 +sudo cmp wolfboot-payload.bin /dev/sdX1 +``` + +Power-cycle. HSS prints its own banner, then wolfBoot's: + +``` +wolfBoot Version: ... +``` + +That line is the whole point of the path: the board runs wolfBoot. Signing an image and putting it in a slot for wolfBoot to verify follows the same steps as path 2. + +### Path 2: wolfBoot replaces HSS and boots Linux + +wolfBoot runs from eNVM on the E51, brings up the PLLs and LPDDR4, loads a signed FIT (kernel + dtb) from the SD card, verifies it with ECDSA P-384 over SHA-384, and hands a U54 into S-mode while staying resident as the SBI. + +#### Build + +```sh +cp config/examples/polarfire_mpfs250_m.config .config +make clean +make LIBERO_FPGA_CONFIG_DIR=/path/to/boards/mpfs-video-kit/fpga_design_config wolfboot.elf +``` + +About 74 KB of text. The build stops if `LIBERO_FPGA_CONFIG_DIR` is empty: an S-mode OS runs from DDR, and those headers carry the board's DDR, clock and SEG settings, which cannot be guessed. Add `CFLAGS_EXTRA+=-DDEBUG_DDR` for register-level training traces during bring-up. + +Append any extra `CFLAGS_EXTRA` as a line in `.config`, never as `make CFLAGS_EXTRA=...`. A make command-line assignment overrides the `CFLAGS_EXTRA+=` lines the config already carries instead of adding to them, which for this target silently drops `-DWOLFBOOT_MMODE_SMODE_BOOT` and with it the whole SBI runtime, so the banner appears and Linux never starts. + +#### Card + +Three GPT partitions: the active boot slot, the update slot, and the rootfs. GPT entries 0, 1 and 2 are what `BOOT_PART_A`, `BOOT_PART_B` and the kernel's root device refer to. + +```sh +sudo parted -s /dev/sdX mklabel gpt \ + mkpart boot 1MiB 33MiB \ + mkpart update 33MiB 65MiB \ + mkpart rootfs 65MiB 100% +``` + +Sign the FIT the Linux build produced, then write both: + +```sh +./tools/keytools/sign --ecc384 --sha384 fitImage wolfboot_signing_private_key.der 1 +sudo dd if=fitImage_v1_signed.bin of=/dev/sdX1 bs=1M status=progress +sudo dd if=core-image-...rootfs.ext4 of=/dev/sdX3 bs=4M status=progress +sync +``` + +The signed file is the input plus a 512-byte manifest header. Slot sizes above suit a 19 MB kernel FIT; grow them for a larger image, or for a FIT that carries the whole rootfs as an initramfs. For where the Linux FIT itself comes from, see [Building Yocto-SDK Linux](#building-yocto-sdk-linux). + +#### Program the eNVM + +Keep a copy of what you are about to replace - see "Restoring HSS" below - and do a `--dryrun` pass first. + +```sh +export SC_INSTALL_DIR=/opt/Microchip/SoftConsole-v2022.2-RISC-V-747 + +$SC_INSTALL_DIR/eclipse/jre/bin/java -jar \ + $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ + --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD wolfboot.elf +``` + +`--bootmode 1` is non-secure boot from eNVM: the System Controller releases the harts at the eNVM reset vector. The die and package must match the part; `--die` defaults to `MPFS250T_ES` and `--package` to `FCVG484`, neither of which is the Video Kit. + +#### Boot + +``` +wolfBoot Version: ... +Running on E51 (hart 0) in M-mode +DDR: Training+MTC PASS +DDR: Initialization COMPLETE +Valid GPT partition table +Versions, A:1 B:0 +Checking image integrity... +Verifying image signature... +Firmware Valid. +M->S handoff: entry=0x80200000 hart=0 dtb=0x8A000000 +[ 0.000000] Linux version ... +[ 0.000000] SBI implementation ID=0x776f6c66 Version=0x1 +... +login: +``` + +Four lines are the ones to check. `DDR: Initialization COMPLETE` means the memory came up; `Firmware Valid.` means the signature verified against the keystore in the image; `M->S handoff` means wolfBoot released a U54; and `SBI implementation ID=0x776f6c66` - ASCII `"wolf"` - means the kernel is talking to wolfBoot's own SBI rather than OpenSBI. The full annotated log is in [Example boot output](#example-boot-output). + +### Path 3: let Yocto build it + +The `meta-wolfssl` layer builds the bootloader, signs the FIT with the same keytools and writes a `.wic` with the partition layout above. Set `WOLFBOOT_CONFIG = "polarfire_mpfs250_m.config"`, point `WOLFBOOT_EXTRA_MAKE_FLAGS` at the `fpga_design_config` directory and supply a pre-generated ECC384 key through `WOLFBOOT_SIGNING_KEY`; the recipes deliberately never generate one. The PolarFire section of `recipes-wolfssl/wolfboot/README.md` in that layer has the full `local.conf` block. The eNVM programming step is still the `mpfsBootmodeProgrammer` command from path 2. + +### Restoring HSS + +Programming the eNVM is not one-way, and the boot mode does not change: both images are boot mode 1. + +```sh +$SC_INSTALL_DIR/eclipse/jre/bin/java -jar \ + $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ + --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD hss-envm-wrapper.elf +``` + +`hss-envm-wrapper.elf` comes from an HSS build for the board (`make BOARD=mpfs-video-kit`, output in `build/`). Build it before you need it. + +### Secure dies + +The programmer's `--die` list covers `MPFS025T` through `MPFS460T` and their `_ES` variants only - a secure ("S") die is not in it. For those parts `tools/scripts/mpfs_bootmode2.sh` drives Libero's `fpgenprog` directly: `--envm` programs the eNVM client only and leaves the boot mode alone, and the default run puts a signed User Boot Loader in sNVM and selects boot mode 2. See [Boot mode 2: User Boot Loader stub](#boot-mode-2-user-boot-loader-stub). + +### When it does not work + +| Symptom | Cause | +|---|---| +| `undefined reference to ...` after switching config | stale objects; `make clean` and rebuild | +| no output at all | wrong serial interface - the E51 is on MMUART0, the first of the four USB-serial interfaces | +| `Invalid elf, falling back to raw binary` | normal: a FIT is not an ELF, and wolfBoot says so before parsing it as a FIT | +| `SD Card: Card init failed (-1)` | the card was not ready: re-seat it and power-cycle. The board routes card-detect through the fabric, so this build assumes a card is present (`SDHCI_FORCE_CARD_DETECT`) and cannot report its absence | +| `Firmware Invalid` or a version of 0 | the image was signed with a key whose public half is not the one in `src/keystore.c`; re-sign, or rebuild the bootloader with the matching key | +| banner, then silence | the FIT loaded but Linux has no console: the dtb's own `/chosen/bootargs` wins unless `LINUX_BOOTARGS` is set at build time. See [bootargs](compile.md#linux-kernel-command-line-bootargs) | +| banner, then silence, and no `[SBI]` output at all | the image was built with `make CFLAGS_EXTRA=...`, which overrode the config's `-DWOLFBOOT_MMODE_SMODE_BOOT`. Check with `nm wolfboot.elf | grep sbi`; an M-mode build that boots an S-mode OS must have those symbols | +| DDR training stops or reports a failure | build with `CFLAGS_EXTRA+=-DDEBUG_DDR` and compare against the board's Libero settings | + +### Next + +* [All seven boot configurations](#boot-configurations) - SD, eMMC, QSPI, L2-LIM with no DDR, M-mode with and without DDR, and the boot mode 2 stub. +* [Build flags](#m-mode-build-flags) - watchdog policy, QSPI clock divider, stack-overflow detection. +* [Hardware root of trust](#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key) - keystore in sNVM, PUF-derived KEK, wrapped image-encryption key. +* [SBI vendor extension](#sbi-vendor-extension-for-the-os) - device serial, PUF-derived keys and System Controller services for the OS. + +## Features + +* RISC-V 64-bit: E51 monitor core `rv64imac` (no FPU), U54 application cores `rv64gc` +* Five-core CPU: 1x E51 monitor + 4x U54 application cores +* Integrated DDR3/4, LPDDR3/4 controller and PHY +* eNVM, L2 scratchpad and LIM on-chip memories, any of which wolfBoot can run from +* System Controller with secure NVM, an SRAM-PUF and the TeraFire (Athena F5200) crypto engine +* MSS and System Controller QSPI controllers, SD/eMMC + +## Boot configurations + +Seven ready-to-use config templates cover all supported boot mode / storage / memory combinations: + +| Configuration | Config File | Boot Mode | Storage | Memory | HSS | +|---------------|-------------|-----------|---------|--------|-----| +| **SDCard** | `polarfire_mpfs250.config` | S-mode (U54 via HSS) | SD Card | DDR | Yes | +| **eMMC** | `polarfire_mpfs250.config` + `DISK_EMMC=1` | S-mode (U54 via HSS) | eMMC | DDR | Yes | +| **QSPI (S-mode)** | `polarfire_mpfs250_qspi.config` | S-mode (U54 via HSS) | MSS or SC QSPI | DDR | Yes | +| **QSPI + L2-LIM** | `polarfire_mpfs250_hss_l2lim.config` | S-mode (U54 via HSS) | SC QSPI | L2-LIM (no DDR) | Yes | +| **M-Mode (no HSS)** | `polarfire_mpfs250_m_qspi.config` | M-mode (E51, no HSS) | SC QSPI | L2 Scratchpad | No | +| **M-Mode + DDR** | `polarfire_mpfs250_m.config` | M-mode (E51, no HSS) | SD Card | LPDDR4 (DDR) | No | +| **Boot mode 2 UBL** | `polarfire_mpfs250_ubl.config` | System Controller -> sNVM stub | eNVM (verifies it) | L2-LIM | No | + +The **M-Mode + DDR** configuration brings up the LPDDR4 controller from +the E51 in M-mode (no HSS), then loads a signed FIT image from SD card, +verifies it (SHA384 + ECC384) and hands off to a U54 hart in S-mode. +wolfBoot includes a minimal SBI runtime (`src/riscv_sbi.c`) so the +hand-off target can be a Linux kernel: tested booting 4-CPU SMP Yocto +Linux to a login prompt in ~40 s from power-on on the MPFS250T Video +Kit. Because all +LIBERO_SETTING_\* values are board-specific, this build pulls them from +a Libero/HSS-generated `fpga_design_config.h` pointed at by the +`LIBERO_FPGA_CONFIG_DIR` makefile variable - typical sources are an +HSS Video Kit build at +`/build/boards/mpfs-video-kit/fpga_design_config` or a Libero MSS +Configurator export. Setting `LIBERO_FPGA_CONFIG_DIR` automatically +defines `MPFS_DDR_INIT` and adds the directory to the include path +(see `arch.mk`); when unset, the DDR HAL is excluded and the build +still produces a working M-mode wolfBoot without DDR. Add +`-DDEBUG_DDR` to `CFLAGS_EXTRA` for verbose register-level traces +during bring-up. + +For GitHub Actions, `tools/ci/gen_mpfs_libero_stub.sh` generates a +compile-only stub (every `LIBERO_SETTING_*` symbol referenced by the +HAL, defined to `0`; loop-bound settings to `1U`) so the M-Mode + DDR +build path stays under continuous integration. The generated header is +**not** committed and **not** runnable - real boards must point +`LIBERO_FPGA_CONFIG_DIR` at the actual Libero / HSS output. + +Key build settings that differ between configurations: + +| Setting | SDCard | eMMC | QSPI | L2-LIM | M-Mode | M-Mode + DDR | +|---------|--------|------|------|--------|--------|--------------| +| `WOLFBOOT_ORIGIN` | `0x80000000` | `0x80000000` | `0x80000000` | `0x08040000` | `0x0A000000` | `0x0A000000` | +| `WOLFBOOT_LOAD_ADDRESS` | `0x8E000000` | `0x8E000000` | `0x8E000000` | `0x08060000` | `0x0A010200` | `0x82000000` | +| `WOLFBOOT_LOAD_DTS_ADDRESS` | `0x8A000000` | `0x8A000000` | `0x8A000000` | - | - | `0x8A000000` | +| `EXT_FLASH` | 0 | 0 | 1 | 1 | 1 | 0 | +| `DISK_SDCARD` | 1 | 0 | 0 | 0 | 0 | 1 | +| `DISK_EMMC` | 0 | 1 | 0 | 0 | 0 | 0 | +| `MPFS_L2LIM` | - | - | - | 1 | - | - | +| `RISCV_MMODE` | - | - | - | - | 1 | 1 | +| `LIBERO_FPGA_CONFIG_DIR` | - | - | - | - | - | required | +| `WOLFBOOT_MMODE_SMODE_BOOT` | - | - | - | - | - | 1 | +| Linker script | `mpfs250.ld` | `mpfs250.ld` | `mpfs250.ld` | `mpfs250-hss.ld` | `mpfs250-m.ld` | `mpfs250-m.ld` | +| HSS YAML | `mpfs.yaml` | `mpfs.yaml` | `mpfs.yaml` | `mpfs-l2lim.yaml` | N/A | N/A | +| `ELF` output | 1 | 1 | 1 | 0 (raw .bin) | 1 | 1 | + +> **Note:** the MPFS250 U54/E51 cores have no RISC-V crypto extensions (Zknh), so none of the +> configurations use them. The SD-card, QSPI and M-Mode + DDR configs build the portable RISC-V +> SHA assembly (`NO_ASM=0`); the size-constrained L2-LIM and M-mode QSPI configs set `NO_ASM=1` +> and use the C implementations. + +## M-mode build flags + +These flags apply to `polarfire_mpfs250_m_qspi.config` and are added via `CFLAGS_EXTRA+=-D...`. + +| Flag | Default | Description | +|------|---------|-------------| +| `WATCHDOG` | undefined (disabled) | When defined, the E51 watchdog timer is **kept enabled** during wolfBoot operation with a generous timeout. When undefined, the WDT is **disabled** in `hal_init()` and re-enabled with the boot ROM default in `hal_prepare_boot()` before jumping to the application. Either way, the application receives a normal WDT. | +| `WATCHDOG_TIMEOUT_MS` | `30000` (30 s) | Watchdog timeout in milliseconds when `WATCHDOG` is defined. ECDSA P-384 verification on E51 with portable C math is bounded at ~5 s; the default 30 s avoids any need to refresh the WDT during the long verify call. | +| `MPFS_WDT_NODES_KEEP` (make variable, `=1`) | undefined | Linux boot only: keep the five MSS watchdog nodes enabled in the dtb and stop the parked E51 from refreshing them, so the OS owns the watchdogs. See "Watchdog policy" below. | +| `MPFS_QSPI_CLK_DIV` (make variable) | `QSPI_CLK_DIV_8` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h`. See "PolarFire SoC QSPI". | + +### Stack overflow detection + +The trap handler in `src/boot_riscv.c` automatically detects stack overflow on synchronous exceptions (requires `DEBUG_BOOT`). When a trap fires with `SP < _main_hart_stack_bottom`, it prints: + +``` +TRAP: cause=2 epc=A000740 tval=0 + sp=A02FFE8 +STACK OVERFLOW: under by 24 +``` + +This is helpful for diagnosing illegal-instruction TRAPs at random valid `.text` addresses, which are the classic signature of stack overflow corrupting the return address. + +`STACK_SIZE` and `STACK_SIZE_PER_HART` come from the `.config` and are substituted into +`hal/mpfs250-m.ld`: 64 KB for the boot hart in `polarfire_mpfs250_m.config`, which is sized for an +ML-DSA-87 verify on top of two `IMAGE_HEADER_SIZE` stack buffers. An ECC384 + SHA384 verify peaks +at about 6 KB. + +## Building wolfBoot + +All build settings come from .config file. For this platform use `TARGET=mpfs250` and `ARCH=RISCV64`. + +See example configuration at `config/examples/polarfire_mpfs250.config`. + +```sh +# Setup .config (build settings) +cp config/examples/polarfire_mpfs250.config .config + +# build boot loader +make wolfboot.elf +``` + +To assemble this as a flashable image you need the 0x100 byte HART header added: + +```sh +git clone https://github.com/polarfire-soc/hart-software-services.git +cd hart-software-services +cd tools/hss-payload-generator +make +# install tool +sudo cp hss-payload-generator /usr/local/bin/ +``` + +The HSS MMC boot source looks for GPT with GUID "21686148-6449-6E6F-744E-656564454649" or sector "0" if no GPT found. That GUID is the default "BIOS" boot partition. + +The resulting image from `hss-payload-generator` can be directly placed into GPT BIOS partition. + +Use this command to assemble a bootable wolfboot image. The output name is yours to choose; do +not reuse `wolfboot.bin`, which is the raw binary the build just produced: + +```sh +hss-payload-generator -vvv -c ./hal/mpfs.yaml wolfboot-payload.bin +``` + +You must generated the Device Tree Binary using: + +```sh +dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb +``` + +Write that payload to the card's HSS boot partition - see [Writing a card](#writing-a-card). + +## QSPI + +PolarFire SoC has two CoreQSPI v2 controllers with identical register layouts. The selection +is made at build time via `MPFS_SC_SPI` and affects which QSPI base address wolfBoot uses: + +```text + +-------------------+ +----------------------+ + | U54 cores | | U54 cores | + | (wolfBoot) | | (wolfBoot) | + +---------+---------+ +----------+-----------+ + | | + | direct register access | direct register access + | (MSS QSPI @ 0x2100_0000) | (SC QSPI @ 0x3702_0100) + v v + +-------------------+ +----------------------+ + | MSS QSPI IP | | SC QSPI IP | + | (CoreQSPI v2) | | (CoreQSPI v2) | + +---------+---------+ +----------+-----------+ + | | + v v + External QSPI flash Fabric-connected flash +``` + +Build options: + +- MSS QSPI controller (direct register access at 0x21000000, read/write/erase) + - `EXT_FLASH=1` + - Do not set `MPFS_SC_SPI` + - Example config: `config/examples/polarfire_mpfs250_qspi.config` with `CFLAGS_EXTRA` line removed. + +- SC QSPI controller (direct register access at 0x37020100, read/write/erase) + - `EXT_FLASH=1` + - `CFLAGS_EXTRA+=-DMPFS_SC_SPI` + - Example config: `config/examples/polarfire_mpfs250_qspi.config` as-is. + - Both controllers share the same CoreQSPI v2 register interface. + The only difference is that SC QSPI does not need MSS clock/reset setup. + +Notes: +- Both modes support full read, write, and erase operations. +- SCK is the controller clock divided by the CoreQSPI `CLKRATE` field, SCK = PCLK / (2 * CLKRATE). + `MPFS_QSPI_CLK_DIV` selects the divider by its `QSPI_CLK_DIV_*` name from `hal/mpfs250.h` + (`QSPI_CLK_DIV_2` through `QSPI_CLK_DIV_30`, CLKRATE 1..15). The default `QSPI_CLK_DIV_8` is + about 18.75 MHz from the 150 MHz APB clock, where read throughput stops improving: from there on + the polled FIFO transfer rather than SCK sets the rate, so a faster SCK buys nothing. The MSS QSPI + PCLK is the MSS APB clock, which is 40 MHz at the M-mode reset clock until wolfBoot's PLL + bring-up, so an M-mode build that starts QSPI before `mss_pll_init()` runs SCK that much slower; + the SC QSPI is clocked by the System Controller and is unaffected by the MSS PLL. +- For QSPI-based boot flows, disable SD/eMMC in the config (`DISK_SDCARD=0`, `DISK_EMMC=0`) unless you + explicitly want wolfBoot to load from disk and the application from QSPI. +- The MSS QSPI path expects external flash on the MSS QSPI pins; the SC QSPI path is for + fabric-connected flash (design flash) accessed via the System Controller's QSPI instance. + +## HSS S-mode with L2-LIM (no DDR) + +wolfBoot can run in S-mode via HSS without DDR by targeting the on-chip **L2 Loosely Integrated +Memory (L2-LIM)**. HSS loads wolfBoot into L2-LIM on a U54 application core, and wolfBoot loads +the signed application from SC QSPI into L2-LIM as well. This is the configuration for systems +that keep HSS and run without DDR. + +**Features:** +* S-mode on U54 application core (hart 1), loaded by HSS +* wolfBoot and application both reside in L2-LIM, in the 1.25 MB above the HSS resident region + (`WOLFBOOT_ORIGIN=0x08040000`, `WOLFBOOT_L2LIM_SIZE=0x140000`) +* No DDR required +* SC QSPI flash for both wolfBoot payload and signed application image +* Raw binary output (`ELF=0`) required - ELF with debug symbols is too large for L2-LIM + +**Relevant files:** + +| File | Description | +|------|-------------| +| `config/examples/polarfire_mpfs250_hss_l2lim.config` | HSS S-mode + SC QSPI + L2-LIM | +| `hal/mpfs250-hss.ld` | Linker script for S-mode with L2-LIM | +| `hal/mpfs-l2lim.yaml` | HSS payload generator YAML for L2-LIM load target | + +**Build:** +```sh +cp config/examples/polarfire_mpfs250_hss_l2lim.config .config +make clean && make wolfboot.bin +dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb +hss-payload-generator -vvv -c ./hal/mpfs-l2lim.yaml wolfboot-payload.bin +``` + +Flash the HSS payload to the card's boot partition - see [Writing a card](#writing-a-card). + +**Build and sign the test application:** +```sh +make test-app/image_v1_signed.bin +``` + +**Flash the signed application to QSPI** using the UART programmer (`UART_QSPI_PROGRAM=1`, which +this config sets, over the U54 console on MMUART1; needs `pyserial`): +```sh +python3 tools/scripts/mpfs_qspi_prog.py /dev/ttyUSB1 \ + test-app/image_v1_signed.bin 0x20000 +``` + +**Notes:** +- `UART_QSPI_PROGRAM=1` adds a 3-second boot pause every time. Set to `0` once the flash contents are stable. +- `ELF=0` is required: the test-app linker script (`test-app/RISCV64-mpfs250.ld`) places `.init` + (containing `_reset()`) first so the raw binary entry point is at offset 0. The full ELF with + debug symbols exceeds L2-LIM capacity. +- wolfBoot is placed at `0x08040000` (above the HSS L2-LIM resident region) and the application + is loaded at `0x08060000`. The stack resides at the top of the 1.25 MB region. +- HSS must be built and programmed to eNVM separately (see [Building Hart Software Services](#building-hart-software-services-hss)). +- **LIM instruction fetch caveat:** Ensure `L2_WAY_ENABLE` leaves enough cache ways unallocated + to back the LIM SRAM region. See the M-mode section for a detailed explanation. +- UART output appears on MMUART1 (`/dev/ttyUSB1`), same as other S-mode configurations. + +## M-mode (bare-metal eNVM boot) + +wolfBoot supports running directly in Machine Mode (M-mode) on PolarFire SoC, replacing the Hart +Software Services (HSS) as the first-stage bootloader. wolfBoot runs on the E51 monitor core from +eNVM and loads a signed application from SC QSPI flash into L2 Scratchpad (on-chip RAM) -- no HSS +or DDR required. This is the minimal on-chip-only configuration. + +**Features:** +* Runs on E51 monitor core (hart 0) directly from eNVM +* Executes from L2 Scratchpad SRAM (256 KB at `0x0A000000`) +* Loads signed application from SC QSPI flash to L2 Scratchpad (`0x0A010200`) +* No HSS or DDR required - boots entirely from on-chip memory +* Parks and releases secondary U54 harts via CLINT IPI +* ECC384 + SHA384 signature verification + +**Relevant files:** + +| File | Description | +|------|-------------| +| `config/examples/polarfire_mpfs250_m_qspi.config` | M-mode + SC QSPI configuration | +| `hal/mpfs250-m.ld` | M-mode linker script (eNVM + L2 SRAM) | +| `hal/mpfs250.c` | HAL with QSPI driver, UART, L2 cache init | +| `src/boot_riscv_start.S` | M-mode assembly startup | + +**Boot flow:** +1. **eNVM reset vector** (`0x20220100`): CPU starts, startup code copies wolfBoot to L2 Scratchpad +2. **L2 Scratchpad execution** (`0x0A000000`): wolfBoot runs from scratchpad +3. **Hardware init**: L2 cache configuration, UART setup +4. **QSPI init**: SC QSPI controller (`0x37020100`), JEDEC ID read, 4-byte address mode +5. **Image load**: Read signed image from QSPI flash (`0x20000`) to L2 Scratchpad (`0x0A010200`) +6. **Verify & boot**: SHA384 integrity check, ECC384 signature verification, jump to app + +**Build:** +```sh +cp config/examples/polarfire_mpfs250_m_qspi.config .config +make clean && make wolfboot.elf +``` + +**Flash wolfBoot to eNVM** with `mpfsBootmodeProgrammer`, exactly as in +[quick start path 2](#path-2-wolfboot-replaces-hss-and-boots-linux) - the only difference is +which `wolfboot.elf` is programmed. Restoring HSS is the same command with the HSS ELF. + +**Build and sign the test application:** +```sh +make test-app/image_v1_signed.bin +``` + +**Flash the signed application to QSPI** using the UART programmer (`UART_QSPI_PROGRAM=1`, which +this config sets, over the E51 console on MMUART0; needs `pyserial`): +```sh +python3 tools/scripts/mpfs_qspi_prog.py /dev/ttyUSB0 \ + test-app/image_v1_signed.bin 0x20000 +``` + +The script: +1. Waits for wolfBoot to print the `QSPI-PROG: Press 'P'` prompt (power-cycle the board) +2. Sends `P` to enter programming mode +3. Transfers the binary in 256-byte ACK-driven chunks +4. wolfBoot erases, writes, and then continues booting the new image + +Use `0x20000` for the boot partition and `0x02000000` for the update partition. + +**QSPI partition layout** (Micron MT25QL01G, 128 MB): + +| Region | Address | Size | +|--------|---------|------| +| Boot partition | `0x00020000` | ~32 MB | +| Update partition | `0x02000000` | ~32 MB | +| Swap partition | `0x04000000` | 64 KB | + +**UART mapping:** + +| Hart | Core | MMUART | USB device | +|------|------|--------|------------| +| 0 | E51 | MMUART0 | /dev/ttyUSB0 | +| 1 | U54_1 | MMUART1 | /dev/ttyUSB1 | +| 2 | U54_2 | MMUART2 | N/A | +| 3 | U54_3 | MMUART3 | N/A | +| 4 | U54_4 | MMUART4 | N/A | + +**Expected serial output on successful boot:** +``` +wolfBoot Version: X.Y.Z (...) +Running on E51 (hart 0) in M-mode +QSPI: Using SC QSPI Controller (0x37020100) +QSPI: Flash ID = 0x20 0xBA 0x21 +QSPI-PROG: Press 'P' within 3s to program flash +QSPI-PROG: No trigger (got 0x00 ...), booting +Versions: Boot 1, Update 0 +... +Firmware Valid +Booting at 0x... +``` + +**Notes:** +- The E51 is `rv64imac` (no FPU or crypto extensions). wolfBoot is compiled with `NO_ASM=1` to + use portable C crypto implementations and `-march=rv64imac -mabi=lp64` for correct code + generation. The `rdtime` CSR instruction is not available in bare-metal M-mode; wolfBoot uses a + calibrated busy-loop for all delays (`udelay()` in `hal/mpfs250.c`). +- `UART_QSPI_PROGRAM=1` adds a 3-second boot pause every time. Set to `0` once the flash + contents are stable. +- The config uses `WOLFBOOT_LOAD_ADDRESS=0x0A010200` to place the application in L2 Scratchpad + above wolfBoot code (~64 KB at `0x0A000000`), with the stack at the top of the 256 KB region. +- **LIM instruction fetch limitation:** The on-chip LIM (`0x08000000`, 2 MB) is backed by L2 + cache ways. When `L2_WAY_ENABLE` is set to `0x0B` (all cache ways 0-7 active for caching), + no ways remain for LIM backing SRAM. Data reads from LIM work through the L2 cache, but + instruction fetch silently hangs - the CPU stalls with no trap generated. For this reason the + application is loaded into L2 Scratchpad (`0x0A000000`), which is always accessible regardless + of `L2_WAY_ENABLE`. To use LIM, reduce `L2_WAY_ENABLE` to free cache ways for LIM backing. +- **Strip debug symbols** before signing the test-app ELF. The debug build is ~150 KB but the + stripped ELF is ~5 KB. L2 Scratchpad has ~150 KB available between wolfBoot code and the stack: + `riscv64-unknown-elf-strip --strip-debug test-app/image.elf` +- **DDR support:** software LPDDR4 initialization is included via the **M-Mode + DDR** + configuration (`polarfire_mpfs250_m.config`, requires `LIBERO_FPGA_CONFIG_DIR`) for use cases + that require loading larger images (e.g. a Linux FIT) to DDR memory. See + [M-mode + DDR](#m-mode--ddr-booting-linux-minimal-sbi). + +## M-mode + DDR: booting Linux (minimal SBI) + +The **M-Mode + DDR** configuration (`config/examples/polarfire_mpfs250_m.config`) replaces both +HSS and OpenSBI: wolfBoot performs the LPDDR4 init/training on the E51, loads and verifies a +signed Yocto FIT image (kernel + dtb) from SD card into DDR, applies device-tree fixups, releases +U54 hart 1 into S-mode at the kernel entry, and then remains resident as a minimal M-mode SBI +runtime. Validated on the MPFS250T Video Kit: 4-CPU SMP Yocto Linux to login in ~40 s from +power-on. + +**Minimal SBI runtime** (`src/riscv_sbi.c`, a clean-room implementation of the OpenSBI/SBI spec; generic RISC-V with HAL hooks; enabled by +`WOLFBOOT_MMODE_SMODE_BOOT`): +* SBI v0.3 extensions: BASE, TIME (per-hart `mtimecmp`, MTIP-to-STIP injection), IPI (SSIP + injection via CLINT MSIP), RFENCE (remote `fence.i` / `sfence.vma` with completion wait), + HSM (`hart_start`/`hart_stop`/`hart_status` backed by per-hart start mailboxes), DBCN and the + legacy console putchar (shared with the wolfBoot UART), SRST (reset types 1 and 2 assert the + MSS reset; type 0, shutdown, parks every hart in `wfi`, as the MSS has no power-off path). +* `rdtime` emulation: the U54/E51 have no `time` CSR, so `rdtime` is emulated from CLINT MTIME. + The M-mode HAL starts the MTIME time base before hand-off via SYSREG `RTC_CLOCK_CR` at 1 MHz + (`mpfs_enable_mtime()`), matching the device-tree `timebase-frequency`. +* Misaligned load/store emulation (not delegatable on these harts), including compressed forms, + for the kernel's unaligned copy tails. +* Per-hart M-mode trap stacks live in the `hss-buffer` reserved (nomap) DDR region; cross-hart + state (HSM mailboxes, IPI flags, the hart-release gate flag) lives in the E51 DTIM at + `0x01000000`, which is uncached and coherent for all harts. Cacheable L2-scratchpad memory + must not be used for cross-hart signalling (stores can be lost on dirty-line eviction). + +**Device-tree fixups** applied to the loaded dtb (`hal/mpfs250.c`): bootargs/root device +(the DTB's own `/chosen/bootargs` wins unless `LINUX_BOOTARGS`/`LINUX_BOOTARGS_ROOT` is set; +see "Linux kernel command line (bootargs)" in `docs/compile.md`), +MAC addresses from the device serial number, and all five MSS watchdog nodes are disabled. + +**Watchdog policy:** the MSS watchdogs always count and reset the chip on timeout (they cannot +be disabled in hardware, and `CONTROL=0` does not prevent the reset). After hand-off the parked +E51 acts as a monitor and refreshes all five watchdogs; the OS watchdog driver is disabled via +the dtb fixup so the two never conflict. `MPFS_WDT_NODES_KEEP=1` hands the watchdogs to the OS +instead: the dtb nodes stay enabled and the parked E51 stops refreshing, so the OS driver arms +them at probe and something on the OS side (a watchdog daemon, or the design's own fabric +scheme) has to service them from then on. If nothing does, the MSS watchdog expires and resets +the board shortly after Linux reaches userspace, which presents as a boot loop with no error +message: the knob must be paired with a servicer in the same image, for example systemd's +`RuntimeWatchdogSec`. A design with its own fabric watchdog needs neither setting; wolfBoot +never touches fabric logic. + +**Hand-off / SMP flow:** secondary harts park in eNVM until the E51 signals image-copy +completion (DTIM gate flag), then park in a WFI loop. The boot hart is released with a staged +mailbox {entry, dtb} plus MSIP; Linux brings up the remaining harts through SBI HSM +`hart_start`, which uses the same mailbox + MSIP path. The release path must stay fast +(no UART access): the kernel allows roughly one second for a started hart to come online. + +**Read-first requirement:** the LPDDR4 controller on this board needs the first CPU-side +transaction after DDR initialization to be a read. When a write comes first on a cold boot it +lands in DDR displaced by several bus beats, and every later cached-port write in that boot +follows it. `mpfs_ddr_init()` therefore reads one block through the non-cached alias before +anything is written, after which CPU, L2 write-back and SDMA writes all land where they should. + +**Driver structure:** the licensed Cadence DDR controller logic (controller CSR programming, the +Memory Test Controller engine, and the LPDDR4 mode-register protocol) lives in the +target-independent `src/ddr_cadence.c` / `include/ddr_cadence.h` (controller base overridable via +`DDR_CADENCE_CTRL_BASE`). The Microchip-specific PHY, PLL, clock mux and training, plus the +board's `LIBERO_SETTING_*` values, stay in `hal/mpfs250_ddr.c`, which builds the controller +register table and composes the generic calls. Both compile only when `MPFS_DDR_INIT` is set. + +## Boot mode 2: User Boot Loader stub + +Boot mode 2 ("user secure boot from sNVM") lets the System Controller authenticate the first code the MSS runs: it copies a User Boot Loader (UBL) out of authenticated sNVM pages into the LIM and releases all five harts at its entry. wolfBoot provides that UBL as a small verifier target, `TARGET=mpfs250_ubl`: it checks an ECC384/SHA384 wolfBoot manifest over the eNVM wolfBoot image against the public key compiled into the stub, then enters the eNVM wolfBoot at its reset vector on every hart exactly as a boot mode 1 boot would. The eNVM wolfBoot itself is unchanged and stays field-updatable; what the sNVM pages pin is the key that must have signed it. + +**eNVM layout** (`hal/mpfs250-m.ld`, `hal/mpfs250.h`) + +| Address | Content | +|---------|---------| +| `0x20220000` | boot ROM meta information (written by `mpfsBootmodeProgrammer`) | +| `0x20220100` | wolfBoot image, reset vector first | +| `0x2023FC00` | wolfBoot manifest header (`MPFS_ENVM_MANIFEST=1`), signed by the stub's key | + +The manifest is detached from the image it signs (the `WOLFBOOT_SELF_HEADER` form), so the image does not move and a boot mode 1 device boots the same eNVM contents. The last two eNVM pages stay free for the programmer. + +**Relevant files:** + +| File | Description | +|------|-------------| +| `config/examples/polarfire_mpfs250_ubl.config` | UBL stub configuration | +| `hal/mpfs250_ubl.c`, `hal/mpfs250_ubl.h` | stub HAL: watchdog hold, MMUART0 at the reset clock, manifest verify, hart hand-off | +| `hal/mpfs250_ubl.ld` | LIM linker script with the sNVM size limit | +| `src/boot_riscv_start.S` | `TARGET_mpfs250_ubl` startup: no copy, U54s park on a DTIM mailbox | +| `tools/scripts/mpfs_bootmode2.sh` | sNVM + boot mode programming on the secure dies, eNVM-only update | + +**Build the stub** (its own signing key; keep it out of the tree that signs application images): +```sh +cp config/examples/polarfire_mpfs250_ubl.config .config +make clean && make +# wolfboot.elf is the UBL; wolfboot_signing_private_key.der is the UBL key +``` +The image must fit the 221 authenticated sNVM pages (52,096 bytes after the 60-byte UBL header); the linker script enforces it. The sNVM pages above the stub stay available for the sNVM keystore and wrapped keys of [Hardware root of trust](#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). + +**Build and sign the eNVM wolfBoot** for the stub (any M-mode configuration): +```sh +cp config/examples/polarfire_mpfs250_m.config .config +make clean && make LIBERO_FPGA_CONFIG_DIR=... MPFS_ENVM_MANIFEST=1 \ + MPFS_UBL_KEY=/path/to/ubl/wolfboot_signing_private_key.der \ + wolfboot_envm_signed.bin +``` +`MPFS_ENVM_MANIFEST=1` links a manifest placeholder into the last used eNVM page; the rule signs the image without it (`wolfboot_envm.bin`, the ELF's loadable bytes with alignment gaps zero-filled, which is what the programmer writes), writes the resulting header into the placeholder and produces `wolfboot_envm_signed.elf`, which programs image and manifest together. `MPFS_UBL_KEY` defaults to the tree's own key, `MPFS_ENVM_VERSION` (default 1) is the manifest version, and `-DMPFS_UBL_MIN_VERSION=N` in the stub build refuses older manifests. + +**Program** (requires `SC_INSTALL_DIR` and `FPGENPROG`, see the M-mode section): +```sh +# 1. eNVM: image + manifest. This also selects boot mode 1, so the device +# boots the new eNVM contents before the stub is in place. +java -jar $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ + --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD \ + wolfboot_envm_signed.elf + +# 2. sNVM: the stub, and boot mode 2. Non-secure dies: the programmer +# itself (--bootmode 2 --snvm_page 0 --usk 0 wolfboot.elf). Secure dies +# (MPFS250TS...), which the programmer does not list but which an +# authenticated sNVM client requires: +tools/scripts/mpfs_bootmode2.sh --die MPFS250TS --workdir $PWD /path/to/ubl/wolfboot.elf +``` +The script runs the programmer in `--dryrun` for the UBL image and the User sNVM Key (USK), replays its `fpgenprog` steps for the requested die and programs. Keep the USK file it names: the sNVM pages can only be rewritten with it. + +The UBL lives in an authenticated sNVM client, which the System Controller keys from the device's sNVM Master Key (SMK) +and the client's 12-byte USK. A device whose SMK has never been initialized cannot hold such a client, and +`mpfsBootmodeProgrammer` never asks for one, so `--regenerate-smk` passes `--regenerate_smk` to the `snvm_client` step +and the generated bitstream carries a new SMK. A new SMK invalidates every authenticated or encrypted sNVM client +already on the device; wolfBoot's own keystore and wrapped image key are plaintext clients and are not SMK-keyed. + +Updating the eNVM wolfBoot under boot mode 2 goes through the same script with `--envm`, which programs the eNVM client only and leaves the boot mode and sNVM alone: +```sh +tools/scripts/mpfs_bootmode2.sh --envm --die MPFS250TS --workdir $PWD wolfboot_envm_signed.elf +``` +`mpfsBootmodeProgrammer --bootmode 1` restores boot mode 1. + +**Expected serial output:** +``` +wolfBoot UBL 2.10.0: eNVM image 0x20220100, manifest 0x2023FC00 +UBL: eNVM wolfBoot signature OK (1051 ms), entering 0x20220100 +UBL: 5 of 5 harts released +wolfBoot Version: ... +``` +A manifest that does not verify prints which check failed (`no manifest`, `digest mismatch`, `signature invalid`) and `UBL: eNVM wolfBoot verify FAILED`; the stub panics and nothing in eNVM runs. + +**Notes:** +- The stub runs at the reset clock and changes no clock, L2 way or peripheral the eNVM wolfBoot expects to find in its reset state. It holds the five MSS watchdogs in the no-reset, maximum-window state for the verify and puts the E51's back before the jump, so the eNVM wolfBoot's own watchdog snapshot is what the boot ROM left. +- The U54s wait in the stub on a DTIM mailbox (`hal/mpfs250_ubl.h`) for the verified entry, acknowledge and jump; the E51 follows last. All five harts then run the eNVM startup as in boot mode 1. +- Immutability of the stub is a Libero decision: the ROM flag on its sNVM pages and a frozen boot configuration. wolfBoot does not program either. +- The chain is: the System Controller authenticates the stub out of sNVM against the device SMK, the stub's compiled-in public key authenticates the eNVM wolfBoot, and that wolfBoot's own keystore authenticates the OS image. Each link holds the next one's key, so the stub carries a keystore of its own (`.keystore` in `hal/mpfs250_ubl.ld`) and never consults the one in eNVM. +- Give the stub a key no application image is ever signed with. `MPFS_UBL_KEY` defaults to the tree's signing key, and under that default anything already signed for the device satisfies the eNVM manifest check, so a party who can rewrite eNVM can have the stub enter a blob of their choosing. +- The stub enforces no rollback floor unless it is built with `-DMPFS_UBL_MIN_VERSION=N`: its sNVM pages are meant to be ROM-flagged, so it has nowhere to keep a counter and the floor is a property of the stub image instead. +- Nothing before wolfBoot initialises the PLIC under boot mode 2, where boot mode 1 reaches it with every priority already set, so the OS inherits the controller in its reset state. That is harmless: Linux's `irq-sifive-plic` driver clears the enable bit and writes the priority for every source on every context at probe, and the Video Kit declares `riscv,ndev = <186>`, which is the whole of `PLIC_NUM_SOURCES`, so no source is left untouched. wolfBoot therefore does not program the PLIC. + +## Encryption + +PolarFire SoC boots from disk with `WOLFBOOT_NO_PARTITIONS`, so there is no partition-resident key store: the encryption key is held in RAM for the life of the boot. A production build keeps it in secure NVM instead, wrapped by the PUF-derived KEK - see [Hardware root of trust](#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). + +Enable encryption in your configuration with `ENCRYPT=1` and one of: `ENCRYPT_WITH_AES256=1`, `ENCRYPT_WITH_AES128=1`, or `ENCRYPT_WITH_CHACHA=1`. + +| Algorithm | Key Size | Nonce/IV Size | +|-----------|----------|---------------| +| ChaCha20 | 32 bytes | 12 bytes | +| AES-128 | 16 bytes | 16 bytes | +| AES-256 | 32 bytes | 16 bytes | + +The `libwolfboot` API provides the following functions for managing the encryption key: + +```c +int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce); +int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce); +int wolfBoot_erase_encrypt_key(void); /* called automatically by wolfBoot_success() */ +``` + +To use your own implementation for getting the encryption key use `CUSTOM_ENCRYPT_KEY` and `OBJS_EXTRA=src/my_custom_encrypt_key.o`. +Then provide your own implementation of `int RAMFUNCTION wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce);` + +Example: + +```c +int RAMFUNCTION wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + int i; + /* Test key: "0123456789abcdef0123456789abcdef" (32 bytes for AES-256) */ + const char test_key[] = "0123456789abcdef0123456789abcdef"; + /* Test nonce: "0123456789abcdef" (16 bytes) */ + const char test_nonce[] = "0123456789abcdef"; + + for (i = 0; i < ENCRYPT_KEY_SIZE && i < (int)sizeof(test_key); i++) { + key[i] = (uint8_t)test_key[i]; + } + for (i = 0; i < ENCRYPT_NONCE_SIZE && i < (int)sizeof(test_nonce); i++) { + nonce[i] = (uint8_t)test_nonce[i]; + } + return 0; +} +``` + +To sign and encrypt an image, create a key file with the concatenated key and nonce, then use the sign tool: + +```sh +# Create key file (32-byte key + 16-byte nonce for AES-256) +printf "0123456789abcdef0123456789abcdef0123456789abcdef" > /tmp/enc_key.der + +# Sign and encrypt +./tools/keytools/sign --ecc384 --sha384 --aes256 --encrypt /tmp/enc_key.der \ + fitImage wolfboot_signing_private_key.der 1 +``` + +The result is `fitImage_v1_signed_and_encrypted.bin`, which gets placed into your OFP_A or OFP_B partitions. + +```sh +sudo dd if=fitImage_v1_signed_and_encrypted.bin of=/dev/sdc2 bs=512 status=progress && sudo cmp fitImage_v1_signed_and_encrypted.bin /dev/sdc2 +sudo dd if=fitImage_v1_signed_and_encrypted.bin of=/dev/sdc3 bs=512 status=progress && sudo cmp fitImage_v1_signed_and_encrypted.bin /dev/sdc3 +``` + +During boot, wolfBoot decrypts the image headers from disk to select the best candidate, loads and decrypts the full image to RAM, then verifies integrity and authenticity before booting. On successful boot, `wolfBoot_success()` clears the key from RAM. + +See the [Encrypted Partitions](encrypted_partitions.md) documentation for additional details. + +## PQC (ML-DSA) + +### Configuration + +Update your `.config` file with the following ML-DSA settings, or pass them as `make` arguments on top of `config/examples/polarfire_mpfs250_m.config` (the standalone M-mode E51 target verifies ML-DSA-87 this way, with SHA-384 as the image hash): + +```makefile +# ML-DSA 87 (Category 5) +SIGN=ML_DSA +HASH=SHA384 +ML_DSA_LEVEL=5 +IMAGE_SIGNATURE_SIZE=4627 +IMAGE_HEADER_SIZE=12288 +WOLFBOOT_SECTOR_SIZE?=0x4000 +``` + +**Important:** +- The `sign` tool requires `IMAGE_HEADER_SIZE` to be set as an environment variable, even if it's already configured in `.config`. This is because the sign tool reads the environment variable separately to determine the header size for padding. Without this, the sign tool may use a smaller default header size, causing a mismatch with wolfBoot's expected header size. +- The `WOLFBOOT_SECTOR_SIZE` must be larger than the `IMAGE_HEADER_SIZE`. + +### Signing and Encryption + +```sh +# Sign and Encrypt with PQ ML-DSA 5 (87) +# NOTE: IMAGE_HEADER_SIZE must match the value in .config +IMAGE_HEADER_SIZE=12288 ML_DSA_LEVEL=5 ./tools/keytools/sign --ml_dsa --sha256 --aes256 --encrypt /tmp/enc_key.der \ + fitImage wolfboot_signing_private_key.der 1 +``` + +**ML-DSA Parameter Reference:** + +| ML_DSA_LEVEL | Security Category | Signature Size | Private Key | Public Key | Recommended IMAGE_HEADER_SIZE | +|--------------|-------------------|----------------|-------------|------------|------------------------------ | +| 2 | Category 2 | 2420 | 2560 | 1312 | 8192 | +| 3 | Category 3 | 3309 | 4032 | 1952 | 8192 | +| 5 | Category 5 | 4627 | 4896 | 2592 | 12288 | + +For other ML-DSA levels, adjust `ML_DSA_LEVEL`, `IMAGE_SIGNATURE_SIZE`, and `IMAGE_HEADER_SIZE` accordingly in both `.config` and the signing command. + +## Writing a card + +Two card layouts are in use, one per boot flow. They are not interchangeable: the partition a +build reads from is fixed at compile time. + +| Layout | Used by | Partitions | +|---|---|---| +| HSS | every S-mode configuration (`polarfire_mpfs250.config`, `_qspi`, `_hss_l2lim`) | p1 8 MB BIOS boot (type GUID `21686148-6449-6E6F-744E-656564454649`) holding the HSS payload, p2/p3 64 MB `OFP_A`/`OFP_B` image slots, p4 rootfs | +| M-mode | `polarfire_mpfs250_m.config` (`BOOT_PART_A=0`, `BOOT_PART_B=1`) | p1 32 MB boot slot holding the signed FIT, p2 32 MB update slot, p3 rootfs; no BIOS partition, because nothing loads wolfBoot from the card | + +The [example boot output](#example-boot-output) below is the M-mode layout: three GPT entries, the +first two 32 MB. + +### HSS layout + +Replace `/dev/sdc` with the device `lsblk` reports. Adjust `+64M` for larger image slots. + +```sh +sudo fdisk /dev/sdc <.rootfs.ext4 of=/dev/sdc4 bs=4M status=progress +sync +``` + +Build and sign the test application first: + +```sh +make test-app/image.elf +./tools/keytools/sign --ecc384 --sha384 test-app/image.elf wolfboot_signing_private_key.der 1 +``` + +With the HSS layout, insert the card and let HSS start wolfBoot. If HSS defaults to eMMC, select +the card with `boot sdcard` at the HSS console, or build HSS with MMC disabled and SD enabled. + +## Building Hart Software Services (HSS) + +The Hart Software Services (HSS) is the PolarFire SoC zero-stage bootloader (E51 monitor core); it is required only for the HSS-based S-mode configurations above, not for the M-mode + DDR path which replaces it. + +```sh +git clone https://github.com/polarfire-soc/hart-software-services.git +cd hart-software-services +make clean +make BOARD=mpfs-video-kit +make BOARD=mpfs-video-kit program +``` + +## Building Yocto-SDK Linux + +The signed Yocto FIT image booted by wolfBoot is produced from Microchip's `meta-mchp` Yocto layer (BSP, drivers, and kernel for the board). + +See: +* https://github.com/linux4microchip/meta-mchp/blob/scarthgap/meta-mchp-common/README.md +* https://github.com/linux4microchip/meta-mchp/blob/scarthgap/meta-mchp-polarfire-soc/README.md +* https://github.com/polarfire-soc/polarfire-soc-documentation/blob/master/reference-designs-fpga-and-development-kits/mpfs-video-kit-embedded-software-user-guide.md + +Building mchp-base-image Yocto Linux: + +```sh +mkdir ../yocto-dev-polarfire +cd ../yocto-dev-polarfire +repo init -u https://github.com/linux4microchip/meta-mchp-manifest.git -b refs/tags/linux4microchip+fpga-2025.10 -m polarfire-soc/default.xml +repo sync +export TEMPLATECONF=${TEMPLATECONF:-../meta-mchp/meta-mchp-polarfire-soc/meta-mchp-polarfire-soc-bsp/conf/templates/default} +source openembedded-core/oe-init-build-env +# A Microchip base image with standard Linux utilities, as well as some Microchip apps and examples +MACHINE=mpfs-video-kit bitbake mchp-base-image +OR +# A Microchip base image with additional support for software development, including toolchains and debug tools +MACHINE=mpfs-video-kit bitbake mchp-base-image-sdk +``` + +Build images are output to: `./tmp-glibc/deploy/images/mpfs-video-kit/` + +### Custom FIT image, signing and copying to SDCard + +wolfBoot can either decompress a gzipped kernel at boot time (`GZIP=1`, +the default for `polarfire_mpfs250.config` and `polarfire_mpfs250_qspi.config`) +or accept a pre-decompressed kernel inside the FIT (`GZIP=0`). Pick one path. + +`src/gzip.c` decodes with lookup tables by default; `GZIP_SMALL=1` selects +its bit-serial decoder instead, with the same checks and return codes, a +few KB less code and no static tables, at a fraction of the rate. The +difference is largest on cores without a data cache, where the bit-serial +decoder can make the inflate the longest phase of a compressed boot. + +#### Option A - Compressed FIT (`GZIP=1`, default) + +Set `compression = "gzip"` and point `data` at the gzipped kernel directly +in `hal/mpfs250.its`: + +```dts +images { + kernel-1 { + data = /incbin/("../yocto-dev-polarfire/build/tmp-glibc/work/mpfs_video_kit-oe-linux/linux-mchp/6.12.22+git/build/linux.bin.gz"); + compression = "gzip"; + load = <0x80200000>; + entry = <0x80200000>; + hash-1 { algo = "sha256"; }; + ... + }; +}; +``` + +Then build the FIT directly - no manual `gzip -cdvk` step: + +```sh +mkimage -f hal/mpfs250.its fitImage +``` + +At boot, wolfBoot decompresses the kernel into `0x80200000` directly out of +the FIT `data` blob. The outer wolfBoot signature covers the whole FIT +(including the compressed `data`), and gzip's CRC32 + ISIZE trailer covers +the decompressed output; the per-image `hash-1` subnodes are not re-checked +at runtime as they would duplicate the outer signature. + +#### Option B - Uncompressed FIT (`GZIP=0`) + +Build wolfBoot with `GZIP=0` and pre-decompress the kernel on the host. +Keep `compression = "none"` in `hal/mpfs250.its`: + +```sh +# Extract GZIP compressed linux kernel to wolfboot root +gzip -cdvk ../yocto-dev-polarfire/build/tmp-glibc/work/mpfs_video_kit-oe-linux/linux-mchp/6.12.22+git/build/linux.bin > kernel.bin + +# Create custom FIT image +mkimage -f hal/mpfs250.its fitImage +FIT description: PolarFire SoC MPFS250T +Created: Tue Dec 23 11:29:02 2025 + Image 0 (kernel-1) + Description: Linux Kernel + Created: Tue Dec 23 11:29:02 2025 + Type: Kernel Image + Compression: uncompressed + Data Size: 19745280 Bytes = 19282.50 KiB = 18.83 MiB + Architecture: RISC-V + OS: Linux + Load Address: 0x80200000 + Entry Point: 0x80200000 + Hash algo: sha256 + Hash value: 800ce147fa91f367ec620936a59a1035c49971ed4b9080c96bdc547471e80487 + Image 1 (fdt-1) + Description: Flattened Device Tree blob + Created: Tue Dec 23 11:29:02 2025 + Type: Flat Device Tree + Compression: uncompressed + Data Size: 19897 Bytes = 19.43 KiB = 0.02 MiB + Architecture: RISC-V + Load Address: 0x8a000000 + Hash algo: sha256 + Hash value: 0b4efca8c0607c9a8f4f9a00ccb7691936e019f3181aab45e6d52dae91975039 + Default Configuration: 'conf1' + Configuration 0 (conf1) + Description: Linux kernel and FDT blob + Kernel: kernel-1 + FDT: fdt-1 + Hash algo: sha256 + Hash value: unavailable + +# Sign FIT image with version 1 +./tools/keytools/sign --ecc384 --sha384 fitImage wolfboot_signing_private_key.der 1 +``` + +Then write `fitImage_v1_signed.bin` to the image slots and the Yocto rootfs +(`tmp-glibc/deploy/images/mpfs-video-kit/*.rootfs.ext4`) to the rootfs partition - see +[Writing a card](#writing-a-card). + +## Debugging + +Start GDB server: + +```sh +$SC_INSTALL_DIR/openocd/bin/openocd --command "set DEVICE MPFS" --file board/microsemi-riscv.cfg +``` + +Start GDB Client: `riscv64-unknown-elf-gdb` + +``` +file wolfboot.elf +tar rem:3333 +add-symbol-file ../hart-software-services/build/hss-l2scratch.elf +set pagination off +foc c + +set $target_riscv=1 +set mem inaccessible-by-default off +set architecture riscv:rv64 +#load wolfboot.elf +#thread apply 2 set $pc=_reset +#thread apply all set $pc=_start +``` + +## Example boot output + +``` +wolfBoot Version: 2.10.0 +Running on E51 (hart 0) in M-mode +Boot RESET_SR: 1FF (bit0=PERIPH bit1=MSS bit2=CPU bit3=DBG bit4=FABRIC bit5=WDOG bit6=GPIO bit7=BUS bit8=SOFT) +======================================== +DDR: Training+MTC PASS +DDR: Initialization COMPLETE +SDHCI: platform init +Reading MBR... +Found GPT PTE at sector 1 +Valid GPT partition table +Max number of partitions: 128 + GPT part 0: 0_2000000h @ 0_100000 + GPT part 1: 0_2000000h @ 0_2100000 + GPT part 2: 7_6A300000h @ 0_4100000 +Total partitions on disk0: 3 +Checking primary OS image in 0,0... +Checking secondary OS image in 0,1... +Versions, A:1 B:0 +Load block size: 512KB +Load address 0x82000000 +Attempting boot from P:A +Boot partition: 0xA03FD90 (sz 19766364, ver 0x1, type 0x601) +Loading image from disk... +done +Checking image integrity... +done +Verifying image signature... +done +Firmware Valid. +Flattened uImage Tree: Version 17, Size 19766364 +Loading Image kernel-1: 0x820000C8 -> 0x80200000 (19745280 bytes) +Loading Image fdt-1: 0x832D4BCC -> 0x8A000000 (19897 bytes) +Loading DTS: 0x8A000000 -> 0x8A000000 (19897 bytes) +Booting at 80200000 +FDT: Set chosen, bootargs=earlycon=sbi root=/dev/mmcblk0p3 rootwait uio_pdrv_genirq.of_id=generic-uio +FDT: Device serial: 219A437C-6AE1F1C2-8EDC4324-685B2288 +FDT: MAC0 = 00:04:A3:5B:22:88 +FDT: Set watchdog@20001000 status=disabled (... all five MSS WDT nodes) +M->S handoff: entry=0x80200000 hart=0 dtb=0x8A000000 +Releasing hart 1 into S-mode at 0x80200000 (dtb=0x8A000000) +[ 0.000000] Linux version 6.12.22-linux4microchip+fpga-2025.07 ... #1 SMP +[ 0.000000] Machine model: Microchip PolarFire-SoC VIDEO Kit +[ 0.000000] SBI specification v0.3 detected +[ 0.000000] SBI implementation ID=0x776f6c66 Version=0x1 +[ 0.000000] SBI TIME extension detected +[ 0.000000] SBI IPI extension detected +[ 0.000000] SBI RFENCE extension detected +[ 0.000000] riscv: providing IPIs using SBI IPI extension +[ 0.016264] smp: Bringing up secondary CPUs ... +[ 0.040738] smp: Brought up 1 node, 4 CPUs +[ 3.764474] Run /sbin/init as init process +... +mpfs-video-kit login: +``` + +The Linux SBI lines confirm the kernel is talking to wolfBoot's own SBI runtime: the implementation ID `0x776f6c66` is ASCII `"wolf"`, distinguishing it from OpenSBI (ID `0x8`). + +## Performance comparison + +### Binary Size Comparison + +The following table compares wolfBoot binary sizes for different signature algorithms on PolarFire SoC (MPFS250): + +| Algorithm | Hash | Text | Data | BSS | Total | Binary Size | +|-----------|--------|---------|------|---------|---------|-------------| +| ECC384 | SHA384 | 67.1 KB | 8 B | 3.0 KB | 70.2 KB | 68 KB | +| ML-DSA 87 | SHA256 | 63.9 KB | 0 B | 14.5 KB | 78.4 KB | 64 KB | + +### Boot Time Comparison + +Boot time measurements on PolarFire SoC (RISC-V 64-bit U54 @ 625 MHz) for a 19MB encrypted FIT image: + +| Algorithm | Hash | Load Time | Decrypt Time | Integrity Check | Signature Verify | Total Boot Time | +|-------------|---------|-----------|--------------|-----------------|------------------|-----------------| +| ECC384 | SHA384 | ~800 ms | ~2900 ms | ~1500 ms | ~70 ms | ~5.3 seconds | +| ML-DSA 87 | SHA256 | ~835 ms | ~2900 ms | ~2100 ms | ~22 ms | ~5.9 seconds | + +Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit with host-side timestamps on the console, plaintext FIT, ECC384/SHA384, software crypto, the same 19.7 MB kernel either gzip-compressed in the FIT (5.9 MB) or stored uncompressed (19.8 MB): + +Elapsed time from power-on at each point, not per-phase duration: + +| Reached | gzip FIT | uncompressed FIT | +|-----------------------------------------------|----------|------------------| +| DDR trained, SD init, GPT read, FIT loaded, SHA384 integrity and ECC384 verify done | ~2 s | ~4 s | +| gzip inflate of the kernel finished | ~18 s | n/a | +| FIT copies and DTB fixups done | ~18 s | ~5 s | +| M-mode -> S-mode hand-off | ~18 s | ~5 s | +| Linux login prompt | ~41 s | ~28 s | + +Both columns are the rootfs-on-a-card image; an initramfs image reaches its login sooner because +it mounts no root device. + +The inflate runs on the E51 at about 1.2 MB/s of output, so a compressed kernel costs far more than the extra card read it saves; for a short boot, build the FIT with `compression = "none"` for the kernel (`FIT_KERNEL_COMP_ALG = "none"` in Yocto). The SD load, the FIT copies, the DTB fixups and the image hash all use DDR directly, see the read-first requirement under [M-mode + DDR](#m-mode--ddr-booting-linux-minimal-sbi). + +On an encrypted image the decrypt dominates everything else, and the Athena F5200 offloads are not uniformly a win. Per phase over a 38.9 MB AES-256-CTR encrypted FIT, ECC384/SHA384, from `BOOT_BENCHMARK=1`: + +| Phase | Software | Athena | +|-------|----------|--------| +| Load from SD (SDMA) | 1.64 s | 1.63 s | +| Decrypt AES-256-CTR | 10.50 s | 2.48 s | +| Integrity SHA-384 | 4.29 s | 5.70 s | +| Verify ECC384 | 0.67 s | 0.67 s | + +The AES offload is worth about 4.2x on the decrypt. **The hash offload is about 1.3x slower than software** over an image this size: the engine's per-block setup dominates once the data is in DDR rather than a small buffer in L2 scratch, so enabling it costs boot time rather than saving it. `MPFS_ATHENA=1` turns on the hash offload and `ENCRYPT_WITH_AES256=1` adds the AES offload to it, so an encrypted build that wants only the AES win cannot currently express that. + +Athena also does not fit eNVM alongside the full feature set: with the manifest linked it needs both `GZIP` and `MPFS_SBI_SC_FLASH` dropped. Dropping `GZIP` costs nothing when the FIT's kernel is stored uncompressed. + +## Source files + +`hal/mpfs250.c` - Hardware abstraction layer (UART, QSPI, SD/eMMC, multi-hart) +`hal/mpfs250.h` - Register definitions and hardware interfaces +`hal/mpfs250_ddr.c` - LPDDR4 PHY/PLL/training and DDR bring-up (M-mode, no HSS) +`src/ddr_cadence.c` - Generic Cadence DDR controller driver (controller CSR +programming, Memory Test Controller, LPDDR4 mode-register protocol) +`include/ddr_cadence.h` - Generic Cadence DDR controller register map + interface +`hal/mpfs250.ld` - Linker script for S-mode (HSS-based boot) +`hal/mpfs250-m.ld` - Linker script for M-mode (eNVM + L2 SRAM) +`hal/mpfs250-hss.ld` - Linker script for S-mode (HSS with L2-LIM) +`hal/mpfs250_ubl.c` / `hal/mpfs250_ubl.ld` - Boot mode 2 User Boot Loader stub (sNVM-authenticated verifier of the eNVM wolfBoot) +`hal/mpfs.dts` - Device tree source +`hal/mpfs.yaml` - HSS payload generator configuration for use of DDR +`hal/mpfs-l2lim.yaml` - HSS payload generator for the use of L2-LIM +`hal/mpfs250.its` - Example FIT image creation template +`hal/mpfs250_snvm.c` / `hal/mpfs250_snvm.h` - System Controller mailbox, sNVM keystore, PUF KEK +and key-wrap +`src/riscv_sbi.c` / `include/riscv_sbi.h` - minimal SBI runtime and the vendor extension +definitions +`tools/linux/wolfboot-sbi/` - the Linux consumer of the vendor extension (`/dev/wolfboot`) +`tools/scripts/mpfs_bootmode2.sh` - sNVM and boot-mode programming for the secure dies +`tools/scripts/mpfs_qspi_prog.py` - UART QSPI flash programmer +`config/examples/polarfire_mpfs250*.config` - the seven configuration templates + +## SBI vendor extension for the OS + +The M-mode wolfBoot stays resident as the SBI for the OS. With `MPFS_SBI_VENDOR=1` it also serves the Microchip vendor extension, EID `0x09000029` (`SBI_EXT_VENDOR_START | mvendorid 0x029`), the same number the Hart Software Services register, so an OS probes one extension for either firmware and tells them apart with `GET_VERSION`. The HSS function IDs for inter-hart communication, remoteproc, crypto and IPC are answered with `SBI_ERR_NOT_SUPPORTED`; `HSS_REBOOT` (0x10) performs a cold MSS reset. + +The definitions live in `include/riscv_sbi.h`; the handler is `sbi_hal_vendor_ecall()` in `hal/mpfs250.c`. Arguments travel in `a0..a2`, the SBI error returns in `a0` and the value in `a1`. Buffers are addresses in the caller's address space: M-mode reads and writes them through the caller's translation, byte by byte, with a trap vector that turns a bad address into `SBI_ERR_INVALID_ADDRESS` instead of a stopped hart. A buffer is at most 4096 bytes and, while the caller runs with paging off, must lie in DDR. + +| FID | Call | Arguments | Result | +|-----|------|-----------|--------| +| 0x40 | `GET_VERSION` | - | `a1` = 1 | +| 0x41 | `GET_SERIAL` | `a0` buffer, `a1` length >= 16 | the 16-byte device serial number | +| 0x42 | `GET_KEY` | `a0` purpose, `a1` buffer, `a2` length == 32 | a PUF-derived key; each purpose is served once per boot, a second call returns `SBI_ERR_DENIED` | +| 0x43 | `DIGEST_CHECK` | `a0` options word | starts the System Controller digest check | +| 0x44 | `IAP_AUTHENTICATE` | `a0` SPI directory index | starts IAP image authentication | +| 0x45 | `IAP_VERIFY` | `a0` SPI directory index | starts IAP verify | +| 0x46 | `IAP_PROGRAM` | `a0` SPI directory index | the device restarts on success; a return is the failure status in `a1` | +| 0x47 | `AUTO_UPDATE` | - | the device restarts on success; a return is the failure status in `a1` | +| 0x48 | `SC_FLASH_ERASE` | `a0` flash offset, `a1` length == one erase block (64 KB), both block aligned | erases that block of the System Controller's SPI flash; `MPFS_SBI_SC_FLASH=1` builds only | +| 0x49 | `SC_FLASH_WRITE` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | programs the bytes; `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4A | `SC_FLASH_READ` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | reads the bytes (any offset, the directory included); `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4B | `SC_FLASH_SET_IMAGE` | `a0` directory index 1 (upgrade) or 2 (blank), `a1` image offset | rewrites the SPI directory block with that entry changed (entry 1 also zeroes entry 2); `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4F | `SC_STATUS` | - | `SBI_ERR_ALREADY_STARTED` while the started service runs, else `a1` = 16-bit status with mailbox word 1 (the digest-check `DIGESTERR` bits) in the upper half | + +`GET_KEY` purposes name a fixed label; the caller never supplies a challenge, so the OS cannot ask the PUF for wolfBoot's own key-encryption key. Purpose 1 is the MRAM log key (`SHA-384("wolfBoot-mram-log-v1" || PUF(SHA-384("wolfBoot-mram-log-v1")[0..15]))[0..31]`); purpose 2 reproduces the earlier user-space `varlog` derivation. The key is device-bound: the same whether the golden or an operational image booted. + +The System Controller services (0x43-0x4F) are served only in a build with `MPFS_SCB_SMODE_DENY=1`, where the OS cannot reach the mailbox itself; in an unfenced build they return `SBI_ERR_DENIED` and the OS uses its own mailbox drivers, which the fence would otherwise race. The long-running ones are started inside the ecall and polled through `SC_STATUS`, because an ecall holds its hart in M-mode with interrupts off. The mailbox is one resource for all harts: a call that finds it taken returns `SBI_ERR_FAILED`. + +The fence also covers the System Controller's QSPI flash controller, so a fenced OS cannot stage a bitstream the way the mainline `mpfs-auto-update` driver does (through `mtd` on that controller). `MPFS_SBI_SC_FLASH=1` adds the flash calls (0x48-0x4B) for that build: the OS erases and writes the image region directly, reads back what it wants verified, and points the SPI directory's upgrade entry at the image with `SC_FLASH_SET_IMAGE` before `IAP_AUTHENTICATE` / `IAP_PROGRAM` or `AUTO_UPDATE`. The first erase block (the directory and the start of the design-info area) is never erased or written through `SC_FLASH_ERASE` / `SC_FLASH_WRITE`; `SET_IMAGE` reads it, changes the one entry (the golden entry 0 stays what it was) and writes it back, the same read-erase-write the OS driver performs. Each erase call takes one block and each transfer at most 4 KB, so no ecall holds its hart for more than the erase time of one block; the OS side loops. The flash layout the mainline driver uses (directory in the first 1 KB, design info at 1 KB, bitstream slots from 1 MB + 1 KB) is the OS's business; wolfBoot only protects the directory block. + +The Linux side is the `wolfboot-sbi` module in `tools/linux/wolfboot-sbi/`: `/dev/wolfboot` (root only) with one ioctl per call, see `wolfboot_sbi.h`. It probes the extension and `GET_VERSION`, so it unloads cleanly under the HSS, and it never touches the mailbox registers, so the mainline `mpfs-sys-controller`, `mpfs-auto-update` and `mpfs-rng` drivers keep working in an unfenced build. A caller of `IAP_PROGRAM` or `AUTO_UPDATE` syncs its filesystems first: the device restarts inside the call. + +Build: `MPFS_SBI_VENDOR=1` on the M-mode target (`RISCV_MMODE=1`); the key derivation is SHA-384 whatever the image hash. + +## Hardware root of trust: PUF KEK, sNVM keystore, wrapped image key + +The PolarFire SoC MPFS250 hardware-root-of-trust features belong to the wolfBoot E51 M-mode boot flow ([M-mode + DDR](#m-mode--ddr-booting-linux-minimal-sbi)). They let wolfBoot anchor key material in the chip's System Controller instead of compiling it into the bootloader image: + +1. A System Controller mailbox driver for secure NVM (sNVM), the SRAM-PUF, and the nonce/TRNG service. +2. An sNVM keystore backend that serves the verification public keys from sNVM at boot. +3. A device-unique Key Encryption Key (KEK) derived from the SRAM-PUF, used to wrap/unwrap individual keys. +4. A PUF-wrapped image-encryption key: the AES image key is stored in sNVM wrapped by the PUF KEK and unwrapped at boot, with the bulk image decrypted on the M-mode disk-boot path. + +All features are off by default and gated behind build flags, so a normal build is unaffected. + +### Trust model + +The intended chain, from the immutable hardware up: + +``` +PolarFire boot ROM (secure boot of wolfBoot: not configured on the Video Kit, bootmode 1) + -> wolfBoot (M-mode, eNVM) + -> System Controller SRAM-PUF (fixed challenge) -> SHA-384 -> 256-bit KEK + -> sNVM keystore: ECC384 verification public key(s) + -> sNVM: AES-256 image key, wrapped by the PUF KEK + -> verify + decrypt the signed/encrypted Yocto FIT + -> 4-CPU SMP Linux +``` + +The KEK is re-derived on every boot and never leaves the device. The public keys are not secret; storing them in sNVM (rather than the image) lets the trust anchor be provisioned and rotated per device, and binds verification to the System Controller. + +#### Production requirement: the keystore pages must not stay writable + +The sNVM write service is reachable from anything that can drive the System Controller mailbox, including the Linux `mpfs-sys-controller` driver, so on the development flow above the trust anchor is only as protected as the OS. A production unit closes this in one of two ways, both opt-in at build time: + +- Deliver the keystore pages as ROM in the bitstream (Libero sNVM client with the keystore image as content and the ROM attribute set) and build with `SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1`: wolfBoot reads the page admin word of every keystore module and refuses to boot unless its ROM flag is set. The ROM flag is a bitstream property; the sNVM system services can neither set nor clear it, so there is no on-device step that can lock a page by accident. The on-device provisioning writer is for development only and cannot write a ROM page. +- Build with `MPFS_SCB_SMODE_DENY=1`: every hart's PMP fences S-mode off the System Controller control and mailbox window (`0x37020000`-`0x37020FFF`) before the hand-off, and the DTB fixup disables the `mailbox@37020800`, `spi@37020100` (System Controller QSPI) and `syscontroller` nodes so the OS does not probe hardware it cannot reach. A supervisor access into the window is otherwise redirected to the kernel's trap vector as an access fault (this core cannot delegate access faults, so wolfBoot's SBI forwards them). The sNVM pages stay writable by wolfBoot (M-mode) only. This removes the OS-side System Controller services (device serial number, bitstream services, sNVM access, the zeroize procedure) and anything the OS image builds on them, so any unit that reads a key or the serial number through the mailbox must be dropped from the image or moved to the [SBI vendor extension](#sbi-vendor-extension-for-the-os). + +### 1. System Controller mailbox services + +`hal/mpfs250.c` drives the System Controller services mailbox (`SCBCTRL` at `0x37020000`, mailbox RAM at `0x37020800`). A small generic `mpfs_scb_request()` issues a service and polls the request/busy bits; `mpfs_scb_read()` reads the response. The following services are wrapped: + +| Service | Opcode | wolfBoot wrapper | +|---------|--------|------------------| +| Device serial number | `0x00` | `mpfs_read_serial_number()` | +| sNVM write (plaintext / authenticated / ciphertext) | `0x10` / `0x11` / `0x12` | `mpfs_snvm_write()` | +| sNVM read | `0x18` | `mpfs_snvm_read()` | +| PUF emulation | `0x20` | `mpfs_puf_emulation()` | +| Nonce (TRNG) | `0x21` | `mpfs_nonce()` | + +sNVM holds 221 modules of 252 bytes (plaintext) or 236 bytes plus a 12-byte User Secret Key (authenticated). The PUF emulation service returns a 32-byte device-unique response for a 16-byte challenge; for a fixed challenge the response is stable within a boot and across cold boots, which is what makes it usable as a KEK seed. The nonce service returns 32 random bytes. + +Note: the TRNG (nonce) and PUF services take far longer than serial/sNVM-read, so the busy-completion wait uses a larger bound (`MPFS_SCB_BUSY_TIMEOUT`) than the request-accept wait (`MPFS_SCB_TIMEOUT`). + +### 2. sNVM public-key keystore (`SNVM_KEYSTORE`) + +`hal/mpfs250_snvm.c` + `hal/mpfs250_snvm.h` implement the five-function keystore API (`keystore_num_pubkeys`, `keystore_get_buffer`, `keystore_get_size`, `keystore_get_mask`, `keystore_get_key_type`) by reading the trust anchor from sNVM. The on-sNVM layout is the same family as the OTP keystore (`src/flash_otp_keystore.c`): a `WOLFBOOT`-magic header followed by packed `struct keystore_slot` entries, read through a linear reader that spans consecutive sNVM modules from `SNVM_KEYSTORE_MODULE` (default 200, `SNVM_KEYSTORE_MAX_MODULES` default 1; an ML-DSA-87 key needs 11). For the on-device provisioning writer every module in the range must be left runtime-writable (not ROM) in the Libero sNVM configuration: on the stock Video Kit design only modules 200 and 201 are. A production bitstream delivers the same page contents as ROM instead (see the trust model above). + +Build wiring (mirrors `FLASH_OTP_KEYSTORE`): + +- `SNVM_KEYSTORE=1` links `hal/mpfs250_snvm.o` in place of the compiled-in `src/keystore.o`, so the running bootloader contains no public keys and serves them from sNVM. +- `SNVM_KEYSTORE_PROVISION=1` keeps the compiled keystore and adds `snvm_keystore_provision()`, a one-time on-device writer that copies the compiled keystore image into sNVM (called from `hal_init`). Run once on a board, then deploy the `SNVM_KEYSTORE=1` build. Both provisioning writers read the page first and skip it when it already holds the content (`snvm: module N already provisioned`), so the extra boot the programmer triggers after flashing, or a second boot of the provisioning image, does not spend another write cycle. + +A keystore page that reads back as all zeros is reported as `sNVM keystore zeroized` and halts the boot. That is the state a zeroize procedure leaves behind (the OS overwrites the keystore and encryption-key pages through the System Controller's plaintext sNVM write service), and it is distinguished from a corrupt header, which merely yields no keys, so a deliberately erased unit is identifiable on the console. Recovery is the provisioning build again. + +### 3. PUF KEK and AES key-wrap (`SNVM_KEK`) + +The KEK and key-wrap live in `hal/mpfs250_snvm.c` as well: + +- `mpfs_puf_kek()` derives the 256-bit KEK as `SHA-384(label || PUF(fixed-challenge))` truncated to 32 bytes. SHA-384 is used because it is the build's hash (SHA-256/HMAC/HKDF are compiled out); the PUF response is already a high-entropy device-unique secret, so a labelled hash provides domain separation without pulling in HKDF. +- `snvm_kek_wrap()` / `snvm_kek_unwrap()` wrap/unwrap a key with the KEK using RFC 3394 AES key-wrap (`wc_AesKeyWrap` / `wc_AesKeyUnWrap`). + +`SNVM_KEK=1` adds RFC 3394 key-wrap to the AES that an `ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1` build already compiles; the KEK has no other consumer, so the build refuses the flag without that combination. Determinism and the wrap/unwrap round trip are covered by the host unit test `tools/unit-tests/unit-snvm-kek.c`. + +The AES key-wrap uses the wolfCrypt software implementation. The Athena F5200 offload (`MPFS_ATHENA=1`) serves SHA-384 and the bulk AES-256-CTR image decrypt through wolfCrypt crypto callbacks; the key-wrap is a few blocks per boot and stays in software. + +### 4. PUF-wrapped image-encryption key + +`hal/mpfs250_snvm.c` provides `wolfBoot_get_encrypt_key()` (under `CUSTOM_ENCRYPT_KEY`): it reads `[wrapped AES key (40 B)][nonce (16 B)]` from sNVM module `SNVM_ENCKEY_MODULE` (default 201, outside the keystore range), unwraps the key with the PUF KEK, and returns the plaintext key + nonce to the decrypt path. `snvm_enckey_provision()` (under `SNVM_ENCKEY_PROVISION`) wraps a key with the device KEK and writes the blob to sNVM. + +#### Disk-image decryption enablement + +Image encryption (`EXT_ENCRYPTED`) needs `EXT_FLASH`, `MMU` or `CUSTOM_ENCRYPT_KEY`. The M-mode disk-FIT path is `WOLFBOOT_NO_PARTITIONS` and has neither external flash nor an MMU, so it qualifies through `CUSTOM_ENCRYPT_KEY`: the platform supplies the key and `hal_set_key()`, the partition-resident key store, is compiled out (`src/libwolfboot.c`). The `DISK_ENCRYPT` path in `src/update_disk.c` zeroes its key material with `ForceZero` on every exit. + +### Field-recoverable zeroize + +`DISK_FAILOVER_ZEROIZE=1` (with `DISK_GOLDEN_SLOT=1`, `SNVM_KEK=1` and the `WOLFBOOT_SNVM_WRITE_APPROVED=1` gate every sNVM writer carries) turns a failover to the golden slot into a zeroize: when both operational slots are given up on, wolfBoot zeroes sNVM page `SNVM_ENCKEY_MODULE` (the PUF-wrapped image key, read back and checked; a page that already reads as zeros is not written again) and the head of both operational slots, then boots the golden image. The keystore page is never touched, so the golden image still verifies; it must be a plaintext signed image, since the key that decrypted the operational images is gone. Nothing derived from the PUF is lost: the MRAM log key is re-derived on every boot. Re-provisioning the image key afterwards is the `SNVM_ENCKEY_PROVISION` service flow; a brownout during the zeroize write can leave the page read-only, in which case the device cannot be re-keyed. + +### Build flags summary + +| Flag | Effect | +|------|--------| +| `SNVM_KEYSTORE` | Serve the public-key trust anchor from sNVM (replaces compiled keystore) | +| `SNVM_KEYSTORE_PROVISION` | Add the one-time on-device keystore writer | +| `SNVM_KEK` | Enable the PUF KEK + RFC 3394 AES key-wrap (needs the AES-256 `ENCRYPT` + `CUSTOM_ENCRYPT_KEY` build) | +| `SNVM_ENCKEY_PROVISION` | Add the one-time PUF-wrapped encryption-key writer | +| `SNVM_KEYSTORE_MODULE` / `SNVM_KEYSTORE_MAX_MODULES` / `SNVM_ENCKEY_MODULE` | sNVM module numbers (defaults 200 / 1 / 201; the key module must lie outside the keystore range and be runtime-writable) | +| `WOLFBOOT_SNVM_WRITE_APPROVED` | Required by every knob that writes sNVM; the build fails without it | +| `SNVM_KEYSTORE_REQUIRE_ROM` | Refuse to boot unless every keystore page is ROM-flagged (bitstream-provisioned keystore) | +| `MPFS_SCB_SMODE_DENY` | PMP-fence S-mode off the System Controller mailbox and disable its DTB nodes | +| `DISK_FAILOVER_ZEROIZE` | Zero the image-key page and the operational slots when falling back to the golden image | +| `SNVM_ENCKEY_INSECURE_TEST_KEY` | Acknowledges provisioning the built-in public test key | +| `SNVM_ENCKEY_PROVISION_EXTERN` | Provision a production key instead: the integrator defines `snvm_enckey_prov_key[32]` and `snvm_enckey_prov_nonce[16]` in another object | + +Example runtime encrypted build (compiled keystore for verification, PUF-wrapped key for decryption): + +```sh +make CROSS_COMPILE=riscv64-unknown-elf- LIBERO_FPGA_CONFIG_DIR= \ + ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1 SNVM_KEK=1 wolfboot.elf +``` + +### Provisioning and validation recipe + +1. Build with `SNVM_ENCKEY_PROVISION=1` (and/or `SNVM_KEYSTORE_PROVISION=1`) plus `WOLFBOOT_SNVM_WRITE_APPROVED=1` added to the runtime build above, flash, and boot once: `hal_init` writes the PUF-wrapped AES key into sNVM[201] (and/or the keystore into sNVM[200]). +2. Sign + encrypt the FIT with a key file matching the provisioned key (`prov_aes_key` || `prov_aes_nonce` in `hal/mpfs250_snvm.c`, i.e. a 32-byte key followed by a 16-byte IV): + + ```sh + IMAGE_HEADER_SIZE=512 ./tools/keytools/sign --ecc384 --sha384 \ + --encrypt enc_key.bin --aes256 fitImage wolfboot_signing_private_key.der 1 + ``` + Note: `make keysclean` regenerates the signing key, so re-sign the image after any keysclean or the compiled keystore will not match. +3. Write `fitImage_v1_signed_and_encrypted.bin` to the SD boot partition. With the stock HSS, boot to the `>>` CLI, run `usbdmsc`, and `dd` to the first partition of the exposed disk. Do not move an SD-Wire mux while `usbdmsc` is running: HSS's MMC layer then serves one stale block for every LBA and does not recover until `usbdmsc` is restarted, which makes a populated card read as blank. +4. Flash the runtime build and cold-boot. Expected UART0: `Disk encryption enabled` -> `Decrypting image... done` -> `Firmware Valid.` -> `M->S handoff`, and UART1 reaches the Linux login with `Brought up 1 node, 4 CPUs` and no `failed to come online`. + +### Sample validation logs (MPFS250TS Video Kit) + +Verified on the kit: the PUF response is identical for a fixed challenge across cold power cycles while the nonce service is random, and the PUF-derived KEK wraps and unwraps a test key to the same bytes on every boot (`tools/unit-tests/unit-snvm-kek.c` covers the KEK logic). A full encrypted boot reaches `Firmware Valid.` by way of `Disk encryption enabled` and `Decrypting image...done`, and Linux reaches its login with `Brought up 1 node, 4 CPUs` and no `failed to come online`. diff --git a/docs/README.md b/docs/README.md index cbe7a1f33a..67be49e2c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,7 @@ See also: [wolfBoot Product Overview](https://www.wolfssl.com/products/wolfboot/ - [**Loader.md**](./Loader.md) - Loader/secondary stage behavior and handoff to application. - [**measured_boot.md**](./measured_boot.md) - Measured boot concepts and recording measurements (e.g., PCRs). - [**png/**](./png) - Folder of images/diagrams referenced by the documentation. +- [**PolarFire.md**](./PolarFire.md) - Microchip PolarFire SoC: quick start, boot configurations, sNVM/PUF key storage, SBI vendor extension, boot mode 2. - [**PQ.md**](./PQ.md) - Post-quantum algorithms and PQC support in wolfBoot. - [**README.md**](./README.md) - Overview and index of the documentation set. - [**remote_flash.md**](./remote_flash.md) - Working with external/remote flash (SPI/QSPI, mapping, access). diff --git a/docs/Targets.md b/docs/Targets.md index 1d1aa9edcd..023811a768 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -882,22 +882,11 @@ add-symbol-file test-app/image.elf 0x20020100 ## Microchip PolarFire SoC -> **Filesystem boot:** this target can also read its signed image from a **file** on a read-only FAT32 or ext4 partition instead of from raw offset 0. Build with `DISK_FS=fat32|ext4|both` and set `BOOT_FILE_A` / `BOOT_FILE_B`. See [Disk boot from a read-only filesystem (FAT32 / ext4)](compile.md#disk-boot-from-a-read-only-filesystem-fat32--ext4). - -The PolarFire SoC is a 64-bit RISC-V SoC featuring a five-core CPU cluster (1× E51 monitor core and 4× U54 application cores) and FPGA fabric. Tested with MPFS250. - -### Features -* RISC-V 64-bit architecture (rv64imac) -* Five-core CPU: 1× E51 monitor + 4× U54 application cores -* Integrated DDR3/4, LPDDR3/4 controller and PHY -* PCIe Gen2, USB 2.0, and Gigabit Ethernet interfaces -* Secure boot capabilities -* Low power consumption -* External flash support +The PolarFire SoC is a 64-bit RISC-V SoC with a five-core CPU cluster (1x E51 monitor core and 4x U54 application cores) and FPGA fabric, tested here on MPFS250. wolfBoot runs on it either as a payload of Microchip's Hart Software Services (HSS) or as a complete replacement for it: in M-mode from eNVM it brings up LPDDR4, loads and verifies a signed image from SD card or QSPI, hands a U54 into S-mode and stays resident as the SBI the OS runs on. -### Supported Boot Configurations +**[PolarFire.md](PolarFire.md) is the documentation for this target** - a quick start that takes one build, one card and one eNVM program end to end, then every configuration in detail, QSPI and DDR notes, boot mode 2, sNVM/PUF key storage, the SBI vendor extension for the OS, Yocto, debugging and benchmarks. The two tables below are the summary. -Six ready-to-use config templates cover all supported boot mode / storage / memory combinations: +### PolarFire SoC boot configurations | Configuration | Config File | Boot Mode | Storage | Memory | HSS | |---------------|-------------|-----------|---------|--------|-----| @@ -907,1230 +896,34 @@ Six ready-to-use config templates cover all supported boot mode / storage / memo | **QSPI + L2-LIM** | `polarfire_mpfs250_hss_l2lim.config` | S-mode (U54 via HSS) | SC QSPI | L2-LIM (no DDR) | Yes | | **M-Mode (no HSS)** | `polarfire_mpfs250_m_qspi.config` | M-mode (E51, no HSS) | SC QSPI | L2 Scratchpad | No | | **M-Mode + DDR** | `polarfire_mpfs250_m.config` | M-mode (E51, no HSS) | SD Card | LPDDR4 (DDR) | No | +| **Boot mode 2 UBL** | `polarfire_mpfs250_ubl.config` | System Controller -> sNVM stub | eNVM (verifies it) | L2-LIM | No | + +### PolarFire SoC build options + +Make variables, on the command line or in the `.config`. See [PolarFire.md](PolarFire.md) for what each one does and what it requires. + +| Option | Purpose | +|--------|---------| +| `LIBERO_FPGA_CONFIG_DIR` | Path to the board's Libero/HSS `fpga_design_config` headers. Enables LPDDR4 init (`MPFS_DDR_INIT`); required for the M-Mode + DDR build. | +| `MPFS_ATHENA` (+ `MPFS_ATHENA_CAL_DIR`) | SHA-384 and AES-256-CTR offload to the Athena F5200 (TeraFire), with a mandatory self-check. | +| `SNVM_KEYSTORE`, `SNVM_KEYSTORE_PROVISION`, `SNVM_KEYSTORE_REQUIRE_ROM` | Serve the verification keystore from secure NVM; the one-time writer; refuse to boot unless every keystore page is ROM-flagged. | +| `SNVM_KEK`, `SNVM_ENCKEY_PROVISION` | PUF-derived KEK with RFC 3394 key-wrap for the sNVM-stored image-encryption key, and its one-time writer. | +| `MPFS_SCB_SMODE_DENY` | Fence S-mode off the System Controller mailbox (PMP plus dtb node removal), so nothing after wolfBoot can reach the sNVM write service. | +| `MPFS_SBI_VENDOR` | SBI vendor extension for the OS: firmware version, device serial, PUF-derived keys by purpose, and the System Controller services behind the fence. | +| `MPFS_SBI_SC_FLASH` | Adds System Controller SPI flash erase, write, read and directory update over that extension, for staging a bitstream from a fenced OS. | +| `MPFS_ENVM_MANIFEST` | Reserve the last eNVM page for the signed manifest the boot mode 2 stub verifies. | +| `DISK_GOLDEN_SLOT`, `DISK_FAILOVER_ZEROIZE` | Golden slot tried after A/B, and zeroizing the image key and slot heads before the golden boot. | +| `MPFS_QSPI_CLK_DIV` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h` (default `QSPI_CLK_DIV_8`). | +| `MPFS_WDT_NODES_KEEP` | Leave the MSS watchdog nodes enabled and stop the parked E51 refreshing them, so the OS owns the watchdogs. | +| `WATCHDOG`, `WATCHDOG_TIMEOUT_MS` | Keep the E51 watchdog running during wolfBoot itself, with the given timeout (default 30 s). | + +> **Note:** the MPFS250 U54/E51 cores have no RISC-V crypto extensions (Zknh), so none of the +> configurations use them. The SD-card, QSPI and M-Mode + DDR configs build the portable RISC-V +> SHA assembly (`NO_ASM=0`); the size-constrained L2-LIM and M-mode QSPI configs set `NO_ASM=1` +> and use the C implementations. -The **M-Mode + DDR** configuration brings up the LPDDR4 controller from -the E51 in M-mode (no HSS), then loads a signed FIT image from SD card, -verifies it (SHA384 + ECC384) and hands off to a U54 hart in S-mode. -wolfBoot includes a minimal SBI runtime (`src/riscv_sbi.c`) so the -hand-off target can be a Linux kernel: tested booting 4-CPU SMP Yocto -Linux to a login prompt in ~40 s from power-on on the MPFS250T Video -Kit. Because all -LIBERO_SETTING_\* values are board-specific, this build pulls them from -a Libero/HSS-generated `fpga_design_config.h` pointed at by the -`LIBERO_FPGA_CONFIG_DIR` makefile variable - typical sources are an -HSS Video Kit build at -`/build/boards/mpfs-video-kit/fpga_design_config` or a Libero MSS -Configurator export. Setting `LIBERO_FPGA_CONFIG_DIR` automatically -defines `MPFS_DDR_INIT` and adds the directory to the include path -(see `arch.mk`); when unset, the DDR HAL is excluded and the build -still produces a working M-mode wolfBoot without DDR. Add -`-DDEBUG_DDR` to `CFLAGS_EXTRA` for verbose register-level traces -during bring-up. - -For GitHub Actions, `tools/ci/gen_mpfs_libero_stub.sh` generates a -compile-only stub (every `LIBERO_SETTING_*` symbol referenced by the -HAL, defined to `0`; loop-bound settings to `1U`) so the M-Mode + DDR -build path stays under continuous integration. The generated header is -**not** committed and **not** runnable - real boards must point -`LIBERO_FPGA_CONFIG_DIR` at the actual Libero / HSS output. - -Key build settings that differ between configurations: - -| Setting | SDCard | eMMC | QSPI | L2-LIM | M-Mode | M-Mode + DDR | -|---------|--------|------|------|--------|--------|--------------| -| `WOLFBOOT_ORIGIN` | `0x80000000` | `0x80000000` | `0x80000000` | `0x08040000` | `0x0A000000` | `0x0A000000` | -| `WOLFBOOT_LOAD_ADDRESS` | `0x8E000000` | `0x8E000000` | `0x8E000000` | `0x08060000` | `0x0A010200` | `0x82000000` | -| `WOLFBOOT_LOAD_DTS_ADDRESS` | `0x8A000000` | `0x8A000000` | `0x8A000000` | – | – | `0x8A000000` | -| `EXT_FLASH` | 0 | 0 | 1 | 1 | 1 | 0 | -| `DISK_SDCARD` | 1 | 0 | 0 | 0 | 0 | 1 | -| `DISK_EMMC` | 0 | 1 | 0 | 0 | 0 | 0 | -| `MPFS_L2LIM` | – | – | – | 1 | – | – | -| `RISCV_MMODE` | – | – | – | – | 1 | 1 | -| `LIBERO_FPGA_CONFIG_DIR` | – | – | – | – | – | required | -| `WOLFBOOT_MMODE_SMODE_BOOT` | – | – | – | – | – | 1 | -| Linker script | `mpfs250.ld` | `mpfs250.ld` | `mpfs250.ld` | `mpfs250-hss.ld` | `mpfs250-m.ld` | `mpfs250-m.ld` | -| HSS YAML | `mpfs.yaml` | `mpfs.yaml` | `mpfs.yaml` | `mpfs-l2lim.yaml` | N/A | N/A | -| `ELF` output | 1 | 1 | 1 | 0 (raw .bin) | 1 | 1 | - -> **Note:** All configurations require `NO_ASM=1` because the MPFS250 U54/E51 cores lack RISC-V -> crypto extensions (Zknh); wolfBoot uses portable C implementations for all cryptographic operations. - -### M-Mode Optional Build Flags - -These flags apply to `polarfire_mpfs250_m_qspi.config` and are added via `CFLAGS_EXTRA+=-D...`. - -| Flag | Default | Description | -|------|---------|-------------| -| `WATCHDOG` | undefined (disabled) | When defined, the E51 watchdog timer is **kept enabled** during wolfBoot operation with a generous timeout. When undefined, the WDT is **disabled** in `hal_init()` and re-enabled with the boot ROM default in `hal_prepare_boot()` before jumping to the application. Either way, the application receives a normal WDT. | -| `WATCHDOG_TIMEOUT_MS` | `30000` (30 s) | Watchdog timeout in milliseconds when `WATCHDOG` is defined. ECDSA P-384 verification on E51 with portable C math is bounded at ~5 s; the default 30 s avoids any need to refresh the WDT during the long verify call. | -| `MPFS_WDT_NODES_KEEP` (make variable, `=1`) | undefined | Linux boot only: keep the five MSS watchdog nodes enabled in the dtb and stop the parked E51 from refreshing them, so the OS owns the watchdogs. See "Watchdog policy" below. | -| `MPFS_QSPI_CLK_DIV` (make variable) | `QSPI_CLK_DIV_8` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h`. See "PolarFire SoC QSPI". | - -#### Stack overflow detection - -The trap handler in `src/boot_riscv.c` automatically detects stack overflow on synchronous exceptions (requires `DEBUG_BOOT`). When a trap fires with `SP < _main_hart_stack_bottom`, it prints: - -``` -TRAP: cause=2 epc=A000740 tval=0 - sp=A02FFE8 -STACK OVERFLOW: under by 24 -``` - -This is helpful for diagnosing illegal-instruction TRAPs at random valid `.text` addresses, which are the classic signature of stack overflow corrupting the return address. - -The current `STACK_SIZE` in `hal/mpfs250-m.ld` is **32 KB**. Measured peak for ECC384 + SHA384 + SPMATHALL + NO_ASM is ~6 KB (5x headroom). - -### PolarFire SoC Files - -`hal/mpfs250.c` - Hardware abstraction layer (UART, QSPI, SD/eMMC, multi-hart) -`hal/mpfs250.h` - Register definitions and hardware interfaces -`hal/mpfs250_ddr.c` - LPDDR4 PHY/PLL/training and DDR bring-up (M-mode, no HSS) -`src/ddr_cadence.c` - Generic Cadence DDR controller driver (controller CSR -programming, Memory Test Controller, LPDDR4 mode-register protocol) -`include/ddr_cadence.h` - Generic Cadence DDR controller register map + interface -`hal/mpfs250.ld` - Linker script for S-mode (HSS-based boot) -`hal/mpfs250-m.ld` - Linker script for M-mode (eNVM + L2 SRAM) -`hal/mpfs250-hss.ld` - Linker script for S-mode (HSS with L2-LIM) -`hal/mpfs250_ubl.c` / `hal/mpfs250_ubl.ld` - Boot mode 2 User Boot Loader stub (sNVM-authenticated verifier of the eNVM wolfBoot) -`hal/mpfs.dts` - Device tree source -`hal/mpfs.yaml` - HSS payload generator configuration for use of DDR -`hal/mpfs-l2lim.yaml` - HSS payload generator for the use of L2-LIM -`hal/mpfs250.its` - Example FIT image creation template - -### PolarFire SoC Building wolfBoot - -All build settings come from .config file. For this platform use `TARGET=mpfs250` and `ARCH=RISCV64`. - -See example configuration at `config/examples/polarfire_mpfs250.config`. - -```sh -# Setup .config (build settings) -cp config/examples/polarfire_mpfs250.config .config - -# build boot loader -make wolfboot.elf -``` - -To assemble this as a flashable image you need the 0x100 byte HART header added: - -```sh -git clone https://github.com/polarfire-soc/hart-software-services.git -cd hart-software-services -cd tools/hss-payload-generator -make -# install tool -sudo cp hss-payload-generator /usr/local/bin/ -``` - -The HSS MMC boot source looks for GPT with GUID "21686148-6449-6E6F-744E-656564454649" or sector "0" if no GPT found. That GUID is the default "BIOS" boot partition. - -The resulting image from `hss-payload-generator` can be directly placed into GPT BIOS partition. - -Use this command to assemble a bootable wolfboot image: - -```sh -hss-payload-generator -vvv -c ./hal/mpfs.yaml wolfboot.bin -``` - -You must generated the Device Tree Binary using: - -```sh -dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb` -``` - -Example one-shot command: - -```sh -cp ./config/examples/polarfire_mpfs250.config .config && make clean && make wolfboot.elf && size wolfboot.elf && dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb && hss-payload-generator -vvv -c ./hal/mpfs.yaml wolfboot.bin -``` - -The HSS tinyCLI supports the `USBDMSC` command to mount the eMMC or SD card as a USB device. You can then use "dd" to copy the boot image to the BOOT partition. Use `lsblk` to locate the boot partition and replace /dev/sdc1 in the example: - -```sh -sudo dd if=wolfboot.bin of=/dev/sdc1 bs=512 && sudo cmp wolfboot.bin /dev/sdc1 -``` - -### PolarFire SoC QSPI - -PolarFire SoC has two CoreQSPI v2 controllers with identical register layouts. The selection -is made at build time via `MPFS_SC_SPI` and affects which QSPI base address wolfBoot uses: - -```text - +-------------------+ +----------------------+ - | U54 cores | | U54 cores | - | (wolfBoot) | | (wolfBoot) | - +---------+---------+ +----------+-----------+ - | | - | direct register access | direct register access - | (MSS QSPI @ 0x2100_0000) | (SC QSPI @ 0x3702_0100) - v v - +-------------------+ +----------------------+ - | MSS QSPI IP | | SC QSPI IP | - | (CoreQSPI v2) | | (CoreQSPI v2) | - +---------+---------+ +----------+-----------+ - | | - v v - External QSPI flash Fabric-connected flash -``` - -Build options: - -- MSS QSPI controller (direct register access at 0x21000000, read/write/erase) - - `EXT_FLASH=1` - - Do not set `MPFS_SC_SPI` - - Example config: `config/examples/polarfire_mpfs250_qspi.config` with `CFLAGS_EXTRA` line removed. - -- SC QSPI controller (direct register access at 0x37020100, read/write/erase) - - `EXT_FLASH=1` - - `CFLAGS_EXTRA+=-DMPFS_SC_SPI` - - Example config: `config/examples/polarfire_mpfs250_qspi.config` as-is. - - Both controllers share the same CoreQSPI v2 register interface. - The only difference is that SC QSPI does not need MSS clock/reset setup. - -Example single-shot build: `cp config/examples/polarfire_mpfs250_qspi.config .config && make clean && make wolfboot.bin && hss-payload-generator -vvv -c ./hal/mpfs.yaml wolfboot.bin && make test-app/image.elf && ./tools/keytools/sign --ecc384 --sha384 test-app/image.elf wolfboot_signing_private_key.der 1` - -Notes: -- Both modes support full read, write, and erase operations. -- SCK is the controller clock divided by the CoreQSPI `CLKRATE` field, SCK = PCLK / (2 * CLKRATE). - `MPFS_QSPI_CLK_DIV` selects the divider by its `QSPI_CLK_DIV_*` name from `hal/mpfs250.h` - (`QSPI_CLK_DIV_2` through `QSPI_CLK_DIV_30`, CLKRATE 1..15). The default `QSPI_CLK_DIV_8` is - about 18.75 MHz from the 150 MHz APB clock, where read throughput stops improving: from there on - the polled FIFO transfer rather than SCK sets the rate, so a faster SCK buys nothing. The MSS QSPI - PCLK is the MSS APB clock, which is 40 MHz at the M-mode reset clock until wolfBoot's PLL - bring-up, so an M-mode build that starts QSPI before `mss_pll_init()` runs SCK that much slower; - the SC QSPI is clocked by the System Controller and is unaffected by the MSS PLL. -- For QSPI-based boot flows, disable SD/eMMC in the config (`DISK_SDCARD=0`, `DISK_EMMC=0`) unless you - explicitly want wolfBoot to load from disk and the application from QSPI. -- The MSS QSPI path expects external flash on the MSS QSPI pins; the SC QSPI path is for - fabric-connected flash (design flash) accessed via the System Controller's QSPI instance. - -### PolarFire SoC HSS S-Mode with L2-LIM (no DDR) - -wolfBoot can run in S-mode via HSS without DDR by targeting the on-chip **L2 Loosely Integrated -Memory (L2-LIM)**. HSS loads wolfBoot into L2-LIM on a U54 application core, and wolfBoot loads -the signed application from SC QSPI into L2-LIM as well. This is the configuration for systems -that keep HSS and run without DDR. - -**Features:** -* S-mode on U54 application core (hart 1), loaded by HSS -* wolfBoot and application both reside in L2-LIM (`0x08000000`, up to 1.5 MB) -* No DDR required -* SC QSPI flash for both wolfBoot payload and signed application image -* Raw binary output (`ELF=0`) required — ELF with debug symbols is too large for L2-LIM - -**Relevant files:** - -| File | Description | -|------|-------------| -| `config/examples/polarfire_mpfs250_hss_l2lim.config` | HSS S-mode + SC QSPI + L2-LIM | -| `hal/mpfs250-hss.ld` | Linker script for S-mode with L2-LIM | -| `hal/mpfs-l2lim.yaml` | HSS payload generator YAML for L2-LIM load target | - -**Build:** -```sh -cp config/examples/polarfire_mpfs250_hss_l2lim.config .config -make clean && make wolfboot.bin -dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb -hss-payload-generator -vvv -c ./hal/mpfs-l2lim.yaml wolfboot.bin -``` - -Flash the HSS payload to the eMMC/SD BIOS partition using HSS `USBDMSC`: -```sh -sudo dd if=wolfboot.bin of=/dev/sdc1 bs=512 && sudo cmp wolfboot.bin /dev/sdc1 -``` - -**Build and sign the test application:** -```sh -make test-app/image_v1_signed.bin -``` - -**Flash the signed application to QSPI** using the UART programmer (enabled by default via -`UART_QSPI_PROGRAM=1`; requires `pyserial` installed): -```sh -python3 tools/scripts/mpfs_qspi_prog.py /dev/ttyUSB1 \ - test-app/image_v1_signed.bin 0x20000 -``` - -**Notes:** -- `UART_QSPI_PROGRAM=1` adds a 3-second boot pause every time. Set to `0` once the flash contents are stable. -- `ELF=0` is required: the test-app linker script (`test-app/RISCV64-mpfs250.ld`) places `.init` - (containing `_reset()`) first so the raw binary entry point is at offset 0. The full ELF with - debug symbols exceeds L2-LIM capacity. -- wolfBoot is placed at `0x08040000` (above the HSS L2-LIM resident region) and the application - is loaded at `0x08060000`. The stack resides at the top of the 1.5 MB L2-LIM region. -- HSS must be built and programmed to eNVM separately (see [PolarFire Building Hart Software Services](#polarfire-building-hart-software-services-hss)). -- **LIM instruction fetch caveat:** Ensure `L2_WAY_ENABLE` leaves enough cache ways unallocated - to back the LIM SRAM region. See the M-mode section for a detailed explanation. -- UART output appears on MMUART1 (`/dev/ttyUSB1`), same as other S-mode configurations. - -### PolarFire SoC M-Mode (bare-metal eNVM boot) - -wolfBoot supports running directly in Machine Mode (M-mode) on PolarFire SoC, replacing the Hart -Software Services (HSS) as the first-stage bootloader. wolfBoot runs on the E51 monitor core from -eNVM and loads a signed application from SC QSPI flash into L2 Scratchpad (on-chip RAM) -- no HSS -or DDR required. This is the minimal on-chip-only configuration. - -**Features:** -* Runs on E51 monitor core (hart 0) directly from eNVM -* Executes from L2 Scratchpad SRAM (256 KB at `0x0A000000`) -* Loads signed application from SC QSPI flash to L2 Scratchpad (`0x0A010200`) -* No HSS or DDR required — boots entirely from on-chip memory -* Parks and releases secondary U54 harts via CLINT IPI -* ECC384 + SHA384 signature verification - -**Relevant files:** - -| File | Description | -|------|-------------| -| `config/examples/polarfire_mpfs250_m_qspi.config` | M-mode + SC QSPI configuration | -| `hal/mpfs250-m.ld` | M-mode linker script (eNVM + L2 SRAM) | -| `hal/mpfs250.c` | HAL with QSPI driver, UART, L2 cache init | -| `src/boot_riscv_start.S` | M-mode assembly startup | - -**Boot flow:** -1. **eNVM reset vector** (`0x20220100`): CPU starts, startup code copies wolfBoot to L2 Scratchpad -2. **L2 Scratchpad execution** (`0x0A000000`): wolfBoot runs from scratchpad -3. **Hardware init**: L2 cache configuration, UART setup -4. **QSPI init**: SC QSPI controller (`0x37020100`), JEDEC ID read, 4-byte address mode -5. **Image load**: Read signed image from QSPI flash (`0x20000`) to L2 Scratchpad (`0x0A010200`) -6. **Verify & boot**: SHA384 integrity check, ECC384 signature verification, jump to app - -**Build:** -```sh -cp config/examples/polarfire_mpfs250_m_qspi.config .config -make clean && make wolfboot.elf -``` - -**Flash wolfBoot to eNVM** (requires SoftConsole / Libero SoC install): -```sh -export SC_INSTALL_DIR=/opt/Microchip/SoftConsole-v2022.2-RISC-V-747 - -$SC_INSTALL_DIR/eclipse/jre/bin/java -jar \ - $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ - --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD wolfboot.elf -``` - -**Build and sign the test application:** -```sh -make test-app/image_v1_signed.bin -``` - -**Flash the signed application to QSPI** using the UART programmer (requires `EXT_FLASH=1` and -`UART_QSPI_PROGRAM=1` in `.config`, and `pyserial` installed): -```sh -python3 tools/scripts/mpfs_qspi_prog.py /dev/ttyUSB0 \ - test-app/image_v1_signed.bin 0x20000 -``` - -The script: -1. Waits for wolfBoot to print the `QSPI-PROG: Press 'P'` prompt (power-cycle the board) -2. Sends `P` to enter programming mode -3. Transfers the binary in 256-byte ACK-driven chunks -4. wolfBoot erases, writes, and then continues booting the new image - -Use `0x20000` for the boot partition and `0x02000000` for the update partition. - -**QSPI partition layout** (Micron MT25QL01G, 128 MB): - -| Region | Address | Size | -|--------|---------|------| -| Boot partition | `0x00020000` | ~32 MB | -| Update partition | `0x02000000` | ~32 MB | -| Swap partition | `0x04000000` | 64 KB | - -**UART mapping:** - -| Hart | Core | MMUART | USB device | -|------|------|--------|------------| -| 0 | E51 | MMUART0 | /dev/ttyUSB0 | -| 1 | U54_1 | MMUART1 | /dev/ttyUSB1 | -| 2 | U54_2 | MMUART2 | N/A | -| 3 | U54_3 | MMUART3 | N/A | -| 4 | U54_4 | MMUART4 | N/A | - -**Expected serial output on successful boot:** -``` -wolfBoot Version: X.Y.Z (...) -Running on E51 (hart 0) in M-mode -QSPI: Using SC QSPI Controller (0x37020100) -QSPI: Flash ID = 0x20 0xBA 0x21 -QSPI-PROG: Press 'P' within 3s to program flash -QSPI-PROG: No trigger (got 0x00 ...), booting -Versions: Boot 1, Update 0 -... -Firmware Valid -Booting at 0x... -``` - -**Notes:** -- The E51 is `rv64imac` (no FPU or crypto extensions). wolfBoot is compiled with `NO_ASM=1` to - use portable C crypto implementations and `-march=rv64imac -mabi=lp64` for correct code - generation. The `rdtime` CSR instruction is not available in bare-metal M-mode; wolfBoot uses a - calibrated busy-loop for all delays (`udelay()` in `hal/mpfs250.c`). -- `UART_QSPI_PROGRAM=1` adds a 3-second boot pause every time. Set to `0` once the flash - contents are stable. -- The config uses `WOLFBOOT_LOAD_ADDRESS=0x0A010200` to place the application in L2 Scratchpad - above wolfBoot code (~64 KB at `0x0A000000`), with the stack at the top of the 256 KB region. -- **LIM instruction fetch limitation:** The on-chip LIM (`0x08000000`, 2 MB) is backed by L2 - cache ways. When `L2_WAY_ENABLE` is set to `0x0B` (all cache ways 0–7 active for caching), - no ways remain for LIM backing SRAM. Data reads from LIM work through the L2 cache, but - instruction fetch silently hangs — the CPU stalls with no trap generated. For this reason the - application is loaded into L2 Scratchpad (`0x0A000000`), which is always accessible regardless - of `L2_WAY_ENABLE`. To use LIM, reduce `L2_WAY_ENABLE` to free cache ways for LIM backing. -- **Strip debug symbols** before signing the test-app ELF. The debug build is ~150 KB but the - stripped ELF is ~5 KB. L2 Scratchpad has ~150 KB available between wolfBoot code and the stack: - `riscv64-unknown-elf-strip --strip-debug test-app/image.elf` -- **DDR support:** software LPDDR4 initialization is included via the **M-Mode + DDR** - configuration (`polarfire_mpfs250_m.config`, requires `LIBERO_FPGA_CONFIG_DIR`) for use cases - that require loading larger images (e.g. a Linux FIT) to DDR memory. See the next section. - -### PolarFire SoC M-Mode + DDR: booting Linux (minimal SBI) - -The **M-Mode + DDR** configuration (`config/examples/polarfire_mpfs250_m.config`) replaces both -HSS and OpenSBI: wolfBoot performs the LPDDR4 init/training on the E51, loads and verifies a -signed Yocto FIT image (kernel + dtb) from SD card into DDR, applies device-tree fixups, releases -U54 hart 1 into S-mode at the kernel entry, and then remains resident as a minimal M-mode SBI -runtime. Validated on the MPFS250T Video Kit: 4-CPU SMP Yocto Linux to login in ~40 s from -power-on. - -**Minimal SBI runtime** (`src/riscv_sbi.c`, a clean-room implementation of the OpenSBI/SBI spec; generic RISC-V with HAL hooks; enabled by -`WOLFBOOT_MMODE_SMODE_BOOT`): -* SBI v0.2 extensions: BASE, TIME (per-hart `mtimecmp`, MTIP-to-STIP injection), IPI (SSIP - injection via CLINT MSIP), RFENCE (remote `fence.i` / `sfence.vma` with completion wait), - HSM (`hart_start`/`hart_stop`/`hart_status` backed by per-hart start mailboxes), DBCN and the - legacy console putchar (shared with the wolfBoot UART), SRST. -* `rdtime` emulation: the U54/E51 have no `time` CSR, so `rdtime` is emulated from CLINT MTIME. - The M-mode HAL starts the MTIME time base before hand-off via SYSREG `RTC_CLOCK_CR` at 1 MHz - (`mpfs_enable_mtime()`), matching the device-tree `timebase-frequency`. -* Misaligned load/store emulation (not delegatable on these harts), including compressed forms, - for the kernel's unaligned copy tails. -* Per-hart M-mode trap stacks live in the `hss-buffer` reserved (nomap) DDR region; cross-hart - state (HSM mailboxes, IPI flags, the hart-release gate flag) lives in the E51 DTIM at - `0x01000000`, which is uncached and coherent for all harts. Cacheable L2-scratchpad memory - must not be used for cross-hart signalling (stores can be lost on dirty-line eviction). - -**Device-tree fixups** applied to the loaded dtb (`hal/mpfs250.c`): bootargs/root device -(the DTB's own `/chosen/bootargs` wins unless `LINUX_BOOTARGS`/`LINUX_BOOTARGS_ROOT` is set; -see "Linux kernel command line (bootargs)" in `docs/compile.md`), -MAC addresses from the device serial number, and all five MSS watchdog nodes are disabled. - -**Watchdog policy:** the MSS watchdogs always count and reset the chip on timeout (they cannot -be disabled in hardware, and `CONTROL=0` does not prevent the reset). After hand-off the parked -E51 acts as a monitor and refreshes all five watchdogs; the OS watchdog driver is disabled via -the dtb fixup so the two never conflict. `MPFS_WDT_NODES_KEEP=1` hands the watchdogs to the OS -instead: the dtb nodes stay enabled and the parked E51 stops refreshing, so the OS driver arms -them at probe and something on the OS side (a watchdog daemon, or the design's own fabric -scheme) has to service them from then on. A design with its own fabric watchdog needs neither -setting; wolfBoot never touches fabric logic. - -**Hand-off / SMP flow:** secondary harts park in eNVM until the E51 signals image-copy -completion (DTIM gate flag), then park in a WFI loop. The boot hart is released with a staged -mailbox {entry, dtb} plus MSIP; Linux brings up the remaining harts through SBI HSM -`hart_start`, which uses the same mailbox + MSIP path. The release path must stay fast -(no UART access): the kernel allows roughly one second for a started hart to come online. - -**Driver structure:** the licensed Cadence DDR controller logic (controller CSR programming, the -Memory Test Controller engine, and the LPDDR4 mode-register protocol) lives in the -target-independent `src/ddr_cadence.c` / `include/ddr_cadence.h` (controller base overridable via -`DDR_CADENCE_CTRL_BASE`). The Microchip-specific PHY, PLL, clock mux and training, plus the -board's `LIBERO_SETTING_*` values, stay in `hal/mpfs250_ddr.c`, which builds the controller -register table and composes the generic calls. Both compile only when `MPFS_DDR_INIT` is set. - -### PolarFire SoC hardware root of trust (PUF KEK, sNVM keystore, wrapped encryption key) - -The System Controller SRAM-PUF, secure NVM (sNVM), and TeraFire crypto can anchor key material in hardware: serve the verification public keys from sNVM, derive a device-unique KEK from the PUF, and store the AES image-encryption key in sNVM wrapped by that KEK. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). - -### PolarFire SoC boot mode 2: User Boot Loader stub - -Boot mode 2 ("user secure boot from sNVM") lets the System Controller authenticate the first code the MSS runs: it copies a User Boot Loader (UBL) out of authenticated sNVM pages into the LIM and releases all five harts at its entry. wolfBoot provides that UBL as a small verifier target, `TARGET=mpfs250_ubl`: it checks an ECC384/SHA384 wolfBoot manifest over the eNVM wolfBoot image against the public key compiled into the stub, then enters the eNVM wolfBoot at its reset vector on every hart exactly as a boot mode 1 boot would. The eNVM wolfBoot itself is unchanged and stays field-updatable; what the sNVM pages pin is the key that must have signed it. - -**eNVM layout** (`hal/mpfs250-m.ld`, `hal/mpfs250.h`) - -| Address | Content | -|---------|---------| -| `0x20220000` | boot ROM meta information (written by `mpfsBootmodeProgrammer`) | -| `0x20220100` | wolfBoot image, reset vector first | -| `0x2023FC00` | wolfBoot manifest header (`MPFS_ENVM_MANIFEST=1`), signed by the stub's key | - -The manifest is detached from the image it signs (the `WOLFBOOT_SELF_HEADER` form), so the image does not move and a boot mode 1 device boots the same eNVM contents. The last two eNVM pages stay free for the programmer. - -**Relevant files:** - -| File | Description | -|------|-------------| -| `config/examples/polarfire_mpfs250_ubl.config` | UBL stub configuration | -| `hal/mpfs250_ubl.c`, `hal/mpfs250_ubl.h` | stub HAL: watchdog hold, MMUART0 at the reset clock, manifest verify, hart hand-off | -| `hal/mpfs250_ubl.ld` | LIM linker script with the sNVM size limit | -| `src/boot_riscv_start.S` | `TARGET_mpfs250_ubl` startup: no copy, U54s park on a DTIM mailbox | -| `tools/scripts/mpfs_bootmode2.sh` | sNVM + boot mode programming on the secure dies, eNVM-only update | - -**Build the stub** (its own signing key; keep it out of the tree that signs application images): -```sh -cp config/examples/polarfire_mpfs250_ubl.config .config -make clean && make -# wolfboot.elf is the UBL; wolfboot_signing_private_key.der is the UBL key -``` -The image must fit the 221 authenticated sNVM pages (52,096 bytes after the 60-byte UBL header); the linker script enforces it. The sNVM pages above the stub stay available for the sNVM keystore and wrapped keys of [polarfire_snvm_puf.md](polarfire_snvm_puf.md). - -**Build and sign the eNVM wolfBoot** for the stub (any M-mode configuration): -```sh -cp config/examples/polarfire_mpfs250_m.config .config -make clean && make LIBERO_FPGA_CONFIG_DIR=... MPFS_ENVM_MANIFEST=1 \ - MPFS_UBL_KEY=/path/to/ubl/wolfboot_signing_private_key.der \ - wolfboot_envm_signed.bin -``` -`MPFS_ENVM_MANIFEST=1` links a manifest placeholder into the last used eNVM page; the rule signs the image without it (`wolfboot_envm.bin`, the ELF's loadable bytes with alignment gaps zero-filled, which is what the programmer writes), writes the resulting header into the placeholder and produces `wolfboot_envm_signed.elf`, which programs image and manifest together. `MPFS_UBL_KEY` defaults to the tree's own key, `MPFS_ENVM_VERSION` (default 1) is the manifest version, and `-DMPFS_UBL_MIN_VERSION=N` in the stub build refuses older manifests. - -**Program** (requires `SC_INSTALL_DIR` and `FPGENPROG`, see the M-mode section): -```sh -# 1. eNVM: image + manifest. This also selects boot mode 1, so the device -# boots the new eNVM contents before the stub is in place. -java -jar $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ - --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD \ - wolfboot_envm_signed.elf - -# 2. sNVM: the stub, and boot mode 2. Non-secure dies: the programmer -# itself (--bootmode 2 --snvm_page 0 --usk 0 wolfboot.elf). Secure dies -# (MPFS250TS...), which the programmer does not list but which an -# authenticated sNVM client requires: -tools/scripts/mpfs_bootmode2.sh --die MPFS250TS --workdir $PWD /path/to/ubl/wolfboot.elf -``` -The script runs the programmer in `--dryrun` for the UBL image and the User sNVM Key (USK), replays its `fpgenprog` steps for the requested die and programs. Keep the USK file it names: the sNVM pages can only be rewritten with it. - -The UBL lives in an authenticated sNVM client, which the System Controller keys from the device's sNVM Master Key (SMK) -and the client's 12-byte USK. A device whose SMK has never been initialized cannot hold such a client, and -`mpfsBootmodeProgrammer` never asks for one, so `--regenerate-smk` passes `--regenerate_smk` to the `snvm_client` step -and the generated bitstream carries a new SMK. A new SMK invalidates every authenticated or encrypted sNVM client -already on the device; wolfBoot's own keystore and wrapped image key are plaintext clients and are not SMK-keyed. - -Updating the eNVM wolfBoot under boot mode 2 goes through the same script with `--envm`, which programs the eNVM client only and leaves the boot mode and sNVM alone: -```sh -tools/scripts/mpfs_bootmode2.sh --envm --die MPFS250TS --workdir $PWD wolfboot_envm_signed.elf -``` -`mpfsBootmodeProgrammer --bootmode 1` restores boot mode 1. - -**Expected serial output:** -``` -wolfBoot UBL 2.10.0: eNVM image 0x20220100, manifest 0x2023FC00 -UBL: eNVM wolfBoot signature OK (1051 ms), entering 0x20220100 -UBL: 5 of 5 harts released -wolfBoot Version: ... -``` -A manifest that does not verify prints which check failed (`no manifest`, `digest mismatch`, `signature invalid`) and `UBL: eNVM wolfBoot verify FAILED`; the stub panics and nothing in eNVM runs. - -**Notes:** -- The stub runs at the reset clock and changes no clock, L2 way or peripheral the eNVM wolfBoot expects to find in its reset state. It holds the five MSS watchdogs in the no-reset, maximum-window state for the verify and puts the E51's back before the jump, so the eNVM wolfBoot's own watchdog snapshot is what the boot ROM left. -- The U54s wait in the stub on a DTIM mailbox (`hal/mpfs250_ubl.h`) for the verified entry, acknowledge and jump; the E51 follows last. All five harts then run the eNVM startup as in boot mode 1. -- Immutability of the stub is a Libero decision: the ROM flag on its sNVM pages and a frozen boot configuration. wolfBoot does not program either. - -### PolarFire testing - -This section describes how to build the test-application, create a custom uSD with required partitions and copying signed test-application to uSD partitions. - -1) Partition uSD card (replace /dev/sdc with your actual media, find using `lsblk`): - -Note: adjust +64M for larger OFP A/B - -```sh -sudo fdisk /dev/sdc <; - entry = <0x80200000>; - hash-1 { algo = "sha256"; }; - ... - }; -}; -``` - -Then build the FIT directly - no manual `gzip -cdvk` step: - -```sh -sudo dd if=wolfboot.bin of=/dev/sdc1 bs=512 && sudo cmp wolfboot.bin /dev/sdc1 -mkimage -f hal/mpfs250.its fitImage -``` - -At boot, wolfBoot decompresses the kernel into `0x80200000` directly out of -the FIT `data` blob. The outer wolfBoot signature covers the whole FIT -(including the compressed `data`), and gzip's CRC32 + ISIZE trailer covers -the decompressed output; the per-image `hash-1` subnodes are not re-checked -at runtime as they would duplicate the outer signature. - -##### Option B - Uncompressed FIT (`GZIP=0`) - -Build wolfBoot with `GZIP=0` and pre-decompress the kernel on the host. -Keep `compression = "none"` in `hal/mpfs250.its`: - -```sh -# Copy wolfBoot to "BIOS" partition -sudo dd if=wolfboot.bin of=/dev/sdc1 bs=512 && sudo cmp wolfboot.bin /dev/sdc1 - -# Extract GZIP compressed linux kernel to wolfboot root -gzip -cdvk ../yocto-dev-polarfire/build/tmp-glibc/work/mpfs_video_kit-oe-linux/linux-mchp/6.12.22+git/build/linux.bin > kernel.bin - -# Create custom FIT image -mkimage -f hal/mpfs250.its fitImage -FIT description: PolarFire SoC MPFS250T -Created: Tue Dec 23 11:29:02 2025 - Image 0 (kernel-1) - Description: Linux Kernel - Created: Tue Dec 23 11:29:02 2025 - Type: Kernel Image - Compression: uncompressed - Data Size: 19745280 Bytes = 19282.50 KiB = 18.83 MiB - Architecture: RISC-V - OS: Linux - Load Address: 0x80200000 - Entry Point: 0x80200000 - Hash algo: sha256 - Hash value: 800ce147fa91f367ec620936a59a1035c49971ed4b9080c96bdc547471e80487 - Image 1 (fdt-1) - Description: Flattened Device Tree blob - Created: Tue Dec 23 11:29:02 2025 - Type: Flat Device Tree - Compression: uncompressed - Data Size: 19897 Bytes = 19.43 KiB = 0.02 MiB - Architecture: RISC-V - Load Address: 0x8a000000 - Hash algo: sha256 - Hash value: 0b4efca8c0607c9a8f4f9a00ccb7691936e019f3181aab45e6d52dae91975039 - Default Configuration: 'conf1' - Configuration 0 (conf1) - Description: Linux kernel and FDT blob - Kernel: kernel-1 - FDT: fdt-1 - Hash algo: sha256 - Hash value: unavailable - -# Sign FIT image with version 1 -./tools/keytools/sign --ecc384 --sha384 fitImage wolfboot_signing_private_key.der 1 - -# Copy signed FIT image to both OFP A/B partitions -sudo dd if=fitImage_v1_signed.bin of=/dev/sdc2 bs=512 status=progress && sudo cmp fitImage_v1_signed.bin /dev/sdc2 -sudo dd if=fitImage_v1_signed.bin of=/dev/sdc3 bs=512 status=progress && sudo cmp fitImage_v1_signed.bin /dev/sdc3 - -# Copy root file system -sudo dd if=../yocto-dev-polarfire/build/tmp-glibc/deploy/images/mpfs-video-kit/mchp-base-image-sdk-mpfs-video-kit.rootfs.ext4 of=/dev/sdc4 bs=4M status=progress -``` - -### PolarFire SoC Encryption - -PolarFire SoC uses MMU mode with disk-based updates. The encryption key is stored in RAM rather than flash. - -Enable encryption in your configuration with `ENCRYPT=1` and one of: `ENCRYPT_WITH_AES256=1`, `ENCRYPT_WITH_AES128=1`, or `ENCRYPT_WITH_CHACHA=1`. - -| Algorithm | Key Size | Nonce/IV Size | -|-----------|----------|---------------| -| ChaCha20 | 32 bytes | 12 bytes | -| AES-128 | 16 bytes | 16 bytes | -| AES-256 | 32 bytes | 16 bytes | - -The `libwolfboot` API provides the following functions for managing the encryption key: - -```c -int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce); -int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce); -int wolfBoot_erase_encrypt_key(void); /* called automatically by wolfBoot_success() */ -``` - -To use your own implementation for getting the encryption key use `CUSTOM_ENCRYPT_KEY` and `OBJS_EXTRA=src/my_custom_encrypt_key.o`. -Then provide your own implementation of `int RAMFUNCTION wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce);` - -Example: - -```c -int RAMFUNCTION wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) -{ - int i; - /* Test key: "0123456789abcdef0123456789abcdef" (32 bytes for AES-256) */ - const char test_key[] = "0123456789abcdef0123456789abcdef"; - /* Test nonce: "0123456789abcdef" (16 bytes) */ - const char test_nonce[] = "0123456789abcdef"; - - for (i = 0; i < ENCRYPT_KEY_SIZE && i < (int)sizeof(test_key); i++) { - key[i] = (uint8_t)test_key[i]; - } - for (i = 0; i < ENCRYPT_NONCE_SIZE && i < (int)sizeof(test_nonce); i++) { - nonce[i] = (uint8_t)test_nonce[i]; - } - return 0; -} -``` - -To sign and encrypt an image, create a key file with the concatenated key and nonce, then use the sign tool: - -```sh -# Create key file (32-byte key + 16-byte nonce for AES-256) -printf "0123456789abcdef0123456789abcdef0123456789abcdef" > /tmp/enc_key.der - -# Sign and encrypt -./tools/keytools/sign --ecc384 --sha384 --aes256 --encrypt /tmp/enc_key.der \ - fitImage wolfboot_signing_private_key.der 1 -``` - -The result is `fitImage_v1_signed_and_encrypted.bin`, which gets placed into your OFP_A or OFP_B partitions. - -```sh -sudo dd if=fitImage_v1_signed_and_encrypted.bin of=/dev/sdc2 bs=512 status=progress && sudo cmp fitImage_v1_signed_and_encrypted.bin /dev/sdc2 -sudo dd if=fitImage_v1_signed_and_encrypted.bin of=/dev/sdc3 bs=512 status=progress && sudo cmp fitImage_v1_signed_and_encrypted.bin /dev/sdc3 -``` - -During boot, wolfBoot decrypts the image headers from disk to select the best candidate, loads and decrypts the full image to RAM, then verifies integrity and authenticity before booting. On successful boot, `wolfBoot_success()` clears the key from RAM. - -See the [Encrypted Partitions](encrypted_partitions.md) documentation for additional details. - -### PolarFire SoC with PQC (ML-DSA) - -#### Configuration - -Update your `.config` file with the following ML-DSA settings, or pass them as `make` arguments on top of `config/examples/polarfire_mpfs250_m.config` (the standalone M-mode E51 target verifies ML-DSA-87 this way, with SHA-384 as the image hash): - -```makefile -# ML-DSA 87 (Category 5) -SIGN=ML_DSA -HASH=SHA256 -ML_DSA_LEVEL=5 -IMAGE_SIGNATURE_SIZE=4627 -IMAGE_HEADER_SIZE=12288 -WOLFBOOT_SECTOR_SIZE?=0x4000 -``` - -**Important:** -- The `sign` tool requires `IMAGE_HEADER_SIZE` to be set as an environment variable, even if it's already configured in `.config`. This is because the sign tool reads the environment variable separately to determine the header size for padding. Without this, the sign tool may use a smaller default header size, causing a mismatch with wolfBoot's expected header size. -- The `WOLFBOOT_SECTOR_SIZE` must be larger than the `IMAGE_HEADER_SIZE`/ - -#### Signing and Encryption - -```sh -# Sign and Encrypt with PQ ML-DSA 5 (87) -# NOTE: IMAGE_HEADER_SIZE must match the value in .config -IMAGE_HEADER_SIZE=12288 ML_DSA_LEVEL=5 ./tools/keytools/sign --ml_dsa --sha256 --aes256 --encrypt /tmp/enc_key.der \ - fitImage wolfboot_signing_private_key.der 1 -``` - -**ML-DSA Parameter Reference:** - -| ML_DSA_LEVEL | Security Category | Signature Size | Private Key | Public Key | Recommended IMAGE_HEADER_SIZE | -|--------------|-------------------|----------------|-------------|------------|------------------------------ | -| 2 | Category 2 | 2420 | 2560 | 1312 | 8192 | -| 3 | Category 3 | 3309 | 4032 | 1952 | 8192 | -| 5 | Category 5 | 4627 | 4896 | 2592 | 12288 | - -For other ML-DSA levels, adjust `ML_DSA_LEVEL`, `IMAGE_SIGNATURE_SIZE`, and `IMAGE_HEADER_SIZE` accordingly in both `.config` and the signing command. - -### PolarFire Performance Comparison - -#### Binary Size Comparison - -The following table compares wolfBoot binary sizes for different signature algorithms on PolarFire SoC (MPFS250): - -| Algorithm | Hash | Text | Data | BSS | Total | Binary Size | -|-----------|--------|---------|------|---------|---------|-------------| -| ECC384 | SHA384 | 67.1 KB | 8 B | 3.0 KB | 70.2 KB | 68 KB | -| ML-DSA 87 | SHA256 | 63.9 KB | 0 B | 14.5 KB | 78.4 KB | 64 KB | - -#### Boot Time Comparison - -Boot time measurements on PolarFire SoC (RISC-V 64-bit U54 @ 625 MHz) for a 19MB encrypted FIT image: - -| Algorithm | Hash | Load Time | Decrypt Time | Integrity Check | Signature Verify | Total Boot Time | -|-------------|---------|-----------|--------------|-----------------|------------------|-----------------| -| ECC384 | SHA384 | ~800 ms | ~2900 ms | ~1500 ms | ~70 ms | ~5.3 seconds | -| ML-DSA 87 | SHA256 | ~835 ms | ~2900 ms | ~2100 ms | ~22 ms | ~5.9 seconds | - -Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit with host-side timestamps on the console, plaintext FIT, ECC384/SHA384, software crypto, the same 19.7 MB kernel either gzip-compressed in the FIT (5.9 MB) or stored uncompressed (19.8 MB): - -| Phase | gzip FIT | uncompressed FIT | -|-----------------------------------------------|----------|------------------| -| DDR training, SD init, GPT read, FIT load, SHA384 integrity, ECC384 verify | ~2 s | ~4 s | -| gzip inflate of the kernel | ~16 s | - | -| FIT copies, DTB fixups | <1 s | <1 s | -| M-mode -> S-mode handoff | ~18 s | ~5 s | -| Linux login prompt | ~41 s | ~28 s | - -The inflate runs on the E51 at about 1.2 MB/s of output, so a compressed kernel costs far more than the extra card read it saves; for a short boot, build the FIT with `compression = "none"` for the kernel (`FIT_KERNEL_COMP_ALG = "none"` in Yocto). The software SHA384 runs at about 11 MB/s on the E51 and the ECC384 verify is well under a second; neither is a lever. `BOOT_BENCHMARK` phase prints on this target are in real time now that the timer uses the live CPU clock. - -The LPDDR4 controller on this board needs the first CPU-side transaction after DDR initialization to be a read: when a write comes first on a cold boot, it lands in DDR displaced by several bus beats and every later cached-port write in that boot follows it. `mpfs_ddr_init()` therefore reads one block through the non-cached alias before anything is written, after which CPU, L2 write-back and SDMA writes land exactly and the SD load, the FIT copies, the DTB fixups and the image hash use DDR directly. Measured on a buffer in L2 scratch the Athena SHA384 is about 1.3x the software rate and AES-256-CTR about 4.4x. ML-DSA-87 on this target (`SIGN=ML_DSA` on `polarfire_mpfs250_m.config`) links to a smaller image than ECC384 because no big-number code is needed. - -### PolarFire Soc Debugging - -Start GDB server: - -```sh -$SC_INSTALL_DIR/openocd/bin/openocd --command "set DEVICE MPFS" --file board/microsemi-riscv.cfg -``` - -Start GDB Client: `riscv64-unknown-elf-gdb` - -``` -file wolfboot.elf -tar rem:3333 -add-symbol-file ../hart-software-services/build/hss-l2scratch.elf -set pagination off -foc c - -set $target_riscv=1 -set mem inaccessible-by-default off -set architecture riscv:rv64 -#load wolfboot.elf -#thread apply 2 set $pc=_reset -#thread apply all set $pc=_start -``` - -### PolarFire Example Boot Output - -``` -wolfBoot Version: 2.8.0 -Running on E51 (hart 0) in M-mode -Boot RESET_SR: 1FF (bit0=PERIPH bit1=MSS bit2=CPU bit3=DBG bit4=FABRIC bit5=WDOG bit6=GPIO bit7=BUS bit8=SOFT) -======================================== -DDR: Training+MTC PASS -DDR: Initialization COMPLETE -SDHCI: platform init -Reading MBR... -Found GPT PTE at sector 1 -Valid GPT partition table -Max number of partitions: 128 - GPT part 0: 0_2000000h @ 0_100000 - GPT part 1: 0_2000000h @ 0_2100000 - GPT part 2: 7_6A300000h @ 0_4100000 -Total partitions on disk0: 3 -Checking primary OS image in 0,0... -Checking secondary OS image in 0,1... -Versions, A:1 B:0 -Load block size: 512KB -Load address 0x82000000 -Attempting boot from P:A -Boot partition: 0xA03FD90 (sz 19766364, ver 0x1, type 0x601) -Loading image from disk... -done -Checking image integrity... -done -Verifying image signature... -done -Firmware Valid. -Flattened uImage Tree: Version 17, Size 19766364 -Loading Image kernel-1: 0x820000C8 -> 0x80200000 (19745280 bytes) -Loading Image fdt-1: 0x832D4BCC -> 0x8A000000 (19897 bytes) -Loading DTS: 0x8A000000 -> 0x8A000000 (19897 bytes) -Booting at 80200000 -FDT: Set chosen, bootargs=earlycon=sbi root=/dev/mmcblk0p3 rootwait uio_pdrv_genirq.of_id=generic-uio -FDT: Device serial: 219A437C-6AE1F1C2-8EDC4324-685B2288 -FDT: MAC0 = 00:04:A3:5B:22:88 -FDT: Set watchdog@20001000 status=disabled (... all five MSS WDT nodes) -M->S handoff: entry=0x80200000 hart=0 dtb=0x8A000000 -Releasing hart 1 into S-mode at 0x80200000 (dtb=0x8A000000) -[ 0.000000] Linux version 6.12.22-linux4microchip+fpga-2025.07 ... #1 SMP -[ 0.000000] Machine model: Microchip PolarFire-SoC VIDEO Kit -[ 0.000000] SBI specification v0.2 detected -[ 0.000000] SBI implementation ID=0x776f6c66 Version=0x1 -[ 0.000000] SBI TIME extension detected -[ 0.000000] SBI IPI extension detected -[ 0.000000] SBI RFENCE extension detected -[ 0.000000] riscv: providing IPIs using SBI IPI extension -[ 0.016264] smp: Bringing up secondary CPUs ... -[ 0.040738] smp: Brought up 1 node, 4 CPUs -[ 3.764474] Run /sbin/init as init process -... -mpfs-video-kit login: -``` - -The Linux SBI lines confirm the kernel is talking to wolfBoot's own SBI runtime: the implementation ID `0x776f6c66` is ASCII `"wolf"`, distinguishing it from OpenSBI (ID `0x8`). - -### PolarFire Benchmarks - -RISC-V 64-bit U54 (RV64GC1) 625 MHz - -``` -./configure --enable-riscv-asm --enable-mldsa --enable-mlkem --enable-sp=yes -make -./wolfcrypt/benchmark/benchmark ------------------------------------------------------------------------------- - wolfSSL version 5.8.4 ------------------------------------------------------------------------------- -Math: Multi-Precision: Wolf(SP) word-size=64 bits=3072 sp_int.c - Single Precision: ecc 256 rsa/dh 2048 3072 sp_c64.c - Assembly Speedups: RISCVASM ALIGN -wolfCrypt Benchmark (block bytes 1048576, min 1.0 sec each) -RNG 5 MiB took 1.225 seconds, 4.081 MiB/s -AES-128-CBC-enc 10 MiB took 1.179 seconds, 8.478 MiB/s -AES-128-CBC-dec 10 MiB took 1.164 seconds, 8.589 MiB/s -AES-192-CBC-enc 10 MiB took 1.373 seconds, 7.281 MiB/s -AES-192-CBC-dec 10 MiB took 1.360 seconds, 7.354 MiB/s -AES-256-CBC-enc 10 MiB took 1.565 seconds, 6.389 MiB/s -AES-256-CBC-dec 10 MiB took 1.550 seconds, 6.451 MiB/s -AES-128-GCM-enc 10 MiB took 1.940 seconds, 5.156 MiB/s -AES-128-GCM-dec 10 MiB took 1.938 seconds, 5.159 MiB/s -AES-192-GCM-enc 5 MiB took 1.068 seconds, 4.680 MiB/s -AES-192-GCM-dec 5 MiB took 1.066 seconds, 4.689 MiB/s -AES-256-GCM-enc 5 MiB took 1.163 seconds, 4.298 MiB/s -AES-256-GCM-dec 5 MiB took 1.163 seconds, 4.301 MiB/s -GMAC Table 4-bit 15 MiB took 1.106 seconds, 13.566 MiB/s -CHACHA 20 MiB took 1.107 seconds, 18.068 MiB/s -CHA-POLY 15 MiB took 1.058 seconds, 14.178 MiB/s -POLY1305 75 MiB took 1.036 seconds, 72.387 MiB/s -SHA 20 MiB took 1.141 seconds, 17.535 MiB/s -SHA-256 10 MiB took 1.071 seconds, 9.336 MiB/s -SHA-384 15 MiB took 1.066 seconds, 14.068 MiB/s -SHA-512 15 MiB took 1.066 seconds, 14.070 MiB/s -SHA-512/224 15 MiB took 1.067 seconds, 14.060 MiB/s -SHA-512/256 15 MiB took 1.070 seconds, 14.023 MiB/s -SHA3-224 15 MiB took 1.328 seconds, 11.292 MiB/s -SHA3-256 15 MiB took 1.398 seconds, 10.731 MiB/s -SHA3-384 10 MiB took 1.206 seconds, 8.291 MiB/s -SHA3-512 10 MiB took 1.729 seconds, 5.785 MiB/s -SHAKE128 15 MiB took 1.142 seconds, 13.135 MiB/s -SHAKE256 15 MiB took 1.402 seconds, 10.699 MiB/s -HMAC-SHA 20 MiB took 1.145 seconds, 17.470 MiB/s -HMAC-SHA256 10 MiB took 1.074 seconds, 9.310 MiB/s -HMAC-SHA384 15 MiB took 1.076 seconds, 13.944 MiB/s -HMAC-SHA512 15 MiB took 1.069 seconds, 14.036 MiB/s -PBKDF2 1 KiB took 1.023 seconds, 1.130 KiB/s -RSA 2048 public 1000 ops took 1.087 sec, avg 1.087 ms, 920.244 ops/sec -RSA 2048 private 100 ops took 5.410 sec, avg 54.100 ms, 18.484 ops/sec -DH 2048 key gen 48 ops took 1.004 sec, avg 20.920 ms, 47.801 ops/sec -DH 2048 agree 100 ops took 2.087 sec, avg 20.873 ms, 47.909 ops/sec -ECC [ SECP256R1] 256 key gen 800 ops took 1.100 sec, avg 1.375 ms, 727.248 ops/sec -ECDHE [ SECP256R1] 256 agree 300 ops took 1.041 sec, avg 3.470 ms, 288.152 ops/sec -ECDSA [ SECP256R1] 256 sign 600 ops took 1.144 sec, avg 1.907 ms, 524.370 ops/sec -ECDSA [ SECP256R1] 256 verify 300 ops took 1.173 sec, avg 3.909 ms, 255.844 ops/sec -ECC [ SECP384R1] 384 key gen 100 ops took 3.887 sec, avg 38.867 ms, 25.729 ops/sec -ECDHE [ SECP384R1] 384 agree 100 ops took 3.883 sec, avg 38.827 ms, 25.755 ops/sec -ECDSA [ SECP384R1] 384 sign 100 ops took 3.948 sec, avg 39.485 ms, 25.326 ops/sec -ECDSA [ SECP384R1] 384 verify 100 ops took 2.619 sec, avg 26.190 ms, 38.183 ops/sec -ML-KEM 512 128 key gen 2000 ops took 1.021 sec, avg 0.511 ms, 1958.111 ops/sec -ML-KEM 512 128 encap 1700 ops took 1.006 sec, avg 0.592 ms, 1690.275 ops/sec -ML-KEM 512 128 decap 1300 ops took 1.075 sec, avg 0.827 ms, 1209.214 ops/sec -ML-KEM 768 192 key gen 1200 ops took 1.035 sec, avg 0.863 ms, 1158.970 ops/sec -ML-KEM 768 192 encap 1100 ops took 1.092 sec, avg 0.993 ms, 1006.925 ops/sec -ML-KEM 768 192 decap 800 ops took 1.055 sec, avg 1.319 ms, 758.026 ops/sec -ML-KEM 1024 256 key gen 800 ops took 1.124 sec, avg 1.405 ms, 711.862 ops/sec -ML-KEM 1024 256 encap 700 ops took 1.090 sec, avg 1.557 ms, 642.343 ops/sec -ML-KEM 1024 256 decap 600 ops took 1.181 sec, avg 1.968 ms, 508.073 ops/sec -ML-DSA 44 key gen 600 ops took 1.107 sec, avg 1.844 ms, 542.217 ops/sec -ML-DSA 44 sign 200 ops took 1.144 sec, avg 5.719 ms, 174.842 ops/sec -ML-DSA 44 verify 600 ops took 1.146 sec, avg 1.910 ms, 523.569 ops/sec -ML-DSA 65 key gen 400 ops took 1.267 sec, avg 3.167 ms, 315.744 ops/sec -ML-DSA 65 sign 200 ops took 1.687 sec, avg 8.436 ms, 118.543 ops/sec -ML-DSA 65 verify 400 ops took 1.272 sec, avg 3.180 ms, 314.428 ops/sec -ML-DSA 87 key gen 200 ops took 1.066 sec, avg 5.331 ms, 187.588 ops/sec -ML-DSA 87 sign 100 ops took 1.162 sec, avg 11.617 ms, 86.084 ops/sec -ML-DSA 87 verify 200 ops took 1.077 sec, avg 5.385 ms, 185.704 ops/sec -Benchmark complete -``` - -### PolarFire SoC: PUF Root of Trust, sNVM Keystore, and PUF-Wrapped Encryption Key (M-Mode) - -This document describes the PolarFire SoC MPFS250 hardware-root-of-trust features added for the wolfBoot E51 M-mode boot flow (the same flow that brings up LPDDR4 and boots a signed Yocto FIT from SD; see `Targets.md`). They let wolfBoot anchor key material in the chip's System Controller instead of compiling it into the bootloader image: - -1. A System Controller mailbox driver for secure NVM (sNVM), the SRAM-PUF, and the nonce/TRNG service. -2. An sNVM keystore backend that serves the verification public keys from sNVM at boot. -3. A device-unique Key Encryption Key (KEK) derived from the SRAM-PUF, used to wrap/unwrap individual keys. -4. A PUF-wrapped image-encryption key: the AES image key is stored in sNVM wrapped by the PUF KEK and unwrapped at boot, with the bulk image decrypted on the M-mode disk-boot path. - -All features are off by default and gated behind build flags, so a normal build is unaffected. - -#### Trust model - -The intended chain, from the immutable hardware up: - -``` -PolarFire boot ROM (secure boot of wolfBoot: not configured on the Video Kit, bootmode 1) - -> wolfBoot (M-mode, eNVM) - -> System Controller SRAM-PUF (fixed challenge) -> SHA-384 -> 256-bit KEK - -> sNVM keystore: ECC384 verification public key(s) - -> sNVM: AES-256 image key, wrapped by the PUF KEK - -> verify + decrypt the signed/encrypted Yocto FIT - -> 4-CPU SMP Linux -``` - -The KEK is re-derived on every boot and never leaves the device. The public keys are not secret; storing them in sNVM (rather than the image) lets the trust anchor be provisioned and rotated per device, and binds verification to the System Controller. - -##### Production requirement: the keystore pages must not stay writable - -The sNVM write service is reachable from anything that can drive the System Controller mailbox, including the Linux `mpfs-sys-controller` driver, so on the development flow above the trust anchor is only as protected as the OS. A production unit closes this in one of two ways, both opt-in at build time: - -- Deliver the keystore pages as ROM in the bitstream (Libero sNVM client with the keystore image as content and the ROM attribute set) and build with `SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1`: wolfBoot reads the page admin word of every keystore module and refuses to boot unless its ROM flag is set. The ROM flag is a bitstream property; the sNVM system services can neither set nor clear it, so there is no on-device step that can lock a page by accident. The on-device provisioning writer is for development only and cannot write a ROM page. -- Build with `MPFS_SCB_SMODE_DENY=1`: every hart's PMP fences S-mode off the System Controller control and mailbox window (`0x37020000`-`0x37020FFF`) before the hand-off, and the DTB fixup disables the `mailbox@37020800`, `spi@37020100` (System Controller QSPI) and `syscontroller` nodes so the OS does not probe hardware it cannot reach. A supervisor access into the window is otherwise redirected to the kernel's trap vector as an access fault (this core cannot delegate access faults, so wolfBoot's SBI forwards them). The sNVM pages stay writable by wolfBoot (M-mode) only. This removes the OS-side System Controller services (device serial number, bitstream services, sNVM access, the zeroize procedure) and anything the OS image builds on them: on the Video Kit Yocto image the `varlog-luks-key.service` unit, which fetches its LUKS key through the System Controller, fails and systemd waits out its device timeout before the login prompt. - -#### 1. System Controller mailbox services - -`hal/mpfs250.c` drives the System Controller services mailbox (`SCBCTRL` at `0x37020000`, mailbox RAM at `0x37020800`). A small generic `mpfs_scb_request()` issues a service and polls the request/busy bits; `mpfs_scb_read()` reads the response. The following services are wrapped: - -| Service | Opcode | wolfBoot wrapper | -|---------|--------|------------------| -| Device serial number | `0x00` | `mpfs_read_serial_number()` | -| sNVM write (plaintext / authenticated / ciphertext) | `0x10` / `0x11` / `0x12` | `mpfs_snvm_write()` | -| sNVM read | `0x18` | `mpfs_snvm_read()` | -| PUF emulation | `0x20` | `mpfs_puf_emulation()` | -| Nonce (TRNG) | `0x21` | `mpfs_nonce()` | - -sNVM holds 221 modules of 252 bytes (plaintext) or 236 bytes plus a 12-byte User Secret Key (authenticated). The PUF emulation service returns a 32-byte device-unique response for a 16-byte challenge; for a fixed challenge the response is stable within a boot and across cold boots, which is what makes it usable as a KEK seed. The nonce service returns 32 random bytes. - -Note: the TRNG (nonce) and PUF services take far longer than serial/sNVM-read, so the busy-completion wait uses a larger bound (`MPFS_SCB_BUSY_TIMEOUT`) than the request-accept wait (`MPFS_SCB_TIMEOUT`). - -#### 2. sNVM public-key keystore (`SNVM_KEYSTORE`) - -`hal/mpfs250_snvm.c` + `hal/mpfs250_snvm.h` implement the five-function keystore API (`keystore_num_pubkeys`, `keystore_get_buffer`, `keystore_get_size`, `keystore_get_mask`, `keystore_get_key_type`) by reading the trust anchor from sNVM. The on-sNVM layout is the same family as the OTP keystore (`src/flash_otp_keystore.c`): a `WOLFBOOT`-magic header followed by packed `struct keystore_slot` entries, read through a linear reader that spans consecutive sNVM modules from `SNVM_KEYSTORE_MODULE` (default 200, `SNVM_KEYSTORE_MAX_MODULES` default 1; an ML-DSA-87 key needs 11). For the on-device provisioning writer every module in the range must be left runtime-writable (not ROM) in the Libero sNVM configuration: on the stock Video Kit design only modules 200 and 201 are. A production bitstream delivers the same page contents as ROM instead (see the trust model above). - -Build wiring (mirrors `FLASH_OTP_KEYSTORE`): - -- `SNVM_KEYSTORE=1` links `hal/mpfs250_snvm.o` in place of the compiled-in `src/keystore.o`, so the running bootloader contains no public keys and serves them from sNVM. -- `SNVM_KEYSTORE_PROVISION=1` keeps the compiled keystore and adds `snvm_keystore_provision()`, a one-time on-device writer that copies the compiled keystore image into sNVM (called from `hal_init`). Run once on a board, then deploy the `SNVM_KEYSTORE=1` build. Both provisioning writers read the page first and skip it when it already holds the content (`snvm: module N already provisioned`), so the extra boot the programmer triggers after flashing, or a second boot of the provisioning image, does not spend another write cycle. - -A keystore page that reads back as all zeros is reported as `sNVM keystore zeroized` and halts the boot. That is the state a zeroize procedure leaves behind (the OS overwrites the keystore and encryption-key pages through the System Controller's plaintext sNVM write service), and it is distinguished from a corrupt header, which merely yields no keys, so a deliberately erased unit is identifiable on the console. Recovery is the provisioning build again. - -#### 3. PUF KEK and AES key-wrap (`SNVM_KEK`) - -The KEK and key-wrap live in `hal/mpfs250_snvm.c` as well: - -- `mpfs_puf_kek()` derives the 256-bit KEK as `SHA-384(label || PUF(fixed-challenge))` truncated to 32 bytes. SHA-384 is used because it is the build's hash (SHA-256/HMAC/HKDF are compiled out); the PUF response is already a high-entropy device-unique secret, so a labelled hash provides domain separation without pulling in HKDF. -- `snvm_kek_wrap()` / `snvm_kek_unwrap()` wrap/unwrap a key with the KEK using RFC 3394 AES key-wrap (`wc_AesKeyWrap` / `wc_AesKeyUnWrap`). - -`SNVM_KEK=1` adds RFC 3394 key-wrap to the AES that an `ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1` build already compiles; the KEK has no other consumer, so the build refuses the flag without that combination. Determinism and the wrap/unwrap round trip are covered by the host unit test `tools/unit-tests/unit-snvm-kek.c`. - -The AES key-wrap uses the wolfCrypt software implementation. The Athena F5200 offload (`MPFS_ATHENA=1`, see `Targets.md`) serves SHA-384 and the bulk AES-256-CTR image decrypt through wolfCrypt crypto callbacks; the key-wrap is a few blocks per boot and stays in software. - -#### 4. PUF-wrapped image-encryption key - -`hal/mpfs250_snvm.c` provides `wolfBoot_get_encrypt_key()` (under `CUSTOM_ENCRYPT_KEY`): it reads `[wrapped AES key (40 B)][nonce (16 B)]` from sNVM module `SNVM_ENCKEY_MODULE` (default 201, outside the keystore range), unwraps the key with the PUF KEK, and returns the plaintext key + nonce to the decrypt path. `snvm_enckey_provision()` (under `SNVM_ENCKEY_PROVISION`) wraps a key with the device KEK and writes the blob to sNVM. - -##### Disk-image decryption enablement - -wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partition model and required `EXT_FLASH` or `MMU`. The M-mode disk-FIT path uses `WOLFBOOT_NO_PARTITIONS` and is neither. Two small, target-independent changes enable encrypted disk boot generally (regression-safe; host unit tests pass): - -- `src/libwolfboot.c`: the `EXT_ENCRYPTED requires EXT_FLASH or MMU` guard now also accepts `CUSTOM_ENCRYPT_KEY` (the platform supplies the key), and `hal_set_key()` (partition-resident key storage) is folded under `#ifndef CUSTOM_ENCRYPT_KEY`. -- `src/update_disk.c`: the `DISK_ENCRYPT` path gains `ForceZero` for secret cleanup. - -#### Field-recoverable zeroize - -`DISK_FAILOVER_ZEROIZE=1` (with `DISK_GOLDEN_SLOT=1`, `SNVM_KEK=1` and the `WOLFBOOT_SNVM_WRITE_APPROVED=1` gate every sNVM writer carries) turns a failover to the golden slot into a zeroize: when both operational slots are given up on, wolfBoot zeroes sNVM page `SNVM_ENCKEY_MODULE` (the PUF-wrapped image key, read back and checked; a page that already reads as zeros is not written again) and the head of both operational slots, then boots the golden image. The keystore page is never touched, so the golden image still verifies; it must be a plaintext signed image, since the key that decrypted the operational images is gone. Nothing derived from the PUF is lost: the MRAM log key is re-derived on every boot. Re-provisioning the image key afterwards is the `SNVM_ENCKEY_PROVISION` service flow; a brownout during the zeroize write can leave the page read-only, in which case the device cannot be re-keyed. - -#### Build flags summary - -| Flag | Effect | -|------|--------| -| `SNVM_KEYSTORE` | Serve the public-key trust anchor from sNVM (replaces compiled keystore) | -| `SNVM_KEYSTORE_PROVISION` | Add the one-time on-device keystore writer | -| `SNVM_KEK` | Enable the PUF KEK + RFC 3394 AES key-wrap (needs the AES-256 `ENCRYPT` + `CUSTOM_ENCRYPT_KEY` build) | -| `SNVM_ENCKEY_PROVISION` | Add the one-time PUF-wrapped encryption-key writer | -| `SNVM_KEYSTORE_MODULE` / `SNVM_KEYSTORE_MAX_MODULES` / `SNVM_ENCKEY_MODULE` | sNVM module numbers (defaults 200 / 1 / 201; the key module must lie outside the keystore range and be runtime-writable) | -| `WOLFBOOT_SNVM_WRITE_APPROVED` | Required by every knob that writes sNVM; the build fails without it | -| `SNVM_KEYSTORE_REQUIRE_ROM` | Refuse to boot unless every keystore page is ROM-flagged (bitstream-provisioned keystore) | -| `MPFS_SCB_SMODE_DENY` | PMP-fence S-mode off the System Controller mailbox and disable its DTB nodes | -| `DISK_FAILOVER_ZEROIZE` | Zero the image-key page and the operational slots when falling back to the golden image | -| `SNVM_ENCKEY_INSECURE_TEST_KEY` | Acknowledges provisioning the built-in public test key | -| `SNVM_ENCKEY_PROVISION_EXTERN` | Provision a production key instead: the integrator defines `snvm_enckey_prov_key[32]` and `snvm_enckey_prov_nonce[16]` in another object | - -Example runtime encrypted build (compiled keystore for verification, PUF-wrapped key for decryption): - -```sh -make CROSS_COMPILE=riscv64-unknown-elf- LIBERO_FPGA_CONFIG_DIR= \ - ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1 SNVM_KEK=1 wolfboot.elf -``` - -#### Provisioning and validation recipe - -1. Build with `SNVM_ENCKEY_PROVISION=1` (and/or `SNVM_KEYSTORE_PROVISION=1`) plus `WOLFBOOT_SNVM_WRITE_APPROVED=1` added to the runtime build above, flash, and boot once: `hal_init` writes the PUF-wrapped AES key into sNVM[201] (and/or the keystore into sNVM[200]). -2. Sign + encrypt the FIT with a key file matching the provisioned key (`prov_aes_key` || `prov_aes_nonce` in `hal/mpfs250_snvm.c`, i.e. a 32-byte key followed by a 16-byte IV): - - ```sh - IMAGE_HEADER_SIZE=512 ./tools/keytools/sign --ecc384 --sha384 \ - --encrypt enc_key.bin --aes256 fitImage wolfboot_signing_private_key.der 1 - ``` - Note: `make keysclean` regenerates the signing key, so re-sign the image after any keysclean or the compiled keystore will not match. -3. Write `fitImage_v1_signed_and_encrypted.bin` to the SD boot partition. With the stock HSS, boot to the `>>` CLI, run `usbdmsc`, and `dd` to the first partition of the exposed disk. Do not move an SD-Wire mux while `usbdmsc` is running: HSS's MMC layer then serves one stale block for every LBA and does not recover until `usbdmsc` is restarted, which makes a populated card read as blank. -4. Flash the runtime build and cold-boot. Expected UART0: `Disk encryption enabled` -> `Decrypting image... done` -> `Firmware Valid.` -> `M->S handoff`, and UART1 reaches the Linux login with `Brought up 1 node, 4 CPUs` and no `failed to come online`. - -#### Sample validation logs (MPFS250TS Video Kit) - -The PUF response is identical for a fixed challenge across cold power cycles while the nonce service is random, and the PUF-derived KEK wraps and unwraps a test key deterministically (the same wrapped bytes on every boot); both were verified on the kit and the KEK logic is covered by `tools/unit-tests/unit-snvm-kek.c`. - -Full encrypted boot captured on the Video Kit (UART1, S-mode wolfBoot under HSS; the standalone M-mode target decrypts the same image in place in DDR): the AES key is unwrapped from sNVM with the PUF KEK, the image is loaded, decrypted and verified. - -``` -wolfBoot Version: 2.9.0 -Disk encryption enabled -... -Load address 0x8E000000 -Attempting boot from P:A -Loading image from disk...done -Decrypting image...done -Checking image integrity...done -Verifying image signature...done -Firmware Valid. -Booting at 8E000000 -PolarFire SoC MPFS250 wolfBoot demo Application -``` - -Linux (UART1): - -``` -[ 0.040726] smp: Brought up 1 node, 4 CPUs -OpenEmbedded nodistro.0 mpfs-video-kit ttyS1 -mpfs-video-kit login: -``` - -No `failed to come online`; the only error lines are the known-cosmetic eth-SGMII PHY, i2c clock-divider, and mmc-tuning messages unrelated to this feature. - -### PolarFire SoC: wolfBoot SBI vendor extension - -The M-mode wolfBoot stays resident as the SBI for the OS. With `MPFS_SBI_VENDOR=1` it also serves the Microchip vendor extension, EID `0x09000029` (`SBI_EXT_VENDOR_START | mvendorid 0x029`), the same number the Hart Software Services register, so an OS probes one extension for either firmware and tells them apart with `GET_VERSION`. The HSS function IDs for inter-hart communication, remoteproc, crypto and IPC are answered with `SBI_ERR_NOT_SUPPORTED`; `HSS_REBOOT` (0x10) performs a cold MSS reset. - -The definitions live in `include/riscv_sbi.h`; the handler is `sbi_hal_vendor_ecall()` in `hal/mpfs250.c`. Arguments travel in `a0..a2`, the SBI error returns in `a0` and the value in `a1`. Buffers are addresses in the caller's address space: M-mode reads and writes them through the caller's translation, byte by byte, with a trap vector that turns a bad address into `SBI_ERR_INVALID_ADDRESS` instead of a stopped hart. A buffer is at most 4096 bytes and, while the caller runs with paging off, must lie in DDR. - -| FID | Call | Arguments | Result | -|-----|------|-----------|--------| -| 0x40 | `GET_VERSION` | - | `a1` = 1 | -| 0x41 | `GET_SERIAL` | `a0` buffer, `a1` length >= 16 | the 16-byte device serial number | -| 0x42 | `GET_KEY` | `a0` purpose, `a1` buffer, `a2` length == 32 | a PUF-derived key; each purpose is served once per boot, a second call returns `SBI_ERR_DENIED` | -| 0x43 | `DIGEST_CHECK` | `a0` options word | starts the System Controller digest check | -| 0x44 | `IAP_AUTHENTICATE` | `a0` SPI directory index | starts IAP image authentication | -| 0x45 | `IAP_VERIFY` | `a0` SPI directory index | starts IAP verify | -| 0x46 | `IAP_PROGRAM` | `a0` SPI directory index | the device restarts on success; a return is the failure status in `a1` | -| 0x47 | `AUTO_UPDATE` | - | the device restarts on success; a return is the failure status in `a1` | -| 0x48 | `SC_FLASH_ERASE` | `a0` flash offset, `a1` length == one erase block (64 KB), both block aligned | erases that block of the System Controller's SPI flash; `MPFS_SBI_SC_FLASH=1` builds only | -| 0x49 | `SC_FLASH_WRITE` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | programs the bytes; `MPFS_SBI_SC_FLASH=1` builds only | -| 0x4A | `SC_FLASH_READ` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | reads the bytes (any offset, the directory included); `MPFS_SBI_SC_FLASH=1` builds only | -| 0x4B | `SC_FLASH_SET_IMAGE` | `a0` directory index 1 (upgrade) or 2 (blank), `a1` image offset | rewrites the SPI directory block with that entry changed (entry 1 also zeroes entry 2); `MPFS_SBI_SC_FLASH=1` builds only | -| 0x4F | `SC_STATUS` | - | `SBI_ERR_ALREADY_STARTED` while the started service runs, else `a1` = 16-bit status with mailbox word 1 (the digest-check `DIGESTERR` bits) in the upper half | - -`GET_KEY` purposes name a fixed label; the caller never supplies a challenge, so the OS cannot ask the PUF for wolfBoot's own key-encryption key. Purpose 1 is the MRAM log key (`SHA-384("wolfBoot-mram-log-v1" || PUF(SHA-384("wolfBoot-mram-log-v1")[0..15]))[0..31]`); purpose 2 reproduces the earlier user-space `varlog` derivation. The key is device-bound: the same whether the golden or an operational image booted. - -The System Controller services (0x43-0x4F) are served only in a build with `MPFS_SCB_SMODE_DENY=1`, where the OS cannot reach the mailbox itself; in an unfenced build they return `SBI_ERR_DENIED` and the OS uses its own mailbox drivers, which the fence would otherwise race. The long-running ones are started inside the ecall and polled through `SC_STATUS`, because an ecall holds its hart in M-mode with interrupts off. The mailbox is one resource for all harts: a call that finds it taken returns `SBI_ERR_FAILED`. - -The fence also covers the System Controller's QSPI flash controller, so a fenced OS cannot stage a bitstream the way the mainline `mpfs-auto-update` driver does (through `mtd` on that controller). `MPFS_SBI_SC_FLASH=1` adds the flash calls (0x48-0x4B) for that build: the OS erases and writes the image region directly, reads back what it wants verified, and points the SPI directory's upgrade entry at the image with `SC_FLASH_SET_IMAGE` before `IAP_AUTHENTICATE` / `IAP_PROGRAM` or `AUTO_UPDATE`. The first erase block (the directory and the start of the design-info area) is never erased or written through `SC_FLASH_ERASE` / `SC_FLASH_WRITE`; `SET_IMAGE` reads it, changes the one entry (the golden entry 0 stays what it was) and writes it back, the same read-erase-write the OS driver performs. Each erase call takes one block and each transfer at most 4 KB, so no ecall holds its hart for more than the erase time of one block; the OS side loops. The flash layout the mainline driver uses (directory in the first 1 KB, design info at 1 KB, bitstream slots from 1 MB + 1 KB) is the OS's business; wolfBoot only protects the directory block. - -The Linux side is the `wolfboot-sbi` module in `tools/linux/wolfboot-sbi/`: `/dev/wolfboot` (root only) with one ioctl per call, see `wolfboot_sbi.h`. It probes the extension and `GET_VERSION`, so it unloads cleanly under the HSS, and it never touches the mailbox registers, so the mainline `mpfs-sys-controller`, `mpfs-auto-update` and `mpfs-rng` drivers keep working in an unfenced build. A caller of `IAP_PROGRAM` or `AUTO_UPDATE` syncs its filesystems first: the device restarts inside the call. +> **Filesystem boot:** this target can also read its signed image from a **file** on a read-only FAT32 or ext4 partition instead of from raw offset 0. Build with `DISK_FS=fat32|ext4|both` and set `BOOT_FILE_A` / `BOOT_FILE_B`. See [Disk boot from a read-only filesystem (FAT32 / ext4)](compile.md#disk-boot-from-a-read-only-filesystem-fat32--ext4). -Build: `MPFS_SBI_VENDOR=1` on the M-mode target (`RISCV_MMODE=1`); the key derivation is SHA-384 whatever the image hash. ## STM32F7 diff --git a/docs/encrypted_partitions.md b/docs/encrypted_partitions.md index 3c36883f5d..59b6c85e9f 100644 --- a/docs/encrypted_partitions.md +++ b/docs/encrypted_partitions.md @@ -42,7 +42,7 @@ You can use the `CUSTOM_ENCRYPT_KEY` option to implement your own functions for: `wolfBoot_get_encrypt_key`, `wolfBoot_set_encrypt_key` and `wolfBoot_erase_encrypt_key`. -For an example that stores the AES key in non-volatile memory wrapped by a device-unique hardware PUF key (Microchip PolarFire SoC), see [polarfire_snvm_puf.md](polarfire_snvm_puf.md). Note that disk/`NO_PARTITIONS` boot can use `CUSTOM_ENCRYPT_KEY` for image encryption without `EXT_FLASH` or `MMU`. +For an example that stores the AES key in non-volatile memory wrapped by a device-unique hardware PUF key (Microchip PolarFire SoC), see [PolarFire.md](PolarFire.md#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). Note that disk/`NO_PARTITIONS` boot can use `CUSTOM_ENCRYPT_KEY` for image encryption without `EXT_FLASH` or `MMU`. To enable: diff --git a/docs/keystore.md b/docs/keystore.md index d010562060..858b3be8e4 100644 --- a/docs/keystore.md +++ b/docs/keystore.md @@ -224,7 +224,7 @@ To support this mode of operation, the `keygen` tool supports the `--nolocalkeys ### Using KeyStore in secure NVM (PolarFire SoC sNVM) -On Microchip PolarFire SoC the trust anchor can be served from the System Controller secure NVM (sNVM) instead of being compiled into the bootloader, using the `SNVM_KEYSTORE` backend. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). +On Microchip PolarFire SoC the trust anchor can be served from the System Controller secure NVM (sNVM) instead of being compiled into the bootloader, using the `SNVM_KEYSTORE` backend. See [PolarFire.md](PolarFire.md#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). ## Build System Integration diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 26aa15fecd..549ad48871 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -1797,7 +1797,7 @@ int mpfs_nonce(uint8_t *nonce) * there keys off build-supplied macros only, not this fallback. */ #ifndef LINUX_BOOTARGS #ifndef LINUX_BOOTARGS_ROOT -/* wolfBoot SD layout (tools/scripts/program-sdcard.sh): p1=boot FIT, +/* wolfBoot SD layout (docs/PolarFire.md): p1=boot FIT, * p2=update, p3=rootfs. */ #define LINUX_BOOTARGS_ROOT "/dev/mmcblk0p3" #endif From 735bb385b71e314a614b5a301d63e0ede74e79ab Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 17 Oct 2026 10:43:29 -0700 Subject: [PATCH 11/23] PolarFire SoC: read-only sNVM page survey behind MPFS_SNVM_DUMP --- Makefile | 3 +++ docs/PolarFire.md | 3 +++ hal/mpfs250.c | 13 ++++++++++- hal/mpfs250_snvm.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++ hal/mpfs250_snvm.h | 6 ++++++ options.mk | 16 ++++++++++++++ 6 files changed, 94 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 80830b091f..69b174a7d7 100644 --- a/Makefile +++ b/Makefile @@ -206,6 +206,9 @@ endif ifeq ($(MPFS_SBI_VENDOR),1) MPFS_SNVM_OBJ=1 endif +ifeq ($(MPFS_SNVM_DUMP),1) + MPFS_SNVM_OBJ=1 +endif ifeq ($(MPFS_SNVM_OBJ),1) OBJS+=./hal/mpfs250_snvm.o endif diff --git a/docs/PolarFire.md b/docs/PolarFire.md index 8012c019d4..91c6696594 100644 --- a/docs/PolarFire.md +++ b/docs/PolarFire.md @@ -290,6 +290,7 @@ These flags apply to `polarfire_mpfs250_m_qspi.config` and are added via `CFLAGS | `WATCHDOG_TIMEOUT_MS` | `30000` (30 s) | Watchdog timeout in milliseconds when `WATCHDOG` is defined. ECDSA P-384 verification on E51 with portable C math is bounded at ~5 s; the default 30 s avoids any need to refresh the WDT during the long verify call. | | `MPFS_WDT_NODES_KEEP` (make variable, `=1`) | undefined | Linux boot only: keep the five MSS watchdog nodes enabled in the dtb and stop the parked E51 from refreshing them, so the OS owns the watchdogs. See "Watchdog policy" below. | | `MPFS_QSPI_CLK_DIV` (make variable) | `QSPI_CLK_DIV_8` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h`. See "PolarFire SoC QSPI". | +| `MPFS_SNVM_DUMP` (make variable, `=1`) | undefined | Read-only: at boot, print the admin word, state and a checksum of `MPFS_SNVM_DUMP_COUNT` sNVM pages from `MPFS_SNVM_DUMP_FIRST` (defaults 2 from 200, the keystore and the wrapped image key). Page contents are never printed. M-mode only. | ### Stack overflow detection @@ -1289,6 +1290,8 @@ The KEK is re-derived on every boot and never leaves the device. The public keys The sNVM write service is reachable from anything that can drive the System Controller mailbox, including the Linux `mpfs-sys-controller` driver, so on the development flow above the trust anchor is only as protected as the OS. A production unit closes this in one of two ways, both opt-in at build time: +`MPFS_SNVM_DUMP=1` surveys a page range read-only at boot and prints each page's admin word (whose bit 23 is the ROM flag), whether the page is blank, zeroized or holds data, and a checksum, so a device's sNVM state can be recorded before and after provisioning or a bitstream change without printing key material. A page that is not a plaintext client refuses the read and reports the System Controller status instead, with its admin word: status 2 is an authentication failure, and an admin word of zero there means the page was never configured as a client at all, which is what distinguishes free pages from occupied ones when planning an sNVM layout. + - Deliver the keystore pages as ROM in the bitstream (Libero sNVM client with the keystore image as content and the ROM attribute set) and build with `SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1`: wolfBoot reads the page admin word of every keystore module and refuses to boot unless its ROM flag is set. The ROM flag is a bitstream property; the sNVM system services can neither set nor clear it, so there is no on-device step that can lock a page by accident. The on-device provisioning writer is for development only and cannot write a ROM page. - Build with `MPFS_SCB_SMODE_DENY=1`: every hart's PMP fences S-mode off the System Controller control and mailbox window (`0x37020000`-`0x37020FFF`) before the hand-off, and the DTB fixup disables the `mailbox@37020800`, `spi@37020100` (System Controller QSPI) and `syscontroller` nodes so the OS does not probe hardware it cannot reach. A supervisor access into the window is otherwise redirected to the kernel's trap vector as an access fault (this core cannot delegate access faults, so wolfBoot's SBI forwards them). The sNVM pages stay writable by wolfBoot (M-mode) only. This removes the OS-side System Controller services (device serial number, bitstream services, sNVM access, the zeroize procedure) and anything the OS image builds on them, so any unit that reads a key or the serial number through the mailbox must be dropped from the image or moved to the [SBI vendor extension](#sbi-vendor-extension-for-the-os). diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 549ad48871..bbb5575a10 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -46,7 +46,7 @@ #include "gpt.h" #include "fdt.h" #if defined(SNVM_KEYSTORE_PROVISION) || defined(SNVM_ENCKEY_PROVISION) || \ - defined(MPFS_SBI_VENDOR) + defined(MPFS_SBI_VENDOR) || defined(MPFS_SNVM_DUMP) #include "mpfs250_snvm.h" #endif #if defined(MPFS_SBI_VENDOR) && defined(WOLFBOOT_MMODE_SMODE_BOOT) @@ -1090,6 +1090,10 @@ void hal_init(void) #endif +#ifdef MPFS_SNVM_DUMP + mpfs_snvm_dump(MPFS_SNVM_DUMP_FIRST, MPFS_SNVM_DUMP_COUNT); +#endif + #ifdef SNVM_KEYSTORE_PROVISION /* One-time: write the compiled-in trust anchor into sNVM so a subsequent * SNVM_KEYSTORE build serves its keys from sNVM. Stop on failure so a @@ -1699,6 +1703,13 @@ int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, ret = mpfs_scb_request(SYS_SERV_CMD_SNVM_READ, frame, sizeof(frame)); if (ret != 0) { + /* The service still returns the page admin word on a refusal, and + * it is the only way to tell a page held as a client from one that + * was never configured: the latter reads back as zero. */ + if (admin != NULL) { + mpfs_scb_read(admin, MPFS_SNVM_READ_RET_OFFSET, + MPFS_SNVM_ADMIN_LEN); + } return ret; } diff --git a/hal/mpfs250_snvm.c b/hal/mpfs250_snvm.c index d27a0b82d0..d758df1f00 100644 --- a/hal/mpfs250_snvm.c +++ b/hal/mpfs250_snvm.c @@ -562,3 +562,57 @@ int snvm_enckey_provision(void) #endif /* SNVM_ENCKEY_PROVISION */ #endif /* CUSTOM_ENCRYPT_KEY && SNVM_KEK */ + +#ifdef MPFS_SNVM_DUMP +/* Read-only survey of a range of sNVM pages, for checking what a device + * actually holds before and after a provisioning or bitstream change. The + * page contents are never printed: one of these pages holds the wrapped + * image key, so only the admin word (ROM flag and page state), a + * classification and a checksum go to the console. The checksum is for + * telling two reads apart, not a security primitive. */ +void mpfs_snvm_dump(uint8_t first, uint8_t count) +{ + static uint8_t page[MPFS_SNVM_PLAIN_DATA_LEN]; + uint8_t admin[MPFS_SNVM_ADMIN_LEN]; + uint32_t sum; + unsigned int i, j, blank, zero; + uint8_t m; + int ret; + + for (i = 0; i < count; i++) { + if ((unsigned int)first + i >= MPFS_SNVM_MODULE_MAX) { + break; + } + m = (uint8_t)(first + i); + ret = mpfs_snvm_read(m, NULL, admin, page, sizeof(page)); + if (ret != 0) { + /* A refusal is not the same as a page that is free: a page + * held as an authenticated or encrypted client refuses a + * plaintext read, and so does one that was never configured. + * The admin word separates them - an unconfigured page reads + * back zero, a configured one carries its flags. */ + wolfBoot_printf("snvm %u: read failed, status %d, admin " + "%02x%02x%02x%02x\n", (unsigned)m, ret, + admin[3], admin[2], admin[1], admin[0]); + continue; + } + /* FNV-1a over the page, so two reads of the same page compare. */ + sum = 0x811C9DC5u; + blank = 1; + zero = 1; + for (j = 0; j < sizeof(page); j++) { + sum = (sum ^ page[j]) * 0x01000193u; + if (page[j] != 0xFFU) { + blank = 0; + } + if (page[j] != 0x00U) { + zero = 0; + } + } + wolfBoot_printf("snvm %u: admin %02x%02x%02x%02x rom=%u %s sum %08x\n", + (unsigned)m, admin[3], admin[2], admin[1], admin[0], + (unsigned)((admin[2] & 0x80U) != 0U), + zero ? "zeroized" : (blank ? "blank" : "data"), (unsigned)sum); + } +} +#endif /* MPFS_SNVM_DUMP */ diff --git a/hal/mpfs250_snvm.h b/hal/mpfs250_snvm.h index 7b173801fa..32c335c24b 100644 --- a/hal/mpfs250_snvm.h +++ b/hal/mpfs250_snvm.h @@ -147,6 +147,12 @@ int mpfs_puf_derive(const char *label, const uint8_t *challenge, uint8_t *out); int mpfs_puf_purpose_key(const char *seed, const char *label, uint8_t *out); #endif +#ifdef MPFS_SNVM_DUMP +/* Read-only: print the admin word, state and a checksum of `count` pages from + * `first`. Page contents are never printed. */ +void mpfs_snvm_dump(uint8_t first, uint8_t count); +#endif + #if defined(SNVM_ENCKEY_PROVISION) && !defined(SNVM_KEK) #error "SNVM_ENCKEY_PROVISION needs SNVM_KEK (the key is wrapped with the PUF KEK)" #endif diff --git a/options.mk b/options.mk index c7a278edeb..6238b722f1 100644 --- a/options.mk +++ b/options.mk @@ -171,6 +171,22 @@ ifneq ($(MPFS_QSPI_CLK_DIV),) CFLAGS+=-DMPFS_QSPI_CLK_DIV=$(MPFS_QSPI_CLK_DIV) endif +# Read-only survey of a range of sNVM pages at boot: each page's admin word (ROM flag), state and +# checksum, never the contents, since one of the default range's pages holds the wrapped image key. +MPFS_SNVM_DUMP_FIRST?=200 +MPFS_SNVM_DUMP_COUNT?=2 +ifeq ($(MPFS_SNVM_DUMP),1) + ifeq ($(WOLFBOOT_TARGET_BUILD),1) + ifneq ($(RISCV_MMODE),1) + $(error MPFS_SNVM_DUMP=1 needs the M-mode target (RISCV_MMODE=1): \ + only M-mode reaches the System Controller mailbox) + endif + endif + CFLAGS+=-D"MPFS_SNVM_DUMP" \ + -DMPFS_SNVM_DUMP_FIRST=$(MPFS_SNVM_DUMP_FIRST) \ + -DMPFS_SNVM_DUMP_COUNT=$(MPFS_SNVM_DUMP_COUNT) +endif + # Leave the MSS watchdog nodes in the dtb and stop the parked E51 refreshing them, so the OS owns # the watchdogs after the hand-off and must service them itself. ifeq ($(MPFS_WDT_NODES_KEEP),1) From d51ae519833bf5b202efbf6fa5bdb2245b24c2bd Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 17 Oct 2026 09:22:11 -0700 Subject: [PATCH 12/23] PolarFire SoC: factor out repeated HAL code and drop unused definitions --- hal/mpfs250.c | 604 +++++++++++++++++++--------------------------- hal/mpfs250.h | 24 +- hal/mpfs250_ddr.c | 8 +- src/riscv_sbi.c | 21 +- src/update_disk.c | 9 +- 5 files changed, 272 insertions(+), 394 deletions(-) diff --git a/hal/mpfs250.c b/hal/mpfs250.c index bbb5575a10..2aca5e2ed8 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -109,6 +109,13 @@ const uint8_t __attribute__((section(".envm_manifest"), used)) static uint32_t mpfs_wdt_default_mvrp = 0; static uint32_t mpfs_wdt_default_ctrl = 0; +#ifdef WOLFBOOT_RISCV_MMODE +static const unsigned long mpfs_wdt_bases[MPFS_NUM_HARTS] = { + MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, + MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE +}; +#endif + /* Snapshots captured at hal_init entry, printed after uart_init; * RESET_SR shows the cause of the most-recent reset. */ static uint32_t mpfs_boot_wdt_snap[6]; @@ -206,10 +213,9 @@ static void mpfs_signal_main_hart_started(void) * or by the SBI HSM hart_start backend (on the calling U54), and consumed * by the target hart's park loop in secondary_hart_entry(). * - * These live in the E51 DTIM, NOT in L2-scratch BSS: cacheable stores to - * the scratchpad can be lost on cache-line eviction, so cross-hart - * mailboxes written by a U54 would silently vanish (observed: the SBI - * HSM hart-state writes never became visible). The DTIM is small, + * They live in the E51 DTIM, not in L2-scratch BSS: a cacheable store to + * the scratchpad can be lost on cache-line eviction, so a cross-hart + * mailbox written by a U54 would never become visible. The DTIM is * uncached and coherent for every hart. The SBI shared state occupies * DTIM+0x000 (see src/riscv_sbi.c); the mailboxes sit at +0x100. */ typedef struct { @@ -232,32 +238,26 @@ extern void riscv_mmode_to_smode(unsigned long entry, unsigned long hartid, * hand-off context has been staged for us) or park in WFI waiting for an * SBI/Linux IPI. * - * Keep this path FAST and free of UART access: the secondaries reach it - * via the kernel's HSM hart_start IPI, inside the kernel's 1s online - * window. A per-hart UART banner here once spent multiple seconds - * spinning on LSR (layout-dependent), making harts miss that window. */ + * This path must stay free of UART access: the secondaries reach it via + * the kernel's HSM hart_start IPI and have to respond inside the kernel's + * 1 s online window, which spinning on LSR can overrun. */ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls) { (void)hls; while (1) { #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) - /* Check the hand-off context BEFORE sleeping: the release IPI was - * already consumed (MSIP cleared) by the eNVM wake path, so a - * wfi-first loop sleeps through an already-staged hand-off. The - * old wfi-first order only appeared to work because mtimecmp's - * reset value of 0 left MTIP permanently pending, making the wfi - * fall through; parking the comparators exposed it. */ + /* Check the hand-off context before sleeping: the eNVM wake path + * has already consumed the release IPI (MSIP cleared), so a + * wfi-first loop would sleep through a staged hand-off. */ if (hartid < (unsigned long)MPFS_NUM_HARTS && mpfs_kernel_handoff[hartid].marker == MPFS_KERNEL_HANDOFF_MARKER) { unsigned long kentry; unsigned long opq; - /* Acquire fence: pair with the writer's release fence so we - * are guaranteed to observe entry / opaque after seeing - * marker. Without this, RISC-V's relaxed memory model - * permits the reader to use stale field values cached before - * marker was published. */ + /* Acquire fence, paired with the writer's release fence: + * without it RVWMO permits stale entry / opaque values read + * before marker was published. */ __asm__ volatile("fence r,rw" ::: "memory"); kentry = (unsigned long)mpfs_kernel_handoff[hartid].entry; opq = (unsigned long)mpfs_kernel_handoff[hartid].opaque; @@ -323,10 +323,6 @@ void __attribute__((noreturn)) hal_smode_boot(unsigned long entry, unsigned long hartid, unsigned long dtb) { #ifndef MPFS_WDT_NODES_KEEP - static const unsigned long park_wdt_bases[5] = { - MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, - MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE - }; unsigned int w; #endif @@ -364,24 +360,21 @@ hal_smode_boot(unsigned long entry, unsigned long hartid, unsigned long dtb) CLINT_MSIP_REG(MPFS_FIRST_U54_HART) = 0x01; __asm__ volatile("fence iorw, iorw" ::: "memory"); - /* Park the E51 as the platform monitor (HSS's watchdog-service role): - * doze on the machine timer and pet all watchdogs every few seconds. - * The MSS watchdogs always count and reset the chip on timeout - * (~28 s at the boot-time settings), so the parked E51 must keep - * them refreshed while the OS boots and runs. + /* Park the E51 as the platform monitor, taking over HSS's + * watchdog-service role: doze on the machine timer and refresh all five + * MSS watchdogs every cycle. They count and reset the chip on timeout + * (~28 s at the boot-time settings) even with CONTROL=0, so something + * has to refresh them while the OS boots and runs; the dtb fixup + * disables the OS watchdog driver so it cannot also claim them, and + * with MVRP at maximum a refresh is always permitted. * - * Wake only on the machine timer; mstatus.MIE stays clear so the - * pending timer wakes WFI without vectoring into the trap path. - * Pet ALL watchdogs (HSS's WDog-service role): the MSS WDTs always - * count and RESET the chip on timeout even with CONTROL=0, and the - * OS watchdog driver is disabled in the dtb fixup (when it owned - * them, its refresh-forbidden window made our blind refreshes trip - * it). With MVRP at maximum a refresh is always permitted. */ + * Only MTIE is enabled and mstatus.MIE stays clear, so the pending + * timer wakes WFI without vectoring into the trap path. */ __asm__ volatile("csrw mie, %0" :: "r"(0x80UL)); /* MTIE */ while (1) { #ifndef MPFS_WDT_NODES_KEEP for (w = 0; w < 5U; w++) { - MSS_WDT_REFRESH(park_wdt_bases[w]) = 0xDEADC0DEU; + MSS_WDT_REFRESH(mpfs_wdt_bases[w]) = 0xDEADC0DEU; } #endif CLINT_MTIMECMP_REG(MPFS_FIRST_HART) = @@ -766,11 +759,38 @@ static const uint8_t athena_kat_aesctr[32] = { static const char *athena_fail_what; static int athena_fail_rc; +#ifdef MPFS_ATHENA_AES +/* One CTR run from a fresh key schedule, split into one or two calls so the + * self-check can prove the counter carries across them. */ +static int athena_ctr_run(const uint8_t *key, const uint8_t *ctr, + const uint8_t *in, uint8_t *out, uint32_t first, uint32_t second) +{ + Aes aes; + int ret; + + ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); + if (ret == 0) { + ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out, in, first); + } + if ((ret == 0) && (second > 0u)) { + ret = wc_AesCtrEncrypt(&aes, out + first, in + first, second); + } + wc_AesFree(&aes); + return ret; +} +#endif + static int mpfs_athena_selfcheck(void) { wc_Sha384 sha; uint8_t buf[256]; uint8_t digest[48]; +#ifdef MPFS_ATHENA_AES + uint8_t key[32], ctr[16], out[32], out2[32]; +#endif int ret; int i; @@ -819,80 +839,48 @@ static int mpfs_athena_selfcheck(void) } #ifdef MPFS_ATHENA_AES - { - Aes aes; - uint8_t key[32], ctr[16], out[32], out2[32]; - - for (i = 0; i < 32; i++) { - key[i] = (uint8_t)i; - } - for (i = 0; i < 16; i++) { - ctr[i] = (uint8_t)i; - } - athena_cb_calls = 0; - ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); - if (ret == 0) { - ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); - } - if (ret == 0) { - ret = wc_AesCtrEncrypt(&aes, out, buf, 32); - } - wc_AesFree(&aes); - /* The disk decrypt path calls CTR once per chunk, so the counter - * must carry across calls: two 16-byte calls equal one 32-byte call. */ - if (ret == 0) { - ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); - } - if (ret == 0) { - ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); - } - if (ret == 0) { - ret = wc_AesCtrEncrypt(&aes, out2, buf, 16); - } - if (ret == 0) { - ret = wc_AesCtrEncrypt(&aes, out2 + 16, buf + 16, 16); - } - wc_AesFree(&aes); - if (ret != 0) { - athena_fail_what = "AES-256-CTR"; - athena_fail_rc = ret; - return -1; - } - if (athena_cb_calls == 0) { - athena_fail_what = "AES-256-CTR callback never serviced (software fallback)"; - return -1; - } - if (memcmp(out, athena_kat_aesctr, sizeof(out)) != 0) { - athena_fail_what = "AES-256-CTR known-answer mismatch"; - return -1; - } + for (i = 0; i < 32; i++) { + key[i] = (uint8_t)i; + } + for (i = 0; i < 16; i++) { + ctr[i] = (uint8_t)i; + } + athena_cb_calls = 0; + ret = athena_ctr_run(key, ctr, buf, out, 32, 0); + if (ret != 0) { + athena_fail_what = "AES-256-CTR"; + athena_fail_rc = ret; + return -1; + } + if (athena_cb_calls == 0) { + athena_fail_what = "AES-256-CTR callback never serviced (software " + "fallback)"; + return -1; + } + if (memcmp(out, athena_kat_aesctr, sizeof(out)) != 0) { + athena_fail_what = "AES-256-CTR known-answer mismatch"; + return -1; + } + /* The disk decrypt path calls CTR once per chunk, so the counter has to + * carry across calls: two 16-byte calls must equal one 32-byte call, and + * a split that is not a whole number of blocks must too. */ + ret = athena_ctr_run(key, ctr, buf, out2, 16, 16); + if (ret == 0) { if (memcmp(out, out2, sizeof(out)) != 0) { - athena_fail_what = "AES-256-CTR counter does not carry across calls"; - return -1; - } - /* A length that is not a whole number of blocks, then the rest: the - * second call starts from the keystream the first one left behind. */ - memset(out2, 0, sizeof(out2)); - ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); - if (ret == 0) { - ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); - } - if (ret == 0) { - ret = wc_AesCtrEncrypt(&aes, out2, buf, 20); - } - if (ret == 0) { - ret = wc_AesCtrEncrypt(&aes, out2 + 20, buf + 20, 12); - } - wc_AesFree(&aes); - if (ret != 0) { - athena_fail_what = "AES-256-CTR partial block"; - athena_fail_rc = ret; - return -1; - } - if (memcmp(out2, athena_kat_aesctr, sizeof(out2)) != 0) { - athena_fail_what = "AES-256-CTR partial block mismatch"; + athena_fail_what = "AES-256-CTR counter does not carry across " + "calls"; return -1; } + ret = athena_ctr_run(key, ctr, buf, out2, 20, 12); + } + if (ret != 0) { + athena_fail_what = "AES-256-CTR split run"; + athena_fail_rc = ret; + return -1; + } + if (memcmp(out2, athena_kat_aesctr, sizeof(out2)) != 0) { + athena_fail_what = "AES-256-CTR partial block mismatch"; + return -1; } #endif return 0; @@ -945,35 +933,88 @@ static void mpfs_athena_init(void) #endif /* MPFS_ATHENA */ +#ifdef WOLFBOOT_RISCV_MMODE +/* Snapshot the boot ROM watchdog state and the reset cause for printing once + * the UART is up, then set the watchdogs up for the life of wolfBoot. */ +static void mpfs_wdt_boot_config(void) +{ + volatile uint32_t *wdt_e51 = (volatile uint32_t *)MSS_WDT_E51_BASE; + volatile uint32_t *reset_sr = (volatile uint32_t *)MPFS_SYSREG_RESET_SR; + unsigned int i; + + mpfs_wdt_default_mvrp = MSS_WDT_MVRP(MSS_WDT_E51_BASE); + mpfs_wdt_default_ctrl = MSS_WDT_CONTROL(MSS_WDT_E51_BASE); + for (i = 0; i < sizeof(mpfs_boot_wdt_snap) / sizeof(uint32_t); i++) { + mpfs_boot_wdt_snap[i] = wdt_e51[i]; + } + mpfs_boot_reset_sr_snap = *reset_sr; + *reset_sr = mpfs_boot_reset_sr_snap; /* W1C */ + +#ifndef WATCHDOG + /* The MSS watchdogs always count, so the default build widens them + * instead: TIME/MVRP to maximum and DEVRST (CONTROL bit 5) cleared, which + * turns a timeout into an NMI rather than a chip reset. hal_prepare_boot() + * restores the boot ROM values before do_boot. A refresh inside the + * triggered window would arm a reset, hence the refresh on each side. */ + for (i = 0; i < MPFS_NUM_HARTS; i++) { + MSS_WDT_REFRESH(mpfs_wdt_bases[i]) = 0xDEADC0DEU; + MSS_WDT_TIME(mpfs_wdt_bases[i]) = 0x00FFFFFFUL; + MSS_WDT_MVRP(mpfs_wdt_bases[i]) = 0x00FFFFFFUL; + MSS_WDT_CONTROL(mpfs_wdt_bases[i]) = 0; + MSS_WDT_REFRESH(mpfs_wdt_bases[i]) = 0xDEADC0DEU; + } +#else + /* WATCHDOG=1 keeps the E51 watchdog armed, with a timeout well clear of + * the ~5 s verify bound so nothing has to pet it mid-signature. */ + MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; + MSS_WDT_MVRP(MSS_WDT_E51_BASE) = WATCHDOG_TIMEOUT_TICKS; + MSS_WDT_CONTROL(MSS_WDT_E51_BASE) |= MSS_WDT_CTRL_ENABLE; +#endif +} + +#ifdef MPFS_DDR_INIT +/* Bring up LPDDR4, or halt. Each mpfs_ddr_init() pulses the SYSREG DDRC soft + * reset, which is the only way a rejected training recovers. There is no safe + * path past a failure: WOLFBOOT_LOAD_ADDRESS is in DDR, so a later load would + * write to a dead controller and hang inside the AXI master. */ +static void mpfs_ddr_bringup(void) +{ + unsigned int retry; + + for (retry = 0; retry < MPFS_DDR_MAX_OUTER_RETRY; retry++) { + if (retry > 0) { + wolfBoot_printf("DDR: Outer retry %u/%u (full DDRC re-init)\n", + retry, MPFS_DDR_MAX_OUTER_RETRY); + } + if (mpfs_ddr_init(retry) == 0) { + return; + } + } + wolfBoot_printf( + "DDR: Init FAILED after %u outer retries -- halting.\n" + "DDR: Power-cycle the board and retry.\n", + MPFS_DDR_MAX_OUTER_RETRY); + while (1) { + /* spin */ + } +} +#endif /* MPFS_DDR_INIT */ +#endif /* WOLFBOOT_RISCV_MMODE */ + void hal_init(void) { #ifdef WOLFBOOT_RISCV_MMODE - volatile uint32_t *wdt_e51 = (volatile uint32_t *)0x20001000UL; - volatile uint32_t *sysreg_reset_sr = (volatile uint32_t *)0x20002020UL; int h; #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) volatile uint32_t *dtim = (volatile uint32_t *)0x01000000UL; unsigned int k; #endif -#ifndef WATCHDOG - static const unsigned long wdt_bases[5] = { - MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, - MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE - }; - unsigned int w; -#endif -#ifdef MPFS_DDR_INIT - unsigned int outer_retry; - int ddr_ok = 0; -#endif /* Park every hart's machine-timer comparator at maximum. CLINT MTIME * is 0 (the RTC time base is not running yet) and mtimecmp resets to 0, * so MTIP is pending on every hart out of reset. A pending interrupt - * makes WFI return immediately, so the parked secondary harts' eNVM - * wait loop SPINS continuously (fetching from eNVM for the entire - * boot) instead of sleeping. Parking the comparators clears MTIP so - * WFI really waits. */ + * makes WFI return immediately, which would leave the parked secondary + * harts spinning on their eNVM wait loop for the whole boot. */ for (h = 0; h < MPFS_NUM_HARTS; h++) { CLINT_MTIMECMP_REG(h) = ~(uint64_t)0; } @@ -988,43 +1029,7 @@ void hal_init(void) __asm__ volatile("fence iorw, iorw" ::: "memory"); #endif - /* Capture boot ROM WDT defaults for restoration in hal_prepare_boot() */ - mpfs_wdt_default_mvrp = MSS_WDT_MVRP(MSS_WDT_E51_BASE); - mpfs_wdt_default_ctrl = MSS_WDT_CONTROL(MSS_WDT_E51_BASE); - /* Snapshot boot-ROM WDT state and SYSREG RESET_SR (reset status - * cause) so we can print them AFTER uart_init. RESET_SR is W1C -- - * we clear after reading. */ - mpfs_boot_wdt_snap[0] = wdt_e51[0]; - mpfs_boot_wdt_snap[1] = wdt_e51[1]; - mpfs_boot_wdt_snap[2] = wdt_e51[2]; - mpfs_boot_wdt_snap[3] = wdt_e51[3]; - mpfs_boot_wdt_snap[4] = wdt_e51[4]; - mpfs_boot_wdt_snap[5] = wdt_e51[5]; - mpfs_boot_reset_sr_snap = *sysreg_reset_sr; - *sysreg_reset_sr = mpfs_boot_reset_sr_snap; /* W1C: clear seen bits */ - -#ifndef WATCHDOG - /* WATCHDOG=0 (default): disable WDT for the duration of wolfBoot; it is - * re-enabled in hal_prepare_boot() before do_boot. The MPFS MSS WDOG - * cannot be disabled outright (it always counts), so clear DEVRST - * (bit 5) -- timeout then raises an NMI instead of a chip reset -- set - * TIME/MVRP to max for the longest window, and refresh with the magic - * key (a refresh inside the triggered window would arm a reset). */ - for (w = 0; w < 5; w++) { - MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; - MSS_WDT_TIME(wdt_bases[w]) = 0x00FFFFFFUL; - MSS_WDT_MVRP(wdt_bases[w]) = 0x00FFFFFFUL; - MSS_WDT_CONTROL(wdt_bases[w]) = 0; - MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; - } -#else - /* WATCHDOG=1: keep WDT enabled with a generous timeout for crypto. - * Verify is bounded at ~5s; configure a much larger timeout so we - * never have to pet the WDT during ECDSA verify. */ - MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; - MSS_WDT_MVRP(MSS_WDT_E51_BASE) = WATCHDOG_TIMEOUT_TICKS; - MSS_WDT_CONTROL(MSS_WDT_E51_BASE) |= MSS_WDT_CTRL_ENABLE; -#endif + mpfs_wdt_boot_config(); mpfs_config_l2_cache(); mpfs_signal_main_hart_started(); @@ -1057,39 +1062,10 @@ void hal_init(void) mpfs_boot_reset_sr_snap); #ifdef MPFS_DDR_INIT - /* Bring up LPDDR4 before any DDR-resident operations. - * - * Each mpfs_ddr_init() call pulses the SYSREG DDRC soft reset; a rejected - * training only recovers through this full controller re-init. */ - for (outer_retry = 0; outer_retry < MPFS_DDR_MAX_OUTER_RETRY; - outer_retry++) { - if (outer_retry > 0) { - wolfBoot_printf( - "DDR: Outer retry %u/%u (full DDRC re-init)\n", - outer_retry, MPFS_DDR_MAX_OUTER_RETRY); - } - if (mpfs_ddr_init(outer_retry) == 0) { - ddr_ok = 1; - break; - } - } - if (!ddr_ok) { - /* No safe path forward: WOLFBOOT_LOAD_ADDRESS is in DDR, so a - * subsequent disk-load would write to a non-functional - * controller and hang silently inside the AXI master. Halt - * with a clear message so the operator can power-cycle. */ - wolfBoot_printf( - "DDR: Init FAILED after %u outer retries -- halting.\n" - "DDR: Power-cycle the board and retry.\n", - MPFS_DDR_MAX_OUTER_RETRY); - while (1) { - /* spin */ - } - } + mpfs_ddr_bringup(); #endif #endif - #ifdef MPFS_SNVM_DUMP mpfs_snvm_dump(MPFS_SNVM_DUMP_FIRST, MPFS_SNVM_DUMP_COUNT); #endif @@ -1879,22 +1855,19 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) && \ (!defined(MPFS_WDT_NODES_KEEP) || defined(MPFS_SCB_SMODE_DENY)) - /* Disable the MSS watchdog dtb nodes BEFORE the serial-number read: this - * does not depend on the serial number and MUST run even if the SC serial - * read below fails. Otherwise the OS watchdog driver would arm them at - * probe while the parked E51 monitor (hal_smode_boot) also refreshes all - * five every cycle -- a conflict that can trip a watchdog-driven reset. + /* Disable the MSS watchdog dtb nodes before the serial-number read, so + * this still runs if that read fails: otherwise the OS watchdog driver + * arms them at probe while the parked E51 monitor (hal_smode_boot) also + * refreshes all five, and the conflict can trip a watchdog reset. * - * Do NOT override the stock /memory nodes: the 32-bit cached DDR window at - * 0x80000000 is only 1 GB wide (0xC0000000 up is the NON-CACHED alias onto - * the same DDR) and the stock Video Kit DTB already describes the full - * 2 GB; an earlier fixup that forced memory@80000000 to 2 GB made the - * kernel treat the alias window as extra RAM (self-aliasing corruption; - * boot hung at the first deep memblock allocation). + * The stock /memory nodes are left alone. The cached DDR window at + * 0x80000000 is 1 GB wide and 0xC0000000 up is a non-cached alias onto + * the same DDR, so describing 2 GB at 0x80000000 would present the alias + * to the kernel as extra RAM. The stock Video Kit DTB is already right. * - * cpu@2..cpu@4 stay ENABLED: the SBI HSM hart_start backend releases the - * parked harts on the kernel's request (SMP). cpu@0 (E51) is already - * disabled in the Yocto DTB; cpu@1 stays enabled so Linux boots on it. */ + * cpu@2..cpu@4 stay enabled for SMP: the SBI HSM hart_start backend + * releases the parked harts when the kernel asks. cpu@0 (E51) is + * already disabled in the Yocto DTB; cpu@1 is where Linux boots. */ for (i = 0; i < sizeof(cpu_off) / sizeof(cpu_off[0]); i++) { off = fdt_find_node_offset(&ctx, -1, cpu_off[i]); if (off >= 0) { @@ -2060,13 +2033,8 @@ static int mpfs_scb_wait_clear(uint32_t reg_offset, uint32_t mask, } #if defined(EXT_FLASH) || defined(MPFS_SBI_SC_FLASH) -/* ========================================================================== - * QSPI Flash Controller Implementation - * - * Both MSS QSPI (0x21000000) and SC QSPI (0x37020100) use CoreQSPI v2 - * with identical register layouts. The controller is selected at build - * time via MPFS_SC_SPI, which changes QSPI_BASE in the header. - * ========================================================================== */ +/* MSS QSPI (0x21000000) and SC QSPI (0x37020100) are both CoreQSPI v2 with + * identical register layouts; MPFS_SC_SPI picks one by changing QSPI_BASE. */ /* Microsecond delay using RISC-V time CSR (1 MHz tick rate) */ #ifndef WOLFBOOT_RISCV_MMODE @@ -2077,26 +2045,45 @@ static void udelay(uint32_t us) ; } #endif -/* Forward declarations */ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, uint32_t cmd_len, uint8_t *data, uint32_t data_len, uint8_t dummy_cycles); static int qspi_read_id(uint8_t *id_buf); static int qspi_enter_4byte_mode(void); -/* Send Release from Deep Power-Down / Wake up command */ +/* Spin until a STATUS bit is set. Call it through QSPI_WAIT, which keeps the + * stage names out of the binary unless DEBUG_QSPI is on. */ +static int qspi_wait_status(uint32_t bit, uint32_t tries, const char *what) +{ + while (!(QSPI_STATUS & bit) && --tries) + ; + if (tries == 0) { +#ifdef DEBUG_QSPI + wolfBoot_printf("QSPI: %s timeout, status=0x%x\n", what, QSPI_STATUS); +#else + (void)what; +#endif + return -1; + } + return 0; +} + +#ifdef DEBUG_QSPI +#define QSPI_WAIT(bit, tries, what) qspi_wait_status((bit), (tries), (what)) +#else +#define QSPI_WAIT(bit, tries, what) qspi_wait_status((bit), (tries), NULL) +#endif + static void qspi_flash_wakeup(void) { uint8_t cmd = 0xAB; /* Release from Deep Power-Down */ qspi_transfer_block(QSPI_MODE_WRITE, &cmd, 1, NULL, 0, 0); - /* Flash needs tRES1 (3us typ) to wake up */ - udelay(10); + udelay(10); /* tRES1, 3us typ */ } int qspi_init(void) { uint8_t id[3]; - uint32_t timeout; #ifdef MPFS_SC_SPI wolfBoot_printf("QSPI: Using SC QSPI Controller (0x%x)\n", QSPI_BASE); @@ -2111,71 +2098,47 @@ int qspi_init(void) QSPI_CONTROL, QSPI_STATUS, QSPI_DIRECT); #endif - /* Disable direct access / XIP mode (SC may have left it enabled) */ + /* The System Controller may have left XIP enabled */ QSPI_DIRECT = 0; #else wolfBoot_printf("QSPI: Using MSS QSPI Controller (0x%x)\n", QSPI_BASE); - /* Enable QSPI peripheral clock (MSS only) */ SYSREG_SUBBLK_CLOCK_CR |= SYSREG_SUBBLK_CLOCK_CR_QSPI; udelay(1); - - /* Release MSS QSPI from reset (MSS only) */ SYSREG_SOFT_RESET_CR &= ~SYSREG_SOFT_RESET_CR_QSPI; udelay(10); #endif - /* Disable controller before configuration */ QSPI_CONTROL = 0; - - /* Disable all interrupts */ QSPI_IEN = 0; - - /* Configure QSPI Control Register: - * - Clock divider (MPFS_QSPI_CLK_DIV, ~5 MHz by default) - * - CPOL=1 (clock idle high) for SPI Mode 3 - * - Sample on SCK edge - * - Enable controller - */ + /* CLKIDLE + SAMPLE_SCK is SPI mode 3 */ QSPI_CONTROL = (MPFS_QSPI_CLK_DIV << QSPI_CTRL_CLKRATE_OFFSET) | QSPI_CTRL_CLKIDLE | QSPI_CTRL_SAMPLE_SCK | QSPI_CTRL_EN; - /* Wait for controller to be ready */ - timeout = QSPI_TIMEOUT_TRIES; - while (!(QSPI_STATUS & QSPI_STATUS_READY) && --timeout); - if (timeout == 0) { + if (QSPI_WAIT(QSPI_STATUS_READY, QSPI_TIMEOUT_TRIES, "init") != 0) { wolfBoot_printf("QSPI: Controller not ready\n"); return -1; } - /* Wake up flash from deep power-down (if applicable) */ qspi_flash_wakeup(); - /* Read and display JEDEC ID for verification */ if (qspi_read_id(id) == 0) { wolfBoot_printf("QSPI: Flash ID = 0x%02x 0x%02x 0x%02x\n", id[0], id[1], id[2]); } - /* Enter 4-byte addressing mode for >16MB flash */ + /* 4-byte addressing for flash larger than 16 MB */ qspi_enter_4byte_mode(); return 0; } -/* QSPI Block Transfer Function - * Modeled after Microchip's MSS_QSPI_polled_transfer_block reference driver. - * - * read_mode: 0=write (QSPI_MODE_WRITE), 1=read (QSPI_MODE_READ) - * cmd: Command buffer (opcode + address bytes) - * cmd_len: Length of command (opcode + address, NOT including opcode separately) - * data: Data buffer for read/write - * data_len: Length of data phase - * dummy_cycles: Number of idle cycles between command and data phase - */ +/* Polled transfer, after Microchip's MSS_QSPI_polled_transfer_block. + * cmd holds the opcode and any address bytes; dummy_cycles are the idle + * cycles the controller inserts between the command and data phases. */ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, uint32_t cmd_len, uint8_t *data, uint32_t data_len, uint8_t dummy_cycles) @@ -2183,52 +2146,31 @@ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, uint32_t total_bytes = cmd_len + data_len; uint32_t frames; uint32_t i; + uint32_t step; uint32_t timeout; uint32_t frame_cmd; uint32_t rx_word; int word_read; - /* Wait for controller to be ready before starting */ - timeout = QSPI_TIMEOUT_TRIES; - while (!(QSPI_STATUS & QSPI_STATUS_READY) && --timeout); - if (timeout == 0) { - #ifdef DEBUG_QSPI - wolfBoot_printf("QSPI: Timeout waiting for READY\n"); - #endif + if (QSPI_WAIT(QSPI_STATUS_READY, QSPI_TIMEOUT_TRIES, "READY") != 0) { return -1; } - /* Drain RX FIFO of any stale data from previous transfers. */ + /* Discard anything a previous transfer left in the RX FIFO. */ timeout = QSPI_TIMEOUT_TRIES; while ((QSPI_STATUS & QSPI_STATUS_RXAVAIL) && --timeout) { (void)QSPI_RX_DATA; } -#ifdef DEBUG_QSPI - if (timeout == 0) { - /* log warning and continue trying to transfer data */ - wolfBoot_printf("QSPI: Timeout draining RX FIFO\n"); - } -#endif - /* Configure FRAMES register: - * - Total bytes: command + data (idle cycles handled by hardware) - * - Command bytes: TX-only bytes before data phase - * - Idle cycles: inserted by hardware between command and data - * - FBYTE: status flags (RXAVAIL/TXAVAIL) refer to individual bytes - * - * For write-mode transfers, set CMDBYTES = TOTALBYTES so the entire - * transfer occurs in the command phase (TX-only). The CoreQSPI data - * phase shifts TX FIFO output by a fixed offset on writes, causing - * data rotation in the programmed page. Keeping everything in the - * command phase avoids this. The flash determines command vs data - * boundaries from the opcode, not the controller's phase. */ + /* Writes put the whole transfer in the command phase (CMDBYTES = + * TOTALBYTES): the CoreQSPI data phase shifts TX FIFO output by a fixed + * offset on writes, which rotates the programmed page. The flash takes + * the command/data boundary from the opcode, not the controller phase. */ frame_cmd = read_mode ? cmd_len : total_bytes; - /* FBYTE makes RXAVAIL/TXAVAIL count single bytes; FWORD makes them count - * 32-bit words, so a read polls STATUS once per four bytes instead of - * once per byte and pulls them through QSPI_X4_RX_DATA in one access. - * The polled loop, not SCK, is what caps the byte-mode read rate (it - * measures the same from CLKRATE /8 down to /2). Word mode needs a data - * phase that is a whole number of words; anything else stays byte-mode. */ + /* FBYTE counts RXAVAIL/TXAVAIL in bytes, FWORD in 32-bit words, so a + * word-mode read polls STATUS once per four bytes and takes them through + * QSPI_X4_RX_DATA in one access. The polling, not SCK, caps the read + * rate. FWORD needs a whole number of words in the data phase. */ word_read = (read_mode && data_len >= 4u && (data_len & 3u) == 0u) ? 1 : 0; frames = ((total_bytes & 0xFFFF) << QSPI_FRAMES_TOTALBYTES_OFFSET) | ((frame_cmd & 0x1FF) << QSPI_FRAMES_CMDBYTES_OFFSET) | @@ -2238,19 +2180,12 @@ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, QSPI_FRAMES = frames; - /* Send command bytes (opcode + address). - * Use TXAVAIL (bit 3) to check for FIFO space -- CoreQSPI v2 does NOT - * have a TXFULL status bit (bit 5 is reserved/always 0). - * A fence (iorw, iorw) after each TX write ensures the store reaches the - * peripheral before we read STATUS again (RISC-V RVWMO allows posted - * stores that could cause stale TXAVAIL reads and FIFO overflow). */ + /* TXAVAIL is the only FIFO-space flag: CoreQSPI v2 has no TXFULL bit. + * The fence after each write keeps RVWMO from leaving the store posted, + * which would make the next STATUS read stale and overflow the FIFO. */ for (i = 0; i < cmd_len; i++) { - timeout = QSPI_TIMEOUT_TRIES; - while (!(QSPI_STATUS & QSPI_STATUS_TXAVAIL) && --timeout); - if (timeout == 0) { - #ifdef DEBUG_QSPI - wolfBoot_printf("QSPI: TX FIFO full timeout\n"); - #endif + if (QSPI_WAIT(QSPI_STATUS_TXAVAIL, QSPI_TIMEOUT_TRIES, + "TX cmd") != 0) { return -2; } QSPI_TX_DATA = cmd[i]; @@ -2258,81 +2193,42 @@ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, } if (read_mode) { - if (word_read) { - /* One STATUS poll and one 32-bit read per four bytes. The - * destination is not necessarily word-aligned, so the word is - * stored a byte at a time; the saving is in the polling and the - * FIFO accesses, not the stores. */ - for (i = 0; i < data_len; i += 4u) { - timeout = QSPI_RX_TIMEOUT_TRIES; - while (!(QSPI_STATUS & QSPI_STATUS_RXAVAIL) && --timeout); - if (timeout == 0) { - #ifdef DEBUG_QSPI - wolfBoot_printf("QSPI: RX timeout at word %d, status=0x%x\n", - i >> 2, QSPI_STATUS); - #endif - return -3; - } + step = word_read ? 4u : 1u; + for (i = 0; i < data_len; i += step) { + if (QSPI_WAIT(QSPI_STATUS_RXAVAIL, QSPI_RX_TIMEOUT_TRIES, + "RX data") != 0) { + return -3; + } + if (word_read) { + /* data is not guaranteed word-aligned, so store bytewise; + * the saving is in the polling and the FIFO accesses. */ rx_word = QSPI_X4_RX_DATA; data[i] = (uint8_t)(rx_word & 0xFFu); data[i + 1] = (uint8_t)((rx_word >> 8) & 0xFFu); data[i + 2] = (uint8_t)((rx_word >> 16) & 0xFFu); data[i + 3] = (uint8_t)((rx_word >> 24) & 0xFFu); } - } - else { - /* Byte mode: poll RXAVAIL for each data byte. */ - for (i = 0; i < data_len; i++) { - timeout = QSPI_RX_TIMEOUT_TRIES; - while (!(QSPI_STATUS & QSPI_STATUS_RXAVAIL) && --timeout); - if (timeout == 0) { - #ifdef DEBUG_QSPI - wolfBoot_printf("QSPI: RX timeout at byte %d, status=0x%x\n", - i, QSPI_STATUS); - #endif - return -3; - } + else { data[i] = QSPI_RX_DATA; } } - /* Wait for receive complete */ - timeout = QSPI_RX_TIMEOUT_TRIES; - while (!(QSPI_STATUS & QSPI_STATUS_RXDONE) && --timeout); - if (timeout == 0) { - #ifdef DEBUG_QSPI - wolfBoot_printf("QSPI: RXDONE timeout\n"); - #endif + if (QSPI_WAIT(QSPI_STATUS_RXDONE, QSPI_RX_TIMEOUT_TRIES, + "RXDONE") != 0) { return -5; } - } else { - /* Write mode: send data bytes. - * Must push bytes without delay -- any gap causes FIFO underflow - * since CoreQSPI continues clocking with empty FIFO. - * Fence (iorw, iorw) after each write ensures the store reaches the - * FIFO before we re-read STATUS (prevents FIFO overflow from posted - * stores). */ - if (data && data_len > 0) { - for (i = 0; i < data_len; i++) { - timeout = QSPI_TIMEOUT_TRIES; - while (!(QSPI_STATUS & QSPI_STATUS_TXAVAIL) && --timeout); - if (timeout == 0) { - #ifdef DEBUG_QSPI - wolfBoot_printf("QSPI: TX data timeout\n"); - #endif - return -4; - } - QSPI_TX_DATA = data[i]; - QSPI_IO_FENCE(); + } + else { + /* Any gap here underflows the FIFO: CoreQSPI keeps clocking. */ + for (i = 0; i < data_len; i++) { + if (QSPI_WAIT(QSPI_STATUS_TXAVAIL, QSPI_TIMEOUT_TRIES, + "TX data") != 0) { + return -4; } + QSPI_TX_DATA = data[i]; + QSPI_IO_FENCE(); } - /* Wait for transmit complete */ - timeout = QSPI_TIMEOUT_TRIES; - while (!(QSPI_STATUS & QSPI_STATUS_TXDONE) && --timeout); - if (timeout == 0) { - #ifdef DEBUG_QSPI - wolfBoot_printf("QSPI: TXDONE timeout, status=0x%x\n", - QSPI_STATUS); - #endif + if (QSPI_WAIT(QSPI_STATUS_TXDONE, QSPI_TIMEOUT_TRIES, + "TXDONE") != 0) { return -5; } } diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 79e3728e75..a2310ff329 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -80,6 +80,10 @@ /* Write "0xDEAD" to cause a full MSS reset*/ #define SYSREG_MSS_RESET_CR (*((volatile uint32_t*)(SYSREG_BASE + 0x18))) +/* Reset status (offset 0x20): the cause of the last reset. Cleared by + * writing zero, not by writing the bits back. */ +#define MPFS_SYSREG_RESET_SR (SYSREG_BASE + 0x20) + /* Peripheral Subblock Clock Control Register (offset 0x84) */ #define SYSREG_SUBBLK_CLOCK_CR (*((volatile uint32_t*)(SYSREG_BASE + 0x84))) #define SYSREG_SUBBLK_CLOCK_CR_ENVM (1U << 0) @@ -259,11 +263,8 @@ /* Bitstream/IAP services. The SPI directory index of 0x22/0x42/0x44 rides * in the SERVICES_CR mailbox-offset field, not in the mailbox. */ #define SYS_SERV_CMD_IAP_IMAGE_AUTH 0x22u /* index in CR[14:7] */ -#define SYS_SERV_CMD_BITSTREAM_AUTH 0x23u /* SPI address in mailbox */ #define SYS_SERV_CMD_IAP_PROGRAM_IDX 0x42u /* restarts the device on success */ -#define SYS_SERV_CMD_IAP_PROGRAM_ADDR 0x43u #define SYS_SERV_CMD_IAP_VERIFY_IDX 0x44u -#define SYS_SERV_CMD_IAP_VERIFY_ADDR 0x45u #define SYS_SERV_CMD_AUTO_UPDATE 0x46u /* restarts the device on success */ #define SYS_SERV_CMD_DIGEST_CHECK 0x47u /* options word in, DIGESTERR at byte 4 */ @@ -337,25 +338,10 @@ int mpfs_nonce(uint8_t *nonce); * config_athena.h (BASE32_ADDR_ATHENAREG = 0x20127000). */ #define ATHENA_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x00))) #define ATHENA_STALL_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x04))) -#define ATHENA_UPPER_ADDRESS (*((volatile uint32_t*)(ATHENA_BASE + 0x08))) -/* Security UG Table 7-7. RESET reads 1 out of power-on reset; MSS_OWNER says - * the Libero ownership mode gave the core to the MSS rather than the fabric. */ +/* Security UG Table 7-7. RESET reads 1 out of power-on reset. */ #define ATHENA_CR_RESET (1U << 0) -#define ATHENA_CR_PURGE (1U << 1) -#define ATHENA_CR_GO (1U << 2) #define ATHENA_CR_RINGOSCON (1U << 3) -#define ATHENA_CR_STREAM_EN (1U << 4) #define ATHENA_CR_STALL_EN (1U << 5) -#define ATHENA_CR_STALL_RATE_SHIFT 6 -#define ATHENA_CR_STALL_RATE_MASK (3U << ATHENA_CR_STALL_RATE_SHIFT) -#define ATHENA_CR_COMPLETE (1U << 8) -#define ATHENA_CR_ALARM (1U << 9) -#define ATHENA_CR_BUSERROR (1U << 10) -#define ATHENA_CR_STREAM_ENABLED (1U << 11) -#define ATHENA_CR_BUSY (1U << 12) -#define ATHENA_CR_MSS_OWNER (1U << 28) -#define ATHENA_CR_FAB_OWNER (1U << 29) - /* L2 Cache Controller (CACHE_CTRL @ 0x02010000) */ #define L2_CACHE_BASE 0x02010000UL diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index c25c7c9b1a..931868edef 100644 --- a/hal/mpfs250_ddr.c +++ b/hal/mpfs250_ddr.c @@ -3151,7 +3151,7 @@ int mpfs_ddr_init(unsigned int outer_retry) do { ret = run_training(0); if (ret != 0) { - continue; + break; } /* HSS DDR_TRAINING_SET_FINAL_MODE: rewrite DDRPHY_MODE with @@ -3206,7 +3206,7 @@ int mpfs_ddr_init(unsigned int outer_retry) wolfBoot_printf( " WRCALIB not all lanes (result=%u) -- retraining\n", (unsigned)wrcal); - continue; + break; } /* HSS DDR_TRAINING_IP_SM_VERIFY: a DQ/DQS window under * DQ_DQS_NUM_TAPS (5) on any lane is a failed training. */ @@ -3314,12 +3314,12 @@ int mpfs_ddr_init(unsigned int outer_retry) wolfBoot_printf( " MTC 256B TIMEOUT (DONE_ACK=0x%x ERR_STS=0x%x)\n", DDRCFG_REG(MT_DONE_ACK), DDRCFG_REG(MT_ERROR_STS)); - continue; + break; } if ((DDRCFG_REG(MT_ERROR_STS) & 0x1UL) != 0) { wolfBoot_printf(" MTC 256B FAIL (err_sts=0x%x)\n", DDRCFG_REG(MT_ERROR_STS)); - continue; + break; } DBG_DDR(" MTC 256B PASS (err_sts=0x%x to_used=0x%x)\n", DDRCFG_REG(MT_ERROR_STS), diff --git a/src/riscv_sbi.c b/src/riscv_sbi.c index e661b26d4e..0758873cab 100644 --- a/src/riscv_sbi.c +++ b/src/riscv_sbi.c @@ -140,12 +140,10 @@ #define MPFS_NUM_HARTS 5 #endif -/* Cross-hart SBI state must NOT live in L2-scratch BSS: cacheable stores - * to the scratchpad (Zero Device) can be silently lost when the dirty - * cache line is eventually evicted, so values written by one hart vanish - * before another hart (or a later cache-cold read) sees them. Keep this - * state in the E51 DTIM instead: a small always-uncached RAM that every - * hart reads and writes coherently (the same role it has under HSS). */ +/* Cross-hart SBI state cannot live in L2-scratch BSS: a cacheable store to + * the scratchpad (Zero Device) is lost when the dirty line is evicted, so a + * value written by one hart can vanish before another hart sees it. The E51 + * DTIM is always uncached and coherent for every hart, as it is under HSS. */ #ifndef SBI_SHARED_DTIM_ADDR #define SBI_SHARED_DTIM_ADDR 0x01000000UL /* E51 DTIM */ #endif @@ -218,12 +216,11 @@ void sbi_hart_mark_started(unsigned long hartid) * switches to the stack armed in mscratch because the trapped S-mode * context's sp is a virtual address once the OS enables paging. * - * The stacks must NOT live in L2-scratch BSS: under SMP cache pressure a - * dirty frame line can be evicted mid-trap and the writeback to the - * scratchpad is lost, so the restore reads zeros (observed: the kernel - * resumed from an ecall with sp=0). Use the OS-invisible reserved DDR - * region instead (hss-buffer@103fc00000, nomap in the stock dtb -- the - * monitor carve-out under HSS). */ + * These cannot live in L2-scratch BSS either: under SMP cache pressure a + * dirty frame line can be evicted mid-trap and the writeback lost, leaving + * the restore to read zeros. They go in the OS-invisible reserved DDR + * region (hss-buffer@103fc00000, nomap in the stock dtb: the monitor + * carve-out under HSS). */ #ifndef SBI_MSTACK_BASE #define SBI_MSTACK_BASE 0x103FC00000UL #endif diff --git a/src/update_disk.c b/src/update_disk.c index 997d6d0419..fccb3e5f92 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -154,9 +154,9 @@ static uint8_t disk_encrypt_nonce[ENCRYPT_NONCE_SIZE]; #define SLOT_IS_GOLDEN(s) 0 #endif -/* With the failover zeroize the image key may already be gone when the - * loader runs, and the golden image is a plaintext one; without it every - * slot is encrypted and a missing key is fatal as before. */ +/* Under the failover zeroize the image key may already be gone and the golden + * image is a plaintext one, so only the operational slots are encrypted. + * Without it every slot is encrypted and a missing key is fatal. */ #ifdef DISK_FAILOVER_ZEROIZE #ifndef DISK_GOLDEN_SLOT #error "DISK_FAILOVER_ZEROIZE needs DISK_GOLDEN_SLOT" @@ -465,7 +465,6 @@ static int slot_state_write(struct boot_slot *s, uint8_t state, return 0; } - /* Read both slots' states, and drop any slot still marked TESTING out of the * election. Such a slot was armed before the previous boot and nothing * confirmed it, so whatever it started did not come up. @@ -474,7 +473,7 @@ static int slot_state_write(struct boot_slot *s, uint8_t state, * from the selection AND from max_ver, which is what lets an older surviving * slot boot without relaxing the anti-rollback guard for anything still live. * A slot that merely fails verification keeps its version and still blocks an - * older one, exactly as before. */ + * older one. */ static void disk_boot_state_reap(uint32_t *pA_ver, uint32_t *pB_ver) { uint8_t st; From 9fc1bf57dcc4763a527548df10901a3658e67463 Mon Sep 17 00:00:00 2001 From: David Garske Date: Sat, 17 Oct 2026 12:21:21 -0700 Subject: [PATCH 13/23] PolarFire SoC: correct the MSS watchdog and reset-cause register semantics --- .../examples/polarfire_mpfs250_m_qspi.config | 13 ++-- docs/PolarFire.md | 13 +++- docs/Targets.md | 2 +- hal/mpfs250.c | 77 +++++++------------ hal/mpfs250.h | 7 +- hal/mpfs250_ubl.c | 20 +---- 6 files changed, 53 insertions(+), 79 deletions(-) diff --git a/config/examples/polarfire_mpfs250_m_qspi.config b/config/examples/polarfire_mpfs250_m_qspi.config index c4a9ee18c6..00bce7d1b2 100644 --- a/config/examples/polarfire_mpfs250_m_qspi.config +++ b/config/examples/polarfire_mpfs250_m_qspi.config @@ -43,14 +43,13 @@ ELF?=1 # Stripped ELF is typically ~5KB. STRIP_ELF?=1 -# Watchdog timer configuration (default: disabled) -# When commented out, the WDT is disabled in hal_init() and re-enabled -# with the boot ROM default in hal_prepare_boot() before do_boot. -# Uncomment -DWATCHDOG to keep the WDT enabled with a generous timeout -# for the duration of wolfBoot. Verify is bounded at ~5s; default 30s -# avoids the need to pet the WDT during long ECDSA verify. +# Watchdog timer configuration +# The MSS watchdogs are armed by the bitstream and count whatever this +# is set to; wolfBoot refreshes all five in hal_init() and again in +# hal_prepare_boot(). There is no timeout to set and no way to disarm +# them from software: the window is a bitstream property (~28.6 s), and +# verify is bounded at ~5 s, so nothing has to pet it mid-signature. #CFLAGS_EXTRA+=-DWATCHDOG -#CFLAGS_EXTRA+=-DWATCHDOG_TIMEOUT_MS=30000 OPTIMIZATION_LEVEL=1 diff --git a/docs/PolarFire.md b/docs/PolarFire.md index 91c6696594..2ee91a7ed4 100644 --- a/docs/PolarFire.md +++ b/docs/PolarFire.md @@ -286,8 +286,7 @@ These flags apply to `polarfire_mpfs250_m_qspi.config` and are added via `CFLAGS | Flag | Default | Description | |------|---------|-------------| -| `WATCHDOG` | undefined (disabled) | When defined, the E51 watchdog timer is **kept enabled** during wolfBoot operation with a generous timeout. When undefined, the WDT is **disabled** in `hal_init()` and re-enabled with the boot ROM default in `hal_prepare_boot()` before jumping to the application. Either way, the application receives a normal WDT. | -| `WATCHDOG_TIMEOUT_MS` | `30000` (30 s) | Watchdog timeout in milliseconds when `WATCHDOG` is defined. ECDSA P-384 verification on E51 with portable C math is bounded at ~5 s; the default 30 s avoids any need to refresh the WDT during the long verify call. | +| `WATCHDOG` | undefined | Has no timeout to set on this target: the MSS watchdogs are armed by the bitstream and their window cannot be changed from software. See "Watchdog policy" below. wolfBoot refreshes all five in `hal_init()` and again in `hal_prepare_boot()` either way. | | `MPFS_WDT_NODES_KEEP` (make variable, `=1`) | undefined | Linux boot only: keep the five MSS watchdog nodes enabled in the dtb and stop the parked E51 from refreshing them, so the OS owns the watchdogs. See "Watchdog policy" below. | | `MPFS_QSPI_CLK_DIV` (make variable) | `QSPI_CLK_DIV_8` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h`. See "PolarFire SoC QSPI". | | `MPFS_SNVM_DUMP` (make variable, `=1`) | undefined | Read-only: at boot, print the admin word, state and a checksum of `MPFS_SNVM_DUMP_COUNT` sNVM pages from `MPFS_SNVM_DUMP_FIRST` (defaults 2 from 200, the keystore and the wrapped image key). Page contents are never printed. M-mode only. | @@ -610,8 +609,14 @@ power-on. see "Linux kernel command line (bootargs)" in `docs/compile.md`), MAC addresses from the device serial number, and all five MSS watchdog nodes are disabled. -**Watchdog policy:** the MSS watchdogs always count and reset the chip on timeout (they cannot -be disabled in hardware, and `CONTROL=0` does not prevent the reset). After hand-off the parked +**Watchdog policy:** the five MSS watchdogs are configured by the bitstream at design time and +are already counting when wolfBoot starts. Software can neither stop them nor retime them: there +is no enable bit, `CONTROL=0` does not prevent the reset (DEVRST is STATUS bit 5, read-only), and +the first write to any of offsets `0x00`-`0x0C` locks TRIGGER, MVRP and TIME until the next +system reset - a refresh is such a write. On the Video Kit they read TIME `0xFFFFF0`, MVRP +`0x989680`, TRIGGER `0x3E8`, CONTROL `0x02`, which is already the longest window the 24-bit +counter allows: the counter runs at the APB clock divided by 256, so a reload lasts about 28.6 s. +Refreshing them is therefore the only control wolfBoot has. After hand-off the parked E51 acts as a monitor and refreshes all five watchdogs; the OS watchdog driver is disabled via the dtb fixup so the two never conflict. `MPFS_WDT_NODES_KEEP=1` hands the watchdogs to the OS instead: the dtb nodes stay enabled and the parked E51 stops refreshing, so the OS driver arms diff --git a/docs/Targets.md b/docs/Targets.md index 023811a768..06821ce85c 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -915,7 +915,7 @@ Make variables, on the command line or in the `.config`. See [PolarFire.md](Pola | `DISK_GOLDEN_SLOT`, `DISK_FAILOVER_ZEROIZE` | Golden slot tried after A/B, and zeroizing the image key and slot heads before the golden boot. | | `MPFS_QSPI_CLK_DIV` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h` (default `QSPI_CLK_DIV_8`). | | `MPFS_WDT_NODES_KEEP` | Leave the MSS watchdog nodes enabled and stop the parked E51 refreshing them, so the OS owns the watchdogs. | -| `WATCHDOG`, `WATCHDOG_TIMEOUT_MS` | Keep the E51 watchdog running during wolfBoot itself, with the given timeout (default 30 s). | +| `WATCHDOG` | No timeout to set on this target: the MSS watchdog window is a bitstream property and cannot be changed from software. | > **Note:** the MPFS250 U54/E51 cores have no RISC-V crypto extensions (Zknh), so none of the > configurations use them. The SD-card, QSPI and M-Mode + DDR configs build the portable RISC-V diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 2aca5e2ed8..e61ace94b5 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -105,10 +105,6 @@ const uint8_t __attribute__((section(".envm_manifest"), used)) { [0 ... MPFS_ENVM_MANIFEST_SIZE - 1] = 0xFF }; #endif -/* Saved boot ROM watchdog values, restored in hal_prepare_boot(). */ -static uint32_t mpfs_wdt_default_mvrp = 0; -static uint32_t mpfs_wdt_default_ctrl = 0; - #ifdef WOLFBOOT_RISCV_MMODE static const unsigned long mpfs_wdt_bases[MPFS_NUM_HARTS] = { MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, @@ -168,22 +164,22 @@ __attribute__((noinline)) void udelay(uint32_t us) extern uint8_t _main_hart_hls; /* linker-provided address symbol; typed as uint8_t to avoid size confusion */ -/* Watchdog timeout configuration. - * WATCHDOG=0 (default): WDT disabled in hal_init() then restored to boot - * ROM defaults in hal_prepare_boot() before do_boot. - * WATCHDOG=1: WDT kept enabled with WATCHDOG_TIMEOUT_MS during wolfBoot. - * Verify is bounded at ~5s; default 30s leaves ample headroom and avoids - * the need to pet the WDT during the long ECDSA verify call. */ -#ifdef WATCHDOG -# ifndef WATCHDOG_TIMEOUT_MS -# define WATCHDOG_TIMEOUT_MS 30000U -# endif -/* MPFS MSS WDT clock is AHB / 256 ~= 150 MHz / 256 ~= 585 kHz at S-mode rate - * but ~80 MHz / 256 ~= 312 kHz on E51 reset clocks. Use a conservative - * 300 ticks/ms; the actual rate may be a bit higher but a slightly longer - * timeout is safe. Caller can override WATCHDOG_TIMEOUT_MS at build time. */ -# define WATCHDOG_TIMEOUT_TICKS ((WATCHDOG_TIMEOUT_MS) * 300U) -#endif +/* Watchdog policy. The five MSS watchdogs are configured by the bitstream at + * design time and are already counting when wolfBoot starts; software cannot + * stop them and cannot usefully retime them. On the Video Kit they read + * TIME 0xFFFFF0, MVRP 0x989680, TRIGGER 0x3E8, CONTROL 0x02 (timeout + * interrupt enabled, forbidden window off), which is the longest window the + * 24-bit counter allows: the counter runs at the APB clock divided by 256, so + * a reload lasts about 28.6 s and the TRIGGER crossing arrives just before + * that. wolfBoot's whole job is therefore to refresh them, here and from the + * parked E51 after hand-off (hal_smode_boot). + * + * Retiming is not available even in principle: the first write to any of + * offsets 0x00-0x0C locks TRIGGER, MVRP and TIME until the next system reset + * (mss_watchdog.c MSS_WD_configure), and a refresh is such a write. So is + * disarming: there is no enable bit, and CONTROL bit 5 is not DEVRST - DEVRST + * is STATUS bit 5, read-only. WATCHDOG therefore has no timeout knob on this + * target; it selects only whether wolfBoot refreshes during its own run. */ /* CLINT MSIP register for IPI delivery */ #define CLINT_MSIP_REG(hart) (*(volatile uint32_t*)(CLINT_BASE + (hart) * 4)) @@ -942,34 +938,20 @@ static void mpfs_wdt_boot_config(void) volatile uint32_t *reset_sr = (volatile uint32_t *)MPFS_SYSREG_RESET_SR; unsigned int i; - mpfs_wdt_default_mvrp = MSS_WDT_MVRP(MSS_WDT_E51_BASE); - mpfs_wdt_default_ctrl = MSS_WDT_CONTROL(MSS_WDT_E51_BASE); for (i = 0; i < sizeof(mpfs_boot_wdt_snap) / sizeof(uint32_t); i++) { mpfs_boot_wdt_snap[i] = wdt_e51[i]; } + /* Read, then zero, so the next reset is captured: this register is not + * write-1-to-clear, and writing the value back would set those bits. */ mpfs_boot_reset_sr_snap = *reset_sr; - *reset_sr = mpfs_boot_reset_sr_snap; /* W1C */ - -#ifndef WATCHDOG - /* The MSS watchdogs always count, so the default build widens them - * instead: TIME/MVRP to maximum and DEVRST (CONTROL bit 5) cleared, which - * turns a timeout into an NMI rather than a chip reset. hal_prepare_boot() - * restores the boot ROM values before do_boot. A refresh inside the - * triggered window would arm a reset, hence the refresh on each side. */ + *reset_sr = 0U; + + /* Start every watchdog's window from full. The design-time window is far + * longer than the ~5 s ECDSA P-384 verify, so nothing has to refresh + * mid-signature; hal_prepare_boot() refreshes again before the hand-off. */ for (i = 0; i < MPFS_NUM_HARTS; i++) { MSS_WDT_REFRESH(mpfs_wdt_bases[i]) = 0xDEADC0DEU; - MSS_WDT_TIME(mpfs_wdt_bases[i]) = 0x00FFFFFFUL; - MSS_WDT_MVRP(mpfs_wdt_bases[i]) = 0x00FFFFFFUL; - MSS_WDT_CONTROL(mpfs_wdt_bases[i]) = 0; - MSS_WDT_REFRESH(mpfs_wdt_bases[i]) = 0xDEADC0DEU; } -#else - /* WATCHDOG=1 keeps the E51 watchdog armed, with a timeout well clear of - * the ~5 s verify bound so nothing has to pet it mid-signature. */ - MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; - MSS_WDT_MVRP(MSS_WDT_E51_BASE) = WATCHDOG_TIMEOUT_TICKS; - MSS_WDT_CONTROL(MSS_WDT_E51_BASE) |= MSS_WDT_CTRL_ENABLE; -#endif } #ifdef MPFS_DDR_INIT @@ -1964,17 +1946,12 @@ void hal_prepare_boot(void) { #ifdef WOLFBOOT_RISCV_MMODE #ifndef WOLFBOOT_MMODE_SMODE_BOOT - /* Restore boot ROM WDT defaults so the application sees a normal WDT. - * Refresh first so the timer doesn't fire immediately after we apply - * the new MVRP. Restore the original CONTROL value (including the - * enable bit) rather than unconditionally enabling. */ + /* Hand the application a full window. Nothing else needs restoring: + * TIME, MVRP and TRIGGER keep their design-time values throughout. */ MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; - MSS_WDT_MVRP(MSS_WDT_E51_BASE) = mpfs_wdt_default_mvrp; - MSS_WDT_CONTROL(MSS_WDT_E51_BASE) = mpfs_wdt_default_ctrl; #else - /* Booting an S-mode OS: keep the watchdogs in the safe state set in - * hal_init (no device reset, maximum window) and give every hart's - * watchdog one final refresh so the OS inherits a full window (the + /* Booting an S-mode OS: give every hart's watchdog one final refresh so + * the OS inherits a full window (the * OS watchdog driver hangs at probe if it finds an already-tripped * watchdog). The OS watchdog nodes are disabled in the dtb, so after * hand-off the parked E51 monitor loop in hal_smode_boot refreshes all diff --git a/hal/mpfs250.h b/hal/mpfs250.h index a2310ff329..983ae62aed 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -130,7 +130,12 @@ #define MSS_WDT_STATUS(base) (*(volatile uint32_t*)((base) + 0x08)) #define MSS_WDT_TIME(base) (*(volatile uint32_t*)((base) + 0x0C)) #define MSS_WDT_MVRP(base) (*(volatile uint32_t*)((base) + 0x10)) -#define MSS_WDT_CTRL_ENABLE (1U << 0) +#define MSS_WDT_TRIGGER(base) (*(volatile uint32_t*)((base) + 0x14)) +/* STATUS bits (mss_watchdog.h): there is no enable bit anywhere - the MSS + * watchdogs are armed by the bitstream and count from reset. */ +#define MSS_WDT_STAT_FORBIDDEN (1U << 2) +#define MSS_WDT_STAT_LOCKED (1U << 4) +#define MSS_WDT_STAT_DEVRST (1U << 5) /* UART */ #define MSS_UART0_LO_BASE 0x20000000UL diff --git a/hal/mpfs250_ubl.c b/hal/mpfs250_ubl.c index 4888d0711e..76818bec22 100644 --- a/hal/mpfs250_ubl.c +++ b/hal/mpfs250_ubl.c @@ -52,11 +52,6 @@ static const unsigned long wdt_bases[MPFS_NUM_HARTS] = { MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE }; -/* Boot ROM E51 watchdog state, restored before the jump so the eNVM - * wolfBoot's hal_init() snapshots what the ROM left. */ -static uint32_t ubl_wdt_mvrp; -static uint32_t ubl_wdt_ctrl; - static uint64_t rdcycle(void) { uint64_t c; @@ -68,17 +63,11 @@ void hal_init(void) { unsigned int w; - ubl_wdt_mvrp = MSS_WDT_MVRP(MSS_WDT_E51_BASE); - ubl_wdt_ctrl = MSS_WDT_CONTROL(MSS_WDT_E51_BASE); - - /* Same sequence as the eNVM wolfBoot: no device reset on timeout and the - * widest window, so the signature check cannot be cut short. */ + /* Start every window from full, so the signature check cannot be cut + * short. The window itself is a design-time property and already the + * widest the counter allows; see the watchdog policy in hal/mpfs250.c. */ for (w = 0; w < MPFS_NUM_HARTS; w++) { MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; - MSS_WDT_TIME(wdt_bases[w]) = 0x00FFFFFFUL; - MSS_WDT_MVRP(wdt_bases[w]) = 0x00FFFFFFUL; - MSS_WDT_CONTROL(wdt_bases[w]) = 0; - MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; } #ifdef DEBUG_UART @@ -90,9 +79,8 @@ void hal_init(void) void hal_prepare_boot(void) { + /* Hand the eNVM wolfBoot a full window. */ MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; - MSS_WDT_MVRP(MSS_WDT_E51_BASE) = ubl_wdt_mvrp; - MSS_WDT_CONTROL(MSS_WDT_E51_BASE) = ubl_wdt_ctrl; } /* The manifest is verified in place; there is no flash backend. */ From d5f8b403a09968603676988aaca1427d5d01e69f Mon Sep 17 00:00:00 2001 From: David Garske Date: Mon, 19 Oct 2026 09:27:29 -0700 Subject: [PATCH 14/23] PolarFire SoC: correct the FIT staging address rationale in the M-mode config --- config/examples/polarfire_mpfs250_m.config | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index 33c26eb329..c566100cd0 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -89,11 +89,11 @@ STACK_SIZE?=64k WOLFBOOT_SECTOR_SIZE?=0x1000 # Scratch address where the signed FIT image is staged before signature -# verification + FIT parse. Placed early in DDR (32 MB into 2 GB) so we -# stay within the fully-trained region near 0x80000000 - the LPDDR4 TIP -# completes BCLK_SCLK only (train_stat=0x1) on this Video Kit and higher -# DDR addresses (e.g. 0x8E000000) have shown intermittent write -# corruption during long disk loads. +# verification + FIT parse. 0x82000000 is 32 MB into the cached DDR window +# at 0x80000000, which is 1 GB wide - 0xC0000000 up is the non-cached alias +# onto the same DDR, so staging must stay below it. It is clear of the +# kernel destination below and 128 MB below the DTB region, which is the +# headroom the staged-image bound further down depends on. # After the FIT is parsed: # kernel is copied to 0x80200000 (FIT-internal "load") # DTB is copied to WOLFBOOT_LOAD_DTS_ADDRESS (0x8A000000) From 6d52fb3bc82e8a0ac689fa04d1d784b450024216 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 20 Oct 2026 09:54:49 -0700 Subject: [PATCH 15/23] sdhci: service the watchdog and re-arm the timeout in the SDMA data phase --- include/sdhci.h | 8 ++++++++ src/sdhci.c | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/include/sdhci.h b/include/sdhci.h index 091b7feb41..ba94c7486e 100644 --- a/include/sdhci.h +++ b/include/sdhci.h @@ -115,6 +115,13 @@ #define SDHCI_DATA_TIMEOUT_US 3000000 /* 3000ms for data operations */ #endif +/* Polls between watchdog refreshes in the data-phase wait. The error + * recovery waits pet on every pass; this one runs on every transfer, so it + * is throttled to keep the refresh cost off the transfer time. */ +#ifndef SDHCI_WDT_PET_INTERVAL +#define SDHCI_WDT_PET_INTERVAL 4096U +#endif + /* Clock frequencies (kHz) */ #ifndef SDHCI_CLK_400KHZ #define SDHCI_CLK_400KHZ 400 @@ -234,6 +241,7 @@ #define SDHCI_SRS10_BVS_1_8V (0x5U << 9) #define SDHCI_SRS10_BVS_3_0V (0x6U << 9) #define SDHCI_SRS10_BVS_3_3V (0x7U << 9) +#define SDHCI_SRS10_DMA_MASK (0x3U << 3) /* DMA Select field */ #define SDHCI_SRS10_DMA_SDMA (0x0U << 3) /* SRS11 - Clock Control / Timeout / Software Reset */ diff --git a/src/sdhci.c b/src/sdhci.c index 492046a43d..55ecd0e1c1 100644 --- a/src/sdhci.c +++ b/src/sdhci.c @@ -248,6 +248,8 @@ static void sdhci_disable_sdma_interrupts(void) * - Polling mode: directly reads SRS12 status register and calls handler */ static int sdhci_wait_irq(uint32_t expected_flags, uint32_t timeout) { + uint32_t pet = 0; + while (timeout-- > 0) { /* Poll SRS12 directly for platforms without interrupt routing. * In interrupt mode this is redundant (bits already cleared by ISR). */ @@ -270,6 +272,12 @@ static int sdhci_wait_irq(uint32_t expected_flags, uint32_t timeout) return 0; } } + /* A watchdog that cannot be disabled counts from the last refresh, so + * an unserviced data phase resets the chip before the timeout below. */ + if (++pet >= SDHCI_WDT_PET_INTERVAL) { + pet = 0; + sdhci_platform_wdt_pet(); + } asm volatile("nop"); } return -1; /* Timeout */ @@ -1636,6 +1644,10 @@ static int sdhci_transfer(int dir, uint32_t cmd_index, uint32_t block_addr, * A64 is cleared in SRS15 to use 32-bit DMA addressing. * Note: Platform may redirect SRS22/SRS23 to SRS00 for legacy * SDMA on controllers that don't support HV4E. */ + /* DMA Select is a field, not a flag: SDHCI_SRS10_DMA_SDMA is 0, + * so an OR cannot clear a non-SDMA selection left by whatever + * ran before us. Clear the field first. */ + sdhci_reg_and(SDHCI_SRS10, ~SDHCI_SRS10_DMA_MASK); sdhci_reg_or(SDHCI_SRS10, SDHCI_SRS10_DMA_SDMA); sdhci_reg_or(SDHCI_SRS15, SDHCI_SRS15_HV4E); sdhci_reg_and(SDHCI_SRS15, ~SDHCI_SRS15_A64); @@ -1667,7 +1679,8 @@ static int sdhci_transfer(int dir, uint32_t cmd_index, uint32_t block_addr, if (cmd_reg & SDHCI_SRS03_DMAE) { /* DMA mode with interrupt support */ while (1) { - status = sdhci_wait_irq(SDHCI_IRQ_FLAG_TC, 0x00FFFFFF); + status = sdhci_wait_irq( + SDHCI_IRQ_FLAG_TC | SDHCI_IRQ_FLAG_DMAINT, 0x00FFFFFF); if (status != 0) { wolfBoot_printf("sdhci_transfer: SDMA timeout/error\n"); status = -1; @@ -1677,6 +1690,11 @@ static int sdhci_transfer(int dir, uint32_t cmd_index, uint32_t block_addr, g_mmc_irq_status &= ~SDHCI_IRQ_FLAG_TC; break; } + /* An SDMA buffer-boundary pause. The handler has re-armed the + * address, so loop and wait again. Each segment gets its own + * budget rather than one budget bounding the whole data phase, + * which is why the wait above services the watchdog. */ + g_mmc_irq_status &= ~SDHCI_IRQ_FLAG_DMAINT; } sdhci_disable_sdma_interrupts(); From 3019107dd8b293ebb0975f14af41b5350633dd40 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 20 Oct 2026 09:54:49 -0700 Subject: [PATCH 16/23] PolarFire SoC: pin the L2 scratchpad before any hart stores into it --- src/boot_riscv_start.S | 43 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 8a540728c0..821bb4e461 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -154,6 +154,23 @@ _reset: sw zero, 0x174(t1) /* SYSREG_L2_SHUTDOWN_CR = 0 */ fence + /* Evict any copy of the scratchpad range that an earlier access left in + * an evictable way. The pin loop below allocates by storing with the + * E51 mask set to one way at a time, but a store that HITS a line already + * resident in ways 0-7 updates that line instead, so the address never + * reaches a scratchpad way and reads as zero once a large DMA sweep + * evicts it. Under boot mode 2 the UBL and the released U54s run before + * this point with every mask wide open. FLUSH64 takes a line address + * and evicts it from whichever way holds it. */ + li t0, 0x02010200 /* L2_FLUSH64 */ + li t1, 0x0A000000 /* ZERO_DEVICE_BOTTOM */ + li t2, 0x0A080000 /* + 512 KB */ +.L_prepin_flush: + sd t1, 0(t0) + addi t1, t1, 64 + bltu t1, t2, .L_prepin_flush + fence + /* Make all 512 KB of the Zero Device resident by enabling one scratchpad * way at a time and storing a marker every 64 bytes: a store can only * allocate in the enabled way, so 2048 blocks x 4 ways fills each once. @@ -250,14 +267,14 @@ _copy_params: addi sp, sp, -64 /* allocate 64-byte HLS at top of stack */ mv s11, sp - sd zero, 0(s11) - sd zero, 8(s11) - sd zero, 16(s11) - sd zero, 24(s11) - sd zero, 32(s11) - sd zero, 40(s11) - sd zero, 48(s11) - sd zero, 56(s11) + /* The HLS block is zeroed AFTER the wait below, not here. It lives in + * L2 scratchpad, and the boot hart does not finish carving the + * scratchpad out (the way-mask pin loop, then mpfs_config_l2_cache) + * until well after it releases us. Under boot mode 2 the UBL releases + * the U54s and jumps last, so a store here is guaranteed to land before + * the scratchpad exists; in boot mode 1 the System Controller starts all + * five harts together and the boot hart merely usually wins. Only + * register setup happens before the wait. */ /* Wait for E51 to signal HLS_MAIN_HART_STARTED. On MPFS250 poll the * DTIM copy of the flag, not the L2-scratch HLS: an E51 store to the @@ -288,6 +305,16 @@ _copy_params: add t0, t0, t1 sw zero, 0(t0) /* clear stale MSIP */ + /* Scratchpad is resident now; zero the HLS block, then publish. */ + sd zero, 0(s11) + sd zero, 8(s11) + sd zero, 16(s11) + sd zero, 24(s11) + sd zero, 32(s11) + sd zero, 40(s11) + sd zero, 48(s11) + sd zero, 56(s11) + li t0, 0x12345678 /* HLS_OTHER_HART_IN_WFI */ sw t0, 0(s11) fence iorw, iorw From 064a102bbe0daec1aed87dc8417fae3052d32861 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 20 Oct 2026 13:17:51 -0700 Subject: [PATCH 17/23] boot_riscv: replace a non-ASCII dash in a comment --- src/boot_riscv_start.S | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 821bb4e461..937f5460b0 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -349,7 +349,7 @@ _copy_params: la t0, secondary_hart_entry jr t0 -/* .text section follows — runs from L2 SRAM after copy from eNVM */ +/* .text section follows - runs from L2 SRAM after copy from eNVM */ .section .text .L_sram_entry: la t0, trap_vector_table From 5ae41920bfec14e8cdad00024b3c33183107ef49 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 22 Oct 2026 07:06:17 -0700 Subject: [PATCH 18/23] PolarFire SoC: drop development narrative from the DDR and SEG comments --- hal/mpfs250.h | 7 ++- hal/mpfs250_ddr.c | 132 +++++++++++++++++----------------------------- 2 files changed, 50 insertions(+), 89 deletions(-) diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 983ae62aed..95d7f4f0b6 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -683,10 +683,9 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); * SEG is a 256-byte-stride peripheral pair (mss_seg.h:54): seg_t has * 8 x u32 control regs + 56 x u32 fill = 256 B. SEG[0] is at base * (DDR_SEG_BASE = 0x20005D00); SEG[1] is at base + 0x100 (= 0x20005E00). - * Phase 3.10.3 (A) finding: wolfBoot previously put SEG1_X at offset - * 0x20-0x3C (overwriting unrelated registers), so the SEG1 cached/ - * non-cached DDR address-mapping registers were never written -- - * 0x80000000 stores faulted with cause=7 (store access fault). */ + * SEG1 carries the cached/non-cached DDR address mapping, so an offset + * short of 0x100 leaves it unwritten and a 0x80000000 store faults with + * cause=7 (store access fault). */ #define SEG0_0 0x00 #define SEG0_1 0x04 #define SEG0_2 0x08 diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index 931868edef..7c838608ac 100644 --- a/hal/mpfs250_ddr.c +++ b/hal/mpfs250_ddr.c @@ -51,11 +51,9 @@ static void ddr_delay(uint32_t us) } } -/* Pet all five MSS watchdogs (E51 + U54_1..4) by writing the refresh - * magic to each WDT refresh register. Used inside the long DDR training - * sweeps so a wedged MTC test cannot let the WDT reset the chip before a - * sweep finishes with a clean FAIL status. Identical 5-write sequence - * previously inlined at four call sites. */ +/* Pet all five MSS watchdogs (E51 + U54_1..4). Used inside the long DDR + * training sweeps so a wedged MTC test cannot let the WDT reset the chip + * before a sweep finishes with a clean FAIL status. */ #define MPFS_DDR_PET_WDT() \ do { \ *(volatile uint32_t*)0x20001000UL = 0xDEADC0DEU; /* E51 */ \ @@ -294,13 +292,10 @@ static int mss_pll_init(void) #endif /* Reprogram the eNVM clock divider for the new (faster) AHB - * clock BEFORE switching, exactly as HSS does in + * clock BEFORE switching, as HSS does in * mss_mux_post_mss_pll_config(): write LIBERO's ENVM_CR, then - * poll clock-okay. wolfBoot previously never wrote ENVM_CR, - * leaving the reset divider while AHB jumped 40 -> 150 MHz, - * so every later eNVM read (e.g. the secondary harts' WFI - * park loop instruction fetches) ran with out-of-spec eNVM - * read timing. */ + * poll clock-okay. The reset divider is out of spec for eNVM + * reads once AHB goes from 40 to 150 MHz. */ envm_to = 1000000UL; #ifdef LIBERO_SETTING_MSS_ENVM_CR SYSREG_ENVM_CR = (uint32_t)LIBERO_SETTING_MSS_ENVM_CR; @@ -1131,14 +1126,13 @@ static int setup_phy(void) DDRPHY_REG(PHY_RPC98_IBUFMD_DQS) = LIBERO_SETTING_RPC_IBUFMD_DQS; mb(); - /* Phase 3.10.3 (1a): per-lane weak pull-up/pull-down config. - * HSS calls config_ddr_io_pull_up_downs_rpc_bits() right after - * set_ddr_rpc_regs() returns (mss_ddr.c:2609 -> 4551). wolfBoot - * was missing this entirely. These 24 registers configure I/O - * override enable per lane (ovrt9-16) and weak pull-up/pull-down - * enables for ADDCMD/DATA/ECC lanes (rpc235-250). Without them, - * lane termination is in an undefined state and TIP cannot - * reliably detect DQ/DQS transitions during WRLVL. + /* Per-lane weak pull-up/pull-down config, as HSS does in + * config_ddr_io_pull_up_downs_rpc_bits() right after set_ddr_rpc_regs() + * returns (mss_ddr.c:2609 -> 4551). These 24 registers set the I/O + * override enable per lane (ovrt9-16) and the weak pull enables for the + * ADDCMD/DATA/ECC lanes (rpc235-250); without them lane termination is + * undefined and TIP cannot reliably detect DQ/DQS transitions during + * WRLVL. */ /* Independent PHY register writes (no interleaved reads / barriers); * one mb() after the loop matches the original single mb(). ovrt9-16 @@ -1400,8 +1394,7 @@ static uint8_t mpfs_write_calibration_using_mtc(uint8_t num_lanes) * lanes get their correct calibration committed. Without this * call PHY retains the last sweep iteration's cal_data (junk like * 0xEEEEE) and ALL lanes write incorrectly, even the ones that - * passed the sweep. Previously we returned early on partial - * failure -> wrong-data on every lane. */ + * passed the sweep. */ mpfs_set_write_calib(num_lanes, lane_lower); DBG_DDR( " MTC WRCALIB: lanes(%u%u%u%u%u) cal=0x%x status=0x%x\n", @@ -1576,13 +1569,11 @@ static void training_config_wrlvl(void) static void training_ca_vref(void) { uint32_t dpc_bits_new; - /* 2026-05-11: re-enabled the CA VREF dual-pass sweep. - * Previously hardcoded to 0x10, but HSS finds different - * vref_answer values per board (HSS log on this Video Kit - * found 0x07). Hardcoding leaves the PHY at a non-optimal - * VREF where ADDCMD sweep is noisy and writes corrupt. The - * sweep below is a port of HSS lpddr4_manual_training lines - * 1005-1185 (the dual ca_indly x vref loop). */ + /* The sweep below is a port of HSS lpddr4_manual_training lines + * 1005-1185 (the dual ca_indly x vref loop). HSS finds a different + * vref_answer per board (0x07 on this Video Kit), so a hardcoded value + * leaves the PHY at a non-optimal VREF where the ADDCMD sweep is noisy + * and writes corrupt. */ uint32_t vref_answer = 128; /* 128 = no answer found */ uint32_t transition_a5_min_last = 129; uint32_t ca_indly; @@ -1729,13 +1720,6 @@ static void training_ca_vref(void) break; } - /* Phase 3.10.3 iter 5 tried adding HSS's CA-VREF expert-mode - * setup writes here (expert_mode_en = 0x21, - * expert_dfi_status_override_to_shim = 0, - * expert_dlycnt_pause toggle). No effect on TIP advancement - * and pattern test mismatches stayed in the elevated 4-5k - * range vs the ~1200 baseline. Reverted. */ - /* Apply final VREF value */ DDR_BANKCONT_REG(0x00) = 0U; ddr_delay(50); @@ -2602,13 +2586,9 @@ static int run_training(uint32_t retry_count) timeout--; } DDRCFG_REG(MC_INIT_ZQ_CAL_START) = 0x00000000UL; - /* 2026-05-12: match HSS Libero value (CFG_AUTO_ZQ_CAL_EN=0). - * Previously hardcoded to 1; reverted in earlier session because - * "pattern test slightly worse and train_stat unchanged". Now - * with the ADDCMD threshold fix (5e27fcb4), reconsider: auto ZQ - * cal injects ZQ commands into the DRAM stream, which can - * collide with data bursts. HSS disables auto cal and relies on - * the explicit INIT_ZQ_CAL_START at init time only. */ + /* HSS's Libero value disables auto ZQ cal and relies on the explicit + * INIT_ZQ_CAL_START above: auto cal injects ZQ commands into the DRAM + * stream, where they can collide with data bursts. */ DDRCFG_REG(MC_CFG_AUTO_ZQ_CAL_EN) = LIBERO_SETTING_CFG_AUTO_ZQ_CAL_EN; mb(); @@ -2659,12 +2639,11 @@ static int run_training(uint32_t retry_count) DDRCFG_REG(0x10034U), DDRCFG_REG(0x10038U)); /* - * Restore ODT after TIP completes. Phase 3.10.3 D-3 v2 audit - * (2026-05-05) found that the previous explicit MR2 = 0x2D write - * with MR_WR_MASK = 0 was clobbering all 20 LPDDR4 MR2 bits and - * breaking the post-training mode -- HSS never writes MR2 like - * this; it only does the no-op refresh via mode_register_masked_ - * write_x5(2) which uses MR_WR_MASK = 0xFFFFF (preserve). Removed. + * Restore ODT after TIP completes. MR2 is deliberately not rewritten + * here: HSS only does the no-op refresh via + * mode_register_masked_write_x5(2), which keeps MR_WR_MASK = 0xFFFFF, + * and an explicit MR2 write with MR_WR_MASK = 0 clobbers all 20 LPDDR4 + * MR2 bits and breaks the post-training mode. */ DDRPHY_REG(PHY_RPC3_ODT) = 0x03U; mb(); @@ -2704,21 +2683,15 @@ static int run_training(uint32_t retry_count) eye[0], eye[1], eye[2], eye[3]); (void)eye; - /* Run HSS-equivalent MTC-based write calibration when TIP reached - * train_stat=0x1D. Previously skipped on the assumption that TIP - * "did its own WRCALIB during autonomous training" -- that turned - * out to be wrong. HSS runs write_calibration_using_mtc() even - * after a successful TIP, because TIP only trains the PHY; the MTC - * test is the only thing that verifies the CPU->AXI->DDRC->DRAM - * data path actually moves bits. Without this step, - * train_stat=0x1D + MTC 256B PASS were both passing while - * boundary-scan reads from cached/non-cached DDR returned the - * same stuck pre-fill bytes regardless of address. */ + /* HSS runs write_calibration_using_mtc() even after a successful TIP, + * because TIP only trains the PHY; the MTC test is the only thing that + * verifies the CPU->AXI->DDRC->DRAM path actually moves bits. Without + * it train_stat=0x1D and an MTC 256B PASS both report success while + * every DDR read returns the same stuck pre-fill bytes. */ train_stat = DDRPHY_REG(PHY_TRAINING_STATUS); - /* Run HSS-style MTC WRCALIB whenever TIP made any progress - * (train_stat has at least BCLK_SCLK set). Previously only ran - * when fully 0x1D; in practice our train_stat often stalls at 0x1 - * but MTC still works enough to do per-lane calibration. */ + /* Run it whenever TIP made any progress (at least BCLK_SCLK set), not + * only on a full 0x1D: train_stat often stalls earlier while MTC still + * calibrates per lane. */ if ((train_stat & BCLK_SCLK_BIT) != 0U) { uint8_t wrcal_res; uint32_t init_to; @@ -3059,7 +3032,7 @@ int mpfs_ddr_init(unsigned int outer_retry) mpfs_phy_rpc156_val = 6U; - (void)outer_retry; /* TUNE sweep removed; outer_retry kept for future use */ + (void)outer_retry; /* Step 1: NWC/PLL initialization. Run only once per boot -- the * MSS / DDR PLLs lock on first call and re-running mss_pll_init() @@ -3102,13 +3075,9 @@ int mpfs_ddr_init(unsigned int outer_retry) if (ret != 0) DBG_DDR("DDR: PHY setup warning\n"); - /* Step 6: Configure controller timing (CFG_* registers). HSS - * runs init_ddrc here in DDR_TRAINING_SETUP_DDRC state, after - * DDR_TRAINING_DDRC_BRINGUP and BANK_CONTROLLER soft_reset. - * Tested also calling this BEFORE setup_phy -- no AXI service - * improvement. Tested forcing EXPERT_WRCALIB=0x5555 (HSS - * post-train value) -- no improvement. Tested clearing - * INIT_SELF_REFRESH -- was already 0. */ + /* Step 6: Configure controller timing (CFG_* registers). HSS runs + * init_ddrc here in DDR_TRAINING_SETUP_DDRC state, after + * DDR_TRAINING_DDRC_BRINGUP and the BANK_CONTROLLER soft reset. */ setup_controller(); /* Step 7: Training reset and clock rotation */ @@ -3125,8 +3094,7 @@ int mpfs_ddr_init(unsigned int outer_retry) * calibrate all lanes), not PHY_TRAINING_STATUS: when the manual ADDCMD * training picks a marginal phase/dly that doesn't resolve into a usable * DRAM alignment, TIP's train_stat self-report can still read "complete" - * even though the write path is bad. Gating on train_stat alone let bad - * boots through and the 19 MB load then hard-failed every block. + * even though the write path is bad. * * MTC sanity is a secondary gate, run only when TIP did not report full * training (0x1C). When TIP does complete full training, MTC is skipped: @@ -3259,13 +3227,10 @@ int mpfs_ddr_init(unsigned int outer_retry) /* Fast path: if TIP completed full training (train_stat * 0x1C bits = WRLVL+RDGATE+DQ_DQS), skip the MTC sanity - * test. MTC engine still TIMEOUTs every iteration even - * after the MC_BASE1 BIT_MAP_INDEX offset bugfix -- the - * MTC engine has a separate issue (DDRC-internal access - * path differs from external AXI). Running it just - * burns all 3 retries and the outer 6-retry loop, ending - * with a WDT reset. Accept TIP-side training and proceed - * to disk-load; the actual AXI reads are independent. */ + * test. The MTC engine reaches the DRAM over a + * DDRC-internal path, not the external AXI the loader + * uses, and times out every iteration on this part; + * running it burns every retry and ends in a WDT reset. */ if ((train_stat & 0x1CU) == 0x1CU) { DBG_DDR(" TIP full training success (0x%x) - skipping MTC sanity\n", train_stat); @@ -3351,12 +3316,9 @@ int mpfs_ddr_init(unsigned int outer_retry) return -4; } - /* PDMA pre-fill: HSS clear_bootup_cache_ways equivalent. With - * MTC traffic injection (commit b4031038) WRLVL now trains real - * per-lane delays and the AXI port no longer hangs, so run this - * unconditionally to prime DDRC row buffers before memory_test. - * Small region (1 MB) to keep boot fast and minimize risk if the - * underlying address-decoder issue is still affecting writes. */ + /* PDMA pre-fill, the HSS clear_bootup_cache_ways equivalent: primes the + * DDRC row buffers before the memory test. 1 MB is enough for that and + * keeps the boot short. */ mpfs_clear_bootup_cache_ways(0xC0000000UL, 1UL * 1024UL * 1024UL); /* The first transaction the CPU side sends to the controller after init From 05933de8eaa222ae6bb48bc10d2530d858b5ff5c Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 22 Oct 2026 07:21:12 -0700 Subject: [PATCH 19/23] PolarFire SoC: make the Athena hash offload selectable with MPFS_ATHENA_SHA --- docs/PolarFire.md | 2 +- hal/mpfs250.c | 28 +++++++++++++++++++++++++--- options.mk | 22 +++++++++++++++++++--- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/PolarFire.md b/docs/PolarFire.md index 2ee91a7ed4..5ccbd9461b 100644 --- a/docs/PolarFire.md +++ b/docs/PolarFire.md @@ -1203,7 +1203,7 @@ On an encrypted image the decrypt dominates everything else, and the Athena F520 | Integrity SHA-384 | 4.29 s | 5.70 s | | Verify ECC384 | 0.67 s | 0.67 s | -The AES offload is worth about 4.2x on the decrypt. **The hash offload is about 1.3x slower than software** over an image this size: the engine's per-block setup dominates once the data is in DDR rather than a small buffer in L2 scratch, so enabling it costs boot time rather than saving it. `MPFS_ATHENA=1` turns on the hash offload and `ENCRYPT_WITH_AES256=1` adds the AES offload to it, so an encrypted build that wants only the AES win cannot currently express that. +The AES offload is worth about 4.2x on the decrypt. **The hash offload is about 1.3x slower than software** over an image this size: the engine's per-block setup dominates once the data is in DDR rather than a small buffer in L2 scratch, so enabling it costs boot time rather than saving it. `MPFS_ATHENA_SHA=0` keeps the AES offload and returns hashing to software, which is the fastest arrangement for an encrypted image at 9.5 s; `MPFS_ATHENA` with neither offload selected is refused. Note that hashing in software under `MPFS_ATHENA_SHA=0` is still about 9% slower than a build with no crypto callback at all, because every block enters the callback and is turned away. Athena also does not fit eNVM alongside the full feature set: with the manifest linked it needs both `GZIP` and `MPFS_SBI_SC_FLASH` dropped. Dropping `GZIP` costs nothing when the FIT's kernel is stored uncompressed. diff --git a/hal/mpfs250.c b/hal/mpfs250.c index e61ace94b5..0f7027c0d7 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -503,6 +503,7 @@ static int mpfs_athena_aes256_ctr(const void *key, void *iv, const void *in, /* SHA-384 + AES-256-CTR via the plain-C mpfs_athena_* API (CAL headers cannot * share a TU with wolfSSL's). Undocumented CAL contract: a non-final update * must be whole 128-byte blocks, so short tails need the buffer below. */ +#ifdef MPFS_ATHENA_SHA #define ATHENA_SHA384_BLOCK 128 #define ATHENA_HASH_SLOTS 2 @@ -516,6 +517,7 @@ struct athena_hash_slot { }; static struct athena_hash_slot athena_hash_slots[ATHENA_HASH_SLOTS]; +#endif /* MPFS_ATHENA_SHA */ /* Set once the self-check has passed: from then on a SHA-384 request the * engine cannot take is a hardware error, never a silent software fallback. */ @@ -525,6 +527,7 @@ static int athena_ready; * digest cannot be mistaken for hardware use when the callback never ran. */ static uint32_t athena_cb_calls; +#ifdef MPFS_ATHENA_SHA /* Keyed by wc_Sha384 address; the cryptocb has no free event, so a context * freed without Final strands its slot and a later one at that address would * resume stale CAL state (wrong digest). Only reachable via ONESHOT today. */ @@ -581,6 +584,7 @@ static int athena_hash_feed(struct athena_hash_slot *slot, const uint8_t *in, } return 0; } +#endif /* MPFS_ATHENA_SHA */ #if defined(MPFS_ATHENA_AES) && defined(WOLFSSL_AES_COUNTER) && !defined(NO_AES) /* AES-256-CTR, one path for both directions. Key from aes->devKey, not the @@ -652,7 +656,9 @@ static int athena_aesctr(wc_CryptoInfo *info) static int mpfs_athena_cryptocb(int devIdArg, wc_CryptoInfo *info, void *ctx) { +#ifdef MPFS_ATHENA_SHA struct athena_hash_slot *slot; +#endif (void)devIdArg; (void)ctx; @@ -667,6 +673,9 @@ static int mpfs_athena_cryptocb(int devIdArg, wc_CryptoInfo *info, void *ctx) } #endif +#ifndef MPFS_ATHENA_SHA + return CRYPTOCB_UNAVAILABLE; +#else if (info->algo_type != WC_ALGO_TYPE_HASH || info->hash.type != WC_HASH_TYPE_SHA384) { return CRYPTOCB_UNAVAILABLE; @@ -724,8 +733,10 @@ static int mpfs_athena_cryptocb(int devIdArg, wc_CryptoInfo *info, void *ctx) } return 0; +#endif /* MPFS_ATHENA_SHA */ } +#ifdef MPFS_ATHENA_SHA /* Known answers for the self-check below. SHA-384 of the bytes 0x00..0xFF: * a multi-block vector, hashed in chunks that straddle the 128-byte block so * the intermediate state has to be carried across calls. */ @@ -737,6 +748,7 @@ static const uint8_t athena_kat_sha384_256b[48] = { 0x28, 0x92, 0xe9, 0x17, 0x9b, 0x34, 0x63, 0x23, 0x31, 0xa5, 0x95, 0x92, 0x73, 0x7d, 0xb5, 0xc5 }; +#endif /* MPFS_ATHENA_SHA */ #ifdef MPFS_ATHENA_AES /* AES-256-CTR of the bytes 0x00..0x1F under key 0x00..0x1F, counter 0x00..0x0F. */ static const uint8_t athena_kat_aesctr[32] = { @@ -781,9 +793,11 @@ static int athena_ctr_run(const uint8_t *key, const uint8_t *ctr, static int mpfs_athena_selfcheck(void) { +#ifdef MPFS_ATHENA_SHA wc_Sha384 sha; - uint8_t buf[256]; uint8_t digest[48]; +#endif + uint8_t buf[256]; #ifdef MPFS_ATHENA_AES uint8_t key[32], ctr[16], out[32], out2[32]; #endif @@ -797,6 +811,7 @@ static int mpfs_athena_selfcheck(void) for (i = 0; i < (int)sizeof(buf); i++) { buf[i] = (uint8_t)i; } +#ifdef MPFS_ATHENA_SHA athena_cb_calls = 0; ret = wc_InitSha384_ex(&sha, NULL, WOLFBOOT_DEVID_HASH); if (ret != 0) { @@ -833,6 +848,9 @@ static int mpfs_athena_selfcheck(void) athena_fail_what = "SHA-384 digest mismatch"; return -1; } +#else + ret = 0; +#endif /* MPFS_ATHENA_SHA */ #ifdef MPFS_ATHENA_AES for (i = 0; i < 32; i++) { @@ -886,12 +904,14 @@ static int mpfs_athena_selfcheck(void) * crypto unnoticed: every failure here halts. */ static void mpfs_athena_init(void) { +#ifdef MPFS_ATHENA_SHA int i; for (i = 0; i < ATHENA_HASH_SLOTS; i++) { athena_hash_slots[i].owner = NULL; athena_hash_slots[i].part_len = 0; } +#endif /* Free slots hold INVALID_DEVID (-2), not 0, so registration returns * BUFFER_E until the zero-initialised device table is initialised. */ @@ -920,10 +940,12 @@ static void mpfs_athena_init(void) wolfBoot_panic(); } athena_ready = 1; -#ifdef MPFS_ATHENA_AES +#if defined(MPFS_ATHENA_SHA) && defined(MPFS_ATHENA_AES) wolfBoot_printf("Athena: SHA-384 + AES-256-CTR offload active\n"); -#else +#elif defined(MPFS_ATHENA_SHA) wolfBoot_printf("Athena: SHA-384 offload active\n"); +#else + wolfBoot_printf("Athena: AES-256-CTR offload active\n"); #endif } #endif /* MPFS_ATHENA */ diff --git a/options.mk b/options.mk index 6238b722f1..855e6d326d 100644 --- a/options.mk +++ b/options.mk @@ -1871,10 +1871,18 @@ ifeq ($(MPFS_ATHENA),1) CFLAGS += -DWOLFBOOT_DEVID_HASH=0xA7 CFLAGS += -DWOLFBOOT_DEVID_CRYPT=0xA7 WOLFCRYPT_OBJS += $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/cryptocb.o - # The offload and its self-check are SHA-384; an image hashed with + # SHA-384 offload. Over a whole image in DDR it is slower than the software + # hash, so an encrypted build wanting only the AES win sets MPFS_ATHENA_SHA=0. + MPFS_ATHENA_SHA ?= 1 + ifeq ($(MPFS_ATHENA_SHA),1) + CFLAGS += -DMPFS_ATHENA_SHA + endif + # The hash offload and its self-check are SHA-384; an image hashed with # something else keeps that in software and still gets the AES offload. - ifneq ($(HASH),SHA384) - AUX_HASH_ALGOS+=sha384 + ifeq ($(MPFS_ATHENA_SHA),1) + ifneq ($(HASH),SHA384) + AUX_HASH_ALGOS+=sha384 + endif endif # Archive name differs between Microchip's two published builds. MPFS_ATHENA_CAL_LIB ?= mpfs-rv64imac-user-crypto-lib.a @@ -1887,6 +1895,14 @@ ifeq ($(MPFS_ATHENA),1) CFLAGS += -DMPFS_ATHENA_AES endif endif + # With neither offload selected the engine is brought up and self-checked for + # nothing, so say so rather than linking CAL into a build that cannot use it. + ifneq ($(MPFS_ATHENA_SHA),1) + ifneq ($(MPFS_ATHENA_AES),1) + $(error MPFS_ATHENA=1 with MPFS_ATHENA_SHA=0 and no AES offload leaves \ + nothing offloaded: drop MPFS_ATHENA, or set ENCRYPT_WITH_AES256=1) + endif + endif endif # Size of the wolfHSM comm data payload, shared by the client and server blocks From 77ac69f29ab2250d52ddf33a7bc0c13e786f5d11 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 22 Oct 2026 19:11:26 -0700 Subject: [PATCH 20/23] PolarFire SoC: skip a watchdog refresh inside the forbidden window --- .gitignore | 2 + docs/PolarFire.md | 8 +- hal/mpfs250.c | 62 +++++---- hal/mpfs250.h | 5 + hal/mpfs250_ubl.c | 22 +++- tools/ci/check-mpfs-wdt-gate.py | 84 ++++++++++++ tools/unit-tests/Makefile | 28 ++++ tools/unit-tests/unit-mpfs-wdt.c | 211 +++++++++++++++++++++++++++++++ 8 files changed, 386 insertions(+), 36 deletions(-) create mode 100755 tools/ci/check-mpfs-wdt-gate.py create mode 100644 tools/unit-tests/unit-mpfs-wdt.c diff --git a/.gitignore b/.gitignore index 355f2597e2..b1ab875328 100644 --- a/.gitignore +++ b/.gitignore @@ -637,6 +637,8 @@ tools/unit-tests/stm32g0_write_extract.h tools/unit-tests/ct_compare_extract.h tools/unit-tests/fdt_memrsv_extract.h tools/unit-tests/hifive1_flash_write_extract.h +tools/unit-tests/mpfs250_wdt_extract.h +tools/unit-tests/unit-mpfs-wdt tools/unit-tests/hifive1_hfrosccfg_extract.h tools/unit-tests/keystore_api_extract.h tools/unit-tests/keystore_emitted.c diff --git a/docs/PolarFire.md b/docs/PolarFire.md index 5ccbd9461b..e69e6a0f1d 100644 --- a/docs/PolarFire.md +++ b/docs/PolarFire.md @@ -616,7 +616,13 @@ the first write to any of offsets `0x00`-`0x0C` locks TRIGGER, MVRP and TIME unt system reset - a refresh is such a write. On the Video Kit they read TIME `0xFFFFF0`, MVRP `0x989680`, TRIGGER `0x3E8`, CONTROL `0x02`, which is already the longest window the 24-bit counter allows: the counter runs at the APB clock divided by 256, so a reload lasts about 28.6 s. -Refreshing them is therefore the only control wolfBoot has. After hand-off the parked +Refreshing them is therefore the only control wolfBoot has, and only once the counter has +dropped below MVRP: a refresh above it falls in the forbidden window, which the hardware +answers with a chip reset rather than an error (STATUS bit 2). An M-mode build refreshes +after setting MVRP itself and never lands there, but a build running as an HSS payload +inherits the supervisor's window, so `mpfs_wdt_pet_all()` tests that bit per watchdog and +skips any refresh that would reset the board; the window is whole seconds wide and every +caller loops, so a skipped refresh costs nothing. After hand-off the parked E51 acts as a monitor and refreshes all five watchdogs; the OS watchdog driver is disabled via the dtb fixup so the two never conflict. `MPFS_WDT_NODES_KEEP=1` hands the watchdogs to the OS instead: the dtb nodes stay enabled and the parked E51 stops refreshing, so the OS driver arms diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 0f7027c0d7..17a11260a7 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -90,6 +90,11 @@ extern void sdhci_irq_handler(void); * HSS) and is updated when M-mode wolfBoot raises the MSS PLL. */ uint32_t mpfs_apb_clk_hz = MSS_APB_AHB_CLK; +static const unsigned long mpfs_wdt_bases[MPFS_NUM_HARTS] = { + MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, + MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE +}; + #ifdef WOLFBOOT_RISCV_MMODE /* CPU frequency in MHz for mcycle-based udelay(); seeded at the E51 reset * clock and bumped to the PLL rate after mss_pll_init() (hal/mpfs250_ddr.c). @@ -105,13 +110,6 @@ const uint8_t __attribute__((section(".envm_manifest"), used)) { [0 ... MPFS_ENVM_MANIFEST_SIZE - 1] = 0xFF }; #endif -#ifdef WOLFBOOT_RISCV_MMODE -static const unsigned long mpfs_wdt_bases[MPFS_NUM_HARTS] = { - MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, - MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE -}; -#endif - /* Snapshots captured at hal_init entry, printed after uart_init; * RESET_SR shows the cause of the most-recent reset. */ static uint32_t mpfs_boot_wdt_snap[6]; @@ -2031,6 +2029,26 @@ static int mpfs_scb_wait_clear(uint32_t reg_offset, uint32_t mask, return (timeout == 0) ? -1 : 0; } +/* Refresh all five MSS watchdogs, skipping any whose counter is still above + * MVRP. A refresh inside that forbidden window is answered with a chip reset + * rather than an error. An M-mode build sets MVRP itself and never lands + * there, but one running under a supervisor that owns the window - wolfBoot as + * an HSS payload - finds every refresh inside it, so each watchdog is tested + * rather than assumed. Skipping one is safe: the window is whole seconds wide + * and every caller loops. */ +void mpfs_wdt_pet_all(void) +{ + unsigned long b; + unsigned int i; + + for (i = 0; i < MPFS_NUM_HARTS; i++) { + b = mpfs_wdt_bases[i]; + if ((MSS_WDT_STATUS(b) & MSS_WDT_STAT_FORBIDDEN) == 0U) { + MSS_WDT_REFRESH(b) = 0xDEADC0DEU; + } + } +} + #if defined(EXT_FLASH) || defined(MPFS_SBI_SC_FLASH) /* MSS QSPI (0x21000000) and SC QSPI (0x37020100) are both CoreQSPI v2 with * identical register layouts; MPFS_SC_SPI picks one by changing QSPI_BASE. */ @@ -2606,13 +2624,9 @@ static void qspi_uart_program(void) int ret; /* The MSS WDTs always count and reset the chip at timeout (~28.6s * at the reset divisor) and cannot be disabled, so a transfer of - * more than a few tens of KB outlives the period: refresh all - * five per sector here and per chunk below. */ - MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_1_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_2_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_3_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_4_BASE) = 0xDEADC0DEU; + * more than a few tens of KB outlives the period: refresh per sector + * here and per chunk below. */ + mpfs_wdt_pet_all(); ret = ext_flash_erase(addr + s * FLASH_SECTOR_SIZE, FLASH_SECTOR_SIZE); if (ret < 0) { @@ -2634,11 +2648,7 @@ static void qspi_uart_program(void) if (chunk_len > QSPI_PROG_CHUNK) chunk_len = QSPI_PROG_CHUNK; - MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_1_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_2_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_3_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_4_BASE) = 0xDEADC0DEU; + mpfs_wdt_pet_all(); uart_qspi_tx(QSPI_PROG_ACK); /* request next chunk */ @@ -2947,17 +2957,13 @@ static void mpfs_mpu_init_mmc(void) } #endif /* MPFS_DDR_INIT */ -/* Pet all five MSS watchdogs during the SDHCI read loops. - * Overrides the weak no-op in src/sdhci.c. The MSS watchdogs always count - * and reset the chip at timeout and cannot be disabled, so the multi-second - * load of a large image must keep refreshing them. */ +/* Pet all five MSS watchdogs during the SDHCI read loops. Overrides the weak + * no-op in src/sdhci.c. The MSS watchdogs always count and reset the chip at + * timeout and cannot be disabled, so the multi-second load of a large image + * must keep refreshing them. */ void sdhci_platform_wdt_pet(void) { - MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_1_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_2_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_3_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_4_BASE) = 0xDEADC0DEU; + mpfs_wdt_pet_all(); } void sdhci_platform_init(void) diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 95d7f4f0b6..0a87a8c0c9 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -773,6 +773,11 @@ void mpfs_iomux_init(void); * timeout. */ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); #endif + +/* Refreshes every MSS watchdog outside its forbidden window. Defined + * unconditionally in hal/mpfs250.c: the SDHCI and QSPI-programmer callers + * exist in the S-mode builds too, where MPFS_DDR_INIT is not set. */ +void mpfs_wdt_pet_all(void); #endif /* __ASSEMBLER__ */ #if defined(EXT_FLASH) || defined(MPFS_SBI_SC_FLASH) diff --git a/hal/mpfs250_ubl.c b/hal/mpfs250_ubl.c index 76818bec22..baae7a3b5a 100644 --- a/hal/mpfs250_ubl.c +++ b/hal/mpfs250_ubl.c @@ -59,16 +59,24 @@ static uint64_t rdcycle(void) return c; } -void hal_init(void) +/* Refresh every watchdog whose counter has dropped below MVRP. At reset MVRP + * leaves most of the period forbidden, and a refresh there resets the chip, so + * the stub tests each one. Skipping is harmless here: the stub starts with a + * fresh window and the signature check is far shorter than the period. */ +static void ubl_wdt_pet_all(void) { unsigned int w; - /* Start every window from full, so the signature check cannot be cut - * short. The window itself is a design-time property and already the - * widest the counter allows; see the watchdog policy in hal/mpfs250.c. */ for (w = 0; w < MPFS_NUM_HARTS; w++) { - MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; + if ((MSS_WDT_STATUS(wdt_bases[w]) & MSS_WDT_STAT_FORBIDDEN) == 0U) { + MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; + } } +} + +void hal_init(void) +{ + ubl_wdt_pet_all(); #ifdef DEBUG_UART SYSREG_SUBBLK_CLOCK_CR |= (MSS_PERIPH_MMUART0 << DEBUG_UART_PORT); @@ -79,8 +87,8 @@ void hal_init(void) void hal_prepare_boot(void) { - /* Hand the eNVM wolfBoot a full window. */ - MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; + /* Hand the eNVM wolfBoot as much window as the counter permits. */ + ubl_wdt_pet_all(); } /* The manifest is verified in place; there is no flash backend. */ diff --git a/tools/ci/check-mpfs-wdt-gate.py b/tools/ci/check-mpfs-wdt-gate.py new file mode 100755 index 0000000000..703ff363c1 --- /dev/null +++ b/tools/ci/check-mpfs-wdt-gate.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Check that no MSS watchdog refresh can land in the forbidden window. + +A write to MSS_WDT_REFRESH while the counter is above MVRP is the forbidden +window, and the hardware answers it with a chip reset rather than an error. +Only two kinds of site may write it: the gate helpers, which test +MSS_WDT_STAT_FORBIDDEN first, and code that only ever runs in M-mode, where +wolfBoot programs MVRP itself so no forbidden window exists. + +This has already been reintroduced in three separate places, so the rule is +checked rather than remembered. + + usage: check-mpfs-wdt-gate.py [hal dir] +""" + +import re +import sys + +GATES = ("mpfs_wdt_pet_all", "ubl_wdt_pet_all") +MMODE = "WOLFBOOT_RISCV_MMODE" + +FUNC = re.compile(r"^(?:static\s+)?(?:void|int|long|unsigned\s+\w+)\s+" + r"(?:RAMFUNCTION\s+)?(\w+)\s*\(") + + +def mmode_guard(line): + """True when this #if directive selects M-mode-only code.""" + return MMODE in line and not re.search(r"!\s*defined\(%s" % MMODE, line) \ + and not line.startswith("#ifndef %s" % MMODE) + + +def check(path): + bad = [] + stack = [] # one entry per open #if: True when it selects M-mode + func = "(file scope)" + + with open(path) as f: + for n, line in enumerate(f, 1): + s = line.strip() + + if s.startswith("#if"): + stack.append(mmode_guard(s)) + elif s.startswith("#elif"): + if stack: + stack[-1] = mmode_guard(s) + elif s.startswith("#else"): + if stack: + stack[-1] = not stack[-1] if stack[-1] else False + elif s.startswith("#endif"): + if stack: + stack.pop() + + m = FUNC.match(line) + if m: + func = m.group(1) + + if "MSS_WDT_REFRESH(" in line and not s.startswith("#define"): + if func in GATES or any(stack): + continue + bad.append("%s:%d: ungated MSS_WDT_REFRESH in %s()" + % (path, n, func)) + return bad + + +def main(): + hal = sys.argv[1] if len(sys.argv) > 1 else "hal" + bad = [] + for name in ("mpfs250.c", "mpfs250_ubl.c"): + bad += check("%s/%s" % (hal, name)) + + if bad: + for b in bad: + print(b) + print("A refresh outside a gate helper and outside M-mode resets the " + "chip.") + print("Call mpfs_wdt_pet_all(), or ubl_wdt_pet_all() in the UBL stub.") + return 1 + + print("mpfs wdt gate: every MSS_WDT_REFRESH is gated or M-mode only") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 2920a513de..a854eaf78a 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -142,6 +142,7 @@ TESTS+=unit-imx95-ele TESTS+=unit-imx95-disk TESTS+=unit-versal-ext-write TESTS+=unit-versal-wdt +TESTS+=unit-mpfs-wdt TESTS+=unit-t10xx-qe-firmware TESTS+=unit-t10xx-flash-status TESTS+=unit-p1021-erase-advance @@ -1628,6 +1629,32 @@ versal_wdt_extract.h: ../../hal/versal.c unit-versal-wdt: unit-versal-wdt.c versal_wdt_extract.h versal_host.h gcc -o $@ unit-versal-wdt.c -DTARGET_versal $(CFLAGS) $(LDFLAGS) +# unit-mpfs-wdt drives the real mpfs_wdt_pet_all() from hal/mpfs250.c against +# an emulated MSS watchdog register file. A refresh written while the counter +# is above MVRP is answered with a chip reset, so the test asserts which harts +# were written rather than a return code. The register offsets and the status +# bits are extracted from hal/mpfs250.h so a renumbering cannot pass. +mpfs250_wdt_extract.h: ../../hal/mpfs250.h ../../hal/mpfs250.c + sed -n '/^\/\* MSS Watchdog Timer (per-hart) \*\//,/^#define MSS_WDT_STAT_DEVRST/p' $< \ + | grep -v '_BASE' > $@ + grep -h '^#define MPFS_NUM_HARTS' $< >> $@ + sed -n '/^static const unsigned long mpfs_wdt_bases/,/^};/p' \ + ../../hal/mpfs250.c >> $@ + sed -n '/^void mpfs_wdt_pet_all(void)$$/,/^}/p' ../../hal/mpfs250.c >> $@ + test -s $@ + @for s in MSS_WDT_REFRESH MSS_WDT_STATUS MSS_WDT_STAT_FORBIDDEN \ + MPFS_NUM_HARTS mpfs_wdt_bases mpfs_wdt_pet_all; do grep -q "$$s" $@ || \ + { echo "$@: $$s not extracted"; false; }; done + +# A refresh outside a gate helper and outside M-mode resets the chip, which no +# unit test can observe: the source rule is checked alongside the test. +.PHONY: mpfs-wdt-gate-check +mpfs-wdt-gate-check: + python3 ../ci/check-mpfs-wdt-gate.py ../../hal + +unit-mpfs-wdt: unit-mpfs-wdt.c mpfs250_wdt_extract.h | mpfs-wdt-gate-check + gcc -o $@ unit-mpfs-wdt.c $(CFLAGS) $(LDFLAGS) + # unit-t10xx-qe-firmware runs the real qe_check_firmware() from # hal/nxp_t10xx.c (the per-microcode code_offset was never # bounded against the declared image, so a self-consistent blob could @@ -2260,6 +2287,7 @@ GENERATED_SRC:=aurix_erased_extract.h \ imx8qm_usdhc_extract.h imx8qm_flexspi_extract.h \ imx95_ele_extract.h imx95_disk_extract.h \ hifive1_flash_write_extract.h nvm_cache_scrub_extract.h \ + mpfs250_wdt_extract.h \ nxp_ls1028a_host.c nxp_p1021_host.c nxp_t10xx_fixup_extract.h \ p1021_erase_extract.h p1021_erase_fn_extract.h \ p1021_read_extract.h p1021_read_fn_extract.h \ diff --git a/tools/unit-tests/unit-mpfs-wdt.c b/tools/unit-tests/unit-mpfs-wdt.c new file mode 100644 index 0000000000..2f545376e9 --- /dev/null +++ b/tools/unit-tests/unit-mpfs-wdt.c @@ -0,0 +1,211 @@ +/* unit-mpfs-wdt.c + * + * Unit tests for the MSS watchdog refresh gate in hal/mpfs250.c. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +/* The extracted function initializes a static const array from the five + * MSS_WDT_*_BASE macros, so the register file has to live at those addresses + * rather than in a host array: the test maps the MSS watchdog span instead of + * rebasing the macros, which keeps the real base addresses under test. */ +#define MPFS_WDT_SPAN_BASE 0x20000000UL +#define MPFS_WDT_SPAN_SIZE 0x00108000UL + +#define MSS_WDT_E51_BASE 0x20001000UL +#define MSS_WDT_U54_1_BASE 0x20101000UL +#define MSS_WDT_U54_2_BASE 0x20103000UL +#define MSS_WDT_U54_3_BASE 0x20105000UL +#define MSS_WDT_U54_4_BASE 0x20107000UL + +#include "mpfs250_wdt_extract.h" + +/* A refresh is one write of this value to offset 0x00. */ +#define WDT_REFRESH_MAGIC 0xDEADC0DEU +/* Pre-seeded into the refresh word, so "untouched" is distinguishable. */ +#define WDT_NO_REFRESH 0x00000000U + +/* Sentinels for the registers a refresh must never touch: the first write to + * any of offsets 0x00-0x0C locks TRIGGER, MVRP and TIME until the next system + * reset, so a stray write here is unrecoverable on hardware. */ +#define CONTROL_SENTINEL 0x11111111U +#define TIME_SENTINEL 0x22222222U +#define MVRP_SENTINEL 0x33333333U +#define TRIGGER_SENTINEL 0x44444444U + +static void wdt_mock_reset(void) +{ + unsigned int i; + + memset((void *)(uintptr_t)MPFS_WDT_SPAN_BASE, 0, MPFS_WDT_SPAN_SIZE); + for (i = 0; i < MPFS_NUM_HARTS; i++) { + MSS_WDT_REFRESH(mpfs_wdt_bases[i]) = WDT_NO_REFRESH; + MSS_WDT_CONTROL(mpfs_wdt_bases[i]) = CONTROL_SENTINEL; + MSS_WDT_STATUS(mpfs_wdt_bases[i]) = 0U; + MSS_WDT_TIME(mpfs_wdt_bases[i]) = TIME_SENTINEL; + MSS_WDT_MVRP(mpfs_wdt_bases[i]) = MVRP_SENTINEL; + MSS_WDT_TRIGGER(mpfs_wdt_bases[i]) = TRIGGER_SENTINEL; + } +} + +static void wdt_mock_setup(void) +{ + void *p = mmap((void *)(uintptr_t)MPFS_WDT_SPAN_BASE, MPFS_WDT_SPAN_SIZE, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + + ck_assert_ptr_eq(p, (void *)(uintptr_t)MPFS_WDT_SPAN_BASE); + wdt_mock_reset(); +} + +static void wdt_mock_teardown(void) +{ + munmap((void *)(uintptr_t)MPFS_WDT_SPAN_BASE, MPFS_WDT_SPAN_SIZE); +} + +static int refreshed(unsigned int hart) +{ + return MSS_WDT_REFRESH(mpfs_wdt_bases[hart]) == WDT_REFRESH_MAGIC; +} + +static void assert_windows_untouched(void) +{ + unsigned int i; + + for (i = 0; i < MPFS_NUM_HARTS; i++) { + ck_assert_uint_eq(MSS_WDT_CONTROL(mpfs_wdt_bases[i]), CONTROL_SENTINEL); + ck_assert_uint_eq(MSS_WDT_TIME(mpfs_wdt_bases[i]), TIME_SENTINEL); + ck_assert_uint_eq(MSS_WDT_MVRP(mpfs_wdt_bases[i]), MVRP_SENTINEL); + ck_assert_uint_eq(MSS_WDT_TRIGGER(mpfs_wdt_bases[i]), TRIGGER_SENTINEL); + } +} + +/* Counter below MVRP on every hart: all five take the refresh. */ +START_TEST(test_refresh_when_permitted) +{ + unsigned int i; + + mpfs_wdt_pet_all(); + for (i = 0; i < MPFS_NUM_HARTS; i++) { + ck_assert_int_eq(refreshed(i), 1); + } + assert_windows_untouched(); +} +END_TEST + +/* The forbidden window: a refresh here is answered with a chip reset, so the + * gate must leave every one of them alone. */ +START_TEST(test_no_refresh_when_forbidden) +{ + unsigned int i; + + for (i = 0; i < MPFS_NUM_HARTS; i++) { + MSS_WDT_STATUS(mpfs_wdt_bases[i]) = MSS_WDT_STAT_FORBIDDEN; + } + mpfs_wdt_pet_all(); + for (i = 0; i < MPFS_NUM_HARTS; i++) { + ck_assert_int_eq(refreshed(i), 0); + } + assert_windows_untouched(); +} +END_TEST + +/* The window is per-hart, so the decision has to be too. */ +START_TEST(test_refresh_is_decided_per_hart) +{ + unsigned int i; + + for (i = 0; i < MPFS_NUM_HARTS; i += 2) { + MSS_WDT_STATUS(mpfs_wdt_bases[i]) = MSS_WDT_STAT_FORBIDDEN; + } + mpfs_wdt_pet_all(); + for (i = 0; i < MPFS_NUM_HARTS; i++) { + ck_assert_int_eq(refreshed(i), (i % 2) ? 1 : 0); + } + assert_windows_untouched(); +} +END_TEST + +/* Only bit 2 gates the refresh: LOCKED and DEVRST say nothing about the + * window, and treating them as a block would starve the watchdogs. */ +START_TEST(test_other_status_bits_do_not_block) +{ + unsigned int i; + + for (i = 0; i < MPFS_NUM_HARTS; i++) { + MSS_WDT_STATUS(mpfs_wdt_bases[i]) = + MSS_WDT_STAT_LOCKED | MSS_WDT_STAT_DEVRST | 0xFFFFFFF8U; + } + mpfs_wdt_pet_all(); + for (i = 0; i < MPFS_NUM_HARTS; i++) { + ck_assert_int_eq(refreshed(i), 1); + } +} +END_TEST + +/* The status read must come from the same hart as the refresh write: a + * single forbidden hart must not suppress the other four. */ +START_TEST(test_one_forbidden_hart_does_not_suppress_the_rest) +{ + unsigned int h, i; + + for (h = 0; h < MPFS_NUM_HARTS; h++) { + wdt_mock_reset(); + MSS_WDT_STATUS(mpfs_wdt_bases[h]) = MSS_WDT_STAT_FORBIDDEN; + mpfs_wdt_pet_all(); + for (i = 0; i < MPFS_NUM_HARTS; i++) { + ck_assert_int_eq(refreshed(i), (i == h) ? 0 : 1); + } + } +} +END_TEST + +Suite *mpfs_wdt_suite(void) +{ + Suite *s = suite_create("mpfs-wdt"); + TCase *tc = tcase_create("pet"); + + tcase_add_checked_fixture(tc, wdt_mock_setup, wdt_mock_teardown); + tcase_add_test(tc, test_refresh_when_permitted); + tcase_add_test(tc, test_no_refresh_when_forbidden); + tcase_add_test(tc, test_refresh_is_decided_per_hart); + tcase_add_test(tc, test_other_status_bits_do_not_block); + tcase_add_test(tc, test_one_forbidden_hart_does_not_suppress_the_rest); + suite_add_tcase(s, tc); + + return s; +} + +int main(void) +{ + int fails; + Suite *s = mpfs_wdt_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} From b26096daefbf33b3f1be7cd0ad6ab96f9ff59f97 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 22 Oct 2026 19:51:38 -0700 Subject: [PATCH 21/23] PolarFire SoC: add CI build jobs for the L2-LIM and UART-programmer configs --- .github/workflows/test-configs.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 78181396ff..a57e6ab8f3 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -527,6 +527,20 @@ jobs: with: arch: riscv64 config-file: ./config/examples/polarfire_mpfs250_qspi.config + # The UART QSPI programmer is the only code that refreshes the MSS watchdogs + # from an S-mode build, where HSS owns the window rather than wolfBoot. + microchip_mpfs250_qspi_prog_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_qspi.config + make-args: UART_QSPI_PROGRAM=1 + # wolfBoot staged in the L2-LIM rather than DDR, loaded by HSS. + microchip_mpfs250_hss_l2lim_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_hss_l2lim.config # M-mode + LPDDR4 build: generate a CI-only Libero settings stub (all # zeros) and point LIBERO_FPGA_CONFIG_DIR at it so MPFS_DDR_INIT actually # compiles in CI. The stub is generated from the HAL sources (not From c18b81d216dc154869077fa44cbf6ee6aa5d355b Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 23 Oct 2026 09:11:57 -0700 Subject: [PATCH 22/23] PolarFire SoC: pin mpfs_bootmode2.sh to the Embedded FP6 programmer --- tools/scripts/mpfs_bootmode2.sh | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tools/scripts/mpfs_bootmode2.sh b/tools/scripts/mpfs_bootmode2.sh index 7828d66b5e..b8a005adde 100755 --- a/tools/scripts/mpfs_bootmode2.sh +++ b/tools/scripts/mpfs_bootmode2.sh @@ -32,6 +32,11 @@ # whose SMK has never been initialized, and the programmer # never asks for one. A new SMK invalidates every existing # authenticated or encrypted sNVM client on the device. +# --programmer ID fpgenprog programmer, as it names it (e.g. CA148625). With +# no id fpgenprog selects EVERY attached programmer, and an +# idle second board fails the scan chain and the whole run. +# Default: the Embedded FP6's USB serial, when exactly one +# is attached. # --workdir DIR output directory (default: current directory) # --dryrun generate everything, do not program # @@ -46,6 +51,7 @@ USK=0 ENCRYPT="" SMK=0 WORKDIR="$PWD" +PROGRAMMER= DRYRUN=0 ENVM=0 @@ -57,6 +63,7 @@ while [ $# -gt 1 ]; do --usk) USK="$2"; shift 2 ;; --encrypt) ENCRYPT="--encrypt"; shift ;; --regenerate-smk) SMK=1; shift ;; + --programmer) PROGRAMMER="$2"; shift 2 ;; --workdir) WORKDIR="$2"; shift 2 ;; --dryrun) DRYRUN=1; shift ;; --envm) ENVM=1; shift ;; @@ -68,6 +75,23 @@ ELF="${1:-}" [ -n "${SC_INSTALL_DIR:-}" ] || { echo "SC_INSTALL_DIR is not set" >&2; exit 1; } [ -x "${FPGENPROG:-}" ] || { echo "FPGENPROG is not an executable" >&2; exit 1; } +# fpgenprog names an Embedded FP6 by its bare USB serial. Pin it by serial +# rather than by hub path: the path moves on every recable. +if [ -z "$PROGRAMMER" ]; then + for d in /sys/bus/usb/devices/*; do + [ "$(cat "$d/idVendor" 2>/dev/null)" = "1514" ] || continue + [ "$(cat "$d/idProduct" 2>/dev/null)" = "200b" ] || continue + sn=$(cat "$d/serial" 2>/dev/null) + [ -n "$sn" ] || continue + if [ -n "$PROGRAMMER" ]; then + echo "several Embedded FP6 probes attached; pass --programmer" >&2 + exit 1 + fi + PROGRAMMER="$sn" + done +fi +[ -n "$PROGRAMMER" ] || { echo "no Embedded FP6 (1514:200b) found; pass --programmer" >&2; exit 1; } + JAVA="$SC_INSTALL_DIR/eclipse/jre/bin/java" JAR="$SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar" # The programmer knows the non-secure die names only; it is just generating @@ -140,7 +164,8 @@ if [ "$DRYRUN" = 1 ]; then exit 0 fi echo "[3/3] programming" -"$FPGENPROG" run_action --location "$PROJ" --action PROGRAM +"$FPGENPROG" run_action --location "$PROJ" --action PROGRAM \ + --programmer_id "$PROGRAMMER" if [ "$MODE" = 2 ]; then echo "done: boot mode 2, UBL in sNVM from page $SNVM_PAGE" else From b8062fae69a299684f0b1196bbddd4dd289756d6 Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 23 Oct 2026 12:12:46 -0700 Subject: [PATCH 23/23] PolarFire SoC: compact the inline comments in the M-mode HAL and SBI runtime --- hal/mpfs250.c | 451 ++++++++++++----------------------------- src/boot_riscv_start.S | 80 +++----- src/riscv_sbi.c | 190 +++++------------ src/vector_riscv.S | 46 ++--- 4 files changed, 218 insertions(+), 549 deletions(-) diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 17a11260a7..2afccecf60 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -20,12 +20,6 @@ */ /* Microchip PolarFire SoC MPFS250T HAL for wolfBoot */ -/* Supports: - * RISC-V 64-bit architecture - * External flash operations - * UART communication - * System initialization - */ #include #include @@ -82,12 +76,9 @@ extern void sdhci_irq_handler(void); /* Video Kit DDR/Clock configuration is included in mpfs250.h */ -/* ------------------------------------------------------------------------ - * File-scope globals - * ---------------------------------------------------------------------- */ -/* APB (PCLK) frequency for UART baud divisors. Starts at the mode's - * compile-time value (40 MHz E51 reset clock in M-mode, 150 MHz under - * HSS) and is updated when M-mode wolfBoot raises the MSS PLL. */ +/* File-scope globals */ +/* APB (PCLK) frequency for UART baud divisors: the mode's compile-time value + * (40 MHz E51 reset, 150 MHz under HSS), raised when M-mode raises the PLL. */ uint32_t mpfs_apb_clk_hz = MSS_APB_AHB_CLK; static const unsigned long mpfs_wdt_bases[MPFS_NUM_HARTS] = { @@ -96,15 +87,13 @@ static const unsigned long mpfs_wdt_bases[MPFS_NUM_HARTS] = { }; #ifdef WOLFBOOT_RISCV_MMODE -/* CPU frequency in MHz for mcycle-based udelay(); seeded at the E51 reset - * clock and bumped to the PLL rate after mss_pll_init() (hal/mpfs250_ddr.c). - * A wrong value here skews timing-sensitive paths (e.g. SD power-up). */ +/* CPU MHz for the mcycle udelay(); seeded at the E51 reset clock and bumped + * after mss_pll_init(). A wrong value skews SD power-up timing. */ uint32_t mpfs_cpu_freq_mhz = MPFS_CPU_FREQ_RESET_MHZ; #ifdef MPFS_ENVM_MANIFEST -/* Last eNVM page: the signed manifest of this image, written in after the - * link by `make wolfboot_envm_signed.bin` and checked by the boot mode 2 - * stub (hal/mpfs250_ubl.c). Erased-state fill until then. */ +/* Last eNVM page: this image's signed manifest, written by + * `make wolfboot_envm_signed.bin` and checked by hal/mpfs250_ubl.c. */ const uint8_t __attribute__((section(".envm_manifest"), used)) mpfs_envm_manifest[MPFS_ENVM_MANIFEST_SIZE] = { [0 ... MPFS_ENVM_MANIFEST_SIZE - 1] = 0xFF }; @@ -162,22 +151,8 @@ __attribute__((noinline)) void udelay(uint32_t us) extern uint8_t _main_hart_hls; /* linker-provided address symbol; typed as uint8_t to avoid size confusion */ -/* Watchdog policy. The five MSS watchdogs are configured by the bitstream at - * design time and are already counting when wolfBoot starts; software cannot - * stop them and cannot usefully retime them. On the Video Kit they read - * TIME 0xFFFFF0, MVRP 0x989680, TRIGGER 0x3E8, CONTROL 0x02 (timeout - * interrupt enabled, forbidden window off), which is the longest window the - * 24-bit counter allows: the counter runs at the APB clock divided by 256, so - * a reload lasts about 28.6 s and the TRIGGER crossing arrives just before - * that. wolfBoot's whole job is therefore to refresh them, here and from the - * parked E51 after hand-off (hal_smode_boot). - * - * Retiming is not available even in principle: the first write to any of - * offsets 0x00-0x0C locks TRIGGER, MVRP and TIME until the next system reset - * (mss_watchdog.c MSS_WD_configure), and a refresh is such a write. So is - * disarming: there is no enable bit, and CONTROL bit 5 is not DEVRST - DEVRST - * is STATUS bit 5, read-only. WATCHDOG therefore has no timeout knob on this - * target; it selects only whether wolfBoot refreshes during its own run. */ +/* The five MSS watchdogs are armed by the bitstream and can be neither stopped + * nor retimed, so WATCHDOG only selects whether wolfBoot refreshes them. */ /* CLINT MSIP register for IPI delivery */ #define CLINT_MSIP_REG(hart) (*(volatile uint32_t*)(CLINT_BASE + (hart) * 4)) @@ -192,26 +167,16 @@ static void mpfs_signal_main_hart_started(void) HLS_DATA* hls = (HLS_DATA*)&_main_hart_hls; hls->in_wfi_indicator = HLS_MAIN_HART_STARTED; hls->my_hart_id = MPFS_FIRST_HART; - /* The eNVM secondary-hart gate polls the DTIM copy of this flag, not - * the L2-scratch HLS above: a cacheable store to the scratchpad can - * be lost on dirty-line eviction (layout-dependent), which parked - * the secondaries until the kernel's hart_start IPI -- too late for - * its 1s online window. DTIM is uncached and visible to all harts. */ + /* The eNVM gate polls the DTIM copy, not the L2-scratch HLS above: a + * cacheable scratchpad store can be lost on dirty-line eviction. */ *(volatile uint32_t *)MPFS_DTIM_MAIN_STARTED_ADDR = (uint32_t)HLS_MAIN_HART_STARTED; __asm__ volatile("fence iorw, iorw" ::: "memory"); } #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) -/* Per-hart S-mode start mailboxes, written by the E51 (boot hart release) - * or by the SBI HSM hart_start backend (on the calling U54), and consumed - * by the target hart's park loop in secondary_hart_entry(). - * - * They live in the E51 DTIM, not in L2-scratch BSS: a cacheable store to - * the scratchpad can be lost on cache-line eviction, so a cross-hart - * mailbox written by a U54 would never become visible. The DTIM is - * uncached and coherent for every hart. The SBI shared state occupies - * DTIM+0x000 (see src/riscv_sbi.c); the mailboxes sit at +0x100. */ +/* Per-hart S-mode start mailboxes, consumed by secondary_hart_entry(). In the + * DTIM (uncached, hart-coherent) at +0x100; SBI shared state is at +0x000. */ typedef struct { volatile uint32_t marker; /* MPFS_KERNEL_HANDOFF_MARKER when valid */ volatile uint64_t entry; /* S-mode entry point */ @@ -228,30 +193,23 @@ extern void riscv_mmode_to_smode(unsigned long entry, unsigned long hartid, unsigned long dtb) __attribute__((noreturn)); #endif /* MPFS_DDR_INIT && WOLFBOOT_MMODE_SMODE_BOOT */ -/* Secondary hart (U54) entry: jump into the waiting Linux kernel (when a - * hand-off context has been staged for us) or park in WFI waiting for an - * SBI/Linux IPI. - * - * This path must stay free of UART access: the secondaries reach it via - * the kernel's HSM hart_start IPI and have to respond inside the kernel's - * 1 s online window, which spinning on LSR can overrun. */ +/* Secondary hart entry: into a staged kernel hand-off, or park in WFI for an + * IPI. No UART here - the kernel allows ~1 s for a hart to come online. */ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls) { (void)hls; while (1) { #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) - /* Check the hand-off context before sleeping: the eNVM wake path - * has already consumed the release IPI (MSIP cleared), so a - * wfi-first loop would sleep through a staged hand-off. */ + /* Check the hand-off before sleeping: the eNVM wake path already + * consumed the release IPI, so a wfi-first loop would sleep past it. */ if (hartid < (unsigned long)MPFS_NUM_HARTS && mpfs_kernel_handoff[hartid].marker == MPFS_KERNEL_HANDOFF_MARKER) { unsigned long kentry; unsigned long opq; - /* Acquire fence, paired with the writer's release fence: - * without it RVWMO permits stale entry / opaque values read - * before marker was published. */ + /* Acquire fence paired with the writer's release fence; without + * it RVWMO permits a stale entry/opaque read. */ __asm__ volatile("fence r,rw" ::: "memory"); kentry = (unsigned long)mpfs_kernel_handoff[hartid].entry; opq = (unsigned long)mpfs_kernel_handoff[hartid].opaque; @@ -270,9 +228,8 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls) } #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) -/* Enable the CLINT MTIME counter via the SYSREG RTC/time-base clock divider - * (HSS set_RTC_divisor equivalent). Without this MTIME never advances and an - * S-mode OS has no time source for its scheduler tick. */ +/* Enable the CLINT MTIME counter via the SYSREG RTC clock divider. Without it + * MTIME never advances and an S-mode OS has no scheduler tick. */ static void mpfs_enable_mtime(void) { volatile uint32_t *rtc_cr = (volatile uint32_t *)(SYSREG_BASE + 0x0CUL); @@ -287,10 +244,8 @@ static void mpfs_enable_mtime(void) __asm__ volatile("fence iorw, iorw" ::: "memory"); } -/* SBI HSM hart_start backend (called from src/riscv_sbi.c on the boot - * hart): stage the target hart's start mailbox and ring its MSIP; the - * parked hart consumes it in secondary_hart_entry() and enters S-mode at - * saddr with a0=hartid, a1=opaque. */ +/* SBI HSM hart_start backend: stage the target hart's mailbox and ring its + * MSIP; it enters S-mode at saddr with a0=hartid, a1=opaque. */ int sbi_hal_hart_start(unsigned long hartid, unsigned long saddr, unsigned long opaque) { @@ -308,11 +263,8 @@ int sbi_hal_hart_start(unsigned long hartid, unsigned long saddr, return 0; } -/* Override of the weak hal_smode_boot in src/boot_riscv.c. The E51 cannot - * run Linux (cpu@0 is marked disabled in the Yocto MPFS DTB), so instead of - * dropping to S-mode on hart 0 we stage the kernel/DTB pointers, IPI a U54, - * and park hart 0 in M-mode. The released U54 picks up the context from - * its WFI loop in secondary_hart_entry() and performs the actual M->S jump. */ +/* Overrides the weak hal_smode_boot. The E51 cannot run Linux (cpu@0 is + * disabled in the DTB), so it stages the context, IPIs a U54 and parks. */ void __attribute__((noreturn)) hal_smode_boot(unsigned long entry, unsigned long hartid, unsigned long dtb) { @@ -325,11 +277,8 @@ hal_smode_boot(unsigned long entry, unsigned long hartid, unsigned long dtb) /* Bring up the MTIME time base before releasing the U54 into S-mode. */ mpfs_enable_mtime(); - /* Enable the clocks and release the soft resets of MMUART1-4 for the - * OS: the kernel's mpfs clock driver gates SUBBLK_CLOCK_CR but does - * not release peripheral soft resets (HSS normally does), so without - * this the serial console (MMUART1) stays dead. Done here on the - * E51, single-threaded, so no SYSREG read-modify-write races. */ + /* Release the MMUART1-4 soft resets for the OS: the kernel's clock driver + * gates SUBBLK_CLOCK_CR but does not, so MMUART1 would stay dead. */ SYSREG_SUBBLK_CLOCK_CR |= (MSS_PERIPH_MMUART0 << 1) | (MSS_PERIPH_MMUART0 << 2) | (MSS_PERIPH_MMUART0 << 3) | @@ -354,16 +303,8 @@ hal_smode_boot(unsigned long entry, unsigned long hartid, unsigned long dtb) CLINT_MSIP_REG(MPFS_FIRST_U54_HART) = 0x01; __asm__ volatile("fence iorw, iorw" ::: "memory"); - /* Park the E51 as the platform monitor, taking over HSS's - * watchdog-service role: doze on the machine timer and refresh all five - * MSS watchdogs every cycle. They count and reset the chip on timeout - * (~28 s at the boot-time settings) even with CONTROL=0, so something - * has to refresh them while the OS boots and runs; the dtb fixup - * disables the OS watchdog driver so it cannot also claim them, and - * with MVRP at maximum a refresh is always permitted. - * - * Only MTIE is enabled and mstatus.MIE stays clear, so the pending - * timer wakes WFI without vectoring into the trap path. */ + /* Park the E51 as the platform monitor: doze on the machine timer and + * refresh all five watchdogs. MTIE only, MIE clear, so WFI never traps. */ __asm__ volatile("csrw mie, %0" :: "r"(0x80UL)); /* MTIE */ while (1) { #ifndef MPFS_WDT_NODES_KEEP @@ -389,12 +330,8 @@ static void qspi_uart_program(void); #endif #ifdef MPFS_ATHENA -/* Ungate and release the Athena F5200 (sequence per HSS opensbi_crypto_ecall.c; - * CRYPTO_CR_INFO.MSS_MODE is informational, software does the un-reset). - * Idempotent: ATHENA_CR_RESET after CALIni() silently kills AES, not hashing. - * The SCA stall countermeasure (Security UG Table 7-7/7-8) is always on at - * rate 0 (1 in 8); DPA resistance is what the "S" part is for. rdcycle, not - * mcycle: an mcycle read traps in the S-mode builds. */ +/* Ungate and release the Athena F5200 (per HSS opensbi_crypto_ecall.c). + * Idempotent: ATHENA_CR_RESET after CALIni() kills AES, not hashing. */ static void mpfs_athena_enable(void) { static int athena_enabled; @@ -498,9 +435,8 @@ static int mpfs_athena_aes256_ctr(const void *key, void *iv, const void *in, #endif /* MPFS_ATHENA */ #ifdef MPFS_ATHENA -/* SHA-384 + AES-256-CTR via the plain-C mpfs_athena_* API (CAL headers cannot - * share a TU with wolfSSL's). Undocumented CAL contract: a non-final update - * must be whole 128-byte blocks, so short tails need the buffer below. */ +/* SHA-384 + AES-256-CTR via the plain-C mpfs_athena_* API. CAL contract: a + * non-final update must be whole 128-byte blocks, so tails are buffered. */ #ifdef MPFS_ATHENA_SHA #define ATHENA_SHA384_BLOCK 128 #define ATHENA_HASH_SLOTS 2 @@ -526,9 +462,8 @@ static int athena_ready; static uint32_t athena_cb_calls; #ifdef MPFS_ATHENA_SHA -/* Keyed by wc_Sha384 address; the cryptocb has no free event, so a context - * freed without Final strands its slot and a later one at that address would - * resume stale CAL state (wrong digest). Only reachable via ONESHOT today. */ +/* Keyed by wc_Sha384 address. The cryptocb has no free event, so a context + * freed without Final strands its slot and a later one there resumes it. */ static struct athena_hash_slot *athena_slot_find(void *owner) { int i; @@ -586,8 +521,7 @@ static int athena_hash_feed(struct athena_hash_slot *slot, const uint8_t *in, #if defined(MPFS_ATHENA_AES) && defined(WOLFSSL_AES_COUNTER) && !defined(NO_AES) /* AES-256-CTR, one path for both directions. Key from aes->devKey, not the - * WOLF_CRYPTO_CB_SETKEY hook: succeeding there strands the software fallback. - * All bail-outs precede any data entering the engine (counter would desync). */ + * SETKEY hook: succeeding there strands the software fallback. */ static int athena_aesctr(wc_CryptoInfo *info) { Aes *aes; @@ -627,10 +561,8 @@ static int athena_aesctr(wc_CryptoInfo *info) } } if (whole < sz) { - /* Trailing partial block: take one keystream block from the engine - * and leave the unused bytes where wolfCrypt keeps its own (aes->tmp - * with aes->left counting them), so the next call on this context - * continues the stream whichever path serves it. */ + /* Trailing partial block: leave the unused keystream where wolfCrypt + * keeps its own (aes->tmp, aes->left) so either path can continue. */ uint8_t ks[WC_AES_BLOCK_SIZE]; word32 i; @@ -688,10 +620,8 @@ static int mpfs_athena_cryptocb(int devIdArg, wc_CryptoInfo *info, void *ctx) slot = NULL; } if ((slot != NULL) && (slot->failed != 0)) { - /* An earlier update on this context faulted, so the engine holds a - * partial digest. Starting a fresh one here would digest only the - * data that follows the fault; callers that ignore an update's - * return code would then act on it. */ + /* An earlier update faulted, so the engine holds a partial digest. + * Starting fresh here would digest only the data after the fault. */ return WC_HW_E; } if (slot == NULL) { @@ -735,9 +665,8 @@ static int mpfs_athena_cryptocb(int devIdArg, wc_CryptoInfo *info, void *ctx) } #ifdef MPFS_ATHENA_SHA -/* Known answers for the self-check below. SHA-384 of the bytes 0x00..0xFF: - * a multi-block vector, hashed in chunks that straddle the 128-byte block so - * the intermediate state has to be carried across calls. */ +/* Known answers for the self-check: SHA-384 of 0x00..0xFF, hashed in chunks + * that straddle the 128-byte block so intermediate state must carry. */ static const uint8_t athena_kat_sha384_256b[48] = { 0xff, 0xda, 0xeb, 0xff, 0x65, 0xed, 0x05, 0xcf, 0x40, 0x0f, 0x02, 0x21, 0xc4, 0xcc, 0xfb, 0x4b, @@ -757,10 +686,8 @@ static const uint8_t athena_kat_aesctr[32] = { }; #endif -/* Run the offload through the wolfCrypt callback path once and compare with - * the known answers. The callback entry count is part of the check: wolfCrypt - * falls back to software silently when no device serves a call, and a correct - * result alone would not show that the hardware ran. */ +/* Run the offload through the callback path once and compare. The callback + * count is part of it: wolfCrypt falls back to software silently. */ /* What the self-check tripped on, for the panic message. */ static const char *athena_fail_what; static int athena_fail_rc; @@ -873,9 +800,8 @@ static int mpfs_athena_selfcheck(void) athena_fail_what = "AES-256-CTR known-answer mismatch"; return -1; } - /* The disk decrypt path calls CTR once per chunk, so the counter has to - * carry across calls: two 16-byte calls must equal one 32-byte call, and - * a split that is not a whole number of blocks must too. */ + /* The disk decrypt path calls CTR per chunk, so the counter must carry: + * two 16-byte calls must equal one 32-byte call, ragged splits too. */ ret = athena_ctr_run(key, ctr, buf, out2, 16, 16); if (ret == 0) { if (memcmp(out, out2, sizeof(out)) != 0) { @@ -966,19 +892,16 @@ static void mpfs_wdt_boot_config(void) mpfs_boot_reset_sr_snap = *reset_sr; *reset_sr = 0U; - /* Start every watchdog's window from full. The design-time window is far - * longer than the ~5 s ECDSA P-384 verify, so nothing has to refresh - * mid-signature; hal_prepare_boot() refreshes again before the hand-off. */ + /* Start every window from full. It far exceeds the ~5 s verify, and + * hal_prepare_boot() refreshes again before the hand-off. */ for (i = 0; i < MPFS_NUM_HARTS; i++) { MSS_WDT_REFRESH(mpfs_wdt_bases[i]) = 0xDEADC0DEU; } } #ifdef MPFS_DDR_INIT -/* Bring up LPDDR4, or halt. Each mpfs_ddr_init() pulses the SYSREG DDRC soft - * reset, which is the only way a rejected training recovers. There is no safe - * path past a failure: WOLFBOOT_LOAD_ADDRESS is in DDR, so a later load would - * write to a dead controller and hang inside the AXI master. */ +/* Bring up LPDDR4, or halt. WOLFBOOT_LOAD_ADDRESS is in DDR, so there is no + * safe path past a failure: a later load would hang in the AXI master. */ static void mpfs_ddr_bringup(void) { unsigned int retry; @@ -1012,11 +935,8 @@ void hal_init(void) unsigned int k; #endif - /* Park every hart's machine-timer comparator at maximum. CLINT MTIME - * is 0 (the RTC time base is not running yet) and mtimecmp resets to 0, - * so MTIP is pending on every hart out of reset. A pending interrupt - * makes WFI return immediately, which would leave the parked secondary - * harts spinning on their eNVM wait loop for the whole boot. */ + /* Park every mtimecmp at maximum. MTIME and mtimecmp both reset to 0, so + * MTIP is pending out of reset and WFI would return immediately. */ for (h = 0; h < MPFS_NUM_HARTS; h++) { CLINT_MTIMECMP_REG(h) = ~(uint64_t)0; } @@ -1073,9 +993,8 @@ void hal_init(void) #endif #ifdef SNVM_KEYSTORE_PROVISION - /* One-time: write the compiled-in trust anchor into sNVM so a subsequent - * SNVM_KEYSTORE build serves its keys from sNVM. Stop on failure so a - * provisioning run cannot look successful. */ + /* One-time: write the compiled-in trust anchor into sNVM. Stop on failure + * so a provisioning run cannot look successful. */ if (snvm_keystore_provision() != 0) { wolfBoot_printf("snvm provision: FAILED\n"); wolfBoot_panic(); @@ -1115,14 +1034,12 @@ static int mpfs_scb_mailbox_busy(void) * write could block on a UART the OS has reconfigured. */ static int mpfs_scb_quiet; -/* Raise a System Controller service. The request bytes go to mailbox word - * 0; index is the SERVICES_CR mailbox-offset field, which carries the SPI - * directory index for the IAP services. Returns 0 once the request is - * accepted, or a negative transport error. */ +/* Raise a System Controller service. index is the SERVICES_CR mailbox-offset + * field, which carries the SPI directory index for the IAP services. */ static int mpfs_scb_start(uint8_t opcode, uint32_t index, const uint8_t *req, uint32_t req_len) { - uint32_t cmd, words, rem, i, v; + uint32_t cmd, nwords, rem, i, v; int timeout; if (mpfs_scb_mailbox_busy()) { @@ -1132,23 +1049,23 @@ static int mpfs_scb_start(uint8_t opcode, uint32_t index, const uint8_t *req, return -2; } - /* Write request words into the mailbox (RMW for a non-word-aligned tail). */ - words = req_len / 4u; - for (i = 0; i < words; i++) { + /* Write the request words into the mailbox (RMW for a ragged tail). */ + nwords = req_len / 4u; + for (i = 0; i < nwords; i++) { v = (uint32_t)req[(i * 4u) + 0u]; v |= ((uint32_t)req[(i * 4u) + 1u]) << 8; v |= ((uint32_t)req[(i * 4u) + 2u]) << 16; v |= ((uint32_t)req[(i * 4u) + 3u]) << 24; SCBMBOX_REG(i * 4u) = v; } - rem = req_len - (words * 4u); + rem = req_len - (nwords * 4u); if (rem > 0u) { - v = SCBMBOX_REG(words * 4u); + v = SCBMBOX_REG(nwords * 4u); for (i = 0; i < rem; i++) { v &= ~(((uint32_t)0xFFu) << (i * 8u)); - v |= ((uint32_t)req[(words * 4u) + i]) << (i * 8u); + v |= ((uint32_t)req[(nwords * 4u) + i]) << (i * 8u); } - SCBMBOX_REG(words * 4u) = v; + SCBMBOX_REG(nwords * 4u) = v; } /* Ensure mailbox writes land before the request is raised. */ @@ -1183,9 +1100,8 @@ static int mpfs_scb_status(void) & 0xFFFFu); } -/* Wait for the service to complete. Returns its status (0 = success) or a - * negative transport error. Uses the larger completion bound: nonce/PUF - * take much longer than serial/sNVM-read. */ +/* Wait for completion. Returns the service status (0 = success) or a negative + * transport error, on the larger bound: nonce/PUF outlast an sNVM read. */ static int mpfs_scb_wait(void) { int timeout = MPFS_SCB_BUSY_TIMEOUT; @@ -1225,10 +1141,8 @@ static void mpfs_scb_read(uint8_t *out, uint32_t off, uint32_t len) } #if defined(MPFS_SBI_VENDOR) && defined(WOLFBOOT_MMODE_SMODE_BOOT) -/* SBI vendor extension (include/riscv_sbi.h) served to the OS. Everything - * here runs inside an ecall on one hart with interrupts off: the System - * Controller services that take seconds are started here and polled by the - * OS through SC_STATUS, the short ones complete in place. */ +/* SBI vendor extension served to the OS. Everything here runs in an ecall on + * one hart with interrupts off; the slow services are started, then polled. */ /* The mailbox is one resource for all harts. Taken for the life of a * started service, released when SC_STATUS reports it complete. */ @@ -1287,10 +1201,8 @@ static long sbi_vendor_get_key(unsigned long purpose, unsigned long buf, if (!sbi_smode_range_ok(buf, len)) { return SBI_ERR_INVALID_ADDRESS; } - /* Claim the serving before deriving anything. A plain check here and a - * set at the end would let two harts asking for the same purpose at once - * both be served; the claim is given back below if the key never reaches - * the caller. */ + /* Claim the serving before deriving: a check here and a set at the end + * would serve two harts at once. The claim is given back on failure. */ if ((__atomic_fetch_or(&sbi_key_served, 1U << i, __ATOMIC_ACQ_REL) & (1U << i)) != 0U) { return SBI_ERR_DENIED; @@ -1317,22 +1229,12 @@ static long sbi_vendor_get_key(unsigned long purpose, unsigned long buf, return SBI_SUCCESS; } -/* Services that reach the System Controller are served only when the OS - * cannot drive the mailbox itself: unfenced, its own driver would race - * these requests. */ +/* Served only when the OS cannot drive the mailbox itself: unfenced, its own + * driver would race these requests. */ #ifdef MPFS_SCB_SMODE_DENY #ifdef MPFS_SBI_SC_FLASH -/* The System Controller's SPI flash for the OS. The fence that keeps the - * OS off the mailbox also covers the flash controller (0x37020100), so under - * it this is the OS's only way to stage a bitstream. The first erase block - * holds the SPI directory the System Controller boots from: its golden entry - * is never written, and the block changes only through SET_IMAGE, which - * edits the upgrade or blank entry in a copy and writes the rest back as it - * was. Every other offset is the OS's. An erase call takes one block and a - * write or read at most SBI_SMODE_BUF_MAX bytes, so an ecall stays well - * under a second with the hart's interrupts masked. The mailbox lock is - * held throughout: the System Controller reads this flash for its own - * services. */ +/* The System Controller's SPI flash, served to the OS: under the mailbox fence + * this is its only way to stage a bitstream. The mailbox lock is held. */ int qspi_init(void); #define SBI_SC_FLASH_UPGRADE_INDEX 1UL @@ -1438,10 +1340,8 @@ static long sbi_vendor_sc_flash_read(unsigned long off, unsigned long buf, return err; } -/* Point directory entry 1 (upgrade) or 2 (blank) at an image offset. - * Setting the upgrade entry also zeroes the blank one, as the OS driver - * does, so a single-image System Controller does not object. Nothing is - * written when the entries already read that way. */ +/* Point directory entry 1 (upgrade) or 2 (blank) at an image offset. Setting + * upgrade also zeroes blank, as the OS driver does. No write if unchanged. */ static long sbi_vendor_sc_flash_set_image(unsigned long index, unsigned long address) { @@ -1681,9 +1581,8 @@ int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, ret = mpfs_scb_request(SYS_SERV_CMD_SNVM_READ, frame, sizeof(frame)); if (ret != 0) { - /* The service still returns the page admin word on a refusal, and - * it is the only way to tell a page held as a client from one that - * was never configured: the latter reads back as zero. */ + /* A refusal still returns the admin word, the only way to tell a page + * held as a client from one never configured (that reads back zero). */ if (admin != NULL) { mpfs_scb_read(admin, MPFS_SNVM_READ_RET_OFFSET, MPFS_SNVM_ADMIN_LEN); @@ -1808,12 +1707,8 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) uint8_t mac_addr[6]; #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) && \ (!defined(MPFS_WDT_NODES_KEEP) || defined(MPFS_SCB_SMODE_DENY)) - /* Nodes disabled below. Watchdogs: the MSS WDTs always count; the OS - * driver arms them at probe but nothing pings them (no userspace - * watchdog daemon in the default image), so the system would reset - * ~28 s into boot. Disabled, they only latch a harmless tripped - * status and the parked E51 monitor keeps them refreshed. Re-enable - * when an OS-side petting story exists. */ + /* Watchdog nodes are disabled: the OS driver arms them at probe and + * nothing in the default image pings them. See MPFS_WDT_NODES_KEEP. */ static const char *const cpu_off[] = { #ifndef MPFS_WDT_NODES_KEEP "watchdog@20001000", "watchdog@20101000", "watchdog@20103000", @@ -1857,19 +1752,8 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) && \ (!defined(MPFS_WDT_NODES_KEEP) || defined(MPFS_SCB_SMODE_DENY)) - /* Disable the MSS watchdog dtb nodes before the serial-number read, so - * this still runs if that read fails: otherwise the OS watchdog driver - * arms them at probe while the parked E51 monitor (hal_smode_boot) also - * refreshes all five, and the conflict can trip a watchdog reset. - * - * The stock /memory nodes are left alone. The cached DDR window at - * 0x80000000 is 1 GB wide and 0xC0000000 up is a non-cached alias onto - * the same DDR, so describing 2 GB at 0x80000000 would present the alias - * to the kernel as extra RAM. The stock Video Kit DTB is already right. - * - * cpu@2..cpu@4 stay enabled for SMP: the SBI HSM hart_start backend - * releases the parked harts when the kernel asks. cpu@0 (E51) is - * already disabled in the Yocto DTB; cpu@1 is where Linux boots. */ + /* Before the serial-number read, so it still runs if that fails. The + * /memory and cpu@ nodes are left alone; see docs/PolarFire.md. */ for (i = 0; i < sizeof(cpu_off) / sizeof(cpu_off[0]); i++) { off = fdt_find_node_offset(&ctx, -1, cpu_off[i]); if (off >= 0) { @@ -1970,23 +1854,16 @@ void hal_prepare_boot(void) * TIME, MVRP and TRIGGER keep their design-time values throughout. */ MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; #else - /* Booting an S-mode OS: give every hart's watchdog one final refresh so - * the OS inherits a full window (the - * OS watchdog driver hangs at probe if it finds an already-tripped - * watchdog). The OS watchdog nodes are disabled in the dtb, so after - * hand-off the parked E51 monitor loop in hal_smode_boot refreshes all - * five each cycle for the life of the OS. */ + /* One final refresh so the OS inherits a full window; its watchdog driver + * hangs at probe on an already-tripped watchdog. */ MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; MSS_WDT_REFRESH(MSS_WDT_U54_1_BASE) = 0xDEADC0DEU; MSS_WDT_REFRESH(MSS_WDT_U54_2_BASE) = 0xDEADC0DEU; MSS_WDT_REFRESH(MSS_WDT_U54_3_BASE) = 0xDEADC0DEU; MSS_WDT_REFRESH(MSS_WDT_U54_4_BASE) = 0xDEADC0DEU; - /* Hand the OS a clean SD controller: wolfBoot just used it for the - * image load, and the leftover state makes the OS driver's re-init - * and tuning intermittently fail ("Waiting for root device"). Guarded - * on the disk backend: an S-mode-boot config that loads the kernel from - * a non-SDHCI source (e.g. QSPI) pulls in no sdhci.h declaration. */ + /* Hand the OS a clean SD controller: leftover state makes its re-init and + * tuning intermittently fail ("Waiting for root device"). */ #if defined(DISK_SDCARD) || defined(DISK_EMMC) sdhci_shutdown(); #endif @@ -2029,13 +1906,8 @@ static int mpfs_scb_wait_clear(uint32_t reg_offset, uint32_t mask, return (timeout == 0) ? -1 : 0; } -/* Refresh all five MSS watchdogs, skipping any whose counter is still above - * MVRP. A refresh inside that forbidden window is answered with a chip reset - * rather than an error. An M-mode build sets MVRP itself and never lands - * there, but one running under a supervisor that owns the window - wolfBoot as - * an HSS payload - finds every refresh inside it, so each watchdog is tested - * rather than assumed. Skipping one is safe: the window is whole seconds wide - * and every caller loops. */ +/* A refresh above MVRP is the forbidden window and resets the chip, so each + * watchdog is tested: under a supervisor that owns the window, every one is. */ void mpfs_wdt_pet_all(void) { unsigned long b; @@ -2153,9 +2025,8 @@ int qspi_init(void) return 0; } -/* Polled transfer, after Microchip's MSS_QSPI_polled_transfer_block. - * cmd holds the opcode and any address bytes; dummy_cycles are the idle - * cycles the controller inserts between the command and data phases. */ +/* Polled transfer, after MSS_QSPI_polled_transfer_block. cmd holds the opcode + * and address bytes; dummy_cycles sit between the command and data phases. */ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, uint32_t cmd_len, uint8_t *data, uint32_t data_len, uint8_t dummy_cycles) @@ -2179,15 +2050,11 @@ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, (void)QSPI_RX_DATA; } - /* Writes put the whole transfer in the command phase (CMDBYTES = - * TOTALBYTES): the CoreQSPI data phase shifts TX FIFO output by a fixed - * offset on writes, which rotates the programmed page. The flash takes - * the command/data boundary from the opcode, not the controller phase. */ + /* Writes go wholly in the command phase (CMDBYTES = TOTALBYTES): the data + * phase shifts TX FIFO output on writes, rotating the programmed page. */ frame_cmd = read_mode ? cmd_len : total_bytes; - /* FBYTE counts RXAVAIL/TXAVAIL in bytes, FWORD in 32-bit words, so a - * word-mode read polls STATUS once per four bytes and takes them through - * QSPI_X4_RX_DATA in one access. The polling, not SCK, caps the read - * rate. FWORD needs a whole number of words in the data phase. */ + /* FWORD counts RXAVAIL in 32-bit words, so a read polls STATUS once per + * four bytes. It needs a whole number of words in the data phase. */ word_read = (read_mode && data_len >= 4u && (data_len & 3u) == 0u) ? 1 : 0; frames = ((total_bytes & 0xFFFF) << QSPI_FRAMES_TOTALBYTES_OFFSET) | ((frame_cmd & 0x1FF) << QSPI_FRAMES_CMDBYTES_OFFSET) | @@ -2197,9 +2064,8 @@ static int qspi_transfer_block(uint8_t read_mode, const uint8_t *cmd, QSPI_FRAMES = frames; - /* TXAVAIL is the only FIFO-space flag: CoreQSPI v2 has no TXFULL bit. - * The fence after each write keeps RVWMO from leaving the store posted, - * which would make the next STATUS read stale and overflow the FIFO. */ + /* TXAVAIL is the only FIFO-space flag; v2 has no TXFULL. The fence stops + * RVWMO leaving the store posted, which would stale the next STATUS. */ for (i = 0; i < cmd_len; i++) { if (QSPI_WAIT(QSPI_STATUS_TXAVAIL, QSPI_TIMEOUT_TRIES, "TX cmd") != 0) { @@ -2305,9 +2171,8 @@ static int qspi_enter_4byte_mode(void) /* Read from QSPI flash (4-byte addressing) */ static int qspi_flash_read(uint32_t address, uint8_t *data, uint32_t len) { - /* total_bytes is 16-bit and the command takes 5 of it; round down to a - * whole number of words so a bulk read stays on the word-mode FIFO path - * in qspi_transfer_block instead of falling back to byte mode. */ + /* total_bytes is 16-bit and the command takes 5; round down to whole words + * so a bulk read stays on the word-mode FIFO path. */ const uint32_t max_chunk = (0xFFFFu - 5u) & ~3u; uint8_t cmd[5]; uint32_t remaining = len; @@ -2399,9 +2264,7 @@ static int qspi_flash_sector_erase(uint32_t address) return qspi_wait_ready(10000); /* 10 second timeout */ } -/* ========================================================================== - * External Flash API Implementation - * ========================================================================== */ +/* External flash API */ void ext_flash_lock(void) { /* Optional: Could implement write protection here */ @@ -2494,23 +2357,8 @@ int ext_flash_erase(uintptr_t address, int len) return total; } -/* ============================================================================ - * UART QSPI Programmer - * - * Allows programming the QSPI flash over the debug UART without a JTAG/Libero - * tool. Enabled at build time with UART_QSPI_PROGRAM=1 in the .config. - * - * Protocol (after wolfBoot prints the "QSPI-PROG" prompt): - * 1. Host sends 'P' within the timeout window to enter programming mode - * 2. wolfBoot sends "READY\r\n" - * 3. Host sends [4-byte LE QSPI address][4-byte LE data length] - * 4. wolfBoot erases required sectors, sends "ERASED\r\n" - * 5. For each 256-byte chunk: - * wolfBoot sends ACK byte (0x06) -> host sends chunk -> wolfBoot writes - * 6. wolfBoot sends "DONE\r\n" and continues normal boot - * - * Host side: tools/scripts/mpfs_qspi_prog.py - * ============================================================================ */ +/* UART QSPI programmer: program the flash over the debug UART, no JTAG tool. + * Protocol and host side in tools/scripts/mpfs_qspi_prog.py. */ #if defined(UART_QSPI_PROGRAM) && defined(__WOLFBOOT) #define QSPI_PROG_CHUNK 256 @@ -2550,10 +2398,8 @@ static void qspi_uart_program(void) uint32_t i, s; uint8_t chunk[QSPI_PROG_CHUNK]; - /* Use uart_qspi_puts (direct UART) for ALL programmer output. - * wolfBoot_printf uses uart_write which adds \r before \n and may - * leave stale bytes in the UART TX pipeline that corrupt the - * binary ACK/data protocol after ERASED. */ + /* Direct UART for all programmer output: wolfBoot_printf adds \r and can + * leave bytes in the TX pipeline that corrupt the binary protocol. */ uart_qspi_puts("QSPI-PROG: Press 'P' within 3s to program flash\r\n"); /* Drain any stale RX bytes before opening the window */ @@ -2622,10 +2468,8 @@ static void qspi_uart_program(void) ext_flash_unlock(); for (s = 0; s < n_sectors; s++) { int ret; - /* The MSS WDTs always count and reset the chip at timeout (~28.6s - * at the reset divisor) and cannot be disabled, so a transfer of - * more than a few tens of KB outlives the period: refresh per sector - * here and per chunk below. */ + /* A transfer of more than a few tens of KB outlives the ~28.6 s + * watchdog period: refresh per sector here and per chunk below. */ mpfs_wdt_pet_all(); ret = ext_flash_erase(addr + s * FLASH_SECTOR_SIZE, FLASH_SECTOR_SIZE); @@ -2638,9 +2482,8 @@ static void qspi_uart_program(void) uart_qspi_puts("ERASED\r\n"); - /* Chunk transfer: wolfBoot requests each 256-byte block with ACK 0x06. - * No wolfBoot_printf allowed in this loop -- only direct UART via - * uart_qspi_tx/uart_qspi_puts to avoid protocol corruption. */ + /* Each 256-byte block is requested with ACK 0x06. No wolfBoot_printf in + * this loop: direct UART only, or the protocol corrupts. */ written = 0; while (written < size) { int ret; @@ -2788,9 +2631,8 @@ int ext_flash_erase(uintptr_t address, int len) void* hal_get_dts_address(void) { #if defined(EXT_FLASH) && defined(NO_XIP) - /* Flash is not memory-mapped when using NO_XIP with external flash - * (e.g. SC SPI). DTS must be loaded via ext_flash_read, not direct - * dereference. Return NULL so the caller skips the direct-access path. */ + /* Not memory-mapped under NO_XIP with external flash, so the DTS needs + * ext_flash_read; NULL makes the caller skip the direct-access path. */ return NULL; #else return (void*)WOLFBOOT_DTS_BOOT_ADDRESS; @@ -2829,12 +2671,8 @@ void plic_dispatch_irq(uint32_t irq) } } -/* MSSIO IOMUX + bank-config register offsets. - * IOMUX0..IOMUX6_CR and the per-pad IO_CFG_*_*_CR registers live in SYSREG - * (base 0x20002000). The two MSSIO_BANK*_CFG_CR registers that set - * bank-wide pcode/ncode/voltage live in the *SCB* register space - * (SYSREGSCB_BASE 0x20003000) at offsets 0x1C4/0x1C8 - HSS writes them via - * SCB_REGS, not SYSREG. */ +/* MSSIO IOMUX and per-pad registers are in SYSREG; the two MSSIO_BANK*_CFG_CR + * bank registers are in SCB space at 0x1C4/0x1C8, as HSS writes them. */ #define SYSREG_IOMUX0_CR_OFFSET 0x200u #define SYSREG_IOMUX1_CR_OFFSET 0x204u #define SYSREG_IOMUX2_CR_OFFSET 0x208u @@ -2847,14 +2685,8 @@ void plic_dispatch_irq(uint32_t irq) #define SCB_MSSIO_BANK2_CFG_CR_OFFSET 0x1C4u #define SCB_MSSIO_BANK4_CFG_CR_OFFSET 0x1C8u -/* Configure the MSSIO IOMUX so the SDHCI controller's pins are actually - * routed to the SD/eMMC slot pads. In S-mode builds HSS does this as - * part of mssio_setup() during nwc init; M-mode wolfBoot has to do it - * itself or the controller talks to floating pads and SD command - * responses come back as garbage (CMD_INDEX_ERR + CMD_END_BIT_ERR). - * - * All values come straight from the Libero/HSS-generated - * fpga_design_config.h that LIBERO_FPGA_CONFIG_DIR points at. */ +/* Route the SDHCI pins to the slot pads, which HSS does in mssio_setup(): + * without it the controller talks to floating pads. Values from Libero. */ #if defined(WOLFBOOT_RISCV_MMODE) && defined(MPFS_DDR_INIT) void mpfs_iomux_init(void) { @@ -2865,21 +2697,8 @@ void mpfs_iomux_init(void) SYSREG_REG(SYSREG_IOMUX2_CR_OFFSET) = LIBERO_SETTING_IOMUX2_CR; SYSREG_REG(SYSREG_IOMUX3_CR_OFFSET) = LIBERO_SETTING_IOMUX3_CR; - /* IOMUX4 + IOMUX5 need MPFS-Video-Kit-specific GPIO drive overrides - * on top of the Libero values to steer the board's external SD/eMMC - * demux mux into SD-card mode and pull a USB pin low. This logic - * is implemented in HSS as a board hook in - * boards/mpfs-video-kit/hss_board_init.c::switch_demux_using_fabric_ip - * and is NOT visible from the generic Libero IOMUX_CR values alone. - * Without this, the SDHCI controller talks to bank4 pads but those - * signals never reach the SD card slot, producing the "CMD8 timeout - * regardless of card insertion" symptom we observed for many runs. - * - * IOMUX4 bits[19:16] (USB pin): 0xD = drive logic 0 - * IOMUX5 bits[3:0] (pad 30): 0xE = drive logic 1 - * IOMUX5 bits[19:16] (pad 34): 0xE = drive logic 1 - * IOMUX5 bits[31:28] (pad 37): 0xD = drive logic 0 - */ + /* Board GPIO drive overrides that steer the external SD/eMMC demux into + * SD-card mode; HSS does this in switch_demux_using_fabric_ip(). */ iomux4 = LIBERO_SETTING_IOMUX4_CR; iomux5 = LIBERO_SETTING_IOMUX5_CR; iomux4 &= ~(0xFu << 16); @@ -2933,19 +2752,14 @@ void mpfs_iomux_init(void) /* SDHCI Platform HAL */ -/* MSS MPU base + per-master offset. Each AXI master (FIC0/1/2, CRYPTO, - * GEM0/1, USB, MMC, SCB, TRACE) has 16 PMPCFG entries (uint64_t each) at - * 0x20005000 + (master_index << 8). HSS calls mpu_configure() during early - * boot to load these from LIBERO_SETTING_*_MPU_CFG_PMP* defaults; without - * that, the master may be locked out of memory regions it needs and stalls - * silently mid-transaction. MMC is master index 7. */ +/* Each AXI master has 16 PMPCFG entries at 0x20005000 + (index << 8); MMC is + * index 7. Without them a master stalls silently mid-transaction. */ #define MSS_MPU_BASE 0x20005000UL #define MSS_MPU_MMC_BASE (MSS_MPU_BASE + (7UL << 8)) #ifdef MPFS_DDR_INIT -/* Only available when LIBERO_FPGA_CONFIG_DIR is set (which also enables - * MPFS_DDR_INIT in arch.mk). HSS already configures these PMP entries - * during its own boot, so non-DDR / HSS-loaded builds don't need this. */ +/* Needs LIBERO_FPGA_CONFIG_DIR. HSS configures these entries itself, so + * HSS-loaded builds do not need it. */ static void mpfs_mpu_init_mmc(void) { volatile uint64_t *pmp = (volatile uint64_t *)MSS_MPU_MMC_BASE; @@ -2957,10 +2771,8 @@ static void mpfs_mpu_init_mmc(void) } #endif /* MPFS_DDR_INIT */ -/* Pet all five MSS watchdogs during the SDHCI read loops. Overrides the weak - * no-op in src/sdhci.c. The MSS watchdogs always count and reset the chip at - * timeout and cannot be disabled, so the multi-second load of a large image - * must keep refreshing them. */ +/* Pet the watchdogs during the SDHCI read loops; overrides the weak no-op in + * src/sdhci.c. A multi-second image load outlives the watchdog period. */ void sdhci_platform_wdt_pet(void) { mpfs_wdt_pet_all(); @@ -2968,19 +2780,12 @@ void sdhci_platform_wdt_pet(void) void sdhci_platform_init(void) { - /* IOMUX/MSSIO routing was already programmed in nwc_init() before the - * MSSIO_CONTROL_CR 4-phase sequence committed the pad config. Here: - * 1. Configure the MMC AXI master MPU so the controller can access - * the regions Libero expects (default state may be all-deny). - * 2. Enable the MMC peripheral clock. - * 3. Deassert the MMC soft reset. - * Mirrors the DDRC sequence in mpfs_ddr_init(). */ + /* Routing is already committed by nwc_init(); this is the MMC master MPU, + * clock and soft reset, mirroring the DDRC sequence in mpfs_ddr_init(). */ wolfBoot_printf("SDHCI: platform init\n"); #ifdef MPFS_DDR_INIT - /* MMC AXI master MPU config requires LIBERO_SETTING_MMC_MPU_CFG_* - * which is only defined when LIBERO_FPGA_CONFIG_DIR is set. When - * not set, this build is intended to run UNDER HSS (which has - * already configured the MPU), so this step is a no-op. */ + /* LIBERO_SETTING_MMC_MPU_CFG_* needs LIBERO_FPGA_CONFIG_DIR; without it + * the build runs under HSS, which has configured the MPU already. */ mpfs_mpu_init_mmc(); #endif SYSREG_REG(SYSREG_SUBBLK_CLOCK_CR_OFF) |= MSS_PERIPH_MMC; @@ -3030,10 +2835,8 @@ void sdhci_reg_write(uint32_t offset, uint32_t val) /* DEBUG UART */ #ifdef DEBUG_UART -/* Baud divisor: integer = PCLK/(baudrate*16), fractional (0-63) via 128x - * scaling. Uses the RUNTIME APB clock so divisors computed after the MSS - * PLL raise stay correct (the compile-time MSS_APB_AHB_CLK garbled every - * post-raise reinit). */ +/* Baud divisor: integer = PCLK/(baudrate*16), fractional via 128x scaling. + * Uses the runtime APB clock so divisors stay correct after the PLL raise. */ static void uart_config_baud(unsigned long base, uint32_t baudrate) { const uint64_t pclk = mpfs_apb_clk_hz; diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 937f5460b0..edeada0361 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -29,20 +29,16 @@ #endif -/* RV64 M-mode: direct boot from eNVM; a0=hartid from CSR - * RV64 S-mode: entered from SBI with a0=hartid, a1=DTB pointer - * RV32: bare metal; stack/gp from linker symbols */ +/* RV64 M-mode: direct from eNVM, a0=hartid from CSR. RV64 S-mode: from SBI + * with a0=hartid, a1=DTB. RV32: bare metal, stack/gp from linker symbols. */ .section .init .globl _reset _reset: #if __riscv_xlen == 64 #if defined(WOLFBOOT_RISCV_MMODE) && defined(TARGET_mpfs250_ubl) - /* Boot mode 2 UBL stub: the System Controller has already copied the - * whole image to the LIM and released all five harts here, so VMA == LMA - * and nothing is copied. The E51 verifies the eNVM wolfBoot manifest; - * the U54s park on the DTIM mailbox until it publishes the entry, then - * every hart enters the eNVM reset vector as in a boot mode 1 boot. */ + /* Boot mode 2 UBL stub: the SC has copied the image to LIM and released + * all five harts, so VMA == LMA and nothing is copied. */ csrr a0, mhartid mv tp, a0 @@ -61,10 +57,8 @@ _reset: bnez a0, .L_ubl_secondary_wait - /* Close the mailbox gate before anything slow (.bss clear, verify): an - * MSS reset leaves the previous boot's magic and entry in DTIM, and a - * stale pair is indistinguishable from a fresh one, so the secondaries - * would jump into an eNVM image this boot has not verified. */ + /* Close the gate before anything slow: an MSS reset leaves the previous + * magic in DTIM, and the secondaries would jump into unverified code. */ li t0, MPFS_UBL_MAGIC_ADDR sw zero, 0(t0) fence iorw, iorw @@ -79,9 +73,8 @@ _reset: and sp, sp, t0 mv s0, sp - /* LIM comes out of reset with uninitialised ECC and a doubleword store is - * what writes it, so cover everything the System Controller did not copy: - * .bss, the heap and the stack, which faults on its first read. */ + /* LIM resets with uninitialised ECC that only a doubleword store writes, + * so cover what the SC did not copy: .bss, the heap and the stack. */ la t0, _start_bss la t1, _end_stack .L_ubl_lim_clear: @@ -154,14 +147,8 @@ _reset: sw zero, 0x174(t1) /* SYSREG_L2_SHUTDOWN_CR = 0 */ fence - /* Evict any copy of the scratchpad range that an earlier access left in - * an evictable way. The pin loop below allocates by storing with the - * E51 mask set to one way at a time, but a store that HITS a line already - * resident in ways 0-7 updates that line instead, so the address never - * reaches a scratchpad way and reads as zero once a large DMA sweep - * evicts it. Under boot mode 2 the UBL and the released U54s run before - * this point with every mask wide open. FLUSH64 takes a line address - * and evicts it from whichever way holds it. */ + /* Evict any scratchpad line left in an evictable way: the pin loop below + * allocates only on a miss, so a hit in ways 0-7 would never move. */ li t0, 0x02010200 /* L2_FLUSH64 */ li t1, 0x0A000000 /* ZERO_DEVICE_BOTTOM */ li t2, 0x0A080000 /* + 512 KB */ @@ -171,12 +158,8 @@ _reset: bltu t1, t2, .L_prepin_flush fence - /* Make all 512 KB of the Zero Device resident by enabling one scratchpad - * way at a time and storing a marker every 64 bytes: a store can only - * allocate in the enabled way, so 2048 blocks x 4 ways fills each once. - * A mask of 0xF00 covers only the stores made while it is set, leaving - * .data/.bss/heap/stack in evictable ways whose writeback is discarded. - * This is HSS config_l2_cache(); the marker matches its INIT_MARKER. */ + /* Make all 512 KB resident one way at a time, storing every 64 bytes, so + * each way fills once. This is HSS config_l2_cache() and its marker. */ li t0, 0x02010828 /* L2_WAY_MASK_E51_DCACHE */ li t1, 0x100 /* scratchpad way 8 */ li t2, 0x0A000000 /* ZERO_DEVICE_BOTTOM */ @@ -202,9 +185,8 @@ _reset: fence #endif - /* Copy .text from eNVM to L2 SRAM. - * Use li/ld for dest/size (la would give wrong address across the - * eNVM-to-L2 gap which exceeds the 32-bit PC-relative range). */ + /* Copy .text from eNVM to L2 SRAM. li/ld, not la: the eNVM-to-L2 gap + * exceeds the 32-bit PC-relative range. */ la t0, _stored_text la t3, _copy_params ld t1, 0(t3) /* dest (L2 SRAM base) */ @@ -267,22 +249,11 @@ _copy_params: addi sp, sp, -64 /* allocate 64-byte HLS at top of stack */ mv s11, sp - /* The HLS block is zeroed AFTER the wait below, not here. It lives in - * L2 scratchpad, and the boot hart does not finish carving the - * scratchpad out (the way-mask pin loop, then mpfs_config_l2_cache) - * until well after it releases us. Under boot mode 2 the UBL releases - * the U54s and jumps last, so a store here is guaranteed to land before - * the scratchpad exists; in boot mode 1 the System Controller starts all - * five harts together and the boot hart merely usually wins. Only - * register setup happens before the wait. */ - - /* Wait for E51 to signal HLS_MAIN_HART_STARTED. On MPFS250 poll the - * DTIM copy of the flag, not the L2-scratch HLS: an E51 store to the - * cacheable scratchpad can be lost on dirty-line eviction (layout- - * dependent), which left the secondaries parked here until the kernel's - * HSM hart_start IPI arrived -- too late for its 1s online window. - * Other RISCV64 M-mode targets poll the generic _main_hart_hls flag - * (MPFS_DTIM_MAIN_STARTED_ADDR is MPFS250-specific). */ + /* HLS is zeroed after the wait, not here: it lives in L2 scratchpad, which + * the boot hart has not finished carving out when it releases us. */ + + /* Wait for HLS_MAIN_HART_STARTED. MPFS250 polls the DTIM copy, not the + * L2-scratch HLS, whose store can be lost on dirty-line eviction. */ li t3, 0x12344321 #ifdef TARGET_mpfs250 li t1, MPFS_DTIM_MAIN_STARTED_ADDR @@ -293,12 +264,8 @@ _copy_params: lwu t2, 0(t1) bne t3, t2, .L_wait_main_hart - /* Clear any stale CLINT MSIP left by a prior boot (a warm/WDT reset does - * not clear CLINT) BEFORE arming the WFI wait, so a stale MSIP cannot fall - * the wfi loop through and resume a previous kernel handoff over - * un-reinitialized DDR. E51 sends its release IPI only after we signal - * HLS_OTHER_HART_IN_WFI below, so clearing here cannot race the real - * wakeup. */ + /* A warm reset does not clear CLINT, so drop a stale MSIP before arming + * the wait or it falls straight through into a previous hand-off. */ csrr a0, mhartid li t0, 0x02000000 /* CLINT_BASE */ slli t1, a0, 2 @@ -336,9 +303,8 @@ _copy_params: fence iorw, iorw fence.i - /* The C code on this hart (secondary_hart_entry and the M->S release - * path) addresses small globals gp-relative; only the E51's - * .L_sram_entry path sets gp, so set it here too. */ + /* The C code on this hart addresses small globals gp-relative, and only + * the E51's .L_sram_entry path sets gp. */ .option push .option norelax la gp, __global_pointer$ diff --git a/src/riscv_sbi.c b/src/riscv_sbi.c index 0758873cab..b406bdb910 100644 --- a/src/riscv_sbi.c +++ b/src/riscv_sbi.c @@ -19,19 +19,8 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ -/* Minimal RISC-V SBI (Supervisor Binary Interface) runtime for wolfBoot. - * - * When wolfBoot replaces a full firmware (e.g. HSS+OpenSBI on PolarFire - * SoC) and boots an S-mode OS (Linux), the OS issues SBI ecalls that must - * be serviced in M-mode. This provides the minimal set: BASE, TIME, IPI, - * RFENCE, HSM, DBCN + the legacy console/timer calls, enough to bring a - * RISC-V Linux kernel up to a console and timer tick. - * - * It is driven from handle_trap_ex() in src/boot_riscv.c on: - * - ecall-from-S (mcause = 9) -> sbi_handle_ecall() - * - M-timer interrupt (mcause INT|7) -> sbi_timer_irq() - * - M-soft interrupt (mcause INT|3) -> sbi_ipi_irq() - */ +/* Minimal RISC-V SBI runtime: BASE, TIME, IPI, RFENCE, HSM, DBCN and the legacy + * calls, driven from handle_trap_ex() in src/boot_riscv.c. */ #include #include "hal/riscv.h" @@ -113,9 +102,8 @@ /* v0.3: the OS only uses SRST (reboot and power-off) from 0.3 up. */ #define SBI_SPEC_VERSION ((0UL << 24) | 3UL) -/* No registry ID is assigned to wolfBoot's SBI; report a custom value that - * cannot collide with the small spec-registry IDs (0=BBL, 1=OpenSBI, 3=KVM, - * 8=PolarFire HSS, ...). 0x776F6C66 = ASCII "wolf". */ +/* wolfBoot has no registry ID; 0x776F6C66 ("wolf") cannot collide with the + * small spec-registry values. */ #define SBI_IMPL_ID 0x776F6C66UL /* "wolf" (custom, unregistered) */ #define SBI_IMPL_VERSION 1UL @@ -124,12 +112,8 @@ #define SBI_HSM_STOPPED 1 #define SBI_HSM_START_PENDING 2 -/* Register-frame indices: regs[N] == xN for the argument registers used here - * (a0/a1/a2/a6/a7). Note that under WOLFBOOT_RISCV_MMODE the trap entry - * (src/vector_riscv.S) repurposes two slots: slot 0 (x0's unused slot) holds - * mscratch-at-entry and slot 2 (x2/sp) holds the trapped sp - so regs[0] and - * regs[2] are NOT x0/x2. This file only indexes the a* slots below, which are - * unaffected. */ +/* regs[N] == xN for the a* registers used here. Slots 0 and 2 are repurposed + * by the trap entry, so they are NOT x0/x2; nothing below indexes them. */ #define A0 10 #define A1 11 #define A2 12 @@ -140,10 +124,8 @@ #define MPFS_NUM_HARTS 5 #endif -/* Cross-hart SBI state cannot live in L2-scratch BSS: a cacheable store to - * the scratchpad (Zero Device) is lost when the dirty line is evicted, so a - * value written by one hart can vanish before another hart sees it. The E51 - * DTIM is always uncached and coherent for every hart, as it is under HSS. */ +/* Cross-hart state cannot live in L2-scratch BSS: a cacheable store there is + * lost on eviction. The E51 DTIM is uncached and coherent, as under HSS. */ #ifndef SBI_SHARED_DTIM_ADDR #define SBI_SHARED_DTIM_ADDR 0x01000000UL /* E51 DTIM */ #endif @@ -153,13 +135,8 @@ typedef struct { volatile uint32_t init_magic; volatile int hart_state[MPFS_NUM_HARTS]; volatile uint32_t ipi_ops[MPFS_NUM_HARTS]; - /* Fence-completion protocol: a requester ORs its op into ipi_ops[h] - * and then takes a ticket from ipi_posted[h]; hart h reads ipi_posted - * before it consumes ipi_ops, executes the fences, and publishes what - * it read as ipi_done[h]. A ticket is complete once ipi_done[h] has - * reached it: the fence that ran consumed an ops word that already - * held the request. A later post keeps its own ticket and is served - * by the next interrupt. */ + /* Fence completion: a requester ORs its op in, then takes a ticket; the + * target publishes the ticket it saw once the fences have run. */ volatile uint32_t ipi_posted[MPFS_NUM_HARTS]; volatile uint32_t ipi_done[MPFS_NUM_HARTS]; } sbi_shared_state_t; @@ -187,9 +164,8 @@ int __attribute__((weak)) sbi_hal_hart_start(unsigned long hartid, return -1; } -/* Called from the M->S release path on the hart entering S-mode. The - * first call (the boot hart, released before any other) also initializes - * the table: zeroed BSS would otherwise read as STARTED for every hart. */ +/* Called on the hart entering S-mode. The first call also initializes the + * table: zeroed BSS would otherwise read as STARTED for every hart. */ void sbi_hart_mark_started(unsigned long hartid) { unsigned int k; @@ -212,15 +188,8 @@ void sbi_hart_mark_started(unsigned long hartid) #define CLINT_MTIME (*(volatile uint64_t *)(CLINT_BASE + 0xBFF8UL)) #define MSTATUS_MPRV_BIT (1UL << 17) -/* Per-hart M-mode trap stacks. The trap entry (src/vector_riscv.S) - * switches to the stack armed in mscratch because the trapped S-mode - * context's sp is a virtual address once the OS enables paging. - * - * These cannot live in L2-scratch BSS either: under SMP cache pressure a - * dirty frame line can be evicted mid-trap and the writeback lost, leaving - * the restore to read zeros. They go in the OS-invisible reserved DDR - * region (hss-buffer@103fc00000, nomap in the stock dtb: the monitor - * carve-out under HSS). */ +/* Per-hart M-mode trap stacks, in the OS-invisible reserved DDR region: in + * L2-scratch BSS a dirty frame line can be evicted mid-trap. */ #ifndef SBI_MSTACK_BASE #define SBI_MSTACK_BASE 0x103FC00000UL #endif @@ -244,13 +213,8 @@ void sbi_mscratch_init(unsigned long hartid) } } -/* Accesses to the trapped context's memory use the MPRV trick: with - * mstatus.MPRV set and MPP=S (true inside a trap taken from S-mode), M-mode - * loads and stores are translated exactly like S-mode accesses. A bad - * address then faults in M-mode, so for the byte below mtvec is pointed at - * sbi_mprv_trap, which records the fault for this hart, steps over the - * access and returns; the default vector would treat the fault as fatal and - * park the hart. */ +/* With mstatus.MPRV and MPP=S, M-mode accesses translate as S-mode ones. A + * bad address faults, so mtvec points at sbi_mprv_trap to step over it. */ volatile unsigned long sbi_mprv_fault[MPFS_NUM_HARTS]; __asm__( @@ -329,9 +293,8 @@ static int sbi_mprv_sb(unsigned long a, uint8_t v) return (sbi_mprv_fault[hart] != 0UL) ? -1 : 0; } -/* A caller's buffer must be bounded and, with paging off (early boot), - * must be DDR: an M-mode access to a device window would not fault the way - * a bad translation does. */ +/* A caller's buffer must be bounded, and DDR with paging off: an M-mode access + * to a device window would not fault the way a bad translation does. */ #if !defined(DDR_BASE_CACHED) || !defined(DDR_SIZE) #error "sbi_smode_range_ok needs DDR_BASE_CACHED and DDR_SIZE" #endif @@ -393,10 +356,8 @@ long __attribute__((weak)) sbi_hal_vendor_ecall(unsigned long fid, #define MSTATUS_MXR_BIT (1UL << 19) -/* Guest-context byte accessors: with mstatus.MPRV set, M-mode memory - * accesses use the trapped (S/U) context's translation and permissions; - * in bare mode addresses pass through. MXR additionally permits reading - * execute-only pages (instruction fetch for emulation). */ +/* Guest-context byte accessors: MPRV borrows the trapped context's + * translation, and MXR additionally permits reading execute-only pages. */ static uint8_t sbi_guest_lb(unsigned long a) { uint8_t v; @@ -422,12 +383,8 @@ static uint16_t sbi_guest_ifetch16(unsigned long a) return v; } -/* Emulate a misaligned load (cause 4) or store (cause 6) from S/U mode. - * These harts take misaligned accesses to M-mode and expect the firmware - * to emulate them byte-wise (OpenSBI behavior). Handles the integer - * I/S-type forms and the common compressed forms; AMO and floating-point - * forms are not emulated (returns 0 -> fatal dump). Returns the - * advanced epc, or 0 if the instruction is not handled. */ +/* Emulate a misaligned load or store from S/U mode byte-wise, as OpenSBI does. + * Integer and common compressed forms only; AMO and FP return 0 (fatal). */ unsigned long sbi_misaligned_ldst(unsigned long *regs, unsigned long epc, unsigned long tval, unsigned long cause) { @@ -544,9 +501,8 @@ unsigned long sbi_misaligned_ldst(unsigned long *regs, unsigned long epc, return epc + ilen; } -/* Emulate instructions that trap as illegal from S-mode. These harts - * have no time CSR (CLINT MTIME is memory-mapped only), so the kernel's - * rdtime traps here. Returns the updated epc, or 0 if not handled. */ +/* Emulate instructions that trap as illegal from S-mode: these harts have no + * time CSR, so the kernel's rdtime arrives here. */ unsigned long sbi_illegal_insn(unsigned long *regs, unsigned long epc, unsigned long tval) { @@ -562,9 +518,8 @@ unsigned long sbi_illegal_insn(unsigned long *regs, unsigned long epc, return 0UL; } -/* The OS owns the UART by now and may have reconfigured or stopped it, so - * never wait on it for long with M-mode interrupts masked. Returns 1 when - * the byte was written. */ +/* The OS owns the UART and may have stopped it, so never wait long on it with + * M-mode interrupts masked. Returns 1 when the byte was written. */ #define SBI_UART_SPIN 200000U static int sbi_uart_ready(void) @@ -601,9 +556,8 @@ static void __attribute__((noreturn)) sbi_system_halt(void) } } -/* Set a hart's S-mode timer. M-mode owns the timer; we program the CLINT - * mtimecmp, drop any already-injected S-timer interrupt, and (re)enable the - * M-timer so it fires when mtime reaches the comparator. */ +/* Set a hart's S-mode timer: program CLINT mtimecmp, drop any injected + * S-timer interrupt and re-enable the M-timer. */ static void sbi_set_timer(unsigned long hartid, uint64_t stime) { CLINT_MTIMECMP(hartid) = stime; @@ -619,10 +573,8 @@ void sbi_timer_irq(void) csr_set_bits(mip, MIP_STIP); } -/* M-soft (IPI) interrupt on a RUNNING (S-mode) hart: clear the CLINT MSIP - * that woke us, perform any requested remote-fence work, and inject a - * supervisor software interrupt when an OS IPI was requested. A bare - * MSIP with no op flags is treated as an OS IPI for compatibility. */ +/* IPI on a running hart: clear the MSIP, run any requested fences, inject SSIP + * for an OS IPI. A bare MSIP with no op flags counts as an OS IPI. */ void sbi_ipi_irq(unsigned long hartid) { uint32_t ops = 0; @@ -634,9 +586,8 @@ void sbi_ipi_irq(unsigned long hartid) /* Read the ticket counter before consuming the ops: every request * posted under a ticket acknowledged below is then already in it. */ posted = __atomic_load_n(&sbi_ipi_posted[hartid], __ATOMIC_ACQUIRE); - /* Atomically read-and-clear: a remote hart may be OR-ing a new op in - * (sbi_post_ipi) concurrently with this consume. amoswap.w (rv64a) - * makes the read+clear a single operation so no posted op is lost. */ + /* amoswap.w: a remote hart may be OR-ing a new op in concurrently, so + * the read and clear must be one operation or a post is lost. */ ops = __atomic_exchange_n(&sbi_ipi_ops[hartid], 0U, __ATOMIC_ACQ_REL); } if ((ops & SBI_IPI_OP_HALT) != 0U) { @@ -657,11 +608,8 @@ void sbi_ipi_irq(unsigned long hartid) } } -/* Post an IPI op to every hart in (mask << base) and ring its MSIP. - * When the mask selects the calling hart: an SSIP IPI is self-delivered by - * raising our own SSIP (OpenSBI delivers self-IPIs; smp_call_function paths - * that include self rely on it), while for an RFENCE the caller has already - * performed the fence locally, so self is skipped there. */ +/* Post an op to every hart in (mask << base) and ring its MSIP. A self-IPI + * raises our own SSIP; a self-RFENCE is skipped, already done locally. */ static void sbi_post_ipi(unsigned long mask, unsigned long base, uint32_t op, unsigned long self, uint32_t *ticket) { @@ -671,9 +619,8 @@ static void sbi_post_ipi(unsigned long mask, unsigned long base, for (i = 0; i < (unsigned long)MPFS_NUM_HARTS; i++) { ticket[i] = 0; } - /* SBI v0.2: hart_mask_base == -1 selects all available harts and - * hart_mask is ignored. Normalize to (base 0, all-harts mask) so the - * loop below does not compute h = (-1)+i and skip every target. */ + /* SBI v0.2: base == -1 selects all harts and the mask is ignored. + * Normalize it, or the loop computes h = (-1)+i and skips every target. */ if (base == (unsigned long)-1) { base = 0; mask = (1UL << MPFS_NUM_HARTS) - 1UL; @@ -695,9 +642,8 @@ static void sbi_post_ipi(unsigned long mask, unsigned long base, if (sbi_hart_state[h] != SBI_HSM_STARTED) { continue; /* parked harts consume MSIP in their wake loop */ } - /* Atomic OR (amoor.w, rv64a): two harts may post to the same target - * concurrently, and the target may be consuming (sbi_ipi_irq) at the - * same time - a plain |= read-modify-write could drop an op. */ + /* amoor.w: two harts may post to one target while it consumes, and a + * plain |= read-modify-write could drop an op. */ (void)__atomic_fetch_or(&sbi_ipi_ops[h], op, __ATOMIC_ACQ_REL); /* Take the ticket after the op is in: the target acknowledges a * ticket only with an ops word that already held this request. */ @@ -715,9 +661,8 @@ static void sbi_post_ipi(unsigned long mask, unsigned long base, __asm__ volatile("fence iorw, iorw" ::: "memory"); } -/* Shutdown has nowhere to go on this board: park every started hart. The - * OS has stopped the others already, but they still take timer interrupts - * and would keep running the supervisor's stall detectors. */ +/* Shutdown has nowhere to go: park every started hart. The stopped ones still + * take timer interrupts and would keep the stall detectors running. */ static void __attribute__((noreturn)) sbi_system_shutdown(unsigned long self) { uint32_t ticket[MPFS_NUM_HARTS]; @@ -727,13 +672,8 @@ static void __attribute__((noreturn)) sbi_system_shutdown(unsigned long self) sbi_system_halt(); } -/* Wait (bounded) for the posted fence ops to be consumed by the targets: - * a target publishes ipi_done after executing the fences, so this - * completes only when every ticket has been reached. The SBI - * remote-fence calls are synchronous; the bound guards against a wedged - * target turning into a wedged caller. Returns SBI_SUCCESS when every - * target completed, SBI_ERR_FAILED when any target did not complete - * within the bound. */ +/* Wait, bounded, for every posted fence ticket to be reached. The SBI calls + * are synchronous; the bound stops a wedged target wedging the caller. */ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, unsigned long self, const uint32_t *ticket) { @@ -755,15 +695,8 @@ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, ticket[h] == 0U) { continue; } - /* A target can be inside a console write or another ecall for tens - * of milliseconds; a fence that gives up early leaves the OS with - * stale TLBs on that hart, so wait well past that: 1 s of CLINT - * time, the one clock every hart shares. A hart that never answers - * then costs the caller a second, not the OS its interrupts for - * minutes. M-mode interrupts are masked in here, so service any - * fence the target (or a third hart) posts to us meanwhile: two - * harts fencing each other would otherwise each wait for the other - * and time out. */ + /* 1 s of CLINT time: giving up early leaves stale TLBs. Interrupts + * are masked, so service fences posted to us or two harts deadlock. */ deadline = CLINT_MTIME + SBI_IPI_WAIT_TICKS; while ((int32_t)(__atomic_load_n(&sbi_ipi_done[h], __ATOMIC_ACQUIRE) - ticket[h]) < 0 && (int64_t)(CLINT_MTIME - deadline) < 0) { @@ -779,11 +712,8 @@ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, return err; } -/* Deliver a trap that arrived in M-mode from S/U mode to the supervisor's - * trap vector, the way delegation would have: sepc/scause/stval describe it, - * sstatus records the interrupted privilege and interrupt enable, and the - * mret lands on stvec in S-mode. Returns the new epc, or 0 when the trap - * came from M-mode or the supervisor has no vector installed yet. */ +/* Deliver an M-mode trap from S/U to the supervisor's vector as delegation + * would: sepc/scause/stval/sstatus describe it and mret lands on stvec. */ unsigned long sbi_redirect_trap(unsigned long cause, unsigned long epc, unsigned long tval) { @@ -830,10 +760,8 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) __asm__ volatile("csrr %0, mhartid" : "=r"(hartid)); #ifdef DEBUG_SBI - /* Bring-up visibility: trace the first few ecalls so a silent kernel - * can be distinguished from a broken console path. Console and timer - * ecalls are exempted: tracing the timer path serializes every tick - * behind the UART and slows the OS dramatically. */ + /* Trace the first few ecalls to tell a silent kernel from a broken console. + * Console and timer are exempt: a traced tick serializes on the UART. */ if (sbi_dbg_calls < 40U && eid != SBI_EXT_0_1_CONSOLE_PUTCHAR && eid != SBI_EXT_DBCN && eid != SBI_EXT_TIME && eid != SBI_EXT_0_1_SET_TIMER) { @@ -895,9 +823,8 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) break; case SBI_EXT_RFENCE: { - /* remote_fence_i(mask, base) / remote_sfence_vma(mask, base, - * start, size) / ..._asid: over-flush with full fences (legal - * per the spec) and wait for the targets to consume the op. */ + /* remote_fence_i / remote_sfence_vma / ..._asid: over-flush with full + * fences, which the spec permits, and wait for the targets. */ uint32_t op; if (fid == 0) { op = SBI_IPI_OP_FENCE_I; @@ -924,11 +851,8 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) err = SBI_ERR_ALREADY_AVAIL; } else { - /* Publish START_PENDING before sbi_hal_hart_start rings the - * target MSIP: the woken target marks its own state STARTED, - * so writing PENDING afterwards could clobber it (TOCTOU). - * Roll back to STOPPED if the start request fails (only the - * pre-publish invalid-hart check can). */ + /* Publish START_PENDING before the MSIP: the woken target + * marks itself STARTED, which a later write would clobber. */ sbi_hart_state[regs[A0]] = SBI_HSM_START_PENDING; if (sbi_hal_hart_start(regs[A0], regs[A1], regs[A2]) != 0) { @@ -962,9 +886,8 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) unsigned long c; unsigned long j; uint8_t cbuf[64]; - /* Bound a single call (the kernel loops on val): this runs in - * M-mode with interrupts off, so a long write holds up fence - * IPIs from the other harts. 256 bytes is ~25 ms at 115200. */ + /* Bound one call; the kernel loops on val. Interrupts are off, so + * a long write holds up fence IPIs: 256 B is ~25 ms at 115200. */ if (n > 256UL) { n = 256UL; } @@ -1007,9 +930,8 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) #endif case SBI_EXT_SRST: - /* system_reset(type, reason). No console output here: by now the OS - * owns the UARTs and a blocked printf leaves the hart stuck in the - * handler with the reset never issued. */ + /* system_reset(type, reason). No console output: the OS owns the UARTs + * and a blocked printf strands the hart with the reset never issued. */ #ifdef DEBUG_SBI wolfBoot_printf("[SBI] SYSTEM RESET requested: type=0x%lx " "reason=0x%lx\n", regs[A0], regs[A1]); diff --git a/src/vector_riscv.S b/src/vector_riscv.S index 86ce97d190..4e4fbbf025 100644 --- a/src/vector_riscv.S +++ b/src/vector_riscv.S @@ -33,25 +33,16 @@ #define SBI_MSTACK_SHIFT 12 #endif -/* ============================================================================ - * Trap Entry/Exit Macros - * ============================================================================ */ +/* Trap entry/exit macros */ #if __riscv_xlen == 64 -/* RV64: Save all caller-saved registers and call handle_trap_ex. - * handle_trap_ex (in src/boot_riscv.c) is the regs-aware weak hook; - * it forwards to the legacy 3-arg weak handle_trap so out-of-tree - * platform overrides keep working. */ +/* RV64: save the caller-saved registers and call handle_trap_ex, the + * regs-aware weak hook, which forwards to the legacy 3-arg handle_trap. */ .macro trap_entry #ifdef WOLFBOOT_RISCV_MMODE - /* If mscratch holds a per-hart M-mode stack top (armed before mret - * into S-mode, see riscv_mmode_to_smode), switch to it: the trapped - * context's sp may be an S-mode VIRTUAL address once the OS enables - * paging, and M-mode (no translation) must not store through it. - * mscratch == 0 means an M-mode context: keep the current stack. - * (mscratch is an M-mode-only CSR; S-mode builds keep the original - * current-stack frame.) */ + /* Switch to the M-stack mscratch holds: once the OS pages, the trapped sp + * may be virtual and M-mode cannot store through it. 0 means M-mode. */ csrrw sp, mscratch, sp bnez sp, 100f csrrw sp, mscratch, sp @@ -90,17 +81,8 @@ STORE x31, 31 * REGBYTES(sp) #ifdef WOLFBOOT_RISCV_MMODE - /* Slot 2 = the trapped sp: what mscratch holds after the switch above - * (any value, 0 included), or sp + frame when no switch happened. - * Slot 0 (x0's unused slot) = 1 when this frame switched to the - * M-stack, which is the case exactly when it sits at this hart's - * M-stack top; 0 for an M-mode fallback or a nested trap. The flag - * is derived from the frame address, never from the trapped sp: a - * context trapping with sp == 0 must still re-arm mscratch on exit - * (trap_exit), or the next trap from S-mode runs the entry on an - * untranslated virtual sp and faults forever. Zero mscratch while - * inside the handler so a nested trap keeps descending on the CURRENT - * stack. x1, t0 and t1 are already saved, so they serve as scratch. */ + /* Slot 2 = the trapped sp, slot 0 = 1 when this frame switched stacks, + * derived from the frame address so an sp of 0 still re-arms mscratch. */ csrr t0, mhartid addi t0, t0, 1 slli t0, t0, SBI_MSTACK_SHIFT @@ -118,9 +100,8 @@ 102: STORE x1, 2 * REGBYTES(sp) - /* The M-mode handler's C code addresses small globals gp-relative: - * reload wolfBoot's gp (the trapped context's gp -- e.g. the OS's -- - * is restored from frame slot 3 on exit). */ + /* The handler's C code addresses small globals gp-relative; the trapped + * context's gp is restored from frame slot 3 on exit. */ .option push .option norelax la gp, __global_pointer$ @@ -141,9 +122,8 @@ .macro trap_exit #ifdef WOLFBOOT_RISCV_MMODE - /* Re-arm mscratch with this M-stack's top, but only for the - * OUTERMOST frame of a stack-switched trap (slot 0 non-zero); nested - * frames and M-mode fallback frames leave mscratch alone. */ + /* Re-arm mscratch only for the outermost frame of a stack-switched trap + * (slot 0 non-zero); nested and M-mode frames leave it alone. */ LOAD t0, 0 * REGBYTES(sp) beqz t0, 102f addi t0, sp, 32 * REGBYTES @@ -237,9 +217,7 @@ #endif /* __riscv_xlen */ -/* ============================================================================ - * Vector Table - * ============================================================================ */ +/* Vector table */ .section .isr_vector .align 8