做嵌入式开发和硬件调试的朋友一定都经历过这种场面功能明明是对的代码翻来覆去查不出毛病偏偏在你快要放弃的时候它又自己好了。这种“偶发 bug”远比必现 bug 更折磨人因为它意味着你面对的很可能不是逻辑错误而是环境、时序、硬件体质、甚至批次差异混合出来的疑难杂症。我最近一口气处理了三个典型的偶发故障分别涉及串口通信假故障、蓝牙连接不稳定、固件烧录失败。三种问题三个维度但排查思路最后收敛到了同一条方法论上先把“嫌疑对象”隔离出来再决定是换机验证、取证复现还是做对照实验。这篇文章就把这三个案例完整拆开讲讲我是怎么一步步从“怀疑人生”到“定位根因”的里面有不少常规文档不会写的实操细节希望能给正在被偶发 bug 折磨的人一点参考。1. 偶发 bug 的通用排查思路先定性再定位1.1 偶发问题为什么难查三类根源先分清碰到的偶发 bug 多了我习惯先给问题做个粗分类因为它们对应的排查手段完全不同。第一类是“资源竞争型”。比如串口数据偶尔丢字节、蓝牙偶尔断开多半是 DMA 通道冲突、中断优先级被打乱、缓冲区被踩或者是多任务环境下共享资源没有加锁。这类问题的特点是频率低、无规律、换一台电脑或者换一个环境可能就没了。第二类是“硬件体质型”。比如某个批次的芯片电平阈值偏了、某颗晶振起振慢、某条排线屏蔽差这种问题往往在特定温湿度、特定供电条件下才暴露而且是“换板子就好过几天又犯”。第三类是“工具链型”。比如烧录失败、驱动时好时坏、编译环境和下载器兼容性问题这类问题的特点是代码本身没问题但工具链某个环节不稳定导致“同样的操作结果不一样”。知道了这三类根源接下来就不是闷头改代码了而是先搭建一个能稳定复现问题的环境。注意这里说的是“能稳定复现问题的环境”不是“能稳定复现问题”——这俩有本质区别。前者是为了让故障更频繁地出现方便观察和取证后者是被动等待故障效率极低。1.2 排查工具的准备日志、录屏、对照物三件套不管是哪类问题我建议手边常备三样东西。日志记录工具是底线。串口数据用串口调试助手或者自己写个 Python 脚本把所有接收到的原始字节带时间戳存到文件里不要只盯着屏幕看。蓝牙相关的日志Android 端可以用开发者选项里的蓝牙 HCI 日志抓取或者用 hcidump 工具如果是调试 BLE 设备nRF Connect 这类工具能直接看到连接参数和断开原因。录屏取证工具是容易被忽视但非常关键的一环。很多硬件问题跟 UI 操作时序强相关特别是蓝牙配对、App 连接这类场景。我一般会在复现问题的时候用手机自带的录屏功能把整个过程录下来包括信号强度指示、操作步骤、断开那一刻的画面。为什么一定要录屏因为偶发问题往往来得快去得快你盯着屏幕看十次都不一定能捕捉到但只要录了像就可以一帧一帧回放找出断开前最后发生了什么。对照样本是第三个法宝。手头最好保留至少两块不同批次或不同来源的板子这就是“新旧批次对照”的基础。很多时候偶发 bug 是某一批物料的问题你拿两块批次不同的板子做对照测试能省掉大量怀疑代码的时间。2. 案例一串口假故障为什么换台电脑就好了2.1 现象描述设备在 A 电脑上“坏了”在 B 电脑上“好了”第一个案例是典型的串口假故障。一块基于 STM32F4 的设备主板通过 USB 转串口芯片CH340跟电脑通信。现场反馈设备在工控机上完全打不开串口表现为“打开串口失败”或者“发数据没反应”但在开发用的笔记本上一切正常。我第一次接到这个反馈下意识怀疑是代码问题于是拿着串口调试助手反复比对配置波特率 115200、8 数据位、1 停止位、无校验完全一致。又怀疑是不是板子坏了换了一块全新的板子接上工控机故障依旧。这时候问题就很有趣了代码没问题板子没问题那问题只能出在“这台电脑”和“这块板子”之间的某个环节上。2.2 逐步排查从端口号到驱动版本再到芯片体质先看设备管理器。打开设备管理器发现 CH340 的驱动装上了但端口号被分配到了 COM9 之后的高编号。很多老旧的上位机软件只扫描 COM1-COM8这会导致“打开串口失败”。解决方法是手动改端口号在设备管理器里右键设备选中“端口设置”里的“高级”把 COM 端口号改到 COM1-COM4 这种低编号区间。改完端口号问题依旧这就说明不只是端口号的问题了。我换了个思路用只读方式打开串口试试结果能打开这说明串口本身是通的。那“发数据没反应”是怎么回事这时候我怀疑到了驱动层级。CH340 的驱动版本很乱Windows 10 和 Windows 11 自带的驱动有时会跟设备不兼容表现为“能识别但通信不正常”。我把工控机上的 CH340 驱动卸载掉重新装了官方最新版结果问题解决了。但我没有就此打住因为这种“卸载重装”式的修复很可能只是碰巧撞上了。真正让我意识到这是“假故障”的是一个关键实验把工控机上的 USB 口从 USB 3.0 换到 USB 2.0发现故障概率明显降低。再深挖下去发现设备板上的 CH340 芯片是一批早期版本对 USB 3.0 接口的兼容性不好偶尔会出现枚举失败或者通信异常。工控机前置 USB 口全是 3.0所以问题频繁开发笔记本的 USB 口是 2.0所以一直正常。2.3 经验的沉淀串口问题排查顺序表这个案例结束之后我总结了一份串口偶发故障的排查顺序按照“软件到硬件、系统到芯片”的优先级排列排查步骤操作内容验证方法命中概率1检查设备管理器端口编号改到 COM1-COM8 再测试中2检查上位机软件是否只扫描低端口号查看软件配置或文档中3卸载重装最新 USB 转串口驱动官方驱动安装后复测高4换 USB 口3.0 换 2.0故障是否消失或明显减少中5用示波器抓 USB 口数据线和 DTR/RTS 信号观察枚举时序和设备复位信号中6换不同批次 CH340 芯片或换 FTDI 芯片模块对照测试高这里要说一个常被忽略的细节很多 USB 转串口模块的 DTR 和 RTS 信号是直接接到 MCU 的复位引脚和 BOOT0 引脚的。如果你的板子用了这种设计上位机软件在打开串口的时候会自动拉低 DTR导致 MCU 被复位表现就是“打开串口后设备没反应但设备管理器里一切正常”。这不是假故障这是硬件设计上的“真坑”。排查这类问题时不要只盯数据收发留意一下控制线的状态。在串口调试助手里打开“DTR”和“RTS”的勾选项来回切换几次看设备是不是跟着复位了。如果确实存在这个问题要么改硬件设计让控制线不经过复位电路要么在上位机软件里不勾选 DTR/RTS 控制。3. 案例二蓝牙断开的“悬案”靠录屏取证翻案3.1 现象描述蓝牙耳机/设备连接时好时坏完全找不到规律第二个案例是个典型的“玄学”问题一个基于 HC05 蓝牙模块也有用杰理蓝牙方案的版本的设备跟手机 App 连接后经常出现“用着用着就断开但过一会儿又能自动连上”的现象。用户反馈了很多次但开发这边怎么测都测不出来因为断开的时间点完全随机有时候几分钟有时候一两个小时。这个问题让人抓狂的点在于设备端代码看了无数遍逻辑没有任何问题手机 App 端的回调也都正常处理了。但就是断而且断开后不总是自动重连。我决定换个思路先做录屏取证。这个方案其实很简单在手机上调出开发者选项里的“不锁定屏幕”功能打开系统自带的屏幕录制让手机保持录屏状态放在那里同时连接设备进行长时间稳定性测试。录屏是为了拿到断开瞬间的第一手证据包括当时的信号强度、设备的连接状态、App 界面上的告警信息。录了大概四个小时终于抓到一次断开。回放录屏的时候我注意到一个细节断开之前手机上方的蓝牙图标短暂消失了一下然后又出现紧接着 App 就提示“设备已断开”。这个细节看起来不起眼但非常重要因为它说明断开的根因不在 App 层而在系统蓝牙协议栈这一层。3.2 深挖系统层从抓包日志到连接参数既然锁定到了蓝牙协议栈接下来要做的就是抓取蓝牙芯片的日志。Android 手机可以开启“蓝牙 HCI 日志”功能抓下来是一个 btsnoop 文件用 Wireshark 打开就能看到蓝牙控制器和主机之间的所有交互。打开抓包文件我看到了一个可疑现象设备在断开之前有几次 L2CAP 层的“连接更新请求”也就是双方在协商连接参数。具体点说Android 系统发起了从 30ms 到 45ms 的连接间隔更新请求而设备端拒绝了这一次请求但紧接着系统又发了第二次设备端同意了。问题就出在“拒绝”这第一次请求上。HC05基于 CSR 芯片这类蓝牙模块的默认连接参数比较激进而 Android 系统对连接参数有功耗要求两者协商不拢时系统会认为设备不符合规范几次失败后就主动断开连接。这就是为什么断开看起来“毫无规律”——它跟手机当前的负载、功耗策略、以及设备的应答时序都有关系。解决方向也明确了修改设备端的连接参数让 GAP/连接参数更符合 Android 的期望值。HC05 模块通过 AT 指令可以配置连接参数具体指令格式因固件版本而异一般是 ATPARAM 这类指令把连接间隔从默认的 20ms 改成 30-50ms 区间同时把 slave latency 调低能大幅降低被系统断开的概率。3.3 实操启示录屏取证的价值和注意点在这个案例里录屏取证起到了“翻案”的作用。没有录屏我就没有证据判断是系统层断开还是 App 层断开没有证据就只能靠猜而靠猜的排查方式在偶发 bug 面前几乎必败。录屏取证有几个实用技巧录屏前先把手机的通知栏里所有无关通知清理干净避免通知提示遮挡关键信息。开启“显示触摸操作”功能这样回放时能看到手指的操作路径对排查 UI 操作触发的问题非常有用。用支架固定手机不要手持保持画面稳定。如果涉及设备端的指示灯状态可以在画面里同时放置一个时钟用来对齐不同日志的时间线。还有一个技巧是“屏幕录制 串口日志同步录”。手机开录屏的同时把设备端的串口调试信息也用电脑录下来然后以“设备和电脑之间的时钟同步”为桥梁把两条时间线对齐。这样复盘的时候就能看到“App 界面上发生了什么”和“设备内部发生了什么”的因果关系。对齐时间线的方式很简单在录屏开始时同时给设备和电脑发送一个带串口打印的复位信号录屏里能看到这一刻串口日志里也能看到这一刻。4. 案例三“新旧批次对照”烧录排查一把辛酸泪4.1 现象描述同样代码旧板子能烧录新板子烧不进去第三个案例特别具有代表性一批新型号的板子刚焊好回厂准备批量烧录固件结果 Keil 5 里点下载进度条走到一半提示“Cannot Access Target”或者干脆就是“擦除失败”。而拿了手头旧批次的板子试同样的接线方式、同样的工程配置一把烧进去。这就尴尬了代码肯定是没问题的因为旧的板子能烧板子大概率也没问题因为换了一块新的也一样失败。我首先怀疑的是烧录器的问题。常见的 ST-Link 或者 J-Link在接线较长或者供电不稳的情况下容易出现烧录失败。但这次换了好几个烧录器、换了 USB 线、换了个电脑问题依旧。这就基本排除了工具本身的问题。接下来排查目标锁定在了“新旧批次差异”上。4.2 对照实验设计逐项排除批次差异“新旧批次对照”不是简单地把两块板子放在一起对比而是要设计一个对照实验逐项排查。先排查供电差异。用万用表量了两块板子的 3.3V 电压旧板子是 3.31V新板子是 3.28V都在正常范围内但烧录不进去的板子在上电瞬间会跌到 3.02V 左右。这个电压跌落就是重大嫌疑对象。烧录器在擦写 FLASH 时需要比较高的电流脉冲如果板子上的电源纹波过大或者稳压器带载能力不足就会在擦写瞬间拉低 MCU 的电压导致烧录中途失败。验证方法很简单用一个外接的 3.3V 稳压电源直接给新板子的 MCU 供电断开原本的 LDO 输出然后再烧录一把过。这就确认了不是 MCU 的问题是板子的供电设计或某颗电容的问题。又做了一组对照把新板子上的一颗 22uF 的陶瓷电容换成了低 ESR 的型号再上电测试电压跌落明显缓解烧录也稳定了。最终定位到是某批次物料里的一颗电容 ESR 偏高在脉冲电流下性能不足导致的“烧录假故障”。4.3 更多烧录失败场景的排查串讲从 Keil 到 ESP32“新旧批次对照”这个思路还可以扩展应用到其他烧录场景。比如用 Keil 5 烧录时提示 “No Target Connected” 和 “Cannot Access Target”两者的排查方向不同。“No Target Connected”多半是接线问题、目标板没上电、或者是烧录器驱动没装好而“Cannot Access Target”则往往意味着烧录器已经发现了芯片但和目标芯片的握手失败。这个握手失败的原因可能是芯片的 SWD 引脚被复用占用也可能是芯片进入了低功耗模式还有一种可能是芯片本身已经被锁死需要先用串口工具擦除或者用烧录器的“unlock”功能解锁。如果你用的是 ESP32 系列烧录失败的排查思路又有不同。ESP32 的烧录模式要求板子在复位后保持 IO0GPIO0为低电平才能进入下载模式。很多时候烧录失败是因为 GPIO0 被外部电路拉高了或者复位时序不对。用 esptool.py 这类官方工具时它会先自动控制 DTR/RTS 来切换复位和 IO0 状态但如果你的板子没有自动下载电路就会一直卡在“Waiting for download”阶段。解决办法是手动按住 BOOT 键点击复位然后松开 BOOT 键让芯片进入下载模式。还有 GD32 和 CH32 系列它们和 STM32 的烧录方式不同。GD32 可以用串口 ISP 或者 SWD但部分型号的串口 ISP 下载需要特定的 BOOT 引脚配置CH32 系列支持 WCH-Link 下载也可以用串口下载但要注意新批次的 CH32X035 这类芯片烧录工具的版本可能要更新到比较新的版本才能支持。遇到“能识别到芯片但烧录一直失败”的情况优先去官网下载最新版本的烧录工具而不是怀疑板子坏了。为了让你少走弯路我把几种常见的烧录失败现象和排查方向整理成了速查表现象首要排查点次要排查点特殊场景Keil 提示 Cannot Access Target复位电路、SWD 引脚被占用供电是否稳定芯片锁死需先解锁Keil 提示 No Target Connected接线、目标板供电驱动安装ST-Link 固件升级ESP32 Waiting for downloadGPIO0 电平、复位时序自动下载电路接线手动按 BOOT 进入下载模式GD32 串口 ISP 失败BOOT 引脚配置串口芯片电平是否匹配波特率误差过大CH32 烧录中途失败烧录工具版本供电瞬态跌落换批次芯片对照测试烧录校验失败时钟晶振是否起振FLASH 写保护芯片体质问题4.4 烧录问题背后的“批次”思维这个案例的核心收获不是“电容 ESR 偏高导致烧录失败”这个结论本身而是“新旧批次对照”这个方法。具体操作是这样的拿到一块无法烧录的新板子先从旧批次中挑一块功能完好的板子作为对照组在完全相同的环境下烧录。如果旧板子能烧就基本确定问题出在新板子的硬件差异或物料批次上。然后用对比法逐步缩小范围把新板子的电源部分、晶振部分、SWD 部分的元器件参数和旧板子逐一比对把新板子和旧板子的关键电压电平量一遍必要时用飞线把旧板子上某个可疑元件替换到新板子上做交叉验证。这种对照法同样适用于其他场景。MCU 主频不对拿新旧两块板子分别输出 PWM用示波器量实际频率看是否因为新批次的晶振负载电容不匹配导致频率偏差。通信距离变短了拿新旧板子的射频匹配电路做对比用网络分析仪看 S11 参数确认是否因阻抗失配导致发射功率回退。功耗异常偏高对比新旧板子的静态电流逐模块断开供电找出差异点。批次性问题最坑的地方在于它不会让你完全不能用而是“偶尔不能用”或者“某些功能弱了一点”。这种灰度的故障最难定位因为单看一台设备你很难判断是设计问题还是个体问题。只有把新旧两块板子放在一起让差异浮出水面才能把模糊的“感觉不对”变成明确的“这里不一样”。5. 偶发 bug 排查的底层逻辑与一套可以抄作业的流程5.1 三条核心方法论隔离、取证、对照三个案例讲完回头总结你会发现它们背后的方法论是相通的。第一条是隔离变量。串口假故障案例里代码、板子、电脑三个变量通过换机、更换驱动、替换 USB 口逐步隔离最终定位到 USB 3.0 接口兼容性上。隔离变量的本质不是“挨个试”而是每次只改变一个变量并记录结果。只有严格控制变量的实验才能告诉你因果关系的方向。第二条是取证优先。蓝牙断开的案例里如果不是录屏拍到了蓝牙图标先消失再出现这个细节我大概率会陷入“改代码-无效-再改代码”的死循环。偶发问题的特点决定了它不会在你盯着看的时候出现所以必须借助工具把现场记录下来。录屏、串口日志、蓝牙抓包、逻辑分析仪的数据都是在案发现场找证据。第三条是对照实验。烧录失败的案例里“新旧批次对照”就是把两个高度相似但行为不同的对象放在一起通过差异点反推问题根源。对照实验对偶发问题尤其有效因为偶发问题往往不具备“稳定的单一状态”你无法在同一个对象上做“改了以后会不会好”的验证因为你不能确定它是真的好了还是偶然好了。但两个对象之间的确定性差异是最好的线索。5.2 一套可以实操的排查流程模板结合三个案例我梳理了一套通用排查流程可以直接复制到你的项目里使用。第一步明确故障定义。写清楚“什么场景下做了什么操作出现了什么现象”把复现步骤和现象描述从“偶尔不行”细化为“每次执行某个特定操作有百分之多少的概率失败”。这一步决定了后面所有排查的效率。第二步建立嫌疑清单。列出所有可能导致这个问题的因素从代码逻辑到硬件设计、从驱动兼容性到信号完整性、从环境因素到批次物料能想到的都写下来。不要急着排除先列全。第三步设计隔离实验。从嫌疑清单里挑出最容易验证的一项设计一个最小实验来验证它。记住一次只验证一个假设。如果实验没有命中就从清单里划掉接着验证下一个。第四步取证和留痕。任何一次故障发生都要留下证据。录屏、截图、日志文件、串口打印全都保存好。如果条件允许把当时的核心参数固件版本、编译时间、板子序列号也一并记录。这样万一后面需要复盘资料是齐全的。第五步对照与交叉验证。当嫌疑集中到具体的硬件或物料时用新旧批次、不同来源的板子做对照。有条件的话把可疑元件互换测试用结果直接说话。第六步修复和回归。修复后不要只验证一次要做一个压力测试。比如原来一小时出现一次修复后至少要连续测试四个小时不出现才能算基本通过。如果能设计一个加速复现的方法比如提高波特率到极限值来压测串口稳定性或者频繁切换蓝牙连接状态来压测连接逻辑会更快得到结论。5.3 偶发 bug 排查的认知门槛最后说个比较务虚、但我觉得最核心的事。接到偶发 bug 的报告最忌讳的第一反应就是“不可能”或“我代码里没这个问题”。这种防御性心态会直接影响你的排查思路让你把所有异常都归因到外部环境、用户误操作结果绕了一大圈最后还是回到自己身上。真正有用的心态是“暂定凶手是所有人”。把代码、硬件、工具链、环境、用户操作全部列为嫌疑人然后逐个审问。审问的唯一依据是证据不是直觉。这种心态不是说你要去怀疑每一行代码而是要避免在没有任何证据的情况下给问题定性。实际调试过程中我发现偶发 bug 还有一个天然特性它会导致排查者丧失“信噪比”判断能力。一次测试通过了你觉得修好了下一次测试失败了你又陷入焦虑。这种情绪波动非常消耗精力。所以我在操作中给自己定了个规矩拿到结论后不急着发消息说“修好了”先做一轮至少两小时的压力测试确认稳定了再跟团队同步。这既是对自己负责也是对团队负责。我在实际调试中还有一个习惯每定位一个偶发 bug就顺手写一条排查笔记。不用写得很长就记下现象、排查过程、最终根因、验证方式这四列。时间长了这就是一套专属的“偶发 bug 排查案例库”。下次再遇到相似的问题直接按图索骥能省下大量的重复劳动。这个方法强烈建议你也试试。