14.1.1.4. 调试固件

硬件调试是指在 VS Code 内部暂停处理器、在 C 源码中设置断点、单步执行,并检查变量、内存、寄存器和外设。这需要三样东西:一个 调试构建、一个 SWD 调试探头(Segger J-Link),以及驱动 arm-none-eabi-gdb 连接 J-Link GDB 服务器的 Cortex-Debug 扩展。

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. VS Code Cortex-Debug 设置

在仓库中创建 .vscode/launch.json。最简单的情况 —— VS Code、J-Link 和构建都在 同一台 Linux / macOS 机器上 —— 使用 servertype: "jlink",这会让 Cortex-Debug 自行启动一个 J-Link GDB 服务器::

{
  "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"
    }
  ]
}

为你的板子更改 executabledevice(参见上表)。按 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,可从 ST / NXP CMSIS 包或 Cortex-Debug SVD 注册表获取 SVD。Alif 的 SVD 已随固件仓库一同提供,位于 lib/micropython/lib/alif_ensemble-cmsis-dfp/Debug/SVD/(AE3 HP 核心请使用 ..._CM55_HP_View.svd)。

14.1.1.4.6. 使用 gdbrunner 进行命令行调试

手动针对嵌入式目标搭建 GDB 会话是一套五步流程:在一个窗口中以正确的设备、端口和接口标志启动 J-Link / ST-Link GDB 服务器;等待它打印 Waiting for GDB connection;在第二个窗口中运行 arm-none-eabi-gdb;输入 target remote localhost:<port>;让 gdb 指向 ELF。当 gdb 会话结束时,还要记得关闭服务器窗口。gdbrunner 是一个小型 CLI,它把这一切收拢为一条前台命令。它随 OpenMV SDK 的 Python 环境一同提供,因此无需安装;通常的入口点是固件仓库的 make debug 目标::

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

这会使用来自板子配置的调试器参数来运行 gdbrunner —— 即 J-Link 设备名称,以及在需要时的 ST-Link 外部闪存加载器 —— 并已将 SDK 的 arm-none-eabi-gdb 置于 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

第一个位置参数选择服务器后端(jlinkstlinkqemu);其余参数则转发给该后端,并带有适用于 OpenMV 摄像头的默认值。gdbrunner --help 会列出每个后端的完整标志列表;每个后端的参数表都由 JSON 驱动(src/gdbrunner/backends.json),因此添加一个新服务器只需修改配置而非代码。

gdbrunner 在命令行工作中所做的事:

  • 单进程,清晰的生命周期。 服务器启动,端口打开后 gdb 接入,gdb 退出时服务器被干净地终止。没有在会话结束后残留的孤立 JLinkGDBServer,也无需管理两个终端。

  • STM32CubeProgrammer 自动发现。 stlink 后端会在常见的安装位置(~/STM32CubeProgrammer//opt/st/、STM32CubeIDE 插件目录树)中搜索 STM32CubeProgrammer 工具,因此不必每次都输入冗长的 --cube-prog 路径。SDK 在 ~/openmv-sdk-<version>/stcubeprog/bin 自带了一份副本 —— 若系统中没有安装,可将 --cube-prog 指向那里。

  • 遵循每个项目的 gdbinit。 当前目录中的 .gdbinit 会用 -ix 加载 —— 覆盖用户级的 ~/.gdbinit —— 因此只要将每个项目的 gdb 脚本(美化打印器、特定板子的宏、断点集)放在工作目录中,它们就会生效。make debug 从仓库根目录运行,因此那里的 .gdbinit 会被应用。

  • 试运行。 --dryrun 会打印服务器命令而不实际运行它,这在将该调用改写成封装脚本、复制到 IDE 启动器配置中,或只是查看 gdbrunner 正在组装哪些参数时很有用。

  • 服务器输出可见。 --show-output 会保持服务器的 stdout / stderr 可见。默认情况下会抑制它(以使 gdb 的界面保持整洁);当出问题的正是服务器本身时,可翻转此标志。

  • QEMU 后端。 qemu-system-arm 可在没有插入任何板子的情况下调试固件构建。MPS2_AN500 目标在其板子配置中选择此后端,因此 make TARGET=MPS2_AN500 DEBUG=1 debug 会为 QEMU 的 mps2-an500 机器构建,并对平台无关的代码 —— 一切不涉及摄像头专用外设的部分 —— 进行单步执行,无需实际硬件。(qemu-system-arm 是主机端安装项,不属于 SDK。)

对于带断点边栏和外设寄存器视图的源码级单步执行,上面的 VS Code Cortex-Debug 设置是更好的工具;而 gdbrunner 则适合一切在命令行中进行的工作。

14.1.1.4.7. 使用调试器

一旦会话开始运行(处理器停在 main 处):

  • 断点 —— 点击 C 代码行旁边的边栏,或在 Debug Console 中输入 break <file>:<line> / break <function>。Cortex-M 核心只有少量 硬件 断点比较器(M7 / H7 上通常为 6—8 个,M55 上为 8 个)。超出这一数量后,对闪存中代码设置的断点会悄无声息地失败 —— 请将活动断点的数量控制在适度范围。

  • 单步执行 —— F10 单步跳过(next),F11 单步进入(step),Shift+F11 单步跳出(finish),F5 继续。指令级单步执行在 Debug Console 中是 stepi / nexti

  • 变量 / 监视 / 调用栈 —— VariablesCall Stack 窗格显示局部变量和回溯;将表达式添加到 Watch。在源码中悬停某个变量即可查看其值。任何显示 <optimized out> 的内容都意味着你不是在 DEBUG=1 构建上。

  • 观察点(数据断点) —— watch <expr> 在变量被写入时暂停,rwatch 在读取时暂停,awatch 在两者任一时暂停。Cortex-M 的 DWT 单元支持约 4 个硬件观察点 —— 对于查清 是谁 破坏了某个变量极为宝贵。

  • 寄存器和外设 —— Cortex Registers 视图显示核心寄存器;在设置了 svdFile 后,Peripherals 视图会解码每个外设寄存器和位字段(DMA、定时器、摄像头 / CSI 接口、XSPI 等)—— 这是查明某个驱动为何行为异常的最快途径。

  • 内存 —— 使用 Cortex-Debug 内存查看器或 gdb 的 x/ 来直接检查帧缓冲区、DMA 缓冲区和结构体。

  • 无需暂停的 printf(SWO/RTT) —— 对于时序敏感的问题,Segger RTTSWO 可在目标运行时提供近乎零开销的 printf。使用 DEBUG_PRINTF=1 构建,并添加 Cortex-Debug 的 rttConfig(RTT)或 swoConfig(SWO,需要核心时钟)。当断点会改变你正试图观察的时序时,这正是合适的工具。

  • 断开连接 —— 在 launch 会话上点击 Stop 会暂停目标;在 attach 会话上点击 Disconnect 则让摄像头继续运行。之后给摄像头断电重启即可使其恢复正常工作。

14.1.1.4.8. 调试中的陷阱

  • 被优化掉的变量。 一切都显示 <optimized out> —— 说明你构建的是 DEBUG=0。请用 DEBUG=1 重新构建。

  • "GDB executable not found" —— SDK 的 gcc/bin 不在 PATH 上;设置 armToolchainPath / gdbPath

  • "Cannot connect" / 内存映射错误 —— device 名称错误或缺失;使用表中确切的字符串。

  • 断点悄无声息地未命中 —— 对闪存驻留代码设置的硬件断点过多;减少它们。

  • 源码路径不匹配(Docker 构建的 ELF) —— 使用 Docker 的 build-firmware-dev 目标构建(容器内外的绝对路径相同),或设置 gdb 的 set substitute-path