This article describes GPIO interrupts, including examples of interrupts and their various functions. It is a continuation of a previous piece that explained the concepts of concurrency and interrupts for microcontrollers.
What Does a GPIO Peripheral Do?
The GPIO peripheral is capable of detecting (or “knowing”) four things: whether the value on the pin is 1 or 0, and by extension whether the value changes from 0 to 1 or 1 to 0.
These are useful for detecting a number of events. For example, if I connect a reed switch to a door, then my program on the microcontroller can tell whether the door just opened or just closed based change in value on the pin the reed switch is connected to, as illustrated in Figure 1.
Figure 1. Using GPIO and reed switch to detect door state (open or closed) and state changes.
I will first explain how interrupts work assuming everything has been configured properly, and then we will look at the various parts that need to be configured properly for the interrupt to work.
Peripheral Interrupt Flag
Let’s assume similar to our earlier example that the event we are trying to detect is when the pin value changes from 1 to 0. Inside the GPIO peripheral, there will be a piece of hardware that detects this change and indicates that this change has occurred by setting what is called the interrupt flag for that pin to 1.
This is shown in Figure 2.
Figure 2. Setting of a GPIO pin interrupt flag after detection of an event that should generate an interrupt.
Interrupt Controller and Interrupt Controller Flags
There are many peripherals in a microcontroller and each can has its own set of interrupts. Most microcontrollers have a piece of hardware called typically called the interrupt controller which manages all the interrupts coming from the peripherals, decides which interrupt gets to run, and interrupts the CPU to cause it to execute the right ISR.
Typically, the interrupt controller has a list of possible interrupts as well as their corresponding priority. Our GPIO peripheral may have one or more interrupts for it in the list that the interrupt controller keeps.
For example, for the CC2544, there is a group of eight pins that are part of the GPIO called PORT0. Each pin is labeled P0_0, P0_1, and so on till P0_7. Although each pin has its own interrupt flag, the interrupt controller has only one interrupt for the whole port, P0INT. Whenever any of the pin flags in the GPIO peripheral are set, the flag in the interrupt controller for the whole port is also set.
Notice that there are two flags here, one is the flag for the particular pin, which is part of the GPIO peripheral, and the other is the interrupt flag for the whole port which is part of the interrupt controller. This is shown in Figure 3.
Figure 3. Setting of GPIO pin interrupt flag and interrupt flag for GPIO in interrupt controller after detection of event that should generate interrupt.
Many microcontrollers use what is called a vectored approach for interrupts. In this approach, there is a vector table in memory that lists, for each interrupt, the address where the ISR that the CPU must execute for that particular interrupt. This address is typically called the interrupt vector.
For example, for CC2544, which uses an 8501 microcontroller architecture, the interrupt vector for PORT0 is memory address 0x6B. When the interrupt controller tells the CPU that there is an interrupt from a particular vector, the CPU does some bookkeeping and then starts to execute the ISR from that interrupt vector. This is illustrated in Figure 4.
Figure 4. Executing of ISR for GPIO interrupt vector after setting of GPIO pin interrupt flag and interrupt flag for GPIO in interrupt controller after detection of event that should generate interrupt.
Configuring Interrupt Behavior
Peripherals like GPIO typically give you the option to configure what kinds of events will cause the peripheral to generate an interrupt. For GPIO, the typical options are when the value changes from 0 to 1, when the value changes from 1 to 0, any change in value (i.e., 0 to 1 or 1 to 0 but doesn’t matter which), or when the value stays 1 or 0.
Depending on the microcontroller, this can be done per pin or for all pins on the port. The ATmega328P has two pins where you can change this individually. The other GPIO pins by default detect any change (0 to 1 or 1 to 0) on the pin. Recall that in the previous article where we illustrated how interrupts work, we assumed that the pin was configured to only detect changes from 1 to 0.
In addition, some microcontrollers require that the pin of interest be configured as an input in order for the interrupt flag to be set when the event occurs (e.g., CC2544). Others (e.g., ATmega328P) will set the flag regardless of whether the pin is configured as an output or input.
A common term used to describe enabling and disabling interrupts is “masking”. Typically, there are various levels at which interrupts can be disabled. The CPU can enable or disable all interrupts, though usually there are some interrupts that are critical called non-maskable interrupts that are never disabled.
Disabling all interrupts in the CPU essentially stops communication between the interrupt controller and the CPU. What this means is that the pin flag in the GPIO peripheral and the flag for its corresponding interrupt in the controller will be set; however, the CPU will not get the interrupt request. This is illustrated in Figure 5.
Figure 5. Disabling interrupts globally at the CPU level.
Another level at which interrupts can be masked is at the interrupt controller level. Here we can enable or disable a specific interrupt inside the controller.
Interrupt Example: CC2544
A concrete example helps. Let’s say we are using the CC2544 and we disabled the PORT0. Let’s say pin P0_3 changed its value so that its flag is set in the GPIO peripheral. The PORT0 interrupt flag will then also be set in the interrupt controller, but the interrupt controller ignores the flag.
This is different from the CPU disabling all interrupts because the interrupt controller is still in communication with the CPU. So if, for example, PORT1 has its interrupt enabled and pin P1_2 changed its value so its flag was set in the GPIO peripheral and the PORT1 interrupt flag was also set, the interrupt controller would interrupt the CPU to handle that interrupt.
The case where the interrupt controller ignores the interrupt vector’s flag is illustrated in Figure 6.
Figure 6. Interrupt masking at the interrupt controller level.
Most microcontrollers also allow interrupts to be masked at the peripheral level. Here, we can enable or disable the interrupt for the particular pin in the GPIO peripheral.
In all the microcontrollers I have come across, the interrupt flag is always set when the event we are looking for occurs in the GPIO peripheral, regardless of whether the interrupt for that pin is enabled or disabled. For example, if pin P0_3 changed its value in the way we are looking for, its flag would be set in the GPIO peripheral. However, the GPIO peripheral will not alert the interrupt controller of this so the PORT0 interrupt flag in the interrupt controller will not be set, and since we need that flag to be set in order to interrupt the CPU, the interrupt will not occur. This is illustrated in Figure 7.
Figure 7. Interrupt masking at the GPIO peripheral level
When an interrupt is masked, it still gets detected. The CPU just does not respond to it. If the interrupt flag is not cleared and the interrupt gets completely unmasked then the CPU will then respond to it if it meets all the other conditions (beyond masking) for it to be executed. An interrupt that has been detected and is waiting for the CPU to execute its ISR is typically called a pending interrupt.
Figure 8 illustrates a case where the interrupt is pending and then later unmasked.
Figure 8. Interrupt masking and unmasking at the GPIO level, assuming interrupt is unmasked at the interrupt controller and CPU level.
To recap, to completely unmask an interrupt so the CPU can respond to it when all other conditions for the interrupt are satisfied:
- The interrupt must be enabled in the peripheral (if applicable).
- Its corresponding interrupt in the interrupt controller must also be enabled.
- All interrupts must be enabled by the CPU (i.e., the communication between the CPU and interrupt controller must be enabled for maskable interrupts).
Sometimes two or more events that result in interrupts happen at the same time. When this occurs, the interrupt controller needs a mechanism to know which one should go first and so on since the CPU can only handle one interrupt at a time. The interrupt controller usually provides a configuration called a priority that allows the user, through their code, to specify which interrupts are higher priority and which are lower priority. Most also provide a default setting for each interrupt.
Whenever multiple events occur at the same time and result in multiple interrupts pending at the interrupt controller, the interrupt controller picks the highest priority interrupt for the CPU to handle. Interrupts can interrupt (or preempt) already running interrupts so if a lower priority interrupt is being handled by the CPU, and event associated with a higher priority interrupt occurs, the controller will interrupt the CPU to handle the higher priority interrupt, and the CPU will resume handling the lower priority interrupt that was preempted when it is done. Figure 9 illustrates how priorities and preemption work for interrupts.
Figure 9. Interrupt handling with priorities assuming only two interrupts.
- The higher priority and lower priority interrupt flags are set at the same time. The CPU executes the higher priority interrupt before the lower priority interrupt (since its flag is still pending when the higher priority interrupt is done)
- The higher priority interrupt flag is set after the CPU has started handling the lower priority interrupt. The higher priority interrupt preempts the lower priority one and the CPU executes the ISR for the higher priority interrupt to completion before resuming the execution of the ISR for lower priority interrupt. Notice that the CPU resumes execution of the ISR for the lower priority interrupt even though its flag is still not set by the time it is done with the ISR for the higher priority one. This is because after executing an ISR, unless the code interferes with the normal interrupt process, the CPU always resumes to whatever state it was in before it began executing the ISR. This state that it returns to could be another ISR.
- The lower priority interrupt flag is set after the higher priority one. Since lower priority interrupts cannot preempt higher priority interrupts, the CPU executes the higher priority interrupt ISR to completion before responding to the lower priority one.
Checking and Clearing Interrupt Flags
We saw earlier that for some microcontrollers like the CC2544, when the ISR code starts executing, we only know which port caused the interrupt, but not the specific pin. For example, if P0_3 changes its value, its flag will be set inside the GPIO peripheral but the CPU only executes the ISR in response to the PORT0 interrupt flag from the interrupt controller. Checking the GPIO peripheral interrupt flags inside the ISR tells us which specific pin produced the interrupt so we can respond accordingly.
Since the interrupt flag indicates that the event we are looking for has occurred, as long as the interrupt flag is set, the CPU will respond to the interrupt each time it has a chance to. For example, let’s say P0_3 changed state only once and caused its interrupt flag to be set. If we leave the flag set, then after the CPU has run the ISR related to the PORT0, it will still think there is a new interrupt so it will run the ISR again.
To avoid this, we need to clear the interrupt flag. Sometimes the interrupt flag is cleared automatically by the CPU when it starts running the ISR; other times you have to clear the flag yourself. The technical documents for the microcontroller will let you know which is the case. The CC2544, for example, does not automatically clear the flag for pin interrupts, but the ATmega328P does. If you have to clear the interrupt yourself, this is usually the first thing you do in ISR code, typically after figuring out which pin interrupt caused the ISR to be executed.
Recap: Getting A GPIO Interrupt to Work
To put all of the above together, in order to get a GPIO interrupt working with your code, you must:
- Write an ISR, inside of which you
- make sure to clear any flags that need clearing
- respond to the interrupt with the desired action
- Associate the ISR with the right interrupt vector
- Configure which GPIO event you want to trigger the interrupt. The possible options, which may not all be available for your specific microcontroller, are only change from 0 to 1, only change from 1 to 0, any change (0 to 1 or 1 to 0), steady at 1, or steady at 0.
- Enable the interrupt for the pin inside the GPIO. It is usually advisable to clear the flag for the pin before enabling the pin interrupt.
- Enable the interrupt inside the interrupt controller.
- Make sure the CPU has all interrupts enabled.
If you have questions about GPIO interrupts as I've described them above, share them in the comments below.