Building Bare-Metal ARM Systems with GNU: Part 9
(Editor's 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 I wrap up the interrupt handling for ARM by describing the interrupt service routines (ISRs) in C, as well as the initialization of the ARM vector table and the interrupt controller.
The examples I present here pertain to the Atmel's Advanced Interrupt Controller (AIC) , but should be easy to modify for other interrupt controllers (e.g., NXP's VIC). I also discuss a rudimentary policy of handling other ARM Exceptions, such as Undefined Instruction or Data Abort.
The BSP_irq Handler Function
As described in Part 6, the low-level interrupt "wrapper" function ARM_irq() calls the C-function BSP_irq(), which encapsulates the particular interrupt controller of your ARM-based MCU. The BSP_irq() indirection layer is only necessary to separate the generic ARM_irq() implementation from the vendor-specific interrupt controller interface.
If only the industry could agree on the standardized interface to the interrupt controller, the low-level IRQ handler ARM_irq could perform the vectoring in a standard way and thus eliminate the need for the BSP_irq() indirection.
However, the various ARM-silicon vendors use different register locations for their interrupt controllers, so it's impossible to perform vectoring generically. (Of course, at the cost of losing generality you can eliminate the BSP_irq() function overhead by in-lining it directly inside ARM_irq()).
(NOTE: ARM Limited has standardized the interrupt controller interface in the new ARM v7-M architecture, which comes with the standard Nested Interrupt Controller (NVIC) .)
|Listing 1. The BSP_irq() function defined in the file isr.c.|
Listing 1 above shows the implementation of the BSP_irq() function for the Atmel's AIC. The highlights of the code are as follows:
(1) The function BSP_irq() is assigned to the section .text.fastcode, which the linker script locates in RAM for faster execution (see Part 2).
(2) The BSP_irq() function is a regular C-function (not an IRQ-function!). It is entered with IRQ disabled and FIQ enabled.
(3) This typedef defines the pointer-to-function type for storing the address of the ISR obtained from the interrupt controller.
(4) The current interrupt vector is loaded from the AIC_IVR register into a temporary variable vect. Please note that BSP_irq() takes full advantage of the vectoring capability of the AIC, even though this is not the traditional auto-vectoring.
For vectoring to work, the appropriate Source Vector Registers in the AIC must be initialized with the addresses of the corresponding interrupt service routines (ISRs).
(5) The AIC_IVR is written, which is necessary if the AIC is configured in protected mode (see Atmel's documentation ). The write cycle to the AIC_IVR starts prioritization of this IRQ.
(6) After the interrupt controller starts prioritizing this IRQ, it's safe to enable interrupts at the ARM core level.
(NOTE: Here the inline assembly is used to clear the I-bit in the CPSR register. The MSR instruction is available only in the ARM instruction set, which means that the module containing BSP_irq() must be compiled to ARM.)
(7) The interrupt handler is invoked via the pointer-to-function (vector address) extracted previously from the AIC_IVR.
(8) After the ISR returns, IRQ interrupts are locked at the ARM core level by means of inline assembly.
(9) The End-Of-Interrupt command is written to the AIC, which informs the interrupt controller to end prioritization of this IRQ.
The BSP_fiq Handler Function
The AIC, as most interrupt controllers integrated into ARM-based MCUs, does not protect the FIQ line with the priority controller . Therefore, even though the AIC is still capable of performing vectoring of the FIQ, it doesn't really add much value. The implementation of BSP_fiq() shown in Listing 2 below handles the entire work of the interrupt directly, without any interaction with the AIC.
|Listing 2 The BSP_fiq() function defined in the file isr.c.|
The highlights of Listing 2 above are as follows:
(1) The function BSP_fiq() is assigned to the section .text.fastcode, which the linker script locates in RAM for faster execution (see Part 2).
(2) The BSP_fiq() function is a regular C-function (not an FIQ-function!). It is entered with both IRQ and FIQ disabled and must never enable interrupts.
(3-4) The function BSP_fiq() performs directly the whole work of the interrupt. In this case, the work consists of clearing the interrupt source and setting a flag in a bitmask that is shared with the task-level code and perhaps other interrupts as well.
The function eventFlagSet() is designed to be called from FIQ, IRQ and the main loop, and thus provides an example of a universal communication mechanism within a foreground/background application. Internally, eventFlagSet() protects the shared bitmask with a critical section, which is specifically designed to be safe to use in all contexts (such as task-level, IRQ, and FIQ).
Please refer to the file isr.c in the code accompanying this article for the self-explanatory implementation of this function. You might also want to go back to the critical section implementation described in Part 7 of this article series.
Interrupt Service Routines
The main job of the BSP_irq() indirection layer is to obtain the address of the interrupt service routine (ISR) from the interrupt controller and to invoke the ISR. The ISRs are regular C-functions (not IRQ-type functions!). You are free to compile the ISRs to ARM or Thumb, as you see fit. Listing 3 below shows two examples of ISRs for the Blinky example application accompanying this article.
|Listing 3 Examples of ISRs.|
The highlights of Listing 3 above are as follows:
(1) The C-level ISR is a regular void (*)(void) C-function. The ISR is called in SYSTEM mode with IRQ/FIQ interrupts unlocked at the ARM core level.
(2) The level-sensitive interrupt source is cleared, which in this case is the AT91 Programmable Interval Timer (PIT). Please note that even though interrupts are unlocked at the ARM core level, they are still prioritized in the interrupt controller, so a level-sensitive interrupt source does not cause recursive ISR reentry.
(3) The work of the interrupt consists in this case of setting a shared flag to inform the main() loop about the time tick. The function eventFlagSet() internally protects the shared bitmask with a critical section, which is necessary because IRQ interrupts can preempt each other [see Listing 1(#6)].
Please note that the interrupt controller only allows preemptions by IRQs prioritized higher than the currently serviced interrupt, so ISR_tick() cannot preempt itself.
(4) The spurious ISR is
[NOTE: Spurious interrupts are possible in ARM7/ARM9-based MCUs due to asynchronous interrupt processing with respect to the system clock. A spurious interrupt is defined as being the assertion of an interrupt source long enough for the interrupt controller to assert the IRQ, but no longer present when interrupt vector register is read. The Atmel datasheet  and NXP Application Note  provide more information about spurious interrupts in ARM-based MCUs.]
Initialization of the Vector Table
and the Interrupt Controller
The whole interrupt handling strategy hinges on the proper initialization of the ARM vector table and the interrupt controller. The code accompanying this article performs this initialization in the function BSP_init() located in the file bsp.c.
|Listing 4. Initialization of the vector table and the interrupt controller.|
The highlights of the BSP initialization in Listing 4 above are as follows:
(1) The function BSP_init() is called from main() to initialize the board. BSP_init() is called with IRQ and FIQ interrupts disabled.
(2-8) The secondary jump table starting at address 0x20 is initialized with the addresses of the low-level exception handlers in assembly (Part 2 of this article series describes the ARM vector table and the secondary jump table). All these low-level handlers are defined in the file arm_exc.s provided in the code accompanying this article. I will briefly discuss the ARM exceptions later in this article.
(7-8) In particular, the jump table entry for the IRQ at address (0x20+0x18==0x38) is initialized with the address of the low-level handler ARM_irq() and the following entry at 0x3C is initialized with the address of ARM_fiq(). Both ARM_irq() and ARM_fiq() are discussed in detail in Part 8 of this article series.
(9) The AIC_SVR (Source Vector Register) for the system time tick (PIT) is initialized with the address of the tick ISR (See also Listing 3).
(10) The AIC_SPU (Spurious Interrupt Vector Register) is initialized with the address of the spurious ISR (See also Listing 3).
(11) The system time tick IRQ priority is set in the AIC.
(12) After the vector table and the AIC have been configured, the interrupts must be enabled at the ARM core level (See Part 7 of this article).
Other ARM Exception Handlers
The BSP_init() function in Listing 4 above initializes the secondary jump table with the addresses of ARM exception handlers, such as ARM_und (Undefined Instruction), ARM_swi (Software Interrupt), ARM_pAbort (Prefetch Abort), and ARM_dAbort (Data Abort).
These low-level exception handlers are defined in the file arm_exc.c included in the code accompanying this article. All these handlers implement a rudimentary exception handling policy that might be adequate for simple bare-metal ARM projects.
|Listing 5 Rudimentary exception handling policy|
As shown in Listing 5 above, every low-level exception handler (such as ARM_undef or ARM_dAbort) loads r0 with the address of the string explaining the exception and then branches to the common handler ARM_except.
The common handler loads r1 with the return address from the exception, switches to the SYSTEM mode and calls C-function BSP_abort(). The board-specific function BSP_abort() should try to log the exception (the information about the exception is provided in r0 and r1, which are the arguments of this function call), put the system in a fail-safe state, and possibly reset the system.
This function should never return because there is nothing to return to in a bare-metal system. During development, BSP_abort() is a good place to set a permanent breakpoint.
Coming Up Next
In the next and final part of this article I'll describe the example application that accompanies this article series and provide strategies for testing of the various preemption scenarios of interrupt handling.
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
To read Part 6, go to General description of interrupt handling
To read Part 7, go to Interrupt Locking and Unlocking
To read Part 8, go to Low-level Interrupt Wrapper Functions.
 Atmel Datasheet "AT91 ARM/Thumb-based Microcontrollers, AT91SAM7S64 " available online..
 ARM Limited, "ARM v7-M Architecture Application Level Reference Manual", available on line..
 NXP Application Note AN10414 "Handling of spurious interrupts in the LPC2000", available online.