断电、重启、断点续跑:嵌入式多步流程的检查点恢复设计
声明: loxseq 是一个开源项目,作者为 Vanderhell。本文是阅读该项目源码和文档后整理的学习笔记,用于理解嵌入式掉电恢复状态机的工程实现方式。本文作者不是该项目的开发者,未参与该项目的任何代码贡献。 文中所有工程细节均来自对开源代码的分析,不代表本文作者的设计决策。
项目仓库:github.com/Vanderhell/loxseq(断电恢复状态机的 C 语言参考实现)
概述
嵌入式设备经常跑多步流程:灌装、加热、排空,或者充电、标定。任何一步执行中掉电、看门狗复位或 OTA 重启,设备就忘了自己干到哪一步。重做的代价有三种形态:重复执行会破坏(投料、加药、校准),会危险(加热中重启再加热可能过温),会浪费(小时级流程从头再来)。
解决办法是一套设计思想:把进度以检查点形式持久化,重启后读回检查点,按每步预设的恢复策略决定续跑、重跑、放弃还是问人。检查点不只是一个步骤号:写入可能只完成一半(撕裂写),固件升级后步骤表会变,旧记录可能比新的更新,这些都要靠校验、版本、指纹等附加信息兜住。本文以开源项目 loxseq 作为这套思想的参考实现展开,重点在思想本身。这个库的适用面很窄,后面会说明。
恢复策略按步骤声明
"断电后怎么办"没有统一答案,同一流程里每步都不一样。把策略声明在步骤表里,执行交给状态机:
| 恢复策略 | 重启后行为 | 典型场景 |
|---|---|---|
LOXSEQ_RESUME_AT_STEP |
从断点继续本步 | 断电不影响步骤前提 |
LOXSEQ_RESUME_FROM_START |
回到本步,重头执行 | 需要重新归零或复位 |
LOXSEQ_NEVER_RESUME |
放弃流程,进入安全初始化 | 步骤状态不可信 |
LOXSEQ_OPERATOR_DECIDES |
挂起,等待操作员裁决 | 需要人工判断 |
举例:灌装可以续(液位到设定值即可),加热可以续(温度到设定值即可),加药绝不恢复(药不能加两次),排空要问人(罐里有什么只有操作员知道)。策略按步声明,机器只执行裁决,人工闸门留在该留的地方。
最小集成示例
以 loxseq 为例看这套思想怎么落地:三步接入。
步骤表
每步声明标签、动作、超时和恢复策略。动作返回 LOXSEQ_STEP_RUNNING 表示进行中,返回 LOXSEQ_STEP_DONE 表示完成。
enum { S_FILL, S_HEAT, S_DRAIN, STEP_COUNT };
static loxseq_step_status_t step_fill(loxseq_t *s, uint32_t now_ms, void *u) {
(void)u;
if (loxseq_step_age_ms(s, now_ms) >= 2000) return LOXSEQ_STEP_DONE;
return LOXSEQ_STEP_RUNNING;
}
/* step_heat、step_drain 结构相同,此处省略 */
static const loxseq_step_def_t steps[STEP_COUNT] = {
[S_FILL] = { .tag = "fill", .action = step_fill,
.timeout_ms = 30000,
.resume_policy = LOXSEQ_RESUME_AT_STEP },
[S_HEAT] = { .tag = "heat", .action = step_heat,
.timeout_ms = 30000,
.resume_policy = LOXSEQ_RESUME_AT_STEP },
[S_DRAIN] = { .tag = "drain", .action = step_drain,
.timeout_ms = 30000,
.resume_policy = LOXSEQ_NEVER_RESUME },
};存储钩子
实现写、读、擦三个回调。后端持久化序列化后的 32 字节检查点,不保存 C 结构体的内存映像。
static const loxseq_storage_t storage = {
.write_checkpoint = my_flash_write, /* 持久化 32 字节检查点 */
.read_checkpoint = my_flash_read,
.erase_checkpoint = my_flash_erase, /* 擦除后表现为冷启动 */
};开机流程
开机先 loxseq_recover() 得出裁决,再按裁决启动;主循环周期性调用 loxseq_tick() 驱动步骤执行。
static loxseq_err_t my_safe_init(loxseq_t *s, uint32_t t, void *u) {
(void)s; (void)t; (void)u; return LOXSEQ_OK; /* 安全初始化,示例为空实现 */
}
static const loxseq_safe_init_t safe_init_cfg = { .run = my_safe_init, .user = NULL };
loxseq_t seq;
loxseq_init(&seq, steps, STEP_COUNT, &storage, &safe_init_cfg);
loxseq_recovery_verdict_t v = loxseq_recover(&seq, LOXSEQ_REBOOT_NORMAL);
switch (v) {
case LOXSEQ_RECOVERY_COLD_START: loxseq_start_fresh(&seq, now_ms); break;
case LOXSEQ_RECOVERY_RESUME: loxseq_start_resume(&seq, now_ms); break;
case LOXSEQ_RECOVERY_RESTART: loxseq_start_restart(&seq, now_ms); break;
case LOXSEQ_RECOVERY_SAFE_INIT: loxseq_start_safe_init(&seq, now_ms); break;
case LOXSEQ_RECOVERY_OPERATOR: loxseq_start_operator_wait(&seq); break;
}
while (1) {
loxseq_tick(&seq, now_ms()); /* 每步的动作在这里被调用 */
}运行中分支
默认流程单向推进。需要按业务条件跳转时,动作里先调 loxseq_set_next_step() 设目标,再返回 LOXSEQ_STEP_BRANCH。以质检步骤为例:合格跳去打包,不合格先跳去重工。跳转目标不限相邻;返回 BRANCH 却没设目标会让序列器进入 FAILED。
完整可编译示例(含打印步骤顺序的 main):下载分支示例
场景约束
这套设计有三个明确的禁区,loxseq 的文档也写明了:
- 涉及生命安全的场景。loxseq 自己声明不提供安全等级:无 SIL 认证、无 IEC 61511、无 ISO 26262。检查点只记录软件知道的进度,不记录液位、温度、位置这些物理事实;断电期间物理过程还在演化,恢复时世界已经变了。安全场景的恢复必须止步于安全状态并等人工确认,自动续跑是错误设计。
- 重大财产损失场景。恢复策略不可证明安全时,机器替人做决定就是风险。
- 生产现场的主控。工厂工艺层的主控是 PLC/DCS 加安全 PLC,联锁、报警、配方都在那一层,断电恢复非常保守。检查点恢复适合的是设备内部的序列(点胶机归零分装、电池充电阶段、打印机断电续打),位置在 PLC 之下,两者叠加使用,互不替代。
另外要说明的是:这个库的适用面很窄,没有公开的采用记录,项目作者也举不出确切的用户。把它当作高质量的参考实现来阅读更合适,价值在思想,不在这份代码。
需要检查点恢复的四个特征
用不用这套设计,判断标准是"重做代价",与断电概率无关。四个特征命中其一就值得考虑:
- 流程里有一次性或不可逆步骤,重复执行会破坏物料或设备。
- 单步分钟到小时级,重跑代价大。
- 断电后"知道刚才在干什么"本身就是安全条件(加热中、均衡中)。
- 重启源多样:看门狗、OTA、电池耗尽、用户关机。
四个特征全不命中,重启后从头跑一遍没损失,就不需要这套设计。大多数项目属于这一类,自己写几十行状态机比引入依赖更划算。
即便不采用检查点恢复,这套思想里的几个设计点值得带走:恢复策略按步声明、终结写墓碑防止重做、步骤表指纹防固件升级误恢复、数据可疑时保守降级、时钟驱动状态机可测可移植。