While living the larger laptop/server CPU lifestyle, we've grown accustomed to having threads. Each thread generally provides the illusion of a straight-line execution of code that owns the entire processor.
This is all great (aside from race conditions), but also quite heavy for achieving parallelism on a small $2 MCU that is already running its tail off to blink some LEDs in a timely manner. With Rust (and several other languages), there's the idea of asynchronous and await, which ultimately represent a way to deal with cooperative multitasking, instead of preemptive as defined by modern threads and processes.
Ultimately, an asynchronous function returns a future that will, from your caller's point-of-view, block when you.wait until it's satisfied. The executor API is pretty much left to the implementation, so read the docs of whichever you choose.
We also prefer to statically allocate as much as we can, without over-allocating memory “just in case” because sometimes we're only rocking 48kb to play with. With some strategies, you can precisely allocate memory with asynchronous tasks and deterministically know you won't inadvertently OOM.
I have some proof-of-concept code deep within a secure arctic bunker that I hope to clean up and publish shortly, but here's an example of... what else... blink, using asynchronous and await. Then they call an asynchronous function named delay(...) with the amount of time they want to wait.
That call itself will return a future immediately, which is a pretty poor delay. Once the.await is satisfied, the task can carry on, toggling it's LED and doing the delay(...) dance again.
Since the.await does not actually block the entire processor though, it allows the other task to keep churning, itself doing real work or possibly.awaiting. Additionally, the normal Embedded HAL Countdown timers may use the NB non-blocking crate, but they ultimately block when you wish to delay.
Of course, you can write code that blinks two LEDs using interrupts and shared resources, but at least to my eye, having self-contained tasks with ostensibly straight-line logic is easier to think about. Additionally, the executor I'm using currently supports exactly 8 tasks, and I've initialized it with 1kb of memory for storing the asynchronous continuation structures that Rust creates behind the scenes.
Also, if you want to create something that is asynchronous, you take on the synchronization burden and end up writing implementations of Future which, while being straight-forward, is also non-trivial. But in general, I find asynchronous embedded code to show much promise, and could ultimately form the basis for a reactive or actor-like framework for your small boards.
Rust is a systems' language that started life out at Mozilla Research, first appearing in 2010. Rust has a growing community of advocates, from hobbyists to software engineers.
A plausible view in a computing field that has two core languages that are over 30 years old, with one nearing 50. “To write code free of certain classes of bug, in any language, you need to follow a bunch of rules.
Constraining them to marked blocks helps with visibility and enforcing design contracts. In properly designed Rust, unsafe blocks are kept small and infrequent.
Control over memory is in the hands of the developer, and as such, Rust ’s performance is equivalent to C/C++, even in low-level, constrained environments. In a world where 70% of all security vulnerabilities are the result of memory bugs, Safe Rust reduces the attack surface of any networked program.
Older embedded solutions have been less exposed to malicious actors, but the rapid growth of Internet of Things (IoT) enabled devices means security can’t be an afterthought; it must be baked in from the start, and Rust enforces it. For example deadlocks, as in the classic dining philosophers’ problem, are unsolvable at a language level.
But there is one category of concurrency bugs that is absent in Safe Rust : data races. This often leads to subtle, difficult to diagnose problems since the consumers of the shared resource can access it in a corrupt state.
In desktop computing, the contended resource is usually memory, but when it comes to embedded programming the field broadens. Peripheral access, sensors, actuators… all manner of hardware resources can be involved and fail in mysterious and creative ways.
This leads to a particularly nefarious family of bugs, as they may appear or disappear depending on the specifics of the chosen compiler and architecture. Here at Blue fruit Software, a great number of projects target the ARM architecture, which LLVM supports and therefore Rust, but some of the more exotic microcontrollers we work with will have to wait.
It is worth noting that compilation time is only a factor during development, the resulting executable code is just as fast as its C or C++ equivalent when running on the target processor. The quality of compile-time error messages in Rust is outstanding, which alleviates this problem, but everyone must fight the compiler sooner or later.
You must consider the cost of bringing developers up to speed when assessing Rust as the language of choice for a project. Thousands of C and C++ libraries exist that span every program problem domain, where Rust isn’t there yet.
In choosing Rust for a commercial project, we need to ensure proven libraries are available for the task at hand. At Blue fruit Software, we develop our Real-Time Operating System (RTS) and drivers in-house, and many of the modules we usually delegate to external libraries (such as a file system, TCP/IP stack) have bare metal alternatives at various stages of development, as well as proven higher level options (such as over embedded Linux).
By real-time we mean the strict or “hard” interpretation of the term, where tasks must be guaranteed to execute to a deterministic deadline. In order to help keep documentation alive and up to date, Rust recommends embedding tests in the documentation, so doc strings don’t compile if the code in their examples doesn’t comply anymore.
There are some operations that implicitly can panic, like array indexing and integer division, but only if there is a bug in your program. To further reinforce these security and correctness properties, there are efforts to create a Sealed Rust.
In the future Rust will thus be formally suitable for automotive and aerospace applications. We will use our experiences with developing the Triply Holder patch, a recording device for ECG signals and ancillary medical data, to illustrate this concept.
This type of code is great because it is very straightforward and easy to understand. Earlier we wrote a device driver for the ECG sensor for the Triply Holder Patch.
Note that we wait 100 milliseconds to be sure that the device is powered up, and subsequently reset to the default settings. Macro invocation is basically a busy-waiting loop, waiting until the function is done.
As you can imagine, we instead would like to use this 200ms startup time to also initialize our other peripheral devices. This driver takes ownership of the underlying communication and timer devices used to properly work with this sensor.
Depending on the state we might need to deal with the separate components or the complete device driver. We wrote all the Triply Holder Code like this, for 22 separate tasks.
Note that the amount of lines required to describe the task in this manner is a multiple of our first blocking implementation. Is also quite stateful: the processor keeps a program counter, and all variables are stored on the stack.
However, for multiple tasks we have to do this administration by hand, and basically move the “stack” to the state object. The program counter is encapsulated by which variant of the State ENIM is active.
For each possible moment of blocking (or yielding for time costing operations) we have a state variant. This might sound a bit familiar if you've already used Rust, as there is a way to describe these asynchronous tasks already in the language: futures.
This infrastructure implies some overhead: in effort to create conforming implementations, memory requirements and runtime. Rust async-await introduces a new syntax that enables us to forego the aforementioned explicit state keeping.
Internally the compiler emits a so-called generator with a structure very similar to our State ENIM. In the above example we omitted the actual hardware communication with the ECG sensor.
All driver communication is implemented using blocking primitives of this hardware abstraction layer crate. To make that code asynchronous, we would have to also keep track of the state in that driver.
Hence, it is a trade-off between programmer effort and cycles (and power) wasted to busy waiting. For the Triply Holder Patch we estimate that 60% of the lines of code could be eliminated by using async-await.
Also, we would be able to make more code asynchronous with small effort, reducing total amount of active CPU time. Depending on the application this skews the trade-off in favor of making the code asynchronous.
For the Triply Holder Patch sleeping is absolutely essential to enable recording data for 48 hours straight on a small cell battery. Sleeping however necessitates the direct use of interrupts to wake the processor up again when a task can be resumed.
Implementations of the blocking and asynchronous communication interfaces in the embedded HAL crate do not necessarily configure these interrupts to be fired. The performance of a state machine created by hand, as we have done for the Triply Holder Patch, is not necessarily more (or less) efficient compared to that generated by async-await.
Generally the generated code is quite similar, and has the potential of being more efficient as detailed in a blog series by Tyler Mandy. The water architecture provides a potential computational benefit by limiting the tasks that need to be woken up, but only if our embedded primitives implement their futures to strictly use interrupts to wake these tasks.
From our experiments we have found out that the futures as generated by asynchronous/await waste memory copiously. Using debuggers like GDB to inspect the memory is only possible when inside the future you want to debug.
This is contrary to our faux-futures, where each future could be inspected at any time because their types are transparent and are allocated on the stack. In a system with adequate RAM memory the benefits of the await syntax greatly outweigh these potential downsides.
At Tweed golf we are working on helping out in this regard, by experimenting with asynchronous await and writing HAL drivers using it. Our next blog posts on this topic will be more in-depth and technical, and will concern our effort to implement multiple simultaneous timers and SPI drivers.