Мир микроконтроллеров десятилетиями жил простым набором: язык C, куски ассемблера в стартовом коде и большой рюкзак привычек. За последние годы на стол легли альтернативы: Rust с гарантиями владения памятью и первым квалифицированным по ISO 26262 тулчейном, C++ с современным урезанным подмножеством и правилами AUTOSAR. Выбор между тремя языками решается не вкусовщиной, а матрицей из сертификации, состава команды, зрелости вендорских библиотек и горизонта поддержки изделия. Разберём, что каждый язык реально даёт на плате с ядром Cortex-M, где прячется цена перехода и когда чистый C остаётся единственным рабочим ответом.

Тридцать лет C в прошивках и объективные причины такого долголетия

Каждый производитель чипов поставляет стартовый набор на C. STM32Cube для серии STM32, ESP-IDF для ESP32, свои пакеты у Nordic и NXP. Прошивка разговаривает с периферией через структуры регистров, драйверы вендора и генераторы кода вроде STM32CubeMX, и всё это написано на C с разной степенью аккуратности. Инструментарий оттачивался десятилетиями: IAR и Keil MDK в коммерческих проектах, Green Hills в сертифицируемых, свободный arm-none-eabi-gcc во всех остальных. Операционные системы реального времени, от FreeRTOS до Zephyr, тоже написаны на C, и мост к ним из другого языка всегда строится через обёртку.

Классическое мигание светодиодом на STM32 выглядит так:

HAL_Init();
SystemClock_Config();
__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitTypeDef pin = {0};
pin.Pin = GPIO_PIN_5;
pin.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &pin);

while (1) {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    HAL_Delay(500);
}

Сильная сторона C в тишине: код делает ровно то, что написано, без скрытых вызовов конструкторов и без прослоек. Слабая сторона ровно там же. Запись за границу буфера не останавливается, пока не испортит стек. Гонка между обработчиком прерывания и главным циклом не ловится компилятором. Переполнение стека объявляет о себе зависанием в поле, у заказчика, в самый дорогой момент. Статические анализаторы вроде Polyspace и Coverity вылавливают часть бед заранее, правила MISRA C:2012 запрещают опасные идиомы, но дисциплина остаётся на людях, а люди устают.

Инерция C подкреплена экономикой: инженер с опытом STM32Cube переезжает на ESP-IDF за две недели, а анонс новой линейки начинается обещанием сохранить кодовую базу.

Rust на плате no_std и типы вместо ручных проверок

Rust в прошивках живёт без операционной системы и без стандартной библиотеки: атрибут no_std отключает std и оставляет компактный core. Аллокатора нет по умолчанию, коллекции фиксированного размера даёт библиотека heapless, и ёмкость буфера известна на этапе компиляции. Периферию описывают через пакеты доступа к регистрам, сгенерированные инструментом svd2rust из файлов SVD производителя, а слой embedded-hal стандартизует пины, шины SPI и I2C, таймеры и преобразователи между семействами чипов.

Мигание на STM32 в переводе на Rust:

#![no_std]
#![no_main]

use cortex_m_rt::entry;
use stm32f4xx_hal as hal;
use stm32f4xx_hal::prelude::*;

#[entry]
fn main() -> ! {
    let dp = hal::pac::Peripherals::take().unwrap();
    let rcc = dp.RCC.constrain();
    let _clocks = rcc.cfgr.sysclk(84.MHz()).freeze();
    let gpioc = dp.GPIOC.split();
    let mut led = gpioc.pc13.into_push_pull_output();
    loop {
        led.set_high().ok();
        cortex_m::asm::delay(8_000_000);
        led.set_low().ok();
        cortex_m::asm::delay(8_000_000);
    }
}

Фокус в типе пина: gpioc.pc13 становится выходом только после into_push_pull_output, и попытка прочитать его как вход дальше по коду не скомпилируется. Ошибка конфигурации становится ошибкой сборки, а не сюрпризом в поле. Прошивка и отладка идут через probe-rs одной командой:

rustup target add thumbv7em-none-eabihf
cargo build --release --target thumbv7em-none-eabihf
cargo embed --chip stm32f407vg

Локальная конфигурация проекта закрывает ежедневную рутину:

[build]
target = "thumbv7em-none-eabihf"

[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip stm32f407vg"

Пакетный менеджер cargo подтягивает драйверы из репозитория крейтов с версиями и контрольными суммами, cargo audit проверяет зависимости на известные бреши. В мире C зависимость это скопированная папка из архива вендора, и её свежесть никто не отслеживает. Цена перехода честная. Язык с владением и временем жизни требует месяцев вхождения, пул разработчиков уже, а вендорский SDK остаётся на C и требует unsafe мостов. Для ядер Xtensa у ESP32 основному компилятору Rust нужен форк от команды esp-rs, зато сам производитель официально поддерживает Rust стек для своих чипов, и это заметный сдвиг на рынке. Компактные логи defmt и защита flip-link, сдвигающая стек вниз ради превращения переполнения в явную аппаратную ошибку, добавляют ежедневной работе приятной резвости.

Асинхронность embassy и планировщики без классической RTOS

Фреймворк embassy переоткрывает структуру прошивки: вместо ручных конечных автоматов логика пишется как обычный последовательный код с await. Таймеры становятся футурами, драйверы выглядят блокирующими, а на деле паркуются до события, и всё это без кучи и без отдельной операционной системы:

use embassy_time::Timer;

#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
    loop {
        led.set_high().await;
        Timer::after_millis(500).await;
        led.set_low().await;
        Timer::after_millis(500).await;
    }
}

Альтернатива по духу это RTIC: статический планировщик, где приоритеты заданы прерываниями, а разделяемые ресурсы проверяются на этапе компиляции. Оба подхода убирают классическую беду C, гонку за разделяемое состояние между прерыванием и циклом, потому что компилятор не даст захватить ресурс без гарантий. Плата за прогресс: асинхронный стек отлаживать сложнее линейного, инженеров с таким опытом меньше, а покрытие периферии крейтами зависит от энтузиазма сообщества.

У асинхронности есть и стоп-сигнал. Жёсткие дедлайны в десятки микросекунд надёжнее закрывает классический планировщик с приоритетным вытеснением, где потолок задержек измерен, а сертифицированные ядра дают аудитору готовые доказательства. Async хорош для связности: сетевые стеки, шины датчиков, интерфейсы, где задержки в миллисекунды не ощущаются.

C++ на микроконтроллерах где RAII помогает а где подводит

Современный C++ в прошивке это не философия больших иерархий, а урезанное подмножество: исключения и RTTI отключаются флагами fno-exceptions и fno-rtti, динамическая память исключается дисциплиной. В награду остаются RAII для владения периферией и буферами DMA, constexpr для расчёта таблиц на этапе компиляции и шаблоны без накладных расходов в рантайме:

class Led {
 public:
  explicit Led(GpioPin pin) noexcept : pin_(pin) {}
  void Toggle() noexcept { HAL_GPIO_TogglePin(pin_.port, pin_.mask); }
 private:
  GpioPin pin_;
};

Ловушки расставлены щедро. Глобальные объекты инициализируются до входа в main, и порядок между единицами трансляции не определён: конструктор, дернувший периферию до настройки тактирования, роняет плату в самое неудобное время. std::function и std::vector тянут кучу, шаблоны раздуливают флеш, если позволить себе генерировать метод под каждую комбинацию аргументов. Руководства AUTOSAR C++14 и свежая редакция MISRA C++ 2023 года срезают опасные углы, библиотека ETL заменяет контейнеры стандартной библиотеки версиями с фиксированной ёмкостью. В руках команды с дисциплиной C++ сокращает шаблонный код без цены в рантайме, в руках команды без дисциплины превращает сборку в поле для многодневной отладки ошибок шаблонов.

Статистика ошибок памяти из открытых отчётов и её перенос на прошивки

Индустрия получила редкий случай крупного эксперимента, когда Google перевела часть системного кода Android на Rust и отчиталась в конце 2022 года. Доля уязвимостей, связанных с памятью, среди всех багов платформы упала с 76 процентов в 2019 году до 35 процентов в 2022. В Android 13 на Rust написано около 21 процента нового нативного кода, полтора миллиона строк, и на момент отчёта в этом коде не нашли ни одной уязвимости памяти. Крупные отчёты о безопасности десктопов и браузеров годами называли похожую причину у большей части критических багов: выход за границы буфера, висячий указатель, гонка за данные.

Для прошивок цифры переносятся с поправкой на жёсткость среды. На Cortex-M нет MMU и нет рандомизации адресов: запись мимо буфера способна испортить что угодно, от соседних задач до конфигурации тактирования, и лечится только полным сбросом. Гонка между обработчиком прерывания и главным циклом в C не видна компилятору и вылавливается осциллографом. В Rust нечестный доступ к памяти возможен только внутри блоков unsafe, которые видны в код ревью как исключение из правила. Логические ошибки это не отменяет: ошибка в формуле регулятора или в парсере протокола не зависит от языка, и тесты никто не отменял.

Отдельный класс ошибок это оптимизатор. Флаг, который меняет обработчик прерывания, обязан быть помечен volatile, иначе компилятор вынесет чтение из цикла, и прошивка зависнет в ожидании события, которое уже случилось. В C это знание из шрамов. В Rust разделяемое состояние строится на атомиках из core::sync::atomic или на каркасе фреймворка, и большинство ошибок доступа просто не собирается.

Сертификация ISO 26262 и IEC 61508 и что изменил Ferrocene

Автомобильный стандарт ISO 26262 делит изделия на уровни ASIL от A до D, промышленный IEC 61508 задаёт уровни SIL, медицинская IEC 62304 различает классы A, B и C, авионика живёт по DO-178C. Сертификация проверяет процесс и инструментарий, а не язык: вендоры IAR и Green Hills десятилетиями поставляют квалифицированные компиляторы C, к ним накоплены горы аудиторских артефактов, и проверяющие знают их наизусть.

В конце 2023 года эту стену пробил Ferrocene, первый квалифицированный тулчейн Rust: допуски ISO 26262 вплоть до ASIL D, IEC 61508 до SIL 3 с маршрутом к SIL 4 при сертификации заказчика и IEC 62304 класса C. Юридический мост в критические системы появился, но шершавости остались: происхождение сторонних крейтов, документация для аудиторов и формальная верификация сгенерированного кода требуют собственного процесса, которого у C складывался двадцать лет.

У открытого компилятора C маршрут в сертификацию тоже выстроен годами: проверенные сборки и пакеты доказательств продаются вместе с поддержкой. Ferrocene закрывает сам компилятор Rust, но не закрывает зависимости: каждую стороннюю библиотеку придётся описать в плане безопасности, а критичные пути пересчитать собственными тестами.

Матрица выбора языка под проект команду и горизонт поддержки

Практический фильтр из полевых решений складывается из четырёх вопросов:
1. сертификация: нужен ли изделию уровень ASIL D или SIL 4 и есть ли у аудиторов опыт с выбранным тулчейном;
2. периферия: закрыт ли целевой чип зрелыми драйверами или придётся портировать вендорский SDK;
3. команда: есть ли в штате инженеры с опытом языка или бюджет на полгода вхождения;
4. горизонт: будет ли изделие жить десять лет и кто будет поддерживать прошивку в конце срока.

Чистый C остаётся ответом для сертифицируемых изделий с готовым вендорским SDK, для жёстких ограничений флеша и для команд, чей опыт весь в C. Rust силён у новых сетевых устройств с каналом обновлений: гарантии памяти окупаются там, где прошивку будут менять годами, а производитель чипа официально поддерживает Rust стек. C++ занимает середину там, где нужны абстракции без цены в рантайме и есть дисциплина по MISRA C++. Смешанные прошивки тоже реальность: новый модуль пишут на Rust и подключают к ядру на C через extern C обёртки, генерируемые cbindgen, не трогая работающий код.

С людьми картина симметричная: рынок C огромен и умел, разработчиков на Rust меньше, зато обучение идёт по официальной книге и готовым примерам для плат, и порог входа ниже репутации.

Три языка не соревнуются в одной весовой категории. C держит инерцию вендоров и аудитов, Rust переносит на компилятор проверки, которые раньше делали люди, C++ даёт абстракции тем, кто готов платить дисциплиной. Ответственный выбор выглядит скучно: матрица из четырёх вопросов, честный аудит рисков и решение на пять лет вперёд, а не на одну вдохновляющую статью о новом языке. Именно поэтому большинство прошивок в мире всё ещё пишут на C, и именно поэтому каждая новая платформа всё чаще получает официальную поддержку Rust с первого дня.