ARM Cortex-M, Interrupts and FreeRTOS: Part 1

The ARM Cortex-M microcontroller are 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 :-(.

NXP KV58F ARM Cortex-M7

ARM Cortex-M7: NXP KV58

Understanding the NVIC and the ARM Cortex-M interrupt system is essential for every embedded application, but even for if using an realtime operating system: if you mess up with interrupts, very bad things will happen….


FreeRTOS is probably the most popular operating system for microcontroller. 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. I have seen so many cases of incorrect interrupt usage within the RTOS that I think it deserves a dedicated article. Amazingly, I see many times over that 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 on 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.

Interrupt Vectors

The ARM Cortex-M is using a NVIC (Nested Vectored Interrupt Controller). The ‘vectored’ means that it uses a vector table, shown for M0/M0+ and M4/M4 below:

Cortex-M Vector Table

Cortex-M Vector Table (Source of images: ARM)

The table is ‘vectored’, because the 32bit entries in it (e.g. the Hard fault vector at address 0x000C’0000) point 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 to 15 are defined by ARM, that is they are part of the core. The exception above 15 are ‘vendor specific’, this 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 above image the numbering or Exception Numbers and IRQ numbers: they easily get mixed up and causes confusion.

Interrupts Priorities

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:

Exception and Priorities

Exception and Priorities

Priority Bits

The priority of the exception/interrupt is assigned with a 8bit 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 a 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:

3 Interrupt Bits Implemented

3 Interrupt Bits Implemented

For three implemented bits, it means I can have 2^3 (8) priority levels, with the following (shifted) values:

  • Hexadecimal: 0x00, 0x20, 0x40, 0x60, 0x80, 0xA0, 0xC0, 0xE0
  • Decimal: 0, 32, 64, 96, 128, 160, 192, 224

💡 There is much confusion about ‘shifted’ and ‘not shifted’ priority values. Carefully check the API of the CMSIS API function if they expects shifted or not shifted values! Using the CMSIS API is the preferred way to deal with the ARM core and its core registers.

If the device has implemented only 4 priority bits (e.g. the Kinetis K series of NXP), then we have 2^4 (16) priority levels, with the following shifted values:

  • Hexadecimal: 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90 0xA0, 0xB0 0xC0, 0xD0, 0xE0, 0xF0
  • Decimal: 0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240

If the device has implemented only 2 priority bits (e.g. the Kinetis L series of NXP), then we have 2^2 (4) priority levels, with the following shifted values:

  • Hexadecimal: 0x00, 0x40, 0x80, 0xC0
  • Decimal: 0, 64, 128, 192

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 (8bit for each interrupt, 4 interrupts in a 32bit register)
NVIC Interrupt Registers on Cortex-M0+

NVIC Interrupt Registers on Cortex-M0+

The above registers are 32bit registers, with one bit for each interrupt. For example the NXP KL25Z has 32 vendor specific interrupts (IRQ 0-31), so 32bits for each bit of the above registers are enough. For the 32 interrupts priorities 32*8bit == 8 32bit priority registers (NVIC_PRI0…PRI7) are used.

Below screenshot shows the individual bits in NVIC_ISER:

NVIC Interrupt Bits

NVIC Interrupt Bits

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 4 32bit 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 much confusing and a common source of wrong 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 value -1).

Notice that NVIC_SetPriority accepts IRQ numbers from -15….0….n where values greater-equal zero are device/vendor specific interrupts. And that the priority is the non-shifted (!!!!) value:

  \brief   Set Interrupt Priority
  \details Sets the priority of an interrupt.
  \note    The priority cannot be set for every core interrupt.
  \param [in]      IRQn  Interrupt number.
  \param [in]  priority  Priority to set.
static void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
  if ((int32_t)(IRQn) < 0)
    SCB->SHP[(((uint32_t)(int32_t)IRQn) & 0xFUL)-4UL] = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
    NVIC->IP[((uint32_t)(int32_t)IRQn)]               = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);


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 zero (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 number of priority bits are 3, then there are 2 main/preemption priorities and one sub priority:

System with 3 Priority bits, 2 are preemption priority and one sub priority bit


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, the

void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)

is used. While sub-priorities provide great flexibility, many systems do not use it because it adds 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 zero.

To enable interrupts for a given device (e.g. I²C interrupt), the following steps are needed:

  1. Optional: set the priority level of the required interrupt in the NVIC
  2. Enable the interrupt inside the device (usually a device specific bit, e.g. bit in the I²C peripheral register)
  3. Enable the interrupt in the NVIC

For an example of the I²C interrupt, this could look like this:

#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 of the device interrupts, simply disable it with a 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 disable first the device 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).

Masking Interrupts

The NVIC not only allows to set interrupt priorities, it allows 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:

PRIMASK Register

PRIMASK Register

Setting that bit masks (disables) the interrupts. There is a simple assembly instruction doing 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:

BASEPRI Register

BASEPRI Register

The BASEPRI register is a mask register and masks all interrupt priorities levels which are ‘numerically equal or higher (lower urgency!) than the BASEPRI value’.

💡 As for the interrupt priorities, this register uses the shifted bits!

From the ARM Infocenter:



💡 Notice the usage of ‘priority value’ and ‘priority level’ in above description!

So it means: if non-zero, it allows interrupts with priority numerical values smaller than BASEPRI value, and blocks interrupts with numerical values equal or higher than BASEPRI value.

Examples (assuming  (hypothetical) 8 priority bits implemented):

  • BASEPRI set to a value of 3: disables interrupts with values 3, 4, 5, 6, …, 255. Allows any interrupts with numerical value of 0, 1 and 2.

Higher numerical values mean lower priority (less urgency!). So setting BASEPRI to 3 will disable all interrupts with a higher prio value (less urgent) than 3, so that 0, 1, 2 will stay enabled, but 3,4,5 6, … are disabled.

💡 Because BASEPRI is a mask register: setting it to zero means interrupts are not masked and therefore enabled. It means that it cannot mask/disable interrupts with priority zero! Use the PRIMASK to disable interrupts with priority zero.

Here is an example for an implementation with 2 priority bits with no subpriorities:

  • Has 2^2 (4) priority values: 0x00, 0x40, 0x80 and 0xC0
  • Setting BASEPRI to 0x80 will mask/disable 0x80 and 0xC0. 0x00 and 0x40 are still enabled.

CMSIS offers the following function to set the BASEPRI register:

void __set_BASEPRI_MAX(uint32_t value);

Let’s have a look at the CMSIS implementation for GNU:

  \brief   Set Base Priority with condition
  \details Assigns the given value to the Base Priority register only if BASEPRI masking is disabled,
           or the new value increases the BASEPRI priority level.
  \param [in]    basePri  Base Priority value to set
__attribute__( ( always_inline ) ) __STATIC_INLINE void __set_BASEPRI_MAX(uint32_t value)
  __ASM volatile ("MSR basepri_max, %0" : : "r" (value) : "memory");

So it does *not* shift the priority inside the function, so I have to do that outside in the function call like this:


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. More about this in Part 2 of this article.


The ARM NVIC and interrupt system is complex, in many areas with the CMSIS API not logical or counter-intuitive, and uses an inverted priority vs. urgency scheme. The shifted vs. non-shifted priority values can cause subtle bugs and issues I have faced. 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 for embedded systems. The interrupt system and controller has an impact on how drivers and middleware are using the interrupts.

The RTOS is such 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.

Happy Cortexing 🙂



15 thoughts on “ARM Cortex-M, Interrupts and FreeRTOS: Part 1

  1. Please be aware that the value passed to __set_BASEPRI() and __set_BASEPRI_MAX() is not automatically shifted, you need to explicitly use something like __set_BASEPRI(priority << (8 – __NVIC_PRIO_BITS)); similarly, actual values in the BASEPRI register rather look like 0x10, than 0x1, as you depict them.


    Liked by 1 person

      • It seems correct now, but the example with 8 priority bits is not realistic, I did not encountered such a chip. I guess less confusing would be a real world example, with 3-4 priority bits.


  2. Great description! As Eric says, the numbering is quite confusing. So confusing that the author mixed one thing up: 🙂

    The BASEPRI register is a mask register and mask all interrupt priorities which 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

    all interrupts with a higher prio value (less urgent) than 3, so that 0, 1, 2 will stay enabled, but 4,5 6, …
    are disabled.


  3. Did you reverse 3.0 and 3.1 in the second sentence here?



  4. Pingback: ARM Cortex-M, Interrupts and FreeRTOS: Part 2 | MCU on Eclipse

  5. Pingback: ARM Cortex-M Interrupts and FreeRTOS: Part 3 | MCU on Eclipse

  6. Pingback: Tutorial: RFID Tags with the NXP NFC Controller PN7120 and Eclipse | MCU on Eclipse

What do you think?


You are commenting using your account. Log Out / Change )

Twitter picture


Facebook photo


Google+ photo


Connecting to %s