Introduction
Interrupts allow for efficient handling of events in embedded real-time applications. Interrupts are signals generated by hardware devices that require immediate attention from the microcontroller. Dealing with interrupts from an embedded microcontroller perspective is more complex than polled code. There is typically additional configuration and maintenance code that needs to be included. Additionally, Rust provides a safe and reliable way to handle interrupts by leveraging its ownership and borrowing system. This makes dealing with interrupts using Rust a bit more involved than usual.
In this post, I will create a GPIO application that is based on interrupts. A button press event will trigger an interrupt service routine (ISR) that will toggle an output LED. In the code, I will also be sticking strictly to the PAC level. Along the way, I will try my best to explain the elements introduced by Rust.
If you find this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:
Subscribe Now to The Embedded Rustacean
π Knowledge Pre-requisites
To understand the content of this post, you need the following:
Basic knowledge of coding in Rust.
Familiarity with the basic template for creating embedded applications in Rust.
Familiarity with interrupts in Cortex-M processors.
πΎ Software Setup
All the code presented in this post in addition to instructions for the environment and toolchain setup is available on the apollolabsdev Nucleo-F401RE git repo. Note that if the code on the git repo is slightly different then it means that it was modified to enhance the code quality or accommodate any HAL/Rust updates.
π Hardware Setup
Materials
π Connections
There will be no need for external connections. On-board connections will be utilized and include the following:
LED is connected to pin PA5 on the microcontroller. The pin will be used as an output.
A user button connected to pin PC13 on the microcontroller. The pin will be used as input.
π¨βπ¨ Software Design
In the application developed in this post, I want to toggle an LED with every button press. In essence, interrupts are software routines that are triggered by hardware events. As such, this application will be programmed to run entirely on interrupts, and the application loop
will be empty.
To provide some context for configuring interrupts, the hardware part is more or less the same across controllers that use a certain architecture (ex. ARM). For ARM-based controllers (the STM32F401RE being one) typically the following steps need to be done:
Configure and enable interrupts at the peripheral level (Ex. GPIO or ADC).
Enable global interrupts at the Cortex-M processor level (enabled by default).
Enable interrupts at the nested vectored interrupt controller (NVIC) level.
After that, one would need to define an Interrupt Service Routine (ISR) in the application code. As one would expect, the ISR contains the code executed in response to a specific interrupt event. Additionally, inside the ISR, it is typical that one would use values that are shared with the main
routine. Also in the ISR, one would have to clear the hardware pending interrupt flag to allow consecutive interrupts to happen. This is a bit of a challenge in Rust for two reasons; First, to clear the pending flag one would need to access the peripheral registers. This is an issue because if you recall, Rust follows a singleton pattern and we cannot have more than one reference to a peripheral. Second, in Rust, global mutable variables, rightly so, are considered unsafe to read or write. This is because without taking special care, a race condition might be triggered. To solve both challenges, in Rust, global mutable data and peripherals need to be wrapped in safe abstractions that allow them to be shared between the ISR and the main thread.
Let's move on to the code.
π¨βπ» Code Implementation
π₯ Crate Imports
In this implementation the crates required are as follows:
The
core
crate to import theCell
andRefCell
pointer constructs.The
cortex_m
crate to import theMutex
construct.The
cortex_m_rt
crate for startup code and minimal runtime for Cortex-M microcontrollers.The
panic_halt
crate to define the panicking behavior to halt on panic.The
stm32f4xx_pac
crate to import the STM32F401 microcontroller device PAC API that was created in the first post in this series.
use core::cell::RefCell;
use cortex_m::interrupt::Mutex;
use cortex_m_rt::entry;
use pac::{interrupt, Peripherals};
use panic_halt as _;
use stm32f401_pac as pac;
π Global Variables
In the application at hand, I'm choosing to enable interrupts for the GPIO peripheral to detect a button press. As such, I would need to create a global shared variable to access the GPIO peripheral (remember the singleton pattern). This is because I would need to subsequently disable the interrupt pending flag in the ISR. In particular, I will be using PC13
as the GPIO input pin that I want to enable interrupts for. For convenience, I create a static
global variable called G_PER
wrapping the complete Peripherals
in a safe abstraction as follows:
// Create a Global Variable for the Peripherals
static G_PER: Mutex<RefCell<Option<Peripherals>>> = Mutex::new(RefCell::new(None));
So here the Peripherals
struct is wrapped in an Option
that is wrapped in a RefCell
, which is wrapped in a Mutex
. The Mutex
makes sure that the peripheral can be safely shared among threads. Consequently, this means that it would require that we use a critical section to be able to access the peripheral. The RefCell
is used to be able to obtain a mutable reference to the peripheral. Finally, the Option
is used to allow for lazy initialization as one would not be able to initialize the variable until later (after I configure all peripherals).
π Notes:
1οΈβ£ These global variables can be viewed as entities that exist in a global context where access is obtained at runtime by the thread that needs them. This is why
RefCell
is needed. Compared to aBox
,RefCell
allows for checking during runtime that only one mutable reference exists to a variable. ABox
makes sure statically at compile time that only one mutable reference exists. ObviouslyBox
cannot be used with interrupts as multiple threads would require mutable access during runtime.2οΈβ£ I would strongly recommend referring to Chapter 6 of the Embedded Rust Book for more detail on this topic.
π Peripheral Configuration Code
1- Obtain a handle for the device peripherals: In embedded Rust, as part of the singleton design pattern, we first have to take the PAC-level device peripherals. This is done using the take()
method. Here I create a device peripheral handler named dp
as follows:
let mut dp = pac::Peripherals::take().unwrap();
2- Enable Clock & Configure GPIO: This is similar to what was done in the GPIO post. First, the clocks to GPIOA
and GPIOC
need to be enabled through the RCC registers. Second, the pins that need to be output (PA2 in our case) need to be configured through the ODR
register.
// Enable Clock to GPIOA & GPIOC
dp.RCC
.ahb1enr
.write(|w| w.gpioaen().set_bit().gpiocen().set_bit());
// Configure PA5 as Output
dp.GPIOA.moder.write(|w| unsafe { w.moder5().bits(0b01) });
βΈ Interrupt Configuration Code
The last thing that remains in the configuration is to configure and enable interrupt operation for the GPIO button peripheral. This is so that when a button is pressed, execution switches over to the interrupt service routine. First I will configure the GPIO peripheral. For that, some system-level configurations are required first.
1- Assert the SYSCFGEN bit in the RCC_APB2ENR register: This is a similar idea to the configuration of GPIOs where clocks needed to be enabled to the peripherals first. Here we are enabling the clock to the system configuration controller that controls external interrupts.
// Assert the SYSCFG EN bit in the RCC register
dp.RCC.apb2enr.write(|w| w.syscfgen().set_bit());
2- Connect External Interrupt Line to PC13: Here the external interrupt line needs to be connected to the pin we want to configure for interrupts (PC13). This is done by configuring the EXTI13
field in the SYSCFG_EXTICR4
register.
// Configure SYSCFG EXTICR4 Register
dp.SYSCFG.exticr4.write(|w| unsafe { w.exti13().bits(2) });
3- Unmask the external interrupt for PC13: All interrupts in the STM32F4 are masked by default. To let them through, they need to be individually unmasked. This is done by asserting the field mr13
in the interrupt mask register EXTI_IMR
.
// Disable the EXTI Mask using Interrupt Mask Register (IMR)
dp.EXTI.imr.write(|w| w.mr13().set_bit());
4- Configure the interrupt trigger: This is the last step in peripheral-level configuration. Here we have to decide what would trigger an interrupt. A rising edge, falling edge, or both. I've elected to go for triggering on a rising edge. This is configured by asserting the tr13
field in the EXTI_RTSR
register.
// Configure the Rising Edge Trigger in the EXTI RTSR Register
dp.EXTI.rtsr.write(|w| w.tr13().set_bit());
5- Enable global interrupts at the Cortex-M processor level: Cortex-M processors have an architectural register named PRIMASK that contains a bit to enable/disable interrupts globally. Note that interrupts are globally enabled by default in the Cortex-M PRIMASK register. Technically nothing needs to be done here from a code perspective, however I wanted to mention this step for awareness.
6- Enable interrupt source in the NVIC: Now that the button interrupt is configured, the corresponding interrupt number in the NVIC needs to be unmasked. This is done using the NVIC unmask
method in the cotrex_m::peripheral
crate. Also it must be noted that unmasking interrupts in the NVIC is considered unsafe
in Rust. However, in this case it's fine since we know that we aren't doing any unsafe
behavior. The unmask
method expects that we pass it the number for the interrupt that we want to unmask. This could be done by leveraging the interrupt
enum in the PAC crate that enumerates all the device interrupts. Our interrupt source name is EXTI15_10
which covers all interrupts for lines 10-15 (ours is 13).
// Enable EXT13 at NVIC Level
unsafe { cortex_m::peripheral::NVIC::unmask(interrupt::EXTI15_10) }
7- Move Peripherals to Global Context: Recall how earlier a global variable G_PER
was introduced to move around the GPIO peripheral between contexts. However, G_PER
was initialized with None
pending the configuration of the GPIO button that is now available. This means that we can now move dp
to the global context in which it can be shared by threads. This is done as follows:
cortex_m::interrupt::free(|cs| {
G_PER.borrow(cs).replace(Some(dp));
});
Here we are introducing a critical section of code enclosed in the closure cortex_m::interrupt::free
. In this critical section of code, preemption from interrupts is disabled to ensure that accessing the global variable does not introduce any race conditions. This is required because G_PER
is wrapped in a Mutex
. The closure passes a token cs
that allows us to borrow
a mutable reference to the global variable and replace the Option
inside of with Some(button)
.
Note that from this point on in code, every time we want to access G_PER
(or any other Mutex
global variable) we would need to introduce a critical section using cortex_m::interrupt::free
.
π± Application Code
π Application Loop
Following the design described earlier there is no application loop. All the code for this example will be managed through the ISR. As such our application loop will remain empty.
loop {}
βΈ Interrupt Service Routine(s)
Here I need to setup the ISR that would include the code that executes once the interrupt is detected. To define the interrupt in Rust, first one would need to use the #[interrupt]
attribute, followed by a function definition that has the interrupt name as an identifier. The interrupt name is obtained from the hal documentation and in our case for the pin PC13 its EXTI15_10
. This looks as follows:
#[interrupt]
fn EXTI15_10() {
// Interrupt Service Routine Code
}
Inside the ISR, the first thing that needs to be done is to check that PA13
was the cause of the interrupt. This is done by checking if the pr13
field in the pending register EXTI_PR
is asserted. If pr13
is asserted then it first needs to be cleared by asserting it again. After that the LED output is toggled via the GPIOA_ODR
register. Though note that as before, to access G_PER
, a critical section is needed. The first line in the critical section binds a handle dp
to a mutable reference of the Option
in G_PER
using the borrow_mut
method. In the following lines, dp
is unwrapped, providing access to the button
handle.
// Start a Critical Section
cortex_m::interrupt::free(|cs| {
// Obtain Access to Peripherals Global Data
let mut dp = G_PER.borrow(cs).borrow_mut();
// Check if PA13 caused the interrupt
if dp.as_mut().unwrap().EXTI.pr.read().pr13().bit() {
// Clear Interrupt Flag for Button
dp.as_mut().unwrap().EXTI.pr.write(|w| w.pr13().set_bit());
// Toggle Output LED
dp.as_mut()
.unwrap()
.GPIOA
.odr
.modify(|r, w| w.odr5().bit(!r.odr5().bit()));
}
});
π± Full Application Code
Here is the full code for the implementation described in this post. You can additionally find the full project and others available on the apollolabsdev Nucleo-F401RE git repo.
#![no_std]
#![no_main]
// Imports
use core::cell::RefCell;
use cortex_m::interrupt::Mutex;
use cortex_m_rt::entry;
use pac::{interrupt, Peripherals};
use panic_halt as _;
use stm32f401_pac as pac;
// Create a Global Variable for the Peripherals
static G_PER: Mutex<RefCell<Option<Peripherals>>> = Mutex::new(RefCell::new(None));
#[entry]
fn main() -> ! {
// Setup handler for device peripherals
let dp = pac::Peripherals::take().unwrap();
// GPIO Configuration
// 1. Enable Clock to GPIOA & GPIOC
dp.RCC
.ahb1enr
.write(|w| w.gpioaen().set_bit().gpiocen().set_bit());
// 2. Configure PA5 as Output
dp.GPIOA.moder.write(|w| unsafe { w.moder5().bits(0b01) });
// Interrupt Configuration
// 1. Enable the SYSCFG bit in the RCC register
dp.RCC.apb2enr.write(|w| w.syscfgen().set_bit());
// 2. Configure SYSCFG EXTICR4 Register
dp.SYSCFG.exticr4.write(|w| unsafe { w.exti13().bits(2) });
// 3. Disable the EXTI Mask using Interrupt Mask Register (IMR)
dp.EXTI.imr.write(|w| w.mr13().set_bit());
// 4. Configure the Rising Edge Trigger in the EXTI RTSR Register
dp.EXTI.rtsr.write(|w| w.tr13().set_bit());
// 5. Enable EXT13 at NVIC Level
unsafe { cortex_m::peripheral::NVIC::unmask(interrupt::EXTI15_10) }
// Since Initialization is complete, move Peripherals struct to Global Context
cortex_m::interrupt::free(|cs| {
G_PER.borrow(cs).replace(Some(dp));
});
// Application Loop
loop {}
}
// Handler for pins connected to line 10 to 15
#[interrupt]
fn EXTI15_10() {
// Start a Critical Section
cortex_m::interrupt::free(|cs| {
// Obtain Access to Peripherals Global Data
let mut dp = G_PER.borrow(cs).borrow_mut();
// Check if PA13 caused the interrupt
if dp.as_mut().unwrap().EXTI.pr.read().pr13().bit() {
// Clear Interrupt Flag for Button
dp.as_mut().unwrap().EXTI.pr.write(|w| w.pr13().set_bit());
// Toggle Output LED
dp.as_mut()
.unwrap()
.GPIOA
.odr
.modify(|r, w| w.odr5().bit(!r.odr5().bit()));
}
});
}
π¬ Further Experimentation/Ideas
If you have extra buttons, try implementing additional interrupts from other input pins where each button toggles the same LED.
A cool mini project is capturing a human response time. Using the LED and a press button, see how long it takes you to press the button after the LED turns on. You can use a counter/timer peripheral to capture duration and UART to propagate the result. Refer to past posts for dealing with the timer and UART.
Conclusion
In this post, an interrupt-based LED control application was created leveraging the GPIO peripheral for the STM32F401RE microcontroller on the Nucleo-F401RE development board. All code was created at the PAC level. Have any questions/comments? Share your thoughts in the comments below π.
Top comments (3)
Hey, thanks for the concise walkthrough! π
One question that I encountered while following your post: in the interrupt handler (link to github to the specific line), you unpend the interrupt using the
EXTI
peripheral. Apparently, this suffices. But I wonder why. Is it unnecessary to also unpend the interrupt using theNVIC
periphal (by a call tocortex_m::peripheral::NVIC::unpend(...)
)? I assume theEXTI
peripheral somehow propagates the unpend to theNVIC
? Do you have some material on this or can you explain in some more detail? π Thank you so much!Thanks for reading @90degs2infty !
You are right that the NVIC pending flag needs to be reset, however, pending interrupts in the NVIC are cleared automatically when the interrupt becomes active (is being serviced). It has nothing to do with the
EXTI
perihpheral propagating information. As a matter of fact, there isn't any mechanism that the ARM processors support where entities external to the processor can propagate their local interrupt pending status. The peripheral has its own flag that is seperate from the NVIC.I see, this has been a misconception on my side. Thank you so much for the clarification, @apollolabsbin ! π
Some comments may only be visible to logged-in visitors. Sign in to view all comments.