From d57a72ccdafb8701243353810854231fbe7d3ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 17 Aug 2026 09:11:22 -0400 Subject: [PATCH] Ran the interrupt handler from ATCM, and measured what that buys The TCM work so far enabled ATCM and left it empty, which buys nothing. This places code in it and measures the result. link.lds gains an ATCM region and an .atcm_text section whose run address is in the bank and whose load address is in CODE. tcm_copy_atcm_text moves it, using 64-bit stores because ECC is enabled on this part and ATCM requires them (Cortex-R52 TRM 6.2.2); both ends of the section are 8-byte aligned so there is no narrower tail to leave without check bits. The copy runs after T4 and T5, which write test patterns to the first and last words of the bank and would otherwise land on top of the code. s32z280_atcm.elf is the same image as s32z280_boot.elf with the interrupt service body placed in ATCM. Both targets exist so the comparison can be repeated on one board in one session without reconfiguring. The service routine is split into a timed wrapper that stays in .text and a body that moves, so the wrapper's own cost appears in both measurements and cancels. Measured in PMU cycles over 64 samples, caches enabled in both: code RAM ATCM change min 454 334 -26.4% mean 458 340 -25.8% max 612 466 -23.9% spread 158 132 -16.5% CNTPCT is not used for this: at 8 MHz it cannot resolve a handler body, let alone the variation in one. The level shift is the solid part. ATCM is a quarter faster even though the caches were on and code RAM had the instruction cache available, which says the handler does not stay resident between interrupts 10 ms apart -- so each one pays a cold fetch from code RAM, which runs at half the core frequency where ATCM runs at full speed with one wait state (S32Z2 RM 6.3.6). The determinism claim deserves less weight than the numbers first suggest. The spread narrows by only 16%, and ATCM's worst case still sits slightly above code RAM's best case, so the two distributions overlap at the tails rather than separating. Whatever jitter remains is not dominated by instruction fetch. Both images pass six of six boot probes. Assisted-by: Claude Code (Opus 5) --- .../example_build/s32z280_evb/CMakeLists.txt | 37 +++++++++ .../gnu/example_build/s32z280_evb/bsp_boot.c | 76 ++++++++++++++++++- .../example_build/s32z280_evb/irq_dispatch.c | 48 ++++++++++++ .../gnu/example_build/s32z280_evb/link.lds | 25 ++++++ .../gnu/example_build/s32z280_evb/tcm.c | 42 ++++++++++ .../gnu/example_build/s32z280_evb/tcm.h | 10 +++ .../gnu/example_build/s32z280_evb/timer.c | 38 ++++++++++ .../gnu/example_build/s32z280_evb/timer.h | 8 ++ 8 files changed, 282 insertions(+), 2 deletions(-) diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt index 2545b3d06..94fe43301 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/CMakeLists.txt @@ -55,6 +55,43 @@ target_link_options(s32z280_boot.elf PRIVATE ) +# Same image as s32z280_boot.elf with the interrupt service body placed in ATCM +# instead of code RAM, so the two can be compared on the same board in the same +# session. Separate target rather than an option so both exist at once and a +# result can be reproduced without reconfiguring. +add_executable(s32z280_atcm.elf EXCLUDE_FROM_ALL + ${EVB_DIR}/entry.S + ${EVB_DIR}/bsp_boot.c + ${EVB_DIR}/linflexd.c + ${EVB_DIR}/timer.c + ${EVB_DIR}/gic_probe.c + ${EVB_DIR}/mpu.c + ${EVB_DIR}/gicv3.c + ${EVB_DIR}/irq_dispatch.c + ${EVB_DIR}/cache.c + ${EVB_DIR}/tcm.c +) + +target_include_directories(s32z280_atcm.elf PRIVATE ${EVB_DIR}) + +# -g only: this image exists to be inspected through a debugger, and without +# DWARF the identity structure has no type for GDB to walk. No effect on the +# generated code. +target_compile_options(s32z280_atcm.elf PRIVATE -g) + +target_link_options(s32z280_atcm.elf PRIVATE + -T${EVB_DIR}/link.lds + -nostartfiles + -Wl,-Map=s32z280_atcm.map + # Bare metal: one code region and one data region, with access control + # belonging to the MPU rather than to segment permissions. + ${EVB_LINK_QUIET_RWX} +) + + +target_compile_definitions(s32z280_atcm.elf PRIVATE TX_R52_ATCM_ISR) + + # ThreadX demo on silicon: threads, preemption and the timer tick. Separate # target from s32z280_boot.elf, which deliberately does not link the kernel so # that a boot failure there is unambiguously a board problem. diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/bsp_boot.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/bsp_boot.c index 96ee23d75..50d22a2b2 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/bsp_boot.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/bsp_boot.c @@ -569,7 +569,26 @@ void bsp_main(void) proves the interrupt is delivered, which is a different claim. */ MARK(0x50); - linflexd_puts("I1 gicv3_init\n"); + /* Copy the handler into ATCM now, and not earlier: T4 and T5 write test + patterns to the first and last words of the bank, which would land on top + of this code. The ECC preload in T3 has already established check bits + for the whole bank, and the 64-bit stores in the copy maintain them for + every location it writes. */ + + linflexd_puts("T6 copying .atcm_text into ATCM\n"); + { + unsigned long copied = tcm_copy_atcm_text(); + + report("atcm bytes", (unsigned int) copied); + if (copied == 0UL) + { + linflexd_puts(" nothing placed in ATCM in this image\n"); + } + } + + timer_cycles_enable(); + + linflexd_puts("I1 gicv3_init\n"); gicv3_init(); MARK(0x51); @@ -595,12 +614,18 @@ void bsp_main(void) MARK(0x53); { + extern unsigned int board_service_count; unsigned int spins = 0U; /* Wait for ticks. Bounded: a missing interrupt must report itself rather than hang the boot. */ - while ((board_irq_count < 5UL) && (spins < 30U)) + /* Wait for a full set of latency samples, not just a few ticks: + the figure of interest is the worst case, and a handful of + samples cannot show it. Still bounded, so a missing interrupt + reports itself rather than hanging the boot. */ + + while ((board_service_count < 64U) && (spins < 400U)) { timer_spin(200000U); spins++; @@ -612,6 +637,53 @@ void bsp_main(void) report("timer INTID", (unsigned int) board_timer_intid); report("spurious ", (unsigned int) board_spurious_count); report("unexpected", (unsigned int) board_unexpected_intid); + + /* Handler body execution time. Reported as min, max and mean rather + than mean alone: the spread is the point. */ + + { + extern unsigned int board_service_cycles[]; + extern unsigned int board_service_count; + + unsigned int i; + unsigned int lo = 0xFFFFFFFFU; + unsigned int hi = 0U; + unsigned long sum = 0UL; + unsigned int n; + + /* Stop the source and snapshot the count before reading any of it. + The timer re-arms inside the handler, so samples keep arriving + otherwise: the first version of this read let the count grow + between computing the extremes and dividing for the mean, and + printed a mean below the minimum. */ + + timer_stop(); + n = board_service_count; + + for (i = 0U; i < n; i++) + { + unsigned int v = board_service_cycles[i]; + + if (v < lo) { lo = v; } + if (v > hi) { hi = v; } + sum += (unsigned long) v; + } + + linflexd_puts("I5 handler body cycles "); +#ifdef TX_R52_ATCM_ISR + linflexd_puts("(body in ATCM)\n"); +#else + linflexd_puts("(body in code RAM)\n"); +#endif + report("samples ", n); + if (n > 0U) + { + report("min ", lo); + report("max ", hi); + report("mean ", (unsigned int) (sum / n)); + report("spread ", hi - lo); + } + } report("first INTID", (unsigned int) board_first_intid); linflexd_puts("I4 interrupt test done\n"); } diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/irq_dispatch.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/irq_dispatch.c index 193f14dea..0ef90b7b4 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/irq_dispatch.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/irq_dispatch.c @@ -164,7 +164,55 @@ void board_init(void) /* before nesting starts. Does not acknowledge and does not EOI. */ /**************************************************************************/ +/**************************************************************************/ +/* Handler execution time, and where the handler's instructions live. */ +/* */ +/* TX_R52_ATCM_ISR places the service routine in ATCM. ATCM runs at full */ +/* core speed with one wait state; .text lives in RTU code RAM, which runs */ +/* at half the core frequency (S32Z2 RM 6.3.6). ATCM also has no cache to */ +/* miss, which is the property that matters for a determinism argument. */ +/* */ +/* Measured in cycles from the PMU counter, not CNTPCT: at 8 MHz CNTPCT */ +/* cannot resolve a handler body, let alone the variation in one. */ +/* */ +/* The timing wrapper below stays in .text in both configurations, so its */ +/* own cost appears in every sample and cancels when the two are compared. */ +/* Only the body moves. What matters in the result is the spread: a warm */ +/* instruction cache can match ATCM on the mean and cannot match it on the */ +/* worst case. */ +/**************************************************************************/ + +#define BOARD_LATENCY_SAMPLES 64U + +unsigned int board_service_cycles[BOARD_LATENCY_SAMPLES]; +unsigned int board_service_count; + +#ifdef TX_R52_ATCM_ISR +#define BOARD_ISR_SECTION __attribute__((section(".atcm_text"))) +#else +#define BOARD_ISR_SECTION +#endif + +static BOARD_ISR_SECTION void board_irq_service_body(unsigned long intid); + void board_irq_service(unsigned long intid) +{ + unsigned int before; + unsigned int after; + + before = timer_read_cycles(); + board_irq_service_body(intid); + after = timer_read_cycles(); + + if (board_service_count < BOARD_LATENCY_SAMPLES) + { + board_service_cycles[board_service_count] = after - before; + board_service_count++; + } +} + + +static BOARD_ISR_SECTION void board_irq_service_body(unsigned long intid) { if (intid == GICV3_SPURIOUS_INTID) { diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds b/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds index ac0583b3c..1b39be82a 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/link.lds @@ -44,6 +44,11 @@ __sys_stack_size__ = 0x0800; MEMORY { CODE (rx) : ORIGIN = 0x79900000, LENGTH = 0x00700000 /* 7 MB */ + /* ATCM. entry.S programmes IMP_ATCMREGIONR to this base and enables the + bank before anything runs from it, and mpu.c maps it executable. Code + placed here runs at full core speed with one wait state, where CODE is + RTU code RAM and runs at half the core frequency (S32Z2 RM 6.3.6). */ + ATCM (rwx) : ORIGIN = 0x30000000, LENGTH = 0x00010000 /* 64 KB */ DATA (rwx) : ORIGIN = 0x31780000, LENGTH = 0x00080000 /* 512 KB: DRAM0 + DRAM1, both full core speed */ } @@ -89,6 +94,26 @@ SECTIONS *(.glue_7t) } > CODE + /* Code that runs from tightly-coupled memory. Loaded into CODE and copied + to ATCM by atcm_copy_text() before it is called, because ECC is enabled + on this part and a TCM location must be written before it is read + (Cortex-R52 TRM 6.2.2). The copy uses 64-bit stores for the same + reason: ATCM requires them where BTCM and CTCM accept 32-bit. + + ALIGN(8) at both ends so the copy can move whole 64-bit units without a + tail case, which is what keeps the ECC requirement satisfied for every + location written. */ + + .atcm_text : ALIGN(8) + { + __atcm_text_start__ = .; + *(.atcm_text*) + . = ALIGN(8); + __atcm_text_end__ = .; + } > ATCM AT> CODE + + __atcm_text_load__ = LOADADDR(.atcm_text); + .rodata : { *(.rodata*) diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.c index a2b9dd68a..081dc4140 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.c @@ -260,3 +260,45 @@ void tcm_preload(unsigned int index, unsigned long base, unsigned long bytes) __asm volatile ("dsb sy" ::: "memory"); } + + +/**************************************************************************/ +/* Copy .atcm_text into ATCM. */ +/**************************************************************************/ + +extern char __atcm_text_start__; +extern char __atcm_text_end__; +extern char __atcm_text_load__; + +unsigned long tcm_copy_atcm_text(void) +{ + /* The linker aligns both ends of the section to 8, so the whole extent is a + whole number of 64-bit units and there is no tail to handle narrowly. + That matters here rather than being tidiness: a 32-bit store to ATCM does + not establish the ECC check bits for its location, so a tail copied 32 + bits at a time would leave words that fault when read. */ + + volatile unsigned long long *dst; + const volatile unsigned long long *src; + unsigned long bytes; + unsigned long i; + + dst = (volatile unsigned long long *) (void *) &__atcm_text_start__; + src = (const volatile unsigned long long *) (void *) &__atcm_text_load__; + bytes = (unsigned long) (&__atcm_text_end__ - &__atcm_text_start__); + + for (i = 0UL; i < (bytes / 8UL); i++) + { + dst[i] = src[i]; + } + + /* Complete the writes, then discard any stale instruction-side copy of the + destination before it is executed. */ + + __asm__ volatile("dsb sy" ::: "memory"); + __asm__ volatile("mcr p15, 0, r0, c7, c5, 0" : : : "memory"); /* ICIALLU */ + __asm__ volatile("dsb sy" ::: "memory"); + __asm__ volatile("isb" ::: "memory"); + + return bytes; +} diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.h index b47358f26..b80f226d3 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.h +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/tcm.h @@ -148,4 +148,14 @@ unsigned int tcm_enable(unsigned int index, unsigned long base); void tcm_preload(unsigned int index, unsigned long base, unsigned long bytes); +/* Copy the .atcm_text section from its load address in CODE to its run address + in ATCM. Must run before anything in that section is called, and after the + bank is enabled. Returns the number of bytes copied, which is zero when the + section is empty -- the normal case for an image that places nothing there. + + Uses 64-bit stores because ECC is enabled on this part and ATCM requires + them; see tcm_preload and Cortex-R52 TRM 6.2.2. */ + +unsigned long tcm_copy_atcm_text(void); + #endif diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.c b/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.c index 544102083..9fc5c0a17 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.c +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.c @@ -146,3 +146,41 @@ void timer_stop(void) __asm volatile ("mcr p15, 0, %0, c14, c2, 1" :: "r" (control)); __asm volatile ("isb"); } + + +/**************************************************************************/ +/* PMU cycle counter. */ +/* */ +/* PMCR.E gates all counters including the dedicated cycle counter, and */ +/* PMCNTENSET bit 31 enables that counter specifically. Both are needed. */ +/* PMCR.D is left clear so the counter advances every cycle rather than */ +/* every 64th, which is the resolution these measurements want. */ +/**************************************************************************/ + +#define PMCR_E (1UL << 0) /* enable all counters */ +#define PMCR_P (1UL << 1) /* reset event counters */ +#define PMCR_C (1UL << 2) /* reset cycle counter */ +#define PMCNTENSET_C (1UL << 31) /* cycle counter enable */ + +void timer_cycles_enable(void) +{ + unsigned long pmcr; + + /* Allow EL0/EL1 access and enable, resetting the counter as we go. */ + + __asm__ volatile("mrc p15, 0, %0, c9, c12, 0" : "=r"(pmcr)); + pmcr |= (PMCR_E | PMCR_C); + __asm__ volatile("mcr p15, 0, %0, c9, c12, 0" : : "r"(pmcr) : "memory"); + + __asm__ volatile("mcr p15, 0, %0, c9, c12, 1" + : : "r"((unsigned long) PMCNTENSET_C) : "memory"); + + __asm__ volatile("isb" ::: "memory"); +} + +unsigned int timer_read_cycles(void) +{ + unsigned long value; + __asm__ volatile("mrc p15, 0, %0, c9, c13, 0" : "=r"(value)); + return (unsigned int) value; +} diff --git a/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.h b/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.h index 7cd411efc..0a0866b8f 100644 --- a/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.h +++ b/ports/cortex_r52/gnu/example_build/s32z280_evb/timer.h @@ -61,4 +61,12 @@ void timer_start_oneshot_irq(unsigned int ticks); unsigned int timer_fired(void); void timer_stop(void); +/* PMU cycle counter. CNTPCT runs at 8 MHz on this part, so its 125 ns + granularity is too coarse to resolve interrupt latency, let alone the jitter + in it. The cycle counter runs at the core clock and is what those + measurements use. timer_cycles_enable must be called before any read. */ + +void timer_cycles_enable(void); +unsigned int timer_read_cycles(void); + #endif /* TIMER_H */