嵌入式设备可靠性测试方法
以几块自己做的板子为对象,整理一套可重复执行的可靠性测试流程: 掉电写参数、升级中断、参数区被破坏、Flash 擦写寿命、宽电压下的存储稳定性、 休眠唤醒时的看门狗、通信异常自恢复,以及错误去抖与无效值约定。 算不上标准,只是一份“我自己每次都要过一遍”的清单。
一、思路:先定义“什么叫做稳定”
我一开始做测试的时候,最常犯的错是“跑一遍没问题就算稳定”。 这种测试的问题在于:它只证明了在当前这一条路径、这一个时刻、这一批器件上没问题, 而现场出问题恰恰都发生在别的路径上。
后来我改成从“异常”出发设计测试:不问“正常情况下能不能工作”, 而问“如果这里断电会怎样”“如果这个字节被写坏了会怎样”“如果这根线被拔了会怎样”。 每一问都对应一个可以注入的条件、一个可以观察的输出、一个可以判定的结果。
这套做法有三个前提,缺一个就没法做:
- 设备要把自己当前的状态说出来。至少要有:复位原因、当前故障码、参数校验结果、 关键计数器的值。看不到状态,就只能靠猜。
- 参数区要有校验。没有 CRC 就没法区分“参数被写坏了”和“参数本来就是这值”。
- 要能区分“计划测试”和“已完成测试”。这一点最容易被自己骗: 测试计划写得很漂亮,实际只跑了前两项,报告上却看起来都覆盖了。 所以下面每一项我都明确标注哪些是已经做过的、哪些是计划要做的。
二、测试项一:掉电时写参数
参数保存最脆弱的时刻不是“写”本身,而是擦除和写入之间那一段。 在单区方案里,擦除一完成、写入还没完成时断电,这一片参数就永久丢了。
注入方法
- 让固件在每次保存参数时拉高一个空闲 GPIO 作为“正在写入”的标记,接逻辑分析仪
- 手动或用继电器控制电源,在标记拉高后的不同时刻断电(覆盖写入前、擦除中、写入中、写完未校验这几个点)
- 每次断电后重新上电,读出参数区内容并与期望值比对
期望结果与判据
- 绝不能出现“设备起不来”。参数坏了可以恢复默认值,但不能卡在死循环里
- 不能出现“半新半旧”的参数。要么全部新值,要么全部旧值
- 判据:连续 20 次随机时刻断电,每次上电后设备都能进入正常工作状态, 且参数要么是完整的旧值、要么是完整的新值
三、测试项二:升级过程中断电
这一项检验的是 Bootloader 的设计,而不是 APP 的实现。测试的关键在于 断电后设备还能不能被再次升级——这比“能不能恢复运行”更重要, 因为一台能重新升级的设备是可以救回来的。
注入方法
- 在固件传输的各个阶段断电:分包传输中、写 Flash 中、校验中、跳转前的等待窗口内
- 每次断电后重新上电,观察:停在 Bootloader 还是进了 APP?还能不能接收新的固件包?
- 重点测“擦除已完成、写入未完成”这个最坏的时间点
期望结果与判据
- 断电后重新上电,设备应当停在可升级状态,并能接受完整固件重新写入
- 不应该出现“既不进 APP、也不收固件”的砖状态
- 判据:对每个断电点各做 5 次,全部能够重新完成一次完整升级
四、测试项三:人为破坏参数区(已完成)
这是最容易做、也最能暴露问题的一项:直接把参数区改成不合法的内容,看设备怎么办。
注入方法与结果
| 注入方式 | 期望结果 | 实际观察 |
|---|---|---|
把参数区里存 CRC 的那个字改成 0xFFFF |
校验失败 → 加载默认参数 | 符合预期。复位后设备以默认从站地址(251)正常工作 |
把整个参数区写成全 0xFF(模拟空片) |
识别为“未初始化” → 写入出厂默认值 | 符合预期,但暴露了一个问题,见下文 |
| 随机涂改参数区中间若干字节 | 校验失败 → 加载默认参数 | 符合预期(CRC 覆盖全部参数,单字节改动必然被发现) |
这一项暴露出的问题:校验通过 ≠ 参数正确
在做“全 0xFF”这一项时发现:第一次启动时某个温度传感器相关参数没有被初始化。
CRC 是能算过的——因为 CRC 只保证“数据没有被破坏”,不保证“数据是被赋过值的”。
如果某个成员恰好是 0 或者全 0xFF,CRC 完全可能通过,但业务上是错的。
修复方式很直接:在默认参数初始化里给这个成员补上明确的默认值(这里是温度传感器的 25 ℃ 标称阻值 10 kΩ),并用连续重启 10 次、每次读回参数比对的方式验证。
五、测试项四:Flash 擦写寿命
这一项最容易被误解,所以要先把两件事分清楚。
5.1 先算账:手册指标决定设计,而不是决定测试
这类小容量 MCU 的片上 Flash 擦写次数,数据手册通常给出的保证值是最低 1000 次。这个数字要先拿来做设计决策,而不是先拿来做测试:
| 参数保存频率 | 每天擦写次数 | 1000 次能用多久 |
|---|---|---|
| 每 10 分钟一次 | 144 | 约 7 天 |
| 每小时一次 | 24 | 约 41 天 |
| 每天一次 | 1 | 约 2.7 年 |
算完这个账,结论就很清楚了:片上 Flash 只能存“几乎不变”的配置 (设备地址、标定系数、硬件号、软件号), 绝不能存“频繁变化的状态”(运行日志、错误事件、累计计数)。 如果确实需要记录,应该外挂 EEPROM/FRAM,或者做写均衡(把一页切成若干槽轮流写、写满才擦一次)。
5.2 写入对齐:一个必然踩到的细节
这类 MCU 的 Flash 编程最小单位往往不是字节,而是双字(64 位 / 8 字节)。 所以“写 N 个字节的参数”这件事在底层必须先补齐到 8 的整数倍。
/* 底层写入:不足 8 字节的尾部补 0xFF 对齐后再编程 */
/* 测试用例:传入 63 字节数据,应自动补 1 字节并写入 */
static void flash_write_aligned(uint32_t addr, const uint8_t *buf, uint16_t len)
{
uint8_t pack[8];
uint16_t i;
for (i = 0; i < len; i += 8) {
uint16_t n = (uint16_t)((len - i) >= 8 ? 8 : (len - i));
uint16_t k;
for (k = 0; k < 8; k++) {
/* 补齐部分填 0xFF —— Flash 擦除后的自然状态也是 0xFF */
pack[k] = (k < n) ? buf[i + k] : 0xFF;
}
hal_flash_program_doubleword(addr + i, pack);
}
}
测试用例很直白:传 63 字节,应当自动补 1 个 0xFF。
这个用例之所以值得单独列出来,是因为补齐值必须是 0xFF 而不是 0——
如果用 0 补齐,将来想把这几个字节改成正式数据时,就必须先擦除整页,
而“多写了几个字节的 0”这种问题在功能测试里根本看不出来。
5.3 擦除地址必须自己校验(已发生的故障)
HAL_ERROR。原因:Flash 擦除的最小单位是页,非对齐地址本身就是无效请求; 而原来的实现把地址原样透传给底层驱动,只拿到一个笼统的错误码。
办法:在封装层的擦除函数里增加地址合法性检查,提前拦截并返回明确的错误。
验证:随机地址写入测试通过率 100%。
复盘:底层 API 的错误码往往太笼统,直接透传上去会让上层无法区分“参数写错了” 和“硬件坏了”。在封装层做参数校验、返回自己的错误码,这件事花不了多少时间,但能省掉很多排查。
六、测试项五:宽电压下的存储稳定性
这一项在测试计划里,目前还未完成,先记下设计思路。
- 方法:用可调电源把供电拉到不同电压点(计划覆盖 2.7 V ~ 3.6 V 这个范围),在每个电压点执行“写参数 → 断电 → 重新上电 → 读回比对”若干轮
- 为什么必须做:Flash 写入是依靠内部电荷泵升压完成的, 供电电压越低,写入失败的概率越高,而且失败往往不是整块失败,而是个别位翻转—— 这正是 CRC 能发现、但功能测试发现不了的那类故障
- 判据:每个电压点各做 N 轮,参数读回必须与写入完全一致, 或在校验失败时可靠地回退到默认值,不允许出现“校验通过但内容不对”
七、测试项六:看门狗与休眠唤醒喂狗
看门狗看起来简单,但它有两个经典的失效方式,而且都是“加了看门狗反而更糟”。
7.1 失效方式一:休眠时没人喂狗
低功耗休眠时 CPU 停止运行,主循环不再执行,如果喂狗写在主循环里, 看门狗必然超时复位。我见过两种解法,都有效:
- 用定时器周期性唤醒喂狗。把原来的计时定时器改成“周期性唤醒 + 喂狗”, 原来的计时功能迁到另一个定时器上。
- 在休眠循环里喂狗。用定时器置一个标志位,休眠的等待循环里检查这个标志, 置位就刷新看门狗。这样 CPU 仍然可以停在低功耗等待指令上,只是每次唤醒都顺手喂一次。
7.2 失效方式二:看门狗超时时间没有跟着主循环算
看门狗的超时时间要用实际的时钟配置反算,不能凭感觉填。举个例子:
如果看门狗时钟经过 2048 分频后是 500000 / 2048 ≈ 244 Hz,
计数上限是 65536,那么超时时间就是 65536 / 244 ≈ 2.68 秒。
知道这个数字之后才能判断:主循环里最长的那个操作(比如一整页 Flash 擦除)
会不会超过它。如果会,就必须在那个操作内部插入喂狗,或者干脆把超时时间调大。
八、测试项七:通信异常恢复
这是一个我花时间最多、也最有收获的测试项。核心结论是: “通信断了”和“通信卡住了”是两类完全不同的问题。
8.1 两类问题的区别
| 类型 | 表现 | 正确的对策 | 错误的对策 |
|---|---|---|---|
| 通信断了 | 报文丢了、对端没应答,但接收通道本身是好的 | 重试、超时、掉线降级 | 整机复位(会打断正常业务) |
| 通信卡住了 | 接收通道进入了一个“再也不会完成”的状态,重试永远不会成功 | 检测到异常状态,强制复位接收通道 | 单纯加大重试次数(永远等不到) |
8.2 一个真实的“卡住”案例(已完成)
原因:频繁拔插会让串口异常地识别到空闲标志位, 接收状态机被带进一个错误的状态;多发生在连接通信线的瞬间,接收电文的时序异常。
办法(两步): ① 把 DMA 填满,强制产生一次接收完成/溢出事件,借此重启接收中断, 把接收状态机强行拉回已知状态; ② 从根上缓解——把 DMA 的接收长度按协议标准的最大数据长度(这里是 256 字节)限定, 这样异常状态更容易被“填满”这个可预期的事件终结。
实测结果:在对端以 10 Hz 频率持续发送报文的情况下, 出现异常后可以在 6 秒内自动重连。
这个做法的思路我觉得值得单独记下来:用一个“可预期的错误”去终止一个“不可预期的错误状态”。 既然不知道接收通道卡在哪个状态,那就让一个必然会发生的条件(缓冲区填满) 把它推出去。这个思路在很多“卡死”场景里都适用—— 关键是那个“必然会发生的条件”要足够安全,不会破坏正常的数据。
8.3 分级恢复:先加重启,后来把它删掉(已完成)
另一块采集板的故障恢复策略演进,是我觉得最有价值的一段过程:
- 第一版:检测到通信或采集异常后,直接调用系统复位重启设备。
- 发现问题:太激进了。偶发的单次错误本来是可以在本地消化掉的, 一旦整机复位,正在进行的业务被打断,用户看到的是“设备莫名其妙重启”。
- 第二版(改成分级重初始化):
- 采集错误 → 重新初始化 ADC 芯片(这类器件在总线被干扰后, 往往需要一次软复位才能恢复)
- 通信错误 → 每累计 12 次错误才重新初始化一次串口, 而不是每次都动
- “连续多次错误就整机复位”的分支被注释掉,只保留打印
这里有两个可以推广的原则: 一是恢复动作要按“层级”来分,能用轻的手段解决就不要用重的; 二是重初始化本身也要限流——如果每次错误都重新初始化外设, 在持续干扰的现场会变成“不停初始化”,反而让通信永远建立不起来。
九、测试项八:错误去抖与无效值约定
这一项不是关于硬件的,而是关于“怎么把错误表达出去”。做不好会引发两类误判。
9.1 连续 N 次失败才报错
单次采集失败就报故障是个常见的错误:现场干扰是脉冲型的, 偶发一次读失败完全正常。我采用的规则是连续失败 6 次才置故障标志, 期间只写无效值并继续采集,一旦成功立刻把失败计数清零。
/* 采集:连续 6 次失败才报错,成功即清零 */
#define ACQ_FAIL_THRESHOLD 6
static void acquisition_step(void)
{
if (sensor_read_ok()) {
fail_cnt = 0;
update_measured_value();
} else {
if (fail_cnt < ACQ_FAIL_THRESHOLD) {
fail_cnt++;
}
if (fail_cnt >= ACQ_FAIL_THRESHOLD) {
set_fault(ERR_ACQ);
}
/* 未达阈值时,只写无效值,不影响控制逻辑的其他部分 */
write_invalid_sentinel();
}
}9.2 无效值必须有一个双方都认识的表示
“读不到值”这件事必须在协议上能表达出来,否则上位机会把一个恰好的数值当成真实测量结果。
我用的约定是:电压类无效值写全 0xFF,电流类无效值写一个
正常量程之外的哨兵值(例如 -(0x7777)),
上位机看到这些值就知道“这一路当前没有有效数据”,而不是“测量结果是这个数”。
十、测试用例矩阵
把上面各项汇总成一张表,就是我现在每次都会过一遍的清单。 “状态”一列如实标注是否已经完成。
| # | 测试项 | 注入方法 | 预期结果 | 状态 |
|---|---|---|---|---|
| 1 | 写入对齐 | 传入 63 字节数据 | 自动补 1 字节,按 8 字节对齐写入 | 已完成 |
| 2 | 参数校验失败 | 修改存储的 CRC 值 | 加载默认参数,设备正常启动 | 已完成 |
| 3 | 参数区为空片 | 整区写 0xFF | 识别为未初始化,写入出厂默认值 | 已完成 |
| 4 | 单个成员未被赋值 | 首次启动读取全部参数 | 每个成员都有明确默认值 | 已完成(发现并修复) |
| 5 | 擦除非法地址 | 传入非页对齐地址 | 返回明确错误,不透传底层笼统错误 | 已完成(发现并修复) |
| 6 | 通信超时 | 发送不完整帧并保持 | 看门狗复位,或按超时策略降级 | 已完成 |
| 7 | 插头频繁拔插 | 反复插拔通信插头 | 自动恢复,不需人工复位 | 已完成(6 秒内重连) |
| 8 | 掉电时写参数 | 擦写过程中随机断电 | 要么旧值要么新值,设备必须能启动 | 计划中 |
| 9 | 升级中断电 | 分包/写入/校验各阶段断电 | 停留在可升级状态,能重新升级 | 计划中 |
| 10 | 宽电压存储 | 2.7 V ~ 3.6 V 各点写读比对 | 内容一致或可靠回退默认值 | 计划中 |
| 11 | Flash 寿命 | 循环擦写(摸实际余量) | 记录失效点,与手册 1000 次下限对比 | 计划中,部分循环完成 |
| 12 | 休眠唤醒喂狗 | 进入低功耗并长时间待机 | 不被看门狗复位,唤醒后立即恢复业务 | 已完成 |
十一、我犯过的两个错
错误一:把“整机复位”当成万能药
上面 8.3 节写的就是这件事。当时觉得“复位一下就好了”最省事, 实际上它把一个小问题升级成了对用户可见的中断。 恢复动作的强度要和故障的严重程度匹配, 并且“重新初始化”这类动作本身也要限流。
错误二:工装里“看起来测过了”的假测试
我在做一个检测工装的时候,把检测流程写成了完整的状态机, 界面上会一步步显示“大小阀检测 1 / 2 / 3”“电压检测”“负载检测”, 看起来很专业。但后来复盘发现:其中四项只有状态迁移和打印, 并没有真正的阈值判定代码——也就是说,不管被测板是什么状态, 流程都会一路走下去并显示“完成”。
这件事的教训比技术本身更重要:测试流程“跑通了”和“测出来了”是两件事。 一个检测工装如果没有失败分支,那它就没有在检测。 现在我给这类工装定的最低要求是:必须能构造出一个必然失败的情形, 并确认工装会报失败。测不出失败的测试,等于没测。
十二、局限与还没做完的部分
- 掉电测试还没有系统化。目前主要是靠手动拔插,随机性不够, 也没有精确控制到“擦除中”这种微秒级的时刻。后面打算用一个 MOSFET 加定时器做可控断电。
- Flash 寿命测试只跑了一部分。10 万次循环是个很长的过程, 而且需要多颗芯片同时跑才有统计意义。目前只有计划与部分数据。
- 宽电压存储稳定性完全没开始。只有测试方法的设计。
- 温度维度完全缺失。高低温下的 Flash 写入、晶振频偏、ADC 基准漂移都没有覆盖。
- 没有自动化。所有测试项都是人工执行 + 人工记录, 这意味着“每次都过一遍”实际上做不到,只能挑重点。理想状态是把这些做成一套 半自动的测试工装,但那是另一个项目的工作量。
- 判定标准是我自己定的,不是行业标准。 本文提到的所有判据都来自个人实践中的经验值, 如果是对外产品,应该参照相应的国家标准或行业标准来设计试验条件与判定准则。
参考资料与说明
- 所用 MCU 数据手册中关于 Flash 存储器擦写寿命、编程单位与页大小的章节。
- 所用 MCU 参考手册中关于看门狗(IWDG/WWDG)时钟源、分频与超时计算的章节。
- 环境试验与可靠性试验相关的国家标准 / 国际标准(例如电工电子产品环境试验系列标准) 的名称与适用范围。本文的判据为个人经验值,并非引用这些标准的具体条款。
- 本文涉及的平台型号与测试数据均为个人学习项目中的实际记录; 文中示例代码为按理解重写的最小片段,不代表任何产品或交付代码。
- 文中芯片与器件型号仅用于说明技术方案,与相关厂商无隶属或授权关系。