CI/CD для Bare-Metal Embedded-разработки — АСУ ТП: сообщество инженеров

CI/CD для Bare-Metal Embedded-разработки

Практическое руководство по автоматизации сборки, прошивки и тестирования микроконтроллеров

Table of Contents

Зачем это нужно?

Многие 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 решает эту проблему: каждый коммит автоматически собирается в чистом окружении, что гарантирует, что сборка не сломана и проект может быть воспроизведён на любой машине.

Разберём необходимые инструменты на конкретном примере.

Необходимые компоненты

  1. GitHub или GitLab — система контроля версий с поддержкой CI/CD. В этой статье все примеры будут для GitHub. Просто создайте новый репозиторий, к нему мы ещё вернёмся.
  2. Сервер для сборки и тестов — это может быть обычный компьютер, Raspberry Pi или даже виртуальная машина. GitHub предоставляет бесплатные серверы (runners), но с ограничениями по времени выполнения. Для embedded-разработки, где нужен доступ к реальному железу, обычно используется собственный сервер (self-hosted runner).Альтернатива: VCON — сторонний сервис для удалённого доступа к устройствам. Его использует, например, проект Mongoose. Работает так: ESP32 с прошивкой VCON подключается к Wi-Fi и регистрируется на их сервере, играя роль программатора по воздуху. К ней подключается целевое устройство, и через CI можно загружать прошивки, читать логи и т.д.Плюсы VCON:Минусы VCON:
    1. Всё готово к использованию из коробки
    2. Не нужно настраивать собственный сервер
    3. Доступ к устройству из любой точки мира
      1. Ограничения по количеству устройств
      2. Зависимость от внешнего сервиса
      3. Нестандартный программатор (не подходит для полевого использования)
  3. Программатор — я буду рассматривать J-Link, так как он предоставляет удобные инструменты для работы с RTT (Real-Time Transfer). Технически можно использовать любой программатор (ST-Link, CMSIS-DAP и др.), но J-Link даёт больше возможностей для автоматизации.
  4. Целевое устройство или отладочный стенд — микроконтроллер или плата, на которой будут выполняться тесты.

Архитектура 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.

Программное обеспечение на сервере

  1. GitHub Actions Runner — агент, который выполняет задачи CI. Скачивается и регистрируется в вашем репозитории через настройки GitHub (Settings → Actions → Runners → New self-hosted runner). После регистрации запускается как фоновый сервис и ожидает команд от GitHub. Можно запустить несколько runners для разных устройств, маркируя их тегами.
  2. J-Link Software — утилиты для работы с программатором J-Link. Включает в себя командные инструменты для прошивки и чтения RTT. Рекомендую именно J-Link благодаря SEGGER RTT — технологии быстрого вывода отладочной информации без задержек.
  3. CMake (или другая система сборки) — в примере используется CMake как кроссплатформенная система метасборки. Вы можете использовать Make, Meson или другие инструменты на ваш выбор.
  4. Python — для автоматизации прошивки и анализа результатов тестов. Библиотека pylink позволяет управлять J-Link программно.
  5. 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 из пяти этапов:

  1. Клонирование репозитория на сервер
  2. Конфигурация CMake
  3. Сборка проекта
  4. Прошивка микроконтроллера
  5. Запуск тестов на устройстве

Файл .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, а не облачный сервер GitHub
  • secrets.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 и использовать для прошивки партии устройств или предоставить команде для тестирования без необходимости собирать локально.

  1. Прошивка микроконтроллера

Используется 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 прерывается.

  1. Запуск тестов и чтение результатов через RTT

Самая интересная часть — как получить результаты тестов с микроконтроллера?

SEGGER RTT — технология быстрой передачи данных

SEGGER RTT (Real-Time Transfer) — это технология двусторонней передачи данных между целевым устройством и хостом через отладочный интерфейс (SWD/JTAG). Разработана компанией SEGGER.

Преимущества RTT:

  • Высокая скорость — до 2 МБ/сек
  • Нет задержек — не блокирует выполнение программы
  • Не требует дополнительных ножек (как UART, например, или SWO) — использует существующий отладочный интерфейс. Так что даже если нет SWO это решение будет работать
  • Двусторонняя связь — можно не только читать данные, но и отправлять команды

Как это работает:

  1. На МК выделяется небольшой буфер в RAM (обычно 1-16 КБ)
  2. Код на МК пишет данные в этот буфер (SEGGER_RTT_printf())
  3. Программатор читает данные из буфера через SWD/JTAG
  4. 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'REPORTs*|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()

Как это работает:

  1. Скрипт подключается к J-Link программатору
  2. Запускает RTT-соединение
  3. Микроконтроллер после сброса начинает выполнять тесты и выводить результаты через SEGGER_RTT_printf()
  4. Скрипт читает вывод в реальном времени (10 секунд)
  5. Парсит результаты по шаблону и определяет, прошли ли тесты
  6. Возвращает код ошибки, если есть проваленные тесты

Пример кода с тестами на МК

Файл 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 %un", 
               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-скрипт парсит этот вывод и определяет результат.

Расширенные возможности тестирования

Стратегии организации тестов

Ваш проект может иметь несколько целей сборки:

  1. Продуктовая сборка — финальная прошивка для production без отладочного кода
  2. Тестовая сборка — специальная версия с unit-тестами библиотек и модулей
  3. Отладочная сборка — рабочая прошивка с флагом 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'x01x03x00x00x00x0AxFFxFF') # Неверный 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: Подготовка репозитория

  1. Создайте репозиторий на GitHub
  2. Добавьте .github/workflows/build.yml с конфигурацией CI
  3. Создайте папку .github/scripts/ для Python-скриптов

Шаг 2: Настройка сервера

  1. Установите необходимые зависимости:sudo apt-get install gcc-arm-none-eabi cmake python3 python3-pip
    pip3 install pylink-square
  2. Скачайте J-Link Software с сайта SEGGER
  3. Зарегистрируйте GitHub Actions Runner:
    1. Откройте Settings → Actions → Runners → New self-hosted runner
    2. Следуйте инструкциям для вашей ОС
    3. Запустите runner как сервис

Шаг 3: Подключение оборудования

  1. Подключите J-Link к серверу по USB
  2. Подключите целевое устройство к J-Link по SWD/JTAG
  3. Проверьте подключение: JLinkExeconnect → укажите MCU
  4. Узнайте серийный номер: $ lsusb -v или через утилиту JLinkExe

Шаг 4: Настройка секретов

  1. В репозитории: Settings → Secrets and variables → Actions → New repository secret
  2. Добавьте JLINK_SERIAL_CI с серийным номером программатора

Шаг 5: Добавление тестов

  1. Интегрируйте runit (или другой фреймворк для тестов) в ваш проект:git submodule add https://github.com/RoboticsHardwareSolutions/runit.git libs/runit
  2. Добавьте SEGGER RTT в проект (через CMake FetchContent или вручную)
  3. Напишите тесты в стиле runit:void test_my_function(void) { runit_eq(my_function(5), 25); runit_gt(my_function(10), 90); }
  4. В main() вызовите тесты и runit_report()

Шаг 6: Первый запуск

  1. Создайте Pull Request
  2. GitHub Actions автоматически запустит CI
  3. Проверьте логи выполнения
  4. При необходимости отладьте прогоните тесты локально теми же скриптами

Заключение

Автоматизация CI/CD для embedded-систем требует начальных усилий, но окупается многократно:

  • Ускорение разработки за счёт быстрой обратной связи
  • Защита от регрессий и повторных багов
  • Объективные метрики качества кода
  • Упрощение командной работы и онбординга
  • Уверенность в каждом релизе

Библиотека runit и описанный подход — это лишь отправная точка. Вы можете расширить систему тестирования под ваши нужды: добавить coverage-анализ, интеграцию с тестовыми стендами, автоматическое создание релизов и многое другое.

Начните с малого — автоматизируйте сборку и базовые тесты. Постепенно добавляйте новые проверки. И помните: каждый автоматический тест — это инвестиция в стабильность и скорость вашей разработки.

Полезные ссылки

Источник: https://habr.com/ru/articles/963298

🔗 CI/CD для Bare-Metal Embedded-разработки

Оставьте комментарий

Войти

Зарегистрироваться

Сбросить пароль

Пожалуйста, введите ваше имя пользователя или эл. адрес, вы получите письмо со ссылкой для сброса пароля.

Прокрутить вверх