Практическое руководство по автоматизации сборки, прошивки и тестирования микроконтроллеров
Зачем это нужно?
Многие embedded-разработчики привыкли работать без автоматизированных тестов, полагаясь на ручное тестирование и отладку через программатор. Это кажется простым и быстрым решением для небольших проектов. Однако при росте кодовой базы и команды такой подход приводит к критическим проблемам: баги возвращаются в новых релизах, знание о системе хранится только в головах разработчиков, а каждое изменение требует длительного ручного тестирования на стенде.
Автоматизация CI/CD для embedded-систем решает эти проблемы, хотя требует начальных усилий на настройку инфраструктуры.
Жесткая правда о embedded-разработке
Типичные отговорки без тестов:
"Это сложная проблема" = "Я не знаю, где баг"
"Нужно протестировать на стенде" = "Надеюсь, что заработает"
"Это аппаратная проблема" = "Не хочу разбираться в коде"
Тесты дают объективность:
Либо тест проходит
Либо тест падает
Либо тестов нет
Ситуация, которая повторяется в 90% компаний:
Нашли баг на стенде (в лучшем случае, если есть стенд)
Разработчик неделю дебажит через J-Link
Меняет одну строчку кода
"Пофиксил!"
Коммитит только фикс без тестов
Через 2 месяца баг возвращается в новом релизе
Новый разработчик тратит еще неделю на поиск
Что по-настоящему происходит:
// ПЛОХО: типичный "фикс" через дебаг
// Было:
if (adc_value > threshold) {
set_alarm();
}
// Стало после 5 дней дебага:
if (adc_value > threshold && !is_calibrating) {
set_alarm();
}
Никто не узнает:
Почему именно такая логика?
Какие edge cases учитывались?
Как воспроизвести проблему?
Что проверялось?
Правильный подход: Test-Driven Bug Fixing:
// 1. Пишем тест, который падает
TEST(AlarmTest, ShouldNotTriggerDuringCalibration) {
start_calibration();
set_adc_value(threshold + 100); // Значение выше порога
process_alarm_logic();
ASSERT_FALSE(alarm_triggered()); // Тест падает!
}
// 2. Фиксим код
if (adc_value > threshold && !is_calibrating) {
set_alarm();
}
// 3. Тест проходит
// 4. Теперь у нас есть регрессионный тест НАВСЕГДА
Профит для менеджмента
Отчет разработчика без тестов:
"Пофиксил баг с ложными срабатываниями сигнализации"
Время: 5 дней
Изменения: 1 строка кода
Гарантии: "вроде работает"
Отчет системы с тестами:
Добавлены регрессионные тесты (в т.ч. воспроизводящий баг): 150 строк
Исправлен баг: 3 строки
Code coverage: +15%
Гарантии: автоматическая проверка на каждом коммите
Профит для разработчика
Без тестов:
"Надо помнить все свои костыли" — когнитивная нагрузка
"Опять этот баг вернулся" — бесконечный рефакторинг одних и тех же мест
"Это не мой баг, это железо глючит" — постоянные споры с hardware team
"Надо прошивать 10 плат вручную" — скучная рутина вместо разработки
С тестами:
"Мои 100 тестов подтверждают, что фикс работает" — уверенность в коде
"CI провалился — значит мой код сломал что-то" — мгновенная обратная связь
"Вот тест, который доказывает проблему с железом" — аргументированные баг-репорты
"Прошил 10 плат одним коммитом" — автоматизация рутины
Профит для команды
Без тестов:
"Кто это сломал?" — поиск виноватого вместо решения проблемы
"У Васи единственный работает, пусть он фиксит" — bottleneck знания
"Мы не можем взять нового разработчика — он ничего не поймет" — bus factor = 1
"Это legacy код, лучше не трогать" — страх изменений
С тестами:
"CI показал, что Пётр сломал GPIO" — объективная диагностика
"Любой может править код — тесты подстрахуют" — коллективное владение
"Новичок за неделю написал работающий фич" — быстрое onboarding
"Рефакторим смело — 200 тестов подтвердят работу" — эволюция архитектуры
Профит для продукта
Без тестов:
"Выкатываем и молимся" — русская рулетка с релизами
"На стенде работало..." — разрыв между dev и prod
"Клиент нашел баг, который мы 2 года не видели" — позор на рынке
"Нельзя добавить фичу — все развалится" — технический долг
С тестами:
"Релиз каждый вторник" — предсказуемый процесс
"CI тестирует на реальном железе" — идентичность стенда и продакшена
"Баги клиентов воспроизводятся за 5 минут" — быстрая реакция
"Добавляем фичи без страха" — скорость разработки
Почему это может быть не нужно?
Если ваша цель — стать "незаменимым" разработчиком, единственным человеком, который понимает код и может его исправить, то автоматизация тестирования действительно вам не нужна. Тесты делают код прозрачным, понятным и доступным для всей команды.
Ваша карьерная стратегия без тестов:
"Это очень сложная система" = "Только я знаю как оно работает"
"Лучше не трогать" = "Мой job security"
"Нужен глубокий контекст" = "Я незаменимый"
"Это legacy код" = "Мой личный фамильный алмаз"
Общий pipeline
Что требуется для минимального CI?
Необходимый минимум — это хотя бы собрать проект и прошить микроконтроллер. Звучит просто, но на практике часто возникают ситуации, когда проект собирается у одного разработчика, но не собирается у другого. Это может быть связано с разными версиями компилятора, отсутствующими зависимостями или изменениями в коде, которые сломали сборку для определённых окружений.
Поиск причины таких проблем может занимать часы. CI решает эту проблему: каждый коммит автоматически собирается в чистом окружении, что гарантирует, что сборка не сломана и проект может быть воспроизведён на любой машине.
Разберём необходимые инструменты на конкретном примере.
Необходимые компоненты
GitHub или GitLab — система контроля версий с поддержкой CI/CD. В этой статье все примеры будут для GitHub. Просто создайте новый репозиторий, к нему мы ещё вернёмся.
-
Сервер для сборки и тестов — это может быть обычный компьютер, Raspberry Pi или даже виртуальная машина. GitHub предоставляет бесплатные серверы (runners), но с ограничениями по времени выполнения. Для embedded-разработки, где нужен доступ к реальному железу, обычно используется собственный сервер (self-hosted runner).
Альтернатива: VCON — сторонний сервис для удалённого доступа к устройствам. Его использует, например, проект Mongoose. Работает так: ESP32 с прошивкой VCON подключается к Wi-Fi и регистрируется на их сервере, играя роль программатора по воздуху. К ней подключается целевое устройство, и через CI можно загружать прошивки, читать логи и т.д.
Плюсы VCON:
Всё готово к использованию из коробки
Не нужно настраивать собственный сервер
Доступ к устройству из любой точки мира
Минусы VCON:
Ограничения по количеству устройств
Зависимость от внешнего сервиса
Нестандартный программатор (не подходит для полевого использования)
Программатор — я буду рассматривать J-Link, так как он предоставляет удобные инструменты для работы с RTT (Real-Time Transfer). Технически можно использовать любой программатор (ST-Link, CMSIS-DAP и др.), но J-Link даёт больше возможностей для автоматизации.
Целевое устройство или отладочный стенд — микроконтроллер или плата, на которой будут выполняться тесты.
Архитектура CI для embedded-систем
┌─────────────┐ ┌──────────────────┐ ┌────────────────┐
│ GitHub │────────>│ Self-hosted │────────>│ J-Link │
│ Repository │ │ Runner │ │ Programmer │
│ │ │ (Linux/Mac/Win) │ │ │
└─────────────┘ └──────────────────┘ └────────┬───────┘
│
│ SWD/JTAG
│
v
┌──────────────┐
│ Target MCU │
│ (STM32F103) │
└──────────────┘
Для обычного ПО достаточно облачных CI-серверов GitHub/GitLab — можно тестировать на разных операционных системах без дополнительного оборудования. Но в embedded-разработке нужен доступ к реальному железу, поэтому требуется собственный сервер с подключённым программатором и целевым устройством.
Если вам нужно только проверить, что прошивка собирается, то достаточно стандартных GitHub Actions без железа. Но для полноценного тестирования функциональности понадобится self-hosted runner.
Программное обеспечение на сервере
GitHub Actions Runner — агент, который выполняет задачи CI. Скачивается и регистрируется в вашем репозитории через настройки GitHub (Settings → Actions → Runners → New self-hosted runner). После регистрации запускается как фоновый сервис и ожидает команд от GitHub. Можно запустить несколько runners для разных устройств, маркируя их тегами.
J-Link Software — утилиты для работы с программатором J-Link. Включает в себя командные инструменты для прошивки и чтения RTT. Рекомендую именно J-Link благодаря SEGGER RTT — технологии быстрого вывода отладочной информации без задержек.
CMake (или другая система сборки) — в примере используется CMake как кроссплатформенная система метасборки. Вы можете использовать Make, Meson или другие инструменты на ваш выбор.
Python — для автоматизации прошивки и анализа результатов тестов. Библиотека
pylinkпозволяет управлять J-Link программно.ARM GCC Toolchain — компилятор для ARM микроконтроллеров (
arm-none-eabi-gcc).
Установка необходимых пакетов на Ubuntu/Debian:
# ARM toolchain
sudo apt-get install gcc-arm-none-eabi
# CMake
sudo apt-get install cmake
# Python и зависимости
sudo apt-get install python3 python3-pip
pip3 install pylink-square
Пример проекта с CI
Структура проекта runit
Рассмотрим реальный пример из библиотеки runit — фреймворка для unit-тестирования на bare-metal системах:
runit/
├── .github/
│ ├── workflows/
│ │ └── build.yml # Конфигурация CI
│ └── scripts/
│ ├── flashing.py # Скрипт прошивки
│ └── units.py # Скрипт запуска тестов
├── src/
│ ├── runit.h # Заголовочный файл библиотеки
│ └── runit.c # Реализация
├── examples/
│ └── f103re-cmake-baremetal-builtin/
│ ├── CMakeLists.txt # Конфигурация сборки
│ ├── main.c # Тесты для МК
│ ├── startup_stm32f103xe.s
│ └── STM32F103RETX_FLASH.ld
└── tst/
└── selftest.c # Тесты для Linux
GitHub Actions Workflow
Создаём CI из пяти этапов:
Клонирование репозитория на сервер
Конфигурация CMake
Сборка проекта
Прошивка микроконтроллера
Запуск тестов на устройстве
Файл .github/workflows/build.yml:
name: Build Runit Selftest
on: [pull_request]
jobs:
# Job 1: Тестирование на Linux (без железа)
linux_build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Configure and Build project
run: |
cmake -S . -B build
cmake --build build
- name: Run selftest
run: ./build/runit-selftest
# Job 2: Тестирование на STM32F103 (с реальным железом)
stm32f103re_build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 1
- name: Configure and Build project
run: |
cmake -S examples/f103re-cmake-baremetal-builtin -B examples/f103re-cmake-baremetal-builtin/build
cmake --build examples/f103re-cmake-baremetal-builtin/build
- name: Flash firmware
run: |
python3 .github/scripts/flashing.py ${{ secrets.JLINK_SERIAL_CI_STM32F103RE }} STM32F103RE examples/f103re-cmake-baremetal-builtin/build/example_f103re.bin
- name: Unit tests
run: |
python3 .github/scripts/units.py ${{ secrets.JLINK_SERIAL_CI_STM32F103RE }} STM32F103RE
Важные моменты:
runs-on: self-hosted— указывает использовать собственный runner, а не облачный сервер GitHubsecrets.JLINK_SERIAL_CI_STM32F103RE— секретная переменная с серийным номером J-Link программатора. Настраивается в Settings → Secrets → Actions вашего репозитория. Это защищает ваше устройство от несанкционированного доступа.Два независимых job выполняются параллельно: один для Linux-версии библиотеки, другой для микроконтроллера.
Детальный разбор этапов
Этап 1: Linux-сборка (проверка кроссплатформенности)
Библиотека runit кроссплатформенная — работает как на микроконтроллерах, так и на обычных ОС. Поэтому первый job просто собирает и запускает тесты на Linux:
cmake -S . -B build
cmake --build build
./build/runit-selftest
Если исполняемый файл возвращает код выхода не равный 0, CI считается проваленным. Это стандартный подход для unit-тестов в Unix-системах.
Этап 2-5: Сборка, прошивка и тестирование на STM32
Теперь перейдём к самому интересному — автоматизированной прошивке и тестированию на реальном микроконтроллере:
1. Сборка проекта
cmake -S examples/f103re-cmake-baremetal-builtin -B examples/f103re-cmake-baremetal-builtin/build
cmake --build examples/f103re-cmake-baremetal-builtin/build
На этом этапе мы гарантируем, что проект собирается без ошибок. Если сборка упала — проблема локализована, и мы знаем, что изменения сломали компиляцию.
Бонус: бинарный файл можно сохранить в артефактах GitHub Actions и использовать для прошивки партии устройств или предоставить команде для тестирования без необходимости собирать локально.
2. Прошивка микроконтроллера
Используется Python-скрипт .github/scripts/flashing.py:
import sys, os
import pylink
from pylink import JLink
def flash_device_by_usb(jlink_serial: int, fw_file: str, mcu: str) -> None:
jlink = pylink.JLink()
jlink.open(serial_no=jlink_serial)
if jlink.opened():
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
jlink.connect(mcu)
print(jlink.flash_file(fw_file, 0x08000000))
jlink.reset(halt=False)
jlink.close()
def main():
try:
jlink_serial = int(sys.argv[1].strip())
mcu = sys.argv[2].strip()
fw_file = os.path.abspath(sys.argv[3].strip())
flash_device_by_usb(jlink_serial, fw_file, mcu)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Скрипт принимает три параметра:
Серийный номер J-Link программатора
Название MCU (например,
STM32F103RE)Путь к бинарному файлу прошивки
Если прошивка не удалась, скрипт возвращает код ошибки 1, и CI прерывается.
3. Запуск тестов и чтение результатов через RTT
Самая интересная часть — как получить результаты тестов с микроконтроллера?
SEGGER RTT — технология быстрой передачи данных
SEGGER RTT (Real-Time Transfer) — это технология двусторонней передачи данных между целевым устройством и хостом через отладочный интерфейс (SWD/JTAG). Разработана компанией SEGGER.
Преимущества RTT:
Высокая скорость — до 2 МБ/сек
Нет задержек — не блокирует выполнение программы
Не требует дополнительных ножек (как UART, например, или SWO) — использует существующий отладочный интерфейс. Так что даже если нет SWO это решение будет работать
Двусторонняя связь — можно не только читать данные, но и отправлять команды
Как это работает:
На МК выделяется небольшой буфер в RAM (обычно 1-16 КБ)
Код на МК пишет данные в этот буфер (
SEGGER_RTT_printf())Программатор читает данные из буфера через SWD/JTAG
Python-скрипт на хосте получает и анализирует эти данные
Недостаток RTT: Ограниченный размер буфера. Если логов слишком много и они не успевают считываться, произойдёт перезапись, и часть данных потеряется. Решение — увеличить размер буфера или оптимизировать вывод логов.
Python-скрипт для запуска тестов
Файл .github/scripts/units.py:
import sys, re, time
import pylink
from pylink import JLink
def remove_ansi_colors(text: str) -> str:
"""Удаляет ANSI-коды цветов из текста"""
return re.sub(r"\x1b\[[0-9;]*m", "", text)
def run_tests_by_rtt(jlink: JLink, duration: float = 10.0) -> bool:
has_error = False
try:
jlink.rtt_start()
start_time = time.time()
while True:
elapsed = time.time() - start_time
if elapsed >= duration:
break
response = jlink.rtt_read(0, 1024)
if response:
text = remove_ansi_colors(bytes(response).decode("utf-8", errors="ignore"))
# Парсим результаты тестов
for line in text.splitlines():
# Ищем строки с отчётами: "REPORT | File: ... | Passes: X | Failures: Y"
match = re.search(
r'REPORT\s*\|\s*File:\s*(.*?)\s*\|\s*Test case:\s*(.*?)\s*\|\s*Passes:\s*(\d+)\s*\|\s*Failures:\s*(\d+)',
line
)
if match:
passed = match.group(3)
failed = match.group(4)
print(f"Test result: {passed} passed, {failed} failed")
if failed != '0':
has_error = True
elif "All tests passed successfully!" in line:
has_error = False
print("All tests passed successfully!")
elif line.strip():
print(line)
finally:
jlink.rtt_stop()
return has_error
def main():
jlink_serial = int(sys.argv[1].strip())
mcu = sys.argv[2].strip()
jlink = pylink.JLink()
jlink.open(serial_no=jlink_serial)
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
jlink.connect(mcu)
has_error = run_tests_by_rtt(jlink, 10.0)
jlink.close()
if has_error:
sys.exit(1)
if __name__ == "__main__":
main()
Как это работает:
Скрипт подключается к J-Link программатору
Запускает RTT-соединение
Микроконтроллер после сброса начинает выполнять тесты и выводить результаты через
SEGGER_RTT_printf()Скрипт читает вывод в реальном времени (10 секунд)
Парсит результаты по шаблону и определяет, прошли ли тесты
Возвращает код ошибки, если есть проваленные тесты
Пример кода с тестами на МК
Файл examples/f103re-cmake-baremetal-builtin/main.c содержит самотестирование библиотеки runit:
#include <stm32f103xe.h>
#include "runit.h"
static size_t expected_failures_counter = 0;
#define SHOULD_FAIL(failing) \
printf("Expected failure: "); \
expected_failures_counter++; \
failing
static void test_eq(void)
{
runit_eq(12, 12);
runit_eq(12.0f, 12U);
SHOULD_FAIL(runit_eq(100, 1)); // Этот тест должен упасть
}
static void test_gt(void)
{
runit_gt(100, 1);
SHOULD_FAIL(runit_gt(1, 100)); // Этот тест должен упасть
}
static void test_fapprox(void)
{
runit_fapprox(1.0f, 1.0f);
runit_fapprox(1.0f, 1.000001f);
SHOULD_FAIL(runit_fapprox(1.0f, 1.1f)); // Этот тест должен упасть
}
int main(void)
{
test_eq();
test_gt();
test_fapprox();
runit_report(); // Выводит итоговый отчёт
if (expected_failures_counter != runit_counter_assert_failures)
printf("Expected %u failures, but got %u\n",
expected_failures_counter, runit_counter_assert_failures);
else
printf("All tests passed successfully!\n");
for (;;) {} // Бесконечный цикл
return 0;
}
Важно: Для вывода через RTT функция _write переопределена для использования SEGGER_RTT_PutChar(). Это позволяет использовать стандартный printf() в коде тестов, и весь вывод автоматически направляется в RTT-буфер.
Пример переопределения _write в файле syscalls.c:
#include "SEGGER_RTT.h"
__attribute__((weak)) int _write(int file, char* ptr, int len)
{
for (int i = 0; i < len; i++)
{
SEGGER_RTT_PutChar(0, ptr[i]);
}
return len;
}
Атрибут weak позволяет при необходимости переопределить эту функцию в другом месте проекта.
Функция runit_report() выводит одну строку с итоговой статистикой выполненных тестов. Можно вызывать runit_report() несколько раз в разных местах программы — каждый вызов выведет отдельный отчёт с накопленной статистикой. Для сброса счётчиков между группами тестов нужно обнулить внутренние переменные библиотеки.
Вывод в RTT выглядит так:
REPORT | File: main.c:42 | Test case: main | Passes: 5 | Failures: 3
All tests passed successfully!
Python-скрипт парсит этот вывод и определяет результат.
Расширенные возможности тестирования
Стратегии организации тестов
Ваш проект может иметь несколько целей сборки:
Продуктовая сборка — финальная прошивка для production без отладочного кода
Тестовая сборка — специальная версия с unit-тестами библиотек и модулей
Отладочная сборка — рабочая прошивка с флагом
DEBUG, где модуль самотестирования включается условной компиляцией
Выбор подхода зависит от ваших потребностей и возможностей:
Вариант 1: Отдельный тестовый проект
# CMakeLists.txt для тестов
add_executable(firmware_tests
tests/test_main.c
tests/test_uart.c
tests/test_modbus.c
src/uart.c
src/modbus.c
)
Вариант 2: Условная компиляция тестов
#ifdef DEBUG_TESTS
static void run_all_tests(void) {
test_uart();
test_modbus();
test_eeprom();
runit_report();
}
#endif
int main(void) {
system_init();
#ifdef DEBUG_TESTS
// Тесты запускаются по команде через RTT
if (check_rtt_command("run_tests")) {
run_all_tests();
}
#endif
// Основной код прошивки
while(1) {
main_loop();
}
}
Лично я использую подход с флагом сборки и добавил возможность вызова тестов через команды по RTT. Это позволяет:
Не пересобирать прошивку для запуска тестов
Запускать тесты в любой момент на работающем устройстве
Тестировать конкретные модули по требованию
Тестирование протоколов и интерфейсов
Python-скрипт может взаимодействовать не только с микроконтроллером через RTT, но и тестировать боевую прошивку через реальные интерфейсы:
Пример: тестирование Modbus RTU
Устройство должно общаться по Modbus RTU. Подключаем его к серверу CI по соответствующему интерфейсу и запускаем Python-тесты:
import serial
from pymodbus.client import ModbusSerialClient
def test_modbus_valid_requests():
"""Проверка корректных запросов"""
client = ModbusSerialClient(port='/dev/ttyUSB0', baudrate=9600)
# Чтение регистров
result = client.read_holding_registers(address=0, count=10, slave=1)
assert not result.isError(), "Должны корректно читаться регистры"
assert len(result.registers) == 10
# Запись регистра
result = client.write_register(address=0, value=100, slave=1)
assert not result.isError(), "Должна быть возможность записи"
# Проверка записи
result = client.read_holding_registers(address=0, count=1, slave=1)
assert result.registers[0] == 100, "Значение должно сохраниться"
def test_modbus_invalid_requests():
"""Проверка обработки некорректных запросов"""
client = ModbusSerialClient(port='/dev/ttyUSB0', baudrate=9600)
# Несуществующий адрес
result = client.read_holding_registers(address=9999, count=1, slave=1)
assert result.isError(), "Должна вернуться ошибка для несуществующего адреса"
# Битые данные (неверный CRC)
# Устройство должно игнорировать такие пакеты
with serial.Serial('/dev/ttyUSB0', 9600) as ser:
ser.write(b'\x01\x03\x00\x00\x00\x0A\xFF\xFF') # Неверный CRC
time.sleep(0.5)
response = ser.read_all()
assert len(response) == 0, "Битые пакеты должны игнорироваться"
if __name__ == "__main__":
test_modbus_valid_requests()
test_modbus_invalid_requests()
print("All Modbus tests passed!")
Такие тесты проверяют:
Корректную обработку валидных данных
Правильную валидацию входных данных
Предсказуемое поведение при некорректных запросах
Соответствие спецификации протокола
Аналогично можно тестировать:
CAN-интерфейс — отправка/приём сообщений, обработка ошибок шины
Ethernet/TCP — установка соединений, обработка разрывов связи
I2C/SPI — взаимодействие с периферией
GPIO — проверка уровней сигналов, тайминги
Измерение производительности — время отклика, пропускная способность
Главный аргумент для внедрения CI
Для тех, кто не безразличен, но ленив:
Больше не нужно:
Уговаривать себя перетестировать всё после каждого изменения
Беспокоиться, что что-то сломалось, если вы не перетестировали
Помнить, какие модули зависят от изменённого кода
Тратить время на ручное тестирование одних и тех же сценариев
CI делает это за вас:
Перетестирует все сценарии автоматически
Точно укажет, где именно проблема
Запустится на каждом Pull Request
Добавили новый тест? Он будет выполняться каждый раз навсегда
Реальная экономия времени:
Внесли изменения в библиотеку работы с UART? CI автоматически прогонит:
Unit-тесты самой библиотеки
Интеграционные тесты с Modbus (который использует UART)
Тесты протокола связи
Проверку на утечки памяти
Валидацию тайминга
Всё это — без вашего участия, за минуты, с точным указанием проблемного места.
Пример из реального проекта:
В устройстве BMPLC автоматически тестируется библиотека работы с EEPROM (в локальной разработке тесты также можно запускать вручную через команды в RTT-интерфейсе). Набор тестов проверяет критические сценарии работы с памятью:
void run_eeprom_tests(void) {
eeprom_partial_page_write_test(); // Корректность записи неполной страницы
eeprom_size_limit_test(); // Защита от выхода за пределы памяти
eeprom_multi_page_write_test(); // Многостраничная запись (несколько страниц за раз)
eeprom_random_access_test(); // Произвольный доступ к разным адресам
runit_report();
}
Эти тесты выявляют типичные проблемы:
Выход за пределы адресного пространства (32 КБ для AT24C256)
Ошибки при записи данных, больших размера страницы
Банальные проверки корректности записи и чтения данных
Важный момент: От постоянных прогонов CI тестовое устройство может израсходовать ресурс EEPROM (обычно 100 000 - 1 000 000 циклов записи). Аналогично Flash-память микроконтроллера деградирует от частых прошивок. Но это малая цена за уверенность в качестве кода — стоимость замены одного тестового устройства несопоставима с ценой ошибки в production.
Если тесты внезапно начинают падать:
Запускаем старую, проверенную версию прошивки → тесты проходят → память работает, проблема в новом коде
Запускаем старую версию → тесты не проходят → тестовое устройство выработало ресурс, заменяем его
Без автоматических тестов такая ошибка в библиотеке могла бы попасть в продакшн и привести к повреждению данных на всех устройствах с необходимостью массовой перепрошивки.
Пошаговая инструкция по внедрению
Шаг 1: Подготовка репозитория
Создайте репозиторий на GitHub
Добавьте
.github/workflows/build.ymlс конфигурацией CIСоздайте папку
.github/scripts/для Python-скриптов
Шаг 2: Настройка сервера
-
Установите необходимые зависимости:
sudo apt-get install gcc-arm-none-eabi cmake python3 python3-pip pip3 install pylink-square Скачайте J-Link Software с сайта SEGGER
-
Зарегистрируйте GitHub Actions Runner:
Откройте Settings → Actions → Runners → New self-hosted runner
Следуйте инструкциям для вашей ОС
Запустите runner как сервис
Шаг 3: Подключение оборудования
Подключите J-Link к серверу по USB
Подключите целевое устройство к J-Link по SWD/JTAG
Проверьте подключение:
JLinkExe→connect→ укажите MCUУзнайте серийный номер:
$ lsusb -vили через утилитуJLinkExe
Шаг 4: Настройка секретов
В репозитории: Settings → Secrets and variables → Actions → New repository secret
Добавьте
JLINK_SERIAL_CIс серийным номером программатора
Шаг 5: Добавление тестов
-
Интегрируйте
runit(или другой фреймворк для тестов) в ваш проект:git submodule add https://github.com/RoboticsHardwareSolutions/runit.git libs/runit Добавьте SEGGER RTT в проект (через CMake FetchContent или вручную)
-
Напишите тесты в стиле
runit:void test_my_function(void) { runit_eq(my_function(5), 25); runit_gt(my_function(10), 90); } В
main()вызовите тесты иrunit_report()
Шаг 6: Первый запуск
Создайте Pull Request
GitHub Actions автоматически запустит CI
Проверьте логи выполнения
При необходимости отладьте прогоните тесты локально теми же скриптами
Заключение
Автоматизация CI/CD для embedded-систем требует начальных усилий, но окупается многократно:
Ускорение разработки за счёт быстрой обратной связи
Защита от регрессий и повторных багов
Объективные метрики качества кода
Упрощение командной работы и онбординга
Уверенность в каждом релизе
Библиотека runit и описанный подход — это лишь отправная точка. Вы можете расширить систему тестирования под ваши нужды: добавить coverage-анализ, интеграцию с тестовыми стендами, автоматическое создание релизов и многое другое.
Начните с малого — автоматизируйте сборку и базовые тесты. Постепенно добавляйте новые проверки. И помните: каждый автоматический тест — это инвестиция в стабильность и скорость вашей разработки.
Полезные ссылки
runit на GitHub — фреймворк для unit-тестов
SEGGER RTT — документация по RTT
pylink-square — Python-библиотека для J-Link
GitHub Actions — документация по CI/CD
almaz1c
Повальное явление. Сколько повидал проектов без единой строчки комментария. Пустые readme. Коммиты с комментариями в стиле "error fix". Исправления багов без тестов.
Делают все, чтобы увеличить свою незаменимость. Эгоизм и нулевая эмпатия.