10. Interrupt Priority and Configuration

1. Interrupt Priority

In a system with multiple interrupt sources, it's crucial to prioritize them to ensure that the most critical tasks are serviced first. This is where interrupt priorities come into play. In ARM Cortex-M processors, lower numerical values represent higher priorities.

Nested Interrupts

Let's say you have three interrupt sources with different priorities: Sensor A (Priority 2), Sensor B (Priority 1), and a Timer (Priority 3).

  1. Scenario 1: While the Timer ISR (Priority 3) is running, Sensor A (Priority 2) triggers an interrupt.

    • Outcome: The Timer ISR is paused, and the Sensor A ISR takes over because it has a higher priority (i.e., a lower numerical value).
  2. Scenario 2: While the Timer ISR (Priority 3) is running, Sensor B (Priority 1) and then Sensor A (Priority 2) both trigger interrupts.

    • Outcome: Sensor B's ISR takes over first due to the highest priority. Once it's done, Sensor A's ISR will execute, followed by the resumption of the Timer ISR.
  3. Scenario 3: Sensor A (Priority 2) triggers an interrupt while Sensor B's ISR (Priority 1) is running.

    • Outcome: Sensor B's ISR will complete first because it has a higher priority. After that, Sensor A's ISR will execute.

Interrupts during Reset

  1. Scenario: During a system reset, Sensor A triggers an interrupt.
    • Outcome: The interrupt won't be serviced. Resets have the highest level of priority, and all other system activities, including interrupts, are ignored during the reset.

Preemption and Tail-chaining

  1. Scenario 1: Sensor A and Sensor B both trigger interrupts almost simultaneously, and both have the same priority.

    • Outcome: The first interrupt to be registered will execute first. Once that ISR is completed, the next ISR will run. This is known as "tail-chaining."
  2. Scenario 2: Sensor A (Priority 2) is in the middle of its ISR when Sensor B (Priority 2) triggers an interrupt.

    • Outcome: Since both have the same priority, the current ISR (Sensor A) will complete before Sensor B's ISR starts. This avoids "preemption" but utilizes "tail-chaining."

2. Interrupt Configuration for STM32F4: Overview

In the STM32F4 series of microcontrollers, interrupt configuration involves a series of steps that typically include setting up the peripheral, configuring the NVIC (Nested Vectored Interrupt Controller), and defining the Interrupt Service Routine (ISR). Here's a simplified guide:

Peripheral Configuration

  1. Enable the Peripheral Clock: For any peripheral to work, its clock must be enabled via the relevant RCC (Reset and Clock Control) register.

  2. Set the Mode: Depending on the peripheral, set it to the appropriate mode to generate interrupts (e.g., edge-triggered or level-sensitive for GPIO).

  3. Configure the Control Register: Configure specific register settings unique to the peripheral that dictate when an interrupt is generated (e.g., when a USART receive buffer is full).

NVIC Configuration

  1. Identify the IRQ Number: Each interrupt has a unique IRQ number, which can be found in the STM32F4 reference manual.

  2. Interrupt Priority: Use the NVIC's priority registers to set the priority of the interrupt. This is usually done with the NVIC_SetPriority() function.

  3. Enable the Interrupt: To activate the interrupt, set the appropriate bit in the NVIC's ISER (Interrupt Set-Enable Register), usually with the NVIC_EnableIRQ() function.

ISR Configuration

  1. ISR Name: The name of the function that serves as the ISR should correspond to the name in the vector table for that IRQ.

  2. ISR Function: Inside this function, handle the interrupt by performing tasks like reading and clearing status flags and performing the intended action (e.g., reading data, toggling an LED).

  3. ISR Attributes: In some development environments, you might need to mark your ISR with specific attributes, like __attribute__((interrupt)), for it to properly act as an interrupt handler.


3. Interrupt Priority Register (IPR)

In the context of the STM32F4, you can directly manipulate the Interrupt Priority Registers (IPR) to set the interrupt priorities. These registers are part of the NVIC and allow for fine-grained control over the priority levels of individual interrupts. It is more low-level than using a function like NVIC_SetPriority().

3.1 Calculating the IPR Register and Bit Position

Refer to the ARM Cortex-M4 Generic User Guide, Section 4.2.7 "Interrupt Priority Registers", we will see the following:

ipr

As you can see, each NVIC_IPRn register is divided into four 8-bit fields, each of which corresponds to a different interrupt. To find the specific NVIC_IPRn register and the starting bit position for an IRQx, you can use the following mathematical expressions.

This equation divides the IRQ number by 4 and rounds down to the nearest integer to find , which will give you the specific NVIC_IPRn register.

This equation takes the IRQ number modulo 4 to find the offset within the NVIC_IPRn register, and then multiplies it by 8 to find the starting bit position.

In code, we can use the following to calculate the NVIC_IPRn register and the starting bit position for an IRQx:

// Base addresses for NVIC registers. Refer to the processor's reference manual.
uint32_t *pNVIC_IPRBase = (uint32_t*) 0xE000E400;
uint32_t *pNVIC_ISERBase = (uint32_t*) 0xE000E100;
uint32_t *pNVIC_ISPRBase = (uint32_t*) 0xE000E200;

void configurePriorityForIRQs(uint8_t irq_no, uint8_t priority_value) {
    // Calculate the IPR register for the IRQ number
    uint8_t iprx = irq_no / 4;
    uint32_t *ipr = pNVIC_IPRBase + iprx;

    // Calculate the position in the IPR register
    uint8_t pos = (irq_no % 4) * 8;

    // Clear existing priority and set new one
    *ipr &= ~(0xFF << pos);
    *ipr |= (priority_value << pos);
}

4. Exercise: Interrupt Priority Configuration

#include <stdint.h>
#include <stdio.h>

#define IRQNO_TIMER2  28
#define IRQNO_I2C1    31

// Base addresses for NVIC registers. Refer to the processor's reference manual.
uint32_t *pNVIC_IPRBase = (uint32_t*) 0xE000E400;
uint32_t *pNVIC_ISERBase = (uint32_t*) 0xE000E100;
uint32_t *pNVIC_ISPRBase = (uint32_t*) 0xE000E200;

void configurePriorityForIRQs(uint8_t irq_no, uint8_t priority_value) {
    // Calculate the IPR register for the IRQ number
    uint8_t iprx = irq_no / 4;
    uint32_t *ipr = pNVIC_IPRBase + iprx;

    // Calculate the position in the IPR register
    uint8_t pos = (irq_no % 4) * 8;

    // Clear existing priority and set new one
    *ipr &= ~(0xFF << pos);
    *ipr |= (priority_value << pos);
}

int main(void) {
    // Configure priority levels for TIMER2 and I2C1
    configurePriorityForIRQs(IRQNO_TIMER2, 0x80);
    configurePriorityForIRQs(IRQNO_I2C1, 0x70);  // Change this priority to see if pre-empting occurs

    // Set the interrupt pending bit for TIMER2
    *pNVIC_ISPRBase |= (1 << IRQNO_TIMER2);

    // Enable TIMER2 and I2C1 interrupts
    *pNVIC_ISERBase |= (1 << IRQNO_I2C1);
    *pNVIC_ISERBase |= (1 << IRQNO_TIMER2);
}

// ISR for TIMER2
void TIM2_IRQHandler(void) {
    printf("[TIM2_IRQHandler]\n");
    // Manually set I2C1 interrupt request to pending
    *pNVIC_ISPRBase |= (1 << IRQNO_I2C1);
    while(1);
}

// ISR for I2C1
void I2C1_EV_IRQHandler(void) {
    printf("[I2C1_EV_IRQHandler]\n");
}

We can see that the NVIC_IPR7 register is used to set the priority levels for IRQ28 (TIMER2) and IRQ31 (I2C1):

sfr_window


5. Pre-Empt and Sub-Priority

Understanding pre-emption levels and sub-priorities is crucial for managing multiple interrupts efficiently in a real-time system, such as those built on the STM32F4 platform. These two aspects work in tandem to decide the "interruptibility" and execution order of pending interrupts.

5.1 Pre-emption Priority

The pre-emption priority level determines the ability of an interrupt to pre-empt another. In simpler terms, if an interrupt with a lower pre-emption level is currently being serviced and another interrupt with a higher pre-emption level (meaning a lower numerical value) arrives, the CPU will pause the current interrupt to service the new, higher-priority one. After servicing the higher-priority interrupt, the CPU will resume the lower-priority one.

For example, if you have a Timer interrupt at pre-emption level 2 and a UART interrupt at pre-emption level 1, the UART interrupt can pre-empt the Timer interrupt but not vice versa.

5.2 Sub-Priority

Sub-priorities act as tie-breakers within the same pre-emption level. They help the CPU decide which interrupt to service first when multiple interrupts with the same pre-emption level become pending at the same time. Note that sub-priorities do not enable an interrupt to pre-empt another; they merely set the order of execution within the same pre-emption level.

For instance, if two GPIO interrupts are set at the same pre-emption level but with different sub-priorities (say, 3 and 5), the one with the lower sub-priority value (3) will be serviced first if both become pending simultaneously.

5.3 Use Cases

  1. Real-time Requirements: In systems with stringent real-time constraints, you may need some interrupts to have the ability to pre-empt others. In such cases, set these interrupts at a lower pre-emption level.

  2. Non-Disruptive Execution: If you have a set of interrupts that should be executed without pre-empting each other, you can set them at the same pre-emption level but use sub-priorities to control the order of execution.

  3. Nested Execution: In complex applications, you might want lower-priority interrupts to be interrupted by higher-priority ones for nested interrupt handling.

5.4 Summary

In short, pre-emption levels control the ability of one interrupt to interrupt another, while sub-priorities control the order of execution within the same pre-emption level. By tuning these two aspects appropriately, you can manage the behavior of multiple interrupts to meet the specific needs of your application. You use pre-emption levels to decide "interruptibility" and sub-priorities to decide the order within those who can't interrupt each other.

5.5 Priority Grouping

Refer to the STM32 Cortex®-M4 MCUs and MPUs programming manual (PM0214), Section 4.4.5 "Application interrupt and reset control register (AIRCR)", we will see the following:

SCB_AIRCR

The PRIGROUP field in the SCB_AIRCR register controls the number of bits used for pre-emption priority and sub-priority. The following table shows the possible values for PRIGROUP and the number of bits used for pre-emption and sub-priority:

priority_grouping

As you can see, STM32 only uses the highest 4 bits of the priority level register (not shown here) for determining the priority level. This means that there are 16 possible priority levels. And depending on the PRIGROUP setting (there are 5 possible settings), we divide these 4 bits among pre-emption and sub-priority.

The lower 4 bits of the priority level registers are not used. Setting them to any value has no effect.

5.6 Examples

Imagine you have 4 bits for setting the priority and you've chosen to use 2 bits for pre-emption priority and 2 bits for sub-priority (a PRIGROUP setting that allows for this division).

Example 1: No Sub-Priority

  • Interrupt A: Pre-emption priority = 2, Sub-priority = 0
  • Interrupt B: Pre-emption priority = 2, Sub-priority = 0 In this case, the interrupts have the same pre-emption and the same sub-priority. The one that arrives first will be handled first; it's essentially a race condition.

Example 2: Different Sub-Priority

  • Interrupt A: Pre-emption priority = 2, Sub-priority = 0
  • Interrupt B: Pre-emption priority = 2, Sub-priority = 1 Here, if both interrupts occur at the same time and have the same pre-emption level, Interrupt A will be handled first because it has a lower sub-priority.

Example 3: Different Pre-emption Priority

  • Interrupt A: Pre-emption priority = 1, Sub-priority = 0
  • Interrupt B: Pre-emption priority = 2, Sub-priority = 0 In this case, Interrupt A will always be handled first, regardless of its sub-priority, because it has a higher pre-emption priority (lower numerical value).

Example 4: Nested Interrupts with Higher Priority

  • Interrupt A: Pre-emption priority = 2, Sub-priority = 0 (currently executing)
  • Interrupt B: Pre-emption priority = 1, Sub-priority = 0 (arrives during A)

In this example, Interrupt A is currently being serviced. But if Interrupt B arrives and it has a higher pre-emption priority (lower numerical value), the processor will pause the handling of Interrupt A, save its state, and then service Interrupt B. Once Interrupt B is done, the processor will resume handling Interrupt A.

Example 5: Nested Interrupts with Equal Priority, Different Sub-Priority

  • Interrupt A: Pre-emption priority = 2, Sub-priority = 0 (currently executing)
  • Interrupt B: Pre-emption priority = 2, Sub-priority = 1 (arrives during A)

Here, because Interrupt B has the same pre-emption priority but a higher sub-priority, it won't interrupt the execution of Interrupt A. It will be serviced after Interrupt A completes.

Example 6: External Higher Priority Interrupt During Lower Priority

  • Interrupt A: Pre-emption priority = 2, Sub-priority = 0 (currently executing)
  • External Interrupt: Pre-emption priority = 1, Sub-priority = 0 (triggered)

If an external interrupt with a higher pre-emption priority is triggered during the execution of a lower-priority interrupt, it will cause the CPU to switch to the higher-priority interrupt, similar to Example 4.

Example 7: External Interrupt with Equal Priority, Different Sub-Priority

  • Interrupt A: Pre-emption priority = 2, Sub-priority = 0 (currently executing)
  • External Interrupt: Pre-emption priority = 2, Sub-priority = 1 (triggered externally)

In this case, Interrupt A is already being serviced. An external event triggers another interrupt that has the same pre-emption priority as Interrupt A but a higher sub-priority. Just like in Example 5, the external interrupt won't pre-empt the ongoing service of Interrupt A because they have the same pre-emption priority. The external interrupt will be queued and will wait until Interrupt A is complete before being serviced.

Example 8: Identical Pre-emption and Sub-Priority Levels

Let's say Interrupt A and Interrupt B have the same pre-emption and sub-priority levels.

Scenario 1: Interrupt B is servicing when Interrupt A arrives

Since Interrupt A and B have identical pre-emption level, the CPU will continue servicing Interrupt B without switching to Interrupt A. After completing B, it will service A.

Scenario 2: Interrupts A and B arrive "simultaneously"

In this case, the CPU uses internal rules to break the tie. On ARM Cortex-M processors, the tie-breaker is the IRQ number: the interrupt with the lower IRQ number will be serviced first. So if Interrupt A has a lower IRQ number, it gets serviced before B, despite both having the same priority settings.

After servicing the first interrupt, the CPU will then proceed to service the second one.