Skip to content

Added Cortex-R52 port with Armv8-R AEM FVP example build - #579

Open
fdesbiens wants to merge 10 commits into
eclipse-threadx:devfrom
fdesbiens:feature/cortex-r52-port
Open

Added Cortex-R52 port with Armv8-R AEM FVP example build#579
fdesbiens wants to merge 10 commits into
eclipse-threadx:devfrom
fdesbiens:feature/cortex-r52-port

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

Summary

Adds a ThreadX port for Arm Cortex-R52 (Armv8-R, AArch32) with the GNU toolchain, plus an example build for the freely available Armv8-R AEM FVP and an automated test suite.

The port is EL2-aware by construction. Cortex-R52 always implements EL2 and resets into it, so the reset path configures EL2, installs both the EL2 and EL1 vector tables, and only then drops to EL1 to run the kernel. Keeping that structure from the start is what allows partitioning work to reuse the boot path rather than replace it; TX_R52_BOOT_AT_EL1 skips the EL2 stage where a vendor EL2 monitor has already dropped privilege.

This is also the first R-profile port in the tree with a working CMake build. cmake/cortex_a9.cmake exists but ports/cortex_a9/gnu/CMakeLists.txt is empty, so no A- or R-profile port could be built this way before.

What is included

Area Notes
Kernel port ports/cortex_r52/gnu/ — 16 assembly sources + tx_port.h, seeded from the Cortex-R5 GNU port
Build cmake/cortex_r52.cmake + port CMakeLists.txt; soft/hard float, VFP, FIQ and IRQ/FIQ nesting options
Example BSP EL2→EL1 boot, GICv3, generic timer, PMSAv8-R MPU, semihosting and PL011 consoles
Tests 6 images registered with CTest; each judges itself and terminates the model
Docs readme_threadx.txt; a matching docs change is prepared separately

Verification

All six images build warning-free and pass through ctest on FVP_BaseR_AEMv8R, in three configurations: protection off, protection and caches on, and both together with hard-float VFP. Six build-option combinations (soft/hard float, VFP, FIQ, IRQ nesting, FIQ nesting) build clean and pass the tick-and-preemption demo at run time.

The tests assert behaviour rather than configuration, which is what caught the problems below. Specifically: the cooperative demo asserts the order of execution, because counting alone cannot distinguish working context switches from one thread running to completion; and the MPU test provokes real faults, because reading SCTLR back only proves a bit was set.

The 96-test regression suite is host-side (-m32, coverage, printf) and passes 96/96 in two configurations on the Linux port. It is not cross-run here: it validates portable kernel logic, while the port-specific risk is the assembly, which the FVP images exercise. Structural coverage is therefore not claimed from this port.

Things a reviewer should look at

Files outside ports/cortex_r52/ — three, all small:

  • cmake/cortex_r52.cmake — new toolchain file.
  • CMakeLists.txt — adds enable_testing() at the top level. Without it, add_test() in a subdirectory generates a CTestTestfile.cmake that the root never references, so ctest reports no tests from the build root.
  • .gitignore — build directories and __pycache__.

Two facts established by experiment, not from documentation, both recorded in the code and the readme because they are traps:

  1. PRBAR.AP bit order. The low bit is read-only and the high bit grants EL0 access — the reverse of the widely-published Armv8-R AArch64 macro set, whose "read-only, no EL0 access" is 0x2. Programming four disjoint regions, one per encoding, and attempting a privileged write to each gave 0b01 and 0b11 faulting, 0b00 and 0b10 allowed. Using the AArch64 ordering yields regions that report as read-only and silently accept writes, because region coverage is still enforced: an unmapped address faults while a "read-only" region does not.

  2. The timer PPI is INTID 30. Rather than assume it, bring-up enabled the whole PPI range 25–31 and recorded whichever INTID arrived in ICC_IAR1.

Model behaviour that a silicon port must revisit: the FVP leaves CNTFRQ at zero and the system counter stopped, so the BSP programs both; CNTHCTL.PL1PCTEN/PL1PCEN and ICC_HSRE must be set at EL2 or EL1 accesses trap; and tx_thread_vfp_enable() sets only a software flag — enabling the FPU (CPACR, FPEXC.EN) is the BSP's job. The FVP reports MIDR 0x410FD0F0 (part 0xD0F), i.e. an architecture envelope model rather than a Cortex-R52, so it validates architecture and not implementation. Nothing in the port is gated on MIDR.

Compile-time offset checks. tx_port_offset_check.c asserts the TX_THREAD offsets the assembly reaches by hard-coded displacement. This is the same class of defect fixed for Cortex-R4/R5 in #578: nothing in the toolchain ties those literals to the C structure. The assertion was verified to reject a wrong offset rather than merely to compile.

Known deviations, deliberately left: passing a string literal to tx_thread_create reports a discarded const qualifier from common/inc/tx_api.h because the name parameter is CHAR *; demo_threadx.c keeps the sample's int main() so it stays byte-identical to the shipped demo; and the floating-point test compares exactly on purpose, since a tolerance would mask a restored register that is close but wrong.

Not included

Modules support, split-mode SMP and any silicon-specific board support. The example targets the FVP only; NXP S32Z280 bring-up is separate work and the readme flags what to re-verify there.

Seeded ports/cortex_r52/gnu from the Cortex-R5 GNU port: 16 assembly
sources plus tx_port.h, with the release banners retargeted to
Cortex-R52/GNU. All 16 sources assemble unchanged for -mcpu=cortex-r52
in the soft-float, +nofp.dp and hard-float variants, so the AArch32
banked-mode context-switch core carries over from Armv7-R as-is; the
Armv8-R semantic work (EL2, PMSAv8-R, GICv3, generic timer) lands in
later milestones.

Added cmake/cortex_r52.cmake and ports/cortex_r52/gnu/CMakeLists.txt.
This is the first R-profile port in the tree with a working CMake+Ninja
build: cmake/cortex_a9.cmake exists but ports/cortex_a9/gnu/CMakeLists.txt
is empty, so no A- or R-profile port could be built this way before.

The toolchain file pins the reference cross compiler by absolute path
(arm-none-eabi 14.3.rel1) so the build does not depend on PATH ordering,
and honours -DARM_TOOLCHAIN_PATH for other compilers. Cortex-R52 always
implements at least a single-precision FPU -- GCC rejects "+nofp" for
this core and offers only "+nofp.dp" -- so the soft-float baseline
selects the soft ABI rather than removing the FPU.

Verified: libthreadx.a builds with zero warnings and the port objects
report Tag_CPU_arch v8-R with profile Realtime.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Cortex-R52 always implements EL2 and resets into it, so entry.S
configures EL2 first, installs both the EL2 and EL1 vector tables, and
only then drops to EL1 (Supervisor) to run the kernel. Keeping the port
EL2-aware from the outset is what lets ZoneX (roadmap AR3) reuse this
boot path rather than replace it. TX_R52_BOOT_AT_EL1 skips the EL2 stage
for targets where an earlier boot stage or a vendor EL2 monitor has
already dropped privilege.

Every exception routed to EL2 from EL1/EL0 arrives at the Hyp Trap Entry
at vector offset 0x14, not at 0x08: offsets 0x04-0x10 are exceptions
taken from Hyp mode itself, and 0x08 is SVC from Hyp. The handler
therefore decodes HSR.EC and dispatches, servicing HVC (EC 0x12) and
reporting anything else. That single funnel is the seam AR3 grows an
HSR.EC dispatch table on. HCR.HCD is cleared explicitly so HVC is
enabled rather than relying on its reset value, and HCPTR.TCP10/TCP11
are cleared so EL1 may use the FPU.

Images link into low DRAM at 0x0. The BaseR memory map is the Base
platform map with its two 2 GB halves swapped, and the high half is not
executable by default, so linking at 0x80000000 fault-loops silently
with no output.

boot_check.elf asserts the resulting exception level rather than merely
printing, because AR3 depends on that transition being correct: it
reports CPSR mode 0x13 (Supervisor/EL1) after the ERET from EL2 and
confirms the EL2 seam is entered and returns to EL1. Every unhandled
vector identifies itself over semihosting, because the FVP exposes only
an Iris server and no GDB stub, which makes a self-identifying fault the
primary debugging tool for this port -- it located the 0x14 vector
behaviour above on the first run.

Runs terminate themselves through semihosting SYS_EXIT, so the
run-boot-check-r52 target needs no host-side timeout.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
First execution of the ported context-switch assembly. tx_kernel_enter
now runs on Cortex-R52: _tx_thread_stack_build lays down each thread's
initial frame, _tx_thread_schedule restores it, and
_tx_thread_system_return saves a solicited context on every
tx_thread_relinquish. The Armv7-R sources carried over from the
Cortex-R5 port work unchanged for this path, which confirms the AArch32
banked-mode core needed no Armv8-R adjustment.

Added _tx_initialize_low_level for this board. It is markedly shorter
than the A-profile reference ports because entry.S already gives every
AArch32 mode its own stack from dedicated linker-script regions: there is
no stack carving to redo and no run-time overlap check to make, since the
linker guarantees the regions are disjoint. It records the system stack
and publishes _end as the first free memory. The periodic timer is left
alone deliberately -- the generic timer and GICv3 arrive in M3, and
raising a tick before an interrupt path exists would only produce
unhandled interrupts.

The demo asserts the execution *order*, not just iteration counts:
counting alone cannot distinguish working context switches from one
thread running to completion by itself, so both threads append a mark to
a trace buffer and the expected alternating sequence is checked. Only
tx_thread_relinquish is used, as tx_thread_sleep would hang with no tick.

Verified on FVP_BaseR_AEMv8R: five slices each, execution order
0101010101, all checks passed, model self-terminating.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Completes the interrupt path: the EL1 IRQ vector now branches into
_tx_thread_context_save, which returns to __tx_irq_processing_return,
where a C dispatcher acknowledges the interrupt, re-arms the timer, calls
_tx_timer_interrupt and ends with _tx_thread_context_restore.
_tx_timer_interrupt is safe to call from C: it touches only r0-r3 and
returns through lr. TX_R52_USE_THREADX_IRQ selects this wiring so images
that do not link ThreadX keep the self-identifying fault reporter.

Two model behaviours had to be handled, both established by reading the
hardware rather than assumed:

  - The system counter is stopped at reset (CNTCR = 0, CNTCV = 0). The
    model documents that firmware is expected to start it, so timer_init
    enables the counter control frame. Without this the timer never
    counts and no tick is ever delivered.
  - CNTFRQ resets to zero and is writable only at the highest implemented
    exception level, so entry.S programs it at EL2 with the frequency read
    back from CNTFID0 (100 MHz). Deriving a tick interval from an
    unprogrammed CNTFRQ would divide by zero.

entry.S also sets CNTHCTL.PL1PCTEN/PL1PCEN, without which every EL1
access to CNTPCT or CNTP_* traps to EL2, and ICC_HSRE.SRE/Enable so EL1
may reach the GICv3 CPU interface through its system registers.

The timer PPI was identified empirically, not assumed: bring-up enabled
the whole PPI range 25-31 and recorded whichever INTID arrived in
ICC_IAR1. The model drove 30, matching the architectural EL1 physical
timer assignment, so only that line is enabled now while the dispatcher
still reports any other INTID. This needs re-verifying on S32Z2 silicon.

All peripheral addresses in platform.h were verified in-model by reading
identification registers -- GICD_PIDR2 and GICR_PIDR2 both 0x3B (GICv3),
PL011 peripheral ID 0x11,0x10,0x24,0x00, CNTFID0 100 MHz -- rather than
taken from documentation. GIC setup follows the model's own reported
configuration: single security state, affinity routing fixed on, and five
implemented priority bits, so priorities are multiples of eight.

Verified on FVP_BaseR_AEMv8R: CNTFRQ 100 MHz, counter advancing, 20 timer
interrupts for a 20-tick sleep, tx_time_get advancing exactly 20 ticks,
and a lower-priority thread observed running during the sleep and then
being preempted. M1 and M2 still pass unchanged.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Runs the shipped eight-thread sample on Cortex-R52 and registers every
image as a CTest test, so the port now has a regression suite rather than
a set of manual runs.

demo_threadx.c is samples/demo_threadx.c kept byte-identical apart from a
header note, one include and one call into the verification harness. That
is the point of the milestone: the standard demo runs on this port with no
demo-side changes. The harness lives in demo_verify.c so the sample body
stays untouched, and it uses its own static stack so the demo's byte-pool
allocations are unperturbed.

The harness requires every one of the ten demo counters to have advanced,
rather than merely checking the system still runs: a lost wakeup or a
mishandled priority inversion would leave one thread's counter at zero
while everything else looked healthy. Observed counts are consistent with
the demo's design -- thread 0 completed 30 iterations of its 10-tick sleep
in 300 ticks, thread 5 tracked it through the event flag, and the threads
sleeping 2 ticks reached 75-76 -- which exercises the queue, semaphore,
mutex, event flags, byte pool and block pool across five priorities.

The test runner is deliberately far simpler than the RISC-V/QEMU one
because this platform permits it: the FVP has no GDB stub, only an Iris
server, and every image terminates itself through semihosting SYS_EXIT,
so there is nothing to script through a debugger. It launches the model,
captures the console and judges the self-reported result. A missing
result line counts as failure, never success, so a silent hang cannot
masquerade as a pass. Its failure paths were exercised deliberately:
hang detection and a bogus image both report a clear reason and exit
non-zero. Doing so found a real defect -- TimeoutExpired carries its
partial output undecoded even under text=True, so the hang path raised
TypeError inside its own error handler instead of reporting the hang.

enable_testing() is added to the root CMakeLists.txt. add_test() in a
subdirectory does generate a CTestTestfile.cmake there, but without the
top-level call the root never references it and ctest reports no tests at
all from the build root.

Note the five -Wextra unused-parameter warnings in demo_threadx.c are
inherited from the upstream sample and are left alone on purpose: silencing
them would mean the demo is no longer byte-identical. The normal build
remains warning-free.

Verified: all four images pass through ctest from the build root in about
six seconds.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The port could not actually be built with TX_ENABLE_VFP_SUPPORT. Its
assembly reads and writes the per-thread VFP enable flag as
[thread, eclipse-threadx#144], but every TX_THREAD_EXTENSION in tx_port.h was empty, so
no such member existed and those accesses would have landed on an
unrelated field. This is inherited from the Cortex-R5 port, which has the
same gap. TX_THREAD_EXTENSION_2 now carries tx_thread_vfp_enable, matching
the Armv7-A ports, and the tx_thread_vfp_enable/disable prototypes are
declared.

The field is defined unconditionally rather than under
TX_ENABLE_VFP_SUPPORT. Because the assembly reaches it by hard-coded
displacement, a conditional member would move every following field
between configurations and leave the offset correct in only one of them.

Added tx_port_offset_check.c, which asserts at compile time that the flag
really is at 144 and that the stack pointer and run counter are at 8 and 4
as the context-switch code assumes. The offset was measured, not assumed.
Nothing in the toolchain ties those literals to the C structure, so
without this a future member or build option would silently retarget the
accesses; now it fails the build instead. C99 has no _Static_assert, so
the check uses a negative array dimension, and the assertion was verified
to reject a deliberately wrong offset.

Enabling VFP then faulted on the first context switch touching
D-registers, with CPACR reading zero: tx_thread_vfp_enable() only sets the
software flag that makes the switch save floating-point state, it does not
turn the hardware on. Enabling the FPU is a BSP responsibility, so
entry.S now grants CP10/CP11 access through CPACR and sets FPEXC.EN at
EL1, complementing the HCPTR clearing already done at EL2. It is guarded
by __ARM_FP, which the compiler defines exactly when the build can emit
floating-point instructions; "vmsr fpexc" does not even assemble for this
target in a soft-float build.

Added CMake options for the floating-point and interrupt build variants:
TX_R52_ENABLE_VFP, TX_R52_ENABLE_FIQ, TX_R52_ENABLE_IRQ_NESTING and
TX_R52_ENABLE_FIQ_NESTING. All are PUBLIC because they change which
registers the assembly saves, so library and application must agree.
Requesting FIQ nesting without FIQ support is rejected outright rather
than quietly producing a half-configured build.

demo_m5.elf covers both halves of the VFP path, which use different
register sets: a checking thread holds eight doubles live across
tx_thread_sleep to exercise the solicited save of D8-D15, while a
lower-priority thread does continuous floating-point work so the tick
interrupts it mid-sequence and exercises the D0-D15 interrupt path. All
constants are exact binary fractions and the two threads use disjoint
ranges, so a single corrupted register or any leakage between contexts
shows as a mismatch rather than rounding noise.

Verified on FVP_BaseR_AEMv8R: all eight values preserved exactly across 50
solicited switches, and 780,761 iterations of the interrupted
floating-point thread with zero corruption. Six build configurations
(soft/hard float, with and without VFP, FIQ, IRQ nesting and FIQ nesting)
all build warning-free and pass the tick and preemption demo at runtime.
The five-image suite passes through ctest.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Completes the console item deferred from the tick milestone. Semihosting
stays the default because it needs no peripheral and therefore cannot be
broken by a wrong memory map, which is what makes it the right tool for
early bring-up. The UART matters because it is what real silicon uses:
exercising it now means the S32Z280 console should differ only in base
address and clocking, not in structure.

Selected with -DTX_R52_CONSOLE_PL011=ON. Both backends are always
compiled and console.c picks one at compile time, so --gc-sections drops
the unused one and every image builds in either configuration with no
per-target source juggling. UART0 is disabled at reset on this model
(bp.pl011_uart0.uart_enable=0), so the control register is programmed
before the first character; the baud divisor is deliberately left alone
because the model does not emulate serial timing and inventing a value
would only mislead. Initialisation happens on first use rather than from
board_init, so images with no ThreadX initialisation path -- the boot check
-- also get a working UART console.

console_exit keeps using semihosting even with the UART selected: SYS_EXIT
tells the model to stop, which is a debug-channel operation rather than
console output.

The run targets and the test runner now route UART0 to stdout
unconditionally, which is harmless for semihosting images and necessary
for UART ones.

Verified that the UART path is genuinely used rather than silently falling
back: with UART routing removed, the PL011 build prints nothing at all
while the semihosting build still prints every check line, and the PL011
build prints again once routing is restored. Both configurations pass the
whole four-image suite warning-free.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The per-thread VFP enable flag is reached from assembly by hard-coded
offset, so if the corresponding TX_THREAD member is missing the access
silently lands on whichever field occupies that offset. On the Cortex-R4
and R5 ports that is tx_thread_filex_ptr.

The demo now plants a sentinel in tx_thread_filex_ptr before enabling
floating point and verifies it afterwards, which turns that class of
mistake into a visible failure.

This check matters more than it looks. Reverting this port's
TX_THREAD_EXTENSION_2 to the state the R4/R5 ports are in today and
rerunning the demo shows every floating-point value still preserved
exactly and no corruption reported across interrupts -- while
tx_thread_filex_ptr reads 0x00000001, the value the enable routine writes
as its flag. The floating-point feature appears to work perfectly; the
only damage is to an unrelated pointer that nothing notices until FileX
is added. Without this check the demo would have passed a corrupting
build.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Completes AR1 milestone M5. The region set is described by a table rather
than a sequence of register writes, because AR2 programs a Stage-1 region
set per ThreadX module and AR3 programs Stage-2 regions per ZoneX
partition: a table can be swapped at a partition switch, open-coded
register writes cannot. Three regions are programmed -- code read-only and
executable, everything writable non-executable, peripherals as Device --
and any regions this image does not use are explicitly disabled so a stale
enable cannot grant access.

The self-test provokes real faults instead of reading SCTLR back, and that
distinction earned its keep three times over. Every problem below passed a
configuration-only check and was caught only by attempting an access that
should be refused.

First, the access-permission encoding. Programming four disjoint regions,
one per AP value, and attempting a privileged write to each gave:

    AP=0b00  write allowed        AP=0b01  write faulted
    AP=0b10  write allowed        AP=0b11  write faulted

so the low bit of PRBAR.AP is read-only and the high bit grants EL0
access. That is the reverse of the widely-published Armv8-R AArch64 macro
set, whose "read-only, no EL0 access" is 0x2. Using the AArch64 ordering
produces regions that report as read-only and accept writes, because the
MPU still enforces region coverage: an unmapped address faults while a
"read-only" region does not. The values are documented as calibrated and
flagged for re-calibration on silicon.

Second, a memory layout error. Writes to the first two variables in .data
faulted, because .data began at 0x44f4 while the code/data boundary symbol
reported 0x4500, so twelve bytes of writable data sat inside the read-only
code region -- including the flag the abort handler consults, which turned
the fault fatal. Aligning the output section address with ".data ALIGN(64) :"
fixes it. Neither an assignment between output sections nor an ALIGN at the
end of the preceding section works: the former does not advance a MEMORY
region's allocation pointer, and the latter does not extend a section when
no data follows it.

Third, a build system gap that hid the two attempted fixes. The linker
script is passed with -T, which CMake does not track, so editing it did not
relink and each attempt was tested against a stale image. LINK_DEPENDS is
now declared for every image.

The data-abort handler gained a recoverable path used only by the
self-test: when mpu_expect_abort is set the fault is counted and execution
resumes at the instruction after the faulting one, since LR_abt holds the
faulting address plus 8 and returning to LR_abt-4 skips it where LR_abt-8
would retry forever.

Verified on FVP_BaseR_AEMv8R: 32 regions available, 3 programmed and read
back from hardware, MPU and both caches enabled, a legal write accepted, an
unmapped write refused, a write to read-only code refused with the contents
unchanged, and the kernel healthy afterwards. The full suite passes with
protection and caches off, with them on, and with them on together with
hard-float VFP -- six images, no warnings.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Applied the strict warning set that upstream uses for the kernel -- including
-Wmissing-prototypes, -Wconversion, -Wshadow, -Wcast-qual and
-Waggregate-return -- to every C source in the port and its board support
package. Two real findings, both in code called from assembly and therefore
lacking any visible declaration, which MISRA C:2012 Rule 8.4 prohibits:
board_init and board_irq_handler.

Added board.h to declare everything that crosses the C/assembly boundary --
bsp_main, board_init, board_irq_handler, the interrupt observability
counters, the hypercall counter and the recoverable-abort flags -- so that a
signature change cannot silently disagree with the assembly that calls it.
All port and board sources are now clean under that warning set.

The findings that remain are not port defects and are left alone
deliberately. Passing a string literal to tx_thread_create reports a
discarded const qualifier from common/inc/tx_api.h, because the name
parameter is CHAR * rather than const CHAR *; that is an upstream API
signature, not something this port can fix. demo_threadx.c reports a
non-prototype declaration for the sample's own "int main()", and correcting
it would break the byte-identity with the shipped sample that the
demonstration exists to prove. The exact floating-point comparisons in
demo_m5.c are the point of that test rather than an oversight, and are now
documented as a deviation: a tolerance would mask a restored register that is
close but wrong.

Added readme_threadx.txt covering the build and its options, the EL2-aware
initialisation and which registers must be programmed before dropping to
EL1, the interrupt path including the Hyp Trap Entry funnel, lazy floating
point and the division of responsibility for enabling the FPU, and memory
protection. It records the two traps most likely to catch the next person:
the calibrated PRBAR.AP encoding, whose bit order is the reverse of the
published AArch64 macros and which yields writable "read-only" regions if
copied from there, and the linker-script alignment needed to keep writable
data out of the read-only code region.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
@fdesbiens fdesbiens self-assigned this Aug 6, 2026
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