🎬 Introduction
UART (Universal Asynchronous Receiver/Transmitter) is a communication protocol used for serial communication between two devices. In embedded systems, UART is still commonly used to communicate with other devices such as sensors, displays, and other microcontrollers. In this post, I will work through implementing a simple UART transmitter in Rust at the PAC (peripheral access crate) level. The application will simply send a character repeatedly to a connected host PC.
If you find this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:
Subscribe Now to The Embedded Rustacean
📚 Knowledge Pre-requisites
To understand the content of this post, you need the following:
Basic knowledge of coding in Rust.
Familiarity with the basic template for creating embedded applications in Rust.
Familiarity with UART communication basics.
💾 Software Setup
All the code presented in this post in addition to instructions for the environment and toolchain setup is 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:
Some installation instructions for the different operating systems are available in the Discovery Book.
🛠 Hardware Setup
🧰 Materials
🔌 Connections
There will be no need for external connections. On-board Nucleo-F401RE connections will be utilized and include the following:
- 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
The application in this post is a simple UART transmitter that repeatedly sends the same letter. However, since we are developing at the PAC level, I'd like to cover the steps of configuration and operation ahead of the implementation. Luckily, the reference manual for the STM32F4 provides us with the steps to transmit a character:
Enable the USART by writing the UE bit in USART_CR1 register to 1.
Program the M bit in USART_CR1 to define the word length.
Program the number of stop bits in USART_CR2.
Select the desired baud rate using the USART_BRR register.
Set the TE bit in USART_CR1 to enable the transmitter (sends an idle frame as the first transmission).
Write the data to send in the USART_DR register (this clears the TXE bit). Repeat this for each data to be transmitted.
After writing the last data into the USART_DR register, wait until TC=1. This indicates that the transmission of the last frame is complete.
📝 Note: I omit steps that are unneeded for the application in this post. This includes setting up a DMA and interrupts.
However, ahead of the above, we need to make sure that we configure the clocks and the pins correctly.
👨💻 Code Implementation
📥 Crate Imports
In this implementation, three crates are required as follows:
The
cortex_m_rt
crate for startup code and minimal runtime for Cortex-M microcontrollers.The
cortex_m
crate to import the Cortex-m peripheral API.The
panic_halt
crate to define the panicking behavior to halt on panic.The
stm32f4xx_pac
crate to import the STM32F401 microcontroller device PAC API that was created in the first post in this series.
use cortex_m_rt::entry;
use cortex_m;
use panic_halt as _;
use stm32f401_pac as pac;
🎛 Peripheral Configuration Code
1- Obtain a Handle for the Device and Core Peripherals: As we always do and part of the singleton pattern in embedded Rust, this needs to be done before accessing any peripheral or core register. Here I create a device peripheral handler named dp
and core peripheral handler named cp
as follows:
let dp = pac::Peripherals::take().unwrap();
Using this handle, I will be accessing the peripherals of the device.
2- Configure System Clocks: I simplified the clock configuration from the last post where I only use the internal HSI oscillator. The internal HSI oscillator has a value of 16 MHz.
// Enable HSI Oscillator
dp.RCC.cr.modify(|_, w| w.hsion().set_bit());
// Wait for HSI clock to become ready
while dp.RCC.cr.read().hsirdy().bit() {}
3- Enable Clocks to GPIO and USART Peripherals: Ahead of using any peripherals, their clocks need to be enabled. This applies to both GPIO and UART. In the earlier GPIO post, we've seen that the GPIOA clock is enabled through the gpioaen
field in the RCC_AHB1ENR
register. On the other hand, USART2 is enabled through the usart2en
field in the RCC_APB1ENR
register.
// Enable Clock to GPIOA
dp.RCC.ahb1enr.write(|w| w.gpioaen().set_bit());
// Enable Clock to USART2
dp.RCC.apb1enr.write(|w| w.usart2en().set_bit());
4- Configure the USART Tx Output Pin: To operate PA2 as a UART transmit pin, it needs to be configured as an alternate output. Additionally, the alternate function needs to be configured to connect to USART2. The PA2 pin is configured as an alternate output in the GPIOA_MODER
register. Further, to select the alternate function, we need to configure the afrl2
field in the GPIOA_AFRL
alternate function register. According to the datasheet, afrl2 needs to be configured with a value of 7 to connect it to USART2.
// Configure PA2 as Alternate Output
dp.GPIOA.moder.modify(|_, w| unsafe { w.moder2().bits(2) });
// Select Alternate Function for PA2
dp.GPIOA.afrl.modify(|_, w| unsafe { w.afrl2().bits(7) });
📝 Note: In cases where
modify
is used instead ofwrite
, this is becausewrite
resets the fields that are not explicitly set. This means if you want to chnage particular bits in a regsiter without affecting others, you need to usemodify
rather thanwrite
.
4- Enable USART2: This is done by asserting the UE bit in the USART2_CR1
register.
// Enable USART2 by setting the UE bit in USART_CR1 register
dp.USART2.cr1.modify(|_, w| { w.ue().set_bit()});
5- Define the Word Length in USART_CR1: The M-bit in the CR1 provides two options for word length either a 1 start bit, 8 data bits, n stop bits option (the default) or a 1 start bit, 9 data bits, n stop bits. Since I want 8 bits of data, nothing needs to be changed here.
6- Program the Number of Stop Bits in USART_CR2: This is done through the STOP field in CR2 which is 2-bits wide. The default configuration 0b00
reflects 1 stop-bit operation. Again, nothing needs to be changed here.
7- Select the Desired Baud Rate: this is done using the USART_BRR
register and is a bit more involved. The BRR register actually has two fields that represent the USART divide factor USARTDIV
; one is the DIV_Mantissa
which is 12-bits wide and the other is the DIV_Fraction
which is 4-bits wide.
The reference manual actually provides a formula to calculate these values for a desired baud rate:
where fclk is the USART clock frequency, OVER8
indicates the setting for oversampling. Since USARTDIV
has 4 fractional bits, the equation can be adjusted as follows:
OVER8
will be 1 in our case indicating oversampling by a factor of 16. In that case, the common factor can be eliminated and we end up with the following:
// Program the UART Baud Rate
dp.USART2.brr.write(|w| unsafe { w.bits(FREQ / BAUD) });
I defined FREQ
and BAUD
earlier in the code as constants representing the USART clock frequency (supplied by PCLK1 through HSI) and the desired baud rate, respectively.
const FREQ: u32 = 16_000_000;
const BAUD: u32 = 115_200;
8- Enable the Transmitter: This is done by setting the TE bit in USART2_CR1 as follows:
// Enable the Transmitter
dp.USART2.cr1.modify(|_, w| w.te().set_bit());
📱 Application Code
In the application code, we're going to set up a loop that will send the character 'A' repeatedly. For that, we need to write the data we want to send in the USART_DR
register. After that, we need to wait for the TC
field in the USART_SR
status register to go high indicating transmission completion, then loop.
loop {
// Put Data in Data Register
dp.USART2.dr.write(|w| unsafe { w.dr().bits(b'A' as u16) });
// Wait for data to get transmitted
while dp.USART2.sr.read().tc().bit_is_clear() {}
}
📀 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 Nucleo-F401RE git repo.
#![no_std]
#![no_main]
// Imports
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f401_pac as pac;
const FREQ: u32 = 16_000_000;
const BAUD: u32 = 115_200;
#[entry]
fn main() -> ! {
// Setup handler for device peripherals
let dp = pac::Peripherals::take().unwrap();
// Enable HSI Oscillator
dp.RCC.cr.modify(|_, w| w.hsion().set_bit());
// Wait for HSI clock to become ready
while dp.RCC.cr.read().hsirdy().bit() {}
// Enable Clock to GPIOA
dp.RCC.ahb1enr.write(|w| w.gpioaen().set_bit());
// Enable Clock to USART2
dp.RCC.apb1enr.write(|w| w.usart2en().set_bit());
// Select Alternate Function for PA2
dp.GPIOA.afrl.modify(|_, w| unsafe { w.afrl2().bits(7) });
// Configure PA2 as Alternate Output
dp.GPIOA.moder.modify(|_, w| unsafe { w.moder2().bits(2) });
// Enable USART2 by setting the UE bit in USART_CR1 register
dp.USART2.cr1.reset();
dp.USART2.cr1.modify(|_, w| {
w.ue().set_bit() // USART enabled
});
// Program the UART Baud Rate
dp.USART2.brr.write(|w| unsafe { w.bits(FREQ / BAUD) });
// Enable the Transmitter
dp.USART2.cr1.modify(|_, w| w.te().set_bit());
// Wait until TXE flag is set
while dp.USART2.sr.read().txe().bit_is_clear() {}
loop {
// Put Data in Data Register
dp.USART2.dr.write(|w| unsafe { w.dr().bits(b'A' as u16) });
// Wait for data to get transmitted
while dp.USART2.sr.read().tc().bit_is_clear() {}
}
}
🔬 Further Experimentation/Ideas
- Configure the board to receive a character from the host PC and echo it back.
Conclusion
In this post, Rust code was developed to transmit a letter using the STM32 device UART peripheral exclusively at the peripheral access crate (PAC) level. The application was developed for an STM32F401RE microcontroller deployed on the Nucleo-F401RE development board. Have any questions? Share your thoughts in the comments below 👇.
Top comments (0)