杰理 SDK 文档中心
首页
首页
  • 概述与入门

    • 项目概览
    • 快速开始与开发环境
  • 应用与运行时

    • 应用入口与主循环
    • 按键驱动与用户消息处理
    • 消息系统
  • 固件升级

    • 双备份升级机制与状态机
    • UART 升级传输
    • 升级校验、启动信息与复位流程
  • 芯片与硬件支持

    • AC63 系列芯片 BSP 结构
    • 外设接口与驱动
    • 低功耗、RTC 与时基唤醒
  • 构建与工具

    • 构建系统与工作区
    • 烧写与量产工具
  • 参考资源

    • 数据手册与原理图
    • 双备份升级文档

双备份升级文档

本文档介绍 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 中,该机制被抽象为两层:

  1. 被动升级 API(passive update):由当前运行中的 App 主动调用(例如从 U 盘、SD 卡、预留区域或蓝牙通道拿到新固件数据后),把数据块逐个交给升级任务写入非易失存储。所有接口定义在 dual_bank_updata_api.h。
  2. 主动升级框架(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

代码要点:

  1. 升级文件存放在资源文件系统路径 "app/UPDATE",通过 resfile_open / resfile_get_len / resfile_get_addr 获取内容;
  2. 使用 sdfile_cpu_addr2flash_addr() 把 CPU 地址转换为 Flash 物理地址,再用 norflash_origin_read() 绕过文件系统直接读 Flash;
  3. 第一遍循环:读文件并累计计算 CRC16(CRC16_with_initval,带初值累加),得到 file_crc;
  4. 第二遍循环:以 UPDATE_TMP_BUFFER (0x1000) 为分块粒度,把数据块逐块送入 DUAL_BANK_UPDATE_DATA;
  5. 每块数据之间调用 wdt_clear() 喂狗——因为擦写 Flash 耗时长,防止看门狗复位;
  6. 任何一步返回非 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_lenu16由调用方传入每次编程的最大包长,决定单次写入粒度;应 ≤ 临时缓冲区大小
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 非 0dual_bank_update_exit,通知 App 无法升级继续运行旧固件
Flash 空间不足dual_bank_update_allow_check 非 0dual_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 场景,其行为遵循本框架的通道接口约定。

扩展点

  1. 自定义 CRC 算法:dual_bank_update_verify(crc_init_hdl, crc_calc_hdl, verify_result_hdl) 允许注入 crc_init_hdl / crc_calc_hdl 替换内部 CRC16-CCITT,适配与上位机约定的非标准校验算法。
  2. 新增升级数据源通道:实现 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 之前,勿改动已有枚举值),即可接入主动升级框架。
  3. 自定义响应协议:源码中多处标注 "need user implement api to response app"(如 dual_bank_write_complete_cb、dual_bank_verify_result_hdl 中的回复逻辑)——升级进度、成功/失败通知的对外协议由业务层实现,BSP 层只提供回调挂点。
  4. 延时复位策略:将 dual_bank_cpu_reset(NULL) 替换为 sys_timeout_add(NULL, dual_bank_cpu_reset, 2000),可先通知 App 升级成功再复位。
  5. 回退机制:通过 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 启动选择逻辑、量产工具协议属于独立主题,不在本文档范围内。

Prev
数据手册与原理图