Timer on rp-pico: Using timer multiple time and timer lifetime for interrupt request

I am new to rust and learning to using it for embedded.
I am having some trouble trying to drive a ws2812 led strip using timer interrupt request on pi pico:

  1. How could I use the timer twice: one for setting up the TIMER_IRQ_1, one for ws2812 count_down. rust won't let me due to timer was moved when I use it the second time for the count_down
  2. The timer does not live long enough when I implements it as below. How do I keep the timer live longer?

Any help is appreciated.

The code:

//! # interrupt request demo for rp-pico

#![no_std]
#![no_main]

use core::cell::RefCell;

use critical_section::Mutex;
use embedded_hal::digital::v2::OutputPin;
use fugit::ExtU32;
use panic_halt as _;
use rp_pico::{
    entry,
    hal::{
        self,
        gpio::{
            self, bank0::Gpio27, FunctionPio0, FunctionSioInput, FunctionSioOutput,
            Interrupt::EdgeLow, Pin, PullDown, PullUp,
        },
        pac::{self, interrupt},
        pio::{PIOExt, SM0},
        prelude::*,
        timer::{Alarm, Alarm1, CountDown, Timer},
        Watchdog,
    },
    pac::PIO0,
};
use smart_leds::{
    // brightness,
    colors,
    // SmartLedsWrite,
    RGB8,
};
use ws2812_pio::Ws2812;

const STRIP_LEN: usize = 16;

type ActionBtn1 = gpio::Pin<gpio::bank0::Gpio20, FunctionSioInput, PullUp>;
type ActionBtn2 = gpio::Pin<gpio::bank0::Gpio21, FunctionSioInput, PullUp>;
type LedPin = gpio::Pin<gpio::bank0::Gpio25, FunctionSioOutput, PullDown>;
type ActionBtns = (ActionBtn1, ActionBtn2);
type NeoStripCtl<'timer> = (Alarm1, LedPin, Ws<'timer>);
type LedStrip = [RGB8; STRIP_LEN];
type Ws<'timer> = Ws2812<PIO0, SM0, CountDown<'timer>, Pin<Gpio27, FunctionPio0, PullDown>>;

static G_BTNS: Mutex<RefCell<Option<ActionBtns>>> = Mutex::new(RefCell::new(None));
Mutex::new(RefCell::new(None));
// Global variables for controlling neo led strip
static G_NEO_CTL: Mutex<RefCell<Option<NeoStripCtl>>> = Mutex::new(RefCell::new(None));
static G_LED_STRIP: Mutex<RefCell<Option<LedStrip>>> = Mutex::new(RefCell::new(None));

const FRAME_DELAY: u32 = 32; // ~30FPS

#[entry]
fn main() -> ! {
    let mut pac = pac::Peripherals::take().unwrap();

    let mut watchdog = Watchdog::new(pac.WATCHDOG);

    // Configure the clocks
    // The default is to generate a 125 MHz system clock
    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();

    // The single-cycle I/O block controls our GPIO pins
    let sio = hal::Sio::new(pac.SIO);

    // Set the pins up according to their function on this particular board
    let pins = rp_pico::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    let led_pin = pins.led.reconfigure();

    // Reconfigure action buttons and enable IRQ on high-to-low
    let action_btn_1 = pins.gpio20.reconfigure();
    action_btn_1.set_interrupt_enabled(EdgeLow, true);

    let action_btn_2 = pins.gpio21.reconfigure();
    action_btn_2.set_interrupt_enabled(EdgeLow, true);

    let mut timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); 
    // --------- binding `timer` declared here
    
    let mut alarm1 = timer.alarm_1().unwrap();
    alarm1.schedule(FRAME_DELAY.micros()).unwrap();
    alarm1.enable_interrupt();

    // LED strip initialized
    let leds: [RGB8; STRIP_LEN] = [colors::BLACK; STRIP_LEN];

    // Split the PIO state machine 0 into individual objects, so that
    // Ws2812 can use it:
    let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);

    let ws = Ws2812::new(
        pins.gpio27.into_function(),
        &mut pio,
        sm0,
        clocks.peripheral_clock.freq(),
        timer.count_down(),
    );

    // Move variables to global
    critical_section::with(|cs| {
        G_BTNS
            .borrow(cs)
            .replace(Some((action_btn_1, action_btn_2)));
        G_NEO_CTL.borrow(cs).replace(Some((alarm1, led_pin, ws)));
        // -------------------- argument requires that `timer` is borrowed for `'static`

        G_LED_STRIP.borrow(cs).replace(Some(leds));
    });

    // unmask interrupt controllers
    unsafe {
        pac::NVIC::unmask(pac::Interrupt::IO_IRQ_BANK0);
        pac::NVIC::unmask(pac::Interrupt::TIMER_IRQ_1);
    }

    loop {
        cortex_m::asm::wfi();
    }
}
// - `timer` dropped here while still borrowed 

#[interrupt]
fn IO_IRQ_BANK0() {
   // Do something when btn is pressed
}

#[interrupt]
fn TIMER_IRQ_1() {

    static mut NEO_STRIP_CTL: Option<NeoStripCtl> = None;
    static mut LED_STRIP: Option<LedStrip> = None;

    // lazy load global variables
    if LED_STRIP.is_none() {
        critical_section::with(|cs| {
            *LED_STRIP = G_LED_STRIP.borrow(cs).take();
        })
    }
    if NEO_STRIP_CTL.is_none() {
        critical_section::with(|cs| {
            *NEO_STRIP_CTL = G_NEO_CTL.borrow(cs).take();
        })
    }

    if let Some((alarm1, led_pin, ws)) = NEO_STRIP_CTL {
        // lit up statup led before doing some work
        led_pin.set_high().unwrap();

        // Do something with ws

        // reschedule alarm1
        alarm1.clear_interrupt();
        alarm1.schedule(FRAME_DELAY.micros()).unwrap();
        // turm off status led when done
        led_pin.set_low().unwrap();
    }
}

what's the compile error for this? I don't think you need to "move" the timer in order to enable the interrupt, unless I'm missing something.

first a disclaimer: this is a hacky workaround just for this example code snippet, this is NOT a proper solution, you have a deeper problem in your design, but anyways, here's how you can promote the timer to static storage (it uses unsafe):

	// static storage for the promoted value
	static mut _DO_NOT_TOUCH_: MaybeUninit<Timer> = MaybeUninit::uninit();
	// safety: the static variable only accessible in main function.
	// after the promotion, it is immediately borrowed.
	// do NOT access the static variable directly
	let timer = unsafe { _DO_NOT_TOUCH_.write(timer) };

	// now type is ok: `timer: &'static Timer`
	let ws = Ws2812::new(...., timer.count_down());

some notes:

I checked the document for Ws2818, and it feels wrong to me that you need to promote the hal type Timer into 'static. either the API design is clumsy, or you are doing something the API is not designed for. I think the main problem is you want to put app logic into the interrupt handler itself, but the app logic depends on many runtime initialized resources (including the count down timer), so you have to pass those from the main funciton to the interrupt handler, which must be 'static.

it's not a good practice to do app logic inside the interrupt handler, instead you should put your code in the main loop (or better yet, use some form of task scheduling system like rtic, or embassy, etc), and the interrupt handler should only wakeup and notify the main task. this way, you get rid of all those ugly global variables and the synchronization boilerplates (it's not fun to use types like Mutex<RefCell<Option<...>>>).

the type system is there to help you write better code, if you find yourself keep fighting the type systems or trying to by pass the type system, chances are you have some fundamental design flaws.

1 Like

Thanks for your response,

Nvm, I can't re-produce it now, maybe I mixed up with the other problem I had when I tried rtic.

Thanks for the snippet, I will try it and share the result

The Mutex<RefCell<Option<...>>> thing comes straight from official example of rp_2040 for irq, I find it ugly as well.

I have successfully get it running when I put everything in the main app logic, but the problem is everything run in a loop with a delay:

    loop {
        if action_btn_1.is_low().unwrap() {
            animation.action_1();
        }
        if action_btn_2.is_low().unwrap() {
            animation.action_2();
        }
        // mutate the leds strip for next frame
        animation.next_tick(&mut leds);
        // Here the magic happens and the `leds` buffer is written to the
        // ws2812 LEDs:
        ws.write(brightness(leds.iter().copied(), strip_brightness))
            .unwrap();
        // Wait a bit until calculating the next frame:
        frame_delay.delay_ms(32); // ~30 FPS
    }

and this leads to the buttons don't response correctly when you press them in the frame_delay period (if the delay is significant).
So I am trying to get it running correctly, I tried rtic but could not get it run as I want either (maybe I will raise a separate question for rtic implementation.

don't get me wrong, the synchronization is necessary to make the program race free (without unsafe), and it is deliberately that way, also to motivate the programmer to think carefully, like: what's the minimal states that must be shared between the main program and the interrupt handler.

in pracetice, unless in very simple use cases [1], it is common to only share a signaling/notification object,( e.g. a boolean flag, or a Waker if you use async, etc), and maybe some shared buffer/queue too, while the real work is done in the main application.

that's where non-blocking comes into play, and why you need a scheduler/reactor mechanism. in its simplest form, you may implement something like a event loop, the structure of the code might look like:

/// sort of like a software irq flags
#[derive(Clone, Copy)]
struct Events {
    timer: bool,
    serial: bool,
    button: bool,
}
static EVENTS: Mutex<Cell<Events>> = ...;

fn main() {
    // initialization 
    __setup_interrupts();
    loop {
        __wait_for_interrupts();
        let events = cortex_m::interrupt::free(|cs| {
            EVENTS.lock(cs).replace(Event {...})
        });
        if events.timer {
            // handle timer tick
        }
        if events.serial {
            // handle serial communication
        }
        if events.button {
            // handle button gpio trigger
        }
    }
}

#[interrupt]
fn Timer() {
    // safety: interrupt is disabled by hardware, we don't enable nested interrupt
    let cs = unsafe { CriticalSection::new() };
    // set the corresponding flag
    let events = EVENTS.borrow(cs);
    events.set(Events {
        timer: true,
        ..events.get()
    }
}

// similar for other irq handlers

that's one reason I feel the Ws2812 API is not ideal. the CountDown API uses non-blocking IO (i.e. the wait() method returns nb::Result<> type), although the Ws28128 constructor takes the ownership of the CountDown object. it doesn't provide an non-blocking API, I would assume it just wait for the timer in a busy loop, which is one of the reason you should not access the ws2812 within the irq handler.


  1. like, the interrupt handler only read and/or write some peripheral registers, e.g. to flips GPIO pins etc ↩ī¸Ž

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.