Recently, we worked on an embedded (STM32) project in Rust
and we got some hands-on experience with
the abstractions commonly used for that.
embedded-hal, which offers abstractions related to
timing, GPIO pins and
common communication peripherals like SPI and USART.
There's also multiple
which offer abstractions over
most of the peripherals of different STM32 CPU families.
Although many of them were nice to use,
we found some parts to be lacking
and we'd like to propose some potential improvements
embedded-hal and its implementing crates.
First, while writing a driver for the SD card peripheral1, we wanted to use the DMA peripheral to transfer data in the background, leaving the CPU available for other tasks. The DMA peripheral consists of several channels which can all be controlled individually, however, they are all controlled through the same register block. In order to prevent other parts from changing the DMA settings for the channel it uses, the SD card driver takes ownership of the entire DMA peripheral. This means none of the other channels can be used anywhere else in the project.
Each DMA channel can only be used for a single peripheral at a time and each peripheral is only connected to some of the DMA channels. Both of these properties can be encoded in Rust's type system by giving every channel its own type. The DMA peripheral can then be split into its component channels and those can be handed out individually to different peripherals.
SPI NCS pin
SPI is one of the most common communication protocols with hardware support in most STM32 chips. It allows controlling multiple devices over mostly the same wires by having a dedicated channel select wire for each device. When the channel select is activated for one device, that device will communicate over the shared wires while other devices will ignore communication.
The current SPI abstraction leaves toggling the NCS pin completely to the user.2 This allows for some flexibility: Some implementations require specific timing between enabling the NCS pin and starting the transmission. It also makes implementations more error prone however. An abstraction that automatically toggles a given channel select pin after a given delay and handles potential errors as well would be very helpful.
Most of the other issues have to do with power management. STM32 chips usually have a number of different power saving states. In order to actually save some power, these states turn off some peripherals. Some peripherals can prevent the chip from entering power saving states altogether, usually because they generate waking interrupts. The hardware abstraction layers currently offer no abstractions to track which peripherals must be turned off or what power states are available.
We think the best way to keep track of those things is with a reference count for each power state. When the HAL is used to initialize a peripheral, it can increment the reference counts of power states it is incompatible with. That way, when power saving is requested, the HAL can automatically determine which power state saves the most power while still compatible with the running peripherals. It could also support accessing information about which power saving states are supported at any given time and which peripherals are preventing certain states.
GPIO power saving
One of the things we wanted to save power on
was the set of pins used for the SD card.
The way to save power on a pin is to set it to analog mode.
stm32l4xx_hal crate does not have that option:
Only pins that aren't explicitly initialized stay in analog mode.
A method to return a pin to analog mode is missing.
We used unsafe code to deinitialize the pins anyway,
which saved us about 0.25 mA or about 10% of our power usage.
Timekeeping in low-power states
One of the last things we missed
is a way to keep track of time and delays
while entering power saving states.
Usually, time is tracked using the
This peripheral is disabled in most power saving states.
We manually reimplemented a bunch of the functionality
based on one of the timer peripherals.
What we'd like to see is a set of timing abstractions
similar to those available for the
but allowing it to be manually ticked instead.
That would make it easy
to use any available timing mechanism
as the source of timekeeping and delays.
embedded-time crate looks to be a decent abstraction
but it does not seem to be in widespread use yet.
Well, those were our thoughts when developing embedded Rust. We'd love to hear what you think over at r/rust. I can also give away that we're recreating some of these abstractions with support for Rust async. If you'd like to hear more about that, stay tuned.