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。
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 选项卡:
- 选择 "J-LINK / J-TRACE Cortex"
- 点 Settings,确认 SWD 模式,速度选 4 MHz(别一上来就拉满,容易出问题)
- Flash Download 里勾选 "Reset and Run"
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 个 | 监控变量读写,不暂停执行 |
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
*(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 窗口来检查:
- 数组是否越界(看数组前后的内存有没有被意外修改)
- 堆栈使用情况(看栈顶附近有没有被踩)
- 外设寄存器配置(直接读寄存器地址)
4.7 实战流程:一个完整的调试案例
假设你有一个 UART 发送函数,但数据发不出来。我会这样排查:
- 在 UART 发送函数入口设断点
- 全速运行,看程序是否停到断点
- 如果没停,说明根本没调用这个函数——检查调用逻辑
- 如果停了,单步走,看寄存器配置是否正确
- 在 Watch 窗口添加 UART 状态寄存器,看 TX 标志位是否置位
- 在 Memory 窗口看 UART 数据寄存器,确认写入的数据是否正确
这套流程走下来,90% 的通信问题都能定位。
4.8 知识体系:J-Link 调试核心逻辑
下面这张图总结了 J-Link 调试的核心流程和工具链关系:
4.9 常见问题与避坑
- 连接不上芯片:先检查接线,SWDIO 和 SWCLK 有没有接反。然后看目标板有没有供电。最后试降低 SWD 速度到 100 kHz。
- 断点不生效:确认断点类型。如果是软件断点,检查目标 RAM 是否可写。如果是硬件断点,检查是否用完了。
- Watch 窗口显示 "not in scope":说明当前程序执行位置不在该变量的作用域内。单步走到变量所在函数里再看。
- Memory 窗口数据不对:检查地址是否正确。ARM 是 32 位地址空间,0x20000000 是 SRAM,0x40000000 是外设,0x08000000 是 Flash。