1. 课程导入

嵌入式调试,说白了就是让芯片“开口说话”。

我刚开始做嵌入式那会儿,调试全靠串口打印。加一行 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 协议使用数据包的概念。每个操作包含:

  1. 请求阶段(8 位):调试器发送操作类型(读/写)、AP/DP 选择、寄存器地址
  2. 等待阶段(3 位):目标芯片返回 ACK 响应
  3. 数据传输阶段(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 —— 把程序烧写到 Flash
  • continue —— 开始运行

我的调试小技巧:在 GDB 里,用 layout src 可以打开源代码窗口,用 bt 查看调用栈,用 info registers 查看所有寄存器。这些命令我每天要用几十遍。

1.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

嵌入式调试知识体系 调试器(GDB + OpenOCD) JTAG 协议(4-5线) SWD 协议(2线) TAP 状态机 + 边界扫描 数据包协议 + AP/DP 说明: 调试器(GDB + OpenOCD)通过 JTAG 或 SWD 协议与目标芯片通信。 JTAG 使用 TAP 状态机,适合多芯片链式调试;SWD 使用数据包协议,引脚少、速度快。 实际项目中,90% 的 ARM MCU 调试都采用 SWD 协议。

1.6 常见问题与避坑

问题 原因 解决方法
OpenOCD 提示 "no device found" 驱动未安装或接线错误 检查调试器驱动,确认 SWDIO/SWCLK 连接正确
GDB 连接后无法 halt 芯片处于低功耗模式或看门狗在跑 先按住复位,再发送 halt 命令
烧写后程序不运行 复位向量表错误或启动文件缺失 检查链接脚本,确认入口地址正确
调试器频繁断开 线缆过长或干扰大 SWD 线尽量短于 20cm,降低 SWCLK 频率

我曾经踩过的坑:有一次调试一块 nRF52840 板子,死活连不上。折腾了两个小时,最后发现是 SWD 线上的 10kΩ 上拉电阻焊错了位置。嗯,硬件问题往往比软件问题更难排查。