OTA
OTA
SLE OTA Service、SSAP 通知/写入、upg 中间件
学习目标
- 理解 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: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() 一行完成 |
— |
程序运行流程:
- Server 和 Client 建立连接、配对、MTU 协商、切换到高吞吐模式
- Server 调用
sle_ota_service_init()初始化 OTA Service,注册数据接收回调 - Client 发送"开始 OTA"指令,附带固件大小和 CRC32 期望值
- Client 分批发送固件数据包(使用
ssapc_write_cmd无确认模式加速) - Server 每收满 50 包回复一次位图 ACK
- Client 检查 ACK 位图,重发丢失的包
- 所有包传输完成后,Server 执行 CRC32 校验
- 校验通过 -> 写入 Boot 标志 -> reboot;校验失败 -> 通知 Client 重新发送
前置条件:已完成连接、配对、服务发现,详见 hello-connect、hello-notify、hello-readwrite。
案例流程说明
本案例的完整运作分为五个阶段,以下从代码视角逐个拆解:
- 连接配对阶段(复用 hello-connect + hello-notify):广播 -> 连接 -> 配对 -> MTU 协商 -> 服务发现
- 高吞吐配置阶段(复用 high-throughput):双方切换到 4M PHY + MCS10
- OTA Service 就绪阶段(Server 独有):
sle_ota_service_init()->sle_ota_reg_chan_data_report_cbk()-> 等待 Client 发起 - 固件传输阶段(核心):Client 分包发送 -> Server 接收回调 -> 位图 ACK -> 丢包重发 -> 循环
- 校验与切换阶段:CRC32 -> Boot 标志 -> reboot(或重发)
案例操作指导
第一步:编译 Server 固件
在 .config 中设置:
第二步:编译 Client 固件
CONFIG_SAMPLE_SUPPORT_SLE_OTA_CLIENT=y
## 固件文件路径(相对于 SDK 目录)
CONFIG_OTA_FIRMWARE_PATH="output/ws63-liteos-app/firmware.bin"
第三步:烧录并运行
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 在启动时会检查新固件分区的完整性标志。若标志不完整,自动从旧固件分区启动。