summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrédéric Desbiens <[email protected]>2026-08-13 17:07:36 -0400
committerGitHub <[email protected]>2026-08-13 17:07:36 -0400
commitb877305d13ea2a45d40cb86416adc24938bbe154 (patch)
treee76a9f414c2e424bccb67bd27b073e56cef7d0e8
parentc991c9e6aebc32a99667ca2941d5b839f5b2b1cd (diff)
Verified lazy VFP context switching on S32Z280 silicon (#609)
The FVP proved the lazy save and restore path at AR1/M5, but the board had never run it. Doing so needed one thing the model did not: the FPU has to be turned on. entry.S opens CPACR for CP10/CP11 and sets FPEXC.EN at EL1, guarded by __ARM_FP. EL2 already cleared HCPTR.TCP10/TCP11, but both of the EL1 gates read 0 out of reset, so a floating-point instruction raised an Undefined Instruction exception before this. tx_thread_vfp_enable() does not help: it sets the per-thread software flag that makes the context switch save and restore the registers, and never touches the hardware. Enabling it is the BSP's job. The __ARM_FP guard is what keeps a soft-float build assemblable, since "vmsr fpexc" is not a valid instruction for that target at all. demo_vfp_s32z280.c is the FVP's demo_m5.c test design over the LINFlexD console. The design is kept deliberately, because the two halves of the VFP context path need separate provocation: "fp check" holds eight live doubles across tx_thread_sleep, eight being enough to force the callee-saved D8-D15 bank that a solicited switch must preserve, while "fp busy" sits at the lowest priority and never sleeps, so the tick interrupts it mid-computation and exercises the interrupt half, D0-D15 plus FPSCR. Only these two threads opt in, so the test also shows that opting in is what does the work. s32z280_vfp.elf is gated on TX_R52_ENABLE_VFP, as demo_m5.elf is in the FVP example, since the image is meaningless unless the library was built with a floating-point ABI. Also made this example's -Wl,--no-warn-rwx-segments conditional on the compiler being GNU. #604 did that for the FVP example and this file still carried the literal, so ld.lld failed the link with "unknown argument". With that fixed the S32Z280 images build with Arm Toolchain for Embedded too. Verified on S32Z280 silicon: V2 D8-D15 bank preserved across switches PASS 50 solicited switches iterations = 0x000140A8 82,088 interrupted rounds corruptions = 0x00000000 V3 interrupted FP thread made progress PASS V4 no FP corruption across interrupts PASS filex_ptr = 0xF11EF11E V5 VFP flag did not alias filex_ptr PASS PASS lazy VFP context switch verified on silicon No regression: the soft-float build is warning-free and its image contains no vmsr at all, confirming the guard elides the block, and the existing demo still reports 100 ticks with 20 preemptions on the board. Both S32Z280 images also build with Arm Toolchain for Embedded 22.1.0, the hard-float VFP image included. No FVP file is touched. Assisted-by: Claude Code (Opus 5) <[email protected]>
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt52
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/demo_vfp_s32z280.c283
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S41
3 files changed, 374 insertions, 2 deletions
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt
index 3319d77f..017aa757 100644
--- a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt
@@ -11,6 +11,17 @@
set(EVB_DIR ${CMAKE_CURRENT_LIST_DIR})
+# A bare-metal image has one flat SRAM region and no OS page permissions; access
+# control belongs to the MPU, so an RWX segment is expected rather than a mistake.
+# GNU ld has warned about them since binutils 2.39 and takes this flag to stay
+# quiet; ld.lld does not warn and rejects the flag outright, failing the link with
+# "unknown argument". Chosen by toolchain, as in the FVP example.
+if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
+ set(EVB_LINK_QUIET_RWX -Wl,--no-warn-rwx-segments)
+else()
+ set(EVB_LINK_QUIET_RWX)
+endif()
+
# Silicon M0 -- boot verification. Deliberately does not link ThreadX: a
# failure here is then unambiguously a boot or board problem rather than a
# kernel one, which is the same reasoning as the FVP's boot_check.elf.
@@ -39,7 +50,7 @@ target_link_options(s32z280_boot.elf PRIVATE
-Wl,-Map=s32z280_boot.map
# Bare metal: one code region and one data region, with access control
# belonging to the MPU rather than to segment permissions.
- -Wl,--no-warn-rwx-segments
+ ${EVB_LINK_QUIET_RWX}
)
@@ -77,5 +88,42 @@ target_link_options(s32z280_demo.elf PRIVATE
-T${EVB_DIR}/link.lds
-nostartfiles
-Wl,-Map=s32z280_demo.map
- -Wl,--no-warn-rwx-segments
+ ${EVB_LINK_QUIET_RWX}
)
+
+# Lazy VFP context save and restore. Only meaningful when the library was built
+# with TX_R52_ENABLE_VFP and a floating-point ABI, so like the FVP's demo_m5.elf
+# the target exists only in that configuration. The FPU hardware enable lives in
+# entry.S under __ARM_FP; tx_thread_vfp_enable() sets only the per-thread flag.
+if(TX_R52_ENABLE_VFP)
+ add_executable(s32z280_vfp.elf EXCLUDE_FROM_ALL
+ ${EVB_DIR}/entry.S
+ ${EVB_DIR}/tx_initialize_low_level.S
+ ${EVB_DIR}/linflexd.c
+ ${EVB_DIR}/timer.c
+ ${EVB_DIR}/mpu.c
+ ${EVB_DIR}/gicv3.c
+ ${EVB_DIR}/irq_dispatch.c
+ ${EVB_DIR}/gic_probe.c
+ ${EVB_DIR}/cache.c
+ ${EVB_DIR}/demo_vfp_s32z280.c
+ )
+
+ target_compile_definitions(s32z280_vfp.elf PRIVATE TX_R52_USE_THREADX_IRQ=1)
+ target_compile_options(s32z280_vfp.elf PRIVATE -g)
+
+ target_link_libraries(s32z280_vfp.elf PRIVATE threadx)
+
+ target_include_directories(s32z280_vfp.elf PRIVATE
+ ${EVB_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+ )
+
+ target_link_options(s32z280_vfp.elf PRIVATE
+ -T${EVB_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=s32z280_vfp.map
+ ${EVB_LINK_QUIET_RWX}
+ )
+endif()
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_vfp_s32z280.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_vfp_s32z280.c
new file mode 100644
index 00000000..456ad319
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_vfp_s32z280.c
@@ -0,0 +1,283 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* demo_vfp_s32z280.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Lazy floating-point context save and restore on S32Z280 silicon. */
+/* */
+/* This is the FVP's demo_m5.c test design, reporting over the */
+/* LINFlexD console instead of semihosting. The design is kept */
+/* deliberately: the model established that the two halves of the VFP */
+/* context path need separate provocation, and silicon does not change */
+/* that, it only changes whether the hardware agrees. */
+/* */
+/* fp check holds eight live doubles across tx_thread_sleep. Eight */
+/* forces the compiler onto the callee-saved bank D8-D15, */
+/* which is what a SOLICITED switch must preserve. */
+/* */
+/* fp busy lowest priority and never sleeps, so the periodic tick */
+/* interrupts it mid-computation. That exercises the */
+/* INTERRUPT half, D0-D15 plus FPSCR, and it checks its */
+/* own values every iteration. */
+/* */
+/* Both call tx_thread_vfp_enable(). With lazy save and restore a */
+/* thread that has not asked for floating point does not get its */
+/* registers saved, so a test where every thread opts in would not */
+/* show whether the opt-in is what does the work. */
+/* */
+/* Every constant is an exact binary fraction and the step is 0.5, so */
+/* the comparisons are exact and no epsilon is needed. MISRA C:2012 */
+/* Dir 4.6 and Rule 13.3 would normally object to == on floating */
+/* point; here it is the point, since any inexactness means a register */
+/* was not preserved. */
+/* */
+/* What silicon adds over the model: the FPU must actually be enabled */
+/* by the BSP. entry.S opens CPACR and FPEXC.EN at EL1 under */
+/* __ARM_FP, because tx_thread_vfp_enable() sets only the per-thread */
+/* software flag and never touches the hardware. Without that, the */
+/* first D-register access after a switch takes an Undefined */
+/* Instruction exception -- which on this board was invisible until */
+/* SCTLR.TE was cleared. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "linflexd.h"
+#include "board.h"
+
+#define DEMO_STACK_SIZE 2048
+#define FILEX_PTR_SENTINEL ((void *) 0xF11EF11EUL)
+#define FP_ITERATIONS 50UL
+#define FP_STEP 0.5 /* exactly representable */
+
+static TX_THREAD thread_check;
+static TX_THREAD thread_fp_busy;
+
+static ULONG thread_check_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+static ULONG thread_fp_busy_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+static volatile ULONG busy_iterations;
+static volatile ULONG busy_corruptions;
+
+
+/* Breakpoint target for tools/read_identity.gdb. Not static and not empty so
+ an optimising build cannot discard it. */
+
+void bsp_done(void)
+{
+ __asm volatile ("nop");
+}
+
+
+static UINT report(const char *label_ptr, UINT passed)
+{
+ linflexd_puts(label_ptr);
+ linflexd_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+ return (passed != 0U) ? 0U : 1U;
+}
+
+
+static void report_hex(const char *label_ptr, unsigned long value)
+{
+ linflexd_puts(label_ptr);
+ linflexd_puts(" = 0x");
+ linflexd_put_hex32((unsigned int) value);
+ linflexd_putc('\n');
+}
+
+
+/**************************************************************************/
+/* thread_fp_busy_entry -- the interrupt half of the VFP path. */
+/**************************************************************************/
+
+static void thread_fp_busy_entry(ULONG thread_input)
+{
+ (void) thread_input;
+
+ tx_thread_vfp_enable();
+
+ for (;;)
+ {
+ /* Deliberately disjoint from the checking thread's range, so that
+ state leaking from one context into the other is detectable rather
+ than coincidentally correct. */
+
+ double v0 = 1024.5;
+ double v1 = 2048.25;
+ double v2 = 4096.125;
+ double v3 = 8192.0625;
+
+ v0 = (v0 * 2.0) - 1024.5; /* 1024.5 */
+ v1 = (v1 * 2.0) - 2048.25; /* 2048.25 */
+ v2 = (v2 * 2.0) - 4096.125; /* 4096.125 */
+ v3 = (v3 * 2.0) - 8192.0625; /* 8192.0625 */
+
+ if ((v0 != 1024.5) || (v1 != 2048.25) ||
+ (v2 != 4096.125) || (v3 != 8192.0625))
+ {
+ busy_corruptions++;
+ }
+
+ busy_iterations++;
+ }
+}
+
+
+/**************************************************************************/
+/* thread_check_entry -- the solicited half, and the judge. */
+/**************************************************************************/
+
+static void thread_check_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ ULONG iteration;
+ TX_THREAD *self;
+
+ double a = 1.5;
+ double b = 2.25;
+ double c = 3.125;
+ double d = 4.0625;
+ double e = 5.03125;
+ double f = 6.015625;
+ double g = 7.0078125;
+ double h = 8.00390625;
+
+ (void) thread_input;
+
+ /* The VFP enable flag is reached from assembly by hard-coded offset. On the
+ Cortex-R4 and R5 ports that offset lands on tx_thread_filex_ptr, because
+ their TX_THREAD_EXTENSION_2 is empty. A sentinel turns that class of
+ mistake into a visible failure instead of corruption that only appears
+ once something actually uses FileX. */
+
+ self = tx_thread_identify();
+ self->tx_thread_filex_ptr = FILEX_PTR_SENTINEL;
+
+ tx_thread_vfp_enable();
+
+ linflexd_puts("V1 floating-point work across solicited switches\n");
+
+ for (iteration = 0UL; iteration < FP_ITERATIONS; iteration++)
+ {
+ /* Suspending here is the provocation: the switch must preserve all
+ eight values, and the lower-priority thread runs meanwhile. */
+
+ tx_thread_sleep(1);
+
+ a += FP_STEP;
+ b += FP_STEP;
+ c += FP_STEP;
+ d += FP_STEP;
+ e += FP_STEP;
+ f += FP_STEP;
+ g += FP_STEP;
+ h += FP_STEP;
+ }
+
+ {
+ const double delta = 0.5 * (double) FP_ITERATIONS;
+
+ failures += report("V2 D8-D15 bank preserved across switches ",
+ ((a == (1.5 + delta)) &&
+ (b == (2.25 + delta)) &&
+ (c == (3.125 + delta)) &&
+ (d == (4.0625 + delta)) &&
+ (e == (5.03125 + delta)) &&
+ (f == (6.015625 + delta)) &&
+ (g == (7.0078125 + delta)) &&
+ (h == (8.00390625 + delta))) ? 1U : 0U);
+ }
+
+ report_hex("iterations", busy_iterations);
+ report_hex("corruptions", busy_corruptions);
+
+ failures += report("V3 interrupted FP thread made progress ",
+ (busy_iterations > 0UL) ? 1U : 0U);
+ failures += report("V4 no FP corruption across interrupts ",
+ (busy_corruptions == 0UL) ? 1U : 0U);
+
+ report_hex("filex_ptr", (unsigned long) self->tx_thread_filex_ptr);
+
+ failures += report("V5 VFP flag did not alias filex_ptr ",
+ (self->tx_thread_filex_ptr == FILEX_PTR_SENTINEL) ? 1U : 0U);
+
+ if (failures == 0U)
+ {
+ linflexd_puts("PASS lazy VFP context switch verified on silicon\n");
+ }
+ else
+ {
+ linflexd_puts("FAIL lazy VFP context switch\n");
+ }
+
+ linflexd_puts("=== vfp demo complete ===\n");
+
+ bsp_done();
+
+ for (;;)
+ {
+ tx_thread_sleep(100UL);
+ }
+}
+
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ (void) tx_thread_create(&thread_check, "fp check", thread_check_entry, 0UL,
+ thread_check_stack, sizeof(thread_check_stack),
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&thread_fp_busy, "fp busy", thread_fp_busy_entry,
+ 0UL, thread_fp_busy_stack,
+ sizeof(thread_fp_busy_stack),
+ 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S; hands over to the kernel. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ unsigned int console_status = linflexd_init();
+
+ linflexd_puts("\n=== ThreadX Cortex-R52 :: S32Z280-594EVB lazy VFP ===\n");
+ report_hex("console", (unsigned long) console_status);
+ linflexd_puts("entering kernel\n");
+
+ tx_kernel_enter();
+
+ /* Not reached. */
+
+ linflexd_puts("FAIL tx_kernel_enter returned\n");
+
+ for (;;)
+ {
+ __asm volatile ("nop");
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S b/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S
index a5b34be7..bb90d000 100644
--- a/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S
@@ -66,6 +66,13 @@
.equ HCPTR_TCP, (3 << 10) /* trap EL1 access to CP10/CP11 */
+/* CPACR grants EL1/EL0 access to CP10 and CP11, the floating-point unit;
+ FPEXC.EN then enables floating-point execution itself. */
+
+ .equ CPACR_CP10, (3 << 20)
+ .equ CPACR_CP11, (3 << 22)
+ .equ FPEXC_EN, (1 << 30)
+
.equ CNTHCTL_PL1PCTEN, (1 << 0)
.equ CNTHCTL_PL1PCEN, (1 << 1)
@@ -345,6 +352,40 @@ el1_entry:
mcr p15, 0, r0, c12, c0, 0 /* VBAR */
isb
+#ifdef __ARM_FP
+
+ /* Enable the floating-point unit for EL1.
+ *
+ * Two gates must be opened and both reset closed:
+ * CPACR grants EL1/EL0 access to CP10/CP11. It reads 0 out of reset,
+ * so the first floating-point instruction would otherwise raise
+ * an Undefined Instruction exception.
+ * FPEXC .EN enables floating-point execution itself.
+ * EL2 has already cleared HCPTR.TCP10/TCP11 above, so neither traps to EL2.
+ *
+ * This is the BSP's job, not the kernel's. tx_thread_vfp_enable() only sets
+ * the per-thread flag that makes the context switch save and restore
+ * floating-point state; it does not turn the hardware on. Without the code
+ * below, a VFP build faults on the first context switch that touches a
+ * D-register -- and on this silicon that fault was invisible until
+ * SCTLR.TE was cleared, which is the trap described further down.
+ *
+ * Guarded by __ARM_FP, which the compiler defines exactly when the build can
+ * emit floating-point instructions. In a soft-float build "vmsr fpexc" is
+ * not assemblable for this target at all.
+ */
+
+ mrc p15, 0, r0, c1, c0, 2 /* CPACR */
+ orr r0, r0, #(CPACR_CP10 | CPACR_CP11)
+ mcr p15, 0, r0, c1, c0, 2
+ isb
+
+ mov r0, #FPEXC_EN
+ vmsr fpexc, r0
+ isb
+
+#endif
+
/* Clear SCTLR.TE. This is not cosmetic and it cost a long detour.
*
* TE resets SET on this silicon -- SCTLR reads 0x70C50838, bit 30 high --