Skip to content

Timers, PWM, and Interrupts

Timers, PWM, and Interrupts hero image
Modified:
Published:

Interrupts are where embedded Rust diverges most sharply from C. In C, you write an ISR, declare some globals as volatile, and hope you got the synchronization right. Rust refuses to compile code with unsynchronized global mutable state. This forces you into patterns (critical sections, Mutex wrappers, atomic types) that are safer and, once you learn them, just as efficient. In this lesson, you will configure the RP2040’s PWM hardware to drive a servo, read a potentiometer through the ADC, handle timer interrupts for buzzer tone generation, and wire it all together into a responsive servo controller with audio feedback. #RP2040 #PWM #Interrupts

What We Are Building

Potentiometer-Controlled Servo with Buzzer Feedback

A potentiometer on ADC0 (GP26) sets the angle of an SG90 servo motor driven by PWM on GP0. A piezo buzzer on GP18 plays a short tone that changes pitch proportionally to the servo angle: low angle gives a low pitch, high angle gives a high pitch. A timer interrupt generates the buzzer frequency. The main loop reads the ADC, updates the servo PWM duty cycle, and communicates the desired buzzer frequency to the interrupt handler through a Mutex-protected static variable.

Project specifications:

ParameterValue
BoardRaspberry Pi Pico (RP2040)
Servo pinGP0 (PWM0A, slice 0, channel A)
Servo signal50 Hz, 0.5 ms to 2.5 ms pulse width
PotentiometerGP26 (ADC0), 10K linear
Buzzer pinGP18 (PWM1A, slice 1, channel A)
Buzzer frequency range200 Hz to 2000 Hz
Timer interruptTIMER_IRQ_0, periodic at variable rate
ADC resolution12-bit (0 to 4095)

Bill of Materials

ComponentQuantityNotes
Raspberry Pi Pico1From previous lessons
Debug probe1From previous lessons
SG90 micro servo14.8 to 6V, standard hobby servo
Potentiometer (10K linear)1Rotary or slider
Piezo buzzer (passive)1Must be passive (not active/self-oscillating)
Breadboard + jumper wires1 setFrom previous lessons
External 5V supply (optional)1For servo if USB power is insufficient

Wiring Table

Pico PinConnectionNotes
GP0Servo signal (orange/yellow wire)PWM output for servo
GP26 (ADC0)Potentiometer wiper (center pin)Analog input
GP18Piezo buzzer (positive lead)PWM output for tone
3V3(OUT)Potentiometer one endADC reference voltage
VSYS (5V)Servo power (red wire)Or use external 5V
GNDPot other end, servo GND, buzzer GNDCommon ground

RP2040 PWM Architecture



The RP2040 has 8 PWM slices, each with two output channels (A and B). Each slice has a 16-bit counter, a configurable divider (integer + 4-bit fractional), and independent duty cycle registers for each channel. Both channels in a slice share the same frequency (determined by the counter wrap value) but can have different duty cycles.

PWM Slice Mapping

Every GPIO pin on the RP2040 can be assigned to a specific PWM slice and channel. The mapping follows a fixed pattern:

GPIOPWM SliceChannel
GP0, GP16Slice 0A
GP1, GP17Slice 0B
GP2, GP18Slice 1A
GP3, GP19Slice 1B
GP4, GP20Slice 2A
GP5, GP21Slice 2B
GP6, GP22Slice 3A
GP7, GP23Slice 3B
GP8, GP24Slice 4A
GP9, GP25Slice 4B
GP10, GP26Slice 5A
GP11, GP27Slice 5B
GP12, GP28Slice 6A
GP13, GP29Slice 6B
GP14Slice 7A
GP15Slice 7B

For this project, GP0 uses slice 0 channel A (servo) and GP18 uses slice 1 channel A (buzzer).

PWM Frequency Calculation

The RP2040 system clock runs at 125 MHz. The PWM counter frequency is:

The PWM output frequency depends on the wrap (top) value:

For a 50 Hz servo signal with a 125 MHz system clock:

The duty cycle is set by the channel compare value (0 to wrap). For servo control, the pulse width maps to:

Servo AnglePulse WidthCompare Value (at 1 MHz counter)
0 degrees0.5 ms500
90 degrees1.5 ms1500
180 degrees2.5 ms2500

Servo PWM Configuration



use rp2040_hal::pwm::{FreeRunning, Pwm0, Slices};
/// Configure PWM slice 0, channel A (GP0) for 50 Hz servo output.
/// Returns the PWM slice so the caller retains ownership.
fn setup_servo_pwm(
pwm_slices: &mut Slices,
pins: &mut rp_pico::Pins, // not actually needed; see below
) -> /* returns configured slice */ {
// ... (full implementation in the complete project below)
}

The rp2040-hal provides typed access to PWM slices. Each slice is a separate type (Pwm0, Pwm1, etc.), and the channel configuration follows the same typestate pattern as GPIO: you call .into_mode::<FreeRunning>() to switch the slice into free-running mode, which consumes the idle-mode slice.

Setting the Duty Cycle for a Specific Angle

/// Map an angle (0 to 180) to a PWM duty cycle value.
/// Servo expects 0.5 ms (500) to 2.5 ms (2500) pulse within a 20 ms period.
fn angle_to_duty(angle: u16) -> u16 {
let angle = if angle > 180 { 180 } else { angle };
// Linear interpolation: 0 deg -> 500, 180 deg -> 2500
500 + ((angle as u32 * 2000) / 180) as u16
}

ADC: Reading the Potentiometer



The RP2040 has a 12-bit SAR ADC with 5 input channels: 4 external (ADC0 to ADC3 on GP26 to GP29) and 1 internal temperature sensor. The ADC runs at up to 500 ksps and produces values from 0 (0V) to 4095 (3.3V).

use rp2040_hal::adc::Adc;
/// Read ADC channel 0 (GP26) and return a 12-bit value (0 to 4095).
fn read_pot(adc: &mut Adc, adc_pin: &mut hal::adc::AdcPin<
hal::gpio::Pin<hal::gpio::bank0::Gpio26, hal::gpio::FunctionNull, hal::gpio::PullNone>,
>) -> u16 {
adc.read(adc_pin).unwrap()
}

Mapping ADC to Servo Angle

The potentiometer produces a 12-bit value. We map it to 0 through 180 degrees:

/// Map a 12-bit ADC value (0 to 4095) to a servo angle (0 to 180).
fn adc_to_angle(adc_value: u16) -> u16 {
((adc_value as u32 * 180) / 4095) as u16
}

Interrupts in Rust



This is where Rust embedded code differs most from C. In C, you declare an ISR function, use volatile globals to share data, and hope you got it right. Rust requires you to use safe patterns for sharing data between the main loop and interrupt handlers.

Why Rust Interrupts Are Different

In C, interrupt handlers access global mutable state freely:

// C: common ISR pattern
volatile uint32_t tone_half_period_us = 0;
volatile bool buzzer_state = false;
void TIMER_IRQ_0_Handler(void) {
timer_hw->intr = 1; // clear interrupt
buzzer_state = !buzzer_state;
gpio_put(18, buzzer_state);
// Schedule next interrupt
timer_hw->alarm[0] = timer_hw->timerawl + tone_half_period_us;
}

This works, but the compiler provides no help verifying correctness:

  • What if tone_half_period_us is written from the main loop while the ISR reads it?
  • What if the ISR reads a partially-updated 32-bit value on a 32-bit bus? (This specific case is atomic on Cortex-M, but the compiler does not know that.)
  • What if another developer adds a second variable that must be updated atomically with the first?

Rust refuses to compile this pattern. Global mutable state requires explicit synchronization.

The Rust ISR Pattern

use core::cell::RefCell;
use critical_section::Mutex;
use rp2040_hal::pac;
use rp2040_hal::pac::interrupt;
// Shared state: the timer alarm peripheral and buzzer configuration
// Wrapped in Mutex<RefCell<Option<T>>> because:
// - Mutex: requires critical section token for access (interrupt-safe)
// - RefCell: provides interior mutability (borrow checking at runtime)
// - Option: starts as None, set to Some() after peripheral init
static ALARM0: Mutex<RefCell<Option<pac::TIMER>>> = Mutex::new(RefCell::new(None));
static BUZZER_HALF_PERIOD: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
static BUZZER_PIN: Mutex<RefCell<Option<hal::gpio::Pin<
hal::gpio::bank0::Gpio18,
hal::gpio::FunctionSio<hal::gpio::SioOutput>,
hal::gpio::PullDown,
>>>> = Mutex::new(RefCell::new(None));
#[interrupt]
fn TIMER_IRQ_0() {
critical_section::with(|cs| {
// Access shared peripherals inside the critical section
if let Some(ref timer) = *ALARM0.borrow_ref(cs) {
// Clear the interrupt
timer.intr().write(|w| w.alarm_0().clear_bit_by_one());
// Read the desired half-period
let half_period = *BUZZER_HALF_PERIOD.borrow_ref(cs);
if half_period > 0 {
// Toggle the buzzer pin
if let Some(ref mut pin) = *BUZZER_PIN.borrow_ref_mut(cs) {
// Toggle using XOR on the GPIO output register
let current = pin.is_set_high().unwrap_or(false);
if current {
pin.set_low().unwrap();
} else {
pin.set_high().unwrap();
}
}
// Schedule next alarm
let next = timer.timerawl().read().bits().wrapping_add(half_period);
timer.alarm0().write(|w| unsafe { w.bits(next) });
}
}
});
}

Key Differences from C

// Global mutable state: no synchronization
volatile uint32_t half_period = 0;
volatile bool pin_state = false;
void TIMER_IRQ_0_Handler(void) {
// Clear interrupt flag
timer_hw->intr = 1;
// Toggle pin
pin_state = !pin_state;
gpio_put(18, pin_state);
// Schedule next
timer_hw->alarm[0] =
timer_hw->timerawl + half_period;
}
int main(void) {
// Set half_period from main loop
half_period = 500; // No synchronization
// What if ISR fires right now and reads
// a partially-updated value? On 32-bit
// Cortex-M this is actually atomic for
// 32-bit writes, but the compiler does
// not verify this.
}

Complete Project: Pot-Controlled Servo with Buzzer



Project Structure

  • Directoryrp2040-servo-buzzer/
    • Directory.cargo/
      • config.toml
    • Directorysrc/
      • main.rs
    • Cargo.toml
    • memory.x
    • build.rs
    • Embed.toml

Cargo.toml

[package]
name = "rp2040-servo-buzzer"
version = "0.1.0"
edition = "2021"
[dependencies]
rp2040-hal = { version = "0.10", features = ["rt", "critical-section-impl"] }
rp-pico = "0.9"
cortex-m = "0.7"
cortex-m-rt = "0.7"
critical-section = "1.2"
panic-halt = "1.0"
defmt = "0.3"
defmt-rtt = "0.4"
embedded-hal = "1.0"
embedded-hal-0-2 = { package = "embedded-hal", version = "0.2" }
fugit = "0.3"
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false
incremental = false
lto = "fat"
opt-level = "s"
overflow-checks = false

Use the same memory.x, build.rs, .cargo/config.toml, and Embed.toml from Lesson 1.

src/main.rs

//! Potentiometer-controlled servo with buzzer feedback on the Raspberry Pi Pico.
//!
//! GP0 (PWM0A): Servo signal (50 Hz, 0.5-2.5 ms pulse)
//! GP18 (GPIO): Piezo buzzer (toggled by timer interrupt)
//! GP26 (ADC0): Potentiometer wiper
//!
//! The potentiometer sets the servo angle (0-180 degrees).
//! The buzzer plays a tone proportional to the angle:
//! 0 degrees -> 200 Hz, 180 degrees -> 2000 Hz.
//! The buzzer frequency is generated by a timer alarm interrupt
//! that toggles the buzzer GPIO at the required half-period.
#![no_std]
#![no_main]
use core::cell::RefCell;
use critical_section::Mutex;
use panic_halt as _;
use defmt_rtt as _;
use rp_pico::entry;
use rp_pico::hal;
use rp_pico::hal::pac;
use rp_pico::hal::pac::interrupt;
use rp_pico::hal::Clock;
use embedded_hal::digital::{OutputPin, StatefulOutputPin};
use embedded_hal::delay::DelayNs;
use embedded_hal::pwm::SetDutyCycle;
use embedded_hal_0_2::adc::OneShot;
// ================================================================
// Shared state between main loop and TIMER_IRQ_0 interrupt handler
// ================================================================
/// The raw TIMER peripheral, moved into the ISR via Option.
static TIMER_PERIPH: Mutex<RefCell<Option<pac::TIMER>>> =
Mutex::new(RefCell::new(None));
/// Buzzer half-period in microseconds. 0 means buzzer off.
/// Updated by main loop, read by ISR.
static BUZZER_HALF_PERIOD_US: Mutex<RefCell<u32>> =
Mutex::new(RefCell::new(0));
/// Buzzer GPIO pin, moved into the ISR via Option.
/// Using the specific pin type for GP18 configured as push-pull output.
type BuzzerPin = hal::gpio::Pin<
hal::gpio::bank0::Gpio18,
hal::gpio::FunctionSio<hal::gpio::SioOutput>,
hal::gpio::PullDown,
>;
static BUZZER_PIN: Mutex<RefCell<Option<BuzzerPin>>> =
Mutex::new(RefCell::new(None));
// ================================================================
// Timer interrupt handler: toggles buzzer at the configured frequency
// ================================================================
#[interrupt]
fn TIMER_IRQ_0() {
critical_section::with(|cs| {
let mut timer_ref = TIMER_PERIPH.borrow_ref_mut(cs);
let half_period = *BUZZER_HALF_PERIOD_US.borrow_ref(cs);
if let Some(ref timer) = *timer_ref {
// Clear the alarm 0 interrupt flag
timer.intr().write(|w| w.alarm_0().clear_bit_by_one());
if half_period > 0 {
// Toggle the buzzer pin
let mut pin_ref = BUZZER_PIN.borrow_ref_mut(cs);
if let Some(ref mut pin) = *pin_ref {
if pin.is_set_high().unwrap_or(false) {
let _ = pin.set_low();
} else {
let _ = pin.set_high();
}
}
// Schedule the next alarm
let now = timer.timerawl().read().bits();
let next = now.wrapping_add(half_period);
timer.alarm0().write(|w| unsafe { w.bits(next) });
}
}
});
}
// ================================================================
// Helper functions
// ================================================================
/// Map a 12-bit ADC value (0-4095) to a servo angle (0-180).
fn adc_to_angle(adc_val: u16) -> u16 {
((adc_val as u32 * 180) / 4095) as u16
}
/// Map a servo angle (0-180) to a PWM compare value for the servo.
/// Servo expects 0.5 ms to 2.5 ms pulse in a 20 ms period.
/// With PSC such that counter runs at 1 MHz and wrap = 19999:
/// 0 degrees -> 500 (0.5 ms)
/// 90 degrees -> 1500 (1.5 ms)
/// 180 degrees -> 2500 (2.5 ms)
fn angle_to_servo_duty(angle: u16) -> u16 {
let angle = if angle > 180 { 180 } else { angle };
500 + ((angle as u32 * 2000) / 180) as u16
}
/// Map a servo angle (0-180) to a buzzer frequency (200-2000 Hz).
/// Returns the half-period in microseconds for the timer interrupt.
/// Returns 0 if angle is 0 (buzzer off at minimum).
fn angle_to_buzzer_half_period(angle: u16) -> u32 {
if angle == 0 {
return 0; // buzzer off
}
// Frequency: 200 Hz at 0 degrees, 2000 Hz at 180 degrees
let freq_hz = 200 + ((angle as u32 * 1800) / 180);
// Half-period in microseconds: 1_000_000 / (2 * freq_hz)
1_000_000 / (2 * freq_hz)
}
// ================================================================
// Entry point
// ================================================================
#[entry]
fn main() -> ! {
defmt::info!("Booting rp2040-servo-buzzer");
// ---- Peripheral setup ----
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
let clocks = hal::clocks::init_clocks_and_plls(
rp_pico::XOSC_CRYSTAL_FREQ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
defmt::info!(
"System clock: {} MHz",
clocks.system_clock.freq().to_MHz()
);
// ---- GPIO setup ----
let sio = hal::Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
// ---- PWM setup for servo (GP0, slice 0, channel A) ----
let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
let mut pwm0 = pwm_slices.pwm0;
// Configure for 50 Hz: 125 MHz / 125 = 1 MHz counter, wrap at 19999
pwm0.set_div_int(125);
pwm0.set_div_frac(0);
pwm0.set_top(19999);
pwm0.enable();
// Assign GP0 to PWM0A
let channel_a = &mut pwm0.channel_a;
channel_a.output_to(pins.gpio0);
// Start at center (90 degrees = 1500 us pulse)
channel_a.set_duty_cycle(1500).unwrap();
defmt::info!("Servo PWM configured: 50 Hz on GP0");
// ---- Buzzer GPIO setup (GP18 as push-pull output) ----
let buzzer_pin = pins.gpio18.into_push_pull_output();
// Move the buzzer pin into the static for ISR access
critical_section::with(|cs| {
BUZZER_PIN.borrow_ref_mut(cs).replace(buzzer_pin);
});
// ---- ADC setup (GP26 = ADC0 for potentiometer) ----
let mut adc = hal::Adc::new(pac.ADC, &mut pac.RESETS);
let mut adc_pin = hal::adc::AdcPin::new(pins.gpio26).unwrap();
defmt::info!("ADC configured on GP26");
// ---- Timer alarm setup for buzzer interrupt ----
// We need the raw TIMER peripheral for alarm interrupts.
// The HAL Timer type does not expose alarm interrupts directly
// in a way that works cleanly with the static Mutex pattern,
// so we use the PAC directly for the alarm.
// Note: pac.TIMER was already used by hal::Timer if we created one.
// Instead, we access it through the Mutex after moving it there.
// Enable alarm 0 interrupt in the TIMER peripheral
// We need to configure this before moving the peripheral
let timer = pac.TIMER;
timer.inte().modify(|_, w| w.alarm_0().set_bit());
// Set an initial alarm (will be reconfigured by the ISR)
let initial_alarm = timer.timerawl().read().bits().wrapping_add(100_000);
timer.alarm0().write(|w| unsafe { w.bits(initial_alarm) });
// Move the TIMER peripheral into the static for ISR access
critical_section::with(|cs| {
TIMER_PERIPH.borrow_ref_mut(cs).replace(timer);
});
// Enable the TIMER_IRQ_0 interrupt in the NVIC
unsafe {
pac::NVIC::unmask(pac::Interrupt::TIMER_IRQ_0);
}
defmt::info!("Timer alarm 0 interrupt enabled for buzzer");
// ---- Create a SysTick-based delay for the main loop ----
let mut delay = cortex_m::delay::Delay::new(
core.SYST,
clocks.system_clock.freq().to_Hz(),
);
// ---- Main loop ----
let mut last_angle: u16 = 255; // impossible value to force first update
defmt::info!("Entering main loop. Turn the potentiometer.");
loop {
// Read the potentiometer (12-bit: 0 to 4095)
let adc_value: u16 = adc.read(&mut adc_pin).unwrap();
// Map to servo angle (0 to 180 degrees)
let angle = adc_to_angle(adc_value);
// Only update if the angle changed (reduces servo jitter)
if angle != last_angle {
// Update servo PWM duty cycle
let duty = angle_to_servo_duty(angle);
pwm0.channel_a.set_duty_cycle(duty).unwrap();
// Update buzzer frequency via the shared Mutex
let half_period = angle_to_buzzer_half_period(angle);
critical_section::with(|cs| {
*BUZZER_HALF_PERIOD_US.borrow_ref_mut(cs) = half_period;
});
defmt::info!(
"ADC: {} | Angle: {} deg | Servo duty: {} | Buzzer half-period: {} us",
adc_value,
angle,
duty,
half_period
);
last_angle = angle;
}
// Small delay to avoid flooding the ADC and RTT
delay.delay_ms(20);
}
}

Build and Flash

Terminal window
cargo run --release

Expected RTT output:

INFO Booting rp2040-servo-buzzer
INFO System clock: 125 MHz
INFO Servo PWM configured: 50 Hz on GP0
INFO ADC configured on GP26
INFO Timer alarm 0 interrupt enabled for buzzer
INFO Entering main loop. Turn the potentiometer.
INFO ADC: 2048 | Angle: 90 deg | Servo duty: 1500 | Buzzer half-period: 454 us
INFO ADC: 3000 | Angle: 131 deg | Servo duty: 1955 | Buzzer half-period: 345 us
INFO ADC: 4095 | Angle: 180 deg | Servo duty: 2500 | Buzzer half-period: 250 us

Testing Checklist

  1. Turn the potentiometer fully counterclockwise. The servo should move to 0 degrees and the buzzer should be silent (or play a very low 200 Hz tone).

  2. Slowly turn the potentiometer clockwise. The servo should smoothly track the knob position. The buzzer pitch should rise as the angle increases.

  3. Turn fully clockwise. The servo should reach 180 degrees and the buzzer should play a high 2000 Hz tone.

  4. Move the potentiometer quickly back and forth. The servo should follow without significant lag. The buzzer tone should track the angle.

  5. Check the RTT output. Angle values should range from 0 to 180 and duty values from 500 to 2500.

  6. If the servo jitters at rest, try adding a 10 uF capacitor across the servo power pins. Noise on the 5V line causes jitter.

Timer Architecture on the RP2040



The RP2040 has a single 64-bit microsecond timer with 4 alarm registers. Each alarm can trigger an interrupt when the lower 32 bits of the timer match the alarm value. This is different from STM32, which has multiple independent timer peripherals with prescalers and auto-reload registers.

FeatureRP2040 TimerSTM32 General-Purpose Timer
Counter width64-bit (read as two 32-bit halves)16-bit or 32-bit
Clock source1 MHz (fixed from reference clock)Configurable via prescaler
Alarm/compare channels4 (alarm 0 through 3)4 (per timer)
Auto-reloadNo (alarms are one-shot, re-arm manually)Yes (ARR register)
PWM generationNot via timer (separate PWM peripheral)Built into timer channels

Why We Use Both Timer and PWM

On the RP2040, the timer peripheral and the PWM peripheral are separate. The timer is a microsecond counter with alarms. The PWM hardware has its own counters with dividers and wrap values. For servo PWM, we use the PWM hardware because it generates the signal in hardware with no CPU involvement. For the buzzer, we use a timer alarm interrupt to toggle a GPIO, because the buzzer frequency changes dynamically and we want to demonstrate interrupt handling.

An alternative approach would be to reconfigure the buzzer’s PWM slice wrap value and divider on every angle change. Both approaches are valid. The interrupt approach teaches a more general pattern you will need in later lessons.

Interrupt Safety Deep Dive



Why unsafe for NVIC::unmask?

unsafe {
pac::NVIC::unmask(pac::Interrupt::TIMER_IRQ_0);
}

Enabling an interrupt is unsafe because it causes code to run asynchronously. The Rust compiler cannot verify that your interrupt handler and your main code do not have data races unless you use the Mutex/critical-section pattern. By marking NVIC::unmask as unsafe, Rust forces you to acknowledge that you are enabling asynchronous execution and that you have handled synchronization correctly.

The Option Pattern for Moving Peripherals into ISRs

Interrupt handlers in Rust are free functions (not closures), so they cannot capture variables from the enclosing scope. The only way to give them access to peripherals is through statics. But statics must be initialized at compile time, and peripherals are only available at runtime (after Peripherals::take()).

The solution is Mutex<RefCell<Option<T>>>:

  1. The static starts as None.
  2. In main(), after configuring the peripheral, you move it into the static with .replace(peripheral).
  3. In the ISR, you borrow the static and match on Some(ref peripheral) to access it.

This is more verbose than C’s approach of directly accessing peripheral registers, but it provides guarantees that C cannot: the compiler verifies that the peripheral is accessed through a critical section, and the type system prevents accessing it before initialization (the if let Some(...) pattern).

Atomic Operations as an Alternative

For simple shared values like the buzzer half-period (a single u32), you can use core::sync::atomic::AtomicU32 instead of Mutex<RefCell<u32>>. Atomics do not require disabling interrupts:

use core::sync::atomic::{AtomicU32, Ordering};
static BUZZER_HALF_PERIOD_ATOMIC: AtomicU32 = AtomicU32::new(0);
// In main loop:
BUZZER_HALF_PERIOD_ATOMIC.store(half_period, Ordering::Relaxed);
// In ISR:
let hp = BUZZER_HALF_PERIOD_ATOMIC.load(Ordering::Relaxed);

This is lighter weight but only works for single values that can be updated atomically. For multiple related values that must be consistent (like a struct with both frequency and duration), use the Mutex pattern.

Production Notes



Production Considerations

Servo signal quality: The RP2040’s PWM hardware generates jitter-free servo signals because the counter runs in hardware. The only source of jitter is updating the duty cycle register, which the hardware double-buffers. In production, this is more reliable than software-generated PWM.

ADC noise: The RP2040’s ADC is noisy, especially when USB is active. For production, average multiple readings, add a 100 nF capacitor between the ADC input and ground, and consider using the ADC’s round-robin mode with DMA (covered in Lesson 5).

Interrupt latency: The critical section in the ISR disables interrupts for the duration of the closure. Keep the closure short. In this project, the ISR toggles one pin and re-arms one alarm, which takes only a few microseconds. For time-critical applications, consider using atomics instead of critical sections.

Buzzer alternative: Instead of toggling a GPIO in a timer ISR, you could use a second PWM slice with its duty cycle set to 50% and change its frequency by adjusting the wrap value. This would be fully hardware-driven with zero CPU overhead. The ISR approach used here is pedagogical; it teaches the interrupt pattern that you will need for more complex scenarios.

Power management: The SG90 servo draws significant current during movement. If your application is battery-powered, consider using a servo with lower idle current, or switching the servo power supply off when the servo is not moving (using a MOSFET controlled by a GPIO pin).

What You Have Learned



Lesson 3 Complete

Timer knowledge:

  • RP2040’s 64-bit microsecond timer with 4 alarm channels
  • Difference between the timer peripheral (alarms, timestamps) and PWM peripheral (free-running counters)
  • Alarm interrupts: one-shot, must be re-armed manually

PWM skills:

  • PWM slice architecture: 8 slices, 2 channels each
  • GPIO-to-slice mapping for the RP2040
  • Servo PWM: 50 Hz, 0.5 to 2.5 ms pulse width, configured with divider and wrap
  • Duty cycle calculation for arbitrary angles

ADC skills:

  • 12-bit ADC read on GP26 (ADC0)
  • Mapping ADC values to application-level quantities

Interrupt handling:

  • #[interrupt] attribute for ISR functions
  • Mutex<RefCell<Option<T>>> pattern for sharing peripherals with ISRs
  • critical_section::with() for synchronized access
  • unsafe { NVIC::unmask() } and why it is marked unsafe
  • Atomics as a lightweight alternative to critical sections
  • Comparison of C volatile globals vs Rust Mutex pattern

Comments

Loading comments...


© 2021-2026 SiliconWit®. All rights reserved.