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:
gic_acknowledge_interrupt()
even though it should have been clearedIt'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.
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
Additionally, for my system:
qemu-system-arm
with QEMU emulator version 8.0.2xilinx-zynq-a9
machine-march=armv7-a -marm
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;
}
I was able to figure out an answer after finding someone having a similar problem on github. It turns out that the ISR wasn't being cleared b/c I was illegally accessing the register to clear it.
Their suggestion was to use the -d guest_errors
option in QEMU. Stepping through the code when I attempted to clear the interrupt via a pointer to my pack struct in my isr call back ptimer_isr
.
typedef volatile struct __attribute__((packed)) {
uint32_t timer_load; /* 0x0 */
uint32_t timer_counter; /* 0x4 */
uint32_t timer_control; /* 0x8 */
uint32_t timer_interrupt_status; /* 0xC */
} ptimer_registers;
static void ptimer_isr(void)
{
// Write 0x1 to PERIPHBASE + 0x600 + 0x0C to clear interrupt
WRITE32(pt_regs->timer_interrupt_status, 0x1);
foo(); // do something
}
I got the following complaint from QEMU:
Invalid write at addr 0x20E, size 1, region '(null)', reason: rejected
Invalid read at addr 0xF, size 1, region 'arm_mptimer_timer', reason: invalid size (min:4 max:4)
This looks like a complaint about byte-size access. Dumping the object code for my isr callback does show a bunch of byte-sized access instructions ldrb
and strb
.
00000bf4 <ptimer_isr>:
bf4: e92d4800 push {fp, lr}
bf8: e28db004 add fp, sp, #4
bfc: e30038d8 movw r3, #2264 ; 0x8d8
c00: e3403030 movt r3, #48 ; 0x30
c04: e5933000 ldr r3, [r3]
c08: e5d3200c ldrb r2, [r3, #12]
c0c: e3a02000 mov r2, #0
c10: e3822001 orr r2, r2, #1
c14: e5c3200c strb r2, [r3, #12]
c18: e5d3200d ldrb r2, [r3, #13]
c1c: e3a02000 mov r2, #0
c20: e5c3200d strb r2, [r3, #13]
c24: e5d3200e ldrb r2, [r3, #14]
c28: e3a02000 mov r2, #0
c2c: e5c3200e strb r2, [r3, #14]
c30: e5d3200f ldrb r2, [r3, #15]
c34: e3a02000 mov r2, #0
c38: e5c3200f strb r2, [r3, #15]
c3c: ebffffc0 bl b44 <tick>
c40: e30038e0 movw r3, #2272 ; 0x8e0
c44: e3403030 movt r3, #48 ; 0x30
c48: e5933000 ldr r3, [r3]
c4c: e12fff33 blx r3
c50: e320f000 nop {0}
c54: e8bd8800 pop {fp, pc}
I changed the struct definition in order to force 32-bit alignment:
typedef volatile struct __attribute__((packed, aligned(4))) {
uint32_t timer_load; /* 0x0 */
uint32_t timer_counter; /* 0x4 */
uint32_t timer_control; /* 0x8 */
uint32_t timer_interrupt_status; /* 0xC */
} ptimer_registers;
and wrote a function which should ensure word-wise access instead of my #define WRITE32(_reg, _val) (*(volatile uint32_t*)&_reg = _val)
macro.
static inline void write_reg32(
uint32_t volatile* const reg,
uint32_t const value )
{
*reg = value;
}
static void ptimer_isr(void)
{
// Write 0x1 to PERIPHBASE + 0x600 + 0x0C to clear interrupt
write_reg32(&(pt_regs->timer_interrupt_status), 0x1);
foo(); // do something
}
Now all the byte-wise access instructions are gone, and my code works as epxected.
00000c24 <ptimer_isr>:
c24: e92d4800 push {fp, lr}
c28: e28db004 add fp, sp, #4
c2c: e30038d8 movw r3, #2264 ; 0x8d8
c30: e3403030 movt r3, #48 ; 0x30
c34: e5933000 ldr r3, [r3]
c38: e283300c add r3, r3, #12
c3c: e3a01001 mov r1, #1
c40: e1a00003 mov r0, r3
c44: ebffffbe bl b44 <write_reg32>
c48: ebffffc9 bl b74 <tick>
c4c: e30038e0 movw r3, #2272 ; 0x8e0
c50: e3403030 movt r3, #48 ; 0x30
c54: e5933000 ldr r3, [r3]
c58: e12fff33 blx r3
c5c: e320f000 nop {0}
c60: e8bd8800 pop {fp, pc}
00000b44 <write_reg32>:
b44: e52db004 push {fp} ; (str fp, [sp, #-4]!)
b48: e28db000 add fp, sp, #0
b4c: e24dd00c sub sp, sp, #12
b50: e50b0008 str r0, [fp, #-8]
b54: e50b100c str r1, [fp, #-12]
b58: e51b3008 ldr r3, [fp, #-8]
b5c: e51b200c ldr r2, [fp, #-12]
b60: e5832000 str r2, [r3]
b64: e320f000 nop {0}
b68: e28bd000 add sp, fp, #0
b6c: e49db004 pop {fp} ; (ldr fp, [sp], #4)
b70: e12fff1e bx lr