From 628af826078694096502d036e079fec96ac7d016 Mon Sep 17 00:00:00 2001 From: Wei-Chen Lai Date: Tue, 9 Jun 2026 16:47:23 +0800 Subject: [PATCH 1/5] add lazy FPU stacking to context save/restore Save mstatus/sstatus to stack slot 29 and skip floating-point register save/restore when FS is Off (bits 14:13). This avoids unnecessary FP context work for threads that do not use the FPU. - context_save: check FS in nested and first-level interrupt paths - context_restore: gate FP restore on nested, no-preempt, and preempt paths - use sstatus when TX_RISCV_SMODE is defined, otherwise mstatus --- .../gnu/src/tx_thread_context_restore.S | 30 ++++++++++++++++--- .../risc-v64/gnu/src/tx_thread_context_save.S | 29 ++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) 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 cebc8a062..b9c799f47 100644 --- a/ports/risc-v64/gnu/src/tx_thread_context_restore.S +++ b/ports/risc-v64/gnu/src/tx_thread_context_restore.S @@ -31,6 +31,7 @@ /* AUTHOR */ /* */ /* Scott Larson, Microsoft Corporation */ +/* Wei-Chen Lai, National Cheng Kung University */ /* */ /* DESCRIPTION */ /* */ @@ -88,7 +89,13 @@ _tx_thread_context_restore: /* Just recover the saved registers and return to the point of interrupt. */ - /* Recover floating point registers. */ + /* Recover floating point registers only if saved mstatus/sstatus.FS was not Off. */ +#if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double) + ld t1, 29*8(sp) // Pickup saved mstatus/sstatus + srli t1, t1, 13 + andi t1, t1, 0x3 + beqz t1, _tx_thread_skip_fp_restore // Skip if FS was Off +#endif #if defined(__riscv_float_abi_single) flw f0, 31*8(sp) // Recover ft0 flw f1, 32*8(sp) // Recover ft1 @@ -134,8 +141,9 @@ _tx_thread_context_restore: fld f30,61*8(sp) // Recover ft10 fld f31,62*8(sp) // Recover ft11 ld t0, 63*8(sp) // Recover fcsr - csrw fcsr, t0 + csrw fcsr, t0 // Restore fcsr #endif +_tx_thread_skip_fp_restore: #if defined(__riscv_vector) /* Recover vector registers v0-v31 */ @@ -285,7 +293,13 @@ _tx_thread_no_preempt_restore: ld sp, 8(t1) // Switch back to thread's stack - /* Recover floating point registers. */ + /* Recover floating point registers only if saved mstatus/sstatus.FS was not Off. */ +#if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double) + ld t3, 29*8(sp) // Pickup saved mstatus/sstatus + srli t3, t3, 13 + andi t3, t3, 0x3 + beqz t3, _tx_thread_no_preempt_skip_fp_restore // Skip if FS was Off +#endif #if defined(__riscv_float_abi_single) flw f0, 31*8(sp) // Recover ft0 flw f1, 32*8(sp) // Recover ft1 @@ -333,6 +347,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 */ @@ -464,7 +479,13 @@ _tx_thread_preempt_restore: ori t3, zero, 1 // Build interrupt stack type sd t3, 0(t0) // Store stack type - /* Store floating point preserved registers. */ + /* Store floating point preserved registers only if saved mstatus/sstatus.FS was not Off. */ +#if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double) + ld t3, 29*8(t0) // Pickup saved mstatus/sstatus + srli t3, t3, 13 + andi t3, t3, 0x3 + beqz t3, _tx_thread_preempt_skip_fp_restore // Skip if FS was Off +#endif #ifdef __riscv_float_abi_single fsw f8, 39*8(t0) // Store fs0 fsw f9, 40*8(t0) // Store fs1 @@ -492,6 +513,7 @@ _tx_thread_preempt_restore: fsd f26, 57*8(t0) // Store fs10 fsd f27, 58*8(t0) // Store fs11 #endif +_tx_thread_preempt_skip_fp_restore: #if defined(__riscv_vector) /* Store vector registers and CSRs */ 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 7935bfee2..10177ee54 100644 --- a/ports/risc-v64/gnu/src/tx_thread_context_save.S +++ b/ports/risc-v64/gnu/src/tx_thread_context_save.S @@ -30,6 +30,7 @@ /* AUTHOR */ /* */ /* Scott Larson, Microsoft Corporation */ +/* Wei-Chen Lai, National Cheng Kung University */ /* */ /* DESCRIPTION */ /* */ @@ -101,6 +102,19 @@ _tx_thread_context_save: #endif sd t0, 30*8(sp) // Save it on the stack + /* Save mstatus/sstatus and skip FP state if FS is Off. */ +#ifdef TX_RISCV_SMODE + csrr t0, sstatus +#else + csrr t0, mstatus +#endif + sd t0, 29*8(sp) // Save mstatus/sstatus +#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. */ #ifdef __riscv_float_abi_single fsw f0, 31*8(sp) // Store ft0 @@ -149,6 +163,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 */ @@ -225,6 +240,19 @@ _tx_thread_not_nested_save: #endif sd t1, 30*8(sp) // Save it on the stack + /* Save mstatus/sstatus and skip FP state if FS is Off. */ +#ifdef TX_RISCV_SMODE + csrr t1, sstatus +#else + csrr t1, mstatus +#endif + sd t1, 29*8(sp) // Save mstatus/sstatus +#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. */ #ifdef __riscv_float_abi_single fsw f0, 31*8(sp) // Store ft0 @@ -273,6 +301,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 */ From 6943895a4825f45bf2cbb7bfa36b961258f98fab Mon Sep 17 00:00:00 2001 From: Wei-Chen Lai Date: Tue, 9 Jun 2026 17:56:03 +0800 Subject: [PATCH 2/5] add QEMU virt CMake build and automated test runner Wire the QEMU virt demo into the CMake build system and add a Python/GDB functional test runner, mirroring the risc-v32/gnu port. - Add qemu_virt/CMakeLists.txt to build kernel.elf and register the check-functional-riscv64 target (requires Python3; skipped if absent) - Link kernel.elf with --whole-archive so all ThreadX symbols resolve - Pin _start at 0x80000000 via .text.boot in entry.s and KEEP(*(.text.boot)) in link.lds - Extend demo_threadx.c with fpu_test_val and shorten thread_0 sleep for GDB-driven FPU, timer, and preemption checks - Add test/azrtos_test_tx_gnu_riscv64_qemu.py; verified passing on QEMU virt (FPU, timer interrupt, preemption) --- ports/risc-v64/gnu/CMakeLists.txt | 4 + .../example_build/qemu_virt/CMakeLists.txt | 49 +++ .../example_build/qemu_virt/demo_threadx.c | 8 +- .../gnu/example_build/qemu_virt/entry.S | 2 +- .../gnu/example_build/qemu_virt/link.lds | 1 + .../test/azrtos_test_tx_gnu_riscv64_qemu.py | 284 ++++++++++++++++++ 6 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 ports/risc-v64/gnu/example_build/qemu_virt/CMakeLists.txt create mode 100644 ports/risc-v64/gnu/example_build/qemu_virt/test/azrtos_test_tx_gnu_riscv64_qemu.py diff --git a/ports/risc-v64/gnu/CMakeLists.txt b/ports/risc-v64/gnu/CMakeLists.txt index e51de7355..bb1c9a996 100644 --- a/ports/risc-v64/gnu/CMakeLists.txt +++ b/ports/risc-v64/gnu/CMakeLists.txt @@ -4,3 +4,7 @@ threadx_add_riscv_port( SRC_DIR ${CMAKE_CURRENT_LIST_DIR}/src INC_DIR ${CMAKE_CURRENT_LIST_DIR}/inc ) + +if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/example_build/qemu_virt/CMakeLists.txt) + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/example_build/qemu_virt) +endif() diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/CMakeLists.txt b/ports/risc-v64/gnu/example_build/qemu_virt/CMakeLists.txt new file mode 100644 index 000000000..1a1c2fc1f --- /dev/null +++ b/ports/risc-v64/gnu/example_build/qemu_virt/CMakeLists.txt @@ -0,0 +1,49 @@ +set(QEMU_DEMO_DIR ${CMAKE_CURRENT_LIST_DIR}) + +add_executable(kernel.elf EXCLUDE_FROM_ALL + ${QEMU_DEMO_DIR}/demo_threadx.c + ${QEMU_DEMO_DIR}/entry.s + ${QEMU_DEMO_DIR}/uart.c + ${QEMU_DEMO_DIR}/plic.c + ${QEMU_DEMO_DIR}/hwtimer.c + ${QEMU_DEMO_DIR}/trap.c + ${QEMU_DEMO_DIR}/board.c + ${QEMU_DEMO_DIR}/tx_initialize_low_level.S +) + +target_link_libraries(kernel.elf PRIVATE + -Wl,--whole-archive + threadx + -Wl,--no-whole-archive +) + +target_include_directories(kernel.elf PRIVATE + ${CMAKE_SOURCE_DIR}/common/inc + ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc + ${QEMU_DEMO_DIR} +) + +target_link_options(kernel.elf PRIVATE + -T${QEMU_DEMO_DIR}/link.lds + -nostartfiles + -Wl,-Map=kernel.map +) + +# QEMU/GDB functional test runner. Optional: skipped silently if the +# host has no Python 3 interpreter on PATH. +find_package(Python3 COMPONENTS Interpreter) +if(Python3_FOUND) + add_custom_target(check-functional-riscv64 + COMMAND ${Python3_EXECUTABLE} + ${QEMU_DEMO_DIR}/test/azrtos_test_tx_gnu_riscv64_qemu.py + --elf $ + --qemu qemu-system-riscv64 + --gdb gdb + DEPENDS kernel.elf + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Running RISC-V64 QEMU/GDB functional test runner..." + ) +else() + message(STATUS + "Python3 not found; check-functional-riscv64 target unavailable.") +endif() diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/demo_threadx.c b/ports/risc-v64/gnu/example_build/qemu_virt/demo_threadx.c index d3195019d..7202bdffe 100644 --- a/ports/risc-v64/gnu/example_build/qemu_virt/demo_threadx.c +++ b/ports/risc-v64/gnu/example_build/qemu_virt/demo_threadx.c @@ -24,7 +24,7 @@ #endif #define DEMO_BLOCK_POOL_SIZE 100 #define DEMO_QUEUE_SIZE 100 - +float fpu_test_val = 0.0f; /* Define the ThreadX object control blocks... */ @@ -204,8 +204,8 @@ UINT status; /* Increment the thread counter. */ thread_0_counter++; - /* Sleep for 10 ticks. */ - tx_thread_sleep(10); + /* Sleep for 1 tick (shortened for QEMU/GDB functional test). */ + tx_thread_sleep(1); /* Set event flag 0 to wakeup thread 5. */ status = tx_event_flags_set(&event_flags_0, 0x1, TX_OR); @@ -357,6 +357,8 @@ UINT status; if (status != TX_SUCCESS) break; + /* FPU Test */ + fpu_test_val += 1.1f; /* Get the mutex again with suspension. This shows that an owning thread may retrieve the mutex it owns multiple times. */ diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/entry.S b/ports/risc-v64/gnu/example_build/qemu_virt/entry.S index 2b68310d4..adc618003 100644 --- a/ports/risc-v64/gnu/example_build/qemu_virt/entry.S +++ b/ports/risc-v64/gnu/example_build/qemu_virt/entry.S @@ -10,7 +10,7 @@ /***************************************************************************/ -.section .text +.section .text.boot, "ax" .align 4 .global _start .extern main diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/link.lds b/ports/risc-v64/gnu/example_build/qemu_virt/link.lds index d0d8a6bc1..1d52bc3ae 100644 --- a/ports/risc-v64/gnu/example_build/qemu_virt/link.lds +++ b/ports/risc-v64/gnu/example_build/qemu_virt/link.lds @@ -10,6 +10,7 @@ SECTIONS . = 0x80000000; .text : { + KEEP(*(.text.boot)) /* entry.s _start — must be first at 0x80000000 */ *(.text .text.*) . = ALIGN(0x1000); PROVIDE(etext = .); diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/test/azrtos_test_tx_gnu_riscv64_qemu.py b/ports/risc-v64/gnu/example_build/qemu_virt/test/azrtos_test_tx_gnu_riscv64_qemu.py new file mode 100644 index 000000000..a0902a514 --- /dev/null +++ b/ports/risc-v64/gnu/example_build/qemu_virt/test/azrtos_test_tx_gnu_riscv64_qemu.py @@ -0,0 +1,284 @@ +import subprocess +import sys +import os +import argparse +import socket +import select + +def print_content(content): + """Prints content using os.write to handle non-blocking stdout robustly.""" + try: + msg = f"{content}\n".encode('utf-8') + total_len = len(msg) + written = 0 + fd = sys.stdout.fileno() + while written < total_len: + try: + n = os.write(fd, msg[written:]) + written += n + except BlockingIOError: + select.select([], [fd], []) + except Exception: + pass + +def get_free_port(): + """Finds a free TCP port.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('', 0)) + return s.getsockname()[1] + +def run_qemu_test(elf_path, qemu_bin, gdb_bin): + """ + Runs a test cycle using QEMU and GDB. + """ + print(f"Testing ELF: {elf_path}") + print(f"QEMU: {qemu_bin}") + print(f"GDB: {gdb_bin}") + + # Find a free port for GDB connection + gdb_port = get_free_port() + print(f"Using GDB port: {gdb_port}") + + # 1. Start QEMU in the background + qemu_cmd = [ + qemu_bin, + "-M", "virt", + "-nographic", + "-bios", "none", # Disable default OpenSBI to avoid overlap at 0x80000000 + "-kernel", elf_path, + "-gdb", f"tcp::{gdb_port}", "-S", + "-monitor", "none", # Disable monitor to avoid clutter + "-serial", "stdio" # Redirect serial output to stdio so we can see it + ] + + print(f"Starting QEMU: {' '.join(qemu_cmd)}") + qemu_process = subprocess.Popen( + qemu_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + if qemu_process.poll() is not None: + print("QEMU failed to start.") + print(qemu_process.stderr.read()) + return False + + # 2. Create a GDB command file + # We use a defined command for the timer interrupt to perform the check automatically + gdb_cmds = """ +file {elf} +target remote :{port} +set pagination off +set confirm off + +# Setup Breakpoints +break tx_application_define +break thread_0_entry +break thread_6_and_7_entry +break _tx_timer_interrupt + +# Execute to Application Definition +continue + +# Inspect mstatus once thread_0 has started (FS bits should be observable; +# kept as a smoke check, the lazy-save logic itself is targeted by a +# follow-up PR). +continue +print/x $mstatus + +# Verify FPU Logic and Register State exercised by thread_6/7 +continue +finish +step +step +step +print/x $mstatus +info registers float +print fpu_test_val + +# Await Timer Interrupt +continue +print "Hit Timer Interrupt" + +# Verify MEPC Integrity - Save State +print/x $mepc +set $saved_pc = $mepc + +# Verify System Timer Before ISR +set $clock_before = _tx_timer_system_clock +print $clock_before + +# Configure Time-Slice Test Conditions +set _tx_timer_time_slice = 1 +set _tx_timer_expired_time_slice = 0 +set $ts_handler_called = 0 + +# Set Breakpoint at Time-Slice Handler with Auto-Continue +tbreak _tx_thread_time_slice +commands + set $ts_handler_called = 1 + continue +end + +# Set Breakpoint at ISR Return Address +set $ret_addr = $ra +tbreak *$ret_addr +continue + +# Verify Time-Slice Handler Was Called +if $ts_handler_called == 1 + print "SUCCESS: Time-slice handler called." +else + print "FAILURE: Time-slice handler NOT called." +end + +# Verify System Timer Increment (Monotonicity) +set $clock_after = _tx_timer_system_clock +print $clock_after + +if $clock_after > $clock_before + print "SUCCESS: System timer incremented." +else + print "FAILURE: System timer did not increment." +end + +# Verify Preemption Logic (Thread Priority) +# +# We are now stopped at the return address from _tx_timer_interrupt, +# after _tx_thread_time_slice has had a chance to update +# _tx_thread_execute_ptr but before trap_handler returns into +# _tx_thread_context_restore. At this point, a pending preemption is +# observable directly by comparing current_ptr (interrupted thread) +# and execute_ptr (thread chosen by the scheduler). +set $curr_ptr = _tx_thread_current_ptr +set $exec_ptr = _tx_thread_execute_ptr +if $curr_ptr != 0 && $exec_ptr != 0 + set $curr_prio = $curr_ptr->tx_thread_priority + set $exec_prio = $exec_ptr->tx_thread_priority + printf "PREEMPT_CHECK current_prio=%d execute_prio=%d\\n", $curr_prio, $exec_prio + if $exec_prio < $curr_prio + printf "PREEMPT_VERIFIED_OK\\n" + else + printf "PREEMPT_VERIFIED_FAIL_NOT_OBSERVED\\n" + end +else + printf "PREEMPT_VERIFIED_FAIL_NULL\\n" +end + +quit +""".format(port=gdb_port, elf=elf_path) + + gdb_cmd_file = "test_cmds.gdb" + with open(gdb_cmd_file, "w") as f: + f.write(gdb_cmds) + + # 3. Run GDB + gdb_cmd = [ + gdb_bin, + "--batch", + "-x", gdb_cmd_file + ] + + print_content(f"Starting GDB: {' '.join(gdb_cmd)}") + + # Cap the GDB session to 30 s so a wedged batch script (e.g. a + # `continue` that never hits its breakpoint) cannot hang CI. + GDB_TIMEOUT_S = 30 + + try: + gdb_process = subprocess.run( + gdb_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=GDB_TIMEOUT_S, + ) + + print_content("GDB Output:") + print_content(gdb_process.stdout) + if gdb_process.stderr: + print_content("GDB Error Output:") + print_content(gdb_process.stderr) + + except subprocess.TimeoutExpired as e: + print_content( + f"FAILURE: GDB session exceeded {GDB_TIMEOUT_S}s timeout; " + "likely stuck on a `continue` that never matched a breakpoint." + ) + if e.stdout: + print_content("GDB Output (partial):") + print_content(e.stdout if isinstance(e.stdout, str) + else e.stdout.decode(errors='replace')) + if e.stderr: + print_content("GDB Error Output (partial):") + print_content(e.stderr if isinstance(e.stderr, str) + else e.stderr.decode(errors='replace')) + return False + + except Exception as e: + print_content(f"An error occurred during test execution: {e}") + return False + + finally: + # 4. Clean up + print_content("Stopping QEMU...") + qemu_process.terminate() + try: + qemu_process.wait(timeout=2) + except subprocess.TimeoutExpired: + print_content("QEMU did not terminate gracefully, killing it forcefullly.") + qemu_process.kill() + + # Verify results + stdout = gdb_process.stdout + timer_hit = "Breakpoint 4, _tx_timer_interrupt" in stdout + fpu_verified = False + preemption_verified = "PREEMPT_VERIFIED_OK" in stdout + + if "Breakpoint 3, thread_6_and_7_entry" in stdout: + if "1.10" in stdout or "fpu_test_val" in stdout: + print_content("SUCCESS: FPU instructions executed and registers inspected.") + fpu_verified = True + else: + print_content("FAILURE: Hit thread, but failed to inspect FPU. Output does not contain expected value.") + + if timer_hit: + print_content("SUCCESS: Timer Interrupt verified! Hit _tx_timer_interrupt.") + else: + print_content("FAILURE: Did not hit timer interrupt.") + + if preemption_verified: + print_content("SUCCESS: Preemption verified (higher-priority thread " + "preempted a lower-priority one).") + else: + if "PREEMPT_VERIFIED_FAIL_INVERTED" in stdout: + print_content("FAILURE: Preemption inverted -- lower priority " + "thread scheduled over higher priority one.") + elif "PREEMPT_VERIFIED_FAIL_NULL" in stdout: + print_content("FAILURE: Preemption check saw NULL thread pointers.") + elif "PREEMPT_VERIFIED_FAIL_NOT_OBSERVED" in stdout: + print_content("FAILURE: Preemption was not observed within the " + "loop budget.") + else: + print_content("FAILURE: Preemption check did not run to completion.") + + if timer_hit and fpu_verified and preemption_verified: + return True + else: + return False + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Run ThreadX QEMU/GDB Test") + parser.add_argument("--elf", required=True, help="Path to the kernel ELF file") + parser.add_argument("--qemu", default="qemu-system-riscv64", help="Path to QEMU binary") + parser.add_argument("--gdb", default="gdb", help="Path to GDB binary") + + args = parser.parse_args() + + success = run_qemu_test(args.elf, args.qemu, args.gdb) + + if success: + sys.exit(0) + else: + sys.exit(1) From 9ec65166af8ac385d75907b6df897e4adc1241ca Mon Sep 17 00:00:00 2001 From: Wei-Chen Lai Date: Mon, 22 Jun 2026 14:51:02 +0800 Subject: [PATCH 3/5] Clean up RV64 PR scope and remove QEMU test integration leftovers Revert accidental RV64 qemu_virt test/CMake integration changes and keep this branch focused on lazy FPU context handling only. Also remove unintended TX_RISCV_SMODE-based mstatus/sstatus save path and align comments/logic to mstatus-only behavior. --- ports/risc-v64/gnu/CMakeLists.txt | 4 - .../example_build/qemu_virt/CMakeLists.txt | 49 --- .../example_build/qemu_virt/demo_threadx.c | 8 +- .../gnu/example_build/qemu_virt/entry.S | 2 +- .../gnu/example_build/qemu_virt/link.lds | 1 - .../test/azrtos_test_tx_gnu_riscv64_qemu.py | 284 ----------------- .../test/threadx_test_tx_gnu_riscv64_qemu.py | 286 ------------------ .../gnu/src/tx_thread_context_restore.S | 14 +- .../risc-v64/gnu/src/tx_thread_context_save.S | 16 +- 9 files changed, 15 insertions(+), 649 deletions(-) delete mode 100644 ports/risc-v64/gnu/example_build/qemu_virt/CMakeLists.txt delete mode 100644 ports/risc-v64/gnu/example_build/qemu_virt/test/azrtos_test_tx_gnu_riscv64_qemu.py delete mode 100644 ports/risc-v64/gnu/example_build/qemu_virt/test/threadx_test_tx_gnu_riscv64_qemu.py diff --git a/ports/risc-v64/gnu/CMakeLists.txt b/ports/risc-v64/gnu/CMakeLists.txt index bb1c9a996..e51de7355 100644 --- a/ports/risc-v64/gnu/CMakeLists.txt +++ b/ports/risc-v64/gnu/CMakeLists.txt @@ -4,7 +4,3 @@ threadx_add_riscv_port( SRC_DIR ${CMAKE_CURRENT_LIST_DIR}/src INC_DIR ${CMAKE_CURRENT_LIST_DIR}/inc ) - -if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/example_build/qemu_virt/CMakeLists.txt) - add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/example_build/qemu_virt) -endif() diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/CMakeLists.txt b/ports/risc-v64/gnu/example_build/qemu_virt/CMakeLists.txt deleted file mode 100644 index 1a1c2fc1f..000000000 --- a/ports/risc-v64/gnu/example_build/qemu_virt/CMakeLists.txt +++ /dev/null @@ -1,49 +0,0 @@ -set(QEMU_DEMO_DIR ${CMAKE_CURRENT_LIST_DIR}) - -add_executable(kernel.elf EXCLUDE_FROM_ALL - ${QEMU_DEMO_DIR}/demo_threadx.c - ${QEMU_DEMO_DIR}/entry.s - ${QEMU_DEMO_DIR}/uart.c - ${QEMU_DEMO_DIR}/plic.c - ${QEMU_DEMO_DIR}/hwtimer.c - ${QEMU_DEMO_DIR}/trap.c - ${QEMU_DEMO_DIR}/board.c - ${QEMU_DEMO_DIR}/tx_initialize_low_level.S -) - -target_link_libraries(kernel.elf PRIVATE - -Wl,--whole-archive - threadx - -Wl,--no-whole-archive -) - -target_include_directories(kernel.elf PRIVATE - ${CMAKE_SOURCE_DIR}/common/inc - ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc - ${QEMU_DEMO_DIR} -) - -target_link_options(kernel.elf PRIVATE - -T${QEMU_DEMO_DIR}/link.lds - -nostartfiles - -Wl,-Map=kernel.map -) - -# QEMU/GDB functional test runner. Optional: skipped silently if the -# host has no Python 3 interpreter on PATH. -find_package(Python3 COMPONENTS Interpreter) -if(Python3_FOUND) - add_custom_target(check-functional-riscv64 - COMMAND ${Python3_EXECUTABLE} - ${QEMU_DEMO_DIR}/test/azrtos_test_tx_gnu_riscv64_qemu.py - --elf $ - --qemu qemu-system-riscv64 - --gdb gdb - DEPENDS kernel.elf - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Running RISC-V64 QEMU/GDB functional test runner..." - ) -else() - message(STATUS - "Python3 not found; check-functional-riscv64 target unavailable.") -endif() diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/demo_threadx.c b/ports/risc-v64/gnu/example_build/qemu_virt/demo_threadx.c index 7202bdffe..d3195019d 100644 --- a/ports/risc-v64/gnu/example_build/qemu_virt/demo_threadx.c +++ b/ports/risc-v64/gnu/example_build/qemu_virt/demo_threadx.c @@ -24,7 +24,7 @@ #endif #define DEMO_BLOCK_POOL_SIZE 100 #define DEMO_QUEUE_SIZE 100 -float fpu_test_val = 0.0f; + /* Define the ThreadX object control blocks... */ @@ -204,8 +204,8 @@ UINT status; /* Increment the thread counter. */ thread_0_counter++; - /* Sleep for 1 tick (shortened for QEMU/GDB functional test). */ - tx_thread_sleep(1); + /* Sleep for 10 ticks. */ + tx_thread_sleep(10); /* Set event flag 0 to wakeup thread 5. */ status = tx_event_flags_set(&event_flags_0, 0x1, TX_OR); @@ -357,8 +357,6 @@ UINT status; if (status != TX_SUCCESS) break; - /* FPU Test */ - fpu_test_val += 1.1f; /* Get the mutex again with suspension. This shows that an owning thread may retrieve the mutex it owns multiple times. */ diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/entry.S b/ports/risc-v64/gnu/example_build/qemu_virt/entry.S index adc618003..2b68310d4 100644 --- a/ports/risc-v64/gnu/example_build/qemu_virt/entry.S +++ b/ports/risc-v64/gnu/example_build/qemu_virt/entry.S @@ -10,7 +10,7 @@ /***************************************************************************/ -.section .text.boot, "ax" +.section .text .align 4 .global _start .extern main diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/link.lds b/ports/risc-v64/gnu/example_build/qemu_virt/link.lds index 1d52bc3ae..d0d8a6bc1 100644 --- a/ports/risc-v64/gnu/example_build/qemu_virt/link.lds +++ b/ports/risc-v64/gnu/example_build/qemu_virt/link.lds @@ -10,7 +10,6 @@ SECTIONS . = 0x80000000; .text : { - KEEP(*(.text.boot)) /* entry.s _start — must be first at 0x80000000 */ *(.text .text.*) . = ALIGN(0x1000); PROVIDE(etext = .); diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/test/azrtos_test_tx_gnu_riscv64_qemu.py b/ports/risc-v64/gnu/example_build/qemu_virt/test/azrtos_test_tx_gnu_riscv64_qemu.py deleted file mode 100644 index a0902a514..000000000 --- a/ports/risc-v64/gnu/example_build/qemu_virt/test/azrtos_test_tx_gnu_riscv64_qemu.py +++ /dev/null @@ -1,284 +0,0 @@ -import subprocess -import sys -import os -import argparse -import socket -import select - -def print_content(content): - """Prints content using os.write to handle non-blocking stdout robustly.""" - try: - msg = f"{content}\n".encode('utf-8') - total_len = len(msg) - written = 0 - fd = sys.stdout.fileno() - while written < total_len: - try: - n = os.write(fd, msg[written:]) - written += n - except BlockingIOError: - select.select([], [fd], []) - except Exception: - pass - -def get_free_port(): - """Finds a free TCP port.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('', 0)) - return s.getsockname()[1] - -def run_qemu_test(elf_path, qemu_bin, gdb_bin): - """ - Runs a test cycle using QEMU and GDB. - """ - print(f"Testing ELF: {elf_path}") - print(f"QEMU: {qemu_bin}") - print(f"GDB: {gdb_bin}") - - # Find a free port for GDB connection - gdb_port = get_free_port() - print(f"Using GDB port: {gdb_port}") - - # 1. Start QEMU in the background - qemu_cmd = [ - qemu_bin, - "-M", "virt", - "-nographic", - "-bios", "none", # Disable default OpenSBI to avoid overlap at 0x80000000 - "-kernel", elf_path, - "-gdb", f"tcp::{gdb_port}", "-S", - "-monitor", "none", # Disable monitor to avoid clutter - "-serial", "stdio" # Redirect serial output to stdio so we can see it - ] - - print(f"Starting QEMU: {' '.join(qemu_cmd)}") - qemu_process = subprocess.Popen( - qemu_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - - if qemu_process.poll() is not None: - print("QEMU failed to start.") - print(qemu_process.stderr.read()) - return False - - # 2. Create a GDB command file - # We use a defined command for the timer interrupt to perform the check automatically - gdb_cmds = """ -file {elf} -target remote :{port} -set pagination off -set confirm off - -# Setup Breakpoints -break tx_application_define -break thread_0_entry -break thread_6_and_7_entry -break _tx_timer_interrupt - -# Execute to Application Definition -continue - -# Inspect mstatus once thread_0 has started (FS bits should be observable; -# kept as a smoke check, the lazy-save logic itself is targeted by a -# follow-up PR). -continue -print/x $mstatus - -# Verify FPU Logic and Register State exercised by thread_6/7 -continue -finish -step -step -step -print/x $mstatus -info registers float -print fpu_test_val - -# Await Timer Interrupt -continue -print "Hit Timer Interrupt" - -# Verify MEPC Integrity - Save State -print/x $mepc -set $saved_pc = $mepc - -# Verify System Timer Before ISR -set $clock_before = _tx_timer_system_clock -print $clock_before - -# Configure Time-Slice Test Conditions -set _tx_timer_time_slice = 1 -set _tx_timer_expired_time_slice = 0 -set $ts_handler_called = 0 - -# Set Breakpoint at Time-Slice Handler with Auto-Continue -tbreak _tx_thread_time_slice -commands - set $ts_handler_called = 1 - continue -end - -# Set Breakpoint at ISR Return Address -set $ret_addr = $ra -tbreak *$ret_addr -continue - -# Verify Time-Slice Handler Was Called -if $ts_handler_called == 1 - print "SUCCESS: Time-slice handler called." -else - print "FAILURE: Time-slice handler NOT called." -end - -# Verify System Timer Increment (Monotonicity) -set $clock_after = _tx_timer_system_clock -print $clock_after - -if $clock_after > $clock_before - print "SUCCESS: System timer incremented." -else - print "FAILURE: System timer did not increment." -end - -# Verify Preemption Logic (Thread Priority) -# -# We are now stopped at the return address from _tx_timer_interrupt, -# after _tx_thread_time_slice has had a chance to update -# _tx_thread_execute_ptr but before trap_handler returns into -# _tx_thread_context_restore. At this point, a pending preemption is -# observable directly by comparing current_ptr (interrupted thread) -# and execute_ptr (thread chosen by the scheduler). -set $curr_ptr = _tx_thread_current_ptr -set $exec_ptr = _tx_thread_execute_ptr -if $curr_ptr != 0 && $exec_ptr != 0 - set $curr_prio = $curr_ptr->tx_thread_priority - set $exec_prio = $exec_ptr->tx_thread_priority - printf "PREEMPT_CHECK current_prio=%d execute_prio=%d\\n", $curr_prio, $exec_prio - if $exec_prio < $curr_prio - printf "PREEMPT_VERIFIED_OK\\n" - else - printf "PREEMPT_VERIFIED_FAIL_NOT_OBSERVED\\n" - end -else - printf "PREEMPT_VERIFIED_FAIL_NULL\\n" -end - -quit -""".format(port=gdb_port, elf=elf_path) - - gdb_cmd_file = "test_cmds.gdb" - with open(gdb_cmd_file, "w") as f: - f.write(gdb_cmds) - - # 3. Run GDB - gdb_cmd = [ - gdb_bin, - "--batch", - "-x", gdb_cmd_file - ] - - print_content(f"Starting GDB: {' '.join(gdb_cmd)}") - - # Cap the GDB session to 30 s so a wedged batch script (e.g. a - # `continue` that never hits its breakpoint) cannot hang CI. - GDB_TIMEOUT_S = 30 - - try: - gdb_process = subprocess.run( - gdb_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - timeout=GDB_TIMEOUT_S, - ) - - print_content("GDB Output:") - print_content(gdb_process.stdout) - if gdb_process.stderr: - print_content("GDB Error Output:") - print_content(gdb_process.stderr) - - except subprocess.TimeoutExpired as e: - print_content( - f"FAILURE: GDB session exceeded {GDB_TIMEOUT_S}s timeout; " - "likely stuck on a `continue` that never matched a breakpoint." - ) - if e.stdout: - print_content("GDB Output (partial):") - print_content(e.stdout if isinstance(e.stdout, str) - else e.stdout.decode(errors='replace')) - if e.stderr: - print_content("GDB Error Output (partial):") - print_content(e.stderr if isinstance(e.stderr, str) - else e.stderr.decode(errors='replace')) - return False - - except Exception as e: - print_content(f"An error occurred during test execution: {e}") - return False - - finally: - # 4. Clean up - print_content("Stopping QEMU...") - qemu_process.terminate() - try: - qemu_process.wait(timeout=2) - except subprocess.TimeoutExpired: - print_content("QEMU did not terminate gracefully, killing it forcefullly.") - qemu_process.kill() - - # Verify results - stdout = gdb_process.stdout - timer_hit = "Breakpoint 4, _tx_timer_interrupt" in stdout - fpu_verified = False - preemption_verified = "PREEMPT_VERIFIED_OK" in stdout - - if "Breakpoint 3, thread_6_and_7_entry" in stdout: - if "1.10" in stdout or "fpu_test_val" in stdout: - print_content("SUCCESS: FPU instructions executed and registers inspected.") - fpu_verified = True - else: - print_content("FAILURE: Hit thread, but failed to inspect FPU. Output does not contain expected value.") - - if timer_hit: - print_content("SUCCESS: Timer Interrupt verified! Hit _tx_timer_interrupt.") - else: - print_content("FAILURE: Did not hit timer interrupt.") - - if preemption_verified: - print_content("SUCCESS: Preemption verified (higher-priority thread " - "preempted a lower-priority one).") - else: - if "PREEMPT_VERIFIED_FAIL_INVERTED" in stdout: - print_content("FAILURE: Preemption inverted -- lower priority " - "thread scheduled over higher priority one.") - elif "PREEMPT_VERIFIED_FAIL_NULL" in stdout: - print_content("FAILURE: Preemption check saw NULL thread pointers.") - elif "PREEMPT_VERIFIED_FAIL_NOT_OBSERVED" in stdout: - print_content("FAILURE: Preemption was not observed within the " - "loop budget.") - else: - print_content("FAILURE: Preemption check did not run to completion.") - - if timer_hit and fpu_verified and preemption_verified: - return True - else: - return False - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Run ThreadX QEMU/GDB Test") - parser.add_argument("--elf", required=True, help="Path to the kernel ELF file") - parser.add_argument("--qemu", default="qemu-system-riscv64", help="Path to QEMU binary") - parser.add_argument("--gdb", default="gdb", help="Path to GDB binary") - - args = parser.parse_args() - - success = run_qemu_test(args.elf, args.qemu, args.gdb) - - if success: - sys.exit(0) - else: - sys.exit(1) diff --git a/ports/risc-v64/gnu/example_build/qemu_virt/test/threadx_test_tx_gnu_riscv64_qemu.py b/ports/risc-v64/gnu/example_build/qemu_virt/test/threadx_test_tx_gnu_riscv64_qemu.py deleted file mode 100644 index 861f50e61..000000000 --- a/ports/risc-v64/gnu/example_build/qemu_virt/test/threadx_test_tx_gnu_riscv64_qemu.py +++ /dev/null @@ -1,286 +0,0 @@ -############################################################################## -# Copyright (c) 2024 Microsoft Corporation -# 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. -# -# SPDX-License-Identifier: MIT -############################################################################## - -import subprocess -import sys -import os -import argparse -import socket -import select - -def print_content(content): - """Prints content using os.write to handle non-blocking stdout robustly.""" - try: - msg = f"{content}\n".encode('utf-8') - total_len = len(msg) - written = 0 - fd = sys.stdout.fileno() - while written < total_len: - try: - n = os.write(fd, msg[written:]) - written += n - except BlockingIOError: - select.select([], [fd], []) - except Exception: - pass - -def get_free_port(): - """Finds a free TCP port.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('', 0)) - return s.getsockname()[1] - -def run_qemu_test(elf_path, qemu_bin, gdb_bin): - """ - Runs a test cycle using QEMU and GDB. - """ - print(f"Testing ELF: {elf_path}") - print(f"QEMU: {qemu_bin}") - print(f"GDB: {gdb_bin}") - - # Find a free port for GDB connection - gdb_port = get_free_port() - print(f"Using GDB port: {gdb_port}") - - # 1. Start QEMU in the background - qemu_cmd = [ - qemu_bin, - "-M", "virt", - "-nographic", - "-bios", "none", # Disable default OpenSBI - "-kernel", elf_path, - "-gdb", f"tcp::{gdb_port}", "-S", - "-monitor", "none", # Disable monitor - "-serial", "stdio" # Redirect serial output to stdio - ] - - print(f"Starting QEMU: {' '.join(qemu_cmd)}") - qemu_process = subprocess.Popen( - qemu_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - - if qemu_process.poll() is not None: - print("QEMU failed to start.") - print(qemu_process.stderr.read()) - return False - - # 2. Create a GDB command file - gdb_cmds = """ -file {elf} -target remote :{port} -set pagination off -set confirm off - -# Setup Breakpoints -break tx_application_define -break thread_0_entry -break thread_6_and_7_entry -break _tx_timer_interrupt - -# Execute to Application Definition -continue - -# Inspect mstatus once thread_0 has started -continue -print/x $mstatus - -# Verify FPU Logic and Register State exercised by thread_6/7 -continue -finish -step -step -step -print/x $mstatus -info registers float -print fpu_test_val - -# Await Timer Interrupt -continue -print "Hit Timer Interrupt" - -# Verify MEPC Integrity - Save State -print/x $mepc -set $saved_pc = $mepc - -# Verify System Timer Before ISR -set $clock_before = _tx_timer_system_clock -print $clock_before - -# Configure Time-Slice Test Conditions -set _tx_timer_time_slice = 1 -set _tx_timer_expired_time_slice = 0 -set $ts_handler_called = 0 - -# Set Breakpoint at Time-Slice Handler with Auto-Continue -tbreak _tx_thread_time_slice -commands - set $ts_handler_called = 1 - continue -end - -# Set Breakpoint at ISR Return Address -set $ret_addr = $ra -tbreak *$ret_addr -continue - -# Verify Time-Slice Handler Was Called -if $ts_handler_called == 1 - print "SUCCESS: Time-slice handler called." -else - print "FAILURE: Time-slice handler NOT called." -end - -# Verify System Timer Increment (Monotonicity) -set $clock_after = _tx_timer_system_clock -print $clock_after - -if $clock_after > $clock_before - print "SUCCESS: System timer incremented." -else - print "FAILURE: System timer did not increment." -end - -# Verify Preemption Logic (Thread Priority) -set $curr_ptr = _tx_thread_current_ptr -set $exec_ptr = _tx_thread_execute_ptr -if $curr_ptr != 0 && $exec_ptr != 0 - set $curr_prio = $curr_ptr->tx_thread_priority - set $exec_prio = $exec_ptr->tx_thread_priority - printf "PREEMPT_CHECK current_prio=%d execute_prio=%d\\n", $curr_prio, $exec_prio - if $exec_prio < $curr_prio - printf "PREEMPT_VERIFIED_OK\\n" - else - printf "PREEMPT_VERIFIED_FAIL_NOT_OBSERVED\\n" - end -else - printf "PREEMPT_VERIFIED_FAIL_NULL\\n" -end - -quit -""".format(port=gdb_port, elf=elf_path) - - gdb_cmd_file = "test_cmds.gdb" - with open(gdb_cmd_file, "w") as f: - f.write(gdb_cmds) - - # 3. Run GDB - gdb_cmd = [ - gdb_bin, - "--batch", - "-x", gdb_cmd_file - ] - - print_content(f"Starting GDB: {' '.join(gdb_cmd)}") - - GDB_TIMEOUT_S = 30 - - try: - gdb_process = subprocess.run( - gdb_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - timeout=GDB_TIMEOUT_S, - ) - - print_content("GDB Output:") - print_content(gdb_process.stdout) - if gdb_process.stderr: - print_content("GDB Error Output:") - print_content(gdb_process.stderr) - - except subprocess.TimeoutExpired as e: - print_content( - f"FAILURE: GDB session exceeded {GDB_TIMEOUT_S}s timeout; " - "likely stuck on a `continue` that never matched a breakpoint." - ) - if e.stdout: - print_content("GDB Output (partial):") - print_content(e.stdout if isinstance(e.stdout, str) - else e.stdout.decode(errors='replace')) - if e.stderr: - print_content("GDB Error Output (partial):") - print_content(e.stderr if isinstance(e.stderr, str) - else e.stderr.decode(errors='replace')) - return False - - except Exception as e: - print_content(f"An error occurred during test execution: {e}") - return False - - finally: - # 4. Clean up - print_content("Stopping QEMU...") - qemu_process.terminate() - try: - qemu_process.wait(timeout=2) - except subprocess.TimeoutExpired: - print_content("QEMU did not terminate gracefully, killing it forcefully.") - qemu_process.kill() - - # Verify results - stdout = gdb_process.stdout - timer_hit = "Breakpoint 4, _tx_timer_interrupt" in stdout - fpu_verified = False - preemption_verified = "PREEMPT_VERIFIED_OK" in stdout - - if "Breakpoint 3, thread_6_and_7_entry" in stdout: - if "1.10" in stdout or "fpu_test_val" in stdout: - print_content("SUCCESS: FPU instructions executed and registers inspected.") - fpu_verified = True - else: - print_content("FAILURE: Hit thread, but failed to inspect FPU. " - "Output does not contain expected value.") - - if timer_hit: - print_content("SUCCESS: Timer Interrupt verified! Hit _tx_timer_interrupt.") - else: - print_content("FAILURE: Did not hit timer interrupt.") - - if preemption_verified: - print_content("SUCCESS: Preemption verified (higher-priority thread " - "preempted a lower-priority one).") - else: - if "PREEMPT_VERIFIED_FAIL_INVERTED" in stdout: - print_content("FAILURE: Preemption inverted -- lower priority " - "thread scheduled over higher priority one.") - elif "PREEMPT_VERIFIED_FAIL_NULL" in stdout: - print_content("FAILURE: Preemption check saw NULL thread pointers.") - elif "PREEMPT_VERIFIED_FAIL_NOT_OBSERVED" in stdout: - print_content("FAILURE: Preemption was not observed within the " - "loop budget.") - else: - print_content("FAILURE: Preemption check did not run to completion.") - - if timer_hit and fpu_verified and preemption_verified: - return True - else: - return False - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Run ThreadX RV64 QEMU/GDB Test") - parser.add_argument("--elf", required=True, help="Path to the kernel ELF file") - parser.add_argument("--qemu", default="qemu-system-riscv64", - help="Path to QEMU binary") - parser.add_argument("--gdb", default="riscv64-unknown-elf-gdb", - help="Path to GDB binary") - - args = parser.parse_args() - - success = run_qemu_test(args.elf, args.qemu, args.gdb) - - if success: - sys.exit(0) - else: - sys.exit(1) 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 b9c799f47..d8190fb67 100644 --- a/ports/risc-v64/gnu/src/tx_thread_context_restore.S +++ b/ports/risc-v64/gnu/src/tx_thread_context_restore.S @@ -89,9 +89,9 @@ _tx_thread_context_restore: /* Just recover the saved registers and return to the point of interrupt. */ - /* Recover floating point registers only if saved mstatus/sstatus.FS was not Off. */ + /* Recover floating point registers only if saved mstatus.FS was not Off. */ #if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double) - ld t1, 29*8(sp) // Pickup saved mstatus/sstatus + 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 @@ -141,7 +141,7 @@ _tx_thread_context_restore: fld f30,61*8(sp) // Recover ft10 fld f31,62*8(sp) // Recover ft11 ld t0, 63*8(sp) // Recover fcsr - csrw fcsr, t0 // Restore fcsr + csrw fcsr, t0 #endif _tx_thread_skip_fp_restore: @@ -293,9 +293,9 @@ _tx_thread_no_preempt_restore: ld sp, 8(t1) // Switch back to thread's stack - /* Recover floating point registers only if saved mstatus/sstatus.FS was not Off. */ + /* Recover floating point registers only if saved mstatus.FS was not Off. */ #if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double) - ld t3, 29*8(sp) // Pickup saved mstatus/sstatus + 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 @@ -479,9 +479,9 @@ _tx_thread_preempt_restore: ori t3, zero, 1 // Build interrupt stack type sd t3, 0(t0) // Store stack type - /* Store floating point preserved registers only if saved mstatus/sstatus.FS was not Off. */ + /* Store floating point preserved registers only if saved mstatus.FS was not Off. */ #if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double) - ld t3, 29*8(t0) // Pickup saved mstatus/sstatus + ld t3, 29*8(t0) // Pickup saved mstatus srli t3, t3, 13 andi t3, t3, 0x3 beqz t3, _tx_thread_preempt_skip_fp_restore // Skip if FS was Off 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 10177ee54..dcc6301a1 100644 --- a/ports/risc-v64/gnu/src/tx_thread_context_save.S +++ b/ports/risc-v64/gnu/src/tx_thread_context_save.S @@ -102,13 +102,9 @@ _tx_thread_context_save: #endif sd t0, 30*8(sp) // Save it on the stack - /* Save mstatus/sstatus and skip FP state if FS is Off. */ -#ifdef TX_RISCV_SMODE - csrr t0, sstatus -#else + /* Save mstatus and skip FP state if FS is Off. */ csrr t0, mstatus -#endif - sd t0, 29*8(sp) // Save mstatus/sstatus + 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 @@ -240,13 +236,9 @@ _tx_thread_not_nested_save: #endif sd t1, 30*8(sp) // Save it on the stack - /* Save mstatus/sstatus and skip FP state if FS is Off. */ -#ifdef TX_RISCV_SMODE - csrr t1, sstatus -#else + /* Save mstatus and skip FP state if FS is Off. */ csrr t1, mstatus -#endif - sd t1, 29*8(sp) // Save mstatus/sstatus + 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 From 81493df8d01747b7f6002783841da498f7bcf168 Mon Sep 17 00:00:00 2001 From: Wei-Chen Lai Date: Sun, 23 Aug 2026 15:55:22 +0800 Subject: [PATCH 4/5] Initialize mstatus.FS in RV64 stack build so new threads start with clean FP state Slot 29 was left uninitialized while context restore reads it as an FP-live hint; garbage FS bits could make a new thread inherit the previous thread's floating-point registers. --- ports/risc-v64/gnu/src/tx_thread_stack_build.S | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 86cab4f09..443f9ede3 100644 --- a/ports/risc-v64/gnu/src/tx_thread_stack_build.S +++ b/ports/risc-v64/gnu/src/tx_thread_stack_build.S @@ -30,6 +30,7 @@ /* AUTHOR */ /* */ /* Scott Larson, Microsoft Corporation */ +/* Wei-Chen Lai, National Cheng Kung University */ /* */ /* DESCRIPTION */ /* */ @@ -92,7 +93,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 @@ -192,6 +193,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 From 3e6de92893c6c30db437bba429495888fb265d8b Mon Sep 17 00:00:00 2001 From: Wei-Chen Lai Date: Sun, 23 Aug 2026 16:37:25 +0800 Subject: [PATCH 5/5] Add RV64 regression test for the FP state of a newly created thread The test dirties every floating point register, then creates a thread and checks that the stack builder wrote the mstatus slot and that the new thread starts with all floating point registers zeroed. It is registered for RV64 only, since the RV32 stack builder still leaves the slot unwritten. --- test/tx/cmake/riscv/regression/CMakeLists.txt | 8 + .../threadx_riscv_new_thread_fpu_state_test.c | 278 ++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 test/tx/cmake/riscv/regression/threadx_riscv_new_thread_fpu_state_test.c 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; +}