Cutting Through the Confusion with ARM Cortex-M Interrupt Priorities

share on: 
ARM Cortex-M interrupt priory registers with 3 bits of priority (A), and 4 bits of priority (B)
Table of Contents

The insanely popular ARM Cortex-M processor offers very versatile interrupt priority management, but unfortunately, the multiple priority numbering conventions used in managing the interrupt priorities are often counter-intuitive, inconsistent, and confusing, which can lead to bugs. In this post I attempt to explain the subject and cut through the confusion.

The Inverse Relationship Between Priority Numbers and Urgency of the Interrupts

The most important fact to know is that ARM Cortex-M uses the “reversed” priority numbering scheme for interrupts, where priority zero corresponds to the highest urgency interrupt and higher numerical values of priority correspond to lower urgency. This numbering scheme poses a constant threat of confusion, because any use of the terms “higher priority” or “lower priority” immediately requires clarification, whether they represent the numerical value of priority, or perhaps, the urgency of an interrupt.

NOTE: To avoid this confusion, in the rest of this post, the term “priority” means the numerical value of interrupt priority in the ARM Cortex-M convention. The term “urgency” means the capability of an interrupt to preempt other interrupts. A higher-urgency interrupt (lower priority number) can preempt a lower-urgency interrupt (higher priority number).

 Interrupt Priority Configuration Registers in the NVIC

The number of priority levels in the ARM Cortex-M core is configurable, meaning that various silicon vendors can implement different number of priority bits in their chips. However, there is a minimum number of interrupt priority bits that need to be implemented, which is 2 bits in ARM Cortex-M0/M0+ and 3 bits in ARM Cortex-M3/M4.

But here again, the most confusing fact is that the priority bits are implemented in the most-significant bits of the priority configuration registers in the NVIC (Nested Vectored Interrupt Controller). The following figure illustrates the bit assignment in a priority configuration register for 3-bit implementation (part A), such as TI Tiva MCUs, and 4-bit implementation (part B), such as the NXP LPC17xx ARM Cortex-M3 MCUs.

 Interrupt priory registers with 3 bits of priority (A), and 4 bits of priority (B)
Interrupt priory registers with 3 bits of priority (A), and 4 bits of priority (B)


The relevance of the bit representation in the NVIC priority register is that this creates another priority numbering scheme, in which the numerical value of the priority is shifted to the left by the number of unimplemented priority bits. If you ever write directly to the priority registers in the NVIC, you must remember to use this convention.

NOTE: The interrupt priorities don’t need to be uniquely assigned, so it is perfectly legal to assign the same interrupt priority to many interrupts in the system. That means that your application can service many more interrupts than the number of interrupt priority levels.

NOTE: Out of reset, all interrupts and exceptions with configurable priority have the same default priority of zero. This priority number represents the highest-possible interrupt urgency.

Interrupt Priority Numbering in the CMSIS

The Cortex Microcontroller Software Interface Standard (CMSIS) provided by ARM Ltd. is the recommended way of programming Cortex-M microcontrollers in a portable way. The CMSIS standard provides the function NVIC_SetPriority(IRQn, priority) for setting the interrupts priorities.

However, it is very important to note that the ‘priority‘ argument of this function must not be shifted by the number of unimplemented bits, because the function performs the shifting by (8 – __NVIC_PRIO_BITS) internally, before writing the value to the appropriate priority configuration register in the NVIC. The number of implemented priority bits __NVIC_PRIO_BITS is defined in CMSIS for each ARM Cortex-M device.

For example, calling NVIC_SetPriority(7, 6) will set the priority configuration register corresponding to IRQ#7 to 1100,0000 binary on ARM Cortex-M with 3-bits of interrupt priority and it will set the same register to 0110,0000 binary on ARM Cortex-M with 4-bits of priority.

NOTE: The confusion about the priority numbering scheme used in the NVIC_SetPriority() is further promulgated by various code examples on the Internet and even in reputable books. For example the book “The Definitive Guide to ARM Cortex-M3, Second Edition”, ISBN 979-0-12-382091-4, Section 8.3 on page 138 includes a call NVIC_SetPriority(7, 0xC0) with the intent to set priority of IR#7 to 6. This call is incorrect and at least in CMSIS version 3.x will set the priority of IR#7 to zero.

Preempt Priority and Subpriority

The interrupt priority registers for each interrupt is further divided into two parts. The upper part (most-significant bits) is the preempt priority, and the lower part (least-significant bits) is the subpriority. The number of bits in each part of the priority registers is configurable via the Application Interrupt and Reset Control Register (AIRC, at address 0xE000ED0C).

The preempt priority level defines whether an interrupt can be serviced when the processor is already running another interrupt handler. In other words, preempt priority determines if one interrupt can preempt another.

The subpriority level value is used only when two exceptions with the same preempt priority level are pending (because interrupts are disabled, for example). When the interrupts are re-enabled, the exception with the lower subpriority (higher urgency) will be handled first.

In most applications, I would highly recommended to assign all the interrupt priority bits to the preempt priority group, leaving no priority bits as subpriority bits, which is the default setting out of reset. Any other configuration complicates the otherwise direct relationship between the interrupt priority number and interrupt urgency.

NOTE: Some third-party code libraries (e.g., the STM32 driver library) change the priority grouping configuration to non-standard. Therefore, it is highly recommended to explicitly re-set the priority grouping to the default by calling the CMSIS function NVIC_SetPriorityGrouping(0U) after initializing such external libraries.

Disabling Interrupts with PRIMASK and BASEPRI Registers

Often in real-time embedded programming it is necessary to perform certain operations atomically to prevent data corruption.  The simplest way to achieve the atomicity is to briefly disable and re-enabe interrupts.

The ARM Cortex-M offers two methods of disabling and re-enabling interrupts. The simplest method is to set and clear the interrupt bit in the PRIMASK register. Specifically, disabling interrupts can be achieved with the “CPSID i” instruction and enabling interrupts with the “CPSIE i” instruction. This method is simple and fast, but it disables all interrupt levels indiscriminately. This is the only method available in the ARMv6-M architecture (Cortex-M0/M0+).

However, the more advanced ARMv7-M (Cortex-M3/M4/M4F) provides additionally the BASEPRI special register, which allows you to disable interrupts more selectively. Specifically, you can disable interrupts only with urgency lower than a certain level and leave the higher-urgency interrupts not disabled at all. (This feature is sometimes called “zero interrupt latency”.)

The CMSIS provides the function __set_BASEPRI(priority) for changing the value of the BASEPRI register. The function uses the hardware convention for the ‘priority’ argument, which means that the priority must be shifted left by the number of unimplemented bits (8 – __NVIC_PRIO_BITS).

NOTE: The priority numbering convention used in __set_BASEPRI(priority) is thus different than in the NVIC_SetPriority(priority) function, which expects the “priority” argument not shifted.

For example, if you want to selectively block interrupts with priority number higher or equal to 6, you could use the following code:

// code before critical section
__set_BASEPRI(6 << (8 - __NVIC_PRIO_BITS));
// critical section
__set_BASEPRI(0U); // remove the BASEPRI masking
// code after critical section



13 Responses

  1. Useful information, but beware of CMSIS, which has “volatility” bugs, even in the latest version, 3.20, which I just downloaded.

    For example, the __set_BASEPRI() function contains the following declaration (for the Keil/ARM compiler):

    register uint32_t __regBasePri __ASM(“basepri”);

    It should be:

    volatile register uint32_t __regBasePri __ASM(“basepri”);

    If you don’t believe me, try setting the optimisation (for this compiler) to “Level 3, Optimize for time”. Your valiant attempt to set BASEPRI will then silently disappear from your code! My modification, above, fixed the problem for me, when I tried it, but I don’t want to be using a library I have to patch.

    This bug is repeated in numerous similar functions.

    I have several personally subjective reasons to dislike CMSIS, but this fundamental error is the objective one which finally persuaded me not to use it. I do tend to make use of its somewhat standardised vector and startup code, though.

    1. Hi Peter,
      I finally came around to testing your finding about the CMSIS __set_BASEPRI() function, but it seems to be working just fine for me. I have compiled the code with optimization “Level 3 (-O3), Optimize for time”. I inspected the disassembly, and I can see the expected code:

      141: __set_BASEPRI(QF_BASEPRI);
      241: __regBasePri = (basePri & 0xff);
      0x00000480 203F MOVS r0,#0x3F
      0x00000482 F3808811 MSR BASEPRI,r0

      I also verified that the BASEPRI register gets set to the expected value (0x20 for the TivaC Cortex-M4F with 3 priority bits).

      I checked again the definition of the CMSIS __set_BASEPRI() function, which is defined in the “core_cmFunc.h” header file without the volatile keyword as follows:

      __STATIC_INLINE void __set_BASEPRI(uint32_t basePri)
      register uint32_t __regBasePri __ASM("basepri");
      __regBasePri = (basePri & 0xff);

      Perhaps the problem was there in the earlier versions of the ARM/Keil toolset. My tests were performed with MDK-Lite, C Compiler [Evaluation].


      1. Interesting!

        Perhaps you are right about the possible difference in compiler behaviour. On the other hand, these CMSIS functions are declared inline (via a macro), so the actual compiler optimisation, at any point, could depend crucially on the “calling” code as well as on the code of the function.

        Either way, I still won’t be using these CMSIS functions, containing non-volatile named registers, because each of them contains an error of principle. On a practical basis, I can’t really use CMSIS anyway, for library code, because the files I have to include (and their names) depend on the actual chip my end-user happens to choose.


        1. Is volatility a problem for opcode related assignments.. I mean basepri is set by MSR. As it is not simple ‘to memory assignment’, compiler should not drop on optimization.. Am I wrong?

  2. Assigning more subpriority bits can be very usefull when writing event driven code without framework like QP.

    You can prioritize events using subpriorities, yet ensuring no preemption, so all interrupts with equal preemption priority can be considered to be run until completion.

    Subpriorities are also usefull when interrupts are not so super tiny few cycles ones and more than one is usually pending – you can prioritize without losing cycles in context saving.

  3. ARM has always been a confusing mess of architecture and scattered difficult to understand documentation. It is why I chose an AVR32 processor on my last project and never regretted it. Everything just worked right off with very little confusion and excellent performance. All the documentation accurate and easy to understand and in one place.

    1. i might be doing more embedded work soon so it is good to see you posting again… has been a long quiet year for the gurus… keep it up

    2. Try TI’s MSP series. Even the register flag naming is not consistent.. They bought some other companies and added chips into their MCU’s, which made the documentation even worse.. 🙂

      I encapsulated all low level stuff into a portable library. So, I underwent the headache once, have been running the code on any platform without virtually re-reading low level stuff..

  4. Hi Miro,

    Thanks for your useful topic, it help me review my knowledge base in regard to ARM architecture.
    However, need to update about STM32 driver library: with the current release (STM32 Cube), the default configuration assigned all the interrupt priority bits to preempt priority group, leaving no priority bits as subpriority bits.

  5. Hello,

    I am familiar with using interrupts on TI’s Stellaris micro-computer. I have recently begun using the NXP LPC4333. I am wondering what the disadvantage of pending an interrupt using the QEI vector number. I did something similar with the Stellaris and it worked quiet well.

    Also….am I better advised to use the software interrupt facility (SVCALL)?

    Any comments.

  6. Hi, Miro,

    Thanks for the useful article. I have one question though. After you call __set_BASEPRI, if the blocked interrupt was triggered, will its ISR be called after you remove the BASEPRI masking by calling__set_BASEPRI(0)?

Leave a Reply