Практическое руководство по автоматизации сборки, прошивки и тестирования микроконтроллеров
Зачем это нужно?
Многие 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
