Embedded Driver with UART

Hey,

I wanted to write a driver for a lidar sensor in the context of the embedded HAL async. The driver should be passed a UART. It appears that there is nothing for a UART in the embedded HAL async and you have to use embedded IO async. Are there any examples anywhere that can be used for guidance? For I2C, for example, there is quite a lot to be found. In my current project, I am using the embassy_nrf crate for my hardware abstraction. It seems as if no traits from embedded IO async are used in the embassy_nrf UART module. I'm a little confused about what the idiomatic way is to write a driver with UART in the Rust embedded ecosystem.

Thanks in advance.

I played a little around and figured out how to build a nrf generic driver. Its also compiled and seems to work.

use embassy_nrf::uarte::{Uarte, Instance, Error};

pub struct NrfDriver<'d, T> 
where
    T: Instance
{
    uarte: T,
}

impl<'d, T> nrfDriver<'d, T>
where
    T: Instance,
{
    pub fn new(uarte: T) -> Self {
        Self { uarte }
    }

    pub async fn do_something(&mut self) -> Result<(), Error> {
        let buffer = [0x01, 0x02, 0x03];
        self.uarte.write(&buffer).await?;
        Ok(())
    }
}

I also tried to port the driver to embedded_io_async. But the problem is that the uarte from nrf seems to not implement embedded_io_async. So I could not use the generic driver with the Uarte modul from the embassy_nrf crate.

use embedded_io_async::{Read, Write, ErrorType};

pub enum GenericDriverError<E> {
    Peripheral(E),
    NoAnswerReceived,
}

impl<E> From<E> for GenericDriverError <E> {
    fn from(error: E) -> Self {
        GenericDriverError::Peripheral(error)
    }
}

pub struct GenericDriver<T> 
where
    T: Read + Write + ErrorType
{
    uarte: T,
}

impl<T> GenericDriver<T>
where
    T: Read + Write + ErrorType,
{
    pub fn new(uarte: T) -> Self {
        Self { uarte }
    }

    pub async fn do_something(&mut self) -> Result<(), GenericDriverError <T::Error>> {
        let buffer = [0x01, 0x02, 0x03];
        self.uarte.write(&buffer).await?;
        Ok(())
    }
}

The goal is to use the driver on a nrf and also on a stm32 controller, so I want to make the driver controller agnostic. Does anyone have any idea how this could work?

Uarte does not implement Read, but BufferedUarte does. The documentation explains when each should be used.

So far, I haven't had any real problems with the borrow checker, but the type system in Rust is driving me a little crazy right now. How would I transfer a BufferedUarte or a driver instance to an embassy task? If I understand correctly, there are no generic tasks. In other words, the following things cannot be done:

#[embassy_executor::task]
pub async fn task(uarte: BufferedUarte<'static, U, T>) 
where
    U: UarteInstance,
    T: TimerInstance,
{
    let mut driver = GenericDriver::new(uarte);
    ...
}
#[embassy_executor::task]
pub async fn task(driver: GenericDriver<T>) 
where
   T: Read + Write + ErrorType,
{
   ...
}

I define the peripherals in main.

let p = embassy_nrf::init(Default::default());

Even if I initialize bufferedUarte and the driver within the task, I still have to pass a reference to p somehow?

Maybe you just pass GenericDriver to your task?

#[embassy_executor::task]
async fn driver_task(mut driver: GenericDriver<&'static mut BufferedUarte<'static>>) {
    match driver.do_something().await {
        Ok(()) => info!("Success!"),
        Err(_e) => info!("Error"),
    }
}

Then the two generic arguments seem to be missing.

error[E0107]: struct takes 2 generic arguments but 0 generic arguments were supplied
   --> src/tasks/rplidar.rs:5:60
    |
5   | pub async fn rplidar_task(mut driver: GenericDriver <&'static mut BufferedUarte<'static>>) {
    |                                                            ^^^^^^^^^^^^^ expected 2 generic arguments
    |

I think you’re using an older version of the embassy crate. The instance generics were removed in this commit:

If you update your embassy-nrf dependency to a version that includes this change, BufferedUarte<'static> should work.

You need the peripheral to construct the BufferedUart. It has to be moved out of the p returned by init, and into the BufferedUart. You then move the BufferedUart into the task you spawn. The trick to being able to move BufferedUart into the other task is that all task arguments must be 'static, and BufferedUart needs references to tx and rx byte buffers. So those have to be 'static. So I had to:

    // the uart driver needs to be 'static to pass to the other task.
    // this is safe because it is the only reference
    #[allow(static_mut_refs)]
    unsafe {
        static mut TX_BUFFER: [u8; 128] = [0u8; 128];
        static mut RX_BUFFER: [u8; 128] = [0u8; 128];
        usart = BufferedUart::new(
            p.USART1,
            p.PA10,
            p.PA9,
            &mut TX_BUFFER,
            &mut RX_BUFFER,
            Irqs,
            uconf,
        )
        .unwrap();
    }

For the ADC peripheral, I wanted that encapsulated in its own module and struct, so getting the generics right to pass the peripheral into ::new() was tricky, but I ended up with this in main:

    let mut ad = AtoD::new(
        p.ADC1,
        p.ADC4,
        p.PC0.degrade_adc(),
        p.PC1.degrade_adc(),
        p.PC3.degrade_adc(),
        p.GPDMA1_CH0.reborrow(),
        p.GPDMA1_CH1.reborrow(),
    );

And this for the struct in the AtoD module:

pub struct AtoD<'a, D1: RxDma<ADC1>, D2: RxDma<ADC4>> {
    adc1: adc::Adc<'static, peripherals::ADC1>,
    adc4: adc::Adc<'static, peripherals::ADC4>,
    co2_pin: adc::AnyAdcChannel<'static, peripherals::ADC1>,
    ref_pin: adc::AnyAdcChannel<'static, peripherals::ADC1>,
    temp_pin: adc::AnyAdcChannel<'static, peripherals::ADC4>,
    dma1: embassy_stm32::Peri<'a, D1>,
    dma4: embassy_stm32::Peri<'a, D2>,
}

And the impl block got a little hairy with generics too:

impl<'a, D1: RxDma<ADC1>, D2: RxDma<ADC4>> AtoD<'a, D1, D2>
where
    crate::Irqs: embassy_stm32::interrupt::typelevel::Binding<
            <D1 as embassy_stm32::dma::ChannelInstance>::Interrupt,
            embassy_stm32::dma::InterruptHandler<D1>,
        >,
    crate::Irqs: embassy_stm32::interrupt::typelevel::Binding<
            <D2 as embassy_stm32::dma::ChannelInstance>::Interrupt,
            embassy_stm32::dma::InterruptHandler<D2>,
        >,
{
    pub fn new(
        adc1: Peri<'static, peripherals::ADC1>,
        adc4: Peri<'static, peripherals::ADC4>,
        co2_pin: adc::AnyAdcChannel<'static, peripherals::ADC1>,
        ref_pin: adc::AnyAdcChannel<'static, peripherals::ADC1>,
        temp_pin: adc::AnyAdcChannel<'static, peripherals::ADC4>,
        dma1: embassy_stm32::Peri<'a, D1>,
        dma4: embassy_stm32::Peri<'a, D2>,
    ) -> Self {
        let adc1 = adc::Adc::new_with_config(
            adc1,
            adc::AdcConfig {
                resolution: Some(adc::Resolution::BITS14),
                averaging: Some(adc::Averaging::Samples64),
            },
        );
        let adc4 = adc::Adc::new_adc4(adc4);

        AtoD {
            adc1,
            adc4,
            co2_pin,
            ref_pin,
            temp_pin,
            dma1,
            dma4,
        }
    }
}

Okay, thank you, that was the problem. I used the microbit_bsp crate, which uses an old version of the embassy_nrf crate internally. I have now forked it and updated the embassy crates to the latest versions. Then it almost worked. Another problem is that embassy_nrf crate uses version 0.6.1 of embedded_io_async crate. I had used the latest version for my driver. Cargo tree is very helpful :grinning_face:. Now everything is building. It just needs to be tested on the actual hardware.

I got excited too soon. Now I'm actually having problems with the lifetimes of the uarte buffers. As an alternative to the following code, I also tried passing the driver_uarte as a reference. But that didn't work either. Basically, the compiler is telling me that the buffers should be static. But I have no idea what the best way to do this is.

pub struct Driver<T>
where
    T: Read + Write + ErrorType,
{
    uarte: T,
}

impl<T> Driver<T>
where
    T: Read + Write + ErrorType,
{
    pub fn new(uarte: T) -> Self {
        Driver { uarte }
    }

    pub async fn do_something(&mut self) -> Result<(), DriverError<T::Error>> {
        let buffer = [START_FLAG_1, 0x25];
        self.uarte.write(&buffer).await?;
        Ok(())
    }
}
#[embassy_executor::task]
pub async fn driver_task(driver: Driver<BufferedUarte<'static>>) {
    driver.do_something().await.unwrap();

    loop {}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {

    ....

    bind_interrupts!(struct Irqs {
        UARTE0 => buffered_uarte::InterruptHandler<peripherals::UARTE0>;
    });


    static mut tx_buffer: [u8; 18] = [0u8; 18];
    static mut rx_buffer: [u8; 18] = [0u8; 18];

    let driver_uart = BufferedUarte::new(
        peripherals.uarte0,
        peripherals.timer0,
        peripherals.ppi_ch0,
        peripherals.ppi_ch1,
        peripherals.ppi_groub0,
        peripherals.p15,
        peripherals.p16,
        Irqs,
        Config::default(),
        &mut rx_buffer,
        &mut tx_buffer,
    );

    let driver = Driver::new(driver_uart);

    spawner
        .spawn(driver_task(driver))
        .unwrap();
}

Additional question. Why doesn't the normal uarte driver implant embedded_io_async?

The issue is that your buffers and BufferedUarte need 'static lifetimes to be passed into a spawned task. StaticCell gives you a safe &'static mut.

use static_cell::StaticCell;

static TX_BUF: StaticCell<[u8; 18]> = StaticCell::new();
static RX_BUF: StaticCell<[u8; 18]> = StaticCell::new();
static UART: StaticCell<BufferedUarte<'static>> = StaticCell::new();

let tx_buf = TX_BUF.init([0u8; 18]);
let rx_buf = RX_BUF.init([0u8; 18]);

let driver_uart = UART.init(BufferedUarte::new(
    peripherals.uarte0,
    peripherals.timer0,
    peripherals.ppi_ch0,
    peripherals.ppi_ch1,
    peripherals.ppi_groub0,
    peripherals.p15,
    peripherals.p16,
    Irqs,
    Config::default(),
    rx_buf,
    tx_buf,
));

Re: your other question — the non-buffered Uarte driver doesn’t implement embedded_io_async::Read.

The documentation explains:

If the Uarte is required to be awaited on with some other future, for example when using futures_util::future::select, then you should consider crate::buffered_uarte::BufferedUarte so that reads may continue while processing these other futures. If you do not then you may lose data between reads.

If Read were implemented, one would expect it to reliably handle reads. However, due to the async model and hardware limitations, it cannot guarantee lossless reads without buffering. That’s why the buffered variant is necessary in scenarios where other futures may delay read handling.

Neverending Story :smiley: I wanted to add a few unit tests. To do that, I built a UART mock that implements the write and read traits. Now I'm getting a traitbound error that I don't really understand: “the trait core::error::Error is not implemented for UartError” Do I really have to implement core::error::Error for my UartError Type? It seems extremely complicated for what I want to archive.

#[cfg(test)]
mod test {
    use super::*;
    use embedded_io_async::{Error, ErrorKind, ReadExactError};

    struct UartStub {
        ...
    }

    impl UartStub {
        fn new() -> Self {
            Self {
                ...
            }
        }

        ...
    }

    enum UartError {
        Other,
    }

    impl Error for UartError {
        fn kind(&self) -> ErrorKind {
            match *self {
                UartError::Other => ErrorKind::Other,
            }
        }
    }

    impl ErrorType for UartStub {
        type Error = UartError;
    }

    impl Write for UartStub {
        async fn write(&mut self, _buf: &[u8]) -> Result<usize, Self::Error> {
            Ok(0)
        }

        async fn flush(&mut self) -> Result<(), Self::Error> {
            Ok(())
        }

        async fn write_all(&mut self, _buf: &[u8]) -> Result<(), Self::Error> {
            Ok(())
        }
    }

    impl Read for UartStub {
        async fn read(&mut self, _buf: &mut [u8]) -> Result<usize, Self::Error> {
            Ok(0)
        }

        async fn read_exact(
            &mut self,
            mut _buf: &mut [u8],
        ) -> Result<(), ReadExactError<Self::Error>> {
            Ok(())
        }
    }

It depends on how much information you need. If you don’t need fine-grained distinctions, you can simply use ErrorKind, which implements Display, Debug, and core::error::Error.

impl ErrorType for UartStub {
    type Error = ErrorKind;
}