Skip to content

Commit

Permalink
Merge pull request #16833 from chrysn-pull-requests/rust-lib
Browse files Browse the repository at this point in the history
Add some Rust library building infrastructure
  • Loading branch information
chrysn authored Jul 10, 2022
2 parents bad2527 + bc8ec6d commit d9879c9
Show file tree
Hide file tree
Showing 40 changed files with 1,288 additions and 12 deletions.
1 change: 1 addition & 0 deletions boards/microbit-v2/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ config BOARD_MICROBIT_V2
select HAS_VDD_LC_FILTER_REG1

select HAVE_SAUL_GPIO
select HAVE_LSM303AGR

source "$(RIOTBOARD)/common/microbit/Kconfig"
source "$(RIOTBOARD)/common/nrf52/Kconfig"
4 changes: 4 additions & 0 deletions boards/microbit-v2/Makefile.dep
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
include $(RIOTBOARD)/common/microbit/Makefile.dep
include $(RIOTBOARD)/common/nrf52/Makefile.dep

ifneq (,$(filter saul_default,$(USEMODULE)))
USEMODULE += lsm303agr
endif
1 change: 1 addition & 0 deletions boards/microbit-v2/include/lsm303agr-config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const I2C_DEVICES: &[u8] = &[0];
22 changes: 22 additions & 0 deletions doc/doxygen/src/using-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ The wrappers are [documented together with riot-sys and some of the examples].
[I2CDevice]: https://rustdoc.etonomy.org/riot_wrappers/i2c/struct.I2CDevice.html
[corresponding embedded-hal I2C traits]: https://rustdoc.etonomy.org/embedded_hal/blocking/i2c/index.html

Library components in Rust
--------------------------

It is possible to use Rust in different modules than the application itself.

Such modules are usually pseudomodules (although they may be mixed with C in regular modules as well).
They always depend on the `rust_riotmodules` module / crate,
which collects all enabled modules into a single crate by means of optional features.

If the application is not written in Rust,
that then depends on `rust_riotmodules_standalone`,
which adds a panic handler and serves as a root crate.

If the application is written in Rust,
`rust_riotmodules` needs to be added as a dependency of the application.
(This helps deduplicate between application and library code,
and also avoids symbol name clashes).
This is done by adding a dependency on the local `rust_riotmodules` crate (which is a no-op when no such modules are enabled),
and placing an `extern crate rust_riotmodules;` statement in the code.
(The latter is needed even after most `extern crate` was abolished in 2018,
because crates depended on but not used otherwise are usually not linked in).

Toolchain {#toolchain}
---------

Expand Down
1 change: 1 addition & 0 deletions drivers/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ rsource "lpd8808/Kconfig"
rsource "lpsxxx/Kconfig"
rsource "lsm6dsl/Kconfig"
rsource "lsm303dlhc/Kconfig"
rsource "lsm303agr/Kconfig"
rsource "ltc4150/Kconfig"
rsource "mag3110/Kconfig"
rsource "mhz19/Kconfig"
Expand Down
17 changes: 17 additions & 0 deletions drivers/lsm303agr/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "riot-module-lsm303agr"
version = "0.1.0"
edition = "2021"

authors = ["Christian Amsüss <[email protected]>"]
license = "LGPL-2.1-only"

# Shipped with RIOT-OS; this has no external API that would make
# sense to consume in any context than from within RIOT
publish = false

[dependencies]
lsm303agr = "^0.2"
riot-wrappers = "^0.7.17"
# Whatever lsm uses
nb = "*"
20 changes: 20 additions & 0 deletions drivers/lsm303agr/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (c) 2022 HAW Hamburg
#
# This file is subject to the terms and conditions of the GNU Lesser
# General Public License v2.1. See the file LICENSE in the top level
# directory for more details.
#

config MODULE_LSM303AGR
bool
prompt "LSM303AGR 3D accelerometer/magnetometer" if !(MODULE_SAUL_DEFAULT && HAVE_LSM303AGR)
default y if (MODULE_SAUL_DEFAULT && HAVE_LSM303AGR)
depends on HAS_PERIPH_I2C
depends on TEST_KCONFIG
select MODULE_RUST_RIOTMODULES
select MODULE_PERIPH_I2C

config HAVE_LSM303AGR
bool
help
Indicates that a lsm303agr sensor is present.
3 changes: 3 additions & 0 deletions drivers/lsm303agr/Makefile.dep
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
USEMODULE += rust_riotmodules

FEATURES_REQUIRED += periph_i2c
1 change: 1 addition & 0 deletions drivers/lsm303agr/Makefile.include
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PSEUDOMODULES += lsm303agr
47 changes: 47 additions & 0 deletions drivers/lsm303agr/doc.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**

@defgroup drivers_lsm303agr LSM303AGR 3D accelerometer/magnetometer
@ingroup drivers_sensors
@ingroup drivers_saul
@brief Device driver for the LSM303AGR 3D accelerometer/magnetometer

This driver is written in Rust,
and based on the externally maintained [lsm303agr] crate.

This means that:

- it is only available on platforms supported by Rust,
- it needs Rust installed on the build machine as described in @ref using-rust, and
- it downloads additional Rust code from crates.io (in versions pinned by RIOT) when first used.

[lsm303agr]: https://crates.io/crates/lsm303agr

## Usage

When configured on a board, the devices are initialized at a fixed data acquisition rate and the chip's default range of +-2g.
Data values are obtained on demand
whenever queried through @ref drivers_saul.

For each device, two SAUL entries are registered labelled "LSM303AGR accelerometer" and "LSM303AGR magnetometer",
which produces 3-axis values in units of g and Tesla, respectively.
Accelerometer values are always scaled to milli-g (they come that way and don't exceed the i16 range of SAUL phydats);
magnetometer readings are dynamically downscaled from their original i32 Nanotesla readings to fit in a phydat.

The driver is configured for a board by placing an `lsm303agr-config.rs` file
in the board's include directory, which lists the I2C device(s) on which an accelerometer should be found:

```
const I2C_DEVICES: &[u8] = &[0];
```

## Limitations

- Advanced features of the sensor
(adjusting acquisition rate or resolution, free-fall detection, interrupts etc.)
are not exposed.

- The driver accepts some memory overhead (roughly, two mutexes) to avoid unsafe code and enhance readability.
The unsafe code would be sound based on the assertion that the initialization code is only called once
(and in particular is not reentrant).

*/
123 changes: 123 additions & 0 deletions drivers/lsm303agr/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#![no_std]

use lsm303agr::{interface, mode, Lsm303agr, AccelOutputDataRate::Hz50};

use riot_wrappers::{saul, println, i2c, cstr::cstr, mutex::Mutex};
use saul::{Phydat, registration};

// FIXME: Is this the way we want to go? It's mimicking the C way, but we could just as well take
// the board config from some YAML.
include!(concat!(env!("BOARDDIR"), "/include/lsm303agr-config.rs"));

const NDEVICES: usize = I2C_DEVICES.len();

static DRIVER: registration::Driver<SaulLSM> = registration::Driver::new();
static DRIVER_MAG: registration::Driver<SaulLSM, MagAspect> = registration::Driver::new();

// These two being in mutexes is somewhat unnecessary (the mutexes are locked at startup and then
// never unlocked). The alternative is to unsafely access them (asserting that auto_init_lsm303agr
// / init will only ever be called once), or hiding that assertion at some preprocessor level (like
// cortex-m-rt's main does).
//
// Doing it at runtime comes at the cost of two global mutexes in memory, and some more startup
// calls.
//
// Using an Option (with .insert) rather than MaybeUninit (with .write) is another step that
// sacrifices minimal resources (could be none at all, didn't check) for readability.

// This can't go into ROM because it has a .next pointer that is altered at runtime when some other
// device is registered. (In an alternative implementation where all SAUL registries are managed by
// XFA, this would be possible; finding the point in time when they are ready to be used would be
// tricky, though.).
static REG: Mutex<[Option<registration::Registration<SaulLSM>>; NDEVICES]> = Mutex::new([None; NDEVICES]);
static REG_MAG: Mutex<[Option<registration::Registration<SaulLSM, MagAspect>>; NDEVICES]> = Mutex::new([None; NDEVICES]);
// This can't go into ROM because it contains an inner Mutex (and possibly state data from the
// Lsm303agr instance, didn't bother to check)
static LSM: Mutex<[Option<SaulLSM>; NDEVICES]> = Mutex::new([None; NDEVICES]);

#[no_mangle]
pub extern "C" fn auto_init_lsm303agr() {
if let Err(e) = init() {
println!("LSM303AGR init error: {}", e);
}
}

/// Initialize the configured LSM303AGR device, returning an error string for debug if anything
/// goes wrong
fn init() -> Result<(), &'static str> {
let lsm = LSM
.try_leak()
.expect("LSM303AGR init is only called once");

let reg = REG
.try_leak()
.expect("LSM303AGR init is only called once");
let reg_mag = REG_MAG
.try_leak()
.expect("LSM303AGR init is only called once");

for (&i2cdev, (lsm, (reg, reg_mag))) in I2C_DEVICES.iter().zip(lsm.iter_mut().zip(reg.iter_mut().zip(reg_mag.iter_mut()))) {
let mut device = Lsm303agr::new_with_i2c(i2c::I2CDevice::new(i2cdev));

device.init()
.map_err(|_| "Device initialization failed")?;
device.set_accel_odr(Hz50)
.map_err(|_| "Device configuration failed")?;

let lsm = lsm.insert(SaulLSM { device: Mutex::new(device) });

let reg = reg.insert(registration::Registration::new(&DRIVER, lsm, Some(cstr!("LSM303AGR accelerometer"))));
let reg_mag = reg_mag.insert(registration::Registration::new(&DRIVER_MAG, lsm, Some(cstr!("LSM303AGR magnetometer"))));

reg.register_static();
reg_mag.register_static();
}

Ok(())
}

struct SaulLSM {
device: Mutex<Lsm303agr<interface::I2cInterface<i2c::I2CDevice>, mode::MagOneShot>>,
}

impl registration::Drivable for &SaulLSM {
const CLASS: saul::Class = saul::Class::Sensor(Some(saul::SensorClass::Accel));

const HAS_READ: bool = true;

fn read(self) -> Result<Phydat, registration::Error> {
// SAUL doesn't guarantee exclusive access; different threads may read simultaneously.
let mut device = self.device.try_lock()
.ok_or(registration::Error)?;

let data = device.accel_data()
.map_err(|_| registration::Error)?;
// Data is in the +-2g range by default, which doesn't overflow even the i16 SAUL uses
Ok(Phydat::new(&[data.x as _, data.y as _, data.z as _], Some(saul::Unit::G), -3))
}
}

struct MagAspect(&'static SaulLSM);

impl From<&'static SaulLSM> for MagAspect {
fn from(input: &'static SaulLSM) -> Self {
Self(input)
}
}

impl registration::Drivable for MagAspect {
const CLASS: saul::Class = saul::Class::Sensor(Some(saul::SensorClass::Mag));

const HAS_READ: bool = true;

fn read(self) -> Result<Phydat, registration::Error> {
// SAUL doesn't guarantee exclusive access; different threads may read simultaneously.
let mut device = self.0.device.try_lock()
.ok_or(registration::Error)?;

let data = nb::block!(device.mag_data())
.map_err(|_| registration::Error)?;
// Original data is in nanotesla
return Ok(Phydat::fit(&[data.x, data.y, data.z], Some(saul::Unit::T), -9))
}
}
4 changes: 4 additions & 0 deletions drivers/saul/init_devs/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ void saul_init_devs(void)
extern void auto_init_lpsxxx(void);
auto_init_lpsxxx();
}
if (IS_USED(MODULE_LSM303AGR)) {
extern void auto_init_lsm303agr(void);
auto_init_lsm303agr();
}
if (IS_USED(MODULE_LSM303DLHC)) {
extern void auto_init_lsm303dlhc(void);
auto_init_lsm303dlhc();
Expand Down
6 changes: 6 additions & 0 deletions examples/rust-gcoap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ riot-wrappers = { version = "^0.7.18", features = [ "set_panic_handler", "panic_
coap-message-demos = { git = "https://gitlab.com/chrysn/coap-message-demos/", default-features = false }
coap-handler-implementations = "0.3"
riot-coap-handler-demos = { git = "https://gitlab.com/etonomy/riot-module-examples/", features = [ "vfs" ] }

# While currently this exmple does not use any RIOT modules implemented in
# Rust, that may change; it is best practice for any RIOT application that has
# its own top-level Rust crate to include rust_riotmodules from inside
# RIOTBASE.
rust_riotmodules = { path = "../../sys/rust_riotmodules/" }
2 changes: 2 additions & 0 deletions examples/rust-gcoap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use riot_wrappers::{gcoap, thread, ztimer, gnrc};

use coap_handler_implementations::{ReportingHandlerBuilder, HandlerBuilder};

extern crate rust_riotmodules;

riot_main!(main);

fn main() {
Expand Down
6 changes: 6 additions & 0 deletions examples/rust-hello-world/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ crate-type = ["staticlib"]
[dependencies]
# `default-features = false` can be removed with 0.8, and enables building on stable during the 0.7 series
riot-wrappers = { version = "0.7", features = [ "set_panic_handler" ], default-features = false }

# While currently this exmple does not use any RIOT modules implemented in
# Rust, that may change; it is best practice for any RIOT application that has
# its own top-level Rust crate to include rust_riotmodules from inside
# RIOTBASE.
rust_riotmodules = { path = "../../sys/rust_riotmodules/" }
2 changes: 2 additions & 0 deletions examples/rust-hello-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use riot_wrappers::riot_main;
use riot_wrappers::println;

extern crate rust_riotmodules;

riot_main!(main);

fn main() {
Expand Down
2 changes: 0 additions & 2 deletions makefiles/arch/riscv.inc.mk
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,3 @@ LINKFLAGS += $(CFLAGS_CPU) $(CFLAGS_LINK) $(CFLAGS_DBG) $(CFLAGS_OPT) -nostartfi

# Platform triple as used by Rust
RUST_TARGET = riscv32imac-unknown-none-elf
# Workaround for https://github.com/rust-lang/rust-bindgen/issues/1555
CARGO_EXTRACFLAGS += --target=riscv32
10 changes: 0 additions & 10 deletions makefiles/cargo-settings.inc.mk
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
# Rust's own version of the target triple / quadruple.
#
# This does not have a sane default, and needs to be set in the architecture
# files.
# RUST_TARGET = ...

# Flags that need to be added to the RIOT_CFLAGS passed to cargo in order to
# make bindgen happy
CARGO_EXTRACFLAGS ?=

# Setting anything other than "debug" or "release" will necessitate additional
# -Z unstable-options as of 2021-03 nightlies.
CARGO_PROFILE ?= release
Expand Down
19 changes: 19 additions & 0 deletions makefiles/cargo-targets.inc.mk
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
CARGO_COMPILE_COMMANDS = $(BINDIR)/cargo-compile-commands.json
CARGO_COMPILE_COMMANDS_FLAGS = --clang

# When an application crate is built, it has to use rust_riotmodules_standalone itself to
# pull in any Rust modules that might be enabled through RIOT's module system.
# (If the application fails to add the rust_riotmodules_standalone dependency, that will
# go unnoticed and work fine if none of the catchall-dispatched modules are
# active -- but if one is enabled, this also serves to ensure the application
# is not silently built without them, as the feature will not be available to
# Cargo).
#
# This list should eventually be autogenerated.
ifneq (,$(filter lsm303agr,$(USEMODULE)))
CARGO_OPTIONS += --features rust_riotmodules/riot-module-lsm303agr
endif
ifneq (,$(filter shell_democommands,$(USEMODULE)))
CARGO_OPTIONS += --features rust_riotmodules/riot-module-shell-democommands
endif

# This is duplicating the compile-commands rule because unlike in the use case
# when a $(RIOTBASE)/compile_commands.json is built, we *want* this to be
# per-board and per-application. (The large mechanisms are shared anyway).
Expand Down Expand Up @@ -64,3 +80,6 @@ $(APPLICATION_RUST_MODULE).module: $(CARGO_LIB) FORCE
# (should they not exist), and also from re-building everything every time
# because the .cargo inside is as ephemeral as the build container.
$(shell mkdir -p ~/.cargo/git ~/.cargo/registry)

FORCE:
.phony: FORCE
5 changes: 5 additions & 0 deletions makefiles/vars.inc.mk
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,8 @@ export AFL_FLAGS # Additional command-line flags passed to afl durin
# LOG_LEVEL # Logging level as integer (NONE: 0, ERROR: 1, WARNING: 2, INFO: 3, DEBUG: 4, default: 3)
# KCONFIG_ADD_CONFIG # List of .config files to be merged used by Boards and CPUs. See kconfig.mk
# VERBOSE_ASSERT # Set to 1 to print the file and line of a failed assert when assertions blow

export RUST_TARGET # Rust's own version of the target triple / quadruple.
#
# It is set by the architecture (and thus eventually the CPU), and exported to
# be available when building Rust modules.
1 change: 1 addition & 0 deletions sys/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ rsource "progress_bar/Kconfig"
rsource "ps/Kconfig"
rsource "random/Kconfig"
rsource "rtc_utils/Kconfig"
rsource "rust_riotmodules/Kconfig"
rsource "saul_reg/Kconfig"
rsource "schedstatistics/Kconfig"
rsource "sema/Kconfig"
Expand Down
Loading

0 comments on commit d9879c9

Please sign in to comment.