做嵌入式开发这些年,最让我头疼的不是复杂的算法,也不是难啃的协议栈,而是那种碰运气才出现的偶发 bug。串口数据偶尔错位、蓝牙链路偶尔断开、烧录偶尔失败——这三件事单独拿出来都不算大事,可一旦叠加在同一个项目里,足以让人怀疑人生。最近一个 ESP32 蓝牙小车的项目,就把这三类问题全凑齐了。今天这篇,我想把整个排查过程完整复盘一遍,重点讲三种我自己验证过好用的手段:串口假故障的换机排除、蓝牙断开的录屏取证、以及新旧批次对照的烧录排查。这些方法不需要高端仪器,靠的是细心和一套固定的排错次序,任何一个做嵌入式的朋友都能直接用。
1. 偶发 bug 的定性思维:先把问题分到正确的"抽屉"里再动手
1.1 必现问题用直觉,偶发问题靠排除
必现 bug 的排查套路其实很简单:按触发路径复现,抓现场,改代码,验证。整个过程是线性的,最多就是加日志、加断点反复试几次,总能逼近根因。但偶发 bug 完全不是这个玩法。它最大的敌人是随机性——你没法保证下一次测试一定会复现,于是所有结论都建立在概率上,这会带来一个很尴尬的局面:你改了一行代码,问题没再出现,但你根本无法确认是代码生效了,还是这次纯粹运气好。
我在项目里吃过太多这种亏。所以后来给自己定了一条规矩:拿到偶发问题,先不急着打开代码编辑器,而是先做定性分析。所谓定性,就是判断这个问题最可能属于哪一类:是硬件电路不稳定,是软件上的竞态或时序问题,还是外部环境(供电、干扰、工具)带来的假象。分类不对,后面的排查全是白费,甚至会把你往错误的方向越带越远。
1.2 三层边界:软硬件、前后端、新旧状态
结合这个项目的三个问题,我把偶发 bug 常遇到的分类边界总结成三层:
- 软件与硬件边界:问题是芯片本身行为不对,还是代码配置没做对?我习惯用"替换法"来验证,串口案例里你会看到具体怎么操作。
- 前端与后端边界:在有手机 App 和嵌入式设备的系统里(比如蓝牙遥控小车),问题发生在 App 侧还是设备侧?录屏取证就是专门解决这个问题的。
- 新旧状态边界:代码没变、工具没变,但换一批板子就出问题。这时候要对照新旧硬件、固件、工具的差异,烧录排查靠的就是这个方法。
这三层边界不是互相独立的,一个偶发 bug 可能同时牵扯到其中两层甚至三层。但没关系,你要做的只是确定先查哪一层,以及用什么手段去验证。我的经验是:先查最好验证的那一层。软件层可以加日志,硬件层可以做替换,前后端可以双端同时对时——哪个手段便宜就先上哪个。
1.3 排错时最容易犯的一个错误:同时改变多个变量
还有一个纪律层面的问题必须强调。很多人排查偶发 bug 时,一旦怀疑某个环节,就顺手把相关的都调整一遍:换条线、换个 USB 口、重新装个驱动、还顺手更新了烧录工具。结果问题确实消失了,但你根本不知道是哪个变更起了作用。更可怕的是,真实原因被掩盖了,换一个环境、换一个操作场景,它又会冒出来。
所以我的铁律是:一次只变更一个变量。每变更完,完整测一轮,做记录。这听起来很笨,但对付偶发 bug 恰恰是最聪明的做法。因为偶发问题的触发本身就有随机性,如果你同时变了三个变量,哪怕问题复现了,你也没法把归因到任何一个变量上;哪怕问题消失了,你也不知道该保住哪个改动。
2. 串口假故障的换机排除:丢包问题不在代码,而在 USB 转串口工具
2.1 症状:串口 DMA 偶发丢包,代码静态检查看不出问题
这个项目的下位机是 GD32F470,跑 FreeRTOS,串口用 DMA 方式接收不定长的传感器数据。现象很气人:上位机连续发 100 包,板子大概会丢 1 到 2 包,丢包位置完全不固定,时间间隔也没有规律。一开始我怀疑是串口 DMA 的配置问题,反复查了接收完成中断、IDLE 中断、缓冲区乒乓切换,还把 DMA 的 FIFO 阈值调了好几个挡位,都没有变化。代码层面看不出任何毛病。
这种时候如果不跳出来,很容易在软件里原地打转,反复加日志、反复调缓冲,最后连自己都分不清是改好了还是碰巧没复现。实际情况是:这类"偶发丢包"有相当一部分根本不是软件 bug,而是物理链路的问题,也就是我常说的"假故障"。它表现得像芯片收发数据坏了,实际上芯片正常得很,问题出在传送数据的介质和工具上。
2.2 换机排除的完整顺序:线材、USB 口、转换工具、电脑
"换机排除"说白了就是把可疑链路上的部件一个一个换掉,看问题会不会跟着某个部件迁移。这个方法的精髓在于:如果故障跟随某个设备走,那这个设备就是嫌疑;如果故障不跟随任何设备,那问题大概率在主机的软件配置或者下位机本身。我当时按下面的顺序做了四轮替换:
- 换串口线材:把原来那根杜邦线换成双头带屏蔽的成品线。丢包依旧。
- 换 USB 口:从测试台前面的 USB Hub 换到主板后置原生 USB 口。丢包依旧,但频率好像低了一点——这种模糊信号最容易骗人,必须继续往下查。
- 换 USB 转串口工具:把 CH340 小板换成 FT232 成品线。问题消失,连续测了 300 包也没丢。
- 换回 CH340,但接在独立供电的 USB Hub 后面:问题也不再出现。
做到这一步,结论就清楚了:丢包问题跟着 USB 转串口工具和它的供电环境走,与下位机的串口 DMA 配置没有直接关系。真正的原因是这个 CH340 小板自身稳压做得不够好,插在电压波动大的前置 USB 口上时,输出电平在 TTL 阈值附近抖动,偶发造成一两个字节的采样错误。换一个供电干净的口,或者换一个转换工具,问题就解决了。
2.3 假故障的物理层元凶:电平阈值、供电纹波、波特率误差
串口假故障的常见物理层元凶,我整理了一张表,方便以后对照排查:
| 因素 | 表现 | 常见原因 |
|---|---|---|
| 电平阈值 | 偶尔丢字节、收到 0xFF 乱码 | 3.3V 和 1.8V 电平不匹配,或发送端驱动能力不足 |
| 供电纹波 | 偶发丢包、USB 转串口工具发热 | USB 口供电不稳,或线材压降大 |
| 波特率误差 | 偶发帧错误、数据错位 | 收发双方时钟偏差大,非整数分频导致采样点偏移 |
| 驱动问题 | 串口整体不能稳定通信 | CH340/CP2102 驱动版本不对,或与其他驱动冲突 |
| 接线接触 | 动一下线就丢包 | 杜邦线松动、虚焊、排针氧化 |
关于电平转换多说一句。很多新板子现在用 1.8V 电平,而普通 USB 转串口工具输出的是 3.3V TTL,直接用大概率出问题。网上很多人在问"串口 3.3V 转 1.8V 电平转化三极管电路",说明这个坎很多人都踩过。如果你拿一个 3.3V 工具去接 1.8V 串口,轻则不工作,重则长期高压应力损伤芯片引脚,而它表现出的症状往往不是"彻底不通信",而是"偶尔通信失败"。这种问题用万用表量静态电平时根本看不出来,非得用示波器抓波形才能发现高电平上不去、毛刺一大堆。
2.4 虚拟串口与串口调试助手在复现与验证中的作用
在做换机排除之前或之后,你还可以用虚拟串口软件做一次纯软件层面的通路验证。它会在电脑上创建一个虚拟的 COM 口对,比如 COM5 和 COM6 互相对应,一个口发数据,另一个口就能收到,完全绕开物理串口。我当时用这个办法验证了上位机的发送逻辑和协议解析是否正常,确认了问题不在用户态代码。
配合串口调试助手,你还可以做两种很实用的测试:
- 回环测试:把串口工具的 TX 和 RX 短接,自发自收,检测工具本身和驱动是否正常。
- 压力测试:用串口调试助手按固定周期发大量数据包,同时记录接收端的日志时间戳,观察丢包是否具有周期性。如果丢包集中在某个发送频率附近,往往是波特率误差或缓冲区溢出,而不是偶发干扰。
这套做法走完,串口假故障基本都能锁定。我后来在项目里直接规定:凡是报"串口偶发异常"的问题,必须先按这个链路做一遍换机排除,再谈改代码。这条规矩帮团队省掉了大量无效的软件排查时间。
3. 蓝牙断开的录屏取证:用时间戳把断连责任钉死在某一端
3.1 为什么蓝牙偶发断开必须取证,不能靠记忆
蓝牙断连是嵌入式项目里最经典的"罗生门"。测试人员拿着手机说:蓝牙又断了。工程师问:什么时候断的?测试人员答:就是刚才,我正操作呢。工程师打开日志一看,什么都有,就是没有对应时段的记录。再问:是 App 断的,还是系统断的?没人能回答。
问题出在蓝牙断连涉及两个端:手机 App 和蓝牙模块或设备固件。任何一端都能主动断开链路,甚至手机系统自己也可能因为省电策略或者射频环境差而断链。没有现场证据,两边都觉得是对方的锅,最后只能靠吵架解决。这种时候,录屏取证是最廉价也最有效的办法。所谓"取证",不是给谁定罪,而是把断连瞬间的客观事实固定下来,让讨论从"我觉得"变成"我们看这段录屏"。
3.2 录屏取证的具体操作:系统录屏 + 蓝牙 HCI 日志 + 双端时间戳
具体怎么做?我在项目里整理了一套标准操作,让测试人员照着做一遍,所有关键信息都能留下来:
- 打开手机开发者选项里的"蓝牙 HCI 日志"(有些手机叫 Bluetooth HCI snoop log),让系统在后台记录底层蓝牙协议帧。
- 开启系统自带的屏幕录制,同时把系统状态栏录制进去,因为状态栏的蓝牙图标能直接反映底层连接状态。
- 在 App 里显示一个连接状态指示灯,并输出带时间戳的日志到串口助手或者写到本地文件。
- 测试人员操作复现问题,每次断连立刻记录时间点:几点几分几秒、当时在哪个页面、正在做什么操作。
录屏的作用是记录"现象",HCI 日志的作用是记录"底层动作",App 日志的作用是记录"自身逻辑"。三者按时间戳对齐后,断连的那一刻是谁的动作、是什么顺序先发生的,就能准确判断。这里的关键是时间戳必须统一,我一般以手机状态栏显示的系统时间为准,App 日志和录屏都按这个时间对齐,偏差控制在秒级以内就够用。
3.3 从录屏判断是前后端 bug:App 触发断开与底层链路断开的分野
拿一个实际例子说明怎么对着录屏判断责任:
- 如果录屏里 App 界面提示"蓝牙已断开",但系统状态栏的蓝牙图标还是实心的、设置页里也显示已连接,那说明断的是 App 层的逻辑连接,物理链路没断。这在责任上更偏向前端——App 可能因为自己的状态机出错发出了断开指令,或者错误地认为连接已经失效。
- 如果录屏里系统状态栏蓝牙图标消失,同时手机设置页里该设备变成"未连接",那就是底层链路确实断了。这时要查模块固件、射频环境、电源稳定性和双方协议栈的配置,属于后端或硬件的责任。
在我这个项目里,录屏抓到的真相很典型:每次进入小车控制页面,App 会先主动断开旧连接,再去扫描附近设备。正常情况下这不会出问题,但在某个 Android 版本上,断开动作和扫描动作相隔太近,系统底层还来不及释放连接资源,扫描就已经开始,信号冲突后连接彻底失效。录屏里能看到 App 的"断开"弹窗一闪而过,然后连接状态变灰,但系统蓝牙图标一直是好的。这个案例里,硬件和蓝牙模块一点问题都没有,纯粹是 App 页面生命周期里的重连逻辑没考虑系统资源释放时序。没有录屏,这种责任很难说清,两个工程师能吵一下午。
3.4 协议栈细节:A2DP 切 SCO 这类隐藏的打断源
除了 App 逻辑和模块本身,有些断连是协议栈层面的"抢资源"导致的。蓝牙音频设备常见的 A2DP 切 SCO 模式就是一个典型:手机正在通过 A2DP 播放音乐,突然来电话或者视频通话,链路模式切到 SCO,音频通道重新协商,此时如果还有一个 SPP 数据通道正在传输数据,就可能被短暂打断,严重时直接断开。
这种问题在排查时尤其难找,因为它不是业务代码的 bug,而是蓝牙协议栈的并发资源管理行为。要判断是不是这类问题,录屏依然是最直观的:你可以看到断连发生的瞬间,手机上正好跳出来电话或者语音助手的界面。有了这个时间点,再去查模块的链路层日志,就能确认是不是 SCO 切换把 SPP 连接挤掉了。
如果你用的是 HC-05、ESP32 这类经典蓝牙方案,做产品测试时不要只测单纯连接放音,要专门测"边连接边放音乐边来电话"这种复合场景。我看过太多案例,测试报告里写着"蓝牙稳定",一上真实使用场景就断连,就是因为复合场景没有覆盖。再提醒一句,杰理蓝牙这类芯片还特别讲究天线匹配,天线阻抗偏了就会表现为"距离稍微远点就断连",这种问题刷固件没用,得回到硬件设计去解决。
3.5 录屏取证的另一个好处:让测试报告真正"可复现"
最后说一个录屏在团队协作里的价值:它让测试报告从主观描述变成客观证据。以前测试一句"蓝牙不稳定",开发只能靠猜;现在测试发给你一段录屏,你按时间点就能定位到自己负责的那一段逻辑有没有动作。这比任何口头复述都有用。我甚至建议把录屏作为蓝牙问题单的强制附件,没有录屏的蓝牙问题单不予受理。这个规则执行之后,团队处理蓝牙问题的效率提升非常明显,因为测试人员自己录屏时也会更仔细地记录操作路径,误报率直线下降。
4. 新旧批次对照的烧录排查:当"烧录不上"和"代码没问题"同时成立
4.1 偶发烧录失败的现象与初始排错
第三个问题出在量产前的烧录环节。现场反馈:同一套 Keil5 工程,旧批次板子烧录一切正常,新批次板子十次里有两三次烧录失败。报错信息五花八门,有时是"Erase Failed",有时是"Cannot Access Target",重启板子和烧录器又能好一阵,你根本不知道下一次会不会再失败。
这类问题最迷惑人的地方,是它和代码一点关系都没有。VS Code 里编译明明成功,Keil 下载就失败,很多人的第一反应是版本问题、驱动问题、破解问题,折腾一圈发现毫无变化。其实"编译成功"和"烧录失败"本来就是两件事,前者只代表代码对象生成了,后者取决于烧录器、目标芯片、复位逻辑和电源状态,代码写没写对都影响不到这一步。想明白这一点,你就能把注意力从软件工程转移到硬件连接的排查上。
4.2 新旧批次对照的操作维度:固件、工具链、复位电路、芯片批次
我处理这种情况的习惯,是把问题按"新旧批次对照"的思路拆开:既然旧批次没问题、新批次有问题,那变化一定出在从旧到新的某个差异上。差异的来源无非这几个:
- 硬件电路设计变更:原理图或 PCB 改了,常见的是复位电路、电源去耦、BOOT 引脚上下拉。
- 芯片批次差异:同一个型号,不同丝印批次的芯片,内部上电时序、烧录时序可能有细微差别。
- 烧录工具与连接方式:J-Link 版本、SWD 速率、连接线长度、是否经过转接板。
- 固件版本或工程配置:链接脚本、烧录算法(FLM)、目标驱动设置变了。
对照的顺序,我会先查最好查的:把新批次板子接到旧批次同一台电脑、同一个烧录器、同一根连接线,排除工具链变量;然后对比新旧批次原理图和 BOM,看硬件差异;最后才动芯片批次层面的尝试。很多人一上来就怀疑芯片是假货或者翻新件,这种猜测没有对照数据支撑,很难让人信服,也不能指导下一步动作。
4.3 一个 Keil5 烧录失败的真实案例:复位电容的一颗之差
我之前处理过一件特别典型的案例,和"新旧批次烧录失败"一模一样。故障板复位电路上的电容从原来的 100nF 改成了 10uF——原因只是硬件工程师想增强抗复位能力。结果这颗电容在 SWD 烧录时把复位脚拉低的时间拖得太长,烧录器握手期间芯片反复处于复位状态,Keil5 就偶发报烧录失败。把 Keil 的烧录速度从 4MHz 降到 100kHz,或者修改烧录器的复位时序,问题就不再出现。
这个案例给我的启发是,遇到烧录偶发失败,先别怀疑芯片质量、别怀疑烧录器坏了,首先想一想:最近板子的复位电路、BOOT 上拉、电源时序有没有改过?如果你手头同时有新旧两块板子,最好用同一个烧录器交叉测试,确认问题是否稳定跟着新板子走。如果稳定跟随新板子,再仔细对比新旧版本的原理图差异,答案往往就在那一颗电阻一颗电容之间。
4.4 类似案例:ESP32 编译成功但烧录不进、J-Link 速度选不对
换成 ESP32 环境,"VS Code 里编译成功却怎么也烧录不进开发板"也是高频问题。ESP32 的进入下载模式依赖 EN 和 IO0 两个引脚的时序配合:下载工具先把 IO0 拉低,再让 EN 产生一次复位,芯片才会进入 bootloader。如果 IO0 被外部电容或后级电路拖累,复位瞬间电平变化不够快,就会出现"编译一切正常、上传偶发失败"的现象。这种问题在合宙、安信可的各种 ESP32 模组上我都见过,多数不是芯片本身的问题,而是下载线太长、IO0 被外部设备拉高、或者串口工具供电不足。
J-Link 烧录速率同理。SWD 速率不是越高越好,线一长、目标板电源不稳,高速握手就会失败。烧录器连不上目标板时,把速率从 4MHz 降到 1MHz 甚至 100kHz,往往立竿见影。如果你的 J-Link 是通过排针转接板连接到目标板,注意排针的接触电阻和线缆长度,有时候缩短十厘米、换一根粗一点的地线,问题就消失了,烧录器本身和芯片本身都没毛病。
4.5 烧录排查清单:按顺序抄作业
我把烧录排查按优先级整理了一份可以直接照做的清单:
- 确认编译已经成功,并且烧录的是最新生成的镜像文件。
- 确认烧录器和板子的连接:SWDIO、SWCLK、GND、目标供电四根线是否可靠,线长是否超过 20cm。
- 检查板子供电是否稳定,尤其是通过烧录器供电时,供电能力不足会导致握手失败。
- 检查 BOOT 和复位电路有没有硬件变更,用新旧批次对照原理图。
- 降低烧录速率(J-Link 在 Keil 里设置到 100kHz 或 1MHz),排除时序问题。
- 如果是 ESP32 类,确认 IO0 和 EN 的下载时序,必要时检查 boot 引脚是否有外部负载。
- 最后才考虑换芯片批次、换烧录工具版本。
照着这个顺序走,绝大多数烧录偶发失败都会落在第 2、4、5 这几条里。我自己的项目里,真正因为芯片质量问题导致烧录失败的案例极少,反倒是接线不良、供电不足、复位电路改版这三种占了八成以上。
5. 一次只改一个变量的排错流程:模板、工具与我的个人体会
5.1 一套可以一直用的排错记录模板
前面讲的串口假故障、蓝牙断开取证、烧录批次排查,三件事看起来不相关,但有同一个内核:在"偶发"两个字面前,靠系统性的方法而不是运气来解决问题。所以最后再分享两个落地工具。
第一个是排错记录模板。每次处理偶发 bug,我会建一个这样的小表格,哪怕只有几行:
| 时间 | 环境 | 现象 | 变更操作 | 结果 | 推断 |
|---|---|---|---|---|---|
| 10:12 | 前置 USB 口 | 100 包丢 2 包 | 更换串口线 | 仍丢包 | 排除线材 |
| 10:20 | 前置 USB 口 | 100 包丢 1 包 | 更换 USB 口 | 频率略降 | 供电可疑 |
| 10:35 | 独立供电 Hub | 300 包丢 0 包 | 更换供电 | 问题消失 | 锁定供电和工具 |
表格看着简单,但它的价值在于每一步都有结果、每一步都可以回溯。排查到第三步时,你已经能很自然地知道下一步该换什么变量,而不是凭感觉乱猜。我见过太多工程师排错三天,最后问"你第一天换过什么",自己都记不清了。有这个模板在,至少你不会犯这种低级错误。
第二个是工具清单。我个人建议嵌入式调试工位常备这几样:一个可靠的 USB 转串口工具(FT232 或者供电设计较好的 CH340),一对带屏蔽的串口线,一个独立供电的 USB Hub,一台装了虚拟串口软件和串口调试助手的电脑,以及一部能录屏、能开蓝牙 HCI 日志的测试手机。这些工具都不贵,但处理偶发 bug 时都是"便宜的解药"。
5.2 最后再讲几句我的体会
踩过这么多坑之后,我对偶发 bug 的理解就八个字:没有证据,不动代码。先取证,再定性,最后才是改。串口问题用换机排除,蓝牙问题用录屏取证,烧录问题用新旧批次对照,每一招的本质都是把"不确定"变成"确定",把"偶发"变成"可复现"。这个思路到了下一个项目、下一个芯片平台,依然成立。
最后说个想起来就感慨的细节:那台让我排查了好久的"问题电脑",其实没有任何故障,只是它的前置 USB 口电压掉得比别的机器多一点。有时候 bug 的根本原因就是这么不起眼。但只要你愿意一层层排除、一条条记录,它总能被揪出来。