Black Magic Probe на ESP32-C5: беспроводной отладчик без OpenOCD

В прошлой статье я сделал обзор популярных способов отладки embedded: GDB-клиент -> GDB-сервер (OpenOCD) -> программатор -> целевой МК. Тогда я сформулировал свои субъективные требования к “идеальному” программатору. И на тот момент у меня уже был минимально рабочий вариант такого программатора. И вот спешу представить к чему привели меня изыскания в этом направлении.

Немного контекста

В прошлой статье я закончил списком того, чего хочу от отладочного инструмента:

  • Отлаживать устройства удаленно

  • Не запускать gdb-серверы руками

  • Не настраивать их и не писать конфигурационные файлы

  • Не ставить драйвера

  • Использовать кроссплатформенную легковесную IDE

  • Поддержка широкого спектра целей: ARM Cortex-M/A/R, RISC-V

Black Magic Probe закрывает почти все пункты: gdb-сервер уже внутри прошивки программатора, OpenOCD не нужен, драйвера не нужны. Да, в комметариях прошлой статьи мне написали что список целей нужно вшивать и они могу не поместиться. В ESP32 не поместиться? В общем поместились ;) Ну и по поводу IDE, можно использовать любую. Это слишком простые интерфейсы чтобы было что-то сложное. Беспроводное соединение устанавливается через TCP, которое поддерживает GDB, а проводное - обычный VCP который gdb тоже поддерживает (но об этом чуть позже).

В процессе разработки список хотелок только расширялся.

  • Тут и возможность отладиться по проводу (почему бы и нет)

  • И поддержка RTT логирования по тому же TCP (черерз telnet или nc) или VCP

  • Еще хочу serial чтобы подключиться к цели в uart и видеть это через telnet или nc. Тоже через TCP или VCP

  • Если из веб инетрфейса нельзя закинуть прошивку Drag & Drop чтобы даже кнопок нажимать не пришлось, то зачем это все вообще делать? [Сарказм] Камень определяется автоматически и выставлен дефолтный адрес который можно поменять на нужный.

  • Стереть тоже надо одним кликом

  • Запоминать wifi точки доступа чтобы удобно было везде

  • А если из известных вокруг ни одной не оказалось то расшарить свою с дефолтными кредами. Можно остаться в ней, но интернета не будет, а можно добавить нужную и ребутнуть.

  • Если бы я все это сделал и искал каждый раз ip устройства, то можно было бы удалить этот проект ибо смысла во всем этом нет. Так что добавил mDNS. Так что как только устройство подключилось к сети, то в этой сети можно зайти на страничку по имени blackmagic.local. Это же имя используется в telnet.

Что внутри: сетевые сервисы

Прошивка поднимает на ESP32-C5 четыре сетевых сервиса:

Порт

Сервис

Назначение

80

HTTP

Веб-интерфейс: OTA, Wi-Fi, конфигурация пинов

2345

GDB remote serial protocol

GDB-сервер - основной сервис

2346

RTT console

Segger RTT-консоль цели (telnet/nc)

2347

Target serial

UART-мост к целевой плате (telnet/nc)

Дальше я опишу как что-либо делать через GDB терминал, это может быть избыточно, я же себе жизнь наоборт упростить хотел. Так что если у вас IDE то она все сделает за вас.

GDB-сервер (порт 2345)

Сердце проекта. Подключение из GDB выглядит так:

(gdb) target extended-remote blackmagic.local:2345
(gdb) monitor swdp_scan
(gdb) attach 1

То есть ровно те же команды monitor swdp_scan / attach 1, что и у классического BMP, только вместо /dev/ttyACM0 - host:port. Никакого OpenOCD, никаких конфигов.

Имя blackmagic.local можно использоавть и здесь и при подключении к RTT и т.д.

swdp_scan - это SWD. JTAG тоже поддерживается: у цели ESP32-C5 есть и SWD, и JTAG-режимы, все линии (TMS/SWDIO, TCK/SWCLK, TDI, TDO/TRACESWO, TRST) разведены на GPIO и переназначаются на лету через веб-интерфейс.

RTT (порт 2346)

RTT (Real-Time Transfer) от Segger - способ вывода логов из цели в реальном времени через отладочный интерфейс, почти без нагрузки на CPU.

$ nc blackmagic.local 2346

или через telnet. Внутри GDB управление такое:

(gdb) monitor rtt enable
(gdb) monitor rtt status

Target serial (порт 2347)

UART-мост: то, что целевая плата пишет в свой UART, доступно по TCP на порту 2347. Удобно, когда у цели есть лог в UART, а ты физически рядом не стоишь.

Честное признание: порт сейчас конфликтует с логами самой ESP32 (которые тоже валятся в UART0), так что сервис в статусе «работает, но с оговорками». Думаю что в релизной сборке логи не нужны.

Веб-интерфейс (порт 80)

Тут три страницы:

1. OTA-прошивка. Просто перетаскиваешь .bin файл в браузер - и плата перепрошивается. Drag & drop, ничего кроме браузера не нужно. Если на телефоне есть бинарник, то и с телефона можно прошить.

2. Настройки Wi-Fi. Список SSID и пароль точкек доступа, куда плата должна подключаться. Сохраняется в NVS и перживает перезагрузку.

3. Конфигурация пинов. Какие GPIO платы отвечают за SWDIO/SWCLK/TDI/TDO/TRST. Тоже в NVS, тоже переживает перезагрузку. Меняешь на лету, без перепрошивки. Это я сделал на всякий случай если поменяется разводка платы.

Первый запуск

Сборка - стандартный ESP-IDF:

idf.py set-target esp32c5
idf.py build
idf.py flash monitor

Фронтенд собирается автоматически через npm install && npm run build - CMake это делает сам, руками ничего запускать не надо.

После первой прошивки:

  1. Плата пытается подключиться к сохраненному Wi-Fi

  2. Если не получилось - поднимает свою точку доступа с SSID blackmagic и паролем blackmagic

  3. Подключаешься к этой точке, открываешь blackmagic.local или http://192.168.4.1, вводишь SSID/пароль своей сети

  4. Плата перезагружается и подключается к твоему Wi-Fi

  5. Дальше она доступна как blackmagic.local

GDB через USB

Wi-Fi - это основная фича, но провод тоже поддерживается. Прошивка умеет работать через USB-Serial-JTAG - встроенный в ESP32-Cx периферийный блок, который торчит в системе как CDC-устройство:

(gdb) target extended-remote /dev/ttyACM0    # Linux
(gdb) target extended-remote /dev/cu.usbmodem11101  # macOS

При этом логи/консоль остаются на UART0 (отдельные пины), чтобы USB-периферия была полностью отдана под GDB и не делилась с консолью. Все фичи (monitor swdp_scan, attach, RTT) работают и по USB, и по сути - код ядра BMP один и тот же, отличается только транспорт.

Собрать без USB-GDB тоже можно: в CMakeLists.txt есть флаги:

  • ENABLE_USB_GDB - включает в сборку отладку по USB

  • ENABLE_UART_SERIAL - пока оставил отдельное включение serial поскольку нужен лог от esp

  • ENABLE_RTT - этот флаг предоставляет сам BMP для включения RTT

SDK config: грабли, на которые я наступил

Тут будет самая «техническая» часть статьи - те нюансы, которые не описаны в документации и которые пришлось выяснять экспериментально.

1. Раздел PARTITION_TABLE_SINGLE_APP_LARGE. Прошивка с BMP-ядром + Wi-Fi + HTTP-сервером + фронтендом не влезает в дефолтный 1 МБ app-раздел. Пришлось использовать “большой” раздел на 3 МБ.

2. WHOLE_ARCHIVE для компонента esp32-platform. В проекте есть target_probe.c со слабыми (weak) заглушками функций-зондов, а реальные реализации - в отдельных файлах. По умолчанию линкер выкидывает «неиспользуемые» объектные файлы, и сильные символы из отдельных файлов не переопределяют слабые заглушки - probe-функции просто не вызываются, отладчик не видит цели. WHOLE_ARCHIVE заставляет линкер взять весь архив целиком, и сильные символы побеждают. Не совсем понимаю почему это так работает, но это так.

3. Отключенные watchdog’и. Операции прошивки и отладки цели (flash erase, jtag-транзакции, длинные последовательности) могут превышать таймауты ESP_INT_WDT и ESP_TASK_WDT - система ребутается посреди прошивки цели. Поэтому в sdkconfig.defaults оба watchdog’а выключены:

CONFIG_ESP_INT_WDT=n
CONFIG_ESP_TASK_WDT_EN=n

4. Консоль на UART0:

CONFIG_ESP_CONSOLE_UART_DEFAULT=y
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y

чтобы USB-Serial-JTAG не перехватывал консоль и оставался целиком под GDB.

Все это уже в sdkconfig.defaults в репозитории - руками ничего настраивать не надо.

А что с целями?

Ядро прошивки - это оригинальный Black Magic Probe (благодаря blackmagic сообществу), поэтому поддерживается весь спектр целей, которые умеет BMP: ARM Cortex-M, Cortex-A, Cortex-R и RISC-V, по SWD и по JTAG.

Распиновка по умолчанию (для ESP32-C5):

Сигнал

GPIO

TMS / SWDIO

26

TCK / SWCLK

25

TDI

23

TDO / TRACESWO

24

TRST

27

Как я уже говорил, переназначается через веб-интерфейс и сохраняется в NVS.

VS Code без конфигов

Без настройки IDE не обойтись в любом случае. Я пользуюсь VS Code и тут сложностей нет потому что сообщество cortex-debug поддерживает BMP и конфиг написать достаточно просто. Я создал шаблон конфигурации: bm-vscode-configs - репозиторий с готовым launch.json для Cortex-Debug, включая RTT. Копируешь в свой проект, там будет шаблон файла settings.json в котором нужно прописать путь до .elf, путь до .svd если надо, и саму цель. Без этого минимума никак. Можно запускать отладку.

Итог

Цена решения - стоимость чипа ESP32-C5. Проект открыт: blackmagic-esp32-c5. Никакого дополнительного железа. Все мои хотелки закрыты.

Я конечно не сомневался, но многие сомневались в качестве беспроводного соединения. Мол все будет отваливаться и т.п. Возможно, мне повезло, возможно, я ни разу не столкнулся с плохой сетью в 2026 году, но у меня ни разу отладка так и не отвалилась просто так.

Плата: всё в одном USB

Прошивка работает на голом модуле ESP32-C5, но чтобы пользоваться всеми описанными преимуществами связанными с проводной отладкой я развёл плату, которая собирает в себе всё, что нужно для отладки, и заводит это в один USB-разъём - хаб на борту.

Что на плате:

Устройство

Что даёт

Как видно в системе

ESP32-C5 (USB-Serial-JTAG)

Прошивка esp и GDB-сервер

JTAG и CDC-устройство, /dev/ttyACM0

USB-UART #1

UART цели

второй CDC

USB-UART #2

RTT цели

третий CDC

Плата
Плата

То есть не нужно три отдельных кабеля и три отдельных переходника. Воткнул один провод - получил и беспроводной BMP, и проводной GDB, и оба UART цели. Порядок /dev/ttyACM* при этом стабильный, потому что устройства на одном хабе и не переподключаются.

Зачем это, если Wi-Fi и так есть?

Wi-Fi - основная фича, но провод остаётся нужен:

  • первый запуск чтобы прошить esp32;

  • банально - питание если у цели его нет;

  • если по каким-то причинам wifi полосы забиты и соединение нестабильно (у меня пока такого не было);

  • любые другие причины по которым нужно подключиться по проводу.