1. 调试基础概念:什么是嵌入式调试、调试与测试的区别、常见调试手段概览

1.1 到底什么是嵌入式调试?

嵌入式调试,说白了就是让代码“开口说话”。

你写了一段程序,烧进芯片里,它跑起来了。然后呢?它真的按你预想的跑了吗?大概率没有。我做了十几年嵌入式,见过太多“我以为它跑对了”的案例。结果一上逻辑分析仪,发现中断进了不该进的地方,或者某个全局变量被莫名其妙改了。

调试,就是你在代码运行的过程中,去观察、干预、验证它的行为。它不是写代码,也不是跑测试,而是“在真实或接近真实的硬件环境下,找出代码为什么没按预期工作”的过程。

核心定义:嵌入式调试 = 在目标硬件上,通过工具手段获取运行时信息,定位并修复软件/硬件故障的过程。

嗯,这里要注意一个点:调试不是只在出问题时才做。我个人习惯在开发阶段就频繁调试——每写完一个模块,先跑起来看看寄存器对不对、内存有没有溢出。等所有代码堆在一起再调,那叫“救火”。

1.2 调试 vs 测试:它们到底有什么区别?

很多新人会把调试和测试混为一谈。我刚开始带团队时,有个同事跟我说:“我测过了,没问题。”结果产品一上线就死机。后来我问他怎么测的,他说“编译没报错,下载进去能跑”。

这其实是把测试当成了调试,或者更准确地说,他根本没做调试。

维度 测试(Testing) 调试(Debugging)
目的 发现缺陷是否存在 定位并修复已知缺陷
时机 开发完成后、版本发布前 开发过程中、测试发现问题后
手段 自动化用例、手动操作、压力测试 断点、单步、Trace、日志、示波器
输出 通过/失败报告 根因分析、修复方案
环境 可重复、可脚本化 往往需要实时交互、硬件介入

你想想看,测试告诉你“这里有问题”,但没告诉你“为什么有问题”。调试就是回答那个“为什么”的。我曾经遇到一个案子:产品在低温下偶尔重启,测试报告写了三页,全是“低温测试失败”。但真正的原因呢?是一个GPIO上拉电阻的时序在低温下漂移了——这个只有靠调试才能挖出来。

我的经验:测试是“筛子”,调试是“镊子”。筛子找出沙子里的石子,镊子把石子夹出来。两者缺一不可。

1.3 常见调试手段概览

嵌入式调试的手段很多,从最原始的“点灯大法”到高大上的JTAG Trace,各有各的适用场景。我按“从简单到复杂”的顺序给你捋一遍。

1.3.1 日志输出(printf / UART)

最经典、最通用的方法。没有之一。我在项目里永远会在主控板上留一个UART口,哪怕量产版本不用,开发板也一定要有。printf一打,程序跑到哪、变量值是多少,一目了然。

// 最简单的调试输出
#define DEBUG_PRINT(fmt, ...) \
    printf("[%s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__)

void app_main(void) {
    int counter = 0;
    while(1) {
        DEBUG_PRINT("counter = %d\n", counter++);
        delay_ms(1000);
    }
}
注意:printf在中断服务函数里慎用!我曾经在一个高优先级中断里加了打印,结果系统直接卡死——printf本身会阻塞,中断嵌套把栈撑爆了。

1.3.2 GPIO点灯 / 示波器

当你的芯片连UART都没有,或者串口被占用了怎么办?点灯。在关键代码位置翻转一个GPIO,用示波器看波形。这个方法土,但极其可靠。

// 用GPIO测量函数执行时间
void measure_function(void) {
    GPIO_SetPin(HIGH);  // 开始
    // ... 要测量的代码 ...
    GPIO_SetPin(LOW);   // 结束
}
// 示波器看高电平宽度,就是执行时间

我记得有一次调一个电机驱动,PWM波形死活不对。用printf打出来全是乱码,后来直接拿示波器勾PWM输出脚,发现是定时器预分频配错了——示波器上一眼就看出来了。

1.3.3 断点调试(JTAG / SWD)

这是最正统的调试手段。通过调试器(J-Link、ST-Link、ULINK等)连接目标芯片,在IDE里设断点、看变量、单步执行。

  • 硬件断点:芯片内部寄存器实现,数量有限(通常2-6个),不依赖代码修改。
  • 软件断点:编译器在目标地址插入BKPT指令,数量几乎无限,但会修改Flash内容。
  • 条件断点:满足特定条件才停下,比如变量等于某个值。调试复杂逻辑时非常有用。
避坑指南:我曾经在RTOS的任务里设了一个断点,结果一停下来,其他任务的时间片就乱了,导致看门狗复位。后来我改用“条件断点+打印”的方式,只记录不暂停,问题就解决了。调试实时系统时,断点要慎用。

1.3.4 Trace(跟踪)

这是调试的“核武器”。通过ETM/ITM等硬件模块,实时记录CPU执行的指令流、数据访问、事件触发,完全不打断程序运行。

我参与过一个车载项目,系统偶尔出现CAN报文丢帧。用断点根本抓不到,因为问题发生的频率太低。后来上了Segger的SystemView(基于Trace),把任务切换和中断事件全部录下来,回放一看——有一个低优先级任务在中断关闭期间占用了太长时间,导致CAN接收中断被延迟了。这个案例让我彻底服了Trace工具。

1.3.5 逻辑分析仪 / 协议分析

当问题出在通信接口上(I2C、SPI、UART、CAN),逻辑分析仪是最好的朋友。它能抓取总线上的所有电平变化,还原出通信数据。

调试手段 适用场景 侵入性 实时性
printf / UART 通用、快速定位 高(修改代码) 低
GPIO + 示波器 时序测量、中断响应 低 高
JTAG/SWD 断点 逻辑错误、变量检查 高(暂停CPU) 低
Trace 实时系统、性能分析 极低 极高
逻辑分析仪 通信协议、时序问题 无 极高

1.4 调试知识体系总览

下面这张图把本章的核心概念串起来了。你可以把它当作整个调试知识体系的“地图”。

嵌入式调试知识体系总览 嵌入式调试 调试 vs 测试 调试核心目标 常见调试手段 发现缺陷 定位根因 观察状态 干预执行 printf/UART GPIO+示波器 JTAG/SWD断点 Trace跟踪 逻辑分析仪 调试手段选择原则 简单问题用简单手段(printf/点灯) 复杂问题用高级手段(Trace/逻辑分析仪) 不要用大炮打蚊子,也不要用牙签挖地雷

这张图把本章的核心内容串在了一起。你从中心“嵌入式调试”出发,左边是调试与测试的对比,中间是调试要达成的目标,右边是各种调试手段及其层级关系。底部那个说明框是我多年总结的经验——选工具要匹配问题复杂度。

本章要点回顾:
  • 嵌入式调试是在真实硬件上获取运行时信息、定位故障的过程
  • 测试发现“有没有问题”,调试回答“为什么有问题”
  • 调试手段从printf到Trace,各有优劣,没有银弹
  • 选择调试工具时,考虑侵入性、实时性、问题复杂度