双备份升级机制与状态机
本文档介绍 AC63 系列 MCU(fw-AC63_GP_MCU)的双备份(Dual Bank / A-B Bank)固件升级机制及其配套的升级状态机,覆盖消息驱动的升级调度入口 dual_bank_update_deal、被动升级流程(初始化 → 写入 → 校验 → 烧录 boot 信息 → 复位切换)、升级通道抽象层(update_op_api_t / active_update)以及基于 UART 的帧协议通道实现。
Purpose and Scope
本页聚焦"双备份升级"这一端到端能力:升级触发入口、被动升级状态机、结果回调链(校验 → 烧录 boot 信息 → CPU 复位)、升级通道抽象,以及 UART 通道的帧协议与接收状态机。内容基于 sdk/bsp/dual_bank_update.c、sdk/bsp/dual_bank_update_uart.c、sdk/bsp/include/dual_bank_update_loop.h、sdk/bsp/include/dual_bank_update_uart.h 等实际源码。
以下主题不属于本页范围,请参见对应页面:
- U 盘 / SD 卡 / BLE 等具体传输介质:本页仅说明它们共用的通道抽象(
UPDATA_TYPE枚举与update_op_api_t),各介质的具体实现请参考各自的升级通道源码。 - 量产烧录与工具链:
doc/双备份升级/serialupdatedemo-master_QT源码.zip中主机端(PC/上位机)实现不在本页展开。 - 底层 Flash 驱动与分区表:双备份分区的实际地址与 boot 标志布局由预编译库
sdk/bsp/include/liba/*/update.a及链接脚本决定,源码仓库中不含其实现细节。
概述(Overview)
双备份升级是嵌入式固件常见的 A/B 分区 OTA 方案:Flash 中同时存在两个可启动的固件副本(当前运行区与目标升级区)。升级时只向"非运行区"(target bank)写入新固件,全部数据写入并完成 CRC 校验后,再烧录 boot 信息切换启动标志,最后 cpu_reset() 复位,从新固件启动。若校验失败或 boot 信息烧录失败,旧固件所在分区完好无损,设备可继续从旧区启动,天然具备失败回滚能力。
该机制的核心设计意图:
- 断电安全:升级过程始终不影响当前运行区,任何时刻掉电都不会破坏可启动固件;
- 状态机驱动:升级被建模为
START → DATA → VERIFY三个消息,配合结果回调(写完成、校验完成、boot 烧录完成)推进状态,避免在中断/主循环中长时间阻塞; - 通道解耦:通过
update_op_api_t函数指针表抽象数据来源(UART/SD/BLE/USB 等),升级核心逻辑与传输介质无关; - 上位机协商:UART 通道采用带同步头(
0xAA 0x55)与 CRC16 的帧协议,支持重试(RETRY_TIME = 4),并对波特率、包长进行协商。
关键源文件:
| 文件 | 作用 |
|---|---|
sdk/bsp/dual_bank_update.c | 被动升级消息调度入口、回调处理、两个升级示例(resfile 读取 + 流式读取) |
sdk/bsp/dual_bank_update_uart.c | UART 通道:帧协议封装、接收状态机、命令交互、波特率协商 |
sdk/bsp/include/dual_bank_update_loop.h | 升级结果/类型/状态枚举、通道操作抽象 update_op_api_t、主动升级入口 active_update |
sdk/bsp/include/dual_bank_update_uart.h | UART 帧结构、消息定义、对外接口声明 |
sdk/bsp/include/liba/*/update.a | 预编译被动升级库:dual_bank_passive_update_init/write/verify/allow_check/burn_boot_info |
架构(Architecture)
flowchart TD
subgraph sg_Trigger["升级触发源"]
HOST["上位机 / 主机工具"]
FILE["预留区升级文件<br/>app/UPDATE"]
end
subgraph sg_Sched["升级调度层"]
DEAL["dual_bank_update_deal<br/>START / DATA / VERIFY 消息"]
ACTIVE["active_update<br/>主动升级(update_mode_info_t)"]
CBK["结果回调链<br/>write_complete / verify / burn_boot_info"]
end
subgraph sg_Lib["被动升级库 (update.a)"]
INIT["dual_bank_passive_update_init"]
WRITE["dual_bank_update_write"]
VERIFY["dual_bank_update_verify"]
CHECK["dual_bank_update_allow_check"]
BOOT["dual_bank_update_burn_boot_info"]
end
subgraph sg_Chan["升级通道抽象"]
OPAPI["update_op_api_t<br/>ch_init/f_open/f_read/f_seek/f_stop"]
UART["UART 通道<br/>dual_bank_update_uart.c"]
end
subgraph sg_Flash["Flash 双备份布局"]
BANK_A["Bank A(运行区)"]
BANK_B["Bank B(目标区)"]
BOOTINFO["boot_info 启动标志"]
end
HOST --> UART
FILE --> DEAL
UART --> DEAL
DEAL --> INIT
DEAL --> WRITE
DEAL --> VERIFY
ACTIVE --> OPAPI
OPAPI --> UART
INIT --> CHECK
CHECK --> BANK_B
WRITE --> BANK_B
VERIFY --> BOOT
BOOT --> BOOTINFO
BOOTINFO --> RESET["cpu_reset() 复位切换"]
RESET --> BANK_A
RESET --> BANK_B
架构说明:升级数据既可以来自主机工具(经 UART 通道,由接收状态机解析帧后以 DUAL_BANK_UPDATE_DATA 消息喂给调度层),也可以来自 MCU 自身预留区文件(dual_bank_update_demo / dual_bank_update_demo_2)。调度层把数据交给预编译库完成初始化、写入、校验与 boot 信息烧录;校验成功后 cpu_reset() 复位,boot_info 决定下次从 Bank A 还是 Bank B 启动。active_update 则走另一条路径:通过 update_mode_info_t 注册通道操作表,由通用升级框架驱动。
升级状态机与消息调度
消息类型与 dual_bank_update_deal
被动升级的顶层状态机定义在 dual_bank_update.c,只有三个状态:DUAL_BANK_UPDATE_START、DUAL_BANK_UPDATE_DATA、DUAL_BANK_UPDATE_VERIFY。升级请求被统一封装为 dual_bank_start_info 结构(文件长度、文件 CRC、最大包长),由 dual_bank_update_deal() 分发:
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,
};
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,先调用库函数dual_bank_passive_update_init()完成升级环境初始化(分配 RAM 缓冲、准备目标分区等),若资源不足(CPU/内存)则直接dual_bank_update_exit。初始化成功后调用dual_bank_update_allow_check(file_size)做目标分区容量检查——目标 bank 剩余空间必须大于升级文件大小,否则同样走退出路径。只有两步都通过,才会向上位机/调用方应答"可以升级"。DUAL_BANK_UPDATE_DATA:直接透传给库函数dual_bank_update_write(data, len, dual_bank_write_complete_cb),写入目标 bank。dual_bank_write_complete_cb是"当前缓冲区写完"的通知,源码中给出空实现(返回 0),注释明确要求用户实现应答上位机"可发下一包"的接口。调用方无需关心目标地址——库内部维护写入偏移。DUAL_BANK_UPDATE_VERIFY:调用dual_bank_update_verify(NULL, NULL, dual_bank_verify_result_hdl)对已写入数据做 CRC 校验。两个NULL参数分别对应自定义crc_init_hdl/crc_calc_hdl,注释说明:若应用侧累计 CRC 不是 CRC16-CCITT 标准算法,则需要提供这两个回调。校验结果通过dual_bank_verify_result_hdl返回。
结果回调链:校验 → 烧录 boot 信息 → 复位
升级的关键决策都在回调链中完成,这是状态机从"数据面"切换到"决策面"的地方:
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;
}
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;
}
void dual_bank_cpu_reset(void *priv)
{
cpu_reset();
}
Source: dual_bank_update.c
流程语义:dual_bank_verify_result_hdl(res) —— res 为真表示 Flash 校验通过,随即调用 dual_bank_update_burn_boot_info() 烧录 boot 信息(即把启动标志切换到新 bank);res 为假则校验失败,直接退出升级,旧 bank 不受影响。burn_boot_info_result_hdl(err) —— err == 0 表示 boot 信息写成功,立即 cpu_reset() 复位进入新固件;写失败则退出(保留旧启动标志,实现回滚)。源码中预留了 sys_timeout_add(NULL, dual_bank_cpu_reset, 2000) 的 2 秒延时复位写法(被注释),说明该决策点支持"先应答上位机、再延时复位"的工程实践。dual_bank_update_exit() 是统一的退出钩子,当前为空实现,可在此做资源清理与状态上报。
通道抽象:update_op_api_t 与 active_update
为了把"数据从哪里来"与"升级怎么执行"解耦,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;
typedef enum _UPDATE_STATE_T {
UPDATE_TASK_INIT,
UPDATE_CH_INIT,
UPDATE_CH_SUCESS_REPORT,
UPDATE_CH_EXIT,
} UPDATE_STATE_T;
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
要点:
UPDATA_RESULT以UPDATA_MAGIC (0x5A00)作为起始值,注释明确"防止 CRC == 0 的情况"——用非零魔法数区分"未开始"与"CRC 恰好为 0"的合法结果,这是嵌入式协议中常见的哨兵值技巧。UPDATA_TYPE枚举从0x5A00起连续编号(USB、SD0/SD1、PC、UART、BT、BLE、SPP、DUAL_BANK、BLE_TEST、NORFLASH),注释强调已有编号不可调整,新通道必须加在USER_NORFLASH_UFW_UPDATA之前,避免与既有存储格式/上位机工具冲突。UPDATE_STATE_T描述通道生命周期:任务初始化 → 通道初始化 → 成功上报 → 通道退出。update_op_api_t是文件系统式的通道接口(open/read/seek/stop/exit),active_update()接收一个update_mode_info_t(类型 + 状态回调 + 操作表 + 使能标志),供通用升级框架在主动升级场景(MCU 主动从某个介质拉取固件)复用同一套数据通路。UART 通道即按此接口实现。
UART 通道:帧协议与接收状态机
帧格式与封装
dual_bank_update_uart.h 定义了带同步头与 CRC 的定长帧:
#define PROTOCAL_SIZE 528
#define SYNC_SIZE 6
#define SYNC_MARK0 0xAA
#define SYNC_MARK1 0x55
typedef union {
u8 raw_data[PROTOCAL_SIZE + SYNC_SIZE];
struct {
u8 mark0;
u8 mark1;
u16 length;
u8 data[PROTOCAL_SIZE + 2]; // 最后CRC16
} data;
} protocal_frame_t;
struct file_info {
u8 cmd;
u32 addr;
u32 len;
} __attribute__((packed));
Source: dual_bank_update_uart.h
帧布局:0xAA | 0x55 | length(u16) | payload(PROTOCAL_SIZE+2 含尾部 CRC16),总长 534 字节。union 设计允许直接按 raw_data 字节流收发、按 data 字段解析,避免拷贝。file_info 用 __attribute__((packed)) 保证 1 字节对齐——这是主机/设备跨平台传输二进制结构的必要条件。
发送封装与接收解析分别见 uart_send_packet 与 uart_loop_rx_handler:
static bool uart_send_packet(u8 *buf, u16 length)
{
bool ret = TRUE;
u16 crc;
u8 *buffer;
buffer = (u8 *)&protocal_frame;
protocal_frame.data.mark0 = SYNC_MARK0;
protocal_frame.data.mark1 = SYNC_MARK1;
protocal_frame.data.length = length;
memcpy((char *)&buffer[4], buf, length);
crc = CRC16(buffer, length + SYNC_SIZE - 2);
memcpy(buffer + 4 + length, &crc, 2);
log_debug("tx(%d)", length + SYNC_SIZE);
log_debug_hexdump((u8 *)&protocal_frame, length + SYNC_SIZE);
uart_tx_buf(dual_uart_config.id, (u8 *)&protocal_frame, length + SYNC_SIZE);
return ret;
}
Source: dual_bank_update_uart.c
CRC16 覆盖"同步头 + 长度 + 载荷"(即整帧去掉尾部 2 字节 CRC 的部分),计算后追加在帧尾,与接收端校验范围一致。发送走 uart_tx_buf(DMA 缓冲发送),并打 log_debug/log_debug_hexdump 便于联调抓包。
接收状态机:逐字节同步与 CRC 校验
static void uart_loop_rx_handler(u8 *buf, u16 len)
{
u16 crc, crc0, i, ch;
for (i = 0; i < len; i++) {
ch = buf[i];
__recheck:
if (rx_cnt == 0) {
if (ch == SYNC_MARK0) {
protocal_frame.raw_data[rx_cnt++] = ch;
}
} else if (rx_cnt == 1) {
protocal_frame.raw_data[rx_cnt++] = ch;
if (ch != SYNC_MARK1) {
rx_cnt = 0;
goto __recheck;
}
} else if (rx_cnt < 4) {
protocal_frame.raw_data[rx_cnt++] = ch;
} else {
protocal_frame.raw_data[rx_cnt++] = ch;
if (rx_cnt == (protocal_frame.data.length + SYNC_SIZE)) {
log_debug("rx(%d)", rx_cnt);
log_debug_hexdump((u8 *)&protocal_frame, rx_cnt);
rx_cnt = 0;
crc = CRC16(protocal_frame.raw_data, protocal_frame.data.length + SYNC_SIZE - 2);
memcpy(&crc0, &protocal_frame.raw_data[protocal_frame.data.length + SYNC_SIZE - 2], 2);
if (CMD_UART_UPDATE_FLAG_INIT_ERR != dual_bank_update_flag) {
if (crc0 == crc) {
dual_bank_update_flag = CMD_UART_UPDATE_FLAG_RECV_DATA;
} else {
dual_bank_update_flag = CMD_UART_UPDATE_FLAG_RETRY;
}
}
if (uart_update_resume_hdl && (CMD_UART_UPDATE_READ == protocal_frame.data.data[0])) {
uart_update_resume_hdl(NULL);
}
}
}
}
}
Source: dual_bank_update_uart.c
接收解析是一个基于 rx_cnt 的逐字节状态机,分四阶段:
rx_cnt == 0:等待同步字节0xAA,非0xAA直接丢弃;rx_cnt == 1:校验第二字节0x55,不匹配则清零并goto __recheck用当前字节重新开始——允许在同一缓冲区后续字节中立即重新同步,增强抗干扰性;rx_cnt < 4:收取长度字段(2 字节);- 之后按
protocal_frame.data.length + SYNC_SIZE收满整帧,随后计算 CRC16 并与帧尾 2 字节比对;结果写入dual_bank_update_flag:CMD_UART_UPDATE_FLAG_RECV_DATA(成功)或CMD_UART_UPDATE_FLAG_RETRY(需重传)。CMD_UART_UPDATE_FLAG_INIT_ERR (0xff)时跳过 CRC 处理,表示通道初始化失败后不再响应数据。
收到完整帧且首字节为 CMD_UART_UPDATE_READ 时调用 uart_update_resume_hdl(NULL) 唤醒升级任务——这是"主机请求数据/应答读请求"时恢复下载流程的关键同步点。dual_bank_update_flag 声明为 volatile,说明它在中断上下文(UART 接收回调)与任务上下文之间共享,靠标志位而非锁传递结果。
命令集与数据读取
dual_bank_update_uart.c 定义了通道命令与状态:
// 命令
#define CMD_UPDATE_START 0x01
#define CMD_UPDATE_READ 0x02
#define CMD_UPDATE_END 0x03
#define CMD_SEND_UPDATE_LEN 0x04
#define CMD_KEEP_ALIVE 0x05
#define RETRY_TIME 4
enum {
CMD_UART_UPDATE_FLAG_NONE,
CMD_UART_UPDATE_FLAG_RECV_DATA,
CMD_UART_UPDATE_FLAG_RETRY,
CMD_UART_UPDATE_FLAG_INIT_ERR = 0xff,
};
Source: dual_bank_update_uart.c
- 命令:
START(0x01)、READ(0x02)、END(0x03)、SEND_UPDATE_LEN(0x04)、KEEP_ALIVE(0x05);另有CMD_UART_UPDATE_READY用于就绪通知。 RETRY_TIME = 4:uart_dev_receive_data()循环发送CMD_UPDATE_READ(携带file_info{cmd, addr, len})请求数据,失败最多重试 4 次,重试时通过putchar('r')输出调试字符。uart_update_state_cbk()在UPDATE_CH_EXIT状态触发时关闭 UART(恢复dual_uart_config.baudrate默认波特率)、复位文件偏移,并且只有当ret_code->stu == 0 && ret_code->err_code == 0时才cpu_reset()——把"升级是否成功"的判定收敛到通道退出点,避免半成功状态复位。
UART 升级整体流程
sequenceDiagram
participant H as 主机上位机
participant R as UART接收状态机<br/>uart_loop_rx_handler
participant D as 调度层<br/>dual_bank_update_deal
participant L as 升级库 update.a
participant F as Flash 双备份区
H->>R: 帧(CMD_UPDATE_START + file_info)
R->>D: DUAL_BANK_UPDATE_START
D->>L: dual_bank_passive_update_init(file_crc,file_size,max_pkt)
L-->>D: init 结果
D->>L: dual_bank_update_allow_check(file_size)
L-->>D: 容量检查结果(ok/不足)
D-->>R: 应答(可升级/失败退出)
loop 按包下载
D->>R: uart_dev_receive_data: 发 CMD_UPDATE_READ
H-->>R: 数据帧(0xAA 0x55 + len + data + CRC16)
R->>R: 逐字节同步 + CRC 校验 → RECV_DATA/RETRY
R->>D: DUAL_BANK_UPDATE_DATA
D->>L: dual_bank_update_write(data,len,cb)
L->>F: 写入目标 Bank
L-->>D: write_complete_cb(可发下一包)
end
D->>L: dual_bank_update_verify(NULL,NULL,verify_hdl)
L-->>D: verify_result_hdl(res)
alt 校验成功
D->>L: dual_bank_update_burn_boot_info
L->>F: 烧录 boot_info(切换启动标志)
D->>D: burn_boot_info_result_hdl(0) → cpu_reset()
else 校验失败
D->>D: dual_bank_update_exit(保留旧 Bank)
end
使用示例(Usage Examples)
源码中提供两个完整示例,分别演示"从预留区读升级文件"与"流式读取(接口回调式)"两种调用方式,是接入双备份升级的最佳模板。
示例一:从预留区读取升级文件并升级(dual_bank_update_demo)
该示例以资源文件 app/UPDATE 为升级源,先计算整文件 CRC16,再依次走 START → DATA → VERIFY 三步:
// =============================================从预留区域获取升级文件例子===================================================== //
#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
关键点:
resfile_open/resfile_get_len/resfile_get_addr获得升级文件在资源区的 CPU 地址,经sdfile_cpu_addr2flash_addr换算为 Flash 地址,再用norflash_origin_read绕过文件系统直接按 Flash 地址读——保证读取的是物理固件字节;- CRC 采用
CRC16_with_initval(..., update_file_crc)的累计(流式)计算方式,update_file_crc作为初值在循环中不断累进,最终得到整文件 CRC; - 写数据循环中调用
wdt_clear()喂狗,避免大文件升级期间看门狗超时复位——这是长时间升级任务的必要保护; - 每步返回值非 0 即
goto统一清理出口,保证 malloc 的临时缓冲与文件句柄必定释放。
示例二:流式读取 + 库内部维护长度/CRC(dual_bank_update_demo_2)
该示例展示"不需要调用方自行维护文件偏移与 CRC"的写法,适合数据源是逐段产生(如网络/流媒体)的场景:
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
关键点:
dual_bank_passive_update_init(0, 0, UPDATE_TMP_BUFFER, NULL)中file_crc/file_size先传 0,随后通过set_update_file_size_info()/set_update_file_verify_crc_info()在写入完成后再把真实长度与 CRC 告知库——适合"先不知道文件有多大"的流式场景;get_target_update_max_size()取得目标 bank 容量,先按最大值dual_bank_update_allow_check()一次性擦除目标区;- 数据源函数
update_file_data_get()每次返回一段缓冲(复用静态update_file_buffer),调用方无需关心目标 Flash 地址,只负责把数据段交给DUAL_BANK_UPDATE_DATA; - 注释"下载数据的过程中,需要统计文件长度"明确:库内部按
set_update_file_size_info的最终值校验写入完整性,因此调用方必须如实统计。
UART 通道初始化入口
extern const struct uart_platform_data dual_uart_config;
void update_download_opt(void);
void dual_bank_update_init(void);
Source: dual_bank_update_uart.h
dual_uart_config 提供 UART 外设平台参数(含默认波特率,dual_bank_update_uart.c 中 update_baudrate = 9600),dual_bank_update_init() 负责注册接收回调与初始化通道,update_download_opt() 为下载任务入口。
配置选项(Configuration Options)
双备份升级模块的配置以宏、常量与平台数据结构形式存在于源码中(无运行时配置文件):
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
UPDATE_FILE_PATH | 宏 | "app/UPDATE" | 预留区升级文件在资源文件系统中的路径(dual_bank_update.c) |
UPDATE_TMP_BUFFER | 宏 | 0x1000 (4096) | 升级读写临时缓冲大小(字节),同时作为示例中的分块与 max_pkt_len |
DUAL_BANK_UPDATE_BY_UFW | 宏 | 0 | 双备份模块使能宏(dual_bank_update_uart.h) |
PROTOCAL_SIZE | 宏 | 528 | UART 帧载荷区大小(字节) |
SYNC_SIZE | 宏 | 6 | 帧头开销:2 同步字节 + 2 长度字节 + 2 CRC 字节 |
SYNC_MARK0 / SYNC_MARK1 | 宏 | 0xAA / 0x55 | 帧同步头,接收状态机据此对齐帧边界 |
RETRY_TIME | 宏 | 4 | UART 读数据重试次数(dual_bank_update_uart.c) |
update_baudrate | static u32 | 9600 | UART 升级波特率初始值,通道退出时恢复为 dual_uart_config.baudrate |
dual_uart_config | uart_platform_data | 外部定义 | UART 外设参数(ID、波特率、引脚等),由具体板级配置提供 |
UPDATA_MAGIC | 宏 | 0x5A00 | 升级结果/类型的起始哨兵值,防止与 CRC==0 混淆(dual_bank_update_loop.h) |
UART 帧协议字段(protocal_frame_t,总长 PROTOCAL_SIZE + SYNC_SIZE = 534 字节):
| 字段 | 偏移 | 长度 | 说明 |
|---|---|---|---|
mark0 | 0 | 1 | 同步头 0xAA |
mark1 | 1 | 1 | 同步头 0x55 |
length | 2 | 2 | 载荷长度(含载荷首字节命令字与尾部 CRC16) |
data[0..len-3] | 4 | 可变 | 命令字 + 参数(如 file_info{cmd,addr,len}) |
data[len-2..len-1] | 帧尾 | 2 | CRC16(覆盖帧头+长度+载荷,即整帧除末尾 2 字节) |
API 参考(API Reference)
u32 dual_bank_update_deal(u8 msg_type, u8 *data, u32 len)
被动升级的消息调度入口,把上层收到的升级请求翻译为库调用。
参数:
msg_type(u8):DUAL_BANK_UPDATE_START/DUAL_BANK_UPDATE_DATA/DUAL_BANK_UPDATE_VERIFYdata(u8*):START 时为dual_bank_start_info*;DATA 时为待写入数据缓冲;VERIFY 时置 NULLlen(u32):data 长度
返回: u32,0 表示该步骤正常受理/写入成功;非 0 表示失败(调用方应立即终止升级)。
实现要点: START 阶段内部依次调用 dual_bank_passive_update_init() 与 dual_bank_update_allow_check(),任一失败即触发 dual_bank_update_exit();DATA 阶段透传 dual_bank_update_write();VERIFY 阶段调用 dual_bank_update_verify() 并注册结果回调。
void dual_bank_update_demo(void) / void dual_bank_update_demo_2(void)
两个参考实现:前者以 app/UPDATE 预留区文件为源并自行维护 CRC;后者演示流式数据源 + set_update_file_size_info/set_update_file_verify_crc_info 后置上报长度与 CRC 的用法。返回 void,失败路径通过 dual_bank_update_exit() 统一处理。
int active_update(update_mode_info_t *info)
主动升级入口。info 携带升级类型(UPDATA_TYPE)、状态回调 state_cbk、通道操作表 p_op_api 与使能位 en。返回 int 表示启动结果。通道实现须按 update_op_api_t 提供 ch_init(注册 resume/sleep 钩子)、f_open、f_read、f_seek、f_stop、notify_update_content_size、ch_exit。
预编译库回调/接口(update.a 提供,本仓库无实现源码)
| 接口 | 用途 |
|---|---|
dual_bank_passive_update_init(file_crc, file_size, max_pkt_len, priv) | 初始化升级环境(分配缓冲、准备目标 bank) |
dual_bank_update_write(data, len, write_complete_cb) | 向目标 bank 写入一段数据,缓冲写完后回调 write_complete_cb |
dual_bank_update_verify(crc_init_hdl, crc_calc_hdl, verify_result_hdl) | 校验已写入数据,通过 verify_result_hdl(res) 返回结果 |
dual_bank_update_allow_check(file_size) | 目标分区容量/擦除检查 |
dual_bank_update_burn_boot_info(result_hdl) | 烧录 boot 信息(切换启动标志),通过 result_hdl(err) 返回 |
get_target_update_max_size() | 返回目标 bank 最大可用容量 |
set_update_file_size_info(file_size) / set_update_file_verify_crc_info(crc) | 后置上报升级文件长度与校验 CRC |
用户需实现的回调(源码注释标注 "need user implement api")
| 回调 | 触发时机 | 要求 |
|---|---|---|
dual_bank_write_complete_cb(priv) | 每包数据写入完成 | 应答上位机可发送下一包 |
burn_boot_info_result_hdl(err) 成功分支 | boot 信息写成功 | 应答上位机升级成功(可延时后 cpu_reset) |
| 校验失败 / 初始化失败 / 容量不足各分支 | 对应错误发生 | 应答上位机失败原因 |
失败模式、边界情况与并发(Failure Modes & Edge Cases & Concurrency)
失败模式与应对
| 失败场景 | 检测点 | 处理方式 |
|---|---|---|
| 升级环境初始化失败(CPU/内存不足) | dual_bank_passive_update_init 返回非 0 | dual_bank_update_exit,旧 bank 不受影响 |
| 目标分区空间不足 | dual_bank_update_allow_check 返回非 0 | dual_bank_update_exit,拒绝升级 |
| Flash 写入校验失败 | dual_bank_verify_result_hdl(res==0) | dual_bank_update_exit,保留旧启动标志实现回滚 |
| boot 信息烧录失败 | burn_boot_info_result_hdl(err!=0) | dual_bank_update_exit,仍从旧 bank 启动 |
| UART 帧 CRC 错误 | crc0 != crc | 置 CMD_UART_UPDATE_FLAG_RETRY,读取侧重试最多 RETRY_TIME=4 次 |
| UART 同步头丢失/错位 | rx_cnt 状态机 | 丢弃非法字节,__recheck 立即重新同步 |
| 通道初始化失败 | CMD_UART_UPDATE_FLAG_INIT_ERR(0xff) | 接收状态机不再处理数据帧(跳过 CRC 判定分支) |
| 升级中途断电 | — | 目标 bank 数据不完整,boot 标志未切换,重启仍从旧 bank 启动(双备份的核心价值) |
边界情况
- CRC 恰好为 0:
UPDATA_MAGIC(0x5A00)专门用于区分"升级未开始/无结果"与"CRC 计算值为 0"的合法情形,升级结果枚举全部以0x5A00起算。 - 文件长度未知的流式升级:示例二先以 0 初始化,写入完成后用
set_update_file_size_info/set_update_file_verify_crc_info补齐长度与 CRC;update_file_data_get返回 0 表示数据源耗尽,循环退出后必须如实上报累计长度,否则库校验会失败。 - 最大包长协商:
dual_bank_start_info.max_pkt_len由调用方上报(示例中为UPDATE_TMP_BUFFER=0x1000),库按此规划缓冲;UART 帧载荷上限为PROTOCAL_SIZE=528,大包需在通道层分帧。 - 非标准 CRC 算法:若累计校验不是 CRC16-CCITT,需向
dual_bank_update_verify传入自定义crc_init_hdl/crc_calc_hdl(当前传 NULL 使用默认算法)。
并发与同步
dual_bank_update_flag、rx_cnt、uart_file_offset均声明为volatile,由 UART 接收中断/回调与升级任务共享;接收结果通过标志位(RECV_DATA/RETRY)而非锁传递,配合uart_update_resume_hdl唤醒任务实现中断↔任务同步。protocal_frame为全局静态缓冲(aligned(4)),同一时刻只服务一个方向的帧组装/解析;若需并发收发需注意缓冲互斥。wdt_clear()在长循环中周期性喂狗,防止大文件升级/擦除期间看门狗误复位。
性能与运维(Performance & Operational Notes)
- 分块写入:升级数据按
UPDATE_TMP_BUFFER(4KB)分块,库在每块写完后回调dual_bank_write_complete_cb,天然形成"写一块→应答→收下一块"的流水线,避免一次性缓冲过大占用 RAM。 - UART 吞吐:默认波特率 9600,帧有效载荷约 528 字节,适合低速可靠传输;
dual_uart_config可在板级配置中调高波特率(示例波特率协商流程见dual_bank_update_uart.c的update_baudrate切换逻辑)。 - 联调手段:收发两侧均输出
log_debug与log_debug_hexdump,便于抓包比对帧格式;重试时putchar('r')输出字符便于肉眼观察丢包。 - 复位策略:升级成功路径直接
cpu_reset();如需先应答上位机再复位,可取消注释sys_timeout_add(NULL, dual_bank_cpu_reset, 2000)实现 2 秒延时复位。
扩展点(Extension Points)
- 新增升级通道:按
UPDATA_TYPE枚举注释要求,在USER_NORFLASH_UFW_UPDATA之前追加新类型,并实现update_op_api_t操作表(ch_init/f_open/f_read/f_seek/f_stop/notify_update_content_size/ch_exit),注册进active_update的update_mode_info_t即可复用整套升级框架。 - 自定义 CRC 校验:向
dual_bank_update_verify注入crc_init_hdl/crc_calc_hdl,支持非 CRC16-CCITT 的文件校验算法。 - 上位机应答:
dual_bank_write_complete_cb、burn_boot_info_result_hdl、校验失败分支等处均预留 "need user implement api to response app" 注释,可在此接入自定义协议应答(如复用 UART 帧协议或其他传输)。 - 退出清理:
dual_bank_update_exit()当前为空实现,可补充资源释放、状态上报、看门狗/低功耗恢复等逻辑。 - 升级源扩展:参照
dual_bank_update_demo(预留区文件)与dual_bank_update_demo_2(流式回调)两种模式,可对接 OTA 下载、BLE/SPP 分包等任意数据源。
测试情况(Tests)
仓库中未发现针对 dual_bank_update_* 模块的单元测试源码;该模块的正确性验证主要依赖:
- 上位机联调(
doc/双备份升级/serialupdatedemo-master_QT源码.zip提供主机端参考实现); - 运行期日志(
log_debug/log_debug_hexdump/putchar('r'))与双备份回滚的实际板级验证。 - 两个内置示例(
dual_bank_update_demo/dual_bank_update_demo_2)可作为回归测试的调用模板。