Serial Communication with RTFM
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:
- initialize USART TX, RX, Timer, and producer/consumer for a global shared buffer and store them for later use.
- When new data arrives on USART, enter an interrupt context to read the data and write it to the buffer.
- 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.