From 0e34d094b9016c26e8cdd5aa989b2191e0fb8b01 Mon Sep 17 00:00:00 2001 From: Brendan Moran Date: Thu, 9 Jul 2026 07:55:28 +0100 Subject: [PATCH] zephyr: fix NUCLEO benchmark timing and TCM layout Use Zephyr's 64-bit cycle counter so long benchmark windows do not wrap, and make benchmark percentile sorting robust for large uint64_t values. Link NUCLEO-N657X0-Q Zephyr images as XIP from ITCM with runtime data and stacks in DTCM. Force the STM32N6 VTOR setup to the ITCM vector table and scrub the expanded DTCM window in the early reset hook. Keep project CFLAGS out of Zephyr's cached global CMake flags, and print the Zephyr build log when a suppressed Ninja build fails. Tested: - nix develop .#zephyr --command make run_func EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=mps3-an547 CYCLES=NO -j1 - nix develop .#zephyr --command make run_bench EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q CYCLES=NO -j1 Signed-off-by: Brendan Moran --- test/bench/bench_components_mlkem.c | 5 ++++- test/bench/bench_mlkem.c | 11 +++++++---- test/hal/hal.c | 2 +- test/zephyr/app/CMakeLists.txt | 8 ++++++++ test/zephyr/app/nucleo_n657x0_q.conf | 16 ++++++++++------ test/zephyr/app/nucleo_n657x0_q.overlay | 15 ++++++++++++++- test/zephyr/app/shim_nucleo_n657x0_q.c | 22 ++++++++++++++++++++++ test/zephyr/nucleo_n657x0_q/README.md | 21 +++++++++------------ test/zephyr/platform.mk | 14 ++++++++++++-- 9 files changed, 87 insertions(+), 27 deletions(-) diff --git a/test/bench/bench_components_mlkem.c b/test/bench/bench_components_mlkem.c index e8e064fad..b50e3eaac 100644 --- a/test/bench/bench_components_mlkem.c +++ b/test/bench/bench_components_mlkem.c @@ -32,7 +32,10 @@ static int cmp_uint64_t(const void *a, const void *b) { - return (int)((*((const uint64_t *)a)) - (*((const uint64_t *)b))); + const uint64_t va = *((const uint64_t *)a); + const uint64_t vb = *((const uint64_t *)b); + + return (va > vb) - (va < vb); } #define CHECK(x) \ diff --git a/test/bench/bench_mlkem.c b/test/bench/bench_mlkem.c index a74509543..7fc54638d 100644 --- a/test/bench/bench_mlkem.c +++ b/test/bench/bench_mlkem.c @@ -37,7 +37,10 @@ static int cmp_uint64_t(const void *a, const void *b) { - return (int)((*((const uint64_t *)a)) - (*((const uint64_t *)b))); + const uint64_t va = *((const uint64_t *)a); + const uint64_t vb = *((const uint64_t *)b); + + return (va > vb) - (va < vb); } static void print_median(const char *txt, uint64_t cyc[MLK_BENCHMARK_NTESTS]) @@ -54,7 +57,7 @@ static void print_percentile_legend(void) printf("%21s", "percentile"); for (i = 0; i < sizeof(percentiles) / sizeof(percentiles[0]); i++) { - printf("%9d", percentiles[i]); + printf("%12d", percentiles[i]); } printf("\n"); } @@ -66,8 +69,8 @@ static void print_percentiles(const char *txt, printf("%10s percentiles:", txt); for (i = 0; i < sizeof(percentiles) / sizeof(percentiles[0]); i++) { - printf("%9" PRIu64, (cyc)[MLK_BENCHMARK_NTESTS * percentiles[i] / 100] / - MLK_BENCHMARK_NITERATIONS); + printf("%12" PRIu64, (cyc)[MLK_BENCHMARK_NTESTS * percentiles[i] / 100] / + MLK_BENCHMARK_NITERATIONS); } printf("\n"); } diff --git a/test/hal/hal.c b/test/hal/hal.c index 8b5e8c05f..4b3fbfb35 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -49,7 +49,7 @@ void enable_cyclecounter(void) {} void disable_cyclecounter(void) {} -uint64_t get_cyclecounter(void) { return k_cycle_get_32(); } +uint64_t get_cyclecounter(void) { return k_cycle_get_64(); } #elif defined(PMU_CYCLES) diff --git a/test/zephyr/app/CMakeLists.txt b/test/zephyr/app/CMakeLists.txt index 42f9cc860..0d014b81c 100644 --- a/test/zephyr/app/CMakeLists.txt +++ b/test/zephyr/app/CMakeLists.txt @@ -32,6 +32,14 @@ target_include_directories(app PRIVATE # emulator with the test's exit code; the NUCLEO-N657X0-Q hardware target needs # its own shim plus libc wrappers that route stdio over ITM/SWO. if(ZEPHYR_NUCLEO_N657X0_Q) + # The STM32N6 SystemInit() fallback derives VTOR from &g_pfnVectors. With + # Zephyr data in DTCM that symbol lives in RAM, so force VTOR to the ITCM + # vector table instead. + zephyr_compile_definitions( + USER_VECT_TAB_ADDRESS + VECT_TAB_BASE_ADDRESS=0x10000000 + VECT_TAB_OFFSET=0x0 + ) target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/shim_nucleo_n657x0_q.c) target_compile_definitions(app PRIVATE printf=__wrap_printf diff --git a/test/zephyr/app/nucleo_n657x0_q.conf b/test/zephyr/app/nucleo_n657x0_q.conf index 16e60e76d..cadbec4de 100644 --- a/test/zephyr/app/nucleo_n657x0_q.conf +++ b/test/zephyr/app/nucleo_n657x0_q.conf @@ -1,12 +1,16 @@ # Copyright (c) The mlkem-native project authors # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT -# The board default exposes a 2 MiB SRAM window at 0x34000000; its first -# 400 KiB is FLEXRAM. The NUCLEO runner repurposes FLEXMEM to expand the TCMs -# to 256 KiB ITCM + 256 KiB DTCM, so keep the Zephyr image above that low -# window and leave fixed host buffers after the linked image. -CONFIG_SRAM_BASE_ADDRESS=0x34080000 -CONFIG_SRAM_SIZE=192 +# The NUCLEO runner repurposes FLEXMEM before loading the ELF, expanding the +# Cortex-M55 TCMs to 256 KiB ITCM + 256 KiB DTCM. Link the RAM-loaded Zephyr +# image as XIP from ITCM and keep runtime data/stacks in DTCM. The fixed +# bootargs handoff block remains in AXISRAM at 0x340b0000. +CONFIG_XIP=y +CONFIG_FLASH_BASE_ADDRESS=0x10000000 +CONFIG_FLASH_SIZE=256 +CONFIG_SRAM_BASE_ADDRESS=0x30000000 +CONFIG_SRAM_SIZE=256 +CONFIG_SOC_EARLY_RESET_HOOK=y # The component benchmark app keeps five 8 KiB scratch buffers on the main # stack; measured max static stack depth is about 49.5 KiB, so 32 KiB is not diff --git a/test/zephyr/app/nucleo_n657x0_q.overlay b/test/zephyr/app/nucleo_n657x0_q.overlay index 15d78f7a9..8596beaf0 100644 --- a/test/zephyr/app/nucleo_n657x0_q.overlay +++ b/test/zephyr/app/nucleo_n657x0_q.overlay @@ -4,6 +4,18 @@ */ / { + mlk_itcm: memory@10000000 { + compatible = "zephyr,memory-region", "arm,itcm"; + reg = <0x10000000 0x00040000>; + zephyr,memory-region = "ITCM"; + }; + + mlk_dtcm: memory@30000000 { + compatible = "zephyr,memory-region", "arm,dtcm"; + reg = <0x30000000 0x00040000>; + zephyr,memory-region = "DTCM"; + }; + mlk_axisram: memory@34080000 { compatible = "zephyr,memory-region", "mmio-sram"; reg = <0x34080000 0x00030000>; @@ -11,6 +23,7 @@ }; chosen { - zephyr,sram = &mlk_axisram; + zephyr,flash = &mlk_itcm; + zephyr,sram = &mlk_dtcm; }; }; diff --git a/test/zephyr/app/shim_nucleo_n657x0_q.c b/test/zephyr/app/shim_nucleo_n657x0_q.c index 640c9c37d..426f1a339 100644 --- a/test/zephyr/app/shim_nucleo_n657x0_q.c +++ b/test/zephyr/app/shim_nucleo_n657x0_q.c @@ -50,6 +50,28 @@ static char nucleo_stdout_buf[8192]; static size_t nucleo_stdout_len; static bool nucleo_swo_enabled; +/* + * Zephyr calls this before stack setup or normal RAM initialization. The + * NUCLEO runner has already expanded DTCM to 256 KiB, and the image now keeps + * runtime data/stacks there, so scrub the whole DTCM window before Zephyr's + * early stack, BSS, and MPU setup touch it. + */ +__attribute__((naked, used)) void soc_early_reset_hook(void) +{ + __asm__ volatile( + "movs r2, #0\n" + "movs r3, #0\n" + "ldr r0, =0x30000000\n" + "ldr r1, =0x30040000\n" + "1:\n" + "strd r2, r3, [r0], #8\n" + "cmp r0, r1\n" + "blo 1b\n" + "dsb\n" + "isb\n" + "bx lr\n"); +} + static void nucleo_swo_pin_setup(void) { LL_AHB4_GRP1_EnableClock(LL_AHB4_GRP1_PERIPH_GPIOB); diff --git a/test/zephyr/nucleo_n657x0_q/README.md b/test/zephyr/nucleo_n657x0_q/README.md index c9785cb16..51df6aea3 100644 --- a/test/zephyr/nucleo_n657x0_q/README.md +++ b/test/zephyr/nucleo_n657x0_q/README.md @@ -82,8 +82,9 @@ Zephyr's default NUCLEO-N657X0-Q RAM region is a 2 MiB SRAM window starting at `0x34000000`; its first 400 KiB is the STM32N657X0 FLEXRAM allocation. After reset, the Cortex-M55 TCM layout is 64 KiB ITCM and 128 KiB DTCM. The NUCLEO runner repurposes FLEXMEM to expand the TCMs to 256 KiB ITCM and 256 KiB DTCM -before each RAM-loaded Zephyr test, so the test image is linked above that low -AXISRAM/FLEXRAM window instead of using the board default: +before each RAM-loaded Zephyr test. The test image is then linked as XIP from +ITCM at `0x10000000`, with runtime data and stacks in DTCM at `0x30000000`; +the fixed bootargs handoff block remains in AXISRAM at `0x340b0000`. 1. OpenOCD attaches with `reset_config none`. 2. The script enables the SYSCFG clock by setting bit 0 in `RCC_APB4ENSR2` at @@ -95,16 +96,12 @@ AXISRAM/FLEXRAM window instead of using the board default: 6. It runs `reset run` so the expanded layout is applied before the Zephyr ELF is loaded. -The board is RAM-loaded, not flashed. Zephyr's default NUCLEO-N657X0-Q RAM -region is a 2 MiB SRAM window starting at `0x34000000`; its first 400 KiB is -the STM32N657X0 FLEXRAM allocation. The hardware benchmark setup is the reason -this target uses a board-specific memory layout: before each run, the hardware -wrapper repurposes FLEXMEM to expand the Cortex-M55 TCMs by setting the low -byte of `SYSCFG_CM55TCMCR` to `0x99`, selecting 256 KiB ITCM and 256 KiB DTCM, -then resets the target so that layout is active. Because this changes the low -AXISRAM/FLEXRAM window that the default Zephyr layout would use, the test image -is instead linked in a 192 KiB AXISRAM window at `0x34080000`; see -`nucleo_n657x0_q/README.md` for the exact register sequence. +The board is RAM-loaded, not flashed. The hardware benchmark setup is the +reason this target uses a board-specific memory layout: before each run, the +hardware wrapper repurposes FLEXMEM to expand the Cortex-M55 TCMs by setting +the low byte of `SYSCFG_CM55TCMCR` to `0x99`, selecting 256 KiB ITCM and +256 KiB DTCM, then resets the target so that layout is active. The Zephyr image +is linked as XIP from ITCM, while runtime data and stacks are placed in DTCM. Test stdout is captured over ITM stimulus port 0 through SWO. This avoids using semihosting for normal output: semihosting is blocking and each host operation diff --git a/test/zephyr/platform.mk b/test/zephyr/platform.mk index b045eccd8..3544f4458 100644 --- a/test/zephyr/platform.mk +++ b/test/zephyr/platform.mk @@ -61,6 +61,7 @@ ZEPHYR_APP := $(PLATFORM_PATH)/app ZEPHYR_BUILD_DIR := $(BUILD_DIR)/zephyr/$(ZEPHYR_TARGET) ZEPHYR_ACTIVE_TARGET := $(BUILD_DIR)/zephyr/.active-target ZEPHYR_APP_INPUTS := \ + $(PLATFORM_PATH)/platform.mk \ $(ZEPHYR_APP)/CMakeLists.txt \ $(ZEPHYR_APP)/Kconfig \ $(ZEPHYR_APP)/prj.conf \ @@ -111,12 +112,20 @@ CFLAGS += -DNTESTS_FUNC=3 -DNTESTS_KAT=100 \ # adds must not reach the Zephyr toolchain, which selects the target arch itself. ZEPHYR_TEST_CFLAGS = $(subst \",\\\",$(patsubst -Imlkem,-I$(abspath mlkem),$(CFLAGS))) # Keep make-exported project flags out of Zephyr's own CMake build; the app -# sources get those flags explicitly via ZEPHYR_TEST_CFLAGS. +# sources get those flags explicitly via ZEPHYR_TEST_CFLAGS. Also overwrite +# cached CMake global flags, so a prior polluted build directory cannot keep +# applying project warning flags to Zephyr's generated/helper targets. ZEPHYR_CMAKE_ENV := env -u CFLAGS -u CXXFLAGS -u CPPFLAGS -u LDFLAGS +ZEPHYR_CMAKE_CLEAR_FLAGS := \ + -DCMAKE_ASM_FLAGS:STRING= \ + -DCMAKE_C_FLAGS:STRING= \ + -DCMAKE_CXX_FLAGS:STRING= \ + -DCMAKE_EXE_LINKER_FLAGS:STRING= CUSTOM_BUILD = \ echo " ZEPHYR $(ZEPHYR_TARGET): $(notdir $@)" && \ $(ZEPHYR_CMAKE_ENV) cmake -GNinja -S $(ZEPHYR_APP) -B $(ZEPHYR_OUT) \ + $(ZEPHYR_CMAKE_CLEAR_FLAGS) \ -DBOARD=$(ZEPHYR_BOARD) \ -DZEPHYR_NATIVE_ROOT=$(CURDIR) \ -DZEPHYR_TEST_SRCS="$(strip $(TEST_SRCS))" \ @@ -126,7 +135,8 @@ CUSTOM_BUILD = \ $(ZEPHYR_TARGET_CMAKE_ARGS) \ -DUSER_CACHE_DIR=$(abspath $(ZEPHYR_OUT)/.cache) \ >/dev/null && \ - $(ZEPHYR_CMAKE_ENV) cmake --build $(ZEPHYR_OUT) >/dev/null && \ + $(ZEPHYR_CMAKE_ENV) cmake --build $(ZEPHYR_OUT) >$(ZEPHYR_OUT)/build.log 2>&1 || \ + { cat $(ZEPHYR_OUT)/build.log; exit 1; }; \ cp $(ZEPHYR_OUT)/zephyr/zephyr.elf $@ # A custom build links the test sources directly rather than from objects, so