起因

机床参数丢了意味着什么,干这行的都见过:丝筒参数不对飞车、坐标丢了对不了刀、一屋子机床挨个重新校。断电恢复是另一半:加工到一半停电,来电之后这台件是报废、续切还是回退,得是系统说了算,不是运气。日志系统解决"发生了什么",这篇解决"配置与现场丢没丢"。

“断电不丢"这件事,数据库行业几十年前就啃透了——预写日志和双副本提交是事务系统的看家本领,Jim Gray 那一代人把"先写数据还是先写提交标志"的每一种顺序都推演过。嵌入式这边另有一本账:EEPROM 和闪存有擦写寿命,参数不能无节制改写,磨损权衡是长期实践磨出来的。这篇把两边的结论搬到机床上:双副本世代提交保参数,单扇区快照保现场。

一、三类持久数据,三种可靠性等级

数据 例子 丢了的代价 策略
配置参数 机床参数、放电规准、坐标补偿、locale/语言 高(要人工重校) 双副本 + 世代
加工现场 绝对坐标、程序指针、状态机 高(工件报废) 快照 + 提交标志
派生数据 日志、统计 可再生(日志篇已覆盖)

二、参数表:定长二进制,不做 INI

坑:文本配置(INI/键值对)在工控上是三个坑的叠加——解析器本身是攻击面;改一个字节也要重写整个文件(写放大);半写的文件到底坏在哪一行极难判定。定长二进制没有这些问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#[repr(C)]
struct ParamTable {
    magic: [u8; 4],               // b"PAR1"
    version: u16,                // 结构版本,升级迁移用
    gen: u32,                    // 世代号,每次保存 +1
    len: u32,
    // —— 以下为定长参数槽,只增不改语义 ——
    machine: MachineParams,      // 丝筒/导轮/行程
    process: [ProcessParams; 64],   // 放电规准表
    coords: [CoordSet; 8],       // 坐标系/补偿
    locale: LocaleParams,        // 语言/单位/日期格式(国际化篇)
    perm: PermissionParams,      // 三级权限:操作/工艺/维修
    _reserved: [u8; 128],        // 未来扩展的余量
    crc32: u32,
}

槽位只增不改语义,version 升高时旧文件读进来按默认值补新槽——十五年生命周期里这张表必然要长,设计期就留好生长方式。

权限三级:操作员只动运行参数;工艺员动规准表;维修动机床参数。不搞账号系统,级密码 + 日志留痕(谁在几点改了哪个槽)就够——工控现场的现实是密码贴在机床边上,防的是误操作不是间谍。

三、双副本 + 世代号:任意时刻断电最多坏一份

两个物理副本 A/B,各自带 (gen, crc)。保存流程:

  1. 挑世代号的那份写
  2. 先写 body + crc,最后一步才写头部 magic/gen——gen 就是提交标志,写一半断电这份因 crc 对不上被丢弃
  3. 下一轮保存写另一份

容易漏的细节:写序要真的落到盘上,每步之间要 NVMe flush。SSD 的 volatile cache 会让"先写 body 后写头"变成"都攒在缓存里、掉电时一锅端”,写序纪律就白守了。完整序列:写旧副本 body → flush → 写提交扇区(magic/gen)→ flush。多两次 flush 换任意时刻断电的确定性,这买卖划得来。

1
2
3
4
5
6
7
8
fn load(a: &ParamTable, b: &ParamTable) -> ParamTable {
    match (a.valid(), b.valid()) {
        (true, true)  => if a.gen >= b.gen { *a } else { *b },
        (true, false) => { repair_from(a, slot_b); *a }
        (false, true) => { repair_from(b, slot_a); *b }
        _ => { /* 双坏 → 出厂默认 + 报警,见下 */ }
    }
}

双坏兜底:出厂默认参数编译进内核映像(include_bytes!),恢复出厂 = 用默认重写双副本、gen 归零重走——这条兜底路径同时服务于固件更新篇的"恢复出厂"能力。

四、加工现场快照:512 字节里的整机状态

快照内容:X/Y/U/V 四轴绝对坐标、当前程序文件 ID + 行/段指针、规准号、走丝/张力/工作液状态、加工计时、状态机枚举。4×f64 + 一堆 u32,一个扇区绰绰有余——快照就设计成单扇区原子单位,双副本各占一个固定 LBA,绕开文件系统元数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#[repr(C)]
struct Snapshot {
    magic: [u8; 4],        // b"SNA1"
    gen: u32,              // 提交标志,同参数表
    coords: [f64; 4],      // X Y U V 绝对坐标,单位 µm
    prog: ProgRef,         // 程序文件 ID + 段号 + 段内插补步数
    process_idx: u16,      // 规准号
    wire: u8, liquid: u8, state: u8,   // 走丝/工作液/状态机
    hours: f32,            // 累计加工计时
    crc32: u32,
}                        // 合计 < 128B,单扇区内原子
  • 触发时机:段边界(程序换段时)+ 周期兜底(500ms~1s)+ 关键状态变更(断丝/停机)
  • 写法与参数同款:写旧副本 body+crc,最后写 gen 提交

坑:别把快照写进 exFAT 普通文件再靠文件系统落盘——每次写都牵动目录项时间戳和缓存回写,既慢又把 SSD 的元数据页磨穿。裸 LBA 双副本一次 512B 干干净净。磨损账:每秒一次 × 双副本 × 15 年 ≈ 500GB 写入,远低于一块消费级 TLC 的 TBW;走 exFAT 元数据路径的写放大能把这笔账放大几十倍。

五、恢复流程:系统说了算

上电 → 挂载 → 读双副本快照 → 有"已提交"快照:

  1. 界面给出三选:续切 / 回退到上一快照点 / 放弃重来。“上一快照点"不用额外存——双副本天然留着上一份:gen 大的是当前,gen 小的就是上一次,回退 = 读旧副本
  2. 续切前动作(工艺细节,跟机床配合):回退一个让缝距离重新搭火(距离是工艺参数,在规准槽里)、检查丝的损耗状态、坐标回快照值后先空走校验一段
  3. 全程日志留痕

快照间隔决定了停电最多损失多少加工量——500ms 的间隔损失就是半秒钟的缝。这是拿快照频率和 SSD 寿命换来的工程平衡点,参数表里留成可调项。

坑:快照恢复后先验证后动作。 坐标读回后先跟限位/机床坐标做一致性检查,超差(比如停电期间人工摇过手轮)直接拒绝续切转人工模式——相信数据,更要怀疑数据。

六、分区布局定稿

到这里三篇(日志/参数/快照)的持久化需求合到一张分区表上:

分区 内容 谁写
UEFI 引导区(FAT32) BOOTX64.EFI 选择器 + 内核 A/B 槽 引导阶段/产线
FLAG 裸区 激活槽、bootcount、升级待装 选择器 + 内核
SYS 裸区 参数双副本 + 快照双副本 + 日志裸分区 内核
DATA exFAT 大区 加工程序、日志归档、语言包、升级包暂存 内核

裸区 LBA 范围由引导阶段解析后经 Handoff 传给内核(内核不自己解析 GPT)。固件更新篇会在这张图上继续加盖楼层。

总结

实战清单:三类数据分级表;定长参数结构 + 版本迁移 + 三级权限;双副本 + 世代提交读写协议(body→flush→提交扇区→flush);单扇区快照结构体 + 三种触发时机;恢复三选(回退 = 读 gen 小的副本)+ 先验证后动作;分区布局定稿。跑通标志:保存参数时随机时刻硬断电 100 次循环,每次开机读到的要么是完整旧值要么完整新值,无第三态;模拟加工中拔电,重启后界面出现续切提示,坐标与停电前一致(误差 ≤ 一个快照周期)。

下一篇:开机自检与固件更新——系统怎么证明自己活着、怎么在十五年里安全地升级自己。

参考链接