固件升级机制
AC82N GP-MCU SDK 采用 双 Bank(A/B 分区)固件升级机制:新固件先写入当前未运行的分区,经 CRC 校验与 boot info 烧录后,重启由 bootloader 切换启动分区,实现安全、可回退的固件更新。
Purpose and Scope
本页面覆盖 AC82N SDK 固件升级的完整机制:
- 双 Bank 升级原理与 boot info 启动切换机制
- 升级库(
update.a)对外暴露的完整 API 流程(初始化 → 空间检查 → 数据写入 → CRC 校验 → boot info 烧录) - 无 CRC 场景下的回读校验方案
- 升级失败时的回退(rollback)机制
- 相关配置项与调试手段
本页面不涉及的内容(由同级页面负责):
- Bootloader 本身的引导细节与 CPU 启动汇编流程(参见"启动引导"相关页面)
- 升级数据的来源(如 BLE/USB/串口传输协议)——本页面聚焦升级库侧的处理逻辑
- 文件系统(vm/fs)内部实现——仅说明升级所需的 flash 信息如何传递
概述
固件升级是嵌入式设备最核心的运维能力之一。AC82N 采用 dual-bank(A/B 双分区) 设计:Flash 中同时存在两个 APP 分区(运行分区与升级分区),升级时只写入非运行的那个分区,完成后更新 boot info 再重启。这样即使升级中途断电、CRC 校验失败,旧固件仍可正常运行,从根本上避免"变砖"。
升级逻辑被封装为预编译库 sdk/cpu/cd09/liba/update.a,公开接口声明在头文件 dual_bank_updata_api.h 中。该库负责:
- 接收上位机(手机 App / 工具)下发的新固件数据流;
- 将数据经临时缓冲写入非运行分区(non-volatile storage);
- 计算/校验 CRC(默认 CRC16-CCITT,可自定义);
- 校验通过后烧录新固件对应的 boot info;
- 由调用方触发
system_reset,bootloader 依据 boot info 从新分区启动。
关键概念:
- RUNNING BANK / UPDATE BANK:运行分区与升级分区,两者交替使用;
- boot info:存放在 RAM 的
.boot_info段中、由 bootloader 传入的启动信息(含 sfc 参数、flash 大小、vm 对齐等),升级库通过改写它来切换启动目标; - CRC 校验:升级完成后对已写入数据重新计算并比对,通过后才允许切换分区。
架构
flowchart TD
subgraph sg_Host["上位机 (手机/工具)"]
Host["Host 固件数据流"]
end
subgraph sg_App["APP 层 (gp_mcu)"]
UpdateApi["dual_bank_updata_api.h<br/>(公开 API)"]
AppConfig["app_config.c<br/>(UPDATE 日志开关)"]
end
subgraph sg_UpdateLib["升级库 (预编译 update.a)"]
Init["dual_bank_passive_update_init<br/>设置 crc/fw_size/max_pkt_len"]
AllowCheck["dual_bank_update_allow_check<br/>空间检查"]
Write["dual_bank_update_write<br/>数据写入非易失存储"]
Verify["dual_bank_update_verify<br/>CRC 校验 (默认 CRC16-CCITT)"]
BurnInfo["dual_bank_update_burn_boot_info<br/>烧录新 boot info"]
end
subgraph sg_Boot["Boot 侧 (cd09)"]
Setup["setup.c<br/>BOOT_INFO boot_info AT(.boot_info)"]
Ld["sdk_ld.c<br/>.boot_info 段 (ram0)"]
Bootloader["Bootloader"]
end
subgraph sg_Flash["Flash"]
BankA["APP Bank A (RUNNING)"]
BankB["APP Bank B (UPDATE)"]
BootInfoFlash["boot info 区"]
end
Host -->|"数据流"| UpdateApi
UpdateApi --> Init
Init --> AllowCheck
AllowCheck --> Write
Write --> Verify
Verify --> BurnInfo
BurnInfo -->|"system_reset"| Bootloader
Setup -->|"boot_info_init 拷贝<br/>sfc/flash_size/vm.align"| Ld
Bootloader --> Setup
Setup --> Bootloader
Write --> BankB
BankA --> Bootloader
Bootloader -->|"依据 boot_info 选择启动分区"| BankA
Bootloader -->|"依据 boot_info 选择启动分区"| BankB
BurnInfo --> BootInfoFlash
AppConfig -.->|"日志开关"| UpdateApi
架构要点:
- 升级库与 boot 侧通过
BOOT_INFO结构解耦:bootloader 在启动时把 flash 设备信息填入boot_info(见 setup.c 的boot_info_init),升级库写数据时依赖这些信息定位分区;升级完成后改写 boot info 即完成"切换"动作。 .boot_info段由链接脚本 sdk_ld.c 显式放置在 ram0 起始处,保证 bootloader 与 APP 都能以固定地址访问。- 校验与切换分离:CRC 校验通过 ≠ 立即切换;只有显式调用
dual_bank_update_burn_boot_info成功烧录后,重启才会进入新固件。这是回退安全性的核心设计。
核心机制详解
双 Bank 切换与 boot info
BOOT_INFO boot_info AT(.boot_info);
来源:setup.c
BOOT_INFO 全局变量通过 AT(.boot_info) 强制放入自定义段,链接脚本将其定位在 ram0 起始处:
.boot_info ALIGN(4) : SUBALIGN(4)
{
*(.boot_info)
. = ALIGN(4);
}
来源:sdk_ld.c
设计意图:bootloader(一级引导)与 APP 共用同一物理内存地址访问该结构,无需通过固定 Flash 地址传递——这既避免了 flash 磨损,也保证了启动早期(RAM 刚初始化)即可读取。
启动时,bootloader 通过 boot_info_init 把设备信息拷入该结构:
void boot_info_init(void *_info)
{
BOOT_DEVICE_INFO *info = (BOOT_DEVICE_INFO *)_info;
memcpy(&(boot_info.sfc), &(info->sfc), sizeof(struct sfc_info));
boot_info.flash_size = info->fs_info->FlashSize;
boot_info.vm.align = info->fs_info->align;
}
来源:setup.c
sfc(串行 Flash 控制器)参数、flash_size 与 vm.align 是后续升级写 flash、管理 vm 分区的基础;boot 与 APP 之间的信息传递依赖这个共享内存结构完成。
升级 API 全流程
升级库的公开接口全部声明在 dual_bank_updata_api.h 中,调用方(App 层)按以下顺序驱动升级:
1. 获取临时缓冲大小
/* @brief:Api for getting the buffer size for temporary storage
*/
u32 get_dual_bank_passive_update_max_buf(void);
升级库内部需要一块"临时存储"缓冲(用于收包拼接、写 flash 前的数据对齐),调用方必须先向该接口询问所需缓冲大小并分配内存,供后续写数据流程使用。
2. 初始化升级任务
/* @brief:Initializes the update task,and setting the crc value and file size of new fw;
* @param fw_crc:crc value of new fw file
* @param fw_size:total size of new fw file
* @param priv:reserved
* @param max_ptk_len: Supported maxium length of every programming,it decides the max size of programming every time
*/
u32 dual_bank_passive_update_init(u32 fw_crc, u32 fw_size, u16 max_pkt_len, void *priv);
设计意图:升级开始前一次性告知"新固件文件的总大小与 CRC 值",库据此确定目标分区(非运行 bank)、预校验空间并初始化内部状态机;max_pkt_len 决定每次编程(programming)的最大数据量,直接影响单次写 flash 的分块粒度与升级速度。
3. 空间检查
/* @brief:Judge whether enough space for new fw file
* @note: it should be called after dual_bank_passive_update_init(...);
* @param fw_size:fw size of new fw file
*/
u32 dual_bank_update_allow_check(u32 fw_size);
必须在 init 之后调用。若目标分区剩余空间不足,返回非零错误,调用方应中止升级,避免写坏相邻分区。
4. 写入固件数据
/* @brief:copy the data to temporary buffer and notify task to write non-volatile storage
* @param data:the pointer to download data
* @param len:the length to download data
* @param write_complete_cb:callback for programming done,return 0 if no err occurred
*/
u32 dual_bank_update_write(void *data, u16 len, int (*write_complete_cb)(void *priv));
上位机每收到一段数据就调用一次本接口:数据先被拷入临时缓冲,库内部任务再异步写入非易失存储;每段写入完成后通过 write_complete_cb 回调通知调用方(返回 0 表示成功)。这种"拷贝-异步落盘-回调"模式把耗时 flash 编程从传输线程中剥离,避免阻塞数据接收。
5. CRC 校验
/* @brief: caculate all the data had flashed,and compare with the cre value intializeed when update init;
* @crc_init_hdl:if it equals NULL,use internal implementation(CRC16-CCITT Standard);otherwise,use user's customization;
* @crc_calc_hdl:if it equals NULL,use internal implementation(CRC16-CCITT Standard);otherwise,use user's customization;
* @verify_result_hdl:when the verification completed,this callback for result notification;
* if crc_res equals 1,crc verification passed,if 0,the verification failed.
*/
u32 dual_bank_update_verify(void (*crc_init_hdl)(void), u32(*crc_calc_hdl)(u32 init_crc, const void *data, u32 len), int (*verify_result_hdl)(int calc_crc));
对已写入 Flash 的全部数据重新计算 CRC,与 init 时设定的 fw_crc 比对。默认实现为 CRC16-CCITT Standard;若产品使用自定义校验算法,可传入 crc_init_hdl / crc_calc_hdl 覆盖,verify_result_hdl 收到 1 表示通过、0 表示失败。校验失败时调用方不得继续烧录 boot info,从而保证旧固件不被破坏。
6. 烧录新 boot info(切换分区)
/* @brief:After the new fw verification succeed,call this api to program the new boot info for new fw
* @param burn_boot_info_result_hdl:this callback for error notification
* if err equals 0,the operate to burn boot info succeed,other value means to fail.
*/
u32 dual_bank_update_burn_boot_info(int (*burn_boot_info_result_hdl)(int err));
校验通过后调用,把"新固件所在分区"写入 boot info 区。回调 err == 0 表示烧录成功,随后调用方应执行 system_reset 重启;bootloader 读取新 boot info 后从新分区启动。烧录 boot info 是升级的唯一"提交点"——在此之前断电,系统仍从旧分区启动。
7. 强制回退(擦除 boot info)
enum {
CLEAR_APP_RUNNING_BANK = 0,
CLEAR_APP_UPDATE_BANK,
};
/* @brief:this api for erasing the boot info of specific bank,it should be called much carefully
* @param type:it decides which bank's boot info would be erased;
* clean the boot info of running bank and call system_reset,system will run the other bank if available;
*/
int flash_update_clr_boot_info(u8 type);
用于紧急回退:擦除 running bank 的 boot info 后 system_reset,bootloader 找不到当前分区引导信息时,若另一分区可用则从另一分区启动。该接口注释明确提示"必须谨慎调用"——这是双 Bank 方案的最后兜底手段,例如新固件运行异常(多次启动失败)时用于恢复旧版本。
8. 辅助接口
/* @brief:this api for user read flash data to calculate crc
* @param offset: the offset relative to update area
read_buf: user data buffer
read_len: read length
@returns: Actual read length
*/
u32 dual_bank_update_read_data(u32 offset, u8 *read_buf, u32 read_len);
u8 dual_bank_update_verify_without_crc_new(int (*verify_result_hdl)(int calc_crc));
/**
* @brief 获取待升级目标地址,在调用dual_bank_passive_update_init之后使用有效
*/
u32 dual_bank_passive_update_get_target_update_addr(void);
/**
* @brief 对于手机没有传CRC过来的方案,需要自己读取每个文件出来校验
*/
u8 dual_bank_update_verify_without_crc(void);
dual_bank_update_read_data:以"升级区相对偏移"读取已写入的 flash 数据,供调用方自行计算 CRC;dual_bank_update_verify_without_crc/_new:针对上位机(如手机)不传 CRC 的场景——库无法预知完整文件的校验值,因此升级完成后由调用方(或库按回调)逐段回读文件自行校验;dual_bank_passive_update_get_target_update_addr:init之后返回本次升级目标分区的起始地址,用于进度显示或校验对比。
日志与调试
UPDATE 模块的日志开关在 App 配置中单独控制:
const char log_tag_const_v_UPDATE AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(FALSE);
const char log_tag_const_d_UPDATE AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(FALSE);
const char log_tag_const_c_UPDATE AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(FALSE);
const char log_tag_const_i_UPDATE AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(TRUE);
const char log_tag_const_w_UPDATE AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(TRUE);
const char log_tag_const_e_UPDATE AT(.LOG_TAG_CONST) = CONFIG_DEBUG_EN(TRUE);
来源:app_config.c
生产版本默认关闭 verbose/debug,保留 info/warn/error 级日志,便于现场定位升级异常(如写入失败、CRC 不匹配、boot info 烧录失败)。
核心流程
成功升级时序
sequenceDiagram
participant H as 上位机 (Host)
participant A as App 升级模块
participant U as 升级库 (update.a)
participant F as Flash
participant B as Bootloader
H->>A: 下发固件头 (crc, size)
A->>U: get_dual_bank_passive_update_max_buf()
U-->>A: 缓冲大小
A->>U: dual_bank_passive_update_init(crc, size, max_pkt_len)
U->>U: 确定目标分区 (非运行 bank)
A->>U: dual_bank_update_allow_check(size)
U-->>A: OK / 空间不足
loop 每包数据
H->>A: 固件数据包
A->>U: dual_bank_update_write(data, len, cb)
U->>F: 写非易失存储
F-->>U: 编程完成
U-->>A: write_complete_cb(0)
end
A->>U: dual_bank_update_verify(crc_init, crc_calc, result_cb)
U->>F: 回读已写数据计算 CRC
F-->>U: 数据
U-->>A: verify_result_hdl(1) 通过
A->>U: dual_bank_update_burn_boot_info(cb)
U->>F: 烧录新 boot info
F-->>U: err == 0
U-->>A: 烧录成功
A->>B: system_reset()
B->>B: 读取 boot info
B-->>B: 切换到新 bank
B->>F: 从新分区启动 APP
失败回退流程
flowchart TD
Start([升级开始]) --> Init["dual_bank_passive_update_init"]
Init --> Check{"allow_check 空间足够?"}
Check -->|"否"| Abort["中止升级<br/>旧固件继续运行"]
Check -->|"是"| WriteLoop["写数据循环"]
WriteLoop --> Verify{"CRC 校验通过?"}
Verify -->|"否 (verify_result=0)"| Abort
Verify -->|"是"| Burn["burn_boot_info"]
Burn -->|"失败 (err!=0)"| Abort
Burn -->|"成功"| Reset["system_reset<br/>bootloader 读取 boot info"]
Reset --> BootNew["从新 bank 启动新固件"]
BootNew -->|"新固件运行异常"| ClrInfo["flash_update_clr_boot_info(CLEAR_APP_RUNNING_BANK)<br/>+ system_reset"]
ClrInfo --> BootOld["回退到另一 bank (旧固件)"]
配置选项
升级机制本身以 API 参数形式配置,无独立配置文件;相关配置点如下:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fw_crc | u32 | 由上位机下发 | 新固件文件整体 CRC,init 时传入,校验阶段比对 |
fw_size | u32 | 由上位机下发 | 新固件总大小,用于空间检查与写入进度控制 |
max_pkt_len | u16 | 调用方决定 | 每次编程的最大长度,决定单次写 flash 分块大小(影响速度与缓冲占用) |
| CRC 算法 | 回调 | CRC16-CCITT Standard | 传 crc_init_hdl/crc_calc_hdl 可替换为自定义算法;传 NULL 用内置实现 |
write_complete_cb | 回调 | 必填 | 每段数据编程完成通知,返回 0 表示成功 |
verify_result_hdl | 回调 | 必填 | 校验结果通知,1=通过,0=失败 |
burn_boot_info_result_hdl | 回调 | 必填 | boot info 烧录结果通知,0=成功 |
| UPDATE 日志级别 | 编译开关 | i/w/e 开启,v/d/c 关闭 | CONFIG_DEBUG_EN() 控制,见 app_config.c |
API 参考
| 函数 | 签名 | 作用 |
|---|---|---|
get_dual_bank_passive_update_max_buf | u32 get_dual_bank_passive_update_max_buf(void) | 获取升级临时缓冲所需大小 |
dual_bank_passive_update_init | u32 dual_bank_passive_update_init(u32 fw_crc, u32 fw_size, u16 max_pkt_len, void *priv) | 初始化升级任务,设定 CRC 与文件大小 |
dual_bank_passive_update_exit | u32 dual_bank_passive_update_exit(void *priv) | 退出升级任务(异常中止时释放资源) |
dual_bank_update_allow_check | u32 dual_bank_update_allow_check(u32 fw_size) | 校验目标分区是否有足够空间(须在 init 之后调用) |
dual_bank_update_write | u32 dual_bank_update_write(void *data, u16 len, int (*write_complete_cb)(void *priv)) | 拷贝数据到临时缓冲并异步写非易失存储 |
dual_bank_update_verify | u32 dual_bank_update_verify(void (*crc_init_hdl)(void), u32(*crc_calc_hdl)(u32, const void*, u32), int (*verify_result_hdl)(int)) | 回读已写数据做 CRC 校验 |
dual_bank_update_burn_boot_info | u32 dual_bank_update_burn_boot_info(int (*burn_boot_info_result_hdl)(int err)) | 校验通过后烧录新固件的 boot info |
flash_update_clr_boot_info | int flash_update_clr_boot_info(u8 type) | 擦除指定 bank 的 boot info(CLEAR_APP_RUNNING_BANK/CLEAR_APP_UPDATE_BANK),用于强制回退 |
dual_bank_update_read_data | u32 dual_bank_update_read_data(u32 offset, u8 *read_buf, u32 read_len) | 按升级区相对偏移回读 flash 数据,返回实际读取长度 |
dual_bank_update_verify_without_crc | u8 dual_bank_update_verify_without_crc(void) | 无 CRC 场景下的整文件回读校验 |
dual_bank_update_verify_without_crc_new | u8 dual_bank_update_verify_without_crc_new(int (*verify_result_hdl)(int calc_crc)) | 无 CRC 场景带结果回调的校验 |
dual_bank_passive_update_get_target_update_addr | u32 dual_bank_passive_update_get_target_update_addr(void) | 获取本次升级目标分区起始地址(init 后有效) |
失败模式、边界情况与并发
- 升级中途断电:由于 boot info 尚未烧录,重启后 bootloader 仍引导旧分区,升级数据留在升级分区等待下次重试——这是双 Bank 相比单分区升级最大的可靠性优势。
- CRC 校验失败:
verify_result_hdl收到 0,调用方不得调用burn_boot_info;系统保持旧固件运行。 - 空间不足:
dual_bank_update_allow_check返回错误,必须在写入前拦截,防止越界覆盖相邻分区(如 vm、资源区)。 - boot info 烧录失败:
burn_boot_info_result_hdl收到非 0 时不可复位,否则仍会启动旧固件,需重新走升级流程。 - 新固件运行异常:通过
flash_update_clr_boot_info(CLEAR_APP_RUNNING_BANK)+system_reset回退到另一 bank;该接口被注释明确标注"谨慎调用",因为擦除运行分区 boot info 是破坏性操作。 - 并发/时序:
dual_bank_update_write采用"拷贝到临时缓冲 + 异步任务写 flash + 回调"模型,调用方(传输线程)与 flash 编程任务解耦;write_complete_cb返回非 0 表示该段写入出错,调用方应停止继续发送并中止升级。升级期间应避免其他任务并发访问升级分区。
性能与运维建议
max_pkt_len权衡:该值决定每次编程的数据量。增大可减少编程次数、提升吞吐,但要求更大的临时缓冲(由get_dual_bank_passive_update_max_buf返回)与单包缓冲;需结合上位机链路 MTU 与 RAM 预算选择。- CRC 计算开销:默认 CRC16-CCITT 为软件逐字节计算,大固件文件回读校验耗时较长;如芯片有硬件 CRC 外设,可通过自定义
crc_calc_hdl接入以缩短校验时间。 - 日志分级:正式产品建议保持 UPDATE 的 i/w/e 日志开启、v/d/c 关闭(
app_config.c默认配置),兼顾现场可诊断性与日志开销。 - 升级入口:升级库以预编译库
sdk/cpu/cd09/liba/update.a形式发布,App 层链接该库并调用头文件声明接口即可,无需修改库内部实现。
扩展点
- 自定义 CRC 算法:
dual_bank_update_verify的crc_init_hdl/crc_calc_hdl参数允许完全替换校验算法(如 CRC32 或硬件加速)。 - 各阶段回调:
write_complete_cb、verify_result_hdl、burn_boot_info_result_hdl提供了写入、校验、提交三个关键节点的通知钩子,可用于实现升级进度 UI、失败重试策略。 - 强制回退策略:
flash_update_clr_boot_info+system_reset是产品侧实现"启动 N 次失败自动回退"等自愈策略的基元。 - 无 CRC 传输方案:
dual_bank_update_verify_without_crc系列接口为手机等不传 CRC 的上位机方案提供回读校验能力,配合dual_bank_update_read_data可自定义逐文件校验逻辑。
相关链接
- 升级 API 头文件:dual_bank_updata_api.h
- 预编译升级库:
sdk/cpu/cd09/liba/update.a - boot info 初始化:setup.c(boot 侧启动信息传递,见"启动引导"页面)
.boot_info链接段:sdk_ld.c- UPDATE 日志配置:app_config.c
- 相关主题:启动引导流程(boot)、Flash 分区与文件系统(vm/fs)