Interrupt Essentials

~10 min slides

Polling vs. Interrupts

POLLING:                          INTERRUPTS:
┌──────────────┐                  ┌──────────────┐
│ loop {       │                  │ loop {       │
│   if pressed │ ← CPU busy      │   // sleep   │ ← CPU idle
│     do_thing │   checking       │   // or do   │   until event
│   }          │   constantly     │   // other   │
│ }            │                  │   // work    │
└──────────────┘                  └──────┬───────┘
                                         │ INTERRUPT!
                                  ┌──────▼───────┐
                                  │ handler() {  │ ← CPU wakes up
                                  │   do_thing   │   handles event
                                  │ }            │   goes back
                                  └──────────────┘

GPIO Interrupt Triggers

When should the interrupt fire?

TriggerWhen It Fires
Event::FallingEdgePin goes from HIGH → LOW (button press with pull-up)
Event::RisingEdgePin goes from LOW → HIGH (button release with pull-up)
Event::AnyEdgeAny transition — both press and release

The Interrupt Pattern in Rust

Interrupts introduce a challenge: the interrupt handler and your main code need to share state. In Rust, this means dealing with Send, Sync, and interior mutability.

The Standard Pattern

#![allow(unused)]
fn main() {
use core::cell::RefCell;
use critical_section::Mutex;

// 1. Shared state: wrap in Mutex<RefCell<Option<T>>>
static BUTTON: Mutex<RefCell<Option<Input>>> = Mutex::new(RefCell::new(None));

// 2. In main: move the peripheral into the shared state
critical_section::with(|cs| {
    BUTTON.borrow_ref_mut(cs).replace(button);
});

// 3. The interrupt handler
#[handler]
fn gpio_handler() {
    critical_section::with(|cs| {
        let mut button = BUTTON.borrow_ref_mut(cs);
        if let Some(button) = button.as_mut() {
            // Handle the interrupt
            button.clear_interrupt();
        }
    });
}
}

Key Pieces

PiecePurpose
Mutex<RefCell<Option<T>>>Safe shared access between main code and interrupt handler
critical_section::withTemporarily disables interrupts to safely access shared state
#[handler]Marks a function as an interrupt handler (esp-hal attribute)
clear_interrupt()Must be called — tells the hardware you've handled the event
In esp-hal 1.0, interrupts are behind the `unstable` feature flag. Make sure your `Cargo.toml` includes:
```toml
[dependencies]
esp-hal = { version = "1.0", features = ["unstable"] }

```admonish key-concept title="Keep handlers short"
Interrupt handlers should do as little as possible — set a flag, clear the interrupt, return. Do the heavy work in your main loop. This prevents blocking other interrupts and keeps the system responsive.