The ARM Cortex-M microcontroller is insanely popular, and it features a flexible and powerful nested vectored interrupt controller (NVIC). But for many, including myself, the Cortex-M interrupt system can be counter-intuitive, complex, inconsistent, and confusing, leading to many bugs and lots of frustration.
Understanding the NVIC (Nested Vectored Interrupt Controller) and the ARM Cortex-M interrupt system is essential for every embedded application, but even for using a realtime operating system: if you mess up with interrupts, very bad things will happen….
FreeRTOS is probably the most popular and most used operating system for microcontrollers. It supports many different architectures, including the ARM Cortex-M architectures.
I’m covering the topic of FreeRTOS and interrupts in my university lecture material. But I have seen so many wrong uses of interrupts with the RTOS that I think it deserves a dedicated article. The amazing thing I see many times is even if the interrupts are configured in a clearly wrong way, surprisingly, the application ‘seems’ to work, at least most of the time. Well, I think everyone agrees that ‘most of the time’ is not good enough. Because problems with interrupts are typically hard to track down, they are not easy to fix.
In this article, I’m discussing ARM Cortex-M0/M0+ (ARMv6-M), M3(ARMv7-M) and M4/M7 (ARMv7E-M). In Part 1 (this article) I give an overview of the ARM Cortex-M interrupt system. In Part 2 (which will follow in the not-so-far-future), I explain how it is used by FreeRTOS and how it affects the application.
The ARM Cortex-M is using an NVIC (Nested Vectored Interrupt Controller). The ‘vectored’ means that it uses a vector table, shown for M0/M0+ and M4/M4 below:
The table is 'vectored' because the 32bit entries in it (e.g. the Hard fault vector at address 0x000C’0000) points to the corresponding interrupt service routine: For example, the entry at address 0x08 ‘vectors’ to the NMI interrupt handler or function.
The exception numbers 1-15 are defined by ARM, that is, they are part of the core. The exceptions above 15 are ‘vendor specific,’ which means that they are implemented by vendors like NXP, TI, STM, and many others.
Or in other words: The negative IRQ numbers (from -1 (SysTick) to -14 (NMI) plus reset) are defined by the ARM core, everything else ‘above’ with IRQ number >=0 are vendor-specific and typically for devices like UART/I²C/USB/etc..
Note: In the above image, the numbering or Exception Numbers and IRQ are easily mixed up and cause confusion.
The ARM Cortex-M core is using a rather confusing interrupt priority numbering: Numerically low values are used to specify logically high interrupt priorities. Or in other words: The lower the number, the higher the urgency.
I try to use the word ‘urgency’ to indicate how ‘important’ an interrupt is, not to confuse with the (ARM hardware) interrupt priority (level). In a nested interrupt system such as on ARM Cortex-M, a ‘more urgent’ interrupt can interrupt a ‘less urgent’ interrupt.
This means that interrupt priority 0 is the ‘most urgent one.’
At reset, all interrupt priorities have a priority of zero assigned. I can assign a priority to each of them. Except that Reset, NMI, and HardFault have a fixed (negative) priority and cannot be disabled. The following table shows all the exceptions/interrupts, and for which ARM Cortex-M core they exist:
The priority of the exception/interrupt is assigned with an 8-bit priority register, and the number of bits implemented is up to the vendor implementation. ARM specifies a minimum of 2 bits for the M0/M0+ and 3 bits for M3/M4/M7.
If using CMSIS-compliant libraries, the number of implemented bits can be checked with:
The example below is for an NXP Cortex-M7 (see "First steps: ARM Cortex-M7 and FreeRTOS on NXP TWR-KV58F220M"), which has 4 bits implemented
#define __NVIC_PRIO_BITS 4 /**< Number of priority bits implemented in the NVIC */
Shifted Priority Bits
The implemented priority bits are ‘left-aligned:’ This keeps priority values compatible between different implementations:
For three implemented bits, it means I can have 2^3 (8) priority levels, with the following (shifted) values: 0x00, 0x20, 0x40, 0x60, 0x80, 0xA0, 0xC0, 0xE0.
There is much confusion about ‘shifted’ and ‘not shifted’ priority values. Carefully check the API of the CMSIS API function if they expect shifted or not shifted values! Using the CMSIS API is the preferred way to deal with the ARM core and its core registers.
NVIC Interrupt Configuration
The NVIC offers several registers to configure the interrupts. On the M0/M0+ there are the following:
- NVIC_ISER (Interrupt Set Enable Register): enable interrupt bit, one bit for each interrupt.
- NVIC_ICER (Interrupt Clear Enable Register): disable interrupt bit, one bit for each interrupt.
- NVIC_ISPR (Interrupt Set Pending Register): mark interrupt as pending bit, one bit for each interrupt.
- NVIC_ICPR (Interrupt Clear Pending Register): clear pending flag bit, one bit for each interrupt.
- NVIC_IPRx (Interrupt Priority Register): interrupt priority (8-bit for each interrupt, 4 interrupts in a 32bit register).
The above registers are 32-bit registers, with one bit for each interrupt. For example, the NXP KL25Z has 32 vendor-specific interrupts (exceptions 0x10-0x47), so 32bits for each bit of the above registers are enough. For the 32 interrupt,s priorities 32*8bit == 8 32bit priority registers (NVIC_PRI0…PRI7) are used.
The following screenshot shows the individual bits in NVIC_ISER:
The Cortex-M3/4/7 has one register more in addition to the one above:
- NVIC_IABR (Interrupt Active Bit Register): set if an interrupt is running, one bit for each interrupt
Here again, there is a single bit for each interrupt. For example, the NXP K22FX512 (ARM Cortex-M4F) has 82 vendor-specific interrupts (exceptions 0x10-0x61), so it needs four 32-bit registers to hold all the bits and 106 32bit registers for the 8bit priorities.
Assigning Interrupt Priority
To set the interrupt priority, the following CMSIS function is used:
void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority);
IRQn is the exception number (0 for the first vendor-specific exception, -1 for SysTick, -2 for PendSV, etc.). ‘priority’ is the not-shifted (!!!!) interrupt priority, e.g. 0-7 for a system with 3 bits.
This mix-up with shifted and non-shifted values is very confusing and a common source of incorrect code — even books have it wrong
For example ...
NVIC_SetPriority(SysTick_IRQn, (1UL<<__NVIC_PRIO_BITS)-1UL); /* set Priority for Systick Interrupt to lowest interrupt */
... sets the SysTick to the lowest interrupt level given the available levels (SysTick_IRQn is a macro having a value of -1).
On the M3/M4/M7, it is possible to have sub-priorities for the interrupts, and the number of subpriority bits is configured by the PRIGROUP register. The PRIGROUP can be changed at runtime:
In the above example, the PRIGROUP is set to 0 (no sub-priorities). The value of PRIGROUP sets the bit position of the sub-priority bits: For example, if PRIGROUP has a value of 5 (bit position 5) and the number of priority bits are three, then there are two main/preemption priorities and one sub-priority:
With sub-priority, there are now two different priorities for an interrupt: <Preempt Prio>.<Sub Prio> to have a notation for it.:
- The Preempt Priority defines if an interrupt can nest/interrupt an already running interrupt. Remember that a lower preemption priority number means higher urgency. For example, an interrupt with 2.1 can nest/interrupt a running interrupt 3.0.
- The Subpriority is used when multiple interrupts with the same Preemption Priority are pending, then the one with the lower sub-priority (higher urgency) will be executed first. For example, if 3.1 and 3.0 are pending, then 3.0 will be executed first. It means as well that the interrupt 3.0 will not be able to interrupt/nest another 3.1 interrupt.
To change the number of sub-priority bits ...
void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
... is used. While sub-priorities provide great flexibility, many systems do not use them because they add more complexity. Even worse, some libraries are setting non-standard sub-priority levels. In that case ...
... disables sub-priorities.
Interrupts and Devices
Multiple interrupt-enabled devices (e.g. UART, USB, etc) can have the same priority assigned, so they don’t need to be uniquely assigned on Cortex-M.
Out of reset, interrupts are disabled and the interrupt priorities for all are set to 0.
To enable interrupts for a given device (e.g. I²C interrupt), the following steps are needed:
- Optional: Set the priority level of the required interrupt in the NVIC.
- Enable the interrupt inside the device (usually a device-specific bit, e.g. bit in the I²C peripheral register).
- Enable the interrupt in the NVIC.
For an example of the I²C interrupt:
#define I2C1_IRQn 24 /* device specific interrupt for I2C */ #define I2C1_BASE (0x40067000u) /* address of peripheral */ #define I2C1 ((I2C_Type *)I2C1_BASE) /** Peripheral I2C0 base pointer */ NVIC_SetPriority(I2C1_IRQn, 1); /* set I2C interrupt level (note: 1 is *not* shifted! */ I2C1->C1 |= I2C_C1_IICIE_MASK; /* enable device specific interrupt flag */ NVIC_EnableIRQ(I2C1_IRQn); /* Enable NVIC interrupt */
To turn off the device interrupts, simply disable it with an NVIC call:
NVIC_DisableIRQ(I2C1_IRQn); /* Enable NVIC interrupt */
The fact that there are both the NVIC- and device-specific interrupt enable/disable bits might be confusing. My recommendation is to first disable the device's internal interrupt bit before doing any device register configuration. This ensures that the device is not creating any interrupts to the NVIC. To turn on/off the interrupts, use the NVIC interrupt enable/disable bits (NVIC_ISER and NVIC_ICER).
The NVIC not only allows to set interrupt priorities, it allows you to enable/disable each interrupt one by one. But for critical sections or atomic accesses, it is necessary to turn off all interrupts.
All the cores discussed here have the ‘I’ (interrupt) bit in the PRIMASK (Primary Mask) register:
Setting that bit masks (disables) the interrupts. There is a simple assembly instruction to do this:
__asm volatile("cpsid i"); /* disable interrupts */
The other way around, clearing the bit enables (globally) all interrupts:
__asm volatile("cpsie i"); /* enable interrupts */
If using CMSIS libraries, then the following functions are available:
void __enable_irq(void); void __disable_irq(void);
Disabling all interrupts in a system increases interrupt latency time, so this should be as short as possible.
The M3/M4/M7 cores (not the M0/M0+!) have another great feature: the BASEPRI (Base Priority Mask) register.
The BASEPRI register is a mask register and masks all interrupt priorities that are ‘numerically equal or lower than the BASEPRI value.’
- BASEPRI set to 3: disables interrupts with priority 3, 2 and 1.
- BASEPRI set to 5: disables interrupts with priority 5, 4, 3, 2 and 1.
Because BASEPRI is a mask register, setting it to 0 means interrupts are not masked and therefore enabled. It means that it cannot mask/disable interrupts with priority 0! Use the PRIMASK to disable interrupts with priority zero.
CMSIS offers the following function to set the BASEPRI register:
Using the BASEPRI it is possible to mask the interrupts up to a certain level. This is critical for a good interrupt partitioning of the system, and FreeRTOS takes advantage of the BASEPRI setting. There will be more about this in Part 2 of this article.
The ARM NVIC and interrupt system are complex, and in many areas with the CMSIS API, they are not logical or counter-intuitive, and they use an inverted priority vs. urgency scheme. The shifted vs. non-shifted priority values can cause many subtle bugs and issues that I have faced in many projects. Between the ARMv6-M, ARMv7-M, and ARMv7E-M, the interrupt system is similar and somewhat compatible, but still different. Vendor libraries might not be consistent dealing with the interrupts or setting some grouping, so carefully check how they are implemented.
Understanding the interrupt system and how to use it is an essential part of embedded systems. The interrupt system and controller have an impact on how drivers and middleware are using the interrupts.
The RTOS is a case where understanding and mastering the interrupts is critical to ensure proper operation and minimize interrupt latency. In Part 2 of this article, I will describe how the ARM Cortex-M interrupts are used by FreeRTOS, and what it means for the application.
So far, I hope this part is already useful for you.