Skip to content

Commit 3890d1d

Browse files
committed
Port: Add NUCLEO-N657X0-Q bare-metal platform
Add support for running bare-metal tests and benchmarks on the STM32 NUCLEO-N657X0-Q. This adds the platform linker scripts, semihosting glue, argv/result helpers, FLEXMEM configuration and recovery tooling, and an OpenOCD-based execution backend with host-side tests. Package the required ST OpenOCD tooling in Nix and expose a nucleo-n657x0-q development shell. Wire the platform into functional tests, component benchmarks, CI workflows, and shared bare-metal result handling, including Cortex-M cycle counter support.
1 parent 3256249 commit 3890d1d

34 files changed

Lines changed: 3618 additions & 6 deletions

.github/actionlint.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ self-hosted-runner:
1212
- pqcp-x64
1313
# RISE RISC-V runner
1414
- ubuntu-24.04-riscv
15+
- self-hosted-nucleo-n657x0

.github/workflows/bench.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,22 @@ jobs:
7979
bench_extra_args: "-r"
8080
nix_shell: ''
8181
cross_prefix: ""
82+
- system: nucleo-n657x0
83+
name: Arm Cortex-M55 (NUCLEO-N657X0-Q) benchmarks
84+
bench_pmu: CYCCNT
85+
archflags: ""
86+
cflags: ""
87+
ldflags: ""
88+
bench_extra_args: ""
89+
nix_shell: nucleo-n657x0-q
90+
cross_prefix: ""
8291
if: github.repository_owner == 'pq-code-package' && !github.event.pull_request.head.repo.fork && (github.event.label.name == 'benchmark' || github.ref == 'refs/heads/main')
8392
runs-on: self-hosted-${{ matrix.target.system }}
8493
steps:
8594
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
8695
- uses: ./.github/actions/bench
96+
env:
97+
EXTRA_MAKEFILE: ${{ matrix.target.system == 'nucleo-n657x0' && 'test/baremetal/platform/nucleo-n657x0-q/platform.mk' || '' }}
8798
with:
8899
name: ${{ matrix.target.name }}
89100
cflags: ${{ matrix.target.cflags }}

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,29 @@ jobs:
218218
nix-shell: ''
219219
gh_token: ${{ secrets.GITHUB_TOKEN }}
220220
cflags: "-DMLKEM_DEBUG -DMLK_FORCE_PPC64LE"
221+
nucleo_n657x0_q_tests:
222+
if: github.repository_owner == 'pq-code-package' && !github.event.pull_request.head.repo.fork
223+
name: Functional tests (NUCLEO-N657X0-Q)
224+
runs-on: self-hosted-nucleo-n657x0
225+
steps:
226+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
227+
- name: baremetal build + test
228+
uses: ./.github/actions/functest
229+
env:
230+
EXTRA_MAKEFILE: test/baremetal/platform/nucleo-n657x0-q/platform.mk
231+
with:
232+
nix-shell: nucleo-n657x0-q
233+
gh_token: ${{ secrets.GITHUB_TOKEN }}
234+
opt: no_opt
235+
func: true
236+
kat: false
237+
acvp: false
238+
wycheproof: false
239+
examples: false
240+
stack: false
241+
unit: false
242+
alloc: false
243+
rng_fail: false
221244
backend_tests:
222245
name: AArch64 FIPS202 backends (${{ matrix.backend }})
223246
strategy:

flake.nix

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
export HOLLIGHT_LOAD_PATH="$IMPORTS_DIR:$S2N_BIGNUM_DIR''${HOLLIGHT_LOAD_PATH:+:$HOLLIGHT_LOAD_PATH}"
5858
export HOLDIR="$HOLLIGHT_DIR"
5959
'';
60+
6061
in
6162
{
6263
_module.args.pkgs = import inputs.nixpkgs {
@@ -94,6 +95,27 @@
9495
} ++ holLightToolchain;
9596
}).overrideAttrs (old: { shellHook = holLightShellHook; });
9697

98+
# arm-none-eabi-gcc + platform files from pqmx
99+
packages.m55-an547 = util.m55-an547;
100+
packages.avr-toolchain = util.avr-toolchain;
101+
packages.st-openocd = util.st-openocd;
102+
devShells.arm-embedded = util.mkShell {
103+
packages = builtins.attrValues
104+
{
105+
inherit (config.packages) m55-an547;
106+
inherit (pkgs) gcc-arm-embedded qemu coreutils python3 git;
107+
};
108+
};
109+
packages.nucleo-n657x0-q = util.nucleo-n657x0-q;
110+
devShells.nucleo-n657x0-q = util.mkShell {
111+
packages = builtins.attrValues ({
112+
inherit (config.packages) linters nucleo-n657x0-q st-openocd;
113+
inherit (pkgs) gcc-arm-embedded coreutils git libffi pkg-config;
114+
});
115+
};
116+
117+
118+
devShells.avr = util.mkShell (import ./nix/avr { inherit pkgs; });
97119
packages.hol_server = util.hol_server.hol_server_start;
98120
devShells.hol_light = (util.mkShell {
99121
packages = builtins.attrValues { inherit (config.packages) linters hol_light s2n_bignum hol_server; } ++ holLightToolchain;

nix/nucleo-n657x0-q/default.nix

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Copyright (c) The mldsa-native project authors
2+
# Copyright (c) The mlkem-native project authors
3+
# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT
4+
5+
{ stdenvNoCC
6+
, fetchFromGitHub
7+
, writeText
8+
}:
9+
# Construct a minimal STM32CubeN6-based platform environment for building
10+
# nucleo-n657x0-q benchmarks. The package copies only the required Cube headers
11+
# and sources, then patches selected files where needed to get the intended
12+
# RAM-loaded benchmark behavior.
13+
stdenvNoCC.mkDerivation {
14+
pname = "mlkem-native-nucleo-n657x0-q";
15+
version = "main";
16+
17+
src = fetchFromGitHub {
18+
owner = "STMicroelectronics";
19+
repo = "STM32CubeN6";
20+
rev = "1fc803683e03b0ce78e64523441d31acd0db2829";
21+
hash = "sha256-7iD7R3A+0YAfMZqKndWBq+z5TUY37XDVxFy5HK8/5Aw=";
22+
fetchSubmodules = true;
23+
};
24+
25+
dontBuild = true;
26+
27+
installPhase = ''
28+
set -eu
29+
30+
outp="$out/platform/nucleo-n657x0-q/src/platform"
31+
fsbl_tpl="Projects/NUCLEO-N657X0-Q/Templates/Template_FSBL_LRUN"
32+
hal="Drivers/STM32N6xx_HAL_Driver"
33+
cmsis_core="Drivers/CMSIS/Core/Include"
34+
cmsis_device="Drivers/CMSIS/Device/ST/STM32N6xx/Include"
35+
36+
install_file() {
37+
src_file="$1"
38+
dst_file="$outp/$2"
39+
mkdir -p "$(dirname "$dst_file")"
40+
cp "$src_file" "$dst_file"
41+
}
42+
43+
# CMSIS headers reached by stm32n657xx.h/core_cm55.h under the GCC build.
44+
for header in \
45+
cmsis_compiler.h \
46+
cmsis_gcc.h \
47+
cmsis_version.h \
48+
core_cm55.h \
49+
m-profile/armv7m_cachel1.h \
50+
m-profile/armv8m_mpu.h \
51+
m-profile/armv8m_pmu.h \
52+
m-profile/cmsis_gcc_m.h
53+
do
54+
install_file "$cmsis_core/$header" "Drivers/CMSIS/Core/Include/$header"
55+
done
56+
57+
for header in \
58+
stm32n6xx.h \
59+
stm32n657xx.h \
60+
system_stm32n6xx.h
61+
do
62+
install_file "$cmsis_device/$header" "Drivers/CMSIS/Device/ST/STM32N6xx/Include/$header"
63+
done
64+
65+
# FSBL files used by platform.mk.
66+
install_file \
67+
"$fsbl_tpl/STM32CubeIDE/Boot/Startup/startup_stm32n657xx_fsbl.s" \
68+
"gcc/startup_stm32n657xx.S"
69+
70+
# Use the linker-provided stack top instead of Cube's fixed SRAM stack.
71+
sed -i.bak -E 's@[.]word[[:space:]]+0x34110000@.word _estack@' \
72+
"$outp/gcc/startup_stm32n657xx.S"
73+
74+
# Mask interrupts as soon as Reset_Handler starts to avoid stale debug/board
75+
# state firing before this RAM-loaded test image initializes C runtime state.
76+
sed -i.bak -E '/^Reset_Handler:/a\ cpsid i' \
77+
"$outp/gcc/startup_stm32n657xx.S"
78+
rm -f "$outp/gcc/startup_stm32n657xx.S.bak"
79+
80+
install_file "$fsbl_tpl/FSBL/Src/system_stm32n6xx_fsbl.c" "system_stm32n6xx.c"
81+
install_file "$fsbl_tpl/FSBL/Src/stm32n6xx_it.c" "stm32n6xx_it.c"
82+
install_file "$fsbl_tpl/FSBL/Inc/stm32n6xx_it.h" "Inc/stm32n6xx_it.h"
83+
install_file "$fsbl_tpl/FSBL/Inc/main.h" "Inc/main.h"
84+
install_file "$fsbl_tpl/FSBL/Inc/stm32n6xx_hal_conf.h" "Inc/stm32n6xx_hal_conf.h"
85+
86+
conf="$outp/Inc/stm32n6xx_hal_conf.h"
87+
88+
# Disable BSEC because the platform test build does not link the BSEC HAL.
89+
sed -i.bak -E 's@^[[:space:]]*#[[:space:]]*define[[:space:]]+HAL_BSEC_MODULE_ENABLED@/* #define HAL_BSEC_MODULE_ENABLED */@' "$conf"
90+
91+
# Disable XSPI because only the extracted clock setup is used, not Cube's
92+
# external memory setup or XSPI HAL driver.
93+
sed -i.bak -E 's@^[[:space:]]*#[[:space:]]*define[[:space:]]+HAL_XSPI_MODULE_ENABLED@/* #define HAL_XSPI_MODULE_ENABLED */@' "$conf"
94+
rm -f "$conf.bak"
95+
96+
# HAL files named directly by platform.mk, plus headers enabled by the
97+
# pruned FSBL HAL config and their direct dependencies.
98+
for header in \
99+
Legacy/stm32_hal_legacy.h \
100+
stm32n6xx_hal.h \
101+
stm32n6xx_hal_cortex.h \
102+
stm32n6xx_hal_def.h \
103+
stm32n6xx_hal_dma.h \
104+
stm32n6xx_hal_dma_ex.h \
105+
stm32n6xx_hal_exti.h \
106+
stm32n6xx_hal_gpio.h \
107+
stm32n6xx_hal_gpio_ex.h \
108+
stm32n6xx_hal_pwr.h \
109+
stm32n6xx_hal_pwr_ex.h \
110+
stm32n6xx_hal_rcc.h \
111+
stm32n6xx_hal_rcc_ex.h \
112+
stm32n6xx_ll_bus.h \
113+
stm32n6xx_ll_rcc.h
114+
do
115+
install_file "$hal/Inc/$header" "Drivers/STM32N6xx_HAL_Driver/Inc/$header"
116+
done
117+
118+
for source in \
119+
stm32n6xx_hal.c \
120+
stm32n6xx_hal_cortex.c \
121+
stm32n6xx_hal_pwr.c \
122+
stm32n6xx_hal_pwr_ex.c \
123+
stm32n6xx_hal_rcc.c \
124+
stm32n6xx_hal_rcc_ex.c
125+
do
126+
install_file "$hal/Src/$source" "Drivers/STM32N6xx_HAL_Driver/Src/$source"
127+
done
128+
129+
fsbl_main_c="$fsbl_tpl/FSBL/Src/main.c"
130+
clock_out="$outp/clock_config.c"
131+
tmpclk="$TMPDIR/clock_config.$$.$RANDOM.c"
132+
: > "$tmpclk"
133+
134+
# Preserve the Cube file header in the generated standalone clock source.
135+
awk '
136+
/\/\* USER CODE BEGIN Header \*\// { print; inhdr=1; next }
137+
inhdr { print }
138+
/\/\* USER CODE END Header \*\// { inhdr=0 }
139+
' "$fsbl_main_c" >> "$tmpclk"
140+
printf "\n#include \"main.h\"\n\n" >> "$tmpclk"
141+
142+
# Keep Cube's clock-tree explanatory comment next to SystemClock_Config.
143+
awk '
144+
/\/\* USER CODE BEGIN CLK 1 \*\// { print; inclk=1; next }
145+
inclk { print }
146+
/\/\* USER CODE END CLK 1 \*\// { inclk=0 }
147+
' "$fsbl_main_c" >> "$tmpclk"
148+
printf "\n" >> "$tmpclk"
149+
150+
# Extract exactly the SystemClock_Config function body from the FSBL main.c
151+
# into the generated clock_config.c, tracking braces so that file does not
152+
# pull in unused FSBL code.
153+
awk '
154+
BEGIN { copy=0; lvl=0; sig=0 }
155+
/^[ \t]*void[ \t]+SystemClock_Config[ \t]*\([ \t]*void[ \t]*\)[ \t]*\{/ {
156+
print; copy=1; lvl=1; next
157+
}
158+
/^[ \t]*void[ \t]+SystemClock_Config[ \t]*\([ \t]*void[ \t]*\)[ \t]*$/ {
159+
print; copy=1; sig=1; next
160+
}
161+
copy {
162+
if (sig && $0 !~ /\{/) { print; next }
163+
nopen=gsub(/{/,"{")
164+
nclose=gsub(/}/,"}")
165+
lvl += nopen - nclose
166+
print
167+
sig=0
168+
if (lvl<=0) exit
169+
}
170+
' "$fsbl_main_c" >> "$tmpclk"
171+
grep -q "SystemClock_Config" "$tmpclk"
172+
mv "$tmpclk" "$clock_out"
173+
174+
# The standalone clock_config.c calls SystemClock_Config, so make sure the
175+
# copied FSBL main.h exposes the declaration expected by local platform code.
176+
if ! grep -q "void[[:space:]]\+SystemClock_Config[[:space:]]*([[:space:]]*void[[:space:]]*)" "$outp/Inc/main.h"; then
177+
printf "\nvoid SystemClock_Config(void);\n" >> "$outp/Inc/main.h"
178+
fi
179+
180+
# SystemClock_Config calls Error_Handler on HAL failures; the implementation
181+
# is provided by the platform test sources, but main.h must declare it.
182+
if ! grep -q "void[[:space:]]\+Error_Handler[[:space:]]*([[:space:]]*void[[:space:]]*)" "$outp/Inc/main.h"; then
183+
printf "void Error_Handler(void);\n" >> "$outp/Inc/main.h"
184+
fi
185+
'';
186+
187+
setupHook = writeText "setup-hook.sh" ''
188+
export NUCLEO_N657X0_Q_PATH="$1/platform/nucleo-n657x0-q/src/platform/"
189+
'';
190+
191+
meta = {
192+
description = "Platform files for STM32 NUCLEO-N657X0-Q RAM-only OpenOCD tests";
193+
homepage = "https://github.com/STMicroelectronics/STM32CubeN6";
194+
};
195+
}

nix/st-openocd/default.nix

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright (c) The mlkem-native project authors
2+
# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT
3+
4+
{ lib
5+
, fetchFromGitHub
6+
, openocd
7+
, autoreconfHook
8+
, autoconf
9+
, automake
10+
, libtool
11+
, which
12+
}:
13+
14+
openocd.overrideAttrs (old: rec {
15+
pname = "st-openocd";
16+
version = "unstable-2026-05-01";
17+
18+
# OpenOCD 0.12.0 lacks the STM32N6 target script required by the
19+
# NUCLEO-N657X0-Q RAM-only OpenOCD backend.
20+
src = fetchFromGitHub {
21+
owner = "openocd-org";
22+
repo = "openocd";
23+
rev = "4e9b167e1ae5ccb437eb0538440988b3f0ec53cb";
24+
fetchSubmodules = true;
25+
hash = "sha256-8aYl7JzulPxH6vgSeTKTMIZVH6d55JJlXTBkfgAPTbU=";
26+
};
27+
28+
buildInputs = lib.filter
29+
(dep: lib.getName dep != "jimtcl")
30+
(old.buildInputs or [ ]);
31+
32+
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
33+
autoreconfHook
34+
autoconf
35+
automake
36+
libtool
37+
which
38+
];
39+
40+
configureFlags = lib.filter
41+
(flag: flag != "--disable-internal-jimtcl")
42+
(old.configureFlags or [ ])
43+
++ [
44+
"--disable-werror"
45+
"--enable-stlink"
46+
"--enable-cmsis-dap"
47+
"--enable-internal-jimtcl"
48+
];
49+
50+
preConfigure = ''
51+
export PATH=$PWD/.nix-wrappers:$PATH
52+
mkdir -p .nix-wrappers
53+
if command -v libtoolize >/dev/null && ! command -v glibtoolize >/dev/null; then
54+
ln -s "$(command -v libtoolize)" .nix-wrappers/glibtoolize || true
55+
fi
56+
if [ -x ./bootstrap ]; then ./bootstrap; fi
57+
if [ -x ./autogen.sh ]; then ./autogen.sh; fi
58+
'';
59+
60+
postInstall = (old.postInstall or "") + ''
61+
test -f "$out/share/openocd/scripts/target/stm32n6x.cfg"
62+
'';
63+
64+
meta = old.meta // {
65+
description = "OpenOCD snapshot with native ST-LINK and STM32N6 target support";
66+
homepage = "https://openocd.org/";
67+
license = lib.licenses.gpl2Plus;
68+
};
69+
})

nix/util.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ rec {
108108
s2n_bignum = pkgs.callPackage ./s2n_bignum { };
109109
slothy = pkgs.callPackage ./slothy { };
110110
pqmx = pkgs.callPackage ./pqmx { };
111+
nucleo-n657x0-q = pkgs.callPackage ./nucleo-n657x0-q { };
112+
st-openocd = pkgs.callPackage ./st-openocd { };
111113
avr-toolchain = pkgs.callPackage ./avr { };
112114

113115
# Helper function to build individual cross toolchains

scripts/tests

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,10 @@ class Tests:
759759

760760
if resultss is None:
761761
self.check_fail()
762+
return
763+
764+
if len(self.failed) > 0:
765+
self.check_fail()
762766

763767
# NOTE: There will only be one items in resultss, as we haven't yet decided how to write both opt/no-opt benchmark results
764768
for k, results in resultss.items():
@@ -1355,8 +1359,8 @@ def cli():
13551359
bench_parser.add_argument(
13561360
"-c",
13571361
"--cycles",
1358-
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.",
1359-
choices=["NO", "PMU", "PERF", "MAC"],
1362+
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.",
1363+
choices=["NO", "CYCCNT", "PMU", "PERF", "MAC"],
13601364
type=str.upper,
13611365
required=True,
13621366
)

0 commit comments

Comments
 (0)