掉电保护与数据完整性 · 技术探讨
掉电那一刻到底发生了什么、掉电窗口有多宽、数据靠什么保证不丢 —— 面向电路设计指南撰写与料号设计文档整理的工程参考 | 编制日期 2026-10-14
掉电保护这件事,缺的不是玄学技巧,而是可算的账。本文把「掉电窗口」拆成四个可以独立估算、独立设计的变量,给出保持时间的工程算法与分档表;再把 NAND 侧在编程 / 擦除 / 元数据更新三个时刻的脆弱性逐条摊开,最后落到原子写、Journal、A/B 槽位与上电恢复的具体做法。
与《eMMC 软件设计规范》第 10 章、第 7 章、第 14 章配套阅读:那份讲驱动怎么写、命令怎么发,本文讲发命令的那几十微秒里,电压在跌什么、数据会不会丢、丢了怎么回来。两份文档刻意不重复彼此的内容。
结论: 掉电不是事件,是一段有明确起止点的电压跌落过程;掉电保护的全部工程量都花在 把这段过程里「器件还能可靠写完一个事务」的时间算出来,并让它大于「软件检测到掉电 + 完成落盘」的时间。 前者是硬件与器件的责任,后者是软件与协议栈的责任,两者必须一起设计—— 任何一侧单独努力都不成立:器件再耐掉电,软件不落盘照样丢数据;软件再 diligent,电容不够大照样写到一半断电。
读者路径
- 做电路 / 硬件:第 1、2 章是主菜(窗口与保持时间估算),第 10 章的测试台是验收手段。
- 做驱动 / RTOS:第 4、5、9 章是主菜(原子写、Journal、上电恢复),第 8 章看裸器件责任边界。
- 做 eMMC / SD NAND 方案:第 6、7 章讲协议与器件已经替你做掉的部分,重点看「没做掉」的清单。
- 写电路设计指南 / 料号文档:附录 B(分档表 + 算法卡)与附录 C(接口原子写对照)可直接摘用。
- 遇到现场问题:直接翻第 11 章速查表与第 10 章的判据。
1. 掉电模型:能量预算与掉电窗口
这一章建立全文的基础模型。掉电不是「有电变没电」这样一个瞬间,而是一段从毫秒到秒的电压跌落过程;这段过程里器件经历了什么、能撑多久,是后面所有设计的地基。
1.1 掉电不是一个瞬间:电压跌落过程与器件的欠压锁定阈值
把电源开关关掉之后,输出电压不会立刻变成 0。能量要按顺序交出去:先由电源输出端的储能电容承担,随后开关器件的关断过程与电感残能继续释放,再往后只剩负载电流与器件漏电流在慢慢耗散。电压因此呈现先陡后缓、最后拖尾的三段式曲线。
这段曲线对软件的意义在于:器件不是「断电」这一瞬间失效的,而是在电压跌过某个门限时进入不确定状态。这个门限不是一个,而是好几个,且各主控实现不同。设计前必须把它们逐个确认清楚。
| 阶段 | 电压表现 | 主导能量去向 | 器件行为 | 工程关注点 |
|---|---|---|---|---|
| ① 陡降段 | 从标称值快速下滑,斜率最陡 | 电源输出电容 + 上游仍在放电的储能 | 完全正常读写 | 最难的一段,斜率由上游阻抗与负载共同决定 |
| ② 缓降段 | 跌落变缓,曲线趋平 | 板上储能电容(体电容 / MLCC)+ 电感残能 | 开始出现欠压告警与接口不稳定 | 决定「还有多少时间」,靠加电容可以显著改善 |
| ③ 拖尾段 | 贴近 0 V 缓慢下降 | 器件漏电流 + 电容介质自放电 | 进入欠压锁定,命令不再被接受,数据可能部分损坏 | 只能靠「早点停下来」避免进入 |
| 门限类别 | 含义 | 典型量级(经验区间) | 跌过之后 | 注意 |
|---|---|---|---|---|
| V_UVLO | 欠压锁定阈值,器件主动关闭接口 | 约 V_nom 的 50% ~ 65% | 命令不再被接受,控制器转入复位态 | 不同主控的锁定点与解锁时序差异很大,依主控而异 |
| V_write_min | 可可靠编程的最低供电 | 约 V_nom 的 70% ~ 85% | 编程动作可能中途失败,页处于「半好」状态 | 与 tPROG 强相关;慢速时序下门限更高 |
| V_read_min | 可可靠读取的最低供电 | 约 V_nom 的 60% ~ 75% | 读出错或直接失败 | 通常低于 V_write_min,即「能读不能写」 |
| V_abs_max | 绝对最大额定电压 | 器件手册给出,不允许超过 | 可能永久损坏 | 这是不能碰的边界,与掉电保护无关但必须核对 |
上表的比例关系是行业里反复出现的量级规律,用来在方案早期做数量级判断是足够的;但任何进入原理图与固件的设计决策,都必须回到目标器件的正式 datasheet,逐条抄录 V_UVLO、V_write_min、V_read_min、V_abs_max 及其测试条件(温度、负载电流、时序档位都会影响这些值)。
1.2 掉电窗口怎么定义:从 VCC 开始跌到器件失去读写能力的这段时间
「掉电窗口」这个词在业界用得比较散,本文把它定义为一个明确、可测量的量:
t_pw = t1 - t0,其中 t0 是 VCC(器件供电轨)开始跌落的时刻,t1 是器件失去可靠写能力(或可靠读写能力,取决于口径)的时刻。
关键在于 t1 的口径必须写清楚,否则「窗口 5 ms」这句话无法验证。工程上常用的三种口径如下,验收时必须与客户或上位明确对齐用的是哪一种。
| 口径 | t1 定义为 | 得到的时间 | 适用场景 | 风险偏好 |
|---|---|---|---|---|
| 写安全窗口 | 跌破 V_write_min 的时刻 | 最短,最保守 | 有写入事务要保证完成的路径:FTL 元数据、日志提交、Bootloader 烧写 | 偏保守,留给软件的余量最大 |
| 读写窗口 | 跌破 V_read_min 的时刻 | 中等 | 只读场景:扫描、校验、读取健康报告 | 折中 |
| 器件存活窗口 | 跌进 V_UVLO、接口停止响应的时刻 | 最长,最乐观 | 只在「反正下一步已经放弃」的场合引用,例如纯诊断流程 | 偏乐观,容易低估风险 |
「窗口有 5 ms」这句话本身没有工程意义,必须同时说清这 5 ms 里器件在流多少电流。同样 5 ms,10 mA 与 500 mA 对储能电容的要求差 50 倍。凡是只谈窗口不谈电流的掉电保护方案,都是没算完的账。
1.3 影响掉电窗口的四个变量:储能电容、负载电流、跌落斜率、器件自身的 tVDW
把窗口从「现象」变成「可计算的量」,只需四个变量。它们彼此独立,可以分别设计、分别验证,但最终取最小值作为有效窗口。
- 储能电容 C:唯一可以由硬件单方面改善的变量,是最有效的手段,也是唯一会占用板面积与 BOM 成本的变量。
- 负载电流 I:由总线宽度、并发控制器数量、是否处于 GC / 磨损均衡高峰决定。峰值电流才是考验对象,平均电流会骗人。
- 跌落斜率 dV/dt:由上游电源的关断速度、输出回路阻抗、储能电容在回路中的位置共同决定。斜率越缓,窗口越宽。
- 器件自身耐受 tVDW:器件在被突然断电时能撑多久。这是器件的固有属性,不可设计,只能选型和实测。
| 变量 | 增大它对窗口的影响 | 由谁决定 | 可设计的手段 | 经验量级 |
|---|---|---|---|---|
| 储能电容 C | 窗口变宽,近似成正比 | 硬件设计 | 加大标称容量、改用更低 ESR 介质、缩短电容到器件的回路 | 每翻倍容量大约多争取一倍时间 |
| 负载电流 I | 窗口变窄,近似成反比 | 总线宽度 / 主控并发数 / GC 策略 | 错峰调度、限制并发写、把 GC 挪到低峰 | 电流翻倍窗口减半,别拿平均电流算 |
| 跌落斜率 dV/dt | 斜率越缓窗口越宽 | 上游电源关断速度 + 回路阻抗 | 加大储能、减小回路阻抗、必要时串隔离二极管 | 陡降段只占全过程的极小一部分 |
| 器件 tVDW | 器件自身的上限 | 器件工艺与内部设计 | 只能选更耐掉电的器件 / 选带掉电保护功能的器件 | 不带保护的器件在陡降段几乎没有余量 |
窗口宽度不等于软件可用时间。软件真正能用的时间约为:
t_sw = t_pw_eff - (t_detect 掉电检测确认时间 + t_enter 中断响应与入口时间 + t_commit 元数据落盘时间 + t_guard 保守余量)
四项里最容易被漏掉的是 t_detect:很多设计假设「电压一掉就立刻知道」,实际还要经过电源状态引脚的电平判断、去抖或延时确认、中断优先级调度,这些加起来常常吃掉窗口的 30% 以上。
1.4 电池 vs 电容掉电的差别
同样是「非电源供电」,电池与电容的行为差异很大,做方案时不能混着谈。电池提供的是电压平台,电容提供的是电流能力。
| 维度 | 电池(含可充电电池) | 板上储能电容 |
|---|---|---|
| 电压跌落形态 | 平台期长、平台缓,之后才是陡降;断电后有一段「还能撑几秒到几十秒」的窗口 | 从第一刻起就连续下跌,窗口以毫秒计 |
| 内阻 | 毫欧级但随 SOC / 温度显著变化 | 毫欧级且基本不受 SOC 影响 |
| 失效模式 | 过放、极化、鼓包;长期搁置会自放电到不可恢复 | 容量衰减、ESR 上升、介质老化 |
| 能否承受峰值电流 | 受电池内阻与保护板限制,脉冲能力有限 | 瞬时放电能力极强,是为脉冲设计的 |
| 热失控风险 | 有,是系统级安全课题 | 基本无 |
| 成本与体积 | 成本高、体积大、需要充电管理 | 成本低、体积小、免维护 |
| 适合承担的角色 | 系统级掉电保持(秒级:保存上下文、刷盘、上报) | 器件级掉电窗口(毫秒级:撑完一个写事务) |
典型做法是电容撑事务、电池撑流程:板上电容负责把写事务的毫秒级窗口撑住;电池负责掉电后到系统彻底关机之间那几秒到几十秒,用来把数据刷到非易失区、上传告警、进入安全关机流程。只有电容没有电池,系统往往在数据刚落盘完就立刻断电,没有余量做后续动作。
2. 供电保持:储能电容与保持时间工程估算
第 1 章把窗口定义清楚了,本章解决「怎么把它撑出来」。这一章是全文唯一带公式的章节,也是最容易直接抄进设计指南的一章。
2.1 两种备选:体电容(bulk)与 MLCC 的取舍
板上储能电容的候选主要是两类:体电容(铝电解、固态高分子混合)与 MLCC(多层陶瓷)。两者不是替代关系,工程上常常是 MLCC 顶瞬态、体电容顶能量的组合。
| 维度 | 体电容(Al electrolytic / polymer) | MLCC(陶瓷) |
|---|---|---|
| 能量密度 | 低,同体积远小于陶瓷 | 高 |
| ESR | 毫欧 ~ 百毫欧量级,且随温度显著上升 | 毫欧级甚至更低,且基本不随温度劣化 |
| 容值稳定性 | 电解液干涸会逐年衰减 | 稳定,但有直流偏压效应(大容量高频下实际容量下降) |
| 低温行为 | 低温下 ESR 显著上升,可能不满足峰值电流 | 低温性能好 |
| 纹波电流能力 | 有寿命上限,大纹波会加速老化 | 几乎无寿命消耗 |
| 机械应力 | 不敏感 | 对板弯敏感,需注意布局与安装(树脂封固、留边) |
| 纹波电流寿命 | 有寿命上限,大纹波会加速电解液老化 | 几乎无寿命消耗 |
| 失效模式 | 容量衰减 → 窗口变窄(渐变,容易被漏测) | 裂纹开路 → 窗口归零(突变) |
| 典型用途 | 承担主要能量,靠近主控与存储的公共回路 | 顶住高频尖峰,紧贴器件电源引脚 |
直流偏压效应:MLCC 的实际容量随施加的直流偏压下降,高容值、大尺寸的型号尤其明显。这意味着「标称 100 µF 的 X5R 陶瓷,在 3.3 V 偏压下可能只剩标称的 50% ~ 70%」。选型必须看规格书里的直流偏压曲线,而不是标称值。
机械裂纹:MLCC 是脆性陶瓷,PCB 分板应力、螺钉压紧、连接器插拔都可能造成内部裂纹,导致瞬时开路。对掉电窗口而言这是「平时正常、偶发归零」的典型故障源。
2.2 保持时间估算算法:E = 0.5·C·(V1²-V2²),含 ESR/ESL 与负载电流积分
最简的估算只用一条式子:可释放能量 E = 0.5 · C · (V1² - V2²),其中 V1 是跌落起点的电压,V2 是跌落终点的电压。
// 最简形式:只考虑电容储能,不计损耗
E = 0.5 * C * (V1*V1 - V2*V2)
t_hold = E / P_load // P_load:恒功率负载功率,电压跌落时电流反升
// 工程形式:把损耗显式列出来
E_net = 0.5 * C * (V1*V1 - V2*V2) // 可释放能量
- I_load^2 * R_esr * t // ESR 焦耳热(随时间累积)
// 寄生电感残能 0.5 * L_parasitic * I_load^2 可能经续流通路部分回灌负载,保守预算中忽略不计
t_hold = E_net / P_load // 恒功率负载口径:反解或迭代求解 t
四个损耗项在实践中按重要性排序如下:ESR 焦耳热通常不可忽略(电流大时它能吃掉可观比例的能量);寄生电感残能与介质自放电在毫秒级窗口里一般是次要项,但在极低电流(µA 级)场景下介质自放电会变成主导项。
| 损耗项 | 表达式 | 什么时候必须算 | 什么时候可忽略 |
|---|---|---|---|
| 负载做功 | W_load = V(t) · I_load(t) 对时间积分 | 永远要算 | — |
| ESR 焦耳热 | P_esr = I_load² · R_esr,积分得 W_esr | 电流 ≥ 100 mA 时通常必须算 | 电流 < 10 mA 且时间极短 |
| 寄生电感残能 | W_L = 0.5 · L · I_load²(可能经续流通路部分回灌负载,保守预算中忽略不计) | 回路电流大、走线长、有大电流开关节点时 | 低压小电流场合 |
| 介质自放电 | 由陶瓷/电解体系的自放电常数给出 | 超低功耗保持场景(µA 级负载、分钟级窗口) | 毫秒级窗口 + mA 级负载 |
| 反向漏流 | 器件与稳压器的反向漏电 | 器件处于掉电关机态的长时保持 | 常规毫秒级窗口 |
真实的负载电流是阶梯状的:空闲时几十毫安,一个写事务冲到几百毫安,GC 期间可能持续更高。把 I_load 当常数算出的窗口通常偏乐观 2 ~ 3 倍。做法是按阶段积分:把一次典型操作拆成空闲 / 读 / 写 / GC 四段,各段用自己的电流与时长代入求和。
2.3 分档速查:不同电流档位需要多大容量
为了让方案早期能快速判断,下表按负载电流分档给出数量级参考。它只回答「大概要这个量级」,不回答「这个料号能不能用」——后者要查规格书的直流偏压曲线与 ESR 温度曲线。
| 负载电流档 | 典型负载构成 | 毫秒级窗口的储能需求 | 体电容标称量级(经验) | 主要风险 |
|---|---|---|---|---|
| ≤ 10 mA | 纯读、状态机驻留、RTC 类负载 | 几百 µF ~ 数千 µF | 47 ~ 220 µF | 低电流下介质自放电占比上升 |
| 10 ~ 100 mA | 单路 SPI 接口 + 轻量主控活动 | 数千 µF ~ 数十 mF | 100 ~ 470 µF | ESR 开始明显,热插拔浪涌 |
| 100 ~ 500 mA | 多路并行存储接口、随机写、GC 活动 | 470 µF ~ 2 200 µF(1 ms 级窗口) | 470 µF ~ 2 200 µF | 峰值电流而非平均电流决定成败 |
| 500 mA ~ 数 A | 多控制器并发、持续大块写、eMMC/UFS 高速模式 | 数百 mF ~ F 级,需多颗并联 | 1 000 µF 以上并联 | 板面积、温升、纹波电流寿命 |
拿「100 mA 档需要 470 µF」这条结论去给一个 800 mA 的系统做设计——这类错误在评审里非常常见。分档只用于判断量级,选型时必须用系统的峰值电流重新算,且要按 1.5 ~ 2 倍余量取整。完整的分档表见附录 B。
2.4 反向电流与掉电后的电感/二极管配合
掉电保持电路有一个容易被忽略的另一半:能量往回流的方向。当主电轨掉电、而某个分支(例如通过保护二极管、或通过仍有外部供电的接口)还带电时,储能电容会反过来向那个分支放电。如果路径上没有整流元件,这条反向电流会造成两类问题。
- 器件反灌:存储器件或主控的电源引脚在 VCC 低于某个门限时允许反向电流,超过额定值会损伤输入保护结构。
- 器件被误唤醒:某些器件的电源引脚存在阈值触发的上电路径,反向电流可能被视为一次「上电事件」,导致器件在主控还没准备好时就开始初始化。
因此保持电路通常包含一个反向阻断元件,把储能电容与「可能还有外部供电」的分支隔开。
| 方案 | 压降 | 反向阻断能力 | 适用位置 | 代价 |
|---|---|---|---|---|
| 肖特基二极管 | 约 0.2 ~ 0.5 V(经验区间,以规格书为准) | 强 | 储能电容到主存储轨的公共点 | 压降直接吃掉窗口预算;大电流下损耗与温升需核算 |
| 肖特基 + P 沟 MOSFET 理想二极管 | 约 10 ~ 100 mV 量级 | 强且压降低 | 窗口极紧、又必须低损耗的场合 | BOM 与调试复杂度上升,需处理环路稳定性 |
| 串联电阻 | I × R | 弱(仅靠压降) | 仅配合整流管做辅助限流 | 会拖慢窗口,不适合当阻断主力 |
| 不做阻断 | 0 | 无 | 全系统同源掉电、且无外部供电分支的场合 | 有外部供电或热插拔接口时绝不可省略 |
① 压降要计入窗口预算。整流管的正向压降在做能量预算时不能忽略——压降抬高电容放电终止电压 V2,使器件实际得到的电压更低、保持窗口更短。
② 有外部供电入口就必须阻断。USB / 调试口 / 充电口 / 通信口都可能成为反灌路径。做掉电保护评审时,先把所有可能从外部送进电的路径列全,再逐条确认阻断措施,这是最常见的漏项。
① 保持时间的账只有一条式子,但要用峰值电流、按分段积分、并把 ESR 损耗显式列进去;② 体电容顶能量、MLCC 顶瞬态,两者分工不是二选一;③ 压降与反向阻断是同一件事的两面,整流管的正向压降要计入预算,外部供电入口必须逐条堵死。
3. NAND 侧的掉电脆弱性
第 2 章解决了「有多少时间」,本章解决「这段时间里 NAND 正在做什么、哪些动作被打断会留下什么」。掉电保护的所有复杂度,最终都落在本章描述的这三类脆弱点上。
3.1 编程中断:已编程页到底是不是「半好」
一次页编程(Program)是一个内部包含上百个脉冲循环的长时间过程(具体时长以正式 datasheet 为准)。掉电如果发生在这个过程中间,结果是部分单元到位、部分单元还在原始态——这就是「半好页」。
关键在于:半好页不等于坏页。它仍然可以被读出来,ECC 仍然可能把它纠对。是否可救,取决于三件事:
- 中断发生在循环的哪个阶段:越靠后,已完成的单元越多,可救比例越高。
- 该页原本的读写类型:全新写入的页半好之后基本无价值;原地更新的页半好之后还残留着旧数据的一部分。
- ECC 能不能纠回来:如果中断只影响少数单元,ECC 可能完全纠回,主机根本感知不到。
| 中断发生的动作 | 残留物 | 数据可救性 | FTL 应该怎么处理 | 上电后的动作 |
|---|---|---|---|---|
| 页编程中途 | 该页部分单元到位、部分仍在原始态 | 低到中,取决于中断位置与该页是否原地更新 | 标记该页为无效,绝不能作为有效数据源 | 读校验一遍,确认可读即忽略该页 |
| 页编程完成、写命令未返回 | 整页数据完好,但主控不确定是否成功 | 高 | 视为成功(幂等写入的前提是页只写一次) | 读校验通过即接受 |
| 多页编程中途 | 前若干页完好,其余页半好 | 中,前面的页是干净的 | 只保留已确认完成的前缀页 | 顺序扫描该区域,采纳完整前缀 |
| 页编程中的读校验(verify)失败重试 | 页数据仍在,但状态未定 | 高 | 按重试次数上限处理,超过即判为坏页并换块 | 由坏块管理接管 |
凡是设计里出现「先写数据、后写标志位」这类两步落盘的地方,就必须假设第一步可能只完成了一半。凡是假设「写失败就等于没写」的地方,都不成立——NAND 上不存在这样的等价关系。
3.2 擦除中断与块状态判定的灰区
擦除比编程更危险,因为擦除是对整块施加高压的批量操作,过程中断留下的状态不是「部分好部分坏」,而是整块处于一个模糊的中间电压态。
更麻烦的是,这种中间态往往能被读出来,而且读出来还基本正确——这意味着系统无法靠读操作可靠地区分「擦干净的块」和「擦到一半的块」。这正是所谓的灰区。
| 块状态 | 物理含义 | 能否读到数据 | 能否直接编程 | 正确的处置 |
|---|---|---|---|---|
| 已擦除(干净) | 全部单元回到低 Vt 的擦除态 | 能(全为擦除态的固定模式) | 可以 | 可正常写入 |
| 擦除中断(灰区) | 部分单元回位、部分单元残留电荷 | 能,且读出值往往「看着对」 | 不能保证 | 重新做一次完整擦除;不允许直接写入 |
| 已写入(使用中) | 单元处在编程后的目标窗口 | 能 | 不能(需先擦) | 按正常数据页处理 |
| 已判定为坏块 | 擦除次数耗尽、校验不可纠 | 能,但数据不可信 | 不能 | 由坏块管理映射到备用块 |
擦除中断后读出的数据往往与期望值一致——因为擦除态本身就是全 1 的固定模式,读出来就是一串合法的全 1。于是系统以为自己读到了正确内容,实际上这块里的数据从未被可靠地写入过。靠读校验发现不了灰区,只能靠「擦除完成标志」这个语义状态。
3.3 映射表 / 元数据损坏:FTL 里最容易死的地方
数据页有 ECC、有校验和、还有坏块保护,容错做得相对到位。真正容易出事的不是用户数据,而是那些描述「数据在哪」的元数据。
这类元数据的共同特点是:体积小、无冗余、地位关键。一块映射表可能只有几十 KB,但它一旦损坏,几十 GB 的用户数据就全部变成无法定位的孤儿数据。
| 元数据类型 | 内容 | 体积量级 | 更新频率 | 掉电敏感度 | 推荐保护手段 |
|---|---|---|---|---|---|
| LBA → PBA 映射表 | 逻辑块到物理块的对应关系 | KB ~ 百 KB 级 | 与写入量同频 | 极高(丢则数据不可定位) | 双副本 + 版本号 + 校验和 |
| 块状态表 | 每块的有效 / 空闲 / 坏块 / 擦除次数 | KB 级 | 中等 | 高 | 双副本 + 校验和,坏块表可另存 |
| 磨损均衡计数 | 各块擦写次数分布 | KB 级 | 慢 | 中(丢则磨损均衡失效) | 允许近似重建,不追求严格 |
| 设备配置信息 | 序列号、几何参数、时序配置 | 很小 | 几乎不变 | 极高(丢则器件不可用) | 双副本 + 校验和 + 恢复默认值兜底 |
| 坏块表 | 出厂坏块 + 运行期新增坏块 | 很小 | 低 | 高(丢则后续会写入坏块) | 出厂表只读 + 运行表双副本 |
3.4 关键区分:电性可靠 vs 语义一致(数据在,但索引丢了)
这是全文最值得记住的一个概念区分。电性可靠回答的是「这一页的比特有没有读对」,由 ECC 负责;语义一致回答的是「这些页拼起来是不是一个自洽的系统」,由元数据与协议负责。
两者是正交的:可以出现「电性全对、语义全错」的情况——这正是掉电事故中最常见、也最难排查的一类,因为它不会触发任何 ECC 告警,看起来一切正常。
| 组合 | 电性 | 语义 | 外部表现 | 排查难度 | 恢复手段 |
|---|---|---|---|---|---|
| ✓ / ✓ | 全部读对 | 自洽 | 系统正常工作 | — | — |
| ✓ / ✗ | 全部读对 | 索引错乱 | 数据看起来在,读取时却拿不到 / 拿到错的 | 极高(无告警可循) | 扫描重建映射,或从备份副本恢复 |
| ✗ / ✓ | 部分读错 | 结构自洽 | ECC 告警、校验和不匹配 | 低(有明确告警) | ECC 纠错 / 重读 / 标记坏块 |
| ✗ / ✗ | 部分读错 | 结构错乱 | 大量错误码、数据完全不可用 | 中(告警多但无规律) | 通常只能整区废弃 + 重建 |
掉电保护验证的判据,不能只看「有没有报错」,必须看「上电后数据结构是否自洽」。一个不报任何错、但映射表错乱的系统,比一个明确报 ECC 错的系统危险得多——后者会有人去处理,前者会一路带到现场。
4. 原子写实现
本章把「原子写」拆成可验证的三要素,给出三种可直接落地的实现结构,并说明器件本身能帮到什么程度。原子写是所有上层数据完整性方案的地基。
4.1 原子性的三要素:唯一性、持久性、原子性
「原子写」这个词经常被当成一个整体,其实它由三个可以分别验证的性质组成。三者缺一,就不是真正的原子写。
| 要素 | 定义 | 破坏它的典型情形 | 如何验证 |
|---|---|---|---|
| 唯一性(Uniqueness) | 任意时刻只有一个副本是权威的,不存在两个都合法的新版本 | 写新版本时旧版本被提前擦掉;两份副本序号相同无法区分 | 断电后检查是否存在两个自洽且序号相同的副本 |
| 持久性(Durability) | 一旦提交成功,数据在掉电后一定还在,且能读到正确值 | 提交标志写在数据之前;写缓存(write cache)未真正落盘就返回成功 | 提交后立刻断电,重启检查数据完整性 |
| 原子性(Atomicity) | 读到的要么是完整的旧版本,要么是完整的新版本,不存在中间态 | 多扇区顺序写在中途断电;写标志与数据分两次落盘 | 在写事务任意位置断电,检查结果只能是两种之一 |
如果数据与提交标志都是单份的,那么标志必须后写。理由:数据写一半就断电,标志未置位 → 上电后不认这批数据,干净地重做。反过来(先写标志)则会出现「标志已置位但数据不全」,系统会拿一份残缺数据当权威,这是不可接受的。
4.2 双缓冲 + 版本序号 + 校验和的经典实现
这是性价比最高的结构:两份缓冲区轮换写入,每份带一个单调递增的序号和一份校验和。它的三条性质都很容易验证,因此在嵌入式与 Bootloader 场景里被广泛采用。
| 结构字段 | 类型 / 宽度 | 作用 | 写入时机 | 掉电风险 |
|---|---|---|---|---|
| magic | 固定常量 | 快速判定「这是不是一个缓冲区」,避免把空白区误认成有效副本 | 随本副本一同写入 | 低(写在最前,但只作为快速判定,不作为权威依据) |
| seq | 32 位或 64 位单调序号 | 区分新旧,是唯一性的核心 | 随本副本一同写入 | 低 |
| len | 长度字段 | 界定负载区范围,防止越界读 | 随本副本一同写入 | 低 |
| crc | CRC-32 或更强 | 判定本副本是否自洽 | 写完负载后最后写入 | 中(必须最后写) |
| payload | 实际数据 | 业务内容 | 最先写 | 高(中断后可能不完整) |
4.3 TLV / 日志块式原子写的取舍
当需要原子化的对象不是「整块配置」而是「成千上万条独立记录」时,双缓冲就不经济了(一次小改动要重写整个缓冲)。这时常用的是 TLV 记录式与日志块式两种结构。
| 维度 | 双缓冲整体式 | TLV 记录式(追加式) | 日志块式(原地更新 + 影子记录) |
|---|---|---|---|
| 最小写入粒度 | 整块重写 | 一条记录(几十字节) | 一条记录 |
| 空间开销 | 2 倍 | 约 1.3 ~ 1.8 倍(含索引与对齐) | 约 1.5 ~ 2 倍 |
| 单条记录原子性 | 天然具备 | 依赖「记录长度 + crc + 提交位」的局部设计 | 依赖日志提交语义 |
| 查找效率 | O(1) 直接定位 | 需要索引;可用有序数组 + 二分(更新成本高)或哈希 | 需要索引 |
| 删除一条记录 | 重写整块 | 追加一条 tombstone,逻辑删除 | 追加 tombstone 或走日志标记 |
| 碎片累积 | 无 | 明显,需定期整理(GC) | 明显,需定期整理 |
| 掉电最坏损失 | 丢最近一次更新前的全部状态 | 丢最近若干条未提交记录 | 丢整个未提交事务 |
| 适用规模 | KB 级 | KB ~ 百 KB 级、变更分散 | 百 KB ~ MB 级、变更分散 |
TLV(Type-Length-Value)结构的优雅之处在于:它把「一次原子写」的问题,转化为「多条独立追加的原子记录」问题。每条记录自带类型、长度、校验和与提交位,写一条不影响其他条,代价是空间开销与碎片管理。
它有一个常被忽略的前提:追加必须是原子的。如果介质或驱动不保证「追加一段不超过 N 字节的数据不会撕裂」,那么在记录中间断电会留下半条记录,必须靠长度字段与 crc 把它识别并丢弃——这就要求 crc 与提交位必须放在记录的末尾。
4.4 flash 器件自身提供的原子写能力
器件层面能提供的「原子性」其实非常有限,但它确实存在几条硬约束,弄清楚能省掉很多软件负担。
| 器件能力 | 含义 | 能保证什么 | 不能保证什么 | 典型量级(以 datasheet 为准) |
|---|---|---|---|---|
| 页内编程的粒度约束 | 编程以页为单位,不能只改页内几个字节 | 整页编程是一次操作 | 不能保证页内部分字节更新 | 页大小常见为 2 KB ~ 16 KB |
| program page 对齐 | 写入地址必须按页对齐,写入长度不超过一页剩余量 | 跨页写会被拆成多次页操作 | 跨页写的中途断电仍会留下部分完成 | 以器件手册的页大小定义为准 |
| 多字节单写命令 | 一条写命令可以一次写多个字节(受总线与器件实现限制) | 命令粒度内的数据一起被接受 | 不保证命令粒度大于页时的一致性 | SPI NOR 常见可一次写一整页或更多 |
| 内部 ECC | 部分器件带片上 ECC,能纠正若干 bit 错误 | 提升电性可靠性 | 完全不解决语义一致性问题 | 纠正能力以规格书为准 |
| 器件级的掉电保护 | 少数带 FTL 的器件宣称元数据掉电保护 | 降低映射表损坏概率 | 不覆盖应用数据与协议语义 | 依主控实现而异,须逐项确认 |
页对齐是寻址约束,不是一致性约束。一条跨越两页的写命令在控制器内部会被拆成两次页操作,两次之间完全可能断电。这在手册里通常写成「一次 program 命令最多一页」或「不得超过页边界」——读手册时务必区分这两类表述。