DEV Community

Cover image for STM32F4 Embedded Rust at the HAL: Timer Interrupts
Omar Hiari
Omar Hiari

Posted on • Edited on

STM32F4 Embedded Rust at the HAL: Timer Interrupts

This blog post is the second of a three-part series of posts where I explore interrupts for the STM32F401RE microcontroller using embedded Rust at the HAL level.

As a word of note, this post heavily depends on the previous post, STM32F4 Embedded Rust at the HAL: GPIO Interrupts. Thus I recommend that the reader refers to the previous post before reading this one.

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

Introduction

In the previous post, STM32F4 Embedded Rust at the HAL: GPIO Interrupts, I've implemented the button controlled blinking led application using interrupts. In that code, only the button event was configured for interrupts. However, there was a second event that was possible to encode as well, which is the timer delay. When the timer delay expires, we get an interrupt that we need to respond to. For those familiar with past posts and how we've done delay, a blocking delay_ms method was used. Using delay_ms essentially halts all operations in the controller until the delay expires. One can imagine how inefficient this kind of operation can be. Instead, with interrupts, the timer can be triggered to count while the controller goes about doing other things. The controller would only respond when the timer expires.

This post will mostly focus on the code implementation as many of the other sections are identical to the ones in STM32F4 Embedded Rust at the HAL: GPIO Interrupts.

πŸ“š 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 are 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 the include the following:

  • LED is connected to pin PA5 on the microcontroller. The pin will be used as an output.
  • User button connected to pin PC13 on the microcontroller. The pin will be used as an input.

πŸ‘¨β€πŸŽ¨ Software Design

Here I want to refer quickly to the state machine I presented in the past post. Recall that the button press event/transition was configured with interrupts in the past post. The idea here is to integrate interrupts for timer delay events as well, thus having the application run completely based on interrupts. This means that two ISRs will be needed instead; one for GPIO and one for Timer, combined with a main loop that does nothing.

FSM

πŸ“ Note

Applications that are completely driven by interrupts are ones that can provide a basic hardware scheduling environment. Typically a low-power application is one that would benefit from such an architecture. Essentially, when the application is idle it would go into low power sleep mode using the wfi or wfe instructions and wake up to process only when events occur.

Let's now jump into implementing this algorithm.

πŸ‘¨β€πŸ’» Code Implementation

πŸ“₯ Crate Imports

In this implementation, the crate imports are more or less the same as before except that I added the timer types needed from the stm32f4xx_hal.

use core::cell::{Cell, RefCell};
use cortex_m::interrupt::Mutex;
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
    gpio::{self, Edge, Input, Output, PushPull},
    pac::{self, interrupt, TIM2},
    prelude::*,
    timer::{CounterMs, Event},
};
Enter fullscreen mode Exit fullscreen mode

🌍 Global Variables

Remember that before I created two global variables wrapped in safe abstractions, one for the button to pass around the GPIO button handle and another for the delay value. I'm going to keep the previous global variables but I'm going to add an additional two to handle the LED GPIO output and the Timer peripheral. This is because I need to control the LED from the timer ISR and additionally clear the timer interrupt flag also in the timer ISR.

Again here, for convenience, I create another alias for the PA5 pin that is connected to the LED as follows:

type LedPin = gpio::PA5<Output<PushPull>>;
Enter fullscreen mode Exit fullscreen mode

Next, exactly as before, I create additional two static global variables called G_LED and G_TIMER wrapping the led pin LedPin and millisecond timer (I chose TIM2) CounterMs<TIM2> in safe abstractions as follows:

static G_TIM: Mutex<RefCell<Option<CounterMs<TIM2>>>> = Mutex::new(RefCell::new(None));

static G_LED: Mutex<RefCell<Option<LedPin>>> = Mutex::new(RefCell::new(None));
Enter fullscreen mode Exit fullscreen mode

πŸŽ› Peripheral Configuration Code

Obtain a handle for the device peripherals: This is the first step that we always need before configuring anything, taking the PAC-level device peripherals:

let mut dp = pac::Peripherals::take().unwrap();
Enter fullscreen mode Exit fullscreen mode

πŸ•Ή GPIO Configuration:

πŸ“ Note

GPIO pin and interrupt configuration in this post are exactly the same as in the previous post. I am keeping it to make the configuration code part of this post as self-contained as possible.

1️⃣ Promote the PAC-level GPIO structs: We need to configure the LED pin as a push-pull output and obtain a handler for the pin so that we can control it. We also need to obtain a handle for the button input pin. According to the connection details, the LED pin connection is part of GPIOA and the button connection is part of GPIOC. Before we can obtain any handles for the LED and the button we need to promote the pac-level GPIOA and GPIOC structs to be able to create handles for individual pins. We do this by using the split() method as follows:

let gpioa = dp.GPIOA.split();
let gpioc = dp.GPIOC.split();
Enter fullscreen mode Exit fullscreen mode

2️⃣ Obtain a handle for the LED and configure it to an output: As earlier stated, the on-board LED on the Nucleo-F401RE is connected to pin PA5 (Pin 5 Port A). As such, we need to create a handle for the LED pin that has PA5 configured to a push-pull output using the into_push_pull_output() method. We will name the handle led and configure it as follows:

let mut led = gpioa.pa5.into_push_pull_output();
Enter fullscreen mode Exit fullscreen mode

For those interested, this HAL documentation page has the full list of methods that the Pin type supports. Also, if you find the split() method confusing, please refer to my blog post here for more detail.

3️⃣ Obtain a handle and configure the input button: The on-board user push button on the Nucleo-F401RE is connected to pin PC13 (Pin 13 Port C) as stated earlier. Pins are configured to an input by default so when creating the handle for the button we don't call any special methods.

let mut button = gpioc.pc13;
Enter fullscreen mode Exit fullscreen mode

⏱ ⏸ Timer Configuration:

1️⃣ Configure the system clocks: The system clocks need to be configured as they are needed in setting up the timer peripheral. To set up the system clocks we need to first promote the RCC struct from the PAC and constrain it using the constrain() method (more detail on the constrain method here) to give use access to the cfgr struct. After that, we create a clocks handle that provides access to the configured (and frozen) system clocks. The clocks are configured to use an HSE frequency of 8MHz by applying the use_hse() method to the cfgr struct. The HSE frequency is defined by the reference manual of the Nucleo-F401RE development board. Finally, the freeze() method is applied to the cfgr struct to freeze the clock configuration. Note that freezing the clocks is a protection mechanism by the HAL to avoid the clock configuration changing during runtime. It follows that the peripherals that require clock information would only accept a frozen Clocks configuration struct.

let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
Enter fullscreen mode Exit fullscreen mode

2️⃣ Obtain a handle for the timer: I'll be using TIM2 to create a timer handle, I also use the counter_ms implementation for convenience as it would allow me to provide a millisecond value directly.

let mut timer = dp.TIM2.counter_ms(&clocks);
Enter fullscreen mode Exit fullscreen mode

The GPIO and Timer are both now configured though not set up for interrupts yet, which is the next step.

⏸ Interrupt Configuration:

The last thing that remains in the configuration is to configure and enable interrupt operation for the GPIO button and timer peripherals. This is so that when a button is pressed or the timer expires, execution switches over to the corresponding interrupt service routine. As mentioned before we need to configure the hardware in three steps:

1️⃣ 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. Nothing needs to be done here from a code perspective, however, I like to include this step for awareness.

2️⃣ Set up and enable interrupts at the peripheral level (Ex. GPIO and Timer): To set up the interrupts for GPIO we need access to the SYSCFG struct. In order to be able to use the SYSCFG struct, we first need to first promote it to the HAL level by constraining it using the constrain() method. How did I know that I need to constrain SYSCFG? I recommend you refer to this post for more detail. SYSCFG contains the information for a set of registers in the STM32 that are needed to configure interrupts for peripherals and is promoted as follows:

    let mut syscfg = dp.SYSCFG.constrain();
Enter fullscreen mode Exit fullscreen mode

Now the gpio button needs to be configured as an interrupt source in the STM32 device. In the STM32, GPIO interrupts are considered a type of external interrupt. In the stm32f4xx-hal documentation for GPIO I've found a bunch of external pin ExtiPin traits that would allow me to configure GPIO pin interrupts. As such, the GPIO pin is set up first by using the make_interrupt_source method to make it an interrupt source in the STM32 device. The make_interrupt_source method has the below signature, of which all it needs is a mutable reference to SysCfg.

fn make_interrupt_source(&mut self, syscfg: &mut SysCfg)
Enter fullscreen mode Exit fullscreen mode

Next, we would need to determine how the interrupt triggers (Ex. on a rising, falling, or both edges). Under the same list of ExtiPin traits, there is a trigger_on_edge method with the following signature:

fn trigger_on_edge(&mut self, exti: &mut EXTI, level: Edge)
Enter fullscreen mode Exit fullscreen mode

The method requires two parameters, a mutable reference to the pac external interrupt/event controller EXTI, and the interrupt trigger level. Edge is an enumeration with the different types of interrupt triggers for GPIO and looks as follows:

pub enum Edge {
    Rising,
    Falling,
    RisingFalling,
}
Enter fullscreen mode Exit fullscreen mode

Finally, one more step remains which is enabling the interrupt at the peripheral level. This is done via the enable_interrupt method:

fn enable_interrupt(&mut self, exti: &mut EXTI)
Enter fullscreen mode Exit fullscreen mode

Here the method only requires a mutable reference to the pac external interrupt/event controller EXTI.

Using the above methods, the GPIO button pin can now be configured for an interrupt as follows:

    button.make_interrupt_source(&mut syscfg);
    button.trigger_on_edge(&mut dp.EXTI, Edge::Rising);
    button.enable_interrupt(&mut dp.EXTI);
Enter fullscreen mode Exit fullscreen mode

As for the timer, first, we need to initialize the timer with a timeout value and start the counting. This is done using the start method, I chose a timeout of 2 seconds:

timer.start(2000.millis()).unwrap();
Enter fullscreen mode Exit fullscreen mode

Note that above I am using the millis method that I used in the past Button Controlled Blinking by Timer Polling post. As a reminder, what millis achieves is converting a u32 to a Duration because that is what the start method requires.

Second, just like GPIO, interrupts need to be enabled at the peripheral level. The method that does that for the timers is called listen. The method takes one parameter which is an Event enum that defines the type of event the timer generates an interrupt for. These events are ones defined by the controller datasheet. Here I chose an Update event:

 timer.listen(Event::Update);
Enter fullscreen mode Exit fullscreen mode

3️⃣ Enable interrupt source in the NVIC: Now that the button and timer interrupts are configured, the corresponding interrupt numbers in the NVIC needs to be unmasked. This is done using the NVIC unmask method in the cotrex_m::peripheral crate. As a reminder, the unmask method expects that we pass it the number for the interrupt that we want to unmask. This has been done by applying the generic Pin interrupt method on the button handle. However, for the timer, I couldn't locate in the documentation a method that returns the interrupt number for the timer. Though, I mentioned in the past post that there is an alternative approach leveraging the interrupt enum that enumerates all interrupt sources. For the timer, the interrupt source is TIM2 and the NVIC unmasking can be done as as follows:

    unsafe {
        cortex_m::peripheral::NVIC::unmask(interrupt::TIM2);
        cortex_m::peripheral::NVIC::unmask(button.interrupt());
    }
Enter fullscreen mode Exit fullscreen mode

4️⃣ Move Variable to Global Context: I initialized three global variables in the beginning with None, pending initialization. Those were G_BUTTON, G_TIM, and G_LED. Now that all associated peripherals have been initialized, they all need to be moved to the global context. This is can be done in one critical section as follows:

    cortex_m::interrupt::free(|cs| {
        G_TIM.borrow(cs).replace(Some(timer));
        G_BUTTON.borrow(cs).replace(Some(button));
        G_LED.borrow(cs).replace(Some(led));
    });
Enter fullscreen mode Exit fullscreen mode

Were done with the configuration! Over to the application code!

πŸ“± Application Code

πŸ” Application Loop

Following the design described earlier, there is no application code! Essentially all we need to do is set up an empty loop that goes on forever until it is preempted by one of our interrupts. However, I do something slightly different like this:

    loop {
        cortex_m::asm::wfi();
    }
Enter fullscreen mode Exit fullscreen mode

I am calling the wfi instruction from the cortex_m library. wfi stands for wait for interrupt and what it does is send the processor to sleep while it's sitting idle. The processor would then wake up when any of our interrupts occur. This is a power saving mechanism, alternatively, I could have kept the loop empty.

⏸ Interrupt Service Routine(s)

Here I need to set up the ISRs that would include the code that executes once any of the interrupts is detected. Here we're going to have two ISRs, one for the timer and one for the button. As before, we would have to use the #[interrupt] attribute, followed by a function name matching the interrupt name. Again, the interrupt name is obtained from the hal documentation, and in our case for the pin PC13 its EXTI15_10 and for the timer its TIM2.

1️⃣ Button Press ISR
Inside the button press ISR, the first thing that needs to be done is to modify the G_DELAYMS delay. Similar to the previous application, I chose to start with a delay of 2 seconds and decrease it in 0.5-second decrements. If the delay value reaches zero then I reset it again to 2 seconds. Though as before, to access G_DELAYMS, a critical section is needed. Additionally, I am using a new set method that allows me to modify the contents of G_DELAYMS.

    cortex_m::interrupt::free(|cs| {
        G_DELAYMS
            .borrow(cs)
            .set(G_DELAYMS.borrow(cs).get() - 500_u32);
        if G_DELAYMS.borrow(cs).get() < 500_u32 {
            G_DELAYMS.borrow(cs).set(2000_u32);
        });
Enter fullscreen mode Exit fullscreen mode

Now that the delay is different, the change needs to be propagated to the timer so that it starts counting with the new delay (that's the point of the button press after all). We'll have to use the start method again, but remember the timer is also in a global context wrapped in the G_TIM variable.

        let mut timer = G_TIM.borrow(cs).borrow_mut();

        timer
            .as_mut()
            .unwrap()
            .start(G_DELAYMS.borrow(cs).get().millis())
            .unwrap();
Enter fullscreen mode Exit fullscreen mode

Finally, as the interrupt pending flag for the button press in the hardware is still set. If it is not reset, then we won't be able to detect any subsequent interrupts. For that I would need a reference to the GPIO button peripheral which can be provided by the G_BUTTON global variable created earlier. As a result, in the same critical section started earlier the following lines can be added:

        let mut button = G_BUTTON.borrow(cs).borrow_mut();
        button.as_mut().unwrap().clear_interrupt_pending_bit();
Enter fullscreen mode Exit fullscreen mode

The first line obtains a mutable reference to the Option in G_BUTTON using the borrow_mut method. In the following line, the mutable reference is unwrapped, providing access to the button handle. Finally, the clear_interrupt_pending_bit method from the ExtiPin traits is applied to clear the interrupt pending flag/bit.

2️⃣ Timer ISR
In the timer ISR only two things need to be done. The first is toggling the LED since the timer expired and the second is clearing the timer interrupt pending flag. This also needs to be done in a critical section since we'll need access to the global G_LED and G_TIM variables. For the timer, the interrupt flag is cleared using a clear_interrupt method, that also requires the type of Event we are clearing.

    cortex_m::interrupt::free(|cs| {
        let mut led = G_LED.borrow(cs).borrow_mut();
        led.as_mut().unwrap().toggle();

        let mut timer = G_TIM.borrow(cs).borrow_mut();
        timer.as_mut().unwrap().clear_interrupt(Event::Update);
    });
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::{Cell, RefCell};
use cortex_m::interrupt::Mutex;
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
    gpio::{self, Edge, Input, Output, PushPull},
    pac::{self, interrupt, TIM2},
    prelude::*,
    timer::{CounterMs, Event},
};

// Create aliases for pins PC13 and PA5
type ButtonPin = gpio::PC13<Input>;
type LedPin = gpio::PA5<Output<PushPull>>;

// Global Variable Definitions
// Global variables are wrapped in safe abstractions.
// If you notice peripherals are wrapped in a different manner than regular global mutable data.
// In the case of peripherals we must be sure only one refrence exists at a time.
// Refer to Chapter 6 of the Embedded Rust Book for more detail.

// Create a Global Variable for the Button GPIO Peripheral that I'm going to pass around.
static G_BUTTON: Mutex<RefCell<Option<ButtonPin>>> = Mutex::new(RefCell::new(None));
// Create a Global Variable for the Timer Peripheral that I'm going to pass around.
static G_TIM: Mutex<RefCell<Option<CounterMs<TIM2>>>> = Mutex::new(RefCell::new(None));
// Create a Global Variable for the LED GPIO Peripheral that I'm going to pass around.
static G_LED: Mutex<RefCell<Option<LedPin>>> = Mutex::new(RefCell::new(None));
// Create a Global Variable for the delay value that I'm going to use to manage the delay.
static G_DELAYMS: Mutex<Cell<u32>> = Mutex::new(Cell::new(2000));

#[entry]
fn main() -> ! {
    // Setup handler for device peripherals
    let mut dp = pac::Peripherals::take().unwrap();

    // Configure the LED pin as a push pull ouput and obtain handle
    // On the Nucleo FR401 theres an on-board LED connected to pin PA5
    // 1) Promote the GPIOA PAC struct
    let gpioa = dp.GPIOA.split();
    // 2) Configure Pin and Obtain Handle
    let led = gpioa.pa5.into_push_pull_output();

    // Configure the button pin as input and obtain handle
    // On the Nucleo FR401 there is a button connected to pin PC13
    // 1) Promote the GPIOC PAC struct
    let gpioc = dp.GPIOC.split();
    // 2) Configure Pin and Obtain Handle
    let mut button = gpioc.pc13;

    // Configure Button Pin for Interrupts
    // 1) Promote SYSCFG structure to HAL to be able to configure interrupts
    let mut syscfg = dp.SYSCFG.constrain();
    // 2) Make button an interrupt source
    button.make_interrupt_source(&mut syscfg);
    // 3) Make button an interrupt source
    button.trigger_on_edge(&mut dp.EXTI, Edge::Rising);
    // 4) Enable gpio interrupt for button
    button.enable_interrupt(&mut dp.EXTI);

    // Configure and obtain handle for delay abstraction
    // 1) Promote RCC structure to HAL to be able to configure clocks
    let rcc = dp.RCC.constrain();
    // 2) Configure the system clocks
    // 8 MHz must be used for HSE on the Nucleo-F401RE board according to manual
    let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
    // 3) Create delay handle
    //let mut delay = dp.TIM1.delay_ms(&clocks);
    let mut timer = dp.TIM2.counter_ms(&clocks);

    // Kick off the timer with 2 seconds timeout first
    // It probably would be better to use the global variable here but I did not to avoid the clutter of having to create a crtical section
    timer.start(2000.millis()).unwrap();

    // Set up to generate interrupt when timer expires
    timer.listen(Event::Update);

    // Enable the external interrupt in the NVIC for all peripherals by passing the interrupt numbers
    unsafe {
        cortex_m::peripheral::NVIC::unmask(interrupt::TIM2);
        cortex_m::peripheral::NVIC::unmask(button.interrupt());
    }

    // Now that all peripherals are configured, move them into global context
    cortex_m::interrupt::free(|cs| {
        G_TIM.borrow(cs).replace(Some(timer));
        G_BUTTON.borrow(cs).replace(Some(button));
        G_LED.borrow(cs).replace(Some(led));
    });

    // Application Loop
    loop {
        // Go to sleep
        cortex_m::asm::wfi();
    }
}

// Button Interrupt
#[interrupt]
fn EXTI15_10() {
    // When Button interrupt happens three things need to be done
    // 1) Adjust Global Delay Variable
    // 2) Update Timer with new Global Delay value
    // 3) Clear Button Pending Interrupt

    // Start a Critical Section
    cortex_m::interrupt::free(|cs| {
        // Obtain Access to Delay Global Data and Adjust Delay
        G_DELAYMS
            .borrow(cs)
            .set(G_DELAYMS.borrow(cs).get() - 500_u32);

        // Reset delay value if it drops below 500 milliseconds
        if G_DELAYMS.borrow(cs).get() < 500_u32 {
            G_DELAYMS.borrow(cs).set(2000_u32);
        }

        // Obtain access to global timer
        let mut timer = G_TIM.borrow(cs).borrow_mut();

        // Adjust and start timer with updated delay value
        timer
            .as_mut()
            .unwrap()
            .start(G_DELAYMS.borrow(cs).get().millis())
            .unwrap();

        // Obtain access to Global Button Peripheral and Clear Interrupt Pending Flag
        let mut button = G_BUTTON.borrow(cs).borrow_mut();
        button.as_mut().unwrap().clear_interrupt_pending_bit();
    });
}

// Timer Interrupt
#[interrupt]
fn TIM2() {
    // When Timer Interrupt Happens Two Things Need to be Done
    // 1) Toggle the LED
    // 2) Clear Timer Pending Interrupt

    // Start a Critical Section
    cortex_m::interrupt::free(|cs| {
        // Obtain Access to Delay Global Data and Adjust Delay
        let mut led = G_LED.borrow(cs).borrow_mut();
        led.as_mut().unwrap().toggle();

        // Obtain access to Global Timer Peripheral and Clear Interrupt Pending Flag
        let mut timer = G_TIM.borrow(cs).borrow_mut();
        timer.as_mut().unwrap().clear_interrupt(Event::Update);
    });
}
Enter fullscreen mode Exit fullscreen mode

πŸ”¬ Further Experimentation/Ideas

Here the experimentation ideas remain the same, though can be adapted to the new structure.

  • If you have extra buttons, try implementing additional interrupts from other input pins where each button press applies a different delay. This is instead of one button applying one delay.
  • 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 counter and UART.

Conclusion

In this post, an interrupt-based LED control application was created leveraging the GPIO and Timer peripherals in the STM32F401RE microcontroller on the Nucleo-F401RE development board. All code was created at the HAL level using the stm32f4xx Rust HAL. As opposed to the prior post, the application is completely driven by interrupts. It can be seen that doing interrupts in Rust can be a bit verbose for all the safe abstractions that are added. In the following post, I will be porting the above code to the RTIC framework that can reduce much of the code. 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 (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.