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..4449417da1 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -28,62 +28,73 @@ 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" + # - 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 + archflags: "" 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: '' + 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 }} @@ -96,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/.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/flake.nix b/flake.nix index 80d3405393..531a18214e 100644 --- a/flake.nix +++ b/flake.nix @@ -53,6 +53,7 @@ ln -sfn "$S2N_BIGNUM_DIR" "$IMPORTS_DIR/s2n_bignum" ln -sfn "$PROOF_DIR" "$IMPORTS_DIR/mlkem_native" ''; + in { _module.args.pkgs = import inputs.nixpkgs { @@ -104,6 +105,27 @@ } ++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ config.packages.valgrind_varlat ]; }).overrideAttrs (old: { shellHook = holLightShellHook; }); + # 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 + { + 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) linters nucleo-n657x0-q st-openocd; + 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..cf3dae9abd --- /dev/null +++ b/nix/nucleo-n657x0-q/default.nix @@ -0,0 +1,247 @@ +# 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 files + # Prefer board-specific FSBL-LRUN files explicitly when available. + fsbl_tpl="Projects/NUCLEO-N657X0-Q/Templates/Template_FSBL_LRUN" + mkdir -p "$outp/gcc" + if [ -d "$fsbl_tpl" ]; then + # Explicitly select: + # - Startup: STM32CubeIDE/Boot/Startup/startup_stm32n657xx_fsbl.s + # - 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" + 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" + # 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 "$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 + + # The repository linker scripts define RAM-only config/test layouts explicitly. + ''; + + setupHook = writeText "setup-hook.sh" '' + export NUCLEO_N657X0_Q_PATH="$1/platform/nucleo-n657x0-q/src/platform/" + # Platform sources only; the devshell provides the OpenOCD runtime backend. + ''; + + meta = { + description = "Platform files for STM32 NUCLEO-N657X0-Q RAM-only OpenOCD tests"; + homepage = "https://github.com/STMicroelectronics/STM32CubeN6"; + }; +} 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 c638357210..7937f5f634 100644 --- a/nix/util.nix +++ b/nix/util.nix @@ -104,6 +104,8 @@ rec { s2n_bignum = pkgs.callPackage ./s2n_bignum { }; 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/tests b/scripts/tests index 147c62f9a1..ddd456a62a 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(): @@ -1355,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/README.md b/test/baremetal/platform/nucleo-n657x0-q/README.md new file mode 100644 index 0000000000..c10b03d48b --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/README.md @@ -0,0 +1,258 @@ + + +# NUCLEO-N657X0-Q Baremetal Platform + +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. 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 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:-8000}` by default. + +Useful environment variables: + +``` +export OPENOCD_SPEED=8000 +export OPENOCD_SERIAL= +export GDB_PORT=3333 +``` + +`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. + +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 +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. 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 +helper. + +## GDB Runtime Load + +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 halts without requesting another +reset. 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 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. +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 +`flexmem_configure.py` and retry according to the recovery environment variables +documented below. + +## Prerequisites + +- A NUCLEO-N657X0-Q connected over USB. +- The board devshell, which provides `arm-none-eabi-gdb` and the pinned + `.#st-openocd` package: + +``` +nix develop .#nucleo-n657x0-q +``` + +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 + +Build the FLEXMEM config binary and one RAM-only test binary: + +``` +make flexmem_config func_512 EXTRA_MAKEFILE=test/baremetal/platform/nucleo-n657x0-q/platform.mk -j1 V=1 +``` + +The config binary is: + +``` +test/build/nucleo-n657x0-q/flexmem_config.elf +``` + +An example test binary is: + +``` +test/build/mlkem512/bin/test_mlkem512 +``` + +## Run + +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 +``` + +Or run each stage explicitly: + +``` +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 +``` + +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 the test wrapper 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 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`: 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 + +## 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, +probe firmware, and OpenOCD behavior. + +## Notes + +- 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 new file mode 100755 index 0000000000..b26bc52e7f --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/exec_wrapper.py @@ -0,0 +1,581 @@ +#!/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 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``, 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 subprocess +import sys +import tempfile +import time +import select + +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 split_stdout_capture +from nucleo_host.symbols import default_readelf +from nucleo_host.symbols import resolve_symbol + +VERBOSE = False +STDOUT_BYTES_EMITTED = 0 +TARGET_FAILURE = False +TARGET_FAILURE_KIND = "" +SUPPRESS_RETRYABLE_DIAGNOSTICS = False +LAST_FAULT_DIAGNOSTICS = "" +LAST_LOAD_FAILURE_DIAGNOSTICS = "" +LOG = logging.getLogger(__name__) + + +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, **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 run(cmd, **kwargs): + """Thin wrapper around ``subprocess.run`` for test-time monkeypatching.""" + return subprocess.run(cmd, **kwargs) + + +def popen(cmd, **kwargs): + """Wrap ``subprocess.Popen`` for test-time monkeypatching.""" + return subprocess.Popen(cmd, **kwargs) + + +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_flexmem(reason: str, failure_message: str) -> 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()) + + 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}") + err(flexmem_config_build_instructions(config_elf)) + return False + + info(f"[exec_wrapper] recovering from {reason}: re-running FLEXMEM config") + recovery_env = os.environ.copy() + cp = run( + [sys.executable, configure_script, config_elf], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + env=recovery_env, + ) + if cp.returncode != 0: + err(failure_message) + log_output(cp.stdout, logging.ERROR) + return False + if VERBOSE: + log_output(cp.stdout, logging.DEBUG) + 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 its wrapper exit code.""" + 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) + 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 # 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", default_readelf()) + port = int(os.environ.get("GDB_PORT", "3333")) + gdb_run_timeout = float(os.environ.get("GDB_RUN_TIMEOUT", "180")) + + # 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 + + def _resolve_symbol_addr(elf_path: str, sym: str): + """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"): + addr = _resolve_symbol_addr(elf, cand) + if addr is not None: + 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", 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 + 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 + + 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") + 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") + 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 " + "flexmem_configure.py; no runtime TCM probing" + ) + + info(f"[exec_wrapper] starting {server_label} 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: + exit_code = None + time.sleep(0.8) + + # Give the server a brief moment, then check for early exit + if stp.poll() is not None: + # 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) + return 2 + + 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 ============") + 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] + + # 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, + stderr=subprocess.PIPE, + text=True, + ) + gdb_deadline = ( + time.time() + gdb_run_timeout if gdb_run_timeout > 0 else None + ) + + # Stream OpenOCD output until gdb finishes without blocking on + # readline(). + while True: + if stp.stdout is not None: + try: + r, _, _ = select.select([stp.stdout], [], [], 0.1) + if r: + line = stp.stdout.readline() + if line: + # OpenOCD stdout is logged only in verbose + # mode. + if VERBOSE: + 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: + 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) + except Exception: + try: + gdbp.kill() + except Exception: + pass + try: + out, errout = gdbp.communicate(timeout=1.0) + if out and not SUPPRESS_RETRYABLE_DIAGNOSTICS: + log_output(out, logging.ERROR) + if errout and not SUPPRESS_RETRYABLE_DIAGNOSTICS: + err(errout, end="") + except Exception: + pass + return 124 + + out, errout = gdbp.communicate() + if out and VERBOSE: + log_output(out, logging.DEBUG) + if errout and VERBOSE: + # gdb chatter / errors (verbose only) + err(errout, end="") + + gdb_text = f"{out}\n{errout}" + layout_failed = LAYOUT_FAIL_SENTINEL in gdb_text + hardfaulted = gdb_observed_hardfault(gdb_text) + target_failed = layout_failed or hardfaulted + + if os.path.exists(stdout_capture_bin): + try: + # Parse the same exit sentinel from dumped RAM output as + # 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: + 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")) + 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 exit_code is not None: + return int(exit_code) if isinstance(exit_code, int) else 1 + + if layout_failed: + TARGET_FAILURE = True + TARGET_FAILURE_KIND = "layout" + err("FAIL!") + err("FLEXMEM layout check failed on target") + return 1 + + 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: + 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 + + if gdbp.returncode != 0: + 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, + 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}") + 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 + + finally: + # Terminate OpenOCD. + try: + stp.terminate() + stp.wait(timeout=1.5) + except Exception: + try: + stp.kill() + except Exception: + pass + # Remove the temp gdb script + try: + if "gdb_script_path" in locals(): + os.unlink(gdb_script_path) + except Exception: + pass + + +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 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( + "[exec_wrapper] retrying after recovered GDB " + "load failure " + f"({load_failure_recoveries}/" + f"{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( + "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) + return last_rc + if TARGET_FAILURE_KIND == "hardfault" and can_retry_hardfault: + hardfault_recoveries += 1 + if _recover_after_hardfault(): + if VERBOSE: + 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: + return last_rc + transport_retries += 1 + if VERBOSE: + err( + "[exec_wrapper] retrying after transport failure " + f"({transport_retries}/{attempts - 1})" + ) + time.sleep(0.5) + + +if __name__ == "__main__": + raise SystemExit(main()) 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..3b91079c7d --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/flexmem_configure.py @@ -0,0 +1,179 @@ +#!/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 + +""" +Configure STM32N6 FLEXMEM before loading RAM-resident test images. + +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 +import logging +import sys +import tempfile + +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." + +# 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(): + """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 resolve_symbol(elf, symbol): + """Resolve a symbol with ``nm`` for direct RAM launch setup.""" + # 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 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, 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, + estack_addr=estack_addr, + timeout_ms=int(timeout_s * 1000), + 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)) + 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 _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: + try: + 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: + 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() + + 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}") + err(flexmem_config_build_instructions(elf)) + 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")) + + return run_openocd_config(elf, main_thumb, estack_addr, timeout_s) + + +if __name__ == "__main__": + raise SystemExit(main()) 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 new file mode 100644 index 0000000000..15339b19ba --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/linker/ram_secure.ld @@ -0,0 +1,203 @@ +/* + * 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 +{ + ITCM (rwx) : ORIGIN = 0x00000000, LENGTH = 256K + DTCM (rwx) : ORIGIN = 0x30000000, LENGTH = 256K + AXISRAM (rwx) : ORIGIN = 0x34080000, LENGTH = 1536K +} + +__itcm_start__ = ORIGIN(ITCM); +__itcm_size__ = LENGTH(ITCM); +__dtcm_start__ = ORIGIN(DTCM); +__dtcm_size__ = LENGTH(DTCM); + +__stack_size__ = 240K; +__heap_size__ = 0; + +__StackTop = ORIGIN(DTCM) + LENGTH(DTCM); +__StackLimit = __StackTop - __stack_size__; +__HeapBase = ORIGIN(DTCM); +__HeapLimit = __StackLimit; + +PROVIDE(__stack = __StackTop); +PROVIDE(_estack = __StackTop); +PROVIDE(__initial_sp = __StackTop); +PROVIDE(_sstack = __StackLimit); + +SECTIONS +{ + .vectors ORIGIN(ITCM) : + { + KEEP(*(.isr_vector)) + KEEP(*(.vectors)) + . = ORIGIN(ITCM) + 0x400; + } > ITCM + + .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__ = .; + } > ITCM + + .gnu.sgstubs : + { + . = ALIGN(32); + KEEP(*(.gnu.sgstubs)) + KEEP(*(.gnu.sgstubs.*)) + . = ALIGN(32); + } > ITCM + + .preinit_array : + { + PROVIDE_HIDDEN(__preinit_array_start = .); + KEEP(*(.preinit_array*)) + PROVIDE_HIDDEN(__preinit_array_end = .); + } > ITCM + + .init_array : + { + PROVIDE_HIDDEN(__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array*)) + PROVIDE_HIDDEN(__init_array_end = .); + } > ITCM + + .fini_array : + { + PROVIDE_HIDDEN(__fini_array_start = .); + KEEP(*(SORT(.fini_array.*))) + KEEP(*(.fini_array*)) + PROVIDE_HIDDEN(__fini_array_end = .); + } > ITCM + + .itcm_probe : + { + . = MAX(., ORIGIN(ITCM) + 0x00010000); + . = ALIGN(32); + KEEP(*(.itcm_probe)) + KEEP(*(.itcm_probe.*)) + . = ALIGN(32); + } > ITCM + + .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.*) + *(.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 + + .stdout_capture (NOLOAD) : + { + . = ALIGN(32); + __stdout_capture_start__ = .; + KEEP(*(.stdout_capture)) + KEEP(*(.stdout_capture.*)) + . = ALIGN(32); + __stdout_capture_end__ = .; + } > AXISRAM + + .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(__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/make_argv_bin.py b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py new file mode 100644 index 0000000000..820d6a3643 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/make_argv_bin.py @@ -0,0 +1,38 @@ +#!/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 + +"""Build a standalone packed argv blob for debugger or fixture use.""" + +import os +import sys + +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: + usage = "Usage: make_argv_bin.py [arg1 ...]" + print(usage, 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:] + # 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) + 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/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..1c9bc4503b --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/argv_blob.py @@ -0,0 +1,38 @@ +# 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 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" + ) + 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 new file mode 100644 index 0000000000..2783868c87 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/flexmem.py @@ -0,0 +1,22 @@ +# 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/nucleo_host/gdb_script.py b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py new file mode 100644 index 0000000000..4a9e4e9783 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/gdb_script.py @@ -0,0 +1,149 @@ +# 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}", + # Keep the GDB script 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 += [ + "set $nucleo_stdout_truncated = *(unsigned int *)" + f"{stdout_capture_truncated_addr}", + "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 + + +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} " + f"{stdout_capture_addr} + $nucleo_stdout_len", + "end", + ] + + +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/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..515c961da9 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/openocd_tools.py @@ -0,0 +1,145 @@ +# 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" + + +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 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"adapter speed {speed}", + ] + if serial: + args += ["-c", f"adapter serial {serial}"] + return args + + +def runtime_gdbserver_cmd( + *, + openocd="openocd", + port=3333, + speed="8000", + 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, + 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, + "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_config none", + "reset run", + "shutdown", + ] 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..a19c27609a --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/results.py @@ -0,0 +1,159 @@ +# 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)" + 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/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..b80d729c78 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/nucleo_host/symbols.py @@ -0,0 +1,78 @@ +# 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/baremetal/platform/nucleo-n657x0-q/platform.mk b/test/baremetal/platform/nucleo-n657x0-q/platform.mk new file mode 100644 index 0000000000..0ad85f30bb --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/platform.mk @@ -0,0 +1,165 @@ +# 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 +BUILD_DIR ?= test/build + +CROSS_PREFIX=arm-none-eabi- +CC=gcc + +# Use the Cortex-M DWT cycle counter by default +CYCLES ?= CYCCNT + +# 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 \ + -Wno-pedantic \ + -Wno-redundant-decls \ + -Wno-missing-prototypes \ + -fno-common \ + -ffunction-sections \ + -fdata-sections \ + --sysroot=$(SYSROOT) \ + -DDEVICE=nucleo-n657x0-q \ + -DSTM32N657xx \ + -DARMCM55 \ + -DNTESTS_FUNC=1 \ + -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) + +SEMIHOST_SPECS := --specs=rdimon.specs + +CFLAGS += \ + $(ARCH_FLAGS) + +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. +# 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) +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 += \ + $(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 \ + $(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) + +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 + +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) + +$(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..d013afd8be --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/run_test_after_flexmem.py @@ -0,0 +1,22 @@ +#!/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:]) + + +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 new file mode 100644 index 0000000000..c96d3b6604 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/cmdline.c @@ -0,0 +1,112 @@ +/* + * 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 "main.h" +#include "semihosting_syscall.h" +#include "stm32n6xx.h" +#include "stm32n6xx_hal.h" +#include "stm32n6xx_it.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[]; +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[]); + +__attribute__((noreturn)) static void semihosting_exit_with_rc(int rc) +{ + if (rc == 0) + { + printf("[[MLKEM-EXIT:0]]\n"); + } + else + { + printf("[[MLKEM-EXIT:1]]\n"); + } + fflush(stdout); + SCB_CleanDCache(); + __DSB(); + __ISB(); + __BKPT(0); + while (1) + { + __WFI(); + } +} + +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. */ +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 new file mode 100644 index 0000000000..e509024a4c --- /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 +/* 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..4952cedcac --- /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..92ca4f9b99 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/flexmem_layout_check.c @@ -0,0 +1,46 @@ +/* + * 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 new file mode 100644 index 0000000000..45cf78b841 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/libfns.c @@ -0,0 +1,60 @@ +/* + * 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 + +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; + 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..117efec670 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.c @@ -0,0 +1,96 @@ +/* + * 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 + +#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]; + +__attribute__((used)) volatile uint32_t nucleo_stdout_capture_len; + +__attribute__((used)) volatile uint32_t nucleo_stdout_capture_truncated; + +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; + 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; + } +} + +#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 /* NUCLEO_USE_SEMIHOSTING_WRITE */ + +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) +{ + 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 new file mode 100644 index 0000000000..2655bd67ef --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/src/semihosting_syscall.h @@ -0,0 +1,12 @@ +/* + * 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 + +void nucleo_stdio_init(void); + +#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 new file mode 100644 index 0000000000..3177b06580 --- /dev/null +++ b/test/baremetal/platform/nucleo-n657x0-q/test_nucleo_host.py @@ -0,0 +1,635 @@ +# 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 +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.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 +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 uses absolute target string pointers.""" + blob = pack_cmdline(["prog", "--flag"], 0x20000000) + + argc, arg0, arg1 = struct.unpack_from(" +#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__) @@ -114,9 +145,14 @@ uint64_t get_cyclecounter(void) { return DWT->CYCCNT; } #elif defined(ARMCM55) /* Cortex-M55: Use dedicated PMU */ +#if defined(STM32N657xx) +#include +#include +#else #include +#include #include -#include "pmu_armv8.h" +#endif void enable_cyclecounter(void) { @@ -368,10 +404,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 */ diff --git a/test/mk/components.mk b/test/mk/components.mk index 4a3768c6c7..db6ad9f0a6 100644 --- a/test/mk/components.mk +++ b/test/mk/components.mk @@ -103,6 +103,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 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