Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 22 additions & 56 deletions advanced/button-interrupt/examples/solution.rs
Original file line number Diff line number Diff line change
@@ -1,70 +1,36 @@
// Reference: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos.html

use anyhow::Result;
use esp_idf_svc::sys::{
esp, gpio_config, gpio_config_t, gpio_install_isr_service, gpio_int_type_t_GPIO_INTR_POSEDGE,
gpio_isr_handler_add, gpio_mode_t_GPIO_MODE_INPUT, xQueueGenericCreate, xQueueGiveFromISR,
xQueueReceive, QueueHandle_t, ESP_INTR_FLAG_IRAM,
use esp_idf_svc::hal::{
gpio::{InterruptType, PinDriver, Pull},
peripherals::Peripherals,
task::notification::Notification,
};
use std::ptr;

// This `static mut` holds the queue handle we are going to get from `xQueueGenericCreate`.
// This is unsafe, but we are careful not to enable our GPIO interrupt handler until after this value has been initialised, and then never modify it again
static mut EVENT_QUEUE: Option<QueueHandle_t> = None;

#[link_section = ".iram0.text"]
unsafe extern "C" fn button_interrupt(_: *mut core::ffi::c_void) {
xQueueGiveFromISR(EVENT_QUEUE.unwrap(), std::ptr::null_mut());
}
use std::num::NonZeroU32;

fn main() -> Result<()> {
const GPIO_NUM: i32 = 9;
esp_idf_svc::sys::link_patches();

let peripherals = Peripherals::take()?;

// Configures the button
let io_conf = gpio_config_t {
pin_bit_mask: 1 << GPIO_NUM,
mode: gpio_mode_t_GPIO_MODE_INPUT,
pull_up_en: true.into(),
pull_down_en: false.into(),
intr_type: gpio_int_type_t_GPIO_INTR_POSEDGE, // Positive edge trigger = button down
};
let mut button = PinDriver::input(peripherals.pins.gpio9)?;
button.set_pull(Pull::Up)?;
button.set_interrupt_type(InterruptType::PosEdge)?;

// Queue configurations
const QUEUE_TYPE_BASE: u8 = 0;
const ITEM_SIZE: u32 = 0; // We're not posting any actual data, just notifying
const QUEUE_SIZE: u32 = 1;
// Configures the notification
let notification = Notification::new();
let notifier = notification.notifier();

// Safety: make sure the `Notification` object is not dropped while the subscription is active
unsafe {
// Writes the button configuration to the registers
esp!(gpio_config(&io_conf))?;

// Installs the generic GPIO interrupt handler
esp!(gpio_install_isr_service(ESP_INTR_FLAG_IRAM as i32))?;

// Instantiates the event queue
EVENT_QUEUE = Some(xQueueGenericCreate(QUEUE_SIZE, ITEM_SIZE, QUEUE_TYPE_BASE));

// Registers our function with the generic GPIO interrupt handler we installed earlier.
esp!(gpio_isr_handler_add(
GPIO_NUM,
Some(button_interrupt),
std::ptr::null_mut()
))?;
button.subscribe(move || {
notifier.notify_and_yield(NonZeroU32::new(1).unwrap());
})?;
}

// Reads the queue in a loop.
loop {
unsafe {
// Maximum delay
const QUEUE_WAIT_TICKS: u32 = 1000;

// Reads the event item out of the queue
let res = xQueueReceive(EVENT_QUEUE.unwrap(), ptr::null_mut(), QUEUE_WAIT_TICKS);

// If the event has the value 0, nothing happens. if it has a different value, the button was pressed.
if res == 1 {
println!("Button pressed!")
}
}
// enable_interrupt should also be called after each received notification from non-ISR context
button.enable_interrupt()?;
notification.wait(esp_idf_svc::hal::delay::BLOCK);
println!("Button pressed!");
}
}
87 changes: 25 additions & 62 deletions advanced/button-interrupt/examples/solution_led.rs
Original file line number Diff line number Diff line change
@@ -1,82 +1,45 @@
// Reference: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos.html

use anyhow::Result;
use esp_idf_svc::{
hal::prelude::Peripherals,
sys::{
esp, esp_random, gpio_config, gpio_config_t, gpio_install_isr_service,
gpio_int_type_t_GPIO_INTR_POSEDGE, gpio_isr_handler_add, gpio_mode_t_GPIO_MODE_INPUT,
xQueueGenericCreate, xQueueGiveFromISR, xQueueReceive, QueueHandle_t, ESP_INTR_FLAG_IRAM,
hal::{
gpio::{InterruptType, PinDriver, Pull},
peripherals::Peripherals,
task::notification::Notification,
},
sys::esp_random,
};
use rgb_led::{RGB8, WS2812RMT};
use std::ptr;

// This `static mut` holds the queue handle we are going to get from `xQueueGenericCreate`.
// This is unsafe, but we are careful not to enable our GPIO interrupt handler until after this value has been initialised, and then never modify it again
static mut EVENT_QUEUE: Option<QueueHandle_t> = None;

#[link_section = ".iram0.text"]
unsafe extern "C" fn button_interrupt(_: *mut core::ffi::c_void) {
xQueueGiveFromISR(EVENT_QUEUE.unwrap(), std::ptr::null_mut());
}
use std::num::NonZeroU32;

fn main() -> Result<()> {
let peripherals = Peripherals::take().unwrap();
esp_idf_svc::sys::link_patches();

let peripherals = Peripherals::take()?;
let mut led = WS2812RMT::new(peripherals.pins.gpio2, peripherals.rmt.channel0)?;
const GPIO_NUM: i32 = 9;

// Configures the button
let io_conf = gpio_config_t {
pin_bit_mask: 1 << GPIO_NUM,
mode: gpio_mode_t_GPIO_MODE_INPUT,
pull_up_en: true.into(),
pull_down_en: false.into(),
intr_type: gpio_int_type_t_GPIO_INTR_POSEDGE, // Positive edge trigger = button down
};
let mut button = PinDriver::input(peripherals.pins.gpio9)?;
button.set_pull(Pull::Up)?;
button.set_interrupt_type(InterruptType::PosEdge)?;

// Queue configurations
const QUEUE_TYPE_BASE: u8 = 0;
const ITEM_SIZE: u32 = 0; // We're not posting any actual data, just notifying
const QUEUE_SIZE: u32 = 1;
// Configures the notification
let notification = Notification::new();
let notifier = notification.notifier();

// Subscribe and create the callback
// Safety: make sure the `Notification` object is not dropped while the subscription is active
unsafe {
// Writes the button configuration to the registers
esp!(gpio_config(&io_conf))?;

// Installs the generic GPIO interrupt handler
esp!(gpio_install_isr_service(ESP_INTR_FLAG_IRAM as i32))?;

// Instantiates the event queue
EVENT_QUEUE = Some(xQueueGenericCreate(QUEUE_SIZE, ITEM_SIZE, QUEUE_TYPE_BASE));

// Registers our function with the generic GPIO interrupt handler we installed earlier.
esp!(gpio_isr_handler_add(
GPIO_NUM,
Some(button_interrupt),
std::ptr::null_mut()
))?;
button.subscribe(move || {
notifier.notify_and_yield(NonZeroU32::new(1).unwrap());
})?;
}

// Reads the queue in a loop.
loop {
unsafe {
// Maximum delay
const QUEUE_WAIT_TICKS: u32 = 1000;

// Reads the event item out of the queue
let res = xQueueReceive(EVENT_QUEUE.unwrap(), ptr::null_mut(), QUEUE_WAIT_TICKS);

// If the event has the value 0, nothing happens. if it has a different value, the button was pressed.
// If the button was pressed, a function that changes the state of the LED is called.

if res == 1 {
println!("Button pressed!");
// Generates random rgb values and sets them in the led.
random_light(&mut led);
};
}
// Enable interrupt and wait for new notificaton
button.enable_interrupt()?;
notification.wait(esp_idf_svc::hal::delay::BLOCK);
println!("Button pressed!");
// Generates random rgb values and sets them in the led.
random_light(&mut led);
}
}

Expand Down
64 changes: 18 additions & 46 deletions advanced/button-interrupt/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,62 +1,34 @@
// Reference: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos.html
use anyhow::Result;
use esp_idf_svc::sys::{
esp, esp_random, gpio_config, gpio_config_t, gpio_install_isr_service,
gpio_int_type_t_GPIO_INTR_POSEDGE, gpio_isr_handler_add, gpio_mode_t_GPIO_MODE_INPUT,
xQueueGenericCreate, xQueueGiveFromISR, xQueueReceive, QueueHandle_t, ESP_INTR_FLAG_IRAM,
use esp_idf_svc::{
hal::{
gpio::{InterruptType, PinDriver, Pull},
peripherals::Peripherals,
task::notification::Notification,
},
sys::esp_random,
};
use std::ptr;

// These imports are needed for part 2.
use rgb_led::{RGB8, WS2812RMT};

// 4. Create a `static mut` that holds the queue handle.
static mut EVENT_QUEUE: Option<QueueHandle_t> = None;

// 6. Define what the interrupt handler does, once the button is pushed. Button_interrupt sends a message into the queue.
#[link_section = ".iram0.text"]
unsafe extern "C" fn button_interrupt(_: *mut core::ffi::c_void) {
xQueueGiveFromISR(EVENT_QUEUE.unwrap(), std::ptr::null_mut());
}
use std::num::NonZeroU32;

fn main() -> Result<()> {
const GPIO_NUM: i32 = 9;

// 1. Add GPIO configuration C struct
// let io_conf = gpio_config_t {
// ...
// };
esp_idf_svc::sys::link_patches();

// Queue configurations
const QUEUE_TYPE_BASE: u8 = 0;
const ITEM_SIZE: u32 = 0;
const QUEUE_SIZE: u32 = 1;
let peripherals = Peripherals::take()?;

unsafe {
// 2. Write the GPIO configuration into the register
// esp!(...)?;

// 3. Install the global GPIO interrupt handler
// esp!(...)?;
// 1. Configure the button using PinDriver
// let mut button = PinDriver...

// 4. Create an event queue
// EVENT_QUEUE = Some(...);
// 2. Instantiate a new notification and notifier

// 5. Add the button GPIO and the function to the interrupt handler
// esp!(...)?;
unsafe {
// 3. Create a subscription and its callback function that notifies and yields.
}

// The loop in main waits until it gets a message through the rx ("receiver") part of the channel
loop {
unsafe {
// Maximum delay
const QUEUE_WAIT_TICKS: u32 = 1000;;

// 6. Receive the event from the queue.
// let res = ...;

// 7. Handle the value of res.
// ...
// 4. Enable the interrupt for the button
// 5. Wait for notification using `esp_idf_svc::hal::delay::BLOCK`
// 6. Print a "button pressed" message
}
}
}
29 changes: 3 additions & 26 deletions book/src/04_4_0_interrupts.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,8 @@ An interrupt is a request for the processor to interrupt currently executing so

The fact that interrupt handlers can be called at any time provides a challenge in embedded Rust: It requires the existence of statically allocated mutable memory that both the interrupt handler and the main code can refer to, and it also requires that this memory is always accessible.

## Challenges

### Flash Memory

Flash memory doesn't fulfill this requirement as it is out of action for example during write operations. Interrupts that occur during this time will go unnoticed. In our example, this would result in no reaction when the button is pushed. We solve this by moving the interrupt handler into RAM.
### Statically Mutable Memory

In Rust, such memory can be declared by defining a `static mut`. But reading and writing to such variables is always unsafe, as without precautions race conditions can be triggered.

How do we handle this problem?

In our example, the ESP-IDF framework provides a `Queue` type that handles the shared-mutable state for us. We simply get a `QueueHandle` which uniquely identifies the particular `Queue` being used. However, the main thread is given this `QueueHandle_t` at run-time, so we still need a small amount of shared-mutable state to share the `QueueHandle_t` with the interrupt routine. We use an `Option<QueueHandle_t>`, which we statically initialize to `None`, and later replace with `Some(queue_handle)` when the queue has been created by ESP-IDF.

In the interrupt routine, Rust forces us to handle the case where the `static mut` is still `None`. If this happens, we can either return early, or we can `unwrap()` the value, which will exit the program with an error if the value wasn't previously set to `Some(queue_handle)`.

There is still a risk that `main()` might be in the processing of changing the value of the variable (i.e. changing the `QueueHandle_t` value) just as the interrupt routine fires, leaving it in an inconsistent or invalid state. We mitigate this by making sure we only set the value once, and we do so before the interrupt is enabled. The compiler can't check that this is safe, so we must use the `unsafe` keyword when we read or write the value.

<!-- An alternative to the `static mut` variable is to convert the `QueueHandle_t` to an integer, and store it in an `AtomicU32` or similar. These atomic types guarantee they can never be read in an intermediate or invalid state. However, they require special hardware support which is not available on the ESP32-C3. You would also still need to distinguish between a valid `QueueHandle_t` and some value that indicates the queue has not yet been created (perhaps `0xFFFF_FFFF`).
Yet another option is to use a special data structure which disables interrupts automatically when the value is being access. This guarantees that no code can interrupt you when reading or writing the value. This does however increase interrupt latency and in this case, because the `QueueHandle_t` is only written once, this is not necessary. -->

Read more about this in the [Embedded Rust Book](https://docs.rust-embedded.org/book/concurrency/index.html)

## `unsafe {}` Blocks:

This code contains a lot of `unsafe {}` blocks. As a general rule, `unsafe` doesn't mean that the contained code isn't memory safe. It means, that Rust can't make safety guarantees in this place and that it is the responsibility of the programmer to ensure memory safety. For example, Calling C Bindings is per se unsafe, as Rust can't make any safety guarantees for the underlying C Code.

## `unsafe {}` Blocks

This code contains a lot of [`unsafe {}` blocks][rust-unsafe]. As a general rule, `unsafe` doesn't mean that the contained code isn't memory safe. It means, that Rust can't make safety guarantees in this place and that it is the responsibility of the programmer to ensure memory safety. For example, Calling C Bindings is per se unsafe, as Rust can't make any safety guarantees for the underlying C Code.

[rust-unsafe]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
Loading