Прошивки на C пишут четвёртый десяток лет. За это время язык оброс вендорскими SDK, графическими конфигураторами, сертифицированными компиляторами и поколением инженеров, свободно читающих карту регистров. Rust в embedded называют сменой парадигмы: память без сборщика мусора, гонки данных, которые ловит компилятор, владение периферией на уровне типов. Звучит вдохновляюще. Но между демо с мигающим светодиодом и прошивкой, которая уходит в серию, лежит рутинная работа: сборка, отладка, HAL под конкретное семейство, размер бинарника, поддержка инструментария на горизонте десяти лет. Ниже разбор того, что в экосистеме Embedded Rust действительно готово, где она спотыкается и когда выбор C остаётся инженерно обоснованным.
Что реально работает в фундаменте после выхода embedded-hal 1.0
Ключевая веха датируется концом декабря 2023 года, когда embedded-hal добрался до версии 1.0.0. До этого драйверы годами писали под черновые трейты 0.2.x, и половина публичных крейтов не стыковалась с другой половиной. Стабильный контракт на GPIO, SPI, I2C, PWM и АЦП закрыл главную боль: драйвер дисплея или датчика, собранный под embedded-hal 1.0, запускается поверх любого HAL, который раскрывает эти трейты. Экран SSD1306, подключённый к STM32F4, переезжает на nRF52840 почти без правок кода, и это уже не маркетинг, а механика интерфейсов.
Рантайм для Cortex-M собирается из горстки крейтов. cortex-m-rt даёт таблицу векторов и обработчик сброса, cortex-m прячет асм и критические секции, panic-halt служит обработчиком паники, defmt 0.3 вместе с defmt-rtt 0.4 отправляет логи по RTT. Идея defmt изящна: строки форматируются на хосте, поэтому литералы не съедают flash. Прошивка и отладка идут через probe-rs, который понимает ST-Link, CMSIS-DAP и J-Link без vendor IDE.
Типичный набор зависимостей для платы на STM32F407 выглядит так:
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "1.0"
panic-halt = "0.2"
defmt = "0.3"
defmt-rtt = "0.4"
stm32f4xx-hal = { version = "0.21", features = ["stm32f407"] }
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
Минимальное приложение с миганием светодиода умещается в несколько строк:
#![no_std]
#![no_main]
use panic_halt as _;
use stm32f4xx_hal::{pac, prelude::*};
#[cortex_m_rt::entry]
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let rcc = dp.RCC.constrain();
let _clocks = rcc.cfgr.sysclk(84.MHz()).freeze();
let gpioa = dp.GPIOA.split();
let mut led = gpioa.pa5.into_push_pull_output();
loop {
led.set_high();
cortex_m::asm::delay(21_000_000);
led.set_low();
cortex_m::asm::delay(21_000_000);
}
}
Версии семейных HAL обновляются каждые несколько месяцев, поэтому перед стартом проекта их сверяют с актуальными: иначе легко собрать связку, отставшую на год. Драйвер датчика пишется против трейтов и не знает, какой чип под ним:
use embedded_hal::i2c::I2c;
pub struct Bme280<I2C> {
i2c: I2C,
addr: u8,
}
impl<I2C, E> Bme280<I2C>
where
I2C: I2c<Error = E>,
{
pub fn new(i2c: I2C, addr: u8) -> Self {
Self { i2c, addr }
}
pub fn read_id(&mut self) -> Result<u8, E> {
let mut id = [0u8; 1];
self.i2c.write_read(self.addr, &[0xD0], &mut id)?;
Ok(id[0])
}
}
Поддержка семейств микроконтроллеров от STM32 и nRF52 до ESP32
Карта поддержки неровная, как поле после вспашки. Для STM32 проект stm32-rs генерирует пакеты доступа к регистрам из вендорских файлов SVD, и покрытие линейки близко к полному: от STM32F1 до STM32H7. Поверх лежат семейные HAL, к примеру stm32f4xx-hal для F4 или stm32l4xx-hal для L4. Серию nRF52 обслуживает nrf-hal, а стек BLE от Nordic доступен через nrf-softdevice из проекта Embassy. RP2040 с двумя ядрами Cortex-M0+ и 264 КБ SRAM закрыт крейтом rp2040-hal. У Espressif своя ветка: семейство esp-hal в no_std охватывает RISC-V чипы ESP32-C3, ESP32-C6 и ESP32-S3, а классический ESP32 на Xtensa требует форкнутого LLVM, который ставится утилитой espup.
Дальше начинаются пробелы. У Texas Instruments и NXP вендорских Rust SDK нет, imxrt-hal развивает сообщество. У Microchip прилично закрыты SAM D21 и D51 через atsamd-hal, тогда как AVR держится на энтузиастах через avr-hal, и бэкенд LLVM для AVR долго числился экспериментальным. В C-мире картина иная: SDK от вендора выходит к дню анонса чипа, CubeMX генерирует инициализацию по щелчку, под любой свежий контроллер найдётся готовый middleware для USB, TCP/IP и файловой системы.
Правило большого пальца простое: чем популярнее плата у разработчиков, тем выше шанс найти под неё готовый HAL. Официальные отладочные платы ST, Nordic и Raspberry Pi в Rust живут отлично. Экзотика уровня специализированных моторных контроллеров или чипов, анонсированных квартал назад, почти наверняка потребует C либо самостоятельной работы с регистрами через сгенерированный svd2rust слой.
Сборка и отладка прошивки на Rust без графических тулзов вендора
Сборка идёт через cargo, и это одновременно сила и слабость. Сила в привычности и воспроизводимости: cargo build тянет зависимости сам, Cargo.lock фиксирует версии, cargo size и cargo objdump показывают размеры и дизассемблер без отдельной установки. Слабость в отсутствии конфигураторов. CubeMX рисует дерево тактирования мышкой и выдаёт код под выбранные частоты. В Rust PLL и прескалеры настраиваются руками, и ошибка в расчёте не всплывёт, пока UART не начнёт выдавать мусор на скорости 115200 вместо ожидаемых 9600.
Конфигурация таргета живёт в файле .cargo/config.toml:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32F407VGTx"
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
rustflags = ["-C", "link-arg=-Tlink.x"]
Базовый цикл разработки умещается в четыре команды:
rustup target add thumbv7em-none-eabihf
cargo build --release
cargo size --release
cargo embed --chip STM32F407VGTx --rtt
Отладка подключается двумя путями: расширение probe-rs для VS Code либо классическая связка из gdb-multiarch и OpenOCD. Окно периферийных регистров собирается из того же SVD-файла, из которого svd2rust генерирует доступ к регистрам. По базовым возможностям это уже близко к тому, что даёт STM32CubeIDE: точки останова, живой просмотр переменных, чтение регистров. Но десятилетия обвязки вендоров не заменяются за вечер, и сложные сценарии вроде трассировки ETM остаются уделом проприетарных инструментов.
Embassy и RTIC две модели многозадачности на реальном железе
Мир Rust-прошивок делится на два лагеря. Embassy тянет асинхронные задачи, RTIC строит многозадачность на прерываниях со статическим анализом доступа к ресурсам. Асинхронная задача выглядит скромно:
#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
loop {
led.set_high();
embassy_time::Timer::after_millis(500).await;
led.set_low();
embassy_time::Timer::after_millis(500).await;
}
}
Асинхронность экономит стек: состояние задачи живёт в статической арене исполнителя, размер которой задаётся фичей task-arena-size-65536, то есть ровно 64 КБ. На nRF52840 с 256 КБ RAM это заметно, на контроллере с 32 КБ приходится считать каждый килобайт. Взамен embassy-usb и асинхронные драйверы UART поверх DMA избавляют от классической возни с кольцевыми буферами и прерываниями по приёму.
Скелет приложения на RTIC 2.x лаконичен:
#[rtic::app(device = stm32f4xx_hal::pac, dispatchers = [UART4])]
mod app {
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
(Shared {}, Local {})
}
}
В RTIC аппаратная задача привязана к прерыванию, приоритеты проецируются в NVIC, а доступ к общим ресурсам проверяется при компиляции: если две задачи делят буфер, фреймворк требует явную блокировку, и без неё код не соберётся. Для сопоставления: ядро FreeRTOS занимает порядка 6-12 КБ, имеет двадцать лет обкатки и тысячи готовых примеров. Биндинги FreeRTOS для Rust годятся для прототипа, но в серьёзных проектах выбор обычно сводится к Embassy, RTIC или голому циклу в main.
Гарантии безопасности типов и граница где начинается unsafe
Главный аргумент Rust перед техлидом: целые классы багов выпадают из бэклога. Режим пина зашит в тип, Input<Floating> не примется там, где ждут Output. Периферия существует в единственном экземпляре, повторный вызов Peripherals::take() не скомпилируется. Гонка данных между задачами требует явно объявленного изменяемого ресурса либо unsafe, который на ревью виден как красная тряпка.
Граница проходит ближе, чем кажется по презентациям. Настройка дескрипторов DMA, работа с кэшем и запись по смещению в регистры это unsafe по определению. Вызов вендорских C-библиотек вроде CMSIS-DSP через FFI снова приоткрывает дверь для ошибок памяти, которую язык только что закрыл. На RP2040 критические секции отключают прерывания только на текущем ядре, и два ядра так и норовят столкнуться за общий ресурс: спасают аппаратные спинлоки чипа, но думать за инженера они не станут. Ошибки в расчёте тактирования, логике протокола и схемотехнике остаются с разработчиком, как и прежде. Компилятор не заметит, что датчик висит не на той ножке, если код честно сконфигурировал ту, что указана в комментарии.
Реальные размеры прошивки и потребление RAM у Rust против C
Демо с мигалкой не показывает главного, сколько стоит абстракция. Замеры на STM32F407 с opt-level "z", LTO и единым codegen-unit дают типичный бинарник на 4-8 КБ, та же мигалка на C с прямой записью в регистры укладывается в 2-3 КБ. На чипе с 1 МБ flash разница тонет в шуме, на STM32G0 с 16 КБ она решает, влезает ли функция вообще.
RAM считают заново: буферы defmt, арена Embassy, стеки задач RTIC распределяются статически, зато предсказуемо. Разобраться, куда ушли килобайты, помогает cargo-bloat: команда cargo bloat --release выстраивает функции по занимаемому месту, и почти всегда в топе списка оказываются форматирование паники и векторы прерываний, а не бизнес-логика. Паника на thumb-таргетах не разворачивает стек, без обработчика вроде panic-halt линковщик просто откажется собирать бинарник. На Cortex-M0 нет аппаратных атомиков, поэтому код для thumbv6m обходится критическими секциями или крейтом portable-atomic. Время компиляции с Embassy подводит: процедурные макросы прожорливы, и полная пересборка занимает десятки секунд там, где make на небольшом C-проекте отрабатывает мгновенно. Мелочь, пока не правишь код в цикле по сто раз на дню.
Сертификация и десятилетний цикл жизни когда C остаётся разумнее
Автопром уже сдвинулся с места: Volvo публично рассказывала о применении Rust в новых блоках управления, а в начале 2024 года вышел квалифицированный тулчейн Ferrocene, собранный под ISO 26262 и IEC 61508. Раньше сертифицированные компиляторы для критичных систем выпускали IAR и Arm с линейкой Keil MDK, и C оставался безальтернативным. Теперь у Rust появился легальный путь в системы с высокими требованиями к безопасности, путь долгий, но уже размеченный.
Сертификация компилятора не решает остальных вопросов. Крейты в публичном репозитории поддерживаются людьми, а не корпорациями, и цепочка поставки требует контроля: cargo audit ищет уязвимости по базе advisory, cargo deny проверяет лицензии и запрещённые зависимости. В C-мире цепочка поставки выглядел иначе: код вендора приезжал в виде zip-архива с SDK и годами лежал без изменений.
Инженеру, выбирающему стек под продукт с жизненным циклом в десять лет, стоит честно ответить себе на четыре пункта:
1. есть ли квалифицированный тулчейн под требуемый профиль безопасности;
2. закрыт ли выбранный чип вендорским SDK и живым HAL сообщества;
3. хватает ли flash и RAM под дополнительные килобайты бинарника;
4. найдётся ли в команде достаточное число людей, уверенно читающих и Rust, и C.
Пока большинство ответов не в пользу Rust, от ставки на язык разумно воздержаться. Для массовых платформ вроде STM32F4, nRF52840 и RP2040 язык уже перешагнул порог серийной разработки: фундамент стабилен, HAL поддерживаются, инструменты зрелые. Гибридная схема, где middleware на C соседствует с приложением на Rust через аккуратный FFI-слой, снижает риск и позволяет команде присмотреться к языку без большой ставки. Экосистема не заменила C за два года и вряд ли заменит за пять, но планку качества новых прошивок она подняла, и у embedded-инженера на столе появился второй полноценный инструмент, а это меняет расклад.