杰理 SDK 文档中心
首页
首页
  • 项目概览与快速开始

    • 项目概述与芯片支持
    • 环境搭建与工具链
    • 工程与构建系统
    • 烧录与升级工具
    • 文档与硬件资料
  • 系统架构与芯片平台

    • 芯片平台与启动流程
    • 预编译库与头文件体系
    • 消息、定时器与中断服务
    • 通用外设驱动
  • 存储与文件系统

    • 文件系统实现
    • 存储设备驱动
    • VM 参数存储系统
  • 音频处理

    • 音频解码器
    • 音频编码器
    • MIDI 合成与播放
    • 音效、变速变调与降噪
  • 语音玩具应用

    • 应用框架与状态机
    • 音乐播放与外部音源
    • MIDI 乐器模式
    • 录音应用
    • 待机、电源管理与 USB 从机
  • 小音箱应用

    • 应用框架与模式管理
    • 播放源:音乐、FM、录音与 LineIn
  • 应用层与示例工程

    • 通用 MCU 应用
  • 固件更新与补丁

    • 固件升级机制
    • AD14N 主动降噪补丁

固件升级机制

本文档介绍 SDK 中固件升级(Firmware Update / OTA)机制的完整实现,涵盖双 Bank(dual bank)升级原理、升级文件获取、Flash 擦写与校验流程、Bank 信息管理与错误处理,并梳理 update.h 暴露的升级接口与错误码。

Purpose and Scope

本页聚焦"固件升级机制"这一能力:以 sdk/app/bsp/common/dual_bank_demo.c 中的双 Bank 升级参考实现为主线,说明固件升级从"读取升级文件 → 擦除空闲 Bank → 写入并计算 CRC → 回读校验 → 更新 Bank 信息 → 复位切换"的完整链路,并介绍 sdk/include_lib/update/update.h 中定义的升级配置结构与错误码。

相关但不属于本页主题的内容(分别由各自页面覆盖):

  • USB MSD 升级(sdk/app/bsp/common/usb/device/msd_upgrade.c):通过 USB Mass Storage 方式把设备枚举为 U 盘、由主机拷入升级文件后触发升级,属于升级文件的"传输通道",详见 USB 设备相关页面。
  • UART 升级(sdk/app/bsp/common/uart_update/uart_update.h):通过串口传输升级数据,属于另一条升级数据通道。
  • 文件系统与 Flash 驱动:vfs、sfc 设备等底层基础设施仅在本页作为被调用方说明,不展开。

Overview

在杰理(Jieli)AD15N/AD1NN 系列 MCU 方案中,固件存储在片外 SPI Nor Flash 上,并被划分为两个 Bank(区)。系统在任意时刻只从其中一个 Bank 启动运行(当前 Bank),另一个 Bank 处于空闲状态。固件升级的核心思想是 "写空闲、切当前":

  1. 从 SD 卡(或其他传输通道)读取完整的升级固件文件;
  2. 把新固件写入空闲 Bank,而不是直接覆盖正在运行的代码——这样即使写入过程中断电或出错,当前系统依然可以正常运行,不会变砖;
  3. 写入完成后回读比对(校验)并通过 CRC16 校验数据完整性;
  4. 更新 Bank 信息(标记新 Bank 为可启动、记录 CRC),系统复位后从新 Bank 启动,完成升级。

这种设计是典型的 A/B 双分区(dual bank / A-B partition)OTA 方案,其核心优势是"升级失败可回退":校验失败时只需擦除空闲 Bank,当前 Bank 不受影响。

参考实现位于 dual_bank_demo.c 的 dual_bank_test() 函数,注释明确说明 "升级流程中不可运行其他操作 flash 的流程"(dual_bank_demo.c#L16),即升级期间对 Flash 的独占性要求。

Architecture

flowchart TD
    subgraph sg_Source["升级数据源 (传输通道)"]
        SD["SD 卡 FAT 文件系统<br/>/db_data.bin"]
        USB["USB MSD 升级<br/>msd_upgrade.c"]
        UART["UART 升级<br/>uart_update.h"]
    end

    subgraph sg_Core["升级核心流程 (dual_bank_demo.c)"]
        OPEN["vfs_openbypath 打开升级文件"]
        SIZE["获取文件长度 (simple_fat 逐块统计)"]
        BANK["jlfs_get_idle_bank_info<br/>获取空闲 Bank 地址/大小"]
        ERASE["按对齐单位擦除 (IOCTL_ERASE_SECTOR/PAGE)"]
        WRITE["dev_byte_write 写入 + CRC16 累积"]
        VERIFY["回读逐字节比对 (IOCTL_SET_SFC_READ)"]
        UPDATE["jlfs_updata_dual_bank_info<br/>更新 Bank 信息 (含 CRC)"]
        CHECK["jlfs_check_dual_bank_info<br/>头信息校验"]
    end

    subgraph sg_Infra["底层基础设施"]
        VFS["vfs 文件系统抽象"]
        SFC["sfc Flash 设备驱动<br/>dev_ioctl / dev_byte_write / dev_byte_read"]
        CRC["chip_crc16_with_init"]
        JLFS["jlfs 双 Bank 信息管理"]
    end

    subgraph sg_Storage["存储"]
        BANK_A["Bank A (当前运行)"]
        BANK_B["Bank B (空闲/待升级)"]
    end

    SD --> OPEN
    OPEN --> SIZE
    SIZE --> BANK
    BANK --> ERASE
    ERASE --> WRITE
    WRITE --> VERIFY
    VERIFY --> UPDATE
    UPDATE --> CHECK

    OPEN --> VFS
    ERASE --> SFC
    WRITE --> SFC
    VERIFY --> SFC
    WRITE --> CRC
    UPDATE --> JLFS
    CHECK --> JLFS
    SFC --> BANK_A
    SFC --> BANK_B

架构说明:

  • 数据源层:SD 卡 FAT 文件系统是参考实现使用的升级文件载体;USB MSD 与 UART 是 SDK 提供的另外两条升级数据通道,最终都会把数据交给 Flash 写入逻辑。
  • 升级核心层:dual_bank_test() 串起"打开文件 → 取空闲 Bank → 擦除 → 写入 → 校验 → 更新 Bank 信息"的完整状态机,每一步都有明确的失败跳转(goto 错误标签)。
  • 基础设施层:vfs 提供文件读写抽象;sfc 提供 Flash 的擦/写/读 IOCTL;chip_crc16_with_init 提供增量 CRC16;jlfs 系列 API 负责管理双 Bank 的地址、大小与有效标志。
  • 存储层:两个 Bank 物理上位于同一片 Flash,升级只触碰空闲 Bank,保证当前运行 Bank 在升级全程不受影响。

双 Bank 升级原理与设计意图

为什么需要双 Bank

单片机的固件升级最怕"写一半断电/出错导致设备变砖"。双 Bank 方案的思路是:永远不覆盖正在执行的代码。新固件写入空闲 Bank,只有写入并校验成功后才通过 Bank 信息切换启动分区。这样:

  • 升级中断时,系统仍从旧 Bank 正常启动,可再次尝试升级;
  • 校验失败时,只需擦除目标 Bank,不产生任何副作用;
  • 无需专用的 Bootloader 引导区来做"先搬移再执行"的复杂流程。

参考实现中,升级成功后的动作是打印 "dual bank update success, waiting for reset" 并 while (1) 等待复位(dual_bank_demo.c#L190-L191),说明 Bank 切换由复位后的启动逻辑(依据 Bank 信息)完成。

升级文件与 Flash 的关系

升级文件在写入前被人为加上 4096 字节的头空间(file_attr.fsize += 4096;,dual_bank_demo.c#L78)。这 4096 字节用于在 Flash 上为 Bank 信息(升级头)预留位置,与 jlfs_updata_dual_bank_info / jlfs_check_dual_bank_info 写入和校验的头信息对应。数据区(文件本体)在校验时只比较 fsize - 4096 字节(dual_bank_demo.c#L145),即头空间不参与数据一致性比对,而是单独由 jlfs_check_dual_bank_info 校验。

升级流程详细实现

1. 打开 Flash 与升级文件

dual_bank_test() 首先打开 sfc Flash 设备与 SD 卡设备,随后在 SD 卡上挂载 FAT 文件系统并打开升级文件 /db_data.bin:

void *flash_dev = dev_open("sfc", NULL);
if (flash_dev == NULL) {
    log_error("flash_dev null !!!! \n");
    res = DEVIVE_OPEN_ERROR;
    goto __close_flash_dev;
}
const char *dev_name = __SD0_NANE;
log_info("dev name %s\n", dev_name);
struct device *device = dev_open((char *)dev_name, NULL);
if (device == NULL) {
    log_error("device null !!!! \n");
    res = DEVIVE_OPEN_ERROR;
    goto __close_dev;
}
void *pfs = NULL, *pfile = NULL;
res = vfs_mount(&pfs, (void *)device, "fat");
if (res) {
    log_error("fat mount error !!! \n");
    res = FAT_MOUNT_ERROR;
    goto __close_fs;
}
const char *file_path = "/db_data.bin";
res = vfs_openbypath(pfs, &pfile, file_path);
if (res) {
    log_error("opend file error !!! 0x%x\n", res);
    res = FILE_OPEN_ERROR;
    goto __close_fs;
}

Source: dual_bank_demo.c

设计要点:每个失败分支都有独立错误码(DEVIVE_OPEN_ERROR / FAT_MOUNT_ERROR / FILE_OPEN_ERROR)与对应的 goto 清理标签,保证资源(设备句柄、文件系统、文件句柄)按打开顺序逆序释放。这种"逐级清理"(__close_flash_dev → __close_dev → __close_fs → __close_file)是嵌入式代码中处理多资源嵌套打开的常见模式。

2. 获取文件长度

文件长度通过两种方式获得:USE_SD_SIMP_FAT 开启时使用 simple_fat,该文件系统没有获取文件长度的 IOCTL,因此代码用 512 字节缓冲逐块 vfs_seek + vfs_read 统计总长度,并在循环中 wdt_clear() 喂狗防止长文件统计期间看门狗复位:

#if USE_SD_SIMP_FAT//simple_fat没有获取文件长度接口
    u32 len_cnt = 0;
    u32 f_offset = 0;
    u8 *fr_buf = (u8 *)my_malloc(512, 0);
    while (1) {
        u32 rlen = 0;
        vfs_seek(pfile, f_offset, SEEK_SET);
        rlen = vfs_read(pfile, fr_buf, 512);
        len_cnt += rlen;
        if (rlen == 0) {
            break;
        }
        f_offset += rlen;
        wdt_clear();
    }
    file_attr.fsize = len_cnt;
    my_free((void *)fr_buf);
#else
    vfs_ioctl(pfile, FS_IOCTL_FILE_ATTR, (int)&file_attr);
#endif

Source: dual_bank_demo.c

设计意图:USE_SD_SIMP_FAT 宏把参考实现解耦为"支持标准 FAT 属性查询"与"仅支持 simple_fat"两种场景,保证在精简文件系统上也能工作。512 字节缓冲既匹配扇区大小,也控制内存占用。

3. 获取空闲 Bank 并校验容量

u32 upgrade_start_addr = 0;
u32 bank_size;
res = jlfs_get_idle_bank_info(&upgrade_start_addr, &bank_size);
log_info("addr %x,size %x\n", upgrade_start_addr, bank_size);
if (res) {
    log_error("opend bank file error !!! 0x%x\n", res);
    res = FILE_OPEN_ERROR;
    goto __write_end;
}

if (bank_size < file_size) {
    log_error("bank size too samll !!! 0x%x < \n", bank_size, file_size);
    res = FILE_OPEN_ERROR;
    goto __write_end;
}

Source: dual_bank_demo.c

jlfs_get_idle_bank_info 返回空闲 Bank(非当前运行 Bank)的起始地址与容量。写入前必须确认空闲 Bank 容量不小于升级文件大小,否则直接失败——这是防止"越界写穿到当前 Bank"的第一道防线。

4. 按对齐单位擦除

Flash 擦除的最小单位由 get_flash_alignsize() 决定:对齐 256 字节时用 IOCTL_ERASE_PAGE(页擦除),否则用 IOCTL_ERASE_SECTOR(扇区擦除):

u8 flash_erase_cmd = IOCTL_ERASE_SECTOR;
u32 flash_alignsize = get_flash_alignsize();
if (flash_alignsize == 256) {
    flash_erase_cmd = IOCTL_ERASE_PAGE;
}

log_info("erase unit %d", file_size / flash_alignsize);
int erase_err = 0;
for (u32 i = 0; i < (file_size / flash_alignsize); i ++) {
    erase_err = dev_ioctl(flash_dev, flash_erase_cmd, upgrade_start_addr + i * flash_alignsize);
    if (erase_err) {
        log_error("erase %x", upgrade_start_addr + i * flash_alignsize);
        break;
    }
    wdt_clear();
    log_info("erase %x", upgrade_start_addr + i * flash_alignsize);
}

Source: dual_bank_demo.c

设计意图:擦除必须与 Flash 物理特性对齐(页/扇区),否则擦不干净或破坏相邻区域。擦除是耗时操作,循环内 wdt_clear() 持续喂狗,避免长时间擦写触发看门狗复位导致升级中断。

5. 数据写入与 CRC16 累积

u8 *tmp_buf = (u8 *)my_malloc(512, 0);

//write data --> flash
file_size = file_attr.fsize;

u32 offset = 0;
u32 data_crc = 0;
while (file_size) {
    vfs_seek(pfile, offset, 0);
    u32 cnt = file_size > 512 ? 512 : file_size;
    vfs_read(pfile, tmp_buf, cnt);
    data_crc = chip_crc16_with_init(tmp_buf, cnt, data_crc);
    dev_byte_write(flash_dev, tmp_buf, upgrade_start_addr + offset, cnt);
    log_info("write %x", upgrade_start_addr + offset);
    offset += cnt;
    file_size -= cnt;
    wdt_clear();
}

Source: dual_bank_demo.c

写入采用"边读边写"流式方式:每块 512 字节,从文件读出后立即写入 Flash 并累计 CRC16。chip_crc16_with_init(ptr, len, init) 的第三个参数是上一次计算的 CRC 初值,因此可以在多块之间连续累加,最终得到整个固件的完整 CRC。这种增量设计避免了为整包固件分配内存缓冲,对 RAM 受限的 MCU 至关重要。

6. 回读校验(数据一致性比对)

log_info("data crc %x\n", data_crc);
dev_ioctl(flash_dev, IOCTL_SET_SFC_READ, 1);

//verify data
file_size = file_attr.fsize - 4096;
log_info("upgrade_start_addr 0x%x\n", upgrade_start_addr);
offset = 0;
u8 *f_tmp_buf = (u8 *)my_malloc(512, 0);
while (file_size) {
    vfs_seek(pfile, offset, 0);
    u32 cnt = file_size > 512 ? 512 : file_size;
    memset(tmp_buf, 0, 512);
    memset(f_tmp_buf, 0, 512);
    vfs_read(pfile, tmp_buf, cnt);
    dev_byte_read(flash_dev, f_tmp_buf, upgrade_start_addr + offset, cnt);
    for (u16 i = 0; i < 512; i++) {
        if (tmp_buf[i] != f_tmp_buf[i]) {
            log_error("verify data err  i %d\n", i);
            goto __verify_fail;
        }
    }
    offset += cnt;
    file_size -= cnt;
    wdt_clear();
}

Source: dual_bank_demo.c

设计要点:

  • 校验前通过 IOCTL_SET_SFC_READ, 1 让 Flash 进入读模式,确保读回的是真实 Flash 内容(而非写缓存);
  • 采用"文件数据 vs Flash 数据逐字节比对"的强校验,任何不一致立即跳转到 __verify_fail 失败处理;
  • 比对范围是 fsize - 4096,跳过升级头空间——头信息由后续 jlfs_check_dual_bank_info 单独校验。

7. 更新 Bank 信息并校验头

jlfs_updata_dual_bank_info(upgrade_start_addr, data_crc);

dev_ioctl(flash_dev, IOCTL_SET_SFC_READ, 1);
u32 head_check_res = jlfs_check_dual_bank_info(upgrade_start_addr);
if (head_check_res) {
    log_error("head verify fail\n");
    goto __verify_fail;
}

dev_ioctl(flash_dev, IOCTL_SET_SFC_READ, 0);
vfs_file_close(&pfile);
vfs_fs_close(&pfs);
dev_close(device);
dev_close(flash_dev);

log_info("dual bank update success, waiting for reset\n");
while (1);

Source: dual_bank_demo.c

jlfs_updata_dual_bank_info(upgrade_start_addr, data_crc) 把数据 CRC 与目标 Bank 地址写入 Bank 信息区(升级头),标记该 Bank 为"可启动";随后 jlfs_check_dual_bank_info 从 Flash 读回头信息做校验,确认头写入成功。头校验通过后按打开顺序逆序关闭所有资源,打印成功信息并 while (1) 等待复位——复位后的启动代码依据 Bank 信息决定从哪个 Bank 引导。

8. 失败处理

__verify_fail:
    dev_ioctl(flash_dev, IOCTL_SET_SFC_READ, 0);
    dev_ioctl(flash_dev, IOCTL_ERASE_SECTOR, upgrade_start_addr);
    my_free((void *)f_tmp_buf);
    my_free((void *)tmp_buf);
__write_end:
__close_file:
    vfs_file_close(&pfile);
__close_fs:
    vfs_fs_close(&pfs);
__close_dev:
    dev_close(device);
__close_flash_dev:
    dev_close(flash_dev);
    log_error("dual bank update fail\n");
    return;

Source: dual_bank_demo.c

失败路径的核心动作是:恢复 Flash 读模式、擦除整个升级 Bank(IOCTL_ERASE_SECTOR + 起始地址)、释放临时缓冲、按序关闭资源。擦除升级 Bank 使该 Bank 回到"未升级"状态,从而保证下次复位仍从当前 Bank 正常启动——这正是双 Bank 方案"失败可回退"的落地点。

核心流程

下图为一次完整的 SD 卡双 Bank 固件升级时序,展示了各模块之间的调用关系与关键 IOCTL:

sequenceDiagram
    participant APP as dual_bank_test()
    participant VFS as vfs (FAT)
    participant JLFS as jlfs Bank 管理
    participant SFC as sfc Flash 驱动
    participant SD as SD 卡

    APP->>SFC: dev_open("sfc")
    APP->>SD: dev_open(__SD0_NANE)
    APP->>VFS: vfs_mount(fat)
    APP->>VFS: vfs_openbypath("/db_data.bin")
    loop 统计文件长度 (simple_fat)
        APP->>VFS: vfs_seek / vfs_read(512)
        VFS-->>APP: len_cnt
    end
    APP->>JLFS: jlfs_get_idle_bank_info(&addr, &size)
    JLFS-->>APP: 空闲 Bank 地址 + 容量
    Note over APP: 校验 bank_size >= file_size
    loop 按对齐单位擦除
        APP->>SFC: dev_ioctl(IOCTL_ERASE_SECTOR/PAGE)
        SFC-->>APP: erase_err
    end
    loop 写入 + CRC
        APP->>VFS: vfs_read(512)
        APP->>APP: chip_crc16_with_init 累积
        APP->>SFC: dev_byte_write(addr + offset)
    end
    APP->>SFC: dev_ioctl(IOCTL_SET_SFC_READ, 1)
    loop 回读比对
        APP->>VFS: vfs_read(512)
        APP->>SFC: dev_byte_read(addr + offset)
        Note over APP: 逐字节比对,不一致则失败
    end
    APP->>JLFS: jlfs_updata_dual_bank_info(addr, data_crc)
    APP->>JLFS: jlfs_check_dual_bank_info(addr)
    Note over APP: 成功 -> while(1) 等待复位
    Note over APP: 失败 -> 擦除目标 Bank 后返回

升级状态机

stateDiagram-v2
    [*] --> 打开资源: dev_open + vfs_mount + vfs_openbypath
    打开资源 --> 获取文件长度
    获取文件长度 --> 获取空闲Bank: jlfs_get_idle_bank_info
    获取空闲Bank --> 擦除Bank: bank_size >= file_size
    获取空闲Bank --> 失败: 容量不足
    擦除Bank --> 写入数据: 逐块 write + CRC16
    写入数据 --> 回读校验: IOCTL_SET_SFC_READ=1
    回读校验 --> 更新Bank信息: 数据一致
    回读校验 --> 失败: 字节不一致
    更新Bank信息 --> 头校验: jlfs_updata_dual_bank_info
    头校验 --> 升级成功: 等待复位 while(1)
    头校验 --> 失败: head verify fail
    失败 --> 清理资源: 擦除目标Bank + 关闭资源
    清理资源 --> [*]
    升级成功 --> [*]

使用示例

示例一:完整升级调用(参考实现入口)

dual_bank_test() 是 SDK 提供的双 Bank 升级参考实现,可直接作为升级入口被业务代码调用:

/* 升级流程中不可运行其他操作flash的流程 */
void dual_bank_test()
{
    log_info("----------------- app 1------------------");

    u32 res = 0;
    void *flash_dev = dev_open("sfc", NULL);
    if (flash_dev == NULL) {
        log_error("flash_dev null !!!! \n");
        res = DEVIVE_OPEN_ERROR;
        goto __close_flash_dev;
    }
    const char *dev_name = __SD0_NANE;
    log_info("dev name %s\n", dev_name);
    struct device *device = dev_open((char *)dev_name, NULL);
    ...
}

Source: dual_bank_demo.c

示例二:Bank 信息管理 API 的声明

dual_bank_demo.c 顶部声明的 jlfs 双 Bank 接口是升级流程与 Bank 信息层之间的契约:

u32 jlfs_get_idle_bank_info(u32 *bank_addr, u32 *bank_size);
u32 get_flash_alignsize(void);
u32 jlfs_updata_dual_bank_info(u32 bank_addr, u16 data_crc);
u32 jlfs_check_dual_bank_info(u32 update_bank_addr);

Source: dual_bank_demo.c

示例三:升级配置结构(update.h)

update.h 定义了升级配置结构,其中 file_patch 为升级文件路径、ota_loader_patch 为 SD 升级使用的 OTA loader 路径、ota_addr 为 OTA 地址:

u8  file_patch[32];         //updata file patch
u8  ota_loader_patch[32];          //sd updata
u32 ota_addr;

Source: update.h

示例四:升级结果错误码(update.h)

update.h 中的枚举定义了升级流程的结果状态,NO_NEED_UPGRADE 表示无需升级,FIND_OTA_ERROR 表示未找到 OTA 数据,FIND_LOADER0_ERROR 表示未找到 loader0:

NO_NEED_UPGRADE,
FIND_OTA_ERROR,
FIND_LOADER0_ERROR,

Source: update.h

示例五:升级数据通道(USB MSD)

SDK 还提供 USB Mass Storage 升级实现 msd_upgrade.c,将设备枚举为 U 盘,由主机侧把升级文件拷入后触发同样的 Flash 升级流程:

// sdk/app/bsp/common/usb/device/msd_upgrade.c
// USB MSD 升级实现:设备作为 U 盘枚举,接收主机拷入的升级文件

Source: msd_upgrade.c

注:msd_upgrade.c 的具体函数体属于 USB 设备页面的主题,此处仅指出其存在与定位。

配置选项

选项类型默认值说明
USE_SD_SIMP_FAT宏1(开启)使用 simple_fat 文件系统时无法通过 FS_IOCTL_FILE_ATTR 获取文件长度,改为逐块读取统计;关闭时使用 vfs_ioctl(pfile, FS_IOCTL_FILE_ATTR, ...)
__SD0_NANE宏SD 设备名SD 卡设备名,决定升级文件从哪张卡读取
/db_data.bin文件路径固定升级固件在 SD 卡上的存放路径
4096 字节头常量4096写入前为 Bank 信息(升级头)预留的空间,数据校验时跳过
512 字节缓冲常量512文件读取/Flash 写读的块大小,同时用于内存复用
get_flash_alignsize()函数由 Flash 决定返回 Flash 擦除对齐单位;等于 256 时用 IOCTL_ERASE_PAGE,否则用 IOCTL_ERASE_SECTOR

API 参考

以下接口均从源码实际调用中提取,签名以源码声明为准。

jlfs_get_idle_bank_info(u32 *bank_addr, u32 *bank_size): u32

获取当前空闲 Bank(非运行 Bank)的起始地址与容量。

参数:

  • bank_addr (u32*):出参,空闲 Bank 起始地址
  • bank_size (u32*):出参,空闲 Bank 容量

返回: 0 表示成功;非 0 表示获取失败(调用方将错误码映射为 FILE_OPEN_ERROR)。

触发位置: dual_bank_demo.c#L86

jlfs_updata_dual_bank_info(u32 bank_addr, u16 data_crc): u32

把目标 Bank 地址与整包数据的 CRC16 写入 Bank 信息区(升级头),标记该 Bank 可启动。

参数:

  • bank_addr (u32):已写入固件的 Bank 起始地址
  • data_crc (u16):写入过程中由 chip_crc16_with_init 累积得到的整包 CRC16

触发位置: dual_bank_demo.c#L175

jlfs_check_dual_bank_info(u32 update_bank_addr): u32

从 Flash 读回 Bank 头信息并校验(含 CRC),确认升级头写入成功。

参数:

  • update_bank_addr (u32):待校验的 Bank 起始地址

返回: 0 表示头校验通过;非 0 表示失败(调用方打印 "head verify fail" 并进入失败清理路径)。

触发位置: dual_bank_demo.c#L178

chip_crc16_with_init(void *ptr, u32 len, u32 init): u16

以 init 为初值对 ptr 指向的 len 字节计算 CRC16,支持跨块增量累积(每块传入上一次结果作为 init)。

参数:

  • ptr (void*):数据块指针
  • len (u32):数据块长度
  • init (u32):CRC 初值(首块为 0,后续块传上一次结果)

返回: 该块的 CRC16 值。

触发位置: dual_bank_demo.c#L133

get_flash_alignsize(): u32

返回 Flash 擦除对齐单位;等于 256 时升级使用 IOCTL_ERASE_PAGE 页擦除,否则使用 IOCTL_ERASE_SECTOR 扇区擦除。

触发位置: dual_bank_demo.c#L103-L107

dual_bank_test(): void

双 Bank 升级参考实现入口:从 SD 卡读取 /db_data.bin,写入空闲 Bank 并校验,成功后 while(1) 等待复位,失败则擦除目标 Bank 并返回。升级期间不允许其他流程操作 Flash(dual_bank_demo.c#L16)。

触发位置: dual_bank_demo.c#L17

关键 IOCTL 与错误码速查

名称类型用途
IOCTL_ERASE_SECTORFlash IOCTL扇区擦除(默认擦除命令)
IOCTL_ERASE_PAGEFlash IOCTL页擦除(对齐 256 时使用)
IOCTL_SET_SFC_READFlash IOCTL切换 SFC 读模式;=1 进入真实读,=0 退出
DEVIVE_OPEN_ERROR错误码设备打开失败
FAT_MOUNT_ERROR错误码FAT 挂载失败
FILE_OPEN_ERROR错误码文件/Bank 打开失败
FS_IOCTL_FILE_ATTRVFS IOCTL获取文件属性(长度),非 simple_fat 场景使用
NO_NEED_UPGRADEupdate.h 枚举无需升级
FIND_OTA_ERRORupdate.h 枚举未找到 OTA 数据
FIND_LOADER0_ERRORupdate.h 枚举未找到 loader0

失败模式、边界情况与并发

失败模式

  1. Flash 设备打开失败:dev_open("sfc") 返回 NULL,错误码 DEVIVE_OPEN_ERROR,直接退出,不影响现有固件。
  2. SD 卡打开/挂载失败:分别由 DEVIVE_OPEN_ERROR 与 FAT_MOUNT_ERROR 标识,资源按序释放。
  3. 升级文件不存在:vfs_openbypath 返回非 0,错误码 FILE_OPEN_ERROR。
  4. 空闲 Bank 容量不足:bank_size < file_size 时拒绝写入,防止越界破坏当前 Bank(dual_bank_demo.c#L94-L98)。
  5. 擦除失败:dev_ioctl 返回非 0 时立即中断擦除循环。
  6. 数据校验失败:回读比对发现任意字节不一致即跳转 __verify_fail。
  7. 头校验失败:jlfs_check_dual_bank_info 非 0,跳转 __verify_fail。

所有失败路径的共同收尾动作:擦除目标 Bank(IOCTL_ERASE_SECTOR + 起始地址)→ 恢复 SFC 读模式 → 释放缓冲 → 逆序关闭资源。这一设计保证失败后 Bank 信息区不残留"半有效"状态,复位后仍从当前 Bank 启动。

边界情况

  • 文件长度为 0:vfs_read 立即返回 0,文件长度统计为 0;写入循环不执行,但 CRC 为 0 的"空固件"仍可能走到 Bank 信息更新——实际产品中应由上层在调用前校验固件有效性。
  • 文件长度非 512 对齐:最后一块 cnt = file_size > 512 ? 512 : file_size 取余量写入,回读比对同样按余量进行,逻辑正确。
  • 升级头 4096 字节:写入范围含头,但数据校验范围是 fsize - 4096,头区域由 jlfs_check_dual_bank_info 负责,两者分工明确。

并发与独占性

  • 源码注释明确 "升级流程中不可运行其他操作 flash 的流程"(dual_bank_demo.c#L16)。升级是 Flash 独占操作,业务代码应在调用 dual_bank_test() 前停止音频解码、录音、U 盘等所有访问 Flash 的任务,否则可能造成数据错乱。
  • 看门狗:所有耗时循环(统计长度、擦除、写入、回读)内部均调用 wdt_clear(),这是对"升级长时间占用 CPU 与 Flash"这一特性的必要保障。

性能与运维考虑

  • 擦除/写入耗时与固件大小线性相关:擦除按页/扇区、写入按 512 字节块进行,固件越大耗时越长。升级期间系统不做其他工作,因此升级前应提示用户勿断电。
  • 校验开销:回读比对是整包级别的全量校验,加上 IOCTL_SET_SFC_READ 切换,属于"以时间为代价换取可靠性"的取舍。
  • 内存占用:全程只使用两个 512 字节缓冲(tmp_buf / f_tmp_buf)与一个统计用缓冲,峰值内存 < 2 KB,对 RAM 受限 MCU 友好。
  • 运维建议:升级包需包含 4096 字节头空间对应的格式约定;升级成功后设备 while(1) 等待复位,由复位后的启动代码依据 Bank 信息切换引导。

扩展点

  1. 升级数据源扩展:参考实现从 SD 卡 FAT 读取文件,SDK 已提供 USB MSD(msd_upgrade.c)与 UART(uart_update.h)两条通道。新增通道只需把"获取升级数据"替换为对应来源,写入/校验/Bank 管理逻辑可复用。
  2. 文件系统适配:USE_SD_SIMP_FAT 宏展示了如何在无 FS_IOCTL_FILE_ATTR 的精简文件系统上工作;支持标准 FAT 时可关闭该宏走 IOCTL 路径。
  3. 升级策略定制:update.h 中的 file_patch、ota_loader_patch、ota_addr 等字段与 NO_NEED_UPGRADE / FIND_OTA_ERROR / FIND_LOADER0_ERROR 错误码,为上层提供"是否需要升级、OTA 数据在哪、loader 是否就绪"的判断依据,可在业务层实现版本号比对、强制升级等策略。

相关链接

  • dual_bank_demo.c(双 Bank 升级参考实现)
  • update.h(升级配置结构与错误码)
  • msd_upgrade.c(USB MSD 升级通道)
  • uart_update.h(UART 升级通道)
  • vfs.h(文件系统抽象接口)
  • ioctl.h(设备 IOCTL 命令编码规范)
Next
AD14N 主动降噪补丁