第三章:硬件调试基础——用JTAG/SWD把芯片“聊”起来

各位同学,咱们今天聊点实在的。你写好了代码,焊好了板子,上电一看——啥反应没有。这时候怎么办?拿万用表戳?拿示波器看?当然可以,但最直接的办法,是把调试器接上去,跟芯片“对话”。

JTAG和SWD,就是干这个的。我个人习惯把调试器比作“芯片的听诊器”。你想想看,医生听心跳,我们听寄存器状态。今天这一章,我就带你亲手把听诊器接上,听听芯片到底在干嘛。

3.1 JTAG与SWD:两种主流调试接口

先说JTAG。这玩意儿诞生得早,IEEE 1149.1标准,最初是用来做边界扫描测试的。说白了,就是通过几个引脚,把芯片内部所有的引脚状态都“扫”出来。后来大家发现,哎,这玩意儿还能用来调试CPU,于是就成了嵌入式调试的标配。

JTAG标准接口是5根线:

信号 全称 作用
TMS Test Mode Select 模式选择,控制状态机跳转
TCK Test Clock 时钟信号,所有操作同步于此
TDI Test Data In 数据输入,往芯片里写数据
TDO Test Data Out 数据输出,从芯片里读数据
TRST(可选) Test Reset 复位调试逻辑,不是必须的

SWD呢,是ARM搞出来的简化版。只用两根线:SWDIO(数据线)和SWCLK(时钟线)。你想想看,5根线变2根,省引脚啊!现在很多Cortex-M系列的芯片,都只引出了SWD接口。

我的建议:如果板子空间允许,尽量把JTAG和SWD都引出来。我在一个项目里吃过亏——板子做小了只留了SWD,结果调试器死活连不上,最后发现是SWCLK信号质量太差。要是当时有JTAG的TMS/TDI冗余,排查起来会快很多。

3.2 连接目标板:别让物理层坑了你

连接调试器,听起来简单——插上不就完了?嗯,这里要注意,很多坑就出在“插上”这一步。

首先,电压匹配。你的调试器是3.3V还是5V?目标芯片又是多少伏?我见过有人把5V的调试器怼到1.8V的芯片上,结果“噗”一声,芯片冒烟了。所以,连接前一定确认:

  • 调试器的Vref引脚,必须接目标板的参考电压
  • 如果调试器不支持自适应电压,中间加电平转换芯片
  • GND必须共地,而且尽量用粗线

其次,线长问题。SWD的时钟频率通常能跑到10MHz以上,但如果你用了一根30厘米的杜邦线,信号反射会让你怀疑人生。我建议:

  • SWD线控制在10厘米以内
  • 如果必须用长线,降低SWCLK频率到1MHz以下
  • SWDIO和SWCLK不要平行走太长,减少串扰

警告:千万不要在目标板带电的情况下插拔调试器!虽然很多调试器号称支持热插拔,但我曾经在一次热插拔中烧掉了JTAG的TDO引脚。正确的顺序是:先连接调试器到PC,再连接调试器到目标板,最后给目标板上电。

3.3 读取芯片ID:确认你连对了“人”

连接成功后,第一件事是什么?不是跑程序,而是读芯片ID。这就像你打电话,先确认对方是不是你要找的人。

对于ARM芯片,读ID的流程大致是这样:

// 以Cortex-M4为例,通过SWD读取IDCODE
// 假设你已经初始化了SWD接口

uint32_t read_idcode() {
    uint32_t idcode = 0;
    
    // 1. 发送SWD请求:读IDCODE寄存器(地址0x00)
    // 2. 等待ACK响应
    // 3. 读取32位数据
    
    // 伪代码示意
    swd_request(AP_ACC, READ, 0x00);
    idcode = swd_read_data();
    
    return idcode;
}

// 常见的IDCODE值
// STM32F103: 0x1BA01477
// STM32F407: 0x2BA01477
// NXP LPC1768: 0x4BA00477

读出来的IDCODE,包含了芯片的厂商ID、型号、版本等信息。比如0x1BA01477,拆开来看:

  • Bit[31:28]:版本号,0x1表示Rev 1
  • Bit[27:12]:芯片型号,0xBA014表示STM32F103
  • Bit[11:1]:厂商ID,0x077是STMicroelectronics
  • Bit[0]:固定为1

我记得有一次,客户说板子上的芯片是STM32F407,但我读出来IDCODE是0x1BA01477。嗯?这不是F103吗?后来一查,是采购搞错了批次,焊错了芯片。要是没读ID这步,后面调试三天三夜都找不出问题。

3.4 验证时钟与复位信号:芯片的“心跳”和“重启键”

读到了芯片ID,说明SWD通信没问题。但芯片能不能正常工作,还得看时钟和复位。

时钟验证:

时钟是芯片的心跳。没时钟,CPU不跑,外设不动。怎么验证?

  • 用示波器看晶振引脚——最直接,但需要物理接触
  • 通过调试器读RCC寄存器——更优雅,不需要额外设备

以STM32为例,读RCC_CR寄存器(时钟控制寄存器):

// 通过调试器读取RCC_CR寄存器(地址0x40023800)
uint32_t rcc_cr = *(volatile uint32_t*)0x40023800;

// 检查HSI(内部高速时钟)是否就绪
if (rcc_cr & (1 << 1)) {
    // HSI Ready,位1为1表示HSI稳定
    printf("HSI时钟已就绪\n");
}

// 检查HSE(外部高速时钟)是否就绪
if (rcc_cr & (1 << 17)) {
    // HSE Ready,位17为1表示HSE稳定
    printf("HSE时钟已就绪\n");
}

// 检查PLL是否就绪
if (rcc_cr & (1 << 25)) {
    // PLL Ready,位25为1表示PLL锁定
    printf("PLL已锁定\n");
}

我习惯先看HSI。如果HSI都没起来,说明芯片内部振荡器有问题,可能是芯片坏了或者供电不足。如果HSI正常但HSE没起来,那问题多半在外部晶振——起振电容不对?晶振虚焊?或者负载电容匹配错了?

小技巧:有时候HSE起振慢,尤其是低温环境下。我曾在-40℃的测试中,HSE要等200ms才稳定。所以代码里不要只等一次,最好加超时重试机制。

复位信号验证:

复位信号,说白了就是芯片的“重启键”。复位不正常,芯片可能卡在某个奇怪的状态。

常见的复位源有:

  • 外部复位(NRST引脚拉低)
  • 上电复位(POR)
  • 看门狗复位
  • 软件复位
  • 低电压检测复位(BOR)

通过调试器读RCC_CSR寄存器(控制/状态寄存器),可以知道上次复位是谁干的:

// 读取RCC_CSR寄存器(地址0x40023874)
uint32_t rcc_csr = *(volatile uint32_t*)0x40023874;

// 检查复位标志
if (rcc_csr & (1 << 26)) {
    printf("上次复位:低电压复位(BOR)\n");
}
if (rcc_csr & (1 << 25)) {
    printf("上次复位:外部引脚复位\n");
}
if (rcc_csr & (1 << 24)) {
    printf("上次复位:看门狗复位\n");
}
if (rcc_csr & (1 << 23)) {
    printf("上次复位:软件复位\n");
}
if (rcc_csr & (1 << 22)) {
    printf("上次复位:上电复位\n");
}

我曾经遇到一个诡异的问题:芯片每隔几分钟就自动重启。读CSR寄存器发现是看门狗复位。但代码里明明没开看门狗啊?后来查了半天,发现是PCB布局问题——NRST引脚旁边走了一根高频信号线,串扰导致NRST被周期性拉低。嗯,硬件工程师当时脸都绿了。

3.5 实战:一个完整的调试连接流程

好了,理论说完了,咱们走一遍完整的流程。假设你手里有一块STM32F407的开发板,一个J-Link调试器:

  1. 硬件连接:J-Link的SWD接口接开发板的SWDIO、SWCLK、GND、Vref(3.3V)。线长控制在10cm以内。
  2. 上电:先连调试器到PC(USB),再连调试器到开发板,最后给开发板上电。
  3. 打开调试软件:我用的是J-Link Commander,命令行输入connect
  4. 选择芯片型号:输入STM32F407VG,或者直接输?让软件自动识别。
  5. 选择接口:选SWD,速度先设1MHz(保险起见)。
  6. 读IDCODE:输入mem32 0xE000ED00 1,读CPUID寄存器。正常应该返回0x410FC241(Cortex-M4 r0p1)。
  7. 读时钟状态:输入mem32 0x40023800 1,看RCC_CR。Bit1和Bit17应该为1。
  8. 读复位源:输入mem32 0x40023874 1,看RCC_CSR。确认复位源正常。

如果以上都正常,恭喜你,硬件调试环境搭建成功。接下来就可以下载程序、单步调试了。

核心要点回顾:

  • JTAG 5线,SWD 2线,优先选SWD省引脚
  • 连接前确认电压匹配,连接时先PC再板子上电
  • 读芯片ID是第一步,确认连对了芯片
  • 时钟验证看RCC_CR寄存器,复位验证看RCC_CSR寄存器
  • 遇到问题先查物理连接,再查寄存器状态

下一章,咱们聊聊怎么用调试器单步执行代码,看变量值,以及——怎么在程序跑飞的时候把它抓回来。到时候见。