diff --git a/.gitignore b/.gitignore index cadb24438..1c68bd826 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,8 @@ build_r52_vfp/ build_r52_uart/ build_r52_mpu/ build_r52_all/ +build_mod/ +build_fvp/ # Keil uVision per-user state, regenerated when a project is opened. # The .uvprojx / .uvproj project files are the ones worth tracking. 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 index c8339fa70..b4eabeb56 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt @@ -131,6 +131,38 @@ target_link_options(demo_threadx.elf PRIVATE ${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. @@ -264,7 +296,8 @@ if(TX_R52_ENABLE_FIQ_NESTING) 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) +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 @@ -303,6 +336,285 @@ if(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 + $ + ${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 image. + # ------------------------------------------------------------------------ + + add_executable(fvp_module.elf EXCLUDE_FROM_ALL + # 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 application + ${MOD_FVP}/sample_threadx_module_manager.c + + # The portable module manager + ${TXM_MANAGER_SRC} + ) + + target_include_directories(fvp_module.elf 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 this image, 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(fvp_module.elf PRIVATE + TXM_MODULE_MANAGER + TX_R52_USE_THREADX_IRQ + TX_R52_ENABLE_MPU + ) + + target_compile_options(fvp_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_FVP}/module_blob.S PROPERTIES + OBJECT_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/demo_module.bin + COMPILE_OPTIONS "-Wa,-I${CMAKE_CURRENT_BINARY_DIR}" + ) + + add_dependencies(fvp_module.elf fvp_demo_module_bin) + + target_link_options(fvp_module.elf PRIVATE + -T${MOD_FVP}/link_module.lds + -nostartfiles + -Wl,-Map=fvp_module.map + ${R52_LINK_QUIET_RWX} + ) + + set_target_properties(fvp_module.elf PROPERTIES + LINK_DEPENDS ${MOD_FVP}/link_module.lds) + + target_link_libraries(fvp_module.elf PRIVATE threadx_module) + +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 @@ -334,6 +646,12 @@ if(FVP_BASER_AEMV8R) "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...") + 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 @@ -344,6 +662,15 @@ if(FVP_BASER_AEMV8R) 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) + endif() + enable_testing() foreach(image IN LISTS R52_TEST_IMAGES) add_test(NAME r52-fvp-${image} 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 000000000..6632096ad --- /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 000000000..0ed8dcfc0 --- /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/demo_clz.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_clz.c new file mode 100644 index 000000000..a156f4ec7 --- /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/entry.S b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S index d59183f8f..77d813361 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S @@ -101,6 +101,20 @@ .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. */ @@ -144,7 +158,11 @@ el2_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 */ @@ -476,6 +494,26 @@ el1_trap_svc: FAULT_REPORT msg_el1_svc 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 @@ -532,6 +570,19 @@ mpu_try_execute_land: 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 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 index 29f9fe25f..07c9ff907 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.c @@ -294,6 +294,80 @@ static void program_region(unsigned int index, const MPU_REGION *region_ptr) } +#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 two enabled regions over one address are CONSTRAINED */ +/* UNPREDICTABLE, 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 */ /**************************************************************************/ @@ -311,6 +385,20 @@ unsigned int mpu_init(void) 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. */ @@ -332,6 +420,20 @@ unsigned int mpu_init(void) 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 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 index aaca31761..d52a03a6e 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/mpu.h @@ -116,4 +116,54 @@ const MPU_REGION *mpu_region_table(unsigned int *count_ptr); 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 index 913befdb8..151f732d4 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/platform.h @@ -71,6 +71,59 @@ #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. */ diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt index 7f216700c..ae0a8b73f 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt @@ -244,3 +244,278 @@ if(TX_R52_ENABLE_FIQ_NESTING) ${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 + $ + ${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/entry.S b/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S index 6813251ae..83818d4d5 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/entry.S @@ -136,6 +136,12 @@ cntfrq_at_el2: .word 0 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 @@ -184,7 +190,11 @@ el2_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 */ @@ -794,6 +804,26 @@ fault_el1_svc: 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, @@ -835,6 +865,26 @@ fault_el1_pabt_fatal: 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 diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds b/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds index cb73bb35a..796cb88d7 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds @@ -54,7 +54,10 @@ MEMORY 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 = 0x00080000 /* 512 KB: DRAM0 + DRAM1, both full core speed */ + 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) diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c index 2073d0db5..2b9e08d56 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c @@ -302,8 +302,12 @@ static void build_table(void) 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_SIZE - 1UL; + + 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; @@ -453,6 +457,72 @@ static void program_region(unsigned int index, const MPU_REGION *region_ptr) } +/**************************************************************************/ +/* 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 overlapping regions are CONSTRAINED UNPREDICTABLE. */ +/* 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 */ /**************************************************************************/ @@ -499,6 +569,16 @@ unsigned int mpu_init(void) 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(); diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h index a9911e92e..19d183cce 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.h @@ -115,4 +115,28 @@ const MPU_REGION *mpu_region_table(unsigned int *count_ptr); 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 index 4adbe55ec..1440fbd10 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h @@ -130,6 +130,61 @@ #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 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 index 9042aeb98..ebb2b736e 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c @@ -66,16 +66,52 @@ static unsigned int max_cycles; /* CP15 accessors. */ /**************************************************************************/ +/* Direct per-region access rather than PRSELR. + + The Cortex-R52 TRM 3.3.85 and 3.3.86 provide direct access to PRBAR0 through + PRBAR15 and PRLAR0 through PRLAR15, encoded CRn c6, CRm c8 + n/2, with opc2 0 + and 1 for an even region and 4 and 5 for an odd one. PRBAR and PRLAR without + a number are the indirect view selected by PRSELR. + + Region 8 is therefore 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. + + Only regions 0 through 15 can be reached this way. Above that, PRSELR is the + only option. */ + +/* Kept for regions above 15, which have no direct encoding. Unused 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"); @@ -154,9 +190,6 @@ void thread_mpu_activate(TX_THREAD *thread_ptr) } } - write_prselr(THREAD_MPU_REGION); - __asm__ volatile("isb" ::: "memory"); - if (base == 0UL) { /* No window for this thread: disable the region rather than leaving the @@ -164,16 +197,16 @@ void thread_mpu_activate(TX_THREAD *thread_ptr) protection silently becomes no protection -- the last thread to run would leave its window open to whatever ran next. */ - write_prlar(0UL); + write_prlar8_direct(0UL); } else { - write_prbar((base & ~0x3FUL) + write_prbar8_direct((base & ~0x3FUL) | ((unsigned long) MPU_SH_NON << 3) | ((unsigned long) MPU_AP_RW_EL1 << 1) | 1UL); /* XN: data only */ - write_prlar(((base + S32Z_THREAD_WINDOW_SIZE - 1UL) & ~0x3FUL) + write_prlar8_direct(((base + S32Z_THREAD_WINDOW_SIZE - 1UL) & ~0x3FUL) | ((unsigned long) MPU_ATTR_NORMAL_WB << 1) | 1UL); /* EN */ } diff --git a/ports/cortex_r52/gnu/inc/tx_port.h b/ports/cortex_r52/gnu/inc/tx_port.h index 7feb2300a..173e485d7 100644 --- a/ports/cortex_r52/gnu/inc/tx_port.h +++ b/ports/cortex_r52/gnu/inc/tx_port.h @@ -255,17 +255,72 @@ typedef unsigned short USHORT; #define TX_TIMER_DELETE_EXTENSION(timer_ptr) -/* Determine if the ARM architecture has the CLZ instruction. This is available on - architectures v5 and above. If available, redefine the macro for calculating the - lowest bit set. */ - -#if __TARGET_ARCH_ARM > 4 +/* 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 & ((ULONG) (-((LONG) m))); \ - asm volatile (" CLZ %0,%1 ": "=r" (b) : "r" (m) ); \ - b = 31 - b; +#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 diff --git a/ports/cortex_r52/gnu/readme_threadx.txt b/ports/cortex_r52/gnu/readme_threadx.txt index ccc2c8d60..c3b026d4a 100644 --- a/ports/cortex_r52/gnu/readme_threadx.txt +++ b/ports/cortex_r52/gnu/readme_threadx.txt @@ -48,7 +48,7 @@ baseline selects the soft ABI rather than removing the FPU. -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_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): @@ -58,6 +58,7 @@ The images target the free Armv8-R AEM FVP (FVP_BaseR_AEMv8R): 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 @@ -132,6 +133,20 @@ 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 @@ -142,13 +157,24 @@ disables the regions it does not use. Two cautions for anyone reusing this code on silicon: - - The PRBAR.AP encoding used here was calibrated against the hardware. - The low bit is read-only and the high bit grants EL0 access, which is - the reverse of the widely-published Armv8-R AArch64 macro set. Getting - it wrong produces regions that report as read-only and accept writes, - because region coverage is still enforced: an unmapped address faults - while a "read-only" region does not. Re-calibrate before trusting - these values on a different implementation. + - 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 diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_demo_module.lds b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_demo_module.lds new file mode 100644 index 000000000..dbe1e069f --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_demo_module.lds @@ -0,0 +1,208 @@ +/*************************************************************************** + * 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 demonstration module alone, for the Armv8-R AEM FVP. + * + * Nothing in this file is board specific: the module is position independent + * and the two segment origins below are fictions. It is a separate copy from + * the S32Z280 one all the same, because the two examples are deliberately + * independent -- silicon bring-up must not be able to perturb the FVP + * regression, and vice versa -- and because a module example is free to change + * its own layout. The two are byte-identical apart from this paragraph today, + * so `diff` is the right way to see whether they have drifted. + * + * The module is a separate link unit from the manager, and it has to be. Both + * sides define the ThreadX API: the kernel defines the real _txe_* entry points + * and the module library defines shims of the same names that trap into the + * kernel instead. Linking them together does not produce a duplicate-symbol + * error, because the kernel arrives as a static library and the module library + * as ordinary objects, and an object always beats an archive member. The module + * shims therefore win for the whole image, and the manager's own calls to + * tx_thread_create and friends are quietly redirected into the module. + * + * That was not a theory. On the S32Z280 it put _txe_thread_create inside the + * module area, which no kernel MPU region covers -- so tx_application_define + * called into unmapped memory and the core took a prefetch abort with nothing on + * the console to say so. + * + * POSITION INDEPENDENT, and the two addresses below are fictions + * ============================================================== + * + * The module is built -fpic -msingle-pic-base, so every reference it makes to + * its own data goes through the global offset table with r9 as the base. It + * therefore does not run at the addresses in this file and is not meant to: the + * two segment origins are nominal, chosen only so that the loader can tell a + * code address from a data address by comparing against __data_segment_start__. + * What actually happens at run time is that _gcc_setup rewrites every GOT entry + * from these nominal addresses to the addresses the manager decided on. + * + * That is why they are far apart and why neither is zero. Far apart, because + * the whole discrimination is "below the data origin means code"; non-zero, + * because _gcc_setup treats a zero GOT entry as one the linker never filled in + * and skips it, so a real address of zero would be silently dropped. + * + * 0x01000000 code preamble, text, rodata, and the load images of the + * GOT and .data -- this is the blob the manager embeds + * 0x02000000 data the GOT the module runs against, .data and .bss, all + * of which live in memory the manager allocates + * + * The load images matter. A module's .data cannot be used where it was linked, + * because the manager never copies it there: _txm_module_manager_internal_load + * allocates the module's data area from its byte pool and TX_MEMSETs it to zero, + * and that is the only memory the module is given a region for. The module's + * own .data, wherever it was linked, is outside every region the module owns. + * On silicon that faulted at the first write to an initialised variable, with + * DFAR pointing into the gap between the granted code and the granted data. + * The same arrangement fails the same way on the model. + * + * So .data and .got have their VMAs in the data segment, where the module will + * run, and their LMAs in the code segment, inside the blob, where _gcc_setup can + * find them and copy them out. AT>CODE is what says that. + */ + +MEMORY +{ + /* Nominal. See the note above: the module runs where the manager puts it, + not here. Sized generously because nothing is reserved by being large -- + the raw binary is only as long as the sections actually emitted. */ + + CODE (rx) : ORIGIN = 0x01000000, LENGTH = 0x00100000 + DATA (rw) : ORIGIN = 0x02000000, LENGTH = 0x00100000 +} + +__code_segment_start__ = 0x01000000; +__data_segment_start__ = 0x02000000; + +SECTIONS +{ + /* --------------------------------------------------------------------- + The code segment, which is the blob byte for byte. + --------------------------------------------------------------------- */ + + .module_image : ALIGN(64) + { + __module_image_start__ = .; + + /* The preamble is first because that is where the manager looks for it: + it reads the properties, the entry points and the two sizes from the + first words of the image. KEEP because nothing references it. */ + + KEEP(*(.txm_module_preamble)) + + *(.text .text.*) + *(.glue_7) + *(.glue_7t) + *(.rodata .rodata.*) + + . = ALIGN(4); + } >CODE AT>CODE + + /* --------------------------------------------------------------------- + The data segment. VMAs here, load images back in the code segment. + --------------------------------------------------------------------- */ + + /* The GOT, and it must be first in the data segment. r9 is the GOT base as + far as the compiler is concerned -- every R_ARM_GOT32 is an offset from + it -- and the manager sets r9 to txm_module_instance_module_data_base_address, + which is the start of the module's data area. So GOT origin and data + origin have to be the same address, or every offset the compiler emitted + is measured from the wrong place. __data_segment_start__ above is that + address on the nominal side; r9 is that address on the real side, and + _gcc_setup's rebase is the difference between the two. */ + + .got : ALIGN(4) + { + __new_got_start__ = .; + *(.got.plt) + *(.igot.plt) + *(.got) + __new_got_end__ = .; + } >DATA AT>CODE + + __got_load_start__ = LOADADDR(.got); + + .data : ALIGN(4) + { + __data_start__ = .; + *(.data .data.*) + *(.gnu.linkonce.d.*) + . = ALIGN(4); + __data_end__ = .; + } >DATA AT>CODE + + __data_load_start__ = LOADADDR(.data); + + /* NOLOAD, so .bss contributes nothing to the blob. The manager has already + zeroed the whole data allocation by the time the module runs -- but + _gcc_setup zeroes .bss anyway, because "the manager happens to memset it" + is a property of one loader and not something a module may rely on. */ + + .bss (NOLOAD) : ALIGN(4) + { + __bss_start__ = .; + *(.bss .bss.*) + *(.gnu.linkonce.b.*) + *(COMMON) + . = ALIGN(64); + __bss_end__ = .; + } >DATA + + /* --------------------------------------------------------------------- + The two sizes the preamble declares. + --------------------------------------------------------------------- */ + + /* Code covers everything in the blob, the load images of the GOT and .data + included, because the module has to be able to read them to copy them out + and the code region is the only mapping it has over the blob. Ending it + at .rodata instead would put the GOT template outside every region the + module owns and _gcc_setup would fault on its first read. */ + + __txm_module_code_end__ = __data_load_start__ + SIZEOF(.data); + __txm_module_code_size = __txm_module_code_end__ - __code_segment_start__; + + /* Data covers the GOT, .data and .bss. Not the thread stacks: the manager + adds the start/stop and callback stack sizes to this figure itself, from + the two stack-size words further down the preamble. Counting them here + as well would double them. */ + + __txm_module_data_size = __bss_end__ - __data_segment_start__; + + /* Unwind tables and toolchain notes would otherwise land at address zero and + be carried into the raw binary. A module has no unwinder. + + .rel.dyn and friends are discarded because this is a static link: ld + resolves every R_ARM_GOT32 itself and writes the finished address into the + GOT, so there is nothing left for a dynamic loader to do. If one of these + ever stops being empty, the assumption above has broken and the module + needs relocation processing rather than a rebase -- so they are listed + explicitly, to be found by whoever goes looking. */ + + /DISCARD/ : + { + *(.ARM.exidx*) + *(.ARM.extab*) + *(.comment) + *(.note.*) + *(.dynsym) + *(.dynstr) + *(.hash) + *(.gnu.hash) + *(.dynamic) + *(.interp) + *(.rel.dyn) + *(.rel.plt) + *(.plt) + } +} diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds new file mode 100644 index 000000000..11b6a778b --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds @@ -0,0 +1,294 @@ +/*************************************************************************** + * 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 module manager 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 upper + * half holds peripherals and is not executable by default. Code therefore has + * to be linked into low DRAM -- linking at 0x80000000 produces a silent fault + * loop with no output. + * + * This is the AR1 link.lds with three differences, and each of them is a + * requirement of the module port rather than a preference. + * + * 1. DRAM stops below a MODULE region. A module's code and data are handed + * to it as MPU regions, and PMSAv8-R has no region priority: two enabled + * regions covering one address are CONSTRAINED UNPREDICTABLE. So module + * memory has to be memory no kernel region covers, and the only way to + * guarantee that is to end the kernel's data region where the module area + * begins. __data_end__ below is ORIGIN(MODULE) for exactly that reason, + * not the "1 MB of heap past the image" the AR1 script uses. + * + * 2. .txm_user_entry sits between the code region and the data region, in a + * gap belonging to neither. It holds the module's gateway into the + * kernel, and a module is given a region covering exactly it -- which + * would overlap the kernel's code region if it were inside .text. + * + * 3. The MODULE region is subdivided into four fixed pieces. See each. + * + * The two addresses here are also in platform.h as FVP_MODULE_AREA_BASE and + * FVP_MODULE_AREA_SIZE, because mpu.c programs the manager's load window over + * the same range. A linker script cannot include a C header, so the ASSERT at + * the bottom of this file pins the two spellings together at link time. + */ + +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 = 0x003F0000 /* ~4 MB less the module area */ + MODULE (rwx) : ORIGIN = 0x003F0000, LENGTH = 0x00010000 /* 64 KB */ +} + +SECTIONS +{ + /* PMSAv8-R regions have a 64-byte granule and their limits are inclusive, + so every boundary the MPU has to describe is aligned to it. */ + + . = ALIGN(64); + __code_start__ = .; + + .text : + { + KEEP(*(.vectors_el2)) + KEEP(*(.vectors_el1)) + *(.text*) + *(.glue_7) + *(.glue_7t) + } > DRAM + + .rodata : + { + . = ALIGN(4); + *(.rodata*) + . = ALIGN(4); + } > DRAM + + /* The module's gateway into the kernel. + * + * __code_end__ is taken at the START of this section, so the kernel's code + * region stops just below it: the region limit is __code_end__ - 1, masked + * down to the granule by PRLAR, and PRLAR's limit is inclusive with the low + * six bits implied as ones -- so the region ends at __code_end__ - 1 exactly + * and this section is outside it. A module gets its own region covering + * exactly this function, and that region would otherwise overlap the + * kernel's. + * + * Alone in its own section on purpose: anything sharing it would become + * executable by every module. ALIGN(64) after the colon aligns the SECTION, + * which is what is wanted. Written as ".txm_user_entry ALIGN(64) :" the + * expression would be the section's ADDRESS instead, and while that form + * happens to work in a region based at zero, it silently overrides "> DRAM" + * and is the wrong habit to leave in a file that will be copied. + */ + + .txm_user_entry : ALIGN(64) + { + __code_end__ = .; + __txm_user_entry_start__ = .; + KEEP(*(.txm_user_entry)) + . = ALIGN(64); + __txm_user_entry_end__ = .; + } > DRAM + + .data : ALIGN(64) + { + __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 = .); + + /* ------------------------------------------------------------------ + The module area. Four pieces, none of which may overlap another: + each becomes an enabled MPU region while a module runs. + ------------------------------------------------------------------ */ + + /* The shared granules the module reports its progress through, and the one + it is not allowed to reach. + + The first granule exists because the FVP has no debugger seam. On + silicon a GDB harness reads the module's own progress variable out of the + data area the manager allocated for it; here nothing outside the image + can read anything, and the manager deliberately knows no symbol of the + module. So the manager grants the module a shared MPU region over this + word and reads it back afterwards, which is what lets the test judge what + the module managed to do rather than only that it faulted. + + Six granules rather than one, because a module may be granted five shared + regions and granting a single one exercises only the first entry of the + five. Five of these are granted, one granule per entry, and the sixth -- + FVP_MODULE_STATUS_UNGRANTED, index 2, so that a granted granule sits on + either side of it -- is never granted to anything. The module writes + every granted granule, reads them all back, and then writes the gap, + which must fault. A limit masked the wrong way or a base off by a + granule leaks into that gap from one side or the other. + + First in the module area, so the base address is ORIGIN(MODULE) and the + module can carry it as a constant. A shared region is a region like any + other and cannot overlap the image below it, hence whole granules and the + alignment at the end. */ + + .module_status (NOLOAD) : + { + __module_status_start__ = .; + . = . + 0x180; /* six 64-byte granules */ + . = ALIGN(64); + __module_status_end__ = .; + } > MODULE + + /* The module image. + + The module is not linked here. It is built as its own image by the + fvp_demo_module.elf target, objcopied to a raw binary and included as + bytes by module_blob.S; this section only decides where those bytes land. + Keeping the module out of the link is not tidiness: the module library + defines shims named after the same ThreadX entry points the kernel + defines, and in one link the shims win, because they arrive as objects and + the kernel arrives as a static library. The manager's own service calls + would then be redirected into the module. */ + + .module_image : + { + . = ALIGN(64); + __module_image_start__ = .; + KEEP(*(.module_blob)) + . = ALIGN(64); + __module_image_end__ = .; + } > MODULE + + /* The pool the manager allocates module data from, in the module area for + the same reason the image is: whatever the manager hands to a module + becomes an MPU region, and that region may not overlap a kernel one. + + NOLOAD because nothing is copied in -- the byte pool writes its own + headers when it is created. */ + + .module_pool (NOLOAD) : + { + . = ALIGN(64); + __module_pool_start__ = .; + *(.module_pool) + . = ALIGN(64); + __module_pool_end__ = .; + } > MODULE + + /* A second place to load the same module from, which is how this example + proves the module is relocatable rather than merely built to be. + + A module that runs correctly at the address it was linked for proves + nothing: the GOT rebase would produce the addresses it started with, and a + rebase that did nothing would look identical. So the manager copies the + blob here and loads it a second time. + + NOLOAD, and deliberately not filled by the linker: if the blob were placed + here too, a failure to copy would be invisible because the right bytes + would already be present. */ + + .module_stage (NOLOAD) : + { + . = ALIGN(64); + __module_stage_start__ = .; + . = . + 0x1000; + . = ALIGN(64); + __module_stage_end__ = .; + } > MODULE + + /* The writable area the MPU describes: data, bss, the stacks and everything + tx_application_define carves out above _end, ending where the module area + begins. Not "_end plus a heap": the kernel's data region has to stop + below the module area or it overlaps every region a module is given. */ + + __data_end__ = ORIGIN(MODULE); + + /* The image must not have grown into the module area. Without this the + overlap appears as a module whose data the kernel also owns, which + PMSAv8-R reports as an abort from an address that looks perfectly legal. */ + + ASSERT(_end <= ORIGIN(MODULE), + "the manager image has grown into the module area; move FVP_MODULE_AREA_BASE up") + + /* The module carries the status address as a constant and platform.h names + it as FVP_MODULE_STATUS_BASE. Neither can see this script, so the + agreement is pinned here. The manager checks the same thing again at run + time, against the symbol, so a build that skipped this file still says so. */ + + ASSERT(__module_status_start__ == 0x003F0000, + "the module status word is not at FVP_MODULE_STATUS_BASE") + + /* And its size, for the same reason: the manager grants one region per + granule from this base and the module addresses them by index, so an area + that is not exactly FVP_MODULE_STATUS_GRANULES granules long would leave + a grant, or a module write, outside it. */ + + ASSERT(__module_status_end__ - __module_status_start__ == 0x180, + "the module status area is not FVP_MODULE_STATUS_GRANULES granules long") +} diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/module_blob.S b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/module_blob.S new file mode 100644 index 000000000..b3c2d44df --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/module_blob.S @@ -0,0 +1,69 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* module_blob.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* Carries the demonstration module into the manager image as data. */ +@/* */ +@/* The module is built as its own link unit and objcopied to a raw */ +@/* binary, which is included here verbatim. It has to be a separate */ +@/* link: the module library defines shims named after the ThreadX API */ +@/* entry points the kernel also defines, and in one link the shims win */ +@/* -- objects beat archive members -- so the manager's own service calls */ +@/* end up trapping into the module. */ +@/* */ +@/* Included as bytes rather than linked as objects, so the module's */ +@/* symbols never enter the manager's link at all. Nothing here is */ +@/* called: the manager finds the preamble at the start of the image and */ +@/* reaches everything else through that. */ +@/* */ +@/* A separate copy from the S32Z280 one, and identical to it: the two */ +@/* module examples are deliberately independent builds, and each names */ +@/* its own raw image through its own assembler include path. */ +@/* */ +@/**************************************************************************/ + + .section .module_blob, "a" + +@ 64-byte aligned, the PMSAv8-R granule, so the image starts where a region +@ can start. The linker script places this section at the module area base, +@ which is the address the module itself was linked for. + + .align 6 + + .global __demo_module_image + .global __demo_module_image_end + +__demo_module_image: + +@ demo_module.bin is produced by the fvp_demo_module.elf target. The +@ assembler finds it through an include path pointing at the build directory, +@ set in CMakeLists.txt -- .incbin searches the -I paths, not the source tree. + + .incbin "demo_module.bin" + +__demo_module_image_end: + + .align 6 diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c new file mode 100644 index 000000000..e2fd2508f --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c @@ -0,0 +1,319 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* SAMPLE MODULE RELEASE */ +/* */ +/* sample_threadx_module.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* A module that exercises the protection boundary rather than */ +/* demonstrating features, for the Armv8-R AEM FVP. */ +/* */ +/* The S32Z280 copy of this file is the same module for the same port; */ +/* what differs is the two addresses at the bottom and the board named */ +/* here, so `diff` is the tool for telling whether the two have drifted. */ +/* */ +/* Four steps: */ +/* */ +/* 1. Writes and reads its own data, which must succeed. */ +/* 2. Makes a kernel call, which must succeed -- proving a module in */ +/* User mode can reach the kernel through the supervisor call */ +/* boundary and come back. */ +/* 3. Violates its protection in one of three ways the manager */ +/* selects, which must fault. */ +/* 4. Never reaches step 4, because step 3 terminates it. */ +/* */ +/* THE THREE VIOLATIONS. Two of them are the two aborts the hardware */ +/* distinguishes: reading the kernel's data is a DATA abort reported */ +/* through DFSR and DFAR, branching out of the code region is a */ +/* PREFETCH abort reported through IFSR and IFAR. The third writes a */ +/* granule of the SHARED area that the manager deliberately did not */ +/* grant, after writing and reading back every granule it did -- so it */ +/* is the shared-region machinery under test rather than the kernel's */ +/* own memory, and a grant that covered one granule too many is what it */ +/* is looking for. */ +/* */ +/* Steps 1 and 2 passing without step 3 faulting would mean the module */ +/* is running unprotected, which is the failure this example exists to */ +/* detect. A module that only ever touched its own memory would pass */ +/* identically with the MPU switched off. */ +/* */ +/* HOW PROGRESS GETS OUT. A module cannot print: the console belongs */ +/* to the board support package, outside every region a module owns, so */ +/* reaching it would fault as surely as step 3 does. So progress is */ +/* recorded twice -- in the module's own data, and in the first granule */ +/* of the shared area the manager granted it. */ +/* */ +/* Which of the two can be read depends on the board. On silicon a GDB */ +/* harness reads the module's own copy out of the data area the manager */ +/* allocated for it; the FVP has no such seam -- it exposes an Iris */ +/* server and no GDB stub -- so there only the shared copy is readable */ +/* and everything the run reports has to be reported by the image */ +/* itself. Both writes are kept on both boards deliberately: if the */ +/* shared write were the only one, a module that could not reach its own */ +/* data would still report progress. */ +/* */ +/* The shared address is a literal on this side. The module has no */ +/* loader to tell it anything and the manager deliberately knows no */ +/* symbol of the module, so the two agree by convention -- and the */ +/* manager checks that they do, against the linker's own symbol, rather */ +/* than trusting them to. */ +/* */ +/**************************************************************************/ + +#include "txm_module.h" + +/* Progress, recorded in two places. See the header: the shared copy is what + the manager reads, and the module's own copy is what proves it could write + its own data at all. */ + +#define MODULE_PROGRESS_OWN_DATA 0x00000001UL +#define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL +#define MODULE_PROGRESS_ATTEMPTED_STEAL 0x00000004UL +#define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL +#define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL +#define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL +#define MODULE_PROGRESS_SHARED_WROTE 0x00000040UL +#define MODULE_PROGRESS_ATTEMPTED_GAP 0x00000080UL +#define MODULE_PROGRESS_SURVIVED_GAP 0x00000100UL + +/* Which violation to commit, taken from the low byte of the module ID the + manager passes to the start thread. The rest of that word is left alone: the + preamble ships it as 0x52520001, so a manager that sets nothing still gets a + working data-abort test rather than a module that does nothing. */ + +#define MODULE_TEST_MASK 0x000000FFUL +#define MODULE_TEST_DATA_ABORT 0x00000001UL +#define MODULE_TEST_PREFETCH_ABORT 0x00000002UL +#define MODULE_TEST_SHARED_ABORT 0x00000003UL + +/* The shared status word, at the base of the module area. + + A literal address rather than anything reached through the GOT, because it is + not the module's own memory: it is memory the manager granted, and the module + has no channel through which to be told where it is. The same number is + FVP_MODULE_STATUS_BASE in platform.h and ORIGIN(MODULE) in link_module.lds; + the manager checks all three agree before it starts this module, so a + disagreement is a reported failure rather than a fault at the first write. + + MISRA C:2012 Rule 11.6 (conversion between an integer and a pointer to void) + is deliberately violated: an address agreed between two separately linked + images can only be written as a literal, and there is no conforming way to + express it. */ + +#define MODULE_STATUS_ADDRESS 0x003F0000UL + +#define MODULE_STATUS (*((volatile ULONG *) MODULE_STATUS_ADDRESS)) + +/* And the rest of the shared area, which exists to exercise the shared-region + machinery rather than to report anything. + + The manager may grant a module TXM_MODULE_MPU_SHARED_ENTRIES regions and one + grant only ever proves the first entry works, so the area holds one granule + per entry -- five granted, and a sixth that the manager never grants. The + ungranted one is index 2, which puts a granted granule on either side of it: + a limit register masked the wrong way, or a base off by a granule, leaks into + that gap from below or from above, and either way a module that can write it + was given more than was asked for. + + The mark for each granule goes at WORD 1, because word 0 of granule 0 is the + progress word above. Uniform across all six so the address the gap write + faults on is one expression on both sides of the agreement. */ + +#define MODULE_STATUS_GRANULE 0x40UL +#define MODULE_STATUS_GRANULES 6UL +#define MODULE_STATUS_UNGRANTED 2UL +#define MODULE_STATUS_MARK_OFFSET 4UL + +#define MODULE_SHARED_SIGNATURE 0x5A5A0000UL + +#define MODULE_SHARED_MARK(index) \ + (*((volatile ULONG *) (MODULE_STATUS_ADDRESS \ + + ((index) * MODULE_STATUS_GRANULE) \ + + MODULE_STATUS_MARK_OFFSET))) + +volatile ULONG module_progress; +volatile ULONG module_scratch[16]; + +/* An address the module has no business touching, used by step 3. + + The last protection granule below the module area, which on this map is + inside the manager's data region: that region is EL1 read/write and + execute-never, so a User-mode read of it is a permission fault and a + User-mode instruction fetch from it is one too. Both are what step 3 needs, + and both prove the KERNEL'S memory is what a module cannot reach -- which is + the property being demonstrated, rather than the absence of a mapping. + + Held in initialised data rather than written as a literal, deliberately and + unlike the status address above. The module reads it through its rebased + GOT, so the value arriving in DFAR or IFAR also proves the GOT was rewritten + and .data was copied. The manager knows the same number and checks it. */ + +volatile ULONG module_forbidden_address = 0x003EFFC0UL; + + +/* Declared as well as defined because the only caller is assembly: + txm_module_preamble.S names it with .extern and stores its offset in the + START entry-point word. An assembly caller supplies no prototype, so + without this the definition has external linkage and no visible + declaration -- prohibited by MISRA C:2012 Rule 8.4 and reported by + -Wmissing-prototypes. Same reason the board support has board.h. */ + +void demo_module_start(ULONG id); + + +void demo_module_start(ULONG id) +{ + ULONG i; + ULONG sum = 0UL; + ULONG shared_ok = 1UL; + + /* 1. The module's own data. If this faults, the data region is wrong and + nothing else in this file will be reached. */ + + for (i = 0UL; i < 16UL; i++) + { + module_scratch[i] = i * 3UL; + } + + for (i = 0UL; i < 16UL; i++) + { + sum += module_scratch[i]; + } + + if (sum == 360UL) /* 3 * (0 + 1 + ... + 15) */ + { + module_progress |= MODULE_PROGRESS_OWN_DATA; + MODULE_STATUS |= MODULE_PROGRESS_OWN_DATA; + } + + /* 2. A kernel call, which leaves User mode through the supervisor call + boundary and must come back. A sleep is used because it is the + simplest service with an observable effect and it yields, so the + scheduler runs a module thread and reloads its regions on the way + back -- exercising the region switch as well as the call. */ + + if (tx_thread_sleep(2UL) == TX_SUCCESS) + { + module_progress |= MODULE_PROGRESS_KERNEL_CALL; + MODULE_STATUS |= MODULE_PROGRESS_KERNEL_CALL; + } + + /* 3. The violation, and 4. the flag that says it was tolerated. Each is + marked before the access rather than after, because after is not + reached if the port is working. The SURVIVED flags are what the + manager fails on: they can only appear if the hardware allowed an + access it was configured to refuse. */ + + if ((id & MODULE_TEST_MASK) == MODULE_TEST_SHARED_ABORT) + { + /* Write every granule the manager granted, read all of them back, and + then write the one it did not grant. + + The readback matters as much as the write. A shared entry programmed + with the wrong base still accepts a store -- it just lands somewhere + else -- so a write that is never read back proves only that the core + did not object. Reading each granule's own mark out of its own + granule is what says the five entries describe five distinct + extents. */ + + for (i = 0UL; i < MODULE_STATUS_GRANULES; i++) + { + if (i != MODULE_STATUS_UNGRANTED) + { + MODULE_SHARED_MARK(i) = MODULE_SHARED_SIGNATURE | i; + } + } + + for (i = 0UL; i < MODULE_STATUS_GRANULES; i++) + { + if ((i != MODULE_STATUS_UNGRANTED) && + (MODULE_SHARED_MARK(i) != (MODULE_SHARED_SIGNATURE | i))) + { + shared_ok = 0UL; + } + } + + if (shared_ok != 0UL) + { + module_progress |= MODULE_PROGRESS_SHARED_WROTE; + MODULE_STATUS |= MODULE_PROGRESS_SHARED_WROTE; + } + + /* The gap. A WRITE rather than a read, so DFSR reports WnR set and the + fault cannot be confused with the read the data-abort passes do. It + lies between two granules this module was granted, so surviving it + means a grant covered a granule nobody asked for -- which is the + whole reason this pass exists. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_GAP; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_GAP; + + MODULE_SHARED_MARK(MODULE_STATUS_UNGRANTED) = + MODULE_SHARED_SIGNATURE | MODULE_STATUS_UNGRANTED; + + module_progress |= MODULE_PROGRESS_SURVIVED_GAP; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_GAP; + } + else if ((id & MODULE_TEST_MASK) == MODULE_TEST_PREFETCH_ABORT) + { + /* Branch outside the code region: a prefetch abort, reported through + IFSR and IFAR. Nothing is executed at the target -- the fault is on + the fetch itself, so the thread never arrives. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_JUMP; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_JUMP; + + /* MISRA C:2012 Rule 11.1 (no conversion between a pointer to a function + and any other type) is deliberately violated here, and Rule 11.6 + (no conversion between an integer and a pointer to void) with it. + Provoking a fetch from an address that holds no function is the entire + purpose of these three lines; there is no conforming way to write it. */ + + ((void (*)(void)) module_forbidden_address)(); + + module_progress |= MODULE_PROGRESS_SURVIVED_JUMP; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_JUMP; + } + else + { + /* Read outside the data region: a data abort, reported through DFSR and + DFAR. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_STEAL; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_STEAL; + + sum += *((volatile ULONG *) module_forbidden_address); + + module_progress |= MODULE_PROGRESS_SURVIVED_STEAL; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_STEAL; + } + + /* Keep the compiler from discarding the read above. */ + + module_scratch[0] = sum; + + while (1) + { + (void) tx_thread_sleep(100UL); + } +} diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c new file mode 100644 index 000000000..db3aeb17b --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c @@ -0,0 +1,1408 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* MODULE MANAGER SAMPLE RELEASE */ +/* */ +/* sample_threadx_module_manager.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Loads the sample module four times, lets it misbehave every time, */ +/* and reports what the hardware did about it -- on the Armv8-R AEM */ +/* FVP, with no debugger and no person in the loop. */ +/* */ +/* This is the S32Z280 module demonstration turned into a regression. */ +/* The silicon version is judged by a human reading a console and by a */ +/* GDB harness that reads the module's memory between passes; neither */ +/* exists here. The model offers an Iris server and no GDB stub, so */ +/* the image has to judge itself and say so in one line the runner can */ +/* match. Everything below that differs from the silicon sample */ +/* differs for that reason. */ +/* */ +/* THE RESULT THIS EXAMPLE EXISTS TO PRODUCE IS THE FAULT. A module */ +/* that starts and runs proves the loader works; a module that is */ +/* stopped by the memory protection unit when it reaches outside its */ +/* own memory proves the port works. So the fault notification is not */ +/* an error path here, it is the expected outcome, and its absence is */ +/* the failure. */ +/* */ +/* TWO PASSES, because one proves nothing about relocation. The module */ +/* is position independent: it is linked against nominal addresses it */ +/* never runs at, and _gcc_setup rewrites its global offset table to */ +/* wherever the manager actually put it. A single run at the linked */ +/* address would exercise a rebase whose input and output are the same */ +/* number, and would look identical if the rebase did nothing at all. */ +/* */ +/* So pass 1 loads the blob where the linker placed it and pass 2 loads */ +/* a byte-for-byte copy of it from the staging area, with pass 1 still */ +/* holding its pool memory so that pass 2's data lands somewhere else */ +/* too. Both bases therefore differ between the passes, which is what */ +/* makes the comparison at the end mean something. */ +/* */ +/* THEN A THIRD PASS, which faults the other way. The two passes above */ +/* make the module read an address it does not own: a data abort, */ +/* reported through DFSR and DFAR. A module can equally leave its code */ +/* region, which is a prefetch abort reported through IFSR and IFAR and */ +/* arrives at the handler by a different vector. Both halves of the */ +/* port's fault path are therefore exercised, and neither is inferred */ +/* from the other. The third pass also runs after the first two have */ +/* been unloaded, which is the other half of what this file shows: a */ +/* module fault must leave the manager able to load and run the next */ +/* module. */ +/* */ +/* AND WHAT THE MODULE ACHIEVED IS READ BACK, not inferred. The manager */ +/* grants each pass a shared MPU region over one granule at a fixed */ +/* address and the module records its progress there. That is what */ +/* replaces the GDB harness, and it is what lets this file fail on the */ +/* two flags that matter -- SURVIVED_STEAL and SURVIVED_JUMP -- by name */ +/* rather than only on the absence of a fault. The two are the same */ +/* event seen from both ends, and a regression that showed one without */ +/* the other would be worth knowing about. */ +/* */ +/* AND A FOURTH PASS FOR THE SHARED REGIONS. The three above are each */ +/* granted one shared region -- the status granule -- which exercises */ +/* the first of the five shared entries the port provides and says */ +/* nothing about the other four. The fourth pass is granted all five, */ +/* one 64-byte granule each, and is NOT granted the granule that sits */ +/* between two of them. It writes every granule it was given, reads */ +/* every one of them back, and then writes the gap, which must fault. */ +/* */ +/* That shape is chosen against a specific defect. A limit register */ +/* masked the wrong way, or a base off by one granule, extends a region */ +/* past what was asked for -- and with the gap sandwiched between two */ +/* granted granules it is reachable from either side if that happens. */ +/* The readback matters as much as the write: a region programmed with */ +/* the wrong base accepts a store and puts it elsewhere, so five marks */ +/* read out of five granules is what says five distinct extents were */ +/* programmed rather than one of them five times. */ +/* */ +/* The same pass probes the two ways the manager refuses a grant, which */ +/* nothing had ever called: an unaligned address must come back */ +/* TXM_MODULE_ALIGNMENT_ERROR, and one grant past the entry count must */ +/* come back TX_NO_MEMORY. Both are checked by name. The order is not */ +/* free -- the entry-count check runs before the alignment check, so the */ +/* unaligned probe has to happen while entries remain. */ +/* */ +/* WHAT A GREEN RUN HERE DOES NOT PROVE. The model reports 32 EL1 MPU */ +/* regions. The Cortex-R52 TRM gives MPUIR.DREGION as 16, 20 or 24, so */ +/* 32 is not an architecturally permitted value for this core and the */ +/* model is the generic AEMv8-R rather than an R52. It is strictly more */ +/* permissive than every real part: this port needs seventeen regions, */ +/* which the S32Z280's twenty supply and a legal 16-region R52 does not, */ +/* and no result from this image can tell you that. The count is */ +/* reported below so the log says what it was rather than implying it. */ +/* */ +/**************************************************************************/ + +#include "tx_api.h" +#include "txm_module.h" +#include "console.h" +#include "platform.h" +#include "mpu.h" +#include "cache.h" + +/* For bsp_main, which entry.S calls and therefore never declares. Every + other example in the board support package includes this header for the + same reason; MISRA C:2012 Rule 8.4 wants the declaration visible at the + definition, and -Wmissing-prototypes reports it when it is not. */ + +#include "board.h" + +/* The fault information the abort vector captured. Declared here because the + module manager expands it into the port's fault handler through the + TXM_MODULE_MANAGER_FAULT_INFO macro rather than declaring it in a header, so an + application that wants to read it has to say so itself. */ + +extern TXM_MODULE_MANAGER_MEMORY_FAULT_INFO _txm_module_manager_memory_fault_info; + +/* Where the module image sits. Provided by the linker script, which places the + module's preamble first so this address is also the preamble address. */ + +extern unsigned char __module_image_start__; +extern unsigned char __module_image_end__; + +/* The second address the same blob is loaded from, so relocation can be shown + rather than assumed. Reserved by the linker script and left empty by it -- + see .module_stage in link_module.lds for why it is not pre-filled. */ + +extern unsigned char __module_stage_start__; +extern unsigned char __module_stage_end__; + +/* The shared status granule the module reports its progress through. */ + +extern unsigned char __module_status_start__; +extern unsigned char __module_status_end__; + +/* The kernel's own writable region, used to check that the address the module + is told to reach for really is memory it must not have. */ + +extern unsigned char __data_start__; +extern unsigned char __data_end__; + +/* The address the module is going to reach for and must not be allowed to have. + The value lives in the module's own initialised data, which is the point -- + the module reads it through its rebased GOT, so seeing it arrive in DFAR + proves the rebase and the .data copy both worked. Kept in step with + sample_threadx_module.c by hand, and checked at run time twice: that it is + what the module faulted on, and that it lies inside the kernel's data region + in the first place. An address in no region at all would fault too, and + would prove much less. */ + +#define MODULE_FORBIDDEN_ADDRESS 0x003EFFC0UL + +/* The shared status word. The same number is FVP_MODULE_STATUS_BASE in + platform.h, ORIGIN(MODULE) in link_module.lds and MODULE_STATUS_ADDRESS in + the module. Three spellings of one address, checked against the linker + symbol below rather than trusted. */ + +#define MODULE_STATUS_ADDRESS FVP_MODULE_STATUS_BASE +#define MODULE_STATUS_LENGTH FVP_MODULE_STATUS_SIZE + +/* The rest of the shared area, which is there to exercise the shared-region + machinery rather than to report anything. + + MODULE_STATUS_LENGTH is one granule and also the length of one grant. The + area is MODULE_STATUS_GRANULES of them: five the shared pass is granted, one + entry each, and MODULE_STATUS_UNGRANTED -- index 2, so that a granted granule + sits on either side of it -- which is never granted to anything. Both sides + of the agreement compute a granule's address the same way from the same base, + and each granule's mark goes at word 1 because word 0 of granule 0 is the + progress word. */ + +#define MODULE_STATUS_GRANULES FVP_MODULE_STATUS_GRANULES +#define MODULE_STATUS_UNGRANTED FVP_MODULE_STATUS_UNGRANTED +#define MODULE_STATUS_AREA_LENGTH (MODULE_STATUS_GRANULES * MODULE_STATUS_LENGTH) +#define MODULE_STATUS_MARK_OFFSET 4UL + +#define MODULE_SHARED_SIGNATURE 0x5A5A0000UL + +/* How many grants the shared pass makes, which must be every entry the port + provides -- granting fewer would leave an entry unexercised, and the point of + the pass is that all of them work. */ + +#define MODULE_SHARED_GRANTS (MODULE_STATUS_GRANULES - 1U) + +/* MISRA C:2012 Rule 11.6 is deliberately violated by the second of these: the + address is an agreement between two separately linked images and there is no + conforming way to express it. */ + +#define MODULE_GRANULE_ADDRESS(index) \ + (MODULE_STATUS_ADDRESS + ((index) * MODULE_STATUS_LENGTH)) + +#define MODULE_SHARED_MARK(index) \ + (*((volatile ULONG *) (MODULE_GRANULE_ADDRESS(index) \ + + MODULE_STATUS_MARK_OFFSET))) + +/* The progress bits the module sets, mirrored from sample_threadx_module.c. + The two SURVIVED flags can only ever be set if the hardware allowed an access + it was configured to refuse, so they are checked for by name. */ + +#define MODULE_PROGRESS_OWN_DATA 0x00000001UL +#define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL +#define MODULE_PROGRESS_ATTEMPTED_STEAL 0x00000004UL +#define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL +#define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL +#define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL +#define MODULE_PROGRESS_SHARED_WROTE 0x00000040UL +#define MODULE_PROGRESS_ATTEMPTED_GAP 0x00000080UL +#define MODULE_PROGRESS_SURVIVED_GAP 0x00000100UL + +#define MODULE_PROGRESS_SURVIVED (MODULE_PROGRESS_SURVIVED_STEAL | \ + MODULE_PROGRESS_SURVIVED_JUMP | \ + MODULE_PROGRESS_SURVIVED_GAP) + +/* Which violation a pass tells the module to commit. Written into the module + instance's application-defined ID after the load and before the start, because + that word is what the manager hands the module's start thread. The high half + is the fingerprint the preamble ships; only the low byte selects the test. */ + +#define MODULE_ID_BASE 0x52520000UL +#define MODULE_TEST_DATA_ABORT 0x00000001UL +#define MODULE_TEST_PREFETCH_ABORT 0x00000002UL +#define MODULE_TEST_SHARED_ABORT 0x00000003UL + +/* Memory the manager hands out to modules: object memory, and the data region a + module's own variables live in. It has to be outside every region a module is + given, or a module could reach another module's data. */ + +#ifndef MODULE_POOL_SIZE +#define MODULE_POOL_SIZE (16U * 1024U) +#endif + +/* In the module area, not in .bss. The manager carves a module's data out of + this pool, and that data is handed to the module as an MPU region -- so if the + pool lived in .bss it would sit inside the kernel's data region and the two + would overlap, which PMSAv8-R does not allow. */ + +static unsigned char module_pool[MODULE_POOL_SIZE] + __attribute__((aligned(64), section(".module_pool"))); + +/* The object pool, which is a separate allocation from the module pool above and + is not optional here. A module that runs in User mode needs a kernel stack + for the privileged side of each system call, and the manager allocates that + from this pool -- txm_module_manager_initialize deliberately leaves the pool + uncreated, so without this a User-mode module fails to start with + TX_NOT_AVAILABLE from deep inside the thread create. */ + +#ifndef MODULE_OBJECT_POOL_SIZE +#define MODULE_OBJECT_POOL_SIZE (8U * 1024U) +#endif + +static unsigned char module_object_pool[MODULE_OBJECT_POOL_SIZE] + __attribute__((aligned(64))); + +/* One instance per pass. Passes 1 and 2 are loaded at once for part of the run: + pass 1 is stopped but still holds its data allocation while pass 2 loads, which + is what pushes pass 2's data base somewhere different. */ + +#define MODULE_PASSES 4U +#define MODULE_RELOCATION_PASSES 2U + +static TXM_MODULE_INSTANCE demo_module[MODULE_PASSES]; + +/* What each pass produced. Recorded rather than printed as it happens, because + the comparison between the passes is the actual result and it cannot be made + until they have all run. */ + +typedef struct PASS_RESULT_STRUCT +{ + CHAR *pass_name; + ULONG pass_test; /* MODULE_TEST_*: which abort is expected */ + ULONG pass_load_status; + ULONG pass_share_status; + ULONG pass_start_status; + ULONG pass_stop_status; + ULONG pass_code_start; + ULONG pass_code_end; + ULONG pass_data_start; + ULONG pass_data_end; + ULONG pass_data_base; + ULONG pass_faults; + ULONG pass_captured; + ULONG pass_progress; + ULONG pass_expect_progress; + + /* The shared-region results. Only the shared pass fills these in, so the + judge reads them only for that pass -- zero is TX_SUCCESS and a pass that + never probed would otherwise read as one that probed and was refused + nothing. */ + + ULONG pass_shared_grants; /* grants that succeeded */ + ULONG pass_shared_count; /* entries the instance holds after */ + ULONG pass_align_status; /* refusing an unaligned grant */ + ULONG pass_exhaust_status; /* refusing one grant too many */ + ULONG pass_marks[MODULE_STATUS_GRANULES]; + + /* The address this pass must fault on. Per pass rather than one constant, + because the shared pass faults on the granule it was not granted and the + other three fault on the kernel's data. */ + + ULONG pass_expect_fault; + ULONG pass_fault_r9; + ULONG pass_dfsr; + ULONG pass_dfar; + ULONG pass_ifsr; + ULONG pass_ifar; + ULONG pass_spsr; + ULONG pass_code_location; + + /* What the notify callback was told, against what it should have been told. + Recorded as plain words rather than pointers so the console can print them + and a mismatch names both values. */ + + ULONG pass_notify_thread; + ULONG pass_notify_instance; + ULONG pass_expect_thread; + ULONG pass_expect_instance; +} PASS_RESULT; + +static PASS_RESULT pass_results[MODULE_PASSES]; + +/* What the fault handler saw. Read by the reporting thread after the module has + been terminated. */ + +static volatile ULONG fault_count; +static volatile ULONG fault_notify_thread; +static volatile ULONG fault_notify_instance; + +static TX_THREAD report_thread; +static unsigned char report_stack[2048] __attribute__((aligned(8))); + + +/**************************************************************************/ +/* Fault notification. */ +/* */ +/* Called by the module manager after it has terminated the offending */ +/* thread. Records rather than prints: this runs in Abort mode on the */ +/* Abort stack, which is a kilobyte and already carries the terminate */ +/* underneath this frame. A callback that printed would work here and */ +/* would still be the wrong shape to copy. */ +/* */ +/* Its two arguments are the point of the hook, so they are recorded and */ +/* checked rather than discarded: an application is being told WHICH */ +/* thread and WHICH module faulted, and a callback that fires with the */ +/* wrong pair is no more use than one that never fires. */ +/**************************************************************************/ + +static void module_fault_notify(TX_THREAD *thread_ptr, TXM_MODULE_INSTANCE *module_instance) +{ + fault_count++; + fault_notify_thread = (ULONG) thread_ptr; + fault_notify_instance = (ULONG) module_instance; +} + + +static void put_field(const char *label, unsigned long value) +{ + console_puts(label); + console_puthex(value); + console_puts("\n"); +} + + +/**************************************************************************/ +/* The shared status word, reached through the manager's load window. */ +/* */ +/* No kernel region covers the module area; region 16 does, and the */ +/* scheduler enables it for every thread that owns no module. This */ +/* thread owns none, so the window is open on it and these two functions */ +/* need no bracketing of their own -- which is the whole reason the */ +/* window is owned by the scheduler rather than by whoever calls. */ +/* */ +/* MISRA C:2012 Rule 11.6 is deliberately violated: the address is an */ +/* agreement between two separately linked images. */ +/**************************************************************************/ + +static void module_status_clear(void) +{ + ULONG granule; + + /* The WHOLE area, not just the progress word. The gap granule has to start + at a known zero, because "the module never wrote it" is one of the results + and a leftover mark from the previous pass would read as a module that + reached memory it was never granted -- or hide one that did. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + *((volatile ULONG *) MODULE_GRANULE_ADDRESS(granule)) = 0UL; + MODULE_SHARED_MARK(granule) = 0UL; + } +} + + +static ULONG module_status_read(void) +{ + return *((volatile ULONG *) MODULE_STATUS_ADDRESS); +} + + +/**************************************************************************/ +/* A byte copy, written out rather than called for. */ +/* */ +/* The manager links -nostartfiles and nothing else in this image reaches */ +/* for a C library, so calling memcpy would pull one in for a single */ +/* copy. The blob is under two kilobytes and this runs once. */ +/**************************************************************************/ + +static void copy_bytes(unsigned char *destination, const unsigned char *source, + unsigned long length) +{ + unsigned long i; + + for (i = 0UL; i < length; i++) + { + destination[i] = source[i]; + } +} + + +/**************************************************************************/ +/* The shared grants a pass gets, and the two ways a grant is refused. */ +/* */ +/* Every pass is granted the first granule, which is the progress word it */ +/* reports through. The shared pass is granted one granule per shared */ +/* entry the port provides, skipping the gap, because a single grant only */ +/* ever exercises the first of the five and this port has never run the */ +/* other four. */ +/* */ +/* It also probes the two ways a grant is refused, which can only be done */ +/* on a LOADED instance. ORDER MATTERS: the manager checks the entry */ +/* count BEFORE it checks alignment, so the unaligned probe has to happen */ +/* while entries remain -- after five grants it would come back */ +/* TX_NO_MEMORY and say nothing about alignment at all. */ +/* */ +/* Returns the first grant status that was not TX_SUCCESS, or TX_SUCCESS. */ +/**************************************************************************/ + +static UINT grant_shared_regions(TXM_MODULE_INSTANCE *instance, PASS_RESULT *result) +{ + UINT status; + UINT first_failure = TX_SUCCESS; + ULONG granule; + + if (result -> pass_test != MODULE_TEST_SHARED_ABORT) + { + /* The progress word, and nothing else. */ + + return txm_module_manager_external_memory_enable(instance, + (VOID *) &__module_status_start__, + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + } + + /* Refused for alignment. One word into the first granule: an address + inside the area, so what is being tested is the 64-byte granule rule and + not the range. It must also consume no entry, which the five grants + below prove by all succeeding. */ + + result -> pass_align_status = + (ULONG) txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + MODULE_STATUS_MARK_OFFSET), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + /* One region per granule, every entry the port has, and never the gap. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule != MODULE_STATUS_UNGRANTED) + { + status = txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + (granule * MODULE_STATUS_LENGTH)), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + if (status == TX_SUCCESS) + { + result -> pass_shared_grants++; + } + else if (first_failure == TX_SUCCESS) + { + first_failure = status; + } + else + { + /* Already recorded; the first failure is the informative one. */ + } + } + } + + /* And one grant too many, which must be refused. Not cosmetic: the entry + written is TXM_MODULE_MPU_SHARED_INDEX plus the count, so a sixth grant + that got past the check would write one past the end of the region table + and into the instance fields that follow it. + + Aimed at the GAP, deliberately. It overlaps nothing, so a refusal costs + nothing -- and if the check ever failed, the gap would become a granted + region and the module would go on to survive writing it. The same defect + would then be reported twice, once as this status and once by name as + MODULE_PROGRESS_SURVIVED_GAP, rather than only as a status nobody reads. */ + + result -> pass_exhaust_status = + (ULONG) txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + + (MODULE_STATUS_UNGRANTED * MODULE_STATUS_LENGTH)), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + result -> pass_shared_count = instance -> txm_module_instance_shared_memory_count; + + return first_failure; +} + + +/**************************************************************************/ +/* One pass: load the blob from a given address, grant it the shared */ +/* granules it is entitled to, start it, wait for the fault it is written */ +/* to provoke, and stop it. */ +/* */ +/* The module is left loaded. Its data allocation is what moves the next */ +/* pass's data base, and releasing it here would defeat half the test. */ +/**************************************************************************/ + +/* name is CHAR * and not const CHAR *, because txm_module_manager_in_place_load + takes it that way -- the manager stores the pointer in the instance and the API + has never promised not to write through it. */ + +static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) +{ + PASS_RESULT *result = &pass_results[index]; + TXM_MODULE_INSTANCE *instance = &demo_module[index]; + unsigned int waited; + ULONG granule; + + result -> pass_name = name; + result -> pass_test = test; + + /* What the module should manage before the hardware stops it: its own data, + a kernel call, and the attempt -- plus, for the shared pass, the five + granules it wrote and read back on the way. Never a SURVIVED bit; that + one is the failure. + + The address it must fault on goes with it, because the three tests do not + share one. Two of them reach into the kernel's data; the shared pass + writes the mark word of the granule it was not granted, and that address + is computed here exactly as the module computes it. */ + + if (test == MODULE_TEST_SHARED_ABORT) + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_SHARED_WROTE + | MODULE_PROGRESS_ATTEMPTED_GAP; + + result -> pass_expect_fault = MODULE_GRANULE_ADDRESS(MODULE_STATUS_UNGRANTED) + + MODULE_STATUS_MARK_OFFSET; + } + else if (test == MODULE_TEST_PREFETCH_ABORT) + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_ATTEMPTED_JUMP; + + result -> pass_expect_fault = MODULE_FORBIDDEN_ADDRESS; + } + else + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_ATTEMPTED_STEAL; + + result -> pass_expect_fault = MODULE_FORBIDDEN_ADDRESS; + } + + /* Cleared before the module starts, so a fault or a progress bit counted + here belongs to this pass and not the previous one. */ + + fault_count = 0UL; + fault_notify_thread = 0UL; + fault_notify_instance = 0UL; + module_status_clear(); + + result -> pass_load_status = (ULONG) txm_module_manager_in_place_load(instance, name, location); + + if (result -> pass_load_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Read from the instance rather than computed here. Where the code ended up + is what the manager decided, and the whole point of the comparison at the + end is to check the module against the manager's own numbers. + + Recorded HERE, immediately after the load and before anything that can + fail, because the load is what decided them. Taken any later, a pass that + failed to be granted its shared regions would carry zeros into the overlap + comparison at the end of the run -- and zero against zero overlaps, so a + grant failure was reported a second time as "its code and data regions + overlap", which is not true and points at the wrong thing. */ + + result -> pass_code_start = (ULONG) instance -> txm_module_instance_code_start; + result -> pass_code_end = (ULONG) instance -> txm_module_instance_code_end; + result -> pass_data_start = (ULONG) instance -> txm_module_instance_data_start; + result -> pass_data_end = (ULONG) instance -> txm_module_instance_data_end; + result -> pass_data_base = (ULONG) instance -> txm_module_instance_module_data_base_address; + + /* The shared granules the module reports through, and for the shared pass + every other entry as well. Granted while the module is LOADED and before + it is STARTED, which is the only window the manager accepts, and granted + per pass because each pass is its own instance with its own region table + -- an instance is memset by the load, so nothing carries over. */ + + result -> pass_share_status = (ULONG) grant_shared_regions(instance, result); + + if (result -> pass_share_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Which violation this pass provokes. Written after the load, which is what + filled the field in from the preamble, and before the start, which is what + hands it to the module's start thread. */ + + instance -> txm_module_instance_application_module_id = MODULE_ID_BASE | test; + + /* What the notify callback must be told, recorded before the module runs so + the comparison afterwards is against an expectation and not against + whatever the callback happened to write. */ + + result -> pass_expect_thread = (ULONG) &(instance -> txm_module_instance_start_stop_thread); + result -> pass_expect_instance = (ULONG) instance; + + result -> pass_start_status = (ULONG) txm_module_manager_start(instance); + + if (result -> pass_start_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Wait for the abort vector to capture a fault belonging to THIS pass. + + The captured fault info is waited on rather than the notify callback, even + though the callback works. The capture happens in the abort vector before + anything else runs, so it is the earliest and most direct evidence that a + fault occurred; the callback is a later consequence of the same event, and + this file checks it separately. Waiting on the earlier of the two means a + regression in the callback path shows up as "notified 0" next to a + captured fault, rather than as "no fault occurred" -- which is the opposite + of the truth. + + Attribution is by thread pointer: each pass has its own module instance and + therefore its own start thread, so a captured fault naming this pass's + thread cannot be a leftover from the previous pass. */ + + waited = 0U; + while ((_txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_thread_ptr + != &(instance -> txm_module_instance_start_stop_thread)) && + (waited < 50U)) + { + tx_thread_sleep(2UL); + waited++; + } + + if (_txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_thread_ptr + == &(instance -> txm_module_instance_start_stop_thread)) + { + result -> pass_captured = 1UL; + result -> pass_dfsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfsr; + result -> pass_dfar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfar; + result -> pass_ifsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifsr; + result -> pass_ifar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifar; + result -> pass_spsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_spsr; + result -> pass_fault_r9 = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_r9; + result -> pass_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location; + } + + /* What the module reached, and what the notify callback saw. Both read + after the wait above, so what ran is recorded and what did not shows as + zero. The status word is read while THIS pass is still the last thing to + have written it, which is the same discipline the silicon harness needs + for a different reason: there, the byte pool reuses a freed block and a + later pass's data lands where an earlier one's was. */ + + result -> pass_progress = module_status_read(); + result -> pass_faults = fault_count; + result -> pass_notify_thread = fault_notify_thread; + result -> pass_notify_instance = fault_notify_instance; + + /* Every granule's mark, granted or not. The granted ones say the module + wrote and read back the granule it meant to, which is what proves five + distinct entries were programmed rather than one entry five times; the + gap's says whether it stayed untouched. Read here, while this pass is + still the last thing to have written the area. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + result -> pass_marks[granule] = MODULE_SHARED_MARK(granule); + } + + /* Stopped, not unloaded. The fault terminated the module's start thread but + left the module STARTED, and unload refuses anything that is not LOADED or + STOPPED -- so this is required, not tidiness. */ + + result -> pass_stop_status = (ULONG) txm_module_manager_stop(instance); +} + + +static void report_one_pass(const PASS_RESULT *result) +{ + console_puts("\n--- "); + console_puts(result -> pass_name); + console_puts(" ---\n"); + + console_puts(" expected abort = "); + if (result -> pass_test == MODULE_TEST_PREFETCH_ABORT) + { + console_puts("prefetch (IFSR/IFAR)\n"); + } + else if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + console_puts("data (DFSR/DFAR), writing an ungranted shared granule\n"); + } + else + { + console_puts("data (DFSR/DFAR)\n"); + } + + put_field(" load status = ", result -> pass_load_status); + put_field(" share status = ", result -> pass_share_status); + put_field(" start status = ", result -> pass_start_status); + put_field(" stop status = ", result -> pass_stop_status); + put_field(" code region = ", result -> pass_code_start); + put_field(" .. to = ", result -> pass_code_end); + put_field(" data region = ", result -> pass_data_start); + put_field(" .. to = ", result -> pass_data_end); + put_field(" data base (r9) = ", result -> pass_data_base); + put_field(" fault captured = ", result -> pass_captured); + put_field(" module progress = ", result -> pass_progress); + put_field(" should be = ", result -> pass_expect_progress); + put_field(" fault address = ", result -> pass_expect_fault); + put_field(" notify callbacks = ", result -> pass_faults); + put_field(" notified thread = ", result -> pass_notify_thread); + put_field(" should be = ", result -> pass_expect_thread); + put_field(" notified module = ", result -> pass_notify_instance); + put_field(" should be = ", result -> pass_expect_instance); + put_field(" r9 at the fault = ", result -> pass_fault_r9); + put_field(" DFSR = ", result -> pass_dfsr); + put_field(" DFAR = ", result -> pass_dfar); + put_field(" IFSR = ", result -> pass_ifsr); + put_field(" IFAR = ", result -> pass_ifar); + put_field(" SPSR = ", result -> pass_spsr); + put_field(" faulting pc = ", result -> pass_code_location); + + /* Meaningful for a data abort, where the module faulted inside its own code. + A prefetch abort faults ON the address it branched to, so the faulting pc + is outside the module and the difference is not an offset into it. */ + + if (result -> pass_test != MODULE_TEST_PREFETCH_ABORT) + { + put_field(" pc - code base = ", result -> pass_code_location - result -> pass_code_start); + } + + /* The shared-region half, reported only by the pass that produces it. */ + + if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + ULONG granule; + + put_field(" shared grants = ", result -> pass_shared_grants); + put_field(" should be = ", (unsigned long) MODULE_SHARED_GRANTS); + put_field(" entries held = ", result -> pass_shared_count); + put_field(" unaligned grant = ", result -> pass_align_status); + put_field(" should be = ", (unsigned long) TXM_MODULE_ALIGNMENT_ERROR); + put_field(" one grant too many = ", result -> pass_exhaust_status); + put_field(" should be = ", (unsigned long) TX_NO_MEMORY); + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule == MODULE_STATUS_UNGRANTED) + { + put_field(" gap granule mark = ", result -> pass_marks[granule]); + put_field(" should be = ", 0UL); + } + else + { + put_field(" granule mark = ", result -> pass_marks[granule]); + put_field(" should be = ", MODULE_SHARED_SIGNATURE | granule); + } + } + } +} + + +/**************************************************************************/ +/* Per-pass verdict. Returns the number of failures it found. */ +/**************************************************************************/ + +static UINT judge_one_pass(const PASS_RESULT *result) +{ + if ((result -> pass_load_status != (ULONG) TX_SUCCESS) || + (result -> pass_share_status != (ULONG) TX_SUCCESS) || + (result -> pass_start_status != (ULONG) TX_SUCCESS)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": did not load, share and start\n"); + return 1U; + } + + /* First, and named explicitly: the module must not have survived what it + was told to attempt. This is the one result that means the protection + did not hold, and it is checked before anything else so that a run in + which the MPU did nothing says so in those words rather than through + some downstream symptom. */ + + if ((result -> pass_progress & MODULE_PROGRESS_SURVIVED) != 0UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the module SURVIVED its violation -- it is not protected\n"); + return 1U; + } + + if (result -> pass_captured == 0UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": reached outside its memory and was not stopped\n"); + return 1U; + } + + /* What it did manage, from the shared granule. A module that faulted + without having read its own data or made a kernel call faulted for the + wrong reason, and the fault alone would not have said so. */ + + if (result -> pass_progress != result -> pass_expect_progress) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": did not reach the point it was supposed to fault at\n"); + return 1U; + } + + /* r9 is the module's PIC base and the manager seeded it from the thread + entry info. If the value captured at the fault is not the data base the + manager handed out, the seeding is wrong and every data reference the + module made went somewhere unintended. */ + + if (result -> pass_fault_r9 != result -> pass_data_base) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": r9 was not the module's data base\n"); + return 1U; + } + + /* A fault from anywhere but User mode is not this test passing: the module + runs unprivileged, so a privileged fault means the fault came from the + kernel and something else is wrong. */ + + if ((result -> pass_spsr & 0x1FUL) != 0x10UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": faulted, but not from User mode\n"); + return 1U; + } + + /* The module got this address out of its own initialised data, through its + rebased GOT. Any other value means it faulted somewhere unintended -- + most likely on its own data, which is what a bad rebase looks like. + + Which register carries it depends on the abort: a data abort reports the + address it tried to touch in DFAR, a prefetch abort reports the address it + tried to fetch in IFAR. Checking the wrong one of the two passes for a + stale value left by an earlier fault, which is why this is selected on the + expected type rather than on whichever register happens to be non-zero. */ + + if (((result -> pass_test == MODULE_TEST_PREFETCH_ABORT) + ? result -> pass_ifar : result -> pass_dfar) != result -> pass_expect_fault) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": faulted at the wrong address, so .data or the GOT is wrong\n"); + return 1U; + } + + /* The shared-region results, for the one pass that produces them. Checked + after the progress word above, so a pass that never got as far as the + granules is reported as that rather than as a grant problem. */ + + if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + ULONG granule; + + /* Every entry the port provides, granted. Fewer means an entry past + the first is not usable, which is the state this port was in before + this pass existed. */ + + if (result -> pass_shared_grants != (ULONG) MODULE_SHARED_GRANTS) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": not every shared entry could be granted\n"); + return 1U; + } + + if (result -> pass_shared_count != (ULONG) TXM_MODULE_MPU_SHARED_ENTRIES) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the instance does not hold the entries it granted\n"); + return 1U; + } + + /* An unaligned grant must be refused BY NAME. Accepting it would not + fault: the low six bits of PRBAR are the shareability, permission and + execute-never fields, so an under-aligned base silently changes what + the region permits instead of where it is. */ + + if (result -> pass_align_status != (ULONG) TXM_MODULE_ALIGNMENT_ERROR) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": an unaligned shared grant was not refused as such\n"); + return 1U; + } + + if (result -> pass_exhaust_status != (ULONG) TX_NO_MEMORY) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": one grant too many was not refused\n"); + return 1U; + } + + /* And what the module actually managed to write where. Each granted + granule must hold its own mark -- a region programmed with the wrong + base accepts the store and puts it somewhere else -- and the gap must + still hold the zero the manager left in it. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule == MODULE_STATUS_UNGRANTED) + { + if (result -> pass_marks[granule] != 0UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the ungranted granule was written -- a grant covered too much\n"); + return 1U; + } + } + else if (result -> pass_marks[granule] != (MODULE_SHARED_SIGNATURE | granule)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": a granted granule does not hold its own mark\n"); + return 1U; + } + else + { + /* This granule is as it should be. */ + } + } + } + + /* The notify callback is a public API of the module manager, so it is + checked and not merely reported. Zero here with a captured fault above + means the fault path reached the hardware's evidence and never reached the + application: on this port that used to be the case for every fault, + because the shared fault handler terminates the offending thread before + calling the hook and the terminate only returns if the abort vector has + told the kernel it is inside an exception. More than one means a single + violation was reported twice. */ + + if (result -> pass_faults != 1UL) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the fault-notify callback did not run exactly once\n"); + return 1U; + } + + /* Being told that something faulted is not the service; being told which + thread and which module is. */ + + if ((result -> pass_notify_thread != result -> pass_expect_thread) || + (result -> pass_notify_instance != result -> pass_expect_instance)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": notified about the wrong thread or module\n"); + return 1U; + } + + console_puts("PASS "); + console_puts(result -> pass_name); + console_puts(": ran, faulted in User mode at the forbidden address, and was reported\n"); + + return 0U; +} + + +/**************************************************************************/ +/* The manager thread. */ +/**************************************************************************/ + +static void manager_entry(ULONG input) +{ + UINT status; + UINT i; + unsigned long blob_size; + ULONG offset_0; + ULONG offset_1; + UINT failures = 0U; + + (void) input; + + /* Every manager call happens here, in a thread, and not in + tx_application_define. Loading a module takes the manager's mutex, and a + service that can block cannot be called before the scheduler runs. */ + + /* ------------------------------------------------------------------ + What the platform actually provides. Reported before anything is + loaded, because a shortfall here explains every failure below it. + ------------------------------------------------------------------ */ + + put_field("MPUIR regions available = ", (unsigned long) mpu_region_count()); + put_field("regions this port needs = ", (unsigned long) MPU_MODULE_REGIONS_REQUIRED); + put_field("D-cache line bytes = ", cache_dcache_line_bytes()); + + if (mpu_region_count() < MPU_MODULE_REGIONS_REQUIRED) + { + console_puts("FAIL the implementation has too few MPU regions for this port\n"); + failures++; + } + + /* The three spellings of the shared status address, and the forbidden + address, checked rather than trusted. Both are constants agreed between + separately linked images, and a disagreement in either would present as a + module faulting somewhere unexpected -- which is exactly what the pass + verdicts are trying to distinguish. */ + + if ((unsigned long) &__module_status_start__ != MODULE_STATUS_ADDRESS) + { + console_puts("FAIL the linker put the status word somewhere the module will not look\n"); + failures++; + } + + if ((unsigned long) (&__module_status_end__ - &__module_status_start__) + < MODULE_STATUS_AREA_LENGTH) + { + console_puts("FAIL the status area is smaller than the granules granted over it\n"); + failures++; + } + + if ((MODULE_FORBIDDEN_ADDRESS < (unsigned long) &__data_start__) || + (MODULE_FORBIDDEN_ADDRESS >= (unsigned long) &__data_end__)) + { + /* The point of the test is that a module cannot reach the KERNEL'S + memory. An address in no region at all would fault too, and would + prove much less, so the address is checked to be inside the kernel's + writable region before anything relies on the fault it causes. */ + + console_puts("FAIL the forbidden address is not inside the kernel's data region\n"); + failures++; + } + + console_puts("\nM1 initialize\n"); + status = txm_module_manager_initialize((VOID *) module_pool, MODULE_POOL_SIZE); + + put_field("initialize = ", status); + put_field("ready = ", (unsigned long) _txm_module_manager_ready); + + /* The pool the module's kernel stacks come from. See module_object_pool. */ + + status = txm_module_manager_object_pool_create((VOID *) module_object_pool, + MODULE_OBJECT_POOL_SIZE); + + put_field("object pool create = ", status); + + /* Registered before anything is loaded, so there is no window in which a + module could fault with nobody listening. */ + + console_puts("M2 fault notify\n"); + txm_module_manager_memory_fault_notify(module_fault_notify); + + /* The copy that makes pass 2 a relocation test. The staging area is left + empty by the linker on purpose: if the blob were already there, a failure + to copy would be invisible because the right bytes would be present. */ + + blob_size = (unsigned long) (&__module_image_end__ - &__module_image_start__); + + console_puts("M3 staging the blob\n"); + put_field(" blob size = ", blob_size); + put_field(" from = ", (unsigned long) &__module_image_start__); + put_field(" to = ", (unsigned long) &__module_stage_start__); + + if (blob_size > (unsigned long) (&__module_stage_end__ - &__module_stage_start__)) + { + /* Reported rather than allowed to overrun. The staging area is a fixed + size in the linker script and the module is free to grow. */ + + console_puts("FAIL the module image does not fit the staging area\n"); + failures++; + } + else + { + copy_bytes(&__module_stage_start__, &__module_image_start__, blob_size); + + /* Those were data writes to memory that is about to be fetched as + instructions, and the module area is mapped Normal write-back. So the + copied bytes may sit in dirty D-cache lines while the instruction + side, which is not coherent with the D cache on this core, fetches + whatever main memory still holds. Clean the range so memory is + correct, then invalidate the I cache so no stale line from a previous + image can be served. + + Not a precaution the passing run justifies: a cold I cache over a + never-executed address happens to work, and keeps happening to work + until the staging area is reused or an eviction lands differently. + Any loader that copies code owes this pair. */ + + cache_clean_range(&__module_stage_start__, blob_size); + cache_invalidate_icache_all(); + + console_puts("M4 pass 1, at the linked address\n"); + run_one_pass(0U, "linked address, data abort", (VOID *) &__module_image_start__, + MODULE_TEST_DATA_ABORT); + + console_puts("M5 pass 2, relocated\n"); + run_one_pass(1U, "relocated, data abort", (VOID *) &__module_stage_start__, + MODULE_TEST_DATA_ABORT); + + /* Both relocation passes are done with their memory now. Unloaded here + rather than at the end, so that the pass below is a load that follows + two faults and two unloads -- which is the state a manager is in after + a module has misbehaved, and the state the next load has to work in. */ + + for (i = 0U; i < MODULE_RELOCATION_PASSES; i++) + { + (void) txm_module_manager_unload(&demo_module[i]); + } + + /* The other abort type, from the staging area so that it is also a + relocated module: the address arriving in IFAR came out of the + module's own initialised data through its rebased GOT, exactly as DFAR + does above. */ + + console_puts("M6 pass 3, prefetch abort\n"); + run_one_pass(2U, "relocated, prefetch abort", (VOID *) &__module_stage_start__, + MODULE_TEST_PREFETCH_ABORT); + + (void) txm_module_manager_unload(&demo_module[2]); + + /* The shared-region pass. Granted every entry the port provides, one + granule each, and never the granule between two of them -- which it + then writes. This is the only pass that exercises a shared entry + past the first, the granule-alignment refusal, and what happens when + the entries run out; the three passes above grant one region each and + would look identical if four of the five entries did not work. */ + + console_puts("M7 pass 4, shared regions\n"); + run_one_pass(3U, "relocated, shared-region gap", (VOID *) &__module_stage_start__, + MODULE_TEST_SHARED_ABORT); + + (void) txm_module_manager_unload(&demo_module[3]); + } + + /* ------------------------------------------------------------------ + The verdict. + ------------------------------------------------------------------ */ + + console_puts("\n=== ThreadX modules on the Armv8-R AEM FVP: " + "isolation and relocation ===\n"); + + for (i = 0U; i < MODULE_PASSES; i++) + { + report_one_pass(&pass_results[i]); + } + + console_puts("\n"); + + /* Before anything else: a module's code, data and shared regions are enabled + at the same time, and PMSAv8-R has no region priority -- two enabled + regions that overlap are CONSTRAINED UNPREDICTABLE, and that means aborts + from addresses that look perfectly legal. Nothing in the manager checks + for it, so this example does. + + The risk is real here and not theoretical: the image, the pool and the + staging area all live in one 64 KB area. A module grown large enough, or + a pool sized differently, and two of them would meet. */ + + for (i = 0U; i < MODULE_PASSES; i++) + { + const PASS_RESULT *result = &pass_results[i]; + + if (result -> pass_load_status != (ULONG) TX_SUCCESS) + { + continue; + } + + if ((result -> pass_code_start <= result -> pass_data_end) && + (result -> pass_data_start <= result -> pass_code_end)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": its code and data regions overlap\n"); + failures++; + } + + if ((result -> pass_code_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_AREA_LENGTH - 1UL)) && + (MODULE_STATUS_ADDRESS <= result -> pass_code_end)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the shared status area overlaps its code\n"); + failures++; + } + + if ((result -> pass_data_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_AREA_LENGTH - 1UL)) && + (MODULE_STATUS_ADDRESS <= result -> pass_data_end)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": the shared status area overlaps its data\n"); + failures++; + } + + /* And what _txm_module_manager_alignment_adjust produced. It is called + by every load and has never been checked, because nothing it gets + wrong faults: it rounds a module's code and data sizes up to the + protection granule and declares the granule as their alignment, and + the loader allocates on that. Get it wrong and the region bases are + written into PRBAR with their low six bits landing on the shareability, + permission and execute-never fields -- so the module runs with + attributes nobody asked for rather than at the wrong address. + + The data region's end is checked as well as its base, because the data + region's length is the rounded size; the CODE region's end is not, it + is the true end of the image and is not rounded to anything. */ + + if (((result -> pass_code_start & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL) || + ((result -> pass_data_start & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL) || + (((result -> pass_data_end + 1UL) & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL)) + { + console_puts("FAIL "); + console_puts(result -> pass_name); + console_puts(": its regions are not aligned to the protection granule\n"); + failures++; + } + } + + /* Each pass on its own. */ + + for (i = 0U; i < MODULE_PASSES; i++) + { + failures += judge_one_pass(&pass_results[i]); + } + + /* And the first two passes against each other, which is the relocation + result. The third is not in this comparison: it faults on the address it + branched to rather than inside its own code, so its faulting pc is not an + offset into the module and there is nothing to compare. */ + + offset_0 = pass_results[0].pass_code_location - pass_results[0].pass_code_start; + offset_1 = pass_results[1].pass_code_location - pass_results[1].pass_code_start; + + if ((pass_results[0].pass_captured == 0UL) || (pass_results[1].pass_captured == 0UL)) + { + console_puts("FAIL relocation: a pass did not fault, nothing to compare\n"); + failures++; + } + else if (pass_results[0].pass_code_start == pass_results[1].pass_code_start) + { + console_puts("FAIL relocation: both passes ran from the same address\n"); + failures++; + } + else if (offset_0 != offset_1) + { + console_puts("FAIL relocation: the two passes faulted at different offsets\n"); + failures++; + } + else + { + console_puts("PASS relocation: the same blob ran correctly from two addresses\n"); + + if (pass_results[0].pass_data_base == pass_results[1].pass_data_base) + { + /* Not a failure -- the code rebase is still proven -- but worth + saying, because it means the data rebase produced the same numbers + twice and was not exercised as thoroughly. */ + + console_puts("NOTE both passes shared a data base; the data rebase was not varied\n"); + } + } + + /* Both abort types, stated as its own line. Each pass above already checked + its own registers; this says that between them the two vectors were both + taken, which is the claim a reader of the log wants to be able to make + without working out what each pass did. */ + + if ((pass_results[0].pass_captured != 0UL) && (pass_results[0].pass_dfsr != 0UL) && + (pass_results[2].pass_captured != 0UL) && (pass_results[2].pass_ifsr != 0UL)) + { + console_puts("PASS both abort types: data through DFSR/DFAR, prefetch through IFSR/IFAR\n"); + } + else + { + console_puts("FAIL only one kind of abort was exercised\n"); + failures++; + } + + /* The manager is still alive to print this, which is itself the last + result: three module faults did not take the kernel with them. */ + + put_field("\nfailures = ", (unsigned long) failures); + + if (failures == 0U) + { + console_puts("\nMODULE RESULT: ALL CHECKS PASSED\n"); + } + else + { + console_puts("\nMODULE RESULT: FAILED\n"); + } + + /* Terminates the model. Every FVP image in this port exits through + semihosting SYS_EXIT, so the runner needs no host-side timeout and a hang + cannot be mistaken for a slow run. */ + + console_exit(failures); +} + + +/**************************************************************************/ +/* Application definition. */ +/**************************************************************************/ + +void tx_application_define(void *first_unused_memory) +{ + UINT status; + + (void) first_unused_memory; + + console_puts("\n=== module manager starting ===\n"); + + /* Nothing but the thread. See manager_entry for why the manager cannot be + driven from here. */ + + status = tx_thread_create(&report_thread, "module manager", manager_entry, 0UL, + report_stack, sizeof(report_stack), + 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START); + + /* Reported rather than discarded. A thread that was never created and a + thread that was created but never scheduled produce exactly the same + silence on the console, and telling those two apart is most of the work + when the port itself is what is under test. */ + + if (status == TX_SUCCESS) + { + console_puts("M0 thread created\n"); + } + else + { + put_field("FAIL tx_thread_create status = ", (unsigned long) status); + } +} + + +/**************************************************************************/ +/* bsp_main -- entered at EL1 from entry.S. Does not return. */ +/**************************************************************************/ + +void bsp_main(void) +{ + console_puts("\n=== ThreadX modules :: Armv8-R AEM FVP (Cortex-R52) ===\n"); + console_puts("entering kernel\n"); + + tx_kernel_enter(); + + /* Not reached. */ + + console_puts("FAIL tx_kernel_enter returned\n"); + + console_exit(1U); +} diff --git a/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/txm_module_preamble.S b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/txm_module_preamble.S new file mode 100644 index 000000000..fa4858f67 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/txm_module_preamble.S @@ -0,0 +1,137 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE PREAMBLE RELEASE */ +@/* */ +@/* txm_module_preamble.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* The header the module manager reads before it will load a module. */ +@/* */ +@/* It must be the first thing in the module image, which is what the */ +@/* linker script arranges, and every entry point in it is an offset */ +@/* from the start of the preamble rather than an address. A module is */ +@/* position independent: the manager decides where it lands, so the */ +@/* module cannot know its own addresses at link time. */ +@/* */ +@/* Code and data sizes come from the linker script rather than being */ +@/* written in by hand. The Cortex-R4 preamble carries literal numbers */ +@/* for them, which is a standing invitation to grow a module past its */ +@/* declared size and have the manager map less memory than it uses -- */ +@/* a fault in a module that did nothing wrong, whose cause is a */ +@/* constant in a file nobody thought to change. */ +@/* */ +@/**************************************************************************/ + + .syntax unified + .arm + + .global __txm_module_preamble + + .extern _txm_module_thread_shell_entry + .extern _txm_module_callback_request_thread_entry + .extern demo_module_start + +@ Supplied by the module's linker script. + + .extern __txm_module_code_size + .extern __txm_module_data_size + +@ Properties. The compiler field tells the manager which set of entry-point +@ adjustments to apply, and the option bits say what the module is asking for: +@ user mode and memory protection, which together are the point of this port. +@ +@ 0x02000000 TXM_MODULE_GNU_COMPILER +@ 0x00000001 TXM_MODULE_USER_MODE +@ 0x00000002 TXM_MODULE_MEMORY_PROTECTION + + .equ MODULE_PROPERTIES, 0x02000003 + + .section .txm_module_preamble, "a" + .align 6 + +__txm_module_preamble: + + .word 0x4D4F4455 @ Module ID, "MODU" + .word 0x6 @ Major version + .word 0x1 @ Minor version + .word 32 @ Preamble size, 32-bit words + .word 0x52520001 @ Application-defined ID + .word MODULE_PROPERTIES @ Properties, see above + +@ Entry points, as offsets from the preamble. + +@ Entry points are stored relative to the word that holds them, not to the +@ start of the preamble. That is what the manager expects: it recovers the +@ offset from the module base by adding the field's own byte offset back -- +@ TXM_MODULE_GNU_SHELL_ADJUST 24, START 28, STOP 32, CALLBACK 44, which are +@ exactly the offsets of the four words below. Storing these relative to +@ __txm_module_preamble instead counts that offset twice, and the module is +@ then entered that many bytes into its shell entry: past the prologue, with +@ the arguments never saved and the frame pointer never set up, so the first +@ dereference goes through a register the stack build had zeroed. On silicon +@ that read faulted at 0x1C, which is offset 0x1C from a null r3. +@ +@ Every other GNU module port writes these the same way; cortex_m33's +@ preamble, which this port was seeded from, spells it "symbol - . - 0". + + .word _txm_module_thread_shell_entry - . + .word demo_module_start - . + .word 0 @ No stop thread + .word 1 @ Start/stop thread priority + .word 1024 @ Start/stop thread stack size + .word _txm_module_callback_request_thread_entry - . + .word 1 @ Callback thread priority + .word 1024 @ Callback thread stack size + +@ Sizes, from the linker script. The manager rounds both up to the 64-byte MPU +@ granule and maps exactly this much; anything the module touches beyond it +@ faults, which is the intended behaviour and not a bug to work around by +@ inflating these numbers. + + .word __txm_module_code_size + .word __txm_module_data_size + + .word 0 @ Reserved 0 + .word 0 @ Reserved 1 + .word 0 @ Reserved 2 + .word 0 @ Reserved 3 + .word 0 @ Reserved 4 + .word 0 @ Reserved 5 + .word 0 @ Reserved 6 + .word 0 @ Reserved 7 + .word 0 @ Reserved 8 + .word 0 @ Reserved 9 + .word 0 @ Reserved 10 + .word 0 @ Reserved 11 + .word 0 @ Reserved 12 + .word 0 @ Reserved 13 + .word 0 @ Reserved 14 + .word 0 @ Reserved 15 + +@ The preamble declares its own length in its fourth word, and the manager +@ believes it. If the two ever disagree the manager reads entry points from the +@ wrong offsets, so the assembler checks rather than the reader. + + .if (. - __txm_module_preamble) != (32 * 4) + .error "txm_module_preamble is not the 32 words its size field declares" + .endif diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds new file mode 100644 index 000000000..3fc17ef86 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds @@ -0,0 +1,199 @@ +/*************************************************************************** + * 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 demonstration module alone, for the S32Z280-594EVB. + * + * The module is a separate link unit from the manager, and it has to be. Both + * sides define the ThreadX API: the kernel defines the real _txe_* entry points + * and the module library defines shims of the same names that trap into the + * kernel instead. Linking them together does not produce a duplicate-symbol + * error, because the kernel arrives as a static library and the module library + * as ordinary objects, and an object always beats an archive member. The module + * shims therefore win for the whole image, and the manager's own calls to + * tx_thread_create and friends are quietly redirected into the module. + * + * That was not a theory. It put _txe_thread_create at 0x317F1700, inside the + * module area, which no kernel MPU region covers -- so tx_application_define + * called into unmapped memory and the core took a prefetch abort with nothing on + * the console to say so. + * + * POSITION INDEPENDENT, and the two addresses below are fictions + * ============================================================== + * + * The module is built -fpic -msingle-pic-base, so every reference it makes to + * its own data goes through the global offset table with r9 as the base. It + * therefore does not run at the addresses in this file and is not meant to: the + * two segment origins are nominal, chosen only so that the loader can tell a + * code address from a data address by comparing against __data_segment_start__. + * What actually happens at run time is that _gcc_setup rewrites every GOT entry + * from these nominal addresses to the addresses the manager decided on. + * + * That is why they are far apart and why neither is zero. Far apart, because + * the whole discrimination is "below the data origin means code"; non-zero, + * because _gcc_setup treats a zero GOT entry as one the linker never filled in + * and skips it, so a real address of zero would be silently dropped. + * + * 0x01000000 code preamble, text, rodata, and the load images of the + * GOT and .data -- this is the blob the manager embeds + * 0x02000000 data the GOT the module runs against, .data and .bss, all + * of which live in memory the manager allocates + * + * The load images matter. A module's .data cannot be used where it was linked, + * because the manager never copies it there: _txm_module_manager_internal_load + * allocates the module's data area from its byte pool and TX_MEMSETs it to zero, + * and that is the only memory the module is given a region for. The module's + * own .data, wherever it was linked, is outside every region the module owns. + * On silicon that faulted at the first write to an initialised variable, with + * DFAR pointing into the gap between the granted code and the granted data. + * + * So .data and .got have their VMAs in the data segment, where the module will + * run, and their LMAs in the code segment, inside the blob, where _gcc_setup can + * find them and copy them out. AT>CODE is what says that. + */ + +MEMORY +{ + /* Nominal. See the note above: the module runs where the manager puts it, + not here. Sized generously because nothing is reserved by being large -- + the raw binary is only as long as the sections actually emitted. */ + + CODE (rx) : ORIGIN = 0x01000000, LENGTH = 0x00100000 + DATA (rw) : ORIGIN = 0x02000000, LENGTH = 0x00100000 +} + +__code_segment_start__ = 0x01000000; +__data_segment_start__ = 0x02000000; + +SECTIONS +{ + /* --------------------------------------------------------------------- + The code segment, which is the blob byte for byte. + --------------------------------------------------------------------- */ + + .module_image : ALIGN(64) + { + __module_image_start__ = .; + + /* The preamble is first because that is where the manager looks for it: + it reads the properties, the entry points and the two sizes from the + first words of the image. KEEP because nothing references it. */ + + KEEP(*(.txm_module_preamble)) + + *(.text .text.*) + *(.glue_7) + *(.glue_7t) + *(.rodata .rodata.*) + + . = ALIGN(4); + } >CODE AT>CODE + + /* --------------------------------------------------------------------- + The data segment. VMAs here, load images back in the code segment. + --------------------------------------------------------------------- */ + + /* The GOT, and it must be first in the data segment. r9 is the GOT base as + far as the compiler is concerned -- every R_ARM_GOT32 is an offset from + it -- and the manager sets r9 to txm_module_instance_module_data_base_address, + which is the start of the module's data area. So GOT origin and data + origin have to be the same address, or every offset the compiler emitted + is measured from the wrong place. __data_segment_start__ above is that + address on the nominal side; r9 is that address on the real side, and + _gcc_setup's rebase is the difference between the two. */ + + .got : ALIGN(4) + { + __new_got_start__ = .; + *(.got.plt) + *(.igot.plt) + *(.got) + __new_got_end__ = .; + } >DATA AT>CODE + + __got_load_start__ = LOADADDR(.got); + + .data : ALIGN(4) + { + __data_start__ = .; + *(.data .data.*) + *(.gnu.linkonce.d.*) + . = ALIGN(4); + __data_end__ = .; + } >DATA AT>CODE + + __data_load_start__ = LOADADDR(.data); + + /* NOLOAD, so .bss contributes nothing to the blob. The manager has already + zeroed the whole data allocation by the time the module runs -- but + _gcc_setup zeroes .bss anyway, because "the manager happens to memset it" + is a property of one loader and not something a module may rely on. */ + + .bss (NOLOAD) : ALIGN(4) + { + __bss_start__ = .; + *(.bss .bss.*) + *(.gnu.linkonce.b.*) + *(COMMON) + . = ALIGN(64); + __bss_end__ = .; + } >DATA + + /* --------------------------------------------------------------------- + The two sizes the preamble declares. + --------------------------------------------------------------------- */ + + /* Code covers everything in the blob, the load images of the GOT and .data + included, because the module has to be able to read them to copy them out + and the code region is the only mapping it has over the blob. Ending it + at .rodata instead would put the GOT template outside every region the + module owns and _gcc_setup would fault on its first read. */ + + __txm_module_code_end__ = __data_load_start__ + SIZEOF(.data); + __txm_module_code_size = __txm_module_code_end__ - __code_segment_start__; + + /* Data covers the GOT, .data and .bss. Not the thread stacks: the manager + adds the start/stop and callback stack sizes to this figure itself, from + the two stack-size words further down the preamble. Counting them here + as well would double them. */ + + __txm_module_data_size = __bss_end__ - __data_segment_start__; + + /* Unwind tables and toolchain notes would otherwise land at address zero and + be carried into the raw binary. A module has no unwinder. + + .rel.dyn and friends are discarded because this is a static link: ld + resolves every R_ARM_GOT32 itself and writes the finished address into the + GOT, so there is nothing left for a dynamic loader to do. If one of these + ever stops being empty, the assumption above has broken and the module + needs relocation processing rather than a rebase -- so they are listed + explicitly, to be found by whoever goes looking. */ + + /DISCARD/ : + { + *(.ARM.exidx*) + *(.ARM.extab*) + *(.comment) + *(.note.*) + *(.dynsym) + *(.dynstr) + *(.hash) + *(.gnu.hash) + *(.dynamic) + *(.interp) + *(.rel.dyn) + *(.rel.plt) + *(.plt) + } +} diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds new file mode 100644 index 000000000..a8962763e --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds @@ -0,0 +1,344 @@ +/*************************************************************************** + * 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 + * " 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 + + /* The shared granules the module reports its progress through, and the one + it is not allowed to reach. + + FIRST in the module area, so the base address is ORIGIN(MODULE) and the + module can carry it as a constant -- the manager deliberately knows no + symbol of the module and has no channel through which to tell it anything. + S32Z_MODULE_STATUS_BASE in platform.h is the same number, and the ASSERTs + at the bottom of this file pin the two together at link time. + + Six granules. Five are granted, one per shared entry the port provides, + because granting a single region only ever exercises the first of the five; + the sixth -- index 2, so a granted granule sits on either side of it -- is + never granted, and the module writes it to prove it still faults. + + NOLOAD: the manager clears the area before every pass, and a granule that + arrived pre-filled would make a module that never wrote one look as though + it had. */ + + .module_status (NOLOAD) : + { + __module_status_start__ = .; + . = . + 0x180; /* six 64-byte granules */ + . = ALIGN(64); + __module_status_end__ = .; + } > MODULE + + /* The module image, in the module area, just above the shared granules. + + The module is not linked here. It is built as its own image by the + s32z280_demo_module.elf target, objcopied to a raw binary and included as + bytes by module_blob.S, and this section only decides where those bytes + land. Which address that is does not matter any more: since the module + became relocatable it is linked against nominal addresses it never runs + at and _gcc_setup rebases it to wherever it was put, so this only has to + be inside the module area and disjoint from everything else in it. + + The earlier arrangement compiled the module's sources into this image and + gathered them here by object name. That cannot work. The module library + defines shims named after the same ThreadX entry points the kernel + defines, and in a single link the shims win, because they arrive as + objects and the kernel arrives as a static library. The manager's own + tx_thread_create resolved to the module's copy at 0x317F1700, called into + the module area that no kernel region covers, and took a silent prefetch + abort before the first thread ever ran. + + Keeping the module out of the link also removes a whole class of quiet + failure that arrangement had: matching input sections by object-file name + fails open. A missing suffix or a wildcard that does not match produces an + empty section, a link that succeeds and an image with no module in it, + twice over here -- and the block had to sit before .text, because + placement is first match and the kernel's *(.text*) would otherwise + swallow the module's code while leaving its data behind. + + This block still sits before .text: harmless now, and it keeps the module + at the head of the map where it is easy to find. */ + + .module_image : + { + __module_image_start__ = .; + KEEP(*(.module_blob)) + . = ALIGN(64); + __module_image_end__ = .; + } > MODULE + + /* The pool the manager allocates module data from, in the module area for + the same reason the image is: whatever the manager hands to a module + becomes an MPU region, and PMSAv8-R will not allow that region to overlap + a kernel one. Anything a module can be given has to live outside every + region in the boot table, and this area is the only such memory. + + NOLOAD because nothing is copied in -- the byte pool writes its own + headers when it is created. */ + + .module_pool (NOLOAD) : + { + . = ALIGN(64); + __module_pool_start__ = .; + *(.module_pool) + . = ALIGN(64); + __module_pool_end__ = .; + } > MODULE + + /* A second place to load the same module from, which is how this example + proves the module is relocatable rather than merely built to be. + + A module that runs correctly at the address it was linked for proves + nothing about relocation -- the GOT rebase would produce the same + addresses it started with and a broken rebase would look identical. So + the manager copies the blob here and loads it a second time, and the two + runs are compared. Any address in the module area would do; what matters + is that it is not __module_image_start__ and that it does not overlap the + pool, because the code region of the second instance and the data regions + the pool hands out are live at the same time and PMSAv8-R does not permit + two enabled regions to overlap. + + NOLOAD, and deliberately not initialised at link time: if the blob were + placed here by the linker as well, a failure to copy it would be invisible + because the right bytes would already be present. */ + + .module_stage (NOLOAD) : + { + . = ALIGN(64); + __module_stage_start__ = .; + . = . + 0x1000; + . = ALIGN(64); + __module_stage_end__ = .; + } > MODULE + + /* The code and data sizes are not computed here any more. They belong to + the module's own link, which is where its preamble is assembled and where + the boundary between its code and its data is actually known. */ + + .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 + + /* The module's gateway into the kernel, deliberately after __code_end__ and + therefore outside the kernel code region, which ends there. A module gets + its own region covering exactly this function; that region would otherwise + overlap the kernel's, which PMSAv8-R does not allow. + + Alone in its own section on purpose: anything sharing it would become + executable by every module. 64-byte aligned at both ends, the PMSAv8-R + granule, so a region can start and stop on it without covering + neighbours. */ + + .txm_user_entry : ALIGN(64) + { + __txm_user_entry_start__ = .; + KEEP(*(.txm_user_entry)) + . = ALIGN(64); + __txm_user_entry_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 = .); + + /* The module carries the status base as a constant and platform.h names it + as S32Z_MODULE_STATUS_BASE. Neither can see this script, so the agreement + is pinned here; the manager checks the same thing again at run time + against the symbol, so a build that skipped this file still says so. */ + + ASSERT(__module_status_start__ == 0x317F0000, + "the module status area is not at S32Z_MODULE_STATUS_BASE") + + /* And its size: the manager grants one region per granule from that base and + the module addresses them by index, so an area that is not exactly + S32Z_MODULE_STATUS_GRANULES granules long would leave a grant, or a module + write, outside it. */ + + ASSERT(__module_status_end__ - __module_status_start__ == 0x180, + "the module status area is not S32Z_MODULE_STATUS_GRANULES granules long") +} diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/module_blob.S b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/module_blob.S new file mode 100644 index 000000000..bd4045422 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/module_blob.S @@ -0,0 +1,65 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* module_blob.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* Carries the demonstration module into the manager image as data. */ +@/* */ +@/* The module is built as its own link unit and objcopied to a raw */ +@/* binary, which is included here verbatim. It has to be a separate */ +@/* link: the module library defines shims named after the ThreadX API */ +@/* entry points the kernel also defines, and in one link the shims win */ +@/* -- objects beat archive members -- so the manager's own service calls */ +@/* end up trapping into the module. */ +@/* */ +@/* Included as bytes rather than linked as objects, so the module's */ +@/* symbols never enter the manager's link at all. Nothing here is */ +@/* called: the manager finds the preamble at the start of the image and */ +@/* reaches everything else through that. */ +@/* */ +@/**************************************************************************/ + + .section .module_blob, "a" + +@ 64-byte aligned, the PMSAv8-R granule, so the image starts where a region +@ can start. The linker script places this section at the module area base, +@ which is the address the module itself was linked for. + + .align 6 + + .global __demo_module_image + .global __demo_module_image_end + +__demo_module_image: + +@ demo_module.bin is produced by the s32z280_demo_module.elf target. The +@ assembler finds it through an include path pointing at the build directory, +@ set in CMakeLists.txt -- .incbin searches the -I paths, not the source tree. + + .incbin "demo_module.bin" + +__demo_module_image_end: + + .align 6 diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c new file mode 100644 index 000000000..dbf0c8a82 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c @@ -0,0 +1,319 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* SAMPLE MODULE RELEASE */ +/* */ +/* sample_threadx_module.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* A module that exercises the protection boundary rather than */ +/* demonstrating features, for the NXP S32Z280-594EVB. */ +/* */ +/* The FVP copy of this file is the same module for the same port; what */ +/* differs is the two addresses at the bottom and the board named here, */ +/* so `diff` is the tool for telling whether the two have drifted. */ +/* */ +/* Four steps: */ +/* */ +/* 1. Writes and reads its own data, which must succeed. */ +/* 2. Makes a kernel call, which must succeed -- proving a module in */ +/* User mode can reach the kernel through the supervisor call */ +/* boundary and come back. */ +/* 3. Violates its protection in one of three ways the manager */ +/* selects, which must fault. */ +/* 4. Never reaches step 4, because step 3 terminates it. */ +/* */ +/* THE THREE VIOLATIONS. Two of them are the two aborts the hardware */ +/* distinguishes: reading the kernel's data is a DATA abort reported */ +/* through DFSR and DFAR, branching out of the code region is a */ +/* PREFETCH abort reported through IFSR and IFAR. The third writes a */ +/* granule of the SHARED area that the manager deliberately did not */ +/* grant, after writing and reading back every granule it did -- so it */ +/* is the shared-region machinery under test rather than the kernel's */ +/* own memory, and a grant that covered one granule too many is what it */ +/* is looking for. */ +/* */ +/* Steps 1 and 2 passing without step 3 faulting would mean the module */ +/* is running unprotected, which is the failure this example exists to */ +/* detect. A module that only ever touched its own memory would pass */ +/* identically with the MPU switched off. */ +/* */ +/* HOW PROGRESS GETS OUT. A module cannot print: the console belongs */ +/* to the board support package, outside every region a module owns, so */ +/* reaching it would fault as surely as step 3 does. So progress is */ +/* recorded twice -- in the module's own data, and in the first granule */ +/* of the shared area the manager granted it. */ +/* */ +/* Which of the two can be read depends on the board. On silicon a GDB */ +/* harness reads the module's own copy out of the data area the manager */ +/* allocated for it; the FVP has no such seam -- it exposes an Iris */ +/* server and no GDB stub -- so there only the shared copy is readable */ +/* and everything the run reports has to be reported by the image */ +/* itself. Both writes are kept on both boards deliberately: if the */ +/* shared write were the only one, a module that could not reach its own */ +/* data would still report progress. */ +/* */ +/* The shared address is a literal on this side. The module has no */ +/* loader to tell it anything and the manager deliberately knows no */ +/* symbol of the module, so the two agree by convention -- and the */ +/* manager checks that they do, against the linker's own symbol, rather */ +/* than trusting them to. */ +/* */ +/**************************************************************************/ + +#include "txm_module.h" + +/* Progress, recorded in two places. See the header: the shared copy is what + the manager reads, and the module's own copy is what proves it could write + its own data at all. */ + +#define MODULE_PROGRESS_OWN_DATA 0x00000001UL +#define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL +#define MODULE_PROGRESS_ATTEMPTED_STEAL 0x00000004UL +#define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL +#define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL +#define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL +#define MODULE_PROGRESS_SHARED_WROTE 0x00000040UL +#define MODULE_PROGRESS_ATTEMPTED_GAP 0x00000080UL +#define MODULE_PROGRESS_SURVIVED_GAP 0x00000100UL + +/* Which violation to commit, taken from the low byte of the module ID the + manager passes to the start thread. The rest of that word is left alone: the + preamble ships it as 0x52520001, so a manager that sets nothing still gets a + working data-abort test rather than a module that does nothing. */ + +#define MODULE_TEST_MASK 0x000000FFUL +#define MODULE_TEST_DATA_ABORT 0x00000001UL +#define MODULE_TEST_PREFETCH_ABORT 0x00000002UL +#define MODULE_TEST_SHARED_ABORT 0x00000003UL + +/* The shared status word, at the base of the module area. + + A literal address rather than anything reached through the GOT, because it is + not the module's own memory: it is memory the manager granted, and the module + has no channel through which to be told where it is. The same number is + S32Z_MODULE_STATUS_BASE in platform.h and ORIGIN(MODULE) in link_module.lds; + the manager checks all three agree before it starts this module, so a + disagreement is a reported failure rather than a fault at the first write. + + MISRA C:2012 Rule 11.6 (conversion between an integer and a pointer to void) + is deliberately violated: an address agreed between two separately linked + images can only be written as a literal, and there is no conforming way to + express it. */ + +#define MODULE_STATUS_ADDRESS 0x317F0000UL + +#define MODULE_STATUS (*((volatile ULONG *) MODULE_STATUS_ADDRESS)) + +/* And the rest of the shared area, which exists to exercise the shared-region + machinery rather than to report anything. + + The manager may grant a module TXM_MODULE_MPU_SHARED_ENTRIES regions and one + grant only ever proves the first entry works, so the area holds one granule + per entry -- five granted, and a sixth that the manager never grants. The + ungranted one is index 2, which puts a granted granule on either side of it: + a limit register masked the wrong way, or a base off by a granule, leaks into + that gap from below or from above, and either way a module that can write it + was given more than was asked for. + + The mark for each granule goes at WORD 1, because word 0 of granule 0 is the + progress word above. Uniform across all six so the address the gap write + faults on is one expression on both sides of the agreement. */ + +#define MODULE_STATUS_GRANULE 0x40UL +#define MODULE_STATUS_GRANULES 6UL +#define MODULE_STATUS_UNGRANTED 2UL +#define MODULE_STATUS_MARK_OFFSET 4UL + +#define MODULE_SHARED_SIGNATURE 0x5A5A0000UL + +#define MODULE_SHARED_MARK(index) \ + (*((volatile ULONG *) (MODULE_STATUS_ADDRESS \ + + ((index) * MODULE_STATUS_GRANULE) \ + + MODULE_STATUS_MARK_OFFSET))) + +volatile ULONG module_progress; +volatile ULONG module_scratch[16]; + +/* An address the module has no business touching, used by step 3. + + The base of DRAM0, where the manager's own data and stacks live on this + board: that region is EL1 read/write and execute-never, so a User-mode read + of it is a permission fault and a User-mode instruction fetch from it is one + too. Both are what step 3 needs, and both prove the KERNEL'S memory is what + a module cannot reach -- which is the property being demonstrated, rather + than the absence of a mapping. + + Held in initialised data rather than written as a literal, deliberately and + unlike the status address above. The module reads it through its rebased + GOT, so the value arriving in DFAR or IFAR also proves the GOT was rewritten + and .data was copied. The manager knows the same number and checks it. */ + +volatile ULONG module_forbidden_address = 0x31780000UL; + + +/* Declared as well as defined because the only caller is assembly: + txm_module_preamble.S names it with .extern and stores its offset in the + START entry-point word. An assembly caller supplies no prototype, so + without this the definition has external linkage and no visible + declaration -- prohibited by MISRA C:2012 Rule 8.4 and reported by + -Wmissing-prototypes. Same reason the board support has board.h. */ + +void demo_module_start(ULONG id); + + +void demo_module_start(ULONG id) +{ + ULONG i; + ULONG sum = 0UL; + ULONG shared_ok = 1UL; + + /* 1. The module's own data. If this faults, the data region is wrong and + nothing else in this file will be reached. */ + + for (i = 0UL; i < 16UL; i++) + { + module_scratch[i] = i * 3UL; + } + + for (i = 0UL; i < 16UL; i++) + { + sum += module_scratch[i]; + } + + if (sum == 360UL) /* 3 * (0 + 1 + ... + 15) */ + { + module_progress |= MODULE_PROGRESS_OWN_DATA; + MODULE_STATUS |= MODULE_PROGRESS_OWN_DATA; + } + + /* 2. A kernel call, which leaves User mode through the supervisor call + boundary and must come back. A sleep is used because it is the + simplest service with an observable effect and it yields, so the + scheduler runs a module thread and reloads its regions on the way + back -- exercising the region switch as well as the call. */ + + if (tx_thread_sleep(2UL) == TX_SUCCESS) + { + module_progress |= MODULE_PROGRESS_KERNEL_CALL; + MODULE_STATUS |= MODULE_PROGRESS_KERNEL_CALL; + } + + /* 3. The violation, and 4. the flag that says it was tolerated. Each is + marked before the access rather than after, because after is not + reached if the port is working. The SURVIVED flags are what the + manager fails on: they can only appear if the hardware allowed an + access it was configured to refuse. */ + + if ((id & MODULE_TEST_MASK) == MODULE_TEST_SHARED_ABORT) + { + /* Write every granule the manager granted, read all of them back, and + then write the one it did not grant. + + The readback matters as much as the write. A shared entry programmed + with the wrong base still accepts a store -- it just lands somewhere + else -- so a write that is never read back proves only that the core + did not object. Reading each granule's own mark out of its own + granule is what says the five entries describe five distinct + extents. */ + + for (i = 0UL; i < MODULE_STATUS_GRANULES; i++) + { + if (i != MODULE_STATUS_UNGRANTED) + { + MODULE_SHARED_MARK(i) = MODULE_SHARED_SIGNATURE | i; + } + } + + for (i = 0UL; i < MODULE_STATUS_GRANULES; i++) + { + if ((i != MODULE_STATUS_UNGRANTED) && + (MODULE_SHARED_MARK(i) != (MODULE_SHARED_SIGNATURE | i))) + { + shared_ok = 0UL; + } + } + + if (shared_ok != 0UL) + { + module_progress |= MODULE_PROGRESS_SHARED_WROTE; + MODULE_STATUS |= MODULE_PROGRESS_SHARED_WROTE; + } + + /* The gap. A WRITE rather than a read, so DFSR reports WnR set and the + fault cannot be confused with the read the data-abort passes do. It + lies between two granules this module was granted, so surviving it + means a grant covered a granule nobody asked for -- which is the + whole reason this pass exists. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_GAP; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_GAP; + + MODULE_SHARED_MARK(MODULE_STATUS_UNGRANTED) = + MODULE_SHARED_SIGNATURE | MODULE_STATUS_UNGRANTED; + + module_progress |= MODULE_PROGRESS_SURVIVED_GAP; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_GAP; + } + else if ((id & MODULE_TEST_MASK) == MODULE_TEST_PREFETCH_ABORT) + { + /* Branch outside the code region: a prefetch abort, reported through + IFSR and IFAR. Nothing is executed at the target -- the fault is on + the fetch itself, so the thread never arrives. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_JUMP; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_JUMP; + + /* MISRA C:2012 Rule 11.1 (no conversion between a pointer to a function + and any other type) is deliberately violated here, and Rule 11.6 + (no conversion between an integer and a pointer to void) with it. + Provoking a fetch from an address that holds no function is the entire + purpose of these three lines; there is no conforming way to write it. */ + + ((void (*)(void)) module_forbidden_address)(); + + module_progress |= MODULE_PROGRESS_SURVIVED_JUMP; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_JUMP; + } + else + { + /* Read outside the data region: a data abort, reported through DFSR and + DFAR. */ + + module_progress |= MODULE_PROGRESS_ATTEMPTED_STEAL; + MODULE_STATUS |= MODULE_PROGRESS_ATTEMPTED_STEAL; + + sum += *((volatile ULONG *) module_forbidden_address); + + module_progress |= MODULE_PROGRESS_SURVIVED_STEAL; + MODULE_STATUS |= MODULE_PROGRESS_SURVIVED_STEAL; + } + + /* Keep the compiler from discarding the read above. */ + + module_scratch[0] = sum; + + while (1) + { + (void) tx_thread_sleep(100UL); + } +} diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c new file mode 100644 index 000000000..f0c3949a9 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c @@ -0,0 +1,1513 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* MODULE MANAGER SAMPLE RELEASE */ +/* */ +/* sample_threadx_module_manager.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Loads the sample module four times, lets it misbehave every time, */ +/* and reports what the hardware did about it. */ +/* */ +/* The result this example exists to produce is the fault. A module */ +/* that starts and runs proves the loader works; a module that is */ +/* stopped by the memory protection unit when it reaches outside its */ +/* own memory proves the port works. So the fault notification is not */ +/* an error path here, it is the expected outcome, and its absence is */ +/* the failure. */ +/* */ +/* TWO passes, because one proves nothing about relocation. The module */ +/* is position independent: it is linked against nominal addresses it */ +/* never runs at, and _gcc_setup rewrites its global offset table to */ +/* wherever the manager actually put it. A single run at the linked */ +/* address would exercise a rebase whose input and output are the same */ +/* number, and would look identical if the rebase did nothing at all. */ +/* */ +/* So pass 1 loads the blob where the linker placed it and pass 2 loads */ +/* a byte-for-byte copy of it from the staging area, with pass 1 still */ +/* holding its pool memory so that pass 2's data lands somewhere else */ +/* too. Both the code base and the data base therefore differ between */ +/* the passes, which is what makes the comparison at the end mean */ +/* something: */ +/* */ +/* * the same instruction faults in both passes -- equal offsets from */ +/* each pass's own code base, at two different absolute addresses. */ +/* The module ran relocated. */ +/* * DFAR is the forbidden address in both passes. The module got */ +/* that value by reading one of its own initialised globals through */ +/* the rebased GOT, so this single register proves the GOT was */ +/* rewritten and .data was copied. */ +/* * SPSR says User mode in both passes. The boundary held. */ +/* */ +/* All of which is checked on the console, with no help from a debugger */ +/* and without the manager needing to know one symbol of the module. */ +/* */ +/* THEN A THIRD PASS, which faults the other way. The two passes above */ +/* make the module read an address it does not own: a data abort, */ +/* reported through DFSR and DFAR. A module can equally leave its code */ +/* region, which is a prefetch abort reported through IFSR and IFAR and */ +/* arrives at the handler by a different vector. Both halves of the */ +/* port's fault path are therefore exercised, and neither is inferred */ +/* from the other. */ +/* */ +/* Which violation a pass commits is chosen here, not in the module: the */ +/* manager writes the application-defined module ID in the instance */ +/* after loading it, and the manager passes that word to the module's */ +/* start thread. So one blob covers both cases and the manager needs no */ +/* symbol of the module to select between them. */ +/* */ +/* The third pass runs after the first two have been unloaded, which is */ +/* the other half of what this file demonstrates: a module fault must */ +/* leave the manager able to load and run the next module. A fault that */ +/* kills the manager is not isolation, and a fault that leaves the MPU */ +/* in a state where the next load misbehaves is not either. */ +/* */ +/* AND THE NOTIFICATION IS CHECKED, not merely printed. The manager */ +/* registers a fault-notify callback, and every pass must see it run */ +/* exactly once with the faulting thread and the right module instance. */ +/* That path used to be dead on this port -- the shared fault handler */ +/* terminates the thread before calling the hook, which only returns if */ +/* the port's abort vector tells the kernel it is inside an exception, */ +/* and this port's did not. It does now, so the hook is a result rather */ +/* than a known defect. */ +/* */ +/* AND A FOURTH PASS FOR THE SHARED REGIONS. The three above are each */ +/* granted one shared region -- the status granule they report their */ +/* progress through -- which exercises the first of the five shared */ +/* entries the port provides and says nothing about the other four. */ +/* The fourth pass is granted all five, one 64-byte granule each, and */ +/* is NOT granted the granule that sits between two of them. It writes */ +/* every granule it was given, reads every one of them back, and then */ +/* writes the gap, which must fault. */ +/* */ +/* That shape is chosen against a specific defect. A limit register */ +/* masked the wrong way, or a base off by one granule, extends a region */ +/* past what was asked for -- and with the gap sandwiched between two */ +/* granted granules it is reachable from either side if that happens. */ +/* The readback matters as much as the write: a region programmed with */ +/* the wrong base accepts a store and puts it elsewhere, so five marks */ +/* read out of five granules is what says five distinct extents were */ +/* programmed rather than one of them five times. */ +/* */ +/* The same pass probes the two ways the manager refuses a grant, which */ +/* nothing had ever called: an unaligned address must come back */ +/* TXM_MODULE_ALIGNMENT_ERROR, and one grant past the entry count must */ +/* come back TX_NO_MEMORY. Both are checked by name. The order is not */ +/* free -- the entry-count check runs before the alignment check, so the */ +/* unaligned probe has to happen while entries remain. */ +/* */ +/* A SECOND READING OF THE PROGRESS WORD comes with those granules. The */ +/* module records what it managed twice: in its own data, which the GDB */ +/* harness reads, and in the first shared granule, which this file now */ +/* reads on the target. The two are independent readings of the same */ +/* event, so the console judges progress without a debugger and the */ +/* harness still cross-checks it against the module's own copy. */ +/* */ +/* What the module image is and where it comes from: it is linked into */ +/* this application as a separate section and loaded in place, so */ +/* nothing is copied and no filesystem or download path is needed. */ +/* The manager still maps it with its own MPU regions, which is what */ +/* matters -- loading in place changes where the code lives, not */ +/* whether it is protected. */ +/* */ +/**************************************************************************/ + +#include "tx_api.h" +#include "txm_module.h" +#include "linflexd.h" +#include "platform.h" +#include "thread_mpu.h" +#include "cache.h" + +/* For bsp_main, which entry.S calls and therefore never declares. Every + other example in the board support package includes this header for the + same reason; MISRA C:2012 Rule 8.4 wants the declaration visible at the + definition, and -Wmissing-prototypes reports it when it is not. */ + +#include "board.h" + +/* The fault information the abort vector captured. Declared here because the + module manager expands it into the port's fault handler through the + TXM_MODULE_MANAGER_FAULT_INFO macro rather than declaring it in a header, so an + application that wants to read it has to say so itself. */ + +extern TXM_MODULE_MANAGER_MEMORY_FAULT_INFO _txm_module_manager_memory_fault_info; + +/* Where the module image sits. Provided by the linker script, which places the + module's preamble first so this address is also the preamble address. */ + +extern unsigned char __module_image_start__; +extern unsigned char __module_image_end__; + +/* The second address the same blob is loaded from, so relocation can be shown + rather than assumed. Reserved by the linker script and left empty by it -- + see .module_stage in link_module.lds for why it is not pre-filled. */ + +extern unsigned char __module_stage_start__; +extern unsigned char __module_stage_end__; + +/* The shared granules the module reports its progress through, and the one it + is not allowed to reach. See S32Z_MODULE_STATUS_BASE in platform.h. */ + +extern unsigned char __module_status_start__; +extern unsigned char __module_status_end__; + +/* The address the module is going to reach for and must not be allowed to have. + Declared here only so the console check can name it; the value lives in the + module's own initialised data, which is the point -- the module reads it + through its rebased GOT, so seeing it arrive in DFAR proves the rebase and + the .data copy both worked. Kept in step with sample_threadx_module.c by + hand, and checked at run time rather than trusted: if the two ever disagree + the relocation verdict says so instead of quietly passing. */ + +#define MODULE_FORBIDDEN_ADDRESS 0x31780000UL + +/* The shared area. The same numbers are S32Z_MODULE_STATUS_* in platform.h, + ORIGIN(MODULE) in link_module.lds and MODULE_STATUS_ADDRESS in the module; + the ASSERTs in the link script pin three of them together at link time and + the manager checks the linker's symbol at run time rather than trusting it. + + MODULE_STATUS_LENGTH is one granule and also the length of one grant. The + area is MODULE_STATUS_GRANULES of them: five the shared pass is granted, one + entry each, and MODULE_STATUS_UNGRANTED -- index 2, so a granted granule sits + on either side of it -- which is never granted to anything. Each granule's + mark goes at word 1, because word 0 of granule 0 is the progress word. + + MISRA C:2012 Rule 11.6 is deliberately violated by the accessors: an address + agreed between two separately linked images can only be written as a literal + and there is no conforming way to express it. */ + +#define MODULE_STATUS_ADDRESS S32Z_MODULE_STATUS_BASE +#define MODULE_STATUS_LENGTH S32Z_MODULE_STATUS_SIZE +#define MODULE_STATUS_GRANULES S32Z_MODULE_STATUS_GRANULES +#define MODULE_STATUS_UNGRANTED S32Z_MODULE_STATUS_UNGRANTED +#define MODULE_STATUS_AREA_LENGTH (MODULE_STATUS_GRANULES * MODULE_STATUS_LENGTH) +#define MODULE_STATUS_MARK_OFFSET 4UL + +#define MODULE_SHARED_SIGNATURE 0x5A5A0000UL + +/* How many grants the shared pass makes, which is every entry the port + provides -- granting fewer would leave an entry unexercised, and that the + other four work at all is what the pass exists to establish. */ + +#define MODULE_SHARED_GRANTS (MODULE_STATUS_GRANULES - 1UL) + +#define MODULE_GRANULE_ADDRESS(index) \ + (MODULE_STATUS_ADDRESS + ((index) * MODULE_STATUS_LENGTH)) + +#define MODULE_SHARED_MARK(index) \ + (*((volatile ULONG *) (MODULE_GRANULE_ADDRESS(index) \ + + MODULE_STATUS_MARK_OFFSET))) + +/* The progress bits the module sets, mirrored from sample_threadx_module.c. + The three SURVIVED flags can only ever be set if the hardware allowed an + access it was configured to refuse, so they are checked for by name. */ + +#define MODULE_PROGRESS_OWN_DATA 0x00000001UL +#define MODULE_PROGRESS_KERNEL_CALL 0x00000002UL +#define MODULE_PROGRESS_ATTEMPTED_STEAL 0x00000004UL +#define MODULE_PROGRESS_SURVIVED_STEAL 0x00000008UL +#define MODULE_PROGRESS_ATTEMPTED_JUMP 0x00000010UL +#define MODULE_PROGRESS_SURVIVED_JUMP 0x00000020UL +#define MODULE_PROGRESS_SHARED_WROTE 0x00000040UL +#define MODULE_PROGRESS_ATTEMPTED_GAP 0x00000080UL +#define MODULE_PROGRESS_SURVIVED_GAP 0x00000100UL + +#define MODULE_PROGRESS_SURVIVED (MODULE_PROGRESS_SURVIVED_STEAL | \ + MODULE_PROGRESS_SURVIVED_JUMP | \ + MODULE_PROGRESS_SURVIVED_GAP) + +/* Which violation a pass tells the module to commit. Written into the module + instance's application-defined ID after the load and before the start, because + that word is what the manager hands the module's start thread. The high half + is the fingerprint the preamble ships; only the low byte selects the test, so a + module started without this still runs the data-abort case. */ + +#define MODULE_ID_BASE 0x52520000UL +#define MODULE_TEST_DATA_ABORT 0x00000001UL +#define MODULE_TEST_PREFETCH_ABORT 0x00000002UL +#define MODULE_TEST_SHARED_ABORT 0x00000003UL + +/* Memory the manager hands out to modules: object memory, and the data region a + module's own variables live in. It has to be outside every region a module is + given, or a module could reach another module's data. */ + +#ifndef MODULE_POOL_SIZE +#define MODULE_POOL_SIZE (16U * 1024U) +#endif + +/* In the module area, not in .bss. The manager carves a module's data out of + this pool, and that data is handed to the module as an MPU region -- so if the + pool lived in .bss it would sit inside the kernel's data region and the two + would overlap, which PMSAv8-R does not allow. Keeping the pool in the module + area is what makes a module's data disjoint from every kernel region. */ + +static unsigned char module_pool[MODULE_POOL_SIZE] + __attribute__((aligned(64), section(".module_pool"))); + +/* The object pool, which is a separate allocation from the module pool above and + is not optional here. A module that runs in User mode needs a kernel stack + for the privileged side of each system call, and the manager allocates that + from this pool -- txm_module_manager_initialize deliberately leaves the pool + uncreated, so without this a User-mode module fails to start with + TX_NOT_AVAILABLE from deep inside the thread create, which is not an obvious + way to be told that a pool is missing. */ + +#ifndef MODULE_OBJECT_POOL_SIZE +#define MODULE_OBJECT_POOL_SIZE (8U * 1024U) +#endif + +static unsigned char module_object_pool[MODULE_OBJECT_POOL_SIZE] + __attribute__((aligned(64))); + +/* One instance per pass. Passes 1 and 2 are loaded at once for part of the run: + pass 1 is stopped but still holds its data allocation while pass 2 loads, which + is what pushes pass 2's data base somewhere different. Pass 3 runs after both + have been unloaded, which is deliberate -- see the header. */ + +#define MODULE_PASSES 4U +#define MODULE_RELOCATION_PASSES 2U + +static TXM_MODULE_INSTANCE demo_module[MODULE_PASSES]; + +/* What each pass produced. Recorded rather than printed as it happens, because + the comparison between the two passes is the actual result and it cannot be + made until both have run. */ + +typedef struct PASS_RESULT_STRUCT +{ + CHAR *pass_name; + ULONG pass_test; /* MODULE_TEST_*: which abort is expected */ + ULONG pass_load_status; + ULONG pass_share_status; + ULONG pass_start_status; + ULONG pass_stop_status; + ULONG pass_code_start; + ULONG pass_code_end; + ULONG pass_data_start; + ULONG pass_data_end; + ULONG pass_data_base; + ULONG pass_faults; + ULONG pass_captured; + ULONG pass_fault_r9; + ULONG pass_dfsr; + ULONG pass_dfar; + ULONG pass_ifsr; + ULONG pass_ifar; + ULONG pass_spsr; + ULONG pass_code_location; + + /* What the module reported through the shared granules. Read on the target + rather than only by the debug harness: the harness reads the module's own + copy of the same word out of its data area, so the two are independent + readings of the same event and a disagreement is worth seeing. */ + + ULONG pass_progress; + ULONG pass_expect_progress; + + /* The shared-region results. Only the shared pass fills these in, so they + are judged only for that pass -- zero is TX_SUCCESS, and a pass that never + probed would otherwise read as one that probed and was refused nothing. */ + + ULONG pass_shared_grants; /* grants that succeeded */ + ULONG pass_shared_count; /* entries the instance holds after */ + ULONG pass_align_status; /* refusing an unaligned grant */ + ULONG pass_exhaust_status; /* refusing one grant too many */ + ULONG pass_marks[S32Z_MODULE_STATUS_GRANULES]; + + /* The address this pass must fault on. Per pass rather than one constant, + because the shared pass faults on the granule it was not granted and the + other three fault on the kernel's data. */ + + ULONG pass_expect_fault; + + /* What the notify callback was told, against what it should have been told. + Recorded as plain words rather than pointers so the console can print them + and a mismatch names both values. */ + + ULONG pass_notify_thread; + ULONG pass_notify_instance; + ULONG pass_expect_thread; + ULONG pass_expect_instance; +} PASS_RESULT; + +static PASS_RESULT pass_results[MODULE_PASSES]; + +/* What the fault handler saw. Read by the reporting thread after the module has + been terminated. */ + +static volatile ULONG fault_count; +static volatile ULONG fault_dfsr; +static volatile ULONG fault_dfar; +static volatile ULONG fault_ifsr; +static volatile ULONG fault_ifar; +static volatile ULONG fault_spsr; +static volatile ULONG fault_code_location; +static volatile ULONG fault_notify_thread; +static volatile ULONG fault_notify_instance; + +static TX_THREAD report_thread; +static unsigned char report_stack[2048] __attribute__((aligned(8))); + +/* The two symbols the debug harness stops on, declared here because they have + external linkage and are called before they are defined -- MISRA C:2012 Rule + 8.4 wants a visible declaration either way. Both are defined below; see each + for why it exists rather than a line number in a loop. */ + +void manager_done(void); +void pass_done(void); + + +/**************************************************************************/ +/* Fault notification. */ +/* */ +/* Called by the module manager after it has terminated the offending */ +/* thread. Records rather than prints, for two reasons: this runs in the */ +/* fault path, where the console is a polled driver that spins waiting */ +/* for a transmit to complete -- and it runs in Abort mode on the Abort */ +/* stack, which is a kilobyte on this board and already carries the */ +/* terminate underneath this frame. A callback that printed would work */ +/* and would still be the wrong shape to copy. */ +/* */ +/* Its two arguments are the point of the hook, so they are recorded and */ +/* checked rather than discarded: an application is being told WHICH */ +/* thread and WHICH module faulted, and a callback that fires with the */ +/* wrong pair is no more use than one that never fires. */ +/**************************************************************************/ + +static void module_fault_notify(TX_THREAD *thread_ptr, TXM_MODULE_INSTANCE *module_instance) +{ + fault_count++; + fault_notify_thread = (ULONG) thread_ptr; + fault_notify_instance = (ULONG) module_instance; + fault_dfsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfsr; + fault_dfar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfar; + fault_ifsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifsr; + fault_ifar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifar; + fault_spsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_spsr; + fault_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location; +} + + +static void put_hex(unsigned long value) +{ + linflexd_put_hex32((unsigned int) value); +} + + +static void put_field(const char *label, unsigned long value) +{ + linflexd_puts(label); + put_hex(value); + linflexd_puts("\n"); +} + + +/**************************************************************************/ +/* A byte copy, written out rather than called for. */ +/* */ +/* The manager links -nostdlib, and reaching for memcpy would pull in a */ +/* libc whose presence this example does not otherwise depend on. The */ +/* blob is under two kilobytes and this runs once. */ +/**************************************************************************/ + +static void copy_bytes(unsigned char *destination, const unsigned char *source, + unsigned long length) +{ + unsigned long i; + + for (i = 0UL; i < length; i++) + { + destination[i] = source[i]; + } +} + + +/**************************************************************************/ +/* The shared granules, reached through the manager's load window. */ +/* */ +/* No kernel region covers the module area; region 16 does, and the */ +/* scheduler enables it for every thread that owns no module. This */ +/* thread owns none, so the window is open on it and these functions */ +/* need no bracketing of their own -- which is the whole reason the */ +/* window is owned by the scheduler rather than by whoever calls. */ +/**************************************************************************/ + +static void module_status_clear(void) +{ + ULONG granule; + + /* The WHOLE area, not just the progress word. The gap granule has to start + at a known zero, because "the module never wrote it" is one of the results + and a leftover mark from the previous pass would read as a module that + reached memory it was never granted -- or hide one that did. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + *((volatile ULONG *) MODULE_GRANULE_ADDRESS(granule)) = 0UL; + MODULE_SHARED_MARK(granule) = 0UL; + } +} + + +static ULONG module_status_read(void) +{ + return *((volatile ULONG *) MODULE_STATUS_ADDRESS); +} + + +/**************************************************************************/ +/* The shared grants a pass gets, and the two ways a grant is refused. */ +/* */ +/* Every pass is granted the first granule, which is the progress word it */ +/* reports through. The shared pass is granted one granule per shared */ +/* entry the port provides, skipping the gap, because a single grant only */ +/* ever exercises the first of the five and this port had never run the */ +/* other four. */ +/* */ +/* It also probes the two ways a grant is refused, which can only be done */ +/* on a LOADED instance. ORDER MATTERS: the manager checks the entry */ +/* count BEFORE it checks alignment, so the unaligned probe has to happen */ +/* while entries remain -- after five grants it would come back */ +/* TX_NO_MEMORY and say nothing about alignment at all. */ +/* */ +/* Returns the first grant status that was not TX_SUCCESS, or TX_SUCCESS. */ +/**************************************************************************/ + +static UINT grant_shared_regions(TXM_MODULE_INSTANCE *instance, PASS_RESULT *result) +{ + UINT status; + UINT first_failure = TX_SUCCESS; + ULONG granule; + + if (result -> pass_test != MODULE_TEST_SHARED_ABORT) + { + /* The progress word, and nothing else. */ + + return txm_module_manager_external_memory_enable(instance, + (VOID *) &__module_status_start__, + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + } + + /* Refused for alignment. One word into the first granule: an address + inside the area, so what is being tested is the 64-byte granule rule and + not the range. It must also consume no entry, which the five grants + below prove by all succeeding. */ + + result -> pass_align_status = + (ULONG) txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + MODULE_STATUS_MARK_OFFSET), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + /* One region per granule, every entry the port has, and never the gap. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule != MODULE_STATUS_UNGRANTED) + { + status = txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + (granule * MODULE_STATUS_LENGTH)), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + if (status == TX_SUCCESS) + { + result -> pass_shared_grants++; + } + else if (first_failure == TX_SUCCESS) + { + first_failure = status; + } + else + { + /* Already recorded; the first failure is the informative one. */ + } + } + } + + /* And one grant too many, which must be refused. Not cosmetic: the entry + written is TXM_MODULE_MPU_SHARED_INDEX plus the count, so a sixth grant + that got past the check would write one past the end of the region table + and into the instance fields that follow it. + + Aimed at the GAP, deliberately. It overlaps nothing, so a refusal costs + nothing -- and if the check ever failed, the gap would become a granted + region and the module would go on to survive writing it. The same defect + would then be reported twice, once as this status and once by name as + MODULE_PROGRESS_SURVIVED_GAP, rather than only as a status nobody reads. */ + + result -> pass_exhaust_status = + (ULONG) txm_module_manager_external_memory_enable( + instance, + (VOID *) (&__module_status_start__ + + (MODULE_STATUS_UNGRANTED * MODULE_STATUS_LENGTH)), + MODULE_STATUS_LENGTH, + TXM_MODULE_ATTRIBUTE_READ_WRITE); + + result -> pass_shared_count = instance -> txm_module_instance_shared_memory_count; + + return first_failure; +} + + +/**************************************************************************/ +/* One pass: load the blob from a given address, grant it the shared */ +/* granules it is entitled to, start it, wait for the fault it is written */ +/* to provoke, and stop it. */ +/* */ +/* The module is left loaded. Its data allocation is what moves the next */ +/* pass's data base, and releasing it here would defeat half the test. */ +/* Unloading happens after both passes have run. */ +/**************************************************************************/ + +/* name is CHAR * and not const CHAR *, because txm_module_manager_in_place_load + takes it that way -- the manager stores the pointer in the instance and the API + has never promised not to write through it. */ + +static void run_one_pass(UINT index, CHAR *name, VOID *location, ULONG test) +{ + PASS_RESULT *result = &pass_results[index]; + TXM_MODULE_INSTANCE *instance = &demo_module[index]; + unsigned int waited; + ULONG granule; + + result -> pass_name = name; + result -> pass_test = test; + + /* What the module should manage before the hardware stops it: its own data, + a kernel call, and the attempt -- plus, for the shared pass, the five + granules it wrote and read back on the way. Never a SURVIVED bit; that + one is the failure. + + The address it must fault on goes with it, because the three tests do not + share one. Two of them reach into the kernel's data; the shared pass + writes the mark word of the granule it was not granted, and that address + is computed here exactly as the module computes it. */ + + if (test == MODULE_TEST_SHARED_ABORT) + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_SHARED_WROTE + | MODULE_PROGRESS_ATTEMPTED_GAP; + + result -> pass_expect_fault = MODULE_GRANULE_ADDRESS(MODULE_STATUS_UNGRANTED) + + MODULE_STATUS_MARK_OFFSET; + } + else if (test == MODULE_TEST_PREFETCH_ABORT) + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_ATTEMPTED_JUMP; + + result -> pass_expect_fault = MODULE_FORBIDDEN_ADDRESS; + } + else + { + result -> pass_expect_progress = MODULE_PROGRESS_OWN_DATA + | MODULE_PROGRESS_KERNEL_CALL + | MODULE_PROGRESS_ATTEMPTED_STEAL; + + result -> pass_expect_fault = MODULE_FORBIDDEN_ADDRESS; + } + + /* Cleared before the module starts, so a fault or a progress bit counted + here belongs to this pass and not the previous one. */ + + fault_count = 0UL; + fault_notify_thread = 0UL; + fault_notify_instance = 0UL; + module_status_clear(); + + result -> pass_load_status = (ULONG) txm_module_manager_in_place_load(instance, name, location); + + if (result -> pass_load_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Read from the instance rather than computed here. Where the code ended up + is what the manager decided, and the whole point of the comparison at the + end is to check the module against the manager's own numbers. + + Recorded HERE, immediately after the load and before anything that can + fail, because the load is what decided them. Taken any later, a pass that + failed to be granted its shared regions would carry zeros into the overlap + comparison at the end of the run -- and zero against zero overlaps, so a + grant failure would be reported a second time as "its code and data + regions overlap", which is not true and points at the wrong thing. */ + + result -> pass_code_start = (ULONG) instance -> txm_module_instance_code_start; + result -> pass_code_end = (ULONG) instance -> txm_module_instance_code_end; + result -> pass_data_start = (ULONG) instance -> txm_module_instance_data_start; + result -> pass_data_end = (ULONG) instance -> txm_module_instance_data_end; + result -> pass_data_base = (ULONG) instance -> txm_module_instance_module_data_base_address; + + /* The shared granules the module reports through, and for the shared pass + every other entry as well. Granted while the module is LOADED and before + it is STARTED, which is the only window the manager accepts, and granted + per pass because each pass is its own instance with its own region table + -- an instance is memset by the load, so nothing carries over. */ + + result -> pass_share_status = (ULONG) grant_shared_regions(instance, result); + + if (result -> pass_share_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Which violation this pass provokes. Written after the load, which is what + filled the field in from the preamble, and before the start, which is what + hands it to the module's start thread. */ + + instance -> txm_module_instance_application_module_id = MODULE_ID_BASE | test; + + /* What the notify callback must be told, recorded before the module runs so + the comparison afterwards is against an expectation and not against + whatever the callback happened to write. */ + + result -> pass_expect_thread = (ULONG) &(instance -> txm_module_instance_start_stop_thread); + result -> pass_expect_instance = (ULONG) instance; + + result -> pass_start_status = (ULONG) txm_module_manager_start(instance); + + if (result -> pass_start_status != (ULONG) TX_SUCCESS) + { + return; + } + + /* Wait for the abort vector to capture a fault belonging to THIS pass. + + The captured fault info is waited on rather than the notify callback, even + though the callback now works. The capture happens in the abort vector + before anything else runs, so it is the earliest and most direct evidence + that a fault occurred; the callback is a later consequence of the same + event, and this file checks it separately. Waiting on the earlier of the + two means a regression in the callback path shows up as "notified 0" next + to a captured fault, rather than as "no fault occurred" -- which is the + opposite of the truth and is exactly what an earlier version of this file + reported. + + Attribution is by thread pointer: each pass has its own module instance and + therefore its own start thread, so a captured fault naming this pass's + thread cannot be a leftover from the previous pass. */ + + waited = 0U; + while ((_txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_thread_ptr + != &(instance -> txm_module_instance_start_stop_thread)) && + (waited < 50U)) + { + tx_thread_sleep(2UL); + waited++; + } + + if (_txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_thread_ptr + == &(instance -> txm_module_instance_start_stop_thread)) + { + result -> pass_captured = 1UL; + result -> pass_dfsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfsr; + result -> pass_dfar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfar; + result -> pass_ifsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifsr; + result -> pass_ifar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifar; + result -> pass_spsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_spsr; + result -> pass_fault_r9 = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_r9; + result -> pass_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location; + } + + /* What the module reached, and what the notify callback saw. Both read + after the wait above, so what ran is recorded and what did not shows as + zero. The status word is read while THIS pass is still the last thing to + have written it -- the same discipline pass_done() exists for on the + module's own copy, for the same reason. */ + + result -> pass_progress = module_status_read(); + result -> pass_faults = fault_count; + result -> pass_notify_thread = fault_notify_thread; + result -> pass_notify_instance = fault_notify_instance; + + /* Every granule's mark, granted or not. The granted ones say the module + wrote and read back the granule it meant to, which is what proves five + distinct entries were programmed rather than one entry five times; the + gap's says whether it stayed untouched. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + result -> pass_marks[granule] = MODULE_SHARED_MARK(granule); + } + + /* Stopped, not unloaded. The fault terminated the module's start thread but + left the module STARTED, and unload refuses anything that is not LOADED or + STOPPED -- so this is required, not tidiness. */ + + result -> pass_stop_status = (ULONG) txm_module_manager_stop(instance); + + /* Still loaded, so the module's own memory is still where this pass's data + base says it is. See pass_done. */ + + pass_done(); +} + + +static void report_one_pass(const PASS_RESULT *result) +{ + linflexd_puts("\n--- "); + linflexd_puts(result -> pass_name); + linflexd_puts(" ---\n"); + + linflexd_puts(" expected abort = "); + if (result -> pass_test == MODULE_TEST_PREFETCH_ABORT) + { + linflexd_puts("prefetch (IFSR/IFAR)\n"); + } + else if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + linflexd_puts("data (DFSR/DFAR), writing an ungranted shared granule\n"); + } + else + { + linflexd_puts("data (DFSR/DFAR)\n"); + } + + put_field(" load status = ", result -> pass_load_status); + put_field(" share status = ", result -> pass_share_status); + put_field(" start status = ", result -> pass_start_status); + put_field(" stop status = ", result -> pass_stop_status); + put_field(" code region = ", result -> pass_code_start); + put_field(" .. to = ", result -> pass_code_end); + put_field(" data region = ", result -> pass_data_start); + put_field(" .. to = ", result -> pass_data_end); + put_field(" data base (r9) = ", result -> pass_data_base); + put_field(" fault captured = ", result -> pass_captured); + put_field(" module progress = ", result -> pass_progress); + put_field(" should be = ", result -> pass_expect_progress); + put_field(" fault address = ", result -> pass_expect_fault); + put_field(" notify callbacks = ", result -> pass_faults); + put_field(" notified thread = ", result -> pass_notify_thread); + put_field(" should be = ", result -> pass_expect_thread); + put_field(" notified module = ", result -> pass_notify_instance); + put_field(" should be = ", result -> pass_expect_instance); + put_field(" r9 at the fault = ", result -> pass_fault_r9); + put_field(" DFSR = ", result -> pass_dfsr); + put_field(" DFAR = ", result -> pass_dfar); + put_field(" IFSR = ", result -> pass_ifsr); + put_field(" IFAR = ", result -> pass_ifar); + put_field(" SPSR = ", result -> pass_spsr); + put_field(" faulting pc = ", result -> pass_code_location); + + /* Meaningful for a data abort, where the module faulted inside its own code. + A prefetch abort faults ON the address it branched to, so the faulting pc + is outside the module and the difference is not an offset into it. */ + + if (result -> pass_test != MODULE_TEST_PREFETCH_ABORT) + { + put_field(" pc - code base = ", result -> pass_code_location - result -> pass_code_start); + } + + /* The shared-region half, reported only by the pass that produces it. */ + + if (result -> pass_test == MODULE_TEST_SHARED_ABORT) + { + ULONG granule; + + put_field(" shared grants = ", result -> pass_shared_grants); + put_field(" should be = ", (unsigned long) MODULE_SHARED_GRANTS); + put_field(" entries held = ", result -> pass_shared_count); + put_field(" unaligned grant = ", result -> pass_align_status); + put_field(" should be = ", (unsigned long) TXM_MODULE_ALIGNMENT_ERROR); + put_field(" one grant too many = ", result -> pass_exhaust_status); + put_field(" should be = ", (unsigned long) TX_NO_MEMORY); + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule == MODULE_STATUS_UNGRANTED) + { + put_field(" gap granule mark = ", result -> pass_marks[granule]); + put_field(" should be = ", 0UL); + } + else + { + put_field(" granule mark = ", result -> pass_marks[granule]); + put_field(" should be = ", MODULE_SHARED_SIGNATURE | granule); + } + } + } +} + + +/**************************************************************************/ +/* The shared-region verdict. Returns the number of failures it found, */ +/* and zero for any pass that does not exercise the shared regions. */ +/**************************************************************************/ + +static UINT judge_shared_regions(const PASS_RESULT *result) +{ + ULONG granule; + + if (result -> pass_test != MODULE_TEST_SHARED_ABORT) + { + return 0U; + } + + /* Every entry the port provides, granted. Fewer means an entry past the + first is not usable, which is the state this port was in before this pass + existed. */ + + if (result -> pass_shared_grants != (ULONG) MODULE_SHARED_GRANTS) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": not every shared entry could be granted\n"); + return 1U; + } + + if (result -> pass_shared_count != (ULONG) TXM_MODULE_MPU_SHARED_ENTRIES) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the instance does not hold the entries it granted\n"); + return 1U; + } + + /* An unaligned grant must be refused BY NAME. Accepting it would not + fault: the low six bits of PRBAR are the shareability, permission and + execute-never fields, so an under-aligned base silently changes what the + region permits instead of where it is. */ + + if (result -> pass_align_status != (ULONG) TXM_MODULE_ALIGNMENT_ERROR) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": an unaligned shared grant was not refused as such\n"); + return 1U; + } + + if (result -> pass_exhaust_status != (ULONG) TX_NO_MEMORY) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": one grant too many was not refused\n"); + return 1U; + } + + /* And what the module actually managed to write where. Each granted + granule must hold its own mark -- a region programmed with the wrong base + accepts the store and puts it somewhere else -- and the gap must still + hold the zero the manager left in it. */ + + for (granule = 0UL; granule < MODULE_STATUS_GRANULES; granule++) + { + if (granule == MODULE_STATUS_UNGRANTED) + { + if (result -> pass_marks[granule] != 0UL) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the ungranted granule was written -- a grant covered too much\n"); + return 1U; + } + } + else if (result -> pass_marks[granule] != (MODULE_SHARED_SIGNATURE | granule)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": a granted granule does not hold its own mark\n"); + return 1U; + } + else + { + /* This granule is as it should be. */ + } + } + + return 0U; +} + + +/**************************************************************************/ +/* Where the debugger stops. */ +/* */ +/* A symbol and not a line number in the report loop. The harness used */ +/* to break on sample_threadx_module_manager.c:259, and every edit to */ +/* this file moved that line -- after which the run stops somewhere */ +/* arbitrary and reports whatever memory happens to hold, which looks */ +/* like a result rather than a mistake. This does not move. */ +/* */ +/* Not static, and noinline, so it survives to the symbol table with an */ +/* address a breakpoint can be set on. */ +/**************************************************************************/ + +__attribute__((noinline)) void manager_done(void) +{ + __asm__ volatile("nop"); +} + + +/**************************************************************************/ +/* Where the debugger stops after each pass. */ +/* */ +/* A module's data is read back by the harness, not by the manager: the */ +/* manager deliberately knows no symbol of the module, so it cannot find */ +/* module_progress, while a debugger can compute its offset from the */ +/* module's ELF and add it to the data base this pass recorded. */ +/* */ +/* But it has to read it WHILE THIS PASS STILL HOLDS THAT MEMORY. The */ +/* byte pool reuses a freed block, so once a later pass has loaded, an */ +/* earlier pass's data base points at the later pass's data -- and reading */ +/* every pass at the end of the run reports the last writer's progress for */ +/* all of them. That is not a hypothetical: pass 3 loads after passes 1 */ +/* and 2 are unloaded and lands exactly where pass 1 was. */ +/* */ +/* So this exists to be broken on, once per pass, after the pass has */ +/* faulted and been stopped and before anything is unloaded. */ +/**************************************************************************/ + +__attribute__((noinline)) void pass_done(void) +{ + __asm__ volatile("nop"); +} + + +/**************************************************************************/ +/* Reporting. */ +/**************************************************************************/ + +static void manager_entry(ULONG input) +{ + UINT status; + UINT i; + unsigned long blob_size; + ULONG offset_0; + ULONG offset_1; + UINT failures = 0U; + + (void) input; + + /* Every manager call happens here, in a thread, and not in + tx_application_define. Loading a module takes the manager's mutex, and a + service that can block cannot be called before the scheduler runs: the + first version of this drove the manager from tx_application_define, + printed its way as far as the load, and stopped there for ever. The + Armv8-M sample does all of this from a thread for the same reason. */ + + /* The spellings of the shared status base, checked rather than trusted. + It is a constant agreed between two separately linked images, and a + disagreement would present as a module faulting somewhere unexpected -- + which is exactly what the pass verdicts are trying to distinguish. */ + + if ((unsigned long) &__module_status_start__ != MODULE_STATUS_ADDRESS) + { + linflexd_puts("FAIL the linker put the status area somewhere the module will not look\n"); + failures++; + } + + if ((unsigned long) (&__module_status_end__ - &__module_status_start__) + < MODULE_STATUS_AREA_LENGTH) + { + linflexd_puts("FAIL the status area is smaller than the granules granted over it\n"); + failures++; + } + + linflexd_puts("M1 initialize\n"); + status = txm_module_manager_initialize((VOID *) module_pool, MODULE_POOL_SIZE); + + linflexd_puts("initialize = "); + put_hex(status); + linflexd_puts(" ready = "); + put_hex((ULONG) _txm_module_manager_ready); + linflexd_puts("\n"); + + /* The pool the module's kernel stacks come from. See module_object_pool. */ + + status = txm_module_manager_object_pool_create((VOID *) module_object_pool, + MODULE_OBJECT_POOL_SIZE); + + linflexd_puts("object pool create = "); + put_hex(status); + linflexd_puts("\n"); + + /* Registered before anything is loaded, so there is no window in which a + module could fault with nobody listening. */ + + linflexd_puts("M2 fault notify\n"); + txm_module_manager_memory_fault_notify(module_fault_notify); + + /* The copy that makes pass 2 a relocation test. The staging area is left + empty by the linker on purpose: if the blob were already there, a failure + to copy would be invisible because the right bytes would be present + anyway. */ + + blob_size = (unsigned long) (&__module_image_end__ - &__module_image_start__); + + linflexd_puts("M3 staging the blob\n"); + put_field(" blob size = ", blob_size); + put_field(" from = ", (unsigned long) &__module_image_start__); + put_field(" to = ", (unsigned long) &__module_stage_start__); + + if (blob_size > (unsigned long) (&__module_stage_end__ - &__module_stage_start__)) + { + /* Reported rather than allowed to overrun. The staging area is a fixed + size in the linker script and the module is free to grow. */ + + linflexd_puts("FAIL the module image does not fit the staging area\n"); + failures++; + } + else + { + copy_bytes(&__module_stage_start__, &__module_image_start__, blob_size); + + /* Those were data writes to memory that is about to be fetched as + instructions, and the module area is mapped Normal write-back + (MAIR attribute 0 is 0xFF). So the copied bytes may sit in dirty D + cache lines while the instruction side, which is not coherent with + the D cache on this core, fetches whatever main memory still holds. + Clean first so memory is correct, then invalidate the I cache so no + stale line from a previous image can be served. + + This is not a precaution the passing run justifies. It ran correctly + before this was added, which is exactly the problem: a cold I cache + over a never-executed address happens to work, and it keeps happening + to work until the staging area is reused or an eviction lands + differently. Any loader that copies code owes this pair of + operations, and by-range variants would be tighter than these + all-cache sweeps -- fine here, where it runs once at startup. */ + + cache_clean_all(); + cache_invalidate_icache_all(); + + /* Nothing to bracket for the MPU here. No kernel region covers the + module area, but the window that reaches it is owned by the scheduler, + which enables it for every thread that does not own a module and + disables it for every thread that does -- so it is already open on + this thread and cannot be open while a module's own regions are. */ + + linflexd_puts("M4 pass 1, at the linked address\n"); + run_one_pass(0U, "linked address, data abort", (VOID *) &__module_image_start__, + MODULE_TEST_DATA_ABORT); + + linflexd_puts("M5 pass 2, relocated\n"); + run_one_pass(1U, "relocated, data abort", (VOID *) &__module_stage_start__, + MODULE_TEST_DATA_ABORT); + + /* Both relocation passes are done with their memory now. Unloaded here + rather than at the end, so that the pass below is a load that follows + two faults and two unloads -- which is the state a manager is in after + a module has misbehaved, and the state the next load has to work in. */ + + for (i = 0U; i < MODULE_RELOCATION_PASSES; i++) + { + (void) txm_module_manager_unload(&demo_module[i]); + } + + /* The other abort type, from the staging area so that it is also a + relocated module: the address arriving in IFAR came out of the module's + own initialised data through its rebased GOT, exactly as DFAR does + above. */ + + linflexd_puts("M6 pass 3, prefetch abort\n"); + run_one_pass(2U, "relocated, prefetch abort", (VOID *) &__module_stage_start__, + MODULE_TEST_PREFETCH_ABORT); + + (void) txm_module_manager_unload(&demo_module[2]); + + /* The shared-region pass. Granted every entry the port provides, one + granule each, and never the granule between two of them -- which it + then writes. This is the only pass that exercises a shared entry + past the first, the granule-alignment refusal, and what happens when + the entries run out; the three passes above grant one region each and + would look identical if four of the five entries did not work. */ + + linflexd_puts("M7 pass 4, shared regions\n"); + run_one_pass(3U, "relocated, shared-region gap", (VOID *) &__module_stage_start__, + MODULE_TEST_SHARED_ABORT); + + (void) txm_module_manager_unload(&demo_module[3]); + } + + /* ------------------------------------------------------------------ + The verdict. + ------------------------------------------------------------------ */ + + linflexd_puts("\n=== ThreadX modules on S32Z280: relocation and protection ===\n"); + + for (i = 0U; i < MODULE_PASSES; i++) + { + report_one_pass(&pass_results[i]); + } + + linflexd_puts("\n"); + + /* Before anything else: a module's code and data regions are enabled at the + same time, and PMSAv8-R has no region priority -- two enabled regions that + overlap are CONSTRAINED UNPREDICTABLE, and on this part that means aborts + from addresses that look perfectly legal. Nothing in the manager checks + for it, so this example does. + + The risk is real here and not theoretical: pass 2 loads its code from the + staging area, which sits in the same 64 KB as the byte pool its data comes + out of. A module grown large enough, or a pool sized differently, and the + two would meet. */ + + for (i = 0U; i < MODULE_PASSES; i++) + { + const PASS_RESULT *result = &pass_results[i]; + + if (result -> pass_load_status != (ULONG) TX_SUCCESS) + { + continue; + } + + if ((result -> pass_code_start <= result -> pass_data_end) && + (result -> pass_data_start <= result -> pass_code_end)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": its code and data regions overlap\n"); + failures++; + } + + /* The shared area is granted as regions too, and it sits in the same + 64 KB as the image, the pool and the staging area. A module grown + large enough, or a pool sized differently, and two of them would + meet. */ + + if ((result -> pass_code_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_AREA_LENGTH - 1UL)) && + (MODULE_STATUS_ADDRESS <= result -> pass_code_end)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the shared status area overlaps its code\n"); + failures++; + } + + if ((result -> pass_data_start <= (MODULE_STATUS_ADDRESS + MODULE_STATUS_AREA_LENGTH - 1UL)) && + (MODULE_STATUS_ADDRESS <= result -> pass_data_end)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the shared status area overlaps its data\n"); + failures++; + } + + /* And what _txm_module_manager_alignment_adjust produced. It is called + by every load and has never been checked, because nothing it gets + wrong faults: it rounds a module's code and data sizes up to the + protection granule and declares the granule as their alignment, and + the loader allocates on that. Get it wrong and the region bases are + written into PRBAR with their low six bits landing on the shareability, + permission and execute-never fields -- so the module runs with + attributes nobody asked for rather than at the wrong address. + + The data region's end is checked as well as its base, because the data + region's length is the rounded size; the CODE region's end is not, it + is the true end of the image and is not rounded to anything. */ + + if (((result -> pass_code_start & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL) || + ((result -> pass_data_start & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL) || + (((result -> pass_data_end + 1UL) & (TXM_MODULE_MPU_ALIGNMENT - 1UL)) != 0UL)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": its regions are not aligned to the protection granule\n"); + failures++; + } + } + + /* Each pass on its own: it has to have loaded, started, faulted once, and + faulted in User mode at the address it was told not to touch. */ + + for (i = 0U; i < MODULE_PASSES; i++) + { + const PASS_RESULT *result = &pass_results[i]; + + if ((result -> pass_load_status != (ULONG) TX_SUCCESS) || + (result -> pass_share_status != (ULONG) TX_SUCCESS) || + (result -> pass_start_status != (ULONG) TX_SUCCESS)) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": did not load, share and start\n"); + failures++; + } + else if ((result -> pass_progress & MODULE_PROGRESS_SURVIVED) != 0UL) + { + /* First, and named explicitly: the module must not have survived + what it was told to attempt. This is the one result that means + the protection did not hold, and it is checked before anything + else so that a run in which the MPU did nothing says so in those + words rather than through some downstream symptom. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the module SURVIVED its violation -- it is not protected\n"); + failures++; + } + else if (result -> pass_captured == 0UL) + { + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": reached outside its memory and was not stopped\n"); + failures++; + } + else if (result -> pass_progress != result -> pass_expect_progress) + { + /* What it did manage, from the shared granule. A module that + faulted without having read its own data or made a kernel call + faulted for the wrong reason, and the fault alone would not have + said so. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": did not reach the point it was supposed to fault at\n"); + failures++; + } + else if (result -> pass_fault_r9 != result -> pass_data_base) + { + /* r9 is the module's PIC base and the manager seeded it from the + thread entry info. If the value captured at the fault is not the + data base the manager handed out, the seeding is wrong and every + data reference the module made went somewhere unintended -- so + check it explicitly rather than inferring it from a symptom. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": r9 was not the module's data base\n"); + failures++; + } + else if ((result -> pass_spsr & 0x1FUL) != 0x10UL) + { + /* A fault from anywhere but User mode is not this test passing: the + module runs unprivileged, so a privileged fault means the fault + came from the kernel and something else is wrong. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": faulted, but not from User mode\n"); + failures++; + } + else if (((result -> pass_test == MODULE_TEST_PREFETCH_ABORT) ? + result -> pass_ifar : result -> pass_dfar) + != result -> pass_expect_fault) + { + /* The module got this address out of its own initialised data, + through its rebased GOT. Any other value means it faulted + somewhere unintended -- most likely on its own data, which is what + a bad rebase looks like. + + Which register carries it depends on the abort: a data abort + reports the address it tried to touch in DFAR, a prefetch abort + reports the address it tried to fetch in IFAR. Checking the wrong + one of the two passes for a stale value left by an earlier fault, + which is why this is selected on the expected type rather than on + whichever register happens to be non-zero. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": faulted at the wrong address, so .data or the GOT is wrong\n"); + failures++; + } + else if (result -> pass_faults != 1UL) + { + /* The notify callback is a public API of the module manager, so it is + checked and not merely reported. Zero here with a captured fault + above means the fault path reached the hardware's evidence and + never reached the application: on this port that used to be the + case for every fault, because the shared fault handler terminates + the offending thread before calling the hook and the terminate only + returns if the abort vector has told the kernel it is inside an + exception. More than one means a single violation was reported + twice. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": the fault-notify callback did not run exactly once\n"); + failures++; + } + else if ((result -> pass_notify_thread != result -> pass_expect_thread) || + (result -> pass_notify_instance != result -> pass_expect_instance)) + { + /* Being told that something faulted is not the service; being told + which thread and which module is. */ + + linflexd_puts("FAIL "); + linflexd_puts(result -> pass_name); + linflexd_puts(": notified about the wrong thread or module\n"); + failures++; + } + else if (judge_shared_regions(result) != 0U) + { + /* judge_shared_regions has already said which of the shared-region + results was wrong. */ + + failures++; + } + else + { + linflexd_puts("PASS "); + linflexd_puts(result -> pass_name); + linflexd_puts(": faulted in User mode at the forbidden address, and was reported\n"); + } + } + + /* And the first two passes against each other, which is the relocation + result. The third is not in this comparison: it faults on the address it + branched to rather than inside its own code, so its faulting pc is not an + offset into the module and there is nothing to compare. */ + + offset_0 = pass_results[0].pass_code_location - pass_results[0].pass_code_start; + offset_1 = pass_results[1].pass_code_location - pass_results[1].pass_code_start; + + if ((pass_results[0].pass_captured == 0UL) || (pass_results[1].pass_captured == 0UL)) + { + linflexd_puts("FAIL relocation: a pass did not fault, nothing to compare\n"); + failures++; + } + else if (pass_results[0].pass_code_start == pass_results[1].pass_code_start) + { + linflexd_puts("FAIL relocation: both passes ran from the same address\n"); + failures++; + } + else if (offset_0 != offset_1) + { + linflexd_puts("FAIL relocation: the two passes faulted at different offsets\n"); + failures++; + } + else + { + linflexd_puts("PASS relocation: the same blob ran correctly from two addresses\n"); + + if (pass_results[0].pass_data_base == pass_results[1].pass_data_base) + { + /* Not a failure -- the code rebase is still proven -- but worth + saying, because it means the data rebase produced the same + numbers twice and was not exercised as thoroughly. */ + + linflexd_puts("NOTE both passes shared a data base; the data rebase was not varied\n"); + } + } + + /* Both abort types, stated as its own line. Each pass above already checked + its own registers; this says that between them the two vectors were both + taken, which is the claim a reader of the log wants to be able to make + without working out what each pass did. */ + + if ((pass_results[0].pass_captured != 0UL) && (pass_results[0].pass_dfsr != 0UL) && + (pass_results[2].pass_captured != 0UL) && (pass_results[2].pass_ifsr != 0UL)) + { + linflexd_puts("PASS both abort types: data through DFSR/DFAR, prefetch through IFSR/IFAR\n"); + } + else + { + linflexd_puts("FAIL only one kind of abort was exercised\n"); + failures++; + } + + put_field("\nfailures = ", (unsigned long) failures); + + if (failures == 0U) + { + linflexd_puts("=== PASS ===\n"); + } + else + { + linflexd_puts("=== FAIL ===\n"); + } + + linflexd_puts("=== end ===\n"); + + /* Everything worth reading is in memory now. */ + + manager_done(); + + while (1) + { + tx_thread_sleep(100UL); + } +} + + +/**************************************************************************/ +/* Application definition. */ +/**************************************************************************/ + +/**************************************************************************/ +/* Board entry. */ +/* */ +/* entry.S calls this once the core is at EL1 with the MPU and caches */ +/* configured. Same shape as the other examples on this board: bring the */ +/* console up, say so, and enter the kernel. */ +/**************************************************************************/ + +void bsp_main(void) +{ + (void) linflexd_init(); + + linflexd_puts("\n=== ThreadX modules :: S32Z280-594EVB ===\n"); + linflexd_puts("entering kernel\n"); + + tx_kernel_enter(); + + /* Not reached. */ + + linflexd_puts("FAIL tx_kernel_enter returned\n"); + + for (;;) + { + __asm__ volatile("nop"); + } +} + + +void tx_application_define(void *first_unused_memory) +{ + UINT status; + + (void) first_unused_memory; + + linflexd_puts("\n=== module manager starting ===\n"); + + /* Nothing but the thread. See manager_entry for why the manager cannot be + driven from here. */ + + status = tx_thread_create(&report_thread, "module manager", manager_entry, 0UL, + report_stack, sizeof(report_stack), + 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START); + + /* Reported rather than discarded. A thread that was never created and a + thread that was created but never scheduled produce exactly the same + silence on the console, and telling those two apart is most of the work + when the port itself is what is under test. */ + + if (status == TX_SUCCESS) + { + linflexd_puts("M0 thread created\n"); + } + else + { + linflexd_puts("FAIL tx_thread_create status 0x"); + put_hex((ULONG) status); + linflexd_puts("\n"); + } +} diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb new file mode 100644 index 000000000..44a2477dd --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb @@ -0,0 +1,221 @@ +# Copyright (c) 2026 Eclipse ThreadX contributors +# SPDX-License-Identifier: MIT +# Some portions generated by Claude Code (Opus 5). +# +# Diagnostic run for the S32Z280 module manager demonstration. Same attach and +# load as run_module_demo.gdb, but instead of judging the result it dumps the +# state needed to explain one: +# +# * every ThreadX thread, with its run count -- which separates "the module +# thread never ran" from "it ran and died", the first question to answer +# when module_progress comes back zero. +# * the raw fault info, which says whether an abort was captured even though +# the notify callback never fired. +# * the module instance, so the addresses the manager decided on can be +# compared against where the module's code actually writes. +# * the live MPU regions, read back from the hardware. +# +# Usage: S32Z280_ELF= arm-none-eabi-gdb-py -batch -x diagnose_module.gdb + +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) + +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 + +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") + +elf = os.environ.get("S32Z280_ELF", "s32z280_module.elf") +gdb.execute("file %s" % elf) +gdb.execute("load") +start = int(gdb.parse_and_eval("(unsigned int)&_start")) & ~1 +gdb.execute("set $pc = 0x%x" % start) +gdb.execute("hbreak sample_threadx_module_manager.c:259") +gdb.execute("continue") +end + +echo \n===== THREADS =====\n +python +import gdb + +def ev(expr): + return gdb.parse_and_eval(expr) + +def u32(expr): + return int(ev(expr)) & 0xFFFFFFFF + +STATE = {0: "READY", 1: "COMPLETED", 2: "TERMINATED", 3: "SUSPENDED", + 4: "SLEEP", 5: "QUEUE_SUSP", 6: "SEMAPHORE_SUSP", 7: "EVENT_FLAG", + 8: "BLOCK_MEMORY", 9: "BYTE_MEMORY", 10: "IO_DRIVER", 11: "FILE", + 12: "TCP_IP", 13: "MUTEX_SUSP", 14: "PRIORITY_CHANGE"} + +try: + count = u32("_tx_thread_created_count") + print(" _tx_thread_created_count = %d" % count) + t = ev("_tx_thread_created_ptr") + print(" %-18s %-4s %-14s %-5s %-10s %-10s %s" + % ("name", "pri", "state", "runs", "stack_ptr", "entry", "module")) + for i in range(count): + name = t["tx_thread_name"].string() if int(t["tx_thread_name"]) else "(unnamed)" + pri = int(t["tx_thread_priority"]) + st = int(t["tx_thread_state"]) + runs = int(t["tx_thread_run_count"]) & 0xFFFFFFFF + sp = int(t["tx_thread_stack_ptr"]) & 0xFFFFFFFF + entry = int(t["tx_thread_entry"]) & 0xFFFFFFFF + try: + mod = int(t["tx_thread_module_instance_ptr"]) & 0xFFFFFFFF + except gdb.error: + mod = 0 + print(" %-18s %-4d %-14s %-5d 0x%08X 0x%08X %s" + % (name, pri, STATE.get(st, "?%d" % st), runs, sp, entry, + ("0x%08X" % mod) if mod else "-")) + t = t["tx_thread_created_next"] +except gdb.error as e: + print(" could not walk the thread list: %s" % e) + +print("") +print(" _tx_thread_current_ptr = 0x%08X" % u32("(unsigned int)_tx_thread_current_ptr")) +print(" _tx_thread_execute_ptr = 0x%08X" % u32("(unsigned int)_tx_thread_execute_ptr")) +end + +echo \n===== RAW FAULT INFO (was an abort captured at all?) =====\n +python +import gdb + +def u32(expr): + return int(gdb.parse_and_eval(expr)) & 0xFFFFFFFF + +f = "_txm_module_manager_memory_fault_info" +try: + info = gdb.parse_and_eval(f) + for field in ("thread_ptr", "code_location", "dfsr", "dfar", "ifsr", "ifar", + "sp", "r0", "r1", "r9", "r10", "r11", "r12", "lr", "spsr"): + key = "txm_module_manager_memory_fault_info_" + field + try: + v = int(info[key]) & 0xFFFFFFFF + print(" %-14s = 0x%08X" % (field, v)) + except gdb.error: + pass +except gdb.error as e: + print(" no fault info: %s" % e) + +print("") +print(" fault_count (sample's own counter) = %d" % u32("fault_count")) + +# Was the callback ever registered? The handler terminates the current thread +# and only then calls the notify hook, so a populated fault info with a zero +# counter has two explanations: the hook was never registered, or +# _tx_thread_terminate on the current thread did not return to the call site. +try: + n = u32("(unsigned int)_txm_module_manager_fault_notify") + want = u32("(unsigned int)&module_fault_notify") + print(" _txm_module_manager_fault_notify = 0x%08X" % n) + print(" &module_fault_notify (sample's) = 0x%08X" % want) + if n == 0: + print(" -> hook NEVER REGISTERED; that alone explains the zero counter") + elif n == want: + print(" -> hook IS registered, so the handler never reached the call:") + print(" _tx_thread_terminate on the current thread did not return") + else: + print(" -> hook points somewhere unexpected") +except gdb.error as e: + print(" could not read the notify hook: %s" % e) +end + +echo \n===== MODULE INSTANCE =====\n +python +import gdb + +def u32(expr): + return int(gdb.parse_and_eval(expr)) & 0xFFFFFFFF + +try: + m = gdb.parse_and_eval("demo_module") + for field in ("txm_module_instance_state", + "txm_module_instance_code_start", + "txm_module_instance_code_end", + "txm_module_instance_code_size", + "txm_module_instance_data_start", + "txm_module_instance_data_end", + "txm_module_instance_data_size", + "txm_module_instance_start_thread_entry", + "txm_module_instance_module_data_base_address", + "txm_module_instance_shell_entry_function", + "txm_module_instance_preamble_ptr", + "txm_module_instance_property_flags"): + try: + print(" %-46s = 0x%08X" % (field.replace("txm_module_instance_", ""), + int(m[field]) & 0xFFFFFFFF)) + except gdb.error: + pass +except gdb.error as e: + print(" cannot read demo_module: %s" % e) + +print("") +print(" where the module area's pieces ended up:") +_status = u32("(unsigned int)&__module_status_start__") +_image = u32("(unsigned int)&__module_image_start__") +print(" __module_status_start__ = 0x%08X" % _status) +print(" __module_image_start__ = 0x%08X" % _image) +print(" __module_pool_start__ = 0x%08X" % u32("(unsigned int)&__module_pool_start__")) +print(" __module_stage_start__ = 0x%08X" % u32("(unsigned int)&__module_stage_start__")) +print("") +print(" first words of the module image, to confirm the blob is really there.") +print(" Read from the linker's symbol, not from a literal: the module moves") +print(" whenever anything above it in the area changes size, and a stale") +print(" address here prints whatever now lives there as though it were the blob.") +for off in (0x00, 0x04, 0x14, 0x18): + a = _image + off + print(" 0x%08X = 0x%08X" % (a, u32("*(unsigned int *)0x%x" % a))) +print("") +print(" the shared granules, which is where the module reports progress and") +print(" where granule %d must have stayed zero:" % 2) +for _g in range(6): + a = _status + _g * 0x40 + print(" granule %d at 0x%08X: word0 = 0x%08X word1 = 0x%08X" + % (_g, a, u32("*(unsigned int *)0x%x" % a), + u32("*(unsigned int *)0x%x" % (a + 4)))) +print("") +print(" module_progress itself lives in the data area the manager allocated for") +print(" the pass, at that pass's data base plus its offset in the module's ELF;") +print(" it has no fixed address since the module became relocatable.") +end + +echo \n===== LIVE MPU REGIONS (read back from hardware) =====\n +python +import gdb + +# Regions 0-15 have direct MCR/MRC encodings; 16 and up need PRSELR. Reading +# them back is the only way to see what the module thread was actually given, +# as opposed to what the manager computed. +print(" (read via the running core; regions 8-15 are the module block, 16 the window)") +try: + gdb.execute("p $pc") +except gdb.error: + pass +print(" NOTE: system registers are not exposed over this RSP connection, so the") +print(" region read-back needs an MRC executed on the core. The manager's own") +print(" copy of what it programmed is in the module instance above; comparing") +print(" that against a hardware read is a job for an on-target routine.") +end diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb new file mode 100644 index 000000000..6f0a6331a --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb @@ -0,0 +1,557 @@ +# Copyright (c) 2026 Eclipse ThreadX contributors +# SPDX-License-Identifier: MIT +# Some portions generated by Claude Code (Opus 5). +# +# Load s32z280_module.elf onto the S32Z280-594EVB, run it, and report what the +# protection boundary did. +# +# Usage, normally through run_module_demo.sh which computes the addresses: +# S32Z280_ELF= \ +# S32Z280_MODULE_ELF= \ +# arm-none-eabi-gdb-py -batch -x tools/run_module_demo.gdb +# +# Requires, and does not do for you: +# * a CCS listening on 41475 -- the *Windows* CCS ("ccs.exe -noportquit"). +# The Linux CCS's pushes to the probe stall after "Sending code ... done", +# and under WSL2 mirrored networking a stray Linux CCS holds the port and +# blocks the Windows one ("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. +# * The UART already being captured to a file BEFORE this runs. The console +# is the test method here, not a convenience: this script reports the same +# verdict from memory, but only the console shows the ordering of events +# that produced it. +# +# WHAT A PASS LOOKS LIKE, and why it is a fault +# +# The sample module writes its own data, calls the kernel through SVC, and then +# deliberately violates its protection. The expected end state is therefore an +# abort taken in User mode, captured by the abort vector, reported through the +# fault-notify callback, and terminating the module thread -- with the manager +# still alive to report it. A run with no fault is a FAILURE: it means a module +# reached kernel memory and was not stopped. +# +# module_progress bit 0x01 wrote and summed its own scratch expect SET +# bit 0x02 reached the kernel through SVC expect SET +# bit 0x04 about to READ the forbidden address data pass only +# bit 0x08 survived the forbidden read expect CLEAR +# bit 0x10 about to BRANCH to it prefetch pass only +# bit 0x20 survived the forbidden branch expect CLEAR +# bit 0x40 wrote and read back every granted +# shared granule shared pass only +# bit 0x80 about to write the UNGRANTED granule shared pass only +# bit 0x100 survived writing it expect CLEAR +# +# so module_progress == 0x7 for a data-abort pass, 0x13 for the prefetch pass and +# 0xC3 for the shared pass. +# +# The manager records the same word a second time, out of the first shared +# granule, and this script reads the module's own copy out of its data area. Two +# independent readings of one event: if they disagree, one of the two channels is +# lying and the run says which. +# +# FOUR PASSES, because one run proves neither relocation nor both abort types +# +# The module is position independent, so a single run at the address it was +# linked for would prove nothing: the GOT rebase would map every address to +# itself and a rebase that did nothing would look the same. So passes 1 and 2 +# run the same blob from two addresses -- once where the linker put it, once from +# a staging area -- and this script compares them. +# +# Pass 3 faults the other way. Passes 1 and 2 read an address they do not own, +# which is a DATA abort reported through DFSR and DFAR. Pass 3 branches outside +# its code region, which is a PREFETCH abort reported through IFSR and IFAR and +# arrives at the handler by a different vector. It runs after passes 1 and 2 have +# been unloaded, so it also shows that a module fault leaves the manager able to +# load and run the next module. +# +# Pass 4 is the shared regions. Passes 1 to 3 are each granted ONE shared +# region -- the granule they report progress through -- which exercises the first +# of the five shared entries the port provides and nothing about the other four. +# Pass 4 is granted all five, one 64-byte granule each, and is deliberately NOT +# granted the granule that sits between two of them; it writes every granule it +# was given, reads them all back, and then writes the gap, which must fault. A +# limit register masked the wrong way, or a base off by a granule, reaches into +# that gap from one side or the other. The same pass probes the two refusals -- +# an unaligned grant must come back TXM_MODULE_ALIGNMENT_ERROR (0xF0) and one +# grant past the entry count must come back TX_NO_MEMORY (0x10). +# +# THE NOTIFY CALLBACK IS A CHECK NOW, not a note +# +# It used to be dead on this port: the shared fault handler terminates the +# offending thread and only then calls the hook, and _tx_thread_terminate returns +# for the running thread only if the abort vector has told the kernel it is inside +# an exception -- which this port's vector did not do. It does now, so every pass +# must see the callback run exactly once, with the faulting thread and the right +# module instance. Zero notifications next to a captured fault is a regression in +# that bracket and nothing else. +# +# Finding module_progress is no longer a matter of reading the module's ELF. A +# relocated module's data lives wherever the manager allocated it, so the address +# is (this pass's data base) + (the nominal offset of module_progress within the +# module's data segment). The first number is read from the module instance on +# the target, the second from the module's ELF, and neither is a constant that +# can be written down here. +# +# AND IT HAS TO BE READ WHILE THE PASS STILL OWNS THE MEMORY. The byte pool +# reuses freed blocks, so pass 3 -- which loads after passes 1 and 2 are unloaded +# -- lands exactly where pass 1 was. Reading all three at the end of the run +# therefore reports pass 3's progress as pass 1's, and pass 1 looks like it took +# the wrong branch. So this script breaks on pass_done once per pass and reads +# that pass's word there, before anything is unloaded. + +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_module.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) + +# Stop after the manager has printed its whole report. manager_done exists for +# this and nothing else: the previous version of this script broke on +# sample_threadx_module_manager.c:259, and every edit to that file moved the line +# out from under the breakpoint, after which the run stopped somewhere arbitrary +# and reported whatever memory happened to hold. +# +# hbreak, not break: the code region is mapped read-only by the MPU, so a +# software breakpoint would have to write to it. +gdb.execute("hbreak pass_done") +gdb.execute("hbreak manager_done") + +# Each pass's module_progress, read at pass_done while that pass still holds its +# data. Kept in a global the reporting block below picks up; gdb's python runs in +# one persistent namespace, and the reader falls back to "skipped" rather than a +# traceback if that ever stops being true. +PROGRESS_BY_PASS = [] + +_offset = os.environ.get("S32Z280_MODULE_PROGRESS_OFFSET") +_offset = int(_offset, 0) if _offset else None + +for _i in range(4): + gdb.execute("continue") + if _offset is None: + PROGRESS_BY_PASS.append(None) + continue + _base = int(gdb.parse_and_eval("pass_results[%d].pass_data_base" % _i)) & 0xFFFFFFFF + if _base == 0: + # The pass never loaded, so there is no data base and nothing to read. + PROGRESS_BY_PASS.append(None) + else: + _addr = _base + _offset + PROGRESS_BY_PASS.append( + int(gdb.parse_and_eval("*(unsigned int *)0x%x" % _addr)) & 0xFFFFFFFF) + print("pass %d: module_progress 0x%08X at 0x%08X (read while loaded)" + % (_i + 1, PROGRESS_BY_PASS[-1], _addr)) + +# On to the report the manager prints for itself. +gdb.execute("continue") +end + +echo \n===== module manager run: what memory says =====\n +python +import gdb, os + +def rd(addr): + return int(gdb.parse_and_eval("*(unsigned int *)0x%x" % addr)) & 0xFFFFFFFF + +def ev(expr): + return int(gdb.parse_and_eval(expr)) & 0xFFFFFFFF + +failures = 0 + +OWN_DATA, KERNEL_CALL = 0x01, 0x02 +ATTEMPTED_STEAL, SURVIVED_STEAL = 0x04, 0x08 +ATTEMPTED_JUMP, SURVIVED_JUMP = 0x10, 0x20 +SHARED_WROTE = 0x40 +ATTEMPTED_GAP, SURVIVED_GAP = 0x80, 0x100 +FORBIDDEN = 0x31780000 + +# The shared area, mirrored from S32Z_MODULE_STATUS_* in platform.h. Five of the +# six granules are granted, one entry each; index 2 never is, and the marks go at +# word 1 because word 0 of granule 0 is the progress word. +STATUS_BASE, STATUS_GRANULE, STATUS_GRANULES = 0x317F0000, 0x40, 6 +STATUS_UNGRANTED, STATUS_MARK_OFFSET = 2, 4 +SHARED_SIGNATURE = 0x5A5A0000 +ALIGNMENT_ERROR, NO_MEMORY = 0xF0, 0x10 +MODE = {0x10: "User", 0x13: "Supervisor", 0x1A: "Hyp", 0x1F: "System"} + +# Which violation each pass was told to commit, from the low byte of the module ID +# the manager writes into the instance before starting it. +TEST_DATA_ABORT, TEST_PREFETCH_ABORT, TEST_SHARED_ABORT = 0x1, 0x2, 0x3 +PASSES = 4 +RELOCATION_PASSES = 2 + +# The nominal offset of module_progress inside the module's data segment. Both +# halves come from the module's own ELF and neither is an address the module ever +# runs at -- see link_demo_module.lds. The run-time address is this offset plus +# whatever data base the manager handed the module, which differs per pass. +progress_offset = os.environ.get("S32Z280_MODULE_PROGRESS_OFFSET") +progress_offset = int(progress_offset, 0) if progress_offset else None + +if progress_offset is None: + print(" NOTE S32Z280_MODULE_PROGRESS_OFFSET unset; skipping the progress checks") +else: + print(" module_progress sits %d bytes into the module's data segment" % progress_offset) + +passes = [] +for i in range(PASSES): + p = {} + for field in ("pass_test", "pass_load_status", "pass_share_status", + "pass_start_status", "pass_stop_status", + "pass_code_start", "pass_code_end", "pass_data_start", + "pass_data_end", "pass_data_base", "pass_faults", + "pass_captured", "pass_fault_r9", + "pass_dfsr", "pass_dfar", "pass_ifsr", "pass_ifar", + "pass_spsr", "pass_code_location", + "pass_notify_thread", "pass_notify_instance", + "pass_expect_thread", "pass_expect_instance", + "pass_progress", "pass_expect_progress", "pass_expect_fault", + "pass_shared_grants", "pass_shared_count", + "pass_align_status", "pass_exhaust_status"): + p[field] = ev("pass_results[%d].%s" % (i, field)) + p["marks"] = [ev("pass_results[%d].pass_marks[%d]" % (i, g)) + for g in range(STATUS_GRANULES)] + try: + p["name"] = gdb.parse_and_eval("pass_results[%d].pass_name" % i).string() + except gdb.error: + p["name"] = "pass %d" % (i + 1) + passes.append(p) + +for i, p in enumerate(passes): + prefetch = (p["pass_test"] == TEST_PREFETCH_ABORT) + shared = (p["pass_test"] == TEST_SHARED_ABORT) + print("") + print(" --- pass %d: %s ---" % (i + 1, p["name"])) + if prefetch: + print(" expected abort = prefetch, through IFSR/IFAR") + elif shared: + print(" expected abort = data, through DFSR/DFAR, writing an") + print(" ungranted shared granule") + else: + print(" expected abort = data, through DFSR/DFAR") + print(" load / share / start / stop = 0x%08X / 0x%08X / 0x%08X / 0x%08X" + % (p["pass_load_status"], p["pass_share_status"], + p["pass_start_status"], p["pass_stop_status"])) + print(" code region = 0x%08X .. 0x%08X" + % (p["pass_code_start"], p["pass_code_end"])) + print(" data region = 0x%08X .. 0x%08X" + % (p["pass_data_start"], p["pass_data_end"])) + print(" data base (r9) = 0x%08X" % p["pass_data_base"]) + + if (p["pass_load_status"] != 0 or p["pass_share_status"] != 0 + or p["pass_start_status"] != 0): + print(" *** FAIL: did not load, share and start") + failures += 1 + continue + + # Code and data regions are enabled together and PMSAv8-R has no region + # priority, so an overlap is CONSTRAINED UNPREDICTABLE rather than merely + # untidy. Checked here as well as on the target because a debugger can say + # which two regions met, and the target can only say that something did. + if (p["pass_code_start"] <= p["pass_data_end"] + and p["pass_data_start"] <= p["pass_code_end"]): + print(" *** FAIL: this module's code and data regions overlap") + failures += 1 + + progress = globals().get("PROGRESS_BY_PASS", [None] * PASSES)[i] + if progress is None: + print(" module_progress = not read") + else: + addr = p["pass_data_base"] + progress_offset + print(" module_progress = 0x%08X (read at 0x%08X while this pass" + % (progress, addr)) + print(" still held that memory)") + + # Every bit is checked, including the ones belonging to the violations + # this pass was NOT told to commit: a module that took more than one + # route, or the wrong one, is not the module this pass asked for and the + # result would not mean what the register checks below claim. + data = not (prefetch or shared) + wanted = [(OWN_DATA, "wrote its own data", True), + (KERNEL_CALL, "reached the kernel through SVC", True), + (ATTEMPTED_STEAL, "attempted the forbidden read", data), + (SURVIVED_STEAL, "SURVIVED the forbidden read", False), + (ATTEMPTED_JUMP, "attempted the forbidden branch", prefetch), + (SURVIVED_JUMP, "SURVIVED the forbidden branch", False), + (SHARED_WROTE, "wrote and read back every granted granule", shared), + (ATTEMPTED_GAP, "attempted the ungranted granule", shared), + (SURVIVED_GAP, "SURVIVED the ungranted granule", False)] + for bit, name, want_set in wanted: + got = bool(progress & bit) + ok = (got == want_set) + if not ok: + failures += 1 + print(" 0x%03X %-42s %-5s %s" + % (bit, name, "set" if got else "clear", + "ok" if ok else "*** WRONG ***")) + if progress & (SURVIVED_STEAL | SURVIVED_JUMP | SURVIVED_GAP): + print(" ISOLATION FAILURE: the module reached memory it was never granted.") + + # The manager's own reading of the same word, out of the first shared + # granule. Two independent channels for one event: the module writes + # both, this script reads its private copy and the manager read the + # shared one, so a disagreement means one of the two paths is lying and + # neither result should be trusted until it is explained. + if p["pass_progress"] != progress: + print(" *** FAIL: the manager read progress 0x%08X out of the shared" + % p["pass_progress"]) + print(" granule, the module's own copy says 0x%08X" % progress) + failures += 1 + if p["pass_progress"] != p["pass_expect_progress"]: + print(" *** FAIL: progress 0x%08X, expected 0x%08X" + % (p["pass_progress"], p["pass_expect_progress"])) + failures += 1 + + # "captured" and "notified" are different events: the abort vector records the + # fault registers before anything else runs, and the notify callback is a + # later consequence of the same fault. Both are required. Reporting them + # apart is what keeps a broken notify path from being read as "no fault + # occurred", which is the opposite of the truth. + print(" fault captured = %d" % p["pass_captured"]) + print(" notify callbacks = %d" % p["pass_faults"]) + if p["pass_captured"] == 0: + print(" *** FAIL: no fault was captured. The module reached outside its") + print(" memory and was not stopped, or never got that far.") + failures += 1 + continue + + # The callback is a public API of the module manager, so its arguments are + # checked and not merely counted. Zero notifications beside a captured fault + # means the fault reached the hardware's evidence and never reached the + # application: on this port that is the system-state bracket in + # txm_module_manager_fault_capture.S having gone missing again. + if p["pass_faults"] != 1: + print(" *** FAIL: the notify callback ran %d times, not once." + % p["pass_faults"]) + failures += 1 + elif (p["pass_notify_thread"] != p["pass_expect_thread"] + or p["pass_notify_instance"] != p["pass_expect_instance"]): + print(" *** FAIL: notified about thread 0x%08X / module 0x%08X," + % (p["pass_notify_thread"], p["pass_notify_instance"])) + print(" expected thread 0x%08X / module 0x%08X" + % (p["pass_expect_thread"], p["pass_expect_instance"])) + failures += 1 + else: + print(" notified about = thread 0x%08X, module 0x%08X (both correct)" + % (p["pass_notify_thread"], p["pass_notify_instance"])) + + dfsr, dfar, spsr, pc = (p["pass_dfsr"], p["pass_dfar"], + p["pass_spsr"], p["pass_code_location"]) + ifsr, ifar = p["pass_ifsr"], p["pass_ifar"] + + # Both pairs are printed for both kinds of pass, because the capture records + # both and the pair that does not belong to this fault is a leftover the + # hardware never cleared -- worth seeing, and worth not mistaking for a + # result. Which pair is meaningful is decided by the expected abort type, + # not by which of them looks plausible. + print(" DFSR = 0x%08X status 0x%02X WnR %d%s" + % (dfsr, dfsr & 0x3F, (dfsr >> 11) & 1, + "" if not prefetch else " (stale: this pass took a prefetch abort)")) + print(" DFAR = 0x%08X" % dfar) + print(" IFSR = 0x%08X status 0x%02X%s" + % (ifsr, ifsr & 0x3F, + " (stale: this pass took a data abort)" if not prefetch else "")) + print(" IFAR = 0x%08X" % ifar) + print(" SPSR = 0x%08X mode 0x%02X (%s)" + % (spsr, spsr & 0x1F, MODE.get(spsr & 0x1F, "?"))) + print(" faulting pc = 0x%08X" % pc) + if prefetch: + # The fault is on the fetch AT the branch target, so the faulting pc is + # the target and not a place inside the module. + print(" (the faulting pc is the branch target, outside the module)") + else: + print(" pc - code base = 0x%08X" % ((pc - p["pass_code_start"]) & 0xFFFFFFFF)) + print(" r9 at the fault = 0x%08X" % p["pass_fault_r9"]) + + # r9 is the module's PIC base, seeded by the manager's thread stack build from + # the thread entry info. Before T3 it was left at 0, which is what made every + # module data reference resolve to a small absolute address. Checking it + # directly turns that class of bug into one line instead of a fault to explain. + if p["pass_fault_r9"] != p["pass_data_base"]: + print(" *** FAIL: r9 should be the data base 0x%08X" % p["pass_data_base"]) + failures += 1 + + if (spsr & 0x1F) != 0x10: + print(" *** FAIL: the fault did not come from User mode, so it is not") + print(" a module being stopped at the boundary.") + failures += 1 + + # The faulting address is the strongest single check in this script. The + # module obtained it by reading one of its own initialised globals through the + # rebased GOT, so the right value here means the GOT was rewritten AND .data + # was copied out of the image. A wrong value usually means the module faulted + # on its own data instead, which is what a bad rebase looks like. + # + # A data abort reports it in DFAR, a prefetch abort in IFAR. Both registers + # are sticky, so checking the pair that does not belong to this fault would + # pass on a value some earlier fault left behind -- including the boot probes, + # which take a prefetch abort of their own before ThreadX starts. + # + # The shared pass is the exception: the address it faults on is the mark word + # of the granule it was not granted, which it computes from a literal rather + # than out of .data -- so for that pass this is a check on the grant and not + # on the rebase. Which is why the expectation is carried per pass. + reg_name, reg_value = ("IFAR", ifar) if prefetch else ("DFAR", dfar) + expect_fault = p["pass_expect_fault"] or FORBIDDEN + if reg_value != expect_fault: + print(" *** FAIL: expected %s 0x%08X, the address the module was told" + % (reg_name, expect_fault)) + print(" not to touch. A different one means .data was not") + print(" copied or the GOT was not rebased.") + failures += 1 + + # A prefetch abort with nothing in IFSR would mean the vector reached the + # capture without the core having recorded an instruction fault, which is the + # one way this pass could look right and be meaningless. + if prefetch and (ifsr & 0x3F) == 0: + print(" *** FAIL: IFSR reports no instruction fault status") + failures += 1 + + # The shared-region results, for the one pass that produces them. Read out + # of the manager's own record rather than out of the MPU: what the hardware + # holds is checked by the module having faulted, and what is checked here is + # that the manager granted what it meant to and refused what it should. + if shared: + print(" shared grants = %d (expected %d)" + % (p["pass_shared_grants"], STATUS_GRANULES - 1)) + print(" entries held = %d" % p["pass_shared_count"]) + print(" unaligned grant = 0x%02X (expected 0x%02X, TXM_MODULE_ALIGNMENT_ERROR)" + % (p["pass_align_status"], ALIGNMENT_ERROR)) + print(" one grant too many = 0x%02X (expected 0x%02X, TX_NO_MEMORY)" + % (p["pass_exhaust_status"], NO_MEMORY)) + + if p["pass_shared_grants"] != STATUS_GRANULES - 1: + print(" *** FAIL: not every shared entry could be granted") + failures += 1 + if p["pass_shared_count"] != STATUS_GRANULES - 1: + print(" *** FAIL: the instance does not hold the entries it granted") + failures += 1 + if p["pass_align_status"] != ALIGNMENT_ERROR: + print(" *** FAIL: an unaligned shared grant was not refused as such.") + print(" Accepting one does not fault -- the low six bits of") + print(" PRBAR are permissions, so it changes what the region") + print(" allows instead of where it is.") + failures += 1 + if p["pass_exhaust_status"] != NO_MEMORY: + print(" *** FAIL: one grant too many was not refused. The entry index") + print(" is TXM_MODULE_MPU_SHARED_INDEX plus the count, so a") + print(" sixth grant writes past the end of the region table.") + failures += 1 + + # And what the module managed to write where. A granted granule holding + # something other than its own mark means that entry was programmed with + # the wrong base -- the store was accepted and landed elsewhere -- and a + # non-zero gap means a grant covered a granule nobody asked for. + for g in range(STATUS_GRANULES): + addr = STATUS_BASE + g * STATUS_GRANULE + STATUS_MARK_OFFSET + want = 0 if g == STATUS_UNGRANTED else (SHARED_SIGNATURE | g) + got = p["marks"][g] + tag = "ungranted" if g == STATUS_UNGRANTED else "granted " + ok = (got == want) + if not ok: + failures += 1 + print(" granule %d %s at 0x%08X = 0x%08X (expected 0x%08X) %s" + % (g, tag, addr, got, want, "ok" if ok else "*** WRONG ***")) + +# --- the relocation result, which is the comparison between the passes -------- +print("") +print(" ===== both abort types =====") +data_passes = [q for q in passes if q["pass_test"] != TEST_PREFETCH_ABORT + and q["pass_captured"] and (q["pass_dfsr"] & 0x3F)] +pabt_passes = [q for q in passes if q["pass_test"] == TEST_PREFETCH_ABORT + and q["pass_captured"] and (q["pass_ifsr"] & 0x3F)] +print(" data aborts captured = %d" % len(data_passes)) +print(" prefetch aborts captured = %d" % len(pabt_passes)) +if not data_passes or not pabt_passes: + print(" *** FAIL: only one kind of abort was exercised, so half of the") + print(" port's fault path never ran") + failures += 1 +else: + print(" PASS: both vectors were taken and both register pairs reported") + +print("") +print(" ===== relocation =====") +a, b = passes[0], passes[1] +off_a = (a["pass_code_location"] - a["pass_code_start"]) & 0xFFFFFFFF +off_b = (b["pass_code_location"] - b["pass_code_start"]) & 0xFFFFFFFF + +if a["pass_captured"] == 0 or b["pass_captured"] == 0: + print(" *** FAIL: a pass did not fault, so there is nothing to compare") + failures += 1 +elif a["pass_code_start"] == b["pass_code_start"]: + print(" *** FAIL: both passes ran from the same address 0x%08X" + % a["pass_code_start"]) + failures += 1 +elif off_a != off_b: + print(" *** FAIL: the passes faulted at different offsets, 0x%08X and 0x%08X" + % (off_a, off_b)) + failures += 1 +else: + print(" code bases 0x%08X and 0x%08X, %d bytes apart" + % (a["pass_code_start"], b["pass_code_start"], + (b["pass_code_start"] - a["pass_code_start"]) & 0xFFFFFFFF)) + print(" both faulted at offset 0x%08X into their own image" % off_a) + print(" PASS: the same blob ran correctly from two different addresses") + if a["pass_data_base"] == b["pass_data_base"]: + print(" NOTE both passes shared a data base 0x%08X, so the data half of" + % a["pass_data_base"]) + print(" the rebase produced the same numbers twice") + +print("") +# Cast, because this symbol has no debug type in the manager image and gdb +# refuses to read an untyped symbol without being told its width. +try: + print(" boot_stage = %d" % ev("*(unsigned int *)&boot_stage")) +except gdb.error as e: + print(" boot_stage = unavailable (%s)" % e) +print("") +if failures == 0: + print("===== PASS: relocated, stopped at the boundary both ways, and reported =====") +else: + print("===== FAIL: %d check(s) wrong =====" % failures) +end diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh new file mode 100755 index 000000000..eb9be3c04 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# Copyright (c) 2026 Eclipse ThreadX contributors +# SPDX-License-Identifier: MIT +# Some portions generated by Claude Code (Opus 5). +# +# Run the ThreadX module manager demonstration on the S32Z280-594EVB and capture +# the evidence: the console log and the fault registers. +# +# Usage: tools/run_module_demo.sh [] +# +# The console is the test method here, not a convenience, so this captures the +# UART to a file before the image runs and prints the captured bytes afterwards. +# Both LINFlexD bugs found during bring-up were invisible from the target and +# showed up only in a byte diff of a captured log. +# +# Prerequisites this cannot do for you: +# * the *Windows* CCS listening on 41475, started as "ccs.exe -noportquit". +# The Linux CCS's pushes to the probe stall after "Sending code ... done". +# * board wiring for the console: LIN9 -> daughtercard USB-UART, jumper J248 +# at 1-2, micro-USB at J119 on the daughtercard. +set -u + +BUILD=${1:-build_mod} +EVB_BIN="$BUILD/ports/cortex_r52/gnu/example_build/s32z280_evb" +HERE=$(cd "$(dirname "$0")" && pwd) + +R=/home/fdesbiens/NXP/S32DS.3.6.10 +GDB=$R/S32DS/tools/gdb-arm/arm32-eabi/bin/arm-none-eabi-gdb-py +GTA=$R/S32DS/tools/S32Debugger/Debugger/Server/gta + +MANAGER_ELF=$EVB_BIN/s32z280_module.elf +MODULE_ELF=$EVB_BIN/s32z280_demo_module.elf +UART=${UART:-/dev/ttyUSB0} +LOG=${LOG:-/tmp/module_demo_uart.log} + +for f in "$MANAGER_ELF" "$MODULE_ELF"; do + if [ ! -f "$f" ]; then + echo "ERROR: $f not found. Build it first:" >&2 + echo " ninja -C $BUILD s32z280_module.elf" >&2 + exit 1 + fi +done + +# gdb-py needs the source-built interpreter: its embedded Python has a minimal +# builtin set and loads _struct/_socket/_ctypes as separate .so files. Compute +# the module address BEFORE exporting PYTHONHOME, since that breaks system tools. +# What is wanted is an OFFSET, not an address. The module is position +# independent: the address nm reports is a nominal one from a segment the module +# never runs at, and its real data lives wherever the manager allocated it -- a +# different place in each pass. So the offset of module_progress +# within the module's data segment is computed here, and the gdb script adds it +# to each pass's data base, which it reads off the target. +PROGRESS_SYM=$(nm "$MODULE_ELF" | awk '$3=="module_progress"{print $1}') +DATA_SEG=$(nm "$MODULE_ELF" | awk '$3=="__data_segment_start__"{print $1}') +if [ -z "$PROGRESS_SYM" ] || [ -z "$DATA_SEG" ]; then + echo "ERROR: module_progress or __data_segment_start__ not found in $MODULE_ELF" >&2 + exit 1 +fi +PROGRESS_OFFSET=$(( 0x$PROGRESS_SYM - 0x$DATA_SEG )) +if [ "$PROGRESS_OFFSET" -lt 0 ]; then + echo "ERROR: module_progress is below the module's data segment; the link map" >&2 + echo " and this script disagree about where the module's data starts." >&2 + exit 1 +fi +echo "module_progress is $PROGRESS_OFFSET bytes into the module's data segment" +echo " (nominal 0x$PROGRESS_SYM, data segment 0x$DATA_SEG -- neither is a real address)" + +if pgrep -x ccs > /dev/null; then + echo "ERROR: a Linux CCS is running (pid $(pgrep -x ccs | tr '\n' ' '))." >&2 + echo " It holds 41475 and blocks the Windows CCS. Kill it: pkill -x ccs" >&2 + exit 1 +fi +# /dev/tcp rather than a Python probe: PYTHONHOME below points at the +# source-built 3.10 for gdb-py, which would make system python3 fail and this +# check spuriously report "no CCS". +if ! timeout 5 bash -c "exec 3<>/dev/tcp/127.0.0.1/41475" 2>/dev/null; then + echo "ERROR: nothing listening on 41475." >&2 + echo " CCS quits when its client disconnects unless started with" >&2 + echo " -noportquit. Relaunch on Windows as: ccs.exe -noportquit" >&2 + exit 1 +fi + +# --- console capture, started BEFORE the image runs ------------------------- +if [ -c "$UART" ]; then + stty -F "$UART" 115200 cs8 -parenb -cstopb -crtscts raw -echo + : > "$LOG" + cat "$UART" > "$LOG" & + CAT_PID=$! + echo "capturing $UART -> $LOG (pid $CAT_PID)" +else + CAT_PID="" + echo "WARNING: $UART is not a character device; running without a console capture." >&2 +fi + +cleanup() { + if [ -n "$CAT_PID" ]; then + kill "$CAT_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +P=$HOME/toolchains/py310-src +export PYTHONHOME=$P +export PYTHONPATH=$P/lib/python3.10:$P/lib/python3.10/lib-dynload:$P/lib/python3.10/site-packages +export S32Z280_ELF=$MANAGER_ELF +export S32Z280_MODULE_ELF=$MODULE_ELF +export S32Z280_MODULE_PROGRESS_OFFSET=$PROGRESS_OFFSET + +# pkill -x, not -f: these run as ./gta, so a -f pattern like 'gta/gta' never +# matches and a stale server silently serves the next attach. +pkill -x gta 2>/dev/null +sleep 2 +( cd "$GTA" && nohup ./gta -p 45000 -k > /tmp/gta_module_demo.log 2>&1 & ) +sleep 4 +if ! pgrep -x gta > /dev/null; then + echo "ERROR: GTA failed to start; see /tmp/gta_module_demo.log" >&2 + exit 1 +fi + +timeout -k 20 500 "$GDB" -batch -x "$HERE/run_module_demo.gdb" +rc=$? + +# Let the last of the console output arrive before the capture is torn down. +sleep 2 + +echo "" +echo "===== console, as captured from $UART =====" +if [ -s "$LOG" ]; then + cat "$LOG" +else + echo "(nothing arrived on the console)" + echo "Check: jumper J248 at 1-2, micro-USB at J119 on the daughtercard, and" + echo "LED J122 for board-to-host traffic." +fi +echo "===== end of console =====" +echo "=== gdb exit=$rc ===" +exit $rc diff --git a/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S new file mode 100644 index 000000000..fa4858f67 --- /dev/null +++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S @@ -0,0 +1,137 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE PREAMBLE RELEASE */ +@/* */ +@/* txm_module_preamble.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* The header the module manager reads before it will load a module. */ +@/* */ +@/* It must be the first thing in the module image, which is what the */ +@/* linker script arranges, and every entry point in it is an offset */ +@/* from the start of the preamble rather than an address. A module is */ +@/* position independent: the manager decides where it lands, so the */ +@/* module cannot know its own addresses at link time. */ +@/* */ +@/* Code and data sizes come from the linker script rather than being */ +@/* written in by hand. The Cortex-R4 preamble carries literal numbers */ +@/* for them, which is a standing invitation to grow a module past its */ +@/* declared size and have the manager map less memory than it uses -- */ +@/* a fault in a module that did nothing wrong, whose cause is a */ +@/* constant in a file nobody thought to change. */ +@/* */ +@/**************************************************************************/ + + .syntax unified + .arm + + .global __txm_module_preamble + + .extern _txm_module_thread_shell_entry + .extern _txm_module_callback_request_thread_entry + .extern demo_module_start + +@ Supplied by the module's linker script. + + .extern __txm_module_code_size + .extern __txm_module_data_size + +@ Properties. The compiler field tells the manager which set of entry-point +@ adjustments to apply, and the option bits say what the module is asking for: +@ user mode and memory protection, which together are the point of this port. +@ +@ 0x02000000 TXM_MODULE_GNU_COMPILER +@ 0x00000001 TXM_MODULE_USER_MODE +@ 0x00000002 TXM_MODULE_MEMORY_PROTECTION + + .equ MODULE_PROPERTIES, 0x02000003 + + .section .txm_module_preamble, "a" + .align 6 + +__txm_module_preamble: + + .word 0x4D4F4455 @ Module ID, "MODU" + .word 0x6 @ Major version + .word 0x1 @ Minor version + .word 32 @ Preamble size, 32-bit words + .word 0x52520001 @ Application-defined ID + .word MODULE_PROPERTIES @ Properties, see above + +@ Entry points, as offsets from the preamble. + +@ Entry points are stored relative to the word that holds them, not to the +@ start of the preamble. That is what the manager expects: it recovers the +@ offset from the module base by adding the field's own byte offset back -- +@ TXM_MODULE_GNU_SHELL_ADJUST 24, START 28, STOP 32, CALLBACK 44, which are +@ exactly the offsets of the four words below. Storing these relative to +@ __txm_module_preamble instead counts that offset twice, and the module is +@ then entered that many bytes into its shell entry: past the prologue, with +@ the arguments never saved and the frame pointer never set up, so the first +@ dereference goes through a register the stack build had zeroed. On silicon +@ that read faulted at 0x1C, which is offset 0x1C from a null r3. +@ +@ Every other GNU module port writes these the same way; cortex_m33's +@ preamble, which this port was seeded from, spells it "symbol - . - 0". + + .word _txm_module_thread_shell_entry - . + .word demo_module_start - . + .word 0 @ No stop thread + .word 1 @ Start/stop thread priority + .word 1024 @ Start/stop thread stack size + .word _txm_module_callback_request_thread_entry - . + .word 1 @ Callback thread priority + .word 1024 @ Callback thread stack size + +@ Sizes, from the linker script. The manager rounds both up to the 64-byte MPU +@ granule and maps exactly this much; anything the module touches beyond it +@ faults, which is the intended behaviour and not a bug to work around by +@ inflating these numbers. + + .word __txm_module_code_size + .word __txm_module_data_size + + .word 0 @ Reserved 0 + .word 0 @ Reserved 1 + .word 0 @ Reserved 2 + .word 0 @ Reserved 3 + .word 0 @ Reserved 4 + .word 0 @ Reserved 5 + .word 0 @ Reserved 6 + .word 0 @ Reserved 7 + .word 0 @ Reserved 8 + .word 0 @ Reserved 9 + .word 0 @ Reserved 10 + .word 0 @ Reserved 11 + .word 0 @ Reserved 12 + .word 0 @ Reserved 13 + .word 0 @ Reserved 14 + .word 0 @ Reserved 15 + +@ The preamble declares its own length in its fourth word, and the manager +@ believes it. If the two ever disagree the manager reads entry points from the +@ wrong offsets, so the assembler checks rather than the reader. + + .if (. - __txm_module_preamble) != (32 * 4) + .error "txm_module_preamble is not the 32 words its size field declares" + .endif diff --git a/ports_module/cortex_r52/gnu/inc/tx_port.h b/ports_module/cortex_r52/gnu/inc/tx_port.h new file mode 100644 index 000000000..d3013b1f0 --- /dev/null +++ b/ports_module/cortex_r52/gnu/inc/tx_port.h @@ -0,0 +1,442 @@ +/*************************************************************************** + * 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 + **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** 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 +#include + + +/* 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 +/* The module thread extension. A thread that belongs to a module carries a + pointer to its module instance, its entry information, the user-mode state to + restore when it is scheduled, and two stacks: the module's own stack for user + mode and a kernel stack for the privileged side of a system call. + + Two stacks rather than one is not an implementation detail that can be + simplified away. A module thread runs in user mode on memory the module owns; + the moment it enters the kernel through a system call, the kernel must not be + writing its own state onto memory the module can also write, or the module + could corrupt the kernel by scribbling on its own stack. */ + +/* tx_thread_vfp_enable comes first and is not optional here. The Cortex-R52 + port keeps the floating-point lazy-enable flag in this extension, and dropping + it while adding the module fields would leave VFP threads with nowhere to + record that they have used the unit -- the kind of breakage that shows up as + corrupted floating-point state in a module, a long way from its cause. */ + +#define TX_THREAD_EXTENSION_2 ULONG tx_thread_vfp_enable; \ + VOID *tx_thread_module_instance_ptr; \ + VOID *tx_thread_module_entry_info_ptr; \ + ULONG tx_thread_module_current_user_mode; \ + ULONG tx_thread_module_user_mode; \ + ULONG tx_thread_module_saved_lr; \ + VOID *tx_thread_module_kernel_stack_start; \ + VOID *tx_thread_module_kernel_stack_end; \ + ULONG tx_thread_module_kernel_stack_size; \ + VOID *tx_thread_module_stack_ptr; \ + VOID *tx_thread_module_stack_start; \ + VOID *tx_thread_module_stack_end; \ + ULONG tx_thread_module_stack_size; \ + VOID *tx_thread_module_reserved; +#define TX_THREAD_EXTENSION_3 + + +/* Define the port extensions of the remaining ThreadX objects. */ + +#define TX_BLOCK_POOL_EXTENSION +#define TX_BYTE_POOL_EXTENSION +/* The four object extensions below each carry the module instance that owns the + object, and the module manager needs them to answer a question it must answer + on every service call: is the object this module is asking about one it owns? + Without them a module could pass any address it liked as a queue or a semaphore + and the kernel would act on whatever was there. + + They are also why a module port cannot share the base port's ThreadX library. + They change the layout of TX_QUEUE, TX_SEMAPHORE, TX_EVENT_FLAGS_GROUP and + TX_TIMER, so a library compiled against the base port's tx_port.h and a manager + compiled against this one disagree about every kernel object -- and both + compile, so nothing says so. + + Block pool, byte pool and mutex stay empty, as they are in the Armv8-M module + port: the manager tracks ownership of those differently. */ + +#define TX_EVENT_FLAGS_GROUP_EXTENSION VOID *tx_event_flags_group_module_instance; \ + VOID (*tx_event_flags_group_set_module_notify)(struct TX_EVENT_FLAGS_GROUP_STRUCT *group_ptr); +#define TX_MUTEX_EXTENSION +#define TX_QUEUE_EXTENSION VOID *tx_queue_module_instance; \ + VOID (*tx_queue_send_module_notify)(struct TX_QUEUE_STRUCT *queue_ptr); +#define TX_SEMAPHORE_EXTENSION VOID *tx_semaphore_module_instance; \ + VOID (*tx_semaphore_put_module_notify)(struct TX_SEMAPHORE_STRUCT *semaphore_ptr); +#define TX_TIMER_EXTENSION VOID *tx_timer_module_instance; \ + VOID (*tx_timer_module_expiration_function)(ULONG id); + + +/* 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.1.202602a *"; +#else +extern CHAR _tx_version_id[]; +#endif + + +#endif + diff --git a/ports_module/cortex_r52/gnu/inc/txm_module_port.h b/ports_module/cortex_r52/gnu/inc/txm_module_port.h new file mode 100644 index 000000000..f8b32fdf7 --- /dev/null +++ b/ports_module/cortex_r52/gnu/inc/txm_module_port.h @@ -0,0 +1,425 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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 + **************************************************************************/ + +// Some portions generated by Claude Code (Opus 5). + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + + +/**************************************************************************/ +/* */ +/* APPLICATION INTERFACE DEFINITION RELEASE */ +/* */ +/* txm_module_port.h Cortex-R52/GNU */ +/* 6.1.10 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This file defines the basic module constants, interface structures, */ +/* and function prototypes. */ +/* */ +/**************************************************************************/ + +#ifndef TXM_MODULE_PORT_H +#define TXM_MODULE_PORT_H + +/* Determine if the optional Modules user define file should be used. */ + +#ifdef TXM_MODULE_INCLUDE_USER_DEFINE_FILE + +/* Yes, include the user defines in txm_module_user.h. The defines in this file may + alternately be defined on the command line. */ + +#include "txm_module_user.h" +#endif + +/* It is assumed that the base ThreadX tx_port.h file has been modified to add the + following extensions to the ThreadX thread control block (this code should replace + the corresponding macro define in tx_port.h): + +#define TX_THREAD_EXTENSION_2 VOID *tx_thread_module_instance_ptr; \ + VOID *tx_thread_module_entry_info_ptr; \ + ULONG tx_thread_module_current_user_mode; \ + ULONG tx_thread_module_user_mode; \ + ULONG tx_thread_module_saved_lr; \ + VOID *tx_thread_module_kernel_stack_start; \ + VOID *tx_thread_module_kernel_stack_end; \ + ULONG tx_thread_module_kernel_stack_size; \ + VOID *tx_thread_module_stack_ptr; \ + VOID *tx_thread_module_stack_start; \ + VOID *tx_thread_module_stack_end; \ + ULONG tx_thread_module_stack_size; \ + VOID *tx_thread_module_reserved; + +The following extensions must also be defined in tx_port.h: + +#define TX_EVENT_FLAGS_GROUP_EXTENSION VOID *tx_event_flags_group_module_instance; \ + VOID (*tx_event_flags_group_set_module_notify)(struct TX_EVENT_FLAGS_GROUP_STRUCT *group_ptr); + +#define TX_QUEUE_EXTENSION VOID *tx_queue_module_instance; \ + VOID (*tx_queue_send_module_notify)(struct TX_QUEUE_STRUCT *queue_ptr); + +#define TX_SEMAPHORE_EXTENSION VOID *tx_semaphore_module_instance; \ + VOID (*tx_semaphore_put_module_notify)(struct TX_SEMAPHORE_STRUCT *semaphore_ptr); + +#define TX_TIMER_EXTENSION VOID *tx_timer_module_instance; \ + VOID (*tx_timer_module_expiration_function)(ULONG id); +*/ + +/* Define the kernel stack size for a module thread. */ +#ifndef TXM_MODULE_KERNEL_STACK_SIZE +#define TXM_MODULE_KERNEL_STACK_SIZE 768 +#endif + +/* Define constants specific to the tools the module can be built with for this particular modules port. */ + +#define TXM_MODULE_IAR_COMPILER 0x00000000 +#define TXM_MODULE_RVDS_COMPILER 0x01000000 +#define TXM_MODULE_GNU_COMPILER 0x02000000 +#define TXM_MODULE_COMPILER_MASK 0xFF000000 +#define TXM_MODULE_OPTIONS_MASK 0x000000FF + + +/* Define the properties for this particular module port. */ + +/* No port dispatch. It exists so a port can service kernel requests specific to + it, and this one has none: the only calls the Armv8-M port dispatches are its + secure-stack allocate and free, which belong to the security extension this core + does not have. Defining it would have the module manager call a + _txm_module_manager_port_dispatch that does not exist. The Cortex-R4 module + port leaves it undefined for the same reason. */ + +/* #define TXM_MODULE_PORT_DISPATCH */ + +#define TXM_MODULE_MEMORY_PROTECTION_ENABLED + +#ifdef TXM_MODULE_MEMORY_PROTECTION_ENABLED +#define TXM_MODULE_REQUIRE_ALLOCATED_OBJECT_MEMORY +#else +#define TXM_MODULE_REQUIRE_LOCAL_OBJECT_MEMORY +#endif + +#define TXM_MODULE_USER_MODE 0x00000001 +#define TXM_MODULE_MEMORY_PROTECTION 0x00000002 +#define TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS 0x00000004 + + +/* Define the supported options for this module. */ + +#define TXM_MODULE_MANAGER_SUPPORTED_OPTIONS (TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION | TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS) +#define TXM_MODULE_MANAGER_REQUIRED_OPTIONS 0 + + +/* Define offset adjustments according to the compiler used to build the module. */ + +#define TXM_MODULE_IAR_SHELL_ADJUST 24 +#define TXM_MODULE_IAR_START_ADJUST 28 +#define TXM_MODULE_IAR_STOP_ADJUST 32 +#define TXM_MODULE_IAR_CALLBACK_ADJUST 44 + +#define TXM_MODULE_RVDS_SHELL_ADJUST 0 +#define TXM_MODULE_RVDS_START_ADJUST 0 +#define TXM_MODULE_RVDS_STOP_ADJUST 0 +#define TXM_MODULE_RVDS_CALLBACK_ADJUST 0 + +#define TXM_MODULE_GNU_SHELL_ADJUST 24 +#define TXM_MODULE_GNU_START_ADJUST 28 +#define TXM_MODULE_GNU_STOP_ADJUST 32 +#define TXM_MODULE_GNU_CALLBACK_ADJUST 44 + + +/* Define other module port-specific constants. */ + +/* Define INLINE_DECLARE to inline for ARM compiler. */ + +#define INLINE_DECLARE inline + +/* Define the number of MPU entries assigned to the code and data sections. + + PMSAv8-R gives this core many more regions than Armv8-M gives an M33, and that + changes the design rather than just the numbers. MPUIR on the S32Z280 reports + 20 EL1 regions, where an M33 has 8 in total and the kernel and a module must + therefore share them. Here the kernel's map can keep its own regions + permanently and the manager owns a separate block, so a module switch + reprograms only the module's regions and never rebuilds the kernel's. + + Eight entries are reserved, laid out as on the M33 -- one for the kernel entry + function, one for module code, one for module data, five for shared memory -- + starting at TXM_MODULE_MPU_FIRST_REGION. The indices below are relative to + that base. + + TXM_MODULE_MPU_FIRST_REGION is 8, and the choice is not arbitrary. The TRM + provides direct access to PRBAR0 through PRBAR15 and PRLAR0 through PRLAR15 + (3.3.85, 3.3.86), encoded CRn c6, CRm c8 + n/2, opc2 0 and 1 for an even + region and 4 and 5 for an odd one. Regions above 15 are reachable only + through PRSELR, which selects a region for the indirect PRBAR and PRLAR view + and needs an ISB before those registers can be written -- per region. + + Measured on this part: programming one region through PRSELR costs 542 to 604 + cycles, and the same region written directly costs 434 to 470. Most of what + remains is the closing dsb and isb rather than the writes, so a block of + regions written directly with one barrier pair at the end is far cheaper than + the per-region figure suggests, while the PRSELR route pays an ISB every time. + + Eight entries at 8 through 15 therefore sit entirely within the directly + addressable range, and the board support package's map occupies 0 through 7. + A board needing more than eight kernel regions has to either shrink this block + or accept PRSELR for the overflow. */ + +#define TXM_MODULE_MPU_FIRST_REGION 8 +#define TXM_MODULE_MPU_TOTAL_ENTRIES 8 +#define TXM_MODULE_MPU_KERNEL_ENTRY_INDEX 0 +#define TXM_MODULE_MPU_CODE_INDEX 1 +#define TXM_MODULE_MPU_DATA_INDEX 2 + +#define TXM_MODULE_MPU_SHARED_INDEX 3 +#define TXM_MODULE_MPU_SHARED_ENTRIES 5 + +#define TXM_MODULE_ATTRIBUTE_NON_SHAREABLE 0x00 +#define TXM_MODULE_ATTRIBUTE_OUTER_SHAREABLE 0x10 +#define TXM_MODULE_ATTRIBUTE_INNER_SHAREABLE 0x18 +#define TXM_MODULE_ATTRIBUTE_READ_WRITE 0x02 +#define TXM_MODULE_ATTRIBUTE_READ_ONLY 0x06 +#define TXM_MODULE_ATTRIBUTE_EXECUTE_NEVER 0x01 +#define TXM_MODULE_ATTRIBUTE_REGION_ENABLE 0x01 +#define TXM_MODULE_ATTRIBUTE_MASK 0x1E +#define TXM_MODULE_ATTRIBUTE_INDEX 0x00 + +/* The attribute encodings above are the PMSAv8 field layout and are the same on + this core as on an M33: shareability in PRBAR[4:3], access permission in + PRBAR[2:1], execute-never in PRBAR[0], attribute index in PRLAR[3:1] and the + region enable in PRLAR[0]. What differs is the granule -- 64 bytes here + against 32 on Armv8-M, so the base and limit fields are [31:6] rather than + [31:5] -- and that the registers are reached through CP15 with PRSELR + selecting the region, rather than being memory mapped. */ + +/* MAIR_ATTR settings +Device-nGnRE : 0b [Outer]0000 [Inner]0100 +Code WT RA : 0b [Outer]1010 [Inner]1010 +DATA WBWA RA : 0b [Outer]1111 [Inner]1111 +*/ + +/* Data aligned to 8 bytes (stacks must be 8-byte aligned). */ +#define TXM_MODULE_DATA_ALIGNMENT 8 + +/* MPU regions must be 64-byte aligned on PMSAv8-R, where Armv8-M requires 32. + Getting this wrong is silent: the low bits of PRBAR and PRLAR hold attributes, + so an under-aligned base does not fault, it changes the shareability and + permissions of the region instead. + + Unsigned, and that is not decoration. Every use of this macro is a mask over + an address or a size -- both ULONG -- and the useful form is the complement, + ~(TXM_MODULE_MPU_ALIGNMENT - 1). Spelled as a plain 64 that complement is a + signed -64 converted to 0xFFFFFFC0 on the way into the expression, which is + what MISRA C:2012 Rule 7.2 requires a U suffix to prevent and what + -Wsign-conversion reports. The value is identical either way here; the + suffix is what keeps the essential type of the mask unsigned. */ +#define TXM_MODULE_MPU_ALIGNMENT 64UL + +/* Mask that keeps only the address bits of PRBAR and PRLAR. The low six bits + hold attributes on this core, so every base and limit written into the region + table must be masked with this and not with the Armv8-M 32-byte equivalent. */ + +#define TXM_MODULE_MPU_ADDRESS_MASK 0xFFFFFFC0 + +/* No secure-stack extension calls. Those are the Armv8-M security extension, + which this core does not have: privilege here is the ARM mode, EL1 against + EL0, and there is no secure world to allocate a second stack in. */ + +/* Two registers set up each MPU region: PRBAR and PRLAR, selected by PRSELR. + Held per module so a switch is a sequence of register writes with nothing to + compute -- the cost measured on this part is dominated by the barriers, not + the writes, so a switch should program every region and issue one dsb and isb + at the end rather than a pair per region. */ +typedef struct TXM_MODULE_MPU_INFO_STRUCT +{ + ULONG txm_module_mpu_region_base_address; + ULONG txm_module_mpu_region_limit_address; +} TXM_MODULE_MPU_INFO; + +/* Define the port-extensions to the module manager instance structure. */ +#define TXM_MODULE_MANAGER_PORT_EXTENSION \ + TXM_MODULE_MPU_INFO txm_module_instance_mpu_registers[TXM_MODULE_MPU_TOTAL_ENTRIES]; \ + ULONG txm_module_instance_shared_memory_count; \ + ULONG txm_module_instance_shared_memory_address[TXM_MODULE_MPU_SHARED_ENTRIES]; \ + ULONG txm_module_instance_shared_memory_length[TXM_MODULE_MPU_SHARED_ENTRIES]; + + +/* Define the memory fault information structure that is populated when a memory fault occurs. */ + +typedef struct TXM_MODULE_MANAGER_MEMORY_FAULT_INFO_STRUCT +{ + TX_THREAD *txm_module_manager_memory_fault_info_thread_ptr; + VOID *txm_module_manager_memory_fault_info_code_location; + /* Fault status and address, read through CP15. Armv8-M reports a memory + fault in SHCSR, CFSR, MMFAR and BFAR; this core splits it by access type + instead -- DFSR and DFAR for a data abort, IFSR and IFAR for a prefetch + abort -- and both pairs are recorded because a module can fault either + way: writing outside its data region, or branching outside its code + region. */ + + ULONG txm_module_manager_memory_fault_info_dfsr; + ULONG txm_module_manager_memory_fault_info_dfar; + ULONG txm_module_manager_memory_fault_info_ifsr; + ULONG txm_module_manager_memory_fault_info_ifar; + ULONG txm_module_manager_memory_fault_info_sp; + ULONG txm_module_manager_memory_fault_info_r0; + ULONG txm_module_manager_memory_fault_info_r1; + ULONG txm_module_manager_memory_fault_info_r2; + ULONG txm_module_manager_memory_fault_info_r3; + ULONG txm_module_manager_memory_fault_info_r4; + ULONG txm_module_manager_memory_fault_info_r5; + ULONG txm_module_manager_memory_fault_info_r6; + ULONG txm_module_manager_memory_fault_info_r7; + ULONG txm_module_manager_memory_fault_info_r8; + ULONG txm_module_manager_memory_fault_info_r9; + ULONG txm_module_manager_memory_fault_info_r10; + ULONG txm_module_manager_memory_fault_info_r11; + ULONG txm_module_manager_memory_fault_info_r12; + ULONG txm_module_manager_memory_fault_info_lr; + /* SPSR rather than xPSR: the mode the faulting code was running in is what + says whether it was the module in user mode or the kernel. */ + + ULONG txm_module_manager_memory_fault_info_spsr; +} TXM_MODULE_MANAGER_MEMORY_FAULT_INFO; + + +#define TXM_MODULE_MANAGER_FAULT_INFO \ + TXM_MODULE_MANAGER_MEMORY_FAULT_INFO _txm_module_manager_memory_fault_info; + + +/* Define the macro to check the code alignment. */ + +#define TXM_MODULE_MANAGER_CHECK_CODE_ALIGNMENT(module_location, code_alignment) \ + { \ + ULONG temp; \ + temp = (ULONG) module_location; \ + temp = temp & (code_alignment - 1); \ + if (temp) \ + { \ + _tx_mutex_put(&_txm_module_manager_mutex); \ + return(TXM_MODULE_ALIGNMENT_ERROR); \ + } \ + } + + +/* Define the macro to adjust the alignment and size for code/data areas. */ + +#define TXM_MODULE_MANAGER_ALIGNMENT_ADJUST(module_preamble, code_size, code_alignment, data_size, data_alignment) _txm_module_manager_alignment_adjust(module_preamble, &code_size, &code_alignment, &data_size, &data_alignment); + + +/* Define the macro to adjust the symbols in the module preamble. */ + +#define TXM_MODULE_MANAGER_CALCULATE_ADJUSTMENTS(properties, shell_function_adjust, start_function_adjust, stop_function_adjust, callback_function_adjust) \ + if ((properties & TXM_MODULE_COMPILER_MASK) == TXM_MODULE_IAR_COMPILER) \ + { \ + shell_function_adjust = TXM_MODULE_IAR_SHELL_ADJUST; \ + start_function_adjust = TXM_MODULE_IAR_START_ADJUST; \ + stop_function_adjust = TXM_MODULE_IAR_STOP_ADJUST; \ + callback_function_adjust = TXM_MODULE_IAR_CALLBACK_ADJUST; \ + } \ + else if ((properties & TXM_MODULE_COMPILER_MASK) == TXM_MODULE_RVDS_COMPILER) \ + { \ + shell_function_adjust = TXM_MODULE_RVDS_SHELL_ADJUST; \ + start_function_adjust = TXM_MODULE_RVDS_START_ADJUST; \ + stop_function_adjust = TXM_MODULE_RVDS_STOP_ADJUST; \ + callback_function_adjust = TXM_MODULE_RVDS_CALLBACK_ADJUST; \ + } \ + else \ + { \ + shell_function_adjust = TXM_MODULE_GNU_SHELL_ADJUST; \ + start_function_adjust = TXM_MODULE_GNU_START_ADJUST; \ + stop_function_adjust = TXM_MODULE_GNU_STOP_ADJUST; \ + callback_function_adjust = TXM_MODULE_GNU_CALLBACK_ADJUST; \ + } + + +/* Define the macro to populate the thread control block with module port-specific information. + Check if the module is in user mode and set up txm_module_thread_entry_info_kernel_call_dispatcher accordingly. +*/ + +#define TXM_MODULE_MANAGER_THREAD_SETUP(thread_ptr, module_instance) \ + thread_ptr -> tx_thread_module_current_user_mode = module_instance -> txm_module_instance_property_flags & TXM_MODULE_USER_MODE; \ + thread_ptr -> tx_thread_module_user_mode = module_instance -> txm_module_instance_property_flags & TXM_MODULE_USER_MODE; \ + if (thread_ptr -> tx_thread_module_user_mode) \ + { \ + thread_entry_info -> txm_module_thread_entry_info_kernel_call_dispatcher = _txm_module_manager_user_mode_entry; \ + } \ + else \ + { \ + thread_entry_info -> txm_module_thread_entry_info_kernel_call_dispatcher = _txm_module_manager_kernel_dispatch; \ + } + + +/* Define the macro to populate the module control block with module port-specific information. + If memory protection is enabled, set up the MPU registers. +*/ +#define TXM_MODULE_MANAGER_MODULE_SETUP(module_instance) \ + if (module_instance -> txm_module_instance_property_flags & TXM_MODULE_USER_MODE) \ + { \ + if (module_instance -> txm_module_instance_property_flags & TXM_MODULE_MEMORY_PROTECTION) \ + { \ + _txm_module_manager_mm_register_setup(module_instance); \ + } \ + } \ + else \ + { \ + /* Do nothing. */ \ + } + +/* Define the macro to perform port-specific functions when unloading the module. */ +/* Nothing needs to be done for this port. */ +#define TXM_MODULE_MANAGER_MODULE_UNLOAD(module_instance) + + +/* Define the macros to perform port-specific checks when passing pointers to the kernel. */ + +/* Define macro to make sure object is inside the module's data. */ +#define TXM_MODULE_MANAGER_CHECK_INSIDE_DATA(module_instance, obj_ptr, obj_size) \ + _txm_module_manager_inside_data_check(module_instance, obj_ptr, obj_size) + +/* Define some internal prototypes to this module port. */ + +#ifndef TX_SOURCE_CODE +#define txm_module_manager_memory_fault_notify _txm_module_manager_memory_fault_notify +#endif + + +#ifdef TX_SOURCE_CODE + +#endif + +#define TXM_MODULE_MANAGER_ADDITIONAL_PROTOTYPES \ +VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, ULONG *code_size, ULONG *code_alignment, ULONG *data_size, ULONG *data_alignment); \ +VOID _txm_module_manager_memory_fault_handler(VOID); \ +UINT _txm_module_manager_memory_fault_notify(VOID (*notify_function)(TX_THREAD *, TXM_MODULE_INSTANCE *)); \ +VOID _txm_module_manager_mm_register_setup(TXM_MODULE_INSTANCE *module_instance); \ +UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr, UINT obj_size); \ +ALIGN_TYPE _txm_module_manager_port_dispatch(TXM_MODULE_INSTANCE *module_instance, ULONG kernel_request, ALIGN_TYPE param_0, ALIGN_TYPE param_1, ALIGN_TYPE param_2); + + +#define TXM_MODULE_MANAGER_VERSION_ID \ +CHAR _txm_module_manager_version_id[] = \ + "Copyright (c) 2024 Microsoft Corporation. * ThreadX Module Cortex-R52/GNU Version 6.5.0.202601 *"; + +#endif diff --git a/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S new file mode 100644 index 000000000..8d2b64d1f --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S @@ -0,0 +1,211 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE LIBRARY RELEASE */ +@/* */ +@/* txm_module_gcc_setup.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* Where a position-independent module fixes up its own data */ +@/* references, before it has run a line of its own code. */ +@/* */ +@/* The module's shell entry calls this once, on the start thread only, */ +@/* passing the address the module's code was actually loaded at. Three */ +@/* things happen here, and the module cannot touch a single global */ +@/* until all three have: */ +@/* */ +@/* 1. The global offset table is copied out of the image into the */ +@/* module's own data, and every entry in it is rewritten from the */ +@/* nominal address the linker chose to the address the manager */ +@/* decided on. */ +@/* 2. .data is copied out of the image into the module's own data. */ +@/* 3. .bss is zeroed. */ +@/* */ +@/* Why any of this is needed: the module is built -fpic with */ +@/* -msingle-pic-base, so a reference to a global compiles to */ +@/* LDR rX, [r9, #offset] -- a load of a GOT entry, at a fixed offset */ +@/* from whatever r9 holds. The manager seeds r9 with the module's data */ +@/* base when it builds the thread's stack frame, so the offsets land in */ +@/* the right place; but the entries there are zero until this function */ +@/* writes them, because the manager memsets the data area and nothing */ +@/* else populates it. */ +@/* */ +@/* And .data has to be copied for the same reason. The manager does */ +@/* not copy it: _txm_module_manager_internal_load allocates a data area */ +@/* from its byte pool and zeroes it, so the module's linked .data -- */ +@/* wherever it sits -- is outside every region the module owns. A */ +@/* module reading an initialised global before this ran would fault, or */ +@/* read a zero, depending on where it looked. */ +@/* */ +@/* This function may not use a global itself, which is the constraint */ +@/* that shapes it: it is what makes the GOT usable, so it cannot use */ +@/* the GOT. Every address it needs comes from a literal pool load of a */ +@/* linker-supplied nominal address, rebased by hand. That is also why */ +@/* it is assembly and not C. */ +@/* */ +@/* INPUT */ +@/* */ +@/* r0 Address the module's code was actually loaded at */ +@/* r9 Address of the module's data area, already seeded by */ +@/* _txm_module_manager_thread_stack_build. Also the base the */ +@/* compiler measures every GOT offset from, so the GOT must go */ +@/* at exactly this address and nowhere else. */ +@/* */ +@/* OUTPUT */ +@/* */ +@/* None. r9 is unchanged, and must be: it is the module's PIC base for */ +@/* the rest of the thread's life. */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* _txm_module_thread_shell_entry, on the start thread only */ +@/* */ +@/**************************************************************************/ + + .syntax unified + .arm + .text + .align 2 + + .global _gcc_setup + .type _gcc_setup, %function + +@ Supplied by the module's linker script. The two segment origins are nominal +@ addresses the module never runs at -- see link_demo_module.lds. + + .extern __code_segment_start__ + .extern __data_segment_start__ + .extern __got_load_start__ + .extern __new_got_start__ + .extern __new_got_end__ + .extern __data_load_start__ + .extern __data_start__ + .extern __data_end__ + .extern __bss_start__ + .extern __bss_end__ + +_gcc_setup: + + PUSH {r4-r7, lr} + +@ The two deltas every address below is corrected by. Everything the linker +@ emitted is expressed against the nominal origins; everything that exists at +@ run time is at the same offset from the real base. + + LDR r3, =__code_segment_start__ + SUB r4, r0, r3 @ r4 = code delta, real - nominal + LDR r3, =__data_segment_start__ + SUB r5, r9, r3 @ r5 = data delta, real - nominal + +@ --------------------------------------------------------------------------- +@ 1. The global offset table. +@ --------------------------------------------------------------------------- +@ +@ Read from the image, written to the module's data area, one entry at a time, +@ with each entry rebased according to which segment it points into. The test +@ is a single unsigned compare against the nominal data origin, which is what +@ the linker script's far-apart origins exist to make sound: everything below +@ it is code or read-only data and moves with the code, everything at or above +@ it is writable data and moves with the data area. +@ +@ A zero entry is left alone. The linker leaves an entry zero when it had +@ nothing to put there, and rebasing it would turn a recognisable null into a +@ plausible-looking pointer into the middle of the module. + + LDR r1, =__got_load_start__ + ADD r1, r1, r4 @ Source, in the image + LDR r2, =__new_got_start__ + ADD r2, r2, r5 @ Destination, in the data area + LDR r3, =__new_got_end__ + ADD r3, r3, r5 @ One past the last entry + LDR r7, =__data_segment_start__ @ The code / data dividing line + +got_loop: + CMP r2, r3 + BHS got_done + LDR r6, [r1], #4 @ Pickup the entry as linked + CMP r6, #0 + BEQ got_store @ Never filled in; leave it alone + CMP r6, r7 + ADDLO r6, r6, r4 @ Below the line: a code address + ADDHS r6, r6, r5 @ At or above: a data address +got_store: + STR r6, [r2], #4 + B got_loop +got_done: + +@ --------------------------------------------------------------------------- +@ 2. Initialised data. +@ --------------------------------------------------------------------------- +@ +@ Byte at a time. The run-time destination is the manager's data base plus a +@ linker offset, and while both happen to be word aligned today, a byte copy +@ does not have to care -- and this runs once per module start, where the cost +@ of not having to think about it is nothing. + + LDR r1, =__data_load_start__ + ADD r1, r1, r4 @ Source, in the image + LDR r2, =__data_start__ + ADD r2, r2, r5 @ Destination, in the data area + LDR r3, =__data_end__ + ADD r3, r3, r5 + +data_loop: + CMP r2, r3 + BHS data_done + LDRB r6, [r1], #1 + STRB r6, [r2], #1 + B data_loop +data_done: + +@ --------------------------------------------------------------------------- +@ 3. Zero-initialised data. +@ --------------------------------------------------------------------------- +@ +@ The manager has already zeroed the whole allocation, so this is redundant +@ with the loader this port ships. It is here because that is the loader's +@ behaviour and not the module's contract: a module that only works because +@ its loader happens to memset is a module that breaks on the next loader. + + LDR r1, =__bss_start__ + ADD r1, r1, r5 + LDR r2, =__bss_end__ + ADD r2, r2, r5 + MOV r6, #0 + +bss_loop: + CMP r1, r2 + BHS bss_done + STRB r6, [r1], #1 + B bss_loop +bss_done: + +@ The module's data is now readable and writable through r9, and the module +@ proper can run. A data barrier because the GOT and .data were just written +@ as data and are about to be read as the module's working set; no instruction +@ barrier, because nothing executable was modified. + + DSB + POP {r4-r7, lr} + BX lr + + .size _gcc_setup, . - _gcc_setup diff --git a/ports_module/cortex_r52/gnu/module_lib/src/txm_module_thread_shell_entry.c b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_thread_shell_entry.c new file mode 100644 index 000000000..4f01b7a8c --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_lib/src/txm_module_thread_shell_entry.c @@ -0,0 +1,168 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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 + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#ifndef TXM_MODULE +#define TXM_MODULE +#endif + +#ifndef TX_SOURCE_CODE +#define TX_SOURCE_CODE +#endif + + +/* Include necessary system files. */ + +#include "txm_module.h" +#include "tx_thread.h" + +/* Define the global module entry pointer from the start thread of the module. */ + +TXM_MODULE_THREAD_ENTRY_INFO *_txm_module_entry_info; + + +/* Define the dispatch function pointer used in the module implementation. */ + +ULONG (*_txm_module_kernel_call_dispatcher)(ULONG kernel_request, ULONG param_1, ULONG param_2, ULONG param3); + + +/* Define the GCC startup code that clears the uninitialized global data and sets up the + preset global variables. */ + +extern VOID _gcc_setup(TXM_MODULE_INSTANCE *); + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_thread_shell_entry Cortex-R52/GNU */ +/* 6.1.5 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function calls the specified entry function of the thread. It */ +/* also provides a place for the thread's entry function to return. */ +/* If the thread returns, this function places the thread in a */ +/* "COMPLETED" state. */ +/* */ +/* INPUT */ +/* */ +/* thread_ptr Pointer to current thread */ +/* thread_info Pointer to thread entry info */ +/* */ +/* OUTPUT */ +/* */ +/* None */ +/* */ +/* CALLS */ +/* */ +/* _gcc_setup GNU global init function */ +/* thread_entry Thread's entry function */ +/* tx_thread_resume Resume the module callback thread */ +/* _txm_module_thread_system_suspend Module thread suspension routine */ +/* */ +/* CALLED BY */ +/* */ +/* Initial thread stack frame */ +/* */ +/**************************************************************************/ +VOID _txm_module_thread_shell_entry(TX_THREAD *thread_ptr, TXM_MODULE_THREAD_ENTRY_INFO *thread_info) +{ + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + VOID (*entry_exit_notify)(TX_THREAD *, UINT); +#endif + + + /* Determine if this is the start thread. If so, we must prepare the module for + execution. If not, simply skip the C startup code. */ + if (thread_info -> txm_module_thread_entry_info_start_thread) + { + /* Initialize the GNU C environment. */ + _gcc_setup(thread_info -> txm_module_thread_entry_info_code_base_address); + + /* Save the entry info pointer, for later use. */ + _txm_module_entry_info = thread_info; + + /* Save the kernel function dispatch address. This is used to make all resident calls from + the module. */ + _txm_module_kernel_call_dispatcher = thread_info -> txm_module_thread_entry_info_kernel_call_dispatcher; + + /* Ensure that we have a valid pointer. */ + while (!_txm_module_kernel_call_dispatcher) + { + /* Loop here, if an error is present getting the dispatch function pointer! + An error here typically indicates the resident portion of _tx_thread_schedule + is not supporting the trap to obtain the function pointer. */ + } + + /* Resume the module's callback thread, already created in the manager. */ + _txe_thread_resume(thread_info -> txm_module_thread_entry_info_callback_request_thread); + } + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* Pickup the entry/exit application callback routine. */ + entry_exit_notify = thread_info -> txm_module_thread_entry_info_exit_notify; + + /* Determine if an application callback routine is specified. */ + if (entry_exit_notify != TX_NULL) + { + + /* Yes, notify application that this thread has been entered! */ + (entry_exit_notify)(thread_ptr, TX_THREAD_ENTRY); + } +#endif + + /* Call current thread's entry function. */ + (thread_info -> txm_module_thread_entry_info_entry) (thread_info -> txm_module_thread_entry_info_parameter); + + /* Suspend thread with a "completed" state. */ + + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* Pickup the entry/exit application callback routine again. */ + entry_exit_notify = thread_info -> txm_module_thread_entry_info_exit_notify; + + /* Determine if an application callback routine is specified. */ + if (entry_exit_notify != TX_NULL) + { + + /* Yes, notify application that this thread has exited! */ + (entry_exit_notify)(thread_ptr, TX_THREAD_EXIT); + } +#endif + + /* Call actual thread suspension routine. */ + _txm_module_thread_system_suspend(thread_ptr); + +#ifdef TX_SAFETY_CRITICAL + + /* If we ever get here, raise safety critical exception. */ + TX_SAFETY_CRITICAL_EXCEPTION(__FILE__, __LINE__, 0); +#endif +} + diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S new file mode 100644 index 000000000..90865906b --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S @@ -0,0 +1,264 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + + .arm + +@ Whole CPSR values, mode field and interrupt masks together -- which is what a +@ plain _MODE name means throughout this port. The bare mode field is spelled +@ _MODE_BITS instead; see tx_thread_stack_build.S. The base port's SVC_MODE is +@ absent because this port restores kernel threads into System mode, so nothing +@ here needs it and a name that is never used is a name waiting to be picked by +@ mistake. + +#ifdef TX_ENABLE_FIQ_SUPPORT +SYS_MODE = 0xDF @ Disable IRQ/FIQ, SYS mode +IRQ_MODE = 0xD2 @ Disable IRQ/FIQ, IRQ mode +#else +SYS_MODE = 0x9F @ Disable IRQ, SYS 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) +@ +@ /* SYS mode rather than SVC, and this is the substantive difference between +@ this file and the base port's. The interrupted thread's stack is the +@ banked sp that User and System mode share, whoever the thread belongs to: +@ a module thread was in User mode, and a kernel thread in this port starts +@ in System mode for exactly this reason. Entering SVC mode here would +@ push the interrupted context onto the supervisor call handler's stack +@ instead of the thread's, and the thread would resume with a corrupted +@ frame. */ +@ + MOV r2, #SYS_MODE @ Build SYS mode CPSR + MSR CPSR_c, r2 @ Enter SYS 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, #SYS_MODE @ Build SYS mode CPSR + MSR CPSR_c, r5 @ Enter SYS 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, #SYS_MODE @ Build SYS mode CPSR + MSR CPSR_c, r0 @ Enter SYS mode + B _tx_thread_schedule @ Return to scheduler +@} + + + diff --git a/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_save.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_save.S new file mode 100644 index 000000000..9b61077e4 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/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 +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** 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_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S new file mode 100644 index 000000000..b97309a15 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S @@ -0,0 +1,435 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + +@ Offsets into TX_THREAD and TXM_MODULE_INSTANCE. Hard-coded offsets in port +@ assembly are a standing hazard -- see issue #577 -- so these are checked +@ against offsetof at compile time in txm_module_manager_offset_check.c, and a +@ mismatch is a build failure rather than a fault at run time. +@ +@ They are not the same as the Armv8-M port's: there the module instance pointer +@ sits at 0x90, and here it is 0x94 because the Cortex-R52 port keeps +@ tx_thread_vfp_enable ahead of the module fields in TX_THREAD_EXTENSION_2. + + .equ TXM_THREAD_MODULE_INSTANCE, 0x94 + .equ TXM_INSTANCE_DATA_START, 0x2C + .equ TXM_INSTANCE_MPU_REGISTERS, 0x64 + +@ Every thread in this port runs on the User-banked stack pointer: kernel threads +@ in System mode, module threads in User mode, and those two modes share SP_usr +@ and LR_usr. That shared bank is what lets one save and restore path reach any +@ thread's stack without first asking who owns it. +@ +@ So the scheduler has to run in System mode rather than SVC. It is already in +@ System mode when it is reached from the context restore, from the system return +@ and from the idle path, but it is in SVC mode on the very first dispatch out of +@ the kernel enter, so the mode is forced below instead of assumed. Getting this +@ wrong is silent rather than loud: writing sp in SVC mode sets SP_svc, the +@ exception return then switches to System mode, and the thread starts life on +@ whatever SP_usr happened to hold. +@ +@ The return itself needs a mode that has an SPSR, which System mode has not got, +@ so the frame is popped in System mode and only the final exception return +@ borrows SVC. + + .equ SVC_MODE_BITS, 0x13 + .equ SYS_MODE_BITS, 0x1F + +@ Offsets from the saved r0 word of an interrupt frame, which is where sp points +@ once the stack type, the saved CPSR and any VFP state have been popped. The +@ frame itself is built by tx_thread_stack_build.S and the context restore. + + .equ FRAME_PC_FROM_R0, 56 + +@ The kernel's window over the module area. Must match MPU_MODULE_LOAD_REGION +@ in mpu.h; the offset check asserts the two agree. + + .equ MPU_MODULE_WINDOW_REGION, 16 + + .global mpu_module_window_prbar + .global mpu_module_window_prlar + + .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 +@ +@ /* Enter System mode before touching sp, so that the stack pointer written +@ below is the User-banked one every thread in this port actually runs on. +@ Nothing between here and the write uses the stack, which matters because +@ sp is undefined for this mode until it is set. */ +@ + CPS #SYS_MODE_BITS @ Threads run on SP_usr +@ + LDR sp, [r0, #8] @ Switch stack pointers + STR r3, [r2] @ Setup time-slice +@ +@ /* Load this thread's MPU regions, if it belongs to a module. */ +@ + LDR r1, [r0, #TXM_THREAD_MODULE_INSTANCE] @ Pickup module instance pointer + CMP r1, #0 @ Is this a module thread? + BEQ __tx_module_regions_off @ No, close the module regions + LDR r2, [r1, #TXM_INSTANCE_DATA_START] @ Pickup module data start + CMP r2, #0 @ Is protection configured? + BEQ __tx_module_regions_off @ No, close the module regions +@ +@ /* Eight regions, written directly rather than through PRSELR. CRm and opc2 +@ are encoded in the instruction, not held in a register, so this cannot be +@ a loop -- and unrolled is what we want anyway: sixteen writes with one +@ barrier pair at the end, where PRSELR would need an ISB per region. +@ +@ Regions 8 and 9 are CRm c12, 10 and 11 are c13, 12 and 13 are c14, 14 and +@ 15 are c15. Even regions use opc2 0 and 1, odd ones 4 and 5. */ +@ + ADD r1, r1, #TXM_INSTANCE_MPU_REGISTERS @ Address of the region table +@ + LDMIA r1!, {r2, r3} @ Region 8: base, limit + MCR p15, 0, r2, c6, c12, 0 @ PRBAR8 + MCR p15, 0, r3, c6, c12, 1 @ PRLAR8 + LDMIA r1!, {r2, r3} @ Region 9 + MCR p15, 0, r2, c6, c12, 4 @ PRBAR9 + MCR p15, 0, r3, c6, c12, 5 @ PRLAR9 + LDMIA r1!, {r2, r3} @ Region 10 + MCR p15, 0, r2, c6, c13, 0 @ PRBAR10 + MCR p15, 0, r3, c6, c13, 1 @ PRLAR10 + LDMIA r1!, {r2, r3} @ Region 11 + MCR p15, 0, r2, c6, c13, 4 @ PRBAR11 + MCR p15, 0, r3, c6, c13, 5 @ PRLAR11 + LDMIA r1!, {r2, r3} @ Region 12 + MCR p15, 0, r2, c6, c14, 0 @ PRBAR12 + MCR p15, 0, r3, c6, c14, 1 @ PRLAR12 + LDMIA r1!, {r2, r3} @ Region 13 + MCR p15, 0, r2, c6, c14, 4 @ PRBAR13 + MCR p15, 0, r3, c6, c14, 5 @ PRLAR13 + LDMIA r1!, {r2, r3} @ Region 14 + MCR p15, 0, r2, c6, c15, 0 @ PRBAR14 + MCR p15, 0, r3, c6, c15, 1 @ PRLAR14 + LDMIA r1!, {r2, r3} @ Region 15 + MCR p15, 0, r2, c6, c15, 4 @ PRBAR15 + MCR p15, 0, r3, c6, c15, 5 @ PRLAR15 +@ +@ +@ /* Close the kernel's window over the module area. It covers the same +@ memory the regions just loaded do, and PMSAv8-R has no region priority: +@ two enabled regions matching one address is CONSTRAINED UNPREDICTABLE, +@ and on this part it aborts. A module thread reaches its own memory +@ through the regions above and needs nothing else. +@ +@ Region 16 is above the direct-access encodings, so it goes through +@ PRSELR. Clearing PRLAR is what disables it. */ +@ + MOV r2, #MPU_MODULE_WINDOW_REGION + MCR p15, 0, r2, c6, c2, 1 @ PRSELR + ISB + MOV r2, #0 + MCR p15, 0, r2, c6, c3, 1 @ PRLAR, region disabled +@ + DSB sy @ One barrier pair for the block + ISB + B __tx_module_regions_done +@ +__tx_module_regions_off: +@ +@ /* Not a module thread, or a module without protection. Close all eight +@ regions rather than leaving the previous module's windows open: a thread +@ that owns nothing must reach nothing, and the alternative is that whatever +@ ran last silently lends its memory to whatever runs next. +@ +@ Only PRLAR is written, because clearing its enable bit is what closes the +@ region; the base is irrelevant while it is disabled. */ +@ + MOV r2, #0 @ Region disabled + MCR p15, 0, r2, c6, c12, 1 @ PRLAR8 + MCR p15, 0, r2, c6, c12, 5 @ PRLAR9 + MCR p15, 0, r2, c6, c13, 1 @ PRLAR10 + MCR p15, 0, r2, c6, c13, 5 @ PRLAR11 + MCR p15, 0, r2, c6, c14, 1 @ PRLAR12 + MCR p15, 0, r2, c6, c14, 5 @ PRLAR13 + MCR p15, 0, r2, c6, c15, 1 @ PRLAR14 + MCR p15, 0, r2, c6, c15, 5 @ PRLAR15 +@ +@ /* Open the kernel's window over the module area, now that no module region +@ is enabled to overlap it. This is the only thing that lets privileged +@ code reach module memory: no boot region covers it, so without this the +@ manager cannot read a module's preamble to load it. +@ +@ Turning it on here, rather than leaving it on permanently, is what keeps +@ it and the module's own regions mutually exclusive without anyone having +@ to remember to bracket a call. A thread that owns no module gets the +@ window; a thread that owns one gets its regions instead. +@ +@ The two register words are computed once by mpu_module_window_init and +@ read from memory here, so this code holds no register layout. */ +@ + MOV r2, #MPU_MODULE_WINDOW_REGION + MCR p15, 0, r2, c6, c2, 1 @ PRSELR + ISB + LDR r2, =mpu_module_window_prbar + LDR r3, [r2] + MCR p15, 0, r3, c6, c3, 0 @ PRBAR + LDR r2, =mpu_module_window_prlar + LDR r3, [r2] + MCR p15, 0, r3, c6, c3, 1 @ PRLAR, region enabled +@ + DSB sy + ISB +@ +__tx_module_regions_done: +@ +@ /* 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 +@ +@ /* r5 holds the thread's CPSR. It cannot go into SPSR yet: this is System +@ mode, which has no SPSR, so it is carried to the SVC borrow below. */ +@ +#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 +@ +@ /* sp now points at the saved r0, whether or not VFP state was in the way, +@ which is why the frame position is captured here rather than computed from +@ the thread's stack pointer. r1 survives the mode change: r0-r7 are common +@ to every mode. +@ +@ The two halves of the return have to be prepared in different modes. SVC +@ holds the SPSR and the return address; System mode holds the sp and lr the +@ thread itself will use. Preparing SVC first means the frame pop can end in +@ System mode with nothing left to do but the return. */ +@ + MOV r1, sp @ Frame position, at the saved r0 + CPS #SVC_MODE_BITS @ Borrow a mode that has an SPSR + MSR SPSR_cxsf, r5 @ Thread's CPSR, applied by the return + LDR lr, [r1, #FRAME_PC_FROM_R0] @ Thread's resume address + CPS #SYS_MODE_BITS @ Back to the thread's own bank +@ + LDMIA sp!, {r0-r12, lr} @ Restore, LR_usr included + ADD sp, sp, #4 @ Step over the saved pc +@ +@ /* SP_usr and LR_usr are now the thread's. Only the exception return is left, +@ and it must happen in the mode holding the SPSR -- switching back does not +@ disturb either, because SVC banks its own sp and lr. */ +@ + CPS #SVC_MODE_BITS @ Mode holding SPSR and the address + MOVS pc, lr @ 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_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S new file mode 100644 index 000000000..935b321ad --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S @@ -0,0 +1,191 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + + .arm + +@ Bare CPSR mode field, spelled _MODE_BITS as the base port does in +@ tx_thread_irq_nesting_start.S and tx_thread_fiq_context_restore.S. In this +@ port a plain _MODE name means a whole CPSR value with the interrupt masks +@ already in it -- tx_thread_context_restore.S defines SYS_MODE as 0xDF or 0x9F +@ -- so the two must not share a name. The base port's SVC_MODE = 0x13 here was +@ the frame's mode before this port moved kernel threads to System mode; nothing +@ uses it now, so it is gone rather than left to be picked up by mistake. + +SYS_MODE_BITS = 0x1F @ SYS mode, privileged +#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 +@ +@ /* SYS mode, not SVC. In a module port no thread runs in SVC mode: SVC is +@ reserved for the supervisor call handler, which is how a module reaches +@ the kernel, and a thread sitting in SVC mode would be using the handler's +@ banked stack pointer as its own. Kernel threads therefore start in SYS +@ mode, which is privileged and shares User mode's banked sp -- the same sp +@ a module thread uses -- so the context save and restore paths can reach a +@ thread's stack the same way whoever it belongs to. +@ +@ This is the one line that differs from the base port's stack build, and +@ leaving it as SVC would have put kernel threads on the SVC stack while the +@ restore path looked for them on the SYS one. */ +@ + ORR r3, r1, #SYS_MODE_BITS @ Build CPSR, SYS 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_module/cortex_r52/gnu/module_manager/src/tx_thread_system_return.S b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_system_return.S new file mode 100644 index 000000000..c0ab615b4 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/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 +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** 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_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c new file mode 100644 index 000000000..6850fe558 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c @@ -0,0 +1,97 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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 + **************************************************************************/ + +// Some portions generated by Claude Code (Opus 5). + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "txm_module.h" + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_alignment_adjust Cortex-R52 */ +/* 6.1.8 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function adjusts the alignment and size of the code and data */ +/* section for a given module implementation. */ +/* */ +/* INPUT */ +/* */ +/* module_preamble Pointer to module preamble */ +/* code_size Size of the code area (updated) */ +/* code_alignment Code area alignment (updated) */ +/* data_size Size of data area (updated) */ +/* data_alignment Data area alignment (updated) */ +/* */ +/* OUTPUT */ +/* */ +/* None */ +/* */ +/* CALLS */ +/* */ +/* None */ +/* */ +/* CALLED BY */ +/* */ +/* Initial thread stack frame */ +/* */ +/**************************************************************************/ +VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble, + ULONG *code_size, + ULONG *code_alignment, + ULONG *data_size, + ULONG *data_alignment) +{ + + /* The preamble is not read here, and the signature is not ours to change: + _txm_module_manager_internal_load calls this through a fixed prototype + shared by every port. Referenced and discarded so that the parameter is + used, which is what MISRA C:2012 Rule 2.7 asks for and what + -Wunused-parameter reports. A port that did have to grow or reposition a + module -- the Cortex-R4 one, below -- reads it. */ + + (VOID)module_preamble; + + /* Rounding to the granule is all this has to do, and that is a property of + PMSAv8-R rather than a shortcut. The Cortex-R4 port's equivalent runs to + 183 lines because PMSAv7 regions must be a power of two in size and + aligned to their own size, so a module's code and data have to be grown + and repositioned to fit the nearest legal region. Base and limit pairs + have no such constraint: any 64-byte-aligned extent is a legal region. */ + + /* Round code and data size UP to TXM_MODULE_MPU_ALIGNMENT bytes. */ + *code_size = (*code_size + TXM_MODULE_MPU_ALIGNMENT - 1) & ~(TXM_MODULE_MPU_ALIGNMENT - 1); + *data_size = (*data_size + TXM_MODULE_MPU_ALIGNMENT - 1) & ~(TXM_MODULE_MPU_ALIGNMENT - 1); + + /* Alignment for code and data is TXM_MODULE_MPU_ALIGNMENT bytes. */ + *code_alignment = TXM_MODULE_MPU_ALIGNMENT; + *data_alignment = TXM_MODULE_MPU_ALIGNMENT; +} diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c new file mode 100644 index 000000000..f63b4c145 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c @@ -0,0 +1,175 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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 + **************************************************************************/ + +// Some portions generated by Claude Code (Opus 5). + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "tx_mutex.h" +#include "tx_queue.h" +#include "tx_thread.h" +#include "txm_module.h" + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_external_memory_enable Cortex-R52 */ +/* 6.1.8 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function creates an entry in the MPU table for a shared */ +/* memory space. The start_address must be aligned to the PMSAv8-R */ +/* protection granule, which is 64 bytes on Cortex-R52 -- the comment */ +/* inherited from the Armv8-M port says 32, and TXM_MODULE_MPU_ALIGNMENT*/ +/* below is what is actually enforced. */ +/* */ +/* INPUT */ +/* */ +/* module_instance Module instance pointer */ +/* start_address Start address of memory */ +/* length Length of external memory */ +/* attributes Memory attributes (r/w) */ +/* */ +/* OUTPUT */ +/* */ +/* status Completion status */ +/* */ +/* CALLS */ +/* */ +/* _tx_mutex_get Get protection mutex */ +/* _tx_mutex_put Release protection mutex */ +/* */ +/* CALLED BY */ +/* */ +/* Application code */ +/* */ +/**************************************************************************/ +UINT _txm_module_manager_external_memory_enable(TXM_MODULE_INSTANCE *module_instance, + VOID *start_address, + ULONG length, + UINT attributes) +{ + +ULONG address; +ULONG shared_index; + + /* Determine if the module manager has not been initialized yet. */ + if (_txm_module_manager_ready != TX_TRUE) + { + /* Module manager has not been initialized. */ + return(TX_NOT_AVAILABLE); + } + + /* Determine if the module is valid. */ + if (module_instance == TX_NULL) + { + /* Invalid module pointer. */ + return(TX_PTR_ERROR); + } + + /* Get module manager protection mutex. */ + _tx_mutex_get(&_txm_module_manager_mutex, TX_WAIT_FOREVER); + + /* Determine if the module instance is valid. */ + if (module_instance -> txm_module_instance_id != TXM_MODULE_ID) + { + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* Invalid module pointer. */ + return(TX_PTR_ERROR); + } + + /* Determine if the module instance is in the loaded state. */ + if (module_instance -> txm_module_instance_state != TXM_MODULE_LOADED) + { + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* Return error if the module is not ready. */ + return(TX_START_ERROR); + } + + /* Determine if there are shared memory entries available. */ + if(module_instance -> txm_module_instance_shared_memory_count >= TXM_MODULE_MPU_SHARED_ENTRIES) + { + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* No more entries available. */ + return(TX_NO_MEMORY); + } + + /* Start address must adhere to Cortex-R52 MPU alignment. */ + address = (ULONG) start_address; + if(address != (address & ~(TXM_MODULE_MPU_ALIGNMENT - 1))) + { + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* Return alignment error. */ + return(TXM_MODULE_ALIGNMENT_ERROR); + } + + /* At this point, we have a valid address. Set up MPU registers. */ + + /* Pick up index into shared memory entries. */ + shared_index = TXM_MODULE_MPU_SHARED_INDEX + module_instance -> txm_module_instance_shared_memory_count; + + /* Set base address register with start address, sanitized attributes and execute never. */ + module_instance -> txm_module_instance_mpu_registers[shared_index].txm_module_mpu_region_base_address = address | (attributes & TXM_MODULE_ATTRIBUTE_MASK) | TXM_MODULE_ATTRIBUTE_EXECUTE_NEVER; + + /* Set the limit address (data start + length-1), attribute index, and enable bit. + + The limit is MASKED to the protection granule before the attributes are + ORed in, exactly as txm_module_manager_mm_register_setup.c does for the + code and data regions, and this is not cosmetic. PRLAR holds LIMIT[31:6] + with the low six bits carrying RES0[5:4], AttrIndx[3:1] and EN[0], and the + hardware reads the limit back as LIMIT concatenated with 0x3F -- so the + masked value describes exactly the same inclusive end address. Without + the mask, a granule-sized region ends in 0x3F and those bits land on top + of the attributes: AttrIndx comes out 7 instead of 0, which selects an + unwritten MAIR byte and gives the shared region Device memory type, and + the two RES0 bits are set as well. The region still works well enough to + pass a functional test, which is why this was worth writing down. */ + module_instance -> txm_module_instance_mpu_registers[shared_index].txm_module_mpu_region_limit_address = ((address + length - 1) & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + + /* Keep track of shared memory address and length in module instance. */ + module_instance -> txm_module_instance_shared_memory_address[module_instance -> txm_module_instance_shared_memory_count] = address; + module_instance -> txm_module_instance_shared_memory_length[module_instance -> txm_module_instance_shared_memory_count] = length; + + /* Increment counter. */ + module_instance -> txm_module_instance_shared_memory_count++; + + /* Release the protection mutex. */ + _tx_mutex_put(&_txm_module_manager_mutex); + + /* Return success. */ + return(TX_SUCCESS); +} diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S new file mode 100644 index 000000000..48090ff33 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S @@ -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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* txm_module_manager_fault_capture.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* Records why a memory protection fault happened, then hands over to */ +@/* the C handler which terminates the offending thread. */ +@/* */ +@/* A board's abort vectors branch here. Two entry points, because this */ +@/* core reports the two kinds of violation in different registers: a */ +@/* module writing outside its data region raises a data abort and */ +@/* reports through DFSR and DFAR, while a module branching outside its */ +@/* code region raises a prefetch abort and reports through IFSR and */ +@/* IFAR. Both are recorded either way, so a reader of the fault info */ +@/* can tell which pair is meaningful, and both entry points converge. */ +@/* */ +@/* This has to be assembly, and it has to run first. The fault */ +@/* registers hold only the most recent fault, so anything that runs */ +@/* before the capture and faults itself destroys the evidence. The */ +@/* abort is also taken in Abort mode, with its own banked sp and lr, so */ +@/* C cannot be entered until a stack is known good. */ +@/* */ +@/* THE CONTRACT WITH THE SHARED C HANDLER */ +@/* */ +@/* _txm_module_manager_memory_fault_handler is common to every module */ +@/* port and it terminates the faulting thread and then calls the */ +@/* application's fault-notify callback. For the second half of that to */ +@/* happen, _tx_thread_terminate has to RETURN -- and terminating the */ +@/* running thread only returns if the kernel believes it is inside an */ +@/* exception. _tx_thread_terminate ends in */ +@/* _tx_thread_system_preempt_check, which calls */ +@/* _tx_thread_system_return whenever _tx_thread_system_state and */ +@/* _tx_thread_preempt_disable are both zero; on this architecture that */ +@/* switches context immediately and never comes back. */ +@/* */ +@/* So this routine owes the handler three things, exactly as the */ +@/* Cortex-A7 module port's abort vector does: */ +@/* */ +@/* 1. _tx_thread_system_state incremented across the call, so the */ +@/* terminate returns instead of scheduling from Abort mode. */ +@/* 2. _tx_thread_current_ptr cleared afterwards -- the thread it */ +@/* names is terminated and the scheduler must not save into it. */ +@/* 3. an exception return into _tx_thread_schedule in System mode, */ +@/* which is where the next thread is chosen. */ +@/* */ +@/* Without step 1 the notify callback is unreachable, and the */ +@/* _tx_thread_system_return that runs in its place saves a solicited */ +@/* frame on the ABORT stack and writes that Abort-mode sp into the */ +@/* dead thread's stack pointer. It looks like it works, because the */ +@/* thread is never resumed -- but nothing gives the Abort stack back, */ +@/* so each module fault costs it about 56 bytes for the life of the */ +@/* run. The Cortex-M ports do not need step 1: there */ +@/* _tx_thread_system_return only pends PendSV and returns. */ +@/* */ +@/**************************************************************************/ + + .arm + .text + .align 2 + + .global _txm_module_manager_data_abort + .global _txm_module_manager_prefetch_abort + .extern _txm_module_manager_memory_fault_info + .extern _txm_module_manager_memory_fault_handler + .extern _tx_thread_current_ptr + .extern _tx_thread_system_state + .extern _tx_thread_schedule + +@ Offsets into TXM_MODULE_MANAGER_MEMORY_FAULT_INFO. Checked against offsetof at +@ compile time in txm_module_manager_offset_check.c. + + .equ FAULT_THREAD_PTR, 0x00 + .equ FAULT_CODE_LOCATION, 0x04 + .equ FAULT_DFSR, 0x08 + .equ FAULT_DFAR, 0x0C + .equ FAULT_IFSR, 0x10 + .equ FAULT_IFAR, 0x14 + .equ FAULT_SP, 0x18 + .equ FAULT_R0, 0x1C + .equ FAULT_LR, 0x50 + .equ FAULT_SPSR, 0x54 + +@ Processor mode and state bits. System mode is used for the return because +@ every kernel thread in this port runs in it, and it is the mode +@ _tx_thread_schedule expects to choose the next thread from. + + .equ MODE_MASK, 0x1F + .equ SYS_MODE_BITS, 0x1F + .equ THUMB_BIT, 0x20 + +@ Bytes pushed on the Abort stack by the capture below, so it can be given back. + + .equ CAPTURE_FRAME_SIZE, 20 + + +@/**************************************************************************/ +@/* Data abort: a module wrote or read outside a region it owns. */ +@/**************************************************************************/ + + .type _txm_module_manager_data_abort, %function +_txm_module_manager_data_abort: + +@ lr on data abort entry is the faulting address plus 8. Recorded as the +@ faulting instruction rather than the return address, because the value that +@ helps is where the module went wrong. + + SUB lr, lr, #8 + B _txm_module_manager_fault_common + + +@/**************************************************************************/ +@/* Prefetch abort: a module tried to execute outside its code region. */ +@/**************************************************************************/ + + .type _txm_module_manager_prefetch_abort, %function +_txm_module_manager_prefetch_abort: + +@ lr on prefetch abort entry is the faulting address plus 4. + + SUB lr, lr, #4 + + +@/**************************************************************************/ +@/* Common path. */ +@/**************************************************************************/ + +_txm_module_manager_fault_common: + +@ Save the registers this routine is about to use, on the Abort mode stack, so +@ the values recorded below are the module's and not ours. + + STMDB sp!, {r0-r3, r12} + +@ Tell the kernel it is inside an exception, before any kernel service runs. +@ This is what makes _tx_thread_terminate return to the C handler rather than +@ scheduling from Abort mode; see THE CONTRACT WITH THE SHARED C HANDLER above. + + LDR r0, =_tx_thread_system_state + LDR r1, [r0] + ADD r1, r1, #1 + STR r1, [r0] + + LDR r0, =_txm_module_manager_memory_fault_info + +@ Which thread was running, and where it was. + + LDR r1, =_tx_thread_current_ptr + LDR r1, [r1] + STR r1, [r0, #FAULT_THREAD_PTR] + STR lr, [r0, #FAULT_CODE_LOCATION] + +@ Both fault register pairs. Read before anything else can fault over them. + + MRC p15, 0, r1, c5, c0, 0 @ DFSR + STR r1, [r0, #FAULT_DFSR] + MRC p15, 0, r1, c6, c0, 0 @ DFAR + STR r1, [r0, #FAULT_DFAR] + MRC p15, 0, r1, c5, c0, 1 @ IFSR + STR r1, [r0, #FAULT_IFSR] + MRC p15, 0, r1, c6, c0, 2 @ IFAR + STR r1, [r0, #FAULT_IFAR] + +@ The mode the faulting code was in, which is what says whether this was the +@ module in user mode or the kernel itself. + + MRS r1, SPSR + STR r1, [r0, #FAULT_SPSR] + +@ The faulting context's own sp and lr, read from its mode rather than Abort's. +@ Without the mode switch these would be Abort mode's banked copies, which say +@ nothing about the module. +@ +@ System mode is used because it shares User mode's banked sp and lr, and a +@ module faults in User mode. That is exactly right for the case this exists +@ to report and wrong for one it does not: a fault taken in a privileged mode +@ gets System's sp and lr, which are not that mode's. SPSR says which happened, +@ so a reader can tell when these two fields do not apply. + + MRS r2, CPSR + BIC r3, r2, #0x1F @ Clear the mode field first: + ORR r3, r3, #0x1F @ ORR alone would only give + MSR CPSR_c, r3 @ System mode by luck, and + @ only from Abort mode. + @ System shares User's + @ banked sp and lr. + MOV r1, sp + MOV r12, lr + MSR CPSR_c, r2 @ Back to Abort mode + STR r1, [r0, #FAULT_SP] + STR r12, [r0, #FAULT_LR] + +@ The module's r0 through r3 and r12, from where they were pushed above, then +@ r4 through r11 which the fault did not disturb. + +@ Five words were pushed, r0 through r3 and r12, and they are copied one at a +@ time. Loading them as a group and then building the destination address in +@ the same registers destroys the values on the way past, which is what the +@ first version of this did. + + LDR r1, [sp, #0] @ Module r0 + STR r1, [r0, #FAULT_R0] + LDR r1, [sp, #4] @ Module r1 + STR r1, [r0, #(FAULT_R0 + 4)] + LDR r1, [sp, #8] @ Module r2 + STR r1, [r0, #(FAULT_R0 + 8)] + LDR r1, [sp, #12] @ Module r3 + STR r1, [r0, #(FAULT_R0 + 12)] + LDR r1, [sp, #16] @ Module r12 + STR r1, [r0, #(FAULT_R0 + 0x30)] @ r12 sits after r11 + +@ r4 through r11 were never disturbed, so they can go straight out. + + ADD r1, r0, #(FAULT_R0 + 16) + STMIA r1, {r4-r11} + +@ Give the Abort stack back. The five words above were read out of it rather +@ than popped, because the copies needed them in place; nothing needs them now. +@ This routine does not return to the faulting code, so an unbalanced sp here +@ would never be corrected and every module fault would cost the Abort stack +@ twenty bytes until it ran out. + + ADD sp, sp, #CAPTURE_FRAME_SIZE + +@ Hand over. The handler terminates the offending thread and then calls the +@ application's fault-notify callback, and it DOES return here -- the +@ incremented system state above is what makes that true. + + BL _txm_module_manager_memory_fault_handler + +@ Out of the exception. Balanced against the increment above. + + LDR r0, =_tx_thread_system_state + LDR r1, [r0] + SUB r1, r1, #1 + STR r1, [r0] + +@ The current thread is terminated, so there is no context to save into it and +@ the scheduler must not try. Clearing the pointer is how this port says "no +@ thread is running", the same state _tx_thread_system_return leaves behind. + + MOV r1, #0 + LDR r0, =_tx_thread_current_ptr + STR r1, [r0] + +@ Return into the scheduler rather than to the faulting instruction, which +@ would fault again immediately and for ever. An exception return, not a +@ branch: it leaves Abort mode properly and restores the interrupt mask the +@ faulting context had, where a branch would run the scheduler in Abort mode +@ on the Abort stack. +@ +@ SPSR is rewritten for the mode this port's kernel threads run in, with the +@ Thumb bit cleared because _tx_thread_schedule is A32. The value recorded in +@ the fault info was stored before this, so what an application reads is still +@ the mode the module faulted in. + + MRS r0, SPSR + BIC r0, r0, #MODE_MASK + ORR r0, r0, #SYS_MODE_BITS + BIC r0, r0, #THUMB_BIT + MSR SPSR_cxsf, r0 + + LDR lr, =_tx_thread_schedule + SUBS pc, lr, #0 diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c new file mode 100644 index 000000000..cac30d2fb --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c @@ -0,0 +1,139 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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 + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +// Some portions generated by Claude Code (Opus 5). + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "tx_thread.h" +#include "txm_module.h" + + +/* This handler is architecture-neutral: it terminates the faulting thread and + calls the notification callback. The fault registers are captured before it + runs, in the abort vector, because DFSR, DFAR, IFSR and IFAR must be read + before anything else can fault and overwrite them -- and on this core the + abort is taken in Abort mode with its own banked lr and sp, so the capture has + to happen there rather than here. + + Data aborts and prefetch aborts both arrive here. A module can violate its + protection either way: writing outside its data region, or branching outside + its code region. Which pair of registers is meaningful depends on which it + was, and the fault info structure carries both. + + THIS FUNCTION RETURNS, AND THAT IS A REQUIREMENT ON THE ABORT VECTOR. + + It terminates the faulting thread and then calls the application's notify + callback. The second of those is only reached if _tx_thread_terminate returns, + and terminating the RUNNING thread returns only when the kernel believes it is + inside an exception: _tx_thread_terminate ends in + _tx_thread_system_preempt_check, which calls _tx_thread_system_return whenever + _tx_thread_system_state and _tx_thread_preempt_disable are both zero. On this + architecture _tx_thread_system_return switches context immediately and never + comes back, so the callback would be unreachable. + + The body below is therefore left exactly as the cortex_m33, cortex_a7 and + cortex_m7 ports have it, and the requirement is met where it belongs -- in + txm_module_manager_fault_capture.S, which increments _tx_thread_system_state + around this call, clears _tx_thread_current_ptr afterwards and returns into + the scheduler. The Cortex-M ports need no such bracket because their + _tx_thread_system_return only pends PendSV and returns. Anyone tempted to + reorder the two statements below to "fix" a notify callback that does not fire + should look at the abort vector first: the ordering here is upstream's and it + is not the defect. */ + +/* Define the user's fault notification callback function pointer. This is + setup via the txm_module_manager_memory_fault_notify API. */ + +VOID (*_txm_module_manager_fault_notify)(TX_THREAD *, TXM_MODULE_INSTANCE *); + + +/* Define a macro that can be used to allocate global variables useful to + store information about the last fault. This macro is defined in + txm_module_port.h and is usually populated in the assembly language + fault handling prior to the code calling _txm_module_manager_memory_fault_handler. */ + +TXM_MODULE_MANAGER_FAULT_INFO + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_memory_fault_handler Cortex-R52 */ +/* 6.1.8 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function handles a fault associated with a memory protected */ +/* module. */ +/* */ +/* INPUT */ +/* */ +/* None */ +/* */ +/* OUTPUT */ +/* */ +/* None */ +/* */ +/* CALLS */ +/* */ +/* _tx_thread_terminate Terminate thread */ +/* */ +/* CALLED BY */ +/* */ +/* Fault handler */ +/* */ +/**************************************************************************/ +VOID _txm_module_manager_memory_fault_handler(VOID) +{ + +TXM_MODULE_INSTANCE *module_instance_ptr; +TX_THREAD *thread_ptr; + + /* Pickup the current thread. */ + thread_ptr = _tx_thread_current_ptr; + + /* Initialize the module instance pointer to NULL. */ + module_instance_ptr = TX_NULL; + + /* Is there a thread? */ + if (thread_ptr) + { + /* Pickup the module instance. */ + module_instance_ptr = thread_ptr -> tx_thread_module_instance_ptr; + + /* Terminate the current thread. */ + _tx_thread_terminate(_tx_thread_current_ptr); + } + + /* Determine if there is a user memory fault notification callback. */ + if (_txm_module_manager_fault_notify) + { + /* Yes, call the user's notification memory fault callback. */ + (_txm_module_manager_fault_notify)(thread_ptr, module_instance_ptr); + } +} diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_notify.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_notify.c new file mode 100644 index 000000000..a86196876 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_notify.c @@ -0,0 +1,78 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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 + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "tx_thread.h" +#include "txm_module.h" + + +/* Define the external user's fault notification callback function pointer. This is + setup via the txm_module_manager_memory_fault_notify API. */ + +extern VOID (*_txm_module_manager_fault_notify)(TX_THREAD *, TXM_MODULE_INSTANCE *); + + + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_memory_fault_notify Cortex-R52 */ +/* 6.1.8 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function registers an application callback when/if a memory */ +/* fault occurs. The supplied thread is automatically terminated, but */ +/* any other threads in the same module may still execute. */ +/* */ +/* INPUT */ +/* */ +/* notify_function Memory fault notification */ +/* function, NULL disables. */ +/* */ +/* OUTPUT */ +/* */ +/* status Completion status */ +/* */ +/* CALLS */ +/* */ +/* None */ +/* */ +/* CALLED BY */ +/* */ +/* Application Code */ +/* */ +/**************************************************************************/ +UINT _txm_module_manager_memory_fault_notify(VOID (*notify_function)(TX_THREAD *, TXM_MODULE_INSTANCE *)) +{ + /* Setup notification function. */ + _txm_module_manager_fault_notify = notify_function; + + /* Return success. */ + return(TX_SUCCESS); +} diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c new file mode 100644 index 000000000..2a3fe6791 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c @@ -0,0 +1,219 @@ +/*************************************************************************** + * Copyright (c) 2024 Microsoft Corporation + * Copyright (c) 2026-present 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 + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Component */ +/** */ +/** Module Manager */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +#define TX_SOURCE_CODE + +#include "tx_api.h" +#include "txm_module.h" + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_mm_register_setup Cortex-R52 */ +/* 6.1.6 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function sets up the Cortex-R52 MPU register definitions based */ +/* on the module's memory characteristics. */ +/* */ +/* INPUT */ +/* */ +/* module_instance Pointer to module instance */ +/* */ +/* OUTPUT */ +/* */ +/* MPU settings for the module in module_instance */ +/* */ +/* CALLS */ +/* */ +/* none */ +/* */ +/* CALLED BY */ +/* */ +/* _txm_module_manager_thread_create */ +/* */ +/**************************************************************************/ +/* Two departures from the Armv8-M version this was derived from, both + deliberate. + + The address mask is 64-byte, not 32-byte. PRBAR and PRLAR hold attributes in + the bits below the granule, so masking to the wrong boundary does not fault -- + it silently writes address bits into the shareability and permission fields + and the region comes up with attributes nobody asked for. + + Regions are non-shareable rather than inner-shareable. The M33 port marks + module memory inner-shareable; the RTU here is a single Cortex-R52 and every + region in the board support package's map is non-shareable, so matching it + keeps one memory model across the kernel and its modules. A multi-core RTU + configuration would need to revisit this, and would need to revisit far more + than this. */ + +/* Marks the end of the user-mode entry function, so the kernel entry region can + be sized to it. */ + +extern VOID _txm_module_manager_user_mode_entry_end(VOID); + +VOID _txm_module_manager_mm_register_setup(TXM_MODULE_INSTANCE *module_instance) +{ + +ULONG data_size; +ULONG start_stop_stack_size; +ULONG callback_stack_size; + + /* Setup MPU region for kernel mode entry. */ + /* Set base address register to user mode entry function address, which is guaranteed to be at least 32-byte aligned. + Mask address to proper range, inner shareable, read only. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_KERNEL_ENTRY_INDEX].txm_module_mpu_region_base_address = ((ULONG) _txm_module_manager_user_mode_entry & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_NON_SHAREABLE | TXM_MODULE_ATTRIBUTE_READ_ONLY; + /* Set the limit address, attribute index, and enable bit. */ + /* Limit taken from the end of the function rather than from its base. The + whole privileged surface a module can execute is 24 bytes, so one granule + covers it today and the base would have done -- but if the entry ever grows + past 64 bytes, sizing from the base would silently leave the tail of it + outside the region and the module would fault on a call it is entitled to + make. */ + + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_KERNEL_ENTRY_INDEX].txm_module_mpu_region_limit_address = (((ULONG) _txm_module_manager_user_mode_entry_end - 1) & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + /* End of kernel mode entry setup. */ + + + /* Setup MPU region for module code protection. */ + /* Set base address register to module code address, which should be at least 32-byte aligned. + Mask address to proper range, inner shareable, read only. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_CODE_INDEX].txm_module_mpu_region_base_address = ((ULONG) module_instance -> txm_module_instance_code_start & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_NON_SHAREABLE | TXM_MODULE_ATTRIBUTE_READ_ONLY; + /* Set the limit address (code start + code size-1), attribute index, and enable bit. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_CODE_INDEX].txm_module_mpu_region_limit_address = (((ULONG) module_instance -> txm_module_instance_code_start + module_instance -> txm_module_instance_preamble_ptr -> txm_module_preamble_code_size - 1) & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + /* End of module code protection. */ + + + /* Setup MPU region for module data protection. */ + /* Set base address register to module data address, which should be at least 32-byte aligned. + Mask address to proper range, inner shareable, read write, execute never. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_DATA_INDEX].txm_module_mpu_region_base_address = ((ULONG) module_instance -> txm_module_instance_data_start & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_NON_SHAREABLE | TXM_MODULE_ATTRIBUTE_READ_WRITE | TXM_MODULE_ATTRIBUTE_EXECUTE_NEVER; + + /* Adjust the size of the module elements to be aligned to the default alignment. We do this + so that when we partition the allocated memory, we can simply place these regions right beside + each other without having to align their pointers. Note this only works when they all have + the same alignment. */ + + data_size = module_instance -> txm_module_instance_preamble_ptr -> txm_module_preamble_data_size; + start_stop_stack_size = module_instance -> txm_module_instance_preamble_ptr -> txm_module_preamble_start_stop_stack_size; + callback_stack_size = module_instance -> txm_module_instance_preamble_ptr -> txm_module_preamble_callback_stack_size; + + data_size = ((data_size + TXM_MODULE_DATA_ALIGNMENT - 1)/TXM_MODULE_DATA_ALIGNMENT) * TXM_MODULE_DATA_ALIGNMENT; + start_stop_stack_size = ((start_stop_stack_size + TXM_MODULE_DATA_ALIGNMENT - 1)/TXM_MODULE_DATA_ALIGNMENT) * TXM_MODULE_DATA_ALIGNMENT; + callback_stack_size = ((callback_stack_size + TXM_MODULE_DATA_ALIGNMENT - 1)/TXM_MODULE_DATA_ALIGNMENT) * TXM_MODULE_DATA_ALIGNMENT; + + /* Update the data size to include thread stacks. */ + data_size = data_size + start_stop_stack_size + callback_stack_size; + + /* Set the limit address (data start + data size-1), attribute index, and enable bit. */ + module_instance -> txm_module_instance_mpu_registers[TXM_MODULE_MPU_DATA_INDEX].txm_module_mpu_region_limit_address = (((ULONG) module_instance -> txm_module_instance_data_start + data_size - 1) & TXM_MODULE_MPU_ADDRESS_MASK) | TXM_MODULE_ATTRIBUTE_INDEX | TXM_MODULE_ATTRIBUTE_REGION_ENABLE; + /* End of module data protection. */ + + /* Remaining MPU entries are disabled for now and can be used for shared memory. */ +} + +/**************************************************************************/ +/* */ +/* FUNCTION RELEASE */ +/* */ +/* _txm_module_manager_inside_data_check Cortex-R52 */ +/* 6.1.6 */ +/* AUTHOR */ +/* */ +/* Scott Larson, Microsoft Corporation */ +/* */ +/* DESCRIPTION */ +/* */ +/* This function checks if the specified object is inside shared */ +/* memory. */ +/* */ +/* INPUT */ +/* */ +/* module_instance Pointer to module instance */ +/* obj_ptr Pointer to the object */ +/* obj_size Size of the object */ +/* */ +/* OUTPUT */ +/* */ +/* Whether the object is inside the shared memory region. */ +/* */ +/* CALLS */ +/* */ +/* None */ +/* */ +/* CALLED BY */ +/* */ +/* Module dispatch check functions */ +/* */ +/* RELEASE HISTORY */ +/* */ +/* DATE NAME DESCRIPTION */ +/* */ +/* 12-31-2020 Scott Larson Initial Version 6.1.3 */ +/* 04-02-2021 Scott Larson Modified comments, */ +/* resulting in version 6.1.6 */ +/* */ +/**************************************************************************/ +UINT _txm_module_manager_inside_data_check(TXM_MODULE_INSTANCE *module_instance, ALIGN_TYPE obj_ptr, UINT obj_size) +{ + +UINT shared_memory_index; +UINT num_shared_memory_mpu_entries; +ALIGN_TYPE shared_memory_address_start; +ALIGN_TYPE shared_memory_address_end; + + /* Check for overflow. */ + if ((obj_ptr) > ((obj_ptr) + (obj_size))) + { + return(TX_FALSE); + } + + /* Check if the object is inside the module data. */ + if ((obj_ptr >= (ALIGN_TYPE) module_instance -> txm_module_instance_data_start) && + ((obj_ptr + obj_size) <= ((ALIGN_TYPE) module_instance -> txm_module_instance_data_end + 1))) + { + return(TX_TRUE); + } + + /* Check if the object is inside the shared memory. */ + num_shared_memory_mpu_entries = module_instance -> txm_module_instance_shared_memory_count; + for (shared_memory_index = 0; shared_memory_index < num_shared_memory_mpu_entries; shared_memory_index++) + { + + shared_memory_address_start = (ALIGN_TYPE) module_instance -> txm_module_instance_shared_memory_address[shared_memory_index]; + shared_memory_address_end = shared_memory_address_start + module_instance -> txm_module_instance_shared_memory_length[shared_memory_index]; + + if ((obj_ptr >= (ALIGN_TYPE) shared_memory_address_start) && + ((obj_ptr + obj_size) <= (ALIGN_TYPE) shared_memory_address_end)) + { + return(TX_TRUE); + } + } + + return(TX_FALSE); +} diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c new file mode 100644 index 000000000..ae4b1ef0f --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c @@ -0,0 +1,213 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* MODULE MANAGER RELEASE */ +/* */ +/* txm_module_manager_offset_check.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frédéric Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Compile-time verification of the structure offsets that this port's */ +/* assembly hard-codes. */ +/* */ +/* tx_thread_schedule.S reaches into TX_THREAD and TXM_MODULE_INSTANCE */ +/* with numeric offsets, because assembly has no other way to do it. */ +/* Nothing in the toolchain connects those numbers to the structures */ +/* they describe, so adding a field, reordering an extension or */ +/* building with a different set of ThreadX options silently moves the */ +/* target and the scheduler reads the wrong word. The failure is a */ +/* corrupted region table or a fault in a thread that did nothing wrong, */ +/* a long way from the change that caused it. */ +/* */ +/* This file exists so that becomes a build error instead. It emits no */ +/* code. */ +/* */ +/* The offsets here are not the same as the Armv8-M module port's, and */ +/* that is the concrete case in point: there the module instance pointer */ +/* sits at 0x90, and here it is 0x94, because the Cortex-R52 port keeps */ +/* tx_thread_vfp_enable ahead of the module fields in */ +/* TX_THREAD_EXTENSION_2. Copying the Armv8-M numbers would have built */ +/* cleanly and misbehaved on the board. */ +/* */ +/* See eclipse-threadx/threadx issue #577, which proposes this check for */ +/* every port rather than only this one. */ +/* */ +/**************************************************************************/ + +#include +#include "tx_api.h" +#include "txm_module.h" +#include "mpu.h" + +/* These must match the .equ values at the top of tx_thread_schedule.S. */ + +#define TXM_THREAD_MODULE_INSTANCE 0x94 +#define TXM_INSTANCE_DATA_START 0x2C +#define TXM_INSTANCE_MPU_REGISTERS 0x64 + +_Static_assert(offsetof(TX_THREAD, tx_thread_module_instance_ptr) + == TXM_THREAD_MODULE_INSTANCE, + "tx_thread_schedule.S TXM_THREAD_MODULE_INSTANCE no longer matches " + "TX_THREAD; the scheduler would read the wrong word"); + +_Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_data_start) + == TXM_INSTANCE_DATA_START, + "tx_thread_schedule.S TXM_INSTANCE_DATA_START no longer matches " + "TXM_MODULE_INSTANCE; the protection check would test the wrong field"); + +_Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_mpu_registers) + == TXM_INSTANCE_MPU_REGISTERS, + "tx_thread_schedule.S TXM_INSTANCE_MPU_REGISTERS no longer matches " + "TXM_MODULE_INSTANCE; the scheduler would load regions from the " + "wrong address"); + +/* Offset the module thread stack build hard-codes. This one decides the mode a + module thread starts in, so a wrong value starts it privileged when it asked + not to be -- protection that is absent from the first instruction and looks + present everywhere else. The Cortex-R4 module port reads 0x9C here, which in + this port is a different field. */ + +_Static_assert(offsetof(TX_THREAD, tx_thread_module_user_mode) == 0xA0, + "txm_module_manager_thread_stack_build.S THREAD_MODULE_USER_MODE " + "no longer matches TX_THREAD; module threads could start in the " + "wrong privilege mode"); + +_Static_assert(offsetof(TX_THREAD, tx_thread_module_user_mode) + != offsetof(TX_THREAD, tx_thread_module_current_user_mode), + "the requested and current user-mode fields have collapsed onto " + "one offset; the stack build and the SVC handler would be reading " + "each other's field"); + +/* Offsets the supervisor call handler hard-codes, from + txm_module_manager_svc_handler.S. This is the privilege boundary, so a wrong + offset here does not merely misbehave: writing the user-mode flag to the wrong + word would leave a thread believing it is privileged when it is not, or the + reverse, and swapping to a stack pointer read from the wrong field would put + the kernel on memory the module can write. */ + +#define THREAD_STACK_PTR 0x08 +#define THREAD_STACK_START 0x0C +#define THREAD_STACK_END 0x10 +#define THREAD_STACK_SIZE 0x14 +#define THREAD_CUR_USER_MODE 0x9C +#define THREAD_KSTACK_START 0xA8 +#define THREAD_KSTACK_END 0xAC +#define THREAD_KSTACK_SIZE 0xB0 +#define THREAD_MSTACK_PTR 0xB4 +#define THREAD_MSTACK_START 0xB8 +#define THREAD_MSTACK_END 0xBC +#define THREAD_MSTACK_SIZE 0xC0 + +#define T_OFF(f) offsetof(TX_THREAD, f) + +_Static_assert(T_OFF(tx_thread_stack_ptr) == THREAD_STACK_PTR, "svc handler: stack_ptr moved"); +_Static_assert(T_OFF(tx_thread_stack_start) == THREAD_STACK_START, "svc handler: stack_start moved"); +_Static_assert(T_OFF(tx_thread_stack_end) == THREAD_STACK_END, "svc handler: stack_end moved"); +_Static_assert(T_OFF(tx_thread_stack_size) == THREAD_STACK_SIZE, "svc handler: stack_size moved"); + +_Static_assert(T_OFF(tx_thread_module_current_user_mode) == THREAD_CUR_USER_MODE, + "svc handler: the user-mode flag moved; a thread could be left " + "believing it is privileged when it is not"); + +_Static_assert(T_OFF(tx_thread_module_kernel_stack_start) == THREAD_KSTACK_START, "svc handler: kernel_stack_start moved"); +_Static_assert(T_OFF(tx_thread_module_kernel_stack_end) == THREAD_KSTACK_END, + "svc handler: kernel_stack_end moved; the kernel would run on a " + "stack read from the wrong field"); +_Static_assert(T_OFF(tx_thread_module_kernel_stack_size) == THREAD_KSTACK_SIZE, "svc handler: kernel_stack_size moved"); +_Static_assert(T_OFF(tx_thread_module_stack_ptr) == THREAD_MSTACK_PTR, "svc handler: module stack_ptr moved"); +_Static_assert(T_OFF(tx_thread_module_stack_start) == THREAD_MSTACK_START, "svc handler: module stack_start moved"); +_Static_assert(T_OFF(tx_thread_module_stack_end) == THREAD_MSTACK_END, "svc handler: module stack_end moved"); +_Static_assert(T_OFF(tx_thread_module_stack_size) == THREAD_MSTACK_SIZE, "svc handler: module stack_size moved"); + +/* Offsets the fault capture hard-codes, from + txm_module_manager_fault_capture.S. Same hazard as the scheduler's: the + capture runs in Abort mode with a fault in progress, and a wrong offset there + writes the evidence into the wrong field of a structure the application is + about to read. */ + +#define FAULT_THREAD_PTR 0x00 +#define FAULT_CODE_LOCATION 0x04 +#define FAULT_DFSR 0x08 +#define FAULT_DFAR 0x0C +#define FAULT_IFSR 0x10 +#define FAULT_IFAR 0x14 +#define FAULT_SP 0x18 +#define FAULT_R0 0x1C +#define FAULT_LR 0x50 +#define FAULT_SPSR 0x54 + +#define FAULT_OFF(f) offsetof(TXM_MODULE_MANAGER_MEMORY_FAULT_INFO, \ + txm_module_manager_memory_fault_info_##f) + +_Static_assert(FAULT_OFF(thread_ptr) == FAULT_THREAD_PTR, "fault capture: thread_ptr offset moved"); +_Static_assert(FAULT_OFF(code_location) == FAULT_CODE_LOCATION, "fault capture: code_location offset moved"); +_Static_assert(FAULT_OFF(dfsr) == FAULT_DFSR, "fault capture: dfsr offset moved"); +_Static_assert(FAULT_OFF(dfar) == FAULT_DFAR, "fault capture: dfar offset moved"); +_Static_assert(FAULT_OFF(ifsr) == FAULT_IFSR, "fault capture: ifsr offset moved"); +_Static_assert(FAULT_OFF(ifar) == FAULT_IFAR, "fault capture: ifar offset moved"); +_Static_assert(FAULT_OFF(sp) == FAULT_SP, "fault capture: sp offset moved"); +_Static_assert(FAULT_OFF(r0) == FAULT_R0, "fault capture: r0 offset moved"); +_Static_assert(FAULT_OFF(lr) == FAULT_LR, "fault capture: lr offset moved"); +_Static_assert(FAULT_OFF(spsr) == FAULT_SPSR, "fault capture: spsr offset moved"); + +/* The capture writes r4 through r11 as one block starting sixteen bytes past r0, + and r12 thirty-two bytes past that, which assumes the registers are laid out + in order with no padding. */ + +_Static_assert(FAULT_OFF(r4) == (FAULT_R0 + 16), "fault capture: r4 is not four words past r0"); +_Static_assert(FAULT_OFF(r12) == (FAULT_R0 + 0x30), "fault capture: r12 is not where the block store expects"); + +/* The scheduler loads exactly eight regions as sixteen consecutive words, with + LDMIA walking the table two words at a time. If the entry stops being two + words, or the table stops holding eight of them, the unrolled sequence walks + off the end of it. */ + +_Static_assert(sizeof(TXM_MODULE_MPU_INFO) == (2U * sizeof(ULONG)), + "TXM_MODULE_MPU_INFO is no longer two words; the unrolled LDMIA " + "sequence in tx_thread_schedule.S would misread the region table"); + +_Static_assert(TXM_MODULE_MPU_TOTAL_ENTRIES == 8, + "tx_thread_schedule.S has an unrolled sequence for exactly eight " + "regions; change both together"); + +/* The block has to sit inside the directly addressable range. Regions above 15 + are reachable only through PRSELR, which the scheduler does not use. */ + +_Static_assert((TXM_MODULE_MPU_FIRST_REGION + TXM_MODULE_MPU_TOTAL_ENTRIES) <= 16, + "the module region block extends past region 15, which has no " + "direct PRBARn encoding; the scheduler cannot reach it"); + +/* The kernel's window over the module area. tx_thread_schedule.S hard-codes the + region number in an MCR to PRSELR, because the assembler cannot include this + board header, so the two spellings are checked against each other here. + + It also has to sit clear of the module's own block: the scheduler enables the + window for a thread that owns no module and the block for a thread that does, + and if the two ever named the same region one would silently be the other. */ + +_Static_assert(MPU_MODULE_LOAD_REGION == 16, + "tx_thread_schedule.S writes region 16 as the module window; " + "change MPU_MODULE_WINDOW_REGION there to match"); + +_Static_assert(MPU_MODULE_LOAD_REGION >= (TXM_MODULE_MPU_FIRST_REGION + + TXM_MODULE_MPU_TOTAL_ENTRIES), + "the module window overlaps the regions handed to a module; the " + "scheduler would enable one believing it was the other"); diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S new file mode 100644 index 000000000..0689ffbaa --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S @@ -0,0 +1,233 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* txm_module_manager_svc_handler.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* The privilege boundary. A board's supervisor call vector branches */ +@/* here. */ +@/* */ +@/* SVC 1 raises a module thread out of User mode and onto its kernel */ +@/* stack; SVC 2 puts it back. Both are only reachable from the two */ +@/* exact instructions inside _txm_module_manager_user_mode_entry, and */ +@/* that check is what makes the boundary a boundary. Without it a */ +@/* module could execute SVC 1 from anywhere in its own code and come */ +@/* back privileged, which is every protection in this port gone at */ +@/* once. */ +@/* */ +@/* The two stacks are the other half of it. A module's own stack is in */ +@/* memory the module can write, so the kernel must not run on it: a */ +@/* module could otherwise corrupt kernel state by scribbling on what it */ +@/* believes is its own stack. SVC 1 therefore switches to a kernel */ +@/* stack the module cannot reach, and SVC 2 switches back. */ +@/* */ +@/* Any other SVC number stops. This core's base port does not use SVC */ +@/* at all -- its vector treats one as a fault -- so there is no third */ +@/* caller to accommodate and nothing legitimate to fall through to. */ +@/* */ +@/**************************************************************************/ + +@ Unified syntax, declared rather than assumed. In divided syntax the condition +@ precedes the size -- LDRNEH -- and in unified it follows -- LDRHNE. The +@ assembler's default rejected the unified form, and a file that hand-writes +@ exception entry is the last place to leave that to chance. + + .syntax unified + .arm + .text + .align 2 + + .global __tx_module_svc_interrupt + .extern _tx_thread_current_ptr + .extern _txm_system_mode_enter + .extern _txm_system_mode_exit + +@ Processor mode encodings and the CPSR mode field. + + .equ MODE_MASK, 0x1F + .equ USR_MODE_BITS, 0x10 + .equ SVC_MODE_BITS, 0x13 + .equ SYS_MODE_BITS, 0x1F + .equ THUMB_MASK, 0x20 + +@ Offsets into TX_THREAD. Checked against offsetof at compile time in +@ txm_module_manager_offset_check.c. They are not the Cortex-R4 module port's +@ values: tx_thread_vfp_enable sits ahead of the module fields in this port, so +@ everything after it moves by a word. + + .equ THREAD_STACK_PTR, 0x08 + .equ THREAD_STACK_START, 0x0C + .equ THREAD_STACK_END, 0x10 + .equ THREAD_STACK_SIZE, 0x14 + .equ THREAD_CUR_USER_MODE, 0x9C + .equ THREAD_KSTACK_START, 0xA8 + .equ THREAD_KSTACK_END, 0xAC + .equ THREAD_KSTACK_SIZE, 0xB0 + .equ THREAD_MSTACK_PTR, 0xB4 + .equ THREAD_MSTACK_START, 0xB8 + .equ THREAD_MSTACK_END, 0xBC + .equ THREAD_MSTACK_SIZE, 0xC0 + + +@/**************************************************************************/ +@/* Vector entry. */ +@/**************************************************************************/ + + .type __tx_module_svc_interrupt, %function +__tx_module_svc_interrupt: + + STMFD sp!, {r0-r3, r12, lr} @ Preserve the caller's registers + MRS r0, spsr + STMFD sp!, {r0, r3} @ SPSR, plus one more to keep the + @ stack eight-byte aligned + +@ Recover the SVC number from the instruction that caused this. The encoding +@ differs between states, so the saved SPSR decides where to read it from. + + TST r0, #THUMB_MASK + LDRHNE r0, [lr, #-2] @ Thumb: halfword, low 8 bits + BICNE r0, r0, #0xFF00 + LDREQ r0, [lr, #-4] @ ARM: word, low 24 bits + BICEQ r0, r0, #0xFF000000 + + CMP r0, #1 + BEQ _tx_module_svc_enter + CMP r0, #2 + BEQ _tx_module_svc_exit + + +@/**************************************************************************/ +@/* Anything else. */ +@/**************************************************************************/ + + .weak _tx_module_svc_unrecognized +_tx_module_svc_unrecognized: + +@ Stop rather than return. Returning would resume the caller as though the +@ call had succeeded, and for an unrecognised number that means resuming with +@ the privilege state undefined. + +_tx_module_svc_unrecognized_loop: + B _tx_module_svc_unrecognized_loop + + +@/**************************************************************************/ +@/* SVC 1: leave User mode. */ +@/**************************************************************************/ + +_tx_module_svc_enter: + +@ Only the SVC inside the user mode entry function may do this. lr points one +@ instruction past the call, so the call site is lr minus four. A module that +@ executes SVC 1 from its own code fails here and stops. + + LDR r2, =_txm_system_mode_enter + SUB r1, lr, #4 + CMP r1, r2 + BNE _tx_module_svc_unrecognized + + LDR r1, =_tx_thread_current_ptr + LDR r2, [r1] @ The running thread + +@ Record that it is no longer in User mode, so a nested service call and the +@ scheduler both see the truth. + + MOV r1, #0 + STR r1, [r2, #THREAD_CUR_USER_MODE] + +@ Return into System mode rather than User mode. + + LDMFD sp!, {r0, r3} @ SPSR back off the stack + BIC r0, r0, #MODE_MASK + ORR r0, r0, #SYS_MODE_BITS + MSR SPSR_cxsf, r0 + +@ Swap stacks. The module's sp is saved so SVC 2 can restore it, and sp is set +@ to the top of the kernel stack, which the module's regions do not cover. +@ System mode shares User mode's banked sp, which is why the swap happens there. + + LDR r1, [r2, #THREAD_KSTACK_END] @ Top of the kernel stack + CPS #SYS_MODE_BITS + MOV r3, sp @ The module's own sp + MOV sp, r1 + CPS #SVC_MODE_BITS + STR r3, [r2, #THREAD_MSTACK_PTR] + +@ Point ThreadX's stack checking at the kernel stack while it is in use. +@ Without this a stack check would measure the kernel's sp against the module's +@ bounds and report an overflow that has not happened. + + LDR r3, [r2, #THREAD_KSTACK_START] + STR r3, [r2, #THREAD_STACK_START] + LDR r3, [r2, #THREAD_KSTACK_END] + STR r3, [r2, #THREAD_STACK_END] + LDR r3, [r2, #THREAD_KSTACK_SIZE] + STR r3, [r2, #THREAD_STACK_SIZE] + + LDMFD sp!, {r0-r3, r12, pc}^ @ Return, restoring CPSR from SPSR + + +@/**************************************************************************/ +@/* SVC 2: return to User mode. */ +@/**************************************************************************/ + +_tx_module_svc_exit: + +@ Same check, against the other call site. + + LDR r2, =_txm_system_mode_exit + SUB r1, lr, #4 + CMP r1, r2 + BNE _tx_module_svc_unrecognized + + LDR r1, =_tx_thread_current_ptr + LDR r2, [r1] + + MOV r1, #1 + STR r1, [r2, #THREAD_CUR_USER_MODE] + +@ Return into User mode. + + LDMFD sp!, {r0, r3} + BIC r0, r0, #MODE_MASK + ORR r0, r0, #USR_MODE_BITS + MSR SPSR_cxsf, r0 + +@ Put the module's own stack back. + + LDR r1, [r2, #THREAD_MSTACK_PTR] + CPS #SYS_MODE_BITS + MOV sp, r1 + CPS #SVC_MODE_BITS + +@ And its own bounds, for the same reason they were changed on the way in. + + LDR r3, [r2, #THREAD_MSTACK_START] + STR r3, [r2, #THREAD_STACK_START] + LDR r3, [r2, #THREAD_MSTACK_END] + STR r3, [r2, #THREAD_STACK_END] + LDR r3, [r2, #THREAD_MSTACK_SIZE] + STR r3, [r2, #THREAD_STACK_SIZE] + + LDMFD sp!, {r0-r3, r12, pc}^ diff --git a/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S new file mode 100644 index 000000000..7e323c2c1 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S @@ -0,0 +1,242 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ Some portions generated by Claude Code (Opus 5). +@ +@ +@/**************************************************************************/ +@/**************************************************************************/ +@/** */ +@/** ThreadX Component */ +@/** */ +@/** Thread */ +@/** */ +@/**************************************************************************/ +@/**************************************************************************/ +#ifdef TX_INCLUDE_USER_DEFINE_FILE +#include "tx_user.h" +#endif + + .syntax unified + .arm + +@ Bare CPSR mode field. See tx_thread_stack_build.S for why the _MODE_BITS +@ suffix is not optional in this port: a plain _MODE name is a whole CPSR value +@ with the interrupt masks in it. SVC is not among them -- no thread in a module +@ port runs in SVC mode, which is reserved for the supervisor call handler. + +USR_MODE_BITS = 0x10 @ User mode, unprivileged +SYS_MODE_BITS = 0x1F @ System mode, privileged +THUMB_MASK = 0x20 @ CPSR Thumb bit + +@ Offset of tx_thread_module_user_mode, checked against offsetof at compile time +@ in txm_module_manager_offset_check.c. + +THREAD_MODULE_USER_MODE = 0xA0 +#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 _txm_module_manager_thread_stack_build for +@ applications calling this function from to 16-bit Thumb mode. */ +@ + .text + .align 2 + .thumb + .global $_txm_module_manager_thread_stack_build + .type $_txm_module_manager_thread_stack_build,function +$_txm_module_manager_thread_stack_build: + BX pc @ Switch to 32-bit mode + NOP @ + .arm + STMFD sp!, {lr} @ Save return address + BL _txm_module_manager_thread_stack_build @ Call _txm_module_manager_thread_stack_build function + LDMFD sp!, {lr} @ Recover saved return address + BX lr @ Return to 16-bit caller +@ +@ + .text + .align 2 +@/**************************************************************************/ +@/* */ +@/* FUNCTION RELEASE */ +@/* */ +@/* _txm_module_manager_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 _txm_module_manager_thread_stack_build(TX_THREAD *thread_ptr, VOID (*function_ptr)(VOID)) +@{ + .global _txm_module_manager_thread_stack_build + .type _txm_module_manager_thread_stack_build,function +_txm_module_manager_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) */ +@ +@ The thread entry information pointer, which _txm_module_manager_thread_create +@ parked in the stack pointer position of the control block for exactly this +@ purpose. It has to be read before the frame is built, because the initial +@ stack pointer is written over that same field further down. +@ +@ The module's shell entry takes two arguments -- the thread and this pointer -- +@ and everything the module needs to start is behind it: its own entry point, +@ its parameter, its data and code bases, and the callback queue. ThreadX's +@ thread shell passes only one argument, so the second one can only arrive as a +@ seeded register, which is what the two stores below are for. Left at zero, +@ as the base port's stack build leaves them, the shell entry dereferences NULL +@ on its first instruction and the module faults before it runs a line of its +@ own code. + + LDR r12, [r0, #8] @ Pickup thread entry info pointer +@ + 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 + STR r0, [r2, #8] @ Store initial r0, the thread pointer + STR r12, [r2, #12] @ Store initial r1, the entry info + MOV r3, #0 @ Build initial register value + 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 +@ +@ r9 is the module's PIC base, and seeding it is not optional for a module +@ built -fpic -msingle-pic-base. Every reference the module makes to one of +@ its own globals compiles to LDR rX, [r9, #offset] -- an offset into the +@ global offset table, measured from whatever r9 holds -- and -msingle-pic-base +@ is precisely the promise that nothing in the module will set r9 up, because +@ the caller already did. Here is the caller. +@ +@ The value is the module's data base, which is also where _gcc_setup puts the +@ GOT, because the compiler measures its offsets from the GOT origin and the +@ linker script puts the GOT first in the data segment for that reason. Offset +@ 8 in the thread entry info is txm_module_thread_entry_info_data_base_address, +@ whose declaration in txm_module.h carries the comment "Don't move this, +@ referenced in stack build to setup module data base register." This is the +@ reference it means. +@ +@ Left at zero, as the base port's stack build leaves it and as this port did +@ until now, every module data reference reads address 0 + a small offset. +@ Silicon showed exactly that: r9 was 0 in every fault captured on the S32Z280. +@ + LDR r3, [r12, #8] @ Pickup the module's data base + STR r3, [r2, #44] @ Store initial r9, the PIC base + 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 +@ +@ /* The mode a module thread starts in is the whole point of this function +@ existing separately. A kernel thread starts in SVC mode; a module thread +@ starts in User mode when it asked for protection, and System mode when it +@ did not. Everything else about the frame is identical, which is why this +@ is derived from the port's own stack build rather than from another +@ architecture's module port. +@ +@ The flag is read from tx_thread_module_user_mode at 0xA0. The Cortex-R4 +@ module port reads it from 0x9C, which in this port is +@ tx_thread_module_current_user_mode -- a different field, holding whether +@ the thread is privileged right now rather than whether it should start +@ unprivileged. Copying that offset would have built cleanly and started +@ module threads in the wrong mode. */ +@ + MRS r1, CPSR @ Pickup CPSR + BIC r1, r1, #CPSR_MASK @ Mask mode bits of CPSR +@ + TST r0, #1 @ Was a Thumb entry point requested? + ORRNE r1, r1, #THUMB_MASK @ Yes, start in Thumb state +@ + LDR r3, [r0, #THREAD_MODULE_USER_MODE] @ Pickup the requested user mode + TST r3, #1 + ORREQ r3, r1, #SYS_MODE_BITS @ Clear: privileged, System mode + ORRNE r3, r1, #USR_MODE_BITS @ Set: unprivileged, User mode + 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_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S new file mode 100644 index 000000000..c52defec4 --- /dev/null +++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S @@ -0,0 +1,132 @@ +@/*************************************************************************** +@ * 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 +@ **************************************************************************/ +@ +@/**************************************************************************/ +@/* */ +@/* MODULE MANAGER RELEASE */ +@/* */ +@/* txm_module_manager_user_mode_entry.S Cortex-R52/GNU */ +@/* 6.5.2 */ +@/* AUTHOR */ +@/* */ +@/* Frédéric Desbiens, Eclipse Foundation */ +@/* */ +@/* DESCRIPTION */ +@/* */ +@/* The only way a module reaches the kernel. */ +@/* */ +@/* A module runs in User mode and cannot execute kernel code or touch */ +@/* kernel memory. When it calls a ThreadX service, the call arrives */ +@/* here: SVC 1 raises privilege, the dispatch function performs the */ +@/* service, SVC 2 drops back to User mode, and the module continues. */ +@/* */ +@/* This function is the entire privileged surface a module can see. */ +@/* It gets an MPU region of its own -- the one at */ +@/* TXM_MODULE_MPU_KERNEL_ENTRY_INDEX -- because a module must be able */ +@/* to execute these few instructions and nothing else on that side of */ +@/* the boundary. Everything the module is allowed to ask for is */ +@/* decided inside _txm_module_manager_kernel_dispatch, in kernel */ +@/* memory the module cannot reach. */ +@/* */ +@/* SVC 1 and SVC 2 are handled by the port's supervisor call vector. */ +@/* */ +@/* CALLS */ +@/* */ +@/* SVC 1 Leave User mode */ +@/* _txm_module_manager_kernel_dispatch Perform the service */ +@/* SVC 2 Return to User mode */ +@/* */ +@/* CALLED BY */ +@/* */ +@/* Modules in User mode */ +@/* */ +@/**************************************************************************/ + + .arm + +@ Its own section, placed after __code_end__ by the linker script, so that this +@ function sits OUTSIDE the kernel's code region. +@ +@ It has to. A module runs in User mode and the kernel code region grants no +@ EL0 access, so the module needs a region of its own covering these +@ instructions -- and PMSAv8-R has no region priority, so that region must not +@ overlap the kernel's. Two enabled regions matching one address is +@ CONSTRAINED UNPREDICTABLE, and on this part it aborts: with this function +@ still inside .text, the first memory access after a module's regions were +@ loaded took a data abort in the scheduler. +@ +@ Nothing else may share the section, because whatever does becomes executable +@ by every module. + + .section .txm_user_entry, "ax", %progbits + +@ 64-byte aligned, which is the PMSAv8-R granule and all that is needed here. +@ +@ The Cortex-R4 module port aligns this to 4 KB, and has to: PMSAv7 regions must +@ be a power of two in size and aligned to their own size, so the smallest +@ region that can cover this function without also covering its neighbours is a +@ page. A base and limit pair has no such constraint, so the kernel entry +@ region can be sized to these instructions and stop. That matters for +@ isolation rather than for memory: on PMSAv7 whatever else shares the page is +@ inside the one region a module is allowed to execute. + + .align 6 + + .global _txm_module_manager_user_mode_entry + .global _txm_module_manager_user_mode_entry_end + +@ The supervisor call handler compares the faulting lr against these two, to +@ refuse an SVC 1 or SVC 2 raised from anywhere else. They have to be visible +@ outside this file for that check to link, which also means they are the two +@ addresses the whole privilege boundary rests on. + + .global _txm_system_mode_enter + .global _txm_system_mode_exit + .extern _txm_module_manager_kernel_dispatch + + .type _txm_module_manager_user_mode_entry, %function +_txm_module_manager_user_mode_entry: + +_txm_system_mode_enter: + +@ Leave User mode. The supervisor call vector recognises 1 and raises the +@ thread to privileged mode on its kernel stack. + + SVC 1 + +_txm_module_priv: + +@ Privileged now. r3 is pushed alongside lr only to keep the stack eight-byte +@ aligned, which the ABI requires at a public interface. + + PUSH {r3, lr} + BL _txm_module_manager_kernel_dispatch + POP {r3, lr} + +_txm_system_mode_exit: + +@ Back to User mode before returning to the module. If this were skipped the +@ module would resume privileged, which is the whole protection gone -- so it is +@ unconditional and sits between the dispatch and every return path. + + SVC 2 + + BX lr + +@ Marks the end of the region a module is allowed to execute, so the region can +@ be sized to this function rather than rounded up to something larger. + +_txm_module_manager_user_mode_entry_end: + NOP