Exercise A: Interrupt-Driven Button
~20 min
Starting Point
Open the examples/interrupt-button/ project in the workshop repo. This is a working GPIO interrupt example that toggles an LED when a button is pressed.
Read through src/main.rs carefully. This is the most complex code pattern in the workshop. Map it to what you learned in the slides:
- Shared state —
static BUTTON: Mutex<RefCell<Option<Input>>> - Moving the peripheral —
BUTTON.borrow_ref_mut(cs).replace(button) - The handler —
#[handler] fn gpio_handler() - Clearing the interrupt —
button.clear_interrupt()
Your Task
Step 1: Adapt and Run
Update the pin assignments for your uFerris board and flash:
cd examples/interrupt-button
cargo build --release
espflash flash target/riscv32imc-unknown-none-elf/release/interrupt-button --monitor
Press the button. The LED should toggle.
Step 2: Explore Trigger Configurations
The example uses one specific edge trigger. Time to explore what else exists.
Look up the `Event` enum in esp-hal's GPIO module. What variants are available?
Try changing the trigger:
-
Switch from
FallingEdgetoRisingEdge— what changes? Does the LED toggle on press or release now? -
Try
AnyEdge— the LED should toggle on both press and release. What happens? -
Think about it: which trigger would you use for:
- A light switch (toggle once per press)?
- A camera shutter (action on press, not release)?
- Measuring how long a button is held?
Step 3: Flag-Based Approach
The example toggles the LED inside the interrupt handler. But best practice says: keep handlers short, do work in the main loop.
Refactor to use a flag:
#![allow(unused)] fn main() { use core::sync::atomic::{AtomicBool, Ordering}; static BUTTON_PRESSED: AtomicBool = AtomicBool::new(false); }
- In the handler: set the flag to
true(and clear the interrupt) - In the main loop: check the flag, toggle the LED, reset the flag
Look up `AtomicBool` in the Rust standard library docs. What methods does it provide? Try `store()`, `load()`, and `swap()`.
Step 4: Compare
Think about the difference between your Part 2 polling code and this interrupt code:
- What can the main loop do now that it couldn't before?
- When would you choose polling over interrupts?
- When would you choose interrupts over polling?
```rust
#[handler]
fn gpio_handler() {
critical_section::with(|cs| {
let mut button = BUTTON.borrow_ref_mut(cs);
if let Some(button) = button.as_mut() {
button.clear_interrupt();
}
});
BUTTON_PRESSED.store(true, Ordering::Relaxed);
}
// In main loop:
loop {
if BUTTON_PRESSED.swap(false, Ordering::Relaxed) {
led.toggle();
}
// CPU could do other work here, or sleep
}