1. 课程导入

各位同学,咱们正式开始嵌入式调试的第一课。说实话,调试这件事,我做了十几年,到现在每次拿到一块新板子,第一件事还是先确认调试链路通不通。这就像开车前先系安全带——看似基础,但真能救命。

今天这一章,咱们把三件事讲透:调试器是怎么工作的、怎么搭一套趁手的调试工具链、以及怎么让板子上的LED听你使唤。

1.1 调试器原理:JTAG 与 SWD

先问一个问题:芯片跑起来了,你怎么知道它内部在干什么?

答案是——你得有一条「后门」。JTAG和SWD就是这条后门。

JTAG(Joint Test Action Group)

JTAG最早是用来做PCB板级测试的。说白了,就是通过几个引脚,把芯片内部的状态「读」出来。后来大家发现,这玩意儿不仅能测板子,还能用来调试程序。

JTAG接口最少需要5根线:

信号 全称 作用
TCK Test Clock 时钟信号,同步所有操作
TMS Test Mode Select 控制状态机跳转
TDI Test Data In 数据输入到芯片
TDO Test Data Out 数据从芯片输出
TRST(可选) Test Reset 复位调试逻辑

JTAG内部有一个状态机,通过TMS引脚的电平组合,可以在16种状态之间切换。每次切换,要么往芯片里塞一位数据(TDI),要么从芯片里读一位数据(TDO)。

关键理解:JTAG本质上是一个串行移位寄存器。你把指令和数据一位一位塞进去,芯片执行完,再把结果一位一位吐出来。速度不快,但胜在稳定、通用。

SWD(Serial Wire Debug)

SWD是ARM公司搞出来的「轻量版」调试接口。它只用了两根线:

  • SWDIO:双向数据线
  • SWCLK:时钟线

我刚开始用SWD的时候,心里也犯嘀咕:两根线能干啥?后来发现,SWD通过数据包协议来传输信息,效率反而比JTAG高。尤其是在PCB空间紧张的项目里,省下来的三个引脚,够你加一个传感器了。

对比项 JTAG SWD
引脚数 4~5根 2根
速度 较慢(串行移位) 较快(数据包协议)
通用性 几乎所有芯片都支持 ARM Cortex-M系列为主
调试能力 完整(支持边界扫描) 完整(不支持边界扫描)
适用场景 板级测试、FPGA、复杂SoC 嵌入式MCU调试

我的建议:做ARM Cortex-M系列开发,优先用SWD。省引脚、速度快。除非你需要做边界扫描测试,否则JTAG的优势在嵌入式调试里体现不出来。

1.2 调试工具链搭建:OpenOCD + GDB

有了调试接口,还得有软件来「说话」。我最常用的组合是OpenOCD + GDB。这套组合拳,免费、开源、跨平台,而且几乎支持市面上所有的调试器硬件。

OpenOCD 是什么?

OpenOCD(Open On-Chip Debugger)是一个开源的调试守护进程。它负责两件事:

  1. 跟调试器硬件通信(比如J-Link、ST-Link、CMSIS-DAP)
  2. 把GDB的调试命令翻译成JTAG/SWD协议

说白了,OpenOCD就是GDB和硬件之间的「翻译官」。

安装与配置

以Ubuntu为例,安装很简单:

sudo apt-get update
sudo apt-get install openocd gdb-multiarch

然后准备一个配置文件。我习惯把配置分成两部分:调试器配置和目标芯片配置。

举个例子,用ST-Link调试STM32F103:

# stm32f103.cfg
source [find interface/stlink.cfg]
source [find target/stm32f1x.cfg]

# 设置SWD模式
transport select swd

# 设置调试时钟频率
adapter speed 1000

注意:调试时钟频率不要设太高。我曾经在一个项目里把adapter speed设到4MHz,结果调试器时不时断开连接。后来降到1MHz,稳如老狗。原因很简单——线缆太长、信号质量不好,高频就容易丢包。

启动调试会话

先启动OpenOCD:

openocd -f stm32f103.cfg

看到类似这样的输出,说明连接成功:

Info : Listening on port 3333 for gdb connections
Info : target state: halted

然后另开一个终端,启动GDB:

gdb-multiarch your_program.elf
(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue

这几条命令的意思:

  • target remote:连接到OpenOCD开的3333端口
  • monitor reset halt:复位芯片并暂停
  • load:把程序烧进去
  • continue:让程序跑起来

避坑指南:我曾经遇到过一个问题——load之后程序跑不起来。折腾了半天,发现是monitor reset halt之后,芯片停在复位向量表,但load会修改向量表内容。正确的做法是:load之后先monitor reset halt再continue,或者用monitor reset init让芯片重新初始化。

1.3 第一个点灯调试实验

理论讲完了,咱们动手。点灯是嵌入式界的「Hello World」,但咱们今天不只是让灯亮,而是用调试器看着它亮。

硬件准备

  • 一块STM32F103开发板(或者任何带MCU的板子)
  • 一个LED(板载的也行)
  • 一个调试器(ST-Link、J-Link都行)

代码

#include "stm32f1xx.h"

void delay(volatile uint32_t count) {
    while(count--);
}

int main(void) {
    // 使能GPIOC时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    // 配置PC13为推挽输出
    GPIOC->CRH &= ~GPIO_CRH_CNF13;
    GPIOC->CRH |= GPIO_CRH_MODE13_0;

    while(1) {
        GPIOC->BSRR = GPIO_BSRR_BR13;  // 低电平,LED亮
        delay(500000);
        GPIOC->BSRR = GPIO_BSRR_BS13;  // 高电平,LED灭
        delay(500000);
    }
}

调试步骤

  1. 编译:arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -nostartfiles -T stm32f103.ld led.c -o led.elf
  2. 启动OpenOCD:openocd -f stm32f103.cfg
  3. 启动GDB:gdb-multiarch led.elf
  4. 连接并烧录:target remote localhost:3333 → monitor reset halt → load
  5. 设置断点:break main
  6. 运行:continue

当程序停在main函数入口时,你可以单步执行(stepi或next),观察LED的状态变化。也可以查看寄存器的值:

(gdb) print/x GPIOC->ODR
$1 = 0xffffffff  // 所有引脚高电平,LED灭
(gdb) next
(gdb) print/x GPIOC->ODR
$2 = 0xffffdfff  // PC13低电平,LED亮

一个小技巧:在GDB里可以用display命令让某个变量或寄存器在每次暂停时自动显示。比如display/x GPIOC->ODR,这样你单步的时候就不用每次都手动打印了。

1.4 本章知识体系

下面这张图,把咱们今天讲的内容串起来了:

嵌入式调试基础:知识体系 调试接口 JTAG(4~5线) SWD(2线) 调试工具链 OpenOCD(翻译官) GDB(调试前端) 断点、单步、寄存器查看 调试器硬件 (J-Link/ST-Link等) 连接PC与目标板

从这张图可以看得很清楚:调试接口是物理层,工具链是软件层,调试操作是应用层。三层缺一不可。

总结一下:今天咱们干了三件事——搞懂了JTAG和SWD的区别、搭好了OpenOCD+GDB的环境、亲手用调试器点了个灯。这些基础打牢了,后面讲性能调优和功耗优化的时候,你才能「看得见」芯片内部发生了什么。