Files
vd_960/docs/DLD960_MQTT_OTA协议.md
T
wangfq b20fc57954 docs(vd_960): MQTT 远程 OTA 协议设计稿 (Loop MCU 先存后刷, ROADMAP P1.4 ①)
- 三阶段: MQTT 分片下载→W25Qxx 暂存(0x010000 512KB)→本地 0x9F ISP 透传刷写
- 7 命令: ota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status/ota_report
  + event_report 扩展 type=ota_error 告警
- 关键参数: 单片 256B(hex, <RECV_BUF 1024), 刷写块 248B(0x9F LEN uint8 上限 254B),
  镜像上限 60KB, CRC32=ISO-HDLC(zlib.crc32 兼容), Slot A/B 双槽回滚
- 安全底线: 升级窗口检查(有车拒绝)/继电器安全侧/双重 CRC/失败重试×3/审计留痕
- 6 项待板上验证(继电器默认态/DBN TX 缓冲/SPI 写阻塞/RAM 预算/bin 大小/MQTT 保活)

协议先行, 未动固件代码; 待设计拍板后并入 MQTT 协议 V1.08
2026-08-20 11:13:43 +08:00

22 KiB
Raw Blame History

DLD960 MQTT 远程 OTA 协议(Loop MCU 先存后刷)

文档版本 V1.00(设计稿)· 2026-08-20 · 适用范围:并入《DLD960 IoT 接口协议(MQTT + JSON)》V1.08 后生效 依据:docs/ROADMAP.md P1.4 ① —— Loop MCU (AT32F421) 远程 OTAv1.2.x 落地 设计原则:下载阶段纯 MQTT 分片 → W25Qxx 暂存;刷写阶段纯本地 ISP 透传(复用 BLE OTA 0x9F 状态机);传输层无关,4G 通道原样复用。


1 背景与目标

1.1 现状基线

说明
双 MCU 架构 DBN = CH32V208(通信 MCU,跑 MQTT/BLE/TCP+ Loop = AT32F421(地感 MCU,跑检测算法)
现有 OTA 通道 BLE → DBN 透传 → UART2 0x9F ISP → Loop bootloader2026-08-19 修复闭环,V1.02.03
本轮目标 MQTT → DBN 分片下载到 W25Qxx 暂存区 → 校验后本地 ISP 刷写 Loop
暂存介质 DBN 外挂 W25QxxSPI1),OTA 镜像暂存区 0x010000 起 512KBSNAP_FIXED_SIZE = 参数区64KB + OTA区512KB,已在 snapshot.c 分区模型预留)

1.2 为什么"先存后刷"(相对 BLE 流式直透)

维度 流式直透(BLE 现状) 先存后刷(本设计)
网络抖动影响 任一分片丢失 → 停等卡死,全流程重来 下载阶段断网无所谓,断点续传
刷写窗口 与传输时间重合(网络慢则窗口长) 下载完成后刷写纯本地,窗口短且可控
校验 依赖传输层(BLE 无 CRC 单片 CRC32 + 全镜像 CRC32 双重校验
回滚 暂存区 Slot A/B 双槽,保留上一版重刷
安全窗口 无检查 刷写前检查无车压线圈、继电器强制断开

1.3 范围

  • 本版只做 Loop (AT32F421) 远程 OTAROADMAP P1.4 ①)
  • DBN (CH32V208) 自身 OTAROADMAP P1.4 ②):需 bootloader 改造,单独评审,不阻塞本设计;协议命令预留 target:"dbn"
  • 传输层:本版 MQTT;4G(UART1 AT 透传)复用同一分片下载协议(target 字段扩展即可)

2 总体架构与数据流

┌─────────┐  MQTT (dld960/{sn}/srv)   ┌──────────────┐  UART2 192000  ┌─────────────┐
│  平台    │ ── ota_begin / ota_data ─▶ │  DBN (CH32V208) │ ── 0x9F ISP ──▶ │ Loop (AT32)  │
│         │ ◀─ 响应 / ota_report ───── │              │ ◀─ ACK ──────── │ bootloader  │
└─────────┘                           │  W25Qxx 暂存   │                └─────────────┘
                                      │  0x010000 512KB │
                                      └──────────────┘
    阶段1 下载:平台 MQTT 分片 → DBN 写 W25Qxx(单片 CRC32 校验)
    阶段2 校验:ota_end 全镜像 CRC32 复核,state → ready
    阶段3 刷写:ota_flash → DBN 从 W25Qxx 读出 → 0x9F A5/A6/A7 逐块透传 → Loop bootloader 写 flash

镜像语义:平台下发的是 bin 原始字节hex 编码传输),即 AT32F421 0x08003400 起的 APP 代码映像(hex→bin 转换由平台侧完成,设备不解析 Intel HEX)。刷写时 A6 地址固定 0x08003400=APP_START_ADDR),A7 数据按 bin 顺序发送。


3 关键参数

3.1 协议参数(平台 ↔ 设备契约)

参数 依据
下载单片原始字节 256B 2 的幂 + W25Qxx 页(256B)对齐;hex 512 字符 + JSON 外壳 ≈ 640B < IOT_MQTT_RECV_BUF_LEN=1024
单片 CRC CRC32256B 独立计算) 与全镜像同算法,查表实现复用
全镜像 CRC CRC32offset 0 ~ size-1 连续) 标准 CRC-32/ISO-HDLC
镜像上限 60KB Loop APP 区 0x08003400~0x08010000 = 51KB 硬上限;暂存 Slot 数据区 60KB 留余量
下载片序号语义 offset 绝对字节偏移(256 对齐) 支持乱序/重复片幂等处理
断点续传 ota_begin 返回已接收字节数(片对齐) 设备持久化 received 到元数据

3.2 刷写参数(设备内部实现约束,平台无需感知)

参数 依据
A7 数据块 ≤254B(实现取 248B bootloader LEN 为 uint8 → DATA = LEN-1 ≤ 254iap.c CMD_9F_DATA_LEN
块间超时 1s(可配) 0x9F 停等协议,超时重发该块
刷写重试 每块失败重发 ×3,整体失败重试 ×3 ROADMAP 安全底线
刷写速率 248B/帧 ≈ 13ms + ACK~20ms/帧 64KB ≈ 265 帧 ≈ 6~8s 窗口
升级触发 DBN 发 9F 01 00 01 A5 A7 → Loop APP 写 IAP_UPGRADE_FLAG_9F=0x444C4439 → 复位 复用 BLE OTA 启动帧(dbn_ble_srv.c check_pkg 同款)

3.3 CRC32 算法定义(必须双方一致)

  • 标准 CRC-32/ISO-HDLCpoly 0x04C11DB7reflected 0xEDB88320),init 0xFFFFFFFFrefin/refout truexorout 0xFFFFFFFF
  • 平台侧 Python zlib.crc32() / binascii.crc32() 即此算法,直接可用;设备侧查表法实现
  • 全镜像 CRC = 从 offset 0 连续计算 size 字节;单片 CRC = 仅该 256B
  • JSON 中 crc32 字段以十进制无符号数传输(与现有 uint32 字段风格一致)

4 暂存区布局(设备内部实现,命令接口不依赖)

0x010000  ┌─────────────────────────┐
          │  OTA 元数据扇区 (4KB)     │  SlotA/SlotB 头 + 会话状态 + 审计
0x010400  ├─────────────────────────┤
          │  Slot A 数据区 (60KB)     │  当前新镜像(bin 原始字节)
0x020400  ├─────────────────────────┤
          │  Slot B 数据区 (60KB)     │  上一版镜像(回滚重刷)
0x030400  ├─────────────────────────┤
          │  预留 384KB              │  DBN 自身镜像 / 4G 扩展
0x090000  └─────────────────────────┘

元数据结构(建议,实现可调,不进入协议字段):

typedef struct {          /* 64B */
    uint32_t magic;       /* 'DLD9' */
    uint32_t state;       /* idle/downloading/ready/flashing/flash_failed/aborted */
    uint8_t  target;      /* 0=loop, 1=dbn(预留) */
    uint8_t  slot;        /* 0=A, 1=B */
    uint16_t rsv;
    uint32_t size;        /* 镜像字节数 */
    uint32_t crc32;       /* 全镜像 CRC32 */
    uint32_t received;    /* 已下载字节数(片对齐,断点续传依据) */
    uint32_t last_result; /* 上次刷写结果码 */
    uint32_t begin_ts;    /* 会话开始(同步后 Unix 秒,0=未同步) */
    char     version[16]; /* 目标固件版本字符串(审计) */
    uint32_t flash_cnt;   /* 刷写尝试次数 */
    uint32_t rsv2;
} OtaMeta;

掉电恢复:元数据每次 ota_begin / 每片落盘 / ota_end / 刷写结果后回写。重启后:

  • state=downloading → 平台 ota_begin 时返回已接收 offset 续传
  • state=ready → 平台 ota_begin 直接确认可刷写,或 ota_flash 直接触发
  • state=flashing → 视为上次刷写中断(掉电/异常),可重试 ota_flash

5 命令详表(新增,并入 MQTT 协议 V1.08)

cmd 方向 说明
ota_begin srv→dev 开启 OTA 会话 / 断点续传定位
ota_data srv→dev 分片下发(256B/片,单片 CRC32)
ota_end srv→dev 结束下载,全镜像 CRC32 复核
ota_abort srv→dev 中止会话,释放暂存
ota_flash srv→dev 触发本地 ISP 刷写(仅 state=ready
ota_status srv→dev 查询 OTA 状态(含进度)
ota_report dev→srv 刷写进度/结果主动上报(QoS 1
event_report dev→srv 扩展 type=ota_error:重试 ×3 仍失败告警(平台必答,复用 §5.3 ACK 闭环)

兼容:老固件(V1.07 及以下)收到 ota_* → 现有 default 分支回 code=4 unsupported command。平台下发前可用 ota_statusdev_info_query.soft_ver 探测能力,避免每命令必报错。

5.1 开启会话 ota_begin

Topic: dld960/{sn}/srv

请求:

{
  "msg_id": 401,
  "cmd": "ota_begin",
  "ts": 1719000000,
  "data": {
    "target": "loop",
    "size": 46864,
    "crc32": 305419896,
    "version": "1.1.0",
    "force": false
  }
}
data 字段 类型 说明
target string loop(当前支持);dbn 预留
size uint32 镜像 bin 字节数(≤ 61440
crc32 uint32 全镜像 CRC32(十进制)
version string 目标固件版本(写入元数据,审计用;bootloader 不校验版本)
force bool true = 覆盖现有暂存镜像 / 忽略冲突(默认 false)

设备行为:

  1. 读元数据:若已有镜像且 size+crc32 与本次一致 →
    • state=ready → 返回 offset=size(平台可直接 ota_flash
    • state=downloading → 返回 offset=received(续传)
  2. 不一致 → 分配 Slot(A 当前 / B 回滚),写元数据 state=downloading, received=0,返回 offset=0
  3. force=false 且目标版本 == 当前运行版本 → 返回 code=1(防重复刷写,可 force 绕过)

响应 data

{
  "msg_id": 401,
  "cmd": "ota_begin",
  "ts": 1719000001,
  "code": 0,
  "msg": "success",
  "data": {
    "target": "loop",
    "slot": "a",
    "offset": 0,
    "received": 0,
    "size": 46864,
    "crc32": 305419896,
    "state": "downloading"
  }
}

5.2 分片下发 ota_data

Topic: dld960/{sn}/srv

请求:

{
  "msg_id": 402,
  "cmd": "ota_data",
  "ts": 1719000002,
  "data": {
    "target": "loop",
    "offset": 0,
    "crc32": 2524764894,
    "data": "6a6173646f6e...512 hex 字符 = 256B"
  }
}
data 字段 类型 说明
target string ota_begin
offset uint32 本片在镜像中的绝对偏移(256 对齐,首片 0
crc32 uint32 本片 256B 的 CRC32(十进制)
data string 256B 原始字节小写 hex512 字符

设备行为:

  1. offset == received(顺序片)→ 单片 CRC32 校验 → 写 W25Qxx256B 页对齐)→ received += 256(≥size 时截断为 size)→ 回 code=0
  2. offset < received(重复片,平台重发)→ 幂等直接回 code=0,不重写
  3. offset > received(缺片/乱序)→ 回 code=1 + data.offset=received(指示平台从该处续传;协议不要求乱序重组,降低设备复杂度)
  4. 单片 CRC 失败 → 回 code=1 + data.err_code=1(平台重发本片;连续失败由平台策略控制,可 abort)
  5. 会话未开始 / 状态非 downloading → code=3(先 ota_begin
  6. data 非 512 hex 字符 / offset 非 256 对齐 / 超 size → code=1 参数错误

响应: 标准成功/失败 + data 回显 {offset, received}

单片大小 256B 是协议常量,不接受协商(避免双方尺寸漂移)。若未来单片加大需协议升版。

5.3 结束下载 ota_end

Topic: dld960/{sn}/srv

请求:

{
  "msg_id": 403,
  "cmd": "ota_end",
  "ts": 1719000003,
  "data": {
    "target": "loop",
    "crc32": 305419896
  }
}

设备行为:

  1. received != sizecode=1 + data.offset=received(不完整,续传)
  2. received == size → 读回暂存区全镜像计算 CRC32,与 ota_begin 声明值比对
    • 一致 → 元数据 state=ready, last_result=0code=0, data={crc_ok:true}
    • 不一致 → 元数据 state=downloading(保留已下载数据,可重发错片)→ code=5, data={crc_ok:false}msg 注明 CRC 不匹配)

5.4 中止会话 ota_abort

Topic: dld960/{sn}/srv

{
  "msg_id": 404,
  "cmd": "ota_abort",
  "ts": 1719000004,
  "data": { "target": "loop" }
}

设备行为:元数据 state=aborted,Slot 标记可覆盖;正在刷写时 abort → 停止发送后续 A7 块(Loop 端由 bootloader 超时复位回 APP 兜底)。响应标准成功。

5.5 触发刷写 ota_flash

Topic: dld960/{sn}/srv会车安全关键命令:平台应确认现场允许(无车压线圈、非高峰)再下发。

请求:

{
  "msg_id": 405,
  "cmd": "ota_flash",
  "ts": 1719000005,
  "data": {
    "target": "loop",
    "slot": "a",
    "force": false
  }
}
data 字段 类型 说明
slot string a / b(缺省 = 当前 ready 的槽)
force bool true = 跳过安全窗口检查(高风险,平台授权)

设备行为(同步检查 → 异步刷写):

  1. 安全窗口检查force=false 时):
    • 4 通道 Loop 有车(VD_FLAG 任一置位)→ code=3 + data.err_code=1(有车,拒绝;平台可提示"车辆离开后重试")
    • 刷写期间会阻断检测与继电器控制 → 建议平台在低峰执行
  2. 元数据非 ready → code=3(先 ota_end 完成校验)
  3. 通过 → 立即回 code=0(异步),进入刷写:
    • 写 offlog 事件日志:固件升级开始target/slot/version/size
    • 下发 0x7F 命令尝试关闭 Loop 全部继电器(失败不阻塞——bootloader 复位后 GPIO 默认态=断开安全侧,待板上验证
    • 9F 01 00 01 A5 A7 启动帧 → Loop APP 写 flag 复位 → bootloader 回 pre_ok
    • 发 A6 地址帧(0x08003400 4 字节大端)→ addr_ok
    • 从 W25Qxx 读镜像,按 248B/块发 A7(停等 ACK,1s 超时重发 ×3)
    • 末块(sub_amount=1)→ bootloader 写剩余 → 清 flag → 复位跑新 APP
  4. 进度经 ota_report 上行(见 §6);失败重试 ×3 仍失败 → 元数据 state=flash_failed + event_report{type:ota_error} 告警(平台必答)

响应: 标准成功/失败(code=0 仅表示已启动,不代表刷写成功——结果以 ota_report/ota_status 为准)。

5.6 查询状态 ota_status

Topic: dld960/{sn}/srv

请求:

{ "msg_id": 406, "cmd": "ota_status", "ts": 1719000006 }

响应 data

{
  "target": "loop",
  "state": "flashing",
  "slot": "a",
  "size": 46864,
  "received": 46864,
  "crc32": 305419896,
  "version": "1.1.0",
  "progress": { "sent": 42112, "total": 46864 },
  "last_result": 0,
  "last_error": 0
}
data 字段 类型 说明
state string idle / downloading / ready / flashing / flash_failed / aborted
progress.sent uint32 刷写阶段已送 Loop 的字节数
last_result uint32 上次刷写结果:0=无/成功,非 0=错误码
last_error uint32 上次失败细分错误码

状态机(设备侧):

        ota_begin(新会话)          ota_data×N          ota_end(CRC✓)
 IDLE ─────────────────▶ DOWNLOADING ────────────▶ READY
  ▲                        │  ▲                        │
  │  ota_abort             │  │ ota_end(CRC✗)          │ ota_flash(安全检查✓)
  │  / flash_failed        │  └──────────────┐         ▼
  └────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ (Loop 重启) ──▶ IDLE(清槽/保留)
                                                      │
                                                      └──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE

6 主动上报 ota_report

Topic: dld960/{sn}/dev · QoS 1 用途:刷写进度与结果主动推送(进度可丢,结果可经 ota_status 兜底查询;设备侧元数据持久化 last_result

上报:

{
  "msg_id": 501,
  "cmd": "ota_report",
  "ts": 1719000007,
  "data": {
    "target": "loop",
    "stage": "flashing",
    "progress": { "sent": 42112, "total": 46864 },
    "code": 0,
    "msg": ""
  }
}
data 字段 说明
stage begin(会话开启)/ downloading(片落盘)/ ready(校验通过)/ flashing(刷写中)/ done(刷写成功,Loop 已重启)/ failed(刷写失败)
progress sent/total 字节(刷写阶段)
code stage 相关结果码

上报节奏begin/ready/done/failed 各 1 次;flashing 阶段按块进度节流(建议每 64 块或每 8KB 一次,避免刷写期间消息风暴)。done/failed 设备侧重发 3 次(间隔 5s,同 msg_id/ts),平台去重窗口建议 10 分钟(与 event_report 同策略)。

6.1 失败告警(扩展 event_report

刷写整体失败(重试 ×3 仍失败)时,设备经 event_report 上报(平台必答,复用 §5.3 ACK + 重发机制):

{
  "msg_id": 502,
  "cmd": "event_report",
  "ts": 1719000008,
  "data": {
    "events": [
      { "type": "ota_error", "ch": 0, "value": 1003 }
    ]
  }
}
type value 说明
ota_error 0x1000 起 0x1001=启动帧无响应 / 0x1002=地址帧错误 / 0x1003=数据块 ACK 超限 / 0x1004=全镜像校验失败 / 0x1005=安全窗口拒绝后强制失败

7 安全设计(会车产品底线)

# 措施 说明
1 升级窗口检查 ota_flash 前检查 4 通道无车;有车 → code=3,平台提示延迟。force=true 可跳过(高风险,需平台权限控制)
2 继电器安全侧 刷写前尝试 0x7F 关闭全部继电器;bootloader 复位后 GPIO 默认态应为断开(常开安全侧)——待板上验证
3 双重 CRC 单片 CRC32(传输层抓错)+ 全镜像 CRC32(落盘完整性);刷写前读回复核
4 失败可重入 下载失败 → 断点续传;刷写失败 → 重试 ×3 → 保留镜像可重刷;AT32 ISP bootloader 兜底,不会真砖
5 版本回滚 Slot A/B 双槽;平台可 ota_begin 指定槽刷旧版(force=true 覆盖)
6 审计留痕 升级开始/结果写 offlog 事件日志(含目标版本、slot、结果码);平台侧 version 字段归档
7 能力探测 老固件 ota_*code=4;平台按 ota_status/dev_info_query.soft_ver 判断,不盲目下发
8 幂等与防呆 重复片幂等 ACK;offset 乱序拒绝并要求续传;同版本默认拒绝重刷(force 绕过)

8 错误码约定

顶层 code 沿用通用语义(0 成功 / 1 参数 / 2 密码 / 3 忙 / 4 不支持 / 5 内部 / 6 超长),OTA 细分错误经 data.err_code 表达:

err_code 场景 顶层 code
1 单片 CRC 失败 / 乱序缺片(data.offset 指示续传点) 1
2 会话状态不允许(未 begin / 非 downloading 收 ota_data 3
3 安全窗口拒绝(有车压线圈) 3
4 版本冲突(同版本且非 force 1
5 全镜像 CRC 不匹配(data.crc_ok=false 5
6 暂存区写失败(SPI 异常/满) 5

9 平台侧实现要点(edc_server / DBNMQTTool

  1. hex↔bindata hex 解码(512 hex = 256B);bin 由 Intel HEX 转换(固定 0x08003400 偏移),转换在平台完成
  2. CRC32zlib.crc32(bin) 全镜像;单片 zlib.crc32(chunk) 与设备逐字节一致(ISO-HDLC 即 Python 内置)
  3. 分片循环for offset in range(0, size, 256) 顺序下发,收到 code=1 + data.offset 时从该处续传;单片失败重发 ≤3 次后 abort
  4. 断点续传:会话中断后重新 ota_begin,读 data.offset 续传
  5. 结果确认:刷写启动后轮询 ota_status(或订阅 ota_report);stage=done 后经 dev_info_query 核对 Loop 版本(需 Loop 侧支持版本上报,见 §10 待办)
  6. 告警闭环:收到 event_report{type:ota_error} 先落库后应答
  7. DBNMQTTool 增加 OTA 页签(工具先行惯例:先模拟分片下发,固件后到)

10 待板上验证项(实现前必须闭环)

# 影响
1 Loop 复位后继电器 GPIO 默认态是否断开(安全侧) 安全底线 2
2 DBN UART2 TX 缓冲容量(能否容纳 254B A7 帧;现 BLE 透传块 ≤94B) 刷写块大小
3 W25Qxx 写 256B 页 + 4KB 扇区擦除在 MQTT 接收回调内的耗时(阻塞窗口 vs MQTT 保活) 单片落盘是否需移主循环
4 DBN RAM 预算:镜像块缓冲(248B)+ CRC 查表(1KB)是否挤占现有栈余量(历史 .bss 事故) 内存方案
5 Loop APP 当前固件实际 bin 大小(验证 60KB 槽余量) 槽位容量
6 刷写期间 MQTT 保活(6~8s 窗口内 PINGREQ/心跳是否正常,IWDG 喂狗节奏) 刷写中断安全

11 与现有协议的关系

协议文档 变更
DLD960_IoT_MQTT协议.md 本设计并入后升 V1.08:§3 命令表 + §4.19~4.24ota_* 详情)+ §5.5ota_report+ §5.3 事件类型表补 ota_error + §8 错误码补 err_code 约定 + 修订记录
DLD960_TCP_JSON协议.md 本版不扩展ROADMAP 先走 MQTT);后续按"复用命令 + stream/字段"模式补(与 log_* 扩展同套路)
DLD960_BLE协议.md 不变(BLE OTA 维持流式透传现状;本地刷写状态机与 BLE 透传共用 lup_feed_byte_ota
README.md / DLD960_技术规格书.md 协议矩阵同步 V1.08;规格书补 OTA 分区/安全约束(并入主文档时执行)

固件代码落点(协议拍板后动):

  • iot_mqtt_srv.c:命令分发链加 6 个 ota_* 分支(现有 if-else 链)
  • 新增 ota_srv.c/h:会话状态机 + W25Qxx 暂存读写 + CRC32 查表
  • usart_biz.c / loop_uart_proto.c:本地刷写状态机(复用 lup_feed_byte_ota 解析 + g_flag_counter_ota 门控,注意退出机制——现 BLE 透传 flag 无退出点,本地刷写必须自管)
  • offlog.c:补"固件升级开始/结果"事件类型
  • 单测:tests/test_ota_srv.cgcc 提取+嵌入模式,mock SPI/MQTT/Loop

修订记录

版本 时间 说明
V1.00 2026-08-20 设计稿:Loop MCU MQTT 远程 OTA 先存后刷协议(依据 ROADMAP P1.4 ①)