diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt index c0824b26..ccb54258 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/CMakeLists.txt @@ -197,6 +197,40 @@ target_link_options(demo_mpu.elf PRIVATE ${R52_LINK_QUIET_RWX} ) +# Nested IRQ handling. Needs the library built with TX_R52_ENABLE_IRQ_NESTING, +# because _tx_thread_irq_nesting_start only re-enables IRQ usefully when +# tx_port.h agrees the configuration supports it, and entry.S only emits the +# start/end pairing under the same macro. The image therefore exists in that +# configuration only, as demo_m5.elf does for VFP. +if(TX_R52_ENABLE_IRQ_NESTING) + add_executable(demo_nesting.elf EXCLUDE_FROM_ALL + ${FVP_DIR}/entry.S + ${R52_CONSOLE_SOURCES} + ${FVP_DIR}/gicv3.c + ${FVP_DIR}/timer.c + ${FVP_DIR}/irq_dispatch.c + ${FVP_DIR}/tx_initialize_low_level.S + ${FVP_DIR}/demo_nesting.c + ) + + target_compile_definitions(demo_nesting.elf PRIVATE TX_R52_USE_THREADX_IRQ) + + target_link_libraries(demo_nesting.elf PRIVATE threadx) + + target_include_directories(demo_nesting.elf PRIVATE + ${FVP_DIR} + ${CMAKE_SOURCE_DIR}/common/inc + ${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc + ) + + target_link_options(demo_nesting.elf PRIVATE + -T${FVP_DIR}/link.lds + -nostartfiles + -Wl,-Map=demo_nesting.map + ${R52_LINK_QUIET_RWX} + ) +endif() + # Every image built here, in the order they should be exercised. set(R52_IMAGES boot_check.elf demo_m2.elf demo_m3.elf demo_threadx.elf demo_mpu.elf) @@ -209,6 +243,9 @@ endforeach() if(TX_R52_ENABLE_VFP) list(APPEND R52_IMAGES demo_m5.elf) endif() +if(TX_R52_ENABLE_IRQ_NESTING) + list(APPEND R52_IMAGES demo_nesting.elf) +endif() # Console backend. Semihosting is the default because it needs no peripheral # and so cannot be broken by a wrong memory map; the PL011 path is what real diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/board.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/board.h index b514d29c..2e985e50 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/board.h +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/board.h @@ -55,6 +55,13 @@ void board_init(void); void board_irq_handler(void); +/* The nesting path splits the handler in three, because the acknowledge has to + happen in IRQ mode before nesting starts and the end-of-interrupt after it + finishes. board_irq_service does the middle part on an INTID that has + already been acknowledged, and does not acknowledge or EOI itself. */ + +void board_irq_service(unsigned long intid); + /* Interrupt observability, maintained by board_irq_handler. */ extern volatile unsigned long board_irq_count; @@ -62,6 +69,22 @@ extern volatile unsigned long board_timer_intid; extern volatile unsigned long board_spurious_count; extern volatile unsigned long board_unexpected_intid; +/* Nesting observability, maintained by board_irq_handler. All of this is inert + unless the image was built with TX_ENABLE_IRQ_NESTING: without it the handler + runs in IRQ mode with interrupts masked and can never be re-entered. */ + +extern volatile unsigned long board_nest_depth; +extern volatile unsigned long board_nest_max; +extern volatile unsigned long board_sgi_count; +extern volatile unsigned long board_sgi_nested_count; + +/* Set by demo_nesting to make the timer handler provoke a nested SGI. Left at + zero every other image behaves exactly as before. */ + +extern volatile unsigned long board_nest_provoke; + +#define BOARD_NEST_SGI_INTID 8U + /* Counted by the EL2 hyp-trap handler in entry.S; the ZoneX seam. */ extern volatile unsigned long _hvc_call_count; diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_nesting.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_nesting.c new file mode 100644 index 00000000..0e204d06 --- /dev/null +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/demo_nesting.c @@ -0,0 +1,216 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +/**************************************************************************/ +/* */ +/* BOARD SUPPORT RELEASE */ +/* */ +/* demo_nesting.c Cortex-R52/GNU */ +/* 6.5.2 */ +/* AUTHOR */ +/* */ +/* Frederic Desbiens, Eclipse Foundation */ +/* */ +/* DESCRIPTION */ +/* */ +/* Nested IRQ handling: _tx_thread_irq_nesting_start and */ +/* _tx_thread_irq_nesting_end. */ +/* */ +/* Those two routines have shipped in this port since it was written */ +/* and nothing had ever called them. They were compiled into every */ +/* build, never entered by any image, on the model or on silicon. */ +/* This is the image that enters them. */ +/* */ +/* HOW NESTING IS PROVOKED */ +/* */ +/* Two interrupt sources are needed and one must outrank the other. */ +/* The generic timer PPI is already there; the second is an SGI, which */ +/* a core can raise on itself, given a numerically lower priority so */ +/* the GIC lets it preempt. */ +/* */ +/* Inside the timer handler, after nesting_start has left IRQ mode for */ +/* System mode and re-enabled IRQ, board_irq_handler raises the SGI and */ +/* spins briefly. The SGI outranks the timer, so it is delivered */ +/* immediately and re-enters el1_irq_entry while the timer handler is */ +/* still on the stack. That is nesting, and the depth counter sees 2. */ +/* */ +/* The spin is bounded. Without TX_ENABLE_IRQ_NESTING the SGI can */ +/* never arrive, and a test that hangs in that configuration would be */ +/* worse than one that fails. */ +/* */ +/* WHAT EACH CHECK IS FOR */ +/* */ +/* N1 the SGI arrives at all. This is separate on purpose: it tests */ +/* the ICC_SGI1R encoding, which does not transcribe from the */ +/* AArch64 alias, so if N1 fails the fault is in gicv3_send_sgi and */ +/* not in the nesting routines. */ +/* N2 the SGI arrived while another handler was active, depth 2. */ +/* N3 depth returned to zero, so the pairing unwound. */ +/* N4 the tick still advances afterwards, so nesting did not damage */ +/* the timer path. */ +/* N5 threads still run and preempt, so the context save and restore */ +/* survived being re-entered. */ +/* */ +/**************************************************************************/ + +#include "tx_api.h" +#include "board.h" +#include "console.h" +#include "gicv3.h" + +#define DEMO_STACK_SIZE 2048 + +static TX_THREAD thread_check; +static TX_THREAD thread_spin; + +static ULONG thread_check_stack[DEMO_STACK_SIZE / sizeof(ULONG)]; +static ULONG thread_spin_stack[DEMO_STACK_SIZE / sizeof(ULONG)]; + +static volatile ULONG spin_runs; + + +static UINT report(const char *label_ptr, UINT passed) +{ + console_puts(label_ptr); + console_puts((passed != 0U) ? "PASS\n" : "FAIL\n"); + return (passed != 0U) ? 0U : 1U; +} + + +static void report_hex(const char *label_ptr, unsigned long value) +{ + console_puts(label_ptr); + console_puthex(value); + console_puts("\n"); +} + + +/* Lowest priority, never blocks: gives the tick something to preempt and shows + the scheduler is still alive after handlers have been nested. */ + +static void thread_spin_entry(ULONG thread_input) +{ + (void) thread_input; + + for (;;) + { + spin_runs++; + } +} + + +static void thread_check_entry(ULONG thread_input) +{ + UINT failures = 0U; + ULONG sgi_before; + ULONG ticks_start; + ULONG ticks_end; + ULONG spin_before; + + (void) thread_input; + + /* N1 first, with provocation still off, so this measures only whether an + SGI raised from thread context is delivered and dispatched. */ + + console_puts("[check] raising an SGI from thread context\n"); + + sgi_before = board_sgi_count; + gicv3_send_sgi(BOARD_NEST_SGI_INTID); + tx_thread_sleep(2); + + failures += report("[check] N1 SGI delivered and dispatched ", + (board_sgi_count > sgi_before) ? 1U : 0U); + + if (board_sgi_count == sgi_before) + { + /* Everything below depends on the SGI working, so say so rather than + reporting four more failures that all have the same cause. */ + + console_puts("[check] SGI never arrived; the remaining checks would\n" + " only restate that, so they are skipped.\n"); + console_puts("\nNESTING RESULT: FAILED\n"); + console_exit(1U); + } + + /* Now let the timer handler provoke a nested SGI, and give it time for a + good number of ticks. */ + + console_puts("[check] enabling nested provocation in the timer handler\n"); + + board_nest_provoke = 1UL; + ticks_start = tx_time_get(); + spin_before = spin_runs; + tx_thread_sleep(50); + ticks_end = tx_time_get(); + board_nest_provoke = 0UL; + + report_hex("[check] max nesting depth observed = ", board_nest_max); + report_hex("[check] nested SGI count = ", board_sgi_nested_count); + report_hex("[check] nesting depth now = ", board_nest_depth); + + failures += report("[check] N2 SGI nested inside another handler ", + (board_sgi_nested_count > 0UL) && + (board_nest_max >= 2UL) ? 1U : 0U); + + failures += report("[check] N3 nesting unwound to depth zero ", + (board_nest_depth == 0UL) ? 1U : 0U); + + failures += report("[check] N4 tick still advancing after nesting ", + ((ticks_end - ticks_start) >= 40UL) ? 1U : 0U); + + failures += report("[check] N5 lower-priority thread still scheduled ", + (spin_runs > spin_before) ? 1U : 0U); + + report_hex("[check] spurious interrupts = ", board_spurious_count); + report_hex("[check] unexpected INTID = ", board_unexpected_intid); + + failures += report("[check] no spurious or unexpected interrupts ", + ((board_spurious_count == 0UL) && + (board_unexpected_intid == 0UL)) ? 1U : 0U); + + if (failures == 0U) + { + console_puts("\nNESTING RESULT: ALL CHECKS PASSED\n"); + } + else + { + console_puts("\nNESTING RESULT: FAILED\n"); + } + + console_exit(failures); +} + + +void tx_application_define(void *first_unused_memory) +{ + (void) first_unused_memory; + + (void) tx_thread_create(&thread_check, "nest check", thread_check_entry, + 0UL, thread_check_stack, sizeof(thread_check_stack), + 10U, 10U, TX_NO_TIME_SLICE, TX_AUTO_START); + + (void) tx_thread_create(&thread_spin, "spinner", thread_spin_entry, + 0UL, thread_spin_stack, sizeof(thread_spin_stack), + 20U, 20U, TX_NO_TIME_SLICE, TX_AUTO_START); +} + + +void bsp_main(void) +{ + console_puts("\n=== ThreadX nested IRQ handling " + "(Cortex-R52, Armv8-R AArch32, Armv8-R AEM FVP) ===\n"); + + tx_kernel_enter(); +} diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S index ec25ac25..ec783b99 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/entry.S @@ -356,7 +356,47 @@ el1_irq_entry: .global __tx_irq_processing_return __tx_irq_processing_return: +#ifdef TX_ENABLE_IRQ_NESTING + + /* Nested IRQ handling. _tx_thread_irq_nesting_start leaves IRQ mode for + * System mode and re-enables IRQ, so an interrupt of higher priority can + * preempt this handler and re-enter at el1_irq_entry above. It is paired + * with _tx_thread_irq_nesting_end before _tx_thread_context_restore. + * + * The ORDER here is the whole difficulty, and getting it wrong does not + * fail gracefully. The interrupt must be ACKNOWLEDGED BEFORE nesting is + * started. Reading ICC_IAR1 is what raises the GIC's running priority to + * this interrupt's own, which is what masks it and everything of equal or + * lower priority; only then is re-enabling IRQ safe. Acknowledge after + * nesting_start instead and the still-pending, still-level-asserted timer + * is taken again the instant IRQ is enabled, and again, until the IRQ and + * System stacks are destroyed. That presents as garbage on the console and + * a hang, with no fault to point at it. The Cortex-R5 example BSP says the + * same thing in one line: "ensure all IRQ interrupts are cleared prior to + * enabling nested IRQ interrupts". + * + * The INTID is carried in r4, which survives the mode switch because only + * SP and LR are banked, and is also pushed on the IRQ stack so a nested + * level that reuses r4 cannot lose this level's value. End-of-interrupt is + * left until after nesting_end, in IRQ mode with interrupts masked, so + * dropping the running priority cannot re-admit this same interrupt. + */ + + bl gicv3_acknowledge /* IRQ mode, interrupts masked */ + mov r4, r0 + stmdb sp!, {r4, r5} /* r5 keeps 8-byte alignment */ + + bl _tx_thread_irq_nesting_start + mov r0, r4 + bl board_irq_service + bl _tx_thread_irq_nesting_end + + ldmia sp!, {r4, r5} + mov r0, r4 + bl gicv3_end_of_interrupt +#else bl board_irq_handler +#endif b _tx_thread_context_restore #else FAULT_REPORT msg_el1_irq diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.c index 4a192509..eb5c029c 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.c +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.c @@ -195,6 +195,56 @@ void gicv3_enable_ppi(unsigned int intid, unsigned int priority) /* gicv3_acknowledge */ /**************************************************************************/ +/**************************************************************************/ +/* gicv3_enable_sgi */ +/* */ +/* An SGI lives in the same redistributor frame as a PPI, so this is */ +/* gicv3_enable_ppi without the ICFGR step: INTIDs 0-15 have no */ +/* configurable edge/level, they are always edge-triggered. */ +/**************************************************************************/ + +void gicv3_enable_sgi(unsigned int intid, unsigned int priority) +{ + if (intid > 15U) + { + return; + } + + REG32(GICR_SGI_BASE + GICR_IGROUPR0) |= (1UL << intid); + + REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) &= + ~(0xFFUL << ((intid & 3U) * 8U)); + REG32(GICR_SGI_BASE + GICR_IPRIORITYR + (intid & ~3U)) |= + ((unsigned long) priority & 0xFFUL) << ((intid & 3U) * 8U); + + REG32(GICR_SGI_BASE + GICR_ISENABLER0) = (1UL << intid); +} + + +/**************************************************************************/ +/* gicv3_send_sgi */ +/* */ +/* ICC_SGI1R is 64-bit, so in AArch32 it is an MCRR rather than an MCR. */ +/* Fields: INTID in [27:24], TargetList in [15:0], Aff1/2/3 and IRM zero */ +/* for this single-core configuration, so TargetList = 1 selects core 0. */ +/* */ +/* The AArch64 name for this register is S3_0_C12_C11_5, which the */ +/* Cortex-A72 example uses, but that encoding does not transcribe to the */ +/* AArch32 64-bit CP15 space. The CRm here was confirmed by observing */ +/* that the SGI is actually delivered and acknowledged with the expected */ +/* INTID rather than by reading it off the A-profile alias. */ +/**************************************************************************/ + +void gicv3_send_sgi(unsigned int intid) +{ + unsigned long low = (((unsigned long) intid & 0xFUL) << 24) | 1UL; + unsigned long high = 0UL; + + __asm volatile ("mcrr p15, 0, %0, %1, c12" :: "r" (low), "r" (high) : "memory"); + instruction_barrier(); +} + + unsigned long gicv3_acknowledge(void) { unsigned long intid; diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.h b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.h index cc35639c..03f1b2c0 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.h +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/gicv3.h @@ -49,6 +49,15 @@ void gicv3_init(void); void gicv3_enable_ppi(unsigned int intid, unsigned int priority); +/* Enable a software-generated interrupt, INTID 0-15. Same registers as a PPI: + the redistributor's SGI/PPI frame covers INTIDs 0-31. */ + +void gicv3_enable_sgi(unsigned int intid, unsigned int priority); + +/* Raise an SGI on this core. */ + +void gicv3_send_sgi(unsigned int intid); + /* Acknowledge the highest-priority pending Group 1 interrupt, returning its INTID (possibly GICV3_SPURIOUS_INTID). */ diff --git a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/irq_dispatch.c b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/irq_dispatch.c index 0028dddb..02672108 100644 --- a/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/irq_dispatch.c +++ b/ports/cortex_r52/gnu/example_build/fvp_baser_aemv8r/irq_dispatch.c @@ -54,6 +54,12 @@ volatile unsigned long board_timer_intid; volatile unsigned long board_spurious_count; volatile unsigned long board_unexpected_intid; +volatile unsigned long board_nest_depth; +volatile unsigned long board_nest_max; +volatile unsigned long board_sgi_count; +volatile unsigned long board_sgi_nested_count; +volatile unsigned long board_nest_provoke; + /* Timer PPI priority. Only the top 5 priority bits are implemented on this model, so the value is a multiple of 8. */ @@ -73,6 +79,13 @@ void board_init(void) { gicv3_init(); gicv3_enable_ppi(TIMER_PPI_INTID, TIMER_PPI_PRIORITY); + + /* The nesting SGI must outrank the timer or it could never preempt it: + in the GIC a numerically lower priority value wins. Enabling it costs + nothing in images that never raise it. */ + + gicv3_enable_sgi(BOARD_NEST_SGI_INTID, TIMER_PPI_PRIORITY / 2U); + timer_init(); } @@ -81,10 +94,15 @@ void board_init(void) /* board_irq_handler */ /**************************************************************************/ -void board_irq_handler(void) -{ - unsigned long intid = gicv3_acknowledge(); +/**************************************************************************/ +/* board_irq_service -- service an already-acknowledged INTID. */ +/* */ +/* Split out so the nesting path in entry.S can acknowledge in IRQ mode */ +/* before nesting starts. Does not acknowledge and does not EOI. */ +/**************************************************************************/ +void board_irq_service(unsigned long intid) +{ if (intid == GICV3_SPURIOUS_INTID) { /* No interrupt was actually pending; must not be acknowledged. */ @@ -93,6 +111,17 @@ void board_irq_handler(void) return; } + /* Depth is incremented after the spurious check so a spurious entry does + not register as nesting. It reaches 2 only if a second interrupt is + taken while this handler is still on the stack, which cannot happen + without TX_ENABLE_IRQ_NESTING. */ + + board_nest_depth++; + if (board_nest_depth > board_nest_max) + { + board_nest_max = board_nest_depth; + } + if (intid == (unsigned long) TIMER_PPI_INTID) { board_timer_intid = intid; @@ -102,12 +131,71 @@ void board_irq_handler(void) this also deasserts the level-sensitive timer output. */ timer_reload(); + + /* Provoke the nesting, if this image asked for it. Raising the SGI + here is only useful because _tx_thread_irq_nesting_start has already + re-enabled IRQ; the bounded spin gives the GIC the chance to deliver + it before this handler returns, so the nesting is deterministic + rather than a race we hope wins. The bound matters: without + TX_ENABLE_IRQ_NESTING the SGI can never arrive and this must not + become a hang. */ + + if (board_nest_provoke != 0UL) + { + unsigned long seen = board_sgi_count; + unsigned long guard; + + gicv3_send_sgi(BOARD_NEST_SGI_INTID); + + for (guard = 0UL; guard < 100000UL; guard++) + { + if (board_sgi_count != seen) + { + break; + } + } + } + _tx_timer_interrupt(); } + else if (intid == (unsigned long) BOARD_NEST_SGI_INTID) + { + board_sgi_count++; + + /* Depth above 1 means this SGI arrived inside another handler, which + is the whole point of the test. */ + + if (board_nest_depth > 1UL) + { + board_sgi_nested_count++; + } + } else { board_unexpected_intid = intid; } - gicv3_end_of_interrupt(intid); + board_nest_depth--; +} + + +/**************************************************************************/ +/* board_irq_handler -- the non-nesting entry point. */ +/* */ +/* Acknowledges, services and ends the interrupt, all in IRQ mode with */ +/* interrupts masked. entry.S calls this when the image was built without */ +/* TX_ENABLE_IRQ_NESTING, so the behaviour of every existing image is */ +/* exactly what it was. */ +/**************************************************************************/ + +void board_irq_handler(void) +{ + unsigned long intid = gicv3_acknowledge(); + + board_irq_service(intid); + + if (intid != GICV3_SPURIOUS_INTID) + { + gicv3_end_of_interrupt(intid); + } }