本站为个人非经营性网站,仅用于技术学习与分享,不提供任何商品、服务、报价或收费咨询。 合规信息

嵌入式设备可靠性测试:
把“稳不稳定”变成可执行的测试项

“这个设备稳不稳定”是个没法回答的问题,因为“稳定”没有被定义。 这个实验做的事情很朴素:把可靠性拆成一条条可以注入、可以观察、可以判定通过或失败的测试项, 每一项都写清楚怎么做、期望看到什么、以及我实际看到了什么。

可靠性 故障注入 存储寿命 异常恢复 已发布
已发布 · 个人学习项目

嵌入式设备可靠性测试方法

以几块自己做的板子为对象,整理一套可重复执行的可靠性测试流程: 掉电写参数、升级中断、参数区被破坏、Flash 擦写寿命、宽电压下的存储稳定性、 休眠唤醒时的看门狗、通信异常自恢复,以及错误去抖与无效值约定。 算不上标准,只是一份“我自己每次都要过一遍”的清单。

PLATFORM 跨平台(Cortex-M0+ 至 M4)
STACK 故障注入 + 存储寿命 + 宽电压 + 异常恢复
TOOLS 可调电源 · 逻辑分析仪 · 自研测试固件
STATUS 个人学习项目 · 测试项持续补充

一、思路:先定义“什么叫做稳定”

我一开始做测试的时候,最常犯的错是“跑一遍没问题就算稳定”。 这种测试的问题在于:它只证明了在当前这一条路径、这一个时刻、这一批器件上没问题, 而现场出问题恰恰都发生在别的路径上。

后来我改成从“异常”出发设计测试:不问“正常情况下能不能工作”, 而问“如果这里断电会怎样”“如果这个字节被写坏了会怎样”“如果这根线被拔了会怎样”。 每一问都对应一个可以注入的条件、一个可以观察的输出、一个可以判定的结果。

这套做法有三个前提,缺一个就没法做:

  • 设备要把自己当前的状态说出来。至少要有:复位原因、当前故障码、参数校验结果、 关键计数器的值。看不到状态,就只能靠猜。
  • 参数区要有校验。没有 CRC 就没法区分“参数被写坏了”和“参数本来就是这值”。
  • 要能区分“计划测试”和“已完成测试”。这一点最容易被自己骗: 测试计划写得很漂亮,实际只跑了前两项,报告上却看起来都覆盖了。 所以下面每一项我都明确标注哪些是已经做过的、哪些是计划要做的。

二、测试项一:掉电时写参数

参数保存最脆弱的时刻不是“写”本身,而是擦除和写入之间那一段。 在单区方案里,擦除一完成、写入还没完成时断电,这一片参数就永久丢了。

注入方法

  • 让固件在每次保存参数时拉高一个空闲 GPIO 作为“正在写入”的标记,接逻辑分析仪
  • 手动或用继电器控制电源,在标记拉高后的不同时刻断电(覆盖写入前、擦除中、写入中、写完未校验这几个点)
  • 每次断电后重新上电,读出参数区内容并与期望值比对

期望结果与判据

  • 绝不能出现“设备起不来”。参数坏了可以恢复默认值,但不能卡在死循环里
  • 不能出现“半新半旧”的参数。要么全部新值,要么全部旧值
  • 判据:连续 20 次随机时刻断电,每次上电后设备都能进入正常工作状态, 且参数要么是完整的旧值、要么是完整的新值
一条写入流程时间轴:擦除中、写入中、写完未校验三个时刻,用闪电图标标出断电注入点
图 1 · 掉电注入点示意图

三、测试项二:升级过程中断电

这一项检验的是 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 擦除地址必须自己校验(已发生的故障)

六、测试项五:宽电压下的存储稳定性

这一项在测试计划里,目前还未完成,先记下设计思路。

  • 方法:用可调电源把供电拉到不同电压点(计划覆盖 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 一个真实的“卡住”案例(已完成)

这个做法的思路我觉得值得单独记下来:用一个“可预期的错误”去终止一个“不可预期的错误状态”。 既然不知道接收通道卡在哪个状态,那就让一个必然会发生的条件(缓冲区填满) 把它推出去。这个思路在很多“卡死”场景里都适用—— 关键是那个“必然会发生的条件”要足够安全,不会破坏正常的数据。

8.3 分级恢复:先加重启,后来把它删掉(已完成)

另一块采集板的故障恢复策略演进,是我觉得最有价值的一段过程:

  1. 第一版:检测到通信或采集异常后,直接调用系统复位重启设备。
  2. 发现问题:太激进了。偶发的单次错误本来是可以在本地消化掉的, 一旦整机复位,正在进行的业务被打断,用户看到的是“设备莫名其妙重启”。
  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 各点写读比对内容一致或可靠回退默认值计划中
11Flash 寿命循环擦写(摸实际余量)记录失效点,与手册 1000 次下限对比计划中,部分循环完成
12休眠唤醒喂狗进入低功耗并长时间待机不被看门狗复位,唤醒后立即恢复业务已完成
八个测试项排成两行四列的矩阵,每格标注注入方式(掉电、破坏、拉偏电压、拔插等)
图 2 · 可靠性测试项全景图

十一、我犯过的两个错

错误一:把“整机复位”当成万能药

上面 8.3 节写的就是这件事。当时觉得“复位一下就好了”最省事, 实际上它把一个小问题升级成了对用户可见的中断。 恢复动作的强度要和故障的严重程度匹配, 并且“重新初始化”这类动作本身也要限流。

错误二:工装里“看起来测过了”的假测试

我在做一个检测工装的时候,把检测流程写成了完整的状态机, 界面上会一步步显示“大小阀检测 1 / 2 / 3”“电压检测”“负载检测”, 看起来很专业。但后来复盘发现:其中四项只有状态迁移和打印, 并没有真正的阈值判定代码——也就是说,不管被测板是什么状态, 流程都会一路走下去并显示“完成”。

这件事的教训比技术本身更重要:测试流程“跑通了”和“测出来了”是两件事。 一个检测工装如果没有失败分支,那它就没有在检测。 现在我给这类工装定的最低要求是:必须能构造出一个必然失败的情形, 并确认工装会报失败。测不出失败的测试,等于没测。

十二、局限与还没做完的部分

  • 掉电测试还没有系统化。目前主要是靠手动拔插,随机性不够, 也没有精确控制到“擦除中”这种微秒级的时刻。后面打算用一个 MOSFET 加定时器做可控断电。
  • Flash 寿命测试只跑了一部分。10 万次循环是个很长的过程, 而且需要多颗芯片同时跑才有统计意义。目前只有计划与部分数据。
  • 宽电压存储稳定性完全没开始。只有测试方法的设计。
  • 温度维度完全缺失。高低温下的 Flash 写入、晶振频偏、ADC 基准漂移都没有覆盖。
  • 没有自动化。所有测试项都是人工执行 + 人工记录, 这意味着“每次都过一遍”实际上做不到,只能挑重点。理想状态是把这些做成一套 半自动的测试工装,但那是另一个项目的工作量。
  • 判定标准是我自己定的,不是行业标准。 本文提到的所有判据都来自个人实践中的经验值, 如果是对外产品,应该参照相应的国家标准或行业标准来设计试验条件与判定准则。

参考资料与说明

  • 所用 MCU 数据手册中关于 Flash 存储器擦写寿命、编程单位与页大小的章节。
  • 所用 MCU 参考手册中关于看门狗(IWDG/WWDG)时钟源、分频与超时计算的章节。
  • 环境试验与可靠性试验相关的国家标准 / 国际标准(例如电工电子产品环境试验系列标准) 的名称与适用范围。本文的判据为个人经验值,并非引用这些标准的具体条款。
  • 本文涉及的平台型号与测试数据均为个人学习项目中的实际记录; 文中示例代码为按理解重写的最小片段,不代表任何产品或交付代码。
  • 文中芯片与器件型号仅用于说明技术方案,与相关厂商无隶属或授权关系。