cat-in-136 / ws2812-esp32-rmt-driver

WS2812 driver using ESP32 RMT for Rust
MIT License
40 stars 22 forks source link

Flickering when using wifi from esp_idf_svc with this crate #33

Open RoyalFoxy opened 1 year ago

RoyalFoxy commented 1 year ago

So I encounter flickering of random colors on my led board. I already checked many different pins (to be exact 2,4,13,16,17,18) and all had the same problems. A small video showing what I mean.

https://github.com/cat-in-136/ws2812-esp32-rmt-driver/assets/48829979/04ebf5a8-a312-4cff-a698-da76c35600cf

Here's a demo of my code I use. You may need to adjust the led amount and what pin.

I tested it and it flickers when the wifi is started up.

thorhs commented 10 months ago

I am also experiencing some flickering/incorrect output from time to time. If I run my test pattern before starting the Wi-Fi, it works flawlessly. With Wi-Fi, I get the incorrect output.

Most likely this is due to Wi-Fi interrupts firing during a critical section causing a slight delay in the output timing.

I have not been able to dig deeper into the code to try to figure out what is causing it.

https://github.com/cat-in-136/ws2812-esp32-rmt-driver/assets/1536878/eae98656-f1ee-4c50-ac64-f11840872ffb

RoyalFoxy commented 10 months ago

It irritates me tho that this is not happening with WLED considering that codebase also runs on esp32, has wifi and can control hundreds of leds. Maybe they implemented it without anything from esp-idf or they edited the source from esp-idf to make it not flicker.

I don't have a video of it at hand but I can confirm that a lamp with 240 leds in serial powered by WLED is not affected by our issue

y34hbuddy commented 10 months ago

If your hardware is dual-core, try running all of your display code on Core 1 as all of the wifi activity (and its interrupts, as @thorhs mentions) will run on Core 0.

Try using unsafe { xTaskCreatePinnedToCore() }

thorhs commented 10 months ago

Interesting idea. I'm currently using std threads, is there a simple migration between them that you know of?

y34hbuddy commented 10 months ago

I don't think it'll be too difficult to migrate from std threads, and in fact, you might not have to do so everywhere in your code. Depending on your implementation, you may just need to call xTaskCreatePinnedToCore() once to spawn your display code.

In my use case, my recommendation works perfectly -- and it took quite a bit of digging into the C++ ESP-IDF and Arduino universe to find the solution, ex: https://github.com/FastLED/FastLED/issues/507 :smile:

thorhs commented 10 months ago

This worked perfectly, haven't gotten a single flicker after switching to core 1.

For others that have the same issue, I moved my display loop to a new function and created a wrapper function to handle the C ffi.

Snippets:

    unsafe {
        xTaskCreatePinnedToCore(
            Some(start_display_loop),
            CString::new("Display Task").unwrap().as_ptr(),
            10000,
            unsafe { &panel_sender as *const _ as *mut std::ffi::c_void },
            0,
            std::ptr::null_mut(),
            1,
        );
    }
unsafe extern "C" fn start_display_loop(receiver: *mut core::ffi::c_void) {
    let panel_receiver: &std::sync::mpsc::Receiver<PanelCommand<'_>> =
        &*(receiver as *const std::sync::mpsc::Receiver<PanelCommand<'_>>);

    display_loop(panel_receiver);
}

fn display_loop(panel_receiver: &std::sync::mpsc::Receiver<PanelCommand<'_>>) {
...
}

There may very well be better ways to go about it, but this works fine. I had to tune the stack size to get my code working, so that is somethign to keep in mind.

RoyalFoxy commented 10 months ago

In the past I had this code which didn't work even tho the commented line sounds familiar to xTaskCreatePinnedToCore. Do you know why this doesn't work?

ThreadSpawnConfiguration {
    name: Some(b"LED-THREAD\0"),
    stack_size: STACK_SIZE,
    priority: 1,
    pin_to_core: Some(Core::Core1), // <- Sounds familiar
    ..Default::default()
}
.set()?;

std::thread::spawn(|| -> anyhow::Result<()> {
    // LED Code
});
DavidVentura commented 8 months ago

I'm seeing the flicker with both a normal thread on core1 and xTaskCreatePinnedToCore -- I see a 3μs interrupt every 1 or 2 seconds when Wifi is enabled.

This is with my task on core 1; I don't think it's a problem on this library, but I wonder why xTaskCreatePinnedToCore works for you

DavidVentura commented 8 months ago

I've found what the issue is: When creating the RMT driver, the current core affinity is stored. Later, when starting a transmission, the RMT driver spawns a thread based on the stored affinity. To fix the issue, you need to call Ws2812Esp32Rmt::new from within Core1 -- calling ws2812.write from Core1 is not enough.

With this, my 1280pix matrix is flicker-free

aligator commented 3 months ago

I tried to do the same. When connecting to a wifi and running it in the normal thread it flickers some times (as expected). If I create a simple webserver and spam it with calls it flickers more (as expected).

But when using the core1 it still does it. It feels a bit less, but even when I send a request only every second, it sometimes still flickers. If I spam many requests, it flickers also very noticeable...

So running the Ws2812Esp32Rmt on core 1 does not really seem to resolve this problem as expected...

As I am not very experienced with rust on the esp, it took me some time to get it working in the extra thread. This is what I use now: https://github.com/aligator/e-chess/blob/d46b8fe666993aa59dee65ecf640145bafa625e4/firmware/src/main.rs

I call Ws2812Esp32Rmt::new from within the core1 thread.

DavidVentura commented 3 months ago

In my project it is working fine, check: https://github.com/DavidVentura/matrix-fire/blob/master/esp/src/main.rs#L24

the wifi settings must be done with affinity on core0 and the call to Ws2812Esp32Rmt::new must be done

this is because the rmt peripheral wil later just start a thread for interrupt handling with the global affinity state

aligator commented 3 months ago

I just found out that it much better runs with release mode with these settings.

[profile.release]
opt-level = "s"

[profile.dev]
debug = true    # Symbols are nice and they don't increase the size on Flash
opt-level = "z"

Now I can spam many requests and only get some flickers. Not zero but ok for now.

However thanks for your code, I will try it how you do it now, since you do not use the low level xTaskCreatePinnedToCore.

aligator commented 3 months ago

Ok, I don't know why, but using the std threads does not work reliable for me. It always throws a stack overflow as soon as I do something inside the thread... (with the same stacksize configuration as I use with xTaskCreatePinnedToCore)

Not exactly sure whats happening there...

However using xTaskCreatePinnedToCore and your tipps regarding the global affinity I got it working more reliable now even in non-release mode.

For me this is ok for now.

About this lib: Shouldn't there be either a more easy way to just configure core the lib uses to send the data to the leds?
Or there should be at least some "best practice" example for usage together with WIFI, as I suspect this is a very common usecase on the esp.

y34hbuddy commented 3 months ago

Same, I also get stack overflows when I refactor from xTaskCreatePinnedToCore to std threads, even if I specify a huge stack size.

Anyone know why?

RoyalFoxy commented 2 months ago

@y34hbuddy You should set CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=10000 in your sdkconfig.defaults file and it should work

RoyalFoxy commented 2 months ago

I've gathered the info from this issue so far and created a small project trying to make it work but I still get flickering when sending requests

The program looks something like this

fn main() -> anyhow::Result<()> {
    esp_idf_svc::sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();

    let peripherals = Peripherals::take().unwrap();
    let modem = peripherals.modem;
    let led_pin = peripherals.pins.gpio13;
    let channel = peripherals.rmt.channel0;

    ThreadSpawnConfiguration {
        pin_to_core: Some(Core::Core0),
        priority: 1,
        name: Some("SERVER\0".as_bytes()),
        stack_size: 0x4000,
        ..Default::default()
    }
    .set()
    .expect("Cannot set thread spawn config");

    std::thread::spawn(move || -> anyhow::Result<()> {
        let sysloop = EspSystemEventLoop::take()?;
        let nvs = EspDefaultNvsPartition::take()?;

        let mut wifi =
            BlockingWifi::wrap(EspWifi::new(modem, sysloop.clone(), Some(nvs))?, sysloop)?;

        // Connect to wifi with BlockingWifi
        // Create http server with EspHttpServer

        loop {
            sleep(Duration::from_millis(10000));
        }
    });

    ThreadSpawnConfiguration {
        name: Some("PIXELS\0".as_bytes()),
        stack_size: 0x4000,
        pin_to_core: Some(Core::Core1),
        priority: 24,
        ..Default::default()
    }
    .set()
    .expect("Cannot set thread spawn config");

    std::thread::spawn(|| -> anyhow::Result<()> {
        let mut data = [RGB8::default(); 100];
        data[0] = RGB8::new(10, 0, 0);
        data[1] = RGB8::new(10, 0, 0);
        data[2] = RGB8::new(10, 0, 0);

        let mut led_driver = Ws2812Esp32Rmt::new(channel, led_pin).unwrap();

        loop {
            led_driver.write_nocopy(data.into_iter())?;

            std::thread::sleep(Duration::from_millis(1000 / 60))
        }
    });

    loop {
        std::thread::sleep(Duration::from_secs(10))
    }
}

I'm unsure what I am doing wrong, I tested various pins again and it seems to not work at all and it always will flicker

I guess the problems are interrupts but shouldn't they not trigger on the second core? And how can I disable them for a certain block of code, I know that I can I just couldn't find how