114. Rust: Cargo, no_std, cortex-m-rt, embedded-hal
Rust обещает то же, что Ada, но другим путём: владение и заимствование
проверяет компилятор, и в безопасном коде висячий указатель или гонка
данных не собираются вовсе. В блоках unsafe (регистры, данные, общие
с прерыванием) за это снова отвечает программист. Переполнение в отладочной сборке останавливает программу паникой, а
в выпускной по умолчанию молча заворачивается, как unsigned в C. Чтобы
проверка осталась и в прошивке, в [profile.release] пишут
overflow-checks = true (это стоит кода), а там, где нужен переход через
ноль, — явно wrapping_add, где нужен признак — checked_add. Cargo — и сборщик, и менеджер пакетов; для прошивки он
ставит зависимости с crates.io, а Cargo.lock закрепляет их версии. Раздел
собирает ту же CRC-32 и то же мигание, что разделы 105 и 113, для F103
и F407, в образе stm-rust:1 — и сравнивает три языка по байтам.
Ada — раздел 113, PlatformIO и CMake — раздел 115, карта памяти — раздел 11.
Как это называется в документации
Оригинал Перевод crate пакет Rust Cargo.toml,Cargo.lockописание пакета, закреплённые версии зависимостей target triple цель сборки: ядро, ОС, ABI no_std,no_mainбез стандартной библиотеки ОС, без обычной точки входа panic handler что делать при панике runtime crate (cortex-m-rt) таблица векторов и сброс для Cortex-M build script ( build.rs)программа, которая работает перед компиляцией feature включаемая при сборке возможность пакета ownership, borrow checker владение, проверка заимствований unsafe, volatile accessнебезопасный блок, доступ без оптимизации PAC, HAL крейт регистров камня, крейт драйверов embedded-hal общие типажи драйверов: вывод, I²C, SPI, задержка
114.1 Cargo и цели сборки
Компилятор Rust знает цели Cortex-M сразу: thumbv7m-none-eabi — Cortex-M3,
то есть F103, и thumbv7em-none-eabihf — Cortex-M4F с аппаратным FPU, то
есть F407. В образе их ставит rustup target add. Камень выбирают
возможностью пакета:
[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
собирает прошивку для F103. В опыте rustc 1.98.1, а Cargo.lock закрепил
8 пакетов: сам проект, cortex-m-rt 0.7.6 с макросами, panic-halt 1.0.0
и то, на чём держатся макросы.
Прошивают и отлаживают обычно через probe-rs: в .cargo/config.toml
пишут runner = "probe-rs run --chip STM32F103C8", и cargo run сам шьёт
образ и показывает вывод. Без него образ переводят в .bin
(cargo objcopy -- -O binary fw.bin) и шьют любым программатором.
114.2 no_std и то, что даёт cortex-m-rt
У прошивки нет ОС, и Rust собирает её без стандартной библиотеки и без обычной точки входа:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
Таблицу векторов, копирование .data и обнуление .bss берёт на себя
cortex-m-rt; #[entry] отмечает функцию, которую он позовёт после сброса.
Карту памяти он читает из memory.x, а тот пишет сценарий сборки — свой
для каждого камня:
let mem = if env::var_os("CARGO_FEATURE_F103").is_some() {
В готовом образе F103 таблица векторов занимает 1024 байта с 0x08000000,
обработчик сброса Reset лежит на 0x08000400, вершина стека — 0x20005000,
у F407 — 0x20020000.
114.3 Регистры: unsafe и volatile
Запись по произвольному адресу компилятор проверить не может, поэтому она
живёт в блоке unsafe, а рядом пишут, почему это безопасно:
// 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 — то же, что volatile в C: запись не выбросят и не
склеят. В настоящем проекте регистры берут из PAC, крейта регистров,
созданного по файлу SVD камня, а поверх него — HAL с типажами embedded-hal:
драйвер датчика пишут один раз для любого камня, у которого есть HAL.
114.4 Переполнение и диапазон
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()
}
Где Ada поднимает Constraint_Error, Rust возвращает None, и принять
его за число вызывающий не может. .unwrap() его пропустит, но на приборе
это паника, а с panic-halt — остановка. cargo test на компьютере прогнал
3 проверки: CRC «123456789» — 0xCBF43926, to_deci(100) — 250,
to_deci(-3) — −7, to_deci(600) — None, а у 255 типа u8
checked_add(1) — None, wrapping_add(1) — 0.
114.5 Три языка — одна прошивка
Одна задача — CRC-32 при старте и мигание на регистрах, — собранная тремя тулчейнами для F103:
| Таблица векторов | Всего во флеш-памяти | |
|---|---|---|
| C, armclang и стартовый файл Keil | 236 байт | 772 байта |
Ada, GNAT 16.1 и свой crt0.S |
16 байт | 1468 байт |
| Rust, rustc 1.98.1 и cortex-m-rt | 1024 байта | 1256 байт |
Разница — не в языке, а в том, что к нему приложено. Стартовый файл Keil
кладёт ровно то, что есть у камня: у F103 это 59 слов — вершина стека,
15 исключений ядра и 43 прерывания. cortex-m-rt без крейта камня
резервирует все 256 слов, а собственно код Rust — 220 байт. Наш crt0.S для Ada держит лишь 4 слова, зато код Ada несёт
проверки диапазона и среду исполнения. Для F407 картина та же: 392, 16
и 1024 байта таблицы.
114.6 Грабли
Собралось, но не для камня. Забыли --target: Cargo собрал программу
для компьютера.
Компоновщик не находит memory.x. cortex-m-rt ищет его в путях поиска;
сценарий сборки должен положить его туда.
Таблица векторов — килобайт на пустом месте. Нет крейта камня: без него cortex-m-rt не знает число прерываний и резервирует все.
Отладочная сборка не помещается во флеш-память. Без оптимизации код
раздувается; для прибора — --release с opt-level = "s".
В выпускной сборке счётчик молча перескочил через ноль. В профиле
release проверка переполнения по умолчанию выключена: 255u8 + 1 даёт 0,
а в отладочной — панику. Нужна проверка — overflow-checks = true
в [profile.release] или явные checked_*.
На другой машине собралось иначе. Не закоммичены Cargo.lock
и rust-toolchain.toml: первый закрепляет версии крейтов, второй — версию
компилятора.
unsafe без объяснения. Блок без комментария SAFETY не проверить
на ревью.
114.7 Итоги
- Цели
thumbv7m-none-eabiдля F103 иthumbv7em-none-eabihfдля F407. no_stdиno_main; векторы и сброс — cortex-m-rt, карта памяти —memory.x.- Регистры —
unsafeиwrite_volatile, в проектах — PAC и HAL с типажами embedded-hal. - Переполнение паникует только в отладочной сборке; в выпускной оно молча
заворачивается, если не включить
overflow-checks = true. Явно —checked_*,wrapping_*,Option. Cargo.lockзакрепляет 8 пакетов; размер образа решает не язык, а таблица векторов и среда.
Полные исходники
Cargo.toml — описание крейта и профилей сборки.
# Раздел 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 и перевод кода в десятые доли градуса с проверкой диапазона.
//! Раздел 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 — мигание на регистрах без 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();
}
}
}
Дальше: раздел 115 «Менеджеры сборки для прошивки: PlatformIO, west (Zephyr), CMake-пресеты» — кто ставит тулчейн и собирает проект за вас.