diff --git a/ports/risc-v64/gnu/src/tx_thread_context_restore.S b/ports/risc-v64/gnu/src/tx_thread_context_restore.S index 6ffb541e7..f5a68d2ec 100644 --- a/ports/risc-v64/gnu/src/tx_thread_context_restore.S +++ b/ports/risc-v64/gnu/src/tx_thread_context_restore.S @@ -90,6 +90,16 @@ _tx_thread_context_restore: /* Just recover the saved registers and return to the point of interrupt. */ + /* Recover the FP state only when the saved mstatus.FS was not Off. The + save side skips the FP stores for a thread that has not touched the + unit, so those slots hold nothing worth loading back. */ +#if (__riscv_flen == 32) || (__riscv_flen == 64) + ld t1, 29*8(sp) // Pickup saved mstatus + srli t1, t1, 13 + andi t1, t1, 0x3 + beqz t1, _tx_thread_skip_fp_restore // Skip if FS was Off +#endif + /* Recover floating point registers. */ #if (__riscv_flen == 32) flw f0, 31*8(sp) // Recover ft0 @@ -152,6 +162,7 @@ _tx_thread_context_restore: ld t0, 63*8(sp) // Recover fcsr csrw fcsr, t0 #endif +_tx_thread_skip_fp_restore: #if defined(__riscv_vector) /* Recover vector registers v0-v31 */ @@ -297,6 +308,16 @@ _tx_thread_no_preempt_restore: ld sp, 8(t1) // Switch back to thread's stack + /* Recover the FP state only when the saved mstatus.FS was not Off. The + save side skips the FP stores for a thread that has not touched the + unit, so those slots hold nothing worth loading back. */ +#if (__riscv_flen == 32) || (__riscv_flen == 64) + ld t3, 29*8(sp) // Pickup saved mstatus + srli t3, t3, 13 + andi t3, t3, 0x3 + beqz t3, _tx_thread_no_preempt_skip_fp_restore // Skip if FS was Off +#endif + /* Recover floating point registers. */ #if (__riscv_flen == 32) flw f0, 31*8(sp) // Recover ft0 @@ -359,6 +380,7 @@ _tx_thread_no_preempt_restore: ld t0, 63*8(sp) // Recover fcsr csrw fcsr, t0 // Restore fcsr #endif +_tx_thread_no_preempt_skip_fp_restore: #if defined(__riscv_vector) /* Recover vector registers v0-v31 */ diff --git a/ports/risc-v64/gnu/src/tx_thread_context_save.S b/ports/risc-v64/gnu/src/tx_thread_context_save.S index d8526fd98..d85833b37 100644 --- a/ports/risc-v64/gnu/src/tx_thread_context_save.S +++ b/ports/risc-v64/gnu/src/tx_thread_context_save.S @@ -33,6 +33,7 @@ /* AUTHOR */ /* */ /* Scott Larson, Microsoft Corporation */ +/* Wei-Chen Lai, National Cheng Kung University */ /* */ /* DESCRIPTION */ /* */ @@ -104,6 +105,15 @@ _tx_thread_context_save: #endif sd t0, 30*8(sp) // Save it on the stack + /* Save mstatus and skip FP state if FS is Off. */ + csrr t0, mstatus + sd t0, 29*8(sp) // Save mstatus +#if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double) + srli t1, t0, 13 + andi t1, t1, 0x3 + beqz t1, _tx_thread_skip_nested_fpu_save // Skip if FS was Off +#endif + /* Save floating point scratch registers if floating point is enabled. */ #if (__riscv_flen == 32) fsw f0, 31*8(sp) // Store ft0 @@ -166,6 +176,7 @@ _tx_thread_context_save: csrr t0, fcsr sd t0, 63*8(sp) // Store fcsr #endif +_tx_thread_skip_nested_fpu_save: #if defined(__riscv_vector) /* Store vector registers and CSRs */ @@ -246,6 +257,15 @@ _tx_thread_not_nested_save: #endif sd t1, 30*8(sp) // Save it on the stack + /* Save mstatus and skip FP state if FS is Off. */ + csrr t1, mstatus + sd t1, 29*8(sp) // Save mstatus +#if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double) + srli t2, t1, 13 + andi t2, t2, 0x3 + beqz t2, _tx_thread_skip_fpu_save // Skip if FS was Off +#endif + /* Save floating point scratch registers if floating point is enabled. */ #if (__riscv_flen == 32) fsw f0, 31*8(sp) // Store ft0 @@ -308,6 +328,7 @@ _tx_thread_not_nested_save: csrr t0, fcsr sd t0, 63*8(sp) // Store fcsr #endif +_tx_thread_skip_fpu_save: #if defined(__riscv_vector) /* Store vector registers and CSRs */ diff --git a/ports/risc-v64/gnu/src/tx_thread_schedule.S b/ports/risc-v64/gnu/src/tx_thread_schedule.S index 85405c9e0..262207265 100644 --- a/ports/risc-v64/gnu/src/tx_thread_schedule.S +++ b/ports/risc-v64/gnu/src/tx_thread_schedule.S @@ -143,7 +143,16 @@ _tx_thread_schedule_loop: ld t2, 0(sp) // Pickup stack type beqz t2, _tx_thread_synch_return // If 0, solicited thread return - /* Determine if floating point registers need to be recovered. */ + /* Determine if floating point registers need to be recovered. This is an + interrupt frame, so _tx_thread_context_save built it and may have + skipped the FP stores when mstatus.FS was Off. Read back the same slot + it wrote rather than reloading whatever those slots happen to hold. */ +#if (__riscv_flen == 32) || (__riscv_flen == 64) + ld t1, 29*8(sp) // Pickup saved mstatus + srli t1, t1, 13 + andi t1, t1, 0x3 + beqz t1, _tx_thread_schedule_skip_fp_restore // Skip if FS was Off +#endif #if (__riscv_flen == 32) flw f0, 31*8(sp) // Recover ft0 @@ -216,6 +225,7 @@ _tx_thread_schedule_loop: ld t0, 63*8(sp) // Recover fcsr csrw fcsr, t0 // Restore fcsr #endif +_tx_thread_schedule_skip_fp_restore: #if defined(__riscv_vector) /* Recover vector registers v0-v31 */ diff --git a/ports/risc-v64/gnu/src/tx_thread_stack_build.S b/ports/risc-v64/gnu/src/tx_thread_stack_build.S index 8d478242e..47e5c995f 100644 --- a/ports/risc-v64/gnu/src/tx_thread_stack_build.S +++ b/ports/risc-v64/gnu/src/tx_thread_stack_build.S @@ -32,6 +32,7 @@ /* AUTHOR */ /* */ /* Scott Larson, Microsoft Corporation */ +/* Wei-Chen Lai, National Cheng Kung University */ /* */ /* DESCRIPTION */ /* */ @@ -94,7 +95,7 @@ _tx_thread_stack_build: x11 26 Initial a1 x10 27 Initial a0 x1 28 Initial ra - -- 29 reserved + mstatus 29 Initial mstatus mepc 30 Initial mepc If floating point support: f0 31 Inital ft0 @@ -193,6 +194,8 @@ If vector extension support: sd zero, 26*8(t0) // Initial a1 sd zero, 27*8(t0) // Initial a0 sd zero, 28*8(t0) // Initial ra + li t1, 0x2000 // mstatus.FS = Initial + sd t1, 29*8(t0) // Initial mstatus sd a1, 30*8(t0) // Initial mepc/sepc (thread entry point) #if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double) sd zero, 31*8(t0) // Initial ft0 diff --git a/test/tx/cmake/riscv/regression/CMakeLists.txt b/test/tx/cmake/riscv/regression/CMakeLists.txt index 80b6e3bdb..3db2eeec5 100644 --- a/test/tx/cmake/riscv/regression/CMakeLists.txt +++ b/test/tx/cmake/riscv/regression/CMakeLists.txt @@ -115,6 +115,14 @@ set(regression_test_cases # interrupt_save on RISC-V. ) +# Architecture specific tests living next to this file. The RV32 stack +# builder still leaves the mstatus slot of the frame unwritten, so the new +# thread FPU state test only applies to RV64 for now. +if(THREADX_ARCH STREQUAL "risc-v64") + list(APPEND regression_test_cases + ${CMAKE_CURRENT_LIST_DIR}/threadx_riscv_new_thread_fpu_state_test.c) +endif() + # Tests that provide their own main() and must NOT link with testcontrol. set(standalone_test_cases ${SOURCE_DIR}/threadx_initialize_kernel_setup_test.c diff --git a/test/tx/cmake/riscv/regression/threadx_riscv_new_thread_fpu_state_test.c b/test/tx/cmake/riscv/regression/threadx_riscv_new_thread_fpu_state_test.c new file mode 100644 index 000000000..b388ad621 --- /dev/null +++ b/test/tx/cmake/riscv/regression/threadx_riscv_new_thread_fpu_state_test.c @@ -0,0 +1,278 @@ +/***************************************************************************/ +/* 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 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 */ +/***************************************************************************/ + +/* This test is designed to verify that a thread created after another + thread has used the floating point registers starts with a clean + floating point state. + + The interrupt stack frame reserves slot 29 for mstatus, and the context + restore path uses the FS field of that slot to decide whether the saved + floating point registers have to be recovered. When the stack builder + leaves the slot untouched, the decision is made on whatever the thread + stack happened to contain, so the new thread can silently inherit the + floating point registers of the thread that ran before it. + + Thread 0 fills every floating point register with a non-zero value and + then creates thread 1. The test checks that the stack builder wrote the + mstatus slot at all, that the slot describes a live floating point state, + and that thread 1 observes all floating point registers as zero once it + preempts thread 0. + + An unwritten slot still holds the stack fill pattern, so it is detected + by comparing the slot against a stack word the builder never touches. */ + +#include +#include "tx_api.h" + + +/* The mstatus slot of the interrupt stack frame. */ + +#define FRAME_MSTATUS_INDEX 29 + + +#if defined(__riscv_float_abi_double) + +#define FP_SUPPORTED 1 +#define FP_LOAD "fld" +#define FP_STORE "fsd" +#define FP_WORD_SIZE "8" +typedef double FP_WORD; + +#elif defined(__riscv_float_abi_single) + +#define FP_SUPPORTED 1 +#define FP_LOAD "flw" +#define FP_STORE "fsw" +#define FP_WORD_SIZE "4" +typedef float FP_WORD; + +#else + +#define FP_SUPPORTED 0 + +#endif + + +#if FP_SUPPORTED + +/* Walk f0-f31 in register number order so that the index is both the + register number and the slot in the state buffers. */ + +#define FP_REG_LIST(op) \ + op(ft0, 0) op(ft1, 1) op(ft2, 2) op(ft3, 3) \ + op(ft4, 4) op(ft5, 5) op(ft6, 6) op(ft7, 7) \ + op(fs0, 8) op(fs1, 9) op(fa0, 10) op(fa1, 11) \ + op(fa2, 12) op(fa3, 13) op(fa4, 14) op(fa5, 15) \ + op(fa6, 16) op(fa7, 17) op(fs2, 18) op(fs3, 19) \ + op(fs4, 20) op(fs5, 21) op(fs6, 22) op(fs7, 23) \ + op(fs8, 24) op(fs9, 25) op(fs10, 26) op(fs11, 27) \ + op(ft8, 28) op(ft9, 29) op(ft10, 30) op(ft11, 31) + +#define FP_LOAD_ONE(reg, index) FP_LOAD " " #reg ", " #index "*" FP_WORD_SIZE "(%0)\n" +#define FP_STORE_ONE(reg, index) FP_STORE " " #reg ", " #index "*" FP_WORD_SIZE "(%0)\n" +#define FP_CLOBBER_ONE(reg, index) #reg, + +#define FP_REGISTERS 32 + +static FP_WORD dirty_pattern[FP_REGISTERS]; +static FP_WORD thread_1_fp_state[FP_REGISTERS]; +static unsigned long thread_1_fp_dirty = 0; + +#endif + + +static TX_THREAD thread_0; +static TX_THREAD thread_1; + +/* Give thread 1 a private stack so it can be cleared before the thread is + created. */ + +static unsigned long thread_1_stack[TEST_STACK_SIZE_PRINTF/sizeof(unsigned long)]; + +static unsigned long thread_1_executed = 0; + + +/* Define thread prototypes. */ + +static void thread_0_entry(ULONG thread_input); +static void thread_1_entry(ULONG thread_input); + + +/* Prototype for test control return. */ +void test_control_return(UINT status); + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_riscv_new_thread_fpu_state_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + + /* Put first available memory address into a character pointer. */ + pointer = (CHAR *) first_unused_memory; + + status = tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + 16, 16, TX_NO_TIME_SLICE, TX_AUTO_START); + + /* Check for status. */ + if (status != TX_SUCCESS) + { + + printf("Running RISC-V New Thread FPU State Test............................ ERROR #1\n"); + test_control_return(1); + } +} + + + +/* Define the test threads. */ + +static void thread_0_entry(ULONG thread_input) +{ + +UINT status; +unsigned long *frame; +unsigned long stack_fill; +unsigned long frame_fs; +#if FP_SUPPORTED +UINT i; +#endif + + + /* Inform user. */ + printf("Running RISC-V New Thread FPU State Test............................ "); + +#if FP_SUPPORTED + + /* Build a pattern that is non-zero in every register. */ + for (i = 0; i < FP_REGISTERS; i++) + { + + dirty_pattern[i] = (FP_WORD) (i + 1); + } + + /* Make the floating point state of this thread dirty. */ + __asm__ volatile (FP_REG_LIST(FP_LOAD_ONE) + : + : "r" (dirty_pattern) + : FP_REG_LIST(FP_CLOBBER_ONE) "memory"); +#endif + + /* Create thread 1 without starting it, so the built frame can be + inspected before the thread is scheduled. */ + status = tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0, + thread_1_stack, sizeof(thread_1_stack), + 15, 15, TX_NO_TIME_SLICE, TX_DONT_START); + + /* Check for status. */ + if (status != TX_SUCCESS) + { + + printf("ERROR #2\n"); + test_control_return(1); + } + + /* The bottom of the stack is never part of the frame, so it still holds + the fill pattern that thread creation left behind. */ + frame = (unsigned long *) thread_1.tx_thread_stack_ptr; + stack_fill = thread_1_stack[0]; + + /* The stack builder must have written the mstatus slot. */ + if (frame[FRAME_MSTATUS_INDEX] == stack_fill) + { + + printf("ERROR #3\n"); + test_control_return(1); + } + + /* The built frame must describe a live floating point state. */ + frame_fs = (frame[FRAME_MSTATUS_INDEX] >> 13) & 0x3UL; + + if (frame_fs == 0) + { + + printf("ERROR #4\n"); + test_control_return(1); + } + + /* Let thread 1 preempt and run to completion. */ + status = tx_thread_resume(&thread_1); + + /* Check for status. */ + if ((status != TX_SUCCESS) || (thread_1_executed != 1)) + { + + printf("ERROR #5\n"); + test_control_return(1); + } + +#if FP_SUPPORTED + + /* Thread 1 must not have inherited the floating point registers of + thread 0. */ + if (thread_1_fp_dirty) + { + + printf("ERROR #6\n"); + test_control_return(1); + } +#endif + + /* Successful test. */ + printf("SUCCESS!\n"); + test_control_return(0); +} + + +static void thread_1_entry(ULONG thread_input) +{ + +#if FP_SUPPORTED + +const unsigned char *state_bytes; +UINT i; + + + /* Capture the floating point registers before anything else can use + them. */ + __asm__ volatile (FP_REG_LIST(FP_STORE_ONE) + : + : "r" (thread_1_fp_state) + : "memory"); + + state_bytes = (const unsigned char *) thread_1_fp_state; + + for (i = 0; i < (UINT) sizeof(thread_1_fp_state); i++) + { + + if (state_bytes[i] != 0) + { + + thread_1_fp_dirty = 1; + break; + } + } +#endif + + thread_1_executed = 1; +}