4. JTAG/SWD调试器:使用J-Link进行单步调试、断点设置、变量监视与内存查看

调试器这东西,说白了就是嵌入式工程师的「听诊器」。板子跑飞了、变量值不对、外设没反应——这时候你需要的不是猜,而是亲眼看看芯片内部到底在干什么。J-Link 是我用得最多的调试器,稳定、快、支持芯片多。今天我就带你把它用透。

4.1 JTAG 与 SWD:两种调试接口,怎么选?

先搞清楚物理层。JTAG 是 5 线(TMS、TCK、TDI、TDO、nTRST),SWD 是 2 线(SWDIO、SWCLK)。SWD 是 ARM 在 JTAG 基础上精简出来的,保留了全部调试功能,但引脚少了一半。

对比项 JTAG SWD
信号线数 5 根(最少) 2 根
调试速度 最高(可达 50 MHz) 略低于 JTAG
边界扫描 支持 不支持
引脚占用 多 少
适用场景 FPGA、多核、边界扫描测试 ARM Cortex-M 系列

我个人习惯:做 ARM Cortex-M 项目,一律用 SWD。省下来的 3 个 GPIO 可以干别的。只有调试 FPGA 或者做边界扫描测试时,才切回 JTAG。

小技巧:J-Link 的 9 针 SWD 接口定义(从左上角开始):1-VCC、2-SWDIO、3-GND、4-SWCLK、5-GND、6-GND、7-GND、8-GND、9-GND。很多开发板直接引出这 9 针,接上就能用。

4.2 环境搭建:J-Link 驱动与 IDE 配置

拿到 J-Link 后,第一步是装驱动。去 Segger 官网下载 J-Link Software Pack,里面包含驱动、J-Flash、J-Link Commander 等工具。装完后插上 J-Link,设备管理器里应该能看到 "J-Link driver"。

IDE 这边,我以 Keil MDK 为例。点开 Project → Options for Target → Debug 选项卡:

  1. 选择 "J-LINK / J-TRACE Cortex"
  2. 点 Settings,确认 SWD 模式,速度选 4 MHz(别一上来就拉满,容易出问题)
  3. Flash Download 里勾选 "Reset and Run"
注意:如果你用的是盗版 J-Link,Segger 会定期封杀。我曾经遇到过调试到一半,J-Link 突然被锁,整块板子没法下载程序。后来换了正版,再没出过这种幺蛾子。建议团队统一采购正版,省心。

4.3 单步调试:逐行看代码怎么跑

单步调试是基本功。你按下 F11(Step Into),CPU 执行一行 C 代码,然后停下来等你。这时候你可以看变量、看寄存器、看外设状态。

Keil 里常用的几个快捷键:

  • F5:全速运行
  • F11:Step Into(进入函数内部)
  • F10:Step Over(跳过函数,不进去)
  • Ctrl+F11:Step Out(跳出当前函数)
  • Ctrl+B:断点窗口

我刚开始做调试时,总喜欢 F11 一路按到底。后来发现这样效率很低——你其实不需要看每一行,只需要关注关键路径。我的习惯是:先设断点,再全速跑到断点处,然后单步走关键代码段。

4.4 断点设置:硬件断点 vs 软件断点

断点不是无限的。ARM Cortex-M 内核通常只有 6 个硬件断点(Flash 中)和 4 个硬件观察点(RAM 中)。超过这个数,调试器会自动用软件断点替代。

断点类型 位置 数量限制 特点
硬件断点 Flash / ROM 6 个(Cortex-M) 不修改代码,不依赖 RAM
软件断点 RAM 无限制 修改指令为 BKPT,依赖 RAM
硬件观察点 RAM 4 个 监控变量读写,不暂停执行
避坑指南:我曾经在调试 Bootloader 时,在 Flash 里设了 8 个断点,结果第 7 个死活不生效。查了半天才发现硬件断点用完了,调试器自动切到软件断点,但 Bootloader 的 Flash 区域是只读的,软件断点写不进去。从那以后,我每次设断点前都会看一眼断点窗口,确认类型。

4.5 变量监视:Watch 窗口的正确用法

Watch 窗口不是摆设。你可以在里面添加变量,实时看它的值变化。但要注意几点:

  • 局部变量只在函数作用域内可见,出了函数就变成 "not in scope"
  • 静态变量和全局变量全程可见
  • 数组和结构体可以展开看每个成员

我常用的操作:右键变量 → "Add to Watch"。然后右键 Watch 窗口里的变量,可以改显示格式(十进制、十六进制、二进制、浮点数)。

举个例子,你有一个 uint32_t 变量,想看它的每一位:

uint32_t status_reg = 0xA5A500FF;
// 在 Watch 窗口里,右键 → Modify Value → 改成 0xA5A500FF
// 然后右键 → Display As → Binary
// 就能看到:1010 0101 1010 0101 0000 0000 1111 1111
小技巧:Watch 窗口支持表达式。你可以直接输入 *(uint32_t*)0x40020000 来查看某个绝对地址的值,不需要先定义变量。这在调试外设寄存器时特别有用。

4.6 内存查看:Memory 窗口的实战技巧

Memory 窗口能让你直接看芯片的物理内存。打开方式:View → Memory Windows → Memory 1。然后在 Address 栏输入地址,比如 0x20000000(SRAM 起始地址)。

你可以选显示格式:

  • 8-bit Hex:每个字节显示为 2 位十六进制
  • 16-bit Hex:每 2 个字节显示为 4 位十六进制
  • 32-bit Hex:每 4 个字节显示为 8 位十六进制
  • ASCII:显示对应的 ASCII 字符

我经常用 Memory 窗口来检查:

  • 数组是否越界(看数组前后的内存有没有被意外修改)
  • 堆栈使用情况(看栈顶附近有没有被踩)
  • 外设寄存器配置(直接读寄存器地址)
注意:Memory 窗口是实时刷新的。如果你在调试时修改了内存值,程序继续运行后可能会崩溃。我曾经手贱改了一个指针的值,结果程序直接 HardFault。所以,改内存前先想清楚后果。

4.7 实战流程:一个完整的调试案例

假设你有一个 UART 发送函数,但数据发不出来。我会这样排查:

  1. 在 UART 发送函数入口设断点
  2. 全速运行,看程序是否停到断点
  3. 如果没停,说明根本没调用这个函数——检查调用逻辑
  4. 如果停了,单步走,看寄存器配置是否正确
  5. 在 Watch 窗口添加 UART 状态寄存器,看 TX 标志位是否置位
  6. 在 Memory 窗口看 UART 数据寄存器,确认写入的数据是否正确

这套流程走下来,90% 的通信问题都能定位。

4.8 知识体系:J-Link 调试核心逻辑

下面这张图总结了 J-Link 调试的核心流程和工具链关系:

J-Link 调试核心逻辑 J-Link 调试器 SWD / JTAG 接口 IDE(Keil / IAR / VS Code) 单步调试 Step Into / Over / Out 断点设置 硬件 / 软件 / 观察点 变量 & 内存 Watch / Memory 窗口 目标芯片(Cortex-M) Flash / SRAM / 外设寄存器 核心思路:调试器通过 SWD/JTAG 连接芯片,IDE 提供人机界面,三者协同完成调试

4.9 常见问题与避坑

  • 连接不上芯片:先检查接线,SWDIO 和 SWCLK 有没有接反。然后看目标板有没有供电。最后试降低 SWD 速度到 100 kHz。
  • 断点不生效:确认断点类型。如果是软件断点,检查目标 RAM 是否可写。如果是硬件断点,检查是否用完了。
  • Watch 窗口显示 "not in scope":说明当前程序执行位置不在该变量的作用域内。单步走到变量所在函数里再看。
  • Memory 窗口数据不对:检查地址是否正确。ARM 是 32 位地址空间,0x20000000 是 SRAM,0x40000000 是外设,0x08000000 是 Flash。
最后说一句:调试器是工具,不是目的。我见过有人花一整天单步调试一个简单的逻辑错误,其实看一眼代码就能发现。我的建议是:先静态分析,再动态调试。代码走读解决不了的问题,再上调试器。这样效率最高。