Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ add_executable(s32z280_demo.elf EXCLUDE_FROM_ALL
${EVB_DIR}/gic_probe.c
${EVB_DIR}/cache.c
${EVB_DIR}/tcm.c
${EVB_DIR}/thread_mpu.c
${EVB_DIR}/demo_s32z280.c
)

Expand Down
124 changes: 124 additions & 0 deletions ports/cortex_r52/gnu/example_build/s32z280_evb/demo_s32z280.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
#include "linflexd.h"
#include "timer.h"
#include "cache.h"
#include "thread_mpu.h"
#include "board.h"

#define DEMO_STACK_SIZE 2048
Expand Down Expand Up @@ -388,6 +389,73 @@ static void deep_entry(ULONG which)
deep_done[which] = 1U;
}

/**************************************************************************/
/* Per-thread memory protection, demonstrated rather than asserted. */
/* */
/* Two threads each own a 4 KB window at the top of DRAM2, carved out of */
/* the broad data region in mpu.c so that nothing else grants access to */
/* them. 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 proves nothing about isolation: it */
/* would pass just as well with no protection at all. */
/* */
/* The fault is made survivable the same way the boot probes do it -- */
/* fault_expected tells the data abort handler to record the violation */
/* and resume at the instruction after the faulting access, rather than */
/* treating it as fatal. That works in thread context because the */
/* handler returns to where it came from rather than to a fixed recovery */
/* point. */
/**************************************************************************/

#define ISO_THREADS 2U

static TX_THREAD iso_thread[ISO_THREADS];

static unsigned char iso_stack[ISO_THREADS][DEMO_STACK_SIZE];

static volatile unsigned int iso_done[ISO_THREADS];
static unsigned int iso_own_ok[ISO_THREADS];
static unsigned int iso_other_faulted[ISO_THREADS];
static unsigned long iso_own_base[ISO_THREADS];

static void iso_entry(ULONG which)
{
extern unsigned int fault_expected;
extern unsigned int fault_taken;

unsigned long own = thread_mpu_window_base((unsigned int) which);
unsigned long other = thread_mpu_window_base((unsigned int) (1UL - which));
unsigned int before;
unsigned int pattern = 0xC0DE0000U + (unsigned int) which;

iso_own_base[which] = own;

/* Install this thread's window. */

thread_mpu_activate(tx_thread_identify());

/* Its own window: must be reachable. */

*((volatile unsigned int *) own) = pattern;
iso_own_ok[which] =
(*((volatile unsigned int *) own) == pattern) ? 1U : 0U;

/* The other thread's window: must not be. Only the write is attempted --
reading it back would fault a second time, and the point is already
made. */

before = fault_taken;
fault_expected = 1U;
*((volatile unsigned int *) other) = 0xBADU;
fault_expected = 0U;

iso_other_faulted[which] = (fault_taken > before) ? 1U : 0U;

iso_done[which] = 1U;
}

static void sleeper_entry(ULONG input)
{
unsigned long spinner_before;
Expand Down Expand Up @@ -443,6 +511,46 @@ static void judge_entry(ULONG input)

linflexd_puts("\n=== ThreadX on S32Z280: results ===\n");

linflexd_puts("per-thread MPU isolation\n");
{
unsigned int w;
unsigned int all_ok = 1U;

for (w = 0U; w < ISO_THREADS; w++)
{
linflexd_puts(" thread ");
demo_dec(w);
linflexd_puts(" window 0x");
linflexd_put_hex32((unsigned int) iso_own_base[w]);
if (iso_done[w] == 0U)
{
linflexd_puts(" did not finish\n");
all_ok = 0U;
}
else
{
linflexd_puts(iso_own_ok[w] ? " own: reachable" : " own: UNREACHABLE");
linflexd_puts(iso_other_faulted[w] ? " other: faulted\n"
: " other: REACHED\n");
if ((iso_own_ok[w] == 0U) || (iso_other_faulted[w] == 0U))
{
all_ok = 0U;
}
}
}

linflexd_puts(all_ok ? " PASS each thread reached its own window and faulted on the other\n"
: " FAIL isolation not demonstrated\n");

linflexd_puts(" region switch cost, cycles: min ");
demo_dec(thread_mpu_min_cycles());
linflexd_puts(" max ");
demo_dec(thread_mpu_max_cycles());
linflexd_puts(" switches ");
demo_dec(thread_mpu_switch_count());
linflexd_puts("\n");
}

linflexd_puts("stack-heavy work, cycles (24 frames, cold cache)\n");
{
static const char *const dwhere[CTX_PAIRS] = { "stack in BTCM ",
Expand Down Expand Up @@ -592,6 +700,22 @@ void tx_application_define(void *first_unused_memory)
ctx_dram_a_stack, DEMO_STACK_SIZE,
3U, 3U, TX_NO_TIME_SLICE, TX_AUTO_START);

/* Isolation pair. Priority 6 and 7, after the measurement threads. */

{
unsigned int w;

for (w = 0U; w < ISO_THREADS; w++)
{
(void) tx_thread_create(&iso_thread[w], "iso", iso_entry,
(ULONG) w,
iso_stack[w], DEMO_STACK_SIZE,
6U + w, 6U + w,
TX_NO_TIME_SLICE, TX_AUTO_START);
(void) thread_mpu_grant(&iso_thread[w], w);
}
}

/* Stack-heavy pair, after the context-switch pairs. */

(void) tx_thread_create(&deep_btcm_thread, "deep btcm", deep_entry, 0UL,
Expand Down
7 changes: 6 additions & 1 deletion ports/cortex_r52/gnu/example_build/s32z280_evb/mpu.c
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,13 @@ static void build_table(void)
bottom of the region -- no fault, just two objects on one address. */

mpu_regions[5].mpu_region_base = S32Z_DRAM2_BASE;
/* Stops short of the per-thread windows at the top of the bank. Those are
mapped one at a time by the thread-entry hook; leaving them inside this
region would make them reachable from every thread and the isolation
below would prove nothing. */

mpu_regions[5].mpu_region_limit = S32Z_DRAM2_BASE
+ S32Z_DRAM2_SIZE - 1UL;
+ S32Z_DRAM2_SHARED_SIZE - 1UL;
mpu_regions[5].mpu_region_ap = MPU_AP_RW_EL1;
mpu_regions[5].mpu_region_execute_never = 1U;
mpu_regions[5].mpu_region_shareability = MPU_SH_NON;
Expand Down
12 changes: 12 additions & 0 deletions ports/cortex_r52/gnu/example_build/s32z280_evb/platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@
#define S32Z_DRAM0_SIZE 0x00040000UL
#define S32Z_DRAM1_BASE 0x317C0000UL
#define S32Z_DRAM1_SIZE 0x00040000UL
/* 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
other region in that map is a wide RW window -- a private buffer placed inside
one of them would be reachable by every thread no matter what else was
programmed. */

#define S32Z_THREAD_WINDOW_BASE 0x3187E000UL
#define S32Z_THREAD_WINDOW_SIZE 0x00001000UL /* 4 KB per thread */
#define S32Z_THREAD_WINDOW_COUNT 2UL
#define S32Z_DRAM2_SHARED_SIZE (S32Z_THREAD_WINDOW_BASE - S32Z_DRAM2_BASE)

#define S32Z_DRAM2_BASE 0x31800000UL
#define S32Z_DRAM2_SIZE 0x00080000UL

Expand Down
197 changes: 197 additions & 0 deletions ports/cortex_r52/gnu/example_build/s32z280_evb/thread_mpu.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/***************************************************************************
* 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 */
/* */
/* thread_mpu.c Cortex-R52/GNU */
/* 6.5.2 */
/* AUTHOR */
/* */
/* Frédéric Desbiens, Eclipse Foundation */
/* */
/* DESCRIPTION */
/* */
/* Per-thread memory protection. See thread_mpu.h for what this is */
/* and is not. */
/* */
/* MISRA C:2012 deviations (justified) */
/* */
/* Directive 4.3 -- the MPU is only reachable through CP15; every */
/* access is encapsulated in a one-line accessor below. */
/* */
/**************************************************************************/

#include "tx_api.h"
#include "platform.h"
#include "mpu.h"
#include "timer.h"
#include "thread_mpu.h"

/* The region reprogrammed on every thread entry. mpu.c programs 0 to 7, and
MPUIR on this part reports 20, so 8 is free. Chosen high on purpose: the
shared regions keep their indices, so a thread that reaches this hook with no
window still has code, stacks, console and GIC exactly as before. */

#define THREAD_MPU_REGION 8UL

typedef struct
{
TX_THREAD *owner;
unsigned long base;
} thread_window_t;

static thread_window_t windows[S32Z_THREAD_WINDOW_COUNT];

static unsigned long switch_count;
static unsigned int last_cycles;
static unsigned int min_cycles = 0xFFFFFFFFU;
static unsigned int max_cycles;


/**************************************************************************/
/* CP15 accessors. */
/**************************************************************************/

static void write_prselr(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c6, c2, 1" : : "r"(value) : "memory");
}

static void write_prbar(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c6, c3, 0" : : "r"(value) : "memory");
}

static void write_prlar(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c6, c3, 1" : : "r"(value) : "memory");
}


/**************************************************************************/
/* Registration. */
/**************************************************************************/

unsigned long thread_mpu_window_base(unsigned int window)
{
if ((unsigned long) window >= S32Z_THREAD_WINDOW_COUNT)
{
return 0UL;
}

return S32Z_THREAD_WINDOW_BASE
+ ((unsigned long) window * S32Z_THREAD_WINDOW_SIZE);
}

unsigned long thread_mpu_grant(TX_THREAD *thread_ptr, unsigned int window)
{
unsigned long base = thread_mpu_window_base(window);

if ((base == 0UL) || (thread_ptr == TX_NULL))
{
return 0UL;
}

windows[window].owner = thread_ptr;
windows[window].base = base;

return base;
}


/**************************************************************************/
/* Activation. */
/* */
/* Called explicitly by a thread rather than from the scheduler, and that */
/* is a deliberate limitation of this step rather than the intended */
/* design. */
/* */
/* The port's scheduler does call _tx_execution_thread_enter under */
/* TX_ENABLE_EXECUTION_CHANGE_NOTIFY, which would make this automatic on */
/* every switch. That macro is read by the 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 this reason, and that is where */
/* the switch belongs. */
/* */
/* What this step establishes without that machinery: that a PMSAv8-R */
/* region can be reprogrammed per thread on this part, what it costs, and */
/* that a violation faults. Those are the questions worth answering */
/* before writing a module manager on top of them. */
/**************************************************************************/

void thread_mpu_activate(TX_THREAD *thread_ptr)
{
unsigned int before;
unsigned int after;
unsigned int delta;
unsigned int i;
unsigned long base = 0UL;

before = timer_read_cycles();

for (i = 0U; (unsigned long) i < S32Z_THREAD_WINDOW_COUNT; i++)
{
if (windows[i].owner == thread_ptr)
{
base = windows[i].base;
break;
}
}

write_prselr(THREAD_MPU_REGION);
__asm__ volatile("isb" ::: "memory");

if (base == 0UL)
{
/* No window for this thread: disable the region rather than leaving the
previous thread's window in place. Forgetting this is how per-thread
protection silently becomes no protection -- the last thread to run
would leave its window open to whatever ran next. */

write_prlar(0UL);
}
else
{
write_prbar((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)
| ((unsigned long) MPU_ATTR_NORMAL_WB << 1)
| 1UL); /* EN */
}

__asm__ volatile("dsb sy" ::: "memory");
__asm__ volatile("isb" ::: "memory");

after = timer_read_cycles();
delta = after - before;

switch_count++;
last_cycles = delta;
if (delta < min_cycles) { min_cycles = delta; }
if (delta > max_cycles) { max_cycles = delta; }
}


unsigned long thread_mpu_switch_count(void) { return switch_count; }
unsigned int thread_mpu_last_cycles(void) { return last_cycles; }
unsigned int thread_mpu_min_cycles(void) { return min_cycles; }
unsigned int thread_mpu_max_cycles(void) { return max_cycles; }
Loading
Loading