mirror of
https://github.com/eclipse-threadx/threadx.git
synced 2026-10-06 06:59:08 +08:00
Exercised the nested IRQ path, which had never once been entered (#611)
_tx_thread_irq_nesting_start and _tx_thread_irq_nesting_end have shipped in this port since it was written, compiled into every build, and nothing had ever called either one. Not on the model, not on silicon, not in any demo. demo_nesting.elf enters them. Provoking nesting needs two sources with different priorities. The generic timer PPI was already there; the second is an SGI, which a core can raise on itself. gicv3.c gains gicv3_enable_sgi and gicv3_send_sgi for that. ICC_SGI1R is 64-bit, so in AArch32 it is an MCRR rather than an MCR, and the AArch64 name the Cortex-A72 example uses, S3_0_C12_C11_5, does not transcribe to the AArch32 CP15 space. The encoding here is confirmed by check N1 in the demo, which raises an SGI from thread context and requires it to be delivered and dispatched. The order of the pairing is the whole difficulty, and getting it wrong does not fail gracefully. The interrupt must be acknowledged BEFORE nesting starts. Reading ICC_IAR1 is what raises the GIC running priority to this interrupt's own, which masks it and everything of equal or lower priority; only then is re-enabling IRQ safe. My first attempt called nesting_start first and acknowledged inside the handler, so the still-pending, still-level-asserted timer was taken again the instant IRQ was enabled, and again, until the IRQ and System stacks were destroyed. It presented as garbage on the console and a hang with no fault to point at, and it broke demo_m3 and demo_threadx while leaving boot_check, demo_m2 and demo_mpu passing, because only the first two depend on the tick advancing. The Cortex-R5 example BSP states the requirement in one line: "ensure all IRQ interrupts are cleared prior to enabling nested IRQ interrupts." So entry.S now acknowledges in IRQ mode, carries the INTID in r4 -- which survives the mode switch, since only SP and LR are banked -- and also pushes it on the IRQ stack so a nested level reusing r4 cannot lose the outer level's value. End-of-interrupt waits until after nesting_end, in IRQ mode with interrupts masked, so dropping the running priority cannot re-admit the same interrupt. board_irq_handler splits in two. board_irq_service does the middle part on an already-acknowledged INTID and neither acknowledges nor EOIs; board_irq_handler keeps its old shape as the non-nesting entry point, so images built without TX_ENABLE_IRQ_NESTING behave exactly as before. The nesting instrumentation in irq_dispatch.c is inert unless an image asks for it. board_nest_provoke gates the SGI that the timer handler raises, so every other image sees the handler it always had. Verified on FVP_BaseR_AEMv8R. The nesting demo reports max depth 2, fifty nested SGIs across fifty ticks, depth unwound to zero, the tick still advancing, the low-priority thread still scheduled, and no spurious or unexpected INTIDs. In the same nesting configuration the five existing images all pass, so the split did not disturb the ordinary path. Both toolchains build it, GNU with no warnings and Arm Toolchain for Embedded 22.1.0 as well. Not done here: the S32Z280 example keeps its own irq_dispatch.c and entry.S and is untouched, so nesting on silicon is a separate change. Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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). */
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user