Added a ThreadX module manager port for the Cortex-R52 (#639)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
gcc_check / gnu (push) Canceled after 0s
r52_fvp / r52 (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s

Added the first GNU ThreadX module port for an Arm R-profile core.

  The port combines the PMSAv8-R MPU model from Cortex-M33 with the AArch32
  privilege and processor-mode handling from Cortex-R4. It provides the complete
  module path: headers, manager sources, scheduler integration, user-mode entry,
  SVC dispatch, data- and prefetch-abort capture, fault notification, relocatable
  module loading, and shared-memory regions.

  Module isolation uses eight MPU regions (8–15) with a 64-byte granule. The
  scheduler replaces those regions on each module switch and manages a separate
  privileged loading window in region 16. Assembly-visible structure offsets and
  region-layout assumptions are checked at build time.

  Added independent module demonstrations for the S32Z280-594EVB and Armv8-R AEM
  FVP. The automated FVP regressions cover:

  - Loading the same position-independent module at different addresses
  - User-mode data and instruction access violations
  - Fault capture and notification for both abort types
  - Shared-region access, alignment, exhaustion, empty-size, and overflow handling
  - Required module-property combinations
  - The GCC CLZ-based priority search

  The port requires user mode and memory protection together, at least 17 EL1 MPU
  regions, and currently validates A32 modules; Thumb module execution remains
  unvalidated.

  Validated with GNU Arm 14.3.1. The FVP suites pass 8/8 in the default
  configuration and 11/11 with hard-float, FIQ, and interrupt nesting enabled.
  The S32Z280 images build cleanly, and the module isolation and relocation paths
  were exercised on S32Z280 silicon during development.

  Matching user documentation is provided by rtos-docs-asciidoc PR #41.

  Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
This commit is contained in:
Frédéric Desbiens
2026-09-17 15:39:30 -04:00
committed by GitHub
parent ad558a7b1f
commit 70a5300977
52 changed files with 14044 additions and 24 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,149 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/**************************************************************************/
/* */
/* BOARD SUPPORT RELEASE */
/* */
/* cache.c Cortex-R52/GNU */
/* 6.5.2 */
/* AUTHOR */
/* */
/* Frédéric Desbiens, Eclipse Foundation */
/* */
/* DESCRIPTION */
/* */
/* Cache maintenance for a loader that copies code. See cache.h for */
/* why only these three functions exist. */
/* */
/* MISRA C:2012 deviations (justified) */
/* */
/* Directive 4.3 -- cache maintenance and CTR are reachable only */
/* through CP15; every access is encapsulated in a one-line accessor */
/* below and nowhere else in this file. */
/* Rule 11.6 (conversion between a pointer and an integer) -- a cache */
/* maintenance operation takes a virtual address as a register value, */
/* so the conversion is what the instruction requires. */
/* */
/**************************************************************************/
#include "cache.h"
/* The smallest data-cache line in the machine, from CTR.DminLine. Held as a
log2 of the number of 32-bit words, so the byte count is 4 << DminLine.
DminLine and not the L1 geometry: a maintenance-by-address walk has to step
by the smallest line any level implements, or it skips lines in that level. */
#define CTR_DMINLINE_SHIFT 16U
#define CTR_DMINLINE_MASK 0xFUL
/**************************************************************************/
/* CP15 accessors. The only assembly in this file (MISRA C:2012 Dir 4.3).*/
/**************************************************************************/
static unsigned long read_ctr(void)
{
unsigned long value;
__asm__ volatile("mrc p15, 0, %0, c0, c0, 1" : "=r"(value));
return value;
}
static void clean_dcache_line(unsigned long address)
{
__asm__ volatile("mcr p15, 0, %0, c7, c10, 1" :: "r"(address) : "memory");
}
static void invalidate_icache_all_op(void)
{
unsigned long zero = 0UL;
__asm__ volatile("mcr p15, 0, %0, c7, c5, 0" :: "r"(zero) : "memory");
}
static void data_sync_barrier(void)
{
__asm__ volatile("dsb sy" ::: "memory");
}
static void instruction_barrier(void)
{
__asm__ volatile("isb" ::: "memory");
}
/**************************************************************************/
/* cache_dcache_line_bytes */
/**************************************************************************/
unsigned long cache_dcache_line_bytes(void)
{
return 4UL << ((read_ctr() >> CTR_DMINLINE_SHIFT) & CTR_DMINLINE_MASK);
}
/**************************************************************************/
/* cache_clean_range */
/* */
/* Clean by virtual address over [start, start + length). */
/* */
/* The start is rounded DOWN to a line boundary and the walk continues */
/* past the end until the last line containing a requested byte has been */
/* cleaned. Rounding the start up instead would leave the first partial */
/* line dirty, which is the whole failure this exists to prevent and is */
/* invisible whenever the caller happens to be line aligned. */
/**************************************************************************/
void cache_clean_range(const void *start_address, unsigned long length)
{
unsigned long line = cache_dcache_line_bytes();
unsigned long address;
unsigned long end;
if (length == 0UL)
{
return;
}
address = (unsigned long) start_address;
end = address + length;
address &= ~(line - 1UL);
while (address < end)
{
clean_dcache_line(address);
address += line;
}
/* The stores have to be complete before anything fetches from the range. */
data_sync_barrier();
}
/**************************************************************************/
/* cache_invalidate_icache_all */
/**************************************************************************/
void cache_invalidate_icache_all(void)
{
invalidate_icache_all_op();
/* DSB then ISB: the invalidate must complete, and the pipeline must be
flushed of anything fetched before it did. */
data_sync_barrier();
instruction_barrier();
}
@@ -0,0 +1,72 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/**************************************************************************/
/* */
/* BOARD SUPPORT RELEASE */
/* */
/* cache.h Cortex-R52/GNU */
/* 6.5.2 */
/* AUTHOR */
/* */
/* Frédéric Desbiens, Eclipse Foundation */
/* */
/* DESCRIPTION */
/* */
/* The cache maintenance a loader owes the instruction side, and */
/* nothing else. */
/* */
/* The S32Z280 board support carries a full cache driver -- enable, */
/* disable, geometry, set/way sweeps -- because silicon bring-up needed */
/* to ask the hardware what it had. None of that is needed here: the */
/* caches are turned on once in mpu_init and never turned off, and the */
/* model's geometry is not in question. What IS needed is the pair of */
/* operations that make copied code executable, so that is all this is. */
/* */
/* Why it is needed at all: the module area is Normal write-back memory, */
/* so a byte copy of module code leaves the bytes in dirty data-cache */
/* lines while the instruction side -- which is not coherent with the */
/* data cache on this core -- fetches whatever main memory still holds. */
/* Cleaning by address range and then invalidating the instruction cache */
/* is what closes that gap. */
/* */
/* By range rather than by set/way, deliberately. A set/way sweep needs */
/* the cache geometry and touches every line in the machine; the loader */
/* knows exactly which bytes it wrote, so the range form is both tighter */
/* and shorter to get right. */
/* */
/**************************************************************************/
#ifndef CACHE_H
#define CACHE_H
/* Clean (write back) the data cache over one address range, so that main
memory holds what the copy wrote. Rounds outwards to cache-line
boundaries; a length of zero does nothing. */
void cache_clean_range(const void *start_address, unsigned long length);
/* Invalidate the whole instruction cache, so no stale line from a previous
image at the same address can be served. Invalidate-all rather than by
range because it is one register write and this runs once per load. */
void cache_invalidate_icache_all(void);
/* Data cache line length in bytes, from CTR.DminLine. Published because the
range walk above depends on it and a caller may want to report it. */
unsigned long cache_dcache_line_bytes(void);
#endif /* CACHE_H */
File diff suppressed because it is too large Load Diff
@@ -101,6 +101,20 @@
.equ HSR_EC_SHIFT, 26
.equ HSR_EC_HVC, 0x12 /* HVC executed in AArch32 */
#ifdef TXM_MODULE_MANAGER
/* The module manager owns three of the EL1 vectors in a manager build: the
supervisor call, which is the privilege boundary a module crosses to reach
the kernel, and the two aborts, which are how a module that reaches outside
its regions is caught. Declared rather than merely referenced so that a
build with the manager half-configured fails at the link with a name in it. */
.extern __tx_module_svc_interrupt
.extern _txm_module_manager_data_abort
.extern _txm_module_manager_prefetch_abort
#endif
/* Report which vector was taken, then halt. The FVP offers no GDB stub
(Iris only), so a self-identifying fault is the primary debugging tool
for this port. Uses no stack: only r0/r1 and a semihosting call. */
@@ -144,7 +158,11 @@ el2_vectors:
el1_vectors:
b el1_trap_reset /* 0x00 reset */
b el1_trap_undef /* 0x04 undefined instruction */
#ifdef TXM_MODULE_MANAGER
b __tx_module_svc_interrupt /* 0x08 module kernel boundary */
#else
b el1_trap_svc /* 0x08 supervisor call */
#endif
b el1_trap_pabt /* 0x0C prefetch abort */
b el1_trap_dabt /* 0x10 data abort */
b el1_trap_reserved /* 0x14 reserved */
@@ -476,6 +494,26 @@ el1_trap_svc: FAULT_REPORT msg_el1_svc
and this handler returns there, restoring the pre-fault mode from SPSR. */
el1_trap_pabt:
#ifdef TXM_MODULE_MANAGER
/* A fault from User mode is a module reaching outside its code region, and
it belongs to the module manager, which records it and terminates the
thread. A fault from a privileged mode falls through to the paths below,
which the MPU self-test depends on.
r0 is pushed and popped around the test rather than simply used: the
capture in the manager records the faulting registers, and clobbering one
here would have it record ours instead. LDM does not affect the flags,
so the comparison still holds after the pop. */
stmdb sp!, {r0}
mrs r0, spsr
and r0, r0, #0x1F
cmp r0, #0x10 /* User mode? */
ldmia sp!, {r0}
beq _txm_module_manager_prefetch_abort
#endif
#ifdef TX_R52_MPU_FAULT_TEST
push {r0, r1}
ldr r0, =mpu_expect_pabt
@@ -532,6 +570,19 @@ mpu_try_execute_land:
Runs on the Abort-mode stack set up above. */
el1_trap_dabt:
#ifdef TXM_MODULE_MANAGER
/* A fault from User mode is a module reaching outside its data region. See
el1_trap_pabt above for why r0 is saved across the test. */
stmdb sp!, {r0}
mrs r0, spsr
and r0, r0, #0x1F
cmp r0, #0x10 /* User mode? */
ldmia sp!, {r0}
beq _txm_module_manager_data_abort
#endif
#ifdef TX_R52_MPU_FAULT_TEST
push {r0, r1}
ldr r0, =mpu_expect_abort
@@ -294,6 +294,80 @@ static void program_region(unsigned int index, const MPU_REGION *region_ptr)
}
#ifdef TXM_MODULE_MANAGER
/**************************************************************************/
/* mpu_module_window_init */
/* */
/* A window over the module area, for the manager to load through. */
/* */
/* No region in the table above covers the module area, which is what */
/* stops every thread from reaching a module's memory -- but the manager */
/* has to read the preamble and write the module's data to load it at */
/* all. Without this the load faults on its first read of the image. */
/* */
/* Not opened and closed around the load. It is left enabled here and */
/* the SCHEDULER owns it from then on: it turns this region on for every */
/* thread that owns no module and off for every thread that does, which */
/* is what keeps it and a module's own regions -- which cover the same */
/* memory -- from ever being enabled together. PMSAv8-R has no region */
/* priority, so an access hitting more than one enabled region takes a */
/* translation fault (TRM 8.1), and mutual exclusion by ownership is the */
/* only form of it that does not depend on remembering to bracket a call. */
/* */
/* EL1 read/write with no EL0 access, and execute-never: the manager can */
/* load through it, and a module cannot use it to reach anything. */
/**************************************************************************/
unsigned long mpu_module_window_prbar;
unsigned long mpu_module_window_prlar;
unsigned int mpu_module_window_init(void)
{
MPU_REGION window;
/* Asked of the hardware rather than assumed. See MPU_MODULE_REGIONS_REQUIRED
in mpu.h: this model reports a region count no real Cortex-R52 can have,
so the number is checked here and the shortfall is a refusal. */
if (mpu_region_count() < MPU_MODULE_REGIONS_REQUIRED)
{
return 0U;
}
window.mpu_region_base = FVP_MODULE_AREA_BASE;
window.mpu_region_limit = FVP_MODULE_AREA_BASE
+ FVP_MODULE_AREA_SIZE - 1UL;
window.mpu_region_ap = MPU_AP_RW_EL1;
window.mpu_region_execute_never = 1U;
window.mpu_region_shareability = MPU_SH_NON;
window.mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
window.mpu_region_name = "module window RW NX EL1";
/* Published for the scheduler, which turns this region on and off on every
dispatch and has no business computing register layouts in assembly. */
mpu_module_window_prbar = (window.mpu_region_base & 0xFFFFFFC0UL)
| (((unsigned long) window.mpu_region_shareability & 0x3UL) << 3)
| (((unsigned long) window.mpu_region_ap & 0x3UL) << 1)
| ((unsigned long) window.mpu_region_execute_never & 0x1UL);
mpu_module_window_prlar = (window.mpu_region_limit & 0xFFFFFFC0UL)
| (((unsigned long) window.mpu_region_attr_index & 0x7UL) << 1)
| 1UL;
program_region(MPU_MODULE_LOAD_REGION, &window);
data_sync_barrier();
instruction_barrier();
return 1U;
}
#endif /* TXM_MODULE_MANAGER */
/**************************************************************************/
/* mpu_init */
/**************************************************************************/
@@ -311,6 +385,20 @@ unsigned int mpu_init(void)
return 0U;
}
#ifdef TXM_MODULE_MANAGER
/* A module manager build needs regions the table above does not describe:
the eight handed to a module and the window over the module area. If the
implementation cannot supply them, stop here rather than enable a
protection map that is missing the half a module depends on. */
if (available < MPU_MODULE_REGIONS_REQUIRED)
{
return 0U;
}
#endif
/* Memory types first: a region's attribute index is meaningless until
MAIR is populated. */
@@ -332,6 +420,20 @@ unsigned int mpu_init(void)
write_prlar(0UL);
}
#ifdef TXM_MODULE_MANAGER
/* After the loop above, which would otherwise disable it again: the module
window lives above the regions this table uses, so it counts as unused
here. The region count was checked at the top of this function, so the
call cannot fail at this point. */
if (mpu_module_window_init() == 0U)
{
return 0U;
}
#endif
data_sync_barrier();
/* Caches are invalidated before enabling. The instruction cache has a
@@ -116,4 +116,54 @@ const MPU_REGION *mpu_region_table(unsigned int *count_ptr);
void mpu_read_region(unsigned int index, unsigned long *prbar_ptr,
unsigned long *prlar_ptr);
/* ---------------------------------------------------------------------------
ThreadX modules.
Kept in this header rather than behind TXM_MODULE_MANAGER because
txm_module_manager_offset_check.c includes it to check the region number
against the one tx_thread_schedule.S writes in assembly, and a definition
that appears only in some builds cannot be checked in the others.
--------------------------------------------------------------------------- */
/* Region index for the manager's load window over the module area. Above the
kernel's 0-2 and above the eight the manager hands to a module (8-15), so
neither the boot table nor the scheduler's per-thread region load can
disturb it. tx_thread_schedule.S spells the same number in an MCR to
PRSELR; the offset check asserts the two agree. */
#define MPU_MODULE_LOAD_REGION 16U
/* How many EL1 regions a module manager build needs: 0-2 kernel, 8-15 for the
module block, 16 for the window. Region 16 is the highest index used, so
seventeen regions is the requirement.
Checked against MPUIR rather than assumed, even though the AEMv8-R model
reports 32. The model reports a region count NO Cortex-R52 can have -- the
TRM gives MPUIR.DREGION as 16, 20 or 24 -- so a green run here says nothing
about whether the budget fits silicon, and a part configured with 16 regions
cannot run this port at all. The check is here so that the shortfall is a
refusal rather than a region that silently does not exist. */
#define MPU_MODULE_REGIONS_REQUIRED 17U
/* The module area window, which is what lets privileged code reach module
memory at all -- no other kernel region covers it.
PMSAv8-R has no region priority, so this must never be enabled at the same
time as the regions a module is given, which cover the same memory. That is
guaranteed by who owns it rather than by careful calling: the scheduler turns
this region on for every thread that is not a module thread and off for every
thread that is, so the window is enabled exactly when no module regions are
loaded. The two register words are published for it below.
Enabled at boot, because everything before the first module thread is
privileged code that may need to reach module memory. Returns 0 if the
implementation has fewer than MPU_MODULE_REGIONS_REQUIRED regions, in which
case nothing was programmed. */
unsigned int mpu_module_window_init(void);
extern unsigned long mpu_module_window_prbar;
extern unsigned long mpu_module_window_prlar;
#endif /* MPU_H */
@@ -71,6 +71,59 @@
#define SYSTEM_COUNTER_HZ 100000000
/* ---------------------------------------------------------------------------
Memory for ThreadX modules.
Only the module manager build uses this, but the addresses are stated here
rather than in the module example because they are a property of the board's
memory map: a module's code and data are handed to it as MPU regions, and
PMSAv8-R will not allow those to overlap a kernel region. So module memory
has to be memory NO kernel region covers, and deciding that is the memory
map's job.
link_module.lds ends the kernel's DRAM region at this base and gives the
64 KB above it to the module area; mpu.c covers exactly the same range with
the manager's load window (region 16), which is the only mapping privileged
code has over it. The three have to agree, so all three read these two
values.
Chosen at 0x003F0000 -- just under 4 MB -- because the manager image with its
stacks and its 1 MB of heap is far smaller than that, and the link script
asserts the two do not meet rather than trusting the margin. */
#define FVP_MODULE_AREA_BASE 0x003F0000
#define FVP_MODULE_AREA_SIZE 0x00010000 /* 64 KB */
/* The shared granules the sample module reports its progress through, at the
base of the module area.
The first of them exists because the FVP has no debugger seam: on silicon a
GDB harness reads the module's own progress variable out of its data area,
and here nothing outside the image can read anything. So the manager grants
the module a shared region over this word and reads it back afterwards, which
is what lets the FVP test judge what the module actually managed to do rather
than only that it faulted.
The rest exist to exercise the shared-region machinery itself. A module may
be granted TXM_MODULE_MPU_SHARED_ENTRIES regions, and one grant proves only
that the first entry works, so the area holds one granule per entry -- plus
one more that the manager NEVER grants, sandwiched between two that it does.
A limit register masked the wrong way, or a base off by a granule, leaks into
that gap from one side or the other, and a module that can write it is a
module whose grant covered more than was asked for.
FVP_MODULE_STATUS_SIZE is the size of ONE granule, which is the length of
each individual grant; the area is FVP_MODULE_STATUS_GRANULES of them.
Fixed addresses on both sides, and checked at run time rather than trusted:
the manager grants the regions from the linker's symbol and refuses to
continue if that symbol is not this address. */
#define FVP_MODULE_STATUS_BASE 0x003F0000
#define FVP_MODULE_STATUS_SIZE 0x40 /* one MPU granule */
#define FVP_MODULE_STATUS_GRANULES 6 /* five granted, one not */
#define FVP_MODULE_STATUS_UNGRANTED 2 /* the one never granted */
#ifndef __ASSEMBLER__
/* 32-bit device register access. */
File diff suppressed because it is too large Load Diff
@@ -149,6 +149,12 @@ cntfrq_at_el2: .word 0
hmpuir_at_el2: .word 0
.global probe_stage
probe_stage: .word 0
#ifdef TXM_MODULE_MANAGER
.extern __tx_module_svc_interrupt
.extern _txm_module_manager_data_abort
.extern _txm_module_manager_prefetch_abort
#endif
.global fault_expected
fault_expected: .word 0
.global fault_taken
@@ -197,7 +203,11 @@ el2_vectors:
el1_vectors:
b fault_el1_reset /* 0x00 */
b fault_el1_undef /* 0x04 */
#ifdef TXM_MODULE_MANAGER
b __tx_module_svc_interrupt /* 0x08 module kernel boundary */
#else
b fault_el1_svc /* 0x08 */
#endif
b fault_el1_pabt /* 0x0C */
b fault_el1_dabt /* 0x10 */
b fault_el1_reserved /* 0x14 */
@@ -891,6 +901,26 @@ fault_el1_svc:
mov r3, lr
FAULT_TAIL 0x48
fault_el1_pabt:
#ifdef TXM_MODULE_MANAGER
/* A fault from User mode is a module reaching outside its regions, and it
belongs to the module manager, which will record it and terminate the
thread. A fault from a privileged mode continues on the recoverable path
below, which the boot probes depend on.
r0 is pushed and popped around the test rather than simply used. The code
below clobbers r0 through r2 without saving them, which is tolerable for a
probe that provokes its own fault, but the module capture records the
faulting registers and would record ours instead. LDM does not affect the
flags, so the comparison still holds after the pop. */
stmdb sp!, {r0}
mrs r0, spsr
and r0, r0, #0x1F
cmp r0, #0x10 /* User mode? */
ldmia sp!, {r0}
beq _txm_module_manager_prefetch_abort
#endif
/* Recoverable when a test has armed fault_expected_pabt.
*
* Recovery differs from the data-abort case in kind, not degree. There,
@@ -932,6 +962,26 @@ fault_el1_pabt_fatal:
mrc p15, 0, r3, c6, c0, 2 /* IFAR */
FAULT_TAIL 0x4C
fault_el1_dabt:
#ifdef TXM_MODULE_MANAGER
/* A fault from User mode is a module reaching outside its regions, and it
belongs to the module manager, which will record it and terminate the
thread. A fault from a privileged mode continues on the recoverable path
below, which the boot probes depend on.
r0 is pushed and popped around the test rather than simply used. The code
below clobbers r0 through r2 without saving them, which is tolerable for a
probe that provokes its own fault, but the module capture records the
faulting registers and would record ours instead. LDM does not affect the
flags, so the comparison still holds after the pop. */
stmdb sp!, {r0}
mrs r0, spsr
and r0, r0, #0x1F
cmp r0, #0x10 /* User mode? */
ldmia sp!, {r0}
beq _txm_module_manager_data_abort
#endif
/* Recoverable when a test has armed fault_expected. The protection map is
only meaningful if a violation actually faults, so the test provokes one
deliberately and this path lets it continue: record the syndrome and
@@ -54,7 +54,10 @@ MEMORY
Used for data that wants deterministic access without depending on a
cache -- an enabled TCM is never cached. */
BTCM (rw) : ORIGIN = 0x30100000, LENGTH = 0x00004000 /* 16 KB */
DATA (rwx) : ORIGIN = 0x31780000, LENGTH = 0x00080000 /* 512 KB: DRAM0 + DRAM1, both full core speed */
DATA (rwx) : ORIGIN = 0x31780000, LENGTH = 0x00070000 /* 448 KB: DRAM0 + DRAM1 below the module area */
/* Module memory, uncovered by any kernel MPU region on purpose. See
S32Z_MODULE_AREA_BASE in platform.h. */
MODULE (rwx) : ORIGIN = 0x317F0000, LENGTH = 0x00010000 /* 64 KB */
}
ENTRY(_start)
@@ -302,8 +302,12 @@ static void build_table(void)
Reference Manual and confirmed on the board. */
mpu_regions[1].mpu_region_base = S32Z_DATA_SRAM_BASE;
/* Stops before the module area at the top of DRAM1. A module's memory must
not be covered by a kernel region, or every thread could reach it and the
manager's own regions would be decoration. */
mpu_regions[1].mpu_region_limit = S32Z_DATA_SRAM_BASE
+ S32Z_DATA_SRAM_SIZE - 1UL;
+ S32Z_DATA_SRAM_SHARED_SIZE - 1UL;
mpu_regions[1].mpu_region_ap = MPU_AP_RW_EL1;
mpu_regions[1].mpu_region_execute_never = 1U;
mpu_regions[1].mpu_region_shareability = MPU_SH_NON;
@@ -453,6 +457,73 @@ static void program_region(unsigned int index, const MPU_REGION *region_ptr)
}
/**************************************************************************/
/* mpu_module_load_window_open / mpu_module_load_window_close */
/* */
/* A window over the module area, for the manager to load through. */
/* */
/* No kernel region covers the module area, which is what stops every */
/* thread from reaching a module's memory -- but the manager has to read */
/* the preamble and write the module's data to load it at all. Without */
/* this the load faulted on its first read of the image, and because a */
/* privileged data abort ends in a handler that only spins, that looked */
/* exactly like the load hanging. */
/* */
/* Opened around the load and closed straight after, rather than left in */
/* place, because PMSAv8-R has no region priority: if this region were */
/* still enabled when a module thread ran it would overlap the module's */
/* own regions, and an access hitting both takes a translation fault */
/* (TRM 8.1). */
/* Closing it before any module thread starts is what keeps the two from */
/* ever being enabled together. */
/* */
/* Region 16, above both the kernel's 0-7 and the eight the manager */
/* hands to a module, so neither the scheduler's per-thread region load */
/* nor the boot table can disturb it. MPUIR reports 20 EL1 regions on */
/* this part. EL1 read/write with no EL0 access: the manager can load */
/* through it and a module cannot use it to reach anything. */
/**************************************************************************/
unsigned long mpu_module_window_prbar;
unsigned long mpu_module_window_prlar;
void mpu_module_window_init(void)
{
MPU_REGION window;
window.mpu_region_base = S32Z_MODULE_AREA_BASE;
window.mpu_region_limit = S32Z_MODULE_AREA_BASE
+ S32Z_MODULE_AREA_SIZE - 1UL;
window.mpu_region_ap = MPU_AP_RW_EL1;
window.mpu_region_execute_never = 1U;
window.mpu_region_shareability = MPU_SH_NON;
window.mpu_region_attr_index = MPU_ATTR_NORMAL_WB;
window.mpu_region_name = "module window RW NX EL1";
/* Published for the scheduler, which turns this region on and off on every
dispatch and has no business computing register layouts in assembly. */
mpu_module_window_prbar = (window.mpu_region_base & 0xFFFFFFC0UL)
| (((unsigned long) window.mpu_region_shareability & 0x3UL) << 3)
| (((unsigned long) window.mpu_region_ap & 0x3UL) << 1)
| ((unsigned long) window.mpu_region_execute_never & 0x1UL);
mpu_module_window_prlar = (window.mpu_region_limit & 0xFFFFFFC0UL)
| (((unsigned long) window.mpu_region_attr_index & 0x7UL) << 1)
| 1UL;
/* Enabled now, because everything running before the first module thread is
privileged code that may need to reach module memory -- the manager loads
a module from a kernel thread. */
program_region(MPU_MODULE_LOAD_REGION, &window);
data_sync_barrier();
instruction_barrier();
}
/**************************************************************************/
/* mpu_init */
/**************************************************************************/
@@ -499,6 +570,16 @@ unsigned int mpu_init(void)
write_prlar(0UL);
}
#ifdef TXM_MODULE_MANAGER
/* After the loop above, which would otherwise disable it again: the module
window lives above the regions this table uses, so it counts as unused
here. */
mpu_module_window_init();
#endif
MARK(0x40);
data_sync_barrier();
@@ -115,4 +115,28 @@ const MPU_REGION *mpu_region_table(unsigned int *count_ptr);
void mpu_read_region(unsigned int index, unsigned long *prbar_ptr,
unsigned long *prlar_ptr);
/* Region index for the manager's load window over the module area. Above the
kernel's 0-7 and above the eight the manager hands to a module, so neither
the boot table nor the scheduler's per-thread region load can disturb it. */
#define MPU_MODULE_LOAD_REGION 16U
/* The module area window, which is what lets privileged code reach module
memory at all -- no other kernel region covers it.
PMSAv8-R has no region priority, so this must never be enabled at the same
time as the regions a module is given, which cover the same memory. That is
guaranteed by who owns it rather than by careful calling: the scheduler turns
this region on for every thread that is not a module thread and off for every
thread that is, so the window is enabled exactly when no module regions are
loaded. The two register words are published for it below.
Enabled here at boot, because everything before the first module thread is
privileged code that may need to reach module memory. */
void mpu_module_window_init(void);
extern unsigned long mpu_module_window_prbar;
extern unsigned long mpu_module_window_prlar;
#endif /* MPU_H */
@@ -130,6 +130,61 @@
#define S32Z_DRAM0_SIZE 0x00040000UL
#define S32Z_DRAM1_BASE 0x317C0000UL
#define S32Z_DRAM1_SIZE 0x00040000UL
/* Memory for loadable modules: the top 64 KB of DRAM1, deliberately left out of
the broad data region in mpu.c.
It has to be outside every region the board support package programs. The
manager gives a module its own regions, and if the kernel's map already covered
that memory then every thread could reach the module and back -- the isolation
would be nominal. Carving a hole is the only way to make it real.
DRAM1 rather than DRAM2 because it runs at full core speed (S32Z2 RM 6.3.6),
and because code and data can be contiguous here. Putting module code in the
code RAM region instead would have been the obvious choice and does not work:
that region is read-only, so a module's data would have to live somewhere else
and the module would straddle two carve-outs.
64 KB is arbitrary but not accidental: it leaves 448 KB of full-speed RAM for
the kernel's data, stacks and bss, which currently use about 14 KB. */
#define S32Z_MODULE_AREA_BASE 0x317F0000UL
#define S32Z_MODULE_AREA_SIZE 0x00010000UL /* 64 KB */
/* The shared granules the sample module reports its progress through, at the
base of the module area.
A module cannot print -- the console belongs to the board support package and
lies outside every region a module owns -- so the manager grants it shared
regions and reads them back. On this board a GDB harness reads the module's
own progress variable as well, out of the data area the manager allocated;
these granules are the channel that needs no debugger, and having both means
the two agree or the run says so.
Six granules rather than one, because a module may be granted
TXM_MODULE_MPU_SHARED_ENTRIES regions and one grant only ever exercises the
first of them. Five are granted, one granule per entry, and
S32Z_MODULE_STATUS_UNGRANTED -- index 2, so a granted granule sits on either
side of it -- is never granted to anything. A limit register masked the wrong
way, or a base off by a granule, extends a region into that gap from one side
or the other, and a module that can write it was given more than was asked
for.
S32Z_MODULE_STATUS_SIZE is the size of ONE granule, which is also the length
of each individual grant; the area is GRANULES of them. Fixed addresses on
both sides, checked at run time against the linker's symbol rather than
trusted. */
#define S32Z_MODULE_STATUS_BASE 0x317F0000UL
#define S32Z_MODULE_STATUS_SIZE 0x40UL /* one MPU granule */
#define S32Z_MODULE_STATUS_GRANULES 6UL /* five granted, one not */
#define S32Z_MODULE_STATUS_UNGRANTED 2UL /* the one never granted */
/* What is left of the full-speed pair for the kernel itself. link.lds sizes its
DATA region from this, so the linker cannot place kernel data in the module
area by accident. */
#define S32Z_DATA_SRAM_SHARED_SIZE (S32Z_MODULE_AREA_BASE - S32Z_DRAM0_BASE)
/* The top 8 KB of DRAM2 is deliberately left out of the broad data region in
mpu.c and mapped one window at a time, per thread, instead. Isolation is only
meaningful in memory that no other region already grants access to, and every
@@ -66,16 +66,59 @@ static unsigned int max_cycles;
/* CP15 accessors. */
/**************************************************************************/
/* Direct per-region access rather than PRSELR.
The Cortex-R52 TRM 8.4 encodes the region number into CRm and opc2: CRm is
0b1rrr where rrr is region_number[3:1], with opc2 0 and 1 for an even region
and 4 and 5 for an odd one. Regions 0 through 15 take CP15 op1 0 and regions
16 through 24 take op1 1. PRBAR and PRLAR without a number are the indirect
view selected by PRSELR.
Region 8 is therefore op1 0, CRm c12, opc2 0 and 1. Using it removes the
PRSELR write and, more importantly, the ISB that has to follow PRSELR before
the region registers can be written.
Measured on this part: 542 to 604 cycles through PRSELR against 434 to 470
direct, for the same region and the same isolation result. The saving matters
most where several regions are programmed at once, which is what a module
switch does -- there the PRSELR route pays an ISB per region while the direct
route pays one barrier pair for the whole block.
The TRM disagrees with itself about the range, and the next reader should not
have to rediscover that. The register descriptions at 3.3.85 and 3.3.86 say
"Direct access is provided to PRBAR0 to PRBAR15" and list only the op1 0
form; 8.4 and Table 8-9 list both forms. Table 8-9 also prints opc2 0 for
the even limit registers where the prose of 8.4 says 1. What is written here
uses op1 0 only, which is the half of the manual this part has been measured
against. */
/* Kept for regions 16 and above. Those have direct op1 1 encodings in TRM 8.4,
but only the selector route is proven on this part, and nothing here needs a
region that high while the per-thread window lives at region 8. */
__attribute__((unused))
static void write_prselr(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c6, c2, 1" : : "r"(value) : "memory");
}
static void write_prbar8_direct(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c6, c12, 0" : : "r"(value) : "memory");
}
static void write_prlar8_direct(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c6, c12, 1" : : "r"(value) : "memory");
}
__attribute__((unused))
static void write_prbar(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c6, c3, 0" : : "r"(value) : "memory");
}
__attribute__((unused))
static void write_prlar(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c6, c3, 1" : : "r"(value) : "memory");
@@ -154,9 +197,6 @@ void thread_mpu_activate(TX_THREAD *thread_ptr)
}
}
write_prselr(THREAD_MPU_REGION);
__asm__ volatile("isb" ::: "memory");
if (base == 0UL)
{
/* No window for this thread: disable the region rather than leaving the
@@ -164,16 +204,16 @@ void thread_mpu_activate(TX_THREAD *thread_ptr)
protection silently becomes no protection -- the last thread to run
would leave its window open to whatever ran next. */
write_prlar(0UL);
write_prlar8_direct(0UL);
}
else
{
write_prbar((base & ~0x3FUL)
write_prbar8_direct((base & ~0x3FUL)
| ((unsigned long) MPU_SH_NON << 3)
| ((unsigned long) MPU_AP_RW_EL1 << 1)
| 1UL); /* XN: data only */
write_prlar(((base + S32Z_THREAD_WINDOW_SIZE - 1UL) & ~0x3FUL)
write_prlar8_direct(((base + S32Z_THREAD_WINDOW_SIZE - 1UL) & ~0x3FUL)
| ((unsigned long) MPU_ATTR_NORMAL_WB << 1)
| 1UL); /* EN */
}
+62 -7
View File
@@ -255,17 +255,72 @@ typedef unsigned short USHORT;
#define TX_TIMER_DELETE_EXTENSION(timer_ptr)
/* Determine if the ARM architecture has the CLZ instruction. This is available on
architectures v5 and above. If available, redefine the macro for calculating the
lowest bit set. */
/* 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.
#if __TARGET_ARCH_ARM > 4
The guard is not upstream's. Upstream asks __TARGET_ARCH_ARM > 4, which is an
Arm Compiler 5 predefine. GCC does not define it -- it predefines the ACLE
macros __ARM_ARCH and __ARM_FEATURE_CLZ instead -- so under GCC the test reads
0 > 4, this whole block is dropped and tx_thread.h's portable loop runs on a
core that has had the instruction since Armv5. Measured with
arm-none-eabi-gcc 14.3 on 20 Aug 2026: zero CLZ instructions in the built
scheduler objects.
That was not a dormant path. Half the TX_LOWEST_SET_BIT_CALCULATE call sites
in tx_thread_suspend.c and tx_thread_system_suspend.c sit OUTSIDE the
TX_MAX_PRIORITIES > 32 guards, so the portable loop was running in the
scheduler's priority search in the default 32-priority configuration, which is
the one every R52 build uses.
__ARM_FEATURE_CLZ is the ACLE answer to the question actually being asked, and
the compiler defines it exactly when the architecture has the instruction, so
a core without CLZ is excluded by construction rather than by an architecture
number. Arm Compiler 5's spelling is kept beside it, now wrapped in defined()
so the test no longer leans on an undefined identifier evaluating to zero --
which is what -Wundef reports and how this was found.
The __thumb__ guard stays, and it is load-bearing rather than inherited
caution: __ARM_FEATURE_CLZ describes the ARCHITECTURE, not the instruction
set. Checked on 20 Aug 2026 -- GCC defines it for -mthumb -march=armv5te,
where Thumb-1 has no CLZ at all and this asm would fail to assemble. A Thumb
build therefore keeps the portable loop on purpose. (On this core it is moot:
the R52 toolchain file builds -marm.)
Two deliberate deviations, per AGENTS.md:
- Rule 1.2, language extensions. Inline assembly is the entire point of the
macro; there is no conforming way to reach CLZ. Spelled __asm__ and not
asm, because the asm keyword is rejected under -std=c99 -- verified, it is
an "'asm' undeclared" error -- and AGENTS.md requires C99 compatibility.
- Rule 10.1 / 10.3 on the isolation step, which is why it is respelled.
Upstream isolates the lowest set bit with (ULONG) (-((LONG) m)): that
converts an unsigned map to signed and negates it, which is undefined for
the one input whose top bit is set. (~(m)) + 1 is the same value in
well-defined unsigned arithmetic, and it is character-for-character what
tx_thread.h's portable version uses -- so the two implementations now
visibly compute the same thing instead of merely agreeing.
Rule 20.7 is a straight fix rather than a deviation: upstream leaves m and b
unparenthesised in the expansion.
PRECONDITION: m must be non-zero, and the two implementations DISAGREE when it
is not. CLZ(0) is 32, so this yields 31 - 32; the portable loop yields 0.
All twelve call sites in common/src reach the macro only on a map already
tested against zero -- every one checked on 20 Aug 2026 -- so the divergence is
unreachable today. It is written down because a new call site is exactly how
it would stop being unreachable, and demo_clz.c pins both answers so that
changing this has to be a decision. */
#if defined(__ARM_FEATURE_CLZ) || (defined(__TARGET_ARCH_ARM) && (__TARGET_ARCH_ARM > 4))
#ifndef __thumb__
#define TX_LOWEST_SET_BIT_CALCULATE(m, b) m = m & ((ULONG) (-((LONG) m))); \
asm volatile (" CLZ %0,%1 ": "=r" (b) : "r" (m) ); \
b = 31 - b;
#define TX_LOWEST_SET_BIT_CALCULATE(m, b) \
(m) = (m) & ((~(m)) + ((ULONG) 1)); \
__asm__ volatile (" CLZ %0,%1 " : "=r" (b) : "r" (m)); \
(b) = 31 - (b);
#endif
#endif
+34 -8
View File
@@ -51,7 +51,7 @@ baseline selects the soft ABI rather than removing the FPU.
-DCMAKE_TOOLCHAIN_FILE=cmake/cortex_r52.cmake \
-DTX_R52_BUILD_FVP_EXAMPLE=ON .
ninja -C build_r52 boot_check.elf demo_m2.elf demo_m3.elf \
demo_threadx.elf demo_mpu.elf
demo_threadx.elf demo_mpu.elf demo_clz.elf
ctest --test-dir build_r52
The images target the free Armv8-R AEM FVP (FVP_BaseR_AEMv8R):
@@ -61,6 +61,7 @@ The images target the free Armv8-R AEM FVP (FVP_BaseR_AEMv8R):
demo_m3.elf generic timer tick, GICv3 and preemption
demo_threadx.elf the standard eight-thread demo plus verification
demo_mpu.elf PMSAv8-R protection and cache enable
demo_clz.elf the CLZ lowest-set-bit priority search
demo_m5.elf lazy VFP context save (needs TX_R52_ENABLE_VFP)
Each image reports its own result and terminates the model through the
@@ -135,6 +136,20 @@ and the thread stack pointer and run counter offsets, at compile time: a
layout change becomes a build failure instead of silent corruption.
Also worth knowing: this port replaces tx_thread.h's portable lowest-set-bit
search with the CLZ instruction, which is what the scheduler uses to pick the
next thread to run. Upstream gates that on __TARGET_ARCH_ARM, an Arm Compiler
5 predefine that GCC does not define, so the optimisation had never once been
compiled in under GCC; the guard here asks __ARM_FEATURE_CLZ instead. It is
worth 40% of the priority search's code size (2188 to 1316 bytes in
tx_thread_system_suspend.o) and it applies in the default 32-priority
configuration, not only above 32. A Thumb build deliberately keeps the
portable loop, because __ARM_FEATURE_CLZ describes the architecture rather
than the instruction set and Thumb-1 has no CLZ. demo_clz.elf is the
regression test, and it fails to build rather than silently testing the
portable loop if the CLZ path is ever disabled again.
7. Memory Protection
PMSAv8-R regions are described by a table rather than a sequence of
@@ -145,13 +160,24 @@ disables the regions it does not use.
Two cautions for anyone reusing this code on silicon:
- The PRBAR.AP encoding used here was calibrated against the hardware.
The low bit is read-only and the high bit grants EL0 access, which is
the reverse of the widely-published Armv8-R AArch64 macro set. Getting
it wrong produces regions that report as read-only and accept writes,
because region coverage is still enforced: an unmapped address faults
while a "read-only" region does not. Re-calibrate before trusting
these values on a different implementation.
- The PRBAR.AP encoding is the standard Armv8-R one, as published:
AP[2] selects read-only and AP[1] grants EL0 access. No calibration is
needed, and an earlier revision of this file said otherwise -- it
claimed the two bits were reversed, on the strength of a real
measurement with a wrong cause. program_region() had been shifting
every PRBAR field one bit too far left, so the AP value's low bit
landed in the true AP[2] and writes faulted exactly when that bit was
set, which looks precisely like a reversed encoding. The shift is
fixed; see the comment on the MPU_AP_* macros in mpu.h for the full
calibration table and what it actually proved.
The reason it took a silicon run to notice: region coverage is enforced
even when permissions are not what you asked for, so an unmapped
address faults while a "read-only" region quietly accepts writes. A
configuration-only review cannot tell the two apart -- provoke a real
fault instead. Verified on S32Z280 silicon: a write to an RO region
faults and execution from an XN region takes a prefetch abort, the
latter never having worked under the old shift.
- The code and data regions must not share a 64-byte granule. The
linker script separates them with ".data ALIGN(64) :" on the output
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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
@@ -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
@@ -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)
}
}
File diff suppressed because it is too large Load Diff
@@ -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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,187 @@
#!/bin/bash
# Copyright (c) 2026 Eclipse ThreadX contributors
# SPDX-License-Identifier: MIT
# Some portions generated by Claude Code (Opus 5).
#
# Run the ThreadX module manager demonstration on the S32Z280-594EVB and capture
# the evidence: the console log and the fault registers.
#
# Usage: tools/run_module_demo.sh [<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
@@ -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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,193 @@
@/***************************************************************************
@ * Copyright (c) 2024 Microsoft Corporation
@ * Copyright (c) 2026 Eclipse ThreadX contributors
@ *
@ * This program and the accompanying materials are made available under the
@ * terms of the MIT License which is available at
@ * https://opensource.org/licenses/MIT.
@ *
@ * SPDX-License-Identifier: MIT
@ **************************************************************************/
@ Some portions generated by Claude Code (Opus 5).
@
@
@/**************************************************************************/
@/**************************************************************************/
@/** */
@/** ThreadX Component */
@/** */
@/** Thread */
@/** */
@/**************************************************************************/
@/**************************************************************************/
#ifdef TX_INCLUDE_USER_DEFINE_FILE
#include "tx_user.h"
#endif
.global _tx_thread_system_state
.global _tx_thread_current_ptr
.global _tx_irq_processing_return
.global _tx_execution_isr_enter
@
@
@/* No 16-bit Thumb mode veneer code is needed for _tx_thread_context_save
@ since it will never be called 16-bit mode. */
@
.arm
.text
.align 2
@/**************************************************************************/
@/* */
@/* FUNCTION RELEASE */
@/* */
@/* _tx_thread_context_save Cortex-R52/GNU */
@/* 6.5.2 */
@/* AUTHOR */
@/* */
@/* Frédéric Desbiens, Eclipse Foundation */
@/* */
@/* Derived from the Cortex-R5/GNU port originally written by */
@/* William E. Lamie, Microsoft Corporation. */
@/* */
@/* DESCRIPTION */
@/* */
@/* This function saves the context of an executing thread in the */
@/* beginning of interrupt processing. The function also ensures that */
@/* the system stack is used upon return to the calling ISR. */
@/* */
@/* INPUT */
@/* */
@/* None */
@/* */
@/* OUTPUT */
@/* */
@/* None */
@/* */
@/* CALLS */
@/* */
@/* None */
@/* */
@/* CALLED BY */
@/* */
@/* ISRs */
@/* */
@/**************************************************************************/
@VOID _tx_thread_context_save(VOID)
@{
.global _tx_thread_context_save
.type _tx_thread_context_save,function
_tx_thread_context_save:
@
@ /* Upon entry to this routine, it is assumed that IRQ interrupts are locked
@ out, we are in IRQ mode, and all registers are intact. */
@
@ /* Check for a nested interrupt condition. */
@ if (_tx_thread_system_state++)
@ {
@
STMDB sp!, {r0-r3} @ Save some working registers
#ifdef TX_ENABLE_FIQ_SUPPORT
CPSID if @ Disable FIQ interrupts
#endif
LDR r3, =_tx_thread_system_state @ Pickup address of system state variable
LDR r2, [r3] @ Pickup system state
CMP r2, #0 @ Is this the first interrupt?
BEQ __tx_thread_not_nested_save @ Yes, not a nested context save
@
@ /* Nested interrupt condition. */
@
ADD r2, r2, #1 @ Increment the interrupt counter
STR r2, [r3] @ Store it back in the variable
@
@ /* Save the rest of the scratch registers on the stack and return to the
@ calling ISR. */
@
MRS r0, SPSR @ Pickup saved SPSR
SUB lr, lr, #4 @ Adjust point of interrupt
STMDB sp!, {r0, r10, r12, lr} @ Store other registers
@
@ /* Return to the ISR. */
@
MOV r10, #0 @ Clear stack limit
#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
@
@ /* Call the ISR enter function to indicate an ISR is executing. */
@
PUSH {lr} @ Save ISR lr
BL _tx_execution_isr_enter @ Call the ISR enter function
POP {lr} @ Recover ISR lr
#endif
B __tx_irq_processing_return @ Continue IRQ processing
@
__tx_thread_not_nested_save:
@ }
@
@ /* Otherwise, not nested, check to see if a thread was running. */
@ else if (_tx_thread_current_ptr)
@ {
@
ADD r2, r2, #1 @ Increment the interrupt counter
STR r2, [r3] @ Store it back in the variable
LDR r1, =_tx_thread_current_ptr @ Pickup address of current thread ptr
LDR r0, [r1] @ Pickup current thread pointer
CMP r0, #0 @ Is it NULL?
BEQ __tx_thread_idle_system_save @ If so, interrupt occurred in
@ scheduling loop - nothing needs saving!
@
@ /* Save minimal context of interrupted thread. */
@
MRS r2, SPSR @ Pickup saved SPSR
SUB lr, lr, #4 @ Adjust point of interrupt
STMDB sp!, {r2, r10, r12, lr} @ Store other registers
@
@ /* Save the current stack pointer in the thread's control block. */
@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp;
@
@ /* Switch to the system stack. */
@ sp = _tx_thread_system_stack_ptr@
@
MOV r10, #0 @ Clear stack limit
#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
@
@ /* Call the ISR enter function to indicate an ISR is executing. */
@
PUSH {lr} @ Save ISR lr
BL _tx_execution_isr_enter @ Call the ISR enter function
POP {lr} @ Recover ISR lr
#endif
B __tx_irq_processing_return @ Continue IRQ processing
@
@ }
@ else
@ {
@
__tx_thread_idle_system_save:
@
@ /* Interrupt occurred in the scheduling loop. */
@
@ /* Not much to do here, just adjust the stack pointer, and return to IRQ
@ processing. */
@
MOV r10, #0 @ Clear stack limit
#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
@
@ /* Call the ISR enter function to indicate an ISR is executing. */
@
PUSH {lr} @ Save ISR lr
BL _tx_execution_isr_enter @ Call the ISR enter function
POP {lr} @ Recover ISR lr
#endif
ADD sp, sp, #16 @ Recover saved registers
B __tx_irq_processing_return @ Continue IRQ processing
@
@ }
@}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
@/***************************************************************************
@ * Copyright (c) 2024 Microsoft Corporation
@ * Copyright (c) 2026 Eclipse ThreadX contributors
@ *
@ * This program and the accompanying materials are made available under the
@ * terms of the MIT License which is available at
@ * https://opensource.org/licenses/MIT.
@ *
@ * SPDX-License-Identifier: MIT
@ **************************************************************************/
@ Some portions generated by Claude Code (Opus 5).
@
@
@/**************************************************************************/
@/**************************************************************************/
@/** */
@/** ThreadX Component */
@/** */
@/** Thread */
@/** */
@/**************************************************************************/
@/**************************************************************************/
#ifdef TX_INCLUDE_USER_DEFINE_FILE
#include "tx_user.h"
#endif
.arm
@ Bare CPSR mode field, spelled _MODE_BITS as the base port does in
@ tx_thread_irq_nesting_start.S and tx_thread_fiq_context_restore.S. In this
@ port a plain _MODE name means a whole CPSR value with the interrupt masks
@ already in it -- tx_thread_context_restore.S defines SYS_MODE as 0xDF or 0x9F
@ -- so the two must not share a name. The base port's SVC_MODE = 0x13 here was
@ the frame's mode before this port moved kernel threads to System mode; nothing
@ uses it now, so it is gone rather than left to be picked up by mistake.
SYS_MODE_BITS = 0x1F @ SYS mode, privileged
#ifdef TX_ENABLE_FIQ_SUPPORT
CPSR_MASK = 0xDF @ Mask initial CPSR, IRQ & FIQ interrupts enabled
#else
CPSR_MASK = 0x9F @ Mask initial CPSR, IRQ interrupts enabled
#endif
@
@
@/* Define the 16-bit Thumb mode veneer for _tx_thread_stack_build for
@ applications calling this function from to 16-bit Thumb mode. */
@
.text
.align 2
.thumb
.global $_tx_thread_stack_build
.type $_tx_thread_stack_build,function
$_tx_thread_stack_build:
BX pc @ Switch to 32-bit mode
NOP @
.arm
STMFD sp!, {lr} @ Save return address
BL _tx_thread_stack_build @ Call _tx_thread_stack_build function
LDMFD sp!, {lr} @ Recover saved return address
BX lr @ Return to 16-bit caller
@
@
.text
.align 2
@/**************************************************************************/
@/* */
@/* FUNCTION RELEASE */
@/* */
@/* _tx_thread_stack_build Cortex-R52/GNU */
@/* 6.5.2 */
@/* AUTHOR */
@/* */
@/* Frédéric Desbiens, Eclipse Foundation */
@/* */
@/* Derived from the Cortex-R5/GNU port originally written by */
@/* William E. Lamie, Microsoft Corporation. */
@/* */
@/* DESCRIPTION */
@/* */
@/* This function builds a stack frame on the supplied thread's stack. */
@/* The stack frame results in a fake interrupt return to the supplied */
@/* function pointer. */
@/* */
@/* INPUT */
@/* */
@/* thread_ptr Pointer to thread control blk */
@/* function_ptr Pointer to return function */
@/* */
@/* OUTPUT */
@/* */
@/* None */
@/* */
@/* CALLS */
@/* */
@/* None */
@/* */
@/* CALLED BY */
@/* */
@/* _tx_thread_create Create thread service */
@/* */
@/**************************************************************************/
@VOID _tx_thread_stack_build(TX_THREAD *thread_ptr, VOID (*function_ptr)(VOID))
@{
.global _tx_thread_stack_build
.type _tx_thread_stack_build,function
_tx_thread_stack_build:
@
@
@ /* Build a fake interrupt frame. The form of the fake interrupt stack
@ on the ARM9 should look like the following after it is built:
@
@ Stack Top: 1 Interrupt stack frame type
@ CPSR Initial value for CPSR
@ a1 (r0) Initial value for a1
@ a2 (r1) Initial value for a2
@ a3 (r2) Initial value for a3
@ a4 (r3) Initial value for a4
@ v1 (r4) Initial value for v1
@ v2 (r5) Initial value for v2
@ v3 (r6) Initial value for v3
@ v4 (r7) Initial value for v4
@ v5 (r8) Initial value for v5
@ sb (r9) Initial value for sb
@ sl (r10) Initial value for sl
@ fp (r11) Initial value for fp
@ ip (r12) Initial value for ip
@ lr (r14) Initial value for lr
@ pc (r15) Initial value for pc
@ 0 For stack backtracing
@
@ Stack Bottom: (higher memory address) */
@
LDR r2, [r0, #16] @ Pickup end of stack area
BIC r2, r2, #7 @ Ensure 8-byte alignment
SUB r2, r2, #76 @ Allocate space for the stack frame
@
@ /* Actually build the stack frame. */
@
MOV r3, #1 @ Build interrupt stack type
STR r3, [r2, #0] @ Store stack type
MOV r3, #0 @ Build initial register value
STR r3, [r2, #8] @ Store initial r0
STR r3, [r2, #12] @ Store initial r1
STR r3, [r2, #16] @ Store initial r2
STR r3, [r2, #20] @ Store initial r3
STR r3, [r2, #24] @ Store initial r4
STR r3, [r2, #28] @ Store initial r5
STR r3, [r2, #32] @ Store initial r6
STR r3, [r2, #36] @ Store initial r7
STR r3, [r2, #40] @ Store initial r8
STR r3, [r2, #44] @ Store initial r9
LDR r3, [r0, #12] @ Pickup stack starting address
STR r3, [r2, #48] @ Store initial r10 (sl)
LDR r3,=_tx_thread_schedule @ Pickup address of _tx_thread_schedule for GDB backtrace
STR r3, [r2, #60] @ Store initial r14 (lr)
MOV r3, #0 @ Build initial register value
STR r3, [r2, #52] @ Store initial r11
STR r3, [r2, #56] @ Store initial r12
STR r1, [r2, #64] @ Store initial pc
STR r3, [r2, #68] @ 0 for back-trace
MRS r1, CPSR @ Pickup CPSR
BIC r1, r1, #CPSR_MASK @ Mask mode bits of CPSR
@
@ /* SYS mode, not SVC. In a module port no thread runs in SVC mode: SVC is
@ reserved for the supervisor call handler, which is how a module reaches
@ the kernel, and a thread sitting in SVC mode would be using the handler's
@ banked stack pointer as its own. Kernel threads therefore start in SYS
@ mode, which is privileged and shares User mode's banked sp -- the same sp
@ a module thread uses -- so the context save and restore paths can reach a
@ thread's stack the same way whoever it belongs to.
@
@ This is the one line that differs from the base port's stack build, and
@ leaving it as SVC would have put kernel threads on the SVC stack while the
@ restore path looked for them on the SYS one. */
@
ORR r3, r1, #SYS_MODE_BITS @ Build CPSR, SYS mode, interrupts enabled
STR r3, [r2, #4] @ Store initial CPSR
@
@ /* Setup stack pointer. */
@ thread_ptr -> tx_thread_stack_ptr = r2;
@
STR r2, [r0, #8] @ Save stack pointer in thread's
@ control block
#ifdef __THUMB_INTERWORK
BX lr @ Return to caller
#else
MOV pc, lr @ Return to caller
#endif
@}
@@ -0,0 +1,169 @@
@/***************************************************************************
@ * Copyright (c) 2024 Microsoft Corporation
@ * Copyright (c) 2026 Eclipse ThreadX contributors
@ *
@ * This program and the accompanying materials are made available under the
@ * terms of the MIT License which is available at
@ * https://opensource.org/licenses/MIT.
@ *
@ * SPDX-License-Identifier: MIT
@ **************************************************************************/
@ Some portions generated by Claude Code (Opus 5).
@
@
@/**************************************************************************/
@/**************************************************************************/
@/** */
@/** ThreadX Component */
@/** */
@/** Thread */
@/** */
@/**************************************************************************/
@/**************************************************************************/
#ifdef TX_INCLUDE_USER_DEFINE_FILE
#include "tx_user.h"
#endif
.arm
@
@
.global _tx_thread_current_ptr
.global _tx_timer_time_slice
.global _tx_thread_schedule
.global _tx_execution_thread_exit
@
@
@
@/* Define the 16-bit Thumb mode veneer for _tx_thread_system_return for
@ applications calling this function from to 16-bit Thumb mode. */
@
.text
.align 2
.global $_tx_thread_system_return
.type $_tx_thread_system_return,function
$_tx_thread_system_return:
.thumb
BX pc @ Switch to 32-bit mode
NOP @
.arm
STMFD sp!, {lr} @ Save return address
BL _tx_thread_system_return @ Call _tx_thread_system_return function
LDMFD sp!, {lr} @ Recover saved return address
BX lr @ Return to 16-bit caller
@
@
.text
.align 2
@/**************************************************************************/
@/* */
@/* FUNCTION RELEASE */
@/* */
@/* _tx_thread_system_return Cortex-R52/GNU */
@/* 6.5.2 */
@/* AUTHOR */
@/* */
@/* Frédéric Desbiens, Eclipse Foundation */
@/* */
@/* Derived from the Cortex-R5/GNU port originally written by */
@/* William E. Lamie, Microsoft Corporation. */
@/* */
@/* DESCRIPTION */
@/* */
@/* This function is target processor specific. It is used to transfer */
@/* control from a thread back to the ThreadX system. Only a */
@/* minimal context is saved since the compiler assumes temp registers */
@/* are going to get slicked by a function call anyway. */
@/* */
@/* INPUT */
@/* */
@/* None */
@/* */
@/* OUTPUT */
@/* */
@/* None */
@/* */
@/* CALLS */
@/* */
@/* _tx_thread_schedule Thread scheduling loop */
@/* */
@/* CALLED BY */
@/* */
@/* ThreadX components */
@/* */
@/**************************************************************************/
@VOID _tx_thread_system_return(VOID)
@{
.global _tx_thread_system_return
.type _tx_thread_system_return,function
_tx_thread_system_return:
@
@ /* Lockout interrupts. */
@
MRS r1, CPSR @ Pickup the CPSR
#ifdef TX_ENABLE_FIQ_SUPPORT
CPSID if @ Disable IRQ and FIQ interrupts
#else
CPSID i @ Disable IRQ interrupts
#endif
@ /* Save minimal context on the stack. */
@
STMDB sp!, {r4-r11, lr} @ Save minimal context
LDR r5, =_tx_thread_current_ptr @ Pickup address of current ptr
LDR r6, [r5, #0] @ Pickup current thread pointer
@
#ifdef TX_ENABLE_VFP_SUPPORT
LDR r0, [r6, #144] @ Pickup the VFP enabled flag
CMP r0, #0 @ Is the VFP enabled?
BEQ _tx_skip_solicited_vfp_save @ No, skip VFP solicited save
VMRS r4, FPSCR @ Pickup the FPSCR
STR r4, [sp, #-4]! @ Save FPSCR
VSTMDB sp!, {D8-D15} @ Save D8-D15
_tx_skip_solicited_vfp_save:
#endif
@
MOV r0, #0 @ Build a solicited stack type
STMDB sp!, {r0-r1} @ Save type and CPSR
@
@
#ifdef TX_ENABLE_EXECUTION_CHANGE_NOTIFY
@
@ /* Call the thread exit function to indicate the thread is no longer executing. */
@
BL _tx_execution_thread_exit @ Call the thread exit function
#endif
@
LDR r2, =_tx_timer_time_slice @ Pickup address of time slice
LDR r1, [r2, #0] @ Pickup current time slice
@
@ /* Save current stack and switch to system stack. */
@ _tx_thread_current_ptr -> tx_thread_stack_ptr = sp;
@ sp = _tx_thread_system_stack_ptr;
@
STR sp, [r6, #8] @ Save thread stack pointer
@
@ /* Determine if the time-slice is active. */
@ if (_tx_timer_time_slice)
@ {
@
MOV r4, #0 @ Build clear value
CMP r1, #0 @ Is a time-slice active?
BEQ __tx_thread_dont_save_ts @ No, don't save the time-slice
@
@ /* Save time-slice for the thread and clear the current time-slice. */
@ _tx_thread_current_ptr -> tx_thread_time_slice = _tx_timer_time_slice;
@ _tx_timer_time_slice = 0;
@
STR r4, [r2, #0] @ Clear time-slice
STR r1, [r6, #24] @ Save current time-slice
@
@ }
__tx_thread_dont_save_ts:
@
@ /* Clear the current thread pointer. */
@ _tx_thread_current_ptr = TX_NULL;
@
STR r4, [r5, #0] @ Clear current thread pointer
B _tx_thread_schedule @ Jump to scheduler!
@
@}
@@ -0,0 +1,97 @@
/***************************************************************************
* Copyright (c) 2024 Microsoft Corporation
* Copyright (c) 2026-present Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* SPDX-License-Identifier: MIT
**************************************************************************/
// Some portions generated by Claude Code (Opus 5).
/**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** Module Manager */
/** */
/**************************************************************************/
/**************************************************************************/
#define TX_SOURCE_CODE
#include "tx_api.h"
#include "txm_module.h"
/**************************************************************************/
/* */
/* FUNCTION RELEASE */
/* */
/* _txm_module_manager_alignment_adjust Cortex-R52 */
/* 6.1.8 */
/* AUTHOR */
/* */
/* Scott Larson, Microsoft Corporation */
/* */
/* DESCRIPTION */
/* */
/* This function adjusts the alignment and size of the code and data */
/* section for a given module implementation. */
/* */
/* INPUT */
/* */
/* module_preamble Pointer to module preamble */
/* code_size Size of the code area (updated) */
/* code_alignment Code area alignment (updated) */
/* data_size Size of data area (updated) */
/* data_alignment Data area alignment (updated) */
/* */
/* OUTPUT */
/* */
/* None */
/* */
/* CALLS */
/* */
/* None */
/* */
/* CALLED BY */
/* */
/* Initial thread stack frame */
/* */
/**************************************************************************/
VOID _txm_module_manager_alignment_adjust(TXM_MODULE_PREAMBLE *module_preamble,
ULONG *code_size,
ULONG *code_alignment,
ULONG *data_size,
ULONG *data_alignment)
{
/* The preamble is not read here, and the signature is not ours to change:
_txm_module_manager_internal_load calls this through a fixed prototype
shared by every port. Referenced and discarded so that the parameter is
used, which is what MISRA C:2012 Rule 2.7 asks for and what
-Wunused-parameter reports. A port that did have to grow or reposition a
module -- the Cortex-R4 one, below -- reads it. */
(VOID)module_preamble;
/* Rounding to the granule is all this has to do, and that is a property of
PMSAv8-R rather than a shortcut. The Cortex-R4 port's equivalent runs to
183 lines because PMSAv7 regions must be a power of two in size and
aligned to their own size, so a module's code and data have to be grown
and repositioned to fit the nearest legal region. Base and limit pairs
have no such constraint: any 64-byte-aligned extent is a legal region. */
/* Round code and data size UP to TXM_MODULE_MPU_ALIGNMENT bytes. */
*code_size = (*code_size + TXM_MODULE_MPU_ALIGNMENT - 1) & ~(TXM_MODULE_MPU_ALIGNMENT - 1);
*data_size = (*data_size + TXM_MODULE_MPU_ALIGNMENT - 1) & ~(TXM_MODULE_MPU_ALIGNMENT - 1);
/* Alignment for code and data is TXM_MODULE_MPU_ALIGNMENT bytes. */
*code_alignment = TXM_MODULE_MPU_ALIGNMENT;
*data_alignment = TXM_MODULE_MPU_ALIGNMENT;
}
@@ -0,0 +1,139 @@
/***************************************************************************
* Copyright (c) 2024 Microsoft Corporation
* Copyright (c) 2026-present Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* SPDX-License-Identifier: MIT
**************************************************************************/
/**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** Module Manager */
/** */
/**************************************************************************/
/**************************************************************************/
// Some portions generated by Claude Code (Opus 5).
#define TX_SOURCE_CODE
#include "tx_api.h"
#include "tx_thread.h"
#include "txm_module.h"
/* This handler is architecture-neutral: it terminates the faulting thread and
calls the notification callback. The fault registers are captured before it
runs, in the abort vector, because DFSR, DFAR, IFSR and IFAR must be read
before anything else can fault and overwrite them -- and on this core the
abort is taken in Abort mode with its own banked lr and sp, so the capture has
to happen there rather than here.
Data aborts and prefetch aborts both arrive here. A module can violate its
protection either way: writing outside its data region, or branching outside
its code region. Which pair of registers is meaningful depends on which it
was, and the fault info structure carries both.
THIS FUNCTION RETURNS, AND THAT IS A REQUIREMENT ON THE ABORT VECTOR.
It terminates the faulting thread and then calls the application's notify
callback. The second of those is only reached if _tx_thread_terminate returns,
and terminating the RUNNING thread returns only when the kernel believes it is
inside an exception: _tx_thread_terminate ends in
_tx_thread_system_preempt_check, which calls _tx_thread_system_return whenever
_tx_thread_system_state and _tx_thread_preempt_disable are both zero. On this
architecture _tx_thread_system_return switches context immediately and never
comes back, so the callback would be unreachable.
The body below is therefore left exactly as the cortex_m33, cortex_a7 and
cortex_m7 ports have it, and the requirement is met where it belongs -- in
txm_module_manager_fault_capture.S, which increments _tx_thread_system_state
around this call, clears _tx_thread_current_ptr afterwards and returns into
the scheduler. The Cortex-M ports need no such bracket because their
_tx_thread_system_return only pends PendSV and returns. Anyone tempted to
reorder the two statements below to "fix" a notify callback that does not fire
should look at the abort vector first: the ordering here is upstream's and it
is not the defect. */
/* Define the user's fault notification callback function pointer. This is
setup via the txm_module_manager_memory_fault_notify API. */
VOID (*_txm_module_manager_fault_notify)(TX_THREAD *, TXM_MODULE_INSTANCE *);
/* Define a macro that can be used to allocate global variables useful to
store information about the last fault. This macro is defined in
txm_module_port.h and is usually populated in the assembly language
fault handling prior to the code calling _txm_module_manager_memory_fault_handler. */
TXM_MODULE_MANAGER_FAULT_INFO
/**************************************************************************/
/* */
/* FUNCTION RELEASE */
/* */
/* _txm_module_manager_memory_fault_handler Cortex-R52 */
/* 6.1.8 */
/* AUTHOR */
/* */
/* Scott Larson, Microsoft Corporation */
/* */
/* DESCRIPTION */
/* */
/* This function handles a fault associated with a memory protected */
/* module. */
/* */
/* INPUT */
/* */
/* None */
/* */
/* OUTPUT */
/* */
/* None */
/* */
/* CALLS */
/* */
/* _tx_thread_terminate Terminate thread */
/* */
/* CALLED BY */
/* */
/* Fault handler */
/* */
/**************************************************************************/
VOID _txm_module_manager_memory_fault_handler(VOID)
{
TXM_MODULE_INSTANCE *module_instance_ptr;
TX_THREAD *thread_ptr;
/* Pickup the current thread. */
thread_ptr = _tx_thread_current_ptr;
/* Initialize the module instance pointer to NULL. */
module_instance_ptr = TX_NULL;
/* Is there a thread? */
if (thread_ptr)
{
/* Pickup the module instance. */
module_instance_ptr = thread_ptr -> tx_thread_module_instance_ptr;
/* Terminate the current thread. */
_tx_thread_terminate(_tx_thread_current_ptr);
}
/* Determine if there is a user memory fault notification callback. */
if (_txm_module_manager_fault_notify)
{
/* Yes, call the user's notification memory fault callback. */
(_txm_module_manager_fault_notify)(thread_ptr, module_instance_ptr);
}
}
@@ -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);
}
File diff suppressed because it is too large Load Diff
@@ -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