1. 嵌入式调试基础:调试器原理、JTAG/SWD协议、调试环境搭建(OpenOCD + GDB)
嵌入式调试,说白了就是给芯片装个「内窥镜」。你写好的代码烧进去,跑起来对不对?卡在哪了?变量值是多少?这些都得靠调试器来回答。我刚开始做嵌入式那会儿,全靠串口打印,一个 bug 能调三天。后来学会了用调试器,效率翻了好几倍。
1.1 调试器到底在干什么?
调试器的核心任务就三个:控制执行、读写内存、读写寄存器。你想想看,没有调试器的时候,程序跑起来就像黑盒,你只能通过 LED 闪烁或者串口输出来猜内部状态。有了调试器,你可以让程序停在任意位置,看看变量对不对,然后单步走几步,再停。
调试器的三大基本能力:
- 断点(Breakpoint):让程序在指定地址停下来。硬件断点数量有限(通常 4-8 个),软件断点则不限数量。
- 单步执行(Step):一次执行一条指令或一行源码,方便观察执行路径。
- 内存/寄存器查看:随时读取或修改任意地址的数据。
我记得第一次用调试器看一个全局变量,发现它莫名其妙被改了。单步跟踪了半小时,才发现是 DMA 写穿了缓冲区。要是没有调试器,这种问题靠打印根本查不出来。
1.2 JTAG 与 SWD 协议
调试器和芯片之间怎么通信?目前主流就两种协议:JTAG 和 SWD。
| 特性 | JTAG | SWD |
|---|---|---|
| 引脚数 | 5 根(TMS, TCK, TDI, TDO, nTRST) | 2 根(SWDIO, SWCLK) |
| 速度 | 最高可达几十 MHz | 与 JTAG 相当,略低 |
| 调试能力 | 完整,支持边界扫描 | 仅调试,不支持边界扫描 |
| 适用场景 | FPGA、复杂 SoC、多核调试 | ARM Cortex-M 系列主流 |
| 硬件占用 | 较多 PCB 走线 | 节省引脚,适合小封装 |
JTAG 全称 Joint Test Action Group,最早是为了 PCB 边界扫描测试设计的。它内部是一个状态机,通过 TMS 和 TCK 两个信号控制状态跳转,TDI 输入数据,TDO 输出数据。说白了就是一条串行移位链路。
SWD 全称 Serial Wire Debug,是 ARM 公司搞出来的简化版。只用两根线:SWDIO(数据线)和 SWCLK(时钟线)。我个人的习惯是,只要芯片支持 SWD,我就用 SWD。省引脚、布线简单,调试速度也够用。
我的经验: 很多开发板上的 JTAG 接口其实只用了 SWD 模式。比如 ST-Link 的 SWD 接口,实际只接了 SWDIO、SWCLK、GND 三根线就能调试。别被 20 针的 JTAG 接口吓到,大部分时候用不到那么多。
1.3 调试环境搭建:OpenOCD + GDB
环境搭建这块,我踩过不少坑。OpenOCD 负责和调试器硬件通信,GDB 负责和开发者交互。两者通过 TCP 端口连接,形成一个完整的调试链路。
整体架构是这样的:
1.3.1 安装 OpenOCD
OpenOCD 的安装方式取决于你的操作系统。我一般在 Linux 下用,直接 apt 安装就行:
# Ubuntu/Debian
sudo apt-get install openocd
# macOS (Homebrew)
brew install open-ocd
# Windows 需要从官网下载预编译包,或者用 MSYS2 安装
安装完后,验证一下:
openocd --version
能看到版本号就说明装好了。
1.3.2 编写 OpenOCD 配置文件
OpenOCD 需要知道你的调试器型号和目标芯片型号。配置文件通常分两部分:接口配置和目标配置。
举个例子,我用 ST-Link 调试 STM32F103:
# stm32f103.cfg
# 接口配置:ST-Link V2
source [find interface/stlink-v2.cfg]
# 目标配置:STM32F1x 系列
source [find target/stm32f1x.cfg]
# 可选:设置 SWD 模式(默认就是 SWD)
transport select swd
# 可选:设置调试时钟频率
adapter speed 1000
注意: 我曾经因为忘记设置 transport select swd,结果 OpenOCD 默认用 JTAG 模式,而我的板子只接了 SWD 引脚,死活连不上。折腾了半小时才发现这个问题。
1.3.3 启动 OpenOCD
写好配置文件后,启动 OpenOCD:
openocd -f stm32f103.cfg
如果一切正常,你会看到类似这样的输出:
Info : ST-LINK V2J29S7 (API v2) VID:PID 0483:3748
Info : Target voltage: 3.3V
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : Listening on port 3333 for gdb connections
看到 Listening on port 3333 就说明 OpenOCD 已经准备好了,正在等待 GDB 连接。
1.3.4 使用 GDB 连接调试
打开另一个终端,启动 arm-none-eabi-gdb(针对 ARM Cortex-M 的 GDB 版本):
arm-none-eabi-gdb your_program.elf
然后在 GDB 内部连接 OpenOCD:
(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue
解释一下这几条命令:
target remote localhost:3333:连接到 OpenOCD 监听的 TCP 端口。monitor reset halt:复位芯片并立即暂停,让程序停在复位向量处。load:将 ELF 文件中的代码和数据烧录到芯片 Flash 中。continue:让程序全速运行。
我的习惯: 每次调试前,我都会先执行 monitor reset halt 再 load。如果不先 halt,芯片正在运行,烧录可能会失败或者导致奇怪的问题。
1.3.5 常用 GDB 调试命令
| 命令 | 作用 | 示例 |
|---|---|---|
break / b |
设置断点 | b main 或 b *0x08000100 |
continue / c |
继续运行 | c |
next / n |
单步跳过(不进入函数) | n |
step / s |
单步进入(进入函数内部) | s |
print / p |
打印变量值 | p counter 或 p *pBuffer |
info registers |
查看所有寄存器 | info registers |
monitor reset halt |
复位并暂停 | monitor reset halt |
load |
烧录程序 | load |
1.4 常见问题与避坑指南
我曾经遇到过的问题:
- 连接不上芯片:检查 SWD 接线,特别是 GND 必须共地。有时候板子供电不足也会导致连接失败。
- OpenOCD 报错 "no device found":大概率是驱动问题。Windows 下需要安装 Zadig 驱动,Linux 下需要 udev 规则。
- 烧录后程序不运行:检查复位引脚是否被拉低,或者芯片的 BOOT 引脚配置是否正确。
- 断点不生效:硬件断点用完了。Cortex-M 通常只有 6 个硬件断点,超过数量会自动降级为软件断点,但软件断点需要修改 Flash,速度慢且有限制。
一个小技巧: 如果你用的是 J-Link 调试器,可以用 J-Link Commander 先测试一下连接是否正常:
JLinkExe -device STM32F103C8 -if SWD -speed 1000 -autoconnect 1
能连上再启动 OpenOCD,这样可以快速排除硬件问题。
1.5 本章小结
调试环境是嵌入式开发的基石。JTAG 和 SWD 是两种主流的调试协议,SWD 更省引脚,适合 ARM Cortex-M 系列。OpenOCD + GDB 的组合是开源社区的标准方案,虽然配置起来有点繁琐,但一旦跑通,调试效率会大幅提升。
嗯,这里要注意:别急着上手复杂的调试技巧,先把环境搭稳。我见过太多人花了一周调 bug,最后发现是调试器没配置对。基础打牢,后面才能走得快。