- PVSM.RU - https://www.pvsm.ru -
Продолжение серии статей о BareMetal CI. В первой части [1] мы рассмотрели базовый подход к автоматизации тестирования микроконтроллеров с использованием J-Link и RTT. Эта статья посвящена масштабируемому решению на базе Docker, которое поддерживает различные типы оборудования и CI-платформы.
Разработка встраиваемых систем и микроконтроллеров традиционно сталкивается с проблемой тестирования реального железа в процессе непрерывной интеграции. Облачные CI-системы, такие как GitHub Actions или GitLab CI, прекрасно справляются с тестированием веб-приложений и серверного ПО, но не имеют доступа к физическим устройствам — программаторам, отладчикам и периферии.
BareMetal CI — это подход к организации CI/CD для встраиваемых систем, при котором тесты выполняются на реальном железе с использованием self-hosted раннеров. Это решение позволяет автоматически прошивать микроконтроллеры, выполнять отладку и тестировать работу с реальной периферией (CAN-шины, USB-устройства и др.) прямо в процессе CI/CD пайплайна.
В отличие от традиционного подхода, где CI-раннер устанавливается на хост-систему и запускает джобы в отдельных контейнерах (Docker executor), в данном решении сам раннер работает внутри контейнера с executor=shell.
Важное уточнение о целевой аудитории:
Этот подход не является best practice для production CI-инфраструктуры. Более правильным решением было бы:
Установить раннер на хост-системе
Настроить Docker executor с монтированием необходимых устройств (GitLab [2], GitHub [3])
Использовать разные образы для разных проектов/задач
Управлять образами централизованно (например, через GHCR)
Почему тогда раннер в контейнере?
Это решение оптимизировано для быстрого развёртывания тестового стенда, когда:
Нет опыта с Ansible/конфигурацией раннеров — новичок может потратить день на настройку runner.toml, разбираться с правами доступа к устройствам, путями к сокету Docker и т.д.
Разовое развёртывание — нужно быстро поднять стенд на Raspberry Pi или другом одноплатнике для тестирования конкретного устройства. Не планируется использование для множества проектов.
Изоляция экспериментов — можно быстро удалить контейнер и пересоздать, не затрагивая хост-систему. Для экспериментов с разными конфигурациями это удобнее, чем править конфиги раннера.
Документированное окружение — весь стек описан в Dockerfile и docker-compose.yml. Для человека, впервые настраивающего CI для embedded, это понятнее, чем конфиг раннера + отдельные скрипты установки.
Сценарий использования:
# Поставили чистую систему на Raspberry Pi
git clone <repo>
cp .env.example .env
# Отредактировали .env (2 переменных: RUNNER_PLATFORM и токен)
docker-compose up -d
# Готово — можно запускать тесты
Вместо:
Установка и настройка GitLab Runner на хосте
Разбор документации по Docker executor
Настройка монтирования устройств в runner.toml
Разбор прав доступа к /dev и Docker socket
Создание и регистрация образов с инструментами
Trade-offs (что теряем):
Гибкость — нельзя использовать разные образы для разных проектов
Изоляция — все джобы одного раннера делят окружение
Best practices — privileged контейнер с раннером внутри — это архитектурный антипаттерн
Когда НЕ стоит использовать этот подход:
❌ У вас уже есть настроенная CI-инфраструктура
❌ Нужно тестировать множество разных проектов на одном стенде
❌ Требуется строгая изоляция между джобами
❌ Есть специалист по DevOps в команде
Когда этот подход имеет смысл:
✅ Первый опыт с CI для embedded
✅ Нужно быстро поднять тестовый стенд
✅ Один раннер = одно устройство/проект
✅ Хочется "просто запустить и чтобы работало"
Этот подход — точка входа для тех, кто только начинает автоматизировать тестирование железа. После получения опыта логично мигрировать на правильную архитектуру с раннером на хосте и Docker executor.
Решение построено на базе Docker-контейнера, который объединяет в себе:
CI Runner — self-hosted раннер для GitHub Actions или GitLab CI
Отладчики и программаторы — поддержка различных debug probe (Segger J-Link, Black Magic Probe и др.)
Интерфейсные адаптеры — работа с периферийными шинами (CAN через PEAK CAN/SocketCAN, USB, UART и др.)
┌─────────────────────────────────────────────────────────┐
│ GitHub / GitLab │
│ (Cloud CI/CD) │
└────────────────────┬────────────────────────────────────┘
│ Webhooks / Job Assignment
▼
┌─────────────────────────────────────────────────────────┐
│ Docker Container (Host Linux) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ GitHub/GitLab Self-Hosted Runner │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Debug Probes & Programmers │ │
│ │ (J-Link, Black Magic Probe, OpenOCD, etc) │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Interface Adapters & Protocols │ │
│ │ (SocketCAN, USB, UART, SPI, I2C, etc) │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────┬──────────────────────────────────────────┘
│ /dev mount (privileged mode)
│ network_mode: host
│
▼
┌─────────────────────────────────────┐
│ Physical Hardware │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Debug Probes │ │ Interface │ │
│ │ (J-Link,BMP) │ │ Adapters │ │
│ └──────┬───────┘ │ (CAN, USB..) │ │
│ │ └──────┬───────┘ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Target MCU │ │ Peripheral │ │
│ │ (ARM/RISC-V)│ │ Devices │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────┘
Контейнер поддерживает как GitHub Actions, так и GitLab CI через единую конфигурацию. Выбор платформы осуществляется через переменную окружения RUNNER_PLATFORM:
# В файле .env
RUNNER_PLATFORM=github # или gitlab
Переключение между платформами происходит автоматически при старте контейнера. Единая точка входа (entrypoint.sh) определяет, какой раннер запускать:
if [ "${RUNNER_PLATFORM}" = "gitlab" ]; then
source /home/runner/scripts/gitlab-runner.sh
run_gitlab_runner
else
source /home/runner/scripts/github-runner.sh
run_github_runner
fi
Контейнер поддерживает различные типы debug probe для прошивки и отладки микроконтроллеров:
Отладчик от Segger с поддержкой широкого спектра MCU.
Опенсорсный debug probe с нативной поддержкой GDB:
Работает как USB CDC устройство
Не требует дополнительных драйверов
Поддержка популярных программаторов через OpenOCD:
ST-Link
CMSIS-DAP совместимые устройства
FT2232-based адаптеры
Установка компонентов выполняется на этапе сборки образа и может быть гибко настроена через переменные окружения. Например, для J-Link:
RUN if [ "${ENABLE_JLINK}" = "true" ]; then
wget --post-data "accept_license_agreement=accepted"
https://www.segger.com/downloads/jlink/JLink_Linux_${JLINK_VERSION}_x86_64.deb
-O JLink.deb &&
dpkg --force-depends -i JLink.deb;
fi
Многие встраиваемые системы используют CAN-шину для коммуникации. Контейнер поддерживает различные CAN-адаптеры через SocketCAN — стандартную подсистему Linux для работы с CAN.
Важно: Для корректной работы SocketCAN контейнер использует
network_mode: host, что обеспечивает прямой доступ к сетевым интерфейсам хост-системы, включая CAN-интерфейсы.
На данный момент реализована поддержка PEAK CAN адаптеров:
setup_socketcan() {
local BAUDRATE=${PCAN_BAUDRATE:-125000}
# Загрузка модулей ядра
sudo modprobe can
sudo modprobe can_raw
sudo modprobe peak_usb
# Настройка интерфейса
CAN_INTERFACE=$(ip link show | grep -o "can[0-9]*" | head -n 1)
sudo ip link set ${CAN_INTERFACE} type can bitrate ${BAUDRATE}
sudo ip link set ${CAN_INTERFACE} up
}
После настройки CAN-интерфейс доступен для тестирования:
# Отправка CAN-сообщения
cansend can0 123#DEADBEEF
# Мониторинг CAN-шины
candump can0
Архитектура контейнера позволяет легко добавлять поддержку других интерфейсов:
USB — прямое взаимодействие с USB-устройствами
UART/Serial — тестирование последовательных интерфейсов
SPI/I2C — работа с периферией через адаптеры (например, FT232H)
Ethernet — все сетевые устройства уже доступны благодаря режиму network_mode: host
Важно: Технически уже реализован доступ к различным типам оборудования:
Локальные устройства через монтирование /dev — любое оборудование, драйвер которого есть в пакетах ОС (см. раздел ниже)
Сетевые устройства через network_mode: host — все устройства, подключенные к сети хоста, напрямую доступны из контейнера
В случае с PEAK CAN потребовалась дополнительная настройка с загрузкой модуля ядра peak_usb и конфигурацией SocketCAN-интерфейса, что усложнило процесс. Для большинства других устройств (UART, USB-адаптеры, сетевое оборудование) достаточно прямого доступа без дополнительной настройки.
Для работы с USB-устройствами (J-Link, PEAK CAN) контейнер запускается в привилегированном режиме с монтированием /dev:
services:
ci-runner:
privileged: true
volumes:
- /dev:/dev:rw
Это обеспечивает:
Динамическое обнаружение подключенных USB-устройств
Доступ к USB без предварительной настройки
Поддержку горячего подключения устройств
При первом запуске контейнер автоматически регистрируется в GitHub/GitLab:
Для GitHub Actions:
./config.sh
--url "${RUNNER_URL}"
--token "${REGISTRATION_TOKEN}"
--name "${RUNNER_NAME}"
--labels "${RUNNER_LABELS}"
--unattended
--replace
Для GitLab CI:
gitlab-runner register
--non-interactive
--url "${GITLAB_URL}"
--token "${GITLAB_REGISTRATION_TOKEN}"
--executor "${RUNNER_EXECUTOR}"
Рабочие директории раннеров сохраняются между перезапусками контейнера через Docker volumes:
volumes:
runner-work: # GitHub Actions workspace
runner-builds: # GitLab Runner builds
gitlab-runner-config: # GitLab конфигурация
GitHub Actions:
name: Build and Flash Firmware
on: [push, pull_request]
jobs:
build-and-flash:
runs-on: [self-hosted, baremetal, jlink]
steps:
- uses: actions/checkout@v4
- name: Build firmware
run: |
make clean
make all
- name: Flash to MCU via J-Link
run: |
JLinkExe -device STM32F407VG -if SWD -speed 4000
-CommandFile flash.jlink
- name: Run hardware tests
run: |
./tests/hardware_test.sh
GitLab CI:
stages:
- build
- flash
- test
build_firmware:
stage: build
tags:
- baremetal
- jlink
script:
- make clean
- make all
artifacts:
paths:
- build/firmware.bin
flash_mcu:
stage: flash
tags:
- baremetal
- jlink
script:
- JLinkExe -device STM32F407VG -if SWD -speed 4000
-CommandFile flash.jlink
dependencies:
- build_firmware
hardware_test:
stage: test
tags:
- baremetal
- socketcan
script:
- ./tests/can_bus_test.sh
#!/bin/bash
# can_bus_test.sh
# Проверка доступности CAN-интерфейса
if ! ip link show can0 &>/dev/null; then
echo "CAN interface not available"
exit 1
fi
# Отправка тестового сообщения
cansend can0 123#1122334455667788
# Ожидание ответа
timeout 5 candump can0 | grep "456#" || {
echo "No response from CAN device"
exit 1
}
echo "CAN communication test passed"
Автоматизация всего цикла — от сборки до прошивки реального железа
Раннее обнаружение проблем — тесты на реальном оборудовании при каждом коммите
Воспроизводимость — изолированное окружение в Docker
Масштабируемость — легко добавить несколько раннеров для параллельного тестирования
Гибкость — поддержка разных CI-платформ и конфигураций
Контейнер запускается от непривилегированного пользователя runner с ограниченным набором sudo-прав:
echo "runner ALL=(ALL) NOPASSWD: /usr/sbin/ip" >> /etc/sudoers.d/runner
echo "runner ALL=(ALL) NOPASSWD: /usr/sbin/modprobe" >> /etc/sudoers.d/runner
Это минимизирует риски при выполнении недоверенного кода в CI-пайплайнах.
Privileged режим и доступ к /dev представляют серьёзные риски безопасности. Данное решение предназначено только для доверенного кода в контролируемом окружении.
Критические ограничения:
❌ Не подходит для Pull Requests от внешних contributors
❌ Не должен быть доступен для непроверенного кода
❌ Не предназначен для публично доступных раннеров
Рекомендуемые меры защиты:
✅ Protected Runners (только для main/protected branches)
✅ Manual Approval для hardware-джоб
✅ CODEOWNERS для ревью изменений в CI конфигурации
✅ Ограничение раннера специальными тегами (tags: [baremetal, trusted-only])
✅ Ведение audit logs запусков на hardware
Для production окружения рассматриваются дополнительные меры:
Изоляция сети (firewall, отдельная VLAN)
Монтирование только необходимых устройств вместо всего /dev
Использование capabilities вместо полного privileged режима (где возможно)
Отдельный раннер для untrusted кода без доступа к оборудованию
Pre-built образы на GHCR — готовые образы с полной конфигурацией для GitHub и GitLab платформ, публикуемые на GitHub Container Registry. Это позволит быстро начать работу без необходимости локальной сборки образа:
ghcr.io/baremetaltestlab/baremetal-ci-docker:github-latest — образ с GitHub Actions runner
ghcr.io/baremetaltestlab/baremetal-ci-docker:gitlab-latest — образ с GitLab Runner
Black Magic Probe — популярный опенсорсный debug probe
OpenOCD — универсальная поддержка различных программаторов (ST-Link, CMSIS-DAP и др.)
Дополнительные интерфейсные адаптеры — расширение возможностей тестирования периферии
Web-интерфейс мониторинга — визуализация состояния оборудования
Описанный подход позволяет применять практики непрерывной интеграции к разработке встраиваемых систем, где традиционные облачные CI-системы не имеют доступа к физическому оборудованию. Автоматизация тестирования на реальных устройствах сокращает время обнаружения проблем и упрощает поддержку проектов в долгосрочной перспективе.
Примечание автора: Буду рад конструктивной критике, предложениям по улучшению архитектуры и обсуждению Docker best practices.
Ссылки:
GitHub репозиторий: BareMetalTestLab/baremetal-ci-docker [4]
Автор: PaulFirs
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/stm32/438653
Ссылки в тексте:
[1] первой части: https://habr.com/ru/articles/963298/
[2] GitLab: https://docs.gitlab.com/runner/configuration/advanced-configuration/#the-runnersdocker-section
[3] GitHub: https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container#setting-container-resource-options
[4] BareMetalTestLab/baremetal-ci-docker: https://github.com/BareMetalTestLab/baremetal-ci-docker
[5] Источник: https://habr.com/ru/articles/975362/?utm_campaign=975362&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.