干嵌入式这些年,我最怕的不是必现的bug,而是“偶发”两个字。你正在调试串口,数据流跑得好好的,突然就卡住不动了,过一会又自己恢复;蓝牙连上设备,用着用着就断开,谁也没碰它;烧录固件,同一台电脑同一个调试器,十次里有那么一两次就是烧不进去。客户报障说产品有问题,你跑到现场复现,它偏偏又一切正常。这种问题最磨人,因为“重现-定位-修复”的常规套路根本走不通。我自己的经验是,偶发bug的排查重点往往不在代码本身,而在外围环境:串口假故障靠换机排除,蓝牙断开靠录屏取证,烧录异常靠新旧批次对照。这套三板斧帮我解决过不少看似无解的疑难杂症,今天把完整思路和实操细节整理出来,希望对同样被偶发bug折磨的工程师有用。
1. 偶发bug为什么难搞:先给问题定性
1.1 偶发bug的本质:时序、环境、批次三大变量
偶发bug难搞,本质上是变量太多,而我们能观察到的信息太少。硬件领域的偶发问题,我归纳下来基本逃不出三个变量。
第一个是时序。上电时序、复位时序、通信时序、中断响应时序,任何一个环节出现微秒级的偏差,都可能导致偶发故障。比如MCU上电后引脚电平还没稳定,外设就已经开始初始化;或者调试器在目标板复位完成之前就尝试连接,导致烧录偶发失败。这类问题跟天气一样,时好时坏,但根因是确定的。
第二个是环境。电源波动、电磁干扰、温度漂移、湿度变化,这些外部因素会直接影响硬件行为。我遇到过一台设备在实验室跑一周都没事,装到客户现场之后每天随机死机一次,最后排查发现是现场的大功率变频器造成了电源谐波污染。环境变量最难复现,因为你不可能把客户的现场整个搬到实验室。
第三个是批次。同一款产品,不同批次之间可能存在物料差异、焊接工艺漂移、固件版本不一致。往往是这批板子没问题、下一批板子突然批量出现“烧录成功率低”或者“偶发通信失败”。这时候你手里的原理图没变、代码没变,但硬件已经不是原来那批硬件了。
软件领域的偶发bug也类似,大多数跟状态、并发、缓存有关。比如某个全局变量在某些操作路径下没初始化,或者多个线程竞争同一个资源。还有一类看起来很像偶发bug、其实必现的问题是环境不一致造成的,比如Gradle构建脚本报bug! exception in phase 'semantic analysis' in source unit '_buildscript_',本质是不同机器上的Gradle版本、JDK版本、插件版本不一致,构建环境有差异,不是代码逻辑的锅。
1.2 排查前的动作:先把现场“冻结”下来
偶发问题第一步不是动手改代码,而是固定现场。无论硬件还是软件,都要先回答三件事:
- 什么条件下触发?供电方式、使用时长、环境温度、负载情况、操作序列,每个细节都可能相关。
- 多久出现一次?一天一次还是一小时一次,跟开机时间有没有关系,跟使用频率有没有关系。
- 有没有日志或截屏?日志级别够不够、时间戳对不对、截屏能否还原当时的操作步骤。
我习惯做一个“必现性测试表”,把操作步骤、环境参数、是否复现逐条记录下来,连测十次以上。如果十次都不复现,就换思路去查外围环境,而不是继续死磕同一个操作路径。对于软件bug,还可以用“添加日志点”代替“打断点”,因为偶发问题你根本不知道什么时候会触发,断点一停就改变时序,反而干扰复现。日志点只记录不阻塞,对原系统影响最小。
在项目里,我会把复现步骤和现场信息直接登记到禅道上,并且配置了创建bug时自动抄送相关同学,这样测试、开发和硬件工程师都能第一时间看到并补充信息。禅道本身就支持自定义通知规则,也可以用API做Webhook推送,具体方式后面讲。
2. 串口假故障排查:换机排除的几个层次
2.1 串口假故障的典型表现与根因
串口“假故障”是什么意思?就是产品本身没问题,是调试工具链路出了问题,导致你误判为产品有bug。这类问题在串口通信领域特别常见,因为串口调试链路里环节太多:USB转串口工具、驱动、调试软件、线材、电平适配,任何一个环节“状态不对”,都表现为“串口偶尔收不到数据”“偶尔乱码”“偶尔打不开端口”。
假故障的典型表现是:设备正常工作时串口日志突然中断,但过一会自己恢复;或者串口助手提示端口被占用,但你看不到任何程序在用;又或者同样的代码和硬件,换一台电脑就好,换回去又坏。
我遇到过最典型的场景:客户反馈设备偶发通信失败,我带着逻辑分析仪过去,结果逻辑分析仪采到的波形完全正常。后来发现是客户用的USB转串口线老化,内部屏蔽层断裂,导致偶发性误码。换一根线之后问题彻底消失。这种就是教科书式的“假故障”,不是你产品的锅,而是你“观察世界的窗口”出了问题。
还有一类隐蔽的假故障来自虚拟串口软件。有些调试环境为了模拟串口会装VSPD之类的虚拟串口工具,这些工具会占用端口号,当虚拟串口和真实USB转串口工具的端口号冲突时,就会出现“串口打不开”“一打开就报错”的偶发现象。判断方法很简单:拔掉USB转串口,看设备管理器里的COM口是否还在。
2.2 换机排除的操作顺序
“换机排除”的核心思路是替换法,通过逐步替换链路中的每个环节,把故障范围缩小到一个点上。串口链路的替换我一般按下面五个层次来:线材、工具、电脑、驱动、电平。
第一层换线。串口线看起来都长一样,但线序、屏蔽、线径差距很大。RS232线要特别注意Tx/Rx是交叉还是直连,USB转TTL线要确认是3.3V还是5V电平版本。怀疑线材时别用万用表量通断就算完,要替换一根全新的线再测。
第二层换工具。USB转串口芯片有很多种,常见的包括CH340、CH341、FTDI、CP2102。不同芯片的驱动稳定性和兼容性差异很大。CH340在Windows 11上偶发“设备无法识别”,FTDI在macOS上偶发掉线,CP2102在Linux下偶发枚举失败,这些都是我实际踩过的坑。遇到偶发问题,直接换一个不同芯片方案的USB转串口工具,是性价比最高的排查手段。
第三层换电脑/端口。USB口供电不足、USB 3.0接口的兼容性问题、静电导致枚举失败,都可能让串口偶发失效。建议把USB转串口插到机箱后置USB 2.0口测试,用一根尽量短的USB线,排除供电和信号完整性问题。
第四层重装驱动。CH340的驱动在Windows下偶尔会进入异常状态,设备管理器里能看到设备,但一打开就是“端口被占用”或“参数错误”。解决方法不是重启电脑,而是右键卸载设备、勾选删除驱动程序、重新扫描硬件,再装一次驱动。
第五层换电平。这个是串口链路里最容易被忽视的环节。MCU的UART电平通常是3.3V或1.8V,而经典USB转TTL工具默认是5V或者3.3V。如果把3.3V的MCU接到5V的TTL工具上,短期能通信,长期会把MCU的IO口打坏,呈现出“用几天之后偶发通信失败”的故障。如果MCU电平是1.8V,那更要注意,必须做电平转换。
除了链路环节,串口调试助手本身也可能有问题。部分助手在高波特率下数据处理有缺陷,115200只是基本要求,460800、921600这些高波特率下丢包、乱码是常事。我排查串口问题时一般会准备两三个调试助手,遇到“模棱两可”的现象先换一个助手对比,低成本高收益。
2.3 电平适配与驱动层的坑
说到电平转换,网上流传的“串口3.3转1.8V电平转化三极管电路”,我实际验证过,能用但有很多限制。单三极管反相电路有个天然问题:信号会被反相。如果你的MCU TX经三极管输出到对端RX,必须确认逻辑极性是否反了,否则通信全是乱码。另一个问题是带宽和开关速度。三极管在低频时开关特性还行,到了115200以上波特率,上升沿和下降沿会变缓,波形畸变导致偶发误码。我的建议是:如果波特率不超过57600、对成本敏感,可以用三极管电路做验证,但正式产品或者调试高频通信,直接用TXS0108E、SGM4553这类专用电平转换芯片,省心很多。
UART通信偶发丢数据,除了电平问题还要看DMA配置。串口DMA接收时,最容易出问题的地方是半字中断、传输完成中断和空闲中断这三个中断的时序关系。常见的坑是:DMA接收缓冲区满了触发传输完成中断,但此时CPU还在处理上一次的数据,新的数据已经覆盖了缓冲区。这种问题平时不出现,一旦业务代码里加了耗时操作,就变成“偶发性丢包”。排查方法是在DMA中断里加一个计数器,丢包时能看到计数器异常跳动,再调整中断优先级和缓冲策略。
Linux下从串口接收数据丢失也是经典问题。多数情况下不是内核驱动的问题,而是应用程序读串口不及时,或者termios的原始模式配置不对。串口缓冲区只有几十KB,应用层读得慢一点,数据就被新数据覆盖。排查时用stty -F /dev/ttyUSB0 -a查看配置,用dmesg | grep tty确认内核识别情况,还可以先用cat /dev/ttyUSB0做裸收测试,分层定位。
还有一种链路更长的场景:通过网口转串口服务器做远程调试。这种方案的好处是能远程访问设备,坏处是链路更长、问题更隐蔽。串口服务器本身的网络延迟、TCP粘包、字节超时配置,都会让数据看起来像“偶发丢失”。排查这类问题时,先绕过网络,直接在现场用USB转串口测物理串口,如果物理串口没问题,再回头查网络链路。
3. 蓝牙断开类故障:录屏取证的艺术
3.1 为什么蓝牙问题必须“有视频有真相”
蓝牙问题的偶发性比串口还强,而且更让人头疼的是,绝大多数蓝牙芯片不会把日常断连的原因写进日志里。BLE协议栈里有个连接监控超时参数,默认情况下超过一定时间没收到对端的包就判定连接断开,但具体是为什么没收到包——是距离太远、数据拥塞、设备休眠、还是干扰,协议栈通常不会告诉你。
所以蓝牙问题最靠谱的证据就是录屏取证。一口咬定“蓝牙老是断开”的用户,你让他描述具体场景,他说不清楚;你让他录个屏,问题一下子就具体了:原来他每次都是把手机放进裤兜、转身走了两步之后才断开——这就是距离和人体遮挡导致的信号衰减。如果不录屏,你根本不可能从他嘴里问出这一步一步的真实操作序列。
录屏的价值还不止于此。蓝牙断开往往有一个触发前置事件:可能是按下某个按键后3秒断开,可能是打开某个App后断开,也可能是靠近微波炉、蓝牙网关、USB 3.0设备时断开。这些环境信息和操作序列,只有录屏能完整还原。就算你人在现场,也不可能时刻盯着用户的手和周围环境。
3.2 录屏取证的具体操作与信息记录
录屏看起来简单,但取证要有用,必须包含足够的信息。我总结了一套“有效录屏”的标准:
- 打开系统时间显示,或者放一个秒表App在屏幕上,让每一帧都有时间参照。
- 手机录屏时,优先录蓝牙设置页和RSSI信号强度。iOS可以在“设置-蓝牙”里看到已连接设备,Android可以用“开发者选项”里的蓝牙调试信息,或者用nRF Connect等App显示实时的RSSI值。
- 电脑录屏用Windows自带的Win+Alt+R或OBS,同时打开蓝牙设备属性窗口、设备管理器、或者串口日志窗口。
- 如果正在用串口或BLE抓包工具采集日志,录屏时要把日志窗口也录进去,这样后续回放时,画面和时间轴对得上。
- 录屏前后,额外记录环境信息:现场温度、设备距离、是否有大功率无线设备、是否有金属遮挡。
拿HC05蓝牙模块为例。很多人玩HC05蓝牙模块连接不上,最常见的原因是根本没进入AT模式。HC05进入AT模式要求KEY引脚在模块上电时保持高电平,很多新手是上电之后再拉高KEY引脚,这时候模块已经进入透传模式,AT命令当然发不进去。如果用户录屏展示了“先按键、再上电”还是“先上电、再按键”的时序,这个问题一秒就能定位。除了AT模式,HC05常见的坑还包括:配对密码默认1234、主从模式配置错误、波特率不匹配导致“连接成功但收不到数据”。
蓝牙断开类问题还有一个特征:和休眠策略强相关。蓝牙键盘、蓝牙鼠标这类输入设备,为了省电会在空闲一段时间后进入深度休眠,看起来像“断开”了。用户说“蓝牙键盘用着用着就断开”,录屏之后你会发现其实是“放着不动五分钟之后再回来打字,发现要重新连”。这不是故障,是设计行为,但用户感知就是“坏了”。
3.3 不同蓝牙方案的排查侧重
不同蓝牙方案,偶发断连的排查侧重点差异很大。
HC05这类经典蓝牙SPP模块,重点查AT模式、波特率、配对状态、主从模式。还有个容易忽略的点是HC05的STATE引脚和LED指示灯,STATE引脚能反映连接状态,配逻辑分析仪观察比纯看LED闪烁可靠得多。
杰理蓝牙这类音频和透传SoC方案,偶发断连往往和配置工具里的连接参数有关。杰理蓝牙的配置工具里有一堆参数:广播间隔、连接间隔、从机延迟、监控超时。监控超时设置得太短,设备稍微忙一点来不及回包,就被主机判定为超时断开。这类问题有时候调一个参数就好了,但前提是你得先有录屏和日志,确认问题类型确实是“超时断开”而不是“用户主动断开”。
ESP32、ESP32S3这类智能蓝牙方案,问题更多出在软件侧。ESP32的BLE回调函数里不能做耗时操作,否则协议栈的事件处理会被阻塞,导致对端长时间收不到回应,从而触发断连。另外ESP32支持休眠,如果休眠策略配置不对,BLE连接会被系统挂起,表现就是“过一会儿断连”。网上很多“ESP32蓝牙连不上”的问题,最后都指向两个点:一个是天线区域被金属遮挡,一个是电源纹波太大导致射频灵敏度下降。这里有个实操心得:给ESP32做蓝牙调试时,外部电源不要用那种廉价开关电源,用线性稳压或电池供电试试,射频对电源纹波非常敏感。
nRF Connect这个App我几乎天天用。它可以扫描设备、查看广播包、发起连接、读写GATT特征值、甚至能模拟客户端连接断开。排查“为什么App连不上设备”时,先用nRF Connect连一次,如果nRF Connect能连上而你自己的App连不上,那就是App的问题;如果nRF Connect也连不上,那就是设备端问题。electron访问蓝牙设备时也有类似逻辑,Electron的Web Bluetooth API到系统蓝牙层之间有很多权限和配对策略,偶发连不上时优先看操作系统的蓝牙权限设置。
C#如何和蓝牙仪表通信这个问题,我实际做过几个项目。C#端用Windows.Devices.Bluetooth命名空间(UWP/WinRT API)或者InTheHand.Net.Bluetooth(32Feet.NET库)都可以。遇到偶发连接失败,重点排查两点:一是设备的配对状态是否稳定,Windows的蓝牙缓存有时候会坏,需要删掉设备重新配对;二是GATT服务发现和特征值读取的异常处理有没有做好,偶发的DisposedException或者ElementNotFoundException如果没有捕获,程序就会直接崩溃。
MIT App Inventor的蓝牙逻辑图我也见过不少坑。图形化编程最容易犯的错误是:在连接成功事件里再次调用连接函数,或者用一个时钟组件频繁轮询蓝牙数据,导致轮询和接收数据事件撞在一起。这种问题看逻辑图很难发现,但录屏可以清楚看到App卡死的节奏感:每次断开都是时钟触发前后的瞬间,那基本就是轮询逻辑有问题。
4. 烧录排查:新旧批次对照法
4.1 烧录失败的常见“玄学”
烧录失败也经常表现为偶发:Keil5里点下载,偶尔报RDDI-DAP Error;J-Flash连接目标板,偶尔显示“Cannot connect to target”;VS Code里编译成功,却怎么也烧录不进开发板。这种问题最容易让人抓狂,因为编译器和下载器都正常,代码也肯定没问题,但就是偶尔烧不进去。
烧录失败的偶发性,绝大多数逃不出三个因素:调试器连接时序、目标板供电与时钟、Flash算法与芯片型号配置。
调试器连接时序问题:调试器在连接目标板时有一个握手过程,如果目标板刚上电还没完成初始化,调试器尝试连接的时机不对,就会失败。典型场景是“目标板通过USB直接供电,烧录器也通过USB连电脑,两边同时上电”,调试器抢在目标板稳定之前开始握手,自然失败。
目标板供电与时钟问题:Flash写入时电流比正常运行大很多,如果目标板的供电能力不足,烧录时电压跌落,Flash写入就会出错。晶振起振时间慢也会导致类似问题,MCU还没跑起来,调试器已经尝试连接了。
Flash算法和型号配置问题:Keil和J-Flash都内置了Flash下载算法,不同厂商的Flash芯片需要对应不同的算法。如果配置的Flash型号和实际芯片不一致,表现就是“有时能烧进去、有时校验失败”。尤其是国产Flash芯片,和原厂型号的算法兼容性参差不齐,最容易出这种偶发问题。
4.2 新旧批次对照的具体操作
“新旧批次对照”是我个人非常推崇的一种排查方法,特别适用于“之前好好的,突然批量出问题”的场景。核心思路是:用同一套工具链去对比不同批次的硬件,快速区分“软件/工具链问题”还是“硬件批次问题”。
具体操作步骤我总结如下:
- 固定软件环境:同一台电脑、同一个调试器、同一份固件、同一个烧录软件版本。这是对照实验的基础,变量越少越容易定位。
- 分别烧录老批次和新批次的板卡,各烧录10次,记录成功率和失败现象。老批次成功率100%、新批次成功率70%,基本可以锁定是新批次硬件差异。
- 交叉组合:老批次板卡+新批次调试器,新批次板卡+老批次调试器,进一步排除调试器本身的问题。
- 对比原理图变更记录和PCB布线版本,重点看复位电路、下载口、电源、晶振相关的改动。
- 对比物料清单BOM差异,重点关注Flash型号、晶振品牌、LDO型号、退耦电容容值。
我之前遇到过的一个实例:新批次板卡换了国产Flash芯片,Keil里烧录时偶发“verify failed at address”,但老批次板卡用同型号的进口Flash芯片一直稳定。把Keil的Flash下载算法换成国产Flash对应的算法之后,问题消失。这就是典型的批次物料差异导致的烧录偶发失败。
还有个更隐蔽的案例:新批次板卡改版时把SWD接口的SWDIO上拉电阻取消了,导致调试器在连接阶段偶发握手失败。上拉电阻对高速信号完整性、线缆阻抗都有影响,引脚悬空时更容易被干扰。这种问题用万用表量是量不出来的,只有新旧批次对照才能发现。
4.3 批次差异背后的硬件原因
批次差异具体体现在哪些硬件因素上?我列一下我实际遇到过的情况。
Flash芯片型号差异是最常见的。不同厂商的Flash芯片在擦除时间、编程时序、ID识别指令上都有差异,配置的算法不匹配时,轻则烧录速度异常,重则偶发校验失败。解决方法是到Keil或J-Flash的器件库里面找对应型号的算法,找不到就手动添加。
晶振差异容易被忽略。同一标称频率的晶振,起振时间可以从几十毫秒到几百毫秒不等。如果新批次换了晶振品牌,起振时间变慢,MCU在调试器握手完成前还没跑起来,烧录自然失败。这种问题往往和“上电后立刻烧录必现、等几秒再烧录就正常”的现象对应。
供电差异也经常引起烧录问题。新批次如果LDO换成了静态功耗更低的型号,动态响应能力可能变差。烧录时Flash写入电流突发增加,LDO输出电压跌落,MCU供电不足,导致烧录失败。这种现象偶尔会在“用仿真器供电时正常、用目标板独立供电时偶发失败”的场景中出现。
复位电路差异:复位电容如果从100nF改成1uF,上电复位时间变长,MCU启动变慢,调试器尝试连接时MCU还没就绪。之前调试器在复位后等待的时间默认是50ms,如果目标板复位时间超过这个值,就会偶发连接失败。
下载口的上下拉电阻:SWDIO和SWCLK引脚如果浮空,线缆稍微长一点或者现场电磁干扰大一点,就可能出现握手失败。新批次如果省掉了这两个电阻,偶发烧录失败率会明显上升。
ESP32烧录还有自己独特的一环:FlashDownloadTool里有个“连接超时”参数,ESP32进入下载模式要求BOOT引脚拉低再复位,如果BOOT引脚电平在上电瞬间不稳定,或者连接超时设置太短,就会出现“有时能进下载模式、有时进不去”的偶发失败。这个问题的排查方式是:先手动把BOOT拉低、按复位键,看是否能稳定烧录。如果能,就说明是自动下载逻辑里的时序问题,在工具里加大超时时间或者优化电路的上电时序。另外ESP32到底能不能烧录成功,USB转串口芯片的质量也很关键,劣质CH340在高速传输时数据容易出错,ESP32的烧录虽然不要求多快,但数据完整性要求还是高的。烧录失败时先看报错里有没有“A fatal error occurred: Failed to connect to ESP32”,有的话先检查串口工具,别急着往固件上怀疑。
J-Flash烧录Motorola S-record(S19)文件时,偶发失败通常和文件本身有关。S19文件的起始地址、数据长度、校验和只要有一处不对,烧录就会失败或部分段烧不进去。排查时先用J-Flash的“Open data file”功能检查文件内容,确认地址段和目标Flash的起始地址对齐。S19和HEX文件格式不同,前者是按地址记录的,后者也是按地址记录的,但两者的地址解析规则有差异,混用容易出问题。
STM32 USB烧录的偶发失败,多半在BOOT0引脚和软件跳转逻辑上。USB DFU模式要求BOOT0上拉高电平,如果开发板上BOOT0的状态在每次上电时不稳定,就会出现“有时进DFU、有时正常跑用户程序”。解决方法是硬件上给BOOT0加一个可靠的上拉或下拉,并且在上电时检测BOOT状态。
C6748串口烧录也是对时序和波特率高度敏感的操作。串口烧录通常要求在上电后非常短的时间内进入Boot模式,如果波特率配置略高、线材质量不好、USB转串口工具延迟偏大,就会偶发“握手失败”。这种时候换一个USB转串口工具、降一档波特率,成功率往往立刻提升。这也印证了我前面说的串口假故障问题:烧录这件事本身也依赖串口链路,链路不可靠,烧录就会偶发失败。
海思烧录工具对连接方式更敏感。用USB转串口线和网口烧录的时序完全不一样,工具里如果配置了错误的接口类型,就会出现“有时识别不到设备”。遇到这类问题,先确认工具版本和目标芯片型号是否匹配,再用新旧批次对照验证是工具链问题还是硬件问题。我见过有人纠结了一周,最后发现是海思烧录工具版本太老、不支持新批次芯片的新型号,升级工具后一切正常。
5. 排查工具箱与日常防坑
5.1 常用工具组合
排查偶发bug,手边的工具直接决定了效率。我的常用组合如下:
串口链路:逻辑分析仪、Usb转TTL工具、USB转RS232工具、带时间戳的串口调试助手、虚拟串口软件。逻辑分析仪一定要买采样率够高的,至少要能采1Mbps以上的串口信号,否则115200波特率的波形看不出毛刺。串口调试助手我习惯同时装两三个,比如sscom、串口猎人、MobaXterm的串口模式,遇到异常时交叉对比,能排除调试助手本身的问题。
蓝牙链路:nRF Connect手机App、蓝牙USB抓包器(如Ubertooth One或Nordic的Sniffer配合Wireshark)、BLE调试开发板。蓝牙协议Core V5.3的规范文档我建议常备一份PDF,遇到连接参数、事件类型、GATT服务定义的细节时直接翻标准,比网上零散的资料靠谱得多。
烧录链路:J-Link、ST-Link、ESP32 FlashDownloadTool、Keil MDK、J-Flash。烧录工具别贪多,稳定最重要。我主力用的是J-Link,兼容性好、驱动稳定,遇到目标板电力不足或者复位时序问题时的容错能力比普通下载器强很多。
软件调试链路:Gradle构建日志分析、前端DevTools的Logpoint、禅道bug管理、录屏软件OBS。软件偶发bug排查时,Gradle的--info和--stacktrace参数建议开起来,报错信息里往往直接带着root cause。
5.2 bug管理记录习惯
禅道能不能提bug自动抄送?能。禅道自身支持在项目配置里设置“抄送人”,但这需要每次提bug时手动选。更自动化的做法是用禅道的Webhook,配置一个自定义脚本,在bug创建事件触发时通过企业微信、钉钉或飞书机器人推送给相关成员,实现真正的“自动抄送”。我团队的做法是写了一个监听禅道Webhook的Python脚本,挂在服务器上,一旦有bug状态变为“激活”或“新增”,就推送到项目群。实测下来,测试人员提bug之后不用再手动@谁,开发人员也不会漏掉通知。
表格化记录是我特别推荐的习惯:
| 字段 | 内容 |
|---|---|
| 触发场景 | 供电方式、负载、操作序列、环境温度 |
| 出现频率 | 每N小时/每N次操作/特定操作前后 |
| 现场留证 | 串口log、录屏、抓包文件、照片 |
| 当前假设 | 替换法定位到哪个环节了 |
| 涉及批次 | 板卡批次、物料批次、固件版本 |
每次排查完偶发bug之后,我会把这些信息整理到项目的wiki里,形成“偶发问题排查档案”。这些档案是以后排查类似问题的金矿。比如我积累了“新批次Flash烧录偶发校验失败”的案例之后,再遇到类似问题,直接翻档案就能定位,不用再从头排查。
5.3 软件bug与硬件bug的边界判断
最后一个想聊的,是判断前后端bug、软件bug和硬件bug的边界问题。偶发问题的归因一旦错了,排查方向就会跑偏,浪费的时间比问题本身还多。
我的经验是:先分清故障发生在哪个层次——用户操作层、接口层、服务层、硬件层。用户操作层包括:操作时序、操作方式、环境位置。接口层包括:协议格式、数据内容、通信参数。服务层包括:业务逻辑、状态管理、并发控制。硬件层包括:电源、时钟、信号完整性、物料批次。
举个例子,用户报“App里蓝牙偶发断开”,第一件事不是打开蓝牙协议栈文档,而是先问:是只有一个用户这样,还是所有用户都这样?如果只有一个用户,优先怀疑用户手机型号、系统版本、使用环境。如果所有用户都这样,再看设备端和App端的日志,确认断开时是设备主动断还是手机主动断。如果日志也看不出来,就上录屏取证。
软件bug里面其实也分“真偶发”和“假偶发”。有些软件bug看起来偶发,其实是缓存没刷新或者状态机没有进入预期状态。比如“mechanical的材料视图bug”,材料变了但视图没更新,本质是缓存失效策略的问题,不是随机故障。“游戏购买商品的bug”,支付回调并发冲突,本质是并发状态管理问题,也是确定性复现的,只是触发条件苛刻。
开发环境问题也经常伪装成“偶发bug”。Gradle构建脚本里那个bug! exception in phase 'semantic analysis',我后来定位到是Git分支合并时build.gradle里的语法冲突没检查出来,导致某些机器上构建必现失败,有些机器上因为缓存了旧构建结果而“看起来没事”。Ubuntu 24.04中文残留bug也类似,系统语言包和应用程序字符编码不匹配导致的显示问题,和硬件故障没有任何关系。
所以说,偶发bug排查的本质是变量隔离。你手里的牌永远只有两个方向:把环境变量一个个固定住,对照实验;把故障现场尽可能完整地记录并还原。串口问题用换机排除,蓝牙问题用录屏取证,烧录问题用新旧批次对照,这三板斧的核心都是在做变量隔离。
我个人在实际操作中的体会是:遇到偶发问题不要慌,更不要急着改代码。先列变量、做对照、留证据。很多看似玄学的bug,最后查出来都是线材老化、驱动版本、批次物料差异、时序冲突这些外围问题。项目里如果早早建立起“三板斧”流程——换机排除、录屏取证、新旧批次对照,后面再遇到偶发bug,至少能少走一半弯路。