Embedded Rust : Framework structure
There are already some embedded rust projects such as tock-os or zync-rs. They all (AFAIK) took the path to a monolithic API in that sense that everything is provided as a single (potentially) heavy crate where all ports and their drivers are provided in the same crate.
A single dependency to all your projects can look appealing, but it has downsides that (IMHO) are making it unworthy. It greatly impairs maintainability and slows down the release pace because it makes the crate heavier (in features/targets) :
- Updating a driver for a single target/family requires a patch bump for the whole framework ;
- Keeping things consistent when changing an API requires you to update ALL drivers which can be a rather tedious task when supporting a wide range or targets ;
- It is way too easy to intricate modules’ dependencies and end up with a tight knot of internal relations between modules. It goes against the KISS principle.
Splitting the framework in a “galaxy” of light weight crates allows adoption of updates of the main APIs to be propagated in some sort of waterfall kind of way. A team responsible of maintaining a certain set of target can use the semantic versioning to only update their release when they are ready. It adds latency between the main API & the target implementation updates but it prevents :
- highly demanded features to be stuck until all team do implement it ;
- code rot of abandoned targets in the framework crates.
This would also help :
- enforcing a mindful design of APIs and keep them clear between crates ;
- benefit from the semantic versioning of APIs ;
- organise communities around focus groups that wouldn’t be limited by slower/less active groups…
This diagram is here to give you a rough idea of what could be achieved. This would of course be extended by more crates dedicated to others tasks such has file system drivers, motor/sensor control (loops?)…
For example here is how things could be spread :
silica1 (api level) :
Defines generic traits that exposes features for common resources such as UART, SPI, I²C, ADC, DAC, CAN, Timers… It also reexport modules from other core crates (such as synchronization primitives etc) ;
Provides generic implementations for Mutexes, Semaphores and critical sections. It expects (at least) two external functions to be defined :
silica_cortexm(mcu core level) :
Implements Mutexes, Semaphores & critical sections’ internals.
This is also where is implemented all the os mechanisms are because they are generic to all cortex cores ;
Implements extra traits that cover cases that can be handled/accelerated by hardware specific features ;
silica_atsam4e(chip level) :
Implements drivers for all peripherals that are common to this family ;
Exports all symbols related to peripheral instances, gpios etc ;
silica_duet(module/board level) :
Exports a subsets of gpio and determine the configuration of most peripherals. It may also contain board specific configuration such as external clock frequency, gpio that are actually always output with a low active level etc.
silica_demo_blinky(application level) :
Implements the actual application. It may contain some code specific to a target that would be feature gated.
Rust’s Features are a nice & clean replacement for what used to be done with
#define in C.
They could be used to select a specific implementation of a given feature.
The most obvious use case that comes to my mind is a bootloader application requiring that ‘no-os’ is provided to get the smallest footprint possible while another app would require a full featured ‘RTOS’ with system calls, os aware synchronisation primitive implementations primitives. Both apps would depend on the same target crate (e.g.
silica_arduino_mega2560) but with slightly different implementations.