固件升级机制
AD24N SDK 的固件升级(Firmware Update)子系统:定义统一的升级通道枚举(USB/SD/UART/BT/BLE/双 Bank 等)、跨阶段升级参数结构 UPDATA_PARM、升级目标注册机制与升级结果校验流程,负责将 .ufw 升级文件安全地写入 Flash 并引导加载器(loader/uboot)完成刷写。
Purpose and Scope
本文档介绍 AD24N 固件升级机制的完整实现,覆盖:
- 升级通道类型枚举(
UPDATA_TYPE)与升级触发方式(UPGRADE_TYPE) - 应用层与 loader 之间传递的升级参数结构
UPDATA_PARM及其扩展参数 - 升级状态机(
UPDATE_STATE_T)、升级目标注册宏(REGISTER_UPDATE_TARGET) - 升级结果校验(
UPDATA_RESULT)、开机恢复与结果上报流程 - 相关 API(
update.h、dev_update.h、uart_update.h)与配置开关
以下主题属于相邻页面,不在本文展开:USB MSD 升级的具体实现(见 sdk/app/bsp/common/usb/device/msd_upgrade.c)、UART 升级协议细节(见 sdk/app/bsp/common/uart_update/uart_update.c)、玩具类应用升级(sdk/app/src/voice_func/toy_update/toy_update.c)以及设备端升级执行器 sdk/app/bsp/common/update/dev_update.c。
概述
AD24N 的固件升级采用「应用层下载 + loader 刷写」的两段式架构:
- 应用层(SDK) 负责通过不同通道(USB、SD 卡、UART、BLE/SPP、双 Bank 等)获取升级文件(默认
*.ufw),并做初步校验。 - 应用层把升级参数(文件路径、目标地址、结果标志等)打包成
UPDATA_PARM传给底层 loader(uboot),随后跳转复位进入 loader。 - loader 根据参数完成 Flash 写入,并把结果写回
UPDATA_PARM.parm_result;下次启动时 SDK 通过update_result_deal()读取结果,决定是否恢复用户区(vm_need_recover)或上报成功信息(succ_report_t)。
核心设计意图:
- 参数段(UPDATA_PARM)固定在 Flash 指定地址(
UPDATA_FLAG_ADDR),使 SDK 与 loader 之间无需函数调用即可通过内存映射交换数据,天然支持跨镜像(应用 → loader)传递。 - 魔法数
UPDATA_MAGIC (0x5A00)防止 CRC 为 0 的误判;升级结果枚举的起始值即该魔法数。 - 升级目标(update target)通过段属性注册(
sec(.update_target)),将「通道驱动关闭」这类升级前置动作与主流程解耦,新通道只需注册自己的update_target即可被list_for_each_update_target自动遍历执行。
架构
flowchart TD
subgraph sg_Channels["升级通道 (Upgrade Channels)"]
USB["USB 升级<br/>msd_upgrade.c / USB_HID"]
UART["UART 升级<br/>uart_update.c"]
SD["SD 卡升级<br/>UPDATA_SD"]
BT["BT/BLE/SPP 升级<br/>toy_update.c / OTA"]
DUAL["双 Bank 升级<br/>dual_bank_updata_api"]
TESTBOX["TestBox 升级<br/>testbox_uart_update"]
end
subgraph sg_App["应用层 (SDK App)"]
INIT["app_update_init()"]
HANDLE["app_update_handle(msg)"]
UPDATE_C["update.c<br/>update_mode_api_v2()"]
DEVC["dev_update.c<br/>dev_update_by_change_stack()"]
RESULT["update_result_deal()"]
end
subgraph sg_Loader["底层引导 (Loader/Uboot)"]
PARM["UPDATA_PARM<br/>(UPDATA_FLAG_ADDR 固定地址)"]
FLASH["Flash 写入"]
BOOT["开机启动检查<br/>update_success_boot_check()"]
end
USB --> INIT
UART --> INIT
SD --> INIT
BT --> INIT
DUAL --> INIT
TESTBOX --> INIT
INIT --> HANDLE
HANDLE --> UPDATE_C
UPDATE_C --> DEVC
DEVC -->|"填写参数并跳转"| PARM
PARM --> FLASH
FLASH -->|"结果写回 parm_result"| BOOT
BOOT -->|"读取结果"| RESULT
RESULT -->|"UPDATA_SUCC 则恢复 VM"| INIT
图中各环节的职责:
- 升级通道:各种获取升级文件的方式,统一由
app_update_init()初始化、app_update_handle(int msg)分发消息驱动。 update.c:升级主控,维护g_updata_flag、ota_status、succ_report等全局状态;update_mode_api_v2()是核心入口,接收UPDATA_TYPE、私有参数填充回调与跳转前处理回调。dev_update.c:设备级升级执行,dev_update_check()校验升级文件,dev_update_by_change_stack()切换栈执行升级写入。UPDATA_PARM:SDK 与 loader 之间的唯一契约,256 字节,存放于固定 Flash 地址,含文件路径、ota_addr、扩展参数(ext_arg)等。- 结果回读:loader 把结果写入
parm_result,SDK 启动后通过update_result_deal()消费,用于判断是否执行vm_need_recover()恢复用户配置。
升级通道类型:UPDATA_TYPE
update.h 用枚举集中定义了 SDK 支持的全部升级通道,这是升级机制的核心抽象——任何升级入口最终都归结为一个 UPDATA_TYPE 值,并伴随一个 UPDATA_PARM 传给 loader:
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,
TESTBOX_UART_UPDATA,
TESTBOX_UART_UPDATA_2,
//NOTE:以上的定义不要调整,新升级方式在此添加,注意加在USER_NORFLASH_UFW_UPDATA之前;
USER_NORFLASH_UFW_UPDATA,
USER_LC_FLASH_UFW_UPDATA,
USB_HID_UPDATA,
NON_DEV = 0xFFFF,
} UPDATA_TYPE;
Source: update.h
设计要点:
- 枚举值从
UPDATA_MAGIC(0x5A00)开始连续编号,因此「类型」与「结果」共用同一套编号空间起始值,且类型值天然携带魔法数校验语义。 - 注释明确约定:已定义的顺序不可调整,因为类型值会持久化到 Flash 参数段,老固件/loader 依赖这些数值;新通道必须追加在
USER_NORFLASH_UFW_UPDATA之前。 NON_DEV = 0xFFFF表示无有效设备,作为非法值哨兵。- 触发方式与通道类型解耦:
UPGRADE_TYPE枚举描述「用户如何进入升级」(如UPGRADE_USB_HARD_KEY硬按键、UPGRADE_USB_SOFTKEY软件命令、UPGRADE_UART_SOFT_KEY、UPGRADE_UART_ONE_WIRE_HARD_KEY单线硬按键),而UPDATA_TYPE描述「升级内容从哪个设备来」。
typedef enum {
UPGRADE_NULL = 0,
UPGRADE_USB_HARD_KEY,
UPGRADE_USB_SOFTKEY,
UPGRADE_UART_SOFT_KEY,
UPGRADE_UART_ONE_WIRE_HARD_KEY,
} UPGRADE_TYPE;
Source: update.h
跨阶段参数契约:UPDATA_PARM
SDK 与 loader 之间不通过函数调用通信,而是通过一块固定地址的共享内存交换升级参数。update.h 中定义了参数段的布局:
#define UPDATA_FLAG_ADDR ((void *)((u32)&UPDATA_BEG + 0x08))
#define BOOT_STATUS_ADDR ((void *)((u32)&UPDATA_BEG)) //预留8个bytes
#define UPDATA_MAGIC (0x5A00) //防止CRC == 0 的情况
#define UPDATA_KEEP_IO_ENABLE 0 //升级时保持住IO功能
typedef struct _UPDATA_PARM {
u16 parm_crc;
u16 parm_type; //UPDATA_TYPE:sdk pass parm to uboot
u16 parm_result; //UPDATA_TYPE:uboot return result to sdk
u16 magic; //0x5441
//8byte
union {
struct { u8 file_path[32]; }; //updata file path
struct { u8 file_patch[32]; }; //updata file path
};
u8 parm_priv[32]; //sd updata
//64byte
u32 ota_addr;
u16 ext_arg_len;
u16 ext_arg_crc;
//8 byte
} UPDATA_PARM;
字段语义:
| 字段 | 方向 | 说明 |
|---|---|---|
parm_crc | SDK→loader | 参数段 CRC 校验,防止参数被破坏 |
parm_type | SDK→loader | UPDATA_TYPE 值,告知 loader 升级来源 |
parm_result | loader→SDK | UPDATA_RESULT 值,loader 回写升级结果 |
magic | 双向 | 0x5441(UPDATE_PARAM_MAGIC),标识参数段有效 |
file_path[32] | SDK→loader | 升级文件路径,如 /*.ufw |
parm_priv[32] | SDK→loader | 私有参数(如 SD 升级的 IO 配置) |
ota_addr | SDK→loader | OTA 目标地址 |
ext_arg_len/ext_arg_crc | SDK→loader | 扩展参数块长度与 CRC |
magic 使用 0x5441 而非 UPDATA_MAGIC,是为了在结果枚举(从 0x5A00 起)与参数段魔法数之间保持独立,避免误判。
扩展参数通过 ext_arg_t 以 type/len/data 三元组传递,EXT_ARG_TYPE 枚举定义了可携带的附加信息,覆盖从 RF 校准到升级行为的方方面面:
enum EXT_ARG_TYPE {
EXT_LDO_TRIM_RES = 0,
EXT_JUMP_FLAG,
EXT_BT_MAC_ADDR,
EXT_RF_PA_INFO,
EXT_RESERVED_UPDATE, // 用于sdk传入参数控制是否升级预留区域
EXT_RESET_TIME_WITHOUT_CONN,// 用于sdk传入未连接状态保持多久就重启的超时时间
EXT_SD_IO_INFO,
EXT_BT_WLA_INFO,
EXT_MUTIL_UPDATE_NAME = 0x8,
EXT_USER_API_BIN_INFO,
EXT_NEW_SDK_UPD_AGAIN,
EXT_NEW_FILENAME,
EXT_KEEP_ROMIO_INFO = 0x10,
EXT_TYPE_MAX = 0xff,
};
Source: update.h
例如 EXT_BT_MAC_ADDR 用于在升级过程中保留蓝牙 MAC,EXT_KEEP_ROMIO_INFO 用于保持 ROM IO 配置,EXT_RESET_TIME_WITHOUT_CONN 让 loader 在未连接状态下等待指定时间后自动重启——这些都是「升级不丢配置、不卡死」的关键设计。
升级状态机与结果枚举
UPDATE_STATE_T 定义了应用层升级任务的生命周期:
typedef enum _UPDATE_STATE_T {
UPDATE_TASK_INIT,
UPDATE_CH_INIT,
UPDATE_CH_SUCESS_REPORT,
UPDATE_CH_EXIT,
} UPDATE_STATE_T;
Source: update.h
升级结果枚举与 UPDATA_MAGIC 对齐,使「未升级」状态携带魔法数语义:
typedef enum {
UPDATA_NON = UPDATA_MAGIC, //0x5A00 初始/无升级
UPDATA_READY, //已就绪
UPDATA_SUCC, //升级成功
UPDATA_PARM_ERR, //参数错误
UPDATA_DEV_ERR, //设备错误
UPDATA_KEY_ERR, //密钥/校验错误
} UPDATA_RESULT;
Source: update.h
update.c 用两个位标志进一步细化状态:DEVICE_UPDATE_KEY_ERR BIT(30) 标记升级密钥错误、DEVICE_FIRST_START BIT(31) 标记设备首次启动。update_result_set()/update_result_get() 封装对 UPDATA_FLAG_ADDR 的读写,vm_need_recover() 则根据 g_updata_flag 的低 16 位是否等于 UPDATA_SUCC 决定是否恢复用户 VM 区。
升级目标注册机制
为了让每个升级通道在跳转 loader 前执行自己的清理动作(如关闭蓝牙、关闭外设驱动),SDK 提供了基于链接器段的注册机制:
struct update_target {
char *name;
update_handler_t driver_close;
};
#define REGISTER_UPDATE_TARGET(target) \
const struct update_target target sec(.update_target)
extern const struct update_target update_target_begin[];
extern const struct update_target update_target_end[];
#define list_for_each_update_target(p) \
for (p = update_target_begin; p < update_target_end; p++)
Source: update.h
使用方式:任意模块用 REGISTER_UPDATE_TARGET(my_target) 声明一个 const struct update_target,链接器会把它放入 .update_target 段;升级流程通过 list_for_each_update_target(p) 遍历 update_target_begin 到 update_target_end,逐一调用 driver_close,从而在跳转前统一关闭各外设驱动(如 hci_controller_destory()、ll_hci_destory()、wifi_det_close())。这是典型的编译期注册 + 段遍历模式(类似 Linux 的 __initcall),新增通道无需改动主流程代码。
核心流程
一次完整升级的执行时序
sequenceDiagram
participant APP as 应用任务 (app)
participant UPD as update.c
participant TGT as update_target 段
participant DEV as dev_update.c
participant LOADER as Loader/Uboot
participant FLASH as Flash
APP->>UPD: app_update_init() 注册回调
APP->>UPD: app_update_handle(msg) 收到升级消息
UPD->>UPD: 设置 g_updata_flag / ota_status
UPD->>TGT: list_for_each_update_target 遍历
TGT-->>UPD: 依次调用 driver_close() 关闭外设驱动
UPD->>DEV: update_mode_api_v2(type, priv_fill, jump_hdl)
DEV->>DEV: dev_update_check(logo, check_flag) 校验升级文件
DEV->>UPD: update_param_priv_fill(p, priv, len) 填充私有参数
UPD->>LOADER: 填写 UPDATA_PARM(parm_type, file_path, ota_addr)
UPD->>LOADER: 跳转复位 (common_update_before_jump_reset_handle)
LOADER->>FLASH: 按 parm_type 读取文件并写入 Flash
FLASH-->>LOADER: 写入完成
LOADER->>UPD: 回写 parm_result (UPDATA_SUCC/ERR)
Note over UPD: 设备重启
APP->>UPD: update_result_deal() 读取结果
UPD->>UPD: update_success_boot_check() 校验成功启动
UPD->>APP: vm_need_recover() == true 时恢复用户 VM 区
主控实现要点
update.c 是升级主控的落地文件,其关键全局定义如下:
#define LOADER_NAME "LOADER.BIN"
#define DEVICE_UPDATE_KEY_ERR BIT(30)
#define DEVICE_FIRST_START BIT(31)
//升级文件路径必须是短文件名(8+3)结构,仅支持2层目录
const char updata_file_name[] = "/*.ufw";
static u32 g_updata_flag = 0;
static volatile u8 ota_status = 0;
static succ_report_t succ_report;
bool vm_need_recover(void)
{
log_info(">>>[test]:g_updata_flag = 0x%x\n", g_updata_flag);
return ((g_updata_flag & 0xffff) == UPDATA_SUCC) ? true : false;
}
u16 *get_updata_flag_addr()
{
return UPDATA_FLAG_ADDR;
}
char *get_last_update_device(void)
{
u32 update_res = get_update_result();
u32 update_type = get_update_type();
...
}
Source: update.c
要点解读:
updata_file_name[] = "/*.ufw"是默认升级文件匹配模式;注释强调升级文件路径必须是短文件名(8+3)结构且仅支持两层目录——这是 loader 文件系统(FAT)的硬约束。g_updata_flag的低 16 位存放UPDATA_RESULT,vm_need_recover()以此判断是否需要恢复 VM——升级成功后用户配置区的数据(音量、蓝牙配对等)需要保留,若升级失败则丢弃。ota_status声明为volatile u8,可能被中断/协议栈异步修改(如get_ota_status()外部查询)。DEVICE_UPDATE_KEY_ERR/DEVICE_FIRST_START两个高位标志用于区分「密钥错误」与「首次启动」,避免与低 16 位的升级结果混淆。- 启动时通过
get_last_update_device()结合get_update_result()与get_update_type()还原上一次升级的来源设备与结果,用于语音/灯效提示(UPDATE_VOICE_REMIND/UPDATE_LED_REMIND编译开关)。
使用示例
1. 发起一次升级(核心 API 组合)
应用通过 update_mode_api_v2() 发起升级,可传入私有参数填充回调与跳转前处理回调:
void update_mode_api_v2(UPDATA_TYPE type, void (*priv_param_fill_hdl)(UPDATA_PARM *p), void (*priv_update_jump_handle)(int type));
void update_param_priv_fill(UPDATA_PARM *p, void *priv, u16 priv_len);
u16 update_result_get(void);
bool device_is_first_start(void);
int update_result_deal(void);
void update_result_set(u16 result);
void update_clear_result(void);
bool update_success_boot_check(void);
void update_enter_cb_register(void *callback);
void app_update_handle(int msg);
int app_update_init(void);
void update_keep_io(UPDATA_PARM *p);
Source: update.h
典型调用链:app_update_init() 完成初始化并注册进入回调 → 收到升级消息后调用 app_update_handle(msg) → 内部按 UPDATA_TYPE 分发到对应通道 → 经 update_mode_api_v2() 组装参数 → update_param_priv_fill() 追加私有数据(如蓝牙 MAC)→ 跳转 loader。
2. 设备级升级校验与执行
dev_update.h 暴露设备侧接口,供 update.c 调用:
void *dev_update_get_parm(int type);
u16 dev_update_check(char *logo, bool check_flag);
u16 dev_update_by_change_stack(update_mode_info_t *p_info);
Source: dev_update.h
dev_update_get_parm(type):按类型取设备升级参数(如UPDATA_SD的 IO/波特率配置)。dev_update_check(logo, check_flag):校验升级文件头部 logo 与标志,决定是否继续。dev_update_by_change_stack(p_info):切换栈后执行升级——升级过程需要较大的栈空间,直接在主任务栈上运行可能溢出,故先切换到专用升级栈再执行 Flash 写入,完成后恢复。
3. 注册升级目标(自定义通道清理动作)
struct update_target {
char *name;
update_handler_t driver_close;
};
#define REGISTER_UPDATE_TARGET(target) \
const struct update_target target sec(.update_target)
#define list_for_each_update_target(p) \
for (p = update_target_begin; p < update_target_end; p++)
Source: update.h
新通道只需在任意 .c 文件中写 REGISTER_UPDATE_TARGET(my_target) 并实现 driver_close,升级跳转前框架会自动调用。update.c 中已经用 extern 声明了一批待关闭驱动:ll_hci_destory()、hci_controller_destory()、ram_protect_close()、hwi_all_close()、wifi_det_close()(weak 符号,未实现时打印提示),可见跳转 loader 前会关闭中断、关闭 RAM 保护、销毁蓝牙控制器,确保 loader 环境干净。
配置选项
升级机制的编译期/运行期开关分散在 app_config.h 与 update.h,update.c 顶部同时通过 #undef 覆盖了一批默认值:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
UPDATA_MAGIC | 宏 | 0x5A00 | 升级结果/类型枚举的起始魔法数,防止 CRC 为 0 的误判 |
UPDATA_KEEP_IO_ENABLE | 宏 | 0 | 升级时是否保持 IO 功能(置 1 则升级期间 IO 不释放) |
UPDATE_PARAM_MAGIC | 宏 | 0x5441 | UPDATA_PARM.magic 参数段魔法数 |
UPDATA_PARM_SIZE | 宏 | 256 | 参数段总大小(字节) |
UPDATE_PRIV_PARAM_LEN | 宏 | 32 | 私有参数区长度 |
CONFIG_UPDATE_STORAGE_DEV_EN | 常量 | 见编译配置 | 存储设备(SD/NOR)升级使能 |
CONFIG_UPDATE_TESTBOX_UART_EN | 常量 | 见编译配置 | TestBox UART 升级使能 |
CONFIG_UPDATE_APP_OTA_EN | 常量 | 见编译配置 | 应用 OTA 使能(依赖 rcsp_bluetooth.h) |
CONFIG_UPDATE_TESTBOX_BLE_EN | 常量 | 见编译配置 | TestBox BLE 升级使能 |
TESTBOX_UART_UPDATE_EN | 宏 | 编译配置 | 是否包含 testbox_uart_update.h |
CONFIG_APP_OTA_EN | 宏 | 编译配置 | 是否包含 rcsp_bluetooth.h(OTA 依赖) |
CONFIG_UPDATE_CHANGE_NAME | 宏 | 1(update.c 覆盖) | 升级后是否修改设备名 |
CONFIG_UPDATE_JUMP_TO_MASK | 宏 | 0(update.c 覆盖) | 升级是否跳转到 Mask ROM |
TCFG_UI_ENABLE | 宏 | 0(update.c 覆盖) | 升级期间 UI 使能 |
TCFG_BT_BACKGROUND_ENABLE | 宏 | 0(update.c 覆盖) | 升级期间蓝牙后台运行 |
OTA_TWS_SAME_TIME_ENABLE | 宏 | 0(update.c 覆盖) | 双耳同时 OTA |
update.c 顶部的 #undef 块是强制覆盖:即使 app_config.h 开了 UI/蓝牙/ANC 等功能,升级流程也会强制关闭这些无关模块,保证升级期间系统资源(CPU、Flash、中断)完全让给升级任务。这是「升级优先」的嵌入式设计惯例——升级失败会导致设备变砖,因此不惜牺牲功能可用性。
API 参考
void update_mode_api_v2(UPDATA_TYPE type, void (*priv_param_fill_hdl)(UPDATA_PARM *p), void (*priv_update_jump_handle)(int type))
升级主入口。按 type 选择升级通道,priv_param_fill_hdl 在参数填充阶段被回调(用于追加私有数据),priv_update_jump_handle 在跳转 loader 前被回调(用于最后的清理动作)。
参数:
type(UPDATA_TYPE):升级通道类型,如USB_UPDATA、UART_UPDATA、DUAL_BANK_UPDATA。priv_param_fill_hdl:私有参数填充回调,可为 NULL。priv_update_jump_handle:跳转前处理回调,可为 NULL。
int app_update_init(void) / void app_update_handle(int msg)
升级模块初始化与消息处理。app_update_init 注册升级进入回调(update_enter_cb_register)并初始化各通道;app_update_handle 处理应用层升级消息(如 OTA 命令、按键触发),驱动状态机从 UPDATE_TASK_INIT 流转。
int update_result_deal(void) / void update_result_set(u16 result) / void update_clear_result(void)
升级结果消费与写入。update_result_deal 在开机早期调用,读取 loader 回写的 parm_result 并执行对应处理(成功恢复 VM、失败清理标志);update_result_set 供上层写入结果;update_clear_result 清空标志。
bool update_success_boot_check(void) / bool device_is_first_start(void)
启动检查。update_success_boot_check 判断本次启动是否为「升级成功后的首次启动」;device_is_first_start 结合 DEVICE_FIRST_START 位判断设备是否首次上电。
bool vm_need_recover(void)
根据 g_updata_flag 低 16 位是否等于 UPDATA_SUCC 判断是否需要恢复用户 VM 区数据,由启动流程调用。
u16 dev_update_check(char *logo, bool check_flag) / u16 dev_update_by_change_stack(update_mode_info_t *p_info)
设备级接口。dev_update_check 校验升级文件 logo;dev_update_by_change_stack 切换到专用栈执行升级写入。
返回: UPDATA_RESULT 值,UPDATA_SUCC 表示成功。
故障模式、边界情况与并发
升级结果错误码
loader 通过 parm_result 回写 UPDATA_RESULT:
| 结果 | 含义 | 处理 |
|---|---|---|
UPDATA_NON (0x5A00) | 未发生升级 | 正常启动路径 |
UPDATA_READY | 升级已就绪 | 等待跳转 |
UPDATA_SUCC | 升级成功 | vm_need_recover() 恢复用户配置并上报成功 |
UPDATA_PARM_ERR | 参数错误(CRC/magic 校验失败) | 清除标志,不恢复 VM |
UPDATA_DEV_ERR | 设备错误(Flash 写入失败) | 清除标志,保留现场以便重试 |
UPDATA_KEY_ERR | 密钥/签名校验失败 | 与 DEVICE_UPDATE_KEY_ERR 位联动标记 |
关键防御机制
- CRC 双重保护:
parm_crc保护整个参数段;ext_arg_crc单独保护扩展参数块。任何一处在 loader 侧校验失败都会以UPDATA_PARM_ERR拒绝升级,避免把损坏参数写入 Flash。 - 魔法数防误判:
UPDATA_MAGIC = 0x5A00刻意选择非零值(注释明言「防止 CRC == 0 的情况」);参数段另有0x5441魔法数,双魔法数互为备份。 - 短文件名约束:升级文件必须是 8+3 短文件名、最多两层目录,违反则 loader 找不到文件,表现为
UPDATA_DEV_ERR。 - 首次启动位:
DEVICE_FIRST_START位用于区分「正常首次开机」与「升级后重启」,避免首次开机误触发 VM 恢复逻辑。
并发与中断考量
ota_status声明为volatile u8,会被协议栈(BLE/SPP OTA 回调)异步修改;主升级流程读取时须容忍中间态。- 跳转 loader 前通过 update_target 的
driver_close依次销毁蓝牙控制器(hci_controller_destory)、关闭 HCI(ll_hci_destory)、关闭全部中断(hwi_all_close)、解除 RAM 保护(ram_protect_close)——顺序很重要:先停协议栈再关中断,避免在中断仍开启时协议栈还在访问将被 loader 覆盖的内存。 dev_update_by_change_stack切换栈执行,规避主任务栈深度不足导致升级写 Flash 时栈溢出。
升级中断/掉电
升级期间掉电的恢复策略依赖两段式设计:UPDATA_PARM 常驻固定 Flash 地址,loader 侧有独立的写入进度保护;SDK 启动时依据 parm_result 与 update_success_boot_check() 判断是「升级成功重启」还是「升级中断重启」,后者不再触发 VM 恢复,保证用户区不被半成品数据污染。
扩展点与自定义通道开发
新增一种升级来源(如自定义私有协议)的标准步骤:
- 追加枚举值:在
UPDATA_TYPE的USER_NORFLASH_UFW_UPDATA之前插入新值(顺序不可打乱,兼容老 loader)。 - 注册升级目标:实现
struct update_target(name+driver_close),用REGISTER_UPDATE_TARGET放入.update_target段;若无需清理驱动,driver_close可为空实现。 - 实现文件获取逻辑:参考现有通道(
uart_update.c、msd_upgrade.c、toy_update.c),在app_update_handle的消息分发中接入;升级文件路径遵循短文件名约束。 - 填充参数:通过
update_param_priv_fill()写入私有数据(如通道 IO、波特率),需要跨阶段携带的附加信息走ext_arg_t扩展参数(新增EXT_ARG_TYPE枚举值)。 - 调用
update_mode_api_v2(new_type, fill_hdl, jump_hdl)完成参数组装与跳转。
扩展参数机制(EXT_ARG_TYPE)是官方预留的扩展点:EXT_MUTIL_UPDATE_NAME(多升级文件名)、EXT_USER_API_BIN_INFO(用户 API 固件信息)、EXT_NEW_SDK_UPD_AGAIN(新 SDK 重复升级)、EXT_NEW_FILENAME(新文件名)等枚举注释表明 SDK 持续在通过该通道演进升级能力,无需改动 UPDATA_PARM 主结构。
测试与相关实现
sdk/app/bsp/common/update/dev_update.c:设备级升级执行器,dev_update_check文件校验 +dev_update_by_change_stack栈切换写入,是update.c的下游核心。sdk/app/bsp/common/uart_update/uart_update.c/.h:UART 升级通道实现(含UPDATA_UART参数结构:tx/rx IO、波特率、超时)。sdk/app/bsp/common/usb/device/msd_upgrade.c:USB 大容量存储升级通道。sdk/app/src/voice_func/toy_update/toy_update.c/.h:玩具类应用的升级逻辑(BLE/SPP 通道的典型实现)。sdk/include_lib/update/code_v2/update_loader_download.h:loader 下载相关接口声明。- 编译产物
sdk/include_lib/liba/sh58/voice_enhanced/update_lib.a、voice_toy/update_lib.a:预编译升级库,说明该机制以静态库形式发布给上层应用。
升级机制的核心正确性依赖「参数段地址固定」这一硬件布局约定:UPDATA_BEG/UPDATA_SIZE 由链接脚本定义,UPDATA_FLAG_ADDR 位于其偏移 0x08 处(前 8 字节为 BOOT_STATUS_ADDR 预留)。任何修改链接脚本或 Flash 分区布局的行为都可能破坏 SDK 与 loader 的参数契约,属于高风险改动,需同步修改 loader 侧代码。