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.2. 硬件:通过 SWD 连接的 J-Link¶
将 Segger J-Link 连接到摄像头的 SWD 引脚(SWDIO、SWCLK、GND,以及作为参考的目标 VCC;摄像头照常通过 USB 供电)。J-Link EDU / Base / Pro 均可使用。调试引脚的引出位置因摄像头而异 —— 许多板子有专用的 JTAG/SWD 连接器,另一些则在 I/O 排针或测试焊盘上引出 SWD —— 因此请查阅 OpenMV 硬件文档中该板子的引脚图和原理图,以确定要接哪些引脚。在物理连接探头的那台机器上,从 segger.com 安装 J-Link Software and Documentation Pack。请保持其版本相对较新 —— 较旧的 J-Link 软件不会识别较新的设备名称(STM32N6、MIMXRT、Alif)。
每个 MCU 都需要其确切的 J-Link 设备名称,以便探头加载正确的闪存加载器和内存映射:
摄像头( |
MCU |
J-Link |
|---|---|---|
|
STM32F427 |
|
|
STM32F765 |
|
|
STM32H743 |
|
|
STM32N657 |
|
|
MIMXRT1062 |
|
|
Alif Ensemble (M55-HP) |
|
|
STM32H747 |
|
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"
}
]
}
为你的板子更改 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,可从 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.5. Windows:WSL ↔ Windows J-Link 桥接¶
WSL 2 无法直接看到 J-Link 的 USB 设备,因此分工如下:Windows 提供探头服务(探头插在那里),而 VS Code + gdb 在 WSL 中运行 并通过 TCP 连接到它。
在 Windows 上,安装 Segger J-Link 包并将 J-Link 插入 Windows 的 USB 端口。
在 Windows 上,启动 J-Link Remote Server(它随 J-Link 包一同提供):在连接 J-Link 的情况下启动它并点击 OK。在提示时允许它通过 Windows 防火墙。窗口会显示它为探头提供服务所用的 IP 地址 —— 记下它。
在 WSL 中,构建
DEBUG=1版本,并确保arm-none-eabi-gdb可访问(如上所述设置armToolchainPath)。在 WSL 的 VS Code 中,保持
servertype: "jlink"—— GDB 服务器在 WSL 中运行,并通过 Remote Server 连接到探头 —— 并添加serverpath+ipAddress::{ "name": "OpenMV J-Link (Windows host)", "type": "cortex-debug", "request": "launch", "cwd": "${workspaceFolder}", "executable": "${workspaceFolder}/build/OPENMV4/bin/firmware.elf", "servertype": "jlink", "serverpath": "/opt/SEGGER/JLink/JLinkGDBServer", "ipAddress": "192.168.x.x", "device": "STM32H743VI", "interface": "swd", "runToEntryPoint": "main", "armToolchainPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin" }
将
ipAddress设置为 Remote Server 窗口显示的地址。这就是整个桥接过程。
小技巧
GDB 服务器桥接的替代方案:usbipd-win。 你无需在 Windows 上运行服务器,而是可以用 usbipd-win 将 J-Link 的 USB 设备直接挂载进 WSL。在管理员 PowerShell 中::
winget install usbipd
usbipd list
usbipd bind --busid <busid>
usbipd attach --wsl --busid <busid>
(<busid> 是 usbipd list 中 J-Link 的总线 ID。)随后探头会出现在 WSL 内部,你就可以使用来自 VS Code Cortex-Debug setup 的普通同机 servertype: "jlink" 配置,无需 IP 地址,也无需单独的 Windows 服务器。对于偶尔使用,GDB 服务器桥接所需的设置较少;而 usbipd-win 对于日常开发更为便利。
小技巧
使用 "request": "attach" 可在固件 已经运行 的状态下调试它,无需复位或重新烧录 —— 非常适合在现场捕捉挂起问题。使用 "request": "launch" 则会复位、烧录 ELF,并在 runToEntryPoint 处全新启动。
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
第一个位置参数选择服务器后端(jlink、stlink、qemu);其余参数则转发给该后端,并带有适用于 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。变量 / 监视 / 调用栈 —— Variables 和 Call 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 RTT 或 SWO 可在目标运行时提供近乎零开销的
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。