Вступление: Эпоха «железа» меняет язык
Если вы когда-нибудь писали прошивку для микроконтроллера на C, вы знаете это чувство. Чувство, когда после трёх часов отладки вы находите висяк — указатель улетел в небо, а watchdog всё стерпел. Embedded-разработка долгие годы была территорией Си и ассемблера. Но в последние пару лет на арену вышел новый игрок — Rust. И он не просто «ещё один язык», а настоящий прорыв для встраиваемых систем.
В этой статье мы разберём, почему embedded разработка на Rust — это не хайп, а необходимость. Посмотрим на реальные примеры кода, сравним с Си и поймём, с чего начать. Если вы пишете под STM32, ESP32 или ARM Cortex-M — вам сюда.
Почему Rust для embedded — это идеальный брак
Встраиваемые системы — это мир ограничений. Мало памяти, нет ОС, нет кучи, нет исключений. Ошибка компиляции — хорошо, ошибка в рантайме — плохо. Rust был спроектирован именно для таких условий.
Безопасность памяти без сборщика мусора
Главная боль embedded-разработчика на C — это use-after-free, двойное освобождение памяти и выход за границы буфера. Rust решает это на уровне типов. Модель владения (ownership) и заимствования (borrowing) работает даже в no_std окружении — без аллокатора, без ОС, без GC.
Представьте, что вы пишете драйвер для светодиода. В C вы бы написали:
// C code
void led_on(GPIO_TypeDef *gpio, uint16_t pin) {
gpio->BSRR = pin;
}
Всё хорошо, пока кто-то не передаст нулевой указатель. Rust не даст вам этого сделать:
// Rust code
fn led_on(gpio: &mut GpioRegisters, pin: u16) {
gpio.bsrr.write(|w| w.bits(pin.into()));
}
Здесь &mut GpioRegisters — это гарантия, что ссылка валидна и доступ к регистрам эксклюзивен. Компилятор просто не пропустит опасный код.
Нулевая стоимость абстракций
Один из главных страхов embedded-разработчиков — что высокоуровневый язык будет «жрать» такты процессора. Rust придерживается принципа zero-cost abstractions: вы платите только за то, что используете. Итераторы, замыкания, трейты — всё это компилируется в такой же эффективный машинный код, как ручное разворачивание циклов на C.
Более того, в embedded-экосистеме Rust есть крейты, которые работают на уровне отдельных инструкций. Например, cortex-m-rt генерирует вектор прерываний и стартап-код без единой лишней инструкции.
Реальный проект: мигаем светодиодом на Rust (STM32)
Давайте напишем простую программу для платы STM32F3Discovery. Мы будем использовать cortex-m-quickstart и HAL от сообщества embedded rust.
Шаг 1: Настройка проекта
Убедитесь, что у вас установлен Rust и тулчейн для ARM Cortex-M:
rustup target add thumbv7em-none-eabihf
cargo install cargo-generate
Создаём проект по шаблону:
cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
Шаг 2: Пишем код
Откроем src/main.rs и напишем классический Blinky:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f3xx_hal::{pac, prelude::*};
#[entry]
fn main() -> ! {
// Получаем доступ к периферии
let dp = pac::Peripherals::take().unwrap();
let mut rcc = dp.RCC.constrain();
let mut gpioe = dp.GPIOE.split(&mut rcc.ahb);
// Настраиваем пин PE9 как выход (светодиод LD1)
let mut led = gpioe
.pe9
.into_push_pull_output(&mut gpioe.moder, &mut gpioe.otyper);
loop {
led.set_high().unwrap();
cortex_m::asm::delay(8_000_000);
led.set_low().unwrap();
cortex_m::asm::delay(8_000_000);
}
}
Что здесь происходит?
#![no_std]— мы не используем стандартную библиотеку (нет ОС).#![no_main]— точка входа —#[entry], а не стандартный main.stm32f3xx_hal— аппаратная абстракция (HAL) для конкретного микроконтроллера.into_push_pull_output— безопасная настройка пина. В C вы бы возились с регистрами MODER и OTYPER вручную.
Собираем и прошиваем:
cargo build --release
cargo flash --chip STM32F303VCT6
Светодиод мигает. И никаких segfault'ов.
Экосистема: что нужно знать новичку
Чтобы начать программирование микроконтроллеров на Rust, вам понадобятся три вещи:
1. Крейт embedded-hal
Это абстрактный слой, который определяет трейты для GPIO, SPI, I2C, UART и т.д. Любой HAL для конкретного чипа (STM32, nRF, ESP32) реализует эти трейты. Это значит, что ваш код драйвера будет переносимым между разными микроконтроллерами.
2. cortex-m и cortex-m-rt
Базовые крейты для работы с ARM Cortex-M. cortex-m-rt отвечает за векторы прерываний, стартап-код и обработку исключений.
3. panic-halt или panic-itm
В no_std нет паники по умолчанию. Эти крейты определяют поведение при панике: остановка или вывод отладочной информации.
Rust vs C: холивар на практике
Давайте сравним один и тот же кусок кода — инициализация UART на STM32.
C (с использованием HAL от ST)
// C code
UART_HandleTypeDef huart2;
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init