From 3e524d4b008813c53dfdd349d18d2daa78ef31d9 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Wed, 4 Feb 2026 15:14:09 +0000 Subject: [PATCH 01/20] Create a nucleo-n657x0-q platform Signed-off-by: Brendan Moran --- flake.nix | 21 + nix/nucleo-n657x0-q/default.nix | 265 +++++++++ nix/util.nix | 1 + .../platform/nucleo-n657x0-q/README.md | 109 ++++ .../platform/nucleo-n657x0-q/exec_wrapper.py | 516 ++++++++++++++++++ .../platform/nucleo-n657x0-q/make_argv_bin.py | 52 ++ .../platform/nucleo-n657x0-q/platform.mk | 104 ++++ .../platform/nucleo-n657x0-q/src/cmdline.c | 63 +++ .../nucleo-n657x0-q/src/cmdline_region.c | 10 + .../platform/nucleo-n657x0-q/src/libfns.c | 67 +++ .../nucleo-n657x0-q/src/semihosting_syscall.c | 27 + .../nucleo-n657x0-q/src/semihosting_syscall.h | 15 + test/hal/hal.c | 8 +- test/hal/pmu_armv8.h | 7 + test/mk/components.mk | 5 + 15 files changed, 1268 insertions(+), 2 deletions(-) create mode 100644 nix/nucleo-n657x0-q/default.nix create mode 100644 test/baremetal/platform/nucleo-n657x0-q/README.md create mode 100755 test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py create mode 100644 test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py create mode 100644 test/baremetal/platform/nucleo-n657x0-q/platform.mk create mode 100644 test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c create mode 100644 test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c create mode 100644 test/baremetal/platform/nucleo-n657x0-q/src/libfns.c create mode 100644 test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c create mode 100644 test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h create mode 100644 test/hal/pmu_armv8.h diff --git a/flake.nix b/flake.nix index 8dac3c4368..092323908f 100644 --- a/flake.nix +++ b/flake.nix @@ -47,6 +47,7 @@ export PATH=$PWD/scripts:$PATH export PROOF_DIR="$PWD/proofs/hol_light" ''; + in { _module.args.pkgs = import inputs.nixpkgs { @@ -90,6 +91,26 @@ } ++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ config.packages.valgrind_varlat ]; }; + # arm-none-eabi-gcc + platform files from pqmx + packages.m55-an547 = util.m55-an547; + packages.avr-toolchain = util.avr-toolchain; + devShells.arm-embedded = util.mkShell { + packages = builtins.attrValues + { + inherit (config.packages) m55-an547; + inherit (pkgs) gcc-arm-embedded qemu coreutils python3 git; + }; + }; + packages.nucleo-n657x0-q = util.nucleo-n657x0-q; + devShells.nucleo-n657x0-q = util.mkShell { + packages = builtins.attrValues ({ + inherit (config.packages) nucleo-n657x0-q; + inherit (pkgs) gcc-arm-embedded coreutils git libffi pkg-config; + }); + }; + + + devShells.avr = util.mkShell (import ./nix/avr { inherit pkgs; }); packages.hol_server = util.hol_server.hol_server_start; devShells.hol_light = (util.mkShell { packages = builtins.attrValues { inherit (config.packages) linters hol_light s2n_bignum hol_server; }; diff --git a/nix/nucleo-n657x0-q/default.nix b/nix/nucleo-n657x0-q/default.nix new file mode 100644 index 0000000000..08ad652981 --- /dev/null +++ b/nix/nucleo-n657x0-q/default.nix @@ -0,0 +1,265 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +{ stdenvNoCC +, fetchFromGitHub +, writeText +}: +stdenvNoCC.mkDerivation { + pname = "mlkem-native-nucleo-n657x0-q"; + version = "main"; + + # Fetch STM32CubeN6 (Template + CMSIS headers) + # TODO(GAP): Pin rev + hash for reproducibility. + src = fetchFromGitHub { + owner = "STMicroelectronics"; + repo = "STM32CubeN6"; + rev = "1fc803683e03b0ce78e64523441d31acd0db2829"; + hash = "sha256-7iD7R3A+0YAfMZqKndWBq+z5TUY37XDVxFy5HK8/5Aw="; + fetchSubmodules = true; + }; + + dontBuild = true; + + propagatedBuildInputs = [ ]; + + installPhase = '' + set -eu + + outp="$out/platform/nucleo-n657x0-q/src/platform" + mkdir -p "$outp" + + tpl="Projects/NUCLEO-N657X0-Q/Templates/Template" + + if [ ! -d "$tpl" ]; then + echo "ERROR: expected Cube template at $tpl" + exit 1 + fi + + # Copy CMSIS headers needed by system/startup from the Cube tree + mkdir -p "$outp/Drivers/CMSIS/Core" + mkdir -p "$outp/Drivers/CMSIS/Device" + + cp -r "Drivers/CMSIS/Core/Include" "$outp/Drivers/CMSIS/Core/" + # Ensure m-profile subdir (pmu_armv8.h) is present, as some builds include it directly + if [ -d "Drivers/CMSIS/Core/Include/m-profile" ]; then + mkdir -p "$outp/Drivers/CMSIS/Core/Include" + cp -r "Drivers/CMSIS/Core/Include/m-profile" "$outp/Drivers/CMSIS/Core/Include/" || true + fi + cp -r "Drivers/CMSIS/Device/ST" "$outp/Drivers/CMSIS/Device/" + + # Copy startup + system + linker script + # Prefer board-specific FSBL-LRUN files explicitly when available. + fsbl_tpl="Projects/NUCLEO-N657X0-Q/Templates/Template_FSBL_LRUN" + mkdir -p "$outp/gcc" "$outp/gcc/linker" + if [ -d "$fsbl_tpl" ]; then + # Explicitly select: + # - Startup: STM32CubeIDE/Boot/Startup/startup_stm32n657xx_fsbl.s + # - Linker: STM32CubeIDE/AppS/STM32N657XX_LRUN.ld + # - System: FSBL/Src/system_stm32n6xx_fsbl.c + # - IT/MSP: FSBL/Src/stm32n6xx_it.c, stm32n6xx_hal_msp.c + start_src="$fsbl_tpl/STM32CubeIDE/Boot/Startup/startup_stm32n657xx_fsbl.s" + ld_src="$fsbl_tpl/STM32CubeIDE/AppS/STM32N657XX_LRUN.ld" + sys_src="$fsbl_tpl/FSBL/Src/system_stm32n6xx_fsbl.c" + it_src="$fsbl_tpl/FSBL/Src/stm32n6xx_it.c" + msp_src="$fsbl_tpl/FSBL/Src/stm32n6xx_hal_msp.c" + if [ -f "$start_src" ]; then + # Normalize to canonical name searched by platform.mk + cp -v "$start_src" "$outp/gcc/startup_stm32n657xx.S" + fi + if [ -f "$ld_src" ]; then + cp -v "$ld_src" "$outp/gcc/linker/STM32N657XX_LRUN.ld" + fi + if [ -f "$sys_src" ]; then + cp -v "$sys_src" "$outp/system_stm32n6xx.c" + fi + if [ -f "$it_src" ]; then + cp -v "$it_src" "$outp/" + fi + if [ -f "$msp_src" ]; then + cp -v "$msp_src" "$outp/" + fi + else + # Generic fallback to CMSIS templates if FSBL files are not located + cp -r Drivers/CMSIS/Device/ST/STM32N6xx/Source/Templates/* "$outp" || true + cp -r Drivers/CMSIS/Device/ST/STM32N6xx/Source/Templates/gcc/* "$outp/gcc/" || true + fi + + # Copy HAL driver includes and sources so FSBL files build if they reference HAL symbols + mkdir -p "$outp/Drivers/STM32N6xx_HAL_Driver" + if [ -d Drivers/STM32N6xx_HAL_Driver/Inc ]; then + cp -r Drivers/STM32N6xx_HAL_Driver/Inc "$outp/Drivers/STM32N6xx_HAL_Driver/" || true + fi + if [ -d Drivers/STM32N6xx_HAL_Driver/Src ]; then + cp -r Drivers/STM32N6xx_HAL_Driver/Src "$outp/Drivers/STM32N6xx_HAL_Driver/" || true + fi + # Guarantee HAL Driver Src exists under output (fallback search if not found in expected path) + if [ ! -d "$outp/Drivers/STM32N6xx_HAL_Driver/Src" ]; then + alt_hal_dir=$(find Drivers -type d -name 'STM32N6xx_HAL_Driver' -print -quit || true) + if [ -n "$alt_hal_dir" ] && [ -d "$alt_hal_dir/Src" ]; then + echo "Copying HAL Driver Src from $alt_hal_dir" + mkdir -p "$outp/Drivers/STM32N6xx_HAL_Driver" + cp -r "$alt_hal_dir/Src" "$outp/Drivers/STM32N6xx_HAL_Driver/" || true + fi + fi + + + # Copy HAL configuration header, preferring FSBL-LRUN Inc; then disable BSEC and XSPI modules + fsbl_inc_conf="$fsbl_tpl/FSBL/Inc/stm32n6xx_hal_conf.h" + if [ -f "$fsbl_inc_conf" ]; then + mkdir -p "$outp/Inc" + cp -v "$fsbl_inc_conf" "$outp/Inc/stm32n6xx_hal_conf.h" + # Also provide FSBL interrupt header in include path if present + fsbl_it_hdr="$fsbl_tpl/FSBL/Inc/stm32n6xx_it.h" + if [ -f "$fsbl_it_hdr" ]; then + cp -v "$fsbl_it_hdr" "$outp/Inc/stm32n6xx_it.h" + fi + # Also provide FSBL main.h in include path if present + fsbl_main_hdr="$fsbl_tpl/FSBL/Inc/main.h" + if [ -f "$fsbl_main_hdr" ]; then + cp -v "$fsbl_main_hdr" "$outp/Inc/main.h" + fi + else + # Fallback: Copy the first hal_conf found under Projects + hal_conf=$(find Projects -type f -name 'stm32n6xx_hal_conf.h' | head -n1 || true) + if [ -n "$hal_conf" ]; then + incdir=$(dirname "$hal_conf") + cp -r "$incdir" "$outp/Inc" + # Attempt to locate an stm32n6xx_it.h nearby if FSBL header not used + it_hdr=$(find "$(dirname "$incdir")" -name 'stm32n6xx_it.h' -type f | head -n1 || true) + if [ -n "$it_hdr" ] && [ ! -f "$outp/Inc/stm32n6xx_it.h" ]; then + cp -v "$it_hdr" "$outp/Inc/stm32n6xx_it.h" + fi + # Attempt to locate a main.h nearby if FSBL header not used + main_hdr=$(find "$(dirname "$incdir")" -name 'main.h' -type f | head -n1 || true) + if [ -n "$main_hdr" ] && [ ! -f "$outp/Inc/main.h" ]; then + cp -v "$main_hdr" "$outp/Inc/main.h" + fi + fi + fi + + # Ensure BSEC and XSPI are disabled for our test build + conf="$outp/Inc/stm32n6xx_hal_conf.h" + if [ -f "$conf" ]; then + # Comment out any explicit enable lines + sed -i.bak -E 's@^\s*#\s*define\s+HAL_BSEC_MODULE_ENABLED@/* #define HAL_BSEC_MODULE_ENABLED */@' "$conf" || true + sed -i.bak -E 's@^\s*#\s*define\s+HAL_XSPI_MODULE_ENABLED@/* #define HAL_XSPI_MODULE_ENABLED */@' "$conf" || true + # Also handle commented-but-enabled variants + sed -i.bak -E 's@^\s*//\s*#\s*define\s+HAL_BSEC_MODULE_ENABLED@/* #define HAL_BSEC_MODULE_ENABLED */@' "$conf" || true + sed -i.bak -E 's@^\s*//\s*#\s*define\s+HAL_XSPI_MODULE_ENABLED@/* #define HAL_XSPI_MODULE_ENABLED */@' "$conf" || true + sed -i.bak -E 's@^\s*/\*\s*#\s*define\s+HAL_BSEC_MODULE_ENABLED\s*\*/@/* #define HAL_BSEC_MODULE_ENABLED */@' "$conf" || true + sed -i.bak -E 's@^\s*/\*\s*#\s*define\s+HAL_XSPI_MODULE_ENABLED\s*\*/@/* #define HAL_XSPI_MODULE_ENABLED */@' "$conf" || true + # Belt-and-suspenders: append explicit undefs to override any includes afterward + printf "\n#undef HAL_BSEC_MODULE_ENABLED\n#undef HAL_XSPI_MODULE_ENABLED\n" >> "$conf" + fi + + # Extract SystemClock_Config and related user clock snippets from FSBL template into a standalone clock_config.c + fsbl_main_c="$fsbl_tpl/FSBL/Src/main.c" + clock_out="$outp/clock_config.c" + if [ -f "$fsbl_main_c" ]; then + echo "Generating clock_config.c from $fsbl_main_c" + tmpclk="$TMPDIR/clock_config.$$.$RANDOM.c" + : > "$tmpclk" + # 1) Header block + awk ' + /\/\* USER CODE BEGIN Header \*\// { print; inhdr=1; next } + inhdr { print } + /\/\* USER CODE END Header \*\// { print; inhdr=0 } + ' "$fsbl_main_c" >> "$tmpclk" + # 2) Includes lines + printf "\n/* Includes ------------------------------------------------------------------*/\n" >> "$tmpclk" + printf "#include \"main.h\"\n\n" >> "$tmpclk" + # 3) USER CLK 1 block + awk ' + /\/\* USER CODE BEGIN CLK 1 \*\// { print; inclk=1; next } + inclk { print } + /\/\* USER CODE END CLK 1 \*\// { print; inclk=0 } + ' "$fsbl_main_c" >> "$tmpclk" + printf "\n" >> "$tmpclk" + # 4) SystemClock_Config (try to include preceding comment header if present) + awk ' + BEGIN { copy=0; lvl=0; sig=0 } + # Signature with brace on same line + /^[ \t]*void[ \t]+SystemClock_Config[ \t]*\([ \t]*void[ \t]*\)[ \t]*\{/ { + print; copy=1; lvl=1; next + } + # Signature line without brace; start copying and wait for opening brace + /^[ \t]*void[ \t]+SystemClock_Config[ \t]*\([ \t]*void[ \t]*\)[ \t]*$/ { + print; copy=1; sig=1; next + } + copy { + if (sig) { + # Print until we see the first brace; then start level tracking + if ($0 ~ /\{/) { + nopen=gsub(/{/,"{"); nclose=gsub(/}/,"}"); lvl += nopen - nclose; print; sig=0; + if (lvl<=0) exit; next + } + print; next + } + nopen=gsub(/{/,"{"); nclose=gsub(/}/,"}"); lvl += nopen - nclose; print; + if (lvl<=0) exit + } + ' "$fsbl_main_c" >> "$tmpclk" + # Sanity check: ensure we captured the function signature + if ! grep -q "SystemClock_Config" "$tmpclk"; then + echo "ERROR: Failed to extract SystemClock_Config from $fsbl_main_c" >&2 + exit 1 + fi + mv "$tmpclk" "$clock_out" + # Ensure main.h exists and declares needed prototypes + mkdir -p "$outp/Inc" + fsbl_main_h="$fsbl_tpl/FSBL/Inc/main.h" + dest_main_h="$outp/Inc/main.h" + if [ ! -f "$dest_main_h" ] && [ -f "$fsbl_main_h" ]; then + cp -v "$fsbl_main_h" "$dest_main_h" + fi + if [ -f "$dest_main_h" ]; then + if ! grep -q "void[[:space:]]\+SystemClock_Config[[:space:]]*([[:space:]]*void[[:space:]]*)" "$dest_main_h"; then + printf "\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n" >> "$dest_main_h" + printf "void SystemClock_Config(void);\n" >> "$dest_main_h" + printf "#ifdef __cplusplus\n}\n#endif\n" >> "$dest_main_h" + fi + if ! grep -q "void[[:space:]]\+Error_Handler[[:space:]]*([[:space:]]*void[[:space:]]*)" "$dest_main_h"; then + printf "void Error_Handler(void);\n" >> "$dest_main_h" + fi + fi + else + echo "WARNING: FSBL main.c not found at $fsbl_main_c; skipping clock_config generation" >&2 + fi + + # Patch linker script to increase stack to 128 KiB (0x20000) for test workloads + # Locate STM32N657XX_LRUN.ld under gcc/ or gcc/linker/ + ldpath="" + for cand in \ + "$outp/gcc/linker/STM32N657XX_LRUN.ld" \ + "$outp/gcc/STM32N657XX_LRUN.ld" \ + $(find "$outp" -type f -name STM32N657XX_LRUN.ld 2>/dev/null | head -n1) + do + if [ -f "$cand" ]; then ldpath="$cand"; break; fi + done + if [ -n "$ldpath" ]; then + echo "Patching stack size in $ldpath to 0x20000" + # Common ST patterns + sed -i.bak -E 's/(\b_Min_Stack_Size\s*=\s*)0x[0-9a-fA-F]+;/\10x20000;/' "$ldpath" || true + sed -i.bak -E 's/(\b__STACK_SIZE\s*=\s*)0x[0-9a-fA-F]+;/\10x20000;/' "$ldpath" || true + sed -i.bak -E 's/(\b__stack_size__\s*=\s*)0x[0-9a-fA-F]+;/\10x20000;/' "$ldpath" || true + # If none of the vars exist, try to adjust the stack reservation directly + if ! grep -Eq '\b_Min_Stack_Size\b|\b__STACK_SIZE\b|\b__stack_size__\b' "$ldpath"; then + sed -i.bak -E 's/(\. = \. \+ )_Min_Stack_Size;/\10x20000;/' "$ldpath" || true + fi + else + echo "WARNING: Could not find STM32N657XX_LRUN.ld to patch stack size" >&2 + fi + ''; + + setupHook = writeText "setup-hook.sh" '' + export NUCLEO_N657X0_Q_PATH="$1/platform/nucleo-n657x0-q/src/platform/" + # Platform sources only; runtime debug server provided by STM32CubeCLT on host. + ''; + + meta = { + description = "Platform files for STM32 NUCLEO-N657X0-Q (use STM32Cube Command Line Tools gdbserver)"; + homepage = "https://github.com/STMicroelectronics/STM32CubeN6"; + }; +} diff --git a/nix/util.nix b/nix/util.nix index 94ba3d9f2f..ad509c0b4b 100644 --- a/nix/util.nix +++ b/nix/util.nix @@ -106,6 +106,7 @@ rec { s2n_bignum = pkgs.callPackage ./s2n_bignum { }; slothy = pkgs.callPackage ./slothy { }; m55-an547 = pkgs.callPackage ./m55-an547-arm-none-eabi { }; + nucleo-n657x0-q = pkgs.callPackage ./nucleo-n657x0-q { }; avr-toolchain = pkgs.callPackage ./avr { }; # Helper function to build individual cross toolchains diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md new file mode 100644 index 0000000000..2dbf8b3a07 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -0,0 +1,109 @@ + + +# NUCLEO-N657X0-Q Baremetal Platform + +This platform runs ML-KEM tests on the ST NUCLEO‑N657X0‑Q board using STM32Cube Command Line Tools (CLT) and ST‑LINK gdbserver. The `exec_wrapper.py` launches the gdbserver, injects argv into target memory, streams semihost output, and runs a batch `gdb` session against the board. + +## Prerequisites +- Install STM32Cube CLT (includes ST‑LINK gdbserver and STM32CubeProgrammer): + - Download: https://www.st.com/en/development-tools/stm32cubeclt.html + - Verify tools are present on your PATH (example macOS install path): + - `/opt/ST/STM32CubeCLT_/STLink-gdb-server/bin/ST-LINK_gdbserver` + - `/opt/ST/STM32CubeCLT_/STM32CubeProgrammer/bin/STM32_Programmer_CLI` +- Hardware: NUCLEO‑N657X0‑Q connected over USB. Update ST‑LINK firmware if prompted: + - macOS app: `/STM32CubeProgrammer/stlink/STLinkUpgrade.app` + - CLI: `/STM32CubeProgrammer/stlink/STLinkUpgrade` + +## Environment Variables (exec_wrapper.py) +- `GDB` (default: `arm-none-eabi-gdb`) – gdb binary. +- `GDB_PORT` (default: `3333`) – gdbserver port. +- `ST_CUBE_CLT_ROOT` – CLT root; helps auto‑locate gdbserver and CLI. +- `ST_CUBE_PROG_PATH` – path to `STM32_Programmer_CLI` bin dir; passed via `-cp`. +- `ST_GDBSERVER_CMD` – optional template to override gdbserver command. +- `STLINK_SPEED` (default: `200`) – SWD speed in kHz (e.g. `50` for reliability). +- `STLINK_SERIAL` – ST‑LINK serial string (strongly recommended when multiple probes). +- `STLINK_APID` (default: `1`) – Access Port/core selection. +- `STLINK_TRANSPORT` (default: `SWD`) – debug transport. +- `STLINK_CONNECT_MODE` (default: `under-reset`) – connection mode hint. +- `ST_DEVICE` (default: `STM32N657X0HxQ`) – device name hint (not always used). +- `STLINK_PEND_HALT_TIMEOUT` (default: `8000`) – pending halt timeout (ms). +- `STLINK_SEMIHOST_PORT` (default: auto) – semihost console TCP port. +- `STLINK_SEMIHOST_LEVEL` (default: `all`) – semihosting level (gdbserver). + +## Recommended ST‑LINK gdbserver template +- Default baseline (auto‑selected when available; expanded by the wrapper): +``` +ST-LINK_gdbserver -p {port} -l 1 -d -s --frequency {speed} {serial_flag} {apid_flag} {cubeprog_flag} -g --semihost-console-port {semi_port} --semihosting {semi_level} --initialize-reset --halt --pend-halt-timeout {pend} +``` +- Placeholders: + - `{serial_flag}` → `-i ` if `STLINK_SERIAL` set; else empty + - `{apid_flag}` → `-m ` if `STLINK_APID` set; else `-m 1` + - `{cubeprog_flag}` → `-cp ` if `ST_CUBE_PROG_PATH` or auto‑located + - `{port}`, `{speed}`, `{semi_port}`, `{semi_level}`, `{pend}` – from env/defaults + +Alternative (STM32_Programmer_CLI gdbserver): +``` +STM32_Programmer_CLI -c port={transport} freq={speed} {serial_prog} -gdbserver port={port} +``` +- `{serial_prog}` → `sn=` if `STLINK_SERIAL` set + +## Semihost output and verbosity +- The wrapper enables semihosting in the gdbserver and connects a TCP listener before GDB attaches. +- Each semihost line is prefixed with `[semi] `; only semihost lines print by default. +- Add `--verbose` (or `-v`) to print wrapper diagnostics, gdbserver output, and gdb chatter. + +## Argv injection +- Tests receive arguments via a memory block named `mlkem_cmdline_block`. +- The wrapper packs argv into a temporary `argv.bin` and restores it via GDB: + - Resolves the symbol’s numeric address using `arm-none-eabi-nm` (fallback: `readelf -s`). + - Uses `restore binary ` in the GDB batch. +- Manual generator: `test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py` + - Example: `python3 .../make_argv_bin.py /tmp/argv.bin [arg1 ...]` + +## Quick start +1) Set environment (adjust paths/serial): +``` +export ST_CUBE_CLT_ROOT=/opt/ST/STM32CubeCLT_1.20.0 +export ST_CUBE_PROG_PATH=/opt/ST/STM32CubeCLT_1.20.0/STM32CubeProgrammer/bin +export STLINK_SERIAL= +export STLINK_SPEED=50 +export GDB_PORT=61234 +``` +2) Run a single test binary directly: +``` +python3 test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py --verbose test/build/mlkem512/bin/test_mlkem512 +``` +3) Or via Makefile targets (from repo root): +``` +make run_func_512 EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1 +``` + +## Manual GDB session (advanced) +``` +# In a terminal, start gdbserver manually (example): +ST-LINK_gdbserver -p 61234 -l 1 -d -s -cp "$ST_CUBE_PROG_PATH" -m 1 --semihost-console-port 7185 --semihosting all --initialize-reset --halt --pend-halt-timeout 8000 + +# In another terminal (gdb): +(gdb) file +(gdb) target remote :61234 +(gdb) monitor reset +(gdb) load +(gdb) restore /tmp/argv.bin binary &mlkem_cmdline_block # or numeric address +(gdb) monitor reset +(gdb) continue +``` + +## Troubleshooting +- USB/Probe: If you see `DEV_USB_COMM_ERR` or timeouts, unplug/replug, try a different USB port, and update ST‑LINK firmware. +- Probe selection: set `STLINK_SERIAL=` to disambiguate. +- Speed: reduce `STLINK_SPEED` (e.g., `50`) for stability. +- Tools not found: set `ST_CUBE_CLT_ROOT` and/or `ST_CUBE_PROG_PATH` so the wrapper can find `ST-LINK_gdbserver` and `STM32_Programmer_CLI`. +- Semihost port: if the listener can’t connect in time, the wrapper proceeds; try a fixed `STLINK_SEMIHOST_PORT`. + +## Notes +- ST‑LINK gdbserver does not implement the QEMU semihost `SYS_EXIT_EXTENDED`. A sentinel‑based exit workaround is planned in the proposal. +- This platform uses FSBL‑LRUN startup/system/linker from the Cube template and a 128 KiB stack for tests. diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py new file mode 100755 index 0000000000..fc87c78c68 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -0,0 +1,516 @@ +#!/usr/bin/env python3 +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +import os +import shlex +import shutil +import struct as st +import subprocess +import sys +import tempfile +import time +import select +import socket +import threading + + +VERBOSE = False + + +def err(msg, **kwargs): + # Always print errors + print(msg, file=sys.stderr, **kwargs) + + +def info(msg, **kwargs): + if VERBOSE: + print(msg, file=sys.stderr, **kwargs) + + +def pack_cmdline(args, base_addr): + """ + Pack argv for the STM32 baremetal target: + u32 argc + u32 argv_ptrs[argc] (absolute addresses: base_addr + string offsets) + NUL-terminated strings + All fields are little-endian 32-bit. + """ + argc = len(args) + header_sz = 4 + 4 * argc + ptrs = [] + strings = b"" + cur = 0 + for s in args: + b = s.encode("utf-8") + b"\x00" + ptrs.append(base_addr + header_sz + cur) + strings += b + cur += len(b) + return st.pack(" int: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + finally: + s.close() + + +def _wait_for_port(host: str, port: int, timeout_s: float) -> bool: + deadline = time.time() + timeout_s + while time.time() < deadline: + try: + with socket.create_connection((host, port), timeout=0.3): + return True + except OSError: + time.sleep(0.1) + return False + + +def main(): + global VERBOSE + + argv = sys.argv[1:] + # Minimal flag parsing for wrapper flags (remove them from argv) + if "--verbose" in argv: + VERBOSE = True + argv.remove("--verbose") + if "-v" in argv: + VERBOSE = True + argv.remove("-v") + + if len(argv) < 1: + err("Usage: exec_wrapper.py [--verbose] [args...]") + return 2 + + elf = os.path.abspath(argv[0]) + args = argv # keep same convention as M55 wrapper: argv[0] is binpath + + if not os.path.exists(elf): + err(f"ELF not found: {elf}") + return 2 + + gdb = os.environ.get("GDB", "arm-none-eabi-gdb") + nm = os.environ.get("NM", "arm-none-eabi-nm") + readelf = os.environ.get( + "READELF", + shutil.which("arm-none-eabi-readelf") or shutil.which("readelf") or "readelf", + ) + port = int(os.environ.get("GDB_PORT", "3333")) + # STM32Cube Command Line Tools integration + # Users must install STM32CubeCLT and provide a gdbserver command. + # Preferred: set ST_GDBSERVER_CMD as a template using Python format keys: + # {port} {speed} {serial} {transport} {device} {connect} + # Example (ST-LINK_gdbserver): + # export ST_GDBSERVER_CMD='ST-LINK_gdbserver -p {port} -f SWD -s {speed}k' + # Example (STM32_Programmer_CLI): + # export ST_GDBSERVER_CMD='STM32_Programmer_CLI -c port=SWD{serial} -s {speed} -gdbserver port={port}' + st_gdbserver_cmd_tpl = os.environ.get("ST_GDBSERVER_CMD") + st_speed = os.environ.get("STLINK_SPEED", "200") # kHz (lower default for reliability) + st_serial = os.environ.get("STLINK_SERIAL", "") # optional, raw value + st_transport = os.environ.get("STLINK_TRANSPORT", "SWD") + st_device = os.environ.get("ST_DEVICE", "STM32N657X0HxQ") + st_connect = os.environ.get("STLINK_CONNECT_MODE", "under-reset") + st_cubeprog = os.environ.get("ST_CUBE_PROG_PATH", "") # Path to STM32CubeProgrammer + st_clt_root = os.environ.get("ST_CUBE_CLT_ROOT", "") # Root of STM32CubeCLT + st_pend = os.environ.get("STLINK_PEND_HALT_TIMEOUT", "8000") + st_apid = os.environ.get("STLINK_APID", "") + # Semihosting configuration (enabled by default) + st_semihost_port_env = os.environ.get("STLINK_SEMIHOST_PORT", "") + try: + st_semihost_port = int(st_semihost_port_env) if st_semihost_port_env else _pick_free_port() + except Exception: + st_semihost_port = _pick_free_port() + st_semihost_level = os.environ.get("STLINK_SEMIHOST_LEVEL", "all") + + # Address extraction for argv block symbol (numeric address avoids debugger symbol issues) + arg_block_sym = os.environ.get("ARG_BLOCK_SYMBOL", "mlkem_cmdline_block") + arg_block_addr = None + + def _resolve_symbol_addr(elf_path: str, sym: str): + # Try nm first: format ' ' + try: + cp = run([nm, "-n", elf_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if cp.returncode == 0: + for line in cp.stdout.splitlines(): + parts = line.strip().split() + if len(parts) >= 3 and parts[-1] == sym: + addr_hex = parts[0] + if not addr_hex.startswith("0x"): + addr_hex = "0x" + addr_hex + return addr_hex + except Exception: + pass + # Fallback: readelf -s + try: + cp = run([readelf, "-s", elf_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if cp.returncode == 0: + for line in cp.stdout.splitlines(): + if sym in line: + fields = line.split() + if len(fields) >= 8 and fields[-1] == sym: + val = fields[1] + if all(c in "0123456789abcdefABCDEF" for c in val): + return "0x" + val + except Exception: + pass + return None + + # Try both expected names in case of historical rename + for cand in (arg_block_sym, "mlk_cmdline_block"): + addr = _resolve_symbol_addr(elf, cand) + if addr is not None: + arg_block_sym = cand + arg_block_addr = addr + break + # Allow override of base address via env (hex string) + arg_block_addr_env = os.environ.get("ARG_BLOCK_ADDR") + base_addr = None + if arg_block_addr_env: + try: + base_addr = int(arg_block_addr_env, 16) + except Exception: + base_addr = None + if base_addr is None and arg_block_addr: + try: + base_addr = int(arg_block_addr, 16) + except Exception: + base_addr = None + + if base_addr is None: + err("Failed to resolve base address of argv block (mlkem_cmdline_block/mlk_cmdline_block).") + err("- Ensure symbols are present in ELF, or set ARG_BLOCK_ADDR to the base address (hex).") + return 2 + + blob = pack_cmdline(args, base_addr) + + with tempfile.TemporaryDirectory() as td: + argv_bin = os.path.join(td, "argv.bin") + with open(argv_bin, "wb") as f: + f.write(blob) + + # Build ST gdbserver command + # Discover ST-LINK_gdbserver + stlink_bin = shutil.which("ST-LINK_gdbserver") + # Allow deriving CLT root from CubeProgrammer path if not provided + if not st_clt_root and st_cubeprog: + # .../STM32CubeCLT_x.y.z/STM32CubeProgrammer -> CLT root is parent dir + st_clt_root = os.path.dirname(os.path.abspath(st_cubeprog)) + # If user pointed directly at the STM32CubeProgrammer dir, take its parent + base = os.path.basename(st_clt_root).lower() + if base.startswith("stm32cubeprogrammer"): + st_clt_root = os.path.dirname(st_clt_root) + candidate = None + if not stlink_bin and st_clt_root: + candidate = os.path.join(st_clt_root, "STLink-gdb-server", "bin", "ST-LINK_gdbserver") + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + stlink_bin = candidate + + # Auto-detect a default template if not provided + if not st_gdbserver_cmd_tpl and stlink_bin: + st_gdbserver_cmd_tpl = ( + f"{shlex.quote(stlink_bin)} -p {{port}} -l 1 -d -s --frequency {{speed}} {{serial_flag}} {{apid_flag}} {{cubeprog_flag}} -g --semihost-console-port {{semi_port}} --semihosting {{semi_level}} --initialize-reset --halt --pend-halt-timeout {{pend}}" + ) + + if st_gdbserver_cmd_tpl: + # Determine best '-cp' path for STM32CubeProgrammer CLI + cp_path = None + # If user provided a path + if st_cubeprog: + p = os.path.abspath(st_cubeprog) + if os.path.isdir(p): + # If directory is 'STM32CubeProgrammer', check for CLI within + cli1 = os.path.join(p, "STM32_Programmer_CLI") + cli2 = os.path.join(p, "bin", "STM32_Programmer_CLI") + if os.path.isfile(cli1) and os.access(cli1, os.X_OK): + cp_path = p + elif os.path.isfile(cli2) and os.access(cli2, os.X_OK): + cp_path = os.path.join(p, "bin") + elif os.path.isfile(p): + # User pointed directly to CLI; use its directory + cp_path = os.path.dirname(p) + # If not resolved yet, try from CLT root + if cp_path is None and st_clt_root: + cli2 = os.path.join(os.path.abspath(st_clt_root), "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") + if os.path.isfile(cli2) and os.access(cli2, os.X_OK): + cp_path = os.path.dirname(cli2) + # If still None, try relative to ST-LINK gdbserver location + if cp_path is None and 'stlink_bin' in locals() and stlink_bin: + # stlink_bin .../STLink-gdb-server/bin/ST-LINK_gdbserver -> root is two parents up + root = os.path.dirname(os.path.dirname(os.path.abspath(stlink_bin))) + cli2 = os.path.join(root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") + if os.path.isfile(cli2) and os.access(cli2, os.X_OK): + cp_path = os.path.dirname(cli2) + + # Provide a flexible set of placeholders for various CLT tools. + # - {serial} -> raw serial value (e.g. 303030303030) + # - {serial_flag} -> '-i ' (ST-LINK_gdbserver) + # - {serial_prog} -> 'sn=' (STM32_Programmer_CLI) + # - {serial_sn} -> ',sn=' (STM32_Programmer_CLI combined) + # - {speed} -> kHz value (e.g. 500) + # - {port} -> GDB server port (e.g. 3333) + # - {transport} -> SWD/JTAG (usually SWD) + # - {device} -> device name (e.g. STM32N657X0HxQ) + # - {connect} -> connection mode hint (e.g. under-reset) + # - {cubeprog_flag}-> '-cp ' if resolved + fmt = { + "port": port, + "speed": st_speed, + "serial": st_serial, + "serial_flag": (f"-i {st_serial}" if st_serial else ""), + "serial_prog": (f"sn={st_serial}" if st_serial else ""), + "serial_sn": (f",sn={st_serial}" if st_serial else ""), + "transport": st_transport, + "device": st_device, + "connect": st_connect, + "cubeprog": cp_path or st_cubeprog, + "cubeprog_flag": (f"-cp {shlex.quote(cp_path)}" if cp_path else (f"-cp {shlex.quote(st_cubeprog)}" if st_cubeprog else "")), + "pend": st_pend, + "apid_flag": (f"-m {st_apid}" if st_apid else "-m 1"), + "semi_port": st_semihost_port, + "semi_level": st_semihost_level, + } + try: + formatted = st_gdbserver_cmd_tpl.format(**fmt) + except KeyError as e: + err(f"Missing format key in ST_GDBSERVER_CMD: {e}") + return 2 + gdbserver_cmd = shlex.split(formatted) + else: + msg = ( + "STM32Cube Command Line Tools required.\n" + "- Install STM32CubeCLT (Linux/macOS).\n" + " Download: https://www.st.com/en/development-tools/stm32cubeclt.html\n" + "- Set ST_GDBSERVER_CMD to a working gdbserver command template, or ensure ST-LINK_gdbserver is on PATH.\n" + " Examples:\n" + " ST-LINK_gdbserver: 'ST-LINK_gdbserver -p {port} -d --frequency {speed} {serial_flag} --initialize-reset {cubeprog_flag}'\n" + " STM32_Programmer_CLI: 'STM32_Programmer_CLI -c port={transport},{serial_prog} -s {speed} -gdbserver port={port}'\n" + " Tip: If ST-LINK_gdbserver errors about STM32CubeProgrammer, set ST_CUBE_PROG_PATH to its installation path,\n" + " or export ST_CUBE_CLT_ROOT to the CubeCLT root so the wrapper can auto-locate ST-LINK_gdbserver.\n" + ) + # Append small diagnostics if we attempted a candidate path + if candidate: + msg += f" Searched for ST-LINK_gdbserver at: {candidate}\n" + if stlink_bin is None and shutil.which("ST-LINK_gdbserver") is None: + msg += " Note: ST-LINK_gdbserver not found on PATH.\n" + err(msg) + return 2 + + info(f"[exec_wrapper] starting ST gdbserver on port {port}...") + info(f"[exec_wrapper] {' '.join(gdbserver_cmd)}") + stp = popen( + gdbserver_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + ) + + try: + # Wait for semihost console to become available and connect before attaching GDB + # First, ensure the process is alive + time.sleep(0.2) + # Then, wait for the semihost port to accept connections (up to 10s) + if not _wait_for_port("127.0.0.1", st_semihost_port, timeout_s=10.0): + info("[exec_wrapper] semihost port not ready within timeout; continuing anyway") + + semihost_sock = None + semihost_stop = threading.Event() + semihost_thr = None + semihost_exit = threading.Event() + shared = {"exit_code": None} + + def _semihost_reader(sock: socket.socket): + buf = b"" + try: + while not semihost_stop.is_set(): + try: + data = sock.recv(4096) + if not data: + break + buf += data + while b"\n" in buf: + line, buf = buf.split(b"\n", 1) + try: + text = line.decode("utf-8", errors="replace") + except Exception: + text = line.decode(errors="replace") + # Detect exit sentinel first + t = text.strip() + is_exit = t.startswith("[[MLKEM-EXIT:") and t.endswith("]]") + if is_exit: + try: + code_str = t[len("[[MLKEM-EXIT:"):-2] + shared["exit_code"] = int(code_str) + except Exception: + shared["exit_code"] = 1 + semihost_exit.set() + # Do not print the sentinel unless verbose + if VERBOSE: + print(f"[semi] {text}") + else: + # Print semihost line; prefix only in verbose mode + if VERBOSE: + print(f"[semi] {text}") + else: + print(text) + except socket.timeout: + continue + finally: + try: + sock.close() + except Exception: + pass + + # Attempt to connect the listener (non-blocking retries) + try: + semihost_sock = socket.create_connection(("127.0.0.1", st_semihost_port), timeout=1.0) + semihost_sock.settimeout(0.5) + semihost_thr = threading.Thread(target=_semihost_reader, args=(semihost_sock,), daemon=True) + semihost_thr.start() + info(f"[exec_wrapper] semihost listener connected on port {st_semihost_port}") + except OSError: + info(f"[exec_wrapper] semihost listener not connected (port {st_semihost_port}); proceeding") + + # Give the server a brief moment, then check for early exit + time.sleep(0.8) + if stp.poll() is not None: + # Server exited early – surface a helpful message + out_rem = stp.stdout.read() if stp.stdout else "" + if out_rem and VERBOSE: + print(out_rem, end="") + merged = out_rem + low = merged.lower() + if "firmware upgrade" in low or "upgrade required" in low: + # Try to suggest STLinkUpgrade locations + hints = [] + if st_clt_root: + app1 = os.path.join(st_clt_root, "STM32CubeProgrammer", "stlink", "STLinkUpgrade") + app2 = os.path.join(st_clt_root, "STM32CubeProgrammer", "stlink", "STLinkUpgrade.app") + if os.path.exists(app1): + hints.append(app1) + if os.path.exists(app2): + hints.append(app2) + if VERBOSE: + err("[exec_wrapper] ST-LINK firmware upgrade required. Please run the STLinkUpgrade tool.") + if hints: + for h in hints: + err(f"[exec_wrapper] STLinkUpgrade candidate: {h}") + return 2 + + # Write GDB commands to a temp script and run with -x + gdb_lines = [ + "set pagination off", + "set confirm off", + f"target remote localhost:{port}", + "monitor reset", + # semihosting enable is handled by gdbserver; keep gdb quiet + "load", + "tbreak __wrap_main", + "continue", + (f"restore {argv_bin} binary {arg_block_addr}" if arg_block_addr else f"restore {argv_bin} binary &{arg_block_sym}"), + "continue", + ] + + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".gdb") as gs: + for line in gdb_lines: + gs.write(line + "\n") + gdb_script_path = gs.name + + gdb_cmd = [gdb, "--batch", "-x", gdb_script_path, elf] + + # Run GDB while streaming gdbserver output (which will include semihost output). + info("[exec_wrapper] running gdb batch (program will continue; semihost output follows)...") + gdbp = popen(gdb_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # Stream gdbserver output until gdb finishes without blocking on readline() + while True: + # Early shutdown if exit sentinel observed + if semihost_exit.is_set(): + info("[exec_wrapper] exit sentinel detected; shutting down gdb and gdbserver...") + try: + if gdbp.poll() is None: + gdbp.terminate() + try: + gdbp.wait(timeout=1.0) + except Exception: + gdbp.kill() + except Exception: + pass + break + if stp.stdout is not None: + try: + r, _, _ = select.select([stp.stdout], [], [], 0.1) + if r: + line = stp.stdout.readline() + if line: + # gdbserver stdout (printed only in verbose mode) + if VERBOSE: + print(line, end="") + except Exception: + # If select/readline fails, avoid blocking the loop + pass + # Check if gdb has completed + if gdbp.poll() is not None: + break + + out, errout = gdbp.communicate() + if out and VERBOSE: + print(out, end="") + if errout and VERBOSE: + # gdb chatter / errors (verbose only) + err(errout, end="") + + if shared.get("exit_code") is not None: + return int(shared["exit_code"]) if isinstance(shared["exit_code"], int) else 1 + + if gdbp.returncode != 0: + err("FAIL!") + err(f"gdb batch failed with code {gdbp.returncode}") + return gdbp.returncode + + return 0 + + finally: + # Terminate ST gdbserver + try: + stp.terminate() + stp.wait(timeout=1.5) + except Exception: + try: + stp.kill() + except Exception: + pass + # Stop semihost listener + try: + if 'semihost_stop' in locals(): + semihost_stop.set() + if 'semihost_sock' in locals() and semihost_sock: + semihost_sock.close() + if 'semihost_thr' in locals() and semihost_thr: + semihost_thr.join(timeout=0.5) + except Exception: + pass + # Remove the temp gdb script + try: + if 'gdb_script_path' in locals(): + os.unlink(gdb_script_path) + except Exception: + pass + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py new file mode 100644 index 0000000000..eafe0d382e --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +import os +import struct as st +import sys + + +def pack_cmdline(args, base_addr): + """ + Pack argv for the STM32 baremetal target: + u32 argc + u32 argv_ptrs[argc] (absolute addresses: base_addr + string offsets) + NUL-terminated strings + All fields are little-endian 32-bit. + """ + argc = len(args) + header_sz = 4 + 4 * argc + ptrs = [] + strings = b"" + cur = 0 + for s in args: + b = s.encode("utf-8") + b"\x00" + ptrs.append(base_addr + header_sz + cur) + strings += b + cur += len(b) + return st.pack(" [arg1 ...]", file=sys.stderr) + return 2 + out = argv[1] + base_hex = argv[2] + try: + base_addr = int(base_hex, 16) + except ValueError: + print(f"Invalid base address hex: {base_hex}", file=sys.stderr) + return 2 + args = argv[3:] + blob = pack_cmdline(args, base_addr) + with open(out, "wb") as f: + f.write(blob) + print(f"Wrote {len(blob)} bytes to {os.path.abspath(out)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/test/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk new file mode 100644 index 0000000000..d4b644255b --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -0,0 +1,104 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +PLATFORM_PATH:=test/baremetal/platform/nucleo-n657x0-q + +CROSS_PREFIX=arm-none-eabi- +CC=gcc + +CFLAGS += \ + -O3 -g \ + -Wall -Wextra -Wshadow \ + -Wno-pedantic \ + -Wno-redundant-decls \ + -Wno-missing-prototypes \ + -fno-common \ + -ffunction-sections \ + -fdata-sections \ + --sysroot=$(SYSROOT) \ + -DDEVICE=nucleo-n657x0-q \ + -DSTM32N657xx \ + -I$(NUCLEO_N657X0_Q_PATH) \ + -I$(NUCLEO_N657X0_Q_PATH)/Inc \ + -I$(NUCLEO_N657X0_Q_PATH)/Drivers/STM32N6xx_HAL_Driver/Inc \ + -I$(NUCLEO_N657X0_Q_PATH)/Drivers/CMSIS/Core/Include \ + -I$(NUCLEO_N657X0_Q_PATH)/Drivers/CMSIS/Core/Include/m-profile \ + -I$(NUCLEO_N657X0_Q_PATH)/Drivers/CMSIS/Device/ST \ + -I$(NUCLEO_N657X0_Q_PATH)/Drivers/CMSIS/Device/ST/STM32N6xx/Include/ \ + -DSEMIHOSTING \ + + +ARCH_FLAGS += \ + -mcmse \ + -march=armv8.1-m.main+mve.fp \ + -mcpu=cortex-m55 \ + -mthumb \ + -mfloat-abi=hard -mfpu=fpv5-sp-d16 + +# TODO(GAP): If the Cube template (or GCC/newlib build) expects softfp, use: +# -mfloat-abi=softfp (and keep -mfpu) + +CFLAGS += \ + $(ARCH_FLAGS) \ + --specs=rdimon.specs + +CFLAGS += $(CFLAGS_EXTRA) + +# Try to auto-detect a GCC linker script from the FSBL or CMSIS template; fall back to linker.ld if present +# Prefer linker scripts under gcc/linker/, fall back to other locations. +# Try to pick an N657-specific script first. +# Pin LRUN linker script explicitly for RAM-run +LDSCRIPT := $(NUCLEO_N657X0_Q_PATH)/gcc/linker/STM32N657XX_LRUN.ld + +# Auto-detect startup assembly case and optional board glue +# Auto-detect startup assembly for STM32N6 family (prefer n657 if present) +STARTUP := $(firstword \ + $(wildcard $(NUCLEO_N657X0_Q_PATH)/gcc/startup_stm32n657xx.[sS]) \ + $(wildcard $(NUCLEO_N657X0_Q_PATH)/gcc/startup_stm32n6*.[sS]) \ + $(wildcard $(NUCLEO_N657X0_Q_PATH)/gcc/startup_stm32n*.[sS]) \ +) +# MSP := $(firstword $(wildcard $(NUCLEO_N657X0_Q_PATH)/stm32n6xx_hal_msp.c)) +IT := $(firstword $(wildcard $(NUCLEO_N657X0_Q_PATH)/stm32n6xx_it.c)) +HAL_SRCS := +HAL_CORE := $(firstword $(wildcard $(NUCLEO_N657X0_Q_PATH)/Drivers/STM32N6xx_HAL_Driver/Src/stm32n6xx_hal.c)) + +LDFLAGS += \ + -Wl,--gc-sections \ + -Wl,--no-warn-rwx-segments \ + -L. + +LDFLAGS += \ + --specs=rdimon.specs \ + -Wl,--wrap=main \ + -ffreestanding \ + -T$(LDSCRIPT) \ + $(ARCH_FLAGS) + +# Extra sources to be included in test binaries +EXTRA_SOURCES = \ + $(PLATFORM_PATH)/src/cmdline.c \ + $(PLATFORM_PATH)/src/cmdline_region.c \ + $(PLATFORM_PATH)/src/semihosting_syscall.c \ + $(NUCLEO_N657X0_Q_PATH)/clock_config.c \ + $(NUCLEO_N657X0_Q_PATH)/system_stm32n6xx.c \ + $(STARTUP) \ + $(IT) \ + $(HAL_CORE) \ + $(NUCLEO_N657X0_Q_PATH)/integration_argv.c \ + $(NUCLEO_N657X0_Q_PATH)/Drivers/STM32N6xx_HAL_Driver/Src/stm32n6xx_hal_rcc.c \ + $(NUCLEO_N657X0_Q_PATH)/Drivers/STM32N6xx_HAL_Driver/Src/stm32n6xx_hal_cortex.c \ + $(NUCLEO_N657X0_Q_PATH)/Drivers/STM32N6xx_HAL_Driver/Src/stm32n6xx_hal_rcc_ex.c \ + $(NUCLEO_N657X0_Q_PATH)/Drivers/STM32N6xx_HAL_Driver/Src/stm32n6xx_hal_pwr.c \ + $(NUCLEO_N657X0_Q_PATH)/Drivers/STM32N6xx_HAL_Driver/Src/stm32n6xx_hal_pwr_ex.c + + # $(MSP) \ + +# The Cube/CMSIS and HAL files often fail compilation with strict warnings; relax for these files +EXTRA_SOURCES_CFLAGS = -Wno-error -Wno-conversion -Wno-sign-conversion -Wno-unused-parameter -Wno-missing-prototypes -Wno-maybe-uninitialized -Wno-unused-function + +# Avoid duplicate __wrap_main by excluding the generic integration_argv.c (not generated anymore) +EXTRA_SOURCES := $(filter-out %/integration_argv.c,$(EXTRA_SOURCES)) + +EXEC_WRAPPER := $(realpath $(PLATFORM_PATH)/exec_wrapper.py) diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c new file mode 100644 index 0000000000..11107add05 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c @@ -0,0 +1,63 @@ +/* + * Copyright (c) The mldsa-native project authors + * Copyright (c) The mlkem-native project authors + * Copyright (c) Arm Ltd. + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ +#include +#include +#include +#include "stm32n6xx.h" +#include "stm32n6xx_hal.h" +#include "stm32n6xx_it.h" +#include "main.h" + +typedef struct cmdline_s { + int argc; + char *argv[]; +} cmdline_t; + +/* Allow overriding CMDLINE_ADDR; default matches AN547 usage. */ +#ifndef CMDLINE_ADDR +#define CMDLINE_ADDR ((cmdline_t *)0x70000) +#endif + +/* Provided by cmdline_region.c */ +extern unsigned char mlk_cmdline_block[]; + +/* Provide a prototype for the real main that the C library expects. */ +extern int __real_main(int argc, char *argv[]); +int __wrap_main(int unused_argc, char *unused_argv[]); + +#ifdef SEMIHOSTING +#include "semihosting_syscall.h" +static void semihosting_exit_with_rc(int rc) { + // Print sentinel for the exec_wrapper to detect and propagate exit code + printf("[[MLKEM-EXIT:%d]]\n",rc); + fflush(stdout); + // Try basic semihost exit (ST-LINK may or may not support it). If unsupported, + // gdbserver may report an error; wrapper already captured the code. + while(1) ; +} +#else +static void semihosting_exit_with_rc(int rc) { (void)rc; } +#endif + +void Error_Handler(void) { + HardFault_Handler(); +} + +/* Wrap main: build argc/argv from cmdline and forward to __real_main. */ +int __wrap_main(int unused_argc, char *unused_argv[]) { + (void)unused_argc; (void)unused_argv; + SCB_EnableICache(); + SCB_EnableDCache(); + HAL_Init(); + SystemClock_Config(); + + cmdline_t *cmdline = (cmdline_t *)&mlk_cmdline_block; + int rc = __real_main(cmdline->argc, cmdline->argv); + semihosting_exit_with_rc(rc); + return rc; +} + diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c new file mode 100644 index 0000000000..8c53062257 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) The mldsa-native project authors + * Copyright (c) The mlkem-native project authors + * Copyright (c) Arm Ltd. + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ +#include +/* 64 KiB command-line buffer in BSS, 8-byte aligned */ +__attribute__((aligned(8), used)) +unsigned char mlk_cmdline_block[64 * 1024]; diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/libfns.c b/test/baremetal/platform/nucleo-n657x0-q/src/libfns.c new file mode 100644 index 0000000000..fc1b5bb5f6 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/libfns.c @@ -0,0 +1,67 @@ +/* + * Copyright (c) The mldsa-native project authors + * Copyright (c) The mlkem-native project authors + * Copyright (c) Arm Ltd. + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ +#include +#include +#include +/* Public semihosting API */ +#include "semihosting_syscall.h" +/* Semihosting definitions */ +static const uint32_t REPORT_EXCEPTION = 0x18; +static const uint32_t ApplicationExit = 0x20026; + +int __wrap__close(int fd); +int __wrap__fstat(int fd, struct stat *buf); +int __wrap__getpid(void); +int __wrap__isatty(int fd); +int __wrap__lseek(int fd, int offset, int whence); +int __wrap__kill(int pid, int sig); + + +int __wrap__close(int fd) +{ + (void)fd; + semihosting_syscall(REPORT_EXCEPTION, ApplicationExit); + return 0; +} + +int __wrap__fstat(int fd, struct stat *buf) +{ + (void)fd; + (void)buf; + errno = ENOSYS; + return -1; +} + +int __wrap__getpid(void) +{ + errno = ENOSYS; + return -1; +} + +int __wrap__isatty(int fd) +{ + (void)fd; + errno = ENOSYS; + return -1; +} + +int __wrap__lseek(int fd, int offset, int whence) +{ + (void)fd; + (void)offset; + (void)whence; + errno = ENOSYS; + return -1; +} + +int __wrap__kill(int pid, int sig) +{ + (void)pid; + (void)sig; + errno = ENOSYS; + return -1; +} diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c new file mode 100644 index 0000000000..2829029769 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c @@ -0,0 +1,27 @@ +/* + * Copyright (c) The mldsa-native project authors + * Copyright (c) The mlkem-native project authors + * Copyright (c) Arm Ltd. + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ +#include +#include +/* Public semihosting API */ +#include "semihosting_syscall.h" +__attribute__((always_inline)) static inline void __semihosting_call(int32_t opnr, int32_t param) { + register int32_t r0 __asm__("r0") = opnr; + register int32_t r1 __asm__("r1") = param; + __asm__ __volatile__("bkpt 0xAB" : "+r"(r0) : "r"(r1) : "memory"); +} +void semihosting_syscall(int32_t opnr, int32_t param) { + __semihosting_call(opnr, param); +} + +// Provided by --specs=rdimon.specs +extern void initialise_monitor_handles(void); + +__attribute__((constructor)) +static void mlkem_semihost_init(void) { + initialise_monitor_handles(); + fflush(stdout); +} diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h new file mode 100644 index 0000000000..0ecca1e700 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h @@ -0,0 +1,15 @@ +/* + * Copyright (c) The mldsa-native project authors + * Copyright (c) The mlkem-native project authors + * Copyright (c) Arm Ltd. + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ +#ifndef MLKEM_NATIVE_SEMIHOSTING_SYSCALL_H +#define MLKEM_NATIVE_SEMIHOSTING_SYSCALL_H + +#include + +void semihosting_syscall(int32_t opnr, int32_t param); + +#endif /* MLKEM_NATIVE_SEMIHOSTING_SYSCALL_H */ + diff --git a/test/hal/hal.c b/test/hal/hal.c index 00194a5cea..54109e9a4d 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -95,8 +95,12 @@ uint64_t get_cyclecounter(void) } #elif defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_8_1M_MAIN__) -#include -#include +#if defined(STM32N657xx) + #include "stm32n6xx.h" +#else + #include + #include +#endif #include "pmu_armv8.h" void enable_cyclecounter(void) diff --git a/test/hal/pmu_armv8.h b/test/hal/pmu_armv8.h new file mode 100644 index 0000000000..66c312255a --- /dev/null +++ b/test/hal/pmu_armv8.h @@ -0,0 +1,7 @@ +/* Compatibility wrapper: CMSIS uses armv8m_pmu.h under m-profile */ +#if defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_8_1M_MAIN__) +#include +#else +#error pmu_armv8.h included on non Armv8-M build +#endif + diff --git a/test/mk/components.mk b/test/mk/components.mk index 131f00cda3..4e7e374def 100644 --- a/test/mk/components.mk +++ b/test/mk/components.mk @@ -96,6 +96,11 @@ $(MLKEM512_DIR)/bin/bench_components_mlkem512: $(MLKEM512_DIR)/test/hal/hal.c.o $(MLKEM768_DIR)/bin/bench_components_mlkem768: $(MLKEM768_DIR)/test/hal/hal.c.o $(MLKEM1024_DIR)/bin/bench_components_mlkem1024: $(MLKEM1024_DIR)/test/hal/hal.c.o +# Build hal.c without pedantic diagnostics (overrides global -pedantic/-Wpedantic) +$(MLKEM512_DIR)/test/hal/hal.c.o: CFLAGS := $(filter-out -pedantic -Wpedantic -Werror -Wconversion -Wsign-conversion,$(CFLAGS)) -Wno-pedantic -Wno-conversion -Wno-sign-conversion -Wno-error +$(MLKEM768_DIR)/test/hal/hal.c.o: CFLAGS := $(filter-out -pedantic -Wpedantic -Werror -Wconversion -Wsign-conversion,$(CFLAGS)) -Wno-pedantic -Wno-conversion -Wno-sign-conversion -Wno-error +$(MLKEM1024_DIR)/test/hal/hal.c.o: CFLAGS := $(filter-out -pedantic -Wpedantic -Werror -Wconversion -Wsign-conversion,$(CFLAGS)) -Wno-pedantic -Wno-conversion -Wno-sign-conversion -Wno-error + $(MLKEM512_DIR)/bin/%: CFLAGS += -DMLK_CONFIG_PARAMETER_SET=512 $(MLKEM768_DIR)/bin/%: CFLAGS += -DMLK_CONFIG_PARAMETER_SET=768 $(MLKEM1024_DIR)/bin/%: CFLAGS += -DMLK_CONFIG_PARAMETER_SET=1024 From 32c4136b815ca13f50f1f852993b1cd507195883 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Wed, 4 Feb 2026 15:14:09 +0000 Subject: [PATCH 02/20] Update readme for correctness Signed-off-by: Brendan Moran --- .../platform/nucleo-n657x0-q/README.md | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md index 2dbf8b3a07..47956f1f43 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/README.md +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -1,5 +1,6 @@ @@ -18,6 +19,15 @@ This platform runs ML-KEM tests on the ST NUCLEO‑N657X0‑Q board using STM32C - macOS app: `/STM32CubeProgrammer/stlink/STLinkUpgrade.app` - CLI: `/STM32CubeProgrammer/stlink/STLinkUpgrade` +## DevShell (required) +Run gdb, make and exec_wrapper.py commands in this README from within the project’s Nix devshell for this board: + +``` +nix develop .#nucleo-n657x0-q +``` + +Then, in that shell, run the Make targets and Python scripts below. + ## Environment Variables (exec_wrapper.py) - `GDB` (default: `arm-none-eabi-gdb`) – gdb binary. - `GDB_PORT` (default: `3333`) – gdbserver port. @@ -57,7 +67,17 @@ STM32_Programmer_CLI -c port={transport} freq={speed} {serial_prog} -gdbserver p - Add `--verbose` (or `-v`) to print wrapper diagnostics, gdbserver output, and gdb chatter. ## Argv injection -- Tests receive arguments via a memory block named `mlkem_cmdline_block`. +- Tests receive arguments via a reserved BSS block symbol `mlk_cmdline_block`. +- The wrapper resolves the numeric base address via `arm-none-eabi-nm` (fallback: `readelf -s`); + override with `ARG_BLOCK_ADDR` (hex) or choose a different symbol with `ARG_BLOCK_SYMBOL`. + +### Argv blob layout (authoritative) +- 4 bytes: `u32 argc` (little-endian). +- `argc` × 4 bytes: `u32 argv_ptrs[i]` (absolute addresses): `base + string_offset[i]`. +- NUL-terminated UTF-8 strings placed sequentially after the pointer table. +- Alignment: strings start at offset `4 + 4*argc` (4-byte aligned). +- Helper: `test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py` can generate `argv.bin` manually. + - The wrapper packs argv into a temporary `argv.bin` and restores it via GDB: - Resolves the symbol’s numeric address using `arm-none-eabi-nm` (fallback: `readelf -s`). - Uses `restore binary ` in the GDB batch. @@ -92,8 +112,9 @@ ST-LINK_gdbserver -p 61234 -l 1 -d -s -cp "$ST_CUBE_PROG_PATH" -m 1 --semihost (gdb) target remote :61234 (gdb) monitor reset (gdb) load -(gdb) restore /tmp/argv.bin binary &mlkem_cmdline_block # or numeric address -(gdb) monitor reset +(gdb) tbreak __wrap_main +(gdb) continue +(gdb) restore /tmp/argv.bin binary &mlk_cmdline_block # or numeric address (gdb) continue ``` @@ -107,3 +128,19 @@ ST-LINK_gdbserver -p 61234 -l 1 -d -s -cp "$ST_CUBE_PROG_PATH" -m 1 --semihost ## Notes - ST‑LINK gdbserver does not implement the QEMU semihost `SYS_EXIT_EXTENDED`. A sentinel‑based exit workaround is planned in the proposal. - This platform uses FSBL‑LRUN startup/system/linker from the Cube template and a 128 KiB stack for tests. + +- Clock configuration: `SystemClock_Config()` is generated from the STM32CubeN6 FSBL template (`Projects/NUCLEO-N657X0-Q/Templates/Template_FSBL_LRUN/FSBL/Src/main.c`) into `${NUCLEO_N657X0_Q_PATH}/clock_config.c` by `nix/nucleo-n657x0-q/default.nix`. The devshell build also ensures `${NUCLEO_N657X0_Q_PATH}/Inc/main.h` declares `void SystemClock_Config(void);` and `void Error_Handler(void);`. Do not edit the generated file directly; update the Cube template or the extraction logic if adjustments are needed. + +## HW‑testing +- Run from inside the Nix devshell: `nix develop .#nucleo-n657x0-q`. +- Hardware runs are opt‑in. Include this platform’s Makefile to target the NUCLEO‑N657X0‑Q board: + +``` +make test EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1 +``` + +- Examples: + - Single test: `make run_func_512 EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1` + - Direct wrapper: `python3 test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py test/build/mlkem512/bin/test_mlkem512` + +- Without `EXTRA_MAKEFILE`, tests run on the host (no hardware). From c5d7cd0d415e6e8d66d45acae26e3ae5308ec72e Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Fri, 6 Feb 2026 10:35:25 +0000 Subject: [PATCH 03/20] Start of work on TCM Stack Signed-off-by: Brendan Moran --- nix/nucleo-n657x0-q/default.nix | 24 +----- .../platform/nucleo-n657x0-q/README.md | 3 +- .../platform/nucleo-n657x0-q/platform.mk | 8 ++ .../nucleo-n657x0-q/src/reset_dtcm_init.S | 80 +++++++++++++++++++ 4 files changed, 91 insertions(+), 24 deletions(-) create mode 100644 test/baremetal/platform/nucleo-n657x0-q/src/reset_dtcm_init.S diff --git a/nix/nucleo-n657x0-q/default.nix b/nix/nucleo-n657x0-q/default.nix index 08ad652981..c2f269ebfd 100644 --- a/nix/nucleo-n657x0-q/default.nix +++ b/nix/nucleo-n657x0-q/default.nix @@ -228,29 +228,7 @@ stdenvNoCC.mkDerivation { echo "WARNING: FSBL main.c not found at $fsbl_main_c; skipping clock_config generation" >&2 fi - # Patch linker script to increase stack to 128 KiB (0x20000) for test workloads - # Locate STM32N657XX_LRUN.ld under gcc/ or gcc/linker/ - ldpath="" - for cand in \ - "$outp/gcc/linker/STM32N657XX_LRUN.ld" \ - "$outp/gcc/STM32N657XX_LRUN.ld" \ - $(find "$outp" -type f -name STM32N657XX_LRUN.ld 2>/dev/null | head -n1) - do - if [ -f "$cand" ]; then ldpath="$cand"; break; fi - done - if [ -n "$ldpath" ]; then - echo "Patching stack size in $ldpath to 0x20000" - # Common ST patterns - sed -i.bak -E 's/(\b_Min_Stack_Size\s*=\s*)0x[0-9a-fA-F]+;/\10x20000;/' "$ldpath" || true - sed -i.bak -E 's/(\b__STACK_SIZE\s*=\s*)0x[0-9a-fA-F]+;/\10x20000;/' "$ldpath" || true - sed -i.bak -E 's/(\b__stack_size__\s*=\s*)0x[0-9a-fA-F]+;/\10x20000;/' "$ldpath" || true - # If none of the vars exist, try to adjust the stack reservation directly - if ! grep -Eq '\b_Min_Stack_Size\b|\b__STACK_SIZE\b|\b__stack_size__\b' "$ldpath"; then - sed -i.bak -E 's/(\. = \. \+ )_Min_Stack_Size;/\10x20000;/' "$ldpath" || true - fi - else - echo "WARNING: Could not find STM32N657XX_LRUN.ld to patch stack size" >&2 - fi + # No linker script stack-size patch: MSP/MSPLIM relocation handled in Reset_Handler (reset_dtcm_init.S) ''; setupHook = writeText "setup-hook.sh" '' diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md index 47956f1f43..75fe8718e9 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/README.md +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -127,7 +127,8 @@ ST-LINK_gdbserver -p 61234 -l 1 -d -s -cp "$ST_CUBE_PROG_PATH" -m 1 --semihost ## Notes - ST‑LINK gdbserver does not implement the QEMU semihost `SYS_EXIT_EXTENDED`. A sentinel‑based exit workaround is planned in the proposal. -- This platform uses FSBL‑LRUN startup/system/linker from the Cube template and a 128 KiB stack for tests. +- This platform uses FSBL‑LRUN startup/system/linker from the Cube template. +- D‑TCM stack: a startup bootstrap (`src/reset_dtcm_init.S`) checks `SYSCFG_CM55TCMCR` and, if needed, writes `0x99` then resets via AIRCR to apply D‑TCM sizing. Otherwise it sets `MSP=0x30040000` and `MSPLIM=0x30000000` so the runtime stack resides entirely in D‑TCM (256 KiB). A one‑time reset occurs the first time sizing is applied. Compatible with GDB resets. - Clock configuration: `SystemClock_Config()` is generated from the STM32CubeN6 FSBL template (`Projects/NUCLEO-N657X0-Q/Templates/Template_FSBL_LRUN/FSBL/Src/main.c`) into `${NUCLEO_N657X0_Q_PATH}/clock_config.c` by `nix/nucleo-n657x0-q/default.nix`. The devshell build also ensures `${NUCLEO_N657X0_Q_PATH}/Inc/main.h` declares `void SystemClock_Config(void);` and `void Error_Handler(void);`. Do not edit the generated file directly; update the Cube template or the extraction logic if adjustments are needed. diff --git a/test/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk index d4b644255b..9b6dbeeae3 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/platform.mk +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -101,4 +101,12 @@ EXTRA_SOURCES_CFLAGS = -Wno-error -Wno-conversion -Wno-sign-conversion -Wno-unus # Avoid duplicate __wrap_main by excluding the generic integration_argv.c (not generated anymore) EXTRA_SOURCES := $(filter-out %/integration_argv.c,$(EXTRA_SOURCES)) +# D-TCM stack enable (wrap Reset_Handler to bootstrap TCM sizing and set MSP/MSPLIM) +NUCLEO_N657X0_Q_DTCM_STACK ?= 1 +ifeq ($(NUCLEO_N657X0_Q_DTCM_STACK),1) + EXTRA_SOURCES += $(PLATFORM_PATH)/src/reset_dtcm_init.S + # Only wrap SystemInit; let vendor Reset_Handler set initial SP, then relocate in wrapper + LDFLAGS += -Wl,--wrap=SystemInit +endif + EXEC_WRAPPER := $(realpath $(PLATFORM_PATH)/exec_wrapper.py) diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/reset_dtcm_init.S b/test/baremetal/platform/nucleo-n657x0-q/src/reset_dtcm_init.S new file mode 100644 index 0000000000..6356d4bf35 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/reset_dtcm_init.S @@ -0,0 +1,80 @@ +/* + * DTCM sizing + MSP/MSPLIM bootstrap for NUCLEO-N657X0-Q (Cortex-M55) + * - Checks SYSCFG_CM55TCMCR lower 8 bits; if not 0x99, writes 0x99 and resets via AIRCR + * - Else sets MSP to 0x30040000 and MSPLIM to 0x30000000 before any C stack usage + * - Injected into Reset_Handler via --wrap=Reset_Handler + */ + + .syntax unified + .thumb + + .global __wrap_SystemInit + .type __wrap_SystemInit, %function + .extern __real_SystemInit + +/* Constants */ +#define SYSCFG_BASE 0x46008000 +#define SYSCFG_CM55TCMCR 0x00000008 /* offset from SYSCFG_BASE */ +/* RCC APB4ENSR2 address and bit for SYSCFGENS (enable) from device header: + * RCC_BASE_S = PERIPH_BASE_S(0x50000000) + AHB4PERIPH(0x06020000) + 0x8000 = 0x56028000 + * APB4ENSR2 offset = 0x0A78 => 0x56028A78 + * SYSCFGENS bit = 1<<0 + */ +#define RCC_APB4ENSR2_ADDR 0x46028A78 + +#define SCB_AIRCR_ADDR 0xE000ED0C +#define AIRCR_SYSRESET 0x05FA0004 /* VECTKEY(0x05FA)<<16 | SYSRESETREQ */ + +#define DTCM_BASE 0x30000000 +#define DTCM_SIZE_256K 0x00040000 + +/* + * Wrap SystemInit to apply/check TCM sizing and set MSP/MSPLIM into DTCM + * before any C stack use within SystemInit or later code. + */ +__wrap_SystemInit: + /* Ensure SYSCFG clock is enabled before touching TCMCR */ + ldr r0, =RCC_APB4ENSR2_ADDR + ldr r1, [r0] + orr r1, r1, #1 + str r1, [r0] + + /* Read SYSCFG_CM55TCMCR */ + ldr r0, =SYSCFG_BASE + ldr r1, =SYSCFG_CM55TCMCR + add r0, r0, r1 + ldr r1, [r0] + /* Mask lower 8 bits and compare to 0x99 */ + movs r0, #0xFF + ands r1, r1, r0 + cmp r1, #0x99 + beq 2f + + /* Not 0x99: write 0x99 into lower byte of SYSCFG_CM55TCMCR */ + /* Reload address */ + ldr r0, =SYSCFG_BASE + ldr r1, =SYSCFG_CM55TCMCR + add r0, r0, r1 + /* Read-modify-write to update only the low byte */ + ldr r1, [r0] + bic r1, r1, #0xFF + orr r1, r1, #0x99 + str r1, [r0] + + /* Trigger system reset via AIRCR */ + ldr r0, =SCB_AIRCR_ADDR + ldr r1, =AIRCR_SYSRESET + str r1, [r0] +1: b 1b + +2: + /* Correct sizing: set MSP = DTCM_BASE + 256K, MSPLIM = DTCM_BASE */ + ldr r0, =DTCM_BASE + ldr r1, =DTCM_SIZE_256K + add r0, r0, r1 + msr MSP, r0 + ldr r0, =DTCM_BASE + msr MSPLIM, r0 + + /* Tail-call real SystemInit */ + b __real_SystemInit From 2d1fee41287ed87163a251392ec442bf0277f008 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Wed, 11 Feb 2026 09:19:06 +0000 Subject: [PATCH 04/20] Try to get TCM working Signed-off-by: Brendan Moran --- nix/nucleo-n657x0-q/default.nix | 22 +++ .../platform/nucleo-n657x0-q/exec_wrapper.py | 16 +- .../nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb | 17 ++ .../nucleo-n657x0-q/linker/ram_secure.ld | 180 ++++++++++++++++++ .../platform/nucleo-n657x0-q/platform.mk | 12 +- 5 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 test/baremetal/platform/nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb create mode 100644 test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld diff --git a/nix/nucleo-n657x0-q/default.nix b/nix/nucleo-n657x0-q/default.nix index c2f269ebfd..1d3923220f 100644 --- a/nix/nucleo-n657x0-q/default.nix +++ b/nix/nucleo-n657x0-q/default.nix @@ -70,6 +70,28 @@ stdenvNoCC.mkDerivation { fi if [ -f "$ld_src" ]; then cp -v "$ld_src" "$outp/gcc/linker/STM32N657XX_LRUN.ld" + # Patch LRUN linker script to set initial stack pointer into D-TCM (256 KiB window) + ld_file="$outp/gcc/linker/STM32N657XX_LRUN.ld" + if [ -f "$ld_file" ]; then + echo "Patching LRUN linker script for D-TCM stack (MSP/MSPLIM in 0x30000000-0x30040000)" + # Common patterns: direct assignments or PROVIDE() wrappers; handle both if present + sed -i.bak -E 's@PROVIDE\(\s*_estack\s*=\s*[^;]+\);@PROVIDE(_estack = 0x30040000);@' "$ld_file" || true + sed -i.bak -E 's@PROVIDE\(\s*__StackTop\s*=\s*[^;]+\);@PROVIDE(__StackTop = 0x30040000);@' "$ld_file" || true + sed -i.bak -E 's@PROVIDE\(\s*__initial_sp\s*=\s*[^;]+\);@PROVIDE(__initial_sp = 0x30040000);@' "$ld_file" || true + sed -i.bak -E 's@PROVIDE\(\s*__StackLimit\s*=\s*[^;]+\);@PROVIDE(__StackLimit = 0x30000000);@' "$ld_file" || true + sed -i.bak -E 's@(^|[^A-Za-z_])(_estack)\s*=\s*0x[0-9A-Fa-f]+@\1\2 = 0x30040000@g' "$ld_file" || true + sed -i.bak -E 's@(^|[^A-Za-z_])(__StackTop)\s*=\s*0x[0-9A-Fa-f]+@\1\2 = 0x30040000@g' "$ld_file" || true + sed -i.bak -E 's@(^|[^A-Za-z_])( __initial_sp|__initial_sp)\s*=\s*0x[0-9A-Fa-f]+@\1__initial_sp = 0x30040000@g' "$ld_file" || true + sed -i.bak -E 's@(^|[^A-Za-z_])( __StackLimit|__StackLimit)\s*=\s*0x[0-9A-Fa-f]+@\1__StackLimit = 0x30000000@g' "$ld_file" || true + # Append fallback PROVIDE definitions (won't override explicit definitions) + if ! grep -q "__StackTop" "$ld_file"; then + printf '\n/* D-TCM stack placement for LRUN */\n' >> "$ld_file" + printf 'PROVIDE(_estack = 0x30040000);\n' >> "$ld_file" + printf 'PROVIDE(__StackTop = _estack);\n' >> "$ld_file" + printf 'PROVIDE(__initial_sp = _estack);\n' >> "$ld_file" + printf 'PROVIDE(__StackLimit = 0x30000000);\n' >> "$ld_file" + fi + fi fi if [ -f "$sys_src" ]; then cp -v "$sys_src" "$outp/system_stm32n6xx.c" diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index fc87c78c68..2bad5f4101 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -411,20 +411,34 @@ def _semihost_reader(sock: socket.socket): err(f"[exec_wrapper] STLinkUpgrade candidate: {h}") return 2 - # Write GDB commands to a temp script and run with -x + # Optionally prepend LRUN DTCM GDB script (env gate) + lrun_gdb_path = os.path.join(os.path.dirname(__file__), "gdb", "lrun_dtcm_stack.gdb") gdb_lines = [ "set pagination off", "set confirm off", f"target remote localhost:{port}", "monitor reset", + ] + if os.path.isfile(lrun_gdb_path): + with open(lrun_gdb_path) as fd: + gdb_lines += map(lambda x: x.strip(),fd.readlines()) + # gdb_lines.append(f"source {lrun_gdb_path}") + # Write GDB commands to a temp script and run with -x + gdb_lines += [ # semihosting enable is handled by gdbserver; keep gdb quiet "load", + "set $pc=(&Reset_Handler)|1", "tbreak __wrap_main", "continue", (f"restore {argv_bin} binary {arg_block_addr}" if arg_block_addr else f"restore {argv_bin} binary &{arg_block_sym}"), "continue", ] + if VERBOSE: + print('============ GDB SCRIPT ============') + print('\n'.join(gdb_lines)) + print('====================================') + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".gdb") as gs: for line in gdb_lines: gs.write(line + "\n") diff --git a/test/baremetal/platform/nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb b/test/baremetal/platform/nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb new file mode 100644 index 0000000000..515ce5d78b --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb @@ -0,0 +1,17 @@ +# LRUN DTCM sizing and stack placement helper for STM32N657 (Cortex-M55) +# - Enables SYSCFG clock +# - Ensures SYSCFG_CM55TCMCR low byte is 0x99 (256 KiB D-TCM) and resets once if changed +# - Startup will load initial SP from the vector table; LRUN linker is patched to place it in D-TCM + +set pagination off +set confirm off + +# Expect target already connected (target remote) before sourcing this file. + +# RCC APB4ENSR2 (enable SYSCFG): 0x46028A78, bit 0 +set *0x46028A78=1 + +# SYSCFG base 0x46008000; CM55TCMCR offset 0x08; low byte selects D-TCM size (0x99 -> 256 KiB) +set *(unsigned int*)0x46008008 = 0x99 +monitor reset + diff --git a/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld b/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld new file mode 100644 index 0000000000..e2a8e72b23 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld @@ -0,0 +1,180 @@ +/* NUCLEO-N657X0-Q RAM-only Secure Linker Script + * - Run from RAM only (no flash LMA) + * - Secure mode only + * - Stack in D-TCM: 0x30000000 .. 0x3003FFFF (MSP at 0x30040000) + * - Hot code in I-TCM: 0x10000000 .. 0x1003FFFF + * - Regular SRAM from 0x34064000 upward + * - First 0x400 bytes of SRAM reserved for .isr_vector + */ + +ENTRY(Reset_Handler) + +/* Memory regions */ +MEMORY +{ + ITCM (rx) : ORIGIN = 0x00000000, LENGTH = 0x00040000 + DTCM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00040000 + VECT (rx) : ORIGIN = 0x34064000, LENGTH = 0x00000400 /* reserved for .isr_vector */ + SRAM (rwx) : ORIGIN = 0x34064400, LENGTH = 0x00FFFC00 /* remainder of SRAM window */ +} + +/* Explicit program headers to avoid orphaned sections */ +PHDRS +{ + itcm_text PT_LOAD FLAGS(5); /* R X */ + sram_rw PT_LOAD FLAGS(7); /* R W X (single segment for simplicity) */ +} + +/* Stack in D-TCM: use top of region */ +PROVIDE(_estack = ORIGIN(DTCM) + LENGTH(DTCM)); +PROVIDE(__StackTop = _estack); +PROVIDE(__initial_sp = _estack); +PROVIDE(__StackLimit = ORIGIN(DTCM)); +PROVIDE(_sstack = __StackLimit); + +/* Optional heap/stack minima (not enforced) */ +_Min_Heap_Size = 0x0000; +_Min_Stack_Size = 0x0000; + +/* Section placement */ +SECTIONS +{ + /* Vector table region (0x34064000..0x340643FF) */ + .isr_vector : + { + . = ALIGN(4); + KEEP(*(.isr_vector)) + KEEP(*(.isr_vector.*)) + . = ALIGN(4); + } > VECT :sram_rw + + /* High-speed code in I-TCM (optional) */ + .itcm_text : + { + . = ALIGN(8); + KEEP(*(.itcm_text)) + *(.itcm_text.*) + *(.text.itcm) + *(.text.itcm.*) + *(.ramfunc.itcm) + *(.ramfunc.itcm.*) + . = ALIGN(8); + } > ITCM :itcm_text + + /* Regular code in SRAM */ + .text : + { + . = ALIGN(8); + KEEP(*(.text.startup)) + *(.text) + *(.text.*) + KEEP(*(.init)) + KEEP(*(.fini)) + . = ALIGN(8); + } > SRAM :sram_rw + + /* CMSE veneers/stubs (secure gateway) must be in executable region */ + .gnu.sgstubs : + { + . = ALIGN(4); + KEEP(*(.gnu.sgstubs)) + . = ALIGN(4); + } > SRAM :sram_rw + + /* Exception unwinding tables */ + .ARM.extab : + { + *(.ARM.extab* .gnu.linkonce.armextab.*) + } > SRAM :sram_rw + + __exidx_start = .; + .ARM.exidx : + { + *(.ARM) + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + } > SRAM :sram_rw + __exidx_end = .; + + /* Read-only data */ + .rodata : + { + . = ALIGN(32); + *(.rodata) + *(.rodata.*) + . = ALIGN(32); + } > SRAM :sram_rw + + /* C++ init/fini arrays */ + .preinit_array : + { + PROVIDE(__preinit_array_start = .); + KEEP(*(.preinit_array*)) + PROVIDE(__preinit_array_end = .); + } > SRAM :sram_rw + + .init_array : + { + PROVIDE(__init_array_start = .); + KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*))) + KEEP(*(.init_array*)) + PROVIDE(__init_array_end = .); + } > SRAM :sram_rw + + .fini_array : + { + PROVIDE(__fini_array_start = .); + KEEP(*(SORT_BY_INIT_PRIORITY(.fini_array.*))) + KEEP(*(.fini_array*)) + PROVIDE(__fini_array_end = .); + } > SRAM :sram_rw + + /* Writable data (no flash LMA; RAM-run) */ + _sidata = .; + .data : + { + . = ALIGN(8); + _sdata = .; + *(.data) + *(.data.*) + *(.data.rel.ro*) + . = ALIGN(8); + _edata = .; + } > SRAM :sram_rw + + /* Optional noncacheable (default to SRAM) */ + .noncacheable (NOLOAD) : + { + *(.noncacheable) + *(.noncacheable.*) + } > SRAM :sram_rw + + /* Zero-initialized data */ + .bss (NOLOAD) : + { + . = ALIGN(8); + _sbss = .; + *(.bss) + *(.bss.*) + *(COMMON) + . = ALIGN(8); + _ebss = .; + } > SRAM :sram_rw + PROVIDE(end = _ebss); + + /* Vendor placeholder (do not use for stack) */ + ._user_heap_stack (NOLOAD) : + { + *(._user_heap_stack*) + } > SRAM :sram_rw + + /* Discard non-allocated/debug sections */ + /DISCARD/ : + { + *(.comment) + *(.note*) + *(.gnu_debuglink) + *(.gnu.lto_*) + *(.ARM.attributes) + *(.debug*) + } +} diff --git a/test/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk index 9b6dbeeae3..f35bb6edc6 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/platform.mk +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -49,8 +49,8 @@ CFLAGS += $(CFLAGS_EXTRA) # Try to auto-detect a GCC linker script from the FSBL or CMSIS template; fall back to linker.ld if present # Prefer linker scripts under gcc/linker/, fall back to other locations. # Try to pick an N657-specific script first. -# Pin LRUN linker script explicitly for RAM-run -LDSCRIPT := $(NUCLEO_N657X0_Q_PATH)/gcc/linker/STM32N657XX_LRUN.ld +# Use custom RAM-only secure linker script for this platform +LDSCRIPT := $(PLATFORM_PATH)/linker/ram_secure.ld # Auto-detect startup assembly case and optional board glue # Auto-detect startup assembly for STM32N6 family (prefer n657 if present) @@ -101,12 +101,4 @@ EXTRA_SOURCES_CFLAGS = -Wno-error -Wno-conversion -Wno-sign-conversion -Wno-unus # Avoid duplicate __wrap_main by excluding the generic integration_argv.c (not generated anymore) EXTRA_SOURCES := $(filter-out %/integration_argv.c,$(EXTRA_SOURCES)) -# D-TCM stack enable (wrap Reset_Handler to bootstrap TCM sizing and set MSP/MSPLIM) -NUCLEO_N657X0_Q_DTCM_STACK ?= 1 -ifeq ($(NUCLEO_N657X0_Q_DTCM_STACK),1) - EXTRA_SOURCES += $(PLATFORM_PATH)/src/reset_dtcm_init.S - # Only wrap SystemInit; let vendor Reset_Handler set initial SP, then relocate in wrapper - LDFLAGS += -Wl,--wrap=SystemInit -endif - EXEC_WRAPPER := $(realpath $(PLATFORM_PATH)/exec_wrapper.py) From e30a9a822b196c1512088b78b18f7dc876b23a82 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Fri, 1 May 2026 11:03:25 +0100 Subject: [PATCH 05/20] Func tests working Signed-off-by: Brendan Moran --- nix/nucleo-n657x0-q/default.nix | 11 +- .../platform/nucleo-n657x0-q/README.md | 201 +++++++-------- .../platform/nucleo-n657x0-q/exec_wrapper.py | 244 +++++++++++++++--- .../nucleo-n657x0-q/flexmem_configure.py | 190 ++++++++++++++ .../nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb | 17 -- .../linker/flexmem_config_default.ld | 171 ++++++++++++ .../nucleo-n657x0-q/linker/ram_secure.ld | 239 +++++++++-------- .../platform/nucleo-n657x0-q/platform.mk | 40 ++- .../nucleo-n657x0-q/run_test_after_flexmem.py | 16 ++ .../platform/nucleo-n657x0-q/src/cmdline.c | 56 +++- .../nucleo-n657x0-q/src/cmdline_region.c | 4 +- .../nucleo-n657x0-q/src/flexmem_config.c | 50 ++++ .../src/flexmem_layout_check.c | 40 +++ .../platform/nucleo-n657x0-q/src/libfns.c | 7 - .../nucleo-n657x0-q/src/reset_dtcm_init.S | 80 ------ .../nucleo-n657x0-q/src/semihosting_syscall.c | 60 +++-- .../nucleo-n657x0-q/src/semihosting_syscall.h | 5 +- 17 files changed, 1035 insertions(+), 396 deletions(-) create mode 100755 test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py delete mode 100644 test/baremetal/platform/nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb create mode 100644 test/baremetal/platform/nucleo-n657x0-q/linker/flexmem_config_default.ld create mode 100755 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py create mode 100644 test/baremetal/platform/nucleo-n657x0-q/src/flexmem_config.c create mode 100644 test/baremetal/platform/nucleo-n657x0-q/src/flexmem_layout_check.c delete mode 100644 test/baremetal/platform/nucleo-n657x0-q/src/reset_dtcm_init.S diff --git a/nix/nucleo-n657x0-q/default.nix b/nix/nucleo-n657x0-q/default.nix index 1d3923220f..011b719a46 100644 --- a/nix/nucleo-n657x0-q/default.nix +++ b/nix/nucleo-n657x0-q/default.nix @@ -67,6 +67,15 @@ stdenvNoCC.mkDerivation { if [ -f "$start_src" ]; then # Normalize to canonical name searched by platform.mk cp -v "$start_src" "$outp/gcc/startup_stm32n657xx.S" + # Replace the Cube FSBL startup's fixed SRAM initial SP with the + # linker-provided stack top so RAM-only tests can use the DTCM stack + # selected by their linker script. + sed -i.bak -E 's@[.]word[[:space:]]+0x34110000@.word _estack@' "$outp/gcc/startup_stm32n657xx.S" + # Disable interrupts immediately on entry to Reset_Handler. The RAM + # test image starts after debugger-controlled load/jump, so this keeps + # stale board/debug state from delivering an interrupt before C startup + # has initialized the test image. + sed -i.bak -E '/^Reset_Handler:/a\ cpsid i' "$outp/gcc/startup_stm32n657xx.S" fi if [ -f "$ld_src" ]; then cp -v "$ld_src" "$outp/gcc/linker/STM32N657XX_LRUN.ld" @@ -250,7 +259,7 @@ stdenvNoCC.mkDerivation { echo "WARNING: FSBL main.c not found at $fsbl_main_c; skipping clock_config generation" >&2 fi - # No linker script stack-size patch: MSP/MSPLIM relocation handled in Reset_Handler (reset_dtcm_init.S) + # The repository linker scripts define RAM-only config/test layouts explicitly. ''; setupHook = writeText "setup-hook.sh" '' diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md index 75fe8718e9..b578ab6446 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/README.md +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -7,141 +7,122 @@ SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT # NUCLEO-N657X0-Q Baremetal Platform -This platform runs ML-KEM tests on the ST NUCLEO‑N657X0‑Q board using STM32Cube Command Line Tools (CLT) and ST‑LINK gdbserver. The `exec_wrapper.py` launches the gdbserver, injects argv into target memory, streams semihost output, and runs a batch `gdb` session against the board. +This platform runs ML-KEM tests on the ST NUCLEO-N657X0-Q board with STM32Cube Command Line Tools and ST-LINK GDB server. The board is never flashed: both the FLEXMEM config binary and test binaries are loaded into RAM with GDB `load`. + +## Required Flow + +STM32N657X0 starts with 64 KiB ITCM and 128 KiB DTCM. Tests require 256 KiB ITCM and 256 KiB DTCM, so CI uses a deterministic two-binary sequence: + +1. Load `flexmem_config.elf` into default-reset RAM. +2. Run it until `SYSCFG->CM55TCMCR` reports the expected FLEXMEM layout, then print `FLEXMEM configuration complete; reset target and load test binary.` on the host. +3. Stop the GDB server session. +4. Reset/reconnect the target. +5. Load the test ELF into the expanded ITCM/DTCM RAM layout. +6. Run the test, dump the target stdout capture buffer at the final breakpoint, and use `[[MLKEM-EXIT:]]` as the exit sentinel. + +RAM is wiped by reset; the test ELF is loaded only after the config binary has completed and the target has been reset. ## Prerequisites -- Install STM32Cube CLT (includes ST‑LINK gdbserver and STM32CubeProgrammer): - - Download: https://www.st.com/en/development-tools/stm32cubeclt.html - - Verify tools are present on your PATH (example macOS install path): - - `/opt/ST/STM32CubeCLT_/STLink-gdb-server/bin/ST-LINK_gdbserver` - - `/opt/ST/STM32CubeCLT_/STM32CubeProgrammer/bin/STM32_Programmer_CLI` -- Hardware: NUCLEO‑N657X0‑Q connected over USB. Update ST‑LINK firmware if prompted: - - macOS app: `/STM32CubeProgrammer/stlink/STLinkUpgrade.app` - - CLI: `/STM32CubeProgrammer/stlink/STLinkUpgrade` - -## DevShell (required) -Run gdb, make and exec_wrapper.py commands in this README from within the project’s Nix devshell for this board: + +- STM32Cube CLT with `ST-LINK_gdbserver`, `STM32_Programmer_CLI`, and `arm-none-eabi-gdb`. +- A NUCLEO-N657X0-Q connected over USB. +- Run all commands from the board devshell: ``` nix develop .#nucleo-n657x0-q ``` -Then, in that shell, run the Make targets and Python scripts below. - -## Environment Variables (exec_wrapper.py) -- `GDB` (default: `arm-none-eabi-gdb`) – gdb binary. -- `GDB_PORT` (default: `3333`) – gdbserver port. -- `ST_CUBE_CLT_ROOT` – CLT root; helps auto‑locate gdbserver and CLI. -- `ST_CUBE_PROG_PATH` – path to `STM32_Programmer_CLI` bin dir; passed via `-cp`. -- `ST_GDBSERVER_CMD` – optional template to override gdbserver command. -- `STLINK_SPEED` (default: `200`) – SWD speed in kHz (e.g. `50` for reliability). -- `STLINK_SERIAL` – ST‑LINK serial string (strongly recommended when multiple probes). -- `STLINK_APID` (default: `1`) – Access Port/core selection. -- `STLINK_TRANSPORT` (default: `SWD`) – debug transport. -- `STLINK_CONNECT_MODE` (default: `under-reset`) – connection mode hint. -- `ST_DEVICE` (default: `STM32N657X0HxQ`) – device name hint (not always used). -- `STLINK_PEND_HALT_TIMEOUT` (default: `8000`) – pending halt timeout (ms). -- `STLINK_SEMIHOST_PORT` (default: auto) – semihost console TCP port. -- `STLINK_SEMIHOST_LEVEL` (default: `all`) – semihosting level (gdbserver). - -## Recommended ST‑LINK gdbserver template -- Default baseline (auto‑selected when available; expanded by the wrapper): -``` -ST-LINK_gdbserver -p {port} -l 1 -d -s --frequency {speed} {serial_flag} {apid_flag} {cubeprog_flag} -g --semihost-console-port {semi_port} --semihosting {semi_level} --initialize-reset --halt --pend-halt-timeout {pend} -``` -- Placeholders: - - `{serial_flag}` → `-i ` if `STLINK_SERIAL` set; else empty - - `{apid_flag}` → `-m ` if `STLINK_APID` set; else `-m 1` - - `{cubeprog_flag}` → `-cp ` if `ST_CUBE_PROG_PATH` or auto‑located - - `{port}`, `{speed}`, `{semi_port}`, `{semi_level}`, `{pend}` – from env/defaults +Useful environment variables: -Alternative (STM32_Programmer_CLI gdbserver): -``` -STM32_Programmer_CLI -c port={transport} freq={speed} {serial_prog} -gdbserver port={port} -``` -- `{serial_prog}` → `sn=` if `STLINK_SERIAL` set - -## Semihost output and verbosity -- The wrapper enables semihosting in the gdbserver and connects a TCP listener before GDB attaches. -- Each semihost line is prefixed with `[semi] `; only semihost lines print by default. -- Add `--verbose` (or `-v`) to print wrapper diagnostics, gdbserver output, and gdb chatter. - -## Argv injection -- Tests receive arguments via a reserved BSS block symbol `mlk_cmdline_block`. -- The wrapper resolves the numeric base address via `arm-none-eabi-nm` (fallback: `readelf -s`); - override with `ARG_BLOCK_ADDR` (hex) or choose a different symbol with `ARG_BLOCK_SYMBOL`. - -### Argv blob layout (authoritative) -- 4 bytes: `u32 argc` (little-endian). -- `argc` × 4 bytes: `u32 argv_ptrs[i]` (absolute addresses): `base + string_offset[i]`. -- NUL-terminated UTF-8 strings placed sequentially after the pointer table. -- Alignment: strings start at offset `4 + 4*argc` (4-byte aligned). -- Helper: `test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py` can generate `argv.bin` manually. - -- The wrapper packs argv into a temporary `argv.bin` and restores it via GDB: - - Resolves the symbol’s numeric address using `arm-none-eabi-nm` (fallback: `readelf -s`). - - Uses `restore binary ` in the GDB batch. -- Manual generator: `test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py` - - Example: `python3 .../make_argv_bin.py /tmp/argv.bin [arg1 ...]` - -## Quick start -1) Set environment (adjust paths/serial): ``` export ST_CUBE_CLT_ROOT=/opt/ST/STM32CubeCLT_1.20.0 export ST_CUBE_PROG_PATH=/opt/ST/STM32CubeCLT_1.20.0/STM32CubeProgrammer/bin -export STLINK_SERIAL= -export STLINK_SPEED=50 -export GDB_PORT=61234 +export STLINK_SERIAL= +export STLINK_SPEED=200 +export GDB_PORT=3333 ``` -2) Run a single test binary directly: + +The wrappers also accept `ST_GDBSERVER_CMD`, `STLINK_APID`, `STLINK_SEMIHOST_PORT`, `STLINK_SEMIHOST_LEVEL`, `STLINK_PEND_HALT_TIMEOUT`, `GDB`, `NM`, and `READELF`. + +## Build + +Build the FLEXMEM config binary and one RAM-only test binary: + ``` -python3 test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py --verbose test/build/mlkem512/bin/test_mlkem512 +make flexmem_config func_512 EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1 ``` -3) Or via Makefile targets (from repo root): + +The config binary is: + ``` -make run_func_512 EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1 +test/build/nucleo-n657x0-q/flexmem_config.elf ``` -## Manual GDB session (advanced) +An example test binary is: + ``` -# In a terminal, start gdbserver manually (example): -ST-LINK_gdbserver -p 61234 -l 1 -d -s -cp "$ST_CUBE_PROG_PATH" -m 1 --semihost-console-port 7185 --semihosting all --initialize-reset --halt --pend-halt-timeout 8000 - -# In another terminal (gdb): -(gdb) file -(gdb) target remote :61234 -(gdb) monitor reset -(gdb) load -(gdb) tbreak __wrap_main -(gdb) continue -(gdb) restore /tmp/argv.bin binary &mlk_cmdline_block # or numeric address -(gdb) continue +test/build/mlkem512/bin/test_mlkem512 ``` -## Troubleshooting -- USB/Probe: If you see `DEV_USB_COMM_ERR` or timeouts, unplug/replug, try a different USB port, and update ST‑LINK firmware. -- Probe selection: set `STLINK_SERIAL=` to disambiguate. -- Speed: reduce `STLINK_SPEED` (e.g., `50`) for stability. -- Tools not found: set `ST_CUBE_CLT_ROOT` and/or `ST_CUBE_PROG_PATH` so the wrapper can find `ST-LINK_gdbserver` and `STM32_Programmer_CLI`. -- Semihost port: if the listener can’t connect in time, the wrapper proceeds; try a fixed `STLINK_SEMIHOST_PORT`. +## Run in CI -## Notes -- ST‑LINK gdbserver does not implement the QEMU semihost `SYS_EXIT_EXTENDED`. A sentinel‑based exit workaround is planned in the proposal. -- This platform uses FSBL‑LRUN startup/system/linker from the Cube template. -- D‑TCM stack: a startup bootstrap (`src/reset_dtcm_init.S`) checks `SYSCFG_CM55TCMCR` and, if needed, writes `0x99` then resets via AIRCR to apply D‑TCM sizing. Otherwise it sets `MSP=0x30040000` and `MSPLIM=0x30000000` so the runtime stack resides entirely in D‑TCM (256 KiB). A one‑time reset occurs the first time sizing is applied. Compatible with GDB resets. +Run the full deterministic sequence for `test_mlkem512`: -- Clock configuration: `SystemClock_Config()` is generated from the STM32CubeN6 FSBL template (`Projects/NUCLEO-N657X0-Q/Templates/Template_FSBL_LRUN/FSBL/Src/main.c`) into `${NUCLEO_N657X0_Q_PATH}/clock_config.c` by `nix/nucleo-n657x0-q/default.nix`. The devshell build also ensures `${NUCLEO_N657X0_Q_PATH}/Inc/main.h` declares `void SystemClock_Config(void);` and `void Error_Handler(void);`. Do not edit the generated file directly; update the Cube template or the extraction logic if adjustments are needed. +``` +make run_flexmem_test EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1 +``` -## HW‑testing -- Run from inside the Nix devshell: `nix develop .#nucleo-n657x0-q`. -- Hardware runs are opt‑in. Include this platform’s Makefile to target the NUCLEO‑N657X0‑Q board: +Or run each stage explicitly: ``` -make test EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1 +python3 test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py \ + test/build/nucleo-n657x0-q/flexmem_config.elf + +python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py \ + test/build/mlkem512/bin/test_mlkem512 ``` -- Examples: - - Single test: `make run_func_512 EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1` - - Direct wrapper: `python3 test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py test/build/mlkem512/bin/test_mlkem512` +`run_test_after_flexmem.py` delegates to `exec_wrapper.py`, which starts ST-LINK GDB server, loads the ELF into RAM, injects argv into `mlk_cmdline_block`, runs from `Reset_Handler`, dumps the target stdout capture buffer, and returns the `[[MLKEM-EXIT:]]` code. + +## Argv Blob Loading + +Target arguments are passed through a RAM blob rather than through semihosting or debugger command-line support. The test image links `cmdline_region.c`, which reserves the 64 KiB `mlk_cmdline_block` symbol in ITCM. `exec_wrapper.py` resolves that symbol with `arm-none-eabi-nm`, falling back to `readelf -s`; `ARG_BLOCK_SYMBOL` can select a different symbol and `ARG_BLOCK_ADDR` can override the resolved address. + +The wrapper packs its target argv into a temporary binary file with this little-endian layout: + +- `uint32_t argc` +- `uint32_t argv[argc]`, where each entry is an absolute target address inside the same blob +- NUL-terminated UTF-8 argument strings immediately after the pointer table + +The GDB command sequence intentionally loads the argv blob after C startup reaches `__wrap_main`: first `load` writes the ELF sections into RAM, then `jump Reset_Handler|1` runs normal startup and zeroes `.bss`, then a temporary breakpoint stops at `__wrap_main`, and only then GDB executes `restore binary `. Loading the blob at this point prevents startup `.bss` initialization from erasing it. `__wrap_main` casts `mlk_cmdline_block` to the command-line structure and calls the real ML-KEM test `main(argc, argv)`. + +## Memory Layout + +`linker/flexmem_config_default.ld` is used only by the config binary: + +- vector/code: SRAM available in the default reset layout +- data/stack: default 128 KiB DTCM +- no flash memory regions or flash LMAs + +`linker/ram_secure.ld` is used by tests after FLEXMEM reset has applied: + +- vector table and executable sections: expanded 256 KiB ITCM at `0x00000000` +- `.data`, `.bss`, argv block: expanded 256 KiB DTCM at `0x30000000` +- stack: top 192 KiB of DTCM, with `_estack = 0x30040000` and `__StackLimit = 0x30010000` +- no flash memory regions or flash LMAs + +## Layout Validation + +Each test binary links `flexmem_layout_check.c` and calls it before ML-KEM tests. It fails with a breakpoint if either expanded region is unavailable: + +- executes `nucleo_itcm_above_default_probe()` from `.itcm_probe` placed beyond the default 64 KiB ITCM boundary +- writes and reads address `0x30020000`, beyond the default 128 KiB DTCM boundary + +This proves the CI flow loaded the test after FLEXMEM configuration and reset. Actual pass/fail execution remains hardware-dependent on the connected board, ST-LINK firmware, and CLT version. + +## Notes -- Without `EXTRA_MAKEFILE`, tests run on the host (no hardware). +- Do not use debugger GUI flows; CI uses ST-LINK GDB server plus `arm-none-eabi-gdb` only. +- Do not run the test binary directly on a freshly reset board; it links into expanded ITCM/DTCM that does not exist until after `flexmem_config.elf` and reset. +- Do not reintroduce runtime probing or linker wrapping of `SystemInit`; FLEXMEM configuration is deterministic and reset-applied. +- Keep target output on normal libc/newlib `printf` and file-I/O paths linked with `--specs=rdimon.specs -lc -lrdimon`; the platform `_write` captures stdout/stderr in RAM for GDB to harvest because raw semihosting `bkpt 0xab` syscalls are not reliable on this board. diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index 2bad5f4101..b034c1e319 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -4,6 +4,7 @@ # Copyright (c) Arm Ltd. # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT +import logging import os import shlex import shutil @@ -18,16 +19,31 @@ VERBOSE = False +LOG = logging.getLogger(__name__) + + +def configure_logging(): + level = logging.DEBUG if VERBOSE else logging.INFO + logging.basicConfig(level=level, format="%(message)s") + + +def log_output(output, level=logging.INFO, prefix=None): + if not output: + return + for line in str(output).rstrip().splitlines(): + if prefix: + line = f"{prefix}{line}" + LOG.log(level, "%s", line) def err(msg, **kwargs): - # Always print errors - print(msg, file=sys.stderr, **kwargs) + # Always report errors, including multiline subprocess diagnostics. + log_output(msg, logging.ERROR) def info(msg, **kwargs): if VERBOSE: - print(msg, file=sys.stderr, **kwargs) + LOG.debug("%s", msg) def pack_cmdline(args, base_addr): @@ -79,6 +95,71 @@ def _wait_for_port(host: str, port: int, timeout_s: float) -> bool: return False +def _stm32_programmer_cli(cp_path: str): + # Accept either a direct CLI path or the containing CubeProgrammer directory. + if not cp_path: + return None + candidates = [] + if os.path.isdir(cp_path): + candidates += [ + os.path.join(cp_path, "STM32_Programmer_CLI"), + os.path.join(cp_path, "bin", "STM32_Programmer_CLI"), + ] + else: + candidates.append(cp_path) + for candidate in candidates: + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return None + + +def _cubeprogrammer_cli(st_cubeprog: str, st_clt_root: str): + # Prefer explicit paths from the environment before falling back to PATH. + candidates = [st_cubeprog] + if st_clt_root: + candidates.append(os.path.join(st_clt_root, "STM32CubeProgrammer")) + cli = None + for candidate in candidates: + cli = _stm32_programmer_cli(candidate) + if cli: + break + if cli is None: + cli = shutil.which("STM32_Programmer_CLI") + return cli + + +def _cubeprogrammer_connect_args(st_speed: str, st_serial: str, st_apid: str): + # Keep reset/readback commands aligned with the selected probe speed and AP. + args = ["-c", "port=SWD", f"freq={st_speed}"] + connect_mode = os.environ.get("STLINK_CONNECT_MODE") + if connect_mode: + args.append(f"mode={connect_mode}") + if st_serial: + args.append(f"sn={st_serial}") + if st_apid: + args.append(f"ap={st_apid}") + return args + + +def _run_cubeprogrammer(cli: str, connect_args, commands, verbose: bool = False) -> bool: + # CubeProgrammer diagnostics are noisy, so log them only on failure or request. + cmd = [cli] + connect_args + commands + cp = run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if verbose or cp.returncode != 0: + output = cp.stdout or "" + if output: + log_output(output, logging.DEBUG if verbose else logging.ERROR) + return cp.returncode == 0 + + +def _reset_target(st_cubeprog: str, st_clt_root: str, st_speed: str, st_serial: str, st_apid: str) -> bool: + # Reset through CubeProgrammer when available; callers can still proceed if absent. + cli = _cubeprogrammer_cli(st_cubeprog, st_clt_root) + if cli is None: + return False + return _run_cubeprogrammer(cli, _cubeprogrammer_connect_args(st_speed, st_serial, st_apid), ["-rst"]) + + def main(): global VERBOSE @@ -91,6 +172,8 @@ def main(): VERBOSE = True argv.remove("-v") + configure_logging() + if len(argv) < 1: err("Usage: exec_wrapper.py [--verbose] [args...]") return 2 @@ -127,6 +210,7 @@ def main(): st_clt_root = os.environ.get("ST_CUBE_CLT_ROOT", "") # Root of STM32CubeCLT st_pend = os.environ.get("STLINK_PEND_HALT_TIMEOUT", "8000") st_apid = os.environ.get("STLINK_APID", "") + gdb_run_timeout = float(os.environ.get("GDB_RUN_TIMEOUT", "180")) # Semihosting configuration (enabled by default) st_semihost_port_env = os.environ.get("STLINK_SEMIHOST_PORT", "") try: @@ -175,6 +259,25 @@ def _resolve_symbol_addr(elf_path: str, sym: str): arg_block_sym = cand arg_block_addr = addr break + + # Numeric breakpoints avoid GDB symbol lookup surprises after loading RAM ELFs. + wrap_main_addr = _resolve_symbol_addr(elf, "__wrap_main") + wrap_main_break = "__wrap_main" + if wrap_main_addr is not None: + wrap_main_break = f"*{wrap_main_addr}" + reset_handler_addr = _resolve_symbol_addr(elf, "Reset_Handler") + reset_handler_jump = "Reset_Handler" + if reset_handler_addr is not None: + reset_handler_jump = f"*{hex(int(reset_handler_addr, 16) | 1)}" + if reset_handler_addr is None: + err("Failed to resolve Reset_Handler in ELF.") + return 2 + + # Resolve the RAM stdout buffer so GDB can dump target output after execution. + stdout_capture_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture") + stdout_capture_len_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture_len") + stdout_capture_truncated_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture_truncated") + stdout_capture_size = int(os.environ.get("NUCLEO_STDOUT_CAPTURE_SIZE", "32768")) # Allow override of base address via env (hex string) arg_block_addr_env = os.environ.get("ARG_BLOCK_ADDR") base_addr = None @@ -200,7 +303,8 @@ def _resolve_symbol_addr(elf_path: str, sym: str): argv_bin = os.path.join(td, "argv.bin") with open(argv_bin, "wb") as f: f.write(blob) - + # GDB writes target stdout here after the run; Python logs it below. + stdout_capture_bin = os.path.join(td, "stdout-capture.bin") # Build ST gdbserver command # Discover ST-LINK_gdbserver stlink_bin = shutil.which("ST-LINK_gdbserver") @@ -221,7 +325,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): # Auto-detect a default template if not provided if not st_gdbserver_cmd_tpl and stlink_bin: st_gdbserver_cmd_tpl = ( - f"{shlex.quote(stlink_bin)} -p {{port}} -l 1 -d -s --frequency {{speed}} {{serial_flag}} {{apid_flag}} {{cubeprog_flag}} -g --semihost-console-port {{semi_port}} --semihosting {{semi_level}} --initialize-reset --halt --pend-halt-timeout {{pend}}" + f"{shlex.quote(stlink_bin)} -p {{port}} -l 1 -d -s --frequency {{speed}} {{serial_flag}} {{apid_flag}} {{cubeprog_flag}} --semihost-console-port {{semi_port}} --semihosting {{semi_level}} -g --halt --pend-halt-timeout {{pend}}" ) if st_gdbserver_cmd_tpl: @@ -248,8 +352,8 @@ def _resolve_symbol_addr(elf_path: str, sym: str): cp_path = os.path.dirname(cli2) # If still None, try relative to ST-LINK gdbserver location if cp_path is None and 'stlink_bin' in locals() and stlink_bin: - # stlink_bin .../STLink-gdb-server/bin/ST-LINK_gdbserver -> root is two parents up - root = os.path.dirname(os.path.dirname(os.path.abspath(stlink_bin))) + # stlink_bin .../STLink-gdb-server/bin/ST-LINK_gdbserver -> CLT root is three parents up + root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(stlink_bin)))) cli2 = os.path.join(root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") if os.path.isfile(cli2) and os.access(cli2, os.X_OK): cp_path = os.path.dirname(cli2) @@ -295,7 +399,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): " Download: https://www.st.com/en/development-tools/stm32cubeclt.html\n" "- Set ST_GDBSERVER_CMD to a working gdbserver command template, or ensure ST-LINK_gdbserver is on PATH.\n" " Examples:\n" - " ST-LINK_gdbserver: 'ST-LINK_gdbserver -p {port} -d --frequency {speed} {serial_flag} --initialize-reset {cubeprog_flag}'\n" + " ST-LINK_gdbserver: 'ST-LINK_gdbserver -p {port} -d --frequency {speed} {serial_flag} -g --halt {cubeprog_flag}'\n" " STM32_Programmer_CLI: 'STM32_Programmer_CLI -c port={transport},{serial_prog} -s {speed} -gdbserver port={port}'\n" " Tip: If ST-LINK_gdbserver errors about STM32CubeProgrammer, set ST_CUBE_PROG_PATH to its installation path,\n" " or export ST_CUBE_CLT_ROOT to the CubeCLT root so the wrapper can auto-locate ST-LINK_gdbserver.\n" @@ -308,6 +412,8 @@ def _resolve_symbol_addr(elf_path: str, sym: str): err(msg) return 2 + info("[exec_wrapper] assuming FLEXMEM was configured by flexmem_configure.py; no runtime TCM probing") + info(f"[exec_wrapper] starting ST gdbserver on port {port}...") info(f"[exec_wrapper] {' '.join(gdbserver_cmd)}") stp = popen( @@ -358,15 +464,15 @@ def _semihost_reader(sock: socket.socket): except Exception: shared["exit_code"] = 1 semihost_exit.set() - # Do not print the sentinel unless verbose + # Do not log the sentinel unless verbose. if VERBOSE: - print(f"[semi] {text}") + LOG.debug("[semi] %s", text) else: - # Print semihost line; prefix only in verbose mode + # Log semihost lines; prefix only in verbose mode. if VERBOSE: - print(f"[semi] {text}") + LOG.debug("[semi] %s", text) else: - print(text) + LOG.info("%s", text) except socket.timeout: continue finally: @@ -391,7 +497,7 @@ def _semihost_reader(sock: socket.socket): # Server exited early – surface a helpful message out_rem = stp.stdout.read() if stp.stdout else "" if out_rem and VERBOSE: - print(out_rem, end="") + log_output(out_rem, logging.DEBUG) merged = out_rem low = merged.lower() if "firmware upgrade" in low or "upgrade required" in low: @@ -411,33 +517,49 @@ def _semihost_reader(sock: socket.socket): err(f"[exec_wrapper] STLinkUpgrade candidate: {h}") return 2 - # Optionally prepend LRUN DTCM GDB script (env gate) - lrun_gdb_path = os.path.join(os.path.dirname(__file__), "gdb", "lrun_dtcm_stack.gdb") gdb_lines = [ "set pagination off", "set confirm off", f"target remote localhost:{port}", - "monitor reset", ] - if os.path.isfile(lrun_gdb_path): - with open(lrun_gdb_path) as fd: - gdb_lines += map(lambda x: x.strip(),fd.readlines()) - # gdb_lines.append(f"source {lrun_gdb_path}") # Write GDB commands to a temp script and run with -x gdb_lines += [ # semihosting enable is handled by gdbserver; keep gdb quiet "load", - "set $pc=(&Reset_Handler)|1", - "tbreak __wrap_main", - "continue", + f"tbreak {wrap_main_break}", + f"jump {reset_handler_jump}", (f"restore {argv_bin} binary {arg_block_addr}" if arg_block_addr else f"restore {argv_bin} binary &{arg_block_sym}"), + "break HardFault_Handler", + "break nucleo_layout_fail", "continue", ] + if stdout_capture_addr and stdout_capture_len_addr: + # Clamp the dump length to the compile-time capture buffer size. + gdb_lines += [ + f"set $nucleo_stdout_len = *(unsigned int *){stdout_capture_len_addr}", + "if $nucleo_stdout_len > 0", + f" if $nucleo_stdout_len > {stdout_capture_size}", + f" set $nucleo_stdout_len = {stdout_capture_size}", + " end", + f" dump binary memory {stdout_capture_bin} {stdout_capture_addr} {stdout_capture_addr} + $nucleo_stdout_len", + "end", + ] + if stdout_capture_truncated_addr: + gdb_lines += [ + f"set $nucleo_stdout_truncated = *(unsigned int *){stdout_capture_truncated_addr}", + "p/x $nucleo_stdout_truncated", + ] + gdb_lines += [ + "info registers", + "x/4wx $sp", + "x/4wx 0xE000ED28", + "x/wx 0xE000ED38", + ] if VERBOSE: - print('============ GDB SCRIPT ============') - print('\n'.join(gdb_lines)) - print('====================================') + LOG.debug("============ GDB SCRIPT ============") + log_output('\n'.join(gdb_lines), logging.DEBUG) + LOG.debug("====================================") with tempfile.NamedTemporaryFile("w", delete=False, suffix=".gdb") as gs: for line in gdb_lines: @@ -449,6 +571,7 @@ def _semihost_reader(sock: socket.socket): # Run GDB while streaming gdbserver output (which will include semihost output). info("[exec_wrapper] running gdb batch (program will continue; semihost output follows)...") gdbp = popen(gdb_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + gdb_deadline = time.time() + gdb_run_timeout if gdb_run_timeout > 0 else None # Stream gdbserver output until gdb finishes without blocking on readline() while True: @@ -471,26 +594,87 @@ def _semihost_reader(sock: socket.socket): if r: line = stp.stdout.readline() if line: - # gdbserver stdout (printed only in verbose mode) + # gdbserver stdout is logged only in verbose mode. if VERBOSE: - print(line, end="") + log_output(line, logging.DEBUG) except Exception: # If select/readline fails, avoid blocking the loop pass # Check if gdb has completed if gdbp.poll() is not None: break + if gdb_deadline is not None and time.time() > gdb_deadline: + err("FAIL!") + err(f"gdb batch timed out after {gdb_run_timeout:.0f}s") + try: + gdbp.terminate() + gdbp.wait(timeout=1.0) + except Exception: + try: + gdbp.kill() + except Exception: + pass + try: + out, errout = gdbp.communicate(timeout=1.0) + if out: + log_output(out, logging.ERROR) + if errout: + err(errout, end="") + except Exception: + pass + return 124 out, errout = gdbp.communicate() if out and VERBOSE: - print(out, end="") + log_output(out, logging.DEBUG) if errout and VERBOSE: # gdb chatter / errors (verbose only) err(errout, end="") + gdb_text = f"{out}\n{errout}" + + captured_text = "" + if os.path.exists(stdout_capture_bin): + try: + # Parse the same exit sentinel from dumped RAM output as from semihosting. + with open(stdout_capture_bin, "rb") as capture_file: + captured = capture_file.read() + captured_text = captured.decode("utf-8", errors="replace") + captured_lines = [] + for capture_line in captured_text.splitlines(): + stripped_line = capture_line.strip() + if stripped_line.startswith("[[MLKEM-EXIT:") and stripped_line.endswith("]]"): + try: + shared["exit_code"] = int(stripped_line[len("[[MLKEM-EXIT:"):-2]) + except Exception: + shared["exit_code"] = 1 + continue + captured_lines.append(capture_line) + if captured_lines: + log_output("\n".join(captured_lines), logging.INFO) + except Exception as exc: + info(f"[exec_wrapper] failed to read stdout capture: {exc}") + + if "$nucleo_stdout_truncated = 0x1" in gdb_text: + err("WARNING: target stdout capture truncated") + if shared.get("exit_code") is not None: return int(shared["exit_code"]) if isinstance(shared["exit_code"], int) else 1 + if "nucleo_layout_fail" in gdb_text: + err("FAIL!") + err("FLEXMEM layout check failed on target") + return 1 + + if "HardFault_Handler" in gdb_text: + err("FAIL!") + err("Target entered HardFault_Handler") + return 1 + + if "Program received signal SIGTRAP" in gdb_text: + info("[exec_wrapper] completion trap observed without exit sentinel") + return 0 + if gdbp.returncode != 0: err("FAIL!") err(f"gdb batch failed with code {gdbp.returncode}") diff --git a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py new file mode 100755 index 0000000000..22512c6f82 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +/* + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ +""" + +# Configure STM32N6 FLEXMEM before loading the RAM-resident test image. + +import os +import re +import logging +import shutil +import subprocess +import sys +import time + +DONE = "FLEXMEM configuration complete; reset target and load test binary." + +# The config ELF writes SYSCFG->CM55TCMCR. Polling the register via SWD proves +# that the new ITCM/DTCM split latched before the next test binary is loaded. +CM55TCMCR_ADDR = "0x56008008" +CM55TCMCR_EXPECTED_MASK = 0xFF +CM55TCMCR_EXPECTED_VALUE = 0x99 + +LOG = logging.getLogger(__name__) + + +def configure_logging(): + level = logging.DEBUG if os.environ.get("FLEXMEM_VERBOSE") else logging.INFO + logging.basicConfig(level=level, format="%(message)s") + + +def log_output(output, level): + if not output: + return + for line in output.rstrip().splitlines(): + LOG.log(level, line) + + +def err(msg): + LOG.error("%s", msg) + + +def find_cubeprogrammer_cli(cp_path): + # Accept a direct binary path, a CubeProgrammer directory, a CubeCLT root, or PATH. + candidates = [] + if cp_path: + if os.path.isdir(cp_path): + candidates.extend([ + os.path.join(cp_path, "STM32_Programmer_CLI"), + os.path.join(cp_path, "bin", "STM32_Programmer_CLI"), + ]) + else: + candidates.append(cp_path) + st_clt_root = os.environ.get("ST_CUBE_CLT_ROOT", "") + if st_clt_root: + candidates.append(os.path.join(st_clt_root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI")) + path_candidate = shutil.which("STM32_Programmer_CLI") + if path_candidate: + candidates.append(path_candidate) + for candidate in candidates: + if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return None + + +def cubeprogrammer_cli(): + cli = find_cubeprogrammer_cli(os.environ.get("ST_CUBE_PROG_PATH", "")) + if cli is None: + err("STM32_Programmer_CLI not found; set ST_CUBE_PROG_PATH or ST_CUBE_CLT_ROOT") + return cli + + +def connect_args(mode=None): + # Keep all CubeProgrammer calls on the same probe, speed, and access port. + args = ["-c", "port=SWD", f"freq={os.environ.get('STLINK_SPEED', '200')}"] + connect_mode = mode if mode is not None else os.environ.get("STLINK_CONNECT_MODE") + if connect_mode: + args.append(f"mode={connect_mode}") + serial = os.environ.get("STLINK_SERIAL", "") + apid = os.environ.get("STLINK_APID", "") + if serial: + args.append(f"sn={serial}") + if apid: + args.append(f"ap={apid}") + return args + + +def run_quiet(cmd): + return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + + +def reset_target(cli): + # Reset is best-effort: the subsequent download/halt sequence reports hard failures. + args = ["-c", "port=SWD", f"freq={os.environ.get('STLINK_SPEED', '200')}"] + serial = os.environ.get("STLINK_SERIAL", "") + if serial: + args.append(f"sn={serial}") + return run_quiet([cli] + args + ["-rst"]).returncode == 0 + + +def resolve_symbol(elf, symbol): + # Resolve entry/stack symbols up front so CubeProgrammer can start from RAM directly. + nm = os.environ.get("NM", "arm-none-eabi-nm") + try: + cp = subprocess.run([nm, "-n", elf], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + except OSError: + return None + if cp.returncode != 0: + return None + for line in cp.stdout.splitlines(): + fields = line.split() + if len(fields) >= 3 and fields[-1] == symbol: + return "0x" + fields[0].lstrip("0x") + return None + + +def read_flexmem_value(cli): + # HOTPLUG reads avoid resetting the core while the config ELF is parked at BKPT. + cp = run_quiet([cli] + connect_args("HOTPLUG") + ["-r32", CM55TCMCR_ADDR, "1"]) + if os.environ.get("FLEXMEM_VERBOSE"): + log_output(cp.stdout, logging.DEBUG) + if cp.returncode != 0: + return None + match = re.search(rf"{re.escape(CM55TCMCR_ADDR)}\s*:\s*([0-9a-fA-F]{{8}})", cp.stdout) + if not match: + return None + return int(match.group(1), 16) + + +def wait_for_flexmem(cli, timeout_s): + # The register update is fast, but polling absorbs probe/transport latency. + deadline = time.time() + timeout_s + while time.time() < deadline: + value = read_flexmem_value(cli) + if value is not None and (value & CM55TCMCR_EXPECTED_MASK) == CM55TCMCR_EXPECTED_VALUE: + return True + time.sleep(0.2) + return False + + +def main(): + configure_logging() + + if len(sys.argv) != 2: + err(f"Usage: {sys.argv[0]} path/to/flexmem_config.elf") + return 2 + + elf = os.path.abspath(sys.argv[1]) + if not os.path.exists(elf): + err(f"Config ELF not found: {elf}") + return 2 + + cli = cubeprogrammer_cli() + if cli is None: + return 2 + + main_addr = resolve_symbol(elf, "main") + estack_addr = resolve_symbol(elf, "_estack") + if main_addr is None or estack_addr is None: + err("Failed to resolve main/_estack in config ELF") + return 2 + main_thumb = hex(int(main_addr, 16) | 1) + + timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "30")) + reset_target(cli) + + # Load the RAM-only config image and seed MSP/PC explicitly because no flash + # boot flow participates in this helper binary. + cmd = [cli] + connect_args() + ["-halt", "-d", elf, "-coreReg", f"MSP={estack_addr}", f"PC={main_thumb}", "-run"] + cp = run_quiet(cmd) + if os.environ.get("FLEXMEM_VERBOSE") or cp.returncode != 0: + log_output(cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR) + if cp.returncode != 0: + err("FLEXMEM config RAM download/start failed") + return cp.returncode + + if not wait_for_flexmem(cli, timeout_s): + err("FLEXMEM configuration register did not reach expected 0x99 value") + return 124 + + LOG.debug("%s", DONE) + reset_target(cli) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/baremetal/platform/nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb b/test/baremetal/platform/nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb deleted file mode 100644 index 515ce5d78b..0000000000 --- a/test/baremetal/platform/nucleo-n657x0-q/gdb/lrun_dtcm_stack.gdb +++ /dev/null @@ -1,17 +0,0 @@ -# LRUN DTCM sizing and stack placement helper for STM32N657 (Cortex-M55) -# - Enables SYSCFG clock -# - Ensures SYSCFG_CM55TCMCR low byte is 0x99 (256 KiB D-TCM) and resets once if changed -# - Startup will load initial SP from the vector table; LRUN linker is patched to place it in D-TCM - -set pagination off -set confirm off - -# Expect target already connected (target remote) before sourcing this file. - -# RCC APB4ENSR2 (enable SYSCFG): 0x46028A78, bit 0 -set *0x46028A78=1 - -# SYSCFG base 0x46008000; CM55TCMCR offset 0x08; low byte selects D-TCM size (0x99 -> 256 KiB) -set *(unsigned int*)0x46008008 = 0x99 -monitor reset - diff --git a/test/baremetal/platform/nucleo-n657x0-q/linker/flexmem_config_default.ld b/test/baremetal/platform/nucleo-n657x0-q/linker/flexmem_config_default.ld new file mode 100644 index 0000000000..af18bce140 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/linker/flexmem_config_default.ld @@ -0,0 +1,171 @@ +/* + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +/* Cortex-M55 GCC linker script: + * SRAM: 64 KiB @ 0x00000000 + * DTCM: 128 KiB @ 0x30000000 + */ + +ENTRY(Reset_Handler) + +MEMORY +{ + SRAM (rx) : ORIGIN = 0x34064000, LENGTH = 64K + DTCM (rwx) : ORIGIN = 0x30000000, LENGTH = 128K +} + +__sram_start__ = ORIGIN(SRAM); +__sram_size__ = LENGTH(SRAM); +__dtcm_start__ = ORIGIN(DTCM); +__dtcm_size__ = LENGTH(DTCM); + +__stack_size__ = 64K; +__heap_size__ = 0; + +__StackTop = ORIGIN(DTCM) + LENGTH(DTCM); +__StackLimit = __StackTop - __stack_size__; + +PROVIDE(__stack = __StackTop); +PROVIDE(_estack = __StackTop); +PROVIDE(__initial_sp = __StackTop); +PROVIDE(_sstack = __StackLimit); + +SECTIONS +{ + .vectors ORIGIN(SRAM) : + { + KEEP(*(.isr_vector)) + KEEP(*(.vectors)) + . = ORIGIN(SRAM) + 0x400; + } > SRAM + + .text : + { + . = ALIGN(4); + __text_start__ = .; + + *(.text) + *(.text.*) + *(.gnu.linkonce.t.*) + + KEEP(*(.init)) + KEEP(*(.fini)) + + *(.rodata) + *(.rodata.*) + *(.gnu.linkonce.r.*) + + . = ALIGN(4); + KEEP(*(.eh_frame*)) + KEEP(*(.ARM.extab* .gnu.linkonce.armextab.*)) + + . = ALIGN(4); + __exidx_start = .; + KEEP(*(.ARM.exidx* .gnu.linkonce.armexidx.*)) + __exidx_end = .; + + . = ALIGN(4); + __text_end__ = .; + } > SRAM + + .gnu.sgstubs : + { + . = ALIGN(32); + KEEP(*(.gnu.sgstubs)) + KEEP(*(.gnu.sgstubs.*)) + . = ALIGN(32); + } > SRAM + + .preinit_array : + { + PROVIDE_HIDDEN(__preinit_array_start = .); + KEEP(*(.preinit_array*)) + PROVIDE_HIDDEN(__preinit_array_end = .); + } > SRAM + + .init_array : + { + PROVIDE_HIDDEN(__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array*)) + PROVIDE_HIDDEN(__init_array_end = .); + } > SRAM + + .fini_array : + { + PROVIDE_HIDDEN(__fini_array_start = .); + KEEP(*(SORT(.fini_array.*))) + KEEP(*(.fini_array*)) + PROVIDE_HIDDEN(__fini_array_end = .); + } > SRAM + + __etext = ALIGN(4); + _etext = __etext; + + .data : AT(__etext) + { + . = ALIGN(4); + __data_start__ = .; + _sdata = .; + + *(.data) + *(.data.*) + *(.gnu.linkonce.d.*) + + . = ALIGN(4); + __data_end__ = .; + _edata = .; + } > DTCM + + __data_load__ = LOADADDR(.data); + _sidata = __data_load__; + + .bss (NOLOAD) : + { + . = ALIGN(4); + __bss_start__ = .; + _sbss = .; + + *(.bss) + *(.bss.*) + *(.gnu.linkonce.b.*) + *(COMMON) + + . = ALIGN(4); + __bss_end__ = .; + _ebss = .; + } > DTCM + + .heap (NOLOAD) : + { + . = ALIGN(8); + __end__ = .; + end = .; + _end = .; + __HeapBase = .; + . += __heap_size__; + . = ALIGN(8); + } > DTCM + + __HeapLimit = __StackLimit; + + .stack __StackLimit (NOLOAD) : + { + . = ALIGN(8); + _sstack = .; + . += __stack_size__; + } > DTCM + + /DISCARD/ : + { + *(.note*) + *(.comment*) + } +} + +ASSERT(SIZEOF(.vectors) <= 0x400, "Vector table exceeds 0x400 bytes") +ASSERT(__StackLimit >= __HeapBase, "DTCM overflow: data/bss/heap collide with stack") +ASSERT(__text_end__ <= ORIGIN(SRAM) + LENGTH(SRAM), "SRAM overflow") +ASSERT(__StackTop == ORIGIN(DTCM) + LENGTH(DTCM), "Bad stack top") diff --git a/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld b/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld index e2a8e72b23..9d9aab9ca6 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld +++ b/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld @@ -1,180 +1,191 @@ -/* NUCLEO-N657X0-Q RAM-only Secure Linker Script - * - Run from RAM only (no flash LMA) - * - Secure mode only - * - Stack in D-TCM: 0x30000000 .. 0x3003FFFF (MSP at 0x30040000) - * - Hot code in I-TCM: 0x10000000 .. 0x1003FFFF - * - Regular SRAM from 0x34064000 upward - * - First 0x400 bytes of SRAM reserved for .isr_vector +/* + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +/* Cortex-M55 GCC linker script: + * ITCM: 256 KiB @ 0x00000000 + * DTCM: 256 KiB @ 0x30000000 */ ENTRY(Reset_Handler) -/* Memory regions */ MEMORY { - ITCM (rx) : ORIGIN = 0x00000000, LENGTH = 0x00040000 - DTCM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00040000 - VECT (rx) : ORIGIN = 0x34064000, LENGTH = 0x00000400 /* reserved for .isr_vector */ - SRAM (rwx) : ORIGIN = 0x34064400, LENGTH = 0x00FFFC00 /* remainder of SRAM window */ + ITCM (rwx) : ORIGIN = 0x00000000, LENGTH = 256K + DTCM (rwx) : ORIGIN = 0x30000000, LENGTH = 256K } -/* Explicit program headers to avoid orphaned sections */ -PHDRS -{ - itcm_text PT_LOAD FLAGS(5); /* R X */ - sram_rw PT_LOAD FLAGS(7); /* R W X (single segment for simplicity) */ -} +__itcm_start__ = ORIGIN(ITCM); +__itcm_size__ = LENGTH(ITCM); +__dtcm_start__ = ORIGIN(DTCM); +__dtcm_size__ = LENGTH(DTCM); -/* Stack in D-TCM: use top of region */ -PROVIDE(_estack = ORIGIN(DTCM) + LENGTH(DTCM)); -PROVIDE(__StackTop = _estack); -PROVIDE(__initial_sp = _estack); -PROVIDE(__StackLimit = ORIGIN(DTCM)); -PROVIDE(_sstack = __StackLimit); +__stack_size__ = 192K; +__heap_size__ = 0; + +__StackTop = ORIGIN(DTCM) + LENGTH(DTCM); +__StackLimit = __StackTop - __stack_size__; +__HeapBase = ORIGIN(DTCM); +__HeapLimit = __StackLimit; -/* Optional heap/stack minima (not enforced) */ -_Min_Heap_Size = 0x0000; -_Min_Stack_Size = 0x0000; +PROVIDE(__stack = __StackTop); +PROVIDE(_estack = __StackTop); +PROVIDE(__initial_sp = __StackTop); +PROVIDE(_sstack = __StackLimit); -/* Section placement */ SECTIONS { - /* Vector table region (0x34064000..0x340643FF) */ - .isr_vector : + .vectors ORIGIN(ITCM) : { - . = ALIGN(4); KEEP(*(.isr_vector)) - KEEP(*(.isr_vector.*)) - . = ALIGN(4); - } > VECT :sram_rw + KEEP(*(.vectors)) + . = ORIGIN(ITCM) + 0x400; + } > ITCM - /* High-speed code in I-TCM (optional) */ - .itcm_text : - { - . = ALIGN(8); - KEEP(*(.itcm_text)) - *(.itcm_text.*) - *(.text.itcm) - *(.text.itcm.*) - *(.ramfunc.itcm) - *(.ramfunc.itcm.*) - . = ALIGN(8); - } > ITCM :itcm_text - - /* Regular code in SRAM */ .text : { - . = ALIGN(8); - KEEP(*(.text.startup)) + . = ALIGN(4); + __text_start__ = .; + *(.text) *(.text.*) + *(.gnu.linkonce.t.*) + KEEP(*(.init)) KEEP(*(.fini)) - . = ALIGN(8); - } > SRAM :sram_rw - /* CMSE veneers/stubs (secure gateway) must be in executable region */ - .gnu.sgstubs : - { - . = ALIGN(4); - KEEP(*(.gnu.sgstubs)) + *(.rodata) + *(.rodata.*) + *(.gnu.linkonce.r.*) + . = ALIGN(4); - } > SRAM :sram_rw + KEEP(*(.eh_frame*)) + KEEP(*(.ARM.extab* .gnu.linkonce.armextab.*)) - /* Exception unwinding tables */ - .ARM.extab : - { - *(.ARM.extab* .gnu.linkonce.armextab.*) - } > SRAM :sram_rw + . = ALIGN(4); + __exidx_start = .; + KEEP(*(.ARM.exidx* .gnu.linkonce.armexidx.*)) + __exidx_end = .; - __exidx_start = .; - .ARM.exidx : - { - *(.ARM) - *(.ARM.exidx* .gnu.linkonce.armexidx.*) - } > SRAM :sram_rw - __exidx_end = .; + . = ALIGN(4); + __text_end__ = .; + } > ITCM - /* Read-only data */ - .rodata : + .gnu.sgstubs : { . = ALIGN(32); - *(.rodata) - *(.rodata.*) + KEEP(*(.gnu.sgstubs)) + KEEP(*(.gnu.sgstubs.*)) . = ALIGN(32); - } > SRAM :sram_rw + } > ITCM - /* C++ init/fini arrays */ .preinit_array : { - PROVIDE(__preinit_array_start = .); + PROVIDE_HIDDEN(__preinit_array_start = .); KEEP(*(.preinit_array*)) - PROVIDE(__preinit_array_end = .); - } > SRAM :sram_rw + PROVIDE_HIDDEN(__preinit_array_end = .); + } > ITCM .init_array : { - PROVIDE(__init_array_start = .); - KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*))) + PROVIDE_HIDDEN(__init_array_start = .); + KEEP(*(SORT(.init_array.*))) KEEP(*(.init_array*)) - PROVIDE(__init_array_end = .); - } > SRAM :sram_rw + PROVIDE_HIDDEN(__init_array_end = .); + } > ITCM .fini_array : { - PROVIDE(__fini_array_start = .); - KEEP(*(SORT_BY_INIT_PRIORITY(.fini_array.*))) + PROVIDE_HIDDEN(__fini_array_start = .); + KEEP(*(SORT(.fini_array.*))) KEEP(*(.fini_array*)) - PROVIDE(__fini_array_end = .); - } > SRAM :sram_rw + PROVIDE_HIDDEN(__fini_array_end = .); + } > ITCM + + .itcm_probe : + { + . = MAX(., ORIGIN(ITCM) + 0x00010000); + . = ALIGN(32); + KEEP(*(.itcm_probe)) + KEEP(*(.itcm_probe.*)) + . = ALIGN(32); + } > ITCM - /* Writable data (no flash LMA; RAM-run) */ - _sidata = .; - .data : + .cmdline (NOLOAD) : { . = ALIGN(8); + KEEP(*(.cmdline)) + KEEP(*(.cmdline.*)) + . = ALIGN(8); + __itcm_end__ = .; + } > ITCM + + __etext = ALIGN(4); + _etext = __etext; + + .data : AT(__etext) + { + . = ALIGN(4); + __data_start__ = .; _sdata = .; + *(.data) *(.data.*) - *(.data.rel.ro*) - . = ALIGN(8); + *(.gnu.linkonce.d.*) + + . = ALIGN(4); + __data_end__ = .; _edata = .; - } > SRAM :sram_rw + } > DTCM - /* Optional noncacheable (default to SRAM) */ - .noncacheable (NOLOAD) : - { - *(.noncacheable) - *(.noncacheable.*) - } > SRAM :sram_rw + __data_load__ = LOADADDR(.data); + _sidata = __data_load__; - /* Zero-initialized data */ .bss (NOLOAD) : { - . = ALIGN(8); + . = ALIGN(4); + __bss_start__ = .; _sbss = .; + *(.bss) *(.bss.*) + *(.gnu.linkonce.b.*) *(COMMON) - . = ALIGN(8); + + . = ALIGN(4); + __bss_end__ = .; _ebss = .; - } > SRAM :sram_rw - PROVIDE(end = _ebss); + } > DTCM - /* Vendor placeholder (do not use for stack) */ - ._user_heap_stack (NOLOAD) : + .heap (NOLOAD) : { - *(._user_heap_stack*) - } > SRAM :sram_rw + . = ALIGN(8); + __end__ = .; + end = .; + _end = .; + __HeapBase = .; + . += __heap_size__; + . = ALIGN(8); + } > DTCM + + __HeapLimit = __StackLimit; + + .stack __StackLimit (NOLOAD) : + { + . = ALIGN(8); + _sstack = .; + . += __stack_size__; + } > DTCM - /* Discard non-allocated/debug sections */ /DISCARD/ : { - *(.comment) *(.note*) - *(.gnu_debuglink) - *(.gnu.lto_*) - *(.ARM.attributes) - *(.debug*) + *(.comment*) } } + +ASSERT(SIZEOF(.vectors) <= 0x400, "Vector table exceeds 0x400 bytes") +ASSERT(__StackLimit >= __HeapBase, "DTCM overflow: data/bss/heap collide with stack") +ASSERT(__itcm_end__ <= ORIGIN(ITCM) + LENGTH(ITCM), "ITCM overflow") +ASSERT(__StackTop == ORIGIN(DTCM) + LENGTH(DTCM), "Bad stack top") diff --git a/test/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk index f35bb6edc6..3a112debe0 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/platform.mk +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -4,6 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT PLATFORM_PATH:=test/baremetal/platform/nucleo-n657x0-q +BUILD_DIR ?= test/build CROSS_PREFIX=arm-none-eabi- CC=gcc @@ -20,6 +21,7 @@ CFLAGS += \ --sysroot=$(SYSROOT) \ -DDEVICE=nucleo-n657x0-q \ -DSTM32N657xx \ + -DNTESTS_FUNC=1 \ -I$(NUCLEO_N657X0_Q_PATH) \ -I$(NUCLEO_N657X0_Q_PATH)/Inc \ -I$(NUCLEO_N657X0_Q_PATH)/Drivers/STM32N6xx_HAL_Driver/Inc \ @@ -40,9 +42,10 @@ ARCH_FLAGS += \ # TODO(GAP): If the Cube template (or GCC/newlib build) expects softfp, use: # -mfloat-abi=softfp (and keep -mfpu) +SEMIHOST_SPECS := --specs=rdimon.specs + CFLAGS += \ - $(ARCH_FLAGS) \ - --specs=rdimon.specs + $(ARCH_FLAGS) CFLAGS += $(CFLAGS_EXTRA) @@ -70,16 +73,19 @@ LDFLAGS += \ -L. LDFLAGS += \ - --specs=rdimon.specs \ + $(SEMIHOST_SPECS) \ -Wl,--wrap=main \ -ffreestanding \ -T$(LDSCRIPT) \ $(ARCH_FLAGS) +LDLIBS += -lc -lrdimon + # Extra sources to be included in test binaries EXTRA_SOURCES = \ $(PLATFORM_PATH)/src/cmdline.c \ $(PLATFORM_PATH)/src/cmdline_region.c \ + $(PLATFORM_PATH)/src/flexmem_layout_check.c \ $(PLATFORM_PATH)/src/semihosting_syscall.c \ $(NUCLEO_N657X0_Q_PATH)/clock_config.c \ $(NUCLEO_N657X0_Q_PATH)/system_stm32n6xx.c \ @@ -102,3 +108,31 @@ EXTRA_SOURCES_CFLAGS = -Wno-error -Wno-conversion -Wno-sign-conversion -Wno-unus EXTRA_SOURCES := $(filter-out %/integration_argv.c,$(EXTRA_SOURCES)) EXEC_WRAPPER := $(realpath $(PLATFORM_PATH)/exec_wrapper.py) + +FLEXMEM_CONFIG_ELF ?= $(BUILD_DIR)/nucleo-n657x0-q/flexmem_config.elf +FLEXMEM_CONFIG_LDSCRIPT := $(PLATFORM_PATH)/linker/flexmem_config_default.ld +FLEXMEM_CONFIG_SOURCES := \ + $(PLATFORM_PATH)/src/flexmem_config.c \ + $(NUCLEO_N657X0_Q_PATH)/system_stm32n6xx.c \ + $(STARTUP) + +.PHONY: flexmem_config run_flexmem_config run_flexmem_test + +flexmem_config: $(FLEXMEM_CONFIG_ELF) + +$(FLEXMEM_CONFIG_ELF): $(FLEXMEM_CONFIG_SOURCES) $(FLEXMEM_CONFIG_LDSCRIPT) + $(Q)echo " LD $@" + $(Q)[ -d $(@D) ] || mkdir -p $(@D) + $(Q)$(CC) $(CFLAGS) $(EXTRA_SOURCES_CFLAGS) \ + -ffreestanding \ + -Wl,--gc-sections -Wl,--no-warn-rwx-segments \ + $(SEMIHOST_SPECS) \ + -T$(FLEXMEM_CONFIG_LDSCRIPT) $(ARCH_FLAGS) \ + -o $@ $(FLEXMEM_CONFIG_SOURCES) -lc -lrdimon + +run_flexmem_config: flexmem_config + $(Q)python3 $(PLATFORM_PATH)/flexmem_configure.py $(FLEXMEM_CONFIG_ELF) + +run_flexmem_test: flexmem_config func_512 + $(Q)python3 $(PLATFORM_PATH)/flexmem_configure.py $(FLEXMEM_CONFIG_ELF) + $(Q)python3 $(PLATFORM_PATH)/run_test_after_flexmem.py $(MLKEM512_DIR)/bin/test_mlkem512 diff --git a/test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py b/test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py new file mode 100755 index 0000000000..4a0c397b0f --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +import os +import sys + + +def main(): + here = os.path.dirname(os.path.abspath(__file__)) + wrapper = os.path.join(here, "exec_wrapper.py") + os.execv(sys.executable, [sys.executable, wrapper] + sys.argv[1:]) + + +if __name__ == "__main__": + main() diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c index 11107add05..ea71754f08 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c @@ -11,6 +11,7 @@ #include "stm32n6xx_hal.h" #include "stm32n6xx_it.h" #include "main.h" +#include "semihosting_syscall.h" typedef struct cmdline_s { int argc; @@ -24,24 +25,55 @@ typedef struct cmdline_s { /* Provided by cmdline_region.c */ extern unsigned char mlk_cmdline_block[]; +void nucleo_flexmem_layout_check(void); + + +extern unsigned char _ebss[]; +extern unsigned char __StackLimit[]; +extern unsigned char _estack[]; + +__attribute__((noinline)) +static void nucleo_init_dtcm_ecc(void) { + uintptr_t sp; + __asm__ volatile("mov %0, sp" : "=r"(sp)); + sp = (sp - 64U) & ~(uintptr_t)31U; + uintptr_t heap_start = ((uintptr_t)_ebss + 31U) & ~(uintptr_t)31U; + uintptr_t heap_end = (uintptr_t)__StackLimit & ~(uintptr_t)31U; + if (heap_start < heap_end) { + for (volatile uint32_t *ptr = (volatile uint32_t *)heap_start; + (uintptr_t)ptr < heap_end; ptr++) { + *ptr = 0; + } + } + + uintptr_t stack_start = ((uintptr_t)__StackLimit + 31U) & ~(uintptr_t)31U; + uintptr_t stack_end = sp; + if (stack_end > (uintptr_t)_estack) { + stack_end = (uintptr_t)_estack; + } + for (volatile uint32_t *ptr = (volatile uint32_t *)stack_start; + (uintptr_t)ptr < stack_end; ptr++) { + *ptr = 0; + } + __DSB(); +} /* Provide a prototype for the real main that the C library expects. */ extern int __real_main(int argc, char *argv[]); int __wrap_main(int unused_argc, char *unused_argv[]); -#ifdef SEMIHOSTING -#include "semihosting_syscall.h" static void semihosting_exit_with_rc(int rc) { - // Print sentinel for the exec_wrapper to detect and propagate exit code - printf("[[MLKEM-EXIT:%d]]\n",rc); + if (rc == 0) { + printf("[[MLKEM-EXIT:0]]\n"); + } else { + printf("[[MLKEM-EXIT:1]]\n"); + } fflush(stdout); - // Try basic semihost exit (ST-LINK may or may not support it). If unsupported, - // gdbserver may report an error; wrapper already captured the code. - while(1) ; + __BKPT(0); + while (1) { + __WFI(); + } } -#else -static void semihosting_exit_with_rc(int rc) { (void)rc; } -#endif void Error_Handler(void) { HardFault_Handler(); @@ -50,14 +82,16 @@ void Error_Handler(void) { /* Wrap main: build argc/argv from cmdline and forward to __real_main. */ int __wrap_main(int unused_argc, char *unused_argv[]) { (void)unused_argc; (void)unused_argv; + nucleo_init_dtcm_ecc(); + nucleo_stdio_init(); SCB_EnableICache(); SCB_EnableDCache(); HAL_Init(); SystemClock_Config(); + nucleo_flexmem_layout_check(); cmdline_t *cmdline = (cmdline_t *)&mlk_cmdline_block; int rc = __real_main(cmdline->argc, cmdline->argv); semihosting_exit_with_rc(rc); return rc; } - diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c index 8c53062257..6f7481c126 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c @@ -5,6 +5,6 @@ * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT */ #include -/* 64 KiB command-line buffer in BSS, 8-byte aligned */ -__attribute__((aligned(8), used)) +/* Argv block in I-TCM; populated by GDB after C startup reaches __wrap_main. */ +__attribute__((aligned(8), used, section(".cmdline"))) unsigned char mlk_cmdline_block[64 * 1024]; diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_config.c b/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_config.c new file mode 100644 index 0000000000..a2f4cc5a94 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_config.c @@ -0,0 +1,50 @@ +/* + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ +#include + +#include "stm32n6xx.h" + +#ifndef SYSCFG_CM55TCMCR_CFGITCMSZ_Pos +#error "STM32N6 CMSIS header is missing SYSCFG_CM55TCMCR_CFGITCMSZ_Pos" +#endif +#ifndef SYSCFG_CM55TCMCR_CFGDTCMSZ_Pos +#error "STM32N6 CMSIS header is missing SYSCFG_CM55TCMCR_CFGDTCMSZ_Pos" +#endif +#ifndef RCC_APB4ENSR2_SYSCFGENS +#error "STM32N6 CMSIS header is missing RCC_APB4ENSR2_SYSCFGENS" +#endif + +#define FLEXMEM_TCM_SIZE_256K 9UL + +int main(void) +{ + const uint32_t flexmem_value = + (FLEXMEM_TCM_SIZE_256K << SYSCFG_CM55TCMCR_CFGITCMSZ_Pos) | + (FLEXMEM_TCM_SIZE_256K << SYSCFG_CM55TCMCR_CFGDTCMSZ_Pos); + + __disable_irq(); + + RCC->APB4ENSR2 = RCC_APB4ENSR2_SYSCFGENS; + (void)RCC->APB4ENR2; + + SYSCFG->CM55TCMCR = (SYSCFG->CM55TCMCR & + ~(SYSCFG_CM55TCMCR_CFGITCMSZ_Msk | + SYSCFG_CM55TCMCR_CFGDTCMSZ_Msk)) | + flexmem_value; + (void)SYSCFG->CM55TCMCR; + +#ifdef SYSCFG_CM55RSTCR_CORE_RESET_TYPE + SYSCFG->CM55RSTCR |= SYSCFG_CM55RSTCR_CORE_RESET_TYPE; + (void)SYSCFG->CM55RSTCR; +#endif + + __DSB(); + __ISB(); + + __BKPT(0); + for (;;) { + __WFI(); + } +} diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_layout_check.c b/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_layout_check.c new file mode 100644 index 0000000000..d118216f3b --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_layout_check.c @@ -0,0 +1,40 @@ +/* + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ +#include + +__attribute__((section(".itcm_probe"), noinline, used)) +static uint32_t nucleo_itcm_above_default_probe(uint32_t x) +{ + return x ^ 0xa5a55a5aUL; +} + +__attribute__((noinline, used)) +void nucleo_layout_fail(uint32_t code) +{ + __asm__ volatile("mov r0, %0\n" + "bkpt 0\n" : : "r"(code) : "r0", "memory"); + for (;;) { + } +} + +void nucleo_flexmem_layout_check(void) +{ + volatile uint32_t *dtcm_above_default = (volatile uint32_t *)0x30020000UL; + const uint32_t saved = *dtcm_above_default; + uint32_t probe; + + *dtcm_above_default = 0x5aa55aa5UL; + __asm__ volatile("dsb sy" ::: "memory"); + probe = *dtcm_above_default; + *dtcm_above_default = saved; + + if (probe != 0x5aa55aa5UL) { + nucleo_layout_fail(1); + } + + if (nucleo_itcm_above_default_probe(0x11223344UL) != 0xb487691eUL) { + nucleo_layout_fail(2); + } +} diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/libfns.c b/test/baremetal/platform/nucleo-n657x0-q/src/libfns.c index fc1b5bb5f6..45cf78b841 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/libfns.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/libfns.c @@ -5,13 +5,7 @@ * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT */ #include -#include #include -/* Public semihosting API */ -#include "semihosting_syscall.h" -/* Semihosting definitions */ -static const uint32_t REPORT_EXCEPTION = 0x18; -static const uint32_t ApplicationExit = 0x20026; int __wrap__close(int fd); int __wrap__fstat(int fd, struct stat *buf); @@ -24,7 +18,6 @@ int __wrap__kill(int pid, int sig); int __wrap__close(int fd) { (void)fd; - semihosting_syscall(REPORT_EXCEPTION, ApplicationExit); return 0; } diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/reset_dtcm_init.S b/test/baremetal/platform/nucleo-n657x0-q/src/reset_dtcm_init.S deleted file mode 100644 index 6356d4bf35..0000000000 --- a/test/baremetal/platform/nucleo-n657x0-q/src/reset_dtcm_init.S +++ /dev/null @@ -1,80 +0,0 @@ -/* - * DTCM sizing + MSP/MSPLIM bootstrap for NUCLEO-N657X0-Q (Cortex-M55) - * - Checks SYSCFG_CM55TCMCR lower 8 bits; if not 0x99, writes 0x99 and resets via AIRCR - * - Else sets MSP to 0x30040000 and MSPLIM to 0x30000000 before any C stack usage - * - Injected into Reset_Handler via --wrap=Reset_Handler - */ - - .syntax unified - .thumb - - .global __wrap_SystemInit - .type __wrap_SystemInit, %function - .extern __real_SystemInit - -/* Constants */ -#define SYSCFG_BASE 0x46008000 -#define SYSCFG_CM55TCMCR 0x00000008 /* offset from SYSCFG_BASE */ -/* RCC APB4ENSR2 address and bit for SYSCFGENS (enable) from device header: - * RCC_BASE_S = PERIPH_BASE_S(0x50000000) + AHB4PERIPH(0x06020000) + 0x8000 = 0x56028000 - * APB4ENSR2 offset = 0x0A78 => 0x56028A78 - * SYSCFGENS bit = 1<<0 - */ -#define RCC_APB4ENSR2_ADDR 0x46028A78 - -#define SCB_AIRCR_ADDR 0xE000ED0C -#define AIRCR_SYSRESET 0x05FA0004 /* VECTKEY(0x05FA)<<16 | SYSRESETREQ */ - -#define DTCM_BASE 0x30000000 -#define DTCM_SIZE_256K 0x00040000 - -/* - * Wrap SystemInit to apply/check TCM sizing and set MSP/MSPLIM into DTCM - * before any C stack use within SystemInit or later code. - */ -__wrap_SystemInit: - /* Ensure SYSCFG clock is enabled before touching TCMCR */ - ldr r0, =RCC_APB4ENSR2_ADDR - ldr r1, [r0] - orr r1, r1, #1 - str r1, [r0] - - /* Read SYSCFG_CM55TCMCR */ - ldr r0, =SYSCFG_BASE - ldr r1, =SYSCFG_CM55TCMCR - add r0, r0, r1 - ldr r1, [r0] - /* Mask lower 8 bits and compare to 0x99 */ - movs r0, #0xFF - ands r1, r1, r0 - cmp r1, #0x99 - beq 2f - - /* Not 0x99: write 0x99 into lower byte of SYSCFG_CM55TCMCR */ - /* Reload address */ - ldr r0, =SYSCFG_BASE - ldr r1, =SYSCFG_CM55TCMCR - add r0, r0, r1 - /* Read-modify-write to update only the low byte */ - ldr r1, [r0] - bic r1, r1, #0xFF - orr r1, r1, #0x99 - str r1, [r0] - - /* Trigger system reset via AIRCR */ - ldr r0, =SCB_AIRCR_ADDR - ldr r1, =AIRCR_SYSRESET - str r1, [r0] -1: b 1b - -2: - /* Correct sizing: set MSP = DTCM_BASE + 256K, MSPLIM = DTCM_BASE */ - ldr r0, =DTCM_BASE - ldr r1, =DTCM_SIZE_256K - add r0, r0, r1 - msr MSP, r0 - ldr r0, =DTCM_BASE - msr MSPLIM, r0 - - /* Tail-call real SystemInit */ - b __real_SystemInit diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c index 2829029769..41de145d61 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c @@ -4,24 +4,50 @@ * Copyright (c) Arm Ltd. * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT */ -#include +#include #include -/* Public semihosting API */ -#include "semihosting_syscall.h" -__attribute__((always_inline)) static inline void __semihosting_call(int32_t opnr, int32_t param) { - register int32_t r0 __asm__("r0") = opnr; - register int32_t r1 __asm__("r1") = param; - __asm__ __volatile__("bkpt 0xAB" : "+r"(r0) : "r"(r1) : "memory"); -} -void semihosting_syscall(int32_t opnr, int32_t param) { - __semihosting_call(opnr, param); -} +#include + +#define NUCLEO_STDOUT_CAPTURE_SIZE 32768U + +__attribute__((used, section(".bss.nucleo_stdout_capture"))) +volatile uint8_t nucleo_stdout_capture[NUCLEO_STDOUT_CAPTURE_SIZE]; + +__attribute__((used)) +volatile uint32_t nucleo_stdout_capture_len; -// Provided by --specs=rdimon.specs -extern void initialise_monitor_handles(void); +__attribute__((used)) +volatile uint32_t nucleo_stdout_capture_truncated; + +int _write(int fd, char *src, int length) { + (void)fd; + + if (src == NULL || length < 0) { + errno = EINVAL; + return -1; + } + + uint32_t offset = nucleo_stdout_capture_len; + if (offset < NUCLEO_STDOUT_CAPTURE_SIZE) { + uint32_t available = NUCLEO_STDOUT_CAPTURE_SIZE - offset; + uint32_t written = (uint32_t)length; + if (written > available) { + written = available; + nucleo_stdout_capture_truncated = 1; + } + + for (uint32_t idx = 0; idx < written; idx++) { + nucleo_stdout_capture[offset + idx] = (uint8_t)src[idx]; + } + nucleo_stdout_capture_len = offset + written; + } else if (length > 0) { + nucleo_stdout_capture_truncated = 1; + } + + return length; +} -__attribute__((constructor)) -static void mlkem_semihost_init(void) { - initialise_monitor_handles(); - fflush(stdout); +void nucleo_stdio_init(void) { + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); } diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h index 0ecca1e700..10965258f7 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h @@ -7,9 +7,6 @@ #ifndef MLKEM_NATIVE_SEMIHOSTING_SYSCALL_H #define MLKEM_NATIVE_SEMIHOSTING_SYSCALL_H -#include - -void semihosting_syscall(int32_t opnr, int32_t param); +void nucleo_stdio_init(void); #endif /* MLKEM_NATIVE_SEMIHOSTING_SYSCALL_H */ - From af2737ebecab2ac4419a7b8cb308f4b09fa3ef51 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Fri, 1 May 2026 11:17:52 +0100 Subject: [PATCH 06/20] KAT-768 and KAT-512 now working Signed-off-by: Brendan Moran --- .../platform/nucleo-n657x0-q/README.md | 4 ++- .../platform/nucleo-n657x0-q/exec_wrapper.py | 26 ++++++++++++------- .../nucleo-n657x0-q/linker/ram_secure.ld | 12 +++++++++ .../platform/nucleo-n657x0-q/src/cmdline.c | 3 +++ .../nucleo-n657x0-q/src/semihosting_syscall.c | 4 +-- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md index b578ab6446..1eae7b2aab 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/README.md +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -107,7 +107,9 @@ The GDB command sequence intentionally loads the argv blob after C startup reach `linker/ram_secure.ld` is used by tests after FLEXMEM reset has applied: - vector table and executable sections: expanded 256 KiB ITCM at `0x00000000` -- `.data`, `.bss`, argv block: expanded 256 KiB DTCM at `0x30000000` +- `.data`, `.bss`: expanded 256 KiB DTCM at `0x30000000` +- argv block: expanded 256 KiB ITCM at `0x00000000` +- stdout capture: AXI SRAM at `0x34080000` - stack: top 192 KiB of DTCM, with `_estack = 0x30040000` and `__StackLimit = 0x30010000` - no flash memory regions or flash LMAs diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index b034c1e319..a5b245fd27 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -277,7 +277,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): stdout_capture_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture") stdout_capture_len_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture_len") stdout_capture_truncated_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture_truncated") - stdout_capture_size = int(os.environ.get("NUCLEO_STDOUT_CAPTURE_SIZE", "32768")) + stdout_capture_size = int(os.environ.get("NUCLEO_STDOUT_CAPTURE_SIZE", str(1536 * 1024))) # Allow override of base address via env (hex string) arg_block_addr_env = os.environ.get("ARG_BLOCK_ADDR") base_addr = None @@ -437,7 +437,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): semihost_stop = threading.Event() semihost_thr = None semihost_exit = threading.Event() - shared = {"exit_code": None} + shared = {"exit_code": None, "stdout_streamed": False} def _semihost_reader(sock: socket.socket): buf = b"" @@ -468,11 +468,12 @@ def _semihost_reader(sock: socket.socket): if VERBOSE: LOG.debug("[semi] %s", text) else: - # Log semihost lines; prefix only in verbose mode. if VERBOSE: LOG.debug("[semi] %s", text) else: - LOG.info("%s", text) + sys.stdout.buffer.write(line + b"\n") + sys.stdout.buffer.flush() + shared["stdout_streamed"] = True except socket.timeout: continue finally: @@ -530,7 +531,13 @@ def _semihost_reader(sock: socket.socket): f"jump {reset_handler_jump}", (f"restore {argv_bin} binary {arg_block_addr}" if arg_block_addr else f"restore {argv_bin} binary &{arg_block_sym}"), "break HardFault_Handler", + "commands", + " echo [[NUCLEO-HARDFAULT]]\\n", + "end", "break nucleo_layout_fail", + "commands", + " echo [[NUCLEO-LAYOUT-FAIL]]\\n", + "end", "continue", ] if stdout_capture_addr and stdout_capture_len_addr: @@ -641,7 +648,7 @@ def _semihost_reader(sock: socket.socket): captured = capture_file.read() captured_text = captured.decode("utf-8", errors="replace") captured_lines = [] - for capture_line in captured_text.splitlines(): + for capture_line in captured_text.splitlines(keepends=True): stripped_line = capture_line.strip() if stripped_line.startswith("[[MLKEM-EXIT:") and stripped_line.endswith("]]"): try: @@ -650,8 +657,9 @@ def _semihost_reader(sock: socket.socket): shared["exit_code"] = 1 continue captured_lines.append(capture_line) - if captured_lines: - log_output("\n".join(captured_lines), logging.INFO) + if captured_lines and not shared.get("stdout_streamed"): + sys.stdout.write("".join(captured_lines)) + sys.stdout.flush() except Exception as exc: info(f"[exec_wrapper] failed to read stdout capture: {exc}") @@ -661,12 +669,12 @@ def _semihost_reader(sock: socket.socket): if shared.get("exit_code") is not None: return int(shared["exit_code"]) if isinstance(shared["exit_code"], int) else 1 - if "nucleo_layout_fail" in gdb_text: + if "[[NUCLEO-LAYOUT-FAIL]]" in gdb_text: err("FAIL!") err("FLEXMEM layout check failed on target") return 1 - if "HardFault_Handler" in gdb_text: + if "[[NUCLEO-HARDFAULT]]" in gdb_text: err("FAIL!") err("Target entered HardFault_Handler") return 1 diff --git a/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld b/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld index 9d9aab9ca6..35a01a932e 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld +++ b/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld @@ -14,6 +14,7 @@ MEMORY { ITCM (rwx) : ORIGIN = 0x00000000, LENGTH = 256K DTCM (rwx) : ORIGIN = 0x30000000, LENGTH = 256K + AXISRAM (rwx) : ORIGIN = 0x34080000, LENGTH = 1536K } __itcm_start__ = ORIGIN(ITCM); @@ -158,6 +159,16 @@ SECTIONS _ebss = .; } > DTCM + .stdout_capture (NOLOAD) : + { + . = ALIGN(32); + __stdout_capture_start__ = .; + KEEP(*(.stdout_capture)) + KEEP(*(.stdout_capture.*)) + . = ALIGN(32); + __stdout_capture_end__ = .; + } > AXISRAM + .heap (NOLOAD) : { . = ALIGN(8); @@ -188,4 +199,5 @@ SECTIONS ASSERT(SIZEOF(.vectors) <= 0x400, "Vector table exceeds 0x400 bytes") ASSERT(__StackLimit >= __HeapBase, "DTCM overflow: data/bss/heap collide with stack") ASSERT(__itcm_end__ <= ORIGIN(ITCM) + LENGTH(ITCM), "ITCM overflow") +ASSERT(__stdout_capture_end__ <= ORIGIN(AXISRAM) + LENGTH(AXISRAM), "AXISRAM overflow") ASSERT(__StackTop == ORIGIN(DTCM) + LENGTH(DTCM), "Bad stack top") diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c index ea71754f08..33bb40e434 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c @@ -69,6 +69,9 @@ static void semihosting_exit_with_rc(int rc) { printf("[[MLKEM-EXIT:1]]\n"); } fflush(stdout); + SCB_CleanDCache(); + __DSB(); + __ISB(); __BKPT(0); while (1) { __WFI(); diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c index 41de145d61..598ca6b346 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c @@ -8,9 +8,9 @@ #include #include -#define NUCLEO_STDOUT_CAPTURE_SIZE 32768U +#define NUCLEO_STDOUT_CAPTURE_SIZE (1536U * 1024U) -__attribute__((used, section(".bss.nucleo_stdout_capture"))) +__attribute__((aligned(32), used, section(".stdout_capture"))) volatile uint8_t nucleo_stdout_capture[NUCLEO_STDOUT_CAPTURE_SIZE]; __attribute__((used)) From bf8ac36ec30ffdcc770307f53da2d05a7ce0a745 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Tue, 5 May 2026 11:59:57 +0100 Subject: [PATCH 07/20] Working nucleo tests Signed-off-by: Brendan Moran --- .../platform/nucleo-n657x0-q/README.md | 2 + .../platform/nucleo-n657x0-q/exec_wrapper.py | 264 ++++++++++++++++-- .../nucleo-n657x0-q/linker/ram_secure.ld | 2 +- .../platform/nucleo-n657x0-q/make_argv_bin.py | 7 +- .../platform/nucleo-n657x0-q/platform.mk | 15 + .../nucleo-n657x0-q/src/semihosting_syscall.c | 50 +++- 6 files changed, 313 insertions(+), 27 deletions(-) diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md index 1eae7b2aab..2e08eadab3 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/README.md +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -84,6 +84,8 @@ python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py \ `run_test_after_flexmem.py` delegates to `exec_wrapper.py`, which starts ST-LINK GDB server, loads the ELF into RAM, injects argv into `mlk_cmdline_block`, runs from `Reset_Handler`, dumps the target stdout capture buffer, and returns the `[[MLKEM-EXIT:]]` code. +The KAT and ACVP run targets depend on `run_flexmem_config`, so `make run_kat` and `make run_acvp` restore the expanded FLEXMEM layout before loading the RAM-resident images. `exec_wrapper.py` retries once after pre-output GDB transport failures; set `GDB_RUN_ATTEMPTS=` to adjust this. If the target enters `HardFault_Handler`, the wrapper re-runs the FLEXMEM config binary and retries once; set `GDB_HARDFAULT_RECOVERY_ATTEMPTS=` to adjust this. + ## Argv Blob Loading Target arguments are passed through a RAM blob rather than through semihosting or debugger command-line support. The test image links `cmdline_region.c`, which reserves the 64 KiB `mlk_cmdline_block` symbol in ITCM. `exec_wrapper.py` resolves that symbol with `arm-none-eabi-nm`, falling back to `readelf -s`; `ARG_BLOCK_SYMBOL` can select a different symbol and `ARG_BLOCK_ADDR` can override the resolved address. diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index a5b245fd27..799a79d8a7 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -6,6 +6,7 @@ import logging import os +import re import shlex import shutil import struct as st @@ -19,7 +20,13 @@ VERBOSE = False +STDOUT_BYTES_EMITTED = 0 +TARGET_FAILURE = False +TARGET_FAILURE_KIND = "" +SUPPRESS_RETRYABLE_DIAGNOSTICS = False +LAST_FAULT_DIAGNOSTICS = "" LOG = logging.getLogger(__name__) +ARGV_BLOCK_SIZE = 64 * 1024 def configure_logging(): @@ -46,6 +53,72 @@ def info(msg, **kwargs): LOG.debug("%s", msg) +def _decode_cfsr(cfsr: int): + bits = [ + (0, "IACCVIOL"), + (1, "DACCVIOL"), + (3, "MUNSTKERR"), + (4, "MSTKERR"), + (5, "MLSPERR"), + (7, "MMARVALID"), + (8, "IBUSERR"), + (9, "PRECISERR"), + (10, "IMPRECISERR"), + (11, "UNSTKERR"), + (12, "STKERR"), + (13, "LSPERR"), + (15, "BFARVALID"), + (16, "UNDEFINSTR"), + (17, "INVSTATE"), + (18, "INVPC"), + (19, "NOCP"), + (24, "UNALIGNED"), + (25, "DIVBYZERO"), + ] + return [name for bit, name in bits if cfsr & (1 << bit)] + + +def _decode_hfsr(hfsr: int): + bits = [(1, "VECTTBL"), (30, "FORCED"), (31, "DEBUGEVT")] + return [name for bit, name in bits if hfsr & (1 << bit)] + + +def _fault_info_from_gdb(gdb_text: str) -> str: + values = {} + for name, value in re.findall(r"^(CFSR|HFSR|DFSR|MMFAR|BFAR|AFSR|SHCSR|CCR|MSP|PSP|LR|PC)=0x([0-9a-fA-F]+)$", gdb_text, re.MULTILINE): + values[name] = int(value, 16) + + if not values: + return "" + + lines = ["Fault registers:"] + for name in ("CFSR", "HFSR", "DFSR", "MMFAR", "BFAR", "AFSR", "SHCSR", "CCR", "MSP", "PSP", "LR", "PC"): + if name in values: + lines.append(f" {name}=0x{values[name]:08x}") + + cfsr_bits = _decode_cfsr(values.get("CFSR", 0)) + hfsr_bits = _decode_hfsr(values.get("HFSR", 0)) + if cfsr_bits: + lines.append(" CFSR bits: " + ", ".join(cfsr_bits)) + if hfsr_bits: + lines.append(" HFSR bits: " + ", ".join(hfsr_bits)) + + stacked = re.search(r"^STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\s*\n((?:0x[0-9a-fA-F]+:\s+.*\n?)?)", gdb_text, re.MULTILINE) + if stacked: + stack_lines = [line.strip() for line in stacked.group(1).splitlines() if line.strip()] + if stack_lines: + lines.append(" stacked frame dump:") + lines.extend(f" {line}" for line in stack_lines) + + return "\n".join(lines) + + +def _gdb_observed_hardfault(gdb_text: str) -> bool: + return "[[NUCLEO-HARDFAULT]]" in gdb_text or re.search( + r"^HardFault_Handler \(\)", gdb_text, re.MULTILINE + ) is not None + + def pack_cmdline(args, base_addr): """ Pack argv for the STM32 baremetal target: @@ -64,7 +137,10 @@ def pack_cmdline(args, base_addr): ptrs.append(base_addr + header_sz + cur) strings += b cur += len(b) - return st.pack(" ARGV_BLOCK_SIZE: + raise ValueError(f"argv blob is {len(blob)} bytes, exceeds {ARGV_BLOCK_SIZE}-byte block") + return blob + bytes(ARGV_BLOCK_SIZE - len(blob)) def run(cmd, **kwargs): @@ -160,8 +236,55 @@ def _reset_target(st_cubeprog: str, st_clt_root: str, st_speed: str, st_serial: return _run_cubeprogrammer(cli, _cubeprogrammer_connect_args(st_speed, st_serial, st_apid), ["-rst"]) -def main(): +def _default_flexmem_config_elf() -> str: + platform_dir = os.path.dirname(os.path.abspath(__file__)) + repo_root = os.path.abspath(os.path.join(platform_dir, "..", "..", "..", "..")) + return os.path.join(repo_root, "test", "build", "nucleo-n657x0-q", "flexmem_config.elf") + + +def _recover_after_hardfault() -> bool: + platform_dir = os.path.dirname(os.path.abspath(__file__)) + configure_script = os.path.join(platform_dir, "flexmem_configure.py") + config_elf = os.environ.get("FLEXMEM_CONFIG_ELF", _default_flexmem_config_elf()) + + if not os.path.exists(configure_script): + err(f"FLEXMEM configure script not found: {configure_script}") + return False + if not os.path.exists(config_elf): + err(f"FLEXMEM config ELF not found: {config_elf}") + return False + + info("[exec_wrapper] recovering from HardFault: re-running FLEXMEM config") + recovery_env = os.environ.copy() + recovery_env.setdefault("STLINK_CONNECT_MODE", "UR") + cp = run( + [sys.executable, configure_script, config_elf], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + env=recovery_env, + ) + if cp.returncode != 0: + err("FLEXMEM reconfiguration after HardFault failed") + log_output(cp.stdout, logging.ERROR) + return False + if VERBOSE: + log_output(cp.stdout, logging.DEBUG) + return True + + +def _run_once(): global VERBOSE + global STDOUT_BYTES_EMITTED + global TARGET_FAILURE + global TARGET_FAILURE_KIND + global SUPPRESS_RETRYABLE_DIAGNOSTICS + global LAST_FAULT_DIAGNOSTICS + + STDOUT_BYTES_EMITTED = 0 + TARGET_FAILURE = False + TARGET_FAILURE_KIND = "" + LAST_FAULT_DIAGNOSTICS = "" argv = sys.argv[1:] # Minimal flag parsing for wrapper flags (remove them from argv) @@ -297,7 +420,11 @@ def _resolve_symbol_addr(elf_path: str, sym: str): err("- Ensure symbols are present in ELF, or set ARG_BLOCK_ADDR to the base address (hex).") return 2 - blob = pack_cmdline(args, base_addr) + try: + blob = pack_cmdline(args, base_addr) + except ValueError as exc: + err(str(exc)) + return 2 with tempfile.TemporaryDirectory() as td: argv_bin = os.path.join(td, "argv.bin") @@ -440,6 +567,8 @@ def _resolve_symbol_addr(elf_path: str, sym: str): shared = {"exit_code": None, "stdout_streamed": False} def _semihost_reader(sock: socket.socket): + global STDOUT_BYTES_EMITTED + buf = b"" try: while not semihost_stop.is_set(): @@ -473,9 +602,12 @@ def _semihost_reader(sock: socket.socket): else: sys.stdout.buffer.write(line + b"\n") sys.stdout.buffer.flush() + STDOUT_BYTES_EMITTED += len(line) + 1 shared["stdout_streamed"] = True except socket.timeout: continue + except OSError: + break finally: try: sock.close() @@ -497,8 +629,8 @@ def _semihost_reader(sock: socket.socket): if stp.poll() is not None: # Server exited early – surface a helpful message out_rem = stp.stdout.read() if stp.stdout else "" - if out_rem and VERBOSE: - log_output(out_rem, logging.DEBUG) + if out_rem and not SUPPRESS_RETRYABLE_DIAGNOSTICS: + log_output(out_rem, logging.DEBUG if VERBOSE else logging.ERROR) merged = out_rem low = merged.lower() if "firmware upgrade" in low or "upgrade required" in low: @@ -559,6 +691,48 @@ def _semihost_reader(sock: socket.socket): gdb_lines += [ "info registers", "x/4wx $sp", + "echo CFSR=", + "output/x *(unsigned int *)0xE000ED28", + "echo \\n", + "echo HFSR=", + "output/x *(unsigned int *)0xE000ED2C", + "echo \\n", + "echo DFSR=", + "output/x *(unsigned int *)0xE000ED30", + "echo \\n", + "echo MMFAR=", + "output/x *(unsigned int *)0xE000ED34", + "echo \\n", + "echo BFAR=", + "output/x *(unsigned int *)0xE000ED38", + "echo \\n", + "echo AFSR=", + "output/x *(unsigned int *)0xE000ED3C", + "echo \\n", + "echo SHCSR=", + "output/x *(unsigned int *)0xE000ED24", + "echo \\n", + "echo CCR=", + "output/x *(unsigned int *)0xE000ED14", + "echo \\n", + "echo MSP=", + "output/x $msp", + "echo \\n", + "echo PSP=", + "output/x $psp", + "echo \\n", + "echo LR=", + "output/x $lr", + "echo \\n", + "echo PC=", + "output/x $pc", + "echo \\n", + "echo STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\\n", + "if ($lr & 4)", + " x/8wx $psp", + "else", + " x/8wx $msp", + "end", "x/4wx 0xE000ED28", "x/wx 0xE000ED38", ] @@ -611,8 +785,9 @@ def _semihost_reader(sock: socket.socket): if gdbp.poll() is not None: break if gdb_deadline is not None and time.time() > gdb_deadline: - err("FAIL!") - err(f"gdb batch timed out after {gdb_run_timeout:.0f}s") + if not SUPPRESS_RETRYABLE_DIAGNOSTICS: + err("FAIL!") + err(f"gdb batch timed out after {gdb_run_timeout:.0f}s") try: gdbp.terminate() gdbp.wait(timeout=1.0) @@ -623,9 +798,9 @@ def _semihost_reader(sock: socket.socket): pass try: out, errout = gdbp.communicate(timeout=1.0) - if out: + if out and not SUPPRESS_RETRYABLE_DIAGNOSTICS: log_output(out, logging.ERROR) - if errout: + if errout and not SUPPRESS_RETRYABLE_DIAGNOSTICS: err(errout, end="") except Exception: pass @@ -639,6 +814,9 @@ def _semihost_reader(sock: socket.socket): err(errout, end="") gdb_text = f"{out}\n{errout}" + layout_failed = "[[NUCLEO-LAYOUT-FAIL]]" in gdb_text + hardfaulted = _gdb_observed_hardfault(gdb_text) + target_failed = layout_failed or hardfaulted captured_text = "" if os.path.exists(stdout_capture_bin): @@ -657,9 +835,11 @@ def _semihost_reader(sock: socket.socket): shared["exit_code"] = 1 continue captured_lines.append(capture_line) - if captured_lines and not shared.get("stdout_streamed"): - sys.stdout.write("".join(captured_lines)) + if captured_lines and not shared.get("stdout_streamed") and not target_failed: + captured_output = "".join(captured_lines) + sys.stdout.write(captured_output) sys.stdout.flush() + STDOUT_BYTES_EMITTED += len(captured_output.encode("utf-8")) except Exception as exc: info(f"[exec_wrapper] failed to read stdout capture: {exc}") @@ -669,14 +849,23 @@ def _semihost_reader(sock: socket.socket): if shared.get("exit_code") is not None: return int(shared["exit_code"]) if isinstance(shared["exit_code"], int) else 1 - if "[[NUCLEO-LAYOUT-FAIL]]" in gdb_text: + if layout_failed: + TARGET_FAILURE = True + TARGET_FAILURE_KIND = "layout" err("FAIL!") err("FLEXMEM layout check failed on target") return 1 - if "[[NUCLEO-HARDFAULT]]" in gdb_text: - err("FAIL!") - err("Target entered HardFault_Handler") + if hardfaulted: + TARGET_FAILURE = True + TARGET_FAILURE_KIND = "hardfault" + fault_info = _fault_info_from_gdb(gdb_text) + LAST_FAULT_DIAGNOSTICS = fault_info + if not SUPPRESS_RETRYABLE_DIAGNOSTICS: + err("FAIL!") + err("Target entered HardFault_Handler") + if fault_info: + err(fault_info) return 1 if "Program received signal SIGTRAP" in gdb_text: @@ -684,8 +873,13 @@ def _semihost_reader(sock: socket.socket): return 0 if gdbp.returncode != 0: - err("FAIL!") - err(f"gdb batch failed with code {gdbp.returncode}") + if not SUPPRESS_RETRYABLE_DIAGNOSTICS: + err("FAIL!") + err(f"gdb batch failed with code {gdbp.returncode}") + if out and not SUPPRESS_RETRYABLE_DIAGNOSTICS: + log_output(out, logging.ERROR) + if errout and not SUPPRESS_RETRYABLE_DIAGNOSTICS: + log_output(errout, logging.ERROR) return gdbp.returncode return 0 @@ -718,5 +912,41 @@ def _semihost_reader(sock: socket.socket): pass +def main(): + global SUPPRESS_RETRYABLE_DIAGNOSTICS + global LAST_FAULT_DIAGNOSTICS + + attempts = max(1, int(os.environ.get("GDB_RUN_ATTEMPTS", "2"))) + hardfault_attempts = max(0, int(os.environ.get("GDB_HARDFAULT_RECOVERY_ATTEMPTS", "1"))) + transport_retries = 0 + hardfault_recoveries = 0 + last_rc = 1 + + while True: + can_retry_transport = transport_retries < attempts - 1 + can_retry_hardfault = hardfault_recoveries < hardfault_attempts + SUPPRESS_RETRYABLE_DIAGNOSTICS = can_retry_transport or can_retry_hardfault + last_rc = _run_once() + if last_rc == 0: + return 0 + if TARGET_FAILURE_KIND == "hardfault" and can_retry_hardfault: + hardfault_recoveries += 1 + if _recover_after_hardfault(): + if VERBOSE: + err(f"[exec_wrapper] retrying after recovered HardFault ({hardfault_recoveries}/{hardfault_attempts})") + time.sleep(0.5) + continue + if LAST_FAULT_DIAGNOSTICS: + err("HardFault diagnostics from failed run:") + err(LAST_FAULT_DIAGNOSTICS) + return last_rc + if TARGET_FAILURE or STDOUT_BYTES_EMITTED != 0 or not can_retry_transport: + return last_rc + transport_retries += 1 + if VERBOSE: + err(f"[exec_wrapper] retrying after transport failure ({transport_retries}/{attempts - 1})") + time.sleep(0.5) + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld b/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld index 35a01a932e..15339b19ba 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld +++ b/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld @@ -22,7 +22,7 @@ __itcm_size__ = LENGTH(ITCM); __dtcm_start__ = ORIGIN(DTCM); __dtcm_size__ = LENGTH(DTCM); -__stack_size__ = 192K; +__stack_size__ = 240K; __heap_size__ = 0; __StackTop = ORIGIN(DTCM) + LENGTH(DTCM); diff --git a/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py index eafe0d382e..26c2de0e28 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py +++ b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py @@ -7,6 +7,8 @@ import struct as st import sys +ARGV_BLOCK_SIZE = 64 * 1024 + def pack_cmdline(args, base_addr): """ @@ -26,7 +28,10 @@ def pack_cmdline(args, base_addr): ptrs.append(base_addr + header_sz + cur) strings += b cur += len(b) - return st.pack(" ARGV_BLOCK_SIZE: + raise ValueError(f"argv blob is {len(blob)} bytes, exceeds {ARGV_BLOCK_SIZE}-byte block") + return blob + bytes(ARGV_BLOCK_SIZE - len(blob)) def main(argv): diff --git a/test/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk index 3a112debe0..11320ac765 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/platform.mk +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -9,6 +9,19 @@ BUILD_DIR ?= test/build CROSS_PREFIX=arm-none-eabi- CC=gcc +# Use PMU cycle counting by default +CYCLES ?= PMU + +# Short benchmark runs for testing +MLK_BENCHMARK_NWARMUP ?= 1 +MLK_BENCHMARK_NITERATIONS ?= 1 +MLK_BENCHMARK_NTESTS ?= 1 + +CFLAGS += \ + -DMLK_BENCHMARK_NWARMUP=$(MLK_BENCHMARK_NWARMUP) \ + -DMLK_BENCHMARK_NITERATIONS=$(MLK_BENCHMARK_NITERATIONS) \ + -DMLK_BENCHMARK_NTESTS=$(MLK_BENCHMARK_NTESTS) + CFLAGS += \ -O3 -g \ -Wall -Wextra -Wshadow \ @@ -118,6 +131,8 @@ FLEXMEM_CONFIG_SOURCES := \ .PHONY: flexmem_config run_flexmem_config run_flexmem_test +run_kat_512 run_kat_768 run_kat_1024 run_acvp: run_flexmem_config + flexmem_config: $(FLEXMEM_CONFIG_ELF) $(FLEXMEM_CONFIG_ELF): $(FLEXMEM_CONFIG_SOURCES) $(FLEXMEM_CONFIG_LDSCRIPT) diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c index 598ca6b346..31488935c1 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c @@ -10,6 +10,10 @@ #define NUCLEO_STDOUT_CAPTURE_SIZE (1536U * 1024U) +#ifndef NUCLEO_USE_SEMIHOSTING_WRITE +#define NUCLEO_USE_SEMIHOSTING_WRITE 0 +#endif + __attribute__((aligned(32), used, section(".stdout_capture"))) volatile uint8_t nucleo_stdout_capture[NUCLEO_STDOUT_CAPTURE_SIZE]; @@ -19,14 +23,7 @@ volatile uint32_t nucleo_stdout_capture_len; __attribute__((used)) volatile uint32_t nucleo_stdout_capture_truncated; -int _write(int fd, char *src, int length) { - (void)fd; - - if (src == NULL || length < 0) { - errno = EINVAL; - return -1; - } - +static void capture_write(const char *src, int length) { uint32_t offset = nucleo_stdout_capture_len; if (offset < NUCLEO_STDOUT_CAPTURE_SIZE) { uint32_t available = NUCLEO_STDOUT_CAPTURE_SIZE - offset; @@ -43,8 +40,45 @@ int _write(int fd, char *src, int length) { } else if (length > 0) { nucleo_stdout_capture_truncated = 1; } +} + +#if NUCLEO_USE_SEMIHOSTING_WRITE +#define SEMIHOST_SYS_WRITE 0x05U + +static int semihost_write(int fd, const char *src, int length) { + uintptr_t params[3]; + int unwritten; + + params[0] = (uintptr_t)fd; + params[1] = (uintptr_t)src; + params[2] = (uintptr_t)length; + + __asm__ volatile("mov r0, %1\n" + "mov r1, %2\n" + "bkpt 0xab\n" + "mov %0, r0\n" + : "=r"(unwritten) + : "r"(SEMIHOST_SYS_WRITE), "r"(params) + : "r0", "r1", "memory"); + + return length - unwritten; +} +#endif + +int _write(int fd, char *src, int length) { + if (src == NULL || length < 0) { + errno = EINVAL; + return -1; + } + + capture_write(src, length); +#if NUCLEO_USE_SEMIHOSTING_WRITE + return semihost_write(fd, src, length); +#else + (void)fd; return length; +#endif } void nucleo_stdio_init(void) { From 4f5634d677c296bcc9a739ed92a6b708bf7fe794 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Tue, 5 May 2026 14:15:49 +0100 Subject: [PATCH 08/20] nucleo-n657x0-q: add host helpers and load recovery Factor common host-side logic for argv blobs, GDB scripts, result parsing, ST tool lookup, and symbol resolution into a nucleo_host package shared by the NUCLEO wrapper scripts. Detect pre-output GDB load failures, rerun FLEXMEM configuration, and retry the same test ELF via GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS. Add unit coverage for the helper modules and retry behavior, extend FLEXMEM setup dependencies to all run targets, and document manual load-recovery validation. Signed-off-by: Brendan Moran --- .../platform/nucleo-n657x0-q/README.md | 4 +- .../platform/nucleo-n657x0-q/exec_wrapper.py | 465 +++++------------- .../nucleo-n657x0-q/flexmem_configure.py | 89 ++-- .../platform/nucleo-n657x0-q/make_argv_bin.py | 32 +- .../nucleo-n657x0-q/nucleo_host/__init__.py | 11 + .../nucleo-n657x0-q/nucleo_host/argv_blob.py | 37 ++ .../nucleo-n657x0-q/nucleo_host/gdb_script.py | 139 ++++++ .../nucleo-n657x0-q/nucleo_host/results.py | 131 +++++ .../nucleo-n657x0-q/nucleo_host/st_tools.py | 98 ++++ .../nucleo-n657x0-q/nucleo_host/symbols.py | 68 +++ .../platform/nucleo-n657x0-q/platform.mk | 2 +- .../nucleo-n657x0-q/run_test_after_flexmem.py | 6 + .../nucleo-n657x0-q/test_nucleo_host.py | 230 +++++++++ 13 files changed, 883 insertions(+), 429 deletions(-) create mode 100644 test/baremetal/platform/nucleo-n657x0-q/nucleo_host/__init__.py create mode 100644 test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py create mode 100644 test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py create mode 100644 test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py create mode 100644 test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py create mode 100644 test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py create mode 100644 test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md index 2e08eadab3..54b9f9c7a3 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/README.md +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -84,7 +84,9 @@ python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py \ `run_test_after_flexmem.py` delegates to `exec_wrapper.py`, which starts ST-LINK GDB server, loads the ELF into RAM, injects argv into `mlk_cmdline_block`, runs from `Reset_Handler`, dumps the target stdout capture buffer, and returns the `[[MLKEM-EXIT:]]` code. -The KAT and ACVP run targets depend on `run_flexmem_config`, so `make run_kat` and `make run_acvp` restore the expanded FLEXMEM layout before loading the RAM-resident images. `exec_wrapper.py` retries once after pre-output GDB transport failures; set `GDB_RUN_ATTEMPTS=` to adjust this. If the target enters `HardFault_Handler`, the wrapper re-runs the FLEXMEM config binary and retries once; set `GDB_HARDFAULT_RECOVERY_ATTEMPTS=` to adjust this. +The KAT and ACVP run targets depend on `run_flexmem_config`, so `make run_kat` and `make run_acvp` restore the expanded FLEXMEM layout before loading the RAM-resident images. `exec_wrapper.py` retries once after pre-output GDB transport failures; set `GDB_RUN_ATTEMPTS=` to adjust this. If the initial GDB `load` command reports `load failed` before target output starts, the wrapper re-runs `flexmem_configure.py` and retries the same ELF once; set `GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS=` to adjust or disable this. If the target enters `HardFault_Handler`, the wrapper re-runs the FLEXMEM config binary and retries once; set `GDB_HARDFAULT_RECOVERY_ATTEMPTS=` to adjust this. + +Manual hardware validation for load-failure recovery can force the retry path by resetting the board to its default FLEXMEM layout, building `flexmem_config` and a test ELF, then running `python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py test/build/mlkem512/bin/test_mlkem512` without a prior `run_flexmem_config` step. ## Argv Blob Loading diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index 799a79d8a7..0b00fb3243 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -4,12 +4,21 @@ # Copyright (c) Arm Ltd. # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT +""" +Run one RAM-resident NUCLEO-N657X0-Q test ELF through ST-LINK GDB server. + +The wrapper expects ``flexmem_configure.py`` to have expanded ITCM/DTCM before +normal runs. If the initial GDB ``load`` reports that loading failed before +target output starts, the wrapper can re-run FLEXMEM configuration and retry the +same ELF. Successful runs load the ELF into RAM, let C startup clear memory, +restore a packed argv blob at ``__wrap_main``, stream or dump target stdout, and +map target sentinels to the process exit status expected by the baremetal test +harness. +""" + import logging import os -import re import shlex -import shutil -import struct as st import subprocess import sys import tempfile @@ -18,6 +27,20 @@ import socket import threading +from nucleo_host.argv_blob import pack_cmdline +from nucleo_host.gdb_script import build_run_script +from nucleo_host.results import LAYOUT_FAIL_SENTINEL +from nucleo_host.results import fault_info_from_gdb +from nucleo_host.results import gdb_load_failed_before_target_output +from nucleo_host.results import gdb_observed_hardfault +from nucleo_host.results import parse_exit_sentinel +from nucleo_host.results import split_stdout_capture +from nucleo_host.st_tools import cubeprogrammer_cp_path +from nucleo_host.st_tools import derive_clt_root +from nucleo_host.st_tools import find_stlink_gdbserver +from nucleo_host.symbols import default_readelf +from nucleo_host.symbols import resolve_symbol + VERBOSE = False STDOUT_BYTES_EMITTED = 0 @@ -25,16 +48,18 @@ TARGET_FAILURE_KIND = "" SUPPRESS_RETRYABLE_DIAGNOSTICS = False LAST_FAULT_DIAGNOSTICS = "" +LAST_LOAD_FAILURE_DIAGNOSTICS = "" LOG = logging.getLogger(__name__) -ARGV_BLOCK_SIZE = 64 * 1024 def configure_logging(): + """Configure process-wide logging after ``VERBOSE`` has been parsed.""" level = logging.DEBUG if VERBOSE else logging.INFO logging.basicConfig(level=level, format="%(message)s") def log_output(output, level=logging.INFO, prefix=None): + """Log multiline subprocess output one line at a time.""" if not output: return for line in str(output).rstrip().splitlines(): @@ -44,114 +69,29 @@ def log_output(output, level=logging.INFO, prefix=None): def err(msg, **kwargs): + """Report an error message regardless of verbose mode.""" # Always report errors, including multiline subprocess diagnostics. log_output(msg, logging.ERROR) def info(msg, **kwargs): + """Report an informational message only in verbose mode.""" if VERBOSE: LOG.debug("%s", msg) -def _decode_cfsr(cfsr: int): - bits = [ - (0, "IACCVIOL"), - (1, "DACCVIOL"), - (3, "MUNSTKERR"), - (4, "MSTKERR"), - (5, "MLSPERR"), - (7, "MMARVALID"), - (8, "IBUSERR"), - (9, "PRECISERR"), - (10, "IMPRECISERR"), - (11, "UNSTKERR"), - (12, "STKERR"), - (13, "LSPERR"), - (15, "BFARVALID"), - (16, "UNDEFINSTR"), - (17, "INVSTATE"), - (18, "INVPC"), - (19, "NOCP"), - (24, "UNALIGNED"), - (25, "DIVBYZERO"), - ] - return [name for bit, name in bits if cfsr & (1 << bit)] - - -def _decode_hfsr(hfsr: int): - bits = [(1, "VECTTBL"), (30, "FORCED"), (31, "DEBUGEVT")] - return [name for bit, name in bits if hfsr & (1 << bit)] - - -def _fault_info_from_gdb(gdb_text: str) -> str: - values = {} - for name, value in re.findall(r"^(CFSR|HFSR|DFSR|MMFAR|BFAR|AFSR|SHCSR|CCR|MSP|PSP|LR|PC)=0x([0-9a-fA-F]+)$", gdb_text, re.MULTILINE): - values[name] = int(value, 16) - - if not values: - return "" - - lines = ["Fault registers:"] - for name in ("CFSR", "HFSR", "DFSR", "MMFAR", "BFAR", "AFSR", "SHCSR", "CCR", "MSP", "PSP", "LR", "PC"): - if name in values: - lines.append(f" {name}=0x{values[name]:08x}") - - cfsr_bits = _decode_cfsr(values.get("CFSR", 0)) - hfsr_bits = _decode_hfsr(values.get("HFSR", 0)) - if cfsr_bits: - lines.append(" CFSR bits: " + ", ".join(cfsr_bits)) - if hfsr_bits: - lines.append(" HFSR bits: " + ", ".join(hfsr_bits)) - - stacked = re.search(r"^STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\s*\n((?:0x[0-9a-fA-F]+:\s+.*\n?)?)", gdb_text, re.MULTILINE) - if stacked: - stack_lines = [line.strip() for line in stacked.group(1).splitlines() if line.strip()] - if stack_lines: - lines.append(" stacked frame dump:") - lines.extend(f" {line}" for line in stack_lines) - - return "\n".join(lines) - - -def _gdb_observed_hardfault(gdb_text: str) -> bool: - return "[[NUCLEO-HARDFAULT]]" in gdb_text or re.search( - r"^HardFault_Handler \(\)", gdb_text, re.MULTILINE - ) is not None - - -def pack_cmdline(args, base_addr): - """ - Pack argv for the STM32 baremetal target: - u32 argc - u32 argv_ptrs[argc] (absolute addresses: base_addr + string offsets) - NUL-terminated strings - All fields are little-endian 32-bit. - """ - argc = len(args) - header_sz = 4 + 4 * argc - ptrs = [] - strings = b"" - cur = 0 - for s in args: - b = s.encode("utf-8") + b"\x00" - ptrs.append(base_addr + header_sz + cur) - strings += b - cur += len(b) - blob = st.pack(" ARGV_BLOCK_SIZE: - raise ValueError(f"argv blob is {len(blob)} bytes, exceeds {ARGV_BLOCK_SIZE}-byte block") - return blob + bytes(ARGV_BLOCK_SIZE - len(blob)) - - def run(cmd, **kwargs): + """Thin wrapper around ``subprocess.run`` for test-time monkeypatching.""" return subprocess.run(cmd, **kwargs) def popen(cmd, **kwargs): + """Thin wrapper around ``subprocess.Popen`` for test-time monkeypatching.""" return subprocess.Popen(cmd, **kwargs) def _pick_free_port() -> int: + """Ask the OS for an available localhost TCP port.""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.bind(("127.0.0.1", 0)) @@ -161,6 +101,7 @@ def _pick_free_port() -> int: def _wait_for_port(host: str, port: int, timeout_s: float) -> bool: + """Wait until a local TCP port accepts connections or timeout expires.""" deadline = time.time() + timeout_s while time.time() < deadline: try: @@ -171,78 +112,15 @@ def _wait_for_port(host: str, port: int, timeout_s: float) -> bool: return False -def _stm32_programmer_cli(cp_path: str): - # Accept either a direct CLI path or the containing CubeProgrammer directory. - if not cp_path: - return None - candidates = [] - if os.path.isdir(cp_path): - candidates += [ - os.path.join(cp_path, "STM32_Programmer_CLI"), - os.path.join(cp_path, "bin", "STM32_Programmer_CLI"), - ] - else: - candidates.append(cp_path) - for candidate in candidates: - if os.path.isfile(candidate) and os.access(candidate, os.X_OK): - return candidate - return None - - -def _cubeprogrammer_cli(st_cubeprog: str, st_clt_root: str): - # Prefer explicit paths from the environment before falling back to PATH. - candidates = [st_cubeprog] - if st_clt_root: - candidates.append(os.path.join(st_clt_root, "STM32CubeProgrammer")) - cli = None - for candidate in candidates: - cli = _stm32_programmer_cli(candidate) - if cli: - break - if cli is None: - cli = shutil.which("STM32_Programmer_CLI") - return cli - - -def _cubeprogrammer_connect_args(st_speed: str, st_serial: str, st_apid: str): - # Keep reset/readback commands aligned with the selected probe speed and AP. - args = ["-c", "port=SWD", f"freq={st_speed}"] - connect_mode = os.environ.get("STLINK_CONNECT_MODE") - if connect_mode: - args.append(f"mode={connect_mode}") - if st_serial: - args.append(f"sn={st_serial}") - if st_apid: - args.append(f"ap={st_apid}") - return args - - -def _run_cubeprogrammer(cli: str, connect_args, commands, verbose: bool = False) -> bool: - # CubeProgrammer diagnostics are noisy, so log them only on failure or request. - cmd = [cli] + connect_args + commands - cp = run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - if verbose or cp.returncode != 0: - output = cp.stdout or "" - if output: - log_output(output, logging.DEBUG if verbose else logging.ERROR) - return cp.returncode == 0 - - -def _reset_target(st_cubeprog: str, st_clt_root: str, st_speed: str, st_serial: str, st_apid: str) -> bool: - # Reset through CubeProgrammer when available; callers can still proceed if absent. - cli = _cubeprogrammer_cli(st_cubeprog, st_clt_root) - if cli is None: - return False - return _run_cubeprogrammer(cli, _cubeprogrammer_connect_args(st_speed, st_serial, st_apid), ["-rst"]) - - def _default_flexmem_config_elf() -> str: + """Return the default build path for the FLEXMEM configuration ELF.""" platform_dir = os.path.dirname(os.path.abspath(__file__)) repo_root = os.path.abspath(os.path.join(platform_dir, "..", "..", "..", "..")) return os.path.join(repo_root, "test", "build", "nucleo-n657x0-q", "flexmem_config.elf") -def _recover_after_hardfault() -> bool: +def _recover_flexmem(reason: str, failure_message: str) -> bool: + """Re-run FLEXMEM configuration after a retryable setup or target failure.""" platform_dir = os.path.dirname(os.path.abspath(__file__)) configure_script = os.path.join(platform_dir, "flexmem_configure.py") config_elf = os.environ.get("FLEXMEM_CONFIG_ELF", _default_flexmem_config_elf()) @@ -254,7 +132,7 @@ def _recover_after_hardfault() -> bool: err(f"FLEXMEM config ELF not found: {config_elf}") return False - info("[exec_wrapper] recovering from HardFault: re-running FLEXMEM config") + info(f"[exec_wrapper] recovering from {reason}: re-running FLEXMEM config") recovery_env = os.environ.copy() recovery_env.setdefault("STLINK_CONNECT_MODE", "UR") cp = run( @@ -265,7 +143,7 @@ def _recover_after_hardfault() -> bool: env=recovery_env, ) if cp.returncode != 0: - err("FLEXMEM reconfiguration after HardFault failed") + err(failure_message) log_output(cp.stdout, logging.ERROR) return False if VERBOSE: @@ -273,18 +151,31 @@ def _recover_after_hardfault() -> bool: return True +def _recover_after_hardfault() -> bool: + """Re-run FLEXMEM configuration after a target HardFault retry trigger.""" + return _recover_flexmem("HardFault", "FLEXMEM reconfiguration after HardFault failed") + + +def _recover_after_load_failure() -> bool: + """Re-run FLEXMEM configuration after a GDB load failure retry trigger.""" + return _recover_flexmem("GDB load failure", "FLEXMEM reconfiguration after GDB load failure failed") + + def _run_once(): + """Run the target ELF once and return the wrapper exit code for that run.""" global VERBOSE global STDOUT_BYTES_EMITTED global TARGET_FAILURE global TARGET_FAILURE_KIND global SUPPRESS_RETRYABLE_DIAGNOSTICS global LAST_FAULT_DIAGNOSTICS + global LAST_LOAD_FAILURE_DIAGNOSTICS STDOUT_BYTES_EMITTED = 0 TARGET_FAILURE = False TARGET_FAILURE_KIND = "" LAST_FAULT_DIAGNOSTICS = "" + LAST_LOAD_FAILURE_DIAGNOSTICS = "" argv = sys.argv[1:] # Minimal flag parsing for wrapper flags (remove them from argv) @@ -310,10 +201,7 @@ def _run_once(): gdb = os.environ.get("GDB", "arm-none-eabi-gdb") nm = os.environ.get("NM", "arm-none-eabi-nm") - readelf = os.environ.get( - "READELF", - shutil.which("arm-none-eabi-readelf") or shutil.which("readelf") or "readelf", - ) + readelf = os.environ.get("READELF", default_readelf()) port = int(os.environ.get("GDB_PORT", "3333")) # STM32Cube Command Line Tools integration # Users must install STM32CubeCLT and provide a gdbserver command. @@ -347,33 +235,8 @@ def _run_once(): arg_block_addr = None def _resolve_symbol_addr(elf_path: str, sym: str): - # Try nm first: format ' ' - try: - cp = run([nm, "-n", elf_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - if cp.returncode == 0: - for line in cp.stdout.splitlines(): - parts = line.strip().split() - if len(parts) >= 3 and parts[-1] == sym: - addr_hex = parts[0] - if not addr_hex.startswith("0x"): - addr_hex = "0x" + addr_hex - return addr_hex - except Exception: - pass - # Fallback: readelf -s - try: - cp = run([readelf, "-s", elf_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - if cp.returncode == 0: - for line in cp.stdout.splitlines(): - if sym in line: - fields = line.split() - if len(fields) >= 8 and fields[-1] == sym: - val = fields[1] - if all(c in "0123456789abcdefABCDEF" for c in val): - return "0x" + val - except Exception: - pass - return None + """Resolve a symbol with the wrapper-selected binary utilities.""" + return resolve_symbol(elf_path, sym, nm=nm, readelf=readelf) # Try both expected names in case of historical rename for cand in (arg_block_sym, "mlk_cmdline_block"): @@ -432,22 +295,10 @@ def _resolve_symbol_addr(elf_path: str, sym: str): f.write(blob) # GDB writes target stdout here after the run; Python logs it below. stdout_capture_bin = os.path.join(td, "stdout-capture.bin") - # Build ST gdbserver command - # Discover ST-LINK_gdbserver - stlink_bin = shutil.which("ST-LINK_gdbserver") # Allow deriving CLT root from CubeProgrammer path if not provided if not st_clt_root and st_cubeprog: - # .../STM32CubeCLT_x.y.z/STM32CubeProgrammer -> CLT root is parent dir - st_clt_root = os.path.dirname(os.path.abspath(st_cubeprog)) - # If user pointed directly at the STM32CubeProgrammer dir, take its parent - base = os.path.basename(st_clt_root).lower() - if base.startswith("stm32cubeprogrammer"): - st_clt_root = os.path.dirname(st_clt_root) - candidate = None - if not stlink_bin and st_clt_root: - candidate = os.path.join(st_clt_root, "STLink-gdb-server", "bin", "ST-LINK_gdbserver") - if os.path.isfile(candidate) and os.access(candidate, os.X_OK): - stlink_bin = candidate + st_clt_root = derive_clt_root(st_cubeprog) + stlink_bin, candidate = find_stlink_gdbserver(st_clt_root) # Auto-detect a default template if not provided if not st_gdbserver_cmd_tpl and stlink_bin: @@ -457,33 +308,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): if st_gdbserver_cmd_tpl: # Determine best '-cp' path for STM32CubeProgrammer CLI - cp_path = None - # If user provided a path - if st_cubeprog: - p = os.path.abspath(st_cubeprog) - if os.path.isdir(p): - # If directory is 'STM32CubeProgrammer', check for CLI within - cli1 = os.path.join(p, "STM32_Programmer_CLI") - cli2 = os.path.join(p, "bin", "STM32_Programmer_CLI") - if os.path.isfile(cli1) and os.access(cli1, os.X_OK): - cp_path = p - elif os.path.isfile(cli2) and os.access(cli2, os.X_OK): - cp_path = os.path.join(p, "bin") - elif os.path.isfile(p): - # User pointed directly to CLI; use its directory - cp_path = os.path.dirname(p) - # If not resolved yet, try from CLT root - if cp_path is None and st_clt_root: - cli2 = os.path.join(os.path.abspath(st_clt_root), "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") - if os.path.isfile(cli2) and os.access(cli2, os.X_OK): - cp_path = os.path.dirname(cli2) - # If still None, try relative to ST-LINK gdbserver location - if cp_path is None and 'stlink_bin' in locals() and stlink_bin: - # stlink_bin .../STLink-gdb-server/bin/ST-LINK_gdbserver -> CLT root is three parents up - root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(stlink_bin)))) - cli2 = os.path.join(root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") - if os.path.isfile(cli2) and os.access(cli2, os.X_OK): - cp_path = os.path.dirname(cli2) + cp_path = cubeprogrammer_cp_path(st_cubeprog, st_clt_root, stlink_bin) # Provide a flexible set of placeholders for various CLT tools. # - {serial} -> raw serial value (e.g. 303030303030) @@ -534,7 +359,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): # Append small diagnostics if we attempted a candidate path if candidate: msg += f" Searched for ST-LINK_gdbserver at: {candidate}\n" - if stlink_bin is None and shutil.which("ST-LINK_gdbserver") is None: + if stlink_bin is None: msg += " Note: ST-LINK_gdbserver not found on PATH.\n" err(msg) return 2 @@ -567,6 +392,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): shared = {"exit_code": None, "stdout_streamed": False} def _semihost_reader(sock: socket.socket): + """Stream semihost console output and detect the exit sentinel.""" global STDOUT_BYTES_EMITTED buf = b"" @@ -584,26 +410,21 @@ def _semihost_reader(sock: socket.socket): except Exception: text = line.decode(errors="replace") # Detect exit sentinel first - t = text.strip() - is_exit = t.startswith("[[MLKEM-EXIT:") and t.endswith("]]") + is_exit, parsed_exit_code = parse_exit_sentinel(text) if is_exit: - try: - code_str = t[len("[[MLKEM-EXIT:"):-2] - shared["exit_code"] = int(code_str) - except Exception: - shared["exit_code"] = 1 + shared["exit_code"] = parsed_exit_code semihost_exit.set() # Do not log the sentinel unless verbose. if VERBOSE: LOG.debug("[semi] %s", text) else: + shared["stdout_streamed"] = True if VERBOSE: LOG.debug("[semi] %s", text) else: sys.stdout.buffer.write(line + b"\n") sys.stdout.buffer.flush() STDOUT_BYTES_EMITTED += len(line) + 1 - shared["stdout_streamed"] = True except socket.timeout: continue except OSError: @@ -650,92 +471,19 @@ def _semihost_reader(sock: socket.socket): err(f"[exec_wrapper] STLinkUpgrade candidate: {h}") return 2 - gdb_lines = [ - "set pagination off", - "set confirm off", - f"target remote localhost:{port}", - ] - # Write GDB commands to a temp script and run with -x - gdb_lines += [ - # semihosting enable is handled by gdbserver; keep gdb quiet - "load", - f"tbreak {wrap_main_break}", - f"jump {reset_handler_jump}", - (f"restore {argv_bin} binary {arg_block_addr}" if arg_block_addr else f"restore {argv_bin} binary &{arg_block_sym}"), - "break HardFault_Handler", - "commands", - " echo [[NUCLEO-HARDFAULT]]\\n", - "end", - "break nucleo_layout_fail", - "commands", - " echo [[NUCLEO-LAYOUT-FAIL]]\\n", - "end", - "continue", - ] - if stdout_capture_addr and stdout_capture_len_addr: - # Clamp the dump length to the compile-time capture buffer size. - gdb_lines += [ - f"set $nucleo_stdout_len = *(unsigned int *){stdout_capture_len_addr}", - "if $nucleo_stdout_len > 0", - f" if $nucleo_stdout_len > {stdout_capture_size}", - f" set $nucleo_stdout_len = {stdout_capture_size}", - " end", - f" dump binary memory {stdout_capture_bin} {stdout_capture_addr} {stdout_capture_addr} + $nucleo_stdout_len", - "end", - ] - if stdout_capture_truncated_addr: - gdb_lines += [ - f"set $nucleo_stdout_truncated = *(unsigned int *){stdout_capture_truncated_addr}", - "p/x $nucleo_stdout_truncated", - ] - gdb_lines += [ - "info registers", - "x/4wx $sp", - "echo CFSR=", - "output/x *(unsigned int *)0xE000ED28", - "echo \\n", - "echo HFSR=", - "output/x *(unsigned int *)0xE000ED2C", - "echo \\n", - "echo DFSR=", - "output/x *(unsigned int *)0xE000ED30", - "echo \\n", - "echo MMFAR=", - "output/x *(unsigned int *)0xE000ED34", - "echo \\n", - "echo BFAR=", - "output/x *(unsigned int *)0xE000ED38", - "echo \\n", - "echo AFSR=", - "output/x *(unsigned int *)0xE000ED3C", - "echo \\n", - "echo SHCSR=", - "output/x *(unsigned int *)0xE000ED24", - "echo \\n", - "echo CCR=", - "output/x *(unsigned int *)0xE000ED14", - "echo \\n", - "echo MSP=", - "output/x $msp", - "echo \\n", - "echo PSP=", - "output/x $psp", - "echo \\n", - "echo LR=", - "output/x $lr", - "echo \\n", - "echo PC=", - "output/x $pc", - "echo \\n", - "echo STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\\n", - "if ($lr & 4)", - " x/8wx $psp", - "else", - " x/8wx $msp", - "end", - "x/4wx 0xE000ED28", - "x/wx 0xE000ED38", - ] + gdb_lines = build_run_script( + port=port, + wrap_main_break=wrap_main_break, + reset_handler_jump=reset_handler_jump, + argv_bin=argv_bin, + arg_block_addr=arg_block_addr, + arg_block_sym=arg_block_sym, + stdout_capture_addr=stdout_capture_addr, + stdout_capture_len_addr=stdout_capture_len_addr, + stdout_capture_truncated_addr=stdout_capture_truncated_addr, + stdout_capture_size=stdout_capture_size, + stdout_capture_bin=stdout_capture_bin, + ) if VERBOSE: LOG.debug("============ GDB SCRIPT ============") @@ -814,29 +562,19 @@ def _semihost_reader(sock: socket.socket): err(errout, end="") gdb_text = f"{out}\n{errout}" - layout_failed = "[[NUCLEO-LAYOUT-FAIL]]" in gdb_text - hardfaulted = _gdb_observed_hardfault(gdb_text) + layout_failed = LAYOUT_FAIL_SENTINEL in gdb_text + hardfaulted = gdb_observed_hardfault(gdb_text) target_failed = layout_failed or hardfaulted - captured_text = "" if os.path.exists(stdout_capture_bin): try: # Parse the same exit sentinel from dumped RAM output as from semihosting. with open(stdout_capture_bin, "rb") as capture_file: captured = capture_file.read() - captured_text = captured.decode("utf-8", errors="replace") - captured_lines = [] - for capture_line in captured_text.splitlines(keepends=True): - stripped_line = capture_line.strip() - if stripped_line.startswith("[[MLKEM-EXIT:") and stripped_line.endswith("]]"): - try: - shared["exit_code"] = int(stripped_line[len("[[MLKEM-EXIT:"):-2]) - except Exception: - shared["exit_code"] = 1 - continue - captured_lines.append(capture_line) - if captured_lines and not shared.get("stdout_streamed") and not target_failed: - captured_output = "".join(captured_lines) + captured_output, captured_exit_code = split_stdout_capture(captured) + if captured_exit_code is not None: + shared["exit_code"] = captured_exit_code + if captured_output and not shared.get("stdout_streamed") and not target_failed: sys.stdout.write(captured_output) sys.stdout.flush() STDOUT_BYTES_EMITTED += len(captured_output.encode("utf-8")) @@ -859,7 +597,7 @@ def _semihost_reader(sock: socket.socket): if hardfaulted: TARGET_FAILURE = True TARGET_FAILURE_KIND = "hardfault" - fault_info = _fault_info_from_gdb(gdb_text) + fault_info = fault_info_from_gdb(gdb_text) LAST_FAULT_DIAGNOSTICS = fault_info if not SUPPRESS_RETRYABLE_DIAGNOSTICS: err("FAIL!") @@ -873,6 +611,17 @@ def _semihost_reader(sock: socket.socket): return 0 if gdbp.returncode != 0: + target_output_observed = bool(shared.get("stdout_streamed")) or STDOUT_BYTES_EMITTED != 0 + exit_code_observed = shared.get("exit_code") is not None + if gdb_load_failed_before_target_output( + gdb_text, + target_output_observed=target_output_observed, + exit_code_observed=exit_code_observed, + ): + TARGET_FAILURE = True + TARGET_FAILURE_KIND = "load-failed" + LAST_LOAD_FAILURE_DIAGNOSTICS = gdb_text + return gdbp.returncode if not SUPPRESS_RETRYABLE_DIAGNOSTICS: err("FAIL!") err(f"gdb batch failed with code {gdbp.returncode}") @@ -913,22 +662,46 @@ def _semihost_reader(sock: socket.socket): def main(): + """Run the wrapper with configured transport and FLEXMEM retry policy.""" global SUPPRESS_RETRYABLE_DIAGNOSTICS global LAST_FAULT_DIAGNOSTICS attempts = max(1, int(os.environ.get("GDB_RUN_ATTEMPTS", "2"))) hardfault_attempts = max(0, int(os.environ.get("GDB_HARDFAULT_RECOVERY_ATTEMPTS", "1"))) + load_failure_attempts = max(0, int(os.environ.get("GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS", "1"))) transport_retries = 0 hardfault_recoveries = 0 + load_failure_recoveries = 0 last_rc = 1 while True: can_retry_transport = transport_retries < attempts - 1 can_retry_hardfault = hardfault_recoveries < hardfault_attempts + can_retry_load_failure = load_failure_recoveries < load_failure_attempts SUPPRESS_RETRYABLE_DIAGNOSTICS = can_retry_transport or can_retry_hardfault last_rc = _run_once() if last_rc == 0: return 0 + if TARGET_FAILURE_KIND == "load-failed": + if can_retry_load_failure: + load_failure_recoveries += 1 + if _recover_after_load_failure(): + if VERBOSE: + err( + "[exec_wrapper] retrying after recovered GDB load failure " + f"({load_failure_recoveries}/{load_failure_attempts})" + ) + time.sleep(0.5) + continue + if LAST_LOAD_FAILURE_DIAGNOSTICS: + err("GDB load-failure diagnostics from failed run:") + err(LAST_LOAD_FAILURE_DIAGNOSTICS) + return last_rc + err("FAIL!") + err(f"gdb batch failed with code {last_rc}") + if LAST_LOAD_FAILURE_DIAGNOSTICS: + log_output(LAST_LOAD_FAILURE_DIAGNOSTICS, logging.ERROR) + return last_rc if TARGET_FAILURE_KIND == "hardfault" and can_retry_hardfault: hardfault_recoveries += 1 if _recover_after_hardfault(): diff --git a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py index 22512c6f82..0ab4b8502a 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py +++ b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py @@ -1,21 +1,28 @@ #!/usr/bin/env python3 +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + """ -/* - * Copyright (c) The mlkem-native project authors - * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT - */ -""" +Configure STM32N6 FLEXMEM before loading RAM-resident test images. -# Configure STM32N6 FLEXMEM before loading the RAM-resident test image. +The helper downloads ``flexmem_config.elf`` into the reset-time memory layout, +starts it directly from RAM, polls ``SYSCFG->CM55TCMCR`` until the requested TCM +split is visible over SWD, then resets the target so the next ELF can be loaded +into the expanded ITCM/DTCM layout. +""" import os import re import logging -import shutil -import subprocess import sys import time +from nucleo_host.st_tools import connect_args as st_connect_args +from nucleo_host.st_tools import find_cubeprogrammer_cli as st_find_cubeprogrammer_cli +from nucleo_host.st_tools import run_quiet +from nucleo_host.symbols import resolve_symbol_with_nm + DONE = "FLEXMEM configuration complete; reset target and load test binary." # The config ELF writes SYSCFG->CM55TCMCR. Polling the register via SWD proves @@ -28,11 +35,13 @@ def configure_logging(): + """Configure logging, using ``FLEXMEM_VERBOSE`` as the debug switch.""" level = logging.DEBUG if os.environ.get("FLEXMEM_VERBOSE") else logging.INFO logging.basicConfig(level=level, format="%(message)s") def log_output(output, level): + """Log multiline CubeProgrammer output at the requested level.""" if not output: return for line in output.rstrip().splitlines(): @@ -40,33 +49,17 @@ def log_output(output, level): def err(msg): + """Report a user-visible error line.""" LOG.error("%s", msg) def find_cubeprogrammer_cli(cp_path): - # Accept a direct binary path, a CubeProgrammer directory, a CubeCLT root, or PATH. - candidates = [] - if cp_path: - if os.path.isdir(cp_path): - candidates.extend([ - os.path.join(cp_path, "STM32_Programmer_CLI"), - os.path.join(cp_path, "bin", "STM32_Programmer_CLI"), - ]) - else: - candidates.append(cp_path) - st_clt_root = os.environ.get("ST_CUBE_CLT_ROOT", "") - if st_clt_root: - candidates.append(os.path.join(st_clt_root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI")) - path_candidate = shutil.which("STM32_Programmer_CLI") - if path_candidate: - candidates.append(path_candidate) - for candidate in candidates: - if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK): - return candidate - return None + """Find ``STM32_Programmer_CLI`` using platform environment defaults.""" + return st_find_cubeprogrammer_cli(cp_path, os.environ.get("ST_CUBE_CLT_ROOT", "")) def cubeprogrammer_cli(): + """Return the CubeProgrammer CLI path, reporting a helpful error if absent.""" cli = find_cubeprogrammer_cli(os.environ.get("ST_CUBE_PROG_PATH", "")) if cli is None: err("STM32_Programmer_CLI not found; set ST_CUBE_PROG_PATH or ST_CUBE_CLT_ROOT") @@ -74,25 +67,18 @@ def cubeprogrammer_cli(): def connect_args(mode=None): + """Build CubeProgrammer SWD connection arguments from the environment.""" # Keep all CubeProgrammer calls on the same probe, speed, and access port. - args = ["-c", "port=SWD", f"freq={os.environ.get('STLINK_SPEED', '200')}"] - connect_mode = mode if mode is not None else os.environ.get("STLINK_CONNECT_MODE") - if connect_mode: - args.append(f"mode={connect_mode}") - serial = os.environ.get("STLINK_SERIAL", "") - apid = os.environ.get("STLINK_APID", "") - if serial: - args.append(f"sn={serial}") - if apid: - args.append(f"ap={apid}") - return args - - -def run_quiet(cmd): - return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + return st_connect_args( + speed=os.environ.get("STLINK_SPEED", "200"), + serial=os.environ.get("STLINK_SERIAL", ""), + apid=os.environ.get("STLINK_APID", ""), + mode=mode if mode is not None else os.environ.get("STLINK_CONNECT_MODE"), + ) def reset_target(cli): + """Best-effort target reset through CubeProgrammer.""" # Reset is best-effort: the subsequent download/halt sequence reports hard failures. args = ["-c", "port=SWD", f"freq={os.environ.get('STLINK_SPEED', '200')}"] serial = os.environ.get("STLINK_SERIAL", "") @@ -102,22 +88,13 @@ def reset_target(cli): def resolve_symbol(elf, symbol): + """Resolve a symbol with ``nm`` for direct RAM launch setup.""" # Resolve entry/stack symbols up front so CubeProgrammer can start from RAM directly. - nm = os.environ.get("NM", "arm-none-eabi-nm") - try: - cp = subprocess.run([nm, "-n", elf], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - except OSError: - return None - if cp.returncode != 0: - return None - for line in cp.stdout.splitlines(): - fields = line.split() - if len(fields) >= 3 and fields[-1] == symbol: - return "0x" + fields[0].lstrip("0x") - return None + return resolve_symbol_with_nm(elf, symbol, nm=os.environ.get("NM", "arm-none-eabi-nm")) def read_flexmem_value(cli): + """Read the FLEXMEM TCM configuration register via SWD HOTPLUG mode.""" # HOTPLUG reads avoid resetting the core while the config ELF is parked at BKPT. cp = run_quiet([cli] + connect_args("HOTPLUG") + ["-r32", CM55TCMCR_ADDR, "1"]) if os.environ.get("FLEXMEM_VERBOSE"): @@ -131,6 +108,7 @@ def read_flexmem_value(cli): def wait_for_flexmem(cli, timeout_s): + """Poll until the FLEXMEM register reports the expected expanded layout.""" # The register update is fast, but polling absorbs probe/transport latency. deadline = time.time() + timeout_s while time.time() < deadline: @@ -142,6 +120,7 @@ def wait_for_flexmem(cli, timeout_s): def main(): + """Download and run the FLEXMEM config ELF, then verify the latched layout.""" configure_logging() if len(sys.argv) != 2: diff --git a/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py index 26c2de0e28..c73f6c2b98 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py +++ b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py @@ -3,38 +3,16 @@ # Copyright (c) Arm Ltd. # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT +"""Build a standalone packed argv blob for debugger or fixture use.""" + import os -import struct as st import sys -ARGV_BLOCK_SIZE = 64 * 1024 - - -def pack_cmdline(args, base_addr): - """ - Pack argv for the STM32 baremetal target: - u32 argc - u32 argv_ptrs[argc] (absolute addresses: base_addr + string offsets) - NUL-terminated strings - All fields are little-endian 32-bit. - """ - argc = len(args) - header_sz = 4 + 4 * argc - ptrs = [] - strings = b"" - cur = 0 - for s in args: - b = s.encode("utf-8") + b"\x00" - ptrs.append(base_addr + header_sz + cur) - strings += b - cur += len(b) - blob = st.pack(" ARGV_BLOCK_SIZE: - raise ValueError(f"argv blob is {len(blob)} bytes, exceeds {ARGV_BLOCK_SIZE}-byte block") - return blob + bytes(ARGV_BLOCK_SIZE - len(blob)) +from nucleo_host.argv_blob import pack_cmdline def main(argv): + """Parse CLI arguments, write the argv blob, and return a process code.""" if len(argv) < 4: print("Usage: make_argv_bin.py [arg1 ...]", file=sys.stderr) return 2 @@ -46,6 +24,8 @@ def main(argv): print(f"Invalid base address hex: {base_hex}", file=sys.stderr) return 2 args = argv[3:] + # The output format is shared with exec_wrapper.py and consumed directly by + # the target ``mlk_cmdline_block`` memory reservation. blob = pack_cmdline(args, base_addr) with open(out, "wb") as f: f.write(blob) diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/__init__.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/__init__.py new file mode 100644 index 0000000000..a2e292433a --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +""" +Host-side helpers for the NUCLEO-N657X0-Q baremetal platform. + +The modules in this package keep debugger orchestration details out of the +entry-point scripts. They are intentionally small and mostly pure so the +hardware-independent pieces can be covered by local unit tests. +""" diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py new file mode 100644 index 0000000000..c1fb72e839 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py @@ -0,0 +1,37 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Build the target-resident argv block consumed by ``__wrap_main``.""" + +import struct as st + + +ARGV_BLOCK_SIZE = 64 * 1024 + + +def pack_cmdline(args, base_addr, block_size=ARGV_BLOCK_SIZE): + """ + Return a padded little-endian argv blob for the STM32 baremetal target. + + The blob starts with ``uint32_t argc`` followed by ``uint32_t argv[argc]``. + Each argv entry is an absolute target address pointing at a NUL-terminated + UTF-8 string stored later in the same blob. The result is padded to the + full target reservation so GDB ``restore`` overwrites stale contents. + """ + argc = len(args) + header_sz = 4 + 4 * argc + ptrs = [] + strings = b"" + cur = 0 + for arg in args: + encoded = arg.encode("utf-8") + b"\x00" + # GDB writes the blob at ``base_addr``; the C side expects argv pointers + # to be valid target addresses rather than offsets into the blob. + ptrs.append(base_addr + header_sz + cur) + strings += encoded + cur += len(encoded) + blob = st.pack(" block_size: + raise ValueError(f"argv blob is {len(blob)} bytes, exceeds {block_size}-byte block") + return blob + bytes(block_size - len(blob)) diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py new file mode 100644 index 0000000000..d4f43fa6a4 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py @@ -0,0 +1,139 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Generate the GDB batch script used to load and run RAM-resident tests.""" + + +def build_run_script( + *, + port, + wrap_main_break, + reset_handler_jump, + argv_bin, + arg_block_addr, + arg_block_sym, + stdout_capture_addr, + stdout_capture_len_addr, + stdout_capture_truncated_addr, + stdout_capture_size, + stdout_capture_bin, +): + """ + Build the full GDB command list for one test-image run. + + The order is part of the platform contract: load the RAM ELF, run normal C + startup until ``__wrap_main``, restore argv after ``.bss`` has been cleared, + install target-failure breakpoints, continue, then harvest stdout and fault + diagnostics after execution stops. + """ + gdb_lines = [ + "set pagination off", + "set confirm off", + f"target remote localhost:{port}", + # Semihosting is configured on the ST-LINK GDB server command line; the + # GDB script stays focused on target state and RAM transfers. + "load", + f"tbreak {wrap_main_break}", + f"jump {reset_handler_jump}", + restore_argv_command(argv_bin, arg_block_addr, arg_block_sym), + "break HardFault_Handler", + "commands", + " echo [[NUCLEO-HARDFAULT]]\\n", + "end", + "break nucleo_layout_fail", + "commands", + " echo [[NUCLEO-LAYOUT-FAIL]]\\n", + "end", + "continue", + ] + if stdout_capture_addr and stdout_capture_len_addr: + gdb_lines += stdout_capture_dump_commands( + stdout_capture_addr=stdout_capture_addr, + stdout_capture_len_addr=stdout_capture_len_addr, + stdout_capture_size=stdout_capture_size, + stdout_capture_bin=stdout_capture_bin, + ) + if stdout_capture_truncated_addr: + gdb_lines += [ + f"set $nucleo_stdout_truncated = *(unsigned int *){stdout_capture_truncated_addr}", + "p/x $nucleo_stdout_truncated", + ] + gdb_lines += fault_diagnostic_commands() + return gdb_lines + + +def restore_argv_command(argv_bin, arg_block_addr, arg_block_sym): + """Return the GDB ``restore`` command for the packed argv blob.""" + if arg_block_addr: + # Prefer a numeric address because some RAM-loaded ELFs have unreliable + # symbol lookup after ``target remote``/``load`` transitions. + return f"restore {argv_bin} binary {arg_block_addr}" + return f"restore {argv_bin} binary &{arg_block_sym}" + + +def stdout_capture_dump_commands(*, stdout_capture_addr, stdout_capture_len_addr, stdout_capture_size, stdout_capture_bin): + """Return commands that dump the target stdout capture buffer to a file.""" + return [ + f"set $nucleo_stdout_len = *(unsigned int *){stdout_capture_len_addr}", + "if $nucleo_stdout_len > 0", + # Clamp to the compile-time buffer size before using the target-provided + # length as a host file dump bound. + f" if $nucleo_stdout_len > {stdout_capture_size}", + f" set $nucleo_stdout_len = {stdout_capture_size}", + " end", + f" dump binary memory {stdout_capture_bin} {stdout_capture_addr} {stdout_capture_addr} + $nucleo_stdout_len", + "end", + ] + + +def fault_diagnostic_commands(): + """Return commands that print Cortex-M fault registers and stack context.""" + return [ + "info registers", + "x/4wx $sp", + "echo CFSR=", + "output/x *(unsigned int *)0xE000ED28", + "echo \\n", + "echo HFSR=", + "output/x *(unsigned int *)0xE000ED2C", + "echo \\n", + "echo DFSR=", + "output/x *(unsigned int *)0xE000ED30", + "echo \\n", + "echo MMFAR=", + "output/x *(unsigned int *)0xE000ED34", + "echo \\n", + "echo BFAR=", + "output/x *(unsigned int *)0xE000ED38", + "echo \\n", + "echo AFSR=", + "output/x *(unsigned int *)0xE000ED3C", + "echo \\n", + "echo SHCSR=", + "output/x *(unsigned int *)0xE000ED24", + "echo \\n", + "echo CCR=", + "output/x *(unsigned int *)0xE000ED14", + "echo \\n", + "echo MSP=", + "output/x $msp", + "echo \\n", + "echo PSP=", + "output/x $psp", + "echo \\n", + "echo LR=", + "output/x $lr", + "echo \\n", + "echo PC=", + "output/x $pc", + "echo \\n", + "echo STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\\n", + "if ($lr & 4)", + " x/8wx $psp", + "else", + " x/8wx $msp", + "end", + "x/4wx 0xE000ED28", + "x/wx 0xE000ED38", + ] diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py new file mode 100644 index 0000000000..8f488d5bb5 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py @@ -0,0 +1,131 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Parse target result sentinels and Cortex-M fault diagnostics.""" + +import re + + +EXIT_SENTINEL_PREFIX = "[[MLKEM-EXIT:" +EXIT_SENTINEL_SUFFIX = "]]" +LAYOUT_FAIL_SENTINEL = "[[NUCLEO-LAYOUT-FAIL]]" +HARDFAULT_SENTINEL = "[[NUCLEO-HARDFAULT]]" + + +def gdb_load_failed(gdb_text: str) -> bool: + """Return whether GDB reported a failed ``load`` command.""" + return re.search(r"\bload\s+failed\b", gdb_text, re.IGNORECASE) is not None + + +def gdb_load_failed_before_target_output( + gdb_text: str, + *, + target_output_observed=False, + exit_code_observed=False, +) -> bool: + """Return whether a GDB load failure is safe to recover with FLEXMEM.""" + exit_sentinel_in_gdb = EXIT_SENTINEL_PREFIX in gdb_text + return gdb_load_failed(gdb_text) and not target_output_observed and not exit_code_observed and not exit_sentinel_in_gdb + + +def decode_cfsr(cfsr: int): + """Return names of set Configurable Fault Status Register bits.""" + bits = [ + (0, "IACCVIOL"), + (1, "DACCVIOL"), + (3, "MUNSTKERR"), + (4, "MSTKERR"), + (5, "MLSPERR"), + (7, "MMARVALID"), + (8, "IBUSERR"), + (9, "PRECISERR"), + (10, "IMPRECISERR"), + (11, "UNSTKERR"), + (12, "STKERR"), + (13, "LSPERR"), + (15, "BFARVALID"), + (16, "UNDEFINSTR"), + (17, "INVSTATE"), + (18, "INVPC"), + (19, "NOCP"), + (24, "UNALIGNED"), + (25, "DIVBYZERO"), + ] + return [name for bit, name in bits if cfsr & (1 << bit)] + + +def decode_hfsr(hfsr: int): + """Return names of set HardFault Status Register bits.""" + bits = [(1, "VECTTBL"), (30, "FORCED"), (31, "DEBUGEVT")] + return [name for bit, name in bits if hfsr & (1 << bit)] + + +def parse_exit_sentinel(line: str): + """Parse a ``[[MLKEM-EXIT:]]`` line into ``(matched, rc)``.""" + stripped = line.strip() + if not stripped.startswith(EXIT_SENTINEL_PREFIX) or not stripped.endswith(EXIT_SENTINEL_SUFFIX): + return False, None + try: + return True, int(stripped[len(EXIT_SENTINEL_PREFIX):-len(EXIT_SENTINEL_SUFFIX)]) + except Exception: + return True, 1 + + +def split_stdout_capture(captured: bytes): + """Decode captured stdout and remove any embedded ML-KEM exit sentinel.""" + captured_text = captured.decode("utf-8", errors="replace") + captured_lines = [] + exit_code = None + for capture_line in captured_text.splitlines(keepends=True): + is_exit, parsed_exit_code = parse_exit_sentinel(capture_line) + if is_exit: + exit_code = parsed_exit_code + continue + captured_lines.append(capture_line) + return "".join(captured_lines), exit_code + + +def fault_info_from_gdb(gdb_text: str) -> str: + """Format fault registers emitted by the GDB script into readable text.""" + values = {} + register_pattern = r"^(CFSR|HFSR|DFSR|MMFAR|BFAR|AFSR|SHCSR|CCR|MSP|PSP|LR|PC)=0x([0-9a-fA-F]+)$" + for name, value in re.findall(register_pattern, gdb_text, re.MULTILINE): + values[name] = int(value, 16) + + if not values: + return "" + + lines = ["Fault registers:"] + for name in ("CFSR", "HFSR", "DFSR", "MMFAR", "BFAR", "AFSR", "SHCSR", "CCR", "MSP", "PSP", "LR", "PC"): + if name in values: + lines.append(f" {name}=0x{values[name]:08x}") + + cfsr_bits = decode_cfsr(values.get("CFSR", 0)) + hfsr_bits = decode_hfsr(values.get("HFSR", 0)) + if cfsr_bits: + lines.append(" CFSR bits: " + ", ".join(cfsr_bits)) + if hfsr_bits: + lines.append(" HFSR bits: " + ", ".join(hfsr_bits)) + + # The stack dump follows a marker printed by the GDB script. Keep parsing + # permissive because GDB may format the memory rows differently by version. + stacked = re.search( + r"^STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\s*\n((?:0x[0-9a-fA-F]+:\s+.*\n?)?)", + gdb_text, + re.MULTILINE, + ) + if stacked: + stack_lines = [line.strip() for line in stacked.group(1).splitlines() if line.strip()] + if stack_lines: + lines.append(" stacked frame dump:") + lines.extend(f" {line}" for line in stack_lines) + + return "\n".join(lines) + + +def gdb_observed_hardfault(gdb_text: str) -> bool: + """Return whether GDB output shows the target entered HardFault_Handler.""" + return HARDFAULT_SENTINEL in gdb_text or re.search( + r"^HardFault_Handler \(\)", gdb_text, re.MULTILINE + ) is not None diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py new file mode 100644 index 0000000000..45c44c5429 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py @@ -0,0 +1,98 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Locate STM32CubeCLT tools and build common ST-LINK command arguments.""" + +import os +import shutil +import subprocess + + +def find_cubeprogrammer_cli(cp_path="", st_clt_root=""): + """Find ``STM32_Programmer_CLI`` from explicit paths, CLT root, or PATH.""" + candidates = [] + if cp_path: + if os.path.isdir(cp_path): + candidates.extend([ + os.path.join(cp_path, "STM32_Programmer_CLI"), + os.path.join(cp_path, "bin", "STM32_Programmer_CLI"), + ]) + else: + candidates.append(cp_path) + if st_clt_root: + candidates.append(os.path.join(st_clt_root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI")) + path_candidate = shutil.which("STM32_Programmer_CLI") + if path_candidate: + candidates.append(path_candidate) + for candidate in candidates: + if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return None + + +def connect_args(speed="200", serial="", apid="", mode=None): + """Return CubeProgrammer ``-c`` arguments for one SWD connection.""" + args = ["-c", "port=SWD", f"freq={speed}"] + if mode: + args.append(f"mode={mode}") + if serial: + args.append(f"sn={serial}") + if apid: + args.append(f"ap={apid}") + return args + + +def run_quiet(cmd): + """Run a command with stdout and stderr merged for delayed diagnostics.""" + return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + + +def derive_clt_root(st_cubeprog: str): + """Infer the STM32CubeCLT root from a CubeProgrammer path if possible.""" + if not st_cubeprog: + return "" + st_clt_root = os.path.dirname(os.path.abspath(st_cubeprog)) + if os.path.basename(st_clt_root).lower().startswith("stm32cubeprogrammer"): + st_clt_root = os.path.dirname(st_clt_root) + return st_clt_root + + +def find_stlink_gdbserver(st_clt_root=""): + """Find ``ST-LINK_gdbserver`` on PATH or below the provided CLT root.""" + stlink_bin = shutil.which("ST-LINK_gdbserver") + candidate = None + if not stlink_bin and st_clt_root: + candidate = os.path.join(st_clt_root, "STLink-gdb-server", "bin", "ST-LINK_gdbserver") + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + stlink_bin = candidate + return stlink_bin, candidate + + +def cubeprogrammer_cp_path(st_cubeprog="", st_clt_root="", stlink_bin=""): + """Return the directory to pass as ST-LINK GDB server's ``-cp`` value.""" + cp_path = None + if st_cubeprog: + path = os.path.abspath(st_cubeprog) + if os.path.isdir(path): + cli1 = os.path.join(path, "STM32_Programmer_CLI") + cli2 = os.path.join(path, "bin", "STM32_Programmer_CLI") + if os.path.isfile(cli1) and os.access(cli1, os.X_OK): + cp_path = path + elif os.path.isfile(cli2) and os.access(cli2, os.X_OK): + cp_path = os.path.join(path, "bin") + elif os.path.isfile(path): + cp_path = os.path.dirname(path) + + if cp_path is None and st_clt_root: + cli2 = os.path.join(os.path.abspath(st_clt_root), "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") + if os.path.isfile(cli2) and os.access(cli2, os.X_OK): + cp_path = os.path.dirname(cli2) + + if cp_path is None and stlink_bin: + root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(stlink_bin)))) + cli2 = os.path.join(root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") + if os.path.isfile(cli2) and os.access(cli2, os.X_OK): + cp_path = os.path.dirname(cli2) + + return cp_path diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py new file mode 100644 index 0000000000..dc9febacbb --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py @@ -0,0 +1,68 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Resolve symbols from ARM ELF files using ``nm`` and ``readelf`` output.""" + +import shutil +import subprocess + + +def default_readelf(): + """Return the preferred readelf executable name available on this host.""" + return shutil.which("arm-none-eabi-readelf") or shutil.which("readelf") or "readelf" + + +def resolve_symbol(elf_path: str, symbol: str, nm="arm-none-eabi-nm", readelf=None): + """Resolve ``symbol`` to a hex address, trying ``nm`` before ``readelf``.""" + addr = resolve_symbol_with_nm(elf_path, symbol, nm) + if addr is not None: + return addr + return resolve_symbol_with_readelf(elf_path, symbol, readelf or default_readelf()) + + +def resolve_symbol_with_nm(elf_path: str, symbol: str, nm="arm-none-eabi-nm"): + """Resolve ``symbol`` with ``nm -n`` and return ``None`` on any failure.""" + try: + cp = subprocess.run([nm, "-n", elf_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + except OSError: + return None + if cp.returncode != 0: + return None + return parse_nm_symbol(cp.stdout, symbol) + + +def parse_nm_symbol(output: str, symbol: str): + """Parse one symbol address from ``nm -n`` output.""" + for line in output.splitlines(): + parts = line.strip().split() + if len(parts) >= 3 and parts[-1] == symbol: + addr_hex = parts[0] + if not addr_hex.startswith("0x"): + addr_hex = "0x" + addr_hex + return addr_hex + return None + + +def resolve_symbol_with_readelf(elf_path: str, symbol: str, readelf=None): + """Resolve ``symbol`` with ``readelf -s`` and return ``None`` on failure.""" + try: + cp = subprocess.run([readelf or default_readelf(), "-s", elf_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + except OSError: + return None + if cp.returncode != 0: + return None + return parse_readelf_symbol(cp.stdout, symbol) + + +def parse_readelf_symbol(output: str, symbol: str): + """Parse one symbol address from ``readelf -s`` output.""" + for line in output.splitlines(): + if symbol not in line: + continue + fields = line.split() + if len(fields) >= 8 and fields[-1] == symbol: + val = fields[1] + if all(char in "0123456789abcdefABCDEF" for char in val): + return "0x" + val + return None diff --git a/test/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk index 11320ac765..e67aa318c3 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/platform.mk +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -131,7 +131,7 @@ FLEXMEM_CONFIG_SOURCES := \ .PHONY: flexmem_config run_flexmem_config run_flexmem_test -run_kat_512 run_kat_768 run_kat_1024 run_acvp: run_flexmem_config +run_kat run_acvp run_bench run_func run_unit run_alloc: run_flexmem_config flexmem_config: $(FLEXMEM_CONFIG_ELF) diff --git a/test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py b/test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py index 4a0c397b0f..d013afd8be 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py +++ b/test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py @@ -1,14 +1,20 @@ #!/usr/bin/env python3 # Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT +"""Compatibility entry point for running a test after FLEXMEM configuration.""" + import os import sys def main(): + """Replace this process with ``exec_wrapper.py`` while preserving argv.""" here = os.path.dirname(os.path.abspath(__file__)) wrapper = os.path.join(here, "exec_wrapper.py") + # os.execv keeps make/CI process trees simple: this shim does not need to + # proxy signals, streams, or exit status from a child process. os.execv(sys.executable, [sys.executable, wrapper] + sys.argv[1:]) diff --git a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py new file mode 100644 index 0000000000..9c125255f3 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py @@ -0,0 +1,230 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Host-only regression tests for NUCLEO-N657X0-Q helper modules.""" + +import os +import struct +import unittest +from unittest import mock + +import exec_wrapper +from nucleo_host.argv_blob import ARGV_BLOCK_SIZE, pack_cmdline +from nucleo_host.gdb_script import build_run_script, restore_argv_command +from nucleo_host.results import fault_info_from_gdb +from nucleo_host.results import gdb_load_failed +from nucleo_host.results import gdb_load_failed_before_target_output +from nucleo_host.results import parse_exit_sentinel +from nucleo_host.results import split_stdout_capture +from nucleo_host.symbols import parse_nm_symbol, parse_readelf_symbol + + +class NucleoHostTest(unittest.TestCase): + """Exercise debugger-independent helper behavior without board access.""" + + def test_pack_cmdline_uses_absolute_string_pointers(self): + """The argv table contains absolute target pointers to inline strings.""" + blob = pack_cmdline(["prog", "--flag"], 0x20000000) + + argc, arg0, arg1 = struct.unpack_from(" Date: Tue, 5 May 2026 14:24:42 +0100 Subject: [PATCH 09/20] nucleo-n657x0-q: add flexmem build hints Remove the unused ST LRUN linker-script copy and D-TCM stack patching from the Nix platform package now that the platform uses repository-owned linker scripts. Add a shared FLEXMEM diagnostic helper and use it from both exec_wrapper.py and flexmem_configure.py so missing flexmem_config.elf errors explain how to build and run the config target. Cover the new diagnostics with host-side unit tests. Signed-off-by: Brendan Moran --- nix/nucleo-n657x0-q/default.nix | 31 +-------------- .../platform/nucleo-n657x0-q/exec_wrapper.py | 2 + .../nucleo-n657x0-q/flexmem_configure.py | 2 + .../nucleo-n657x0-q/nucleo_host/flexmem.py | 20 ++++++++++ .../nucleo-n657x0-q/test_nucleo_host.py | 39 +++++++++++++++++++ 5 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 test/baremetal/platform/nucleo-n657x0-q/nucleo_host/flexmem.py diff --git a/nix/nucleo-n657x0-q/default.nix b/nix/nucleo-n657x0-q/default.nix index 011b719a46..4a497e81cd 100644 --- a/nix/nucleo-n657x0-q/default.nix +++ b/nix/nucleo-n657x0-q/default.nix @@ -49,18 +49,16 @@ stdenvNoCC.mkDerivation { fi cp -r "Drivers/CMSIS/Device/ST" "$outp/Drivers/CMSIS/Device/" - # Copy startup + system + linker script + # Copy startup + system files # Prefer board-specific FSBL-LRUN files explicitly when available. fsbl_tpl="Projects/NUCLEO-N657X0-Q/Templates/Template_FSBL_LRUN" - mkdir -p "$outp/gcc" "$outp/gcc/linker" + mkdir -p "$outp/gcc" if [ -d "$fsbl_tpl" ]; then # Explicitly select: # - Startup: STM32CubeIDE/Boot/Startup/startup_stm32n657xx_fsbl.s - # - Linker: STM32CubeIDE/AppS/STM32N657XX_LRUN.ld # - System: FSBL/Src/system_stm32n6xx_fsbl.c # - IT/MSP: FSBL/Src/stm32n6xx_it.c, stm32n6xx_hal_msp.c start_src="$fsbl_tpl/STM32CubeIDE/Boot/Startup/startup_stm32n657xx_fsbl.s" - ld_src="$fsbl_tpl/STM32CubeIDE/AppS/STM32N657XX_LRUN.ld" sys_src="$fsbl_tpl/FSBL/Src/system_stm32n6xx_fsbl.c" it_src="$fsbl_tpl/FSBL/Src/stm32n6xx_it.c" msp_src="$fsbl_tpl/FSBL/Src/stm32n6xx_hal_msp.c" @@ -77,31 +75,6 @@ stdenvNoCC.mkDerivation { # has initialized the test image. sed -i.bak -E '/^Reset_Handler:/a\ cpsid i' "$outp/gcc/startup_stm32n657xx.S" fi - if [ -f "$ld_src" ]; then - cp -v "$ld_src" "$outp/gcc/linker/STM32N657XX_LRUN.ld" - # Patch LRUN linker script to set initial stack pointer into D-TCM (256 KiB window) - ld_file="$outp/gcc/linker/STM32N657XX_LRUN.ld" - if [ -f "$ld_file" ]; then - echo "Patching LRUN linker script for D-TCM stack (MSP/MSPLIM in 0x30000000-0x30040000)" - # Common patterns: direct assignments or PROVIDE() wrappers; handle both if present - sed -i.bak -E 's@PROVIDE\(\s*_estack\s*=\s*[^;]+\);@PROVIDE(_estack = 0x30040000);@' "$ld_file" || true - sed -i.bak -E 's@PROVIDE\(\s*__StackTop\s*=\s*[^;]+\);@PROVIDE(__StackTop = 0x30040000);@' "$ld_file" || true - sed -i.bak -E 's@PROVIDE\(\s*__initial_sp\s*=\s*[^;]+\);@PROVIDE(__initial_sp = 0x30040000);@' "$ld_file" || true - sed -i.bak -E 's@PROVIDE\(\s*__StackLimit\s*=\s*[^;]+\);@PROVIDE(__StackLimit = 0x30000000);@' "$ld_file" || true - sed -i.bak -E 's@(^|[^A-Za-z_])(_estack)\s*=\s*0x[0-9A-Fa-f]+@\1\2 = 0x30040000@g' "$ld_file" || true - sed -i.bak -E 's@(^|[^A-Za-z_])(__StackTop)\s*=\s*0x[0-9A-Fa-f]+@\1\2 = 0x30040000@g' "$ld_file" || true - sed -i.bak -E 's@(^|[^A-Za-z_])( __initial_sp|__initial_sp)\s*=\s*0x[0-9A-Fa-f]+@\1__initial_sp = 0x30040000@g' "$ld_file" || true - sed -i.bak -E 's@(^|[^A-Za-z_])( __StackLimit|__StackLimit)\s*=\s*0x[0-9A-Fa-f]+@\1__StackLimit = 0x30000000@g' "$ld_file" || true - # Append fallback PROVIDE definitions (won't override explicit definitions) - if ! grep -q "__StackTop" "$ld_file"; then - printf '\n/* D-TCM stack placement for LRUN */\n' >> "$ld_file" - printf 'PROVIDE(_estack = 0x30040000);\n' >> "$ld_file" - printf 'PROVIDE(__StackTop = _estack);\n' >> "$ld_file" - printf 'PROVIDE(__initial_sp = _estack);\n' >> "$ld_file" - printf 'PROVIDE(__StackLimit = 0x30000000);\n' >> "$ld_file" - fi - fi - fi if [ -f "$sys_src" ]; then cp -v "$sys_src" "$outp/system_stm32n6xx.c" fi diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index 0b00fb3243..f289c75244 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -28,6 +28,7 @@ import threading from nucleo_host.argv_blob import pack_cmdline +from nucleo_host.flexmem import flexmem_config_build_instructions from nucleo_host.gdb_script import build_run_script from nucleo_host.results import LAYOUT_FAIL_SENTINEL from nucleo_host.results import fault_info_from_gdb @@ -130,6 +131,7 @@ def _recover_flexmem(reason: str, failure_message: str) -> bool: return False if not os.path.exists(config_elf): err(f"FLEXMEM config ELF not found: {config_elf}") + err(flexmem_config_build_instructions(config_elf)) return False info(f"[exec_wrapper] recovering from {reason}: re-running FLEXMEM config") diff --git a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py index 0ab4b8502a..e057dcdb15 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py +++ b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py @@ -21,6 +21,7 @@ from nucleo_host.st_tools import connect_args as st_connect_args from nucleo_host.st_tools import find_cubeprogrammer_cli as st_find_cubeprogrammer_cli from nucleo_host.st_tools import run_quiet +from nucleo_host.flexmem import flexmem_config_build_instructions from nucleo_host.symbols import resolve_symbol_with_nm DONE = "FLEXMEM configuration complete; reset target and load test binary." @@ -130,6 +131,7 @@ def main(): elf = os.path.abspath(sys.argv[1]) if not os.path.exists(elf): err(f"Config ELF not found: {elf}") + err(flexmem_config_build_instructions(elf)) return 2 cli = cubeprogrammer_cli() diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/flexmem.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/flexmem.py new file mode 100644 index 0000000000..327a8ab98c --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/flexmem.py @@ -0,0 +1,20 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""User-facing helpers for NUCLEO FLEXMEM configuration.""" + + +PLATFORM_MK = "test/baremetal/platform/nucleo-n657x0-q/platform.mk" + + +def flexmem_config_build_instructions(config_elf: str) -> str: + """Return instructions for generating a missing FLEXMEM config ELF.""" + return "\n".join([ + "Build the FLEXMEM config ELF from the repository root with:", + f" make flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", + "Then configure the board with:", + f" make run_flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", + "If you override BUILD_DIR or FLEXMEM_CONFIG_ELF, pass the same override to the make command.", + f"Expected FLEXMEM config ELF: {config_elf}", + ]) diff --git a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py index 9c125255f3..4035d62c3a 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py +++ b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py @@ -10,7 +10,9 @@ from unittest import mock import exec_wrapper +import flexmem_configure from nucleo_host.argv_blob import ARGV_BLOCK_SIZE, pack_cmdline +from nucleo_host.flexmem import PLATFORM_MK, flexmem_config_build_instructions from nucleo_host.gdb_script import build_run_script, restore_argv_command from nucleo_host.results import fault_info_from_gdb from nucleo_host.results import gdb_load_failed @@ -93,6 +95,43 @@ def test_gdb_load_failed_requires_no_target_output(self): self.assertFalse(gdb_load_failed_before_target_output(gdb_text, exit_code_observed=True)) self.assertFalse(gdb_load_failed_before_target_output("Load failed\n[[MLKEM-EXIT:0]]\n")) + def test_flexmem_config_build_instructions_show_make_command(self): + """Missing config diagnostics explain how to build the helper ELF.""" + instructions = flexmem_config_build_instructions("/tmp/flexmem_config.elf") + + self.assertIn(f"make flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", instructions) + self.assertIn(f"make run_flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", instructions) + self.assertIn("/tmp/flexmem_config.elf", instructions) + + def test_load_failure_recovery_reports_build_hint_when_config_missing(self): + """The wrapper points users at flexmem_config when recovery cannot start.""" + messages = [] + env = {"FLEXMEM_CONFIG_ELF": "/tmp/missing_flexmem_config.elf"} + + def fake_exists(path): + return path.endswith("flexmem_configure.py") + + with mock.patch.dict(os.environ, env), \ + mock.patch.object(exec_wrapper.os.path, "exists", side_effect=fake_exists), \ + mock.patch.object(exec_wrapper, "err", side_effect=messages.append): + self.assertFalse(exec_wrapper._recover_after_load_failure()) + + self.assertIn("FLEXMEM config ELF not found: /tmp/missing_flexmem_config.elf", messages) + self.assertTrue(any("make flexmem_config" in message for message in messages)) + + def test_flexmem_configure_reports_build_hint_when_config_missing(self): + """Direct configure invocations also report the build command.""" + messages = [] + argv = ["flexmem_configure.py", "/tmp/missing_flexmem_config.elf"] + + with mock.patch.object(flexmem_configure.sys, "argv", argv), \ + mock.patch.object(flexmem_configure.os.path, "exists", return_value=False), \ + mock.patch.object(flexmem_configure, "err", side_effect=messages.append): + self.assertEqual(flexmem_configure.main(), 2) + + self.assertIn("Config ELF not found: /tmp/missing_flexmem_config.elf", messages) + self.assertTrue(any("make flexmem_config" in message for message in messages)) + def test_main_recovers_once_after_load_failure(self): """The wrapper invokes FLEXMEM configuration once before retrying.""" run_results = iter([23, 0]) From c95d72aad78da691399763b24f551f265d2d29b4 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Tue, 5 May 2026 14:44:05 +0100 Subject: [PATCH 10/20] Fix linting errors Signed-off-by: Brendan Moran --- .../platform/nucleo-n657x0-q/exec_wrapper.py | 348 +++++++++++++----- .../nucleo-n657x0-q/flexmem_configure.py | 81 ++-- .../platform/nucleo-n657x0-q/make_argv_bin.py | 6 +- .../nucleo-n657x0-q/nucleo_host/argv_blob.py | 15 +- .../nucleo-n657x0-q/nucleo_host/flexmem.py | 20 +- .../nucleo-n657x0-q/nucleo_host/gdb_script.py | 28 +- .../nucleo-n657x0-q/nucleo_host/results.py | 53 ++- .../nucleo-n657x0-q/nucleo_host/st_tools.py | 48 ++- .../nucleo-n657x0-q/nucleo_host/symbols.py | 32 +- .../nucleo-n657x0-q/test_nucleo_host.py | 208 +++++++---- 10 files changed, 612 insertions(+), 227 deletions(-) diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index f289c75244..9b852b2794 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -9,11 +9,11 @@ The wrapper expects ``flexmem_configure.py`` to have expanded ITCM/DTCM before normal runs. If the initial GDB ``load`` reports that loading failed before -target output starts, the wrapper can re-run FLEXMEM configuration and retry the -same ELF. Successful runs load the ELF into RAM, let C startup clear memory, -restore a packed argv blob at ``__wrap_main``, stream or dump target stdout, and -map target sentinels to the process exit status expected by the baremetal test -harness. +target output starts, the wrapper can re-run FLEXMEM configuration and retry +the same ELF. Successful runs load the ELF into RAM, let C startup clear +memory, restore a packed argv blob at ``__wrap_main``, stream or dump target +stdout, and map target sentinels to the process exit status expected by the +baremetal test harness. """ import logging @@ -42,7 +42,6 @@ from nucleo_host.symbols import default_readelf from nucleo_host.symbols import resolve_symbol - VERBOSE = False STDOUT_BYTES_EMITTED = 0 TARGET_FAILURE = False @@ -87,7 +86,7 @@ def run(cmd, **kwargs): def popen(cmd, **kwargs): - """Thin wrapper around ``subprocess.Popen`` for test-time monkeypatching.""" + """Wrap ``subprocess.Popen`` for test-time monkeypatching.""" return subprocess.Popen(cmd, **kwargs) @@ -116,15 +115,21 @@ def _wait_for_port(host: str, port: int, timeout_s: float) -> bool: def _default_flexmem_config_elf() -> str: """Return the default build path for the FLEXMEM configuration ELF.""" platform_dir = os.path.dirname(os.path.abspath(__file__)) - repo_root = os.path.abspath(os.path.join(platform_dir, "..", "..", "..", "..")) - return os.path.join(repo_root, "test", "build", "nucleo-n657x0-q", "flexmem_config.elf") + repo_root = os.path.abspath( + os.path.join(platform_dir, "..", "..", "..", "..") + ) + return os.path.join( + repo_root, "test", "build", "nucleo-n657x0-q", "flexmem_config.elf" + ) def _recover_flexmem(reason: str, failure_message: str) -> bool: - """Re-run FLEXMEM configuration after a retryable setup or target failure.""" + """Re-run FLEXMEM after a retryable setup or target failure.""" platform_dir = os.path.dirname(os.path.abspath(__file__)) configure_script = os.path.join(platform_dir, "flexmem_configure.py") - config_elf = os.environ.get("FLEXMEM_CONFIG_ELF", _default_flexmem_config_elf()) + config_elf = os.environ.get( + "FLEXMEM_CONFIG_ELF", _default_flexmem_config_elf() + ) if not os.path.exists(configure_script): err(f"FLEXMEM configure script not found: {configure_script}") @@ -155,16 +160,21 @@ def _recover_flexmem(reason: str, failure_message: str) -> bool: def _recover_after_hardfault() -> bool: """Re-run FLEXMEM configuration after a target HardFault retry trigger.""" - return _recover_flexmem("HardFault", "FLEXMEM reconfiguration after HardFault failed") + return _recover_flexmem( + "HardFault", "FLEXMEM reconfiguration after HardFault failed" + ) def _recover_after_load_failure() -> bool: """Re-run FLEXMEM configuration after a GDB load failure retry trigger.""" - return _recover_flexmem("GDB load failure", "FLEXMEM reconfiguration after GDB load failure failed") + return _recover_flexmem( + "GDB load failure", + "FLEXMEM reconfiguration after GDB load failure failed", + ) def _run_once(): - """Run the target ELF once and return the wrapper exit code for that run.""" + """Run the target ELF once and return its wrapper exit code.""" global VERBOSE global STDOUT_BYTES_EMITTED global TARGET_FAILURE @@ -210,29 +220,40 @@ def _run_once(): # Preferred: set ST_GDBSERVER_CMD as a template using Python format keys: # {port} {speed} {serial} {transport} {device} {connect} # Example (ST-LINK_gdbserver): - # export ST_GDBSERVER_CMD='ST-LINK_gdbserver -p {port} -f SWD -s {speed}k' + # export ST_GDBSERVER_CMD='ST-LINK_gdbserver -p {port} ...' # Example (STM32_Programmer_CLI): - # export ST_GDBSERVER_CMD='STM32_Programmer_CLI -c port=SWD{serial} -s {speed} -gdbserver port={port}' + # export ST_GDBSERVER_CMD='STM32_Programmer_CLI ...' st_gdbserver_cmd_tpl = os.environ.get("ST_GDBSERVER_CMD") - st_speed = os.environ.get("STLINK_SPEED", "200") # kHz (lower default for reliability) - st_serial = os.environ.get("STLINK_SERIAL", "") # optional, raw value + st_speed = os.environ.get( + "STLINK_SPEED", "200" + ) # kHz (lower default for reliability) + st_serial = os.environ.get("STLINK_SERIAL", "") # optional, raw value st_transport = os.environ.get("STLINK_TRANSPORT", "SWD") st_device = os.environ.get("ST_DEVICE", "STM32N657X0HxQ") st_connect = os.environ.get("STLINK_CONNECT_MODE", "under-reset") - st_cubeprog = os.environ.get("ST_CUBE_PROG_PATH", "") # Path to STM32CubeProgrammer - st_clt_root = os.environ.get("ST_CUBE_CLT_ROOT", "") # Root of STM32CubeCLT + st_cubeprog = os.environ.get( + "ST_CUBE_PROG_PATH", "" + ) # Path to STM32CubeProgrammer + st_clt_root = os.environ.get( + "ST_CUBE_CLT_ROOT", "" + ) # Root of STM32CubeCLT st_pend = os.environ.get("STLINK_PEND_HALT_TIMEOUT", "8000") st_apid = os.environ.get("STLINK_APID", "") gdb_run_timeout = float(os.environ.get("GDB_RUN_TIMEOUT", "180")) # Semihosting configuration (enabled by default) st_semihost_port_env = os.environ.get("STLINK_SEMIHOST_PORT", "") try: - st_semihost_port = int(st_semihost_port_env) if st_semihost_port_env else _pick_free_port() + st_semihost_port = ( + int(st_semihost_port_env) + if st_semihost_port_env + else _pick_free_port() + ) except Exception: st_semihost_port = _pick_free_port() st_semihost_level = os.environ.get("STLINK_SEMIHOST_LEVEL", "all") - # Address extraction for argv block symbol (numeric address avoids debugger symbol issues) + # Address extraction for argv block symbol. Numeric addresses avoid + # debugger symbol issues. arg_block_sym = os.environ.get("ARG_BLOCK_SYMBOL", "mlkem_cmdline_block") arg_block_addr = None @@ -248,7 +269,8 @@ def _resolve_symbol_addr(elf_path: str, sym: str): arg_block_addr = addr break - # Numeric breakpoints avoid GDB symbol lookup surprises after loading RAM ELFs. + # Numeric breakpoints avoid GDB symbol lookup surprises after loading + # RAM ELFs. wrap_main_addr = _resolve_symbol_addr(elf, "__wrap_main") wrap_main_break = "__wrap_main" if wrap_main_addr is not None: @@ -261,11 +283,18 @@ def _resolve_symbol_addr(elf_path: str, sym: str): err("Failed to resolve Reset_Handler in ELF.") return 2 - # Resolve the RAM stdout buffer so GDB can dump target output after execution. + # Resolve the RAM stdout buffer so GDB can dump target output after + # execution. stdout_capture_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture") - stdout_capture_len_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture_len") - stdout_capture_truncated_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture_truncated") - stdout_capture_size = int(os.environ.get("NUCLEO_STDOUT_CAPTURE_SIZE", str(1536 * 1024))) + stdout_capture_len_addr = _resolve_symbol_addr( + elf, "nucleo_stdout_capture_len" + ) + stdout_capture_truncated_addr = _resolve_symbol_addr( + elf, "nucleo_stdout_capture_truncated" + ) + stdout_capture_size = int( + os.environ.get("NUCLEO_STDOUT_CAPTURE_SIZE", str(1536 * 1024)) + ) # Allow override of base address via env (hex string) arg_block_addr_env = os.environ.get("ARG_BLOCK_ADDR") base_addr = None @@ -281,8 +310,14 @@ def _resolve_symbol_addr(elf_path: str, sym: str): base_addr = None if base_addr is None: - err("Failed to resolve base address of argv block (mlkem_cmdline_block/mlk_cmdline_block).") - err("- Ensure symbols are present in ELF, or set ARG_BLOCK_ADDR to the base address (hex).") + err( + "Failed to resolve base address of argv block " + "(mlkem_cmdline_block/mlk_cmdline_block)." + ) + err( + "- Ensure symbols are present in ELF, or set ARG_BLOCK_ADDR to " + "the base address (hex)." + ) return 2 try: @@ -304,19 +339,35 @@ def _resolve_symbol_addr(elf_path: str, sym: str): # Auto-detect a default template if not provided if not st_gdbserver_cmd_tpl and stlink_bin: - st_gdbserver_cmd_tpl = ( - f"{shlex.quote(stlink_bin)} -p {{port}} -l 1 -d -s --frequency {{speed}} {{serial_flag}} {{apid_flag}} {{cubeprog_flag}} --semihost-console-port {{semi_port}} --semihosting {{semi_level}} -g --halt --pend-halt-timeout {{pend}}" - ) + stlink_parts = [ + shlex.quote(stlink_bin), + "-p {port}", + "-l 1", + "-d", + "-s", + "--frequency {speed}", + "{serial_flag}", + "{apid_flag}", + "{cubeprog_flag}", + "--semihost-console-port {semi_port}", + "--semihosting {semi_level}", + "-g", + "--halt", + "--pend-halt-timeout {pend}", + ] + st_gdbserver_cmd_tpl = " ".join(stlink_parts) if st_gdbserver_cmd_tpl: # Determine best '-cp' path for STM32CubeProgrammer CLI - cp_path = cubeprogrammer_cp_path(st_cubeprog, st_clt_root, stlink_bin) + cp_path = cubeprogrammer_cp_path( + st_cubeprog, st_clt_root, stlink_bin + ) # Provide a flexible set of placeholders for various CLT tools. # - {serial} -> raw serial value (e.g. 303030303030) # - {serial_flag} -> '-i ' (ST-LINK_gdbserver) # - {serial_prog} -> 'sn=' (STM32_Programmer_CLI) - # - {serial_sn} -> ',sn=' (STM32_Programmer_CLI combined) + # - {serial_sn} -> ',sn=' (combined CLI arg) # - {speed} -> kHz value (e.g. 500) # - {port} -> GDB server port (e.g. 3333) # - {transport} -> SWD/JTAG (usually SWD) @@ -334,7 +385,15 @@ def _resolve_symbol_addr(elf_path: str, sym: str): "device": st_device, "connect": st_connect, "cubeprog": cp_path or st_cubeprog, - "cubeprog_flag": (f"-cp {shlex.quote(cp_path)}" if cp_path else (f"-cp {shlex.quote(st_cubeprog)}" if st_cubeprog else "")), + "cubeprog_flag": ( + f"-cp {shlex.quote(cp_path)}" + if cp_path + else ( + f"-cp {shlex.quote(st_cubeprog)}" + if st_cubeprog + else "" + ) + ), "pend": st_pend, "apid_flag": (f"-m {st_apid}" if st_apid else "-m 1"), "semi_port": st_semihost_port, @@ -350,13 +409,20 @@ def _resolve_symbol_addr(elf_path: str, sym: str): msg = ( "STM32Cube Command Line Tools required.\n" "- Install STM32CubeCLT (Linux/macOS).\n" - " Download: https://www.st.com/en/development-tools/stm32cubeclt.html\n" - "- Set ST_GDBSERVER_CMD to a working gdbserver command template, or ensure ST-LINK_gdbserver is on PATH.\n" + " Download: " + "https://www.st.com/en/development-tools/stm32cubeclt.html\n" + "- Set ST_GDBSERVER_CMD to a working gdbserver template, " + "or ensure ST-LINK_gdbserver is on PATH.\n" " Examples:\n" - " ST-LINK_gdbserver: 'ST-LINK_gdbserver -p {port} -d --frequency {speed} {serial_flag} -g --halt {cubeprog_flag}'\n" - " STM32_Programmer_CLI: 'STM32_Programmer_CLI -c port={transport},{serial_prog} -s {speed} -gdbserver port={port}'\n" - " Tip: If ST-LINK_gdbserver errors about STM32CubeProgrammer, set ST_CUBE_PROG_PATH to its installation path,\n" - " or export ST_CUBE_CLT_ROOT to the CubeCLT root so the wrapper can auto-locate ST-LINK_gdbserver.\n" + " ST-LINK_gdbserver: 'ST-LINK_gdbserver -p {port} " + "-d --frequency {speed} ...'\n" + " STM32_Programmer_CLI: 'STM32_Programmer_CLI " + "-c port={transport},{serial_prog} ...'\n" + " Tip: If ST-LINK_gdbserver errors about " + "STM32CubeProgrammer, " + "set ST_CUBE_PROG_PATH to its installation path,\n" + " or export ST_CUBE_CLT_ROOT to the CubeCLT root so the " + "wrapper can auto-locate ST-LINK_gdbserver.\n" ) # Append small diagnostics if we attempted a candidate path if candidate: @@ -366,7 +432,10 @@ def _resolve_symbol_addr(elf_path: str, sym: str): err(msg) return 2 - info("[exec_wrapper] assuming FLEXMEM was configured by flexmem_configure.py; no runtime TCM probing") + info( + "[exec_wrapper] assuming FLEXMEM was configured by " + "flexmem_configure.py; no runtime TCM probing" + ) info(f"[exec_wrapper] starting ST gdbserver on port {port}...") info(f"[exec_wrapper] {' '.join(gdbserver_cmd)}") @@ -380,12 +449,18 @@ def _resolve_symbol_addr(elf_path: str, sym: str): ) try: - # Wait for semihost console to become available and connect before attaching GDB + # Wait for semihost console to become available and connect before + # attaching GDB. # First, ensure the process is alive time.sleep(0.2) - # Then, wait for the semihost port to accept connections (up to 10s) - if not _wait_for_port("127.0.0.1", st_semihost_port, timeout_s=10.0): - info("[exec_wrapper] semihost port not ready within timeout; continuing anyway") + # Then wait for the semihost port to accept connections. + if not _wait_for_port( + "127.0.0.1", st_semihost_port, timeout_s=10.0 + ): + info( + "[exec_wrapper] semihost port not ready within timeout; " + "continuing anyway" + ) semihost_sock = None semihost_stop = threading.Event() @@ -394,7 +469,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): shared = {"exit_code": None, "stdout_streamed": False} def _semihost_reader(sock: socket.socket): - """Stream semihost console output and detect the exit sentinel.""" + """Stream semihost output and detect the exit sentinel.""" global STDOUT_BYTES_EMITTED buf = b"" @@ -408,11 +483,15 @@ def _semihost_reader(sock: socket.socket): while b"\n" in buf: line, buf = buf.split(b"\n", 1) try: - text = line.decode("utf-8", errors="replace") + text = line.decode( + "utf-8", errors="replace" + ) except Exception: text = line.decode(errors="replace") # Detect exit sentinel first - is_exit, parsed_exit_code = parse_exit_sentinel(text) + is_exit, parsed_exit_code = ( + parse_exit_sentinel(text) + ) if is_exit: shared["exit_code"] = parsed_exit_code semihost_exit.set() @@ -439,13 +518,23 @@ def _semihost_reader(sock: socket.socket): # Attempt to connect the listener (non-blocking retries) try: - semihost_sock = socket.create_connection(("127.0.0.1", st_semihost_port), timeout=1.0) + semihost_sock = socket.create_connection( + ("127.0.0.1", st_semihost_port), timeout=1.0 + ) semihost_sock.settimeout(0.5) - semihost_thr = threading.Thread(target=_semihost_reader, args=(semihost_sock,), daemon=True) + semihost_thr = threading.Thread( + target=_semihost_reader, args=(semihost_sock,), daemon=True + ) semihost_thr.start() - info(f"[exec_wrapper] semihost listener connected on port {st_semihost_port}") + info( + "[exec_wrapper] semihost listener connected on port " + f"{st_semihost_port}" + ) except OSError: - info(f"[exec_wrapper] semihost listener not connected (port {st_semihost_port}); proceeding") + info( + "[exec_wrapper] semihost listener not connected " + f"(port {st_semihost_port}); proceeding" + ) # Give the server a brief moment, then check for early exit time.sleep(0.8) @@ -453,24 +542,43 @@ def _semihost_reader(sock: socket.socket): # Server exited early – surface a helpful message out_rem = stp.stdout.read() if stp.stdout else "" if out_rem and not SUPPRESS_RETRYABLE_DIAGNOSTICS: - log_output(out_rem, logging.DEBUG if VERBOSE else logging.ERROR) + log_output( + out_rem, logging.DEBUG if VERBOSE else logging.ERROR + ) merged = out_rem low = merged.lower() if "firmware upgrade" in low or "upgrade required" in low: # Try to suggest STLinkUpgrade locations hints = [] if st_clt_root: - app1 = os.path.join(st_clt_root, "STM32CubeProgrammer", "stlink", "STLinkUpgrade") - app2 = os.path.join(st_clt_root, "STM32CubeProgrammer", "stlink", "STLinkUpgrade.app") + app1 = os.path.join( + st_clt_root, + "STM32CubeProgrammer", + "stlink", + "STLinkUpgrade", + ) + app2 = os.path.join( + st_clt_root, + "STM32CubeProgrammer", + "stlink", + "STLinkUpgrade.app", + ) if os.path.exists(app1): hints.append(app1) if os.path.exists(app2): hints.append(app2) if VERBOSE: - err("[exec_wrapper] ST-LINK firmware upgrade required. Please run the STLinkUpgrade tool.") + err( + "[exec_wrapper] ST-LINK firmware upgrade " + "required. " + "Please run the STLinkUpgrade tool." + ) if hints: for h in hints: - err(f"[exec_wrapper] STLinkUpgrade candidate: {h}") + err( + "[exec_wrapper] STLinkUpgrade candidate: " + + h + ) return 2 gdb_lines = build_run_script( @@ -489,26 +597,42 @@ def _semihost_reader(sock: socket.socket): if VERBOSE: LOG.debug("============ GDB SCRIPT ============") - log_output('\n'.join(gdb_lines), logging.DEBUG) + log_output("\n".join(gdb_lines), logging.DEBUG) LOG.debug("====================================") - with tempfile.NamedTemporaryFile("w", delete=False, suffix=".gdb") as gs: + with tempfile.NamedTemporaryFile( + "w", delete=False, suffix=".gdb" + ) as gs: for line in gdb_lines: gs.write(line + "\n") gdb_script_path = gs.name gdb_cmd = [gdb, "--batch", "-x", gdb_script_path, elf] - # Run GDB while streaming gdbserver output (which will include semihost output). - info("[exec_wrapper] running gdb batch (program will continue; semihost output follows)...") - gdbp = popen(gdb_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - gdb_deadline = time.time() + gdb_run_timeout if gdb_run_timeout > 0 else None + # Run GDB while streaming gdbserver output, including semihost + # output. + info( + "[exec_wrapper] running gdb batch; semihost output follows" + ) + gdbp = popen( + gdb_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + gdb_deadline = ( + time.time() + gdb_run_timeout if gdb_run_timeout > 0 else None + ) - # Stream gdbserver output until gdb finishes without blocking on readline() + # Stream gdbserver output until gdb finishes without blocking on + # readline(). while True: # Early shutdown if exit sentinel observed if semihost_exit.is_set(): - info("[exec_wrapper] exit sentinel detected; shutting down gdb and gdbserver...") + info( + "[exec_wrapper] exit sentinel detected; shutting " + "down gdb and gdbserver..." + ) try: if gdbp.poll() is None: gdbp.terminate() @@ -525,7 +649,8 @@ def _semihost_reader(sock: socket.socket): if r: line = stp.stdout.readline() if line: - # gdbserver stdout is logged only in verbose mode. + # gdbserver stdout is logged only in verbose + # mode. if VERBOSE: log_output(line, logging.DEBUG) except Exception: @@ -537,7 +662,9 @@ def _semihost_reader(sock: socket.socket): if gdb_deadline is not None and time.time() > gdb_deadline: if not SUPPRESS_RETRYABLE_DIAGNOSTICS: err("FAIL!") - err(f"gdb batch timed out after {gdb_run_timeout:.0f}s") + err( + f"gdb batch timed out after {gdb_run_timeout:.0f}s" + ) try: gdbp.terminate() gdbp.wait(timeout=1.0) @@ -570,24 +697,39 @@ def _semihost_reader(sock: socket.socket): if os.path.exists(stdout_capture_bin): try: - # Parse the same exit sentinel from dumped RAM output as from semihosting. + # Parse the same exit sentinel from dumped RAM output as + # from semihosting. with open(stdout_capture_bin, "rb") as capture_file: captured = capture_file.read() - captured_output, captured_exit_code = split_stdout_capture(captured) + captured_output, captured_exit_code = split_stdout_capture( + captured + ) if captured_exit_code is not None: shared["exit_code"] = captured_exit_code - if captured_output and not shared.get("stdout_streamed") and not target_failed: + if ( + captured_output + and not shared.get("stdout_streamed") + and not target_failed + ): sys.stdout.write(captured_output) sys.stdout.flush() - STDOUT_BYTES_EMITTED += len(captured_output.encode("utf-8")) + STDOUT_BYTES_EMITTED += len( + captured_output.encode("utf-8") + ) except Exception as exc: - info(f"[exec_wrapper] failed to read stdout capture: {exc}") + info( + f"[exec_wrapper] failed to read stdout capture: {exc}" + ) if "$nucleo_stdout_truncated = 0x1" in gdb_text: err("WARNING: target stdout capture truncated") if shared.get("exit_code") is not None: - return int(shared["exit_code"]) if isinstance(shared["exit_code"], int) else 1 + return ( + int(shared["exit_code"]) + if isinstance(shared["exit_code"], int) + else 1 + ) if layout_failed: TARGET_FAILURE = True @@ -609,11 +751,17 @@ def _semihost_reader(sock: socket.socket): return 1 if "Program received signal SIGTRAP" in gdb_text: - info("[exec_wrapper] completion trap observed without exit sentinel") + info( + "[exec_wrapper] completion trap observed without exit " + "sentinel" + ) return 0 if gdbp.returncode != 0: - target_output_observed = bool(shared.get("stdout_streamed")) or STDOUT_BYTES_EMITTED != 0 + target_output_observed = ( + bool(shared.get("stdout_streamed")) + or STDOUT_BYTES_EMITTED != 0 + ) exit_code_observed = shared.get("exit_code") is not None if gdb_load_failed_before_target_output( gdb_text, @@ -647,17 +795,17 @@ def _semihost_reader(sock: socket.socket): pass # Stop semihost listener try: - if 'semihost_stop' in locals(): + if "semihost_stop" in locals(): semihost_stop.set() - if 'semihost_sock' in locals() and semihost_sock: + if "semihost_sock" in locals() and semihost_sock: semihost_sock.close() - if 'semihost_thr' in locals() and semihost_thr: + if "semihost_thr" in locals() and semihost_thr: semihost_thr.join(timeout=0.5) except Exception: pass # Remove the temp gdb script try: - if 'gdb_script_path' in locals(): + if "gdb_script_path" in locals(): os.unlink(gdb_script_path) except Exception: pass @@ -669,8 +817,12 @@ def main(): global LAST_FAULT_DIAGNOSTICS attempts = max(1, int(os.environ.get("GDB_RUN_ATTEMPTS", "2"))) - hardfault_attempts = max(0, int(os.environ.get("GDB_HARDFAULT_RECOVERY_ATTEMPTS", "1"))) - load_failure_attempts = max(0, int(os.environ.get("GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS", "1"))) + hardfault_attempts = max( + 0, int(os.environ.get("GDB_HARDFAULT_RECOVERY_ATTEMPTS", "1")) + ) + load_failure_attempts = max( + 0, int(os.environ.get("GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS", "1")) + ) transport_retries = 0 hardfault_recoveries = 0 load_failure_recoveries = 0 @@ -679,8 +831,12 @@ def main(): while True: can_retry_transport = transport_retries < attempts - 1 can_retry_hardfault = hardfault_recoveries < hardfault_attempts - can_retry_load_failure = load_failure_recoveries < load_failure_attempts - SUPPRESS_RETRYABLE_DIAGNOSTICS = can_retry_transport or can_retry_hardfault + can_retry_load_failure = ( + load_failure_recoveries < load_failure_attempts + ) + SUPPRESS_RETRYABLE_DIAGNOSTICS = ( + can_retry_transport or can_retry_hardfault + ) last_rc = _run_once() if last_rc == 0: return 0 @@ -690,8 +846,10 @@ def main(): if _recover_after_load_failure(): if VERBOSE: err( - "[exec_wrapper] retrying after recovered GDB load failure " - f"({load_failure_recoveries}/{load_failure_attempts})" + "[exec_wrapper] retrying after recovered GDB " + "load failure " + f"({load_failure_recoveries}/" + f"{load_failure_attempts})" ) time.sleep(0.5) continue @@ -708,18 +866,30 @@ def main(): hardfault_recoveries += 1 if _recover_after_hardfault(): if VERBOSE: - err(f"[exec_wrapper] retrying after recovered HardFault ({hardfault_recoveries}/{hardfault_attempts})") + err( + "[exec_wrapper] retrying after recovered " + "HardFault " + f"({hardfault_recoveries}/" + f"{hardfault_attempts})" + ) time.sleep(0.5) continue if LAST_FAULT_DIAGNOSTICS: err("HardFault diagnostics from failed run:") err(LAST_FAULT_DIAGNOSTICS) return last_rc - if TARGET_FAILURE or STDOUT_BYTES_EMITTED != 0 or not can_retry_transport: + if ( + TARGET_FAILURE + or STDOUT_BYTES_EMITTED != 0 + or not can_retry_transport + ): return last_rc transport_retries += 1 if VERBOSE: - err(f"[exec_wrapper] retrying after transport failure ({transport_retries}/{attempts - 1})") + err( + "[exec_wrapper] retrying after transport failure " + f"({transport_retries}/{attempts - 1})" + ) time.sleep(0.5) diff --git a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py index e057dcdb15..a57e158b21 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py +++ b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py @@ -6,10 +6,10 @@ """ Configure STM32N6 FLEXMEM before loading RAM-resident test images. -The helper downloads ``flexmem_config.elf`` into the reset-time memory layout, -starts it directly from RAM, polls ``SYSCFG->CM55TCMCR`` until the requested TCM -split is visible over SWD, then resets the target so the next ELF can be loaded -into the expanded ITCM/DTCM layout. +The helper downloads ``flexmem_config.elf`` into the reset-time memory layout +and starts it directly from RAM. It polls ``SYSCFG->CM55TCMCR`` until the +requested TCM split is visible over SWD, then resets the target so the next ELF +can be loaded into the expanded ITCM/DTCM layout. """ import os @@ -19,7 +19,9 @@ import time from nucleo_host.st_tools import connect_args as st_connect_args -from nucleo_host.st_tools import find_cubeprogrammer_cli as st_find_cubeprogrammer_cli +from nucleo_host.st_tools import ( + find_cubeprogrammer_cli as st_find_cubeprogrammer_cli, +) from nucleo_host.st_tools import run_quiet from nucleo_host.flexmem import flexmem_config_build_instructions from nucleo_host.symbols import resolve_symbol_with_nm @@ -37,7 +39,9 @@ def configure_logging(): """Configure logging, using ``FLEXMEM_VERBOSE`` as the debug switch.""" - level = logging.DEBUG if os.environ.get("FLEXMEM_VERBOSE") else logging.INFO + level = ( + logging.DEBUG if os.environ.get("FLEXMEM_VERBOSE") else logging.INFO + ) logging.basicConfig(level=level, format="%(message)s") @@ -56,14 +60,19 @@ def err(msg): def find_cubeprogrammer_cli(cp_path): """Find ``STM32_Programmer_CLI`` using platform environment defaults.""" - return st_find_cubeprogrammer_cli(cp_path, os.environ.get("ST_CUBE_CLT_ROOT", "")) + return st_find_cubeprogrammer_cli( + cp_path, os.environ.get("ST_CUBE_CLT_ROOT", "") + ) def cubeprogrammer_cli(): - """Return the CubeProgrammer CLI path, reporting a helpful error if absent.""" + """Return the CubeProgrammer CLI path, or report a helpful error.""" cli = find_cubeprogrammer_cli(os.environ.get("ST_CUBE_PROG_PATH", "")) if cli is None: - err("STM32_Programmer_CLI not found; set ST_CUBE_PROG_PATH or ST_CUBE_CLT_ROOT") + err( + "STM32_Programmer_CLI not found; set ST_CUBE_PROG_PATH or " + "ST_CUBE_CLT_ROOT" + ) return cli @@ -74,13 +83,16 @@ def connect_args(mode=None): speed=os.environ.get("STLINK_SPEED", "200"), serial=os.environ.get("STLINK_SERIAL", ""), apid=os.environ.get("STLINK_APID", ""), - mode=mode if mode is not None else os.environ.get("STLINK_CONNECT_MODE"), + mode=( + mode if mode is not None else os.environ.get("STLINK_CONNECT_MODE") + ), ) def reset_target(cli): """Best-effort target reset through CubeProgrammer.""" - # Reset is best-effort: the subsequent download/halt sequence reports hard failures. + # Reset is best-effort: the subsequent download/halt sequence reports hard + # failures. args = ["-c", "port=SWD", f"freq={os.environ.get('STLINK_SPEED', '200')}"] serial = os.environ.get("STLINK_SERIAL", "") if serial: @@ -90,19 +102,27 @@ def reset_target(cli): def resolve_symbol(elf, symbol): """Resolve a symbol with ``nm`` for direct RAM launch setup.""" - # Resolve entry/stack symbols up front so CubeProgrammer can start from RAM directly. - return resolve_symbol_with_nm(elf, symbol, nm=os.environ.get("NM", "arm-none-eabi-nm")) + # Resolve entry/stack symbols up front so CubeProgrammer can start from RAM + # directly. + return resolve_symbol_with_nm( + elf, symbol, nm=os.environ.get("NM", "arm-none-eabi-nm") + ) def read_flexmem_value(cli): """Read the FLEXMEM TCM configuration register via SWD HOTPLUG mode.""" - # HOTPLUG reads avoid resetting the core while the config ELF is parked at BKPT. - cp = run_quiet([cli] + connect_args("HOTPLUG") + ["-r32", CM55TCMCR_ADDR, "1"]) + # HOTPLUG reads avoid resetting the core while the config ELF is parked at + # BKPT. + cp = run_quiet( + [cli] + connect_args("HOTPLUG") + ["-r32", CM55TCMCR_ADDR, "1"] + ) if os.environ.get("FLEXMEM_VERBOSE"): log_output(cp.stdout, logging.DEBUG) if cp.returncode != 0: return None - match = re.search(rf"{re.escape(CM55TCMCR_ADDR)}\s*:\s*([0-9a-fA-F]{{8}})", cp.stdout) + match = re.search( + rf"{re.escape(CM55TCMCR_ADDR)}\s*:\s*([0-9a-fA-F]{{8}})", cp.stdout + ) if not match: return None return int(match.group(1), 16) @@ -114,14 +134,17 @@ def wait_for_flexmem(cli, timeout_s): deadline = time.time() + timeout_s while time.time() < deadline: value = read_flexmem_value(cli) - if value is not None and (value & CM55TCMCR_EXPECTED_MASK) == CM55TCMCR_EXPECTED_VALUE: + if ( + value is not None + and (value & CM55TCMCR_EXPECTED_MASK) == CM55TCMCR_EXPECTED_VALUE + ): return True time.sleep(0.2) return False def main(): - """Download and run the FLEXMEM config ELF, then verify the latched layout.""" + """Download and run the config ELF, then verify the latched layout.""" configure_logging() if len(sys.argv) != 2: @@ -148,12 +171,26 @@ def main(): timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "30")) reset_target(cli) - # Load the RAM-only config image and seed MSP/PC explicitly because no flash - # boot flow participates in this helper binary. - cmd = [cli] + connect_args() + ["-halt", "-d", elf, "-coreReg", f"MSP={estack_addr}", f"PC={main_thumb}", "-run"] + # Load the RAM-only config image and seed MSP/PC explicitly because no + # flash boot flow participates in this helper binary. + cmd = ( + [cli] + + connect_args() + + [ + "-halt", + "-d", + elf, + "-coreReg", + f"MSP={estack_addr}", + f"PC={main_thumb}", + "-run", + ] + ) cp = run_quiet(cmd) if os.environ.get("FLEXMEM_VERBOSE") or cp.returncode != 0: - log_output(cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR) + log_output( + cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR + ) if cp.returncode != 0: err("FLEXMEM config RAM download/start failed") return cp.returncode diff --git a/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py index c73f6c2b98..4deacbb8e3 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py +++ b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py @@ -14,7 +14,11 @@ def main(argv): """Parse CLI arguments, write the argv blob, and return a process code.""" if len(argv) < 4: - print("Usage: make_argv_bin.py [arg1 ...]", file=sys.stderr) + usage = ( + "Usage: make_argv_bin.py " + " [arg1 ...]" + ) + print(usage, file=sys.stderr) return 2 out = argv[1] base_hex = argv[2] diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py index c1fb72e839..24475e3928 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py @@ -6,7 +6,6 @@ import struct as st - ARGV_BLOCK_SIZE = 64 * 1024 @@ -26,12 +25,18 @@ def pack_cmdline(args, base_addr, block_size=ARGV_BLOCK_SIZE): cur = 0 for arg in args: encoded = arg.encode("utf-8") + b"\x00" - # GDB writes the blob at ``base_addr``; the C side expects argv pointers - # to be valid target addresses rather than offsets into the blob. + # GDB writes the blob at ``base_addr``; the C side expects argv + # pointers to be valid target addresses rather than blob offsets. ptrs.append(base_addr + header_sz + cur) strings += encoded cur += len(encoded) - blob = st.pack(" block_size: - raise ValueError(f"argv blob is {len(blob)} bytes, exceeds {block_size}-byte block") + raise ValueError( + f"argv blob is {len(blob)} bytes, exceeds {block_size}-byte block" + ) return blob + bytes(block_size - len(blob)) diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/flexmem.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/flexmem.py index 327a8ab98c..2783868c87 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/flexmem.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/flexmem.py @@ -4,17 +4,19 @@ """User-facing helpers for NUCLEO FLEXMEM configuration.""" - PLATFORM_MK = "test/baremetal/platform/nucleo-n657x0-q/platform.mk" def flexmem_config_build_instructions(config_elf: str) -> str: """Return instructions for generating a missing FLEXMEM config ELF.""" - return "\n".join([ - "Build the FLEXMEM config ELF from the repository root with:", - f" make flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", - "Then configure the board with:", - f" make run_flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", - "If you override BUILD_DIR or FLEXMEM_CONFIG_ELF, pass the same override to the make command.", - f"Expected FLEXMEM config ELF: {config_elf}", - ]) + return "\n".join( + [ + "Build the FLEXMEM config ELF from the repository root with:", + f" make flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", + "Then configure the board with:", + f" make run_flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", + "If you override BUILD_DIR or FLEXMEM_CONFIG_ELF, pass the " + "same override to the make command.", + f"Expected FLEXMEM config ELF: {config_elf}", + ] + ) diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py index d4f43fa6a4..ddec14b3ae 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py @@ -22,10 +22,10 @@ def build_run_script( """ Build the full GDB command list for one test-image run. - The order is part of the platform contract: load the RAM ELF, run normal C - startup until ``__wrap_main``, restore argv after ``.bss`` has been cleared, - install target-failure breakpoints, continue, then harvest stdout and fault - diagnostics after execution stops. + The order is part of the platform contract: load the RAM ELF, run normal + C startup until ``__wrap_main``, restore argv after ``.bss`` has been + cleared, install target-failure breakpoints, continue, then harvest stdout + and fault diagnostics after execution stops. """ gdb_lines = [ "set pagination off", @@ -56,7 +56,8 @@ def build_run_script( ) if stdout_capture_truncated_addr: gdb_lines += [ - f"set $nucleo_stdout_truncated = *(unsigned int *){stdout_capture_truncated_addr}", + "set $nucleo_stdout_truncated = *(unsigned int *)" + f"{stdout_capture_truncated_addr}", "p/x $nucleo_stdout_truncated", ] gdb_lines += fault_diagnostic_commands() @@ -72,23 +73,30 @@ def restore_argv_command(argv_bin, arg_block_addr, arg_block_sym): return f"restore {argv_bin} binary &{arg_block_sym}" -def stdout_capture_dump_commands(*, stdout_capture_addr, stdout_capture_len_addr, stdout_capture_size, stdout_capture_bin): +def stdout_capture_dump_commands( + *, + stdout_capture_addr, + stdout_capture_len_addr, + stdout_capture_size, + stdout_capture_bin, +): """Return commands that dump the target stdout capture buffer to a file.""" return [ f"set $nucleo_stdout_len = *(unsigned int *){stdout_capture_len_addr}", "if $nucleo_stdout_len > 0", - # Clamp to the compile-time buffer size before using the target-provided - # length as a host file dump bound. + # Clamp to the compile-time buffer size before using the + # target-provided length as a host file dump bound. f" if $nucleo_stdout_len > {stdout_capture_size}", f" set $nucleo_stdout_len = {stdout_capture_size}", " end", - f" dump binary memory {stdout_capture_bin} {stdout_capture_addr} {stdout_capture_addr} + $nucleo_stdout_len", + f" dump binary memory {stdout_capture_bin} {stdout_capture_addr} " + f"{stdout_capture_addr} + $nucleo_stdout_len", "end", ] def fault_diagnostic_commands(): - """Return commands that print Cortex-M fault registers and stack context.""" + """Return commands that print Cortex-M fault diagnostics.""" return [ "info registers", "x/4wx $sp", diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py index 8f488d5bb5..76c96d4f8e 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py @@ -6,7 +6,6 @@ import re - EXIT_SENTINEL_PREFIX = "[[MLKEM-EXIT:" EXIT_SENTINEL_SUFFIX = "]]" LAYOUT_FAIL_SENTINEL = "[[NUCLEO-LAYOUT-FAIL]]" @@ -26,7 +25,12 @@ def gdb_load_failed_before_target_output( ) -> bool: """Return whether a GDB load failure is safe to recover with FLEXMEM.""" exit_sentinel_in_gdb = EXIT_SENTINEL_PREFIX in gdb_text - return gdb_load_failed(gdb_text) and not target_output_observed and not exit_code_observed and not exit_sentinel_in_gdb + return ( + gdb_load_failed(gdb_text) + and not target_output_observed + and not exit_code_observed + and not exit_sentinel_in_gdb + ) def decode_cfsr(cfsr: int): @@ -64,10 +68,14 @@ def decode_hfsr(hfsr: int): def parse_exit_sentinel(line: str): """Parse a ``[[MLKEM-EXIT:]]`` line into ``(matched, rc)``.""" stripped = line.strip() - if not stripped.startswith(EXIT_SENTINEL_PREFIX) or not stripped.endswith(EXIT_SENTINEL_SUFFIX): + if not stripped.startswith(EXIT_SENTINEL_PREFIX) or not stripped.endswith( + EXIT_SENTINEL_SUFFIX + ): return False, None try: - return True, int(stripped[len(EXIT_SENTINEL_PREFIX):-len(EXIT_SENTINEL_SUFFIX)]) + return True, int( + stripped[len(EXIT_SENTINEL_PREFIX) : -len(EXIT_SENTINEL_SUFFIX)] + ) except Exception: return True, 1 @@ -89,7 +97,10 @@ def split_stdout_capture(captured: bytes): def fault_info_from_gdb(gdb_text: str) -> str: """Format fault registers emitted by the GDB script into readable text.""" values = {} - register_pattern = r"^(CFSR|HFSR|DFSR|MMFAR|BFAR|AFSR|SHCSR|CCR|MSP|PSP|LR|PC)=0x([0-9a-fA-F]+)$" + register_pattern = ( + r"^(CFSR|HFSR|DFSR|MMFAR|BFAR|AFSR|SHCSR|CCR|MSP|PSP|LR|PC)" + r"=0x([0-9a-fA-F]+)$" + ) for name, value in re.findall(register_pattern, gdb_text, re.MULTILINE): values[name] = int(value, 16) @@ -97,7 +108,20 @@ def fault_info_from_gdb(gdb_text: str) -> str: return "" lines = ["Fault registers:"] - for name in ("CFSR", "HFSR", "DFSR", "MMFAR", "BFAR", "AFSR", "SHCSR", "CCR", "MSP", "PSP", "LR", "PC"): + for name in ( + "CFSR", + "HFSR", + "DFSR", + "MMFAR", + "BFAR", + "AFSR", + "SHCSR", + "CCR", + "MSP", + "PSP", + "LR", + "PC", + ): if name in values: lines.append(f" {name}=0x{values[name]:08x}") @@ -111,12 +135,17 @@ def fault_info_from_gdb(gdb_text: str) -> str: # The stack dump follows a marker printed by the GDB script. Keep parsing # permissive because GDB may format the memory rows differently by version. stacked = re.search( - r"^STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\s*\n((?:0x[0-9a-fA-F]+:\s+.*\n?)?)", + r"^STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\s*\n" + r"((?:0x[0-9a-fA-F]+:\s+.*\n?)?)", gdb_text, re.MULTILINE, ) if stacked: - stack_lines = [line.strip() for line in stacked.group(1).splitlines() if line.strip()] + stack_lines = [ + line.strip() + for line in stacked.group(1).splitlines() + if line.strip() + ] if stack_lines: lines.append(" stacked frame dump:") lines.extend(f" {line}" for line in stack_lines) @@ -126,6 +155,8 @@ def fault_info_from_gdb(gdb_text: str) -> str: def gdb_observed_hardfault(gdb_text: str) -> bool: """Return whether GDB output shows the target entered HardFault_Handler.""" - return HARDFAULT_SENTINEL in gdb_text or re.search( - r"^HardFault_Handler \(\)", gdb_text, re.MULTILINE - ) is not None + return ( + HARDFAULT_SENTINEL in gdb_text + or re.search(r"^HardFault_Handler \(\)", gdb_text, re.MULTILINE) + is not None + ) diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py index 45c44c5429..afcb92d882 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py @@ -14,19 +14,32 @@ def find_cubeprogrammer_cli(cp_path="", st_clt_root=""): candidates = [] if cp_path: if os.path.isdir(cp_path): - candidates.extend([ - os.path.join(cp_path, "STM32_Programmer_CLI"), - os.path.join(cp_path, "bin", "STM32_Programmer_CLI"), - ]) + candidates.extend( + [ + os.path.join(cp_path, "STM32_Programmer_CLI"), + os.path.join(cp_path, "bin", "STM32_Programmer_CLI"), + ] + ) else: candidates.append(cp_path) if st_clt_root: - candidates.append(os.path.join(st_clt_root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI")) + candidates.append( + os.path.join( + st_clt_root, + "STM32CubeProgrammer", + "bin", + "STM32_Programmer_CLI", + ) + ) path_candidate = shutil.which("STM32_Programmer_CLI") if path_candidate: candidates.append(path_candidate) for candidate in candidates: - if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK): + if ( + candidate + and os.path.isfile(candidate) + and os.access(candidate, os.X_OK) + ): return candidate return None @@ -45,7 +58,9 @@ def connect_args(speed="200", serial="", apid="", mode=None): def run_quiet(cmd): """Run a command with stdout and stderr merged for delayed diagnostics.""" - return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + return subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) def derive_clt_root(st_cubeprog: str): @@ -63,7 +78,9 @@ def find_stlink_gdbserver(st_clt_root=""): stlink_bin = shutil.which("ST-LINK_gdbserver") candidate = None if not stlink_bin and st_clt_root: - candidate = os.path.join(st_clt_root, "STLink-gdb-server", "bin", "ST-LINK_gdbserver") + candidate = os.path.join( + st_clt_root, "STLink-gdb-server", "bin", "ST-LINK_gdbserver" + ) if os.path.isfile(candidate) and os.access(candidate, os.X_OK): stlink_bin = candidate return stlink_bin, candidate @@ -85,13 +102,22 @@ def cubeprogrammer_cp_path(st_cubeprog="", st_clt_root="", stlink_bin=""): cp_path = os.path.dirname(path) if cp_path is None and st_clt_root: - cli2 = os.path.join(os.path.abspath(st_clt_root), "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") + cli2 = os.path.join( + os.path.abspath(st_clt_root), + "STM32CubeProgrammer", + "bin", + "STM32_Programmer_CLI", + ) if os.path.isfile(cli2) and os.access(cli2, os.X_OK): cp_path = os.path.dirname(cli2) if cp_path is None and stlink_bin: - root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(stlink_bin)))) - cli2 = os.path.join(root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") + root = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(stlink_bin))) + ) + cli2 = os.path.join( + root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI" + ) if os.path.isfile(cli2) and os.access(cli2, os.X_OK): cp_path = os.path.dirname(cli2) diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py index dc9febacbb..f30bf7d152 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py @@ -10,21 +10,34 @@ def default_readelf(): """Return the preferred readelf executable name available on this host.""" - return shutil.which("arm-none-eabi-readelf") or shutil.which("readelf") or "readelf" + return ( + shutil.which("arm-none-eabi-readelf") + or shutil.which("readelf") + or "readelf" + ) -def resolve_symbol(elf_path: str, symbol: str, nm="arm-none-eabi-nm", readelf=None): - """Resolve ``symbol`` to a hex address, trying ``nm`` before ``readelf``.""" +def resolve_symbol( + elf_path: str, symbol: str, nm="arm-none-eabi-nm", readelf=None +): + """Resolve ``symbol`` to a hex address.""" addr = resolve_symbol_with_nm(elf_path, symbol, nm) if addr is not None: return addr - return resolve_symbol_with_readelf(elf_path, symbol, readelf or default_readelf()) + return resolve_symbol_with_readelf( + elf_path, symbol, readelf or default_readelf() + ) def resolve_symbol_with_nm(elf_path: str, symbol: str, nm="arm-none-eabi-nm"): """Resolve ``symbol`` with ``nm -n`` and return ``None`` on any failure.""" try: - cp = subprocess.run([nm, "-n", elf_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + cp = subprocess.run( + [nm, "-n", elf_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) except OSError: return None if cp.returncode != 0: @@ -45,9 +58,14 @@ def parse_nm_symbol(output: str, symbol: str): def resolve_symbol_with_readelf(elf_path: str, symbol: str, readelf=None): - """Resolve ``symbol`` with ``readelf -s`` and return ``None`` on failure.""" + """Resolve ``symbol`` with ``readelf -s``.""" try: - cp = subprocess.run([readelf or default_readelf(), "-s", elf_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + cp = subprocess.run( + [readelf or default_readelf(), "-s", elf_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) except OSError: return None if cp.returncode != 0: diff --git a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py index 4035d62c3a..b604d8645c 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py +++ b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py @@ -26,7 +26,7 @@ class NucleoHostTest(unittest.TestCase): """Exercise debugger-independent helper behavior without board access.""" def test_pack_cmdline_uses_absolute_string_pointers(self): - """The argv table contains absolute target pointers to inline strings.""" + """The argv table uses absolute target string pointers.""" blob = pack_cmdline(["prog", "--flag"], 0x20000000) argc, arg0, arg1 = struct.unpack_from(" bool: """Re-run FLEXMEM after a retryable setup or target failure.""" platform_dir = os.path.dirname(os.path.abspath(__file__)) configure_script = os.path.join(platform_dir, "flexmem_configure.py") - config_elf = os.environ.get( - "FLEXMEM_CONFIG_ELF", _default_flexmem_config_elf() - ) + config_elf = os.environ.get("FLEXMEM_CONFIG_ELF", _default_flexmem_config_elf()) if not os.path.exists(configure_script): err(f"FLEXMEM configure script not found: {configure_script}") @@ -231,12 +227,8 @@ def _run_once(): st_transport = os.environ.get("STLINK_TRANSPORT", "SWD") st_device = os.environ.get("ST_DEVICE", "STM32N657X0HxQ") st_connect = os.environ.get("STLINK_CONNECT_MODE", "under-reset") - st_cubeprog = os.environ.get( - "ST_CUBE_PROG_PATH", "" - ) # Path to STM32CubeProgrammer - st_clt_root = os.environ.get( - "ST_CUBE_CLT_ROOT", "" - ) # Root of STM32CubeCLT + st_cubeprog = os.environ.get("ST_CUBE_PROG_PATH", "") # Path to STM32CubeProgrammer + st_clt_root = os.environ.get("ST_CUBE_CLT_ROOT", "") # Root of STM32CubeCLT st_pend = os.environ.get("STLINK_PEND_HALT_TIMEOUT", "8000") st_apid = os.environ.get("STLINK_APID", "") gdb_run_timeout = float(os.environ.get("GDB_RUN_TIMEOUT", "180")) @@ -244,9 +236,7 @@ def _run_once(): st_semihost_port_env = os.environ.get("STLINK_SEMIHOST_PORT", "") try: st_semihost_port = ( - int(st_semihost_port_env) - if st_semihost_port_env - else _pick_free_port() + int(st_semihost_port_env) if st_semihost_port_env else _pick_free_port() ) except Exception: st_semihost_port = _pick_free_port() @@ -286,9 +276,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): # Resolve the RAM stdout buffer so GDB can dump target output after # execution. stdout_capture_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture") - stdout_capture_len_addr = _resolve_symbol_addr( - elf, "nucleo_stdout_capture_len" - ) + stdout_capture_len_addr = _resolve_symbol_addr(elf, "nucleo_stdout_capture_len") stdout_capture_truncated_addr = _resolve_symbol_addr( elf, "nucleo_stdout_capture_truncated" ) @@ -359,9 +347,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): if st_gdbserver_cmd_tpl: # Determine best '-cp' path for STM32CubeProgrammer CLI - cp_path = cubeprogrammer_cp_path( - st_cubeprog, st_clt_root, stlink_bin - ) + cp_path = cubeprogrammer_cp_path(st_cubeprog, st_clt_root, stlink_bin) # Provide a flexible set of placeholders for various CLT tools. # - {serial} -> raw serial value (e.g. 303030303030) @@ -388,11 +374,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): "cubeprog_flag": ( f"-cp {shlex.quote(cp_path)}" if cp_path - else ( - f"-cp {shlex.quote(st_cubeprog)}" - if st_cubeprog - else "" - ) + else (f"-cp {shlex.quote(st_cubeprog)}" if st_cubeprog else "") ), "pend": st_pend, "apid_flag": (f"-m {st_apid}" if st_apid else "-m 1"), @@ -454,9 +436,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): # First, ensure the process is alive time.sleep(0.2) # Then wait for the semihost port to accept connections. - if not _wait_for_port( - "127.0.0.1", st_semihost_port, timeout_s=10.0 - ): + if not _wait_for_port("127.0.0.1", st_semihost_port, timeout_s=10.0): info( "[exec_wrapper] semihost port not ready within timeout; " "continuing anyway" @@ -483,15 +463,11 @@ def _semihost_reader(sock: socket.socket): while b"\n" in buf: line, buf = buf.split(b"\n", 1) try: - text = line.decode( - "utf-8", errors="replace" - ) + text = line.decode("utf-8", errors="replace") except Exception: text = line.decode(errors="replace") # Detect exit sentinel first - is_exit, parsed_exit_code = ( - parse_exit_sentinel(text) - ) + is_exit, parsed_exit_code = parse_exit_sentinel(text) if is_exit: shared["exit_code"] = parsed_exit_code semihost_exit.set() @@ -542,9 +518,7 @@ def _semihost_reader(sock: socket.socket): # Server exited early – surface a helpful message out_rem = stp.stdout.read() if stp.stdout else "" if out_rem and not SUPPRESS_RETRYABLE_DIAGNOSTICS: - log_output( - out_rem, logging.DEBUG if VERBOSE else logging.ERROR - ) + log_output(out_rem, logging.DEBUG if VERBOSE else logging.ERROR) merged = out_rem low = merged.lower() if "firmware upgrade" in low or "upgrade required" in low: @@ -575,10 +549,7 @@ def _semihost_reader(sock: socket.socket): ) if hints: for h in hints: - err( - "[exec_wrapper] STLinkUpgrade candidate: " - + h - ) + err("[exec_wrapper] STLinkUpgrade candidate: " + h) return 2 gdb_lines = build_run_script( @@ -600,9 +571,7 @@ def _semihost_reader(sock: socket.socket): log_output("\n".join(gdb_lines), logging.DEBUG) LOG.debug("====================================") - with tempfile.NamedTemporaryFile( - "w", delete=False, suffix=".gdb" - ) as gs: + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".gdb") as gs: for line in gdb_lines: gs.write(line + "\n") gdb_script_path = gs.name @@ -611,9 +580,7 @@ def _semihost_reader(sock: socket.socket): # Run GDB while streaming gdbserver output, including semihost # output. - info( - "[exec_wrapper] running gdb batch; semihost output follows" - ) + info("[exec_wrapper] running gdb batch; semihost output follows") gdbp = popen( gdb_cmd, stdout=subprocess.PIPE, @@ -662,9 +629,7 @@ def _semihost_reader(sock: socket.socket): if gdb_deadline is not None and time.time() > gdb_deadline: if not SUPPRESS_RETRYABLE_DIAGNOSTICS: err("FAIL!") - err( - f"gdb batch timed out after {gdb_run_timeout:.0f}s" - ) + err(f"gdb batch timed out after {gdb_run_timeout:.0f}s") try: gdbp.terminate() gdbp.wait(timeout=1.0) @@ -701,9 +666,7 @@ def _semihost_reader(sock: socket.socket): # from semihosting. with open(stdout_capture_bin, "rb") as capture_file: captured = capture_file.read() - captured_output, captured_exit_code = split_stdout_capture( - captured - ) + captured_output, captured_exit_code = split_stdout_capture(captured) if captured_exit_code is not None: shared["exit_code"] = captured_exit_code if ( @@ -713,13 +676,9 @@ def _semihost_reader(sock: socket.socket): ): sys.stdout.write(captured_output) sys.stdout.flush() - STDOUT_BYTES_EMITTED += len( - captured_output.encode("utf-8") - ) + STDOUT_BYTES_EMITTED += len(captured_output.encode("utf-8")) except Exception as exc: - info( - f"[exec_wrapper] failed to read stdout capture: {exc}" - ) + info(f"[exec_wrapper] failed to read stdout capture: {exc}") if "$nucleo_stdout_truncated = 0x1" in gdb_text: err("WARNING: target stdout capture truncated") @@ -751,16 +710,12 @@ def _semihost_reader(sock: socket.socket): return 1 if "Program received signal SIGTRAP" in gdb_text: - info( - "[exec_wrapper] completion trap observed without exit " - "sentinel" - ) + info("[exec_wrapper] completion trap observed without exit sentinel") return 0 if gdbp.returncode != 0: target_output_observed = ( - bool(shared.get("stdout_streamed")) - or STDOUT_BYTES_EMITTED != 0 + bool(shared.get("stdout_streamed")) or STDOUT_BYTES_EMITTED != 0 ) exit_code_observed = shared.get("exit_code") is not None if gdb_load_failed_before_target_output( @@ -831,12 +786,8 @@ def main(): while True: can_retry_transport = transport_retries < attempts - 1 can_retry_hardfault = hardfault_recoveries < hardfault_attempts - can_retry_load_failure = ( - load_failure_recoveries < load_failure_attempts - ) - SUPPRESS_RETRYABLE_DIAGNOSTICS = ( - can_retry_transport or can_retry_hardfault - ) + can_retry_load_failure = load_failure_recoveries < load_failure_attempts + SUPPRESS_RETRYABLE_DIAGNOSTICS = can_retry_transport or can_retry_hardfault last_rc = _run_once() if last_rc == 0: return 0 @@ -878,11 +829,7 @@ def main(): err("HardFault diagnostics from failed run:") err(LAST_FAULT_DIAGNOSTICS) return last_rc - if ( - TARGET_FAILURE - or STDOUT_BYTES_EMITTED != 0 - or not can_retry_transport - ): + if TARGET_FAILURE or STDOUT_BYTES_EMITTED != 0 or not can_retry_transport: return last_rc transport_retries += 1 if VERBOSE: diff --git a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py index a57e158b21..2a4af7c2e4 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py +++ b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py @@ -39,9 +39,7 @@ def configure_logging(): """Configure logging, using ``FLEXMEM_VERBOSE`` as the debug switch.""" - level = ( - logging.DEBUG if os.environ.get("FLEXMEM_VERBOSE") else logging.INFO - ) + level = logging.DEBUG if os.environ.get("FLEXMEM_VERBOSE") else logging.INFO logging.basicConfig(level=level, format="%(message)s") @@ -60,19 +58,14 @@ def err(msg): def find_cubeprogrammer_cli(cp_path): """Find ``STM32_Programmer_CLI`` using platform environment defaults.""" - return st_find_cubeprogrammer_cli( - cp_path, os.environ.get("ST_CUBE_CLT_ROOT", "") - ) + return st_find_cubeprogrammer_cli(cp_path, os.environ.get("ST_CUBE_CLT_ROOT", "")) def cubeprogrammer_cli(): """Return the CubeProgrammer CLI path, or report a helpful error.""" cli = find_cubeprogrammer_cli(os.environ.get("ST_CUBE_PROG_PATH", "")) if cli is None: - err( - "STM32_Programmer_CLI not found; set ST_CUBE_PROG_PATH or " - "ST_CUBE_CLT_ROOT" - ) + err("STM32_Programmer_CLI not found; set ST_CUBE_PROG_PATH or ST_CUBE_CLT_ROOT") return cli @@ -83,9 +76,7 @@ def connect_args(mode=None): speed=os.environ.get("STLINK_SPEED", "200"), serial=os.environ.get("STLINK_SERIAL", ""), apid=os.environ.get("STLINK_APID", ""), - mode=( - mode if mode is not None else os.environ.get("STLINK_CONNECT_MODE") - ), + mode=(mode if mode is not None else os.environ.get("STLINK_CONNECT_MODE")), ) @@ -113,9 +104,7 @@ def read_flexmem_value(cli): """Read the FLEXMEM TCM configuration register via SWD HOTPLUG mode.""" # HOTPLUG reads avoid resetting the core while the config ELF is parked at # BKPT. - cp = run_quiet( - [cli] + connect_args("HOTPLUG") + ["-r32", CM55TCMCR_ADDR, "1"] - ) + cp = run_quiet([cli] + connect_args("HOTPLUG") + ["-r32", CM55TCMCR_ADDR, "1"]) if os.environ.get("FLEXMEM_VERBOSE"): log_output(cp.stdout, logging.DEBUG) if cp.returncode != 0: @@ -188,9 +177,7 @@ def main(): ) cp = run_quiet(cmd) if os.environ.get("FLEXMEM_VERBOSE") or cp.returncode != 0: - log_output( - cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR - ) + log_output(cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR) if cp.returncode != 0: err("FLEXMEM config RAM download/start failed") return cp.returncode diff --git a/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py index 4deacbb8e3..820d6a3643 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py +++ b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py @@ -14,10 +14,7 @@ def main(argv): """Parse CLI arguments, write the argv blob, and return a process code.""" if len(argv) < 4: - usage = ( - "Usage: make_argv_bin.py " - " [arg1 ...]" - ) + usage = "Usage: make_argv_bin.py [arg1 ...]" print(usage, file=sys.stderr) return 2 out = argv[1] diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py index 24475e3928..1c9bc4503b 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py @@ -30,11 +30,7 @@ def pack_cmdline(args, base_addr, block_size=ARGV_BLOCK_SIZE): ptrs.append(base_addr + header_sz + cur) strings += encoded cur += len(encoded) - blob = ( - st.pack(" block_size: raise ValueError( f"argv blob is {len(blob)} bytes, exceeds {block_size}-byte block" diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py index 76c96d4f8e..a19c27609a 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py @@ -142,9 +142,7 @@ def fault_info_from_gdb(gdb_text: str) -> str: ) if stacked: stack_lines = [ - line.strip() - for line in stacked.group(1).splitlines() - if line.strip() + line.strip() for line in stacked.group(1).splitlines() if line.strip() ] if stack_lines: lines.append(" stacked frame dump:") @@ -157,6 +155,5 @@ def gdb_observed_hardfault(gdb_text: str) -> bool: """Return whether GDB output shows the target entered HardFault_Handler.""" return ( HARDFAULT_SENTINEL in gdb_text - or re.search(r"^HardFault_Handler \(\)", gdb_text, re.MULTILINE) - is not None + or re.search(r"^HardFault_Handler \(\)", gdb_text, re.MULTILINE) is not None ) diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py index afcb92d882..ec32610a78 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py @@ -35,11 +35,7 @@ def find_cubeprogrammer_cli(cp_path="", st_clt_root=""): if path_candidate: candidates.append(path_candidate) for candidate in candidates: - if ( - candidate - and os.path.isfile(candidate) - and os.access(candidate, os.X_OK) - ): + if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK): return candidate return None @@ -115,9 +111,7 @@ def cubeprogrammer_cp_path(st_cubeprog="", st_clt_root="", stlink_bin=""): root = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(stlink_bin))) ) - cli2 = os.path.join( - root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI" - ) + cli2 = os.path.join(root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") if os.path.isfile(cli2) and os.access(cli2, os.X_OK): cp_path = os.path.dirname(cli2) diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py index f30bf7d152..b80d729c78 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py @@ -10,23 +10,15 @@ def default_readelf(): """Return the preferred readelf executable name available on this host.""" - return ( - shutil.which("arm-none-eabi-readelf") - or shutil.which("readelf") - or "readelf" - ) + return shutil.which("arm-none-eabi-readelf") or shutil.which("readelf") or "readelf" -def resolve_symbol( - elf_path: str, symbol: str, nm="arm-none-eabi-nm", readelf=None -): +def resolve_symbol(elf_path: str, symbol: str, nm="arm-none-eabi-nm", readelf=None): """Resolve ``symbol`` to a hex address.""" addr = resolve_symbol_with_nm(elf_path, symbol, nm) if addr is not None: return addr - return resolve_symbol_with_readelf( - elf_path, symbol, readelf or default_readelf() - ) + return resolve_symbol_with_readelf(elf_path, symbol, readelf or default_readelf()) def resolve_symbol_with_nm(elf_path: str, symbol: str, nm="arm-none-eabi-nm"): diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c index 33bb40e434..3af69e730b 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c @@ -7,13 +7,14 @@ #include #include #include +#include "main.h" +#include "semihosting_syscall.h" #include "stm32n6xx.h" #include "stm32n6xx_hal.h" #include "stm32n6xx_it.h" -#include "main.h" -#include "semihosting_syscall.h" -typedef struct cmdline_s { +typedef struct cmdline_s +{ int argc; char *argv[]; } cmdline_t; @@ -32,27 +33,31 @@ extern unsigned char _ebss[]; extern unsigned char __StackLimit[]; extern unsigned char _estack[]; -__attribute__((noinline)) -static void nucleo_init_dtcm_ecc(void) { +__attribute__((noinline)) static void nucleo_init_dtcm_ecc(void) +{ uintptr_t sp; __asm__ volatile("mov %0, sp" : "=r"(sp)); sp = (sp - 64U) & ~(uintptr_t)31U; uintptr_t heap_start = ((uintptr_t)_ebss + 31U) & ~(uintptr_t)31U; uintptr_t heap_end = (uintptr_t)__StackLimit & ~(uintptr_t)31U; - if (heap_start < heap_end) { + if (heap_start < heap_end) + { for (volatile uint32_t *ptr = (volatile uint32_t *)heap_start; - (uintptr_t)ptr < heap_end; ptr++) { + (uintptr_t)ptr < heap_end; ptr++) + { *ptr = 0; } } uintptr_t stack_start = ((uintptr_t)__StackLimit + 31U) & ~(uintptr_t)31U; uintptr_t stack_end = sp; - if (stack_end > (uintptr_t)_estack) { + if (stack_end > (uintptr_t)_estack) + { stack_end = (uintptr_t)_estack; } for (volatile uint32_t *ptr = (volatile uint32_t *)stack_start; - (uintptr_t)ptr < stack_end; ptr++) { + (uintptr_t)ptr < stack_end; ptr++) + { *ptr = 0; } __DSB(); @@ -62,10 +67,14 @@ static void nucleo_init_dtcm_ecc(void) { extern int __real_main(int argc, char *argv[]); int __wrap_main(int unused_argc, char *unused_argv[]); -static void semihosting_exit_with_rc(int rc) { - if (rc == 0) { +static void semihosting_exit_with_rc(int rc) +{ + if (rc == 0) + { printf("[[MLKEM-EXIT:0]]\n"); - } else { + } + else + { printf("[[MLKEM-EXIT:1]]\n"); } fflush(stdout); @@ -73,18 +82,19 @@ static void semihosting_exit_with_rc(int rc) { __DSB(); __ISB(); __BKPT(0); - while (1) { + while (1) + { __WFI(); } } -void Error_Handler(void) { - HardFault_Handler(); -} +void Error_Handler(void) { HardFault_Handler(); } /* Wrap main: build argc/argv from cmdline and forward to __real_main. */ -int __wrap_main(int unused_argc, char *unused_argv[]) { - (void)unused_argc; (void)unused_argv; +int __wrap_main(int unused_argc, char *unused_argv[]) +{ + (void)unused_argc; + (void)unused_argv; nucleo_init_dtcm_ecc(); nucleo_stdio_init(); SCB_EnableICache(); diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c index 6f7481c126..e509024a4c 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline_region.c @@ -6,5 +6,5 @@ */ #include /* Argv block in I-TCM; populated by GDB after C startup reaches __wrap_main. */ -__attribute__((aligned(8), used, section(".cmdline"))) -unsigned char mlk_cmdline_block[64 * 1024]; +__attribute__((aligned(8), used, + section(".cmdline"))) unsigned char mlk_cmdline_block[64 * 1024]; diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_config.c b/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_config.c index a2f4cc5a94..4952cedcac 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_config.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_config.c @@ -29,9 +29,8 @@ int main(void) RCC->APB4ENSR2 = RCC_APB4ENSR2_SYSCFGENS; (void)RCC->APB4ENR2; - SYSCFG->CM55TCMCR = (SYSCFG->CM55TCMCR & - ~(SYSCFG_CM55TCMCR_CFGITCMSZ_Msk | - SYSCFG_CM55TCMCR_CFGDTCMSZ_Msk)) | + SYSCFG->CM55TCMCR = (SYSCFG->CM55TCMCR & ~(SYSCFG_CM55TCMCR_CFGITCMSZ_Msk | + SYSCFG_CM55TCMCR_CFGDTCMSZ_Msk)) | flexmem_value; (void)SYSCFG->CM55TCMCR; @@ -44,7 +43,8 @@ int main(void) __ISB(); __BKPT(0); - for (;;) { + for (;;) + { __WFI(); } } diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_layout_check.c b/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_layout_check.c index d118216f3b..92ca4f9b99 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_layout_check.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_layout_check.c @@ -4,18 +4,22 @@ */ #include -__attribute__((section(".itcm_probe"), noinline, used)) -static uint32_t nucleo_itcm_above_default_probe(uint32_t x) +__attribute__((section(".itcm_probe"), noinline, used)) static uint32_t +nucleo_itcm_above_default_probe(uint32_t x) { return x ^ 0xa5a55a5aUL; } -__attribute__((noinline, used)) -void nucleo_layout_fail(uint32_t code) +__attribute__((noinline, used)) void nucleo_layout_fail(uint32_t code) { - __asm__ volatile("mov r0, %0\n" - "bkpt 0\n" : : "r"(code) : "r0", "memory"); - for (;;) { + __asm__ volatile( + "mov r0, %0\n" + "bkpt 0\n" + : + : "r"(code) + : "r0", "memory"); + for (;;) + { } } @@ -30,11 +34,13 @@ void nucleo_flexmem_layout_check(void) probe = *dtcm_above_default; *dtcm_above_default = saved; - if (probe != 0x5aa55aa5UL) { + if (probe != 0x5aa55aa5UL) + { nucleo_layout_fail(1); } - if (nucleo_itcm_above_default_probe(0x11223344UL) != 0xb487691eUL) { + if (nucleo_itcm_above_default_probe(0x11223344UL) != 0xb487691eUL) + { nucleo_layout_fail(2); } } diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c index 31488935c1..117efec670 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c @@ -14,30 +14,34 @@ #define NUCLEO_USE_SEMIHOSTING_WRITE 0 #endif -__attribute__((aligned(32), used, section(".stdout_capture"))) -volatile uint8_t nucleo_stdout_capture[NUCLEO_STDOUT_CAPTURE_SIZE]; +__attribute__((aligned(32), used, section(".stdout_capture"))) volatile uint8_t + nucleo_stdout_capture[NUCLEO_STDOUT_CAPTURE_SIZE]; -__attribute__((used)) -volatile uint32_t nucleo_stdout_capture_len; +__attribute__((used)) volatile uint32_t nucleo_stdout_capture_len; -__attribute__((used)) -volatile uint32_t nucleo_stdout_capture_truncated; +__attribute__((used)) volatile uint32_t nucleo_stdout_capture_truncated; -static void capture_write(const char *src, int length) { +static void capture_write(const char *src, int length) +{ uint32_t offset = nucleo_stdout_capture_len; - if (offset < NUCLEO_STDOUT_CAPTURE_SIZE) { + if (offset < NUCLEO_STDOUT_CAPTURE_SIZE) + { uint32_t available = NUCLEO_STDOUT_CAPTURE_SIZE - offset; uint32_t written = (uint32_t)length; - if (written > available) { + if (written > available) + { written = available; nucleo_stdout_capture_truncated = 1; } - for (uint32_t idx = 0; idx < written; idx++) { + for (uint32_t idx = 0; idx < written; idx++) + { nucleo_stdout_capture[offset + idx] = (uint8_t)src[idx]; } nucleo_stdout_capture_len = offset + written; - } else if (length > 0) { + } + else if (length > 0) + { nucleo_stdout_capture_truncated = 1; } } @@ -45,7 +49,8 @@ static void capture_write(const char *src, int length) { #if NUCLEO_USE_SEMIHOSTING_WRITE #define SEMIHOST_SYS_WRITE 0x05U -static int semihost_write(int fd, const char *src, int length) { +static int semihost_write(int fd, const char *src, int length) +{ uintptr_t params[3]; int unwritten; @@ -53,20 +58,23 @@ static int semihost_write(int fd, const char *src, int length) { params[1] = (uintptr_t)src; params[2] = (uintptr_t)length; - __asm__ volatile("mov r0, %1\n" - "mov r1, %2\n" - "bkpt 0xab\n" - "mov %0, r0\n" - : "=r"(unwritten) - : "r"(SEMIHOST_SYS_WRITE), "r"(params) - : "r0", "r1", "memory"); + __asm__ volatile( + "mov r0, %1\n" + "mov r1, %2\n" + "bkpt 0xab\n" + "mov %0, r0\n" + : "=r"(unwritten) + : "r"(SEMIHOST_SYS_WRITE), "r"(params) + : "r0", "r1", "memory"); return length - unwritten; } -#endif +#endif /* NUCLEO_USE_SEMIHOSTING_WRITE */ -int _write(int fd, char *src, int length) { - if (src == NULL || length < 0) { +int _write(int fd, char *src, int length) +{ + if (src == NULL || length < 0) + { errno = EINVAL; return -1; } @@ -81,7 +89,8 @@ int _write(int fd, char *src, int length) { #endif } -void nucleo_stdio_init(void) { +void nucleo_stdio_init(void) +{ setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); } diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h index 10965258f7..2655bd67ef 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h @@ -9,4 +9,4 @@ void nucleo_stdio_init(void); -#endif /* MLKEM_NATIVE_SEMIHOSTING_SYSCALL_H */ +#endif /* !MLKEM_NATIVE_SEMIHOSTING_SYSCALL_H */ diff --git a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py index b604d8645c..473ff5693f 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py +++ b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py @@ -45,16 +45,12 @@ def test_pack_cmdline_rejects_oversized_blob(self): def test_parse_exit_sentinel(self): """Exit sentinels are recognized and malformed codes map to failure.""" self.assertEqual(parse_exit_sentinel("[[MLKEM-EXIT:0]]\n"), (True, 0)) - self.assertEqual( - parse_exit_sentinel("[[MLKEM-EXIT:not-an-int]]\n"), (True, 1) - ) + self.assertEqual(parse_exit_sentinel("[[MLKEM-EXIT:not-an-int]]\n"), (True, 1)) self.assertEqual(parse_exit_sentinel("ordinary output"), (False, None)) def test_split_stdout_capture_removes_exit_sentinel(self): """RAM stdout dumps are decoded with the exit sentinel stripped out.""" - output, exit_code = split_stdout_capture( - b"hello\n[[MLKEM-EXIT:7]]\nworld\n" - ) + output, exit_code = split_stdout_capture(b"hello\n[[MLKEM-EXIT:7]]\nworld\n") self.assertEqual(output, "hello\nworld\n") self.assertEqual(exit_code, 7) @@ -63,9 +59,7 @@ def test_parse_nm_symbol(self): """The nm parser returns the exact requested symbol address.""" nm_output = "00000000 T Reset_Handler\n30000000 B mlk_cmdline_block\n" - self.assertEqual( - parse_nm_symbol(nm_output, "mlk_cmdline_block"), "0x30000000" - ) + self.assertEqual(parse_nm_symbol(nm_output, "mlk_cmdline_block"), "0x30000000") self.assertIsNone(parse_nm_symbol(nm_output, "missing")) def test_parse_readelf_symbol(self): @@ -95,12 +89,8 @@ def test_gdb_load_failed_detects_representative_output(self): self.assertTrue( gdb_load_failed("Error finishing flash operation\nLoad failed\n") ) - self.assertTrue( - gdb_load_failed("warning: section write failed: load FAILED") - ) - self.assertFalse( - gdb_load_failed("Loading section .text, size 0x100 lma 0x0") - ) + self.assertTrue(gdb_load_failed("warning: section write failed: load FAILED")) + self.assertFalse(gdb_load_failed("Loading section .text, size 0x100 lma 0x0")) def test_gdb_load_failed_requires_no_target_output(self): """Load-failure recovery is skipped once target output has started.""" @@ -108,30 +98,20 @@ def test_gdb_load_failed_requires_no_target_output(self): self.assertTrue(gdb_load_failed_before_target_output(gdb_text)) self.assertFalse( - gdb_load_failed_before_target_output( - gdb_text, target_output_observed=True - ) + gdb_load_failed_before_target_output(gdb_text, target_output_observed=True) ) self.assertFalse( - gdb_load_failed_before_target_output( - gdb_text, exit_code_observed=True - ) + gdb_load_failed_before_target_output(gdb_text, exit_code_observed=True) ) self.assertFalse( - gdb_load_failed_before_target_output( - "Load failed\n[[MLKEM-EXIT:0]]\n" - ) + gdb_load_failed_before_target_output("Load failed\n[[MLKEM-EXIT:0]]\n") ) def test_flexmem_config_build_instructions_show_make_command(self): """Missing config diagnostics explain how to build the helper ELF.""" - instructions = flexmem_config_build_instructions( - "/tmp/flexmem_config.elf" - ) + instructions = flexmem_config_build_instructions("/tmp/flexmem_config.elf") - self.assertIn( - f"make flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", instructions - ) + self.assertIn(f"make flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", instructions) self.assertIn( f"make run_flexmem_config EXTRA_MAKEFILE={PLATFORM_MK}", instructions, @@ -148,39 +128,33 @@ def test_load_failure_recovery_reports_build_hint_when_config_missing( def fake_exists(path): return path.endswith("flexmem_configure.py") - with mock.patch.dict(os.environ, env), mock.patch.object( - exec_wrapper.os.path, "exists", side_effect=fake_exists - ), mock.patch.object(exec_wrapper, "err", side_effect=messages.append): + with ( + mock.patch.dict(os.environ, env), + mock.patch.object(exec_wrapper.os.path, "exists", side_effect=fake_exists), + mock.patch.object(exec_wrapper, "err", side_effect=messages.append), + ): self.assertFalse(exec_wrapper._recover_after_load_failure()) self.assertIn( "FLEXMEM config ELF not found: /tmp/missing_flexmem_config.elf", messages, ) - self.assertTrue( - any("make flexmem_config" in message for message in messages) - ) + self.assertTrue(any("make flexmem_config" in message for message in messages)) def test_flexmem_configure_reports_build_hint_when_config_missing(self): """Direct configure invocations also report the build command.""" messages = [] argv = ["flexmem_configure.py", "/tmp/missing_flexmem_config.elf"] - with mock.patch.object( - flexmem_configure.sys, "argv", argv - ), mock.patch.object( - flexmem_configure.os.path, "exists", return_value=False - ), mock.patch.object( - flexmem_configure, "err", side_effect=messages.append + with ( + mock.patch.object(flexmem_configure.sys, "argv", argv), + mock.patch.object(flexmem_configure.os.path, "exists", return_value=False), + mock.patch.object(flexmem_configure, "err", side_effect=messages.append), ): self.assertEqual(flexmem_configure.main(), 2) - self.assertIn( - "Config ELF not found: /tmp/missing_flexmem_config.elf", messages - ) - self.assertTrue( - any("make flexmem_config" in message for message in messages) - ) + self.assertIn("Config ELF not found: /tmp/missing_flexmem_config.elf", messages) + self.assertTrue(any("make flexmem_config" in message for message in messages)) def test_main_recovers_once_after_load_failure(self): """The wrapper invokes FLEXMEM configuration once before retrying.""" @@ -202,12 +176,13 @@ def fake_run_once(): "GDB_HARDFAULT_RECOVERY_ATTEMPTS": "0", "GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS": "1", } - with mock.patch.dict(os.environ, env), mock.patch.object( - exec_wrapper, "_run_once", side_effect=fake_run_once - ), mock.patch.object( - exec_wrapper, "_recover_after_load_failure", return_value=True - ) as recover, mock.patch.object( - exec_wrapper.time, "sleep" + with ( + mock.patch.dict(os.environ, env), + mock.patch.object(exec_wrapper, "_run_once", side_effect=fake_run_once), + mock.patch.object( + exec_wrapper, "_recover_after_load_failure", return_value=True + ) as recover, + mock.patch.object(exec_wrapper.time, "sleep"), ): self.assertEqual(exec_wrapper.main(), 0) @@ -218,20 +193,18 @@ def test_load_failure_recovery_invokes_flexmem_configure(self): completed = mock.Mock(returncode=0, stdout="") env = {"FLEXMEM_CONFIG_ELF": "/tmp/flexmem_config.elf"} - with mock.patch.dict(os.environ, env), mock.patch.object( - exec_wrapper.os.path, "exists", return_value=True - ), mock.patch.object( - exec_wrapper, "run", return_value=completed - ) as run: + with ( + mock.patch.dict(os.environ, env), + mock.patch.object(exec_wrapper.os.path, "exists", return_value=True), + mock.patch.object(exec_wrapper, "run", return_value=completed) as run, + ): self.assertTrue(exec_wrapper._recover_after_load_failure()) cmd = run.call_args.args[0] self.assertEqual(cmd[0], exec_wrapper.sys.executable) self.assertTrue(cmd[1].endswith("flexmem_configure.py")) self.assertEqual(cmd[2], "/tmp/flexmem_config.elf") - self.assertEqual( - run.call_args.kwargs["env"]["STLINK_CONNECT_MODE"], "UR" - ) + self.assertEqual(run.call_args.kwargs["env"]["STLINK_CONNECT_MODE"], "UR") def test_main_reports_diagnostics_when_load_recovery_fails(self): """Load-failure diagnostics survive a failed FLEXMEM recovery.""" @@ -250,20 +223,18 @@ def fake_run_once(): "GDB_HARDFAULT_RECOVERY_ATTEMPTS": "0", "GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS": "1", } - with mock.patch.dict(os.environ, env), mock.patch.object( - exec_wrapper, "_run_once", side_effect=fake_run_once - ), mock.patch.object( - exec_wrapper, "_recover_after_load_failure", return_value=False - ), mock.patch.object( - exec_wrapper, "err", side_effect=messages.append - ), mock.patch.object( - exec_wrapper.time, "sleep" + with ( + mock.patch.dict(os.environ, env), + mock.patch.object(exec_wrapper, "_run_once", side_effect=fake_run_once), + mock.patch.object( + exec_wrapper, "_recover_after_load_failure", return_value=False + ), + mock.patch.object(exec_wrapper, "err", side_effect=messages.append), + mock.patch.object(exec_wrapper.time, "sleep"), ): self.assertEqual(exec_wrapper.main(), 23) - self.assertIn( - "GDB load-failure diagnostics from failed run:", messages - ) + self.assertIn("GDB load-failure diagnostics from failed run:", messages) self.assertIn("Load failed\nCannot access memory\n", messages) def test_main_does_not_recover_when_load_recovery_disabled(self): @@ -281,16 +252,16 @@ def fake_run_once(): "GDB_HARDFAULT_RECOVERY_ATTEMPTS": "0", "GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS": "0", } - with mock.patch.dict(os.environ, env), mock.patch.object( - exec_wrapper, "_run_once", side_effect=fake_run_once - ), mock.patch.object( - exec_wrapper, "_recover_after_load_failure" - ) as recover, mock.patch.object( - exec_wrapper, "err", side_effect=messages.append - ), mock.patch.object( - exec_wrapper, - "log_output", - side_effect=lambda msg, *args, **kwargs: messages.append(msg), + with ( + mock.patch.dict(os.environ, env), + mock.patch.object(exec_wrapper, "_run_once", side_effect=fake_run_once), + mock.patch.object(exec_wrapper, "_recover_after_load_failure") as recover, + mock.patch.object(exec_wrapper, "err", side_effect=messages.append), + mock.patch.object( + exec_wrapper, + "log_output", + side_effect=lambda msg, *args, **kwargs: messages.append(msg), + ), ): self.assertEqual(exec_wrapper.main(), 23) @@ -341,8 +312,7 @@ def test_build_run_script_contains_required_sequence(self): self.assertIn("break HardFault_Handler", gdb_lines) self.assertIn("break nucleo_layout_fail", gdb_lines) expected_dump = ( - " dump binary memory stdout.bin 0x34080000 " - "0x34080000 + $nucleo_stdout_len" + " dump binary memory stdout.bin 0x34080000 0x34080000 + $nucleo_stdout_len" ) self.assertIn(expected_dump, gdb_lines) self.assertIn("p/x $nucleo_stdout_truncated", gdb_lines) diff --git a/test/hal/pmu_armv8.h b/test/hal/pmu_armv8.h index 66c312255a..65cbd8839e 100644 --- a/test/hal/pmu_armv8.h +++ b/test/hal/pmu_armv8.h @@ -1,7 +1,12 @@ +/* + * Copyright (c) The mlkem-native project authors + * Copyright (c) Arm Ltd. + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + /* Compatibility wrapper: CMSIS uses armv8m_pmu.h under m-profile */ #if defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_8_1M_MAIN__) #include #else #error pmu_armv8.h included on non Armv8-M build #endif - From 7794e36d2f3e3ec99981d060c549d9ab88dcd1b0 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Thu, 7 May 2026 14:50:52 +0100 Subject: [PATCH 12/20] nucleo-n657x0-q: add OpenOCD backend Add NUCLEO_DEBUG_BACKEND=openocd support for FLEXMEM setup and runtime GDB loading. Share OpenOCD command construction in nucleo_host.openocd_tools, use connect-under-reset only for the FLEXMEM helper, and skip ST-LINK semihost socket setup for OpenOCD runtime sessions. Package an OpenOCD snapshot with STM32N6 target support as st-openocd and add it to the NUCLEO devshell. Document the ST-LINK/OpenOCD workflows and add host-side tests for OpenOCD command generation and wrapper selection. Signed-off-by: Brendan Moran --- flake.nix | 3 +- nix/st-openocd/default.nix | 69 +++++ nix/util.nix | 1 + scripts/autogen | 7 +- scripts/tests | 4 +- .../platform/nucleo-n657x0-q/README.md | 105 ++++++- .../platform/nucleo-n657x0-q/exec_wrapper.py | 271 ++++++++++-------- .../nucleo-n657x0-q/flexmem_configure.py | 74 ++++- .../nucleo_host/openocd_tools.py | 122 ++++++++ .../nucleo-n657x0-q/test_nucleo_host.py | 159 ++++++++++ 10 files changed, 669 insertions(+), 146 deletions(-) create mode 100644 nix/st-openocd/default.nix create mode 100644 test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py diff --git a/flake.nix b/flake.nix index 7cf6cfccc9..7354b51621 100644 --- a/flake.nix +++ b/flake.nix @@ -102,6 +102,7 @@ # arm-none-eabi-gcc + platform files from pqmx packages.m55-an547 = util.m55-an547; packages.avr-toolchain = util.avr-toolchain; + packages.st-openocd = util.st-openocd; devShells.arm-embedded = util.mkShell { packages = builtins.attrValues { @@ -112,7 +113,7 @@ packages.nucleo-n657x0-q = util.nucleo-n657x0-q; devShells.nucleo-n657x0-q = util.mkShell { packages = builtins.attrValues ({ - inherit (config.packages) nucleo-n657x0-q; + inherit (config.packages) nucleo-n657x0-q st-openocd; inherit (pkgs) gcc-arm-embedded coreutils git libffi pkg-config; }); }; diff --git a/nix/st-openocd/default.nix b/nix/st-openocd/default.nix new file mode 100644 index 0000000000..518ece712d --- /dev/null +++ b/nix/st-openocd/default.nix @@ -0,0 +1,69 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +{ lib +, fetchFromGitHub +, openocd +, autoreconfHook +, autoconf +, automake +, libtool +, which +}: + +openocd.overrideAttrs (old: rec { + pname = "st-openocd"; + version = "unstable-2026-05-01"; + + # OpenOCD 0.12.0 lacks the STM32N6 target script required by the + # NUCLEO-N657X0-Q RAM-only OpenOCD backend. + src = fetchFromGitHub { + owner = "openocd-org"; + repo = "openocd"; + rev = "4e9b167e1ae5ccb437eb0538440988b3f0ec53cb"; + fetchSubmodules = true; + hash = "sha256-8aYl7JzulPxH6vgSeTKTMIZVH6d55JJlXTBkfgAPTbU="; + }; + + buildInputs = lib.filter + (dep: lib.getName dep != "jimtcl") + (old.buildInputs or [ ]); + + nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ + autoreconfHook + autoconf + automake + libtool + which + ]; + + configureFlags = lib.filter + (flag: flag != "--disable-internal-jimtcl") + (old.configureFlags or [ ]) + ++ [ + "--disable-werror" + "--enable-stlink" + "--enable-cmsis-dap" + "--enable-internal-jimtcl" + ]; + + preConfigure = '' + export PATH=$PWD/.nix-wrappers:$PATH + mkdir -p .nix-wrappers + if command -v libtoolize >/dev/null && ! command -v glibtoolize >/dev/null; then + ln -s "$(command -v libtoolize)" .nix-wrappers/glibtoolize || true + fi + if [ -x ./bootstrap ]; then ./bootstrap; fi + if [ -x ./autogen.sh ]; then ./autogen.sh; fi + ''; + + postInstall = (old.postInstall or "") + '' + test -f "$out/share/openocd/scripts/target/stm32n6x.cfg" + ''; + + meta = old.meta // { + description = "OpenOCD snapshot with native ST-LINK and STM32N6 target support"; + homepage = "https://openocd.org/"; + license = lib.licenses.gpl2Plus; + }; +}) diff --git a/nix/util.nix b/nix/util.nix index aa027ec86f..7937f5f634 100644 --- a/nix/util.nix +++ b/nix/util.nix @@ -105,6 +105,7 @@ rec { slothy = pkgs.callPackage ./slothy { python3 = python3-for-slothy; }; pqmx = pkgs.callPackage ./pqmx { }; nucleo-n657x0-q = pkgs.callPackage ./nucleo-n657x0-q { }; + st-openocd = pkgs.callPackage ./st-openocd { }; avr-toolchain = pkgs.callPackage ./avr { }; # Helper function to build individual cross toolchains diff --git a/scripts/autogen b/scripts/autogen index ba7801fb52..3de57f7e31 100755 --- a/scripts/autogen +++ b/scripts/autogen @@ -2344,10 +2344,9 @@ def gen_macro_undefs(extra_notes=None): yield "" yield "#if !defined(MLK_CONFIG_MONOBUILD_KEEP_SHARED_HEADERS)" yield from gen_monolithic_undef_all_core( - filt=lambda c: not native(c) - and k_generic(c) - and not fips202(c) - and "cbmc.h" not in c, + filt=lambda c: ( + not native(c) and k_generic(c) and not fips202(c) and "cbmc.h" not in c + ), desc="MLK_CONFIG_PARAMETER_SET-generic files", ) # Handle cbmc.h manually -- most #define's therein are only defined when CBMC is set diff --git a/scripts/tests b/scripts/tests index 147c62f9a1..250dea3aa7 100755 --- a/scripts/tests +++ b/scripts/tests @@ -243,7 +243,9 @@ class TEST_TYPES(Enum): if str.lower(e.name) == str.lower(s): return e raise Exception( - f"Could not find example {s}. Examples: {list(map(lambda e: str.lower(e.name), TEST_TYPES.examples()))}" + f"Could not find example {s}. Examples: { + list(map(lambda e: str.lower(e.name), TEST_TYPES.examples())) + }" ) def __str__(self): diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md index 54b9f9c7a3..02454b7281 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/README.md +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -7,24 +7,74 @@ SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT # NUCLEO-N657X0-Q Baremetal Platform -This platform runs ML-KEM tests on the ST NUCLEO-N657X0-Q board with STM32Cube Command Line Tools and ST-LINK GDB server. The board is never flashed: both the FLEXMEM config binary and test binaries are loaded into RAM with GDB `load`. +This platform runs ML-KEM tests on the ST NUCLEO-N657X0-Q board with STM32Cube Command Line Tools plus ST-LINK GDB server, or with OpenOCD when `NUCLEO_DEBUG_BACKEND=openocd` is selected. The board is never flashed: the FLEXMEM config binary is downloaded into RAM, and test binaries are loaded into RAM with GDB `load`. -## Required Flow +## Required Workflow -STM32N657X0 starts with 64 KiB ITCM and 128 KiB DTCM. Tests require 256 KiB ITCM and 256 KiB DTCM, so CI uses a deterministic two-binary sequence: +STM32N657X0 starts with 64 KiB ITCM and 128 KiB DTCM. Tests require 256 KiB ITCM and 256 KiB DTCM, so CI uses a deterministic two-binary sequence. Both binaries are loaded into RAM; nothing is written to flash. -1. Load `flexmem_config.elf` into default-reset RAM. -2. Run it until `SYSCFG->CM55TCMCR` reports the expected FLEXMEM layout, then print `FLEXMEM configuration complete; reset target and load test binary.` on the host. -3. Stop the GDB server session. -4. Reset/reconnect the target. -5. Load the test ELF into the expanded ITCM/DTCM RAM layout. -6. Run the test, dump the target stdout capture buffer at the final breakpoint, and use `[[MLKEM-EXIT:]]` as the exit sentinel. +1. Connect to the board with STM32CubeProgrammer over SWD and reset it. For board recovery, run this FLEXMEM stage with `STLINK_CONNECT_MODE=UR` so CubeProgrammer uses connect-under-reset. +2. Load `flexmem_config.elf` into the default reset-time RAM layout, run it, poll `SYSCFG->CM55TCMCR` until it reports the expanded FLEXMEM layout, then reset the target again. +3. Start `ST-LINK_gdbserver` against the reset target and connect `arm-none-eabi-gdb` with `target remote localhost:`. The default runtime server command does not request connect-under-reset. +4. GDB `load`s the test ELF into the expanded ITCM/DTCM layout and starts it from `Reset_Handler`. +5. C startup clears `.bss`; GDB stops at `__wrap_main` and restores the packed command-line blob into the ITCM-resident `mlk_cmdline_block`. +6. The wrapper continues execution, captures stdout, and uses `[[MLKEM-EXIT:]]` as the exit sentinel. -RAM is wiped by reset; the test ELF is loaded only after the config binary has completed and the target has been reset. +RAM is wiped by reset; the test ELF and argv blob are loaded only after the config binary has completed and the target has been reset into the expanded layout. + +## Connection + +The host tools connect to the NUCLEO-N657X0-Q through the on-board ST-LINK using SWD. The default backend uses `STM32_Programmer_CLI` for `flexmem_configure.py`; `exec_wrapper.py` starts `ST-LINK_gdbserver` and then runs `arm-none-eabi-gdb` in batch mode. Set `NUCLEO_DEBUG_BACKEND=openocd` to use OpenOCD for both the FLEXMEM config stage and the runtime GDB server. + +The default connection is intentionally conservative for reliability: + +- CubeProgrammer calls use `-c port=SWD freq=${STLINK_SPEED:-200}` and add `mode=${STLINK_CONNECT_MODE}` only when `STLINK_CONNECT_MODE` is set. Use `STLINK_CONNECT_MODE=UR` for the FLEXMEM config stage when the board must be recovered under reset. +- OpenOCD calls use `interface/stlink.cfg`, `target/stm32n6x.cfg`, `transport select swd`, and `adapter speed ${OPENOCD_SPEED:-${STLINK_SPEED:-200}}`; `OPENOCD`, `OPENOCD_INTERFACE`, and `OPENOCD_TARGET` can override these defaults. +- `STLINK_SERIAL=` selects one probe when multiple ST-LINKs are attached. +- `STLINK_APID=` is forwarded to CubeProgrammer as `ap=` and to `ST-LINK_gdbserver` as `-m `. +- Runtime GDB sessions use `GDB_PORT=${GDB_PORT:-3333}`; set `STLINK_SEMIHOST_PORT` to pin the semihost console port, otherwise the wrapper picks a free localhost port. +- The default runtime `ST-LINK_gdbserver` command does not consume `STLINK_CONNECT_MODE`, so test ELFs are not loaded with connect-under-reset in the standard flow. If you provide a custom `ST_GDBSERVER_CMD` that uses `{connect}`, do not let it request under-reset for the test binary: resetting under GDB can discard the FLEXMEM layout that was just latched. + +`exec_wrapper.py` auto-locates `ST-LINK_gdbserver` from `PATH` or `ST_CUBE_CLT_ROOT`. If needed, `ST_GDBSERVER_CMD` can override the server command template. The template accepts placeholders such as `{port}`, `{speed}`, `{serial_flag}`, `{apid_flag}`, `{cubeprog_flag}`, `{semi_port}`, `{semi_level}`, and `{pend}`. + +## FLEXMEM Expansion and Reboot + +`flexmem_configure.py` performs the memory expansion before any RAM-resident test is loaded: + +1. Resolve `main` and `_estack` in `flexmem_config.elf` with `arm-none-eabi-nm`. +2. Reset the target with CubeProgrammer. If `STLINK_CONNECT_MODE=UR` is set, the subsequent RAM download/start sequence connects under reset. +3. Download the config ELF into the default reset-time RAM layout with `STM32_Programmer_CLI -c port=SWD freq= [mode=UR] -halt -d `, or with OpenOCD `load_image` when `NUCLEO_DEBUG_BACKEND=openocd` is selected. +4. Seed `MSP=<_estack>` and `PC=` with `-coreReg`, then `-run` the config binary directly from RAM. +5. The config binary writes `SYSCFG->CM55TCMCR` so both ITCM and DTCM are configured as 256 KiB, then stops at a breakpoint. +6. The host polls `SYSCFG->CM55TCMCR` at `0x56008008` in HOTPLUG mode until `(value & 0xff) == 0x99`. +7. The host resets the target again so the new FLEXMEM layout is applied before the test ELF is loaded. + +The reset after configuration is required: FLEXMEM register writes are latched for the next boot, and the reset also clears the RAM contents used by the config helper. + +## GDB Runtime Load + +After FLEXMEM expansion and reset, `run_test_after_flexmem.py` delegates to `exec_wrapper.py`. In the standard ST-LINK flow, the default `ST-LINK_gdbserver` command does not use connect-under-reset for this runtime connection. In the OpenOCD flow, the runtime server uses `reset_config srst_only srst_nogate` and does not request `connect_assert_srst`. Avoid custom runtime templates that request under-reset: the reset performed by an under-reset GDB-server connection can lose the expanded FLEXMEM configuration before `load` writes the test ELF. The wrapper creates a temporary GDB script and runs: + +``` +arm-none-eabi-gdb --batch -x +``` + +The generated GDB script follows this order: + +1. `target remote localhost:` connects to `ST-LINK_gdbserver`. +2. `load` writes the test ELF sections into expanded ITCM, DTCM, and AXI SRAM according to `linker/ram_secure.ld`. +3. `tbreak <__wrap_main>` installs a temporary breakpoint after C runtime startup, using a numeric address when symbol resolution succeeds. +4. `jump *` starts the image as a Thumb Cortex-M program. +5. `restore binary ` writes the packed argv blob after startup has cleared `.bss`. +6. Breakpoints are installed for `HardFault_Handler` and `nucleo_layout_fail`, then `continue` runs the test. +7. At completion, GDB dumps the RAM stdout capture buffer and fault diagnostics if needed. + +The wrapper terminates `ST-LINK_gdbserver` after each run. If `load` fails before target output, or if the target enters `HardFault_Handler`, the wrapper can re-run `flexmem_configure.py` and retry according to the recovery environment variables documented below. ## Prerequisites - STM32Cube CLT with `ST-LINK_gdbserver`, `STM32_Programmer_CLI`, and `arm-none-eabi-gdb`. +- For `NUCLEO_DEBUG_BACKEND=openocd`, the `.#st-openocd` Nix package provides an OpenOCD build with native ST-LINK support and `target/stm32n6x.cfg`. Stock OpenOCD 0.12.0 is not sufficient for this backend. - A NUCLEO-N657X0-Q connected over USB. - Run all commands from the board devshell: @@ -42,8 +92,31 @@ export STLINK_SPEED=200 export GDB_PORT=3333 ``` +To use OpenOCD instead of STM32Cube CLT / `ST-LINK_gdbserver`: + +``` +export NUCLEO_DEBUG_BACKEND=openocd +export OPENOCD_SPEED=200 +``` + +The `nucleo-n657x0-q` devshell includes `.#st-openocd` on `PATH`, so `OPENOCD` only needs to be set when using a non-Nix OpenOCD build. + +When running the stages manually, scope connect-under-reset to the FLEXMEM configuration command only: + +``` +STLINK_CONNECT_MODE=UR \ + python3 test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py \ + test/build/nucleo-n657x0-q/flexmem_config.elf + +unset STLINK_CONNECT_MODE +python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py \ + test/build/mlkem512/bin/test_mlkem512 +``` + The wrappers also accept `ST_GDBSERVER_CMD`, `STLINK_APID`, `STLINK_SEMIHOST_PORT`, `STLINK_SEMIHOST_LEVEL`, `STLINK_PEND_HALT_TIMEOUT`, `GDB`, `NM`, and `READELF`. +The OpenOCD backend also accepts `OPENOCD_INTERFACE` and `OPENOCD_TARGET` if the installed OpenOCD uses non-standard script locations or a patched STM32N6 target script. + ## Build Build the FLEXMEM config binary and one RAM-only test binary: @@ -66,7 +139,7 @@ test/build/mlkem512/bin/test_mlkem512 ## Run in CI -Run the full deterministic sequence for `test_mlkem512`: +Run the full deterministic sequence for `test_mlkem512`. The Make targets do not set `STLINK_CONNECT_MODE`; they inherit it from the environment if present: ``` make run_flexmem_test EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1 @@ -75,17 +148,21 @@ make run_flexmem_test EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/pla Or run each stage explicitly: ``` -python3 test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py \ - test/build/nucleo-n657x0-q/flexmem_config.elf +STLINK_CONNECT_MODE=UR \ + python3 test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py \ + test/build/nucleo-n657x0-q/flexmem_config.elf +unset STLINK_CONNECT_MODE python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py \ test/build/mlkem512/bin/test_mlkem512 ``` -`run_test_after_flexmem.py` delegates to `exec_wrapper.py`, which starts ST-LINK GDB server, loads the ELF into RAM, injects argv into `mlk_cmdline_block`, runs from `Reset_Handler`, dumps the target stdout capture buffer, and returns the `[[MLKEM-EXIT:]]` code. +`run_test_after_flexmem.py` delegates to `exec_wrapper.py`, which starts ST-LINK GDB server, loads the ELF into RAM, runs from `Reset_Handler`, restores argv into `mlk_cmdline_block` at `__wrap_main`, dumps the target stdout capture buffer, and returns the `[[MLKEM-EXIT:]]` code. The KAT and ACVP run targets depend on `run_flexmem_config`, so `make run_kat` and `make run_acvp` restore the expanded FLEXMEM layout before loading the RAM-resident images. `exec_wrapper.py` retries once after pre-output GDB transport failures; set `GDB_RUN_ATTEMPTS=` to adjust this. If the initial GDB `load` command reports `load failed` before target output starts, the wrapper re-runs `flexmem_configure.py` and retries the same ELF once; set `GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS=` to adjust or disable this. If the target enters `HardFault_Handler`, the wrapper re-runs the FLEXMEM config binary and retries once; set `GDB_HARDFAULT_RECOVERY_ATTEMPTS=` to adjust this. +Recovery runs launched by `exec_wrapper.py` default `STLINK_CONNECT_MODE` to `UR` for the reconfiguration step, without changing the default runtime GDB-server command used for the retried test ELF. + Manual hardware validation for load-failure recovery can force the retry path by resetting the board to its default FLEXMEM layout, building `flexmem_config` and a test ELF, then running `python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py test/build/mlkem512/bin/test_mlkem512` without a prior `run_flexmem_config` step. ## Argv Blob Loading diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index 208966ef4c..6f77b76dbc 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -30,6 +30,9 @@ from nucleo_host.argv_blob import pack_cmdline from nucleo_host.flexmem import flexmem_config_build_instructions from nucleo_host.gdb_script import build_run_script +from nucleo_host.openocd_tools import find_openocd +from nucleo_host.openocd_tools import runtime_gdbserver_cmd +from nucleo_host.openocd_tools import speed_khz_from_env from nucleo_host.results import LAYOUT_FAIL_SENTINEL from nucleo_host.results import fault_info_from_gdb from nucleo_host.results import gdb_load_failed_before_target_output @@ -211,6 +214,7 @@ def _run_once(): nm = os.environ.get("NM", "arm-none-eabi-nm") readelf = os.environ.get("READELF", default_readelf()) port = int(os.environ.get("GDB_PORT", "3333")) + backend = os.environ.get("NUCLEO_DEBUG_BACKEND", "stlink").strip().lower() # STM32Cube Command Line Tools integration # Users must install STM32CubeCLT and provide a gdbserver command. # Preferred: set ST_GDBSERVER_CMD as a template using Python format keys: @@ -320,98 +324,119 @@ def _resolve_symbol_addr(elf_path: str, sym: str): f.write(blob) # GDB writes target stdout here after the run; Python logs it below. stdout_capture_bin = os.path.join(td, "stdout-capture.bin") - # Allow deriving CLT root from CubeProgrammer path if not provided - if not st_clt_root and st_cubeprog: - st_clt_root = derive_clt_root(st_cubeprog) - stlink_bin, candidate = find_stlink_gdbserver(st_clt_root) - - # Auto-detect a default template if not provided - if not st_gdbserver_cmd_tpl and stlink_bin: - stlink_parts = [ - shlex.quote(stlink_bin), - "-p {port}", - "-l 1", - "-d", - "-s", - "--frequency {speed}", - "{serial_flag}", - "{apid_flag}", - "{cubeprog_flag}", - "--semihost-console-port {semi_port}", - "--semihosting {semi_level}", - "-g", - "--halt", - "--pend-halt-timeout {pend}", - ] - st_gdbserver_cmd_tpl = " ".join(stlink_parts) - - if st_gdbserver_cmd_tpl: - # Determine best '-cp' path for STM32CubeProgrammer CLI - cp_path = cubeprogrammer_cp_path(st_cubeprog, st_clt_root, stlink_bin) - - # Provide a flexible set of placeholders for various CLT tools. - # - {serial} -> raw serial value (e.g. 303030303030) - # - {serial_flag} -> '-i ' (ST-LINK_gdbserver) - # - {serial_prog} -> 'sn=' (STM32_Programmer_CLI) - # - {serial_sn} -> ',sn=' (combined CLI arg) - # - {speed} -> kHz value (e.g. 500) - # - {port} -> GDB server port (e.g. 3333) - # - {transport} -> SWD/JTAG (usually SWD) - # - {device} -> device name (e.g. STM32N657X0HxQ) - # - {connect} -> connection mode hint (e.g. under-reset) - # - {cubeprog_flag}-> '-cp ' if resolved - fmt = { - "port": port, - "speed": st_speed, - "serial": st_serial, - "serial_flag": (f"-i {st_serial}" if st_serial else ""), - "serial_prog": (f"sn={st_serial}" if st_serial else ""), - "serial_sn": (f",sn={st_serial}" if st_serial else ""), - "transport": st_transport, - "device": st_device, - "connect": st_connect, - "cubeprog": cp_path or st_cubeprog, - "cubeprog_flag": ( - f"-cp {shlex.quote(cp_path)}" - if cp_path - else (f"-cp {shlex.quote(st_cubeprog)}" if st_cubeprog else "") - ), - "pend": st_pend, - "apid_flag": (f"-m {st_apid}" if st_apid else "-m 1"), - "semi_port": st_semihost_port, - "semi_level": st_semihost_level, - } - try: - formatted = st_gdbserver_cmd_tpl.format(**fmt) - except KeyError as e: - err(f"Missing format key in ST_GDBSERVER_CMD: {e}") + semihost_listener_enabled = True + server_label = "ST-LINK GDB server" + if backend == "openocd": + openocd = find_openocd(os.environ.get("OPENOCD", "")) + if openocd is None: + err("OpenOCD not found; set OPENOCD or ensure openocd is on PATH") return 2 - gdbserver_cmd = shlex.split(formatted) - else: - msg = ( - "STM32Cube Command Line Tools required.\n" - "- Install STM32CubeCLT (Linux/macOS).\n" - " Download: " - "https://www.st.com/en/development-tools/stm32cubeclt.html\n" - "- Set ST_GDBSERVER_CMD to a working gdbserver template, " - "or ensure ST-LINK_gdbserver is on PATH.\n" - " Examples:\n" - " ST-LINK_gdbserver: 'ST-LINK_gdbserver -p {port} " - "-d --frequency {speed} ...'\n" - " STM32_Programmer_CLI: 'STM32_Programmer_CLI " - "-c port={transport},{serial_prog} ...'\n" - " Tip: If ST-LINK_gdbserver errors about " - "STM32CubeProgrammer, " - "set ST_CUBE_PROG_PATH to its installation path,\n" - " or export ST_CUBE_CLT_ROOT to the CubeCLT root so the " - "wrapper can auto-locate ST-LINK_gdbserver.\n" + gdbserver_cmd = runtime_gdbserver_cmd( + openocd=openocd, + port=port, + speed=speed_khz_from_env(), + serial=st_serial, + transport=st_transport.lower(), ) - # Append small diagnostics if we attempted a candidate path - if candidate: - msg += f" Searched for ST-LINK_gdbserver at: {candidate}\n" - if stlink_bin is None: - msg += " Note: ST-LINK_gdbserver not found on PATH.\n" - err(msg) + semihost_listener_enabled = False + server_label = "OpenOCD" + elif backend in ("stlink", "stm32cube", "cube"): + # Allow deriving CLT root from CubeProgrammer path if not provided + if not st_clt_root and st_cubeprog: + st_clt_root = derive_clt_root(st_cubeprog) + stlink_bin, candidate = find_stlink_gdbserver(st_clt_root) + + # Auto-detect a default template if not provided + if not st_gdbserver_cmd_tpl and stlink_bin: + stlink_parts = [ + shlex.quote(stlink_bin), + "-p {port}", + "-l 1", + "-d", + "-s", + "--frequency {speed}", + "{serial_flag}", + "{apid_flag}", + "{cubeprog_flag}", + "--semihost-console-port {semi_port}", + "--semihosting {semi_level}", + "-g", + "--halt", + "--pend-halt-timeout {pend}", + ] + st_gdbserver_cmd_tpl = " ".join(stlink_parts) + + if st_gdbserver_cmd_tpl: + # Determine best '-cp' path for STM32CubeProgrammer CLI + cp_path = cubeprogrammer_cp_path(st_cubeprog, st_clt_root, stlink_bin) + + # Provide a flexible set of placeholders for various CLT tools. + # - {serial} -> raw serial value (e.g. 303030303030) + # - {serial_flag} -> '-i ' (ST-LINK_gdbserver) + # - {serial_prog} -> 'sn=' (STM32_Programmer_CLI) + # - {serial_sn} -> ',sn=' (combined CLI arg) + # - {speed} -> kHz value (e.g. 500) + # - {port} -> GDB server port (e.g. 3333) + # - {transport} -> SWD/JTAG (usually SWD) + # - {device} -> device name (e.g. STM32N657X0HxQ) + # - {connect} -> connection mode hint (e.g. under-reset) + # - {cubeprog_flag}-> '-cp ' if resolved + fmt = { + "port": port, + "speed": st_speed, + "serial": st_serial, + "serial_flag": (f"-i {st_serial}" if st_serial else ""), + "serial_prog": (f"sn={st_serial}" if st_serial else ""), + "serial_sn": (f",sn={st_serial}" if st_serial else ""), + "transport": st_transport, + "device": st_device, + "connect": st_connect, + "cubeprog": cp_path or st_cubeprog, + "cubeprog_flag": ( + f"-cp {shlex.quote(cp_path)}" + if cp_path + else (f"-cp {shlex.quote(st_cubeprog)}" if st_cubeprog else "") + ), + "pend": st_pend, + "apid_flag": (f"-m {st_apid}" if st_apid else "-m 1"), + "semi_port": st_semihost_port, + "semi_level": st_semihost_level, + } + try: + formatted = st_gdbserver_cmd_tpl.format(**fmt) + except KeyError as e: + err(f"Missing format key in ST_GDBSERVER_CMD: {e}") + return 2 + gdbserver_cmd = shlex.split(formatted) + else: + msg = ( + "STM32Cube Command Line Tools required.\n" + "- Install STM32CubeCLT (Linux/macOS).\n" + " Download: " + "https://www.st.com/en/development-tools/stm32cubeclt.html\n" + "- Set ST_GDBSERVER_CMD to a working gdbserver template, " + "or ensure ST-LINK_gdbserver is on PATH.\n" + " Examples:\n" + " ST-LINK_gdbserver: 'ST-LINK_gdbserver -p {port} " + "-d --frequency {speed} ...'\n" + " STM32_Programmer_CLI: 'STM32_Programmer_CLI " + "-c port={transport},{serial_prog} ...'\n" + " Tip: If ST-LINK_gdbserver errors about " + "STM32CubeProgrammer, " + "set ST_CUBE_PROG_PATH to its installation path,\n" + " or export ST_CUBE_CLT_ROOT to the CubeCLT root so the " + "wrapper can auto-locate ST-LINK_gdbserver.\n" + " Or set NUCLEO_DEBUG_BACKEND=openocd to use OpenOCD.\n" + ) + # Append small diagnostics if we attempted a candidate path + if candidate: + msg += f" Searched for ST-LINK_gdbserver at: {candidate}\n" + if stlink_bin is None: + msg += " Note: ST-LINK_gdbserver not found on PATH.\n" + err(msg) + return 2 + else: + err(f"Unsupported NUCLEO_DEBUG_BACKEND: {backend}") return 2 info( @@ -419,7 +444,7 @@ def _resolve_symbol_addr(elf_path: str, sym: str): "flexmem_configure.py; no runtime TCM probing" ) - info(f"[exec_wrapper] starting ST gdbserver on port {port}...") + info(f"[exec_wrapper] starting {server_label} on port {port}...") info(f"[exec_wrapper] {' '.join(gdbserver_cmd)}") stp = popen( gdbserver_cmd, @@ -431,17 +456,6 @@ def _resolve_symbol_addr(elf_path: str, sym: str): ) try: - # Wait for semihost console to become available and connect before - # attaching GDB. - # First, ensure the process is alive - time.sleep(0.2) - # Then wait for the semihost port to accept connections. - if not _wait_for_port("127.0.0.1", st_semihost_port, timeout_s=10.0): - info( - "[exec_wrapper] semihost port not ready within timeout; " - "continuing anyway" - ) - semihost_sock = None semihost_stop = threading.Event() semihost_thr = None @@ -492,28 +506,41 @@ def _semihost_reader(sock: socket.socket): except Exception: pass - # Attempt to connect the listener (non-blocking retries) - try: - semihost_sock = socket.create_connection( - ("127.0.0.1", st_semihost_port), timeout=1.0 - ) - semihost_sock.settimeout(0.5) - semihost_thr = threading.Thread( - target=_semihost_reader, args=(semihost_sock,), daemon=True - ) - semihost_thr.start() - info( - "[exec_wrapper] semihost listener connected on port " - f"{st_semihost_port}" - ) - except OSError: - info( - "[exec_wrapper] semihost listener not connected " - f"(port {st_semihost_port}); proceeding" - ) + if semihost_listener_enabled: + # Wait for semihost console to become available and connect + # before attaching GDB. OpenOCD runs use the RAM stdout capture + # path and skip this ST-LINK-specific listener. + time.sleep(0.2) + if not _wait_for_port("127.0.0.1", st_semihost_port, timeout_s=10.0): + info( + "[exec_wrapper] semihost port not ready within timeout; " + "continuing anyway" + ) + + try: + semihost_sock = socket.create_connection( + ("127.0.0.1", st_semihost_port), timeout=1.0 + ) + semihost_sock.settimeout(0.5) + semihost_thr = threading.Thread( + target=_semihost_reader, args=(semihost_sock,), daemon=True + ) + semihost_thr.start() + info( + "[exec_wrapper] semihost listener connected on port " + f"{st_semihost_port}" + ) + except OSError: + info( + "[exec_wrapper] semihost listener not connected " + f"(port {st_semihost_port}); proceeding" + ) + else: + time.sleep(0.8) # Give the server a brief moment, then check for early exit - time.sleep(0.8) + if semihost_listener_enabled: + time.sleep(0.8) if stp.poll() is not None: # Server exited early – surface a helpful message out_rem = stp.stdout.read() if stp.stdout else "" diff --git a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py index 2a4af7c2e4..8709ce448a 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py +++ b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py @@ -17,6 +17,7 @@ import logging import sys import time +import tempfile from nucleo_host.st_tools import connect_args as st_connect_args from nucleo_host.st_tools import ( @@ -24,6 +25,10 @@ ) from nucleo_host.st_tools import run_quiet from nucleo_host.flexmem import flexmem_config_build_instructions +from nucleo_host.openocd_tools import find_openocd +from nucleo_host.openocd_tools import flexmem_script_lines +from nucleo_host.openocd_tools import openocd_base_args +from nucleo_host.openocd_tools import speed_khz_from_env from nucleo_host.symbols import resolve_symbol_with_nm DONE = "FLEXMEM configuration complete; reset target and load test binary." @@ -132,6 +137,59 @@ def wait_for_flexmem(cli, timeout_s): return False +def selected_backend(): + """Return the configured debug backend name.""" + return os.environ.get("NUCLEO_DEBUG_BACKEND", "stlink").strip().lower() + + +def openocd_cli(): + """Return the OpenOCD executable path, or report a helpful error.""" + openocd = find_openocd(os.environ.get("OPENOCD", "")) + if openocd is None: + err("OpenOCD not found; set OPENOCD or ensure openocd is on PATH") + return openocd + + +def run_openocd_config(elf, main_thumb, estack_addr, timeout_s): + """Download and run the config ELF using OpenOCD.""" + openocd = openocd_cli() + if openocd is None: + return 2 + + script_lines = flexmem_script_lines( + elf=elf, + main_thumb=main_thumb, + estack_addr=estack_addr, + timeout_ms=int(timeout_s * 1000), + flexmem_addr=CM55TCMCR_ADDR, + expected_mask=CM55TCMCR_EXPECTED_MASK, + expected_value=CM55TCMCR_EXPECTED_VALUE, + ) + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".cfg") as script: + script.write("\n".join(script_lines)) + script.write("\n") + script_path = script.name + + cmd = openocd_base_args( + openocd=openocd, + speed=speed_khz_from_env(), + serial=os.environ.get("STLINK_SERIAL", ""), + transport=os.environ.get("STLINK_TRANSPORT", "swd"), + ) + ["-f", script_path] + try: + cp = run_quiet(cmd) + finally: + try: + os.unlink(script_path) + except OSError: + pass + if os.environ.get("FLEXMEM_VERBOSE") or cp.returncode != 0: + log_output(cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR) + if cp.returncode != 0: + err("OpenOCD FLEXMEM config RAM download/start failed") + return cp.returncode + + def main(): """Download and run the config ELF, then verify the latched layout.""" configure_logging() @@ -146,10 +204,6 @@ def main(): err(flexmem_config_build_instructions(elf)) return 2 - cli = cubeprogrammer_cli() - if cli is None: - return 2 - main_addr = resolve_symbol(elf, "main") estack_addr = resolve_symbol(elf, "_estack") if main_addr is None or estack_addr is None: @@ -158,6 +212,18 @@ def main(): main_thumb = hex(int(main_addr, 16) | 1) timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "30")) + + backend = selected_backend() + if backend == "openocd": + return run_openocd_config(elf, main_thumb, estack_addr, timeout_s) + if backend not in ("stlink", "stm32cube", "cube"): + err(f"Unsupported NUCLEO_DEBUG_BACKEND: {backend}") + return 2 + + cli = cubeprogrammer_cli() + if cli is None: + return 2 + reset_target(cli) # Load the RAM-only config image and seed MSP/PC explicitly because no diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py new file mode 100644 index 0000000000..6653e62965 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py @@ -0,0 +1,122 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Locate OpenOCD and build NUCLEO-N657X0-Q command lines.""" + +import os +import shutil + + +DEFAULT_INTERFACE = "interface/stlink.cfg" +DEFAULT_TARGET = "target/stm32n6x.cfg" + + +def find_openocd(openocd=""): + """Find ``openocd`` from an explicit path or ``PATH``.""" + candidates = [] + if openocd: + candidates.append(openocd) + path_candidate = shutil.which("openocd") + if path_candidate: + candidates.append(path_candidate) + for candidate in candidates: + if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return None + + +def speed_khz_from_env(default="200"): + """Return adapter speed in kHz using the platform's existing variable.""" + return os.environ.get("OPENOCD_SPEED", os.environ.get("STLINK_SPEED", default)) + + +def openocd_base_args( + *, + openocd="openocd", + interface=None, + target=None, + speed="200", + serial="", + transport="swd", +): + """Return common OpenOCD arguments for the NUCLEO ST-LINK connection.""" + args = [ + openocd, + "-f", + interface or os.environ.get("OPENOCD_INTERFACE", DEFAULT_INTERFACE), + "-c", + f"transport select {transport}", + "-f", + target or os.environ.get("OPENOCD_TARGET", DEFAULT_TARGET), + "-c", + f"adapter speed {speed}", + ] + if serial: + args += ["-c", f"adapter serial {serial}"] + return args + + +def runtime_gdbserver_cmd( + *, + openocd="openocd", + port=3333, + speed="200", + serial="", + transport="swd", +): + """Return the OpenOCD command used as the runtime GDB server.""" + return openocd_base_args( + openocd=openocd, + speed=speed, + serial=serial, + transport=transport, + ) + [ + "-c", + "reset_config srst_only srst_nogate", + "-c", + f"gdb_port {port}", + "-c", + "tcl_port disabled", + "-c", + "telnet_port disabled", + "-c", + "init", + "-c", + "halt", + ] + + +def flexmem_script_lines( + *, + elf, + main_thumb, + estack_addr, + timeout_ms, + flexmem_addr="0x56008008", + expected_mask=0xFF, + expected_value=0x99, +): + """Return an OpenOCD TCL script for RAM-loading the FLEXMEM helper.""" + quoted_elf = "{" + elf.replace("\\", "\\\\").replace("}", "\\}") + "}" + return [ + "reset_config srst_only srst_nogate connect_assert_srst", + "init", + "reset halt", + f"load_image {quoted_elf}", + f"reg msp {estack_addr}", + f"reg pc {main_thumb}", + "resume", + "proc wait_flexmem_configured {} {", + f" set deadline [expr {{[clock milliseconds] + {int(timeout_ms)}}}]", + " while {[clock milliseconds] < $deadline} {", + f" set value [lindex [read_memory {flexmem_addr} 32 1] 0]", + f" if {{($value & 0x{expected_mask:x}) == 0x{expected_value:x}}} {{ return }}", + " sleep 200", + " }", + f" error \"FLEXMEM configuration register did not reach expected 0x{expected_value:x} value\"", + "}", + "wait_flexmem_configured", + "reset run", + "shutdown", + ] diff --git a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py index 473ff5693f..33707d372d 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py +++ b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py @@ -14,6 +14,9 @@ from nucleo_host.argv_blob import ARGV_BLOCK_SIZE, pack_cmdline from nucleo_host.flexmem import PLATFORM_MK, flexmem_config_build_instructions from nucleo_host.gdb_script import build_run_script, restore_argv_command +from nucleo_host.openocd_tools import flexmem_script_lines +from nucleo_host.openocd_tools import openocd_base_args +from nucleo_host.openocd_tools import runtime_gdbserver_cmd from nucleo_host.results import fault_info_from_gdb from nucleo_host.results import gdb_load_failed from nucleo_host.results import gdb_load_failed_before_target_output @@ -156,6 +159,162 @@ def test_flexmem_configure_reports_build_hint_when_config_missing(self): self.assertIn("Config ELF not found: /tmp/missing_flexmem_config.elf", messages) self.assertTrue(any("make flexmem_config" in message for message in messages)) + def test_openocd_base_args_use_stlink_stm32n6_target(self): + """OpenOCD defaults select the ST-LINK interface and STM32N6 target.""" + args = openocd_base_args( + openocd="/bin/openocd", + speed="500", + serial="ABC", + transport="swd", + ) + + self.assertEqual(args[0], "/bin/openocd") + self.assertIn("interface/stlink.cfg", args) + self.assertIn("target/stm32n6x.cfg", args) + self.assertIn("transport select swd", args) + self.assertIn("adapter speed 500", args) + self.assertIn("adapter serial ABC", args) + self.assertLess( + args.index("transport select swd"), args.index("target/stm32n6x.cfg") + ) + + def test_openocd_runtime_cmd_avoids_under_reset(self): + """Runtime OpenOCD server does not request connect-under-reset.""" + cmd = runtime_gdbserver_cmd( + openocd="openocd", + port=4444, + speed="200", + serial="", + transport="swd", + ) + joined = "\n".join(cmd) + + self.assertIn("gdb_port 4444", cmd) + self.assertIn("reset_config srst_only srst_nogate", cmd) + self.assertIn("halt", cmd) + self.assertNotIn("connect_assert_srst", joined) + + def test_openocd_flexmem_script_uses_under_reset_and_polls_register(self): + """FLEXMEM OpenOCD script connects under reset and polls CM55TCMCR.""" + lines = flexmem_script_lines( + elf="/tmp/flexmem_config.elf", + main_thumb="0x34064001", + estack_addr="0x30020000", + timeout_ms=30000, + flexmem_addr="0x56008008", + expected_mask=0xFF, + expected_value=0x99, + ) + joined = "\n".join(lines) + + self.assertIn("reset_config srst_only srst_nogate connect_assert_srst", lines) + self.assertIn("reset halt", lines) + self.assertIn("load_image {/tmp/flexmem_config.elf}", lines) + self.assertIn("reg msp 0x30020000", lines) + self.assertIn("reg pc 0x34064001", lines) + self.assertIn("read_memory 0x56008008 32 1", joined) + self.assertIn("== 0x99", joined) + self.assertIn("reset run", lines) + + def test_openocd_flexmem_configure_runs_openocd_script(self): + """OpenOCD FLEXMEM backend writes and runs an OpenOCD script.""" + completed = mock.Mock(returncode=0, stdout="") + messages = [] + env = { + "NUCLEO_DEBUG_BACKEND": "openocd", + "OPENOCD": "/usr/bin/openocd", + "STLINK_SPEED": "123", + "STLINK_SERIAL": "SERIAL", + } + + with ( + mock.patch.dict(os.environ, env), + mock.patch.object(flexmem_configure, "find_openocd", return_value="/usr/bin/openocd"), + mock.patch.object(flexmem_configure, "run_quiet", return_value=completed) as run, + mock.patch.object(flexmem_configure, "log_output", side_effect=messages.append), + ): + rc = flexmem_configure.run_openocd_config( + "/tmp/flexmem_config.elf", "0x34064001", "0x30020000", 30 + ) + + self.assertEqual(rc, 0) + cmd = run.call_args.args[0] + self.assertEqual(cmd[0], "/usr/bin/openocd") + self.assertIn("interface/stlink.cfg", cmd) + self.assertIn("target/stm32n6x.cfg", cmd) + self.assertIn("adapter speed 123", cmd) + self.assertIn("adapter serial SERIAL", cmd) + script_path = cmd[-1] + self.assertFalse(os.path.exists(script_path)) + + def test_exec_wrapper_openocd_backend_builds_runtime_server(self): + """The OpenOCD backend starts OpenOCD without semihost TCP setup.""" + popen_calls = [] + + class FakeProcess: + def __init__(self, returncode=None, stdout_text=""): + self.returncode = returncode + self.stdout = mock.Mock() + self.stdout.read.return_value = stdout_text + self.stdout.readline.return_value = "" + + def poll(self): + return self.returncode + + def wait(self, timeout=None): + return self.returncode + + def terminate(self): + self.returncode = 0 + + def kill(self): + self.returncode = -9 + + def communicate(self, timeout=None): + return ("", "") + + def fake_popen(cmd, **kwargs): + popen_calls.append(cmd) + if len(popen_calls) == 1: + return FakeProcess(returncode=None) + return FakeProcess(returncode=0) + + env = { + "NUCLEO_DEBUG_BACKEND": "openocd", + "OPENOCD": "/usr/bin/openocd", + "GDB_PORT": "4567", + "GDB_RUN_TIMEOUT": "0", + "STLINK_SEMIHOST_PORT": "4568", + } + symbol_values = { + "mlkem_cmdline_block": None, + "mlk_cmdline_block": "0x10000", + "__wrap_main": "0x200", + "Reset_Handler": "0x4", + "nucleo_stdout_capture": None, + "nucleo_stdout_capture_len": None, + "nucleo_stdout_capture_truncated": None, + } + + with ( + mock.patch.dict(os.environ, env), + mock.patch.object(exec_wrapper.sys, "argv", ["exec_wrapper.py", "/tmp/test.elf"]), + mock.patch.object(exec_wrapper.os.path, "exists", return_value=True), + mock.patch.object(exec_wrapper, "find_openocd", return_value="/usr/bin/openocd"), + mock.patch.object(exec_wrapper, "resolve_symbol", side_effect=lambda _elf, sym, **_kw: symbol_values[sym]), + mock.patch.object(exec_wrapper, "popen", side_effect=fake_popen), + mock.patch.object(exec_wrapper, "_wait_for_port") as wait_for_port, + mock.patch.object(exec_wrapper.time, "sleep"), + mock.patch.object(exec_wrapper.select, "select", return_value=([], [], [])), + ): + self.assertEqual(exec_wrapper._run_once(), 0) + + self.assertEqual(popen_calls[0][0], "/usr/bin/openocd") + self.assertIn("gdb_port 4567", popen_calls[0]) + self.assertNotIn("connect_assert_srst", "\n".join(popen_calls[0])) + self.assertEqual(popen_calls[1][0], "arm-none-eabi-gdb") + wait_for_port.assert_not_called() + def test_main_recovers_once_after_load_failure(self): """The wrapper invokes FLEXMEM configuration once before retrying.""" run_results = iter([23, 0]) From fe1caf419f7c0f134b0c3ee8a51e5578877ebbd1 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Thu, 7 May 2026 15:17:30 +0100 Subject: [PATCH 13/20] Use OpenOCD as NUCLEO-N657X0-Q backend Remove the STM32Cube CLT/ST-LINK_gdbserver backend and make the RAM-only NUCLEO-N657X0-Q flow use OpenOCD directly for both FLEXMEM configuration and runtime GDB sessions. Keep the required reset split: FLEXMEM configuration uses connect-under-reset, while the runtime OpenOCD server does not. Replace STLINK_* controls with OPENOCD_* controls, update host tests and docs, and remove the obsolete st_tools helper. Also define ARMCM55 for the platform and use the STM32N6 CMSIS device header in the PMU benchmark path so benchmark builds work on target. Validated with host unit tests, py_compile, git diff --check, and on-target ML-KEM test/benchmark runs. Signed-off-by: Brendan Moran --- nix/nucleo-n657x0-q/default.nix | 4 +- .../platform/nucleo-n657x0-q/README.md | 240 ++++++----- .../platform/nucleo-n657x0-q/exec_wrapper.py | 374 ++---------------- .../nucleo-n657x0-q/flexmem_configure.py | 135 +------ .../nucleo-n657x0-q/nucleo_host/gdb_script.py | 3 +- .../nucleo_host/openocd_tools.py | 26 +- .../nucleo-n657x0-q/nucleo_host/st_tools.py | 118 ------ .../platform/nucleo-n657x0-q/platform.mk | 1 + .../nucleo-n657x0-q/test_nucleo_host.py | 42 +- test/hal/hal.c | 4 + 10 files changed, 239 insertions(+), 708 deletions(-) delete mode 100644 test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py diff --git a/nix/nucleo-n657x0-q/default.nix b/nix/nucleo-n657x0-q/default.nix index 4a497e81cd..cf3dae9abd 100644 --- a/nix/nucleo-n657x0-q/default.nix +++ b/nix/nucleo-n657x0-q/default.nix @@ -237,11 +237,11 @@ stdenvNoCC.mkDerivation { setupHook = writeText "setup-hook.sh" '' export NUCLEO_N657X0_Q_PATH="$1/platform/nucleo-n657x0-q/src/platform/" - # Platform sources only; runtime debug server provided by STM32CubeCLT on host. + # Platform sources only; the devshell provides the OpenOCD runtime backend. ''; meta = { - description = "Platform files for STM32 NUCLEO-N657X0-Q (use STM32Cube Command Line Tools gdbserver)"; + description = "Platform files for STM32 NUCLEO-N657X0-Q RAM-only OpenOCD tests"; homepage = "https://github.com/STMicroelectronics/STM32CubeN6"; }; } diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md index 02454b7281..289a991d19 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/README.md +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -7,53 +7,79 @@ SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT # NUCLEO-N657X0-Q Baremetal Platform -This platform runs ML-KEM tests on the ST NUCLEO-N657X0-Q board with STM32Cube Command Line Tools plus ST-LINK GDB server, or with OpenOCD when `NUCLEO_DEBUG_BACKEND=openocd` is selected. The board is never flashed: the FLEXMEM config binary is downloaded into RAM, and test binaries are loaded into RAM with GDB `load`. +This platform runs ML-KEM tests on the ST NUCLEO-N657X0-Q board using the +Nix-provided OpenOCD backend. The board is never flashed: the FLEXMEM config +binary is downloaded into RAM, and test binaries are loaded into RAM with GDB +`load`. ## Required Workflow -STM32N657X0 starts with 64 KiB ITCM and 128 KiB DTCM. Tests require 256 KiB ITCM and 256 KiB DTCM, so CI uses a deterministic two-binary sequence. Both binaries are loaded into RAM; nothing is written to flash. - -1. Connect to the board with STM32CubeProgrammer over SWD and reset it. For board recovery, run this FLEXMEM stage with `STLINK_CONNECT_MODE=UR` so CubeProgrammer uses connect-under-reset. -2. Load `flexmem_config.elf` into the default reset-time RAM layout, run it, poll `SYSCFG->CM55TCMCR` until it reports the expanded FLEXMEM layout, then reset the target again. -3. Start `ST-LINK_gdbserver` against the reset target and connect `arm-none-eabi-gdb` with `target remote localhost:`. The default runtime server command does not request connect-under-reset. -4. GDB `load`s the test ELF into the expanded ITCM/DTCM layout and starts it from `Reset_Handler`. -5. C startup clears `.bss`; GDB stops at `__wrap_main` and restores the packed command-line blob into the ITCM-resident `mlk_cmdline_block`. -6. The wrapper continues execution, captures stdout, and uses `[[MLKEM-EXIT:]]` as the exit sentinel. - -RAM is wiped by reset; the test ELF and argv blob are loaded only after the config binary has completed and the target has been reset into the expanded layout. +STM32N657X0 starts with 64 KiB ITCM and 128 KiB DTCM. Tests require 256 KiB +ITCM and 256 KiB DTCM, so CI uses a deterministic two-binary sequence. Both +binaries are loaded into RAM; nothing is written to flash. + +1. Start OpenOCD for the FLEXMEM stage with connect-under-reset enabled. +2. Load `flexmem_config.elf` into the default reset-time RAM layout, set + `MSP=<_estack>` and `PC=`, run it, poll `SYSCFG->CM55TCMCR` at + `0x56008008` until `(value & 0xff) == 0x99`, then `reset run` so the + expanded layout is latched. +3. Start a fresh OpenOCD runtime GDB server without connect-under-reset. +4. GDB `load`s the test ELF into the expanded ITCM/DTCM layout and starts it + from `Reset_Handler`. +5. C startup clears `.bss`; GDB stops at `__wrap_main` and restores the packed + command-line blob into the ITCM-resident `mlk_cmdline_block`. +6. The wrapper continues execution, dumps the RAM stdout capture buffer, and + uses `[[MLKEM-EXIT:]]` as the exit sentinel. + +The reset distinction is critical. The FLEXMEM config stage must use +connect-under-reset for recovery, while the runtime test stage must not request +connect-under-reset because that can lose the just-latched FLEXMEM layout. ## Connection -The host tools connect to the NUCLEO-N657X0-Q through the on-board ST-LINK using SWD. The default backend uses `STM32_Programmer_CLI` for `flexmem_configure.py`; `exec_wrapper.py` starts `ST-LINK_gdbserver` and then runs `arm-none-eabi-gdb` in batch mode. Set `NUCLEO_DEBUG_BACKEND=openocd` to use OpenOCD for both the FLEXMEM config stage and the runtime GDB server. +The host tools connect to the NUCLEO-N657X0-Q through the on-board debug probe +using SWD. OpenOCD uses `interface/stlink.cfg`, `target/stm32n6x.cfg`, +`transport select swd`, and `adapter speed ${OPENOCD_SPEED:-200}` by default. -The default connection is intentionally conservative for reliability: +Useful environment variables: -- CubeProgrammer calls use `-c port=SWD freq=${STLINK_SPEED:-200}` and add `mode=${STLINK_CONNECT_MODE}` only when `STLINK_CONNECT_MODE` is set. Use `STLINK_CONNECT_MODE=UR` for the FLEXMEM config stage when the board must be recovered under reset. -- OpenOCD calls use `interface/stlink.cfg`, `target/stm32n6x.cfg`, `transport select swd`, and `adapter speed ${OPENOCD_SPEED:-${STLINK_SPEED:-200}}`; `OPENOCD`, `OPENOCD_INTERFACE`, and `OPENOCD_TARGET` can override these defaults. -- `STLINK_SERIAL=` selects one probe when multiple ST-LINKs are attached. -- `STLINK_APID=` is forwarded to CubeProgrammer as `ap=` and to `ST-LINK_gdbserver` as `-m `. -- Runtime GDB sessions use `GDB_PORT=${GDB_PORT:-3333}`; set `STLINK_SEMIHOST_PORT` to pin the semihost console port, otherwise the wrapper picks a free localhost port. -- The default runtime `ST-LINK_gdbserver` command does not consume `STLINK_CONNECT_MODE`, so test ELFs are not loaded with connect-under-reset in the standard flow. If you provide a custom `ST_GDBSERVER_CMD` that uses `{connect}`, do not let it request under-reset for the test binary: resetting under GDB can discard the FLEXMEM layout that was just latched. +``` +export OPENOCD_SPEED=200 +export OPENOCD_SERIAL= +export GDB_PORT=3333 +``` -`exec_wrapper.py` auto-locates `ST-LINK_gdbserver` from `PATH` or `ST_CUBE_CLT_ROOT`. If needed, `ST_GDBSERVER_CMD` can override the server command template. The template accepts placeholders such as `{port}`, `{speed}`, `{serial_flag}`, `{apid_flag}`, `{cubeprog_flag}`, `{semi_port}`, `{semi_level}`, and `{pend}`. +`OPENOCD`, `OPENOCD_INTERFACE`, `OPENOCD_TARGET`, and `OPENOCD_TRANSPORT` can +override the executable, script names, and transport when needed. `GDB`, `NM`, +and `READELF` can override the binary utilities. ## FLEXMEM Expansion and Reboot -`flexmem_configure.py` performs the memory expansion before any RAM-resident test is loaded: - -1. Resolve `main` and `_estack` in `flexmem_config.elf` with `arm-none-eabi-nm`. -2. Reset the target with CubeProgrammer. If `STLINK_CONNECT_MODE=UR` is set, the subsequent RAM download/start sequence connects under reset. -3. Download the config ELF into the default reset-time RAM layout with `STM32_Programmer_CLI -c port=SWD freq= [mode=UR] -halt -d `, or with OpenOCD `load_image` when `NUCLEO_DEBUG_BACKEND=openocd` is selected. -4. Seed `MSP=<_estack>` and `PC=` with `-coreReg`, then `-run` the config binary directly from RAM. -5. The config binary writes `SYSCFG->CM55TCMCR` so both ITCM and DTCM are configured as 256 KiB, then stops at a breakpoint. -6. The host polls `SYSCFG->CM55TCMCR` at `0x56008008` in HOTPLUG mode until `(value & 0xff) == 0x99`. -7. The host resets the target again so the new FLEXMEM layout is applied before the test ELF is loaded. - -The reset after configuration is required: FLEXMEM register writes are latched for the next boot, and the reset also clears the RAM contents used by the config helper. +`flexmem_configure.py` performs the memory expansion before any RAM-resident +test is loaded: + +1. Resolve `main` and `_estack` in `flexmem_config.elf` with + `arm-none-eabi-nm`. +2. Generate an OpenOCD script that uses + `reset_config srst_only srst_nogate connect_assert_srst`, then `reset halt`. +3. Download the config ELF into the default reset-time RAM layout with + `load_image`. +4. Seed `MSP=<_estack>` and `PC=`, then `resume` the config binary + directly from RAM. +5. Poll `SYSCFG->CM55TCMCR` at `0x56008008` until `(value & 0xff) == 0x99`. +6. Run `reset run` so the new FLEXMEM layout is applied before the test ELF is + loaded, then shut down OpenOCD. + +The reset after configuration is required: FLEXMEM register writes are latched +for the next boot, and the reset also clears the RAM contents used by the config +helper. ## GDB Runtime Load -After FLEXMEM expansion and reset, `run_test_after_flexmem.py` delegates to `exec_wrapper.py`. In the standard ST-LINK flow, the default `ST-LINK_gdbserver` command does not use connect-under-reset for this runtime connection. In the OpenOCD flow, the runtime server uses `reset_config srst_only srst_nogate` and does not request `connect_assert_srst`. Avoid custom runtime templates that request under-reset: the reset performed by an under-reset GDB-server connection can lose the expanded FLEXMEM configuration before `load` writes the test ELF. The wrapper creates a temporary GDB script and runs: +After FLEXMEM expansion and reset, `run_test_after_flexmem.py` delegates to +`exec_wrapper.py`. The runtime OpenOCD server uses +`reset_config srst_only srst_nogate` and does not request +`connect_assert_srst`. The wrapper creates a temporary GDB script and runs: ``` arm-none-eabi-gdb --batch -x @@ -61,61 +87,37 @@ arm-none-eabi-gdb --batch -x The generated GDB script follows this order: -1. `target remote localhost:` connects to `ST-LINK_gdbserver`. -2. `load` writes the test ELF sections into expanded ITCM, DTCM, and AXI SRAM according to `linker/ram_secure.ld`. -3. `tbreak <__wrap_main>` installs a temporary breakpoint after C runtime startup, using a numeric address when symbol resolution succeeds. +1. `target remote localhost:` connects to OpenOCD. +2. `load` writes the test ELF sections into expanded ITCM, DTCM, and AXI SRAM + according to `linker/ram_secure.ld`. +3. `tbreak <__wrap_main>` installs a temporary breakpoint after C runtime + startup, using a numeric address when symbol resolution succeeds. 4. `jump *` starts the image as a Thumb Cortex-M program. -5. `restore binary ` writes the packed argv blob after startup has cleared `.bss`. -6. Breakpoints are installed for `HardFault_Handler` and `nucleo_layout_fail`, then `continue` runs the test. -7. At completion, GDB dumps the RAM stdout capture buffer and fault diagnostics if needed. - -The wrapper terminates `ST-LINK_gdbserver` after each run. If `load` fails before target output, or if the target enters `HardFault_Handler`, the wrapper can re-run `flexmem_configure.py` and retry according to the recovery environment variables documented below. +5. `restore binary ` writes the packed + argv blob after startup has cleared `.bss`. +6. Breakpoints are installed for `HardFault_Handler` and `nucleo_layout_fail`, + then `continue` runs the test. +7. At completion, GDB dumps the RAM stdout capture buffer and fault diagnostics + if needed. + +The wrapper terminates OpenOCD after each run. If `load` fails before target +output, or if the target enters `HardFault_Handler`, the wrapper can re-run +`flexmem_configure.py` and retry according to the recovery environment variables +documented below. ## Prerequisites -- STM32Cube CLT with `ST-LINK_gdbserver`, `STM32_Programmer_CLI`, and `arm-none-eabi-gdb`. -- For `NUCLEO_DEBUG_BACKEND=openocd`, the `.#st-openocd` Nix package provides an OpenOCD build with native ST-LINK support and `target/stm32n6x.cfg`. Stock OpenOCD 0.12.0 is not sufficient for this backend. - A NUCLEO-N657X0-Q connected over USB. -- Run all commands from the board devshell: +- The board devshell, which provides `arm-none-eabi-gdb` and the pinned + `.#st-openocd` package: ``` nix develop .#nucleo-n657x0-q ``` -Useful environment variables: - -``` -export ST_CUBE_CLT_ROOT=/opt/ST/STM32CubeCLT_1.20.0 -export ST_CUBE_PROG_PATH=/opt/ST/STM32CubeCLT_1.20.0/STM32CubeProgrammer/bin -export STLINK_SERIAL= -export STLINK_SPEED=200 -export GDB_PORT=3333 -``` - -To use OpenOCD instead of STM32Cube CLT / `ST-LINK_gdbserver`: - -``` -export NUCLEO_DEBUG_BACKEND=openocd -export OPENOCD_SPEED=200 -``` - -The `nucleo-n657x0-q` devshell includes `.#st-openocd` on `PATH`, so `OPENOCD` only needs to be set when using a non-Nix OpenOCD build. - -When running the stages manually, scope connect-under-reset to the FLEXMEM configuration command only: - -``` -STLINK_CONNECT_MODE=UR \ - python3 test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py \ - test/build/nucleo-n657x0-q/flexmem_config.elf - -unset STLINK_CONNECT_MODE -python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py \ - test/build/mlkem512/bin/test_mlkem512 -``` - -The wrappers also accept `ST_GDBSERVER_CMD`, `STLINK_APID`, `STLINK_SEMIHOST_PORT`, `STLINK_SEMIHOST_LEVEL`, `STLINK_PEND_HALT_TIMEOUT`, `GDB`, `NM`, and `READELF`. - -The OpenOCD backend also accepts `OPENOCD_INTERFACE` and `OPENOCD_TARGET` if the installed OpenOCD uses non-standard script locations or a patched STM32N6 target script. +The `.#st-openocd` package pins an upstream OpenOCD snapshot with native debug +probe support and `target/stm32n6x.cfg`. Stock OpenOCD 0.12.0 is not sufficient +for this board flow. ## Build @@ -137,9 +139,9 @@ An example test binary is: test/build/mlkem512/bin/test_mlkem512 ``` -## Run in CI +## Run -Run the full deterministic sequence for `test_mlkem512`. The Make targets do not set `STLINK_CONNECT_MODE`; they inherit it from the environment if present: +Run the full deterministic sequence for `test_mlkem512`: ``` make run_flexmem_test EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1 @@ -148,34 +150,60 @@ make run_flexmem_test EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/pla Or run each stage explicitly: ``` -STLINK_CONNECT_MODE=UR \ - python3 test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py \ - test/build/nucleo-n657x0-q/flexmem_config.elf +python3 test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py \ + test/build/nucleo-n657x0-q/flexmem_config.elf -unset STLINK_CONNECT_MODE python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py \ test/build/mlkem512/bin/test_mlkem512 ``` -`run_test_after_flexmem.py` delegates to `exec_wrapper.py`, which starts ST-LINK GDB server, loads the ELF into RAM, runs from `Reset_Handler`, restores argv into `mlk_cmdline_block` at `__wrap_main`, dumps the target stdout capture buffer, and returns the `[[MLKEM-EXIT:]]` code. +The KAT and ACVP run targets depend on `run_flexmem_config`, so `make run_kat` +and `make run_acvp` restore the expanded FLEXMEM layout before loading the +RAM-resident images. -The KAT and ACVP run targets depend on `run_flexmem_config`, so `make run_kat` and `make run_acvp` restore the expanded FLEXMEM layout before loading the RAM-resident images. `exec_wrapper.py` retries once after pre-output GDB transport failures; set `GDB_RUN_ATTEMPTS=` to adjust this. If the initial GDB `load` command reports `load failed` before target output starts, the wrapper re-runs `flexmem_configure.py` and retries the same ELF once; set `GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS=` to adjust or disable this. If the target enters `HardFault_Handler`, the wrapper re-runs the FLEXMEM config binary and retries once; set `GDB_HARDFAULT_RECOVERY_ATTEMPTS=` to adjust this. +`exec_wrapper.py` retries once after pre-output GDB transport failures; set +`GDB_RUN_ATTEMPTS=` to adjust this. If the initial GDB `load` command +reports `load failed` before target output starts, the wrapper re-runs +`flexmem_configure.py` and retries the same ELF once; set +`GDB_LOAD_FAILURE_RECOVERY_ATTEMPTS=` to adjust or disable this. If the target +enters `HardFault_Handler`, the wrapper re-runs the FLEXMEM config binary and +retries once; set `GDB_HARDFAULT_RECOVERY_ATTEMPTS=` to adjust this. -Recovery runs launched by `exec_wrapper.py` default `STLINK_CONNECT_MODE` to `UR` for the reconfiguration step, without changing the default runtime GDB-server command used for the retried test ELF. +Manual hardware validation for load-failure recovery can force the retry path by +resetting the board to its default FLEXMEM layout, building `flexmem_config` and +a test ELF, then running the test wrapper without a prior `run_flexmem_config` +step: -Manual hardware validation for load-failure recovery can force the retry path by resetting the board to its default FLEXMEM layout, building `flexmem_config` and a test ELF, then running `python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py test/build/mlkem512/bin/test_mlkem512` without a prior `run_flexmem_config` step. +``` +python3 test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py \ + test/build/mlkem512/bin/test_mlkem512 +``` ## Argv Blob Loading -Target arguments are passed through a RAM blob rather than through semihosting or debugger command-line support. The test image links `cmdline_region.c`, which reserves the 64 KiB `mlk_cmdline_block` symbol in ITCM. `exec_wrapper.py` resolves that symbol with `arm-none-eabi-nm`, falling back to `readelf -s`; `ARG_BLOCK_SYMBOL` can select a different symbol and `ARG_BLOCK_ADDR` can override the resolved address. +Target arguments are passed through a RAM blob rather than through debugger +command-line support. The test image links `cmdline_region.c`, which reserves +the 64 KiB `mlk_cmdline_block` symbol in ITCM. `exec_wrapper.py` resolves that +symbol with `arm-none-eabi-nm`, falling back to `readelf -s`; +`ARG_BLOCK_SYMBOL` can select a different symbol and `ARG_BLOCK_ADDR` can +override the resolved address. -The wrapper packs its target argv into a temporary binary file with this little-endian layout: +The wrapper packs its target argv into a temporary binary file with this +little-endian layout: - `uint32_t argc` -- `uint32_t argv[argc]`, where each entry is an absolute target address inside the same blob +- `uint32_t argv[argc]`, where each entry is an absolute target address inside + the same blob - NUL-terminated UTF-8 argument strings immediately after the pointer table -The GDB command sequence intentionally loads the argv blob after C startup reaches `__wrap_main`: first `load` writes the ELF sections into RAM, then `jump Reset_Handler|1` runs normal startup and zeroes `.bss`, then a temporary breakpoint stops at `__wrap_main`, and only then GDB executes `restore binary `. Loading the blob at this point prevents startup `.bss` initialization from erasing it. `__wrap_main` casts `mlk_cmdline_block` to the command-line structure and calls the real ML-KEM test `main(argc, argv)`. +The GDB command sequence intentionally loads the argv blob after C startup +reaches `__wrap_main`: first `load` writes the ELF sections into RAM, then +`jump Reset_Handler|1` runs normal startup and zeroes `.bss`, then a temporary +breakpoint stops at `__wrap_main`, and only then GDB executes +`restore binary `. Loading the blob at +this point prevents startup `.bss` initialization from erasing it. +`__wrap_main` casts `mlk_cmdline_block` to the command-line structure and calls +the real ML-KEM test `main(argc, argv)`. ## Memory Layout @@ -191,21 +219,33 @@ The GDB command sequence intentionally loads the argv blob after C startup reach - `.data`, `.bss`: expanded 256 KiB DTCM at `0x30000000` - argv block: expanded 256 KiB ITCM at `0x00000000` - stdout capture: AXI SRAM at `0x34080000` -- stack: top 192 KiB of DTCM, with `_estack = 0x30040000` and `__StackLimit = 0x30010000` +- stack: top 192 KiB of DTCM, with `_estack = 0x30040000` and + `__StackLimit = 0x30010000` - no flash memory regions or flash LMAs ## Layout Validation -Each test binary links `flexmem_layout_check.c` and calls it before ML-KEM tests. It fails with a breakpoint if either expanded region is unavailable: +Each test binary links `flexmem_layout_check.c` and calls it before ML-KEM +tests. It fails with a breakpoint if either expanded region is unavailable: -- executes `nucleo_itcm_above_default_probe()` from `.itcm_probe` placed beyond the default 64 KiB ITCM boundary -- writes and reads address `0x30020000`, beyond the default 128 KiB DTCM boundary +- executes `nucleo_itcm_above_default_probe()` from `.itcm_probe` placed beyond + the default 64 KiB ITCM boundary +- writes and reads address `0x30020000`, beyond the default 128 KiB DTCM + boundary -This proves the CI flow loaded the test after FLEXMEM configuration and reset. Actual pass/fail execution remains hardware-dependent on the connected board, ST-LINK firmware, and CLT version. +This proves the CI flow loaded the test after FLEXMEM configuration and reset. +Actual pass/fail execution remains hardware-dependent on the connected board, +probe firmware, and OpenOCD behavior. ## Notes -- Do not use debugger GUI flows; CI uses ST-LINK GDB server plus `arm-none-eabi-gdb` only. -- Do not run the test binary directly on a freshly reset board; it links into expanded ITCM/DTCM that does not exist until after `flexmem_config.elf` and reset. -- Do not reintroduce runtime probing or linker wrapping of `SystemInit`; FLEXMEM configuration is deterministic and reset-applied. -- Keep target output on normal libc/newlib `printf` and file-I/O paths linked with `--specs=rdimon.specs -lc -lrdimon`; the platform `_write` captures stdout/stderr in RAM for GDB to harvest because raw semihosting `bkpt 0xab` syscalls are not reliable on this board. +- Do not use debugger GUI flows; CI uses OpenOCD plus `arm-none-eabi-gdb` only. +- Do not run the test binary directly on a freshly reset board; it links into + expanded ITCM/DTCM that does not exist until after `flexmem_config.elf` and + reset. +- Do not reintroduce runtime probing or linker wrapping of `SystemInit`; + FLEXMEM configuration is deterministic and reset-applied. +- Keep target output on normal libc/newlib `printf` and file-I/O paths linked + with `--specs=rdimon.specs -lc -lrdimon`; the platform `_write` captures + stdout/stderr in RAM for GDB to harvest because raw semihosting `bkpt 0xab` + syscalls are not reliable on this board. diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index 6f77b76dbc..da25a67c85 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -5,43 +5,38 @@ # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT """ -Run one RAM-resident NUCLEO-N657X0-Q test ELF through ST-LINK GDB server. +Run one RAM-resident NUCLEO-N657X0-Q test ELF through OpenOCD. The wrapper expects ``flexmem_configure.py`` to have expanded ITCM/DTCM before normal runs. If the initial GDB ``load`` reports that loading failed before target output starts, the wrapper can re-run FLEXMEM configuration and retry the same ELF. Successful runs load the ELF into RAM, let C startup clear -memory, restore a packed argv blob at ``__wrap_main``, stream or dump target -stdout, and map target sentinels to the process exit status expected by the -baremetal test harness. +memory, restore a packed argv blob at ``__wrap_main``, dump target stdout from +the RAM capture buffer, and map target sentinels to the process exit status +expected by the baremetal test harness. """ import logging import os -import shlex import subprocess import sys import tempfile import time import select -import socket -import threading from nucleo_host.argv_blob import pack_cmdline from nucleo_host.flexmem import flexmem_config_build_instructions from nucleo_host.gdb_script import build_run_script from nucleo_host.openocd_tools import find_openocd from nucleo_host.openocd_tools import runtime_gdbserver_cmd +from nucleo_host.openocd_tools import serial_from_env from nucleo_host.openocd_tools import speed_khz_from_env +from nucleo_host.openocd_tools import transport_from_env from nucleo_host.results import LAYOUT_FAIL_SENTINEL from nucleo_host.results import fault_info_from_gdb from nucleo_host.results import gdb_load_failed_before_target_output from nucleo_host.results import gdb_observed_hardfault -from nucleo_host.results import parse_exit_sentinel from nucleo_host.results import split_stdout_capture -from nucleo_host.st_tools import cubeprogrammer_cp_path -from nucleo_host.st_tools import derive_clt_root -from nucleo_host.st_tools import find_stlink_gdbserver from nucleo_host.symbols import default_readelf from nucleo_host.symbols import resolve_symbol @@ -93,28 +88,6 @@ def popen(cmd, **kwargs): return subprocess.Popen(cmd, **kwargs) -def _pick_free_port() -> int: - """Ask the OS for an available localhost TCP port.""" - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s.bind(("127.0.0.1", 0)) - return s.getsockname()[1] - finally: - s.close() - - -def _wait_for_port(host: str, port: int, timeout_s: float) -> bool: - """Wait until a local TCP port accepts connections or timeout expires.""" - deadline = time.time() + timeout_s - while time.time() < deadline: - try: - with socket.create_connection((host, port), timeout=0.3): - return True - except OSError: - time.sleep(0.1) - return False - - def _default_flexmem_config_elf() -> str: """Return the default build path for the FLEXMEM configuration ELF.""" platform_dir = os.path.dirname(os.path.abspath(__file__)) @@ -140,7 +113,6 @@ def _recover_flexmem(reason: str, failure_message: str) -> bool: info(f"[exec_wrapper] recovering from {reason}: re-running FLEXMEM config") recovery_env = os.environ.copy() - recovery_env.setdefault("STLINK_CONNECT_MODE", "UR") cp = run( [sys.executable, configure_script, config_elf], stdout=subprocess.PIPE, @@ -214,37 +186,7 @@ def _run_once(): nm = os.environ.get("NM", "arm-none-eabi-nm") readelf = os.environ.get("READELF", default_readelf()) port = int(os.environ.get("GDB_PORT", "3333")) - backend = os.environ.get("NUCLEO_DEBUG_BACKEND", "stlink").strip().lower() - # STM32Cube Command Line Tools integration - # Users must install STM32CubeCLT and provide a gdbserver command. - # Preferred: set ST_GDBSERVER_CMD as a template using Python format keys: - # {port} {speed} {serial} {transport} {device} {connect} - # Example (ST-LINK_gdbserver): - # export ST_GDBSERVER_CMD='ST-LINK_gdbserver -p {port} ...' - # Example (STM32_Programmer_CLI): - # export ST_GDBSERVER_CMD='STM32_Programmer_CLI ...' - st_gdbserver_cmd_tpl = os.environ.get("ST_GDBSERVER_CMD") - st_speed = os.environ.get( - "STLINK_SPEED", "200" - ) # kHz (lower default for reliability) - st_serial = os.environ.get("STLINK_SERIAL", "") # optional, raw value - st_transport = os.environ.get("STLINK_TRANSPORT", "SWD") - st_device = os.environ.get("ST_DEVICE", "STM32N657X0HxQ") - st_connect = os.environ.get("STLINK_CONNECT_MODE", "under-reset") - st_cubeprog = os.environ.get("ST_CUBE_PROG_PATH", "") # Path to STM32CubeProgrammer - st_clt_root = os.environ.get("ST_CUBE_CLT_ROOT", "") # Root of STM32CubeCLT - st_pend = os.environ.get("STLINK_PEND_HALT_TIMEOUT", "8000") - st_apid = os.environ.get("STLINK_APID", "") gdb_run_timeout = float(os.environ.get("GDB_RUN_TIMEOUT", "180")) - # Semihosting configuration (enabled by default) - st_semihost_port_env = os.environ.get("STLINK_SEMIHOST_PORT", "") - try: - st_semihost_port = ( - int(st_semihost_port_env) if st_semihost_port_env else _pick_free_port() - ) - except Exception: - st_semihost_port = _pick_free_port() - st_semihost_level = os.environ.get("STLINK_SEMIHOST_LEVEL", "all") # Address extraction for argv block symbol. Numeric addresses avoid # debugger symbol issues. @@ -324,120 +266,18 @@ def _resolve_symbol_addr(elf_path: str, sym: str): f.write(blob) # GDB writes target stdout here after the run; Python logs it below. stdout_capture_bin = os.path.join(td, "stdout-capture.bin") - semihost_listener_enabled = True - server_label = "ST-LINK GDB server" - if backend == "openocd": - openocd = find_openocd(os.environ.get("OPENOCD", "")) - if openocd is None: - err("OpenOCD not found; set OPENOCD or ensure openocd is on PATH") - return 2 - gdbserver_cmd = runtime_gdbserver_cmd( - openocd=openocd, - port=port, - speed=speed_khz_from_env(), - serial=st_serial, - transport=st_transport.lower(), - ) - semihost_listener_enabled = False - server_label = "OpenOCD" - elif backend in ("stlink", "stm32cube", "cube"): - # Allow deriving CLT root from CubeProgrammer path if not provided - if not st_clt_root and st_cubeprog: - st_clt_root = derive_clt_root(st_cubeprog) - stlink_bin, candidate = find_stlink_gdbserver(st_clt_root) - - # Auto-detect a default template if not provided - if not st_gdbserver_cmd_tpl and stlink_bin: - stlink_parts = [ - shlex.quote(stlink_bin), - "-p {port}", - "-l 1", - "-d", - "-s", - "--frequency {speed}", - "{serial_flag}", - "{apid_flag}", - "{cubeprog_flag}", - "--semihost-console-port {semi_port}", - "--semihosting {semi_level}", - "-g", - "--halt", - "--pend-halt-timeout {pend}", - ] - st_gdbserver_cmd_tpl = " ".join(stlink_parts) - - if st_gdbserver_cmd_tpl: - # Determine best '-cp' path for STM32CubeProgrammer CLI - cp_path = cubeprogrammer_cp_path(st_cubeprog, st_clt_root, stlink_bin) - - # Provide a flexible set of placeholders for various CLT tools. - # - {serial} -> raw serial value (e.g. 303030303030) - # - {serial_flag} -> '-i ' (ST-LINK_gdbserver) - # - {serial_prog} -> 'sn=' (STM32_Programmer_CLI) - # - {serial_sn} -> ',sn=' (combined CLI arg) - # - {speed} -> kHz value (e.g. 500) - # - {port} -> GDB server port (e.g. 3333) - # - {transport} -> SWD/JTAG (usually SWD) - # - {device} -> device name (e.g. STM32N657X0HxQ) - # - {connect} -> connection mode hint (e.g. under-reset) - # - {cubeprog_flag}-> '-cp ' if resolved - fmt = { - "port": port, - "speed": st_speed, - "serial": st_serial, - "serial_flag": (f"-i {st_serial}" if st_serial else ""), - "serial_prog": (f"sn={st_serial}" if st_serial else ""), - "serial_sn": (f",sn={st_serial}" if st_serial else ""), - "transport": st_transport, - "device": st_device, - "connect": st_connect, - "cubeprog": cp_path or st_cubeprog, - "cubeprog_flag": ( - f"-cp {shlex.quote(cp_path)}" - if cp_path - else (f"-cp {shlex.quote(st_cubeprog)}" if st_cubeprog else "") - ), - "pend": st_pend, - "apid_flag": (f"-m {st_apid}" if st_apid else "-m 1"), - "semi_port": st_semihost_port, - "semi_level": st_semihost_level, - } - try: - formatted = st_gdbserver_cmd_tpl.format(**fmt) - except KeyError as e: - err(f"Missing format key in ST_GDBSERVER_CMD: {e}") - return 2 - gdbserver_cmd = shlex.split(formatted) - else: - msg = ( - "STM32Cube Command Line Tools required.\n" - "- Install STM32CubeCLT (Linux/macOS).\n" - " Download: " - "https://www.st.com/en/development-tools/stm32cubeclt.html\n" - "- Set ST_GDBSERVER_CMD to a working gdbserver template, " - "or ensure ST-LINK_gdbserver is on PATH.\n" - " Examples:\n" - " ST-LINK_gdbserver: 'ST-LINK_gdbserver -p {port} " - "-d --frequency {speed} ...'\n" - " STM32_Programmer_CLI: 'STM32_Programmer_CLI " - "-c port={transport},{serial_prog} ...'\n" - " Tip: If ST-LINK_gdbserver errors about " - "STM32CubeProgrammer, " - "set ST_CUBE_PROG_PATH to its installation path,\n" - " or export ST_CUBE_CLT_ROOT to the CubeCLT root so the " - "wrapper can auto-locate ST-LINK_gdbserver.\n" - " Or set NUCLEO_DEBUG_BACKEND=openocd to use OpenOCD.\n" - ) - # Append small diagnostics if we attempted a candidate path - if candidate: - msg += f" Searched for ST-LINK_gdbserver at: {candidate}\n" - if stlink_bin is None: - msg += " Note: ST-LINK_gdbserver not found on PATH.\n" - err(msg) - return 2 - else: - err(f"Unsupported NUCLEO_DEBUG_BACKEND: {backend}") + openocd = find_openocd(os.environ.get("OPENOCD", "")) + if openocd is None: + err("OpenOCD not found; set OPENOCD or ensure openocd is on PATH") return 2 + gdbserver_cmd = runtime_gdbserver_cmd( + openocd=openocd, + port=port, + speed=speed_khz_from_env(), + serial=serial_from_env(), + transport=transport_from_env(), + ) + server_label = "OpenOCD" info( "[exec_wrapper] assuming FLEXMEM was configured by " @@ -456,127 +296,15 @@ def _resolve_symbol_addr(elf_path: str, sym: str): ) try: - semihost_sock = None - semihost_stop = threading.Event() - semihost_thr = None - semihost_exit = threading.Event() - shared = {"exit_code": None, "stdout_streamed": False} - - def _semihost_reader(sock: socket.socket): - """Stream semihost output and detect the exit sentinel.""" - global STDOUT_BYTES_EMITTED - - buf = b"" - try: - while not semihost_stop.is_set(): - try: - data = sock.recv(4096) - if not data: - break - buf += data - while b"\n" in buf: - line, buf = buf.split(b"\n", 1) - try: - text = line.decode("utf-8", errors="replace") - except Exception: - text = line.decode(errors="replace") - # Detect exit sentinel first - is_exit, parsed_exit_code = parse_exit_sentinel(text) - if is_exit: - shared["exit_code"] = parsed_exit_code - semihost_exit.set() - # Do not log the sentinel unless verbose. - if VERBOSE: - LOG.debug("[semi] %s", text) - else: - shared["stdout_streamed"] = True - if VERBOSE: - LOG.debug("[semi] %s", text) - else: - sys.stdout.buffer.write(line + b"\n") - sys.stdout.buffer.flush() - STDOUT_BYTES_EMITTED += len(line) + 1 - except socket.timeout: - continue - except OSError: - break - finally: - try: - sock.close() - except Exception: - pass - - if semihost_listener_enabled: - # Wait for semihost console to become available and connect - # before attaching GDB. OpenOCD runs use the RAM stdout capture - # path and skip this ST-LINK-specific listener. - time.sleep(0.2) - if not _wait_for_port("127.0.0.1", st_semihost_port, timeout_s=10.0): - info( - "[exec_wrapper] semihost port not ready within timeout; " - "continuing anyway" - ) - - try: - semihost_sock = socket.create_connection( - ("127.0.0.1", st_semihost_port), timeout=1.0 - ) - semihost_sock.settimeout(0.5) - semihost_thr = threading.Thread( - target=_semihost_reader, args=(semihost_sock,), daemon=True - ) - semihost_thr.start() - info( - "[exec_wrapper] semihost listener connected on port " - f"{st_semihost_port}" - ) - except OSError: - info( - "[exec_wrapper] semihost listener not connected " - f"(port {st_semihost_port}); proceeding" - ) - else: - time.sleep(0.8) + exit_code = None + time.sleep(0.8) # Give the server a brief moment, then check for early exit - if semihost_listener_enabled: - time.sleep(0.8) if stp.poll() is not None: - # Server exited early – surface a helpful message + # Server exited early: surface its output as the setup failure. out_rem = stp.stdout.read() if stp.stdout else "" if out_rem and not SUPPRESS_RETRYABLE_DIAGNOSTICS: log_output(out_rem, logging.DEBUG if VERBOSE else logging.ERROR) - merged = out_rem - low = merged.lower() - if "firmware upgrade" in low or "upgrade required" in low: - # Try to suggest STLinkUpgrade locations - hints = [] - if st_clt_root: - app1 = os.path.join( - st_clt_root, - "STM32CubeProgrammer", - "stlink", - "STLinkUpgrade", - ) - app2 = os.path.join( - st_clt_root, - "STM32CubeProgrammer", - "stlink", - "STLinkUpgrade.app", - ) - if os.path.exists(app1): - hints.append(app1) - if os.path.exists(app2): - hints.append(app2) - if VERBOSE: - err( - "[exec_wrapper] ST-LINK firmware upgrade " - "required. " - "Please run the STLinkUpgrade tool." - ) - if hints: - for h in hints: - err("[exec_wrapper] STLinkUpgrade candidate: " + h) return 2 gdb_lines = build_run_script( @@ -605,9 +333,9 @@ def _semihost_reader(sock: socket.socket): gdb_cmd = [gdb, "--batch", "-x", gdb_script_path, elf] - # Run GDB while streaming gdbserver output, including semihost - # output. - info("[exec_wrapper] running gdb batch; semihost output follows") + # Run GDB while draining OpenOCD output so probe diagnostics are + # available in verbose mode without blocking the wrapper. + info("[exec_wrapper] running gdb batch") gdbp = popen( gdb_cmd, stdout=subprocess.PIPE, @@ -618,32 +346,16 @@ def _semihost_reader(sock: socket.socket): time.time() + gdb_run_timeout if gdb_run_timeout > 0 else None ) - # Stream gdbserver output until gdb finishes without blocking on + # Stream OpenOCD output until gdb finishes without blocking on # readline(). while True: - # Early shutdown if exit sentinel observed - if semihost_exit.is_set(): - info( - "[exec_wrapper] exit sentinel detected; shutting " - "down gdb and gdbserver..." - ) - try: - if gdbp.poll() is None: - gdbp.terminate() - try: - gdbp.wait(timeout=1.0) - except Exception: - gdbp.kill() - except Exception: - pass - break if stp.stdout is not None: try: r, _, _ = select.select([stp.stdout], [], [], 0.1) if r: line = stp.stdout.readline() if line: - # gdbserver stdout is logged only in verbose + # OpenOCD stdout is logged only in verbose # mode. if VERBOSE: log_output(line, logging.DEBUG) @@ -690,17 +402,13 @@ def _semihost_reader(sock: socket.socket): if os.path.exists(stdout_capture_bin): try: # Parse the same exit sentinel from dumped RAM output as - # from semihosting. + # the target prints before stopping at the completion trap. with open(stdout_capture_bin, "rb") as capture_file: captured = capture_file.read() captured_output, captured_exit_code = split_stdout_capture(captured) if captured_exit_code is not None: - shared["exit_code"] = captured_exit_code - if ( - captured_output - and not shared.get("stdout_streamed") - and not target_failed - ): + exit_code = captured_exit_code + if captured_output and not target_failed: sys.stdout.write(captured_output) sys.stdout.flush() STDOUT_BYTES_EMITTED += len(captured_output.encode("utf-8")) @@ -710,12 +418,8 @@ def _semihost_reader(sock: socket.socket): if "$nucleo_stdout_truncated = 0x1" in gdb_text: err("WARNING: target stdout capture truncated") - if shared.get("exit_code") is not None: - return ( - int(shared["exit_code"]) - if isinstance(shared["exit_code"], int) - else 1 - ) + if exit_code is not None: + return int(exit_code) if isinstance(exit_code, int) else 1 if layout_failed: TARGET_FAILURE = True @@ -741,10 +445,8 @@ def _semihost_reader(sock: socket.socket): return 0 if gdbp.returncode != 0: - target_output_observed = ( - bool(shared.get("stdout_streamed")) or STDOUT_BYTES_EMITTED != 0 - ) - exit_code_observed = shared.get("exit_code") is not None + target_output_observed = STDOUT_BYTES_EMITTED != 0 + exit_code_observed = exit_code is not None if gdb_load_failed_before_target_output( gdb_text, target_output_observed=target_output_observed, @@ -766,7 +468,7 @@ def _semihost_reader(sock: socket.socket): return 0 finally: - # Terminate ST gdbserver + # Terminate OpenOCD. try: stp.terminate() stp.wait(timeout=1.5) @@ -775,16 +477,6 @@ def _semihost_reader(sock: socket.socket): stp.kill() except Exception: pass - # Stop semihost listener - try: - if "semihost_stop" in locals(): - semihost_stop.set() - if "semihost_sock" in locals() and semihost_sock: - semihost_sock.close() - if "semihost_thr" in locals() and semihost_thr: - semihost_thr.join(timeout=0.5) - except Exception: - pass # Remove the temp gdb script try: if "gdb_script_path" in locals(): diff --git a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py index 8709ce448a..5970d031ec 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py +++ b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py @@ -13,22 +13,18 @@ """ import os -import re import logging import sys -import time import tempfile -from nucleo_host.st_tools import connect_args as st_connect_args -from nucleo_host.st_tools import ( - find_cubeprogrammer_cli as st_find_cubeprogrammer_cli, -) -from nucleo_host.st_tools import run_quiet from nucleo_host.flexmem import flexmem_config_build_instructions from nucleo_host.openocd_tools import find_openocd from nucleo_host.openocd_tools import flexmem_script_lines from nucleo_host.openocd_tools import openocd_base_args +from nucleo_host.openocd_tools import run_quiet +from nucleo_host.openocd_tools import serial_from_env from nucleo_host.openocd_tools import speed_khz_from_env +from nucleo_host.openocd_tools import transport_from_env from nucleo_host.symbols import resolve_symbol_with_nm DONE = "FLEXMEM configuration complete; reset target and load test binary." @@ -49,7 +45,7 @@ def configure_logging(): def log_output(output, level): - """Log multiline CubeProgrammer output at the requested level.""" + """Log multiline subprocess output at the requested level.""" if not output: return for line in output.rstrip().splitlines(): @@ -61,87 +57,15 @@ def err(msg): LOG.error("%s", msg) -def find_cubeprogrammer_cli(cp_path): - """Find ``STM32_Programmer_CLI`` using platform environment defaults.""" - return st_find_cubeprogrammer_cli(cp_path, os.environ.get("ST_CUBE_CLT_ROOT", "")) - - -def cubeprogrammer_cli(): - """Return the CubeProgrammer CLI path, or report a helpful error.""" - cli = find_cubeprogrammer_cli(os.environ.get("ST_CUBE_PROG_PATH", "")) - if cli is None: - err("STM32_Programmer_CLI not found; set ST_CUBE_PROG_PATH or ST_CUBE_CLT_ROOT") - return cli - - -def connect_args(mode=None): - """Build CubeProgrammer SWD connection arguments from the environment.""" - # Keep all CubeProgrammer calls on the same probe, speed, and access port. - return st_connect_args( - speed=os.environ.get("STLINK_SPEED", "200"), - serial=os.environ.get("STLINK_SERIAL", ""), - apid=os.environ.get("STLINK_APID", ""), - mode=(mode if mode is not None else os.environ.get("STLINK_CONNECT_MODE")), - ) - - -def reset_target(cli): - """Best-effort target reset through CubeProgrammer.""" - # Reset is best-effort: the subsequent download/halt sequence reports hard - # failures. - args = ["-c", "port=SWD", f"freq={os.environ.get('STLINK_SPEED', '200')}"] - serial = os.environ.get("STLINK_SERIAL", "") - if serial: - args.append(f"sn={serial}") - return run_quiet([cli] + args + ["-rst"]).returncode == 0 - - def resolve_symbol(elf, symbol): """Resolve a symbol with ``nm`` for direct RAM launch setup.""" - # Resolve entry/stack symbols up front so CubeProgrammer can start from RAM - # directly. + # Resolve entry/stack symbols up front so OpenOCD can start from RAM + # directly without relying on a flash boot flow. return resolve_symbol_with_nm( elf, symbol, nm=os.environ.get("NM", "arm-none-eabi-nm") ) -def read_flexmem_value(cli): - """Read the FLEXMEM TCM configuration register via SWD HOTPLUG mode.""" - # HOTPLUG reads avoid resetting the core while the config ELF is parked at - # BKPT. - cp = run_quiet([cli] + connect_args("HOTPLUG") + ["-r32", CM55TCMCR_ADDR, "1"]) - if os.environ.get("FLEXMEM_VERBOSE"): - log_output(cp.stdout, logging.DEBUG) - if cp.returncode != 0: - return None - match = re.search( - rf"{re.escape(CM55TCMCR_ADDR)}\s*:\s*([0-9a-fA-F]{{8}})", cp.stdout - ) - if not match: - return None - return int(match.group(1), 16) - - -def wait_for_flexmem(cli, timeout_s): - """Poll until the FLEXMEM register reports the expected expanded layout.""" - # The register update is fast, but polling absorbs probe/transport latency. - deadline = time.time() + timeout_s - while time.time() < deadline: - value = read_flexmem_value(cli) - if ( - value is not None - and (value & CM55TCMCR_EXPECTED_MASK) == CM55TCMCR_EXPECTED_VALUE - ): - return True - time.sleep(0.2) - return False - - -def selected_backend(): - """Return the configured debug backend name.""" - return os.environ.get("NUCLEO_DEBUG_BACKEND", "stlink").strip().lower() - - def openocd_cli(): """Return the OpenOCD executable path, or report a helpful error.""" openocd = find_openocd(os.environ.get("OPENOCD", "")) @@ -173,8 +97,8 @@ def run_openocd_config(elf, main_thumb, estack_addr, timeout_s): cmd = openocd_base_args( openocd=openocd, speed=speed_khz_from_env(), - serial=os.environ.get("STLINK_SERIAL", ""), - transport=os.environ.get("STLINK_TRANSPORT", "swd"), + serial=serial_from_env(), + transport=transport_from_env(), ) + ["-f", script_path] try: cp = run_quiet(cmd) @@ -213,48 +137,7 @@ def main(): timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "30")) - backend = selected_backend() - if backend == "openocd": - return run_openocd_config(elf, main_thumb, estack_addr, timeout_s) - if backend not in ("stlink", "stm32cube", "cube"): - err(f"Unsupported NUCLEO_DEBUG_BACKEND: {backend}") - return 2 - - cli = cubeprogrammer_cli() - if cli is None: - return 2 - - reset_target(cli) - - # Load the RAM-only config image and seed MSP/PC explicitly because no - # flash boot flow participates in this helper binary. - cmd = ( - [cli] - + connect_args() - + [ - "-halt", - "-d", - elf, - "-coreReg", - f"MSP={estack_addr}", - f"PC={main_thumb}", - "-run", - ] - ) - cp = run_quiet(cmd) - if os.environ.get("FLEXMEM_VERBOSE") or cp.returncode != 0: - log_output(cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR) - if cp.returncode != 0: - err("FLEXMEM config RAM download/start failed") - return cp.returncode - - if not wait_for_flexmem(cli, timeout_s): - err("FLEXMEM configuration register did not reach expected 0x99 value") - return 124 - - LOG.debug("%s", DONE) - reset_target(cli) - return 0 + return run_openocd_config(elf, main_thumb, estack_addr, timeout_s) if __name__ == "__main__": diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py index ddec14b3ae..ed783ac8b4 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py @@ -31,8 +31,7 @@ def build_run_script( "set pagination off", "set confirm off", f"target remote localhost:{port}", - # Semihosting is configured on the ST-LINK GDB server command line; the - # GDB script stays focused on target state and RAM transfers. + # Keep the GDB script focused on target state and RAM transfers. "load", f"tbreak {wrap_main_break}", f"jump {reset_handler_jump}", diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py index 6653e62965..c2d6eb2bf0 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py @@ -6,6 +6,7 @@ import os import shutil +import subprocess DEFAULT_INTERFACE = "interface/stlink.cfg" @@ -27,8 +28,25 @@ def find_openocd(openocd=""): def speed_khz_from_env(default="200"): - """Return adapter speed in kHz using the platform's existing variable.""" - return os.environ.get("OPENOCD_SPEED", os.environ.get("STLINK_SPEED", default)) + """Return adapter speed in kHz.""" + return os.environ.get("OPENOCD_SPEED", default) + + +def serial_from_env(default=""): + """Return the optional OpenOCD adapter serial selector.""" + return os.environ.get("OPENOCD_SERIAL", default) + + +def transport_from_env(default="swd"): + """Return the OpenOCD transport name.""" + return os.environ.get("OPENOCD_TRANSPORT", default).strip().lower() + + +def run_quiet(cmd): + """Run a command with stdout and stderr merged for delayed diagnostics.""" + return subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) def openocd_base_args( @@ -40,7 +58,7 @@ def openocd_base_args( serial="", transport="swd", ): - """Return common OpenOCD arguments for the NUCLEO ST-LINK connection.""" + """Return common OpenOCD arguments for the NUCLEO debug connection.""" args = [ openocd, "-f", @@ -114,7 +132,7 @@ def flexmem_script_lines( f" if {{($value & 0x{expected_mask:x}) == 0x{expected_value:x}}} {{ return }}", " sleep 200", " }", - f" error \"FLEXMEM configuration register did not reach expected 0x{expected_value:x} value\"", + f' error "FLEXMEM configuration register did not reach expected 0x{expected_value:x} value"', "}", "wait_flexmem_configured", "reset run", diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py deleted file mode 100644 index ec32610a78..0000000000 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/st_tools.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) The mlkem-native project authors -# Copyright (c) Arm Ltd. -# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT - -"""Locate STM32CubeCLT tools and build common ST-LINK command arguments.""" - -import os -import shutil -import subprocess - - -def find_cubeprogrammer_cli(cp_path="", st_clt_root=""): - """Find ``STM32_Programmer_CLI`` from explicit paths, CLT root, or PATH.""" - candidates = [] - if cp_path: - if os.path.isdir(cp_path): - candidates.extend( - [ - os.path.join(cp_path, "STM32_Programmer_CLI"), - os.path.join(cp_path, "bin", "STM32_Programmer_CLI"), - ] - ) - else: - candidates.append(cp_path) - if st_clt_root: - candidates.append( - os.path.join( - st_clt_root, - "STM32CubeProgrammer", - "bin", - "STM32_Programmer_CLI", - ) - ) - path_candidate = shutil.which("STM32_Programmer_CLI") - if path_candidate: - candidates.append(path_candidate) - for candidate in candidates: - if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK): - return candidate - return None - - -def connect_args(speed="200", serial="", apid="", mode=None): - """Return CubeProgrammer ``-c`` arguments for one SWD connection.""" - args = ["-c", "port=SWD", f"freq={speed}"] - if mode: - args.append(f"mode={mode}") - if serial: - args.append(f"sn={serial}") - if apid: - args.append(f"ap={apid}") - return args - - -def run_quiet(cmd): - """Run a command with stdout and stderr merged for delayed diagnostics.""" - return subprocess.run( - cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True - ) - - -def derive_clt_root(st_cubeprog: str): - """Infer the STM32CubeCLT root from a CubeProgrammer path if possible.""" - if not st_cubeprog: - return "" - st_clt_root = os.path.dirname(os.path.abspath(st_cubeprog)) - if os.path.basename(st_clt_root).lower().startswith("stm32cubeprogrammer"): - st_clt_root = os.path.dirname(st_clt_root) - return st_clt_root - - -def find_stlink_gdbserver(st_clt_root=""): - """Find ``ST-LINK_gdbserver`` on PATH or below the provided CLT root.""" - stlink_bin = shutil.which("ST-LINK_gdbserver") - candidate = None - if not stlink_bin and st_clt_root: - candidate = os.path.join( - st_clt_root, "STLink-gdb-server", "bin", "ST-LINK_gdbserver" - ) - if os.path.isfile(candidate) and os.access(candidate, os.X_OK): - stlink_bin = candidate - return stlink_bin, candidate - - -def cubeprogrammer_cp_path(st_cubeprog="", st_clt_root="", stlink_bin=""): - """Return the directory to pass as ST-LINK GDB server's ``-cp`` value.""" - cp_path = None - if st_cubeprog: - path = os.path.abspath(st_cubeprog) - if os.path.isdir(path): - cli1 = os.path.join(path, "STM32_Programmer_CLI") - cli2 = os.path.join(path, "bin", "STM32_Programmer_CLI") - if os.path.isfile(cli1) and os.access(cli1, os.X_OK): - cp_path = path - elif os.path.isfile(cli2) and os.access(cli2, os.X_OK): - cp_path = os.path.join(path, "bin") - elif os.path.isfile(path): - cp_path = os.path.dirname(path) - - if cp_path is None and st_clt_root: - cli2 = os.path.join( - os.path.abspath(st_clt_root), - "STM32CubeProgrammer", - "bin", - "STM32_Programmer_CLI", - ) - if os.path.isfile(cli2) and os.access(cli2, os.X_OK): - cp_path = os.path.dirname(cli2) - - if cp_path is None and stlink_bin: - root = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(stlink_bin))) - ) - cli2 = os.path.join(root, "STM32CubeProgrammer", "bin", "STM32_Programmer_CLI") - if os.path.isfile(cli2) and os.access(cli2, os.X_OK): - cp_path = os.path.dirname(cli2) - - return cp_path diff --git a/test/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk index e67aa318c3..3a706bb93c 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/platform.mk +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -34,6 +34,7 @@ CFLAGS += \ --sysroot=$(SYSROOT) \ -DDEVICE=nucleo-n657x0-q \ -DSTM32N657xx \ + -DARMCM55 \ -DNTESTS_FUNC=1 \ -I$(NUCLEO_N657X0_Q_PATH) \ -I$(NUCLEO_N657X0_Q_PATH)/Inc \ diff --git a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py index 33707d372d..905213763b 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py +++ b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py @@ -221,17 +221,22 @@ def test_openocd_flexmem_configure_runs_openocd_script(self): completed = mock.Mock(returncode=0, stdout="") messages = [] env = { - "NUCLEO_DEBUG_BACKEND": "openocd", "OPENOCD": "/usr/bin/openocd", - "STLINK_SPEED": "123", - "STLINK_SERIAL": "SERIAL", + "OPENOCD_SPEED": "123", + "OPENOCD_SERIAL": "SERIAL", } with ( mock.patch.dict(os.environ, env), - mock.patch.object(flexmem_configure, "find_openocd", return_value="/usr/bin/openocd"), - mock.patch.object(flexmem_configure, "run_quiet", return_value=completed) as run, - mock.patch.object(flexmem_configure, "log_output", side_effect=messages.append), + mock.patch.object( + flexmem_configure, "find_openocd", return_value="/usr/bin/openocd" + ), + mock.patch.object( + flexmem_configure, "run_quiet", return_value=completed + ) as run, + mock.patch.object( + flexmem_configure, "log_output", side_effect=messages.append + ), ): rc = flexmem_configure.run_openocd_config( "/tmp/flexmem_config.elf", "0x34064001", "0x30020000", 30 @@ -280,11 +285,9 @@ def fake_popen(cmd, **kwargs): return FakeProcess(returncode=0) env = { - "NUCLEO_DEBUG_BACKEND": "openocd", "OPENOCD": "/usr/bin/openocd", "GDB_PORT": "4567", "GDB_RUN_TIMEOUT": "0", - "STLINK_SEMIHOST_PORT": "4568", } symbol_values = { "mlkem_cmdline_block": None, @@ -298,12 +301,19 @@ def fake_popen(cmd, **kwargs): with ( mock.patch.dict(os.environ, env), - mock.patch.object(exec_wrapper.sys, "argv", ["exec_wrapper.py", "/tmp/test.elf"]), + mock.patch.object( + exec_wrapper.sys, "argv", ["exec_wrapper.py", "/tmp/test.elf"] + ), mock.patch.object(exec_wrapper.os.path, "exists", return_value=True), - mock.patch.object(exec_wrapper, "find_openocd", return_value="/usr/bin/openocd"), - mock.patch.object(exec_wrapper, "resolve_symbol", side_effect=lambda _elf, sym, **_kw: symbol_values[sym]), + mock.patch.object( + exec_wrapper, "find_openocd", return_value="/usr/bin/openocd" + ), + mock.patch.object( + exec_wrapper, + "resolve_symbol", + side_effect=lambda _elf, sym, **_kw: symbol_values[sym], + ), mock.patch.object(exec_wrapper, "popen", side_effect=fake_popen), - mock.patch.object(exec_wrapper, "_wait_for_port") as wait_for_port, mock.patch.object(exec_wrapper.time, "sleep"), mock.patch.object(exec_wrapper.select, "select", return_value=([], [], [])), ): @@ -313,7 +323,6 @@ def fake_popen(cmd, **kwargs): self.assertIn("gdb_port 4567", popen_calls[0]) self.assertNotIn("connect_assert_srst", "\n".join(popen_calls[0])) self.assertEqual(popen_calls[1][0], "arm-none-eabi-gdb") - wait_for_port.assert_not_called() def test_main_recovers_once_after_load_failure(self): """The wrapper invokes FLEXMEM configuration once before retrying.""" @@ -353,7 +362,7 @@ def test_load_failure_recovery_invokes_flexmem_configure(self): env = {"FLEXMEM_CONFIG_ELF": "/tmp/flexmem_config.elf"} with ( - mock.patch.dict(os.environ, env), + mock.patch.dict(os.environ, env, clear=True), mock.patch.object(exec_wrapper.os.path, "exists", return_value=True), mock.patch.object(exec_wrapper, "run", return_value=completed) as run, ): @@ -363,7 +372,10 @@ def test_load_failure_recovery_invokes_flexmem_configure(self): self.assertEqual(cmd[0], exec_wrapper.sys.executable) self.assertTrue(cmd[1].endswith("flexmem_configure.py")) self.assertEqual(cmd[2], "/tmp/flexmem_config.elf") - self.assertEqual(run.call_args.kwargs["env"]["STLINK_CONNECT_MODE"], "UR") + self.assertEqual( + run.call_args.kwargs["env"], + {"FLEXMEM_CONFIG_ELF": "/tmp/flexmem_config.elf"}, + ) def test_main_reports_diagnostics_when_load_recovery_fails(self): """Load-failure diagnostics survive a failed FLEXMEM recovery.""" diff --git a/test/hal/hal.c b/test/hal/hal.c index 0a1b9f23f1..3885d6f49b 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -114,8 +114,12 @@ uint64_t get_cyclecounter(void) { return DWT->CYCCNT; } #elif defined(ARMCM55) /* Cortex-M55: Use dedicated PMU */ +#if defined(STM32N657xx) +#include +#else #include #include +#endif #include "pmu_armv8.h" void enable_cyclecounter(void) From 336b98dc56bcd5610e2f4140866b90c8cf06928e Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Fri, 8 May 2026 13:09:53 +0100 Subject: [PATCH 14/20] nucleo-n657x0-q: harden FLEXMEM recovery and target exits Improve OpenOCD/FLEXMEM handling for the NUCLEO-N657X0-Q platform by retrying FLEXMEM configuration without connect-under-reset after attach failures, resetting the target after runtime output harvesting, and using the recovered FLEXMEM path for GDB load failures. Route target-side _exit(status) through the platform semihosting exit path so libc exit() calls emit the ML-KEM exit sentinel. Treat SIGTRAP without that sentinel as a target failure instead of a successful run. Update the Nucleo platform docs and host-side regression tests for the new reset sequencing, attach fallback, load-failure recovery, default OpenOCD speed, and missing-sentinel handling. Signed-off-by: Brendan Moran --- scripts/autogen | 7 +- scripts/tests | 4 +- .../platform/nucleo-n657x0-q/README.md | 25 ++-- .../platform/nucleo-n657x0-q/exec_wrapper.py | 20 ++- .../nucleo-n657x0-q/flexmem_configure.py | 47 +++++- .../nucleo-n657x0-q/nucleo_host/gdb_script.py | 3 + .../nucleo_host/openocd_tools.py | 13 +- .../platform/nucleo-n657x0-q/src/cmdline.c | 4 +- .../nucleo-n657x0-q/test_nucleo_host.py | 141 ++++++++++++++++++ 9 files changed, 237 insertions(+), 27 deletions(-) diff --git a/scripts/autogen b/scripts/autogen index 3de57f7e31..ba7801fb52 100755 --- a/scripts/autogen +++ b/scripts/autogen @@ -2344,9 +2344,10 @@ def gen_macro_undefs(extra_notes=None): yield "" yield "#if !defined(MLK_CONFIG_MONOBUILD_KEEP_SHARED_HEADERS)" yield from gen_monolithic_undef_all_core( - filt=lambda c: ( - not native(c) and k_generic(c) and not fips202(c) and "cbmc.h" not in c - ), + filt=lambda c: not native(c) + and k_generic(c) + and not fips202(c) + and "cbmc.h" not in c, desc="MLK_CONFIG_PARAMETER_SET-generic files", ) # Handle cbmc.h manually -- most #define's therein are only defined when CBMC is set diff --git a/scripts/tests b/scripts/tests index 250dea3aa7..147c62f9a1 100755 --- a/scripts/tests +++ b/scripts/tests @@ -243,9 +243,7 @@ class TEST_TYPES(Enum): if str.lower(e.name) == str.lower(s): return e raise Exception( - f"Could not find example {s}. Examples: { - list(map(lambda e: str.lower(e.name), TEST_TYPES.examples())) - }" + f"Could not find example {s}. Examples: {list(map(lambda e: str.lower(e.name), TEST_TYPES.examples()))}" ) def __str__(self): diff --git a/test/baremetal/platform/nucleo-n657x0-q/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md index 289a991d19..c10b03d48b 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/README.md +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -31,20 +31,19 @@ binaries are loaded into RAM; nothing is written to flash. 6. The wrapper continues execution, dumps the RAM stdout capture buffer, and uses `[[MLKEM-EXIT:]]` as the exit sentinel. -The reset distinction is critical. The FLEXMEM config stage must use -connect-under-reset for recovery, while the runtime test stage must not request -connect-under-reset because that can lose the just-latched FLEXMEM layout. +The FLEXMEM config stage uses connect-under-reset. The runtime test stage does +not request connect-under-reset. ## Connection The host tools connect to the NUCLEO-N657X0-Q through the on-board debug probe using SWD. OpenOCD uses `interface/stlink.cfg`, `target/stm32n6x.cfg`, -`transport select swd`, and `adapter speed ${OPENOCD_SPEED:-200}` by default. +`transport select swd`, and `adapter speed ${OPENOCD_SPEED:-8000}` by default. Useful environment variables: ``` -export OPENOCD_SPEED=200 +export OPENOCD_SPEED=8000 export OPENOCD_SERIAL= export GDB_PORT=3333 ``` @@ -53,6 +52,10 @@ export GDB_PORT=3333 override the executable, script names, and transport when needed. `GDB`, `NM`, and `READELF` can override the binary utilities. +The 8 MHz SWD default matches the ST tools' successful recovery connection for +this board. Very low SWD speeds can leave OpenOCD waiting on the SWD DP in +some post-test states. + ## FLEXMEM Expansion and Reboot `flexmem_configure.py` performs the memory expansion before any RAM-resident @@ -67,8 +70,9 @@ test is loaded: 4. Seed `MSP=<_estack>` and `PC=`, then `resume` the config binary directly from RAM. 5. Poll `SYSCFG->CM55TCMCR` at `0x56008008` until `(value & 0xff) == 0x99`. -6. Run `reset run` so the new FLEXMEM layout is applied before the test ELF is - loaded, then shut down OpenOCD. +6. Switch OpenOCD reset handling back to a Cortex-M system reset with + `reset_config none`, then run `reset run` so the new FLEXMEM layout is + applied before the test ELF is loaded. The reset after configuration is required: FLEXMEM register writes are latched for the next boot, and the reset also clears the RAM contents used by the config @@ -78,8 +82,9 @@ helper. After FLEXMEM expansion and reset, `run_test_after_flexmem.py` delegates to `exec_wrapper.py`. The runtime OpenOCD server uses -`reset_config srst_only srst_nogate` and does not request -`connect_assert_srst`. The wrapper creates a temporary GDB script and runs: +`reset_config srst_only srst_nogate` and halts without requesting another +reset. The wrapper +creates a temporary GDB script and runs: ``` arm-none-eabi-gdb --batch -x @@ -99,6 +104,8 @@ The generated GDB script follows this order: then `continue` runs the test. 7. At completion, GDB dumps the RAM stdout capture buffer and fault diagnostics if needed. +8. GDB asks OpenOCD to switch to `reset_config none` and `reset run` after + harvesting output so the next FLEXMEM setup starts from a fresh boot state. The wrapper terminates OpenOCD after each run. If `load` fails before target output, or if the target enters `HardFault_Handler`, the wrapper can re-run diff --git a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py index da25a67c85..b26bc52e7f 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -441,6 +441,13 @@ def _resolve_symbol_addr(elf_path: str, sym: str): return 1 if "Program received signal SIGTRAP" in gdb_text: + if stdout_capture_addr and stdout_capture_len_addr: + TARGET_FAILURE = True + TARGET_FAILURE_KIND = "missing-exit-sentinel" + if not SUPPRESS_RETRYABLE_DIAGNOSTICS: + err("FAIL!") + err("target stopped at SIGTRAP without ML-KEM exit sentinel") + return 1 info("[exec_wrapper] completion trap observed without exit sentinel") return 0 @@ -506,13 +513,20 @@ def main(): can_retry_transport = transport_retries < attempts - 1 can_retry_hardfault = hardfault_recoveries < hardfault_attempts can_retry_load_failure = load_failure_recoveries < load_failure_attempts - SUPPRESS_RETRYABLE_DIAGNOSTICS = can_retry_transport or can_retry_hardfault + SUPPRESS_RETRYABLE_DIAGNOSTICS = ( + can_retry_transport or can_retry_hardfault or can_retry_load_failure + ) last_rc = _run_once() if last_rc == 0: return 0 if TARGET_FAILURE_KIND == "load-failed": if can_retry_load_failure: load_failure_recoveries += 1 + if not VERBOSE: + err( + "[exec_wrapper] GDB load failed before target output; " + "re-running FLEXMEM config" + ) if _recover_after_load_failure(): if VERBOSE: err( @@ -528,6 +542,10 @@ def main(): err(LAST_LOAD_FAILURE_DIAGNOSTICS) return last_rc err("FAIL!") + err( + "GDB load failed before target output and FLEXMEM recovery " + "attempts are exhausted" + ) err(f"gdb batch failed with code {last_rc}") if LAST_LOAD_FAILURE_DIAGNOSTICS: log_output(LAST_LOAD_FAILURE_DIAGNOSTICS, logging.ERROR) diff --git a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py index 5970d031ec..3b91079c7d 100755 --- a/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py +++ b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py @@ -74,12 +74,8 @@ def openocd_cli(): return openocd -def run_openocd_config(elf, main_thumb, estack_addr, timeout_s): - """Download and run the config ELF using OpenOCD.""" - openocd = openocd_cli() - if openocd is None: - return 2 - +def _openocd_config_cmd(openocd, elf, main_thumb, estack_addr, timeout_s, under_reset): + """Build the OpenOCD command for one FLEXMEM configuration attempt.""" script_lines = flexmem_script_lines( elf=elf, main_thumb=main_thumb, @@ -88,6 +84,7 @@ def run_openocd_config(elf, main_thumb, estack_addr, timeout_s): flexmem_addr=CM55TCMCR_ADDR, expected_mask=CM55TCMCR_EXPECTED_MASK, expected_value=CM55TCMCR_EXPECTED_VALUE, + connect_under_reset=under_reset, ) with tempfile.NamedTemporaryFile("w", delete=False, suffix=".cfg") as script: script.write("\n".join(script_lines)) @@ -100,6 +97,22 @@ def run_openocd_config(elf, main_thumb, estack_addr, timeout_s): serial=serial_from_env(), transport=transport_from_env(), ) + ["-f", script_path] + return cmd, script_path + + +def _openocd_init_failed(output): + """Return whether OpenOCD failed before it could attach to the target.""" + lower = (output or "").lower() + return "init mode failed" in lower or "unable to connect to the target" in lower + + +def _run_openocd_config_once( + openocd, elf, main_thumb, estack_addr, timeout_s, under_reset +): + """Run one OpenOCD FLEXMEM configuration attempt.""" + cmd, script_path = _openocd_config_cmd( + openocd, elf, main_thumb, estack_addr, timeout_s, under_reset + ) try: cp = run_quiet(cmd) finally: @@ -107,6 +120,28 @@ def run_openocd_config(elf, main_thumb, estack_addr, timeout_s): os.unlink(script_path) except OSError: pass + return cp + + +def run_openocd_config(elf, main_thumb, estack_addr, timeout_s): + """Download and run the config ELF using OpenOCD.""" + openocd = openocd_cli() + if openocd is None: + return 2 + + cp = _run_openocd_config_once( + openocd, elf, main_thumb, estack_addr, timeout_s, under_reset=True + ) + if cp.returncode != 0 and _openocd_init_failed(cp.stdout): + if os.environ.get("FLEXMEM_VERBOSE"): + log_output(cp.stdout, logging.DEBUG) + LOG.debug( + "OpenOCD connect-under-reset attach failed; retrying FLEXMEM " + "configuration without connect_assert_srst" + ) + cp = _run_openocd_config_once( + openocd, elf, main_thumb, estack_addr, timeout_s, under_reset=False + ) if os.environ.get("FLEXMEM_VERBOSE") or cp.returncode != 0: log_output(cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR) if cp.returncode != 0: diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py index ed783ac8b4..4a9e4e9783 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py @@ -60,6 +60,9 @@ def build_run_script( "p/x $nucleo_stdout_truncated", ] gdb_lines += fault_diagnostic_commands() + # Leave the board in a fresh boot state for the next FLEXMEM setup. This + # runs after stdout/fault harvesting and does not affect the current test. + gdb_lines += ["monitor reset_config none", "monitor reset run"] return gdb_lines diff --git a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py index c2d6eb2bf0..515c961da9 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py @@ -27,7 +27,7 @@ def find_openocd(openocd=""): return None -def speed_khz_from_env(default="200"): +def speed_khz_from_env(default="8000"): """Return adapter speed in kHz.""" return os.environ.get("OPENOCD_SPEED", default) @@ -54,7 +54,7 @@ def openocd_base_args( openocd="openocd", interface=None, target=None, - speed="200", + speed="8000", serial="", transport="swd", ): @@ -79,7 +79,7 @@ def runtime_gdbserver_cmd( *, openocd="openocd", port=3333, - speed="200", + speed="8000", serial="", transport="swd", ): @@ -114,11 +114,15 @@ def flexmem_script_lines( flexmem_addr="0x56008008", expected_mask=0xFF, expected_value=0x99, + connect_under_reset=True, ): """Return an OpenOCD TCL script for RAM-loading the FLEXMEM helper.""" quoted_elf = "{" + elf.replace("\\", "\\\\").replace("}", "\\}") + "}" + reset_config = "reset_config srst_only srst_nogate" + if connect_under_reset: + reset_config += " connect_assert_srst" return [ - "reset_config srst_only srst_nogate connect_assert_srst", + reset_config, "init", "reset halt", f"load_image {quoted_elf}", @@ -135,6 +139,7 @@ def flexmem_script_lines( f' error "FLEXMEM configuration register did not reach expected 0x{expected_value:x} value"', "}", "wait_flexmem_configured", + "reset_config none", "reset run", "shutdown", ] diff --git a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c index 3af69e730b..c96d3b6604 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c +++ b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c @@ -67,7 +67,7 @@ __attribute__((noinline)) static void nucleo_init_dtcm_ecc(void) extern int __real_main(int argc, char *argv[]); int __wrap_main(int unused_argc, char *unused_argv[]); -static void semihosting_exit_with_rc(int rc) +__attribute__((noreturn)) static void semihosting_exit_with_rc(int rc) { if (rc == 0) { @@ -88,6 +88,8 @@ static void semihosting_exit_with_rc(int rc) } } +void _exit(int status) { semihosting_exit_with_rc(status); } + void Error_Handler(void) { HardFault_Handler(); } /* Wrap main: build argc/argv from cmdline and forward to __real_main. */ diff --git a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py index 905213763b..3177b06580 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py +++ b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py @@ -17,6 +17,7 @@ from nucleo_host.openocd_tools import flexmem_script_lines from nucleo_host.openocd_tools import openocd_base_args from nucleo_host.openocd_tools import runtime_gdbserver_cmd +from nucleo_host.openocd_tools import speed_khz_from_env from nucleo_host.results import fault_info_from_gdb from nucleo_host.results import gdb_load_failed from nucleo_host.results import gdb_load_failed_before_target_output @@ -178,6 +179,11 @@ def test_openocd_base_args_use_stlink_stm32n6_target(self): args.index("transport select swd"), args.index("target/stm32n6x.cfg") ) + def test_openocd_default_speed_matches_st_recovery_speed(self): + """The platform default uses the robust ST-LINK recovery speed.""" + with mock.patch.dict(os.environ, {}, clear=True): + self.assertEqual(speed_khz_from_env(), "8000") + def test_openocd_runtime_cmd_avoids_under_reset(self): """Runtime OpenOCD server does not request connect-under-reset.""" cmd = runtime_gdbserver_cmd( @@ -192,6 +198,7 @@ def test_openocd_runtime_cmd_avoids_under_reset(self): self.assertIn("gdb_port 4444", cmd) self.assertIn("reset_config srst_only srst_nogate", cmd) self.assertIn("halt", cmd) + self.assertNotIn("reset halt", joined) self.assertNotIn("connect_assert_srst", joined) def test_openocd_flexmem_script_uses_under_reset_and_polls_register(self): @@ -214,8 +221,24 @@ def test_openocd_flexmem_script_uses_under_reset_and_polls_register(self): self.assertIn("reg pc 0x34064001", lines) self.assertIn("read_memory 0x56008008 32 1", joined) self.assertIn("== 0x99", joined) + self.assertLess(lines.index("reset_config none"), lines.index("reset run")) self.assertIn("reset run", lines) + def test_openocd_flexmem_script_can_skip_under_reset(self): + """The fallback FLEXMEM script can attach without connect-under-reset.""" + lines = flexmem_script_lines( + elf="/tmp/flexmem_config.elf", + main_thumb="0x34064001", + estack_addr="0x30020000", + timeout_ms=30000, + connect_under_reset=False, + ) + joined = "\n".join(lines) + + self.assertIn("reset_config srst_only srst_nogate", lines) + self.assertNotIn("connect_assert_srst", joined) + self.assertIn("reset halt", lines) + def test_openocd_flexmem_configure_runs_openocd_script(self): """OpenOCD FLEXMEM backend writes and runs an OpenOCD script.""" completed = mock.Mock(returncode=0, stdout="") @@ -252,6 +275,39 @@ def test_openocd_flexmem_configure_runs_openocd_script(self): script_path = cmd[-1] self.assertFalse(os.path.exists(script_path)) + def test_openocd_flexmem_configure_retries_without_under_reset_after_init_failure( + self, + ): + """OpenOCD FLEXMEM retries a normal attach if under-reset init fails.""" + failed = mock.Mock( + returncode=1, + stdout="Error: init mode failed (unable to connect to the target)", + ) + completed = mock.Mock(returncode=0, stdout="") + env = { + "OPENOCD": "/usr/bin/openocd", + } + + with ( + mock.patch.dict(os.environ, env), + mock.patch.object( + flexmem_configure, "find_openocd", return_value="/usr/bin/openocd" + ), + mock.patch.object( + flexmem_configure, "run_quiet", side_effect=[failed, completed] + ) as run, + ): + rc = flexmem_configure.run_openocd_config( + "/tmp/flexmem_config.elf", "0x34064001", "0x30020000", 30 + ) + + self.assertEqual(rc, 0) + self.assertEqual(run.call_count, 2) + first_script = run.call_args_list[0].args[0][-1] + second_script = run.call_args_list[1].args[0][-1] + self.assertFalse(os.path.exists(first_script)) + self.assertFalse(os.path.exists(second_script)) + def test_exec_wrapper_openocd_backend_builds_runtime_server(self): """The OpenOCD backend starts OpenOCD without semihost TCP setup.""" popen_calls = [] @@ -324,6 +380,83 @@ def fake_popen(cmd, **kwargs): self.assertNotIn("connect_assert_srst", "\n".join(popen_calls[0])) self.assertEqual(popen_calls[1][0], "arm-none-eabi-gdb") + def test_exec_wrapper_rejects_sigtrap_without_exit_sentinel(self): + """A bare semihosting SIGTRAP is not a successful target exit.""" + popen_calls = [] + messages = [] + + class FakeProcess: + def __init__(self, returncode=None, communicate_text=("", "")): + self.returncode = returncode + self.communicate_text = communicate_text + self.stdout = mock.Mock() + self.stdout.read.return_value = "" + self.stdout.readline.return_value = "" + + def poll(self): + return self.returncode + + def wait(self, timeout=None): + return self.returncode + + def terminate(self): + self.returncode = 0 + + def kill(self): + self.returncode = -9 + + def communicate(self, timeout=None): + return self.communicate_text + + def fake_popen(cmd, **kwargs): + popen_calls.append(cmd) + if len(popen_calls) == 1: + return FakeProcess(returncode=None) + return FakeProcess( + returncode=0, + communicate_text=("Program received signal SIGTRAP\n", ""), + ) + + env = { + "OPENOCD": "/usr/bin/openocd", + "GDB_RUN_TIMEOUT": "0", + } + symbol_values = { + "mlkem_cmdline_block": "0x10000", + "mlk_cmdline_block": None, + "__wrap_main": "0x200", + "Reset_Handler": "0x4", + "nucleo_stdout_capture": "0x34080000", + "nucleo_stdout_capture_len": "0x30000100", + "nucleo_stdout_capture_truncated": None, + } + + with ( + mock.patch.dict(os.environ, env), + mock.patch.object( + exec_wrapper.sys, "argv", ["exec_wrapper.py", "/tmp/test.elf"] + ), + mock.patch.object(exec_wrapper.os.path, "exists", return_value=True), + mock.patch.object( + exec_wrapper, "find_openocd", return_value="/usr/bin/openocd" + ), + mock.patch.object( + exec_wrapper, + "resolve_symbol", + side_effect=lambda _elf, sym, **_kw: symbol_values[sym], + ), + mock.patch.object(exec_wrapper, "popen", side_effect=fake_popen), + mock.patch.object(exec_wrapper.time, "sleep"), + mock.patch.object(exec_wrapper.select, "select", return_value=([], [], [])), + mock.patch.object(exec_wrapper, "err", side_effect=messages.append), + ): + self.assertEqual(exec_wrapper._run_once(), 1) + + self.assertIn("FAIL!", messages) + self.assertIn( + "target stopped at SIGTRAP without ML-KEM exit sentinel", messages + ) + def test_main_recovers_once_after_load_failure(self): """The wrapper invokes FLEXMEM configuration once before retrying.""" run_results = iter([23, 0]) @@ -350,6 +483,7 @@ def fake_run_once(): mock.patch.object( exec_wrapper, "_recover_after_load_failure", return_value=True ) as recover, + mock.patch.object(exec_wrapper, "err"), mock.patch.object(exec_wrapper.time, "sleep"), ): self.assertEqual(exec_wrapper.main(), 0) @@ -438,6 +572,10 @@ def fake_run_once(): recover.assert_not_called() self.assertIn("FAIL!", messages) + self.assertIn( + "GDB load failed before target output and FLEXMEM recovery attempts are exhausted", + messages, + ) self.assertIn("gdb batch failed with code 23", messages) self.assertIn("Load failed\n", messages) @@ -488,6 +626,9 @@ def test_build_run_script_contains_required_sequence(self): self.assertIn(expected_dump, gdb_lines) self.assertIn("p/x $nucleo_stdout_truncated", gdb_lines) self.assertIn("echo CFSR=", gdb_lines) + self.assertEqual( + gdb_lines[-2:], ["monitor reset_config none", "monitor reset run"] + ) if __name__ == "__main__": From 848e4e11a0c528abfb46905211e98d53074474c2 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Tue, 12 May 2026 11:05:50 +0100 Subject: [PATCH 15/20] Add NUCLEO-N657X0-Q CI and benchmark jobs Signed-off-by: Brendan Moran --- .github/actionlint.yaml | 1 + .github/workflows/bench.yml | 11 +++++++++ .github/workflows/ci.yml | 23 +++++++++++++++++++ .../platform/nucleo-n657x0-q/platform.mk | 13 ++++++++++- 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 34c37e15d7..cfce88ceb5 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -10,3 +10,4 @@ self-hosted-runner: - pqcp-arm64 - pqcp-ppc64 - pqcp-x64 + - self-hosted-nucleo-n657x0 diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 20d95b6e37..9f7f055bb0 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -79,11 +79,22 @@ jobs: bench_extra_args: "-r" nix_shell: '' cross_prefix: "" + - system: nucleo-n657x0 + name: Arm Cortex-M55 (NUCLEO-N657X0-Q) benchmarks + bench_pmu: PMU + archflags: "" + cflags: "" + ldflags: "" + bench_extra_args: "" + nix_shell: nucleo-n657x0-q + cross_prefix: "" if: github.repository_owner == 'pq-code-package' && (github.event.label.name == 'benchmark' || github.ref == 'refs/heads/main') runs-on: self-hosted-${{ matrix.target.system }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/bench + env: + EXTRA_MAKEFILE: ${{ matrix.target.system == 'nucleo-n657x0' && 'test/baremetal/platform/nucleo-n657x0-q/platform.mk' || '' }} with: name: ${{ matrix.target.name }} cflags: ${{ matrix.target.cflags }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebec2df791..68a4c419e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,6 +191,29 @@ jobs: nix-shell: '' gh_token: ${{ secrets.GITHUB_TOKEN }} cflags: "-DMLKEM_DEBUG -DMLK_FORCE_PPC64LE" + nucleo_n657x0_q_tests: + if: github.repository_owner == 'pq-code-package' && !github.event.pull_request.head.repo.fork + name: Functional tests (NUCLEO-N657X0-Q) + runs-on: self-hosted-nucleo-n657x0 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: baremetal build + test + uses: ./.github/actions/functest + env: + EXTRA_MAKEFILE: test/baremetal/platform/nucleo-n657x0-q/platform.mk + with: + nix-shell: nucleo-n657x0-q + gh_token: ${{ secrets.GITHUB_TOKEN }} + opt: no_opt + func: true + kat: false + acvp: false + wycheproof: false + examples: false + stack: false + unit: false + alloc: false + rng_fail: false backend_tests: name: AArch64 FIPS202 backends (${{ matrix.backend }}) strategy: diff --git a/test/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk index 3a706bb93c..7ff7d01cb5 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/platform.mk +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -132,7 +132,18 @@ FLEXMEM_CONFIG_SOURCES := \ .PHONY: flexmem_config run_flexmem_config run_flexmem_test -run_kat run_acvp run_bench run_func run_unit run_alloc: run_flexmem_config +NUCLEO_N657X0_Q_RUN_TARGETS := \ + run_func run_func_512 run_func_768 run_func_1024 \ + run_kat run_kat_512 run_kat_768 run_kat_1024 \ + run_acvp \ + run_bench run_bench_512 run_bench_768 run_bench_1024 \ + run_bench_components run_bench_components_512 run_bench_components_768 run_bench_components_1024 \ + run_unit run_unit_512 run_unit_768 run_unit_1024 \ + run_alloc run_alloc_512 run_alloc_768 run_alloc_1024 \ + run_rng_fail run_rng_fail_512 run_rng_fail_768 run_rng_fail_1024 \ + run_wycheproof + +$(NUCLEO_N657X0_Q_RUN_TARGETS): run_flexmem_config flexmem_config: $(FLEXMEM_CONFIG_ELF) From 1ad95e2865cebdbc5c5f48507e977bfc11697040 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Tue, 12 May 2026 13:53:03 +0100 Subject: [PATCH 16/20] Adjust test result handling Signed-off-by: Brendan Moran --- scripts/tests | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/tests b/scripts/tests index 147c62f9a1..80e58f9771 100755 --- a/scripts/tests +++ b/scripts/tests @@ -759,6 +759,10 @@ class Tests: if resultss is None: self.check_fail() + return + + if len(self.failed) > 0: + self.check_fail() # NOTE: There will only be one items in resultss, as we haven't yet decided how to write both opt/no-opt benchmark results for k, results in resultss.items(): From cdad34432bf687774a35f669a4e24bb21b4bac67 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Tue, 12 May 2026 16:29:49 +0100 Subject: [PATCH 17/20] Add support for cycle counter on Cortex-M Signed-off-by: Brendan Moran --- .github/workflows/bench.yml | 2 +- scripts/tests | 4 +-- .../platform/nucleo-n657x0-q/platform.mk | 4 +-- test/hal/hal.c | 33 ++++++++++++++++++- test/mk/config.mk | 4 +++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 9f7f055bb0..5fa1c2880d 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -81,7 +81,7 @@ jobs: cross_prefix: "" - system: nucleo-n657x0 name: Arm Cortex-M55 (NUCLEO-N657X0-Q) benchmarks - bench_pmu: PMU + bench_pmu: CYCCNT archflags: "" cflags: "" ldflags: "" diff --git a/scripts/tests b/scripts/tests index 80e58f9771..ddd456a62a 100755 --- a/scripts/tests +++ b/scripts/tests @@ -1359,8 +1359,8 @@ def cli(): bench_parser.add_argument( "-c", "--cycles", - help="Method for counting clock cycles. PMU requires (user-space) access to the Arm Performance Monitor Unit (PMU). PERF requires a kernel with perf support. MAC works on some Apple platforms, at least Apple M1.", - choices=["NO", "PMU", "PERF", "MAC"], + help="Method for counting clock cycles. CYCCNT uses the Cortex-M DWT cycle counter. PMU requires (user-space) access to the Arm Performance Monitor Unit (PMU). PERF requires a kernel with perf support. MAC works on some Apple platforms, at least Apple M1.", + choices=["NO", "CYCCNT", "PMU", "PERF", "MAC"], type=str.upper, required=True, ) diff --git a/test/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk index 7ff7d01cb5..0ad85f30bb 100644 --- a/test/baremetal/platform/nucleo-n657x0-q/platform.mk +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -9,8 +9,8 @@ BUILD_DIR ?= test/build CROSS_PREFIX=arm-none-eabi- CC=gcc -# Use PMU cycle counting by default -CYCLES ?= PMU +# Use the Cortex-M DWT cycle counter by default +CYCLES ?= CYCCNT # Short benchmark runs for testing MLK_BENCHMARK_NWARMUP ?= 1 diff --git a/test/hal/hal.c b/test/hal/hal.c index 3885d6f49b..c3cd2067c6 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -42,7 +42,38 @@ #include "hal.h" -#if defined(PMU_CYCLES) +#if defined(CYCCNT_CYCLES) + +#if defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_8_1M_MAIN__) + +#if defined(STM32N657xx) +#include +#elif defined(ARMCM55) +#include +#include +#elif defined(ARMCM33) +#include +#include +#else +#error "CYCCNT_CYCLES on Arm M-profile requires a CMSIS device header" +#endif + +void enable_cyclecounter(void) +{ + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CYCCNT = 0; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; +} + +void disable_cyclecounter(void) { DWT->CTRL &= ~DWT_CTRL_CYCCNTENA_Msk; } + +uint64_t get_cyclecounter(void) { return DWT->CYCCNT; } + +#else /* !(__ARM_ARCH_8M_MAIN__ || __ARM_ARCH_8_1M_MAIN__) */ +#error CYCCNT_CYCLES option only supported on Arm M-profile +#endif /* !(__ARM_ARCH_8M_MAIN__ || __ARM_ARCH_8_1M_MAIN__) */ + +#elif defined(PMU_CYCLES) #if defined(__x86_64__) diff --git a/test/mk/config.mk b/test/mk/config.mk index df8708246d..c8dee57d10 100644 --- a/test/mk/config.mk +++ b/test/mk/config.mk @@ -67,6 +67,10 @@ ifeq ($(CYCLES),PMU) CFLAGS += -DPMU_CYCLES endif +ifeq ($(CYCLES),CYCCNT) + CFLAGS += -DCYCCNT_CYCLES +endif + ifeq ($(CYCLES),PERF) CFLAGS += -DPERF_CYCLES endif From 869e9dc6c1d0b13cb1e5d38347a77f372ba8c0ba Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Thu, 14 May 2026 14:25:35 +0100 Subject: [PATCH 18/20] Disable other CI benchmarks, run autogen. Signed-off-by: Brendan Moran --- .github/workflows/bench.yml | 246 ++++++++++++++++++------------------ flake.nix | 2 +- test/hal/hal.c | 6 +- 3 files changed, 127 insertions(+), 127 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 5fa1c2880d..4449417da1 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -28,57 +28,57 @@ jobs: fail-fast: true matrix: target: - - system: rpi4 - name: Arm Cortex-A72 (Raspberry Pi 4) benchmarks - bench_pmu: PMU - archflags: -mcpu=cortex-a72 -DMLK_SYS_AARCH64_SLOW_BARREL_SHIFTER - cflags: "-flto -DMLK_FORCE_AARCH64" - ldflags: "-flto" - bench_extra_args: "" - nix_shell: bench - - system: rpi5 - name: Arm Cortex-A76 (Raspberry Pi 5) benchmarks - bench_pmu: PERF - archflags: "-mcpu=cortex-a76 -march=armv8.2-a" - cflags: "-flto -DMLK_FORCE_AARCH64" - ldflags: "-flto" - bench_extra_args: "" - nix_shell: bench - cross_prefix: "" - - system: a55 - name: Arm Cortex-A55 (Snapdragon 888) benchmarks - bench_pmu: PERF - archflags: "-mcpu=cortex-a55 -march=armv8.2-a" - cflags: "-flto -DMLK_FORCE_AARCH64 -DMLK_CONFIG_FIPS202_BACKEND_FILE=\\\\\\\"fips202/native/aarch64/x1_scalar.h\\\\\\\"" - ldflags: "-flto -static" - bench_extra_args: -w exec-on-a55 - nix_shell: bench - - system: bpi - name: SpacemiT K1 8 (Banana Pi F3) benchmarks - bench_pmu: PERF - archflags: "-march=rv64imafdcv_zicsr_zifencei" - cflags: "" - ldflags: "-static" - bench_extra_args: -w exec-on-bpi - cross_prefix: riscv64-unknown-linux-gnu- - nix_shell: cross-riscv64 - - system: m1-mac-mini - name: Mac Mini (M1, 2020) benchmarks - bench_pmu: MAC - archflags: "-mcpu=apple-m1 -march=armv8.4-a+sha3" - cflags: "-flto" - ldflags: "-flto" - bench_extra_args: "-r" - nix_shell: bench - - system: pqcp-ppc64 - name: ppc64le (POWER10) benchmarks - bench_pmu: PERF - archflags: "-mcpu=native" - cflags: "-flto -DMLK_FORCE_PPC64LE" - ldflags: "-flto" - bench_extra_args: "-r" - nix_shell: '' - cross_prefix: "" + # - system: rpi4 + # name: Arm Cortex-A72 (Raspberry Pi 4) benchmarks + # bench_pmu: PMU + # archflags: -mcpu=cortex-a72 -DMLK_SYS_AARCH64_SLOW_BARREL_SHIFTER + # cflags: "-flto -DMLK_FORCE_AARCH64" + # ldflags: "-flto" + # bench_extra_args: "" + # nix_shell: bench + # - system: rpi5 + # name: Arm Cortex-A76 (Raspberry Pi 5) benchmarks + # bench_pmu: PERF + # archflags: "-mcpu=cortex-a76 -march=armv8.2-a" + # cflags: "-flto -DMLK_FORCE_AARCH64" + # ldflags: "-flto" + # bench_extra_args: "" + # nix_shell: bench + # cross_prefix: "" + # - system: a55 + # name: Arm Cortex-A55 (Snapdragon 888) benchmarks + # bench_pmu: PERF + # archflags: "-mcpu=cortex-a55 -march=armv8.2-a" + # cflags: "-flto -DMLK_FORCE_AARCH64 -DMLK_CONFIG_FIPS202_BACKEND_FILE=\\\\\\\"fips202/native/aarch64/x1_scalar.h\\\\\\\"" + # ldflags: "-flto -static" + # bench_extra_args: -w exec-on-a55 + # nix_shell: bench + # - system: bpi + # name: SpacemiT K1 8 (Banana Pi F3) benchmarks + # bench_pmu: PERF + # archflags: "-march=rv64imafdcv_zicsr_zifencei" + # cflags: "" + # ldflags: "-static" + # bench_extra_args: -w exec-on-bpi + # cross_prefix: riscv64-unknown-linux-gnu- + # nix_shell: cross-riscv64 + # - system: m1-mac-mini + # name: Mac Mini (M1, 2020) benchmarks + # bench_pmu: MAC + # archflags: "-mcpu=apple-m1 -march=armv8.4-a+sha3" + # cflags: "-flto" + # ldflags: "-flto" + # bench_extra_args: "-r" + # nix_shell: bench + # - system: pqcp-ppc64 + # name: ppc64le (POWER10) benchmarks + # bench_pmu: PERF + # archflags: "-mcpu=native" + # cflags: "-flto -DMLK_FORCE_PPC64LE" + # ldflags: "-flto" + # bench_extra_args: "-r" + # nix_shell: '' + # cross_prefix: "" - system: nucleo-n657x0 name: Arm Cortex-M55 (NUCLEO-N657X0-Q) benchmarks bench_pmu: CYCCNT @@ -107,75 +107,75 @@ jobs: nix-shell: ${{ matrix.target.nix_shell }} cross_prefix: ${{ matrix.target.cross_prefix }} - ec2_all: - name: ${{ matrix.target.name }} - permissions: - contents: write - pull-requests: write - id-token: write - strategy: - fail-fast: false - matrix: - target: - - name: Graviton2 - ec2_instance_type: t4g.small - ec2_ami: ubuntu-latest (aarch64) - archflags: -mcpu=cortex-a76 -march=armv8.2-a - cflags: "-flto -DMLK_FORCE_AARCH64" - ldflags: "-flto" - perf: PERF - - name: Graviton3 - ec2_instance_type: c7g.medium - ec2_ami: ubuntu-latest (aarch64) - archflags: -march=armv8.4-a+sha3 - cflags: "-flto -DMLK_FORCE_AARCH64" - ldflags: "-flto" - perf: PERF - - name: Graviton4 - ec2_instance_type: c8g.medium - ec2_ami: ubuntu-latest (aarch64) - archflags: -march=armv9-a+sha3 - cflags: "-flto -DMLK_FORCE_AARCH64" - ldflags: "-flto" - perf: PERF - - name: AMD EPYC 4th gen (c7a) - ec2_instance_type: c7a.medium - ec2_ami: ubuntu-latest (x86_64) - archflags: -mavx2 -mbmi2 -mpopcnt -march=znver4 - cflags: "-flto -DMLK_FORCE_X86_64" - ldflags: "-flto" - perf: PMU - - name: Intel Xeon 4th gen (c7i) - ec2_instance_type: c7i.metal-24xl - ec2_ami: ubuntu-latest (x86_64) - archflags: -mavx2 -mbmi2 -mpopcnt -march=sapphirerapids - cflags: "-flto -DMLK_FORCE_X86_64" - ldflags: "-flto" - perf: PMU - - name: AMD EPYC 3rd gen (c6a) - ec2_instance_type: c6a.large - ec2_ami: ubuntu-latest (x86_64) - archflags: -mavx2 -mbmi2 -mpopcnt -march=znver3 - cflags: "-flto -DMLK_FORCE_X86_64" - ldflags: "-flto" - perf: PMU - - name: Intel Xeon 3rd gen (c6i) - ec2_instance_type: c6i.large - ec2_ami: ubuntu-latest (x86_64) - archflags: -mavx2 -mbmi2 -mpopcnt -march=icelake-server - cflags: "-flto -DMLK_FORCE_X86_64" - ldflags: "-flto" - perf: PMU - uses: ./.github/workflows/bench_ec2_reusable.yml - if: github.repository_owner == 'pq-code-package' && (github.event.label.name == 'benchmark' || github.ref == 'refs/heads/main') - with: - ec2_instance_type: ${{ matrix.target.ec2_instance_type }} - ec2_ami: ${{ matrix.target.ec2_ami }} - archflags: ${{ matrix.target.archflags }} - cflags: ${{ matrix.target.cflags }} - ldflags: ${{ matrix.target.ldflags }} - opt: "all" - store_results: ${{ github.repository_owner == 'pq-code-package' && github.ref == 'refs/heads/main' }} # Only store optimized results - name: ${{ matrix.target.name }} - perf: ${{ matrix.target.perf }} - secrets: inherit + # ec2_all: + # name: ${{ matrix.target.name }} + # permissions: + # contents: write + # pull-requests: write + # id-token: write + # strategy: + # fail-fast: false + # matrix: + # target: + # - name: Graviton2 + # ec2_instance_type: t4g.small + # ec2_ami: ubuntu-latest (aarch64) + # archflags: -mcpu=cortex-a76 -march=armv8.2-a + # cflags: "-flto -DMLK_FORCE_AARCH64" + # ldflags: "-flto" + # perf: PERF + # - name: Graviton3 + # ec2_instance_type: c7g.medium + # ec2_ami: ubuntu-latest (aarch64) + # archflags: -march=armv8.4-a+sha3 + # cflags: "-flto -DMLK_FORCE_AARCH64" + # ldflags: "-flto" + # perf: PERF + # - name: Graviton4 + # ec2_instance_type: c8g.medium + # ec2_ami: ubuntu-latest (aarch64) + # archflags: -march=armv9-a+sha3 + # cflags: "-flto -DMLK_FORCE_AARCH64" + # ldflags: "-flto" + # perf: PERF + # - name: AMD EPYC 4th gen (c7a) + # ec2_instance_type: c7a.medium + # ec2_ami: ubuntu-latest (x86_64) + # archflags: -mavx2 -mbmi2 -mpopcnt -march=znver4 + # cflags: "-flto -DMLK_FORCE_X86_64" + # ldflags: "-flto" + # perf: PMU + # - name: Intel Xeon 4th gen (c7i) + # ec2_instance_type: c7i.metal-24xl + # ec2_ami: ubuntu-latest (x86_64) + # archflags: -mavx2 -mbmi2 -mpopcnt -march=sapphirerapids + # cflags: "-flto -DMLK_FORCE_X86_64" + # ldflags: "-flto" + # perf: PMU + # - name: AMD EPYC 3rd gen (c6a) + # ec2_instance_type: c6a.large + # ec2_ami: ubuntu-latest (x86_64) + # archflags: -mavx2 -mbmi2 -mpopcnt -march=znver3 + # cflags: "-flto -DMLK_FORCE_X86_64" + # ldflags: "-flto" + # perf: PMU + # - name: Intel Xeon 3rd gen (c6i) + # ec2_instance_type: c6i.large + # ec2_ami: ubuntu-latest (x86_64) + # archflags: -mavx2 -mbmi2 -mpopcnt -march=icelake-server + # cflags: "-flto -DMLK_FORCE_X86_64" + # ldflags: "-flto" + # perf: PMU + # uses: ./.github/workflows/bench_ec2_reusable.yml + # if: github.repository_owner == 'pq-code-package' && (github.event.label.name == 'benchmark' || github.ref == 'refs/heads/main') + # with: + # ec2_instance_type: ${{ matrix.target.ec2_instance_type }} + # ec2_ami: ${{ matrix.target.ec2_ami }} + # archflags: ${{ matrix.target.archflags }} + # cflags: ${{ matrix.target.cflags }} + # ldflags: ${{ matrix.target.ldflags }} + # opt: "all" + # store_results: ${{ github.repository_owner == 'pq-code-package' && github.ref == 'refs/heads/main' }} # Only store optimized results + # name: ${{ matrix.target.name }} + # perf: ${{ matrix.target.perf }} + # secrets: inherit diff --git a/flake.nix b/flake.nix index b7e3fa07af..531a18214e 100644 --- a/flake.nix +++ b/flake.nix @@ -119,7 +119,7 @@ packages.nucleo-n657x0-q = util.nucleo-n657x0-q; devShells.nucleo-n657x0-q = util.mkShell { packages = builtins.attrValues ({ - inherit (config.packages) nucleo-n657x0-q st-openocd; + inherit (config.packages) linters nucleo-n657x0-q st-openocd; inherit (pkgs) gcc-arm-embedded coreutils git libffi pkg-config; }); }; diff --git a/test/hal/hal.c b/test/hal/hal.c index c3cd2067c6..69a4a3af72 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -69,7 +69,7 @@ void disable_cyclecounter(void) { DWT->CTRL &= ~DWT_CTRL_CYCCNTENA_Msk; } uint64_t get_cyclecounter(void) { return DWT->CYCCNT; } -#else /* !(__ARM_ARCH_8M_MAIN__ || __ARM_ARCH_8_1M_MAIN__) */ +#else /* __ARM_ARCH_8M_MAIN__ || __ARM_ARCH_8_1M_MAIN__ */ #error CYCCNT_CYCLES option only supported on Arm M-profile #endif /* !(__ARM_ARCH_8M_MAIN__ || __ARM_ARCH_8_1M_MAIN__) */ @@ -403,10 +403,10 @@ uint64_t get_cyclecounter(void) return g_counters[2]; } -#else /* !PMU_CYCLES && !PERF_CYCLES && MAC_CYCLES */ +#else /* !CYCCNT_CYCLES && !PMU_CYCLES && !PERF_CYCLES && MAC_CYCLES */ void enable_cyclecounter(void) { return; } void disable_cyclecounter(void) { return; } uint64_t get_cyclecounter(void) { return (0); } -#endif /* !PMU_CYCLES && !PERF_CYCLES && !MAC_CYCLES */ +#endif /* !CYCCNT_CYCLES && !PMU_CYCLES && !PERF_CYCLES && !MAC_CYCLES */ From e56866764ff4fff42157437697b4261182b3a2c2 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Thu, 14 May 2026 15:22:39 +0100 Subject: [PATCH 19/20] Fix pmu incompatibility Signed-off-by: Brendan Moran --- test/hal/hal.c | 3 ++- test/hal/pmu_armv8.h | 12 ------------ 2 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 test/hal/pmu_armv8.h diff --git a/test/hal/hal.c b/test/hal/hal.c index 69a4a3af72..7dc714ee08 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -147,11 +147,12 @@ uint64_t get_cyclecounter(void) { return DWT->CYCCNT; } /* Cortex-M55: Use dedicated PMU */ #if defined(STM32N657xx) #include +#include #else #include #include +#include #endif -#include "pmu_armv8.h" void enable_cyclecounter(void) { diff --git a/test/hal/pmu_armv8.h b/test/hal/pmu_armv8.h deleted file mode 100644 index 65cbd8839e..0000000000 --- a/test/hal/pmu_armv8.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) The mlkem-native project authors - * Copyright (c) Arm Ltd. - * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT - */ - -/* Compatibility wrapper: CMSIS uses armv8m_pmu.h under m-profile */ -#if defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_8_1M_MAIN__) -#include -#else -#error pmu_armv8.h included on non Armv8-M build -#endif From a276ffa0834a6e63c0dd6605efc300159a40aea1 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Fri, 29 May 2026 10:48:45 +0100 Subject: [PATCH 20/20] Correct linting error Signed-off-by: Brendan Moran --- test/hal/hal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hal/hal.c b/test/hal/hal.c index 7dc714ee08..c05cfd1d50 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -146,12 +146,12 @@ uint64_t get_cyclecounter(void) { return DWT->CYCCNT; } #elif defined(ARMCM55) /* Cortex-M55: Use dedicated PMU */ #if defined(STM32N657xx) -#include #include +#include #else #include -#include #include +#include #endif void enable_cyclecounter(void)