Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] API tailored for specific microcontrollers #122

Open
japaric opened this issue Jul 6, 2017 · 5 comments
Open

[RFC] API tailored for specific microcontrollers #122

japaric opened this issue Jul 6, 2017 · 5 comments

Comments

@japaric
Copy link
Member

japaric commented Jul 6, 2017

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

@protomors
Copy link

If we were able to implement #96, then the solution to this problem would be greatly simplified. Then third-party crates could use not "stm32f103c8::TIM2" but something like "stm32::GPTimer" or "stm32::DAC". This should work very well for
STM32 microcontrollers because their peripherals are very unified. But now svd2rust starts to support other types of microcontrollers so I do not know whether this will be a good solution, for example, for MSP430.

@pftbest
Copy link
Contributor

pftbest commented Jul 6, 2017

I think MSP430 is not affected by this issue yet, because we generate a separate SVD file for each specific MCU.

But we can highly benefit from grouping devices into families and grouping peripherals across devices or even families.
For example, for 596 devices in dslite database, we have only 4 kinds of watchdog timer, 12 kinds of rtc clock, 3 kinds of AES accelerator, etc. In theory we can put the most popular peripherals in one big crate, and have device crates to pull only ones they need.

@protomors
Copy link

For example, for 596 devices in dslite database, we have only 4 kinds of watchdog timer, 12 kinds of rtc clock, 3 kinds of AES accelerator, etc. In theory we can put the most popular peripherals in one big crate, and have device crates to pull only ones they need.

This is much more diverse than in STM32 devices (in them there are only two types of RTC). But not so bad as I was afraid. Benefits could still outweigh the need to write custom SVD files for this to work.

@japaric
Copy link
Member Author

japaric commented Jul 6, 2017

@protomors

If we were able to implement #96, then the solution to this problem would be greatly simplified.

Could you elaborate? I see #96 as the "dual" of this issue. #96 is about reducing the number of types to share them across different device crates. This issue is about restricting the use of instances (not types; i.e. const TIM2 not struct TIM2) when developing for specific devices and it's more tied to conditional compilation, not code reuse.

@protomors
Copy link

@japaric I was referring to "re-exports" solution.

If there was some way to tell svd2rust that peripherals in different devices are the same (like proposed in #96) then common crate could be not for stm32f103 but for the whole stm32f1 family or even more generic. Like @pftbest said

In theory we can put the most popular peripherals in one big crate, and have device crates to pull only ones they need.

And #96 would allow to automate this process (if SVD files were modified to include such information).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants