There are valid concerns with embedded devices, specifically IoT devices, ranging from the use of secure communication protocols, to memory safety issues. This makes rust a natural choice of language to program embedded devices.

Enter stm32, also known as Blue Pill. This is a very available and cheap (~2usd) developer board, powered with a 72Mhz ARM 32-bit CPU, the “same” architecture that powers most mobile devices. Rust currently provides Tier 2 support for ARM, making this a perfect choice of board.

Note: There is also a newer model, called Black Pill, which costs more or less the same and comes with better specifications and maintains most of the layout.

## Setup

There are many embedded environmental sensors, in particular the sensors BME280, BMP280, and BME680 by Bosch have rust crates. I will use a BME280 sensor in this example with the following wiring.

STM32 BME280
3.3 VCC
G GND
B6 SCL
B7 SDA

In this example, I use a st-link v2 programmer for flashing and debugging, configured as follows.

+3.3V 3.3V
DIO SWDIO
DCLK SWCLK
GND GND

Important: Disconnect the blue-pill from any external source of power before connecting the debugger to your machine.

## Rust and Cargo setup

Unless you are using an ARM machine, cross-compilation utilities have to be installed via

sudo dnf install arm-none-eabi-binutils-cs
sudo apt-get install binutils-arm-linux-gnueabi


depending on the host distribution, and add the target architecture of the stm32 to the rust toolchain.

rustup target add thumbv7m-none-eabi


We start the project cargo init stm32_bme280 and add the following dependencies to Cargo.toml.

[dependencies]
bme280 = "0.2"
cortex-m-rt = "0.6"
cortex-m = "0.6"
stm32f1xx-hal = {version = "0.6", features = ["stm32f103", "rt"]}
panic-halt = "0.2"
rtt-target = { version = "0.2", features = ["cortex-m"] }


In order for the program to fit into memory we implement the following compile-time optimizations

# Cargo.toml
[profile.release]
opt-level = 's' # optimize for size ('z' would optimize even more)


The cortex-m-rt crate requires a memory.x file at the project root specifying the memory layout of the board, luckily, the documentation contains an example for this specific board.

/* Linker script for the STM32F103C8T6 */
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}


To make life more convenient we add the following to .cargo/config, to be able to run cargo build without additional flags,

[build]
target = "thumbv7m-none-eabi"

rustflags = [
]


## Computing the altitude

It is a known-fact that the altitude can be computed as a function of the pressure, a model for this is

$h = h_0 \left( 1 - \left( \frac{p}{p_0}\right)^\alpha \right),$

were $$p_0$$ is the pressure at sea-level, $$p$$ the pressure, $$h \sim 44330\,m$$, and $$\alpha \sim \frac{1}{5.255}$$. But there is a single problem, the implementation of the power function fmt::powf() requires intrinsic CPU instructions which are not available or implemented for this board. We can do the following approximation.

Let $$\varepsilon = 1- \frac{p}{p_0}$$. Then, since the preassure at sea level is very similar to what can be measured at home, $$\lvert \varepsilon \rvert \ll 1$$, and we can do the second order approximation

$h \sim h_0 \left( \alpha \varepsilon - \frac{1}{2} \alpha (\alpha - 1) \varepsilon^2 \right).$

## Code

The program used is the following.

#![deny(unsafe_code)]
#![no_std]
#![no_main]

extern crate panic_halt;

use stm32f1xx_hal::{
prelude::*,
pac,
delay::Delay,
i2c,
};

use cortex_m_rt::entry;
use bme280::BME280;
use rtt_target::{rtt_init_print, rprintln};

#[entry]
fn main() -> ! {
let cp = cortex_m::Peripherals::take().unwrap();
let dp = pac::Peripherals::take().unwrap();
let mut rcc = dp.RCC.constrain();

// Set up the I2C bus
let mut flash = dp.FLASH.constrain();
let afio = dp.AFIO.constrain(&mut rcc.apb2);
let mut gpiob = dp.GPIOB.split(&mut rcc.apb2);
let scl = gpiob.pb6.into_alternate_open_drain(&mut gpiob.crl);
let sda = gpiob.pb7.into_alternate_open_drain(&mut gpiob.crl);
let mut mapr = afio.mapr;
let mode = i2c::Mode::Standard { frequency: 40.hz() };
let clocks = rcc.cfgr.freeze(&mut flash.acr);
let mut apb = rcc.apb1;
let start_timeout_us: u32 = 10000;
let start_retries: u8 = 5;
let data_timeout_us: u32 = 10000;

// Creates a blocking I2C1 object on pins PB6 and PB7
let i2c = i2c::BlockingI2c::i2c1(
dp.I2C1,
(scl, sda),
&mut mapr,
mode,
clocks,
&mut apb,
start_timeout_us,
start_retries,
data_timeout_us,
);

// Set up the BME280
let delay = Delay::new(cp.SYST, clocks);
let mut bme280 = BME280::new_primary(i2c, delay);
bme280.init().expect("BME280 initialization failed.");

rtt_init_print!();

let delay_time = 1_000_000; // millis
let sea_level: f32 = 1013.25; // hPa
let h: f32 =  44330.0 // m;
let alpha: f32 = 1.0/5.225;

loop {
cortex_m::asm::delay(delay_time);

let measurements = bme280.measure().unwrap();
let pressure = measurements.pressure / 100.0; // measured in hPa
// Second order Taylor serie for (1- epsilon)^alpha
let epsilon = 1.0 - pressure / sea_level;
let altitude = h * ( alpha * epsilon - 0.5 * alpha * (alpha - 1.0) * epsilon* epsilon );

rprintln!("Temperature: {}°C", measurements.temperature);
rprintln!("Altitude: {}m", altitude);
rprintln!("Humidty: {}%", measurements.humidity);
rprintln!("Pressure: {}hPa", pressure);
}
}


Some important things to note.

• We have to implement #[panic_handler], the crate panic-halt, provides a lightweight implementation.
• The bme280 crate does not implement the required traits to use i2c::i2c1, so we use its blocking cousin i2c::BlockingI2c::i2c1, see [2].
• cargo-embed uses the Real-Time transfer protocol, provided by rtt-target, instead of the more common semi-hosting method.
• The standard library is a no-go for most embedded applications, hence the use of #![no_std].

## Flashing and testing

To run this program we are going to use cargo-embed from [probe-rs][probe.rs], which is a drop-in replacement for cargo run for embedded devices, but it can do much more, such as logging RTT output and opening a GDB server. It can be installed with,

cargo install cargo-embed


It is advisable to set add configuration file Embed.toml, with the following contents, at the root of the project.

[default.general]
chip = "STM32F103C8" # Use --list-chips to see more.

[default.rtt]
enabled = true


The default values can be seen at the default Embed.toml. Finally, to run the program we simply run

cargo embed --release


and we can see the output of our program,

9:01:27.862  BME280 ready.
9:02:36.586  Temperature: 16.995693°C
19:02:36.586 Altitude: 596.0528m
19:02:36.586 Humidty: 69.90904%
19:02:36.586 Pressure: 943.97943hPa


In my measurements, the approximation works just fine. I compared the output with those from another BME280 sensor running on an Arduino board using Adafruit’s BME280 library and saw a difference in the measurement of altitude of about 0.15%.

### Closing thoughts

Rust provides a rapidly moving environment for embedded devices, with a friendly community, and tools like cargo-embed which make it a more streamlined process, similar to the usual x86_64 experience. There are still a few rough edges, like the board auto-detection not working in some cases but overall rust is becoming a good language for IoT and embedded.