Embedded Rust Frustrations

21 Jan 2019

Part 2 of my custom keyboard project!

I didn't mean for this to become its own post, but it kind of got away from me. I've had a blast getting my feet wet in embedded dev so far with Rust, but it hasn't been without its share of headaches. I'm sure most of my problems can be attributed to my lack of experience in this realm, but hopefully I'll be able to turn my frustrations into something interesting by parts 3 and 4!

Part 1 can be found here: Bootstrapping My Embedded Rust Development Environment

Existing Libraries

In the process of exploring my options for building the firmware for my keyboard , I've seen a ton of awesome stuff from the embedded working group. In particular, I've been quite pleased with the support for the STM32F103 family of boards, and for Cortex-M boards in general. Unfortunately, support for asynchronous programming seems to be a bit lacking, especially in the embedded-hal implementations such as stm32f103xx-hal. While most of the HAL traits return the nb::Error type, which can have a WouldBlock variant, the main way to work with these interfaces is simply to poll them until they quit blocking, which is less than ideal.

HAL Interrupt Frustrations

While there's some interrupt support in the embedded-hal world, it's not quite there yet. For example, the stm32f103xx_hal::serial::Serial type has a listen method that can be used to enable/disable the "receive register not empty" (RXNE) and the "transmit register empty" (TXE) interrupts. The problem is that, in order to be used for actual IO, the Serial struct has to be split() into Tx/Rx parts, which consumes it and makes the listen method unavailable. This presents a problem because the TXE interrupt will fire continuously until its disabled. From my understanding, the intended way for the interrupt to be used is to only enable it while there's buffered data to be transmitted, and then disable it once everything has been written. This doesn't appear to be possible with the current HAL interface. Also, the interrupt handlers aren't defined by the HAL crate, so they're left to users of the library to implement and carefully plumb with statics to interact with the other parts of the firmware.


I should also mention RTFM. RTFM is a framework for building real-time embedded systems, with a focus on interrupt-driven flow control. It's got a really cool system of hardware tasks (interrupts) and software tasks that can be assigned different priority levels, which allows resources to be shared safely and without critical sections if no preemption is possible. My biggest concerns with it are how macro-heavy it is and how unique to embedded (specifically Cortex-M) systems it is. Every task, including the init and idle loop have to be wrapped in the #[app] macro that does all of the magic. Things that look like regular statics get wrapped in mutex-like constructs that get handled differently based on priority at the start of the tasks that declare that they use that resource.

While you can do some really cool things with RTFM, I can't help but feel like its system of hardware/software tasks is simply an alternate route to accomplishing the same goals as a Futures-driven solution, albeit in a largely incompatible manner.

My Ideal World

In my ideal world, IO on embedded devices ends up looking much the same as asynchronous IO in std-capable contexts. A lot of work has gone into making the Future trait no_std compatible and it would be really cool to see it as the standard for IO everywhere. Unfortunately, there are still some issues around async/await in no_std contexts: thread_local! is currently used to get the LocalWaker down to the Future being polled (see thread). This isn't an insurmountable problem, as Nemo157 points out, and they even have a working alternative async/await implementation here. I would love to see a set of Futures-first interfaces that define their own interrupt handlers to handle task wakeup/notification. To run these Futures, we'll also need a sufficiently robust executor, ideally one that can juggle more than one task at a time and that doesn't simply poll everything in a tight loop. To that end, I've been working on a no_std-compatible executor, which will be the topic of Part 3!