summaryrefslogtreecommitdiff
path: root/ports_module/cortex_r52
diff options
context:
space:
mode:
Diffstat (limited to 'ports_module/cortex_r52')
-rw-r--r--ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_demo_module.lds208
-rw-r--r--ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds295
-rw-r--r--ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/module_blob.S69
-rw-r--r--ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module.c319
-rw-r--r--ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c1624
-rw-r--r--ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_properties.c1086
-rw-r--r--ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/txm_module_preamble.S151
-rw-r--r--ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_demo_module.lds199
-rw-r--r--ports_module/cortex_r52/gnu/example_build/s32z280_evb/link_module.lds344
-rw-r--r--ports_module/cortex_r52/gnu/example_build/s32z280_evb/module_blob.S65
-rw-r--r--ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module.c319
-rw-r--r--ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c1736
-rw-r--r--ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb252
-rw-r--r--ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb657
-rwxr-xr-xports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh187
-rw-r--r--ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S151
-rw-r--r--ports_module/cortex_r52/gnu/inc/tx_port.h442
-rw-r--r--ports_module/cortex_r52/gnu/inc/txm_module_port.h491
-rw-r--r--ports_module/cortex_r52/gnu/module_lib/src/txm_module_gcc_setup.S211
-rw-r--r--ports_module/cortex_r52/gnu/module_lib/src/txm_module_thread_shell_entry.c168
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_restore.S264
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/tx_thread_context_save.S193
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S504
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/tx_thread_stack_build.S191
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/tx_thread_system_return.S169
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_alignment_adjust.c97
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c228
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_fault_capture.S285
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_handler.c139
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_memory_fault_notify.c78
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_mm_register_setup.c219
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c261
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_svc_handler.S233
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S251
-rw-r--r--ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S133
35 files changed, 12219 insertions, 0 deletions
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 00000000..dbe1e069
--- /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 00000000..14ced186
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/link_module.lds
@@ -0,0 +1,295 @@
+/***************************************************************************
+ * 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: an access
+ * hitting more than one enabled region takes a translation fault
+ * (TRM 8.1). 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 00000000..b3c2d44d
--- /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 00000000..e2fd2508
--- /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 00000000..365dfecf
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_manager.c
@@ -0,0 +1,1624 @@
+/***************************************************************************
+ * 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)
+
+/* The two grants that must be refused on size, and the one that must not be.
+
+ The top granule is the boundary case of the overflow guard: its inclusive end
+ is exactly 0xFFFFFFFF, so a guard written as (address + length) < address, or
+ one that tested the end against the top with the wrong comparison, would
+ refuse a perfectly legal grant. The wrapping grant starts at the same place
+ and asks for twice the room that is left.
+
+ Neither address is ever programmed into the MPU: the guard probes run on an
+ instance that is loaded and unloaded without being started, so the accepted
+ grant is recorded in the instance and nothing more. That is deliberate --
+ the top granule sits inside the 0x80000000..0xFFFFFFFF device region the BSP
+ maps at boot, and two enabled regions over one address is exactly the overlap
+ this image checks for everywhere else. */
+
+#define MODULE_TOP_GRANULE_ADDRESS (0xFFFFFFFFUL - (TXM_MODULE_MPU_ALIGNMENT - 1UL))
+#define MODULE_TOP_GRANULE_LENGTH TXM_MODULE_MPU_ALIGNMENT
+#define MODULE_WRAP_LENGTH (2UL * TXM_MODULE_MPU_ALIGNMENT)
+
+/* 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];
+
+/* The shared-grant size guards, which are about the manager and not about the
+ module. They need a LOADED instance and nothing else, so they get their own
+ -- loaded after the four passes are done with the pool, never started, and
+ unloaded again. Not folded into the shared pass because that pass grants
+ every entry the port has and has none to spare for a grant that is meant to
+ succeed, and not folded into one of the other three because a grant they
+ accepted would be programmed into the MPU when their module ran. */
+
+typedef struct GUARD_RESULT_STRUCT
+{
+ ULONG guard_load_status;
+ ULONG guard_count_start; /* entries held before any probe */
+ ULONG guard_zero_status; /* refusing a zero-length grant */
+ ULONG guard_count_zero; /* entries held after it */
+ ULONG guard_wrap_status; /* refusing a grant that runs off 4 GB */
+ ULONG guard_count_wrap;
+ ULONG guard_top_status; /* accepting one that ends exactly at it */
+ ULONG guard_count_top;
+ ULONG guard_unload_status;
+} GUARD_RESULT;
+
+static TXM_MODULE_INSTANCE guard_module;
+static GUARD_RESULT guard_result;
+
+/* 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;
+}
+
+
+/**************************************************************************/
+/* The size guards on a shared grant. */
+/* */
+/* A grant of no bytes and a grant whose inclusive end wraps past the top */
+/* of the address space both used to compute a limit BELOW the base, */
+/* program it, return TX_SUCCESS and spend one of the five entries on a */
+/* region the hardware cannot honour. Both must now be refused as */
+/* TX_SIZE_ERROR, and -- the half that a status code alone does not say -- */
+/* must leave the entry count exactly where they found it, because an */
+/* entry spent on a refused grant is one the caller can never get back. */
+/* */
+/* The third probe is the one that must SUCCEED: a grant ending exactly */
+/* at 0xFFFFFFFF is legal, and a guard that refused it would be a new bug */
+/* in place of the old one. It is checked last so that the count it does */
+/* move is unambiguous. */
+/* */
+/* Loaded here and unloaded below without ever being started, so nothing */
+/* these probes accept is programmed into an MPU region. See the comment */
+/* on MODULE_TOP_GRANULE_ADDRESS for why that matters. */
+/**************************************************************************/
+
+static void run_guard_probes(VOID *location)
+{
+ GUARD_RESULT *result = &guard_result;
+ TXM_MODULE_INSTANCE *instance = &guard_module;
+
+ result -> guard_load_status = (ULONG) txm_module_manager_in_place_load(instance,
+ "shared-grant guards",
+ location);
+
+ if (result -> guard_load_status != (ULONG) TX_SUCCESS)
+ {
+ return;
+ }
+
+ result -> guard_count_start = instance -> txm_module_instance_shared_memory_count;
+
+ /* No bytes at all, at an address that is otherwise perfectly grantable --
+ the progress granule every other pass is given. So what is refused is
+ the length and nothing else. */
+
+ result -> guard_zero_status =
+ (ULONG) txm_module_manager_external_memory_enable(
+ instance,
+ (VOID *) &__module_status_start__,
+ 0UL,
+ TXM_MODULE_ATTRIBUTE_READ_WRITE);
+
+ result -> guard_count_zero = instance -> txm_module_instance_shared_memory_count;
+
+ /* Twice the room left above the base, so the inclusive end wraps to a low
+ address. Aligned, so again it is the length that is being refused. */
+
+ result -> guard_wrap_status =
+ (ULONG) txm_module_manager_external_memory_enable(
+ instance,
+ (VOID *) MODULE_TOP_GRANULE_ADDRESS,
+ MODULE_WRAP_LENGTH,
+ TXM_MODULE_ATTRIBUTE_READ_WRITE);
+
+ result -> guard_count_wrap = instance -> txm_module_instance_shared_memory_count;
+
+ /* And the boundary: the same base, asking for exactly the room there is. */
+
+ result -> guard_top_status =
+ (ULONG) txm_module_manager_external_memory_enable(
+ instance,
+ (VOID *) MODULE_TOP_GRANULE_ADDRESS,
+ MODULE_TOP_GRANULE_LENGTH,
+ TXM_MODULE_ATTRIBUTE_READ_WRITE);
+
+ result -> guard_count_top = instance -> txm_module_instance_shared_memory_count;
+
+ /* Unloaded, not stopped: it was never started. Recorded rather than
+ discarded, because a module that will not unload holds the pool the
+ nothing-after-it would need. */
+
+ result -> guard_unload_status = (ULONG) txm_module_manager_unload(instance);
+}
+
+
+/**************************************************************************/
+/* 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 size guards, reported and judged. Returns failures found. */
+/**************************************************************************/
+
+static UINT report_and_judge_guards(void)
+{
+ const GUARD_RESULT *result = &guard_result;
+
+ console_puts("\n--- shared-grant size guards ---\n");
+
+ put_field(" load status = ", result -> guard_load_status);
+ put_field(" entries at start = ", result -> guard_count_start);
+ put_field(" zero length = ", result -> guard_zero_status);
+ put_field(" should be = ", (unsigned long) TX_SIZE_ERROR);
+ put_field(" entries after = ", result -> guard_count_zero);
+ put_field(" wrapping end = ", result -> guard_wrap_status);
+ put_field(" should be = ", (unsigned long) TX_SIZE_ERROR);
+ put_field(" entries after = ", result -> guard_count_wrap);
+ put_field(" ends at 0xFFFFFFFF = ", result -> guard_top_status);
+ put_field(" should be = ", (unsigned long) TX_SUCCESS);
+ put_field(" entries after = ", result -> guard_count_top);
+ put_field(" unload status = ", result -> guard_unload_status);
+
+ if (result -> guard_load_status != (ULONG) TX_SUCCESS)
+ {
+ console_puts("FAIL shared-grant guards: the probe module did not load\n");
+ return 1U;
+ }
+
+ if (result -> guard_count_start != 0UL)
+ {
+ console_puts("FAIL shared-grant guards: a fresh instance already held entries\n");
+ return 1U;
+ }
+
+ if (result -> guard_zero_status != (ULONG) TX_SIZE_ERROR)
+ {
+ console_puts("FAIL shared-grant guards: a zero-length grant was not refused as a size error\n");
+ return 1U;
+ }
+
+ if (result -> guard_wrap_status != (ULONG) TX_SIZE_ERROR)
+ {
+ console_puts("FAIL shared-grant guards: a grant running off the top of memory was not refused\n");
+ return 1U;
+ }
+
+ /* The half a status code does not say. A refused grant that still spent an
+ entry would report exactly the same two codes above and quietly cost the
+ caller one of the five, with nothing to show for it. */
+
+ if ((result -> guard_count_zero != 0UL) || (result -> guard_count_wrap != 0UL))
+ {
+ console_puts("FAIL shared-grant guards: a refused grant consumed an entry\n");
+ return 1U;
+ }
+
+ /* And the boundary the guard must not overreach into. */
+
+ if (result -> guard_top_status != (ULONG) TX_SUCCESS)
+ {
+ console_puts("FAIL shared-grant guards: a grant ending at 0xFFFFFFFF was refused\n");
+ return 1U;
+ }
+
+ if (result -> guard_count_top != 1UL)
+ {
+ console_puts("FAIL shared-grant guards: the accepted grant did not consume exactly one entry\n");
+ return 1U;
+ }
+
+ if (result -> guard_unload_status != (ULONG) TX_SUCCESS)
+ {
+ console_puts("FAIL shared-grant guards: the probe module did not unload\n");
+ return 1U;
+ }
+
+ console_puts("PASS shared-grant guards: empty and wrapping grants refused, the boundary grant accepted\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 size guards on a grant, which are the manager's business and not
+ the module's. Last, because it wants the pool to itself: every pass
+ above has released its data allocation by now. */
+
+ console_puts("M8 shared-grant size guards\n");
+ run_guard_probes((VOID *) &__module_image_start__);
+ }
+
+ /* ------------------------------------------------------------------
+ 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]);
+ }
+
+ failures += report_and_judge_guards();
+
+ 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 -- an access that
+ hits more than one enabled region takes a translation fault (TRM 8.1), so
+ an overlap 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/sample_threadx_module_properties.c b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_properties.c
new file mode 100644
index 00000000..a2493409
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/sample_threadx_module_properties.c
@@ -0,0 +1,1086 @@
+/***************************************************************************
+ * 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_properties.c Cortex-R52/GNU */
+/* 6.5.2 */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* DESCRIPTION */
+/* */
+/* Walks the module property combinations this port accepts and */
+/* refuses, and checks each one against the documented contract, on */
+/* the Armv8-R AEM FVP with no debugger and no person in the loop. */
+/* */
+/* WHY THIS IS A SEPARATE IMAGE from fvp_module.elf. That one loads */
+/* one property word -- 0x02000003, the only combination any example */
+/* in the tree ships -- and asks what the hardware does about a module */
+/* that misbehaves. This one asks what the LOADER does about a module */
+/* whose property word is something else, which is a question about a */
+/* module that never runs at all in four of the six cases below. The */
+/* two have different subjects and different verdicts, and the only */
+/* thing they share is the module blob. */
+/* */
+/* WHAT THE CONTRACT IS. TXM_MODULE_MANAGER_REQUIRED_OPTIONS in */
+/* txm_module_port.h requires TXM_MODULE_USER_MODE and */
+/* TXM_MODULE_MEMORY_PROTECTION together, which every other module port */
+/* in the tree leaves at zero. The reason is PMSAv8-R and not policy: */
+/* the port programs a module's region table only when both bits are */
+/* set, the scheduler closes the kernel's window over module memory */
+/* before it dispatches a module thread, and per the Cortex-R52 TRM */
+/* section 8.2.1 there is no background region to fall back on -- EL0 */
+/* accesses are faulted whenever the MPU is enabled, and EL1 needs */
+/* SCTLR.BR, which neither board support package sets. So a module */
+/* that asks for user mode without protection is not an unprotected */
+/* module. It is a module with no mapping, and it aborts on the fetch */
+/* of its own first instruction. */
+/* */
+/* TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS is supported and optional, */
+/* and both halves of that are checked below rather than asserted. */
+/* */
+/* ONE BLOB, SIX PROPERTY WORDS. The property word is a field of the */
+/* preamble, and the preamble is the first thing in the module image, so */
+/* a case is set up by copying the blob into the staging area and */
+/* writing one word of the copy. Nothing else about the module changes */
+/* between cases, which is what makes the six verdicts comparable: the */
+/* only variable is the word under test. Six separately built module */
+/* images would each also carry their own code, and a difference in */
+/* outcome could then be blamed on any of it. */
+/* */
+/* THE FOUR REFUSALS ARE CHECKED BY NAME, not by "it did not run". A */
+/* load that failed with TX_NO_MEMORY, or one that succeeded and then */
+/* faulted in the module's first instruction, would both leave the */
+/* module not running. The whole point of the contract is that the */
+/* refusal arrives from txm_module_manager_in_place_load as */
+/* TXM_MODULE_INVALID_PROPERTIES, at the moment the property word can */
+/* still be corrected, so that status is what is compared. */
+/* */
+/* A REFUSAL MUST ALSO COST NOTHING. Each case gets its own instance */
+/* out of .bss, and a refused case's instance is checked to be */
+/* untouched and the manager's loaded count to be unmoved. The port */
+/* would still be wrong if it rejected the module and left half an */
+/* instance behind, and the four refusals run before the two loads for */
+/* the same reason: what follows them proves the manager is still able */
+/* to load. */
+/* */
+/* THE TWO ACCEPTED CASES ARE RUN, not merely loaded. A load that */
+/* returns TX_SUCCESS says nothing about whether the region table it */
+/* programmed describes anything, which is exactly the defect this file */
+/* exists for. So both accepted cases start the module and wait for the */
+/* abort it is written to provoke, and the abort is attributed to the */
+/* case by thread pointer and checked for its address, its mode and its */
+/* offset into the module's own code. */
+/* */
+/* The two differ in what they are granted, which is the optional half */
+/* of the contract: */
+/* */
+/* 0x03, user mode and protection. Granted NOTHING. It reports */
+/* through no shared granule, so what is read back is the abort: the */
+/* module wrote its own data, then reached the status word it was */
+/* never granted, and faulted there. A module that did not ask for */
+/* shared access and was given none cannot reach the shared area, and */
+/* the fault address says so. */
+/* */
+/* 0x07, and the status granule granted. The module reports its */
+/* progress through that granule, gets as far as a kernel call, and */
+/* faults where fvp_module.elf's first pass does -- on the kernel's */
+/* data. So the shared grant is what is under test here, and the */
+/* progress word is the evidence that it worked. */
+/* */
+/* WHAT A GREEN RUN HERE DOES NOT PROVE. Two things, and both are */
+/* limits of the contract rather than gaps in the checking. */
+/* */
+/* The port does not gate txm_module_manager_external_memory_enable on */
+/* TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS, and neither does any other */
+/* module port; a manager that granted a region to the 0x03 module would */
+/* be obliged. That is why the 0x03 case above is granted nothing rather */
+/* than granted something and expected to be refused: this file checks */
+/* the contract the port states, and the port does not state that one. */
+/* */
+/* And nothing here exercises the property-flag test in */
+/* tx_thread_schedule.S, which decides the same question a second time */
+/* for a thread being dispatched. It cannot: the loader refuses every */
+/* combination that would reach the scheduler with an unprogrammed region */
+/* table, so the assembly's test is unreachable by construction while */
+/* TXM_MODULE_MANAGER_REQUIRED_OPTIONS stands. That redundancy is the */
+/* point of it -- the scheduler stops depending on a loader invariant it */
+/* cannot see -- but it does mean the test is covered by the offset */
+/* assertion in txm_module_manager_offset_check.c and by reading it, and */
+/* not by any run of this image. */
+/* */
+/**************************************************************************/
+
+#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. */
+
+extern TXM_MODULE_MANAGER_MEMORY_FAULT_INFO _txm_module_manager_memory_fault_info;
+
+/* How many modules the manager believes are loaded. Read rather than inferred:
+ a refused load that still linked an instance into the loaded list would be a
+ defect this file is in a position to see. The upstream spelling of the symbol
+ has "manger" in it. */
+
+extern ULONG _txm_module_manger_loaded_count;
+
+/* Where the module image sits, and the writable copy every case is built in.
+ Both provided by link_module.lds, which places the module's preamble first --
+ so __module_image_start__ is also the preamble address. */
+
+extern unsigned char __module_image_start__;
+extern unsigned char __module_image_end__;
+
+extern unsigned char __module_stage_start__;
+extern unsigned char __module_stage_end__;
+
+/* The shared granule the 0x07 case reports its progress through, and the one
+ address the 0x03 case is expected to fault on. */
+
+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
+ reaches for really is memory it must not have. */
+
+extern unsigned char __data_start__;
+extern unsigned char __data_end__;
+
+/* The address the module reaches for in step 3, kept in step with
+ sample_threadx_module.c by hand and checked at run time to lie inside the
+ kernel's data region. An address in no region at all would fault too, and
+ would prove much less. */
+
+#define MODULE_FORBIDDEN_ADDRESS 0x003EFFC0UL
+
+/* The shared status granule. The same number is FVP_MODULE_STATUS_BASE in
+ platform.h, ORIGIN(MODULE) in link_module.lds and MODULE_STATUS_ADDRESS in the
+ module. Checked against the linker symbol below rather than trusted.
+
+ MISRA C:2012 Rule 11.6 is deliberately violated when this is dereferenced:
+ the address is an agreement between two separately linked images and there is
+ no conforming way to express it. */
+
+#define MODULE_STATUS_ADDRESS FVP_MODULE_STATUS_BASE
+#define MODULE_STATUS_LENGTH FVP_MODULE_STATUS_SIZE
+#define MODULE_STATUS_GRANULES FVP_MODULE_STATUS_GRANULES
+#define MODULE_STATUS_AREA_LENGTH (MODULE_STATUS_GRANULES * MODULE_STATUS_LENGTH)
+
+/* The progress bits the module sets, mirrored from sample_threadx_module.c. The
+ SURVIVED bits can only ever appear 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)
+
+/* What the module does when it gets that far, selected from the low byte of the
+ application-defined module ID. The preamble ships 0x52520001, so the default
+ is the data abort on the kernel's data and no case here has to set anything. */
+
+#define MODULE_PROGRESS_RAN_AND_STOLE (MODULE_PROGRESS_OWN_DATA | \
+ MODULE_PROGRESS_KERNEL_CALL | \
+ MODULE_PROGRESS_ATTEMPTED_STEAL)
+
+/* Memory the manager hands out to modules. In the module area, not in .bss: a
+ module's data becomes an MPU region, and a region may not overlap a kernel
+ one. See the same declaration in sample_threadx_module_manager.c. */
+
+#ifndef MODULE_POOL_SIZE
+#define MODULE_POOL_SIZE (16U * 1024U)
+#endif
+
+static unsigned char module_pool[MODULE_POOL_SIZE]
+ __attribute__((aligned(64), section(".module_pool")));
+
+/* The object pool the kernel stacks of a User-mode module's threads come from.
+ Not optional: txm_module_manager_initialize deliberately leaves this pool
+ uncreated, and without it 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)));
+
+
+/**************************************************************************/
+/* The cases. */
+/* */
+/* Every low-byte combination the port has an opinion about, and the */
+/* opinion. The compiler field is ORed in when the word is written, so */
+/* the option bits stay legible here. */
+/**************************************************************************/
+
+typedef struct PROPERTY_CASE_STRUCT
+{
+ CHAR *case_name;
+ ULONG case_options; /* the option bits under test */
+ UINT case_expect_load; /* what in_place_load must return */
+ UINT case_grant_status; /* grant the status granule? */
+ ULONG case_expect_fault; /* where it must fault, when it runs */
+ ULONG case_expect_progress; /* what the granule must show, if any */
+} PROPERTY_CASE;
+
+/* The four refusals come first, and the two loads after them, so that what
+ follows a refusal proves the manager is still able to load a module. The
+ order is otherwise free: every case unloads before the next one starts. */
+
+static const PROPERTY_CASE property_cases[] =
+{
+ {
+ "0x00 nothing asked for",
+ 0UL,
+ TXM_MODULE_INVALID_PROPERTIES,
+ 0U, 0UL, 0UL
+ },
+ {
+ "0x01 user mode without protection",
+ TXM_MODULE_USER_MODE,
+ TXM_MODULE_INVALID_PROPERTIES,
+ 0U, 0UL, 0UL
+ },
+ {
+ "0x02 protection without user mode",
+ TXM_MODULE_MEMORY_PROTECTION,
+ TXM_MODULE_INVALID_PROPERTIES,
+ 0U, 0UL, 0UL
+ },
+ {
+ "0x05 user mode and shared, without protection",
+ TXM_MODULE_USER_MODE | TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS,
+ TXM_MODULE_INVALID_PROPERTIES,
+ 0U, 0UL, 0UL
+ },
+
+ /* Accepted, and granted nothing. It faults on the status word, which is
+ the shared area it never asked for and was never given. */
+
+ {
+ "0x03 user mode and protection",
+ TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION,
+ TX_SUCCESS,
+ 0U, MODULE_STATUS_ADDRESS, 0UL
+ },
+
+ /* Accepted, and granted the status granule. It therefore reports its
+ progress and gets as far as the kernel's data before it faults. */
+
+ {
+ "0x07 user mode, protection and shared memory",
+ TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION
+ | TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS,
+ TX_SUCCESS,
+ 1U, MODULE_FORBIDDEN_ADDRESS, MODULE_PROGRESS_RAN_AND_STOLE
+ },
+};
+
+#define PROPERTY_CASES (sizeof(property_cases) / sizeof(property_cases[0]))
+
+/* One instance per case, so that a refused case's instance can be checked to
+ have been left alone. Sharing one would make that unprovable. */
+
+static TXM_MODULE_INSTANCE demo_module[PROPERTY_CASES];
+
+
+/**************************************************************************/
+/* What each case produced. */
+/**************************************************************************/
+
+typedef struct CASE_RESULT_STRUCT
+{
+ ULONG result_properties; /* the whole word that was written */
+ ULONG result_load_status;
+ ULONG result_share_status;
+ ULONG result_start_status;
+ ULONG result_stop_status;
+ ULONG result_unload_status;
+
+ /* What a refused load must not have done. */
+
+ ULONG result_instance_id;
+ ULONG result_loaded_count;
+
+ /* Where the manager put it, for the cases that loaded. */
+
+ ULONG result_code_start;
+ ULONG result_code_end;
+ ULONG result_data_start;
+ ULONG result_data_end;
+ ULONG result_data_base;
+
+ /* What the hardware and the module said, for the cases that ran. */
+
+ ULONG result_captured;
+ ULONG result_dfsr;
+ ULONG result_dfar;
+ ULONG result_ifsr;
+ ULONG result_ifar;
+ ULONG result_spsr;
+ ULONG result_fault_r9;
+ ULONG result_code_location;
+ ULONG result_progress;
+ ULONG result_faults;
+ ULONG result_notify_thread;
+ ULONG result_notify_instance;
+ ULONG result_expect_thread;
+ ULONG result_expect_instance;
+} CASE_RESULT;
+
+static CASE_RESULT case_results[PROPERTY_CASES];
+
+/* What the fault handler saw. */
+
+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. */
+/* */
+/* 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. */
+/**************************************************************************/
+
+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 granule, 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. */
+/* */
+/* 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 word;
+
+ /* The whole area, not just the progress word. A case that is granted
+ nothing must be seen to have written nothing, and a mark left by the
+ previous case would read as one that reached memory it never held. */
+
+ for (word = 0UL; word < (MODULE_STATUS_AREA_LENGTH / 4UL); word++)
+ {
+ ((volatile ULONG *) MODULE_STATUS_ADDRESS)[word] = 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 copy of under two kilobytes */
+/* that happens six times. */
+/**************************************************************************/
+
+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];
+ }
+}
+
+
+/**************************************************************************/
+/* Build one case in the staging area: the blob, with one word replaced. */
+/* */
+/* Copied afresh for every case rather than patched in place, so that a */
+/* case cannot inherit anything from the one before it -- including the */
+/* previous property word, which is the single variable under test. */
+/* */
+/* The cache maintenance is not a precaution the passing run justifies. */
+/* These are 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 what */
+/* main memory still holds. A cold I cache over a never-executed address */
+/* happens to work, and keeps happening to work until the staging area is */
+/* reused, which is exactly what this file does six times. */
+/**************************************************************************/
+
+static ULONG stage_module(ULONG options)
+{
+ unsigned long blob_size;
+ TXM_MODULE_PREAMBLE *preamble;
+ ULONG properties;
+
+ blob_size = (unsigned long) (&__module_image_end__ - &__module_image_start__);
+
+ copy_bytes(&__module_stage_start__, &__module_image_start__, blob_size);
+
+ /* The preamble is the first thing in the image, so the staged copy's base is
+ the preamble's address. Written through the structure rather than at a
+ hard-coded word offset: txm_module_manager_in_place_load reads the same
+ structure, and a field that moved would move for both. */
+
+ preamble = (TXM_MODULE_PREAMBLE *) (VOID *) &__module_stage_start__;
+
+ /* The compiler field is preserved and the option bits replaced. Rebuilding
+ the whole word from TXM_MODULE_GNU_COMPILER would work today and would
+ silently stop testing what it means to test if the module were ever built
+ with something else. */
+
+ properties = (preamble -> txm_module_preamble_property_flags
+ & ~((ULONG) TXM_MODULE_OPTIONS_MASK)) | options;
+
+ preamble -> txm_module_preamble_property_flags = properties;
+
+ cache_clean_range(&__module_stage_start__, blob_size);
+ cache_invalidate_icache_all();
+
+ return properties;
+}
+
+
+/**************************************************************************/
+/* One case: stage it, load it, and -- if the contract says it loads -- */
+/* start it and wait for the abort it is written to provoke. */
+/**************************************************************************/
+
+static void run_one_case(UINT index)
+{
+ const PROPERTY_CASE *test = &property_cases[index];
+ CASE_RESULT *result = &case_results[index];
+ TXM_MODULE_INSTANCE *instance = &demo_module[index];
+ unsigned int waited;
+
+ /* Cleared before the module starts, so a fault or a progress bit counted
+ here belongs to this case and not the previous one. */
+
+ fault_count = 0UL;
+ fault_notify_thread = 0UL;
+ fault_notify_instance = 0UL;
+ module_status_clear();
+
+ result -> result_properties = stage_module(test -> case_options);
+
+ /* name is CHAR * and not const CHAR *, because 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. */
+
+ result -> result_load_status =
+ (ULONG) txm_module_manager_in_place_load(instance, test -> case_name,
+ (VOID *) &__module_stage_start__);
+
+ /* Read for every case, and it is the refused ones that need it: a refusal
+ must leave the instance as .bss had it and the loaded count where it was.
+ Taken immediately, before anything else can move either. */
+
+ result -> result_instance_id = instance -> txm_module_instance_id;
+ result -> result_loaded_count = _txm_module_manger_loaded_count;
+
+ if (result -> result_load_status != (ULONG) TX_SUCCESS)
+ {
+ return;
+ }
+
+ result -> result_code_start = (ULONG) instance -> txm_module_instance_code_start;
+ result -> result_code_end = (ULONG) instance -> txm_module_instance_code_end;
+ result -> result_data_start = (ULONG) instance -> txm_module_instance_data_start;
+ result -> result_data_end = (ULONG) instance -> txm_module_instance_data_end;
+ result -> result_data_base =
+ (ULONG) instance -> txm_module_instance_module_data_base_address;
+
+ /* The shared granule, for the case that asked for shared access. Granted
+ while the module is LOADED and before it is STARTED, which is the only
+ window the manager accepts. */
+
+ if (test -> case_grant_status != 0U)
+ {
+ result -> result_share_status =
+ (ULONG) txm_module_manager_external_memory_enable(
+ instance, (VOID *) &__module_status_start__,
+ MODULE_STATUS_LENGTH, TXM_MODULE_ATTRIBUTE_READ_WRITE);
+
+ if (result -> result_share_status != (ULONG) TX_SUCCESS)
+ {
+ return;
+ }
+ }
+
+ /* What the notify callback must be told, recorded before the module runs so
+ the comparison afterwards is against an expectation. */
+
+ result -> result_expect_thread =
+ (ULONG) &(instance -> txm_module_instance_start_stop_thread);
+ result -> result_expect_instance = (ULONG) instance;
+
+ result -> result_start_status = (ULONG) txm_module_manager_start(instance);
+
+ if (result -> result_start_status != (ULONG) TX_SUCCESS)
+ {
+ return;
+ }
+
+ /* Wait for the abort vector to capture a fault belonging to THIS case.
+ Attribution is by thread pointer: each case has its own instance and
+ therefore its own start thread, so a captured fault naming this case's
+ thread cannot be a leftover from the previous one. */
+
+ 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 -> result_captured = 1UL;
+ result -> result_dfsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfsr;
+ result -> result_dfar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_dfar;
+ result -> result_ifsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifsr;
+ result -> result_ifar = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_ifar;
+ result -> result_spsr = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_spsr;
+ result -> result_fault_r9 = _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_r9;
+ result -> result_code_location = (ULONG) _txm_module_manager_memory_fault_info.txm_module_manager_memory_fault_info_code_location;
+ }
+
+ result -> result_progress = module_status_read();
+ result -> result_faults = fault_count;
+ result -> result_notify_thread = fault_notify_thread;
+ result -> result_notify_instance = fault_notify_instance;
+
+ /* Stopped, then 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 the stop is required, not tidiness. Unloaded
+ because the next case must start from a manager holding nothing, which is
+ what makes its loaded count comparable. */
+
+ result -> result_stop_status = (ULONG) txm_module_manager_stop(instance);
+ result -> result_unload_status = (ULONG) txm_module_manager_unload(instance);
+}
+
+
+/**************************************************************************/
+/* What one case produced, printed whether it passed or not. */
+/**************************************************************************/
+
+static void report_one_case(UINT index)
+{
+ const PROPERTY_CASE *test = &property_cases[index];
+ const CASE_RESULT *result = &case_results[index];
+
+ console_puts("\n--- ");
+ console_puts(test -> case_name);
+ console_puts(" ---\n");
+
+ put_field(" property word = ", result -> result_properties);
+ put_field(" load status = ", result -> result_load_status);
+ put_field(" should be = ", (unsigned long) test -> case_expect_load);
+ put_field(" instance id = ", result -> result_instance_id);
+ put_field(" modules loaded = ", result -> result_loaded_count);
+
+ if (result -> result_load_status != (ULONG) TX_SUCCESS)
+ {
+ return;
+ }
+
+ put_field(" code start = ", result -> result_code_start);
+ put_field(" code end = ", result -> result_code_end);
+ put_field(" data start = ", result -> result_data_start);
+ put_field(" data end = ", result -> result_data_end);
+ put_field(" data base (r9) = ", result -> result_data_base);
+ put_field(" share status = ", result -> result_share_status);
+ put_field(" start status = ", result -> result_start_status);
+ put_field(" stop status = ", result -> result_stop_status);
+ put_field(" unload status = ", result -> result_unload_status);
+ put_field(" faults notified = ", result -> result_faults);
+ put_field(" fault captured = ", result -> result_captured);
+ put_field(" progress = ", result -> result_progress);
+ put_field(" should be = ", test -> case_expect_progress);
+ put_field(" DFSR = ", result -> result_dfsr);
+ put_field(" DFAR = ", result -> result_dfar);
+ put_field(" should be = ", test -> case_expect_fault);
+ put_field(" SPSR = ", result -> result_spsr);
+ put_field(" faulting pc = ", result -> result_code_location);
+ put_field(" pc - code base = ",
+ result -> result_code_location - result -> result_code_start);
+}
+
+
+/**************************************************************************/
+/* And whether it is what the contract says. */
+/**************************************************************************/
+
+static UINT judge_one_case(UINT index)
+{
+ const PROPERTY_CASE *test = &property_cases[index];
+ const CASE_RESULT *result = &case_results[index];
+
+ /* The status, by name, first. Every other verdict below depends on the
+ load having done what the contract says, and "it did not run" is not the
+ same claim as "it was refused for its properties". */
+
+ if (result -> result_load_status != (ULONG) test -> case_expect_load)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": the load did not return what the contract says\n");
+ return 1U;
+ }
+
+ if (test -> case_expect_load != TX_SUCCESS)
+ {
+ /* A refusal must cost nothing. The property check in the portable
+ loader runs before it memsets the instance, so a refused case's
+ instance is still whatever .bss made it -- and if it were not, the
+ application would be holding a half-built module the manager does not
+ know about. */
+
+ if (result -> result_instance_id != 0UL)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": refused, but the instance was written to anyway\n");
+ return 1U;
+ }
+
+ if (result -> result_loaded_count != 0UL)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": refused, but the manager counts a loaded module\n");
+ return 1U;
+ }
+
+ console_puts("PASS ");
+ console_puts(test -> case_name);
+ console_puts(": refused as TXM_MODULE_INVALID_PROPERTIES, and cost nothing\n");
+ return 0U;
+ }
+
+ /* From here on the module was accepted, so it has to have RUN. A load that
+ returns TX_SUCCESS and programs a region table describing nothing is
+ exactly the defect this file exists for, and it looks identical to a
+ correct load until the module is dispatched. */
+
+ if ((test -> case_grant_status != 0U) &&
+ (result -> result_share_status != (ULONG) TX_SUCCESS))
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": asked for shared access and could not be granted any\n");
+ return 1U;
+ }
+
+ if (result -> result_start_status != (ULONG) TX_SUCCESS)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": loaded but would not start\n");
+ return 1U;
+ }
+
+ if ((result -> result_progress & MODULE_PROGRESS_SURVIVED) != 0UL)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": the module SURVIVED its violation -- it is not protected\n");
+ return 1U;
+ }
+
+ if (result -> result_captured == 0UL)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": started, and then never reached its own first fault\n");
+ return 1U;
+ }
+
+ /* The faulting pc must be an address inside the code region the manager
+ handed out. This is what separates "the module ran" from "the module was
+ dispatched and aborted immediately", which is the shape the property
+ contract exists to prevent: a zeroed region table faults on the fetch of
+ the first instruction, and that fault is reported at an address the
+ manager never mapped. */
+
+ if ((result -> result_code_location < result -> result_code_start) ||
+ (result -> result_code_location > result -> result_code_end))
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": faulted outside its own code, so it never really ran\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 -> result_spsr & 0x1FUL) != 0x10UL)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": faulted, but not from User mode\n");
+ return 1U;
+ }
+
+ /* Both accepted cases fault on a data access, so DFAR is the register that
+ carries the address in both. Which address differs, and that difference
+ IS the optional half of the contract: the case granted nothing faults on
+ the shared area it was never given, and the case granted the status
+ granule gets past it and faults on the kernel's data instead. */
+
+ if (result -> result_dfar != test -> case_expect_fault)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": faulted at the wrong address\n");
+ return 1U;
+ }
+
+ /* r9 is the module's PIC base, seeded by the manager from the thread entry
+ info. If the value captured at the fault is not the data base the manager
+ handed out, every data reference the module made went somewhere
+ unintended -- including the ones that appeared to work. */
+
+ if (result -> result_fault_r9 != result -> result_data_base)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": r9 was not the module's data base\n");
+ return 1U;
+ }
+
+ /* And what it managed on the way, for the case that could report. The case
+ granted nothing cannot write the granule, so its expectation is zero and
+ this comparison says the granule stayed as module_status_clear left it --
+ which is the same claim as the fault address above, made from the other
+ end. */
+
+ if (result -> result_progress != test -> case_expect_progress)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": did not reach the point it was supposed to fault at\n");
+ return 1U;
+ }
+
+ if (result -> result_faults != 1UL)
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": the fault-notify callback did not run exactly once\n");
+ return 1U;
+ }
+
+ if ((result -> result_notify_thread != result -> result_expect_thread) ||
+ (result -> result_notify_instance != result -> result_expect_instance))
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": notified about the wrong thread or module\n");
+ return 1U;
+ }
+
+ /* And the manager took it back. A module that cannot be unloaded leaves the
+ next case's loaded count wrong, so this is checked here rather than
+ inferred from the next verdict. */
+
+ if ((result -> result_stop_status != (ULONG) TX_SUCCESS) ||
+ (result -> result_unload_status != (ULONG) TX_SUCCESS))
+ {
+ console_puts("FAIL ");
+ console_puts(test -> case_name);
+ console_puts(": ran, but could not be stopped and unloaded\n");
+ return 1U;
+ }
+
+ console_puts("PASS ");
+ console_puts(test -> case_name);
+ console_puts(": loaded, ran in User mode, and faulted where it should\n");
+
+ return 0U;
+}
+
+
+/**************************************************************************/
+/* The manager thread. */
+/**************************************************************************/
+
+static void manager_entry(ULONG input)
+{
+ UINT status;
+ UINT i;
+ unsigned long blob_size;
+ 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. */
+
+ put_field("MPUIR regions available = ", (unsigned long) mpu_region_count());
+ put_field("regions this port needs = ", (unsigned long) MPU_MODULE_REGIONS_REQUIRED);
+
+ if (mpu_region_count() < MPU_MODULE_REGIONS_REQUIRED)
+ {
+ console_puts("FAIL the implementation has too few MPU regions for this port\n");
+ failures++;
+ }
+
+ /* The contract this image checks, printed so the log states it rather than
+ leaving a reader to infer it from six verdicts. */
+
+ put_field("supported options = ", (unsigned long) TXM_MODULE_MANAGER_SUPPORTED_OPTIONS);
+ put_field("required options = ", (unsigned long) TXM_MODULE_MANAGER_REQUIRED_OPTIONS);
+
+ if (TXM_MODULE_MANAGER_REQUIRED_OPTIONS
+ != (TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION))
+ {
+ /* Every case below encodes this contract in its expected load status, so
+ they are all stale the moment it changes and none of their verdicts
+ would mean anything. Reported and abandoned here rather than run:
+ relaxing the requirement makes the loader accept a module whose region
+ table was never programmed, and this image would then sit for the
+ runner's whole timeout waiting for a fault that a privileged module
+ under the kernel's window never takes. A stale contract is a result,
+ and it should arrive in a second rather than in three minutes. */
+
+ console_puts("FAIL this port no longer requires user mode and memory "
+ "protection together; every case below is stale\n");
+ console_puts("\nMODULE PROPERTY RESULT: FAILED\n");
+ console_exit(1U);
+ }
+
+ /* The addresses agreed between separately linked images, checked rather than
+ trusted. A disagreement here would present as a module faulting somewhere
+ unexpected, which is exactly what the 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 over it\n");
+ failures++;
+ }
+
+ if ((MODULE_FORBIDDEN_ADDRESS < (unsigned long) &__data_start__) ||
+ (MODULE_FORBIDDEN_ADDRESS >= (unsigned long) &__data_end__))
+ {
+ 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);
+
+ status = txm_module_manager_object_pool_create((VOID *) module_object_pool,
+ MODULE_OBJECT_POOL_SIZE);
+
+ put_field("object pool create = ", status);
+
+ console_puts("M2 fault notify\n");
+ txm_module_manager_memory_fault_notify(module_fault_notify);
+
+ /* Every case is built in the staging area, so the blob has to fit it before
+ any of them runs. Reported rather than allowed to overrun: the staging
+ area is a fixed size in the linker script and the module is free to grow. */
+
+ blob_size = (unsigned long) (&__module_image_end__ - &__module_image_start__);
+
+ console_puts("M3 the blob\n");
+ put_field(" blob size = ", blob_size);
+ put_field(" staging area = ",
+ (unsigned long) (&__module_stage_end__ - &__module_stage_start__));
+
+ if (blob_size > (unsigned long) (&__module_stage_end__ - &__module_stage_start__))
+ {
+ console_puts("FAIL the module image does not fit the staging area\n");
+ failures++;
+ }
+ else
+ {
+ for (i = 0U; i < (UINT) PROPERTY_CASES; i++)
+ {
+ console_puts("M4 case: ");
+ console_puts(property_cases[i].case_name);
+ console_puts("\n");
+
+ run_one_case(i);
+ }
+ }
+
+ /* ------------------------------------------------------------------
+ The verdict.
+ ------------------------------------------------------------------ */
+
+ console_puts("\n=== ThreadX module properties on the Armv8-R AEM FVP ===\n");
+
+ for (i = 0U; i < (UINT) PROPERTY_CASES; i++)
+ {
+ report_one_case(i);
+ }
+
+ console_puts("\n");
+
+ for (i = 0U; i < (UINT) PROPERTY_CASES; i++)
+ {
+ failures += judge_one_case(i);
+ }
+
+ /* The manager is still alive to print this, which is itself the last
+ result: four refusals and two module faults did not take the kernel with
+ them, and the manager ends holding nothing. */
+
+ if (_txm_module_manger_loaded_count != 0UL)
+ {
+ console_puts("FAIL the manager still believes a module is loaded\n");
+ failures++;
+ }
+
+ put_field("\nfailures = ", (unsigned long) failures);
+
+ if (failures == 0U)
+ {
+ console_puts("\nMODULE PROPERTY RESULT: ALL CHECKS PASSED\n");
+ }
+ else
+ {
+ console_puts("\nMODULE PROPERTY 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 property manager starting ===\n");
+
+ status = tx_thread_create(&report_thread, "property 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. */
+
+ 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 module properties :: 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 00000000..19bd8931
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/example_build/fvp_baser_aemv8r/txm_module_preamble.S
@@ -0,0 +1,151 @@
+@/***************************************************************************
+@ * 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
+@
+@ Both option bits are REQUIRED here, unlike on every other module port, and a
+@ preamble that carries either alone is refused by the loader with
+@ TXM_MODULE_INVALID_PROPERTIES. On PMSAv8-R that is not a policy: the port
+@ programs a module's regions only when both bits are set, the scheduler closes
+@ the kernel's window over module memory before it dispatches a module thread,
+@ and there is no background region to fall back on -- so a module that asked
+@ for user mode without protection would have no mapping at all rather than a
+@ weaker one. See TXM_MODULE_MANAGER_REQUIRED_OPTIONS in txm_module_port.h.
+@
+@ 0x00000004 TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS
+@
+@ is supported and optional. A module that does not ask for it simply never has
+@ a shared region granted to it.
+
+ .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 00000000..3fc17ef8
--- /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 00000000..a8962763
--- /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
+ * "<name> ALIGN(n) : { }" the ALIGN is the section's *address*, evaluated
+ * from a location counter that starts at zero, and it silently overrides
+ * "> CODE" -- every section then lands near address 0 and the link fails
+ * with "not within region". The FVP script uses that form harmlessly only
+ * because its region starts at 0x00000000. Alignment comes from the input
+ * sections (the vector tables carry .align 5) or from an explicit
+ * ". = ALIGN(n);" inside the braces.
+ */
+ .boot :
+ {
+ KEEP(*(.text.boot))
+ } > CODE
+
+ .vectors_el2 :
+ {
+ KEEP(*(.vectors_el2))
+ } > CODE
+
+ .vectors_el1 :
+ {
+ KEEP(*(.vectors_el1))
+ } > CODE
+
+ /* 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 00000000..bd404542
--- /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 00000000..dbf0c8a8
--- /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 00000000..7b7d822e
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/sample_threadx_module_manager.c
@@ -0,0 +1,1736 @@
+/***************************************************************************
+ * 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)
+
+/* The two grants that must be refused on size, and the one that must not be.
+
+ The top granule is the boundary case of the overflow guard: its inclusive end
+ is exactly 0xFFFFFFFF, so a guard written as (address + length) < address, or
+ one that tested the end against the top with the wrong comparison, would
+ refuse a perfectly legal grant. The wrapping grant starts at the same place
+ and asks for twice the room that is left.
+
+ Neither address is ever programmed into the MPU: the guard probes run on an
+ instance that is loaded and unloaded without being started, so the accepted
+ grant is recorded in the instance and nothing more. That matters more here
+ than on the model -- the top of the address space on this part is peripheral
+ space the boot regions already cover, and two enabled regions over one
+ address is a translation fault waiting for whoever reads it. */
+
+#define MODULE_TOP_GRANULE_ADDRESS (0xFFFFFFFFUL - (TXM_MODULE_MPU_ALIGNMENT - 1UL))
+#define MODULE_TOP_GRANULE_LENGTH TXM_MODULE_MPU_ALIGNMENT
+#define MODULE_WRAP_LENGTH (2UL * TXM_MODULE_MPU_ALIGNMENT)
+
+#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];
+
+/* The shared-grant size guards, which are about the manager and not about the
+ module. They need a LOADED instance and nothing else, so they get their own
+ -- loaded after the four passes are done with the pool, never started, and
+ unloaded again. Not folded into the shared pass because that pass grants
+ every entry the port has and has none to spare for a grant that is meant to
+ succeed, and not folded into one of the other three because a grant they
+ accepted would be programmed into the MPU when their module ran.
+
+ The byte pool reuses a freed block, so this instance's data lands on top of
+ an earlier pass's. Harmless: it never runs, and every pass has been read
+ back at its own pass_done() long before this loads. */
+
+typedef struct GUARD_RESULT_STRUCT
+{
+ ULONG guard_load_status;
+ ULONG guard_count_start; /* entries held before any probe */
+ ULONG guard_zero_status; /* refusing a zero-length grant */
+ ULONG guard_count_zero; /* entries held after it */
+ ULONG guard_wrap_status; /* refusing a grant that runs off 4 GB */
+ ULONG guard_count_wrap;
+ ULONG guard_top_status; /* accepting one that ends exactly at it */
+ ULONG guard_count_top;
+ ULONG guard_unload_status;
+} GUARD_RESULT;
+
+static TXM_MODULE_INSTANCE guard_module;
+static GUARD_RESULT guard_result;
+
+/* 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;
+}
+
+
+/**************************************************************************/
+/* The size guards on a shared grant. */
+/* */
+/* A grant of no bytes and a grant whose inclusive end wraps past the top */
+/* of the address space both used to compute a limit BELOW the base, */
+/* program it, return TX_SUCCESS and spend one of the five entries on a */
+/* region the hardware cannot honour. Both must now be refused as */
+/* TX_SIZE_ERROR, and -- the half that a status code alone does not say -- */
+/* must leave the entry count exactly where they found it, because an */
+/* entry spent on a refused grant is one the caller can never get back. */
+/* */
+/* The third probe is the one that must SUCCEED: a grant ending exactly */
+/* at 0xFFFFFFFF is legal, and a guard that refused it would be a new bug */
+/* in place of the old one. It is checked last so that the count it does */
+/* move is unambiguous. */
+/* */
+/* Loaded here and unloaded below without ever being started, so nothing */
+/* these probes accept is programmed into an MPU region. See the comment */
+/* on MODULE_TOP_GRANULE_ADDRESS for why that matters on this part. */
+/* */
+/* Nothing here needs the debugger: every value it produces is a manager */
+/* return code or a field of the manager's own instance, so the console */
+/* carries the whole result and there is no pass_done() for it. */
+/**************************************************************************/
+
+static void run_guard_probes(VOID *location)
+{
+ GUARD_RESULT *result = &guard_result;
+ TXM_MODULE_INSTANCE *instance = &guard_module;
+
+ result -> guard_load_status = (ULONG) txm_module_manager_in_place_load(instance,
+ "shared-grant guards",
+ location);
+
+ if (result -> guard_load_status != (ULONG) TX_SUCCESS)
+ {
+ return;
+ }
+
+ result -> guard_count_start = instance -> txm_module_instance_shared_memory_count;
+
+ /* No bytes at all, at an address that is otherwise perfectly grantable --
+ the progress granule every other pass is given. So what is refused is
+ the length and nothing else. */
+
+ result -> guard_zero_status =
+ (ULONG) txm_module_manager_external_memory_enable(
+ instance,
+ (VOID *) &__module_status_start__,
+ 0UL,
+ TXM_MODULE_ATTRIBUTE_READ_WRITE);
+
+ result -> guard_count_zero = instance -> txm_module_instance_shared_memory_count;
+
+ /* Twice the room left above the base, so the inclusive end wraps to a low
+ address. Aligned, so again it is the length that is being refused. */
+
+ result -> guard_wrap_status =
+ (ULONG) txm_module_manager_external_memory_enable(
+ instance,
+ (VOID *) MODULE_TOP_GRANULE_ADDRESS,
+ MODULE_WRAP_LENGTH,
+ TXM_MODULE_ATTRIBUTE_READ_WRITE);
+
+ result -> guard_count_wrap = instance -> txm_module_instance_shared_memory_count;
+
+ /* And the boundary: the same base, asking for exactly the room there is. */
+
+ result -> guard_top_status =
+ (ULONG) txm_module_manager_external_memory_enable(
+ instance,
+ (VOID *) MODULE_TOP_GRANULE_ADDRESS,
+ MODULE_TOP_GRANULE_LENGTH,
+ TXM_MODULE_ATTRIBUTE_READ_WRITE);
+
+ result -> guard_count_top = instance -> txm_module_instance_shared_memory_count;
+
+ /* Unloaded, not stopped: it was never started. Recorded rather than
+ discarded, because a module that will not unload holds the pool. */
+
+ result -> guard_unload_status = (ULONG) txm_module_manager_unload(instance);
+}
+
+
+/**************************************************************************/
+/* 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");
+}
+
+
+/**************************************************************************/
+/* The size guards, reported and judged. Returns failures found. */
+/**************************************************************************/
+
+static UINT report_and_judge_guards(void)
+{
+ const GUARD_RESULT *result = &guard_result;
+
+ linflexd_puts("\n--- shared-grant size guards ---\n");
+
+ put_field(" load status = ", result -> guard_load_status);
+ put_field(" entries at start = ", result -> guard_count_start);
+ put_field(" zero length = ", result -> guard_zero_status);
+ put_field(" should be = ", (unsigned long) TX_SIZE_ERROR);
+ put_field(" entries after = ", result -> guard_count_zero);
+ put_field(" wrapping end = ", result -> guard_wrap_status);
+ put_field(" should be = ", (unsigned long) TX_SIZE_ERROR);
+ put_field(" entries after = ", result -> guard_count_wrap);
+ put_field(" ends at 0xFFFFFFFF = ", result -> guard_top_status);
+ put_field(" should be = ", (unsigned long) TX_SUCCESS);
+ put_field(" entries after = ", result -> guard_count_top);
+ put_field(" unload status = ", result -> guard_unload_status);
+
+ if (result -> guard_load_status != (ULONG) TX_SUCCESS)
+ {
+ linflexd_puts("FAIL shared-grant guards: the probe module did not load\n");
+ return 1U;
+ }
+
+ if (result -> guard_count_start != 0UL)
+ {
+ linflexd_puts("FAIL shared-grant guards: a fresh instance already held entries\n");
+ return 1U;
+ }
+
+ if (result -> guard_zero_status != (ULONG) TX_SIZE_ERROR)
+ {
+ linflexd_puts("FAIL shared-grant guards: a zero-length grant was not refused as a size error\n");
+ return 1U;
+ }
+
+ if (result -> guard_wrap_status != (ULONG) TX_SIZE_ERROR)
+ {
+ linflexd_puts("FAIL shared-grant guards: a grant running off the top of memory was not refused\n");
+ return 1U;
+ }
+
+ /* The half a status code does not say. A refused grant that still spent an
+ entry would report exactly the same two codes above and quietly cost the
+ caller one of the five, with nothing to show for it. */
+
+ if ((result -> guard_count_zero != 0UL) || (result -> guard_count_wrap != 0UL))
+ {
+ linflexd_puts("FAIL shared-grant guards: a refused grant consumed an entry\n");
+ return 1U;
+ }
+
+ /* And the boundary the guard must not overreach into. */
+
+ if (result -> guard_top_status != (ULONG) TX_SUCCESS)
+ {
+ linflexd_puts("FAIL shared-grant guards: a grant ending at 0xFFFFFFFF was refused\n");
+ return 1U;
+ }
+
+ if (result -> guard_count_top != 1UL)
+ {
+ linflexd_puts("FAIL shared-grant guards: the accepted grant did not consume exactly one entry\n");
+ return 1U;
+ }
+
+ if (result -> guard_unload_status != (ULONG) TX_SUCCESS)
+ {
+ linflexd_puts("FAIL shared-grant guards: the probe module did not unload\n");
+ return 1U;
+ }
+
+ linflexd_puts("PASS shared-grant guards: empty and wrapping grants refused, the boundary grant accepted\n");
+
+ return 0U;
+}
+
+
+/**************************************************************************/
+/* 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 size guards on a grant, which are the manager's business and not
+ the module's. Last, because it wants the pool to itself: every pass
+ above has released its data allocation by now. */
+
+ linflexd_puts("M8 shared-grant size guards\n");
+ run_guard_probes((VOID *) &__module_image_start__);
+ }
+
+ /* ------------------------------------------------------------------
+ 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]);
+ }
+
+ failures += report_and_judge_guards();
+
+ 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 -- an access that hits more
+ than one enabled region takes a translation fault (TRM 8.1), so an overlap
+ 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 00000000..d1a1a489
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/diagnose_module.gdb
@@ -0,0 +1,252 @@
+# Copyright (c) 2026 Eclipse ThreadX contributors
+# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
+#
+# 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=<manager elf> arm-none-eabi-gdb-py -batch -x diagnose_module.gdb
+
+# The probe and the S32DS installation, from the environment when
+# run_module_demo.sh set them and from the same defaults it uses when this
+# script is run by hand. Mirrored in run_module_demo.gdb and
+# diagnose_module.gdb rather than shared, because gdb resolves a relative
+# "source" against the working directory and these are run from anywhere.
+python
+import os
+
+_PROBE_IP = os.environ.get("S32Z280_PROBE", "s32dbg:192.168.50.238")
+
+_S32DS_ROOT = os.environ.get("S32DS_ROOT")
+if not _S32DS_ROOT:
+ for _candidate in ("/opt/nxp/S32DS.3.6.10",
+ os.path.expanduser("~/NXP/S32DS.3.6.10")):
+ if os.path.isdir(_candidate):
+ _S32DS_ROOT = _candidate
+ break
+
+_S32DS_SCRIPT = os.path.join(
+ _S32DS_ROOT or "",
+ "S32DS/tools/S32Debugger/Debugger/scripts/s32z2e2",
+ "s32z2e2_generic_bareboard_all_cores.py")
+
+if not os.path.isfile(_S32DS_SCRIPT):
+ raise gdb.GdbError(
+ "S32DS core script not found at %s\n"
+ "Set S32DS_ROOT to your S32 Design Studio installation."
+ % (_S32DS_SCRIPT or "<unset>"))
+
+gdb.execute("source " + _S32DS_SCRIPT)
+end
+
+py _SOC_NAME = "S32Z280"
+py _CORE_NAME = "R52_0_0"
+py _GDB_SERVER_PORT = 45000
+
+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 at CP15 op1 0; 16 and up have them
+# at op1 1 (TRM 8.4, Table 8-9), though only the PRSELR route is proven on this
+# part and that is what the manager uses for region 16. 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 00000000..9c2fe3b5
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.gdb
@@ -0,0 +1,657 @@
+# Copyright (c) 2026 Eclipse ThreadX contributors
+# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
+#
+# 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=<path to s32z280_module.elf> \
+# S32Z280_MODULE_ELF=<path to s32z280_demo_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.
+
+# The probe and the S32DS installation, from the environment when
+# run_module_demo.sh set them and from the same defaults it uses when this
+# script is run by hand. Mirrored in run_module_demo.gdb and
+# diagnose_module.gdb rather than shared, because gdb resolves a relative
+# "source" against the working directory and these are run from anywhere.
+python
+import os
+
+_PROBE_IP = os.environ.get("S32Z280_PROBE", "s32dbg:192.168.50.238")
+
+_S32DS_ROOT = os.environ.get("S32DS_ROOT")
+if not _S32DS_ROOT:
+ for _candidate in ("/opt/nxp/S32DS.3.6.10",
+ os.path.expanduser("~/NXP/S32DS.3.6.10")):
+ if os.path.isdir(_candidate):
+ _S32DS_ROOT = _candidate
+ break
+
+_S32DS_SCRIPT = os.path.join(
+ _S32DS_ROOT or "",
+ "S32DS/tools/S32Debugger/Debugger/scripts/s32z2e2",
+ "s32z2e2_generic_bareboard_all_cores.py")
+
+if not os.path.isfile(_S32DS_SCRIPT):
+ raise gdb.GdbError(
+ "S32DS core script not found at %s\n"
+ "Set S32DS_ROOT to your S32 Design Studio installation."
+ % (_S32DS_SCRIPT or "<unset>"))
+
+gdb.execute("source " + _S32DS_SCRIPT)
+end
+
+py _SOC_NAME = "S32Z280"
+py _CORE_NAME = "R52_0_0"
+py _GDB_SERVER_PORT = 45000
+
+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
+SUCCESS, SIZE_ERROR = 0x00, 0x05
+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 access hitting both takes a translation fault (TRM 8.1)
+ # rather than merely being 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("")
+print(" ===== shared-grant size guards =====")
+
+# The manager's own record for the fifth module, which is loaded after the four
+# passes and unloaded again without ever being started. Read from memory for the
+# same reason everything above is: the console says what the manager believes,
+# and this says what it actually stored.
+#
+# A grant of no bytes, and one whose inclusive end wraps past 0xFFFFFFFF, both
+# used to describe a region whose limit lay below its base -- and both were
+# programmed, charged an entry and reported as TX_SUCCESS. What is checked here
+# is the pair: the status, and that the entry count did not move. A guard that
+# returned the right code and still spent an entry would look correct on the
+# console and would silently cost an application one of its five.
+try:
+ g = {f: ev("guard_result.guard_%s" % f) for f in
+ ("load_status", "count_start",
+ "zero_status", "count_zero",
+ "wrap_status", "count_wrap",
+ "top_status", "count_top",
+ "unload_status")}
+except gdb.error as e:
+ print(" *** FAIL: guard_result unreadable (%s)" % e)
+ failures += 1
+ g = None
+
+if g is not None:
+ print(" probe module load = 0x%02X (expected 0x%02X)"
+ % (g["load_status"], SUCCESS))
+ print(" entries at start = %d (expected 0)" % g["count_start"])
+ print(" zero length = 0x%02X (expected 0x%02X, TX_SIZE_ERROR), "
+ "entries after = %d" % (g["zero_status"], SIZE_ERROR, g["count_zero"]))
+ print(" wrapping end = 0x%02X (expected 0x%02X, TX_SIZE_ERROR), "
+ "entries after = %d" % (g["wrap_status"], SIZE_ERROR, g["count_wrap"]))
+ print(" ends at 0xFFFFFFFF = 0x%02X (expected 0x%02X, TX_SUCCESS), "
+ "entries after = %d" % (g["top_status"], SUCCESS, g["count_top"]))
+ print(" probe module unload = 0x%02X (expected 0x%02X)"
+ % (g["unload_status"], SUCCESS))
+
+ if g["load_status"] != SUCCESS:
+ print(" *** FAIL: the probe module did not load, so nothing below ran")
+ failures += 1
+ else:
+ if g["count_start"] != 0:
+ print(" *** FAIL: a freshly loaded instance already held shared entries")
+ failures += 1
+ if g["zero_status"] != SIZE_ERROR:
+ print(" *** FAIL: a zero-length grant was not refused as a size error.")
+ print(" Its limit is base - 1, which lies below its base.")
+ failures += 1
+ if g["wrap_status"] != SIZE_ERROR:
+ print(" *** FAIL: a grant running off the top of memory was not refused.")
+ print(" base + length - 1 wraps to a low address.")
+ failures += 1
+ if g["count_zero"] != 0 or g["count_wrap"] != 0:
+ print(" *** FAIL: a refused grant still consumed one of the five entries")
+ failures += 1
+ if g["top_status"] != SUCCESS:
+ print(" *** FAIL: a grant whose last byte is 0xFFFFFFFF was refused.")
+ print(" It needs exactly the room there is; the overflow")
+ print(" guard has overreached into a legal case.")
+ failures += 1
+ elif g["count_top"] != 1:
+ print(" *** FAIL: the accepted grant did not consume exactly one entry")
+ failures += 1
+ if g["unload_status"] != SUCCESS:
+ print(" *** FAIL: the probe module did not unload")
+ failures += 1
+
+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 00000000..bbbdee43
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/tools/run_module_demo.sh
@@ -0,0 +1,187 @@
+#!/bin/bash
+# Copyright (c) 2026 Eclipse ThreadX contributors
+# SPDX-License-Identifier: MIT
+# Portions of this file were generated with AI assistance.
+#
+# 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 [<build directory>]
+#
+# Paths that vary by machine come from the environment: S32DS_ROOT, S32Z280_PROBE
+# and GDB_PY_HOME, each described where it is resolved below. UART and LOG
+# override the console device and the capture file.
+#
+# 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)
+
+# Where the tools live. None of these paths are portable, so each is
+# overridable and each is checked before it is used -- a wrong one otherwise
+# surfaces as a gdb that will not start or an attach that never connects.
+#
+# S32DS_ROOT the S32 Design Studio installation. Tried in order: the
+# variable, a system-wide install, the per-user layout.
+# S32Z280_PROBE the debug probe, as the gdb scripts expect to address it.
+# GDB_PY_HOME the source-built CPython that gdb-py needs; see PYTHONHOME
+# below for why the one inside gdb is not enough.
+if [ -n "${S32DS_ROOT:-}" ]; then
+ R=$S32DS_ROOT
+elif [ -d /opt/nxp/S32DS.3.6.10 ]; then
+ R=/opt/nxp/S32DS.3.6.10
+else
+ R=$HOME/NXP/S32DS.3.6.10
+fi
+
+if [ ! -d "$R" ]; then
+ echo "ERROR: S32 Design Studio not found at $R" >&2
+ echo " Set S32DS_ROOT to your installation, for example:" >&2
+ echo " S32DS_ROOT=/opt/nxp/S32DS.3.6.10 $0 ${1:-}" >&2
+ exit 1
+fi
+
+GDB=$R/S32DS/tools/gdb-arm/arm32-eabi/bin/arm-none-eabi-gdb-py
+GTA=$R/S32DS/tools/S32Debugger/Debugger/Server/gta
+
+for f in "$GDB" "$GTA/gta"; do
+ if [ ! -x "$f" ]; then
+ echo "ERROR: $f is missing or not executable." >&2
+ echo " S32DS_ROOT=$R does not look like an S32 Design Studio install." >&2
+ exit 1
+ fi
+done
+
+# Resolved here and exported, so the gdb scripts use these values rather than
+# resolving the same defaults again and possibly differently.
+export S32DS_ROOT=$R
+export S32Z280_PROBE=${S32Z280_PROBE:-s32dbg:192.168.50.238}
+
+echo "S32DS_ROOT = $R"
+echo "S32Z280_PROBE = $S32Z280_PROBE"
+
+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=${GDB_PY_HOME:-$HOME/toolchains/py310-src}
+if [ ! -d "$P/lib/python3.10" ]; then
+ echo "ERROR: no source-built CPython 3.10 at $P" >&2
+ echo " gdb-py's embedded interpreter loads _struct, _socket and" >&2
+ echo " _ctypes as separate .so files and needs a real install to" >&2
+ echo " find them. Set GDB_PY_HOME to one." >&2
+ exit 1
+fi
+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 00000000..19bd8931
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/example_build/s32z280_evb/txm_module_preamble.S
@@ -0,0 +1,151 @@
+@/***************************************************************************
+@ * 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
+@
+@ Both option bits are REQUIRED here, unlike on every other module port, and a
+@ preamble that carries either alone is refused by the loader with
+@ TXM_MODULE_INVALID_PROPERTIES. On PMSAv8-R that is not a policy: the port
+@ programs a module's regions only when both bits are set, the scheduler closes
+@ the kernel's window over module memory before it dispatches a module thread,
+@ and there is no background region to fall back on -- so a module that asked
+@ for user mode without protection would have no mapping at all rather than a
+@ weaker one. See TXM_MODULE_MANAGER_REQUIRED_OPTIONS in txm_module_port.h.
+@
+@ 0x00000004 TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS
+@
+@ is supported and optional. A module that does not ask for it simply never has
+@ a shared region granted to it.
+
+ .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 00000000..8f099300
--- /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
+ **************************************************************************/
+// Portions of this file were generated with AI assistance.
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** ThreadX Component */
+/** */
+/** Port Specific */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+
+/**************************************************************************/
+/* */
+/* PORT SPECIFIC C INFORMATION RELEASE */
+/* */
+/* tx_port.h Cortex-R52/GNU */
+/* 6.5.2 */
+/* */
+/* AUTHOR */
+/* */
+/* Frédéric Desbiens, Eclipse Foundation */
+/* */
+/* Derived from the Cortex-R5/GNU port originally written by */
+/* William E. Lamie, Microsoft Corporation. */
+/* */
+/* DESCRIPTION */
+/* */
+/* This file contains data type definitions that make the ThreadX */
+/* real-time kernel function identically on a variety of different */
+/* processor architectures. For example, the size or number of bits */
+/* in an "int" data type vary between microprocessor architectures and */
+/* even C compilers for the same microprocessor. ThreadX does not */
+/* directly use native C data types. Instead, ThreadX creates its */
+/* own special types that can be mapped to actual data types by this */
+/* file to guarantee consistency in the interface and functionality. */
+/* */
+/**************************************************************************/
+
+#ifndef TX_PORT_H
+#define TX_PORT_H
+
+
+/* Determine if the optional ThreadX user define file should be used. */
+
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+
+
+/* Yes, include the user defines in tx_user.h. The defines in this file may
+ alternately be defined on the command line. */
+
+#include "tx_user.h"
+#endif
+
+
+/* Define compiler library include files. */
+
+#include <stdlib.h>
+#include <string.h>
+
+
+/* Define ThreadX basic types for this port. */
+
+#define VOID void
+typedef char CHAR;
+typedef unsigned char UCHAR;
+typedef int INT;
+typedef unsigned int UINT;
+typedef long LONG;
+typedef unsigned long ULONG;
+typedef short SHORT;
+typedef unsigned short USHORT;
+
+
+/* Define the priority levels for ThreadX. Legal values range
+ from 32 to 1024 and MUST be evenly divisible by 32. */
+
+#ifndef TX_MAX_PRIORITIES
+#define TX_MAX_PRIORITIES 32
+#endif
+
+
+/* Define the minimum stack for a ThreadX thread on this processor. If the size supplied during
+ thread creation is less than this value, the thread create call will return an error. */
+
+#ifndef TX_MINIMUM_STACK
+#define TX_MINIMUM_STACK 200 /* Minimum stack size for this port */
+#endif
+
+
+/* Define the system timer thread's default stack size and priority. These are only applicable
+ if TX_TIMER_PROCESS_IN_ISR is not defined. */
+
+#ifndef TX_TIMER_THREAD_STACK_SIZE
+#define TX_TIMER_THREAD_STACK_SIZE 1024 /* Default timer thread stack size */
+#endif
+
+#ifndef TX_TIMER_THREAD_PRIORITY
+#define TX_TIMER_THREAD_PRIORITY 0 /* Default timer thread priority */
+#endif
+
+
+/* Define various constants for the ThreadX ARM port. */
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+#define TX_INT_DISABLE 0xC0 /* Disable IRQ & FIQ interrupts */
+#else
+#define TX_INT_DISABLE 0x80 /* Disable IRQ interrupts */
+#endif
+#define TX_INT_ENABLE 0x00 /* Enable IRQ interrupts */
+
+
+/* Define the clock source for trace event entry time stamp. The following two item are port specific.
+ For example, if the time source is at the address 0x0a800024 and is 16-bits in size, the clock
+ source constants would be:
+
+#define TX_TRACE_TIME_SOURCE *((ULONG *) 0x0a800024)
+#define TX_TRACE_TIME_MASK 0x0000FFFFUL
+
+*/
+
+#ifndef TX_TRACE_TIME_SOURCE
+#define TX_TRACE_TIME_SOURCE ++_tx_trace_simulated_time
+#endif
+#ifndef TX_TRACE_TIME_MASK
+#define TX_TRACE_TIME_MASK 0xFFFFFFFFUL
+#endif
+
+
+/* Define the port specific options for the _tx_build_options variable. This variable indicates
+ how the ThreadX library was built. */
+
+#ifdef TX_ENABLE_FIQ_SUPPORT
+#define TX_FIQ_ENABLED 1
+#else
+#define TX_FIQ_ENABLED 0
+#endif
+
+#ifdef TX_ENABLE_IRQ_NESTING
+#define TX_IRQ_NESTING_ENABLED 2
+#else
+#define TX_IRQ_NESTING_ENABLED 0
+#endif
+
+#ifdef TX_ENABLE_FIQ_NESTING
+#define TX_FIQ_NESTING_ENABLED 4
+#else
+#define TX_FIQ_NESTING_ENABLED 0
+#endif
+
+#define TX_PORT_SPECIFIC_BUILD_OPTIONS TX_FIQ_ENABLED | TX_IRQ_NESTING_ENABLED | TX_FIQ_NESTING_ENABLED
+
+
+/* Define the in-line initialization constant so that modules with in-line
+ initialization capabilities can prevent their initialization from being
+ a function call. */
+
+#define TX_INLINE_INITIALIZATION
+
+
+/* Determine whether or not stack checking is enabled. By default, ThreadX stack checking is
+ disabled. When the following is defined, ThreadX thread stack checking is enabled. If stack
+ checking is enabled (TX_ENABLE_STACK_CHECKING is defined), the TX_DISABLE_STACK_FILLING
+ define is negated, thereby forcing the stack fill which is necessary for the stack checking
+ logic. */
+
+#ifdef TX_ENABLE_STACK_CHECKING
+#undef TX_DISABLE_STACK_FILLING
+#endif
+
+
+/* Define the TX_THREAD control block extensions for this port. The main reason
+ for the multiple macros is so that backward compatibility can be maintained with
+ existing ThreadX kernel awareness modules. */
+
+/* TX_THREAD_EXTENSION_2 carries the per-thread VFP enable flag used by the
+ lazy floating-point save and restore in tx_thread_schedule.S,
+ tx_thread_system_return.S and tx_thread_context_restore.S.
+
+ It is defined UNCONDITIONALLY, not under TX_ENABLE_VFP_SUPPORT, and that is
+ deliberate: the assembly reaches this field through a hard-coded structure
+ offset, so making the field conditional would move every following member
+ between build configurations and leave the offset correct in only one of
+ them. Keeping it always present makes the layout independent of the
+ floating-point build options. The offset is checked at compile time in
+ tx_port_offset_check.c, which turns a wrong offset into a build failure
+ instead of silent corruption of an unrelated thread field. */
+
+#define TX_THREAD_EXTENSION_0
+#define TX_THREAD_EXTENSION_1
+/* 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.2.202603 *";
+#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 00000000..1d71d40a
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/inc/txm_module_port.h
@@ -0,0 +1,491 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+// Portions of this file were generated with AI assistance.
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** 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.
+
+ TXM_MODULE_SHARED_EXTERNAL_MEMORY_ACCESS is supported and optional: a module
+ that does not ask for it simply never has a shared region granted to it, and
+ everything else about it works.
+
+ TXM_MODULE_USER_MODE and TXM_MODULE_MEMORY_PROTECTION are supported and
+ REQUIRED, together, which every other module port in the tree leaves at 0.
+ The divergence is a property of PMSAv8-R rather than a policy choice, and the
+ alternative is not a module that runs unprotected -- it is a module that
+ cannot run at all:
+
+ - TXM_MODULE_MANAGER_MODULE_SETUP below programs the eight-entry region
+ table only when both bits are set, so for any other accepted combination
+ the table stays as the load memset it, all zeros.
+
+ - tx_thread_schedule.S closes the kernel's window over the module area
+ before it dispatches a module thread, because PMSAv8-R has no region
+ priority and the window covers the same memory the module's own regions
+ do. A zeroed table therefore leaves nothing enabled over module memory.
+
+ - There is no background region to fall back on. Per the Cortex-R52 TRM
+ section 8.2.1, the EL1-controlled background region is used only when the
+ MPU is disabled or when SCTLR.BR is set, and accesses from EL0 are
+ faulted whenever the MPU is enabled. Neither example board support
+ package sets SCTLR.BR, and a board that did would still not help a
+ User-mode module.
+
+ So a module asking for user mode without protection, or for neither, is
+ accepted by the portable loader, started, and aborts on the fetch of its own
+ first instruction. Requiring both bits turns that into
+ TXM_MODULE_INVALID_PROPERTIES from txm_module_manager_in_place_load, at the
+ point where the property word can still be corrected.
+
+ Supporting them for real -- a privileged module mapped through the kernel's
+ window, or an unprotected User-mode module -- needs a second mapping path in
+ the scheduler and its own tests. Until that exists this is the honest
+ contract rather than a restriction. */
+
+#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 (TXM_MODULE_USER_MODE | TXM_MODULE_MEMORY_PROTECTION)
+
+
+/* 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
+ at CP15 op1 0, encoded CRn c6, CRm 0b1rrr where rrr is region_number[3:1],
+ opc2 0 and 1 for an even region and 4 and 5 for an odd one. Regions 16
+ through 24 have direct encodings as well, at op1 1 with the same CRm and opc2
+ rule (8.4, Table 8-9), so no region on this core is reachable only through
+ PRSELR. PRSELR remains the indirect route: it selects a region for the
+ unnumbered PRBAR and PRLAR view and needs an ISB before those can be written,
+ per region.
+
+ The TRM contradicts itself on this and the disagreement is worth recording,
+ so that a reader who checks only one half does not correct it back. The
+ register descriptions at 3.3.85 and 3.3.86 say flatly "Direct access is
+ provided to PRBAR0 to PRBAR15" and give only the op1 0 form; 8.4 and Table
+ 8-9 give both forms. Table 8-9 is itself imprecise -- it prints opc2 0 for
+ the even limit registers where the prose of 8.4 says 1 -- so the op1 1
+ encoding has to be proven on the part before anything relies on it.
+
+ What 8 through 15 buys is that all eight regions sit in the op1 0 range, the
+ form already exercised on this part, and that they are contiguous, so a
+ module switch writes them as one unrolled block with a single barrier pair.
+ 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.
+
+ 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 move it, and
+ moving it past 15 changes the op1 the scheduler's MCR encodings carry. */
+
+/* The kernel's window over the module area. tx_thread_schedule.S carries the
+ same number as a .equ, because the assembler cannot include this header, and
+ txm_module_manager_offset_check.c checks the two spellings against each
+ other. Moving it past 15 changes the op1 the scheduler's MCR encodings
+ carry, which is what the first assertion in that file is about. */
+
+#define TXM_MODULE_MPU_WINDOW_REGION 16
+
+#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, either by a
+ region's own direct encoding or through PRSELR, 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, reached either by the
+ region's own direct encoding or through 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.
+
+ TXM_MODULE_MANAGER_REQUIRED_OPTIONS makes both tests below true for every
+ module the loader accepts, so the nesting is redundant today. It is kept
+ because it is the only thing that decides whether the region table is
+ programmed: a future port that learned to map a privileged module would relax
+ REQUIRED_OPTIONS, and this macro is where the second path would go.
+*/
+#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 00000000..8d2b64d1
--- /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 00000000..4f01b7a8
--- /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 00000000..d6d7f63f
--- /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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** 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 00000000..26443700
--- /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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .global _tx_thread_system_state
+ .global _tx_thread_current_ptr
+ .global _tx_irq_processing_return
+ .global _tx_execution_isr_enter
+@
+@
+@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_context_save
+@ since it will never be called 16-bit mode. */
+@
+ .arm
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_context_save Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function saves the context of an executing thread in the */
+@/* beginning of interrupt processing. The function also ensures that */
+@/* the system stack is used upon return to the calling ISR. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* None */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ISRs */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_context_save(VOID)
+@{
+ .global _tx_thread_context_save
+ .type _tx_thread_context_save,function
+_tx_thread_context_save:
+@
+@ /* Upon entry to this routine, it is assumed that IRQ interrupts are locked
+@ out, we are in IRQ mode, and all registers are intact. */
+@
+@ /* Check for a nested interrupt condition. */
+@ if (_tx_thread_system_state++)
+@ {
+@
+ STMDB sp!, {r0-r3} @ Save some working registers
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable FIQ interrupts
+#endif
+ LDR r3, =_tx_thread_system_state @ Pickup address of system state variable
+ LDR r2, [r3] @ Pickup system state
+ CMP r2, #0 @ Is this the first interrupt?
+ BEQ __tx_thread_not_nested_save @ Yes, not a nested context save
+@
+@ /* Nested interrupt condition. */
+@
+ ADD r2, r2, #1 @ Increment the interrupt counter
+ STR r2, [r3] @ Store it back in the variable
+@
+@ /* Save the rest of the scratch registers on the stack and return to the
+@ calling ISR. */
+@
+ MRS r0, SPSR @ Pickup saved SPSR
+ SUB lr, lr, #4 @ Adjust point of interrupt
+ STMDB sp!, {r0, r10, r12, lr} @ Store other registers
+@
+@ /* Return to the ISR. */
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ B __tx_irq_processing_return @ Continue IRQ processing
+@
+__tx_thread_not_nested_save:
+@ }
+@
+@ /* Otherwise, not nested, check to see if a thread was running. */
+@ else if (_tx_thread_current_ptr)
+@ {
+@
+ ADD r2, r2, #1 @ Increment the interrupt counter
+ STR r2, [r3] @ Store it back in the variable
+ LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr
+ LDR r0, [r1] @ Pickup current thread pointer
+ CMP r0, #0 @ Is it NULL?
+ BEQ __tx_thread_idle_system_save @ If so, interrupt occurred in
+ @ scheduling loop - nothing needs saving!
+@
+@ /* Save minimal context of interrupted thread. */
+@
+ MRS r2, SPSR @ Pickup saved SPSR
+ SUB lr, lr, #4 @ Adjust point of interrupt
+ STMDB sp!, {r2, r10, r12, lr} @ Store other registers
+@
+@ /* Save the current stack pointer in the thread's control block. */
+@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp;
+@
+@ /* Switch to the system stack. */
+@ sp = _tx_thread_system_stack_ptr@
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ B __tx_irq_processing_return @ Continue IRQ processing
+@
+@ }
+@ else
+@ {
+@
+__tx_thread_idle_system_save:
+@
+@ /* Interrupt occurred in the scheduling loop. */
+@
+@ /* Not much to do here, just adjust the stack pointer, and return to IRQ
+@ processing. */
+@
+ MOV r10, #0 @ Clear stack limit
+
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the ISR enter function to indicate an ISR is executing. */
+@
+ PUSH {lr} @ Save ISR lr
+ BL _tx_execution_isr_enter @ Call the ISR enter function
+ POP {lr} @ Recover ISR lr
+#endif
+
+ ADD sp, sp, #16 @ Recover saved registers
+ B __tx_irq_processing_return @ Continue IRQ processing
+@
+@ }
+@}
+
+
+
diff --git a/ports_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 00000000..e10d7c23
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/module_manager/src/tx_thread_schedule.S
@@ -0,0 +1,504 @@
+@/***************************************************************************
+@ * Copyright (c) 2024 Microsoft Corporation
+@ * Copyright (c) 2026 Eclipse ThreadX contributors
+@ *
+@ * This program and the accompanying materials are made available under the
+@ * terms of the MIT License which is available at
+@ * https://opensource.org/licenses/MIT.
+@ *
+@ * SPDX-License-Identifier: MIT
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+@ 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_PROPERTY_FLAGS, 0x0C
+ .equ TXM_INSTANCE_MPU_REGISTERS, 0x64
+
+@ The property bits that say a module's region table was programmed:
+@ TXM_MODULE_USER_MODE and TXM_MODULE_MEMORY_PROTECTION, which
+@ TXM_MODULE_MANAGER_MODULE_SETUP requires together before it calls
+@ _txm_module_manager_mm_register_setup. The instance carries the whole
+@ preamble word, compiler field included, so the two bits are masked out rather
+@ than compared against.
+
+ .equ TXM_MODULE_PROTECTED_MASK, 0x03
+
+@ 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
+@
+@ /* Ask the PROPERTY FLAGS whether this module has a region table, not the
+@ data start. The data start is set for every module the loader accepts,
+@ protected or not, so testing it says only "a module was loaded" -- and
+@ the branch not taken here is the branch that closes the kernel's window
+@ over module memory. Programming a zeroed table and then closing the
+@ window leaves an unprotected module with no mapping at all rather than
+@ with a weaker one.
+@
+@ TXM_MODULE_MANAGER_REQUIRED_OPTIONS makes this test redundant: the loader
+@ refuses anything that does not have both bits. It is written anyway so
+@ that the scheduler depends on a field it can see rather than on a loader
+@ invariant it cannot, and so that relaxing REQUIRED_OPTIONS degrades to
+@ "the module runs under the kernel's window" instead of to a fault on its
+@ first instruction. */
+@
+ LDR r2, [r1, #TXM_INSTANCE_PROPERTY_FLAGS] @ Pickup module property flags
+ AND r2, r2, #TXM_MODULE_PROTECTED_MASK @ Keep user mode and protection
+ CMP r2, #TXM_MODULE_PROTECTED_MASK @ Both asked for?
+ BNE __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.
+@
+@ ON THE ORDER, which is worth stating because it looks wrong. These eight
+@ regions describe memory the kernel's window over the module area also
+@ describes, and that window is not closed until after the last write below.
+@ Between the two, the programmed map holds an overlap -- the thing the
+@ whole design exists to avoid, and an access hitting two enabled regions
+@ is an architected translation fault on this core (TRM 8.1), not something
+@ left to the implementation.
+@
+@ So the window has to be bounded exactly rather than waved at. It is not
+@ shortened by the barriers: the ISB that PRSELR needs before the window's
+@ PRLAR can be written comes first, so from that ISB to the closing ISB the
+@ eight regions and the window are both architecturally in force. What
+@ makes the overlap harmless is that no access inside it can hit both.
+@
+@ - Every instruction fetched from the first MCR below to the closing ISB
+@ comes from this function, which lives in the kernel's own code region
+@ and not in the module area.
+@ - The only data read is the region table walked by the LDMIAs, which
+@ lives in the TXM_MODULE_INSTANCE the application supplied.
+@ - The thread's stack is module memory, and nothing here touches it: sp
+@ was loaded above, and the first access through it is the frame pop
+@ after __tx_module_regions_done, past the closing ISB.
+@ - IRQ, and FIQ where it is built in, have been masked since the CPSID
+@ taken once a thread was picked, so no exception entry can insert an
+@ access of its own.
+@
+@ Speculation does not reopen it either: the MPU stops a speculative access
+@ from reaching the bus but raises no translation or permission fault for
+@ it (TRM 8.1). Only committed accesses fault, and the list above is all of
+@ them. The barrier pair at the end is therefore placed for the transition
+@ as a whole rather than to shorten this window.
+@
+@ That does put one requirement on the application: a TXM_MODULE_INSTANCE
+@ must not live inside the module area. Both examples keep theirs in .bss,
+@ which is inside the kernel's data region and outside every region a
+@ module is given. */
+@
+ 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:
+@ an access hitting more than one enabled region takes a translation fault
+@ (TRM 8.1). The abort seen on this part is that architected result, not a
+@ part-specific resolution of unpredictable behaviour. A module thread
+@ reaches its own memory through the regions above and needs nothing else.
+@
+@ Region 16 is programmed through PRSELR here, and not because it has no
+@ direct encoding: TRM 8.4 and Table 8-9 give PRBAR16 and PRLAR16 at CP15
+@ op1 1, CRm c8, opc2 0 and 1. The register descriptions at 3.3.85 and
+@ 3.3.86 contradict that and say direct access stops at PRBAR15. The
+@ selector sequence is the one proven on silicon, so it stays until the
+@ op1 1 form has been measured on both targets; do not "correct" this back
+@ to the 3.3.85 claim. Clearing PRLAR is what disables the region. */
+@
+ 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 00000000..e45805f7
--- /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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** 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 00000000..ae0eac7d
--- /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
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .arm
+@
+@
+ .global _tx_thread_current_ptr
+ .global _tx_timer_time_slice
+ .global _tx_thread_schedule
+ .global _tx_execution_thread_exit
+@
+@
+@
+@/* Define the 16-bit Thumb mode veneer for _tx_thread_system_return for
+@ applications calling this function from to 16-bit Thumb mode. */
+@
+ .text
+ .align 2
+ .global $_tx_thread_system_return
+ .type $_tx_thread_system_return,function
+$_tx_thread_system_return:
+ .thumb
+ BX pc @ Switch to 32-bit mode
+ NOP @
+ .arm
+ STMFD sp!, {lr} @ Save return address
+ BL _tx_thread_system_return @ Call _tx_thread_system_return function
+ LDMFD sp!, {lr} @ Recover saved return address
+ BX lr @ Return to 16-bit caller
+@
+@
+ .text
+ .align 2
+@/**************************************************************************/
+@/* */
+@/* FUNCTION RELEASE */
+@/* */
+@/* _tx_thread_system_return Cortex-R52/GNU */
+@/* 6.5.2 */
+@/* AUTHOR */
+@/* */
+@/* Frédéric Desbiens, Eclipse Foundation */
+@/* */
+@/* Derived from the Cortex-R5/GNU port originally written by */
+@/* William E. Lamie, Microsoft Corporation. */
+@/* */
+@/* DESCRIPTION */
+@/* */
+@/* This function is target processor specific. It is used to transfer */
+@/* control from a thread back to the ThreadX system. Only a */
+@/* minimal context is saved since the compiler assumes temp registers */
+@/* are going to get slicked by a function call anyway. */
+@/* */
+@/* INPUT */
+@/* */
+@/* None */
+@/* */
+@/* OUTPUT */
+@/* */
+@/* None */
+@/* */
+@/* CALLS */
+@/* */
+@/* _tx_thread_schedule Thread scheduling loop */
+@/* */
+@/* CALLED BY */
+@/* */
+@/* ThreadX components */
+@/* */
+@/**************************************************************************/
+@VOID _tx_thread_system_return(VOID)
+@{
+ .global _tx_thread_system_return
+ .type _tx_thread_system_return,function
+_tx_thread_system_return:
+@
+@ /* Lockout interrupts. */
+@
+ MRS r1, CPSR @ Pickup the CPSR
+#ifdef TX_ENABLE_FIQ_SUPPORT
+ CPSID if @ Disable IRQ and FIQ interrupts
+#else
+ CPSID i @ Disable IRQ interrupts
+#endif
+@ /* Save minimal context on the stack. */
+@
+ STMDB sp!, {r4-r11, lr} @ Save minimal context
+ LDR r5, =_tx_thread_current_ptr @ Pickup address of current ptr
+ LDR r6, [r5, #0] @ Pickup current thread pointer
+@
+#ifdef TX_ENABLE_VFP_SUPPORT
+ LDR r0, [r6, #144] @ Pickup the VFP enabled flag
+ CMP r0, #0 @ Is the VFP enabled?
+ BEQ _tx_skip_solicited_vfp_save @ No, skip VFP solicited save
+ VMRS r4, FPSCR @ Pickup the FPSCR
+ STR r4, [sp, #-4]! @ Save FPSCR
+ VSTMDB sp!, {D8-D15} @ Save D8-D15
+_tx_skip_solicited_vfp_save:
+#endif
+@
+ MOV r0, #0 @ Build a solicited stack type
+ STMDB sp!, {r0-r1} @ Save type and CPSR
+@
+@
+#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
+@
+@ /* Call the thread exit function to indicate the thread is no longer executing. */
+@
+ BL _tx_execution_thread_exit @ Call the thread exit function
+#endif
+@
+ LDR r2, =_tx_timer_time_slice @ Pickup address of time slice
+ LDR r1, [r2, #0] @ Pickup current time slice
+@
+@ /* Save current stack and switch to system stack. */
+@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp;
+@ sp = _tx_thread_system_stack_ptr;
+@
+ STR sp, [r6, #8] @ Save thread stack pointer
+@
+@ /* Determine if the time-slice is active. */
+@ if (_tx_timer_time_slice)
+@ {
+@
+ MOV r4, #0 @ Build clear value
+ CMP r1, #0 @ Is a time-slice active?
+ BEQ __tx_thread_dont_save_ts @ No, don't save the time-slice
+@
+@ /* Save time-slice for the thread and clear the current time-slice. */
+@ _tx_thread_current_ptr -> tx_thread_time_slice = _tx_timer_time_slice;
+@ _tx_timer_time_slice = 0;
+@
+ STR r4, [r2, #0] @ Clear time-slice
+ STR r1, [r6, #24] @ Save current time-slice
+@
+@ }
+__tx_thread_dont_save_ts:
+@
+@ /* Clear the current thread pointer. */
+@ _tx_thread_current_ptr = TX_NULL;
+@
+ STR r4, [r5, #0] @ Clear current thread pointer
+ B _tx_thread_schedule @ Jump to scheduler!
+@
+@}
+
diff --git a/ports_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 00000000..e06225f6
--- /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
+ **************************************************************************/
+
+// Portions of this file were generated with AI assistance.
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** 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 00000000..f60f1ad7
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_external_memory_enable.c
@@ -0,0 +1,228 @@
+/***************************************************************************
+ * 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
+ **************************************************************************/
+
+// Portions of this file were generated with AI assistance.
+
+
+/**************************************************************************/
+/**************************************************************************/
+/** */
+/** 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. */
+/* */
+/* The length must describe at least one byte and must not run off the */
+/* top of the address space; either is TX_SIZE_ERROR. It need not be a */
+/* multiple of the granule, but the hardware grant is rounded up to */
+/* one -- see the limit-address comment below for what the caller is */
+/* therefore agreeing to hand over. */
+/* */
+/* 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);
+ }
+
+ /* Reject a grant that describes no memory, and one whose inclusive end
+ address runs off the top of the address space. Both produce a limit
+ BELOW the base: a zero length computes address - 1 outright, and a
+ wrapping end computes some low address. Either would be programmed into
+ PRLAR, returned as TX_SUCCESS, and would consume one of the five shared
+ entries for a region the hardware cannot honour.
+
+ Checked here rather than at the limit computation because a refused grant
+ must cost the caller nothing -- no register written and no entry spent --
+ and the entry count is incremented further down.
+
+ The wrap test is written against the room left above the base rather than
+ as (address + length) < address, which would have to commit the overflow
+ in order to detect it. length is known non-zero by then, so length - 1 is
+ the number of bytes above the base the grant needs, and a grant ending
+ exactly at the top of memory needs precisely the room there is and is
+ accepted.
+
+ TX_SIZE_ERROR for both: the portable API already defines it as the size
+ fault, so neither case needs a new return code for callers to handle. */
+
+ if ((length == 0UL) || ((length - 1UL) > (((ULONG) ~0UL) - address)))
+ {
+ /* Release the protection mutex. */
+ _tx_mutex_put(&_txm_module_manager_mutex);
+
+ /* Return size error. */
+ return(TX_SIZE_ERROR);
+ }
+
+ /* At this point, we have a valid address and length. 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.
+
+ What the mask does NOT do is round the grant down. PRLAR describes an
+ inclusive end of LIMIT:0x3F, so the smallest region the hardware can
+ express is one 64-byte granule and the effective grant is the requested
+ range rounded UP to the next granule boundary: a one-byte grant hands the
+ module 64 bytes, a 65-byte grant hands it 128. The caller owns every byte
+ of that rounded range and must intend the module to have it -- there is no
+ length this function can accept that gives the module less.
+
+ The length recorded below, and therefore the length
+ _txm_module_manager_inside_data_check() validates kernel-call pointers
+ against, is the UNROUNDED one the caller asked for. So a module can reach
+ the surplus bytes directly while a kernel call carrying a pointer into
+ them is refused. That asymmetry is deliberate and inherited: every other
+ module port computes the same rounded-up grant, and one that did not would
+ reject code moved from an Armv8-M part. Pass a granule multiple and the
+ two lengths agree. */
+ 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 00000000..48090ff3
--- /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 00000000..62f287e0
--- /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 */
+/** */
+/**************************************************************************/
+/**************************************************************************/
+
+// Portions of this file were generated with AI assistance.
+
+#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 00000000..a8619687
--- /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 00000000..2a3fe679
--- /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 00000000..09ba7420
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c
@@ -0,0 +1,261 @@
+/***************************************************************************
+ * 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 <stddef.h>
+#include "tx_api.h"
+#include "txm_module.h"
+
+/* The board's MPU header is not on the include path when this file is compiled
+ as a port source, and it must not have to be: the region number this file
+ asserts on is the port's, spelled in txm_module_port.h. Where a board header
+ is present the two are still checked against each other below, so a board
+ support package cannot quietly disagree with the scheduler. */
+#if defined(__has_include)
+# if __has_include("mpu.h")
+# include "mpu.h"
+# define TXM_R52_BOARD_MPU_H_PRESENT
+# endif
+#endif
+
+/* These must match the .equ values at the top of tx_thread_schedule.S. */
+
+#define TXM_THREAD_MODULE_INSTANCE 0x94
+#define TXM_INSTANCE_PROPERTY_FLAGS 0x0C
+#define TXM_INSTANCE_MPU_REGISTERS 0x64
+
+/* The offset the scheduler no longer reads, kept as its own constant because the
+ assert below still pins it. */
+
+#define TXM_INSTANCE_DATA_START 0x2C
+
+_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");
+
+/* The field the scheduler decides protection on. Reading a word that happens to
+ have both low bits set would program a region table for a module that has
+ none; reading one that does not would send a protected module down the path
+ that closes the kernel's window and enables nothing in its place. Neither
+ shows up as a wrong number anywhere -- one is a fault in a module that asked
+ correctly, the other is the silent absence of one. */
+
+_Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_property_flags)
+ == TXM_INSTANCE_PROPERTY_FLAGS,
+ "tx_thread_schedule.S TXM_INSTANCE_PROPERTY_FLAGS no longer matches "
+ "TXM_MODULE_INSTANCE; the protection check would test the wrong field");
+
+/* The data start, which the assembly stopped reading when the protection check
+ moved to the property flags. Kept, because the two offsets pin opposite ends
+ of the same region of TXM_MODULE_INSTANCE -- property flags is its fourth word
+ and data start its twelfth -- and a reordering that moved either without
+ moving the other is exactly what this file exists to catch. It costs no
+ code. */
+
+_Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_data_start)
+ == TXM_INSTANCE_DATA_START,
+ "TXM_MODULE_INSTANCE has been reordered around "
+ "txm_module_instance_data_start; check every offset in this file "
+ "against tx_thread_schedule.S");
+
+_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 CP15 op1 0 direct-access range. Regions 16
+ and above are directly addressable too, but at op1 1 (TRM 8.4, Table 8-9);
+ the scheduler emits op1 0 encodings for all eight, so a block crossing 15
+ would be programming the wrong registers rather than merely being out of
+ reach. */
+
+_Static_assert((TXM_MODULE_MPU_FIRST_REGION + TXM_MODULE_MPU_TOTAL_ENTRIES) <= 16,
+ "the module region block extends past region 15; the unrolled "
+ "sequence in tx_thread_schedule.S emits CP15 op1 0 encodings, "
+ "which do not address regions 16 and above");
+
+/* 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
+ 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(TXM_MODULE_MPU_WINDOW_REGION == 16,
+ "tx_thread_schedule.S writes region 16 as the module window; "
+ "change MPU_MODULE_WINDOW_REGION there to match");
+
+_Static_assert(TXM_MODULE_MPU_WINDOW_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");
+
+/* A board support package naming a different region than the scheduler uses
+ would program its window somewhere the scheduler never enables. */
+#ifdef TXM_R52_BOARD_MPU_H_PRESENT
+_Static_assert(MPU_MODULE_LOAD_REGION == TXM_MODULE_MPU_WINDOW_REGION,
+ "the board's MPU_MODULE_LOAD_REGION disagrees with the port's "
+ "TXM_MODULE_MPU_WINDOW_REGION");
+#endif
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 00000000..0689ffba
--- /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 00000000..1d1d711a
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_thread_stack_build.S
@@ -0,0 +1,251 @@
+@/***************************************************************************
+@ * Copyright (c) 2024 Microsoft Corporation
+@ * Copyright (c) 2026 Eclipse ThreadX contributors
+@ *
+@ * This program and the accompanying materials are made available under the
+@ * terms of the MIT License which is available at
+@ * https://opensource.org/licenses/MIT.
+@ *
+@ * SPDX-License-Identifier: MIT
+@ **************************************************************************/
+@ Portions of this file were generated with AI assistance.
+@
+@
+@/**************************************************************************/
+@/**************************************************************************/
+@/** */
+@/** ThreadX Component */
+@/** */
+@/** Thread */
+@/** */
+@/**************************************************************************/
+@/**************************************************************************/
+#ifdef TX_INCLUDE_USER_DEFINE_FILE
+#include "tx_user.h"
+#endif
+
+ .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 r3, CPSR @ Pickup CPSR
+ BIC r3, r3, #CPSR_MASK @ Mask mode bits of CPSR
+@
+@ The state bit comes from bit 0 of the entry pointer, so the test has to
+@ happen while r1 still holds it -- CPSR is staged in r3 and the user mode
+@ flag is read into r1 afterwards, the same register allocation the
+@ Cortex-R4 module port uses. The pointer stored as the initial PC above
+@ is the unmasked one on purpose: on the A and R profiles the saved T bit
+@ selects the state on exception return and the low bit of the restored PC
+@ is ignored, so masking it would be pointless and clearing it would break
+@ an interworking entry.
+@
+ TST r1, #1 @ Was a Thumb entry point requested?
+ ORRNE r3, r3, #THUMB_MASK @ Yes, start in Thumb state
+@
+ LDR r1, [r0, #THREAD_MODULE_USER_MODE] @ Pickup the requested user mode
+ TST r1, #1
+ ORREQ r3, r3, #SYS_MODE_BITS @ Clear: privileged, System mode
+ ORRNE r3, r3, #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 00000000..afc307b5
--- /dev/null
+++ b/ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_user_mode_entry.S
@@ -0,0 +1,133 @@
+@/***************************************************************************
+@ * 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. An access that hits more than one enabled region takes
+@ a translation fault (TRM 8.1); the abort is specified, not a part-specific
+@ resolution of unpredictable behaviour. 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