October 21, 2023

Orange Pi H3 -- ARM Interrupts

We are talking about a 32 bit ARM Cortex-A chip here. Specifically an ARM Cortex-A7 MPCore processor. There is a cluster of 4 ot ehse used in the Allwinner H3 chip.

Interrupts on other ARM chips can be quite different. The M series ARM chips like the STM32F103 have a very different vector scheme, yet a lot in common.

Interrupts are a specific case of what are called "exceptions". I won't try to generalize the discussion to include these.

IRQ and FIQ

The Cortex A offers you a choice of two flavors of interrupts. You can set up an interrupt to be handled as an FIQ or an IRQ. To make my life simpler, I make everything an IRQ. The advantage of an FIQ would be that fewer registers need to be saved.

Vector table

        .align  5
.globl vectors
vectors:
        b       reset
        ldr     pc, _undef
        ldr     pc, _swi
        ldr     pc, _iabt
        ldr     pc, _dabt
        ldr     pc, _unused
        ldr     pc, _irq
        ldr     pc, _fiq
Don't ask me why I use the "ldr" instruction instead of just "b".

On the Cortex-A ARM, the vector table looks like this. It only has 8 entries. The entries are actual ARM instructions and each entry thus ought to be 32 bits in size. On reset, the processor expects to find this table at address zero (or whatever address the processor fires up at, perhaps 0xffff0000). In fact, the H3 bootrom looks like this:

ffff0000:       ea000008        b       0xffff0028      ; reset
ffff0004:       ea000006        b       0xffff0024      ; undefined instruction
ffff0008:       ea000005        b       0xffff0024      ; software interrupt
ffff000c:       ea000004        b       0xffff0024      ; prefetch abort
ffff0010:       ea000003        b       0xffff0024      ; data abort
ffff0014:       ea000002        b       0xffff0024      ; unused
ffff0018:       ea000011        b       0xffff0064      ; IRQ
ffff001c:       ea000000        b       0xffff0024      ; FIQ
After your own code gets running, you can specify the location of the vector table by loading the VBAR register with the table location using code like this:
asm volatile ( "mcr p15, 0, %0, c12, c0, 0" : : "r" ( val ) )

Interrupt Stack

The processor switches to an entirely different stack to handle interrupts. Mode bits in the PSR (processor status register) are set to indicate that the processor is in interrupt mode and this causes it to use the other stack.

This means you either must have initialized this stack pointer when your code set itself up, or you must load the stack pointer for every interrupts.

Saving registers - a brief introduction

Since you have no idea what the processor might have been doing when the interrupt hit, you must save ALL the registers when you handle an interrupt. And you must restore all of them too when you finish.

The ARM has a pair of instructions that make it fairly easy to save and restore all the registers, namely "stm" and "ldm" (store and load multiple). The ARM also has a bunch of interesting (and tricky) addressing modes that can be used to specify how all those registers get shoved onto the stack.

As a quick aside, here is code from the H3 bootrom to handle FIQ:

ffff0064:       e24ee004        sub     lr, lr, #4
ffff0068:       e92d5fff        push    {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip, lr}
ffff006c:       eb000723        bl      0xffff1d00
ffff0070:       e8fd9fff        ldm     sp!, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip, pc}^

Returning from an interrupt

The first thing to know is a surprise. The return address is in the "lr" register, almost. This is just like a subroutine call (sort of). The surprise (the almost part) is that you have to subtract 4 from the value of "lr" to return to the right place. This is true for both IRQ and FIQ and some (but not all) other exceptions. For other exceptions, you can either use "lr" as-is or (as in the case of a data abort) you may need to subtract 8 from it.

Now the mystery. How does the processor know that you are returning from an interrupt so that it can clear the "interrupt mode" bits in the PSR? For example the following instruction is a commonly recommended way to return from an interrupt:

subs    pc,lr,#4
Take a look at the FIQ code from the bootrom up above. Here, the first thing done is to adjust the value in "lr" by subtracting 4 from it. This means that when the "ldm" instruction restores that value into the PC when the above returns, it will resume at the right location.

While we are looking at that bootrom code, why is that "up-arrow" (caret) at the end of that ldm statement? It is crucial. It says that "for an LDM instruction that loads the PC, the CPSR should also be set (copied) from the SPSR.

What about the SUBS instruction. First of all, note that this is not the usual "SUB" instruction. The trailing letter "S" indicates that the instruction updates the CPSR, specifically if the destination register is R15 (the PC) then the CPSR is set (copied) from the SPSR.

Some notes on ARM registers

Consider the register list from the bootrom FIQ code above, namely:
push    {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip, lr}
Registers sp and lr are "banked" for an IRQ. This means that the processor switches to an entirely separate pair of registers for the duration of the interrupt. It sets "lr" as we have already discussed to allow return from the interrupt (the other "lr" is left unmolested). The other "sp" is switched to, but no value is set, as previously mentioned.

As a side note, an FIQ would switch to banked registers for R8 and up. This means that you would either need to save fewer registers (only R0 to R7), or if you were really clever and avoided R0-R7 you would not need to save/restore any registers!

If this is true, why is the bootrom code above saving r8 and so forth? Why indeed?

The ARM has 16 general registers. R15 is the PC, and we have no need to push that. If we did and then restored it, we would loop back to this push instruction all over again or do something else equally pointless. R14 is the lr. R13 is the sp, and we have no interest in pushing it. For whatever reason, R10 has the alias sl, R11 has the alias fp, and R12 has the alias ip. That accounts for all the registers that ought to be accounted for.

push and pop

These are really aliases for more general instructions:
PUSH {r3}	is: str r3, [sp, #-4]!
POP {r3}	is: ldr r3, [sp], #4
Actually ARM allows a full list of registers in each, but these then become aliases for stmdb and ldmia. so:
pop {r0-r3}	is: ldm sp!, {r0-r3}

It is bad form to push the pc or pop the lr. Such is either deprecated or perhaps even illegal. Having the base register in the list is also bad form.

Addressing modes and stm/ldm

The general description of stm is:
STMxx Ry{!}, reglist{^}
The exclamation says to modify Ry to reflect the end result.
The caret says to store user mode registers rather than the current mode.

The "xx" is one of four modes: IA, IB, DA, DB.
Where IA is increment after, IB is increment before.
Where DA is decrement after, DB is decrement before.

So, a push would use a stmdb and a pop would use a ldmia.

Three levels of interrupt masking

We start at the source, let's say the UART. There is a control register as part of the UART to enable interrupts. Once enabled, the interrupt goes to the interrupt controller (the GIC). Here a multitude of incoming interrupts get channeled (multiplexed) to the single IRQ input (or perhaps the duo of IRQ and FIQ). Interrupts can be enabled or disabled here. Lastly the processor PSR register has mask bits for IRQ and FIQ. It is called a mask bit because you set the bit to prohibit (mask) the interrupt.

So, to get an interrupt you have to allow it at each of these 3 levels. When you handle the interrupt, you need to acknowledge it at two levels. You have to tell the GIC that you are handling it, and you need to tell the device (such as the UART) that you have handled the interrupt. If you fail to do this, and try to return from the interrupt, you will immediately get kicked back in! This is a well known "hard interrupt loop" that will just hang your software.

Code to mask interrupts in the PSR

I use these macros to enable and disable interrupts. Another approach might be to use inline assembly to read and write the CPSR, i.e. implement "get_PSR" and "set_PSR". This would allow other monkey business withe the PSR, but I like using something like the following as it tends to yield more readable code.

#define int_DISABLE \ asm volatile ( "mrs r0, cpsr; \ orr r0, r0, #0xc0; \ msr cpsr, r0" ::: "r0" ) #define int_ENABLE \ asm volatile ( "mrs r0, cpsr; \ bic r0, r0, #0xc0; \ msr cpsr, r0" ::: "r0" )


Note that I am allowing or blocking both IRQ and FIQ together.

More

Here is a great article on ARM interrupt handling: Have any comments? Questions? Drop me a line!

Tom's electronics pages / tom@mmto.org