Original issue: japaric/stm32f103xx#9
The problem
SVD files usually describe a family of microcontrollers and contain
information about all the peripherals any member of the family could have.
When svd2rust produces a device crate from a SVD file it produces an API to
access every single of these peripherals.
The problem is that the lower density members of a family are likely to contain
less peripherals than the set of peripherals described by the SVD file. As a
result using the device crate with such devices lets you access nonexistent
peripherals.
As a concrete example the stm32f103xx
crate exposes an API for the TIM6 and
TIM7 peripherals (basic timers) but these peripherals are not available on the
STM32F103C8 microcontroller. So if you write an application for that
microcontroller using the stm32f103xx
crate you may end up using those timers
without realizing they are not available. Worst part is that the program won't
crash -- it won't hit an exception -- but rather it will likely have undefined
behavior (writes are no-op and reads return junk values or zero)
Possible solutions
Constraints
- AFAIS SVD files contain no information about which peripherals are present on
device X. So this information will have to be supplied by a human and most
likely we won't be able to add this information to the SVD file.
Cargo features
One of the ideas brought up in the original issue was to encode the presence of
each peripheral through a Cargo feature and then have one Cargo feature per
microcontroller. That microcontroller feature would enable all the peripherals,
through their features, that are present on that microcontroller. Example:
# Cargo.toml
[features]
TIM2 = []
TIM3 = []
TIM4 = []
TIM6 = []
TIM7 = []
stm32f103c8 = ["TIM2", "TIM3", "TIM4"]
stm32f103vg = ["TIM2", "TIM3", "TIM4", "TIM6", "TIM7"]
The device crate would make use #[cfg]
attributes like this:
#[cfg(feature = "TIM2")]
pub const TIM2: Peripheral<TIM2> = ..;
#[cfg(feature = "TIM3")]
pub const TIM3: Peripheral<TIM3> = ..;
to prevent exposing APIs not available to a certain microcontroller.
As you know Cargo features are additive so there's nothing stopping
you from enabling more than one microcontroller feature at the same time, even
by mistake (e.g. a dependency enables one microcontroller feature and another
dependency enables a different one). In those cases we can raise an error in the
device crate like this:
#[allow(dead_code)]
#[cfg(feature = "stm32f103c8")]
const ERROR: &str = "feature stm32f103c8 is enabled";
#[allow(dead_code)]
#[cfg(feature = "stm32f103vg")]
const ERROR: &str = "feature stm32f103vg is enabled";
If more than one microcontroller feature is enabled this will raise a name
collision error.
Library crates that depend on the device crate can write device specific APIs
like this:
extern crate stm32f103xx;
#[cfg(feature = "TIM2")]
fn foo(tim2: &stm32f103xx::TIM2) { .. }
#[cfg(feature = "TIM3")]
fn bar(tim2: &stm32f103xx::TIM3) { .. }
Testing a library crate that depends on a device crate for the different devices
that the device crate supports is as simple as calling the Cargo command with
different --feature
arguments:
$ cargo check --feature stm32f103c8
$ cargo check --feature stm32f103vg
Upsides
This is straightforward to implement in svd2rust.
Downsides
Due to the additive nature of Cargo features it seems to be easy to break the
device selection mechanism: it's just enough that a dependency directly enables
a peripheral feature:
# Cargo.toml
[package]
name = "application"
[dependencies.stm32f103xx]
features = ["stm32f103c8"]
version = "0.1.0"
[dependencies]
# this crate depends on the stm32f103xx crate and directly enables its TIM6
# feature, but this peripheral is not available on the stm32f103c8
# microcontroller
foo = "0.1.0"
This problem can be avoided if library crates never enable any feature of the
device crate but there's no mechanism to enforce this so discipline would be
required.
--cfg device=
Another approach is to not use Cargo features at all but to directly use
#[cfg]
attributes and the --cfg
rustc flag. With this approach the device
crate would look like this:
#[cfg(any(device = "stm32f103c8", device = "stm32f103vg"))]
pub const TIM2: Peripheral<TIM2> = ..;
#[cfg(any(device = "stm32f103c8", device = "stm32f103vg"))]
pub const TIM3: Peripheral<TIM3> = ..;
// ..
#[cfg(device = "stm32f103vg")]
pub const TIM6: Peripheral<TIM6> = ..;
Application crates that depend on the device crate can then pick one specific
device or other using the --cfg
flag:
$ RUSTFLAGS='--cfg device="stm32f103vg"' cargo build
Library crates would require #[cfg]
attributes similar to the ones used in the
device crate:
extern crate stm32f103xx;
#[cfg(any(device = "stm32f103c8", device = "stm32f103vg"))]
fn foo(tim2: &stm32f103xx::TIM2) { .. }
#[cfg(device = "stm32f103vg")]
fn bar(tim6: &stm32f103xx::TIM6) { .. }
Enabling more than one device cfg
seems hard to do by mistake but an error can
be raised in the device crate like this:
#[allow(dead_code)]
#[cfg(device = "stm32f103c8")]
const ERROR: &str = "feature stm32f103c8 is enabled";
#[allow(dead_code)]
#[cfg(device = "stm32f103vg")]
const ERROR: &str = "feature stm32f103vg is enabled";
The more common error scenario is that people will likely forget to pass the
--cfg device=
flag. In that case a helpful error can be raised in the device
crate:
#[cfg(not(any(device = "stm32f103c8", device = "stm32f103vg")))]
const ERROR: &str = "No device selected! Add `--cfg device=something` to `RUSTFLAGS`";
Downsides
Implementing this is hard and would require teaching svd2rust to parse a file
that maps a device to the peripherals it has.
Writing library crates is tedious as it requires checking which peripheral is
available for each specific device the device crate supports.
RUSTFLAGS is not the first thing that comes to people's mind when they think
about configuring dependencies.
one crate per device
Another approach is to not solve this in svd2rust. Instead we can create device
specific SVD files from a more generic one, and then generate one device crate
for each of those files. This means that instead of a generic stm32f103xx crate
we would have several crates: stm32f103c8, stm32f103vg, etc.
Downsides
Lots of duplicated code.
More work would likely be required to write crates that abstract over device
specific details. Mainly because stm32f103c8::TIM2
and stm32f103vg::TIM2
are
not the same type.
re-exports
Yet another approach is to have device specific crates but that only include
re-exports of a more generic device crate. For instance:
// crate: stm32f103c8
extern crate stm32f103xx;
pub use stm32f103xx::{..,TIM2,TIM3,TIM4};
// crate: stm32f103vg
extern crate stm32f103xx;
pub use stm32f103xx::{..,TIM2,TIM3,TIM4,TIM5,TIM6};
Downsides
Unless the application crate directly depends on a device specific crate some
intermediate library crate will end up looking like this:
// Looks familiar?
#[cfg(..)]
extern crate stm32f103c8;
#[cfg(..)]
extern crate stm32f103vg;
Unresolved questions
- Is there a simpler alternative?
cc @protomors