固件升级机制
本文档介绍 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 处于空闲状态。固件升级的核心思想是 "写空闲、切当前":
- 从 SD 卡(或其他传输通道)读取完整的升级固件文件;
- 把新固件写入空闲 Bank,而不是直接覆盖正在运行的代码——这样即使写入过程中断电或出错,当前系统依然可以正常运行,不会变砖;
- 写入完成后回读比对(校验)并通过 CRC16 校验数据完整性;
- 更新 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_SECTOR | Flash IOCTL | 扇区擦除(默认擦除命令) |
IOCTL_ERASE_PAGE | Flash IOCTL | 页擦除(对齐 256 时使用) |
IOCTL_SET_SFC_READ | Flash IOCTL | 切换 SFC 读模式;=1 进入真实读,=0 退出 |
DEVIVE_OPEN_ERROR | 错误码 | 设备打开失败 |
FAT_MOUNT_ERROR | 错误码 | FAT 挂载失败 |
FILE_OPEN_ERROR | 错误码 | 文件/Bank 打开失败 |
FS_IOCTL_FILE_ATTR | VFS IOCTL | 获取文件属性(长度),非 simple_fat 场景使用 |
NO_NEED_UPGRADE | update.h 枚举 | 无需升级 |
FIND_OTA_ERROR | update.h 枚举 | 未找到 OTA 数据 |
FIND_LOADER0_ERROR | update.h 枚举 | 未找到 loader0 |
失败模式、边界情况与并发
失败模式
- Flash 设备打开失败:
dev_open("sfc")返回 NULL,错误码DEVIVE_OPEN_ERROR,直接退出,不影响现有固件。 - SD 卡打开/挂载失败:分别由
DEVIVE_OPEN_ERROR与FAT_MOUNT_ERROR标识,资源按序释放。 - 升级文件不存在:
vfs_openbypath返回非 0,错误码FILE_OPEN_ERROR。 - 空闲 Bank 容量不足:
bank_size < file_size时拒绝写入,防止越界破坏当前 Bank(dual_bank_demo.c#L94-L98)。 - 擦除失败:
dev_ioctl返回非 0 时立即中断擦除循环。 - 数据校验失败:回读比对发现任意字节不一致即跳转
__verify_fail。 - 头校验失败:
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 信息切换引导。
扩展点
- 升级数据源扩展:参考实现从 SD 卡 FAT 读取文件,SDK 已提供 USB MSD(
msd_upgrade.c)与 UART(uart_update.h)两条通道。新增通道只需把"获取升级数据"替换为对应来源,写入/校验/Bank 管理逻辑可复用。 - 文件系统适配:
USE_SD_SIMP_FAT宏展示了如何在无FS_IOCTL_FILE_ATTR的精简文件系统上工作;支持标准 FAT 时可关闭该宏走 IOCTL 路径。 - 升级策略定制:
update.h中的file_patch、ota_loader_patch、ota_addr等字段与NO_NEED_UPGRADE/FIND_OTA_ERROR/FIND_LOADER0_ERROR错误码,为上层提供"是否需要升级、OTA 数据在哪、loader 是否就绪"的判断依据,可在业务层实现版本号比对、强制升级等策略。