做过嵌入式开发的朋友,大概率都碰到过这种“见了鬼”的偶发 bug:功能绝大部分时间正常,隔三差五来一次故障;你盯着代码翻来覆去看了好几遍,逻辑上挑不出毛病,一重启又好了;拿去给同事复现,怎么试都不出问题,结果一回到现场又犯。串口通信偶发收不到应答、蓝牙连接时不时断开、烧录器时而能连上时而报错,这类问题最磨人,因为常规的“改代码—验证—再改代码”闭环根本推不动。
我的经验是,偶发 bug 基本都不是凭空出现的,它背后一定藏着某个必要条件。只不过这个条件常常不在代码里,而在物理连接、操作流程甚至物料批次里。这篇文章想聊聊我用过的三个针对性招法:串口假故障时的换机排除、蓝牙断开的录屏取证,以及烧录异常时的“新旧批次对照”。它们不一定能让你一次定位到根因,但至少能把“玄学”问题变成可以正常推进的工程问题。
1. 串口假故障:优先“换机”,而不是优先“改代码”
1.1 一段典型的假故障现象复盘
先还原一个我踩过的真实场景。设备通过 USB 转串口模块和上位机通信,平时工作一切正常,但跑到两三个小时后,上位机偶尔会有一帧数据应答不上来。点名让现场重试,又一直正常。程序里串口初始化我翻了一遍又一遍:波特率、数据位、校验位、停止位全部正确,中断优先级没问题,环形缓冲区也没溢出,还特意把 FIFO 和 DMA 的配置都过了一遍。实在没辙了,把波特率从 115200 降到 9600,故障率确实降了一些,但没有彻底消失。
这时候如果继续在代码里折腾,大概率是浪费时间。后来我随手把 USB 线从电脑前置面板换到后置主板接口,再换了一个 USB 转串口模块,跑了一整天再也没出问题。这个 bug 的根源根本不在应用程序代码,而在物理链路。这类问题我习惯叫它“假故障”:现象表现为软件偶发异常,实际根因在硬件物理层或工具链,和你的业务逻辑一毛钱关系都没有。
那为什么它表现成“偶发”?因为劣质模块、虚焊、供电波动这些因素,都是在某个临界条件下才会显现。表现出来就是时好时坏、时快时慢,极其容易被误判成代码问题。经验数据说话:我排查过的串口偶发问题里,物理连接、电平匹配、驱动/系统、代码逻辑的比例保守估计是 4:3:2:1,代码出问题的概率反而最低。
1.2 把链路拆开:每次只换一个变量
“换机排除”听起来很土,但它本质上是经典的单一变量排查法。你要做的,是把整条串口传输链路拆成一截一截,然后一次只替换其中一段,观察现象是否消失。串口链路每一截是什么,我习惯按这个顺序拆:
| 排查环节 | 涉及对象 | 换机动作 |
|---|---|---|
| UART引脚 | 目标板 MCU 引脚 | 换另一路可用 UART 口验证 |
| 电平转换电路 | 3.3V/1.8V 或 RS232 电平转换 | 换电平转换芯片/改造电路 |
| 物理连接 | 杜邦线、排线、接线端子 | 换线、重新压接、量通断 |
| USB转串口模块 | CH340、FT232、CP2102 等 | 更换模块品牌/型号 |
| USB线及接口 | 线缆、电脑 USB 口 | 换线、换 USB 口(优先后置) |
| 驱动与系统 | CH340/FTDI 驱动、操作系统版本 | 重装驱动、换电脑验证 |
| 串口助手软件 | XCOM、SSCOM、PuTTY 等 | 换软件交叉验证 |
实际操作时,先固定目标板和电平转换部分,只换 USB 转串口模块,这是最容易出问题也最容易忽略的一环。我遇到过一款看起来很新的 CH340 模块,插上能识别、能打开串口,但 115200 波特率下就是偶发丢字节。换了一个老款 FT232 模块之后,问题消失。后来拆开那个 CH340 模块发现,晶振用的不是 12MHz 而是 12.000MHz 的廉价替代,频偏已经超出芯片允许范围。波特率一高,误差累积到采样窗口边缘,就开始随机丢包。这种模块故障,你改一万行代码都没用。
换机排除的另一个重点,是“一次只能换一个变量”。很多人一急起来,同时换线、换模块、换电脑、换软件,结果问题消失了,却根本不知道是哪一步治好的。下次换个环境又复发,等于白忙。我自己的做法是拿纸列个清单,每换一样就在后面打勾,现象消失的那一勾,就是根因所在。
1.3 电平匹配和三极管电路的隐形坑
换机排除还能帮你发现一类特别隐蔽的问题:电平匹配。现在很多新主控的 IO 是 1.8V 逻辑电平,而外接的串口模块或者传感器是 3.3V TTL 电平。两边的“高电平”定义不一样,直接相连时逻辑判定会出现边界情况。
最常见的廉价方案,是用 NPN 三极管做开关电平转换:3.3V 侧通过三极管控制 1.8V 侧的电平。这个电路原理没问题,但它有个天生的弱点——上升沿靠上拉电阻对负载电容充电,速度远不如下降沿。当串口波特率较高时,上升沿还没充到高电平阈值,接收端就已经开始采样了,于是偶发出现错误位。你用示波器去看,原本应该是方波的串口信号变成了梯形波,上升沿明显变缓。
这时候换机排除法就特别能迷惑人:你换了一个模块,因为模块的输入电容特性不同,上升沿恰好够了,问题就“消失”了;过几天换了另一个模块,电容又变了,问题又出现。如果不理解原理,你会以为故障在模块和接线之间飘忽不定。真正的解法是动电路:把上拉电阻从 10k 降到 1k~2.2k,或者改用 TXB0108、PCA9306 这类专用电平转换芯片。如果只是为了验证判断,也可以先降低波特率,看故障率是否明显下降。
另一个容易误判是供电。USB 供电的转串口模块带载能力本来就有限,如果模块还顺带给你板上的传感器供电,睡眠唤醒或蓝牙发射瞬间的电流尖峰能把 VCC 拉到逻辑阈值附近,导致一瞬间的误码。下次遇到偶发串口故障,先拿万用表量一下模块供电脚在通信瞬间的电压跌落,很多“软件问题”当场就破案了。
2. 蓝牙偶发断开:录屏取证,把“玄学”问题变成看得见的现场
2.1 为什么录屏比“口头反馈”有价值
如果说串口假故障是物理层问题伪装成软件问题,那蓝牙偶发断开就是典型的“证据缺失型故障”。硬件产品如果用到了 HC-05 这类经典蓝牙 SPP 模块,或者 BLE 模块,用户反馈最多的就是“设备连不上”“用着用着就断了”。可等你拿着和用户同型号的手机、同样的 App 在办公室测试,连着跑几天一次问题都不出。你能做的只有对着模块说明书干瞪眼。
这时候需要换一种思路:既然问题不能在实验室复现,那就让现场把它“录下来”。让测试人员或用户打开手机自带的录屏功能,把完整的操作过程录下来发给你。别小看这一步,录屏解决的是偶发问题最要命的信息失真问题。
口头反馈为什么会失真?因为很多人描述问题时会自动补上自己的“猜测”和“理解”。比如用户说“蓝牙连不上”,他可能没告诉你:他先打开 App 再开蓝牙;又或者他根本没到系统设置里配对,直接在 App 里点了几次连接就放弃了。这些操作差异,工程师不到现场根本看不出来。而录屏能把操作顺序、系统弹窗、连接时长、异常时机全部记录下来,信息量提升不是一个量级。
我给现场传的“录屏取证模板”很简单,就四步:
- 打开录屏,开启系统时间显示。
- 先进入系统设置—蓝牙,展示设备搜索和配对状态。
- 打开 App,执行一次完整的连接操作。
- 复现异常之后不要立刻关录屏,继续录 30 秒,方便后面数时间轴。
这个模板执行下来,一般都能解决“到底是用户操作问题还是设备问题”的争论。
2.2 从录屏画面反推断连原因的几条线索
拿到录屏之后,不要急着看热闹,要按几条线索去读画面。我总结过几个高频特征,可以直接对号入座。
第一种:系统设置里能看到设备、也配对成功了,但 App 里一直提示“连接失败”。这种情况多半出在 App 的 SPP UUID 或服务发现逻辑上,模块本身没有问题。经典蓝牙 SPP 虽然叫“串口协议”,但连接时还是要匹配 UUID 的,App 里写错了服务 UUID,就连不上。这种问题实验室里用特定手机测不出来,因为有的手机连接管理器会缓存配对信息,绕过部分流程。
第二种:连接是成功了,但过几秒到几十秒就自动断开,重新连又断开。看到这个“固定时间断开”的模式,就要优先怀疑连接参数更新失败,或者设备端进入了低功耗模式后链路被挂起。BLE 模块的 connection interval、slave latency 这些参数如果配得太激进,手机端处理不过来就会主动断开。HC-05 这类经典蓝牙模块没有这么细的配置,但如果供电不稳,模块在发射瞬间复位,也会出现连上后随机掉线。
第三种:断开之后,系统蓝牙列表里设备直接消失了,要重新扫描才能看到。这通常说明模块端掉了电,或者模块进入了 AT 指令模式。HC-05 有个经典的坑:上电前如果按住板上的按键(KEY 引脚拉高),模块会进入 AT 模式,工作波特率变成 38400,此时手机根本不能正常连接它。产线如果贴片时按键被异物压住、或者用户自己玩过 AT 指令,就会出现这种“时好时坏”的连不上。判断方法也简单:录屏里看断开后设备是否从蓝牙列表消失。
第四种:手机系统升级之后才开始频繁断连。现在的 Android 和 iOS 对经典蓝牙 SPP 的兼容性逐年变差,Android 13/14 扫描蓝牙设备需要定位权限,还要申请 BLUETOOTH_SCAN 权限;新版蓝牙协议 Core v5.3 引入的一些新特性,老模块根本不知道,两边在连接参数协商上可能出现问题。这种问题要是现场录屏看得多了,就别再纠结兼容性补丁了,直接评估迁移到 BLE 方案更划算。
2.3 更进一步的取证:HCI 抓包与日志比对
录屏只能证明“视频里那一刻发生了断开”,但要说清楚“断开发生在哪一层”,还得靠协议栈的日志。Android 系统自带了蓝牙 HCI 抓包功能,在开发者选项里开启“Bluetooth HCI snoop log”,断开操作之后,用 adb pull 把日志导出来,文件通常是 btsnoop_hci.log,用 Wireshark 直接打开。
Wireshark 里过滤一下,可以看到连接请求发出、连接完成、断开连接这几条关键事件。最关键的是断开时带一个 Reason code:0x08 表示 Connection Timeout,说明双方在一定时间内没接收到对方的包,链路预算出问题了;0x16 表示本地主机主动终止连接,通常是 App 或系统策略主动断的。这一下就能把责任划分到具体某一端,不用再来回猜。
设备端也别忘了同步取证。把串口调试助手打开,开启时间戳保存功能,把模块的串口输出全部留档。然后和手机端的 HCI 日志做时间轴比对:模块端是不是在某一次收到手机断开指令后直接关闭了连接?还是模块端一直没收到数据,被自己的超时机制切掉了?双向一对照,问题基本原形毕露。
说实话,蓝牙偶发断开是目前我见过最能体现“取证意识”的一类问题。只要养成了“先录屏、再抓包、后分析”的习惯,很多看似无法定位的玄学故障,最后都能落地到某一个明确的协议参数或者供电缺陷上。
3. 烧录异常:用“新旧批次对照”锁定差异化根因
3.1 典型案例:替代料 Flash 导致的 JFlash 校验失败
烧录问题也是偶发 bug 的重灾区,而且它有一个非常显著的特点:往往和硬件批次强相关。同一份固件,老批次板子烧得又快又稳,新批次板子一上产线就频繁报错,烧录器时灵时不灵,产线班长一脸无辜地看着你。这时候别急着怀疑烧录器坏了,先做“新旧批次对照”。
所谓新旧批次对照,就是把一块已知正常的老批次样板和一块新批次板子并排放好,用同一台电脑、同一条 USB 线、同一个烧录器去分别烧录。老批次一切正常、新批次报错,说明问题不在烧录环境,而在板子本身的某个差异上。然后再去 diff 两个批次的原理图和物料清单变更记录。
我自己就碰过一个典型:新批次把 SPI Flash 从 A 厂商换成了 B 厂商的“PIN TO PIN 兼容”替代料,容量、电压、封装都一样,原理图都不用改。结果烧录时 JFlash 报 Verify failed,或者烧进去之后上电读出来全是 FF。原因很简单:PIN TO PIN 兼容只意味着引脚定义一样,不代表烧录算法一样。不同厂商的 Flash 在页大小、页编程时间、状态寄存器定义上都有差异,甚至器件 ID 都不一样。JFlash 里得选择对应的厂商型号,Keil MDK 里也要在 Options for Target—Utilities—Flash Download 中选择匹配的 Flash Algorithm。如果还沿用旧型号的算法,擦除和校验必然对不上。
这个坑说明一个道理:烧录失败不要只盯烧录器和电脑,硬件的“身份信息”已经在物料层面悄悄变了。如果你没有“先问批次再查工具”的意识和习惯,很容易陷入反复重装驱动、换线换电脑的死循环。
3.2 下载模式的物理条件:STM32 与 ESP32 的差异陷阱
新旧批次对照同样适用于下载模式相关的故障。先拿 STM32 举例,串口 ISP 下载时,BOOT0 引脚的电平决定芯片复位后进入哪种启动模式。新批次 PCB 如果改版的时候换了 BOOT0 的上拉/下拉电阻,或者贴片时漏贴了,芯片可能直接跳进正常用户程序模式,根本不会进入 bootloader。于是你会看到:烧录器能识别芯片、通信也正常,但就是写不进去,或者写进去之后不跑。
用 SWD 方式烧录时也有类似的版本差异问题。如果新批次固件里启用了 SWD 引脚复用成普通 GPIO,而调试口没有保护逻辑,那么连接 SWD 就会失败。这时候可以用“Connect under Reset”模式,在芯片复位期间抢占调试接口,再把引脚配置重新刷掉。这个选项在 Keil 的 Flash Download 设置里就有,“Reset and Run”那一页下面。
ESP32 就更典型了。UART 下载模式需要自动下载电路配合:通常用 CH340 的 DTR/RTS 信号经过三极管控制 EN 和 IO0 的时序。新批次板子如果换了 USB 转串口芯片型号、改了自动下载电路的三极管参数,或者把 PCB 上控制时序的电阻电容调整过,就可能出现帖子热词里那种“VS Code 编译成功、却怎么也烧录不进开发板”的现象。此时你可以先把 IO0 手动拉低再给板子上电,强制进入下载模式。如果能正常烧录,就说明问题出在自动下载电路的时序上,然后用示波器量 EN 和 IO0 在点击烧录瞬间的波形,对照老批次板子的波形,找出差异。
还要留意烧录器固件本身。J-Link、ST-Link 的固件版本太旧,对新出的主控批次或新内核支持不全,会导致连接异常。遇到新批次板子烧录失败时,更新一下烧录器固件再做对照,这是成本最低的一个操作。
3.3 对照排查的实操清单与产线注意事项
把新旧批次对照变成可以落地的操作,我一般按这个清单来:
| 排查步骤 | 具体动作 | 目的 |
|---|---|---|
| 保留老批次样板 | 至少留两块正常的样板,贴上标签防止混用 | 提供“已知正常”的参照基准 |
| 固定环境 | 电脑、USB线、烧录器、烧录软件全部不换,只换板子批次 | 保证单一变量 |
| 导出批次信息 | 查 PCB 版本号、ECO/ECN 变更记录、关键物料标记 | 找到差异候选 |
| 记录报错现场 | 每次失败都截图或拍照,拍下器件型号、算法选择、电压选项、报错代码 | 建立证据链 |
| 验证替代料 | 把新批次物料临时换回旧批次物料,看是否恢复正常 | 锁定物料差异 |
产线对照还要多留一个心眼:烧录夹具。新批次 PCB 如果换过表面处理工艺,比如从 HASL 换成 ENIG,测试探针的接触特性会变,压针氧化后接触电阻增大,烧录器会偶发连不上或中途校验失败。这种问题做对照时很容易被误判为“板子不行”,其实只是夹具接触不良。判断方法很简单:同一个 PCB,手动飞线烧录正常,放在产线夹具上烧录失败,那问题就在夹具。
我一直觉得,烧录类问题是最适合用“新旧批次对照”来解决的,因为它的变量边界非常清楚:固件是同一个,工具是同一个,变的就是板子本身。只要养成一上来先问“这是哪个批次的板子”的习惯,一大半的烧录玄学当场就被戳穿了。
4. 回到方法论:偶发 bug 的本质与通用排查动作
4.1 时间和空间:给偶发 bug 分类
把上面三个案例串起来看,偶发 bug 虽然长得千奇百怪,本质上都可以归进两个维度:时间相关和空间相关。
时间相关的偶发问题,是某个触发条件在时间轴上不定期满足。典型例子:蓝牙连接后固定 3 秒断开、串口通信运行两小时后才开始丢包、板子冷机启动不稳定而热机正常。这类问题适合用“取证”思路打:录屏、带时间戳的串口日志、HCI 抓包、示波器长时间记录,把触发那一刻的事件链完整保留下来,再分析共同点。
空间相关的偶发问题,是某个条件只在特定环境下存在。典型例子:只有某个批次的板子烧录失败、只在一台电脑上串口乱码、换了模块就好。这类问题适合用“剪枝”和“对照”组合拳:换机排除是空间维度的剪枝,新旧批次对照是空间维度的差异分析。
用一张表收敛一下:
| 故障类型 | 特征 | 首选招法 | 案例 |
|---|---|---|---|
| 时间相关 | 固定时长或固定操作后触发 | 录屏+日志+抓包取证 | 蓝牙连接3秒后断开 |
| 空间相关 | 换环境/换机器/换批次后消失 | 换机排除+批次对照 | 只有某批板子烧录失败 |
| 混合相关 | 需要特定环境和特定时机 | 取证+对照并行 | 新批次板子在产线上偶发烧录失败 |
分类不是目的,目的是选对工具。拿到一个偶发 bug,先不要急着动代码,问自己两个问题:它是在哪个“空间”出现的?它在出现之前发生了什么“时间”事件?这两个问题的答案,直接决定了你该用换机法、取证法还是对照法。
4.2 工具清单:平时备好,用时才不慌
偶发 bug 的排查很像警察办案,证据链有了,破案就是时间问题。所以工具提前备齐非常重要。我这些年常驻的一个工具清单是这样的:
串口调试助手:XCOM 或 SSCOM 都可以,重点用它的“带时间戳保存日志”功能,不要只当发数据用。
虚拟串口软件:VSPD 这类工具可以虚拟出一对串口,用来模拟测试上位机逻辑,排除串口硬件干扰。
逻辑分析仪:Saleae 或国产 Kingst 的 USB 逻辑分析仪,对付 UART 时序、电平翻转、波形毛刺足够,不需要每次搬示波器。
示波器:排查供电跌落、信号上升沿、EN/IO0 时序这类问题必备,至少两通道、要有单次触发功能。
手机录屏与蓝牙 HCI 日志:Android 开发者选项的“Bluetooth HCI snoop log”,专门对付蓝牙断开类问题。
adb 与 logcat:抓 Android 系统蓝牙错误日志,配合 btsnoop 做时间轴比对。
Wireshark:解析 btsnoop_hci.log 和网络抓包,查看断开 Reason code。
烧录工具组:Keil MDK、JFlash、STM32CubeProgrammer、ESP32 Flash Download Tools、海思 HiTool,每个平台都至少要保住一个“手动模式”的操作路径,便于做自动下载电路的 A/B 测试。
产线管理类:PCB 版本号、ECO/ECN 记录、物料批次号。这些东西平时不显眼,排查批次相关 bug 时就是命根子。
工具的意义不只是“能用”,而是能在关键时刻帮你把偶发变成速发。逻辑分析仪和示波器如果平时不熟,等出问题再临时翻手册,黄花菜都凉了。我建议每个项目至少安排一个人把这些工具的操作流程摸熟,遇到偶发 bug 时直接拉出来用。
4.3 养成三个习惯,偶发 bug 会越来越少
工具和方法都是术,真正管用的是三个小习惯。
第一个习惯:遇到偶发 bug,先把手从键盘上拿开,花十分钟列一张表,左栏写“已知”,右栏写“未知”。很多工程师一着急就改代码,改完发现不是这回事,把现场证据也弄丢了。先列已知和未知,能避免自己在错误方向上消耗宝贵时间。
第二个习惯:一次只改一个变量。换机排除法、新旧批次对照法,本质上都在围绕“单一变量”打转。你改代码的同时换了模块、还更新了驱动程序,就算问题消失了你也不知道是谁治好的。坚持单一变量,哪怕这次没定位到根因,至少拿到了一条可靠的“有效线索”。
第三个习惯:把偶发 bug 的记录写进团队 wiki。问题的现象、排查链路、最终根因、怎么验证的,全部沉淀下来。下次再有人碰到类似问题,直接搜得到,不用重新踩一遍坑。这个习惯坚持一年,团队处理偶发问题的速度会明显提升一个档次。
我自己后来养成的一个小技巧是:复述现场描述。用户或测试说“它坏了”,我会让他们把当时正在做什么、屏幕上所有能看到的东西原样念一遍。这个方法和录屏取证一脉相承——永远不相信二手描述,永远追求让现场自己说话。偶发 bug 再多,只要证据链完整,就没有真正“无法定位”的问题。