diff --git a/advanced/button-interrupt/examples/solution.rs b/advanced/button-interrupt/examples/solution.rs index aa428d07..5de4ae4d 100644 --- a/advanced/button-interrupt/examples/solution.rs +++ b/advanced/button-interrupt/examples/solution.rs @@ -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 = 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!"); } } diff --git a/advanced/button-interrupt/examples/solution_led.rs b/advanced/button-interrupt/examples/solution_led.rs index 9680d9a3..f6f5f351 100644 --- a/advanced/button-interrupt/examples/solution_led.rs +++ b/advanced/button-interrupt/examples/solution_led.rs @@ -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 = 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); } } diff --git a/advanced/button-interrupt/src/main.rs b/advanced/button-interrupt/src/main.rs index 5c828f27..07a8d7c8 100644 --- a/advanced/button-interrupt/src/main.rs +++ b/advanced/button-interrupt/src/main.rs @@ -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 = 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 } } } diff --git a/book/src/04_4_0_interrupts.md b/book/src/04_4_0_interrupts.md index 424d64cd..1d39f3b0 100644 --- a/book/src/04_4_0_interrupts.md +++ b/book/src/04_4_0_interrupts.md @@ -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`, 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. - - - -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 diff --git a/book/src/04_4_1_interrupts.md b/book/src/04_4_1_interrupts.md index d413cf13..19af03c1 100644 --- a/book/src/04_4_1_interrupts.md +++ b/book/src/04_4_1_interrupts.md @@ -1,7 +1,6 @@ # Building the Interrupt Handler The goal of this exercise is to handle the interrupt that fires if the `BOOT` button is pushed. -This exercise involves working with C bindings to the ESP-IDF and other unsafe operations, as well as non-typical Rust documentation. In a first step we will go line by line to build this interrupt handler. You can find a skeleton code for this exercise in `advanced/button-interrupt/src/main.rs`. @@ -12,77 +11,25 @@ cargo run --example solution ``` ## ✅ Tasks -1. Configure the [BOOT button](https://github.com/esp-rs/esp-rust-board#ios) (GPIO9) with a C struct [`gpio_config_t`](https://esp-rs.github.io/esp-idf-sys/esp_idf_sys/struct.gpio_config_t.html) and the following settings: +1. Configure the [BOOT button](https://github.com/esp-rs/esp-rust-board#ios) (GPIO9), using the `PinDriver` struct with the following settings: - Input mode - Pull up - Interrupt on positive edge - -The struct has the following fields: - - * `pin_bit_mask`: Represents the Pin number, the value 1 shifted by the number of the pin. - * `mode`: Sets the mode of the pin, it can have the following settings: - * `gpio_mode_t_GPIO_MODE_INPUT` - * `gpio_mode_t_GPIO_MODE_OUTPUT` - * `gpio_mode_t_GPIO_MODE_DISABLE` // Disable GPIO - * `gpio_mode_t_GPIO_MODE_OUTPUT_OD` // Open drain output - * `gpio_mode_t_GPIO_MODE_INPUT_OUTPUT` // Input and output - * `gpio_mode_t_GPIO_MODE_INPUT_OUTPUT_OD` // Open drain input and output - * `pull_up_en`: `true.into()`, if the GPIO is pulled up, - * `pull_down_en`: `true.into()`, if the GPIO is pulled down, - * `intr_type`: Sets the interrupt type, it can have the following settings: - * `gpio_int_type_t_GPIO_INTR_ANYEDGE` // Interrupt at any edge - * `gpio_int_type_t_GPIO_INTR_DISABLE` // Interrupt disabled - * `gpio_int_type_t_GPIO_INTR_NEGEDGE` // Interrupt at negative edge - * `gpio_int_type_t_GPIO_INTR_POSEDGE` // Interrupt at positive edge - -They are constants with numbers representing the bit that must be set in the corresponding register. - -2. Write the configuration into the register with [`unsafe extern "C" fn gpio_config`](https://esp-rs.github.io/esp-idf-sys/esp_idf_sys/fn.gpio_config.html). This needs to happen in the unsafe block. To make these FFI calls, we can use the macro `esp!($Cfunktion)`. - -3. Install a generic GPIO interrupt handler with [`unsafe extern "C" fn gpio_install_isr_service`](https://esp-rs.github.io/esp-idf-sys/esp_idf_sys/fn.gpio_install_isr_service.html). This function takes `ESP_INTR_FLAG_IRAM` as argument. - -4. Create a `static mut` that holds the queue handle we are going to get from `xQueueGenericCreate`. This is a number that uniquely identifies one particular queue, as opposed to any of the other queues in our program. The queue storage itself if managed by the Operating System. - - ```rust - static mut EVENT_QUEUE: Option = None; - ``` - -5. Create the event queue using [`pub unsafe extern "C" fn xQueueGenericCreate`](https://esp-rs.github.io/esp-idf-sys/esp_idf_sys/fn.xQueueGenericCreate.html). This lets us safely pass events from an interrupt routine to our main thread. - - ```rust - EVENT_QUEUE = Some(xQueueGenericCreate(QUEUE_SIZE, ITEM_SIZE, QUEUE_TYPE_BASE)); - ``` - -6. Add a function which that will be called whenever there is a GPIO interrupt on our button pin. We put this function in a special block of RAM (`iram0`), so it will still be available even if the external flash is busy doing something else (like filesystem work). The function needs to get the queue handle from `EVENT_QUEUE` and call the `xQueueGiveFromISR` function with a `std::ptr::null_mut()` - the objects in our queue are of size zero, so we don't actually need a 'thing' to put on the queue. Instead, the act of pushing a 'nothing' is enough to wake up the other end! - - ```rust - #[link_section = ".iram0.text"] - unsafe extern "C" fn button_interrupt(_: *mut c_void) { - xQueueGiveFromISR(EVENT_QUEUE.unwrap(), std::ptr::null_mut()); - } - ``` - -If the interrupt fires, an event is added to the queue. - -7. Pass the function we just wrote to the generic GPIO interrupt handler we registered earlier, along with the number of the GPIO pin that should cause this function to be executed. - - ```rust - esp!(gpio_isr_handler_add( - GPIO_NUM, - Some(button_interrupt), - std::ptr::null_mut() - ))?; - ``` - -8. Inside a loop, wait until the queue has an item in it. That is, until the `button_interrupt` function puts something in the queue. - - ```rust - let res = xQueueReceive(EVENT_QUEUE.unwrap(), ptr::null_mut(), QUEUE_WAIT_TICKS); - ``` - -9. Handle the value of `res`, so that "Button pushed!" is logged, if the button is pushed. - -10. Run the program and push the `BOOT` button, so see how it works! +2. Instantiate a new notification and notifier + - See `hal::task::notification` documentation +3. In an `unsafe` block, create a subscription and its callback function. + - See `PinDriver::subscribe` and `task::notify_and_yield` + - The reasons for being `unsafe` are: + - The callback function will run in the [ISR (Interrupt Service Routine)](https://en.wikipedia.org/wiki/Interrupt_handler), so we should avoid calling any blocking functions on it, this includes STD, `libc` or FreeRTOS APIs (except for a few allowed ones). + - Callback closure is capturing its environment and you can use static variables inserted onto it. Captured variables need to outlive the subscription. You can also, use non-static variables, but that requires extra caution, see `esp_idf_hal::gpio::PinDriver::subscribe_nonstatic` documentation for more details. +4. In the loop, enable the interrupt, and wait for the notification + - The interruption should be enabled after each received notification, from a non-ISR context + - `esp_idf_svc::hal::delay::BLOCK` can be used for waiting +5. Run the program, push the `BOOT` button, and see how it works! + +🔎 In this exercise we are using notifications, which only give the latest value, so if the interrupt is triggered +multiple times before the value of the notification is read, you will only be able to read the latest one. Queues, +on the other hand, allow receiving multiple values. See `esp_idf_hal::task::queue::Queue` for more details. ## Simulation diff --git a/book/src/04_4_2_interrupts.md b/book/src/04_4_2_interrupts.md index 7331a474..a37cd4e7 100644 --- a/book/src/04_4_2_interrupts.md +++ b/book/src/04_4_2_interrupts.md @@ -16,5 +16,5 @@ cargo run --example solution_led * The LED's part number is WS2812RMT. * It's a programmable RGB LED. This means there aren't single pins to set for red, green and blue, but we need to instantiate it to be able to send `RGB8` type values to it with a method. * The board has a hardware random number generator. It can be called with `esp_random()`. -* Calling functions from the `esp-idf-sys` is unsafe in Rust terms and requires an `unsafe()` block. You can assume that these functions are safe to use, so no other measures are required. +* Calling functions from the `esp-idf-svc::sys` is unsafe in Rust terms and requires an `unsafe()` block. You can assume that these functions are safe to use, so no other measures are required. diff --git a/book/src/04_4_3_interrupts.md b/book/src/04_4_3_interrupts.md index 367e8787..e7c30f46 100644 --- a/book/src/04_4_3_interrupts.md +++ b/book/src/04_4_3_interrupts.md @@ -6,13 +6,9 @@ led.set_pixel(RGB8::new(20, 0, 20)).unwrap(); // Remove this line after you tried it once ``` -2. Light up the LED only when the button is pressed. You can do this for now by exchanging the print statement. +2. Light up the LED only when the button is pressed. You can do this for now by adding the following line after the button pressed message: ```rust - 1 => { - led.set_pixel(arbitrary_color)?; - - }, - _ => {}, + led.set_pixel(arbitrary_color)?; ``` 3. Create random RGB values by calling `esp_random()`. * This function is `unsafe`. @@ -37,18 +33,13 @@ ```rust // ... - - unsafe { - // ... - match res { - 1 => { - // Generates random rgb values - random_light(&mut led); - - }, - _ => {}, - }; - } + loop { + // 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!"); + // Generates random rgb values and sets them in the led. + random_light(&mut led); } // ... fn random_light(led: &mut WS2812RMT) {