1. 嵌入式调试基础:调试器原理、JTAG/SWD协议、调试环境搭建
嵌入式调试,说白了就是让芯片“开口说话”。
我刚开始做嵌入式那会儿,调试全靠串口打印。加一行 printf,烧进去,看输出,再改,再烧……一个 bug 能折腾一整天。后来用了调试器,才明白什么叫“降维打击”。
这一章,我们就从最底层开始,把调试这件事彻底讲透。
1.1 调试器到底在干什么?
调试器不是魔法,它本质上是一个内存和寄存器的读写工具。你想想看,CPU 执行程序,无非就是取指令、读写内存、操作寄存器。调试器能让你:
- 暂停 CPU —— 让程序停在某一行
- 查看/修改内存 —— 看看变量到底是多少
- 查看/修改寄存器 —— 包括 PC(程序计数器)、SP(堆栈指针)
- 单步执行 —— 一条指令一条指令地走
- 设置断点 —— 硬件断点或软件断点
核心概念:调试器通过一个专用的调试接口(JTAG 或 SWD)与目标芯片通信。这个接口独立于程序的主运行逻辑,所以即使程序跑飞了,调试器仍然能控制 CPU。
我在项目中遇到过最典型的情况:程序跑着跑着就进 HardFault 了。没有调试器,你只能猜。有了调试器,直接暂停,看 PC 指向哪里,再看 LR(链接寄存器)回溯调用栈,问题一目了然。
1.2 JTAG 协议:老牌标准
JTAG(Joint Test Action Group)最初是为了测试电路板焊点而设计的,后来被“借用”来做芯片调试。它定义了 5 个信号:
| 信号 | 方向 | 作用 |
|---|---|---|
| TMS | 调试器 → 芯片 | 模式选择,控制 TAP 状态机 |
| TCK | 调试器 → 芯片 | 时钟信号,所有操作同步于此 |
| TDI | 调试器 → 芯片 | 数据输入 |
| TDO | 芯片 → 调试器 | 数据输出 |
| nTRST(可选) | 调试器 → 芯片 | 复位 TAP 控制器 |
JTAG 的核心是一个有限状态机,叫做 TAP(Test Access Port)控制器。每次在 TCK 的上升沿,TMS 的电平决定状态机的下一步走向。说白了,就是通过 TMS 信号“指挥”状态机进入不同的状态,然后通过 TDI/TDO 读写数据。
我的经验:JTAG 占用 4-5 个引脚,对于引脚紧张的芯片来说有点奢侈。而且 JTAG 链可以串联多个芯片,适合板级测试。但如果你只是调试一个 MCU,SWD 更香。
1.3 SWD 协议:ARM 的轻量级方案
SWD(Serial Wire Debug)是 ARM 公司推出的调试协议,只用了 2 个信号:
- SWDIO —— 双向数据线
- SWCLK —— 时钟线
别小看这两根线。SWD 在功能上几乎完全覆盖了 JTAG,而且速度更快(最高可达 50MHz 以上)。它是怎么做到的?
SWD 协议使用数据包的概念。每个操作包含:
- 请求阶段(8 位):调试器发送操作类型(读/写)、AP/DP 选择、寄存器地址
- 等待阶段(3 位):目标芯片返回 ACK 响应
- 数据传输阶段(33 位):1 位奇偶校验 + 32 位数据
我个人的习惯是:能用 SWD 就用 SWD。省下来的两个引脚,可以留给 GPIO 或者传感器。只有在需要调试 FPGA 或者多核系统时,我才会切回 JTAG。
注意:有些芯片的 SWD 引脚和 GPIO 是复用的。如果你在代码里把 SWDIO 配置成了普通 GPIO,调试器就再也连不上了。我曾经踩过这个坑,最后只能按住复位键抢时间擦除 Flash……
1.4 调试环境搭建:OpenOCD + GDB
理论说完了,我们来点实际的。搭建一个调试环境,我推荐 OpenOCD + GDB 的组合。开源、免费、跨平台,而且几乎支持所有主流调试器(J-Link、ST-Link、CMSIS-DAP 等)。
1.4.1 安装 OpenOCD
在 Ubuntu 上,直接:
sudo apt-get install openocd
Windows 用户可以去官网下载预编译包,或者用 msys2 安装。macOS 用户:
brew install openocd
1.4.2 准备配置文件
OpenOCD 需要两个配置文件:
- 接口配置文件:描述调试器型号(比如 ST-Link、J-Link)
- 目标配置文件:描述目标芯片(比如 STM32F103、nRF52840)
举个例子,用 ST-Link 调试 STM32F103:
# interface/stlink.cfg
source [find interface/stlink.cfg]
transport select hla_swd
# target/stm32f1x.cfg
source [find target/stm32f1x.cfg]
然后启动 OpenOCD:
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg
看到 Info : Listening on port 3333 for gdb connections,就说明成功了。
1.4.3 连接 GDB
打开另一个终端,启动 arm-none-eabi-gdb:
arm-none-eabi-gdb your_program.elf
(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue
这几条命令的意思是:
target remote—— 连接到 OpenOCD 的 GDB 服务器monitor reset halt—— 复位芯片并暂停load—— 把程序烧写到 Flashcontinue—— 开始运行
我的调试小技巧:在 GDB 里,用 layout src 可以打开源代码窗口,用 bt 查看调用栈,用 info registers 查看所有寄存器。这些命令我每天要用几十遍。
1.5 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
1.6 常见问题与避坑
| 问题 | 原因 | 解决方法 |
|---|---|---|
| OpenOCD 提示 "no device found" | 驱动未安装或接线错误 | 检查调试器驱动,确认 SWDIO/SWCLK 连接正确 |
| GDB 连接后无法 halt | 芯片处于低功耗模式或看门狗在跑 | 先按住复位,再发送 halt 命令 |
| 烧写后程序不运行 | 复位向量表错误或启动文件缺失 | 检查链接脚本,确认入口地址正确 |
| 调试器频繁断开 | 线缆过长或干扰大 | SWD 线尽量短于 20cm,降低 SWCLK 频率 |
我曾经踩过的坑:有一次调试一块 nRF52840 板子,死活连不上。折腾了两个小时,最后发现是 SWD 线上的 10kΩ 上拉电阻焊错了位置。嗯,硬件问题往往比软件问题更难排查。