14.1.1.4. Отладка прошивки

Аппаратная отладка означает остановку процессора, установку точек останова в исходном коде на C, пошаговое выполнение и просмотр переменных, памяти, регистров и периферийных устройств – прямо из VS Code. Для этого нужны три вещи: отладочная сборка, отладочный зонд SWD (Segger J-Link) и расширение Cortex-Debug, управляющее arm-none-eabi-gdb через GDB-сервер J-Link.

14.1.1.4.1. Сборка для отладки

Всегда пересобирайте целевую прошивку с DEBUG=1:

make -j$(nproc) TARGET=<TARGET> DEBUG=1

Релизный образ (DEBUG=0) компилируется с -O2; в отладчике вы будете видеть <optimized out> для многих переменных, встраиваемые функции сворачиваются в свои вызывающие функции, а пошаговое выполнение перескакивает непредсказуемо. Сборки с DEBUG=1 используют -Og -ggdb3, что обеспечивает отлаживаемость, при этом прошивка всё ещё загружается на камере. ELF-файл, на который вы направляете отладчик, это:

build/<TARGET>/bin/firmware.elf

(Для Alif AE3 отлаживайте build/OPENMV_AE3/bin/firmware_M55_HP.elf – высокопроизводительное ядро.)

14.1.1.4.3. Настройка Cortex-Debug в VS Code

Создайте .vscode/launch.json в репозитории. Простейший случай – VS Code, J-Link и сборка находятся на одной машине Linux / macOS – использует servertype: "jlink", что заставляет Cortex-Debug самостоятельно запускать GDB-сервер J-Link:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "OpenMV J-Link",
      "type": "cortex-debug",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "executable": "${workspaceFolder}/build/OPENMV4/bin/firmware.elf",
      "servertype": "jlink",
      "device": "STM32H743VI",
      "interface": "swd",
      "runToEntryPoint": "main",
      "armToolchainPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin",
      "gdbPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin/arm-none-eabi-gdb"
    }
  ]
}

Измените executable и device под вашу плату (см. таблицу выше). Нажмите F5, чтобы собрать, прошить, запустить до main и остановиться там.

Совет

Чтобы автоматически выполнять пересборку при каждом запуске отладки, добавьте задачу сборки в .vscode/tasks.json и сошлитесь на неё из конфигурации запуска через "preLaunchTask". Например, задача, выполняющая make -j$(nproc) TARGET=OPENMV4 DEBUG=1, с именем "build-firmware", плюс "preLaunchTask": "build-firmware" в приведённой выше конфигурации, чтобы F5 пересобирал, прошивал и запускал отладчик за один шаг.

Предупреждение

Cortex-Debug требует arm-none-eabi-gdb. Он поставляется в составе SDK по пути ~/openmv-sdk-<version>/gcc/bin, но по умолчанию не добавлен в PATH, поэтому отладка завершается ошибкой «GDB executable „arm-none-eabi-gdb“ was not found». Исправьте это либо установив armToolchainPath / gdbPath, как показано выше, либо добавив ~/openmv-sdk-<version>/gcc/bin в ваш PATH (тогда printenv PATH должен его перечислять).

14.1.1.4.4. Просмотр регистров периферии (SVD)

Направьте Cortex-Debug на файл CMSIS SVD, чтобы получить декодированный просмотр регистров периферии (таймеры, DMA, интерфейс камеры и т.д.) по имени и битовым полям:

"svdFile": "/path/to/STM32H743.svd"

Для STM32 и MIMXRT получите SVD из пакетов CMSIS ST / NXP или из реестра SVD Cortex-Debug. SVD-файлы Alif включены в репозиторий прошивки по пути lib/micropython/lib/alif_ensemble-cmsis-dfp/Debug/SVD/ (используйте ..._CM55_HP_View.svd для HP-ядра AE3).

14.1.1.4.6. Отладка из командной строки с помощью gdbrunner

Настройка сеанса GDB для встраиваемой цели вручную – это танец из пяти шагов: запустить GDB-сервер J-Link / ST-Link в одном окне с правильными флагами устройства, порта и интерфейса; дождаться, пока он выведет Waiting for GDB connection; запустить arm-none-eabi-gdb во втором окне; ввести target remote localhost:<port>; направить gdb на ELF. Когда сеанс gdb завершается, нужно не забыть закрыть окно сервера. gdbrunner – это небольшой CLI, сворачивающий всё это в одну команду на переднем плане. Он поставляется в составе Python-окружения SDK OpenMV, поэтому устанавливать ничего не нужно; обычная точка входа – цель make debug репозитория прошивки:

make -j$(nproc) TARGET=<TARGET> DEBUG=1 debug

Это запускает gdbrunner с аргументами отладчика из конфигурации платы – именем устройства J-Link и, где это необходимо, внешним загрузчиком флеш-памяти ST-Link – с arm-none-eabi-gdb из SDK, уже находящимся в PATH. Бэкенд по умолчанию – J-Link; make DEBUGGER=STLINK debug работает с зондом ST-Link.

gdbrunner также можно вызывать напрямую (вне SDK, pip install gdbrunner):

gdbrunner jlink --device STM32H743VI build/OPENMV4/bin/firmware.elf
gdbrunner stlink build/OPENMV4/bin/firmware.elf
gdbrunner qemu --machine mps2-an500 build/MPS2_AN500/bin/firmware.elf

Первый позиционный аргумент выбирает бэкенд сервера (jlink, stlink, qemu); остальные передаются этому бэкенду, со значениями по умолчанию, подходящими для камер OpenMV. gdbrunner --help выводит полный список флагов для каждого бэкенда; таблица аргументов каждого бэкенда задаётся через JSON (src/gdbrunner/backends.json), поэтому добавление нового сервера – это правка конфигурации, а не кода.

Что gdbrunner делает для работы из командной строки:

  • Один процесс, чистый жизненный цикл. Сервер запускается, gdb подключается, когда порт открыт, сервер корректно завершается при выходе из gdb. Никаких осиротевших процессов JLinkGDBServer, переживающих сеанс, никаких двух терминалов для управления.

  • Автообнаружение STM32CubeProgrammer. Бэкенд stlink ищет инструменты STM32CubeProgrammer в обычных местах установки (~/STM32CubeProgrammer/, /opt/st/, дерево плагинов STM32CubeIDE), чтобы длинный путь --cube-prog не приходилось вводить каждый раз. SDK содержит собственную копию по пути ~/openmv-sdk-<version>/stcubeprog/bin – направьте --cube-prog туда, если системной установки нет.

  • Учёт gdbinit для каждого проекта. Файл .gdbinit в текущем каталоге загружается с -ix – переопределяя общесистемный ~/.gdbinit – поэтому gdb-скрипты для конкретного проекта (pretty-printer’ы, макросы для конкретной платы, наборы точек останова) подключаются простым присутствием в рабочем каталоге. make debug запускается из корня репозитория, поэтому .gdbinit там применяется.

  • Пробный прогон. --dryrun выводит команду сервера без её запуска, что полезно для адаптации вызова к скрипту-обёртке, копирования в конфигурацию запуска IDE или просто для проверки того, какие аргументы составляет gdbrunner.

  • Видимый вывод сервера. --show-output сохраняет stdout / stderr сервера видимыми. По умолчанию они подавляются (чтобы интерфейс gdb оставался чистым); включайте этот флаг, когда некорректно ведёт себя сам сервер.

  • Бэкенд QEMU. qemu-system-arm отлаживает сборку прошивки без подключённой платы. Цель MPS2_AN500 выбирает этот бэкенд в своей конфигурации платы, поэтому make TARGET=MPS2_AN500 DEBUG=1 debug собирает для машины mps2-an500 QEMU и пошагово выполняет платформенно-независимый код – всё, что не затрагивает специфичную для камеры периферию – в полёте. (qemu-system-arm устанавливается на хост, а не входит в SDK.)

Для пошагового выполнения на уровне исходного кода с маркерами точек останова на полях и просмотром регистров периферии лучше подходит описанная выше настройка Cortex-Debug в VS Code; gdbrunner – правильный инструмент для всего, что живёт в командной строке.

14.1.1.4.7. Использование отладчика

Когда сеанс запущен (процессор остановлен на main):

  • Точки останова – кликните по полю рядом со строкой на C или в Debug Console введите break <file>:<line> / break <function>. Ядра Cortex-M имеют небольшое число аппаратных компараторов точек останова (обычно 6–8 на M7 / H7, 8 на M55). Превышение этого числа для кода во флеш-памяти молча не срабатывает – держите количество активных точек останова умеренным.

  • Пошаговое выполнениеF10 шаг с обходом (next), F11 шаг с заходом (step), Shift+F11 шаг с выходом (finish), F5 продолжить. Пошаговое выполнение на уровне инструкций – это stepi / nexti в Debug Console.

  • Переменные / наблюдение / стек вызовов – панели Variables и Call Stack показывают локальные переменные и обратную трассировку; добавляйте выражения в Watch. Наведите курсор на переменную в исходном коде, чтобы увидеть её значение. Всё, что отображается как <optimized out>, означает, что вы не на сборке с DEBUG=1.

  • Точки наблюдения (точки останова по данным)watch <expr> останавливается при записи переменной, rwatch при чтении, awatch при любом из них. Блок DWT Cortex-M поддерживает ~4 аппаратных точки наблюдения – незаменимо для выяснения того, кто повредил переменную.

  • Регистры и периферия – представление Cortex Registers показывает регистры ядра; при установленном svdFile представление Peripherals декодирует каждый регистр периферии и битовое поле (DMA, таймеры, интерфейс камеры / CSI, XSPI и т.д.) – самый быстрый способ понять, почему драйвер ведёт себя неправильно.

  • Память – используйте просмотрщик памяти Cortex-Debug или x/ в gdb для прямого просмотра буферов кадров, буферов DMA и структур.

  • printf без остановки (SWO/RTT) – для проблем, чувствительных к таймингу, Segger RTT или SWO обеспечивает printf с почти нулевыми накладными расходами во время работы целевой системы. Соберите с DEBUG_PRINTF=1 и добавьте rttConfig (RTT) или swoConfig (SWO, требует частоту ядра) Cortex-Debug. Это правильный инструмент, когда точка останова изменила бы тайминг, который вы пытаетесь наблюдать.

  • ОтключениеStop в сеансе launch останавливает целевую систему; Disconnect в сеансе attach оставляет камеру работающей. После этого выполните перезагрузку питания камеры, чтобы вернуть её к нормальной работе.

14.1.1.4.8. Подводные камни отладки

  • Оптимизированные переменные. Всё отображается как <optimized out> – вы собрали с DEBUG=0. Пересоберите с DEBUG=1.

  • «GDB executable not found»gcc/bin из SDK не в PATH; установите armToolchainPath / gdbPath.

  • «Cannot connect» / неверная карта памяти – неверное или отсутствующее имя device; используйте точную строку из таблицы.

  • Точки останова молча не срабатывают – слишком много аппаратных точек останова на коде, размещённом во флеш-памяти; уменьшите их количество.

  • Пути к исходникам не совпадают (ELF, собранный в Docker) – собирайте с целью Docker build-firmware-dev (одинаковый абсолютный путь внутри и вне контейнера) или установите set substitute-path в gdb.