summaryrefslogtreecommitdiff
path: root/ports/cortex_r52/gnu
diff options
context:
space:
mode:
Diffstat (limited to 'ports/cortex_r52/gnu')
-rw-r--r--ports/cortex_r52/gnu/CMakeLists.txt89
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt727
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/board.h117
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/bsp_boot.c170
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.c149
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.h72
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/console.c170
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/console.h51
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_clz.c359
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_fiq.c215
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m2.c224
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m3.c249
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m5.c285
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_mpu.c284
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_nesting.c216
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_threadx.c393
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_verify.c186
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_verify.h41
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S710
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.c360
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.h81
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/irq_dispatch.c306
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link.lds141
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c456
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h169
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h135
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/test/run_fvp_test.py111
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/timer.c174
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/timer.h67
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/tx_initialize_low_level.S107
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/uart_pl011.c127
-rw-r--r--ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/uart_pl011.h47
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt521
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/board.h145
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/bsp_boot.c1037
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/cache.c346
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/cache.h70
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/demo_fiq_s32z280.c253
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/demo_nesting_s32z280.c240
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/demo_s32z280.c812
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/demo_vfp_s32z280.c283
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S1034
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/gic_probe.c72
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/gic_probe.h44
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/gicv3.c409
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/gicv3.h86
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/irq_dispatch.c462
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/linflexd.c276
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/linflexd.h61
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds198
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c622
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h142
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h274
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/readme_s32z280.txt178
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.c304
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.h161
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c237
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.h79
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/timer.c186
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/timer.h72
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/tools/read_identity.gdb160
-rw-r--r--ports/cortex_r52/gnu/example_build/s32z280_evb/tx_initialize_low_level.S110
-rw-r--r--ports/cortex_r52/gnu/inc/tx_port.h393
-rw-r--r--ports/cortex_r52/gnu/readme_threadx.txt201
-rw-r--r--ports/cortex_r52/gnu/src/tx_port_offset_check.c86
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_context_restore.S247
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_context_save.S193
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_fiq_context_restore.S237
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_fiq_context_save.S195
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_fiq_nesting_end.S107
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_fiq_nesting_start.S99
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_interrupt_control.S108
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_interrupt_disable.S105
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_interrupt_restore.S96
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_irq_nesting_end.S106
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_irq_nesting_start.S99
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_schedule.S240
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_stack_build.S170
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_system_return.S169
-rw-r--r--ports/cortex_r52/gnu/src/tx_thread_vectored_context_save.S181
-rw-r--r--ports/cortex_r52/gnu/src/tx_timer_interrupt.S270
81 files changed, 19164 insertions, 0 deletions
diff --git a/ports/cortex_r52/gnu/CMakeLists.txt b/ports/cortex_r52/gnu/CMakeLists.txt
new file mode 100644
index 00000000..6444a063
--- /dev/null
+++ b/ports/cortex_r52/gnu/CMakeLists.txt
@@ -0,0 +1,89 @@
+# Copyright (c) 2026 Eclipse ThreadX contributors
+# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
+
+target_sources(${PROJECT_NAME}
+ PRIVATE
+ # {{BEGIN_TARGET_SOURCES}}
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_context_restore.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_context_save.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_fiq_context_restore.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_fiq_context_save.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_fiq_nesting_end.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_fiq_nesting_start.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_interrupt_control.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_interrupt_disable.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_interrupt_restore.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_irq_nesting_end.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_irq_nesting_start.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_schedule.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_stack_build.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_system_return.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_thread_vectored_context_save.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_timer_interrupt.S
+ ${CMAKE_CURRENT_LIST_DIR}/src/tx_port_offset_check.c
+ # {{END_TARGET_SOURCES}}
+)
+
+target_include_directories(${PROJECT_NAME}
+ SYSTEM
+ PUBLIC
+ ${CMAKE_CURRENT_LIST_DIR}/inc
+)
+
+# Lazy floating-point context save and restore. PUBLIC because the assembly
+# in the library and the application must agree: the flag changes which
+# registers the context switch saves, so a mismatched application would either
+# lose floating-point state or misread the thread control block. Requires a
+# floating-point ABI, so pair it with -DTX_R52_FLOAT_ABI=hard.
+option(TX_R52_ENABLE_VFP "Build with lazy VFP context save/restore" OFF)
+
+# FIQ support and interrupt nesting. These change which registers the
+# assembly saves and what tx_port.h reports in TX_PORT_SPECIFIC_BUILD_OPTIONS,
+# so like the VFP flag they must be PUBLIC: library and application have to
+# agree. FIQ nesting additionally requires FIQ support.
+option(TX_R52_ENABLE_FIQ "Build with FIQ support" OFF)
+option(TX_R52_ENABLE_IRQ_NESTING "Build with nested IRQ support" OFF)
+option(TX_R52_ENABLE_FIQ_NESTING "Build with nested FIQ support" OFF)
+
+if(TX_R52_ENABLE_FIQ)
+ target_compile_definitions(${PROJECT_NAME} PUBLIC TX_ENABLE_FIQ_SUPPORT)
+endif()
+if(TX_R52_ENABLE_IRQ_NESTING)
+ target_compile_definitions(${PROJECT_NAME} PUBLIC TX_ENABLE_IRQ_NESTING)
+endif()
+if(TX_R52_ENABLE_FIQ_NESTING)
+ if(NOT TX_R52_ENABLE_FIQ)
+ message(FATAL_ERROR
+ "TX_R52_ENABLE_FIQ_NESTING requires TX_R52_ENABLE_FIQ: nested FIQ "
+ "handling is meaningless without FIQ support compiled in.")
+ endif()
+ target_compile_definitions(${PROJECT_NAME} PUBLIC TX_ENABLE_FIQ_NESTING)
+endif()
+if(TX_R52_ENABLE_VFP)
+ if(NOT TX_R52_FLOAT_ABI STREQUAL "hard")
+ message(FATAL_ERROR
+ "TX_R52_ENABLE_VFP requires TX_R52_FLOAT_ABI=hard, but it is "
+ "'${TX_R52_FLOAT_ABI}'. TX_ENABLE_VFP_SUPPORT enables the VMRS, "
+ "VSTMDB and VLDMIA blocks in the port assembly, and a soft float "
+ "ABI leaves the assembler with no FPU to accept them, so this "
+ "combination fails to assemble. Use -DTX_R52_FLOAT_ABI=hard.")
+ endif()
+ target_compile_definitions(${PROJECT_NAME} PUBLIC TX_ENABLE_VFP_SUPPORT)
+endif()
+
+# Armv8-R AEM FVP example builds (boot check now, kernel demo from AR1/M2).
+# Off by default so the default build stays a pure static-library build.
+option(TX_R52_BUILD_FVP_EXAMPLE "Build the Armv8-R AEM FVP example targets" OFF)
+if(TX_R52_BUILD_FVP_EXAMPLE)
+ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/example_build/fvp_baser_aemv8r)
+endif()
+
+# NXP S32Z280-594EVB example build. Separate option from the FVP example so
+# that FVP regression stays green while silicon bring-up is in progress, and so
+# neither build can perturb the other: the two boards differ in reset state,
+# memory map and instruction set at reset.
+option(TX_R52_BUILD_S32Z280_EXAMPLE "Build the NXP S32Z280-594EVB example targets" OFF)
+if(TX_R52_BUILD_S32Z280_EXAMPLE)
+ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/example_build/s32z280_evb)
+endif()
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt
new file mode 100644
index 00000000..ee01eb3e
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt
@@ -0,0 +1,727 @@
+# Copyright (c) 2026 Eclipse ThreadX contributors
+# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
+#
+# Example builds for Cortex-R52 on the Armv8-R AEM FVP (FVP_BaseR_AEMv8R).
+
+set(FVP_DIR ${CMAKE_CURRENT_LIST_DIR})
+
+# A bare-metal image has one flat DRAM region and no OS page permissions; access
+# control belongs to the MPU, so an RWX segment is expected here rather than a
+# mistake. GNU ld has warned about them since binutils 2.39 and takes this flag
+# to stay quiet. ld.lld does not warn and rejects the flag outright, failing the
+# link with "unknown argument", so the suppression is chosen by toolchain
+# instead of being spelled into every target.
+if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
+ set(R52_LINK_QUIET_RWX -Wl,--no-warn-rwx-segments)
+else()
+ set(R52_LINK_QUIET_RWX)
+endif()
+
+# Console sources. Both backends are always compiled; console.c selects one at
+# compile time and --gc-sections drops the unused one, which keeps every image
+# building in either configuration without per-target source juggling.
+set(R52_CONSOLE_SOURCES
+ ${FVP_DIR}/console.c
+ ${FVP_DIR}/uart_pl011.c
+ ${FVP_DIR}/mpu.c
+)
+
+# AR1/M1 -- boot check. Verifies the EL2 configuration, the drop to EL1 and
+# the EL2 HVC seam. Standalone by design: it does not link ThreadX, so a
+# failure here is unambiguously a boot problem rather than a kernel problem.
+add_executable(boot_check.elf EXCLUDE_FROM_ALL
+ ${FVP_DIR}/entry.S
+ ${R52_CONSOLE_SOURCES}
+ ${FVP_DIR}/bsp_boot.c
+)
+
+target_include_directories(boot_check.elf PRIVATE ${FVP_DIR})
+
+target_link_options(boot_check.elf PRIVATE
+ -T${FVP_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=boot_check.map
+ ${R52_LINK_QUIET_RWX}
+)
+
+# AR1/M2 -- cooperative two-thread demo. Links ThreadX and exercises the
+# ported context-switch assembly with no interrupts and no timer tick.
+add_executable(demo_m2.elf EXCLUDE_FROM_ALL
+ ${FVP_DIR}/entry.S
+ ${R52_CONSOLE_SOURCES}
+ ${FVP_DIR}/tx_initialize_low_level.S
+ ${FVP_DIR}/demo_m2.c
+)
+
+target_link_libraries(demo_m2.elf PRIVATE threadx)
+
+target_include_directories(demo_m2.elf PRIVATE
+ ${FVP_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+)
+
+target_link_options(demo_m2.elf PRIVATE
+ -T${FVP_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=demo_m2.map
+ ${R52_LINK_QUIET_RWX}
+)
+
+# AR1/M3 -- periodic tick and preemptive scheduling. Adds GICv3, the generic
+# timer and the ThreadX IRQ path. TX_R52_USE_THREADX_IRQ routes the EL1 IRQ
+# vector into _tx_thread_context_save and has _tx_initialize_low_level bring up
+# the interrupt controller; images without it keep the fault reporter.
+add_executable(demo_m3.elf EXCLUDE_FROM_ALL
+ ${FVP_DIR}/entry.S
+ ${R52_CONSOLE_SOURCES}
+ ${FVP_DIR}/gicv3.c
+ ${FVP_DIR}/timer.c
+ ${FVP_DIR}/irq_dispatch.c
+ ${FVP_DIR}/tx_initialize_low_level.S
+ ${FVP_DIR}/demo_m3.c
+)
+
+target_compile_definitions(demo_m3.elf PRIVATE TX_R52_USE_THREADX_IRQ)
+
+target_link_libraries(demo_m3.elf PRIVATE threadx)
+
+target_include_directories(demo_m3.elf PRIVATE
+ ${FVP_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+)
+
+target_link_options(demo_m3.elf PRIVATE
+ -T${FVP_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=demo_m3.map
+ ${R52_LINK_QUIET_RWX}
+)
+
+# AR1/M4 -- the standard eight-thread ThreadX demo. demo_threadx.c is the
+# shipped sample kept byte-identical apart from its header note and one call
+# into the verification harness, which lives in demo_verify.c.
+add_executable(demo_threadx.elf EXCLUDE_FROM_ALL
+ ${FVP_DIR}/entry.S
+ ${R52_CONSOLE_SOURCES}
+ ${FVP_DIR}/gicv3.c
+ ${FVP_DIR}/timer.c
+ ${FVP_DIR}/irq_dispatch.c
+ ${FVP_DIR}/tx_initialize_low_level.S
+ ${FVP_DIR}/demo_threadx.c
+ ${FVP_DIR}/demo_verify.c
+)
+
+target_compile_definitions(demo_threadx.elf PRIVATE TX_R52_USE_THREADX_IRQ)
+
+target_link_libraries(demo_threadx.elf PRIVATE threadx)
+
+target_include_directories(demo_threadx.elf PRIVATE
+ ${FVP_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+)
+
+target_link_options(demo_threadx.elf PRIVATE
+ -T${FVP_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=demo_threadx.map
+ ${R52_LINK_QUIET_RWX}
+)
+
+# CLZ lowest-set-bit regression. tx_port.h replaces tx_thread.h's portable
+# priority search with a CLZ instruction, and that macro decides which thread
+# runs next on every suspend and resume -- so it gets an image of its own rather
+# than being covered incidentally by demos that would merely hang if it broke.
+# Needs the tick because the checks run in a thread; needs nothing else.
+add_executable(demo_clz.elf EXCLUDE_FROM_ALL
+ ${FVP_DIR}/entry.S
+ ${R52_CONSOLE_SOURCES}
+ ${FVP_DIR}/gicv3.c
+ ${FVP_DIR}/timer.c
+ ${FVP_DIR}/irq_dispatch.c
+ ${FVP_DIR}/tx_initialize_low_level.S
+ ${FVP_DIR}/demo_clz.c
+)
+
+target_compile_definitions(demo_clz.elf PRIVATE TX_R52_USE_THREADX_IRQ)
+
+target_link_libraries(demo_clz.elf PRIVATE threadx)
+
+target_include_directories(demo_clz.elf PRIVATE
+ ${FVP_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+)
+
+target_link_options(demo_clz.elf PRIVATE
+ -T${FVP_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=demo_clz.map
+ ${R52_LINK_QUIET_RWX}
+)
+
+# AR1/M5 -- lazy VFP context save and restore. Only meaningful when the
+# library was built with TX_R52_ENABLE_VFP and a floating-point ABI, so the
+# target exists only in that configuration.
+if(TX_R52_ENABLE_VFP)
+ add_executable(demo_m5.elf EXCLUDE_FROM_ALL
+ ${FVP_DIR}/entry.S
+ ${R52_CONSOLE_SOURCES}
+ ${FVP_DIR}/gicv3.c
+ ${FVP_DIR}/timer.c
+ ${FVP_DIR}/irq_dispatch.c
+ ${FVP_DIR}/tx_initialize_low_level.S
+ ${FVP_DIR}/demo_m5.c
+ )
+
+ target_compile_definitions(demo_m5.elf PRIVATE TX_R52_USE_THREADX_IRQ)
+
+ target_link_libraries(demo_m5.elf PRIVATE threadx)
+
+ target_include_directories(demo_m5.elf PRIVATE
+ ${FVP_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+ )
+
+ target_link_options(demo_m5.elf PRIVATE
+ -T${FVP_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=demo_m5.map
+ ${R52_LINK_QUIET_RWX}
+ )
+endif()
+
+# AR1/M5 -- PMSAv8-R protection and caches. Verifies enforcement, so it needs
+# the recoverable data-abort path in entry.S (TX_R52_MPU_FAULT_TEST) and the
+# protection itself (TX_R52_ENABLE_MPU), independently of the global option.
+add_executable(demo_mpu.elf EXCLUDE_FROM_ALL
+ ${FVP_DIR}/entry.S
+ ${R52_CONSOLE_SOURCES}
+ ${FVP_DIR}/gicv3.c
+ ${FVP_DIR}/timer.c
+ ${FVP_DIR}/irq_dispatch.c
+ ${FVP_DIR}/tx_initialize_low_level.S
+ ${FVP_DIR}/demo_mpu.c
+)
+
+target_compile_definitions(demo_mpu.elf PRIVATE
+ TX_R52_USE_THREADX_IRQ
+ TX_R52_ENABLE_MPU
+ TX_R52_MPU_FAULT_TEST
+)
+
+target_link_libraries(demo_mpu.elf PRIVATE threadx)
+
+target_include_directories(demo_mpu.elf PRIVATE
+ ${FVP_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+)
+
+target_link_options(demo_mpu.elf PRIVATE
+ -T${FVP_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=demo_mpu.map
+ ${R52_LINK_QUIET_RWX}
+)
+
+# Nested IRQ handling. Needs the library built with TX_R52_ENABLE_IRQ_NESTING,
+# because _tx_thread_irq_nesting_start only re-enables IRQ usefully when
+# tx_port.h agrees the configuration supports it, and entry.S only emits the
+# start/end pairing under the same macro. The image therefore exists in that
+# configuration only, as demo_m5.elf does for VFP.
+if(TX_R52_ENABLE_IRQ_NESTING)
+ add_executable(demo_nesting.elf EXCLUDE_FROM_ALL
+ ${FVP_DIR}/entry.S
+ ${R52_CONSOLE_SOURCES}
+ ${FVP_DIR}/gicv3.c
+ ${FVP_DIR}/timer.c
+ ${FVP_DIR}/irq_dispatch.c
+ ${FVP_DIR}/tx_initialize_low_level.S
+ ${FVP_DIR}/demo_nesting.c
+ )
+
+ target_compile_definitions(demo_nesting.elf PRIVATE TX_R52_USE_THREADX_IRQ)
+
+ target_link_libraries(demo_nesting.elf PRIVATE threadx)
+
+ target_include_directories(demo_nesting.elf PRIVATE
+ ${FVP_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+ )
+
+ target_link_options(demo_nesting.elf PRIVATE
+ -T${FVP_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=demo_nesting.map
+ ${R52_LINK_QUIET_RWX}
+ )
+endif()
+
+# Nested FIQ handling. Needs TX_R52_ENABLE_FIQ_NESTING, which the port-level
+# CMakeLists already requires TX_R52_ENABLE_FIQ alongside, so the image exists
+# only where both the FIQ vector path and the nesting pairing are compiled in.
+if(TX_R52_ENABLE_FIQ_NESTING)
+ add_executable(demo_fiq.elf EXCLUDE_FROM_ALL
+ ${FVP_DIR}/entry.S
+ ${R52_CONSOLE_SOURCES}
+ ${FVP_DIR}/gicv3.c
+ ${FVP_DIR}/timer.c
+ ${FVP_DIR}/irq_dispatch.c
+ ${FVP_DIR}/tx_initialize_low_level.S
+ ${FVP_DIR}/demo_fiq.c
+ )
+
+ target_compile_definitions(demo_fiq.elf PRIVATE TX_R52_USE_THREADX_IRQ)
+
+ target_link_libraries(demo_fiq.elf PRIVATE threadx)
+
+ target_include_directories(demo_fiq.elf PRIVATE
+ ${FVP_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+ )
+
+ target_link_options(demo_fiq.elf PRIVATE
+ -T${FVP_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=demo_fiq.map
+ ${R52_LINK_QUIET_RWX}
+ )
+endif()
+
+# Every image built here, in the order they should be exercised.
+set(R52_IMAGES boot_check.elf demo_m2.elf demo_m3.elf demo_threadx.elf demo_mpu.elf
+ demo_clz.elf)
+
+# The linker script is passed with -T, which CMake does not treat as a
+# dependency, so editing it would not trigger a relink and stale images would
+# be tested against new region boundaries. Declare it explicitly.
+foreach(image IN LISTS R52_IMAGES)
+ set_target_properties(${image} PROPERTIES LINK_DEPENDS ${FVP_DIR}/link.lds)
+endforeach()
+if(TX_R52_ENABLE_VFP)
+ list(APPEND R52_IMAGES demo_m5.elf)
+endif()
+if(TX_R52_ENABLE_IRQ_NESTING)
+ list(APPEND R52_IMAGES demo_nesting.elf)
+endif()
+if(TX_R52_ENABLE_FIQ_NESTING)
+ list(APPEND R52_IMAGES demo_fiq.elf)
+endif()
+
+# Console backend. Semihosting is the default because it needs no peripheral
+# and so cannot be broken by a wrong memory map; the PL011 path is what real
+# silicon will use.
+# Protection and caches for every image. Off by default so a bring-up failure
+# can always be reproduced with the simplest possible memory configuration.
+option(TX_R52_ENABLE_MPU
+ "Enable the PMSAv8-R MPU and caches in every image" OFF)
+if(TX_R52_ENABLE_MPU)
+ foreach(image IN LISTS R52_IMAGES)
+ target_compile_definitions(${image} PRIVATE TX_R52_ENABLE_MPU)
+ endforeach()
+endif()
+
+option(TX_R52_CONSOLE_PL011
+ "Use the PL011 UART for console output instead of semihosting" OFF)
+if(TX_R52_CONSOLE_PL011)
+ foreach(image IN LISTS R52_IMAGES)
+ target_compile_definitions(${image} PRIVATE TX_R52_CONSOLE_PL011)
+ endforeach()
+endif()
+
+# ---------------------------------------------------------------------------
+# ThreadX modules: the module manager, with a sample module linked into the
+# module area and loaded in place from there.
+#
+# This is the AR2 deliverable that the S32Z280 example cannot be: the silicon
+# module demonstration is judged by a person reading a console through a debug
+# probe, and this one is judged by ctest. The image below is the same port
+# under the same three passes, reporting one result line the runner matches.
+#
+# This target deliberately does NOT link the threadx library's port assembly.
+# The module port carries its own copies of the scheduler, context restore and
+# stack build, because the region switch happens in the scheduler and a module
+# thread starts in a different processor mode. Linking both would give two
+# definitions of every one of them.
+#
+# Guarded by an option, off by default, because it is the only target that needs
+# TXM_MODULE_MANAGER in entry.S and the only one that links common_modules.
+# ---------------------------------------------------------------------------
+
+option(TX_R52_BUILD_FVP_MODULE_EXAMPLE
+ "Build the Armv8-R AEM FVP ThreadX module manager example" OFF)
+
+if(TX_R52_BUILD_FVP_MODULE_EXAMPLE)
+
+ set(MOD_DIR ${CMAKE_SOURCE_DIR}/ports_module/cortex_r52/gnu)
+ set(MOD_FVP ${MOD_DIR}/example_build/fvp_baser_aemv8r)
+
+ # ---------------------------------------------------------------------
+ # A second ThreadX library, built with the module port's headers.
+ #
+ # This is not optional and not a convenience. The module port's tx_port.h
+ # adds the owning module instance to TX_QUEUE, TX_SEMAPHORE,
+ # TX_EVENT_FLAGS_GROUP and TX_TIMER, and the module fields to TX_THREAD.
+ # Measured: TX_QUEUE is 68 bytes against the base port's 60, TX_SEMAPHORE 40
+ # against 32, TX_THREAD 236 against 184. A kernel compiled against the base
+ # port and a manager compiled against this one disagree about every object,
+ # and they link without complaint, because C linking does not compare struct
+ # layouts. The result would be memory corruption at run time with nothing in
+ # the build to hint at it.
+ #
+ # Guarded by if(NOT TARGET) because the S32Z280 module example defines the
+ # same library from its own directory, and the two example options can be on
+ # at once. The definitions are identical -- both describe the module port,
+ # not a board -- so whichever directory CMake reaches first may create it.
+ # ---------------------------------------------------------------------
+
+ if(NOT TARGET threadx_module)
+
+ file(GLOB TX_COMMON_SRC ${CMAKE_SOURCE_DIR}/common/src/*.c)
+
+ set(TX_R52_PORT_SRC ${CMAKE_SOURCE_DIR}/ports/cortex_r52/gnu/src)
+
+ # The base port's assembly is reused except for the five files the module
+ # port replaces, and tx_port_offset_check.c is included deliberately: if
+ # the module headers moved any offset that assembly depends on, that check
+ # is what says so.
+ add_library(threadx_module STATIC EXCLUDE_FROM_ALL
+ ${TX_COMMON_SRC}
+ ${TX_R52_PORT_SRC}/tx_port_offset_check.c
+ ${TX_R52_PORT_SRC}/tx_thread_fiq_context_restore.S
+ ${TX_R52_PORT_SRC}/tx_thread_fiq_context_save.S
+ ${TX_R52_PORT_SRC}/tx_thread_fiq_nesting_end.S
+ ${TX_R52_PORT_SRC}/tx_thread_fiq_nesting_start.S
+ ${TX_R52_PORT_SRC}/tx_thread_interrupt_control.S
+ ${TX_R52_PORT_SRC}/tx_thread_interrupt_disable.S
+ ${TX_R52_PORT_SRC}/tx_thread_interrupt_restore.S
+ ${TX_R52_PORT_SRC}/tx_thread_irq_nesting_end.S
+ ${TX_R52_PORT_SRC}/tx_thread_irq_nesting_start.S
+ ${TX_R52_PORT_SRC}/tx_thread_vectored_context_save.S
+ ${TX_R52_PORT_SRC}/tx_timer_interrupt.S
+ )
+
+ target_include_directories(threadx_module PUBLIC
+ ${MOD_DIR}/inc
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_manager/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/inc
+ )
+
+ target_compile_definitions(threadx_module PUBLIC TXM_MODULE_MANAGER)
+
+ endif()
+
+ # The portable half of the module manager. Globbed rather than listed: it is
+ # a whole upstream component, not a selection from one, and a file added to it
+ # upstream should not need a change here to be compiled.
+
+ file(GLOB TXM_MANAGER_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_manager/src/*.c)
+
+ # The module-side library: the API shims that run inside the module. These
+ # belong to the module's image and must not reach the manager's link -- they
+ # define the same names as the kernel's own entry points, and as objects they
+ # beat the kernel's static library, so in one link every service call the
+ # manager makes is redirected into the module.
+
+ file(GLOB TXM_MODULE_LIB_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_lib/src/*.c)
+
+ # ------------------------------------------------------------------------
+ # The module, built as its own image and reduced to a raw binary.
+ # ------------------------------------------------------------------------
+
+ add_executable(fvp_demo_module.elf EXCLUDE_FROM_ALL
+ ${MOD_FVP}/txm_module_preamble.S
+ ${MOD_FVP}/sample_threadx_module.c
+ ${MOD_DIR}/module_lib/src/txm_module_thread_shell_entry.c
+ ${MOD_DIR}/module_lib/src/txm_module_gcc_setup.S
+ ${TXM_MODULE_LIB_SRC}
+ )
+
+ # No TXM_MODULE_MANAGER here: that define selects the kernel side of the
+ # shared headers, and this is the other side.
+
+ target_include_directories(fvp_demo_module.elf PRIVATE
+ ${FVP_DIR}
+ ${MOD_DIR}/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/inc
+ ${CMAKE_SOURCE_DIR}/common/inc
+ )
+
+ # Position independent, and only here. These five flags are what make the
+ # module relocatable, and putting any of them on the manager would be a bug:
+ # the manager is the resident image, linked absolutely at the address it
+ # boots from, and -msingle-pic-base in particular would have it treat r9 as
+ # a PIC base that nothing sets up.
+ #
+ # -fpic data references go through the GOT
+ # -msingle-pic-base r9 is the GOT base, and the caller sets
+ # it -- which is what the manager's
+ # thread stack build does
+ # -mno-pic-data-is-text-relative the module's data is NOT at a fixed
+ # offset from its code: code is loaded in
+ # place in the module area and data is
+ # allocated from the byte pool, so the gap
+ # between them is decided at run time
+ # -fno-plt no procedure linkage table, which a
+ # module has no loader to populate
+ # -mno-long-calls see below
+ #
+ # -mno-long-calls is the interesting one. The project-wide C flags carry
+ # -mlong-calls, and under -fpic that makes GCC route EVERY call through the
+ # GOT -- R_ARM_GOT32 instead of R_ARM_CALL -- including the shell entry's
+ # call to _gcc_setup, which is the function that fills the GOT in. The
+ # module would load, enter its shell entry, read a zero out of the
+ # not-yet-written GOT and branch to address 0. A module never needs long
+ # calls: it is one contiguous blob and it never calls out of itself, because
+ # kernel services go through the dispatcher function pointer in its entry
+ # info.
+
+ target_compile_options(fvp_demo_module.elf PRIVATE
+ -g
+ -fpic
+ -fno-plt
+ -mno-pic-data-is-text-relative
+ -msingle-pic-base
+ -mno-long-calls
+ )
+
+ target_link_options(fvp_demo_module.elf PRIVATE
+ -T${MOD_FVP}/link_demo_module.lds
+ -nostartfiles
+ -nostdlib
+ -Wl,-Map=fvp_demo_module.map
+ ${R52_LINK_QUIET_RWX}
+ )
+
+ set_target_properties(fvp_demo_module.elf PROPERTIES
+ LINK_DEPENDS ${MOD_FVP}/link_demo_module.lds)
+
+ # The raw image the manager embeds. Written into the binary directory
+ # because that is the include path module_blob.S searches.
+
+ add_custom_command(
+ OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin
+ COMMAND ${CMAKE_OBJCOPY} -O binary
+ $<TARGET_FILE:fvp_demo_module.elf>
+ ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin
+ DEPENDS fvp_demo_module.elf
+ COMMENT "Converting the demonstration module to a raw image"
+ VERBATIM
+ )
+
+ add_custom_target(fvp_demo_module_bin
+ DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin
+ )
+
+ # ------------------------------------------------------------------------
+ # The manager images.
+ #
+ # Two of them, and everything except the application file is the same, so
+ # the shared part is a list and a function rather than a second copy. The
+ # duplicate that a copy would create is not a style question here: an image
+ # built with a different set of module manager sources, or without
+ # TX_R52_ENABLE_MPU, would still link and would test something other than
+ # what its name says.
+ # ------------------------------------------------------------------------
+
+ set(TX_R52_MODULE_IMAGE_SRC
+ # Board support, shared with the other FVP examples
+ ${FVP_DIR}/entry.S
+ ${FVP_DIR}/console.c
+ ${FVP_DIR}/uart_pl011.c
+ ${FVP_DIR}/mpu.c
+ ${FVP_DIR}/cache.c
+ ${FVP_DIR}/gicv3.c
+ ${FVP_DIR}/timer.c
+ ${FVP_DIR}/irq_dispatch.c
+ ${FVP_DIR}/tx_initialize_low_level.S
+
+ # Module manager port
+ ${MOD_DIR}/module_manager/src/tx_thread_schedule.S
+ ${MOD_DIR}/module_manager/src/tx_thread_context_save.S
+ ${MOD_DIR}/module_manager/src/tx_thread_context_restore.S
+ ${MOD_DIR}/module_manager/src/tx_thread_stack_build.S
+ ${MOD_DIR}/module_manager/src/tx_thread_system_return.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_svc_handler.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_fault_capture.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_user_mode_entry.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_thread_stack_build.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_mm_register_setup.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_memory_fault_handler.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_memory_fault_notify.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_alignment_adjust.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_external_memory_enable.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_offset_check.c
+
+ # The module, as bytes rather than as objects. module_blob.S includes the
+ # raw image built by fvp_demo_module.elf, so none of the module's symbols
+ # enter this link.
+ ${MOD_FVP}/module_blob.S
+
+ # The portable module manager
+ ${TXM_MANAGER_SRC}
+ )
+
+ # .incbin searches the assembler's include paths, not the source tree, and
+ # demo_module.bin is generated -- so the build directory has to be on that
+ # path or module_blob.S cannot find the image. Set on the file rather than
+ # per target, because both images include the same blob the same way.
+
+ set_source_files_properties(${MOD_FVP}/module_blob.S PROPERTIES
+ OBJECT_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin
+ COMPILE_OPTIONS "-Wa,-I${CMAKE_CURRENT_BINARY_DIR}"
+ )
+
+ function(threadx_r52_add_module_image target_name application)
+
+ add_executable(${target_name} EXCLUDE_FROM_ALL
+ ${TX_R52_MODULE_IMAGE_SRC}
+ ${application}
+ )
+
+ target_include_directories(${target_name} PRIVATE
+ ${FVP_DIR}
+ ${MOD_DIR}/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_manager/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/inc
+ )
+
+ # TX_R52_ENABLE_MPU is not optional for these images, unlike the AR1 demos
+ # where it is a switch: without protection there is no module boundary to
+ # test, and the manager's load window over the module area is programmed by
+ # mpu_init. TX_R52_USE_THREADX_IRQ brings up the tick, which the module
+ # needs for the tx_thread_sleep that proves a kernel call returns.
+
+ target_compile_definitions(${target_name} PRIVATE
+ TXM_MODULE_MANAGER
+ TX_R52_USE_THREADX_IRQ
+ TX_R52_ENABLE_MPU
+ )
+
+ target_compile_options(${target_name} PRIVATE -g)
+
+ add_dependencies(${target_name} fvp_demo_module_bin)
+
+ target_link_options(${target_name} PRIVATE
+ -T${MOD_FVP}/link_module.lds
+ -nostartfiles
+ -Wl,-Map=${target_name}.map
+ ${R52_LINK_QUIET_RWX}
+ )
+
+ set_target_properties(${target_name} PROPERTIES
+ LINK_DEPENDS ${MOD_FVP}/link_module.lds)
+
+ target_link_libraries(${target_name} PRIVATE threadx_module)
+
+ endfunction()
+
+ # AR2. What the hardware does about a module that misbehaves.
+
+ threadx_r52_add_module_image(fvp_module.elf
+ ${MOD_FVP}/sample_threadx_module_manager.c)
+
+ # And what the LOADER does about a module whose property word is not the
+ # 0x02000003 every example in the tree ships. A separate image because four
+ # of its six cases never run a module at all, so it has a different subject
+ # and a different verdict; it shares the blob and nothing else.
+
+ threadx_r52_add_module_image(fvp_module_properties.elf
+ ${MOD_FVP}/sample_threadx_module_properties.c)
+
+endif()
+
+# Run a target on the FVP. Each image exits by itself through the
+# semihosting SYS_EXIT call, so no host-side timeout is needed. UART0 is
+# routed to stdout unconditionally so that PL011-console images are visible
+# too; it is harmless for semihosting images.
+find_program(FVP_BASER_AEMV8R FVP_BaseR_AEMv8R
+ HINTS $ENV{HOME}/FVP_Base_AEMv8R_11.32_19/bin
+)
+if(FVP_BASER_AEMV8R)
+ function(threadx_r52_add_fvp_run target_name run_target description)
+ add_custom_target(${run_target}
+ COMMAND ${FVP_BASER_AEMV8R}
+ -C cluster0.NUM_CORES=1
+ -C bp.vis.disable_visualisation=1
+ -C bp.terminal_0.start_telnet=0
+ -C bp.pl011_uart0.out_file=-
+ -C bp.pl011_uart0.unbuffered_output=1
+ -a $<TARGET_FILE:${target_name}>
+ DEPENDS ${target_name}
+ USES_TERMINAL
+ COMMENT "${description}"
+ )
+ endfunction()
+
+ threadx_r52_add_fvp_run(boot_check.elf run-boot-check-r52
+ "Running AR1/M1 boot check on FVP_BaseR_AEMv8R...")
+ threadx_r52_add_fvp_run(demo_m2.elf run-demo-m2-r52
+ "Running AR1/M2 cooperative switch demo on FVP_BaseR_AEMv8R...")
+ threadx_r52_add_fvp_run(demo_m3.elf run-demo-m3-r52
+ "Running AR1/M3 tick and preemption demo on FVP_BaseR_AEMv8R...")
+ threadx_r52_add_fvp_run(demo_threadx.elf run-demo-threadx-r52
+ "Running the standard ThreadX demo on FVP_BaseR_AEMv8R...")
+ threadx_r52_add_fvp_run(demo_clz.elf run-demo-clz-r52
+ "Running the CLZ lowest-set-bit regression on FVP_BaseR_AEMv8R...")
+ if(TX_R52_BUILD_FVP_MODULE_EXAMPLE)
+ threadx_r52_add_fvp_run(fvp_module.elf run-module-r52
+ "Running the AR2 module isolation example on FVP_BaseR_AEMv8R...")
+ threadx_r52_add_fvp_run(fvp_module_properties.elf run-module-properties-r52
+ "Running the AR2 module property contract regression on FVP_BaseR_AEMv8R...")
+ endif()
+
+ # Automated checks. The runner judges each image by its self-reported
+ # result and treats a missing result line as failure, so a hang cannot
+ # pass. Registered with CTest when Python is available, plus a plain
+ # target so the suite is runnable without CTest.
+ find_package(Python3 COMPONENTS Interpreter)
+ if(Python3_FOUND)
+ set(R52_RUNNER ${FVP_DIR}/test/run_fvp_test.py)
+ set(R52_TEST_IMAGES ${R52_IMAGES})
+
+ # AR2. Kept out of R52_IMAGES above rather than added to it, because
+ # that list is what the TX_R52_ENABLE_MPU and TX_R52_CONSOLE_PL011
+ # options are applied to and neither is optional for this image: the
+ # module boundary IS the MPU, and the result line has to reach the
+ # runner whichever console the rest of the suite was built with.
+ if(TX_R52_BUILD_FVP_MODULE_EXAMPLE)
+ list(APPEND R52_TEST_IMAGES fvp_module.elf fvp_module_properties.elf)
+ endif()
+
+ enable_testing()
+ foreach(image IN LISTS R52_TEST_IMAGES)
+ add_test(NAME r52-fvp-${image}
+ COMMAND ${Python3_EXECUTABLE} ${R52_RUNNER}
+ --elf $<TARGET_FILE:${image}>
+ --fvp ${FVP_BASER_AEMV8R})
+ endforeach()
+
+ add_custom_target(check-r52-fvp
+ COMMENT "Running every Cortex-R52 FVP image through the test runner..."
+ )
+ foreach(image IN LISTS R52_TEST_IMAGES)
+ add_custom_command(TARGET check-r52-fvp POST_BUILD
+ COMMAND ${Python3_EXECUTABLE} ${R52_RUNNER}
+ --elf $<TARGET_FILE:${image}>
+ --fvp ${FVP_BASER_AEMV8R}
+ )
+ add_dependencies(check-r52-fvp ${image})
+ endforeach()
+ else()
+ message(STATUS "Python3 not found; check-r52-fvp unavailable.")
+ endif()
+else()
+ message(STATUS "FVP_BaseR_AEMv8R not found; FVP run targets unavailable.")
+endif()
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/board.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/board.h
new file mode 100644
index 00000000..d5e50b80
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/board.h
@@ -0,0 +1,117 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* board.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Declarations for the objects that cross the C/assembly boundary in */
+/* this board support package. */
+/* */
+/* These exist because assembly callers do not provide prototypes: */
+/* entry.S calls bsp_main, _tx_initialize_low_level calls board_init, */
+/* and __tx_irq_processing_return calls board_irq_handler. Without a */
+/* header the definitions have external linkage and no visible */
+/* declaration, which MISRA C:2012 Rule 8.4 prohibits and which */
+/* -Wmissing-prototypes reports. Declaring them in one place also */
+/* means a signature change cannot silently disagree with the assembly.*/
+/* */
+/**************************************************************************/
+
+#ifndef BOARD_H
+#define BOARD_H
+
+/* Application entry, called from entry.S after EL1 setup; never returns. */
+
+void bsp_main(void);
+
+/* Interrupt controller and periodic tick bring-up, called from
+ _tx_initialize_low_level while interrupts are still masked. */
+
+void board_init(void);
+
+/* Interrupt dispatch, called from __tx_irq_processing_return in entry.S
+ after the interrupted context has been saved. */
+
+void board_irq_handler(void);
+
+/* The nesting path splits the handler in three, because the acknowledge has to
+ happen in IRQ mode before nesting starts and the end-of-interrupt after it
+ finishes. board_irq_service does the middle part on an INTID that has
+ already been acknowledged, and does not acknowledge or EOI itself. */
+
+void board_irq_service(unsigned long intid);
+
+/* Interrupt observability, maintained by board_irq_handler. */
+
+extern volatile unsigned long board_irq_count;
+extern volatile unsigned long board_timer_intid;
+extern volatile unsigned long board_spurious_count;
+extern volatile unsigned long board_unexpected_intid;
+
+/* Nesting observability, maintained by board_irq_handler. All of this is inert
+ unless the image was built with TX_ENABLE_IRQ_NESTING: without it the handler
+ runs in IRQ mode with interrupts masked and can never be re-entered. */
+
+extern volatile unsigned long board_nest_depth;
+extern volatile unsigned long board_nest_max;
+extern volatile unsigned long board_sgi_count;
+extern volatile unsigned long board_sgi_nested_count;
+
+/* Set by demo_nesting to make the timer handler provoke a nested SGI. Left at
+ zero every other image behaves exactly as before. */
+
+extern volatile unsigned long board_nest_provoke;
+
+#define BOARD_NEST_SGI_INTID 8U
+
+/* FIQ observability. Two Group 0 SGIs, because FIQ nesting means an FIQ taken
+ while an FIQ handler is running, so a single source cannot show it. The
+ higher-priority one is raised from inside the lower one's handler. */
+
+void board_fiq_handler(void);
+void board_fiq_service(unsigned long intid);
+
+extern volatile unsigned long board_fiq_count;
+extern volatile unsigned long board_fiq_high_count;
+extern volatile unsigned long board_fiq_nested_count;
+extern volatile unsigned long board_fiq_depth;
+extern volatile unsigned long board_fiq_max_depth;
+extern volatile unsigned long board_fiq_unexpected;
+extern volatile unsigned long board_fiq_provoke;
+
+#define BOARD_FIQ_LOW_INTID 10U
+#define BOARD_FIQ_HIGH_INTID 11U
+
+/* Counted by the EL2 hyp-trap handler in entry.S; the ZoneX seam. */
+
+extern volatile unsigned long _hvc_call_count;
+
+/* Recoverable data-abort support for the MPU enforcement self-test, handled
+ in entry.S. Setting mpu_expect_abort makes the next data abort resume at
+ the instruction following the faulting one instead of halting. */
+
+extern volatile unsigned long mpu_expect_abort;
+extern volatile unsigned long mpu_abort_count;
+
+#endif /* BOARD_H */
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/bsp_boot.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/bsp_boot.c
new file mode 100644
index 00000000..4c60de8c
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/bsp_boot.c
@@ -0,0 +1,170 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* bsp_boot.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* AR1 milestone M1 boot check. Verifies that the reset path really */
+/* configured EL2 and then dropped to EL1, and that the EL2 HVC seam */
+/* used by ZoneX (AR3) is reachable from EL1 and returns correctly. */
+/* */
+/* This deliberately asserts the resulting exception level rather than */
+/* merely printing a message: "it printed something" does not prove a */
+/* correct EL2-to-EL1 transition, and AR3 depends on that transition. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Directive 4.3 -- the two asm statements below read CPSR and issue */
+/* HVC; neither has a standard C equivalent. Both are encapsulated */
+/* in dedicated one-line functions. */
+/* */
+/**************************************************************************/
+
+#include "board.h"
+#include "console.h"
+
+/* AArch32 CPSR mode field encodings. */
+
+#define CPSR_MODE_MASK 0x1FUL
+#define CPSR_MODE_SVC 0x13UL /* Supervisor -- an EL1 mode */
+#define CPSR_MODE_HYP 0x1AUL /* Hyp -- EL2 */
+
+/* Incremented by the EL2 HVC handler in entry.S. */
+
+
+
+/**************************************************************************/
+/* read_cpsr -- current program status, including the mode field. */
+/**************************************************************************/
+
+static unsigned long read_cpsr(void)
+{
+ unsigned long value;
+
+ __asm__ volatile("mrs %0, cpsr" : "=r"(value));
+
+ return value;
+}
+
+
+/**************************************************************************/
+/* issue_hvc -- call into EL2 through the ZoneX seam and return. */
+/**************************************************************************/
+
+static void issue_hvc(void)
+{
+ __asm__ volatile("hvc #0" : : : "memory");
+}
+
+
+/**************************************************************************/
+/* report -- print one labelled check result. */
+/**************************************************************************/
+
+static void report(const char *label_ptr, unsigned int passed)
+{
+ console_puts(label_ptr);
+ console_puts(passed != 0U ? "PASS\n" : "FAIL\n");
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S. Does not return. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ unsigned long cpsr;
+ unsigned long mode;
+ unsigned long hvc_before;
+ unsigned long hvc_after;
+ unsigned int failures = 0U;
+
+ console_puts("\n=== ThreadX AR1/M1 boot check "
+ "(Cortex-R52, Armv8-R AArch32, Armv8-R AEM FVP) ===\n");
+
+ /* Check 1: we are executing at EL1, not still at EL2. */
+
+ cpsr = read_cpsr();
+ mode = cpsr & CPSR_MODE_MASK;
+
+ console_puts("[EL1] CPSR = ");
+ console_puthex(cpsr);
+ console_puts("\n[EL1] CPSR.M = ");
+ console_puthex(mode);
+ console_puts("\n");
+
+ if (mode == CPSR_MODE_SVC)
+ {
+ report("[EL1] running in Supervisor mode (EL1) ", 1U);
+ }
+ else
+ {
+ failures++;
+ if (mode == CPSR_MODE_HYP)
+ {
+ console_puts("[EL1] still in Hyp mode -- ERET to EL1 did not happen FAIL\n");
+ }
+ else
+ {
+ report("[EL1] unexpected CPSR mode ", 0U);
+ }
+ }
+
+ /* Check 2: the EL2 HVC seam is reachable from EL1 and returns. This is
+ what AR3 (ZoneX Phase-0) builds on, so it is verified from AR1. */
+
+ hvc_before = _hvc_call_count;
+ issue_hvc();
+ hvc_after = _hvc_call_count;
+
+ console_puts("[EL1] HVC count before/after = ");
+ console_puthex(hvc_before);
+ console_puts(" / ");
+ console_puthex(hvc_after);
+ console_puts("\n");
+
+ if (hvc_after == (hvc_before + 1UL))
+ {
+ report("[EL1] EL2 HVC seam reachable and returns to EL1 ", 1U);
+ }
+ else
+ {
+ failures++;
+ report("[EL1] EL2 HVC seam did not take effect ", 0U);
+ }
+
+ /* Summary. The exact strings are what the automated runner greps for. */
+
+ if (failures == 0U)
+ {
+ console_puts("\nM1 RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nM1 RESULT: FAILED\n");
+ }
+
+ console_exit(failures);
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.c
new file mode 100644
index 00000000..6632096a
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.c
@@ -0,0 +1,149 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* cache.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Cache maintenance for a loader that copies code. See cache.h for */
+/* why only these three functions exist. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Directive 4.3 -- cache maintenance and CTR are reachable only */
+/* through CP15; every access is encapsulated in a one-line accessor */
+/* below and nowhere else in this file. */
+/* Rule 11.6 (conversion between a pointer and an integer) -- a cache */
+/* maintenance operation takes a virtual address as a register value, */
+/* so the conversion is what the instruction requires. */
+/* */
+/**************************************************************************/
+
+#include "cache.h"
+
+/* The smallest data-cache line in the machine, from CTR.DminLine. Held as a
+ log2 of the number of 32-bit words, so the byte count is 4 << DminLine.
+ DminLine and not the L1 geometry: a maintenance-by-address walk has to step
+ by the smallest line any level implements, or it skips lines in that level. */
+
+#define CTR_DMINLINE_SHIFT 16U
+#define CTR_DMINLINE_MASK 0xFUL
+
+
+/**************************************************************************/
+/* CP15 accessors. The only assembly in this file (MISRA C:2012 Dir 4.3).*/
+/**************************************************************************/
+
+static unsigned long read_ctr(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c0, c0, 1" : "=r"(value));
+ return value;
+}
+
+static void clean_dcache_line(unsigned long address)
+{
+ __asm__ volatile("mcr p15, 0, %0, c7, c10, 1" :: "r"(address) : "memory");
+}
+
+static void invalidate_icache_all_op(void)
+{
+ unsigned long zero = 0UL;
+ __asm__ volatile("mcr p15, 0, %0, c7, c5, 0" :: "r"(zero) : "memory");
+}
+
+static void data_sync_barrier(void)
+{
+ __asm__ volatile("dsb sy" ::: "memory");
+}
+
+static void instruction_barrier(void)
+{
+ __asm__ volatile("isb" ::: "memory");
+}
+
+
+/**************************************************************************/
+/* cache_dcache_line_bytes */
+/**************************************************************************/
+
+unsigned long cache_dcache_line_bytes(void)
+{
+ return 4UL << ((read_ctr() >> CTR_DMINLINE_SHIFT) & CTR_DMINLINE_MASK);
+}
+
+
+/**************************************************************************/
+/* cache_clean_range */
+/* */
+/* Clean by virtual address over [start, start + length). */
+/* */
+/* The start is rounded DOWN to a line boundary and the walk continues */
+/* past the end until the last line containing a requested byte has been */
+/* cleaned. Rounding the start up instead would leave the first partial */
+/* line dirty, which is the whole failure this exists to prevent and is */
+/* invisible whenever the caller happens to be line aligned. */
+/**************************************************************************/
+
+void cache_clean_range(const void *start_address, unsigned long length)
+{
+ unsigned long line = cache_dcache_line_bytes();
+ unsigned long address;
+ unsigned long end;
+
+ if (length == 0UL)
+ {
+ return;
+ }
+
+ address = (unsigned long) start_address;
+ end = address + length;
+
+ address &= ~(line - 1UL);
+
+ while (address < end)
+ {
+ clean_dcache_line(address);
+ address += line;
+ }
+
+ /* The stores have to be complete before anything fetches from the range. */
+
+ data_sync_barrier();
+}
+
+
+/**************************************************************************/
+/* cache_invalidate_icache_all */
+/**************************************************************************/
+
+void cache_invalidate_icache_all(void)
+{
+ invalidate_icache_all_op();
+
+ /* DSB then ISB: the invalidate must complete, and the pipeline must be
+ flushed of anything fetched before it did. */
+
+ data_sync_barrier();
+ instruction_barrier();
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.h
new file mode 100644
index 00000000..0ed8dcfc
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/cache.h
@@ -0,0 +1,72 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* cache.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* The cache maintenance a loader owes the instruction side, and */
+/* nothing else. */
+/* */
+/* The S32Z280 board support carries a full cache driver -- enable, */
+/* disable, geometry, set/way sweeps -- because silicon bring-up needed */
+/* to ask the hardware what it had. None of that is needed here: the */
+/* caches are turned on once in mpu_init and never turned off, and the */
+/* model's geometry is not in question. What IS needed is the pair of */
+/* operations that make copied code executable, so that is all this is. */
+/* */
+/* Why it is needed at all: the module area is Normal write-back memory, */
+/* so a byte copy of module code leaves the bytes in dirty data-cache */
+/* lines while the instruction side -- which is not coherent with the */
+/* data cache on this core -- fetches whatever main memory still holds. */
+/* Cleaning by address range and then invalidating the instruction cache */
+/* is what closes that gap. */
+/* */
+/* By range rather than by set/way, deliberately. A set/way sweep needs */
+/* the cache geometry and touches every line in the machine; the loader */
+/* knows exactly which bytes it wrote, so the range form is both tighter */
+/* and shorter to get right. */
+/* */
+/**************************************************************************/
+
+#ifndef CACHE_H
+#define CACHE_H
+
+/* Clean (write back) the data cache over one address range, so that main
+ memory holds what the copy wrote. Rounds outwards to cache-line
+ boundaries; a length of zero does nothing. */
+
+void cache_clean_range(const void *start_address, unsigned long length);
+
+/* Invalidate the whole instruction cache, so no stale line from a previous
+ image at the same address can be served. Invalidate-all rather than by
+ range because it is one register write and this runs once per load. */
+
+void cache_invalidate_icache_all(void);
+
+/* Data cache line length in bytes, from CTR.DminLine. Published because the
+ range walk above depends on it and a caller may want to report it. */
+
+unsigned long cache_dcache_line_bytes(void);
+
+#endif /* CACHE_H */
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/console.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/console.c
new file mode 100644
index 00000000..2f75f605
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/console.c
@@ -0,0 +1,170 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* console.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Semihosting console for the Armv8-R AEM FVP. Armv8 AArch32 uses */
+/* HLT 0xF000 as the semihosting trap; the model implements it with no */
+/* peripheral configuration, which keeps early bring-up independent of */
+/* the platform memory map. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Directive 4.3 (assembly language shall be encapsulated and isolated)*/
+/* -- observed rather than violated: the single asm statement lives */
+/* in semihost_call() and nowhere else in the port. */
+/* Rule 1.1 / 1.2 (language extensions) */
+/* -- register-asm bindings and inline assembly are unavoidable to */
+/* invoke a semihosting trap; no standard C construct expresses it. */
+/* */
+/**************************************************************************/
+
+#include "console.h"
+
+#ifdef TX_R52_CONSOLE_PL011
+#include "uart_pl011.h"
+#endif
+
+/* Semihosting operation numbers (Arm semihosting specification). */
+
+#define SYS_WRITE0 0x04U
+#define SYS_EXIT 0x18U
+
+/* SYS_EXIT reason codes. On AArch32 the parameter is a reason, not an exit
+ status, so a failing run is reported by its printed result line. */
+
+#define ADP_STOPPED_APPLICATION_EXIT 0x20026U
+#define ADP_STOPPED_RUN_TIME_ERROR 0x20023U
+
+/* Number of hexadecimal digits in a 32-bit value. */
+
+#define HEX_DIGITS 8U
+
+
+/**************************************************************************/
+/* semihost_call */
+/* */
+/* Issues a single semihosting operation. This is the only assembly in */
+/* the console driver (MISRA C:2012 Dir 4.3). */
+/**************************************************************************/
+
+static int semihost_call(int operation, const void *argument_ptr)
+{
+ register int result __asm__("r0") = operation;
+ register const void *argument __asm__("r1") = argument_ptr;
+
+ __asm__ volatile("hlt 0xf000"
+ : "+r"(result)
+ : "r"(argument)
+ : "memory");
+
+ return result;
+}
+
+
+/**************************************************************************/
+/* console_puts */
+/**************************************************************************/
+
+void console_puts(const char *string_ptr)
+{
+ if (string_ptr == 0)
+ {
+ return;
+ }
+
+#ifdef TX_R52_CONSOLE_PL011
+
+ /* Initialise on first use rather than from board_init, so that images
+ with no ThreadX initialisation path (the M1 boot check) also get a
+ working UART console. */
+
+ static unsigned int initialised = 0U;
+
+ if (initialised == 0U)
+ {
+ pl011_init();
+ initialised = 1U;
+ }
+
+ pl011_puts(string_ptr);
+
+#else
+
+ (void) semihost_call((int) SYS_WRITE0, string_ptr);
+
+#endif
+}
+
+
+/**************************************************************************/
+/* console_puthex */
+/**************************************************************************/
+
+void console_puthex(unsigned long value)
+{
+ static const char digits[] = "0123456789abcdef";
+ char buffer[3U + HEX_DIGITS]; /* "0x" + digits + NUL */
+ unsigned int index;
+
+ buffer[0] = '0';
+ buffer[1] = 'x';
+
+ for (index = 0U; index < HEX_DIGITS; index++)
+ {
+ unsigned int shift = (HEX_DIGITS - 1U - index) * 4U;
+
+ buffer[2U + index] = digits[(value >> shift) & 0xFUL];
+ }
+
+ buffer[2U + HEX_DIGITS] = '\0';
+
+ console_puts(buffer);
+}
+
+
+/**************************************************************************/
+/* console_exit */
+/**************************************************************************/
+
+/* Note this always uses semihosting, even with the PL011 console selected:
+ SYS_EXIT is how the model is told to stop, which is a debug-channel
+ operation rather than console output. */
+
+void console_exit(unsigned int status)
+{
+ unsigned int reason = (status == 0U) ? ADP_STOPPED_APPLICATION_EXIT
+ : ADP_STOPPED_RUN_TIME_ERROR;
+
+ (void) semihost_call((int) SYS_EXIT, (const void *) reason);
+
+ /* The model terminates on the call above; loop defensively in case a
+ host or debug configuration ignores semihosting exit. */
+
+ for (;;)
+ {
+ /* Intentionally empty. */
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/console.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/console.h
new file mode 100644
index 00000000..a9bc5ba0
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/console.h
@@ -0,0 +1,51 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* console.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Minimal console for the Armv8-R AEM FVP. Backed by semihosting so */
+/* that early bring-up needs no peripheral base addresses; the PL011 */
+/* UART implementation arrives in AR1/M3 behind this same interface. */
+/* */
+/**************************************************************************/
+
+#ifndef CONSOLE_H
+#define CONSOLE_H
+
+/* Write a NUL-terminated string to the console. */
+
+void console_puts(const char *string_ptr);
+
+/* Write value as 0x-prefixed, zero-padded 32-bit hexadecimal. */
+
+void console_puthex(unsigned long value);
+
+/* Terminate the simulation (semihosting SYS_EXIT). Does not return.
+ Pass/fail is reported through the printed result line rather than a host
+ exit status, because the AArch32 SYS_EXIT reason code is not a status. */
+
+void console_exit(unsigned int status);
+
+#endif /* CONSOLE_H */
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_clz.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_clz.c
new file mode 100644
index 00000000..a156f4ec
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_clz.c
@@ -0,0 +1,359 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* DEMONSTRATION RELEASE */
+/* */
+/* demo_clz.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Regression test for this port's CLZ implementation of */
+/* TX_LOWEST_SET_BIT_CALCULATE, which replaces the portable loop in */
+/* tx_thread.h and which the scheduler's priority search runs on */
+/* every suspend and resume. */
+/* */
+/* Why this image exists. The macro was DEAD until 20 Aug 2026: */
+/* tx_port.h guarded it with __TARGET_ARCH_ARM > 4, an Arm Compiler 5 */
+/* predefine that GCC never defines, so the test read 0 > 4 and the */
+/* portable loop ran on a core that has had CLZ since Armv5. Turning */
+/* it on means hand-written inline assembly now decides which thread */
+/* runs next, so it gets a test that pins the answer for every input */
+/* class rather than a build that merely compiles. */
+/* */
+/* What is checked, against an obvious linear-scan reference rather */
+/* than against the implementation being tested: */
+/* */
+/* 1. Each of the 32 single-bit maps returns its own bit position. */
+/* 2. Each bit position with a deterministic spray of HIGHER bits */
+/* set still returns that position -- the macro must find the */
+/* LOWEST set bit, and a single-bit test cannot tell the */
+/* difference between "lowest" and "only". */
+/* 3. A table of hand-picked patterns, including the two the */
+/* respelled isolation step could plausibly break: 0x80000000, */
+/* the one input for which upstream's signed negation was */
+/* undefined, and 0xFFFFFFFF. */
+/* 4. Every group is run twice, once through a UINT result and once */
+/* through a ULONG one, because the kernel calls the macro with */
+/* both and the closing 31 - b is where a width mistake would */
+/* hide. */
+/* 5. The m == 0 divergence is PINNED, not fixed. CLZ(0) is 32, so */
+/* this implementation yields 31 - 32 while the portable loop */
+/* yields 0. Every one of the twelve call sites in common/src */
+/* reaches the macro only on a map already tested against zero, */
+/* so the difference is unreachable -- and a test that records */
+/* it is how it stays a decision instead of becoming a surprise. */
+/* */
+/* The checks run inside a ThreadX thread on purpose. By the time the */
+/* thread's entry is reached the scheduler has already executed the */
+/* macro on the way to dispatching it, so a macro broken badly enough */
+/* to misdirect the priority search shows up as a hang -- which the */
+/* runner scores as a failure -- before any check is evaluated. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "console.h"
+#include "board.h"
+
+/* The port's definition must be the one in scope. tx_thread.h supplies a
+ portable fallback under #ifndef, so an image that reached this file with the
+ macro undefined would be testing that fallback and reporting a pass for it --
+ the exact failure mode this whole image exists to rule out. */
+
+#ifndef TX_LOWEST_SET_BIT_CALCULATE
+#error "demo_clz: tx_port.h did not define TX_LOWEST_SET_BIT_CALCULATE, so this image would test the portable loop and call it a pass."
+#endif
+
+/* And it must be defined for the reason we think it is. These are the two
+ conditions tx_port.h uses; asserting them here means a future edit that
+ silently drops the CLZ path cannot leave this image passing. */
+
+#if !defined(__ARM_FEATURE_CLZ)
+#error "demo_clz: __ARM_FEATURE_CLZ is not defined, so the CLZ path in tx_port.h is not the definition under test."
+#endif
+
+#if defined(__thumb__)
+#error "demo_clz: built for Thumb, where tx_port.h deliberately keeps the portable loop. Build this image -marm."
+#endif
+
+
+#define DEMO_STACK_SIZE 2048
+
+static TX_THREAD check_thread;
+static UCHAR check_stack[DEMO_STACK_SIZE];
+
+/* Failure detail, kept as counters rather than printed per case: 32 passing
+ lines tell a reader nothing and would bury the one failing line. */
+
+static UINT failures;
+
+
+/**************************************************************************/
+/* reference_lowest_bit */
+/* */
+/* The ground truth, written to be read rather than to be fast. It is */
+/* deliberately NOT the portable macro from tx_thread.h: comparing an */
+/* implementation against a cleverer implementation of the same idea */
+/* tests that they share their mistakes. A linear scan has nowhere to */
+/* hide one. */
+/**************************************************************************/
+
+static ULONG reference_lowest_bit(ULONG map)
+{
+ ULONG index;
+ ULONG result = 0xFFFFFFFFUL; /* No bit set. */
+
+ for (index = 0UL; index < 32UL; index++)
+ {
+ if ((map & (1UL << index)) != 0UL)
+ {
+ result = index;
+ break;
+ }
+ }
+
+ return result;
+}
+
+
+/**************************************************************************/
+/* next_pattern */
+/* */
+/* A fixed linear congruential sequence. A regression test must produce */
+/* the same inputs on every run, so the spray of high bits in group 2 is */
+/* pseudo-random and seeded, never actually random. */
+/**************************************************************************/
+
+static ULONG next_pattern(ULONG state)
+{
+ return (state * 1103515245UL) + 12345UL;
+}
+
+
+/**************************************************************************/
+/* check_one */
+/* */
+/* Runs one map through the macro twice, once landing in a UINT and once */
+/* in a ULONG, and compares both against the reference. The macro */
+/* CONSUMES its first argument -- both implementations rewrite the map */
+/* in place -- so each call gets its own copy, which is also the trap a */
+/* caller reusing the variable afterwards would fall into. */
+/**************************************************************************/
+
+static void check_one(ULONG map, ULONG expected, const char *group_ptr)
+{
+ ULONG working;
+ UINT bit_as_uint;
+ ULONG bit_as_ulong;
+
+ working = map;
+ TX_LOWEST_SET_BIT_CALCULATE(working, bit_as_uint)
+
+ working = map;
+ TX_LOWEST_SET_BIT_CALCULATE(working, bit_as_ulong)
+
+ if (((ULONG) bit_as_uint != expected) || (bit_as_ulong != expected))
+ {
+ failures++;
+
+ console_puts("[FAIL] ");
+ console_puts(group_ptr);
+ console_puts(" map=");
+ console_puthex(map);
+ console_puts(" expected=");
+ console_puthex(expected);
+ console_puts(" got UINT=");
+ console_puthex((unsigned long) bit_as_uint);
+ console_puts(" ULONG=");
+ console_puthex(bit_as_ulong);
+ console_puts("\n");
+ }
+}
+
+
+/**************************************************************************/
+/* report */
+/**************************************************************************/
+
+static void report(const char *label_ptr, UINT failures_before)
+{
+ console_puts(label_ptr);
+ console_puts((failures == failures_before) ? "PASS\n" : "FAIL\n");
+}
+
+
+/**************************************************************************/
+/* thread_check_entry */
+/**************************************************************************/
+
+static void thread_check_entry(ULONG thread_input)
+{
+ ULONG index;
+ ULONG spray;
+ ULONG state;
+ ULONG map;
+ ULONG working;
+ ULONG bit;
+ UINT before;
+
+ /* Hand-picked patterns. 0x80000000 is the one input for which upstream's
+ (ULONG)(-((LONG) m)) isolation was undefined behaviour, and 0xFFFFFFFF is
+ the densest map the scheduler can present. */
+
+ static const ULONG patterns[] =
+ {
+ 0xFFFFFFFFUL, 0x80000000UL, 0xAAAAAAAAUL, 0x55555555UL,
+ 0xF0F0F000UL, 0x00000003UL, 0x7FFFFFFFUL, 0xC0000000UL,
+ 0x00010000UL, 0xFFFF0000UL
+ };
+
+ (void) thread_input;
+
+ console_puts("\n=== ThreadX Cortex-R52 CLZ lowest-set-bit regression ===\n\n");
+
+ /* Group 1 -- every single-bit map. */
+
+ before = failures;
+ for (index = 0UL; index < 32UL; index++)
+ {
+ check_one(1UL << index, index, "single bit");
+ }
+ report("[check] 32 single-bit maps ................ ", before);
+
+ /* Group 2 -- every bit position with higher bits sprayed above it. This is
+ the group that distinguishes "lowest set bit" from "only set bit". */
+
+ before = failures;
+ state = 0x12345678UL;
+ for (index = 0UL; index < 32UL; index++)
+ {
+ state = next_pattern(state);
+
+ /* Keep only bits strictly above index, then put index back. Shifting a
+ 32-bit value by 32 is undefined, so the top position is special-cased
+ to no spray at all -- there is nothing above bit 31 to set. */
+
+ if (index < 31UL)
+ {
+ spray = (state << (index + 1UL)) & 0xFFFFFFFFUL;
+ }
+ else
+ {
+ spray = 0UL;
+ }
+
+ check_one(spray | (1UL << index), index, "lowest of many");
+ }
+ report("[check] 32 sprayed maps, lowest bit wins .. ", before);
+
+ /* Group 3 -- the hand-picked table. */
+
+ before = failures;
+ for (index = 0UL; index < (ULONG) (sizeof(patterns) / sizeof(patterns[0])); index++)
+ {
+ check_one(patterns[index], reference_lowest_bit(patterns[index]), "pattern");
+ }
+ report("[check] 10 hand-picked patterns .......... ", before);
+
+ /* Group 4 -- the pinned divergence at zero. Not a fix: a record. CLZ(0)
+ is 32 and the closing subtraction is unsigned, so the answer is 31 - 32
+ taken modulo 2^32. The portable loop in tx_thread.h answers 0 for the
+ same input. Neither is reachable from the kernel, and this check exists
+ so that changing which one is true has to be deliberate. */
+
+ before = failures;
+ working = 0UL;
+ TX_LOWEST_SET_BIT_CALCULATE(working, bit)
+
+ if (bit != 0xFFFFFFFFUL)
+ {
+ failures++;
+ console_puts("[FAIL] map=0 answered ");
+ console_puthex(bit);
+ console_puts(", documented answer for the CLZ implementation is 0xFFFFFFFF\n");
+ }
+ report("[check] map = 0 answers as documented .... ", before);
+
+ /* Group 5 -- the macro consumes its first argument, in both
+ implementations. Stated as a check because it is a property callers rely
+ on by never relying on it, and a "fix" that stopped rewriting the map
+ would quietly change what every call site's variable holds afterwards. */
+
+ before = failures;
+ map = 0x00000280UL; /* Lowest set bit is 7. */
+ working = map;
+ TX_LOWEST_SET_BIT_CALCULATE(working, bit)
+
+ if ((bit != 7UL) || (working != (1UL << 7)))
+ {
+ failures++;
+ console_puts("[FAIL] expected bit 7 and map reduced to 0x80, got bit ");
+ console_puthex(bit);
+ console_puts(" map ");
+ console_puthex(working);
+ console_puts("\n");
+ }
+ report("[check] map is reduced to its lowest bit . ", before);
+
+ console_puts("\n[info] 75 maps checked, each through a UINT and a ULONG result.\n");
+
+ if (failures == 0U)
+ {
+ console_puts("\nCLZ RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nCLZ RESULT: FAILED\n");
+ }
+
+ console_exit(failures);
+}
+
+
+/**************************************************************************/
+/* tx_application_define */
+/**************************************************************************/
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ tx_thread_create(&check_thread, "clz check", thread_check_entry, 0UL,
+ check_stack, DEMO_STACK_SIZE,
+ 1U, 1U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S. Does not return. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ tx_kernel_enter();
+
+ /* Not reached. */
+
+ for (;;)
+ {
+ /* Nothing. */
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_fiq.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_fiq.c
new file mode 100644
index 00000000..b2899035
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_fiq.c
@@ -0,0 +1,215 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* demo_fiq.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Nested FIQ handling: _tx_thread_fiq_nesting_start and */
+/* _tx_thread_fiq_nesting_end, the last pair in this port that nothing */
+/* had ever called. */
+/* */
+/* FIQ needs more of the GIC than IRQ does. With a single security */
+/* state the controller delivers Group 0 as FIQ and Group 1 as IRQ, so */
+/* an interrupt only arrives as an FIQ if it has been moved into Group */
+/* 0, the distributor and CPU interface both have Group 0 enabled, and */
+/* it is acknowledged through the Group 0 registers. Group 1's */
+/* acknowledge returns the spurious INTID for a Group 0 interrupt and */
+/* leaves it pending, which presents as a storm rather than an error. */
+/* */
+/* FIQ nesting means an FIQ taken while an FIQ handler is running, so */
+/* one source cannot demonstrate it. Two Group 0 SGIs are used, the */
+/* second at a numerically lower priority so it can preempt the first, */
+/* and the first's handler raises it. */
+/* */
+/* WHAT EACH CHECK IS FOR */
+/* */
+/* F1 an FIQ arrives at all. Separate on purpose: it covers the whole */
+/* Group 0 chain -- IGRPEN0, the ICC_SGI0R encoding, IAR0 and */
+/* EOIR0, the EL1 FIQ vector, and F being unmasked. If F1 fails */
+/* the fault is in one of those and not in the nesting routines. */
+/* F2 an FIQ arrived while another FIQ handler was active, depth 2. */
+/* F3 the depth returned to zero, so the pairing unwound. */
+/* F4 the IRQ path is undisturbed: the tick still advances. */
+/* F5 threads still run, so FIQ context save and restore survived */
+/* being re-entered. */
+/* F6 no unexpected Group 0 INTID arrived. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "board.h"
+#include "console.h"
+#include "gicv3.h"
+
+#define DEMO_STACK_SIZE 2048
+
+static TX_THREAD thread_check;
+static TX_THREAD thread_spin;
+
+static ULONG thread_check_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+static ULONG thread_spin_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+static volatile ULONG spin_runs;
+
+
+static UINT report(const char *label_ptr, UINT passed)
+{
+ console_puts(label_ptr);
+ console_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+ return (passed != 0U) ? 0U : 1U;
+}
+
+
+static void report_hex(const char *label_ptr, unsigned long value)
+{
+ console_puts(label_ptr);
+ console_puthex(value);
+ console_puts("\n");
+}
+
+
+static void thread_spin_entry(ULONG thread_input)
+{
+ (void) thread_input;
+
+ for (;;)
+ {
+ spin_runs++;
+ }
+}
+
+
+static void thread_check_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ ULONG fiq_before;
+ ULONG ticks_start;
+ ULONG ticks_end;
+ ULONG spin_before;
+
+ (void) thread_input;
+
+ /* F1: the whole Group 0 chain, before nesting is involved. */
+
+ console_puts("[check] raising a Group 0 SGI from thread context\n");
+
+ fiq_before = board_fiq_count;
+ gicv3_send_sgi_group0(BOARD_FIQ_LOW_INTID);
+ tx_thread_sleep(2);
+
+ report_hex("[check] FIQ count = ", board_fiq_count);
+
+ failures += report("[check] F1 FIQ delivered and dispatched ",
+ (board_fiq_count > fiq_before) ? 1U : 0U);
+
+ if (board_fiq_count == fiq_before)
+ {
+ /* Everything below needs an FIQ to arrive, so repeating the failure
+ five more times would only bury the cause. */
+
+ console_puts("[check] no FIQ arrived; the nesting checks would only\n"
+ " restate that, so they are skipped.\n");
+ console_puts("\nFIQ RESULT: FAILED\n");
+ console_exit(1U);
+ }
+
+ /* F2 onwards: let the low-priority FIQ handler raise the high one. */
+
+ console_puts("[check] enabling nested provocation in the FIQ handler\n");
+
+ board_fiq_provoke = 1UL;
+ ticks_start = tx_time_get();
+ spin_before = spin_runs;
+
+ {
+ ULONG i;
+
+ for (i = 0UL; i < 20UL; i++)
+ {
+ gicv3_send_sgi_group0(BOARD_FIQ_LOW_INTID);
+ tx_thread_sleep(2);
+ }
+ }
+
+ ticks_end = tx_time_get();
+ board_fiq_provoke = 0UL;
+
+ report_hex("[check] low-priority FIQ count = ", board_fiq_count);
+ report_hex("[check] high-priority FIQ count = ", board_fiq_high_count);
+ report_hex("[check] nested FIQ count = ", board_fiq_nested_count);
+ report_hex("[check] max FIQ depth = ", board_fiq_max_depth);
+ report_hex("[check] FIQ depth now = ", board_fiq_depth);
+
+ failures += report("[check] F2 FIQ nested inside an FIQ handler ",
+ ((board_fiq_nested_count > 0UL) &&
+ (board_fiq_max_depth >= 2UL)) ? 1U : 0U);
+
+ failures += report("[check] F3 FIQ nesting unwound to depth zero ",
+ (board_fiq_depth == 0UL) ? 1U : 0U);
+
+ failures += report("[check] F4 IRQ tick undisturbed by FIQ work ",
+ ((ticks_end - ticks_start) >= 30UL) ? 1U : 0U);
+
+ failures += report("[check] F5 lower-priority thread still scheduled ",
+ (spin_runs > spin_before) ? 1U : 0U);
+
+ report_hex("[check] unexpected Group 0 INTID = ", board_fiq_unexpected);
+
+ failures += report("[check] F6 no unexpected Group 0 INTID ",
+ (board_fiq_unexpected == 0UL) ? 1U : 0U);
+
+ if (failures == 0U)
+ {
+ console_puts("\nFIQ RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nFIQ RESULT: FAILED\n");
+ }
+
+ console_exit(failures);
+}
+
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ (void) tx_thread_create(&thread_check, "fiq check", thread_check_entry,
+ 0UL, thread_check_stack, sizeof(thread_check_stack),
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&thread_spin, "spinner", thread_spin_entry,
+ 0UL, thread_spin_stack, sizeof(thread_spin_stack),
+ 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+void bsp_main(void)
+{
+ console_puts("\n=== ThreadX nested FIQ handling "
+ "(Cortex-R52, Armv8-R AArch32, Armv8-R AEM FVP) ===\n");
+
+ tx_kernel_enter();
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m2.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m2.c
new file mode 100644
index 00000000..e6b7da2c
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m2.c
@@ -0,0 +1,224 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* DEMONSTRATION RELEASE */
+/* */
+/* demo_m2.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* AR1 milestone M2: two threads switching cooperatively, with no */
+/* interrupts and no timer tick. This is the first real exercise of */
+/* the ported assembly -- _tx_thread_stack_build builds each thread's */
+/* initial frame, _tx_thread_schedule restores it, and */
+/* _tx_thread_system_return saves a solicited context on every */
+/* tx_thread_relinquish call. */
+/* */
+/* The threads record their execution order in a trace buffer, and the */
+/* expected alternating sequence is asserted at the end. Counting */
+/* iterations alone would not prove a context switch happened: if */
+/* switching were broken, one thread could run to completion by itself.*/
+/* */
+/* Only tx_thread_relinquish is used. tx_thread_sleep would hang */
+/* without a tick, which AR1/M3 adds. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "board.h"
+#include "console.h"
+
+#define DEMO_STACK_SIZE 2048
+#define DEMO_ITERATIONS 5U
+#define TRACE_LENGTH (2U * DEMO_ITERATIONS)
+
+static TX_THREAD thread_0;
+static TX_THREAD thread_1;
+
+static ULONG thread_0_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+static ULONG thread_1_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+static volatile ULONG thread_0_counter;
+static volatile ULONG thread_1_counter;
+
+/* Execution order, one character per slice. */
+
+static char trace[TRACE_LENGTH + 1U];
+static volatile UINT trace_index;
+
+
+/**************************************************************************/
+/* trace_record -- append one thread's mark to the execution trace. */
+/**************************************************************************/
+
+static void trace_record(char mark)
+{
+ if (trace_index < TRACE_LENGTH)
+ {
+ trace[trace_index] = mark;
+ trace_index++;
+ }
+}
+
+
+/**************************************************************************/
+/* report -- print one labelled check result. */
+/**************************************************************************/
+
+static void report(const char *label_ptr, UINT passed)
+{
+ console_puts(label_ptr);
+ console_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+}
+
+
+/**************************************************************************/
+/* thread_1_entry -- yields back to thread 0 on every slice. */
+/**************************************************************************/
+
+static void thread_1_entry(ULONG thread_input)
+{
+ (void) thread_input;
+
+ while (thread_1_counter < DEMO_ITERATIONS)
+ {
+ thread_1_counter++;
+ trace_record('1');
+
+ console_puts("[thread 1] slice ");
+ console_puthex(thread_1_counter);
+ console_puts("\n");
+
+ tx_thread_relinquish();
+ }
+}
+
+
+/**************************************************************************/
+/* thread_0_entry -- yields to thread 1, then checks the outcome. */
+/**************************************************************************/
+
+static void thread_0_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ UINT index;
+
+ (void) thread_input;
+
+ while (thread_0_counter < DEMO_ITERATIONS)
+ {
+ thread_0_counter++;
+ trace_record('0');
+
+ console_puts("[thread 0] slice ");
+ console_puthex(thread_0_counter);
+ console_puts("\n");
+
+ tx_thread_relinquish();
+ }
+
+ /* Check 1: both threads ran the expected number of slices. A broken
+ context switch would leave thread 1 at zero. */
+
+ console_puts("\n[check] slices thread 0 / thread 1 = ");
+ console_puthex(thread_0_counter);
+ console_puts(" / ");
+ console_puthex(thread_1_counter);
+ console_puts("\n");
+
+ if ((thread_0_counter == (ULONG) DEMO_ITERATIONS) &&
+ (thread_1_counter == (ULONG) DEMO_ITERATIONS))
+ {
+ report("[check] both threads ran every slice ", 1U);
+ }
+ else
+ {
+ failures++;
+ report("[check] both threads ran every slice ", 0U);
+ }
+
+ /* Check 2: execution strictly alternated, proving each relinquish
+ really switched context rather than returning to the same thread. */
+
+ trace[TRACE_LENGTH] = '\0';
+ console_puts("[check] execution order = ");
+ console_puts(trace);
+ console_puts(" (expected 0101010101)\n");
+
+ for (index = 0U; index < TRACE_LENGTH; index++)
+ {
+ char expected = ((index % 2U) == 0U) ? '0' : '1';
+
+ if (trace[index] != expected)
+ {
+ failures++;
+ break;
+ }
+ }
+
+ report("[check] threads alternated on every relinquish ",
+ (UINT) ((failures == 0U) ? 1U : 0U));
+
+ if (failures == 0U)
+ {
+ console_puts("\nM2 RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nM2 RESULT: FAILED\n");
+ }
+
+ console_exit(failures);
+}
+
+
+/**************************************************************************/
+/* tx_application_define -- create the demo threads. */
+/* */
+/* Static stacks are used rather than a byte pool so that M2 exercises */
+/* only the context-switch path, with no allocator in the way. */
+/**************************************************************************/
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ (void) tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0UL,
+ thread_0_stack, sizeof(thread_0_stack),
+ 16U, 16U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&thread_1, "thread 1", thread_1_entry, 0UL,
+ thread_1_stack, sizeof(thread_1_stack),
+ 16U, 16U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S. Does not return. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ console_puts("\n=== ThreadX AR1/M2 cooperative switch "
+ "(Cortex-R52, Armv8-R AArch32, Armv8-R AEM FVP) ===\n");
+
+ tx_kernel_enter();
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m3.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m3.c
new file mode 100644
index 00000000..96beac48
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m3.c
@@ -0,0 +1,249 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* DEMONSTRATION RELEASE */
+/* */
+/* demo_m3.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* AR1 milestone M3: periodic tick and preemptive scheduling. This */
+/* exercises the parts of the port that M2 could not reach -- */
+/* _tx_thread_context_save, _tx_timer_interrupt and */
+/* _tx_thread_context_restore -- by driving them from a real generic */
+/* timer interrupt routed through GICv3. */
+/* */
+/* The checks build on each other, so a failure localises the cause: */
+/* 1. CNTFRQ is non-zero -> entry.S programmed it at EL2 */
+/* 2. counter is enabled -> the control frame was started */
+/* 3. CNTPCT advances -> the counter really runs */
+/* 4. interrupts arrive -> GICv3 + PPI + vector wiring work */
+/* 5. tx_time_get advances -> _tx_timer_interrupt drives the tick*/
+/* 6. tx_thread_sleep returns -> timer-driven thread resumption */
+/* 7. a lower-priority thread ran while we slept -> preemption and */
+/* context save/restore across an interrupt */
+/* */
+/* The timer PPI INTID is reported, not assumed: the whole PPI range is*/
+/* enabled and whichever INTID the model drives is recorded. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "board.h"
+#include "console.h"
+#include "timer.h"
+
+#define DEMO_STACK_SIZE 2048
+#define SLEEP_TICKS 20UL
+#define SLEEP_SLACK_TICKS 10UL
+
+static TX_THREAD thread_main;
+static TX_THREAD thread_busy;
+
+static ULONG thread_main_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+static ULONG thread_busy_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+static volatile ULONG busy_counter;
+
+/* Recorded by the interrupt dispatcher. */
+
+
+
+/**************************************************************************/
+/* report -- print one labelled check result. */
+/**************************************************************************/
+
+static UINT report(const char *label_ptr, UINT passed)
+{
+ console_puts(label_ptr);
+ console_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+
+ return (passed != 0U) ? 0U : 1U;
+}
+
+
+/**************************************************************************/
+/* thread_busy_entry -- lowest priority; only runs when nothing else can.*/
+/**************************************************************************/
+
+static void thread_busy_entry(ULONG thread_input)
+{
+ (void) thread_input;
+
+ for (;;)
+ {
+ busy_counter++;
+ }
+}
+
+
+/**************************************************************************/
+/* thread_main_entry -- runs the checks and terminates the simulation. */
+/**************************************************************************/
+
+static void thread_main_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ unsigned long frequency;
+ unsigned long long counter_first;
+ unsigned long long counter_second;
+ ULONG time_before;
+ ULONG time_after;
+ ULONG elapsed;
+ ULONG busy_before;
+ ULONG busy_after;
+ unsigned long irq_before;
+
+ (void) thread_input;
+
+ /* Check 1: CNTFRQ was programmed at EL2 (the model resets it to zero). */
+
+ frequency = timer_frequency();
+ console_puts("[check] CNTFRQ = ");
+ console_puthex(frequency);
+ console_puts(" Hz\n");
+ failures += report("[check] CNTFRQ programmed at EL2 (non-zero) ",
+ (frequency != 0UL) ? 1U : 0U);
+
+ /* Check 2: the system counter was started (stopped at reset). */
+
+ failures += report("[check] system counter enabled by BSP ",
+ timer_counter_enabled());
+
+ /* Check 3: the counter actually advances. */
+
+ counter_first = timer_counter();
+ {
+ volatile unsigned int spin;
+ for (spin = 0U; spin < 20000U; spin++)
+ {
+ /* Burn a little time. */
+ }
+ }
+ counter_second = timer_counter();
+ failures += report("[check] CNTPCT advancing ",
+ (counter_second > counter_first) ? 1U : 0U);
+
+ /* Check 4: timer interrupts are being delivered through GICv3. */
+
+ irq_before = board_irq_count;
+ time_before = tx_time_get();
+ busy_before = busy_counter;
+
+ /* Check 6: a timer-driven sleep returns. While suspended, the only
+ runnable thread is the lower-priority busy thread. */
+
+ tx_thread_sleep(SLEEP_TICKS);
+
+ time_after = tx_time_get();
+ busy_after = busy_counter;
+
+ console_puts("[check] timer PPI INTID observed = ");
+ console_puthex(board_timer_intid);
+ console_puts("\n[check] timer interrupts taken = ");
+ console_puthex(board_irq_count);
+ console_puts("\n");
+
+ failures += report("[check] timer interrupts delivered via GICv3 ",
+ (board_irq_count > irq_before) ? 1U : 0U);
+
+ /* Check 5: the ThreadX clock is driven by those interrupts. */
+
+ elapsed = time_after - time_before;
+ console_puts("[check] tx_time_get elapsed = ");
+ console_puthex(elapsed);
+ console_puts(" ticks (slept ");
+ console_puthex(SLEEP_TICKS);
+ console_puts(")\n");
+
+ failures += report("[check] tx_time_get advancing ",
+ (elapsed >= SLEEP_TICKS) ? 1U : 0U);
+
+ failures += report("[check] sleep lasted about the requested time ",
+ (elapsed <= (SLEEP_TICKS + SLEEP_SLACK_TICKS)) ? 1U : 0U);
+
+ /* Check 7: the lower-priority thread ran while we were suspended, then
+ was preempted when the tick made this thread ready again. That
+ requires a correct context save and restore across the interrupt. */
+
+ console_puts("[check] busy-thread counter delta = ");
+ console_puthex(busy_after - busy_before);
+ console_puts("\n");
+
+ failures += report("[check] lower-priority thread ran, then preempted ",
+ (busy_after > busy_before) ? 1U : 0U);
+
+ /* Nothing unexpected should have arrived on the interrupt path. */
+
+ if (board_unexpected_intid != 0UL)
+ {
+ console_puts("[check] unexpected INTID = ");
+ console_puthex(board_unexpected_intid);
+ console_puts("\n");
+ }
+ failures += report("[check] no unexpected interrupt IDs ",
+ (board_unexpected_intid == 0UL) ? 1U : 0U);
+
+ if (failures == 0U)
+ {
+ console_puts("\nM3 RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nM3 RESULT: FAILED\n");
+ }
+
+ console_exit(failures);
+}
+
+
+/**************************************************************************/
+/* tx_application_define */
+/**************************************************************************/
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ /* Priority 10 runs the checks; priority 20 only gets the processor when
+ the checking thread is suspended. */
+
+ (void) tx_thread_create(&thread_main, "check thread", thread_main_entry,
+ 0UL, thread_main_stack, sizeof(thread_main_stack),
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&thread_busy, "busy thread", thread_busy_entry,
+ 0UL, thread_busy_stack, sizeof(thread_busy_stack),
+ 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S. Does not return. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ console_puts("\n=== ThreadX AR1/M3 tick and preemption "
+ "(Cortex-R52, Armv8-R AArch32, Armv8-R AEM FVP) ===\n");
+
+ tx_kernel_enter();
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m5.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m5.c
new file mode 100644
index 00000000..783bed06
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_m5.c
@@ -0,0 +1,285 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* DEMONSTRATION RELEASE */
+/* */
+/* demo_m5.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* AR1 milestone M5: lazy floating-point context save and restore. */
+/* */
+/* Two paths must be covered, and they use different register sets: */
+/* */
+/* solicited tx_thread_system_return saves D8-D15 and FPSCR, because*/
+/* the callee-saved half is all a voluntary switch can */
+/* lose. Exercised by the checking thread, which keeps */
+/* eight doubles live across tx_thread_sleep. */
+/* interrupt tx_thread_context_restore restores D0-D15 and FPSCR, */
+/* because an asynchronous interrupt can land anywhere. */
+/* Exercised by a lower-priority thread doing continuous */
+/* floating-point work while the tick fires into it. */
+/* */
+/* Every constant is an exact binary fraction, so the comparisons are */
+/* exact and a single corrupted register shows up as a mismatch rather */
+/* than as rounding noise. The two threads use disjoint value ranges, */
+/* so leakage from one context into the other is also caught. */
+/* */
+/* Both threads call tx_thread_vfp_enable(): with lazy save/restore, a */
+/* thread that has not asked for floating-point support does not get */
+/* it, so forgetting the call would show up as corruption rather than */
+/* as a build error. */
+/* */
+/* MISRA C:2012 / warning deviations (justified) */
+/* */
+/* Rule 13.3 style equality on floating point (-Wfloat-equal) */
+/* -- exact comparison is the point of this test, not an oversight. */
+/* Every constant is an exact binary fraction and every operation is */
+/* an addition of 0.5, so the arithmetic is exact and the expected */
+/* results are representable. A tolerance-based comparison would */
+/* mask precisely the corruption being looked for: a restored */
+/* register that is close but wrong would pass. Confined to this */
+/* test file. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "board.h"
+#include "console.h"
+
+#define DEMO_STACK_SIZE 2048
+#define FILEX_PTR_SENTINEL ((void *) 0xF11EF11EUL)
+#define FP_ITERATIONS 50UL
+#define FP_STEP 0.5 /* exactly representable */
+
+static TX_THREAD thread_check;
+static TX_THREAD thread_fp_busy;
+
+static ULONG thread_check_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+static ULONG thread_fp_busy_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+/* Set by the busy thread if its floating-point state was ever seen wrong.*/
+
+static volatile ULONG busy_iterations;
+static volatile ULONG busy_corruptions;
+
+
+/**************************************************************************/
+/* report */
+/**************************************************************************/
+
+static UINT report(const char *label_ptr, UINT passed)
+{
+ console_puts(label_ptr);
+ console_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+
+ return (passed != 0U) ? 0U : 1U;
+}
+
+
+/**************************************************************************/
+/* thread_fp_busy_entry */
+/* */
+/* Lowest priority, never sleeps: the periodic tick therefore interrupts */
+/* it in the middle of floating-point work, which is what exercises the */
+/* interrupt half of the VFP context path (D0-D15 + FPSCR). Its values */
+/* are checked every iteration against exactly representable constants. */
+/**************************************************************************/
+
+static void thread_fp_busy_entry(ULONG thread_input)
+{
+ (void) thread_input;
+
+ tx_thread_vfp_enable();
+
+ for (;;)
+ {
+ /* Disjoint from the checking thread's range, so cross-contamination
+ between contexts is detectable. */
+
+ double v0 = 1024.5;
+ double v1 = 2048.25;
+ double v2 = 4096.125;
+ double v3 = 8192.0625;
+
+ v0 = (v0 * 2.0) - 1024.5; /* 1024.5 */
+ v1 = (v1 * 2.0) - 2048.25; /* 2048.25 */
+ v2 = (v2 * 2.0) - 4096.125; /* 4096.125 */
+ v3 = (v3 * 2.0) - 8192.0625; /* 8192.0625*/
+
+ if ((v0 != 1024.5) || (v1 != 2048.25) ||
+ (v2 != 4096.125) || (v3 != 8192.0625))
+ {
+ busy_corruptions++;
+ }
+
+ busy_iterations++;
+ }
+}
+
+
+/**************************************************************************/
+/* thread_check_entry */
+/**************************************************************************/
+
+static void thread_check_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ ULONG iteration;
+ ULONG expected_steps;
+ TX_THREAD *self;
+
+ /* Eight live doubles held across a blocking call. Eight is chosen so
+ the compiler must use the callee-saved bank D8-D15 (or spill), which
+ is exactly what a solicited context switch has to preserve. */
+
+ double a = 1.5;
+ double b = 2.25;
+ double c = 3.125;
+ double d = 4.0625;
+ double e = 5.03125;
+ double f = 6.015625;
+ double g = 7.0078125;
+ double h = 8.00390625;
+
+ (void) thread_input;
+
+ /* Guard against the VFP enable flag aliasing another thread member. The
+ flag is reached from assembly by hard-coded offset, and on the
+ Cortex-R4/R5 ports that offset lands on tx_thread_filex_ptr because
+ their TX_THREAD_EXTENSION_2 is empty. Planting a sentinel here turns
+ that class of mistake into a visible failure rather than corruption
+ that only surfaces once FileX is added. */
+
+ self = tx_thread_identify();
+ self->tx_thread_filex_ptr = FILEX_PTR_SENTINEL;
+
+ tx_thread_vfp_enable();
+
+ console_puts("[check] running floating-point work across ");
+ console_puthex(FP_ITERATIONS);
+ console_puts(" solicited switches...\n");
+
+ for (iteration = 0UL; iteration < FP_ITERATIONS; iteration++)
+ {
+ /* Suspend: the switch must preserve every one of these values, and
+ the lower-priority floating-point thread runs meanwhile. */
+
+ tx_thread_sleep(1);
+
+ a += FP_STEP;
+ b += FP_STEP;
+ c += FP_STEP;
+ d += FP_STEP;
+ e += FP_STEP;
+ f += FP_STEP;
+ g += FP_STEP;
+ h += FP_STEP;
+ }
+
+ expected_steps = FP_ITERATIONS;
+
+ /* Exact comparisons: every value above is an exact binary fraction and
+ FP_STEP is 0.5, so no rounding is involved. */
+
+ failures += report("[check] D-register value a preserved exactly ",
+ (a == (1.5 + (0.5 * (double) expected_steps))) ? 1U : 0U);
+ failures += report("[check] D-register value b preserved exactly ",
+ (b == (2.25 + (0.5 * (double) expected_steps))) ? 1U : 0U);
+ failures += report("[check] D-register value c preserved exactly ",
+ (c == (3.125 + (0.5 * (double) expected_steps))) ? 1U : 0U);
+ failures += report("[check] D-register value d preserved exactly ",
+ (d == (4.0625 + (0.5 * (double) expected_steps))) ? 1U : 0U);
+ failures += report("[check] D-register value e preserved exactly ",
+ (e == (5.03125 + (0.5 * (double) expected_steps))) ? 1U : 0U);
+ failures += report("[check] D-register value f preserved exactly ",
+ (f == (6.015625 + (0.5 * (double) expected_steps))) ? 1U : 0U);
+ failures += report("[check] D-register value g preserved exactly ",
+ (g == (7.0078125 + (0.5 * (double) expected_steps))) ? 1U : 0U);
+ failures += report("[check] D-register value h preserved exactly ",
+ (h == (8.00390625 + (0.5 * (double) expected_steps))) ? 1U : 0U);
+
+ /* The busy thread must have run, and never seen its own state damaged
+ by an interrupt landing inside its floating-point sequence. */
+
+ console_puts("[check] busy-thread iterations = ");
+ console_puthex(busy_iterations);
+ console_puts("\n[check] busy-thread corruptions = ");
+ console_puthex(busy_corruptions);
+ console_puts("\n");
+
+ failures += report("[check] interrupted FP thread made progress ",
+ (busy_iterations > 0UL) ? 1U : 0U);
+ failures += report("[check] no FP corruption across interrupts ",
+ (busy_corruptions == 0UL) ? 1U : 0U);
+
+ /* The VFP flag must not have aliased tx_thread_filex_ptr. */
+
+ console_puts("[check] tx_thread_filex_ptr = ");
+ console_puthex((unsigned long) self->tx_thread_filex_ptr);
+ console_puts(" (expected 0xf11ef11e)\n");
+
+ failures += report("[check] VFP flag did not alias filex_ptr ",
+ (self->tx_thread_filex_ptr == FILEX_PTR_SENTINEL) ? 1U : 0U);
+
+ if (failures == 0U)
+ {
+ console_puts("\nM5 RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nM5 RESULT: FAILED\n");
+ }
+
+ console_exit(failures);
+}
+
+
+/**************************************************************************/
+/* tx_application_define */
+/**************************************************************************/
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ (void) tx_thread_create(&thread_check, "fp check", thread_check_entry, 0UL,
+ thread_check_stack, sizeof(thread_check_stack),
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&thread_fp_busy, "fp busy", thread_fp_busy_entry,
+ 0UL, thread_fp_busy_stack,
+ sizeof(thread_fp_busy_stack),
+ 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S. Does not return. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ console_puts("\n=== ThreadX AR1/M5 lazy VFP context switch "
+ "(Cortex-R52, Armv8-R AArch32, Armv8-R AEM FVP) ===\n");
+
+ tx_kernel_enter();
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_mpu.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_mpu.c
new file mode 100644
index 00000000..c96afc08
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_mpu.c
@@ -0,0 +1,284 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* DEMONSTRATION RELEASE */
+/* */
+/* demo_mpu.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* AR1 milestone M5: PMSAv8-R protection and caches. */
+/* */
+/* The important check here is enforcement, not configuration. Reading*/
+/* SCTLR back only proves a bit was set; it says nothing about whether */
+/* the region table actually describes memory correctly. So the test */
+/* provokes a real permission fault by writing to the read-only code */
+/* region and requires the abort to arrive, then confirms a legal write*/
+/* to the data region still succeeds. A region set that permitted */
+/* everything would pass the first kind of check and fail this one. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "board.h"
+#include "console.h"
+#include "mpu.h"
+
+#define DEMO_STACK_SIZE 2048
+
+static TX_THREAD thread_check;
+static ULONG thread_check_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+/* Provided by entry.S and manipulated by its data-abort handler. */
+
+
+/* A known-writable location, used to show legal access still works. */
+
+static volatile unsigned long writable_probe;
+
+/* Execute-never probe. Lives in the data region, holds one A32 "bx lr", and
+ is only ever reached if XN is NOT enforced -- in which case this check has
+ already failed and the encoded instruction is simply the most harmless
+ thing to land on. Not static: nothing in this file reads it, and an
+ optimising build would otherwise discard it. */
+
+volatile unsigned long executable_probe = 0xE12FFF1EUL;
+
+extern void mpu_try_execute(void (*target)(void));
+extern volatile unsigned long mpu_expect_pabt;
+extern volatile unsigned long mpu_pabt_count;
+
+
+/**************************************************************************/
+/* report */
+/**************************************************************************/
+
+static UINT report(const char *label_ptr, UINT passed)
+{
+ console_puts(label_ptr);
+ console_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+
+ return (passed != 0U) ? 0U : 1U;
+}
+
+
+/**************************************************************************/
+/* thread_check_entry */
+/**************************************************************************/
+
+static void thread_check_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ unsigned int available;
+ unsigned int used;
+ unsigned int index;
+ const MPU_REGION *table;
+ unsigned long aborts_before;
+
+ (void) thread_input;
+
+ /* What the implementation provides. */
+
+ available = mpu_region_count();
+ console_puts("[check] MPUIR regions available = ");
+ console_puthex(available);
+ console_puts("\n");
+ failures += report("[check] implementation reports MPU regions ",
+ (available > 0U) ? 1U : 0U);
+
+ /* What this image programmed. */
+
+ table = mpu_region_table(&used);
+ console_puts("[check] regions programmed = ");
+ console_puthex(used);
+ console_puts("\n");
+
+ for (index = 0U; index < used; index++)
+ {
+ console_puts(" ");
+ console_puts(table[index].mpu_region_name);
+ console_puts(" base ");
+ console_puthex(table[index].mpu_region_base);
+ console_puts(" limit ");
+ console_puthex(table[index].mpu_region_limit);
+ console_puts("\n");
+ }
+
+ failures += report("[check] region table programmed ",
+ (used > 0U) ? 1U : 0U);
+
+ /* Read the hardware back: if the register encodings were wrong the
+ writes would be silently discarded and a permissive background map
+ would make everything appear to work. */
+
+ for (index = 0U; index < used; index++)
+ {
+ unsigned long prbar = 0UL;
+ unsigned long prlar = 0UL;
+
+ mpu_read_region(index, &prbar, &prlar);
+ console_puts(" region ");
+ console_puthex(index);
+ console_puts(" PRBAR=");
+ console_puthex(prbar);
+ console_puts(" PRLAR=");
+ console_puthex(prlar);
+ console_puts("\n");
+ if ((prbar == 0UL) && (prlar == 0UL))
+ {
+ failures++;
+ }
+ }
+ failures += report("[check] hardware holds the programmed regions ",
+ (failures == 0U) ? 1U : 0U);
+
+ failures += report("[check] MPU enabled (SCTLR.M) ",
+ mpu_is_enabled());
+ failures += report("[check] caches enabled (SCTLR.C and SCTLR.I) ",
+ mpu_caches_enabled());
+
+ /* Enforcement, part 1: a legal write to the data region must work. */
+
+ writable_probe = 0xA5A5A5A5UL;
+ failures += report("[check] legal write to data region succeeds ",
+ (writable_probe == 0xA5A5A5A5UL) ? 1U : 0U);
+
+ /* Enforcement, part 1b: an address covered by NO region must fault.
+ This distinguishes "the MPU is not enforcing at all" from "the MPU
+ enforces coverage but not the access permission". 0x40000000 lies
+ between the end of the data region and the peripheral region. */
+
+ /* Enforcement, part 2: an address covered by no region must fault. This
+ distinguishes "not enforcing at all" from "enforcing coverage only". */
+
+ aborts_before = mpu_abort_count;
+ mpu_expect_abort = 1UL;
+ {
+ volatile unsigned long *gap_ptr = (volatile unsigned long *) 0x40000000UL;
+
+ *gap_ptr = 0x11223344UL;
+ }
+ failures += report("[check] write to unmapped address faulted ",
+ (mpu_abort_count == (aborts_before + 1UL)) ? 1U : 0U);
+ mpu_expect_abort = 0UL;
+
+ /* Enforcement, part 3: the read-only code region must reject a write.
+ This is the check that matters: region coverage is enforced even with a
+ wrong access-permission encoding, so only a write-permission test can
+ show that the permissions themselves are right. */
+
+ aborts_before = mpu_abort_count;
+ mpu_expect_abort = 1UL;
+ {
+ volatile unsigned long *code_ptr =
+ (volatile unsigned long *) table[0].mpu_region_base;
+
+ *code_ptr = 0xDEADBEEFUL;
+ }
+ mpu_expect_abort = 0UL;
+
+ console_puts("[check] aborts before/after write to code = ");
+ console_puthex(aborts_before);
+ console_puts(" / ");
+ console_puthex(mpu_abort_count);
+ console_puts("\n");
+
+ failures += report("[check] write to read-only code region faulted ",
+ (mpu_abort_count == (aborts_before + 1UL)) ? 1U : 0U);
+
+ {
+ volatile unsigned long *code_ptr =
+ (volatile unsigned long *) table[0].mpu_region_base;
+
+ failures += report("[check] code region contents unmodified ",
+ (*code_ptr != 0xDEADBEEFUL) ? 1U : 0U);
+ }
+
+ /* Enforcement, part 4: the execute-never data region must reject an
+ instruction fetch. This is the counterpart to part 3 and the check that
+ pins down PRBAR.XN specifically. It is worth stating why it exists: an
+ earlier version of program_region() shifted every PRBAR field one bit
+ too far left, which left XN in the real AP[1] and every "non-executable"
+ region freely executable. Every check above still passed, because the
+ accidental AP bits happened to land correctly. Only fetching from a
+ supposedly non-executable region exposed it -- on S32Z280 silicon,
+ where the core cheerfully ran instructions out of .data. */
+
+ {
+ unsigned long pabts_before = mpu_pabt_count;
+
+ mpu_expect_pabt = 1UL;
+ mpu_try_execute((void (*)(void)) (void *) &executable_probe);
+ mpu_expect_pabt = 0UL;
+
+ console_puts("[check] prefetch aborts before/after exec from data = ");
+ console_puthex(pabts_before);
+ console_puts(" / ");
+ console_puthex(mpu_pabt_count);
+ console_puts("\n");
+
+ failures += report("[check] execute from non-executable data faulted ",
+ (mpu_pabt_count == (pabts_before + 1UL)) ? 1U : 0U);
+ }
+
+ /* And the kernel is still healthy afterwards. */
+
+ failures += report("[check] kernel still running after the fault ",
+ (tx_thread_identify() == &thread_check) ? 1U : 0U);
+
+ if (failures == 0U)
+ {
+ console_puts("\nMPU RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nMPU RESULT: FAILED\n");
+ }
+
+ console_exit(failures);
+}
+
+
+/**************************************************************************/
+/* tx_application_define */
+/**************************************************************************/
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ (void) tx_thread_create(&thread_check, "mpu check", thread_check_entry,
+ 0UL, thread_check_stack, sizeof(thread_check_stack),
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S. Does not return. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ console_puts("\n=== ThreadX AR1/M5 PMSAv8-R protection and caches "
+ "(Cortex-R52, Armv8-R AEM FVP) ===\n");
+
+ tx_kernel_enter();
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_nesting.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_nesting.c
new file mode 100644
index 00000000..0e204d06
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_nesting.c
@@ -0,0 +1,216 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* demo_nesting.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Nested IRQ handling: _tx_thread_irq_nesting_start and */
+/* _tx_thread_irq_nesting_end. */
+/* */
+/* Those two routines have shipped in this port since it was written */
+/* and nothing had ever called them. They were compiled into every */
+/* build, never entered by any image, on the model or on silicon. */
+/* This is the image that enters them. */
+/* */
+/* HOW NESTING IS PROVOKED */
+/* */
+/* Two interrupt sources are needed and one must outrank the other. */
+/* The generic timer PPI is already there; the second is an SGI, which */
+/* a core can raise on itself, given a numerically lower priority so */
+/* the GIC lets it preempt. */
+/* */
+/* Inside the timer handler, after nesting_start has left IRQ mode for */
+/* System mode and re-enabled IRQ, board_irq_handler raises the SGI and */
+/* spins briefly. The SGI outranks the timer, so it is delivered */
+/* immediately and re-enters el1_irq_entry while the timer handler is */
+/* still on the stack. That is nesting, and the depth counter sees 2. */
+/* */
+/* The spin is bounded. Without TX_ENABLE_IRQ_NESTING the SGI can */
+/* never arrive, and a test that hangs in that configuration would be */
+/* worse than one that fails. */
+/* */
+/* WHAT EACH CHECK IS FOR */
+/* */
+/* N1 the SGI arrives at all. This is separate on purpose: it tests */
+/* the ICC_SGI1R encoding, which does not transcribe from the */
+/* AArch64 alias, so if N1 fails the fault is in gicv3_send_sgi and */
+/* not in the nesting routines. */
+/* N2 the SGI arrived while another handler was active, depth 2. */
+/* N3 depth returned to zero, so the pairing unwound. */
+/* N4 the tick still advances afterwards, so nesting did not damage */
+/* the timer path. */
+/* N5 threads still run and preempt, so the context save and restore */
+/* survived being re-entered. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "board.h"
+#include "console.h"
+#include "gicv3.h"
+
+#define DEMO_STACK_SIZE 2048
+
+static TX_THREAD thread_check;
+static TX_THREAD thread_spin;
+
+static ULONG thread_check_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+static ULONG thread_spin_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+static volatile ULONG spin_runs;
+
+
+static UINT report(const char *label_ptr, UINT passed)
+{
+ console_puts(label_ptr);
+ console_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+ return (passed != 0U) ? 0U : 1U;
+}
+
+
+static void report_hex(const char *label_ptr, unsigned long value)
+{
+ console_puts(label_ptr);
+ console_puthex(value);
+ console_puts("\n");
+}
+
+
+/* Lowest priority, never blocks: gives the tick something to preempt and shows
+ the scheduler is still alive after handlers have been nested. */
+
+static void thread_spin_entry(ULONG thread_input)
+{
+ (void) thread_input;
+
+ for (;;)
+ {
+ spin_runs++;
+ }
+}
+
+
+static void thread_check_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ ULONG sgi_before;
+ ULONG ticks_start;
+ ULONG ticks_end;
+ ULONG spin_before;
+
+ (void) thread_input;
+
+ /* N1 first, with provocation still off, so this measures only whether an
+ SGI raised from thread context is delivered and dispatched. */
+
+ console_puts("[check] raising an SGI from thread context\n");
+
+ sgi_before = board_sgi_count;
+ gicv3_send_sgi(BOARD_NEST_SGI_INTID);
+ tx_thread_sleep(2);
+
+ failures += report("[check] N1 SGI delivered and dispatched ",
+ (board_sgi_count > sgi_before) ? 1U : 0U);
+
+ if (board_sgi_count == sgi_before)
+ {
+ /* Everything below depends on the SGI working, so say so rather than
+ reporting four more failures that all have the same cause. */
+
+ console_puts("[check] SGI never arrived; the remaining checks would\n"
+ " only restate that, so they are skipped.\n");
+ console_puts("\nNESTING RESULT: FAILED\n");
+ console_exit(1U);
+ }
+
+ /* Now let the timer handler provoke a nested SGI, and give it time for a
+ good number of ticks. */
+
+ console_puts("[check] enabling nested provocation in the timer handler\n");
+
+ board_nest_provoke = 1UL;
+ ticks_start = tx_time_get();
+ spin_before = spin_runs;
+ tx_thread_sleep(50);
+ ticks_end = tx_time_get();
+ board_nest_provoke = 0UL;
+
+ report_hex("[check] max nesting depth observed = ", board_nest_max);
+ report_hex("[check] nested SGI count = ", board_sgi_nested_count);
+ report_hex("[check] nesting depth now = ", board_nest_depth);
+
+ failures += report("[check] N2 SGI nested inside another handler ",
+ (board_sgi_nested_count > 0UL) &&
+ (board_nest_max >= 2UL) ? 1U : 0U);
+
+ failures += report("[check] N3 nesting unwound to depth zero ",
+ (board_nest_depth == 0UL) ? 1U : 0U);
+
+ failures += report("[check] N4 tick still advancing after nesting ",
+ ((ticks_end - ticks_start) >= 40UL) ? 1U : 0U);
+
+ failures += report("[check] N5 lower-priority thread still scheduled ",
+ (spin_runs > spin_before) ? 1U : 0U);
+
+ report_hex("[check] spurious interrupts = ", board_spurious_count);
+ report_hex("[check] unexpected INTID = ", board_unexpected_intid);
+
+ failures += report("[check] no spurious or unexpected interrupts ",
+ ((board_spurious_count == 0UL) &&
+ (board_unexpected_intid == 0UL)) ? 1U : 0U);
+
+ if (failures == 0U)
+ {
+ console_puts("\nNESTING RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nNESTING RESULT: FAILED\n");
+ }
+
+ console_exit(failures);
+}
+
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ (void) tx_thread_create(&thread_check, "nest check", thread_check_entry,
+ 0UL, thread_check_stack, sizeof(thread_check_stack),
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&thread_spin, "spinner", thread_spin_entry,
+ 0UL, thread_spin_stack, sizeof(thread_spin_stack),
+ 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+void bsp_main(void)
+{
+ console_puts("\n=== ThreadX nested IRQ handling "
+ "(Cortex-R52, Armv8-R AArch32, Armv8-R AEM FVP) ===\n");
+
+ tx_kernel_enter();
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_threadx.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_threadx.c
new file mode 100644
index 00000000..90fe257d
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_threadx.c
@@ -0,0 +1,393 @@
+/**************************************************************************/
+/* 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 */
+/**************************************************************************/
+// Portions of this file were generated with AI assistance.
+
+/* Copied unmodified from samples/demo_threadx.c for the Armv8-R AEM FVP
+ example build, then extended with a verification thread and a bsp_main
+ entry point. Keeping the demo body byte-identical to the shipped sample
+ is the point of AR1 milestone M4: it shows the standard ThreadX demo runs
+ on Cortex-R52 with no demo-side changes. The additions are confined to
+ the end of this file and one call in tx_application_define. */
+
+/* This is a small demo of the high-performance ThreadX kernel. It includes examples of eight
+ threads of different priorities, using a message queue, semaphore, mutex, event flags group,
+ byte pool, and block pool. */
+
+#include "tx_api.h"
+#include "demo_verify.h"
+
+#define DEMO_STACK_SIZE 1024
+#define DEMO_BYTE_POOL_SIZE 9120
+#define DEMO_BLOCK_POOL_SIZE 100
+#define DEMO_QUEUE_SIZE 100
+
+
+/* Define the ThreadX object control blocks... */
+
+TX_THREAD thread_0;
+TX_THREAD thread_1;
+TX_THREAD thread_2;
+TX_THREAD thread_3;
+TX_THREAD thread_4;
+TX_THREAD thread_5;
+TX_THREAD thread_6;
+TX_THREAD thread_7;
+TX_QUEUE queue_0;
+TX_SEMAPHORE semaphore_0;
+TX_MUTEX mutex_0;
+TX_EVENT_FLAGS_GROUP event_flags_0;
+TX_BYTE_POOL byte_pool_0;
+TX_BLOCK_POOL block_pool_0;
+UCHAR memory_area[DEMO_BYTE_POOL_SIZE];
+
+
+/* Define the counters used in the demo application... */
+
+ULONG thread_0_counter;
+ULONG thread_1_counter;
+ULONG thread_1_messages_sent;
+ULONG thread_2_counter;
+ULONG thread_2_messages_received;
+ULONG thread_3_counter;
+ULONG thread_4_counter;
+ULONG thread_5_counter;
+ULONG thread_6_counter;
+ULONG thread_7_counter;
+
+
+/* Define thread prototypes. */
+
+void thread_0_entry(ULONG thread_input);
+void thread_1_entry(ULONG thread_input);
+void thread_2_entry(ULONG thread_input);
+void thread_3_and_4_entry(ULONG thread_input);
+void thread_5_entry(ULONG thread_input);
+void thread_6_and_7_entry(ULONG thread_input);
+
+
+/* Define main entry point. */
+
+int main()
+{
+
+ /* Enter the ThreadX kernel. */
+ tx_kernel_enter();
+}
+
+
+/* Define what the initial system looks like. */
+
+void tx_application_define(void *first_unused_memory)
+{
+
+CHAR *pointer = TX_NULL;
+
+
+ /* Create a byte memory pool from which to allocate the thread stacks. */
+ tx_byte_pool_create(&byte_pool_0, "byte pool 0", memory_area, DEMO_BYTE_POOL_SIZE);
+
+ /* Put system definition stuff in here, e.g. thread creates and other assorted
+ create information. */
+
+ /* Allocate the stack for thread 0. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create the main thread. */
+ tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, DEMO_STACK_SIZE,
+ 1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+
+ /* Allocate the stack for thread 1. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create threads 1 and 2. These threads pass information through a ThreadX
+ message queue. It is also interesting to note that these threads have a time
+ slice. */
+ tx_thread_create(&thread_1, "thread 1", thread_1_entry, 1,
+ pointer, DEMO_STACK_SIZE,
+ 16, 16, 4, TX_AUTO_START);
+
+ /* Allocate the stack for thread 2. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ tx_thread_create(&thread_2, "thread 2", thread_2_entry, 2,
+ pointer, DEMO_STACK_SIZE,
+ 16, 16, 4, TX_AUTO_START);
+
+ /* Allocate the stack for thread 3. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create threads 3 and 4. These threads compete for a ThreadX counting semaphore.
+ An interesting thing here is that both threads share the same instruction area. */
+ tx_thread_create(&thread_3, "thread 3", thread_3_and_4_entry, 3,
+ pointer, DEMO_STACK_SIZE,
+ 8, 8, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the stack for thread 4. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ tx_thread_create(&thread_4, "thread 4", thread_3_and_4_entry, 4,
+ pointer, DEMO_STACK_SIZE,
+ 8, 8, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the stack for thread 5. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create thread 5. This thread simply pends on an event flag which will be set
+ by thread_0. */
+ tx_thread_create(&thread_5, "thread 5", thread_5_entry, 5,
+ pointer, DEMO_STACK_SIZE,
+ 4, 4, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the stack for thread 6. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create threads 6 and 7. These threads compete for a ThreadX mutex. */
+ tx_thread_create(&thread_6, "thread 6", thread_6_and_7_entry, 6,
+ pointer, DEMO_STACK_SIZE,
+ 8, 8, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the stack for thread 7. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ tx_thread_create(&thread_7, "thread 7", thread_6_and_7_entry, 7,
+ pointer, DEMO_STACK_SIZE,
+ 8, 8, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the message queue. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_QUEUE_SIZE*sizeof(ULONG), TX_NO_WAIT);
+
+ /* Create the message queue shared by threads 1 and 2. */
+ tx_queue_create(&queue_0, "queue 0", TX_1_ULONG, pointer, DEMO_QUEUE_SIZE*sizeof(ULONG));
+
+ /* Create the semaphore used by threads 3 and 4. */
+ tx_semaphore_create(&semaphore_0, "semaphore 0", 1);
+
+ /* Create the event flags group used by threads 1 and 5. */
+ tx_event_flags_create(&event_flags_0, "event flags 0");
+
+ /* Create the mutex used by thread 6 and 7 without priority inheritance. */
+ tx_mutex_create(&mutex_0, "mutex 0", TX_NO_INHERIT);
+
+ /* Allocate the memory for a small block pool. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_BLOCK_POOL_SIZE, TX_NO_WAIT);
+
+ /* Create a block memory pool to allocate a message buffer from. */
+ tx_block_pool_create(&block_pool_0, "block pool 0", sizeof(ULONG), pointer, DEMO_BLOCK_POOL_SIZE);
+
+ /* Allocate a block and release the block memory. */
+ tx_block_allocate(&block_pool_0, (VOID **) &pointer, TX_NO_WAIT);
+
+ /* Release the block back to the pool. */
+ tx_block_release(pointer);
+
+ /* AR1/M4 addition: create the verification thread. */
+ demo_verify_create();
+}
+
+
+
+/* Define the test threads. */
+
+void thread_0_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* This thread simply sits in while-forever-sleep loop. */
+ while(1)
+ {
+
+ /* Increment the thread counter. */
+ thread_0_counter++;
+
+ /* 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);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+ }
+}
+
+
+void thread_1_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* This thread simply sends messages to a queue shared by thread 2. */
+ while(1)
+ {
+
+ /* Increment the thread counter. */
+ thread_1_counter++;
+
+ /* Send message to queue 0. */
+ status = tx_queue_send(&queue_0, &thread_1_messages_sent, TX_WAIT_FOREVER);
+
+ /* Check completion status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Increment the message sent. */
+ thread_1_messages_sent++;
+ }
+}
+
+
+void thread_2_entry(ULONG thread_input)
+{
+
+ULONG received_message;
+UINT status;
+
+ /* This thread retrieves messages placed on the queue by thread 1. */
+ while(1)
+ {
+
+ /* Increment the thread counter. */
+ thread_2_counter++;
+
+ /* Retrieve a message from the queue. */
+ status = tx_queue_receive(&queue_0, &received_message, TX_WAIT_FOREVER);
+
+ /* Check completion status and make sure the message is what we
+ expected. */
+ if ((status != TX_SUCCESS) || (received_message != thread_2_messages_received))
+ break;
+
+ /* Otherwise, all is okay. Increment the received message count. */
+ thread_2_messages_received++;
+ }
+}
+
+
+void thread_3_and_4_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* This function is executed from thread 3 and thread 4. As the loop
+ below shows, these function compete for ownership of semaphore_0. */
+ while(1)
+ {
+
+ /* Increment the thread counter. */
+ if (thread_input == 3)
+ thread_3_counter++;
+ else
+ thread_4_counter++;
+
+ /* Get the semaphore with suspension. */
+ status = tx_semaphore_get(&semaphore_0, TX_WAIT_FOREVER);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Sleep for 2 ticks to hold the semaphore. */
+ tx_thread_sleep(2);
+
+ /* Release the semaphore. */
+ status = tx_semaphore_put(&semaphore_0);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+ }
+}
+
+
+void thread_5_entry(ULONG thread_input)
+{
+
+UINT status;
+ULONG actual_flags;
+
+
+ /* This thread simply waits for an event in a forever loop. */
+ while(1)
+ {
+
+ /* Increment the thread counter. */
+ thread_5_counter++;
+
+ /* Wait for event flag 0. */
+ status = tx_event_flags_get(&event_flags_0, 0x1, TX_OR_CLEAR,
+ &actual_flags, TX_WAIT_FOREVER);
+
+ /* Check status. */
+ if ((status != TX_SUCCESS) || (actual_flags != 0x1))
+ break;
+ }
+}
+
+
+void thread_6_and_7_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* This function is executed from thread 6 and thread 7. As the loop
+ below shows, these function compete for ownership of mutex_0. */
+ while(1)
+ {
+
+ /* Increment the thread counter. */
+ if (thread_input == 6)
+ thread_6_counter++;
+ else
+ thread_7_counter++;
+
+ /* Get the mutex with suspension. */
+ status = tx_mutex_get(&mutex_0, TX_WAIT_FOREVER);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Get the mutex again with suspension. This shows
+ that an owning thread may retrieve the mutex it
+ owns multiple times. */
+ status = tx_mutex_get(&mutex_0, TX_WAIT_FOREVER);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Sleep for 2 ticks to hold the mutex. */
+ tx_thread_sleep(2);
+
+ /* Release the mutex. */
+ status = tx_mutex_put(&mutex_0);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Release the mutex again. This will actually
+ release ownership since it was obtained twice. */
+ status = tx_mutex_put(&mutex_0);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_verify.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_verify.c
new file mode 100644
index 00000000..a22c7e13
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_verify.c
@@ -0,0 +1,186 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* DEMONSTRATION RELEASE */
+/* */
+/* demo_verify.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* AR1 milestone M4: verifies that the standard eight-thread ThreadX */
+/* demo makes progress on Cortex-R52, then terminates the simulation */
+/* so the run can be automated. */
+/* */
+/* Where M3 proved the tick and a single preemption, this exercises */
+/* the demo's full object set -- queue, semaphore, mutex, event flags, */
+/* byte pool and block pool -- across eight threads at five priorities.*/
+/* Each of the ten demo counters is required to have advanced: a */
+/* stalled thread (a lost wakeup, a mishandled priority inversion) */
+/* shows up as a counter still at zero rather than as a plausible- */
+/* looking run. */
+/* */
+/* The verification thread runs at priority 0 so it can always preempt */
+/* the demo, and spends nearly all its life suspended. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "board.h"
+#include "console.h"
+#include "demo_verify.h"
+
+#define VERIFY_STACK_SIZE 2048
+
+/* Ticks to let the demo run before checking. thread_0 sleeps 10 ticks per
+ iteration and thread_5 waits on the event flag it sets, so these two are
+ the slowest; 300 ticks (3 s at 100 Hz) gives them ~30 iterations each. */
+
+#define VERIFY_SETTLE_TICKS 300UL
+
+/* Counters defined by the standard demo. */
+
+extern ULONG thread_0_counter;
+extern ULONG thread_1_counter;
+extern ULONG thread_1_messages_sent;
+extern ULONG thread_2_counter;
+extern ULONG thread_2_messages_received;
+extern ULONG thread_3_counter;
+extern ULONG thread_4_counter;
+extern ULONG thread_5_counter;
+extern ULONG thread_6_counter;
+extern ULONG thread_7_counter;
+
+static TX_THREAD verify_thread;
+static ULONG verify_stack[VERIFY_STACK_SIZE / sizeof(ULONG)];
+
+
+/**************************************************************************/
+/* check_counter -- report one counter and whether it advanced. */
+/**************************************************************************/
+
+static UINT check_counter(const char *label_ptr, ULONG value)
+{
+ console_puts(label_ptr);
+ console_puthex(value);
+ console_puts((value > 0UL) ? " PASS\n" : " FAIL\n");
+
+ return (value > 0UL) ? 0U : 1U;
+}
+
+
+/**************************************************************************/
+/* verify_thread_entry */
+/**************************************************************************/
+
+static void verify_thread_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ ULONG elapsed;
+
+ (void) thread_input;
+
+ console_puts("[verify] letting the standard demo run for ");
+ console_puthex(VERIFY_SETTLE_TICKS);
+ console_puts(" ticks...\n");
+
+ tx_thread_sleep(VERIFY_SETTLE_TICKS);
+
+ elapsed = tx_time_get();
+
+ console_puts("\n[verify] demo counters after settling:\n");
+
+ failures += check_counter(" thread 0 counter = ", thread_0_counter);
+ failures += check_counter(" thread 1 counter = ", thread_1_counter);
+ failures += check_counter(" thread 1 messages sent = ", thread_1_messages_sent);
+ failures += check_counter(" thread 2 counter = ", thread_2_counter);
+ failures += check_counter(" thread 2 messages received = ", thread_2_messages_received);
+ failures += check_counter(" thread 3 counter = ", thread_3_counter);
+ failures += check_counter(" thread 4 counter = ", thread_4_counter);
+ failures += check_counter(" thread 5 counter = ", thread_5_counter);
+ failures += check_counter(" thread 6 counter = ", thread_6_counter);
+ failures += check_counter(" thread 7 counter = ", thread_7_counter);
+
+ /* The queue must not have silently dropped or duplicated messages. */
+
+ console_puts("\n[verify] queue balance (sent >= received) ");
+ if (thread_1_messages_sent >= thread_2_messages_received)
+ {
+ console_puts("PASS\n");
+ }
+ else
+ {
+ failures++;
+ console_puts("FAIL\n");
+ }
+
+ console_puts("[verify] elapsed ticks = ");
+ console_puthex(elapsed);
+ console_puts("\n[verify] tick advanced past the settle period ");
+ if (elapsed >= VERIFY_SETTLE_TICKS)
+ {
+ console_puts("PASS\n");
+ }
+ else
+ {
+ failures++;
+ console_puts("FAIL\n");
+ }
+
+ if (failures == 0U)
+ {
+ console_puts("\nM4 RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nM4 RESULT: FAILED\n");
+ }
+
+ console_exit(failures);
+}
+
+
+/**************************************************************************/
+/* demo_verify_create */
+/**************************************************************************/
+
+void demo_verify_create(void)
+{
+ /* Priority 0 and its own static stack: the verification thread must be
+ able to preempt every demo thread, and must not perturb the demo's
+ byte-pool allocations. */
+
+ (void) tx_thread_create(&verify_thread, "verify", verify_thread_entry, 0UL,
+ verify_stack, sizeof(verify_stack),
+ 0U, 0U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S. Does not return. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ console_puts("\n=== ThreadX AR1/M4 standard eight-thread demo "
+ "(Cortex-R52, Armv8-R AArch32, Armv8-R AEM FVP) ===\n");
+
+ tx_kernel_enter();
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_verify.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_verify.h
new file mode 100644
index 00000000..71ebe6be
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_verify.h
@@ -0,0 +1,41 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* DEMONSTRATION RELEASE */
+/* */
+/* demo_verify.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Verification harness for the standard ThreadX demo, kept separate */
+/* so demo_threadx.c stays byte-identical to the shipped sample apart */
+/* from its header note and a single call to demo_verify_create(). */
+/* */
+/**************************************************************************/
+
+#ifndef DEMO_VERIFY_H
+#define DEMO_VERIFY_H
+
+/* Create the verification thread. Called from tx_application_define. */
+
+void demo_verify_create(void);
+
+#endif /* DEMO_VERIFY_H */
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S
new file mode 100644
index 00000000..77d81336
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S
@@ -0,0 +1,710 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* entry.S Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Reset entry for Cortex-R52 (Armv8-R, AArch32) on the Armv8-R AEM */
+/* FVP. The R52 always implements EL2 and resets into it, so this */
+/* file configures EL2 first, installs both the EL2 and EL1 vector */
+/* tables, and only then drops to EL1 (Supervisor mode) to run the */
+/* kernel. The HVC entry in the EL2 table is the seam that ZoneX */
+/* (roadmap AR3) grows into -- it is deliberately present from AR1 so */
+/* that partitioning does not require re-architecting the boot path. */
+/* */
+/* Define TX_R52_BOOT_AT_EL1 to skip EL2 configuration, for targets */
+/* where an earlier boot stage or a vendor EL2 monitor (for example */
+/* NXP's EL2M on S32Z2) has already dropped privilege to EL1. */
+/* */
+/**************************************************************************/
+
+#include "platform.h"
+
+ /* No .arch directive here. GNU as accepts "armv8-r", LLVM's integrated
+ assembler accepts no spelling of it at all -- not armv8-r, not armv8r --
+ and fails with "Unknown Arch". The architecture is already selected by
+ -mcpu=cortex-r52 on the command line, which both toolchains require and
+ both toolchain files pass, so the directive only restated it. */
+
+ .syntax unified
+ .arm
+
+/* AArch32 CPSR mode encodings and interrupt mask bits. */
+
+ .equ MODE_USR, 0x10
+ .equ MODE_FIQ, 0x11
+ .equ MODE_IRQ, 0x12
+ .equ MODE_SVC, 0x13
+ .equ MODE_ABT, 0x17
+ .equ MODE_HYP, 0x1A
+ .equ MODE_UND, 0x1B
+ .equ MODE_SYS, 0x1F
+ .equ PSR_A, 0x100 /* asynchronous abort mask */
+ .equ PSR_I, 0x80 /* IRQ mask */
+ .equ PSR_F, 0x40 /* FIQ mask */
+
+/* HCR (Hyp Configuration Register) bits used here. */
+
+ .equ HCR_VM, (1 << 0) /* stage-2 MPU enable */
+ .equ HCR_AMO, (1 << 3) /* route aborts to EL2 */
+ .equ HCR_IMO, (1 << 4) /* route IRQ to EL2 */
+ .equ HCR_FMO, (1 << 5) /* route FIQ to EL2 */
+ .equ HCR_TGE, (1 << 27) /* trap general exceptions to EL2 */
+ .equ HCR_HCD, (1 << 29) /* HVC disable -- must be clear */
+
+ .equ HCPTR_TCP, (3 << 10) /* trap EL1 access to CP10/CP11 */
+
+/* CNTHCTL: permit EL1/EL0 use of the physical counter and timer. */
+
+ .equ CNTHCTL_PL1PCTEN, (1 << 0)
+ .equ CNTHCTL_PL1PCEN, (1 << 1)
+
+/* ICC_HSRE: enable the GICv3 system-register interface for EL1. */
+
+ .equ ICC_HSRE_SRE, (1 << 0)
+ .equ ICC_HSRE_ENABLE, (1 << 3)
+
+/* CPACR grants EL1/EL0 access to CP10 and CP11 (the floating-point unit);
+ FPEXC.EN then enables floating-point execution. */
+
+ .equ CPACR_CP10, (3 << 20)
+ .equ CPACR_CP11, (3 << 22)
+ .equ FPEXC_EN, (1 << 30)
+
+ .equ SYS_WRITE0, 0x04 /* semihosting: write NUL-string */
+
+/* HSR (Hyp Syndrome Register) exception classes. Every exception taken to
+ EL2 from EL1/EL0 arrives at the single Hyp Trap Entry vector, so the cause
+ must be decoded from HSR.EC (bits [31:26]). */
+
+ .equ HSR_EC_SHIFT, 26
+ .equ HSR_EC_HVC, 0x12 /* HVC executed in AArch32 */
+
+#ifdef TXM_MODULE_MANAGER
+
+/* The module manager owns three of the EL1 vectors in a manager build: the
+ supervisor call, which is the privilege boundary a module crosses to reach
+ the kernel, and the two aborts, which are how a module that reaches outside
+ its regions is caught. Declared rather than merely referenced so that a
+ build with the manager half-configured fails at the link with a name in it. */
+
+ .extern __tx_module_svc_interrupt
+ .extern _txm_module_manager_data_abort
+ .extern _txm_module_manager_prefetch_abort
+
+#endif
+
+/* Report which vector was taken, then halt. The FVP offers no GDB stub
+ (Iris only), so a self-identifying fault is the primary debugging tool
+ for this port. Uses no stack: only r0/r1 and a semihosting call. */
+
+ .macro FAULT_REPORT msglabel
+ ldr r1, =\msglabel
+ mov r0, #SYS_WRITE0
+ hlt 0xf000
+ b fault_halt
+ .endm
+
+/**************************************************************************/
+/* EL2 (Hyp) vector table. Must be 32-byte aligned for HVBAR. */
+/**************************************************************************/
+
+ .section .vectors_el2, "ax"
+ .balign 32
+ .global el2_vectors
+el2_vectors:
+/* Offsets 0x04-0x10 are exceptions taken *from* Hyp mode itself. Offset
+ 0x14 is the Hyp Trap Entry: every exception routed to EL2 from EL1/EL0 --
+ HVC, stage-2 faults, trapped register accesses -- funnels through it and
+ is decoded from HSR. That single funnel is the ZoneX (AR3) seam. */
+
+ b el2_trap_reset /* 0x00 reset (unused at EL2) */
+ b el2_trap_undef /* 0x04 undef, from Hyp */
+ b el2_trap_svc /* 0x08 SVC, from Hyp */
+ b el2_trap_pabt /* 0x0C prefetch abort, Hyp */
+ b el2_trap_dabt /* 0x10 data abort, from Hyp */
+ b el2_hyp_trap_entry /* 0x14 from EL1/EL0 <-- seam */
+ b el2_trap_irq /* 0x18 IRQ */
+ b el2_trap_fiq /* 0x1C FIQ */
+
+/**************************************************************************/
+/* EL1 vector table. Must be 32-byte aligned for VBAR. */
+/**************************************************************************/
+
+ .section .vectors_el1, "ax"
+ .balign 32
+ .global el1_vectors
+el1_vectors:
+ b el1_trap_reset /* 0x00 reset */
+ b el1_trap_undef /* 0x04 undefined instruction */
+#ifdef TXM_MODULE_MANAGER
+ b __tx_module_svc_interrupt /* 0x08 module kernel boundary */
+#else
+ b el1_trap_svc /* 0x08 supervisor call */
+#endif
+ b el1_trap_pabt /* 0x0C prefetch abort */
+ b el1_trap_dabt /* 0x10 data abort */
+ b el1_trap_reserved /* 0x14 reserved */
+ b el1_irq_entry /* 0x18 IRQ */
+ b el1_trap_fiq /* 0x1C FIQ */
+
+ .text
+ .balign 4
+
+/**************************************************************************/
+/* _start -- reset entry. Entered at EL2 (Hyp mode) on Cortex-R52. */
+/**************************************************************************/
+
+ .global _start
+ .type _start, %function
+_start:
+#ifndef TX_R52_BOOT_AT_EL1
+
+ /* Hyp-mode stack must exist before anything can be pushed at EL2. */
+
+ ldr sp, =__hyp_stack_top
+
+ /* Install the EL2 vector table (HVBAR). */
+
+ ldr r0, =el2_vectors
+ mcr p15, 4, r0, c12, c0, 0
+ isb
+
+ /* Configure EL2: keep exceptions and interrupts routed to EL1, and
+ leave the stage-2 MPU disabled for AR1 (the FVP background memory
+ map already permits execution and data access in low DRAM). ZoneX
+ enables HCR_VM and programs stage 2 in AR3.
+
+ HCR_HCD is cleared explicitly so HVC is enabled from EL1 rather than
+ relying on its reset value -- the ZoneX seam depends on it. */
+
+ mrc p15, 4, r0, c1, c1, 0 /* HCR */
+ ldr r1, =(HCR_TGE | HCR_AMO | HCR_IMO | HCR_FMO | HCR_VM | HCR_HCD)
+ bic r0, r0, r1
+ mcr p15, 4, r0, c1, c1, 0
+ isb
+
+ /* Let EL1 use the FPU: clear HCPTR.TCP11/TCP10. R52 always implements
+ at least a single-precision FPU, so these traps must be cleared even
+ for a soft-float kernel build if application code uses the FPU. */
+
+ mrc p15, 4, r0, c1, c1, 2 /* HCPTR */
+ ldr r1, =HCPTR_TCP
+ bic r0, r0, r1
+ mcr p15, 4, r0, c1, c1, 2
+ isb
+
+ /* Generic timer, part 1 of 2 (part 2 is timer_init at EL1).
+
+ CNTFRQ resets to ZERO on this model and is writable only at the
+ highest implemented exception level, so it must be programmed here at
+ EL2. The value is CNTFID0 read back from the counter control frame.
+ Software that derived a tick interval from CNTFRQ without this would
+ divide by zero. */
+
+ ldr r0, =SYSTEM_COUNTER_HZ
+ mcr p15, 0, r0, c14, c0, 0 /* CNTFRQ */
+
+ /* Permit EL1/EL0 to use the physical counter and the physical timer.
+ CNTHCTL.PL1PCTEN/PL1PCEN reset disabled, so without this every EL1
+ access to CNTPCT or CNTP_* would trap to EL2. */
+
+ mrc p15, 4, r0, c14, c1, 0 /* CNTHCTL */
+ orr r0, r0, #(CNTHCTL_PL1PCTEN | CNTHCTL_PL1PCEN)
+ mcr p15, 4, r0, c14, c1, 0
+ isb
+
+ /* Permit EL1 to reach the GICv3 CPU interface through its system
+ registers (ICC_*), which is how the R52 acknowledges interrupts. */
+
+ mrc p15, 4, r0, c12, c9, 5 /* ICC_HSRE */
+ orr r0, r0, #(ICC_HSRE_SRE | ICC_HSRE_ENABLE)
+ mcr p15, 4, r0, c12, c9, 5
+ isb
+
+ /* Progress marker: proves we reached EL2 and semihosting works. */
+
+ ldr r1, =msg_el2
+ mov r0, #SYS_WRITE0
+ hlt 0xf000
+
+ /* Drop to EL1 (Supervisor), interrupts still masked. */
+
+ ldr r0, =el1_entry
+ msr ELR_hyp, r0
+#ifdef TX_ENABLE_FIQ_SUPPORT
+
+ /* FIQ left unmasked, IRQ still masked. ThreadX unmasks IRQ when it
+ * schedules the first thread; FIQ is different, because tx_thread_stack_build
+ * only clears the F bit in a thread's CPSR when the library was built with
+ * FIQ support, and nothing else ever clears it. Leaving PSR_F set here
+ * would mask FIQ until the first thread ran, which is survivable but makes
+ * an FIQ raised from bsp_main silently do nothing.
+ */
+
+ ldr r0, =(PSR_A | PSR_I | MODE_SVC)
+#else
+ ldr r0, =(PSR_A | PSR_I | PSR_F | MODE_SVC)
+#endif
+ msr SPSR_hyp, r0
+ isb
+ eret
+
+#else
+ b el1_entry
+#endif
+
+/**************************************************************************/
+/* el1_entry -- first instruction executed at EL1. */
+/**************************************************************************/
+
+ .global el1_entry
+ .type el1_entry, %function
+el1_entry:
+
+ /* Install the EL1 vector table (VBAR). */
+
+ ldr r0, =el1_vectors
+ mcr p15, 0, r0, c12, c0, 0
+ isb
+
+ /* Give every EL1 mode its own stack. Interrupts stay masked: CPS
+ changes mode only. System mode shares SP_usr, so it is set last
+ before returning to Supervisor mode. */
+
+ cps #MODE_IRQ
+ ldr sp, =__irq_stack_top
+ cps #MODE_FIQ
+ ldr sp, =__fiq_stack_top
+ cps #MODE_ABT
+ ldr sp, =__abt_stack_top
+ cps #MODE_UND
+ ldr sp, =__und_stack_top
+ cps #MODE_SYS
+ ldr sp, =__sys_stack_top
+ cps #MODE_SVC
+ ldr sp, =__svc_stack_top
+
+#ifdef __ARM_FP
+
+ /* Enable the floating-point unit for EL1.
+ *
+ * Two separate gates must be opened and both reset closed:
+ * CPACR grants EL1/EL0 access to CP10/CP11. It reads 0 out of reset,
+ * so the first floating-point instruction would otherwise raise
+ * an Undefined Instruction exception.
+ * FPEXC .EN enables floating-point execution itself.
+ * EL2 has already cleared HCPTR.TCP10/TCP11 so neither access traps to EL2.
+ *
+ * This is a BSP responsibility, not the kernel's: tx_thread_vfp_enable()
+ * only sets the per-thread software flag that makes the context switch
+ * save and restore floating-point state. It does not turn the hardware
+ * on, so without the code below a VFP-enabled build faults on its first
+ * context switch that touches D-registers.
+ *
+ * Guarded by __ARM_FP, which the compiler defines exactly when the build
+ * can emit floating-point instructions. In a soft-float build "vmsr
+ * fpexc" is not even assemblable for this target.
+ */
+
+ mrc p15, 0, r0, c1, c0, 2 /* CPACR */
+ orr r0, r0, #(CPACR_CP10 | CPACR_CP11)
+ mcr p15, 0, r0, c1, c0, 2
+ isb
+
+ mov r0, #FPEXC_EN
+ vmsr fpexc, r0
+ isb
+
+#endif
+
+ /* Zero .bss before any C code runs. */
+
+ ldr r0, =__bss_start__
+ ldr r1, =__bss_end__
+ mov r2, #0
+bss_zero_loop:
+ cmp r0, r1
+ bhs bss_zero_done
+ str r2, [r0], #4
+ b bss_zero_loop
+bss_zero_done:
+
+#ifdef TX_R52_ENABLE_MPU
+
+ /* Protection and caches on before any application code runs. This is
+ after .bss is cleared because mpu_init keeps its region table there. */
+
+ bl mpu_init
+
+#endif
+
+ bl bsp_main
+
+ /* bsp_main is not expected to return. */
+_start_hang:
+ b _start_hang
+
+/**************************************************************************/
+/* el1_irq_entry -- IRQ vector. */
+/* */
+/* With ThreadX linked in, the IRQ vector branches straight into */
+/* _tx_thread_context_save, which saves the interrupted context and */
+/* branches back to __tx_irq_processing_return. The branch must be a */
+/* plain B: context save adjusts lr itself to find the point of */
+/* interrupt. Interrupts are still disabled on return from it. */
+/* */
+/* Images that do not link ThreadX (the M1 boot check) keep the */
+/* self-identifying fault reporter instead. */
+/**************************************************************************/
+
+ .global el1_irq_entry
+ .type el1_irq_entry, %function
+el1_irq_entry:
+#ifdef TX_R52_USE_THREADX_IRQ
+ b _tx_thread_context_save
+
+ .global __tx_irq_processing_return
+__tx_irq_processing_return:
+#ifdef TX_ENABLE_IRQ_NESTING
+
+ /* Nested IRQ handling. _tx_thread_irq_nesting_start leaves IRQ mode for
+ * System mode and re-enables IRQ, so an interrupt of higher priority can
+ * preempt this handler and re-enter at el1_irq_entry above. It is paired
+ * with _tx_thread_irq_nesting_end before _tx_thread_context_restore.
+ *
+ * The ORDER here is the whole difficulty, and getting it wrong does not
+ * fail gracefully. The interrupt must be ACKNOWLEDGED BEFORE nesting is
+ * started. Reading ICC_IAR1 is what raises the GIC's running priority to
+ * this interrupt's own, which is what masks it and everything of equal or
+ * lower priority; only then is re-enabling IRQ safe. Acknowledge after
+ * nesting_start instead and the still-pending, still-level-asserted timer
+ * is taken again the instant IRQ is enabled, and again, until the IRQ and
+ * System stacks are destroyed. That presents as garbage on the console and
+ * a hang, with no fault to point at it. The Cortex-R5 example BSP says the
+ * same thing in one line: "ensure all IRQ interrupts are cleared prior to
+ * enabling nested IRQ interrupts".
+ *
+ * The INTID is carried in r4, which survives the mode switch because only
+ * SP and LR are banked, and is also pushed on the IRQ stack so a nested
+ * level that reuses r4 cannot lose this level's value. End-of-interrupt is
+ * left until after nesting_end, in IRQ mode with interrupts masked, so
+ * dropping the running priority cannot re-admit this same interrupt.
+ */
+
+ bl gicv3_acknowledge /* IRQ mode, interrupts masked */
+ mov r4, r0
+ stmdb sp!, {r4, r5} /* r5 keeps 8-byte alignment */
+
+ bl _tx_thread_irq_nesting_start
+ mov r0, r4
+ bl board_irq_service
+ bl _tx_thread_irq_nesting_end
+
+ ldmia sp!, {r4, r5}
+ mov r0, r4
+ bl gicv3_end_of_interrupt
+#else
+ bl board_irq_handler
+#endif
+ b _tx_thread_context_restore
+#else
+ FAULT_REPORT msg_el1_irq
+#endif
+
+/**************************************************************************/
+/* el2_hyp_trap_entry -- the ZoneX (AR3) seam. */
+/* */
+/* Reached for every exception routed to EL2 from EL1/EL0, so the cause */
+/* is decoded from HSR.EC before dispatch. AR1 handles only HVC, which */
+/* it counts before returning to EL1; that proves EL2 is live and */
+/* reachable without the boot path changing later. AR3 grows the */
+/* dispatch table here (stage-2 faults, trapped accesses, IRQ paravirt). */
+/* */
+/* EL1's registers must be preserved -- this runs on the Hyp stack. */
+/**************************************************************************/
+
+ .global el2_hyp_trap_entry
+ .type el2_hyp_trap_entry, %function
+el2_hyp_trap_entry:
+ push {r0, r1}
+ mrc p15, 4, r0, c5, c2, 0 /* HSR */
+ lsr r1, r0, #HSR_EC_SHIFT
+ cmp r1, #HSR_EC_HVC
+ bne el2_hyp_trap_unhandled
+
+ /* HVC from EL1: AR1 placeholder for the ZoneX hypercall dispatch. */
+
+ ldr r0, =_hvc_call_count
+ ldr r1, [r0]
+ add r1, r1, #1
+ str r1, [r0]
+ pop {r0, r1}
+ eret
+
+el2_hyp_trap_unhandled:
+ pop {r0, r1}
+ FAULT_REPORT msg_el2_hyp
+
+/**************************************************************************/
+/* Unhandled exceptions. Each records a distinct code in r12 so a halted*/
+/* model or a later fault dump (AR1/M5) can identify the cause. Real */
+/* handlers arrive with the interrupt work in M3. */
+/**************************************************************************/
+
+/* Unhandled exceptions. Real handlers arrive with the interrupt work in
+ AR1/M3; until then each vector identifies itself and halts. */
+
+el2_trap_reset: FAULT_REPORT msg_el2_reset
+el2_trap_undef: FAULT_REPORT msg_el2_undef
+el2_trap_svc: FAULT_REPORT msg_el2_svc
+el2_trap_pabt: FAULT_REPORT msg_el2_pabt
+el2_trap_dabt: FAULT_REPORT msg_el2_dabt
+el2_trap_irq: FAULT_REPORT msg_el2_irq
+el2_trap_fiq: FAULT_REPORT msg_el2_fiq
+
+el1_trap_reset: FAULT_REPORT msg_el1_reset
+el1_trap_undef: FAULT_REPORT msg_el1_undef
+el1_trap_svc: FAULT_REPORT msg_el1_svc
+/* Prefetch abort. Fatal, except during the MPU self-test's execute-never
+ check. This one cannot recover the way the data-abort path does: skipping
+ "the faulting instruction" is impossible when the instruction is precisely
+ what could not be fetched. So mpu_try_execute below records where to land
+ and this handler returns there, restoring the pre-fault mode from SPSR. */
+
+el1_trap_pabt:
+#ifdef TXM_MODULE_MANAGER
+
+ /* A fault from User mode is a module reaching outside its code region, and
+ it belongs to the module manager, which records it and terminates the
+ thread. A fault from a privileged mode falls through to the paths below,
+ which the MPU self-test depends on.
+
+ r0 is pushed and popped around the test rather than simply used: the
+ capture in the manager records the faulting registers, and clobbering one
+ here would have it record ours instead. LDM does not affect the flags,
+ so the comparison still holds after the pop. */
+
+ stmdb sp!, {r0}
+ mrs r0, spsr
+ and r0, r0, #0x1F
+ cmp r0, #0x10 /* User mode? */
+ ldmia sp!, {r0}
+ beq _txm_module_manager_prefetch_abort
+
+#endif
+#ifdef TX_R52_MPU_FAULT_TEST
+ push {r0, r1}
+ ldr r0, =mpu_expect_pabt
+ ldr r1, [r0]
+ cmp r1, #0
+ beq el1_pabt_fatal
+ mov r1, #0
+ str r1, [r0] /* consume the expectation */
+ ldr r0, =mpu_pabt_count
+ ldr r1, [r0]
+ add r1, r1, #1
+ str r1, [r0]
+ dsb
+ ldr r0, =mpu_abort_return
+ ldr r1, [r0]
+ mov lr, r1 /* land point, set before pop */
+ pop {r0, r1}
+ subs pc, lr, #0 /* return, restoring CPSR */
+el1_pabt_fatal:
+ pop {r0, r1}
+#endif
+ FAULT_REPORT msg_el1_pabt
+
+
+#ifdef TX_R52_MPU_FAULT_TEST
+/* mpu_try_execute(void (*target)(void))
+ Call target and return normally if the fetch is permitted. If it is not,
+ el1_trap_pabt returns to the same place, so the caller continues either way
+ and can tell the two apart by mpu_pabt_count. SP is untouched by the
+ faulting branch, so the saved LR is still on the stack when we land. */
+
+ .global mpu_try_execute
+ .type mpu_try_execute, %function
+mpu_try_execute:
+ push {r4, lr}
+ ldr r1, =mpu_abort_return
+ adr r2, mpu_try_execute_land
+ str r2, [r1]
+ dsb
+ isb
+ blx r0
+mpu_try_execute_land:
+ pop {r4, lr}
+ bx lr
+ .size mpu_try_execute, . - mpu_try_execute
+#endif
+
+/* Data abort. Normally fatal and self-reporting, but the MPU self-test
+ needs to provoke a permission fault and carry on: without that, "the MPU
+ is enabled" is an unverified claim. When mpu_expect_abort is non-zero the
+ fault is counted and execution resumes at the instruction *after* the one
+ that faulted. On a data abort LR_abt holds the faulting address plus 8,
+ so returning to LR_abt-4 skips it, where LR_abt-8 would retry it forever.
+ Runs on the Abort-mode stack set up above. */
+
+el1_trap_dabt:
+#ifdef TXM_MODULE_MANAGER
+
+ /* A fault from User mode is a module reaching outside its data region. See
+ el1_trap_pabt above for why r0 is saved across the test. */
+
+ stmdb sp!, {r0}
+ mrs r0, spsr
+ and r0, r0, #0x1F
+ cmp r0, #0x10 /* User mode? */
+ ldmia sp!, {r0}
+ beq _txm_module_manager_data_abort
+
+#endif
+#ifdef TX_R52_MPU_FAULT_TEST
+ push {r0, r1}
+ ldr r0, =mpu_expect_abort
+ ldr r1, [r0]
+ cmp r1, #0
+ beq el1_dabt_fatal
+ mov r1, #0
+ str r1, [r0] /* consume the expectation */
+ ldr r0, =mpu_abort_count
+ ldr r1, [r0]
+ add r1, r1, #1
+ str r1, [r0]
+ dsb
+ pop {r0, r1}
+ subs pc, lr, #4 /* resume after the fault */
+el1_dabt_fatal:
+ pop {r0, r1}
+#endif
+ FAULT_REPORT msg_el1_dabt
+el1_trap_reserved: FAULT_REPORT msg_el1_reserved
+el1_trap_fiq:
+#if defined(TX_ENABLE_FIQ_SUPPORT) && defined(TX_R52_USE_THREADX_IRQ)
+
+ /* Both guards are needed, and the second is not obvious.
+ * TX_ENABLE_FIQ_SUPPORT is PUBLIC on the threadx target, so it reaches every
+ * image the moment the library is built with FIQ, including images that link
+ * no interrupt controller at all. demo_m2 is one: it has no gicv3.c and no
+ * irq_dispatch.c, so referencing gicv3_acknowledge_group0 and
+ * board_fiq_service from here broke its link outright. Images without the
+ * interrupt infrastructure keep the fault reporter on this vector, which is
+ * how the IRQ vector above is gated too.
+ */
+
+ /* FIQ has its own context save and restore in the port, and its own nesting
+ * pair. The shape mirrors the IRQ path, and so does the trap: the interrupt
+ * must be acknowledged before nesting starts. Group 0 has its own
+ * acknowledge and end-of-interrupt registers -- using the Group 1 ones here
+ * returns the spurious INTID and leaves the interrupt pending, which
+ * presents as an FIQ storm rather than as an error.
+ *
+ * FIQ mode banks r8-r12 as well as SP and LR, so the INTID cannot ride in a
+ * high register across the switch to System mode the way r4 does on the IRQ
+ * path. r4 is not banked in FIQ mode either, so it still works, and it is
+ * pushed on the FIQ stack as well so a nested level cannot lose it.
+ */
+
+ b _tx_thread_fiq_context_save
+
+ .global __tx_fiq_processing_return
+__tx_fiq_processing_return:
+#ifdef TX_ENABLE_FIQ_NESTING
+ bl gicv3_acknowledge_group0
+ mov r4, r0
+ stmdb sp!, {r4, r5}
+
+ bl _tx_thread_fiq_nesting_start
+ mov r0, r4
+ bl board_fiq_service
+ bl _tx_thread_fiq_nesting_end
+
+ ldmia sp!, {r4, r5}
+ mov r0, r4
+ bl gicv3_end_of_interrupt_group0
+#else
+ bl board_fiq_handler
+#endif
+ b _tx_thread_fiq_context_restore
+#else
+ FAULT_REPORT msg_el1_fiq
+#endif
+
+fault_halt: b fault_halt
+
+ .section .rodata
+ .balign 4
+msg_el2:
+ .asciz "[EL2] reset entry reached; configuring EL2 and dropping to EL1\n"
+
+msg_el2_reset: .asciz "[FAULT] EL2 reset vector\n"
+msg_el2_undef: .asciz "[FAULT] EL2 undefined instruction\n"
+msg_el2_svc: .asciz "[FAULT] EL2 supervisor call from Hyp mode\n"
+msg_el2_pabt: .asciz "[FAULT] EL2 prefetch abort\n"
+msg_el2_dabt: .asciz "[FAULT] EL2 data abort\n"
+msg_el2_hyp: .asciz "[FAULT] EL2 hyp trap with unhandled HSR.EC\n"
+msg_el2_irq: .asciz "[FAULT] EL2 IRQ (unexpected: IRQs route to EL1)\n"
+msg_el2_fiq: .asciz "[FAULT] EL2 FIQ (unexpected: FIQs route to EL1)\n"
+
+msg_el1_reset: .asciz "[FAULT] EL1 reset vector\n"
+msg_el1_undef: .asciz "[FAULT] EL1 undefined instruction\n"
+msg_el1_svc: .asciz "[FAULT] EL1 supervisor call (no handler yet)\n"
+msg_el1_pabt: .asciz "[FAULT] EL1 prefetch abort\n"
+msg_el1_dabt: .asciz "[FAULT] EL1 data abort\n"
+msg_el1_reserved: .asciz "[FAULT] EL1 reserved vector\n"
+msg_el1_irq: .asciz "[FAULT] EL1 IRQ (no handler yet)\n"
+msg_el1_fiq: .asciz "[FAULT] EL1 FIQ (no handler yet)\n"
+
+ .section .data
+ .balign 4
+ .global _hvc_call_count
+_hvc_call_count:
+ .word 0
+
+/* Used by the MPU enforcement self-test (see el1_trap_dabt). */
+
+ .global mpu_expect_abort
+mpu_expect_abort:
+ .word 0
+
+ .global mpu_abort_count
+mpu_abort_count:
+ .word 0
+
+/* Used by the execute-never self-test (see el1_trap_pabt). */
+
+ .global mpu_expect_pabt
+mpu_expect_pabt:
+ .word 0
+
+ .global mpu_pabt_count
+mpu_pabt_count:
+ .word 0
+
+ .global mpu_abort_return
+mpu_abort_return:
+ .word 0
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.c
new file mode 100644
index 00000000..4771625d
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.c
@@ -0,0 +1,360 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* gicv3.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* GICv3 bring-up for Cortex-R52 on the Armv8-R AEM FVP. */
+/* */
+/* Model characteristics that shape this code, taken from its own */
+/* parameters rather than assumed: */
+/* */
+/* has-two-security-states=0 single security state, so GICD_CTLR.DS */
+/* reads 1 and the Group 1 enable is bit 1*/
+/* ARE-fixed-to-one=1 affinity routing cannot be turned off */
+/* priority-bits=5 only the top 5 priority bits exist, so */
+/* priorities must be multiples of 8 */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Directive 4.3 -- the CPU interface is only reachable through CP15 */
+/* system registers, so inline assembly is unavoidable. Every such */
+/* access is encapsulated in a one-line accessor below and appears */
+/* nowhere else. */
+/* Rule 11.4/11.6 -- casting integer addresses to volatile pointers is */
+/* inherent to memory-mapped device access; confined to REG32. */
+/* */
+/**************************************************************************/
+
+#include "platform.h"
+#include "gicv3.h"
+
+/* Distributor registers. */
+
+#define GICD_CTLR 0x0000U
+#define GICD_CTLR_ENABLE_GRP0 (1UL << 0)
+#define GICD_CTLR_ENABLE_GRP1 (1UL << 1)
+#define GICD_CTLR_ARE (1UL << 4)
+
+/* Redistributor RD frame. */
+
+#define GICR_WAKER 0x0014U
+#define GICR_WAKER_PROC_SLEEP (1UL << 1)
+#define GICR_WAKER_CHILD_ASLEEP (1UL << 2)
+
+/* Redistributor SGI frame (SGIs and PPIs, INTID 0-31). */
+
+#define GICR_IGROUPR0 0x0080U
+#define GICR_ISENABLER0 0x0100U
+#define GICR_IPRIORITYR 0x0400U
+#define GICR_ICFGR1 0x0C04U
+
+#define ICC_PMR_UNMASK_ALL 0xFFUL
+#define ICC_SRE_SRE (1UL << 0)
+#define ICC_IGRPEN1_ENABLE (1UL << 0)
+
+#define PPI_FIRST_INTID 16U
+
+
+/**************************************************************************/
+/* CPU interface accessors (AArch32 ICC_* system registers). */
+/**************************************************************************/
+
+static unsigned long read_icc_sre(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c12, c12, 5" : "=r"(value));
+ return value;
+}
+
+static void write_icc_sre(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 5" : : "r"(value) : "memory");
+}
+
+static void write_icc_pmr(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c4, c6, 0" : : "r"(value) : "memory");
+}
+
+static void write_icc_bpr1(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 3" : : "r"(value) : "memory");
+}
+
+static void write_icc_igrpen0(unsigned long value)
+{
+ /* ICC_IGRPEN0 sits at c12, c12, 6, beside IGRPEN1 at opc2 7. */
+
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 6" : : "r"(value) : "memory");
+}
+
+static void write_icc_bpr0(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c8, 3" : : "r"(value) : "memory");
+}
+
+static void write_icc_igrpen1(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 7" : : "r"(value) : "memory");
+}
+
+static void instruction_barrier(void)
+{
+ __asm__ volatile("isb" : : : "memory");
+}
+
+
+/**************************************************************************/
+/* gicv3_init */
+/**************************************************************************/
+
+void gicv3_init(void)
+{
+ unsigned long waker;
+
+ /* Distributor: affinity routing first, then the Group 1 enable. With a
+ single security state Group 1 is bit 1 of GICD_CTLR. */
+
+ REG32(GICD_BASE + GICD_CTLR) |= GICD_CTLR_ARE;
+ REG32(GICD_BASE + GICD_CTLR) |= GICD_CTLR_ENABLE_GRP1;
+
+ /* Group 0 as well, which is what the GIC delivers as FIQ. Enabling it costs
+ nothing while no interrupt is assigned to that group, and images that never
+ use FIQ see no change: assignment is per interrupt, in GICR_IGROUPR0. */
+
+ REG32(GICD_BASE + GICD_CTLR) |= GICD_CTLR_ENABLE_GRP0;
+
+ /* Redistributor: clear ProcessorSleep and wait for the redistributor to
+ report that its children are awake, otherwise no interrupt can be
+ delivered to this core. */
+
+ waker = REG32(GICR_RD_BASE + GICR_WAKER);
+ waker &= ~GICR_WAKER_PROC_SLEEP;
+ REG32(GICR_RD_BASE + GICR_WAKER) = waker;
+
+ while ((REG32(GICR_RD_BASE + GICR_WAKER) & GICR_WAKER_CHILD_ASLEEP) != 0UL)
+ {
+ /* Wait for the redistributor to wake. */
+ }
+
+ /* CPU interface: system-register access, then unmask all priorities and
+ enable Group 1. EL2 has already permitted EL1 system-register access
+ via ICC_HSRE in entry.S. */
+
+ write_icc_sre(read_icc_sre() | ICC_SRE_SRE);
+ instruction_barrier();
+
+ write_icc_pmr(ICC_PMR_UNMASK_ALL);
+ write_icc_bpr1(0UL);
+ write_icc_igrpen1(ICC_IGRPEN1_ENABLE);
+
+ /* The CPU interface half of Group 0: a Group 0 interrupt reaches the core
+ only if both the distributor and this are enabled. BPR0 at zero gives no
+ subpriority grouping, so priority alone decides preemption, which is what
+ the FIQ nesting test depends on. */
+
+ write_icc_bpr0(0UL);
+ write_icc_igrpen0(ICC_IGRPEN1_ENABLE);
+ instruction_barrier();
+}
+
+
+/**************************************************************************/
+/* gicv3_enable_ppi */
+/**************************************************************************/
+
+void gicv3_enable_ppi(unsigned int intid, unsigned int priority)
+{
+ unsigned long shift;
+
+ if ((intid < PPI_FIRST_INTID) || (intid > 31U))
+ {
+ return;
+ }
+
+ /* Group 1. */
+
+ REG32(GICR_SGI_BASE + GICR_IGROUPR0) |= (1UL << intid);
+
+ /* Priority is a byte per INTID. Only the top 5 bits are implemented on
+ this model, so callers pass multiples of 8. */
+
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) &=
+ ~(0xFFUL << ((intid & 3U) * 8U));
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) |=
+ ((unsigned long) priority & 0xFFUL) << ((intid & 3U) * 8U);
+
+ /* Level-sensitive: two configuration bits per INTID, 00 = level. The
+ generic timer asserts a level, not an edge. */
+
+ shift = ((unsigned long) intid - PPI_FIRST_INTID) * 2UL;
+ REG32(GICR_SGI_BASE + GICR_ICFGR1) &= ~(3UL << shift);
+
+ /* Enable. */
+
+ REG32(GICR_SGI_BASE + GICR_ISENABLER0) = (1UL << intid);
+}
+
+
+/**************************************************************************/
+/* gicv3_acknowledge */
+/**************************************************************************/
+
+/**************************************************************************/
+/* gicv3_enable_sgi */
+/* */
+/* An SGI lives in the same redistributor frame as a PPI, so this is */
+/* gicv3_enable_ppi without the ICFGR step: INTIDs 0-15 have no */
+/* configurable edge/level, they are always edge-triggered. */
+/**************************************************************************/
+
+void gicv3_enable_sgi(unsigned int intid, unsigned int priority)
+{
+ if (intid > 15U)
+ {
+ return;
+ }
+
+ REG32(GICR_SGI_BASE + GICR_IGROUPR0) |= (1UL << intid);
+
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) &=
+ ~(0xFFUL << ((intid & 3U) * 8U));
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) |=
+ ((unsigned long) priority & 0xFFUL) << ((intid & 3U) * 8U);
+
+ REG32(GICR_SGI_BASE + GICR_ISENABLER0) = (1UL << intid);
+}
+
+
+/**************************************************************************/
+/* gicv3_send_sgi */
+/* */
+/* ICC_SGI1R is 64-bit, so in AArch32 it is an MCRR rather than an MCR. */
+/* Fields: INTID in [27:24], TargetList in [15:0], Aff1/2/3 and IRM zero */
+/* for this single-core configuration, so TargetList = 1 selects core 0. */
+/* */
+/* The AArch64 name for this register is S3_0_C12_C11_5, which the */
+/* Cortex-A72 example uses, but that encoding does not transcribe to the */
+/* AArch32 64-bit CP15 space. The CRm here was confirmed by observing */
+/* that the SGI is actually delivered and acknowledged with the expected */
+/* INTID rather than by reading it off the A-profile alias. */
+/**************************************************************************/
+
+void gicv3_send_sgi(unsigned int intid)
+{
+ unsigned long low = (((unsigned long) intid & 0xFUL) << 24) | 1UL;
+ unsigned long high = 0UL;
+
+ __asm volatile ("mcrr p15, 0, %0, %1, c12" :: "r" (low), "r" (high) : "memory");
+ instruction_barrier();
+}
+
+
+/**************************************************************************/
+/* gicv3_enable_sgi_group0 */
+/* */
+/* As gicv3_enable_sgi, but leaves the interrupt in Group 0 so it arrives */
+/* as an FIQ. The group is chosen by clearing the GICR_IGROUPR0 bit; the */
+/* Group 1 version sets it. */
+/**************************************************************************/
+
+void gicv3_enable_sgi_group0(unsigned int intid, unsigned int priority)
+{
+ if (intid > 15U)
+ {
+ return;
+ }
+
+ REG32(GICR_SGI_BASE + GICR_IGROUPR0) &= ~(1UL << intid);
+
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) &=
+ ~(0xFFUL << ((intid & 3U) * 8U));
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) |=
+ ((unsigned long) priority & 0xFFUL) << ((intid & 3U) * 8U);
+
+ REG32(GICR_SGI_BASE + GICR_ISENABLER0) = (1UL << intid);
+}
+
+
+/**************************************************************************/
+/* gicv3_send_sgi_group0 */
+/* */
+/* ICC_SGI0R rather than ICC_SGI1R: the two differ only in opc1, 2 against */
+/* 0, and each raises the SGI into its own group. Raising a Group 0 */
+/* interrupt through the Group 1 register does not deliver it as an FIQ. */
+/**************************************************************************/
+
+void gicv3_send_sgi_group0(unsigned int intid)
+{
+ unsigned long low = (((unsigned long) intid & 0xFUL) << 24) | 1UL;
+ unsigned long high = 0UL;
+
+ __asm volatile ("mcrr p15, 2, %0, %1, c12" :: "r" (low), "r" (high) : "memory");
+ instruction_barrier();
+}
+
+
+/**************************************************************************/
+/* gicv3_acknowledge_group0 / gicv3_end_of_interrupt_group0 */
+/* */
+/* Group 0 has its own pair at c12, c8, where Group 1 uses c12, c12. */
+/* Acknowledging a Group 0 interrupt through IAR1 returns the spurious */
+/* INTID and leaves the interrupt pending, which presents as an FIQ storm. */
+/**************************************************************************/
+
+unsigned long gicv3_acknowledge_group0(void)
+{
+ unsigned long intid;
+
+ __asm__ volatile("mrc p15, 0, %0, c12, c8, 0" : "=r"(intid));
+ return intid & 0xFFFFFFUL;
+}
+
+
+void gicv3_end_of_interrupt_group0(unsigned long intid)
+{
+ instruction_barrier();
+ __asm__ volatile("mcr p15, 0, %0, c12, c8, 1" : : "r"(intid) : "memory");
+}
+
+
+unsigned long gicv3_acknowledge(void)
+{
+ unsigned long intid;
+
+ __asm__ volatile("mrc p15, 0, %0, c12, c12, 0" : "=r"(intid));
+
+ return intid;
+}
+
+
+/**************************************************************************/
+/* gicv3_end_of_interrupt */
+/**************************************************************************/
+
+void gicv3_end_of_interrupt(unsigned long intid)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 1" : : "r"(intid) : "memory");
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.h
new file mode 100644
index 00000000..1a998019
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.h
@@ -0,0 +1,81 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* gicv3.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* GICv3 interrupt controller for Cortex-R52. The CPU interface is */
+/* reached through AArch32 ICC_* system registers; the Distributor and */
+/* Redistributor are memory mapped. */
+/* */
+/**************************************************************************/
+
+#ifndef GICV3_H
+#define GICV3_H
+
+/* Spurious interrupt ID returned by the CPU interface when no interrupt is
+ pending. It must be neither dispatched nor acknowledged with EOI. */
+
+#define GICV3_SPURIOUS_INTID 1023UL
+
+/* Bring up the Distributor, this core's Redistributor and the CPU
+ interface. Interrupts remain masked at the PSTATE level. */
+
+void gicv3_init(void);
+
+/* Enable one private peripheral interrupt (PPI, INTID 16-31) as a
+ level-sensitive Group 1 interrupt at the given priority. */
+
+void gicv3_enable_ppi(unsigned int intid, unsigned int priority);
+
+/* Enable a software-generated interrupt, INTID 0-15. Same registers as a PPI:
+ the redistributor's SGI/PPI frame covers INTIDs 0-31. */
+
+void gicv3_enable_sgi(unsigned int intid, unsigned int priority);
+
+/* Raise an SGI on this core. */
+
+void gicv3_send_sgi(unsigned int intid);
+
+/* Group 0 support, which is what produces FIQ rather than IRQ. With a single
+ security state the GIC routes Group 0 to FIQ and Group 1 to IRQ, so an
+ interrupt has to be moved into Group 0 to arrive as an FIQ at all. Group 0
+ has its own acknowledge and end-of-interrupt registers; using the Group 1 ones
+ on a Group 0 interrupt does not work. */
+
+void gicv3_enable_sgi_group0(unsigned int intid, unsigned int priority);
+void gicv3_send_sgi_group0(unsigned int intid);
+unsigned long gicv3_acknowledge_group0(void);
+void gicv3_end_of_interrupt_group0(unsigned long intid);
+
+/* Acknowledge the highest-priority pending Group 1 interrupt, returning its
+ INTID (possibly GICV3_SPURIOUS_INTID). */
+
+unsigned long gicv3_acknowledge(void);
+
+/* Signal completion of the interrupt previously acknowledged. */
+
+void gicv3_end_of_interrupt(unsigned long intid);
+
+#endif /* GICV3_H */
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/irq_dispatch.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/irq_dispatch.c
new file mode 100644
index 00000000..f244c3d7
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/irq_dispatch.c
@@ -0,0 +1,306 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* irq_dispatch.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Interrupt dispatch for Cortex-R52 on the Armv8-R AEM FVP. Called */
+/* from __tx_irq_processing_return in entry.S, that is after */
+/* _tx_thread_context_save has saved the interrupted context and with */
+/* interrupts still disabled. */
+/* */
+/* _tx_timer_interrupt is safe to call from C: it uses only r0-r3 and */
+/* returns through lr like an ordinary function. */
+/* */
+/**************************************************************************/
+
+#include "board.h"
+#include "gicv3.h"
+#include "timer.h"
+
+/* ThreadX periodic timer entry point (assembly, AAPCS-compatible). */
+
+extern void _tx_timer_interrupt(void);
+
+/* Observability for the M3 checks. board_timer_intid records the INTID that
+ actually arrived -- this is how the timer PPI was identified during bring-up
+ (see TIMER_PPI_INTID) and it keeps reporting it, so a platform that assigns
+ the timer elsewhere shows up as an unexpected INTID rather than a silent
+ absence of ticks. */
+
+volatile unsigned long board_irq_count;
+volatile unsigned long board_timer_intid;
+volatile unsigned long board_spurious_count;
+volatile unsigned long board_unexpected_intid;
+
+volatile unsigned long board_nest_depth;
+volatile unsigned long board_nest_max;
+volatile unsigned long board_sgi_count;
+volatile unsigned long board_sgi_nested_count;
+volatile unsigned long board_nest_provoke;
+
+volatile unsigned long board_fiq_count;
+volatile unsigned long board_fiq_high_count;
+volatile unsigned long board_fiq_nested_count;
+volatile unsigned long board_fiq_depth;
+volatile unsigned long board_fiq_max_depth;
+volatile unsigned long board_fiq_unexpected;
+volatile unsigned long board_fiq_provoke;
+
+
+/* Timer PPI priority. Only the top 5 priority bits are implemented on this
+ model, so the value is a multiple of 8. */
+
+#define TIMER_PPI_PRIORITY 0xA0U
+
+
+/**************************************************************************/
+/* board_init */
+/* */
+/* Called from _tx_initialize_low_level. Interrupts are still masked at */
+/* this point, so the tick cannot be delivered before the kernel is */
+/* ready for it. */
+/**************************************************************************/
+
+void board_init(void)
+{
+ gicv3_init();
+ gicv3_enable_ppi(TIMER_PPI_INTID, TIMER_PPI_PRIORITY);
+
+ /* The nesting SGI must outrank the timer or it could never preempt it:
+ in the GIC a numerically lower priority value wins. Enabling it costs
+ nothing in images that never raise it. */
+
+ gicv3_enable_sgi(BOARD_NEST_SGI_INTID, TIMER_PPI_PRIORITY / 2U);
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+
+ /* Two Group 0 SGIs for the FIQ path. Group 0 is what the GIC delivers as
+ FIQ; the high one is given a numerically lower priority so it can preempt
+ the low one, which is what FIQ nesting means. Both sit above the timer so
+ the two paths do not interfere. */
+
+ gicv3_enable_sgi_group0(BOARD_FIQ_LOW_INTID, 0x40U);
+ gicv3_enable_sgi_group0(BOARD_FIQ_HIGH_INTID, 0x20U);
+#endif
+
+ timer_init();
+}
+
+
+/**************************************************************************/
+/* board_irq_handler */
+/**************************************************************************/
+
+/**************************************************************************/
+/* board_irq_service -- service an already-acknowledged INTID. */
+/* */
+/* Split out so the nesting path in entry.S can acknowledge in IRQ mode */
+/* before nesting starts. Does not acknowledge and does not EOI. */
+/**************************************************************************/
+
+void board_irq_service(unsigned long intid)
+{
+ if (intid == GICV3_SPURIOUS_INTID)
+ {
+ /* No interrupt was actually pending; must not be acknowledged. */
+
+ board_spurious_count++;
+ return;
+ }
+
+ /* Depth is incremented after the spurious check so a spurious entry does
+ not register as nesting. It reaches 2 only if a second interrupt is
+ taken while this handler is still on the stack, which cannot happen
+ without TX_ENABLE_IRQ_NESTING. */
+
+ board_nest_depth++;
+ if (board_nest_depth > board_nest_max)
+ {
+ board_nest_max = board_nest_depth;
+ }
+
+ if (intid == (unsigned long) TIMER_PPI_INTID)
+ {
+ board_timer_intid = intid;
+ board_irq_count++;
+
+ /* Re-arm before servicing so the next interval starts immediately;
+ this also deasserts the level-sensitive timer output. */
+
+ timer_reload();
+
+ /* Provoke the nesting, if this image asked for it. Raising the SGI
+ here is only useful because _tx_thread_irq_nesting_start has already
+ re-enabled IRQ; the bounded spin gives the GIC the chance to deliver
+ it before this handler returns, so the nesting is deterministic
+ rather than a race we hope wins. The bound matters: without
+ TX_ENABLE_IRQ_NESTING the SGI can never arrive and this must not
+ become a hang. */
+
+ if (board_nest_provoke != 0UL)
+ {
+ unsigned long seen = board_sgi_count;
+ unsigned long guard;
+
+ gicv3_send_sgi(BOARD_NEST_SGI_INTID);
+
+ for (guard = 0UL; guard < 100000UL; guard++)
+ {
+ if (board_sgi_count != seen)
+ {
+ break;
+ }
+ }
+ }
+
+ _tx_timer_interrupt();
+ }
+ else if (intid == (unsigned long) BOARD_NEST_SGI_INTID)
+ {
+ board_sgi_count++;
+
+ /* Depth above 1 means this SGI arrived inside another handler, which
+ is the whole point of the test. */
+
+ if (board_nest_depth > 1UL)
+ {
+ board_sgi_nested_count++;
+ }
+ }
+ else
+ {
+ board_unexpected_intid = intid;
+ }
+
+ board_nest_depth--;
+}
+
+
+/**************************************************************************/
+/* board_irq_handler -- the non-nesting entry point. */
+/* */
+/* Acknowledges, services and ends the interrupt, all in IRQ mode with */
+/* interrupts masked. entry.S calls this when the image was built without */
+/* TX_ENABLE_IRQ_NESTING, so the behaviour of every existing image is */
+/* exactly what it was. */
+/**************************************************************************/
+
+void board_irq_handler(void)
+{
+ unsigned long intid = gicv3_acknowledge();
+
+ board_irq_service(intid);
+
+ if (intid != GICV3_SPURIOUS_INTID)
+ {
+ gicv3_end_of_interrupt(intid);
+ }
+}
+
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+/**************************************************************************/
+/* board_fiq_service -- service an already-acknowledged Group 0 INTID. */
+/* */
+/* The FIQ counterpart of board_irq_service, and split for the same */
+/* reason: the acknowledge has to happen before nesting starts. Does not */
+/* acknowledge and does not EOI. */
+/**************************************************************************/
+
+void board_fiq_service(unsigned long intid)
+{
+ if (intid == GICV3_SPURIOUS_INTID)
+ {
+ return;
+ }
+
+ board_fiq_depth++;
+ if (board_fiq_depth > board_fiq_max_depth)
+ {
+ board_fiq_max_depth = board_fiq_depth;
+ }
+
+ if (intid == (unsigned long) BOARD_FIQ_LOW_INTID)
+ {
+ board_fiq_count++;
+
+ /* Provoke the nested FIQ from inside this handler. Only reachable
+ because _tx_thread_fiq_nesting_start has re-enabled FIQ; the bounded
+ spin lets the GIC deliver the higher-priority Group 0 SGI before this
+ handler returns. Bounded, because with nesting compiled out the
+ second FIQ cannot arrive and this must not hang. */
+
+ if (board_fiq_provoke != 0UL)
+ {
+ unsigned long seen = board_fiq_high_count;
+ unsigned long guard;
+
+ gicv3_send_sgi_group0(BOARD_FIQ_HIGH_INTID);
+
+ for (guard = 0UL; guard < 100000UL; guard++)
+ {
+ if (board_fiq_high_count != seen)
+ {
+ break;
+ }
+ }
+ }
+ }
+ else if (intid == (unsigned long) BOARD_FIQ_HIGH_INTID)
+ {
+ board_fiq_high_count++;
+
+ /* Depth above 1 means this FIQ arrived inside another FIQ handler. */
+
+ if (board_fiq_depth > 1UL)
+ {
+ board_fiq_nested_count++;
+ }
+ }
+ else
+ {
+ board_fiq_unexpected = intid;
+ }
+
+ board_fiq_depth--;
+}
+
+
+/**************************************************************************/
+/* board_fiq_handler -- the non-nesting FIQ entry point. */
+/**************************************************************************/
+
+void board_fiq_handler(void)
+{
+ unsigned long intid = gicv3_acknowledge_group0();
+
+ board_fiq_service(intid);
+
+ if (intid != GICV3_SPURIOUS_INTID)
+ {
+ gicv3_end_of_interrupt_group0(intid);
+ }
+}
+#endif
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link.lds b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link.lds
new file mode 100644
index 00000000..b7aac5c8
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link.lds
@@ -0,0 +1,141 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/* Linker script for Cortex-R52 on the Armv8-R AEM FVP (BaseR platform).
+ *
+ * The BaseR memory map is the Base platform map with its two 2 GB halves
+ * swapped: Base DRAM at 0x80000000 appears at 0x00000000 here, and the
+ * 0x80000000-0xFFFFFFFF half holds peripherals and is NOT executable by
+ * default. Code must therefore be linked into low DRAM -- linking at
+ * 0x80000000 produces a silent fault loop with no output.
+ */
+
+ENTRY(_start)
+
+__hyp_stack_size__ = 0x0800;
+__svc_stack_size__ = 0x1000;
+__irq_stack_size__ = 0x0800;
+__fiq_stack_size__ = 0x0400;
+__abt_stack_size__ = 0x0400;
+__und_stack_size__ = 0x0400;
+__sys_stack_size__ = 0x0800;
+
+MEMORY
+{
+ DRAM (rwx) : ORIGIN = 0x00000000, LENGTH = 0x08000000 /* 128 MB */
+}
+
+SECTIONS
+{
+ /* PMSAv8-R regions have a 64-byte granule and their limits are inclusive,
+ so the code and data areas are separated on a 64-byte boundary. Without
+ this a single region would have to span both, forcing writable code or
+ executable data. */
+
+ . = ALIGN(64);
+ __code_start__ = .;
+
+ .text :
+ {
+ KEEP(*(.vectors_el2))
+ KEEP(*(.vectors_el1))
+ *(.text*)
+ *(.glue_7)
+ *(.glue_7t)
+ } > DRAM
+
+ .rodata :
+ {
+ . = ALIGN(4);
+ *(.rodata*)
+ . = ALIGN(4);
+ } > DRAM
+
+ /* Start the writable area on a protection-region granule.
+ *
+ * The alignment belongs on the output section itself. Two things that
+ * look equivalent do not work: an assignment such as ". = ALIGN(64);"
+ * between sections does not advance a MEMORY region's allocation
+ * pointer, and ". = ALIGN(64);" at the end of .rodata does not extend
+ * that section when no data follows it. In both cases .data keeps its
+ * unaligned address while __data_start__ reports the aligned one, so the
+ * first bytes of .data fall inside the read-only code region. The MPU
+ * caught precisely that: writes to the two variables at the start of
+ * .data faulted, and since one of them is the flag the abort handler
+ * consults, the fault turned itself fatal.
+ */
+
+ .data ALIGN(64) :
+ {
+ __code_end__ = .; /* code region ends where writable data begins */
+ __data_start__ = .;
+ *(.data*)
+ . = ALIGN(4);
+ } > DRAM
+
+ .bss (NOLOAD) :
+ {
+ . = ALIGN(4);
+ __bss_start__ = .;
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ __bss_end__ = .;
+ } > DRAM
+
+ /* One stack per AArch32 processor mode. SP must stay 8-byte aligned
+ (AAPCS), so each region is aligned before its top symbol is taken. */
+
+ .stacks (NOLOAD) :
+ {
+ . = ALIGN(8);
+ . = . + __hyp_stack_size__;
+ __hyp_stack_top = .;
+
+ . = ALIGN(8);
+ . = . + __svc_stack_size__;
+ __svc_stack_top = .;
+
+ . = ALIGN(8);
+ . = . + __irq_stack_size__;
+ __irq_stack_top = .;
+
+ . = ALIGN(8);
+ . = . + __fiq_stack_size__;
+ __fiq_stack_top = .;
+
+ . = ALIGN(8);
+ . = . + __abt_stack_size__;
+ __abt_stack_top = .;
+
+ . = ALIGN(8);
+ . = . + __und_stack_size__;
+ __und_stack_top = .;
+
+ . = ALIGN(8);
+ . = . + __sys_stack_size__;
+ __sys_stack_top = .;
+ } > DRAM
+
+ . = ALIGN(8);
+ _end = .;
+ PROVIDE(end = .);
+
+ /* Writable area for the MPU: data, bss, stacks and the heap that
+ tx_application_define carves out of everything above _end. */
+
+ . = ALIGN(64);
+ __data_end__ = . + 0x00100000; /* 1 MB of heap beyond the image */
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c
new file mode 100644
index 00000000..4fad2f7e
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c
@@ -0,0 +1,456 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* mpu.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* PMSAv8-R memory protection and cache enable for Cortex-R52. */
+/* */
+/* Register model (AArch32): a region is selected through PRSELR and */
+/* then described by PRBAR (base, shareability, access permission, */
+/* execute-never) and PRLAR (inclusive limit, attribute index, enable).*/
+/* Both addresses have a 64-byte granule, so the low six bits of each */
+/* register hold attributes rather than address. Memory types come */
+/* from MAIR through the attribute index, not from the region itself. */
+/* */
+/* Caches are invalidated before being enabled. On this model they */
+/* come out of reset invalid, but silicon does not guarantee that, and */
+/* this code is meant to be the template the S32Z280 port starts from. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Directive 4.3 -- the MPU, MAIR, SCTLR and cache maintenance are only*/
+/* reachable through CP15; every access is encapsulated in a one-line*/
+/* accessor below. */
+/* */
+/**************************************************************************/
+
+#include "platform.h"
+#include "mpu.h"
+
+/* SCTLR bits. */
+
+#define SCTLR_M (1UL << 0) /* MPU enable */
+#define SCTLR_C (1UL << 2) /* data cache enable */
+#define SCTLR_I (1UL << 12) /* instruction cache enable */
+
+/* MAIR attribute bytes. Index 0 is Normal write-back read/write-allocate,
+ index 1 is Device-nGnRnE, matching MPU_ATTR_* in the header. */
+
+#define MAIR_ATTR_NORMAL_WB 0xFFUL
+#define MAIR_ATTR_DEVICE 0x00UL
+
+/* Boundaries supplied by the linker script, all 64-byte aligned. */
+
+extern char __code_start__;
+extern char __code_end__;
+extern char __data_start__;
+extern char __data_end__;
+
+
+/**************************************************************************/
+/* The region table. */
+/* */
+/* Deliberately minimal and readable: code is read-only and executable, */
+/* everything writable is non-executable, and the peripheral half of the */
+/* address map is Device. A write-protected code region is the point -- */
+/* it is what makes the MPU do something observable, and AR2 extends the */
+/* same table with per-module regions. */
+/**************************************************************************/
+
+static MPU_REGION mpu_regions[3];
+static unsigned int mpu_regions_used;
+
+
+/**************************************************************************/
+/* CP15 accessors. */
+/**************************************************************************/
+
+static unsigned long read_mpuir(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c0, c0, 4" : "=r"(value));
+ return value;
+}
+
+static unsigned long read_sctlr(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c1, c0, 0" : "=r"(value));
+ return value;
+}
+
+static void write_sctlr(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c1, c0, 0" : : "r"(value) : "memory");
+}
+
+static void write_prselr(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c2, 1" : : "r"(value) : "memory");
+}
+
+static void write_prbar(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c3, 0" : : "r"(value) : "memory");
+}
+
+static void write_prlar(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c3, 1" : : "r"(value) : "memory");
+}
+
+static unsigned long read_prbar(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c6, c3, 0" : "=r"(value));
+ return value;
+}
+
+static unsigned long read_prlar(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c6, c3, 1" : "=r"(value));
+ return value;
+}
+
+static void write_mair0(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c10, c2, 0" : : "r"(value) : "memory");
+}
+
+static void write_mair1(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c10, c2, 1" : : "r"(value) : "memory");
+}
+
+static void invalidate_icache_all(void)
+{
+ __asm__ volatile("mcr p15, 0, r0, c7, c5, 0" : : : "memory"); /* ICIALLU */
+}
+
+static void data_sync_barrier(void)
+{
+ __asm__ volatile("dsb" : : : "memory");
+}
+
+static void instruction_barrier(void)
+{
+ __asm__ volatile("isb" : : : "memory");
+}
+
+
+/**************************************************************************/
+/* mpu_region_count */
+/**************************************************************************/
+
+unsigned int mpu_region_count(void)
+{
+ /* MPUIR.REGION occupies bits [15:8]. */
+
+ return (unsigned int) ((read_mpuir() >> 8) & 0xFFUL);
+}
+
+
+/**************************************************************************/
+/* mpu_is_enabled / mpu_caches_enabled */
+/**************************************************************************/
+
+unsigned int mpu_is_enabled(void)
+{
+ return ((read_sctlr() & SCTLR_M) != 0UL) ? 1U : 0U;
+}
+
+unsigned int mpu_caches_enabled(void)
+{
+ unsigned long sctlr = read_sctlr();
+
+ return (((sctlr & SCTLR_C) != 0UL) && ((sctlr & SCTLR_I) != 0UL)) ? 1U : 0U;
+}
+
+
+/**************************************************************************/
+/* mpu_region_table */
+/**************************************************************************/
+
+const MPU_REGION *mpu_region_table(unsigned int *count_ptr)
+{
+ if (count_ptr != 0)
+ {
+ *count_ptr = mpu_regions_used;
+ }
+
+ return mpu_regions;
+}
+
+
+/**************************************************************************/
+/* mpu_read_region */
+/**************************************************************************/
+
+void mpu_read_region(unsigned int index, unsigned long *prbar_ptr,
+ unsigned long *prlar_ptr)
+{
+ write_prselr((unsigned long) index);
+ instruction_barrier();
+
+ if (prbar_ptr != 0)
+ {
+ *prbar_ptr = read_prbar();
+ }
+ if (prlar_ptr != 0)
+ {
+ *prlar_ptr = read_prlar();
+ }
+}
+
+
+/**************************************************************************/
+/* build_table -- describe this image's memory. */
+/**************************************************************************/
+
+static void build_table(void)
+{
+ /* Code: read-only and executable. */
+
+ mpu_regions[0].mpu_region_base = (unsigned long) &__code_start__;
+ mpu_regions[0].mpu_region_limit = (unsigned long) &__code_end__ - 1UL;
+ mpu_regions[0].mpu_region_ap = MPU_AP_RO_EL1;
+ mpu_regions[0].mpu_region_execute_never = 0U;
+ mpu_regions[0].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[0].mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
+ mpu_regions[0].mpu_region_name = "code RO X normal-wb";
+
+ /* Data, bss, stacks and heap: writable, never executable. */
+
+ mpu_regions[1].mpu_region_base = (unsigned long) &__data_start__;
+ mpu_regions[1].mpu_region_limit = (unsigned long) &__data_end__ - 1UL;
+ mpu_regions[1].mpu_region_ap = MPU_AP_RW_EL1;
+ mpu_regions[1].mpu_region_execute_never = 1U;
+ mpu_regions[1].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[1].mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
+ mpu_regions[1].mpu_region_name = "data RW NX normal-wb";
+
+ /* Peripherals occupy the upper half of the BaseR map. */
+
+ mpu_regions[2].mpu_region_base = 0x80000000UL;
+ mpu_regions[2].mpu_region_limit = 0xFFFFFFFFUL;
+ mpu_regions[2].mpu_region_ap = MPU_AP_RW_EL1;
+ mpu_regions[2].mpu_region_execute_never = 1U;
+ mpu_regions[2].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[2].mpu_region_attr_index = MPU_ATTR_DEVICE;
+ mpu_regions[2].mpu_region_name = "dev RW NX device";
+
+ mpu_regions_used = 3U;
+}
+
+
+/**************************************************************************/
+/* program_region */
+/**************************************************************************/
+
+static void program_region(unsigned int index, const MPU_REGION *region_ptr)
+{
+ unsigned long prbar;
+ unsigned long prlar;
+
+ /* PRBAR: BASE[31:6], RES0[5], SH[4:3], AP[2:1], XN[0].
+ Cortex-R52 TRM r1p3 figure 3-39 / table 3-80. */
+
+ prbar = (region_ptr->mpu_region_base & 0xFFFFFFC0UL)
+ | (((unsigned long) region_ptr->mpu_region_shareability & 0x3UL) << 3)
+ | (((unsigned long) region_ptr->mpu_region_ap & 0x3UL) << 1)
+ | ((unsigned long) region_ptr->mpu_region_execute_never & 0x1UL);
+
+ prlar = (region_ptr->mpu_region_limit & 0xFFFFFFC0UL)
+ | (((unsigned long) region_ptr->mpu_region_attr_index & 0x7UL) << 1)
+ | 1UL; /* region enable */
+
+ write_prselr((unsigned long) index);
+ instruction_barrier();
+ write_prbar(prbar);
+ write_prlar(prlar);
+}
+
+
+#ifdef TXM_MODULE_MANAGER
+
+/**************************************************************************/
+/* mpu_module_window_init */
+/* */
+/* A window over the module area, for the manager to load through. */
+/* */
+/* No region in the table above covers the module area, which is what */
+/* stops every thread from reaching a module's memory -- but the manager */
+/* has to read the preamble and write the module's data to load it at */
+/* all. Without this the load faults on its first read of the image. */
+/* */
+/* Not opened and closed around the load. It is left enabled here and */
+/* the SCHEDULER owns it from then on: it turns this region on for every */
+/* thread that owns no module and off for every thread that does, which */
+/* is what keeps it and a module's own regions -- which cover the same */
+/* memory -- from ever being enabled together. PMSAv8-R has no region */
+/* priority, so an access hitting more than one enabled region takes a */
+/* translation fault (TRM 8.1), and mutual exclusion by ownership is the */
+/* only form of it that does not depend on remembering to bracket a call. */
+/* */
+/* EL1 read/write with no EL0 access, and execute-never: the manager can */
+/* load through it, and a module cannot use it to reach anything. */
+/**************************************************************************/
+
+unsigned long mpu_module_window_prbar;
+unsigned long mpu_module_window_prlar;
+
+
+unsigned int mpu_module_window_init(void)
+{
+ MPU_REGION window;
+
+ /* Asked of the hardware rather than assumed. See MPU_MODULE_REGIONS_REQUIRED
+ in mpu.h: this model reports a region count no real Cortex-R52 can have,
+ so the number is checked here and the shortfall is a refusal. */
+
+ if (mpu_region_count() < MPU_MODULE_REGIONS_REQUIRED)
+ {
+ return 0U;
+ }
+
+ window.mpu_region_base = FVP_MODULE_AREA_BASE;
+ window.mpu_region_limit = FVP_MODULE_AREA_BASE
+ + FVP_MODULE_AREA_SIZE - 1UL;
+ window.mpu_region_ap = MPU_AP_RW_EL1;
+ window.mpu_region_execute_never = 1U;
+ window.mpu_region_shareability = MPU_SH_NON;
+ window.mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
+ window.mpu_region_name = "module window RW NX EL1";
+
+ /* Published for the scheduler, which turns this region on and off on every
+ dispatch and has no business computing register layouts in assembly. */
+
+ mpu_module_window_prbar = (window.mpu_region_base & 0xFFFFFFC0UL)
+ | (((unsigned long) window.mpu_region_shareability & 0x3UL) << 3)
+ | (((unsigned long) window.mpu_region_ap & 0x3UL) << 1)
+ | ((unsigned long) window.mpu_region_execute_never & 0x1UL);
+
+ mpu_module_window_prlar = (window.mpu_region_limit & 0xFFFFFFC0UL)
+ | (((unsigned long) window.mpu_region_attr_index & 0x7UL) << 1)
+ | 1UL;
+
+ program_region(MPU_MODULE_LOAD_REGION, &window);
+
+ data_sync_barrier();
+ instruction_barrier();
+
+ return 1U;
+}
+
+#endif /* TXM_MODULE_MANAGER */
+
+
+/**************************************************************************/
+/* mpu_init */
+/**************************************************************************/
+
+unsigned int mpu_init(void)
+{
+ unsigned long sctlr;
+ unsigned int index;
+ unsigned int available = mpu_region_count();
+
+ build_table();
+
+ if (available < mpu_regions_used)
+ {
+ return 0U;
+ }
+
+#ifdef TXM_MODULE_MANAGER
+
+ /* A module manager build needs regions the table above does not describe:
+ the eight handed to a module and the window over the module area. If the
+ implementation cannot supply them, stop here rather than enable a
+ protection map that is missing the half a module depends on. */
+
+ if (available < MPU_MODULE_REGIONS_REQUIRED)
+ {
+ return 0U;
+ }
+
+#endif
+
+ /* Memory types first: a region's attribute index is meaningless until
+ MAIR is populated. */
+
+ write_mair0((MAIR_ATTR_DEVICE << 8) | MAIR_ATTR_NORMAL_WB);
+ write_mair1(0UL);
+
+ for (index = 0U; index < mpu_regions_used; index++)
+ {
+ program_region(index, &mpu_regions[index]);
+ }
+
+ /* Disable any regions this image does not use, so that stale enables
+ cannot grant access we did not intend. */
+
+ for (index = mpu_regions_used; index < available; index++)
+ {
+ write_prselr((unsigned long) index);
+ instruction_barrier();
+ write_prlar(0UL);
+ }
+
+#ifdef TXM_MODULE_MANAGER
+
+ /* After the loop above, which would otherwise disable it again: the module
+ window lives above the regions this table uses, so it counts as unused
+ here. The region count was checked at the top of this function, so the
+ call cannot fail at this point. */
+
+ if (mpu_module_window_init() == 0U)
+ {
+ return 0U;
+ }
+
+#endif
+
+ data_sync_barrier();
+
+ /* Caches are invalidated before enabling. The instruction cache has a
+ single invalidate-all; the data cache is left to the model's reset
+ state, which is invalid, because a set/way sweep belongs with the
+ silicon bring-up where it can be verified against the real cache
+ geometry. */
+
+ invalidate_icache_all();
+ data_sync_barrier();
+ instruction_barrier();
+
+ sctlr = read_sctlr();
+ sctlr |= SCTLR_M | SCTLR_C | SCTLR_I;
+ write_sctlr(sctlr);
+ data_sync_barrier();
+ instruction_barrier();
+
+ return mpu_regions_used;
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h
new file mode 100644
index 00000000..d52a03a6
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h
@@ -0,0 +1,169 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* mpu.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* PMSAv8-R memory protection and cache enable for Cortex-R52. */
+/* */
+/* The region set is described by a table rather than a sequence of */
+/* register writes. That is deliberate and is a roadmap decision, not */
+/* a style preference: AR2 programs a Stage-1 region set per ThreadX */
+/* module and AR3 programs Stage-2 regions per ZoneX partition, and */
+/* both reuse this shape. A table can be swapped at a partition */
+/* switch; open-coded register writes cannot. */
+/* */
+/**************************************************************************/
+
+#ifndef MPU_H
+#define MPU_H
+
+/* Access permissions, PRBAR.AP (bits [2:1]).
+ *
+ * AP[2] selects read-only, AP[1] grants EL0 access -- the standard Armv8-R
+ * encoding, as published. An earlier version of this file claimed the two
+ * bits were reversed, on the strength of a calibration that programmed four
+ * regions and attempted a privileged write to each:
+ *
+ * AP=0b00 write allowed AP=0b01 write faulted
+ * AP=0b10 write allowed AP=0b11 write faulted
+ *
+ * That table is real, but the cause was not the AP ordering. program_region()
+ * shifted every PRBAR field one bit too far left, so the AP value's low bit
+ * landed in the real AP[2] -- the read-only bit -- making writes fault exactly
+ * when that bit was set. The "high bit grants EL0 access" half was never
+ * tested; under the old shift that bit landed in SH[0], programming a
+ * shareability the TRM defines as UNPREDICTABLE.
+ *
+ * The shift is fixed in program_region(). These values are now the
+ * architectural ones and need no calibration. Verified on S32Z280 silicon:
+ * a write to an RO region faults, and execution from an XN region takes a
+ * prefetch abort -- the latter never worked under the old shift, because the
+ * intended XN bit landed in AP[1] instead.
+ */
+
+#define MPU_AP_RW_EL1 0U /* 0b00 EL1 read/write, no EL0 */
+#define MPU_AP_RW_EL1_EL0 1U /* 0b01 EL1 read/write, EL0 read/write */
+#define MPU_AP_RO_EL1 2U /* 0b10 EL1 read-only, no EL0 */
+#define MPU_AP_RO_EL1_EL0 3U /* 0b11 EL1 read-only, EL0 read-only */
+
+/* Shareability, PRBAR.SH. */
+
+#define MPU_SH_NON 0U
+#define MPU_SH_OUTER 2U
+#define MPU_SH_INNER 3U
+
+/* Attribute indices into MAIR, PRLAR.AttrIndx. */
+
+#define MPU_ATTR_NORMAL_WB 0U /* Normal, inner/outer write-back */
+#define MPU_ATTR_DEVICE 1U /* Device-nGnRnE */
+
+/* One protection region. The limit is inclusive, matching the hardware. */
+
+typedef struct MPU_REGION_STRUCT
+{
+ unsigned long mpu_region_base;
+ unsigned long mpu_region_limit;
+ unsigned char mpu_region_ap;
+ unsigned char mpu_region_execute_never;
+ unsigned char mpu_region_shareability;
+ unsigned char mpu_region_attr_index;
+ const char *mpu_region_name;
+} MPU_REGION;
+
+/* Number of regions this implementation provides, from MPUIR. */
+
+unsigned int mpu_region_count(void);
+
+/* Program the region table, then enable the MPU and the caches. Returns
+ the number of regions programmed, or 0 if the table does not fit. */
+
+unsigned int mpu_init(void);
+
+/* True once SCTLR reports the MPU and both caches enabled. */
+
+unsigned int mpu_is_enabled(void);
+unsigned int mpu_caches_enabled(void);
+
+/* The active table, for reporting. */
+
+const MPU_REGION *mpu_region_table(unsigned int *count_ptr);
+
+/* Read back what the hardware holds for one region, so that programming can
+ be verified rather than assumed. */
+
+void mpu_read_region(unsigned int index, unsigned long *prbar_ptr,
+ unsigned long *prlar_ptr);
+
+/* ---------------------------------------------------------------------------
+ ThreadX modules.
+
+ Kept in this header rather than behind TXM_MODULE_MANAGER because
+ txm_module_manager_offset_check.c includes it to check the region number
+ against the one tx_thread_schedule.S writes in assembly, and a definition
+ that appears only in some builds cannot be checked in the others.
+ --------------------------------------------------------------------------- */
+
+/* Region index for the manager's load window over the module area. Above the
+ kernel's 0-2 and above the eight the manager hands to a module (8-15), so
+ neither the boot table nor the scheduler's per-thread region load can
+ disturb it. tx_thread_schedule.S spells the same number in an MCR to
+ PRSELR; the offset check asserts the two agree. */
+
+#define MPU_MODULE_LOAD_REGION 16U
+
+/* How many EL1 regions a module manager build needs: 0-2 kernel, 8-15 for the
+ module block, 16 for the window. Region 16 is the highest index used, so
+ seventeen regions is the requirement.
+
+ Checked against MPUIR rather than assumed, even though the AEMv8-R model
+ reports 32. The model reports a region count NO Cortex-R52 can have -- the
+ TRM gives MPUIR.DREGION as 16, 20 or 24 -- so a green run here says nothing
+ about whether the budget fits silicon, and a part configured with 16 regions
+ cannot run this port at all. The check is here so that the shortfall is a
+ refusal rather than a region that silently does not exist. */
+
+#define MPU_MODULE_REGIONS_REQUIRED 17U
+
+/* The module area window, which is what lets privileged code reach module
+ memory at all -- no other kernel region covers it.
+
+ PMSAv8-R has no region priority, so this must never be enabled at the same
+ time as the regions a module is given, which cover the same memory. That is
+ guaranteed by who owns it rather than by careful calling: the scheduler turns
+ this region on for every thread that is not a module thread and off for every
+ thread that is, so the window is enabled exactly when no module regions are
+ loaded. The two register words are published for it below.
+
+ Enabled at boot, because everything before the first module thread is
+ privileged code that may need to reach module memory. Returns 0 if the
+ implementation has fewer than MPU_MODULE_REGIONS_REQUIRED regions, in which
+ case nothing was programmed. */
+
+unsigned int mpu_module_window_init(void);
+
+extern unsigned long mpu_module_window_prbar;
+extern unsigned long mpu_module_window_prlar;
+
+#endif /* MPU_H */
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h
new file mode 100644
index 00000000..151f732d
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h
@@ -0,0 +1,135 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* platform.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Memory map for the Armv8-R AEM FVP (BaseR platform). */
+/* */
+/* Every address here was VERIFIED IN-MODEL, not taken from */
+/* documentation or recollection. The BaseR map is the Base platform */
+/* map with its two 2 GB halves swapped (so a Base peripheral at X */
+/* below 0x80000000 appears at X + 0x80000000), and each base was then */
+/* confirmed by reading its identification register: */
+/* */
+/* PL011 UART0 peripheral ID 0x11,0x10,0x24,0x00, and a marker */
+/* written to it appeared on the host console */
+/* GIC GICD_PIDR2 = 0x3B and GICR_PIDR2 = 0x3B, i.e. */
+/* architecture revision 3 = GICv3 */
+/* Counter CNTFID0 = 0x05F5E100 = 100 MHz, matching the */
+/* model's bp.refcounter.base_frequency parameter */
+/* */
+/* Values are written without integer suffixes so that this header can */
+/* also be included from preprocessed assembly. */
+/* */
+/**************************************************************************/
+
+#ifndef PLATFORM_H
+#define PLATFORM_H
+
+/* PL011 UART0. */
+
+#define PL011_UART0_BASE 0x9C090000
+
+/* GICv3. A redistributor is two consecutive 64 KB frames per core: the RD
+ frame carries the identification and wake-up registers, the SGI frame the
+ per-interrupt enable, priority and configuration registers for SGIs/PPIs. */
+
+#define GICD_BASE 0xAF000000
+#define GICR_RD_BASE 0xAF100000
+#define GICR_SGI_BASE 0xAF110000
+
+/* System counter control frame (CNTControlBase). */
+
+#define CNT_CONTROL_BASE 0xAA430000
+
+/* The model resets CNTFRQ to zero and leaves the system counter stopped --
+ bp.refcounter.non_arch_start_at_default=0, documented as "firmware is
+ expected to enable the timer at boot time". This BSP therefore programs
+ both. The value is CNTFID0 read back from the counter control frame. */
+
+#define SYSTEM_COUNTER_HZ 100000000
+
+/* ---------------------------------------------------------------------------
+ Memory for ThreadX modules.
+
+ Only the module manager build uses this, but the addresses are stated here
+ rather than in the module example because they are a property of the board's
+ memory map: a module's code and data are handed to it as MPU regions, and
+ PMSAv8-R will not allow those to overlap a kernel region. So module memory
+ has to be memory NO kernel region covers, and deciding that is the memory
+ map's job.
+
+ link_module.lds ends the kernel's DRAM region at this base and gives the
+ 64 KB above it to the module area; mpu.c covers exactly the same range with
+ the manager's load window (region 16), which is the only mapping privileged
+ code has over it. The three have to agree, so all three read these two
+ values.
+
+ Chosen at 0x003F0000 -- just under 4 MB -- because the manager image with its
+ stacks and its 1 MB of heap is far smaller than that, and the link script
+ asserts the two do not meet rather than trusting the margin. */
+
+#define FVP_MODULE_AREA_BASE 0x003F0000
+#define FVP_MODULE_AREA_SIZE 0x00010000 /* 64 KB */
+
+/* The shared granules the sample module reports its progress through, at the
+ base of the module area.
+
+ The first of them exists because the FVP has no debugger seam: on silicon a
+ GDB harness reads the module's own progress variable out of its data area,
+ and here nothing outside the image can read anything. So the manager grants
+ the module a shared region over this word and reads it back afterwards, which
+ is what lets the FVP test judge what the module actually managed to do rather
+ than only that it faulted.
+
+ The rest exist to exercise the shared-region machinery itself. A module may
+ be granted TXM_MODULE_MPU_SHARED_ENTRIES regions, and one grant proves only
+ that the first entry works, so the area holds one granule per entry -- plus
+ one more that the manager NEVER grants, sandwiched between two that it does.
+ A limit register masked the wrong way, or a base off by a granule, leaks into
+ that gap from one side or the other, and a module that can write it is a
+ module whose grant covered more than was asked for.
+
+ FVP_MODULE_STATUS_SIZE is the size of ONE granule, which is the length of
+ each individual grant; the area is FVP_MODULE_STATUS_GRANULES of them.
+
+ Fixed addresses on both sides, and checked at run time rather than trusted:
+ the manager grants the regions from the linker's symbol and refuses to
+ continue if that symbol is not this address. */
+
+#define FVP_MODULE_STATUS_BASE 0x003F0000
+#define FVP_MODULE_STATUS_SIZE 0x40 /* one MPU granule */
+#define FVP_MODULE_STATUS_GRANULES 6 /* five granted, one not */
+#define FVP_MODULE_STATUS_UNGRANTED 2 /* the one never granted */
+
+#ifndef __ASSEMBLER__
+
+/* 32-bit device register access. */
+
+#define REG32(address) (*(volatile unsigned long *)(unsigned long)(address))
+
+#endif /* __ASSEMBLER__ */
+
+#endif /* PLATFORM_H */
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/test/run_fvp_test.py b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/test/run_fvp_test.py
new file mode 100644
index 00000000..37aa0adc
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/test/run_fvp_test.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2026 Eclipse ThreadX contributors
+# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
+#
+"""Run a Cortex-R52 image on the Armv8-R AEM FVP and assert its result.
+
+This runner is deliberately much simpler than the RISC-V/QEMU one, because
+the platform allows it: the FVP exposes only an Iris server and no GDB stub,
+so there is nothing to script through a debugger, and every image terminates
+itself with a semihosting SYS_EXIT. The runner therefore just launches the
+model, captures its console, and judges the self-reported result.
+
+A missing result line is treated as FAILURE, never as success: a silent hang
+or a crash before the first check must not be able to masquerade as a pass.
+"""
+
+import argparse
+import subprocess
+import sys
+
+PASS_MARK = "ALL CHECKS PASSED"
+FAIL_MARK = "RESULT: FAILED"
+FAULT_MARK = "[FAULT]"
+
+
+def as_text(stream):
+ """Normalise a captured stream to str.
+
+ subprocess.TimeoutExpired carries its partial output undecoded even when
+ the call used text=True, so the timeout path can hand us bytes where the
+ normal path hands us str. Decoding defensively here keeps the hang
+ report readable instead of raising TypeError inside the error handler.
+ """
+ if stream is None:
+ return ""
+ if isinstance(stream, bytes):
+ return stream.decode(errors="replace")
+ return stream
+
+
+def run(elf, fvp, timeout):
+ command = [
+ fvp,
+ "-C", "cluster0.NUM_CORES=1",
+ "-C", "bp.vis.disable_visualisation=1",
+ "-C", "bp.terminal_0.start_telnet=0",
+ # Route UART0 to stdout so PL011-console images are captured too.
+ # Harmless for semihosting images, which do not touch the UART.
+ "-C", "bp.pl011_uart0.out_file=-",
+ "-C", "bp.pl011_uart0.unbuffered_output=1",
+ "-a", elf,
+ ]
+
+ print("Running:", " ".join(command), flush=True)
+
+ try:
+ completed = subprocess.run(
+ command, capture_output=True, text=True, timeout=timeout
+ )
+ output = as_text(completed.stdout) + as_text(completed.stderr)
+ except subprocess.TimeoutExpired as expired:
+ print(as_text(expired.stdout) + as_text(expired.stderr))
+ print(
+ f"FAIL: the simulation did not terminate within {timeout}s. "
+ "Every image is expected to exit through semihosting SYS_EXIT, "
+ "so this is a hang, not a slow run.",
+ file=sys.stderr,
+ )
+ return 1
+
+ print(output)
+
+ if FAULT_MARK in output:
+ faults = [line for line in output.splitlines() if FAULT_MARK in line]
+ print("FAIL: unhandled exception reported:", file=sys.stderr)
+ for line in faults:
+ print(" " + line.strip(), file=sys.stderr)
+ return 1
+
+ if FAIL_MARK in output:
+ print("FAIL: the image reported failing checks.", file=sys.stderr)
+ return 1
+
+ if PASS_MARK not in output:
+ print(
+ "FAIL: no result line found. The image neither passed nor "
+ "reported failure, so it did not reach its checks.",
+ file=sys.stderr,
+ )
+ return 1
+
+ print("PASS")
+ return 0
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--elf", required=True, help="image to run")
+ parser.add_argument("--fvp", default="FVP_BaseR_AEMv8R", help="model binary")
+ parser.add_argument(
+ "--timeout", type=int, default=180, help="seconds before declaring a hang"
+ )
+ arguments = parser.parse_args()
+
+ return run(arguments.elf, arguments.fvp, arguments.timeout)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/timer.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/timer.c
new file mode 100644
index 00000000..eac0b3dc
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/timer.c
@@ -0,0 +1,174 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* timer.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Arm generic timer tick for Cortex-R52 on the Armv8-R AEM FVP. */
+/* */
+/* Two model behaviours make this longer than a bare timer setup, both */
+/* confirmed by reading the hardware rather than assumed: */
+/* */
+/* 1. The system counter is STOPPED at reset (CNTCR = 0, CNTCV = 0). */
+/* The model documents that firmware is expected to start it, so */
+/* the counter control frame is enabled here. Without this the */
+/* timer never counts and no tick is ever delivered. */
+/* 2. CNTFRQ resets to ZERO. It is writable only at the highest */
+/* implemented exception level, so entry.S programs it at EL2; */
+/* this file only reads it. Deriving the tick interval from an */
+/* unprogrammed CNTFRQ would divide by zero. */
+/* */
+/* EL1 access to the physical timer and counter also depends on */
+/* CNTHCTL.PL1PCEN/PL1PCTEN, which reset disabled and are set by */
+/* entry.S at EL2; otherwise these accesses would trap to EL2. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Directive 4.3 -- the generic timer is only reachable through CP15 */
+/* registers; every such access is encapsulated in an accessor below.*/
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "platform.h"
+#include "timer.h"
+
+/* Counter control frame (CNTControlBase). */
+
+#define CNTCR 0x0000U
+#define CNTCR_EN (1UL << 0)
+#define CNTSR 0x0004U
+
+/* CNTP_CTL bits. */
+
+#define CNTP_CTL_ENABLE (1UL << 0)
+#define CNTP_CTL_IMASK (1UL << 1)
+
+static unsigned long tick_interval;
+
+
+/**************************************************************************/
+/* Generic timer accessors (AArch32 CP15). */
+/**************************************************************************/
+
+static unsigned long read_cntfrq(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c14, c0, 0" : "=r"(value));
+ return value;
+}
+
+static void write_cntp_tval(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c14, c2, 0" : : "r"(value) : "memory");
+}
+
+static void write_cntp_ctl(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c14, c2, 1" : : "r"(value) : "memory");
+}
+
+static unsigned long long read_cntpct(void)
+{
+ unsigned long low;
+ unsigned long high;
+
+ __asm__ volatile("mrrc p15, 0, %0, %1, c14" : "=r"(low), "=r"(high));
+
+ return ((unsigned long long) high << 32) | (unsigned long long) low;
+}
+
+
+/**************************************************************************/
+/* timer_frequency */
+/**************************************************************************/
+
+unsigned long timer_frequency(void)
+{
+ return read_cntfrq();
+}
+
+
+/**************************************************************************/
+/* timer_counter */
+/**************************************************************************/
+
+unsigned long long timer_counter(void)
+{
+ return read_cntpct();
+}
+
+
+/**************************************************************************/
+/* timer_counter_enabled */
+/**************************************************************************/
+
+unsigned int timer_counter_enabled(void)
+{
+ return ((REG32(CNT_CONTROL_BASE + CNTCR) & CNTCR_EN) != 0UL) ? 1U : 0U;
+}
+
+
+/**************************************************************************/
+/* timer_reload */
+/**************************************************************************/
+
+void timer_reload(void)
+{
+ /* Writing CNTP_TVAL restarts the down-count, which also deasserts the
+ level-sensitive timer output. */
+
+ write_cntp_tval(tick_interval);
+}
+
+
+/**************************************************************************/
+/* timer_init */
+/**************************************************************************/
+
+void timer_init(void)
+{
+ unsigned long frequency;
+
+ /* Start the system counter -- it is stopped at reset on this model. */
+
+ REG32(CNT_CONTROL_BASE + CNTCR) |= CNTCR_EN;
+
+ /* Derive the tick interval. CNTFRQ was programmed at EL2; fall back to
+ the known counter frequency rather than dividing by zero should that
+ ever not have happened. */
+
+ frequency = read_cntfrq();
+ if (frequency == 0UL)
+ {
+ frequency = (unsigned long) SYSTEM_COUNTER_HZ;
+ }
+
+ tick_interval = frequency / TX_TIMER_TICKS_PER_SECOND;
+
+ /* Enable the timer, unmasked, and arm the first interval. */
+
+ write_cntp_tval(tick_interval);
+ write_cntp_ctl(CNTP_CTL_ENABLE);
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/timer.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/timer.h
new file mode 100644
index 00000000..5b7968a3
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/timer.h
@@ -0,0 +1,67 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* timer.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Arm generic timer tick for Cortex-R52. */
+/* */
+/**************************************************************************/
+
+#ifndef TIMER_H
+#define TIMER_H
+
+/* PPI driven by the EL1 physical timer.
+ *
+ * This was not assumed. Bring-up enabled the entire PPI range 25-31 and
+ * recorded whichever INTID came back from ICC_IAR1; the model drove 30, which
+ * matches the architectural assignment for the EL1 physical timer. Only that
+ * line is enabled now, and irq_dispatch.c still records any other INTID so a
+ * different platform assignment would be reported rather than silently
+ * mishandled. Re-verify when moving to S32Z2 silicon. */
+
+#define TIMER_PPI_INTID 30U
+
+/* Enable the system counter, then start the periodic tick at
+ TX_TIMER_TICKS_PER_SECOND. gicv3_init() must have run first. */
+
+void timer_init(void);
+
+/* Re-arm the timer for the next tick. Called from the interrupt path. */
+
+void timer_reload(void);
+
+/* CNTFRQ as programmed by entry.S at EL2. */
+
+unsigned long timer_frequency(void);
+
+/* Current 64-bit physical counter value, for verifying the counter runs. */
+
+unsigned long long timer_counter(void);
+
+/* True once the counter control frame reports the counter enabled. */
+
+unsigned int timer_counter_enabled(void);
+
+#endif /* TIMER_H */
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/tx_initialize_low_level.S b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/tx_initialize_low_level.S
new file mode 100644
index 00000000..c8d3f188
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/tx_initialize_low_level.S
@@ -0,0 +1,107 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** Initialize */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+ /* No .arch directive here. GNU as accepts "armv8-r", LLVM's integrated
+ assembler accepts no spelling of it at all -- not armv8-r, not armv8r --
+ and fails with "Unknown Arch". The architecture is already selected by
+ -mcpu=cortex-r52 on the command line, which both toolchains require and
+ both toolchain files pass, so the directive only restated it. */
+
+ .syntax unified
+ .arm
+
+ .global _tx_thread_system_stack_ptr
+ .global _tx_initialize_unused_memory
+
+ .text
+ .balign 4
+
+/**************************************************************************/
+/* */
+/* FUNCTION RELEASE */
+/* */
+/* _tx_initialize_low_level Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Low-level initialization for Cortex-R52 on the Armv8-R AEM FVP. */
+/* Called by _tx_initialize_kernel_enter with the processor already in */
+/* Supervisor mode at EL1. */
+/* */
+/* This is deliberately shorter than the A-profile reference ports: */
+/* entry.S has already given every AArch32 mode its own stack from */
+/* dedicated linker-script regions, so there is no stack carving to do */
+/* here and no run-time stack-overlap check to perform -- the linker */
+/* guarantees the regions do not overlap. */
+/* */
+/* When built with TX_R52_USE_THREADX_IRQ, board_init() brings up GICv3*/
+/* and the generic timer tick here. That is safe this early because */
+/* interrupts stay masked until _tx_thread_schedule enables them, so no*/
+/* tick can be delivered before the kernel is ready to take one. */
+/* Without that symbol (the M2 cooperative demo) no tick is created, */
+/* which keeps M2 free of any interrupt dependency. */
+/* */
+/* CALLED BY */
+/* */
+/* _tx_initialize_kernel_enter ThreadX entry function */
+/* */
+/**************************************************************************/
+
+ .global _tx_initialize_low_level
+ .type _tx_initialize_low_level, %function
+_tx_initialize_low_level:
+
+ /* Record the system stack. The interrupt path uses it when an
+ exception arrives while no thread is running. */
+
+ ldr r0, =__sys_stack_top
+ ldr r1, =_tx_thread_system_stack_ptr
+ str r0, [r1]
+
+ /* Publish the first free memory address for tx_application_define.
+ _end sits above the image, .bss and every per-mode stack. Round up
+ to an 8-byte boundary so pool control blocks are aligned. */
+
+ ldr r0, =_end
+ add r0, r0, #7
+ bic r0, r0, #7
+ ldr r1, =_tx_initialize_unused_memory
+ str r0, [r1]
+
+#ifdef TX_R52_USE_THREADX_IRQ
+
+ /* Bring up the interrupt controller and the periodic tick. */
+
+ push {lr}
+ bl board_init
+ pop {lr}
+
+#endif
+
+ bx lr
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/uart_pl011.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/uart_pl011.c
new file mode 100644
index 00000000..b4c78127
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/uart_pl011.c
@@ -0,0 +1,127 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* uart_pl011.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* PL011 UART output for the Armv8-R AEM FVP, used as the console when */
+/* the image is built with TX_R52_CONSOLE_PL011. */
+/* */
+/* Semihosting remains the default because it needs no peripheral at */
+/* all, which keeps early bring-up independent of the memory map. The */
+/* UART matters because it is what real silicon will use: exercising it*/
+/* here means the S32Z280 console differs only in its base address and */
+/* clocking, not in structure. */
+/* */
+/* The model leaves UART0 disabled at reset */
+/* (bp.pl011_uart0.uart_enable=0), so the control register must be */
+/* written before the first character. Baud rate is left alone: the */
+/* model does not emulate serial timing, and inventing a divisor here */
+/* would only be misleading. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Rule 11.4/11.6 -- casting integer addresses to volatile pointers is */
+/* inherent to memory-mapped device access; confined to REG32. */
+/* */
+/**************************************************************************/
+
+#include "platform.h"
+#include "uart_pl011.h"
+
+/* PL011 register offsets. */
+
+#define UART_DR 0x000U /* data */
+#define UART_FR 0x018U /* flags */
+#define UART_LCR_H 0x02CU /* line control */
+#define UART_CR 0x030U /* control */
+
+#define UART_FR_TXFF (1UL << 5) /* transmit FIFO full */
+
+#define UART_LCR_H_WLEN_8 (3UL << 5) /* 8 data bits */
+#define UART_LCR_H_FEN (1UL << 4) /* enable FIFOs */
+
+#define UART_CR_UARTEN (1UL << 0) /* UART enable */
+#define UART_CR_TXE (1UL << 8) /* transmit enable */
+#define UART_CR_RXE (1UL << 9) /* receive enable */
+
+
+/**************************************************************************/
+/* pl011_init */
+/**************************************************************************/
+
+void pl011_init(void)
+{
+ /* Disable while reconfiguring, as the programming model requires. */
+
+ REG32(PL011_UART0_BASE + UART_CR) = 0UL;
+
+ REG32(PL011_UART0_BASE + UART_LCR_H) = UART_LCR_H_WLEN_8 | UART_LCR_H_FEN;
+
+ REG32(PL011_UART0_BASE + UART_CR) =
+ UART_CR_UARTEN | UART_CR_TXE | UART_CR_RXE;
+}
+
+
+/**************************************************************************/
+/* pl011_putc */
+/**************************************************************************/
+
+void pl011_putc(char character)
+{
+ /* Expand newlines so the host console shows discrete lines. */
+
+ if (character == '\n')
+ {
+ pl011_putc('\r');
+ }
+
+ while ((REG32(PL011_UART0_BASE + UART_FR) & UART_FR_TXFF) != 0UL)
+ {
+ /* Wait for room in the transmit FIFO. */
+ }
+
+ REG32(PL011_UART0_BASE + UART_DR) = (unsigned long) (unsigned char) character;
+}
+
+
+/**************************************************************************/
+/* pl011_puts */
+/**************************************************************************/
+
+void pl011_puts(const char *string_ptr)
+{
+ const char *cursor = string_ptr;
+
+ if (cursor == 0)
+ {
+ return;
+ }
+
+ while (*cursor != '\0')
+ {
+ pl011_putc(*cursor);
+ cursor++;
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/uart_pl011.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/uart_pl011.h
new file mode 100644
index 00000000..89e416ce
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/uart_pl011.h
@@ -0,0 +1,47 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* uart_pl011.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* PL011 UART console backend for the Armv8-R AEM FVP. */
+/* */
+/**************************************************************************/
+
+#ifndef UART_PL011_H
+#define UART_PL011_H
+
+/* Configure and enable UART0. The model leaves it disabled at reset. */
+
+void pl011_init(void);
+
+/* Send one character, expanding a newline into CR LF. */
+
+void pl011_putc(char character);
+
+/* Send a NUL-terminated string. */
+
+void pl011_puts(const char *string_ptr);
+
+#endif /* UART_PL011_H */
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt
new file mode 100644
index 00000000..85dd29d6
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt
@@ -0,0 +1,521 @@
+# Copyright (c) 2026 Eclipse ThreadX contributors
+# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
+#
+# Example build for Cortex-R52 on the NXP S32Z280-594EVB, RTU0 core 0.
+#
+# Unlike the FVP example there is no automated test target here: the board is
+# reached through a debug probe and a vendor GDB server, not by launching a
+# model, so running an image is a manual step. tools/read_identity.gdb loads
+# an image, runs it and reports what it found.
+
+set(EVB_DIR ${CMAKE_CURRENT_LIST_DIR})
+
+# A bare-metal image has one flat SRAM region and no OS page permissions; access
+# control belongs to the MPU, so an RWX segment is expected rather than a mistake.
+# GNU ld has warned about them since binutils 2.39 and takes this flag to stay
+# quiet; ld.lld does not warn and rejects the flag outright, failing the link with
+# "unknown argument". Chosen by toolchain, as in the FVP example.
+if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
+ set(EVB_LINK_QUIET_RWX -Wl,--no-warn-rwx-segments)
+else()
+ set(EVB_LINK_QUIET_RWX)
+endif()
+
+# Silicon M0 -- boot verification. Deliberately does not link ThreadX: a
+# failure here is then unambiguously a boot or board problem rather than a
+# kernel one, which is the same reasoning as the FVP's boot_check.elf.
+add_executable(s32z280_boot.elf EXCLUDE_FROM_ALL
+ ${EVB_DIR}/entry.S
+ ${EVB_DIR}/bsp_boot.c
+ ${EVB_DIR}/linflexd.c
+ ${EVB_DIR}/timer.c
+ ${EVB_DIR}/gic_probe.c
+ ${EVB_DIR}/mpu.c
+ ${EVB_DIR}/gicv3.c
+ ${EVB_DIR}/irq_dispatch.c
+ ${EVB_DIR}/cache.c
+ ${EVB_DIR}/tcm.c
+)
+
+target_include_directories(s32z280_boot.elf PRIVATE ${EVB_DIR})
+
+# -g only: this image exists to be inspected through a debugger, and without
+# DWARF the identity structure has no type for GDB to walk. No effect on the
+# generated code.
+target_compile_options(s32z280_boot.elf PRIVATE -g)
+
+target_link_options(s32z280_boot.elf PRIVATE
+ -T${EVB_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=s32z280_boot.map
+ # Bare metal: one code region and one data region, with access control
+ # belonging to the MPU rather than to segment permissions.
+ ${EVB_LINK_QUIET_RWX}
+)
+
+
+# Same image as s32z280_boot.elf with the interrupt service body placed in ATCM
+# instead of code RAM, so the two can be compared on the same board in the same
+# session. Separate target rather than an option so both exist at once and a
+# result can be reproduced without reconfiguring.
+add_executable(s32z280_atcm.elf EXCLUDE_FROM_ALL
+ ${EVB_DIR}/entry.S
+ ${EVB_DIR}/bsp_boot.c
+ ${EVB_DIR}/linflexd.c
+ ${EVB_DIR}/timer.c
+ ${EVB_DIR}/gic_probe.c
+ ${EVB_DIR}/mpu.c
+ ${EVB_DIR}/gicv3.c
+ ${EVB_DIR}/irq_dispatch.c
+ ${EVB_DIR}/cache.c
+ ${EVB_DIR}/tcm.c
+)
+
+target_include_directories(s32z280_atcm.elf PRIVATE ${EVB_DIR})
+
+# -g only: this image exists to be inspected through a debugger, and without
+# DWARF the identity structure has no type for GDB to walk. No effect on the
+# generated code.
+target_compile_options(s32z280_atcm.elf PRIVATE -g)
+
+target_link_options(s32z280_atcm.elf PRIVATE
+ -T${EVB_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=s32z280_atcm.map
+ # Bare metal: one code region and one data region, with access control
+ # belonging to the MPU rather than to segment permissions.
+ ${EVB_LINK_QUIET_RWX}
+)
+
+
+target_compile_definitions(s32z280_atcm.elf PRIVATE TX_R52_ATCM_ISR)
+
+
+# ThreadX demo on silicon: threads, preemption and the timer tick. Separate
+# target from s32z280_boot.elf, which deliberately does not link the kernel so
+# that a boot failure there is unambiguously a board problem.
+add_executable(s32z280_demo.elf EXCLUDE_FROM_ALL
+ ${EVB_DIR}/entry.S
+ ${EVB_DIR}/tx_initialize_low_level.S
+ ${EVB_DIR}/linflexd.c
+ ${EVB_DIR}/timer.c
+ ${EVB_DIR}/mpu.c
+ ${EVB_DIR}/gicv3.c
+ ${EVB_DIR}/irq_dispatch.c
+ ${EVB_DIR}/gic_probe.c
+ ${EVB_DIR}/cache.c
+ ${EVB_DIR}/tcm.c
+ ${EVB_DIR}/thread_mpu.c
+ ${EVB_DIR}/demo_s32z280.c
+)
+
+# TX_R52_USE_THREADX_IRQ routes the IRQ vector through _tx_thread_context_save
+# instead of the standalone handler, and makes _tx_initialize_low_level call
+# board_init().
+target_compile_definitions(s32z280_demo.elf PRIVATE TX_R52_USE_THREADX_IRQ=1)
+target_compile_options(s32z280_demo.elf PRIVATE -g)
+
+target_link_libraries(s32z280_demo.elf PRIVATE threadx)
+
+target_include_directories(s32z280_demo.elf PRIVATE
+ ${EVB_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+)
+
+target_link_options(s32z280_demo.elf PRIVATE
+ -T${EVB_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=s32z280_demo.map
+ ${EVB_LINK_QUIET_RWX}
+)
+
+# Lazy VFP context save and restore. Only meaningful when the library was built
+# with TX_R52_ENABLE_VFP and a floating-point ABI, so like the FVP's demo_m5.elf
+# the target exists only in that configuration. The FPU hardware enable lives in
+# entry.S under __ARM_FP; tx_thread_vfp_enable() sets only the per-thread flag.
+if(TX_R52_ENABLE_VFP)
+ add_executable(s32z280_vfp.elf EXCLUDE_FROM_ALL
+ ${EVB_DIR}/entry.S
+ ${EVB_DIR}/tx_initialize_low_level.S
+ ${EVB_DIR}/linflexd.c
+ ${EVB_DIR}/timer.c
+ ${EVB_DIR}/mpu.c
+ ${EVB_DIR}/gicv3.c
+ ${EVB_DIR}/irq_dispatch.c
+ ${EVB_DIR}/gic_probe.c
+ ${EVB_DIR}/cache.c
+ ${EVB_DIR}/tcm.c
+ ${EVB_DIR}/demo_vfp_s32z280.c
+ )
+
+ target_compile_definitions(s32z280_vfp.elf PRIVATE TX_R52_USE_THREADX_IRQ=1)
+ target_compile_options(s32z280_vfp.elf PRIVATE -g)
+
+ target_link_libraries(s32z280_vfp.elf PRIVATE threadx)
+
+ target_include_directories(s32z280_vfp.elf PRIVATE
+ ${EVB_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+ )
+
+ target_link_options(s32z280_vfp.elf PRIVATE
+ -T${EVB_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=s32z280_vfp.map
+ ${EVB_LINK_QUIET_RWX}
+ )
+endif()
+
+# Nested IRQ handling on silicon. Needs the library built with
+# TX_R52_ENABLE_IRQ_NESTING, since entry.S only emits the nesting_start and
+# nesting_end pairing under that macro, so the image exists in that
+# configuration only -- as s32z280_vfp.elf does for the floating-point ABI.
+if(TX_R52_ENABLE_IRQ_NESTING)
+ add_executable(s32z280_nesting.elf EXCLUDE_FROM_ALL
+ ${EVB_DIR}/entry.S
+ ${EVB_DIR}/tx_initialize_low_level.S
+ ${EVB_DIR}/linflexd.c
+ ${EVB_DIR}/timer.c
+ ${EVB_DIR}/mpu.c
+ ${EVB_DIR}/gicv3.c
+ ${EVB_DIR}/irq_dispatch.c
+ ${EVB_DIR}/gic_probe.c
+ ${EVB_DIR}/cache.c
+ ${EVB_DIR}/tcm.c
+ ${EVB_DIR}/demo_nesting_s32z280.c
+ )
+
+ target_compile_definitions(s32z280_nesting.elf PRIVATE TX_R52_USE_THREADX_IRQ=1)
+ target_compile_options(s32z280_nesting.elf PRIVATE -g)
+
+ target_link_libraries(s32z280_nesting.elf PRIVATE threadx)
+
+ target_include_directories(s32z280_nesting.elf PRIVATE
+ ${EVB_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+ )
+
+ target_link_options(s32z280_nesting.elf PRIVATE
+ -T${EVB_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=s32z280_nesting.map
+ ${EVB_LINK_QUIET_RWX}
+ )
+endif()
+
+# Nested FIQ handling on silicon. Needs TX_R52_ENABLE_FIQ_NESTING, which the
+# port-level CMakeLists already requires TX_R52_ENABLE_FIQ alongside, so the image
+# exists only where both the FIQ vector path and the nesting pairing are compiled
+# in -- as s32z280_nesting.elf does for the IRQ side.
+if(TX_R52_ENABLE_FIQ_NESTING)
+ add_executable(s32z280_fiq.elf EXCLUDE_FROM_ALL
+ ${EVB_DIR}/entry.S
+ ${EVB_DIR}/tx_initialize_low_level.S
+ ${EVB_DIR}/linflexd.c
+ ${EVB_DIR}/timer.c
+ ${EVB_DIR}/mpu.c
+ ${EVB_DIR}/gicv3.c
+ ${EVB_DIR}/irq_dispatch.c
+ ${EVB_DIR}/gic_probe.c
+ ${EVB_DIR}/cache.c
+ ${EVB_DIR}/tcm.c
+ ${EVB_DIR}/demo_fiq_s32z280.c
+ )
+
+ target_compile_definitions(s32z280_fiq.elf PRIVATE TX_R52_USE_THREADX_IRQ=1)
+ target_compile_options(s32z280_fiq.elf PRIVATE -g)
+
+ target_link_libraries(s32z280_fiq.elf PRIVATE threadx)
+
+ target_include_directories(s32z280_fiq.elf PRIVATE
+ ${EVB_DIR}
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
+ )
+
+ target_link_options(s32z280_fiq.elf PRIVATE
+ -T${EVB_DIR}/link.lds
+ -nostartfiles
+ -Wl,-Map=s32z280_fiq.map
+ ${EVB_LINK_QUIET_RWX}
+ )
+endif()
+
+# ---------------------------------------------------------------------------
+# ThreadX modules: the module manager, with a sample module linked into the
+# module area and loaded in place from there.
+#
+# This target deliberately does NOT link the threadx library's port assembly.
+# The module port carries its own copies of the scheduler, context restore and
+# stack build, because the region switch happens in the scheduler and a module
+# thread starts in a different processor mode. Linking both would give two
+# definitions of every one of them.
+#
+# Guarded by an option, off by default, because it is the only target that needs
+# TXM_MODULE_MANAGER in entry.S and the only one that links common_modules.
+# ---------------------------------------------------------------------------
+
+option(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE
+ "Build the NXP S32Z280-594EVB ThreadX module manager example" OFF)
+
+if(TX_R52_BUILD_S32Z280_MODULE_EXAMPLE)
+
+ set(MOD_DIR ${CMAKE_SOURCE_DIR}/ports_module/cortex_r52/gnu)
+ set(MOD_EVB ${MOD_DIR}/example_build/s32z280_evb)
+
+ # ---------------------------------------------------------------------
+ # A second ThreadX library, built with the module port's headers.
+ #
+ # This is not optional and not a convenience. The module port's tx_port.h
+ # adds the owning module instance to TX_QUEUE, TX_SEMAPHORE,
+ # TX_EVENT_FLAGS_GROUP and TX_TIMER, and the module fields to TX_THREAD.
+ # Measured: TX_QUEUE is 60 bytes against the base port's, TX_SEMAPHORE 40
+ # against 32, TX_THREAD 236 against 184. A kernel compiled against the base
+ # port and a manager compiled against this one disagree about every object,
+ # and they link without complaint, because C linking does not compare struct
+ # layouts. The result would be memory corruption at run time with nothing in
+ # the build to hint at it.
+ #
+ # The base port's assembly is reused except for the five files the module port
+ # replaces, and tx_port_offset_check.c is included deliberately: if the module
+ # headers moved any offset that assembly depends on, that check is what says
+ # so, and it is better to find out here than on the board.
+ # ---------------------------------------------------------------------
+
+ # Guarded by if(NOT TARGET) because the FVP module example defines the same
+ # library from its own directory, and the two example options can be on at
+ # once. The definitions are identical -- both describe the module port, not
+ # a board -- so whichever directory CMake reaches first may create it.
+
+ if(NOT TARGET threadx_module)
+
+ file(GLOB TX_COMMON_SRC ${CMAKE_SOURCE_DIR}/common/src/*.c)
+
+ set(TX_R52_PORT_SRC ${CMAKE_SOURCE_DIR}/ports/cortex_r52/gnu/src)
+
+ add_library(threadx_module STATIC EXCLUDE_FROM_ALL
+ ${TX_COMMON_SRC}
+ ${TX_R52_PORT_SRC}/tx_port_offset_check.c
+ ${TX_R52_PORT_SRC}/tx_thread_fiq_context_restore.S
+ ${TX_R52_PORT_SRC}/tx_thread_fiq_context_save.S
+ ${TX_R52_PORT_SRC}/tx_thread_fiq_nesting_end.S
+ ${TX_R52_PORT_SRC}/tx_thread_fiq_nesting_start.S
+ ${TX_R52_PORT_SRC}/tx_thread_interrupt_control.S
+ ${TX_R52_PORT_SRC}/tx_thread_interrupt_disable.S
+ ${TX_R52_PORT_SRC}/tx_thread_interrupt_restore.S
+ ${TX_R52_PORT_SRC}/tx_thread_irq_nesting_end.S
+ ${TX_R52_PORT_SRC}/tx_thread_irq_nesting_start.S
+ ${TX_R52_PORT_SRC}/tx_thread_vectored_context_save.S
+ ${TX_R52_PORT_SRC}/tx_timer_interrupt.S
+ )
+
+ target_include_directories(threadx_module PUBLIC
+ ${MOD_DIR}/inc
+ ${CMAKE_SOURCE_DIR}/common/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_manager/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/inc
+ )
+
+ target_compile_definitions(threadx_module PUBLIC TXM_MODULE_MANAGER)
+
+ endif()
+
+ # The portable half of the module manager. Globbed rather than listed: it is
+ # a whole upstream component, not a selection from one, and a file added to it
+ # upstream should not need a change here to be compiled.
+
+ file(GLOB TXM_MANAGER_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_manager/src/*.c)
+
+ # The module-side library: the API shims that run inside the module. These
+ # belong to the module's image and must not reach the manager's link -- they
+ # define the same names as the kernel's own entry points, and as objects they
+ # beat the kernel's static library, so in one link every service call the
+ # manager makes is redirected into the module. See link_demo_module.lds.
+
+ file(GLOB TXM_MODULE_LIB_SRC ${CMAKE_SOURCE_DIR}/common_modules/module_lib/src/*.c)
+
+ # ------------------------------------------------------------------------
+ # The module, built as its own image and reduced to a raw binary.
+ # ------------------------------------------------------------------------
+
+ add_executable(s32z280_demo_module.elf EXCLUDE_FROM_ALL
+ ${MOD_EVB}/txm_module_preamble.S
+ ${MOD_EVB}/sample_threadx_module.c
+ ${MOD_DIR}/module_lib/src/txm_module_thread_shell_entry.c
+ ${MOD_DIR}/module_lib/src/txm_module_gcc_setup.S
+ ${TXM_MODULE_LIB_SRC}
+ )
+
+ # No TXM_MODULE_MANAGER here: that define selects the kernel side of the
+ # shared headers, and this is the other side.
+
+ target_include_directories(s32z280_demo_module.elf PRIVATE
+ ${EVB_DIR}
+ ${MOD_DIR}/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/inc
+ ${CMAKE_SOURCE_DIR}/common/inc
+ )
+
+ # Position independent, and only here. These four flags are what make the
+ # module relocatable, and putting any of them on the manager would be a bug:
+ # the manager is the resident image, linked absolutely at the address it
+ # boots from, and -msingle-pic-base in particular would have it treat r9 as
+ # a PIC base that nothing sets up.
+ #
+ # -fpic data references go through the GOT
+ # -msingle-pic-base r9 is the GOT base, and the caller sets
+ # it -- which is what the manager's
+ # thread stack build now does
+ # -mno-pic-data-is-text-relative the module's data is NOT at a fixed
+ # offset from its code. It is not: code
+ # is loaded in place in the module area
+ # and data is allocated from the byte
+ # pool, so the gap between them is
+ # decided at run time
+ # -fno-plt no procedure linkage table, which a
+ # module has no loader to populate
+ # -mno-long-calls see below -- this one is ours, not the
+ # reference port's, and without it the
+ # module cannot start at all
+ #
+ # -mno-long-calls is the interesting one, and it is here because of a bug
+ # this build found before silicon did. The project-wide C flags carry
+ # -mlong-calls, which the manager needs: its image spans TCM at 0x30000000
+ # and code at 0x79900000, far beyond the +/-32 MB a direct BL reaches. But
+ # -mlong-calls under -fpic makes GCC route EVERY call through the GOT --
+ # R_ARM_GOT32 instead of R_ARM_CALL -- and one of those calls is the shell
+ # entry's call to _gcc_setup, which is the function that fills the GOT in.
+ # The module would have loaded, entered its shell entry, loaded a zero out
+ # of the not-yet-written GOT and branched to address 0.
+ #
+ # A module does not need long calls. It is one contiguous blob under two
+ # kilobytes, every internal call is a few hundred bytes away, and it never
+ # calls out of itself: kernel services go through the dispatcher function
+ # pointer in its entry info, which is data and reached through the GOT
+ # either way. So the flag is pure cost here, and dropping it is what makes
+ # the bootstrap possible rather than a workaround for it.
+ #
+ # The same four, in the same order, are what the Cortex-A7 GNU module
+ # example uses. Verified on the generated code rather than assumed: with
+ # these flags every reference to a module global comes out as an R_ARM_GOT32
+ # and loads through LDR rX, [r9, rY], while calls stay direct R_ARM_CALL.
+
+ target_compile_options(s32z280_demo_module.elf PRIVATE
+ -g
+ -fpic
+ -fno-plt
+ -mno-pic-data-is-text-relative
+ -msingle-pic-base
+ -mno-long-calls
+ )
+
+ target_link_options(s32z280_demo_module.elf PRIVATE
+ -T${MOD_EVB}/link_demo_module.lds
+ -nostartfiles
+ -nostdlib
+ -Wl,-Map=s32z280_demo_module.map
+ ${EVB_LINK_QUIET_RWX}
+ )
+
+ # The raw image the manager embeds. Written into the binary directory
+ # because that is the include path module_blob.S searches.
+
+ add_custom_command(
+ OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin
+ COMMAND ${CMAKE_OBJCOPY} -O binary
+ $<TARGET_FILE:s32z280_demo_module.elf>
+ ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin
+ DEPENDS s32z280_demo_module.elf
+ COMMENT "Converting the demonstration module to a raw image"
+ VERBATIM
+ )
+
+ add_custom_target(s32z280_demo_module_bin
+ DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin
+ )
+
+ add_executable(s32z280_module.elf EXCLUDE_FROM_ALL
+ # Board support, shared with the other examples
+ ${EVB_DIR}/entry.S
+ ${EVB_DIR}/linflexd.c
+ ${EVB_DIR}/timer.c
+ ${EVB_DIR}/mpu.c
+ ${EVB_DIR}/gicv3.c
+ ${EVB_DIR}/irq_dispatch.c
+ ${EVB_DIR}/gic_probe.c
+ ${EVB_DIR}/cache.c
+ ${EVB_DIR}/tcm.c
+ ${EVB_DIR}/thread_mpu.c
+ ${EVB_DIR}/tx_initialize_low_level.S
+
+ # Module manager port
+ ${MOD_DIR}/module_manager/src/tx_thread_schedule.S
+ ${MOD_DIR}/module_manager/src/tx_thread_context_save.S
+ ${MOD_DIR}/module_manager/src/tx_thread_context_restore.S
+ ${MOD_DIR}/module_manager/src/tx_thread_stack_build.S
+ ${MOD_DIR}/module_manager/src/tx_thread_system_return.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_svc_handler.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_fault_capture.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_user_mode_entry.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_thread_stack_build.S
+ ${MOD_DIR}/module_manager/src/txm_module_manager_mm_register_setup.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_memory_fault_handler.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_memory_fault_notify.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_alignment_adjust.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_external_memory_enable.c
+ ${MOD_DIR}/module_manager/src/txm_module_manager_offset_check.c
+
+ # The module, as bytes rather than as objects. module_blob.S includes the
+ # raw image built by s32z280_demo_module.elf, so none of the module's
+ # symbols enter this link.
+ ${MOD_EVB}/module_blob.S
+
+ # The application
+ ${MOD_EVB}/sample_threadx_module_manager.c
+
+ # The portable module manager
+ ${TXM_MANAGER_SRC}
+ )
+
+ target_include_directories(s32z280_module.elf PRIVATE
+ ${EVB_DIR}
+ ${MOD_DIR}/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_manager/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/module_lib/inc
+ ${CMAKE_SOURCE_DIR}/common_modules/inc
+ )
+
+ target_compile_definitions(s32z280_module.elf PRIVATE
+ TXM_MODULE_MANAGER
+ TX_R52_USE_THREADX_IRQ
+ )
+
+ target_compile_options(s32z280_module.elf PRIVATE -g)
+
+ # .incbin searches the assembler's include paths, not the source tree, and
+ # demo_module.bin is generated -- so the build directory has to be on that
+ # path or module_blob.S cannot find the image.
+
+ set_source_files_properties(${MOD_EVB}/module_blob.S PROPERTIES
+ OBJECT_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin
+ COMPILE_OPTIONS "-Wa,-I${CMAKE_CURRENT_BINARY_DIR}"
+ )
+
+ add_dependencies(s32z280_module.elf s32z280_demo_module_bin)
+
+ target_link_options(s32z280_module.elf PRIVATE
+ -T${MOD_EVB}/link_module.lds
+ -nostartfiles
+ -Wl,-Map=s32z280_module.map
+ ${EVB_LINK_QUIET_RWX}
+ )
+
+ target_link_libraries(s32z280_module.elf PRIVATE threadx_module)
+
+endif()
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/board.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/board.h
new file mode 100644
index 00000000..8c520d65
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/board.h
@@ -0,0 +1,145 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* board.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Interface between the S32Z280 reset path and the C code it enters, */
+/* plus the layout of the structure a debugger reads to confirm what */
+/* the core reported about itself. */
+/* */
+/* Field order here is load-bearing: the debug script reads the */
+/* structure by offset, so members must not be reordered without */
+/* updating tools/read_identity.gdb alongside. */
+/* */
+/**************************************************************************/
+
+#ifndef BOARD_H
+#define BOARD_H
+
+/* Written to r52_identity.magic only after every other field is set, so a
+ reader can tell a complete structure from a half-filled one. */
+
+#define R52_IDENTITY_MAGIC 0x52520001UL
+
+/* What the core reports about itself. EL2-only registers are captured by
+ entry.S before the drop to EL1; the rest are read in bsp_main. */
+
+typedef struct r52_identity_struct
+{
+ unsigned int magic;
+ unsigned int midr; /* expect part 0xD13 = Cortex-R52 */
+ unsigned int mpuir; /* EL1 MPU, DREGION in bits [15:8] */
+ unsigned int ctr;
+ unsigned int mpidr;
+ unsigned int id_pfr0;
+ unsigned int id_pfr1; /* virtualisation field => EL2 */
+ unsigned int sctlr_el1; /* MPU and cache enables as found */
+ unsigned int hmpuir_el2; /* EL2 MPU, captured at EL2 */
+ unsigned int cntfrq_el2; /* recorded, not programmed */
+ unsigned int cpsr_el2; /* mode bits should read 0x1A, Hyp */
+ unsigned int cpsr_el1; /* mode bits should read 0x13, SVC */
+ unsigned int mpu_regions_el1; /* decoded from mpuir */
+ unsigned int console_status; /* linflexd_init() result, 0 = OK */
+} r52_identity_t;
+
+extern r52_identity_t r52_identity;
+
+/* Entered from entry.S once EL1 is set up; never returns. Declared here to
+ satisfy MISRA Rule 8.4, which entry.S cannot do for itself. */
+
+void bsp_main(void);
+
+/* Breakpoint target for the debug script; reached once r52_identity is
+ complete. Declared here so it has a prototype (MISRA Rule 8.4). */
+
+void bsp_done(void);
+
+/* SCTLR as it reads after mpu_init(); M should be set. */
+
+unsigned int read_sctlr_after_mpu(void);
+
+/* Called from el1_irq_entry in entry.S; declared here to satisfy MISRA
+ Rule 8.4, which assembly cannot do for itself. */
+
+void board_irq_handler(void);
+
+/* The nesting path splits the handler in three: the acknowledge happens in IRQ
+ mode before nesting starts, and the end-of-interrupt after it finishes.
+ board_irq_service does the middle part on an already-acknowledged INTID and
+ neither acknowledges nor EOIs. */
+
+void board_irq_service(unsigned long intid);
+
+/* Called from _tx_initialize_low_level when the kernel is linked. */
+
+void board_init(void);
+
+/* Branches to a non-executable address to provoke a prefetch abort. Returns
+ via the abort handler, not normally. */
+
+void try_execute_data(unsigned long address);
+
+/* Interrupt observations, written by the handler. */
+
+extern volatile unsigned long board_irq_count;
+extern volatile unsigned long board_timer_intid;
+extern volatile unsigned long board_spurious_count;
+extern volatile unsigned long board_unexpected_intid;
+extern volatile unsigned long board_first_intid;
+
+/* Nesting observability. Inert unless the image was built with
+ TX_ENABLE_IRQ_NESTING: without it the handler runs in IRQ mode with
+ interrupts masked and can never be re-entered. */
+
+extern volatile unsigned long board_nest_depth;
+extern volatile unsigned long board_nest_max;
+extern volatile unsigned long board_sgi_count;
+extern volatile unsigned long board_sgi_nested_count;
+extern volatile unsigned long board_nest_provoke;
+extern volatile unsigned long board_priority_bits;
+extern volatile unsigned long board_timer_priority;
+extern volatile unsigned long board_sgi_priority;
+
+#define BOARD_NEST_SGI_INTID 8U
+
+/* FIQ observability. Two Group 0 SGIs, because FIQ nesting means an FIQ taken
+ while an FIQ handler is running, so a single source cannot show it. The
+ higher-priority one is raised from inside the lower one's handler. */
+
+void board_fiq_handler(void);
+void board_fiq_service(unsigned long intid);
+
+extern volatile unsigned long board_fiq_count;
+extern volatile unsigned long board_fiq_high_count;
+extern volatile unsigned long board_fiq_nested_count;
+extern volatile unsigned long board_fiq_depth;
+extern volatile unsigned long board_fiq_max_depth;
+extern volatile unsigned long board_fiq_unexpected;
+extern volatile unsigned long board_fiq_provoke;
+
+#define BOARD_FIQ_LOW_INTID 10U
+#define BOARD_FIQ_HIGH_INTID 11U
+
+#endif /* BOARD_H */
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/bsp_boot.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/bsp_boot.c
new file mode 100644
index 00000000..13ea498b
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/bsp_boot.c
@@ -0,0 +1,1037 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* bsp_boot.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* First-boot verification for the NXP S32Z280-594EVB. Records what */
+/* the core reports about itself into a structure a debugger reads. */
+/* */
+/* There is no console on this board yet: LIN9 reaches the host through*/
+/* the daughtercard USB-UART, but its baud rate depends on clock */
+/* configuration that is not yet established, and a console that prints*/
+/* at the wrong rate produces garbage indistinguishable from a crash. */
+/* So this milestone reports through memory instead, which cannot be */
+/* mis-configured. The console arrives in the next step. */
+/* */
+/* Only EL1-accessible registers are read here. EL2-only ones */
+/* (HMPUIR, CNTFRQ) are captured by entry.S before the drop to EL1, */
+/* because reading them at EL1 would trap rather than return a value. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Dir 4.3 -- assembly language shall be encapsulated and isolated. */
+/* Each MRC is wrapped in its own single-statement static function */
+/* below, which is the encapsulation the directive asks for. There */
+/* is no other way to read a coprocessor register from C. */
+/* */
+/* Rule 8.4 -- bsp_main is called from assembly and so has no C caller */
+/* to provide a prototype; it is declared in board.h, which entry.S */
+/* cannot include. */
+/* */
+/**************************************************************************/
+
+#include "board.h"
+#include "platform.h"
+#include "linflexd.h"
+#include "timer.h"
+#include "gic_probe.h"
+#include "mpu.h"
+#include "gicv3.h"
+#include "cache.h"
+#include "tcm.h"
+
+/* Captured at EL2 by entry.S, before the drop to EL1. */
+
+extern unsigned int boot_stage;
+extern unsigned int cpsr_at_el2;
+extern unsigned int cpsr_at_el1;
+extern unsigned int cntfrq_at_el2;
+extern unsigned int hmpuir_at_el2;
+extern unsigned int probe_stage;
+extern unsigned int fault_expected;
+extern unsigned int fault_taken;
+
+/* Mirror of the console markers into memory. The UART has twice gone away at
+ the moment a marker mattered -- once when a stalled access took the debug
+ connection down with it, once when the USB passthrough detached -- and a
+ stalled access records no fault, so boot_stage alone cannot say how far the
+ risky sequence got. This can be read post-mortem without a console. */
+
+#define MARK(n) do { probe_stage = (unsigned int)(n); } while (0)
+
+/* Filled in below and read by the debugger. Not static: the whole point is
+ for something outside this translation unit to find it. */
+
+r52_identity_t r52_identity;
+
+
+/* One MRC per function, each isolated, per MISRA Dir 4.3. */
+
+static unsigned int read_midr(void)
+{
+ unsigned int value;
+ __asm volatile ("mrc p15, 0, %0, c0, c0, 0" : "=r" (value));
+ return value;
+}
+
+static unsigned int read_mpuir(void)
+{
+ unsigned int value;
+ __asm volatile ("mrc p15, 0, %0, c0, c0, 4" : "=r" (value));
+ return value;
+}
+
+static unsigned int read_ctr(void)
+{
+ unsigned int value;
+ __asm volatile ("mrc p15, 0, %0, c0, c0, 1" : "=r" (value));
+ return value;
+}
+
+static unsigned int read_mpidr(void)
+{
+ unsigned int value;
+ __asm volatile ("mrc p15, 0, %0, c0, c0, 5" : "=r" (value));
+ return value;
+}
+
+static unsigned int read_id_pfr0(void)
+{
+ unsigned int value;
+ __asm volatile ("mrc p15, 0, %0, c0, c1, 0" : "=r" (value));
+ return value;
+}
+
+static unsigned int read_id_pfr1(void)
+{
+ unsigned int value;
+ __asm volatile ("mrc p15, 0, %0, c0, c1, 1" : "=r" (value));
+ return value;
+}
+
+static unsigned int read_sctlr(void)
+{
+ unsigned int value;
+ __asm volatile ("mrc p15, 0, %0, c1, c0, 0" : "=r" (value));
+ return value;
+}
+
+
+/**************************************************************************/
+/* report -- one "NAME = VALUE" line on the console. */
+/**************************************************************************/
+
+static void report(const char *name, unsigned int value)
+{
+ linflexd_puts(name);
+ linflexd_puts(" = 0x");
+ linflexd_put_hex32(value);
+ linflexd_putc('\n');
+}
+
+
+/**************************************************************************/
+/* bsp_done -- breakpoint target, reached once the structure is complete.*/
+/* */
+/* Deliberately not static and deliberately not inlined: it exists purely*/
+/* so a debugger can break on a symbol. An optimising build would remove*/
+/* an empty static function, and then the script would have nothing to */
+/* attach to. */
+/**************************************************************************/
+
+/* Clear CPSR.I so the GIC can deliver to this core. Interrupts stay masked
+ from reset all the way to here on purpose: nothing before this point is
+ prepared to service one. */
+
+static void enable_irq_at_el1(void)
+{
+ __asm volatile ("cpsie i" ::: "memory");
+ __asm volatile ("isb");
+}
+
+
+/* Cache benchmark.
+ *
+ * Two things have to be right for a cache to show a measurable effect, and the
+ * first version of this test got both wrong. It used a 4 KB static buffer in
+ * DRAM0, a full-core-speed bank, and reported a 0.036% difference -- noise --
+ * as a pass. A bank running at core speed leaves a cache almost nothing to
+ * save. DRAM2 runs at half core speed, which is why the buffer lives there.
+ *
+ * 1. The memory must be slower than the core. This uses the extended SRAM at
+ * S32Z_DRAM2_BASE, the RTU data bank that runs at half the core
+ * frequency, so caching it has something to show.
+ *
+ * 2. The working set must fit in the cache and be revisited. It is sized
+ * from CCSIDR at run time -- half the reported L1 data cache -- rather
+ * than from a guessed constant. A set larger than the cache would stream
+ * and evict, and would show little benefit even over slow memory.
+ *
+ * Reported alongside the timings so the numbers can be judged, not just
+ * believed.
+ */
+
+/* Decimal, because a measurement read by a person should not need converting.
+ report() stays hex for register values, where hex is the right form. */
+
+static void report_dec(unsigned long value)
+{
+ char digits[12];
+ unsigned int n = 0U;
+
+ if (value == 0UL) { linflexd_putc('0'); return; }
+
+ while ((value > 0UL) && (n < 12U))
+ {
+ digits[n] = (char) ('0' + (value % 10UL));
+ value /= 10UL;
+ n++;
+ }
+ while (n > 0U) { n--; linflexd_putc(digits[n]); }
+}
+
+static void report_dec_small(unsigned int value)
+{
+ report_dec((unsigned long) value);
+}
+
+#define BUFFER_BASE S32Z_DRAM2_BASE
+
+static unsigned long cache_bench_words;
+
+/* Aligned to a cache line, and that alignment is load-bearing rather than
+ cosmetic. Without it this measurement is bimodal: it reports either about
+ 24% cache benefit or none at all, decided purely by where the loop happens
+ to fall relative to a 64-byte line, and therefore by any unrelated change
+ that shifts code ahead of it. Adding two nop instructions to the startup
+ file was enough to flip it, which is how a run of conclusions drawn from
+ this probe -- including a defect report to NXP -- turned out to be measuring
+ code layout rather than the part.
+
+ Pinning the entry point does not make the workload fast or slow, it makes it
+ the same across builds, which is the only property that lets two numbers be
+ compared. */
+
+/* Four copies of the same loop, each 64-byte aligned and then displaced by a
+ different offset within the line. The measurement runs all four and reports
+ the spread.
+
+ One copy is not enough, and a single number from this workload is actively
+ misleading. Unpinned, it reported either about 24% cache benefit or none at
+ all depending on where the loop fell relative to a 64-byte line -- two nop
+ instructions added to the startup file flipped it. Pinning the entry to a
+ line does not fix that, it just picks one of the two modes and hides the
+ other: aligned to 64 the loop sits permanently in the low mode.
+
+ The variation is real behaviour, not noise. Both modes reproduce exactly
+ across runs. The cold pass runs with both caches off, so it is heavily
+ instruction-fetch bound out of code RAM at half the core frequency, and how
+ the loop straddles lines decides how much of the data cache's contribution
+ is visible at all. Reporting min and max across alignments says that out
+ loud instead of letting one arbitrary placement stand in for the part. */
+
+#define CACHE_WORKLOAD_COPIES 4U
+
+#define MAKE_CACHE_WORKLOAD(name, pad_words) \
+__attribute__((aligned(64), noinline)) \
+static void name(volatile unsigned int *buffer) \
+{ \
+ unsigned long pass; \
+ unsigned long i; \
+ \
+ __asm__ volatile(".rept " #pad_words "\n\tnop\n\t.endr"); \
+ \
+ for (pass = 0UL; pass < 32UL; pass++) \
+ { \
+ for (i = 0UL; i < cache_bench_words; i++) \
+ { \
+ buffer[i] = buffer[i] + i + pass; \
+ } \
+ } \
+}
+
+MAKE_CACHE_WORKLOAD(cache_workload_a, 0)
+MAKE_CACHE_WORKLOAD(cache_workload_b, 4)
+MAKE_CACHE_WORKLOAD(cache_workload_c, 8)
+MAKE_CACHE_WORKLOAD(cache_workload_d, 12)
+
+static void (*const cache_workloads[CACHE_WORKLOAD_COPIES])(volatile unsigned int *) =
+{
+ cache_workload_a, /* loop at line offset 0 */
+ cache_workload_b, /* loop at line offset 16 */
+ cache_workload_c, /* loop at line offset 32 */
+ cache_workload_d /* loop at line offset 48 */
+};
+
+static void cache_workload(void)
+{
+ cache_workloads[0]((volatile unsigned int *) BUFFER_BASE);
+}
+
+
+unsigned int fault_syndrome_value(void)
+{
+ extern unsigned int fault_syndrome;
+ return fault_syndrome;
+}
+
+
+unsigned int fault_address_value(void)
+{
+ extern unsigned int fault_address;
+ return fault_address;
+}
+
+
+unsigned int read_sctlr_after_mpu(void)
+{
+ return read_sctlr();
+}
+
+
+void bsp_done(void)
+{
+ __asm volatile ("nop");
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 (Supervisor) from entry.S. Never returns. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ /* Written last, so a debugger that finds the magic knows every other
+ field is already valid. A partially filled structure would otherwise
+ be indistinguishable from a complete one. */
+
+ r52_identity.magic = 0U;
+
+ r52_identity.midr = read_midr();
+ r52_identity.mpuir = read_mpuir();
+ r52_identity.ctr = read_ctr();
+ r52_identity.mpidr = read_mpidr();
+ r52_identity.id_pfr0 = read_id_pfr0();
+ r52_identity.id_pfr1 = read_id_pfr1();
+ r52_identity.sctlr_el1 = read_sctlr();
+
+ r52_identity.hmpuir_el2 = hmpuir_at_el2;
+ r52_identity.cntfrq_el2 = cntfrq_at_el2;
+ r52_identity.cpsr_el2 = cpsr_at_el2;
+ r52_identity.cpsr_el1 = cpsr_at_el1;
+
+ /* Derived counts, so the debugger script does not have to know where the
+ field sits in each register. MPUIR reports DREGION in bits [15:8].
+ HMPUIR is recorded raw as well, because on this part the count appears
+ in bits [7:0] instead and that layout still needs confirming against
+ the Armv8-R specification. */
+
+ r52_identity.mpu_regions_el1 = (r52_identity.mpuir >> 8) & 0xFFU;
+
+ r52_identity.magic = R52_IDENTITY_MAGIC;
+
+ /* Report over the console as well as through memory. The memory copy
+ stays: it is what proved the boot path before a console existed, and it
+ still works if the console itself is misconfigured. */
+
+ r52_identity.console_status = linflexd_init();
+ linflexd_puts("\n=== ThreadX Cortex-R52 :: NXP S32Z280-594EVB ===\n");
+ report("MIDR ", r52_identity.midr);
+ report("MPUIR ", r52_identity.mpuir);
+ report("HMPUIR ", r52_identity.hmpuir_el2);
+ report("CTR ", r52_identity.ctr);
+ report("MPIDR ", r52_identity.mpidr);
+ report("ID_PFR0 ", r52_identity.id_pfr0);
+ report("ID_PFR1 ", r52_identity.id_pfr1);
+ report("SCTLR ", r52_identity.sctlr_el1);
+ report("CNTFRQ ", r52_identity.cntfrq_el2);
+ report("CPSR@EL2 ", r52_identity.cpsr_el2);
+ report("CPSR@EL1 ", r52_identity.cpsr_el1);
+ linflexd_puts("EL1 MPU regions: ");
+ linflexd_put_hex32(r52_identity.mpu_regions_el1);
+ linflexd_putc('\n');
+ report("CONSOLE ", r52_identity.console_status);
+
+ /* --- generic timer ----------------------------------------------------
+ CNTFRQ is now programmed at EL2 (8 MHz, measured and cross-checked
+ against CFG_CNTDV and FXOSC). Prove the timer end to end: read it back,
+ then arm a one-second one-shot and poll for it, bracketed by CNTPCT
+ samples. If the counter and the compare agree, the elapsed count is
+ CNTFRQ; if the host also sees one second pass, the rate itself is right
+ rather than merely self-consistent.
+
+ Interrupt stays masked -- no GIC yet. Polling first means a later
+ interrupt problem cannot be confused with the timer being wrong. */
+
+ report("CNTFRQ ", timer_read_cntfrq());
+
+ {
+ unsigned long long before;
+ unsigned long long after;
+ unsigned int hz = timer_read_cntfrq();
+
+ /* Sample AFTER the message, not before: at 115200 the line itself is
+ about 2 ms, and sampling first puts that inside the measurement. */
+
+ linflexd_puts("TIMER arming 1s one-shot\n");
+ before = timer_read_cntpct();
+ timer_start_oneshot(hz);
+
+ while (timer_fired() == 0U)
+ {
+ /* poll */
+ }
+
+ after = timer_read_cntpct();
+ timer_stop();
+
+ linflexd_puts("TIMER fired, elapsed counts = 0x");
+ linflexd_put_hex32((unsigned int)((after - before) >> 32));
+ linflexd_put_hex32((unsigned int)(after - before));
+ linflexd_puts("\n");
+ }
+
+ /* --- where is the GIC, and is the port to it open? ---------------------
+ System-register reads only: a load from the GIC window stalls the core,
+ so nothing here touches memory. */
+
+ report("IMP_CBAR ", gic_read_cbar());
+ report("PERIPHPRG", gic_read_periphpregionr());
+
+ /* The peripheral port is now enabled at EL2, so RTU peripheral space
+ should be reachable. Marker first: if this still stalls, the run dies
+ here and says so. */
+
+ linflexd_puts("P1 RTU0.GPR CFG_CNTDV via LLPP\n");
+ report("CFG_CNTDV", *(volatile unsigned int *)S32Z_RTU0_GPR_CFG_CNTDV);
+
+ /* --- TCM: what the hardware reports about itself ---------------------
+ Reads only. platform.h has recorded since bring-up that the TCM windows
+ fault at reset, and the Cortex-R52 TRM says why: every bit of the region
+ registers resets to zero apart from SIZE and WAITSTATES, so the banks sit
+ at base 0 with both enables clear. This confirms that on the part rather
+ than trusting it, and cross-checks the reported sizes against the S32Z2
+ reference manual, which documents TCMA 64KB, TCMB 16KB and TCMC 16KB.
+
+ Nothing is written here. Enabling TCM also requires initialising the ECC
+ bits before any read (TRM 6.2.2), with ATCM needing 64-bit aligned stores
+ where BTCM and CTCM accept 32-bit, so the enable belongs in its own step
+ once these values are known. */
+
+ linflexd_puts("T1 TCM configuration as reported by the core\n");
+ {
+ static const char *const bank_name[3] = { "ATCM", "BTCM", "CTCM" };
+ static const unsigned long expect_bytes[3] = { 0x10000UL, 0x4000UL, 0x4000UL };
+ unsigned int i;
+
+ for (i = 0U; i < 3U; i++)
+ {
+ tcm_bank_t bank;
+
+ tcm_read_bank(i, &bank);
+
+ linflexd_puts(bank_name[i]);
+ linflexd_puts(" raw=0x");
+ linflexd_put_hex32((unsigned int) bank.raw);
+ linflexd_puts(" base=0x");
+ linflexd_put_hex32((unsigned int) bank.base);
+ linflexd_puts(" size=0x");
+ linflexd_put_hex32((unsigned int) bank.size_bytes);
+ linflexd_puts(" ws=");
+ linflexd_putc((char) ('0' + (char) bank.wait_states));
+ linflexd_puts(" en2=");
+ linflexd_putc((char) ('0' + (char) bank.enable_el2));
+ linflexd_puts(" en10=");
+ linflexd_putc((char) ('0' + (char) bank.enable_el10));
+ linflexd_puts((bank.size_bytes == expect_bytes[i])
+ ? " size matches the RM\n"
+ : " SIZE DISAGREES WITH THE RM\n");
+ }
+
+ report("MEMPROTCTLR", (unsigned int) tcm_read_memprotctlr());
+ linflexd_puts("ECC implemented = ");
+ linflexd_putc((char) ('0' + (char) tcm_ecc_implemented()));
+ linflexd_puts(", enabled = ");
+ linflexd_putc((char) ('0' + (char) tcm_ecc_enabled()));
+ linflexd_putc('\n');
+ }
+
+ /* --- TCM: enable, preload, prove ------------------------------------
+ Done before the MPU goes on, so a failure here is not confused with a
+ missing MPU region. The preload is not optional: ECC is enabled on this
+ part, so a location read before it has been written reports an error, and
+ that error would look exactly like an inaccessible TCM. */
+
+ MARK(0x20);
+ linflexd_puts("T2 TCM state as entry.S left it\n");
+ {
+ static const char *const bank_name[3] = { "ATCM", "BTCM", "CTCM" };
+ unsigned int i;
+ unsigned int atcm_ok;
+
+ /* Verification only. Nothing is programmed here: ENABLEEL2 is ignored
+ when written from EL1, so TCM setup belongs at EL2 in entry.S, and
+ calling tcm_enable() from here would additionally switch on the banks
+ entry.S deliberately leaves off. */
+
+ for (i = 0U; i < 3U; i++)
+ {
+ tcm_bank_t bank;
+
+ tcm_read_bank(i, &bank);
+
+ linflexd_puts(bank_name[i]);
+ linflexd_puts(" base=0x");
+ linflexd_put_hex32((unsigned int) bank.base);
+ linflexd_puts(" en2=");
+ linflexd_putc((char) ('0' + (char) bank.enable_el2));
+ linflexd_puts(" en10=");
+ linflexd_putc((char) ('0' + (char) bank.enable_el10));
+ linflexd_puts((bank.enable_el10 != 0U)
+ ? " enabled\n"
+ : " off on purpose, see entry.S\n");
+ }
+
+ tcm_read_bank(0U, &(tcm_bank_t){0});
+ {
+ tcm_bank_t atcm;
+
+ tcm_read_bank(0U, &atcm);
+ atcm_ok = ((atcm.base == S32Z_ATCM_BASE) &&
+ (atcm.enable_el2 != 0U) && (atcm.enable_el10 != 0U)) ? 1U : 0U;
+ }
+
+ if (atcm_ok != 0U)
+ {
+ MARK(0x21);
+ linflexd_puts("T3 preloading ATCM to establish ECC check bits\n");
+ tcm_preload(0U, S32Z_ATCM_BASE, S32Z_ATCM_SIZE);
+
+ /* BTCM too, now that entry.S enables it. Whole bank: nothing is
+ placed there at link time, and with ECC on, a location must be
+ written before it can be read. BTCM accepts 32-bit stores where
+ ATCM needs 64-bit (TRM 6.2.2); tcm_preload handles the
+ difference. */
+
+ linflexd_puts("T3b preloading BTCM\n");
+ tcm_preload(1U, S32Z_BTCM_BASE, S32Z_BTCM_SIZE);
+
+ MARK(0x22);
+ linflexd_puts("T4 write and read back ATCM\n");
+ {
+ volatile unsigned int *first = (volatile unsigned int *) S32Z_ATCM_BASE;
+ volatile unsigned int *last =
+ (volatile unsigned int *) (S32Z_ATCM_BASE + S32Z_ATCM_SIZE - 4UL);
+
+ *first = 0xA5A5A5A5U;
+ *last = 0x5A5A5A5AU;
+
+ linflexd_puts(((*first == 0xA5A5A5A5U) && (*last == 0x5A5A5A5AU))
+ ? "ATCM PASS first and last word hold\n"
+ : "ATCM FAIL readback mismatch\n");
+ }
+ }
+ else
+ {
+ linflexd_puts("T3 skipped: ATCM is not enabled as expected\n");
+ }
+ }
+
+ /* --- MPU, then the GIC ------------------------------------------------
+ The GIC needs its region mapped Device nGnRnE (Cortex-R52 TRM), which
+ needs the MPU on. Markers around the enable: if the console survives it,
+ the UART region is right; if it does not, that is the answer. */
+
+ MARK(0x10);
+ linflexd_puts("M1 enabling MPU\n");
+ {
+ unsigned int regions = mpu_init();
+
+ MARK(0x11);
+
+ /* Read back what the region programming actually did. Five regions die on
+ the SCTLR.M write while three survive, and the difference is the region
+ COUNT rather than the Device attribute -- so the question is whether
+ PRSELR/PRBAR/PRLAR programming takes effect at all for indices 3 and 4,
+ which nothing has ever exercised: the FVP example this code came from
+ only ever programs three regions. The MPU is not enabled in this build,
+ so this cannot stall. */
+
+ linflexd_puts("R idx PRBAR PRLAR\n");
+ {
+ unsigned int i;
+ for (i = 0U; i < 8U; i++)
+ {
+ unsigned long prbar = 0UL;
+ unsigned long prlar = 0UL;
+
+ mpu_read_region(i, &prbar, &prlar);
+ linflexd_puts("R ");
+ linflexd_put_hex32(i);
+ linflexd_puts(" ");
+ linflexd_put_hex32((unsigned int) prbar);
+ linflexd_puts(" ");
+ linflexd_put_hex32((unsigned int) prlar);
+ linflexd_putc('\n');
+ }
+ }
+
+
+ report("MPU rgns ", regions);
+ report("SCTLR ", read_sctlr_after_mpu());
+ MARK(0x12);
+ linflexd_puts("M2 MPU enabled, console alive\n");
+
+ linflexd_puts("M3 GICD_PIDR2 (was stalling before the MPU)\n");
+ MARK(0x13);
+ report("GICD_PIDR2", *(volatile unsigned int *)(S32Z_GIC_BASE + 0xFFE8UL));
+ MARK(0x14);
+ report("GICD_TYPER", *(volatile unsigned int *)(S32Z_GIC_BASE + 0x0004UL));
+ MARK(0x15);
+ report("GICR_PIDR2", *(volatile unsigned int *)(S32Z_GIC_BASE + 0x10FFE8UL));
+ MARK(0x16);
+ linflexd_puts("M4 GIC reachable\n");
+
+ /* TCM again, now that the MPU is enforcing. The TRM requires an MPU
+ region per TCM before it can be used, so this is the check that those
+ three regions are right -- an enabled TCM with no region faults here
+ and nowhere earlier. */
+
+ MARK(0x23);
+ linflexd_puts("T5 ATCM still reachable with the MPU on\n");
+ {
+ volatile unsigned int *p = (volatile unsigned int *) S32Z_ATCM_BASE;
+ tcm_bank_t atcm;
+ unsigned int answers;
+
+ tcm_read_bank(0U, &atcm);
+
+ *p = 0x5A5A0000U;
+ answers = (*p == 0x5A5A0000U) ? 1U : 0U;
+
+ /* Both halves matter. A disabled TCM's range is serviced through
+ AXIM, so memory answering there says nothing about the TCM: an
+ earlier version of this check reported every bank accessible while
+ two of them were disabled. */
+
+ if ((answers != 0U) && (atcm.enable_el10 != 0U))
+ {
+ linflexd_puts("ATCM PASS enabled and holding data under MPU\n");
+ }
+ else if (answers != 0U)
+ {
+ linflexd_puts("ATCM NOT TCM: address answers but the bank is\n"
+ " disabled, so this is AXIM behind it\n");
+ }
+ else
+ {
+ linflexd_puts("ATCM FAIL address does not answer\n");
+ }
+ }
+
+ /* --- interrupts -------------------------------------------------
+ Bring up the GIC properly, enable the timer PPI, arm a 10 ms
+ one-shot and unmask IRQ at EL1. Then wait and see whether the
+ handler runs. Polling proved the timer counts and fires; this
+ proves the interrupt is delivered, which is a different claim. */
+
+ MARK(0x50);
+ /* Copy the handler into ATCM now, and not earlier: T4 and T5 write test
+ patterns to the first and last words of the bank, which would land on top
+ of this code. The ECC preload in T3 has already established check bits
+ for the whole bank, and the 64-bit stores in the copy maintain them for
+ every location it writes. */
+
+ linflexd_puts("T6 copying .atcm_text into ATCM\n");
+ {
+ unsigned long copied = tcm_copy_atcm_text();
+
+ report("atcm bytes", (unsigned int) copied);
+ if (copied == 0UL)
+ {
+ linflexd_puts(" nothing placed in ATCM in this image\n");
+ }
+ }
+
+ timer_cycles_enable();
+
+ linflexd_puts("I1 gicv3_init\n");
+ gicv3_init();
+
+ MARK(0x51);
+ /* Enable the WHOLE PPI range rather than just 30. All three counters
+ reading zero means nothing reached the CPU interface, and an enabled
+ but wrong INTID produces exactly that silence -- so let the hardware
+ say which PPI the timer actually drives. This is how INTID 30 was
+ established on the FVP too: assume nothing, record what arrives. */
+
+ linflexd_puts("I2 enable PPIs 16..31\n");
+ {
+ unsigned int ppi;
+ for (ppi = 16U; ppi <= 31U; ppi++)
+ {
+ gicv3_enable_ppi(ppi, 0xA0U);
+ }
+ }
+
+ MARK(0x52);
+ linflexd_puts("I3 arm timer, unmask IRQ\n");
+ timer_start_oneshot_irq(timer_read_cntfrq() / 100U); /* 10 ms */
+ enable_irq_at_el1();
+
+ MARK(0x53);
+ {
+ extern unsigned int board_service_count;
+ unsigned int spins = 0U;
+
+ /* Wait for ticks. Bounded: a missing interrupt must report
+ itself rather than hang the boot. */
+
+ /* Wait for a full set of latency samples, not just a few ticks:
+ the figure of interest is the worst case, and a handful of
+ samples cannot show it. Still bounded, so a missing interrupt
+ reports itself rather than hanging the boot. */
+
+ while ((board_service_count < 64U) && (spins < 400U))
+ {
+ timer_spin(200000U);
+ spins++;
+ }
+ }
+
+ MARK(0x54);
+ report("IRQ count", (unsigned int) board_irq_count);
+ report("timer INTID", (unsigned int) board_timer_intid);
+ report("spurious ", (unsigned int) board_spurious_count);
+ report("unexpected", (unsigned int) board_unexpected_intid);
+
+ /* Handler body execution time. Reported as min, max and mean rather
+ than mean alone: the spread is the point. */
+
+ {
+ extern unsigned int board_service_cycles[];
+ extern unsigned int board_service_count;
+
+ unsigned int i;
+ unsigned int lo = 0xFFFFFFFFU;
+ unsigned int hi = 0U;
+ unsigned long sum = 0UL;
+ unsigned int n;
+
+ /* Stop the source and snapshot the count before reading any of it.
+ The timer re-arms inside the handler, so samples keep arriving
+ otherwise: the first version of this read let the count grow
+ between computing the extremes and dividing for the mean, and
+ printed a mean below the minimum. */
+
+ timer_stop();
+ n = board_service_count;
+
+ for (i = 0U; i < n; i++)
+ {
+ unsigned int v = board_service_cycles[i];
+
+ if (v < lo) { lo = v; }
+ if (v > hi) { hi = v; }
+ sum += (unsigned long) v;
+ }
+
+ linflexd_puts("I5 handler body cycles, four placements ");
+#ifdef TX_R52_ATCM_ISR
+ linflexd_puts("(bodies in ATCM)\n");
+#else
+ linflexd_puts("(bodies in code RAM)\n");
+#endif
+ report("samples ", n);
+
+ /* Per placement, because the aggregate hides exactly the thing that
+ invalidated the previous version of this comparison. 16 samples
+ each, in the order the wrapper rotated through them. */
+
+ {
+ unsigned int slot;
+ unsigned int each = 64U / 4U;
+
+ for (slot = 0U; slot < 4U; slot++)
+ {
+ unsigned int s_lo = 0xFFFFFFFFU;
+ unsigned int s_hi = 0U;
+ unsigned long s_sum = 0UL;
+ unsigned int c = 0U;
+ unsigned int m;
+
+ for (m = slot * each; (m < ((slot + 1U) * each)) && (m < n); m++)
+ {
+ unsigned int v = board_service_cycles[m];
+
+ if (v < s_lo) { s_lo = v; }
+ if (v > s_hi) { s_hi = v; }
+ s_sum += (unsigned long) v;
+ c++;
+ }
+
+ linflexd_puts(" offset ");
+ report_dec((unsigned long) (slot * 16U));
+ linflexd_puts(": ");
+ if (c == 0U)
+ {
+ linflexd_puts("no samples\n");
+ }
+ else
+ {
+ linflexd_puts("min ");
+ report_dec((unsigned long) s_lo);
+ linflexd_puts(" max ");
+ report_dec((unsigned long) s_hi);
+ linflexd_puts(" mean ");
+ report_dec(s_sum / c);
+ linflexd_puts("\n");
+ }
+ }
+ }
+ }
+ report("first INTID", (unsigned int) board_first_intid);
+ linflexd_puts("I4 interrupt test done\n");
+ }
+
+ /* --- GIC: not reachable yet, probe deliberately removed ----------------
+ The GIC window NXP's memory map gives for this cluster, 0x47800000, is
+ unreachable from BOTH paths in the current state. The debugger cannot
+ read any of it, and a load from the core stalls the core outright: the
+ run dies at the marker before the access, with no fault recorded and the
+ debug connection dropped -- the same signature as RTU0.GPR.
+
+ The Reference Manual contains no GIC base address anywhere, so that
+ address rests entirely on NXP's debugger script. What the manual does
+ mention is the R52's "Arm low-latency peripheral port". On Cortex-R52
+ the LLPP is an implementation-defined region that software programs and
+ enables; if it is disabled out of reset, or covers a different base,
+ accesses to a peripheral in that window stall exactly like this.
+ Settling it needs the Cortex-R52 TRM, which documents the region
+ registers -- NXP's manual does not.
+
+ The probe is removed rather than left disabled: while it was present the
+ image stalled here every run, which costs the timer and console results
+ that come before it. */
+
+ /* --- caches ------------------------------------------------------------
+ Enabled last, and measured rather than merely switched on. Reading
+ SCTLR back proves a bit was set; timing the same workload before and
+ after proves the caches are actually doing something. The generic timer
+ supplies the clock -- 8 MHz, established earlier -- so the comparison is
+ in real units rather than loop counts. */
+
+ MARK(0x70);
+ report("CLIDR ", (unsigned int) cache_read_clidr());
+ report("D line ", (unsigned int) cache_dcache_line_bytes());
+ report("D ways ", (unsigned int) cache_dcache_ways());
+ report("D sets ", (unsigned int) cache_dcache_sets());
+ report("D bytes ", (unsigned int) cache_dcache_bytes());
+
+ /* Half the data cache, in words: fits comfortably and is revisited every
+ pass, which is the case a cache is meant to win. */
+
+ cache_bench_words = cache_dcache_bytes() / (2UL * sizeof(unsigned int));
+ report("bench wds", (unsigned int) cache_bench_words);
+
+ /* Measured at four loop alignments, not one, and the spread is reported
+ because it is large and real. See the comment on the workload copies. */
+
+ /* Three memories, four loop alignments each. The memories are the point:
+ DRAM2 runs at half the core frequency, DRAM0 at full, and BTCM at full
+ with zero wait states and no cache in the path at all -- an enabled TCM is
+ Non-cacheable Non-shareable Normal memory whatever the MPU says.
+
+ The alignment sweep is what makes the three comparable. A single figure
+ per memory would be picking one of two modes per memory and calling it a
+ result, which is how this example previously produced a defect report that
+ had to be withdrawn. */
+
+ linflexd_puts("C1 timing three memories, four loop alignments each\n");
+ {
+ static const struct
+ {
+ const char *name;
+ unsigned long base;
+ } memories[3] =
+ {
+ { "DRAM2 half-speed", S32Z_DRAM2_BASE },
+ { "DRAM0 full-speed", S32Z_DRAM0_BASE + 0x10000UL },
+ { "BTCM 0 wait ", S32Z_BTCM_BASE }
+ };
+
+ unsigned long overall_hi = 0UL;
+ unsigned int m;
+
+ for (m = 0U; m < 3U; m++)
+ {
+ unsigned int k;
+
+ linflexd_puts(" ");
+ linflexd_puts(memories[m].name);
+ linflexd_puts(" at 0x");
+ linflexd_put_hex32((unsigned int) memories[m].base);
+ linflexd_puts("\n");
+
+ for (k = 0U; k < CACHE_WORKLOAD_COPIES; k++)
+ {
+ volatile unsigned int *buf =
+ (volatile unsigned int *) memories[m].base;
+ unsigned long long before;
+ unsigned long long after;
+ unsigned long cold;
+ unsigned long warm;
+ unsigned long gain;
+
+ cache_disable_all();
+
+ before = timer_read_cntpct();
+ cache_workloads[k](buf);
+ after = timer_read_cntpct();
+ cold = (unsigned long) (after - before);
+
+ cache_enable();
+
+ before = timer_read_cntpct();
+ cache_workloads[k](buf);
+ after = timer_read_cntpct();
+ warm = (unsigned long) (after - before);
+
+ gain = (cold > warm) ? (((cold - warm) * 1000UL) / cold) : 0UL;
+ if (gain > overall_hi) { overall_hi = gain; }
+
+ linflexd_puts(" offset ");
+ report_dec((unsigned long) (k * 16U));
+ linflexd_puts(": cold ");
+ report_dec(cold);
+ linflexd_puts(" warm ");
+ report_dec(warm);
+ linflexd_puts(" gain/1000 ");
+ report_dec(gain);
+ linflexd_puts("\n");
+ }
+ }
+
+ MARK(0x72);
+ if (cache_enabled() == 1U)
+ {
+ linflexd_puts("C3 PASS caches enabled (SCTLR.C and SCTLR.I set)\n");
+ }
+ else
+ {
+ linflexd_puts("C3 FAIL caches did not enable\n");
+ }
+
+ /* The claim is that the caches can help somewhere, which needs one
+ memory at one alignment to show it. BTCM is expected to show nothing:
+ it is not cached, so there is nothing for enabling the cache to
+ improve, and a zero there is the correct answer rather than a
+ failure. */
+
+ if (overall_hi >= 100UL)
+ {
+ linflexd_puts("C4 PASS caches measurably faster somewhere\n");
+ }
+ else
+ {
+ linflexd_puts("C4 no memory and alignment showed a 10% speedup\n");
+ }
+ }
+
+ /* --- protection test ---------------------------------------------------
+ The map claims code is read-only. Prove it: arm the recoverable abort
+ path, write to the code region, and check that a fault was actually
+ taken. Reading SCTLR back or listing the regions would only show what
+ was programmed; provoking the violation shows it is enforced.
+
+ This is worth doing now for a second reason: until SCTLR.TE was cleared
+ no fault handler could run at all, so no protection claim about this
+ board could be checked. */
+
+ MARK(0x60);
+ linflexd_puts("X1 write to read-only code region\n");
+ fault_taken = 0U;
+ fault_expected = 1U;
+
+ *(volatile unsigned int *)S32Z_CODE_SRAM_BASE = 0xDEADBEEFUL;
+
+ MARK(0x61);
+ report("faults ", fault_taken);
+ report("fault DFSR", fault_syndrome_value());
+ report("fault addr", fault_address_value());
+
+ if (fault_taken == 1U)
+ {
+ linflexd_puts("X2 PASS write to code faulted and was recovered\n");
+ }
+ else
+ {
+ linflexd_puts("X2 FAIL write to code did NOT fault -- code is writable\n");
+ }
+
+ /* --- second protection case: execute from data -------------------------
+ The map marks the data region execute-never. Prove that too: branch
+ into it and check a prefetch abort was taken. Separate from the
+ write-to-code case because they exercise different permissions and
+ different fault paths -- a map could plausibly enforce one and not the
+ other, and DFSR/IFSR come from different registers. */
+
+ MARK(0x62);
+ linflexd_puts("X3 execute from non-executable data region\n");
+ fault_taken = 0U;
+
+ try_execute_data(S32Z_DATA_SRAM_BASE);
+
+ MARK(0x63);
+ report("faults ", fault_taken);
+ report("fault IFSR", fault_syndrome_value());
+ report("fault addr", fault_address_value());
+
+ if (fault_taken == 1U)
+ {
+ linflexd_puts("X4 PASS execute from data faulted and was recovered\n");
+ }
+ else
+ {
+ linflexd_puts("X4 FAIL data region is executable\n");
+ }
+
+ /* Deliberate breakpoint target. The debug script sets a hardware
+ breakpoint here, so "the image ran to completion" is something the script
+ observes rather than infers from a timeout. */
+
+ bsp_done();
+
+ /* Park. The debugger halts the core and reads r52_identity.
+ A busy loop rather than WFI: this core's debug behaviour is still being
+ characterised, and a core sitting in a low-power state is one more
+ variable between "the code ran" and "the debugger sees it". The nop
+ also stops the compiler discarding a side-effect-free infinite loop. */
+
+ for (;;)
+ {
+ __asm volatile ("nop");
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/cache.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/cache.c
new file mode 100644
index 00000000..a7b0d6a1
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/cache.c
@@ -0,0 +1,346 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* cache.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Cache maintenance and enable for the S32Z280-594EVB. */
+/* */
+/* The FVP example enables SCTLR.C and SCTLR.I but only invalidates the*/
+/* instruction cache, with a note that a data-cache set/way sweep */
+/* belongs with silicon bring-up "where it can be verified against the */
+/* real cache geometry". This is that: the sweep here reads CLIDR and */
+/* CCSIDR and walks every set and way the hardware reports, rather than*/
+/* assuming a size. */
+/* */
+/* The sweep matters more here than on a model. This image is written */
+/* into SRAM by a debugger with the caches off, so nothing has been */
+/* allocated yet -- but the caches are not architecturally guaranteed */
+/* to be invalid out of reset, and enabling a write-back data cache */
+/* holding stale valid lines would evict them over live memory. */
+/* */
+/* A CONSEQUENCE WORTH KNOWING */
+/* */
+/* With the write-back data cache on, values this image writes may sit */
+/* dirty in cache where a DEBUGGER READING SRAM CANNOT SEE THEM. The */
+/* memory-resident progress markers and fault records used throughout */
+/* this bring-up are read exactly that way. They stay reliable only */
+/* because cache_clean_all() is called before parking; without that a */
+/* post-mortem read could report stale values and look like a fault */
+/* that never happened. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Dir 4.3 -- assembly encapsulated one operation per function. */
+/* */
+/**************************************************************************/
+
+#include "cache.h"
+
+#define SCTLR_C (1UL << 2)
+#define SCTLR_I (1UL << 12)
+
+
+static unsigned long read_sctlr(void)
+{
+ unsigned long value;
+ __asm volatile ("mrc p15, 0, %0, c1, c0, 0" : "=r" (value));
+ return value;
+}
+
+static void write_sctlr(unsigned long value)
+{
+ __asm volatile ("mcr p15, 0, %0, c1, c0, 0" :: "r" (value));
+}
+
+static void data_sync_barrier(void)
+{
+ __asm volatile ("dsb sy" ::: "memory");
+}
+
+static void instruction_barrier(void)
+{
+ __asm volatile ("isb sy" ::: "memory");
+}
+
+
+unsigned long cache_read_clidr(void)
+{
+ unsigned long value;
+ __asm volatile ("mrc p15, 1, %0, c0, c0, 1" : "=r" (value));
+ return value;
+}
+
+
+unsigned long cache_read_ccsidr(void)
+{
+ unsigned long value;
+ __asm volatile ("mrc p15, 1, %0, c0, c0, 0" : "=r" (value));
+ return value;
+}
+
+
+/* Select a cache level and type for CCSIDR to describe. Level is zero-based;
+ type 0 is data/unified, 1 is instruction. */
+
+static void select_cache(unsigned long level, unsigned long instruction)
+{
+ unsigned long csselr = (level << 1) | (instruction & 1UL);
+
+ __asm volatile ("mcr p15, 2, %0, c0, c0, 0" :: "r" (csselr));
+ instruction_barrier();
+}
+
+
+void cache_invalidate_icache_all(void)
+{
+ unsigned long zero = 0UL;
+
+ __asm volatile ("mcr p15, 0, %0, c7, c5, 0" :: "r" (zero));
+ data_sync_barrier();
+ instruction_barrier();
+}
+
+
+/**************************************************************************/
+/* cache_invalidate_dcache_all -- set/way sweep over real geometry */
+/**************************************************************************/
+
+void cache_invalidate_dcache_all(void)
+{
+ unsigned long clidr = cache_read_clidr();
+ unsigned long level;
+
+ for (level = 0UL; level < 7UL; level++)
+ {
+ unsigned long ctype = (clidr >> (level * 3UL)) & 7UL;
+ unsigned long ccsidr;
+ unsigned long line_shift;
+ unsigned long ways;
+ unsigned long sets;
+ unsigned long way_shift;
+ unsigned long way;
+
+ /* 0 = none, 1 = instruction only: neither needs a data sweep. */
+
+ if ((ctype == 0UL) || (ctype == 1UL))
+ {
+ continue;
+ }
+
+ select_cache(level, 0UL);
+ ccsidr = cache_read_ccsidr();
+
+ line_shift = (ccsidr & 7UL) + 4UL; /* log2 line bytes */
+ ways = ((ccsidr >> 3) & 0x3FFUL) + 1UL;
+ sets = ((ccsidr >> 13) & 0x7FFFUL) + 1UL;
+
+ /* Way number sits in the top bits of the DCISW operand, positioned by
+ the associativity: log2(ways) rounded up. */
+
+ way_shift = 32UL;
+ {
+ unsigned long probe = ways - 1UL;
+ way_shift = 0UL;
+ while (probe != 0UL)
+ {
+ probe >>= 1;
+ way_shift++;
+ }
+ way_shift = 32UL - way_shift;
+ }
+
+ for (way = 0UL; way < ways; way++)
+ {
+ unsigned long set;
+
+ for (set = 0UL; set < sets; set++)
+ {
+ unsigned long operand = (way << way_shift)
+ | (set << line_shift)
+ | (level << 1);
+
+ /* DCISW: invalidate data cache line by set/way. */
+
+ __asm volatile ("mcr p15, 0, %0, c7, c6, 2" :: "r" (operand));
+ }
+ }
+ }
+
+ data_sync_barrier();
+ instruction_barrier();
+}
+
+
+/**************************************************************************/
+/* cache_clean_all -- write dirty lines back to memory */
+/* */
+/* Needed before a debugger reads anything this image wrote: with a */
+/* write-back cache the value may exist only in cache. */
+/**************************************************************************/
+
+void cache_clean_all(void)
+{
+ unsigned long clidr = cache_read_clidr();
+ unsigned long level;
+
+ for (level = 0UL; level < 7UL; level++)
+ {
+ unsigned long ctype = (clidr >> (level * 3UL)) & 7UL;
+ unsigned long ccsidr;
+ unsigned long line_shift;
+ unsigned long ways;
+ unsigned long sets;
+ unsigned long way_shift;
+ unsigned long way;
+
+ if ((ctype == 0UL) || (ctype == 1UL))
+ {
+ continue;
+ }
+
+ select_cache(level, 0UL);
+ ccsidr = cache_read_ccsidr();
+
+ line_shift = (ccsidr & 7UL) + 4UL;
+ ways = ((ccsidr >> 3) & 0x3FFUL) + 1UL;
+ sets = ((ccsidr >> 13) & 0x7FFFUL) + 1UL;
+
+ {
+ unsigned long probe = ways - 1UL;
+ way_shift = 0UL;
+ while (probe != 0UL)
+ {
+ probe >>= 1;
+ way_shift++;
+ }
+ way_shift = 32UL - way_shift;
+ }
+
+ for (way = 0UL; way < ways; way++)
+ {
+ unsigned long set;
+
+ for (set = 0UL; set < sets; set++)
+ {
+ unsigned long operand = (way << way_shift)
+ | (set << line_shift)
+ | (level << 1);
+
+ /* DCCSW: clean data cache line by set/way. */
+
+ __asm volatile ("mcr p15, 0, %0, c7, c10, 2" :: "r" (operand));
+ }
+ }
+ }
+
+ data_sync_barrier();
+}
+
+
+/**************************************************************************/
+/* cache_enable -- invalidate both caches, then turn them on */
+/**************************************************************************/
+
+void cache_enable(void)
+{
+ unsigned long sctlr;
+
+ cache_invalidate_dcache_all();
+ cache_invalidate_icache_all();
+
+ sctlr = read_sctlr();
+ sctlr |= SCTLR_C | SCTLR_I;
+ write_sctlr(sctlr);
+
+ data_sync_barrier();
+ instruction_barrier();
+}
+
+
+/* L1 data cache geometry, decoded from CCSIDR with level 0 selected. */
+
+static unsigned long dcache_ccsidr(void)
+{
+ select_cache(0UL, 0UL);
+ return cache_read_ccsidr();
+}
+
+
+unsigned long cache_dcache_line_bytes(void)
+{
+ return 1UL << ((dcache_ccsidr() & 7UL) + 4UL);
+}
+
+
+unsigned long cache_dcache_ways(void)
+{
+ return ((dcache_ccsidr() >> 3) & 0x3FFUL) + 1UL;
+}
+
+
+unsigned long cache_dcache_sets(void)
+{
+ return ((dcache_ccsidr() >> 13) & 0x7FFFUL) + 1UL;
+}
+
+
+unsigned long cache_dcache_bytes(void)
+{
+ return cache_dcache_line_bytes() * cache_dcache_ways() * cache_dcache_sets();
+}
+
+
+unsigned int cache_enabled(void)
+{
+ unsigned long sctlr = read_sctlr();
+
+ return (((sctlr & SCTLR_C) != 0UL) && ((sctlr & SCTLR_I) != 0UL)) ? 1U : 0U;
+}
+
+
+void cache_disable_all(void)
+{
+ unsigned long sctlr;
+
+ /* Clean before disabling, so dirty lines reach memory while the cache is
+ still on to write them back. Invalidate afterwards, so nothing stale is
+ left to be hit if the caches are enabled again. */
+
+ cache_clean_all();
+ __asm__ volatile("dsb sy" ::: "memory");
+
+ __asm__ volatile("mrc p15, 0, %0, c1, c0, 0" : "=r"(sctlr));
+ sctlr &= ~((1UL << 2) | (1UL << 12)); /* SCTLR.C, SCTLR.I */
+ __asm__ volatile("mcr p15, 0, %0, c1, c0, 0" : : "r"(sctlr) : "memory");
+
+ __asm__ volatile("dsb sy" ::: "memory");
+ __asm__ volatile("isb" ::: "memory");
+
+ cache_invalidate_dcache_all();
+ cache_invalidate_icache_all();
+
+ __asm__ volatile("dsb sy" ::: "memory");
+ __asm__ volatile("isb" ::: "memory");
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/cache.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/cache.h
new file mode 100644
index 00000000..3202c97e
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/cache.h
@@ -0,0 +1,70 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* cache.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Cache maintenance and enable for the S32Z280-594EVB. */
+/* */
+/**************************************************************************/
+
+#ifndef CACHE_H
+#define CACHE_H
+
+/* Geometry as the hardware reports it; printed during bring-up so the sweep
+ below can be checked against real numbers rather than assumed ones. */
+
+unsigned long cache_read_clidr(void);
+unsigned long cache_read_ccsidr(void);
+
+void cache_invalidate_icache_all(void);
+void cache_invalidate_dcache_all(void);
+
+/* Write dirty lines back. Call before a debugger reads memory this image
+ wrote: with a write-back cache the value may exist only in cache. */
+
+void cache_clean_all(void);
+
+/* Invalidate both caches, then set SCTLR.C and SCTLR.I. */
+
+void cache_enable(void);
+
+/* Clean, then disable both caches, then invalidate. Needed by any measurement
+ that wants more than one cold pass in a single run: cache_enable is
+ one-way, and a second cold reading is otherwise impossible. */
+
+void cache_disable_all(void);
+
+unsigned int cache_enabled(void);
+
+/* Decoded L1 data-cache geometry, from CCSIDR. Sizing a cache benchmark
+ against real numbers rather than a guess is the difference between measuring
+ the cache and measuring nothing. */
+
+unsigned long cache_dcache_line_bytes(void);
+unsigned long cache_dcache_ways(void);
+unsigned long cache_dcache_sets(void);
+unsigned long cache_dcache_bytes(void);
+
+#endif /* CACHE_H */
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_fiq_s32z280.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_fiq_s32z280.c
new file mode 100644
index 00000000..ad359704
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_fiq_s32z280.c
@@ -0,0 +1,253 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* demo_fiq_s32z280.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Nested FIQ handling: _tx_thread_fiq_nesting_start and */
+/* _tx_thread_fiq_nesting_end, the last pair in this port that nothing */
+/* had ever called. */
+/* */
+/* The FVP established that the Group 0 chain works and what breaks it. */
+/* What it cannot show is whether a real GIC-600 routes Group 0 to FIQ */
+/* the same way, which is the point of running this here. */
+/* */
+/* FIQ needs more of the GIC than IRQ does. With a single security */
+/* state the controller delivers Group 0 as FIQ and Group 1 as IRQ, so */
+/* an interrupt only arrives as an FIQ if it has been moved into Group */
+/* 0, the distributor and CPU interface both have Group 0 enabled, and */
+/* it is acknowledged through the Group 0 registers. Group 1's */
+/* acknowledge returns the spurious INTID for a Group 0 interrupt and */
+/* leaves it pending, which presents as a storm rather than an error. */
+/* */
+/* FIQ nesting means an FIQ taken while an FIQ handler is running, so */
+/* one source cannot demonstrate it. Two Group 0 SGIs are used, the */
+/* second at a numerically lower priority so it can preempt the first, */
+/* and the first's handler raises it. */
+/* */
+/* WHAT EACH CHECK IS FOR */
+/* */
+/* F1 an FIQ arrives at all. Separate on purpose: it covers the whole */
+/* Group 0 chain -- IGRPEN0, the ICC_SGI0R encoding, IAR0 and */
+/* EOIR0, the EL1 FIQ vector, and F being unmasked. If F1 fails */
+/* the fault is in one of those and not in the nesting routines. */
+/* F2 an FIQ arrived while another FIQ handler was active, depth 2. */
+/* F3 the depth returned to zero, so the pairing unwound. */
+/* F4 the IRQ path is undisturbed: the tick still advances. */
+/* F5 threads still run, so FIQ context save and restore survived */
+/* being re-entered. */
+/* F6 no unexpected Group 0 INTID arrived. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "board.h"
+#include "linflexd.h"
+#include "gicv3.h"
+
+#define DEMO_STACK_SIZE 2048
+
+static TX_THREAD thread_check;
+static TX_THREAD thread_spin;
+
+static ULONG thread_check_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+static ULONG thread_spin_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+static volatile ULONG spin_runs;
+
+
+/* Breakpoint target for tools/read_identity.gdb. Not static and not empty so an
+ optimising build cannot discard it. */
+
+void bsp_done(void)
+{
+ __asm volatile ("nop");
+}
+
+
+static UINT report(const char *label_ptr, UINT passed)
+{
+ linflexd_puts(label_ptr);
+ linflexd_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+ return (passed != 0U) ? 0U : 1U;
+}
+
+
+static void report_hex(const char *label_ptr, unsigned long value)
+{
+ linflexd_puts(label_ptr);
+ linflexd_puts("0x");
+ linflexd_put_hex32((unsigned int) value);
+ linflexd_putc('\n');
+}
+
+
+static void thread_spin_entry(ULONG thread_input)
+{
+ (void) thread_input;
+
+ for (;;)
+ {
+ spin_runs++;
+ }
+}
+
+
+static void thread_check_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ ULONG fiq_before;
+ ULONG ticks_start;
+ ULONG ticks_end;
+ ULONG spin_before;
+
+ (void) thread_input;
+
+ /* F1: the whole Group 0 chain, before nesting is involved. */
+
+ linflexd_puts("raising a Group 0 SGI from thread context\n");
+
+ fiq_before = board_fiq_count;
+ gicv3_send_sgi_group0(BOARD_FIQ_LOW_INTID);
+ tx_thread_sleep(2);
+
+ report_hex("FIQ count = ", board_fiq_count);
+
+ failures += report("F1 FIQ delivered and dispatched ",
+ (board_fiq_count > fiq_before) ? 1U : 0U);
+
+ if (board_fiq_count == fiq_before)
+ {
+ /* Everything below needs an FIQ to arrive, so repeating the failure
+ five more times would only bury the cause. */
+
+ linflexd_puts("no FIQ arrived; the nesting checks would only\n"
+ " restate that, so they are skipped.\n");
+ linflexd_puts("FAIL nested FIQ handling\n");
+ linflexd_puts("=== fiq demo complete ===\n");
+ bsp_done();
+ for (;;)
+ {
+ tx_thread_sleep(100UL);
+ }
+ }
+
+ /* F2 onwards: let the low-priority FIQ handler raise the high one. */
+
+ linflexd_puts("enabling nested provocation in the FIQ handler\n");
+
+ board_fiq_provoke = 1UL;
+ ticks_start = tx_time_get();
+ spin_before = spin_runs;
+
+ {
+ ULONG i;
+
+ for (i = 0UL; i < 20UL; i++)
+ {
+ gicv3_send_sgi_group0(BOARD_FIQ_LOW_INTID);
+ tx_thread_sleep(2);
+ }
+ }
+
+ ticks_end = tx_time_get();
+ board_fiq_provoke = 0UL;
+
+ report_hex("low-priority FIQ count = ", board_fiq_count);
+ report_hex("high-priority FIQ count = ", board_fiq_high_count);
+ report_hex("nested FIQ count = ", board_fiq_nested_count);
+ report_hex("max FIQ depth = ", board_fiq_max_depth);
+ report_hex("FIQ depth now = ", board_fiq_depth);
+
+ failures += report("F2 FIQ nested inside an FIQ handler ",
+ ((board_fiq_nested_count > 0UL) &&
+ (board_fiq_max_depth >= 2UL)) ? 1U : 0U);
+
+ failures += report("F3 FIQ nesting unwound to depth zero ",
+ (board_fiq_depth == 0UL) ? 1U : 0U);
+
+ failures += report("F4 IRQ tick undisturbed by FIQ work ",
+ ((ticks_end - ticks_start) >= 30UL) ? 1U : 0U);
+
+ failures += report("F5 lower-priority thread still scheduled ",
+ (spin_runs > spin_before) ? 1U : 0U);
+
+ report_hex("unexpected Group 0 INTID = ", board_fiq_unexpected);
+
+ failures += report("F6 no unexpected Group 0 INTID ",
+ (board_fiq_unexpected == 0UL) ? 1U : 0U);
+
+ if (failures == 0U)
+ {
+ linflexd_puts("PASS nested FIQ handling verified on silicon\n");
+ }
+ else
+ {
+ linflexd_puts("FAIL nested FIQ handling\n");
+ }
+
+ linflexd_puts("=== fiq demo complete ===\n");
+
+ bsp_done();
+
+ for (;;)
+ {
+ tx_thread_sleep(100UL);
+ }
+}
+
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ (void) tx_thread_create(&thread_check, "fiq check", thread_check_entry,
+ 0UL, thread_check_stack, sizeof(thread_check_stack),
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&thread_spin, "spinner", thread_spin_entry,
+ 0UL, thread_spin_stack, sizeof(thread_spin_stack),
+ 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+void bsp_main(void)
+{
+ unsigned int console_status = linflexd_init();
+
+ linflexd_puts("\n=== ThreadX Cortex-R52 :: S32Z280-594EVB nested FIQ ===\n");
+ report_hex("console = ", (unsigned long) console_status);
+ linflexd_puts("entering kernel\n");
+
+ tx_kernel_enter();
+
+ /* Not reached. */
+
+ linflexd_puts("FAIL tx_kernel_enter returned\n");
+
+ for (;;)
+ {
+ __asm volatile ("nop");
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_nesting_s32z280.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_nesting_s32z280.c
new file mode 100644
index 00000000..ca2a6775
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_nesting_s32z280.c
@@ -0,0 +1,240 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* demo_nesting_s32z280.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Nested IRQ handling on S32Z280 silicon. */
+/* */
+/* The FVP established that the nesting routines work and, more */
+/* usefully, what breaks them: the interrupt has to be acknowledged */
+/* before nesting starts, or the still-pending level-asserted timer is */
+/* retaken the moment IRQ is enabled and recurses until the stacks are */
+/* gone. entry.S here has the same ordering for the same reason. */
+/* */
+/* What the model could not answer is whether a real GIC-600 agrees. */
+/* Two things differ from a model and both are visible here: */
+/* */
+/* P1 how many priority bits the GIC implements. Equal priorities */
+/* do not preempt, and it is the low bits that vanish, so if the */
+/* timer and the SGI collapse to the same value after truncation */
+/* nesting cannot happen at all. board_init discovers the count */
+/* by write and readback rather than assuming the model's five, */
+/* and this demo reports it. */
+/* */
+/* P2 whether an SGI raised on real silicon is delivered at all. */
+/* ICC_SGI1R is a 64-bit AArch32 CP15 register and its encoding */
+/* does not transcribe from the AArch64 alias, so N1 tests SGI */
+/* delivery on its own before nesting is involved. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "linflexd.h"
+#include "board.h"
+#include "gicv3.h"
+
+#define DEMO_STACK_SIZE 2048
+
+static TX_THREAD thread_check;
+static TX_THREAD thread_spin;
+
+static ULONG thread_check_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+static ULONG thread_spin_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+static volatile ULONG spin_runs;
+
+
+/* Breakpoint target for tools/read_identity.gdb. Not static and not empty so
+ an optimising build cannot discard it. */
+
+void bsp_done(void)
+{
+ __asm volatile ("nop");
+}
+
+
+static UINT report(const char *label_ptr, UINT passed)
+{
+ linflexd_puts(label_ptr);
+ linflexd_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+ return (passed != 0U) ? 0U : 1U;
+}
+
+
+static void report_hex(const char *label_ptr, unsigned long value)
+{
+ linflexd_puts(label_ptr);
+ linflexd_puts(" = 0x");
+ linflexd_put_hex32((unsigned int) value);
+ linflexd_putc('\n');
+}
+
+
+static void thread_spin_entry(ULONG thread_input)
+{
+ (void) thread_input;
+
+ for (;;)
+ {
+ spin_runs++;
+ }
+}
+
+
+static void thread_check_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ ULONG sgi_before;
+ ULONG ticks_start;
+ ULONG ticks_end;
+ ULONG spin_before;
+
+ (void) thread_input;
+
+ /* P1: what the GIC actually implements, and whether the two priorities this
+ test depends on survive truncation. */
+
+ report_hex("priority bits", board_priority_bits);
+ report_hex("timer effective", board_timer_priority);
+ report_hex("sgi effective", board_sgi_priority);
+
+ failures += report("P1 SGI outranks timer after truncation ",
+ (board_sgi_priority < board_timer_priority) ? 1U : 0U);
+
+ /* N1: SGI delivery on its own, before nesting is involved. */
+
+ linflexd_puts("N1 raising an SGI from thread context\n");
+
+ sgi_before = board_sgi_count;
+ gicv3_send_sgi(BOARD_NEST_SGI_INTID);
+ tx_thread_sleep(2);
+
+ failures += report("N1 SGI delivered and dispatched ",
+ (board_sgi_count > sgi_before) ? 1U : 0U);
+
+ if (board_sgi_count == sgi_before)
+ {
+ /* Everything below needs the SGI, so reporting four more failures with
+ the same cause would only obscure it. */
+
+ linflexd_puts("SGI never arrived; the nesting checks would only\n"
+ "restate that, so they are skipped.\n");
+ linflexd_puts("FAIL nested IRQ handling\n");
+ linflexd_puts("=== nesting demo complete ===\n");
+ bsp_done();
+ for (;;)
+ {
+ tx_thread_sleep(100UL);
+ }
+ }
+
+ /* N2 onwards: let the timer handler provoke a nested SGI. */
+
+ linflexd_puts("N2 enabling nested provocation in the timer handler\n");
+
+ board_nest_provoke = 1UL;
+ ticks_start = tx_time_get();
+ spin_before = spin_runs;
+ tx_thread_sleep(50);
+ ticks_end = tx_time_get();
+ board_nest_provoke = 0UL;
+
+ report_hex("max depth", board_nest_max);
+ report_hex("nested SGIs", board_sgi_nested_count);
+ report_hex("depth now", board_nest_depth);
+
+ failures += report("N2 SGI nested inside another handler ",
+ ((board_sgi_nested_count > 0UL) &&
+ (board_nest_max >= 2UL)) ? 1U : 0U);
+
+ failures += report("N3 nesting unwound to depth zero ",
+ (board_nest_depth == 0UL) ? 1U : 0U);
+
+ failures += report("N4 tick still advancing after nesting ",
+ ((ticks_end - ticks_start) >= 40UL) ? 1U : 0U);
+
+ failures += report("N5 lower-priority thread still scheduled ",
+ (spin_runs > spin_before) ? 1U : 0U);
+
+ report_hex("spurious", board_spurious_count);
+ report_hex("unexpected", board_unexpected_intid);
+
+ failures += report("N6 no spurious or unexpected interrupts ",
+ ((board_spurious_count == 0UL) &&
+ (board_unexpected_intid == 0UL)) ? 1U : 0U);
+
+ if (failures == 0U)
+ {
+ linflexd_puts("PASS nested IRQ handling verified on silicon\n");
+ }
+ else
+ {
+ linflexd_puts("FAIL nested IRQ handling\n");
+ }
+
+ linflexd_puts("=== nesting demo complete ===\n");
+
+ bsp_done();
+
+ for (;;)
+ {
+ tx_thread_sleep(100UL);
+ }
+}
+
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ (void) tx_thread_create(&thread_check, "nest check", thread_check_entry,
+ 0UL, thread_check_stack, sizeof(thread_check_stack),
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&thread_spin, "spinner", thread_spin_entry,
+ 0UL, thread_spin_stack, sizeof(thread_spin_stack),
+ 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+void bsp_main(void)
+{
+ unsigned int console_status = linflexd_init();
+
+ linflexd_puts("\n=== ThreadX Cortex-R52 :: S32Z280-594EVB nested IRQ ===\n");
+ report_hex("console", (unsigned long) console_status);
+ linflexd_puts("entering kernel\n");
+
+ tx_kernel_enter();
+
+ /* Not reached. */
+
+ linflexd_puts("FAIL tx_kernel_enter returned\n");
+
+ for (;;)
+ {
+ __asm volatile ("nop");
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_s32z280.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_s32z280.c
new file mode 100644
index 00000000..9f09d8d3
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_s32z280.c
@@ -0,0 +1,812 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* demo_s32z280.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* ThreadX running on S32Z280 silicon: threads, preemption and the */
+/* timer tick. */
+/* */
+/* The demo judges itself and says why it failed, because a hang and a */
+/* wrong answer are equally likely on first silicon and look the same */
+/* from a console. Each check is separate: */
+/* */
+/* ticks -- the tick advanced, so the GIC delivers the timer PPI */
+/* and _tx_timer_interrupt is reached */
+/* sleeper -- tx_thread_sleep returned, so tick-driven scheduling */
+/* works, not merely the interrupt */
+/* spinner -- the low-priority thread ran, so the sleeper actually */
+/* yielded the core rather than blocking it */
+/* preempt -- the sleeper resumed while the spinner was runnable, */
+/* which is preemption rather than cooperative handoff */
+/* */
+/* Counting alone would not distinguish these: a demo that only checked*/
+/* that both threads ran would pass with a broken tick if the threads */
+/* happened to yield to each other. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "linflexd.h"
+#include "timer.h"
+#include "cache.h"
+#include "thread_mpu.h"
+#include "board.h"
+
+#define DEMO_STACK_SIZE 2048
+
+static TX_THREAD sleeper_thread;
+static TX_THREAD spinner_thread;
+static TX_THREAD judge_thread;
+
+/* One stack in BTCM, the rest in DRAM0, so a run exercises both and the
+ difference is visible if one of them is wrong.
+
+ BTCM is 16 KB at zero wait states and is never cached, whatever the MPU says
+ about it. That is the point for a stack: access cost does not depend on
+ whether a line happens to be resident, which is what a determinism argument
+ needs. Measured on this part, uncached BTCM comes within 6.6% of the best
+ cached case, where uncached DRAM0 is 22% off it.
+
+ entry.S writes every location in the bank before any C runs, which is
+ required before a read because ECC is enabled (TRM 6.2.2). A stack is read
+ before it is written -- the first context restore pops what tx_thread_create
+ built -- so that preload is not optional here. */
+
+__attribute__((section(".btcm_bss"), aligned(8)))
+static unsigned char sleeper_stack[DEMO_STACK_SIZE];
+static unsigned char spinner_stack[DEMO_STACK_SIZE];
+static unsigned char judge_stack[DEMO_STACK_SIZE];
+
+static volatile unsigned long sleeper_runs;
+static volatile unsigned long spinner_runs;
+static volatile unsigned long preempt_observed;
+
+
+/* Breakpoint target for tools/read_identity.gdb. Defined here rather than
+ pulled from bsp_boot.c, whose bsp_main would collide with this file's.
+ Not static and not empty, so an optimising build cannot discard it. */
+
+void bsp_done(void)
+{
+ __asm volatile ("nop");
+}
+
+
+static void report(const char *name, unsigned long value)
+{
+ linflexd_puts(name);
+ linflexd_puts(" = 0x");
+ linflexd_put_hex32((unsigned int) value);
+ linflexd_putc('\n');
+}
+
+
+/* Highest priority. Sleeps, so it can only run again if the tick advances
+ and the scheduler resumes it. */
+
+/**************************************************************************/
+/* Context-switch cost, stack in BTCM against stack in DRAM0. */
+/* */
+/* Two pairs of equal-priority threads hand control back and forth with */
+/* tx_thread_relinquish, and the measuring thread of each pair times the */
+/* round trip. One pair has both stacks in BTCM, the other in DRAM0. */
+/* */
+/* A round trip is two context switches plus the partner's loop, and the */
+/* partner's code is the same for both pairs, so the difference between */
+/* the pairs is the memory holding the stacks and nothing else. */
+/* */
+/* Both pairs run in one image from one copy of the measuring code, which */
+/* is what makes this comparison safe. The alignment trap that invalid- */
+/* ated earlier work here bites when two builds with different layouts */
+/* are compared; a code shift moves both pairs equally and cancels. */
+/* */
+/* Cycles, from the PMU counter: CNTPCT at 8 MHz cannot resolve a context */
+/* switch. Priorities 2 and 3 put the BTCM pair first and keep both */
+/* above judge and spinner. The sleeper at priority 1 still preempts */
+/* occasionally, which can land in max; min and mean are the robust */
+/* figures and all three are reported. */
+/**************************************************************************/
+
+static void demo_dec(unsigned long value)
+{
+ char digits[12];
+ unsigned int n = 0U;
+
+ if (value == 0UL) { linflexd_putc('0'); return; }
+
+ while ((value > 0UL) && (n < 12U))
+ {
+ digits[n] = (char) ('0' + (value % 10UL));
+ value /= 10UL;
+ n++;
+ }
+ while (n > 0U) { n--; linflexd_putc(digits[n]); }
+}
+
+#define CTX_SAMPLES 256U
+#define CTX_PAIRS 2U
+
+static TX_THREAD ctx_btcm_a_thread;
+static TX_THREAD ctx_btcm_b_thread;
+static TX_THREAD ctx_dram_a_thread;
+static TX_THREAD ctx_dram_b_thread;
+
+__attribute__((section(".btcm_bss"), aligned(8)))
+static unsigned char ctx_btcm_a_stack[DEMO_STACK_SIZE];
+__attribute__((section(".btcm_bss"), aligned(8)))
+static unsigned char ctx_btcm_b_stack[DEMO_STACK_SIZE];
+
+static unsigned char ctx_dram_a_stack[DEMO_STACK_SIZE];
+static unsigned char ctx_dram_b_stack[DEMO_STACK_SIZE];
+
+static volatile unsigned int ctx_done[CTX_PAIRS];
+static unsigned int ctx_lo[CTX_PAIRS];
+static unsigned int ctx_hi[CTX_PAIRS];
+static unsigned int ctx_mean[CTX_PAIRS];
+static unsigned int ctx_over[CTX_PAIRS];
+
+/* A round trip costs about 1400 cycles. Anything past this is not a context
+ switch, it is a switch that got interrupted -- the sleeper at priority 1
+ preempts either pair when a tick lands mid-measurement. Counted rather than
+ averaged in, because max is otherwise a report on which pair was unlucky:
+ across three runs the maximum appeared in the BTCM pair once and the DRAM0
+ pair twice, while min and mean held to the cycle. */
+
+#define CTX_OUTLIER_CYCLES 2000U
+
+/* Cache state, and why it is forced rather than left to chance.
+
+ With the cache left alone this workload never evicts anything: the DRAM0
+ stack stays resident in the 16 KB data cache, every access is a hit, and
+ DRAM0 measures beautifully. That is a true result for an idle system and a
+ misleading one for the argument TCM is usually made for, which is about worst
+ case when other work has pushed your stack out.
+
+ Loading the partner thread with a cache walk does not work either: the timed
+ round trip includes the partner, so the walk dominates every sample. It was
+ tried and discarded, and it put every one of 256 samples past the outlier
+ threshold.
+
+ Instead the cache is cleaned and invalidated immediately before each timed
+ switch, outside the timed region. Every switch then runs from a cold cache,
+ which is the state a stack in DRAM0 finds itself in after other work has run
+ and the state a stack in BTCM is immune to, because an enabled TCM is never
+ cached. Both pairs get the same treatment, so the difference is the memory. */
+
+
+/* One copy, used by both pairs. */
+
+static void ctx_measure_entry(ULONG which)
+{
+ unsigned int lo = 0xFFFFFFFFU;
+ unsigned int hi = 0U;
+ unsigned long sum = 0UL;
+ unsigned int over = 0U;
+ unsigned int counted = 0U;
+ unsigned int i;
+
+ for (i = 0U; i < CTX_SAMPLES; i++)
+ {
+ unsigned int before;
+ unsigned int after;
+ unsigned int delta;
+
+ /* Cold cache for every sample, and clean before invalidate so dirty
+ stack lines reach memory rather than being discarded. Outside the
+ timestamps, so its own cost is not in the measurement. */
+
+ cache_clean_all();
+ cache_invalidate_dcache_all();
+
+ before = timer_read_cycles();
+ tx_thread_relinquish();
+ after = timer_read_cycles();
+
+ delta = after - before;
+
+ if (delta > CTX_OUTLIER_CYCLES)
+ {
+ /* Preempted mid-switch. Excluded from min, max and mean alike: a
+ maximum that reports whichever pair a tick landed on is not a
+ measurement of anything, and reporting it as jitter was wrong
+ once already. */
+
+ over++;
+ }
+ else
+ {
+ if (delta < lo) { lo = delta; }
+ if (delta > hi) { hi = delta; }
+ sum += (unsigned long) delta;
+ counted++;
+ }
+ }
+
+ ctx_lo[which] = lo;
+ ctx_hi[which] = hi;
+ ctx_mean[which] = (counted > 0U) ? (unsigned int) (sum / counted) : 0U;
+ ctx_over[which] = over;
+
+ ctx_done[which] = 1U; /* releases the partner */
+}
+
+static void ctx_partner_entry(ULONG which)
+{
+ while (ctx_done[which] == 0U)
+ {
+ tx_thread_relinquish();
+ }
+}
+
+/**************************************************************************/
+/* Stack-heavy work, stack in BTCM against stack in DRAM0. */
+/* */
+/* #635 measured context switches and found BTCM worth about 7.4%, with */
+/* no determinism advantage, and said why: a switch saves sixteen */
+/* registers, roughly one cache line, so the stack's cache state has */
+/* almost nothing to contribute. It also said the interesting case was */
+/* work with a large stack working set, that it had not been measured, and */
+/* that it should not be assumed. This measures it. */
+/* */
+/* deep_touch recurses, writing a frame on the way down and reading it on */
+/* the way up, so the stack working set is the whole descent rather than */
+/* one frame. The cache is cleaned and invalidated before each sample, so */
+/* the descent starts cold: a stack in DRAM0 must fill a line per frame, */
+/* while a stack in BTCM has no cache in the path to miss. */
+/* */
+/* No partner threads and no relinquish here. The measurement is entirely */
+/* within one thread, which removes the confound that made the context */
+/* switch figure hard to read -- there, the timed region included the */
+/* partner's execution. */
+/* */
+/* Samples are kept and post-processed rather than filtered against a */
+/* fixed threshold, because the cost of this workload was not known in */
+/* advance and a guessed threshold discarded every sample once already. */
+/**************************************************************************/
+
+#define DEEP_SAMPLES 64U
+#define DEEP_DEPTH 24U
+#define DEEP_FRAME 16U /* words: 64 bytes, one cache line per frame */
+#define DEEP_PLACEMENTS 4U
+
+static TX_THREAD deep_btcm_thread;
+static TX_THREAD deep_dram_thread;
+
+/* 4 KB: 24 frames of 64 bytes plus call overhead is about 2.1 KB, and ThreadX
+ needs its own room on top. */
+
+__attribute__((section(".btcm_bss"), aligned(8)))
+static unsigned char deep_btcm_stack[4096];
+static unsigned char deep_dram_stack[4096];
+
+static unsigned int deep_samples[CTX_PAIRS][DEEP_SAMPLES];
+static unsigned int deep_lo[CTX_PAIRS][DEEP_PLACEMENTS];
+static unsigned int deep_hi[CTX_PAIRS][DEEP_PLACEMENTS];
+static unsigned int deep_mean[CTX_PAIRS][DEEP_PLACEMENTS];
+static unsigned int deep_over[CTX_PAIRS][DEEP_PLACEMENTS];
+static volatile unsigned int deep_done[CTX_PAIRS];
+
+/* Four placements of the recursive body, each 64-byte aligned and then
+ displaced within its line, and the measurement sweeps all four.
+
+ The first version of this measurement had one placement, which is the
+ methodology this example spent two changes correcting elsewhere. It showed
+ BTCM with a threefold tighter spread than DRAM0; that did not survive an
+ image with two more threads in it, where both spreads came out near 6500 and
+ the minima moved 15%. A single placement was reporting where the linker had
+ put the code.
+
+ The pad nops execute on every recursion level rather than once, so each
+ placement carries a slightly different constant cost -- about 0.6% at the
+ widest pad. That cancels in the comparison that matters, because BTCM and
+ DRAM0 are measured at the same placement, and it does not affect the spread
+ within a placement. */
+
+#define MAKE_DEEP_TOUCH(name, pad_words) \
+__attribute__((aligned(64), noinline)) \
+static unsigned int name(unsigned int depth) \
+{ \
+ volatile unsigned int frame[DEEP_FRAME]; \
+ unsigned int acc = 0U; \
+ unsigned int i; \
+ \
+ __asm__ volatile(".rept " #pad_words "\n\tnop\n\t.endr"); \
+ \
+ for (i = 0U; i < DEEP_FRAME; i++) \
+ { \
+ frame[i] = depth + i; \
+ } \
+ \
+ if (depth > 0U) \
+ { \
+ acc = name(depth - 1U); \
+ } \
+ \
+ for (i = 0U; i < DEEP_FRAME; i++) \
+ { \
+ acc += frame[i]; \
+ } \
+ \
+ return acc; \
+}
+
+MAKE_DEEP_TOUCH(deep_touch_a, 0)
+MAKE_DEEP_TOUCH(deep_touch_b, 4)
+MAKE_DEEP_TOUCH(deep_touch_c, 8)
+MAKE_DEEP_TOUCH(deep_touch_d, 12)
+
+static unsigned int (*const deep_touches[DEEP_PLACEMENTS])(unsigned int) =
+{
+ deep_touch_a, deep_touch_b, deep_touch_c, deep_touch_d
+};
+
+static void deep_entry(ULONG which)
+{
+ unsigned int i;
+ unsigned int lo;
+ unsigned long sum;
+ unsigned int counted;
+ unsigned int over;
+ unsigned int hi;
+
+ for (i = 0U; i < DEEP_SAMPLES; i++)
+ {
+ unsigned int before;
+ unsigned int after;
+ unsigned int slot = i / (DEEP_SAMPLES / DEEP_PLACEMENTS);
+
+ if (slot >= DEEP_PLACEMENTS)
+ {
+ slot = DEEP_PLACEMENTS - 1U;
+ }
+
+ cache_clean_all();
+ cache_invalidate_dcache_all();
+
+ before = timer_read_cycles();
+ (void) deep_touches[slot](DEEP_DEPTH);
+ after = timer_read_cycles();
+
+ deep_samples[which][i] = after - before;
+ }
+
+ /* Post-processed per placement. A figure spanning placements would mix the
+ four constant costs and hide exactly what this sweep exists to show. */
+
+ {
+ unsigned int slot;
+ unsigned int each = DEEP_SAMPLES / DEEP_PLACEMENTS;
+
+ for (slot = 0U; slot < DEEP_PLACEMENTS; slot++)
+ {
+ unsigned int s;
+ unsigned int lo = 0xFFFFFFFFU;
+ unsigned int hi = 0U;
+ unsigned long sum = 0UL;
+ unsigned int counted = 0U;
+ unsigned int over = 0U;
+
+ for (s = slot * each; s < ((slot + 1U) * each); s++)
+ {
+ if (deep_samples[which][s] < lo) { lo = deep_samples[which][s]; }
+ }
+
+ for (s = slot * each; s < ((slot + 1U) * each); s++)
+ {
+ unsigned int v = deep_samples[which][s];
+
+ if (v > (lo * 2U))
+ {
+ over++;
+ }
+ else
+ {
+ if (v > hi) { hi = v; }
+ sum += (unsigned long) v;
+ counted++;
+ }
+ }
+
+ deep_lo[which][slot] = lo;
+ deep_hi[which][slot] = hi;
+ deep_mean[which][slot] = (counted > 0U) ? (unsigned int) (sum / counted) : 0U;
+ deep_over[which][slot] = over;
+ }
+ }
+
+ deep_done[which] = 1U;
+}
+
+/**************************************************************************/
+/* Per-thread memory protection, demonstrated rather than asserted. */
+/* */
+/* Two threads each own a 4 KB window at the top of DRAM2, carved out of */
+/* the broad data region in mpu.c so that nothing else grants access to */
+/* them. Each thread writes its own window, which must succeed, and then */
+/* reaches for the other thread's, which must fault. */
+/* */
+/* The second half is the part that matters. A test that only shows a */
+/* thread reaching its own memory proves nothing about isolation: it */
+/* would pass just as well with no protection at all. */
+/* */
+/* The fault is made survivable the same way the boot probes do it -- */
+/* fault_expected tells the data abort handler to record the violation */
+/* and resume at the instruction after the faulting access, rather than */
+/* treating it as fatal. That works in thread context because the */
+/* handler returns to where it came from rather than to a fixed recovery */
+/* point. */
+/**************************************************************************/
+
+#define ISO_THREADS 2U
+
+static TX_THREAD iso_thread[ISO_THREADS];
+
+static unsigned char iso_stack[ISO_THREADS][DEMO_STACK_SIZE];
+
+static volatile unsigned int iso_done[ISO_THREADS];
+static unsigned int iso_own_ok[ISO_THREADS];
+static unsigned int iso_other_faulted[ISO_THREADS];
+static unsigned long iso_own_base[ISO_THREADS];
+
+static void iso_entry(ULONG which)
+{
+ extern unsigned int fault_expected;
+ extern unsigned int fault_taken;
+
+ unsigned long own = thread_mpu_window_base((unsigned int) which);
+ unsigned long other = thread_mpu_window_base((unsigned int) (1UL - which));
+ unsigned int before;
+ unsigned int pattern = 0xC0DE0000U + (unsigned int) which;
+
+ iso_own_base[which] = own;
+
+ /* Install this thread's window. */
+
+ thread_mpu_activate(tx_thread_identify());
+
+ /* Its own window: must be reachable. */
+
+ *((volatile unsigned int *) own) = pattern;
+ iso_own_ok[which] =
+ (*((volatile unsigned int *) own) == pattern) ? 1U : 0U;
+
+ /* The other thread's window: must not be. Only the write is attempted --
+ reading it back would fault a second time, and the point is already
+ made. */
+
+ before = fault_taken;
+ fault_expected = 1U;
+ *((volatile unsigned int *) other) = 0xBADU;
+ fault_expected = 0U;
+
+ iso_other_faulted[which] = (fault_taken > before) ? 1U : 0U;
+
+ iso_done[which] = 1U;
+}
+
+static void sleeper_entry(ULONG input)
+{
+ unsigned long spinner_before;
+
+ (void) input;
+
+ for (;;)
+ {
+ spinner_before = spinner_runs;
+
+ tx_thread_sleep(5UL); /* 50 ms at a 10 ms tick */
+
+ sleeper_runs++;
+
+ /* If the spinner advanced while this thread slept, and this thread is
+ the higher priority of the two, then resuming it took the core away
+ from a runnable lower-priority thread. That is preemption. */
+
+ if (spinner_runs != spinner_before)
+ {
+ preempt_observed++;
+ }
+ }
+}
+
+
+/* Lowest priority. Never blocks, so it only runs when nothing else can. */
+
+static void spinner_entry(ULONG input)
+{
+ (void) input;
+
+ for (;;)
+ {
+ spinner_runs++;
+ }
+}
+
+
+/* Middle priority. Waits, then judges and reports. */
+
+static void judge_entry(ULONG input)
+{
+ ULONG ticks_start;
+ ULONG ticks_end;
+ UINT failures = 0U;
+
+ (void) input;
+
+ ticks_start = tx_time_get();
+ tx_thread_sleep(100UL); /* about one second */
+ ticks_end = tx_time_get();
+
+ linflexd_puts("\n=== ThreadX on S32Z280: results ===\n");
+
+ linflexd_puts("per-thread MPU isolation\n");
+ {
+ unsigned int w;
+ unsigned int all_ok = 1U;
+
+ for (w = 0U; w < ISO_THREADS; w++)
+ {
+ linflexd_puts(" thread ");
+ demo_dec(w);
+ linflexd_puts(" window 0x");
+ linflexd_put_hex32((unsigned int) iso_own_base[w]);
+ if (iso_done[w] == 0U)
+ {
+ linflexd_puts(" did not finish\n");
+ all_ok = 0U;
+ }
+ else
+ {
+ linflexd_puts(iso_own_ok[w] ? " own: reachable" : " own: UNREACHABLE");
+ linflexd_puts(iso_other_faulted[w] ? " other: faulted\n"
+ : " other: REACHED\n");
+ if ((iso_own_ok[w] == 0U) || (iso_other_faulted[w] == 0U))
+ {
+ all_ok = 0U;
+ }
+ }
+ }
+
+ linflexd_puts(all_ok ? " PASS each thread reached its own window and faulted on the other\n"
+ : " FAIL isolation not demonstrated\n");
+
+ linflexd_puts(" region switch cost, cycles: min ");
+ demo_dec(thread_mpu_min_cycles());
+ linflexd_puts(" max ");
+ demo_dec(thread_mpu_max_cycles());
+ linflexd_puts(" switches ");
+ demo_dec(thread_mpu_switch_count());
+ linflexd_puts("\n");
+ }
+
+ linflexd_puts("stack-heavy work, cycles (24 frames, cold cache, 4 placements)\n");
+ {
+ static const char *const dwhere[CTX_PAIRS] = { "BTCM ", "DRAM0" };
+ unsigned int q;
+ unsigned int slot;
+
+ for (q = 0U; q < CTX_PAIRS; q++)
+ {
+ if (deep_done[q] == 0U)
+ {
+ linflexd_puts(" ");
+ linflexd_puts(dwhere[q]);
+ linflexd_puts(": did not finish\n");
+ continue;
+ }
+
+ for (slot = 0U; slot < DEEP_PLACEMENTS; slot++)
+ {
+ linflexd_puts(" ");
+ linflexd_puts(dwhere[q]);
+ linflexd_puts(" offset ");
+ demo_dec((unsigned long) (slot * 16U));
+ linflexd_puts(": min ");
+ demo_dec(deep_lo[q][slot]);
+ linflexd_puts(" mean ");
+ demo_dec(deep_mean[q][slot]);
+ linflexd_puts(" max ");
+ demo_dec(deep_hi[q][slot]);
+ linflexd_puts(" spread ");
+ demo_dec(deep_hi[q][slot] - deep_lo[q][slot]);
+ linflexd_puts(" cut ");
+ demo_dec(deep_over[q][slot]);
+ linflexd_puts("\n");
+ }
+ }
+ }
+
+ linflexd_puts("context switch round trip, cycles\n");
+ {
+ static const char *const where[CTX_PAIRS] = { "stacks in BTCM ",
+ "stacks in DRAM0" };
+ unsigned int q;
+
+ for (q = 0U; q < CTX_PAIRS; q++)
+ {
+ linflexd_puts(" ");
+ linflexd_puts(where[q]);
+ if (ctx_done[q] == 0U)
+ {
+ linflexd_puts(": did not finish\n");
+ }
+ else
+ {
+ linflexd_puts(": min ");
+ demo_dec(ctx_lo[q]);
+ linflexd_puts(" mean ");
+ demo_dec(ctx_mean[q]);
+ linflexd_puts(" max ");
+ demo_dec(ctx_hi[q]);
+ linflexd_puts(" preempted ");
+ demo_dec(ctx_over[q]);
+ linflexd_puts(" of ");
+ demo_dec(CTX_SAMPLES);
+ linflexd_puts("\n");
+ }
+ }
+ }
+ report("ticks ", (unsigned long) (ticks_end - ticks_start));
+ report("sleeper ", sleeper_runs);
+ report("spinner ", spinner_runs);
+ report("preempt ", preempt_observed);
+
+ if ((ticks_end - ticks_start) < 50UL)
+ {
+ linflexd_puts("FAIL tick did not advance as expected\n");
+ failures++;
+ }
+
+ if (sleeper_runs == 0UL)
+ {
+ linflexd_puts("FAIL sleeper never resumed\n");
+ failures++;
+ }
+
+ if (spinner_runs == 0UL)
+ {
+ linflexd_puts("FAIL spinner never ran\n");
+ failures++;
+ }
+
+ if (preempt_observed == 0UL)
+ {
+ linflexd_puts("FAIL no preemption observed\n");
+ failures++;
+ }
+
+ if (failures == 0U)
+ {
+ linflexd_puts("PASS threads, tick and preemption all verified\n");
+ }
+
+ linflexd_puts("=== demo complete ===\n");
+
+ bsp_done();
+
+ for (;;)
+ {
+ tx_thread_sleep(100UL);
+ }
+}
+
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ linflexd_puts("stacks: sleeper 0x");
+ linflexd_put_hex32((unsigned int) (unsigned long) sleeper_stack);
+ linflexd_puts(" (BTCM) spinner 0x");
+ linflexd_put_hex32((unsigned int) (unsigned long) spinner_stack);
+ linflexd_puts(" judge 0x");
+ linflexd_put_hex32((unsigned int) (unsigned long) judge_stack);
+ linflexd_puts("\n");
+
+ (void) tx_thread_create(&sleeper_thread, "sleeper", sleeper_entry, 0UL,
+ sleeper_stack, DEMO_STACK_SIZE,
+ 1U, 1U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&judge_thread, "judge", judge_entry, 0UL,
+ judge_stack, DEMO_STACK_SIZE,
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* The PMU cycle counter has to be started here. bsp_boot.c starts it for
+ the probe image, and this image does not run bsp_boot.c, so without this
+ every reading below would be zero -- which would read as a free context
+ switch rather than as a dead counter. */
+
+ timer_cycles_enable();
+
+ /* BTCM pair first at priority 2, DRAM0 pair after it at 3. */
+
+ (void) tx_thread_create(&ctx_btcm_b_thread, "ctxB btcm", ctx_partner_entry, 0UL,
+ ctx_btcm_b_stack, DEMO_STACK_SIZE,
+ 2U, 2U, TX_NO_TIME_SLICE, TX_AUTO_START);
+ (void) tx_thread_create(&ctx_btcm_a_thread, "ctxA btcm", ctx_measure_entry, 0UL,
+ ctx_btcm_a_stack, DEMO_STACK_SIZE,
+ 2U, 2U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&ctx_dram_b_thread, "ctxB dram", ctx_partner_entry, 1UL,
+ ctx_dram_b_stack, DEMO_STACK_SIZE,
+ 3U, 3U, TX_NO_TIME_SLICE, TX_AUTO_START);
+ (void) tx_thread_create(&ctx_dram_a_thread, "ctxA dram", ctx_measure_entry, 1UL,
+ ctx_dram_a_stack, DEMO_STACK_SIZE,
+ 3U, 3U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Isolation pair. Priority 6 and 7, after the measurement threads. */
+
+ {
+ unsigned int w;
+
+ for (w = 0U; w < ISO_THREADS; w++)
+ {
+ (void) tx_thread_create(&iso_thread[w], "iso", iso_entry,
+ (ULONG) w,
+ iso_stack[w], DEMO_STACK_SIZE,
+ 6U + w, 6U + w,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+ (void) thread_mpu_grant(&iso_thread[w], w);
+ }
+ }
+
+ /* Stack-heavy pair, after the context-switch pairs. */
+
+ (void) tx_thread_create(&deep_btcm_thread, "deep btcm", deep_entry, 0UL,
+ deep_btcm_stack, sizeof(deep_btcm_stack),
+ 4U, 4U, TX_NO_TIME_SLICE, TX_AUTO_START);
+ (void) tx_thread_create(&deep_dram_thread, "deep dram", deep_entry, 1UL,
+ deep_dram_stack, sizeof(deep_dram_stack),
+ 5U, 5U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&spinner_thread, "spinner", spinner_entry, 0UL,
+ spinner_stack, DEMO_STACK_SIZE,
+ 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S; hands over to the kernel. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ unsigned int console_status = linflexd_init();
+
+ linflexd_puts("\n=== ThreadX Cortex-R52 :: S32Z280-594EVB ===\n");
+ report("console ", (unsigned long) console_status);
+ linflexd_puts("entering kernel\n");
+
+ tx_kernel_enter();
+
+ /* Not reached. */
+
+ linflexd_puts("FAIL tx_kernel_enter returned\n");
+
+ for (;;)
+ {
+ __asm volatile ("nop");
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_vfp_s32z280.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_vfp_s32z280.c
new file mode 100644
index 00000000..456ad319
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/demo_vfp_s32z280.c
@@ -0,0 +1,283 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* demo_vfp_s32z280.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Lazy floating-point context save and restore on S32Z280 silicon. */
+/* */
+/* This is the FVP's demo_m5.c test design, reporting over the */
+/* LINFlexD console instead of semihosting. The design is kept */
+/* deliberately: the model established that the two halves of the VFP */
+/* context path need separate provocation, and silicon does not change */
+/* that, it only changes whether the hardware agrees. */
+/* */
+/* fp check holds eight live doubles across tx_thread_sleep. Eight */
+/* forces the compiler onto the callee-saved bank D8-D15, */
+/* which is what a SOLICITED switch must preserve. */
+/* */
+/* fp busy lowest priority and never sleeps, so the periodic tick */
+/* interrupts it mid-computation. That exercises the */
+/* INTERRUPT half, D0-D15 plus FPSCR, and it checks its */
+/* own values every iteration. */
+/* */
+/* Both call tx_thread_vfp_enable(). With lazy save and restore a */
+/* thread that has not asked for floating point does not get its */
+/* registers saved, so a test where every thread opts in would not */
+/* show whether the opt-in is what does the work. */
+/* */
+/* Every constant is an exact binary fraction and the step is 0.5, so */
+/* the comparisons are exact and no epsilon is needed. MISRA C:2012 */
+/* Dir 4.6 and Rule 13.3 would normally object to == on floating */
+/* point; here it is the point, since any inexactness means a register */
+/* was not preserved. */
+/* */
+/* What silicon adds over the model: the FPU must actually be enabled */
+/* by the BSP. entry.S opens CPACR and FPEXC.EN at EL1 under */
+/* __ARM_FP, because tx_thread_vfp_enable() sets only the per-thread */
+/* software flag and never touches the hardware. Without that, the */
+/* first D-register access after a switch takes an Undefined */
+/* Instruction exception -- which on this board was invisible until */
+/* SCTLR.TE was cleared. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "linflexd.h"
+#include "board.h"
+
+#define DEMO_STACK_SIZE 2048
+#define FILEX_PTR_SENTINEL ((void *) 0xF11EF11EUL)
+#define FP_ITERATIONS 50UL
+#define FP_STEP 0.5 /* exactly representable */
+
+static TX_THREAD thread_check;
+static TX_THREAD thread_fp_busy;
+
+static ULONG thread_check_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+static ULONG thread_fp_busy_stack[DEMO_STACK_SIZE / sizeof(ULONG)];
+
+static volatile ULONG busy_iterations;
+static volatile ULONG busy_corruptions;
+
+
+/* Breakpoint target for tools/read_identity.gdb. Not static and not empty so
+ an optimising build cannot discard it. */
+
+void bsp_done(void)
+{
+ __asm volatile ("nop");
+}
+
+
+static UINT report(const char *label_ptr, UINT passed)
+{
+ linflexd_puts(label_ptr);
+ linflexd_puts((passed != 0U) ? "PASS\n" : "FAIL\n");
+ return (passed != 0U) ? 0U : 1U;
+}
+
+
+static void report_hex(const char *label_ptr, unsigned long value)
+{
+ linflexd_puts(label_ptr);
+ linflexd_puts(" = 0x");
+ linflexd_put_hex32((unsigned int) value);
+ linflexd_putc('\n');
+}
+
+
+/**************************************************************************/
+/* thread_fp_busy_entry -- the interrupt half of the VFP path. */
+/**************************************************************************/
+
+static void thread_fp_busy_entry(ULONG thread_input)
+{
+ (void) thread_input;
+
+ tx_thread_vfp_enable();
+
+ for (;;)
+ {
+ /* Deliberately disjoint from the checking thread's range, so that
+ state leaking from one context into the other is detectable rather
+ than coincidentally correct. */
+
+ double v0 = 1024.5;
+ double v1 = 2048.25;
+ double v2 = 4096.125;
+ double v3 = 8192.0625;
+
+ v0 = (v0 * 2.0) - 1024.5; /* 1024.5 */
+ v1 = (v1 * 2.0) - 2048.25; /* 2048.25 */
+ v2 = (v2 * 2.0) - 4096.125; /* 4096.125 */
+ v3 = (v3 * 2.0) - 8192.0625; /* 8192.0625 */
+
+ if ((v0 != 1024.5) || (v1 != 2048.25) ||
+ (v2 != 4096.125) || (v3 != 8192.0625))
+ {
+ busy_corruptions++;
+ }
+
+ busy_iterations++;
+ }
+}
+
+
+/**************************************************************************/
+/* thread_check_entry -- the solicited half, and the judge. */
+/**************************************************************************/
+
+static void thread_check_entry(ULONG thread_input)
+{
+ UINT failures = 0U;
+ ULONG iteration;
+ TX_THREAD *self;
+
+ double a = 1.5;
+ double b = 2.25;
+ double c = 3.125;
+ double d = 4.0625;
+ double e = 5.03125;
+ double f = 6.015625;
+ double g = 7.0078125;
+ double h = 8.00390625;
+
+ (void) thread_input;
+
+ /* The VFP enable flag is reached from assembly by hard-coded offset. On the
+ Cortex-R4 and R5 ports that offset lands on tx_thread_filex_ptr, because
+ their TX_THREAD_EXTENSION_2 is empty. A sentinel turns that class of
+ mistake into a visible failure instead of corruption that only appears
+ once something actually uses FileX. */
+
+ self = tx_thread_identify();
+ self->tx_thread_filex_ptr = FILEX_PTR_SENTINEL;
+
+ tx_thread_vfp_enable();
+
+ linflexd_puts("V1 floating-point work across solicited switches\n");
+
+ for (iteration = 0UL; iteration < FP_ITERATIONS; iteration++)
+ {
+ /* Suspending here is the provocation: the switch must preserve all
+ eight values, and the lower-priority thread runs meanwhile. */
+
+ tx_thread_sleep(1);
+
+ a += FP_STEP;
+ b += FP_STEP;
+ c += FP_STEP;
+ d += FP_STEP;
+ e += FP_STEP;
+ f += FP_STEP;
+ g += FP_STEP;
+ h += FP_STEP;
+ }
+
+ {
+ const double delta = 0.5 * (double) FP_ITERATIONS;
+
+ failures += report("V2 D8-D15 bank preserved across switches ",
+ ((a == (1.5 + delta)) &&
+ (b == (2.25 + delta)) &&
+ (c == (3.125 + delta)) &&
+ (d == (4.0625 + delta)) &&
+ (e == (5.03125 + delta)) &&
+ (f == (6.015625 + delta)) &&
+ (g == (7.0078125 + delta)) &&
+ (h == (8.00390625 + delta))) ? 1U : 0U);
+ }
+
+ report_hex("iterations", busy_iterations);
+ report_hex("corruptions", busy_corruptions);
+
+ failures += report("V3 interrupted FP thread made progress ",
+ (busy_iterations > 0UL) ? 1U : 0U);
+ failures += report("V4 no FP corruption across interrupts ",
+ (busy_corruptions == 0UL) ? 1U : 0U);
+
+ report_hex("filex_ptr", (unsigned long) self->tx_thread_filex_ptr);
+
+ failures += report("V5 VFP flag did not alias filex_ptr ",
+ (self->tx_thread_filex_ptr == FILEX_PTR_SENTINEL) ? 1U : 0U);
+
+ if (failures == 0U)
+ {
+ linflexd_puts("PASS lazy VFP context switch verified on silicon\n");
+ }
+ else
+ {
+ linflexd_puts("FAIL lazy VFP context switch\n");
+ }
+
+ linflexd_puts("=== vfp demo complete ===\n");
+
+ bsp_done();
+
+ for (;;)
+ {
+ tx_thread_sleep(100UL);
+ }
+}
+
+
+void tx_application_define(void *first_unused_memory)
+{
+ (void) first_unused_memory;
+
+ (void) tx_thread_create(&thread_check, "fp check", thread_check_entry, 0UL,
+ thread_check_stack, sizeof(thread_check_stack),
+ 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ (void) tx_thread_create(&thread_fp_busy, "fp busy", thread_fp_busy_entry,
+ 0UL, thread_fp_busy_stack,
+ sizeof(thread_fp_busy_stack),
+ 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START);
+}
+
+
+/**************************************************************************/
+/* bsp_main -- entered at EL1 from entry.S; hands over to the kernel. */
+/**************************************************************************/
+
+void bsp_main(void)
+{
+ unsigned int console_status = linflexd_init();
+
+ linflexd_puts("\n=== ThreadX Cortex-R52 :: S32Z280-594EVB lazy VFP ===\n");
+ report_hex("console", (unsigned long) console_status);
+ linflexd_puts("entering kernel\n");
+
+ tx_kernel_enter();
+
+ /* Not reached. */
+
+ linflexd_puts("FAIL tx_kernel_enter returned\n");
+
+ for (;;)
+ {
+ __asm volatile ("nop");
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S b/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S
new file mode 100644
index 00000000..6601b25e
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S
@@ -0,0 +1,1034 @@
+@/***************************************************************************
+@ * Copyright (c) 2026 Eclipse ThreadX contributors
+@ *
+@ * This program and the accompanying materials are made available under the
+@ * terms of the MIT License which is available at
+@ * https://opensource.org/licenses/MIT.
+@ *
+@ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+@ * The AI-generated portions may be considered public domain (CC0-1.0)
+@ * and not subject to the project's licence. The human contributor has
+@ * reviewed and verified that the code is correct.
+@ *
+@ * SPDX-License-Identifier: MIT and CC0-1.0
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@/**************************************************************************/
+@/* */
+@/* BOARD SUPPORT RELEASE */
+@/* */
+@/* entry.S Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* Reset path for the NXP S32Z280-594EVB, RTU0 core 0: EL2 setup, the */
+@/* drop to EL1, stacks, .bss and the call into bsp_main. */
+@/* */
+@/* Two things differ from the Armv8-R AEM FVP entry path and both are */
+@/* silicon facts rather than choices: */
+@/* */
+@/* 1. The core resets in THUMB state. CPSR reads 0x1FA on this board */
+@/* -- Hyp mode with T set -- because the RTU boot instruction is a */
+@/* T32 branch (NXP writes R52_B0_INSTR_T32 to the boot address). */
+@/* So _start must be T32 and switch to A32 itself. An A32 entry */
+@/* executes the first halfword of its own instruction as Thumb and */
+@/* goes anywhere. */
+@/* */
+@/* 2. There is no semihosting here, so progress cannot be printed. */
+@/* Instead each stage records a marker and the identity registers */
+@/* into .data, where a debugger reads them. boot_stage says how */
+@/* far the reset path got; if a fault handler runs it records the */
+@/* vector, syndrome and return address before parking. Without */
+@/* this a fault would be indistinguishable from a hang. */
+@/* */
+@/* Define TX_R52_BOOT_AT_EL1 to skip the EL2 configuration, for */
+@/* targets where an earlier boot stage or a vendor EL2 monitor (for */
+@/* example NXP's EL2M, or Eclipse ThreadX ZoneX) has already dropped */
+@/* privilege to EL1. The kernel then enters at el1_entry in A32 */
+@/* state, and the register list that becomes the monitor's */
+@/* responsibility is enumerated at the #ifndef itself rather than */
+@/* here, next to the code it replaces. */
+@/* */
+@/* The Armv8-R AEM FVP entry path has carried the same option since */
+@/* it was written; this file gained it later, which is why the two */
+@/* brackets are not identical in shape -- the EL2 work this board */
+@/* does is considerably larger. */
+@/* */
+@/* CNTFRQ is read and recorded rather than written. The FVP leaves it */
+@/* zero and its BSP programs a known value, but the correct frequency */
+@/* for this board is not yet established, and writing a wrong one would*/
+@/* silently mis-scale every tick interval derived from it. */
+@/* */
+@/**************************************************************************/
+
+ .syntax unified
+
+/* HCR (Hyp Configuration Register) bits cleared below. */
+
+ .equ HCR_VM, (1 << 0) /* stage-2 MPU enable */
+ .equ HCR_AMO, (1 << 3) /* route aborts to EL2 */
+ .equ HCR_IMO, (1 << 4) /* route IRQ to EL2 */
+ .equ HCR_FMO, (1 << 5) /* route FIQ to EL2 */
+ .equ HCR_TGE, (1 << 27) /* trap general exceptions to EL2 */
+ .equ HCR_HCD, (1 << 29) /* HVC disable -- must be clear */
+
+ .equ HCPTR_TCP, (3 << 10) /* trap EL1 access to CP10/CP11 */
+
+/* CPACR grants EL1/EL0 access to CP10 and CP11, the floating-point unit;
+ FPEXC.EN then enables floating-point execution itself. */
+
+ .equ CPACR_CP10, (3 << 20)
+ .equ CPACR_CP11, (3 << 22)
+ .equ FPEXC_EN, (1 << 30)
+
+ .equ CNTHCTL_PL1PCTEN, (1 << 0)
+ .equ CNTHCTL_PL1PCEN, (1 << 1)
+
+ .equ ICC_HSRE_SRE, (1 << 0)
+ .equ ICC_HSRE_ENABLE, (1 << 3)
+
+/* IMP_PERIPHPREGIONR: peripheral port enables. Both reset to 0. */
+
+ .equ PERIPHP_ENABLE_EL10, (1 << 0)
+ .equ PERIPHP_ENABLE_EL2, (1 << 1)
+
+/* SCTLR/HSCTLR.TE -- Thumb Exception enable. Set means exceptions are entered
+ in T32 state. It RESETS SET on this part, and every vector table here is
+ A32, so it must be cleared or no handler is reachable. */
+
+ .equ SCTLR_TE, (1 << 30)
+
+/* CPSR mode and mask bits. */
+
+ .equ MODE_USR, 0x10
+ .equ MODE_FIQ, 0x11
+ .equ MODE_IRQ, 0x12
+ .equ MODE_SVC, 0x13
+ .equ MODE_ABT, 0x17
+ .equ MODE_UND, 0x1B
+ .equ MODE_SYS, 0x1F
+ .equ PSR_F, 0x40
+ .equ PSR_I, 0x80
+ .equ PSR_A, 0x100
+
+/* System counter frequency; see where it is programmed below. */
+
+ .equ SYSTEM_COUNTER_HZ, 8000000
+
+/* Stage markers, recorded in boot_stage. */
+
+ .equ STAGE_A32, 1 /* switched out of Thumb */
+ .equ STAGE_EL2_DONE, 2 /* EL2 configured */
+ .equ STAGE_EL1, 3 /* running at EL1 */
+ .equ STAGE_MAIN, 4 /* about to enter bsp_main */
+ .equ STAGE_RETURNED, 5 /* bsp_main returned (unexpected) */
+ .equ STAGE_FAULT, 0xFF /* a fault handler ran */
+
+@/**************************************************************************/
+@/* Observation area. In .data so the debugger's load initialises it and */
+@/* a store needs no stack. */
+@/**************************************************************************/
+
+ .section .data
+ .align 2
+
+ .global boot_stage
+boot_stage: .word 0
+ .global cpsr_at_el2
+cpsr_at_el2: .word 0
+ .global cpsr_at_el1
+cpsr_at_el1: .word 0
+ .global cntfrq_at_el2
+cntfrq_at_el2: .word 0
+ .global hmpuir_at_el2
+hmpuir_at_el2: .word 0
+ .global probe_stage
+probe_stage: .word 0
+#ifdef TXM_MODULE_MANAGER
+ .extern __tx_module_svc_interrupt
+ .extern _txm_module_manager_data_abort
+ .extern _txm_module_manager_prefetch_abort
+#endif
+
+ .global fault_expected
+fault_expected: .word 0
+ .global fault_taken
+fault_taken: .word 0
+ .global fault_expected_pabt
+fault_expected_pabt: .word 0
+ .global abort_return
+abort_return: .word 0
+ .global fault_vector
+fault_vector: .word 0
+ .global fault_syndrome
+fault_syndrome: .word 0
+ .global fault_address
+fault_address: .word 0
+
+@/**************************************************************************/
+@/* EL2 (Hyp) vector table. 32-byte aligned for HVBAR. */
+@/* */
+@/* Offsets 0x04-0x10 are exceptions taken FROM Hyp mode. Everything */
+@/* routed to EL2 from EL1 or EL0 arrives at 0x14 instead and is decoded */
+@/* from HSR.EC -- the single funnel that partitioning work will use. */
+@/**************************************************************************/
+
+ .section .vectors_el2, "ax"
+ .align 5
+ .arm
+ .global el2_vectors
+el2_vectors:
+ b fault_el2_reset /* 0x00 */
+ b fault_el2_undef /* 0x04 */
+ b fault_el2_svc /* 0x08 */
+ b fault_el2_pabt /* 0x0C */
+ b fault_el2_dabt /* 0x10 */
+ b fault_el2_hyp /* 0x14 from EL1/EL0 */
+ b fault_el2_irq /* 0x18 */
+ b fault_el2_fiq /* 0x1C */
+
+@/**************************************************************************/
+@/* EL1 vector table. */
+@/**************************************************************************/
+
+ .section .vectors_el1, "ax"
+ .align 5
+ .arm
+ .global el1_vectors
+el1_vectors:
+ b fault_el1_reset /* 0x00 */
+ b fault_el1_undef /* 0x04 */
+#ifdef TXM_MODULE_MANAGER
+ b __tx_module_svc_interrupt /* 0x08 module kernel boundary */
+#else
+ b fault_el1_svc /* 0x08 */
+#endif
+ b fault_el1_pabt /* 0x0C */
+ b fault_el1_dabt /* 0x10 */
+ b fault_el1_reserved /* 0x14 */
+ b el1_irq_entry /* 0x18 IRQ */
+ b el1_fiq_entry /* 0x1C FIQ */
+
+#ifndef TX_R52_BOOT_AT_EL1
+
+@/**************************************************************************/
+@/* THE EL2 RESET PATH, which a guest build skips entirely. */
+@/* */
+@/* Everything between this #ifndef and its #else configures EL2 and then */
+@/* drops to EL1. With TX_R52_BOOT_AT_EL1 defined, an earlier boot stage */
+@/* or a vendor EL2 monitor has already done that, so the kernel starts */
+@/* straight at el1_entry and none of it runs. */
+@/* */
+@/* WHAT THE MONITOR MUST THEN DO, because a guest at EL1 cannot: */
+@/* */
+@/* CNTFRQ writable only at the highest implemented */
+@/* exception level, and it reads ZERO out of */
+@/* reset on this part. A guest deriving a */
+@/* tick interval from it divides by zero. */
+@/* HCPTR.TCP10/TCP11 both reset SET, trapping every EL1 and EL0 */
+@/* floating-point access to EL2. */
+@/* HSCTLR.TE an EL2 register; a guest cannot write it. */
+@/* (SCTLR.TE is EL1's and el1_entry clears it */
+@/* below, so the guest still handles its own.) */
+@/* ICC_HSRE.SRE until it is set, every other ICC_* and */
+@/* ICH_* system register is UNDEFINED -- so an */
+@/* EL1 kernel cannot acknowledge an interrupt */
+@/* at all. */
+@/* IMP_PERIPHPREGIONR the low-latency peripheral port enables */
+@/* reset to zero and an EL1 write to this */
+@/* register traps to EL2 when */
+@/* HACTLR.PERIPHPREGIONR is clear. A guest */
+@/* that needs the RTU peripheral window at */
+@/* 0x76000000 therefore depends on the monitor */
+@/* having opened it; one that does not need it */
+@/* is unaffected. */
+@/* IMP_ATCMREGIONR and the TCM bases and enables are per-core, and */
+@/* IMP_BTCMREGIONR ENABLEEL2 is silently IGNORED when written */
+@/* from EL1 -- measured on this part, on both */
+@/* BTCM and CTCM: the base took and bit 0 took */
+@/* while bit 1 stayed clear. A guest that */
+@/* places anything in a TCM therefore needs */
+@/* the monitor to program and ECC-preload it, */
+@/* because ECC is enabled here and a TCM */
+@/* location must be WRITTEN before it can be */
+@/* read (TRM 6.2.2). */
+@/* */
+@/* CNTHCTL.PL1PCTEN and PL1PCEN are the interesting omission from that */
+@/* list. This path opens both, because a standalone kernel owns the */
+@/* physical timer. A monitor that TIME-partitions its guests must NOT */
+@/* open them: a partition's physical time keeps running while it is */
+@/* descheduled, so a guest reading it can observe that it was not */
+@/* running. Such a monitor gives its guests the virtual timer and a */
+@/* per-guest CNTVOFF instead. That is the monitor's decision to make, */
+@/* not this file's, which is why this list says what a guest CANNOT do */
+@/* rather than what a monitor SHOULD do. */
+@/**************************************************************************/
+@/**************************************************************************/
+@/* _start -- reset entry. Entered in THUMB state, Hyp mode (EL2). */
+@/**************************************************************************/
+
+ .section .text.boot, "ax"
+ .thumb
+ .align 1
+ .global _start
+ .type _start, %function
+_start:
+ /* Leave Thumb before anything else. start_a32 is an A32 symbol, so the
+ linker clears bit 0 of its address and BX switches state. */
+
+ ldr r0, =start_a32
+ bx r0
+
+ .arm
+ .align 2
+ .type start_a32, %function
+start_a32:
+ ldr r0, =boot_stage
+ mov r1, #STAGE_A32
+ str r1, [r0]
+
+ /* Hyp stack, before anything can be pushed at EL2. */
+
+ ldr sp, =__hyp_stack_top
+
+ /* Install the EL2 vector table. */
+
+ ldr r0, =el2_vectors
+ mcr p15, 4, r0, c12, c0, 0 /* HVBAR */
+ isb
+
+ /* Keep exceptions and interrupts routed to EL1 and the stage-2 MPU off.
+ HCR_HCD is cleared explicitly so HVC works from EL1 rather than
+ depending on its reset value. */
+
+ mrc p15, 4, r0, c1, c1, 0 /* HCR */
+ ldr r1, =(HCR_TGE | HCR_AMO | HCR_IMO | HCR_FMO | HCR_VM | HCR_HCD)
+ bic r0, r0, r1
+ mcr p15, 4, r0, c1, c1, 0
+ isb
+
+ /* TCM: program the bases and enable all three banks, here at EL2.
+ *
+ * This is at EL2 deliberately. Writing ENABLEEL10 from EL1 works, but
+ * ENABLEEL2 is silently ignored there -- measured, on both BTCM and CTCM:
+ * the base took and bit 0 took while bit 1 stayed clear. The TRM does not
+ * say so outright, but EL1 being unable to change what EL2 can see is the
+ * sensible reading, and doing the whole job at EL2 avoids the question.
+ *
+ * ATCM boots enabled at address 0 because CFGTCMBOOTx is tied high here, so
+ * this also moves it off zero, which makes a null-pointer write fault rather
+ * than quietly land in tightly-coupled memory. Each base must be
+ * size-aligned (TRM r1p3 6.2); 64KB at 0x30000000 and 16KB at 0x30100000
+ * and 0x30200000 satisfy that.
+ *
+ * SIZE and WAITSTATES are read-only descriptions of the hardware, so the
+ * values written here keep the bits the banks already report: ATCM 64KB with
+ * one wait state, BTCM 16KB with none, CTCM 16KB with one.
+ *
+ * Nothing may READ the TCMs after this until they have been written, because
+ * ECC is enabled on this part and the check bits are not initialised by the
+ * core (TRM 6.2.2). bsp_main preloads them.
+ */
+
+ ldr r0, =(0x30000000 | (0x07 << 2) | (1 << 8) | (1 << 1) | (1 << 0))
+ mcr p15, 0, r0, c9, c1, 0 /* IMP_ATCMREGIONR */
+
+ /* BTCM as well. 16 KB at zero wait states, where ATCM has one, so it is the
+ * faster bank for data -- which is what it is wanted for here. The claim
+ * that a second bank costs the data cache was withdrawn; see the note below.
+ * Preloaded by bsp_main before any read, as ECC requires, and BTCM accepts
+ * 32-bit stores where ATCM needs 64-bit (TRM 6.2.2).
+ */
+ ldr r0, =(0x30100000 | (0x05 << 2) | (0 << 8) | (1 << 1) | (1 << 0))
+ mcr p15, 0, r0, c9, c1, 1 /* IMP_BTCMREGIONR */
+ isb
+
+ /* Preload the whole bank. ECC is enabled on this part, so a TCM location
+ * must be written before it can be read (TRM 6.2.2), and this has to happen
+ * before any C runs: the demo images place thread stacks here and do not run
+ * bsp_boot.c, which is where the ATCM preload lives. 32-bit stores are
+ * sufficient for BTCM where ATCM needs 64-bit.
+ */
+
+ ldr r1, =0x30100000
+ ldr r2, =0x00004000
+ mov r3, #0
+
+3: str r3, [r1], #4
+ subs r2, r2, #4
+ bgt 3b
+
+ dsb sy
+ /* BTCM and CTCM are not enabled, because nothing in this example uses them.
+ * That is the whole reason, and it is worth stating plainly because the
+ * reason given here previously was wrong.
+ *
+ * This comment used to claim that enabling a second bank removed all
+ * measurable data-cache benefit, and it listed five configurations to prove
+ * it: ATCM alone showing a 24% cache gain, and four combinations involving a
+ * second bank showing none. It concluded that the trigger was not a
+ * particular bank, not the bank's address and not the ECC preload, but
+ * enabling a second bank at all. It cited the Cortex-R52 and S32Z2 errata as
+ * not covering it, and offered a shared RAM pool between the LLC and the TCMs
+ * as an explanation. A defect report went to NXP on that basis.
+ *
+ * All of it was an artifact of the benchmark that produced the numbers. That
+ * benchmark was bimodal with respect to where its loop fell inside a 64-byte
+ * cache line, reporting either 24% or nothing at all for identical silicon,
+ * and every one of those five configurations was an edit to this file, so
+ * every one shifted the code that followed and flipped the mode. Adding two
+ * nop instructions here reproduces the "second bank" result exactly, to the
+ * digit. The report to NXP has been withdrawn.
+ *
+ * Measured again with a benchmark that sweeps four loop alignments and
+ * reports the distribution, ATCM alone and ATCM plus CTCM are
+ * indistinguishable:
+ *
+ * loop offset in line ATCM only ATCM + CTCM
+ * 0 gain 0 gain 0
+ * 16 gain 0 gain 0
+ * 32 gain 240/1000 gain 240/1000
+ * 48 gain 240/1000 gain 240/1000
+ *
+ * So enabling a second bank costs nothing measurable. Enabling one is a
+ * single line, and the only prerequisites are the ECC preload before any read
+ * (TRM 6.2.2) and an MPU region, both of which tcm.c and mpu.c already
+ * handle for ATCM. They stay off until something needs them.
+ */
+
+ isb
+
+ /* Let EL1 reach the FPU: clear HCPTR.TCP10/TCP11. The R52 always has at
+ least a single-precision FPU, so this matters even for a soft-float
+ kernel if application code uses it. */
+
+ mrc p15, 4, r0, c1, c1, 2 /* HCPTR */
+ ldr r1, =HCPTR_TCP
+ bic r0, r0, r1
+ mcr p15, 4, r0, c1, c1, 2
+ isb
+
+ /* Record CNTFRQ as found. Deliberately not written: see the file
+ header. */
+
+ mrc p15, 0, r0, c14, c0, 0 /* CNTFRQ as found */
+ ldr r1, =cntfrq_at_el2
+ str r0, [r1]
+
+ /* Program CNTFRQ. It is writable only at the highest implemented
+ exception level, so it must happen here rather than in bsp_main.
+ CNTFRQ is a software-declared constant: nothing derives it from the
+ hardware, and it reads zero out of reset on this silicon exactly as it
+ does on the FVP. Any code computing a tick interval from it would
+ divide by zero.
+
+ 8 MHz is not a guess. The counter was measured against host wall-clock
+ time at 8.0227 MHz over a 32-second interval, and the clock tree agrees:
+ RTU.GPR CFG_CNTDV reads 4, so the divider is (4+1) = 5, and the board's
+ FXOSC is 40 MHz -- itself confirmed independently by the LINFlexD baud
+ divisors the BootROM left behind. 40 / 5 = 8. */
+
+ ldr r0, =SYSTEM_COUNTER_HZ
+ mcr p15, 0, r0, c14, c0, 0 /* CNTFRQ */
+
+ /* HMPUIR is EL2-only, so it has to be read here rather than in bsp_main.
+ It gives the EL2 MPU region count, which stage-2 partitioning needs and
+ which the EL1 MPUIR does not report. */
+
+ mrc p15, 4, r0, c0, c0, 4 /* HMPUIR */
+ ldr r1, =hmpuir_at_el2
+ str r0, [r1]
+
+ /* Permit EL1/EL0 use of the physical counter and timer; both reset
+ disabled, so an EL1 access to CNTPCT or CNTP_* would otherwise trap. */
+
+ mrc p15, 4, r0, c14, c1, 0 /* CNTHCTL */
+ orr r0, r0, #(CNTHCTL_PL1PCTEN | CNTHCTL_PL1PCEN)
+ mcr p15, 4, r0, c14, c1, 0
+ isb
+
+ /* Permit EL1 to reach the GIC CPU interface through ICC_* system
+ registers. Needs no GIC base address, so it is safe to do here even
+ though the distributor address for this part is not yet established. */
+
+ mrc p15, 4, r0, c12, c9, 5 /* ICC_HSRE */
+ orr r0, r0, #(ICC_HSRE_SRE | ICC_HSRE_ENABLE)
+ mcr p15, 4, r0, c12, c9, 5
+ isb
+
+ /* Clear HSCTLR.TE so EL2 exceptions are entered in A32, matching the EL2
+ vector table. See the EL1 equivalent below for why this matters. */
+
+ mrc p15, 4, r0, c1, c0, 0 /* HSCTLR */
+ bic r0, r0, #SCTLR_TE
+ mcr p15, 4, r0, c1, c0, 0
+ isb
+
+ /* Open the low-latency peripheral port.
+ IMP_PERIPHPREGIONR (Cortex-R52 TRM r1p3, 3.3.80) describes a region at
+ 0x76000000 of 4 MB on this part -- the RTU peripheral space -- with
+ separate enables for EL2 and EL1/0 that BOTH RESET TO ZERO. Until they
+ are set, any access in that window stalls the core: no abort, no fault
+ handler, no exception at all, and the debug connection drops with it.
+ Nothing in the SoC reference manual mentions this; it cost a run before
+ the TRM identified it. Written here because EL1 writes to this register
+ trap to EL2 when HACTLR.PERIPHPREGIONR is clear. */
+
+ mrc p15, 0, r0, c15, c0, 0 /* IMP_PERIPHPREGIONR */
+ orr r0, r0, #(PERIPHP_ENABLE_EL2 | PERIPHP_ENABLE_EL10)
+ mcr p15, 0, r0, c15, c0, 0
+ isb
+
+ /* Record CPSR while still at EL2: mode bits should read 0x1A (Hyp). */
+
+ mrs r0, cpsr
+ ldr r1, =cpsr_at_el2
+ str r0, [r1]
+
+ ldr r0, =boot_stage
+ mov r1, #STAGE_EL2_DONE
+ str r1, [r0]
+
+ /* Drop to EL1 (Supervisor), interrupts still masked. */
+
+ ldr r0, =el1_entry
+ msr ELR_hyp, r0
+#ifdef TX_ENABLE_FIQ_SUPPORT
+
+ /* FIQ left unmasked, IRQ still masked. tx_thread_stack_build only clears a
+ * thread's F bit when the library was built with FIQ support and nothing
+ * else ever clears it, so leaving PSR_F set here would mask FIQ until the
+ * first thread ran and an FIQ raised from bsp_main would silently do
+ * nothing.
+ */
+
+ ldr r0, =(PSR_A | PSR_I | MODE_SVC)
+#else
+ ldr r0, =(PSR_A | PSR_I | PSR_F | MODE_SVC)
+#endif
+ msr SPSR_hyp, r0
+ isb
+ eret
+
+#else
+
+@/**************************************************************************/
+@/* _start, when an EL2 monitor has already dropped privilege. */
+@/* */
+@/* A32, not T32, and that is the one real difference from the standalone */
+@/* entry above. The core resets in Thumb state here because the RTU boot */
+@/* instruction NXP plants is a T32 branch -- but a guest is not reached */
+@/* by reset. It is reached by the monitor's ERET, and the monitor */
+@/* chooses the state through SPSR.T. Both ZoneX and NXP's EL2M enter a */
+@/* guest in A32, matching the vector tables in this file, so this entry */
+@/* is A32 and needs no state switch of its own. */
+@/* */
+@/* SPSR.T MUST AGREE WITH THIS. If it does not, the guest dies on its */
+@/* first instruction with an undefined-instruction exception -- which */
+@/* looks exactly like a bad entry address and sends the reader to the */
+@/* loader instead of to the ERET. */
+@/**************************************************************************/
+
+ .section .text.boot, "ax"
+ .arm
+ .align 2
+ .global _start
+ .type _start, %function
+_start:
+ b el1_entry
+ .size _start, . - _start
+
+#endif
+
+@/**************************************************************************/
+@/* el1_entry -- first instruction at EL1. */
+@/**************************************************************************/
+
+ .align 2
+ .type el1_entry, %function
+el1_entry:
+ ldr r0, =boot_stage
+ mov r1, #STAGE_EL1
+ str r1, [r0]
+
+ /* EL1 vector table. */
+
+ ldr r0, =el1_vectors
+ mcr p15, 0, r0, c12, c0, 0 /* VBAR */
+ isb
+
+#ifdef __ARM_FP
+
+ /* Enable the floating-point unit for EL1.
+ *
+ * Two gates must be opened and both reset closed:
+ * CPACR grants EL1/EL0 access to CP10/CP11. It reads 0 out of reset,
+ * so the first floating-point instruction would otherwise raise
+ * an Undefined Instruction exception.
+ * FPEXC .EN enables floating-point execution itself.
+ * EL2 has already cleared HCPTR.TCP10/TCP11 above, so neither traps to EL2.
+ *
+ * This is the BSP's job, not the kernel's. tx_thread_vfp_enable() only sets
+ * the per-thread flag that makes the context switch save and restore
+ * floating-point state; it does not turn the hardware on. Without the code
+ * below, a VFP build faults on the first context switch that touches a
+ * D-register -- and on this silicon that fault was invisible until
+ * SCTLR.TE was cleared, which is the trap described further down.
+ *
+ * Guarded by __ARM_FP, which the compiler defines exactly when the build can
+ * emit floating-point instructions. In a soft-float build "vmsr fpexc" is
+ * not assemblable for this target at all.
+ */
+
+ mrc p15, 0, r0, c1, c0, 2 /* CPACR */
+ orr r0, r0, #(CPACR_CP10 | CPACR_CP11)
+ mcr p15, 0, r0, c1, c0, 2
+ isb
+
+ mov r0, #FPEXC_EN
+ vmsr fpexc, r0
+ isb
+
+#endif
+
+ /* Clear SCTLR.TE. This is not cosmetic and it cost a long detour.
+ *
+ * TE resets SET on this silicon -- SCTLR reads 0x70C50838, bit 30 high --
+ * which makes the core enter every exception in T32 state. All the vector
+ * tables here are A32. So an exception would branch into the table, decode
+ * the A32 branch as Thumb, land mid-instruction at a misaligned address and
+ * take an undefined-instruction exception, which then did the same thing
+ * again. No handler ever ran.
+ *
+ * The consequence was worse than a crash: every fault became invisible.
+ * boot_stage stayed at its last value, fault_vector stayed zero, and the
+ * core sat at el1_vectors+4 -- so a data abort from an unmapped peripheral,
+ * an abort from a bad MPU map, and a correctly delivered timer interrupt all
+ * presented identically, as an unexplained hang with nothing recorded. Two
+ * real bugs were diagnosed through that fog before its cause was found.
+ *
+ * The Armv8-R AEM FVP resets with TE clear, which is why the same vector
+ * tables work there and why this never appeared during the FVP work. */
+
+ mrc p15, 0, r0, c1, c0, 0 /* SCTLR */
+ bic r0, r0, #SCTLR_TE
+ mcr p15, 0, r0, c1, c0, 0
+ isb
+
+ /* One stack per mode: an exception taken inside a handler would
+ otherwise reuse the interrupted mode's stack pointer. */
+
+ mrs r0, cpsr
+ bic r0, r0, #0x1F
+
+ orr r1, r0, #MODE_IRQ
+ msr cpsr_c, r1
+ ldr sp, =__irq_stack_top
+
+ orr r1, r0, #MODE_FIQ
+ msr cpsr_c, r1
+ ldr sp, =__fiq_stack_top
+
+ orr r1, r0, #MODE_ABT
+ msr cpsr_c, r1
+ ldr sp, =__abt_stack_top
+
+ orr r1, r0, #MODE_UND
+ msr cpsr_c, r1
+ ldr sp, =__und_stack_top
+
+ orr r1, r0, #MODE_SYS
+ msr cpsr_c, r1
+ ldr sp, =__sys_stack_top
+
+ orr r1, r0, #MODE_SVC
+ msr cpsr_c, r1
+ ldr sp, =__svc_stack_top
+
+ /* Zero .bss. Nothing above this point may rely on a .bss object. */
+
+ ldr r0, =__bss_start__
+ ldr r1, =__bss_end__
+ mov r2, #0
+bss_loop:
+ cmp r0, r1
+ strlo r2, [r0], #4
+ blo bss_loop
+
+ /* Record CPSR at EL1: mode bits should read 0x13 (Supervisor), which is
+ what proves the EL2 to EL1 drop actually happened. */
+
+ mrs r0, cpsr
+ ldr r1, =cpsr_at_el1
+ str r0, [r1]
+
+ ldr r0, =boot_stage
+ mov r1, #STAGE_MAIN
+ str r1, [r0]
+
+ bl bsp_main
+
+ /* bsp_main is not expected to return. */
+
+ ldr r0, =boot_stage
+ mov r1, #STAGE_RETURNED
+ str r1, [r0]
+park:
+ b park
+
+@/**************************************************************************/
+@/**************************************************************************/
+@/* el1_irq_entry -- IRQ vector, standalone. */
+@/* */
+@/* The FVP example routes IRQ through _tx_thread_context_save and */
+@/* __tx_irq_processing_return, which this image cannot do: it does not */
+@/* link ThreadX, deliberately, so that a failure here is a board problem */
+@/* and not a kernel one. This is the classic A32 form instead. */
+@/* */
+@/* lr is adjusted by 4 because an IRQ is taken with lr pointing one */
+@/* instruction past the one that was interrupted. The caret on the ldm */
+@/* restores CPSR from SPSR_irq as it loads pc, which is what returns to */
+@/* the interrupted mode rather than staying in IRQ mode. */
+@/**************************************************************************/
+
+ .align 2
+ .global el1_irq_entry
+ .type el1_irq_entry, %function
+el1_irq_entry:
+#ifdef TX_R52_USE_THREADX_IRQ
+
+ /* With the kernel linked, the vector goes straight into
+ _tx_thread_context_save, which saves the interrupted context and
+ branches back to __tx_irq_processing_return. A plain B, not BL:
+ context save adjusts lr itself to find the point of interrupt. */
+
+ b _tx_thread_context_save
+
+ .global __tx_irq_processing_return
+__tx_irq_processing_return:
+#ifdef TX_ENABLE_IRQ_NESTING
+
+ /* Nested IRQ handling, the same shape as the FVP example, and the ordering
+ * matters as much here. The interrupt is acknowledged in IRQ mode with
+ * interrupts still masked, BEFORE nesting starts: reading ICC_IAR1 raises
+ * the GIC running priority to this interrupt's own, which masks it and
+ * everything of equal or lower priority, and only then is re-enabling IRQ
+ * safe. Start nesting first and the still-pending, still-level-asserted
+ * timer is retaken the instant IRQ is enabled, recursing until the IRQ and
+ * System stacks are gone -- a hang with no fault to point at.
+ *
+ * The INTID rides in r4, which survives the mode switch because only SP and
+ * LR are banked, and is pushed on the IRQ stack as well so a nested level
+ * reusing r4 cannot lose this level's value. End-of-interrupt waits for
+ * IRQ mode with interrupts masked again, so dropping the running priority
+ * cannot re-admit this same interrupt.
+ */
+
+ bl gicv3_acknowledge
+ mov r4, r0
+ stmdb sp!, {r4, r5} /* r5 keeps 8-byte alignment */
+
+ bl _tx_thread_irq_nesting_start
+ mov r0, r4
+ bl board_irq_service
+ bl _tx_thread_irq_nesting_end
+
+ ldmia sp!, {r4, r5}
+ mov r0, r4
+ bl gicv3_end_of_interrupt
+#else
+ bl board_irq_handler
+#endif
+ b _tx_thread_context_restore
+
+
+#else
+
+ /* Standalone: the classic A32 form. lr is adjusted by 4 because an IRQ
+ is taken with lr one instruction past the interrupted one, and the
+ caret on the ldm restores CPSR from SPSR_irq as it loads pc. */
+
+ sub lr, lr, #4
+ stmfd sp!, {r0-r3, r12, lr}
+ bl board_irq_handler
+ ldmfd sp!, {r0-r3, r12, pc}^
+
+#endif
+
+@/**************************************************************************/
+@/* el1_fiq_entry -- FIQ vector at EL1. */
+@/**************************************************************************/
+
+el1_fiq_entry:
+#if defined(TX_ENABLE_FIQ_SUPPORT) && defined(TX_R52_USE_THREADX_IRQ)
+
+ /* Both guards are needed. TX_ENABLE_FIQ_SUPPORT is PUBLIC on the threadx
+ * target, so it reaches every image once the library is built with FIQ,
+ * including images that link no interrupt controller; referencing the GIC
+ * and board symbols below from those breaks the link outright. That is why
+ * the IRQ vector is gated the same way.
+ *
+ * Group 0 is what the GIC delivers as FIQ, and it has its own acknowledge
+ * and end-of-interrupt registers -- the Group 1 pair returns the spurious
+ * INTID for a Group 0 interrupt and leaves it pending, which looks like a
+ * storm rather than a mistake. As on the IRQ path, the acknowledge happens
+ * before nesting starts, so the running priority masks this interrupt before
+ * FIQ is re-enabled.
+ */
+
+ b _tx_thread_fiq_context_save
+
+ .global __tx_fiq_processing_return
+__tx_fiq_processing_return:
+#ifdef TX_ENABLE_FIQ_NESTING
+ bl gicv3_acknowledge_group0
+ mov r4, r0
+ stmdb sp!, {r4, r5}
+
+ bl _tx_thread_fiq_nesting_start
+ mov r0, r4
+ bl board_fiq_service
+ bl _tx_thread_fiq_nesting_end
+
+ ldmia sp!, {r4, r5}
+ mov r0, r4
+ bl gicv3_end_of_interrupt_group0
+#else
+ bl board_fiq_handler
+#endif
+ b _tx_thread_fiq_context_restore
+#else
+ b fault_el1_fiq
+#endif
+
+@/**************************************************************************/
+@/* try_execute_data -- branch to a non-executable address on purpose. */
+@/* */
+@/* Saves its own return address for the prefetch-abort handler to use, */
+@/* arms that handler, then branches to the address in r0. It never */
+@/* returns by itself: the abort handler returns on its behalf. Written */
+@/* in assembly because the return path is not something C can express. */
+@/**************************************************************************/
+
+ .align 2
+ .global try_execute_data
+ .type try_execute_data, %function
+try_execute_data:
+ ldr r1, =abort_return
+ str lr, [r1]
+ ldr r1, =fault_expected_pabt
+ mov r2, #1
+ str r2, [r1]
+ dsb
+ isb
+ bx r0 /* fetch from XN memory: aborts */
+
+@/* Fault reporters. Each records which vector ran, a syndrome and a */
+@/* faulting address, then parks. Recording before parking is what makes */
+@/* a fault distinguishable from a hang when there is no console. */
+@/**************************************************************************/
+
+@/* Each handler loads its syndrome into r0 and a faulting or return */
+@/* address into r3, then falls into FAULT_TAIL. The instructions are not*/
+@/* passed as macro arguments: GNU as macro arguments are textual, and */
+@/* an instruction containing a comma cannot be passed without quoting, */
+@/* whose handling differs between assembler versions. */
+
+ .macro FAULT_TAIL vecid
+ ldr r1, =fault_vector
+ mov r2, #\vecid
+ str r2, [r1]
+ ldr r1, =fault_syndrome
+ str r0, [r1]
+ ldr r1, =fault_address
+ str r3, [r1]
+ ldr r1, =boot_stage
+ mov r2, #STAGE_FAULT
+ str r2, [r1]
+1: b 1b
+ .endm
+
+ .align 2
+
+fault_el2_reset:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x20
+fault_el2_undef:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x24
+fault_el2_svc:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x28
+fault_el2_pabt:
+ mrc p15, 4, r0, c5, c1, 0 /* HSR */
+ mov r3, lr
+ FAULT_TAIL 0x2C
+fault_el2_dabt:
+ mrc p15, 4, r0, c5, c1, 0 /* HSR */
+ mrc p15, 4, r3, c6, c0, 0 /* HDFAR */
+ FAULT_TAIL 0x30
+fault_el2_hyp:
+ mrc p15, 4, r0, c5, c2, 0 /* HSR (EC identifies the trap) */
+ mrc p15, 4, r3, c6, c0, 0 /* HDFAR */
+ FAULT_TAIL 0x34
+fault_el2_irq:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x38
+fault_el2_fiq:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x3C
+
+fault_el1_reset:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x40
+fault_el1_undef:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x44
+fault_el1_svc:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x48
+fault_el1_pabt:
+#ifdef TXM_MODULE_MANAGER
+ /* A fault from User mode is a module reaching outside its regions, and it
+ belongs to the module manager, which will record it and terminate the
+ thread. A fault from a privileged mode continues on the recoverable path
+ below, which the boot probes depend on.
+
+ r0 is pushed and popped around the test rather than simply used. The code
+ below clobbers r0 through r2 without saving them, which is tolerable for a
+ probe that provokes its own fault, but the module capture records the
+ faulting registers and would record ours instead. LDM does not affect the
+ flags, so the comparison still holds after the pop. */
+
+ stmdb sp!, {r0}
+ mrs r0, spsr
+ and r0, r0, #0x1F
+ cmp r0, #0x10 /* User mode? */
+ ldmia sp!, {r0}
+ beq _txm_module_manager_prefetch_abort
+#endif
+
+ /* Recoverable when a test has armed fault_expected_pabt.
+ *
+ * Recovery differs from the data-abort case in kind, not degree. There,
+ * the faulting instruction is a load or store and skipping it is enough.
+ * Here the fault is on the instruction FETCH at the target address, so
+ * there is no instruction to skip -- resuming anywhere near the fault
+ * just faults again. So the test saves the address it wants to come back
+ * to in abort_return, and this handler returns there on its behalf. */
+
+ ldr r0, =fault_expected_pabt
+ ldr r1, [r0]
+ cmp r1, #0
+ beq fault_el1_pabt_fatal
+
+ mov r1, #0
+ str r1, [r0] /* one-shot: disarm */
+
+ mrc p15, 0, r2, c5, c0, 1 /* IFSR */
+ ldr r0, =fault_syndrome
+ str r2, [r0]
+ mrc p15, 0, r2, c6, c0, 2 /* IFAR */
+ ldr r0, =fault_address
+ str r2, [r0]
+ ldr r0, =fault_vector
+ mov r1, #0x4C
+ str r1, [r0]
+ ldr r0, =fault_taken
+ ldr r1, [r0]
+ add r1, r1, #1
+ str r1, [r0]
+ dsb
+
+ ldr lr, =abort_return
+ ldr lr, [lr]
+ subs pc, lr, #0 /* return to the test, not here */
+
+fault_el1_pabt_fatal:
+ mrc p15, 0, r0, c5, c0, 1 /* IFSR */
+ mrc p15, 0, r3, c6, c0, 2 /* IFAR */
+ FAULT_TAIL 0x4C
+fault_el1_dabt:
+#ifdef TXM_MODULE_MANAGER
+ /* A fault from User mode is a module reaching outside its regions, and it
+ belongs to the module manager, which will record it and terminate the
+ thread. A fault from a privileged mode continues on the recoverable path
+ below, which the boot probes depend on.
+
+ r0 is pushed and popped around the test rather than simply used. The code
+ below clobbers r0 through r2 without saving them, which is tolerable for a
+ probe that provokes its own fault, but the module capture records the
+ faulting registers and would record ours instead. LDM does not affect the
+ flags, so the comparison still holds after the pop. */
+
+ stmdb sp!, {r0}
+ mrs r0, spsr
+ and r0, r0, #0x1F
+ cmp r0, #0x10 /* User mode? */
+ ldmia sp!, {r0}
+ beq _txm_module_manager_data_abort
+#endif
+
+ /* Recoverable when a test has armed fault_expected. The protection map is
+ only meaningful if a violation actually faults, so the test provokes one
+ deliberately and this path lets it continue: record the syndrome and
+ address, count it, then resume at the instruction AFTER the faulting one.
+ lr on data-abort entry is faulting_address + 8, so subs pc, lr, #4 skips
+ the access rather than retrying it forever.
+
+ Without the flag set, a data abort is still fatal and reported, which is
+ what any real bug should get. */
+
+ ldr r0, =fault_expected
+ ldr r1, [r0]
+ cmp r1, #0
+ beq fault_el1_dabt_fatal
+
+ mov r1, #0
+ str r1, [r0] /* one-shot: disarm */
+
+ mrc p15, 0, r2, c5, c0, 0 /* DFSR */
+ ldr r0, =fault_syndrome
+ str r2, [r0]
+ mrc p15, 0, r2, c6, c0, 0 /* DFAR */
+ ldr r0, =fault_address
+ str r2, [r0]
+ ldr r0, =fault_vector
+ mov r1, #0x50
+ str r1, [r0]
+ ldr r0, =fault_taken
+ ldr r1, [r0]
+ add r1, r1, #1
+ str r1, [r0]
+ dsb
+ subs pc, lr, #4 /* resume after the access */
+
+fault_el1_dabt_fatal:
+ mrc p15, 0, r0, c5, c0, 0 /* DFSR */
+ mrc p15, 0, r3, c6, c0, 0 /* DFAR */
+ FAULT_TAIL 0x50
+fault_el1_reserved:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x54
+fault_el1_irq:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x58
+fault_el1_fiq:
+ mov r0, #0
+ mov r3, lr
+ FAULT_TAIL 0x5C
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/gic_probe.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/gic_probe.c
new file mode 100644
index 00000000..b861ae5b
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/gic_probe.c
@@ -0,0 +1,72 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* gic_probe.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Asks the core where its GIC is, and whether it can be reached. */
+/* */
+/* A load from the GIC window NXP documents for this cluster stalls */
+/* the core outright, so nothing here touches memory. Both values */
+/* below are system registers: reading them cannot hang the bus. */
+/* */
+/* IMP_CBAR holds the physical base address of the memory-mapped GIC */
+/* distributor -- bits [31:21], wired by the SoC's CFGPERIPHBASE */
+/* input. That is the authoritative answer, as against an address */
+/* taken from a vendor debugger script. */
+/* */
+/* IMP_PERIPHPREGIONR describes the low-latency peripheral port: base */
+/* address in [31:12], size in [6:2], and separate enables for EL2 */
+/* (bit 1) and EL1/0 (bit 0). If the port is disabled, accesses in */
+/* its window go nowhere, which is what a stalled load looks like. */
+/* */
+/* Both from the Cortex-R52 TRM r1p3 (100026_0103_00_en), sections */
+/* 3.3.17 and 3.3.80 -- the SoC reference manual documents neither. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Dir 4.3 -- assembly encapsulated one access per function. */
+/* */
+/**************************************************************************/
+
+#include "gic_probe.h"
+
+unsigned int gic_read_cbar(void)
+{
+ unsigned int value;
+
+ __asm volatile ("mrc p15, 1, %0, c15, c3, 0" : "=r" (value));
+
+ return value;
+}
+
+
+unsigned int gic_read_periphpregionr(void)
+{
+ unsigned int value;
+
+ __asm volatile ("mrc p15, 0, %0, c15, c0, 0" : "=r" (value));
+
+ return value;
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/gic_probe.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/gic_probe.h
new file mode 100644
index 00000000..eaa992de
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/gic_probe.h
@@ -0,0 +1,44 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* gic_probe.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Where the GIC is, and whether the peripheral port reaches it. */
+/* */
+/**************************************************************************/
+
+#ifndef GIC_PROBE_H
+#define GIC_PROBE_H
+
+/* IMP_CBAR[31:21] = PERIPHBASE, the GIC distributor base. */
+
+unsigned int gic_read_cbar(void);
+
+/* IMP_PERIPHPREGIONR: LLPP base [31:12], size [6:2], EL2 enable bit 1,
+ EL1/0 enable bit 0. */
+
+unsigned int gic_read_periphpregionr(void);
+
+#endif /* GIC_PROBE_H */
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/gicv3.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/gicv3.c
new file mode 100644
index 00000000..1bca8dde
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/gicv3.c
@@ -0,0 +1,409 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* gicv3.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* GICv3 bring-up for Cortex-R52 on the Armv8-R AEM FVP. */
+/* */
+/* Model characteristics that shape this code, taken from its own */
+/* parameters rather than assumed: */
+/* */
+/* has-two-security-states=0 single security state, so GICD_CTLR.DS */
+/* reads 1 and the Group 1 enable is bit 1*/
+/* ARE-fixed-to-one=1 affinity routing cannot be turned off */
+/* priority-bits=5 only the top 5 priority bits exist, so */
+/* priorities must be multiples of 8 */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Directive 4.3 -- the CPU interface is only reachable through CP15 */
+/* system registers, so inline assembly is unavoidable. Every such */
+/* access is encapsulated in a one-line accessor below and appears */
+/* nowhere else. */
+/* Rule 11.4/11.6 -- casting integer addresses to volatile pointers is */
+/* inherent to memory-mapped device access; confined to REG32. */
+/* */
+/**************************************************************************/
+
+#include "platform.h"
+#include "gicv3.h"
+
+/* Distributor registers. */
+
+#define GICD_CTLR 0x0000U
+#define GICD_CTLR_ENABLE_GRP0 (1UL << 0)
+#define GICD_CTLR_ENABLE_GRP1 (1UL << 1)
+#define GICD_CTLR_ARE (1UL << 4)
+
+/* Redistributor RD frame. */
+
+#define GICR_WAKER 0x0014U
+#define GICR_WAKER_PROC_SLEEP (1UL << 1)
+#define GICR_WAKER_CHILD_ASLEEP (1UL << 2)
+
+/* Redistributor SGI frame (SGIs and PPIs, INTID 0-31). */
+
+#define GICR_IGROUPR0 0x0080U
+#define GICR_ISENABLER0 0x0100U
+#define GICR_IPRIORITYR 0x0400U
+#define GICR_ICFGR1 0x0C04U
+
+#define ICC_PMR_UNMASK_ALL 0xFFUL
+#define ICC_SRE_SRE (1UL << 0)
+#define ICC_IGRPEN1_ENABLE (1UL << 0)
+
+#define PPI_FIRST_INTID 16U
+
+
+/**************************************************************************/
+/* CPU interface accessors (AArch32 ICC_* system registers). */
+/**************************************************************************/
+
+static unsigned long read_icc_sre(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c12, c12, 5" : "=r"(value));
+ return value;
+}
+
+static void write_icc_sre(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 5" : : "r"(value) : "memory");
+}
+
+static void write_icc_pmr(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c4, c6, 0" : : "r"(value) : "memory");
+}
+
+static void write_icc_bpr1(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 3" : : "r"(value) : "memory");
+}
+
+static void write_icc_igrpen0(unsigned long value)
+{
+ /* ICC_IGRPEN0 sits at c12, c12, 6, beside IGRPEN1 at opc2 7. */
+
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 6" : : "r"(value) : "memory");
+}
+
+static void write_icc_bpr0(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c8, 3" : : "r"(value) : "memory");
+}
+
+static void write_icc_igrpen1(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 7" : : "r"(value) : "memory");
+}
+
+static void instruction_barrier(void)
+{
+ __asm__ volatile("isb" : : : "memory");
+}
+
+
+/**************************************************************************/
+/* gicv3_init */
+/**************************************************************************/
+
+void gicv3_init(void)
+{
+ unsigned long waker;
+
+ /* Distributor: affinity routing first, then the Group 1 enable. With a
+ single security state Group 1 is bit 1 of GICD_CTLR. */
+
+ REG32(GICD_BASE + GICD_CTLR) |= GICD_CTLR_ARE;
+ REG32(GICD_BASE + GICD_CTLR) |= GICD_CTLR_ENABLE_GRP1;
+
+ /* Group 0 as well, which is what the GIC delivers as FIQ. Enabling it costs
+ nothing while no interrupt is assigned to that group, and images that never
+ use FIQ see no change: assignment is per interrupt, in GICR_IGROUPR0. */
+
+ REG32(GICD_BASE + GICD_CTLR) |= GICD_CTLR_ENABLE_GRP0;
+
+ /* Redistributor: clear ProcessorSleep and wait for the redistributor to
+ report that its children are awake, otherwise no interrupt can be
+ delivered to this core. */
+
+ waker = REG32(GICR_RD_BASE + GICR_WAKER);
+ waker &= ~GICR_WAKER_PROC_SLEEP;
+ REG32(GICR_RD_BASE + GICR_WAKER) = waker;
+
+ while ((REG32(GICR_RD_BASE + GICR_WAKER) & GICR_WAKER_CHILD_ASLEEP) != 0UL)
+ {
+ /* Wait for the redistributor to wake. */
+ }
+
+ /* CPU interface: system-register access, then unmask all priorities and
+ enable Group 1. EL2 has already permitted EL1 system-register access
+ via ICC_HSRE in entry.S. */
+
+ write_icc_sre(read_icc_sre() | ICC_SRE_SRE);
+ instruction_barrier();
+
+ write_icc_pmr(ICC_PMR_UNMASK_ALL);
+ write_icc_bpr1(0UL);
+ write_icc_igrpen1(ICC_IGRPEN1_ENABLE);
+
+ /* The CPU interface half of Group 0: a Group 0 interrupt reaches the core
+ only if both the distributor and this are enabled. BPR0 at zero gives no
+ subpriority grouping, so priority alone decides preemption, which is what
+ the FIQ nesting test depends on. */
+
+ write_icc_bpr0(0UL);
+ write_icc_igrpen0(ICC_IGRPEN1_ENABLE);
+ instruction_barrier();
+}
+
+
+/**************************************************************************/
+/* gicv3_enable_ppi */
+/**************************************************************************/
+
+void gicv3_enable_ppi(unsigned int intid, unsigned int priority)
+{
+ unsigned long shift;
+
+ if ((intid < PPI_FIRST_INTID) || (intid > 31U))
+ {
+ return;
+ }
+
+ /* Group 1. */
+
+ REG32(GICR_SGI_BASE + GICR_IGROUPR0) |= (1UL << intid);
+
+ /* Priority is a byte per INTID. Only the top 5 bits are implemented on
+ this model, so callers pass multiples of 8. */
+
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) &=
+ ~(0xFFUL << ((intid & 3U) * 8U));
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) |=
+ ((unsigned long) priority & 0xFFUL) << ((intid & 3U) * 8U);
+
+ /* Level-sensitive: two configuration bits per INTID, 00 = level. The
+ generic timer asserts a level, not an edge. */
+
+ shift = ((unsigned long) intid - PPI_FIRST_INTID) * 2UL;
+ REG32(GICR_SGI_BASE + GICR_ICFGR1) &= ~(3UL << shift);
+
+ /* Enable. */
+
+ REG32(GICR_SGI_BASE + GICR_ISENABLER0) = (1UL << intid);
+}
+
+
+/**************************************************************************/
+/* gicv3_acknowledge */
+/**************************************************************************/
+
+/**************************************************************************/
+/* gicv3_enable_sgi */
+/* */
+/* An SGI lives in the same redistributor frame as a PPI, so this is */
+/* gicv3_enable_ppi without the ICFGR step: INTIDs 0-15 have no */
+/* configurable edge/level, they are always edge-triggered. */
+/**************************************************************************/
+
+void gicv3_enable_sgi(unsigned int intid, unsigned int priority)
+{
+ if (intid > 15U)
+ {
+ return;
+ }
+
+ REG32(GICR_SGI_BASE + GICR_IGROUPR0) |= (1UL << intid);
+
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) &=
+ ~(0xFFUL << ((intid & 3U) * 8U));
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) |=
+ ((unsigned long) priority & 0xFFUL) << ((intid & 3U) * 8U);
+
+ REG32(GICR_SGI_BASE + GICR_ISENABLER0) = (1UL << intid);
+}
+
+
+/**************************************************************************/
+/* gicv3_send_sgi */
+/* */
+/* ICC_SGI1R is 64-bit, so in AArch32 it is an MCRR rather than an MCR. */
+/* Fields: INTID in [27:24], TargetList in [15:0], Aff1/2/3 and IRM zero */
+/* for this single-core configuration, so TargetList = 1 selects core 0. */
+/* */
+/* The AArch64 name for this register is S3_0_C12_C11_5, which the */
+/* Cortex-A72 example uses, but that encoding does not transcribe to the */
+/* AArch32 64-bit CP15 space. The CRm here was confirmed by observing */
+/* that the SGI is actually delivered and acknowledged with the expected */
+/* INTID rather than by reading it off the A-profile alias. */
+/**************************************************************************/
+
+void gicv3_send_sgi(unsigned int intid)
+{
+ unsigned long low = (((unsigned long) intid & 0xFUL) << 24) | 1UL;
+ unsigned long high = 0UL;
+
+ __asm volatile ("mcrr p15, 0, %0, %1, c12" :: "r" (low), "r" (high) : "memory");
+ instruction_barrier();
+}
+
+
+/**************************************************************************/
+/* gicv3_priority_bits */
+/* */
+/* How many priority bits this GIC actually implements, discovered rather */
+/* than assumed: write 0xFF to a priority byte and see which bits stick. */
+/* The unimplemented bits are the low ones and they read as zero. */
+/* */
+/* This matters for nesting. Two interrupts only preempt one another if */
+/* their priorities differ AFTER truncation, so a test that picks values */
+/* 0x20 apart on a GIC keeping four bits is testing nothing. The FVP */
+/* keeps five; real silicon need not agree. */
+/* */
+/* Called before the SGI is configured, and it leaves the byte at zero, so */
+/* the caller must set the priority it wants afterwards. */
+/**************************************************************************/
+
+unsigned int gicv3_priority_bits(unsigned int scratch_intid)
+{
+ unsigned long shift;
+ unsigned long readback;
+ unsigned int bits = 0U;
+
+ if (scratch_intid > 31U)
+ {
+ return 0U;
+ }
+
+ shift = (unsigned long) (scratch_intid & 3U) * 8UL;
+
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (scratch_intid & ~3U)) |=
+ (0xFFUL << shift);
+ readback = (REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (scratch_intid & ~3U))
+ >> shift) & 0xFFUL;
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (scratch_intid & ~3U)) &=
+ ~(0xFFUL << shift);
+
+ while (readback != 0UL)
+ {
+ if ((readback & 1UL) != 0UL)
+ {
+ bits++;
+ }
+ readback >>= 1;
+ }
+
+ return bits;
+}
+
+
+/**************************************************************************/
+/* gicv3_enable_sgi_group0 */
+/* */
+/* As gicv3_enable_sgi, but leaves the interrupt in Group 0 so it arrives */
+/* as an FIQ. The group is chosen by clearing the GICR_IGROUPR0 bit; the */
+/* Group 1 version sets it. */
+/**************************************************************************/
+
+void gicv3_enable_sgi_group0(unsigned int intid, unsigned int priority)
+{
+ if (intid > 15U)
+ {
+ return;
+ }
+
+ REG32(GICR_SGI_BASE + GICR_IGROUPR0) &= ~(1UL << intid);
+
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) &=
+ ~(0xFFUL << ((intid & 3U) * 8U));
+ REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) |=
+ ((unsigned long) priority & 0xFFUL) << ((intid & 3U) * 8U);
+
+ REG32(GICR_SGI_BASE + GICR_ISENABLER0) = (1UL << intid);
+}
+
+
+/**************************************************************************/
+/* gicv3_send_sgi_group0 */
+/* */
+/* ICC_SGI0R rather than ICC_SGI1R: the two differ only in opc1, 2 against */
+/* 0, and each raises the SGI into its own group. Raising a Group 0 */
+/* interrupt through the Group 1 register does not deliver it as an FIQ. */
+/**************************************************************************/
+
+void gicv3_send_sgi_group0(unsigned int intid)
+{
+ unsigned long low = (((unsigned long) intid & 0xFUL) << 24) | 1UL;
+ unsigned long high = 0UL;
+
+ __asm volatile ("mcrr p15, 2, %0, %1, c12" :: "r" (low), "r" (high) : "memory");
+ instruction_barrier();
+}
+
+
+/**************************************************************************/
+/* gicv3_acknowledge_group0 / gicv3_end_of_interrupt_group0 */
+/* */
+/* Group 0 has its own pair at c12, c8, where Group 1 uses c12, c12. */
+/* Acknowledging a Group 0 interrupt through IAR1 returns the spurious */
+/* INTID and leaves the interrupt pending, which presents as an FIQ storm. */
+/**************************************************************************/
+
+unsigned long gicv3_acknowledge_group0(void)
+{
+ unsigned long intid;
+
+ __asm__ volatile("mrc p15, 0, %0, c12, c8, 0" : "=r"(intid));
+ return intid & 0xFFFFFFUL;
+}
+
+
+void gicv3_end_of_interrupt_group0(unsigned long intid)
+{
+ instruction_barrier();
+ __asm__ volatile("mcr p15, 0, %0, c12, c8, 1" : : "r"(intid) : "memory");
+}
+
+
+unsigned long gicv3_acknowledge(void)
+{
+ unsigned long intid;
+
+ __asm__ volatile("mrc p15, 0, %0, c12, c12, 0" : "=r"(intid));
+
+ return intid;
+}
+
+
+/**************************************************************************/
+/* gicv3_end_of_interrupt */
+/**************************************************************************/
+
+void gicv3_end_of_interrupt(unsigned long intid)
+{
+ __asm__ volatile("mcr p15, 0, %0, c12, c12, 1" : : "r"(intid) : "memory");
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/gicv3.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/gicv3.h
new file mode 100644
index 00000000..184a5b6d
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/gicv3.h
@@ -0,0 +1,86 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* gicv3.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* GICv3 interrupt controller for Cortex-R52. The CPU interface is */
+/* reached through AArch32 ICC_* system registers; the Distributor and */
+/* Redistributor are memory mapped. */
+/* */
+/**************************************************************************/
+
+#ifndef GICV3_H
+#define GICV3_H
+
+/* Spurious interrupt ID returned by the CPU interface when no interrupt is
+ pending. It must be neither dispatched nor acknowledged with EOI. */
+
+#define GICV3_SPURIOUS_INTID 1023UL
+
+/* Bring up the Distributor, this core's Redistributor and the CPU
+ interface. Interrupts remain masked at the PSTATE level. */
+
+void gicv3_init(void);
+
+/* Enable one private peripheral interrupt (PPI, INTID 16-31) as a
+ level-sensitive Group 1 interrupt at the given priority. */
+
+void gicv3_enable_ppi(unsigned int intid, unsigned int priority);
+
+/* Enable a software-generated interrupt, INTID 0-15. Same registers as a PPI:
+ the redistributor's SGI/PPI frame covers INTIDs 0-31. */
+
+void gicv3_enable_sgi(unsigned int intid, unsigned int priority);
+
+/* Raise an SGI on this core. */
+
+void gicv3_send_sgi(unsigned int intid);
+
+/* Group 0 support, which is what produces FIQ rather than IRQ. With a single
+ security state the GIC routes Group 0 to FIQ and Group 1 to IRQ, so an
+ interrupt has to be moved into Group 0 to arrive as an FIQ at all. Group 0
+ has its own acknowledge and end-of-interrupt registers; using the Group 1 ones
+ on a Group 0 interrupt does not work. */
+
+void gicv3_enable_sgi_group0(unsigned int intid, unsigned int priority);
+void gicv3_send_sgi_group0(unsigned int intid);
+unsigned long gicv3_acknowledge_group0(void);
+void gicv3_end_of_interrupt_group0(unsigned long intid);
+
+/* Implemented priority bits, discovered by write and readback. Leaves the
+ scratch INTID's priority byte at zero. */
+
+unsigned int gicv3_priority_bits(unsigned int scratch_intid);
+
+/* Acknowledge the highest-priority pending Group 1 interrupt, returning its
+ INTID (possibly GICV3_SPURIOUS_INTID). */
+
+unsigned long gicv3_acknowledge(void);
+
+/* Signal completion of the interrupt previously acknowledged. */
+
+void gicv3_end_of_interrupt(unsigned long intid);
+
+#endif /* GICV3_H */
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/irq_dispatch.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/irq_dispatch.c
new file mode 100644
index 00000000..92fbe97b
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/irq_dispatch.c
@@ -0,0 +1,462 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* irq_dispatch.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Interrupt dispatch for the S32Z280-594EVB, standalone. */
+/* */
+/* Called from el1_irq_entry in entry.S. Unlike the FVP example this */
+/* does not call _tx_timer_interrupt: the image does not link ThreadX, */
+/* so a failure here is a board or GIC problem and not a kernel one. */
+/* It counts ticks instead, which is what makes the interrupt visible. */
+/* */
+/* Every count is volatile and read by the debugger as well as printed.*/
+/* An interrupt that never arrives and an interrupt that arrives and is*/
+/* mishandled look identical from the console alone, and the separate */
+/* spurious and unexpected counters distinguish them. */
+/* */
+/**************************************************************************/
+
+#include "gicv3.h"
+#include "timer.h"
+#include "board.h"
+#include "mpu.h"
+#ifdef TX_R52_USE_THREADX_IRQ
+#include "tx_api.h"
+
+/* ThreadX periodic timer entry point. Assembly, and internal to the kernel,
+ so it is not declared by tx_api.h. Safe to call from C: it uses only r0-r3
+ and returns through lr like an ordinary function. */
+
+extern void _tx_timer_interrupt(void);
+#endif
+
+/* Observable from both the console and a debugger. */
+
+volatile unsigned long board_irq_count;
+volatile unsigned long board_timer_intid;
+volatile unsigned long board_spurious_count;
+volatile unsigned long board_unexpected_intid;
+volatile unsigned long board_first_intid = 0xFFFFFFFFUL;
+
+volatile unsigned long board_nest_depth;
+volatile unsigned long board_nest_max;
+volatile unsigned long board_sgi_count;
+volatile unsigned long board_sgi_nested_count;
+volatile unsigned long board_nest_provoke;
+
+volatile unsigned long board_fiq_count;
+volatile unsigned long board_fiq_high_count;
+volatile unsigned long board_fiq_nested_count;
+volatile unsigned long board_fiq_depth;
+volatile unsigned long board_fiq_max_depth;
+volatile unsigned long board_fiq_unexpected;
+volatile unsigned long board_fiq_provoke;
+volatile unsigned long board_priority_bits;
+volatile unsigned long board_timer_priority;
+volatile unsigned long board_sgi_priority;
+
+/* The EL1 physical timer PPI. 30 is the architectural assignment and is what
+ the FVP turned out to use, but it was discovered there by enabling the whole
+ PPI range and recording whichever INTID arrived in ICC_IAR1 rather than by
+ assuming. board_timer_intid records what actually arrives here, so a wrong
+ value shows up as an unexpected INTID rather than as silence. */
+
+#define TIMER_PPI_INTID 30U
+
+/* Only the top priority bits are implemented; hence a multiple of 8. */
+
+#define TIMER_PPI_PRIORITY 0xA0U
+
+
+#ifdef TX_R52_USE_THREADX_IRQ
+
+/**************************************************************************/
+/* board_init -- called from _tx_initialize_low_level. */
+/* */
+/* Order matters. mpu_init() must run before anything touches the GIC: */
+/* the distributor is unreachable until its region is mapped Device */
+/* nGnRnE, and an access before that aborts. Arming the timer here is */
+/* safe because interrupts stay masked until _tx_thread_schedule enables */
+/* them, so no tick can arrive before the kernel can service one. */
+/**************************************************************************/
+
+void board_init(void)
+{
+ (void) mpu_init();
+ gicv3_init();
+ gicv3_enable_ppi(TIMER_PPI_INTID, TIMER_PPI_PRIORITY);
+
+ /* How many priority bits this GIC keeps decides whether the SGI can preempt
+ the timer at all: equal priorities do not preempt, and it is the low bits
+ that vanish. Discovered rather than assumed, because the FVP keeps five
+ and real silicon need not agree. Recorded so the demo can report it, and
+ a nesting failure caused by the two priorities colliding is visible
+ instead of mysterious. */
+
+ board_priority_bits = gicv3_priority_bits(BOARD_NEST_SGI_INTID);
+
+ /* Half the timer's value so the SGI outranks it: numerically lower wins.
+ With TIMER_PPI_PRIORITY at 0xA0 that is 0x50, which stays distinct even
+ if only three priority bits survive truncation. */
+
+ gicv3_enable_sgi(BOARD_NEST_SGI_INTID, TIMER_PPI_PRIORITY / 2U);
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+
+ /* Two Group 0 SGIs for the FIQ path. Group 0 is what the GIC delivers as
+ FIQ; the high one takes a numerically lower priority so it can preempt the
+ low one, which is what FIQ nesting means. Both sit above the timer so the
+ IRQ and FIQ paths do not interfere. */
+
+ gicv3_enable_sgi_group0(BOARD_FIQ_LOW_INTID, 0x40U);
+ gicv3_enable_sgi_group0(BOARD_FIQ_HIGH_INTID, 0x20U);
+#endif
+
+ /* The priorities as the GIC will actually compare them. Only the top
+ board_priority_bits survive; the low ones read back as zero, so two values
+ that differ only there would collapse together and never preempt. Recorded
+ here because this is where the constants live. */
+
+ {
+ unsigned long keep = (board_priority_bits >= 8UL)
+ ? 0xFFUL
+ : ((0xFFUL << (8UL - board_priority_bits)) & 0xFFUL);
+
+ board_timer_priority = (unsigned long) TIMER_PPI_PRIORITY & keep;
+ board_sgi_priority = ((unsigned long) TIMER_PPI_PRIORITY / 2UL) & keep;
+ }
+
+ /* One tick every 10 ms at the measured 8 MHz counter rate. */
+
+ timer_start_oneshot_irq(timer_read_cntfrq() / 100U);
+}
+
+#endif
+
+
+/**************************************************************************/
+/* board_irq_service -- service an already-acknowledged INTID. */
+/* */
+/* Split out so the nesting path in entry.S can acknowledge in IRQ mode */
+/* before nesting starts. Does not acknowledge and does not EOI. */
+/**************************************************************************/
+
+/**************************************************************************/
+/* Handler execution time, and where the handler's instructions live. */
+/* */
+/* TX_R52_ATCM_ISR places the service routine in ATCM. ATCM runs at full */
+/* core speed with one wait state; .text lives in RTU code RAM, which runs */
+/* at half the core frequency (S32Z2 RM 6.3.6). ATCM also has no cache to */
+/* miss, which is the property that matters for a determinism argument. */
+/* */
+/* Measured in cycles from the PMU counter, not CNTPCT: at 8 MHz CNTPCT */
+/* cannot resolve a handler body, let alone the variation in one. */
+/* */
+/* The timing wrapper below stays in .text in both configurations, so its */
+/* own cost appears in every sample and cancels when the two are compared. */
+/* Only the body moves. What matters in the result is the spread: a warm */
+/* instruction cache can match ATCM on the mean and cannot match it on the */
+/* worst case. */
+/**************************************************************************/
+
+#define BOARD_LATENCY_SAMPLES 64U
+
+unsigned int board_service_cycles[BOARD_LATENCY_SAMPLES];
+unsigned int board_service_count;
+
+#ifdef TX_R52_ATCM_ISR
+#define BOARD_ISR_SECTION __attribute__((section(".atcm_text")))
+#else
+#define BOARD_ISR_SECTION
+#endif
+
+static inline __attribute__((always_inline))
+void board_irq_service_impl(unsigned long intid);
+
+
+
+
+/* Aligned for the same reason as cache_workload: this body is timed, and
+ without a fixed alignment the figure moves with unrelated code changes
+ elsewhere in the image. */
+
+/* Four placements of one implementation. The logic below is inlined into each
+ of the four wrappers further down, each 64-byte aligned and then displaced by
+ a different offset within the line, and the measurement rotates through them.
+
+ A single placement is not measurable. The cache benchmark in this file's
+ sibling turned out to be bimodal with respect to exactly this, reporting 24%
+ or 0% for identical silicon depending on where a loop fell in a line, and it
+ invalidated a handler comparison and a defect report to NXP before anyone
+ noticed. Pinning one alignment does not fix that, it just chooses a mode. */
+
+static inline __attribute__((always_inline))
+void board_irq_service_impl(unsigned long intid)
+{
+ if (intid == GICV3_SPURIOUS_INTID)
+ {
+ /* Nothing was actually pending. Must not be acknowledged with an
+ end-of-interrupt write, so return without one. */
+
+ board_spurious_count++;
+ return;
+ }
+
+ /* After the spurious check, so a spurious entry does not read as nesting.
+ Reaches 2 only if a second interrupt is taken while this one is still on
+ the stack, which cannot happen without TX_ENABLE_IRQ_NESTING. */
+
+ board_nest_depth++;
+ if (board_nest_depth > board_nest_max)
+ {
+ board_nest_max = board_nest_depth;
+ }
+
+ if (board_first_intid == 0xFFFFFFFFUL)
+ {
+ board_first_intid = intid; /* whatever actually arrived */
+ }
+
+ if (intid == (unsigned long) TIMER_PPI_INTID)
+ {
+ board_timer_intid = intid;
+ board_irq_count++;
+
+ /* Re-arm first. The generic timer output is level-sensitive, so until
+ the compare is moved forward the condition stays asserted and the
+ interrupt would be taken again immediately on return. */
+
+ timer_start_oneshot_irq(timer_read_cntfrq() / 100U);
+
+ /* Provoke nesting, if this image asked for it. Only useful because
+ _tx_thread_irq_nesting_start has already re-enabled IRQ; the bounded
+ spin gives the GIC time to deliver the higher-priority SGI before this
+ handler returns, so the nesting is deterministic rather than a race we
+ hope wins. The bound matters: with nesting compiled out the SGI can
+ never arrive here and this must not become a hang. */
+
+ if (board_nest_provoke != 0UL)
+ {
+ unsigned long seen = board_sgi_count;
+ unsigned long guard;
+
+ gicv3_send_sgi(BOARD_NEST_SGI_INTID);
+
+ for (guard = 0UL; guard < 100000UL; guard++)
+ {
+ if (board_sgi_count != seen)
+ {
+ break;
+ }
+ }
+ }
+
+#ifdef TX_R52_USE_THREADX_IRQ
+ /* Drive the kernel's time base. Called after the re-arm so the next
+ interval is already running while the kernel does its bookkeeping. */
+
+ _tx_timer_interrupt();
+#endif
+ }
+ else if (intid == (unsigned long) BOARD_NEST_SGI_INTID)
+ {
+ board_sgi_count++;
+
+ if (board_nest_depth > 1UL)
+ {
+ board_sgi_nested_count++;
+ }
+ }
+ else
+ {
+ board_unexpected_intid = intid;
+ }
+
+ board_nest_depth--;
+}
+
+
+/* The four placements. pad nops shift the inlined body within its line. */
+
+#define MAKE_ISR_BODY(name, pad) \
+__attribute__((aligned(64), noinline)) \
+static BOARD_ISR_SECTION void name(unsigned long intid) \
+{ \
+ __asm__ volatile(".rept " #pad "\n\tnop\n\t.endr"); \
+ board_irq_service_impl(intid); \
+}
+
+MAKE_ISR_BODY(board_irq_body_a, 0)
+MAKE_ISR_BODY(board_irq_body_b, 4)
+MAKE_ISR_BODY(board_irq_body_c, 8)
+MAKE_ISR_BODY(board_irq_body_d, 12)
+
+#define BOARD_ISR_PLACEMENTS 4U
+#define BOARD_SAMPLES_EACH (BOARD_LATENCY_SAMPLES / BOARD_ISR_PLACEMENTS)
+
+static void (*const board_irq_bodies[BOARD_ISR_PLACEMENTS])(unsigned long) =
+{
+ board_irq_body_a, /* body at line offset 0 */
+ board_irq_body_b, /* body at line offset 16 */
+ board_irq_body_c, /* body at line offset 32 */
+ board_irq_body_d /* body at line offset 48 */
+};
+
+
+void board_irq_service(unsigned long intid)
+{
+ unsigned int before;
+ unsigned int after;
+
+ /* A block of samples per placement, so each alignment is measured under the
+ same conditions rather than interleaved with the others. */
+
+ {
+ unsigned int slot = board_service_count / BOARD_SAMPLES_EACH;
+
+ if (slot >= BOARD_ISR_PLACEMENTS)
+ {
+ slot = BOARD_ISR_PLACEMENTS - 1U;
+ }
+
+ before = timer_read_cycles();
+ board_irq_bodies[slot](intid);
+ after = timer_read_cycles();
+ }
+
+ if (board_service_count < BOARD_LATENCY_SAMPLES)
+ {
+ board_service_cycles[board_service_count] = after - before;
+ board_service_count++;
+ }
+}
+
+
+/**************************************************************************/
+/* board_irq_handler -- the non-nesting entry point. */
+/* */
+/* Acknowledges, services and ends the interrupt in IRQ mode with */
+/* interrupts masked, which is what every image built without */
+/* TX_ENABLE_IRQ_NESTING did before this split. */
+/**************************************************************************/
+
+void board_irq_handler(void)
+{
+ unsigned long intid = gicv3_acknowledge();
+
+ board_irq_service(intid);
+
+ if (intid != GICV3_SPURIOUS_INTID)
+ {
+ gicv3_end_of_interrupt(intid);
+ }
+}
+
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+/**************************************************************************/
+/* board_fiq_service -- service an already-acknowledged Group 0 INTID. */
+/* */
+/* The FIQ counterpart of board_irq_service, and split for the same */
+/* reason: the acknowledge has to happen before nesting starts. Does not */
+/* acknowledge and does not EOI. */
+/**************************************************************************/
+
+void board_fiq_service(unsigned long intid)
+{
+ if (intid == GICV3_SPURIOUS_INTID)
+ {
+ return;
+ }
+
+ board_fiq_depth++;
+ if (board_fiq_depth > board_fiq_max_depth)
+ {
+ board_fiq_max_depth = board_fiq_depth;
+ }
+
+ if (intid == (unsigned long) BOARD_FIQ_LOW_INTID)
+ {
+ board_fiq_count++;
+
+ /* Provoke the nested FIQ from inside this handler. Only reachable
+ because _tx_thread_fiq_nesting_start has re-enabled FIQ; the bounded
+ spin lets the GIC deliver the higher-priority Group 0 SGI before this
+ handler returns. Bounded, because with nesting compiled out the
+ second FIQ cannot arrive and this must not hang. */
+
+ if (board_fiq_provoke != 0UL)
+ {
+ unsigned long seen = board_fiq_high_count;
+ unsigned long guard;
+
+ gicv3_send_sgi_group0(BOARD_FIQ_HIGH_INTID);
+
+ for (guard = 0UL; guard < 100000UL; guard++)
+ {
+ if (board_fiq_high_count != seen)
+ {
+ break;
+ }
+ }
+ }
+ }
+ else if (intid == (unsigned long) BOARD_FIQ_HIGH_INTID)
+ {
+ board_fiq_high_count++;
+
+ /* Depth above 1 means this FIQ arrived inside another FIQ handler. */
+
+ if (board_fiq_depth > 1UL)
+ {
+ board_fiq_nested_count++;
+ }
+ }
+ else
+ {
+ board_fiq_unexpected = intid;
+ }
+
+ board_fiq_depth--;
+}
+
+
+/**************************************************************************/
+/* board_fiq_handler -- the non-nesting FIQ entry point. */
+/**************************************************************************/
+
+void board_fiq_handler(void)
+{
+ unsigned long intid = gicv3_acknowledge_group0();
+
+ board_fiq_service(intid);
+
+ if (intid != GICV3_SPURIOUS_INTID)
+ {
+ gicv3_end_of_interrupt_group0(intid);
+ }
+}
+#endif
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/linflexd.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/linflexd.c
new file mode 100644
index 00000000..cc6a7773
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/linflexd.c
@@ -0,0 +1,276 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* linflexd.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Polled console on LINFlex_9, which is the instance wired to the */
+/* S32Z280-594EVB daughtercard USB-UART through jumper J248. Not */
+/* LINFlex_0: that is simply the first instance in the Reference */
+/* Manual's list and reaches no connector on this board. */
+/* */
+/* BAUD RATE */
+/* */
+/* LINFlex_9 is clocked by P5_LIN_BAUD_CLK, which MC_CGM_5 MUX 2 */
+/* drives. Read from the board: MUX_2_CSS selects source 2 with the */
+/* divider set to 1, and the EVB carries a 40 MHz crystal (UG10268 */
+/* section 3.3.1.1), so the clock is 40 MHz. */
+/* */
+/* LFDIV = 40000000 / (16 * 115200) = 21.7014 */
+/* LINIBRR = 21 */
+/* LINFBRR = round(0.7014 * 16) = 11 */
+/* actual = 40000000 / (16 * 21.6875) = 115274 baud, 0.06% error */
+/* */
+/* Those are exactly the values the BootROM had already left in the */
+/* registers, which is what confirms the 40 MHz figure rather than */
+/* merely assuming it. They are programmed here anyway: inheriting */
+/* another program's register state makes this image's behaviour */
+/* depend on how the board was last booted. */
+/* */
+/* The BootROM's configuration is NOT reused for a second reason: it */
+/* leaves PCE set (parity enabled) and TxEn clear, because it is */
+/* listening for a serial-boot download rather than printing. A host */
+/* terminal set to 8N1 against that would show framing errors. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Rule 11.4/11.6 -- casting integer addresses to volatile pointers is */
+/* inherent to memory-mapped device access; confined to REG32. */
+/* */
+/**************************************************************************/
+
+#include "linflexd.h"
+#include "platform.h"
+
+/* Register offsets, from the S32Z2 Reference Manual section 75.5.1. */
+
+#define LINFLEXD_LINCR1 0x00U
+#define LINFLEXD_LINSR 0x08U
+#define LINFLEXD_UARTCR 0x10U
+#define LINFLEXD_UARTSR 0x14U
+#define LINFLEXD_LINFBRR 0x24U
+#define LINFLEXD_LINIBRR 0x28U
+#define LINFLEXD_BDRL 0x38U
+
+/* LINCR1 */
+#define LINCR1_INIT (1U << 0) /* initialisation mode */
+#define LINCR1_SLEEP (1U << 1)
+
+/* UARTCR. Bit positions verified against the register diagram; PCE at bit 2
+ and TxEn at bit 4 are the two that matter here and are easy to transpose. */
+#define UARTCR_UART (1U << 0) /* UART rather than LIN mode */
+#define UARTCR_WL0 (1U << 1) /* with WL1 clear: 8-bit */
+#define UARTCR_PCE (1U << 2) /* parity enable */
+#define UARTCR_TXEN (1U << 4)
+#define UARTCR_RXEN (1U << 5)
+
+/* UARTSR */
+#define UARTSR_DTF (1U << 1) /* transmit complete, w1c */
+
+/* LINSR[15:12] is the LIN state; 0x1 is initialisation mode. Confirmed by
+ reading 0x2000 (idle) from the BootROM-configured module. */
+#define LINSR_LINS_MASK (0xFU << 12)
+#define LINSR_LINS_INIT (0x1U << 12)
+
+/* Bounded rather than infinite: a console that hangs the boot is worse than
+ one that reports it could not configure itself. */
+#define LINFLEXD_INIT_GUARD 100000U
+
+/* 115200 from a 40 MHz P5_LIN_BAUD_CLK; see the file header. */
+#define LINFLEXD_IBRR_115200 21U
+#define LINFLEXD_FBRR_115200 11U
+
+#define CONSOLE_BASE S32Z_LINFLEX_9_BASE
+
+
+/* One full pass of the configuration sequence. Called twice by
+ linflexd_init; see the comment there for why. */
+
+static unsigned int linflexd_configure_once(void)
+{
+ unsigned int status = LINFLEXD_INIT_OK;
+ unsigned int guard;
+ unsigned int wanted;
+ unsigned int got;
+
+ /* Enter initialisation mode and WAIT until the module reports it.
+ Most of UARTCR is writable only in initialisation mode, and the module
+ does not enter it in the same cycle as the LINCR1 write. Configuring
+ without waiting silently half-works: bits being *set* take effect while
+ bits being *cleared* do not, so PCE stays enabled and the line runs 8E1
+ against a host expecting 8N1 -- which corrupts only those characters
+ whose parity bit happens to be 0 and leaves the rest readable, looking
+ for all the world like a marginal baud rate. */
+
+ REG32(CONSOLE_BASE + LINFLEXD_LINCR1) = LINCR1_INIT;
+
+ guard = LINFLEXD_INIT_GUARD;
+ while (((REG32(CONSOLE_BASE + LINFLEXD_LINSR) & LINSR_LINS_MASK)
+ != LINSR_LINS_INIT) && (guard > 0U))
+ {
+ guard--;
+ }
+
+ if (guard == 0U)
+ {
+ status |= LINFLEXD_INIT_NO_INITMODE;
+ }
+
+ /* The UART bit gates writes to the rest of UARTCR, so it has to be set
+ before the fields that depend on it, not alongside them. */
+
+ REG32(CONSOLE_BASE + LINFLEXD_UARTCR) = UARTCR_UART;
+
+ /* 8N1, transmit and receive. PCE deliberately left clear: the BootROM
+ enables parity for serial boot, and a host on 8N1 would see framing
+ errors. */
+
+ wanted = UARTCR_UART | UARTCR_WL0 | UARTCR_TXEN | UARTCR_RXEN;
+ REG32(CONSOLE_BASE + LINFLEXD_UARTCR) = wanted;
+
+ REG32(CONSOLE_BASE + LINFLEXD_LINFBRR) = LINFLEXD_FBRR_115200;
+ REG32(CONSOLE_BASE + LINFLEXD_LINIBRR) = LINFLEXD_IBRR_115200;
+
+ /* Read back before leaving initialisation mode. A write that was
+ silently ignored is the failure this driver already made once, so it is
+ checked rather than assumed. */
+
+ got = REG32(CONSOLE_BASE + LINFLEXD_UARTCR) & 0xFFU;
+ if (got != wanted)
+ {
+ status |= LINFLEXD_INIT_UARTCR_MISMATCH;
+ }
+
+ /* Leave initialisation mode; the module starts operating. */
+
+ REG32(CONSOLE_BASE + LINFLEXD_LINCR1) = 0U;
+
+ return status;
+}
+
+
+unsigned int linflexd_init(void)
+{
+ unsigned int status;
+
+ /* The sequence runs twice, and one pass is genuinely not enough.
+ A single pass gives a working console when this file is compiled at -O0
+ and a corrupted one at -O2: every character partially wrong, while UARTCR
+ reads back exactly the value written, LINIBRR and LINFBRR read back
+ exactly the values written, and the returned status is
+ LINFLEXD_INIT_OK. The registers are right and the line is wrong, so the
+ failure is invisible to the caller -- the worst property a console can
+ have, and the reason this is worth two passes at boot.
+
+ The mechanism is not understood. Tested and rejected as explanations:
+ the wait for initialisation mode (a bound 100x larger changes nothing),
+ a settling delay before the first LINSR read, a settling delay after
+ leaving initialisation mode, a barrier and read-back between the two
+ UARTCR writes, and waiting for LINSR to report the exit from
+ initialisation mode. None of those makes a single pass work at -O2.
+ A second pass does, reliably, at both optimisation levels.
+
+ Verified on the S32Z280-594EVB with the whole BSP at -O0 and at -O2.
+ If the underlying behaviour is ever identified, revisit this. */
+
+ status = linflexd_configure_once();
+ status |= linflexd_configure_once();
+
+ return status;
+}
+
+
+void linflexd_putc(char c)
+{
+ if (c == '\n')
+ {
+ linflexd_putc('\r');
+ }
+
+ unsigned int guard;
+
+ /* Start the byte, wait for completion, clear the flag, then wait for the
+ clear to actually take effect.
+ *
+ * The last step is the subtle one. DTF is write-one-to-clear and does
+ * not de-assert in the same cycle as the clearing write, so without it
+ * the *next* byte's poll can observe this byte's flag, conclude the line
+ * is free while it is still busy, and have its write silently dropped by
+ * the transmitter. That cost exactly one character after every "\r\n"
+ * pair -- the only place two bytes are sent back to back -- and showed
+ * up as the first letter of every line going missing.
+ *
+ * Clearing *before* the write instead does not fix it and is worse: the
+ * write then lands while the previous byte is still shifting and is
+ * dropped, and the poll afterwards sees the previous byte's completion,
+ * so most of the output disappears. Order matters in both directions.
+ */
+
+ REG32(CONSOLE_BASE + LINFLEXD_BDRL) = (unsigned int)(unsigned char)c;
+
+ /* Polled on purpose: this console runs before any interrupt controller is
+ configured, and it must work inside a fault handler. */
+
+ while ((REG32(CONSOLE_BASE + LINFLEXD_UARTSR) & UARTSR_DTF) == 0U)
+ {
+ /* wait for this byte to go out */
+ }
+
+ REG32(CONSOLE_BASE + LINFLEXD_UARTSR) = UARTSR_DTF;
+
+ guard = LINFLEXD_INIT_GUARD;
+ while (((REG32(CONSOLE_BASE + LINFLEXD_UARTSR) & UARTSR_DTF) != 0U)
+ && (guard > 0U))
+ {
+ guard--;
+ }
+}
+
+
+void linflexd_puts(const char *string)
+{
+ const char *p = string;
+
+ if (p != (const char *)0)
+ {
+ while (*p != '\0')
+ {
+ linflexd_putc(*p);
+ p++;
+ }
+ }
+}
+
+
+void linflexd_put_hex32(unsigned int value)
+{
+ static const char digits[] = "0123456789ABCDEF";
+ unsigned int shift = 32U;
+
+ while (shift > 0U)
+ {
+ shift -= 4U;
+ linflexd_putc(digits[(value >> shift) & 0xFU]);
+ }
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/linflexd.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/linflexd.h
new file mode 100644
index 00000000..e1945733
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/linflexd.h
@@ -0,0 +1,61 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* linflexd.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Polled transmit-only console on LINFlexD, for the S32Z280-594EVB. */
+/* */
+/**************************************************************************/
+
+#ifndef LINFLEXD_H
+#define LINFLEXD_H
+
+/* Status bits returned by linflexd_init(). */
+
+#define LINFLEXD_INIT_OK 0x00U
+#define LINFLEXD_INIT_NO_INITMODE 0x01U /* never entered init mode */
+#define LINFLEXD_INIT_UARTCR_MISMATCH 0x02U /* a write was ignored */
+
+/* Configure LINFlex_9 for 115200 8N1 and enable the transmitter. Returns
+ LINFLEXD_INIT_OK, or a mask of the above. It is worth checking: writes to
+ UARTCR outside initialisation mode are silently dropped, which produces
+ output that looks like a baud-rate problem rather than a framing one. */
+
+unsigned int linflexd_init(void);
+
+/* Blocking single character. Translates \n to \r\n so the output is
+ readable on a terminal that does not add the carriage return itself. */
+
+void linflexd_putc(char c);
+
+void linflexd_puts(const char *string);
+
+/* Eight hex digits, no 0x prefix. Printing register values is the whole
+ reason this console exists, and a printf would drag in a C library this
+ image deliberately does not link. */
+
+void linflexd_put_hex32(unsigned int value);
+
+#endif /* LINFLEXD_H */
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds b/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds
new file mode 100644
index 00000000..796cb88d
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds
@@ -0,0 +1,198 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/* Link map for the NXP S32Z280-594EVB, RTU0 core 0.
+ *
+ * Code goes to the RTU code SRAM at its instruction-fetch address, 0x79900000,
+ * which is also where the core resets: MC_ME_PRTN0_CORE0_ADDR reads 0x79900000
+ * on this board, so a debugger-loaded image needs no relocation to be reached
+ * by the reset vector. That window is writable over the debug AXI port even
+ * though NXP's debugger memory map declares it read-only, so plain `load`
+ * works and no LMA alias is required. A flash-booted image is different: it
+ * would have to be written through the data alias at 0x32100000.
+ *
+ * Data, bss and the per-mode stacks go to the RTU-local data SRAM at
+ * 0x31780000. Keeping them out of the code SRAM avoids the boundary problem
+ * the FVP build hit, where an aligned .data start straddled the code/data
+ * split, and RTU-local traffic does not leave the RTU.
+ *
+ * The TCMs are deliberately unused: they are not accessible at reset until
+ * their region registers are programmed, so nothing needed for early boot can
+ * live there.
+ */
+
+__hyp_stack_size__ = 0x0800;
+__svc_stack_size__ = 0x1000;
+__irq_stack_size__ = 0x0800;
+__fiq_stack_size__ = 0x0400;
+__abt_stack_size__ = 0x0400;
+__und_stack_size__ = 0x0400;
+__sys_stack_size__ = 0x0800;
+
+MEMORY
+{
+ CODE (rx) : ORIGIN = 0x79900000, LENGTH = 0x00700000 /* 7 MB */
+ /* ATCM. entry.S programmes IMP_ATCMREGIONR to this base and enables the
+ bank before anything runs from it, and mpu.c maps it executable. Code
+ placed here runs at full core speed with one wait state, where CODE is
+ RTU code RAM and runs at half the core frequency (S32Z2 RM 6.3.6). */
+ ATCM (rwx) : ORIGIN = 0x30000000, LENGTH = 0x00010000 /* 64 KB */
+ /* BTCM. 16 KB at zero wait states, enabled by entry.S, and preloaded there
+ as well because ECC means a location must be written before it is read.
+ Used for data that wants deterministic access without depending on a
+ cache -- an enabled TCM is never cached. */
+ BTCM (rw) : ORIGIN = 0x30100000, LENGTH = 0x00004000 /* 16 KB */
+ DATA (rwx) : ORIGIN = 0x31780000, LENGTH = 0x00070000 /* 448 KB: DRAM0 + DRAM1 below the module area */
+ /* Module memory, uncovered by any kernel MPU region on purpose. See
+ S32Z_MODULE_AREA_BASE in platform.h. */
+ MODULE (rwx) : ORIGIN = 0x317F0000, LENGTH = 0x00010000 /* 64 KB */
+}
+
+ENTRY(_start)
+
+SECTIONS
+{
+ /* .text.boot must come FIRST: MC_ME_PRTN0_CORE0_ADDR is 0x79900000 on
+ * this board, so whatever lands at the start of CODE is what the core
+ * executes out of reset, and that has to be _start. It must also be a
+ * T32 instruction, since the core resets in Thumb state. The vector
+ * tables need alignment but not a fixed address, because HVBAR and VBAR
+ * are programmed at run time -- so they follow rather than lead.
+ */
+ /* No ALIGN() address expression on any output section below. Written as
+ * "<name> ALIGN(n) : { }" the ALIGN is the section's *address*, evaluated
+ * from a location counter that starts at zero, and it silently overrides
+ * "> CODE" -- every section then lands near address 0 and the link fails
+ * with "not within region". The FVP script uses that form harmlessly only
+ * because its region starts at 0x00000000. Alignment comes from the input
+ * sections (the vector tables carry .align 5) or from an explicit
+ * ". = ALIGN(n);" inside the braces.
+ */
+ .boot :
+ {
+ KEEP(*(.text.boot))
+ } > CODE
+
+ .vectors_el2 :
+ {
+ KEEP(*(.vectors_el2))
+ } > CODE
+
+ .vectors_el1 :
+ {
+ KEEP(*(.vectors_el1))
+ } > CODE
+
+ .text :
+ {
+ *(.text*)
+ *(.glue_7)
+ *(.glue_7t)
+ } > CODE
+
+ /* Code that runs from tightly-coupled memory. Loaded into CODE and copied
+ to ATCM by atcm_copy_text() before it is called, because ECC is enabled
+ on this part and a TCM location must be written before it is read
+ (Cortex-R52 TRM 6.2.2). The copy uses 64-bit stores for the same
+ reason: ATCM requires them where BTCM and CTCM accept 32-bit.
+
+ ALIGN(8) at both ends so the copy can move whole 64-bit units without a
+ tail case, which is what keeps the ECC requirement satisfied for every
+ location written. */
+
+ .atcm_text : ALIGN(8)
+ {
+ __atcm_text_start__ = .;
+ *(.atcm_text*)
+ . = ALIGN(8);
+ __atcm_text_end__ = .;
+ } > ATCM AT> CODE
+
+ __atcm_text_load__ = LOADADDR(.atcm_text);
+
+ .rodata :
+ {
+ *(.rodata*)
+ . = ALIGN(4);
+ __code_end__ = .;
+ } > CODE
+
+ .data :
+ {
+ . = ALIGN(8);
+ __data_start__ = .;
+ *(.data*)
+ . = ALIGN(8);
+ __data_end__ = .;
+ } > DATA
+
+ /* Data placed in BTCM. NOLOAD: nothing is copied in, and entry.S has
+ already written every location in the bank to establish ECC check bits,
+ so anything here is safe to read before it is written by the program. */
+
+ .btcm_bss (NOLOAD) : ALIGN(8)
+ {
+ __btcm_bss_start__ = .;
+ *(.btcm_bss*)
+ . = ALIGN(8);
+ __btcm_bss_end__ = .;
+ } > BTCM
+
+ .bss (NOLOAD) :
+ {
+ . = ALIGN(8);
+ __bss_start__ = .;
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(8);
+ __bss_end__ = .;
+ } > DATA
+
+ /* Per-mode stacks. Each mode needs its own on an R-profile core: an
+ * exception taken while a handler is running would otherwise reuse the
+ * interrupted mode's stack pointer.
+ */
+ .stacks (NOLOAD) :
+ {
+ . = ALIGN(8);
+ . = . + __hyp_stack_size__;
+ __hyp_stack_top = .;
+ . = ALIGN(8);
+ . = . + __svc_stack_size__;
+ __svc_stack_top = .;
+ . = ALIGN(8);
+ . = . + __irq_stack_size__;
+ __irq_stack_top = .;
+ . = ALIGN(8);
+ . = . + __fiq_stack_size__;
+ __fiq_stack_top = .;
+ . = ALIGN(8);
+ . = . + __abt_stack_size__;
+ __abt_stack_top = .;
+ . = ALIGN(8);
+ . = . + __und_stack_size__;
+ __und_stack_top = .;
+ . = ALIGN(8);
+ . = . + __sys_stack_size__;
+ __sys_stack_top = .;
+ } > DATA
+
+ /* Both spellings: _tx_initialize_low_level publishes the first free
+ address to tx_application_define from _end, while C library conventions
+ use end. */
+
+ PROVIDE(end = .);
+ PROVIDE(_end = .);
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c
new file mode 100644
index 00000000..c8277fc4
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c
@@ -0,0 +1,622 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* mpu.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* PMSAv8-R memory protection for the NXP S32Z280-594EVB. */
+/* */
+/* Register model (AArch32): a region is selected through PRSELR and */
+/* then described by PRBAR (base, shareability, access permission, */
+/* execute-never) and PRLAR (inclusive limit, attribute index, enable).*/
+/* Both addresses have a 64-byte granule, so the low six bits of each */
+/* register hold attributes rather than address. Memory types come */
+/* from MAIR through the attribute index, not from the region itself. */
+/* */
+/* Caches are invalidated before being enabled. On this model they */
+/* come out of reset invalid, but silicon does not guarantee that, and */
+/* this code is meant to be the template the S32Z280 port starts from. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Directive 4.3 -- the MPU, MAIR, SCTLR and cache maintenance are only*/
+/* reachable through CP15; every access is encapsulated in a one-line*/
+/* accessor below. */
+/* */
+/**************************************************************************/
+
+#include "platform.h"
+
+/* Progress marker, mirrored into memory. Enabling the MPU can take the core
+ down in a way that records no fault and takes the console with it, so each
+ step here is stamped where a debugger can read it afterwards. */
+
+extern unsigned int probe_stage;
+#define MARK(n) do { probe_stage = (unsigned int)(n); } while (0)
+#include "mpu.h"
+#include "platform.h"
+
+/* Progress marker, mirrored into memory. Enabling the MPU can take the core
+ down in a way that records no fault and takes the console with it, so each
+ step here is stamped where a debugger can read it afterwards. */
+
+extern unsigned int probe_stage;
+#define MARK(n) do { probe_stage = (unsigned int)(n); } while (0)
+
+/* SCTLR bits. */
+
+#define SCTLR_M (1UL << 0) /* MPU enable */
+#define SCTLR_C (1UL << 2) /* data cache enable */
+#define SCTLR_I (1UL << 12) /* instruction cache enable */
+
+/* MAIR attribute bytes. Index 0 is Normal write-back read/write-allocate,
+ index 1 is Device-nGnRnE, matching MPU_ATTR_* in the header. */
+
+#define MAIR_ATTR_NORMAL_WB 0xFFUL
+#define MAIR_ATTR_DEVICE 0x00UL
+
+/* Boundaries supplied by the linker script, all 64-byte aligned. */
+
+extern char __code_end__;
+extern char __data_start__;
+extern char __data_end__;
+
+
+/**************************************************************************/
+/* The region table. */
+/* */
+/* Deliberately minimal and readable: code is read-only and executable, */
+/* everything writable is non-executable, and the peripheral half of the */
+/* address map is Device. A write-protected code region is the point -- */
+/* it is what makes the MPU do something observable, and AR2 extends the */
+/* same table with per-module regions. */
+/**************************************************************************/
+
+/* Table capacity. The FVP example this file came from declared this as [3],
+ matching the three regions it programs; a five-region table then wrote past
+ the end of it, and the region-3 base assignment landed outside the array.
+ The region read back with base 0 and limit 0x763FFFFF, overlapping regions
+ 0-2, and PMSAv8-R makes overlapping regions UNPREDICTABLE -- which is what
+ killed the core on the SCTLR.M write. Named and guarded so the next map
+ cannot repeat it. */
+
+#define MPU_TABLE_REGIONS 16U
+
+static MPU_REGION mpu_regions[MPU_TABLE_REGIONS];
+static unsigned int mpu_regions_used;
+
+
+/**************************************************************************/
+/* CP15 accessors. */
+/**************************************************************************/
+
+static unsigned long read_mpuir(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c0, c0, 4" : "=r"(value));
+ return value;
+}
+
+static unsigned long read_sctlr(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c1, c0, 0" : "=r"(value));
+ return value;
+}
+
+static void write_sctlr(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c1, c0, 0" : : "r"(value) : "memory");
+}
+
+static void write_prselr(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c2, 1" : : "r"(value) : "memory");
+}
+
+static void write_prbar(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c3, 0" : : "r"(value) : "memory");
+}
+
+static void write_prlar(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c3, 1" : : "r"(value) : "memory");
+}
+
+static unsigned long read_prbar(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c6, c3, 0" : "=r"(value));
+ return value;
+}
+
+static unsigned long read_prlar(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c6, c3, 1" : "=r"(value));
+ return value;
+}
+
+static void write_mair0(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c10, c2, 0" : : "r"(value) : "memory");
+}
+
+static void write_mair1(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c10, c2, 1" : : "r"(value) : "memory");
+}
+
+static void invalidate_icache_all(void)
+{
+ __asm__ volatile("mcr p15, 0, r0, c7, c5, 0" : : : "memory"); /* ICIALLU */
+}
+
+static void data_sync_barrier(void)
+{
+ __asm__ volatile("dsb" : : : "memory");
+}
+
+static void instruction_barrier(void)
+{
+ __asm__ volatile("isb" : : : "memory");
+}
+
+
+/**************************************************************************/
+/* mpu_region_count */
+/**************************************************************************/
+
+unsigned int mpu_region_count(void)
+{
+ /* MPUIR.REGION occupies bits [15:8]. */
+
+ return (unsigned int) ((read_mpuir() >> 8) & 0xFFUL);
+}
+
+
+/**************************************************************************/
+/* mpu_is_enabled / mpu_caches_enabled */
+/**************************************************************************/
+
+unsigned int mpu_is_enabled(void)
+{
+ return ((read_sctlr() & SCTLR_M) != 0UL) ? 1U : 0U;
+}
+
+unsigned int mpu_caches_enabled(void)
+{
+ unsigned long sctlr = read_sctlr();
+
+ return (((sctlr & SCTLR_C) != 0UL) && ((sctlr & SCTLR_I) != 0UL)) ? 1U : 0U;
+}
+
+
+/**************************************************************************/
+/* mpu_region_table */
+/**************************************************************************/
+
+const MPU_REGION *mpu_region_table(unsigned int *count_ptr)
+{
+ if (count_ptr != 0)
+ {
+ *count_ptr = mpu_regions_used;
+ }
+
+ return mpu_regions;
+}
+
+
+/**************************************************************************/
+/* mpu_read_region */
+/**************************************************************************/
+
+void mpu_read_region(unsigned int index, unsigned long *prbar_ptr,
+ unsigned long *prlar_ptr)
+{
+ write_prselr((unsigned long) index);
+ instruction_barrier();
+
+ if (prbar_ptr != 0)
+ {
+ *prbar_ptr = read_prbar();
+ }
+ if (prlar_ptr != 0)
+ {
+ *prlar_ptr = read_prlar();
+ }
+}
+
+
+/**************************************************************************/
+/* build_table -- describe this image's memory. */
+/**************************************************************************/
+
+/* Bisection switch. probe_stage stops at 0x42 with the real five-region map,
+ meaning the SCTLR.M write itself kills the core with no exception taken.
+ That has two very different causes: the map is wrong for the first fetch or
+ stack access, or enabling the MPU on this part needs something we have not
+ done at all. A single region spanning the whole address space, permissive
+ and Normal, distinguishes them: if the core survives with this, the map is
+ at fault; if it dies anyway, enabling M is. */
+
+static void build_table(void)
+{
+ /* Protection map: narrow regions with real permissions, so that anything
+ this image should not do actually faults.
+ *
+ * This supersedes the permissive bring-up map (everything Normal RW X,
+ * whole address space covered). That map existed because a narrow one
+ * had failed and the reason was unknown -- the failure looked like a
+ * stall with nothing recorded, and coverage was blamed. Coverage was
+ * never the problem: an out-of-bounds write to a [3]-sized table put a
+ * region at base 0 overlapping everything, and SCTLR.TE being set meant
+ * the resulting abort could not run a handler. Both are fixed, so a
+ * narrow map is now both correct and diagnosable: if something outside
+ * these regions is touched, the fault handler records vector, syndrome
+ * and faulting address instead of hanging mutely.
+ *
+ * Anything not described below is unmapped and faults on access. That
+ * is the point.
+ */
+
+ /* Code: read-only and executable. A write here must fault. */
+
+ mpu_regions[0].mpu_region_base = S32Z_CODE_SRAM_BASE;
+ mpu_regions[0].mpu_region_limit = (unsigned long) &__code_end__ - 1UL;
+ mpu_regions[0].mpu_region_ap = MPU_AP_RO_EL1;
+ mpu_regions[0].mpu_region_execute_never = 0U;
+ mpu_regions[0].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[0].mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
+ mpu_regions[0].mpu_region_name = "code RO X normal-wb";
+
+ /* Data, bss and every per-mode stack: writable, never executable. Sized
+ from the SRAM banks, not from __data_end__: the stacks are NOLOAD and sit
+ above that symbol, so sizing from it would leave them unmapped and the
+ first push after enabling the MPU would abort.
+
+ This covers DRAM0 and DRAM1 together -- 512 KB, contiguous, both at full
+ core speed. DRAM1 went undeclared until the banks were read out of the
+ Reference Manual and confirmed on the board. */
+
+ mpu_regions[1].mpu_region_base = S32Z_DATA_SRAM_BASE;
+ /* Stops before the module area at the top of DRAM1. A module's memory must
+ not be covered by a kernel region, or every thread could reach it and the
+ manager's own regions would be decoration. */
+
+ mpu_regions[1].mpu_region_limit = S32Z_DATA_SRAM_BASE
+ + S32Z_DATA_SRAM_SHARED_SIZE - 1UL;
+ mpu_regions[1].mpu_region_ap = MPU_AP_RW_EL1;
+ mpu_regions[1].mpu_region_execute_never = 1U;
+ mpu_regions[1].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[1].mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
+ mpu_regions[1].mpu_region_name = "data RW NX normal-wb";
+
+ /* Console. Mapped narrowly: one 64 KB LINFlexD instance. */
+
+ mpu_regions[2].mpu_region_base = S32Z_LINFLEX_9_BASE;
+ mpu_regions[2].mpu_region_limit = S32Z_LINFLEX_9_BASE + 0xFFFFUL;
+ mpu_regions[2].mpu_region_ap = MPU_AP_RW_EL1;
+ mpu_regions[2].mpu_region_execute_never = 1U;
+ mpu_regions[2].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[2].mpu_region_attr_index = MPU_ATTR_DEVICE;
+ mpu_regions[2].mpu_region_name = "uart RW NX device";
+
+ /* GIC. Device nGnRnE is required by the Cortex-R52 TRM, and confirmed
+ here: mapped Normal the distributor stalls the core. */
+
+ mpu_regions[3].mpu_region_base = S32Z_GIC_BASE;
+ mpu_regions[3].mpu_region_limit = S32Z_GIC_BASE + S32Z_GIC_SIZE - 1UL;
+ mpu_regions[3].mpu_region_ap = MPU_AP_RW_EL1;
+ mpu_regions[3].mpu_region_execute_never = 1U;
+ mpu_regions[3].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[3].mpu_region_attr_index = MPU_ATTR_DEVICE;
+ mpu_regions[3].mpu_region_name = "gic RW NX device";
+
+ /* RTU peripheral window behind the low-latency peripheral port. */
+
+ mpu_regions[4].mpu_region_base = S32Z_RTU_PERIPH_BASE;
+ mpu_regions[4].mpu_region_limit = S32Z_RTU_PERIPH_BASE
+ + S32Z_RTU_PERIPH_SIZE - 1UL;
+ mpu_regions[4].mpu_region_ap = MPU_AP_RW_EL1;
+ mpu_regions[4].mpu_region_execute_never = 1U;
+ mpu_regions[4].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[4].mpu_region_attr_index = MPU_ATTR_DEVICE;
+ mpu_regions[4].mpu_region_name = "rtu RW NX device";
+
+ /* DRAM2. Deliberately mapped so the cache benchmark has slower memory to
+ work against: this bank runs at half the core frequency, where region 1's
+ banks run at full speed and caching them shows little. Normal write-back
+ and never executable.
+
+ Sized 512 KB, which is the whole bank. An earlier revision mapped 2 MB
+ here; the upper 1.5 MB of that aliased back onto the bank, so anything
+ placed above 0x31880000 would have silently shared storage with the
+ bottom of the region -- no fault, just two objects on one address. */
+
+ mpu_regions[5].mpu_region_base = S32Z_DRAM2_BASE;
+ /* Stops short of the per-thread windows at the top of the bank. Those are
+ mapped one at a time by the thread-entry hook; leaving them inside this
+ region would make them reachable from every thread and the isolation
+ below would prove nothing. */
+
+ mpu_regions[5].mpu_region_limit = S32Z_DRAM2_BASE
+ + S32Z_DRAM2_SHARED_SIZE - 1UL;
+ mpu_regions[5].mpu_region_ap = MPU_AP_RW_EL1;
+ mpu_regions[5].mpu_region_execute_never = 1U;
+ mpu_regions[5].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[5].mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
+ mpu_regions[5].mpu_region_name = "dram2 RW NX normal-wb";
+
+ /* One region per TCM bank. The TRM is explicit that an enabled TCM still
+ needs an MPU region before it can be used, and that an enabled TCM always
+ behaves as Non-cacheable Non-shareable Normal memory whatever attributes
+ the MPU carries -- so the attribute index below does not describe the TCM,
+ only the permissions do. Kept as three regions rather than one spanning
+ 0x30000000 to 0x30203FFF, because a single region would also make the
+ 1MB gaps between banks accessible, and those are not TCM.
+
+ Left executable on purpose: the point of TCM is deterministic access for
+ code as much as data, so an image that wants a TCM-resident handler can
+ have one. That makes these regions writable and executable, which for
+ tightly-coupled memory is the normal arrangement rather than an
+ oversight. */
+
+ mpu_regions[6].mpu_region_base = S32Z_ATCM_BASE;
+ mpu_regions[6].mpu_region_limit = S32Z_ATCM_BASE + S32Z_ATCM_SIZE - 1UL;
+ mpu_regions[6].mpu_region_ap = MPU_AP_RW_EL1;
+ mpu_regions[6].mpu_region_execute_never = 0U;
+ mpu_regions[6].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[6].mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
+ mpu_regions[6].mpu_region_name = "atcm RW X tcm";
+
+ /* No regions for BTCM and CTCM: entry.S leaves those banks disabled, and a
+ region covering a disabled TCM would map its address range to whatever
+ AXIM provides there, which is not TCM and would read as success. See the
+ measurement in entry.S. */
+
+ /* BTCM. Data only, so execute-never, unlike the ATCM region above which
+ holds code. An enabled TCM behaves as Non-cacheable Non-shareable Normal
+ memory whatever the MPU says, so the attribute index below describes the
+ permissions and nothing else -- but a region is still required before the
+ bank can be touched at all. */
+
+ mpu_regions[7].mpu_region_base = S32Z_BTCM_BASE;
+ mpu_regions[7].mpu_region_limit = S32Z_BTCM_BASE + S32Z_BTCM_SIZE - 1UL;
+ mpu_regions[7].mpu_region_ap = MPU_AP_RW_EL1;
+ mpu_regions[7].mpu_region_execute_never = 1U;
+ mpu_regions[7].mpu_region_shareability = MPU_SH_NON;
+ mpu_regions[7].mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
+ mpu_regions[7].mpu_region_name = "btcm RW NX tcm";
+
+ mpu_regions_used = 8U;
+}
+
+
+/**************************************************************************/
+/* program_region */
+/**************************************************************************/
+
+static void program_region(unsigned int index, const MPU_REGION *region_ptr)
+{
+ unsigned long prbar;
+ unsigned long prlar;
+
+ /* PRBAR: BASE[31:6], RES0[5], SH[4:3], AP[2:1], XN[0].
+ *
+ * Per Cortex-R52 TRM r1p3 figure 3-39. Note the field positions: the
+ * FVP example this file came from shifts every field one bit too far
+ * left -- SH<<4, AP<<2, XN<<1 -- which silently produces a different
+ * region than intended. With XN written to bit 1 it lands in the real
+ * AP[1] (EL0 access) and the region stays EXECUTABLE, which is how this
+ * board ended up running instructions out of .data.
+ *
+ * That bug is also the origin of the "PRBAR.AP bit order is reversed"
+ * claim recorded during the FVP work. Calibrating AP empirically under
+ * the wrong shift puts the intended AP's low bit into the real AP[2]
+ * (the read-only bit) and the intended XN into the real AP[1], which
+ * looks exactly like a reversed encoding. The architectural order is
+ * correct as published; there was no reversal.
+ */
+
+ prbar = (region_ptr->mpu_region_base & 0xFFFFFFC0UL)
+ | (((unsigned long) region_ptr->mpu_region_shareability & 0x3UL) << 3)
+ | (((unsigned long) region_ptr->mpu_region_ap & 0x3UL) << 1)
+ | ((unsigned long) region_ptr->mpu_region_execute_never & 0x1UL);
+
+ prlar = (region_ptr->mpu_region_limit & 0xFFFFFFC0UL)
+ | (((unsigned long) region_ptr->mpu_region_attr_index & 0x7UL) << 1)
+ | 1UL; /* region enable */
+
+ write_prselr((unsigned long) index);
+ instruction_barrier();
+ write_prbar(prbar);
+ write_prlar(prlar);
+}
+
+
+/**************************************************************************/
+/* mpu_module_load_window_open / mpu_module_load_window_close */
+/* */
+/* A window over the module area, for the manager to load through. */
+/* */
+/* No kernel region covers the module area, which is what stops every */
+/* thread from reaching a module's memory -- but the manager has to read */
+/* the preamble and write the module's data to load it at all. Without */
+/* this the load faulted on its first read of the image, and because a */
+/* privileged data abort ends in a handler that only spins, that looked */
+/* exactly like the load hanging. */
+/* */
+/* Opened around the load and closed straight after, rather than left in */
+/* place, because PMSAv8-R has no region priority: if this region were */
+/* still enabled when a module thread ran it would overlap the module's */
+/* own regions, and an access hitting both takes a translation fault */
+/* (TRM 8.1). */
+/* Closing it before any module thread starts is what keeps the two from */
+/* ever being enabled together. */
+/* */
+/* Region 16, above both the kernel's 0-7 and the eight the manager */
+/* hands to a module, so neither the scheduler's per-thread region load */
+/* nor the boot table can disturb it. MPUIR reports 20 EL1 regions on */
+/* this part. EL1 read/write with no EL0 access: the manager can load */
+/* through it and a module cannot use it to reach anything. */
+/**************************************************************************/
+
+unsigned long mpu_module_window_prbar;
+unsigned long mpu_module_window_prlar;
+
+
+void mpu_module_window_init(void)
+{
+ MPU_REGION window;
+
+ window.mpu_region_base = S32Z_MODULE_AREA_BASE;
+ window.mpu_region_limit = S32Z_MODULE_AREA_BASE
+ + S32Z_MODULE_AREA_SIZE - 1UL;
+ window.mpu_region_ap = MPU_AP_RW_EL1;
+ window.mpu_region_execute_never = 1U;
+ window.mpu_region_shareability = MPU_SH_NON;
+ window.mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
+ window.mpu_region_name = "module window RW NX EL1";
+
+ /* Published for the scheduler, which turns this region on and off on every
+ dispatch and has no business computing register layouts in assembly. */
+
+ mpu_module_window_prbar = (window.mpu_region_base & 0xFFFFFFC0UL)
+ | (((unsigned long) window.mpu_region_shareability & 0x3UL) << 3)
+ | (((unsigned long) window.mpu_region_ap & 0x3UL) << 1)
+ | ((unsigned long) window.mpu_region_execute_never & 0x1UL);
+
+ mpu_module_window_prlar = (window.mpu_region_limit & 0xFFFFFFC0UL)
+ | (((unsigned long) window.mpu_region_attr_index & 0x7UL) << 1)
+ | 1UL;
+
+ /* Enabled now, because everything running before the first module thread is
+ privileged code that may need to reach module memory -- the manager loads
+ a module from a kernel thread. */
+
+ program_region(MPU_MODULE_LOAD_REGION, &window);
+
+ data_sync_barrier();
+ instruction_barrier();
+}
+
+
+/**************************************************************************/
+/* mpu_init */
+/**************************************************************************/
+
+unsigned int mpu_init(void)
+{
+ unsigned long sctlr;
+ unsigned int index;
+ unsigned int available = mpu_region_count();
+
+ build_table();
+
+ if (mpu_regions_used > MPU_TABLE_REGIONS)
+ {
+ return 0U; /* build_table overran the table */
+ }
+
+ if (available < mpu_regions_used)
+ {
+ return 0U; /* hardware has fewer regions than we need */
+ }
+
+ /* Memory types first: a region's attribute index is meaningless until
+ MAIR is populated. */
+
+ MARK(0x20);
+ write_mair0((MAIR_ATTR_DEVICE << 8) | MAIR_ATTR_NORMAL_WB);
+ write_mair1(0UL);
+ MARK(0x21);
+
+ for (index = 0U; index < mpu_regions_used; index++)
+ {
+ program_region(index, &mpu_regions[index]);
+ MARK(0x30U + index);
+ }
+
+ /* Disable any regions this image does not use, so that stale enables
+ cannot grant access we did not intend. */
+
+ for (index = mpu_regions_used; index < available; index++)
+ {
+ write_prselr((unsigned long) index);
+ instruction_barrier();
+ write_prlar(0UL);
+ }
+
+#ifdef TXM_MODULE_MANAGER
+
+ /* After the loop above, which would otherwise disable it again: the module
+ window lives above the regions this table uses, so it counts as unused
+ here. */
+
+ mpu_module_window_init();
+
+#endif
+
+ MARK(0x40);
+ data_sync_barrier();
+
+ /* Caches are invalidated before enabling. The instruction cache has a
+ single invalidate-all; the data cache is left to the model's reset
+ state, which is invalid, because a set/way sweep belongs with the
+ silicon bring-up where it can be verified against the real cache
+ geometry. */
+
+ invalidate_icache_all();
+ data_sync_barrier();
+ instruction_barrier();
+
+ /* MPU only. Caches are deliberately left off on this first pass: the
+ point here is reaching the GIC, and enabling caches at the same time
+ would add coherency questions -- the debugger writes this image straight
+ into SRAM behind the caches' back -- to a change whose result is meant to
+ be unambiguous. C and I are a separate step with their own check. */
+
+ MARK(0x41);
+ sctlr = read_sctlr();
+ MARK(0x42);
+#if !defined(MPU_READBACK_ONLY)
+ sctlr |= SCTLR_M;
+ write_sctlr(sctlr);
+#else
+ (void) sctlr; /* readback experiment: MPU deliberately NOT enabled */
+#endif
+
+ /* If the map is wrong, the core dies HERE -- the next instruction fetch or
+ the next stack access is the first thing the new map has to satisfy. A
+ marker written after this point therefore proves the map holds. */
+
+ MARK(0x43);
+ data_sync_barrier();
+ instruction_barrier();
+ MARK(0x44);
+
+ return mpu_regions_used;
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h
new file mode 100644
index 00000000..19d183cc
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h
@@ -0,0 +1,142 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* mpu.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* PMSAv8-R memory protection and cache enable for Cortex-R52. */
+/* */
+/* The region set is described by a table rather than a sequence of */
+/* register writes. That is deliberate and is a roadmap decision, not */
+/* a style preference: AR2 programs a Stage-1 region set per ThreadX */
+/* module and AR3 programs Stage-2 regions per ZoneX partition, and */
+/* both reuse this shape. A table can be swapped at a partition */
+/* switch; open-coded register writes cannot. */
+/* */
+/**************************************************************************/
+
+#ifndef MPU_H
+#define MPU_H
+
+/* Access permissions, PRBAR.AP (bits [3:2]).
+ *
+ * These values were CALIBRATED AGAINST THE HARDWARE, not copied from a
+ * reference, because the widely-published Armv8-R AArch64 macro set has the
+ * two bits the other way round (its "read-only, no EL0 access" is 0x2).
+ * Programming four disjoint regions, one per encoding, and attempting a
+ * privileged write to each gave:
+ *
+ * AP=0b00 write allowed AP=0b01 write faulted
+ * AP=0b10 write allowed AP=0b11 write faulted
+ *
+ * so the low bit is read-only and the high bit grants EL0 access. Using the
+ * AArch64 ordering here silently produces writable "read-only" regions: the
+ * MPU still enforces region coverage, so everything appears to work and only
+ * an explicit write-permission test exposes it. Re-calibrate on S32Z280
+ * silicon before trusting these values there.
+ */
+
+/* PRBAR.AP[2:1] as the architecture defines it: AP[2] selects read-only,
+ AP[1] grants EL0 access. These are the published values -- the "reversed
+ bit order" recorded during the FVP work was an artefact of that code
+ shifting AP into the wrong bits, not a property of the hardware. */
+
+#define MPU_AP_RW_EL1 0U /* 0b00 EL1 read/write, no EL0 */
+#define MPU_AP_RW_EL1_EL0 1U /* 0b01 EL1 read/write, EL0 read/write */
+#define MPU_AP_RO_EL1 2U /* 0b10 EL1 read-only, no EL0 */
+#define MPU_AP_RO_EL1_EL0 3U /* 0b11 EL1 read-only, EL0 read-only */
+
+/* Shareability, PRBAR.SH. */
+
+#define MPU_SH_NON 0U
+#define MPU_SH_OUTER 2U
+#define MPU_SH_INNER 3U
+
+/* Attribute indices into MAIR, PRLAR.AttrIndx. */
+
+#define MPU_ATTR_NORMAL_WB 0U /* Normal, inner/outer write-back */
+#define MPU_ATTR_DEVICE 1U /* Device-nGnRnE */
+
+/* One protection region. The limit is inclusive, matching the hardware. */
+
+typedef struct MPU_REGION_STRUCT
+{
+ unsigned long mpu_region_base;
+ unsigned long mpu_region_limit;
+ unsigned char mpu_region_ap;
+ unsigned char mpu_region_execute_never;
+ unsigned char mpu_region_shareability;
+ unsigned char mpu_region_attr_index;
+ const char *mpu_region_name;
+} MPU_REGION;
+
+/* Number of regions this implementation provides, from MPUIR. */
+
+unsigned int mpu_region_count(void);
+
+/* Program the region table, then enable the MPU and the caches. Returns
+ the number of regions programmed, or 0 if the table does not fit. */
+
+unsigned int mpu_init(void);
+
+/* True once SCTLR reports the MPU and both caches enabled. */
+
+unsigned int mpu_is_enabled(void);
+unsigned int mpu_caches_enabled(void);
+
+/* The active table, for reporting. */
+
+const MPU_REGION *mpu_region_table(unsigned int *count_ptr);
+
+/* Read back what the hardware holds for one region, so that programming can
+ be verified rather than assumed. */
+
+void mpu_read_region(unsigned int index, unsigned long *prbar_ptr,
+ unsigned long *prlar_ptr);
+
+/* Region index for the manager's load window over the module area. Above the
+ kernel's 0-7 and above the eight the manager hands to a module, so neither
+ the boot table nor the scheduler's per-thread region load can disturb it. */
+
+#define MPU_MODULE_LOAD_REGION 16U
+
+/* The module area window, which is what lets privileged code reach module
+ memory at all -- no other kernel region covers it.
+
+ PMSAv8-R has no region priority, so this must never be enabled at the same
+ time as the regions a module is given, which cover the same memory. That is
+ guaranteed by who owns it rather than by careful calling: the scheduler turns
+ this region on for every thread that is not a module thread and off for every
+ thread that is, so the window is enabled exactly when no module regions are
+ loaded. The two register words are published for it below.
+
+ Enabled here at boot, because everything before the first module thread is
+ privileged code that may need to reach module memory. */
+
+void mpu_module_window_init(void);
+
+extern unsigned long mpu_module_window_prbar;
+extern unsigned long mpu_module_window_prlar;
+
+#endif /* MPU_H */
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h
new file mode 100644
index 00000000..1440fbd1
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h
@@ -0,0 +1,274 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* platform.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Memory map and peripheral addresses for the NXP S32Z280-594EVB, */
+/* RTU0 core 0 (R52_0_0). */
+/* */
+/* Every address below was either read out of the S32Z2 Reference */
+/* Manual (S32Z27RM Rev. 5.1) or confirmed on the board itself, and */
+/* each is marked accordingly. The distinction matters because this */
+/* part differs from the Armv8-R AEM FVP in ways that are silent */
+/* rather than loud: an address that is merely wrong tends to read */
+/* back zero instead of faulting. */
+/* */
+/* MEMORY */
+/* */
+/* The RTU exposes its code SRAM through two windows onto the same */
+/* physical memory: 0x79900000 for instruction fetch (AXI-F) and */
+/* 0x32100000 as data. Confirmed by writing a sentinel to one and */
+/* reading it back through the other. NXP's debugger memory map */
+/* declares the fetch window read-only, but it is writable over the */
+/* debug AXI port -- verified -- so a debugger-loaded image can be */
+/* placed there directly and no load-address alias is needed. A */
+/* self-booting image loaded from flash would still need the data */
+/* alias, since the core's own data side may not write through AXI-F. */
+/* */
+/* The TCMs need care, and the earlier note here was wrong about why. */
+/* Reads of 0x30000000, 0x30100000 and 0x30200000 do fault, but not */
+/* because the TCMs are disabled. Reading the region registers on */
+/* this part gives: */
+/* */
+/* ATCM 0x0000011F 64KB, 1 wait state, ENABLED at EL2 and EL1/0 */
+/* BTCM 0x00000014 16KB, 0 wait states, disabled */
+/* CTCM 0x00000114 16KB, 1 wait state, disabled */
+/* */
+/* Every BASEADDRESS field is zero, so ATCM is live as 64KB at address */
+/* 0x00000000 -- not at 0x30000000. The faulting reads were of an */
+/* address the TCM is not at. ATCM's enables reset set because */
+/* CFGTCMBOOTx is tied high on this part (Cortex-R52 TRM r1p3 3.3.94). */
+/* The sizes and wait states match the S32Z2 reference manual exactly. */
+/* */
+/* ECC is implemented and enabled: IMP_MEMPROTCTLR reads 0x00000011, */
+/* so RAMPROTIMP and RAMPROTEN are both set. TRM 6.2.2 therefore */
+/* applies -- a TCM location must be written before it is read, and */
+/* ATCM needs 64-bit aligned stores where BTCM and CTCM accept 32-bit. */
+/* */
+/* The DDR window at 0x7A000000 is dark until DDR is initialised. */
+/* */
+/**************************************************************************/
+
+#ifndef PLATFORM_H
+#define PLATFORM_H
+
+/* TCM bases. The values the S32Z2 reference manual documents, which the core
+ does not use until they are programmed -- ATCM boots at 0x00000000 and the
+ other two boot disabled with an UNKNOWN base. Each must be size-aligned
+ (Cortex-R52 TRM r1p3 6.2), which these are: 64KB, 16KB and 16KB banks at
+ 64KB, 1MB and 1MB alignment respectively. Programming ATCM here also moves
+ it off address 0, so a null-pointer write faults instead of quietly landing
+ in tightly-coupled memory. */
+
+#define S32Z_ATCM_BASE 0x30000000UL
+#define S32Z_ATCM_SIZE 0x00010000UL /* 64 KB, 1 wait state */
+#define S32Z_BTCM_BASE 0x30100000UL
+#define S32Z_BTCM_SIZE 0x00004000UL /* 16 KB, 0 wait states */
+#define S32Z_CTCM_BASE 0x30200000UL
+#define S32Z_CTCM_SIZE 0x00004000UL /* 16 KB, 1 wait state */
+
+/* Memory-mapped register access. Kept here rather than per-driver so that
+ every peripheral in this BSP goes through one definition.
+ MISRA C:2012 Rule 11.4/11.6 deviation: casting an integer address to a
+ volatile pointer is inherent to memory-mapped device access. */
+
+#define REG32(address) (*(volatile unsigned long *)(unsigned long)(address))
+
+/* Code SRAM, instruction-fetch view. 7 MB. This is where the core */
+/* resets to, and where .text is linked and loaded. [verified on board] */
+
+#define S32Z_CODE_SRAM_BASE 0x79900000UL
+#define S32Z_CODE_SRAM_SIZE 0x00700000UL
+
+/* The same physical SRAM seen as data. Kept here because the boot-from- */
+/* flash path will need it, and because it is how the core's reset */
+/* instruction is planted by an external debugger. [verified on board] */
+
+#define S32Z_CODE_SRAM_DATA_ALIAS 0x32100000UL
+
+/* RTU data SRAM. The RTU has 1 MB of it, in three contiguous banks that */
+/* are all RTU-local -- when an RTU accesses its own blocks the traffic */
+/* stays inside the RTU. The banks differ in speed, not in locality: */
+/* */
+/* DRAM0 0x31780000 256 KB full core speed */
+/* DRAM1 0x317C0000 256 KB full core speed */
+/* DRAM2 0x31800000 512 KB half core speed */
+/* */
+/* S32Z2 Reference Manual Rev. 5, section 6.3.6 and Table 13. Confirmed */
+/* on the board: writing distinct values to all three banks and reading */
+/* them back shows 1 MB of independent storage, and the first address */
+/* past DRAM2 aliases back onto its base, which is what fixes its size at */
+/* 512 KB rather than the 2 MB an earlier revision of this file claimed. */
+/* */
+/* Note that NXP's own debugger memory map, s32z2e2_memory_regions.py in */
+/* S32 Design Studio, describes this last bank as 2 MB. The Reference */
+/* Manual and the silicon agree it is 512 KB; the script is wrong here. */
+
+#define S32Z_DRAM0_BASE 0x31780000UL
+#define S32Z_DRAM0_SIZE 0x00040000UL
+#define S32Z_DRAM1_BASE 0x317C0000UL
+#define S32Z_DRAM1_SIZE 0x00040000UL
+/* Memory for loadable modules: the top 64 KB of DRAM1, deliberately left out of
+ the broad data region in mpu.c.
+
+ It has to be outside every region the board support package programs. The
+ manager gives a module its own regions, and if the kernel's map already covered
+ that memory then every thread could reach the module and back -- the isolation
+ would be nominal. Carving a hole is the only way to make it real.
+
+ DRAM1 rather than DRAM2 because it runs at full core speed (S32Z2 RM 6.3.6),
+ and because code and data can be contiguous here. Putting module code in the
+ code RAM region instead would have been the obvious choice and does not work:
+ that region is read-only, so a module's data would have to live somewhere else
+ and the module would straddle two carve-outs.
+
+ 64 KB is arbitrary but not accidental: it leaves 448 KB of full-speed RAM for
+ the kernel's data, stacks and bss, which currently use about 14 KB. */
+
+#define S32Z_MODULE_AREA_BASE 0x317F0000UL
+#define S32Z_MODULE_AREA_SIZE 0x00010000UL /* 64 KB */
+
+/* The shared granules the sample module reports its progress through, at the
+ base of the module area.
+
+ A module cannot print -- the console belongs to the board support package and
+ lies outside every region a module owns -- so the manager grants it shared
+ regions and reads them back. On this board a GDB harness reads the module's
+ own progress variable as well, out of the data area the manager allocated;
+ these granules are the channel that needs no debugger, and having both means
+ the two agree or the run says so.
+
+ Six granules rather than one, because a module may be granted
+ TXM_MODULE_MPU_SHARED_ENTRIES regions and one grant only ever exercises the
+ first of them. Five are granted, one granule per entry, and
+ S32Z_MODULE_STATUS_UNGRANTED -- index 2, so a granted granule sits on either
+ side of it -- is never granted to anything. A limit register masked the wrong
+ way, or a base off by a granule, extends a region into that gap from one side
+ or the other, and a module that can write it was given more than was asked
+ for.
+
+ S32Z_MODULE_STATUS_SIZE is the size of ONE granule, which is also the length
+ of each individual grant; the area is GRANULES of them. Fixed addresses on
+ both sides, checked at run time against the linker's symbol rather than
+ trusted. */
+
+#define S32Z_MODULE_STATUS_BASE 0x317F0000UL
+#define S32Z_MODULE_STATUS_SIZE 0x40UL /* one MPU granule */
+#define S32Z_MODULE_STATUS_GRANULES 6UL /* five granted, one not */
+#define S32Z_MODULE_STATUS_UNGRANTED 2UL /* the one never granted */
+
+/* What is left of the full-speed pair for the kernel itself. link.lds sizes its
+ DATA region from this, so the linker cannot place kernel data in the module
+ area by accident. */
+
+#define S32Z_DATA_SRAM_SHARED_SIZE (S32Z_MODULE_AREA_BASE - S32Z_DRAM0_BASE)
+
+/* The top 8 KB of DRAM2 is deliberately left out of the broad data region in
+ mpu.c and mapped one window at a time, per thread, instead. Isolation is only
+ meaningful in memory that no other region already grants access to, and every
+ other region in that map is a wide RW window -- a private buffer placed inside
+ one of them would be reachable by every thread no matter what else was
+ programmed. */
+
+#define S32Z_THREAD_WINDOW_BASE 0x3187E000UL
+#define S32Z_THREAD_WINDOW_SIZE 0x00001000UL /* 4 KB per thread */
+#define S32Z_THREAD_WINDOW_COUNT 2UL
+#define S32Z_DRAM2_SHARED_SIZE (S32Z_THREAD_WINDOW_BASE - S32Z_DRAM2_BASE)
+
+#define S32Z_DRAM2_BASE 0x31800000UL
+#define S32Z_DRAM2_SIZE 0x00080000UL
+
+/* The full-speed pair, contiguous, and the right home for stacks and */
+/* .data. This is what link.lds calls DATA. */
+
+#define S32Z_DATA_SRAM_BASE S32Z_DRAM0_BASE
+#define S32Z_DATA_SRAM_SIZE (S32Z_DRAM0_SIZE + S32Z_DRAM1_SIZE)
+
+/* Console. The EVB user guide (UG10268) dedicates LIN9 to the */
+/* daughtercard USB-UART, selected by jumper J248, so LINFlex_9 is the */
+/* instance that reaches the host -- not LINFlex_0, which is the first */
+/* one in the Reference Manual's list and the tempting wrong choice. */
+/* Base addresses from the RM; LINFlex_0's LINCR1 reset value 0x00000082 */
+/* was read back from the board, which is what confirms the address space.*/
+
+#define S32Z_LINFLEX_9_BASE 0x42980000UL /* USB-UART [RM] */
+#define S32Z_LINFLEX_0_BASE 0x40170000UL /* [RM+board] */
+
+/* Mode Entry and Reset Generation, used by the debugger's init sequence */
+/* to release and clock the RTU cores. Recorded because their contents */
+/* were what proved the core was released rather than held in reset. [RM]*/
+
+#define S32Z_MC_ME_PRTN0_PCONF 0x41900300UL
+#define S32Z_MC_ME_PRTN0_CORE0_ADDR 0x4190034CUL
+#define S32Z_RGM_PRST0 0x41850048UL
+
+/* Debug system as seen from a core: MDM-AP holds the per-core external */
+/* debug request bits that must be clear for the core to execute at all. */
+/* Reachable from the core context only. [RM + verified on board] */
+
+/* RTU0 general-purpose registers. CFG_CNTDV holds the divider that makes*/
+/* the Cortex-R52 generic timer clock-enable, CNTCLKEN, from a cluster */
+/* clock. Reset value of the CNTDV field is 4. [RM section 9.2.1.6] */
+
+/* RTU peripheral window, as described by IMP_PERIPHPREGIONR: 4 MB at */
+/* 0x76000000, reached through the low-latency peripheral port. */
+
+#define S32Z_RTU_PERIPH_BASE 0x76000000UL
+#define S32Z_RTU_PERIPH_SIZE 0x00400000UL
+
+#define S32Z_RTU0_GPR_BASE 0x76120000UL
+#define S32Z_RTU0_GPR_CFG_CNTDV (S32Z_RTU0_GPR_BASE + 0x10UL)
+
+/* GICv3 for this R52 cluster. IMP_CBAR reports the distributor base in */
+/* bits [31:21] and reads 0x47800000 on this board, confirming NXP's map */
+/* from the hardware -- the SoC reference manual gives no GIC address at */
+/* all, and its note that the "R52 Cluster set addr [31:21]" is just */
+/* CFGPERIPHBASE[31:21] restated. Frame layout from Cortex-R52 TRM Table */
+/* 9-1: distributor at +0x000000, redistributor control at +0x100000, */
+/* redistributor SGI/PPI at +0x110000, then +0x20000 per further core. */
+/* The region must be mapped Device nGnRnE before it can be read; with the*/
+/* MPU disabled an access stalls the core. */
+
+#define S32Z_GIC_BASE 0x47800000UL
+#define S32Z_GIC_SIZE 0x00200000UL
+
+/* Frames within that window, from Cortex-R52 TRM Table 9-1: distributor at
+ +0x000000, redistributor control at +0x100000, redistributor SGI/PPI at
+ +0x110000, then +0x20000 per further core. Both frames verified on the
+ board -- GICD_PIDR2 and GICR_PIDR2 both read 0x3B.
+
+ These must be mapped Device nGnRnE before use. With the region Normal, or
+ with the MPU disabled, an access stalls the core outright: no abort, no
+ fault handler, and the debug connection drops. mpu.c maps them. */
+
+#define GICD_BASE (S32Z_GIC_BASE + 0x000000UL)
+#define GICR_RD_BASE (S32Z_GIC_BASE + 0x100000UL)
+#define GICR_SGI_BASE (S32Z_GIC_BASE + 0x110000UL)
+
+#define S32Z_MDM_AP_BASE 0x4DC11000UL
+#define S32Z_MDM_AP_CONTROL2 (S32Z_MDM_AP_BASE + 0x44UL)
+#define S32Z_MDM_AP_RTU0_CORE0_EDBGREQ 0x00010000UL
+
+#endif /* PLATFORM_H */
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/readme_s32z280.txt b/ports/cortex_r52/gnu/example_build/s32z280_evb/readme_s32z280.txt
new file mode 100644
index 00000000..30aef5dc
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/readme_s32z280.txt
@@ -0,0 +1,178 @@
+Cortex-R52 example build for the NXP S32Z280-594EVB
+===================================================
+
+RTU0 core 0 (R52_0_0) on the S32Z280-594EVB daughtercard, reached through an
+S32 Debug Probe. This is silicon bring-up: the FVP example under
+example_build/fvp_baser_aemv8r validates the architecture, and this one
+validates the implementation. MIDR here is 0x411FD133 (Cortex-R52 r1p3);
+the FVP reports 0x410FD0F0, part 0xD0F, which is an architecture envelope
+model and not an R52 at all.
+
+
+1. Build
+
+ cmake -B build_s32z -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE=cmake/cortex_r52.cmake \
+ -DTX_R52_BUILD_S32Z280_EXAMPLE=ON .
+ ninja -C build_s32z s32z280_boot.elf
+
+ The option is separate from TX_R52_BUILD_FVP_EXAMPLE so that FVP
+ regression stays green regardless of what bring-up is doing, and so the
+ two boards' differing reset state and memory maps cannot interact.
+
+
+2. Run
+
+ S32Z280_ELF=build_s32z/ports/cortex_r52/gnu/example_build/s32z280_evb/s32z280_boot.elf \
+ arm-none-eabi-gdb-py -batch -x tools/read_identity.gdb
+
+ The host side is not optional and not obvious; see setup.md section 9. In
+ short: a CCS must be listening on 41475 and it must be the *Windows* CCS,
+ because its code pushes to the probe complete while the Linux CCS's stall
+ after "Sending code ... done". Under WSL2 mirrored networking both share
+ one port namespace, so a stray Linux CCS silently blocks the Windows one.
+ GTA runs on 45000, and arm-none-eabi-gdb-py needs a source-built Python
+ 3.10.11 in PYTHONHOME.
+
+
+3. Three silicon facts this build exists to encode
+
+ a. The core resets in THUMB state. CPSR reads 0x1FA out of reset -- Hyp
+ mode with T set -- because the RTU boot instruction NXP plants at the
+ boot address is a T32 branch. _start is therefore T32 and switches to
+ A32 itself. An A32 entry would execute the first halfword of its own
+ instruction as Thumb and end up anywhere.
+
+ b. The debugger holds every core in debug state. NXP's
+ _reset_to_first_instruction() asserts MDM_AP CONTROL2[19:16] =
+ CR52_RTU0_{3,2,1,0}_EDBGREQ and nothing ever clears them, despite the
+ comment in NXP's own source claiming start_debug_by_core_name() does.
+ While EDBGREQ is asserted the core cannot execute at all, yet registers
+ and memory still respond and MC_ME/RGM report the core released and
+ clocked -- so it presents as working hardware that ignores every
+ attempt to run. tools/read_identity.gdb clears core 0's bit and
+ verifies the clear took.
+
+ c. CNTFRQ reads zero, the same as on the FVP. It is writable only at the
+ highest implemented exception level, so it must be programmed at EL2.
+ entry.S deliberately records it rather than writing a value, because
+ the correct frequency for this board is not yet established and a wrong
+ one would silently mis-scale every interval derived from it. Any timer
+ work here must program CNTFRQ first.
+
+
+4. Memory map
+
+ Code SRAM is visible through two windows onto the same physical memory:
+ 0x79900000 for instruction fetch, 0x32100000 as data. The core resets to
+ 0x79900000 -- MC_ME_PRTN0_CORE0_ADDR reads exactly that -- so .text is
+ linked and loaded there. That window is writable over the debug AXI port
+ even though NXP's debugger memory map declares it read-only, so a
+ debugger-loaded image needs no alias. A flash-booted image would.
+
+ Data, bss and the per-mode stacks live in RTU-local data SRAM at
+ 0x31780000 (256 KB); 0x31800000 gives 2 MB if an image outgrows it.
+
+ Reads of 0x30000000/0x30100000/0x30200000 do fault, but the earlier note
+ here had the reason wrong. Reading the region registers on this part shows
+ ATCM already ENABLED at both exception levels, 64 KB with one wait state,
+ and every BASEADDRESS field zero -- so ATCM is live at address 0x00000000,
+ not at 0x30000000. The faulting reads were of an address the TCM is not
+ at. ATCM's enables reset set because CFGTCMBOOTx is tied high here. BTCM
+ (16 KB, 0 wait states) and CTCM (16 KB, 1 wait state) really are disabled.
+ Sizes and wait states agree with the S32Z2 reference manual exactly.
+
+ ECC is implemented and enabled -- IMP_MEMPROTCTLR reads 0x11 -- so per
+ Cortex-R52 TRM 6.2.2 a TCM location must be written before it is read, and
+ ATCM needs 64-bit aligned stores where BTCM and CTCM accept 32-bit. Using
+ TCM therefore takes more than setting an enable bit.
+
+ entry.S now programs ATCM to 0x30000000 and enables it at both exception
+ levels, at EL2 -- writing ENABLEEL2 from EL1 is silently ignored, measured on
+ two banks. The boot image preloads it (ECC check bits are not initialised by
+ the core) and proves it holds data both before and after the MPU is enabled.
+
+ BTCM and CTCM are left disabled because nothing in this example uses them.
+
+ An earlier version of this file said something much stronger and it was
+ wrong. It claimed that enabling any second bank removes all measurable
+ data-cache benefit on this part, and gave five configurations as evidence:
+ ATCM alone at a 24% cache gain, and four combinations involving a second bank
+ at none. It concluded the cause was not a particular bank, not its address
+ and not the ECC preload, but enabling a second bank at all. A defect report
+ went to NXP on that basis and has since been withdrawn.
+
+ The benchmark that produced those numbers was bimodal with respect to where
+ its timing loop fell inside a 64-byte cache line. The same workload on the
+ same silicon reported either 24% or nothing at all depending on that
+ placement, and each of the five configurations was an edit to entry.S, so
+ each shifted the code that followed and moved the loop between the two
+ modes. Adding two nop instructions to entry.S reproduces the "second bank"
+ figure exactly.
+
+ Re-measured with a benchmark that sweeps four loop alignments and reports the
+ distribution, one bank and two are indistinguishable:
+
+ loop offset in line ATCM only ATCM + CTCM
+ 0 gain 0 gain 0
+ 16 gain 0 gain 0
+ 32 gain 240/1000 gain 240/1000
+ 48 gain 240/1000 gain 240/1000
+
+ Enabling a second bank costs nothing measurable. It is one line in entry.S,
+ and the prerequisites are the ECC preload before any read (TRM 6.2.2) and an
+ MPU region, both already handled for ATCM.
+
+ The wider lesson is worth more than the TCM detail: a single-figure timing
+ result from this example cannot be compared across builds unless the timed
+ loop's alignment is controlled, because almost any change shifts code. The
+ cache and interrupt-handler measurements here now sweep alignments and report
+ the spread for that reason.
+
+ The DDR window at 0x7A000000 is dark until DDR is initialised. TCM is still
+ absent from link.lds: nothing is placed there yet.
+
+
+5. What this image reports
+
+ boot_stage says how far the reset path got, so a fault is distinguishable
+ from a hang; if a fault handler ran it records the vector, syndrome and
+ faulting address before parking. r52_identity.magic is written last, so a
+ half-filled structure cannot be mistaken for a complete one. bsp_done() is
+ a deliberate breakpoint target, so "the image finished" is observed rather
+ than inferred from a timeout.
+
+ Verified on the board:
+
+ MIDR 0x411FD133 Cortex-R52 r1p3
+ MPUIR 0x00001400 20 MPU regions at EL1
+ HMPUIR 0x00000014 EL2; the count field sits in bits [7:0] here
+ rather than [15:8] as in MPUIR, which still
+ needs confirming against the Armv8-R spec
+ CTR 0x8144C004
+ MPIDR 0x80000000
+ ID_PFR0 0x00000131
+ ID_PFR1 0x10111001 virtualisation field 1: EL2 implemented
+ SCTLR 0x70C50838 MPU, D-cache and I-cache all off at reset
+ CNTFRQ 0x00000000
+
+ CPSR at EL2 0x000001DA mode 0x1A, Hyp
+ CPSR at EL1 0x600001D3 mode 0x13, Supervisor
+
+ The two CPSR values are the point of the milestone: they are the EL2 to EL1
+ drop performed by this code on silicon rather than on a model.
+
+
+6. Not here yet
+
+ No console. LIN9 reaches the host through the daughtercard USB-UART
+ (jumper J248), which is LINFlex_9 at 0x42980000 -- not LINFlex_0 at
+ 0x40170000, the tempting wrong choice as the first instance in the
+ Reference Manual's list. What is missing is the clock configuration needed
+ to compute a baud rate; a console at the wrong rate produces garbage
+ indistinguishable from a crash, which is why this milestone reports through
+ memory instead.
+
+ No timer, no GIC, no MPU programming and no ThreadX: this image does not
+ link the kernel, so a failure here is unambiguously a boot or board problem
+ rather than a kernel one.
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.c
new file mode 100644
index 00000000..081dc414
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.c
@@ -0,0 +1,304 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* tcm.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Reads what the TCM hardware reports about itself. See tcm.h for */
+/* the register layouts and where they come from; nothing here is */
+/* inferred from a neighbouring register's shape. */
+/* */
+/* Reading only. Programming a base address and setting the enables */
+/* comes after these values have been seen and agree with both the */
+/* Cortex-R52 TRM and the S32Z2 reference manual, because a disagreement*/
+/* would mean one of the two documents does not describe this part and */
+/* guessing which would be the whole PRBAR mistake again. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Dir 4.3 -- assembly encapsulated one operation per function. */
+/* */
+/**************************************************************************/
+
+#include "tcm.h"
+
+#define TCM_SIZE_SHIFT 2U
+#define TCM_SIZE_MASK 0x1FUL
+#define TCM_WAITSTATES_SHIFT 8U
+#define TCM_ENABLE_EL2 (1UL << 1)
+#define TCM_ENABLE_EL10 (1UL << 0)
+#define TCM_BASE_SHIFT 13U /* TRM table 3-136: [31:13] */
+
+#define MEMPROT_RAMPROTIMP (1UL << 4)
+#define MEMPROT_RAMPROTEN (1UL << 0)
+
+
+static unsigned long read_atcmregionr(void)
+{
+ unsigned long value;
+ __asm volatile ("mrc p15, 0, %0, c9, c1, 0" : "=r" (value));
+ return value;
+}
+
+static unsigned long read_btcmregionr(void)
+{
+ unsigned long value;
+ __asm volatile ("mrc p15, 0, %0, c9, c1, 1" : "=r" (value));
+ return value;
+}
+
+static unsigned long read_ctcmregionr(void)
+{
+ unsigned long value;
+ __asm volatile ("mrc p15, 0, %0, c9, c1, 2" : "=r" (value));
+ return value;
+}
+
+
+/* SIZE is an encoding, not a shift count: 4 means 8KB and each step doubles,
+ so 8KB << (field - 4). Anything outside the documented range yields 0
+ rather than a plausible-looking wrong number. */
+
+static unsigned long decode_size(unsigned int field)
+{
+ unsigned long bytes = 0UL;
+
+ if ((field >= 4U) && (field <= 11U))
+ {
+ bytes = 0x2000UL << (field - 4U);
+ }
+
+ return bytes;
+}
+
+
+void tcm_read_bank(unsigned int index, tcm_bank_t *bank_ptr)
+{
+ unsigned long raw;
+
+ if (bank_ptr == 0)
+ {
+ return;
+ }
+
+ switch (index)
+ {
+ case 0U: raw = read_atcmregionr(); break;
+ case 1U: raw = read_btcmregionr(); break;
+ case 2U: raw = read_ctcmregionr(); break;
+ default: raw = 0UL; break;
+ }
+
+ bank_ptr->raw = raw;
+ bank_ptr->base = raw & ~((1UL << TCM_BASE_SHIFT) - 1UL);
+ bank_ptr->size_field = (unsigned int) ((raw >> TCM_SIZE_SHIFT) & TCM_SIZE_MASK);
+ bank_ptr->size_bytes = decode_size(bank_ptr->size_field);
+ bank_ptr->wait_states = (unsigned int) ((raw >> TCM_WAITSTATES_SHIFT) & 1UL);
+ bank_ptr->enable_el2 = ((raw & TCM_ENABLE_EL2) != 0UL) ? 1U : 0U;
+ bank_ptr->enable_el10 = ((raw & TCM_ENABLE_EL10) != 0UL) ? 1U : 0U;
+}
+
+
+unsigned long tcm_read_memprotctlr(void)
+{
+ unsigned long value;
+
+ /* Note opc1 is 1 here, not 0 as for the region registers. */
+
+ __asm volatile ("mrc p15, 1, %0, c9, c1, 2" : "=r" (value));
+ return value;
+}
+
+
+unsigned int tcm_ecc_implemented(void)
+{
+ return ((tcm_read_memprotctlr() & MEMPROT_RAMPROTIMP) != 0UL) ? 1U : 0U;
+}
+
+
+unsigned int tcm_ecc_enabled(void)
+{
+ unsigned long ctlr = tcm_read_memprotctlr();
+
+ /* RAMPROTEN is ignored when RAMPROTIMP is 0: the TRM says the design
+ behaves as if it were 0, so report it that way rather than echoing a
+ bit that has no effect. */
+
+ if ((ctlr & MEMPROT_RAMPROTIMP) == 0UL)
+ {
+ return 0U;
+ }
+
+ return ((ctlr & MEMPROT_RAMPROTEN) != 0UL) ? 1U : 0U;
+}
+
+
+static void write_regionr(unsigned int index, unsigned long value)
+{
+ switch (index)
+ {
+ case 0U:
+ __asm volatile ("mcr p15, 0, %0, c9, c1, 0" :: "r" (value) : "memory");
+ break;
+ case 1U:
+ __asm volatile ("mcr p15, 0, %0, c9, c1, 1" :: "r" (value) : "memory");
+ break;
+ case 2U:
+ __asm volatile ("mcr p15, 0, %0, c9, c1, 2" :: "r" (value) : "memory");
+ break;
+ default:
+ return;
+ }
+
+ __asm volatile ("isb sy" ::: "memory");
+}
+
+
+unsigned int tcm_enable(unsigned int index, unsigned long base)
+{
+ tcm_bank_t bank;
+ unsigned long value;
+
+ if (index > 2U)
+ {
+ return 0U;
+ }
+
+ tcm_read_bank(index, &bank);
+
+ if (bank.size_bytes == 0UL)
+ {
+ return 0U; /* not implemented; nothing to enable */
+ }
+
+ /* Size-aligned, per TRM 6.2. Rejecting a misaligned base is better than
+ programming it and discovering later that the bank answers somewhere
+ unexpected. */
+
+ if ((base & (bank.size_bytes - 1UL)) != 0UL)
+ {
+ return 0U;
+ }
+
+ /* Preserve the read-only SIZE and WAITSTATES fields as read: they describe
+ the hardware and writing a different value there would be meaningless. */
+
+ value = (base & ~((1UL << TCM_BASE_SHIFT) - 1UL))
+ | (bank.raw & ((TCM_SIZE_MASK << TCM_SIZE_SHIFT) | (1UL << TCM_WAITSTATES_SHIFT)))
+ | TCM_ENABLE_EL2
+ | TCM_ENABLE_EL10;
+
+ write_regionr(index, value);
+
+ /* Read back rather than trusting the write. */
+
+ tcm_read_bank(index, &bank);
+
+ /* ENABLEEL10 is the criterion, not both enables. Writing ENABLEEL2 from
+ EL1 is ignored -- measured on BTCM and CTCM, where the base and bit 0 took
+ while bit 1 stayed clear -- so requiring it here would report failure for
+ a bank that is perfectly usable at the level this code runs at. EL2
+ access is arranged in entry.S, before the drop. */
+
+ return ((bank.base == (base & ~((1UL << TCM_BASE_SHIFT) - 1UL))) &&
+ (bank.enable_el10 != 0U)) ? 1U : 0U;
+}
+
+
+void tcm_preload(unsigned int index, unsigned long base, unsigned long bytes)
+{
+ if (index == 0U)
+ {
+ /* ATCM: 64-bit stores at 64-bit alignment. TRM 6.2.2 is explicit that
+ STR of a word is not sufficient here, so this writes unsigned long
+ long and the generated code is checked to be STRD or STM rather than
+ two word stores. */
+
+ volatile unsigned long long *p = (volatile unsigned long long *) base;
+ unsigned long count = bytes / 8UL;
+ unsigned long i;
+
+ for (i = 0UL; i < count; i++)
+ {
+ p[i] = 0ULL;
+ }
+ }
+ else
+ {
+ /* BTCM and CTCM: 32-bit stores at 32-bit alignment are enough. */
+
+ volatile unsigned int *p = (volatile unsigned int *) base;
+ unsigned long count = bytes / 4UL;
+ unsigned long i;
+
+ for (i = 0UL; i < count; i++)
+ {
+ p[i] = 0U;
+ }
+ }
+
+ __asm volatile ("dsb sy" ::: "memory");
+}
+
+
+/**************************************************************************/
+/* Copy .atcm_text into ATCM. */
+/**************************************************************************/
+
+extern char __atcm_text_start__;
+extern char __atcm_text_end__;
+extern char __atcm_text_load__;
+
+unsigned long tcm_copy_atcm_text(void)
+{
+ /* The linker aligns both ends of the section to 8, so the whole extent is a
+ whole number of 64-bit units and there is no tail to handle narrowly.
+ That matters here rather than being tidiness: a 32-bit store to ATCM does
+ not establish the ECC check bits for its location, so a tail copied 32
+ bits at a time would leave words that fault when read. */
+
+ volatile unsigned long long *dst;
+ const volatile unsigned long long *src;
+ unsigned long bytes;
+ unsigned long i;
+
+ dst = (volatile unsigned long long *) (void *) &__atcm_text_start__;
+ src = (const volatile unsigned long long *) (void *) &__atcm_text_load__;
+ bytes = (unsigned long) (&__atcm_text_end__ - &__atcm_text_start__);
+
+ for (i = 0UL; i < (bytes / 8UL); i++)
+ {
+ dst[i] = src[i];
+ }
+
+ /* Complete the writes, then discard any stale instruction-side copy of the
+ destination before it is executed. */
+
+ __asm__ volatile("dsb sy" ::: "memory");
+ __asm__ volatile("mcr p15, 0, r0, c7, c5, 0" : : : "memory"); /* ICIALLU */
+ __asm__ volatile("dsb sy" ::: "memory");
+ __asm__ volatile("isb" ::: "memory");
+
+ return bytes;
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.h
new file mode 100644
index 00000000..b80f226d
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.h
@@ -0,0 +1,161 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* tcm.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frederic Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Tightly-coupled memory on the S32Z280. This header is the reading */
+/* half: what the hardware reports about itself. Enabling comes next */
+/* and depends on what these reads say. */
+/* */
+/* WHERE THE FACTS COME FROM */
+/* */
+/* Cortex-R52 TRM r1p3 (100026_0103_00_en), section 3.3.94, table */
+/* 3-136. IMP_ATCMREGIONR, IMP_BTCMREGIONR and IMP_CTCMREGIONR are */
+/* at MRC/MCR p15, 0, <Rt>, c9, c1, {0,1,2} and are laid out: */
+/* */
+/* [31:13] BASEADDRESS bits [31:13] of the TCM base address */
+/* [12:9] RES0 */
+/* [8] WAITSTATES wait states for TCM accesses */
+/* [7] RES0 */
+/* [6:2] SIZE size indicator, read-only in effect */
+/* [1] ENABLEEL2 enable at EL2 */
+/* [0] ENABLEEL10 enable at EL1 and EL0 */
+/* */
+/* ⚠ BASEADDRESS is [31:13], not [31:12]. IMP_PERIPHPREGIONR uses */
+/* [31:12] and inferring the same here would be wrong by one bit -- */
+/* the same class of mistake that made PRBAR silently drop XN. The */
+/* base is therefore 8KB-aligned, not 4KB. */
+/* */
+/* SIZE encodings: 0 none, 4 8KB, 5 16KB, 6 32KB, 7 64KB, 8 128KB, */
+/* 9 256KB, 10 512KB, 11 1MB. */
+/* */
+/* "At reset all bits are 0 apart from SIZE and WAITSTATES", unless */
+/* CFGTCMBOOTx is high, which resets the ATCM enables to 1. So a */
+/* part that has not been told otherwise has every TCM based at 0 with */
+/* both enables clear, which is exactly why reads of the documented */
+/* TCM windows fault on this board. */
+/* */
+/* IMP_MEMPROTCTLR is at p15, 1, <Rt>, c9, c1, 2 (TRM 3.3.76, table */
+/* 3-114): RAMPROTIMP [4] says whether RAM protection exists at all, */
+/* RAMPROTEN [0] whether it is on, and RAMPROTEN is ignored when */
+/* RAMPROTIMP is 0. */
+/* */
+/* S32Z2 reference manual: TCMA 64KB with 1 wait state, TCMB 16KB with */
+/* 0, TCMC 16KB with 1, per core. Those give expected SIZE values of */
+/* 7, 5 and 5, which is a cross-check between the two documents and */
+/* the silicon rather than a restatement of either. */
+/* */
+/* WHY ENABLING IS NOT ENOUGH */
+/* */
+/* TRM section 6.2.2: the ECC bits in TCM RAM are not initialised by */
+/* the processor, and a location must be written before it is read */
+/* with ECC enabled. An uninitialised read reports an error, which */
+/* looks exactly like "the TCM is not accessible" and would send the */
+/* reader back to these region registers, which were already right. */
+/* */
+/* The preload widths are not uniform: BTCM and CTCM take STR, STRD */
+/* or STM at 32-bit alignment, but ATCM needs STRD or STM at 64-bit */
+/* alignment. A C loop storing 32-bit words would leave ATCM's check */
+/* bits invalid. */
+/* */
+/**************************************************************************/
+
+#ifndef TCM_H
+#define TCM_H
+
+/* One bank's reported configuration, decoded from its region register. */
+
+typedef struct
+{
+ unsigned long raw; /* register as read */
+ unsigned long base; /* BASEADDRESS << 13 */
+ unsigned int size_field; /* SIZE, as encoded */
+ unsigned long size_bytes; /* decoded, 0 if the encoding is unknown */
+ unsigned int wait_states;
+ unsigned int enable_el2;
+ unsigned int enable_el10;
+} tcm_bank_t;
+
+/* Read and decode one bank. Index 0 is ATCM, 1 BTCM, 2 CTCM; any other
+ index yields a zeroed structure. */
+
+void tcm_read_bank(unsigned int index, tcm_bank_t *bank_ptr);
+
+/* IMP_MEMPROTCTLR, raw. */
+
+unsigned long tcm_read_memprotctlr(void);
+
+/* Whether RAM protection is implemented, and whether it is enabled. Both are
+ decoded from IMP_MEMPROTCTLR, and the second is meaningless when the first
+ is zero -- the TRM says the design behaves as if RAMPROTEN were 0. */
+
+unsigned int tcm_ecc_implemented(void);
+unsigned int tcm_ecc_enabled(void);
+
+/**************************************************************************/
+/* ENABLING */
+/* */
+/* Constraints, all from Cortex-R52 TRM r1p3 section 6.2 and 3.3.94: */
+/* */
+/* - The base address must be SIZE-ALIGNED, not merely 8KB-aligned as */
+/* the BASEADDRESS field's width alone would suggest. */
+/* - A disabled TCM's base address is UNKNOWN, not zero, so every bank */
+/* must be programmed explicitly rather than adjusted from what it */
+/* appears to hold. */
+/* - "Before using the TCM you must program MPU regions to cover the */
+/* TCM regions to give access." An enabled TCM with no MPU region */
+/* still faults once the MPU is on. */
+/* - An enabled TCM always behaves as Non-cacheable Non-shareable */
+/* Normal memory whatever the MPU says; the MPU supplies only the */
+/* permissions. So the MPU region's attribute index is irrelevant */
+/* here and its AP and XN bits are not. */
+/* - With ECC on, every location must be written before it is read. */
+/* */
+/**************************************************************************/
+
+/* Program one bank's base and enable it at both exception levels. Returns 1 on
+ success, 0 if the index is out of range, the bank reports no size, or the base
+ is not size-aligned. Verifies by reading the register back. */
+
+unsigned int tcm_enable(unsigned int index, unsigned long base);
+
+/* Write every location of a bank so its ECC check bits become valid. The store
+ width is chosen from the bank: index 0 is ATCM and needs 64-bit stores, and
+ the others take 32-bit. Must be called while the bank is enabled and
+ accessible, and before anything reads it. */
+
+void tcm_preload(unsigned int index, unsigned long base, unsigned long bytes);
+
+/* Copy the .atcm_text section from its load address in CODE to its run address
+ in ATCM. Must run before anything in that section is called, and after the
+ bank is enabled. Returns the number of bytes copied, which is zero when the
+ section is empty -- the normal case for an image that places nothing there.
+
+ Uses 64-bit stores because ECC is enabled on this part and ATCM requires
+ them; see tcm_preload and Cortex-R52 TRM 6.2.2. */
+
+unsigned long tcm_copy_atcm_text(void);
+
+#endif
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c
new file mode 100644
index 00000000..5681754f
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c
@@ -0,0 +1,237 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* thread_mpu.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Per-thread memory protection. See thread_mpu.h for what this is */
+/* and is not. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Directive 4.3 -- the MPU is only reachable through CP15; every */
+/* access is encapsulated in a one-line accessor below. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include "platform.h"
+#include "mpu.h"
+#include "timer.h"
+#include "thread_mpu.h"
+
+/* The region reprogrammed on every thread entry. mpu.c programs 0 to 7, and
+ MPUIR on this part reports 20, so 8 is free. Chosen high on purpose: the
+ shared regions keep their indices, so a thread that reaches this hook with no
+ window still has code, stacks, console and GIC exactly as before. */
+
+#define THREAD_MPU_REGION 8UL
+
+typedef struct
+{
+ TX_THREAD *owner;
+ unsigned long base;
+} thread_window_t;
+
+static thread_window_t windows[S32Z_THREAD_WINDOW_COUNT];
+
+static unsigned long switch_count;
+static unsigned int last_cycles;
+static unsigned int min_cycles = 0xFFFFFFFFU;
+static unsigned int max_cycles;
+
+
+/**************************************************************************/
+/* CP15 accessors. */
+/**************************************************************************/
+
+/* Direct per-region access rather than PRSELR.
+
+ The Cortex-R52 TRM 8.4 encodes the region number into CRm and opc2: CRm is
+ 0b1rrr where rrr is region_number[3:1], with opc2 0 and 1 for an even region
+ and 4 and 5 for an odd one. Regions 0 through 15 take CP15 op1 0 and regions
+ 16 through 24 take op1 1. PRBAR and PRLAR without a number are the indirect
+ view selected by PRSELR.
+
+ Region 8 is therefore op1 0, CRm c12, opc2 0 and 1. Using it removes the
+ PRSELR write and, more importantly, the ISB that has to follow PRSELR before
+ the region registers can be written.
+
+ Measured on this part: 542 to 604 cycles through PRSELR against 434 to 470
+ direct, for the same region and the same isolation result. The saving matters
+ most where several regions are programmed at once, which is what a module
+ switch does -- there the PRSELR route pays an ISB per region while the direct
+ route pays one barrier pair for the whole block.
+
+ The TRM disagrees with itself about the range, and the next reader should not
+ have to rediscover that. The register descriptions at 3.3.85 and 3.3.86 say
+ "Direct access is provided to PRBAR0 to PRBAR15" and list only the op1 0
+ form; 8.4 and Table 8-9 list both forms. Table 8-9 also prints opc2 0 for
+ the even limit registers where the prose of 8.4 says 1. What is written here
+ uses op1 0 only, which is the half of the manual this part has been measured
+ against. */
+
+/* Kept for regions 16 and above. Those have direct op1 1 encodings in TRM 8.4,
+ but only the selector route is proven on this part, and nothing here needs a
+ region that high while the per-thread window lives at region 8. */
+
+__attribute__((unused))
+static void write_prselr(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c2, 1" : : "r"(value) : "memory");
+}
+
+static void write_prbar8_direct(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c12, 0" : : "r"(value) : "memory");
+}
+
+static void write_prlar8_direct(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c12, 1" : : "r"(value) : "memory");
+}
+
+__attribute__((unused))
+static void write_prbar(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c3, 0" : : "r"(value) : "memory");
+}
+
+__attribute__((unused))
+static void write_prlar(unsigned long value)
+{
+ __asm__ volatile("mcr p15, 0, %0, c6, c3, 1" : : "r"(value) : "memory");
+}
+
+
+/**************************************************************************/
+/* Registration. */
+/**************************************************************************/
+
+unsigned long thread_mpu_window_base(unsigned int window)
+{
+ if ((unsigned long) window >= S32Z_THREAD_WINDOW_COUNT)
+ {
+ return 0UL;
+ }
+
+ return S32Z_THREAD_WINDOW_BASE
+ + ((unsigned long) window * S32Z_THREAD_WINDOW_SIZE);
+}
+
+unsigned long thread_mpu_grant(TX_THREAD *thread_ptr, unsigned int window)
+{
+ unsigned long base = thread_mpu_window_base(window);
+
+ if ((base == 0UL) || (thread_ptr == TX_NULL))
+ {
+ return 0UL;
+ }
+
+ windows[window].owner = thread_ptr;
+ windows[window].base = base;
+
+ return base;
+}
+
+
+/**************************************************************************/
+/* Activation. */
+/* */
+/* Called explicitly by a thread rather than from the scheduler, and that */
+/* is a deliberate limitation of this step rather than the intended */
+/* design. */
+/* */
+/* The port's scheduler does call _tx_execution_thread_enter under */
+/* TX_ENABLE_EXECUTION_CHANGE_NOTIFY, which would make this automatic on */
+/* every switch. That macro is read by the port assembly compiled into */
+/* the shared threadx library, so enabling it would oblige all nine */
+/* example targets in this port -- including the FVP ones -- to supply */
+/* the four execution hooks. A ThreadX module port carries its own */
+/* copies of the port assembly for exactly this reason, and that is where */
+/* the switch belongs. */
+/* */
+/* What this step establishes without that machinery: that a PMSAv8-R */
+/* region can be reprogrammed per thread on this part, what it costs, and */
+/* that a violation faults. Those are the questions worth answering */
+/* before writing a module manager on top of them. */
+/**************************************************************************/
+
+void thread_mpu_activate(TX_THREAD *thread_ptr)
+{
+ unsigned int before;
+ unsigned int after;
+ unsigned int delta;
+ unsigned int i;
+ unsigned long base = 0UL;
+
+ before = timer_read_cycles();
+
+ for (i = 0U; (unsigned long) i < S32Z_THREAD_WINDOW_COUNT; i++)
+ {
+ if (windows[i].owner == thread_ptr)
+ {
+ base = windows[i].base;
+ break;
+ }
+ }
+
+ if (base == 0UL)
+ {
+ /* No window for this thread: disable the region rather than leaving the
+ previous thread's window in place. Forgetting this is how per-thread
+ protection silently becomes no protection -- the last thread to run
+ would leave its window open to whatever ran next. */
+
+ write_prlar8_direct(0UL);
+ }
+ else
+ {
+ write_prbar8_direct((base & ~0x3FUL)
+ | ((unsigned long) MPU_SH_NON << 3)
+ | ((unsigned long) MPU_AP_RW_EL1 << 1)
+ | 1UL); /* XN: data only */
+
+ write_prlar8_direct(((base + S32Z_THREAD_WINDOW_SIZE - 1UL) & ~0x3FUL)
+ | ((unsigned long) MPU_ATTR_NORMAL_WB << 1)
+ | 1UL); /* EN */
+ }
+
+ __asm__ volatile("dsb sy" ::: "memory");
+ __asm__ volatile("isb" ::: "memory");
+
+ after = timer_read_cycles();
+ delta = after - before;
+
+ switch_count++;
+ last_cycles = delta;
+ if (delta < min_cycles) { min_cycles = delta; }
+ if (delta > max_cycles) { max_cycles = delta; }
+}
+
+
+unsigned long thread_mpu_switch_count(void) { return switch_count; }
+unsigned int thread_mpu_last_cycles(void) { return last_cycles; }
+unsigned int thread_mpu_min_cycles(void) { return min_cycles; }
+unsigned int thread_mpu_max_cycles(void) { return max_cycles; }
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.h
new file mode 100644
index 00000000..cba6226e
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.h
@@ -0,0 +1,79 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* thread_mpu.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Per-thread memory protection for the NXP S32Z280-594EVB. */
+/* */
+/* One MPU region is reprogrammed on every thread entry to grant the */
+/* incoming thread access to its own window and nothing else. A thread */
+/* with no window registered runs with that region disabled, so it */
+/* reaches none of them. */
+/* */
+/* This is a step towards a ThreadX module port for this core, not a */
+/* substitute for one. It demonstrates that PMSAv8-R regions can be */
+/* switched per thread on this part, at a measured cost, and that a */
+/* violation faults -- which are the questions worth answering before */
+/* building a module manager on top of them. There is no user mode, no */
+/* syscall boundary and no loader here. */
+/* */
+/* The hook is _tx_execution_thread_enter, which the port's scheduler */
+/* already calls under TX_ENABLE_EXECUTION_CHANGE_NOTIFY, after the */
+/* stack pointer has been switched. No port assembly is modified. */
+/* */
+/**************************************************************************/
+
+#ifndef THREAD_MPU_H
+#define THREAD_MPU_H
+
+#include "tx_api.h"
+
+/* Apply a thread's window, or disable the region if it has none. Called by the
+ thread itself in this step; see the comment in thread_mpu.c for why it is not
+ yet driven from the scheduler. */
+
+void thread_mpu_activate(TX_THREAD *thread_ptr);
+
+/* Give a thread exclusive access to one of the per-thread windows. The index
+ selects the window, 0 to S32Z_THREAD_WINDOW_COUNT - 1. Returns the window's
+ base address, or 0 if the index is out of range or the table is full. */
+
+unsigned long thread_mpu_grant(TX_THREAD *thread_ptr, unsigned int window);
+
+/* Base address of a window, whether or not it is granted to anyone. Lets a
+ test reach for a window it does not own. */
+
+unsigned long thread_mpu_window_base(unsigned int window);
+
+/* How many thread entries have reprogrammed the region, and what the last one
+ cost in cycles. The cost is the price of per-thread protection on this core
+ and is the reason the hook records it. */
+
+unsigned long thread_mpu_switch_count(void);
+unsigned int thread_mpu_last_cycles(void);
+unsigned int thread_mpu_min_cycles(void);
+unsigned int thread_mpu_max_cycles(void);
+
+#endif
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.c
new file mode 100644
index 00000000..9fc5c0a1
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.c
@@ -0,0 +1,186 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* timer.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Arm generic timer access for the S32Z280-594EVB. */
+/* */
+/* CNTFRQ reads zero on this silicon, exactly as it does on the */
+/* Armv8-R AEM FVP. CNTFRQ is only a software-populated constant -- */
+/* nothing derives the counter's real rate from it -- so a zero there */
+/* says nothing about whether the counter is running. The only way to */
+/* find that out is to read CNTPCT twice and see whether it moved, */
+/* which is what this file exists to support. */
+/* */
+/* The rate itself is measured rather than computed. The RTU divides */
+/* a cluster clock by RTU.GPR CFG_CNTDV to generate the timer's */
+/* clock-enable, so deriving it needs the divider AND the cluster clock*/
+/* AND the PLL configuration feeding it -- three places to be wrong. */
+/* Sampling CNTPCT against host wall-clock time needs none of them and */
+/* is checkable against the clock tree afterwards. */
+/* */
+/* MISRA C:2012 deviations (justified) */
+/* */
+/* Dir 4.3 -- assembly language shall be encapsulated and isolated. */
+/* Each coprocessor access is wrapped in its own function below. */
+/* */
+/**************************************************************************/
+
+#include "timer.h"
+
+unsigned long long timer_read_cntpct(void)
+{
+ unsigned int low;
+ unsigned int high;
+
+ /* CNTPCT is 64-bit and read as a pair through MRRC. The read is
+ atomic in the architecture, so no high/low re-read dance is needed. */
+
+ __asm volatile ("mrrc p15, 0, %0, %1, c14" : "=r" (low), "=r" (high));
+
+ return ((unsigned long long)high << 32) | (unsigned long long)low;
+}
+
+
+unsigned int timer_read_cntfrq(void)
+{
+ unsigned int value;
+
+ __asm volatile ("mrc p15, 0, %0, c14, c0, 0" : "=r" (value));
+
+ return value;
+}
+
+
+void timer_spin(unsigned int iterations)
+{
+ unsigned int i;
+
+ for (i = 0U; i < iterations; i++)
+ {
+ /* Kept from being optimised away, and kept from being turned into
+ something clever: the loop's only job is to burn a repeatable
+ amount of time between two counter samples. */
+
+ __asm volatile ("nop" ::: "memory");
+ }
+}
+
+
+/* CNTP_CTL bits. IMASK is set so the compare raises no interrupt: there is
+ no GIC configured yet, and an unhandled PPI would be taken as an exception
+ rather than politely ignored. */
+
+#define CNTP_CTL_ENABLE (1U << 0)
+#define CNTP_CTL_IMASK (1U << 1)
+#define CNTP_CTL_ISTATUS (1U << 2)
+
+
+void timer_start_oneshot_irq(unsigned int ticks)
+{
+ /* Same as timer_start_oneshot but with IMASK CLEAR, so the compare raises
+ an interrupt instead of only setting ISTATUS. Keeping the two arming
+ functions separate rather than adding a flag: the polled path must never
+ accidentally unmask before the GIC is up, and an interrupt-driven path
+ that silently leaves IMASK set produces a timer that fires internally
+ while no interrupt is ever delivered -- IRQ count 0 with no spurious and
+ no unexpected INTID, which is exactly how that mistake presents. */
+
+ unsigned int control = CNTP_CTL_ENABLE;
+
+ __asm volatile ("mcr p15, 0, %0, c14, c2, 0" :: "r" (ticks));
+ __asm volatile ("mcr p15, 0, %0, c14, c2, 1" :: "r" (control));
+ __asm volatile ("isb");
+}
+
+
+void timer_start_oneshot(unsigned int ticks)
+{
+ unsigned int control = CNTP_CTL_ENABLE | CNTP_CTL_IMASK;
+
+ /* TVAL is a down-counter loaded relative to now, which avoids the 64-bit
+ read-modify-write that setting CVAL absolutely would need. */
+
+ __asm volatile ("mcr p15, 0, %0, c14, c2, 0" :: "r" (ticks));
+ __asm volatile ("mcr p15, 0, %0, c14, c2, 1" :: "r" (control));
+ __asm volatile ("isb");
+}
+
+
+unsigned int timer_fired(void)
+{
+ unsigned int control;
+
+ __asm volatile ("mrc p15, 0, %0, c14, c2, 1" : "=r" (control));
+
+ return ((control & CNTP_CTL_ISTATUS) != 0U) ? 1U : 0U;
+}
+
+
+void timer_stop(void)
+{
+ unsigned int control = 0U;
+
+ __asm volatile ("mcr p15, 0, %0, c14, c2, 1" :: "r" (control));
+ __asm volatile ("isb");
+}
+
+
+/**************************************************************************/
+/* PMU cycle counter. */
+/* */
+/* PMCR.E gates all counters including the dedicated cycle counter, and */
+/* PMCNTENSET bit 31 enables that counter specifically. Both are needed. */
+/* PMCR.D is left clear so the counter advances every cycle rather than */
+/* every 64th, which is the resolution these measurements want. */
+/**************************************************************************/
+
+#define PMCR_E (1UL << 0) /* enable all counters */
+#define PMCR_P (1UL << 1) /* reset event counters */
+#define PMCR_C (1UL << 2) /* reset cycle counter */
+#define PMCNTENSET_C (1UL << 31) /* cycle counter enable */
+
+void timer_cycles_enable(void)
+{
+ unsigned long pmcr;
+
+ /* Allow EL0/EL1 access and enable, resetting the counter as we go. */
+
+ __asm__ volatile("mrc p15, 0, %0, c9, c12, 0" : "=r"(pmcr));
+ pmcr |= (PMCR_E | PMCR_C);
+ __asm__ volatile("mcr p15, 0, %0, c9, c12, 0" : : "r"(pmcr) : "memory");
+
+ __asm__ volatile("mcr p15, 0, %0, c9, c12, 1"
+ : : "r"((unsigned long) PMCNTENSET_C) : "memory");
+
+ __asm__ volatile("isb" ::: "memory");
+}
+
+unsigned int timer_read_cycles(void)
+{
+ unsigned long value;
+ __asm__ volatile("mrc p15, 0, %0, c9, c13, 0" : "=r"(value));
+ return (unsigned int) value;
+}
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.h
new file mode 100644
index 00000000..0a0866b8
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.h
@@ -0,0 +1,72 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/* */
+/* BOARD SUPPORT RELEASE */
+/* */
+/* timer.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Arm generic timer access for the S32Z280-594EVB. */
+/* */
+/**************************************************************************/
+
+#ifndef TIMER_H
+#define TIMER_H
+
+/* The physical count, CNTPCT. 64-bit, read as a pair. Reading it is the
+ only way to find out whether the system counter is running at all: CNTFRQ
+ reads zero on this part and so says nothing about the counter itself. */
+
+unsigned long long timer_read_cntpct(void);
+
+/* CNTFRQ as currently programmed. Zero out of reset on this silicon. */
+
+unsigned int timer_read_cntfrq(void);
+
+/* Crude calibrated-by-nothing delay, used only to separate counter samples
+ far enough apart to measure a rate. Not a time delay: the core clock is
+ not yet established, which is precisely what the measurement is for. */
+
+void timer_spin(unsigned int iterations);
+
+/* One-shot compare on the EL1 physical timer, interrupt masked. Polled
+ deliberately: this proves the timer counts and fires before the GIC is
+ configured, so a later interrupt failure cannot be confused with the timer
+ itself being wrong. */
+
+void timer_start_oneshot(unsigned int ticks);
+
+/* Arms with the interrupt UNMASKED, for the interrupt-driven path. */
+
+void timer_start_oneshot_irq(unsigned int ticks);
+unsigned int timer_fired(void);
+void timer_stop(void);
+
+/* PMU cycle counter. CNTPCT runs at 8 MHz on this part, so its 125 ns
+ granularity is too coarse to resolve interrupt latency, let alone the jitter
+ in it. The cycle counter runs at the core clock and is what those
+ measurements use. timer_cycles_enable must be called before any read. */
+
+void timer_cycles_enable(void);
+unsigned int timer_read_cycles(void);
+
+#endif /* TIMER_H */
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/tools/read_identity.gdb b/ports/cortex_r52/gnu/example_build/s32z280_evb/tools/read_identity.gdb
new file mode 100644
index 00000000..1bb0890e
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/tools/read_identity.gdb
@@ -0,0 +1,160 @@
+# Copyright (c) 2026 Eclipse ThreadX contributors
+# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
+#
+# Load s32z280_boot.elf onto the S32Z280-594EVB, run it, and report what the
+# core said about itself.
+#
+# Usage:
+# S32Z280_ELF=<path to s32z280_boot.elf> \
+# arm-none-eabi-gdb-py -batch -x tools/read_identity.gdb
+#
+# Requires, and does not do for you:
+# * a CCS listening on 41475 -- use the *Windows* CCS ("ccs.exe -noportquit"),
+# whose code pushes to the probe complete; the Linux CCS's push stalls.
+# Under WSL2 mirrored networking both share one port namespace, so kill any
+# Linux CCS first ("pkill -x ccs", not -f: it runs as ./ccs).
+# * GTA on 45000: Server/gta/gta -p 45000 -k
+# * PYTHONHOME pointing at a source-built Python 3.10.11, which is what
+# arm-none-eabi-gdb-py's embedded interpreter needs.
+#
+# Two silicon quirks are handled below rather than assumed away: the debugger
+# holds every RTU core in debug state through MDM_AP CONTROL2, and the core
+# resets in Thumb state.
+
+py _PROBE_IP = "s32dbg:192.168.50.238"
+py _SOC_NAME = "S32Z280"
+py _CORE_NAME = "R52_0_0"
+py _GDB_SERVER_PORT = 45000
+
+source /home/fdesbiens/NXP/S32DS.3.6.10/S32DS/tools/S32Debugger/Debugger/scripts/s32z2e2/s32z2e2_generic_bareboard_all_cores.py
+
+py board_init()
+py s32z2e2_cores.init(_CORE_NAME)
+py s32z2e2_cores.start_debug_by_core_name(_CORE_NAME)
+
+# Select the core context before asking about core state. NXP's core_init()
+# omits this and then stop_core() reads the mode of the SoC context, which has
+# none, and reports "Core can't be stopped. State is undefined/unknown".
+py gta_lib.set_context(context.CORE_CTX[_SOC_NAME + "_" + _CORE_NAME]).unwrap()
+py gta_lib.stop_core().unwrap()
+py gta_lib.attach().unwrap()
+
+python
+import gdb, os
+
+# ---------------------------------------------------------------------------
+# Release the core from the debugger's external debug request.
+#
+# _reset_to_first_instruction() asserts MDM_AP CONTROL2[19:16] =
+# CR52_RTU0_{3,2,1,0}_EDBGREQ (0x000F0000) and nothing clears them, so every
+# RTU0 core is pinned in debug state and executes nothing -- registers and
+# memory still respond, which is what makes it look like working hardware.
+# ---------------------------------------------------------------------------
+CONTROL2 = 0x4DC11044
+EDBGREQ_CORE0 = 0x00010000
+
+def rd(addr):
+ return int(gdb.parse_and_eval("*(unsigned int *)0x%x" % addr)) & 0xFFFFFFFF
+
+c2 = rd(CONTROL2)
+gdb.execute("set *(unsigned int *)0x%x = 0x%x" % (CONTROL2, c2 & ~EDBGREQ_CORE0))
+if rd(CONTROL2) & EDBGREQ_CORE0:
+ raise gdb.GdbError("CR52_RTU0_0_EDBGREQ is still set; the core will not run")
+print("EDBGREQ cleared: CONTROL2 0x%08X -> 0x%08X" % (c2, rd(CONTROL2)))
+
+elf = os.environ.get("S32Z280_ELF", "s32z280_boot.elf")
+gdb.execute("file %s" % elf)
+gdb.execute("load")
+
+# The core resets in Thumb state and CPSR cannot be written through this
+# connection, so entry must be the T32 _start. Mask bit 0: GDB reports a Thumb
+# function's address with it set, but PC itself must be even.
+start = int(gdb.parse_and_eval("(unsigned int)&_start")) & ~1
+gdb.execute("set $pc = 0x%x" % start)
+print("PC set to 0x%08X" % start)
+
+gdb.execute("hbreak bsp_done")
+gdb.execute("continue")
+end
+
+echo \n===== boot progress =====\n
+python
+import gdb
+
+def rd(addr):
+ return int(gdb.parse_and_eval("*(unsigned int *)0x%x" % addr)) & 0xFFFFFFFF
+
+STAGES = {1: "switched out of Thumb", 2: "EL2 configured", 3: "running at EL1",
+ 4: "entered bsp_main", 5: "bsp_main returned (unexpected)",
+ 0xFF: "A FAULT HANDLER RAN"}
+
+stage = int(gdb.parse_and_eval("*(unsigned int *)&boot_stage")) & 0xFFFFFFFF
+print(" boot_stage = %d (%s)" % (stage, STAGES.get(stage, "unknown")))
+
+ident = None
+
+if stage == 0xFF:
+ print(" fault_vector = 0x%02X" % (int(gdb.parse_and_eval("*(unsigned int *)&fault_vector")) & 0xFFFFFFFF))
+ print(" fault_syndrome = 0x%08X" % (int(gdb.parse_and_eval("*(unsigned int *)&fault_syndrome")) & 0xFFFFFFFF))
+ print(" fault_address = 0x%08X" % (int(gdb.parse_and_eval("*(unsigned int *)&fault_address")) & 0xFFFFFFFF))
+else:
+ # r52_identity belongs to the boot image, which is what this script is for.
+ # The kernel demo shares the same breakpoint and progress marker but carries
+ # no identity structure, so against that image the lookup raised and gdb
+ # exited 1 -- a failure report for a run that had already passed, which is
+ # worse than printing nothing.
+ try:
+ ident = gdb.parse_and_eval("r52_identity")
+ except gdb.error:
+ print("\n This image has no r52_identity structure, so there is no "
+ "identity report to print.")
+ print(" Expected for the kernel demo, which reports its own result "
+ "over the console.")
+
+if ident is not None:
+ def f(name):
+ return int(ident[name]) & 0xFFFFFFFF
+
+ magic = f("magic")
+ if magic != 0x52520001:
+ print(" magic = 0x%08X -- structure INCOMPLETE, values below are not "
+ "trustworthy" % magic)
+ else:
+ print(" magic = 0x%08X (complete)" % magic)
+
+ print("\n===== S32Z280 R52_0_0, reported by the core itself =====")
+ midr = f("midr")
+ part = (midr >> 4) & 0xFFF
+ print(" MIDR = 0x%08X implementer 0x%02X variant 0x%X part 0x%03X rev 0x%X"
+ % (midr, (midr >> 24) & 0xFF, (midr >> 20) & 0xF, part, midr & 0xF))
+ print(" part 0x%03X -> %s" % (part,
+ {0xD13: "Cortex-R52", 0xD16: "Cortex-R52+"}.get(part, "UNRECOGNISED")))
+ print(" MPUIR = 0x%08X EL1 MPU regions = %d"
+ % (f("mpuir"), f("mpu_regions_el1")))
+ print(" HMPUIR = 0x%08X (EL2; count field position still to be confirmed)"
+ % f("hmpuir_el2"))
+ print(" CTR = 0x%08X" % f("ctr"))
+ print(" MPIDR = 0x%08X" % f("mpidr"))
+ print(" ID_PFR0 = 0x%08X" % f("id_pfr0"))
+ pfr1 = f("id_pfr1")
+ print(" ID_PFR1 = 0x%08X virtualisation field 0x%X -> EL2 %s"
+ % (pfr1, (pfr1 >> 12) & 0xF,
+ "implemented" if ((pfr1 >> 12) & 0xF) != 0 else "ABSENT"))
+ sctlr = f("sctlr_el1")
+ print(" SCTLR = 0x%08X M(MPU)=%d C(dcache)=%d I(icache)=%d"
+ % (sctlr, sctlr & 1, (sctlr >> 2) & 1, (sctlr >> 12) & 1))
+ print(" CNTFRQ = 0x%08X (%s)" % (f("cntfrq_el2"),
+ "zero as on the FVP; a tick interval derived from it would divide by zero"
+ if f("cntfrq_el2") == 0 else "non-zero, programmed before we got here"))
+
+ print("\n===== exception levels traversed =====")
+ for label, field, want, wname in (("at EL2 ", "cpsr_el2", 0x1A, "Hyp"),
+ ("at EL1 ", "cpsr_el1", 0x13, "Supervisor")):
+ v = f(field)
+ mode = v & 0x1F
+ print(" CPSR %s = 0x%08X mode 0x%02X (%s) %s"
+ % (label, v, mode,
+ {0x1A: "Hyp", 0x13: "Supervisor", 0x1F: "System"}.get(mode, "?"),
+ "as expected" if mode == want else "EXPECTED %s (0x%02X)" % (wname, want)))
+end
diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/tx_initialize_low_level.S b/ports/cortex_r52/gnu/example_build/s32z280_evb/tx_initialize_low_level.S
new file mode 100644
index 00000000..097eeafe
--- /dev/null
+++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/tx_initialize_low_level.S
@@ -0,0 +1,110 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** Initialize */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+ /* No .arch directive. GNU as accepts "armv8-r"; LLVM's integrated
+ assembler accepts no spelling of it at all -- not armv8-r, not armv8r --
+ and fails with "Unknown Arch". The architecture comes from
+ -mcpu=cortex-r52 on the command line, which every toolchain file passes,
+ so the directive only restated it. */
+
+ .syntax unified
+ .arm
+
+ .global _tx_thread_system_stack_ptr
+ .global _tx_initialize_unused_memory
+
+ .text
+ .balign 4
+
+/**************************************************************************/
+/* */
+/* FUNCTION RELEASE */
+/* */
+/* _tx_initialize_low_level Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Low-level initialization for Cortex-R52 on the S32Z280-594EVB. */
+/* Called by _tx_initialize_kernel_enter with the processor already in */
+/* Supervisor mode at EL1. */
+/* */
+/* This is deliberately shorter than the A-profile reference ports: */
+/* entry.S has already given every AArch32 mode its own stack from */
+/* dedicated linker-script regions, so there is no stack carving to do */
+/* here and no run-time stack-overlap check to perform -- the linker */
+/* guarantees the regions do not overlap. */
+/* */
+/* When built with TX_R52_USE_THREADX_IRQ, board_init() brings up the */
+/* MPU, GICv3 and the generic timer tick here. The MPU comes first on */
+/* this board: the GIC region has to be mapped Device nGnRnE before any*/
+/* GIC register can be touched at all. */
+/* and the generic timer tick here. That is safe this early because */
+/* interrupts stay masked until _tx_thread_schedule enables them, so no*/
+/* tick can be delivered before the kernel is ready to take one. */
+/* Without that symbol (the M2 cooperative demo) no tick is created, */
+/* which keeps M2 free of any interrupt dependency. */
+/* */
+/* CALLED BY */
+/* */
+/* _tx_initialize_kernel_enter ThreadX entry function */
+/* */
+/**************************************************************************/
+
+ .global _tx_initialize_low_level
+ .type _tx_initialize_low_level, %function
+_tx_initialize_low_level:
+
+ /* Record the system stack. The interrupt path uses it when an
+ exception arrives while no thread is running. */
+
+ ldr r0, =__sys_stack_top
+ ldr r1, =_tx_thread_system_stack_ptr
+ str r0, [r1]
+
+ /* Publish the first free memory address for tx_application_define.
+ _end sits above the image, .bss and every per-mode stack. Round up
+ to an 8-byte boundary so pool control blocks are aligned. */
+
+ ldr r0, =_end
+ add r0, r0, #7
+ bic r0, r0, #7
+ ldr r1, =_tx_initialize_unused_memory
+ str r0, [r1]
+
+#ifdef TX_R52_USE_THREADX_IRQ
+
+ /* Bring up the interrupt controller and the periodic tick. */
+
+ push {lr}
+ bl board_init
+ pop {lr}
+
+#endif
+
+ bx lr
diff --git a/ports/cortex_r52/gnu/inc/tx_port.h b/ports/cortex_r52/gnu/inc/tx_port.h
new file mode 100644
index 00000000..afc67c11
--- /dev/null
+++ b/ports/cortex_r52/gnu/inc/tx_port.h
@@ -0,0 +1,393 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+// Portions of this file were generated with AI assistance.
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** Port Specific */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+
+/**************************************************************************/
+/* */
+/* PORT SPECIFIC C INFORMATION RELEASE */
+/* */
+/* tx_port.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* Derived from the Cortex-R5/GNU port originally written by */
+/* William E. Lamie, Microsoft Corporation. */
+/* */
+/* DESCRIPTION */
+/* */
+/* This file contains data type definitions that make the ThreadX */
+/* real-time kernel function identically on a variety of different */
+/* processor architectures. For example, the size or number of bits */
+/* in an "int" data type vary between microprocessor architectures and */
+/* even C compilers for the same microprocessor. ThreadX does not */
+/* directly use native C data types. Instead, ThreadX creates its */
+/* own special types that can be mapped to actual data types by this */
+/* file to guarantee consistency in the interface and functionality. */
+/* */
+/**************************************************************************/
+
+#ifndef TX_PORT_H
+#define TX_PORT_H
+
+
+/* Determine if the optional ThreadX user define file should be used. */
+
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+
+
+/* Yes, include the user defines in tx_user.h. The defines in this file may
+ alternately be defined on the command line. */
+
+#include "tx_user.h"
+#endif
+
+
+/* Define compiler library include files. */
+
+#include <stdlib.h>
+#include <string.h>
+
+
+/* Define ThreadX basic types for this port. */
+
+#define VOID void
+typedef char CHAR;
+typedef unsigned char UCHAR;
+typedef int INT;
+typedef unsigned int UINT;
+typedef long LONG;
+typedef unsigned long ULONG;
+typedef short SHORT;
+typedef unsigned short USHORT;
+
+
+/* Define the priority levels for ThreadX. Legal values range
+ from 32 to 1024 and MUST be evenly divisible by 32. */
+
+#ifndef TX_MAX_PRIORITIES
+#define TX_MAX_PRIORITIES 32
+#endif
+
+
+/* Define the minimum stack for a ThreadX thread on this processor. If the size supplied during
+ thread creation is less than this value, the thread create call will return an error. */
+
+#ifndef TX_MINIMUM_STACK
+#define TX_MINIMUM_STACK 200 /* Minimum stack size for this port */
+#endif
+
+
+/* Define the system timer thread's default stack size and priority. These are only applicable
+ if TX_TIMER_PROCESS_IN_ISR is not defined. */
+
+#ifndef TX_TIMER_THREAD_STACK_SIZE
+#define TX_TIMER_THREAD_STACK_SIZE 1024 /* Default timer thread stack size */
+#endif
+
+#ifndef TX_TIMER_THREAD_PRIORITY
+#define TX_TIMER_THREAD_PRIORITY 0 /* Default timer thread priority */
+#endif
+
+
+/* Define various constants for the ThreadX ARM port. */
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+#define TX_INT_DISABLE 0xC0 /* Disable IRQ & FIQ interrupts */
+#else
+#define TX_INT_DISABLE 0x80 /* Disable IRQ interrupts */
+#endif
+#define TX_INT_ENABLE 0x00 /* Enable IRQ interrupts */
+
+
+/* Define the clock source for trace event entry time stamp. The following two item are port specific.
+ For example, if the time source is at the address 0x0a800024 and is 16-bits in size, the clock
+ source constants would be:
+
+#define TX_TRACE_TIME_SOURCE *((ULONG *) 0x0a800024)
+#define TX_TRACE_TIME_MASK 0x0000FFFFUL
+
+*/
+
+#ifndef TX_TRACE_TIME_SOURCE
+#define TX_TRACE_TIME_SOURCE ++_tx_trace_simulated_time
+#endif
+#ifndef TX_TRACE_TIME_MASK
+#define TX_TRACE_TIME_MASK 0xFFFFFFFFUL
+#endif
+
+
+/* Define the port specific options for the _tx_build_options variable. This variable indicates
+ how the ThreadX library was built. */
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+#define TX_FIQ_ENABLED 1
+#else
+#define TX_FIQ_ENABLED 0
+#endif
+
+#ifdef TX_ENABLE_IRQ_NESTING
+#define TX_IRQ_NESTING_ENABLED 2
+#else
+#define TX_IRQ_NESTING_ENABLED 0
+#endif
+
+#ifdef TX_ENABLE_FIQ_NESTING
+#define TX_FIQ_NESTING_ENABLED 4
+#else
+#define TX_FIQ_NESTING_ENABLED 0
+#endif
+
+#define TX_PORT_SPECIFIC_BUILD_OPTIONS TX_FIQ_ENABLED | TX_IRQ_NESTING_ENABLED | TX_FIQ_NESTING_ENABLED
+
+
+/* Define the in-line initialization constant so that modules with in-line
+ initialization capabilities can prevent their initialization from being
+ a function call. */
+
+#define TX_INLINE_INITIALIZATION
+
+
+/* Determine whether or not stack checking is enabled. By default, ThreadX stack checking is
+ disabled. When the following is defined, ThreadX thread stack checking is enabled. If stack
+ checking is enabled (TX_ENABLE_STACK_CHECKING is defined), the TX_DISABLE_STACK_FILLING
+ define is negated, thereby forcing the stack fill which is necessary for the stack checking
+ logic. */
+
+#ifdef TX_ENABLE_STACK_CHECKING
+#undef TX_DISABLE_STACK_FILLING
+#endif
+
+
+/* Define the TX_THREAD control block extensions for this port. The main reason
+ for the multiple macros is so that backward compatibility can be maintained with
+ existing ThreadX kernel awareness modules. */
+
+/* TX_THREAD_EXTENSION_2 carries the per-thread VFP enable flag used by the
+ lazy floating-point save and restore in tx_thread_schedule.S,
+ tx_thread_system_return.S and tx_thread_context_restore.S.
+
+ It is defined UNCONDITIONALLY, not under TX_ENABLE_VFP_SUPPORT, and that is
+ deliberate: the assembly reaches this field through a hard-coded structure
+ offset, so making the field conditional would move every following member
+ between build configurations and leave the offset correct in only one of
+ them. Keeping it always present makes the layout independent of the
+ floating-point build options. The offset is checked at compile time in
+ tx_port_offset_check.c, which turns a wrong offset into a build failure
+ instead of silent corruption of an unrelated thread field. */
+
+#define TX_THREAD_EXTENSION_0
+#define TX_THREAD_EXTENSION_1
+#define TX_THREAD_EXTENSION_2 ULONG tx_thread_vfp_enable;
+#define TX_THREAD_EXTENSION_3
+
+
+/* Define the port extensions of the remaining ThreadX objects. */
+
+#define TX_BLOCK_POOL_EXTENSION
+#define TX_BYTE_POOL_EXTENSION
+#define TX_EVENT_FLAGS_GROUP_EXTENSION
+#define TX_MUTEX_EXTENSION
+#define TX_QUEUE_EXTENSION
+#define TX_SEMAPHORE_EXTENSION
+#define TX_TIMER_EXTENSION
+
+
+/* Define the user extension field of the thread control block. Nothing
+ additional is needed for this port so it is defined as white space. */
+
+#ifndef TX_THREAD_USER_EXTENSION
+#define TX_THREAD_USER_EXTENSION
+#endif
+
+
+/* Define the macros for processing extensions in tx_thread_create, tx_thread_delete,
+ tx_thread_shell_entry, and tx_thread_terminate. */
+
+
+#define TX_THREAD_CREATE_EXTENSION(thread_ptr)
+#define TX_THREAD_DELETE_EXTENSION(thread_ptr)
+#define TX_THREAD_COMPLETED_EXTENSION(thread_ptr)
+#define TX_THREAD_TERMINATED_EXTENSION(thread_ptr)
+
+
+/* Define the ThreadX object creation extensions for remaining objects. */
+
+#define TX_BLOCK_POOL_CREATE_EXTENSION(pool_ptr)
+#define TX_BYTE_POOL_CREATE_EXTENSION(pool_ptr)
+#define TX_EVENT_FLAGS_GROUP_CREATE_EXTENSION(group_ptr)
+#define TX_MUTEX_CREATE_EXTENSION(mutex_ptr)
+#define TX_QUEUE_CREATE_EXTENSION(queue_ptr)
+#define TX_SEMAPHORE_CREATE_EXTENSION(semaphore_ptr)
+#define TX_TIMER_CREATE_EXTENSION(timer_ptr)
+
+
+/* Define the ThreadX object deletion extensions for remaining objects. */
+
+#define TX_BLOCK_POOL_DELETE_EXTENSION(pool_ptr)
+#define TX_BYTE_POOL_DELETE_EXTENSION(pool_ptr)
+#define TX_EVENT_FLAGS_GROUP_DELETE_EXTENSION(group_ptr)
+#define TX_MUTEX_DELETE_EXTENSION(mutex_ptr)
+#define TX_QUEUE_DELETE_EXTENSION(queue_ptr)
+#define TX_SEMAPHORE_DELETE_EXTENSION(semaphore_ptr)
+#define TX_TIMER_DELETE_EXTENSION(timer_ptr)
+
+
+/* Determine whether this core has the CLZ instruction and this compiler will
+ admit to it, and if so replace the portable lowest-set-bit search with it.
+
+ The guard is not upstream's. Upstream asks __TARGET_ARCH_ARM > 4, which is an
+ Arm Compiler 5 predefine. GCC does not define it -- it predefines the ACLE
+ macros __ARM_ARCH and __ARM_FEATURE_CLZ instead -- so under GCC the test reads
+ 0 > 4, this whole block is dropped and tx_thread.h's portable loop runs on a
+ core that has had the instruction since Armv5. Measured with
+ arm-none-eabi-gcc 14.3 on 20 Aug 2026: zero CLZ instructions in the built
+ scheduler objects.
+
+ That was not a dormant path. Half the TX_LOWEST_SET_BIT_CALCULATE call sites
+ in tx_thread_suspend.c and tx_thread_system_suspend.c sit OUTSIDE the
+ TX_MAX_PRIORITIES > 32 guards, so the portable loop was running in the
+ scheduler's priority search in the default 32-priority configuration, which is
+ the one every R52 build uses.
+
+ __ARM_FEATURE_CLZ is the ACLE answer to the question actually being asked, and
+ the compiler defines it exactly when the architecture has the instruction, so
+ a core without CLZ is excluded by construction rather than by an architecture
+ number. Arm Compiler 5's spelling is kept beside it, now wrapped in defined()
+ so the test no longer leans on an undefined identifier evaluating to zero --
+ which is what -Wundef reports and how this was found.
+
+ The __thumb__ guard stays, and it is load-bearing rather than inherited
+ caution: __ARM_FEATURE_CLZ describes the ARCHITECTURE, not the instruction
+ set. Checked on 20 Aug 2026 -- GCC defines it for -mthumb -march=armv5te,
+ where Thumb-1 has no CLZ at all and this asm would fail to assemble. A Thumb
+ build therefore keeps the portable loop on purpose. (On this core it is moot:
+ the R52 toolchain file builds -marm.)
+
+ Two deliberate deviations, per AGENTS.md:
+
+ - Rule 1.2, language extensions. Inline assembly is the entire point of the
+ macro; there is no conforming way to reach CLZ. Spelled __asm__ and not
+ asm, because the asm keyword is rejected under -std=c99 -- verified, it is
+ an "'asm' undeclared" error -- and AGENTS.md requires C99 compatibility.
+
+ - Rule 10.1 / 10.3 on the isolation step, which is why it is respelled.
+ Upstream isolates the lowest set bit with (ULONG) (-((LONG) m)): that
+ converts an unsigned map to signed and negates it, which is undefined for
+ the one input whose top bit is set. (~(m)) + 1 is the same value in
+ well-defined unsigned arithmetic, and it is character-for-character what
+ tx_thread.h's portable version uses -- so the two implementations now
+ visibly compute the same thing instead of merely agreeing.
+
+ Rule 20.7 is a straight fix rather than a deviation: upstream leaves m and b
+ unparenthesised in the expansion.
+
+ PRECONDITION: m must be non-zero, and the two implementations DISAGREE when it
+ is not. CLZ(0) is 32, so this yields 31 - 32; the portable loop yields 0.
+ All twelve call sites in common/src reach the macro only on a map already
+ tested against zero -- every one checked on 20 Aug 2026 -- so the divergence is
+ unreachable today. It is written down because a new call site is exactly how
+ it would stop being unreachable, and demo_clz.c pins both answers so that
+ changing this has to be a decision. */
+
+#if defined(__ARM_FEATURE_CLZ) || (defined(__TARGET_ARCH_ARM) && (__TARGET_ARCH_ARM > 4))
+
+#ifndef __thumb__
+
+#define TX_LOWEST_SET_BIT_CALCULATE(m, b) \
+ (m) = (m) & ((~(m)) + ((ULONG) 1)); \
+ __asm__ volatile (" CLZ %0,%1 " : "=r" (b) : "r" (m)); \
+ (b) = 31 - (b);
+
+#endif
+#endif
+
+
+/* Define ThreadX interrupt lockout and restore macros for protection on
+ access of critical kernel information. The restore interrupt macro must
+ restore the interrupt posture of the running thread prior to the value
+ present prior to the disable macro. In most cases, the save area macro
+ is used to define a local function save area for the disable and restore
+ macros. */
+
+/* Per-thread floating-point control. Implemented in tx_thread_schedule.S and
+ available only when the library is built with TX_ENABLE_VFP_SUPPORT. A
+ thread's floating-point context is saved and restored lazily: only threads
+ that have called tx_thread_vfp_enable() pay for it. */
+
+#ifdef TX_ENABLE_VFP_SUPPORT
+void tx_thread_vfp_enable(void);
+void tx_thread_vfp_disable(void);
+#endif
+
+
+#ifdef __thumb__
+
+unsigned int _tx_thread_interrupt_disable(void);
+unsigned int _tx_thread_interrupt_restore(UINT old_posture);
+
+
+#define TX_INTERRUPT_SAVE_AREA UINT interrupt_save;
+
+#define TX_DISABLE interrupt_save = _tx_thread_interrupt_disable();
+#define TX_RESTORE _tx_thread_interrupt_restore(interrupt_save);
+
+#else
+
+#define TX_INTERRUPT_SAVE_AREA UINT interrupt_save;
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+#define TX_DISABLE asm volatile (" MRS %0,CPSR; CPSID if ": "=r" (interrupt_save) );
+#else
+#define TX_DISABLE asm volatile (" MRS %0,CPSR; CPSID i ": "=r" (interrupt_save) );
+#endif
+
+#define TX_RESTORE asm volatile (" MSR CPSR_c,%0 "::"r" (interrupt_save) );
+
+#endif
+
+
+/* Define the interrupt lockout macros for each ThreadX object. */
+
+#define TX_BLOCK_POOL_DISABLE TX_DISABLE
+#define TX_BYTE_POOL_DISABLE TX_DISABLE
+#define TX_EVENT_FLAGS_GROUP_DISABLE TX_DISABLE
+#define TX_MUTEX_DISABLE TX_DISABLE
+#define TX_QUEUE_DISABLE TX_DISABLE
+#define TX_SEMAPHORE_DISABLE TX_DISABLE
+
+
+/* Define the version ID of ThreadX. This may be used by the application.*/
+
+#ifdef TX_THREAD_INIT
+CHAR _tx_version_id[] =
+ "(c) 2024 Microsoft Corp. (c) 2026-present Eclipse ThreadX contributors. * ThreadXCortex-R52/GNU Version 6.5.2.202603 *";
+#else
+extern CHAR _tx_version_id[];
+#endif
+
+
+#endif
+
diff --git a/ports/cortex_r52/gnu/readme_threadx.txt b/ports/cortex_r52/gnu/readme_threadx.txt
new file mode 100644
index 00000000..73e0dbfe
--- /dev/null
+++ b/ports/cortex_r52/gnu/readme_threadx.txt
@@ -0,0 +1,201 @@
+ Eclipse ThreadX for Cortex-R52
+ Using GNU Tools
+
+
+1. Building the ThreadX run-time Library
+
+The port is built with CMake and Ninja. From the repository root:
+
+ cmake -B build_r52 -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE=cmake/cortex_r52.cmake .
+ ninja -C build_r52
+
+This produces libthreadx.a for Cortex-R52 in AArch32 state. The toolchain
+file pins the reference cross compiler by absolute path so that the build
+does not depend on PATH ordering; override it with
+
+ -DARM_TOOLCHAIN_PATH=<directory containing arm-none-eabi-gcc>
+
+Note this is the first R-profile port in the tree with a working CMake
+build. cmake/cortex_a9.cmake exists but ports/cortex_a9/gnu/CMakeLists.txt
+is empty, so no A- or R-profile port could previously be built this way.
+
+
+2. Build options
+
+ -DTX_R52_FLOAT_ABI=soft|hard floating-point ABI, default soft
+ -DTX_R52_ENABLE_VFP=ON lazy VFP context save and restore
+ (requires TX_R52_FLOAT_ABI=hard)
+ -DTX_R52_ENABLE_FIQ=ON FIQ support
+ -DTX_R52_ENABLE_IRQ_NESTING=ON nested IRQ support
+ -DTX_R52_ENABLE_FIQ_NESTING=ON nested FIQ support (requires the above)
+ -DTX_R52_BUILD_FVP_EXAMPLE=ON build the Armv8-R AEM FVP examples
+ -DTX_R52_ENABLE_MPU=ON PMSAv8-R protection and caches
+ -DTX_R52_CONSOLE_PL011=ON console on the PL011 UART, not semihosting
+
+TX_R52_ENABLE_VFP is PUBLIC: it changes which registers the context switch
+saves, so the library and the application must agree. It requires
+TX_R52_FLOAT_ABI=hard, and configure refuses the combination rather than
+warning about it: the flag enables the VFP blocks in the port assembly, and a
+soft float ABI leaves the assembler with no FPU to accept them, so the build
+fails on the first VMRS rather than quietly skipping the VFP context path.
+
+Cortex-R52 always implements at least a single-precision FPU. GCC rejects
+"-mcpu=cortex-r52+nofp" and offers only "+nofp.dp", so the soft-float
+baseline selects the soft ABI rather than removing the FPU.
+
+
+3. Example builds and tests
+
+ cmake -B build_r52 -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE=cmake/cortex_r52.cmake \
+ -DTX_R52_BUILD_FVP_EXAMPLE=ON .
+ ninja -C build_r52 boot_check.elf demo_m2.elf demo_m3.elf \
+ demo_threadx.elf demo_mpu.elf demo_clz.elf
+ ctest --test-dir build_r52
+
+The images target the free Armv8-R AEM FVP (FVP_BaseR_AEMv8R):
+
+ boot_check.elf EL2 configuration, the drop to EL1 and the EL2 seam
+ demo_m2.elf cooperative context switching, no interrupts
+ demo_m3.elf generic timer tick, GICv3 and preemption
+ demo_threadx.elf the standard eight-thread demo plus verification
+ demo_mpu.elf PMSAv8-R protection and cache enable
+ demo_clz.elf the CLZ lowest-set-bit priority search
+ demo_m5.elf lazy VFP context save (needs TX_R52_ENABLE_VFP)
+
+Each image reports its own result and terminates the model through the
+semihosting SYS_EXIT call, so no host-side timeout is needed. The test
+runner treats a missing result line as failure, so a hang cannot pass.
+
+
+4. System Initialization
+
+The entry point is _start in the example build's entry.S. Cortex-R52
+always implements EL2 and resets into it, so the reset path configures EL2
+first, installs both the EL2 and EL1 vector tables, and only then drops to
+EL1 (Supervisor mode) to run the kernel. Define TX_R52_BOOT_AT_EL1 to skip
+the EL2 stage where an earlier boot stage or a vendor EL2 monitor has
+already dropped privilege.
+
+Work that must happen at EL2, because the registers are inaccessible or
+read-only later:
+
+ CNTFRQ the model resets it to zero and it is writable
+ only at the highest implemented exception level
+ CNTHCTL.PL1PCTEN/PL1PCEN otherwise every EL1 access to CNTPCT or CNTP_*
+ traps to EL2
+ ICC_HSRE.SRE/Enable lets EL1 reach the GICv3 CPU interface through
+ its system registers
+ HCR.HCD cleared enables HVC, which the EL2 seam depends on
+ HCPTR.TCP10/TCP11 cleared lets EL1 use the FPU
+
+_tx_initialize_low_level records the system stack and publishes the first
+free memory address. It is shorter than the A-profile equivalents because
+entry.S has already given every AArch32 mode its own stack from dedicated
+linker-script regions, so there is no stack carving and no run-time overlap
+check to perform. With TX_R52_USE_THREADX_IRQ it also calls board_init to
+bring up GICv3 and the periodic tick; that is safe this early because
+interrupts stay masked until _tx_thread_schedule enables them.
+
+
+5. Interrupt Handling
+
+The EL1 IRQ vector branches directly into _tx_thread_context_save, which
+returns to __tx_irq_processing_return. A C dispatcher acknowledges the
+interrupt, re-arms the timer, calls _tx_timer_interrupt and finishes with
+_tx_thread_context_restore. The branch must be a plain B: context save
+adjusts lr itself to locate the point of interrupt.
+
+Exceptions routed to EL2 from EL1 or EL0 all arrive at the Hyp Trap Entry,
+vector offset 0x14, not at 0x08. Offsets 0x04 to 0x10 are exceptions taken
+from Hyp mode itself, and 0x08 is specifically SVC from Hyp. The handler
+therefore decodes HSR.EC and dispatches; it services HVC (EC 0x12) and
+reports anything else. That single funnel is the seam that partitioning
+work extends.
+
+Every unhandled vector identifies itself over the console. The FVP exposes
+only an Iris server and no GDB stub, which makes a self-identifying fault
+the primary debugging tool for this port.
+
+
+6. Floating Point
+
+Floating-point context is saved lazily: only threads that call
+tx_thread_vfp_enable() pay for it. Note that function sets a per-thread
+software flag for the context switch; it does not enable the FPU. Enabling
+the hardware is the board support package's responsibility -- CPACR grants
+CP10/CP11 access and FPEXC.EN enables execution, both of which reset
+disabled. The example build does this in entry.S under __ARM_FP.
+
+The flag lives in TX_THREAD_EXTENSION_2 and is reached from assembly by the
+hard-coded offset 144. It is defined unconditionally rather than under
+TX_ENABLE_VFP_SUPPORT so that the structure layout does not change with the
+floating-point build options. tx_port_offset_check.c asserts that offset,
+and the thread stack pointer and run counter offsets, at compile time: a
+layout change becomes a build failure instead of silent corruption.
+
+
+Also worth knowing: this port replaces tx_thread.h's portable lowest-set-bit
+search with the CLZ instruction, which is what the scheduler uses to pick the
+next thread to run. Upstream gates that on __TARGET_ARCH_ARM, an Arm Compiler
+5 predefine that GCC does not define, so the optimisation had never once been
+compiled in under GCC; the guard here asks __ARM_FEATURE_CLZ instead. It is
+worth 40% of the priority search's code size (2188 to 1316 bytes in
+tx_thread_system_suspend.o) and it applies in the default 32-priority
+configuration, not only above 32. A Thumb build deliberately keeps the
+portable loop, because __ARM_FEATURE_CLZ describes the architecture rather
+than the instruction set and Thumb-1 has no CLZ. demo_clz.elf is the
+regression test, and it fails to build rather than silently testing the
+portable loop if the CLZ path is ever disabled again.
+
+
+7. Memory Protection
+
+PMSAv8-R regions are described by a table rather than a sequence of
+register writes, so that a region set can be swapped wholesale. The
+example programs three regions -- code read-only and executable, all
+writable memory non-executable, peripherals as Device -- and explicitly
+disables the regions it does not use.
+
+Two cautions for anyone reusing this code on silicon:
+
+ - The PRBAR.AP encoding is the standard Armv8-R one, as published:
+ AP[2] selects read-only and AP[1] grants EL0 access. No calibration is
+ needed, and an earlier revision of this file said otherwise -- it
+ claimed the two bits were reversed, on the strength of a real
+ measurement with a wrong cause. program_region() had been shifting
+ every PRBAR field one bit too far left, so the AP value's low bit
+ landed in the true AP[2] and writes faulted exactly when that bit was
+ set, which looks precisely like a reversed encoding. The shift is
+ fixed; see the comment on the MPU_AP_* macros in mpu.h for the full
+ calibration table and what it actually proved.
+
+ The reason it took a silicon run to notice: region coverage is enforced
+ even when permissions are not what you asked for, so an unmapped
+ address faults while a "read-only" region quietly accepts writes. A
+ configuration-only review cannot tell the two apart -- provoke a real
+ fault instead. Verified on S32Z280 silicon: a write to an RO region
+ faults and execution from an XN region takes a prefetch abort, the
+ latter never having worked under the old shift.
+
+ - The code and data regions must not share a 64-byte granule. The
+ linker script separates them with ".data ALIGN(64) :" on the output
+ section. An assignment to the location counter between sections does
+ not advance a MEMORY region's allocation pointer, and an ALIGN at the
+ end of the preceding section does not extend it when no data follows,
+ so both of those leave writable data inside the read-only region.
+
+
+8. Validation
+
+Functional validation is performed on the Armv8-R AEM FVP through the
+images listed in section 3. Structural coverage is not claimed from this
+port: the platform-agnostic regression suite runs on the host, which is
+where the coverage instrumentation and the certification evidence live.
+
+The FVP is an architecture envelope model, not a Cortex-R52
+implementation -- it reports MIDR 0x410FD0F0, part number 0xD0F. It
+validates architecture; implementation details such as MPU region count,
+TCM, lockstep and RAS must be validated on silicon. Nothing in this port
+is gated on MIDR.
diff --git a/ports/cortex_r52/gnu/src/tx_port_offset_check.c b/ports/cortex_r52/gnu/src/tx_port_offset_check.c
new file mode 100644
index 00000000..5e853fca
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_port_offset_check.c
@@ -0,0 +1,86 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** Port Specific */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+/**************************************************************************/
+/* */
+/* PORT SPECIFIC C INFORMATION RELEASE */
+/* */
+/* tx_port_offset_check.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Compile-time verification of the TX_THREAD structure offsets that */
+/* this port's assembly reaches by hard-coded displacement. The file */
+/* emits no code; it exists purely so that a layout change becomes a */
+/* build failure. */
+/* */
+/* Why this is needed: tx_thread_schedule.S, */
+/* tx_thread_system_return.S and tx_thread_context_restore.S read and */
+/* write the per-thread VFP enable flag as [thread, #144]. Nothing in */
+/* the toolchain connects that literal to the C structure, so adding a */
+/* member, enabling an option that adds one, or reordering the port */
+/* extensions would silently retarget those accesses at an unrelated */
+/* field -- corrupting thread state in a way that is extremely hard to */
+/* diagnose from the symptom. The offset was measured at 144 for the */
+/* default build and is asserted here. */
+/* */
+/* A negative array dimension is used rather than _Static_assert */
+/* because this project targets C99, where _Static_assert does not */
+/* exist. If an assertion below fails, the compiler reports a negative*/
+/* or zero-sized array for the named typedef. */
+/* */
+/**************************************************************************/
+
+#include "tx_api.h"
+#include <stddef.h>
+
+/* Offset of the VFP enable flag as encoded in this port's assembly.
+ Overridable only so that the assertion itself can be tested. */
+
+#ifndef TX_PORT_VFP_ENABLE_OFFSET
+#define TX_PORT_VFP_ENABLE_OFFSET 144
+#endif
+
+typedef char
+tx_port_assert_vfp_enable_offset_is_144[
+ (offsetof(TX_THREAD, tx_thread_vfp_enable) == TX_PORT_VFP_ENABLE_OFFSET)
+ ? 1 : -1];
+
+/* The stack pointer is read as [thread, #8] by tx_thread_schedule.S and
+ written by tx_thread_system_return.S, and the run counter as
+ [thread, #4]. Assert those too, since they are on the context-switch
+ critical path. */
+
+typedef char
+tx_port_assert_stack_ptr_offset_is_8[
+ (offsetof(TX_THREAD, tx_thread_stack_ptr) == 8) ? 1 : -1];
+
+typedef char
+tx_port_assert_run_count_offset_is_4[
+ (offsetof(TX_THREAD, tx_thread_run_count) == 4) ? 1 : -1];
diff --git a/ports/cortex_r52/gnu/src/tx_thread_context_restore.S b/ports/cortex_r52/gnu/src/tx_thread_context_restore.S
new file mode 100644
index 00000000..5c0e503d
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_context_restore.S
@@ -0,0 +1,247 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .arm
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+SVC_MODE = 0xD3 @ Disable IRQ/FIQ, SVC mode
+IRQ_MODE = 0xD2 @ Disable IRQ/FIQ, IRQ mode
+#else
+SVC_MODE = 0x93 @ Disable IRQ, SVC mode
+IRQ_MODE = 0x92 @ Disable IRQ, IRQ mode
+#endif
+@
+ .global _tx_thread_system_state
+ .global _tx_thread_current_ptr
+ .global _tx_thread_execute_ptr
+ .global _tx_timer_time_slice
+ .global _tx_thread_schedule
+ .global _tx_thread_preempt_disable
+ .global _tx_execution_isr_exit
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_context_restore
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_context_restore Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function restores the interrupt context if it is processing a */
+@/* nested interrupt. If not, it returns to the interrupt thread if no */
+@/* preemption is necessary. Otherwise, if preemption is necessary or */
+@/* if no thread was running, the function returns to the scheduler. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* _tx_thread_schedule Thread scheduling routine */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ISRs Interrupt Service Routines */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_context_restore(VOID)
+@{
+ .global _tx_thread_context_restore
+ .type _tx_thread_context_restore,function
+_tx_thread_context_restore:
+@
+@ /* Lockout interrupts. */
+@
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable IRQ and FIQ interrupts
+#else
+ CPSID i @ Disable IRQ interrupts
+#endif
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR exit function to indicate an ISR is complete. */
+@
+ BL _tx_execution_isr_exit @ Call the ISR exit function
+#endif
+@
+@ /* Determine if interrupts are nested. */
+@ if (--_tx_thread_system_state)
+@ {
+@
+ LDR r3, =_tx_thread_system_state @ Pickup address of system state variable
+ LDR r2, [r3] @ Pickup system state
+ SUB r2, r2, #1 @ Decrement the counter
+ STR r2, [r3] @ Store the counter
+ CMP r2, #0 @ Was this the first interrupt?
+ BEQ __tx_thread_not_nested_restore @ If so, not a nested restore
+@
+@ /* Interrupts are nested. */
+@
+@ /* Just recover the saved registers and return to the point of
+@ interrupt. */
+@
+ LDMIA sp!, {r0, r10, r12, lr} @ Recover SPSR, POI, and scratch regs
+ MSR SPSR_cxsf, r0 @ Put SPSR back
+ LDMIA sp!, {r0-r3} @ Recover r0-r3
+ MOVS pc, lr @ Return to point of interrupt
+@
+@ }
+__tx_thread_not_nested_restore:
+@
+@ /* Determine if a thread was interrupted and no preemption is required. */
+@ else if (((_tx_thread_current_ptr) && (_tx_thread_current_ptr == _tx_thread_execute_ptr))
+@ || (_tx_thread_preempt_disable))
+@ {
+@
+ LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr
+ LDR r0, [r1] @ Pickup actual current thread pointer
+ CMP r0, #0 @ Is it NULL?
+ BEQ __tx_thread_idle_system_restore @ Yes, idle system was interrupted
+@
+ LDR r3, =_tx_thread_preempt_disable @ Pickup preempt disable address
+ LDR r2, [r3] @ Pickup actual preempt disable flag
+ CMP r2, #0 @ Is it set?
+ BNE __tx_thread_no_preempt_restore @ Yes, don't preempt this thread
+ LDR r3, =_tx_thread_execute_ptr @ Pickup address of execute thread ptr
+ LDR r2, [r3] @ Pickup actual execute thread pointer
+ CMP r0, r2 @ Is the same thread highest priority?
+ BNE __tx_thread_preempt_restore @ No, preemption needs to happen
+@
+@
+__tx_thread_no_preempt_restore:
+@
+@ /* Restore interrupted thread or ISR. */
+@
+@ /* Pickup the saved stack pointer. */
+@ tmp_ptr = _tx_thread_current_ptr -> tx_thread_stack_ptr;
+@
+@ /* Recover the saved context and return to the point of interrupt. */
+@
+ LDMIA sp!, {r0, r10, r12, lr} @ Recover SPSR, POI, and scratch regs
+ MSR SPSR_cxsf, r0 @ Put SPSR back
+ LDMIA sp!, {r0-r3} @ Recover r0-r3
+ MOVS pc, lr @ Return to point of interrupt
+@
+@ }
+@ else
+@ {
+__tx_thread_preempt_restore:
+@
+ LDMIA sp!, {r3, r10, r12, lr} @ Recover temporarily saved registers
+ MOV r1, lr @ Save lr (point of interrupt)
+ MOV r2, #SVC_MODE @ Build SVC mode CPSR
+ MSR CPSR_c, r2 @ Enter SVC mode
+ STR r1, [sp, #-4]! @ Save point of interrupt
+ STMDB sp!, {r4-r12, lr} @ Save upper half of registers
+ MOV r4, r3 @ Save SPSR in r4
+ MOV r2, #IRQ_MODE @ Build IRQ mode CPSR
+ MSR CPSR_c, r2 @ Enter IRQ mode
+ LDMIA sp!, {r0-r3} @ Recover r0-r3
+ MOV r5, #SVC_MODE @ Build SVC mode CPSR
+ MSR CPSR_c, r5 @ Enter SVC mode
+ STMDB sp!, {r0-r3} @ Save r0-r3 on thread's stack
+@
+ LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr
+ LDR r0, [r1] @ Pickup current thread pointer
+@
+#ifdef TX_ENABLE_VFP_SUPPORT
+ LDR r2, [r0, #144] @ Pickup the VFP enabled flag
+ CMP r2, #0 @ Is the VFP enabled?
+ BEQ _tx_skip_irq_vfp_save @ No, skip VFP IRQ save
+ VMRS r2, FPSCR @ Pickup the FPSCR
+ STR r2, [sp, #-4]! @ Save FPSCR
+ VSTMDB sp!, {D0-D15} @ Save D0-D15
+_tx_skip_irq_vfp_save:
+#endif
+@
+ MOV r3, #1 @ Build interrupt stack type
+ STMDB sp!, {r3, r4} @ Save interrupt stack type and SPSR
+ STR sp, [r0, #8] @ Save stack pointer in thread control
+ @ block
+@
+@ /* Save the remaining time-slice and disable it. */
+@ if (_tx_timer_time_slice)
+@ {
+@
+ LDR r3, =_tx_timer_time_slice @ Pickup time-slice variable address
+ LDR r2, [r3] @ Pickup time-slice
+ CMP r2, #0 @ Is it active?
+ BEQ __tx_thread_dont_save_ts @ No, don't save it
+@
+@ _tx_thread_current_ptr -> tx_thread_time_slice = _tx_timer_time_slice;
+@ _tx_timer_time_slice = 0;
+@
+ STR r2, [r0, #24] @ Save thread's time-slice
+ MOV r2, #0 @ Clear value
+ STR r2, [r3] @ Disable global time-slice flag
+@
+@ }
+__tx_thread_dont_save_ts:
+@
+@
+@ /* Clear the current task pointer. */
+@ _tx_thread_current_ptr = TX_NULL;
+@
+ MOV r0, #0 @ NULL value
+ STR r0, [r1] @ Clear current thread pointer
+@
+@ /* Return to the scheduler. */
+@ _tx_thread_schedule();
+@
+ B _tx_thread_schedule @ Return to scheduler
+@ }
+@
+__tx_thread_idle_system_restore:
+@
+@ /* Just return back to the scheduler! */
+@
+ MOV r0, #SVC_MODE @ Build SVC mode CPSR
+ MSR CPSR_c, r0 @ Enter SVC mode
+ B _tx_thread_schedule @ Return to scheduler
+@}
+
+
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_context_save.S b/ports/cortex_r52/gnu/src/tx_thread_context_save.S
new file mode 100644
index 00000000..26443700
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_context_save.S
@@ -0,0 +1,193 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .global _tx_thread_system_state
+ .global _tx_thread_current_ptr
+ .global _tx_irq_processing_return
+ .global _tx_execution_isr_enter
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_context_save
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_context_save Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function saves the context of an executing thread in the */
+@/* beginning of interrupt processing. The function also ensures that */
+@/* the system stack is used upon return to the calling ISR. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ISRs */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_context_save(VOID)
+@{
+ .global _tx_thread_context_save
+ .type _tx_thread_context_save,function
+_tx_thread_context_save:
+@
+@ /* Upon entry to this routine, it is assumed that IRQ interrupts are locked
+@ out, we are in IRQ mode, and all registers are intact. */
+@
+@ /* Check for a nested interrupt condition. */
+@ if (_tx_thread_system_state++)
+@ {
+@
+ STMDB sp!, {r0-r3} @ Save some working registers
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable FIQ interrupts
+#endif
+ LDR r3, =_tx_thread_system_state @ Pickup address of system state variable
+ LDR r2, [r3] @ Pickup system state
+ CMP r2, #0 @ Is this the first interrupt?
+ BEQ __tx_thread_not_nested_save @ Yes, not a nested context save
+@
+@ /* Nested interrupt condition. */
+@
+ ADD r2, r2, #1 @ Increment the interrupt counter
+ STR r2, [r3] @ Store it back in the variable
+@
+@ /* Save the rest of the scratch registers on the stack and return to the
+@ calling ISR. */
+@
+ MRS r0, SPSR @ Pickup saved SPSR
+ SUB lr, lr, #4 @ Adjust point of interrupt
+ STMDB sp!, {r0, r10, r12, lr} @ Store other registers
+@
+@ /* Return to the ISR. */
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ B __tx_irq_processing_return @ Continue IRQ processing
+@
+__tx_thread_not_nested_save:
+@ }
+@
+@ /* Otherwise, not nested, check to see if a thread was running. */
+@ else if (_tx_thread_current_ptr)
+@ {
+@
+ ADD r2, r2, #1 @ Increment the interrupt counter
+ STR r2, [r3] @ Store it back in the variable
+ LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr
+ LDR r0, [r1] @ Pickup current thread pointer
+ CMP r0, #0 @ Is it NULL?
+ BEQ __tx_thread_idle_system_save @ If so, interrupt occurred in
+ @ scheduling loop - nothing needs saving!
+@
+@ /* Save minimal context of interrupted thread. */
+@
+ MRS r2, SPSR @ Pickup saved SPSR
+ SUB lr, lr, #4 @ Adjust point of interrupt
+ STMDB sp!, {r2, r10, r12, lr} @ Store other registers
+@
+@ /* Save the current stack pointer in the thread's control block. */
+@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp;
+@
+@ /* Switch to the system stack. */
+@ sp = _tx_thread_system_stack_ptr@
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ B __tx_irq_processing_return @ Continue IRQ processing
+@
+@ }
+@ else
+@ {
+@
+__tx_thread_idle_system_save:
+@
+@ /* Interrupt occurred in the scheduling loop. */
+@
+@ /* Not much to do here, just adjust the stack pointer, and return to IRQ
+@ processing. */
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ ADD sp, sp, #16 @ Recover saved registers
+ B __tx_irq_processing_return @ Continue IRQ processing
+@
+@ }
+@}
+
+
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_fiq_context_restore.S b/ports/cortex_r52/gnu/src/tx_thread_fiq_context_restore.S
new file mode 100644
index 00000000..f47c9662
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_fiq_context_restore.S
@@ -0,0 +1,237 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+SVC_MODE = 0xD3 @ SVC mode
+FIQ_MODE = 0xD1 @ FIQ mode
+MODE_MASK = 0x1F @ Mode mask
+THUMB_MASK = 0x20 @ Thumb bit mask
+IRQ_MODE_BITS = 0x12 @ IRQ mode bits
+@
+@
+ .global _tx_thread_system_state
+ .global _tx_thread_current_ptr
+ .global _tx_thread_system_stack_ptr
+ .global _tx_thread_execute_ptr
+ .global _tx_timer_time_slice
+ .global _tx_thread_schedule
+ .global _tx_thread_preempt_disable
+ .global _tx_execution_isr_exit
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_fiq_context_restore
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_fiq_context_restore Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function restores the fiq interrupt context when processing a */
+@/* nested interrupt. If not, it returns to the interrupt thread if no */
+@/* preemption is necessary. Otherwise, if preemption is necessary or */
+@/* if no thread was running, the function returns to the scheduler. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* _tx_thread_schedule Thread scheduling routine */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* FIQ ISR Interrupt Service Routines */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_fiq_context_restore(VOID)
+@{
+ .global _tx_thread_fiq_context_restore
+ .type _tx_thread_fiq_context_restore,function
+_tx_thread_fiq_context_restore:
+@
+@ /* Lockout interrupts. */
+@
+ CPSID if @ Disable IRQ and FIQ interrupts
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR exit function to indicate an ISR is complete. */
+@
+ BL _tx_execution_isr_exit @ Call the ISR exit function
+#endif
+@
+@ /* Determine if interrupts are nested. */
+@ if (--_tx_thread_system_state)
+@ {
+@
+ LDR r3, =_tx_thread_system_state @ Pickup address of system state variable
+ LDR r2, [r3] @ Pickup system state
+ SUB r2, r2, #1 @ Decrement the counter
+ STR r2, [r3] @ Store the counter
+ CMP r2, #0 @ Was this the first interrupt?
+ BEQ __tx_thread_fiq_not_nested_restore @ If so, not a nested restore
+@
+@ /* Interrupts are nested. */
+@
+@ /* Just recover the saved registers and return to the point of
+@ interrupt. */
+@
+ LDMIA sp!, {r0, r10, r12, lr} @ Recover SPSR, POI, and scratch regs
+ MSR SPSR_cxsf, r0 @ Put SPSR back
+ LDMIA sp!, {r0-r3} @ Recover r0-r3
+ MOVS pc, lr @ Return to point of interrupt
+@
+@ }
+__tx_thread_fiq_not_nested_restore:
+@
+@ /* Determine if a thread was interrupted and no preemption is required. */
+@ else if (((_tx_thread_current_ptr) && (_tx_thread_current_ptr == _tx_thread_execute_ptr))
+@ || (_tx_thread_preempt_disable))
+@ {
+@
+ LDR r1, [sp] @ Pickup the saved SPSR
+ MOV r2, #MODE_MASK @ Build mask to isolate the interrupted mode
+ AND r1, r1, r2 @ Isolate mode bits
+ CMP r1, #IRQ_MODE_BITS @ Was an interrupt taken in IRQ mode before we
+ @ got to context save? */
+ BEQ __tx_thread_fiq_no_preempt_restore @ Yes, just go back to point of interrupt
+
+
+ LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr
+ LDR r0, [r1] @ Pickup actual current thread pointer
+ CMP r0, #0 @ Is it NULL?
+ BEQ __tx_thread_fiq_idle_system_restore @ Yes, idle system was interrupted
+
+ LDR r3, =_tx_thread_preempt_disable @ Pickup preempt disable address
+ LDR r2, [r3] @ Pickup actual preempt disable flag
+ CMP r2, #0 @ Is it set?
+ BNE __tx_thread_fiq_no_preempt_restore @ Yes, don't preempt this thread
+ LDR r3, =_tx_thread_execute_ptr @ Pickup address of execute thread ptr
+ LDR r2, [r3] @ Pickup actual execute thread pointer
+ CMP r0, r2 @ Is the same thread highest priority?
+ BNE __tx_thread_fiq_preempt_restore @ No, preemption needs to happen
+
+
+__tx_thread_fiq_no_preempt_restore:
+@
+@ /* Restore interrupted thread or ISR. */
+@
+@ /* Pickup the saved stack pointer. */
+@ tmp_ptr = _tx_thread_current_ptr -> tx_thread_stack_ptr;
+@
+@ /* Recover the saved context and return to the point of interrupt. */
+@
+ LDMIA sp!, {r0, lr} @ Recover SPSR, POI, and scratch regs
+ MSR SPSR_cxsf, r0 @ Put SPSR back
+ LDMIA sp!, {r0-r3} @ Recover r0-r3
+ MOVS pc, lr @ Return to point of interrupt
+@
+@ }
+@ else
+@ {
+__tx_thread_fiq_preempt_restore:
+@
+ LDMIA sp!, {r3, lr} @ Recover temporarily saved registers
+ MOV r1, lr @ Save lr (point of interrupt)
+ MOV r2, #SVC_MODE @ Build SVC mode CPSR
+ MSR CPSR_c, r2 @ Enter SVC mode
+ STR r1, [sp, #-4]! @ Save point of interrupt
+ STMDB sp!, {r4-r12, lr} @ Save upper half of registers
+ MOV r4, r3 @ Save SPSR in r4
+ MOV r2, #FIQ_MODE @ Build FIQ mode CPSR
+ MSR CPSR_c, r2 @ Reenter FIQ mode
+ LDMIA sp!, {r0-r3} @ Recover r0-r3
+ MOV r5, #SVC_MODE @ Build SVC mode CPSR
+ MSR CPSR_c, r5 @ Enter SVC mode
+ STMDB sp!, {r0-r3} @ Save r0-r3 on thread's stack
+ MOV r3, #1 @ Build interrupt stack type
+ STMDB sp!, {r3, r4} @ Save interrupt stack type and SPSR
+ LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr
+ LDR r0, [r1] @ Pickup current thread pointer
+ STR sp, [r0, #8] @ Save stack pointer in thread control
+ @ block */
+@
+@ /* Save the remaining time-slice and disable it. */
+@ if (_tx_timer_time_slice)
+@ {
+@
+ LDR r3, =_tx_timer_time_slice @ Pickup time-slice variable address
+ LDR r2, [r3] @ Pickup time-slice
+ CMP r2, #0 @ Is it active?
+ BEQ __tx_thread_fiq_dont_save_ts @ No, don't save it
+@
+@ _tx_thread_current_ptr -> tx_thread_time_slice = _tx_timer_time_slice;
+@ _tx_timer_time_slice = 0;
+@
+ STR r2, [r0, #24] @ Save thread's time-slice
+ MOV r2, #0 @ Clear value
+ STR r2, [r3] @ Disable global time-slice flag
+@
+@ }
+__tx_thread_fiq_dont_save_ts:
+@
+@
+@ /* Clear the current task pointer. */
+@ _tx_thread_current_ptr = TX_NULL;
+@
+ MOV r0, #0 @ NULL value
+ STR r0, [r1] @ Clear current thread pointer
+@
+@ /* Return to the scheduler. */
+@ _tx_thread_schedule();
+@
+ B _tx_thread_schedule @ Return to scheduler
+@ }
+@
+__tx_thread_fiq_idle_system_restore:
+@
+@ /* Just return back to the scheduler! */
+@
+ ADD sp, sp, #24 @ Recover FIQ stack space
+ MOV r3, #SVC_MODE @ Build SVC mode CPSR
+ MSR CPSR_c, r3 @ Lockout interrupts
+ B _tx_thread_schedule @ Return to scheduler
+@
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_fiq_context_save.S b/ports/cortex_r52/gnu/src/tx_thread_fiq_context_save.S
new file mode 100644
index 00000000..510fe36e
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_fiq_context_save.S
@@ -0,0 +1,195 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .global _tx_thread_system_state
+ .global _tx_thread_current_ptr
+ .global __tx_fiq_processing_return
+ .global _tx_execution_isr_enter
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_fiq_context_save
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_fiq_context_save Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function saves the context of an executing thread in the */
+@/* beginning of interrupt processing. The function also ensures that */
+@/* the system stack is used upon return to the calling ISR. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ISRs */
+@/* */
+@/**************************************************************************/
+@ VOID _tx_thread_fiq_context_save(VOID)
+@{
+ .global _tx_thread_fiq_context_save
+ .type _tx_thread_fiq_context_save,function
+_tx_thread_fiq_context_save:
+@
+@ /* Upon entry to this routine, it is assumed that IRQ interrupts are locked
+@ out, we are in IRQ mode, and all registers are intact. */
+@
+@ /* Check for a nested interrupt condition. */
+@ if (_tx_thread_system_state++)
+@ {
+@
+ STMDB sp!, {r0-r3} @ Save some working registers
+ LDR r3, =_tx_thread_system_state @ Pickup address of system state variable
+ LDR r2, [r3] @ Pickup system state
+ CMP r2, #0 @ Is this the first interrupt?
+ BEQ __tx_thread_fiq_not_nested_save @ Yes, not a nested context save
+@
+@ /* Nested interrupt condition. */
+@
+ ADD r2, r2, #1 @ Increment the interrupt counter
+ STR r2, [r3] @ Store it back in the variable
+@
+@ /* Save the rest of the scratch registers on the stack and return to the
+@ calling ISR. */
+@
+ MRS r0, SPSR @ Pickup saved SPSR
+ SUB lr, lr, #4 @ Adjust point of interrupt
+ STMDB sp!, {r0, r10, r12, lr} @ Store other registers
+@
+@ /* Return to the ISR. */
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ B __tx_fiq_processing_return @ Continue FIQ processing
+@
+__tx_thread_fiq_not_nested_save:
+@ }
+@
+@ /* Otherwise, not nested, check to see if a thread was running. */
+@ else if (_tx_thread_current_ptr)
+@ {
+@
+ ADD r2, r2, #1 @ Increment the interrupt counter
+ STR r2, [r3] @ Store it back in the variable
+ LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr
+ LDR r0, [r1] @ Pickup current thread pointer
+ CMP r0, #0 @ Is it NULL?
+ BEQ __tx_thread_fiq_idle_system_save @ If so, interrupt occurred in
+@ @ scheduling loop - nothing needs saving!
+@
+@ /* Save minimal context of interrupted thread. */
+@
+ MRS r2, SPSR @ Pickup saved SPSR
+ SUB lr, lr, #4 @ Adjust point of interrupt
+ STMDB sp!, {r2, lr} @ Store other registers, Note that we don't
+@ @ need to save sl and ip since FIQ has
+@ @ copies of these registers. Nested
+@ @ interrupt processing does need to save
+@ @ these registers.
+@
+@ /* Save the current stack pointer in the thread's control block. */
+@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp;
+@
+@ /* Switch to the system stack. */
+@ sp = _tx_thread_system_stack_ptr;
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ B __tx_fiq_processing_return @ Continue FIQ processing
+@
+@ }
+@ else
+@ {
+@
+__tx_thread_fiq_idle_system_save:
+@
+@ /* Interrupt occurred in the scheduling loop. */
+@
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+@
+@ /* Not much to do here, save the current SPSR and LR for possible
+@ use in IRQ interrupted in idle system conditions, and return to
+@ FIQ interrupt processing. */
+@
+ MRS r0, SPSR @ Pickup saved SPSR
+ SUB lr, lr, #4 @ Adjust point of interrupt
+ STMDB sp!, {r0, lr} @ Store other registers that will get used
+@ @ or stripped off the stack in context
+@ @ restore
+ B __tx_fiq_processing_return @ Continue FIQ processing
+@
+@ }
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_fiq_nesting_end.S b/ports/cortex_r52/gnu/src/tx_thread_fiq_nesting_end.S
new file mode 100644
index 00000000..5b323208
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_fiq_nesting_end.S
@@ -0,0 +1,107 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+DISABLE_INTS = 0xC0 @ Disable IRQ/FIQ interrupts
+#else
+DISABLE_INTS = 0x80 @ Disable IRQ interrupts
+#endif
+MODE_MASK = 0x1F @ Mode mask
+FIQ_MODE_BITS = 0x11 @ FIQ mode bits
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_fiq_nesting_end
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_fiq_nesting_end Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function is called by the application from FIQ mode after */
+@/* _tx_thread_fiq_nesting_start has been called and switches the FIQ */
+@/* processing from system mode back to FIQ mode prior to the ISR */
+@/* calling _tx_thread_fiq_context_restore. Note that this function */
+@/* assumes the system stack pointer is in the same position after */
+@/* nesting start function was called. */
+@/* */
+@/* This function assumes that the system mode stack pointer was setup */
+@/* during low-level initialization (tx_initialize_low_level.s). */
+@/* */
+@/* This function returns with FIQ interrupts disabled. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ISRs */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_fiq_nesting_end(VOID)
+@{
+ .global _tx_thread_fiq_nesting_end
+ .type _tx_thread_fiq_nesting_end,function
+_tx_thread_fiq_nesting_end:
+ MOV r3,lr @ Save ISR return address
+ MRS r0, CPSR @ Pickup the CPSR
+ ORR r0, r0, #DISABLE_INTS @ Build disable interrupt value
+ MSR CPSR_c, r0 @ Disable interrupts
+ LDMIA sp!, {r1, lr} @ Pickup saved lr (and r1 throw-away for
+ @ 8-byte alignment logic)
+ BIC r0, r0, #MODE_MASK @ Clear mode bits
+ ORR r0, r0, #FIQ_MODE_BITS @ Build IRQ mode CPSR
+ MSR CPSR_c, r0 @ Reenter IRQ mode
+
+#ifdef __THUMB_INTERWORK
+ BX r3 @ Return to caller
+#else
+ MOV pc, r3 @ Return to caller
+#endif
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_fiq_nesting_start.S b/ports/cortex_r52/gnu/src/tx_thread_fiq_nesting_start.S
new file mode 100644
index 00000000..516eef63
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_fiq_nesting_start.S
@@ -0,0 +1,99 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+FIQ_DISABLE = 0x40 @ FIQ disable bit
+MODE_MASK = 0x1F @ Mode mask
+SYS_MODE_BITS = 0x1F @ System mode bits
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_fiq_nesting_start
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_fiq_nesting_start Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function is called by the application from FIQ mode after */
+@/* _tx_thread_fiq_context_save has been called and switches the FIQ */
+@/* processing to the system mode so nested FIQ interrupt processing */
+@/* is possible (system mode has its own "lr" register). Note that */
+@/* this function assumes that the system mode stack pointer was setup */
+@/* during low-level initialization (tx_initialize_low_level.s). */
+@/* */
+@/* This function returns with FIQ interrupts enabled. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ISRs */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_fiq_nesting_start(VOID)
+@{
+ .global _tx_thread_fiq_nesting_start
+ .type _tx_thread_fiq_nesting_start,function
+_tx_thread_fiq_nesting_start:
+ MOV r3,lr @ Save ISR return address
+ MRS r0, CPSR @ Pickup the CPSR
+ BIC r0, r0, #MODE_MASK @ Clear the mode bits
+ ORR r0, r0, #SYS_MODE_BITS @ Build system mode CPSR
+ MSR CPSR_c, r0 @ Enter system mode
+ STMDB sp!, {r1, lr} @ Push the system mode lr on the system mode stack
+ @ and push r1 just to keep 8-byte alignment
+ BIC r0, r0, #FIQ_DISABLE @ Build enable FIQ CPSR
+ MSR CPSR_c, r0 @ Enter system mode
+#ifdef __THUMB_INTERWORK
+ BX r3 @ Return to caller
+#else
+ MOV pc, r3 @ Return to caller
+#endif
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_interrupt_control.S b/ports/cortex_r52/gnu/src/tx_thread_interrupt_control.S
new file mode 100644
index 00000000..3bc0e54a
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_interrupt_control.S
@@ -0,0 +1,108 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+
+INT_MASK = 0x03F
+
+@
+@/* Define the 16-bit Thumb mode veneer for _tx_thread_interrupt_control for
+@ applications calling this function from to 16-bit Thumb mode. */
+@
+ .text
+ .align 2
+ .global $_tx_thread_interrupt_control
+$_tx_thread_interrupt_control:
+ .thumb
+ BX pc @ Switch to 32-bit mode
+ NOP @
+ .arm
+ STMFD sp!, {lr} @ Save return address
+ BL _tx_thread_interrupt_control @ Call _tx_thread_interrupt_control function
+ LDMFD sp!, {lr} @ Recover saved return address
+ BX lr @ Return to 16-bit caller
+@
+@
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_interrupt_control Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function is responsible for changing the interrupt lockout */
+@/* posture of the system. */
+@/* */
+@/* INPUT */
+@/* */
+@/* new_posture New interrupt lockout posture */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* old_posture Old interrupt lockout posture */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* Application Code */
+@/* */
+@/**************************************************************************/
+@UINT _tx_thread_interrupt_control(UINT new_posture)
+@{
+ .global _tx_thread_interrupt_control
+ .type _tx_thread_interrupt_control,function
+_tx_thread_interrupt_control:
+@
+@ /* Pickup current interrupt lockout posture. */
+@
+ MRS r3, CPSR @ Pickup current CPSR
+ MOV r2, #INT_MASK @ Build interrupt mask
+ AND r1, r3, r2 @ Clear interrupt lockout bits
+ ORR r1, r1, r0 @ Or-in new interrupt lockout bits
+@
+@ /* Apply the new interrupt posture. */
+@
+ MSR CPSR_c, r1 @ Setup new CPSR
+ BIC r0, r3, r2 @ Return previous interrupt mask
+#ifdef __THUMB_INTERWORK
+ BX lr @ Return to caller
+#else
+ MOV pc, lr @ Return to caller
+#endif
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_interrupt_disable.S b/ports/cortex_r52/gnu/src/tx_thread_interrupt_disable.S
new file mode 100644
index 00000000..9b3f215a
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_interrupt_disable.S
@@ -0,0 +1,105 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+@/* Define the 16-bit Thumb mode veneer for _tx_thread_interrupt_disable for
+@ applications calling this function from to 16-bit Thumb mode. */
+@
+ .text
+ .align 2
+ .global $_tx_thread_interrupt_disable
+$_tx_thread_interrupt_disable:
+ .thumb
+ BX pc @ Switch to 32-bit mode
+ NOP @
+ .arm
+ STMFD sp!, {lr} @ Save return address
+ BL _tx_thread_interrupt_disable @ Call _tx_thread_interrupt_disable function
+ LDMFD sp!, {lr} @ Recover saved return address
+ BX lr @ Return to 16-bit caller
+@
+@
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_interrupt_disable Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function is responsible for disabling interrupts */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* old_posture Old interrupt lockout posture */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* Application Code */
+@/* */
+@/**************************************************************************/
+@UINT _tx_thread_interrupt_disable(void)
+@{
+ .global _tx_thread_interrupt_disable
+ .type _tx_thread_interrupt_disable,function
+_tx_thread_interrupt_disable:
+@
+@ /* Pickup current interrupt lockout posture. */
+@
+ MRS r0, CPSR @ Pickup current CPSR
+@
+@ /* Mask interrupts. */
+@
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable IRQ and FIQ
+#else
+ CPSID i @ Disable IRQ
+#endif
+
+#ifdef __THUMB_INTERWORK
+ BX lr @ Return to caller
+#else
+ MOV pc, lr @ Return to caller
+#endif
+@}
+
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_interrupt_restore.S b/ports/cortex_r52/gnu/src/tx_thread_interrupt_restore.S
new file mode 100644
index 00000000..b014afd9
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_interrupt_restore.S
@@ -0,0 +1,96 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+@/* Define the 16-bit Thumb mode veneer for _tx_thread_interrupt_restore for
+@ applications calling this function from to 16-bit Thumb mode. */
+@
+ .text
+ .align 2
+ .global $_tx_thread_interrupt_restore
+$_tx_thread_interrupt_restore:
+ .thumb
+ BX pc @ Switch to 32-bit mode
+ NOP @
+ .arm
+ STMFD sp!, {lr} @ Save return address
+ BL _tx_thread_interrupt_restore @ Call _tx_thread_interrupt_restore function
+ LDMFD sp!, {lr} @ Recover saved return address
+ BX lr @ Return to 16-bit caller
+@
+@
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_interrupt_restore Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function is responsible for restoring interrupts to the state */
+@/* returned by a previous _tx_thread_interrupt_disable call. */
+@/* */
+@/* INPUT */
+@/* */
+@/* old_posture Old interrupt lockout posture */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* Application Code */
+@/* */
+@/**************************************************************************/
+@UINT _tx_thread_interrupt_restore(UINT old_posture)
+@{
+ .global _tx_thread_interrupt_restore
+ .type _tx_thread_interrupt_restore,function
+_tx_thread_interrupt_restore:
+@
+@ /* Apply the new interrupt posture. */
+@
+ MSR CPSR_c, r0 @ Setup new CPSR
+#ifdef __THUMB_INTERWORK
+ BX lr @ Return to caller
+#else
+ MOV pc, lr @ Return to caller
+#endif
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_irq_nesting_end.S b/ports/cortex_r52/gnu/src/tx_thread_irq_nesting_end.S
new file mode 100644
index 00000000..f14ce5cf
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_irq_nesting_end.S
@@ -0,0 +1,106 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+DISABLE_INTS = 0xC0 @ Disable IRQ/FIQ interrupts
+#else
+DISABLE_INTS = 0x80 @ Disable IRQ interrupts
+#endif
+MODE_MASK = 0x1F @ Mode mask
+IRQ_MODE_BITS = 0x12 @ IRQ mode bits
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_irq_nesting_end
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_irq_nesting_end Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function is called by the application from IRQ mode after */
+@/* _tx_thread_irq_nesting_start has been called and switches the IRQ */
+@/* processing from system mode back to IRQ mode prior to the ISR */
+@/* calling _tx_thread_context_restore. Note that this function */
+@/* assumes the system stack pointer is in the same position after */
+@/* nesting start function was called. */
+@/* */
+@/* This function assumes that the system mode stack pointer was setup */
+@/* during low-level initialization (tx_initialize_low_level.s). */
+@/* */
+@/* This function returns with IRQ interrupts disabled. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ISRs */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_irq_nesting_end(VOID)
+@{
+ .global _tx_thread_irq_nesting_end
+ .type _tx_thread_irq_nesting_end,function
+_tx_thread_irq_nesting_end:
+ MOV r3,lr @ Save ISR return address
+ MRS r0, CPSR @ Pickup the CPSR
+ ORR r0, r0, #DISABLE_INTS @ Build disable interrupt value
+ MSR CPSR_c, r0 @ Disable interrupts
+ LDMIA sp!, {r1, lr} @ Pickup saved lr (and r1 throw-away for
+ @ 8-byte alignment logic)
+ BIC r0, r0, #MODE_MASK @ Clear mode bits
+ ORR r0, r0, #IRQ_MODE_BITS @ Build IRQ mode CPSR
+ MSR CPSR_c, r0 @ Reenter IRQ mode
+#ifdef __THUMB_INTERWORK
+ BX r3 @ Return to caller
+#else
+ MOV pc, r3 @ Return to caller
+#endif
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_irq_nesting_start.S b/ports/cortex_r52/gnu/src/tx_thread_irq_nesting_start.S
new file mode 100644
index 00000000..e300a5be
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_irq_nesting_start.S
@@ -0,0 +1,99 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+IRQ_DISABLE = 0x80 @ IRQ disable bit
+MODE_MASK = 0x1F @ Mode mask
+SYS_MODE_BITS = 0x1F @ System mode bits
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_irq_nesting_start
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_irq_nesting_start Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function is called by the application from IRQ mode after */
+@/* _tx_thread_context_save has been called and switches the IRQ */
+@/* processing to the system mode so nested IRQ interrupt processing */
+@/* is possible (system mode has its own "lr" register). Note that */
+@/* this function assumes that the system mode stack pointer was setup */
+@/* during low-level initialization (tx_initialize_low_level.s). */
+@/* */
+@/* This function returns with IRQ interrupts enabled. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ISRs */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_irq_nesting_start(VOID)
+@{
+ .global _tx_thread_irq_nesting_start
+ .type _tx_thread_irq_nesting_start,function
+_tx_thread_irq_nesting_start:
+ MOV r3,lr @ Save ISR return address
+ MRS r0, CPSR @ Pickup the CPSR
+ BIC r0, r0, #MODE_MASK @ Clear the mode bits
+ ORR r0, r0, #SYS_MODE_BITS @ Build system mode CPSR
+ MSR CPSR_c, r0 @ Enter system mode
+ STMDB sp!, {r1, lr} @ Push the system mode lr on the system mode stack
+ @ and push r1 just to keep 8-byte alignment
+ BIC r0, r0, #IRQ_DISABLE @ Build enable IRQ CPSR
+ MSR CPSR_c, r0 @ Enter system mode
+#ifdef __THUMB_INTERWORK
+ BX r3 @ Return to caller
+#else
+ MOV pc, r3 @ Return to caller
+#endif
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_schedule.S b/ports/cortex_r52/gnu/src/tx_thread_schedule.S
new file mode 100644
index 00000000..2dfc1da7
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_schedule.S
@@ -0,0 +1,240 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .global _tx_thread_execute_ptr
+ .global _tx_thread_current_ptr
+ .global _tx_timer_time_slice
+ .global _tx_execution_thread_enter
+@
+@
+@/* Define the 16-bit Thumb mode veneer for _tx_thread_schedule for
+@ applications calling this function from to 16-bit Thumb mode. */
+@
+ .text
+ .align 2
+ .global $_tx_thread_schedule
+ .type $_tx_thread_schedule,function
+$_tx_thread_schedule:
+ .thumb
+ BX pc @ Switch to 32-bit mode
+ NOP @
+ .arm
+ STMFD sp!, {lr} @ Save return address
+ BL _tx_thread_schedule @ Call _tx_thread_schedule function
+ LDMFD sp!, {lr} @ Recover saved return address
+ BX lr @ Return to 16-bit caller
+@
+@
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_schedule Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function waits for a thread control block pointer to appear in */
+@/* the _tx_thread_execute_ptr variable. Once a thread pointer appears */
+@/* in the variable, the corresponding thread is resumed. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* _tx_initialize_kernel_enter ThreadX entry function */
+@/* _tx_thread_system_return Return to system from thread */
+@/* _tx_thread_context_restore Restore thread's context */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_schedule(VOID)
+@{
+ .global _tx_thread_schedule
+ .type _tx_thread_schedule,function
+_tx_thread_schedule:
+@
+@ /* Enable interrupts. */
+@
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSIE if @ Enable IRQ and FIQ interrupts
+#else
+ CPSIE i @ Enable IRQ interrupts
+#endif
+@
+@ /* Wait for a thread to execute. */
+@ do
+@ {
+ LDR r1, =_tx_thread_execute_ptr @ Address of thread execute ptr
+@
+__tx_thread_schedule_loop:
+@
+ LDR r0, [r1] @ Pickup next thread to execute
+ CMP r0, #0 @ Is it NULL?
+ BEQ __tx_thread_schedule_loop @ If so, keep looking for a thread
+@
+@ }
+@ while(_tx_thread_execute_ptr == TX_NULL);
+@
+@ /* Yes! We have a thread to execute. Lockout interrupts and
+@ transfer control to it. */
+@
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable IRQ and FIQ interrupts
+#else
+ CPSID i @ Disable IRQ interrupts
+#endif
+@
+@ /* Setup the current thread pointer. */
+@ _tx_thread_current_ptr = _tx_thread_execute_ptr;
+@
+ LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread
+ STR r0, [r1] @ Setup current thread pointer
+@
+@ /* Increment the run count for this thread. */
+@ _tx_thread_current_ptr -> tx_thread_run_count++;
+@
+ LDR r2, [r0, #4] @ Pickup run counter
+ LDR r3, [r0, #24] @ Pickup time-slice for this thread
+ ADD r2, r2, #1 @ Increment thread run-counter
+ STR r2, [r0, #4] @ Store the new run counter
+@
+@ /* Setup time-slice, if present. */
+@ _tx_timer_time_slice = _tx_thread_current_ptr -> tx_thread_time_slice;
+@
+ LDR r2, =_tx_timer_time_slice @ Pickup address of time-slice
+ @ variable
+ LDR sp, [r0, #8] @ Switch stack pointers
+ STR r3, [r2] @ Setup time-slice
+@
+@ /* Switch to the thread's stack. */
+@ sp = _tx_thread_execute_ptr -> tx_thread_stack_ptr;
+@
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the thread entry function to indicate the thread is executing. */
+@
+ BL _tx_execution_thread_enter @ Call the thread execution enter function
+#endif
+@
+@ /* Determine if an interrupt frame or a synchronous task suspension frame
+@ is present. */
+@
+ LDMIA sp!, {r4, r5} @ Pickup the stack type and saved CPSR
+ CMP r4, #0 @ Check for synchronous context switch
+ BEQ _tx_solicited_return
+ MSR SPSR_cxsf, r5 @ Setup SPSR for return
+#ifdef TX_ENABLE_VFP_SUPPORT
+ LDR r1, [r0, #144] @ Pickup the VFP enabled flag
+ CMP r1, #0 @ Is the VFP enabled?
+ BEQ _tx_skip_interrupt_vfp_restore @ No, skip VFP interrupt restore
+ VLDMIA sp!, {D0-D15} @ Recover D0-D15
+ LDR r4, [sp], #4 @ Pickup FPSCR
+ VMSR FPSCR, r4 @ Restore FPSCR
+_tx_skip_interrupt_vfp_restore:
+#endif
+ LDMIA sp!, {r0-r12, lr, pc}^ @ Return to point of thread interrupt
+@
+_tx_solicited_return:
+#ifdef TX_ENABLE_VFP_SUPPORT
+ LDR r1, [r0, #144] @ Pickup the VFP enabled flag
+ CMP r1, #0 @ Is the VFP enabled?
+ BEQ _tx_skip_solicited_vfp_restore @ No, skip VFP solicited restore
+ VLDMIA sp!, {D8-D15} @ Recover D8-D15
+ LDR r4, [sp], #4 @ Pickup FPSCR
+ VMSR FPSCR, r4 @ Restore FPSCR
+_tx_skip_solicited_vfp_restore:
+#endif
+ MOV r0, r5 @ Move CPSR to scratch register
+ LDMIA sp!, {r4-r11, lr} @ Return to thread synchronously
+ MSR CPSR_cxsf, r0 @ Recover CPSR
+@
+#ifdef __THUMB_INTERWORK
+ BX lr @ Return to caller
+#else
+ MOV pc, lr @ Return to caller
+#endif
+@
+@}
+@
+@
+#ifdef TX_ENABLE_VFP_SUPPORT
+ .global tx_thread_vfp_enable
+ .type tx_thread_vfp_enable,function
+tx_thread_vfp_enable:
+ MRS r2, CPSR @ Pickup the CPSR
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable IRQ and FIQ interrupts
+#else
+ CPSID i @ Disable IRQ interrupts
+#endif
+ LDR r0, =_tx_thread_current_ptr @ Build current thread pointer address
+ LDR r1, [r0] @ Pickup current thread pointer
+ CMP r1, #0 @ Check for NULL thread pointer
+ BEQ __tx_no_thread_to_enable @ If NULL, skip VFP enable
+ MOV r0, #1 @ Build enable value
+ STR r0, [r1, #144] @ Set the VFP enable flag (tx_thread_vfp_enable field in TX_THREAD)
+__tx_no_thread_to_enable:
+ MSR CPSR_cxsf, r2 @ Recover CPSR
+ BX LR @ Return to caller
+@
+ .global tx_thread_vfp_disable
+ .type tx_thread_vfp_disable,function
+tx_thread_vfp_disable:
+ MRS r2, CPSR @ Pickup the CPSR
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable IRQ and FIQ interrupts
+#else
+ CPSID i @ Disable IRQ interrupts
+#endif
+ LDR r0, =_tx_thread_current_ptr @ Build current thread pointer address
+ LDR r1, [r0] @ Pickup current thread pointer
+ CMP r1, #0 @ Check for NULL thread pointer
+ BEQ __tx_no_thread_to_disable @ If NULL, skip VFP disable
+ MOV r0, #0 @ Build disable value
+ STR r0, [r1, #144] @ Clear the VFP enable flag (tx_thread_vfp_enable field in TX_THREAD)
+__tx_no_thread_to_disable:
+ MSR CPSR_cxsf, r2 @ Recover CPSR
+ BX LR @ Return to caller
+#endif
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_stack_build.S b/ports/cortex_r52/gnu/src/tx_thread_stack_build.S
new file mode 100644
index 00000000..1be3b5f3
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_stack_build.S
@@ -0,0 +1,170 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .arm
+
+SVC_MODE = 0x13 @ SVC mode
+#ifdef TX_ENABLE_FIQ_SUPPORT
+CPSR_MASK = 0xDF @ Mask initial CPSR, IRQ & FIQ interrupts enabled
+#else
+CPSR_MASK = 0x9F @ Mask initial CPSR, IRQ interrupts enabled
+#endif
+@
+@
+@/* Define the 16-bit Thumb mode veneer for _tx_thread_stack_build for
+@ applications calling this function from to 16-bit Thumb mode. */
+@
+ .text
+ .align 2
+ .thumb
+ .global $_tx_thread_stack_build
+ .type $_tx_thread_stack_build,function
+$_tx_thread_stack_build:
+ BX pc @ Switch to 32-bit mode
+ NOP @
+ .arm
+ STMFD sp!, {lr} @ Save return address
+ BL _tx_thread_stack_build @ Call _tx_thread_stack_build function
+ LDMFD sp!, {lr} @ Recover saved return address
+ BX lr @ Return to 16-bit caller
+@
+@
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_stack_build Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function builds a stack frame on the supplied thread's stack. */
+@/* The stack frame results in a fake interrupt return to the supplied */
+@/* function pointer. */
+@/* */
+@/* INPUT */
+@/* */
+@/* thread_ptr Pointer to thread control blk */
+@/* function_ptr Pointer to return function */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* _tx_thread_create Create thread service */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_stack_build(TX_THREAD *thread_ptr, VOID (*function_ptr)(VOID))
+@{
+ .global _tx_thread_stack_build
+ .type _tx_thread_stack_build,function
+_tx_thread_stack_build:
+@
+@
+@ /* Build a fake interrupt frame. The form of the fake interrupt stack
+@ on the ARM9 should look like the following after it is built:
+@
+@ Stack Top: 1 Interrupt stack frame type
+@ CPSR Initial value for CPSR
+@ a1 (r0) Initial value for a1
+@ a2 (r1) Initial value for a2
+@ a3 (r2) Initial value for a3
+@ a4 (r3) Initial value for a4
+@ v1 (r4) Initial value for v1
+@ v2 (r5) Initial value for v2
+@ v3 (r6) Initial value for v3
+@ v4 (r7) Initial value for v4
+@ v5 (r8) Initial value for v5
+@ sb (r9) Initial value for sb
+@ sl (r10) Initial value for sl
+@ fp (r11) Initial value for fp
+@ ip (r12) Initial value for ip
+@ lr (r14) Initial value for lr
+@ pc (r15) Initial value for pc
+@ 0 For stack backtracing
+@
+@ Stack Bottom: (higher memory address) */
+@
+ LDR r2, [r0, #16] @ Pickup end of stack area
+ BIC r2, r2, #7 @ Ensure 8-byte alignment
+ SUB r2, r2, #76 @ Allocate space for the stack frame
+@
+@ /* Actually build the stack frame. */
+@
+ MOV r3, #1 @ Build interrupt stack type
+ STR r3, [r2, #0] @ Store stack type
+ MOV r3, #0 @ Build initial register value
+ STR r3, [r2, #8] @ Store initial r0
+ STR r3, [r2, #12] @ Store initial r1
+ STR r3, [r2, #16] @ Store initial r2
+ STR r3, [r2, #20] @ Store initial r3
+ STR r3, [r2, #24] @ Store initial r4
+ STR r3, [r2, #28] @ Store initial r5
+ STR r3, [r2, #32] @ Store initial r6
+ STR r3, [r2, #36] @ Store initial r7
+ STR r3, [r2, #40] @ Store initial r8
+ STR r3, [r2, #44] @ Store initial r9
+ LDR r3, [r0, #12] @ Pickup stack starting address
+ STR r3, [r2, #48] @ Store initial r10 (sl)
+ LDR r3,=_tx_thread_schedule @ Pickup address of _tx_thread_schedule for GDB backtrace
+ STR r3, [r2, #60] @ Store initial r14 (lr)
+ MOV r3, #0 @ Build initial register value
+ STR r3, [r2, #52] @ Store initial r11
+ STR r3, [r2, #56] @ Store initial r12
+ STR r1, [r2, #64] @ Store initial pc
+ STR r3, [r2, #68] @ 0 for back-trace
+ MRS r1, CPSR @ Pickup CPSR
+ BIC r1, r1, #CPSR_MASK @ Mask mode bits of CPSR
+ ORR r3, r1, #SVC_MODE @ Build CPSR, SVC mode, interrupts enabled
+ STR r3, [r2, #4] @ Store initial CPSR
+@
+@ /* Setup stack pointer. */
+@ thread_ptr -> tx_thread_stack_ptr = r2;
+@
+ STR r2, [r0, #8] @ Save stack pointer in thread's
+ @ control block
+#ifdef __THUMB_INTERWORK
+ BX lr @ Return to caller
+#else
+ MOV pc, lr @ Return to caller
+#endif
+@}
+
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_system_return.S b/ports/cortex_r52/gnu/src/tx_thread_system_return.S
new file mode 100644
index 00000000..ae0eac7d
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_system_return.S
@@ -0,0 +1,169 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .arm
+@
+@
+ .global _tx_thread_current_ptr
+ .global _tx_timer_time_slice
+ .global _tx_thread_schedule
+ .global _tx_execution_thread_exit
+@
+@
+@
+@/* Define the 16-bit Thumb mode veneer for _tx_thread_system_return for
+@ applications calling this function from to 16-bit Thumb mode. */
+@
+ .text
+ .align 2
+ .global $_tx_thread_system_return
+ .type $_tx_thread_system_return,function
+$_tx_thread_system_return:
+ .thumb
+ BX pc @ Switch to 32-bit mode
+ NOP @
+ .arm
+ STMFD sp!, {lr} @ Save return address
+ BL _tx_thread_system_return @ Call _tx_thread_system_return function
+ LDMFD sp!, {lr} @ Recover saved return address
+ BX lr @ Return to 16-bit caller
+@
+@
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_system_return Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function is target processor specific. It is used to transfer */
+@/* control from a thread back to the ThreadX system. Only a */
+@/* minimal context is saved since the compiler assumes temp registers */
+@/* are going to get slicked by a function call anyway. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* _tx_thread_schedule Thread scheduling loop */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ThreadX components */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_system_return(VOID)
+@{
+ .global _tx_thread_system_return
+ .type _tx_thread_system_return,function
+_tx_thread_system_return:
+@
+@ /* Lockout interrupts. */
+@
+ MRS r1, CPSR @ Pickup the CPSR
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable IRQ and FIQ interrupts
+#else
+ CPSID i @ Disable IRQ interrupts
+#endif
+@ /* Save minimal context on the stack. */
+@
+ STMDB sp!, {r4-r11, lr} @ Save minimal context
+ LDR r5, =_tx_thread_current_ptr @ Pickup address of current ptr
+ LDR r6, [r5, #0] @ Pickup current thread pointer
+@
+#ifdef TX_ENABLE_VFP_SUPPORT
+ LDR r0, [r6, #144] @ Pickup the VFP enabled flag
+ CMP r0, #0 @ Is the VFP enabled?
+ BEQ _tx_skip_solicited_vfp_save @ No, skip VFP solicited save
+ VMRS r4, FPSCR @ Pickup the FPSCR
+ STR r4, [sp, #-4]! @ Save FPSCR
+ VSTMDB sp!, {D8-D15} @ Save D8-D15
+_tx_skip_solicited_vfp_save:
+#endif
+@
+ MOV r0, #0 @ Build a solicited stack type
+ STMDB sp!, {r0-r1} @ Save type and CPSR
+@
+@
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the thread exit function to indicate the thread is no longer executing. */
+@
+ BL _tx_execution_thread_exit @ Call the thread exit function
+#endif
+@
+ LDR r2, =_tx_timer_time_slice @ Pickup address of time slice
+ LDR r1, [r2, #0] @ Pickup current time slice
+@
+@ /* Save current stack and switch to system stack. */
+@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp;
+@ sp = _tx_thread_system_stack_ptr;
+@
+ STR sp, [r6, #8] @ Save thread stack pointer
+@
+@ /* Determine if the time-slice is active. */
+@ if (_tx_timer_time_slice)
+@ {
+@
+ MOV r4, #0 @ Build clear value
+ CMP r1, #0 @ Is a time-slice active?
+ BEQ __tx_thread_dont_save_ts @ No, don't save the time-slice
+@
+@ /* Save time-slice for the thread and clear the current time-slice. */
+@ _tx_thread_current_ptr -> tx_thread_time_slice = _tx_timer_time_slice;
+@ _tx_timer_time_slice = 0;
+@
+ STR r4, [r2, #0] @ Clear time-slice
+ STR r1, [r6, #24] @ Save current time-slice
+@
+@ }
+__tx_thread_dont_save_ts:
+@
+@ /* Clear the current thread pointer. */
+@ _tx_thread_current_ptr = TX_NULL;
+@
+ STR r4, [r5, #0] @ Clear current thread pointer
+ B _tx_thread_schedule @ Jump to scheduler!
+@
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_thread_vectored_context_save.S b/ports/cortex_r52/gnu/src/tx_thread_vectored_context_save.S
new file mode 100644
index 00000000..60eeca7b
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_thread_vectored_context_save.S
@@ -0,0 +1,181 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .global _tx_thread_system_state
+ .global _tx_thread_current_ptr
+ .global _tx_execution_isr_enter
+@
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_vectored_context_save
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_vectored_context_save Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function saves the context of an executing thread in the */
+@/* beginning of interrupt processing. The function also ensures that */
+@/* the system stack is used upon return to the calling ISR. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ISRs */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_vectored_context_save(VOID)
+@{
+ .global _tx_thread_vectored_context_save
+ .type _tx_thread_vectored_context_save,function
+_tx_thread_vectored_context_save:
+@
+@ /* Upon entry to this routine, it is assumed that IRQ interrupts are locked
+@ out, we are in IRQ mode, and all registers are intact. */
+@
+@ /* Check for a nested interrupt condition. */
+@ if (_tx_thread_system_state++)
+@ {
+@
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable IRQ and FIQ interrupts
+#endif
+ LDR r3, =_tx_thread_system_state @ Pickup address of system state variable
+ LDR r2, [r3, #0] @ Pickup system state
+ CMP r2, #0 @ Is this the first interrupt?
+ BEQ __tx_thread_not_nested_save @ Yes, not a nested context save
+@
+@ /* Nested interrupt condition. */
+@
+ ADD r2, r2, #1 @ Increment the interrupt counter
+ STR r2, [r3, #0] @ Store it back in the variable
+@
+@ /* Note: Minimal context of interrupted thread is already saved. */
+@
+@ /* Return to the ISR. */
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ MOV pc, lr @ Return to caller
+@
+__tx_thread_not_nested_save:
+@ }
+@
+@ /* Otherwise, not nested, check to see if a thread was running. */
+@ else if (_tx_thread_current_ptr)
+@ {
+@
+ ADD r2, r2, #1 @ Increment the interrupt counter
+ STR r2, [r3, #0] @ Store it back in the variable
+ LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr
+ LDR r0, [r1, #0] @ Pickup current thread pointer
+ CMP r0, #0 @ Is it NULL?
+ BEQ __tx_thread_idle_system_save @ If so, interrupt occurred in
+ @ scheduling loop - nothing needs saving!
+@
+@ /* Note: Minimal context of interrupted thread is already saved. */
+@
+@ /* Save the current stack pointer in the thread's control block. */
+@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp;
+@
+@ /* Switch to the system stack. */
+@ sp = _tx_thread_system_stack_ptr;
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ MOV pc, lr @ Return to caller
+@
+@ }
+@ else
+@ {
+@
+__tx_thread_idle_system_save:
+@
+@ /* Interrupt occurred in the scheduling loop. */
+@
+@ /* Not much to do here, just adjust the stack pointer, and return to IRQ
+@ processing. */
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ ADD sp, sp, #32 @ Recover saved registers
+ MOV pc, lr @ Return to caller
+@
+@ }
+@}
+
diff --git a/ports/cortex_r52/gnu/src/tx_timer_interrupt.S b/ports/cortex_r52/gnu/src/tx_timer_interrupt.S
new file mode 100644
index 00000000..e70a4d02
--- /dev/null
+++ b/ports/cortex_r52/gnu/src/tx_timer_interrupt.S
@@ -0,0 +1,270 @@
+@/***************************************************************************
+@ * 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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Timer */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .arm
+
+@
+@/* Define Assembly language external references... */
+@
+ .global _tx_timer_time_slice
+ .global _tx_timer_system_clock
+ .global _tx_timer_current_ptr
+ .global _tx_timer_list_start
+ .global _tx_timer_list_end
+ .global _tx_timer_expired_time_slice
+ .global _tx_timer_expired
+ .global _tx_thread_time_slice
+@
+@
+@
+@/* Define the 16-bit Thumb mode veneer for _tx_timer_interrupt for
+@ applications calling this function from to 16-bit Thumb mode. */
+@
+ .text
+ .align 2
+ .thumb
+ .global $_tx_timer_interrupt
+ .type $_tx_timer_interrupt,function
+$_tx_timer_interrupt:
+ BX pc @ Switch to 32-bit mode
+ NOP @
+ .arm
+ STMFD sp!, {lr} @ Save return address
+ BL _tx_timer_interrupt @ Call _tx_timer_interrupt function
+ LDMFD sp!, {lr} @ Recover saved return address
+ BX lr @ Return to 16-bit caller
+@
+@
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_timer_interrupt Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function processes the hardware timer interrupt. This */
+@/* processing includes incrementing the system clock and checking for */
+@/* time slice and/or timer expiration. If either is found, the */
+@/* interrupt context save/restore functions are called along with the */
+@/* expiration functions. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* _tx_thread_time_slice Time slice interrupted thread */
+@/* _tx_timer_expiration_process Timer expiration processing */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* interrupt vector */
+@/* */
+@/**************************************************************************/
+@VOID _tx_timer_interrupt(VOID)
+@{
+ .global _tx_timer_interrupt
+ .type _tx_timer_interrupt,function
+_tx_timer_interrupt:
+@
+@ /* Upon entry to this routine, it is assumed that context save has already
+@ been called, and therefore the compiler scratch registers are available
+@ for use. */
+@
+@ /* Increment the system clock. */
+@ _tx_timer_system_clock++;
+@
+ LDR r1, =_tx_timer_system_clock @ Pickup address of system clock
+ LDR r0, [r1] @ Pickup system clock
+ ADD r0, r0, #1 @ Increment system clock
+ STR r0, [r1] @ Store new system clock
+@
+@ /* Test for time-slice expiration. */
+@ if (_tx_timer_time_slice)
+@ {
+@
+ LDR r3, =_tx_timer_time_slice @ Pickup address of time-slice
+ LDR r2, [r3] @ Pickup time-slice
+ CMP r2, #0 @ Is it non-active?
+ BEQ __tx_timer_no_time_slice @ Yes, skip time-slice processing
+@
+@ /* Decrement the time_slice. */
+@ _tx_timer_time_slice--;
+@
+ SUB r2, r2, #1 @ Decrement the time-slice
+ STR r2, [r3] @ Store new time-slice value
+@
+@ /* Check for expiration. */
+@ if (__tx_timer_time_slice == 0)
+@
+ CMP r2, #0 @ Has it expired?
+ BNE __tx_timer_no_time_slice @ No, skip expiration processing
+@
+@ /* Set the time-slice expired flag. */
+@ _tx_timer_expired_time_slice = TX_TRUE;
+@
+ LDR r3, =_tx_timer_expired_time_slice @ Pickup address of expired flag
+ MOV r0, #1 @ Build expired value
+ STR r0, [r3] @ Set time-slice expiration flag
+@
+@ }
+@
+__tx_timer_no_time_slice:
+@
+@ /* Test for timer expiration. */
+@ if (*_tx_timer_current_ptr)
+@ {
+@
+ LDR r1, =_tx_timer_current_ptr @ Pickup current timer pointer address
+ LDR r0, [r1] @ Pickup current timer
+ LDR r2, [r0] @ Pickup timer list entry
+ CMP r2, #0 @ Is there anything in the list?
+ BEQ __tx_timer_no_timer @ No, just increment the timer
+@
+@ /* Set expiration flag. */
+@ _tx_timer_expired = TX_TRUE;
+@
+ LDR r3, =_tx_timer_expired @ Pickup expiration flag address
+ MOV r2, #1 @ Build expired value
+ STR r2, [r3] @ Set expired flag
+ B __tx_timer_done @ Finished timer processing
+@
+@ }
+@ else
+@ {
+__tx_timer_no_timer:
+@
+@ /* No timer expired, increment the timer pointer. */
+@ _tx_timer_current_ptr++;
+@
+ ADD r0, r0, #4 @ Move to next timer
+@
+@ /* Check for wraparound. */
+@ if (_tx_timer_current_ptr == _tx_timer_list_end)
+@
+ LDR r3, =_tx_timer_list_end @ Pickup address of timer list end
+ LDR r2, [r3] @ Pickup list end
+ CMP r0, r2 @ Are we at list end?
+ BNE __tx_timer_skip_wrap @ No, skip wraparound logic
+@
+@ /* Wrap to beginning of list. */
+@ _tx_timer_current_ptr = _tx_timer_list_start;
+@
+ LDR r3, =_tx_timer_list_start @ Pickup address of timer list start
+ LDR r0, [r3] @ Set current pointer to list start
+@
+__tx_timer_skip_wrap:
+@
+ STR r0, [r1] @ Store new current timer pointer
+@ }
+@
+__tx_timer_done:
+@
+@
+@ /* See if anything has expired. */
+@ if ((_tx_timer_expired_time_slice) || (_tx_timer_expired))
+@ {
+@
+ LDR r3, =_tx_timer_expired_time_slice @ Pickup address of expired flag
+ LDR r2, [r3] @ Pickup time-slice expired flag
+ CMP r2, #0 @ Did a time-slice expire?
+ BNE __tx_something_expired @ If non-zero, time-slice expired
+ LDR r1, =_tx_timer_expired @ Pickup address of other expired flag
+ LDR r0, [r1] @ Pickup timer expired flag
+ CMP r0, #0 @ Did a timer expire?
+ BEQ __tx_timer_nothing_expired @ No, nothing expired
+@
+__tx_something_expired:
+@
+@
+ STMDB sp!, {r0, lr} @ Save the lr register on the stack
+ @ and save r0 just to keep 8-byte alignment
+@
+@ /* Did a timer expire? */
+@ if (_tx_timer_expired)
+@ {
+@
+ LDR r1, =_tx_timer_expired @ Pickup address of expired flag
+ LDR r0, [r1] @ Pickup timer expired flag
+ CMP r0, #0 @ Check for timer expiration
+ BEQ __tx_timer_dont_activate @ If not set, skip timer activation
+@
+@ /* Process timer expiration. */
+@ _tx_timer_expiration_process();
+@
+ BL _tx_timer_expiration_process @ Call the timer expiration handling routine
+@
+@ }
+__tx_timer_dont_activate:
+@
+@ /* Did time slice expire? */
+@ if (_tx_timer_expired_time_slice)
+@ {
+@
+ LDR r3, =_tx_timer_expired_time_slice @ Pickup address of time-slice expired
+ LDR r2, [r3] @ Pickup the actual flag
+ CMP r2, #0 @ See if the flag is set
+ BEQ __tx_timer_not_ts_expiration @ No, skip time-slice processing
+@
+@ /* Time slice interrupted thread. */
+@ _tx_thread_time_slice();
+@
+ BL _tx_thread_time_slice @ Call time-slice processing
+@
+@ }
+@
+__tx_timer_not_ts_expiration:
+@
+ LDMIA sp!, {r0, lr} @ Recover lr register (r0 is just there for
+ @ the 8-byte stack alignment
+@
+@ }
+@
+__tx_timer_nothing_expired:
+@
+#ifdef __THUMB_INTERWORK
+ BX lr @ Return to caller
+#else
+ MOV pc, lr @ Return to caller
+#endif
+@
+@}
+