ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

偶发bug排查实战:串口、蓝牙与烧录问题的定位思路

2026/10/1 15:04:52 拓冰建站 浏览量
偶发bug排查实战:串口、蓝牙与烧录问题的定位思路 能稳定复现的 bug 是天使偶发 bug 是魔鬼。做嵌入式开发的这几年我对这点体会太深了。你盯着代码看一天它就是不犯病一到客户现场、一到批量生产、一到演示给领导看的时候它就给你来个串口乱码、蓝牙闪断、板子行为异常。而且最气人的是你刚准备抓现场它又好了。这种“偶发 bug”之所以难搞不是因为它真的没有原因而是因为证据链断了——你不知道它是在什么条件下触发、哪个环节先出错、又是哪一层的问题。这篇文章我就拿三个我实际踩过的坑来拆串口假故障怎么用换机排除法定位蓝牙偶发断开的现场怎么靠录屏取证留住证据以及新旧批次板子行为不一致时怎么从烧录环节入手做排查。不管你是做单片机、Linux 驱动、还是搞蓝牙外设和量产导入这三套思路应该都能直接用上。1. 偶发 bug 的排查底层逻辑先对齐坐标系再谈定位1.1 偶发 bug 为什么难难在“证据链断裂”你可以回忆一下自己处理过的偶发 bug它们几乎都有三个共同特征触发概率低、外部条件复杂、复现瞬间极短。就拿串口偶发乱码来说你可能一天一夜都等不到一次但那次乱码出现时你根本来不及抓波形等你接上逻辑分析仪它又好了。蓝牙断开更夸张往往就发生在你切后台、走远两步、或者是周围蓝牙设备突然多起来的那一两秒等你想起来抓日志链路已经断了系统早就清掉了断连现场。这就是偶发 bug 和普通 bug 最大的区别普通 bug 你只要复现就能定位偶发 bug 你连“复现”本身都很难。我之前带过的一个项目就遇到过这样的问题——底层串口偶尔会丢一个字节大概跑三四个小时丢一次频率低到根本没法用常规打印日志的方式去定位。后来我们是怎么解决的不是靠看代码而是靠搭了一套持续运行的环境把现场数据先留下来等它再犯的时候去翻记录。这件事让我意识到一个道理排查偶发 bug 的第一步不是去猜原因而是先想办法把“发生时的现场”完整保留下来。所以这篇文章所有的思路本质上都围绕一个核心想办法拿到证据再在证据的基础上对齐坐标系。什么坐标系就是你得先确定“这是谁的问题”。你的板子、你的上位机、你的线材、你的软件环境、你的烧录过程……在这么多变量里你得先划出嫌疑范围然后把怪罪对象锁定到某一个环节上。换机排除法、录屏取证法、批次对照法本质上都是这套逻辑在不同场景下的具体操作。1.2 一套通用的三类归因框架环境、时序、批次排查偶发问题我自己的习惯是不急着上工具先按归因框架把自己掌握的线索分个类。经常遇到的偶发 bug原因基本逃不出下面这三类。第一类是环境类。包括供电波动、电磁干扰、温度漂移、接地不良甚至是一根劣质 USB 线导致的电平不稳。这类问题有个典型特征换一个环境就好换一个场景就坏。比如同一个设备在研发桌上跑一晚上没问题拿到车间或者客户现场就频繁出怪事。串口偶发乱码里有相当大一部分其实不是芯片串口外设的问题而是环境噪声耦合进了信号线或者 USB 转串口芯片的供电被劣质 hub 拖垮了。第二类是时序类。包括上电时序不对、外设初始化顺序有竞争、软件里某个超时设置过短、DMA 和中断抢占导致的数据竞争以及蓝牙协议栈里经典的 A2DP 切 SCO 失败、休眠和唤醒瞬间的链路处理异常。这类问题往往发生在“切换、初始化、恢复、休眠”这些边界时刻你的代码在正常流程下跑得通但一旦某个事件提前或者延后了一点点偶发 bug 就冒出来了。第三类是批次类。这也是最容易忽略、但量产项目里最常见的坑——同一份固件烧到不同批次的板子上表现不一样。这里的原因可能出在元件批次差异、PCB 改版导致的布局变化、以及最容易被漏掉的烧录环节本身出了问题。烧录时报“成功”不代表 flash 里的内容和你编译出来的 bin 完全一致。校验和没开、烧录速度过快、烧录器接触不良都可能导致个别 bit 写错而这种错误往往是随机的、偶发的。这三类归因不是要你立刻判断出是哪一类而是要你把后续的排查动作带上方向。环境类问题就往供电、干扰、线材上查时序类问题就往事件触发点和日志时间戳上查批次类问题就往硬件差异和烧录过程上查。下面三个案例正好是这三类问题的典型代表我会把每一步操作和背后的逻辑拆开讲。2. 串口假故障的换机排除法 只换不修也能揪出真凶2.1 “假故障”是真问题的障眼法先分清主机、线缆和目标板串口偶发问题里我第一件想强调的事是很多看着像“单片机串口坏了”的情况其实根本不是单片机的锅。你可以把串口通信链路想象成一条水管水龙头上位机、水管USB 转串口线和电平转换电路、水嘴单片机的 UART 外设。哪一段堵塞或者漏水都会导致“出水不正常”但这不代表水嘴本身坏了。串口假故障这个词说的就是这种“故障现象是真的但故障部位不在你怀疑的那个点”的情况。举一个我印象特别深的例子。之前调试一块板子串口偶发收不到数据大概每十分钟一次一次卡住好几秒然后又自己恢复。我一开始怀疑是单片机 UART 配置有问题翻寄存器、翻 DMA 代码折腾了两天没结果。后来无意中把 USB 线从劣质 hub 换到了主机直连的 USB 口问题居然就消失了。再进一步测发现是那个 USB hub 供电能力不足带载之后电压跌落间接影响到了 USB 转串口芯片的稳定性。所以串口假故障的排查第一个核心动作就是别一上来就把嫌疑锁定在目标板上。你手里有三个可替换的环节——主机包括操作系统、驱动、调试助手、线缆和转接设备包括 USB 线、hub、USB 转串口芯片、目标板包括核心板、底板、供电电路。按“换机排除”的思路你得一个一个换着来测把某个环节从嫌疑名单里划掉最终剩下的那个才是真凶。这跟警察排查嫌疑人是一个道理你手里证据再少只要把没问题的都放走最后那个跑不掉的基本就是问题所在。2.2 具体换机流程 交叉替换的每一步怎么走换机排除法说起来简单但很多人做的时候没章法换了一通最后还是不知道问题出在哪。我建议你按下面的顺序来并且每做一步就记录一次现象不要靠脑子记。第一步先在当前环境下做一次基准测试。用你平时用的串口调试助手波特率、数据位、停止位、校验位都和实际项目保持一致持续发一段时间的数据确认偶发问题是否真的存在。注意这里的“一段时间”要足够长最好跑到问题至少出现一两次否则你后面换了机却看不到问题也没法判断是“换好了”还是“这次没赶上发作”。第二步换上另一台主机。操作系统可以是同型号不同机器也可以是不同系统比如 Windows 换到 Linux用 minicom 或者 Python 写个小脚本做同样的收发测试。如果换主机后问题消失说明嫌疑集中在主机的 USB 口、驱动或软件设置上如果问题依旧说明嫌疑在主机这一侧之外的其他环节。第三步换线、换 USB 口、换 USB 转串口模块。把原来的 USB 线换掉从直连口换到扩展口从 CH340 换成 CP2102或者用一个 USB 隔离器隔离一下电脑端的噪声。这些替换的目的是把“链路”这个变量尽可能清洗干净。在这个步骤里我发现过很多奇怪的问题有的线外表看没问题但屏蔽层已经断了干扰一进来就出乱码有的 USB 转串口模块用的是劣质晶振波特率误差超过 2%短距离通信看不出线一长就开始偶发错帧。第四步把嫌疑转到目标板本身。用一块确认正常的板子接到你的测试环境里再把疑似的板子接到另一台确认正常的主机和线材上形成交叉验证。这样做的意义在于两个变量主机、板子各自都被验证过你就能精确判断问题到底跟随哪一方。如果交叉下来发现问题只跟某一块板子走那就得查这块板子的供电、晶振、电平转换电路、串口引脚虚焊这些硬件点了。整个流程走完你至少能把嫌疑从“茫茫多的可能性”缩小到一个或者两个具体环节。我强烈建议你做一个记录表哪怕就是一张草稿纸把每一轮的操作、环境、现象都写下来。偶发问题隔几小时才出现一次如果你全凭记忆很容易陷入“所有组合看起来都有问题又都没问题”的糊涂状态。这里补充一个细节串口调试助手的设置本身也可能制造假故障。不少上位机软件默认开启了 DTR/RTS 自动复位接 STM32 这类带自动下载电路的板子时每次连接串口都会触发芯片复位表现起来就像“一打开串口设备就重启”“收不到数据”。这不是硬件坏了而是你的调试工具和硬件之间打了个配合失误。遇到这种情况先检查上位机的 DTR/RTS 设置再看板子上有没有上电复位电路不要急着拆芯片。2.3 压力测试脚本 让偶发问题快速显形换机排除法的前提是你能在合理时间内让问题复现。如果问题一天才出现一次你的排除流程会拖得非常久。所以我还有一个经验手动用串口助手点发送按钮效率太低一定要做一个持续压力测试的脚本把链路跑起来。用一个简单的 Python 脚本就能完成这件事核心逻辑就是持续向目标设备发送带特定帧头和校验字的数据同时接收回显检查收到的内容是否和发送的一致一旦发现不一致就记录时间戳和内容。下面这个脚本框架我一直在用你可以直接拿去改改看import serial import time import hashlib ser serial.Serial(COM3, 115200, timeout0.1) frame bytearray([0xAA, 0x55, 0x01, 0x02, 0x03, 0x04]) # 自定义测试帧 count 0 error_count 0 while True: ser.write(frame) time.sleep(0.01) resp ser.read(6) if resp ! frame: error_count 1 print(f[{time.time():.3f}] mismatch, send{frame.hex()}, recv{resp.hex()}) count 1 if count % 1000 0: print(f[{time.time():.3f}] sent {count}, errors {error_count}) error_count 0 # 按千帧统计错误笔便于观察频率这里你可以按实际情况调整帧长、波特率和发送间隔但关键是两点一是要加校验逻辑不能只发不收二是要有时间戳统计这样你能知道问题发生的频率是均匀的还是突发的这个信息会直接影响后续判断。如果问题集中在某个固定间隔之后出现那大概率是缓存溢出、流控或者超时处理的问题如果完全随机就要更多往供电和干扰方向想。还有一种情况是 Linux 环境下用串口收到的数据偶发丢失这种问题我在实际项目里碰到过多次。排查时的重点要放在内核串口驱动的缓冲区、termios 配置以及 DMA 路径上。尤其是 DMA 模式下如果 DMA 描述符分配或者环形缓冲区处理有瑕疵就很容易出现“偶发丢一包”的现象。这类问题用上位机的 Python 脚本也能感知到但定位时需要把串口接收路径上的日志或硬件计数器打开看看丢包到底发生在驱动层还是硬件 FIFO 层。3. 蓝牙偶发断开的录屏取证 让一闪而过的现场变成可复盘的事故录像3.1 录屏取证的核心价值 先解决“没证据没法沟通”的问题蓝牙偶发断开比串口问题更让人抓狂因为它的现场稍纵即逝。你可能正拿着手机连 HC05 模块或者 ESP32 透传设备一边走神一边观察日志结果设备悄悄断了又悄悄连回来了——整个过程可能只有一两秒你根本来不及捕捉任何信息。更麻烦的是“偶发”两个字带来的沟通困境。问题给不了硬件同事复现路径硬件同事给不了协议栈同事日志协议栈同事说“你连 log 都没有我怎么查”然后项目卡住。这是典型的证据链断裂。在这种局面下录屏取证是我用过最直接、最有效的手段。不要小看这一段屏幕录像它的作用不光是让你自己事后能反复看更重要的是它能把“偶发表现”变成一段可共享、可对照、可讨论的时间线素材。我举一个具体案例。之前调试一个蓝牙低功耗外设手机端 App 每隔一段时间就会提示设备已断开。我们改了一版固件把断开日志加得满满当当但就是等不到复现。后来我在测试手机上调了录屏功能连着录了两个小时终于抓到一次断连瞬间。回放录像的时候才发现断开发生的时刻正好是我把手机屏幕从 App 切到桌面的那一瞬间——这个细节在现场根本发现不了只有回放录像才看得出来。后面按这个线索去查果然是蓝牙协议栈在应用切后台时进入了不合理的休眠流程导致 ACL 链路超时断开。没有那段录像这个结论不知道要推多久。3.2 取证操作指南 手机、PC、日志三方同步时间线录屏取证不是随便按一下录制就行。我建议的完整取证方案分三层画面层、系统日志层、现场笔记层三层必须时间同步。画面层在手机上直接用系统自带的屏幕录制功能。iOS 和 Android 都自带录屏但要注意 Android 机型差异部分厂商的录屏会有时间限制或自动停止测试前先确认好。针对蓝牙键盘这类外设问题我建议录屏时把屏幕停留在“蓝牙设置”页面或者调试 App 的页面这样断开时你能在画面里同时看到连接状态的变化。系统日志层Android 手机可以打开开发者选项里的“蓝牙 HCI 日志”和“无线调试日志”断连后可以用 adb 抓取 bugreport里面会有完整的蓝牙协议栈事件记录。Windows PC 上也有类似的东西可以在事件查看器里找 Bluetooth 相关的系统日志或者用 Wireshark 配合支持的蓝牙适配器抓取 HCI 数据包。这一步是定位断连根因的关键素材但前提是你得能在一个日志文件里找到对应的那一秒。现场笔记层这是很多人会漏掉的。我建议准备一个小本子或者手机备忘录录屏期间每隔一段时间记录一下当前场景比如“坐在工位”“走到窗边”“周围有五六个蓝牙设备”、手机距离设备的距离、以及你的操作动作。这些信息看起来琐碎但在分析录屏的时候特别有用因为录屏只能告诉你“什么时候断了”现场笔记能告诉你“那时候我在做什么、周围什么样”。三层的时间同步也很关键最实用的是让录屏画面里出现一个实时时间显示。Android 状态栏本身就有时间PC 上可以在角落放一个带秒数的时钟窗口这样你事后对照日志时就能用录屏里的时间和 HCI 日志里的时间戳做对齐误差控制在秒级就够了。不要只依赖记忆里的大致时间偶发问题对时间精度的要求往往比你想象的严格。3.3 拿到录屏后怎么读 从断连前几秒看出根因端倪录屏拿到手很多人会直接把进度条拖到断开的那一秒然后发现什么都看不出来。正确做法是看断开前五到十秒的完整片段把注意力放在几个关键信号上。第一断连瞬间的链路状态表现。如果画面里的设备状态是先变成“正在连接”再变成“已断开”说明链路层先丢失了配对信息还在系统尝试重连但失败如果直接从“已连接”跳到“未连接”没有任何过渡说明是一次干净的链路关闭。这两种表现在协议栈层面的处理路径完全不同前者大概率是链路超时物理层丢失后者大概率是收到了远程断连请求或者本地主动断开。第二断连前的操作行为。就像我上面说的案例切后台、锁屏、接电话、切换音频输出这些操作都可能导致蓝牙协议栈进入异常状态。尤其是经典蓝牙场景下的 A2DP 切 SCO 模式很多耳机的偶发断连都发生在通话开始和音乐播放切换的瞬间。录屏能直观地记录这种操作和断连之间的时间关系这也解释了为什么我一直强调录屏要和系统日志配合看——操作是画面里的证据断连原因是日志里的证据两者对上了定位就进了一大步。第三周围环境的干扰线索。如果录屏显示断连发生时你正经过一个 WiFi 路由器旁边或者附近有人在使用 USB 3.0 设备那就要高度怀疑 2.4G 频段的射频干扰。蓝牙和 WiFi、USB 3.0 在 2.4G 频段上会互相挤占偶发断连的现场往往就发生在这些干扰源附近。录像里记录不到射频波形但它能记录下你的位置和环境这个信息对后续做干扰验证很有价值。读录屏和读日志结合后常见的结果归纳起来无非三种。第一种日志里看到对端设备发来的断连请求说明问题在对端的策略第二种日志里出现链路超时说明物理层或射频链路出了问题干扰或距离因素优先查第三种日志里显示本地协议栈异常退出或驱动报错说明问题出在本地软件栈、操作系统适配或者芯片驱动上。拿到分类结果你再去拉硬件、协议栈、App 各方的负责人就有了明确的目标不会大家围在一起瞎猜。4. “新旧批次对照”的烧录排查 烧录成功不等于烧录正确4.1 烧录环节导致行为差异的几个隐蔽点量产场景里还有一种特别折磨人的偶发 bug旧批次的板子跑同样的固件稳如老狗新批次的板子偶尔行为异常但按概率来说又不能算“全坏”属于时好时坏。很多人一遇到这种情况第一反应是怀疑新批次的某个元件有问题于是换芯片、换电容、换 PCB折腾一圈发现还是老样子。我的经验是在动硬件之前先花半天时间把烧录环节排查一遍——因为“烧录成功”这个提示比你想象的要不可靠得多。先讲一个我曾经踩过的坑。有一次客户反馈新批次板子偶尔上电后跑飞行为毫无规律。我们换了三块板子反复跑能复现但概率不高。后来我把新批次里表现异常的板子读回 flash 里的内容和编译好的固件 bin 做哈希对比结果发现竟然是不同的一批数据。烧录软件明明显示校验通过但读回来的数据有几个字节就是不一样。最后查到原因是烧录时选择的时钟频率过高加上烧录器夹具接触电阻偏大导致个别地址写入不稳定而烧录软件自带的校验机制恰好没覆盖到那几个区域。这就是典型的“烧录成功但不正确”。除了时钟频率还有几个隐蔽点在烧录排查时非常值得留意。首先是烧录器与目标板之间的电平匹配问题比如用 5V 的烧录器去烧 3.3V 的芯片长期使用可能损伤引脚偶发写入错误其次不同的烧录器对目标板的供电能力不一样如果板子从烧录器取电烧录时电源纹波偏大也会造成偶发失败再次如果你的固件里包含了 Bootloader 和 App 两个区域烧录时地址范围稍有偏差就可能出现 App 偶尔被覆盖或者跳转异常的问题最后还有一种很隐蔽的情况hex 文件和 bin 文件混用地址偏移没对齐表面上功能正常但某个中断向量恰好写到错误的位置上。4.2 新旧批次对照的具体操作流程既然怀疑烧录环节就不能只拿新批次的板子去查你得把新旧批次的对照做起来。这里的核心逻辑和换机排除法是一样的通过对比找出“问题跟随哪个变量走”。我建议按下面五步走。第一步备份旧批次正常板子的 flash 内容。找一个还算健康的旧板子用烧录器“读回”功能把整个 flash 导出来保存成 bin 文件然后计算它的哈希值。这个动作相当于给“正常状态”拍了一张底片后面所有对比都拿它做基准。第二步把当前正在用的烧录文件也做同样的哈希计算。对比一下烧录文件和读回内容是否一致。这一对比就能看出之前烧录过程是否真的把固件完整写进去了。注意如果你用的是 hex 格式要先转成 bin 再对比因为 hex 自带地址信息直接比文件内容容易看不出差异。第三步在新批次的异常板子上做一次完整烧录并读回校验。烧录速度先用保守设置比如 SPI 模式下的低时钟频率然后烧完立即读回全部内容再次哈希对比。如果这次对比一致而且问题不再出现那基本可以断定问题出在之前烧录过程的可靠性上。第四步换不同烧录器对比测试。有条件的话拿 J-Link、ST-Link、串口 ISP 各烧一块同样的板子分别读回校验再上电跑测试。不同烧录器对时序和电平的处理方式不同如果某一种烧录器烧出来的板子故障率明显偏高问题就锁定在烧录器与板子的配套关系上。这个步骤在量产时尤其重要因为很多工厂的烧录工位用的烧录器型号杂、保养差、夹具老化严重都是偶发故障的温床。第五步对比新旧批次的硬件差异。这一步可以和烧录对比同时进行重点看晶振型号、匹配电容、上拉电阻、电源滤波电容、USB 转串口芯片这些与烧录时序相关的电路是否有改版。很多时候新批次的问题根源不在烧录器而在板子供电或时钟电路的变化导致同样的烧录器在新板子上不稳定。这五步走下来你基本能分清问题到底是“烧录过程不可靠”还是“新批次硬件有差异”还是“固件文件本身就不一致”。三者的解决路径完全不同前者优化烧录参数和工位流程后者对比硬件改动点并评估影响最后一种则要追查固件管理流程看看是不是有人拿错了文件或者编译路径有问题。4.3 烧录排查的常见坑和速查表最后我把烧录排查里最常踩的几个坑列一下这些事我几乎每次带项目都能碰上。第一个坑是默认校验不开启或者校验范围不全。很多烧录器软件的“校验”选项默认不是全片校验或者只校验代码区不校验配置区而问题往往就出在配置区。更稳妥的做法是烧录后主动执行一次全片读回再和源文件做哈希对比。虽然慢一点但这一步能挡掉绝大多数烧录环节的偶发问题。第二个坑是烧录速度被盲目拉满。为了赶产量工厂会把烧录速度调到最高但高速烧录对信号质量的要求也更高夹具稍微有点脏或者线材稍微有点长就会开始出现偶发错误。我的习惯是量产时选一个中等偏保守的时钟频率换取稳定性巡检时再用高速模式抽测。第三个坑是烧录器和板子的地线接触不良。特别是用夹具批量烧录的场景测试探针氧化、弹力衰减都可能导致 GND 接触阻抗变大烧录过程就会偶发失败。如果你发现某一台烧录工位的故障率比其他工位高第一个要查的就是夹具和探针。第四个坑是混淆 Bootloader 和 App 的烧录范围。很多芯片的 Bootloader 区和 App 区是分开管理的如果你的量产脚本没有严格区分地址范围每次烧录都相当于把 App 覆盖到 Bootloader 区附近或者把 Bootloader 冲掉表现就是“偶尔上电后程序不跑”“偶尔中断向量错乱”。这类问题很难查因为烧录软件显示成功硬件也没坏但就是行为怪异。建议把量产烧录脚本的地址映射和起始偏移列成一张表逐项核对别光看最后的“成功”标志。下面这张速查表我每次做烧录排查都会打印出来贴在工位上照着上面逐项点检基本不会漏排查项检查内容常见异常表现优先处理动作烧录文件一致性源文件哈希 vs 读回哈希读写不一致、个别字节差异全片读回对比转 bin 后比对烧录速度与时钟烧录器时钟频率、SPI 模式偶发写入失败、校验报错降速重试换保守参数供电稳定性烧录器取电/外部供电、纹波烧录中途掉线、芯片锁死改用外部独立稳压源供电接触可靠性夹具、探针、线缆、GND特定工位故障率高清洁/更换探针检查夹具压力烧录地址范围Bootloader/App/配置区映射程序跑飞、中断异常核对烧录脚本地址配置芯片批次差异晶振、电容、电平转换电路同固件不同表现对照新旧批次原理图/BOM这张表里的每一项都对应着“烧录成功但实际有问题”的某一种实现路径。真遇到新旧批次行为不一致别急着怀疑芯片批次或者代码逻辑先花一天时间把这张表过一遍很多时候就是烧录环节的一个小参数在多台设备之间产生了差异。写在最后的一点个人体会做嵌入式这么多年我越来越觉得偶发 bug 考验的不是智商而是耐心和方法。串口假故障教会我“没有调查就没有发言权”先换机排除再下结论蓝牙断连教会我“好记性不如烂笔头”录屏取证是建立证据链最朴素但最有效的手段新旧批次的烧录问题教会我“永远不要盲目信任工具的成功提示”烧录成功不等于烧录正确哈希对比才是硬道理。这三套方法单独看都不复杂但组合起来就是一种非常实用的工程素养遇到偶发问题先把现场留住再把变量分清楚最后用对比实验锁定真凶。下次再遇到那种“偶尔犯一次、抓也抓不住”的问题你可以试试先把手边能录的录下来能换的换一遍能比对的比一遍——大多数时候藏在暗处的 bug 没那么多只是我们手里缺了足够亮的灯而已。