Linux应用开发:全链路 OTA 升级架构

全链路 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                          第四层:实际固件二进制数据
│  ...                                 
└──────────────────────────────────────┘

设计核心考量

  1. 所有头部字段固定长度,解析逻辑简单,避免变长字段带来的解析风险;
  2. 采用大端序(网络字节序)存储所有多字节字段,兼容 Linux 小端与 MCU 大端平台,避免字节序错乱;
  3. 分层 CRC+SHA256 双校验,从整包到子固件层层校验,确保固件完整性与防篡改;
  4. 子固件独立索引,可按需提取对应模块的固件,支持整机升级与单模块升级。

所有结构定义均在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),避免内存占用过高,同时支持网络异常后的断点续传。

设计考量

  1. 分片写入:每下载一片固件数据,立即写入文件,不缓存到内存,降低内存占用;
  2. 进度防抖:每下载进度提升 5% 才上报一次,避免频繁发送消息导致消息总线拥堵、UI 卡顿;
  3. 异常处理:下载过程中出现网络异常,SDK 会自动重试,重试失败后清理无效文件;
  4. 下载完成后,立即执行 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 全链路校验机制

从固件下载到升级完成,设计了层层校验,确保固件的完整性与合法性:

  1. 下载完成校验:固件下载完成后,立即执行 SHA256 全量校验,与固件包头部的摘要信息对比,确保固件未被篡改、下载完整;
  2. 固件包分层校验:解析固件包时,先校验整包 CRC,再校验描述信息 CRC,再校验子固件描述 CRC,层层校验,避免解析损坏的固件;
  3. 子固件独立校验:每个子固件都有独立的 CRC 与 SHA256 校验值,升级前校验,确保子固件完整;
  4. 串口传输校验:每个串口帧都有累加和校验,确保传输过程中数据不丢失、不错误。

7.2 防变砖设计

  1. AB 双分区升级:核心板升级时,新固件写入备用分区,只有所有升级流程完成后才切换启动分区,升级过程中断电,设备仍可从原分区启动,不会变砖;
  2. MCU 升级回滚机制:MCU 升级时,先将新固件写入备份分区,只有全部写入、校验通过后,才覆盖原固件,升级失败自动回滚,不会导致 MCU 变砖;
  3. 原子性配置操作:分区切换的配置文件修改采用临时文件 + rename 原子操作,避免断电导致配置文件损坏;
  4. 开机自检兜底:开机后自动检测本地固件,若升级中断,可自动恢复升级流程,无需人工干预。

7.3 异常容错处理

  1. 网络异常容错:下载过程中断网,SDK 自动重试,支持断点续传,无需全量重新下载;
  2. 串口通信容错:串口丢包、错包时,MCU 会重新请求对应分片,不会导致升级失败;
  3. 空间不足预判:解压固件前,先检查分区剩余空间,避免空间不足导致解压失败;
  4. 超时机制:所有线程等待、串口通信都设置了超时时间,避免死等导致的系统卡死。

7.4 安全防护设计

  1. 固件防篡改:采用 SHA256 哈希校验,确保固件未被恶意篡改,只有合法的官方固件才能被刷入;
  2. 机型匹配校验:固件包中的产品标识与设备硬件 ID 校验,避免刷错机型、错版本的固件;
  3. 版本控制:可配置是否允许版本降级,避免恶意降级到存在安全漏洞的旧版本;
  4. 权限控制: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 对齐,需取有效位

海极网的固件包处理流程是:

  1. 上传原始固件包 (长度为L)到海极网;
  2. 海极网对原始包进行加密 ,加密算法(如 AES)要求数据长度是 16 的整数倍,因此会在原始包末尾补 0 对齐到 16 的整数倍 (加密后长度为L'L' ≥ LL'是 16 的倍数);
  3. 海极网在固件信息里填的total_len原始包长度L (不是加密后的L');
  4. SDK 下载加密包后,会在内部解密,解密后的数据包含原始包 + 补的 0;
  5. 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;
}
  1. 效率进一步提升 :SDK 内部自动连续请求分片,完全消除你之前发现的「协议交互空窗期」,文件增长会更连续,14M 固件下载时间可能进一步缩短到2 分钟以内
  2. 代码更简洁:删除手动循环请求分片的逻辑,代码量减少 30%+,出错概率更低;
  3. 固件 100% 正确:有效位截断逻辑保证固件包大小和原始包一致,后续的 SHA256 校验、固件解析不会失败。

目前已经可以网络端实现快速完整下载

相关推荐
小黑要努力2 小时前
json-c安装以及amixer使用
linux·运维·json
Du_chong_huan2 小时前
1.5 协议层次及其服务模型 | 计算机网络的 “分层架构” 哲学
计算机网络·架构
JiMoKuangXiangQu2 小时前
Linux:ARM64 启动流程
linux·arm64 boot
一叶飘零_sweeeet3 小时前
MySQL高可用生产落地全解:主从同步、MGR集群、读写分离从原理到实战
数据库·mysql·架构·mysql高可用
TroubleMakerQi3 小时前
[虚拟机环境配置]07_Ubuntu中安装vscode教程
linux·人工智能·vscode·ubuntu
源远流长jerry3 小时前
RDMA vs 传统以太网:寻址粒度为何决定性能天花板
linux·网络
知识分享小能手3 小时前
Redis入门学习教程,从入门到精通,Redis集群架构:语法知识点、使用方法与综合案例(6)
redis·学习·架构
zzzsde3 小时前
【Linux】进程控制(1):进程创建&&进程终止
linux·运维·服务器
tianyuanwo3 小时前
Koji 分布式编译调度机制深度解析:多架构异构节点的资源优化方案
分布式·架构