Our experiment with RTFM continues. This week’s project involves USART read/write and Timer interrupt. We will read when new data is available on USART, and send the accumulated data through USART every second.

Hardware

Crates

Code

Full code is available on GitHub

Implementation

Here are three things we need to implement:

  1. initialize USART TX, RX, Timer, and producer/consumer for a global shared buffer and store them for later use.
  2. When new data arrives on USART, enter an interrupt context to read the data and write it to the buffer.
  3. Every second, retrieve available data from the buffer and send them through USART.

Here is the pseudo code:

#[rtfm::app(device = hal::stm32, peripherals = true)]
const APP: () = {
    struct Resources {
        cons: Consumer<'static, U1024>,
        prod: Producer<'static, U1024>,
        tx: hal::serial::Tx<USART3>,
        rx: hal::serial::Rx<USART3>,
        timer: Timer<stm32::TIM2>,
    }

    #[init]
    fn init(cx: init::Context) -> init::LateResources {
        // initialization of resources
		// ...

        init::LateResources {
            cons,
            prod,
            tx,
            rx,
            timer,
        }
    }

    // UART interrupt
    #[task(binds = USART3, resources = [prod, rx])]
    fn usart3(cx: usart3::Context) {
		// read from the RX buffer and write to the queue
    }

    // Timer interrupt 
    #[task(binds = TIM2, resources = [timer, cons, tx])]
    fn tim2(cx: tim2::Context) {
        // read the currently available data from the queue and write to the TX buffer
    }
};

BBQueue: A SPSC, lockless, no_std, thread safe, queue

The challenge is how to write and read the shared buffer. Writing to the buffer occurs in USART ISR contexts and reading from it occurs in Timer ISR contexts. For this project, we use BBQueue, First-In-First-Out queue, to safely perform write/read transactions.

First, we create a large enough buffer. Here is how we create a buffer with 1024 elements.

static BB: BBBuffer<U1024> = BBBuffer(ConstBBBuffer::new());

To access the buffer, we use Producer and Consumer. In init(), we split BBQueue Producer and Consumer and store them in our Resources. We will later use prod (for writing) in the USART ISR and cons (for reading) in the Timer ISR.

#[init]
fn init(cx: init::Context) -> init::LateResources {
	// Set up USART, Timer
	// ...

    // Split bbqueue Producer and Consumer
    let (prod, cons) = BB.try_split().unwrap();

    // Initialization of late resources
    init::LateResources {
        cons,
        prod,
        tx,
        rx,
        timer,
    }
}

USART and Timer Resources

The initialization of USART resources is straightforward. Crate a Serial instance with pins and clocks. When we call listen(SerialEvent::Rxne), our Serial starts listening on RX and triggers an interrupt when a new data arrives. Finally, we split TX and RX so that we can use them in separate contexts.

let gpioc = cx.device.GPIOC.split();
let tx = gpioc.pc10.into_alternate_af7();
let rx = gpioc.pc11.into_alternate_af7();
let mut serial = Serial::usart3(
    cx.device.USART3,
    (tx, rx),
    Config::default().baudrate(9_600.bps()),
    clocks,
)
.unwrap();
// Start listening on RX
serial.listen(SerialEvent::Rxne);
// Split TX and RX
let (tx, rx) = serial.split();

We also initialize a Timer in init() and let it start running.

let mut timer = Timer::tim2(cx.device.TIM2, 1.hz(), clocks);
timer.listen(TimerEvent::TimeOut);

After initializing all the resources, we store them as LateResources .

init::LateResources {
    cons,
    prod,
    tx,
    rx,
    timer,
}

Interrupt Handlers

Now, let’s look at the interrupt handlers. USART3 interrupt is handled like this.

#[task(binds = USART3, resources = [prod, rx])]
fn usart3(cx: usart3::Context) {
    match block!(cx.resources.rx.read()) {
        Ok(byte) => {
            if let Ok(mut wgr) = cx.resources.prod.grant_exact(1) {
                wgr[0] = byte;
                wgr.commit(1);
            }
        }
        Err(error) => {
            iprintln!(itm(), "[RX] Err: {:?}", error);
        }
    }
}

binds = USART3 establishes a link to USART3 hardware interrupt. In this ISR context, we use rx: hal::serial::Rx<USART3> to read a new byte and write to the buffer using prod: bbqueue::Producer.

prod.grant_exact(1) request space for one byte. We write the value of the received byte to it and call commit. Committing returns the space for later use. Next time around, we request another grant and write a new value.

While we accumulate incoming data and put them in the buffer in USART interrupts, we read from the buffer and send the data out through USART’s TX in Timer interrupts.

Here is the TIM2 interrupt handler.

#[task(binds = TIM2, resources = [timer, cons, tx])]
fn tim2(cx: tim2::Context) {
    cx.resources.timer.clear_interrupt(TimerEvent::TimeOut);
    let rgr = match cx.resources.cons.read() {
        Ok(it) => it,
        _ => return,
    };
    
    rgr.buf()
        .iter()
        .for_each(|&byte| block!(cx.resources.tx.write(byte)).unwrap());

    // Release the space for later writes
    let len = rgr.len();
    rgr.release(len);
}

We use cons: bbqueue::Consumer to read the buffer. When the buffer is empty, cons.read() returns Err. If there are bytes to read, we access the data slice by calling buf(). The size of the slice could be anywhere between 1 byte and the size of the buffer. Whatever the size is, it is guaranteed to be contiguous.

We then iterate through it and write to TX. When we are done with writing, we release the space for later writes.