搞嵌入式的人,谁没被Bug熬过几个深夜?点灯不亮、串口无输出、偶发抖动、上电莫名重启、跑十分钟死机一次……跟纯桌面软件调试相比,嵌入式排Bug的酸爽之处在于:你面对的不是一段随时能停下来的代码,而是一块连着电机、传感器、通信总线的板子,一段跑在片上Flash里的固件。资源就那么点,动不动就进HardFault,想打印一行日志都得算好波特率和RAM占用。
我按自己多年项目里反复用、验证过真正有效的几种嵌入式排查手段,跟你聊点实在的:从最朴素的串口打印、断点单步,到示波器抓信号、内存越界检测、HardFault栈回溯,再到自动化回归。重点不是罗列工具,而是讲清楚每种方法背后的思路和取舍:什么场景该用哪种、为什么能用、怎么用才不会掉坑。新手可以拿它当调试入门地图,老手可以直接翻后面的排查速查表,也许能帮你少走一次弯路。
1. 别急着开调试器,先把“排查Bug的思路”理顺
1.1 为什么嵌入式调试比桌面软件调试更“熬人”
嵌入式软件调试难,难在三个客观条件。
第一是资源受限。一片中等定位的MCU,RAM也就几十到几百KB,Flash也在百K级别。你不能像在PC上那样把一堆监控代码塞进去,打印日志要纠结多久打一条,缓冲区开多大,串口波特率多少合适。日志打太勤,系统实时性就崩;日志打太懒,关键信息说丢就丢。
第二是硬件强耦合。一个Bug可能不在软件,而在线序、电源纹波、信号完整性、外设时序。你写代码明明没问题,片上外设偏偏不按手册干活,你以为自己在排软件Bug,实际上在跟硬件吵架。还记得调试I2C总线的时候,软件里查了半天寄存器配置,最后是上拉电阻没焊好。
第三是调试器本身会改变系统行为。很多嵌入式系统是实时系统,你让CPU在断点停住,中断照样在触发,外设DMA照样搬数据,看门狗可能先炸了。更气人的是,在实验室接上仿真器一切正常,一拔掉就出问题——处理器跑得快了,时序变了,Bug才暴露出来。这种现象叫“调试器效应”,专治各种不甘心。
所以嵌入式排Bug的第一步,不是马上去连调试器,而是先承认:我能看到的信息是有限的,我要想办法在“尽可能不干扰系统”的前提下,把信息补足。这个思路贯穿下文所有方法。
1.2 动手排查前,先做这三件“笨事”
很多人拿到Bug报告就冲过去改代码,我建议先做三件看起来很笨、但能救你大命的事。
第一,完整记录复现步骤。包括上电顺序、触发操作、供电电压、环境温度、外设接线方式。嵌入式Bug常带有随机性,随手上电一次可能复现不了,复现不了就没法“边跑边看”。我之前排过一个偶发串口乱码的问题,排查了三天,最后发现是板子供电电压在瞬间跌落导致电平不稳,而这个瞬间只在某个继电器吸合时出现。没有那天的操作记录,这个结论根本推不出来。
第二,最小化复现环境。把无关外设全部关掉,把无关功能裁剪掉,把输出全部导向串口或指示灯,让问题以最少的变量暴露出来。变量越少,定位越准。你还得确认编译器的优化级别(O0还是O2)、代码分支、工具链版本。同样一套代码,O0正常、O2出问题,九成是未定义行为(UB)或者volatile缺失——忘了这件事,你可能会在错误的方向上浪费很长时间。
第三,确认“这到底是谁的Bug”。嵌入式里软硬件边界模糊,很多问题其实发生在两边交界处。比如SPI通信出错,可能是GPIO速度配置不对、时钟极性配置反了、线太长干扰大、焊接短路,根本不关应用程序的事。动手调试前先问一句:这个问题在软件层面能完全解释吗?解释不了,就该叫上硬件工程师一起看。
2. 五种主力排查方法的实操拆解
2.1 串口日志法:用好printf,别让它成为系统杀手
串口日志是嵌入式排查Bug的第一主力,设备不连调试器也能用,产线、外场都能干活。但很多人的“串口日志”就是到处printf,然后把系统打崩。
我自己的做法是把日志做成一个小模块,分等级(ERROR、WARN、INFO、DEBUG),用一个编译宏控制哪些等级能输出,调试版开DEBUG,发布版只留ERROR。这样既保证平时代码里“埋雷”的日志不会拖累系统,出问题时又能快速打开。
关键点是别在原函数里直接printf,而是往一个环形缓冲区写日志,让后台DMA把缓冲区内容搬运到串口外设。直接printf进串口,要等发送寄存器空、等发送完成,一次阻塞可能几十到几百微秒。如果在中断服务函数里这么干,实时性直接完蛋;如果波特率不高、日志又长,系统会被拖死。环形缓冲区加DMA,发送变成异步的,打过日志你也几乎感觉不到处理器被占用。
在日志内容上,强烈建议加时间戳。嵌入式里可以选一个微秒级或毫秒级的硬件定时器做时间基准,日志格式类似:
[ERR][00012345us] uart1 rx overrun, rx_idx=32, head=41有了时间戳,你不仅能看执行流,还能算时间差,比如一次任务调用从进入到退出用了多少微秒、中断服务函数最长关闭中断多少时间。这些数据对排查偶发问题非常有价值。
还要留个心眼:有些MCU默认把printf指向半主机模式(semi-hosting),接上调试器还好,不接直接卡死在某个断点或异常里。解决方法是重写fputc或者单独实现日志输出函数,操作UART寄存器即可,千万不要指望IDE默认帮你搞定。另外浮点格式化输出(%f)在部分标准库里会拉入大量代码和栈占用,嵌入式日志尽量用整数打印转换。
实操上还有一个偷懒技巧:写日志的时候给关键路径编号,比如“A1进入中断”“A2退出中断”“B1进入主循环轮询”“B2外设初始化完成”,然后把这些编号对应的GPIO拉高拉低,用示波器去看。这样即使串口来不及打印,逻辑也一清二楚。这个技巧后面详细说。
2.2 在线断点与单步:调试器不是万能的
在线调试(JTAG/SWD)是桌面开发转嵌入式的人最先学的招:设断点,单步,看变量。它当然能干活,但你马上会发现它有不少局限性。
首先,硬件断点数量有限。Cortex-M内核一般也就几个硬件断点,软件断点虽然多,但要往RAM里写指令,有些flash保护场景根本不让你写。数据断点(也叫观察点)更稀缺,Cortex-M通常只有几个,但这是排查内存数据被莫名修改的神器:你设一个数据断点,地址指向某变量,一旦这个变量被写入,CPU立刻停下,你就能看到底是哪一行代码在搞鬼。
其次,断点会干扰实时系统。你在主循环里设断点停住了,定时器中断不会等你,缓冲区可能被填满,DMA可能把数据搬完触发完成中断,看门狗更不会惯着你。所以常见套路是:逻辑Bug单步调试没问题,但一旦涉及中断、DMA、外部信号同步,断点的可信度就比较低。
第三,开优化后断点和单步都容易“看不懂”。O2优化下你写的局部变量可能被优化到寄存器里,单步执行会跳来跳去,断点条件不好使。正确姿势是:需要在线调的时候临时把优化级别降到O0,复现问题,定位根因,改完代码再开优化回归。千万不要在O2下纠结单步行为,那是在跟编译器玩捉迷藏。
使用在线调试时,建议优先用“条件断点”和“观察窗口”的组合:条件断点解决“什么时候”停,数据断点解决“谁改了它”,调用栈解决“从哪来的”。比如一个全局变量在运行中被改成非法值,你可以设数据断点,让它第一次被改时就停住,然后看调用栈中是谁在写,比自己猜快得多。
2.3 波形与逻辑分析仪:把看不见的时序变成图
做嵌入式如果只盯着寄存器值,永远抓不到时序类Bug。这时候就该上示波器和逻辑分析仪。
用得最多的手段是GPIO翻转测试法。事先在代码里预留几个空闲的GPIO测试点,在关键事件入口拉高电平,在出口拉低电平。用示波器挂在这个引脚上,你就能直接“看”到这段代码到底跑了多久。比如怀疑中断服务函数过长,就在ISR入口拉高、出口拉低,测出来的高电平宽度就是中断占用时间;怀疑某个函数在循环里超时,就测相邻两次翻转的时间间隔有没有突变。
这个方法几乎零成本,不改协议、不走总线、不依赖任何调试软件,而且完全不干扰系统运行。跑现场排查的时候,示波器探头往板子上一夹,代码里加两条GPIO操作,真相往往就在波形里。我之前调过一个周期性数据采集掉帧的问题,改成GPIO翻转后一眼看出采集函数在某个条件下耗时翻倍,直接锁定了是等待外设标志位超时导致的,然后修改了等待策略修好。
逻辑分析仪则适合抓协议波形和多个信号之间的时序关系。I2C、SPI、UART这些总线,仪器的解码功能直接就能把二进制解析成帧内容。排查“协议解析错乱”“某字节偶尔丢”这类问题,逻辑分析仪比断点直观得多。抓一次波形,你就能看到从机是否回了ACK、时序是否满足手册要求、字节间间隔是否异常。
需要注意的是:示波器带宽太低会测不准快沿信号,普通排查建议带宽100MHz起步;逻辑分析仪采样率至少要高于信号最高频率的4到8倍,宁高勿低。团队里至少配一台示波器和一台入门级逻辑分析仪,别省这个钱,这两个工具在嵌入式调试里跟螺丝刀一样刚需。
2.4 代码走查与二分定位:没有调试器的绝境求生
有些场景会让你相当绝望:设备发到现场,没调试器,甚至没有串口输出,只能靠现象猜。这时候就只能用“软件层”的笨办法。
第一个是二分法。假如系统有8个功能模块,Bug只在全开时出现。你先注释掉一半模块的调用,看Bug还在不在;还在,说明问题出在保留的一半里;不在了,说明出在被注释掉的一半里。如此反复,四轮就能锁定到某个模块。关键操作是不要一次注释太多,“二分”的效率远高于“一次猜一个”的遍历。
第二个是版本历史二分。配合Git等版本管理工具,找出上次“稳定”的提交和当前“出问题”的提交,然后在两个提交之间来回切换,定位是哪一次提交引入了Bug。实测下来,用这个办法找回归Bug最快。强烈建议每次改代码前先打一个tag或commit,别等到调试时才发现没有历史版本可查。
第三个是请同事走查代码。不是叫你“求谁救命”,而是换个视角。你自己写代码时容易带着一堆默认假设:这个变量一定是先赋值再使用、这个中断一定不会在此时触发、这条总线初始化一定成功……而别人一眼就能看出你漏了哪种可能。你跟同事说“我发现XX现象,我觉得是YY原因”,对方可能直接指出“你怎么知道YY就是原因,你验证过ZZ吗?”,瞬间帮你跳出死胡同。
配合代码走查,还有一个经典做法是加断言。在函数入口、出口、循环边界、中断临界区里加断言(assert),检查指针是否为空、索引是否越界、状态机是否处于合法状态。发布版可以把assert关掉,调试版留着。断言是面向开发者的“自检灯”,比你在Bug出现后瞎猜更早暴露问题。
3. 三块硬骨头怎么啃:内存、时序与死机
3.1 内存越界、泄漏与野指针排查
嵌入式里内存相关问题通常是“最凶残”的Bug:平时跑得好好,突然数组越界把相邻变量写成非法值,程序行为立刻乱套。而且越界写入的地方离你声明变量的地方可能十万八千里,你盯着变量定义看半天没用,要去找是谁动了它。
我对付这类问题的第一招是“哨兵字节”。在可疑的数组、缓冲区、结构体周围预留一些填充区,初始化时填成固定值(比如0xAA、0x55),程序跑一段时间后再检查这些填充区是否被改写。被改了,就是越界写入的铁证,而且填充区的位置能大致告诉你越界的边界。这个方法简单、低成本,在调试版里特别管用。
第二招是数据断点。前文提到的调试器数据断点,专门用于“某地址被写入”时触发。把变量地址或者数组尾部的地址设成数据断点,写操作一发生,CPU立刻停下来,你直接看是哪条代码干的。这套组合“哨兵字节+数据断点”能解决大部分静默内存破坏问题。
第三招是针对动态内存的追踪。嵌入式里用的内存分配通常是malloc/free或者池化内存管理。泄漏排查的核心是记录“谁分配了、分配多少次、是否释放”。给分配函数加钩子,记录调用者(可用__FILE__和__LINE__宏)和分配大小,维护一个分配表,周期性打印分配计数和内存水位。一旦发现某段代码的“当前未释放块数”持续增长,就是泄漏。堆和栈共用一片RAM时还要关注堆溢出冲击栈底的情况,这个用MPU(内存保护单元)或编译器自带栈检测工具辅助最好。
还有一个高频坑:free之后没有把指针置NULL,后续代码再次使用该指针,变成访问野指针。嵌入式里更要命的是中断与主循环都在用同一块动态内存,主循环释放的同时中断正在访问——轻则数据错乱,重则HardFault。这种问题没有任何“优雅”的解法,只能靠设计规约:同一块缓冲区只允许同一上下文访问,跨上下文传递用队列,而不是把指针到处散。
3.2 中断、时序与竞态条件导致的偶发Bug
偶发Bug是嵌入式调试里最磨人的一类,经典台词是“它有时出现,有时候不出现,我什么也没改它就正常了”。这类问题多半跟时序和竞态条件有关。
典型场景:主循环和中断服务函数共享一个全局变量。主循环读变量判断要不要处理,中断里写变量。如果没有关中断保护,读取和写入可能在中断发生的瞬间交错执行,导致数据错乱。排查时第一步就是把所有中断和主循环共享的变量全揪出来,逐个检查:访问它的时候有没有关中断?关的中断优先级是否恰当?是否用了原子操作或临界区?是否声明了volatile?很多时候问题就出在“忘了加volatile”,编译器把循环里的读取优化成只读一次,结果变量悄悄变了,程序还在按旧值运行。
第二步是把时序画出来。用GPIO翻转法在中断入口和出口打标记,在关键操作前后也打标记,用逻辑分析仪看事件之间的时延、中断的触发频率、中断被阻塞的最大时间。比如怀疑某中断被长时间屏蔽,你就在被屏蔽区间拉电平,如果你发现屏蔽时间远超预期,立刻去查是谁关了中断忘了开。
第三步是验证“临界区”是否真的安全。Cortex-M上习惯用“关全局中断”或“设置BASEPRI”来实现临界区。但要注意:关全局中断时间过长,会错过实时事件;BASEPRI虽然能屏蔽低优先级中断,但也会改变中断的实时性。所以临界区代码要尽量短,最好是几十行内就结束。调试这类问题,我习惯加一个“中断延迟统计”的模块:每次中断进入时记录当前时间,退出时统计从中断触发到真正进入ISR的时间差,超过阈值就打印。这个数据能帮你量化“中断被关了多久”,而不是靠感觉猜。
实战里还有一个隐藏坑:中断嵌套导致栈溢出。你设了中断嵌套,高优先级中断打断低优先级中断,栈使用量会临时暴增。如果系统栈本来没留够余量,某次恰好嵌套深度达到最大,栈顶直接顶爆,然后你看到系统随机死机。排查方法是在启动时把栈区用固定模式填充(比如0xCD),定期检查栈顶地址附近有没有“被吃掉”的标记;更简单粗暴的是把栈区设得足够大,调试版直接加一倍,先确认是不是栈的问题再说。
3.3 死机与复位:HardFault现场还原术
嵌入式最常见也最恼人的就是“代码跑飞”,症状是系统毫无征兆地复位、卡死、看门狗重启。这种现象背后通常是某种异常(Exception),最常见的Cortex-M异常是HardFault。
遇到这种情况别急着复位重来,第一步是抓现场。如果你的工程支持异常回调(比如HardFault_Handler),在这个回调里先不要做复杂操作,而是保存关键寄存器到全局变量或RAM里,然后停止执行(比如while(1))。这样你至少能在调试器里看到异常发生时的程序计数器(PC)、链接寄存器(LR)、堆栈指针和异常状态寄存器。PC指向的是触发异常的那条指令,配合反汇编还能看到具体是哪次内存访问出了问题。
第二步是看异常状态寄存器。Cortex-M有硬件Fault状态寄存器(如HFSR、CFSR、MMFAR、BFAR),它们能区分异常类型:是总线错误(访问了非法地址)、用法错误(除零、未对齐内存访问)、还是内存管理错误(MPU保护)。比如CFSR里的BFAR寄存器保存了触发总线错误的地址,你一看这个地址就能猜到是哪个外设或哪块内存被非法访问。
第三步是栈回溯。根据LR寄存器的值判断异常时用的是主堆栈指针(MSP)还是进程堆栈指针(PSP),然后沿着栈帧往回找。调试器自带的调用栈窗口在优化开大时不一定可靠,但大体上能帮你还原“异常发生在哪个函数、谁调用的它”。如果栈本身已经被破坏,回溯的大部分内容都是垃圾,那就回到第一节的“哨兵字节+栈填充”组合拳去查栈溢出。
实操建议是:所有量产固件都保持一个“异常现场信息”的保留区。开机时在RAM里放一个标志结构体,异常回调里把PC、LR、各Fault寄存器、栈顶附近数据、以及预先填充的栈水位标记一起写入保留区,然后打印到串口或存Flash。这样即使没有调试器,设备复位后你也能通过串口日志拿到完整的“事故现场”,再做离线分析。我自己的项目里就靠这个机制,在现场设备出问题时远程取回了异常地址,直接定位到是某外设库函数里数组下标越界,半小时锁定根因。
4. 工具选型与场景化排查路线图
4.1 一张表看清“什么时候该用哪种方法”
很多人问我:这些方法倒是都懂,就是不知道当前这个Bug到底用哪个。我的习惯是先做两个判断:能否在线连调试器?Bug是否稳定复现?四个组合对应不同路线。
| 问题类型 | 能否在线调试 | 优先排查手段 | 原因 |
|---|---|---|---|
| 逻辑分支/状态机错乱 | 能 | 单步断点、条件断点 | 直观比对执行路径 |
| 内存数据被莫名修改 | 能 | 数据断点、哨兵字节 | 在写入瞬间触发定位 |
| 资源受限没法打日志 | 否 | GPIO翻转+示波器 | 硬件级观测,不占软件资源 |
| 偶发、与时序关联 | 否/不稳定 | 逻辑分析仪、事件统计日志 | 捕捉一次次完整时序 |
| 编译优化后行为变化 | 能但不可信 | 临时O0复现、排查未定义行为 | 先稳定现场再归因 |
| 现场/产线远程设备 | 否 | 串口日志、复位统计、异常保存 | 最低成本回收信息 |
这个表格不是死规矩,但能帮你在慌乱时候快速决定先动手干什么。多数情况你会同时用到2到3种手段,比如一边串口打日志,一边示波器抓GPIO波形,两边一对比,真相就出来了。
4.2 我手边常用的小工具清单
标准工具组合:带SWD接口的调试器(可选带虚拟串口功能的),一台100MHz带宽以上的示波器,一台入门级逻辑分析仪,万用表,可调电源。软件上:一个串口调试助手、一个支持条件断点的IDE、Git、Python写的小日志分析脚本。
大部分时间“调试器+串口”就够用,两者互为补充:调试器看执行流和内存,串口看系统真实运行状态下打印的信息。切记不要在调试时只用调试器而完全信任它的“暂停”状态,因为暂停会干扰外设和时序,看到的现象可能是被调试器扭曲过的。
工具不在多,而在于你熟不熟。比如很多串口调试助手支持波形显示、时间戳、文件保存、彩色区分日志等级,能用好这些功能,外场排查效率翻倍。Python脚本则适合分析长串日志:过滤关键字符串、统计出现次数、对比时间间隔,用脚本处理远快于肉眼盯屏。
4.3 从“排Bug”到“让Bug少进门”:测试与自动化
说实话,排Bug只是后端兜底。我这些年最大的感受是:嵌入式团队真正的效率提升,来自让Bug在合入代码前就被尽量拦住。
基本盘是单元测试。协议解析、状态机、位运算这类纯逻辑代码,完全可以在主机上跑单元测试,不需要硬件也能执行。采用像Unity、Ceedling这类轻量测试框架,把测试用例编进构建系统,每次提交自动跑一遍。很多“逻辑分支写错”“边界条件处理缺失”的Bug,这样就能在代码评审之前暴露。
第二道防线是静态分析。编译器警告全开,外加靠谱的静态扫描工具,能查出未初始化的变量、空指针解引用、数组越界、资源泄漏等常见问题。这些工具跑一遍,等于一个永不疲倦的代码审查员在帮你扫雷。尤其是编译器警告,很多人为了过编译而关掉警告,这是因小失大。
第三道防线是板级自动化测试。用继电器控制被测板供电,用GPIO模拟按键输入,用串口或者专用调试口回传结果,一套脚本跑几百个用例,每次出完测试报告。有了这套东西,你在实验室修好的Bug,才能真正被“固化”住,否则过两周同事改版本,同一个Bug还会再回来。
自动化测试初期投入不小,但对“减少回归”非常值。我的建议是从核心模块起步:先把你经常改、Bug最多的模块做成自动化,慢慢扩展到全系统。
5. 常见问题与避坑实录
5.1 五个极易踩中的“经典坑”
这里我列五个代码层面大家几乎都踩过的坑,每个都写清楚现象、根因和对应策略。
第一,printf半主机模式坑。现象:跑着跑着程序卡死,接上调试器有时能运行、不接就卡。根因:标准库printf默认走半主机通道,需要调试器辅助才“输出”,没有调试器就阻塞等待。对策:重写底层输出函数,直接操作UART寄存器,或使用环形缓冲区+DMA异步输出。
第二,数组越界静默破坏相邻变量。现象:某个变量莫名其妙被改成极大或极小值,但复制该变量的代码看着没问题。根因:相邻缓冲区越界写入。对策:哨兵字节、数据断点、静态分析,三件套必备。
第三,中断里调用printf或delay。现象:系统响应变慢,偶发死机,串口输出乱码。根因:中断处理时间过长,实时性被破坏,或中断反复嵌套最后栈溢出。对策:中断服务只做标记和数据搬移,真正的处理放主循环。
第四,忘加volatile。现象:优化后循环等待标志位永远不退出,临时改成O0又正常。根因:编译器把反复读取的变量优化成寄存器值,看不到外部修改。对策:共享变量、标志位、被中断和外设修改的变量都加volatile,但要注意volatile不解决原子性问题。
第五,看门狗喂太早。现象:系统反复复位,但现场没被抓到,复位后一切正常。根因:喂狗在主循环里,某次任务跑太久,主循环还没跑到喂狗点,看门狗就超时了。对策:用“任务调度式喂狗”:只在所有关键任务完成后统一喂一次,或把喂狗点放在最不会被堵住的地方。同时保留复位原因寄存器(RCC的复位标志)读取,区分是上电复位、外部复位还是看门狗复位。
5.2 排查思路速查表
我整理了一份自己贴在工作站边上的速查表,遇到问题先按表里顺序去查,能省下大量拍脑袋的时间。
| 现象 | 第一怀疑对象 | 用什么排查 |
|---|---|---|
| 变量值莫名变化 | 数组越界 / 数据竞争 | 数据断点、哨兵、临界区审查 |
| 上电复位 / 循环重启 | 初始化顺序 / 栈溢出 / 看门狗 | 复位原因寄存器、栈水位填充 |
| 偶发协议解析错误 | 字节错位 / 同步丢失 | 逻辑分析仪、单元测试 |
| 运行一段时间死机 | 内存泄漏 / 堆溢出 | malloc钩子、MPU监控、内存水位 |
| 高负载下行为错乱 | 中断延迟 / 任务超时 | GPIO翻转、日志时间戳 |
| 某外设初始化偶尔失败 | 时序不满足 / GPIO配置 | 示波器、逻辑分析仪验证电平与时序 |
| 两模块相互干扰 | 电源不稳 / 地弹 / 共享变量 | 分别隔离电源与数据,观察波形 |
这个表的核心逻辑是:不要直接跳到“改代码”,先确认问题的类型,再选择最匹配的观测手段。信息不到位,改进代码就是撞运。
5.3 给新人的几句实在话
我刚入行那阵子,排Bug靠的是“多快好省地给自己加打印、改代码、看不行再改”,结果往往是在几个猜测之间反复横跳,效率极低。后来才慢慢明白,真正的高手不靠猜,靠的是系统地把信息补到足够完整,再用推理排除掉错误分支。
两个小建议对新人特别有用:第一,每次只改一处代码。改完立刻验证,验证完再做下一处。一次改三处,Bug修好了你也不知道是哪处的功劳;Bug还在,你更抓狂。第二,每次调试的记录一定要留痕。哪怕是随手记的几行字,包括修改时间、修改内容、现象变化,回看时都是宝贵线索。
还有一个看起来“务虚”但很要命的提醒:学会区分“自己代码的问题”和“电路/硬件的问题”。嵌入式项目里,软硬件不配合才是常态。千万不要因为自己是软件负责就默认所有Bug都在自己代码里,也别因为能打印日志就忽略了波形和电源。工具越多,越能交叉验证,排Bug就是从不同的维度互相印证,直到只剩一个可能。
我在几个项目里最深的体会是:排Bug排到最后,真正考验的不是某个调试器用得有多熟,而是你有没有一套把自己拉出信息盲区的方法。日志、波形、断点、栈回溯、自动化测试,本质都是在帮你补全信息。下次再遇到难缠的Bug,先问自己:我手里现在的信息够不够判断根因?如果不够,别急着改代码,先把信息补上——补信息的方法,就是上面这些技术背后真正的价值。