DEV Community

Cover image for STM32F4 Embedded Rust at the PAC: GPIO Interrupts
Omar Hiari
Omar Hiari

Posted on • Edited on

STM32F4 Embedded Rust at the PAC: GPIO Interrupts

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

Nucleo Board

🔌 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 the Cell and RefCell pointer constructs.

  • The cortex_m crate to import the Mutex 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;
Enter fullscreen mode Exit fullscreen mode

🌍 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));
Enter fullscreen mode Exit fullscreen mode

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 a Box, RefCell allows for checking during runtime that only one mutable reference exists to a variable. A Box makes sure statically at compile time that only one mutable reference exists. Obviously Box 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();
Enter fullscreen mode Exit fullscreen mode

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) });
Enter fullscreen mode Exit fullscreen mode

⏸ 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.

RCC

// Assert the SYSCFG EN bit in the RCC register
dp.RCC.apb2enr.write(|w| w.syscfgen().set_bit());
Enter fullscreen mode Exit fullscreen mode

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.

SYSCFG

// Configure SYSCFG EXTICR4 Register
dp.SYSCFG.exticr4.write(|w| unsafe { w.exti13().bits(2) });
Enter fullscreen mode Exit fullscreen mode

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 .

EXTI IMR

// Disable the EXTI Mask using Interrupt Mask Register (IMR)
dp.EXTI.imr.write(|w| w.mr13().set_bit());
Enter fullscreen mode Exit fullscreen mode

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.

RTSR

// Configure the Rising Edge Trigger in the EXTI RTSR Register
dp.EXTI.rtsr.write(|w| w.tr13().set_bit());
Enter fullscreen mode Exit fullscreen mode

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) }
Enter fullscreen mode Exit fullscreen mode

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));
});
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

⏸ 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
}
Enter fullscreen mode Exit fullscreen mode

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()));
     }
});
Enter fullscreen mode Exit fullscreen mode

📱 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()));
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

🔬 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 👇.

If you found 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

Top comments (3)

Collapse
 
90degs2infty profile image
90degs2infty

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 the NVIC periphal (by a call to cortex_m::peripheral::NVIC::unpend(...))? I assume the EXTI peripheral somehow propagates the unpend to the NVIC? Do you have some material on this or can you explain in some more detail? 🙂 Thank you so much!

Collapse
 
theembeddedrustacean profile image
Omar Hiari

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.

Collapse
 
90degs2infty profile image
90degs2infty

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.