Verified nested FIQ handling on S32Z280 silicon (#614)

#613 exercised the FIQ nesting routines on the FVP. What the model could not show
is whether a real GIC-600 routes Group 0 to FIQ the same way, which is the reason
to run it here. It does, and the counts match the model exactly.

The Group 0 support ports across unchanged: IGRPEN0 and BPR0 on the CPU interface,
Group 0 in the distributor, gicv3_enable_sgi_group0, gicv3_send_sgi_group0 through
ICC_SGI0R, and the separate Group 0 acknowledge and end-of-interrupt pair. entry.S
routes the EL1 FIQ vector into _tx_thread_fiq_context_save with the acknowledge
before nesting starts, and leaves FIQ unmasked on the drop to EL1 when FIQ support
is compiled in, for the same reason as on the FVP: tx_thread_stack_build only
clears a thread's F bit in that configuration.

One structural difference from the FVP cost a link. This example reports faults
through FAULT_TAIL rather than FAULT_REPORT, so the vector table needed a new
el1_fiq_entry label that falls back to fault_el1_fiq. Placing that label inside the
TX_R52_USE_THREADX_IRQ guard broke s32z280_boot.elf, which does not define it: the
vector reference is unconditional, so the label has to be too. It now sits outside
the guard and carries its own, the same shape the demo_m2 link break in #613
forced on the FVP side.

Verified on S32Z280 silicon:

    F1 FIQ delivered and dispatched            PASS
    low-priority FIQ count  = 0x00000015       21
    high-priority FIQ count = 0x00000014       20
    nested FIQ count        = 0x00000014       20 of 20 nested
    max FIQ depth           = 0x00000002
    FIQ depth now           = 0x00000000
    F2 FIQ nested inside an FIQ handler        PASS
    F3 FIQ nesting unwound to depth zero       PASS
    F4 IRQ tick undisturbed by FIQ work        PASS
    F5 lower-priority thread still scheduled   PASS
    F6 no unexpected Group 0 INTID             PASS

No regression, both configurations checked on the board. In the FIQ build the
ThreadX demo still reports 100 ticks with 20 preemptions and the boot image still
passes its cache and protection checks. In the default build the demo is unchanged
and the image links no FIQ or Group 0 symbol at all, so the work is absent rather
than dormant where it is not wanted -- worth confirming on hardware rather than
reasoning about, because the default build now reaches the FIQ vector through a new
label even though that label only branches to the fault reporter.

entry.S assembles in all four combinations of TX_R52_USE_THREADX_IRQ and FIQ
support, on both toolchains, and every file builds with GNU without warnings and
with Arm Toolchain for Embedded 22.1.0.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
This commit is contained in:
Frédéric Desbiens
2026-08-14 13:34:34 -04:00
committed by GitHub
parent 19d90a49c4
commit ea91beae42
7 changed files with 579 additions and 1 deletions
@@ -164,3 +164,40 @@ if(TX_R52_ENABLE_IRQ_NESTING)
${EVB_LINK_QUIET_RWX}
)
endif()
# Nested FIQ handling on silicon. Needs TX_R52_ENABLE_FIQ_NESTING, which the
# port-level CMakeLists already requires TX_R52_ENABLE_FIQ alongside, so the image
# exists only where both the FIQ vector path and the nesting pairing are compiled
# in -- as s32z280_nesting.elf does for the IRQ side.
if(TX_R52_ENABLE_FIQ_NESTING)
add_executable(s32z280_fiq.elf EXCLUDE_FROM_ALL
${EVB_DIR}/entry.S
${EVB_DIR}/tx_initialize_low_level.S
${EVB_DIR}/linflexd.c
${EVB_DIR}/timer.c
${EVB_DIR}/mpu.c
${EVB_DIR}/gicv3.c
${EVB_DIR}/irq_dispatch.c
${EVB_DIR}/gic_probe.c
${EVB_DIR}/cache.c
${EVB_DIR}/demo_fiq_s32z280.c
)
target_compile_definitions(s32z280_fiq.elf PRIVATE TX_R52_USE_THREADX_IRQ=1)
target_compile_options(s32z280_fiq.elf PRIVATE -g)
target_link_libraries(s32z280_fiq.elf PRIVATE threadx)
target_include_directories(s32z280_fiq.elf PRIVATE
${EVB_DIR}
${CMAKE_SOURCE_DIR}/common/inc
${CMAKE_SOURCE_DIR}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc
)
target_link_options(s32z280_fiq.elf PRIVATE
-T${EVB_DIR}/link.lds
-nostartfiles
-Wl,-Map=s32z280_fiq.map
${EVB_LINK_QUIET_RWX}
)
endif()
@@ -124,4 +124,22 @@ extern volatile unsigned long board_sgi_priority;
#define BOARD_NEST_SGI_INTID 8U
/* FIQ observability. Two Group 0 SGIs, because FIQ nesting means an FIQ taken
while an FIQ handler is running, so a single source cannot show it. The
higher-priority one is raised from inside the lower one's handler. */
void board_fiq_handler(void);
void board_fiq_service(unsigned long intid);
extern volatile unsigned long board_fiq_count;
extern volatile unsigned long board_fiq_high_count;
extern volatile unsigned long board_fiq_nested_count;
extern volatile unsigned long board_fiq_depth;
extern volatile unsigned long board_fiq_max_depth;
extern volatile unsigned long board_fiq_unexpected;
extern volatile unsigned long board_fiq_provoke;
#define BOARD_FIQ_LOW_INTID 10U
#define BOARD_FIQ_HIGH_INTID 11U
#endif /* BOARD_H */
File diff suppressed because it is too large Load Diff
@@ -189,7 +189,7 @@ el1_vectors:
b fault_el1_dabt /* 0x10 */
b fault_el1_reserved /* 0x14 */
b el1_irq_entry /* 0x18 IRQ */
b fault_el1_fiq /* 0x1C */
b el1_fiq_entry /* 0x1C FIQ */
@/**************************************************************************/
@/* _start -- reset entry. Entered in THUMB state, Hyp mode (EL2). */
@@ -330,7 +330,19 @@ start_a32:
ldr r0, =el1_entry
msr ELR_hyp, r0
#ifdef TX_ENABLE_FIQ_SUPPORT
/* FIQ left unmasked, IRQ still masked. tx_thread_stack_build only clears a
* thread's F bit when the library was built with FIQ support and nothing
* else ever clears it, so leaving PSR_F set here would mask FIQ until the
* first thread ran and an FIQ raised from bsp_main would silently do
* nothing.
*/
ldr r0, =(PSR_A | PSR_I | MODE_SVC)
#else
ldr r0, =(PSR_A | PSR_I | PSR_F | MODE_SVC)
#endif
msr SPSR_hyp, r0
isb
eret
@@ -536,6 +548,7 @@ __tx_irq_processing_return:
#endif
b _tx_thread_context_restore
#else
/* Standalone: the classic A32 form. lr is adjusted by 4 because an IRQ
@@ -549,6 +562,52 @@ __tx_irq_processing_return:
#endif
@/**************************************************************************/
@/* el1_fiq_entry -- FIQ vector at EL1. */
@/**************************************************************************/
el1_fiq_entry:
#if defined(TX_ENABLE_FIQ_SUPPORT) && defined(TX_R52_USE_THREADX_IRQ)
/* Both guards are needed. TX_ENABLE_FIQ_SUPPORT is PUBLIC on the threadx
* target, so it reaches every image once the library is built with FIQ,
* including images that link no interrupt controller; referencing the GIC
* and board symbols below from those breaks the link outright. That is why
* the IRQ vector is gated the same way.
*
* Group 0 is what the GIC delivers as FIQ, and it has its own acknowledge
* and end-of-interrupt registers -- the Group 1 pair returns the spurious
* INTID for a Group 0 interrupt and leaves it pending, which looks like a
* storm rather than a mistake. As on the IRQ path, the acknowledge happens
* before nesting starts, so the running priority masks this interrupt before
* FIQ is re-enabled.
*/
b _tx_thread_fiq_context_save
.global __tx_fiq_processing_return
__tx_fiq_processing_return:
#ifdef TX_ENABLE_FIQ_NESTING
bl gicv3_acknowledge_group0
mov r4, r0
stmdb sp!, {r4, r5}
bl _tx_thread_fiq_nesting_start
mov r0, r4
bl board_fiq_service
bl _tx_thread_fiq_nesting_end
ldmia sp!, {r4, r5}
mov r0, r4
bl gicv3_end_of_interrupt_group0
#else
bl board_fiq_handler
#endif
b _tx_thread_fiq_context_restore
#else
b fault_el1_fiq
#endif
@/**************************************************************************/
@/* try_execute_data -- branch to a non-executable address on purpose. */
@/* */
@@ -53,6 +53,7 @@
/* Distributor registers. */
#define GICD_CTLR 0x0000U
#define GICD_CTLR_ENABLE_GRP0 (1UL << 0)
#define GICD_CTLR_ENABLE_GRP1 (1UL << 1)
#define GICD_CTLR_ARE (1UL << 4)
@@ -102,6 +103,18 @@ static void write_icc_bpr1(unsigned long value)
__asm__ volatile("mcr p15, 0, %0, c12, c12, 3" : : "r"(value) : "memory");
}
static void write_icc_igrpen0(unsigned long value)
{
/* ICC_IGRPEN0 sits at c12, c12, 6, beside IGRPEN1 at opc2 7. */
__asm__ volatile("mcr p15, 0, %0, c12, c12, 6" : : "r"(value) : "memory");
}
static void write_icc_bpr0(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c12, c8, 3" : : "r"(value) : "memory");
}
static void write_icc_igrpen1(unsigned long value)
{
__asm__ volatile("mcr p15, 0, %0, c12, c12, 7" : : "r"(value) : "memory");
@@ -127,6 +140,12 @@ void gicv3_init(void)
REG32(GICD_BASE + GICD_CTLR) |= GICD_CTLR_ARE;
REG32(GICD_BASE + GICD_CTLR) |= GICD_CTLR_ENABLE_GRP1;
/* Group 0 as well, which is what the GIC delivers as FIQ. Enabling it costs
nothing while no interrupt is assigned to that group, and images that never
use FIQ see no change: assignment is per interrupt, in GICR_IGROUPR0. */
REG32(GICD_BASE + GICD_CTLR) |= GICD_CTLR_ENABLE_GRP0;
/* Redistributor: clear ProcessorSleep and wait for the redistributor to
report that its children are awake, otherwise no interrupt can be
delivered to this core. */
@@ -150,6 +169,14 @@ void gicv3_init(void)
write_icc_pmr(ICC_PMR_UNMASK_ALL);
write_icc_bpr1(0UL);
write_icc_igrpen1(ICC_IGRPEN1_ENABLE);
/* The CPU interface half of Group 0: a Group 0 interrupt reaches the core
only if both the distributor and this are enabled. BPR0 at zero gives no
subpriority grouping, so priority alone decides preemption, which is what
the FIQ nesting test depends on. */
write_icc_bpr0(0UL);
write_icc_igrpen0(ICC_IGRPEN1_ENABLE);
instruction_barrier();
}
@@ -294,6 +321,74 @@ unsigned int gicv3_priority_bits(unsigned int scratch_intid)
}
/**************************************************************************/
/* gicv3_enable_sgi_group0 */
/* */
/* As gicv3_enable_sgi, but leaves the interrupt in Group 0 so it arrives */
/* as an FIQ. The group is chosen by clearing the GICR_IGROUPR0 bit; the */
/* Group 1 version sets it. */
/**************************************************************************/
void gicv3_enable_sgi_group0(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_group0 */
/* */
/* ICC_SGI0R rather than ICC_SGI1R: the two differ only in opc1, 2 against */
/* 0, and each raises the SGI into its own group. Raising a Group 0 */
/* interrupt through the Group 1 register does not deliver it as an FIQ. */
/**************************************************************************/
void gicv3_send_sgi_group0(unsigned int intid)
{
unsigned long low = (((unsigned long) intid & 0xFUL) << 24) | 1UL;
unsigned long high = 0UL;
__asm volatile ("mcrr p15, 2, %0, %1, c12" :: "r" (low), "r" (high) : "memory");
instruction_barrier();
}
/**************************************************************************/
/* gicv3_acknowledge_group0 / gicv3_end_of_interrupt_group0 */
/* */
/* Group 0 has its own pair at c12, c8, where Group 1 uses c12, c12. */
/* Acknowledging a Group 0 interrupt through IAR1 returns the spurious */
/* INTID and leaves the interrupt pending, which presents as an FIQ storm. */
/**************************************************************************/
unsigned long gicv3_acknowledge_group0(void)
{
unsigned long intid;
__asm__ volatile("mrc p15, 0, %0, c12, c8, 0" : "=r"(intid));
return intid & 0xFFFFFFUL;
}
void gicv3_end_of_interrupt_group0(unsigned long intid)
{
instruction_barrier();
__asm__ volatile("mcr p15, 0, %0, c12, c8, 1" : : "r"(intid) : "memory");
}
unsigned long gicv3_acknowledge(void)
{
unsigned long intid;
@@ -58,6 +58,17 @@ void gicv3_enable_sgi(unsigned int intid, unsigned int priority);
void gicv3_send_sgi(unsigned int intid);
/* Group 0 support, which is what produces FIQ rather than IRQ. With a single
security state the GIC routes Group 0 to FIQ and Group 1 to IRQ, so an
interrupt has to be moved into Group 0 to arrive as an FIQ at all. Group 0
has its own acknowledge and end-of-interrupt registers; using the Group 1 ones
on a Group 0 interrupt does not work. */
void gicv3_enable_sgi_group0(unsigned int intid, unsigned int priority);
void gicv3_send_sgi_group0(unsigned int intid);
unsigned long gicv3_acknowledge_group0(void);
void gicv3_end_of_interrupt_group0(unsigned long intid);
/* Implemented priority bits, discovered by write and readback. Leaves the
scratch INTID's priority byte at zero. */
@@ -66,6 +66,14 @@ 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;
volatile unsigned long board_fiq_count;
volatile unsigned long board_fiq_high_count;
volatile unsigned long board_fiq_nested_count;
volatile unsigned long board_fiq_depth;
volatile unsigned long board_fiq_max_depth;
volatile unsigned long board_fiq_unexpected;
volatile unsigned long board_fiq_provoke;
volatile unsigned long board_priority_bits;
volatile unsigned long board_timer_priority;
volatile unsigned long board_sgi_priority;
@@ -116,6 +124,17 @@ void board_init(void)
gicv3_enable_sgi(BOARD_NEST_SGI_INTID, TIMER_PPI_PRIORITY / 2U);
#ifdef TX_ENABLE_FIQ_SUPPORT
/* Two Group 0 SGIs for the FIQ path. Group 0 is what the GIC delivers as
FIQ; the high one takes a numerically lower priority so it can preempt the
low one, which is what FIQ nesting means. Both sit above the timer so the
IRQ and FIQ paths do not interfere. */
gicv3_enable_sgi_group0(BOARD_FIQ_LOW_INTID, 0x40U);
gicv3_enable_sgi_group0(BOARD_FIQ_HIGH_INTID, 0x20U);
#endif
/* The priorities as the GIC will actually compare them. Only the top
board_priority_bits survive; the low ones read back as zero, so two values
that differ only there would collapse together and never preempt. Recorded
@@ -249,3 +268,89 @@ void board_irq_handler(void)
gicv3_end_of_interrupt(intid);
}
}
#ifdef TX_ENABLE_FIQ_SUPPORT
/**************************************************************************/
/* board_fiq_service -- service an already-acknowledged Group 0 INTID. */
/* */
/* The FIQ counterpart of board_irq_service, and split for the same */
/* reason: the acknowledge has to happen before nesting starts. Does not */
/* acknowledge and does not EOI. */
/**************************************************************************/
void board_fiq_service(unsigned long intid)
{
if (intid == GICV3_SPURIOUS_INTID)
{
return;
}
board_fiq_depth++;
if (board_fiq_depth > board_fiq_max_depth)
{
board_fiq_max_depth = board_fiq_depth;
}
if (intid == (unsigned long) BOARD_FIQ_LOW_INTID)
{
board_fiq_count++;
/* Provoke the nested FIQ from inside this handler. Only reachable
because _tx_thread_fiq_nesting_start has re-enabled FIQ; the bounded
spin lets the GIC deliver the higher-priority Group 0 SGI before this
handler returns. Bounded, because with nesting compiled out the
second FIQ cannot arrive and this must not hang. */
if (board_fiq_provoke != 0UL)
{
unsigned long seen = board_fiq_high_count;
unsigned long guard;
gicv3_send_sgi_group0(BOARD_FIQ_HIGH_INTID);
for (guard = 0UL; guard < 100000UL; guard++)
{
if (board_fiq_high_count != seen)
{
break;
}
}
}
}
else if (intid == (unsigned long) BOARD_FIQ_HIGH_INTID)
{
board_fiq_high_count++;
/* Depth above 1 means this FIQ arrived inside another FIQ handler. */
if (board_fiq_depth > 1UL)
{
board_fiq_nested_count++;
}
}
else
{
board_fiq_unexpected = intid;
}
board_fiq_depth--;
}
/**************************************************************************/
/* board_fiq_handler -- the non-nesting FIQ entry point. */
/**************************************************************************/
void board_fiq_handler(void)
{
unsigned long intid = gicv3_acknowledge_group0();
board_fiq_service(intid);
if (intid != GICV3_SPURIOUS_INTID)
{
gicv3_end_of_interrupt_group0(intid);
}
}
#endif