1. 课程导入
嵌入式调试,说白了就是让芯片“开口说话”。
我刚开始做嵌入式那会儿,调试全靠串口打印。加一行 printf,烧进去,看输出,再改,再烧……一个 bug 能折腾一整天。后来我学会了用 JTAG 和 GDB,才真正体会到什么叫“掌控感”。
这一章,我们就从最核心的概念讲起。我会把调试器怎么工作的、JTAG 和 SWD 有什么区别、以及怎么搭一套 OpenOCD + GDB 的环境,都给你捋清楚。
1.1 嵌入式调试的核心概念
调试的本质是什么?
是让 CPU 停下来,让你看它的内部状态。你想想看,一个跑着的程序,你没法直接看它的寄存器、内存、堆栈。但如果你能让它“暂停”,那一切就都透明了。
调试的三个基本能力:
- 暂停/恢复执行 —— 让 CPU 在指定位置停下来
- 读写内存和寄存器 —— 查看和修改变量、外设状态
- 单步执行 —— 一条指令一条指令地走,看程序是怎么跑的
这三个能力,是所有调试器的基础。不管是 JTAG 还是 SWD,最终都是干这三件事。
我记得有一次,一个同事调一个 I2C 通信的 bug,死活找不到原因。我让他用调试器挂上去,单步走了一遍 I2C 的中断服务函数,结果发现是状态机里少了一个状态转移。用串口打印?那种时序问题根本抓不到。这就是调试器的价值。
1.2 调试器工作原理:JTAG 与 SWD
调试器怎么跟芯片通信?
靠的是调试接口。目前主流的有两种:JTAG 和 SWD。
1.2.1 JTAG(Joint Test Action Group)
JTAG 最早是用来做芯片测试的,后来被拿来当调试接口用。它用了 5 根线:
| 信号 | 全称 | 作用 |
|---|---|---|
| TCK | Test Clock | 时钟信号,同步所有操作 |
| TMS | Test Mode Select | 控制状态机跳转 |
| TDI | Test Data In | 数据输入 |
| TDO | Test Data Out | 数据输出 |
| TRST(可选) | Test Reset | 复位调试逻辑 |
JTAG 的核心是一个有限状态机。TMS 信号控制状态机的跳转,TDI 和 TDO 负责数据的输入输出。说白了,就是通过这 5 根线,往芯片内部“塞”指令和数据,再把结果“读”出来。
我的经验:JTAG 最大的好处是可以级联多个芯片。如果你板子上有多个 JTAG 设备(比如 CPU + FPGA),可以用一根 JTAG 链把它们串起来。我在一个多核项目里就这么干过,省了不少调试口。
1.2.2 SWD(Serial Wire Debug)
SWD 是 ARM 搞出来的,比 JTAG 更省引脚。它只用 2 根线:
| 信号 | 作用 |
|---|---|
| SWDIO | 双向数据线,既传数据也传控制 |
| SWCLK | 时钟线 |
SWD 的协议比 JTAG 简单。它通过一个数据包格式来传输:先发一个起始位,然后是 8 位数据,最后是奇偶校验。每次传输可以读或写一个寄存器。
为什么 ARM 要搞 SWD?因为引脚太贵了。你想想看,一个 QFN 封装的芯片,总共就 32 个脚,JTAG 占 5 个,太奢侈了。SWD 只要 2 根,省下来的引脚可以给 GPIO 或者外设用。
注意:SWD 只支持 ARM 内核。如果你用的是 RISC-V 或者 MIPS,那还是得用 JTAG。另外,SWD 的速度通常比 JTAG 慢一点,但实际调试中感觉不太出来。
1.2.3 JTAG vs SWD 对比
| 特性 | JTAG | SWD |
|---|---|---|
| 引脚数 | 4-5 根 | 2 根 |
| 协议复杂度 | 复杂(状态机) | 简单(数据包) |
| 芯片支持 | 几乎所有芯片 | 仅 ARM Cortex 系列 |
| 级联能力 | 支持 | 不支持 |
| 调试速度 | 快 | 中等 |
我个人习惯:如果板子空间允许,我会同时引出 JTAG 和 SWD 接口。JTAG 做备用,SWD 日常用。万一 SWD 出了问题,还能切到 JTAG 救急。
1.3 调试环境搭建:OpenOCD + GDB
工具链选好了,接下来就是搭环境。我推荐 OpenOCD + GDB 的组合。这套组合免费、开源、跨平台,而且支持市面上绝大多数调试器和芯片。
1.3.1 整体架构
先看一张图,把整个调试链路理清楚:
这条链路很清晰:你在 GDB 里敲命令,GDB 把命令发给 OpenOCD,OpenOCD 转成 JTAG/SWD 信号,通过调试器硬件送到目标芯片。反过来,芯片的响应也沿着这条路原路返回。
1.3.2 安装 OpenOCD
OpenOCD 的安装方式取决于你的操作系统。
- Linux(Ubuntu/Debian):
sudo apt-get install openocd - macOS:
brew install open-ocd - Windows:
去 OpenOCD 官网 下载预编译包,解压后把openocd.exe所在目录加到 PATH 里。
装完之后,验证一下:
openocd --version
如果能看到版本号,说明装好了。
1.3.3 配置 OpenOCD
OpenOCD 需要两个配置文件:一个是调试器配置,一个是目标芯片配置。
举个例子,我用 ST-Link 调试 STM32F407:
# stlink.cfg - 调试器配置
source [find interface/stlink.cfg]
transport select hla_swd
# stm32f4x.cfg - 目标芯片配置
source [find target/stm32f4x.cfg]
启动 OpenOCD:
openocd -f stlink.cfg -f stm32f4x.cfg
如果一切正常,你会看到类似这样的输出:
Info : Listening on port 3333 for gdb connections
Info : Listening on port 4444 for telnet connections
嗯,这里要注意:端口 3333 是给 GDB 用的,端口 4444 是 telnet 接口,可以用来手动发命令。
避坑指南:我曾经在一个项目里,OpenOCD 怎么都连不上芯片。折腾了半天,发现是调试器的驱动没装好。Windows 下 ST-Link 需要装驱动,Linux 下需要 udev 规则。别小看这一步,很多人卡在这里。
1.3.4 使用 GDB 连接
OpenOCD 跑起来之后,打开另一个终端,启动 GDB:
arm-none-eabi-gdb your_program.elf
然后在 GDB 里连接 OpenOCD:
(gdb) target remote localhost:3333
连接成功后,你就可以开始调试了。常用的命令:
| 命令 | 作用 |
|---|---|
load |
将程序烧录到目标板 |
break main |
在 main 函数处设置断点 |
continue |
继续执行 |
step |
单步进入 |
next |
单步跳过 |
print var |
打印变量值 |
info registers |
查看寄存器 |
monitor reset |
通过 OpenOCD 复位芯片 |
一个典型的调试流程:
(gdb) target remote localhost:3333
(gdb) load
(gdb) break main
(gdb) continue
(gdb) step
(gdb) print counter
$1 = 0
(gdb) next
(gdb) print counter
$2 = 1
看到了吗?每一步都能看到变量的变化。这就是调试器的威力。
1.4 本章小结
这一章我们讲了三个东西:
- 调试的核心概念:暂停、读写、单步
- JTAG 和 SWD 的工作原理:5 根线 vs 2 根线,各有优劣
- OpenOCD + GDB 的环境搭建:从安装到连接,一条完整的调试链路
这些东西是后面所有调试技巧的基础。你想想看,如果连调试环境都搭不起来,那后面的性能分析和故障定位就无从谈起。