DEV Community

Cover image for STM32F4 Embedded Rust at the PAC: Creating Hardware Abstractions with embedded-hal
Omar Hiari
Omar Hiari

Posted on • Edited on

STM32F4 Embedded Rust at the PAC: Creating Hardware Abstractions with embedded-hal

Introduction

In last week's post, I demonstrated the creation of a simple UART abstraction used to hide register-level details. What is obvious from the prior code is that its behavior is limited to a single UART peripheral. Meaning, that as we create more abstractions for other UART instances, we'll need to duplicate code. Rust provides an alternative through generics and traits. Through generics, a type can be determined at compile time. Traits, on the other hand, provide a way of defining common behavior. Thinking in an embedded context, peripherals among devices share a lot of common traits. For example, serial peripherals always have a Write or Read function. This is where the embedded-hal comes in. It's a community effort crate that was created to define common behavior among controllers through traits.

embedded-hal is a really powerful concept. The existence of embedded-hal enables the implementation of platform-agnostic drivers. This is done by creating a component driver that uses the embedded-hal traits and then deploying the driver on any platform that supports embedded-hal . From a PAC context, to implement the embedded-hal one would have to import the embedded-hal crate and then implement the particular behavior for the different peripherals.

In this post, I will be adjusting the code from last week's post such that the USART abstraction would instead implement the embedded-hal for it's Write function.

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

1- Import the embedded-hal and nb Crates 📥

In the imports need to add embedded_hal and nb . embedded_hal includes all the required traits and nb is a crate that adds blocking operation support (more on this later).

#![no_std]
use embedded_hal as ehal;
use nb;
use stm32f401_pac as pac;
Enter fullscreen mode Exit fullscreen mode

2- Update the Driver Struct 📲

In the previous code, recall the abstraction unit struct Uart2Tx :

pub struct Uart2Tx;
Enter fullscreen mode Exit fullscreen mode

Which now changes to the following:

pub struct SerUart<USART> {
    usart: USART,
}
Enter fullscreen mode Exit fullscreen mode

Note that I modified the struct name to SerUart such that the name is not particular to UART2, but more importantly, I introduced a generic type UART. This potentially would allow the struct to be instantiated to different UART instances.

3- Implement the Write Trait ✍️

Defining the Write trait behavior starts with impl ehal::serial::Write<u8> for SerUart<pac::USART2> . This line of code essentially says that we want to define the behavior of the ehal::serial::Write<u8> trait for the SerUart struct that implements the type pac::USART2 . Within ehal::serial::Write<u8> there are two functions that need to be defined; write and flush .

The seril::Write trait requires us to implement an Error type that is associated with the serial interface. To do that one can enumerate all the possible errors and associate them with the type. Ideally, these would be used to identify the types of errors occurring. For now, I didn't want to implement any error functionality so I created an empty enum and associated it with the Error type:

pub enum Errors {}
impl ehal::serial::Write<u8> for SerUart<pac::USART2> {
    type Error = Errors;
    fn write(&mut self, word: u8) -> nb::Result<(), Self::Error> {
        // Implementation of write 
    }
    fn flush(&mut self) -> nb::Result<(), Self::Error> {
        // Implementation of flush
    }
Enter fullscreen mode Exit fullscreen mode

Following that, I copied over the prior implementation of write to transmit a byte over the UART channel. Additionally, I left the flush implementation empty since I have no use for it in this particular implementation.

pub enum Errors {}

impl ehal::serial::Write<u8> for SerUart<pac::USART2> {
    type Error = Errors;
    fn write(&mut self, word: u8) -> nb::Result<(), Self::Error> {
        // Put Data in Data Register
        self.usart.dr.write(|w| unsafe { w.dr().bits(word as u16) });
        // Wait for data to get transmitted
        while self.usart.sr.read().tc().bit_is_clear() {}
        Ok(())
    }
    fn flush(&mut self) -> nb::Result<(), Self::Error> {
        Ok(())
    }
}
Enter fullscreen mode Exit fullscreen mode

Note how the write and flush functions have a nb::Result type returned. nb is a special type of crate that allows the implementation of the Result enum with additional variants. The purpose of the additional variants is to allow the implementation of blocking (or non-blocking) operations if required. This is mainly to support blocking asynchronous programming models if required.

4- Update the Initialization Function Implementation 💾

Initialization of the UART peripheral remains more or less the same as before. The only minor difference is that the type for SerUart is specified in the implementation:

impl SerUart<pac::USART2> {
    pub fn init(clocks: &pac::RCC, usart: &pac::USART2, cnfg: Config) {
        // Enable Clock to USART2
        clocks.apb1enr.write(|w| w.usart2en().set_bit());

        // Enable USART2 by setting the UE bit in USART_CR1 register
        usart.cr1.reset();
        usart.cr1.modify(|_, w| {
            w.ue().set_bit() // USART enabled
        });

        // Program the UART Baud Rate
        usart
            .brr
            .write(|w| unsafe { w.bits(cnfg.freq / cnfg.baud) });

        // Enable the Transmitter
        usart.cr1.modify(|_, w| w.te().set_bit());

        // Wait until TXE flag is set
        while usart.sr.read().txe().bit_is_clear() {}
    }
}
Enter fullscreen mode Exit fullscreen mode

Thats it! now we have a hardware abstraction that supports embedded-hal!

Conclusion

The embedded-hal is commonly mistaken to be a hardware abstraction layer (HAL) itself although it is not. On the contrary, embedded-hal is a crate that provides a common API that can be adopted among multiple platforms. This enables powerful concepts like platform-agnostic drivers. The embedded-hal crate is used alongside a HAL implementation to define the behavior of particular peripherals through traits. In this post, I modify a previously created USART abstraction layer in Rust for the STM32F4 to adopt the embedded-hal traits.

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

Top comments (0)