做嵌入式开发和硬件联调时间长了,最怕遇到的就是“偶发的 bug”。功能测试跑一整天都好好的,偏偏在客户现场、在给领导演示的时候冒出来;串口偶尔乱码、蓝牙用着用着断开、烧录十次有两三次失败——这类问题最折磨人,因为它不像必现 bug 那样有清晰的复现路径。你问它为什么,它自己也说不清;你盯着它看,它反而不出来。今天我想分享的不是什么高深技巧,而是三个我自己踩过的坑和沉淀下来的排查方法:串口假故障的换机排除、蓝牙断开的录屏取证、烧录问题的“新旧批次对照”。这三个方法有一个共同点:把偶发问题变成可控的、可观察的、可对比的问题,剩下的就是水到渠成。
1. 偶发Bug排查的第一课:先把“信息不足”当成根因
1.1 “偶发”到底意味着什么
每一行代码、每一个信号在物理世界都有确定的原因。偶发 bug 的本质不是玄学,而是触发条件太复杂、太隐蔽,我们手上的信息不足以定位它。串口乱码,背后可能是 USB 转串口的时序问题,可能是目标板波特率误差超标,也可能是电源纹波把信号毛刺打到了接收端;蓝牙断连,背后可能是空口调度冲突、连接参数不匹配、休眠策略把链路饿死;烧录失败,背后可能是供电跌落、复位时序不对、芯片进入 bootloader 的状态随机。
一个 bug 如果每 100 次出现 1 次,说明存在一个至少 1% 概率出现的外界干扰或状态组合。这时候你盯着屏幕等它再犯一次,效率极低。更聪明的做法是提高信息密度,比如用逻辑分析仪抓波形、用 HCI 日志记录蓝牙空口事件、用录屏把用户操作动作变成时间轴。我用一个生活化的比喻给刚入行的朋友讲:偶发 bug 就像家里偶尔听到的滴水声,白天环境噪声大,你听不见;凌晨安静了,声音就变得很清楚。问题不在于那滴水是否存在,而在于你有没有在正确的时间、正确的位置去监听。
我自己排查时有个习惯:不管多急,先写清楚“什么条件下出现、什么条件下不出现”,再动手改代码。很多时候,光是记录现象这个动作,就能把问题概率从 1/100 提到 1/3。因为你很快会注意到规律:总是在开机后第几分钟出现,总是在特定波特率下出现,总是在另一台设备靠近时出现。
1.2 三个案例背后共同的排查逻辑
串口假故障的换机排除、蓝牙断开的录屏取证、烧录问题的“新旧批次对照”,表面上是三种不同手段,底层逻辑其实是同一个:要么控制变量,要么补充证据,要么对比差异。
换机排除,是为了快速切分故障域:是电脑的问题、线材的问题,还是目标板的问题;录屏取证,是为了把“用户说的现象”变成我可以反复回放的现场,补上日志里缺失的用户操作时间线;“新旧批次对照”,则是把“烧录时好时坏”这个结果拆到不同硬件版本上,用批次差异来逼近根因。
这也是我这几年越来越深的体会:偶发 bug 排查肯定绕不开三个动作——留证据、控变量、做对照。只留证据不控变量,你会被一堆假线索淹没;只控变量不留证据,你没法向别人证明你真的修对了;只做对照不复现,你连对照的基线都没有。把这三件事同时做好,偶发 bug 迟早会现形。
2. 串口“假故障”怎么定位:换机排除法拆掉工具背的锅
2.1 现场还原:串口助手卡死、乱码和端口丢失
先讲一个特别常见的场景。你在用串口调试助手调试一块 STM32 或者 ESP32 开发板,波特率 115200。跑着跑着,串口助手界面突然卡住,数据停在一个奇怪的位置;关掉重开,又能继续工作。有时候现象更隐蔽,是“十次连接,有两次提示端口被占用”——设备管理器里明明没有任何程序占用,可端口就是打不开。在 Linux 下则是打开/dev/ttyUSB0时报失败,或者dmesg里出现和 USB 串口相关的异常。
很多工程师的第一反应是怀疑固件:是不是串口中断优先级配错了?是不是 DMA 和空闲中断的配合有问题?于是开始改代码、加超时重发、调整 FIFO。这些工作不是毫无意义,但在动手改固件之前,有一个更值得做的动作:先确认工具链本身是否可靠。
这里要单独提一下“串口 DMA”。不少项目为了提高吞吐量,会在单片机上开启串口 DMA 接收。本身没有问题,但 DMA 和某些 USB 虚拟串口驱动的流控机制叠加在一起,会放大工具侧的偶发故障。等你查完固件回头再看,会发现最初几次“卡死”根本就是 USB 转串口模块被电脑的电源管理策略挂起了。
2.2 换机测试的两个关键动作:交叉换口、换整机
所谓换机排除,不是简单地把“设备拿到另一台电脑上试试”,而是要设计成一套可记录、可对比的测试。
第一步,先做交叉换口。把 USB 线从电脑前面板换到主板后置 USB 口,或者从集线器换到主板直出接口。这个动作可以排除前面板 USB 供电不足、机箱内电磁干扰、劣质集线器这几个变量。
第二步,做换整机。找一台干净的电脑,装最新的官方驱动,用同一款串口调试助手软件、同一根 USB 线、同一个开发板,连续跑半小时压力测试。如果换机后完全不出现,说明问题大概率在原电脑的 USB 链路、驱动程序或电源设置上。
第三步,如果换机后依然偶发,再换 USB 转串口线/模块。我一般会准备三种不同的模块:CH340、CP2102、FT232,来回来换着试。不用迷信某一款,但一般来说 FT232 的驱动稳定性和抗干扰能力会好一些。
测试过程建议记一张表,比如:
| 测试对象 | 测试方法 | 观察结果 | 初步结论 |
|---|---|---|---|
| 原电脑 + 前面板 USB | 原配置跑 30 分钟 | 卡死 2 次 | 问题频率基线 |
| 原电脑 + 后置 USB | 换口跑 30 分钟 | 卡死 1 次 | 有缓解但未根除 |
| 干净电脑 + 同板 | 换机跑 60 分钟 | 0 次异常 | 问题指向原电脑环境 |
| 原电脑关闭 USB 节能 | 改系统设置跑 120 分钟 | 0 次异常 | 根因锁定为省电策略 |
这张表的意义在于:每一步都有记录,每一条结论都有数据支撑。排查到后面,不管是自己复盘还是向同事解释,都能拿得出手。
2.3 真凶往往在“工具链”里:USB节能与劣质线材
我遇到最典型的一次“假故障”,前面换机测试已经明显指向原电脑了,最后发现是 Windows 的“USB 选择性暂停”设置。系统在空闲一段时间后,会自动把 USB 设备挂起;而串口助手打开端口时,驱动已经和设备建立了状态,这时候设备被系统暂停、再恢复,驱动状态就乱了。表现就是串口长时间不通信,第一次收发必丢数据,或者端口直接不可用,必须断开重连。
解决办法很简单:控制面板 -> 电源选项 -> 更改计划设置 -> 更改高级电源设置 -> USB 设置 -> USB 选择性暂停,设为“禁用”。再顺手把设备管理器里每个 USB Root Hub 的“电源管理”选项卡中,“允许计算机关闭此设备以节约电源”的勾选去掉。Linux 下则可以用udevadm monitor观察插拔事件,也可以写 udev 规则关闭 autosuspend。
另一个藏得很深的真凶是劣质 USB 线或杜邦线。有些线材本身阻抗不稳,用万用表量电阻是正常的,但数据量一上来就时不时产生毛刺。这种问题换机排除特别好使:你把同样一块开发板从“电脑A + 线A”换到“电脑B + 线A”,如果还偶发,再换一根线就稳定了,结论立刻清晰。
所以我的建议是:遇到串口偶发问题,先默认工具链有嫌疑,用换机法把工具链排除掉,再回头查固件。这能帮你省掉大量“改代码改到怀疑人生”的时间。
2.4 开发阶段怎么预防串口假故障
排查经验沉淀下来,其实可以从源头减少踩坑。第一,开发调试阶段不要省 USB 转串口模块的钱,尽量选带隔离方案的模块,至少选驱动稳定的主流芯片。隔离模块能阻断地环路干扰,在很多噪音比较大的现场环境下有奇效。
第二,养成关闭系统 USB 节能的习惯。不管 Windows 还是 Linux,拿到开发机第一件事就是关掉 USB 节电策略,能避掉一大类“用着用着断连”的问题。
第三,串口调试工具不要同时开太多。尤其是安装了某些虚拟串口软件后,多个工具同时抢占同一个虚拟 COM 端口,会让问题变得极其诡异。我自己用串口调试助手,一般只保留一个,并且优先选支持时间戳显示的版本。有了时间戳,日志回看时才能和操作动作对齐。
3. 蓝牙用着用着就断开:录屏取证还原真实现场
3.1 为什么蓝牙断连特别难查
蓝牙断连是典型的“偶发、难复现、主观描述模糊”三类问题的集合体。蓝牙链路是无线链路,受距离、遮挡、干扰、双方蓝牙栈调度、功耗策略影响,任何一个环节偶发异常都可能表现为“闪断”。更麻烦的是,很多蓝牙断连并没有让设备弹出任何报错,用户只会说一句“用着用着就断了”。
开发时你拿一块开发板连着手机测试,可能跑十分钟都正常;用户拿回家,放在口袋里绕一圈,就断了。你问他“断开前做了什么”,他大概率回答“什么都没做”。但设备不会无缘无故断开,一定有什么触发条件。比如手机息屏后蓝牙芯片进入低调度的调度策略、设备端连接间隔过长导致超时、或者某个 App 在后台抢占了蓝牙权限。
另外一个难点在于蓝牙日志非常不直观。Android 的 HCI snoop log 导出来是一大段十六进制,你不把它和用户操作时间轴放在一起看,根本不知道这段日志对应哪个场景。所以我们需要录屏,录的不是屏幕本身,而是一条可以对齐时间线索。
3.2 录屏取证的完整流程
我用 Android 手机加 BLE 设备举例,流程基本通用。
第一步,打开开发者选项,找到“蓝牙 HCI 日志”这类选项,开启抓取。不同品牌名字略有差异,有的叫“启用蓝牙 HCI 信息收集日志”,有的藏在“调试”菜单里。
第二步,开启系统录屏。要求测试人员从“点击连接”开始,一直录到“断开后十秒”,中间不要停。关键是在操作时口述动作,比如“我现在息屏了”“我打开了一个 App”“我走到房间另一头了”。这些口述会变成录屏里的时间锚点。
第三步,复现断开后,立刻关闭 HCI 日志抓取,导出日志。Android 导出的文件通常是btsnoop_hci.log,用 Wireshark 打开,找到Disconnect Complete事件,记下断开原因码。
第四步,把录屏时间轴和 HCI 日志时间轴对齐。Android snoop log 自带时间戳,你只需要把录屏里的“息屏动作”时间和日志里的“断开事件”时间做一下换算,就能知道断开前几秒钟发生了什么。这一步是整个流程的灵魂。
| 断开原因码 | 含义 | 常见触发场景 |
|---|---|---|
| 0x08 | Connection Timeout | 链路长时间无数据包,连接参数或睡眠策略有误 |
| 0x13 | Remote User Terminated | 对端主动断开,需要查对端应用逻辑 |
| 0x16 | Connection Terminated by Local Host | 本机主动断开,查本机蓝牙栈或 App 逻辑 |
3.3 真实案例:息屏后90秒必断
我之前遇到一个 BLE 数据采集设备,用户的反馈是:手机息屏后放一会儿,再解锁,设备已经断了,需要手动重连。开发那边一直复现不了,因为测试人员都是亮着屏连着电源在测。
后来我们按前面的流程做了一次完整取证。录屏显示,断开发生前,用户只是把手机息屏放到桌面上;HCI 日志里的断原因是 0x08,也就是 Connection Timeout。再看日志里的时间戳,从息屏到断开大约是 90 秒。这 90 秒里,设备侧一个数据包都没有发,手机侧也没有发起任何事件。
回到固件查,真相很快浮出水面:设备端设置了很长的连接间隔,并且在空闲时进入了低功耗模式,把连接事件当成无关紧要的定时器来对待;而手机侧因为长时间收不到任何包,按协议栈超时把链路断开了。这不是蓝牙“偶尔抽风”,而是功耗策略和连接参数不匹配导致的必然结果,只是触发条件恰好是“息屏”。
修复也简单:调整设备端的连接间隔,让低功耗状态下依然保持最低频率的连接事件;或者在进入睡眠前主动向手机发起断开,并设计好重连逻辑。如果当时没有录屏取证的这组时间轴,我们可能会在距离、天线、电磁干扰这些方向白费很多功夫。
3.4 蓝牙取证中的几个延伸思路
有些时候模块不自带 HCI 日志,比如 HC05/HC06 这种经典蓝牙串口模块,你可能只能看到 UART 上的 AT 指令和错误响应。这时候可以用逻辑分析仪去抓模块 TX/RX 引脚的电平,再看断开前主控发了什么指令、模块回了什么。信号和指令一对上,很多疑问就清楚了。
如果你是做协议层面的排查,可以找蓝牙 Core Spec 相关章节看连接事件、断连原因的说明,内容虽然多,但按目录找对应问题并不难。小到连接参数更新,大到配对绑定流程,官方文档里都有明确定义。
还要注意一类容易被忽略的问题:蓝牙设备名或 MAC 冲突。比如两批设备使用同一个克隆 MAC,或者同一个名称,手机自动连接时可能挂到另一台设备上,导致原设备显示断开。录屏里如果能看到手机蓝牙扫描列表的顺序和名称,这类问题往往一眼就能识别。
4. 烧录时好时坏?用“新旧批次对照”找出隐藏硬件变量
4.1 烧录失败的现象与常见误判
分享一个很典型的“烧录时好时坏”案例:用 Keil5 配合 ST-Link 或者 J-Link 给 GD32、CH32、STM32 这类 MCU 下载程序,编译明明没有问题,点下载却偶尔报Cannot access target或Flash Download failed。有时候重复点几次又能成功,有时候必须断电重来。这种问题如果只靠换机排除,很容易陷入死循环:换一个烧录器,还是时好时坏;换一根 USB 线,还是时好时坏;换一台电脑,依旧时好时坏。
到这里,基本可以判断问题不在烧录器,而在目标板或板子之外的供电环境。这时候,“新旧批次对照”是最有效率的一招,条件是你手头恰好有旧批次样机。没有旧批次,就找两块当前批次中“稳定”和“不稳定”的板子做对比。
4.2 新旧批次对照怎么设计
先建立基线。拿旧批次板子,固定烧录器、固定同一根 USB 线、固定同一台电脑、固定同一个固件 hex 文件,连续烧录 10 次,记录成功率。然后再拿新批次板子,用完全相同的环境连续烧录 10 次。如果旧批次 10 次全过,新批次只过 7 次,那问题大概率是批次差异引入的硬件变量,而不是烧录器和电脑的偶发状态。
得到这个结论后,不要急着改板子,先把“批次差异”拆成更细的变量。具体可以按这三步来:
- 用示波器量新批次板子在烧录瞬间的关键波形:nRST 引脚、VDD 供电、SWDIO/SWCLK 时钟线;
- 对比新旧批次的原理图、PCB 版本变更记录,重点看电源去耦电容、BOOT 引脚的上下拉电阻、晶振负载电容这些和启动、烧录相关的部分;
- 如果硬件查不到明显问题,再做固件交叉测试:把旧固件烧到新板子上,看是否仍然失败。如果旧固件在新板上稳定,说明问题可能和新固件的 Flash 占用、保护配置有关。
4.3 找到变量:电容、Boot引脚与固件变更
这里讲两个我实际遇到过的根因。
第一个根因是电源去耦电容的批次变更。新批次板子因为物料到货问题,换了一种 ESR 更高的陶瓷电容,容量还不变。表面看规格相同,但烧录时 MCU 要往 Flash 里写数据,这个瞬态电流比正常跑代码时大不少;新电容在瞬态下没能把电压稳住,VDD 跌落超过 300mV,芯片直接复位,烧录器自然就报错了。示波器一量,那个跌落波形非常明显。修复也简单:换回低 ESR 电容,或者适当降低烧录接口速度也能缓解,但治本还是改物料。
第二个根因是 BOOT 引脚的上拉电阻被调整。新批次板子把 BOOT0 的上拉电阻从 10K 改成了 100K,抗干扰能力下降。上电瞬间,BOOT0 引脚电平受电磁环境干扰,有时随机拉高,芯片就直接进了 bootloader,导致烧录器识别不到目标芯片。后来的规避办法是“按住复位键,点下载,再松开复位”来提高成功率,但根因还是引脚状态不确定。最后恢复 10K 上拉,问题绝迹。
还有一个常见变量在固件侧:比如新版固件启用了读保护或者 Flash 加密,旧的烧录脚本没有处理 RDP 或解锁流程,就会出现“十次能成功七次”的现象。这种也要通过新旧批次对照加新旧固件交叉测试才能定位。
4.4 烧录排查的其他常见坑
烧录这类问题的变量非常多,我把自己踩过和听同行提过的常见坑都列在这里。
第一,USB 线供电问题。很多烧录器是直接从 USB 取电再供给目标板的,如果线材过细、过长,目标板电流一大电压就掉。换一根粗短 USB 线,成功率可能立刻就不一样。
第二,接触电阻。开发板的排针、母座在空气中氧化后,尤其在冬天热胀冷缩,接触电阻会变大。这种问题极其隐蔽,因为每次插拔的位置不同,接触质量就不同。遇到烧录偶发失败,可以先换一个插拔位置,或者用酒精清洁触点。
第三,Windows 下的杀毒软件或系统服务干扰。听起来像玄学,但确实有人遇到过杀毒软件周期性扫描 USB 设备,正好撞上烧录握手过程,导致失败。关闭杀毒软件后成功率明显提升。这不算常规排查项,但如果你把其他变量都排干净了,可以留意一下。
第四,烧录器和 IDE 的版本记录。J-Link 固件升级前后,对旧芯片的适配可能出现差异;Keil5 的 DFP 版本不对,也可能导致 Flash 算法异常。所以每次排查,都要把烧录器版本、IDE 版本、目标板批次号、固件构建号完整记下来,否则你根本不知道哪个变量变了。
5. 沉淀一套自己的偶发Bug排查流程:留证、控变量、做对照
5.1 把三个案例的方法抽象成通用动作
这三个案例,本质上分别是三个通用动作的教学样本。
串口的换机排除,对应的是故障域切分:把问题可能所在的区域分成电脑、线材、工具软件、目标板几块,然后用替换法一块一块排除。蓝牙的录屏取证,对应的是高密度信息采集 + 时间轴对齐:单纯靠日志找不到用户操作触点,就把录屏变成第二条时间轴。烧录的新旧批次对照,对应的是批次变量对比 + 单变量验证:把“时好时坏”拆到两个群体上,再逐步拆出真正元凶。
不管以后遇到的是哪种偶发 bug,先问自己三个问题:第一,我手上有什么证据?没有就补。第二,我能控制哪些变量?把它们逐个固定。第三,有没有可对比的基线?没有就制造一个。这三个问题问完,排查路径基本清晰了。
5.2 我的取证工具箱与“病案本”习惯
给大家看看我现在常用的取证工具,不一定多高端,但足够覆盖大部分偶发问题。
串口类:支持时间戳的串口调试助手、一个稳定可靠的 USB 转串口模块、一个 24MHz 采样率的逻辑分析仪。示波器属于通用必备,有条件就上。
蓝牙类:Android 手机的蓝牙 HCI 日志,配合 Wireshark 分析;嵌入式侧用逻辑分析仪抓 UART 上的 HCI 或 AT 指令;如果涉及到信号强度变化,可以用手机装一个蓝牙扫描器看 RSSI。
烧录类:J-Link Commander 命令行工具,或者 OpenOCD 写一个循环烧录脚本。脚本的好处是可以连续跑 50 次,自动统计成功率,比自己手动点击可靠得多。我经常写一个小脚本,每烧录一次记录时间戳和结果,失败时打印异常输出,方便回看。
文件也好、脚本也好,最重要的是所有采集到的数据都必须带上时间戳。没有时间戳的日志只能用来确认“发生过”,无法用来回答“为什么发生”。录屏、日志、测试记录,三个时间轴一旦对齐,偶发问题通常都会露出马脚。
另外我特别想推荐一个习惯:维护一份“病案本”。每次排查完,无论有没有找到根因,都把现象、现场记录、怀疑列表、验证动作、最终结论写进去。这个动作麻烦不了几分钟,但长期价值极高。我遇到过好几次新问题,排查到一半翻到旧记录,发现现象和两年前某次一模一样,直接省掉一整天的摸索。
5.3 给偶发Bug一份“耐心预算”
排查偶发 bug,最忌讳的是焦虑和瞎试。一次改一堆变量,成功了也不知道是哪个动作起效;失败了更惨,可能把原本正常的部分改坏了。我现在的节奏是“先侦察,再打仗”:先花半天时间收集信息、布置记录工具、设计对照方案,再动手改任何东西。前期的侦察越充分,后期的修复越精准。
给自己设一个“耐心预算”也很重要。一个偶发问题,如果连续三个小时没有任何进展,我会主动停手,去睡一觉或者跑个步,回来再看日志。很多时候,信息就那么多,一直在屏幕前死磕只会让思路越来越窄。换个状态回来,反而能注意到之前忽略的时间戳、某个边缘条件。
最后再分享一个小技巧:我习惯在桌面留一个“排查模板”的 Markdown 文件,遇到问题就复制一份,里面固定字段是时间、现象、操作、观察结果、疑点、下一步验证。用不了两分钟就能填完,但比任何记性都可靠。排查偶发 bug,本质上是一个耐心收集证据的过程,只要证据链完整,它迟早会现形。