摘要
网络 OTA 的核心不是把新固件写进 Flash,而是保证任何时刻掉电、复位或新镜像启动失败时,设备仍能回到一个可信、可启动的版本。本文以 AS32S601 的 eFlash 约束为边界,给出 A/B 双镜像、双份状态记录、Pending 启动意图与确认回滚机制的实现框架。文中涉及寄存器位值和厂商底层函数的地方均保留为 BSP/伪代码接口,避免把未经验证的实现细节当成芯片事实。
1. 为什么 OTA 不能覆盖正在运行的镜像
单镜像升级的最大风险是:下载任务、Flash 驱动和业务代码本身都在当前镜像中执行。擦除或改写当前执行区,会让取指、异常处理和中断向量同时失去稳定性;即使暂时没有跑飞,掉电也会留下半写入镜像。
- 写入目标必须是非活动区;活动镜像在新版本确认前始终保留。
- “下载完成”不等于“可启动”:镜像完整性、状态记录提交、首次启动与应用确认必须分阶段处理。
- 回滚不是故障补丁,而是启动器在超时、复位或确认失败时的正常决策路径。
2. AS32S601 eFlash:决定方案边界的硬件约束
根据芯片设计手册第 5 章:PFlash 最大容量为 2 MB(4 个 512 KB block),DFlash 最大容量为 512 KB。eFlash 的 sector 为 4 KB、row 为 512 B;编程最小粒度为 64 bit,目标地址需 8 字节对齐,并且一次编程不得跨越 row。擦除命令为 sector erase 0x01、block erase 0x02,编程命令为 0x10。
状态轮询应围绕 BUSY 与 FINISH,错误收敛至少检查 OPERR、WPERR、ECC1ERR、ECC2ERR。设计手册还明确:当一个 block 正在擦除或编程时,只支持读取另一个 block。因此 Bootloader、Flash 驱动和被读取的关键常量必须落在与被操作 block 不同的区域,或在进入 Flash 操作前转入 RAM 执行。
时钟配置也应被纳入启动顺序:仅在系统时钟初始化完成后,且 eFlash 空闲(未执行擦除/编程)时设置 EFLASH_CNFG.CLKFRQ。具体频率编码、寄存器位值及解锁序列请以当前芯片版本手册和 BSP 为准,本文不假设其数值。
|
约束 |
已验证的设计事实 |
实现含义 |
|
容量与 block |
PFlash 最大 2 MB / 4 × 512 KB;DFlash 最大 512 KB |
分区只是示例;链接脚本和区大小必须以实际型号容量为准。 |
|
擦除与 row |
sector 4 KB;row 512 B |
擦除按 sector 规划;下载块可小于等于 512 B。 |
|
编程粒度 |
最小 64 bit;地址 8 字节对齐;不得跨 row |
写入前做对齐、长度和 row 边界检查。 |
|
并发读取 |
操作一个 block 时仅可读取另一个 block |
擦/写期间,完整执行路径和中断行为要么都在另一物理 block,要么整体转入 RAM。 |
|
完成与错误 |
BUSY / FINISH;OPERR / WPERR / ECC1ERR / ECC2ERR |
每次操作后等待完成、读取状态、清错并记录失败原因。 |
3. A/B 分区与双份状态记录设计
一个实用的布局是:Bootloader 放在固定、受保护的启动区;应用槽 A 和槽 B 分别存放完整镜像;状态记录放在独立的小型元数据区。这里的容量分配只是一种示例,不能直接套用到所有 AS32S601 型号:链接脚本、镜像上限、状态区位置和分区大小必须与实际 device capacity、block 边界及产品功能一起确定。
状态记录采用双份(Record-0 / Record-1)并增加序号 generation。明确建议把 Record-0 与 Record-1 放在两个可独立擦除的 4 KB sector:更新前先确认另一份(当前被启动器选中的)记录已提交且 CRC 合法,绝不擦除唯一有效副本;随后只擦除非活动记录所在的 sector。这样可避免“擦掉唯一状态页后掉电”的单点风险。
非活动记录的提交顺序应是:擦除后写入除 commit 外的 staged payload(commit 保持擦除态),回读并校验字段与 payload CRC,再把 commit 作为最后一步从擦除态编程为完成态。commit 必须独占一个 8 字节对齐的 64-bit 编程单元,且不纳入 staged payload CRC;最终还要回读,确认 commit、字段和 CRC 同时有效。启动时只在 commit 与 CRC 都正确的副本中选择 generation 更大的记录。
|
字段 |
示例含义 |
启动器处理 |
|
magic / format_version |
记录身份与格式版本 |
不匹配则忽略该副本。 |
|
generation |
单调递增的记录序号 |
在 CRC 合法副本中选较新者。 |
|
active_slot / pending_slot |
当前确认槽与候选槽 |
Pending 仅用于一次受控试启动。 |
|
image_size / image_crc |
候选镜像长度与完整性摘要 |
跳转前按产品策略校验长度和 CRC。 |
|
boot_attempts / confirm_deadline |
试启动计数或确认窗口 |
超限或超时则撤销 Pending 并回到 active_slot。 |
|
record_crc / commit |
payload CRC 与最终提交标志(commit 不计入 payload CRC) |
commit 是独立 64-bit 单元的最后一次编程;任一项无效即回退另一份记录。 |

图 1 总体升级策略
4. 网络 OTA 主流程:先写非活动区,再切换启动意图
- 启动器从双份状态记录恢复 active_slot,并启动已经确认的应用镜像。
- 应用接收版本、镜像长度、摘要和签名等元数据,确认目标为非活动槽。
- 按 sector 擦除目标槽,并以不跨 512 B row 的小块下载、编程和读取回校。
- 完整镜像验证通过后,预先验证当前状态副本仍有效,擦除非活动记录所在的独立 4 KB sector;回读校验 staged payload 后,最后以独立 64-bit commit 单元提交 pending_slot。此时尚未改变 active_slot。
- 复位后 Bootloader 仅把 pending_slot 当作候选镜像试启动;应用自检成功后显式确认,才提升为 active_slot。
镜像认证机制应结合产品安全目标选择,例如受保护的版本元数据、签名验证、防回滚版本计数和密钥存储。CRC 可以发现传输或存储错误,但不能替代来源认证。
5. eFlash 写入实现:擦除、行编程与校验
将网络分包与 Flash 物理写入解耦:网络层可以收到任意长度的数据,但 Flash 层必须把它整理为 8 字节对齐、最多一个 row 的写入单元。跨 row 的尾部数据应留在 RAM 缓冲区,与下一包拼接后再写入;未满 64 bit 的镜像尾部可按镜像格式填充并计入完整性校验。
读写并发约束要按“完整执行闭包”审查,而不是只把一个 Flash 驱动函数放入 RAM:任一 PFlash erase/program 事务期间,要么活动可执行区以及所有可达的指令、常量、向量表和中断处理函数都位于被操作 block 以外的物理 block;要么把完整调用路径连同相关中断行为整体迁入 RAM(或在事务窗口内按产品策略禁止/接管中断)。仅搬移 driver 函数仍可能因常量访问、异常或中断取指而违反跨 block 读取限制。
清单 1 单个下载块的边界检查(BSP/伪代码)
|
static ota_result_t ota_program_chunk(uint32_t dst, const uint8_t *src, size_t len) |
上例的 eflash_unlock、eflash_wait_idle、eflash_program_64bit_units 与状态清除均为 BSP 接口名称。实际实现应在每个 sector 擦除和每次编程后检查 FINISH/错误状态,并把 OPERR、WPERR、ECC1ERR、ECC2ERR 映射为可追踪的失败原因。
清单 2 双份状态记录的安全提交顺序(BSP/伪代码)
|
ota_result_t ota_commit_pending(const ota_state_t *next) |
清单中的 state_* 是表达顺序与前置条件的 BSP/伪代码接口,不代表厂商 API。`state_erase_inactive_record_4k_sector` 只能针对未被当前启动决策依赖的那一个记录 sector;`state_write_payload_except_commit` 保持 commit 擦除态;`state_program_commit_64bit` 则把独占、8 字节对齐的 commit 单元作为最后一次 64-bit 编程。payload CRC 的计算范围明确排除 record_crc 自身和 commit 字段,避免 commit 状态变化破坏 staged payload 的校验。
6. 启动确认与自动回滚
候选镜像第一次启动不应立即覆盖 active_slot。Bootloader 可在跳转前递增 boot_attempts 或写入短期试启动标志;应用完成时钟、外设、配置迁移和业务自检后,调用一个受控的确认接口。若确认窗口内发生看门狗复位、硬复位或再次上电,Bootloader 保持/撤销 Pending 并回到已确认槽。
清单 3 启动器选择逻辑(伪代码)
|
boot_target_t boot_select_target(const ota_state_t *state) |

图 2 启动确认与回滚
7. 断电恢复:把每一个中间态设计成可启动
断电恢复设计的关键是区分“数据还没写完”和“意图已经提交”。擦除/下载中的非活动镜像可以丢弃;状态切换则必须先验证另一独立记录 sector 有效,再擦除非活动 sector,随后写入 payload、回读校验,最后以单独的 64-bit commit 单元提交。启动器永远只信任 CRC 与 commit 都正确的最新记录,因此在任意写入指令之间掉电都能找到旧记录或新记录,而不是半条记录。
- 擦除过程中掉电:目标槽保持无效,已确认槽继续启动。
- 镜像写入过程中掉电:不写 pending_slot,下一次 OTA 从断点策略或重新下载开始。
- 状态副本擦除、payload 写入或预提交回读中掉电:commit/CRC 不通过,启动器选择另一独立 sector 中的旧副本。
- Pending 试启动中掉电:确认未发生,按尝试次数或确认窗口回到已确认镜像。
- 确认写入中掉电:双份状态记录保证至少保留一个可判定的有效副本。

图 3 异常与断电恢复
8. 安全边界与测试清单
A/B 解决的是可用性和可恢复性,不自动等于安全 OTA。生产方案应把传输保护、镜像来源认证、密钥生命周期、版本防回滚、调试口策略和故障日志纳入同一威胁模型。不要把下载通道的 TLS 或 CRC 当成固件签名验证的替代品。
|
测试项 |
注入方式 |
期望结果 |
|
row 边界 |
在 512 B row 末尾构造跨界分包 |
Flash 层拒绝跨 row 写入或正确拆分,不产生越界。 |
|
对齐与尾包 |
制造非 8 字节地址、非完整 64-bit 尾部 |
按镜像格式缓冲/填充;非法地址被拒绝。 |
|
擦除/写入掉电 |
对每个 sector、row 的关键操作点断电 |
旧 active_slot 仍可启动,未完成目标槽不被选择。 |
|
状态记录掉电 |
在另一有效副本的前提下,对 inactive 4 KB sector 的擦除、payload、回读、独立 64-bit commit 间断电 |
启动器选择 CRC+commit 有效的另一份记录。 |
|
读写并发闭包 |
擦/写 block 时触发可达常量访问、异常或中断 |
完整执行路径在另一 block 或 RAM;不得仅验证 Flash driver 在 RAM。 |
|
Pending 失败 |
候选镜像看门狗复位或不确认 |
超出策略后自动回滚至确认槽。 |
|
错误状态 |
由测试桩模拟 OPERR/WPERR/ECC 错误 |
记录失败原因、清理状态,不提交 pending_slot。 |
|
安全验证 |
篡改摘要、签名或版本号 |
认证失败不切换启动意图,保留已确认镜像。 |
9. 结语
AS32S601 的 sector、row、64-bit 编程和跨 block 读写限制,决定了 OTA 不应只被实现为一个网络下载任务。把非活动槽写入、镜像校验、双份状态、Pending 试启动和应用确认串成一个原子性逐步增强的流程,设备就能在升级失败和断电场景下保留可启动路径。落地前,请以实际芯片容量、当前版本手册、BSP 和链接脚本为最终依据完成地址规划与底层寄存器实现。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/ANSILIC/article/details/163994607




