diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 2e78d911b..b3682e809 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -12,3 +12,4 @@ self-hosted-runner: - pqcp-x64 # RISE RISC-V runner - ubuntu-24.04-riscv + - self-hosted-nucleo-n657x0 diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml index f28879790..fb8d02e70 100644 --- a/.github/workflows/all.yml +++ b/.github/workflows/all.yml @@ -13,98 +13,8 @@ on: types: [ "opened", "synchronize" ] jobs: - base: - name: Base + zephyr: + name: Zephyr permissions: contents: 'read' - id-token: 'write' - uses: ./.github/workflows/base.yml - secrets: inherit - lint-markdown: - name: Lint Markdown - permissions: - contents: 'read' - id-token: 'write' - uses: ./.github/workflows/lint_markdown.yml - nix: - name: Nix - permissions: - actions: 'write' - contents: 'read' - id-token: 'write' - uses: ./.github/workflows/nix.yml - secrets: inherit - riscv: - name: RISC-V - permissions: - contents: 'read' - id-token: 'write' - needs: [ base ] - uses: ./.github/workflows/riscv.yml - ci: - name: Extended - permissions: - contents: 'read' - id-token: 'write' - needs: [ base, nix ] - uses: ./.github/workflows/ci.yml - secrets: inherit - cbmc: - name: CBMC - permissions: - contents: 'read' - id-token: 'write' - pull-requests: 'write' - needs: [ base, nix ] - uses: ./.github/workflows/cbmc.yml - secrets: inherit - oqs_integration: - name: libOQS - permissions: - contents: 'read' - id-token: 'write' - needs: [ base ] - uses: ./.github/workflows/integration-liboqs.yml - secrets: inherit - pavona_integration: - name: Pavona - permissions: - contents: 'read' - id-token: 'write' - needs: [ base ] - uses: ./.github/workflows/integration-pavona.yml - secrets: inherit - awslc_integration: - name: AWS-LC - permissions: - contents: 'read' - id-token: 'write' - needs: [ base ] - uses: ./.github/workflows/integration-awslc.yml - with: - commit: v5.0.0 - secrets: inherit - ct-test: - name: Constant-time - permissions: - contents: 'read' - id-token: 'write' - needs: [ base, nix ] - uses: ./.github/workflows/ct-tests.yml - secrets: inherit - slothy: - name: SLOTHY - permissions: - contents: 'read' - id-token: 'write' - needs: [ base, nix ] - uses: ./.github/workflows/slothy.yml - secrets: inherit - baremetal: - name: Baremetal - permissions: - contents: 'read' - id-token: 'write' - needs: [ base ] - uses: ./.github/workflows/baremetal.yml - secrets: inherit + uses: ./.github/workflows/zephyr.yml diff --git a/.github/workflows/baremetal.yml b/.github/workflows/baremetal.yml index 5be619b38..2c83c7b7a 100644 --- a/.github/workflows/baremetal.yml +++ b/.github/workflows/baremetal.yml @@ -16,27 +16,6 @@ jobs: fail-fast: false matrix: target: - - runner: ubuntu-latest - name: 'M55-AN547' - makefile: test/baremetal/platform/m55-an547/platform.mk - nix-shell: cross-arm-embedded - func: true - kat: true - acvp: true - wycheproof: false - alloc: true - bench: true - opt: all - - runner: ubuntu-latest - name: 'M33-AN524' - makefile: test/baremetal/platform/m33-an524/platform.mk - nix-shell: cross-arm-embedded - func: true - kat: true - acvp: true - alloc: true - bench: true - opt: no_opt - runner: ubuntu-latest name: 'AVR ATmega128RFR2 (modified for 32K RAM)' makefile: test/baremetal/platform/avr/platform.mk diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index aee2718cb..246aed67d 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -36,6 +36,8 @@ jobs: ldflags: "-flto" bench_extra_args: "" nix_shell: bench + extra_makefile: "" + zephyr_target: "" - system: rpi5 name: Arm Cortex-A76 (Raspberry Pi 5) benchmarks bench_pmu: PERF @@ -45,6 +47,8 @@ jobs: bench_extra_args: "" nix_shell: bench cross_prefix: "" + extra_makefile: "" + zephyr_target: "" - system: a55 name: Arm Cortex-A55 (Snapdragon 888) benchmarks bench_pmu: PERF @@ -53,6 +57,8 @@ jobs: ldflags: "-flto -static" bench_extra_args: -w exec-on-a55 nix_shell: bench + extra_makefile: "" + zephyr_target: "" - system: bpi name: SpacemiT K1 8 (Banana Pi F3) benchmarks bench_pmu: PERF @@ -62,6 +68,8 @@ jobs: bench_extra_args: -w exec-on-bpi cross_prefix: riscv64-unknown-linux-gnu- nix_shell: cross-riscv64 + extra_makefile: "" + zephyr_target: "" - system: m1-mac-mini name: Mac Mini (M1, 2020) benchmarks bench_pmu: MAC @@ -70,6 +78,8 @@ jobs: ldflags: "-flto" bench_extra_args: "-r" nix_shell: bench + extra_makefile: "" + zephyr_target: "" - system: pqcp-ppc64 name: ppc64le (POWER10) benchmarks bench_pmu: PERF @@ -79,11 +89,27 @@ jobs: bench_extra_args: "-r" nix_shell: '' cross_prefix: "" + extra_makefile: "" + zephyr_target: "" + - system: nucleo-n657x0 + name: Arm Cortex-M55 (NUCLEO-N657X0-Q) benchmarks + bench_pmu: NO + archflags: "" + cflags: "" + ldflags: "" + bench_extra_args: "" + nix_shell: zephyr + cross_prefix: "" + extra_makefile: test/zephyr/platform.mk + zephyr_target: nucleo-n657x0-q if: github.repository_owner == 'pq-code-package' && !github.event.pull_request.head.repo.fork && (github.event.label.name == 'benchmark' || github.ref == 'refs/heads/main') runs-on: self-hosted-${{ matrix.target.system }} steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/bench + env: + EXTRA_MAKEFILE: ${{ matrix.target.extra_makefile }} + ZEPHYR_TARGET: ${{ matrix.target.zephyr_target }} with: name: ${{ matrix.target.name }} cflags: ${{ matrix.target.cflags }} diff --git a/.github/workflows/hol_light.yml b/.github/workflows/hol_light.yml index c381c287d..6294724c2 100644 --- a/.github/workflows/hol_light.yml +++ b/.github/workflows/hol_light.yml @@ -5,34 +5,7 @@ name: HOL-Light permissions: contents: read on: - push: - branches: ["main"] - paths: - - '.github/workflows/hol_light.yml' - - 'proofs/hol_light/aarch64/Makefile' - - 'proofs/hol_light/aarch64/**/*.S' - - 'proofs/hol_light/aarch64/**/*.ml' - - 'proofs/hol_light/x86_64/Makefile' - - 'proofs/hol_light/x86_64/**/*.S' - - 'proofs/hol_light/x86_64/**/*.ml' - - 'flake.nix' - - 'flake.lock' - - 'nix/hol_light/*' - - 'nix/s2n_bignum/*' - pull_request: - branches: ["main"] - paths: - - '.github/workflows/hol_light.yml' - - 'proofs/hol_light/aarch64/Makefile' - - 'proofs/hol_light/aarch64/**/*.S' - - 'proofs/hol_light/aarch64/**/*.ml' - - 'proofs/hol_light/x86_64/Makefile' - - 'proofs/hol_light/x86_64/**/*.S' - - 'proofs/hol_light/x86_64/**/*.ml' - - 'flake.nix' - - 'flake.lock' - - 'nix/hol_light/*' - - 'nix/s2n_bignum/*' + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/zephyr.yml b/.github/workflows/zephyr.yml new file mode 100644 index 000000000..7bc2951c9 --- /dev/null +++ b/.github/workflows/zephyr.yml @@ -0,0 +1,54 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +name: Zephyr +permissions: + contents: read +on: + workflow_call: + workflow_dispatch: + +jobs: + zephyr_tests: + name: Zephyr tests (${{ matrix.target.board }}, ${{ matrix.target.cpu }}) + strategy: + fail-fast: false + matrix: + target: + - { board: mps2-an385, cpu: Cortex-M3, opt: no_opt } + - { board: mps2-an386, cpu: Cortex-M4, opt: no_opt } + - { board: mps2-an500, cpu: Cortex-M7, opt: no_opt } + - { board: mps2-an521, cpu: Cortex-M33, opt: no_opt } + - { board: mps3-an547, cpu: Cortex-M55, opt: all } + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: zephyr build + test + uses: ./.github/actions/functest + env: + EXTRA_MAKEFILE: test/zephyr/platform.mk + ZEPHYR_TARGET: ${{ matrix.target.board }} + with: + nix-shell: zephyr + gh_token: ${{ secrets.GITHUB_TOKEN }} + opt: ${{ matrix.target.opt }} + func: true + kat: true + acvp: true + wycheproof: false + examples: false + unit: false + stack: false + alloc: false + rng_fail: false + check_namespace: false + # Smoke only: QEMU doesn't model useful cycle counts; Zephyr bench builds + # use k_cycle_get_32(), so no generic PMU/CYCCNT backend is selected. + - name: bench (smoke) + env: + EXTRA_MAKEFILE: test/zephyr/platform.mk + ZEPHYR_TARGET: ${{ matrix.target.board }} + run: | + opt=${{ matrix.target.opt == 'all' && 'opt' || 'no_opt' }} + nix develop .#zephyr --command ./scripts/tests bench -c NO --opt=$opt + nix develop .#zephyr --command ./scripts/tests bench --components -c NO --opt=$opt diff --git a/Makefile b/Makefile index 53c72053a..629af42ab 100644 --- a/Makefile +++ b/Makefile @@ -182,7 +182,7 @@ lib: $(BUILD_DIR)/libmlkem.a $(BUILD_DIR)/libmlkem512.a $(BUILD_DIR)/libmlkem768 # building benchmarking binaries check_defined = $(if $(value $1),, $(error $2)) check-defined-CYCLES: - @:$(call check_defined,CYCLES,CYCLES undefined. Benchmarking requires setting one of NO PMU PERF MAC) + @:$(call check_defined,CYCLES,CYCLES undefined. Benchmarking requires setting one of NO CYCCNT PMU PERF MAC) bench_512: check-defined-CYCLES \ $(MLKEM512_DIR)/bin/bench_mlkem512 diff --git a/flake.nix b/flake.nix index 1e6d3b3f3..e03470647 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,7 @@ export HOLLIGHT_LOAD_PATH="$IMPORTS_DIR:$S2N_BIGNUM_DIR''${HOLLIGHT_LOAD_PATH:+:$HOLLIGHT_LOAD_PATH}" export HOLDIR="$HOLLIGHT_DIR" ''; + in { _module.args.pkgs = import inputs.nixpkgs { @@ -94,6 +95,19 @@ } ++ holLightToolchain; }).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.openocd = util.openocd; + devShells.arm-embedded = util.mkShell { + packages = builtins.attrValues + { + inherit (config.packages) m55-an547; + inherit (pkgs) gcc-arm-embedded qemu coreutils python3 git; + }; + }; + + 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; } ++ holLightToolchain; @@ -148,15 +162,17 @@ ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isAarch64 [ config.packages.toolchain_x86_64 ]; }; - # arm-none-eabi-gcc + platform files from pqmx - devShells.cross-arm-embedded = util.mkShell { + # Zephyr build environment (board chosen at make time via EXTRA_MAKEFILE) + packages.zephyr = util.zephyr; + devShells.zephyr = util.mkShell { packages = builtins.attrValues { - inherit (util) pqmx; - inherit (config.packages) linters; - inherit (pkgs) gcc-arm-embedded qemu coreutils git; - }; + inherit (config.packages) openocd; + inherit (util) zephyr; + inherit (pkgs) gcc-arm-embedded qemu cmake ninja dtc gperf coreutils git; + } ++ [ util.zephyrPythonEnv ]; }; + devShells.cross-aarch64-embedded = util.mkShell { packages = builtins.attrValues { diff --git a/nix/openocd/default.nix b/nix/openocd/default.nix new file mode 100644 index 000000000..067930236 --- /dev/null +++ b/nix/openocd/default.nix @@ -0,0 +1,21 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +{ fetchFromGitHub +, openocd +, autoreconfHook +}: + +openocd.overrideAttrs (old: rec { + pname = "openocd"; + version = "unstable-2026-05-01"; + nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ autoreconfHook ]; + + src = fetchFromGitHub { + owner = "openocd-org"; + repo = "openocd"; + rev = "4e9b167e1ae5ccb437eb0538440988b3f0ec53cb"; + fetchSubmodules = true; + hash = "sha256-8aYl7JzulPxH6vgSeTKTMIZVH6d55JJlXTBkfgAPTbU="; + }; +}) diff --git a/nix/pqmx/default.nix b/nix/pqmx/default.nix deleted file mode 100644 index 7794c5c79..000000000 --- a/nix/pqmx/default.nix +++ /dev/null @@ -1,44 +0,0 @@ -# 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-pqmx"; - version = "main-2026-02-10"; - - - # Fetch platform files from pqmx - src = fetchFromGitHub { - owner = "slothy-optimizer"; - repo = "pqmx"; - rev = "904451a615dc7926eba07b4f3d1a4137c368bb4a"; - hash = "sha256-BjsToEWGlykKIKRfPom84BkD5RfetUKKwRAw3PecebU="; - }; - - dontBuild = true; - - installPhase = '' - mkdir -p $out/platform/m33-an524/src/platform/ - cp -r envs/m33-an524/src/platform/. $out/platform/m33-an524/src/platform/ - cp integration/*.c $out/platform/m33-an524/src/platform/ - - mkdir -p $out/platform/m55-an547/src/platform/ - cp -r envs/m55-an547/src/platform/. $out/platform/m55-an547/src/platform/ - cp integration/*.c $out/platform/m55-an547/src/platform/ - ''; - - setupHook = writeText "setup-hook.sh" '' - export M33_AN524_PATH="$1/platform/m33-an524/src/platform/" - export M55_AN547_PATH="$1/platform/m55-an547/src/platform/" - ''; - - meta = { - description = "Platform files from pqmx for baremetal targets"; - homepage = "https://github.com/slothy-optimizer/pqmx"; - }; -} diff --git a/nix/util.nix b/nix/util.nix index bbe0d8688..362160f91 100644 --- a/nix/util.nix +++ b/nix/util.nix @@ -107,7 +107,18 @@ rec { hol_server = pkgs.callPackage ./hol_light/hol_server.nix { inherit hol_light'; }; s2n_bignum = pkgs.callPackage ./s2n_bignum { }; slothy = pkgs.callPackage ./slothy { }; - pqmx = pkgs.callPackage ./pqmx { }; + zephyr = pkgs.callPackage ./zephyr { }; + zephyrPythonEnv = pkgs.python3.withPackages (ps: with ps; [ + pyelftools + pyyaml + packaging + pykwalify + jsonschema + anytree + intelhex + colorama + ]); + openocd = pkgs.callPackage ./openocd { }; avr-toolchain = pkgs.callPackage ./avr { }; # Helper function to build individual cross toolchains diff --git a/nix/zephyr/default.nix b/nix/zephyr/default.nix new file mode 100644 index 000000000..fd78e4301 --- /dev/null +++ b/nix/zephyr/default.nix @@ -0,0 +1,63 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +{ stdenvNoCC +, fetchFromGitHub +, gcc-arm-embedded +, writeText +}: + +# Board-agnostic Zephyr build environment: a pinned Zephyr tree plus the +# modules needed by the boards we target, exposed via a setup hook so a plain +# `cmake` build works with no west workspace. +let + zephyr = fetchFromGitHub { + owner = "zephyrproject-rtos"; + repo = "zephyr"; + rev = "v4.4.1"; + hash = "sha256-8bzykJs6fFGiofCxRKh8M9jdXr5R8FM0lAbA28yanGk="; + }; + + # Revision pinned by the Zephyr v4.4.1 manifest (west.yml). + cmsis_6 = fetchFromGitHub { + owner = "zephyrproject-rtos"; + repo = "CMSIS_6"; + rev = "30a859f44ef8ab4dc8f84b03ed586fd16ccf9d74"; + hash = "sha256-nTehISN0pu9gnOZMpGaBQ3DFmNxAqAZPGpvbKfEM35o="; + }; + + # Revision pinned by the Zephyr v4.4.1 manifest (west.yml). + hal_stm32 = fetchFromGitHub { + owner = "zephyrproject-rtos"; + repo = "hal_stm32"; + rev = "fc11896dd39cfca37bf9b4aeaaa2df8861b81875"; + hash = "sha256-AtNq2yTZsTFMTlWn/Ns0wuEiN4Wv/OTV2vWPRu0SnOE="; + }; +in +stdenvNoCC.mkDerivation { + pname = "mlkem-native-zephyr"; + version = "4.4.1"; + + dontUnpack = true; + + installPhase = '' + mkdir -p $out + ln -s ${zephyr} $out/zephyr + ln -s ${cmsis_6} $out/cmsis_6 + ln -s ${hal_stm32} $out/hal_stm32 + ''; + + setupHook = writeText "setup-hook.sh" '' + export ZEPHYR_BASE="$1/zephyr" + export ZEPHYR_CMSIS_6_MODULE="$1/cmsis_6" + export ZEPHYR_HAL_STM32_MODULE="$1/hal_stm32" + export ZEPHYR_MODULES="$1/cmsis_6;$1/hal_stm32" + export ZEPHYR_TOOLCHAIN_VARIANT=gnuarmemb + export GNUARMEMB_TOOLCHAIN_PATH=${gcc-arm-embedded} + ''; + + meta = { + description = "Pinned Zephyr tree and modules for the Zephyr-based test flows"; + homepage = "https://www.zephyrproject.org/"; + }; +} diff --git a/scripts/format b/scripts/format index b0cb919ab..0bb2f5ef6 100755 --- a/scripts/format +++ b/scripts/format @@ -69,7 +69,7 @@ info "Expanding tabs" expand-tabs() { git ls-files -- ":/" ":/!:Makefile" ":/!:**/Makefile" ":/!:**/Makefile.*" ":/!:Makefile.*" ":/!:*.mk" ":/!:*.patch" ":/!:*.S" ":/!:*.inc" ":/!:nix/valgrind/*.txt" | xargs -P "$nproc" -I {} sh -c ' - if [ ! -L {} ] && grep -Pq '"'"'\t'"'"' "{}"; then + if [ ! -L {} ] && grep -q "$(printf '"'"'\t'"'"')" "{}"; then tmp=$(mktemp) expand -t 4 "{}" > "$tmp" && mv "$tmp" "{}" echo "{}" diff --git a/scripts/lint b/scripts/lint index d85b9134a..8a3e4057c 100755 --- a/scripts/lint +++ b/scripts/lint @@ -269,8 +269,8 @@ gh_group_end check-tabs() { for file in $(git ls-files -- ":/" ":/!:Makefile" ":/!:**/Makefile" ":/!:**/Makefile.*" ":/!:Makefile.*" ":/!:*.mk" ":/!:*.patch" ":/!:*.S" ":/!:*.inc" ":/!:nix/valgrind/*.txt"); do - if [[ ! -L $file ]] && grep -Pq '\t' "$file"; then - l=$(grep -Pn '\t' "$file" | head -1 | cut -d: -f1) + if [[ ! -L $file ]] && grep -q $'\t' "$file"; then + l=$(grep -n $'\t' "$file" | head -1 | cut -d: -f1) echo "$file $l" fi done diff --git a/scripts/tests b/scripts/tests index 147c62f9a..487d1f232 100755 --- a/scripts/tests +++ b/scripts/tests @@ -757,8 +757,11 @@ class Tests: test_type, self.do_opt(), suppress_output=False ) - if resultss is None: + # Nothing below should write benchmark output unless the run produced + # results and all compile/run steps succeeded. + if resultss is None or len(self.failed) > 0: self.check_fail() + return # NOTE: There will only be one items in resultss, as we haven't yet decided how to write both opt/no-opt benchmark results for k, results in resultss.items(): @@ -1355,8 +1358,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/m33-an524/exec_wrapper.py b/test/baremetal/platform/m33-an524/exec_wrapper.py deleted file mode 100755 index 28493a539..000000000 --- a/test/baremetal/platform/m33-an524/exec_wrapper.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) The mldsa-native project authors -# Copyright (c) The mlkem-native project authors -# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT - -"""QEMU wrapper for executing Cortex-M33 bare-metal ELF binaries (mps3-an524).""" - -import struct as st -import sys -import subprocess -import tempfile -import os - - -def err(msg, **kwargs): - print(msg, file=sys.stderr, **kwargs) - - -binpath = sys.argv[1] -args = sys.argv[1:] - -# Memory layout: [argc] [offset1] [offset2] ... [string1\0] [string2\0] ... -# M33-AN524 RAM: 0x20000000-0x2001FFFF (128KB) -# Heap ends at: ~0x20000b20 -# Stack: 0x20008000-0x2001FFFF (96KB, grows downward) -# Use address after heap but before stack -# cmdline.c CMDLINE_ADDR must match this value -cmdline_offset = 0x20007000 -arg0_offset = cmdline_offset + 4 + len(args) * 4 -arg_offsets = [sum(map(len, args[:i])) + i + arg0_offset for i in range(len(args))] - -binargs = st.pack( - f"<{1 + len(args)}I" + "".join(f"{len(a) + 1}s" for a in args), - len(args), - *arg_offsets, - *map(lambda x: x.encode("utf-8"), args), -) - -with tempfile.NamedTemporaryFile(mode="wb", delete=False, suffix=".bin") as fd: - args_file = fd.name - fd.write(binargs) - -try: - qemu_cmd = f"qemu-system-arm -M mps3-an524 -cpu cortex-m33 -nographic -semihosting -kernel {binpath} -device loader,file={args_file},addr=0x{cmdline_offset:x}".split() - result = subprocess.run(qemu_cmd, encoding="utf-8", capture_output=True) -finally: - os.unlink(args_file) -if result.returncode != 0: - err("FAIL!") - err(f"{qemu_cmd} failed with error code {result.returncode}") - err(result.stderr) - exit(1) - -for line in result.stdout.splitlines(): - print(line) diff --git a/test/baremetal/platform/m33-an524/platform.mk b/test/baremetal/platform/m33-an524/platform.mk deleted file mode 100644 index 7c51c5a45..000000000 --- a/test/baremetal/platform/m33-an524/platform.mk +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) The mldsa-native project authors -# Copyright (c) The mlkem-native project authors -# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT - -PLATFORM_PATH:=test/baremetal/platform/m33-an524 - -CROSS_PREFIX=arm-none-eabi- -CC=gcc - -# Use PMU cycle counting by default -CYCLES ?= PMU - -# Reduce iterations for benchmarking and functional tests -CFLAGS += -DMLK_BENCHMARK_NTESTS=10 -DMLK_BENCHMARK_NITERATIONS=10 -DMLK_BENCHMARK_NWARMUP=10 -CFLAGS += -DNTESTS_FUNC=100 - - -CFLAGS += -DMLK_BUMP_ALLOC_SIZE=65536 -CFLAGS += \ - -O3 \ - -Wall -Wextra -Wshadow \ - -Wno-pedantic \ - -Wno-redundant-decls \ - -Wno-missing-prototypes \ - -Wno-conversion \ - -Wno-sign-conversion \ - -fno-common \ - -ffunction-sections \ - -fdata-sections \ - --sysroot=$(SYSROOT) \ - -DDEVICE=an524 \ - -I$(M33_AN524_PATH) \ - -I$(M33_AN524_PATH)/m-profile \ - -DARMCM33 \ - -DSEMIHOSTING \ - -DCMDLINE_BASE_ADDR=0x20007000 - -ARCH_FLAGS += \ - -mcpu=cortex-m33+nodsp \ - -mthumb \ - -mfloat-abi=soft - -CFLAGS += \ - $(ARCH_FLAGS) \ - --specs=nosys.specs - -CFLAGS += $(CFLAGS_EXTRA) - -LDSCRIPT = $(M33_AN524_PATH)/m33-an524.ld - -LDFLAGS += \ - -Wl,--gc-sections \ - -Wl,--no-warn-rwx-segments \ - -L. - -LDFLAGS += \ - --specs=nosys.specs \ - -Wl,--wrap=_open \ - -Wl,--wrap=_close \ - -Wl,--wrap=_read \ - -Wl,--wrap=_write \ - -Wl,--wrap=_fstat \ - -Wl,--wrap=_getpid \ - -Wl,--wrap=_isatty \ - -Wl,--wrap=_kill \ - -Wl,--wrap=_lseek \ - -Wl,--wrap=main \ - -ffreestanding \ - -T$(LDSCRIPT) \ - $(ARCH_FLAGS) - -# Extra sources to be included in test binaries -EXTRA_SOURCES = $(wildcard $(M33_AN524_PATH)/*.c) -# The CMSIS files fail compilation if conversion warnings are enabled -EXTRA_SOURCES_CFLAGS = -Wno-conversion -Wno-sign-conversion - -EXEC_WRAPPER := $(realpath $(PLATFORM_PATH)/exec_wrapper.py) diff --git a/test/baremetal/platform/m55-an547/exec_wrapper.py b/test/baremetal/platform/m55-an547/exec_wrapper.py deleted file mode 100755 index 820674b34..000000000 --- a/test/baremetal/platform/m55-an547/exec_wrapper.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) The mldsa-native project authors -# Copyright (c) The mlkem-native project authors -# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT - -import struct as st -import sys -import subprocess -import tempfile -import os - - -def err(msg, **kwargs): - print(msg, file=sys.stderr, **kwargs) - - -binpath = sys.argv[1] -args = sys.argv[1:] -cmdline_offset = 0x70000 - -arg0_offset = cmdline_offset + 4 + len(args) * 4 - -arg_offsets = [sum(map(len, args[:i])) + i + arg0_offset for i in range(len(args))] - -binargs = st.pack( - f"<{1 + len(args)}I" + "".join(f"{len(a) + 1}s" for a in args), - len(args), - *arg_offsets, - *map(lambda x: x.encode("utf-8"), args), -) - -with tempfile.NamedTemporaryFile(mode="wb", delete=False, suffix=".bin") as fd: - args_file = fd.name - fd.write(binargs) - -try: - qemu_cmd = f"qemu-system-arm -M mps3-an547 -monitor none -nographic -semihosting -kernel {binpath} -device loader,file={args_file},addr=0x{cmdline_offset:x}".split() - result = subprocess.run(qemu_cmd, encoding="utf-8", capture_output=True) -finally: - os.unlink(args_file) -if result.returncode != 0: - err("FAIL!") - err(f"{qemu_cmd} failed with error code {result.returncode}") - err(result.stderr) - exit(1) - -for line in result.stdout.splitlines(): - print(line) diff --git a/test/baremetal/platform/m55-an547/platform.mk b/test/baremetal/platform/m55-an547/platform.mk deleted file mode 100644 index 85985c907..000000000 --- a/test/baremetal/platform/m55-an547/platform.mk +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) The mldsa-native project authors -# Copyright (c) The mlkem-native project authors -# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT - -PLATFORM_PATH:=test/baremetal/platform/m55-an547 - -CROSS_PREFIX=arm-none-eabi- -CC=gcc - -# Use PMU cycle counting by default -CYCLES ?= PMU - -# Reduce iterations for benchmarking and functional tests -CFLAGS += -DMLK_BENCHMARK_NTESTS=10 -DMLK_BENCHMARK_NITERATIONS=10 -DMLK_BENCHMARK_NWARMUP=10 -CFLAGS += -DNTESTS_FUNC=100 - -# Explicitly include experimental Armv8.1-M + MVE backend -# Remove this once backend is finalized and enabled by default. -CFLAGS += "-DMLK_CONFIG_FIPS202_BACKEND_FILE=\"fips202/native/armv81m/mve.h\"" - -CFLAGS += \ - -O3 \ - -Wall -Wextra -Wshadow \ - -Wno-pedantic \ - -Wno-redundant-decls \ - -Wno-missing-prototypes \ - -Wno-conversion \ - -Wno-sign-conversion \ - -fno-common \ - -ffunction-sections \ - -fdata-sections \ - --sysroot=$(SYSROOT) \ - -DDEVICE=an547 \ - -I$(M55_AN547_PATH) \ - -DARMCM55 \ - -DSEMIHOSTING - -ARCH_FLAGS += \ - -march=armv8.1-m.main+mve.fp \ - -mcpu=cortex-m55 \ - -mthumb \ - -mfloat-abi=hard -mfpu=fpv4-sp-d16 - -CFLAGS += \ - $(ARCH_FLAGS) \ - --specs=nosys.specs - -CFLAGS += $(CFLAGS_EXTRA) - -LDSCRIPT = $(M55_AN547_PATH)/mps3.ld - -LDFLAGS += \ - -Wl,--gc-sections \ - -Wl,--no-warn-rwx-segments \ - -L. - -LDFLAGS += \ - --specs=nosys.specs \ - -Wl,--wrap=_open \ - -Wl,--wrap=_close \ - -Wl,--wrap=_read \ - -Wl,--wrap=_write \ - -Wl,--wrap=_fstat \ - -Wl,--wrap=_getpid \ - -Wl,--wrap=_isatty \ - -Wl,--wrap=_kill \ - -Wl,--wrap=_lseek \ - -Wl,--wrap=main \ - -ffreestanding \ - -T$(LDSCRIPT) \ - $(ARCH_FLAGS) - -# Extra sources to be included in test binaries -EXTRA_SOURCES = $(wildcard $(M55_AN547_PATH)/*.c) -# The CMSIS files fail compilation if conversion warnings are enabled -EXTRA_SOURCES_CFLAGS = -Wno-conversion -Wno-sign-conversion - -EXEC_WRAPPER := $(realpath $(PLATFORM_PATH)/exec_wrapper.py) diff --git a/test/hal/hal.c b/test/hal/hal.c index 0a1b9f23f..2cc1f1b71 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -42,7 +42,45 @@ #include "hal.h" -#if defined(PMU_CYCLES) +#if defined(__ZEPHYR__) + +/* Zephyr: use the kernel cycle counter. */ +#include + +void enable_cyclecounter(void) {} +void disable_cyclecounter(void) {} +uint64_t get_cyclecounter(void) { return k_cycle_get_32(); } + +#elif defined(CYCCNT_CYCLES) + +#if defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_8_1M_MAIN__) + +#if 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__) @@ -115,8 +153,8 @@ uint64_t get_cyclecounter(void) { return DWT->CYCCNT; } #elif defined(ARMCM55) /* Cortex-M55: Use dedicated PMU */ #include +#include #include -#include "pmu_armv8.h" void enable_cyclecounter(void) { @@ -368,10 +406,12 @@ uint64_t get_cyclecounter(void) return g_counters[2]; } -#else /* !PMU_CYCLES && !PERF_CYCLES && MAC_CYCLES */ +#else /* !__ZEPHYR__ && !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 /* !__ZEPHYR__ && !CYCCNT_CYCLES && !PMU_CYCLES && !PERF_CYCLES && \ + !MAC_CYCLES */ diff --git a/test/mk/config.mk b/test/mk/config.mk index df8708246..c8dee57d1 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 diff --git a/test/zephyr/app/CMakeLists.txt b/test/zephyr/app/CMakeLists.txt new file mode 100644 index 000000000..13bde1fe7 --- /dev/null +++ b/test/zephyr/app/CMakeLists.txt @@ -0,0 +1,75 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(mlkem_native_zephyr) + +# Parameters supplied via -D from platform.mk: +# ZEPHYR_NATIVE_ROOT - mlkem-native checkout the test is built from +# ZEPHYR_LEVEL - ML-KEM parameter set: 512 | 768 | 1024 +# ZEPHYR_TEST_SRC - test entrypoint, relative to ZEPHYR_NATIVE_ROOT +# ZEPHYR_TEST_DEFS - optional extra -D defines (space separated) +set(R ${ZEPHYR_NATIVE_ROOT}) + +target_sources(app PRIVATE + ${R}/mlkem/mlkem_native.c + ${R}/${ZEPHYR_TEST_SRC} + ${R}/test/notrandombytes/notrandombytes.c +) + +target_include_directories(app PRIVATE + ${R}/mlkem + ${R}/test/notrandombytes +) + +if(ZEPHYR_NUCLEO_N657X0_Q) + target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/shim_nucleo_n657x0_q.c) + target_compile_definitions(app PRIVATE + MLK_ZEPHYR_NUCLEO_N657X0_Q + printf=__wrap_printf + fprintf=__wrap_fprintf + puts=__wrap_puts + putchar=__wrap_putchar + fflush=__wrap_fflush + exit=__wrap_exit + ) + target_link_options(app PRIVATE + -Wl,--wrap=printf + -Wl,--wrap=fprintf + -Wl,--wrap=puts + -Wl,--wrap=putchar + -Wl,--wrap=fflush + -Wl,--wrap=exit + ) +else() + target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/shim.c) +endif() + +target_compile_definitions(app PRIVATE MLK_CONFIG_PARAMETER_SET=${ZEPHYR_LEVEL}) + +if(ZEPHYR_TEST_DEFS) + separate_arguments(_defs UNIX_COMMAND "${ZEPHYR_TEST_DEFS}") + target_compile_definitions(app PRIVATE ${_defs}) +endif() + +# Optional native FIPS202 backend (e.g. Armv8.1-M MVE on Cortex-M55). The +# monolithic mlkem_native.c already includes the backend C sources; its +# assembly counterpart comes from mlkem_native_asm.S. +if(ZEPHYR_FIPS202_BACKEND) + target_sources(app PRIVATE ${R}/mlkem/mlkem_native_asm.S) + target_compile_definitions(app PRIVATE + MLK_CONFIG_USE_NATIVE_BACKEND_FIPS202 + "MLK_CONFIG_FIPS202_BACKEND_FILE=\"${ZEPHYR_FIPS202_BACKEND}\"") +endif() + +# The bench tests need the HAL wrapper; under Zephyr it reads k_cycle_get_32(). +if(ZEPHYR_TEST_HAL) + target_sources(app PRIVATE ${R}/test/hal/hal.c) + target_include_directories(app PRIVATE ${R}/test/hal) +endif() + +# Each test brings its own int main(void); rename it so the Zephyr shim +# (shim.c) owns main() and can stop QEMU with the test's exit code. +set_source_files_properties(${R}/${ZEPHYR_TEST_SRC} + PROPERTIES COMPILE_DEFINITIONS "main=mlk_test_main") diff --git a/test/zephyr/app/Kconfig b/test/zephyr/app/Kconfig new file mode 100644 index 000000000..04396300b --- /dev/null +++ b/test/zephyr/app/Kconfig @@ -0,0 +1,14 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +# Corstone-300's Zephyr SoC does not advertise MVE, but the Cortex-M55 it +# models (and QEMU's mps3-an547) supports it. Force-select the extension so the +# Armv8.1-M MVE FIPS202 backend can be built. Enabled per target by platform.mk. +config FIPS202_MVE_BACKEND + bool "Build with the Armv8.1-M MVE FIPS202 backend" + select FPU + select ARMV8_M_DSP + select ARMV8_1_M_MVEI + select ARMV8_1_M_MVEF + +source "Kconfig.zephyr" diff --git a/test/zephyr/app/nucleo_n657x0_q.conf b/test/zephyr/app/nucleo_n657x0_q.conf new file mode 100644 index 000000000..79cb204e9 --- /dev/null +++ b/test/zephyr/app/nucleo_n657x0_q.conf @@ -0,0 +1,15 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +# The FLEXMEM setup used by the NUCLEO runner repurposes the low AXISRAM +# window, so keep the Zephyr image above it and leave fixed host buffers after +# the linked image. +CONFIG_SRAM_BASE_ADDRESS=0x34080000 +CONFIG_SRAM_SIZE=192 + +# Component benchmarks keep several 8 KiB scratch buffers on the main stack. +CONFIG_MAIN_STACK_SIZE=65536 + +# Test stdout is wrapped by shim_nucleo_n657x0_q.c and emitted on ITM/SWO. +CONFIG_STDOUT_CONSOLE=n +CONFIG_UART_CONSOLE=n diff --git a/test/zephyr/app/nucleo_n657x0_q.overlay b/test/zephyr/app/nucleo_n657x0_q.overlay new file mode 100644 index 000000000..6ea5aa353 --- /dev/null +++ b/test/zephyr/app/nucleo_n657x0_q.overlay @@ -0,0 +1,11 @@ +/ { + mlk_axisram: memory@34080000 { + compatible = "zephyr,memory-region", "mmio-sram"; + reg = <0x34080000 0x00030000>; + zephyr,memory-region = "MLK_AXISRAM"; + }; + + chosen { + zephyr,sram = &mlk_axisram; + }; +}; diff --git a/test/zephyr/app/prj.conf b/test/zephyr/app/prj.conf new file mode 100644 index 000000000..0e42e22a6 --- /dev/null +++ b/test/zephyr/app/prj.conf @@ -0,0 +1,4 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT +CONFIG_MAIN_STACK_SIZE=32768 +CONFIG_BOOT_BANNER=n diff --git a/test/zephyr/app/shim.c b/test/zephyr/app/shim.c new file mode 100644 index 000000000..1860c4adc --- /dev/null +++ b/test/zephyr/app/shim.c @@ -0,0 +1,96 @@ +/* + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +/* + * Zephyr entrypoint shim for the mlkem-native tests. + * + * Each test provides its own main(), which we rename to mlk_test_main + * (-Dmain=mlk_test_main on that source) and call from the Zephyr main thread + * below. Command-line arguments (used by the acvp/wycheproof tests) are read + * from the host via Arm semihosting SYS_GET_CMDLINE; func/kat take main(void) + * and ignore them. When the test returns we stop QEMU via a semihosting exit + * carrying its return code, so the host Makefile sees the pass/fail status. + */ + +#include + +extern int mlk_test_main(int argc, char **argv); + +/* Arm semihosting operations, trapped by QEMU via `bkpt 0xab`. */ +#define SYS_GET_CMDLINE 0x15 +#define SYS_EXIT_EXTENDED 0x20 +#define ADP_STOPPED_APPLICATION_EXIT 0x20026UL + +#define MAX_ARGS 32 +#define CMDLINE_BUF_SIZE 65536 + +static long semihost(unsigned op, void *arg) +{ + register unsigned r0 __asm__("r0") = op; + register void *r1 __asm__("r1") = arg; + __asm__ volatile("bkpt 0xab" : "+r"(r0) : "r"(r1) : "memory"); + return (long)r0; +} + +static char cmdline[CMDLINE_BUF_SIZE]; +static char *argv_buf[MAX_ARGS + 1]; + +/* Fetch the host command line via semihosting and split it into argv. */ +static int get_args(char ***argv_out) +{ + struct + { + char *buf; + size_t len; + } block = {cmdline, sizeof(cmdline) - 1}; + int argc = 0; + char *p = cmdline; + + *argv_out = argv_buf; + if (semihost(SYS_GET_CMDLINE, &block) != 0) + { + argv_buf[0] = "test"; + argv_buf[1] = NULL; + return 1; + } + + while (*p != '\0' && argc < MAX_ARGS) + { + while (*p == ' ' || *p == '\t') + { + p++; + } + if (*p == '\0') + { + break; + } + argv_buf[argc++] = p; + while (*p != '\0' && *p != ' ' && *p != '\t') + { + p++; + } + if (*p != '\0') + { + *p++ = '\0'; + } + } + argv_buf[argc] = NULL; + return argc; +} + +static void semihost_exit(int code) +{ + unsigned long block[2] = {ADP_STOPPED_APPLICATION_EXIT, (unsigned long)code}; + semihost(SYS_EXIT_EXTENDED, block); +} + +int main(void) +{ + char **argv; + int argc = get_args(&argv); + int rc = mlk_test_main(argc, argv); + semihost_exit(rc); + return rc; +} diff --git a/test/zephyr/app/shim_nucleo_n657x0_q.c b/test/zephyr/app/shim_nucleo_n657x0_q.c new file mode 100644 index 000000000..4a696f6e2 --- /dev/null +++ b/test/zephyr/app/shim_nucleo_n657x0_q.c @@ -0,0 +1,235 @@ +/* + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +/* + * Zephyr entrypoint shim for RAM-loaded NUCLEO-N657X0-Q tests. + * + * The OpenOCD/GDB wrapper breaks at __wrap_main after Zephyr has cleared BSS, + * restores a packed argv block into mlk_cmdline_block, then continues here. + * Target stdout/stderr goes through ITM stimulus port 0 and is captured by the + * host over SWO. The test return code is passed to nucleo_test_done(), where + * the host wrapper reads r0. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern int mlk_test_main(int argc, char **argv); + +typedef struct cmdline_s +{ + int argc; + char *argv[]; +} cmdline_t; + +#define CMDLINE_BLOCK_SIZE (64U * 1024U) +#define NUCLEO_ITM_LAR (*(volatile uint32_t *)0xE0000FB0UL) + +__asm__( + ".global mlk_cmdline_block\n" + ".set mlk_cmdline_block, 0x340b0000\n"); + +extern unsigned char mlk_cmdline_block[CMDLINE_BLOCK_SIZE]; + +static char nucleo_stdout_buf[8192]; +static size_t nucleo_stdout_len; +static bool nucleo_swo_enabled; + +static void nucleo_swo_pin_setup(void) +{ + LL_AHB4_GRP1_EnableClock(LL_AHB4_GRP1_PERIPH_GPIOB); + LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_5, LL_GPIO_MODE_ALTERNATE); + LL_GPIO_SetPinSpeed(GPIOB, LL_GPIO_PIN_5, LL_GPIO_SPEED_FREQ_VERY_HIGH); + LL_GPIO_SetPinPull(GPIOB, LL_GPIO_PIN_5, LL_GPIO_PULL_NO); + LL_GPIO_SetAFPin_0_7(GPIOB, LL_GPIO_PIN_5, LL_GPIO_AF_0); +} + +static void nucleo_swo_enable(void) +{ + if (nucleo_swo_enabled) + { + return; + } + + nucleo_swo_pin_setup(); + LL_BUS_EnableClock(LL_APB3); + LL_DBGMCU_EnableDBGClock(); + LL_DBGMCU_EnableTPIUExportClock(); + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + + NUCLEO_ITM_LAR = 0xC5ACCE55UL; + ITM->TER = 0UL; + ITM->TCR &= ~ITM_TCR_ITMENA_Msk; + while ((ITM->TCR & ITM_TCR_BUSY_Msk) != 0UL) + { + } + ITM->TPR = 0UL; + ITM->TCR = ITM_TCR_ITMENA_Msk | ITM_TCR_DWTENA_Msk | + (1UL << ITM_TCR_TRACEBUSID_Pos); + ITM->TER = 1UL; + nucleo_swo_enabled = true; +} + +static void nucleo_swo_write_byte(char ch) +{ + nucleo_swo_enable(); + + if (((ITM->TCR & ITM_TCR_ITMENA_Msk) == 0UL) || ((ITM->TER & 1UL) == 0UL)) + { + return; + } + + while (ITM->PORT[0U].u32 == 0UL) + { + } + ITM->PORT[0U].u8 = (uint8_t)ch; +} + +static void nucleo_swo_write_raw(const char *src, size_t len) +{ + for (size_t offset = 0; offset < len; offset++) + { + nucleo_swo_write_byte(src[offset]); + } +} + +static void nucleo_swo_wait_idle(void) +{ + for (uint32_t timeout = 10000000UL; timeout > 0U; timeout--) + { + if ((ITM->TCR & ITM_TCR_BUSY_Msk) == 0UL) + { + break; + } + } + __DSB(); +} + +__attribute__((noinline, used)) void nucleo_test_breakpoint(int rc) +{ + (void)rc; + __asm__ volatile("bkpt 0" ::: "memory"); +} + +static void nucleo_stdout_flush(void) +{ + if (nucleo_stdout_len == 0U) + { + return; + } + + nucleo_swo_write_raw(nucleo_stdout_buf, nucleo_stdout_len); + nucleo_stdout_len = 0U; +} + +static void nucleo_stdout_write(const char *src, size_t len) +{ + for (size_t i = 0; i < len; i++) + { + nucleo_stdout_buf[nucleo_stdout_len++] = src[i]; + if (src[i] == '\n' || nucleo_stdout_len == sizeof(nucleo_stdout_buf)) + { + nucleo_stdout_flush(); + } + } +} + +static int nucleo_vprintf(const char *format, va_list ap) +{ + char buf[8192]; + int rc = vsnprintf(buf, sizeof(buf), format, ap); + + if (rc <= 0) + { + return rc; + } + + nucleo_stdout_write(buf, + (size_t)((rc < (int)sizeof(buf)) ? rc : (int)sizeof(buf) - 1)); + return rc; +} + +int __wrap_printf(const char *format, ...) +{ + va_list ap; + int rc; + + va_start(ap, format); + rc = nucleo_vprintf(format, ap); + va_end(ap); + return rc; +} + +int __wrap_fprintf(FILE *stream, const char *format, ...) +{ + va_list ap; + int rc; + + (void)stream; + va_start(ap, format); + rc = nucleo_vprintf(format, ap); + va_end(ap); + return rc; +} + +int __wrap_puts(const char *s) +{ + size_t len = strlen(s); + + nucleo_stdout_write(s, len); + nucleo_stdout_write("\n", 1); + return (int)len + 1; +} + +int __wrap_putchar(int c) +{ + char ch = (char)c; + + nucleo_stdout_write(&ch, 1); + return c; +} + +int __wrap_fflush(FILE *stream) +{ + (void)stream; + nucleo_stdout_flush(); + return 0; +} + +__attribute__((noreturn, noinline, used)) void nucleo_test_done(int rc) +{ + nucleo_stdout_flush(); + nucleo_swo_wait_idle(); + nucleo_test_breakpoint(rc); + for (;;) + { + k_sleep(K_FOREVER); + } +} + +void __wrap_exit(int status) { nucleo_test_done(status); } + +__attribute__((noinline, used)) void __wrap_main(void) +{ + cmdline_t *cmdline = (cmdline_t *)mlk_cmdline_block; + int rc = mlk_test_main(cmdline->argc, cmdline->argv); + + nucleo_test_done(rc); +} + +int main(void) +{ + __wrap_main(); + return 0; +} diff --git a/test/zephyr/exec_wrapper.py b/test/zephyr/exec_wrapper.py new file mode 100755 index 000000000..539398768 --- /dev/null +++ b/test/zephyr/exec_wrapper.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +# Runs a Zephyr ELF for an Arm MPS board under QEMU. The QEMU machine is taken +# from QEMU_MACHINE (set per target in platform.mk). Any extra arguments +# (used by the acvp/wycheproof tests) are handed to the guest via semihosting +# SYS_GET_CMDLINE (see app/shim.c); the application console (UART) is routed to +# stdout via -nographic; and the test stops the machine through a semihosting +# exit, so QEMU's return code reflects the test's pass/fail status. + +import os +import sys +import subprocess + +binpath = sys.argv[1] +args = sys.argv[2:] +machine = os.environ.get("QEMU_MACHINE", "mps3-an547") + +semihosting_args = [binpath] + args +semihosting_config = "enable=on," + ",".join(f"arg={a}" for a in semihosting_args) + +qemu_cmd = [ + "qemu-system-arm", + "-M", + machine, + "-monitor", + "none", + "-nographic", + "-semihosting-config", + semihosting_config, + "-kernel", + binpath, +] + +result = subprocess.run(qemu_cmd, encoding="utf-8", capture_output=True, timeout=300) +sys.stdout.write(result.stdout) +if result.returncode != 0: + sys.stderr.write(result.stderr) +sys.exit(result.returncode) diff --git a/test/zephyr/nucleo_n657x0_q/README.md b/test/zephyr/nucleo_n657x0_q/README.md new file mode 100644 index 000000000..5f379bb4c --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/README.md @@ -0,0 +1,86 @@ + + +# NUCLEO-N657X0-Q Zephyr Hardware Helpers + +This directory contains the NUCLEO-N657X0-Q Zephyr hardware entry point and +supporting host/debug helpers. The board is not flashed: OpenOCD first expands +FLEXMEM with direct register writes, then GDB loads the Zephyr ELF into RAM. + +Only two target-specific behaviors remain here: + +- FLEXMEM expansion for the STM32N657X0 ITCM/DTCM layout. +- GDB argv insertion after Zephyr startup reaches `__wrap_main`. + +Test output uses ITM stimulus port 0 and OpenOCD SWO capture. The host wrapper +decodes that SWO stream and keeps OpenOCD/GDB control chatter out of stdout +unless `--verbose` is passed. + +## Files + +- `exec_wrapper.py`: configures FLEXMEM, starts OpenOCD with SWO capture + enabled, loads a Zephyr ELF with GDB, restores the packed argv blob, decodes + ITM port 0 output, and reads the target return code at `nucleo_test_done`. +- `nucleo_host/`: Python helpers for FLEXMEM configuration, argv packing, + OpenOCD command generation, GDB script generation, symbol lookup, fault + diagnostics. + +The Zephyr application shim lives in `test/zephyr/app/shim_nucleo_n657x0_q.c`. + +## Run + +Use the Zephyr platform makefile and select the NUCLEO target: + +```sh +nix develop .#zephyr +make run_func_512 EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q +``` + +The execution wrapper configures FLEXMEM before each ELF run. The supported +Zephyr hardware run targets are: + +```text +run_kat run_func run_acvp run_wycheproof +``` + +The top-level `make test` target also includes generic unit, alloc, and RNG +failure tests; those are not wired as NUCLEO Zephyr applications. + +Benchmark targets remain explicit and require the usual `CYCLES` setting: + +```sh +make run_bench_512 CYCLES=NO EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q +make run_bench_components_512 CYCLES=NO EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q +``` + +Useful environment variables: + +```sh +export OPENOCD_SPEED=8000 +export OPENOCD_SERIAL= +export GDB_PORT=3333 +export SWO_TRACECLK=100000000 +export SWO_PIN_FREQ=1000000 +``` + +`OPENOCD`, `OPENOCD_INTERFACE`, `OPENOCD_TARGET`, `OPENOCD_TRANSPORT`, `GDB`, +`NM`, and `READELF` can override the default tools and OpenOCD scripts. + +## FLEXMEM Sequence + +STM32N657X0 starts with a smaller reset-time TCM layout. The NUCLEO runner +expands both ITCM and DTCM before each RAM-loaded Zephyr test: + +1. OpenOCD attaches with `reset_config none`. +2. The script enables the SYSCFG clock by setting bit 0 in `RCC_APB4ENSR2` at + `0x56028a78`. +3. It read-modify-writes `SYSCFG_CM55TCMCR` at `0x56008008` so the low byte is + `0x99`. +4. It sets bit 0 in `SYSCFG_CM55RSTCR` at `0x56008018`. +5. It polls until `(SYSCFG_CM55TCMCR & 0xff) == 0x99`. +6. It runs `reset run` so the expanded layout is applied before the Zephyr ELF + is loaded. diff --git a/test/zephyr/nucleo_n657x0_q/exec_wrapper.py b/test/zephyr/nucleo_n657x0_q/exec_wrapper.py new file mode 100755 index 000000000..473a9c092 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/exec_wrapper.py @@ -0,0 +1,491 @@ +#!/usr/bin/env python3 +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +""" +Run one RAM-resident NUCLEO-N657X0-Q Zephyr test ELF through OpenOCD. + +The wrapper first expands ITCM/DTCM with the direct FLEXMEM OpenOCD script. It +then starts OpenOCD with SWO trace redirected to a local TCP listener, +GDB-loads the Zephyr ELF into RAM, restores a packed argv blob after Zephyr +startup reaches ``__wrap_main``, decodes target stdout from ITM port 0, and +reads the target return code from ``r0`` at the ``nucleo_test_done`` +breakpoint. +""" + +import logging +import os +import re +import select +import socket +import subprocess +import sys +import tempfile +import time + +from nucleo_host.argv_blob import pack_cmdline +from nucleo_host.flexmem_configure import run_openocd_config as run_flexmem_config +from nucleo_host.gdb_script import build_run_script +from nucleo_host.openocd_tools import find_openocd +from nucleo_host.openocd_tools import runtime_gdbserver_cmd +from nucleo_host.openocd_tools import serial_from_env +from nucleo_host.openocd_tools import speed_khz_from_env +from nucleo_host.openocd_tools import swo_formatter_from_env +from nucleo_host.openocd_tools import swo_pin_freq_from_env +from nucleo_host.openocd_tools import swo_traceclk_from_env +from nucleo_host.openocd_tools import transport_from_env +from nucleo_host.results import fault_info_from_gdb +from nucleo_host.results import gdb_observed_hardfault +from nucleo_host.symbols import default_readelf +from nucleo_host.symbols import resolve_symbol + +VERBOSE = False +LOG = logging.getLogger(__name__) + + +def configure_logging(): + """Configure process-wide logging after ``VERBOSE`` has been parsed.""" + level = logging.DEBUG if VERBOSE else logging.INFO + logging.basicConfig(level=level, format="%(message)s") + + +def log_output(output, level=logging.INFO, prefix=None): + """Log multiline subprocess output one line at a time.""" + if not output: + return + for line in str(output).rstrip().splitlines(): + if prefix: + line = f"{prefix}{line}" + LOG.log(level, "%s", line) + + +def err(msg): + """Report an error message regardless of verbose mode.""" + log_output(msg, logging.ERROR) + + +def info(msg): + """Report an informational message only in verbose mode.""" + if VERBOSE: + LOG.debug("%s", msg) + + +def popen(cmd, **kwargs): + """Wrap ``subprocess.Popen`` for test-time monkeypatching.""" + return subprocess.Popen(cmd, **kwargs) + + +def _reserve_localhost_port(): + """Return an available localhost TCP port for OpenOCD to listen on.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def _emit_target_bytes(data: bytes): + """Forward decoded target stdout/stderr bytes to host stdout.""" + if not data: + return + out = getattr(sys.stdout, "buffer", None) + if out is not None: + out.write(data) + out.flush() + else: + sys.stdout.write(data.decode("utf-8", errors="replace")) + sys.stdout.flush() + + +def _decode_swo_itm(state, data: bytes): + """Extract software stimulus port 0 bytes from raw ITM trace packets.""" + out = bytearray() + remaining = state.get("itm_remaining", 0) + emit = state.get("itm_emit", False) + + for byte in data: + if remaining: + if emit: + out.append(byte) + remaining -= 1 + continue + + size_code = byte & 0x03 + if size_code == 0: + continue + + remaining = 4 if size_code == 3 else size_code + port = (byte >> 3) & 0x1F + is_software_packet = (byte & 0x04) == 0 + emit = is_software_packet and port == 0 + + state["itm_remaining"] = remaining + state["itm_emit"] = emit + return bytes(out) + + +def _drain_swo(state): + """Connect to and drain OpenOCD SWO TCP output without blocking.""" + conn = state.get("conn") + if conn is None: + try: + conn = socket.create_connection(("127.0.0.1", state["port"]), timeout=0.01) + conn.setblocking(False) + state["conn"] = conn + except OSError: + return + + conn = state.get("conn") + if conn is None: + return + + while True: + try: + readable, _, _ = select.select([conn], [], [], 0) + if not readable: + return + data = conn.recv(4096) + except OSError: + state["conn"] = None + return + if not data: + try: + conn.close() + except OSError: + pass + state["conn"] = None + return + decoded = _decode_swo_itm(state, data) + state["swo_raw_bytes"] = state.get("swo_raw_bytes", 0) + len(data) + _emit_target_bytes(decoded) + + +def _wait_swo_connected(state, timeout_s=3.0): + """Connect to OpenOCD's SWO TCP listener before target execution.""" + deadline = time.time() + timeout_s + while time.time() < deadline: + _drain_swo(state) + if state.get("conn") is not None: + return True + time.sleep(0.05) + return False + + +def _close_swo_state(state): + """Close SWO connection handles.""" + conn = state.get("conn") + if conn is not None: + try: + conn.close() + except OSError: + pass + + +def _drain_swo_until_idle(state, idle_s=0.5, timeout_s=5.0): + """Drain delayed SWO bytes until the stream is briefly idle.""" + deadline = time.time() + timeout_s + idle_deadline = time.time() + idle_s + last_raw_bytes = state.get("swo_raw_bytes", 0) + + while time.time() < deadline: + _drain_swo(state) + raw_bytes = state.get("swo_raw_bytes", 0) + if raw_bytes != last_raw_bytes: + last_raw_bytes = raw_bytes + idle_deadline = time.time() + idle_s + elif time.time() >= idle_deadline: + break + time.sleep(0.01) + + +def _parse_exit_code(gdb_text: str): + """Return the target exit code printed by the GDB script, if present.""" + match = re.search(r"^NUCLEO_EXIT_CODE=(-?\d+)$", gdb_text, re.MULTILINE) + if match: + return int(match.group(1)) + return None + + +def _run_once(): + """Run the target ELF once and return its wrapper exit code.""" + global VERBOSE + + argv = sys.argv[1:] + if "--verbose" in argv: + VERBOSE = True + argv.remove("--verbose") + if "-v" in argv: + VERBOSE = True + argv.remove("-v") + + configure_logging() + + if len(argv) < 1: + err("Usage: exec_wrapper.py [--verbose] [args...]") + return 2 + + elf = os.path.abspath(argv[0]) + args = argv # Preserve the existing convention: argv[0] is the ELF path. + + if not os.path.exists(elf): + err(f"ELF not found: {elf}") + return 2 + + gdb = os.environ.get("GDB", "arm-none-eabi-gdb") + nm = os.environ.get("NM", "arm-none-eabi-nm") + readelf = os.environ.get("READELF", default_readelf()) + port = int(os.environ.get("GDB_PORT", "3333")) + gdb_run_timeout = float(os.environ.get("GDB_RUN_TIMEOUT", "180")) + + arg_block_sym = "mlkem_cmdline_block" + arg_block_addr = None + + def _resolve_symbol_addr(elf_path: str, sym: str): + return resolve_symbol(elf_path, sym, nm=nm, readelf=readelf) + + def _resolve_first_symbol(elf_path: str, symbols): + for sym in symbols: + addr = _resolve_symbol_addr(elf_path, sym) + if addr is not None: + return sym, addr + return symbols[0], None + + for cand in (arg_block_sym, "mlk_cmdline_block"): + addr = _resolve_symbol_addr(elf, cand) + if addr is not None: + arg_block_sym = cand + arg_block_addr = addr + break + + wrap_main_sym, wrap_main_addr = _resolve_first_symbol(elf, ["__wrap_main"]) + wrap_main_break = wrap_main_sym + if wrap_main_addr is not None: + wrap_main_break = f"*{wrap_main_addr}" + + done_sym, done_addr = _resolve_first_symbol( + elf, ["nucleo_test_breakpoint", "nucleo_test_done"] + ) + done_break = done_sym + if done_addr is not None: + done_break = f"*{done_addr}" + + hardfault_sym, hardfault_addr = _resolve_first_symbol( + elf, ["HardFault_Handler", "z_arm_hard_fault"] + ) + hardfault_break = hardfault_sym + if hardfault_addr is not None: + hardfault_break = f"*{hardfault_addr}" + + reset_handler_sym, reset_handler_addr = _resolve_first_symbol( + elf, ["Reset_Handler", "z_arm_reset"] + ) + reset_handler_jump = reset_handler_sym + if reset_handler_addr is not None: + reset_handler_jump = f"*{hex(int(reset_handler_addr, 16) | 1)}" + if reset_handler_addr is None: + err("Failed to resolve Reset_Handler/z_arm_reset in ELF.") + return 2 + + base_addr = None + if arg_block_addr: + try: + base_addr = int(arg_block_addr, 16) + except ValueError: + base_addr = None + + if base_addr is None: + err( + "Failed to resolve base address of argv block " + "(mlkem_cmdline_block/mlk_cmdline_block)." + ) + err("- Ensure symbols are present in ELF.") + 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) + + flexmem_timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "5")) + info("[exec_wrapper] configuring FLEXMEM...") + flexmem_rc = run_flexmem_config(flexmem_timeout_s) + if flexmem_rc != 0: + return flexmem_rc + + swo_port = _reserve_localhost_port() + swo_state = {"conn": None, "port": swo_port} + openocd = find_openocd(os.environ.get("OPENOCD", "")) + if openocd is None: + _close_swo_state(swo_state) + err("OpenOCD not found; set OPENOCD or ensure openocd is on PATH") + return 2 + gdbserver_cmd = runtime_gdbserver_cmd( + openocd=openocd, + port=port, + swo_port=swo_port, + swo_traceclk=swo_traceclk_from_env(), + swo_pin_freq=swo_pin_freq_from_env(), + swo_formatter=swo_formatter_from_env(), + speed=speed_khz_from_env(), + serial=serial_from_env(), + transport=transport_from_env(), + ) + + info(f"[exec_wrapper] starting OpenOCD on port {port}...") + info(f"[exec_wrapper] SWO redirect port {swo_port}") + info(f"[exec_wrapper] {' '.join(gdbserver_cmd)}") + stp = popen( + gdbserver_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + ) + + try: + time.sleep(0.8) + + if stp.poll() is not None: + out_rem = stp.stdout.read() if stp.stdout else "" + if out_rem: + log_output(out_rem, logging.DEBUG if VERBOSE else logging.ERROR) + return 2 + if not _wait_swo_connected(swo_state): + err("FAIL!") + err("failed to connect to OpenOCD SWO TCP listener") + return 2 + + gdb_lines = build_run_script( + port=port, + wrap_main_break=wrap_main_break, + reset_handler_jump=reset_handler_jump, + hardfault_break=hardfault_break, + done_break=done_break, + argv_bin=argv_bin, + arg_block_addr=arg_block_addr, + arg_block_sym=arg_block_sym, + ) + + if VERBOSE: + LOG.debug("============ GDB SCRIPT ============") + log_output("\n".join(gdb_lines), logging.DEBUG) + LOG.debug("====================================") + + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".gdb") as gs: + for line in gdb_lines: + gs.write(line + "\n") + gdb_script_path = gs.name + + gdb_cmd = [gdb, "--batch", "-x", gdb_script_path, elf] + + info("[exec_wrapper] running gdb batch") + gdbp = popen( + gdb_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + gdb_deadline = ( + time.time() + gdb_run_timeout if gdb_run_timeout > 0 else None + ) + + while True: + _drain_swo(swo_state) + if stp.stdout is not None: + try: + r, _, _ = select.select([stp.stdout], [], [], 0.1) + if r: + line = stp.stdout.readline() + if line and VERBOSE: + log_output(line, logging.DEBUG) + except Exception: + pass + if gdbp.poll() is not None: + break + if gdb_deadline is not None and time.time() > gdb_deadline: + err("FAIL!") + err(f"gdb batch timed out after {gdb_run_timeout:.0f}s") + try: + gdbp.terminate() + gdbp.wait(timeout=1.0) + except Exception: + try: + gdbp.kill() + except Exception: + pass + try: + out, errout = gdbp.communicate(timeout=1.0) + if out: + log_output(out, logging.ERROR) + if errout: + log_output(errout, logging.ERROR) + except Exception: + pass + return 124 + + out, errout = gdbp.communicate() + _drain_swo_until_idle(swo_state) + if out and VERBOSE: + log_output(out, logging.DEBUG) + if errout and VERBOSE: + log_output(errout, logging.DEBUG) + + gdb_text = f"{out}\n{errout}" + exit_code = _parse_exit_code(gdb_text) + hardfaulted = gdb_observed_hardfault(gdb_text) + + if exit_code is not None: + return exit_code + + if hardfaulted: + fault_info = fault_info_from_gdb(gdb_text) + err("FAIL!") + err("Target entered HardFault_Handler") + if fault_info: + err(fault_info) + return 1 + + if gdbp.returncode != 0: + err("FAIL!") + err(f"gdb batch failed with code {gdbp.returncode}") + if out: + log_output(out, logging.ERROR) + if errout: + log_output(errout, logging.ERROR) + return gdbp.returncode + + err("FAIL!") + err("target did not hit nucleo_test_done") + return 1 + + finally: + try: + stp.terminate() + stp.wait(timeout=1.5) + except Exception: + try: + stp.kill() + except Exception: + pass + _close_swo_state(swo_state) + try: + if "gdb_script_path" in locals(): + os.unlink(gdb_script_path) + except Exception: + pass + + +def main(): + """Run the wrapper once with the configured debugger transport.""" + return _run_once() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/argv_blob.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/argv_blob.py new file mode 100644 index 000000000..1c9bc4503 --- /dev/null +++ b/test/zephyr/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/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py new file mode 100755 index 000000000..500c00030 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py @@ -0,0 +1,132 @@ +#!/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 writes the STM32N6 FLEXMEM control registers over SWD, polls +``SYSCFG->CM55TCMCR`` until the requested TCM split is visible, then resets the +target so the next ELF can be loaded into the expanded ITCM/DTCM layout. +""" + +import logging +import os +import sys +import tempfile + +from nucleo_host.openocd_tools import find_openocd +from nucleo_host.openocd_tools import flexmem_script_lines +from nucleo_host.openocd_tools import openocd_base_args +from nucleo_host.openocd_tools import run_quiet +from nucleo_host.openocd_tools import serial_from_env +from nucleo_host.openocd_tools import speed_khz_from_env +from nucleo_host.openocd_tools import transport_from_env + +DONE = "FLEXMEM configuration complete; reset target and load test binary." + +# Polling the register via SWD proves that the new ITCM/DTCM split latched +# before the next test binary is loaded. +RCC_APB4ENSR2_ADDR = "0x56028a78" +CM55TCMCR_ADDR = "0x56008008" +CM55RSTCR_ADDR = "0x56008018" +CM55TCMCR_EXPECTED_MASK = 0xFF +CM55TCMCR_EXPECTED_VALUE = 0x99 + +LOG = logging.getLogger(__name__) + + +def configure_logging(): + """Configure logging, using ``FLEXMEM_VERBOSE`` as the debug switch.""" + level = logging.DEBUG if os.environ.get("FLEXMEM_VERBOSE") else logging.INFO + logging.basicConfig(level=level, format="%(message)s") + + +def log_output(output, level): + """Log multiline subprocess output at the requested level.""" + if not output: + return + for line in output.rstrip().splitlines(): + LOG.log(level, line) + + +def err(msg): + """Report a user-visible error line.""" + LOG.error("%s", msg) + + +def openocd_cli(): + """Return the OpenOCD executable path, or report a helpful error.""" + openocd = find_openocd(os.environ.get("OPENOCD", "")) + if openocd is None: + err("OpenOCD not found; set OPENOCD or ensure openocd is on PATH") + return openocd + + +def _openocd_config_cmd(openocd, timeout_s): + """Build the OpenOCD command for one FLEXMEM configuration attempt.""" + script_lines = flexmem_script_lines( + timeout_ms=int(timeout_s * 1000), + rcc_apb4ensr2_addr=RCC_APB4ENSR2_ADDR, + cm55tcmcr_addr=CM55TCMCR_ADDR, + cm55rstcr_addr=CM55RSTCR_ADDR, + expected_mask=CM55TCMCR_EXPECTED_MASK, + expected_value=CM55TCMCR_EXPECTED_VALUE, + ) + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".cfg") as script: + script.write("\n".join(script_lines)) + script.write("\n") + script_path = script.name + + cmd = openocd_base_args( + openocd=openocd, + speed=speed_khz_from_env(), + serial=serial_from_env(), + transport=transport_from_env(), + ) + ["-f", script_path] + return cmd, script_path + + +def _run_openocd_config_once(openocd, timeout_s): + """Run the OpenOCD FLEXMEM configuration script.""" + cmd, script_path = _openocd_config_cmd(openocd, timeout_s) + try: + cp = run_quiet(cmd) + finally: + try: + os.unlink(script_path) + except OSError: + pass + return cp + + +def run_openocd_config(timeout_s): + """Configure FLEXMEM using OpenOCD memory reads and writes.""" + openocd = openocd_cli() + if openocd is None: + return 2 + + cp = _run_openocd_config_once(openocd, timeout_s) + if os.environ.get("FLEXMEM_VERBOSE") or cp.returncode != 0: + log_output(cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR) + if cp.returncode != 0: + err("OpenOCD FLEXMEM register configuration failed") + return cp.returncode + + +def main(): + """Configure FLEXMEM and verify the latched layout.""" + configure_logging() + + if len(sys.argv) != 1: + err(f"Usage: {sys.argv[0]}") + return 2 + + timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "5")) + + return run_openocd_config(timeout_s) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py new file mode 100644 index 000000000..107a5cd6d --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py @@ -0,0 +1,120 @@ +# 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, + hardfault_break, + done_break, + argv_bin, + arg_block_addr, + arg_block_sym, +): + """ + Build the full GDB command list for one test-image run. + + The order is part of the platform contract: load the RAM ELF, run normal + C startup until ``__wrap_main``, restore argv after ``.bss`` has been + cleared, install completion/fault breakpoints, continue, then read the + target return code from the completion breakpoint. + """ + gdb_lines = [ + "set pagination off", + "set confirm off", + f"target remote localhost:{port}", + # Keep the GDB script focused on target state and RAM transfers. + "load", + f"tbreak {wrap_main_break}", + f"jump {reset_handler_jump}", + restore_argv_command(argv_bin, arg_block_addr, arg_block_sym), + "set $nucleo_exit_code = -1", + f"break {done_break}", + "commands", + " set $nucleo_exit_code = $r0", + " echo [[NUCLEO-DONE]]\\n", + "end", + f"break {hardfault_break}", + "commands", + " echo [[NUCLEO-HARDFAULT]]\\n", + "end", + ] + gdb_lines += [ + "continue", + "if $nucleo_exit_code != -1", + " echo NUCLEO_EXIT_CODE=", + " output/d $nucleo_exit_code", + " echo \\n", + "end", + ] + gdb_lines += fault_diagnostic_commands() + # Leave the board in a fresh boot state for the next FLEXMEM setup. This + # runs after stdout/fault harvesting and does not affect the current test. + gdb_lines += ["monitor reset_config none", "monitor reset run"] + return gdb_lines + + +def restore_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 fault_diagnostic_commands(): + """Return commands that print Cortex-M fault diagnostics.""" + return [ + "info registers", + "x/4wx $sp", + "echo CFSR=", + "output/x *(unsigned int *)0xE000ED28", + "echo \\n", + "echo HFSR=", + "output/x *(unsigned int *)0xE000ED2C", + "echo \\n", + "echo DFSR=", + "output/x *(unsigned int *)0xE000ED30", + "echo \\n", + "echo MMFAR=", + "output/x *(unsigned int *)0xE000ED34", + "echo \\n", + "echo BFAR=", + "output/x *(unsigned int *)0xE000ED38", + "echo \\n", + "echo AFSR=", + "output/x *(unsigned int *)0xE000ED3C", + "echo \\n", + "echo SHCSR=", + "output/x *(unsigned int *)0xE000ED24", + "echo \\n", + "echo CCR=", + "output/x *(unsigned int *)0xE000ED14", + "echo \\n", + "echo MSP=", + "output/x $msp", + "echo \\n", + "echo PSP=", + "output/x $psp", + "echo \\n", + "echo LR=", + "output/x $lr", + "echo \\n", + "echo PC=", + "output/x $pc", + "echo \\n", + "echo STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\\n", + "if ($lr & 4)", + " x/8wx $psp", + "else", + " x/8wx $msp", + "end", + "x/4wx 0xE000ED28", + "x/wx 0xE000ED38", + ] diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py new file mode 100644 index 000000000..af673091c --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py @@ -0,0 +1,214 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Locate OpenOCD and build NUCLEO-N657X0-Q command lines.""" + +import os +import shutil +import subprocess + + +DEFAULT_INTERFACE = "interface/stlink.cfg" +DEFAULT_TARGET = "target/stm32n6x.cfg" +DEFAULT_CPU_TARGET = "stm32n6x.cpu" +DEFAULT_SWO_TARGET = "stm32n6x.swo" +DEFAULT_TPIU_TARGET = "" + + +def find_openocd(openocd=""): + """Find ``openocd`` from an explicit path or ``PATH``.""" + candidates = [] + if openocd: + candidates.append(openocd) + path_candidate = shutil.which("openocd") + if path_candidate: + candidates.append(path_candidate) + for candidate in candidates: + if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return None + + +def speed_khz_from_env(default="8000"): + """Return adapter speed in kHz.""" + return os.environ.get("OPENOCD_SPEED", default) + + +def swo_traceclk_from_env(default="100000000"): + """Return the target trace clock in Hz for SWO capture.""" + return os.environ.get("SWO_TRACECLK", default) + + +def swo_pin_freq_from_env(default="1000000"): + """Return the async SWO pin bitrate in Hz.""" + return os.environ.get("SWO_PIN_FREQ", default) + + +def swo_formatter_from_env(default="0"): + """Return the OpenOCD TPIU formatter setting for SWO capture.""" + return os.environ.get("SWO_FORMATTER", default) + + +def swo_target_from_env(default=DEFAULT_SWO_TARGET): + """Return the OpenOCD trace component used for SWO capture.""" + return os.environ.get("OPENOCD_SWO_TARGET", default) + + +def tpiu_target_from_env(default=DEFAULT_TPIU_TARGET): + """Return the OpenOCD TPIU component used to configure target trace output.""" + return os.environ.get("OPENOCD_TPIU_TARGET", default) + + +def serial_from_env(default=""): + """Return the optional OpenOCD adapter serial selector.""" + return os.environ.get("OPENOCD_SERIAL", default) + + +def transport_from_env(default="swd"): + """Return the OpenOCD transport name.""" + return os.environ.get("OPENOCD_TRANSPORT", default).strip().lower() + + +def run_quiet(cmd): + """Run a command with stdout and stderr merged for delayed diagnostics.""" + return subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) + + +def openocd_base_args( + *, + openocd="openocd", + interface=None, + target=None, + speed="8000", + serial="", + transport="swd", +): + """Return common OpenOCD arguments for the NUCLEO debug connection.""" + args = [ + openocd, + "-f", + interface or os.environ.get("OPENOCD_INTERFACE", DEFAULT_INTERFACE), + "-c", + f"transport select {transport}", + "-f", + target or os.environ.get("OPENOCD_TARGET", DEFAULT_TARGET), + "-c", + ( + f"if {{[lsearch -exact [target names] {DEFAULT_CPU_TARGET}] >= 0}} " + f"{{ {DEFAULT_CPU_TARGET} configure -defer-examine }}" + ), + "-c", + f"adapter speed {speed}", + ] + if serial: + args += ["-c", f"adapter serial {serial}"] + return args + + +def runtime_gdbserver_cmd( + *, + openocd="openocd", + port=3333, + swo_port=None, + swo_traceclk="100000000", + swo_pin_freq="1000000", + swo_formatter="0", + swo_target=None, + tpiu_target=None, + speed="8000", + serial="", + transport="swd", +): + """Return the OpenOCD command used as the runtime GDB server.""" + cmd = openocd_base_args( + openocd=openocd, + speed=speed, + serial=serial, + transport=transport, + ) + [ + "-c", + "reset_config srst_only srst_nogate", + "-c", + f"gdb_port {port}", + "-c", + "tcl_port disabled", + "-c", + "telnet_port disabled", + "-c", + "init", + "-c", + f"{DEFAULT_CPU_TARGET} arp_examine", + ] + if swo_port is not None: + swo_target = swo_target or swo_target_from_env() + tpiu_target = tpiu_target or tpiu_target_from_env() + if tpiu_target: + cmd += [ + "-c", + ( + f"{tpiu_target} configure -protocol uart " + f"-traceclk {swo_traceclk} -pin-freq {swo_pin_freq} " + f"-formatter {swo_formatter}" + ), + "-c", + f"{tpiu_target} enable", + ] + cmd += [ + "-c", + ( + f"{swo_target} configure -protocol uart " + f"-traceclk {swo_traceclk} -pin-freq {swo_pin_freq} " + f"-output :{swo_port} -formatter {swo_formatter}" + ), + "-c", + f"{swo_target} enable", + "-c", + f"{DEFAULT_CPU_TARGET} itm port 0 on", + ] + return cmd + + +def flexmem_script_lines( + *, + timeout_ms, + mem_target=DEFAULT_CPU_TARGET, + rcc_apb4ensr2_addr="0x56028a78", + cm55tcmcr_addr="0x56008008", + cm55rstcr_addr="0x56008018", + expected_mask=0xFF, + expected_value=0x99, +): + """Return an OpenOCD TCL script for configuring STM32N6 FLEXMEM.""" + return [ + "reset_config none", + "init", + f"{mem_target} arp_examine", + "proc read32 {addr} {", + f" return [lindex [{mem_target} read_memory $addr 32 1] 0]", + "}", + "proc write32 {addr value} {", + f" {mem_target} mww $addr $value", + "}", + f"set rcc_apb4ensr2 [read32 {rcc_apb4ensr2_addr}]", + f"write32 {rcc_apb4ensr2_addr} [expr {{$rcc_apb4ensr2 | 0x1}}]", + f"set cm55tcmcr [read32 {cm55tcmcr_addr}]", + f"write32 {cm55tcmcr_addr} [expr {{($cm55tcmcr & ~0xff) | 0x{expected_value:x}}}]", + f"set cm55rstcr [read32 {cm55rstcr_addr}]", + f"write32 {cm55rstcr_addr} [expr {{$cm55rstcr | 0x1}}]", + "proc wait_flexmem_configured {} {", + f" set deadline [expr {{[clock milliseconds] + {int(timeout_ms)}}}]", + " while {[clock milliseconds] < $deadline} {", + f" set value [read32 {cm55tcmcr_addr}]", + f" if {{($value & 0x{expected_mask:x}) == 0x{expected_value:x}}} {{ return }}", + " sleep 200", + " }", + f' error "FLEXMEM configuration register did not reach expected 0x{expected_value:x} value"', + "}", + "wait_flexmem_configured", + "reset_config none", + "reset run", + "shutdown", + ] diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/results.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/results.py new file mode 100644 index 000000000..17cb6516c --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/results.py @@ -0,0 +1,106 @@ +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Parse GDB failure output and Cortex-M fault diagnostics.""" + +import re + +HARDFAULT_SENTINEL = "[[NUCLEO-HARDFAULT]]" + + +def decode_cfsr(cfsr: int): + """Return names of set Configurable Fault Status Register bits.""" + bits = [ + (0, "IACCVIOL"), + (1, "DACCVIOL"), + (3, "MUNSTKERR"), + (4, "MSTKERR"), + (5, "MLSPERR"), + (7, "MMARVALID"), + (8, "IBUSERR"), + (9, "PRECISERR"), + (10, "IMPRECISERR"), + (11, "UNSTKERR"), + (12, "STKERR"), + (13, "LSPERR"), + (15, "BFARVALID"), + (16, "UNDEFINSTR"), + (17, "INVSTATE"), + (18, "INVPC"), + (19, "NOCP"), + (24, "UNALIGNED"), + (25, "DIVBYZERO"), + ] + return [name for bit, name in bits if cfsr & (1 << bit)] + + +def decode_hfsr(hfsr: int): + """Return names of set HardFault Status Register bits.""" + bits = [(1, "VECTTBL"), (30, "FORCED"), (31, "DEBUGEVT")] + return [name for bit, name in bits if hfsr & (1 << bit)] + + +def fault_info_from_gdb(gdb_text: str) -> str: + """Format fault registers emitted by the GDB script into readable text.""" + values = {} + register_pattern = ( + r"^(CFSR|HFSR|DFSR|MMFAR|BFAR|AFSR|SHCSR|CCR|MSP|PSP|LR|PC)" + r"=0x([0-9a-fA-F]+)$" + ) + for name, value in re.findall(register_pattern, gdb_text, re.MULTILINE): + values[name] = int(value, 16) + + if not values: + return "" + + lines = ["Fault registers:"] + for name in ( + "CFSR", + "HFSR", + "DFSR", + "MMFAR", + "BFAR", + "AFSR", + "SHCSR", + "CCR", + "MSP", + "PSP", + "LR", + "PC", + ): + if name in values: + lines.append(f" {name}=0x{values[name]:08x}") + + cfsr_bits = decode_cfsr(values.get("CFSR", 0)) + hfsr_bits = decode_hfsr(values.get("HFSR", 0)) + if cfsr_bits: + lines.append(" CFSR bits: " + ", ".join(cfsr_bits)) + if hfsr_bits: + lines.append(" HFSR bits: " + ", ".join(hfsr_bits)) + + # The stack dump follows a marker printed by the GDB script. Keep parsing + # permissive because GDB may format the memory rows differently by version. + stacked = re.search( + r"^STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\s*\n" + r"((?:0x[0-9a-fA-F]+:\s+.*\n?)?)", + gdb_text, + re.MULTILINE, + ) + if stacked: + stack_lines = [ + line.strip() for line in stacked.group(1).splitlines() if line.strip() + ] + if stack_lines: + lines.append(" stacked frame dump:") + lines.extend(f" {line}" for line in stack_lines) + + return "\n".join(lines) + + +def gdb_observed_hardfault(gdb_text: str) -> bool: + """Return whether GDB output shows the target entered HardFault_Handler.""" + return ( + HARDFAULT_SENTINEL in gdb_text + or re.search(r"^HardFault_Handler \(\)", gdb_text, re.MULTILINE) is not None + ) diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/symbols.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/symbols.py new file mode 100644 index 000000000..b80d729c7 --- /dev/null +++ b/test/zephyr/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/zephyr/platform.mk b/test/zephyr/platform.mk new file mode 100644 index 000000000..b92f78c83 --- /dev/null +++ b/test/zephyr/platform.mk @@ -0,0 +1,133 @@ +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +PLATFORM_PATH := test/zephyr + +# BUILD_DIR is set by the top-level Makefile after this file is included; +# define it here too so the explicit bin rules below expand to the right path. +BUILD_DIR ?= test/build + +# Pick a target with ZEPHYR_TARGET= (default below). QEMU targets map to +# both a Zephyr board and QEMU machine; hardware targets map to a Zephyr board +# and provide their own execution wrapper. +ZEPHYR_TARGET ?= mps3-an547 + +ZEPHYR_BOARD_mps2-an385 := mps2/an385 +ZEPHYR_QEMU_mps2-an385 := mps2-an385 # Cortex-M3 +ZEPHYR_BOARD_mps2-an386 := mps2/an386 +ZEPHYR_QEMU_mps2-an386 := mps2-an386 # Cortex-M4 +ZEPHYR_BOARD_mps2-an500 := mps2/an500 +ZEPHYR_QEMU_mps2-an500 := mps2-an500 # Cortex-M7 +ZEPHYR_BOARD_mps2-an521 := mps2/an521/cpu0 +ZEPHYR_QEMU_mps2-an521 := mps2-an521 # Cortex-M33 +ZEPHYR_BOARD_mps3-an547 := mps3/corstone300/an547 +ZEPHYR_QEMU_mps3-an547 := mps3-an547 # Cortex-M55 +ZEPHYR_BOARD_nucleo-n657x0-q := nucleo_n657x0_q # Cortex-M55 + +ZEPHYR_FIPS202_BACKEND_mps3-an547 := fips202/native/armv81m/mve.h +ZEPHYR_FIPS202_BACKEND_nucleo-n657x0-q := fips202/native/armv81m/mve.h + +ZEPHYR_TARGETS := mps2-an385 mps2-an386 mps2-an500 mps2-an521 mps3-an547 nucleo-n657x0-q + +ZEPHYR_BOARD := $(ZEPHYR_BOARD_$(ZEPHYR_TARGET)) +export QEMU_MACHINE := $(strip $(ZEPHYR_QEMU_$(ZEPHYR_TARGET))) +ZEPHYR_IS_NUCLEO_N657X0_Q := $(filter nucleo-n657x0-q,$(ZEPHYR_TARGET)) + +ifneq ($(ZEPHYR_IS_NUCLEO_N657X0_Q),) +CROSS_PREFIX ?= arm-none-eabi- +CC = gcc +endif + +ifeq ($(ZEPHYR_BOARD),) +$(error Unknown ZEPHYR_TARGET '$(ZEPHYR_TARGET)'. Supported: $(ZEPHYR_TARGETS)) +endif + +# The test binaries are built by Zephyr's CMake (which uses its own arm +# toolchain via the .#zephyr dev shell), not the generic Make rules. The +# top-level targets still attach the usual object/library prerequisites to the +# bin paths; with OPT=0 those are portable host objects that compile cleanly +# and are simply discarded (the Zephyr ELF is copied over them). +OPT ?= 0 + +# Native backends are an OPT=1 feature (an547 builds the Armv8.1-M MVE backend). +ZEPHYR_FIPS202_BACKEND := $(if $(filter 1,$(OPT)),$(strip $(ZEPHYR_FIPS202_BACKEND_$(ZEPHYR_TARGET)))) + +ZEPHYR_APP := $(PLATFORM_PATH)/app +ZEPHYR_BUILD_DIR := $(BUILD_DIR)/zephyr/$(ZEPHYR_TARGET) +ZEPHYR_ACTIVE_TARGET := $(BUILD_DIR)/zephyr/.active-target +ZEPHYR_APP_INPUTS := \ + $(ZEPHYR_APP)/CMakeLists.txt \ + $(ZEPHYR_APP)/Kconfig \ + $(ZEPHYR_APP)/prj.conf \ + $(ZEPHYR_APP)/nucleo_n657x0_q.conf \ + $(ZEPHYR_APP)/shim.c \ + $(ZEPHYR_APP)/shim_nucleo_n657x0_q.c \ + $(ZEPHYR_APP)/nucleo_n657x0_q.overlay +ZEPHYR_NUCLEO_PLATFORM_PATH := $(PLATFORM_PATH)/nucleo_n657x0_q +ZEPHYR_NUCLEO_OVERLAY := $(abspath $(ZEPHYR_APP)/nucleo_n657x0_q.overlay) +ZEPHYR_NUCLEO_CONF := $(abspath $(ZEPHYR_APP)/nucleo_n657x0_q.conf) +ZEPHYR_TARGET_CMAKE_ARGS := $(if $(ZEPHYR_IS_NUCLEO_N657X0_Q),\ + -DZEPHYR_NUCLEO_N657X0_Q=ON \ + -DEXTRA_CONF_FILE=$(ZEPHYR_NUCLEO_CONF) \ + -DDTC_OVERLAY_FILE=$(ZEPHYR_NUCLEO_OVERLAY)) + +.PHONY: zephyr_target_marker_force +$(ZEPHYR_ACTIVE_TARGET): zephyr_target_marker_force + $(Q)[ -d $(@D) ] || mkdir -p $(@D) + $(Q)if [ ! -f $@ ] || [ "$$(cat $@)" != "$(ZEPHYR_TARGET)" ]; then \ + echo "$(ZEPHYR_TARGET)" > $@; \ + fi + +# Build a test as a Zephyr application and drop the resulting ELF at the path +# the top-level Makefile expects. An explicit rule for the exact bin path wins +# over the generic link pattern rule in test/mk/rules.mk. +# $(1) level $(2) bin name $(3) test source (repo-relative) $(4) extra -D +define ZEPHYR_BIN +$(BUILD_DIR)/mlkem$(1)/bin/$(2): $(ZEPHYR_ACTIVE_TARGET) $(ZEPHYR_APP_INPUTS) $(3) + $$(Q)echo " ZEPHYR $(ZEPHYR_TARGET) ML-KEM-$(1): $(3)" + $$(Q)cmake -GNinja -S $(ZEPHYR_APP) -B $(ZEPHYR_BUILD_DIR)/$(2) \ + -DBOARD=$(ZEPHYR_BOARD) \ + -DZEPHYR_NATIVE_ROOT=$(CURDIR) \ + -DZEPHYR_LEVEL=$(1) \ + -DZEPHYR_TEST_SRC=$(3) \ + -DZEPHYR_TEST_DEFS="NTESTS_FUNC=3 NTESTS_KAT=100 MLK_BENCHMARK_NTESTS=10 MLK_BENCHMARK_NITERATIONS=10 MLK_BENCHMARK_NWARMUP=10" \ + -DZEPHYR_FIPS202_BACKEND=$(ZEPHYR_FIPS202_BACKEND) \ + $(if $(ZEPHYR_FIPS202_BACKEND),-DCONFIG_FIPS202_MVE_BACKEND=y) \ + $(ZEPHYR_TARGET_CMAKE_ARGS) \ + $(4) \ + -DUSER_CACHE_DIR=$(abspath $(ZEPHYR_BUILD_DIR)/$(2)/.cache) \ + >/dev/null + $$(Q)cmake --build $(ZEPHYR_BUILD_DIR)/$(2) >/dev/null + $$(Q)[ -d $$(@D) ] || mkdir -p $$(@D) + $$(Q)cp $(ZEPHYR_BUILD_DIR)/$(2)/zephyr/zephyr.elf $$@ +endef + +$(eval $(call ZEPHYR_BIN,512,test_mlkem512,test/src/test_mlkem.c)) +$(eval $(call ZEPHYR_BIN,768,test_mlkem768,test/src/test_mlkem.c)) +$(eval $(call ZEPHYR_BIN,1024,test_mlkem1024,test/src/test_mlkem.c)) + +$(eval $(call ZEPHYR_BIN,512,gen_KAT512,test/src/gen_KAT.c)) +$(eval $(call ZEPHYR_BIN,768,gen_KAT768,test/src/gen_KAT.c)) +$(eval $(call ZEPHYR_BIN,1024,gen_KAT1024,test/src/gen_KAT.c)) + +$(eval $(call ZEPHYR_BIN,512,acvp_mlkem512,test/acvp/acvp_mlkem.c)) +$(eval $(call ZEPHYR_BIN,768,acvp_mlkem768,test/acvp/acvp_mlkem.c)) +$(eval $(call ZEPHYR_BIN,1024,acvp_mlkem1024,test/acvp/acvp_mlkem.c)) + +$(eval $(call ZEPHYR_BIN,512,wycheproof_mlkem512,test/wycheproof/wycheproof_mlkem.c)) +$(eval $(call ZEPHYR_BIN,768,wycheproof_mlkem768,test/wycheproof/wycheproof_mlkem.c)) +$(eval $(call ZEPHYR_BIN,1024,wycheproof_mlkem1024,test/wycheproof/wycheproof_mlkem.c)) + +$(eval $(call ZEPHYR_BIN,512,bench_mlkem512,test/bench/bench_mlkem.c,-DZEPHYR_TEST_HAL=ON)) +$(eval $(call ZEPHYR_BIN,768,bench_mlkem768,test/bench/bench_mlkem.c,-DZEPHYR_TEST_HAL=ON)) +$(eval $(call ZEPHYR_BIN,1024,bench_mlkem1024,test/bench/bench_mlkem.c,-DZEPHYR_TEST_HAL=ON)) + +$(eval $(call ZEPHYR_BIN,512,bench_components_mlkem512,test/bench/bench_components_mlkem.c,-DZEPHYR_TEST_HAL=ON)) +$(eval $(call ZEPHYR_BIN,768,bench_components_mlkem768,test/bench/bench_components_mlkem.c,-DZEPHYR_TEST_HAL=ON)) +$(eval $(call ZEPHYR_BIN,1024,bench_components_mlkem1024,test/bench/bench_components_mlkem.c,-DZEPHYR_TEST_HAL=ON)) + +ifeq ($(ZEPHYR_IS_NUCLEO_N657X0_Q),) +EXEC_WRAPPER := $(abspath $(PLATFORM_PATH)/exec_wrapper.py) +else +EXEC_WRAPPER := $(abspath $(ZEPHYR_NUCLEO_PLATFORM_PATH)/exec_wrapper.py) +endif