1. 偶发 bug 的复盘纪律:不急着改代码,先锁住变量
调试这行干久了,最怕听到的就是一句话:“这个 bug 是偶发的,刚才还在,现在又不出来了。”
这句话一出来,就意味着前面几个小时的排查动作大概率全是白做的。偶发问题真正的难度不在于“修”,而在于“复现”。你连现场都抓不住,改什么代码都是在蒙着眼睛拆弹,改完也没法验证到底是不是这个原因。
我自己踩过的坑太多了。有一回是产品端的串口偶发收不到数据,客户那边隔几天报一次,每次都是重启一下就好,换个人测试就死活复现不了。后来折腾了大半个月,最后发现不是代码逻辑问题,而是 USB 转串口线在特定电脑上供电不足导致电平不稳。你看,方向偏了,时间全浪费在“猜”上。
所以我现在处理偶发 bug,第一反应不是开调试器,而是先做三件事:建时间线、锁环境差、留证据快照。
1.1 偶发问题最难的不是修,而是复现
复现偶发 bug 有一个基本共识:它不是完全随机,一定有个触发条件,只是这个条件平时很难碰到。这个条件可能是某个温度区间、某路供电电压的波动、某个外设中断来得太密集、某条指令执行到了特定时序,甚至只是某根线接触不良的抖动。
我一般会让反馈问题的人回答四个问题:
- 第一次出现是什么时候?最近一次出现又是什么时候?中间隔了多久?
- 出现时是在做什么操作?待机、通信、刷机、还是批量数据处理?
- 最近有没有动过硬件?换过模块、改过线序、更新过工具?
- 现场能不能留下点东西?截图、录屏、日志、串口输出,什么都有用。
这四个问题卡住任何一个,后面的排查就很容易走弯路。尤其是“最近有没有动过硬件”这条,很多人默认排除,但实际上一大半偶发问题的根子就在这。
1.2 现场信息的三件套:时间线、环境差异、证据快照
我在项目里会强制自己养成一套“现场信息三件套”的习惯,每次都按这个顺序来收集,不跳步:
| 信息类型 | 具体内容 | 为什么重要 |
|---|---|---|
| 时间线 | 首次出现、频次、持续时长、恢复方式 | 判断问题是瞬态还是稳态,是否与定时任务相关 |
| 环境差异 | 当时用的电脑、电源、线材、固件版本、周边设备 | 锁定变量,排除“换了一个条件就变了”的情况 |
| 证据快照 | 录屏、串口日志、寄存器读数、拍照记录接线 | 事后复盘有据可依,不靠记忆 |
这套方法最直接的价值就是逼着你把“偶发”变成“可描述”,把“模糊”变成“可查”。等到信息收集全了,你往往已经能圈出两到三个可疑变量,剩下的就是逐一排除。
1.3 一个值得养成的习惯:改动前先归档
这里我必须多说一句:每一次准备动手修改之前,先把当前能正常工作的版本完整归档。这不是废话,而是很多人到了紧急时刻根本想不起来做。
有一次我调 ESP32 的蓝牙连接,为了测试一个猜想改了一行初始化参数,结果改完更糟,想回去改原来的配置,发现早忘了原值是什么。那一行参数找了半天才从 Git 记录里翻出来,白白浪费了时间。
所以我现在的做法是:任何 firmware 改动之前,先在本地做一次完整的状态备份,包括编译产物、配置文件、烧录工具版本。这样不管改出了什么结果,随时能退回去对照。尤其是后面要讲到的“新旧批次对照”排查法,没有干净的归档对照,你根本没资格谈“对比”两个字。
2. 串口假故障的换机排除:换电脑有效不等于电脑坏了
串口调试应该是最常见的嵌入式开发场景了,但串口问题也是“假故障”重灾区。所谓假故障,就是现象上看着是设备坏了、芯片通信异常,实际上问题出在链路某个不起眼的环节,甚至出在调试工具和电脑的配合上。
我先说一个几乎每个人都遇到过的情形:串口调试助手打开,发指令,设备没反应,数据收不到。换一台电脑插上,测试,一切正常。第一反应往往是“原来那台电脑有问题”。这个结论不完整——换电脑有效不等于那台电脑坏了,更不代表项目里的设备没问题,它只是帮你把问题范围切掉了一块。
2.1 为什么“换台电脑”常常能治好串口假故障
我在多个项目里验证过,换电脑之后串口恢复正常的背后,往往藏着下面这些真实原因:
| 原因 | 具体表现 | 为什么换电脑就“好”了 |
|---|---|---|
| USB 主机控制器差异 | 老台式机前置 USB 口供电不足,设备枚举失败或丢数据 | 换台电脑供电充足,枚举稳定 |
| 转串口芯片驱动版本不同 | CH340、CP2102 在不同版本驱动下,缓冲机制有差异 | 新电脑驱动版本匹配,行为完全不同 |
| 地线电位差 | 设备地、电脑地之间存在压差,导致电平判断错误 | 另一台电脑接地更干净,压差被掩盖 |
| USB 转串口线材老化 | 线缆内部断股、屏蔽差,传输不稳定 | 换电脑时顺手换了线,但没意识到 |
| 串口被占用残留 | 上次程序异常退出,驱动没释放串口句柄 | 新电脑没有历史残留信息 |
这个表看懂之后你就能明白,换电脑其实是一个“整体替换变量”的操作——你同时换了 USB 控制器、驱动、供电环境、接地环境、历史状态。它非常适合用来做排查,但绝不能当成最终结论。正确的做法是:确认换电脑能解决问题之后,再回头用排除法找出具体是哪个变量导致的问题。
2.2 换机只是起点:串口链路排查清单
串口链路看似简单,就是 TX、RX、GND 三根线,但实际排查起来有固定套路。我一贯的顺序如下:
- 先测电平。拿万用表量 TX、RX 对 GND 的电压,正常空闲状态应该是接近供电电压(3.3V 或 5V),如果量出来只有零点几伏,那大概率是芯片没正常工作或者引脚被复用。
- 再查接线。TX 要接到对端的 RX,RX 接对端的 TX,GND 必须共地。这个简单到不能再简单的错误,我见过新手犯,也见过老手在跳线堆里翻车。
- 接着排除流控。很多 USB 转串口工具默认开启了 RTS/CTS 流控,但目标板硬件上根本没接这两个脚。这时候表现就是:数据发出去收不到回包,或者收回包时断断续续。直接把流控关闭,问题可能当场消失。
- 然后看设备枚举。在电脑的设备管理器里确认串口号、驱动是否正常。如果枚举出来是未知设备,先别怀疑芯片,把 USB 线换一根短的再试。
- 最后看波特率误差。串口通信是有容错范围的,一般要求在正负百分之二以内。如果你的 MCU 用了内部 RC 时钟,和上位机设置波特率偏差稍大一点,正常温度下没问题,温度一高就可能偶发乱码和丢包。
有一次排查一个 GD32F470VET6 开发板的串口,偶发收不到数据,折腾了三天,最后发现是开发板上的 USB 转串口芯片 VCC 脚被一根杜邦线短路到了 GND,电压被拉低,电平逻辑混乱。这种问题,换几台电脑都没用,必须回到链路本身去量。
2.3 MCU 侧容易漏掉的点:DMA 与中断优先级
链路全部检查完之后,问题如果还在,那就要往 MCU 内部看了。串口收发偶发异常,常见内因有两个:DMA 配置不当和中断优先级冲突。
用 DMA 接收串口数据时,通常配合空闲中断来判断一帧数据的结束。很多人只在初始化里开了 DMA,但忘了配置 DMA 通道的优先级,导致在 CAN、定时器中断密集时,串口数据迟迟搬不走,环形缓冲被覆盖。表现出来就是偶发丢帧,频率不高,但每次一来就是一批数据里丢几个字节。
我给一个排查思路:第一,确认 DMA 接收通道确实处于开启状态,很多人初始化了 DMA 但忘了调用HAL_UART_Receive_DMA这类启动函数;第二,检查串口全局中断的抢占优先级,不要让它在中断风暴里被饿死;第三,如果用过LL库和HAL库,明确函数内部的处理差异,不要把两套机制混用。
另一类常见情况是:中断服务函数里做了耗时操作,比如打印、延时、写 Flash,导致本应很快完成的数据接收被拖慢。串口在高速率下跑起来,中断稍微慢一点,硬件接收缓冲区就溢出了。你把中断服务函数精简到只做读数据和置标志位,问题往往就消失了。
总结一句话:串口假故障的排查顺序必须是“链路 → 工具 → 芯片内部”,永远别倒过来。
3. 蓝牙断开的录屏取证:把“偶尔掉线”变成一条可复盘的时间轴
蓝牙问题比串口问题更让人头疼,因为它的现场往往不在你的工位上,而是在客户手里、用户手里、路上跑的手持设备里。蓝牙断开的偶发性非常高,而且很多人反馈的时候只会说“连不上”“总掉线”,给不出任何依据。
这时候录屏就是最好用的取证手段。别觉得录屏是“甩锅”或“留后路”,实际上录屏能把模糊问题变成一条有时间轴、有操作路径、有现象界面的完整记录,后面分析起来效率能翻几倍。
3.1 录屏录什么:状态栏、信号强度、操作路径三要素
很多人一说录屏,就是拿手机从头录到尾,录完了发现全是垃圾信息。真正有效的蓝牙故障录屏至少要包含三个要素:
| 要素 | 说明 | 作用 |
|---|---|---|
| 状态栏 | 全程记录蓝牙图标是否消失、信号格数变化 | 直接反映 RF 链路是否中断 |
| 信号强度 | 开启开发者选项里显示 dBm 数值 | 判断是信号弱导致掉线,还是信号正常但协议层异常 |
| 操作路径 | 从打开蓝牙、配对、连接、使用到掉线的完整操作 | 复现触发条件,确认掉线和某个动作是否相关 |
以 Android 为例,在开发者选项里可以勾选“显示蓝牙设备信号强度”和“蓝牙 HCI 信息采集器”。前者能在状态栏显示实时的 RSSI,后者会把蓝牙芯片收发的 HCI 数据包记录下来。录屏的同时把这两项打开,掉线前后到底发生了什么,基本就藏不住了。
我要求客户录屏的时候会说清楚:第一,不要中途暂停;第二,掉线后继续录三十秒,方便我看到重连过程;第三,如果能再截一张系统“关于蓝牙”或“蓝牙日志”的图更好。
3.2 系统日志与 HCI 抓包怎么配合
录屏是给人看的,日志是给机器挖的。单独用录屏只能看到“掉了”这个现象,要定位“为什么掉”,必须配合日志和协议层抓包。
常用的组合是这三样:
- Android 的 “蓝牙 HCI 信息采集器”。开启后,系统会自动生成 btsnoop 文件,导出后可以用 Wireshark 打开,直接看到 LMP、LL、ACL 层的帧。掉线原因在协议层几乎是明摆着的:是对方发了断连请求(reason code),还是本机超时判定链路丢失,都会在 HCI 包里有体现。
dumpsys bluetooth_manager或厂商自带的蓝牙日志。能查到当前连接状态、休眠策略、是否被系统回收。- 应用侧的代码日志。能确认掉线时应用还在做什么操作,比如正在发送音频数据还是处于后台休眠。
实际配合方法是:先将录屏的时间轴和系统日志的时间戳对齐,在录屏画面里故意做一次“看时间”的动作,比如点开系统时钟,这样后期可以准确对齐到秒级。然后在日志里从掉线时间点往前推两秒,看有没有异常状态变更。
3.3 三个典型场景的取证重点
我这几年遇到的蓝牙断开问题,翻来覆去无非三种典型情况,每种取证重点都不同。
第一种:串口透传模块掉线,比如 HC-05。这种模块出问题多半和配置有关:主从模式设错、波特率不匹配、连接间隔过大导致对端超时。取证时重点录清楚模块上的 LED 状态:快闪表示可配对,慢闪表示已连接。掉线时如果是快闪,说明连接真的断了;如果模块侧慢闪但手机侧显示断开,那多半是手机系统把蓝牙“静默回收”了。
第二种:蓝牙音频 A2DP/SCO 切换掉线。典型表现是听歌正常,一来电话就断。这时候要看 HCI 日志里是否存在 A2DP 断开后尝试切 SCO 失败的过程。很多国产蓝牙芯片在 A2DP 和 SCO 之间切换需要重新配置链路参数,参数不对就掉线。取证时要明确记录:掉线发生是在电话接通的瞬间,还是在挂断瞬间。
第三种:BLE HID 设备,比如蓝牙键盘、自拍杆。这类设备为省电会进入休眠,必然导致连接暂时断开,这是正常现象。但如果每次唤醒都要十几秒才能重连,那就是从机广播参数或主机扫描策略有问题。取证重点是把“按键唤醒后到重新输入生效”的秒数录下来,连续测三次,基本能确认问题程度。
蓝牙问题里还有一个容易被忽视的因素:信道干扰。在办公区、地铁站这种 2.4GHz 极其拥挤的环境,BLE 掉线概率会明显上升。如果用户反馈“在公司经常掉,在家里不掉”,那就先在录屏里记下环境和时间点,然后再考虑是不是需要进行跳频或调整广播信道设置。
4. “新旧批次对照”查烧录:同一份固件,新批次进不了程序
烧录问题也是嵌入式开发里的高频场景,尤其是量产阶段。最常见的一句话是:“这固件在上一批板子上随便烧,新到的板子死活连不上烧录器,J-Link 都识别不到。”
出现这种情况,很多人的第一反应是怀疑芯片坏了。但“整批芯片都坏了”在概率上非常低,更合理的解释是:这批板子和上一批之间,发生了什么你没注意到的硬件差异。
这就是“新旧批次对照”排查法的核心思路:用新旧板卡互相交叉验证,把问题定位到具体差异上。
4.1 “新旧批次对照”到底对照什么
对照不是把新旧板子摆在一起看外观,而是对照这几个维度:
| 对照维度 | 具体操作 | 目的 |
|---|---|---|
| MCU 型号与封装 | 确认新旧批次物料是否完全一致,包括丝印、批次号 | 查清是否换了同系列但不同型号的芯片 |
| 引脚与原理图 | 对比新版原理图改动、网络表差异 | 排除板级设计变化带来的影响 |
| 烧录工具与配置 | 确认 J-Link 固件版本、Keil 配置、烧录速率是否一致 | 排除工具侧变量 |
| 核心供电与时钟 | 实测新板 VCC、复位脚、晶振起振情况 | 排查硬件工作条件差异 |
| 芯片内部状态 | 读取 ID Code、Option Byte、读保护级别 | 排查芯片被锁或配置异常 |
这四个维度对照下来,绝大多数烧录异常都能圈出范围。注意,新旧批次对照排查时最忌讳的是一上来就怀疑某一方“绝对没问题”,要保持两边条件完全可互换,结论才可信。
4.2 四组交叉实验的经典剧本
把新旧变量分开,最标准的做法是四组交叉实验:
- 旧板 + 旧烧录工具:确认基线可用。
- 旧板 + 新烧录工具:判断烧录工具是否被换坏。
- 新板 + 旧烧录工具:验证是不是新板子本身的问题。
- 新板 + 新烧录工具:复现客户的完整现场。
如果只有第 3、4 组失败,那问题几乎可以锁定在新板子硬件上。这时再针对 MCU 的供电、复位、时钟、烧录引脚逐项测量。如果第 4 组失败但第 3 组成功,那问题在烧录工具或软件配置上,优先排查驱动版本和烧录器固件。
有一次我们遇到新批次板子 J-Link 能识别内核,但一烧录就报错,用旧板测什么事都没有。交叉实验做完后,发现新旧板子的供电设计完全一样,但新板子的一颗去耦电容换了个容值更小的规格,导致烧录时内核瞬间电流拉低电压,烧录器直接掉线。这种问题不通过对照实验,单纯盯代码绝对查不出来。
4.3 新批次里最容易变的五个隐藏项
交叉实验确定是新板子问题之后,再把范围从 Board 收窄到具体原因。我做这类排查时,会优先检查下面五个点,优先级从上到下:
- 芯片读保护(RDP)级别。很多芯片出厂自带的 RDP 级联不一样,或者上一批贴片前被烧录过工厂测试代码并开启了读保护,新批没有正确解除。表现是:J-Link 能连上,但读取 ID 码或擦除 Flash 时被拒绝。解决路径是执行整片擦除或切换到烧录器的“unsecure”模式。
- Option Byte 配置。新批次的 Option Byte 可能和旧批次不同,比如把 BOOT0 引脚配成了别的功能、看门狗进了硬启动状态。表现复杂多样,但读一次目标芯片内部寄存器就能对比出来。
- SWD 引脚被复用。如果新版代码上电后立刻将 SWDIO/SWCLK 配成普通 GPIO,烧录器就再也连不上了。这个情况在“前一版能烧,新固件烧不了”的问题里特别常见。处理办法是烧录前让 MCU 先进入复位状态,再使用“Connect under Reset”模式连接。
- 供电能力不足。新批次板子如果带了更多外设,或者 DC-DC 输出电流余量不够,烧录器通过调试接口向芯片索取电流时电压就会塌陷。看现象是烧录到一半报错断开,重试偶尔成功。
- 晶振参数变化。更换了晶振品牌或负载电容,导致内部时钟不稳定,进而影响烧录器与芯片的同步时序。这种问题可以通过降低烧录器速率临时验证,如果降低速率后烧录稳定,就重点查时钟回路。
4.4 一次 J-Link 连不上新批次的完整复盘
分享一个我做过的实际案例。当时项目用 Keil5 + J-Link 给一批新板烧录 GD32 固件,现象是旧板秒连,新板报“Cannot connect to target”。
我按交叉实验跑了一遍,锁定是板子差异。然后量了供电,3.3V 正常,复位脚电压稳定,SWDIO/SWCLK 对应 MCU 引脚的波形在空闲时正常。最后用 J-Link Commander 尝试连接,发现设备能枚举,但读取 ID 码失败。
这时我不再反复连接,而是把两块板并排放在桌上,用万用表直接量新旧板 SWDIO 到 MCU 引脚的导通性。结果发现:新板上 MCU 的 SWDIO 引脚虚焊,探针点在测试点上正常,但内部根本没和芯片脚连通。补焊之后,一次通过。
那个批次其实只有这一块板子虚焊,但排查路径是完整的。虚焊问题在烧录故障里通常最隐蔽,因为它不像供电异常有波形可看,也不像读保护有寄存器可查,只能靠导通性测试去验证。所以每次测烧录问题,我强烈建议备一根万用表笔,把烧录引脚逐个量一遍,这个动作花不了两分钟,但能省下半天时间。
5. 我在现场养成的几个工作习惯,分享给你
说回最初的问题:偶发的 bug 怎么办?我的答案其实一直都没变——不要急着修,先让问题变得能复现、能描述、能对比。串口假故障用换机排除,蓝牙断开用录屏取证,烧录失败用新旧批次对照,本质上都在做同一件事:把模糊的偶发问题,改造成清晰的、有对照组的、可验证的问题。
这几条习惯是我这几年踩坑踩出来的,分享给同样被偶发问题折磨过的人:
- 排查记录永远写在纸面上或文档里,不要写在脑子里。复盘的时候,你唯一可信的参考就是当时的记录。
- 工具链版本一定要记录清楚。驱动版本、烧录器固件、IDE 版本,任何一个升级都可能改变行为。
- 同一项目里同时出现多个问题时,一次只处理一个。很多人把偶发问题越查越乱的原因,就是同时动了多根线。
- 怀疑“玄学”之前,先把地线、供电、接线这种最基础的东西重新检查一遍。我见过太多所谓玄学问题,最终都死在这三件事上。
- 不要害怕向别人求助时给对方“完整背景”。很多人报 bug 时只说现象,不说自己改过什么。学会把改动历史交代清楚,排查效率能提升一大截。
排查偶发问题,本质上就是一场和不确定性的拉锯战。你能做的不是消除所有未知,而是把未知范围一步步压缩到可控区域。希望这篇记录能帮你下次遇到类似问题时,少走几段弯路。