How to Set the Priority of an External Interrupt with an STM32 Microcontroller Board in C
In this article, we show how to set the priority of an external interrupt with an STM32 microcontroller board.
Say, for example, you have a program where you are using multiple external interrupts. If both interrupts are triggered at the same time, the interrupt with the higher priority will execute first.
Another scenario is an interrupt currently being executed by a program will be interrupted if another interrupt with higher priority is triggered.
If an interrupt with lower priority is triggered while an interrupt with higher priority is being executed, that interrupt will finish its execution because the other interrupt is of lower priority.
So knowing how to set the priority of an external interrupt is important, especially when you have a program dealing with multiple external interrupts and you need one to be prioritized over another.
So in order to set hte priority of an external interrupt, this is done on the processor side of the STM32 microcontroller board.
For an STM32 microcontroller board such as the STM32F407G discovery board, the processor is a Cortex-M4 processor.
The datasheet for the Cortex-M4 processor can be found here: Cortex-M4 Devices- Generic User Guide.
In the datasheet are various core registers of the processor.
In order to set the priority of an external interrupt, we need to reference the Interrupt Priority Registers of the Cortex-M4 processor.
The Interrupt Priority Registers allows us to set the priority level for any of the various external interrupts.
The priority level we can set for an external interrupt is a number from 0 to 15. The lower the number, the higher the priority, which means a priority of 0 is the highest priority an external interrupt can have.
The Interrupt Priority Registers comprise 60 registers in total.
These registers are shown in the diagram below.
So you can see there are 60 interrupt priority registers.
These registers are NVIC_IPR0 to NVIC_IPR59.
Each register is 32 bits in size and is divided into 4 parts consisting of 4 8-bit sections.
Each 8-bit section references 1 external interrupt pertaining to a specific IRQ.
The IRQ numbers are sequential, starting at 0 and going all the way to 240 (60 registers x 4 interrupts per register= 240 interrupts).
So the interrupt priority registers can set the interrupt priority of 240 external interrupts based on the IRQ number of the interrupt.
If you look at the vector table for the STM32F407G microcontroller in its datasheet, you will see that there are only 82 external interrupts possible for this microcontroller.
So it doesn't use the full capability that the processor can offer.
The first interrupt priority register, IPR0, sets the priorities for IRQ 0, IRQ 1, IRQ 2, and IRQ 3.
The first 8 bits of the register are for IRQ 0.
The next 8 bits are for IRQ 1.
The next 8 bits are for IRQ 2.
And the last (most upper) 8 bits of the 32 bits are for IRQ 3.
Register IPR1 sets the priorities for IRQ 4, IRQ 5, IRQ 6, and IRQ 7.
And so on.
To know which register you need to be working with when dealing with a particular IRQ number, take the IRQ number and divide it by 4. In the case of IRQ 6, 6/4 = 1. Therefore, we need to deal with register IPR1.
Now in order to know which 8-bit section you need to modify of this register IPR1, you take the IRQ number and use the modulus operator on it. 6%4= 2. Therefore, we write to the 3rd 8-bit section of the IPR1 register (IPR1[2]).
Continuing with IRQ 6 as an example, we need to modify the register IPR1 in bits 16-23.
So a generic formula to set the priority for a given IRQ number is shown as follows, *(pNVIC_PR_BASEADDR +(IRQ_NO/4)) |= (2 << (8 *(IRQ_NO % 4) + 4));
pNVIC_PR_BASEADDR represents the base address of the priority register. This was found in the same Cortex-M4 processor datasheet.
The base address is, 0xE000E400U
With an IRQ number of 6; we have, 6/4 = 1 in integer mathematics. We add 1 to the base address, which adds 4 bytes to the address. This gives us the address of 0xE000E404U, which is the address of register IPR1.
So now the pointer variable is referring to register, IPR1.
We then set the priority value of 2 to 8 times the value of 6%4, and then we add 4 to this value.
To understand this, multiplying the value by 8 gives us the start of the 8-bit section of the register.
The reason we add 4 to this register is that the value of the priority is set by setting the upper 4 bits of the 8 bits. The lower 4 bits are ignored.
The priority levels we can set ranges in value from 0 to 15. The lower the number, the greater the priority, so a priority value of 0 is the highest priority.
15 in binary is 4 digits, 1111
Therefore, only 4 bits are needed to set the priority and it is the upper 4 bits of the 8-bit section.
So this is all the work that goes into setting the priority for a specific IRQ number.
Let's do another example.
Let's say we want to set the priority of IRQ 40.
The interrupt priority register that we need to reference to set the priority for this IRQ is, 40/4= IPR10.
40%4= 0. Therefore, we need to reference the first 8 bits of this IPR10 register.
If we want to set the priority of it to 1, we have the following code, *(pNVIC_PR_BASEADDR +(IRQ_NO_40/4)) |= (1 << (8 *(IRQ_NO_40 % 4) + 4));
So now that you have the general idea of how to set the priority for a given IRQ number, let's do so in full code.
In this project, we will have 2 GPIO pins acting as sources of external interrupts.
GPIO port A pins 0 and 2 will act as sources of external interrupts, having push buttons connected to them.
GPIO port A pins 1 and 3 will act as outputs powering LEDs.
The GPIO port A pin 0 will be given a higher priority than the GPIO port A pin 2.
When you press down both push buttons at the same time, the external interrupt arising from GPIO port A pin 0 should take precedence and have its ISR executed over the external interrupt at pin 2.
Also if you press down the push button at pin 2 first and then press the push button at pin 0 next, the interrupt at pin 0 should interrupt the ISR at pin 2, because it has higher priority. So these are all things that you can try.
So the circuit schematic for this project is shown below.
So this is the physical setup of the circuit.
Again, there is no need to connect the pull-up resistors, because we do this in our software code. This is good because it requires less external parts to build the circuit.
So below we have our header file, which contains needed macros and structures.
So this header file contains all the definitions we need to create our main C file.
The contents of the main.c file is shown below.
We now go through our code, but emphasizing the part where we set the priorities for each of the 2 interrupts.
The first thing we must do is include the header file in our main.c file.
We next have our main function.
In our main function, the first thing we do is enable the peripheral clocks for the GPIO Port A and the SYSCFG register. The GPIO Port A pin 0 and pin 2 are where we will have the source of our external interrupts through push buttons connected to them. We must turn on the SYSCFG register because it selects pins 0 and 2 to be sources of external interrupts.
So in order to use a GPIO pin as an external interrupt, we must set it in the SYSCFG register. This process is a little involved. To find an another article dedicated to this, see How to select a GPIO pin to be the source input of an external interrupt in an STM32F407GG Board in C.
To set GPIO port A pins 0 and 2 as sources of external interrupts, we set register EXTICR1 bits 0-3 as 0000 and bits 8-11 as 0000. 0000 because each are connected to GPIO Port A.
So what we do next is make pins 0 and 2 of the FTSR register logic HIGHs, which makes the interrupts triggered at the falling edge of the signal.
We want to make sure that rising edge isn't triggered also, so we clear bits 0 and 2 of the RTSR register.
We then enable the interrupt by setting pins 0 and 2 to logic HIGH states in the IMR register, or interrupt mask register. The interrupt mask register allows us to enable or disable external interrupts for the GPIO pins.
Next we have to enable the interrupt for EXTI0 on the processor side. We do this by setting the appropriate bit in the Interrupt Set-enable register. So we just before enabled the interrupt on the microcontroller peripheral side. Now in order for the processor to take in the interrupt, we must enable it on the processor end. We do this by modifying the NVIC_ISER0 register; we write a logic HIGH to the 6th bit and the 8th bit of this register; these, respectively, enables the external interrupt fo IRQ 6 (GPIO Pin 0) and IRQ 8 (GPIO Pin 2).
One thing that is important to know that for the Interrupt set-enable registers is that there are 8 of these registers in total, ISER0-ISER7.
Each register is 32 bits, which means for the 8 registers, they compose 256 bits, which covers all possible 240 external interrupts that the Cortex-M4 processor can process.
Each bit refers to 1 of the possible external interrupts corresponding to the IRQ number of the interrupt.
So for the first register of the Interrupt Set-enable registers, ISER0, this addresses the external interrupts IRQ 0 to IRQ 31.
ISER1 corresponds to interrupts IRQ 32 to 63.
ISER2 corresponds to interrupts IRQ 64 to 93.
In our case, we want to enable the external interrupt, IRQ 6, which is ISER0 register bit 6; so we set this bit to 1. We also enable the external interrupt, IRQ 8, which is ISER0 register bit 8; we set this bit also to 1.
In the next step, we set the priority of the external interrupt.
This is important if you have multiple interrupts and want to prioritize one or some over others, as explained previously. In this example, we want to prioritize the external interrupt from GPIO port A pin 0 over the pin 2. To do this, we mmust supply GPIO pin 0 a higher priority level, which translates into a lower value. 0 to 15 can be assigned as the interrupt priority value, with the lower the number being the higher the priority. Therefore, the highest priority value is a 0.
In this program, we set GPIO pin 0 as the higher priority external interrupt; we assign it an interrupt priority value of 1. We assign GPIO pin 2 an interrupt priority value of 15.
We assign the GPIO pin 0 an interrupt priority value of 1 with the following line, *(pNVIC_PR_BASEADDR +(IRQ_NO_EXTI0/4)) |= (1 << (8 *(IRQ_NO_EXTI0 % 4) + 4));
The line above assigns 1 to bits 20-23 of the IPR2 register; thus, giving a priority value of 1 to the external interrupt at GPIO pin 0.
We assign the GPIO pin 2 an interrupt priority value of 15 with the following line, *(pNVIC_PR_BASEADDR +(IRQ_NO_EXTI2/4)) |= (15 << (8 *(IRQ_NO_EXTI2 % 4) + 4));
The line above assigns 15 to bits 4-7 of the IPR2 register; thus, giving a priority value of 1 to the external interrupt at GPIO pin 2.
If you don't understand this, read the beginning of this article again so that you can know how all these values have been derived.
So after this, we set GPIO port A pins 1 and 3 to be outputs. We do this by writing a 1 to bits 2 and 6 of the GPIO mode register.
We set GPIO port A pins 0 and 2 to be of high speed.
We then enable the internal pull up resistor on GPIOA port A pins 0 and 2. This makes the push buttons pulled up to VDD when not pressed. When they are pressed, they will be brought down to VSS, or GND.
So with an internal pull-up resistor, we do not have to connect external resistors, saving parts.
We then have the program loop forever with the statement, while(1);
After this main() function, we then create a delay() function, because we need to eliminate debouncing with the button press. We can do this in software. In hardware, you could use capacitors. In software, you just create a delay() function. The delay() function we create gives a delay of about 400ms or 4/10th of a second. This prevents multiple keypresses from being detected within a short period of time.
Next, we have our interrupt service routine, or ISR.
This is what is done when the external interrupt is triggered. We have 2 for each external interrupt. The routines are the same for each interrupt.
We use the delay() function to prevent debouncing.
The next thing we have to do is clear the corresponding bit pertaining to the GPIO pin in the pending register.
So when an external interrupt is triggered by the falling edge of the signal, what happens is the pending register of the EXTI registers is set to 1.
What we must do now is clear this bit. If this bit is not clear, the external interrupt, once triggered, stays triggered. To allow the interrupt to execute only once, we must clear the bit in the interrupt service routine. The bit is cleared by setting the bit to 1.
Lastly, we turn on the LED for about 4 seconds and then turn it off.
We turn on the LED for a relatively long period of time, so that you can see what happens when you trigger the lower priority external interrupt and then press the higher priority external interrupt. You should notice that the higher priority interrupt will interrupt the lower priority interrupt and the LED will not be on for the full 4 seconds. This is because the other interrupt has a higher priority and can interrupt lower priority interrupts.
So now you see the importance of setting the priority of interrupts when working
with multiple
external interrupts in a program, where you want one interrupt to have precedence over another.
So this is how to set the priority of an external interrupt in an STM32 board.
Related Resources