1. 故障定位基础:嵌入式软件故障分类、故障生命周期、故障定位方法论
各位同学好,我是老周。做嵌入式软件测试这行,掐指一算也有十几年了。今天咱们开始第一课,聊聊故障定位的基础。说白了,就是当你发现程序跑飞了、数据错了、系统死机了,你该怎么下手去查。
很多人一上来就急着打日志、接仿真器,结果忙活半天,问题还在那。为什么?因为你没搞清楚你面对的是个什么“鬼”。
1.1 嵌入式软件故障分类
我个人习惯,先把故障分个类。就像医生看病,先得知道是内科还是外科。嵌入式软件的故障,我一般分成三大类:
| 故障类别 | 典型表现 | 我遇到过的例子 |
|---|---|---|
| 逻辑故障 | 功能跑偏、条件判断错误、状态机卡死 | 一个温控器,温度到了设定值还不跳断,结果是if条件里写了个“&”而不是“&&” |
| 时序故障 | 数据采集错位、外设响应超时、任务饿死 | 某次做电机驱动,SPI读取编码器数据总是丢帧,后来发现是中断优先级配错了,读取被高优先级任务打断了 |
| 资源故障 | 内存泄漏、堆栈溢出、DMA冲突 | 一个跑了一周才死机的设备,查到最后是malloc了没free,内存碎片把系统拖垮了 |
1.2 故障生命周期
故障不是凭空出现的,它有自己的“一生”。理解这个生命周期,你才能知道在哪个阶段该做什么事。
我把它画成了一张图,你们看看:
你看,故障在潜伏期你是看不见的。代码里有个bug,但一直没触发。直到某个特定条件出现,它被激活了,然后像多米诺骨牌一样传播,最后在表现期被你看到。
1.3 故障定位方法论
好,分类和生命周期都清楚了,接下来就是动手查。我常用的方法就三个:二分法、排除法、因果链分析。别觉得老套,真正好用的东西往往不花哨。
1.3.1 二分法
说白了就是“折半查找”。你有一堆代码,不知道问题在哪,那就从中间切一刀。
举个例子:
// 假设这是一个处理传感器数据的函数
void process_sensor_data(void) {
// 步骤1: 读取原始数据
raw = read_adc();
// 步骤2: 滤波处理
filtered = low_pass_filter(raw);
// 步骤3: 单位转换
converted = convert_to_physical(filtered);
// 步骤4: 数据校验
if (!validate(converted)) {
set_error_flag();
}
// 步骤5: 输出结果
output_result(converted);
}
如果输出结果不对,我怎么做?先在第3步后面加个打印,看converted对不对。如果converted是对的,那问题就在第4或第5步。如果converted是错的,那问题就在第1到第3步。然后继续折半,直到定位到具体某一行。
1.3.2 排除法
排除法就是“逐个关掉嫌疑对象”。你怀疑几个模块可能有问题,那就一个一个屏蔽掉,看问题还在不在。
我举个例子,一个系统有A、B、C三个任务,系统偶尔死机。我怀疑是任务间通信出了问题。怎么做?
- 先把任务C停掉,跑一天,没死机。说明C嫌疑最大。
- 再把C恢复,停掉B,跑一天,又死机了。说明B不是问题。
- 最后单独分析C,发现它有个消息队列没及时取数据,导致队列满了,阻塞了发送任务。
你看,排除法就像剥洋葱,一层一层去掉无关因素,剩下的就是真相。
1.3.3 因果链分析
这个方法我最常用,也最推荐。它要求你从故障现象出发,一步步往回推,找出“因”和“果”之间的链条。
我画个图帮你们理解:
你看,从“转速异常”这个现象出发,我问自己:为什么转速异常?因为PWM占空比错了。为什么PWM错了?因为PID输出饱和了。为什么PID输出饱和?因为积分项溢出了。好,根因找到了。
因果链分析的关键就是“连续问为什么”,问到不能再问为止。我一般会问3到5个“为什么”,基本就能摸到根因。
- 二分法:适合线性流程、代码结构清晰的场景。快,但容易被全局变量干扰。
- 排除法:适合多模块、多任务的系统。稳,但要求故障能稳定复现。
- 因果链分析:适合复杂、偶发的故障。深,但需要你对系统有整体理解。
我个人习惯,先快速用二分法扫一遍,如果不行,就上因果链分析。排除法作为备选,实在没招了再用。
1.4 本章小结
这一章我们聊了三个东西:故障分类让你知道面对的是什么,故障生命周期让你知道问题处在哪个阶段,三种定位方法让你知道怎么下手。嗯,这些都是基本功,但基本功往往最管用。
我记得我刚入行那会儿,遇到一个bug,折腾了一周没搞定。后来师傅过来看了一眼,说:“你先把故障分个类,再画个因果链。”我照着做了,半小时就找到了问题。从那以后,我再也不盲目调试了。