跳转至

OTA

OTA

SLE OTA Service、SSAP 通知/写入、upg 中间件

前置阅读:Hello NotifyHello ReadWrite高吞吐传输

学习目标

  • 理解 SLE OTA 的整体流程——Client 推送固件 -> Server 接收并写入 Flash -> CRC32 校验 -> 重启进入新固件
  • 掌握 sle_ota_service_init() 一站式初始化 OTA Service 的方法
  • 理解 OTA 数据分包策略、位图 ACK 机制和丢包重发逻辑
  • 理解升级状态机各阶段的职责与错误回滚路径
  • 能够在 WS63 上通过 SLE 实现固件无线升级

基本概念

典型使用场景

已部署的设备需要更新固件,通过 SLE 无线升级避免拆机 JTAG 烧录。SLE OTA 利用高吞吐配置(4M PHY + MCS10)可达 ~3 Mbps 传输速率,是 BLE OTA 的 3~4 倍。例如:

  • 工业传感器批量升级——网关通过 SLE 向 8 个传感器并发推送固件,车间无需停机拆装
  • 智能家居设备升级——手机 APP 直连 SLE 设备推送新固件,用户无感升级
  • 物流标签固件更新——手持终端靠近电子价签即可推送新固件,省去逐个回收

SLE OTA vs BLE OTA

对比维度 SLE OTA BLE OTA
初始化 API sle_ota_service_init() 一行初始化 bth_ota_init() 需手动配置 Service
最高 PHY 速率 4M 2M
有效吞吐 ~3 Mbps (MCS10) ~800 Kbps
分包大小 MTU - 4 = 516 字节 ATT_MTU - 3 = 244 字节
ACK 机制 位图 ACK(一轮确认一批) 每包确认
适用场景 中大型固件(> 200 KB) 小型固件

SLE OTA 吞吐量约为 BLE OTA 的 3~4 倍,对于 512 KB 固件仅需 ~1.5 秒空中传输时间。SLE 的位图 ACK 也显著减少了确认往返次数。

OTA 数据分包与 ACK 机制

固件按 MTU - 4 字节分包,每包带 4 字节 OTA 协议头:

[type:1B][seq:2B][total:1B] [payload: 0~516B]
  • type:0x01=数据包,0x02=控制指令(开始/结束/校验)
  • seq:递增序号(0, 1, 2, ...),用于丢包检测
  • total:保留字段

Client 每发送一轮(默认 50 包),Server 通过 sle_ota_data_ack() 回复一个位图——每位代表一个包的接收状态(1=收到,0=丢失)。Client 根据位图重发丢失的包,直到所有包到位。

ACK 策略 每包确认 位图 ACK(每 50 包) 全量 ACK(结束确认)
确认往返次数 N 次 N/50 次 1 次
丢包重发粒度 单包 按位图精确重发 全量重发
适用场景 极不可靠链路 推荐,平衡效率与可靠性 极可靠链路

位图 ACK 是本案例的默认策略——每 50 包确认一次,兼顾效率和可靠性。

升级状态机

stateDiagram-v2
    [*] --> IDLE
    IDLE --> RECEIVING: Client发起OTA请求
    RECEIVING --> RECEIVING: 接收数据包/回复ACK
    RECEIVING --> VERIFYING: 所有数据包传输完成
    VERIFYING --> APPLYING: CRC32校验通过
    VERIFYING --> RECEIVING: CRC32校验失败/请求重发
    APPLYING --> REBOOT: Boot标志写入完成
    REBOOT --> [*]
    RECEIVING --> IDLE: 传输超时/连接断开
    VERIFYING --> IDLE: 校验失败超过最大重试次数

每个状态的职责:

状态 Server 职责 触发条件 退出条件
IDLE 等待 Client 发起 OTA 请求 上电 / 上一次升级结束 收到 Client 的"开始 OTA"指令
RECEIVING 接收固件数据包,写入 Flash Buffer,定时回复 ACK 进入 OTA 流程 收到"传输完成"指令 或 超时
VERIFYING 对已写入 Flash 的固件做 CRC32 校验 传输完成 校验通过/失败
APPLYING 写入 Boot 分区标志(upg 中间件处理) 校验通过 标志写入完成
REBOOT 系统重启,Bootloader 加载新固件 标志写入完成 新固件启动

任何阶段出错可回滚到原固件——Bootloader 在启动时检查标志,若新固件验证失败则自动回退。

通信流程:OTA 升级完整序列

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C,S: 阶段1: 连接建立 已通过前置案例完成
    C->>S: ssapc_write_req 开始OTA 固件大小+CRC32期望值
    S-->>C: sle_ota_data_ack 确认就绪
    Note over S: 状态切换 IDLE -> RECEIVING

    loop 阶段2: 分批传输 每50包一轮
        C->>S: ssapc_write_cmd 固件数据包 seq=0~49
        S->>S: 写入Flash Buffer
        S-->>C: sle_ota_data_ack 位图ACK
        alt 存在丢包 seq=12,33丢失
            C->>S: ssapc_write_cmd 重发 seq=12
            C->>S: ssapc_write_cmd 重发 seq=33
            S-->>C: sle_ota_data_ack 确认补全
        end
    end

    C->>S: ssapc_write_req 传输完成通知
    Note over S: 状态切换 RECEIVING -> VERIFYING
    S->>S: CRC32校验
    alt CRC32通过
        Note over S: 状态切换 VERIFYING -> APPLYING
        S->>S: 写入Boot标志
        Note over S: 状态切换 APPLYING -> REBOOT
        S->>S: reboot 重启进入新固件
    else CRC32失败
        S-->>C: 校验失败 请求重发
        Note over S: 状态回退 VERIFYING -> RECEIVING
    end

涉及 API

API 谁调用 用途
sle_ota_service_init(server_id) Server 初始化 OTA Service,自动注册 Service + Property
sle_ota_data_ack(value_len, value) Server 回复固件数据接收 位图 ACK
sle_ota_reg_chan_data_report_cbk(cb) Server 注册 OTA 数据接收回调
ssapc_write_req() Client 写入控制指令(开始 OTA/结束传输/校验结果),含确认
ssapc_write_cmd() Client 快速写入固件数据包,无确认,提升速度
ssapc_register_callbacks() Client 注册 SSAP Client 回调,含 notification_cb 接收 ACK

Flash 写入和固件校验/Slot 切换使用 Update 中间件(upg.h),不在此文档详述。

案例说明

做什么

Client 将固件 .bin 文件通过 SLE 推送到 Server。Server 接收完整固件后执行 CRC32 校验,校验通过则写入 Boot 标志并重启进入新固件。

规格与功能

规格项 Server 端 Client 端
连接与配对 继承 hello-connect + hello-notify 的完整流程 连接成功后主动发起配对
MTU 520 字节,配对完成后协商 同 Server
PHY/MCS 4M PHY + MCS10(高吞吐模式) 同 Server
数据流向 Client -> Server(固件数据);Server -> Client(ACK) 主动推送固件,接收 ACK
分包策略 MTU - 4 = 516 字节/包 读取 .bin 文件,按 516 字节分包
ACK 机制 每 50 包回复位图 检查位图,重发丢失的包
校验方式 CRC32 全量校验 提供期望 CRC32 值
回滚策略 校验失败 -> 请求重发;超时 -> 回退原固件 收到重发请求 -> 重新推送
OTA Service 初始化 sle_ota_service_init() 一行完成

程序运行流程:

  1. Server 和 Client 建立连接、配对、MTU 协商、切换到高吞吐模式
  2. Server 调用 sle_ota_service_init() 初始化 OTA Service,注册数据接收回调
  3. Client 发送"开始 OTA"指令,附带固件大小和 CRC32 期望值
  4. Client 分批发送固件数据包(使用 ssapc_write_cmd 无确认模式加速)
  5. Server 每收满 50 包回复一次位图 ACK
  6. Client 检查 ACK 位图,重发丢失的包
  7. 所有包传输完成后,Server 执行 CRC32 校验
  8. 校验通过 -> 写入 Boot 标志 -> reboot;校验失败 -> 通知 Client 重新发送

前置条件:已完成连接、配对、服务发现,详见 hello-connect、hello-notify、hello-readwrite。

案例流程说明

本案例的完整运作分为五个阶段,以下从代码视角逐个拆解:

  1. 连接配对阶段(复用 hello-connect + hello-notify):广播 -> 连接 -> 配对 -> MTU 协商 -> 服务发现
  2. 高吞吐配置阶段(复用 high-throughput):双方切换到 4M PHY + MCS10
  3. OTA Service 就绪阶段(Server 独有):sle_ota_service_init() -> sle_ota_reg_chan_data_report_cbk() -> 等待 Client 发起
  4. 固件传输阶段(核心):Client 分包发送 -> Server 接收回调 -> 位图 ACK -> 丢包重发 -> 循环
  5. 校验与切换阶段:CRC32 -> Boot 标志 -> reboot(或重发)

案例操作指导

第一步:编译 Server 固件

.config 中设置:

CONFIG_SAMPLE_SUPPORT_SLE_OTA_SERVER=y
CONFIG_OTA_SUPPORT=y
fbb build ws63-liteos-app

第二步:编译 Client 固件

CONFIG_SAMPLE_SUPPORT_SLE_OTA_CLIENT=y
## 固件文件路径(相对于 SDK 目录)
CONFIG_OTA_FIRMWARE_PATH="output/ws63-liteos-app/firmware.bin"
fbb build ws63-liteos-app

第三步:烧录并运行

Server 先烧录旧版本固件并上电,Client 后上电。

第四步:观察 OTA 升级过程

Server 预期输出:

[sle ota server] connected.
[sle ota server] paired, mtu=520.
[sle ota server] phy set to 4M, ota service init done.
[sle ota server] OTA start: size=524288, crc32=0xA1B2C3D4
[sle ota server] receiving... [round 1] ack bitmap: 0xFFFFFFFFFFFF
[sle ota server] receiving... [round 2] ack bitmap: 0xFFFFFFFFFFFF (missing seq 63 -> retransmit)
[sle ota server] receiving... [round 10] transfer complete.
[sle ota server] verifying... crc32 pass!
[sle ota server] applying... boot flag set.
[sle ota server] rebooting...

Client 预期输出:

[sle ota client] connected.
[sle ota client] paired, mtu=520.
[sle ota client] phy set to 4M.
[sle ota client] firmware: 524288 bytes, 1016 packets.
[sle ota client] sending... round 1 done, ack: 50/50
[sle ota client] sending... round 2 done, ack: 49/50 -> retransmit seq=63
[sle ota client] all packets sent! total time: 1.8s
[sle ota client] server CRC32 pass, upgrade success!

关键配置

高吞吐 OTA 参数

// PHY 参数:4M 最大化吞吐
sle_set_phy_t phy_param = {0};
phy_param.tx_format        = SLE_RADIO_FRAME_4_M0;
phy_param.rx_format        = SLE_RADIO_FRAME_4_M0;
phy_param.tx_phy           = SLE_PHY_4M;
phy_param.rx_phy           = SLE_PHY_4M;
phy_param.tx_pilot_density = SLE_PHY_PILOT_DENSITY_16_TO_1;
phy_param.rx_pilot_density = SLE_PHY_PILOT_DENSITY_16_TO_1;

分包与 ACK 参数

配置项 推荐值 说明
MTU 520 字节 SLE 最大值,最大化单包效率
分片大小 516 字节/包 MTU - 4(OTA 协议头)
ACK 窗口 50 包 每 50 包回复一次位图,平衡效率和可靠性
位图大小 7 字节 50 位足矣(50 包最多 50 位 = 7 字节)
CRC 校验 CRC32 传输完成后全量校验
最大重试次数 3 次 校验失败超过 3 次则放弃,回退原固件
传输超时 30 秒 连续 30 秒无新数据则判定传输失败

ACK 窗口大小的权衡

窗口大小 优点 缺点 推荐场景
10 包 位图小,确认频繁,丢包快速发现 确认开销占比高(~10%) 极不稳定链路
50 包 平衡——确认开销 ~2%,丢包发现及时 暂无 推荐,本案例默认
200 包 确认开销极低 位图大,丢包发现延迟,一轮重发量大 极稳定链路

50 包的窗口大小是实验后的经验值。如果实测丢包率持续 < 1%,可以放大到 100 包以进一步减少确认开销。

代码详解

1. Server 端 OTA 初始化

sle_ota_service_init() 一行完成 OTA Service 的注册——内部自动创建 Service UUID、添加 OTA 数据 Property 和 CCCD 描述符。应用层只需关注数据接收回调。

// OTA Service 初始化 —— 一行搞定,无需手动注册 Service/Property
static errcode_t ota_server_init(uint8_t server_id)
{
    // 1. 初始化 OTA Service(自动注册 SSAP Service + Property + CCCD)
    errcode_t ret = sle_ota_service_init(server_id);
    if (ret != ERRCODE_SUCC) {
        printf("[ota] sle_ota_service_init failed: %d\n", ret);
        return ret;
    }

    // 2. 注册 OTA 数据接收回调
    ret = sle_ota_reg_chan_data_report_cbk(ota_data_receive_cb);
    if (ret != ERRCODE_SUCC) {
        printf("[ota] register data cb failed: %d\n", ret);
        return ret;
    }

    printf("[ota] service init done, waiting for client...\n");
    return ERRCODE_SUCC;
}

sle_ota_service_init() 是专门为 OTA 场景封装的便捷 API。如果手动用 ssaps_add_service_sync() + ssaps_add_property_sync() 搭建,代码量约为 30~50 行。一行 API 省去了 UUID 定义、权限配置、描述符选择等重复劳动。

2. OTA 数据接收回调

数据回调在 BTS 线程上下文中被调用,不能做长时间阻塞操作。回调中解析 OTA 协议头,将 payload 写入 Flash Buffer。

#define OTA_HEADER_SIZE   4   // type(1B) + seq(2B) + total(1B)
#define OTA_PAYLOAD_SIZE  516 // MTU(520) - header(4)
#define ACK_WINDOW        50  // 每 50 包回复一次 ACK

static uint32_t g_ota_seq_bitmap = 0;    // 位图:每 bit 标记一个包的接收状态
static uint32_t g_ota_pkt_count  = 0;    // 本窗口累计包数
static uint32_t g_ota_total_pkts = 0;    // 固件总包数
static bool     g_ota_in_progress = false;

static errcode_t ota_data_receive_cb(const uint8_t *data, uint16_t data_len)
{
    if (data == NULL || data_len <= OTA_HEADER_SIZE) {
        return ERRCODE_FAIL;
    }

    // 1. 解析 OTA 协议头
    uint8_t  type  = data[0];
    uint16_t seq   = (data[1] << 8) | data[2];
    uint8_t  total = data[3];
    const uint8_t *payload = data + OTA_HEADER_SIZE;
    uint16_t payload_len   = data_len - OTA_HEADER_SIZE;

    // 2. 处理控制指令
    if (type == 0x02) {  // 控制指令
        if (seq == 0xFFFF) {  // 开始 OTA
            g_ota_total_pkts = total;
            g_ota_in_progress = true;
            g_ota_pkt_count = 0;
            g_ota_seq_bitmap = 0;
            upg_erase_ota_partition();  // 擦除 OTA 分区
            printf("[ota] start: total=%d pkts\n", g_ota_total_pkts);
            return ERRCODE_SUCC;
        } else if (seq == 0xFFFE) {  // 传输完成,触发校验
            g_ota_in_progress = false;
            osal_send_event(OTA_TASK_ID, OTA_EVT_VERIFY);  // 异步通知主任务
            printf("[ota] transfer complete, verifying...\n");
            return ERRCODE_SUCC;
        }
    }

    // 3. 处理数据包(type == 0x01)
    if (!g_ota_in_progress) {
        return ERRCODE_FAIL;  // 未在 OTA 状态,忽略
    }

    // 4. 写入 Flash Buffer(upg 中间件接口)
    uint32_t offset = seq * OTA_PAYLOAD_SIZE;
    upg_write_data(offset, payload, payload_len);

    // 5. 标记位图:该序号已收到
    g_ota_seq_bitmap |= (1 << (seq % ACK_WINDOW));
    g_ota_pkt_count++;

    // 6. 收到 ACK_WINDOW 包时回复 ACK
    if (g_ota_pkt_count >= ACK_WINDOW) {
        ota_send_ack();
        g_ota_pkt_count = 0;
        g_ota_seq_bitmap = 0;
    }

    return ERRCODE_SUCC;
}

回调在 BTS 线程上下文中执行,不可调用阻塞 API。Flash 写入通过 upg_write_data() 接口完成——它对 BTS 上下文是安全的(非阻塞、队列化写入)。校验触发通过 osal_send_event() 异步通知主任务,避免在回调中做耗时 CRC32 计算。

3. ACK 回复

每收满一轮后,Server 通过 sle_ota_data_ack() 将位图发回 Client。

// ACK 数据格式: [window_start_seq:2B][bitmap:7B]
#define ACK_PAYLOAD_SIZE 9  // 2 + 7

static void ota_send_ack(void)
{
    uint8_t ack_data[ACK_PAYLOAD_SIZE] = {0};
    // 窗口起始序号(低字节在前)
    uint16_t window_start = (g_ota_pkt_count / ACK_WINDOW) * ACK_WINDOW;
    ack_data[0] = window_start & 0xFF;
    ack_data[1] = (window_start >> 8) & 0xFF;
    // 位图(50 bits = 7 bytes)
    memcpy(&ack_data[2], &g_ota_seq_bitmap, 7);

    sle_ota_data_ack(ACK_PAYLOAD_SIZE, ack_data);
    printf("[ota] ack sent: window=%d, bitmap=0x%08X\n",
           window_start, g_ota_seq_bitmap);
}

位图格式:window_start 标识本轮起始包序号,bitmap 的 bit[i] = 1 表示序号为 window_start + i 的包已收到。Client 据此计算丢失包的精确序号并重发。

4. Client 端固件推送

Client 读取 .bin 文件,分包后使用 ssapc_write_cmd() 连续发送。write_cmd 无底层确认(比 write_req 更快),可靠性由应用层 ACK 保证。

#define CHUNK_SIZE (MTU_SIZE - OTA_HEADER_SIZE)  // 516 bytes

static errcode_t ota_client_push_firmware(const char *fw_path)
{
    // 1. 读取固件文件 + 计算 CRC32
    uint32_t fw_size;
    uint8_t *fw_data = read_firmware_file(fw_path, &fw_size);
    uint32_t expected_crc = crc32_compute(fw_data, fw_size);
    uint16_t total_pkts = (fw_size + CHUNK_SIZE - 1) / CHUNK_SIZE;

    // 2. 发送"开始 OTA"控制指令
    uint8_t start_cmd[OTA_HEADER_SIZE] = {
        0x02,               // type: control
        0xFF, 0xFF,         // seq: 0xFFFF 表示"开始"
        (uint8_t)total_pkts // total: 总包数
        // CRC32 值通过单独属性写入,此处省略
    };
    ssapc_write_req(g_conn_id, g_ota_ctrl_handle,
                    SSAP_PROPERTY_TYPE_VALUE,
                    start_cmd, sizeof(start_cmd));

    // 3. 分包发送
    uint8_t ota_packet[MTU_SIZE];
    uint16_t seq = 0;
    uint32_t tx_start_ms = osal_get_cur_time_ms();

    while (seq < total_pkts) {
        uint32_t offset = seq * CHUNK_SIZE;
        uint16_t chunk_len = (offset + CHUNK_SIZE > fw_size)
                             ? (fw_size - offset) : CHUNK_SIZE;

        ota_packet[0] = 0x01;                // type: data
        ota_packet[1] = (seq >> 8) & 0xFF;   // seq high byte
        ota_packet[2] = seq & 0xFF;          // seq low byte
        ota_packet[3] = (uint8_t)total_pkts;  // total
        memcpy(&ota_packet[OTA_HEADER_SIZE], &fw_data[offset], chunk_len);

        errcode_t ret = ssapc_write_cmd(g_conn_id, g_ota_data_handle,
                                         SSAP_PROPERTY_TYPE_VALUE,
                                         ota_packet,
                                         OTA_HEADER_SIZE + chunk_len);
        if (ret == ERRCODE_SLE_BUSY) {
            osal_sleep_ms(1);  // 协议栈忙,等 1ms 再试
            continue;
        } else if (ret != ERRCODE_SUCC) {
            printf("[ota] write_cmd failed at seq=%d: %d\n", seq, ret);
        } else {
            seq++;
        }
    }

    uint32_t elapsed = osal_get_cur_time_ms() - tx_start_ms;
    printf("[ota] all %d packets sent in %d ms\n", total_pkts, elapsed);

    // 4. 发送"传输完成"控制指令
    uint8_t done_cmd[OTA_HEADER_SIZE] = {
        0x02, 0xFE, 0xFF, 0x00  // type+seq=0xFFFE
    };
    ssapc_write_req(g_conn_id, g_ota_ctrl_handle,
                    SSAP_PROPERTY_TYPE_VALUE,
                    done_cmd, sizeof(done_cmd));

    free(fw_data);
    return ERRCODE_SUCC;
}

为什么用 ssapc_write_cmd 而不是 ssapc_write_req 来发送数据包?write_cmd 不等待底层确认,单包延迟约 0.5ms;write_req 等确认后返回,单包延迟约 2ms。以 1000 包为例:cmd 模式 ~0.5s,req 模式 ~2s——4 倍差距。丢包由应用层位图 ACK 兜底,底层确认不是必需的。

发送方式 单包延迟 1000 包总耗时 可靠性保证 适用场景
ssapc_write_cmd ~0.5 ms ~0.5 s 应用层 ACK 大数据量推送(推荐)
ssapc_write_req ~2 ms ~2 s 底层确认 + 应用层 ACK 小数据量、控制指令

5. 固件校验与切换

校验逻辑在主任务中异步执行(通过 osal_send_event 触发),避免阻塞 BTS 线程。

#define MAX_VERIFY_RETRIES 3

static uint8_t g_verify_retries = 0;

static void ota_verify_and_apply(void)
{
    printf("[ota] computing CRC32...\n");

    // 1. 从 Flash 读取已写入的固件,计算 CRC32
    uint32_t computed_crc = upg_compute_firmware_crc32();

    if (computed_crc == g_expected_crc32) {
        // 2. CRC32 通过 -> 写入 Boot 标志 -> 重启
        printf("[ota] CRC32 pass! applying...\n");
        upg_set_boot_flag(UPG_SLOT_NEW);  // 设置启动标志指向新固件
        printf("[ota] boot flag set, rebooting in 1s...\n");
        osal_sleep_ms(1000);
        reboot();  // 系统重启,Bootloader 加载新固件
    } else {
        // 3. CRC32 失败 -> 重试或放弃
        g_verify_retries++;
        printf("[ota] CRC32 mismatch! expected=0x%08X got=0x%08X, retry=%d/%d\n",
               g_expected_crc32, computed_crc,
               g_verify_retries, MAX_VERIFY_RETRIES);

        if (g_verify_retries >= MAX_VERIFY_RETRIES) {
            printf("[ota] max retries reached, OTA failed. Rolling back...\n");
            upg_erase_ota_partition();  // 清除不完整固件
            g_ota_in_progress = false;
            g_verify_retries = 0;
        } else {
            // 通知 Client 重新发送
            ota_send_retry_notification();
        }
    }
}
校验结果 Server 行为 Client 行为 系统影响
CRC32 一次通过 写入 Boot 标志 -> reboot 收到成功通知 -> 结束 进入新固件
CRC32 失败(重试 1~3 次) 请求 Client 重发 重新推送全部固件 仍在旧固件运行
CRC32 失败(超过最大重试) 清除 OTA 分区 -> 放弃升级 收到失败通知 保持旧固件,设备正常运行

回滚安全由 Bootloader 保证:即使 APPLYING 阶段断电,Bootloader 在启动时会检查新固件分区的完整性标志。若标志不完整,自动从旧固件分区启动。