From 001b5842323d1a1106120cf4cad2fbbe3fa70c32 Mon Sep 17 00:00:00 2001 From: "Matthias J. Kannwischer" Date: Wed, 8 Jul 2026 13:21:35 +0800 Subject: [PATCH 1/5] Add NUCLEO-N657X0-Q Zephyr hardware platform - Port of pq-code-package/mlkem-native#1547 Signed-off-by: Matthias J. Kannwischer --- .github/actionlint.yaml | 1 + .github/workflows/bench.yml | 28 + .github/workflows/zephyr.yml | 4 +- flake.nix | 3 + nix/openocd/default.nix | 22 + nix/util.nix | 1 + nix/zephyr/default.nix | 16 +- scripts/tests | 5 +- test/hal/hal.c | 2 +- test/mk/components.mk | 3 + test/zephyr/README.md | 22 +- test/zephyr/app/CMakeLists.txt | 38 +- test/zephyr/app/nucleo_n657x0_q.conf | 25 + test/zephyr/app/nucleo_n657x0_q.overlay | 17 + test/zephyr/app/shim_nucleo_n657x0_q.c | 241 ++++++++ test/zephyr/nucleo_n657x0_q/README.md | 115 ++++ test/zephyr/nucleo_n657x0_q/exec_wrapper.py | 526 ++++++++++++++++++ .../nucleo_host/flexmem_configure.py | 133 +++++ .../nucleo_n657x0_q/nucleo_host/gdb_script.py | 121 ++++ .../nucleo_host/openocd_tools.py | 215 +++++++ .../nucleo_n657x0_q/nucleo_host/results.py | 107 ++++ .../nucleo_n657x0_q/nucleo_host/symbols.py | 79 +++ test/zephyr/platform.mk | 55 +- 23 files changed, 1754 insertions(+), 25 deletions(-) create mode 100644 nix/openocd/default.nix create mode 100644 test/zephyr/app/nucleo_n657x0_q.conf create mode 100644 test/zephyr/app/nucleo_n657x0_q.overlay create mode 100644 test/zephyr/app/shim_nucleo_n657x0_q.c create mode 100644 test/zephyr/nucleo_n657x0_q/README.md create mode 100755 test/zephyr/nucleo_n657x0_q/exec_wrapper.py create mode 100755 test/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py create mode 100644 test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py create mode 100644 test/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py create mode 100644 test/zephyr/nucleo_n657x0_q/nucleo_host/results.py create mode 100644 test/zephyr/nucleo_n657x0_q/nucleo_host/symbols.py diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 0bab547865..08d998b657 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -12,3 +12,4 @@ self-hosted-runner: - pqcp-x64 # RISE RISC-V runner - ubuntu-24.04-riscv + - self-hosted-nucleo-n657x0 diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 3ba9d960f7..06da220a87 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -38,6 +38,8 @@ jobs: bench_extra_args: "" nix_shell: bench only_no_opt: false + extra_makefile: "" + zephyr_target: "" - system: rpi5 name: Arm Cortex-A76 (Raspberry Pi 5) benchmarks bench_pmu: PERF @@ -48,6 +50,8 @@ jobs: nix_shell: bench only_no_opt: false cross_prefix: "" + extra_makefile: "" + zephyr_target: "" - system: a55 name: Arm Cortex-A55 (Snapdragon 888) benchmarks bench_pmu: PERF @@ -57,6 +61,8 @@ jobs: bench_extra_args: -w exec-on-a55 nix_shell: bench only_no_opt: false + extra_makefile: "" + zephyr_target: "" - system: bpi name: SpacemiT K1 8 (Banana Pi F3) benchmarks bench_pmu: PERF @@ -67,6 +73,8 @@ jobs: cross_prefix: riscv64-unknown-linux-gnu- nix_shell: cross-riscv64 only_no_opt: true + extra_makefile: "" + zephyr_target: "" - system: m1-mac-mini name: Mac Mini (M1, 2020) benchmarks bench_pmu: MAC @@ -76,12 +84,29 @@ jobs: bench_extra_args: "-r" nix_shell: bench only_no_opt: false + extra_makefile: "" + zephyr_target: "" + - system: nucleo-n657x0 + name: Arm Cortex-M55 (NUCLEO-N657X0-Q) benchmarks + bench_pmu: NO + archflags: "" + cflags: "" + ldflags: "" + bench_extra_args: "" + nix_shell: zephyr + only_no_opt: false + cross_prefix: "" + extra_makefile: test/zephyr/platform.mk + zephyr_target: nucleo-n657x0-q if: github.repository_owner == 'pq-code-package' && !github.event.pull_request.head.repo.fork && (github.event.label.name == 'benchmark' || github.ref == 'refs/heads/main') runs-on: self-hosted-${{ matrix.target.system }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: ./.github/actions/bench if: ${{ !matrix.target.only_no_opt }} + env: + EXTRA_MAKEFILE: ${{ matrix.target.extra_makefile }} + ZEPHYR_TARGET: ${{ matrix.target.zephyr_target }} with: name: ${{ matrix.target.name }} (opt) cflags: ${{ matrix.target.cflags }} @@ -95,6 +120,9 @@ jobs: cross_prefix: ${{ matrix.target.cross_prefix }} opt: true - uses: ./.github/actions/bench + env: + EXTRA_MAKEFILE: ${{ matrix.target.extra_makefile }} + ZEPHYR_TARGET: ${{ matrix.target.zephyr_target }} with: name: ${{ matrix.target.name }} (no-opt) cflags: ${{ matrix.target.cflags }} diff --git a/.github/workflows/zephyr.yml b/.github/workflows/zephyr.yml index 3b01d3f69b..6341734653 100644 --- a/.github/workflows/zephyr.yml +++ b/.github/workflows/zephyr.yml @@ -53,5 +53,5 @@ jobs: ZEPHYR_TARGET: ${{ matrix.target.board }} run: | opt=${{ matrix.target.opt == 'all' && 'opt' || 'no_opt' }} - nix develop .#zephyr --command ./scripts/tests bench --no-auto -c PMU --opt=$opt - nix develop .#zephyr --command ./scripts/tests bench --no-auto --components -c PMU --opt=$opt + nix develop .#zephyr --command ./scripts/tests bench --no-auto -c NO --opt=$opt + nix develop .#zephyr --command ./scripts/tests bench --no-auto --components -c NO --opt=$opt diff --git a/flake.nix b/flake.nix index e68ec05b3f..5fe88aae34 100644 --- a/flake.nix +++ b/flake.nix @@ -169,9 +169,12 @@ # Zephyr build environment (board chosen at make time via EXTRA_MAKEFILE) packages.zephyr = util.zephyr; + # OpenOCD with STM32N6 support, for the NUCLEO-N657X0-Q hardware target. + packages.openocd = util.openocd; devShells.zephyr = util.mkShell { packages = builtins.attrValues { + inherit (config.packages) openocd; inherit (util) zephyr; inherit (pkgs) gcc-arm-embedded qemu cmake ninja dtc gperf coreutils git; } ++ [ util.zephyrPythonEnv ]; diff --git a/nix/openocd/default.nix b/nix/openocd/default.nix new file mode 100644 index 0000000000..3c68ebe73a --- /dev/null +++ b/nix/openocd/default.nix @@ -0,0 +1,22 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +{ fetchFromGitHub +, openocd +, autoreconfHook +}: + +openocd.overrideAttrs (old: rec { + pname = "openocd"; + version = "unstable-2026-05-01"; + nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ autoreconfHook ]; + + src = fetchFromGitHub { + owner = "openocd-org"; + repo = "openocd"; + rev = "4e9b167e1ae5ccb437eb0538440988b3f0ec53cb"; + fetchSubmodules = true; + hash = "sha256-8aYl7JzulPxH6vgSeTKTMIZVH6d55JJlXTBkfgAPTbU="; + }; +}) diff --git a/nix/util.nix b/nix/util.nix index 8465c445cb..aced6ad76c 100644 --- a/nix/util.nix +++ b/nix/util.nix @@ -109,6 +109,7 @@ rec { hol_server = pkgs.callPackage ./hol_light/hol_server.nix { inherit hol_light'; }; s2n_bignum = pkgs.callPackage ./s2n_bignum { }; slothy = pkgs.callPackage ./slothy { }; + openocd = pkgs.callPackage ./openocd { }; zephyr = pkgs.callPackage ./zephyr { }; zephyrPythonEnv = pkgs.python3.withPackages (ps: with ps; [ pyelftools diff --git a/nix/zephyr/default.nix b/nix/zephyr/default.nix index e80b3395f7..94ebbd22a8 100644 --- a/nix/zephyr/default.nix +++ b/nix/zephyr/default.nix @@ -10,8 +10,7 @@ # Board-agnostic Zephyr build environment: a pinned Zephyr tree plus the # modules needed by the boards we target, exposed via a setup hook so a plain -# `cmake` build works with no west workspace. CMSIS-6 covers the Cortex-M -# boards; add further modules here as more boards are wired up. +# `cmake` build works with no west workspace. let zephyr = fetchFromGitHub { owner = "zephyrproject-rtos"; @@ -27,6 +26,14 @@ let rev = "30a859f44ef8ab4dc8f84b03ed586fd16ccf9d74"; hash = "sha256-nTehISN0pu9gnOZMpGaBQ3DFmNxAqAZPGpvbKfEM35o="; }; + + # Revision pinned by the Zephyr v4.4.1 manifest (west.yml). + hal_stm32 = fetchFromGitHub { + owner = "zephyrproject-rtos"; + repo = "hal_stm32"; + rev = "fc11896dd39cfca37bf9b4aeaaa2df8861b81875"; + hash = "sha256-AtNq2yTZsTFMTlWn/Ns0wuEiN4Wv/OTV2vWPRu0SnOE="; + }; in stdenvNoCC.mkDerivation { pname = "mldsa-native-zephyr"; @@ -38,11 +45,14 @@ stdenvNoCC.mkDerivation { mkdir -p $out ln -s ${zephyr} $out/zephyr ln -s ${cmsis_6} $out/cmsis_6 + ln -s ${hal_stm32} $out/hal_stm32 ''; setupHook = writeText "setup-hook.sh" '' export ZEPHYR_BASE="$1/zephyr" - export ZEPHYR_MODULES="$1/cmsis_6" + export ZEPHYR_CMSIS_6_MODULE="$1/cmsis_6" + export ZEPHYR_HAL_STM32_MODULE="$1/hal_stm32" + export ZEPHYR_MODULES="$1/cmsis_6;$1/hal_stm32" export ZEPHYR_TOOLCHAIN_VARIANT=gnuarmemb export GNUARMEMB_TOOLCHAIN_PATH=${gcc-arm-embedded} ''; diff --git a/scripts/tests b/scripts/tests index dbb063e0fd..cb56ea3b55 100755 --- a/scripts/tests +++ b/scripts/tests @@ -782,8 +782,11 @@ class Tests: test_type, self.do_opt(), suppress_output=False ) - if resultss is None: + # Nothing below should write benchmark output unless the run produced + # results and all compile/run steps succeeded. + if resultss is None or len(self.failed) > 0: self.check_fail() + return # 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(): diff --git a/test/hal/hal.c b/test/hal/hal.c index 01d00e117a..87fd7e25db 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -125,8 +125,8 @@ uint64_t get_cyclecounter(void) { return DWT->CYCCNT; } #elif defined(ARMCM55) /* Cortex-M55: Use dedicated PMU */ #include +#include #include -#include "pmu_armv8.h" void enable_cyclecounter(void) { diff --git a/test/mk/components.mk b/test/mk/components.mk index 3a814a818e..52ba35b2e0 100644 --- a/test/mk/components.mk +++ b/test/mk/components.mk @@ -154,6 +154,9 @@ $(call MAKE_OBJS, $(MLDSA87_DIR), $(EXTRA_SOURCES)): CFLAGS += $(EXTRA_SOURCES_C endif else $(ALL_BINS): $$(TEST_SRCS) $(LIB_SRCS) +# Extra per-binary prerequisites a custom-build platform needs (e.g. Zephyr's +# app inputs and active-target marker, set in test/zephyr/platform.mk). +$(ALL_BINS): $(CUSTOM_BUILD_DEPS) endif # ABI checker diff --git a/test/zephyr/README.md b/test/zephyr/README.md index 480bbada91..f47204566f 100644 --- a/test/zephyr/README.md +++ b/test/zephyr/README.md @@ -4,8 +4,9 @@ This is a test platform that builds the mldsa-native test applications as [Zephyr](https://www.zephyrproject.org/) applications, so they can run on -QEMU-emulated Arm MPS boards. It covers Cortex-M3/M4/M7/M33/M55 through a -single platform, without the need for per-board hardware abstraction layers. +QEMU-emulated Arm MPS boards and the NUCLEO-N657X0-Q hardware board. It covers +Cortex-M3/M4/M7/M33/M55 through a single platform, without the need for +per-board hardware abstraction layers. ## Usage @@ -23,16 +24,17 @@ nix develop .#zephyr --command ./scripts/tests func --opt=opt Currently supported targets: -| `ZEPHYR_TARGET` | Zephyr board | QEMU machine | Core | -| --------------- | --------------------- | ------------ | ---------- | -| `mps2-an385` | `mps2/an385` | `mps2-an385` | Cortex-M3 | -| `mps2-an386` | `mps2/an386` | `mps2-an386` | Cortex-M4 | -| `mps2-an500` | `mps2/an500` | `mps2-an500` | Cortex-M7 | -| `mps2-an521` | `mps2/an521/cpu0` | `mps2-an521` | Cortex-M33 | -| `mps3-an547` | `mps3/corstone300/an547` | `mps3-an547` | Cortex-M55 | +| `ZEPHYR_TARGET` | Zephyr board | Runner | Core | +| ------------------- | ------------------------ | -------------- | ---------- | +| `mps2-an385` | `mps2/an385` | `mps2-an385` | Cortex-M3 | +| `mps2-an386` | `mps2/an386` | `mps2-an386` | Cortex-M4 | +| `mps2-an500` | `mps2/an500` | `mps2-an500` | Cortex-M7 | +| `mps2-an521` | `mps2/an521/cpu0` | `mps2-an521` | Cortex-M33 | +| `mps3-an547` | `mps3/corstone300/an547` | `mps3-an547` | Cortex-M55 | +| `nucleo-n657x0-q` | `nucleo_n657x0_q` | OpenOCD + GDB | Cortex-M55 | The Armv8.1-M MVE FIPS202 backend is an `OPT=1` feature and is built for -`mps3-an547` only (the only listed core with MVE). +`mps3-an547` and `nucleo-n657x0-q`. ## How it works diff --git a/test/zephyr/app/CMakeLists.txt b/test/zephyr/app/CMakeLists.txt index c426bc2c9c..914ee0e7c3 100644 --- a/test/zephyr/app/CMakeLists.txt +++ b/test/zephyr/app/CMakeLists.txt @@ -21,7 +21,6 @@ list(TRANSFORM _test_srcs PREPEND ${R}/) target_sources(app PRIVATE ${R}/mldsa/mldsa_native.c ${_test_srcs} - ${CMAKE_CURRENT_SOURCE_DIR}/shim.c ) target_include_directories(app PRIVATE @@ -30,6 +29,31 @@ target_include_directories(app PRIVATE ${R}/test/hal ) +# The Zephyr shim owns main() (see below). On QEMU targets shim.c stops the +# emulator with the test's exit code; the NUCLEO-N657X0-Q hardware target needs +# its own shim plus libc wrappers that route stdio over ITM/SWO. +if(ZEPHYR_NUCLEO_N657X0_Q) + target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/shim_nucleo_n657x0_q.c) + target_compile_definitions(app PRIVATE + printf=__wrap_printf + fprintf=__wrap_fprintf + puts=__wrap_puts + putchar=__wrap_putchar + fflush=__wrap_fflush + exit=__wrap_exit + ) + target_link_options(app PRIVATE + -Wl,--wrap=printf + -Wl,--wrap=fprintf + -Wl,--wrap=puts + -Wl,--wrap=putchar + -Wl,--wrap=fflush + -Wl,--wrap=exit + ) +else() + target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/shim.c) +endif() + # The test binary's CFLAGS, forwarded from the build (test/zephyr/platform.mk # assembles and escapes them) and applied to the mldsa amalgamation and the test # sources alike, so both honour the project's own warning/opt/std policy and @@ -47,6 +71,9 @@ endif() set_source_files_properties(${R}/test/hal/hal.c PROPERTIES COMPILE_OPTIONS "-Wno-error") +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/shim_nucleo_n657x0_q.c + PROPERTIES COMPILE_OPTIONS "-Wno-error") + # Optional native FIPS202 backend (e.g. Armv8.1-M MVE on Cortex-M55). The # monolithic mldsa_native.c already includes the backend C sources; its # assembly counterpart comes from mldsa_native_asm.S. @@ -57,9 +84,10 @@ if(ZEPHYR_FIPS202_BACKEND) "MLD_CONFIG_FIPS202_BACKEND_FILE=\"${ZEPHYR_FIPS202_BACKEND}\"") endif() -# Each test brings its own int main(void); rename it so the Zephyr shim -# (shim.c) owns main() and can stop QEMU with the test's exit code. Only the -# test entrypoint in ZEPHYR_TEST_SRCS defines main(); the support sources -# (notrandombytes, hal) don't, so applying the define to all is harmless. +# Each test brings its own int main(void); rename it so the selected Zephyr +# shim owns main() and can return the test's exit code through the target's +# runner. Only the test entrypoint in ZEPHYR_TEST_SRCS defines main(); the +# support sources (notrandombytes, hal) don't, so applying the define to all is +# harmless. set_source_files_properties(${_test_srcs} PROPERTIES COMPILE_DEFINITIONS "main=mld_test_main") diff --git a/test/zephyr/app/nucleo_n657x0_q.conf b/test/zephyr/app/nucleo_n657x0_q.conf new file mode 100644 index 0000000000..a517a7a0d4 --- /dev/null +++ b/test/zephyr/app/nucleo_n657x0_q.conf @@ -0,0 +1,25 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +# The board default exposes a 2 MiB SRAM window at 0x34000000; its first +# 400 KiB is FLEXRAM. The NUCLEO runner repurposes FLEXMEM to expand the TCMs +# to 256 KiB ITCM + 256 KiB DTCM, so keep the Zephyr image above that low +# window and leave fixed host buffers after the linked image. +CONFIG_SRAM_BASE_ADDRESS=0x34080000 +CONFIG_SRAM_SIZE=192 + +# The component benchmark app keeps five 8 KiB scratch buffers on the main +# stack; measured max static stack depth is about 49.5 KiB, so 32 KiB is not +# enough for that benchmark. +CONFIG_MAIN_STACK_SIZE=65536 + +# Test stdout is wrapped by shim_nucleo_n657x0_q.c and emitted on ITM/SWO. +CONFIG_STDOUT_CONSOLE=n +CONFIG_UART_CONSOLE=n + +# The host runner restores a dynamic command-line string into the fixed +# bootargs handoff window before Zephyr calls get_bootargs(). +CONFIG_BOOTARGS=y +CONFIG_DYNAMIC_BOOTARGS=y +CONFIG_BOOTARGS_ARGS_BUFFER_SIZE=16384 diff --git a/test/zephyr/app/nucleo_n657x0_q.overlay b/test/zephyr/app/nucleo_n657x0_q.overlay new file mode 100644 index 0000000000..2e87f5e984 --- /dev/null +++ b/test/zephyr/app/nucleo_n657x0_q.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) The mldsa-native project authors + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +/ { + mld_axisram: memory@34080000 { + compatible = "zephyr,memory-region", "mmio-sram"; + reg = <0x34080000 0x00030000>; + zephyr,memory-region = "NUCLEO_AXISRAM"; + }; + + chosen { + zephyr,sram = &mld_axisram; + }; +}; diff --git a/test/zephyr/app/shim_nucleo_n657x0_q.c b/test/zephyr/app/shim_nucleo_n657x0_q.c new file mode 100644 index 0000000000..eebb01685a --- /dev/null +++ b/test/zephyr/app/shim_nucleo_n657x0_q.c @@ -0,0 +1,241 @@ +/* + * Copyright (c) The mldsa-native project authors + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +/* + * Zephyr entrypoint shim for RAM-loaded NUCLEO-N657X0-Q tests. + * + * The OpenOCD/GDB wrapper breaks at get_bootargs() after Zephyr has cleared + * BSS, restores a Zephyr dynamic bootargs command line into + * mld_bootargs_block, then continues. Zephyr parses the command line and calls + * main(argc, argv). Target stdout/stderr goes through ITM stimulus port 0 and + * is captured by the host over SWO. The test return code is passed to + * nucleo_test_done(), where the host wrapper reads r0. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern int mld_test_main(int argc, char **argv); + +void nucleo_test_breakpoint(int rc); +int __wrap_printf(const char *format, ...); +int __wrap_fprintf(FILE *stream, const char *format, ...); +int __wrap_puts(const char *s); +int __wrap_putchar(int c); +int __wrap_fflush(FILE *stream); +__attribute__((noreturn)) void nucleo_test_done(int rc); +__attribute__((noreturn)) void __wrap_exit(int status); +const char *get_bootargs(void); + +#define BOOTARGS_BLOCK_SIZE (64U * 1024U) +#define NUCLEO_ITM_LAR (*(volatile uint32_t *)0xE0000FB0UL) + +__asm__( + ".global mld_bootargs_block\n" + ".set mld_bootargs_block, 0x340b0000\n"); + +extern char mld_bootargs_block[BOOTARGS_BLOCK_SIZE]; + +static char nucleo_stdout_buf[8192]; +static size_t nucleo_stdout_len; +static bool nucleo_swo_enabled; + +static void nucleo_swo_pin_setup(void) +{ + LL_AHB4_GRP1_EnableClock(LL_AHB4_GRP1_PERIPH_GPIOB); + LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_5, LL_GPIO_MODE_ALTERNATE); + LL_GPIO_SetPinSpeed(GPIOB, LL_GPIO_PIN_5, LL_GPIO_SPEED_FREQ_VERY_HIGH); + LL_GPIO_SetPinPull(GPIOB, LL_GPIO_PIN_5, LL_GPIO_PULL_NO); + LL_GPIO_SetAFPin_0_7(GPIOB, LL_GPIO_PIN_5, LL_GPIO_AF_0); +} + +static void nucleo_swo_enable(void) +{ + if (nucleo_swo_enabled) + { + return; + } + + nucleo_swo_pin_setup(); + LL_BUS_EnableClock(LL_APB3); + LL_DBGMCU_EnableDBGClock(); + LL_DBGMCU_EnableTPIUExportClock(); + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + + NUCLEO_ITM_LAR = 0xC5ACCE55UL; + ITM->TER = 0UL; + ITM->TCR &= ~ITM_TCR_ITMENA_Msk; + while ((ITM->TCR & ITM_TCR_BUSY_Msk) != 0UL) + { + } + ITM->TPR = 0UL; + ITM->TCR = + ITM_TCR_ITMENA_Msk | ITM_TCR_DWTENA_Msk | (1UL << ITM_TCR_TRACEBUSID_Pos); + ITM->TER = 1UL; + nucleo_swo_enabled = true; +} + +static void nucleo_swo_write_byte(char ch) +{ + nucleo_swo_enable(); + + if (((ITM->TCR & ITM_TCR_ITMENA_Msk) == 0UL) || ((ITM->TER & 1UL) == 0UL)) + { + return; + } + + while (ITM->PORT[0U].u32 == 0UL) + { + } + ITM->PORT[0U].u8 = (uint8_t)ch; +} + +static void nucleo_swo_write_raw(const char *src, size_t len) +{ + for (size_t offset = 0; offset < len; offset++) + { + nucleo_swo_write_byte(src[offset]); + } +} + +static void nucleo_swo_wait_idle(void) +{ + for (uint32_t timeout = 10000000UL; timeout > 0U; timeout--) + { + if ((ITM->TCR & ITM_TCR_BUSY_Msk) == 0UL) + { + break; + } + } + __DSB(); +} + +__attribute__((noinline, used)) void nucleo_test_breakpoint(int rc) +{ + (void)rc; + __asm__ volatile("bkpt 0" ::: "memory"); +} + +static void nucleo_stdout_flush(void) +{ + if (nucleo_stdout_len == 0U) + { + return; + } + + nucleo_swo_write_raw(nucleo_stdout_buf, nucleo_stdout_len); + nucleo_stdout_len = 0U; +} + +static void nucleo_stdout_write(const char *src, size_t len) +{ + for (size_t i = 0; i < len; i++) + { + nucleo_stdout_buf[nucleo_stdout_len++] = src[i]; + if (src[i] == '\n' || nucleo_stdout_len == sizeof(nucleo_stdout_buf)) + { + nucleo_stdout_flush(); + } + } +} + +static int nucleo_vprintf(const char *format, va_list ap) +{ + char buf[8192]; + int rc = vsnprintf(buf, sizeof(buf), format, ap); + + if (rc <= 0) + { + return rc; + } + + nucleo_stdout_write( + buf, (size_t)((rc < (int)sizeof(buf)) ? rc : (int)sizeof(buf) - 1)); + return rc; +} + +int __wrap_printf(const char *format, ...) +{ + va_list ap; + int rc; + + va_start(ap, format); + rc = nucleo_vprintf(format, ap); + va_end(ap); + return rc; +} + +int __wrap_fprintf(FILE *stream, const char *format, ...) +{ + va_list ap; + int rc; + + (void)stream; + va_start(ap, format); + rc = nucleo_vprintf(format, ap); + va_end(ap); + return rc; +} + +int __wrap_puts(const char *s) +{ + size_t len = strlen(s); + + nucleo_stdout_write(s, len); + nucleo_stdout_write("\n", 1); + return (int)len + 1; +} + +int __wrap_putchar(int c) +{ + char ch = (char)c; + + nucleo_stdout_write(&ch, 1); + return c; +} + +int __wrap_fflush(FILE *stream) +{ + (void)stream; + nucleo_stdout_flush(); + return 0; +} + +__attribute__((noreturn, noinline, used)) void nucleo_test_done(int rc) +{ + nucleo_stdout_flush(); + nucleo_swo_wait_idle(); + nucleo_test_breakpoint(rc); + for (;;) + { + __WFI(); + } +} + +__attribute__((noreturn)) void __wrap_exit(int status) +{ + nucleo_test_done(status); +} + +__attribute__((noinline, used)) const char *get_bootargs(void) +{ + return mld_bootargs_block; +} + +int main(int argc, char **argv) +{ + int rc = mld_test_main(argc, argv); + + nucleo_test_done(rc); +} diff --git a/test/zephyr/nucleo_n657x0_q/README.md b/test/zephyr/nucleo_n657x0_q/README.md new file mode 100644 index 0000000000..3c79861b00 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/README.md @@ -0,0 +1,115 @@ + + +# NUCLEO-N657X0-Q Zephyr Hardware Helpers + +This directory contains the NUCLEO-N657X0-Q Zephyr hardware entry point and +supporting host/debug helpers. The board is not flashed: OpenOCD first expands +FLEXMEM with direct register writes, then GDB loads the Zephyr ELF into RAM. + +Only two target-specific behaviors remain here: + +- FLEXMEM expansion for the STM32N657X0 ITCM/DTCM layout. +- GDB bootargs insertion after Zephyr startup reaches `get_bootargs()`. + +Test output uses ITM stimulus port 0 and OpenOCD SWO capture. The host wrapper +decodes that SWO stream and keeps OpenOCD/GDB control chatter out of stdout +unless `--verbose` is passed. + +## Files + +- `exec_wrapper.py`: configures FLEXMEM, starts OpenOCD with SWO capture + enabled, loads a Zephyr ELF with GDB, restores the dynamic bootargs string, + decodes ITM port 0 output, and reads the target return code at + `nucleo_test_done`. +- `nucleo_host/`: Python helpers for FLEXMEM configuration, OpenOCD command + generation, GDB script generation, symbol lookup, fault diagnostics. + +The Zephyr application shim lives in `test/zephyr/app/shim_nucleo_n657x0_q.c`. + +## Run + +Use the Zephyr platform makefile and select the NUCLEO target: + +```sh +nix develop .#zephyr +make run_func_44 EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q +``` + +The execution wrapper configures FLEXMEM before each ELF run. The working +Zephyr hardware run targets are: + +```text +run_kat run_func run_unit run_alloc run_rng_fail run_acvp run_wycheproof +``` + +Known exceptions: + +- `run_stack` is a host-side stack analysis target. In this platform mode it + builds a Zephyr Arm ELF, then the stack script attempts to execute that ELF + on the host. +- `run_abicheck` is not currently built as a NUCLEO Zephyr application when + `OPT=1`; the NUCLEO wrapper expects the Zephyr shim symbols used for bootargs + injection and completion reporting. + +Benchmark targets remain explicit and require the usual `CYCLES` setting: + +```sh +make run_bench_44 CYCLES=NO EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q +make run_bench_components_44 CYCLES=NO EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q +``` + +Useful environment variables: + +```sh +export OPENOCD_SPEED=8000 +export OPENOCD_SERIAL= +export GDB_PORT=3333 +export SWO_TRACECLK=100000000 +export SWO_PIN_FREQ=1000000 +``` + +`OPENOCD`, `OPENOCD_INTERFACE`, `OPENOCD_TARGET`, `OPENOCD_TRANSPORT`, `GDB`, +`NM`, and `READELF` can override the default tools and OpenOCD scripts. + +## FLEXMEM Sequence + +Zephyr's default NUCLEO-N657X0-Q RAM region is a 2 MiB SRAM window starting at +`0x34000000`; its first 400 KiB is the STM32N657X0 FLEXRAM allocation. After +reset, the Cortex-M55 TCM layout is 64 KiB ITCM and 128 KiB DTCM. The NUCLEO +runner repurposes FLEXMEM to expand the TCMs to 256 KiB ITCM and 256 KiB DTCM +before each RAM-loaded Zephyr test, so the test image is linked above that low +AXISRAM/FLEXRAM window instead of using the board default: + +1. OpenOCD attaches with `reset_config none`. +2. The script enables the SYSCFG clock by setting bit 0 in `RCC_APB4ENSR2` at + `0x56028a78`. +3. It read-modify-writes `SYSCFG_CM55TCMCR` at `0x56008008` so the low byte is + `0x99`. +4. It sets bit 0 in `SYSCFG_CM55RSTCR` at `0x56008018`. +5. It polls until `(SYSCFG_CM55TCMCR & 0xff) == 0x99`. +6. It runs `reset run` so the expanded layout is applied before the Zephyr ELF + is loaded. + +The board is RAM-loaded, not flashed. Zephyr's default NUCLEO-N657X0-Q RAM +region is a 2 MiB SRAM window starting at `0x34000000`; its first 400 KiB is +the STM32N657X0 FLEXRAM allocation. The hardware benchmark setup is the reason +this target uses a board-specific memory layout: before each run, the hardware +wrapper repurposes FLEXMEM to expand the Cortex-M55 TCMs by setting the low +byte of `SYSCFG_CM55TCMCR` to `0x99`, selecting 256 KiB ITCM and 256 KiB DTCM, +then resets the target so that layout is active. Because this changes the low +AXISRAM/FLEXRAM window that the default Zephyr layout would use, the test image +is instead linked in a 192 KiB AXISRAM window at `0x34080000`; see +`nucleo_n657x0_q/README.md` for the exact register sequence. + +Test stdout is captured over ITM stimulus port 0 through SWO. This avoids using +semihosting for normal output: semihosting is blocking and each host operation +requires debug exception/interrupt handling, which makes it too slow and too +intrusive for repeated hardware test and benchmark runs. SWO is also simpler +than UART in this setup because the OpenOCD SWO endpoint is tied directly to +the selected debug probe, while UART output requires identifying the correct +virtual COM port for that specific probe. diff --git a/test/zephyr/nucleo_n657x0_q/exec_wrapper.py b/test/zephyr/nucleo_n657x0_q/exec_wrapper.py new file mode 100755 index 0000000000..9cf3f880af --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/exec_wrapper.py @@ -0,0 +1,526 @@ +#!/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 + +""" +Run one RAM-resident NUCLEO-N657X0-Q Zephyr test ELF through OpenOCD. + +The wrapper first expands ITCM/DTCM with the direct FLEXMEM OpenOCD script. It +then starts OpenOCD with SWO trace redirected to a local TCP listener, +GDB-loads the Zephyr ELF into RAM, restores a dynamic bootargs string after +Zephyr startup reaches ``get_bootargs``, decodes target stdout from ITM port 0, +and reads the target return code from ``r0`` at the ``nucleo_test_done`` +breakpoint. +""" + +import logging +import os +import re +import select +import socket +import subprocess +import sys +import tempfile +import time + +from nucleo_host.flexmem_configure import run_openocd_config as run_flexmem_config +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 swo_formatter_from_env +from nucleo_host.openocd_tools import swo_pin_freq_from_env +from nucleo_host.openocd_tools import swo_traceclk_from_env +from nucleo_host.openocd_tools import transport_from_env +from nucleo_host.results import fault_info_from_gdb +from nucleo_host.results import gdb_observed_hardfault +from nucleo_host.symbols import default_readelf +from nucleo_host.symbols import resolve_symbol + +VERBOSE = False +LOG = logging.getLogger(__name__) +BOOTARGS_BLOCK_SIZE = 64 * 1024 + + +def _quote_bootarg(arg): + """ + Quote one argument for Zephyr's boot_args.c parser. + + Zephyr groups a token when it starts with a matching single or double quote, + and strips the quote characters. It does not implement shell-style escapes. + """ + if arg == "" or arg[0] in ("'", '"') or any(ch.isspace() for ch in arg): + if "'" not in arg: + return f"'{arg}'" + if '"' not in arg: + return f'"{arg}"' + raise ValueError( + "Zephyr bootargs cannot represent arguments that need quoting " + "and contain both quote characters" + ) + return arg + + +def _pack_bootargs(args, block_size=BOOTARGS_BLOCK_SIZE): + """ + Return a padded UTF-8 dynamic bootargs command line for Zephyr. + + The NUCLEO runner restores this string into the fixed target reservation + before Zephyr calls ``get_bootargs()``. The result is padded to the full + reservation so GDB ``restore`` overwrites stale contents from prior runs. + """ + cmdline = " ".join(_quote_bootarg(arg) for arg in args).encode("utf-8") + b"\x00" + if len(cmdline) > block_size: + raise ValueError( + f"bootargs string is {len(cmdline)} bytes, exceeds {block_size}-byte block" + ) + return cmdline + bytes(block_size - len(cmdline)) + + +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(): + if prefix: + line = f"{prefix}{line}" + LOG.log(level, "%s", line) + + +def err(msg): + """Report an error message regardless of verbose mode.""" + log_output(msg, logging.ERROR) + + +def info(msg): + """Report an informational message only in verbose mode.""" + if VERBOSE: + LOG.debug("%s", msg) + + +def popen(cmd, **kwargs): + """Wrap ``subprocess.Popen`` for test-time monkeypatching.""" + return subprocess.Popen(cmd, **kwargs) + + +def _reserve_localhost_port(): + """Return an available localhost TCP port for OpenOCD to listen on.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def _emit_target_bytes(data: bytes): + """Forward decoded target stdout/stderr bytes to host stdout.""" + if not data: + return + out = getattr(sys.stdout, "buffer", None) + if out is not None: + out.write(data) + out.flush() + else: + sys.stdout.write(data.decode("utf-8", errors="replace")) + sys.stdout.flush() + + +def _decode_swo_itm(state, data: bytes): + """Extract software stimulus port 0 bytes from raw ITM trace packets.""" + out = bytearray() + remaining = state.get("itm_remaining", 0) + emit = state.get("itm_emit", False) + + for byte in data: + if remaining: + if emit: + out.append(byte) + remaining -= 1 + continue + + size_code = byte & 0x03 + if size_code == 0: + continue + + remaining = 4 if size_code == 3 else size_code + port = (byte >> 3) & 0x1F + is_software_packet = (byte & 0x04) == 0 + emit = is_software_packet and port == 0 + + state["itm_remaining"] = remaining + state["itm_emit"] = emit + return bytes(out) + + +def _drain_swo(state): + """Connect to and drain OpenOCD SWO TCP output without blocking.""" + conn = state.get("conn") + if conn is None: + try: + conn = socket.create_connection(("127.0.0.1", state["port"]), timeout=0.01) + conn.setblocking(False) + state["conn"] = conn + except OSError: + return + + conn = state.get("conn") + if conn is None: + return + + while True: + try: + readable, _, _ = select.select([conn], [], [], 0) + if not readable: + return + data = conn.recv(4096) + except OSError: + state["conn"] = None + return + if not data: + try: + conn.close() + except OSError: + pass + state["conn"] = None + return + decoded = _decode_swo_itm(state, data) + state["swo_raw_bytes"] = state.get("swo_raw_bytes", 0) + len(data) + _emit_target_bytes(decoded) + + +def _wait_swo_connected(state, timeout_s=3.0): + """Connect to OpenOCD's SWO TCP listener before target execution.""" + deadline = time.time() + timeout_s + while time.time() < deadline: + _drain_swo(state) + if state.get("conn") is not None: + return True + time.sleep(0.05) + return False + + +def _close_swo_state(state): + """Close SWO connection handles.""" + conn = state.get("conn") + if conn is not None: + try: + conn.close() + except OSError: + pass + + +def _drain_swo_until_idle(state, idle_s=0.5, timeout_s=5.0): + """Drain delayed SWO bytes until the stream is briefly idle.""" + deadline = time.time() + timeout_s + idle_deadline = time.time() + idle_s + last_raw_bytes = state.get("swo_raw_bytes", 0) + + while time.time() < deadline: + _drain_swo(state) + raw_bytes = state.get("swo_raw_bytes", 0) + if raw_bytes != last_raw_bytes: + last_raw_bytes = raw_bytes + idle_deadline = time.time() + idle_s + elif time.time() >= idle_deadline: + break + time.sleep(0.01) + + +def _parse_exit_code(gdb_text: str): + """Return the target exit code printed by the GDB script, if present.""" + match = re.search(r"^NUCLEO_EXIT_CODE=(-?\d+)$", gdb_text, re.MULTILINE) + if match: + return int(match.group(1)) + return None + + +def _run_once(): + """Run the target ELF once and return its wrapper exit code.""" + global VERBOSE + + argv = sys.argv[1:] + if "--verbose" in argv: + VERBOSE = True + argv.remove("--verbose") + if "-v" in argv: + VERBOSE = True + argv.remove("-v") + + configure_logging() + + if len(argv) < 1: + err("Usage: exec_wrapper.py [--verbose] [args...]") + return 2 + + elf = os.path.abspath(argv[0]) + args = argv # Preserve the existing convention: argv[0] is the ELF path. + + 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", default_readelf()) + port = int(os.environ.get("GDB_PORT", "3333")) + gdb_run_timeout = float(os.environ.get("GDB_RUN_TIMEOUT", "180")) + + arg_block_sym = "mld_bootargs_block" + arg_block_addr = None + + def _resolve_symbol_addr(elf_path: str, sym: str): + return resolve_symbol(elf_path, sym, nm=nm, readelf=readelf) + + def _resolve_first_symbol(elf_path: str, symbols): + for sym in symbols: + addr = _resolve_symbol_addr(elf_path, sym) + if addr is not None: + return sym, addr + return symbols[0], None + + for cand in (arg_block_sym, "mldsa_bootargs_block"): + addr = _resolve_symbol_addr(elf, cand) + if addr is not None: + arg_block_sym = cand + arg_block_addr = addr + break + + bootargs_sym, bootargs_addr = _resolve_first_symbol(elf, ["get_bootargs"]) + bootargs_break = bootargs_sym + if bootargs_addr is not None: + bootargs_break = f"*{bootargs_addr}" + + done_sym, done_addr = _resolve_first_symbol( + elf, ["nucleo_test_breakpoint", "nucleo_test_done"] + ) + done_break = done_sym + if done_addr is not None: + done_break = f"*{done_addr}" + + hardfault_sym, hardfault_addr = _resolve_first_symbol( + elf, ["HardFault_Handler", "z_arm_hard_fault"] + ) + hardfault_break = hardfault_sym + if hardfault_addr is not None: + hardfault_break = f"*{hardfault_addr}" + + reset_handler_sym, reset_handler_addr = _resolve_first_symbol( + elf, ["Reset_Handler", "z_arm_reset"] + ) + reset_handler_jump = reset_handler_sym + 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/z_arm_reset in ELF.") + return 2 + + base_addr = None + if arg_block_addr: + try: + base_addr = int(arg_block_addr, 16) + except ValueError: + base_addr = None + + if base_addr is None: + err( + "Failed to resolve base address of bootargs block " + "(mld_bootargs_block/mldsa_bootargs_block)." + ) + err("- Ensure symbols are present in ELF.") + return 2 + + try: + blob = _pack_bootargs(args) + except ValueError as exc: + err(str(exc)) + return 2 + + with tempfile.TemporaryDirectory() as td: + argv_bin = os.path.join(td, "argv.bin") + with open(argv_bin, "wb") as f: + f.write(blob) + + flexmem_timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "5")) + info("[exec_wrapper] configuring FLEXMEM...") + flexmem_rc = run_flexmem_config(flexmem_timeout_s) + if flexmem_rc != 0: + return flexmem_rc + + swo_port = _reserve_localhost_port() + swo_state = {"conn": None, "port": swo_port} + openocd = find_openocd(os.environ.get("OPENOCD", "")) + if openocd is None: + _close_swo_state(swo_state) + err("OpenOCD not found; set OPENOCD or ensure openocd is on PATH") + return 2 + gdbserver_cmd = runtime_gdbserver_cmd( + openocd=openocd, + port=port, + swo_port=swo_port, + swo_traceclk=swo_traceclk_from_env(), + swo_pin_freq=swo_pin_freq_from_env(), + swo_formatter=swo_formatter_from_env(), + speed=speed_khz_from_env(), + serial=serial_from_env(), + transport=transport_from_env(), + ) + + info(f"[exec_wrapper] starting OpenOCD on port {port}...") + info(f"[exec_wrapper] SWO redirect port {swo_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: + time.sleep(0.8) + + if stp.poll() is not None: + out_rem = stp.stdout.read() if stp.stdout else "" + if out_rem: + log_output(out_rem, logging.DEBUG if VERBOSE else logging.ERROR) + return 2 + if not _wait_swo_connected(swo_state): + err("FAIL!") + err("failed to connect to OpenOCD SWO TCP listener") + return 2 + + gdb_lines = build_run_script( + port=port, + bootargs_break=bootargs_break, + reset_handler_jump=reset_handler_jump, + hardfault_break=hardfault_break, + done_break=done_break, + argv_bin=argv_bin, + arg_block_addr=arg_block_addr, + arg_block_sym=arg_block_sym, + ) + + if VERBOSE: + 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: + gs.write(line + "\n") + gdb_script_path = gs.name + + gdb_cmd = [gdb, "--batch", "-x", gdb_script_path, elf] + + info("[exec_wrapper] running gdb batch") + 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 + ) + + while True: + _drain_swo(swo_state) + if stp.stdout is not None: + try: + r, _, _ = select.select([stp.stdout], [], [], 0.1) + if r: + line = stp.stdout.readline() + if line and VERBOSE: + log_output(line, logging.DEBUG) + except Exception: + pass + 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: + log_output(errout, logging.ERROR) + except Exception: + pass + return 124 + + out, errout = gdbp.communicate() + _drain_swo_until_idle(swo_state) + if out and VERBOSE: + log_output(out, logging.DEBUG) + if errout and VERBOSE: + log_output(errout, logging.DEBUG) + + gdb_text = f"{out}\n{errout}" + exit_code = _parse_exit_code(gdb_text) + hardfaulted = gdb_observed_hardfault(gdb_text) + + if exit_code is not None: + return exit_code + + if hardfaulted: + fault_info = fault_info_from_gdb(gdb_text) + err("FAIL!") + err("Target entered HardFault_Handler") + if fault_info: + err(fault_info) + return 1 + + if gdbp.returncode != 0: + err("FAIL!") + err(f"gdb batch failed with code {gdbp.returncode}") + if out: + log_output(out, logging.ERROR) + if errout: + log_output(errout, logging.ERROR) + return gdbp.returncode + + err("FAIL!") + err("target did not hit nucleo_test_done") + return 1 + + finally: + try: + stp.terminate() + stp.wait(timeout=1.5) + except Exception: + try: + stp.kill() + except Exception: + pass + _close_swo_state(swo_state) + try: + if "gdb_script_path" in locals(): + os.unlink(gdb_script_path) + except Exception: + pass + + +def main(): + """Run the wrapper once with the configured debugger transport.""" + return _run_once() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py new file mode 100755 index 0000000000..7bb3afbbc2 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py @@ -0,0 +1,133 @@ +#!/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 + +""" +Configure STM32N6 FLEXMEM before loading RAM-resident test images. + +The helper writes the STM32N6 FLEXMEM control registers over SWD, polls +``SYSCFG->CM55TCMCR`` until the requested TCM split is visible, then resets the +target so the next ELF can be loaded into the expanded ITCM/DTCM layout. +""" + +import logging +import os +import sys +import tempfile + +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 + +DONE = "FLEXMEM configuration complete; reset target and load test binary." + +# Polling the register via SWD proves that the new ITCM/DTCM split latched +# before the next test binary is loaded. +RCC_APB4ENSR2_ADDR = "0x56028a78" +CM55TCMCR_ADDR = "0x56008008" +CM55RSTCR_ADDR = "0x56008018" +CM55TCMCR_EXPECTED_MASK = 0xFF +CM55TCMCR_EXPECTED_VALUE = 0x99 + +LOG = logging.getLogger(__name__) + + +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 subprocess output at the requested level.""" + if not output: + return + for line in output.rstrip().splitlines(): + LOG.log(level, line) + + +def err(msg): + """Report a user-visible error line.""" + LOG.error("%s", msg) + + +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 _openocd_config_cmd(openocd, timeout_s): + """Build the OpenOCD command for one FLEXMEM configuration attempt.""" + script_lines = flexmem_script_lines( + timeout_ms=int(timeout_s * 1000), + rcc_apb4ensr2_addr=RCC_APB4ENSR2_ADDR, + cm55tcmcr_addr=CM55TCMCR_ADDR, + cm55rstcr_addr=CM55RSTCR_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=serial_from_env(), + transport=transport_from_env(), + ) + ["-f", script_path] + return cmd, script_path + + +def _run_openocd_config_once(openocd, timeout_s): + """Run the OpenOCD FLEXMEM configuration script.""" + cmd, script_path = _openocd_config_cmd(openocd, timeout_s) + try: + cp = run_quiet(cmd) + finally: + try: + os.unlink(script_path) + except OSError: + pass + return cp + + +def run_openocd_config(timeout_s): + """Configure FLEXMEM using OpenOCD memory reads and writes.""" + openocd = openocd_cli() + if openocd is None: + return 2 + + cp = _run_openocd_config_once(openocd, timeout_s) + 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 register configuration failed") + return cp.returncode + + +def main(): + """Configure FLEXMEM and verify the latched layout.""" + configure_logging() + + if len(sys.argv) != 1: + err(f"Usage: {sys.argv[0]}") + return 2 + + timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "5")) + + return run_openocd_config(timeout_s) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py new file mode 100644 index 0000000000..f95f9f0e53 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py @@ -0,0 +1,121 @@ +# 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 + +"""Generate the GDB batch script used to load and run RAM-resident tests.""" + + +def build_run_script( + *, + port, + bootargs_break, + reset_handler_jump, + hardfault_break, + done_break, + argv_bin, + arg_block_addr, + arg_block_sym, +): + """ + 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 Zephyr calls ``get_bootargs()``, restore bootargs after + ``.bss`` has been cleared, install completion/fault breakpoints, continue, + then read the target return code from the completion breakpoint. + """ + gdb_lines = [ + "set pagination off", + "set confirm off", + f"target remote localhost:{port}", + # Keep the GDB script focused on target state and RAM transfers. + "load", + f"tbreak {bootargs_break}", + f"jump {reset_handler_jump}", + restore_bootargs_command(argv_bin, arg_block_addr, arg_block_sym), + "set $nucleo_exit_code = -1", + f"break {done_break}", + "commands", + " set $nucleo_exit_code = $r0", + " echo [[NUCLEO-DONE]]\\n", + "end", + f"break {hardfault_break}", + "commands", + " echo [[NUCLEO-HARDFAULT]]\\n", + "end", + ] + gdb_lines += [ + "continue", + "if $nucleo_exit_code != -1", + " echo NUCLEO_EXIT_CODE=", + " output/d $nucleo_exit_code", + " echo \\n", + "end", + ] + 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 + + +def restore_bootargs_command(argv_bin, arg_block_addr, arg_block_sym): + """Return the GDB ``restore`` command for the bootargs string.""" + 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 fault_diagnostic_commands(): + """Return commands that print Cortex-M fault diagnostics.""" + 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/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py new file mode 100644 index 0000000000..ab0e6e8e11 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py @@ -0,0 +1,215 @@ +# 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 + +"""Locate OpenOCD and build NUCLEO-N657X0-Q command lines.""" + +import os +import shutil +import subprocess + + +DEFAULT_INTERFACE = "interface/stlink.cfg" +DEFAULT_TARGET = "target/stm32n6x.cfg" +DEFAULT_CPU_TARGET = "stm32n6x.cpu" +DEFAULT_SWO_TARGET = "stm32n6x.swo" +DEFAULT_TPIU_TARGET = "" + + +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="8000"): + """Return adapter speed in kHz.""" + return os.environ.get("OPENOCD_SPEED", default) + + +def swo_traceclk_from_env(default="100000000"): + """Return the target trace clock in Hz for SWO capture.""" + return os.environ.get("SWO_TRACECLK", default) + + +def swo_pin_freq_from_env(default="1000000"): + """Return the async SWO pin bitrate in Hz.""" + return os.environ.get("SWO_PIN_FREQ", default) + + +def swo_formatter_from_env(default="0"): + """Return the OpenOCD TPIU formatter setting for SWO capture.""" + return os.environ.get("SWO_FORMATTER", default) + + +def swo_target_from_env(default=DEFAULT_SWO_TARGET): + """Return the OpenOCD trace component used for SWO capture.""" + return os.environ.get("OPENOCD_SWO_TARGET", default) + + +def tpiu_target_from_env(default=DEFAULT_TPIU_TARGET): + """Return the OpenOCD TPIU component used to configure target trace output.""" + return os.environ.get("OPENOCD_TPIU_TARGET", 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( + *, + openocd="openocd", + interface=None, + target=None, + speed="8000", + serial="", + transport="swd", +): + """Return common OpenOCD arguments for the NUCLEO debug 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"if {{[lsearch -exact [target names] {DEFAULT_CPU_TARGET}] >= 0}} " + f"{{ {DEFAULT_CPU_TARGET} configure -defer-examine }}" + ), + "-c", + f"adapter speed {speed}", + ] + if serial: + args += ["-c", f"adapter serial {serial}"] + return args + + +def runtime_gdbserver_cmd( + *, + openocd="openocd", + port=3333, + swo_port=None, + swo_traceclk="100000000", + swo_pin_freq="1000000", + swo_formatter="0", + swo_target=None, + tpiu_target=None, + speed="8000", + serial="", + transport="swd", +): + """Return the OpenOCD command used as the runtime GDB server.""" + cmd = 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", + f"{DEFAULT_CPU_TARGET} arp_examine", + ] + if swo_port is not None: + swo_target = swo_target or swo_target_from_env() + tpiu_target = tpiu_target or tpiu_target_from_env() + if tpiu_target: + cmd += [ + "-c", + ( + f"{tpiu_target} configure -protocol uart " + f"-traceclk {swo_traceclk} -pin-freq {swo_pin_freq} " + f"-formatter {swo_formatter}" + ), + "-c", + f"{tpiu_target} enable", + ] + cmd += [ + "-c", + ( + f"{swo_target} configure -protocol uart " + f"-traceclk {swo_traceclk} -pin-freq {swo_pin_freq} " + f"-output :{swo_port} -formatter {swo_formatter}" + ), + "-c", + f"{swo_target} enable", + "-c", + f"{DEFAULT_CPU_TARGET} itm port 0 on", + ] + return cmd + + +def flexmem_script_lines( + *, + timeout_ms, + mem_target=DEFAULT_CPU_TARGET, + rcc_apb4ensr2_addr="0x56028a78", + cm55tcmcr_addr="0x56008008", + cm55rstcr_addr="0x56008018", + expected_mask=0xFF, + expected_value=0x99, +): + """Return an OpenOCD TCL script for configuring STM32N6 FLEXMEM.""" + return [ + "reset_config none", + "init", + f"{mem_target} arp_examine", + "proc read32 {addr} {", + f" return [lindex [{mem_target} read_memory $addr 32 1] 0]", + "}", + "proc write32 {addr value} {", + f" {mem_target} mww $addr $value", + "}", + f"set rcc_apb4ensr2 [read32 {rcc_apb4ensr2_addr}]", + f"write32 {rcc_apb4ensr2_addr} [expr {{$rcc_apb4ensr2 | 0x1}}]", + f"set cm55tcmcr [read32 {cm55tcmcr_addr}]", + f"write32 {cm55tcmcr_addr} [expr {{($cm55tcmcr & ~0xff) | 0x{expected_value:x}}}]", + f"set cm55rstcr [read32 {cm55rstcr_addr}]", + f"write32 {cm55rstcr_addr} [expr {{$cm55rstcr | 0x1}}]", + "proc wait_flexmem_configured {} {", + f" set deadline [expr {{[clock milliseconds] + {int(timeout_ms)}}}]", + " while {[clock milliseconds] < $deadline} {", + f" set value [read32 {cm55tcmcr_addr}]", + 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_config none", + "reset run", + "shutdown", + ] diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/results.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/results.py new file mode 100644 index 0000000000..e534e74736 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/results.py @@ -0,0 +1,107 @@ +# 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 + +"""Parse GDB failure output and Cortex-M fault diagnostics.""" + +import re + +HARDFAULT_SENTINEL = "[[NUCLEO-HARDFAULT]]" + + +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 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)" + r"=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" + 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() + ] + 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/zephyr/nucleo_n657x0_q/nucleo_host/symbols.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/symbols.py new file mode 100644 index 0000000000..4951b35763 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/symbols.py @@ -0,0 +1,79 @@ +# 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 + +"""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.""" + 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``.""" + 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/zephyr/platform.mk b/test/zephyr/platform.mk index 8656e68ddc..d4cadbcae8 100644 --- a/test/zephyr/platform.mk +++ b/test/zephyr/platform.mk @@ -16,8 +16,9 @@ PLATFORM_PATH := test/zephyr # define it here too so CUSTOM_BUILD below expands to the right path. BUILD_DIR ?= test/build -# ZEPHYR_TARGET= selects a target. Each key maps to a Zephyr board and the -# QEMU machine emulating it; add a board with a row below. +# ZEPHYR_TARGET= selects a target. QEMU targets map to both a Zephyr board +# and the QEMU machine emulating it; hardware targets map to a Zephyr board and +# provide their own execution wrapper. Add a target with a row below. ZEPHYR_TARGET ?= mps3-an547 ZEPHYR_BOARD_mps2-an385 := mps2/an385 @@ -30,13 +31,21 @@ ZEPHYR_BOARD_mps2-an521 := mps2/an521/cpu0 ZEPHYR_QEMU_mps2-an521 := mps2-an521 # Cortex-M33 ZEPHYR_BOARD_mps3-an547 := mps3/corstone300/an547 ZEPHYR_QEMU_mps3-an547 := mps3-an547 # Cortex-M55 +ZEPHYR_BOARD_nucleo-n657x0-q := nucleo_n657x0_q # Cortex-M55 (hardware) ZEPHYR_FIPS202_BACKEND_mps3-an547 := fips202/native/armv81m/mve.h +ZEPHYR_FIPS202_BACKEND_nucleo-n657x0-q := fips202/native/armv81m/mve.h -ZEPHYR_TARGETS := mps2-an385 mps2-an386 mps2-an500 mps2-an521 mps3-an547 +ZEPHYR_TARGETS := mps2-an385 mps2-an386 mps2-an500 mps2-an521 mps3-an547 nucleo-n657x0-q ZEPHYR_BOARD := $(ZEPHYR_BOARD_$(ZEPHYR_TARGET)) export QEMU_MACHINE := $(strip $(ZEPHYR_QEMU_$(ZEPHYR_TARGET))) +ZEPHYR_IS_NUCLEO_N657X0_Q := $(filter nucleo-n657x0-q,$(ZEPHYR_TARGET)) + +ifneq ($(ZEPHYR_IS_NUCLEO_N657X0_Q),) +CROSS_PREFIX ?= arm-none-eabi- +CC = gcc +endif ifeq ($(ZEPHYR_BOARD),) $(error Unknown ZEPHYR_TARGET '$(ZEPHYR_TARGET)'. Supported: $(ZEPHYR_TARGETS)) @@ -51,6 +60,34 @@ ZEPHYR_FIPS202_BACKEND := $(if $(filter 1,$(OPT)),$(strip $(ZEPHYR_FIPS202_BACKE ZEPHYR_APP := $(PLATFORM_PATH)/app ZEPHYR_BUILD_DIR := $(BUILD_DIR)/zephyr/$(ZEPHYR_TARGET) +ZEPHYR_ACTIVE_TARGET := $(BUILD_DIR)/zephyr/.active-target +ZEPHYR_APP_INPUTS := \ + $(ZEPHYR_APP)/CMakeLists.txt \ + $(ZEPHYR_APP)/Kconfig \ + $(ZEPHYR_APP)/prj.conf \ + $(ZEPHYR_APP)/nucleo_n657x0_q.conf \ + $(ZEPHYR_APP)/shim.c \ + $(ZEPHYR_APP)/shim_nucleo_n657x0_q.c \ + $(ZEPHYR_APP)/nucleo_n657x0_q.overlay +ZEPHYR_NUCLEO_PLATFORM_PATH := $(PLATFORM_PATH)/nucleo_n657x0_q +ZEPHYR_NUCLEO_OVERLAY := $(abspath $(ZEPHYR_APP)/nucleo_n657x0_q.overlay) +ZEPHYR_NUCLEO_CONF := $(abspath $(ZEPHYR_APP)/nucleo_n657x0_q.conf) +ZEPHYR_TARGET_CMAKE_ARGS := $(if $(ZEPHYR_IS_NUCLEO_N657X0_Q),\ + -DZEPHYR_NUCLEO_N657X0_Q=ON \ + -DEXTRA_CONF_FILE=$(ZEPHYR_NUCLEO_CONF) \ + -DDTC_OVERLAY_FILE=$(ZEPHYR_NUCLEO_OVERLAY)) + +# Test binary output paths are shared across ZEPHYR_TARGET values, while the +# CMake build directory is target-specific. Keep a lightweight marker containing +# the last requested target, and only touch it when the target changes. Binaries +# depending on this marker are then rebuilt after a target switch without +# forcing a clean rebuild when the target is unchanged. +.PHONY: zephyr_target_marker_force +$(ZEPHYR_ACTIVE_TARGET): zephyr_target_marker_force + $(Q)[ -d $(@D) ] || mkdir -p $(@D) + $(Q)if [ ! -f $@ ] || [ "$$(cat $@)" != "$(ZEPHYR_TARGET)" ]; then \ + echo "$(ZEPHYR_TARGET)" > $@; \ + fi # Per-binary CMake build dir, keyed on $(notdir $@) so binaries build in # parallel. Recipe-expanded, so $@ is the specific bin being built. @@ -88,9 +125,21 @@ CUSTOM_BUILD = \ -DZEPHYR_TEST_CFLAGS="$(ZEPHYR_TEST_CFLAGS)" \ -DZEPHYR_FIPS202_BACKEND=$(ZEPHYR_FIPS202_BACKEND) \ $(if $(ZEPHYR_FIPS202_BACKEND),-DCONFIG_FIPS202_MVE_BACKEND=y) \ + $(ZEPHYR_TARGET_CMAKE_ARGS) \ -DUSER_CACHE_DIR=$(abspath $(ZEPHYR_OUT)/.cache) \ >/dev/null && \ cmake --build $(ZEPHYR_OUT) >/dev/null && \ cp $(ZEPHYR_OUT)/zephyr/zephyr.elf $@ +# A custom build links the test sources directly rather than from objects, so +# nothing otherwise makes the bins depend on the Zephyr app inputs or the +# active-target marker. components.mk attaches CUSTOM_BUILD_DEPS to every test +# binary (in its CUSTOM_BUILD branch), so a CMakeLists/shim/overlay edit or a +# target switch forces a rebuild. Set here (before components.mk is included). +CUSTOM_BUILD_DEPS := $(ZEPHYR_ACTIVE_TARGET) $(ZEPHYR_APP_INPUTS) + +ifeq ($(ZEPHYR_IS_NUCLEO_N657X0_Q),) EXEC_WRAPPER := $(abspath $(PLATFORM_PATH)/exec_wrapper.py) +else +EXEC_WRAPPER := $(abspath $(ZEPHYR_NUCLEO_PLATFORM_PATH)/exec_wrapper.py) +endif From 439cbef21f1097ac10da48a266c0a9b05d4dfc60 Mon Sep 17 00:00:00 2001 From: "Matthias J. Kannwischer" Date: Wed, 8 Jul 2026 13:24:05 +0800 Subject: [PATCH 2/5] zephyr: keep project CFLAGS off Zephyr internals - Port of pq-code-package/mlkem-native#1791 Signed-off-by: Matthias J. Kannwischer --- .github/actions/bench/action.yml | 4 ---- test/zephyr/platform.mk | 7 +++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/actions/bench/action.yml b/.github/actions/bench/action.yml index 93148afc20..54ceb7b0d9 100644 --- a/.github/actions/bench/action.yml +++ b/.github/actions/bench/action.yml @@ -89,10 +89,6 @@ runs: --ldflags="${{ inputs.ldflags }}" \ --opt=$([[ ${{ inputs.opt }} == "false" ]] && echo "no_opt" || echo "opt") \ -v ${{ inputs.bench_extra_args }} - - name: Check namespace - shell: ${{ env.SHELL }} - run: | - check-namespace - name: Store benchmark result uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1 with: diff --git a/test/zephyr/platform.mk b/test/zephyr/platform.mk index d4cadbcae8..03b863ff8c 100644 --- a/test/zephyr/platform.mk +++ b/test/zephyr/platform.mk @@ -115,10 +115,13 @@ CFLAGS += -DNTESTS=3 \ # Requires AUTO=0 (see .github/workflows/zephyr.yml): the host-arch flags AUTO=1 # adds must not reach the Zephyr toolchain, which selects the target arch itself. ZEPHYR_TEST_CFLAGS = $(subst \",\\\",$(patsubst -Imldsa,-I$(abspath mldsa),$(CFLAGS))) +# Keep make-exported project flags out of Zephyr's own CMake build; the app +# sources get those flags explicitly via ZEPHYR_TEST_CFLAGS. +ZEPHYR_CMAKE_ENV := env -u CFLAGS -u CXXFLAGS -u CPPFLAGS -u LDFLAGS CUSTOM_BUILD = \ echo " ZEPHYR $(ZEPHYR_TARGET): $(notdir $@)" && \ - cmake -GNinja -S $(ZEPHYR_APP) -B $(ZEPHYR_OUT) \ + $(ZEPHYR_CMAKE_ENV) cmake -GNinja -S $(ZEPHYR_APP) -B $(ZEPHYR_OUT) \ -DBOARD=$(ZEPHYR_BOARD) \ -DZEPHYR_NATIVE_ROOT=$(CURDIR) \ -DZEPHYR_TEST_SRCS="$(strip $(TEST_SRCS))" \ @@ -128,7 +131,7 @@ CUSTOM_BUILD = \ $(ZEPHYR_TARGET_CMAKE_ARGS) \ -DUSER_CACHE_DIR=$(abspath $(ZEPHYR_OUT)/.cache) \ >/dev/null && \ - cmake --build $(ZEPHYR_OUT) >/dev/null && \ + $(ZEPHYR_CMAKE_ENV) cmake --build $(ZEPHYR_OUT) >/dev/null && \ cp $(ZEPHYR_OUT)/zephyr/zephyr.elf $@ # A custom build links the test sources directly rather than from objects, so From 303042fd590e5474294dfe87c03a7a769773c88d Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Wed, 8 Jul 2026 14:45:37 +0100 Subject: [PATCH 3/5] zephyr: use 64-bit cycle counter for benchmarks Zephyr's k_cycle_get_32() can wrap during long NUCLEO benchmark windows, producing huge unsigned deltas in the benchmark output. Use k_cycle_get_64() for Zephyr targets. Also make ML-DSA benchmark percentile sorting and formatting robust for large uint64_t cycle values. Signed-off-by: Brendan Moran --- test/bench/bench_mldsa.c | 11 +++++++---- test/hal/hal.c | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/test/bench/bench_mldsa.c b/test/bench/bench_mldsa.c index a8b5a3851d..15d11f2014 100644 --- a/test/bench/bench_mldsa.c +++ b/test/bench/bench_mldsa.c @@ -42,7 +42,10 @@ static int cmp_uint64_t(const void *a, const void *b) { - return (int)((*((const uint64_t *)a)) - (*((const uint64_t *)b))); + const uint64_t va = *((const uint64_t *)a); + const uint64_t vb = *((const uint64_t *)b); + + return (va > vb) - (va < vb); } static void print_avg(const char *txt, uint64_t cyc[MLD_BENCHMARK_NTESTS]) @@ -66,7 +69,7 @@ static void print_percentile_legend(void) printf("%21s", "percentile"); for (i = 0; i < sizeof(percentiles) / sizeof(percentiles[0]); i++) { - printf("%9d", percentiles[i]); + printf("%12d", percentiles[i]); } printf("\n"); } @@ -78,8 +81,8 @@ static void print_percentiles(const char *txt, printf("%10s percentiles:", txt); for (i = 0; i < sizeof(percentiles) / sizeof(percentiles[0]); i++) { - printf("%9" PRIu64, (cyc)[MLD_BENCHMARK_NTESTS * percentiles[i] / 100] / - MLD_BENCHMARK_NITERATIONS); + printf("%12" PRIu64, (cyc)[MLD_BENCHMARK_NTESTS * percentiles[i] / 100] / + MLD_BENCHMARK_NITERATIONS); } printf("\n"); } diff --git a/test/hal/hal.c b/test/hal/hal.c index 87fd7e25db..ddd0133867 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -50,7 +50,7 @@ void enable_cyclecounter(void) {} void disable_cyclecounter(void) {} -uint64_t get_cyclecounter(void) { return k_cycle_get_32(); } +uint64_t get_cyclecounter(void) { return k_cycle_get_64(); } #elif defined(PMU_CYCLES) From cd266857849e8226894474bdd56c0024c95b99b1 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Thu, 9 Jul 2026 07:17:55 +0100 Subject: [PATCH 4/5] zephyr: link NUCLEO test images into TCM Run the NUCLEO-N657X0-Q Zephyr test image as XIP from ITCM and place runtime data/stacks in DTCM after the runner expands FLEXMEM to 256 KiB ITCM and 256 KiB DTCM. Force STM32N6 SystemInit() to use the ITCM vector table address for VTOR. Without this, its fallback derives VTOR from &g_pfnVectors, which moves into DTCM with Zephyr data and points VTOR at RAM data instead of the vector table. Add an early reset hook that scrubs the expanded DTCM window before Zephyr stack, BSS, and MPU setup touch it. Tested: - nix develop .#zephyr --command make run_func EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=mps3-an547 CYCLES=NO -j1 - nix develop .#zephyr --command make run_bench_components EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q CYCLES=NO -j1 Signed-off-by: Brendan Moran --- test/zephyr/app/CMakeLists.txt | 8 ++++++++ test/zephyr/app/nucleo_n657x0_q.conf | 16 ++++++++++------ test/zephyr/app/nucleo_n657x0_q.overlay | 15 ++++++++++++++- test/zephyr/app/shim_nucleo_n657x0_q.c | 22 ++++++++++++++++++++++ 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/test/zephyr/app/CMakeLists.txt b/test/zephyr/app/CMakeLists.txt index 914ee0e7c3..b5d56d4761 100644 --- a/test/zephyr/app/CMakeLists.txt +++ b/test/zephyr/app/CMakeLists.txt @@ -33,6 +33,14 @@ target_include_directories(app PRIVATE # emulator with the test's exit code; the NUCLEO-N657X0-Q hardware target needs # its own shim plus libc wrappers that route stdio over ITM/SWO. if(ZEPHYR_NUCLEO_N657X0_Q) + # The STM32N6 SystemInit() fallback derives VTOR from &g_pfnVectors. With + # Zephyr data in DTCM that symbol lives in RAM, so force VTOR to the ITCM + # vector table instead. + zephyr_compile_definitions( + USER_VECT_TAB_ADDRESS + VECT_TAB_BASE_ADDRESS=0x10000000 + VECT_TAB_OFFSET=0x0 + ) target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/shim_nucleo_n657x0_q.c) target_compile_definitions(app PRIVATE printf=__wrap_printf diff --git a/test/zephyr/app/nucleo_n657x0_q.conf b/test/zephyr/app/nucleo_n657x0_q.conf index a517a7a0d4..511152f1e1 100644 --- a/test/zephyr/app/nucleo_n657x0_q.conf +++ b/test/zephyr/app/nucleo_n657x0_q.conf @@ -2,12 +2,16 @@ # Copyright (c) The mlkem-native project authors # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT -# The board default exposes a 2 MiB SRAM window at 0x34000000; its first -# 400 KiB is FLEXRAM. The NUCLEO runner repurposes FLEXMEM to expand the TCMs -# to 256 KiB ITCM + 256 KiB DTCM, so keep the Zephyr image above that low -# window and leave fixed host buffers after the linked image. -CONFIG_SRAM_BASE_ADDRESS=0x34080000 -CONFIG_SRAM_SIZE=192 +# The NUCLEO runner repurposes FLEXMEM before loading the ELF, expanding the +# Cortex-M55 TCMs to 256 KiB ITCM + 256 KiB DTCM. Link the RAM-loaded Zephyr +# image as XIP from ITCM and keep runtime data/stacks in DTCM. The fixed +# bootargs handoff block remains in AXISRAM at 0x340b0000. +CONFIG_XIP=y +CONFIG_FLASH_BASE_ADDRESS=0x10000000 +CONFIG_FLASH_SIZE=256 +CONFIG_SRAM_BASE_ADDRESS=0x30000000 +CONFIG_SRAM_SIZE=256 +CONFIG_SOC_EARLY_RESET_HOOK=y # The component benchmark app keeps five 8 KiB scratch buffers on the main # stack; measured max static stack depth is about 49.5 KiB, so 32 KiB is not diff --git a/test/zephyr/app/nucleo_n657x0_q.overlay b/test/zephyr/app/nucleo_n657x0_q.overlay index 2e87f5e984..bc41225134 100644 --- a/test/zephyr/app/nucleo_n657x0_q.overlay +++ b/test/zephyr/app/nucleo_n657x0_q.overlay @@ -5,6 +5,18 @@ */ / { + mld_itcm: memory@10000000 { + compatible = "zephyr,memory-region", "arm,itcm"; + reg = <0x10000000 0x00040000>; + zephyr,memory-region = "ITCM"; + }; + + mld_dtcm: memory@30000000 { + compatible = "zephyr,memory-region", "arm,dtcm"; + reg = <0x30000000 0x00040000>; + zephyr,memory-region = "DTCM"; + }; + mld_axisram: memory@34080000 { compatible = "zephyr,memory-region", "mmio-sram"; reg = <0x34080000 0x00030000>; @@ -12,6 +24,7 @@ }; chosen { - zephyr,sram = &mld_axisram; + zephyr,flash = &mld_itcm; + zephyr,sram = &mld_dtcm; }; }; diff --git a/test/zephyr/app/shim_nucleo_n657x0_q.c b/test/zephyr/app/shim_nucleo_n657x0_q.c index eebb01685a..a1545b39dd 100644 --- a/test/zephyr/app/shim_nucleo_n657x0_q.c +++ b/test/zephyr/app/shim_nucleo_n657x0_q.c @@ -51,6 +51,28 @@ static char nucleo_stdout_buf[8192]; static size_t nucleo_stdout_len; static bool nucleo_swo_enabled; +/* + * Zephyr calls this before stack setup or normal RAM initialization. The + * NUCLEO runner has already expanded DTCM to 256 KiB, and the image now keeps + * runtime data/stacks there, so scrub the whole DTCM window before Zephyr's + * early stack, BSS, and MPU setup touch it. + */ +__attribute__((naked, used)) void soc_early_reset_hook(void) +{ + __asm__ volatile( + "movs r2, #0\n" + "movs r3, #0\n" + "ldr r0, =0x30000000\n" + "ldr r1, =0x30040000\n" + "1:\n" + "strd r2, r3, [r0], #8\n" + "cmp r0, r1\n" + "blo 1b\n" + "dsb\n" + "isb\n" + "bx lr\n"); +} + static void nucleo_swo_pin_setup(void) { LL_AHB4_GRP1_EnableClock(LL_AHB4_GRP1_PERIPH_GPIOB); From 0c48ca96e6795b1c03d3c4d43131be349f505cb9 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Mon, 13 Jul 2026 13:12:21 +0100 Subject: [PATCH 5/5] bench: document uint64_t comparator pattern Signed-off-by: Brendan Moran --- test/bench/bench_mldsa.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/bench/bench_mldsa.c b/test/bench/bench_mldsa.c index 15d11f2014..9fbbaf878b 100644 --- a/test/bench/bench_mldsa.c +++ b/test/bench/bench_mldsa.c @@ -45,6 +45,8 @@ static int cmp_uint64_t(const void *a, const void *b) const uint64_t va = *((const uint64_t *)a); const uint64_t vb = *((const uint64_t *)b); + /* Avoid subtracting uint64_t values: qsort only needs the sign, and the + * difference may exceed int range after long benchmark runs. */ return (va > vb) - (va < vb); }