Gave each thread its own MPU window, and made a violation fault - #637
Merged
fdesbiens merged 1 commit intoAug 17, 2026
Merged
Conversation
First step towards a ThreadX module port for this core: establish that PMSAv8-R
regions can be switched per thread on this part, what that costs, and that a
violation actually faults. Those are the questions worth answering before
writing a module manager on top of them.
Two threads each own a 4 KB window at the top of DRAM2. The windows are carved
out of the broad data region in mpu.c, because isolation is only meaningful in
memory no other region already covers -- every other region in that map is a
wide RW window, so a private buffer inside one of them would be reachable by
every thread whatever else was programmed.
Each thread writes its own window, which must succeed, and then reaches for the
other thread's, which must fault. The second half is the part that matters: a
test that only shows a thread reaching its own memory would pass just as well
with no protection at all.
Measured on the S32Z280-594EVB, reproducible across three runs:
thread 0 window 0x3187E000 own: reachable other: faulted
thread 1 window 0x3187F000 own: reachable other: faulted
region switch cost: 562 to 604 cycles
The cost is worth noting for the module port to come. A context switch on this
part is about 1400 cycles, so switching one region adds roughly 40% to it, and
most of that is the dsb and isb rather than the register writes. A module switch
programming several regions should therefore batch the barriers once at the end
rather than per region.
Scope, stated plainly. The window is applied by the thread calling
thread_mpu_activate, not by the scheduler. The port's scheduler does call
_tx_execution_thread_enter under TX_ENABLE_EXECUTION_CHANGE_NOTIFY, which would
make it automatic, but that macro is read by port assembly compiled into the
shared threadx library, so enabling it would oblige all nine example targets in
this port to supply the four execution hooks. A ThreadX module port carries its
own copies of the port assembly for exactly that reason, and that is where the
switch belongs. There is no user mode, no syscall boundary and no loader here.
The fault is survivable the same way the boot probes make it survivable:
fault_expected tells the data abort handler to record the violation and resume
after the faulting access. That works in thread context because the handler
returns where it came from rather than to a fixed recovery point.
Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
fdesbiens
added a commit
that referenced
this pull request
Aug 18, 2026
…s result (#638) #636 reported that a stack in BTCM gave a threefold tighter spread than DRAM0 for stack-heavy work. That measurement used a single code placement, which is the methodology #631 and #633 exist to correct: the cache benchmark got an alignment sweep and the interrupt handler got one, and this measurement never did. It was noticed when #637 added two threads to the same image and the figure moved -- both spreads came out near 6500 and the minima rose 15%. The recursive body is now generated at four placements and all four are measured, per placement, in one image. placement BTCM min / spread DRAM0 min / spread offset 0 41854 / 6850 42036 / 6880 offset 16 48670 / 1946 48792 / 1978 offset 32 42388 / 6914 42752 / 7018 offset 48 48914 / 1860 49198 / 6786 Reproducible across runs to within a few hundred cycles. Spread is dominated by code placement rather than by the memory holding the stack. It ranges from 1860 to 6914 depending on where the body falls in a cache line, and placement also moves the minimum by 17%, from 41854 to 49214. Against that, the memory contributes a consistent but small advantage: BTCM's minimum is lower at all four placements, by 0.4% to 0.9%. BTCM's spread beats DRAM0's decisively at one placement of the four, offset 48, at 1860 against 6786. At the other three the two are within 2% of each other. So the effect #636 reported is real where it occurs and is not a property of the part: quoting it as one invited the reader to expect it everywhere. #636's claim should be read as qualified by this. A stack in BTCM buys a small consistent improvement in the best case and a large improvement in spread at some code placements and not others. Anyone building a determinism argument on it needs the placement sweep in the loop, not a single figure. The pad nops that displace each placement execute on every recursion level rather than once, so each placement carries a slightly different constant cost, about 0.6% at the widest. That cancels in the BTCM against DRAM0 comparison, which is made at the same placement, and does not affect spread within one. Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
fdesbiens
added a commit
to fdesbiens/threadx-fd
that referenced
this pull request
Aug 18, 2026
Three more port-specific sources: alignment adjustment, the fault notification
API, and shared external memory enable. All five C files now compile clean
against the module headers.
alignment_adjust is worth a note because of how little it does. It rounds code
and data size up to the 64-byte granule and sets both alignments to it, and that
is the whole function. 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. The comment says so, because the brevity looks like something
missing otherwise.
What remains is the assembly and an example, and that is the larger half:
- its own copies of the port assembly -- schedule, context save and restore,
stack build, system return -- so the region switch happens in the scheduler
rather than being called by the thread as in eclipse-threadx#637
- the module thread stack build and user-mode entry
- the abort vector's register capture, which has to read DFSR, DFAR, IFSR and
IFAR before anything else can fault over them, in Abort mode with its own
banked lr and sp
- the port dispatch
- a sample module and its build
None of that is a copy of the M33 equivalents. Armv8-M scheduling is built on
exception return with PSP and MSP and a 697-line scheduler; this core switches
ARM modes between EL1 and EL0, and its base scheduler is far smaller. The
assembly starts from the working Cortex-R52 port rather than from the M33 module
port, with the region load added.
Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
fdesbiens
added a commit
to fdesbiens/threadx-fd
that referenced
this pull request
Aug 18, 2026
…the module block
The Cortex-R52 TRM provides direct access to PRBAR0 through PRBAR15 and PRLAR0
through PRLAR15 (3.3.85, 3.3.86), encoded CRn c6, CRm c8 + n/2, with opc2 0 and 1
for an even region and 4 and 5 for an odd one. PRBAR and PRLAR without a number
are the indirect view that PRSELR selects, and PRSELR needs an ISB before the
region registers can be written -- once per region.
This was found while sizing the module manager's region switch and it changes the
design, so it is applied in both places.
Measured on the S32Z280-594EVB, same region and same isolation result either way:
through PRSELR 542 to 604 cycles
direct 434 to 470 cycles
The isolation test still passes, which is what confirms the encoding reaches the
region it is supposed to: each thread reaches its own window and faults on the
other. A wrong CRm would have programmed some other region and the fault would
have gone away.
What remains of the 434 is mostly the closing dsb and isb, the table search and
two counter reads rather than the two register writes. So a block of regions
written directly with one barrier pair at the end costs far less than the
per-region figure implies, where the PRSELR route pays an ISB every time. That is
the difference between a module switch that is affordable and one that is not:
eight regions through PRSELR would have added several thousand cycles to a
context switch of about 1400.
The module port's region block therefore moves from 9 through 16 to 8 through 15,
so all eight entries sit inside the directly addressable range. The board support
package's map occupies 0 through 7, so nothing has to move to make room. A board
needing more than eight kernel regions must either shrink the module block or
accept PRSELR for the overflow, and the header says so.
thread_mpu.c uses the direct path as well, which makes the per-thread window from
eclipse-threadx#637 about 22% cheaper. Its PRSELR accessors are kept and marked unused, because
regions above 15 have no direct encoding and a later board may need them.
Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
fdesbiens
added a commit
to fdesbiens/threadx-fd
that referenced
this pull request
Aug 18, 2026
…lt for it Six example images share this entry.S, so the wiring is behind TXM_MODULE_MANAGER and the other five see no change at all. SVC becomes the module boundary. It was a fault, because nothing in this port used it; under the guard it branches to __tx_module_svc_interrupt instead. The abort vectors are the interesting half, because they cannot simply be redirected. The boot probes provoke deliberate faults and rely on the existing recoverable path -- that is what X2 and X4 test -- while a module's violation has to reach the module manager. So both handlers now test who faulted first: SPSR's mode field being User means a module reaching outside its regions, and anything else continues down the path that was already there. r0 is pushed and popped around that test rather than simply used, and the reason is worth recording. The existing handler clobbers r0 through r2 immediately without saving them, which is tolerable for a probe that provokes its own fault and does not care what it resumes with. The module fault capture, though, records the faulting registers so an application can see what the module was doing, and it would have recorded ours. LDM does not affect the flags, so the comparison still holds after the pop. That existing clobber is a fragility this commit does not fix but should not leave unmentioned: any code recovering from a deliberate fault on this board resumes with r0, r1 and r2 destroyed. The boot probes survive it because their next actions do not use those registers, and the per-thread isolation test in eclipse-threadx#637 survives it for the same reason rather than by design. Verified both ways. All three existing targets build clean, entry.S assembles with the guard on, and the boot image still passes six of six probes on the board -- including X2 and X4, which are the two that exercise the abort handlers this commit touches. Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First step towards a ThreadX module port for this core. Before writing a module manager, three questions are worth answering on the actual part: can PMSAv8-R regions be switched per thread, what does that cost, and does a violation actually fault.
What it does
Two threads each own a 4 KB window at the top of DRAM2. The windows are carved out of the broad data region in
mpu.c, because isolation is only meaningful in memory no other region already covers — every other region in that map is a wide RW window, so a private buffer inside one would be reachable by every thread whatever else was programmed.Each thread writes its own window, which must succeed, then reaches for the other thread's, which must fault. The second half is the part that matters: a test that only shows a thread reaching its own memory would pass just as well with no protection at all.
Result
Reproducible across three runs:
The cost matters for what comes next
A context switch on this part is about 1400 cycles, so switching one region adds roughly 40%. Most of that is the
dsb/isbpair rather than the register writes, so a module switch programming several regions should batch the barriers once at the end rather than per region. Worth knowing before the module manager is designed around per-region barriers.Scope, stated plainly
The window is applied by the thread calling
thread_mpu_activate, not by the scheduler. The port's scheduler does call_tx_execution_thread_enterunderTX_ENABLE_EXECUTION_CHANGE_NOTIFY, which would make it automatic — but that macro is read by port assembly compiled into the sharedthreadxlibrary, so enabling it would oblige all nine example targets in this port, including the FVP ones, to supply the four execution hooks. A ThreadX module port carries its own copies of the port assembly for exactly that reason, and that is where the switch belongs.There is no user mode, no syscall boundary and no loader here. This is a step, not a substitute.
Fault recovery
The fault is survivable the same way the boot probes make it survivable:
fault_expectedtells the data abort handler to record the violation and resume after the faulting access. That works in thread context because the handler returns where it came from rather than to a fixed recovery point.Known issue this run exposed, not in this PR
The deep-stack figures from #636 do not reproduce in this image: BTCM spread is ~6,500 against DRAM0's ~6,800, where #636 measured 2,020 against 6,860, and absolute minima moved from 42,868 to 49,520. The cause looks like a gap in #636 —
deep_touchis a single placement, never given the alignment sweep that #631 applied to the cache benchmark and #632 to the handler. So #636's 3.4×-tighter-jitter claim should not be relied on until that measurement is alignment-swept. Filed here so it is not lost; it needs its own change.