Skip to main content
  1. posts/

Arduino Uno R4 Rust - Part 2

·17 mins
Better serial on the UNO R4 with rust.

  Source Code

I have a PCB specifically for the UNO R4 that has an RS232 transceiver, CAN transceiver, an SD card and some analog signal conditioning. It makes a decent platform for interfacing with devices like motor controllers via CAN or RS232, sampling analog sensors and logging data, as well as interfacing with any I2C or SPI devices.

I am aiming to get all these peripherals working from Rust, ideally with some helpful abstractions over the underlying registers.

alt text

RS232 #

Given the previous success of getting UART to work, I thought I would try RS232. The MAX3221 transceiver I am using is connected to D0 and D1 and I have previously had it working via the Arduino IDE, so I didn’t anticipate any problems…

Problems #

The serial Tx pin works fine but there is no output on the MAX3221 Tx pin. First thought is i’ve fried it somehow. Probing around the board, I discover that the 3.3V supply to the IC is about 0.5V. Hmm, maybe there is a short somewhere.

Removing the PCB from the Arduino, the 3.3 V pin is still not 3.3 V. Uh-oh, maybe the LDO on the Arduino is dead.

Checking the schematics, it turns out the LDO is part of the MCU. This is promising as maybe it just needs enabling. Checking the datasheet:

alt text

Relief! Probably haven’t fried anything. Just need to enable the LDO output:

p.USBFS.usbmc.write(|w| w.vdcen()._1());

After flashing, the 3.3 V pin was indeed 3.3 V. However, this was in the bootloader. After resetting and entering my program, the voltage slowly dropped backed to 0.

So clearly the bootloader successfully enabled the LDO output, then disabled it on exit. What else could it be?

Its because the USB module is disabled 🤦. I always forget this, especially the RCC peripheral enable registers on STM32 devices.

alt text

alt text

So now it works with:

p.MSTP.mstpcrb.modify(|_, w| {   
    w.mstpb11()._0()
});
p.USBFS.usbmc.write(|w| w.vdcen()._1());

Here is the RS232 TX pin trace:

alt text

DTE and DCE #

The male DB9 connector on the PCB is wired as an RS232 DTE (Data Terminal Equipment), the idea being that it would communicate with a device with female connector wired as a DCE (Data Communication Equipment).

However, to talk to a PC via a USB-RS232 adaptor with DTE male connector requires a cross-over cable. Luckily I found one and its working!

serial monitor gif

RX #

I mentioned receiving data briefly in the last post. The device I am building needs to be able to receive serial commands so this is important.

IEL2 is called when a byte is received, which clears the interrupt, fetches the byte and places it in the circular buffer. A call to serial_read can fetch one char at a time from the buffer.

IEL3 is triggered by a receive error, either an overrun, framing or parity error. Without clearing these flags, both the transmit and receive function are disabled, which I found out occurs if the RX pin is left floating. A better handler might check which error has occurred and take action accordingly, rather than just clearing the errors.

struct Rx {
    buffer: circular_buffer::CircularBuffer<64, u8>,
}

#[interrupt]
fn IEL2() {
    // SCI_RXI interrupt handler
    // Clear the interrupt flag
    let p = unsafe { ra4m1::Peripherals::steal() };

    p.ICU.ielsr[2].modify(|_, w| w.ir()._0());
    p.PORT1.podr().write(|w| unsafe { w.bits(0) });

    // Read the received data
    let data = p.SCI2.rdr.read().bits();
    // Put it in the RX buffer
    critical_section::with(|cs| {
        let mut rx = RX.borrow(cs).borrow_mut();
        // Try to push the data to the buffer
        if rx.buffer.try_push_back(data).is_err() {
            // Maybe should set an overrun flag here or something
        }
    });
}

#[interrupt]
fn IEL3() {
    // This is the interrupt for SCI2_ERI
    let p = unsafe { ra4m1::Peripherals::steal() };
    // Clear the interrupt flag
    p.ICU.ielsr[3].modify(|_, w| w.ir()._0());

    // Clear error flags
    p.SCI2
        .ssr()
        .modify(|_, w| w.per()._0().fer()._0().orer()._0());
}


pub fn serial_read() -> Option<char> {    
    critical_section::with(|cs| {
        let mut rx = RX.borrow(cs).borrow_mut();
        // Try to pop a byte from the buffer
        rx.buffer.pop_front().map(|byte| byte as char)
    })
}

The main loop of the application code now looks like this:

loop {
    if let Some(c) = uart::serial_read() {
        // echo the received character as integer
        string.clear();
        writeln!(string, "{}", c as u8).unwrap();
        serial_print(&string);
    }
}

which echos the receive characters back with their integer representation:

serial monitor receive gif

Better UART #

The main aims are:

  • Use a lock-free buffer
  • Implement the embedded_io traits
  • Make the interrupts configurable by the user
  • Be generic over SCI instances

This is mostly based on the embassy-stm32 USART, but with some device specific changes (and also not as complete).

Interrupts #

Embassy uses the bind interrupts macro to inform the HAL that a specified interrupt handler will call a library interrupt function when it is fired. To do this, the macro:

  • Defines the interrupt handler which calls the chosen function which exists on a type implementing the Handler trait.
  • Implements a Binding trait on an Irq struct for a compile time guarantee that the interrupt handler will call the chosen function.

The HAL peripheral can then require the Irq struct with trait bounds for Binding.

The RA4M1 is a little different to STM32 devices as the interrupt handlers are generic and configurable. 32 are available and mappable to any of over 170 peripheral events.

The Handler and Binding traits are defined as:

pub trait Handler {
    unsafe fn on_interrupt(interrupt: Interrupt);
}

pub unsafe trait Binding<H: Handler> {    
    fn interrupt() -> Interrupt;
}

The on_interrupt function now takes an Interrupt enum, which can be used to clear the interrupt in the IELSRx register. Equally, the HAL needs to know the interrupt which the handler is bound to in order to configure it, hence the interrupt() method returning an Interrupt.

The handler that is called on the UART transmit interrupt is defined as:

pub struct TXI_Handler {}

impl Handler for TXI_Handler {
    unsafe fn on_interrupt(interrupt: ra4m1::Interrupt) {
        // clear flags, send data etc...
    }
}

which is bound to e.g the 4th interrupt like this:

bind_interrupts!(struct Irq {
    IEL4 => uart::TXI_Handler;    
});

The macro can be found here. Its a bit long for the post and the easiest way to see what this does is to use cargo expand. The expanded macro invocation looks like:

struct Irq;
#[automatically_derived]
impl ::core::marker::Copy for Irq {}
#[automatically_derived]
impl ::core::clone::Clone for Irq {
    #[inline]
    fn clone(&self) -> Irq {
        *self
    }
}
#[allow(non_snake_case)]
#[unsafe(no_mangle)]
unsafe extern "C" fn IEL4() {
    unsafe {
        <uart::TXI_Handler as crate::interrupts::Handler>::on_interrupt(ra4m1::Interrupt::IEL4)
    };
}
unsafe impl crate::interrupts::Binding<uart::TXI_Handler> for Irq {
    fn interrupt() -> ra4m1::Interrupt {
        ra4m1::Interrupt::IEL4
    }
}

The definition of the IEL4 handler calls the specified Handler::on_interrupt() and Binding is implemented with interrupt() returning the Interrupt::IEL4 variant

Now a UART type could require that a struct that implements Binding for the required handler is passed on creation, also accessing the specific interrupt that is bound:

struct Uart {}

impl Uart {
    pub fn new<IRQ: Binding<TXI_Handler<T>>>(irq: IRQ,...) {
        let txi: Interrupt = <IRQ as Binding<TXI_Handler<T>>>::interrupt();
    }
}

Aside from the binding guarantees, it has a few nice features:

  • The HAL user is free to choose any interrupt number for each handler and assign a priority to it.
  • The user is free to define interrupt handlers for their own use, rather than the handlers all existing inside a HAL.
  • Each peripheral can have a standard method for assigning, enabling and mapping interrupts.

Generics #

An Instance trait defines the behaviour of an SCI instance, giving access to its registers and to static State. It also defines the event base which is the event ID of the RXI event, with the TXI, TEI, RXE at fixed offsets (shown below).

The sci2::RegisterBlock is used as the generic register type, with other instances converting their own register address into it, since the register layout is the same, just at different addresses. An alternative is to have a SCI type defined in the PAC that all SCI instances are based on, as is achieved in embassy with a different PAC generation process.


/// An SCI UART instance.
pub trait Instance {
    // Get access to the peripheral's register block.
    fn peripheral() -> *const sci2::RegisterBlock;    
    // Event ID of first event in this instance (RXI)
    fn event_base() -> u8;
}

impl Instance for SCI2 {
    fn peripheral() -> *const sci2::RegisterBlock {
        unsafe { &*SCI2::ptr() }
    }    
    fn event_base() -> u8 {
        0xA3
    }
}

alt text

The UART, TXI_Handler and TEI_Handler types are now generic over an Instance and can access the methods through T::peripheral() to return the register block for example.

pub struct UART<T: Instance> {    
    _phantom: core::marker::PhantomData<T>,
}
pub struct TXI_Handler<T: Instance> {
    _phantom: core::marker::PhantomData<T>,
}
pub struct TEI_Handler<T: Instance> {
    _phantom: core::marker::PhantomData<T>,
}

Lock-free Buffer #

Straight from embassy-hal-internal is a RingBuffer used in many of the embassy hal peripherals. It is added as a field in a static State that exists as part of an SCI Instance:

struct State {
    tx_buf: RingBuffer,
}

pub trait Instance {
    // Get access to the peripheral's register block.
    fn peripheral() -> *const sci2::RegisterBlock;
    // Event ID of first event in this instance (RXI)
    fn event_base() -> u8;
    // Get access to state
    fn state() -> &'static State;
}

impl Instance for SCI2 {
    ...
    fn state() -> &'static State {
        static STATE: State = State::new();
        &STATE
    }
    
}

As State is only available by shared reference, mutating the RingBuffer requires interior mutability, and it designed to do so. The generic handlers and UART type above can now access this buffer via T::state()

Bringing it all together #

On initialisation, the UART instance needs to:

  • Map the SCI events to the user chosen interrupt handler
  • Unmask the interrupts in the NVIC
  • Configure all the SCI registers and IO pins
  • Initialise the ring buffer and other state

This looks like:

impl<T: Instance> UART<T> {
    pub fn new<IRQ: Binding<TEI_Handler<T>> + Binding<TXI_Handler<T>>>(
        _instance: T,
        tx_buf: &mut [u8],
        _irq: IRQ,
    ) -> Self {
        let sci = unsafe { &*T::peripheral() };
        let state = T::state();

        // Get interrupt variants for TXE and TEI
        let tei = <IRQ as Binding<TEI_Handler<T>>>::interrupt();
        let txi = <IRQ as Binding<TXI_Handler<T>>>::interrupt();

        // Unmask the interrupts in the NVIC
        unsafe {
            ra4m1::NVIC::unmask(tei);
            ra4m1::NVIC::unmask(txi);
        }
        let p = unsafe { ra4m1::Peripherals::steal() };
        // Event number of RXI
        let event_base = T::event_base();
        // Map event to interrupt handlers
        p.ICU.ielsr[tei as usize].write(|w| unsafe { w.iels().bits(event_base + 2) });
        p.ICU.ielsr[txi as usize].write(|w| unsafe { w.iels().bits(event_base + 1) });

        // Initialise the buffer
        unsafe { state.tx_buf.init(tx_buf.as_mut_ptr(), tx_buf.len()) };
        // Configure the SCI peripheral
        init(&p, sci);

        Self {
            state,
            _phantom: core::marker::PhantomData,
        }
    }
}

The init(&p, sci) call configures the SCI for UART at 115200 baud. In the future, a Config struct will be passed to new() to configure the UART as the user desires. This is also the only part that isn’t generic yet as the IO pins are hardcoded for SCI2.

The TX ready (TXI) interrupt is required to:

  • Clear the interrupt flag
  • Get a byte from the buffer (something has gone wrong if not available)
  • Send the byte to the peripheral
  • If its the last byte, disable TXI interrupts and enable TEI.

The transmit end (TEI) interrupt:

  • Clears the interrupt flag
  • Disables transmission
impl<T: Instance> Handler for TXI_Handler<T> {
    unsafe fn on_interrupt(interrupt: ra4m1::Interrupt) {
        let sci = unsafe { &*T::peripheral() };
        // clear the interrupt flag
        let p = unsafe { ra4m1::Peripherals::steal() };
        p.ICU.ielsr[interrupt as usize].modify(|_, w| w.ir()._0());
        // Grab a byte from the transmit buffer
        let state = T::state();

        // Get read access to buffer
        let mut reader = unsafe { state.tx_buf.reader() };
        let data = reader.pop_slice();

        if !data.is_empty() {
            // Write the byte to the transmit data register
            sci.tdr.write(|w| unsafe { w.bits(data[0]) });
            // Inform the reader that we popped a byte
            reader.pop_done(1);
            // Check the buffer len here not the reader slice as the
            // reader slice may be a single byte at the end of the buffer
            if state.tx_buf.is_empty() {
                // Sent byte but trigger TEI next
                sci.scr().modify(|_, w| w.teie()._1().tie()._0());
            }
        } else {
            // This shouldnt happen, but if it does, disable the TX interrupts
            sci.scr().modify(|_, w| w.tie()._0().teie()._0().te()._0());
        }
    }
}

impl<T: Instance> Handler for TEI_Handler<T> {
    unsafe fn on_interrupt(interrupt: ra4m1::Interrupt) {
        // Clear the interrupt flag
        let p = unsafe { ra4m1::Peripherals::steal() };
        p.ICU.ielsr[interrupt as usize].modify(|_, w| w.ir()._0());
        // Disable the TEI and TX interrupts and end transmission
        let sci = unsafe { &*T::peripheral() };
        sci.scr().modify(|_, w| w.teie()._0().tie()._0().te()._0());
    }
}

Everything is ready to go, but there is no way to get data in the buffer to send it…

Embedded_IO traits #

The embedded_io traits are no-std equivalents of those in std::io. They define methods for reading and writing bytes from a source/sink. Right now, it is the Write trait that is of particular interest. 2 methods are required:

  • The write method writes at least 1 byte and returns the number of bytes written.
  • The flush method blocks until all buffered data has been sent.

The write implementation repeatedly tries to write a slice of data into the buffer. When it succeeds, it ensures that TXI interrupt will fire to pull the data from the buffer, then returns the length of data written.

Whilst there is no space in the buffer, it ensures that transmission is active and waits for an interrupt before checking the buffer again.

fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
    loop {
        let state = self.state;
        let mut writer = unsafe { state.tx_buf.writer() };
        let data = writer.push_slice();
        if !data.is_empty() {
            // Copy data to the buffer
            let len = data.len().min(buf.len());
            data[..len].copy_from_slice(&buf[..len]);
            // Inform the writer that we pushed some data
            writer.push_done(len);
            // Check if transmission is already in progress
            let sci = unsafe { &*T::peripheral() };
            let reg = sci.scr().read();
            // If te is clear, TEI has triggered and we need to start transmission
            if reg.te().bit_is_clear() {
                sci.scr().modify(|_, w| w.tie()._1().teie()._0().te()._1());
            } else if reg.teie().bit_is_set() {
                // final byte is in flight, wait until done then start a new transmission
                // This can't be done in the TEI interrupt handler as it seems
                // to cause a data race and bytes are lost.
                loop {
                    // Wait for the TEI interrupt to be triggered
                    cortex_m::asm::wfi();
                    // Check if the TEI interrupt has been triggered
                    let reg = sci.scr().read();
                    if reg.teie().bit_is_clear() && reg.te().bit_is_clear() {
                        // TEI has been triggered, we can start a new transmission
                        break;
                    }
                }
                // Start transmission
                sci.scr().modify(|_, w| w.tie()._1().teie()._0().te()._1());
            }

            // Return the number of bytes written
            return Ok(len);
        } else {
            // No space in the buffer.
            // Make sure transmission is started
            let sci = unsafe { &*T::peripheral() };
            let reg = sci.scr().read();
            if reg.te().bit_is_clear() {
                sci.scr().modify(|_, w| w.tie()._1().teie()._0().te()._1());
            }
            // Wait for space in the buffer
            cortex_m::asm::wfi();
        }
    }
}

The flush function just waits in a loop until the buffer is empty:

fn flush(&mut self) -> Result<(), Self::Error> {
    loop {
        let state = self.state;
        if state.tx_buf.is_empty() {            
            return Ok(());
        } else {
            // Wait for the buffer to be empty
            cortex_m::asm::wfi();
        }
    }
}

RX and split #

Configuring the UART to receive follows the same pattern, with an RXI_Handler and ERI_Handler to respond to interrupts, similar to presented at the start of the post. RXI tries to put data in the buffer (silently failing if the buffer is full), ERI clears receive errors.

The embedded_io::Read trait has a single required read method that must return at least 1 byte, blocking until it is received. This could be problematic for code that cannot block, so there is also the ReadReady trait for checking if data is available.

These are implemented as so:

impl<T: Instance> embedded_io::Read for UartRx<T> {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
        loop {
            let state = self.state;
            let mut reader = unsafe { state.rx_buf.reader() };
            let data = reader.pop_slice();
            if !data.is_empty() {
                // Copy data to the buffer
                let len = data.len().min(buf.len());
                buf[..len].copy_from_slice(&data[..len]);
                // Inform the reader that we popped some data
                reader.pop_done(len);
                // Return the number of bytes read
                return Ok(len);
            } else {
                // No data in the buffer, wait for more data
                cortex_m::asm::wfi();
            }
        }
    }
}

impl<T: Instance> embedded_io::ReadReady for UartRx<T> {
    fn read_ready(&mut self) -> Result<bool, Self::Error> {
        Ok(!self.state.rx_buf.is_empty())
    }
}

Another nice feature to have is the ability to split the UART into a transmitter and a receiver, which could then be sent to different tasks/threads etc. This is possible as they operate independently, aside from accessing the same registers. There are now 3 user-facing types:

pub struct Uart<T: Instance> {
    tx: UartTx<T>,
    rx: UartRx<T>,
}

pub struct UartTx<T: Instance> {
    state: &'static State,
    _phantom: core::marker::PhantomData<T>,
}

pub struct UartRx<T: Instance> {
    state: &'static State,
    _phantom: core::marker::PhantomData<T>,
}

The Uart type still initialises the interrupts, registers and buffers and is able to split itself into the contained UartTx and UartRx:

impl<T: Instance> Uart<T> {
    pub fn new<IRQ>(_instance: T, tx_buf: &mut [u8], rx_buf: &mut [u8], _irq: IRQ) -> Self
    where
        IRQ: Binding<TEI_Handler<T>>
            + Binding<TXI_Handler<T>>
            + Binding<RXI_Handler<T>>
            + Binding<ERI_Handler<T>>,
    {
        let sci = unsafe { &*T::peripheral() };
        let state = T::state();

        // Get interrupts for TXE and TEI
        let tei = <IRQ as Binding<TEI_Handler<T>>>::interrupt();
        let txi = <IRQ as Binding<TXI_Handler<T>>>::interrupt();
        let rxi = <IRQ as Binding<RXI_Handler<T>>>::interrupt();
        let eri = <IRQ as Binding<ERI_Handler<T>>>::interrupt();

        // Unmask the interrupts in the NVIC
        unsafe {
            ra4m1::NVIC::unmask(rxi);
            ra4m1::NVIC::unmask(txi);
            ra4m1::NVIC::unmask(tei);
            ra4m1::NVIC::unmask(eri);
        }
        let p = unsafe { ra4m1::Peripherals::steal() };
        // Event number of RXI
        let event_base = T::event_base();
        // Map events to interrupts
        p.ICU.ielsr[rxi as usize].write(|w| unsafe { w.iels().bits(event_base) });
        p.ICU.ielsr[txi as usize].write(|w| unsafe { w.iels().bits(event_base + 1) });
        p.ICU.ielsr[tei as usize].write(|w| unsafe { w.iels().bits(event_base + 2) });
        p.ICU.ielsr[eri as usize].write(|w| unsafe { w.iels().bits(event_base + 3) });

        // Initialise the buffers
        unsafe { state.tx_buf.init(tx_buf.as_mut_ptr(), tx_buf.len()) };
        unsafe { state.rx_buf.init(rx_buf.as_mut_ptr(), rx_buf.len()) };
        // Configure the SCI peripheral
        init(&p, sci);

        Self {
            tx: UartTx {
                state,
                _phantom: core::marker::PhantomData,
            },
            rx: UartRx {
                state,
                _phantom: core::marker::PhantomData,
            },
        }
    }

    /// Split the Uart into a transmitter and receiver.
    pub fn split(self) -> (UartTx<T>, UartRx<T>) {
        (self.tx, self.rx)
    }
}

The Write trait is implemented on UartTx and as a wrapper on Uart:

impl<T: Instance> embedded_io::Write for UartTx<T> {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
        ...
    }

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

impl<T: Instance> embedded_io::Write for Uart<T> {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
        self.tx.write(buf)
    }

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

Trying it out #

The main.rs file looks like this:

...

bind_interrupts!(struct Irq {
    IEL4 => uart::TXI_Handler<ra4m1::SCI2>;
    IEL5 => uart::TEI_Handler<ra4m1::SCI2>;
    IEL6 => uart::RXI_Handler<ra4m1::SCI2>;
    IEL7 => uart::ERI_Handler<ra4m1::SCI2>;
});

#[entry]
fn main() -> ! {
    // Get access to the peripherals
    let p = unsafe { ra4m1::Peripherals::steal() };

    // Create uart with buffers and interrupt bindings
    let mut tx_buf = [0u8; 64];
    let mut rx_buf = [0u8; 64];
    let mut uart = uart::Uart::new(p.SCI2, &mut tx_buf, &mut rx_buf, Irq);

    // Enable interrupts
    unsafe { cortex_m::interrupt::enable() }

    // Enable usb 3.3V for rs232 converter
    p.MSTP.mstpcrb.modify(|_, w| {
        // Enable USBFS
        w.mstpb11()._0()
    });
    p.USBFS.usbmc.write(|w| w.vdcen()._1());

    // wait for a bit to stabilize the USB power
    cortex_m::asm::delay(1_000_000);

    // Serial should be ready now
    uart.write_all("Hello, RA4M1!\n".as_bytes()).unwrap();

    loop {
        let mut buf = [0u8; 64];
        // Try Read data from the UART
        if uart.read_ready().unwrap() {
            let bytes = uart.read(&mut buf).unwrap();
            // Echo the data back
            for v in &buf[..bytes] {
                // Write the byte to the UART
                uart.write_fmt(format_args!("0x{:02X} ", v)).unwrap();
            }
            uart.write(b"\n").unwrap();
        } else {
            // No data ready, just wait
            cortex_m::asm::wfi();
        }
    }
}

The Write trait only required implementation of write and flush functions, with which is provides write_all to send a whole byte slice to the buffer, and write_fmt to do string formatting directly the UART tx buffer, without an intermediate buffer such as a heapless::String.

serial monitor receive gif

Most of the time it responds fast enough to only pull a single byte from the buffer and print it back.

Conclusion #

This is the core implementation of quite a usable UART HAL for the RA4M1. There are a few things missing to finish it off:

  • Making it actual generic over SCI instances by either automatically configuring IO pins or require the user to supply the correct Pin instances.
  • Adding configuration options for baudrate, parity, frame size etc.
  • Making the Handler traits sealed, as they are currently public and leak a lot of internal types.
  • Improving the interrupt configuration. The interrupt unmasking and event mapping could be implemented on the Interrupt type itself, or a helper function. It is repetitive and will be used by other peripherals. I might need to look at how other HALs implement this.
  • Add the async embedded_io traits.

The state of the repo when I published this post can be found here.

Look out for Part 3, which will likely be all about CAN.