LV013-升级通道协议
一、 通道的角色与分层原则
1. 通道在 OTA 中的位置
前文的方案多以"网络通道"为背景,但嵌入式 OTA 的升级包并不一定来自云端服务器。先建立一个全景视角:无论包从哪里来,通道的职责始终只是把字节流可靠地送达设备,之后的校验、写槽、选槽、回滚流程与通道无关。
按包源距离,通道分两大类,本篇第二、三节分别展开:
- 近场通道:包源在设备旁边(调试电脑、手机、产线工位),通过 UART / USB / CAN / BLE 直传
- 网络通道:包源在远程服务器,经 Wi-Fi / 4G / 以太网下发
2. 通道与升级逻辑必须解耦
"通道只搬运字节"不是一句口号,而是必须落到代码组织上的分层约束。无论选哪条通道,下面五条都要守住:
(1)通道层只搬运字节:它不知道什么是版本、什么是槽位、什么是签名,向上只提供 read(offset, len) 或 recv(buf, len) 的抽象
(2)升级逻辑与通道无关:OTA 代理通过统一接口取数据,UART / USB / BLE / 网络模组可以互换而不改动上层
(3)每通道一个实现文件:ota_uart.c、ota_usb.c、ota_ble.c……各自实现统一接口,便于裁剪与维护
(4)断点信息不依赖通道:断点记录存放在参数区 / 下载记录里(LV007-升级包格式与传输机制),换通道或断连重连都能续传
(5)救砖通道与日常通道分离:日常走网络或 BLE;救砖走 USB DFU 或 UART YMODEM,两者互不干扰
这五条是后面所有协议实现的共同前提:各通道的帧格式差异极大,但只要都收敛到同一组接口,上层升级逻辑就一份代码通用。
3. 选型维度对比
| 通道 | 速率量级 | 距离 | 典型场景 | 关键约束 |
|---|---|---|---|---|
| UART | 10~100 KB/s | 数米 | 开发调试、产线烧录、模组透传 | 需设计帧协议与流控 |
| USB DFU | 1~10 MB/s | 数米 | 消费类设备本地升级、量产救砖 | 需要 USB 外设与上位机 |
| CAN | 1~5 KB/s | 数十米 | 汽车 ECU、工业总线 | 帧短(8/64 字节),需分帧重组 |
| BLE | 5~50 KB/s | 十米级 | 可穿戴、传感节点 | MTU 小、功耗敏感、需断点续传 |
| Wi-Fi 模组 | 100 KB/s~MB/s | 远程 | 家用 IoT | 依赖模组固件稳定性 |
| 4G 模组 | 100 KB/s | 远程 | 无 Wi-Fi 场景的工业/车载 | 流量计费、信号抖动 |
二、 近场通道的协议实现
1. 串口:YMODEM 与私有分块协议
1.1 YMODEM 协议要点
YMODEM(以及更老的 XMODEM)是串口文件传输的经典协议,至今仍被 SecureCRT、Tera Term、xshell 等终端软件原生支持,用现有工具就能完成升级,非常适合开发与产线:
- 帧结构:
SOH/STX + 帧号 + 帧号反码 + 128/1024 字节数据 + CRC16 - 首帧是文件名与文件大小,设备端可借此预判升级包规模
- 应答式流控:每帧需要
ACK,出错重传NAK,自然具备可靠性 - 128/1024 两种块长,128 兼容性好、1024 效率高,由接收方选
实现建议:Bootloader 内置一个精简 YMODEM 接收器(几百行 C 即可),作为恢复通道——两槽全毁时设备还能被救回来(见 LV016-选槽与回滚机制 的恢复模式)。
1.2 私有分块协议设计
生产环境更常用私有协议,以便携带版本、偏移与校验信息。一个精简帧格式:
┌──────┬──────┬────────┬────────┬──────┬───────────┬────────┐
│SOH │CMD │SEQ │OFFSET │LEN │DATA │CRC16 │
│0x01 │1字节 │2字节 │4字节 │2字节 │0~512字节 │2字节 │
└──────┴──────┴────────┴────────┴──────┴───────────┴────────┘CMD 建议:0x10 查询版本、0x20 开始升级、0x21 数据块、0x22 结束与校验、0x23 请求重传。接收端状态机:
// uart_ota.c —— 私有分块协议接收状态机(核心逻辑)
#include <stdint.h>
#include <string.h>
#define FRAME_SOH 0x01u
#define CMD_QUERY 0x10u
#define CMD_START 0x20u
#define CMD_DATA 0x21u
#define CMD_END 0x22u
#define DATA_MAX 512u
typedef enum {
ST_WAIT_SOH,
ST_CMD,
ST_SEQ,
ST_OFFSET,
ST_LEN,
ST_DATA,
ST_CRC,
} rx_state_t;
typedef struct {
rx_state_t state;
uint8_t cmd;
uint16_t seq;
uint32_t offset;
uint16_t len;
uint8_t data[DATA_MAX];
uint16_t data_idx;
uint8_t crc[2];
} ota_rx_t;
/* 每收到 1 字节调用一次;返回 1 表示成功处理了一帧 */
int ota_rx_feed(ota_rx_t *r, uint8_t byte, uint16_t *out_ack_seq)
{
switch (r->state) {
case ST_WAIT_SOH:
if (byte == FRAME_SOH) { r->state = ST_CMD; }
break;
case ST_CMD:
r->cmd = byte;
r->state = ST_SEQ;
r->data_idx = 0;
break;
case ST_SEQ:
((uint8_t *)&r->seq)[r->data_idx++] = byte;
if (r->data_idx == 2) { r->state = ST_OFFSET; r->data_idx = 0; }
break;
case ST_OFFSET:
((uint8_t *)&r->offset)[r->data_idx++] = byte;
if (r->data_idx == 4) { r->state = ST_LEN; r->data_idx = 0; }
break;
case ST_LEN:
((uint8_t *)&r->len)[r->data_idx++] = byte;
if (r->data_idx == 2) {
r->state = (r->len > 0) ? ST_DATA : ST_CRC;
r->data_idx = 0;
}
break;
case ST_DATA:
r->data[r->data_idx++] = byte;
if (r->data_idx >= r->len) { r->state = ST_CRC; r->data_idx = 0; }
break;
case ST_CRC:
r->crc[r->data_idx++] = byte;
if (r->data_idx == 2) {
uint16_t crc_calc = crc16(r->data, r->len); /* 省略实现 */
uint16_t crc_recv = (r->crc[0] << 8) | r->crc[1];
r->state = ST_WAIT_SOH;
if (crc_calc == crc_recv) {
*out_ack_seq = r->seq;
return 1; /* 一帧处理成功 */
}
return -1; /* CRC 错误,请求重传 */
}
break;
default:
r->state = ST_WAIT_SOH;
break;
}
return 0;
}【注意】
私有协议的 OFFSET 字段是断点续传的关键:上位机断线重连后,先查询设备已收到的偏移,从断点继续而不是从头发送。
2. USB:DFU
USB DFU(Device Firmware Upgrade)是 USB 官方定义的固件升级类规范,Linux / Windows / macOS 均有现成上位机(dfu-util),是消费类设备本地升级的事实标准:
- 设备以 DFU 类描述符声明"可被升级",上位机按 DFU 状态机(
IDLE → DNLOAD_SYNC → DNLOAD_BUSY → MANIFEST)下发 - 数据按块(通常 256 B~2 KB)传输,每块需要
GETSTATUS轮询确认 - 两种实现方式:MCU 内置 USB 设备固件实现 DFU;或利用 SoC BootROM 的 DFU 支持(很多 STM32 / NXP 芯片的 BootROM 原生支持 USB DFU,无需任何代码)
工程建议:作为救砖与产线通道使用 DFU 极其合适——免驱动、速度快、上位机现成。不建议用它做日常 OTA(距离太短、需要人工操作)。
3. CAN:UDS
3.1 UDS 的升级相关服务
汽车与工业总线场景下,CAN 升级走 UDS(ISO 14229)诊断协议,关键服务:
| 服务 | SID | 作用 |
|---|---|---|
| 会话控制 | 0x10 | 进入编程会话(Programming Session) |
| 安全访问 | 0x27 | 种子-密钥解锁,防止未授权刷写 |
| 请求下载 | 0x34 | 告知目标地址与大小,协商分块长度 |
| 传输数据 | 0x36 | 按块下发固件数据 |
| 传输退出 | 0x37 | 告知传输结束 |
| 例程控制 | 0x31 | 触发擦除、校验、激活新固件等例程 |
3.2 实现要点
- 分帧重组:CAN 单帧数据域只有 8 字节(CAN FD 64 字节),大固件块需用 ISO-TP(ISO 15765-2)做分帧与重组
- 流控:ISO-TP 的流控帧(FC)负责限速,避免接收方缓冲溢出
- 并行会话:升级期间普通诊断会话与编程会话并存,需要会话管理
- 安全访问:
0x27的种子-密钥算法是防未授权刷写的最后一道闸门,密钥算法不能硬编码在可被读出的固件里
4. BLE:nRF DFU
BLE 是可穿戴与电池供电设备的主流 OTA 通道,Nordic 的 DFU 方案是行业事实标准:
- 分包结构:升级包(zip)包含固件 bin、
dat初始化包(版本、哈希、签名等元数据) - 传输过程:先传
dat建立升级上下文,再分块传 bin;每块有 CRC 校验 - 断点续传:连接断开后再连时,设备上报已收到的块数,从断点继续
- 双 Bank / 单 Bank:nRF52 系列通常双 Bank(新固件先写另一 Bank,校验后切换),与 LV025-MCU有BankSwap落地方案 思想一致
- 安全:官方 DFU 支持签名校验(ECDSA),务必开启,否则 BLE 范围内的任何人都能刷机
非 Nordic 平台(如 TI CC26xx、ESP32)同样有各自的 OTA Profile,设计原则相同:小包分块、块级 CRC、断点续传、签名校验。
三、 网络通道:模组透传与 AT 指令
MCU 自身没有协议栈时,Wi-Fi / 4G 模组通过 AT 指令提供网络能力。这是网络通道在 MCU 上最主要的形态,也是"通道与升级逻辑解耦"最容易被破坏的地方——很多方案把升级状态机写进了模组,结果升级逻辑被模组固件绑架。
1. 职责划分:模组做转发,MCU 做状态机
- 模组做转发,MCU 做状态机:升级逻辑(版本判断、分块、校验、断点记录)全部在 MCU,模组只负责"把一段数据取回来"
- HTTP Range:优先让模组支持 Range 请求,实现真正的断点续传;不支持则只能在模组侧缓存整段再转交,受模组内存限制
- 流控:模组的 UART 输出速度可能快于 MCU 处理速度,需要硬件流控(RTS/CTS)或主动限速
- 异常恢复:模组掉线、AT 响应超时都要能在 MCU 侧超时检测并重新发起,而不是死等
2. 分块下载时序
MCU 主控 + 模组透传的完整时序如下:
时序里的每一块都构成一个最小事务:"请求 → 收数 → CRC → 写 Flash → 更新断点",任何一步失败只重试本块,不回退整个下载。
这个事务划分正是第一节分层原则的落地:事务的边界(分块大小、CRC 算法、断点记录格式)由升级逻辑定义,模组只是事务里"取数据"那一步的执行者。换用另一款模组时,只有 AT 指令序列需要重写,事务逻辑一行不改。
写 Flash 那一步的擦除与对齐约束见 LV007-升级包格式与传输机制 第四节。
本文档由 markdowncli 技能辅助生成