summaryrefslogtreecommitdiff
path: root/test/tx/cmake
diff options
context:
space:
mode:
authorFrédéric Desbiens <[email protected]>2026-10-02 10:42:44 -0400
committerGitHub <[email protected]>2026-10-02 10:42:44 -0400
commit93387b0a60382a29579bbad5045c68328e3751df (patch)
tree42be790d602cb76f55090e5a7c418df7ea2b3428 /test/tx/cmake
parent44d7c95c582d415c4ad84527180b29c93c3bf664 (diff)
parent28281cd5647d4671236ca08d19bd18ed15b2265a (diff)
Merge pull request #783 from eclipse-threadx/devv6.5.2.202603_rel
Merged changes ahead of the v6.5.2.202603 release
Diffstat (limited to 'test/tx/cmake')
-rw-r--r--test/tx/cmake/CMakeLists.txt59
-rwxr-xr-xtest/tx/cmake/coverage.sh142
-rw-r--r--test/tx/cmake/module_manager/CMakeLists.txt254
-rw-r--r--test/tx/cmake/regression/CMakeLists.txt4
-rw-r--r--test/tx/cmake/riscv/CMakeLists.txt11
-rw-r--r--test/tx/cmake/riscv/bsp/entry.S6
-rw-r--r--test/tx/cmake/riscv/bsp/hwtimer.c84
-rw-r--r--test/tx/cmake/riscv/bsp/hwtimer.h4
-rw-r--r--test/tx/cmake/riscv/bsp/link.lds32
-rw-r--r--test/tx/cmake/riscv/bsp/plic.c25
-rw-r--r--test/tx/cmake/riscv/bsp/syscalls.c49
-rw-r--r--test/tx/cmake/riscv/bsp/trap.c3
-rw-r--r--test/tx/cmake/riscv/bsp/tx_initialize_low_level.S35
-rw-r--r--test/tx/cmake/riscv/regression/CMakeLists.txt12
-rw-r--r--test/tx/cmake/riscv/regression/threadx_riscv_new_thread_fpu_state_test.c278
-rwxr-xr-xtest/tx/cmake/riscv/run.sh33
-rw-r--r--test/tx/cmake/thread_transition/CMakeLists.txt131
17 files changed, 1114 insertions, 48 deletions
diff --git a/test/tx/cmake/CMakeLists.txt b/test/tx/cmake/CMakeLists.txt
index 97b3615b..2f9c9b9f 100644
--- a/test/tx/cmake/CMakeLists.txt
+++ b/test/tx/cmake/CMakeLists.txt
@@ -14,7 +14,8 @@ set(CMAKE_C_EXTENSIONS OFF)
# Set build configurations
set(BUILD_CONFIGURATIONS default_build_coverage disable_notify_callbacks_build
- stack_checking_build stack_checking_rand_fill_build trace_build)
+ stack_checking_build stack_checking_rand_fill_build trace_build
+ misra_build misra_trace_build)
set(CMAKE_CONFIGURATION_TYPES
${BUILD_CONFIGURATIONS}
CACHE STRING "list of supported configuration types" FORCE)
@@ -36,6 +37,21 @@ set(stack_checking_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_ENABLE_STACK_CHECKING)
set(stack_checking_rand_fill_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_ENABLE_STACK_CHECKING TX_ENABLE_RANDOM_NUMBER_STACK_FILLING)
set(trace_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_ENABLE_EVENT_TRACE)
+# TX_MISRA_ENABLE routes the kernel's pointer conversions, its memset and its
+# stack check through the shim in common/src/tx_misra.c, which every
+# configuration above compiles out entirely. It is also the source the MISRA
+# analysis runs against, so without this the build that is analysed and the build
+# that is tested are different builds of the same tree.
+#
+# The second adds event tracing, because the shim has a region behind
+# TX_ENABLE_EVENT_TRACE that neither macro alone reaches.
+#
+# Both defects found here were invisible for the same reason: #741 broke under
+# TX_MISRA_ENABLE and #745 under both macros together, and neither combination
+# was built anywhere.
+set(misra_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_MISRA_ENABLE)
+set(misra_trace_build TX_QUEUE_MESSAGE_MAX_SIZE=32 TX_MISRA_ENABLE TX_ENABLE_EVENT_TRACE)
+
add_compile_definitions(
TX_REGRESSION_TEST
TEST_STACK_SIZE_PRINTF=4096
@@ -61,8 +77,47 @@ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../.. threadx)
add_subdirectory(regression)
add_subdirectory(samples)
+# The two configurations this change has to reach, TX_NOT_INTERRUPTABLE and
+# TX_DISABLE_ERROR_CHECKING, are not among the five above and cannot be reached
+# from the regression suite, which links one library per configuration. This
+# directory compiles the sources concerned directly. It is listed last rather than
+# beside regression on purpose: every branch in the current fix set adds an
+# add_subdirectory line immediately after regression, and putting a second one
+# there turns a set of clean merges into a set of one-line conflicts.
+# These two compile a hand-picked set of common/src sources directly instead of
+# linking the threadx library, which is what lets them reach feature macros the
+# library-per-configuration model cannot. That does not survive TX_MISRA_ENABLE:
+# the kernel's TX_MEMSET and its conversions become calls into tx_misra.c, and
+# pulling the shim into these targets pulls its own callees after it. Neither
+# exists to test the shim, and the regression suite covers it, so the MISRA
+# configurations skip them.
+if(NOT ("TX_MISRA_ENABLE" IN_LIST ${CMAKE_BUILD_TYPE}))
+ add_subdirectory(thread_transition)
+endif()
+
+# Host tests for the Module Manager. Listed after thread_transition for the reason
+# given above it: additions here are appended rather than inserted, so that branches
+# adding a directory of their own merge cleanly.
+if(NOT ("TX_MISRA_ENABLE" IN_LIST ${CMAKE_BUILD_TYPE}))
+ add_subdirectory(module_manager)
+endif()
+
# Coverage
-if(CMAKE_BUILD_TYPE MATCHES ".*_coverage")
+#
+# The gate here used to be the build type alone, and only one of the five
+# configurations -- default_build_coverage -- has a name matching "*_coverage".
+# So four configurations built and ran every test and their coverage was thrown
+# away. That is not redundancy discarded: each configuration selects a different
+# set of TX_ feature macros, so the code the other four compile is *absent from
+# the denominator* rather than reported as uncovered. A figure taken from one of
+# them is the executable-line count of one configuration, not of the kernel.
+#
+# TX_COVERAGE instruments a build regardless of its name. It defaults to OFF and
+# the build-type match is kept, so configuring a single configuration by hand
+# behaves exactly as it did before; the regression run turns it on for all five
+# and merges the results.
+option(TX_COVERAGE "Instrument this build for coverage regardless of build type" OFF)
+if(TX_COVERAGE OR CMAKE_BUILD_TYPE MATCHES ".*_coverage")
if(NOT MSVC)
target_compile_options(threadx PRIVATE -fprofile-arcs -ftest-coverage)
target_link_options(threadx PRIVATE -fprofile-arcs -ftest-coverage)
diff --git a/test/tx/cmake/coverage.sh b/test/tx/cmake/coverage.sh
index 6dda78b3..32d466b7 100755
--- a/test/tx/cmake/coverage.sh
+++ b/test/tx/cmake/coverage.sh
@@ -14,6 +14,142 @@
set -e
cd $(dirname $0)
-mkdir -p coverage_report/$1
-gcovr --object-directory=build/$1/threadx/CMakeFiles/threadx.dir/common/src -r build/$1 -f ../../../common/src --xml-pretty --output coverage_report/$1.xml
-gcovr --object-directory=build/$1/threadx/CMakeFiles/threadx.dir/common/src -r build/$1 -f ../../../common/src --html --html-details --output coverage_report/$1/index.html
+
+# gcov reads a data format tied to the compiler that produced it, so gcov has to match
+# the gcc that built the objects. Since cmake/linux.cmake began honouring CC, taking
+# whatever gcov happens to be first on PATH is no longer safe: building with gcc-14 and
+# reading with gcov-13 gives "version 'B42*', prefer 'B33*'" from gcov, and gcovr turns
+# that into "GCOV returncode was 3" and exits 64. The tests pass and then the coverage
+# step fails with a Python traceback, which reads like a coverage bug rather than a
+# toolchain mismatch.
+#
+# So derive gcov from CC rather than asking the caller to remember both. GCOV still
+# overrides, for a toolchain that does not follow the gcc/gcov naming.
+: "${CC:=gcc}"
+if [ -z "${GCOV:-}" ]; then
+ cc_base=$(basename "$CC")
+ case "$cc_base" in
+ gcc*) GCOV="gcov${cc_base#gcc}" ;;
+ *) GCOV="gcov" ;;
+ esac
+fi
+
+if ! command -v "$GCOV" >/dev/null 2>&1; then
+ echo "coverage.sh: '$GCOV' not found, derived from CC='$CC'." >&2
+ echo "Install it, or set GCOV to the gcov matching that compiler." >&2
+ exit 1
+fi
+
+# The repository root, needed by every mode below. It has to be absolute: see the
+# note on the per-configuration paths further down.
+repo_root=$(cd ../../.. && pwd)
+filter=$repo_root/common/src
+
+# --merge unions the per-configuration reports into the one number that means
+# something. Each configuration writes an intermediate JSON beside its XML, and
+# this pass adds them all together.
+#
+# Reporting five separate percentages instead would invite a reader to average
+# them, and an average is not a coverage figure -- a line covered only by
+# trace_build is covered, and only the union says so.
+#
+# Do not be alarmed when the merged percentage is lower than the single
+# configuration this used to report. That is the point: the denominator now
+# includes code the old report never counted at all, because it was compiled out
+# of the only instrumented build.
+if [ "$1" = "--merge" ]; then
+ shopt -s nullglob
+ tracefiles=(coverage_report/per_configuration/*.json)
+ shopt -u nullglob
+ if [ ${#tracefiles[@]} -eq 0 ]; then
+ echo "coverage.sh --merge: no JSON in coverage_report/per_configuration/." >&2
+ echo "Run the suites with TX_COVERAGE=ON first." >&2
+ exit 1
+ fi
+
+ add_args=()
+ for t in "${tracefiles[@]}"; do
+ add_args+=(--add-tracefile "$t")
+ done
+
+ mkdir -p coverage_report/merged
+ gcovr -r "$repo_root" "${add_args[@]}" --xml-pretty --output coverage_report/merged.xml
+ gcovr -r "$repo_root" "${add_args[@]}" --html --html-details --output coverage_report/merged/index.html
+
+ if ! grep -q "<class " coverage_report/merged.xml; then
+ echo "coverage.sh --merge: the merged report contains no files." >&2
+ exit 1
+ fi
+
+ # Named, not just counted. coverage_report/ is not cleaned between runs, so a
+ # tracefile left by an earlier run of a different set of configurations would
+ # otherwise be merged in without anything saying so.
+ echo "coverage.sh --merge: ThreadX, ${#tracefiles[@]} configuration(s):"
+ for t in "${tracefiles[@]}"; do
+ echo " $(basename "$t" .json)"
+ done
+ exit 0
+fi
+
+# gcovr is given three paths below, and each of them has to be absolute, for a
+# different reason.
+#
+# -r is the repository root rather than the build directory, so the report names
+# files the way the repository does -- "common/src/tx_block_allocate.c" instead
+# of "/home/runner/work/threadx/threadx/common/src/tx_block_allocate.c". With -r
+# inside build/, gcovr cannot express the sources relative to it, because they
+# are outside it, and falls back to absolute paths. Those paths then differ on
+# every machine and disagree with the <source> element written beside them in
+# the same file, so anything that maps coverage back to the repository -- PR
+# annotations, Codecov, SonarQube -- cannot follow them.
+#
+# Both -r and -f must be absolute. "-r ../../.. -f common/src" produces a report
+# containing zero files and exits 0, which is the worst failure mode available
+# here: a green run carrying an empty report. Measured, not assumed. repo_root
+# and filter are set above, before the merge mode, because it needs them too.
+
+# This is what actually scopes the report to one build configuration, and it is
+# the positional search path -- not --object-directory, which used to be here
+# and was doing nothing at all. That flag tells gcovr how to get from a gcda
+# file back to the compiler's working directory; it does not restrict which gcda
+# files are found. Pointed at an empty directory it still produced the full
+# 177-file report, because gcovr searches -r as well.
+#
+# That matters more now than it did before. While -r was build/$1 it happened to
+# constrain the search to this configuration by accident. -r is the repository
+# root now, and every configuration's gcda lies somewhere under it, so without
+# an explicit search path the report would silently merge all five. Measured on
+# gcovr 8.6: with this search path, an empty directory yields an empty report
+# and the real one yields 177 files.
+objdir=$PWD/build/$1/threadx/CMakeFiles/threadx.dir/common/src
+
+# The Linux port's own sources are deliberately outside -f. gcno files exist for
+# two directories -- common/src and ports/linux/gnu/src -- and only the first is
+# reported. The kernel is what this suite is here to cover; the architecture
+# ports are validated functionally rather than structurally, and linux/gnu is a
+# development host port that nothing ships on. Written down because a filter
+# argument on its own is not a decision the next reader can see.
+
+# Per-configuration output is kept in a subdirectory of its own, and the merged
+# report sits alongside it at the top. That is not tidiness: the Pages deploy
+# uploads coverage_report wholesale and merges the ThreadX and SMP artifacts
+# into one tree, and every configuration directory has the same name in both
+# suites. Left at the top level, default_build_coverage/ from one suite would
+# overwrite the other's on the published site. Nested here, the top level still
+# holds exactly the one suite directory the deploy expects.
+mkdir -p coverage_report/per_configuration/$1
+gcovr --gcov-executable "$GCOV" -r "$repo_root" -f "$filter" "$objdir" \
+ --json coverage_report/per_configuration/$1.json \
+ --xml-pretty --output coverage_report/per_configuration/$1.xml
+gcovr --gcov-executable "$GCOV" -r "$repo_root" -f "$filter" "$objdir" --html --html-details --output coverage_report/per_configuration/$1/index.html
+
+# An empty report is not an error as far as gcovr is concerned: it warns and
+# exits 0. Worse, it advertises line-rate="1.0" alongside lines-valid="0", so
+# every downstream consumer reads "no data at all" as "100% covered". A coverage
+# threshold cannot catch that, because an empty report passes any threshold. So
+# the assertion belongs here, next to the paths that would cause it.
+if ! grep -q "<class " coverage_report/per_configuration/$1.xml; then
+ echo "coverage.sh: the report for '$1' contains no files." >&2
+ echo "Expected gcda files under $objdir." >&2
+ exit 1
+fi
diff --git a/test/tx/cmake/module_manager/CMakeLists.txt b/test/tx/cmake/module_manager/CMakeLists.txt
new file mode 100644
index 00000000..17511fd5
--- /dev/null
+++ b/test/tx/cmake/module_manager/CMakeLists.txt
@@ -0,0 +1,254 @@
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+cmake_policy(SET CMP0057 NEW)
+
+project(module_manager_test LANGUAGES C)
+
+set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..)
+set(SOURCE_DIR ${REPO_ROOT}/test/tx/module_manager)
+
+# The Module Manager is not part of the ThreadX library this test tree builds, and
+# its control blocks come from a module port rather than a base port. These tests
+# therefore compile the manager sources they exercise directly against one module
+# port's headers, and stand stubs behind the kernel services those sources call, so
+# that they run on the host like every other test here. It is the same technique the
+# thread_transition directory uses, for the same reason: the code under test cannot
+# be reached through the library the suite links.
+#
+# Every module port is an embedded architecture, so the interrupt primitives those
+# sources use are inline assembly the host assembler cannot take. The port shim
+# header is force-included ahead of every translation unit here and replaces the
+# three of them with host equivalents that count.
+
+set(module_manager_dir ${REPO_ROOT}/common_modules/module_manager)
+set(cortex_a7_module_dir ${REPO_ROOT}/ports_module/cortex_a7/gnu)
+set(cortex_m4_module_dir ${REPO_ROOT}/ports_module/cortex_m4/gnu)
+
+add_executable(
+ threadx_module_manager_thread_kernel_stack_test
+ ${SOURCE_DIR}/threadx_module_manager_thread_kernel_stack_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c)
+
+target_include_directories(
+ threadx_module_manager_thread_kernel_stack_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+# The dispatch layer is one header of static functions, one per kernel service. A
+# test reaches the dispatcher it exercises by including that header, which defines
+# every other dispatcher too, and an unoptimised build emits them all -- so their
+# references to services this harness stands behind nothing for would reach the
+# linker.
+#
+# The header guards each dispatcher with its own TXM_<SERVICE>_CALL_NOT_USED macro,
+# which is the supported way to trim the dispatch table. Defining every guard except
+# the ones a test needs leaves that test's translation unit holding only the
+# dispatcher under test. The list is read out of the header rather than written down
+# here, so a service added later is excluded without anyone having to remember.
+file(STRINGS ${module_manager_dir}/inc/txm_module_manager_dispatch.h
+ dispatch_guard_lines REGEX "^#ifndef TXM_[A-Z0-9_]+_CALL_NOT_USED")
+
+set(dispatch_guards "")
+foreach(guard_line ${dispatch_guard_lines})
+ string(REGEX MATCH "TXM_[A-Z0-9_]+_CALL_NOT_USED" guard "${guard_line}")
+ list(APPEND dispatch_guards ${guard})
+endforeach()
+list(REMOVE_DUPLICATES dispatch_guards)
+
+# The dispatchers this test calls, which therefore must stay in.
+set(kernel_stack_dispatchers TXM_THREAD_DELETE_CALL_NOT_USED)
+
+set(kernel_stack_guards ${dispatch_guards})
+list(REMOVE_ITEM kernel_stack_guards ${kernel_stack_dispatchers})
+
+target_compile_definitions(threadx_module_manager_thread_kernel_stack_test
+ PRIVATE ${kernel_stack_guards})
+
+# The port shim is force-included ahead of every translation unit, the test included,
+# because the manager sources and the dispatch header both reach for the port's
+# interrupt primitives.
+target_compile_options(
+ threadx_module_manager_thread_kernel_stack_test
+ PRIVATE -Wall
+ -Wextra
+ -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_thread_kernel_stack_test
+ threadx_module_manager_thread_kernel_stack_test)
+
+# This test drives the manager services directly rather than through a dispatcher,
+# so it needs none of the dispatch table and no guard list.
+add_executable(
+ threadx_module_manager_object_authentication_test
+ ${SOURCE_DIR}/threadx_module_manager_object_authentication_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c
+ ${module_manager_dir}/src/txm_module_manager_thread_reset.c)
+
+target_include_directories(
+ threadx_module_manager_object_authentication_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_object_authentication_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_object_authentication_test
+ threadx_module_manager_object_authentication_test)
+
+# This test drives the manager services directly rather than through a dispatcher,
+# so it needs none of the dispatch table and no guard list.
+add_executable(
+ threadx_module_manager_delete_ownership_test
+ ${SOURCE_DIR}/threadx_module_manager_delete_ownership_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c)
+
+target_include_directories(
+ threadx_module_manager_delete_ownership_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_delete_ownership_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_delete_ownership_test
+ threadx_module_manager_delete_ownership_test)
+
+# This test drives the allocation and deallocation services directly rather than
+# through a dispatcher, so it needs none of the dispatch table and no guard list.
+add_executable(
+ threadx_module_manager_live_object_deallocation_test
+ ${SOURCE_DIR}/threadx_module_manager_live_object_deallocation_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c)
+
+target_include_directories(
+ threadx_module_manager_live_object_deallocation_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_live_object_deallocation_test
+ PRIVATE -Wall
+ -Wextra
+ -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_live_object_deallocation_test
+ threadx_module_manager_live_object_deallocation_test)
+add_executable(
+ threadx_module_manager_object_allocate_overflow_test
+ ${SOURCE_DIR}/threadx_module_manager_object_allocate_overflow_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c
+ ${module_manager_dir}/src/txm_module_manager_object_allocate.c
+ ${module_manager_dir}/src/txm_module_manager_object_deallocate.c)
+
+target_include_directories(
+ threadx_module_manager_object_allocate_overflow_test
+ PRIVATE ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_a7_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_object_allocate_overflow_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_object_allocate_overflow_test
+ threadx_module_manager_object_allocate_overflow_test)
+
+add_executable(
+ threadx_module_manager_block_pool_parameters_test
+ ${SOURCE_DIR}/threadx_module_manager_block_pool_parameters_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c)
+
+target_include_directories(
+ threadx_module_manager_block_pool_parameters_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_m4_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_block_pool_parameters_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_block_pool_parameters_test
+ threadx_module_manager_block_pool_parameters_test)
+
+add_executable(
+ threadx_module_manager_object_name_range_test
+ ${SOURCE_DIR}/threadx_module_manager_object_name_range_test.c
+ ${module_manager_dir}/src/txm_module_manager_object_pointer_get.c
+ ${module_manager_dir}/src/txm_module_manager_object_pointer_get_extended.c
+ ${module_manager_dir}/src/txm_module_manager_util.c)
+
+target_include_directories(
+ threadx_module_manager_object_name_range_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_m4_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_object_name_range_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_object_name_range_test
+ threadx_module_manager_object_name_range_test)
+
+add_executable(
+ threadx_module_manager_queue_message_range_test
+ ${SOURCE_DIR}/threadx_module_manager_queue_message_range_test.c
+ ${module_manager_dir}/src/txm_module_manager_util.c)
+
+target_include_directories(
+ threadx_module_manager_queue_message_range_test
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${module_manager_dir}/inc
+ ${cortex_m4_module_dir}/inc)
+
+target_compile_options(
+ threadx_module_manager_queue_message_range_test
+ PRIVATE -include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_queue_message_range_test
+ threadx_module_manager_queue_message_range_test)
+
+add_executable(
+ threadx_module_manager_cortex_a7_range_check_test
+ ${SOURCE_DIR}/threadx_module_manager_cortex_a7_range_check_test.c
+ ${cortex_a7_module_dir}/module_manager/src/txm_module_manager_inside_data_check.c)
+
+target_include_directories(
+ threadx_module_manager_cortex_a7_range_check_test
+ PRIVATE ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/ports/cortex_a7/gnu/inc
+ ${REPO_ROOT}/common_modules/inc
+ ${REPO_ROOT}/common_modules/module_manager/inc
+ ${cortex_a7_module_dir}/inc)
+
+add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_cortex_a7_range_check_test
+ threadx_module_manager_cortex_a7_range_check_test)
diff --git a/test/tx/cmake/regression/CMakeLists.txt b/test/tx/cmake/regression/CMakeLists.txt
index 34fba1b2..6b94a857 100644
--- a/test/tx/cmake/regression/CMakeLists.txt
+++ b/test/tx/cmake/regression/CMakeLists.txt
@@ -80,6 +80,7 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_thread_create_preemption_threshold_test.c
${SOURCE_DIR}/threadx_thread_delayed_suspension_test.c
${SOURCE_DIR}/threadx_thread_information_test.c
+ ${SOURCE_DIR}/threadx_thread_misaligned_stack_test.c
${SOURCE_DIR}/threadx_thread_multi_level_preemption_threshold_test.c
${SOURCE_DIR}/threadx_thread_multiple_non_current_test.c
${SOURCE_DIR}/threadx_thread_multiple_sleep_test.c
@@ -96,7 +97,9 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_thread_sleep_for_100ticks_test.c
${SOURCE_DIR}/threadx_thread_sleep_terminate_test.c
${SOURCE_DIR}/threadx_thread_stack_checking_test.c
+ ${SOURCE_DIR}/threadx_thread_stack_fill_value_test.c
${SOURCE_DIR}/threadx_thread_terminate_delete_test.c
+ ${SOURCE_DIR}/threadx_thread_exit_callback_transition_test.c
${SOURCE_DIR}/threadx_thread_time_slice_change_test.c
${SOURCE_DIR}/threadx_thread_wait_abort_and_isr_test.c
${SOURCE_DIR}/threadx_thread_wait_abort_test.c
@@ -109,6 +112,7 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_timer_multiple_test.c
${SOURCE_DIR}/threadx_timer_simple_test.c
${SOURCE_DIR}/threadx_trace_basic_test.c
+ ${SOURCE_DIR}/threadx_trace_entry_update_test.c
${SOURCE_DIR}/threadx_initialize_kernel_setup_test.c)
add_custom_command(
diff --git a/test/tx/cmake/riscv/CMakeLists.txt b/test/tx/cmake/riscv/CMakeLists.txt
index a4f7e7e8..41b44949 100644
--- a/test/tx/cmake/riscv/CMakeLists.txt
+++ b/test/tx/cmake/riscv/CMakeLists.txt
@@ -5,7 +5,7 @@ cmake_policy(SET CMP0057 NEW)
project(threadx_riscv_test LANGUAGES C ASM)
# Build configurations (same defines as Linux tests, minus coverage instrumentation)
-set(BUILD_CONFIGURATIONS default_build disable_notify_callbacks_build
+set(BUILD_CONFIGURATIONS default_build optimized_build disable_notify_callbacks_build
stack_checking_build stack_checking_rand_fill_build trace_build)
set(CMAKE_CONFIGURATION_TYPES
${BUILD_CONFIGURATIONS}
@@ -26,14 +26,21 @@ message(STATUS "THREADX_ARCH: ${THREADX_ARCH}")
# Per-configuration compile definitions
set(default_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32)
+set(optimized_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32)
set(disable_notify_callbacks_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_DISABLE_NOTIFY_CALLBACKS)
set(stack_checking_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_ENABLE_STACK_CHECKING)
set(stack_checking_rand_fill_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_ENABLE_STACK_CHECKING -DTX_ENABLE_RANDOM_NUMBER_STACK_FILLING)
set(trace_build -DTX_QUEUE_MESSAGE_MAX_SIZE=32 -DTX_ENABLE_EVENT_TRACE)
+if(CMAKE_BUILD_TYPE STREQUAL "optimized_build")
+ set(REGRESSION_OPTIMIZATION -O2)
+else()
+ set(REGRESSION_OPTIMIZATION -O0)
+endif()
+
add_compile_options(
-std=c99
- -O0
+ ${REGRESSION_OPTIMIZATION}
-g3
-fdiagnostics-color
-Werror
diff --git a/test/tx/cmake/riscv/bsp/entry.S b/test/tx/cmake/riscv/bsp/entry.S
index 2f16c052..5abe9852 100644
--- a/test/tx/cmake/riscv/bsp/entry.S
+++ b/test/tx/cmake/riscv/bsp/entry.S
@@ -18,6 +18,7 @@
.extern _sysstack_end
.extern _bss_start
.extern _bss_end
+.extern __global_pointer$
_start:
csrr t0, mhartid
@@ -56,6 +57,11 @@ _start:
li x30, 0
li x31, 0
+ .option push
+ .option norelax
+ la gp, __global_pointer$
+ .option pop
+
/* Set up stack pointer from linker symbol. */
la sp, _sysstack_end
diff --git a/test/tx/cmake/riscv/bsp/hwtimer.c b/test/tx/cmake/riscv/bsp/hwtimer.c
index 99821f1e..8b81268a 100644
--- a/test/tx/cmake/riscv/bsp/hwtimer.c
+++ b/test/tx/cmake/riscv/bsp/hwtimer.c
@@ -9,25 +9,99 @@
**************************************************************************/
#include "csr.h"
+#include "tx_api.h"
#include "hwtimer.h"
-#include <tx_port.h>
#define CLINT (0x02000000L)
#define CLINT_TIME (CLINT + 0xBFF8)
#define CLINT_TIMECMP(id) (CLINT + 0x4000 + 8 * (id))
+#if __riscv_xlen == 32
+
+/* RV32 has no 64-bit load/store, so the 64-bit CLINT registers are
+ accessed as two volatile 32-bit MMIO words. */
+#define MTIME_LO (*(volatile uint32_t *)(CLINT_TIME))
+#define MTIME_HI (*(volatile uint32_t *)(CLINT_TIME + 4))
+#define MTIMECMP_LO(id) (*(volatile uint32_t *)(CLINT_TIMECMP(id)))
+#define MTIMECMP_HI(id) (*(volatile uint32_t *)(CLINT_TIMECMP(id) + 4))
+
+/* RV32 mtime read: re-read the high word until it is stable, so a
+ low-word rollover between the two loads cannot tear the value. */
+static uint64_t clint_time_read(void)
+{
+ uint32_t hi;
+ uint32_t lo;
+
+ do
+ {
+ hi = MTIME_HI;
+ lo = MTIME_LO;
+ } while (hi != MTIME_HI);
+
+ return ((uint64_t)hi << 32) | lo;
+}
+
+/* Only this hart writes its own mtimecmp, so a plain two-word read
+ cannot tear. */
+static uint64_t clint_timecmp_read(uintptr_t hart)
+{
+ uint32_t lo = MTIMECMP_LO(hart);
+ uint32_t hi = MTIMECMP_HI(hart);
+
+ return ((uint64_t)hi << 32) | lo;
+}
+
+/* RV32 mtimecmp write, safe sequence: park the low word at all-ones
+ first, so no intermediate 64-bit value compares below mtime and
+ raises a spurious timer interrupt. */
+static void clint_timecmp_write(uintptr_t hart, uint64_t value)
+{
+ MTIMECMP_LO(hart) = 0xFFFFFFFF;
+ MTIMECMP_HI(hart) = (uint32_t)(value >> 32);
+ MTIMECMP_LO(hart) = (uint32_t)value;
+}
+
+#else /* __riscv_xlen == 64 */
+
+/* RV64: naturally aligned 64-bit MMIO accesses are single loads/stores. */
+static uint64_t clint_time_read(void)
+{
+ return *(volatile uint64_t *)CLINT_TIME;
+}
+
+static uint64_t clint_timecmp_read(uintptr_t hart)
+{
+ return *(volatile uint64_t *)CLINT_TIMECMP(hart);
+}
+
+static void clint_timecmp_write(uintptr_t hart, uint64_t value)
+{
+ *(volatile uint64_t *)CLINT_TIMECMP(hart) = value;
+}
+
+#endif /* __riscv_xlen */
+
int hwtimer_init(void)
{
uintptr_t hart = riscv_get_core();
- uint64_t time = *((volatile uint64_t *)CLINT_TIME);
- *((volatile uint64_t *)CLINT_TIMECMP(hart)) = time + TICKNUM_PER_TIMER;
+
+ clint_timecmp_write(hart, clint_time_read() + TICKNUM_PER_TIMER);
return 0;
}
int hwtimer_handler(void)
{
uintptr_t hart = riscv_get_core();
- uint64_t time = *((volatile uint64_t *)CLINT_TIME);
- *((volatile uint64_t *)CLINT_TIMECMP(hart)) = time + TICKNUM_PER_TIMER;
+
+ /* Absolute re-arm: advance from the previous compare value, so trap
+ latency does not accumulate as tick drift; catch up if the next
+ compare already passed. */
+ uint64_t next = clint_timecmp_read(hart) + TICKNUM_PER_TIMER;
+ uint64_t now = clint_time_read();
+
+ if (next <= now)
+ next = now + TICKNUM_PER_TIMER;
+
+ clint_timecmp_write(hart, next);
return 0;
}
diff --git a/test/tx/cmake/riscv/bsp/hwtimer.h b/test/tx/cmake/riscv/bsp/hwtimer.h
index e6b9efa4..bde0bba5 100644
--- a/test/tx/cmake/riscv/bsp/hwtimer.h
+++ b/test/tx/cmake/riscv/bsp/hwtimer.h
@@ -13,8 +13,8 @@
#include <stdint.h>
-#define TICKNUM_PER_SECOND 10000000
-#define TICKNUM_PER_TIMER (TICKNUM_PER_SECOND / 10)
+#define TICKNUM_PER_SECOND 10000000UL
+#define TICKNUM_PER_TIMER (TICKNUM_PER_SECOND / TX_TIMER_TICKS_PER_SECOND)
int hwtimer_init(void);
int hwtimer_handler(void);
diff --git a/test/tx/cmake/riscv/bsp/link.lds b/test/tx/cmake/riscv/bsp/link.lds
index 42f08fb6..717530b3 100644
--- a/test/tx/cmake/riscv/bsp/link.lds
+++ b/test/tx/cmake/riscv/bsp/link.lds
@@ -12,6 +12,12 @@ MEMORY
RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M
}
+PHDRS
+{
+ text PT_LOAD FLAGS(5);
+ data PT_LOAD FLAGS(6);
+}
+
SECTIONS
{
. = 0x80000000;
@@ -21,23 +27,24 @@ SECTIONS
*(.text .text.*)
. = ALIGN(0x1000);
PROVIDE(etext = .);
- } > RAM
+ } > RAM :text
.rodata : {
. = ALIGN(16);
*(.srodata .srodata.*)
. = ALIGN(16);
*(.rodata .rodata.*)
- } > RAM
+ } > RAM :text
.data : {
- . = ALIGN(16);
+ . = ALIGN(0x1000);
+ PROVIDE(__global_pointer$ = . + 0x800);
*(.sdata .sdata.*)
. = ALIGN(16);
*(.data .data.*)
- } > RAM
+ } > RAM :data
- .bss : {
+ .bss (NOLOAD) : {
. = ALIGN(16);
_bss_start = .;
*(.sbss .sbss.*)
@@ -45,14 +52,23 @@ SECTIONS
*(.bss .bss.*)
*(COMMON)
_bss_end = .;
- } > RAM
+ } > RAM :data
- .stack : {
+ .stack (NOLOAD) : {
. = ALIGN(4096);
_sysstack_start = .;
. += 0x4000;
_sysstack_end = .;
- } > RAM
+ } > RAM :data
+
+ /* Fixed 64 KiB newlib sbrk heap window. _end stays after it, so the
+ ThreadX first-unused-memory pointer starts past the heap. */
+ .heap (NOLOAD) : {
+ . = ALIGN(16);
+ _heap_start = .;
+ . += 0x10000;
+ _heap_end = .;
+ } > RAM :data
PROVIDE(_end = .);
diff --git a/test/tx/cmake/riscv/bsp/plic.c b/test/tx/cmake/riscv/bsp/plic.c
index a2b44fec..16dad815 100644
--- a/test/tx/cmake/riscv/bsp/plic.c
+++ b/test/tx/cmake/riscv/bsp/plic.c
@@ -52,32 +52,47 @@ int plic_unregister_callback(int irqno)
int plic_init(void)
{
+ uintptr_t hart = riscv_get_core();
+
for (int i = 0; i < MAX_CALLBACK_NUM; i++)
{
callbacks[i] = NULL;
}
+
+ /* Do not depend on the reset state or on a prior boot stage: accept
+ every priority (threshold 0) and start with all sources for this
+ hart disabled. */
+ *(volatile uint32_t *)PLIC_MPRIORITY(hart) = 0;
+ for (int w = 0; w < MAX_CALLBACK_NUM / 32; w++)
+ *(volatile uint32_t *)(PLIC_MENABLE(hart) + w * 4) = 0;
+
return 0;
}
int plic_claim(void)
{
uintptr_t hart = riscv_get_core();
- return (int)(*(uint32_t *)PLIC_MCLAIM(hart));
+ return (int)(*(volatile uint32_t *)PLIC_MCLAIM(hart));
}
void plic_complete(int irqno)
{
uintptr_t hart = riscv_get_core();
- *(uint32_t *)(PLIC_MCOMPLETE(hart)) = (uint32_t)irqno;
+ *(volatile uint32_t *)(PLIC_MCOMPLETE(hart)) = (uint32_t)irqno;
}
int plic_irq_intr(void)
{
int ret = -1;
int irqno = plic_claim();
- if (irqno <= 0 || irqno >= MAX_CALLBACK_NUM) {
- plic_complete(irqno);
- return 0; // spurious or out-of-range, not an error
+
+ if (irqno == 0)
+ return 0;
+
+ if (irqno < 0 || irqno >= MAX_CALLBACK_NUM) {
+ if (irqno > 0)
+ plic_complete(irqno);
+ return -1;
}
if (callbacks[irqno] != NULL)
ret = (callbacks[irqno])(irqno);
diff --git a/test/tx/cmake/riscv/bsp/syscalls.c b/test/tx/cmake/riscv/bsp/syscalls.c
index 802fe8af..40ca86f3 100644
--- a/test/tx/cmake/riscv/bsp/syscalls.c
+++ b/test/tx/cmake/riscv/bsp/syscalls.c
@@ -15,6 +15,7 @@
#include <sys/stat.h>
#include <errno.h>
#include <stdint.h>
+#include <time.h>
#define UART0_THR (*(volatile unsigned char *)0x10000000L)
#define UART0_LSR (*(volatile unsigned char *)0x10000005L)
@@ -25,6 +26,39 @@
#define VIRT_TEST_PASS 0x5555u
#define VIRT_TEST_FAIL 0x3333u
+#define QEMU_TIMEBASE_HZ 10000000ULL
+
+static uint64_t read_time_counter(void)
+{
+#if __riscv_xlen == 64
+ uint64_t value;
+ __asm__ volatile("rdtime %0" : "=r" (value));
+ return value;
+#else
+ uint32_t high;
+ uint32_t low;
+ uint32_t high_check;
+
+ do
+ {
+ __asm__ volatile("rdtimeh %0" : "=r" (high));
+ __asm__ volatile("rdtime %0" : "=r" (low));
+ __asm__ volatile("rdtimeh %0" : "=r" (high_check));
+ } while (high != high_check);
+
+ return ((uint64_t)high << 32) | low;
+#endif
+}
+
+time_t time(time_t *timer)
+{
+ time_t seconds = (time_t)(read_time_counter() / QEMU_TIMEBASE_HZ);
+
+ if (timer != NULL)
+ *timer = seconds;
+ return seconds;
+}
+
/* Called by testcontrol.c when EXTERNAL_EXIT is defined. */
__attribute__((noreturn)) void external_exit(unsigned int code)
{
@@ -50,13 +84,24 @@ int _write(int fd, const char *buf, int count)
return count;
}
-extern char _end[];
+/* Fixed 64 KiB heap window from the linker script. It keeps the newlib
+ heap out of the ThreadX first-unused-memory area, which starts at
+ _end (placed after _heap_end). */
+extern char _heap_start[];
+extern char _heap_end[];
static char *heap_ptr = 0;
void *_sbrk(int incr)
{
if (heap_ptr == 0)
- heap_ptr = _end;
+ heap_ptr = _heap_start;
+
+ if ((incr > 0 && _heap_end - heap_ptr < incr) ||
+ (incr < 0 && heap_ptr - _heap_start < -incr))
+ {
+ errno = ENOMEM;
+ return (void *)-1;
+ }
char *prev = heap_ptr;
heap_ptr += incr;
diff --git a/test/tx/cmake/riscv/bsp/trap.c b/test/tx/cmake/riscv/bsp/trap.c
index 0ac7990a..f54ea2ff 100644
--- a/test/tx/cmake/riscv/bsp/trap.c
+++ b/test/tx/cmake/riscv/bsp/trap.c
@@ -36,9 +36,6 @@ extern void _exit(int code) __attribute__((noreturn));
void trap_handler(uintptr_t mcause, uintptr_t mepc, uintptr_t mtval)
{
- (void)mepc;
- (void)mtval;
-
if (OS_IS_INTERUPT(mcause))
{
if (OS_IS_TICK_INT(mcause))
diff --git a/test/tx/cmake/riscv/bsp/tx_initialize_low_level.S b/test/tx/cmake/riscv/bsp/tx_initialize_low_level.S
index 8d474d66..d72c9b6a 100644
--- a/test/tx/cmake/riscv/bsp/tx_initialize_low_level.S
+++ b/test/tx/cmake/riscv/bsp/tx_initialize_low_level.S
@@ -1,5 +1,6 @@
/***************************************************************************
* Copyright (c) 2026 10xEngineers
+ * 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
@@ -8,10 +9,13 @@
* SPDX-License-Identifier: MIT
**************************************************************************/
+// Portions of this file were generated with AI assistance.
+
/* Trap entry and low-level init for RISC-V QEMU virt regression tests.
Supports both RV32 and RV64 via __riscv_xlen conditionals. */
#include "csr.h"
+#include "tx_port.h"
#if __riscv_xlen == 64
#define STORE sd
@@ -33,13 +37,28 @@
.extern trap_handler
.extern _tx_thread_context_restore
-trap_entry:
+/* Take the interrupt frame size from the port that this BSP is linked
+ against, so the two can never disagree. A port that publishes no
+ contract keeps the historical layout. */
+#ifndef TX_RISCV_TRAP_FRAME_SIZE
#if defined(__riscv_float_abi_single) || defined(__riscv_float_abi_double)
- addi sp, sp, -(65 * REGBYTES)
+#define TX_RISCV_TRAP_FRAME_SIZE (65 * REGBYTES)
#else
- addi sp, sp, -(32 * REGBYTES)
+#define TX_RISCV_TRAP_FRAME_SIZE (32 * REGBYTES)
+#endif
+#endif
+
+/* The RISC-V psABI requires sp to be 16-byte aligned at a call. */
+#ifndef TX_RISCV_TRAP_CALL_FRAME_SIZE
+#define TX_RISCV_TRAP_CALL_FRAME_SIZE 16
#endif
+ .equ TX_TRAP_FRAME_SIZE, TX_RISCV_TRAP_FRAME_SIZE
+ .equ TX_TRAP_CALL_FRAME_SIZE, TX_RISCV_TRAP_CALL_FRAME_SIZE
+
+trap_entry:
+ addi sp, sp, -TX_TRAP_FRAME_SIZE
+
STORE x1, (28 * REGBYTES)(sp)
call _tx_thread_context_save
@@ -47,11 +66,11 @@ trap_entry:
csrr a0, mcause
csrr a1, mepc
csrr a2, mtval
- addi sp, sp, -REGBYTES
+ addi sp, sp, -TX_TRAP_CALL_FRAME_SIZE
STORE ra, 0(sp)
call trap_handler
LOAD ra, 0(sp)
- addi sp, sp, REGBYTES
+ addi sp, sp, TX_TRAP_CALL_FRAME_SIZE
call _tx_thread_context_restore
@@ -80,7 +99,7 @@ _tx_initialize_low_level:
csrrc zero, mstatus, t0
li t0, (MSTATUS_MPP_M | MSTATUS_MPIE)
csrrs zero, mstatus, t0
- li t0, (MIE_MTIE | MIE_MSIE | MIE_MEIE)
+ li t0, (MIE_MTIE | MIE_MEIE) // No MSIE: software interrupts are never dispatched here
csrrs zero, mie, t0
#ifdef __riscv_flen
@@ -89,11 +108,11 @@ _tx_initialize_low_level:
fscsr x0
#endif
- addi sp, sp, -REGBYTES
+ addi sp, sp, -TX_TRAP_CALL_FRAME_SIZE
STORE ra, 0(sp)
call board_init
LOAD ra, 0(sp)
- addi sp, sp, REGBYTES
+ addi sp, sp, TX_TRAP_CALL_FRAME_SIZE
la t0, trap_entry
csrw mtvec, t0
diff --git a/test/tx/cmake/riscv/regression/CMakeLists.txt b/test/tx/cmake/riscv/regression/CMakeLists.txt
index 80b6e3bd..b15247ff 100644
--- a/test/tx/cmake/riscv/regression/CMakeLists.txt
+++ b/test/tx/cmake/riscv/regression/CMakeLists.txt
@@ -110,11 +110,17 @@ set(regression_test_cases
${SOURCE_DIR}/threadx_timer_multiple_accuracy_test.c
${SOURCE_DIR}/threadx_timer_multiple_test.c
${SOURCE_DIR}/threadx_timer_simple_test.c
- # threadx_trace_basic_test.c excluded: references tx_saved_posture
- # (the Linux port's interrupt save variable) which is named
- # interrupt_save on RISC-V.
+ ${SOURCE_DIR}/threadx_trace_basic_test.c
)
+# 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 00000000..b388ad62
--- /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 <stdio.h>
+#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;
+}
diff --git a/test/tx/cmake/riscv/run.sh b/test/tx/cmake/riscv/run.sh
index 3b0905e7..0e4fb7a1 100755
--- a/test/tx/cmake/riscv/run.sh
+++ b/test/tx/cmake/riscv/run.sh
@@ -61,7 +61,11 @@ function generate() {
function build() {
local arch=$1
local build=$2
- cmake --build "build/${arch}_${build}"
+ # -k 0 keeps Ninja going after a target fails, so one broken target no
+ # longer decides whether the targets after it exist. ctest reports a
+ # missing binary as a failing test, which turned a single link error into
+ # a failure count that varied with build scheduling order.
+ cmake --build "build/${arch}_${build}" -- -k 0
}
function run_test() {
@@ -69,13 +73,20 @@ function run_test() {
local build=$2
local parallel_jobs=$3
- pushd "build/${arch}_${build}"
+ # Guard the pushd: with the caller capturing this function's status, set -e
+ # no longer aborts here, so a missing build directory would otherwise let
+ # ctest run in the source tree and report "no tests" as success.
+ pushd "build/${arch}_${build}" || return 1
[ -z "${CTEST_PARALLEL_LEVEL}" ] && parallel="-j${parallel_jobs}"
if [ -z "${CTEST_REPEAT_FAIL}" ]; then
repeat_fail=2
else
repeat_fail=${CTEST_REPEAT_FAIL}
fi
+ # ctest's status is captured and returned past the popd. popd succeeds, so
+ # without this the function would return 0 and a failing configuration
+ # would be reported as a pass.
+ local status=0
ctest $parallel --timeout 1000 \
-O "${arch}_${build}.txt" \
-T test --no-compress-output \
@@ -83,8 +94,9 @@ function run_test() {
--test-output-size-failed 4194304 \
--output-on-failure \
--repeat until-pass:${repeat_fail} \
- --output-junit "${arch}_${build}.xml"
+ --output-junit "${arch}_${build}.xml" || status=$?
popd
+ return $status
}
# Determine repo root and script directory
@@ -129,11 +141,16 @@ if [ "$command" == "build" ]; then
echo ""
done
+ # A failing configuration must not stop the ones after it: under set -e
+ # the loop would abort and leave them unbuilt, which then reads as a wall
+ # of missing-binary test failures. The status is accumulated and returned.
+ build_status=0
for item in $builds; do
echo "Building ${arch} ${item}"
- build "$arch" "$item"
+ build "$arch" "$item" || build_status=$?
echo ""
done
+ [ $build_status -eq 0 ] || exit $build_status
elif [ "$command" == "test" ]; then
cores=$(nproc)
if [ -z "${CTEST_PARALLEL_LEVEL}" ]; then
@@ -152,10 +169,16 @@ elif [ "$command" == "test" ]; then
done
exit $exit_code
else
+ # ctest's status is accumulated rather than allowed to abort the loop.
+ # set -e would otherwise stop at the first failing configuration and
+ # leave every configuration after it untested. This mirrors what the
+ # parallel branch above already does with wait.
+ exit_code=0
for item in $builds; do
echo "Testing ${arch} ${item}"
- run_test "$arch" "$item" "$parallel_jobs"
+ run_test "$arch" "$item" "$parallel_jobs" || exit_code=$?
done
+ exit $exit_code
fi
else
help
diff --git a/test/tx/cmake/thread_transition/CMakeLists.txt b/test/tx/cmake/thread_transition/CMakeLists.txt
new file mode 100644
index 00000000..342ca839
--- /dev/null
+++ b/test/tx/cmake/thread_transition/CMakeLists.txt
@@ -0,0 +1,131 @@
+# Portions of this file were generated with AI assistance.
+
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+cmake_policy(SET CMP0057 NEW)
+
+project(thread_transition_test LANGUAGES C)
+
+set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..)
+set(SOURCE_DIR ${REPO_ROOT}/test/tx/thread_transition)
+
+# TX_NOT_INTERRUPTABLE and TX_DISABLE_ERROR_CHECKING are not among the five build
+# configurations this tree compiles, and the tree builds the whole ThreadX library
+# once per configuration, so neither can be reached from inside the regression
+# suite. Both matter to the completion and termination transitions:
+#
+# - TX_NOT_INTERRUPTABLE selects a different transition, in which the exit
+# notification callback runs with interrupts disabled and the interruptable
+# suspension path -- which is what sets and clears the transition marker in the
+# configurations the tree does build -- is not used at all. The marker is set
+# and cleared there by stores that exist only in that configuration, and a
+# marker left set would make a normally completed thread permanently
+# undeletable.
+#
+# - TX_DISABLE_ERROR_CHECKING removes the _txe_ wrappers, so the public names bind
+# straight to the core services. The refusal lives in the core services for that
+# reason, and the way to show it is to call the public names in a build with no
+# wrappers.
+#
+# So the four sources that change are compiled directly into a test executable, once
+# per combination, with recorders standing behind the scheduler services they call.
+# That is the same technique the module manager tests in this tree use, for the same
+# reason: the code under test cannot be reached through the library the suite links.
+#
+# These executables are deliberately left out of the coverage instrumentation. The
+# same source compiled under different feature macros has a different line set, and
+# merging those into the union the coverage report takes would confuse the figure
+# rather than add to it. The coverage figure belongs to the five configurations the
+# tree builds, and every line this change adds outside a TX_NOT_INTERRUPTABLE guard
+# is in them.
+
+set(transition_kernel_sources
+ ${REPO_ROOT}/common/src/tx_thread_shell_entry.c
+ ${REPO_ROOT}/common/src/tx_thread_terminate.c
+ ${REPO_ROOT}/common/src/tx_thread_delete.c
+ ${REPO_ROOT}/common/src/tx_thread_reset.c
+ ${REPO_ROOT}/common/src/txe_thread_delete.c
+ ${REPO_ROOT}/common/src/txe_thread_reset.c
+ ${REPO_ROOT}/common/src/txe_thread_terminate.c
+ ${REPO_ROOT}/common/src/tx_thread_initialize.c)
+
+set(transition_sources
+ ${SOURCE_DIR}/threadx_thread_transition_configuration_test.c
+ ${transition_kernel_sources})
+
+# The shim goes on the kernel sources only, not on the test. It defines
+# TX_SOURCE_CODE, which is what tells tx_api.h to leave the public service names
+# unmapped so that internal sources can use the core names; the test needs the
+# opposite, because calling the public names and letting tx_api.h decide what they
+# bind to is exactly what the TX_DISABLE_ERROR_CHECKING configuration is here to
+# demonstrate.
+if(MSVC)
+ set(transition_kernel_compile_options
+ "/FI${SOURCE_DIR}/threadx_thread_transition_host_test_port.h")
+ set(transition_undefine_options /UTX_ENABLE_EVENT_TRACE
+ /UTX_ENABLE_EVENT_LOG)
+else()
+ set(transition_kernel_compile_options
+ "-include;${SOURCE_DIR}/threadx_thread_transition_host_test_port.h")
+ set(transition_undefine_options -UTX_ENABLE_EVENT_TRACE
+ -UTX_ENABLE_EVENT_LOG)
+endif()
+
+set_source_files_properties(
+ ${transition_kernel_sources}
+ DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
+ PROPERTIES COMPILE_OPTIONS "${transition_kernel_compile_options}")
+
+# Each entry is a test-name suffix, a colon, and the feature macros that define the
+# configuration, separated by "|". A semicolon cannot be used as that separator: it
+# is CMake's own list separator, so the foreach below would iterate the macros
+# instead of the configurations.
+set(transition_configurations
+ "ni:TX_NOT_INTERRUPTABLE"
+ "ni_no_error_checking:TX_NOT_INTERRUPTABLE|TX_DISABLE_ERROR_CHECKING"
+ "ni_no_notify:TX_NOT_INTERRUPTABLE|TX_DISABLE_NOTIFY_CALLBACKS"
+ "no_error_checking:TX_DISABLE_ERROR_CHECKING")
+
+foreach(configuration ${transition_configurations})
+
+ string(REPLACE ":" ";" configuration_parts ${configuration})
+ list(GET configuration_parts 0 configuration_name)
+ list(GET configuration_parts 1 configuration_macro_text)
+ string(REPLACE "|" ";" configuration_macros ${configuration_macro_text})
+
+ set(test_name threadx_thread_transition_${configuration_name}_test)
+ # Keep the internal target name short. CMake includes it in every object path,
+ # and the descriptive test name can otherwise reach the legacy MSVC MAX_PATH
+ # boundary in a normally nested Windows worktree.
+ set(test_target tx_tt_${configuration_name})
+
+ add_executable(${test_target} ${transition_sources})
+
+ target_include_directories(
+ ${test_target}
+ PRIVATE ${SOURCE_DIR}
+ ${REPO_ROOT}/common/inc
+ ${REPO_ROOT}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc)
+
+ target_compile_definitions(${test_target} PRIVATE ${configuration_macros})
+
+ # This directory is configured once per build configuration of the tree, so
+ # these executables inherit whichever feature macros that configuration sets --
+ # which is useful, since it means the two configurations above are also seen in
+ # combination with stack checking and with the other four. The one that cannot
+ # be inherited is event tracing: the trace macros in these sources reference the
+ # trace component's buffer and registry, and this harness links four kernel
+ # sources rather than the library, so the references would not resolve. The
+ # trace insertions in these same sources are covered by the tree's own
+ # trace_build configuration, where the whole library is linked, so nothing is
+ # lost by turning tracing off here. Event logging is turned off for the same
+ # reason; no configuration of the tree enables it.
+ #
+ # The undefine options have to reach the test source as well as the kernel sources,
+ # because TX_ENABLE_EVENT_TRACE is visible to tx_api.h and the two must agree on
+ # what the headers declare. Target compile options land after the directory's
+ # definition flags on the command line, which is what makes them effective.
+ target_compile_options(${test_target} PRIVATE ${transition_undefine_options})
+
+ add_test(${CMAKE_BUILD_TYPE}::${test_name} ${test_target})
+
+endforeach()