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 整体架构

先看一张图,把整个调试链路理清楚:

调试链路架构图 主机(PC) GDB 客户端 OpenOCD 调试代理 调试器硬件 J-Link / ST-Link 目标板 STM32 / 其他 各层职责: • GDB:提供调试命令接口(break、step、print 等),解析调试信息 • OpenOCD:将 GDB 的调试命令翻译成 JTAG/SWD 协议,通过调试器硬件发送 • 调试器硬件:物理连接,负责电平转换和时序控制 • 目标板:执行代码,响应调试请求

这条链路很清晰:你在 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 的环境搭建:从安装到连接,一条完整的调试链路

这些东西是后面所有调试技巧的基础。你想想看,如果连调试环境都搭不起来,那后面的性能分析和故障定位就无从谈起。