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 端口连接,形成一个完整的调试链路。

整体架构是这样的:

调试环境架构图 GDB 客户端 (命令行/IDE) OpenOCD (中间层) 调试器硬件 (J-Link/ST-Link) 目标芯片 (STM32/EFM32 等) TCP:3333 USB/驱动 SWD/JTAG 说明: GDB 通过 TCP 端口 3333 连接 OpenOCD,OpenOCD 通过 USB 驱动控制调试器硬件, 调试器硬件通过 SWD/JTAG 协议与目标芯片通信。整个链路是:GDB ↔ OpenOCD ↔ 调试器 ↔ 芯片。

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,最后发现是调试器没配置对。基础打牢,后面才能走得快。