第二十五讲:嵌入式调试——交叉调试、JTAG/SWD调试、裸机调试技巧

嵌入式调试,说白了就是“在目标板上找bug”。跟PC上开发不一样,你没法直接在芯片上跑IDE。你得通过调试器连上去,远程控制它。这一讲,我就把交叉调试、JTAG/SWD协议、还有裸机调试的实战技巧,一次性讲清楚。

一、交叉调试:主机与目标板的“远程协作”

交叉调试,就是主机(你的电脑)上运行调试器,目标板(单片机/ARM)上运行被调试的程序。两者通过调试接口通信。

核心流程:

  • 主机端:GDB + IDE(如VS Code、IAR、Keil)
  • 目标端:调试代理(如OpenOCD、JLinkGDBServer)
  • 连接方式:JTAG / SWD / 串口 / 以太网

我个人的习惯: 调试ARM Cortex-M系列时,我几乎只用SWD。两根线(SWDIO + SWCLK)就能搞定,省下的IO口还能干别的。JTAG虽然功能全,但线多,在小板子上布线麻烦。

交叉调试的典型命令(以GDB + OpenOCD为例):

# 启动OpenOCD(连接JLink + STM32F4)
openocd -f interface/jlink.cfg -f target/stm32f4x.cfg

# 启动GDB并连接
arm-none-eabi-gdb my_firmware.elf
(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue

这里要注意:monitor reset halt 是让芯片复位后停在第一条指令。如果不做这一步,直接load可能会失败。我刚开始用的时候吃过这个亏,程序跑飞了都不知道。

二、JTAG与SWD:调试接口的“双雄对决”

特性 JTAG SWD
引脚数 5(TCK, TMS, TDI, TDO, nTRST) 2(SWDIO, SWCLK)
速度 最高可达几十MHz 通常4MHz~10MHz
调试能力 全功能(断点、单步、内存读写) 全功能(与JTAG一致)
边界扫描 支持 不支持
适用场景 FPGA、复杂SoC、多核调试 ARM Cortex-M系列、低引脚数MCU

为什么我推荐SWD? 说白了,对于绝大多数ARM Cortex-M项目,SWD完全够用。而且很多调试器(如JLink、ST-Link)都同时支持JTAG和SWD,你只需要在IDE里切换一下模式就行。

避坑指南: 我曾经遇到一个板子,SWD死活连不上。折腾了半天,发现是SWDIO引脚上拉电阻没焊。SWDIO需要外部上拉(通常4.7kΩ到10kΩ),否则信号不稳定。嗯,硬件细节真的不能忽略。

三、裸机调试技巧:没有操作系统,怎么调?

裸机调试,就是芯片上只跑你的代码,没有RTOS。这时候调试手段更“原始”,但也更直接。

3.1 断点与单步执行

硬件断点(Hardware Breakpoint)是ARM Cortex-M内置的。你可以在IDE里设置最多6个断点(具体看芯片)。软件断点(Software Breakpoint)则是在代码里插入BKPT指令。

// 软件断点示例
void debug_breakpoint(void) {
    __asm("BKPT #0");  // 执行到这里会触发调试异常
}

我个人的经验:硬件断点用在关键路径上,比如中断入口、状态机跳转。软件断点用在调试信息输出前,方便观察变量。

3.2 内存与寄存器查看

裸机调试时,最常用的就是看寄存器值和内存内容。比如检查GPIO输出状态:

// 查看GPIOA的ODR寄存器(输出数据寄存器)
(gdb) p/x *(uint32_t*)0x40020014
$1 = 0x00000001  // 只有PA0输出高电平

为什么用p/x?因为十六进制看寄存器更直观。我习惯把关键寄存器地址定义成宏,调试时直接敲宏名,省得记地址。

3.3 串口打印调试

没有操作系统,串口就是你的“printf”。但要注意:串口打印会占用CPU时间,影响实时性。我一般只在调试阶段打开,发布固件时关掉。

// 简易串口打印函数
void debug_printf(const char *fmt, ...) {
    char buf[128];
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
    // 通过UART发送buf
    uart_send_string(buf);
}

注意: 串口打印函数里不要用printf,它太占栈空间。用vsnprintf指定缓冲区大小,更安全。我曾经在中断里用printf,结果栈溢出,程序直接跑飞。嗯,血的教训。

3.4 使用LED和GPIO做“穷人的调试器”

有时候调试器不在手边,或者芯片太小没法接调试线。这时候,LED就是你的好朋友。

// 用LED指示程序运行状态
#define LED_PIN  (1 << 5)  // PA5

void indicate_state(int state) {
    switch(state) {
        case 0: GPIOA->BSRR = LED_PIN; break;  // 亮
        case 1: GPIOA->BRR  = LED_PIN; break;  // 灭
        case 2: // 闪烁
            for(int i=0; i<5; i++) {
                GPIOA->BSRR = LED_PIN;
                delay_ms(100);
                GPIOA->BRR  = LED_PIN;
                delay_ms(100);
            }
            break;
    }
}

你想想看,一个LED闪烁5次代表初始化成功,常亮代表错误,是不是比看串口日志快多了?我在现场调试时经常用这招。

四、SVG知识结构图:嵌入式调试核心逻辑

嵌入式调试核心逻辑 主机(PC) GDB / IDE / 调试器驱动 调试器硬件 JLink / ST-Link / OpenOCD 目标板(MCU) ARM Cortex-M / RISC-V 调试接口:JTAG(5线) / SWD(2线) / 串口 / 以太网 裸机调试常用手段 硬件断点 / 单步 内存 / 寄存器查看 串口打印调试 LED / GPIO指示 核心原则:先硬件后软件,先静态后动态,先简单后复杂

五、实战经验总结

嵌入式调试,说白了就是“跟硬件斗智斗勇”。我总结了几条铁律:

  1. 先确认硬件没问题:电源、时钟、复位、调试接口引脚,这些先拿万用表量一遍。我见过太多“软件bug”最后发现是硬件虚焊。
  2. 调试器连不上?先检查驱动和线序:SWDIO和SWCLK别接反,GND必须共地。我曾经因为杜邦线接触不良,折腾了俩小时。
  3. 裸机调试时,别贪多:一次只调一个功能模块。比如先调GPIO,再调UART,最后调中断。模块化调试,定位问题快得多。
  4. 善用“半主机”模式:ARM的半主机(Semihosting)可以在不占用串口的情况下,把调试信息输出到主机控制台。但注意,它依赖调试器,发布固件时要关掉。

一个小技巧: 调试中断服务程序时,别在中断里设断点。中断频率高的话,断点会频繁触发,系统直接卡死。我一般用GPIO翻转来观察中断是否触发,或者用逻辑分析仪抓波形。

嗯,嵌入式调试就是这样,工具和方法都很成熟,但真正考验的是你的耐心和细心。多练、多踩坑,慢慢就有感觉了。


公众号:蓝海资料掘金营,微信deep3321