Building Bare-Metal ARM Systems with GNU: Part 6
note: In this series of ten articles Miro Samek of Quantum Leaps
details developing apps on the ARM processor using QNU, complete with
source code in C and C++.
In this part of the series I tackle interrupt handling for the ARM processor in the simple foreground/background architecture without any underlying multitasking OS or kernel (bare metal). The interrupt handling scheme presented here fully supports nesting of interrupts and can work with or without an interrupt controller external to the ARM7/ARM9 core.
In this part I describe interrupt handling in general terms and in the following installments I provide detailed description of interrupt locking policy, interrupt handler "wrappers" in assembly, C-level interrupt service routines, and finally interrupt testing strategies for ARM-based MCUs.
The recommended reading for this part includes: ARM Technical Support Note "Writing Interrupt Handlers" , Philips Application Note AN10381 "Nesting of Interrupts in the LPC2000" , and Atmel Application Note "Interrupt Management: Auto-vectoring and Prioritization" .
The ARM core supports two types of interrupts: Interrupt Request (IRQ) and Fast Interrupt Request (FIQ), as well as several exceptions: Undefined Instruction, Prefetch Abort, Data Abort, and Software Interrupt. Upon encountering an interrupt or an exception the ARM core does not automatically push any registers to the stack.
If the application wants to nest interrupts (to take advantage of the prioritized interrupt controller available in most ARM-based MCUs), the responsibility is entirely with the application programmer to save and restore the ARM registers.
GNU gcc provides the function __attribute__ ((interrupt ("IRQ"))) to indicate that the specified C/C++ function is an IRQ handler (similarly the __attribute__ ((interrupt ("FIQ"))) is provided for FIQ handlers).
However, these attributes are only designed for "simple" (non-nesting) interrupt handlers. This is because functions designated as interrupts do not store all of the context information (e.g., the SPSR is not saved), which is necessary for fully re-entrant interrupts .
At the same time, most ARM-based MCUs contain a prioritized interrupt controller that specifically supports nesting and prioritization of multiple interrupt sources. This powerful hardware feature cannot be used, however, unless the software is actually capable of handling nested interrupts.
Interrupt Handling Strategy
To enable interrupt nesting, the handler must at some point unlock interrupts, which are automatically locked at the ARM core level upon the IRQ/FIQ entry. Generally, all documented strategies for handling nested interrupts in the ARM architecture involve switching the mode away from IRQ (or FIQ) to the mode used by the task-level code before enabling interrupts [1, 2, 3].
The standard techniques also use multiple stacks during interrupt handling. The IRQ/FIQ mode stack is used for saving a part of the interrupt context and the SYSTEM/USER stack (or sometimes the SVC stack) is used for saving the rest of the context. ARM Ltd. recommends using SYSTEM mode while programming reentrant interrupt handlers .
The interrupt handling strategy for bare-metal ARM system described here also switches away from the IRQ/FIQ mode to SYSTEM mode before enabling interrupt nesting, but differs from the other schemes in that all the CPU context is saved to the SYSTEM/USER stack and the IRQ/FIQ stacks are not used at all.
Saving the context to the separate interrupt stack has value only in multitasking kernels that employ a separate stack for each task. Using multiple stacks in the simple foreground/background architecture with only one background task (the main() loop) has no value and only adds complexity.
|Figure 1. General interrupt handling strategy with the interrupt controller|
Figure 1 above illustrates the steps of IRQ processing. The sequence starts when the ARM core recognizes the IRQ. The ARM core switches to the IRQ mode (CPSR[0-4]==0x12) and the PC is forced to 0x18.
As described in Part 2 of this series ("Startup Code and the Low-level Initialization"), the hardware vector table at address 0x18 is initialized to the instruction LDR pc,[pc,#0x18].
This instruction loads the PC with the secondary jump table entry at 0x38, which must be initialized to the address of the ARM_irq handler "wrapper" function written in assembly. The upcoming Part 8 of this series of articles will describe the ARM_irq "wrapper" function in detail.
For now, in this general overview I'll ignore the implementation details of ARM_irq and simply summarize that it saves all required registers and switches the mode away from IRQ to the SYSTEM mode (CPSR[0-4]==0x1F). The ARM_irq assembler "wrapper" function is generic and you don't need to adapt it in any way for various ARM MCUs.
As shown in the middle section of Figure 1 above, the generic ARM_irq "wrapper" function then calls the interrupt handler BSP_irq(), which is board-specific because it depends on the particular interrupt controller (or the lack of it).
BSP_irq() can be coded in C as a regular C-function (not an interrupt ("IRQ") function!) and is called in the SYSTEM mode, just like all other C functions in the application. Please note, though, that BSP_irq() is invoked with the IRQ disabled and FIQ interrupt enabled, which is the same state of the interrupt bits as the setting established in hardware upon the IRQ entry.
In the absence of an interrupt controller, you can handle the IRQ interrupt directly in BSP_irq(). Generally, in this case you should not re-enable IRQ interrupt throughout the IRQ processing because you have no hardware mechanism to prevent a second instance of the same interrupt from preempting the current one.
The interrupt locking policy that I describe in the next part of this article series is safe to use in the IRQ C-level handlers without an interrupt controller.
In the presence of an interrupt controller, the sequence is a bit more involved (see again Figure 1). The function BSP_irq() first reads the current interrupt vector from the interrupt controller.
The read cycle of the vector address starts prioritization of this interrupt level in the interrupt controller, so after this instruction it's safe to re-enable all interrupts (e.g., via the assembly instruction asm("MSR cpsr_c,#0x1F");).
Next, the BSP_irq() function calls the interrupt vector via the pointer to function syntax ((*vect)()). For this to work, the interrupt controller must be initialized with the addresses of the interrupt service routines (ISRs), as shown in the bottom part of Figure 1 above.
After the interrupt handler returns, the BSP_irq() function locks both IRQ and FIQ interrupts (e.g., via the assembly instruction asm("MSR cpsr_c,#(0x1F | 0x80 | 0x40)");). Finally, BSP_irq() writes the End-Of-Interrupt instruction to the interrupt controller, which terminates the prioritization of this interrupt level.
The code accompanying this article provides the example of the BSP_irq() function for the Atmel Advanced Interrupt Controller (AIC). Other interrupt controllers use slightly different register names and addresses, but work very similarly.
NOTE: The function BSP_irq() must be compiled to ARM, if you use the inline assembly instruction asm("MSR cpsr_c,#0x1F") to unlock and instruction asm("MSR cpsr_c,#(0x1F | 0x80)") to lock interrupts. The MSR instruction is not available in the Thumb instruction set.
The BSP_irq() function returns eventually to the generic ARM_irq() assembler wrapper function (see the middle section of Figure 1). The ARM_irq() assembler "wrapper" restores the context from the SYSTEM stack, performs the mode switch back to IRQ and performs the standard return from exception via the MOVS pc,lr instruction.
Handling of FIQ interrupts is similar to IRQ as far as the assembler "wrapper" function and the vector table initialization are concerned. The main difference between FIQ and IRQ is that the FIQ line is typically not managed by the priority controller (such as the Atmel AIC, NXP VIC, or others), as illustrated in Figure 2 below.
|Figure 2 Typical ARM system with an interrupt controller external to the ARM core|
The consequences of this hardware design are at least two-fold. First, you can simply handle the FIQ directly in the BSP_fiq() C-level function, without any indirection via the interrupt controller.
In other words, even though most interrupt controllers inside popular ARM MCUs support the vectoring feature for FIQ, it does not add much value. The second, and far more important consequence, is that you should never enable FIQ or IRQ interrupts throughout the FIQ processing.
If you were to enable FIQ or IRQ, you would make the currently executing FIQ handler vulnerable to preemptions by the IRQs or the second instance of the currently handled FIQ. Both cases represent priority inversions.
The interrupt locking policy that I describe in the next part of this article series (Part 7) is safe to use in the FIQ C-level handler function. The accompanying code for this article includes the example of the FIQ coded directly in BSP_fiq() handler function.
It's perhaps important to note that the interrupt handling strategy presented here does not use the auto-vectoring feature described in the application notes [2} and . Auto-vectoring occurs when the following LDR instruction is located at the address 0x18 for the IRQ (this example pertains to the Atmel's AIC):
When an IRQ occurs, the ARM core forces the PC to address 0x18 and executes the LDR pc,[pc,#-0xF20] instruction. When the instruction at address 0x18 is executed, the effective address is: 0x20 " 0xF20 = 0xFFFFF100 (0x20 is the value of the PC when the instruction at address 0x18 is executed due to pipelining of the ARM core).
This causes the ARM core to load the PC with the value read from the AIC_IVR register located at 0xFFFFF100. The read cycle causes the AIC_IVR register to return the address of the currently active interrupt service routine.
Thus, the single LDR pc,[pc,#-0xF20] instruction has the effect of starting the prioritization of the current IRQ and directly jumping to the correct ISR, which is called auto-vectoring.
The consequence of auto-vectoring is that the interrupt service routines hooked to the interrupt controller cannot be plain C-functions but rather each one of them must deal directly with the complexities of the IRQ or FIQ modes.
Instead of repeating the IRQ entry and exit sequence in each and every IRQ interrupt service routine, the implementation I describe here uses only one generic, low-level, re-entrant IRQ handler (ARM_irq) that encapsulates the "ARM-magic" and then calls the higher-level handler BSP_irq(), which can be a plain C function. Similar approach is taken for handling FIQs.
Please note that even though "auto-vectoring" is not used, the BSP_irq() function can take full advantage of the vectoring feature of the interrupt controller by reading the vector from the interrupt controller and calling the handler via a pointer-to-function. This, however, happens later in the IRQ sequence and strictly speaking cannot be called "auto-vectoring".
Coming Up Next
In Part 7 next in this series of articles I'll describe the interrupt locking policy for ARM that would be safe for both IRQ and FIQ interrupts as well as the task level code (the code called from main()). I'll explain the details of the low-level interrupt handlers in Part 8. Stay tuned.
To read Part 1, go to What's
need to get started.
To read Part 2, go to Startup code and the low level initialization
To read Part 3, go to The Linker Script.
To read Part 4, go to C and C++ compiler options
To read Part 5, go to Fine-tuning the application
Miro Samek, Ph.D., is president of Quantum Leaps, LLC. He can be contacted at firstname.lastname@example.org.
 ARM Technical Support Note "Writing Interrupt Handlers" available online.
 Philips Application Note AN10381 "Nesting of Interrupts in the LPC2000" available online as a PDF file.
 Atmel Application Note "Interrupt Management: Auto-vectoring and Prioritization" available online as a PDF file.