114. Rust: Cargo, no_std, cortex-m-rt, embedded-hal
Rust promises the same as Ada, but by a different road: ownership and borrowing
are checked by the compiler, and in safe code a dangling pointer or a data
race simply does not build. In unsafe blocks (registers, data shared
with an interrupt) the programmer is responsible for this again. In a debug build an overflow stops the program with a panic, while
in a release build it silently wraps by default, like unsigned in C. To
keep the check in the firmware too, you write
overflow-checks = true in [profile.release] (it costs code), and where you need to wrap past
zero, you write wrapping_add explicitly; where you need a flag, checked_add. Cargo is both the builder and the package manager; for firmware it
installs dependencies from crates.io, and Cargo.lock pins their versions. This section
builds the same CRC-32 and the same blink as sections 105 and 113, for F103
and F407, in the stm-rust:1 image — and compares the three languages byte by byte.
Ada is section 113, PlatformIO and CMake are section 115, the memory map is section 11.
Terms used in the documentation
Term Meaning crate a Rust package Cargo.toml,Cargo.lockthe package description, the pinned dependency versions target triple the build target: core, OS, ABI no_std,no_mainno OS standard library, no ordinary entry point panic handler what to do on a panic runtime crate (cortex-m-rt) the vector table and reset for Cortex-M build script ( build.rs)a program that runs before compilation feature an optional package capability switched on at build time ownership, borrow checker ownership, the borrow check unsafe, volatile accessan unsafe block, access without optimization PAC, HAL the chip's register crate, the driver crate embedded-hal common driver traits: pin output, I²C, SPI, delay
114.1 Cargo and build targets
The Rust compiler knows the Cortex-M targets out of the box: thumbv7m-none-eabi is Cortex-M3,
that is F103, and thumbv7em-none-eabihf is Cortex-M4F with a hardware FPU, that
is F407. In the image they are installed by rustup target add. The chip is chosen
by a package feature:
[features]
f103 = []
f407 = []
[target.'cfg(target_os = "none")'.dependencies]
cortex-m-rt = "0.7"
panic-halt = "1"
[profile.release]
opt-level = "s"
lto = true
cargo build --release --target thumbv7m-none-eabi --features f103 --bin blink
builds the firmware for F103. In the experiment it is rustc 1.98.1, and Cargo.lock pinned
8 packages: the project itself, cortex-m-rt 0.7.6 with its macros, panic-halt 1.0.0
and what the macros depend on.
Flashing and debugging are usually done through probe-rs: in .cargo/config.toml
you write runner = "probe-rs run --chip STM32F103C8", and cargo run flashes the
image by itself and shows the output. Without it, the image is converted to .bin
(cargo objcopy -- -O binary fw.bin) and flashed with any programmer.
114.2 no_std and what cortex-m-rt provides
The firmware has no OS, and Rust builds it without the standard library and without the ordinary entry point:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
The vector table, copying .data and zeroing .bss are taken care of by
cortex-m-rt; #[entry] marks the function it will call after reset.
It reads the memory map from memory.x, and that file is written by the build script — one
per chip:
let mem = if env::var_os("CARGO_FEATURE_F103").is_some() {
In the finished F103 image the vector table takes 1024 bytes from 0x08000000,
the Reset handler sits at 0x08000400, the top of the stack is 0x20005000,
and on F407 it is 0x20020000.
114.3 Registers: unsafe and volatile
The compiler cannot check a write to an arbitrary address, so it
lives in an unsafe block, and next to it you write why it is safe:
// SAFETY: адреса — регистры периферии этого камня, других владельцев у них нет
unsafe {
board::RCC_ENABLE.write_volatile(board::RCC_ENABLE.read_volatile() | board::ENABLE_BIT);
let m = board::MODE.read_volatile();
board::MODE.write_volatile((m & !board::MODE_CLEAR) | board::MODE_SET);
}
write_volatile is the same as volatile in C: the write will not be removed or
merged. In a real project the registers come from a PAC, a register crate
generated from the chip's SVD file, and on top of it sits a HAL with embedded-hal traits:
a sensor driver is written once for any chip that has a HAL.
114.4 Overflow and range
pub fn to_deci(raw_q4: i32) -> Option<i16> {
if !(-160..=500).contains(&raw_q4) {
return None;
}
i16::try_from(raw_q4 * 10 / 4).ok()
}
Where Ada raises Constraint_Error, Rust returns None, and the caller cannot
mistake it for a number. .unwrap() lets it through, but on the device
that is a panic, and with panic-halt it is a halt. cargo test on a PC ran
3 tests: CRC of "123456789" is 0xCBF43926, to_deci(100) gives 250,
to_deci(-3) gives −7, to_deci(600) gives None, and for 255 of type u8
checked_add(1) gives None, wrapping_add(1) gives 0.
114.5 Three languages, one firmware
One task — CRC-32 at startup and a register-level blink — built with three toolchains for F103:
| Vector table | Total in Flash | |
|---|---|---|
| C, armclang and the Keil startup file | 236 bytes | 772 bytes |
Ada, GNAT 16.1 and our own crt0.S |
16 bytes | 1468 bytes |
| Rust, rustc 1.98.1 and cortex-m-rt | 1024 bytes | 1256 bytes |
The difference is not in the language but in what comes with it. The Keil startup file
places exactly what the chip has: for F103 that is 59 words — the top of the stack,
15 core exceptions and 43 interrupts. cortex-m-rt without a chip crate
reserves all 256 words, while the Rust code proper is 220 bytes. Our crt0.S for Ada holds only 4 words, but the Ada code carries
range checks and the runtime. For F407 the picture is the same: 392, 16
and 1024 bytes of table.
114.6 Pitfalls
It built, but not for the chip. You forgot --target: Cargo built a program
for the PC.
The linker cannot find memory.x. cortex-m-rt looks for it in the search paths;
the build script must put it there.
The vector table is a kilobyte out of nowhere. There is no chip crate: without it cortex-m-rt does not know the number of interrupts and reserves them all.
The debug build does not fit in Flash. Without optimization the code
bloats; for the device use --release with opt-level = "s".
In the release build a counter silently wrapped past zero. In the release
profile the overflow check is off by default: 255u8 + 1 gives 0,
while in the debug build it panics. If you need the check, use overflow-checks = true
in [profile.release] or explicit checked_*.
It built differently on another machine. Cargo.lock
and rust-toolchain.toml are not committed: the first pins the crate versions, the second the compiler
version.
unsafe without an explanation. A block without a SAFETY comment cannot be checked
in review.
114.7 Summary
- Targets
thumbv7m-none-eabifor F103 andthumbv7em-none-eabihffor F407. no_stdandno_main; vectors and reset come from cortex-m-rt, the memory map frommemory.x.- Registers —
unsafeandwrite_volatile; in projects — a PAC and a HAL with embedded-hal traits. - Overflow panics only in the debug build; in the release build it silently
wraps unless you enable
overflow-checks = true. Explicitly —checked_*,wrapping_*,Option. Cargo.lockpins 8 packages; the image size is decided not by the language but by the vector table and the runtime.
Full sources
Cargo.toml — the crate description and build profiles.
# Раздел 114: Rust для Cortex-M через Cargo.
# cargo test --lib — проверки на компьютере
# cargo build --release --target thumbv7m-none-eabi --features f103 --bin blink
# cargo build --release --target thumbv7em-none-eabihf --features f407 --bin blink
[package]
name = "rust114"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"
[[bin]]
name = "blink"
path = "src/main.rs"
test = false
bench = false
[features]
f103 = []
f407 = []
[target.'cfg(target_os = "none")'.dependencies]
cortex-m-rt = "0.7"
panic-halt = "1"
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
debug = false
lib.rs — CRC-32 and conversion of the code to tenths of a degree with a range check.
//! Раздел 114: то же, что CRC-32 из разделов 105 и 113, и перевод кода
//! в десятые доли градуса — с проверкой диапазона вместо молчаливого переполнения.
#![cfg_attr(not(test), no_std)]
/// CRC-32 кадра Ethernet: «123456789» → 0xCBF43926
pub fn crc32(data: &[u8]) -> u32 {
let mut c: u32 = 0xFFFF_FFFF;
for &b in data {
c ^= b as u32;
for _ in 0..8 {
c = if c & 1 != 0 { (c >> 1) ^ 0xEDB8_8320 } else { c >> 1 };
}
}
!c
}
/// код в четвертях градуса → десятые; вне диапазона датчика — None
pub fn to_deci(raw_q4: i32) -> Option<i16> {
if !(-160..=500).contains(&raw_q4) {
return None;
}
i16::try_from(raw_q4 * 10 / 4).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crc_check_value() {
assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
}
#[test]
fn deci_range() {
assert_eq!(to_deci(100), Some(250));
assert_eq!(to_deci(-3), Some(-7));
assert_eq!(to_deci(600), None);
}
#[test]
fn overflow_is_explicit() {
let x: u8 = 255;
assert_eq!(x.checked_add(1), None);
assert_eq!(x.wrapping_add(1), 0);
}
}
main.rs — a register-level blink without a HAL.
//! Раздел 114: мигание на регистрах без HAL — адреса те же, что в разделе 113.
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
use rust114::crc32;
#[cfg(feature = "f103")]
mod board {
pub const RCC_ENABLE: *mut u32 = 0x4002_1018 as *mut u32; // RCC_APB2ENR
pub const ENABLE_BIT: u32 = 0x10; // IOPCEN
pub const MODE: *mut u32 = 0x4001_1004 as *mut u32; // GPIOC_CRH
pub const MODE_CLEAR: u32 = 0x00F0_0000; // поле PC13
pub const MODE_SET: u32 = 0x0020_0000; // выход 2 МГц, двухтактный
pub const ODR: *mut u32 = 0x4001_100C as *mut u32; // GPIOC_ODR
pub const PIN: u32 = 13;
}
#[cfg(feature = "f407")]
mod board {
pub const RCC_ENABLE: *mut u32 = 0x4002_3830 as *mut u32; // RCC_AHB1ENR
pub const ENABLE_BIT: u32 = 0x08; // GPIODEN
pub const MODE: *mut u32 = 0x4002_0C00 as *mut u32; // GPIOD_MODER
pub const MODE_CLEAR: u32 = 0x0300_0000; // поле PD12
pub const MODE_SET: u32 = 0x0100_0000; // выход
pub const ODR: *mut u32 = 0x4002_0C14 as *mut u32; // GPIOD_ODR
pub const PIN: u32 = 12;
}
#[entry]
fn main() -> ! {
if crc32(b"123456789") != 0xCBF4_3926 {
loop {} // арифметика сломана: не мигать
}
// SAFETY: адреса — регистры периферии этого камня, других владельцев у них нет
unsafe {
board::RCC_ENABLE.write_volatile(board::RCC_ENABLE.read_volatile() | board::ENABLE_BIT);
let m = board::MODE.read_volatile();
board::MODE.write_volatile((m & !board::MODE_CLEAR) | board::MODE_SET);
}
loop {
unsafe { board::ODR.write_volatile(board::ODR.read_volatile() ^ (1 << board::PIN)) };
for _ in 0..400_000 {
core::hint::spin_loop();
}
}
}
Next: section 115 "Build managers for firmware: PlatformIO, west (Zephyr), CMake presets" — who installs the toolchain and builds the project for you.