Skip to content

LV013-升级通道协议 ​

一、 通道的角色与分层原则 ​

1. 通道在 OTA 中的位置 ​

前文的方案多以"网络通道"为背景,但嵌入式 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. 选型维度对比 ​

通道速率量级距离典型场景关键约束
UART10~100 KB/s数米开发调试、产线烧录、模组透传需设计帧协议与流控
USB DFU1~10 MB/s数米消费类设备本地升级、量产救砖需要 USB 外设与上位机
CAN1~5 KB/s数十米汽车 ECU、工业总线帧短(8/64 字节),需分帧重组
BLE5~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 私有分块协议设计 ​

生产环境更常用私有协议,以便携带版本、偏移与校验信息。一个精简帧格式:

text
┌──────┬──────┬────────┬────────┬──────┬───────────┬────────┐
│SOH   │CMD   │SEQ     │OFFSET  │LEN   │DATA       │CRC16   │
│0x01  │1字节  │2字节   │4字节    │2字节  │0~512字节  │2字节    │
└──────┴──────┴────────┴────────┴──────┴───────────┴────────┘

CMD 建议:0x10 查询版本、0x20 开始升级、0x21 数据块、0x22 结束与校验、0x23 请求重传。接收端状态机:

c
// 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 主控 + 模组透传的完整时序如下:

MCU 分块下载时序

时序里的每一块都构成一个最小事务:"请求 → 收数 → CRC → 写 Flash → 更新断点",任何一步失败只重试本块,不回退整个下载。

这个事务划分正是第一节分层原则的落地:事务的边界(分块大小、CRC 算法、断点记录格式)由升级逻辑定义,模组只是事务里"取数据"那一步的执行者。换用另一款模组时,只有 AT 指令序列需要重写,事务逻辑一行不改。

写 Flash 那一步的擦除与对齐约束见 LV007-升级包格式与传输机制 第四节。


本文档由 markdowncli 技能辅助生成