双备份升级文档
本文档介绍 AC63_GP_MCU 固件中"双备份(Dual Bank / A-B)升级"机制的完整设计与实现,涵盖被动升级 API、升级任务调度、主动升级框架、UART 升级通道、CRC 校验、Boot 信息烧写与失败回退策略。
Purpose and Scope
本文档面向需要理解或扩展固件升级能力的开发者,覆盖以下内容:
- 双备份(A/B Bank)存储布局与启动切换原理;
- 被动升级 API(
dual_bank_updata_api.h)各接口的语义与调用顺序; - 升级任务调度器
dual_bank_update_deal()的状态分发逻辑; - 主动升级框架(
dual_bank_update_loop.h)中的通道抽象、升级类型与状态机; - 基于预留区域文件升级的两个完整示例(
dual_bank_update_demo/dual_bank_update_demo_2); - CRC16 校验、Boot 信息烧写、失败回退等边界行为。
不在本文档范围内的内容:具体 Bootloader 内部实现、量产烧录工具协议、上位机(PC/手机 App)侧的升级协议细节,这些属于独立的配套主题,请参见相关文档。
Overview
双备份升级(Dual Bank Update)是嵌入式系统常用的固件升级方案:Flash 中保存两份 App 固件(运行区 Bank 与升级区 Bank),升级时只写升级区,校验通过后仅改写"Boot 信息"(boot info)即可切换下次启动的 Bank;若校验失败或升级中断,系统仍可回退到旧固件继续运行,从而将"刷机变砖"的风险降到最低。
在 AC63_GP_MCU 中,该机制被抽象为两层:
- 被动升级 API(passive update):由当前运行中的 App 主动调用(例如从 U 盘、SD 卡、预留区域或蓝牙通道拿到新固件数据后),把数据块逐个交给升级任务写入非易失存储。所有接口定义在
dual_bank_updata_api.h。 - 主动升级框架(active update loop):一个统一的上层框架,通过
update_op_api_t通道操作接口把不同数据源(USB、SD、UART、BLE、SPP、NOR Flash 等)抽象为统一的"打开 → 读 → 写 → 校验 → 报告"流程,入口为active_update()。dual_bank_update_uart.c即 UART 通道的实现。
升级的核心思想是**"数据与元数据分离"**:固件数据写入升级区(Update Bank),而决定从哪个 Bank 启动的 Boot 信息只在最后一步才烧写;CRC 校验通过之前,系统状态完全可回退。
Architecture
flowchart TD
subgraph sg_App["App 层(当前运行固件)"]
AppCaller["业务代码 / Demo"]
Api["被动升级 API 层<br/>dual_bank_updata_api.h"]
end
subgraph sg_Core["升级核心(BSP 层)"]
Deal["dual_bank_update_deal()<br/>状态分发器"]
Task["升级任务<br/>passive update task"]
Verify["CRC16 校验器"]
BootBurn["Boot 信息烧写"]
end
subgraph sg_Channel["主动升级框架"]
Loop["active_update()<br/>update loop"]
OpApi["update_op_api_t 通道接口"]
UartCh["UART 通道<br/>dual_bank_update_uart.c"]
end
subgraph sg_Flash["非易失存储"]
BankA["Bank A(运行区)"]
BankB["Bank B(升级区)"]
BootInfo["Boot 信息<br/>(启动选择/回退标志)"]
end
AppCaller -->|"START / DATA / VERIFY"| Deal
Deal -->|"调 API"| Api
Api --> Task
Task -->|"写数据"| BankB
Task -->|"allow_check"| BankB
Verify -->|"读回比对"| BankB
Verify -->|"结果回调"| BootBurn
BootBurn -->|"成功"| BootInfo
BootBurn -->|"失败"| AppCaller
Loop --> OpApi
OpApi --> UartCh
UartCh -->|"UART 数据流"| AppCaller
架构说明:
- App 层是升级的发起者:它决定固件数据的来源(文件、UART、蓝牙等),并按照
START → DATA → VERIFY的固定顺序调用 API。 - 升级核心负责真正写入 Flash、做空间检查、CRC 校验和 Boot 信息烧写。App 层不需要关心目标地址——
dual_bank_update_write()等接口内部自动定位升级区。 - 主动升级框架是可选的上层封装:它通过
update_op_api_t把"数据源通道"抽象出来,使 USB/SD/UART/BT 等升级方式共享同一套升级状态机;DUAL_BANK_UPDATA只是UPDATA_TYPE枚举中的一种升级方式。 - Boot 信息是升级成功与否的唯一"提交点":只有它被改写,系统下次启动才会进入新固件;否则永远从旧固件启动,实现天然回退。
被动升级 API 详解
被动升级(Passive Update)是双备份机制的核心 API 层,全部接口声明在 dual_bank_updata_api.h 中。它的设计思路是:调用方(App)只负责"喂数据",不接触任何 Flash 地址细节;地址计算、擦除、写入、CRC 校验全部由升级任务内部完成。
调用顺序与生命周期
升级过程是一个严格有序的"五步走":
flowchart LR
A["get_dual_bank_passive_update_max_buf()<br/>获取缓冲区大小"] --> B["dual_bank_passive_update_init()<br/>传入 CRC / 文件大小 / 包长"]
B --> C["dual_bank_update_allow_check()<br/>空间是否足够"]
C -->|"不够"| X["dual_bank_passive_update_exit()"]
C -->|"足够"| D["dual_bank_update_write()<br/>循环写入数据块"]
D --> E["dual_bank_update_verify()<br/>CRC16 校验"]
E -->|"通过"| F["dual_bank_update_burn_boot_info()<br/>烧写 Boot 信息"]
E -->|"失败"| X
F -->|"成功"| G["系统复位,从新 Bank 启动"]
F -->|"失败"| X
关键接口语义
| 接口 | 职责 | 设计意图 |
|---|---|---|
get_dual_bank_passive_update_max_buf() | 返回升级任务内部临时缓冲区的最大可用大小 | 调用方据此决定每次 write 的最大数据块长度,避免缓冲区溢出 |
dual_bank_passive_update_init(fw_crc, fw_size, max_pkt_len, priv) | 初始化升级任务,登记新固件的 CRC 值与文件总大小 | 校验参数、分配任务资源;max_pkt_len 决定每次编程的最大长度 |
dual_bank_passive_update_exit(priv) | 退出升级任务 | 释放资源;任何一步失败后都应调用,恢复系统到升级前状态 |
dual_bank_update_allow_check(fw_size) | 检查目标升级区是否有足够空间容纳新固件 | 必须在 init 之后调用;返回非 0 表示空间不足 |
dual_bank_update_write(data, len, write_complete_cb) | 把下载数据拷贝到临时缓冲并通知任务写入非易失存储 | 内部管理"下载缓冲 → Flash 编程"的异步交接;write_complete_cb 返回 0 表示本块编程完成,可发送下一块 |
dual_bank_update_verify(crc_init_hdl, crc_calc_hdl, verify_result_hdl) | 读取已写入 Flash 的全部数据并与 init 时登记的 CRC 比对 | 默认使用 CRC16-CCITT 标准;允许调用方注入自定义 CRC 算法;verify_result_hdl(1) 表示校验通过 |
dual_bank_update_burn_boot_info(burn_boot_info_result_hdl) | 校验通过后烧写新固件的 Boot 信息 | 这是"提交点":只有这一步成功,新固件才可能被启动;err == 0 表示烧写成功 |
flash_update_clr_boot_info(type) | 擦除指定 Bank 的 Boot 信息 | CLEAR_APP_RUNNING_BANK(0)清除运行区 Boot 信息后复位,系统将尝试从另一 Bank 启动——这是回退手段 |
dual_bank_update_read_data(offset, read_buf, read_len) | 读取升级区 Flash 数据(相对升级区偏移) | 供自定义 CRC 校验场景使用,返回实际读取长度 |
设计意图:把"空间检查"与"写数据"分离为独立接口,使 App 在开始下载前就能提前拒绝"固件过大"的情况;把 CRC 校验做成可注入回调的形式,兼顾了"默认开箱即用(CRC16-CCITT)"与"特殊算法兼容"两种需求。
升级任务的内部状态
dual_bank_update.c 中定义了升级任务的三个处理阶段:
typedef struct {
u32 file_size;
u32 file_crc;
u16 max_pkt_len;
} dual_bank_start_info;
enum {
DUAL_BANK_UPDATE_START,
DUAL_BANK_UPDATE_DATA,
DUAL_BANK_UPDATE_VERIFY,
};
Source: dual_bank_update.c
dual_bank_start_info 是升级启动阶段携带的参数包:file_size 为新固件总长度,file_crc 为调用方预先算好的 CRC 值,max_pkt_len 为每次编程的最大包长。三个枚举值对应升级任务的三种消息类型,由 dual_bank_update_deal() 统一分发。
消息分发器:dual_bank_update_deal()
u32 dual_bank_update_deal(u8 msg_type, u8 *data, u32 len)
{
u32 ret = 0;
switch (msg_type) {
case DUAL_BANK_UPDATE_START:
dual_bank_start_info *info = (dual_bank_start_info *)data;
ret = dual_bank_passive_update_init(info->file_crc, info->file_size, info->max_pkt_len, NULL);
if (ret == 0) {
ret = dual_bank_update_allow_check(info->file_size);
if (ret == 0) {
//alloc_check ok, rsp app can update (need user implement api to response app)
} else {
//alloc_check error ,rsp app flash size no enough (need user implement api to response app)
dual_bank_update_exit(NULL);
}
} else {
//cpu resource no enough and rsp app can not update (need user implement api to response app)
dual_bank_update_exit(NULL);
}
break;
case DUAL_BANK_UPDATE_DATA:
ret = dual_bank_update_write(data, len, dual_bank_write_complete_cb);
break;
case DUAL_BANK_UPDATE_VERIFY:
//if app calculate crc no use CRC16-CCITT Standard, then user should implement crc_init_hdl and crc_calc_hdl functions
ret = dual_bank_update_verify(NULL, NULL, dual_bank_verify_result_hdl);
break;
}
return ret;
}
Source: dual_bank_update.c
控制流分析:
DUAL_BANK_UPDATE_START:解析dual_bank_start_info→ 先init(检查 CPU/任务资源)→ 再allow_check(检查 Flash 空间)。两步都通过才允许 App 开始传数据;任何一步失败都调用dual_bank_update_exit()清理现场。DUAL_BANK_UPDATE_DATA:把收到的数据块直接转交dual_bank_update_write(),写完成回调dual_bank_write_complete_cb目前是空实现占位——源码注释明确指出"需要用户实现接口来回复 App 可以发送下一块数据"。DUAL_BANK_UPDATE_VERIFY:以默认 CRC16-CCITT(NULL回调)启动校验,结果通过dual_bank_verify_result_hdl回调上报;若 App 侧使用非标准 CRC,则需在调用处传入自定义crc_init_hdl/crc_calc_hdl。
设计意图:
dual_bank_update_deal()把"消息类型 + 数据"统一收敛到一个入口,方便不同数据源(文件读取、UART 流、BLE 分包)复用同一套升级流程——数据源只管组包,升级任务只管落盘。
主动升级框架与状态机
除了由 App 直接调用的被动 API,BSP 层还提供了一套主动升级框架(update loop),把各种升级通道统一收敛。其类型定义位于 dual_bank_update_loop.h。
升级结果码与升级类型
#define UPDATA_MAGIC (0x5A00) //防止CRC == 0 的情况
typedef enum {
UPDATA_NON = UPDATA_MAGIC,
UPDATA_READY,
UPDATA_SUCC,
UPDATA_PARM_ERR,
UPDATA_DEV_ERR,
UPDATA_KEY_ERR,
} UPDATA_RESULT;
typedef enum {
USB_UPDATA = UPDATA_MAGIC, //0x5A00
SD0_UPDATA, //0x5A01
SD1_UPDATA,
PC_UPDATA,
UART_UPDATA,
BT_UPDATA,
BLE_APP_UPDATA,
SPP_APP_UPDATA,
DUAL_BANK_UPDATA,
BLE_TEST_UPDATA,
NORFLASH_UPDATA,
//NOTE:以上的定义不要调整,新升级方式在此添加,注意加在USER_NORFLASH_UFW_UPDATA之前;
USER_NORFLASH_UFW_UPDATA,
NON_DEV = 0xFFFF,
} UPDATA_TYPE;
Source: dual_bank_update_loop.h
要点解读:
UPDATA_MAGIC (0x5A00)作为枚举起始值,源码注释说明其目的是"防止 CRC == 0 的情况"——即用非零魔数保证结果码/类型值不会与"全 0 的 CRC"混淆。UPDATA_RESULT定义了升级的六种终态:UPDATA_NON(未开始)、UPDATA_READY(就绪)、UPDATA_SUCC(成功)、UPDATA_PARM_ERR(参数错误)、UPDATA_DEV_ERR(设备/介质错误)、UPDATA_KEY_ERR(密钥/认证错误)。UPDATA_TYPE枚举了所有升级数据源通道:USB、SD0/SD1、PC、UART、BT、BLE App、SPP App、双备份(DUAL_BANK_UPDATA)、BLE 测试、NOR Flash 等。注释明确要求新升级方式必须添加在USER_NORFLASH_UFW_UPDATA之前,且不要调整已有枚举值——因为该值可能已固化在上位机协议或存储中,改动会破坏兼容性。NON_DEV = 0xFFFF表示"无设备"的哨兵值。
通道操作接口与升级模式
typedef struct _update_op_api_t {
void (*ch_init)(int (*resume_hdl)(void *priv), int (*sleep_hdl)(void *priv));
u16(*f_open)(void);
u16(*f_read)(void *fp, void *buff, u32 len);
int (*f_seek)(void *fp, u8 type, u32 offset);
u16(*f_stop)(u8 err);
int (*notify_update_content_size)(void *priv, u32 size);
void (*ch_exit)(void *priv);
} update_op_api_t;
typedef struct _update_mode_info_t {
s32 type;
void (*state_cbk)(int type, u32 status, void *priv);
const update_op_api_t *p_op_api;
// u8 task_en;
u8 en;
} update_mode_info_t;
int active_update(update_mode_info_t *info);
Source: dual_bank_update_loop.h
update_op_api_t 是一个典型的"策略模式"接口,把数据源通道抽象为文件式操作:
| 函数指针 | 职责 |
|---|---|
ch_init | 通道初始化,注册"恢复/休眠"回调,用于与系统电源管理联动 |
f_open | 打开升级数据源(返回句柄/错误码) |
f_read | 从数据源读取一块数据到缓冲 |
f_seek | 在数据源中定位偏移(type 区分相对/绝对定位) |
f_stop | 停止通道(带错误码,供上报失败原因) |
notify_update_content_size | 向上层通知升级内容的总大小 |
ch_exit | 通道退出清理 |
update_mode_info_t 将"类型 + 状态回调 + 通道实现"打包,active_update(info) 是主动升级的统一入口。UART_UPDATA 通道正是通过实现这套接口(dual_bank_update_uart.c)接入框架的。
升级任务状态机
UPDATE_STATE_T 定义了升级任务的四个宏观状态,配合 UPDATA_RESULT 终态构成完整状态机:
stateDiagram-v2
[*] --> UPDATE_TASK_INIT
UPDATE_TASK_INIT --> UPDATE_CH_INIT: 通道就绪
UPDATE_CH_INIT --> UPDATE_CH_SUCESS_REPORT: 数据写入并校验通过
UPDATE_CH_INIT --> UPDATE_TASK_INIT: 通道失败 / 用户中断
UPDATE_CH_SUCESS_REPORT --> UPDATE_CH_EXIT: 上报成功 (UPDATA_SUCC)
UPDATE_CH_SUCESS_REPORT --> UPDATE_CH_EXIT: 上报失败 (PARM/DEV/KEY_ERR)
UPDATE_CH_EXIT --> [*]
typedef enum _UPDATE_STATE_T {
UPDATE_TASK_INIT,
UPDATE_CH_INIT,
UPDATE_CH_SUCESS_REPORT,
UPDATE_CH_EXIT,
} UPDATE_STATE_T;
Source: dual_bank_update_loop.h
设计意图:框架把"通道"与"升级动作"解耦——ch_init 里可以挂起低功耗休眠(通过 sleep_hdl),升级过程中通道自己管理数据读取节奏,完成后通过 state_cbk 上报。这样无论数据来自 USB、SD 还是 UART,上层升级流程(擦除 → 写入 → CRC → 烧 Boot 信息)都完全一致。
与被动 API 的关系
- 被动 API 是底层原语,App 需要自行编排顺序、管理数据源;
- 主动升级框架是高层编排器,通过
update_op_api_t通道把数据源标准化,最终仍会调用被动 API 完成 Flash 写入与校验(DUAL_BANK_UPDATA即双备份方式在框架中的注册类型); - 两者共用同一个 Flash 升级核心,因此"先擦后写、CRC 后提交 Boot 信息"的安全语义完全一致。
核心流程:从数据到新固件
以 UART/文件数据源为例,一次完整的双备份升级时序如下:
sequenceDiagram
participant S as 数据源(文件/UART/BLE)
participant A as App 调用方
participant D as dual_bank_update_deal()
participant T as 升级任务
participant F as Flash(升级区+Boot信息)
A->>D: START(file_crc, file_size, max_pkt_len)
D->>T: dual_bank_passive_update_init()
T-->>D: ok
D->>T: dual_bank_update_allow_check(size)
T-->>D: 空间足够
D-->>A: 0 (允许升级)
loop 直到文件末尾
S-->>A: 数据块 (≤ max_pkt_len)
A->>D: DATA(data, len)
D->>T: dual_bank_update_write()
T->>F: 写入升级区
T-->>D: 写完成
D-->>A: 0
end
A->>D: VERIFY
D->>T: dual_bank_update_verify()
T->>F: 读回全部数据
T->>T: CRC16 比对
T-->>A: dual_bank_verify_result_hdl(1)
A->>T: dual_bank_update_burn_boot_info()
T->>F: 烧写 Boot 信息
T-->>A: burn_boot_info_result_hdl(0)
A->>A: 系统复位
F-->>A: 从新 Bank 启动
关键点:整个过程中升级区数据对当前系统"不可见"(不参与启动),直到 Boot 信息烧写成功并复位,新固件才接管;若任一步失败,App 调用 dual_bank_update_exit() 清理资源,系统继续运行旧固件,升级可随时重试。
使用示例
dual_bank_update.c 中自带两个完整的参考实现,展示了"从预留区域读取升级文件并完成双备份升级"的标准写法。
示例一:标准五步流程(dual_bank_update_demo)
#define UPDATE_FILE_PATH "app/UPDATE"
#define UPDATE_TMP_BUFFER 0x1000
extern u32 sdfile_cpu_addr2flash_addr(u32 offset);
extern int norflash_origin_read(u8 *buf, u32 offset, u32 len);
void dual_bank_update_demo(void)
{
// 打开资源文件
void *update_file = resfile_open(UPDATE_FILE_PATH);
u8 *update_tmp_buffer = (u8 *) malloc(UPDATE_TMP_BUFFER);
memset(update_tmp_buffer, 0, UPDATE_TMP_BUFFER);
if (NULL == update_file) {
printf("%s is not exist\n", UPDATE_FILE_PATH);
}
// 调用升级初始化函数
dual_bank_start_info update_file_info = {0};
update_file_info.file_size = resfile_get_len(update_file);
u32 update_file_addr = sdfile_cpu_addr2flash_addr(resfile_get_addr(update_file));
u16 update_file_crc = 0;
// 文件crc校验
for (u32 offset = 0, data_len = 0; offset < update_file_info.file_size;) {
wdt_clear();
data_len = ((update_file_info.file_size - offset) > UPDATE_TMP_BUFFER) ? UPDATE_TMP_BUFFER : (update_file_info.file_size - offset);
norflash_origin_read(update_tmp_buffer, update_file_addr + offset, data_len);
update_file_crc = CRC16_with_initval(update_tmp_buffer, data_len, update_file_crc);
offset += data_len;
}
update_file_info.file_crc = update_file_crc;
update_file_info.max_pkt_len = UPDATE_TMP_BUFFER;
if (dual_bank_update_deal(DUAL_BANK_UPDATE_START, (u8 *) &update_file_info, sizeof(dual_bank_start_info))) {
goto __dual_bank_update_demo_end;
}
// 调用写函数
for (u32 offset = 0, data_len = 0; offset < update_file_info.file_size;) {
wdt_clear();
data_len = ((update_file_info.file_size - offset) > UPDATE_TMP_BUFFER) ? UPDATE_TMP_BUFFER : (update_file_info.file_size - offset);
norflash_origin_read(update_tmp_buffer, update_file_addr + offset, data_len);
if (dual_bank_update_deal(DUAL_BANK_UPDATE_DATA, update_tmp_buffer, data_len)) {
goto __dual_bank_update_demo_end;
}
offset += data_len;
}
// 调用校验函数
if (dual_bank_update_deal(DUAL_BANK_UPDATE_VERIFY, NULL, 0)) {
goto __dual_bank_update_demo_end;
}
__dual_bank_update_demo_end:
if (update_tmp_buffer) {
free(update_tmp_buffer);
}
if (update_file) {
resfile_close(update_file);
}
}
Source: dual_bank_update.c
代码要点:
- 升级文件存放在资源文件系统路径
"app/UPDATE",通过resfile_open/resfile_get_len/resfile_get_addr获取内容; - 使用
sdfile_cpu_addr2flash_addr()把 CPU 地址转换为 Flash 物理地址,再用norflash_origin_read()绕过文件系统直接读 Flash; - 第一遍循环:读文件并累计计算 CRC16(
CRC16_with_initval,带初值累加),得到file_crc; - 第二遍循环:以
UPDATE_TMP_BUFFER (0x1000)为分块粒度,把数据块逐块送入DUAL_BANK_UPDATE_DATA; - 每块数据之间调用
wdt_clear()喂狗——因为擦写 Flash 耗时长,防止看门狗复位; - 任何一步返回非 0 即
goto到清理标签,统一释放缓冲与文件句柄。
注意:示例中校验由升级任务内部完成(
dual_bank_update_verify读取 Flash 中已写入的数据再算 CRC),App 侧第一遍算 CRC 是为了在 START 阶段登记期望值file_crc。
示例二:按目标 Bank 大小驱动(dual_bank_update_demo_2)
extern u32 get_target_update_max_size(void);
extern void set_update_file_verify_crc_info(u16 data_crc);
extern void set_update_file_size_info(u32 file_size);
static u8 update_file_buffer[UPDATE_TMP_BUFFER] = {0};
static u32 record_data_len = 0;
static u32 update_file_data_get(u8 *buffer[])
{
u32 file_len = 0;
u32 data_len = 0;
*buffer = update_file_buffer;
void *update_file = resfile_open(UPDATE_FILE_PATH);
if (NULL == update_file) {
goto __update_file_data_get_end;
}
file_len = resfile_get_len(update_file);
if (record_data_len >= file_len) {
goto __update_file_data_get_end;
}
u32 update_file_addr = sdfile_cpu_addr2flash_addr(resfile_get_addr(update_file));
data_len = file_len > record_data_len ? UPDATE_TMP_BUFFER : (file_len - record_data_len);
// 每次从文件中读取UPDATE_TMP_BUFFER这么多的数据
norflash_origin_read(update_file_buffer, update_file_addr + record_data_len, data_len);
record_data_len += data_len;
__update_file_data_get_end:
if (update_file) {
resfile_close(update_file);
}
return data_len;
}
void dual_bank_update_demo_2(void)
{
u32 target_bank_addr = 0;
u32 target_bank_max_size = 0;
if (dual_bank_passive_update_init(0, 0, UPDATE_TMP_BUFFER, NULL)) {
dual_bank_update_exit(NULL);
return;
}
target_bank_max_size = get_target_update_max_size();
// 设置上面获取的文件长度
set_update_file_size_info(target_bank_max_size);
// 把升级区域全部擦除
if (dual_bank_update_allow_check(target_bank_max_size)) {
dual_bank_update_exit(NULL);
return;
}
record_data_len = 0;
// 写入数据
u16 update_file_crc = 0;
u32 update_file_len = 0;
while (update_file_len < target_bank_max_size) {
u8 *update_data = NULL;
u32 data_len = update_file_data_get(&update_data);
if (data_len) {
// 下载数据的过程中,需要统计文件长度
update_file_len += data_len;
// 只需要调用接口即可,地址不需要获取
if (dual_bank_update_deal(DUAL_BANK_UPDATE_DATA, update_data, data_len)) {
dual_bank_update_exit(NULL);
return;
}
// 使用CRC16_with_initval接口可进行crc的累计计算
update_file_crc = CRC16_with_initval(update_data, data_len, update_file_crc);
} else {
break;
}
}
// 设置上面计算完的crc
set_update_file_verify_crc_info(update_file_crc);
// 设置上面统计的文件长度
set_update_file_size_info(update_file_len);
if (dual_bank_update_verify(NULL, NULL, dual_bank_verify_result_hdl)) {
dual_bank_update_exit(NULL);
return;
}
}
Source: dual_bank_update.c
与示例一的差异(设计意图):
- 示例二直接调用底层 API 而不是
dual_bank_update_deal(),展示更精细的控制; - 通过
get_target_update_max_size()查询升级区容量,以目标 Bank 大小为驱动:set_update_file_size_info()先把整区擦除预留,写循环按 Bank 容量推进; - 边写边统计
update_file_len、边累计update_file_crc,结束后通过set_update_file_verify_crc_info()/set_update_file_size_info()把期望 CRC 与长度登记给校验器; - 数据获取被封装成
update_file_data_get()(返回本次读取长度,0 表示文件结束),代码结构更适合移植为"流式数据源"(如 UART/BLE 分包)。
配置选项
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
UPDATE_FILE_PATH | 字符串 | "app/UPDATE" | 预留区域中升级固件文件的资源路径 |
UPDATE_TMP_BUFFER | 宏 | 0x1000 (4096) | 临时缓冲大小,即每次 DATA 消息的最大数据块长度 |
max_pkt_len | u16 | 由调用方传入 | 每次编程的最大包长,决定单次写入粒度;应 ≤ 临时缓冲区大小 |
UPDATA_MAGIC | 宏 | 0x5A00 | 升级结果码/类型的起始魔数,防止与 CRC==0 混淆 |
DUAL_BANK_UPDATE_* 状态 | 枚举 | START=0 / DATA=1 / VERIFY=2 | 升级任务消息类型,顺序不可颠倒 |
| CRC 算法 | 回调 | CRC16-CCITT 标准 | dual_bank_update_verify 传入 NULL 时使用内部实现 |
API 参考
u32 get_dual_bank_passive_update_max_buf(void)
获取升级任务临时存储的最大缓冲区大小,调用方应据此决定单次写入的数据块上限。
u32 dual_bank_passive_update_init(u32 fw_crc, u32 fw_size, u16 max_pkt_len, void *priv)
初始化升级任务并登记新固件的 CRC 与文件大小。
- 参数:
fw_crc新固件 CRC 值;fw_size新固件总大小;max_pkt_len每次编程的最大包长;priv保留。 - 返回:0 表示成功;非 0 表示 CPU/任务资源不足,App 不应继续升级。
u32 dual_bank_passive_update_exit(void *priv)
退出升级任务、释放资源。任何失败路径都应调用,使系统回到升级前状态。
u32 dual_bank_update_allow_check(u32 fw_size)
检查升级区是否有足够空间。
- 注意:必须在
dual_bank_passive_update_init()之后调用。 - 返回:0 表示空间足够;非 0 表示空间不足。
u32 dual_bank_update_write(void *data, u16 len, int (*write_complete_cb)(void *priv))
把下载数据拷贝到临时缓冲并通知任务写入非易失存储。
- 参数:
data下载数据指针;len数据长度;write_complete_cb编程完成回调(返回 0 表示无错误,可发送下一块)。 - 返回:0 表示成功接收本块数据。
u32 dual_bank_update_verify(void (*crc_init_hdl)(void), u16(*crc_calc_hdl)(u16 init_crc, u8 *data, u32 len), int (*verify_result_hdl)(int crc_res))
对已写入升级区的全部数据进行 CRC 校验。
- 参数:
crc_init_hdl/crc_calc_hdl传NULL使用内部 CRC16-CCITT;verify_result_hdl校验完成通知回调,crc_res == 1表示通过,0表示失败。
u32 dual_bank_update_burn_boot_info(int (*burn_boot_info_result_hdl)(int err))
校验通过后烧写新固件的 Boot 信息(升级"提交点")。
- 参数:
burn_boot_info_result_hdl结果通知回调,err == 0表示烧写成功,其他值表示失败。
int flash_update_clr_boot_info(u8 type)
擦除指定 Bank 的 Boot 信息,用于回退场景。
- 参数:
type为CLEAR_APP_RUNNING_BANK(0,清除运行区并复位,系统将尝试从另一 Bank 启动)或CLEAR_APP_UPDATE_BANK(1)。 - 警告:接口注释明确提示"应非常谨慎地调用"——错误使用可能导致系统无可用固件启动。
u32 dual_bank_update_read_data(u32 offset, u8 *read_buf, u32 read_len)
读取升级区 Flash 数据(offset 相对升级区起点),供自定义 CRC 校验使用。
- 返回:实际读取长度。
故障模式、边界与并发
失败路径与回退策略
源码中的回调实现明确展示了各失败点的处理方式:
int burn_boot_info_result_hdl(int err)
{
if (err == 0) {
//boot_info write ok, and rsp app update success (need user implement api to response app)
/* sys_timeout_add(NULL, dual_bank_cpu_reset, 2000); */
dual_bank_cpu_reset(NULL);
} else {
//boot_info write failed, rsp app update failed (need user implement api to response app)
dual_bank_update_exit(NULL);
}
return 0;
}
int dual_bank_verify_result_hdl(int res)
{
if (res) {
//flash verify success, write boot info
dual_bank_update_burn_boot_info(burn_boot_info_result_hdl);
} else {
//rsp app flash verify failed
dual_bank_update_exit(NULL);
}
return 0;
}
Source: dual_bank_update.c
| 故障场景 | 检测点 | 处理方式 | 系统后果 |
|---|---|---|---|
| 升级任务资源不足 | dual_bank_passive_update_init 非 0 | dual_bank_update_exit,通知 App 无法升级 | 继续运行旧固件 |
| Flash 空间不足 | dual_bank_update_allow_check 非 0 | dual_bank_update_exit,通知 App 文件过大 | 继续运行旧固件 |
| 数据写入失败 | dual_bank_update_write 非 0 | 逐块上报,App 终止循环 | 升级区为半写状态,下次升级重新擦写 |
| CRC 校验失败 | dual_bank_verify_result_hdl(0) | dual_bank_update_exit,通知 App | 旧固件继续运行,Boot 信息未动 |
| Boot 信息烧写失败 | burn_boot_info_result_hdl(err≠0) | dual_bank_update_exit | 启动选择不变,仍从旧 Bank 启动 |
| 升级中途掉电 | 无(硬件级) | 无 | Boot 信息未提交,下次启动仍走旧 Bank;升级区可被下次升级覆盖 |
| 双 Bank 均不可用 | — | flash_update_clr_boot_info 滥用时可能发生 | 需配合 Bootloader 兜底,慎用该接口 |
核心安全语义:Boot 信息是唯一"提交点"。CRC 校验通过之前绝不改写它,因此任何失败都不会破坏当前可启动的固件;升级区即使写坏,也只需重新执行一次升级。这就是双备份相对单备份升级(升级中掉电即变砖)的本质优势。
并发与时序约束
- 升级过程要求串行执行:
START → DATA* → VERIFY必须按序调用,dual_bank_update_deal()是同步分发,不提供并发保护,多任务并发调用会破坏升级区数据一致性。 dual_bank_update_write()内部采用"下载缓冲 → Flash 编程"的交接模型:write_complete_cb返回 0 表示本块已落盘,调用方必须等该回调后才能发送下一块,否则可能覆盖仍在编程的缓冲。- 长循环中(Demo 的写数据循环)每块都调用
wdt_clear()喂狗——Flash 擦写时间远大于看门狗周期,若不喂狗升级会在半途被复位。 burn_boot_info_result_hdl中提供了sys_timeout_add(..., dual_bank_cpu_reset, 2000)的延时复位示例(当前代码直接立即复位)。生产环境中建议采用延时复位:先通过 UART/蓝牙通知 App"升级成功",给上位机留出收尾时间,再复位进入新固件。
性能与运维考虑
- 单次写入粒度:
max_pkt_len与UPDATE_TMP_BUFFER (0x1000)决定每次 Flash 编程的块大小。块越大,擦写次数越少、整体耗时越短,但占用 RAM 越多;块太小则会放大擦写开销。示例默认 4KB,是 RAM 占用与升级速度的折中。 - 升级耗时主要由 Flash 擦写主导:示例二先整区擦除(
dual_bank_update_allow_check触发),再顺序写满,最后读回校验(一次完整读 Flash),整体时间 = 擦除 + 写入 ×2(写一次、校验读一次)。 - 看门狗:任何长时间循环(CRC 预计算、写数据、校验)都必须周期性
wdt_clear(),否则升级会被 WDT 复位打断。 - 升级中断可重入:由于 Boot 信息未提交前系统状态不变,升级失败/中断后可直接重新发起
START,升级任务会重新初始化并覆盖升级区。 - UART 通道:
dual_bank_update_uart.c提供了 UART 升级通道实现(对应UART_UPDATA类型),常用于产线或 Debug 场景,其行为遵循本框架的通道接口约定。
扩展点
- 自定义 CRC 算法:
dual_bank_update_verify(crc_init_hdl, crc_calc_hdl, verify_result_hdl)允许注入crc_init_hdl/crc_calc_hdl替换内部 CRC16-CCITT,适配与上位机约定的非标准校验算法。 - 新增升级数据源通道:实现
update_op_api_t(ch_init / f_open / f_read / f_seek / f_stop / notify_update_content_size / ch_exit),并在UPDATA_TYPE枚举中注册新类型(必须加在USER_NORFLASH_UFW_UPDATA之前,勿改动已有枚举值),即可接入主动升级框架。 - 自定义响应协议:源码中多处标注 "need user implement api to response app"(如
dual_bank_write_complete_cb、dual_bank_verify_result_hdl中的回复逻辑)——升级进度、成功/失败通知的对外协议由业务层实现,BSP 层只提供回调挂点。 - 延时复位策略:将
dual_bank_cpu_reset(NULL)替换为sys_timeout_add(NULL, dual_bank_cpu_reset, 2000),可先通知 App 升级成功再复位。 - 回退机制:通过
flash_update_clr_boot_info(CLEAR_APP_RUNNING_BANK)清除当前运行 Bank 的 Boot 信息并复位,系统自动切换到另一 Bank 启动,可作为"新固件自检失败后的主动回退"手段(需业务层在启动早期判断新固件健康状态)。
Tests 与验证建议
仓库中双备份升级的验证主要依赖源码内建的两个 Demo(dual_bank_update_demo / dual_bank_update_demo_2)而非独立测试工程。建议的验证路径:
- 功能验证:将新固件放入预留区域
"app/UPDATE",调用dual_bank_update_demo(),观察是否依次完成 START(空间检查)→ DATA(逐块写入)→ VERIFY(CRC 通过)→ Boot 信息烧写 → 复位后从新 Bank 启动。 - 失败注入:故意传错误的
file_crc触发校验失败,确认系统停留在旧固件且 Boot 信息未被改写;故意传超大的file_size触发allow_check失败。 - 掉电测试:在 DATA 阶段随机断电,确认重新上电后仍从旧 Bank 启动,可再次发起升级。
- 回退测试:在新固件启动早期调用
flash_update_clr_boot_info(CLEAR_APP_RUNNING_BANK),确认系统回退到旧 Bank。
Related Links
- 被动升级 API 头文件 dual_bank_updata_api.h
- 升级任务实现与示例 dual_bank_update.c
- 主动升级框架定义 dual_bank_update_loop.h
- UART 升级通道实现 dual_bank_update_uart.c
- UART 升级通道头文件 dual_bank_update_uart.h
与双备份升级配套的 Bootloader 启动选择逻辑、量产工具协议属于独立主题,不在本文档范围内。