summaryrefslogtreecommitdiff
path: root/common_modules/module_manager/src/txm_module_manager_kernel_dispatch.c
diff options
context:
space:
mode:
authorFrédéric Desbiens <[email protected]>2026-08-12 10:25:35 -0400
committerGitHub <[email protected]>2026-08-12 10:25:35 -0400
commit990d51b65d7559e6d82329b0479a5219a8c4f091 (patch)
tree23064d108fbe39ad6082c870cee15766f5a8e947 /common_modules/module_manager/src/txm_module_manager_kernel_dispatch.c
parentb0ad67bae9d99fba90f0d4b8f9c98172f30841e4 (diff)
Brought up ThreadX on S32Z280 silicon and fixed the MPU encoding it exposed (#606)
* Added an S32Z280-594EVB example build for the Cortex-R52 port First silicon bring-up for the Cortex-R52 port: an image that boots RTU0 core 0 on the NXP S32Z280-594EVB, drops from EL2 to EL1 and reports what the core says about itself. Gated behind TX_R52_BUILD_S32Z280_EXAMPLE, separate from the FVP example option so that FVP regression is unaffected by bring-up work and the two boards' differing reset states cannot interact. Verified on the board. The image runs from reset and reaches its completion breakpoint, and the two CPSR values are the point of the exercise: mode 0x1A (Hyp) at EL2 then 0x13 (Supervisor) at EL1, i.e. the EL2 to EL1 drop performed by this code on silicon rather than on a model. MIDR 0x411FD133 Cortex-R52 r1p3, part 0xD13 MPUIR 0x00001400 20 MPU regions at EL1 HMPUIR 0x00000014 EL2 CTR 0x8144C004 MPIDR 0x80000000 ID_PFR0 0x00000131 ID_PFR1 0x10111001 virtualisation field 1: EL2 implemented SCTLR 0x70C50838 MPU, D-cache and I-cache all off at reset CNTFRQ 0x00000000 The FVP reports MIDR 0x410FD0F0, part 0xD0F, which is an architecture envelope model and not a Cortex-R52 -- so these are the first implementation-specific numbers the port has had. Three silicon facts the code exists to encode, each of which presents as working hardware that quietly does the wrong thing: - The core resets in THUMB state. CPSR reads 0x1FA out of reset because the boot instruction NXP plants at the boot address is a T32 branch, so _start is T32 and switches to A32 itself. An A32 entry would execute the first halfword of its own instruction as Thumb. - The debugger holds every core in debug state. NXP's _reset_to_first_instruction() asserts MDM_AP CONTROL2[19:16] = CR52_RTU0_{3,2,1,0}_EDBGREQ and never clears them, though its own comment says start_debug_by_core_name() does. While asserted the core executes nothing, yet registers and memory still respond and MC_ME/RGM report the core released and clocked. tools/read_identity.gdb clears core 0's bit and verifies the clear took. - CNTFRQ reads zero, exactly as on the FVP, and is writable only at the highest implemented exception level. entry.S records it rather than writing a value, because the correct frequency for this board is not yet established and a wrong one would silently mis-scale every derived interval. Timer work must program it first. Memory map: .text is linked and loaded at 0x79900000, the instruction-fetch window and the reset address (MC_ME_PRTN0_CORE0_ADDR reads exactly that). It is writable over the debug AXI port despite NXP's map declaring it read-only, so no load-address alias is needed for a debugger-loaded image; a flash-booted one would use the data alias at 0x32100000, which is the same physical SRAM as confirmed by a sentinel write. Data, bss and the per-mode stacks go to RTU-local data SRAM at 0x31780000. The TCMs are absent on purpose: they are inaccessible at reset until their region registers are programmed, so nothing needed for early boot can live there. Reporting is built so that a failure cannot read as a success: boot_stage distinguishes a fault from a hang and fault handlers record vector, syndrome and address before parking; r52_identity.magic is written last so a half-filled structure is detectable; and bsp_done() is a deliberate breakpoint target so completion is observed rather than inferred from a timeout. The image does not link ThreadX, so a failure here is unambiguously a boot or board problem rather than a kernel one -- the same reasoning as the FVP's boot_check.elf. Not included: console, timer, GIC, MPU programming. LIN9 reaches the host through the daughtercard USB-UART, which is LINFlex_9 at 0x42980000 and not LINFlex_0; what is missing is the clock configuration needed to compute a baud rate, and a console at the wrong rate produces garbage indistinguishable from a crash. Hence reporting through memory for now. FVP example builds and passes 6/6 unchanged. Assisted-by: Claude Code (Opus 5) <[email protected]> * Added a LINFlexD console to the S32Z280-594EVB example Polled transmit console on LINFlex_9, which is the instance wired to the daughtercard USB-UART through jumper J248. Not LINFlex_0: that is merely the first instance in the Reference Manual's list and reaches no connector on this board. bsp_boot.c now prints the identity registers as well as recording them in memory; the memory copy stays, because it is what proved the boot path before a console existed and it still works if the console is misconfigured. The baud rate was derived rather than assumed, which mattered because a console at the wrong rate produces garbage indistinguishable from a crash: - LINFlex_9 is clocked by P5_LIN_BAUD_CLK, driven by MC_CGM_5 MUX 2. - Read from the board: MUX_2_CSS selects source 2, MUX_2_DC_0 divides by 1. - The EVB carries a 40 MHz crystal (UG10268 section 3.3.1.1), so 40 MHz. - LFDIV = 40000000 / (16 * 115200) = 21.7014, giving LINIBRR 21 and LINFBRR 11, for an actual 115274 baud -- 0.06% error. Those are exactly the values the BootROM had already left in the registers, which is the corroboration for the 40 MHz figure rather than merely a plausible-looking calculation. They are programmed here anyway instead of inherited, for two reasons. Inheriting register state makes this image's behaviour depend on how the board was last booted. And the BootROM's own configuration is wrong for a console: it leaves PCE set and TxEn clear, because it is listening for a serial-boot download rather than printing, so a host terminal on 8N1 would see framing errors. Verified on the board: the image reaches its completion breakpoint with the console calls in the path, so every byte was accepted and UARTSR.DTF was set for each -- the transmitter is configured, enabled and clocked. That does not by itself prove the rate, since DTF sets at any baud; the rate rests on the arithmetic above and the BootROM's independently matching divisors. Observing the text needs the daughtercard USB-UART connected to a host terminal at 115200 8N1. Register offsets and bit positions are from Reference Manual section 75.5.1 and the UARTCR/UARTSR diagrams. PCE at bit 2 and TxEn at bit 4 are called out in the source because they are easy to transpose and the failure is silent. Assisted-by: Claude Code (Opus 5) <[email protected]> * Fixed two silent failures in the S32Z280 LINFlexD console The console transmitted from the first attempt, but what arrived on the wire was wrong in two independent ways, neither of which the target could detect. Both are now fixed and the output is byte-for-byte correct, captured from the USB-UART rather than read off a terminal. 1. UARTCR was written outside initialisation mode. Setting LINCR1.INIT does not put the module in initialisation mode in the same cycle, and most of UARTCR is writable only there. Configuring immediately after the LINCR1 write half-worked: bits being *set* took effect while bits being *cleared* did not. TxEn came on, but PCE stayed on, so the line ran 8E1 against a host expecting 8N1. That corrupts only those characters whose parity bit happens to be 0 and leaves the rest readable, which looks like a marginal baud rate rather than a framing error -- and would have sent the next hour into the clock tree. linflexd_init() now polls LINSR[LINS] until the module reports initialisation mode, reads UARTCR back afterwards, and returns a status mask. bsp_boot.c records it and prints it as CONSOLE; 0 means both the mode entry and every field write were confirmed rather than assumed. 2. DTF was cleared after the byte rather than waited on. DTF is write-one-to-clear and does not de-assert in the same cycle as the clearing write, so the next byte's poll could observe the previous byte's flag, conclude the line was free while it was still busy, and have its own write silently discarded. That cost exactly one character after every "\r\n" pair -- the only place two bytes go out back to back -- so the first letter of every line went missing: MIDR read as IDR, SCTLR as CTLR. The transmit sequence is now write, wait for DTF, clear DTF, then wait for the clear to take effect, with the same bounded guard as the init loop so a stuck flag degrades to slow output rather than a hung boot. Clearing before the write was tried and is worse, not better: the write then lands while the previous byte is still shifting and is dropped, and the poll afterwards sees the previous byte's completion, so most of the output disappears. That failure is what established that the transmitter discards writes while busy, which is the fact both bugs turn on. The source says so, because the ordering looks arbitrary otherwise. Verified end to end on the board, with the USB-UART passed through to WSL2 so the received bytes could be compared against what the code intended to send: === ThreadX Cortex-R52 :: NXP S32Z280-594EVB === MIDR = 0x411FD133 MPUIR = 0x00001400 HMPUIR = 0x00000014 CTR = 0x8144C004 MPIDR = 0x80000000 ID_PFR0 = 0x00000131 ID_PFR1 = 0x10111001 SCTLR = 0x70C50838 CNTFRQ = 0x00000000 CPSR@EL2 = 0x000001DA CPSR@EL1 = 0x600001D3 EL1 MPU regions: 00000014 CONSOLE = 0x00000000 === boot complete === Reaching the completion breakpoint proved only that the transmitter ran; it could not have caught either fault. Comparing bytes is what did. Assisted-by: Claude Code (Opus 5) <[email protected]> * Added generic timer support to the S32Z280-594EVB example The Arm generic timer now runs on silicon: CNTFRQ is programmed at EL2 and a one-shot compare fires when it should. CNTFRQ programmed = 0x007A1200 (8000000) elapsed counts = 8000043 (requested 8000000, +0.001%) host interval = 1.0033 s Both halves matter. The elapsed count shows the counter and the compare agree with each other; the host interval shows the rate is actually 8 MHz rather than merely self-consistent, which a wrong CNTFRQ would also produce. The residual 43 counts is about 5 us of polling overhead. CNTFRQ reads zero out of reset here, exactly as on the Armv8-R AEM FVP -- so that FVP behaviour is real silicon behaviour, not a model artefact, and the port readme's warning to re-verify it was well placed. The parallel stops there: the FVP also leaves the system counter itself stopped and its BSP must start it, whereas on this board the BootROM has the counter running and only the software-declared constant was missing. CNTFRQ is writable only at the highest implemented exception level, so entry.S programs it before the drop to EL1. 8 MHz was established three independent ways rather than assumed: - Measured: CNTPCT sampled against host wall-clock time over a 32-second interval gave 8.0227 MHz. - Derived: RTU.GPR CFG_CNTDV reads 4, so the divider is (4+1) = 5, and the board's FXOSC is 40 MHz -- itself already corroborated by the LINFlexD baud divisors the BootROM left behind. 40 / 5 = 8. - Confirmed: the one-shot compare above. The compare is polled with the interrupt masked, deliberately. There is no GIC configured yet, and proving the timer counts and fires first means a later interrupt failure cannot be confused with the timer itself being wrong -- the same staging that made the console tractable. Also recorded, from a detour that cost a run: RTU0.GPR CFG_CNTDV at 0x76120010 is readable by the debugger, which returns 4, but a load from the core at EL1 kills the image. No fault handler runs, boot_stage stays at 4, and the debug connection drops -- the signature of a stalled bus access rather than an abort, and a failure mode that leaves nothing on-target to inspect. The debugger reaches that window over the AXI-AP, which does not go through whatever gates the core's own access to RTU peripheral space. The image no longer touches it; the value lives in platform.h instead. This is the second address on this part that is debugger-visible but not core-reachable, so peripheral windows are now worth probing from the core before relying on them. Assisted-by: Claude Code (Opus 5) <[email protected]> * Enabled the R52 peripheral port and located the GIC on S32Z280 Two peripheral windows on this part stalled the core outright when read: no abort, no fault handler, boot_stage frozen at its last value, and the debug connection dropping with it. A stall gives you nothing on-target to inspect, so both were found by printing a marker before each access and seeing which marker came last. The Cortex-R52 TRM r1p3 (100026_0103_00_en) explains both; NXP's reference manual mentions neither. 1. The low-latency peripheral port is disabled at reset. IMP_PERIPHPREGIONR (TRM 3.3.80) describes a region at 0x76000000 of 4 MB on this part -- the RTU peripheral space -- with separate enables for EL2 and EL1/0. Read from the board it was 0x76000034: base 0x76000000, size 0b01101 = 4 MB, and both enable bits clear. The TRM is explicit that each "resets to 0". Until they are set, every access in that window stalls. entry.S now sets both at EL2, which is also where it has to happen: EL1 writes to this register trap to EL2 when HACTLR.PERIPHPREGIONR is clear. PERIPHPRG now reads 0x76000037 and the core reads RTU0.GPR CFG_CNTDV = 4 for itself -- the same value the debugger saw, which independently confirms the divider behind the 8 MHz counter rate from the core's own view rather than the debug path's. 2. The GIC is where NXP says, and needs an MPU mapping to reach. IMP_CBAR (TRM 3.3.17) holds the physical base of the memory-mapped GIC distributor in bits [31:21], its reset value wired from CFGPERIPHBASE. Read from the board it is 0x47800000, confirming NXP's memory map from the hardware rather than from a vendor debugger script -- the reference manual contains no GIC base address anywhere. NXP's cryptic note that the "R52 Cluster set addr [31:21]" is just CFGPERIPHBASE[31:21] restated. TRM Table 9-1 gives the frame layout relative to that base: distributor at +0x000000, redistributor control at +0x100000, redistributor SGI/PPI at +0x110000, then +0x20000 per further core. That matches what the FVP example's driver already assumes, so it should port with address changes. The distributor still stalls, and the TRM says why: "Ensure that the memory region used for the GIC Distributor is configured as Device nGnRnE." The MPU is disabled here -- SCTLR.M reads 0 -- so nothing maps that region. Enabling the peripheral port does not help, the GIC being outside that window and reached over AXIM. Reading it waits on MPU programming, which is the next piece of work; the probe is removed rather than left in, since while present it stalled every run and cost the console and timer results that precede it. gic_probe.c holds the two system-register reads. They cannot hang the bus, unlike the memory accesses they diagnose, which is the point of them. Assisted-by: Claude Code (Opus 5) <[email protected]> * Reached the GIC on S32Z280 by mapping it Device nGnRnE The GIC distributor and redistributor now respond on silicon: MPU rgns = 0x00000003 SCTLR = 0x70C50839 (M set; caches still off) GICD_PIDR2 = 0x0000003B architecture revision 3 = GICv3 GICD_TYPER = 0x0248001E GICR_PIDR2 = 0x0000003B redistributor at base + 0x100000 Three independent sources had to agree to get here, and NXP's reference manual supplied none of them. IMP_CBAR reports the distributor base from the hardware (0x47800000). Cortex-R52 TRM Table 9-1 gives the frame layout: distributor at +0x000000, redistributor control at +0x100000, SGI/PPI at +0x110000, then +0x20000 per further core -- the layout the FVP example's gicv3.c already assumes. And the TRM requires the region be Device nGnRnE, which was confirmed rather than taken on faith: mapped Normal write-back the distributor stalls the core outright, mapped Device it reads 0x3B. mpu.c is ported from the FVP example, keeping its PRBAR/PRLAR encoding and the reversed-AP-bit-order finding, with an S32Z280 region table. Caches are left off: the point of this pass was reaching the GIC, and the debugger writes this image straight into SRAM behind the caches, so enabling C and I deserves its own step and its own check. Diagnosis needed two new pieces of machinery, both kept: - Memory-resident progress markers (probe_stage), mirroring the console markers. Enabling the MPU can take the core down in a way that records no fault AND takes the console with it, so boot_stage alone could not say how far a risky sequence got. With markers inside mpu_init the failure went from "somewhere in the MPU code" to "the SCTLR.M write itself" in one run. - Resolving symbol addresses from the current ELF with nm on every post-mortem. Adding probe_stage to the .data block shifted fault_vector, so reusing addresses across builds reads the wrong words -- which nearly produced a conclusion from a stale read. OPEN QUESTION, recorded in mpu.c rather than papered over. A five-region map that adds a second Device window for the RTU peripheral space (0x76000000, 4 MB) makes the SCTLR.M write kill the core: all regions program without error, probe_stage reaches 0x42, and the marker one instruction later never lands. No exception is taken, so it stalls rather than aborts. Coverage is not the explanation -- that map spanned the whole address space, and an earlier comment of mine claiming otherwise was wrong and has been corrected. The cause is not known. Consequence: RTU peripheral access after the MPU is enabled is untested; the CFG_CNTDV read this image does happens before the enable. Also not tightened yet: code is writable and data executable in this map. Wrong for a protection demo, right for bring-up where an unmapped address costs a silent stall rather than a fault. Narrowing it belongs with the ThreadX integration, which can verify it. Assisted-by: Claude Code (Opus 5) <[email protected]> * Fixed an out-of-bounds MPU table write that made enabling the MPU stall the core The five-region map now programs correctly and the MPU enables with the GIC reachable: R idx PRBAR PRLAR R 0 00000000 477FFFC1 R 1 47800002 479FFFC3 GIC, XN, Device nGnRnE R 2 47A00000 75FFFFC1 R 3 76000002 763FFFC3 RTU peripherals, XN, Device nGnRnE R 4 76400000 FFFFFFC1 MPU rgns = 5, SCTLR = 0x70C50839 (M set) GICD_PIDR2 = 0x3B, GICD_TYPER = 0x0248001E, GICR_PIDR2 = 0x3B The bug was mine and it was simple: mpu_regions[] is declared with a fixed size, and the FVP example this file came from sized it [3] to match the three regions it programs. A five-region table wrote mpu_regions[3] and [4] past the end of the array. What made it hard to see is what the corruption produced. Every region appeared to program without error, and the failure was a stall on the SCTLR.M write -- no abort, no fault handler, no exception, and the debug connection dropping. Reading the regions back out of the hardware, with the MPU deliberately left disabled so the readback could not itself stall, showed region 3 with base 0x00000000 instead of 0x76000000. With its correct limit that region spanned 0x00000000-0x763FFFFF and overlapped regions 0 to 2, and PMSAv8-R leaves overlapping regions UNPREDICTABLE -- so stalling was permitted behaviour rather than a hardware fault. One bug accounts for every observation: three regions worked, five failed, and it failed identically whether the extra window was Device or Normal, because the memory type was never involved. Fixes: the table capacity is now named (MPU_TABLE_REGIONS) and sized 16, and mpu_init checks mpu_regions_used against it as well as against MPUIR. The existing check only compared against the hardware's region count, which said 20 and told us nothing about the array. Two earlier claims of mine, corrected here rather than left standing. A comment asserting "narrow coverage was the problem" was wrong: the failing map covered the whole address space. And this is NOT a defect inherited from the FVP example -- that code declares [3] and uses three regions, entirely self-consistent. Worth offering upstream only as hardening: the bare [3] with no bound check is what let a growing map overrun it silently. The region readback is kept in the image. It costs eight lines a boot and verifies the map against the hardware every time, which is precisely what turned an unexplained stall into a one-line diagnosis. Assisted-by: Claude Code (Opus 5) <[email protected]> * Enabled interrupt-driven ticks by clearing SCTLR.TE Timer interrupts are delivered, acknowledged and re-armed on silicon: SCTLR = 0x30C50838 -> 0x30C50839 (TE cleared, M set) IRQ count = 7 timer INTID = 30 measured, not assumed spurious = 0 unexpected = 0 SCTLR.TE -- Thumb Exception enable -- RESETS SET on this part. Every vector table in entry.S is A32, so the core entered each exception in T32 state, decoded an A32 branch as Thumb, landed mid-instruction at a misaligned address and took an undefined-instruction exception, which repeated the same way. No handler ever ran. entry.S now clears it at EL1 and clears HSCTLR.TE at EL2. The consequence was worse than a crash: it made every fault invisible. boot_stage stayed at its last value, fault_vector stayed zero, and the core sat at el1_vectors+4. A data abort from an unmapped peripheral, an abort from an overlapping MPU region, and a correctly delivered timer interrupt all presented identically -- an unexplained hang with nothing recorded. What gave it away was reading the banked registers: LR_irq and SPSR_irq showed an IRQ had been taken from SVC with interrupts enabled, CPSR showed UND mode with T set, and LR_und pointed at a misaligned address inside the A32 vector table. The Armv8-R AEM FVP resets with TE clear, which is why the same vector tables work there and why nothing in the FVP work anticipated this. This commit also corrects earlier comments of mine. Several places described these failures as "a stalled bus access rather than an abort", with the absence of a recorded fault as the evidence. That was wrong: they were ordinary exceptions whose handlers were unreachable. The two underlying bugs -- the peripheral port disabled at reset, and the out-of-bounds MPU table write -- were real and are correctly fixed, but the explanation for why they presented so mutely was not. Fixed in platform.h, mpu.c and bsp_boot.c. Interrupt plumbing: gicv3.c is ported from the FVP example with the frame bases in platform.h, derived from IMP_CBAR and TRM Table 9-1. el1_irq_entry in entry.S uses the classic A32 form rather than ThreadX's context save/restore, since this image does not link the kernel. irq_dispatch.c counts ticks instead of calling _tx_timer_interrupt, and keeps separate spurious and unexpected-INTID counters so that "no interrupt arrived" and "an interrupt arrived and was mishandled" cannot be confused. timer_start_oneshot_irq arms with IMASK clear; the polled timer_start_oneshot keeps IMASK set, and the two are separate functions because either mistake is silent. Assisted-by: Claude Code (Opus 5) <[email protected]> * Ran ThreadX on S32Z280 silicon with threads, tick and preemption The kernel runs on the board: === ThreadX Cortex-R52 :: S32Z280-594EVB === console = 0x00000000 entering kernel === ThreadX on S32Z280: results === ticks = 0x00000064 100 ticks sleeper = 0x00000014 20 wakeups spinner = 0x00159B48 1415496 iterations preempt = 0x00000014 20 preemptions PASS threads, tick and preemption all verified The figures are internally consistent, which is what makes them evidence rather than encouragement: 100 ticks elapsed for a 100-tick sleep, so the tick rate is exactly as configured; 20 sleeper wakeups is precisely 100/5 for a 5-tick sleep; and 20 preemptions means every wakeup took the core from a runnable lower-priority thread rather than receiving it by cooperative handoff. This validates the context-switch assembly merged in #579 on real Cortex-R52 silicon. Until now it had only run against the Armv8-R AEM FVP, which reports MIDR part 0xD0F -- an architecture envelope model, not an R52 implementation. The demo judges four things separately because on first silicon they fail independently: that time advanced (the GIC delivers the timer PPI and _tx_timer_interrupt is reached), that tx_thread_sleep returned (tick-driven scheduling, not merely the interrupt), that the lowest-priority thread ran (the sleeper actually yielded), and that the sleeper resumed while the spinner was runnable (preemption). A demo that only counted whether both threads ran would pass with a broken tick if they happened to yield to each other. Integration pieces, all from the FVP example with the board's differences: - tx_initialize_low_level.S publishes the system stack and the first free address, then calls board_init(). Shorter than the A-profile reference ports because entry.S has already given every mode its own stack from dedicated linker regions. - entry.S routes the IRQ vector through _tx_thread_context_save and _tx_thread_context_restore under TX_R52_USE_THREADX_IRQ, keeping the standalone A32 handler for s32z280_boot.elf, which does not link the kernel so that a boot failure there stays unambiguous. - board_init() runs mpu_init() FIRST. The GIC distributor is unreachable until its region is mapped Device nGnRnE, so any GIC access before the MPU is enabled aborts. - link.lds provides _end as well as end; _tx_initialize_low_level publishes unused memory from _end. - bsp_done() is defined in the demo rather than shared from bsp_boot.c, whose bsp_main would collide with the demo's. s32z280_boot.elf still builds and the FVP example still passes 6/6. Assisted-by: Claude Code (Opus 5) <[email protected]> * Tightened the S32Z280 MPU map and verified protection is enforced The map now carries real permissions -- code read-only and executable, data writable and never executable, peripherals Device nGnRnE and never executable, everything else unmapped -- and the enforcement is demonstrated rather than asserted: X1 write to read-only code region faults = 0x00000001 fault DFSR = 0x00000A0C bit 11 (WnR) set: a write fault fault addr = 0x79900000 the address written X2 PASS write to code faulted and was recovered Exactly one fault, identified as a write, at the precise address, followed by successful resumption. ThreadX still passes on the same map: ticks 100, sleeper 20, spinner ~1e6, preempt 20, and IRQ count keeps advancing during the protection test. Reading SCTLR back or listing the programmed regions would only show what was configured. Provoking the violation shows it is enforced, which is the claim that matters for a protection story. entry.S gains a recoverable data-abort path, gated on a fault_expected flag the test arms. It records DFSR and DFAR, counts the fault, and resumes at the instruction after the faulting access -- lr on data-abort entry is the faulting address plus 8, so subs pc, lr, #4 skips the access instead of retrying it forever. Unarmed, a data abort stays fatal and reported, which is what a real bug should get. This supersedes the permissive bring-up map, and the reason that map existed is worth recording: a narrow map had failed earlier and the cause was unknown, so coverage was blamed. Coverage was never the problem. An out-of-bounds write to a [3]-sized region table put a region at base 0 overlapping everything, and SCTLR.TE being set meant the resulting abort could not reach a handler. With both fixed a narrow map works first time, and -- more usefully -- a mistake in it now reports vector, syndrome and faulting address instead of hanging mutely. Until TE was cleared no fault handler on this board could run, so no protection claim about it could be tested at all. This is the first one that could. Assisted-by: Claude Code (Opus 5) <[email protected]> * Enabled the S32Z280 caches, with an honest effectiveness result Both caches are on and verified from SCTLR: CLIDR = 0x09200003 SCTLR = 0x30C50839 -> 0x30C5183D (C and I set) cachesOn = 1 counts off = 0x00094DF4 counts on = 0x00094CC8 gain/1000 = 0 C3 PASS caches enabled (SCTLR.C and SCTLR.I set) C4 no significant speedup -- expected here, the workload is already in fast local SRAM cache.c supplies what the FVP example explicitly deferred to silicon: a data-cache set/way sweep that reads CLIDR and CCSIDR and walks every set and way the hardware reports, rather than assuming a geometry. The FVP file notes that this "belongs with the silicon bring-up where it can be verified against the real cache geometry"; this is that. The sweep matters because caches are not architecturally guaranteed invalid out of reset, and enabling a write-back data cache holding stale valid lines would evict them over live memory. The effectiveness result is reported separately from the enable, and that separation was earned. The first version of this test passed on warm < cold and printed "caches on and the workload got faster" for 609904 counts against 609686 -- a 0.036% difference that is measurement noise. That criterion was worthless and the PASS was misleading. It now reports the gain as a fraction and only claims a speedup above 10%. No speedup here is a plausible result rather than a defect: both the code and the data this workload touches live in RTU-local low-latency SRAM, close to core speed already. There is no slow memory on this board to demonstrate a cache against -- DDR would be the place, and DDR is not initialised. So the honest claim is "enabled and harmless", not "enabled and beneficial". cache_clean_all() is called before parking, and this is not optional. With a write-back data cache, values this image writes can sit dirty in cache where a debugger reading SRAM cannot see them -- and the memory-resident progress markers and fault records this bring-up depends on are read exactly that way. Without the clean, a post-mortem read could report stale values and look like a fault that never happened. ThreadX still passes on the same configuration and the protection test still faults correctly: write to the read-only code region takes exactly one write fault at the written address and recovers. Assisted-by: Claude Code (Opus 5) <[email protected]> * Made the cache benchmark actually measure the cache The caches now show a real effect instead of noise: D line = 64 bytes D ways = 4 D sets = 64 D bytes = 0x4000 16 KB L1 data cache, read from CCSIDR bench wds = 0x800 8 KB working set, half the cache counts off = 0x000D1B87 858503 counts on = 0x0009F515 652053 gain/1000 = 0xF0 24% faster C4 PASS caches measurably faster (>=10%) The previous benchmark could not have measured anything, and its 0.036% "speedup" was noise. Two things were wrong with it. It used a static buffer in the RTU-local fast-data bank. That memory is close to core speed already, so caching it saves almost nothing -- there is no latency to hide. The benchmark now runs over the extended SRAM at S32Z_EXT_SRAM_BASE, which the Reference Manual describes as NOT RTU-local, and mpu.c gains a sixth region to map it Normal write-back and never executable. And its working set was a guessed 4 KB constant. It is now sized at run time from CCSIDR to half the reported data cache, so it fits and is revisited every pass. A set larger than the cache would stream through and evict, showing little benefit even over slow memory -- a guess could have produced a null result for a reason that has nothing to do with whether the cache works. 24% on this loop is a believable figure rather than a suspiciously large one: the workload is a store-heavy read-modify-write over a write-back cache, so part of the cost is write traffic that still has to reach SRAM. Useful by-product: the L1 data cache geometry is now read and printed rather than assumed. Nothing in the SoC reference manual gives it, and the set/way invalidate sweep in cache.c depends on it being right. ThreadX still passes and the protection test still faults correctly. Assisted-by: Claude Code (Opus 5) <[email protected]> * Fixed the PRBAR field encoding and proved execute-never enforcement program_region() shifted every PRBAR field one bit too far left: SH<<4, AP<<2 and XN<<1, where the Cortex-R52 TRM (r1p3 figure 3-39, table 3-80) places BASE[31:6], RES0[5], SH[4:3], AP[2:1] and XN[0]. The intended XN therefore landed in AP[1] -- the EL0-access bit -- and no region was ever execute-never. On this board the core ran instructions straight out of .data with LR_abt at 0x3178000c to prove it. The write-to-read-only test passed throughout, which is why this survived: under the old shift the AP value's low bit happened to land in the real AP[2], the read-only bit, so permissions came out right by accident while XN was silently discarded. Only an instruction fetch from a region marked non-executable could distinguish the two. The AP macros in mpu.h go back to the architectural encoding, AP[2] for read-only and AP[1] for EL0 access. No calibration is needed; the values were correct as published all along. Verified on S32Z280 silicon: the data region now reads back PRBAR 0x31780001 with XN set, a write to the read-only code region faults with DFSR 0xA0C, execution from the data region takes a prefetch abort with IFSR 0x20C at 0x31780000, both faults recover, and the ThreadX demo still reports 100 ticks with 20 preemptions. Assisted-by: Claude Code (Opus 5) <[email protected]> * Corrected the same PRBAR encoding bug in the FVP MPU example The FVP example carries the identical off-by-one shift that the S32Z280 bring-up exposed, so no region there was execute-never either: the data region and the whole peripheral region were both freely executable, and each also gained unintended EL0 access from the misplaced XN bit. This also retires the "PRBAR.AP bit order is reversed" claim in mpu.h. That note rested on a real measurement -- four regions, one per AP encoding, privileged write attempted on each, writes faulting for 0b01 and 0b11 -- but the cause was the shift, not the bit order. With AP written into bits[3:2], its low bit lands in the real AP[2], the read-only bit, so writes fault exactly when that bit is set. The "high bit grants EL0 access" half of the conclusion was never tested; under the old shift that bit landed in SH[0], programming a shareability the TRM calls UNPREDICTABLE. The architectural encoding needs no calibration. demo_mpu.c gains the check that would have caught this: an instruction fetch from the execute-never data region must take a prefetch abort. Recovering from one cannot work the way the data-abort path does, since skipping the faulting instruction is impossible when the instruction is what could not be fetched, so entry.S gains a recoverable prefetch-abort path that returns to a landing point recorded by mpu_try_execute. Verified both ways. All five FVP images pass, and the new check reports prefetch aborts 0 -> 1. Restoring the old shift for one run makes it fail with 0 -> 0, so the check is not vacuous. Assisted-by: Claude Code (Opus 5) <[email protected]> * Removed the unassemblable .arch directive from the S32Z280 example GNU as accepts ".arch armv8-r"; LLVM's integrated assembler accepts no spelling of it -- not armv8-r, not armv8r, not armv8-r+crc -- and stops with "Unknown Arch: armv8-r". There is nothing to substitute, so the directive goes. The architecture comes from -mcpu=cortex-r52 on the command line, which every toolchain file passes, so it only restated it. entry.S in this example never had one. The two equivalents in the FVP example are handled separately, in the change that brings those images under the LLVM check. Verified: both S32Z280 assembly sources now assemble with Arm Toolchain for Embedded 22.1.0, and the GNU build of s32z280_boot.elf and s32z280_demo.elf is unchanged with no warnings. Assisted-by: Claude Code (Opus 5) <[email protected]> * Stopped read_identity.gdb reporting failure after a successful demo run The script reads r52_identity, which only the boot image defines. The kernel demo shares the same bsp_done breakpoint and the same boot_stage marker, so it is convenient to point the same script at either image -- but against the demo the lookup raised and gdb exited 1, after the demo had already printed "PASS threads, tick and preemption all verified" over the console. A tool that reports failure on success is worse than one that prints nothing. The lookup is now guarded, and says which image it is looking at rather than falling over. Everything after it is skipped when the structure is absent. Verified against both images on S32Z280 silicon: the demo exits 0 and reports its own PASS, and the boot image still prints the full identity report with MIDR 0x411FD133 and C3, C4, X2 and X4 all passing. Assisted-by: Claude Code (Opus 5) <[email protected]>
Diffstat (limited to 'common_modules/module_manager/src/txm_module_manager_kernel_dispatch.c')
0 files changed, 0 insertions, 0 deletions