Search code examples
ctimerarmqemu

Arm Cortex-a9 timer triggered IRQ not clearing


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;
}

Solution

  • 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