Skip to content

Add wolfBoot port for STM32N6 (NUCLEO-N657X0-Q)#720

Open
aidangarske wants to merge 4 commits intomasterfrom
stm32n6-port-wolfboot
Open

Add wolfBoot port for STM32N6 (NUCLEO-N657X0-Q)#720
aidangarske wants to merge 4 commits intomasterfrom
stm32n6-port-wolfboot

Conversation

@aidangarske
Copy link
Copy Markdown
Member

@aidangarske aidangarske commented Mar 11, 2026

Summary

Add TrustZone (TZEN=1) support for the STM32N6 port with proper secure/non-secure SAU configuration, firmware update swap support, and an enhanced test application with UART output.

Features

  • TrustZone support (TZEN=1): wolfBoot runs from secure SRAM (0x24000000) using secure peripheral aliases. SAU configured with proper secure/non-secure regions. Application boots into non-secure state.
  • Non-TrustZone mode (TZEN=0): wolfBoot runs from non-secure SRAM (0x34000000) with blanket SAU NSC region for full memory access.
  • Firmware update (A/B swap): Full sector-by-sector swap working with PART_BOOT_EXT to handle shared XSPI2 NOR flash between boot and update partitions.
  • Enhanced test application: UART output with firmware version, partition state, boot status, and automatic wolfBoot_success() handling for TESTING state. LED indicates firmware version (blue=v1, red=v2+).
  • New config: config/examples/stm32n6-tz.config for TrustZone-enabled builds.
  • CI: Added stm32n6 and stm32n6-tz build tests to test-configs.yml.
  • Flash script: --test-update mode now writes update trigger magic (pBOOT) and auto-detects TZEN for correct SRAM load address.

Fixes

  • UART clock: Corrected PCLK2 frequency from 300 MHz to 200 MHz (IC2=400 MHz / AHB prescaler 2).
  • uart_write linkage: Removed static qualifier and fixed signature to match printf.h (unsigned int len).
  • SAU configuration: Added SAU init to hal_init() — without it, the IDAU blocks secure CPU access to XSPI2 memory-mapped region (0x70000000), causing bus faults during image verification.
  • Shared flash (PART_BOOT_EXT): Boot and update partitions share the same XSPI2 NOR flash. Without PART_BOOT_EXT, the update swap reads boot partition data via XIP while XSPI2 is in SPI command mode, causing bus faults. The ext_flash_* functions now translate absolute memory-mapped addresses to device-relative offsets.
  • XIP write buffer: nor_flash_write() copies source data to a stack buffer before issuing SPI commands, since the source pointer may reference XIP flash that becomes inaccessible when XSPI2 leaves memory-mapped mode.
  • dcache ordering: Moved dcache_enable() after octospi_init() to prevent caching stale data from the flash region before memory-mapped mode is configured.
  • Boot path for Cortex-M55: Excluded STM32N6 from blxns non-secure boot path and non-secure VTOR, since the CPU security state depends on the SRAM alias used (IDAU-based), not a runtime TrustZone transition.
  • OpenOCD: Requires upstream openocd-org/openocd (not ST fork) for target/stm32n6x.cfg support.

Test Results (NUCLEO-N657X0-Q hardware)

Test TZEN=0 TZEN=1
Build Pass Pass
Basic boot (v1) Pass Pass
UART output Pass Pass
Firmware update swap (v1→v2) Pass Pass
Test-app auto-success Pass Pass
TrustZone state Off Secure
  • Boot from cold power on

@aidangarske aidangarske self-assigned this Mar 11, 2026
Copilot AI review requested due to automatic review settings March 11, 2026 01:17

This comment was marked as resolved.

@aidangarske aidangarske force-pushed the stm32n6-port-wolfboot branch from fedaf00 to 1416f2f Compare March 11, 2026 18:37
@aidangarske aidangarske requested a review from Copilot March 11, 2026 18:40

This comment was marked as resolved.

@aidangarske aidangarske marked this pull request as ready for review March 11, 2026 18:58
Copy link
Copy Markdown
Contributor

@dgarske dgarske left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I haven't tested on hardware yet, but have looked over each line.

@dgarske dgarske assigned dgarske and wolfSSL-Bot and unassigned aidangarske Mar 16, 2026
@dgarske dgarske self-requested a review March 16, 2026 23:31
  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.
  Fix PLL1 bypass bit (PLL1BYP) in PLL1CFGR1 that Boot ROM leaves set,
  which was routing HSI 64 MHz directly to PLL output instead of the
  1200 MHz VCO. CPU now runs at 600 MHz (verified via DWT CYCCNT).

  - Clear PLL1CFGR1 BYP bit to enable VCO output
  - Simplify PLL1CFGR3 configuration to single write
  - Consolidate flash write/erase into shared nor_flash_write/erase helpers
  - Rename xspi_ functions to octospi_ for consistency with register macros
  - Add CORTEX_M55 define to arch.mk for future use
  - Add clock tree documentation in clock_config() and PWR_VOSCR
  - Combine CPUSW and SYSSW clock switch into single register write
  - Add XSPI2 RAMFUNCTION comments and TEF error handling
  - Add release announcement doc (docs/release-stm32n6.md)
  - wolfBoot binary: 23KB, test-app: 3KB
dgarske
dgarske previously approved these changes Mar 18, 2026
@dgarske dgarske assigned danielinux and unassigned dgarske Mar 18, 2026
Copilot AI review requested due to automatic review settings March 18, 2026 19:54
@dgarske dgarske force-pushed the stm32n6-port-wolfboot branch from 4100805 to cc789ae Compare March 18, 2026 19:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@dgarske dgarske assigned dgarske and unassigned danielinux Mar 18, 2026
- Fix UART: remove static from uart_write, fix signature to match
  printf.h, correct PCLK2 clock frequency (200 MHz not 300 MHz)
- Add SAU configuration: blanket NSC region for non-TZ, proper
  secure/non-secure SAU regions for TZEN=1
- Add PART_BOOT_EXT support: boot and update partitions share the
  same XSPI2 NOR flash, ext_flash_addr() translates absolute
  memory-mapped addresses to device-relative offsets
- Buffer XIP data in nor_flash_write() before SPI commands
- Move dcache_enable() after octospi_init() to prevent stale reads
- Add TZ_SECURE() macro with conditional secure/non-secure peripheral
  base addresses in hal/stm32n6.h
- Add TZEN=1 support: wolfBoot runs from secure SRAM (0x24000000),
  app boots into non-secure state, flash script auto-detects TZEN
- Exclude STM32N6 from stm32_tz.o (uses its own SAU config) and
  from blxns boot path (CORTEX_M55 uses regular boot)
- Enhanced test-app with UART output, partition info, version display,
  state handling, and auto-success for TESTING state
- Add stm32n6-tz.config example and CI entries in test-configs.yml
- Update Targets.md with TrustZone, SAU, PART_BOOT_EXT, and UART
  clock documentation
- Add DEBUG_UART=1 and RAM_CODE=1 to stm32n6.config
@dgarske dgarske force-pushed the stm32n6-port-wolfboot branch from cc789ae to 896c2a7 Compare March 18, 2026 20:14
Copilot AI review requested due to automatic review settings March 18, 2026 23:58

This comment was marked as off-topic.

Copy link
Copy Markdown
Contributor

@dgarske dgarske left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is in good shape, but the boot ROM from QSPI isn't working. @aidangarske please work on it. Not rush on this, it can be post release. I think it does require some OTP registers to be set, but I tried setting OTA124 and it didn't change the results. Note you also have to set BOOT0/BOOT1 jumpers to switch between boot modes.

@dgarske dgarske assigned aidangarske and unassigned dgarske Mar 23, 2026
Copy link
Copy Markdown
Member Author

@aidangarske aidangarske left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skoll Code Review

Scan type: review-security
Overall recommendation: COMMENT
Findings: 5 total — 4 posted, 1 skipped
4 finding(s) posted as inline comments (see file-level comments below)

Posted findings

  • [Medium] TZEN=1 boot path sets secure VTOR to non-secure application memorysrc/boot_arm.c:422-466
  • [Medium] octospi_write_enable ignores return value allowing silent flash operation failureshal/stm32n6.c:143-147
  • [Low] Flash script uses predictable temporary file pathtools/scripts/stm32n6_flash.sh:142
  • [Info] Documentation describes TZEN=1 secure SRAM alias but code uses non-secure alias for both modesdocs/Targets.md:1855-1860

Skipped findings

  • [Low] Unaligned uint32_t pointer dereference in octospi_cmd data transfer loop

Review generated by Skoll

*/

#ifdef TZEN
#if defined(__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U) && \
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [Medium] TZEN=1 boot path sets secure VTOR to non-secure application memory
Category: Security

When TZEN=1 and CORTEX_M55 are both defined (STM32N6 TrustZone build), the !defined(CORTEX_M55) guard causes the VTOR macro to use the secure VTOR address (0xE000ED08) instead of the non-secure VTOR (0xE002ED08). In do_boot(), the code writes VTOR = ((uint32_t)app_offset) where app_offset is 0x70020400 — an address in SAU region 1 which is marked as Non-Secure. This means the secure vector table pointer references NS-controlled memory.

On other TZEN platforms (stm32h5, stm32l5, stm32u5), the code uses the NS VTOR (0xE002ED08) and transitions via blxns, leaving the secure VTOR pointing to wolfBoot's own (secure) vector table. The STM32N6 path instead points the secure VTOR to non-secure memory and does a regular mov pc jump.

If a secure exception occurs after boot (e.g., SecureFault, NMI, HardFault in secure state from hardware error), the exception handler address would be fetched from NS-controlled memory. In practice, exploitation is limited because: (1) the app runs from XIP NOR flash which is read-only in memory-mapped mode, (2) no secure code remains active after boot. However, this violates the TrustZone security model where secure structures should not reference NS memory.

#if defined(__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U) && \
    defined(TZEN) && !defined(CORTEX_M55)
#define VTOR (*(volatile uint32_t *)(0xE002ED08)) /* Non-secure VTOR */
#else
#define VTOR (*(volatile uint32_t *)(0xE000ED08))  /* Used for CORTEX_M55+TZEN */
#endif

// In do_boot():
VTOR = ((uint32_t)app_offset);  // Sets SECURE VTOR to NS address 0x70020400
asm volatile("msr msp, %0" ::"r"(app_end_stack));
asm volatile("mov pc, %0":: "r"(app_entry));  // No blxns transition

Recommendation: For the TZEN=1 STM32N6 boot path, consider: (1) writing the app vector table address to the NS VTOR (0xE002ED08) instead of the secure VTOR, and (2) keeping the secure VTOR pointing to wolfBoot's own vector table in SRAM (secure memory). If the STM32N6 truly doesn't need a blxns transition, at minimum avoid corrupting the secure VTOR by writing a separate NS VTOR update: *(volatile uint32_t *)(0xE002ED08) = (uint32_t)app_offset;

Comment on lines +143 to +147
static void RAMFUNCTION octospi_write_enable(void)
{
octospi_cmd(0, WRITE_ENABLE_CMD, 0, SPI_MODE_NONE,
NULL, 0, SPI_MODE_NONE, 0);
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [Medium] octospi_write_enable ignores return value allowing silent flash operation failures
Category: Logic

The octospi_write_enable() function calls octospi_cmd() but discards the return value. If the Write Enable (WREN) command fails due to a transfer error (TEF), the function returns silently. The caller (nor_flash_write and nor_flash_erase) then proceeds with the page program or sector erase command. Without WREN set, the NOR flash ignores the write/erase command — the operation appears to succeed (no SPI error) but no data is actually written/erased.

In a bootloader context, this means a firmware update swap could silently produce corrupted data in the boot or update partition. While wolfBoot's signature verification would catch this corruption before booting, the update would fail in an unclear way, and repeated WREN failures could leave the device in a state where updates never succeed (boot loop).

static void RAMFUNCTION octospi_write_enable(void)
{
    octospi_cmd(0, WRITE_ENABLE_CMD, 0, SPI_MODE_NONE,
                NULL, 0, SPI_MODE_NONE, 0);  // Return value discarded
}

// Called from nor_flash_write:
octospi_write_enable();  // If this fails silently...
ret = octospi_cmd(0, PAGE_PROG_4B_CMD, ...);  // ...flash ignores the write

Recommendation: Return the result of octospi_cmd() from octospi_write_enable() and check it in nor_flash_write() and nor_flash_erase() before proceeding with the program/erase command. Example:

static int RAMFUNCTION octospi_write_enable(void) {
    return octospi_cmd(0, WRITE_ENABLE_CMD, ...);
}

And in callers:

if (octospi_write_enable() < 0) { ret = -1; break; }

PART_SIZE=$(grep -E '^WOLFBOOT_PARTITION_SIZE' "${WOLFBOOT_ROOT}/.config" | head -1 | sed 's/.*=//;s/[[:space:]]//g')
TRIGGER_ADDR=$(printf "0x%08x" $(( ${UPDATE_ADDR} + ${PART_SIZE} - 5 )))
echo -e "${CYAN} Update trigger -> NOR ${TRIGGER_ADDR}${NC}"
printf 'pBOOT' > /tmp/trigger_magic.bin
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [Low] Flash script uses predictable temporary file path
Category: Security

The flash script writes the update trigger magic bytes to a hardcoded predictable path /tmp/trigger_magic.bin. In a multi-user development environment, another user could pre-create a symlink at this path pointing to a sensitive file, causing the printf write to overwrite that file. The impact is limited since this is a development flashing tool, not production code, and the data written is just 5 bytes ('pBOOT').

printf 'pBOOT' > /tmp/trigger_magic.bin
OPENOCD_CMDS+="flash write_image /tmp/trigger_magic.bin ${TRIGGER_ADDR}; "

Recommendation: Use mktemp for a unique temporary file:

TRIGGER_FILE=$(mktemp /tmp/trigger_magic.XXXXXX)
printf 'pBOOT' > "${TRIGGER_FILE}"
OPENOCD_CMDS+="flash write_image ${TRIGGER_FILE} ${TRIGGER_ADDR}; "
# Clean up after use
trap 'rm -f ${TRIGGER_FILE}' EXIT

Comment on lines +1855 to +1860

```sh
# Without TrustZone:
cp config/examples/stm32n6.config .config
make
make flash
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚪ [Info] Documentation describes TZEN=1 secure SRAM alias but code uses non-secure alias for both modes
Category: Logic

The documentation states that with TrustZone enabled, wolfBoot runs from the secure SRAM alias at 0x24180400, while without TrustZone it runs from 0x34180400. However, arch.mk sets WOLFBOOT_ORIGIN=0x34180400 unconditionally for both TZEN=0 and TZEN=1, with a comment explaining: 'Use the same address for both TZEN=0 and TZEN=1 since the Boot ROM always uses the non-secure alias.'

The flash script also hardcodes SRAM_ADDR=0x34180400 without TZEN detection, despite the PR description stating 'auto-detects TZEN for correct SRAM load address.'

This is a documentation inconsistency rather than a code bug — the code and arch.mk comment are self-consistent, but the docs and PR description are misleading.

# arch.mk:
WOLFBOOT_ORIGIN=0x34180400  # Same for both TZEN=0 and TZEN=1

# docs/Targets.md claims:
# AXISRAM2 — with TrustZone (TZEN=1, secure alias):
#   0x24180400  wolfBoot FSBL (secure — IDAU marks 0x24xxxxxx as secure)

# stm32n6_flash.sh:
SRAM_ADDR=0x34180400  # No TZEN detection

Recommendation: Update the documentation in docs/Targets.md to clarify that wolfBoot is always loaded and linked at the non-secure alias 0x34180400, with SAU configuration in hal_init() providing the necessary access control. The memory layout table should note that 0x24180400 is the secure alias of the same physical memory, not a separate load address. Also update the PR description to remove the claim about TZEN auto-detection in the flash script.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants