Added Cortex-R52 port with Armv8-R AEM FVP example build - #579
Open
fdesbiens wants to merge 10 commits into
Open
Added Cortex-R52 port with Armv8-R AEM FVP example build#579fdesbiens wants to merge 10 commits into
fdesbiens wants to merge 10 commits into
Conversation
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>
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.
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_EL1skips 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.cmakeexists butports/cortex_a9/gnu/CMakeLists.txtis empty, so no A- or R-profile port could be built this way before.What is included
ports/cortex_r52/gnu/— 16 assembly sources +tx_port.h, seeded from the Cortex-R5 GNU portcmake/cortex_r52.cmake+ portCMakeLists.txt; soft/hard float, VFP, FIQ and IRQ/FIQ nesting optionsreadme_threadx.txt; a matching docs change is prepared separatelyVerification
All six images build warning-free and pass through
ctestonFVP_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
SCTLRback 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— addsenable_testing()at the top level. Without it,add_test()in a subdirectory generates aCTestTestfile.cmakethat the root never references, soctestreports 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:
PRBAR.APbit 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" is0x2. Programming four disjoint regions, one per encoding, and attempting a privileged write to each gave0b01and0b11faulting,0b00and0b10allowed. 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.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
CNTFRQat zero and the system counter stopped, so the BSP programs both;CNTHCTL.PL1PCTEN/PL1PCENandICC_HSREmust be set at EL2 or EL1 accesses trap; andtx_thread_vfp_enable()sets only a software flag — enabling the FPU (CPACR,FPEXC.EN) is the BSP's job. The FVP reportsMIDR 0x410FD0F0(part0xD0F), 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.casserts theTX_THREADoffsets 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_createreports a discardedconstqualifier fromcommon/inc/tx_api.hbecause the name parameter isCHAR *;demo_threadx.ckeeps the sample'sint 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.