Rust врывается в мир embedded: почему Си больше не король

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вступление: Эпоха «железа» меняет язык

Если вы когда-нибудь писали прошивку для микроконтроллера на 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                

Блоги

Рекомендаций по книгам