esp-rs / esp-hal

no_std Hardware Abstraction Layers for ESP32 microcontrollers
https://docs.esp-rs.org/esp-hal/
Apache License 2.0
767 stars 214 forks source link

ESP32: Async SPI Master freezes in `write` #1728

Closed raphaelhetzel closed 4 months ago

raphaelhetzel commented 5 months ago

Hi, I wanted to use hal::spi::master::Spi as an embedded_hal_async::spi::SpiBus. Given the asynch module is not public I don't see any way to do that. Is this hidden by accident, or am I missing something?

bjoernQ commented 5 months ago

What exactly sure what you are after but something like this works:

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use esp_backtrace as _;
use esp_hal::{
    clock::ClockControl,
    dma::*,
    dma_descriptors,
    gpio::Io,
    peripherals::Peripherals,
    prelude::*,
    spi::{
        master::{prelude::*, Spi},
        SpiMode,
    },
    system::SystemControl,
    timer::timg::TimerGroup,
};

#[main]
async fn main(_spawner: Spawner) {
    esp_println::println!("Init!");
    let peripherals = Peripherals::take();
    let system = SystemControl::new(peripherals.SYSTEM);
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    let timg0 = TimerGroup::new_async(peripherals.TIMG0, &clocks);
    esp_hal_embassy::init(&clocks, timg0);

    let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);
    let sclk = io.pins.gpio0;
    let miso = io.pins.gpio2;
    let mosi = io.pins.gpio4;
    let cs = io.pins.gpio5;

    let dma = Dma::new(peripherals.DMA);

    #[cfg(any(feature = "esp32", feature = "esp32s2"))]
    let dma_channel = dma.spi2channel;
    #[cfg(not(any(feature = "esp32", feature = "esp32s2")))]
    let dma_channel = dma.channel0;

    let (descriptors, rx_descriptors) = dma_descriptors!(32000);

    let spi = Spi::new(peripherals.SPI2, 100.kHz(), SpiMode::Mode0, &clocks)
        .with_pins(Some(sclk), Some(mosi), Some(miso), Some(cs))
        .with_dma(
            dma_channel.configure_for_async(false, DmaPriority::Priority0),
            descriptors,
            rx_descriptors,
        );

    do_spi(spi).await;
}

async fn do_spi<'a>(
    mut spi: esp_hal::spi::master::dma::SpiDma<
        'a,
        esp_hal::peripherals::SPI2,
        Channel0,
        esp_hal::spi::FullDuplexMode,
        esp_hal::Async,
    >,
) {
    esp_println::println!("here");
    let send_buffer = [0, 1, 2, 3, 4, 5, 6, 7];
    loop {
        let mut buffer = [0; 8];
        esp_println::println!("Sending bytes");
        embedded_hal_async::spi::SpiBus::transfer(&mut spi, &mut buffer, &send_buffer)
            .await
            .unwrap();
        esp_println::println!("Bytes received: {:?}", buffer);
        Timer::after(Duration::from_millis(5_000)).await;
    }
}

If you want to spawn an embassy task you would need to promote the lifetime to static (e.g. make_static!)

raphaelhetzel commented 4 months ago

Thanks, I missed that.

Is there some example using SpiDMA as an embedded_hal_async SpiDevice (using embassy_embedded_hal or embedded_hal_bus)?

I am still trying to figure out why my code gets blocked at the flush at the end of a transaction (which seems to be waiting for some peripheral ref).

bjoernQ commented 4 months ago

We used to have those examples but decided to consolidate the mostly redundant examples. flush just waits until the device isn't busy anymore - if you have some minimal-repro code for that issue we can have a look

raphaelhetzel commented 4 months ago

I will try to fix it and will create a repro example if I'm sure it is an issue with the crate (most likely, I'm just using it wrong). For now, it would be sufficient to understand who resets the register block in the busy method used by the flush: https://github.com/esp-rs/esp-hal/blob/main/esp-hal/src/spi/master.rs#L2943

bjoernQ commented 4 months ago

SPI_USR_COMMAND is set by software to start the transfer and reset by hardware when done.

You can read about the details in the TRM - e.g. for ESP32-C6: https://www.espressif.com/sites/default/files/documentation/esp32-c6_technical_reference_manual_en.pdf#page=793

raphaelhetzel commented 4 months ago

I further experimented with it, but the async spi stack in esp-hal (0.18) always gets stuck during a write. Currently, the write gets stuck during crate::dma::asynch::DmaTxFuture.

I have attached a stripped-down version of my code as well as the sync equivalent that works.

Do you have any idea what might cause this?

Broken Async (embedded_hal_bus also did not work):

// SPDX-FileCopyrightText: © 2023 Technical University of Munich, Chair of Connected Mobility
// SPDX-License-Identifier: MIT
// Based on https://github.com/esp-rs/esp-hal/blob/main/esp32-hal/examples/embassy_hello_world.rs, https://github.com/esp-rs/esp-template/blob/main/src/main.rs & https://github.com/esp-rs/esp-wifi/blob/main/examples-esp32/examples/embassy_dhcp.rs

#![no_std]
#![no_main]

extern crate alloc;
pub mod wifi;
pub mod epaper_display_impl;
use embedded_hal_async::{delay::DelayNs, spi::SpiDevice};
use esp_backtrace as _;
use hal::prelude::*;

pub mod epaper_v2;

use hal::spi::master::prelude::*;

#[global_allocator]
static ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty();

static RNG: once_cell::sync::OnceCell<hal::rng::Rng> = once_cell::sync::OnceCell::new();

const ESP_GETRANDOM_ERROR: u32 = getrandom::Error::CUSTOM_START + 1;

const NODE_ID: uuid::Uuid = uuid::uuid!("0827240a-3050-4604-bf3e-564c41c77106");

fn init_heap() {
    const HEAP_SIZE: usize = 32 * 1024;
    static mut HEAP: core::mem::MaybeUninit<[u8; HEAP_SIZE]> = core::mem::MaybeUninit::uninit();

    unsafe {
        ALLOCATOR.init(HEAP.as_mut_ptr() as *mut u8, HEAP_SIZE);
    }
}

#[entry]
fn main() -> ! {
    esp_println::logger::init_logger(log::LevelFilter::Info);
    esp_println::println!("Start Edgeless Embedded.");

    // https://github.com/esp-rs/esp-template/blob/main/src/main.rs
    init_heap();

    let peripherals = hal::peripherals::Peripherals::take();
    #[allow(unused_variables)]
    let io = hal::gpio::Io::new(peripherals.GPIO, peripherals.IO_MUX);
    let system = hal::system::SystemControl::new(peripherals.SYSTEM);

    let clocks = hal::clock::ClockControl::max(system.clock_control).freeze();
    let timer_group0 = hal::timer::timg::TimerGroup::new_async(peripherals.TIMG0, &clocks);
    let timer_group1 = hal::timer::timg::TimerGroup::new(peripherals.TIMG1, &clocks, None);

    esp_hal_embassy::init(&clocks, timer_group0);

    let dma = hal::dma::Dma::new(peripherals.DMA);
    let dma_channel = dma.spi2channel;
    let (mut descriptors, mut rx_descriptors) = hal::dma_descriptors!(32000);

    static DESCRIPTORS : static_cell::StaticCell<[hal::dma::DmaDescriptor; 8]> = static_cell::StaticCell::new();
    let desc = DESCRIPTORS.init_with(|| descriptors);

    static RX_DESCRIPTORS : static_cell::StaticCell<[hal::dma::DmaDescriptor; 8]> = static_cell::StaticCell::new();
    let rx_desc = RX_DESCRIPTORS.init_with(|| rx_descriptors);

    let spi = hal::spi::master::Spi::new(peripherals.SPI2, 100u32.kHz(), hal::spi::SpiMode::Mode0, &clocks)
    .with_sck(io.pins.gpio18)
    .with_mosi(io.pins.gpio23);

    let spi_dma = spi.with_dma(
        dma_channel.configure_for_async(false, desc, rx_desc, hal::dma::DmaPriority::Priority0)
    );

    let display_pin = hal::gpio::Output::new(io.pins.gpio5,  hal::gpio::Level::Low);

    let spi_mutex = embassy_sync::mutex::Mutex::new(spi_dma);

    static SPI_MUTEX : static_cell::StaticCell<embassy_sync::mutex::Mutex<
        embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex,
        hal::spi::master::dma::SpiDma<
        'static,
        hal::peripherals::SPI2,
        hal::dma::Spi2DmaChannel,
        hal::spi::FullDuplexMode,
        hal::Async
    >,
    >> = static_cell::StaticCell::new();

    let spi_mutex = SPI_MUTEX.init_with(|| spi_mutex);

    let mut spi_dev = embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice::new(spi_mutex, display_pin);
    let busy_pin = hal::gpio::Input::new(io.pins.gpio4, hal::gpio::Pull::None);
    let dc_pin = hal::gpio::Output::new(io.pins.gpio17, hal::gpio::Level::High);
    let rst_pin = hal::gpio::Output::new(io.pins.gpio16, hal::gpio::Level::High);
    let mut epaper_delay = embassy_time::Delay{};

    static SPI_DEV: static_cell::StaticCell<embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice<
            'static,
            embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex,
                hal::spi::master::dma::SpiDma<
                    'static,
                    hal::peripherals::SPI2,
                    hal::dma::Spi2DmaChannel,
                    hal::spi::FullDuplexMode,
                    hal::Async
                >,
                hal::gpio::Output<
                    'static,
                    hal::gpio::GpioPin<5>
                >,
        >> = static_cell::StaticCell::new();

    let spi_dev = SPI_DEV.init_with(|| spi_dev);

    static EXECUTOR_RAW: static_cell::StaticCell<esp_hal_embassy::Executor> = static_cell::StaticCell::new();
    let executor = EXECUTOR_RAW.init_with(|| esp_hal_embassy::Executor::new());

    executor.run(|spawner| {
        spawner.spawn(async_main(
            spi_dev,
            busy_pin,
            dc_pin,
            rst_pin,
            epaper_delay
        ));
    });

    #[allow(unreachable_code)]
    loop {}
}

#[embassy_executor::task]
async fn async_main (
    spi: &'static mut embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice<
            'static,
            embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex,
            hal::spi::master::dma::SpiDma<
                'static,
                hal::peripherals::SPI2,
                hal::dma::Spi2DmaChannel,
                hal::spi::FullDuplexMode,
                hal::Async
            >,
            hal::gpio::Output<
                'static,
                hal::gpio::GpioPin<5>
            >,
        >,
    mut busy: hal::gpio::Input<'static, hal::gpio::Gpio4>,
    mut dc: hal::gpio::Output<'static, hal::gpio::Gpio17>,
    mut rst: hal::gpio::Output<'static, hal::gpio::Gpio16>,
    mut delay: embassy_time::Delay
) {
    log::info!("Start");

    log::info!("Reset");
    rst.set_high();
    delay.delay_ms(10).await;
    rst.set_low();
    delay.delay_ms(10).await;
    rst.set_high();
    delay.delay_ms(200).await;

    log::info!("Wait Idle");
    while busy.is_high() {
        delay.delay_ms(10).await;
    }

    log::info!("SPI DC");

    dc.set_low();

    log::info!("SPI Write");

    log::info!("{:?}", spi.write(&[18]).await);

    log::info!("SPI_WRITE_DONE");
}

Working Sync Version:

// SPDX-FileCopyrightText: © 2023 Technical University of Munich, Chair of Connected Mobility
// SPDX-License-Identifier: MIT
// Based on https://github.com/esp-rs/esp-hal/blob/main/esp32-hal/examples/embassy_hello_world.rs, https://github.com/esp-rs/esp-template/blob/main/src/main.rs & https://github.com/esp-rs/esp-wifi/blob/main/examples-esp32/examples/embassy_dhcp.rs

#![no_std]
#![no_main]

extern crate alloc;
pub mod wifi;
pub mod epaper_display_impl;
use embedded_hal_async::delay::DelayNs;
use esp_backtrace as _;
use hal::prelude::*;

use embedded_hal::spi::SpiDevice;

pub mod epaper_v2;

// use hal::spi::master::prelude::*;

#[global_allocator]
static ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty();

static RNG: once_cell::sync::OnceCell<hal::rng::Rng> = once_cell::sync::OnceCell::new();

const ESP_GETRANDOM_ERROR: u32 = getrandom::Error::CUSTOM_START + 1;

const NODE_ID: uuid::Uuid = uuid::uuid!("0827240a-3050-4604-bf3e-564c41c77106");

fn init_heap() {
    const HEAP_SIZE: usize = 32 * 1024;
    static mut HEAP: core::mem::MaybeUninit<[u8; HEAP_SIZE]> = core::mem::MaybeUninit::uninit();

    unsafe {
        ALLOCATOR.init(HEAP.as_mut_ptr() as *mut u8, HEAP_SIZE);
    }
}

#[entry]
fn main() -> ! {
    esp_println::logger::init_logger(log::LevelFilter::Info);
    esp_println::println!("Start Edgeless Embedded.");

    // https://github.com/esp-rs/esp-template/blob/main/src/main.rs
    init_heap();

    let peripherals = hal::peripherals::Peripherals::take();
    #[allow(unused_variables)]
    let io = hal::gpio::Io::new(peripherals.GPIO, peripherals.IO_MUX);
    let system = hal::system::SystemControl::new(peripherals.SYSTEM);

    let clocks = hal::clock::ClockControl::max(system.clock_control).freeze();
    let timer_group0 = hal::timer::timg::TimerGroup::new_async(peripherals.TIMG0, &clocks);
    let timer_group1 = hal::timer::timg::TimerGroup::new(peripherals.TIMG1, &clocks, None);

    esp_hal_embassy::init(&clocks, timer_group0);

    let spi = hal::spi::master::Spi::new(peripherals.SPI2, 100u32.kHz(), hal::spi::SpiMode::Mode0, &clocks)
    .with_sck(io.pins.gpio18)
    .with_mosi(io.pins.gpio23);

    let display_pin = hal::gpio::Output::new(io.pins.gpio5,  hal::gpio::Level::Low);

    let mut spi_dev = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, display_pin).unwrap();

    let busy_pin = hal::gpio::Input::new(io.pins.gpio4, hal::gpio::Pull::None);
    let dc_pin = hal::gpio::Output::new(io.pins.gpio17, hal::gpio::Level::High);
    let rst_pin = hal::gpio::Output::new(io.pins.gpio16, hal::gpio::Level::High);
    let mut epaper_delay = embassy_time::Delay{};

    static SPI_DEV: static_cell::StaticCell<embedded_hal_bus::spi::ExclusiveDevice<
                hal::spi::master::Spi<
                    'static,
                    hal::peripherals::SPI2,
                    hal::spi::FullDuplexMode,
                >,
                hal::gpio::Output<
                    'static,
                    hal::gpio::GpioPin<5>
                >,
                embedded_hal_bus::spi::NoDelay,
        >> = static_cell::StaticCell::new();

    let spi_dev = SPI_DEV.init_with(|| spi_dev);

    static EXECUTOR_RAW: static_cell::StaticCell<esp_hal_embassy::Executor> = static_cell::StaticCell::new();
    let executor = EXECUTOR_RAW.init_with(|| esp_hal_embassy::Executor::new());

    executor.run(|spawner| {
        spawner.spawn(async_main(
            spi_dev,
            busy_pin,
            dc_pin,
            rst_pin,
            epaper_delay
        ));
    });

    #[allow(unreachable_code)]
    loop {}
}

#[embassy_executor::task]
async fn async_main (
    spi: &'static mut embedded_hal_bus::spi::ExclusiveDevice<
    hal::spi::master::Spi<
        'static,
        hal::peripherals::SPI2,
        // hal::dma::Spi2DmaChannel,
        hal::spi::FullDuplexMode,
        // hal::Async
    >,
    hal::gpio::Output<
        'static,
        hal::gpio::GpioPin<5>
    >,
    embedded_hal_bus::spi::NoDelay,
>,
    mut busy: hal::gpio::Input<'static, hal::gpio::Gpio4>,
    mut dc: hal::gpio::Output<'static, hal::gpio::Gpio17>,
    mut rst: hal::gpio::Output<'static, hal::gpio::Gpio16>,
    mut delay: embassy_time::Delay
) {
    log::info!("Start");

    log::info!("Reset");
    rst.set_high();
    delay.delay_ms(10).await;
    rst.set_low();
    delay.delay_ms(10).await;
    rst.set_high();
    delay.delay_ms(200).await;

    log::info!("Wait Idle");
    while busy.is_high() {
        delay.delay_ms(10).await;
    }

    log::info!("SPI DC");

    dc.set_low();

    log::info!("SPI Write");

    log::info!("{:?}", spi.write(&[18]));

    log::info!("SPI_WRITE_DONE");
}

Deps:

[dependencies]
log = { version = "0.4", default-features = false }
embedded-hal = "1"
embedded-hal-async = "1"
embedded-hal-bus = {version = "0.2", features=["async"] }
esp-alloc = { version = "0.3.0" }

embassy-executor = { version = "0.5.0", features=["task-arena-size-65536"] }
embassy-sync = { version = "0.5" }
embassy-time = { version = "0.3", features = ["tick-hz-1_000_000", "generic-queue"]}
embassy-net = { version = "0.4", features = ["tcp", "udp", "dhcpv4", "medium-ethernet", "proto-ipv4"] }
embassy-futures = { version =  "0.1" }
embassy-embedded-hal  = {version = "0.1" }

static_cell = { version = "1.2.0" }
heapless = "0.7"
once_cell = {version = "1.18", default-features = false, features = ["critical-section"]}

embedded-svc = { version = "0.25.1", default-features = false, features = [] }

getrandom = { version = "0.2", features = ["custom"] }
uuid = {version= "1.3", default-features = false, features = ["v4"] }

edgeless_embedded = {path = "../edgeless_embedded" }
edgeless_api_core = {path = "../edgeless_api_core" }

smoltcp = { version = "0.11.0", default-features = false, features = [
  "socket",
  "async",
] }

hal = { package = "esp-hal", version="0.18", features = ["async" ] }
esp-backtrace = { version = "0.8.0", features = ["esp32", "panic-handler", "exception-handler", "print-uart"] }
esp-println = { version = "0.6.0", default-features=false, features = ["esp32", "uart", "log", "critical-section"] }
esp-wifi = { version = "0.6", features = ["esp32", "utils", "async", "wifi", "embassy-net"] }

esp-hal-embassy = { version = "0.1.0", features = [
    "time-timg0",            # Compatible with all chips
    # "time-systimer-16mhz", # Compatible with all chips except ESP32 and ESP32-S2
    # "time-systimer-80mhz", # Compatible with ESP32-S2 only
] }
bjoernQ commented 4 months ago

Ok took a while to get your example to compile.

It works for me on ESP32-C3 but I can confirm I see it waiting forever on ESP32

raphaelhetzel commented 4 months ago

Sorry for that, is there anything I can do to simplify it for repro? Edit: Just saw that I did not remove the modules at the top. Sorry for that, I can clean them up if needed.

bjoernQ commented 4 months ago

No problem!

Here is a minimal repro (just change the embassy_spi.rs example to look like this)

//! Embassy SPI
//!
//! Folowing pins are used:
//! SCLK    GPIO0
//! MISO    GPIO2
//! MOSI    GPIO4
//! CS      GPIO5
//!
//! Depending on your target and the board you are using you have to change the
//! pins.
//!
//! Connect MISO and MOSI pins to see the outgoing data is read as incoming
//! data.
//!
//! This is an example of running the embassy executor with SPI.

//% CHIPS: esp32 esp32c2 esp32c3 esp32c6 esp32h2 esp32s2 esp32s3
//% FEATURES: async embassy embassy-time-timg0 embassy-generic-timers

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use esp_backtrace as _;
use esp_hal::{
    clock::ClockControl,
    dma::*,
    dma_descriptors,
    gpio::Io,
    peripherals::Peripherals,
    prelude::*,
    spi::{
        master::{prelude::*, Spi},
        SpiMode,
    },
    system::SystemControl,
    timer::timg::TimerGroup,
};

#[main]
async fn main(_spawner: Spawner) {
    esp_println::println!("Init!");
    let peripherals = Peripherals::take();
    let system = SystemControl::new(peripherals.SYSTEM);
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    let timg0 = TimerGroup::new_async(peripherals.TIMG0, &clocks);
    esp_hal_embassy::init(&clocks, timg0);

    let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);
    let sclk = io.pins.gpio0;
    let miso = io.pins.gpio2;
    let mosi = io.pins.gpio4;
    let cs = io.pins.gpio5;

    let dma = Dma::new(peripherals.DMA);

    #[cfg(any(feature = "esp32", feature = "esp32s2"))]
    let dma_channel = dma.spi2channel;
    #[cfg(not(any(feature = "esp32", feature = "esp32s2")))]
    let dma_channel = dma.channel0;

    let (descriptors, rx_descriptors) = dma_descriptors!(32000);

    let mut spi = Spi::new(peripherals.SPI2, 100.kHz(), SpiMode::Mode0, &clocks)
        .with_pins(Some(sclk), Some(mosi), Some(miso), Some(cs))
        .with_dma(
            dma_channel.configure_for_async(false, DmaPriority::Priority0),
            descriptors,
            rx_descriptors,
        );

    let send_buffer = [0; 5];
    loop {
        esp_println::println!("Sending bytes");
        embedded_hal_async::spi::SpiBus::write(&mut spi, &send_buffer)
            .await
            .unwrap();
        esp_println::println!("Done");
        Timer::after(Duration::from_millis(1_000)).await;
    }
}

Very small writes (< 17 in my tests) will make ESP32 never finish awaiting the write. It works on e.g. ESP32-C3