Skip to content

Gave each thread its own MPU window, and made a violation fault - #637

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feature/r52-s32z280-thread-mpu
Aug 17, 2026
Merged

Gave each thread its own MPU window, and made a violation fault#637
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feature/r52-s32z280-thread-mpu

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

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:

thread 0 window 0x3187E000  own: reachable  other: faulted
thread 1 window 0x3187F000  own: reachable  other: faulted
PASS each thread reached its own window and faulted on the other
region switch cost, cycles: 562 to 604

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/isb pair 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_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, 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_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.

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 #636deep_touch is 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.

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
fdesbiens merged commit 1ef4933 into eclipse-threadx:dev Aug 17, 2026
4 checks passed
@fdesbiens
fdesbiens deleted the feature/r52-s32z280-thread-mpu branch August 17, 2026 21:59
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant