只读分享解决方案文档原理方案设计📅 2026-10-14🔖 Rev 1.0⏳ 还有3日到期
🔧解决方案&应用市场分析 -> 原理方案设计 -> 电子设计&技术探讨 -> 固件OTA与AB更新架构_技术探讨

固件OTA与AB更新架构_技术探讨

同系列 · 原理方案设计
扫码打开本页
微信扫码

固件 OTA 与双分区 / A-B 更新架构 · 技术探讨

更新这个词被用得太随意了 —— 本文把它拆成传输、写入、切换三件互不相同的事,分别讲清它们的原子性关口、判据与回滚手段。面向嵌入式存储与固件工程参考。 | 编制日期 2026-10-14 | 版本号 Rev 1.0

3 个关口
传输 · 写入 · 切换
各自独立,不能合并成一步
原子不等于可靠
闪存的擦写本身不是原子操作
原子性只能靠顺序与冗余构造出来
1 条底线
任何时刻都要有一个可启动镜像
这句是全部设计的唯一公理
A / B
用空间换恢复能力的经典做法
代价是可以算出来的容量
报告总结

绝大多数「OTA 方案」文章讲的是怎么把包传下去:用什么协议、怎么断点续传、服务器怎么编排灰度。这些内容重要,但它们解决的是失败代价最低的那一半问题 —— 传输失败无非重来一次,设备在原地运行得好好的。

本文讲的是另外两件更难的事:写到哪里、按什么顺序落盘,以及什么时候新的那一份才算真正生效、生效不了怎么退回来。核心矛盾只有一句:更新要求原子,但闪存的擦写本身不是原子的,而且设备随时可能在任何一个字节之后断电。

全文围绕「三个原子性关口」这个框架展开:传输关口、写入关口、切换关口。任何一处把两步并成一步来省事,都会在断电时刻留下无法判定的半写状态。

前言

做固件的人迟早会遇到同一个噩梦:设备在现场更新到一半断电了,从此再也起不来。事后复盘时,往往发现所有「该做的」都做了 —— 用了 TLS 通道、做了分块校验、写了重试逻辑 —— 唯独没有回答一个问题:假如现在这一刻掉电,上电后第一段代码会读到什么?

这个问题问的已经不是传输,而是可信性:系统在任何时刻被切断电源,重新上电时都必须落在一个可以被明确判定的状态里 —— 要么明确地跑旧版本,要么明确地跑新版本,要么明确地进入恢复流程;绝不能出现「看情况,取决于运气」。

与第 2 章配合的这个视角,也划清了本文与《嵌入式启动介质与启动架构》篇的边界:那篇回答「谁负责把第一段代码读起来」,本文回答「当读到的东西是坏的、或者刚刚被换过一轮的时候,谁负责改判、按什么凭据改判」。

结论: OTA 的核心不是把新固件传下去,而是保证更新过程中的任何一次中断都不会把设备变成砖。 做法是:把它拆成传输、写入、切换三件彼此独立的事,分别为它们建一道原子性关口 —— 传输关口允许任意次失败且零代价;写入关口保证「旧镜像在任何一个断电时刻都仍然完整」; 切换关口由一个持久化的、可被下一段代码读懂的状态来决定「现在跑哪一份」,并且任何没能在确认窗口内证明自己活着的新版本都必须能自动退回。

读者路径

1. 为什么 OTA 是一个可信性问题而不是传输问题

这一章给出全文的分析框架。结论先行:把 OTA 当成传输问题的团队,会把绝大部分精力花在代价最低的那一段上,而在代价最高的那一段裸奔。

1.1 OTA 失败的三种代价

「更新失败了」是一句信息量极低的描述。同样是失败,发生在还没有开始写的那一刻,和发生在已经把旧镜像的前 4 KB 擦掉的那一刻,是两种完全不同的事件。要讨论设计,先把代价量化。

工程上把 OTA 失败的代价分三类,它们的数量级相差好几个数量级,这决定了资源应该怎么分配:

代价类别典型表现是否需要人到场谁来承担数量级
① 可用性代价设备重启一次或几次,服务中断几十秒到几分钟;最终回到旧版本继续提供服务不需要,系统自愈产品口碑,通常可控最轻:可接受
② 返工代价设备无法自恢复,需要现场拆机、接串口或返厂;单台处理成本远高于设备本身的毛利需要,且往往要人到现场售后与供应链,可能侵蚀全部利润重:可能击穿项目
③ 信任代价信任常被误以为只是安全问题,实际还包括:回滚到了有已知漏洞的旧版本;版本号被恶意降级;更新通道被当作投放入口可能需要召回级处置安全团队与合规,可能上升到法律责任最不可控

这里值得强调的是第三种代价常被漏掉。很多团队的防御清单里只有「不能变砖」,却没有「不能悄悄退回到有漏洞的旧版本」和「不能让人往里投放任意固件」。这两条属于更新机制自身的Trustworthy,与传输是否可靠无关 —— 用最结实的 TLS 通道送一个被降级签名的旧固件,攻击照样成立。第 6 章会把这条展开。

三种代价对应三处设计,不要混为一谈

可用性代价主要由切换机制决定(有没有明确的判据、能不能自动退回);返工代价主要由写入路径与恢复手段决定(有没有第二份、有没有 ROM 兜底通道);信任代价主要由签名、版本单调性与密钥管理决定。

一个只在传输层层层加固、而在另外三处裸奔的方案,三种代价一个都不会少。

1.2 「传下去」与「变得可用」是两个独立问题

把「更新」这个词拆开,它其实包含三件性质完全不同的工作:

  1. 传输:把服务端产出的字节串,通过某个通道(蜂窝、以太网、串口、本地总线、离线 USB)搬到设备端的一块暂存区。
  2. 写入:把暂存区里的字节串,按某种顺序写进某个非易失的目标位置,并且在这个过程中不动正在运行的那一套代码。
  3. 切换:在某个明确的时刻,让下一次上电(或下一次跳转)跑到新的那一套代码上去,并且为「它跑不起来」准备退路。

关键区别在于失败的可重试性:传输失败,重试的代价只是时间和流量,设备的状态完全没有被修改;写入与切换失败,设备的非易失状态已经被修改了,重试未必能把状态还原到修改之前。

维度传输写入切换
责任主体服务端 + 设备上的通信栈设备上的更新守护进程 / Loader启动判决者(Loader 或引导加载程序)
失败的判据超时、连接中断、包序号不连续写入/擦除返回失败、回读不一致启动后未在窗口内自确认、或崩溃
失败时代设备的状态没有被修改,旧版本在跑目标分区已被修改,可能是半写状态持久状态:已经改了指向,但还不知道它能不能跑
重试是否有副作用没有,可任意次重试有,整槽重写前必须先擦除有,需要靠尝试计数来防止无限重试
代价上限时间与流量备用槽成为废数据设备起不来 —— 但有自动回滚则为可用性代价
用什么证明做对了整包摘要与签名校验回读校验 + 顺序化的提交断电注入测试 + 尝试计数触底测试

这张表里最需要记住的是第三行:只有传输阶段的失败是「零副作用」的。写入与切换一旦开始,设备的持久状态就已经在被修改,此后的一切设计目标就变成了「让每个中间状态都可判定」。

同一件事被拆成两件:把字节送到位,与让设备开始跑新版本① 把新固件传下去 —— 失败零代价打包与签名服务端产出自洽的完整镜像通道传输整包 / 差分 / 压缩都发生在这一层落到本地暂存暂存区可以是文件,也可以是裸分区② 让它变得可用 —— 失败有代价写入备用槽旧槽必须保持逐字节完整提交元数据一个持久化的宣告:可以试新槽自确认或回滚未自证存活,则由计数触底回退关口一 · 传输起点:服务端产出并签名的镜像终点:设备端暂存区校验通过跨不过去:设备零改动,只损失流量关口二 · 写入起点:暂存区里已校验过的镜像终点:备用槽写毕且元数据已提交跨不过去:备用槽作废,旧槽完好关口三 · 切换起点:元数据已经指向新槽终点:新版本在窗口内自证存活跨不过去:计数触底,自动回落旧槽传输可以失败任意多次,代价是时间与流量;写入与切换的失败,代价是设备还能不能提供服务
图 1 · 更新被拆成两件独立的事:把新固件传下去,与让它变得可用

这张图是全文的骨架。左侧那一列无论失败多少次都不留痕迹;右侧那一列从第一个字节写进闪存开始,设备就有可能被断电,而每一次断电都会留下一个需要被解读的中间状态。

1.3 更新过程的三个原子性关口

「原子性」用在嵌入式更新里,含义与数据库事务并不完全相同。这里把它定义为一个可判定性要求:

更新在任意时刻被中断后,重新上电的那段代码必须能够回答两个问题:现在这个状态属于哪一类?接下来该做什么?如果答案依赖于「运气」或者「写得快不快」,这套方案就没有原子性。

要满足这个要求,只需要盯住三个关口。每个关口都有明确的跨越动作、成功判据和中断后果:

三个关口是串行关系:没越过前一个,就没资格谈后一个的安全① 旧版运行无事发生时② 接收并暂存分块校验 / 断点续传③ 写入备用槽按擦除块顺序落盘④ 改判并重启元数据指向新槽⑤ 自确认清标记 / 计数触底关口一 · 传输关口跨越动作整包摘要与签名校验全部通过成功判据暂存区内容与服务端产出一致此处中断设备零改动,仍在跑旧版本恢复手段重传即可,没有任何残留副作用关口二 · 写入关口跨越动作备用槽写毕,元数据按既定顺序提交成功判据任意断电时刻都至少有一个完整镜像此处中断备用槽成为废数据,旧槽逐字节完好恢复手段整槽重写,旧版本继续提供服务关口三 · 切换关口跨越动作新版本在确认窗口内自证存活成功判据自确认写入成功,或计数触底回落此处中断切换前无代价;切换后须能退回恢复手段尝试计数 + 自动回落 + 标坏三个关口是串行关系:只有越过前一个关口,才有资格谈后一个关口的安全。任何一处把两步合成一步来省事,都会在断电时刻留下一个无法判定的半写状态。
图 2 · 更新过程的三个原子性关口:跨越动作、成功判据与中断后果
关口跨越它意味着什么成功的判据(必须可写下来)在此中断的典型后果恢复手段
关口一 · 传输设备端已拥有一份与服务端逐字节一致的镜像整包摘要通过、签名通过、长度与头部一致暂存区残缺,重启后只是重新下载重传,无副作用
关口二 · 写入备用槽里是一份完整且自洽的镜像,且元数据已宣告它的存在「旧槽头部 + 旧槽体」完好,且新槽头部最后落盘备用槽成为未完成的字节序列整槽擦除后重写
关口三 · 切换下一次启动会跑到新槽,并且有办法知道它跑起来了没有自确认标记写入成功;或尝试计数触底并成功回落到另一槽可能起不来 —— 这是唯一需要回滚机制的关口尝试计数 + 自动回落 + 把相关槽标记为不可用
最常见的错误:把两个关口合成一步

典型反例是「一边接收一边写入(streaming write)」:为了省掉暂存区,直接把收到的字节写进应用分区。这样做等于把传输关口的失败变成了写入关口的失败 —— 网络抖一下,设备的非易失状态就被修改了。

正确的做法是:先完整拿到、先校验通过(关口一),再开始动非易失介质并确保它不会伤到还在跑的旧那份(关口二),最后才改指向(关口三)。暂存区的那份存储介质容量或 RAM,就是为了让这三个关口可以分开而必须付出的门票。

1.4 与启动介质篇的分工:谁负责读起第一段代码

本系列里还有一篇《嵌入式启动介质与启动架构》,它和本文看的是同一条芯片上电到应用跑起来的过程,但角度完全不同。混读容易串线,这里先划清边界:

问题《启动介质与启动架构》篇负责本文(OTA 与 A/B 更新)负责为什么必须分开
第一段代码怎么被读起来是。介质初始化、XIP 还是拷贝运行、BootROM 做什么不。假定这条链路已经存在且能工作启动链路本身的正确性,与更新期间如何不被破坏,是两件正交的事
读到坏镜像时怎么办提到「失败怎么办」这一层为止是。谁改判、按什么凭据改判、改判几次这是本文的核心:启动路径本身可以被复用,判决逻辑才是更新的实质
介质选型(NOR / NAND / eMMC / UFS)是只在影响写入粒度与寿命时引用选型结论在那边,本文只消费它的结论
更新过程中系统能不能保持连续服务不涉及是。三关口模型、A/B 与恢复分区这是可用性工程问题

一句话概括分工:那边负责「读起来」,这边负责「读到的东西换了、或者坏了的时候,改由谁裁决」。

这里有一个容易踩的坑值得单独点出来 —— 有些人把所有期望寄托在不可更新的那一层(BootROM 或掩膜固化的第一级引导)上,希望它来做版本判决、降级保护和回滚。这不成立,原因很简单:不可更新就意味着它对「版本」的理解永远停留在出厂那一天。版本单调性、密钥轮换、新引入的镜像格式,它一概看不懂。把判决逻辑写死在不可更新的层里,等于在出厂的那一刻给整个产品线判了刑。

一条硬规则

凡是需要随产品演进的策略(版本比较规则、哪一种摘要算法、允许哪些槽、降级是否允许),都必须放在可更新的层里;不可更新的层只保留一种能力:在下一层全部失败时,提供一条把字节写进来的恢复通道,以及一个足够保守的默认读取顺序。

1.5 本文主线与读者路径

全文的主线是一条:说清楚「任何时候断电,设备为什么一定还能起来」。后续各章按这条主线推进:

第 2 章
先看更新能挂在哪些环节
沿启动链往下走一遍,标出每一层能不能被现场改写、写坏了能不能退回。这一步给出的是「哪些地方本来就没有第二次机会」。
关键结论:不可更新的第一级必须足够保守。
第 3 ~ 4 章
分区方案与切换机制
单区为什么必然有救不回来的时刻;双区怎么用空间换回恢复能力;切换到底发生在哪个瞬间才算「生效」,元数据按什么顺序写。
关键结论:元数据的落盘顺序本身就是正确性。
第 5 ~ 6 章
字节怎么来,以及怎么证明它可信
整包 / 差分 / 压缩的取舍;镜像头里必须有哪些字段;为什么校验不等于信任;签名、密钥层级与防回滚。
关键结论:版本单调性必须写在持久化状态里。
第 7 ~ 9 章
落到具体介质与平台
擦除粒度、写入顺序、掉电时刻、寿命预算;MCU 路线与 Linux 路线分别是怎么把前面这套抽象机制落地的。
关键结论:介质特性决定具体写法,不决定框架。
第 10 ~ 12 章 + 附录
失败之后怎么办
砖化的四种形态各自还能不能救;现场排障的顺序;横向对照与选型;最后是可以直接摘用的速查表与排查手册。
关键结论:恢复手段必须在出事之前就存在。

关于数值:本文刻意不写寄存器地址、位域、操作码数值与厂商商业数据。所有涉及具体数值的地方(时间、擦写次数、容量开销)都只给量级与关系,落地时请以正式 datasheet / 规范为准。

2. 启动链视角:更新能挂在哪些环节

要讨论更新安全,必须先把「从复位到应用跑起来」这段路拆成层,然后问每一层同一个问题:它能不能被现场改写?写坏了能不能退回来?

2.1 从 ROM Boot 到应用的分层

不同芯片、不同 SoC 的分层名称各不相同(ROM / SPL / SBL / 二级 Loader / bootloader / 应用),但抽象结构是稳定的:每一层负责把机器收拾到一个「能拿下一层」的状态,然后把控制权交出去。更新的绝大部分工作发生在最后两三层。

层级典型载体位置这一层负责什么交给下一层的产出能否现场更新
① BootROM / 掩膜固化引导芯片内部掩膜 ROM,或出厂一次性烧录的受保护区最小介质初始化、按固定顺序找候选镜像、做最基本的完整性检查、进入下载模式一块可以从中找出候选镜像的可读窗口不可(这是它的定义)
② 第一级引导(SPL / boot 块)外部介质的固定起始位置,或介质的专用引导分区把外部存储控制器 / 接口初始化到可用、把下一级搬进内部 RAM一块已经加载进 RAM 并可以跳转的代码通常不可,少数方案支持受限更新
③ 第二级 Loader介质的普通分区(可能是双份)读元数据、选槽、做完整校验与签名验证、加载并跳转、维护尝试计数一个已经验证过、明确知道入口点的应用镜像可 —— 它是更新机制的载体
④ 应用 / 操作系统应用槽 A / B业务逻辑本身不再移交,自己跑到底可 —— 这是被更新频率最高的部分
⑤ 数据与配置独立的数据分区 / 文件系统运行时状态、配置、日志不参与启动判决可,但要与版本兼容

这张表里最值得注意的不是某一层的职责,而是层与层之间的交接面要尽可能窄。理想情况下,上一层只需要知道「下一层放在哪里、多大、头部长什么样、从哪里开始执行」这四件事,其余一概不问。交接面越窄,就越容易做校验,也越容易在未来替换某一层的实现。

启动链五层:每往下一层,可写性上升,回滚责任也上升① BootROM / 掩膜固化引导上电拿起的第一段代码,出厂写死,没有任何第二次机会可更新性:不可更新回滚能力:不适用② 第一级引导(SPL / boot 块)把外部介质初始化到可随机读,再把下一级搬进内部 RAM可更新性:通常不可更新回滚能力:基本没有③ 第二级 Loader读元数据、选槽、校验、签名验证、加载跳转、维护尝试计数可更新性:可更新,但必须受保护回滚能力:靠双份或专用恢复流程④ 应用镜像 A / B被更新频率最高的部分,业务逻辑的载体可更新性:可更新回滚能力:A/B 互为主备,天然可退⑤ 数据与配置不参与启动判决,但必须与应用版本彼此兼容可更新性:可更新回滚能力:靠版本迁移与兼容层判据:不可更新的那一层越靠下,它的行为就必须越保守 —— 因为它没有第二次机会。反过来,凡是可以现场改写的层级,都必须同时准备好「写坏也能退回来」的手段。
图 3 · 启动链五层各自的载体、职责、可更新性与回滚能力

2.2 不可更新的第一级能兜住什么

BootROM 与掩膜固化的第一级引导有一个共同点:它们的行为在产品生命周期内是不可变的。这个性质带来一个极强的推论 —— 它们所有的能力都是出厂那一刻的快照,无法吸收后来出现的任何新需求。

因此,正确的设计思路是给它们做减法:把能做的事情做到最小且足够健壮,把不能做的事情明确地不承担,留给可以更新的下一层。

候选能力不可更新的第一级是否能做如果指望它做会发生什么应该由谁补位
按固定顺序扫描若干候选镜像能,而且这是它最该做的事没有 —— 这是它的本分无需补位
最基本的完整性检查(长度 + 简单校验)部分能,取决于芯片能力指望它做完整加密验证,会导致新算法上线当天全部设备拒绝启动第二级 Loader 做完整的摘要与签名验证
进入下载模式(串口 / USB / 网络)通常能,且这是出厂唯一留下的救命通道若连这条通道也依赖可被覆盖的配置,通道本身会一起坏掉保留不可覆盖的默认触发方式(管脚 / 按键时序)
理解版本号、比较版本单调性不该做版本规则一改,老版本策略与新策略冲突第二级 Loader 读持久化的版本基线
理解业务语义的多镜像选择不该做分区语义一变,第一级就读偏第二级 Loader 读分区表与槽位元数据
一个致命的反模式

把「优先启动哪一个镜像」这个决定硬编码在不可更新的第一级里。这个决定一旦出货就永远无法改进:后来想引入三槽轮换、想支持动态 slot 优先级、想按健康标记剔除某个槽,全部做不到。

正确的做法是让第一级只做无状态的事情:按既定的几个候选依次尝试,谁能通过最基本的检查就交给谁;而「优先谁」这个信息,放在可以被解析、可以被演进的元数据里。

2.3 可更新的第二级 Loader

第二级 Loader 处在一个尴尬的位置:它是整个更新机制的执行者,同时它自己也是一个需要被更新的固件。它负责校验别的镜像,那谁校验它?它负责在应用挂的时候回滚,那它自己挂了怎么办?

这个问题没有免费的答案,只有三种各有代价的解法:

策略做法好处代价与风险适合谁
A · Loader 出厂后冻结Loader 永远不参与常规更新,只更新应用层最简单,风险最低;Loader 的行为在整个产品生命周期里稳定Loader 里的 bug 无法通过 OTA 修复;功能上限受出厂版本约束资源紧张、生命周期短、或已经把 Loader 打磨到足够稳的量产产品
B · Loader 双份 + 由下一层选择Loader 也做 A/B,或者在 Loader 前再加一层极薄的选择器Loader 可以 OTA 演进,且写坏一份还能退回另一份多一份容量开销;多一层选择逻辑,也就多一处需要考虑断电的顺序生命周期长、后期还要加新算法或新外设的网关、车载类产品
C · Loader 只能走恢复流程更新常规走应用层,Loader 只在专门的维护模式下更新常规路径足够简单;Loader 更新被视为一次有监督的维护操作Loader 的缺陷无法远程自愈,必须触发一次专门的维护流程有条件做现场维护、或法规要求关键固件变更需受控执行的产品

无论选哪一种,有三条纪律是共通的:

  • Loader 必须验证它要加载的东西,不能把信任交给「上一次似乎成功了」。
  • Loader 自身的更新必须走与业务镜像同样严格(通常更严格)的流程,因为它是最不能被写坏的那一份。
  • Loader 必须能在「自己刚刚被更新过,但应用还没起来」这个状态下被识别 —— 也就是说,Loader 的更新也必须有「待确认」语义,只不过它的确认者通常是紧接着的一次成功启动。

2.4 应用镜像的位置与启动判决

「应用镜像放在哪里」看似是个地址问题,实际上是个元数据驱动问题。硬编码绝对地址的好处是简单,代价是:一旦分区布局调整,整个启动链都要跟着改,而分区布局恰恰是产品演进过程中最容易变的东西之一。

更稳的模型是:槽位是逻辑概念,物理位置由分区表给出,当前该跑哪个槽由持久化元数据给出。判决的执行者(第二级 Loader)只做三件事:读元数据 → 选候选 → 验证候选。

左边是介质上放什么,右边是启动时读什么 —— 判决只依赖右侧这几个字段介质布局 · 一份典型双分区安排元数据区 · 通常双份冗余第二级 Loader(可能双份)应用槽 A · 当前活跃应用槽 B · 当前备用数据与配置区判决元数据 · 必须持久化的那几个字段槽位状态:活跃 / 备用 / 待确认 / 已标坏尝试计数:连续未能通过确认的次数上一次槽:用于判定刚刚是不是一次尝试各槽版本:用于单调性检查与防回滚成功标记:上一次确实跑起来了回滚标记:不要再试这个槽了这些字段自己也会在写的过程中被写坏,所以它们必须自带冗余与校验,并且写在上一步完成之后 —— 顺序本身就是一种正确性保证。情形① · 无待确认标记按槽位状态直接选槽,头部校验通过就跳过去,通不过就换另一个槽再试,两个都不过才进恢复路径。情形② · 有待确认标记说明上一次是改判后的一次尝试;先看尝试计数是否触底,触底就把该槽标坏并回落到另一槽。情形③ · 两槽头部都坏已经不属于常规判决范围;交给恢复手段:下载模式或恢复分区,以及人工介入。
图 4 · 双分区介质布局与启动判决所依赖的持久化元数据字段

这里有一个关键的顺序问题常被搞反:是先「判决」再「校验」,还是先「校验」再「判决」?

正确的答案是先读元数据选出一个候选,再对候选做校验,校验不通过就换下一个候选。原因是:元数据本身很小、校验很快、而且可以有冗余;而逐个校验所有候选镜像的代价太大(尤其是要验签名的时候)。把元数据作为「先看哪里」的索引,把真正的重验证留给被选中的那一个,既能保证安全性,也把启动时间压了下来。

这条结论还有一个隐含要求:元数据自身必须是可校验的,否则「元数据说 A 槽坏了」这句话本身可能是字节翻转的产物。第 4 章会把元数据的冗余、校验与落盘顺序展开。

2.5 各环节的可写性与回滚能力对照

把前面几节合并成一个可以贴在评审会上的结论表:

环节能否现场改写写坏之后的直接后果是否有第二次机会必须具备的保护
BootROM / 掩膜固化引导不能芯片级失效,通常不可现场修复没有不需要保护机制,只需要它自身极度保守
第一级引导(SPL / boot 块)通常不能,少数受限支持设备连第一段 Loader 都拉不起来基本没有写保护、默认候选顺序、不可覆盖的下载通道
第二级 Loader能,但应当谨慎失去判决能力,即便应用完好也起不来取决于是否做了双份双份或恢复分区、待确认语义、清晰的失败判据
应用槽 A / B能,且是常规操作该槽无法启动有,另一槽完好尝试计数、自动回落、写前整槽擦除
元数据区能,且写得非常频繁判决失去依据 —— 后果比单个应用槽坏更严重靠冗余与双份双份、序号、自身校验、原子更新
数据与配置能应用起来但行为异常有,靠格式本身的向后兼容版本迁移脚本、默认值兜底
这一章要记住的一句话

越靠下的层越不可更新,所以它必须越保守;越靠下的层越承担「给出下一个候选」的责任,所以它的判断依据(让对方能读懂的那点元数据)必须越小、越冗余、越容易被重新算出来。

3. 分区方案:单区、双区与 A/B

分区方案回答的是一个非常具体的问题:当我要写新镜像的那一刻,正在运行的那个镜像放在哪里、它会不会被这次写入碰到。

3.1 单区就地更新的三种失败模式

单区方案(就地更新 / in-place update)的定义很简单:整个介质上只有一份应用镜像,更新就是把新的字节覆盖到旧的字节上面去。它的优点是几乎不占额外空间,实现也最短 —— 一个发送端、一个写介质的循环、一个跳转地址。

它的缺点同样简单:从「擦除命令生效」到「新镜像写完并通过校验」之间,介质上不存在任何一份完整的、可启动的应用镜像。这段时间里只要断电,设备的凭据就断了。

需要区分的是三种后果完全不同的失败模式:

失败模式中断点在哪里介质上剩下什么现场表现能否自愈
模式① · 擦了旧的没写完新的擦除完成之后、写入完成之前既没有完整旧镜像,也没有完整新镜像(可能连头部都被抹掉)上层 Loader 能起来,但没有任何应用可跳不能,只能靠下载模式或恢复分区
模式② · 写完了但接管信息不一致镜像体写完之后、头部或长度 / 入口信息更新完成之前新旧字节混在同一块区域里,摘要校验通不过可能被判为成功也可能直接卡死,取决于校验做在哪一步不能,而且极易被误判为成功
模式③ · 起来了但没人替它打包票新版本实际运行之后只有新版本,旧版本已经被无可挽回地覆盖反复重启、功能异常、跑一阵就崩不能 —— 唯一的出路是重新刷写

模式①最直观,也最让人警觉;真正阴险的是模式②和模式③。模式②的问题在于「看似成功」—— 如果只检查「写入返回 OK」而不做回读和摘要比对,设备会把一个半新半旧的镜像当成好东西来启动。模式③的问题则在于它根本不存在退路:即使新版本明显跑不稳,也没有另一份可以回去。

单区就地更新:有一段无法回避的「没有任何完整镜像」的时间窗从擦除生效到写入完成 —— 这段窗口的长度由镜像大小与写入吞吐共同决定旧镜像完整在线擦除命令发出新镜像写入完成校验通过并生效既没有旧镜像,也没有新镜像断电无影响新镜像接管模式① · 擦了旧的却没写完断在哪里擦除之后、写入完成之前介质剩什么两份都不完整能自愈吗不能 —— 只能靠第二份或下载模式模式② · 写完了但接管信息不对断在哪里镜像体写完、入口与长度标记写到一半介质剩什么新字节与旧字节混在一处能自愈吗不能 —— 且更容易被误判为成功模式③ · 起来了但没人替它打包票断在哪里新版本跑不稳,反复重启介质剩什么只有新版本,旧版本已被覆盖能自愈吗不能 —— 唯一出路是重新刷写单区不是不能做,而是必须承认:这段窗口里唯一的救命稻草,是介质之外的那条恢复通道。换句话说 —— 选单区,等于把「能不能救回来」的全部赌注押在下载模式能不能被触发上。
图 5 · 单区就地更新的「无镜像窗口」与三种无法自愈的失败模式
单区方案唯一能成立的前提

只有在满足以下全部条件时,单区就地更新才是可以接受的方案:

  • 存在一条不依赖任何可更新内容的恢复通道(例如出厂固化在 ROM 里的串口 / USB 下载模式,且有不可被覆盖的触发方式);
  • 更新窗口可以被人为控制在「设备不会被断电」的场合,或者说设备有人值守、有可靠供电;
  • 更新失败的代价可接受 —— 例如开发样机、短期试用机、或本来就有现场维护排期的设备。

任何一条不满足,都应该认真考虑双区。

3.2 双区(A/B)的切换模型

双区(A/B,也常写作双槽、slot A / slot B)的核心思想只有一句:任何时刻都只有一份镜像在被运行,而写入永远只发生在当前不运行的那一份上。

这句话的威力在于它把一个复杂的顺序问题变成了一个简单的归属问题:既然正在跑的那一槽不会被碰,那么「更新写到一半断电」这件事在语义上就不可能破坏正在运行的系统。剩下要回答的只有两个小问题:新写的这一份什么时候生效,以及它如果不争气怎么退回来。

切换从来不是「把新固件拷到运行区」,而是改写一个持久化的指向① 常态槽 A活跃 · 运行旧版本槽 B备用 · 内容任意旧版本在 A 上正常跑,更新还没有开始。② 写入槽 A活跃 · 仍在跑旧版本槽 B正在写入新版本写 B 期间断电,最坏也只是 B 作废。③ 切换槽 A保留 · 退路还在槽 B待确认 · 已改指向改判只是元数据里的一件事,成本高的事早已做完。④ 完成槽 A备用 · 下次的写入目标槽 B活跃 · 已自确认退路重新建立,下一轮从这里开始。双区的本质不是「有两份代码」,而是「任何时刻只有一份在被跑,修改永远只落在不跑的那一份上」这一句话把更新过程中最凶险的那种失败 —— 正在运行的东西被自己写坏 —— 一次性消除了。
图 6 · 双区 A/B 切换模型的四个阶段:写入只落在未被运行的那一槽
阶段槽 A 角色槽 B 角色此刻写入会发生什么最坏后果
常态活跃 · 正在运行备用 · 内容无意义还没有开始写无
写入中活跃 · 继续运行正在被写入新版本只写 B,A 一个字节都不碰B 作废,A 照常提供服务
切换点仍是退路,暂时不被指向待确认 · 尚未被证明能活下来改写一个持久化的指向与标记元数据写坏 —— 这正是元数据必须冗余的原因
确认后备用 · 下次的写入目标活跃 · 已自确认可以开始下一轮更新无

注意表里第四行:切换本身几乎不搬任何东西。这正是双区方案在工程上可行的原因 —— 真正耗时的部分(擦除、写入、回读校验)全都发生在「不急着生效」的阶段,而真正决定生死的那个动作只是一个很小的写操作。

这个性质带来一个重要推论:如果切换动作本身做不到原子且可靠,那么前面搬了多少字节都不会让设备更安全。所以第 4 章要专门讨论这「很小的一步」。

3.3 双区的容量代价与「两份都放不下」

双区不是免费的。它最直接的成本是一句简单的算术:假设介质总容量减去引导区、元数据区、数据区之后,剩下来的可用空间是 S,那么双区方案下单个应用镜像可用的上限约为 S 的一半。镜像尺寸稍一膨胀,就会撞上这堵墙。

需要强调:这里的「约一半」说的是关系,不是精确数字。真实开销还要算上擦除块的对齐损耗、坏块预留、旧布局切换期的浪费,以及更新机制自身(例如第二份元数据槽)所占的地皮。具体请以正式 datasheet 与实际布局为准。

被更新机制吃掉的那部分容量,是从应用可用空间里直接扣掉的单区就地更新应用占比最高应用可用 —— 绝大部分空间都属于它数据 / 文件系统双区 A / B应用约占一半应用槽 A应用槽 B数据区双区 + 恢复分区再被拿走一份应用槽 A应用槽 B恢复分区数据引导 Loader应用槽 A / B恢复分区元数据数据区① 把镜像做小裁掉用不到的功能,把资源数据外置,更新包本身走差分。代价:功能受这里约束。② 第二份放在别处把第二份写到外部SPI NOR 或 SD 卡上,用时再搬进主介质。代价:多一颗器件。③ 第二份只需保底槽可以不对称:新 + 压缩保底镜像,只保证能起能再更新。代价:恢复要一段时间。④ 拆核心包与功能包核心 OS 走 A/B,业务功能包只放一份,坏了重新拉取即可。代价:接口必须清晰。
图 7 · 单区 / 双区 / 双区加恢复分区三种布局下的应用可用容量对照
布局应用镜像可用上限(相对关系)额外的退路来自哪里典型适用场景
单区就地更新最高的那一个:扣掉固定开销后几乎全给应用介质里没有退路,只有介质之外的下载通道镜像很小、或有人值守、或设备本身不重要的场合
双区 A / B大约是可用空间的一半另一槽始终保留着上一份可用镜像绝大多数需要远程维护的产品
双区 + 恢复分区一半再多扣一份专门给恢复用除另一槽外,还有一个不受常规更新影响的最小系统镜像构成复杂、或要求「两槽都坏也能起」的场合
「两份放不下」时的解法怎么做省下多少代价何时不该用
把镜像本身做小裁剪功能、资源外置、压缩后启动时解压取决于镜像构成,通常见效最直观功能被一次性约束:发布后没法临时加回来产品功能仍处于快速扩张期时
第二份放到外部介质备用槽放在 SPI NOR / SD 卡 / 另一颗裸片上主介质的可用空间基本还给应用多一颗器件、多一组布线、多一处可靠性薄弱点对成本与面积极度敏感时
第二份只放压缩保底镜像备用槽只存「能启动且能再更新」的最小系统省得最多 —— 保底镜像可能只有完整镜像的很小一部分恢复过程要一次解压与搬运,比纯 A/B 慢要求恢复必须在极短时间内完成时
拆核心包与功能包核心系统与加载器走 A/B,业务包单份可重拉只需要为较小的核心部分付双份的钱两部分接口必须极其清晰,且业务包要能安全降级业务包坏了设备就完全没服务时
容量账要在方案定稿之前算完

「两份放不下」几乎总是一个可以提前算出的结论:把镜像当前尺寸、未来一到两年的功能规划、以及布局里的固定开销列在一张表里,就能立刻知道会不会撞墙。

最糟的处理方式是先按单区量产,等产品要做远程更新时才改双区 —— 那通常意味着换一颗更大容量的器件,并把整套验证重走一遍。

3.4 恢复分区与工厂分区

「恢复分区」和「工厂分区」这两个词经常被混用,但它们承担的职责不同,混用会导致保护策略搞错:

分区类型里面放的是什么谁会去动它是否需要常规写保护它消失了会怎么样
恢复分区(recovery)一个功能被裁剪到最小、但具备「再更新一次」能力的小系统更新框架:正常启动跳过它,只在恢复流程里进入建议保护 —— 常规路径不该有机会改写它失去自我修复能力,兜底手段下降一档
工厂分区(factory / golden)出厂时的完整镜像,用于「无论如何回到出厂状态」基本没人动;只有特殊维护流程才可能改写应该强保护 —— 理想状态下常规固件完全擦不动它失去最后那条退路,但不影响日常可靠性
暂存 / 下载区(staging)传输阶段临时落脚的那个包每一次更新都会写,写完全就可以丢不需要保护 —— 它本来就是可牺牲的只是本次更新要重来,设备不受影响
出厂校准与设备身份区序列号、校准参数、设备证书一类一旦丢失就无法复现的数据产线写一次,之后基本只读必须强保护,且要有独立备份设备身份丢失,可能引发归属与售后纠纷

这里最容易被忽略的是最后一行。更新机制再完美,也架不住一次分区表错误把序列号与校准区擦掉 —— 那类数据一旦丢了,设备即便还能启动也已经不再是原来那台设备。

一条实用规则:凡是不能被重新生成的数据,都必须放在更新流程永远不会写入的区域,并且独立冗余。「我们不会去写那里」这种口头承诺不构成保护,必须是布局上写不到、或者硬件上写不动。

3.5 分区表自身如何更新

分区表是一个很特殊的对象:它描述的正是「别的更新该怎么落盘」这件事本身。如果分区表自身的更新不安全,那么再好的镜像更新流程也建立在一个可能随时被改写的基础上。

常见做法按安全程度从低到高排列:

做法怎么操作风险适用场合
就地改写单一分区表直接在原位更新那一份描述信息最高:写一半断电就等于不知道还有几个槽不建议用于任何需要远程更新的产品
双份 + 序号 + 自身校验写第二份时用递增序号与校验;读取时取序号最大且校验通过的那份低:任何时刻至少有一份可读推荐作为默认做法
分区表放在受保护区、出厂后不变把布局信息写死在很少被更新的区域,其它都是固定偏移很低,但布局灵活性也最低布局在产品生命周期内确实不会变的产品
追加式世代记录每次布局变更写一条新记录,读取时回溯到最后一条完整记录低,但要考虑空间回收与写满之后的处理布局变动比较频繁、且介质允许追加写的产品
不要在同一次上电里既改分区表又写应用镜像

这是一条经验性禁令:分区表与镜像内容最好不要在同一个断电周期内一起更新。原因是两者的组合状态数量会成倍增加,而其中至少有一种组合是无法判定的(分区表已经指向新槽位,镜像却还是旧的那一套)。

非要一起改时,可靠的做法是分两次上电完成:第一次只把新布局安全地写好并保证旧布局仍可读;确认之后,下一次上电才使用新布局去写镜像。

3.6 分区方案对照

把三种主要方案放在一起对照:

维度单区就地更新双区 A/B双区 + 恢复分区
应用可用容量最高约一半一半再多扣一份
写入失败会不会影响运行中的系统会 —— 这正是它的问题所在不会不会
是否存在「切换」这个动作没有,写完就是它有,且必须被单独设计有,且要能进恢复
回滚能力无有 —— 换回另一槽即可有,且多一档兜底
实现复杂度最低中等 —— 多一个 Loader 的判决逻辑较高 —— 多一个系统要维护
验证工作量小中等:要覆盖三个关口的断电注入大:要额外验证恢复流程本身
适合承担多大代价的失败几乎不承担承担得起单机软件层面的失败承担得起「连更新框架本身都坏」的情况

4. 切换机制:什么时候新固件才算生效

这一章是全文技术密度最高的部分。前面所有搬字节的工作,最后都要落到「新固件到底在哪一瞬间算数」这个点上;而这个点本身,是全过程中唯一必须做得绝对可靠的一步。

4.1 切换点的三个候选

「什么时候新固件才算生效」这个问题有几个天然的候选答案。它们的差别看起来只是措辞不同,实际决定了整套方案的失败代价:

候选切换点表述为什么会被想到它到底错在哪里结论
候选① · 最后一个字节下载完成包收完了,就算更新成功实现最简单,逻辑最顺此时新镜像还在暂存区,介质上的运行内容压根没动 —— 它描述的是「传输完成」而不是「更新完成」错:这只是关口一,不是关口三
候选② · 写入非易失存储的那一刻写完那一刻起,新固件就是「当前的」了考虑了持久化,看起来更认真写入是一个持续过程,没有「那一刻」;而且写完之后设备仍在跑旧版本,两套东西同时宣称自己是当前版本更接近但仍然不成立:必须有一个离散的、可判定的瞬间
候选③ · 下一次上电时由判决者读取元数据谁读到这个状态,就从哪一槽启动把「生效」定义为一个可以被重复读出的持久状态它需要额外准备一套元数据,以及一个能在读到坏状态时改判的判决者正确:这是唯一可判定的切换点

关键区别在于可重复性。候选①依赖的是一个稍纵即逝的事件,断电之后没有人知道它发生过没有;候选②依赖的是一个持续过程的中间时刻,本身就定义不清;只有候选③把「当前跑哪一份」变成了一个写在非易失载体上、每次上电都会被读取、读完就能作出决定的状态。

换句话说:生效不是一个事件,而是一个持久状态被下一次启动读到。

4.2 启动标志与尝试计数

要让「当前生效的活动 slot」这个状态能被可靠地读出,至少需要两个持久化变量:

  • 待确认标记(pending / trial flag):表示「下一次上电要走一次可能是新的路径」。它区分了「稳定运行的常规启动」与「刚刚改判过的一次尝试」。
  • 尝试计数(attempt counter / rollback index):表示「这条路已经失败过几次」。它是防止系统陷入无限重启的唯一手段。

这两个变量都必须写在断电后会保留的地方,而且必须能被原子地更新。把它们放在内存里、或者放在会随内核重启而重建的地方,是这一类设计里最常见也最致命的错误 —— 断电之后那里什么都不剩。

待确认标记与尝试计数共同决定了:这次启动该跑哪一份,以及跑挂了怎么办何时进入上一次更新已经确认,或者设备出厂的默认状态状态 ① 稳定态跑哪一份是明确的,不需要额外解释;新槽可以有内容,但没人指向它元数据此刻写着什么待确认标记:无尝试计数:0何时进入写入动作完成之后,元数据刚刚改判到新槽状态 ② 待确认下一次上电会跑到新槽上,但它还没证明自己能在设备上活下来元数据此刻写着什么待确认标记:有尝试计数:1何时进入上一次没能自确认,但计数还没有触限状态 ③ 重试中继续按同一份元数据再试一次;每一次尝试都要留下痕迹元数据此刻写着什么待确认标记:有尝试计数:k何时进入尝试计数达到了预先设定的上限状态 ④ 触限回落把这个槽踢出候选列表,回到另一个槽,并清除待确认标记元数据此刻写着什么该槽被标记为不可用计数归零、槽位互换上限取多大,取决于能容忍的最长中断时间,与偶发故障被误判为不可用的概率。这两个数字必须写在能被读出来的地方 —— 记在内存里的东西,断电之后什么都不剩。
图 8 · 槽位状态的四种取值、迁移条件,以及对应的持久化元数据内容
持久化字段取值由谁写入由谁读取写坏或读不到会怎样
待确认标记有 / 无更新框架在改判那一刻写入;自确认成功后清除启动判决者在每次上电时读取读不到就该走「保守到上一层」的路径,而不是随便猜一个
尝试计数整数,通常与上限比较判决者在每次未能确认时递增;成功后归零判决者读不到时应按最坏情况处理 —— 当作已经触限
槽位状态活跃 / 备用 / 待确认 / 已标坏更新框架与判决者共同维护判决者选择候选时使用应当拒绝启动该槽,而不是「勉强试一下」
版本信息各槽各自的版本,以及一条防回滚基线更新框架在安装成功时更新安全校验环节读取基线的处理要格外保守,见第 6 章
计数归零的时机比计数递增的时机更容易写错

递增通常在「没跑起来」的时候发生,比较容易想到。真正容易漏的是归零:很多实现只在「自确认成功」时归零,却忘了设备因为其它原因(人为重启、远程命令重启、正常关机再开机)而重启的情形。

更稳的约定是把「寿命计数」绑定在一次具体的更新尝试上:只要不是在这次尝试的上下文里失败,就不该累加。判据写不清,现场就会表现为「设备用得好好的,某天突然回滚了」。

4.3 回滚触发的判据

有了尝试计数,接下来要回答的是:什么叫做「这次没通过」?这个问题没有唯一答案,它取决于产品能容忍什么,也取决于做可行性权衡时的取舍。这里给出常见的几种判据与它们的副作用:

判据怎么检测到优点副作用 / 风险适合
看门狗复位且未自确认复位原因指示 + 待确认标记仍在实现简单,覆盖面广:跑飞、死锁都能抓到无法区分「固件有问题」与「外部偶发干扰」作为基础判据,几乎所有方案都会配上
确认窗口超时启动后的一段时间内没有收到自确认不依赖复位原因,逻辑清晰窗口取短了会误杀慢启动的版本;取长了会延长故障暴露时间主判据,通常与前者组合
应用主动上报失败应用自检不过时主动写一个失败标记再重启误判率低 —— 是应用自己说自己不行依赖应用本身的配合;应用如果崩得早,就没有机会上报作为补充判据
与服务端握手成功规定时间内连上服务器并报告版本把「能用」定义得非常贴近业务真实可用网络不可用会被误判为固件不可用联网设备谨慎使用,需与网络状态解耦
关键外设自检通过业务关键的传感器 / 通信口初始化成功贴近该设备的实际功能定义容易把环境问题误判成固件问题作为补充,不宜单独作为回滚判据

一条重要的设计原则:判据应当「宁可迟钝,不要冲动」。冲动地回滚会把「偶发的、本来能自愈的问题」升级成「一次完整的服务中断」,而且一旦开始回滚,往往在很短的时间内连续波及整批次设备。判据越激进,誤杀率越高,这在第 10 章讨论批量更新时会变得更严重。

4.4 元数据的落盘顺序

这是本章最核心的一节。前面说过,闪存的擦写本身不是原子的:擦了不一定写完,写了不一定写完头部,写完头部不一定写完指向。我们能做的最有力的事情,不是去消除这些中间状态(做不到),而是把它们的发生顺序安排成「任何一种中间状态都能被判定」。

核心规律:先写「没有它也能活」的东西,后写「唯一的那个开关」。切换开关必须是整个过程中最后一个落盘的字节。

顺序本身就是正确性:任何一个时刻断电,都还有一种说得清的解释顺序(不可交换)这一步做了什么此刻断电,上电后会看到什么①写入备用槽先写镜像体,把头部留到最后一步写写完之前,这个槽对外是不可用的备用槽是一串作废的字节;正在跑的旧槽毫发无损。②校验并写槽元数据回读比对摘要,写入长度、版本、状态状态写成「已就绪」,但还不被指向最坏也只是多了一份没人指向的完整镜像,无害。③改判全局元数据写候选槽指向、写待确认标记、把尝试计数置 1 —— 这就是切换本身这是唯一的切换点;在此之前任何时刻断电都不改变运行内容。④重启并走到新槽判决者读元数据、选候选、校验、跳转计数在此累加或清零走不到就计数加一再来;触限则标坏该槽并回落。⑤确认窗口内自确认稳定运行一段时间后写入成功标记写入成功即认定这一轮更新完成没写上就被判为失败,计数累加直到触限回落。所有步骤都按「先写可以没有的,后写唯一的开关」排列,切换开关永远是最后一个字节。只要每一步都能区分「没做」与「做完了」,顺序本身就构成了正确性 —— 这是唯一可行的原子性来源。
图 9 · 元数据落盘的正确顺序,以及每一步中断后上电会看到什么

把这张图读成一句话:越往后的一步,它的失败代价越高,所以它必须写得越小、越靠后。第③步是整个更新过程中唯一在语义上不可替代的一步 —— 在它之前,任何中断都只是「白干一场」;在它之后,一切由尝试计数负责。

由此还可以推出几条实用的纪律:

  • 镜像体先写,头部最后写。头部是这段字节串的「自我描述」,它的存在与否就是这支镜像是否成立的唯一凭据。
  • 槽元数据(这一槽的存在与健康状况)先于全局指向。先让世界知道「这份东西存在且完整」,再让判决者考虑要不要用它。
  • 永远不要试图「同时」更新两件东西。没有同时,只有顺序;凡是以为在同时更新的地方,实际都是在赌运气。
  • 任何写完之后都必须回读校验。写入返回成功只是说命令被接受,不代表字节落到位了。

4.5 元数据自身的冗余与校验

元数据有一个残酷的属性:它比它描述的对象更值得关心。一个槽里的镜像坏了,判决者还能去挑另一个槽;元数据坏了,判决者连「有哪几个槽」都不知道。

保护手段做法能防住什么防不住什么备注
双份(或两份以上)冗余同一份元数据保存两遍,各自独立更新单份被写坏 —— 另一份仍然可读两份恰好在同一断电周期内被先后更新的情形必须配合「不同时刻写」的顺序使用,不能同一次断电周期内全写
递增序号每次更新后序号加一,读取时取序号最大且合法的那份分不清谁更新的问题序号本身回绕 —— 需要预留足够宽度与校验配套使用才有意义
自身校验和 / 摘要元数据自带一份校验,读出来先算一遍比特翻转、半写状态恶意篡改 —— 校验不是签名校验代替不了信任,见第 6 章
写完后回读比对写完立刻读出来比对再确认这一步完成写入没有真正落盘的情形回读本身被干扰的极小概率情形成本很低,收益很高的习惯
读到不可用时的保守默认读不到或通不过校验时,按预先写死的保守策略处理元数据整体不可用无 —— 这是最后一道兜底默认策略必须提前定义,不能临时猜
读到坏元数据时,不要「顺手修一下」

很多实现在发现一份元数据坏了但另一份好的时候,会顺手把坏的修回去。这个动作看似贴心,实际会抹掉一次重要的现场证据:到底是哪一个环节在写坏它,从此无从追查。

正确的做法是:使用好的那份继续工作,同时把「坏的这一份曾经坏过」这件事记录下来,并上报。修复动作可以延后到下一次正常的元数据写入时自然完成。

4.6 失败注入测试要点

更新机制的正确性无法靠正常路径的测试来证明,只能靠刻意的失败注入。以下是必须覆盖的项目:

测试项怎么注入期望结果不通过时的典型症状
写入过程中断电扫描在写入过程的多个点上切断电源,逐步扫描整个写入区间任何时刻上电都能启动到某一完整版本,且不出现不可判定状态出现「起不来且没有进入恢复」的状态 —— 这是最高优先级的缺陷
元数据更新瞬间断电在改写全局指向的那一步断电,且扫描该步内部要么用旧的指向启动,要么用新的指向启动,二者必居其一且都能继续运行读到半个元数据后行为随机,或干脆拒绝启动
确认窗口内反复断电在新版本已经启动但尚未自确认时反复断电尝试计数稳定递增,触限后自动回落到另一槽计数不递增(无限重启)或过早触限(一两次就回滚)
强制写入残缺或缺校验的版本人为破坏镜像体某个字节,或在头部写错长度校验不通过,切换到另一个候选或进入恢复带着坏镜像强行启动;或者反过来,好镜像被误判为坏
元数据单份/双份损坏逐份写入已知非法内容使用剩余合法的那份继续工作,并留下记录直接拒绝启动,或静默接受坏的那份
批量回滚压力测试让一批设备同时进入「更新后不可用」状态回滚比例符合预期,服务端有明确的熔断与观察窗口短时间内整批设备一起回滚,服务端失控
这一章的一句话

更新过程中唯一的原子性来源,是顺序:把「可以没有的东西」写在前,把「唯一的开关」写在后。剩下的事情由待确认标记与尝试计数这两个持久化变量接管,它们决定了设备在不知道新版本好坏的时候应该做什么。

相关报告 · 交叉引用 1 器件全部展开收起 ▾
点开类目即可展开:具体文档 / 章节(点章直接跳转到该章)
eMMC
电路设计指南
软件设计规范
Linux 驱动开发
样机问题排查
量产问题排查
其他提及器件(共 2)
SPI NOR
电路设计指南
软件设计规范
Linux 驱动开发
样机问题排查
量产问题排查2
系统设计指南