偶发 bug 这东西,只要干过嵌入式或者硬件调试的人,基本都见过它最磨人的一面:你盯着它的时候它不出现,你一松手、合上电脑、客户开始演示,它准时来。更麻烦的是,你反复抓日志抓不到,复现概率又低,查起来像大海捞针。串口假故障、蓝牙偶发断开、烧录时好时坏,这三类问题表面上看风马牛不相及,实际底层逻辑是一样的——都是某种间歇性、环境相关性、难以稳定复现的异常。这篇博文就拿这三类问题开刀,聊聊我自己常用的排查方法,包括怎么用“换机”来隔离串口假故障、怎么用“录屏取证”抓蓝牙断开现场、怎么做“新旧批次对照”来定位烧录失败,适合刚从单片机调试入门、或者已经在量产阶段被偶发问题折磨到头疼的朋友参考。
1. 偶发 bug 的本质:先把“偶发”变成“可观察”
1.1 偶发 bug 为什么这么难查
必现 bug 是好兄弟,改一行、跑一遍、复现了,二分法很快就能缩小范围。偶发 bug 恶心在哪?它不是每次都出错,出错也没有明显规律。你靠打 log 去猜,log 太多反而污染现场,log 太少又抓不到关键信息。更要命的是,一旦你试图加打印、加断点去“观察”它,问题反而消失了。嵌入式圈子里管这叫“观察者效应”,虽然不是量子力学那种严谨定义,但现象是真实存在的:调试手段改变了时序,bug 就被惊跑了。
偶发问题的根源,我总结下来无非三类。第一类是时序类,比如上电时序不满足、外设初始化竞态、中断优先级配错导致偶发抢占,这类问题跟时间强相关。第二类是干扰类,电源纹波、地弹、电磁干扰、静电,这类问题跟环境强相关,你在实验室没事,一上产线、一开大功率设备就出问题。第三类是变量漂移类,芯片批次换了、Flash 厂商换了、烧录器老化、固件大小变了导致 Flash 布局变化,这类问题跟“看起来没变但其实变了”的因素相关,最隐蔽。
第三类是最容易被忽视的,也是我在实际项目里踩得最惨的。程序代码一个字节都没改,昨天的板子好好的,今天新贴的板子就是各种怪。一开始总怀疑自己哪里弄错了,后来才意识到,硬件物料、芯片版本、Flash 型号、烧录器固件这些“隐性变量”都在漂移。做偶发 bug 排查,第一步不是急着定位,而是先想清楚:哪些东西是真正不变的?哪些你以为没变、其实已经变了?
1.2 我的排查原则:隔离变量、保留现场、分级复现
我处理偶发问题,有一套固定的原则,核心就三个词:隔离变量、保留现场、分级复现。
先说隔离变量。一次只允许一个变量发生变化,别的全部锁死。比如怀疑串口问题,就锁死固件、锁死板子,只换电脑 USB 口、只换 USB 线、只换转接工具;怀疑烧录问题,就锁死烧录工具和软件版本,只换板子批次。千万不能一上来就“顺便”升级一下驱动、“顺手”改一下配置,然后问题好了,你根本不知道是哪个改动起效的。变量一旦混在一起,排查就变成了玄学。
保留现场就更好理解了。偶发 bug 最重要的一点是:问题发生的那一刻,现场信息一定要足。日志、截图、录屏、硬件状态指示、故障码,能留多少留多少。宁可事后发现信息冗余,也不要出问题时两手空空只能靠回忆。说句实在话,很多偶发问题最后能定位,靠的不是当时灵感爆发,而是把现场数据摊开之后,在细节里找到了对不上的那个点。
分级复现也很关键。如果问题在 A 环境下 1 小时出现 1 次,在 B 环境下 10 分钟出现 1 次,那就优先用 B 环境来复现,速度就是效率。复现概率实在低的,就想办法构造压力条件。串口容易丢数据的就提升波特率、加长数据量测试;蓝牙不稳定的就在办公室各角落走动、旁边开 WiFi 路由器;烧录偶发失败的就不停刷循环烧录,跑 100 次统计成功率。把偶发问题从“偶尔”变成“高频”,你才有资格谈定位。
2. 串口假故障的换机排查:不是先怀疑芯片,而是先怀疑链路
2.1 串口“假故障”的典型表现
串口问题在调试里出现的频率高得离谱,而且九成以上都不是设备端坏了。我见过太多“板子好像死机了”“串口没反应”的报障,最后查下来要么是 USB 转串口工具驱动崩了,要么是 USB 线接触不良,要么是电脑的 USB 口供电不稳。这类问题我习惯叫“串口假故障”,意思是设备端其实好好的,是链路某一环出了问题。
假故障的典型表现有这么几种。一是上电完全没打印,串口调试助手打开端口后什么数据都不来,点发送也没反应。二是乱码,数据里夹杂大量 0xFF、0x00,或者字符对不上,这种通常是波特率不匹配、参考地没共接、电平不对。三是时通时断,刚开始正常,过几分钟卡死,拔一下 USB 线再插又好了。四是只在一个端口或一台电脑上有问题,换到另一个 USB 口就消失。
这里特别想提一下 CH340 和 CP2102 这类常见 USB 转串口芯片,它们看起来都是“插上就能用”,实际差异非常大。CH340 在 Windows 下的驱动版本混乱,系统自带的驱动和官网最新驱动行为不一样,休眠唤醒后偶发不枚举的情况在 CH340 上明显多于 CP2102。但这不代表 CH340 不能用,只能说在工位调试这种反复插拔的场景里,链路易受影响的环节更多。调试时碰到怪问题,先想想你用的转接芯片是什么、驱动是哪个版本,能少走很多弯路。
2.2 换机排查的操作步骤与原理
我遇到串口假故障,第一反应不是拿示波器去量波形,而是做一轮“换环境”排查。这个叫“换机排查”,但核心不是换设备,而是用更换环境来做故障隔离。
具体步骤我一般这样走。第一步,换 USB 物理口,把线从机箱前置口换到后置主板口,排除供电和信号完整性问题。第二步,换 USB 线,很多线看着是好的,里面芯线已经断了,尤其在接头根部,换个线往往就好了。第三步,换转接工具,把手头的 CH340 换成 CP2102 或者 FTDI 芯片的工具,这一步是为了排除转接芯片本身的枚举异常。第四步,换电脑,换到另一台笔记本或者台式机上测试,排除上位机环境问题。第五步,查驱动和设备状态,Windows 设备管理器看有没有黄色感叹号、错误代码 10 或 43,Linux 下看dmesg | grep tty有没有异常断开记录。
这里每一步都不是瞎换,而是有明确目的的。换 USB 口是排查物理链路和供电,换线是排查线材,换转接工具是排查串口芯片状态,换电脑是排查驱动和 USB 控制器兼容性。当你在某一步之后问题稳定消失,故障范围就缩小到了那一环之前。注意我说的是“稳定消失”,不是“碰巧好了”。判断标准很简单:换完之后连续跑 30 到 100 次上下电和数据收发,全都没问题才算通过。如果只是刚换完那一会儿好,没过多久又犯,说明故障点根本不在这。
还有一个容易被忽略的点:有些串口假故障其实是设备端供电不足。板子通过 USB 口供电,电脑 USB 口本身电流有限,再加上转接工具和传感器抢电,主控一上电瞬间电流拉高,电压跌落,串口就初始化失败了。你在换机排查的时候,如果换了电脑问题就消失,不一定是电脑的 USB 控制器更好,也可能仅仅因为那台电脑的 5V 供电更足。所以我通常在串口排查前先确认一件事:给板子单独供电,USB 转串口只接 TX、RX、GND,不接 VCC。这样就把设备和链路供电分成两个独立变量,排查起来干净得多。
提示:排查串口问题务必先确认参考地。串口通信是异步的,GND 没接好,电平根本没参考点,数据必然出错。换过很多线材和工具都没用的时候,优先检查 GND 链路,尤其是杜邦线插接件氧化和接触不良的问题。
3. 蓝牙断开的录屏取证:让偶发问题自己“开口说话”
3.1 蓝牙断开的两种根因:射频环境还是协议栈时序
蓝牙偶发断开是另一大折磨王,尤其是用 HC-05、HC-06 这类串口透传模块做小项目的时候,表现就是“用着用着就断了,重连又好了,也不知道啥时候断的”。要定位蓝牙断开,第一步是分清根因方向:是射频环境造成的链路丢失,还是协议栈/主机时序造成的异常断开。
区分方法其实不难。如果是射频环境干扰,断开通常发生在设备移动、障碍物遮挡、WiFi 或微波设备工作的时候,而且断开后有一定的重连延迟,模块本身没有被“打懵”。如果是协议栈层面的问题,断开往往有固定的行为模式:比如主机主动断开、被动断开(对端超时)、模块自身复位,行为特征各不相同。
这里我想拿 HC-05 举个例子。HC-05 模块上电后有两种模式,命令响应模式和自动连接透传模式。偶发断开的常见原因有这么几个:一是电源纹波大,蓝牙射频模块对供电很敏感,锂电池供电和 USB 供电表现就可能不一样;二是波特率不匹配导致数据堆积,看起来像“卡死”,其实没断,只是数据堵住了;三是主机端心率和看门狗冲突,比如单片机有看门狗,蓝牙没回数据就喂狗失败,系统复位,外行人看起来就是“蓝牙断了”。这一类问题,光靠看模块灯的闪烁状态是判断不出来的,必须把时间线拉出来对着看。
3.2 取证三件套:录屏、串口日志、时间轴对齐
我调试蓝牙偶发断开的固定姿势是这样的:把操作录屏、串口日志、时间轴对齐三件事同时做,一次性把现场数据拿全。这套方法我管它叫“取证三件套”。
第一步是录屏。很多年轻工程师觉得录屏很土,实际上是最好用的取证手段。手机架在旁边对准模块和电脑屏幕,电脑上同时开着串口调试助手和 AT 指令测试窗口,从问题开始前一分钟一直录到问题复现之后。录屏记录的是现象和操作序列,比任何描述都靠谱。这里有个细节:录屏时一定要把电脑右下角的时间显示出来,或者手机开一个带秒表/时间显示的应用,这样后续才能精确对齐事件。
第二步是串口日志。如果蓝牙模块是连到主控的,主控的调试串口一定要输出事件日志。日志里至少要有这几类信息:收到多少字节、发了多少字节、环形缓冲区剩余空间、重连状态、模块复位标志、AT 指令响应码。日志格式建议带毫秒级时间戳,标准格式化输出,不要用裸的printf("disconnect\n")这种没时间的日志,事后根本没法对齐。
第三步是时间轴对齐。把录屏里的现象、串口日志里的断点、AT 指令的响应时间放在同一个时间轴上对比。比如发现问题在 12:03:15 断开,串口日志显示 12:03:14 收到主机下发的断开指令,AT 响应在 12:03:16 返回 OK,那基本可以说明这不是射频干扰,而是主机主动发起了断开。反过来,如果断开瞬间模块连 AT 都不响应了,板子上的电源指示灯也在闪,那就是供电问题,跟蓝牙协议半毛钱关系都没有。
这几个典型场景我从实际案例里拆过好几个:
| 现象 | 串口日志表现 | 初步结论 |
|---|---|---|
| 模块自动断开,AT 马上能响应 | 断开前接收到主机断开指令 | 主机主动断开或超时策略触发 |
| 模块断开,AT 无响应,LED 灭 | 日志戛然而止,无任何异常输出 | 模块复位/供电瞬断,查电源 |
| 数据收发中断,但连接未断 | 日志显示缓冲区溢出,丢包 | 波特率过高或流控没处理 |
| 断开后无法自动重连 | 模块反复初始化搜索 | 主从模式配置或配对信息丢失 |
这里多嘴一句:蓝牙协议的调试,不要一上来就拿着频谱仪去扫信道,先把日志和时序对清楚。很多“射频问题”最后都是逻辑问题。
4. “新旧批次对照”的烧录排查:程序没变,芯片已经变了
4.1 烧录失败里的批次玄学
烧录问题表面上看是最“硬”的,因为烧录是一个确定性的操作,步骤对了、工具对了、芯片对了,就应该成功。但烧录类的偶发问题恰恰是最玄的,尤其是量产阶段,我见过太多“代码明明没问题,就是烧不进去”的情况。Keil5 里编译成功,一点下载就报Cannot access target;J-Flash 连不上芯片,提示Wrong CPU ID;或者烧录过程到一半 Flash 擦除失败,报Error: Flash Download failed。这类问题如果时好时坏,大概率跟芯片批次有关。
批次问题的底层逻辑是:芯片制造商会调整晶圆工艺、Flash 供应商也可能更换,芯片的 IDCODE(ID 代码)和 Flash 的擦除/编程时序看似一致,细节兼容性却可能发生变化。简单说,你以为固件是二进制世界的唯一真相,实际上芯片内部对烧录信号的响应一直在变。
举个例子。某个项目用的 STM32F103 系列,旧批次烧录一切正常,新批次回来之后,同一个 J-Link、同一个 Keil 工程,报Cannot access target,但用 ST-Link 就能烧进去。排查到最后发现,新旧批次芯片内部的 Flash 型号虽然都兼容,但对擦除时序的要求略有差异,J-Link 的默认擦除算法和这个新 Flash 型号配合不好,换成 ST-Link 或者调整烧录器速度就好了。这就是典型的“批次差异导致的烧录兼容性问题”。
4.2 新旧批次对照实验怎么设计
遇到烧录问题时好时坏,我第一反应是做一个“新旧批次对照”实验。这个实验的核心思路是:用旧板子和新板子互相交叉烧录,固定其他变量,观察故障是否跟随某一块板子或某一个固件。
最标准的做法是这样。手头如果有一块旧批次板子、一块新批次板子,旧固件(之前确定能烧录成功的镜像)和新固件(当前编译的镜像),按四组组合测试:
| 实验组 | 板子 | 固件 | 预期结果 |
|---|---|---|---|
| A | 旧批次 | 旧固件 | 基准组,确认工具链正常 |
| B | 新批次 | 旧固件 | 验证故障是否跟随板子 |
| C | 旧批次 | 新固件 | 验证故障是否跟随固件 |
| D | 新批次 | 新固件 | 验证组合复现 |
如果 B 组失败而 A 组成功,说明问题跟随新板子,跟固件无关。此时重点检查新批次板子的供电电压、晶振配置、芯片型号识别、复位电路。如果 B 组成功但 D 组失败,看起来是新固件的问题,但实际上仍需要确认新固件是否用到了新芯片特有的一些外设差异,比如新芯片的 Flash 容量布局变化导致烧录算法不匹配。
如果 A、C 都能烧,B、D 都不能烧,问题就非常明确了:新批次硬件有兼容性差异。这时候再往下拆,从硬件电路的物料批次、电源、芯片本身三个角度找根因。我需要强调一个原则:所有对照实验必须用同一个烧录器、同一台电脑、同一个烧录软件版本,烧录器固件版本也要固定。烧录器固件升级经常悄悄改变行为,这是个极容易引入干扰变量的环节。
实操中,我还习惯记录一张“烧录现象记录表”,格式大概是这样的:板子编号、芯片丝印批次、烧录器型号、烧录器固件版本、IDE/烧录软件版本、目标固件哈希、烧录电压、报错代码、重试次数、最终结果。这张表不是给别人看的,是给自己留证据的。很多偶发烧录问题,最后就是靠着记录表里的“电压从 3.3V 掉到 3.1V”“换了另一根 USB 线就好了”这种细节找到答案的。
这里再补一个特别常见的坑:芯片读保护。新批次芯片出厂时 option bytes(选项字节)里的读保护等级可能不是默认值,之前用的批次恰好都是默认状态,新批次一进去,J-Flash 或 ST-Link 提示Read protection is enabled,很多朋友第一次遇到会懵。解决办法是先用烧录器全片擦除或者解除读保护,再重新烧录。但是注意,解除读保护通常会触发芯片的全片擦除,如果你的产品有校准参数或者序列号存在 Flash 里,这一步操作前务必先备份,别烧完才发现数据没了。
5. 几个让我印象深刻的实战复盘
光讲方法不讲案例,总觉得差点意思,这里挑两个我亲手排查过的真实场景还原一下。都是那种“看起来完全没希望”,找准思路之后 20 分钟就解决的事。
第一个是串口假故障加蓝牙断开混合出现的情况。当时是一个带 BT 透传的工装,现场反馈“用一会就断,串口也经常不打印”。大家一开始猜测蓝牙模块坏、主控坏、固件 bug,轮流排查了好几天。我在现场看一下,发现一个线索:串口调试助手用的是虚拟串口软件映射出来的 COM 口,这个软件在 Windows 休眠唤醒后经常掉设备,蓝牙断开的瞬间,串口助手实际上也已经处于假死状态。录屏取证之后一对照时间轴,所有问题都指向同一个源头:USB 总线的电源管理策略把设备给挂起了。修改系统电源选项、禁用 USB 选择性挂起、更新虚拟串口工具版本之后,问题消失。这整个案子根本不是蓝牙问题,也不是串口问题,是上位机 USB 电源管理策略问题。
第二个是烧录偶发失败的八字没一撇案例。板子是同一批次的,但烧录成功率从 95% 掉到了 60%,而且烧录失败的报错每次都不一样,有时候擦除失败,有时候校验失败。一开始怀疑 J-Link 坏了,换了一个还是这样。后来我用新旧批次对照的记录表拉出来看了一下,发现一个被忽略的变量:新到货的一批烧录线是镀金头,但线芯是细线,压降比之前的粗线大。板子在烧录时电流需求高,电压一跌落,芯片供电不稳,烧录自然失败。换回粗线瞬间恢复。这个案例给我的教训是:排查偶发硬件问题,永远不要嫌“线材”太低级而不去怀疑它。
这里的经验很直接:很多偶发问题,最后都是“最不起眼的变量”在捣鬼。那些大家都盯着的大块头——主控、固件、算法——反而很少出问题。
6. 偶发 bug 排查的长期习惯
排查偶发 bug,技术能力是一方面,另外很重要的一方面是习惯。我个人的体会是,偶发 bug 最怕的从来不是复杂,而是“证据不足”加“变量混乱”。只要每次动手前想清楚这次要验证什么、锁死什么变量,大部分偶发问题都能在几次实验之内圈定范围。
所以最后分享几个我一直在用的习惯,谈不上高深,但实际救过我好多次。第一,手边常备一个记录本,任何一次调试操作都记下来:几点几分、改了啥、结果如何、当时的硬件/软件版本是什么。偶发问题隔几天再看,笔记就是你唯一的记忆。第二,做任何排查之前先把“不会变的变量”列出来,明确写着本次实验不改动什么,这样能防止手滑和冲动改配置。第三,复现问题的时候不要怕次数多,跑 50 次、100 次,把成功率跑出来,用数据代替感觉。第四,善用录屏和日志,手机、电脑的录屏都能用上,时间戳对齐永远比肉眼观察可靠。
一个小技巧作为结尾:遇到偶发问题解决之后,别急着把现场收拾干净,先在记录本上写一遍“这个问题是怎么被定位的、关键证据是什么、为什么以前的排查方向不对”。这几十个字,能帮你把排查经验固化下来,而不是每次都像第一次遇到一样从头踩坑。以后同类型的偶发问题再出现,翻看这些记录往往就能直接定位,效率能提高一个量级。