Add wolfBoot port for STM32N6 (NUCLEO-N657X0-Q)#720
Add wolfBoot port for STM32N6 (NUCLEO-N657X0-Q)#720aidangarske wants to merge 1 commit intomasterfrom
Conversation
Add HAL, build system, test app, and documentation for the STM32N6 (Cortex-M55) targeting the NUCLEO-N657X0-Q board. wolfBoot runs from SRAM as FSBL and boots a signed application via XIP from external NOR flash on XSPI2.
There was a problem hiding this comment.
Pull request overview
Adds a new wolfBoot port for STM32N6 / NUCLEO-N657X0-Q, including a bare-metal HAL for XSPI2 NOR XIP, flash/debug tooling via OpenOCD, and CI/docs updates.
Changes:
- Adds STM32N6 HAL + linker scripts to run wolfBoot from SRAM and boot an XIP app from external NOR
- Adds a STM32N6 test-app, build integration, example config, and CI build job
- Adds OpenOCD configuration + a flash script and updates target documentation
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/scripts/stm32n6_flash.sh | Automates building/flashing via OpenOCD and boots wolfBoot from SRAM |
| test-app/app_stm32n6.c | New bare-metal test app for STM32N6 (LED + XIP flash op + wolfBoot_success) |
| test-app/Makefile | Adds stm32n6 build flags/linker script integration for the test app |
| test-app/ARM-stm32n6.ld | Linker script for XIP test-app in NOR and runtime data in SRAM |
| hal/stm32n6.ld | Linker script for wolfBoot executing from SRAM |
| hal/stm32n6.h | STM32N6 register/bit definitions for clocks, GPIO, XSPI2, UART, cache ops |
| hal/stm32n6.c | STM32N6 HAL implementation (clock/power, XSPI2 NOR driver, flash/ext_flash API) |
| docs/Targets.md | New STM32N6 target documentation (memory map, build/flash/debug workflow) |
| config/openocd/openocd_stm32n6.cfg | OpenOCD target config + XSPI2 init and stmqspi flash bank setup |
| config/examples/stm32n6.config | Example wolfBoot config for STM32N6 + external flash partitioning |
| arch.mk | Adds stm32n6 target selection and ARM build settings (mcpu=cortex-m55, origins) |
| Makefile | Sets stm32n6 main targets and introduces a stm32n6-specific flash target |
| .github/workflows/test-configs.yml | Adds CI build verification job for stm32n6 example config |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| # Flash via OpenOCD | ||
| echo -e "${GREEN}[2/2] Programming via OpenOCD...${NC}" | ||
| pkill -9 openocd 2>/dev/null || true |
There was a problem hiding this comment.
pkill -9 openocd will forcibly terminate any OpenOCD instance on the system (including unrelated sessions) and may cause data loss. Prefer targeting only the OpenOCD instance started by this script (e.g., track PID) or at least restrict the match (e.g., exact process name) and avoid SIGKILL unless necessary.
| pkill -9 openocd 2>/dev/null || true | |
| pkill -x openocd 2>/dev/null || true |
| ENTRY_ADDR=$(od -A n -t x4 -N 8 "${WOLFBOOT_ROOT}/wolfboot.bin" | awk '{print "0x"$2}') | ||
| ENTRY_THUMB=$(printf "0x%08x" $(( ${ENTRY_ADDR} | 1 ))) | ||
| echo -e "${CYAN} Booting wolfBoot (entry: ${ENTRY_THUMB})...${NC}" | ||
| OPENOCD_CMDS+="reg msplim_s 0x00000000; " | ||
| OPENOCD_CMDS+="reg psplim_s 0x00000000; " | ||
| OPENOCD_CMDS+="reg msp 0x34020000; " |
There was a problem hiding this comment.
The script hard-codes MSP to 0x34020000 instead of using the initial stack pointer from the wolfBoot vector table (word 0 of the image). If wolfBoot’s link script/startup changes stack placement (or uses a different top-of-stack), this can crash immediately; parse the initial SP from the first 32-bit word of wolfboot.bin and program reg msp from that value.
| ENTRY_ADDR=$(od -A n -t x4 -N 8 "${WOLFBOOT_ROOT}/wolfboot.bin" | awk '{print "0x"$2}') | |
| ENTRY_THUMB=$(printf "0x%08x" $(( ${ENTRY_ADDR} | 1 ))) | |
| echo -e "${CYAN} Booting wolfBoot (entry: ${ENTRY_THUMB})...${NC}" | |
| OPENOCD_CMDS+="reg msplim_s 0x00000000; " | |
| OPENOCD_CMDS+="reg psplim_s 0x00000000; " | |
| OPENOCD_CMDS+="reg msp 0x34020000; " | |
| # Read initial stack pointer (word 0) and reset vector (word 1) from vector table | |
| read -r INITIAL_SP_WORD ENTRY_WORD <<< "$(od -A n -t x4 -N 8 "${WOLFBOOT_ROOT}/wolfboot.bin")" | |
| INITIAL_SP="0x${INITIAL_SP_WORD}" | |
| ENTRY_ADDR="0x${ENTRY_WORD}" | |
| ENTRY_THUMB=$(printf "0x%08x" $(( ${ENTRY_ADDR} | 1 ))) | |
| echo -e "${CYAN} Booting wolfBoot (entry: ${ENTRY_THUMB})...${NC}" | |
| OPENOCD_CMDS+="reg msplim_s 0x00000000; " | |
| OPENOCD_CMDS+="reg psplim_s 0x00000000; " | |
| OPENOCD_CMDS+="reg msp ${INITIAL_SP}; " |
| if (fmode == 0) { | ||
| while (!(XSPI2_SR & (XSPI_SR_FTF | XSPI_SR_TEF))) | ||
| ; | ||
| if (XSPI2_SR & XSPI_SR_TEF) goto xspi_err; | ||
| XSPI2_DR32 = *(uint32_t *)data; | ||
| } else { | ||
| while (!(XSPI2_SR & (XSPI_SR_FTF | XSPI_SR_TCF | | ||
| XSPI_SR_TEF))) | ||
| ; | ||
| if (XSPI2_SR & XSPI_SR_TEF) goto xspi_err; | ||
| *(uint32_t *)data = XSPI2_DR32; |
There was a problem hiding this comment.
Casting uint8_t* to uint32_t* can cause unaligned 32-bit loads/stores (and potentially faults) when callers pass non-4-byte-aligned buffers. Use a temporary uint32_t + memcpy (or fall back to byte I/O when unaligned) to make the read/write path alignment-safe.
| if (fmode == 0) { | |
| while (!(XSPI2_SR & (XSPI_SR_FTF | XSPI_SR_TEF))) | |
| ; | |
| if (XSPI2_SR & XSPI_SR_TEF) goto xspi_err; | |
| XSPI2_DR32 = *(uint32_t *)data; | |
| } else { | |
| while (!(XSPI2_SR & (XSPI_SR_FTF | XSPI_SR_TCF | | |
| XSPI_SR_TEF))) | |
| ; | |
| if (XSPI2_SR & XSPI_SR_TEF) goto xspi_err; | |
| *(uint32_t *)data = XSPI2_DR32; | |
| uint32_t tmp32; | |
| if (fmode == 0) { | |
| while (!(XSPI2_SR & (XSPI_SR_FTF | XSPI_SR_TEF))) | |
| ; | |
| if (XSPI2_SR & XSPI_SR_TEF) goto xspi_err; | |
| memcpy(&tmp32, data, sizeof(tmp32)); | |
| XSPI2_DR32 = tmp32; | |
| } else { | |
| while (!(XSPI2_SR & (XSPI_SR_FTF | XSPI_SR_TCF | | |
| XSPI_SR_TEF))) | |
| ; | |
| if (XSPI2_SR & XSPI_SR_TEF) goto xspi_err; | |
| tmp32 = XSPI2_DR32; | |
| memcpy(data, &tmp32, sizeof(tmp32)); |
| xspi_write_enable(); | ||
| xspi_cmd(0, NOR_CMD_PAGE_PROG_4B, | ||
| offset, XSPI_MODE_SINGLE, | ||
| (uint8_t *)data, write_sz, XSPI_MODE_SINGLE, 0); | ||
|
|
||
| xspi_wait_ready(); |
There was a problem hiding this comment.
xspi_cmd() reports errors (returns -1), but the result is ignored in hal_flash_write() (and similarly in erase/read/write paths). This makes failures silent and still returns success to the caller; capture the return value and propagate an error (and avoid re-enabling mmap / cache ops as if the transaction succeeded).
|
|
||
| void hal_prepare_boot(void) | ||
| { | ||
| xspi_enable_mmap(); |
There was a problem hiding this comment.
Disabling D-cache without cleaning/invalidation can lose dirty data and cause memory corruption. hal_prepare_boot() calls dcache_disable() directly; dcache_disable() should perform a clean+invalidate (typically by set/way) before clearing SCB_CCR_DC, or hal_prepare_boot() should do so prior to disabling.
| void hal_prepare_boot(void) | |
| { | |
| xspi_enable_mmap(); | |
| /* | |
| * Clean and invalidate the entire data cache by set/way. | |
| * This ensures all dirty lines are written back to memory | |
| * before the D-cache is disabled. | |
| */ | |
| static void dcache_clean_invalidate_all(void) | |
| { | |
| uint32_t clid = SCB->CLIDR; | |
| uint32_t level; | |
| for (level = 0U; level < 7U; level++) { | |
| uint32_t ctype = (clid >> (level * 3U)) & 0x7U; | |
| if (ctype == 0U) { | |
| /* No more cache levels present */ | |
| break; | |
| } | |
| /* Skip levels without a data or unified cache */ | |
| if ((ctype < 2U) || (ctype == 4U)) { | |
| continue; | |
| } | |
| /* Select this level, data/unified cache */ | |
| SCB->CSSELR = (level << SCB_CSSELR_LEVEL_Pos); | |
| __DSB(); | |
| uint32_t ccsidr = SCB->CCSIDR; | |
| uint32_t num_sets = | |
| (ccsidr & SCB_CCSIDR_NUMSETS_Msk) >> SCB_CCSIDR_NUMSETS_Pos; | |
| uint32_t num_ways = | |
| (ccsidr & SCB_CCSIDR_ASSOCIATIVITY_Msk) >> | |
| SCB_CCSIDR_ASSOCIATIVITY_Pos; | |
| for (uint32_t set = 0U; set <= num_sets; set++) { | |
| for (uint32_t way = 0U; way <= num_ways; way++) { | |
| uint32_t sw = (level << SCB_DCISW_LEVEL_Pos) | | |
| (set << SCB_DCISW_SET_Pos) | | |
| (way << SCB_DCISW_WAY_Pos); | |
| SCB->DCCISW = sw; | |
| } | |
| } | |
| } | |
| __DSB(); | |
| __ISB(); | |
| } | |
| void hal_prepare_boot(void) | |
| { | |
| xspi_enable_mmap(); | |
| dcache_clean_invalidate_all(); |
| .data : AT (_stored_data) | ||
| { | ||
| _start_data = .; | ||
| KEEP(*(.data*)) |
There was a problem hiding this comment.
Using KEEP(*(.data*)) prevents --gc-sections from discarding unused data sections, which can significantly inflate the XIP image size (especially with -fdata-sections). Consider removing KEEP for .data* and only keeping what must be retained (e.g., vector table and .ramcode).
| KEEP(*(.data*)) | |
| *(.data*) |
| .edidx : | ||
| { | ||
| . = ALIGN(4); | ||
| *(.ARM.exidx*) | ||
| } > FLASH |
There was a problem hiding this comment.
The output section name .edidx is unusual and looks like a typo; the conventional naming is .ARM.exidx (or at least .exidx). While the linker will still place *(.ARM.exidx*) correctly, renaming the section improves clarity and makes map files easier to interpret.
Description
sets up XSPI2, probes the NOR flash, and then flash write_image programs it. On top of that, we also need to load_image wolfBoot into SRAM and manually set up VTOR/MSP/resume
Details
hal/stm32n6.c,hal/stm32n6.h): Clock, PWR, GPIO, XSPI2, cache, and debug UART. All flash functions are RAMFUNCTION for safe operation fromXIP.
arch.mk,Makefile,test-app/Makefile): CORTEX_M33 with -mcpu=cortex-m55 override, EXT_FLASH for update/swap partitionstest-app/app_stm32n6.c): LED blink on Port G (active LOW), flash erase test, wolfBoot_success() call from XIPhal/stm32n6.ld,test-app/ARM-stm32n6.ld): wolfBoot at 0x34000000 (SRAM), test app RAM at 0x34010000tools/scripts/stm32n6_flash.sh,config/openocd/openocd_stm32n6.cfg): Programs NOR flash and loads wolfBoot to SRAM viaOpenOCD/ST-Link
config/examples/stm32n6.config): ECC256+SHA256, boot at 0x70020000, update at 0x70120000.github/workflows/test-configs.yml): Build verificationdocs/Targets.md): Memory layout, build/flash instructions, XIP constraints, debuggingTest plan
makewith stm32n6.config)