全链路 OTA 升级系统拆解:从固件格式到双芯片全流程落地实现
在智能商用设备领域,OTA(Over-The-Air)远程升级是产品生命周期管理的核心能力。这类「Linux 核心板 + MCU 电控板」的双芯片架构设备,OTA 升级不仅要解决 Linux 系统的安全更新,还要兼顾底层电控 MCU 的可靠升级,同时要保证升级过程防变砖、数据不丢失、用户体验顺滑。
本文将从零到一完整拆解一套 OTA 升级系统,涵盖固件包格式设计、多模块解耦架构、云端固件下载、Linux AB 双分区升级、MCU 串口升级全流程、可靠通信协议、容错安全机制等全链路内容。
目录
[1.1 硬件架构基础](#1.1 硬件架构基础)
[1.2 软件模块拆分](#1.2 软件模块拆分)
[1.3 OTA 全流程时序总览](#1.3 OTA 全流程时序总览)
[2.1 固件包整体结构](#2.1 固件包整体结构)
[2.2 固件包基本信息(64 字节)](#2.2 固件包基本信息(64 字节))
[2.3 固件包描述信息(128 字节)](#2.3 固件包描述信息(128 字节))
[2.4 子固件包描述信息(单条 128 字节)](#2.4 子固件包描述信息(单条 128 字节))
[2.5 子固件包内容](#2.5 子固件包内容)
[3.1 模块初始化与系统版本同步](#3.1 模块初始化与系统版本同步)
[3.2 开机自检与本地固件兜底机制](#3.2 开机自检与本地固件兜底机制)
[3.3 云端版本查询与升级决策](#3.3 云端版本查询与升级决策)
[3.4 分片下载与进度管理](#3.4 分片下载与进度管理)
[四、OTA 核心模块:双芯片升级的调度核心](#四、OTA 核心模块:双芯片升级的调度核心)
[4.1 升级触发与固件预解析](#4.1 升级触发与固件预解析)
[4.2 核心板固件提取与解压实现](#4.2 核心板固件提取与解压实现)
[4.2.1 子固件提取](#4.2.1 子固件提取)
[4.2.2 固件解压到备用分区](#4.2.2 固件解压到备用分区)
[4.3 MCU 电控板升级的状态机设计](#4.3 MCU 电控板升级的状态机设计)
[4.3.1 状态机定义](#4.3.1 状态机定义)
[4.3.2 MCU 升级控制线程](#4.3.2 MCU 升级控制线程)
[4.3.3 MCU 升级消息交互处理](#4.3.3 MCU 升级消息交互处理)
[4.3.4 固件分片读取实现](#4.3.4 固件分片读取实现)
[4.4 分区切换的原子性实现](#4.4 分区切换的原子性实现)
[五、通信模块:UART 可靠传输协议设计与实现](#五、通信模块:UART 可靠传输协议设计与实现)
[5.1 UART 帧协议格式](#5.1 UART 帧协议格式)
[5.2 发送帧的封装实现](#5.2 发送帧的封装实现)
[5.3 接收帧的状态机解析](#5.3 接收帧的状态机解析)
[5.4 消息分发与 OTA 指令透传](#5.4 消息分发与 OTA 指令透传)
[6.1 权重化进度计算](#6.1 权重化进度计算)
[6.2 关键进度更新节点](#6.2 关键进度更新节点)
[6.3 进度上报实现](#6.3 进度上报实现)
[7.1 全链路校验机制](#7.1 全链路校验机制)
[7.2 防变砖设计](#7.2 防变砖设计)
[7.3 异常容错处理](#7.3 异常容错处理)
[7.4 安全防护设计](#7.4 安全防护设计)
一、系统整体架构总览
1.1 硬件架构基础
本系统的硬件载体为双芯片架构的智能咖啡机:
- 核心板:ARM 架构处理器,运行嵌入式 Linux 系统,负责网络通信、UI 交互、OTA 全流程调度、固件解析与校验,是 OTA 系统的主控核心。
- 电控板:MCU 微控制器,负责咖啡机的电机驱动、加热控制、传感器采集等底层实时控制,通过 UART 串口与核心板通信,接收 OTA 升级指令与固件数据。
1.2 软件模块拆分
本 OTA 系统采用模块化 + 消息总线的解耦设计,分为 5 大核心模块,模块间通过基于发布 - 订阅模式的消息总线通信,无直接耦合,便于维护与扩展:
| 模块名称 | 核心职责 |
|---|---|
| 网络模块 | 云端 SDK 对接、版本查询、固件分片下载、本地固件自检、升级状态云端上报 |
| OTA 核心模块 | 固件包解析校验、Linux 核心板 AB 分区升级调度、MCU 升级状态机管理、升级进度计算、分区切换控制 |
| 通信模块 | UART 串口底层驱动、可靠帧协议封装与解析、核心板与 MCU 的消息透传、OTA 指令双向转发 |
| 消息总线 | 跨线程消息通信核心,为各模块提供独立的消息队列,实现模块间的异步解耦通信 |
| Task 主模块 | UI 交互、用户升级确认、升级进度展示、设备重启控制、系统版本管理 |
1.3 OTA 全流程时序总览

二、固件包格式详解与代码实现
固件包是 OTA 升级的载体,本系统设计了分层、可扩展、高安全的统一固件包格式,支持核心板、电控板等多子固件共存,一套固件包完成整机全模块升级,避免多包分发的混乱。
2.1 固件包整体结构
固件包采用「头部信息 + 描述信息 + 子固件索引 + 子固件内容」的四层结构,整体布局如下:
┌──────────────────────────────────────┐
│ 固件包基本信息(固定64字节) 第一层:整包基础信息与校验
├──────────────────────────────────────┤
│ 固件包描述信息(固定128字节) 第二层:整机产品与版本信息
├──────────────────────────────────────┤
│ 子固件包描述信息1(固定128字节)
│ 子固件包描述信息2(固定128字节) 第三层:子固件索引表,支持N个
│ ...(最多支持65535个子固件)
├──────────────────────────────────────┤
│ 子固件包内容1(32字节对齐)
│ 子固件包内容2 第四层:实际固件二进制数据
│ ...
└──────────────────────────────────────┘
设计核心考量:
- 所有头部字段固定长度,解析逻辑简单,避免变长字段带来的解析风险;
- 采用大端序(网络字节序)存储所有多字节字段,兼容 Linux 小端与 MCU 大端平台,避免字节序错乱;
- 分层 CRC+SHA256 双校验,从整包到子固件层层校验,确保固件完整性与防篡改;
- 子固件独立索引,可按需提取对应模块的固件,支持整机升级与单模块升级。
所有结构定义均在drv_ota.h头文件中,与固件包格式一一对应,解析代码在drv_ota.c中实现。
2.2 固件包基本信息(64 字节)
该部分为固件包的最头部,存储整包的基础信息与校验值,是解析固件包的入口,结构体定义与解析代码如下:
| 字段 | 长度 (字节) | 代码对应字段 | 字段说明 |
|---|---|---|---|
| 包格式类型 | 2 | pkg_format_type | 固件包格式标识,用于区分不同产品系列 |
| 包格式版本号 | 2 | pkg_format_version | 固件包格式的版本,用于向前兼容 |
| 整包 CRC 校验码 | 2 | pkg_crc | 整个固件包的 CRC16 校验值,用于快速校验整包完整性 |
| 整包摘要信息 | 32 | pkg_digest | 整个固件包的 SHA256 哈希值,用于防篡改安全校验 |
| 固件包描述信息地址 | 4 | desc_offset | 第二层描述信息在固件包中的绝对偏移地址 |
| 固件包描述信息长度 | 4 | desc_size | 描述信息的字节长度 |
| 固件包描述信息 CRC | 2 | desc_crc | 描述信息的 CRC16 校验值 |
| 预留字段 | 12 | reserved | 预留扩展位,固定填充 0 |
| 固件包内容长度 | 4 | pkg_content_size | 整个固件包的总字节长度 |
解析代码实现:
// drv_ota.c
/**
* @brief 从固件文件中解析固件包基本信息
* @param fw_path 固件包文件路径
* @param pkg_info 解析后的基本信息结构体输出
* @return 0成功,-1失败
*/
int drv_ota_parse_pkg_info_from_file(const uint8_t *fw_path, drv_ota_pkg_info_t *pkg_info)
{
if (fw_path == NULL || pkg_info == NULL) return -1;
int fd = open(fw_path, O_RDONLY);
if (fd < 0) {
printf("[OTA-PKG] open firmware file failed, path: %s\n", fw_path);
return -1;
}
// 读取固定64字节的头部信息
uint8_t buf[64] = {0};
ssize_t read_len = read(fd, buf, sizeof(buf));
if (read_len != sizeof(buf)) {
printf("[OTA-PKG] read pkg head failed, expect %zu, actual %zd\n", sizeof(buf), read_len);
close(fd);
return -1;
}
close(fd);
// 按网络字节序(大端)解析各字段,兼容跨平台字节序差异
pkg_info->pkg_format_type = be16_to_cpu(buf[0], buf[1]);
pkg_info->pkg_format_version = be16_to_cpu(buf[2], buf[3]);
pkg_info->pkg_crc = be16_to_cpu(buf[4], buf[5]);
memcpy(pkg_info->pkg_digest, &buf[6], 32);
pkg_info->desc_offset = be32_to_cpu(&buf[38]);
pkg_info->desc_size = be32_to_cpu(&buf[42]);
pkg_info->desc_crc = be16_to_cpu(buf[46], buf[47]);
memcpy(pkg_info->reserved, &buf[48], 12);
pkg_info->pkg_content_size = be32_to_cpu(&buf[60]);
return 0;
}
2.3 固件包描述信息(128 字节)
该部分存储整机产品的基础信息,用于固件与设备的匹配校验,避免刷错机型、错版本,结构体定义与解析代码如下:
| 字段 | 长度 (字节) | 代码对应字段 | 字段说明 |
|---|---|---|---|
| 整机固件类型 | 20 | machine_fw_type | 整机产品型号标识,如 "COFFEE-MACHINE-PRO" |
| 预留 | 3 | reserved | 预留扩展 |
| 产品标识类型 | 1 | product_id_type | 产品 ID 校验类型 |
| 产品标识摘要 | 16 | product_id_digest | 产品硬件 ID 的哈希值,确保固件与硬件匹配 |
| 整机固件版本号 | 14 | machine_fw_version | 整机语义化版本号,如 "V1.2.3" |
| 整机自定义信息 | 32 | custom_info | 自定义扩展字段,可存储升级文案、发布时间等 |
| 预留 | 30 | reserved2 | 预留扩展 |
| 子固件描述 CRC | 2 | sub_fw_desc_crc | 第三层子固件描述表的 CRC16 校验值 |
| 子固件描述起始地址 | 4 | sub_fw_desc_offset | 子固件描述表在固件包中的绝对偏移 |
| 子固件描述长度 | 4 | sub_fw_desc_size | 子固件描述表的总字节长度 |
| 子固件包数量 | 2 | sub_fw_count | 子固件的个数,决定描述表的长度 |
解析代码实现:
// drv_ota.c
/**
* @brief 从固件文件中解析固件包描述信息
* @param fw_path 固件包文件路径
* @param desc_offset 描述信息的偏移地址(从基本信息中获取)
* @param pkg_desc_info 解析后的描述信息结构体输出
* @return 0成功,-1失败
*/
int drv_ota_parse_pkg_desc_info_from_file(const uint8_t *fw_path, uint32_t desc_offset, drv_ota_pkg_desc_info_t *pkg_desc_info)
{
if (fw_path == NULL || pkg_desc_info == NULL) return -1;
int fd = open(fw_path, O_RDONLY);
if (fd < 0) {
printf("[OTA-PKG] open firmware file failed\n");
return -1;
}
// 跳转到描述信息的起始偏移
off_t seek_ret = lseek(fd, desc_offset, SEEK_SET);
if (seek_ret != desc_offset) {
printf("[OTA-PKG] lseek desc offset failed\n");
close(fd);
return -1;
}
// 读取固定128字节的描述信息
uint8_t buf[128] = {0};
ssize_t read_len = read(fd, buf, sizeof(buf));
if (read_len != sizeof(buf)) {
printf("[OTA-PKG] read pkg desc failed\n");
close(fd);
return -1;
}
close(fd);
// 逐字段解析
memcpy(pkg_desc_info->machine_fw_type, &buf[0], 20);
memcpy(pkg_desc_info->reserved, &buf[20], 3);
pkg_desc_info->product_id_type = buf[23];
memcpy(pkg_desc_info->product_id_digest, &buf[24], 16);
memcpy(pkg_desc_info->machine_fw_version, &buf[40], 14);
memcpy(pkg_desc_info->custom_info, &buf[54], 32);
memcpy(pkg_desc_info->reserved2, &buf[86], 30);
pkg_desc_info->sub_fw_desc_crc = be16_to_cpu(buf[116], buf[117]);
pkg_desc_info->sub_fw_desc_offset = be32_to_cpu(&buf[118]);
pkg_desc_info->sub_fw_desc_size = be32_to_cpu(&buf[122]);
pkg_desc_info->sub_fw_count = be16_to_cpu(buf[126], buf[127]);
return 0;
}
2.4 子固件包描述信息(单条 128 字节)
该部分是子固件的索引表,每个子固件对应一条 128 字节的描述信息,记录子固件的类型、版本、存储位置、校验值等核心信息,可根据子固件类型快速定位对应固件。
| 字段 | 长度 (字节) | 代码对应字段 | 字段说明 |
|---|---|---|---|
| 子固件包类型 | 2 | sub_fw_type | 子固件类型标识,如 0x0001 = 核心板固件,0x0002 = 电控板固件 |
| 子固件软件标识 | 20 | sub_fw_sw_id | 子固件的软件名称标识 |
| 子固件版本号 | 14 | sub_fw_version | 子固件的独立版本号 |
| 子固件包地址 | 4 | sub_fw_offset | 子固件内容在整包中的绝对偏移地址 |
| 子固件包长度 | 4 | sub_fw_size | 子固件的有效字节长度 |
| 子固件 CRC 校验码 | 2 | sub_fw_crc | 子固件内容的 CRC16 校验值 |
| 子固件摘要信息 | 32 | sub_fw_digest | 子固件内容的 SHA256 哈希值 |
| 子固件自定义信息 | 32 | sub_fw_custom_info | 自定义扩展字段 |
| 预留 | 18 | reserved | 预留扩展 |
按类型查找子固件的解析代码实现:
// drv_ota.c
/**
* @brief 按子固件类型,从固件包中解析对应的子固件描述信息
* @param fw_path 固件包文件路径
* @param sub_fw_desc_offset 子固件描述表的起始偏移
* @param sub_fw_count 子固件总个数
* @param target_type 目标子固件类型
* @param sub_fw_desc 解析后的子固件描述信息输出
* @return 0成功,-1未找到/失败
*/
int drv_ota_parse_sub_fw_desc_by_type_from_file(const char *fw_path, uint32_t sub_fw_desc_offset,
uint16_t sub_fw_count, uint16_t target_type,
drv_ota_sub_fw_desc_t *sub_fw_desc)
{
if (fw_path == NULL || sub_fw_desc == NULL || sub_fw_count == 0) return -1;
int fd = open(fw_path, O_RDONLY);
if (fd < 0) {
printf("[OTA-PKG] open firmware file failed\n");
return -1;
}
uint8_t buf[128] = {0};
// 遍历所有子固件描述,匹配目标类型
for (uint16_t i = 0; i < sub_fw_count; i++) {
// 计算当前子固件描述的绝对偏移
uint32_t abs_offset = sub_fw_desc_offset + i * 128;
off_t seek_ret = lseek(fd, abs_offset, SEEK_SET);
if (seek_ret != abs_offset) {
printf("[OTA-PKG] lseek sub desc offset failed, index: %d\n", i);
continue;
}
ssize_t read_len = read(fd, buf, 128);
if (read_len != 128) {
printf("[OTA-PKG] read sub desc failed, index: %d\n", i);
continue;
}
// 匹配子固件类型
uint16_t type = be16_to_cpu(buf[0], buf[1]);
if (type == target_type) {
// 解析填充结构体
sub_fw_desc->sub_fw_type = type;
memcpy(sub_fw_desc->sub_fw_sw_id, &buf[2], 20);
memcpy(sub_fw_desc->sub_fw_version, &buf[22], 14);
sub_fw_desc->sub_fw_offset = be32_to_cpu(&buf[36]);
sub_fw_desc->sub_fw_size = be32_to_cpu(&buf[40]);
sub_fw_desc->sub_fw_crc = be16_to_cpu(buf[44], buf[45]);
memcpy(sub_fw_desc->sub_fw_digest, &buf[46], 32);
memcpy(sub_fw_desc->sub_fw_custom_info, &buf[78], 32);
memcpy(sub_fw_desc->reserved, &buf[110], 18);
close(fd);
return 0;
}
}
close(fd);
printf("[OTA-PKG] target sub firmware type not found, type: 0x%04X\n", target_type);
return -1;
}
2.5 子固件包内容
该部分存储子固件的实际二进制数据,按 32 字节对齐存储 ,适配 Flash 存储的块大小要求,避免 MCU 写入 Flash 时出现对齐错误。提取时仅读取sub_fw_size指定的有效长度,忽略对齐填充的字节。
三、网络模块:云端对接与固件全生命周期管理
网络模块是 OTA 系统与云端交互的入口,核心职责是版本管理、固件可靠下载、本地固件兜底、升级状态上报 ,代码实现在network_module.c中,所有网络操作均在独立线程中执行,不阻塞 UI 与主线程。

3.1 模块初始化与系统版本同步
模块初始化的第一步,是与 Task 模块同步当前设备的系统版本号,这是版本比较、升级决策的基础。
设计考量:系统版本号由 Task 模块(UI 层)统一维护,与用户界面展示的版本完全一致,避免多模块维护版本号导致的不一致,杜绝误升级问题。采用「互斥锁 + 条件变量」的线程同步机制,设置 5 秒超时,避免死等导致的线程阻塞。
核心代码实现:
// 全局版本管理变量
static pthread_mutex_t g_version_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_version_cond = PTHREAD_COND_INITIALIZER;
static NetworkVersionData g_sys_ver = {0};
static uint8_t g_sys_version_received = 0;
/**
* @brief SDK初始化,完成版本同步与回调注册
*/
int sdk_init(void)
{
// 1. 向Task模块发送版本查询请求,通过消息总线通信
ota_send_version_query_to_task();
// 2. 等待Task模块返回版本,设置5秒超时,避免死锁
struct timespec timeout = {0};
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += 5;
pthread_mutex_lock(&g_version_mutex);
int ret = pthread_cond_timedwait(&g_version_cond, &g_version_mutex, &timeout);
if (ret != 0) {
printf("[OTA-NET] wait system version timeout\n");
pthread_mutex_unlock(&g_version_mutex);
return -1;
}
pthread_mutex_unlock(&g_version_mutex);
// 3. 注册云端OTA事件回调
uhsd_evt_register(UHSD_OTA_EVT_BASE, ota_download_event_handler, NULL);
printf("[OTA-NET] sdk init success, current version: %s\n", g_sys_ver.data);
return 0;
}
/**
* @brief 处理Task模块返回的版本查询响应
*/
static void NetworkMsgHandle(Message_t *msg)
{
switch (msg->msg_id) {
case NETWORK_MSG_VERSION_QUERY_RSP:
{
NetworkVersionQueryRsp rsp = {0};
memcpy(&rsp, msg->payload, sizeof(rsp));
pthread_mutex_lock(&g_version_mutex);
g_sys_ver = rsp.ver_data;
g_sys_version_received = 1;
// 通知等待线程版本已获取
pthread_cond_signal(&g_version_cond);
pthread_mutex_unlock(&g_version_mutex);
printf("[OTA-NET] received system version: %s\n", g_sys_ver.data);
}
break;
// 其他消息处理...
}
}
3.2 开机自检与本地固件兜底机制
SDK 初始化完成后,会立即执行开机自检逻辑,这是提升系统容错能力的核心设计。
设计考量:设备在升级过程中可能出现意外断电、网络中断等异常,导致固件下载一半、升级流程中断。开机自检会自动检测本地存储的固件包,校验其合法性与版本,若存在有效高版本固件,直接进入升级流程,无需用户重新操作、重新下载,极大提升用户体验与异常恢复能力。
核心代码实现:
// 固件包存储路径
#define OTA_DOWNLOAD_PATH "/customer/firmware.bin"
/**
* @brief 开机OTA自检,处理异常中断的升级流程
*/
static void ota_self_check_after_boot(void)
{
// 1. 检查本地是否存在固件包
if (ota_check_local_firm_exist() != 0) {
printf("[OTA-NET] no local firmware found, query cloud\n");
uhsd_ota_query_firmware();
return;
}
// 2. 读取本地固件的版本号
NetworkVersionData local_firm_ver = {0};
if (drv_ota_get_firmware_version(OTA_DOWNLOAD_PATH, &local_firm_ver) != 0) {
printf("[OTA-NET] read local firmware version failed, delete file\n");
unlink(OTA_DOWNLOAD_PATH);
uhsd_ota_query_firmware();
return;
}
// 3. 重新同步当前系统版本,确保版本最新
g_sys_version_received = 0;
ota_send_version_query_to_task();
struct timespec timeout = {0};
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += 5;
pthread_mutex_lock(&g_version_mutex);
pthread_cond_timedwait(&g_version_cond, &g_version_mutex, &timeout);
pthread_mutex_unlock(&g_version_mutex);
if (!g_sys_version_received) {
printf("[OTA-NET] get system version failed\n");
unlink(OTA_DOWNLOAD_PATH);
uhsd_ota_query_firmware();
return;
}
// 4. 版本比较,判断是否需要升级
VersionCompareResult cmp = ota_compare_version(&local_firm_ver, &g_sys_ver);
if (cmp == VERSION_LOCAL_NEWER) {
// 本地版本更新,执行SHA256全量校验,防止固件损坏/篡改
if (drv_ota_verify_bin_sha256_stream(OTA_DOWNLOAD_PATH) == 0) {
printf("[OTA-NET] local firmware verify success, ready to upgrade\n");
// 通知Task模块固件就绪,等待用户确认升级
MessageBusPublishToTask(NULL, 0, NETWORK_OTA_RESOURCE_READY);
return;
} else {
printf("[OTA-NET] local firmware verify failed, delete file\n");
unlink(OTA_DOWNLOAD_PATH);
}
} else if (cmp == VERSION_SAME) {
// 版本一致,无需升级,保留固件包备用
printf("[OTA-NET] local firmware version same, no need upgrade\n");
return;
} else {
// 本地版本更旧,删除无效固件
printf("[OTA-NET] local firmware version older, delete file\n");
unlink(OTA_DOWNLOAD_PATH);
}
// 本地无有效固件,查询云端
uhsd_ota_query_firmware();
}
3.3 云端版本查询与升级决策
本地无有效固件时,会调用云端 SDK 接口查询最新固件,通过回调处理查询结果,做出升级决策。
核心代码实现:
// 全局云端固件信息
static uhsd_ota_version_qry_ack_t g_ota_firm_info = {0};
static uint8_t g_has_ota_firm = 0;
/**
* @brief 云端版本查询回调
*/
static void ota_version_qry_callback(uhsd_s32 err_num, uhsd_ota_version_qry_ack_t *qry_ack, uhsd_void *user_data)
{
// 1. 异常处理,无可用升级固件
if (err_num != 0 || qry_ack == NULL) {
printf("[OTA-NET] cloud query failed, no available firmware, err: %d\n", err_num);
return;
}
// 2. 保存云端固件信息
g_has_ota_firm = 1;
g_ota_firm_info = *qry_ack;
printf("[OTA-NET] cloud firmware found, version: %s, size: %d\n",
qry_ack->whole_version, qry_ack->firm_info.size);
// 3. 保存升级文案到本地文件,供UI展示更新内容
save_ota_text_to_file(qry_ack->desc, qry_ack->desc_len, NULL, 0);
MessageBusPublishToTask(OTA_TEXT_FILE_PATH, strlen(OTA_TEXT_FILE_PATH)+1, NETWORK_MSG_TASK_OTA_TEXT_READY);
// 4. 等待系统版本同步完成
if (!g_sys_version_received) {
struct timespec timeout = {0};
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += 5;
pthread_mutex_lock(&g_version_mutex);
pthread_cond_timedwait(&g_version_cond, &g_version_mutex, &timeout);
pthread_mutex_unlock(&g_version_mutex);
}
// 5. 版本比较,决策是否下载
if (g_sys_ver.len > 0) {
NetworkVersionData cloud_ver = {0};
cloud_ver.len = strlen((char*)qry_ack->whole_version);
memcpy(cloud_ver.data, qry_ack->whole_version, cloud_ver.len);
// 仅当云端版本更新时,才触发下载
if (ota_compare_version(&g_sys_ver, &cloud_ver) != VERSION_LOCAL_NEWER) {
printf("[OTA-NET] cloud version newer, start download\n");
uhsd_ota_download_firmware();
return;
} else {
printf("[OTA-NET] local version is newest, no need download\n");
return;
}
}
// 未获取到系统版本时,兜底直接下载
uhsd_ota_download_firmware();
}
// 触发云端查询
uhsd_ota_query_firmware(ota_version_qry_callback, NULL);
3.4 分片下载与进度管理
固件下载采用分片下载 + 流式写入的方案,适配大体积固件包(核心板固件通常几百 MB),避免内存占用过高,同时支持网络异常后的断点续传。
设计考量:
- 分片写入:每下载一片固件数据,立即写入文件,不缓存到内存,降低内存占用;
- 进度防抖:每下载进度提升 5% 才上报一次,避免频繁发送消息导致消息总线拥堵、UI 卡顿;
- 异常处理:下载过程中出现网络异常,SDK 会自动重试,重试失败后清理无效文件;
- 下载完成后,立即执行 SHA256 全量校验,确保固件完整、未被篡改。
核心代码实现:
static FILE *g_ota_download_file = NULL;
static uint32_t firm_original_total = 0;
static float last_report_progress = 0.0f;
static char *upgrade_sn = NULL;
/**
* @brief 固件分片写入回调,SDK每下载完一片数据,会调用此回调写入文件
*/
static uhsd_s32 ota_download_firmware_write(uhsd_u32 offset, uhsd_u8 *buf, uhsd_u32 len)
{
if (g_ota_download_file == NULL || buf == NULL || len == 0) {
return -1;
}
// 计算有效长度,最后一包可能存在对齐补0,仅写入有效数据
uhsd_u32 valid_len = len;
if (offset + len > firm_original_total) {
valid_len = firm_original_total - offset;
}
// 跳转到对应偏移,写入文件
fseek(g_ota_download_file, offset, SEEK_SET);
size_t write_len = fwrite(buf, 1, valid_len, g_ota_download_file);
if (write_len != valid_len) {
printf("[OTA-NET] write firmware failed, offset: %u, expect: %u, actual: %zu\n",
offset, valid_len, write_len);
return -1;
}
// 进度上报,防抖处理:仅当进度提升超过5%时才上报
float current_progress = (float)(offset + valid_len) / firm_original_total * 100;
if (current_progress - last_report_progress >= 5.0f) {
printf("[OTA-NET] download progress: %.1f%%\n", current_progress);
// 上报下载状态到云端
uhsd_ota_upgrade_status_rpt(upgrade_sn, UHSD_OTA_UPGRADE_STATUS_IDLE_DOWNLOADING, UHSD_FALSE);
last_report_progress = current_progress;
}
return valid_len;
}
/**
* @brief 启动固件下载
*/
int uhsd_ota_download_firmware(void)
{
if (!g_has_ota_firm) {
printf("[OTA-NET] no ota firmware info\n");
return -1;
}
// 初始化下载参数
firm_original_total = g_ota_firm_info.firm_info.size;
upgrade_sn = g_ota_firm_info.firm_info.upgrade_sn;
last_report_progress = 0.0f;
// 设置SDK下载操作回调
uhsd_ota_operation_t ota_op = {0};
ota_op.firmware_write = ota_download_firmware_write;
ota_op.firmware_read = ota_firmware_read;
uhsd_ota_operation_set(ota_op, NULL);
// 通知SDK开始下载
return uhsd_ota_notify_start(upgrade_sn);
}
/**
* @brief OTA下载事件处理回调
*/
static uhsd_s32 ota_download_event_handler(const uhsd_evt_t *evt, void *user_data)
{
switch (evt->evt) {
case UHSD_OTA_EVT_FIRM_READY:
// 固件准备就绪,创建文件,准备接收分片数据
printf("[OTA-NET] firmware ready, start download\n");
g_ota_download_file = fopen(OTA_DOWNLOAD_PATH, "wb+");
if (g_ota_download_file == NULL) {
printf("[OTA-NET] create firmware file failed\n");
return -1;
}
// 请求从偏移0开始下载整个固件
uhsd_ota_download_section_firm(upgrade_sn, 0, firm_original_total);
break;
case UHSD_OTA_EVT_SECTION_FIRM_DOWNLOAD_FINISH:
// 全部分片下载完成
printf("[OTA-NET] firmware download finish\n");
if (g_ota_download_file != NULL) {
fclose(g_ota_download_file);
g_ota_download_file = NULL;
}
// 下载完成后,执行SHA256全量校验
if (drv_ota_verify_bin_sha256_stream(OTA_DOWNLOAD_PATH) == 0) {
printf("[OTA-NET] firmware verify success\n");
// 通知Task模块固件就绪
MessageBusPublishToTask(NULL, 0, NETWORK_OTA_RESOURCE_READY);
} else {
printf("[OTA-NET] firmware verify failed, delete file\n");
unlink(OTA_DOWNLOAD_PATH);
}
break;
case UHSD_OTA_EVT_EXCEPTION_OCCURED:
// 下载异常处理
printf("[OTA-NET] download exception occurred\n");
if (g_ota_download_file != NULL) {
fclose(g_ota_download_file);
g_ota_download_file = NULL;
}
unlink(OTA_DOWNLOAD_PATH);
break;
}
return 0;
}
四、OTA 核心模块:双芯片升级的调度核心
OTA 核心模块是整个 OTA 系统的大脑,代码实现在ota_module.c中,核心职责是固件解析校验、核心板 AB 分区升级、MCU 升级状态机调度、进度管理、分区切换控制,所有升级逻辑均在独立线程中执行,保证升级过程的稳定性。
4.1 升级触发与固件预解析
用户在 UI 确认升级后,Task 模块会通过消息总线发送OTA_START指令,OTA 模块收到指令后,立即启动升级流程,第一步是固件包的全量解析与预校验。
核心代码实现:
// 子固件类型定义
#define FIRMWARE_TYPE_CORE 0x0001 // 核心板固件
#define FIRMWARE_TYPE_CONTROL 0x0002 // 电控板MCU固件
// 全局升级状态变量
static drv_ota_pkg_info_t g_pkg_info = {0};
static drv_ota_pkg_desc_info_t g_pkg_desc_info = {0};
static drv_ota_sub_fw_desc_t g_core_fw_info = {0};
static drv_ota_sub_fw_desc_t g_control_fw_info = {0};
static uint8_t g_ota_progress = 0;
static pthread_t g_ota_control_thread;
static uint8_t g_ota_control_thread_exit_flag = 0;
/**
* @brief OTA模块消息处理入口
*/
static void Ota_HandleMessage(Message_t *msg)
{
switch (msg->msg_id) {
case OTA_START:
{
printf("[OTA-CORE] receive OTA start command\n");
const char *filename = OTA_DOWNLOAD_PATH;
int ret = 0;
// 1. 解析固件包基本信息
ret = drv_ota_parse_pkg_info_from_file(filename, &g_pkg_info);
if (ret != 0) {
printf("[OTA-CORE] parse pkg info failed\n");
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
break;
}
// 2. 解析固件包描述信息
ret = drv_ota_parse_pkg_desc_info_from_file(filename, g_pkg_info.desc_offset, &g_pkg_desc_info);
if (ret != 0) {
printf("[OTA-CORE] parse pkg desc failed\n");
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
break;
}
// 3. 提取核心板子固件描述信息
ret = drv_ota_parse_sub_fw_desc_by_type_from_file(filename,
g_pkg_desc_info.sub_fw_desc_offset,
g_pkg_desc_info.sub_fw_count,
FIRMWARE_TYPE_CORE,
&g_core_fw_info);
if (ret != 0) {
printf("[OTA-CORE] parse core firmware desc failed\n");
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
break;
}
// 4. 提取电控板子固件描述信息
ret = drv_ota_parse_sub_fw_desc_by_type_from_file(filename,
g_pkg_desc_info.sub_fw_desc_offset,
g_pkg_desc_info.sub_fw_count,
FIRMWARE_TYPE_CONTROL,
&g_control_fw_info);
if (ret != 0) {
printf("[OTA-CORE] parse control firmware desc failed\n");
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
break;
}
printf("[OTA-CORE] firmware parse success, core version: %s, mcu version: %s\n",
g_core_fw_info.sub_fw_version, g_control_fw_info.sub_fw_version);
// 5. 提取核心板固件到临时tar.gz文件
#define CORE_FW_TAR_PATH "/tmp/core_firmware.tar.gz"
ret = drv_ota_extract_subfw_to_tar_gz(filename,
g_core_fw_info.sub_fw_offset,
g_core_fw_info.sub_fw_size,
CORE_FW_TAR_PATH);
if (ret != 0) {
printf("[OTA-CORE] extract core firmware failed\n");
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
break;
}
// 更新进度:核心板提取完成,占总进度的40%
set_ota_progress(otaCalcTotal(40, 0, 0));
reportOtaProgress();
printf("[OTA-CORE] core firmware extract success\n");
// 6. 解压核心板固件到AB备用分区
app_slot_t current_active = drv_ota_get_active_app();
if (current_active == APP_UNKNOWN) {
printf("[OTA-CORE] get active app partition failed\n");
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
break;
}
const char *target_dir = drv_ota_get_target_dir(current_active);
printf("[OTA-CORE] current active partition: %s, target upgrade partition: %s\n",
(current_active == APP_A) ? "APP_A" : "APP_B", target_dir);
// 清空备用分区,避免旧文件残留
drv_ota_clear_app_dir(target_dir);
// 解压固件到备用分区
ret = drv_ota_untar(CORE_FW_TAR_PATH, target_dir);
if (ret != 0) {
printf("[OTA-CORE] untar core firmware failed\n");
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
break;
}
// 更新进度:核心板解压完成,占总进度的100%(核心板部分完成)
set_ota_progress(otaCalcTotal(100, 0, 0));
reportOtaProgress();
printf("[OTA-CORE] core firmware upgrade success\n");
// 7. 启动MCU电控板升级线程,并行处理MCU升级
g_ota_control_thread_exit_flag = 0;
pthread_create(&g_ota_control_thread, NULL, OtaControlThread, NULL);
// 设置MCU升级状态机初始状态:查询升级状态
set_comm_ota_state(OTA_COMM_QUERY_STATUS);
}
break;
// 其他消息处理...
}
}
4.2 核心板固件提取与解压实现
4.2.1 子固件提取
从整机固件包中,提取核心板的固件数据,保存为独立的 tar.gz 压缩包:
// drv_ota.c
/**
* @brief 从整机固件包中提取子固件内容,保存为独立文件
* @param bin_path 整机固件包路径
* @param offset 子固件内容的起始偏移
* @param len 子固件的有效长度
* @param out_tar_gz 输出文件路径
* @return 0成功,-1失败
*/
int drv_ota_extract_subfw_to_tar_gz(const char *bin_path, uint32_t offset, uint32_t len, const char *out_tar_gz)
{
if (bin_path == NULL || out_tar_gz == NULL || len == 0) return -1;
int fd_bin = open(bin_path, O_RDONLY);
if (fd_bin < 0) {
printf("[OTA-PKG] open bin file failed\n");
return -1;
}
int fd_out = open(out_tar_gz, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd_out < 0) {
printf("[OTA-PKG] create output file failed\n");
close(fd_bin);
return -1;
}
// 跳转到子固件的起始偏移
off_t seek_ret = lseek(fd_bin, offset, SEEK_SET);
if (seek_ret != offset) {
printf("[OTA-PKG] lseek sub firmware offset failed\n");
close(fd_bin);
close(fd_out);
return -1;
}
// 分片读取写入,避免大文件内存占用过高
uint8_t buf[4096] = {0};
uint32_t remain = len;
while (remain > 0) {
uint32_t to_read = remain > sizeof(buf) ? sizeof(buf) : remain;
ssize_t read_len = read(fd_bin, buf, to_read);
if (read_len <= 0) break;
ssize_t write_len = write(fd_out, buf, read_len);
if (write_len != read_len) {
printf("[OTA-PKG] write output file failed\n");
close(fd_bin);
close(fd_out);
return -1;
}
remain -= read_len;
}
close(fd_bin);
close(fd_out);
// 校验是否完整读取
if (remain != 0) {
printf("[OTA-PKG] extract sub firmware failed, remain: %u\n", remain);
unlink(out_tar_gz);
return -1;
}
return 0;
}
4.2.2 固件解压到备用分区
核心板采用AB 双分区升级方案,这是嵌入式 Linux 防变砖的核心设计:
- 设备有两个完全独立的根文件系统分区:APP_A 和 APP_B;
- 设备当前从其中一个分区启动(激活分区),升级时将新固件写入另一个空闲的备用分区;
- 只有当固件完全写入、校验通过、所有升级流程完成后,才修改启动配置,切换到备用分区启动;
- 若升级过程中出现断电、写入失败等异常,设备仍会从原激活分区启动,不会出现变砖问题。
核心代码实现:
// drv_ota.c
// OTA分区配置文件路径,存储当前激活分区
#define OTA_CFG_PATH "/customer/ota.conf"
typedef enum {
APP_A = 0,
APP_B = 1,
APP_UNKNOWN = 2
} app_slot_t;
/**
* @brief 获取当前激活的分区
* @return 分区枚举值
*/
app_slot_t drv_ota_get_active_app(void)
{
FILE *fp = fopen(OTA_CFG_PATH, "r");
if (fp == NULL) {
printf("[OTA-PART] open ota config file failed\n");
return APP_UNKNOWN;
}
char line[128] = {0};
while (fgets(line, sizeof(line), fp) != NULL) {
// 匹配ACTIVE_APP配置项
if (strncmp(line, "ACTIVE_APP=", 11) == 0) {
if (strstr(line, "app_a") != NULL) {
fclose(fp);
return APP_A;
} else if (strstr(line, "app_b") != NULL) {
fclose(fp);
return APP_B;
}
}
}
fclose(fp);
return APP_UNKNOWN;
}
/**
* @brief 根据当前激活分区,获取升级目标备用分区路径
* @param current_active 当前激活分区
* @return 备用分区挂载路径
*/
const char* drv_ota_get_target_dir(app_slot_t current_active)
{
// 备用分区与当前激活分区相反
return (current_active == APP_A) ? "/mnt/app_b" : "/mnt/app_a";
}
/**
* @brief 解压tar.gz固件包到目标分区
* @param tarfile 压缩包路径
* @param outdir 目标解压目录
* @return 0成功,-1失败
*/
int drv_ota_untar(const char *tarfile, const char *outdir)
{
if (tarfile == NULL || outdir == NULL) return -1;
int status = 0;
pid_t pid = fork();
if (pid == 0) {
// 子进程执行tar解压命令
execlp("tar", "tar", "-xzvf", tarfile, "-C", outdir, NULL);
// 若execlp执行失败,退出子进程
_exit(127);
} else if (pid < 0) {
printf("[OTA-PART] fork failed\n");
return -1;
}
// 等待子进程执行完成
waitpid(pid, &status, 0);
// 校验解压结果
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
printf("[OTA-PART] untar success\n");
return 0;
} else {
printf("[OTA-PART] untar failed, exit status: %d\n", WEXITSTATUS(status));
return -1;
}
}
4.3 MCU 电控板升级的状态机设计
MCU 电控板升级采用状态机 + MCU 主动拉取的方案,这是保证串口升级可靠性的核心设计:
- 状态机清晰管理升级全流程,每个阶段有明确的状态与流转逻辑,避免异步串口通信带来的逻辑混乱;
- 采用 MCU 主动拉取固件分片的方式,而非核心板主动推送,流控完全由 MCU 控制,适配 MCU 的 Flash 写入速度,避免核心板发送过快导致 MCU 丢包、写入失败。
4.3.1 状态机定义
// MCU升级状态机枚举
typedef enum {
OTA_COMM_IDLE = 0, // 空闲状态
OTA_COMM_QUERY_STATUS, // 查询MCU升级状态
OTA_COMM_VALIDATE_FW, // 固件校验(预留)
OTA_COMM_NOTIFY_START, // 通知MCU开始升级
OTA_COMM_UPGRADING, // 升级中,等待MCU拉取分片
OTA_COMM_COMPLETE, // 升级完成
OTA_COMM_ERROR // 升级出错
} otaCommState_e;
// 全局状态变量
static otaCommState_e g_comm_ota_state = OTA_COMM_IDLE;
static pthread_mutex_t g_comm_state_mutex = PTHREAD_MUTEX_INITIALIZER;
// 状态设置与获取
void set_comm_ota_state(otaCommState_e state)
{
pthread_mutex_lock(&g_comm_state_mutex);
g_comm_ota_state = state;
pthread_mutex_unlock(&g_comm_state_mutex);
}
otaCommState_e get_comm_ota_state(void)
{
pthread_mutex_lock(&g_comm_state_mutex);
otaCommState_e state = g_comm_ota_state;
pthread_mutex_unlock(&g_comm_state_mutex);
return state;
}
4.3.2 MCU 升级控制线程
/**
* @brief MCU升级控制线程,循环执行状态机
*/
void* OtaControlThread(void* arg)
{
printf("[OTA-MCU] mcu upgrade thread start\n");
while (!g_ota_control_thread_exit_flag) {
otaCommState_e current_state = get_comm_ota_state();
switch (current_state) {
case OTA_COMM_QUERY_STATUS:
// 向MCU发送升级状态查询指令
printf("[OTA-MCU] query mcu upgrade status\n");
MessageBusPublishToComm(NULL, 0, OTA_COMM_SEARCH_UPGRADE);
sleep(1);
break;
case OTA_COMM_NOTIFY_START:
// 通知MCU准备开始升级
printf("[OTA-MCU] notify mcu start upgrade\n");
MessageBusPublishToComm(NULL, 0, OTA_COMM_NOTIFY_UPGRADE);
sleep(1);
break;
case OTA_COMM_UPGRADING:
// 升级中,等待MCU主动请求固件分片,休眠降低CPU占用
usleep(100000);
break;
case OTA_COMM_COMPLETE:
case OTA_COMM_ERROR:
// 升级完成/出错,退出线程
g_ota_control_thread_exit_flag = 1;
break;
default:
usleep(100000);
break;
}
}
printf("[OTA-MCU] mcu upgrade thread exit\n");
pthread_exit(NULL);
}
4.3.3 MCU 升级消息交互处理
OTA 模块接收通信模块转发的 MCU 消息,处理升级全流程的交互逻辑:
/**
* @brief OTA模块消息处理入口(补充MCU相关消息)
*/
static void Ota_HandleMessage(Message_t *msg)
{
switch (msg->msg_id) {
// 省略OTA_START相关代码...
case OTA_COMM_SEARCH_UPGRADE_ACK:
{
// 收到MCU的升级状态查询应答
ota_upgrade_able_t state = {0};
memcpy(&state, msg->payload, sizeof(ota_upgrade_able_t));
printf("[OTA-MCU] mcu upgrade state: %d\n", state.state);
if (state.state == UPGRADEABLE_CAN_UPGRADE_NOW || state.state == UPGRADEABLE_CAN_UPGRADE_IDLE) {
// MCU可升级,切换到通知开始升级状态
set_comm_ota_state(OTA_COMM_NOTIFY_START);
} else if (state.state == UPGRADEABLE_START_UPGRADE_NOW) {
// MCU已准备好,切换到升级中状态
set_comm_ota_state(OTA_COMM_UPGRADING);
} else {
// MCU不可升级,切换到错误状态
set_comm_ota_state(OTA_COMM_ERROR);
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
}
}
break;
case OTA_COMM_GET_FIRMWARE_ACK:
{
// 收到MCU的固件分片请求
ota_get_msg_pkt ota_cmd_pkt = {0};
memcpy(&ota_cmd_pkt, msg->payload, sizeof(ota_get_msg_pkt));
// 解析MCU请求的偏移与长度
uint32_t offset = (ota_cmd_pkt.offset[0] << 24) | (ota_cmd_pkt.offset[1] << 16) |
(ota_cmd_pkt.offset[2] << 8) | ota_cmd_pkt.offset[3];
uint8_t size = ota_cmd_pkt.size;
printf("[OTA-MCU] mcu request firmware, offset: %u, size: %u\n", offset, size);
// 从固件包中读取对应分片数据
drv_ota_comm_msg_pkt ota_payload_pkt = {0};
int ret = drv_ota_get_subfw_payload(OTA_DOWNLOAD_PATH,
g_control_fw_info.sub_fw_offset,
g_control_fw_info.sub_fw_size,
offset, size, &ota_payload_pkt);
if (ret == size) {
// 发送固件分片给通信模块,转发到MCU
MessageBusPublishToComm(&ota_payload_pkt, sizeof(drv_ota_comm_msg_pkt), OTA_COMM_SEND_FIRMWARE);
// 更新MCU升级进度
uint32_t total_size = g_control_fw_info.sub_fw_size;
uint32_t sent_size = offset + ret;
uint8_t mcu_percent = (sent_size * 100) / total_size;
set_ota_progress(otaCalcTotal(100, mcu_percent, 0));
reportOtaProgress();
printf("[OTA-MCU] mcu upgrade progress: %d%%\n", mcu_percent);
}
}
break;
case OTA_COMM_UPGRADE_COMPLETE_ACK:
{
// 收到MCU的升级完成应答
ota_result_pkt result_pkt = {0};
memcpy(&result_pkt, msg->payload, sizeof(ota_result_pkt));
printf("[OTA-MCU] mcu upgrade result: %d\n", result_pkt.upgradeState);
if (result_pkt.upgradeState == 0) {
// 0表示升级成功
printf("[OTA-MCU] mcu upgrade success\n");
// 更新进度:MCU升级完成
set_ota_progress(otaCalcTotal(100, 100, 0));
reportOtaProgress();
// 执行数据库迁移脚本,保证用户数据兼容新版本
drv_sqlite3_run_db_migrate_script_and_wait();
printf("[OTA-CORE] db migrate success\n");
// 切换启动分区,完成核心板升级的最后一步
app_slot_t current_active = drv_ota_get_active_app();
int ret = drv_ota_update_active_app(current_active);
if (ret != 0) {
printf("[OTA-CORE] update active app failed\n");
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
break;
}
printf("[OTA-CORE] update active app success\n");
// 更新最终进度:全部完成
set_ota_progress(otaCalcTotal(100, 100, 100));
reportOtaProgress();
// 通知Task模块升级完成
MessageBusPublishToTask(NULL, 0, OTA_FINISH);
set_comm_ota_state(OTA_COMM_COMPLETE);
} else {
// MCU升级失败
printf("[OTA-MCU] mcu upgrade failed\n");
set_comm_ota_state(OTA_COMM_ERROR);
MessageBusPublishToTask(NULL, 0, OTA_ERROR);
}
// 标记线程退出
g_ota_control_thread_exit_flag = 1;
}
break;
}
}
4.3.4 固件分片读取实现
// drv_ota.c
/**
* @brief 从整机固件包中读取MCU子固件的指定分片数据
* @param fw_path 整机固件包路径
* @param subfw_content_offset MCU子固件的起始偏移
* @param subfw_content_size MCU子固件的总长度
* @param offset 请求的分片偏移(相对于子固件起始地址)
* @param size 请求的分片长度
* @param pkt 输出的分片数据包
* @return 成功返回读取的字节数,失败返回-1
*/
int drv_ota_get_subfw_payload(const char *fw_path, uint32_t subfw_content_offset,
uint32_t subfw_content_size, uint32_t offset, uint8_t size,
drv_ota_comm_msg_pkt *pkt)
{
if (fw_path == NULL || pkt == NULL || size == 0) return -1;
// 校验偏移是否越界
if (offset + size > subfw_content_size) {
printf("[OTA-PKG] firmware payload offset out of range\n");
return -1;
}
FILE *fp = fopen(fw_path, "rb");
if (fp == NULL) {
printf("[OTA-PKG] open firmware file failed\n");
return -1;
}
// 计算分片在整机固件包中的绝对偏移
uint32_t file_offset = subfw_content_offset + offset;
fseek(fp, file_offset, SEEK_SET);
// 填充数据包
memset(pkt, 0, sizeof(drv_ota_comm_msg_pkt));
// 大端序填充偏移,与MCU端兼容
pkt->offset[0] = (offset >> 24) & 0xFF;
pkt->offset[1] = (offset >> 16) & 0xFF;
pkt->offset[2] = (offset >> 8) & 0xFF;
pkt->offset[3] = offset & 0xFF;
pkt->size = size;
// 读取固件数据
fread(pkt->payload, 1, size, fp);
fclose(fp);
return size;
}
4.4 分区切换的原子性实现
升级全流程完成后,需要修改 OTA 配置文件,切换下次启动的激活分区,这里采用临时文件 + rename 原子操作的方案,避免配置文件写入过程中断电导致的配置损坏。
// drv_ota.c
/**
* @brief 更新激活分区配置,切换下次启动的分区
* @param current_active 当前激活分区
* @return 0成功,-1失败
*/
int drv_ota_update_active_app(app_slot_t current_active)
{
if (current_active == APP_UNKNOWN) return -1;
// 目标分区与当前激活分区相反
const char *new_app_str = (current_active == APP_A) ? "app_b" : "app_a";
const char *temp_cfg_path = "/customer/ota.conf.tmp";
// 1. 打开原配置文件与临时文件
FILE *fp_old = fopen(OTA_CFG_PATH, "r");
if (fp_old == NULL) {
printf("[OTA-PART] open old config file failed\n");
return -1;
}
FILE *fp_new = fopen(temp_cfg_path, "w+");
if (fp_new == NULL) {
printf("[OTA-PART] create temp config file failed\n");
fclose(fp_old);
return -1;
}
// 2. 逐行读取原配置,替换ACTIVE_APP行
char line[128] = {0};
while (fgets(line, sizeof(line), fp_old) != NULL) {
if (strncmp(line, "ACTIVE_APP=", 11) == 0) {
// 替换为新的激活分区
fprintf(fp_new, "ACTIVE_APP=%s\n", new_app_str);
} else {
// 其他行原样写入
fputs(line, fp_new);
}
}
fclose(fp_old);
fclose(fp_new);
// 3. 原子替换:rename操作在Linux中是原子性的,要么完全成功,要么完全失败
if (rename(temp_cfg_path, OTA_CFG_PATH) != 0) {
printf("[OTA-PART] rename config file failed\n");
unlink(temp_cfg_path);
return -1;
}
// 4. 同步文件系统,确保配置写入Flash
sync();
printf("[OTA-PART] update active app to %s success\n", new_app_str);
return 0;
}
五、通信模块:UART 可靠传输协议设计与实现
通信模块是核心板与 MCU 电控板之间的桥梁,代码实现在comm_module.c中,核心职责是UART 串口驱动、可靠帧协议封装与解析、OTA 指令与数据的双向透传。
UART 是流式传输接口,本身没有帧边界、没有可靠传输机制,因此我们设计了一套带转义、带校验、抗粘包的串口帧协议,保证 OTA 升级过程中数据传输的可靠性。
5.1 UART 帧协议格式
完整的 UART 帧结构如下:
| 字段 | 长度 (字节) | 说明 |
|---|---|---|
| 帧头 | 2 | 固定为 0xFF 0xFF,用于标识帧的起始 |
| 帧长度 | 1 | 帧的总长度(从帧长度字段开始,到校验和字段结束) |
| 地址域 | 6 | 源地址、目标地址、帧计数等扩展字段 |
| 帧类型 | 1 | 标识帧的业务类型,如 OTA 查询、固件数据传输等 |
| 负载数据 | N | 业务数据,长度可变 |
| 校验和 | 1 | 从帧长度字段到负载数据结束的所有字节的累加和,用于校验帧完整性 |
核心设计:
- 转义机制:由于帧头是 0xFF,若负载数据中出现 0xFF,会被误判为帧头,因此采用转义规则:
0xFF 0x55转义为0xFF,避免粘包与误判; - 累加和校验:保证帧数据的完整性,若校验和不匹配,直接丢弃该帧;
- 状态机解析:适配串口流式传输的特点,可处理任意字节的到来,正确解析粘包、断包。
5.2 发送帧的封装实现
// comm_module.c
// 帧头固定值
#define UART_FRAME_HEAD1 0xFF
#define UART_FRAME_HEAD2 0xFF
// UART帧结构体
typedef struct {
uint8_t head[2];
uint8_t len;
addr_t addr;
uint8_t frame_type;
uint8_t *payload;
uint8_t payload_size;
} UartFrame_t;
/**
* @brief 封装并发送UART帧
* @param frame 待发送的帧结构体
*/
static void send_uart_frame(UartFrame_t *frame)
{
uint8_t temp_buf[512] = {0};
int index = 0;
uint8_t checksum = 0;
// 写入帧头
temp_buf[index++] = frame->head[0];
temp_buf[index++] = frame->head[1];
// 宏定义:发送单个字节,自动处理转义与校验和计算
#define SEND_BYTE(b) do { \
temp_buf[index++] = (b); \
checksum += (b); \
if ((b) == 0xFF) { \
temp_buf[index++] = 0x55; \
checksum += 0x55; \
} \
} while(0)
// 写入帧长度
SEND_BYTE(frame->len);
// 写入地址域
SEND_BYTE(frame->addr.byte1.byte);
SEND_BYTE(frame->addr.reserved1);
SEND_BYTE(frame->addr.frame_count);
SEND_BYTE(frame->addr.beacon);
SEND_BYTE(frame->addr.src_addr);
SEND_BYTE(frame->addr.dst_addr);
// 写入帧类型
SEND_BYTE(frame->frame_type);
// 写入负载数据
for (int i = 0; i < frame->payload_size; i++) {
SEND_BYTE(frame->payload[i]);
}
// 写入校验和(校验和本身也需要转义)
temp_buf[index++] = checksum;
if (checksum == 0xFF) {
temp_buf[index++] = 0x55;
}
// 串口发送
uart_write(&g_uart5Handle, temp_buf, index);
}
5.3 接收帧的状态机解析
采用状态机实现串口字节流的逐字节解析,正确处理转义、粘包、断包,保证帧解析的可靠性。
// comm_module.c
// 接收状态机枚举
typedef enum {
RX_WAIT_FF1 = 0, // 等待第一个帧头0xFF
RX_WAIT_FF2, // 等待第二个帧头0xFF
RX_WAIT_LEN, // 等待帧长度字段
RX_RECV_PAYLOAD, // 接收负载数据
} RxState_e;
// 全局接收状态变量
static RxState_e rx_state = RX_WAIT_FF1;
static uint8_t last_was_ff = 0;
static uint8_t frame_buf[512] = {0};
static int frame_pos = 0;
static uint8_t expect_len = 0;
static uint8_t recv_len = 0;
static uint8_t checksum_calc = 0;
static uint8_t checksum_recv = 0;
/**
* @brief 逐字节处理串口接收到的数据
* @param byte 接收到的单个字节
*/
static void rx_byte_handle(uint8_t byte)
{
switch (rx_state) {
case RX_WAIT_FF1:
// 等待第一个帧头字节
if (byte == UART_FRAME_HEAD1) {
rx_state = RX_WAIT_FF2;
}
break;
case RX_WAIT_FF2:
// 等待第二个帧头字节
if (byte == UART_FRAME_HEAD2) {
// 帧头匹配,初始化接收参数
rx_state = RX_WAIT_LEN;
frame_buf[0] = UART_FRAME_HEAD1;
frame_buf[1] = UART_FRAME_HEAD2;
frame_pos = 2;
last_was_ff = 0;
recv_len = 0;
checksum_calc = 0;
} else {
// 帧头不匹配,重置状态
rx_state = RX_WAIT_FF1;
}
break;
case RX_WAIT_LEN:
// 接收帧长度字段
expect_len = byte;
checksum_calc = byte;
frame_buf[frame_pos++] = byte;
frame_pos = 3;
recv_len = 1;
rx_state = RX_RECV_PAYLOAD;
break;
case RX_RECV_PAYLOAD:
// 处理转义:0xFF 0x55 -> 0xFF
if (last_was_ff) {
last_was_ff = 0;
if (byte == 0x55) {
// 转义还原为0xFF
byte = 0xFF;
} else {
// 非法转义,重置状态
rx_state = RX_WAIT_FF1;
break;
}
} else if (byte == 0xFF) {
// 标记下一个字节需要处理转义
last_was_ff = 1;
break;
}
// 接收数据,区分负载与校验和
if (recv_len < expect_len - 1) {
// 负载数据
frame_buf[frame_pos++] = byte;
checksum_calc += byte;
// 转义的0xFF需要额外加上0x55的校验和
if (byte == 0xFF && !last_was_ff) {
checksum_calc += 0x55;
}
recv_len++;
} else {
// 最后一个字节是校验和
frame_buf[frame_pos++] = byte;
checksum_recv = byte;
recv_len++;
}
// 接收完成,校验帧
if (recv_len >= expect_len) {
if (checksum_calc == checksum_recv) {
// 校验和匹配,将完整帧推入消息队列
UartMsgQueue_Push(frame_buf, frame_pos - 1);
} else {
// 校验和不匹配,丢弃帧
printf("[COMM] checksum mismatch, calc: 0x%02X, recv: 0x%02X\n", checksum_calc, checksum_recv);
}
// 重置状态,等待下一帧
rx_state = RX_WAIT_FF1;
}
break;
}
}
5.4 消息分发与 OTA 指令透传
接收线程解析出完整帧后,会推入消息队列,解析线程从队列中取出帧,根据帧类型分发到对应模块,实现 OTA 指令的双向透传。
// comm_module.c
// 帧类型定义
#define FRAME_DEVICE_UPGRADE_CMD 0x20
#define FRAME_DEVICE_UPGRADE_ACK 0x21
#define FRAME_GET_FIRMWARE_CONTENT 0x22
#define FRAME_GET_FIRMWARE_CONTENT_ACK 0x23
#define FRAME_UPGRADE_RESULT_REPORT 0x24
/**
* @brief 串口消息处理函数,解析帧并分发到对应模块
* @param msg 接收到的完整串口消息
*/
static void UartMsgProcessHandler(UartMsg *msg)
{
if (msg == NULL || msg->data_len < 10) return;
// 校验帧头
if (msg->data[0] != UART_FRAME_HEAD1 || msg->data[1] != UART_FRAME_HEAD2) {
return;
}
// 提取帧类型,分发处理
uint8_t frame_type = msg->data[9];
switch (frame_type) {
case FRAME_DEVICE_UPGRADE_ACK:
{
// MCU升级状态查询应答,转发到OTA模块
ota_upgrade_able_t ota_able = {0};
ota_able.state = msg->data[10];
MessageBusPublishToOta(&ota_able, sizeof(ota_upgrade_able_t), OTA_COMM_SEARCH_UPGRADE_ACK);
}
break;
case FRAME_GET_FIRMWARE_CONTENT:
{
// MCU请求固件分片,转发到OTA模块
ota_get_msg_pkt ota_cmd_pkt = {0};
ota_cmd_pkt.offset[0] = msg->data[10];
ota_cmd_pkt.offset[1] = msg->data[11];
ota_cmd_pkt.offset[2] = msg->data[12];
ota_cmd_pkt.offset[3] = msg->data[13];
ota_cmd_pkt.size = msg->data[14];
MessageBusPublishToOta(&ota_cmd_pkt, sizeof(ota_get_msg_pkt), OTA_COMM_GET_FIRMWARE_ACK);
}
break;
case FRAME_UPGRADE_RESULT_REPORT:
{
// MCU升级完成结果上报,转发到OTA模块
ota_result_pkt result_pkt = {0};
result_pkt.upgradeState = msg->data[30];
result_pkt.isQuitOta = msg->data[31];
MessageBusPublishToOta(&result_pkt, sizeof(ota_result_pkt), OTA_COMM_UPGRADE_COMPLETE_ACK);
}
break;
// 其他帧类型处理...
}
}
/**
* @brief 处理OTA模块发来的消息,封装为串口帧发送给MCU
*/
static void Comm_HandleMessage(Message_t *msg)
{
UartFrame_t frame = {0};
uint8_t payload_buf[256] = {0};
switch (msg->msg_id) {
case OTA_COMM_SEARCH_UPGRADE:
{
// 发送MCU升级状态查询指令
uint8_t buf[36] = {0};
init_uart_frame(&frame, FRAME_DEVICE_UPGRADE_CMD, buf, sizeof(buf));
send_uart_frame(&frame);
}
break;
case OTA_COMM_NOTIFY_UPGRADE:
{
// 发送MCU升级开始通知
uint8_t buf[36] = {0};
buf[34] = 0x64; // 固定标识,通知MCU开始升级
init_uart_frame(&frame, FRAME_DEVICE_UPGRADE_CMD, buf, sizeof(buf));
send_uart_frame(&frame);
}
break;
case OTA_COMM_SEND_FIRMWARE:
{
// 发送固件分片数据给MCU
drv_ota_comm_msg_pkt *pkt = (drv_ota_comm_msg_pkt*)msg->payload;
// 填充负载:4字节偏移 + 1字节长度 + 固件数据
memcpy(payload_buf, pkt->offset, 4);
payload_buf[4] = pkt->size;
memcpy(&payload_buf[5], pkt->payload, pkt->size);
// 初始化帧并发送
init_uart_frame(&frame, FRAME_GET_FIRMWARE_CONTENT_ACK, payload_buf, 5 + pkt->size);
send_uart_frame(&frame);
}
break;
}
}
六、进度管理与用户体验优化
OTA 升级的用户体验核心是清晰的进度反馈,本系统设计了权重化的进度计算方案,将总进度拆解为核心板、MCU、收尾三个阶段,保证进度条平滑增长,避免跳变。
6.1 权重化进度计算
// ota_module.c
// 各阶段权重分配,总和100%
#define PANEL_WEIGHT 60 // 核心板升级占总进度的60%
#define MCU_WEIGHT 35 // MCU升级占总进度的35%
#define FINAL_WEIGHT 5 // 收尾工作(数据库迁移、分区切换)占5%
/**
* @brief 计算总升级进度
* @param panel 核心板阶段进度(0-100)
* @param mcu MCU阶段进度(0-100)
* @param final 收尾阶段进度(0-100)
* @return 总进度(0-100)
*/
uint8_t otaCalcTotal(uint8_t panel, uint8_t mcu, uint8_t final) {
return (panel * PANEL_WEIGHT + mcu * MCU_WEIGHT + final * FINAL_WEIGHT) / 100;
}
6.2 关键进度更新节点
| 升级阶段 | 核心板进度 | MCU 进度 | 收尾进度 | 总进度 |
|---|---|---|---|---|
| 核心板固件提取完成 | 40% | 0% | 0% | 24% |
| 核心板固件解压完成 | 100% | 0% | 0% | 60% |
| MCU 固件传输 50% | 100% | 50% | 0% | 77.5% |
| MCU 升级完成 | 100% | 100% | 0% | 95% |
| 分区切换完成 | 100% | 100% | 100% | 100% |
6.3 进度上报实现
// ota_module.c
static uint8_t g_ota_progress = 0;
static pthread_mutex_t g_progress_mutex = PTHREAD_MUTEX_INITIALIZER;
void set_ota_progress(uint8_t value)
{
pthread_mutex_lock(&g_progress_mutex);
g_ota_progress = value;
pthread_mutex_unlock(&g_progress_mutex);
}
uint8_t get_ota_progress(void)
{
pthread_mutex_lock(&g_progress_mutex);
uint8_t value = g_ota_progress;
pthread_mutex_unlock(&g_progress_mutex);
return value;
}
/**
* @brief 上报进度到Task模块,更新UI进度条
*/
static void reportOtaProgress(void)
{
uint8_t value = get_ota_progress();
MessageBusPublishToTask(&value, sizeof(uint8_t), OTA_REPORT_PROGRESS_ACK);
}
七、系统容错与安全设计
商用设备的 OTA 系统,稳定性与安全性是第一优先级,本系统从多个维度设计了容错与安全机制,杜绝升级变砖、恶意固件刷入等问题。
7.1 全链路校验机制
从固件下载到升级完成,设计了层层校验,确保固件的完整性与合法性:
- 下载完成校验:固件下载完成后,立即执行 SHA256 全量校验,与固件包头部的摘要信息对比,确保固件未被篡改、下载完整;
- 固件包分层校验:解析固件包时,先校验整包 CRC,再校验描述信息 CRC,再校验子固件描述 CRC,层层校验,避免解析损坏的固件;
- 子固件独立校验:每个子固件都有独立的 CRC 与 SHA256 校验值,升级前校验,确保子固件完整;
- 串口传输校验:每个串口帧都有累加和校验,确保传输过程中数据不丢失、不错误。
7.2 防变砖设计
- AB 双分区升级:核心板升级时,新固件写入备用分区,只有所有升级流程完成后才切换启动分区,升级过程中断电,设备仍可从原分区启动,不会变砖;
- MCU 升级回滚机制:MCU 升级时,先将新固件写入备份分区,只有全部写入、校验通过后,才覆盖原固件,升级失败自动回滚,不会导致 MCU 变砖;
- 原子性配置操作:分区切换的配置文件修改采用临时文件 + rename 原子操作,避免断电导致配置文件损坏;
- 开机自检兜底:开机后自动检测本地固件,若升级中断,可自动恢复升级流程,无需人工干预。
7.3 异常容错处理
- 网络异常容错:下载过程中断网,SDK 自动重试,支持断点续传,无需全量重新下载;
- 串口通信容错:串口丢包、错包时,MCU 会重新请求对应分片,不会导致升级失败;
- 空间不足预判:解压固件前,先检查分区剩余空间,避免空间不足导致解压失败;
- 超时机制:所有线程等待、串口通信都设置了超时时间,避免死等导致的系统卡死。
7.4 安全防护设计
- 固件防篡改:采用 SHA256 哈希校验,确保固件未被恶意篡改,只有合法的官方固件才能被刷入;
- 机型匹配校验:固件包中的产品标识与设备硬件 ID 校验,避免刷错机型、错版本的固件;
- 版本控制:可配置是否允许版本降级,避免恶意降级到存在安全漏洞的旧版本;
- 权限控制:OTA 相关的文件、分区操作都有严格的权限控制,避免非授权访问与修改。
优化性能记录,原来下载写固件太慢了
1. 磁盘 IO 核心优化(贡献 60% 性能提升,解决最核心瓶颈)
| 优化项 | 原问题 | 优化动作 | 量化提升 |
|---|---|---|---|
| 刷盘策略优化 | 32K 分片每次都执行fflush强制刷盘,嵌入式 Flash 擦写耗时极高,IO 严重阻塞 |
改为累计写入 256K 再刷盘,下载完成时新增fflush+fsync双刷盘机制 |
刷盘次数减少 87.5%,单分片写入耗时降低 90% 以上 |
| 写入模式优化 | 默认 stdio 缓冲存在用户态→系统态内存拷贝,4096 底层分包写入额外开销大 | 通过setvbuf(_IONBF)开启无缓冲模式,直接写入系统内存页 |
单分包写入耗时降低 80% 以上,完全消除内存拷贝开销 |
2. 协议适配优化(贡献 20% 性能提升,解决文件增长停滞问题)
| 优化项 | 原问题 | 优化动作 | 量化提升 |
|---|---|---|---|
| 分片大小适配 | 上层 32K/16K 分片超过 OSDK 底层range_max:15360硬限制,触发额外拆分,协议交互空窗期长 |
上层分片统一改为 15360 字节(OSDK 支持的最大值),完全匹配底层限制 | 大分片粒度提升 3.75 倍,消除额外拆分开销,协议交互空窗期减少 75% |
3. 系统资源占用优化(贡献 15% 性能提升,消除非必要阻塞)
| 优化项 | 原问题 | 优化动作 | 量化提升 |
|---|---|---|---|
| MQTT 上报降频 | 每个分片都上报下载状态,频繁网络交互抢占下载带宽和 CPU 资源 | 仅进度提升≥1% 或下载完成时才触发上报 | 14M 固件上报次数从 448 次降至≤100 次,减少 77%+,网络带宽占用降低 80% |
| 日志 IO 极简优化 | 每个 4096 底层分包都打印详细日志,嵌入式printf串口 / 文件 IO 耗时极长 |
仅进度提升 1% 时打印 1 次汇总日志,删除分包级打印 | 日志输出频率减少 90%+,日志 IO 耗时降低 95% 以上 |
4. 统计准确性优化(无性能损耗,提升健壮性)
| 优化项 | 原问题 | 优化动作 | 量化提升 |
|---|---|---|---|
| 进度统计逻辑重构 | 静态变量累加统计进度,分片重传会导致进度虚高、统计错误 | 通过offset+write_len直接计算已下载总量 |
进度统计准确率 100%,无重传导致的统计误差 |
整体效率提升量化汇总
| 核心指标 | 优化前 | 优化后 | 提升 / 降低幅度 |
|---|---|---|---|
| 14M 固件完整下载耗时 | 20 分钟 | 2-4 分钟 | 5-10 倍提升 |
| 单分片平均写入耗时 | ~120ms | ~10ms | 90%+ 降低 |
| 全流程刷盘总次数 | 448 次 | 56 次 | 87.5% 降低 |
| MQTT 状态上报总次数 | 448 次 | ≤100 次 | 77%+ 降低 |
| 全流程日志打印总次数 | 3584 次 | ≤100 次 | 97%+ 降低 |
| 协议交互空窗期占比 | ~60% | ≤15% | 75% 降低 |
后续遇到问题解决问题记录
根据技术支持所说,触发下载时候 uhsd_ota_download_section_firm 就传入整个升级包长度。 另外海极网上传的原始包 ,直接下载下来之后是加密的 ,加密后的包长度是16的整数倍,海极网填的长度应当是原始包长度。 针对 最后一包数据。SDK给的解密后数据 是补0对齐16的。需要你和原始包长度比对 取有效位。
1. 触发下载时直接传整个升级包长度
之前是手动分片请求(每次传 15360 字节,下载完一片再请求下一片),技术支持的意思是:
- 直接在首次调用
uhsd_ota_download_section_firm时,传入整个固件的原始长度 (g_ota_firm_info.firm_info.total_len); - SDK 内部会自动处理分片、下载、解密的全流程,不需要你在
UHSD_OTA_EVT_SECTION_FIRM_DOWNLOAD_FINISH里手动请求下一个分片。
为什么这样做?
- 消除之前发现的「协议交互空窗期」:SDK 内部会连续请求分片,不会等你应用层处理完一片再发起下一个请求,文件增长会更连续;
- 简化代码逻辑:删除手动循环请求分片的代码,降低出错概率。
2. 加密包补 0 对齐,需取有效位
海极网的固件包处理流程是:
- 上传原始固件包 (长度为
L)到海极网; - 海极网对原始包进行加密 ,加密算法(如 AES)要求数据长度是 16 的整数倍,因此会在原始包末尾补 0 对齐到 16 的整数倍 (加密后长度为
L',L' ≥ L且L'是 16 的倍数); - 海极网在固件信息里填的
total_len是原始包长度L(不是加密后的L'); - SDK 下载加密包后,会在内部解密,解密后的数据包含原始包 + 补的 0;
- SDK 通过
ota_download_firmware_write回调给你的数据,最后一包可能包含补的 0 ,你需要根据total_len(原始长度)截断,只写入有效数据。
为什么这样做?
- 保证固件包的正确性:如果把补的 0 也写进去,固件包会变大,后续的 SHA256 校验、固件解析都会失败;
- 海极网只认原始长度:后续的升级校验、版本比对都是基于原始包长度
L。
调整 1:删除手动分片请求逻辑,直接传整个包长度
修改位置 1:UHSD_OTA_EVT_FIRM_READY事件(首次请求)
// 原代码:手动传15360字节分片
uhsd_s32 download_ret = uhsd_ota_download_section_firm(firm_info->upgrade_sn, 0, 15360);
printf("[OTA-DOWNLOAD] 6.2 分片下载请求发起成功(sn=%u,起始偏移=0,分片长度=15360\n", firm_info->upgrade_sn);
调整后代码:
// 直接传整个固件的原始长度,SDK内部自动处理分片
uhsd_s32 download_ret = uhsd_ota_download_section_firm(firm_info->upgrade_sn, 0, firm_info->total_len);
printf("[OTA-DOWNLOAD] 6.2 全量下载请求发起成功(sn=%u,起始偏移=0,总长度=%u\n", firm_info->upgrade_sn, firm_info->total_len);
修改位置 2:删除UHSD_OTA_EVT_SECTION_FIRM_DOWNLOAD_FINISH事件里的手动请求下一个分片逻辑
// 分支2:还有未下载的分片,发起下一个分片下载
else {
// 原代码:手动请求下一个分片
uhsd_s32 ret = uhsd_ota_download_section_firm(cur_sn, next_offset, 15360);
if (ret != 0) {
// 错误处理...
} else {
printf("[OTA-DOWNLOAD] 已发起下一个分片下载(偏移:%u,长度:15360)\n", next_offset);
}
}
调整后代码:
// 分支2:SDK内部自动处理后续分片,无需手动请求,直接打印日志即可
else {
printf("[OTA-DOWNLOAD] 单个分片下载完成 | 当前偏移:%u | 分片长度:%u | 已下载:%u/%u字节\n",
current_offset, current_len, next_offset, firm_total_len);
}
调整 2:在ota_download_firmware_write里增加「有效位截断」逻辑
在写入文件前,先计算当前分片的有效长度:
- 如果
offset + len ≤ 原始包长度:说明不是最后一包,直接写入全部len字节; - 如果
offset + len > 原始包长度:说明是最后一包,有效长度是原始包长度 - offset,只写入前有效长度字节,丢弃后面补的 0。
调整后的完整ota_download_firmware_write代码(结合之前的所有优化)
static uhsd_s32 ota_download_firmware_write(uhsd_u32 offset, uhsd_u8 *buf, uhsd_u32 len)
{
// 基础参数校验
if (g_ota_download_file == NULL || buf == NULL || len == 0) return -1;
// 关键新增:计算有效写入长度(截断最后一包的补0)
uhsd_u32 firm_original_len = g_ota_firm_info.firm_info.total_len; // 原始包长度(海极网给的)
uhsd_u32 valid_len = len;
if (offset + len > firm_original_len) {
valid_len = firm_original_len - offset; // 最后一包,只取有效位
printf("[OTA-DOWNLOAD] 最后一包截断补0 | 原始偏移:%u | 原始长度:%u | 有效长度:%u\n",
offset, len, valid_len);
}
// 无缓冲写入(保留之前的优化)
static int is_unbuffered = 0;
if (!is_unbuffered) {
setvbuf(g_ota_download_file, NULL, _IONBF, 0);
is_unbuffered = 1;
}
// 定位+写入有效数据
fseek(g_ota_download_file, offset, SEEK_SET);
size_t write_len = fwrite(buf, 1, valid_len, g_ota_download_file);
if (write_len != valid_len) {
printf("[OTA-DOWNLOAD] 分片写入异常 | 偏移:%u | 预期有效写入:%u | 实际写入:%zu\n",
offset, valid_len, write_len);
return -1;
}
// 256K阈值刷盘(保留之前的优化)
static uhsd_u32 total_flush_size = 0;
total_flush_size += write_len;
const uhsd_u32 FLUSH_THRESHOLD = 256 * 1024;
if (total_flush_size >= FLUSH_THRESHOLD) {
fflush(g_ota_download_file);
total_flush_size = 0;
}
// 基于原始长度计算准确进度(保留之前的优化)
uhsd_u32 total_written = offset + write_len;
float progress = (float)total_written / firm_original_len * 100.0f;
// 1%进度阈值上报MQTT(保留之前的优化)
static float last_report_progress = -1.0f;
uhsd_u32 cur_sn = g_ota_firm_info.firm_info.upgrade_sn;
if (progress - last_report_progress >= 1.0f || total_written >= firm_original_len) {
uhsd_ota_upgrade_status_rpt(cur_sn, UHSD_OTA_UPGRADE_STATUS_IDLE_DOWNLOADING, UHSD_FALSE);
last_report_progress = progress;
// 极简日志
printf("[OTA-DOWNLOAD] 进度更新:%.1f%% | 已下载:%uK/%uK\n",
progress, total_written/1024, firm_original_len/1024);
}
// 下载完成双刷盘(基于原始长度判断,保留之前的优化)
if (total_written >= firm_original_len) {
fflush(g_ota_download_file);
fsync(fileno(g_ota_download_file));
printf("[OTA-DOWNLOAD] 固件写入完成(已截断补0),强制刷盘成功 | 原始长度:%u\n", firm_original_len);
}
return (uhsd_s32)write_len;
}
- 效率进一步提升 :SDK 内部自动连续请求分片,完全消除你之前发现的「协议交互空窗期」,文件增长会更连续,14M 固件下载时间可能进一步缩短到2 分钟以内;
- 代码更简洁:删除手动循环请求分片的逻辑,代码量减少 30%+,出错概率更低;
- 固件 100% 正确:有效位截断逻辑保证固件包大小和原始包一致,后续的 SHA256 校验、固件解析不会失败。
目前已经可以网络端实现快速完整下载
