Potential improvements for Rust embedded abstractions

Lars
Software engineer

Recently, we worked on an embedded (STM32) project in Rust and we got some hands-on experience with the abstractions commonly used for that. There's embedded-hal, which offers abstractions related to timing, GPIO pins and common communication peripherals like SPI and USART. There's also multiple stm32xxx-hal crates 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 to embedded-hal and its implementing crates.

DMA channels

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.

Power states

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. The 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 systick peripheral. 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 systick peripheral, 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. The 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.

Stay up-to-date

Stay up-to-date with our work and blog posts?

Related articles

Welcome to the age of communication. It's 2021 and technology has come a long way. People, large machines and small devices communicate more intensively than ever before, and many technologies to enable them to do so have been developed. Some of those technologies use physical pathways like fibreglass to reach their receivers, others use radio signals to send messages. It's these wireless communication technologies that spark the imagination the most.
Rust is nice for a lot of things. At Tweede golf we've been using the language primarily for high-performance web applications. But that's not all Rust can do. Rust can be used to write embedded applications as well.

While using a full-blown filesystem for storing your data in non-volatile memory is common practice, those filesystems are often too big, not to mention annoying to use, for the things I want to do. My solution?

I've been hard at work creating the sequential-storage crate. In this blog post I'd like to go over what it is, why I created it and what it does.