This blog post is the second of a multi-part series where I will explore various peripherals of the STM32 microcontroller using the embedded Rust embassy framework.
Prior Posts (in order of publishing):
Introduction
Setting up UART serial communication is useful for any type of device-to-device (point-to-point) communication. On of the common examples is printing output to a PC. In this post, I will be configuring and setting up UART communication with a PC terminal using embassy for an STM32 device. I will build on the application from the GPIO button-controlled blinking post to print to the output how many times a GPIO button has been pressed.
๐ For those interested in seeing how embassy compares to other HAL frameworks, you can check out how I created the same application using the stm32f4xx-hal here.
๐ Knowledge Pre-requisites
To understand the content of this post, you need the following:
- Basic knowledge of coding in Rust.
- Knowledge of
async/await
andFutures
. - Familiarity with the basic template for creating embedded applications in Rust.
- Familiarity with interrupts in Cortex-M processors.
- Familiarity with UART communication basics.
๐พ Software Setup
All the code presented in this post in addition to instructions for the environment and toolchain setup are available on the apollolabsdev Nucleo-F401RE git repo. Note that if the code on the git repo is slightly different then it means that it was modified to enhance the code quality or accommodate any HAL/Rust updates.
In addition to the above, you would need to install some sort of serial communication terminal on your host PC. Some recommendations include:
For Windows:
For Mac and Linux:
Apart from Serial Studio, some detailed instructions for the different operating systems are available in the Discovery Book.
For me, Serial Studio comes highly recommended. I personally came across Serial Studio recently and found it to be awesome for two main reasons. First is that you can skip many of those instructions for other tools, especially in Mac and Linux systems. Second, if you are you want to graph data over UART, it has a really nice and easy-to-configure setup. It's also open-source and free to use.
๐ Hardware Setup
Materials
๐ Connections
There will be no need for external connections. On-board connections will be utilized and include the following:
- LED is connected to pin PA5 on the microcontroller. The pin will be used as an output.
- User button connected to pin PC13 on the microcontroller. The pin will be used as input.
- The UART Tx line that connects to the PC through the onboard USB bridge is via pin PA2 on the microcontroller. This is a hardwired pin, meaning you cannot use any other for this setup. Unless you are using a different board other than the Nucleo-F401RE, you have to check the relevant documentation (reference manual or datasheet) to determine the number of the pin.
๐จโ๐จ Software Design
For the purposes of this post, there isn't much to change from an algorithmic perspective compared to the last post. However, the tasks done would differ in one of the state. Meaning, I will still cycle through several LED blinking frequencies based on a button press but do something different in the button press state. As such, the application algorithm representation looks the same:
Although this can be reflected on the chart, the button press behavior can be contained in the button press state. Consequently, the button press state behaviour looks something like this:
The difference is that now there is a count maintained every time we enter the button press state. Also before exiting the button press state, the count is transmitted over UART.
Let's now jump into implementing this algorithm.
๐จโ๐ป Code Implementation
๐ Setting up an Embassy Project:
Although I followed the embassy documentation to set up my first project, it wasn't all smooth sailing. Issues mainly had to do with configurations/settings of configuration (toml) files. It wasn't all too clear what had to be done without needing to ask. I figure this has to do with embassy still being considered unstable. If using the STM32F401RE-Nucleo, the easiest path would be to clone the project from the apollolabsdev git repo.
๐ฅ Crate Imports
In this implementation the crates required are as follows:
- The
core::fmt::Write
crate will allow us to use thewriteln!
macro for easy printing. - The
core::sync::atomic
crate to import theAtomic
sharing type. - The
embassy_executor
crate to import the embassy embedded async/await executor. - The
embassy_time
crate to import timekeeping capabilities. - The
embassy_stm32
crate to import the embassy STM32 series microcontroller device hardware abstractions. The needed abstractions are imported accordingly. - The
panic_halt
crate to define the panicking behavior to halt on panic.
use core::fmt::Write;
use core::sync::atomic::{AtomicU32, Ordering};
use embassy_executor::Spawner;
use embassy_stm32::dma::NoDma;
use embassy_stm32::exti::ExtiInput;
use embassy_stm32::gpio::{AnyPin, Input, Level, Output, Pin, Pull, Speed};
use embassy_stm32::usart::{Config, UartTx};
use embassy_time::{Duration, Timer};
use panic_halt as _;
๐ Global Variables
This part remains the same as before. The AtomicU32
type is used to safely share the delay value BLINK_MS
among several tasks:
static BLINK_MS: AtomicU32 = AtomicU32::new(0);
๐ก The LED Blinking Task
This task is the one that manages the LED blinking (switches between the LED blinking states). It is a task that is spawned by the main task in the device config/setup phase, ahead of the main task entering its continuous loop. As indicated in the software design section, the only changes in the code affect the button press state. The steps taken previously in this task included:
- Create a blinking task and handle for the LED GPIO pin.
- Define the task loop.
As such, nothing needs to be changed here and the code remains the same as before:
#[embassy_executor::task]
async fn led_task(led: AnyPin) {
// Configure the LED pin as a low-speed output and obtain a handler
// Initialize the LED output to Low
let mut led = Output::new(led, Level::Low, Speed::Low);
loop {
let del = BLINK_MS.load(Ordering::Relaxed);
Timer::after(Duration::from_millis(del.into())).await;
led.toggle();
}
}
๐ฑ The Main Task (Button Press Task)
๐ Peripheral Configuration & Task Spawning
The start of the main task is marked by the following code:
#[embassy_executor::main]
async fn main(spawner: Spawner)
As indicated before, the main task will be where we manage the button press and its count. The following steps will mark the tasks performed in the main task:
1๏ธโฃ Initialize MCU and obtain a handle for the device peripherals: A device peripheral handler p
is created:
let p = embassy_stm32::init(Default::default());
Here we're just passing the default value for the Config
type.
2๏ธโฃ Obtain a handle and configure the input button: The on-board user push button on the Nucleo-F401RE is connected to pin PC13 (Pin 13 Port C) as stated earlier. A button
handle for the input button is created with the following line of code:
let button = Input::new(p.PC13, Pull::None);
3๏ธโฃ Attach the interrupt to the input button:
At this point, the button
instance is just an input pin. In order to make the device respond to push button interrupts, button
needs to be attached to one of the device's external interrupt inputs. Using the EXTI
external interrupt input driver and knowing from the datasheet that PC13 attaches to EXTI13, the interrupt is attached as follows:
let mut button = ExtiInput::new(button, p.EXTI13);
4๏ธโฃ Publish Delay Value and Spawn LED Blinking Task: before entering the button press loop, we're going to need to kick off our LED blinking task. Though ahead of that, we need to also provide an initial value for the delay and update it to the global context:
let mut del_var = 2000;
BLINK_MS.store(del_var, Ordering::Relaxed);
Then led_task
is kicked off using the spawn
method:
spawner.spawn(led_task(p.PA5.degrade())).unwrap();
5๏ธโฃ Configure UART and obtain handle: Looking into the Nucleo-F401RE board pinout, the Tx line pin PA2 connects to the USART2 peripheral in the microcontroller device. As such, this means we need to configure USART2 and somehow pass it to the handle of the pin we want to use. Under embassy_stm32::usart::UartTx
there exists a new
method to configure UART with the following signature:
pub fn new(
_inner: impl Peripheral<P = T> + 'd,
tx: impl Peripheral<P = impl TxPin<T>> + 'd,
tx_dma: impl Peripheral<P = TxDma> + 'd,
config: Config
) -> Self
Where inner
expects an argument passing in a UART peripheral instance, tx
a Pin
instance for the UART Tx pin, tx_dma
an instance for a DMA channel, and config
a Config
configuration struct to configure UART.
This results in the following line of code:
let mut usart = UartTx::new(p.USART2, p.PA2, NoDma, Config::default());
Here USART2
is passed since it corresponds in the STM32F401 to pin PA2
which is also passed as a second parameter. PA2
is also the pin that connects to the onboard USB bridge.
Finally, I create a value
variable to track the number of counts, and a String
type msg
handle to store the formatted text that will be transmitted over UART.
let mut value: u8 = 0;
let mut msg: String<8> = String::new();
Next, we can move on to the application Loop.
๐ UART with Interrupts:
The UART can actually be configured easily with interrupts. This would enable us to later use
await
with the UART handle. However, in embassy, it seems to require that UART is attached to a DMA channel an interrupt is going to be attached. Setting up DMA in embassy turns out to be quite smooth actually. THough I leave the DMA topic to a different post.
๐ Main Task Loop
Following the design described earlier in the state machine, the button press will be the only event that is interrupt-based. However, using embassy, events from interrupts or otherwise are managed through Futures
. This means that execution can be yielded to allow other code to progress until the event attached to a Future
occurs. There is an executor that manages all of this in the background and more detail about it is provided in the documentation.
Given that, in the main task loop all we need to do is wait for a button press to occur. This is done through the following line:
button.wait_for_rising_edge().await;
wait_for_rising_edge
is an ExtiInput
method that returns a Future
as well. This is a Future
that is attached to the interrupt event of a button press. This can be read as the task yielding execution to the executor until a rising edge occurs on the button.
This means that once a button press occurs, the code following the above line will execute. The following code adjusts the delay value and updates the global context:
del_var = del_var - 300_u32;
if del_var < 500_u32 {
del_var = 2000_u32;
}
BLINK_MS.store(del_var, Ordering::Relaxed);
Next, the counter message is prepared using the writeln!
macro, and the message is transmitted using the blocking_write
method from embassy_stm32::usart::UartTx
:
core::writeln!(&mut msg, "{:02}", value).unwrap();
usart.blocking_write(msg.as_bytes()).unwrap();
๐ Notice that here that this is a blocking write for UART. This means that this line will block operations, not allowing any others to execute until it completes. For non-blocking UART, there is an
async
method, however it requires that interrupts are configured for UART.
Finally, the push button counter value is recorded by incrementing value
and msg
is cleared for the next transmit.
value = value.wrapping_add(1);
msg.clear();
This concludes the code for the full application.
๐ฑ Full Application Code
Here is the full code for the implementation described in this post. You can additionally find the full project and others available on the apollolabsdev git repo.
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
use core::fmt::Write;
use core::sync::atomic::{AtomicU32, Ordering};
use heapless::String;
use embassy_executor::Spawner;
use embassy_stm32::dma::NoDma;
use embassy_stm32::exti::ExtiInput;
use embassy_stm32::gpio::{AnyPin, Input, Level, Output, Pin, Pull, Speed};
use embassy_stm32::usart::{Config, UartTx};
use embassy_time::{Duration, Timer};
use panic_halt as _;
static BLINK_MS: AtomicU32 = AtomicU32::new(0);
#[embassy_executor::task]
async fn led_task(led: AnyPin) {
// Configure the LED pin as a low -speed output and obtain a handler
// Initialize LED output to Low
// On the Nucleo FR401 theres an on-board LED connected to pin PA5
let mut led = Output::new(led, Level::Low, Speed::Low);
loop {
let del = BLINK_MS.load(Ordering::Relaxed);
Timer::after(Duration::from_millis(del.into())).await;
led.toggle();
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
// Initialize and create handle for devicer peripherals
let p = embassy_stm32::init(Default::default());
// Configure the button pin (if needed) and obtain handler.
// On the Nucleo FR401 there is a button connected to pin PC13.
let button = Input::new(p.PC13, Pull::None);
let mut button = ExtiInput::new(button, p.EXTI13);
//Configure UART
let mut usart = UartTx::new(p.USART2, p.PA2, NoDma, Config::default());
// Create and initialize a delay variable to manage delay loop
let mut del_var = 2000;
// Publish blink duration value to global context
BLINK_MS.store(del_var, Ordering::Relaxed);
// Spawn LED blinking task
spawner.spawn(led_task(p.PA5.degrade())).unwrap();
// Variable to keep track of how many button presses occured
let mut value: u8 = 0;
// Create empty String for message
let mut msg: String<8> = String::new();
loop {
// Check if button got pressed
button.wait_for_rising_edge().await;
// If button pressed decrease the delay value
del_var = del_var - 300;
// If updated delay value drops below 300 then reset it back to starting value
if del_var < 500 {
del_var = 2000;
}
// Publish updated delay value to global context
BLINK_MS.store(del_var, Ordering::Relaxed);
// Format Message
core::writeln!(&mut msg, "{:02}", value).unwrap();
// Transmit Message
usart.blocking_write(msg.as_bytes()).unwrap();
// Update Value Parameter
value = value.wrapping_add(1);
// Clear String for next message
msg.clear();
}
}
Conclusion
In this post, a UART-based application was created using Rust on the Nucleo-F401RE development board. All code was created leveraging the embassy framework for STM32. It is also shown in this post how global variables can be created and shared among tasks. It still remains that leveraging embassy provides for a smoother sail compared to other existing approaches. Have any questions/comments? Share your thoughts in the comments below ๐.