Embasy-rp ws2812-pio does not work from a task

I'm trying to use the neopixel PIO program from embassy_rp, based on this example. That works, but if I try to move my use of the RGB led to a task, it does not. It's not panicing, but the led does not change. I can verify the task is running by blinking a plain led in the same loop.

My board is the waveshare rp2040-zero, which unfortunately does not expose debug pins.

Can anyone help me figure out what's going on?

Full code below:

//! This example shows powerful PIO module in the RP2040 chip to communicate with WS2812 LED modules.
//! See (https://www.sparkfun.com/categories/tags/ws2812)
//! original example: https://raw.githubusercontent.com/embassy-rs/embassy/refs/tags/embassy-rp-v0.9.0/examples/rp/src/bin/pio_ws2812.rs

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_rp::bind_interrupts;
use embassy_rp::gpio::{Level, Output};
use embassy_rp::peripherals::PIO0;
use embassy_rp::pio::{InterruptHandler, Pio};
use embassy_rp::pio_programs::ws2812::{Grb, PioWs2812, PioWs2812Program};
use embassy_time::{Duration, Ticker};
use smart_leds::RGB8;

use panic_probe as _;

bind_interrupts!(struct Irqs {
    PIO0_IRQ_0 => InterruptHandler<PIO0>;
});

/// Input a value 0 to 255 to get a color value
/// The colours are a transition r - g - b - back to r.
fn wheel(mut wheel_pos: u8) -> RGB8 {
    wheel_pos = 255 - wheel_pos;
    if wheel_pos < 85 {
        return (255 - wheel_pos * 3, 0, wheel_pos * 3).into();
    }
    if wheel_pos < 170 {
        wheel_pos -= 85;
        return (0, wheel_pos * 3, 255 - wheel_pos * 3).into();
    }
    wheel_pos -= 170;
    (wheel_pos * 3, 255 - wheel_pos * 3, 0).into()
}


#[embassy_executor::main]
async fn main(#[allow(unused_variables)]spawner: Spawner) {
    //info!("Start");
    let p = embassy_rp::init(Default::default());

    // normal led on breadboard at pin 29
    let led = Output::new(p.PIN_29, Level::Low);

    let Pio { mut common, sm0, .. } = Pio::new(p.PIO0, Irqs);

    // waveshare rp2040-zero neopixel on pin 16:
    let program = PioWs2812Program::new(&mut common);
    let ws2812 = PioWs2812::new(&mut common, sm0, p.DMA_CH0, p.PIN_16, &program);

    // Works:
    // blink(led, ws2812).await;

    // Doesn't work, RBG does not change:
    spawner.spawn(blink_task(led, ws2812)).unwrap();
}

async fn blink(mut led: Output<'static>, mut ws2812: PioWs2812<'static, PIO0, 0, 1, Grb>) {
    // Loop forever making RGB values and pushing them out to the WS2812.
    const NUM_LEDS: usize = 1;
    let mut data = [RGB8::default(); NUM_LEDS];
    let mut ticker = Ticker::every(Duration::from_millis(100));
    loop {
        for j in 0..(256 * 5) {
            // debug!("New Colors:");
            for i in 0..NUM_LEDS {
                data[i] = wheel((((i * 256) as u16 / NUM_LEDS as u16 + j as u16) & 255) as u8);
                // debug!("R: {} G: {} B: {}", data[i].r, data[i].g, data[i].b);
            }
            ws2812.write(&data).await;

            led.toggle(); // blink normal led as proof of life
            ticker.next().await;
        }
    }
}

#[embassy_executor::task]
async fn blink_task(led: Output<'static>, ws2812: PioWs2812<'static, PIO0, 0, 1, Grb>) {
    blink(led, ws2812).await;
}

Thanks.

1 Like

From the examples I have seen, main shouldn't return. Try putting an infinite loop of async sleeps at the end of main.

Thanks for the fast reply, that appears to work.

In my larger application, it's only this piece that seems to need main to stick around. USB-CDC, USB-HID, and UARTS continue just fine.

Embassy does have an example that returns from main here. It is quite simple, though.

I'm curious what is going on under the hood. What pieces need that main loop?

That is strange. Unfortunately, I don't know how embassy generates async main well enough to know.

For those following along at home, I suspect I'm hitting this bug in embassy_rp v0.9.0 :

I'm testing with embassy main now, and it seems to be better behaved.

1 Like