DEV Community

Cover image for STM32F4 Embedded Rust at the PAC: UART Communication
Omar Hiari
Omar Hiari

Posted on • Updated on

STM32F4 Embedded Rust at the PAC: UART Communication

🎬 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

Nucleo Board

🔌 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:

  1. Enable the USART by writing the UE bit in USART_CR1 register to 1.

  2. Program the M bit in USART_CR1 to define the word length.

  3. Program the number of stop bits in USART_CR2.

  4. Select the desired baud rate using the USART_BRR register.

  5. Set the TE bit in USART_CR1 to enable the transmitter (sends an idle frame as the first transmission).

  6. Write the data to send in the USART_DR register (this clears the TXE bit). Repeat this for each data to be transmitted.

  7. 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;
Enter fullscreen mode Exit fullscreen mode

🎛 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();
Enter fullscreen mode Exit fullscreen mode

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() {}
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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) });
Enter fullscreen mode Exit fullscreen mode

📝 Note: In cases where modify is used instead of write, this is because write 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 use modify rather than write.

4- Enable USART2: This is done by asserting the UE bit in the USART2_CR1 register.

CR1 regsiter

// Enable USART2 by setting the UE bit in USART_CR1 register
dp.USART2.cr1.modify(|_, w| { w.ue().set_bit()});
Enter fullscreen mode Exit fullscreen mode

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.

brr register

The reference manual actually provides a formula to calculate these values for a desired baud rate:

TxRxbaud=fclk8×(2OVER8)×USARTDIVTxRx_{baud} = \frac{f_{clk}}{8 \times (2 - \text{OVER8}) \times \text{USARTDIV}}

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:

TxRxbaud=fclk×168×(2OVER8)×USARTDIVTxRx_{baud} = \frac{f_{clk} \times 16}{8 \times (2 - \text{OVER8}) \times \text{USARTDIV}}

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) });
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

📱 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() {}
}
Enter fullscreen mode Exit fullscreen mode

📀 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() {}
    }
}
Enter fullscreen mode Exit fullscreen mode

🔬 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 👇.

If you found 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

Latest comments (0)