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

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

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

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

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

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

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

双备份升级机制与状态机

本文档介绍 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 信息烧录失败,旧固件所在分区完好无损,设备可继续从旧区启动,天然具备失败回滚能力。

该机制的核心设计意图:

  1. 断电安全:升级过程始终不影响当前运行区,任何时刻掉电都不会破坏可启动固件;
  2. 状态机驱动:升级被建模为 START → DATA → VERIFY 三个消息,配合结果回调(写完成、校验完成、boot 烧录完成)推进状态,避免在中断/主循环中长时间阻塞;
  3. 通道解耦:通过 update_op_api_t 函数指针表抽象数据来源(UART/SD/BLE/USB 等),升级核心逻辑与传输介质无关;
  4. 上位机协商:UART 通道采用带同步头(0xAA 0x55)与 CRC16 的帧协议,支持重试(RETRY_TIME = 4),并对波特率、包长进行协商。

关键源文件:

文件作用
sdk/bsp/dual_bank_update.c被动升级消息调度入口、回调处理、两个升级示例(resfile 读取 + 流式读取)
sdk/bsp/dual_bank_update_uart.cUART 通道:帧协议封装、接收状态机、命令交互、波特率协商
sdk/bsp/include/dual_bank_update_loop.h升级结果/类型/状态枚举、通道操作抽象 update_op_api_t、主动升级入口 active_update
sdk/bsp/include/dual_bank_update_uart.hUART 帧结构、消息定义、对外接口声明
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 的逐字节状态机,分四阶段:

  1. rx_cnt == 0:等待同步字节 0xAA,非 0xAA 直接丢弃;
  2. rx_cnt == 1:校验第二字节 0x55,不匹配则清零并 goto __recheck 用当前字节重新开始——允许在同一缓冲区后续字节中立即重新同步,增强抗干扰性;
  3. rx_cnt < 4:收取长度字段(2 字节);
  4. 之后按 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宏528UART 帧载荷区大小(字节)
SYNC_SIZE宏6帧头开销:2 同步字节 + 2 长度字节 + 2 CRC 字节
SYNC_MARK0 / SYNC_MARK1宏0xAA / 0x55帧同步头,接收状态机据此对齐帧边界
RETRY_TIME宏4UART 读数据重试次数(dual_bank_update_uart.c)
update_baudratestatic u329600UART 升级波特率初始值,通道退出时恢复为 dual_uart_config.baudrate
dual_uart_configuart_platform_data外部定义UART 外设参数(ID、波特率、引脚等),由具体板级配置提供
UPDATA_MAGIC宏0x5A00升级结果/类型的起始哨兵值,防止与 CRC==0 混淆(dual_bank_update_loop.h)

UART 帧协议字段(protocal_frame_t,总长 PROTOCAL_SIZE + SYNC_SIZE = 534 字节):

字段偏移长度说明
mark001同步头 0xAA
mark111同步头 0x55
length22载荷长度(含载荷首字节命令字与尾部 CRC16)
data[0..len-3]4可变命令字 + 参数(如 file_info{cmd,addr,len})
data[len-2..len-1]帧尾2CRC16(覆盖帧头+长度+载荷,即整帧除末尾 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_VERIFY
  • data (u8*):START 时为 dual_bank_start_info*;DATA 时为待写入数据缓冲;VERIFY 时置 NULL
  • len (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 返回非 0dual_bank_update_exit,旧 bank 不受影响
目标分区空间不足dual_bank_update_allow_check 返回非 0dual_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)

  1. 新增升级通道:按 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 即可复用整套升级框架。
  2. 自定义 CRC 校验:向 dual_bank_update_verify 注入 crc_init_hdl / crc_calc_hdl,支持非 CRC16-CCITT 的文件校验算法。
  3. 上位机应答:dual_bank_write_complete_cb、burn_boot_info_result_hdl、校验失败分支等处均预留 "need user implement api to response app" 注释,可在此接入自定义协议应答(如复用 UART 帧协议或其他传输)。
  4. 退出清理:dual_bank_update_exit() 当前为空实现,可补充资源释放、状态上报、看门狗/低功耗恢复等逻辑。
  5. 升级源扩展:参照 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)可作为回归测试的调用模板。

相关链接(Related Links)

  • dual_bank_update.c — 升级调度与示例
  • dual_bank_update_uart.c — UART 通道实现
  • dual_bank_update_loop.h — 升级类型/状态/通道抽象
  • dual_bank_update_uart.h — UART 帧协议定义
  • 预编译升级库 liba(AC632N/AC635N/AC636N/AC638N)
  • 双备份升级上位机参考源码(QT)
Next
UART 升级传输