I'm running a baremetal application in Qemu on a xilinx-zynq-a9
machine. I'm
trying to leverage the private timer interrupt but am running into issues with
the interrupt re-triggering when I don't think it should be. I successfully
enable the private timer and sure enough my interrupt does trigger after
several seconds (like I expect it to), but then it never seems to re-trigger
continuously and not at the fixed interval I expect.
Just stepping through code with the debugger I do re-enter my main function
(although this only happens when I step through instruction by instruction,
letting it free-run it never seems to touch main again). I manually set-up the
IRQ, FIQ, and normal stack and thought initially that I was corrupting one of
them, but when I enter the IRQ (and when I leave it by manually stepping
through code) I see the $sp
register is jumping back to the region of memory
I expect, the cpsr
register reports that its in the appropriate mode (IRQ or
SVC depending).
I think this is because the GIC isn't de-asserting the interrupt even though I
think I'm doing it. Following an irq example on
github
and gic example on
github I do hit irq_handler
when the private timer counts down the first time, and isr()
is successfully executed:
void __attribute__((interrupt("IRQ"))) irq_handler(void)
{
uint16_t irq = gic_acknowledge_interrupt();
isr_ptr isr = callback(irq);
if (isr != NULL)
{
isr();
}
gic_end_interrupt(irq);
}
But even after acknowledging the interrupts, clearing the ISR of the timer, and
signaling the end of the interrupt (in that order) I essentially re-enter the
ISR immediately. Indeed, setting a breakpoint at address 0x18
where my vector
table lives is hit almost immediately.
uint16_t gic_acknowledge_interrupt(void)
{
// read from PERIPHBASE + 0x100 + 0x0C to
// get pending interrupt. This seems correct and returns 29 (which is the
// ID corresponding to the private timer ISR
return gic_ifregs->ICCIAR & ICCIAR_ID_MASK; // ICCIAR_ID_MASK = 0x3FFFu
}
static void ptimer_isr(void)
{
// Write 0x1 to PERIPHBASE + 0x600 + 0x0C to clear interrupt
WRITE32(pt_regs->timer_interrupt_status, 0x1);
foo(); // do something
}
void gic_end_interrupt(uint16_t number)
{
// This is a WO register
// Write ID(29 for private timer) to PERIPHBASE + 0x100 + 0x10 to clear interrupt
WRITE32(gic_ifregs->ICCEOIR, (number & ICCEOIR_ID_MASK)); // ICCEOIR_ID_MASK = 0x3FFFu
}
Moreover, I've put the private timer into single shot mode and verified that it does not start counting again after the first countdown event occurs. Even in that case the IRQ handler is hit again.
I've even tried using the global timer instead of the private timer and I see the exact same behavior with it.
So in short:
- I seem to be properly enabling the private timer
- I seem to be properly enabling interrupts and registering the private timer interrupt with the GIC
- I do hit the IRQ handler when I expect to the first time
- If I step through with the debugger I do leave the IRQ for a bit, which leads to me believe my stack isn't corrupted or anything
- I re-enter the irq_handler unexpectedly and do still detect a pending
interrupt with
gic_acknowledge_interrupt()
even though it should have been cleared
It's like the interrupt isn't being cleared, even though I think I'm doing that, and the GIC is still signaling that the interrupt is pending, but I'm not sure why.
Edit:
Adding trace
After adding -d trace:gic*
to my QEMU invocation. I now see the behavior below. I'm not familiar with how to interpret tracepoints, but immediately after a write to gic_end_interrupt()
I see gic_update_bestirq cpu 0 irq 29 priority 0 cpu priority mask 248 cpu running priority 256
and
gic_update_set_irq cpu[0]: irq = 1
. but NOT gic_set_irq irq 29 level 1 cpumask 0x1 target 0x1
.
// Entry into irq_handler
gic_set_irq irq 29 level 1 cpumask 0x1 target 0x1
gic_update_bestirq cpu 0 irq 29 priority 0 cpu priority mask 248 cpu running priority 256
gic_update_set_irq cpu[0]: irq = 1
// gic_acknowledge_interrupt()
gic_acknowledge_irq cpu 0 acknowledged irq 29
gic_cpu_read cpu 0 iface read at 0x0000000c: 0x0000001d
// gic_end_interrupt()
gic_cpu_write cpu 0 iface write at 0x00000010 0x0000001d
// Why is this immeadietly set again?
gic_update_bestirq cpu 0 irq 29 priority 0 cpu priority mask 248 cpu running priority 256
gic_update_set_irq cpu[0]: irq = 1
System Information
Additionally, for my system:
- Invoking
qemu-system-arm
with QEMU emulator version 8.0.2 - Running a bare-metal application on the
xilinx-zynq-a9
machine - Compiled with
-march=armv7-a -marm
Timer configuration
I didn't add the entire source code here, but it should be enough to get an idea of what's happening. I borrowed some from an example on github that uses QEMU and an interrupt successfully albeit w/ a different machine. Additionally, I've verified that the control register and the load register have the value I expect after configuration. I've also verified that the timer does start counting down and triggers an interrupt after the counter reaches zero (though again, I never seem to be able to clear the interrupt despite calling WRITE32(pt_regs->timer_interrupt_status, 0x1);
when the interrupt is handled).
// using coprocessor to get PERIPHBASE
uint32_t cpu_get_periphbase(void) {
uint32_t result;
_asm("mrc p15, #4, %0, c15, c0, #0" : "=r" (result));
return result;
}
#define PRIVATE_TIMER_OFFSET (0x600u) // offset per documentation
#define PT_BASE ((cpu_get_periphbase() + PRIVATE_TIMER_OFFSET))
error_code_t init_ptimer(
const timer_auto_control_t continuous,
const uint16_t clock_period_ms,
const uint8_t prescaler,
isr_ptr callback
)
{
// Validate clock_period_ms and prescaler is valid
//...
// Calculate load_value to put into load register
pt_regs = (ptimer_registers*) PT_BASE;
// Disable timer by writing 0 to first bit of
// PERIPHBASE + PRIVATE_TIMER_OFFSET + 0x8 (timer control register
toggle_ptimer(TIMER_DISABLE);
// Update load value
WRITE32(pt_regs->timer_load, load_value);
uint32_t control_reg_mask = 0;
control_reg_mask |=
(continuous << PRIVATE_AUTO_RELOAD_BIT_OFFSET) | // offset bit 1 of ctrl reg
(prescaler << PRESCALER_BIT_OFFSET); // offset bit 8 of ctrl reg
// Enable IRQ if that's desired
if(callback != NULL)
{
control_reg_mask |=
(0x1 << IRQ_ENABLE_BIT_OFFSET); // offset bit 2 of ctrl reg
ptimer_isr_callback = callback;
// register interrupt with irq handler
irq_register_isr(
PTIMER_INTERRUPT_ID,
ptimer_isr);
}
// Update control register
WRITE32(pt_regs->timer_control, control_reg_mask);
return NO_ERR;
}