В статье приведен мой опыт знакомства с Zephyr RTOS на примере создания беспроводной клавиатуры с тремя кнопками (Enter, Copy, Paste). Затронуты ключевые аспекты: настройка BLE-стека и HID-сервиса, управление энергопотреблением (STOP2, параметры соединения, программный сон), работа с Devicetree (overlay и правки dtsi), отладка через RTT, а также типичные проблемы (прошивка CPU2, EXTI-конфликты, требования HID к шифрованию). Проект полностью воспроизводим и содержит ссылки на полный исходный код и готовую прошивку.

Введение

Zephyr RTOS выглядит крайне привлекательной для домашних поделок взамен того же Arduino, и по данной ОС много теоретических и обзорных материалов/курсов (например, платный пятидневный курс за 3500$ от Linux Foundation или бесплатный - от Nordic). Но практических примеров немного и из-за этого трудно сложить однозначное представление о ней, особенно с учетом быстрого развития. Лично у меня от неё большие ожидания.

Цель - ознакомиться с процессом разработки на Zephyr RTOS и понять её применимость для изделий с аккумуляторным питанием.

Подход - сделать памятный сувенир.

Подготовка инструментов

В качестве аппаратной платформы выбрана плата от WeAct на STM32WB55 (двухъядерный чип с интегрированным BLE-радио). Плата поддерживается Zephyr и подходит для широкого круга беспроводных DIY-задач. Плата представляет собою современное и интересное продолжение широко полюбившейся платы blue pill (на stm32f103), поэтому возможность поэкспериментировать с ней для меня была самоценной и тут речи нет про техническую оптимальность выбора.

Изначально я попробовал поставить средства для сборки прошивок на Zephyr RTOS на свой ПК и некоторые компоненты оказались несовместимы с моей старенькой macOS Monterey, поэтому я переключился на сборку в Docker.

Toolchain в docker'e

Скачать образ со всем необходимым можно по ссылке:

docker pull zephyrprojectrtos/zephyr‑build:latest

Далее следует перейти в папку, где хотите располагать свои проекты, и там выполнить:

docker run ‑rm ‑it ‑v $(pwd):/workdir zephyrprojectrtos/zephyr‑build:latest bash

Далее развернуть репозиторий Zephyr:

west init ‑m https://github.com/zephyrproject‑rtos/zephyr ‑mr main

После этого можно будет собирать свой проект на выбранной плате с помощью команды

west build ‑b weact_stm32wb55_core ‑p auto

Для отладки проекта использовался VS Code с расширением Cortex-Debug и отладчик J-Link. Так как сборка выполнялась в Docker-контейнере, а отладка - на хост-машине напрямую через SWD, то потребовалась корректная подстановки путей к исходным файлам, поскольку внутри контейнера рабочая директория отличается от локальной.

Подстановка путей (substitutePath)

Из-за того, что пути к исходным файлам внутри контейнера (/workdir/...) отличаются от локальных путей на хост-машине, GDB не может найти исходники для пошаговой отладки, если не указать соответствие.

В добавленном к данной статье репозитории приведён конфигурационный файл .vscode/launch.json с помощью которого можно "совмещать" предсобранный elf-файл с имеющимися исходниками.

Поле "substitutePath" задаёт замену префиксов: все пути, начинающиеся с /workdir/zephyr, заменяются на локальный путь к исходникам Zephyr и т.д.

Внимание! в .vscode/launch.json указаны настройки для конкретно моего ПК, но любая нейронка на основе этого примера легко подскажет, что и как скорректировать именно вам.

Кнопки

Были взяты простейшие механические свитчи, которые работают по принципу замыкания цепи:

Внешний вид клавиш
Внешний вид клавиш

В моём наборе разброс времени дребезга был от 0,2 до 1,2 мс:

Замер дребезга контактов
Замер дребезга контактов

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

Подключение кнопок.

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

Первая кнопка очень легко подключилась на пин boot_button из базового DTS платы:

Описание клавиши Enter в device tree
Описание клавиши Enter в device tree
Первые проблемы

Но далее я случайным образом выбрал PA0 и PA1 и не угадал. Сначала возникла проблема с тем, что именно эти два вывода уже заняты АЦП:

Надо знать про альтернативное использование аппаратных ресурсов в DTS
Надо знать про альтернативное использование аппаратных ресурсов в DTS

Но даже после того, как я напрямую отключил adc1 в overlay - всё равно осталась коллизия по каналу EXTI, с которой я с ходу не разобрался. Пока что выбрал другие пустые пины.

Две другие кнопки заняли свободные пины PB6 и PB7:

     buttons {
         compatible = "gpio-keys";
         sw1: button_1 {
             gpios = <&gpiob 6 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>;
             label = "Copy Button (Ctrl+C)";
         };
         sw2: button_2 {
             gpios = <&gpiob 7 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>;
             label = "Paste Button (Ctrl+V)";
         };
     };

Работа с кнопками в итоге выглядит так:

Алгоритм обработки нажатия с учетом Ble и энергосбережения
Алгоритм обработки нажатий с учетом BLE и энергосбережения

Для отладки и проверки каждый раз при нажатии любой из клавиш зажигается встроенный светодиод на выводе PE4.

Отладочная схема:

Предварительная сборка
Предварительная сборка

Bluetooth

Принцип работы BLE в Zephyr.

Zephyr реализует Host-контроллерную архитектуру, где:

  • Host — всё, что работает на CPU1 (HCI-драйвер, L2CAP, ATT, GATT, SMP).

  • Controller — физический уровень, LL, PHY (реализовано в прошивке CPU2).

В Zephyr для STM32WB55 используется драйвер st,stm32wb-rf, который через IPCC передаёт HCI-команды и данные между CPU1 и CPU2.

Настройка BLE.

В overlay ничего добавлять не нужно, т. к. DTS на SoC уже есть:

В описании платы включено описание SoC
В описании платы включено описание SoC

Но вprj.conf довольно много компонентов операционной системы связано с BLE:

Фрагмент prj.conf проекта связанный с BLE
# ================================================================== #
#  Bluetooth LE HID-устройство ( stm32_button )                       #
#                                                                     #
#  Радиомодуль STM32WB55 (CPU2) управляется через IPCC/IPM-драйвер    #
#  (drivers/bluetooth/hci/ipm_stm32wb.c). Узел ble_rf уже включён     #
#  в stm32wb.dtsi, а zephyr,bt-hci = &ble_rf — в chosen.              #
#  Поэтому CONFIG_BT_STM32_IPM включается автоматически.              #
# ================================================================== #

# Базовый стек Bluetooth LE (Host + Controller-драйвер IPM)
CONFIG_BT=y

# Отладочный лог HCI-драйвера IPM (ipm_stm32wb.c): показывает запуск CPU2,
# ошибки IPCC/клоков, таймаут C2_STARTED и т.п. Временно для диагностики.
# CONFIG_BT_HCI_DRIVER_LOG_LEVEL_DBG=y

# Периферийная роль (обходимся как HID-клавиатура/пульт)
CONFIG_BT_PERIPHERAL=y

# Менеджер безопасности (SMP): HID требует шифрованного канала
CONFIG_BT_SMP=y

# Хранение ключей спаривания и ID-адреса во flash (NVS).
# ВКЛЮЧЕНО: сохраняет bonding между перезагрузками (ключи, IRK, CSRK).
# ВАЖНО: при CONFIG_BT_SETTINGS=y стек НЕ создаёт ID-адрес сам.
# Приложение обязано вызвать settings_load() ПОСЛЕ bt_enable()
# (в callback'е bt_ready в ble.c), иначе реклама падает с -EAGAIN (-11):
#   "No ID address. App must call settings_load()".
CONFIG_BT_SETTINGS=y
CONFIG_SETTINGS=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y

# Имя устройства, которое увидит хост (телефон/ПК) при сканировании
CONFIG_BT_DEVICE_NAME="Badge"

# Внешний вид (Appearance): 0x03C1 = 961 = HID Generic Keyboard
CONFIG_BT_DEVICE_APPEARANCE=961

# Увеличиваем стек системной очереди работ (settings/BT требуют больше)
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048

# ================================================================== #
#  Энергосбережение BLE (connection parameters)                       #
#                                                                     #
#  CPU1 теперь засыпает (CONFIG_PM + TICKLESS, см. верх файла).      #
#  Дополнительно минимизируем активность радио CPU2 через            #
#  большие connection interval + slave latency:                      #
#  радио просыпается лишь раз в ~4 секунды.                          #
#                                                                     #
#  Параметры передаются хосту через GAP; хост применяет их           #
#  ПОСЛЕ успешного спаривания.                                        #
# ================================================================== #
CONFIG_BT_GAP_PERIPHERAL_PREF_PARAMS=y
# interval: единица 1.25 мс.
# MIN=320, MAX=320 → 400 мс между connection events.
CONFIG_BT_PERIPHERAL_PREF_MIN_INT=320
CONFIG_BT_PERIPHERAL_PREF_MAX_INT=320
# slave latency: пропуск 9 events → радио спит ~4 сек (9*400 мс)
CONFIG_BT_PERIPHERAL_PREF_LATENCY=9
# supervision timeout: единица 10 мс. 600 = 6 сек.
# Требование BLE: timeout > (1+latency)*interval*2 → 6 > (1+9)*0.4*2=8 ❌
# Поэтому при latency=9 interval=400мс нужен timeout > 8 сек → ставим 100 (10с).
CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=1000

Ранее я никогда с BLE не работал и опыт с Zephyr показался мне весьма интуитивно понятным. Вероятно, я просто не понимаю, что упускаю из-за отсутсвия опыта, но внешне всё выглядит работающим. Вопросы были только с повторным переподключением к ПК из-за того, что при перепрошивке МК содержимое flash-памяти с ключами безопасности было пустым, но это поправилось простой проверкой.

Требования к прошивке CPU2.

Прошивка второго ядра должна поддерживать HCI-интерфейс (Host Controller Interface). Но на плате изначально установлена Full Stack прошивка от ST, которая несовместима с Zephyr.

Для работы с Zephyr необходимо заменить её на HCI Layer прошивку:

  1. Скачать файл stm32wb5x_BLE_HCILayer_extended_fw.binиз репозитория ST.

  2. Установить эту прошивку с помощью STM32CubeProgrammer через раздел работы с FUS по адресу 0x080DC000.

  3. Запустить эту прошивку (требуется только один раз).

Энергопотребление

Изначально замеренное энергопотребление с включенным BLE у меня составило примерно от 6 до 12 мА. Что для аккумуляторного устройства значит не более одного дня работы. На текущей версии прошивки у меня получилось достичь среднего тока потребления ~900 мкА на полностью рабочем устройстве, что уже немного лучше, но всё равно на два порядка больше желаемого.

Текущий средний ток потребления во время работы Ble
Текущий средний ток потребления во время работы Ble

Ключевые меры, принятые для достижения указанной величины тока:

  • CPU1 засыпает (CONFIG_PM=y, CONFIG_PM_DEVICE=y, CONFIG_TICKLESS_KERNEL=y)

  • Дополнительно минимизируем активность радио CPU2 через большие connection interval + slave latency.

  • Через несколько секунд бездействия клавиатура уходит в сон и по нажатии на клавишу снова подключается к ПК.

Сборка устройства

Преимущества кокосовой скорлупы для DIY-электроники очевидны:

  • Крепко.

  • Экологично.

  • Эргономично.

  • Передаёт дух vibe-кодинга.

  • Создаёт рабочие места для молодёжи.

Процесс сборки верхней половины корпуса
Подбор сырья
Подбор сырья
Убираем лишнее
Убираем лишнее
Доработка напильником
Доработка напильником
Создаём технологические отверстия для посадочных мест кнопок
Создаём технологические отверстия для посадочных мест кнопок
Создаём технологические отверстия для посадочных мест кнопок
Создаём технологические отверстия для посадочных мест кнопок
Отверстие для свитча
Отверстие для свитча
Установка
Установка
Кастомизация кейкапов
Кастомизация кейкапов

Преимущества сухих строительных смесей для DIY-электроники очевидны:

  • Крепко.

  • Экологично.

  • Эргономично.

  • Передаёт дух vibe-кодинга.

  • Создаёт рабочие места для молодёжи.

Процесс сборки верхней половины корпуса
Разводим с водой 2:3
Разводим с водой 2:3
Выходной контроль
Выходной контроль

К сожалению на этапе лепки всё было в грязи и было не до фотографий.

Исходники

https://github.com/RomanBashmakov/coconut_vibe

Конечно, можно программно задать какие-то полезные комбинации по нажатии вроде ввода пароля от ПК или команды "git log --graph --oneline --all" и таким образом сделать девайс более "полезным", но зачем. В любом случае все исходники есть по ссылке и они будут постепенно дополняться, так как есть намерение и дальше разбираться с этой ОС.