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 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" [1], Philips Application Note
AN10381 "Nesting of Interrupts in the LPC2000" [2], and Atmel Application Note
"Interrupt Management: Auto-vectoring and Prioritization" [3].
Problem Description
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 [1].
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 [1].
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.
FIQ Handling
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.
No Auto-Vectoring
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 [3]. 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):
ORG 0x18
LDR pc,[pc,#-0xF20]
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
To download
the C and C++ source code associated with this article series, go to Embedded.com's Downloadable Code page,
or go to Blinky for C and Blinky for C++ to download the
Zip files.
Miro Samek, Ph.D., is president of Quantum Leaps, LLC. He can be
contacted at miro@quantum-leaps.com.
References
[1] ARM Technical Support Note "Writing Interrupt
Handlers" available online.
[2] Philips Application Note
AN10381 "Nesting
of Interrupts in the LPC2000" available online as a PDF file.
[3] Atmel Application Note "Interrupt
Management: Auto-vectoring and Prioritization" available online as
a PDF file.