Тестирование прошивки: хост, симулятор, стенд и статический анализ
Вопрос «как вы тестируете прошивку?» на собеседовании во встраиваемую команду выдаёт зрелость проекта быстрее любого другого. Три типичных ответа: «руками, на плате», «есть отдел испытаний, они прогоняют регламент» и — гораздо реже — «две тысячи юнит-тестов на хосте плюс ночной прогон на стенде».
Причина, по которой первые два ответа так живучи, не в лени. Обычная пирамида тестов молча опирается на четыре допущения, и во встраиваемом мире не выполняется ни одно:
- Код можно запустить на машине разработчика. Нельзя: он собран под другую архитектуру и обращается к регистрам, которых на хосте не существует.
- Окружение воспроизводимо. Нет: датчик даёт разный шум, шина иногда сбоит, а батарея садится.
- Тест дёшев. Нет: прогон на плате требует физической платы, прошивки, сброса — и плат столько, сколько людей в команде, а не сколько запусков в CI.
- Состояние сбрасывается между тестами. Нет: у прошивки есть флеш, который переживает перезагрузку, и физический мир вокруг.
При этом цена пропущенной ошибки здесь выше, чем почти где угодно: обновление по воздуху есть не у всех, а откатить партию из десяти тысяч изделий нельзя в принципе. Отсюда практический вывод, вокруг которого построена вся статья: тестируемость прошивки — это в первую очередь свойство её архитектуры, и только во вторую — набор инструментов. Код, где расчёт скользящего среднего живёт в одном файле с записью в ADC->CR2, не тестируется ничем, кроме паяльника. Код с проведённым швом проверяется обычным gcc за миллисекунды — и большинство ошибок ловится там.
Три уровня, три разные цены
У проверки прошивки есть ровно три площадки, и выбор между ними механический — по тому, какое утверждение вы хотите доказать.
| Площадка | Что проверяет | Цена прогона | Сколько тестов реально |
|---|---|---|---|
Хост (gcc, clang) |
логика: автоматы, парсеры, фильтры, расчёты, обработка ошибок | миллисекунды | сотни и тысячи |
Симулятор (Renode, QEMU, native_sim) |
интеграция: порядок инициализации, драйвер против модели устройства, работа RTOS | секунды | десятки |
| Железный стенд (HIL) | физика и время: сроки, ток, фронты, реальный датчик, сброс питания | минуты | единицы и десятки |
Ключевая ошибка новичков — пытаться доказать всё на нижнем правом уровне: гонять на плате то, что прекрасно проверяется на хосте. Тесты становятся медленными, хрупкими и их перестают чинить. Обратная ошибка — считать, что раз логика покрыта на хосте, железо можно не трогать: ни один хостовый тест не поймает, что фронт I2C не успевает подняться из-за слишком крупной подтяжки.
Шов: почему это разговор про архитектуру
Тестируемый код отличается от нетестируемого одной вещью — наличием шва: места, где обращение к внешнему миру выражено интерфейсом, а не прямой записью в регистр.
Плохой код и хороший отличаются буквально несколькими строчками:
/* НЕ ТЕСТИРУЕТСЯ: логика намертво сцеплена с чипом и с системным временем */
float read_pressure(void)
{
ADC1->CR2 |= ADC_CR2_SWSTART;
while (!(ADC1->SR & ADC_SR_EOC)) { }
uint16_t raw = (uint16_t)ADC1->DR;
if (HAL_GetTick() - g_last > 1000u) { g_offset = raw; g_last = HAL_GetTick(); }
return (float)(raw - g_offset) * 0.0244f;
}
/* ТЕСТИРУЕТСЯ: порт — узкий интерфейс, время и АЦП пришли снаружи */
typedef struct {
bool (*adc_read)(void *ctx, uint16_t *out); /* чем измеряем */
uint32_t (*now_ms)(void *ctx); /* что считаем временем */
void *ctx;
} sensor_port_t;
typedef struct { uint16_t offset; uint32_t last_cal_ms; } pressure_state_t;
bool pressure_update(pressure_state_t *st, const sensor_port_t *port, float *out_kpa)
{
uint16_t raw;
if (!port->adc_read(port->ctx, &raw)) return false; /* отказ датчика — часть логики */
uint32_t now = port->now_ms(port->ctx);
if ((uint32_t)(now - st->last_cal_ms) > 1000u) { /* вычитание переживает переполнение */
st->offset = raw;
st->last_cal_ms = now;
}
*out_kpa = (float)((int32_t)raw - st->offset) * 0.0244f;
return true;
}
Второй вариант компилируется обычным gcc без единого заголовка вендора. Это и есть рабочий критерий: если файл с логикой не собирается хостовым компилятором — шва нет. Стоимость шва — один косвенный вызов там, где следующая же строка ждёт ответа шины сотни микросекунд; на фоне физики это ничто. Там, где даже такой ценой платить нельзя (обработчик прерывания на сотню наносекунд), шов делают на этапе компоновки: одинаковые сигнатуры, разные файлы реализации для целевой и хостовой сборки. Про архитектурную сторону вопроса — слоистая и гексагональная архитектуры и приёмы отделения логики от ввода-вывода.
Три вещи, которые обязаны быть портами почти всегда: время (now_ms), энергонезависимая память (config_save) и шина к внешней микросхеме (i2c_xfer). Именно они делают тесты недетерминированными и требуют железа.
Юнит-тесты на хосте: две тысячи прогонов в секунду
Инструменты: Unity плюс Ceedling (генерация моков через CMock, минимум ручной работы), CppUTest (популярен благодаря книге Гренинга), GoogleTest для проектов на C++. Разница между ними меньше, чем кажется; выбирать стоит по тому, что команда готова поддерживать.
/* test_pressure.c — обычная хостовая сборка: gcc -std=c11 -O1 -g -fsanitize=address,undefined */
#include "unity.h"
#include "pressure.h"
/* Подставной порт: очередь заранее заданных отсчётов и часы, которые двигаем руками */
typedef struct { uint16_t samples[8]; size_t idx, n; uint32_t clock_ms; bool fail; } fake_t;
static bool fake_adc(void *c, uint16_t *out) {
fake_t *f = c;
if (f->fail || f->idx >= f->n) return false;
*out = f->samples[f->idx++];
return true;
}
static uint32_t fake_now(void *c) { return ((fake_t *)c)->clock_ms; }
void test_first_read_sets_zero(void) /* первое измерение калибрует ноль */
{
fake_t f = { .samples = {2048, 2148}, .n = 2, .clock_ms = 5000 };
sensor_port_t port = { fake_adc, fake_now, &f };
pressure_state_t st = { 0 };
float kpa = 0.0f;
TEST_ASSERT_TRUE(pressure_update(&st, &port, &kpa));
TEST_ASSERT_EQUAL_FLOAT(0.0f, kpa); /* первый отсчёт стал нулём шкалы */
f.clock_ms += 100; /* окно калибровки ещё не прошло */
TEST_ASSERT_TRUE(pressure_update(&st, &port, &kpa));
TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.44f, kpa); /* 100 отсчётов * 0.0244 */
}
void test_sensor_failure_keeps_state(void) /* отказ датчика не портит состояние */
{
fake_t f = { .fail = true, .clock_ms = 1 };
sensor_port_t port = { fake_adc, fake_now, &f };
pressure_state_t st = { .offset = 2048, .last_cal_ms = 0 };
float kpa = -1.0f;
TEST_ASSERT_FALSE(pressure_update(&st, &port, &kpa));
TEST_ASSERT_EQUAL_UINT16(2048, st.offset); /* состояние не тронуто */
}
/* Отдельный тест на переполнение счётчика миллисекунд: раз в 49.7 суток
HAL_GetTick обнуляется, и наивное сравнение now > last + T ломается. */
void test_tick_wraparound(void) /* переполнение счётчика не ломает калибровку */
{
fake_t f = { .samples = {100, 200}, .n = 2, .clock_ms = 0xFFFFFF00u };
sensor_port_t port = { fake_adc, fake_now, &f };
pressure_state_t st = { .last_cal_ms = 0xFFFFFF00u - 500u };
float kpa = 0.0f;
f.clock_ms += 1000; /* прошли через ноль */
TEST_ASSERT_TRUE(pressure_update(&st, &port, &kpa));
TEST_ASSERT_EQUAL_UINT32(f.clock_ms, st.last_cal_ms); /* калибровка сработала */
}
Три приёма, которые делают такие тесты по-настоящему полезными:
- Санитайзеры. Хостовая сборка с
-fsanitize=address,undefinedловит выход за границу массива и знаковое переполнение — на целевом чипе те же ошибки просто портят соседнюю переменную и всплывают через месяц. Это едва ли не главный аргумент в пользу хостовых тестов вообще; подробнее про класс этих ошибок — в главе про неопределённое поведение. - Тесты на переполнение времени. Ошибка «работает 49 суток, потом зависает» находится за секунду, если время — параметр, и не находится никогда, если время берётся из
HAL_GetTick(). - Покрытие как навигатор, а не цель.
gcc --coverageплюсgcovrпоказывает, какие ветки обработки ошибок никто ни разу не выполнил. Обычно это ровно те ветки, которые сработают в поле.
# Полный прогон хостовых тестов: сборка, санитайзеры, покрытие — секунды
gcc -std=c11 -O1 -g -Wall -Wextra -Werror -fsanitize=address,undefined --coverage \
-Isrc -Itests/unity src/pressure.c tests/unity/unity.c tests/test_pressure.c -o build/tests
./build/tests && gcovr --root . --fail-under-line 70 --exclude tests/
Фаззинг: единственный способ проверить парсер кадров
Разбор пакета из внешнего мира — код, к которому противник (или просто сломанное устройство на шине) имеет прямой доступ, и одновременно самый плотный по ветвлениям. Тестами вручную его покрыть нельзя: интересные случаи — не «правильный кадр», а «длина 0xFFFF при буфере 64 байта».
/* fuzz_frame.c: собирается clang -g -O1 -fsanitize=fuzzer,address,undefined */
#include <stdint.h>
#include <stddef.h>
#include "frame.h"
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
frame_t out;
(void)frame_parse(data, size, &out); /* нас интересует не результат, а падение */
return 0;
}
Полчаса такого прогона на ноутбуке обычно находит два-три способа выйти за границу буфера — в коде, который «работает в проде год». Это дёшево и делается один раз: фаззинг и его место описан в треке тестирования, а требования к разбору чужих данных — в главе про безопасное программирование.
Рядом стоят тесты свойств (property-based): для протокола проверяется инвариант «parse(serialize(x)) == x для любого допустимого x», для фильтра — «выход монотонной последовательности монотонен», для очереди — «после N записей и N чтений очередь пуста». Одна такая проверка заменяет двадцать примеров.
Симуляция: между хостом и платой
Хостовые тесты не видят прерываний, порядка инициализации периферии и планировщика RTOS. Между «логика верна» и «работает на плате» есть промежуточный слой, и он полезнее, чем принято думать.
- Renode — функциональный симулятор целых плат: ядро, память, периферия, несколько устройств на одной шине и даже радиоканал между ними. Умеет исполнять ровно тот
.elf, что уезжает в чип. Отдельная сила — детерминизм: гонка, воспроизводящаяся раз в час на столе, здесь воспроизводится каждый раз. - QEMU (
-machine mps2-an385,netduinoplus2) — быстрее, но моделей периферии заметно меньше; хорош для проверки самого ядра и RTOS. native_simв Zephyr — сборка всей прошивки, включая планировщик и стек драйверов, в обычный процесс Linux. Пожалуй, лучший компромисс, если проект живёт на Zephyr.- Wokwi — браузерный симулятор для ESP32, RP2040 и AVR; в CI применим через свой раннер, удобен для демонстраций и учебных сборок.
# tests/renode/blink.robot — сценарий Renode: запустить .elf и проверить поведение вывода
# Тест проверяет то, чего хостовая сборка не видит: таймер, прерывание, реальный NVIC
*** Test Cases ***
Светодиод мигает раз в секунду
Execute Command mach create "stm32"
Execute Command machine LoadPlatformDescription @platforms/boards/stm32f4_discovery.repl
Execute Command sysbus LoadELF @build/firmware.elf
Create Terminal Tester sysbus.usart2
Start Emulation
Wait For Line On Uart tick 1 timeout=1.5
Wait For Line On Uart tick 2 timeout=1.5
Границы честности симулятора надо понимать: он моделирует цифровую логику, а не физику. Просевшее питание, звон на фронте, дребезг контакта, нагрев и севшая батарея в Renode не существуют. Всё, что вы выясняете симуляцией, — «программа делает то, что задумано»; «устройство работает» доказывается только железом.
Стенд hardware-in-the-loop
HIL — это плата, окружённая приборами, которыми управляет тот же CI, что собирает прошивку. Минимальный состав, который окупается на второй месяц:
- отладочный зонд (
probe-rs, OpenOCD) — прошить, сбросить, забрать логи по RTT; - реле или управляемый источник питания — снять и подать питание, а не «нажать reset»: это единственный способ проверить холодный старт и запись во флеш при обрыве;
- логический анализатор с программным интерфейсом (
sigrok-cli) — доказать, что на шине действительно то, что задумано; - измеритель тока (например, Nordic PPK2 или INA219 на отдельной плате) — превратить энергопотребление в число, которое можно сравнивать между сборками;
- стимул: вторая плата, изображающая датчик или мастера шины.
# tests/hil/test_boot.py — HIL-тест как обычный pytest
import subprocess, time, pytest
@pytest.fixture
def dut(power_relay, rtt):
power_relay.off(); time.sleep(1.0)
subprocess.run(["probe-rs", "download", "--chip", "STM32F411CEUx",
"build/firmware.elf"], check=True)
power_relay.on()
yield rtt
power_relay.off()
def test_холодный_старт_за_500_мс(dut):
t0 = time.monotonic()
assert dut.wait_for("boot ok", timeout=2.0), "устройство не стартовало"
assert time.monotonic() - t0 < 0.5, "старт дольше бюджета: проверьте задержки в init"
def test_обрыв_питания_при_записи_конфига(dut, power_relay):
dut.send("cfg set threshold 42")
time.sleep(0.003) # ровно в окно записи страницы флеша
power_relay.off(); time.sleep(0.5); power_relay.on()
assert dut.wait_for("boot ok", timeout=2.0)
assert dut.query("cfg get threshold") in ("42", "41"), "конфиг повреждён обрывом"
Последнее утверждение — образец правильного HIL-теста: он допускает оба корректных исхода (новое значение записалось или осталось старое) и запрещает третий — повреждённую конфигурацию. Требовать от прошивки «всегда 42» здесь было бы неверно: обрыв в момент записи имеет право потерять транзакцию, но не имеет права оставить мусор. Механика такой записи разобрана в главе про питание и ограничения.
Организационные детали, о которых узнают на своих ошибках: стенд обязан уметь восстанавливаться сам (зависшую плату спасает реле, а не человек); тесты нумеруют платы и не делят одну между параллельными задачами; флейки не «перезапускают», а расследуют — на железе мигающий тест обычно означает настоящую гонку. Для сложных ферм есть LabGrid и pytest-embedded, покрывающие резервирование плат и удалённый доступ; общая дисциплина — в главах тесты в CI и стратегия автоматизации.
Инъекция отказов: проверяем то, ради чего всё затевалось
Прошивка почти всегда падает не на «нормальном» сценарии, а на отказе, который никто не проиграл. Хорошая новость: большинство отказов инжектируется дёшево, причём часть — прямо на хосте.
| Отказ | Как воспроизвести | Что обязано произойти |
|---|---|---|
| Датчик не отвечает | подставной порт возвращает ошибку; на стенде — вынуть перемычку | деградация, а не зависание в while (!flag) |
| Шина зависла в низком уровне | стенд: замкнуть SDA на землю |
таймаут, переинициализация, счётчик в журнале |
| Обрыв питания при записи | реле в момент записи страницы | конфиг цел: старая или новая версия, не мусор |
| Просадка питания | источник опускает напряжение до порога | контролируемый сброс, а не непредсказуемое поведение |
| Мусор в эфире | фаззинг парсера кадров на хосте | кадр отброшен, состояние не изменилось |
| Зависание задачи | тестовая сборка не кормит сторожевой таймер | сброс и запись причины в журнал |
| Флеш исчерпал ресурс | подставной драйвер возвращает ошибку записи | переход в режим только чтения, диагностика наружу |
Отдельный обязательный тест — проверка самого сторожевого таймера. Устройство должно иметь отладочную команду «зависнуть навсегда», и HIL-тест обязан убедиться, что после неё плата перезагружается за отведённое время. Ненастроенный или неправильно кормимый watchdog — классика: он «есть» три года и ни разу не срабатывал, потому что кормится из таймерного прерывания, которое живо даже при мёртвом приложении.
Регрессии, которые не выражаются словом «упало»
У прошивки есть метрики, которые ухудшаются постепенно и никогда не дают красный тест сами по себе. Их нужно измерять и сравнивать с порогом на каждом изменении:
- размер прошивки —
sizeиз прошлой главы, порог в процентах от флеша; - глубина стека — сумма
.suпо графу вызовов и watermark с прогона на стенде; - худшее время цикла управления — счётчик
DWT->CYCCNTвокруг критичного участка, максимум за прогон; - средний ток — измеритель на стенде, сравнение с бюджетом из главы про энергию;
- время холодного старта — от подачи питания до готовности.
# metrics.sh — выгружаем метрики сборки в JSON и сравниваем с базовой линией
arm-none-eabi-size -A build/firmware.elf | awk '/\.text|\.data|\.bss/ {s[$1]=$2}
END {printf "{\"flash\":%d,\"ram\":%d}\n", s[".text"]+s[".data"], s[".data"]+s[".bss"]}' \
> build/metrics.json
python3 tools/compare_metrics.py build/metrics.json baseline.json --max-growth 3%
Правило, экономящее нервы: порог должен быть относительным (рост более 3 % за один PR) и обсуждаемым. Абсолютный лимит «не больше 200 КБ» либо не срабатывает годами, либо блокирует релиз в самый неудобный день. Методика измерений вообще — в главе про измерение производительности.
Компилятор и статический анализ: самый дешёвый тест
Прежде чем писать первый юнит-тест, стоит включить то, что уже куплено и ничего не стоит.
# Набор, который окупается на первой же неделе
-Wall -Wextra -Werror -Wconversion -Wsign-conversion -Wshadow -Wundef
-Wdouble-promotion -Wstack-usage=1024 -Wformat=2 -Wswitch-enum -Wnull-dereference
clang-tidy src/*.c --checks=bugprone-*,cert-*,clang-analyzer-* -- -Isrc
cppcheck --enable=warning,style --error-exitcode=1 --std=c11 src/
-Wconversion при первом включении даёт сотни предупреждений и вызывает желание его выключить. Не выключайте: каждое из них — место, где uint16_t молча превратился в int8_t, и половина «плавающих» багов встраиваемых проектов живёт именно там. Для критичных изделий сверху ложатся отраслевые своды правил (MISRA C, -fanalyzer в GCC, коммерческие анализаторы) — они дороги и шумны, но в медицине, автомобиле и авиации обсуждению не подлежат.
Отдельный разговор про assert. Стандартный тянет строки в .rodata и в релизе обычно выключается — а зря: проверка инварианта в поле полезнее, чем непредсказуемое поведение. Компромисс — свой макрос, который сохраняет файл и строку в компактном виде и перезагружает устройство с записью причины в журнал:
#define FW_ASSERT(cond) do { if (!(cond)) fw_assert_fail(__LINE__, FILE_ID); } while (0)
/* fw_assert_fail пишет строку и идентификатор файла в .noinit, затем NVIC_SystemReset().
После перезагрузки причина уезжает наружу — это и есть чёрный ящик из главы 11. */
Как это выглядит целиком в CI
секунды"] FMT --> HOST["Хостовая сборка тестов:
gcc + ASan/UBSan, покрытие"] HOST --> SA["clang-tidy, cppcheck
предупреждения = ошибки"] SA --> XBUILD["Кросс-сборка прошивки
-Os -flto, три конфигурации плат"] XBUILD --> SIZE["Метрики: флеш, ОЗУ, стек
сравнение с базовой линией"] SIZE --> SIM["Renode: 20 сценариев
старт, драйверы, RTOS"] SIM --> ART["Артефакты: elf, map, su, bin
хранить вечно"] ART --> OK["PR можно вливать"] OK --> NIGHT["Ночью: HIL-ферма"] NIGHT --> HIL1["Холодный старт и обрыв питания"] NIGHT --> HIL2["Тракт датчиков и шины"] NIGHT --> HIL3["Ток и сроки, сравнение с бюджетом"] NIGHT --> SOAK["Прогон 12 часов:
утечки памяти, переполнение счётчиков"] HIL1 --> REL["Кандидат в релиз"] HIL2 --> REL HIL3 --> REL SOAK --> REL REL --> SIGN["Подпись образа и публикация
для OTA"] FUZZ["Раз в неделю: фаззинг парсеров"] --> REL
Разделение принципиальное: на каждый PR — только то, что укладывается в 5–10 минут и не требует железа; всё, что требует плат и времени, уезжает в ночной прогон и на релизного кандидата. Иначе очередь к единственному стенду становится узким местом всей команды. Общая механика конвейеров — основы CI и современные платформы.
Минимальный набор для команды из трёх человек
Полный конвейер выше — цель, а не стартовая точка. Порядок внедрения по отдаче на вложенный час:
Чего сознательно не делать: гнаться за покрытием драйверов (там мало ветвлений и много обращений к железу — цена теста высока, отдача мала); писать моки на вендорский HAL целиком (тонете в поддержке); тестировать на плате то, что проверяется на хосте. И отдельно: не пытайтесь ввести всё сразу — набор из пяти хостовых тестов и одного HIL-сценария, который живёт год, полезнее сотни, заброшенной через месяц.
Типичные ошибки
- Считать, что «нет тестов, потому что железо». Восемьдесят процентов прошивки — логика, которая не знает про регистры и проверяется на хосте.
- Смешивать логику и доступ к регистрам в одной функции — единственная ошибка, из-за которой невозможно всё остальное.
- Брать время из
HAL_GetTick()внутри логики — тесты становятся недетерминированными, а переполнение счётчика не проверяется никогда. - Гонять на плате то, что проверяется на хосте — очередь к стенду и медленный CI, который перестают ждать.
- Перезапускать флейки вместо расследования — на железе мигающий тест почти всегда означает настоящую гонку.
- Проверять только счастливый путь. Ветки обработки отказов — те самые, что сработают в поле, и именно они не покрыты.
- Не проверять сторожевой таймер — он «есть» три года и ни разу не сработал, потому что кормится из живого прерывания.
- Игнорировать нефункциональные регрессии — прошивка вырастает по килобайту за спринт и упирается во флеш за неделю до релиза.
- Требовать в HIL-тесте единственного исхода там, где корректны два — тест становится источником ложных срабатываний.
- Выключить
-Wconversion, потому что «слишком много ругани» — эта ругань и есть список будущих багов.
Мини-итог
Тестирование прошивки строится на трёх решениях, и первое из них — не про тесты.
- Шов. Логика отделена от железа узкими портами: время, память, шина. Критерий проверяемый: файл с логикой собирается обычным
gccбез заголовков вендора. Нет шва — нет тестов, и никакой инструмент этого не исправит. - Раскладка по цене. Сотни быстрых тестов логики на хосте с санитайзерами; десятки сценариев в симуляторе на порядок инициализации и драйверы; единицы сценариев на железе — там, где решает физика: холодный старт, обрыв питания, сроки, ток. Уровень выбирается по тому, что именно доказывается.
- Метрики как тесты. Размер прошивки, глубина стека, худшее время цикла, средний ток и время старта измеряются на каждой сборке и сравниваются с базовой линией. Эти регрессии не дают красный тест сами по себе, но именно они убивают проекты за неделю до релиза.
Практический старт на завтра: возьмите самый запутанный файл проекта, вынесите из него обращения к регистрам за интерфейс из двух функций, напишите пять тестов на его логику и соберите их обычным gcc с -fsanitize=address,undefined. Почти наверняка первый же прогон найдёт ошибку, которую вы искали на плате.
Источники
- James W. Grenning. «Test-Driven Development for Embedded C» — базовая книга по швам и моделированию железа в тестах: https://pragprog.com/titles/jgade/test-driven-development-for-embedded-c/
- Unity, CMock и Ceedling — https://www.throwtheswitch.org/ ; CppUTest — https://cpputest.github.io/
- Memfault Interrupt. «Unit Testing Basics» — https://interrupt.memfault.com/blog/unit-testing-basics ; «Building a CI Setup for Firmware Projects» — https://interrupt.memfault.com/blog/building-a-ci-setup-for-firmware-projects
- Renode — https://renode.io/ и документация сценариев https://renode.readthedocs.io/en/latest/
- Zephyr.
ztestиnative_sim— https://docs.zephyrproject.org/latest/develop/test/ztest.html - Espressif.
pytest-embeddedдля тестов на реальных платах — https://docs.espressif.com/projects/pytest-embedded/en/latest/ - LabGrid, управление фермой устройств — https://labgrid.readthedocs.io/
- LLVM. libFuzzer — https://llvm.org/docs/LibFuzzer.html ; sanitizers — https://clang.llvm.org/docs/AddressSanitizer.html
- cppcheck — https://cppcheck.sourceforge.io/ ; clang-tidy — https://clang.llvm.org/extra/clang-tidy/ ; GCC static analyzer — https://gcc.gnu.org/onlinedocs/gcc/Static-Analyzer-Options.html
- MISRA C — https://misra.org.uk/ ; Barr Group Embedded C Coding Standard — https://barrgroup.com/embedded-systems/books/embedded-c-coding-standard
- sigrok и
sigrok-cliдля захвата в скриптах — https://sigrok.org/wiki/Sigrok-cli - Nordic Power Profiler Kit II, измерение тока в CI — https://www.nordicsemi.com/Products/Development-hardware/Power-Profiler-Kit-2
Что дальше
Трек «Встраиваемые системы и робототехника» на этом закончен: от карты трека и железа через C без динамики, периферию, прерывания, протоколы, RTOS, энергию, IoT-связь, датчики и приводы, робототехнику и отладку с производством — до сборки прошивки и её тестирования. Куда идти дальше, зависит от того, какая сторона задачи оказалась ближе:
- Дисциплина проверок вообще — принципы и терминология тестирования, интеграционные тесты и TDD и BDD.
- Надёжность и процессы выпуска — релизные стратегии и CD и наблюдаемость и дежурства: те же канареечные раскаты и метрики здоровья, но с другой ценой ошибки.
- Защита устройств — моделирование угроз, прикладная криптография и защита цепочки поставок.
- Вниз, к устройству процессора — трек архитектура процессоров: ISA, конвейеры и пределы по мощности объясняют, почему ваш контроллер устроен именно так.
- Вверх, к автономности роботов — машинное обучение и MLOps, а также научные работы портала: определение и классификация роботизированных систем и результаты проектирования.
Чтобы выбрать следующий трек осознанно, загляните в дорожную карту портала.