This blog post is the eighth of a multi-part series of posts where I explore various peripherals in the ESP32C3 using standard library embedded Rust and the esp-idf-hal. Please be aware that certain concepts in newer posts could depend on concepts in prior posts.
Introduction
It's well established that interrupts are a tough concept to grasp for the embedded beginner. Add to that when doing it in Rust the complexity of dealing with mutable static
variables. This is because working with shared variables and interrupts is inherently unsafe
if proper measures are not taken. When looking at how to do interrupts using the esp-idf-hal
I first resorted to the Embedded Rust on Espressif book. Interrupts are covered under the Advanced Workshop in section 4.3, and to be honest, I was taken aback a little at what could be an additional level of complexity for a beginner. Without too much detail, this is because the book resorts to using lower-level implementations. For those interested, by that, I mean FFI interfaces to FreeRTOS which I will be creating a separate post about later.
I figured there must be a more friendly interface at the esp-idf-hal
level. However what got me a bit worried at first is that when I dug into the esp-idf-hal
examples, I didn't find any with interrupts π±. With some further digging into the esp-idf-hal
documentation afterward, I found some interfaces that I figured could help. Although the methods didn't have any descriptions, the names were more or less self-explanatory.
The thing about such interfaces, you need to understand how interrupts work to be able to use them. The good news is that regardless of device/architecture interrupts follow more or less a similar process. As such, in this post, I will be using the esp-idf-hal
interfaces to create an interrupt-based application. The application would detect button presses, count them, and print the count to the console.
If you find this post useful, and if Embedded Rust interests you, stay in the know 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.
πΎ Software Setup
All the code presented in this post is available on the apollolabs ESP32C3 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.
Additionally, the full project (code and simulation) is available on Wokwi here.
π Hardware Setup
Materials
Pushbutton
π Connections
π Note
All connection details are also shown in the Wokwi example.
Connections include the following:
- On one end, the button pin should be connected to gpio0 of the devkit. The gpio0 pin will be configured as input. On the same button end, the other pin of the switch will be connected to the devkit GND.
π¨βπ¨ Software Design
In the application software in this post, a count variable will be incremented every time the button is pressed. Upon an interrupt occurrence, the count variable will also be printed to the console. In this application, the button would be configured for interrupts.
Interrupts are software routines (or functions) that get triggered/called by hardware events. These software routines are usually referred to as interrupt service routines or ISRs. Generally, in a microcontroller, there are several hardware sources that can be configured to provide interrupts on an event. In our case, we want to configure a GPIO input pin to call an ISR based on a button press event. An input pin can be configured to detect either signal edge (positive or negative) caused by the event. Regardless of the peripheral, typically the steps to configure a hardware source to trigger an interrupt follow a similar pattern. For the input GPIO button, these are the steps:
Configure the button pin as input.
(Optional) Configure any internal pull-ups or pull-downs. This isn't needed if the pull-up/down resistor exists external to the pin.
Configure the interrupt type you want the pin to detect.
Attach/subscribe the interrupt service routine to the button interrupt. Here we are telling the controller which routine (ISR) to call on the occurrence of the event.
Enable the interrupt to start listening for events.
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. This is a bit of a challenge in Rust. In Rust, global mutable (static
) variables, rightly so, are considered unsafe to read or write. This is because without taking special care, a race condition might emerge. To solve the challenges, in Rust, global mutable data needs to be wrapped in a safe abstraction that allow it to be shared between the ISR and the main thread. Enough explanations, lets move on to the actual code.
π¨βπ» Code Implementation
π₯ Crate Imports
In this implementation the crates required are as follows:
The
std::sync
to import synchronization primitives.The
esp_idf_hal
crate to import the needed device hardware abstractions.
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use esp_idf_hal::gpio::*;
use esp_idf_hal::peripherals::Peripherals;
π Initialization/Configuration Code
π 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 (static
) shared variable to allow the ISR to indicate that an interrupt occurred. This static
needs to be wrapped in a safe abstraction. Consequently, I create a static
global variable called FLAG
with type AtomicBool
:
static FLAG: AtomicBool = AtomicBool::new(false);
The AtomicBool
is a synchronization primitive from std::sync
that makes sure that the peripheral can be safely shared among threads. It's initialized to false
as no interrupts have been detected yet. On a side note,std::sync
provides several synchronization primitives that one would use depending on the type that needs to be wrapped.
GPIO Peripheral Configuration:
1οΈβ£ Obtain a handle for the device peripherals: Similar to all past blog posts, in embedded Rust, as part of the singleton design pattern, we first have to take the device peripherals. This is done using the take()
method. Here I create a device peripheral handler named peripherals
as follows:
let dp = Peripherals::take().unwrap();
2οΈβ£ Obtain a Handle for the Button Pin and Configure it as Input: Using the input
instance method in gpio::PinDriver
, we create a button
configuring gpio0
to an input:
let mut button = PinDriver::input(dp.pins.gpio0).unwrap();
3οΈβ£ Configure Button Pin with an Internal Pull Up: The PinDriver
type contains a set_pull
method that allows us to select the type of pull from the Pull
enum. In this case we need an internal pull-up leading to the following line:
button.set_pull(Pull::Up).unwrap();
4οΈβ£ Configure Button Pin Interrupt Type: Now we need to Configure the button pin to detect interrupts. For that, we use the set_interrupt_type
method from PinDriver
. We have several options to choose from in the InterruptType
enum. For this application's purpose, I am going to use positive edge detection. This means that when the pin sees a transition from low to high on the pin, it will fire an interrupt. Here's the configuration code:
button.set_interrupt_type(InterruptType::PosEdge).unwrap();
4οΈβ£ Attach the ISR to the Interrupt: Now that all is configured at a pin level, we still need to inform the controller where to go when the interrupt event happens. We are going to create a function called gpio_int_callback
for that which we'll show the implementation for shortly. As such, to attach gpio_int_callback
to the interrupt button we use the subscribe
method from PinDriver
. Note that subscribe
is unsafe
so it needs to be wrapped in an unsafe
block. However, this doesn't mean that subscribing to an ISR is actually unsafe
, it's only that the compiler cannot guarantee what's happening in the underlying code. Moving on, we subscribe
to the ISR as follows:
unsafe { button.subscribe(gpio_int_callback).unwrap() }
5οΈβ£ Enable the Interrupt: After all the configuration has been done, believe it or not, if the button is pressed an event won't be detected. Why? because the button interrupt is not switched on (or enabled) yet. Is only configured. Sort of like buying a car with a ton of bells and whistles, Unless you turn it on, technically many of those features won't work. Again, from PinDriver
there is the enable_interrupt
method that would allow us to enable the button interrupt:
button.enable_interrupt().unwrap();
π± Application Code
βΈοΈ The Interrupt Service Routine
Recall earlier that we attached (subscribed to) an ISR called gpio_int_callback
. As such, we still need to define what happens inside the ISR when an interrupt happens. The ISR is defined exactly in the same style as a function. Also, in gpio_int_callback
all we need to do is set the FLAG
variable to true
marking the occurrence of the interrupt. Since FLAG
is an AtomicBool
we can use the store
method for that:
fn gpio_int_callback() {
// Assert FLAG indicating a press button happened
FLAG.store(true, Ordering::Relaxed);
}
Note that in AtomicBool
there are store()
and load()
methods that we are using. Both require an Ordering
enum argument to be specified. The Ordering
enum refers to the way atomic operations synchronize memory which I chose Relaxed
. For more details, one can refer to the full list of options here. There is even a more detailed explanation of ordering here.
π The Main Loop
In the main loop, we are going to have to keep checking FLAG
using the load
method. If FLAG
gets asserted, then we know that a press button happened. Since we are going to track how many times the press occurred, then we need some variable to do that which I created count
for that. As such, if FLAG
is asserted, we need reset it first to allow consecutive interrupts to happen, increment count
, then finally print count
to the console. Here's the full code:
// Set up a variable that keeps track of press button count
let mut count = 0_u32;
loop {
// Check if global flag is asserted
if FLAG.load(Ordering::Relaxed) {
// Reset global flag
FLAG.store(false, Ordering::Relaxed);
// Update Press count and print
count = count.wrapping_add(1);
println!("Press Count {}", count);
}
}
π±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 apollolabs ESP32C3 git repo. Also, the Wokwi project can be accessed here.
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use esp_idf_hal::gpio::*;
use esp_idf_hal::peripherals::Peripherals;
use esp_idf_sys::{self as _};
static FLAG: AtomicBool = AtomicBool::new(false);
fn gpio_int_callback() {
// Assert FLAG indicating a press button happened
FLAG.store(true, Ordering::Relaxed);
}
fn main() -> ! {
// It is necessary to call this function once. Otherwise some patches to the runtime
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
esp_idf_sys::link_patches();
// Take Peripherals
let dp = Peripherals::take().unwrap();
// Configure button pin as input
let mut button = PinDriver::input(dp.pins.gpio0).unwrap();
// Configure button pin with internal pull up
button.set_pull(Pull::Up).unwrap();
// Configure button pin to detect interrupts on a positive edge
button.set_interrupt_type(InterruptType::PosEdge).unwrap();
// Attach the ISR to the button interrupt
unsafe { button.subscribe(gpio_int_callback).unwrap() }
// Enable interrupts
button.enable_interrupt().unwrap();
// Set up a variable that keeps track of press button count
let mut count = 0_u32;
loop {
// Check if global flag is asserted
if FLAG.load(Ordering::Relaxed) {
// Reset global flag
FLAG.store(false, Ordering::Relaxed);
// Update Press count and print
count = count.wrapping_add(1);
println!("Press Count {}", count);
}
}
}
Conclusion
In this post, a GPIO interrupt application was created by configuring a GPIO pin to detect button press events. This was using the GPIO peripheral for the ESP32C3 and the esp-idf-hal
. Have any questions? Share your thoughts in the comments below π.
Top comments (3)
I want to write some data to nvs periodically in main loop and clear them when button is pressed.but how can I sharing the nvs object to interrupt handler?
Sharing objects with interrupt handlers might be cumbersome in Rust due to safety reasons. You'd need to wrap the object in a safe abstraction that allows access to a critical section. The following post might help give some insight:
dev.to/apollolabsbin/stm32f4-embed...
Although for the STM32 its similar context.
What might be a simpler approach is to use a
'static
bool variable with the code in this post. The variable would be written by the button event ISR and only read by the main function. If it's true you then clear the nvs from the main. You'd still need a safe abstraction but it's less verbose.There are also crates with synchronization primitives that provide safe abstractions. Those could be an option. Something like using a
Mutex
.thanks for your help