ESP32_COMM · DCU 通信与 OTA 中枢固件 — 项目全景介绍
ESP32-C3FH4 × GD32E508 双 MCU 架构:WiFi / BLE 双通道 OTA 中继 · SPI Link v2 高速桥 · 三路 CANFD 电机总线控制
固件版本 v0.5.2 (git 770ae14 ) · ESP-IDF v6.0 · 平台 ESP32-C3(RISC-V 单核 160MHz) ·
文档版本 2026-10-11 · 配套浓缩版 README.md (见文末「文档同步说明」)
1. 项目概述与总体拓扑
2. 硬件平台与 Flash 分区
3. 软件架构与 FreeRTOS 任务模型
4. 启动流程与自检(SystemBoot 位图)
5. SPI Link v2 传输层(核心协议)
6. 安全握手与加密算法
7. OTA 体系(协调器 / 中继 / 本机 / 存储)
8. BLE OTA(NimBLE)
9. WiFi 管理与 HTTP API
10. CANFD 桥接(0xC0~0xC8)
11. 电机 OTA 中继(0xD0~0xD2)
12. 监控与可观测性
13. 诊断与容错机制总表
14. 关键配置常量速查
15. 目录结构
16. 构建与烧录
17. 网页控制台前端
18. 功能实现状态清单
19. 安全审计与已知限制
20. 版本演进摘要
21. 文档同步说明(HTML ↔ README)
1. 项目概述与总体拓扑
ESP32_COMM 是机器人 DCU(Domain Control Unit,域控制器)板上的 ESP32-C3 固件,承担三类职责:
① OTA 中继枢纽 :上位机通过 WiFi(HTTP)或 BLE(GATT)把固件交给 ESP32;ESP32 自身固件写入 AB 双分区升级,
GD32 固件缓存到本地 Flash 后经 SPI 安全通道转发烧写,电机节点固件经 GD32 的 CAN 总线二次中继。
② SPI Link v2 主站 :以 13.33 MHz 五线 SPI(含 DATA_READY 通知线)与 GD32E508 建立带会话、序号、
信用流控和 CRC32 校验的可靠链路,是所有跨芯片业务的唯一通道。
③ 电机总线控制面 :把 HTTP/BLE 请求翻译成 GD32 上三路 CAN/CANFD 收发(单帧、批量、跨通道同步突发、
只听模式),并周期回收电机状态、ADC、版本等遥测,通过网页控制台呈现。
上位机
PC / 手机浏览器
网页控制台(tools/)
固件上传 · 状态 · CAN · 电机
ESP32-C3FH4
RISC-V 单核 160MHz · 4MB Flash
HTTP 服务器(:80)
NimBLE GATT(MTU 512)
OtaCoordinator / OtaRelay
SPI Link v2 主站
gd32_ota 分区 252KB 固件缓存
app0 / app1 AB 双分区自升级
GD32E508VET6
Cortex-M4 · DCU 业务主控
Bootloader(72KB)/ APP(216KB)
AES-128 会话解密 · Flash 烧写
3×CANFD 控制器 + 收发器
电机 OTA CAN 二次中继
ADC / ECAT(LAN9252) 等业务
CAN 通道 0
CANFD 最高 64B
CAN 通道 1
桥接 / 过滤可配
CAN 通道 2
电机节点 ×9/通道
WiFi HTTP
BLE GATT
SPI Link v2
13.33MHz · Mode0 · 5线
CS/MOSI/MISO/SCLK
+ DATA_READY 通知
OTA · CAN 遥测
电机 OTA · AES-128
TCAN1057AV 收发器
(S 脚可切 Silent 只听)
UART0 日志 3Mbps(TX21/RX20 RX泵)+ UDP 日志 6666 → 网页 /logs
OTA 数据流:固件经 ESP32 缓存 / 直通,按 目标→通道 分发(详见 §7/§8/§11)
图 1-1 系统总体拓扑 :上位机仅经由 ESP32 接入;GD32 的所有远程操作(OTA、CAN、遥测)都以 SPI Link v2 为唯一通道
1.1 固件版本与代码规模
项 值 说明
固件版本 v0.5.2 (FW_VERSION_MAJOR/MINOR/PATCH)main/include/version.h 手动维护;构建时间由构建系统自动生成(fwBuildTime())
Git 提交 770ae14手动填入 version.h
平台 / 框架 ESP32-C3FH4 · ESP-IDF v6.0 · FreeRTOS RISC-V 单核 160MHz,4MB Flash
代码规模 约 19,000 行 C++(main/ 下 30 个源文件) 另含三工程共享协议 spi_link/spi_link_v2.h(兄弟目录)
对端固件 GD32 Bootloader + GD32 DCU_APP + 电机节点 本仓只含 ESP32 侧;协议真源 SPEC/spi_link_v2.md 与 GD32 仓共享
1.2 功能一览(实现状态)
功能域 能力 状态 详见
OTA 中继 GD32 固件 WiFi/BLE 上传 → SPI 加密转发(六阶段握手/协商/转发/验证) 已实现 §7
ESP32 自身 AB 分区 OTA + SHA-256 回读校验 + 回滚保护 已实现 §7.5
电机节点固件 CAN 二次中继(0xD0/D1/D2) 已实现 §11
GD32 固件包级防呆(Cortex-M 向量表身份校验,禁止 BOOT 包刷入 APP 区) 已实现 §7.6
传输链路 SPI Link v2:会话 / 序号 / 信用流控 / CRC32 / HELLO 重同步 / 延迟一拍协议 已实现 §5
AES-128 会话密钥挑战-应答握手(GD32 OTA 数据面) 已实现 §6
链路熔断(3 连败×2s 冷却)、透明重试(kTxRetryMax=3)、撕裂诊断 ring_steal_diag 已实现 §5.8/§13
CAN 控制 0xC0 单帧 / 0xC7 跨通道同步突发(多电机同发 <1ms 偏差) 已实现 §10
0xC1 统计+ID 快照分页查询 / 0xC2 收模式(全收/过滤/桥接)/ 0xC3 完整快照 已实现 §10.2
0xC8 收发器 Silent 只听不发(TCAN1057AV S 脚) 已实现 §10.4
0xC1 GD32 1Hz 主动推送路径 保留但默认请求驱动 §12.2
接入通道 WiFi STA/AP 自动降级 + HTTP 22 端点 已实现 §9
BLE GATT 窗口 ACK(64)+ 乱序重排 + 负 ACK 续传 已实现 §8
BLE/WiFi 双通道互斥与射频共存(软挂起) 已实现 §8.5
监控诊断 健康监控(温度/RSSI/堆,4 级状态,OTA 门控) 已实现 §12.1
日志三路输出(UART 3M / UDP / 环形缓冲 → /logs)+ UART0 RX 泵 已实现 §12.3
GD32 ADC 遥测(0xC4)/ 电机状态(0xC5×14 页)/ 电机版本(0xC6)/ 初始化状态(0xA3) 已实现 §12.2
CPU 占用 / 任务栈水位 / 堆低水位快照 / 启动自检位图 已实现 §4/§12
网页控制台 OTA 控制台 / 电机控制台 / CAN 工具 / CAN 监控 四页面 已实现 §17
DBC 报文编辑、波形显示、启动自检面板、堆/任务抽屉 已实现 §17
HTTP 接口认证(token) 未实现 §19
🚫 顺带澄清两处历史文档讹误:本固件没有 mDNS 功能(仅 lwIP 具备 mDNS 解析编译项),设备访问走固定 IP;旧 README 的「115200 调试串口」已过时,日志 UART0 实为 3 Mbps (debug_log.h:34),115200 仅为 sdkconfig 控制台项。
2. 硬件平台与 Flash 分区
2.1 硬件参数
项目 规格 备注
芯片 ESP32-C3FH4(RISC-V 单核 160MHz,Wi-Fi 4 + BLE 5) 所有任务固定核 0
Flash 4MB,DIO 模式 曾试 QIO 致 OTA 读回 SHA 必错,已锁定 DIO(sdkconfig 注释警告)
SPI MOSI=GPIO5 · MISO=GPIO7 · SCLK=GPIO6 · CS=GPIO4 · DATA_READY=GPIO10 13.33MHz(CONFIG_DCU_SPI_CLOCK_HZ=13333333)Mode 0;GD32 PB6 开漏驱动 DATA_READY
调试 UART0 TX=GPIO21 · RX=GPIO20 · 3,000,000 baud 非阻塞直写 FIFO;RX 泵把外部串口行汇入 /logs
欠压检测 LVL7(约 2.8V) 长断电后 CHIP_EN RC 充电慢,LVL5 会停在半复位状态
GD32 侧存储 BOOT 0x08000000~0x08011FFF(72KB)· APP 0x08048000~0x0807DFFB(216KB)· SRAM 0x20000000~0x2001FFFF 固件身份校验的判据区间(§7.6)
2.2 4MB Flash 分区地图(config/partitions.csv)
boot 36KB(放大显示)
nvs 24KB
otadata 8KB(AB 启动标志)
gd32_ota 252KB
app0 — 1.25MB(ota_0 当前运行)
app1 — 1.25MB(ota_1 OTA 目标)
spiffs 1.06MB(用户数据)
0x0000
0x9000
0xF000
0x11000
0x50000
0x190000
0x2D0000
0x400000
• gd32_ota :GD32 固件缓存区(spiffs 子类型,便于 esp_partition API 直接读写);OTA 元数据存 NVS 命名空间 ota_meta (total_size / stored_size / last_seq / checksum)
• app0/app1 :ESP32 自身 AB 分区;配合 bootloader 回滚(CONFIG_APP_ROLLBACK_ENABLE)新固件崩溃自动回退
• 擦除策略:GD32 存储按「擦除水位」按需擦扇区(4KB);电机 OTA 上传则强制整区预擦除,杜绝 NOR 与逻辑残留叠加的单 bit 翻转
图 2-1 Flash 分区布局 (示意:早期小分区为可读性放大,app0 起按比例;GD32 OTA 中继与电机 OTA 共用 gd32_ota 分区,靠互斥串行化)
3. 软件架构与 FreeRTOS 任务模型
3.1 模块分层
接入层(server/)
wifi_ota.cpp
STA/AP · 重连 · RSSI · 路由表
http_handlers.cpp
22 个端点 · 流式上传 · JSON 工作区
ble_ota / ble_ota_host / ble_ota_data
GATT 服务 · 窗口 ACK · 乱序重排 · 内存池 · 共存挂起
协调层(ota/)
OtaCoordinator(单例 · 9 状态系统级状态机)
唯一权威入口 · 互斥 · 忙态看门狗自愈 · epoch 会话保护 · BLE 共存通知
Esp32OtaCore(本机 OTA)
begin/write/finalize/abort · SHA-256 读回 · 分区互斥 · SPI 暂停
执行层(relay/ · storage/)
OtaRelay(事件驱动状态机 ×7 态)
握手→协商→转发→收尾·验证 · 专用任务
Gd32FlashStore
gd32_ota 分区读写 · 擦除水位 · NVS 元数据 · SHA
MotorOtaRelay
电机 OTA 喂数任务
SpiMaster
Link v2 链路 + worker
transport/(底层)
monitor/ 横切:健康 · 日志 · 遥测 · 统计
图 3-1 模块分层与依赖 :传输层(wifi/ble)不直接触达 OtaRelay/Esp32OtaCore,一律经 OtaCoordinator 统一入口
3.2 FreeRTOS 任务全景
23
21
22
12
8
7
5/4
1
优先级(C3 单核 0)
ota_relay(栈 2560)GD32 转发状态机
health_mon(3072)5s 巡检
wifi_ota(3072)WiFi / HTTP / 事件
ble_ota(2560)NimBLE host 初始化
ble_data_worker(2560,prio 12)
spi_worker(5120,prio 8)唯一持 SPI
motor_feed(4096,prio 7)0xD1 喂数
wifi_reconnect(2560,p5)退避重连
can_fb(4096,p4)0xC5/C4 周期查询
cpu_stats(3072,p1)1Hz 统计
uart_rx 泵(3072,p1)串口行→/logs
httpd(7168,lprio)
ble_finish / gd32_wait / restart
ota_cancel(3072,p4)
DATA_IN 队列 144
内存池 8×512B = 4KB
· 跨任务队列:wifi/ble → ota_relay 走 dispatchQueue/resultQueue(深度 4);业务任务经 transact Job 池(7)提交 spi_worker 串行执行
· DATA_READY(PB6)下降沿 ISR → 任务通知精确唤醒 spi_worker;EVENT 上报走 4 槽事件环(满丢最旧)+ takeEvent 消费
· 另有 NimBLE host 内部任务;任务栈按板端水位实测收敛(S ≈ 峰值 + 1.5~2KB);httpd 为 esp_http_server 内部任务(lprio)
图 3-2 任务与队列 :SPI 总线由 spi_worker 独占串行化,业务任务经 Job 池提交;OTA 优先级高于网络栈任务以保证 50ms ACK 窗口
关键设计约束(线上事故沉淀):UART 日志直写曾阻塞 ota_relay(23)/spi_worker(6) 数毫秒 ,导致 50ms ACK 窗口拖爆、12/777 包重发——因此日志改非阻塞 FIFO 写、BLE worker 提到 prio 12 尽快排空队列、spi_worker 提到 prio 8(高于 motor_feed 的 7,低于状态机任务)。
4. 启动流程与自检(SystemBoot 位图)
app_main 启动序列(每步成功 → SystemBoot::mark(bit))
冷启动判定
POWERON·BROWNOUT
先延 200ms
netif · 事件循环
DebugLog 环 64 槽
WifiOta.earlyInit
冷启动清 wifi/phy
NVS 连接缓存
HealthMonitor
+ health 任务
温度 1s · 巡检 5s
Protocol.init
RNG 生成扰码常数
SpiMaster.init
L0 自检 · 对端
HELLO 探测
创建业务任务
relay·wifi·ble
can_fb·feed
✔ 任务启动后 mark_app_valid_cancel_rollback() — 唯一回滚取消点
打印 banner「OTA Relay v0.5.2 启动」— 前端正则匹配判定重启完成
SystemBoot 16 位自检位图(/system/status → boot{...},前端「启动自检」面板)
NETIF critical
EVT_LOOP critical
DBG_LOG critical
WIFI_EARLY critical
HLTH_MON critical
HLTH_TASK critical
CPU_STATS 非关键
PROTOCOL critical
OTA_COORD critical
SPI critical
RELAY_TASK critical
WIFI_TASK critical
BLE_TASK critical
CAN_LINK 非关键
CAN_FB 非关键
OTA_VALID 非关键
关键位失败 → DEBUG_ERR + 3 秒后 esp_restart()
非关键位失败仅记录,report() 输出「OK (critical), skipped/failed: 清单」
can_fb 任务:冷启动/欠压复位时额外延迟 1500ms 再开始查询
错开 GD32 上电初始化窗口,避免无效查询风暴
WiFi 侧另有 NVS 不完整元数据检测(stored<total → 清 meta + abort + GD32 APP 探测,防 Bootloader 卡死拖垮 HTTP socket)
图 4-1 启动时序与自检位图 :冷启动两级延时(主 200ms / can_fb 1500ms),关键位失败自动重启,全部结果经 /system/status 暴露
5. SPI Link v2 传输层(核心协议)
SPI Link v2 是三个工程(ESP32_COMM / GD32 DCU_APP / GD32 Bootloader)共享的链路协议,真源 spi_link/spi_link_v2.h,PC 端 golden-vector 自测交叉锁定。它解决了裸 SPI 没有「帧界、会话、背压、应答归属」的问题。
5.1 帧格式(16B 头 + 0~236B 载荷 + CRC32,总开销 20B)
SOF0 A5
SOF1 5A
VER 02
FLAGS 位图①
SESSION_ID 4B LE 会话归属
TX_SEQ 发方序号
ACK_SEQ 应答归属
CREDIT 信用窗口
TYPE 帧类型
LEN 2B ≤236
RSVD 2B =0
PAYLOAD 0~236B
业务数据(APP 面)
CRC32 4B LE
MPEG-2 覆盖头+载荷
SPI Link v2 帧 = 16B Header + Payload + CRC32(V2_OVERHEAD=20)
① FLAGS:0x01 REQUEST · 0x02 RESPONSE · 0x04 EVENT · 0x08 ACK_ONLY · 0x10 RESET_SESSION(HELLO 请求 = REQUEST|RESET_SESSION)
② CRC32-MPEG2(多项式 0x04C11DB7,初值 0xFFFFFFFF,无反射),解析器支持帧在缓冲区内任意偏移(前导垃圾字节容忍)
③ 读预算按 GD32 角色:BOOT=128B / APP=256B;短响应类型再收紧到 96B 省填充时钟;发送时钟字节数 clamp(帧长, 64, 256)
TYPE 分族:
0x00 HELLO · 0x7E CREDIT_UPDATE · 0x7F LINK_ERROR(err,期望seq)
0x01 OTA_REQ · 0x02 OTA_DATA · 0x03 OTA_CANCEL · 0xB0 ENTER_BOOT(ACK=0xB5)
0xC0 CAN发 · 0xC1 反馈页 · 0xC2 收模式 · 0xC3 ID快照 · 0xC7 突发 · 0xC8 Silent
0xA1 PING · 0xA3 INIT_STATUS · 0xC4 ADC · 0xC5 电机状态 · 0xC6 电机版本 · 0xD0/D1/D2 电机OTA · 0xF7
图 5-1 帧格式 :响应帧(0x80 ACK / 0x81 WAIT / 0x82 ERROR)复用同一帧头;LINK_ERROR 携带错误码与期望序号用于失步归因
5.2 五线 SPI 与「延迟一拍」全双工
CS(GP4)
SCLK(GP6)
MOSI(GP5)
命令帧 #N(tx_seq=T)
命令帧 #N+1
MISO(GP7)
陈旧响应(丢弃)
命令 #N 的真实响应
DATA_READY
↓PB6 下降沿:GD32 有数据(ISR 置位 + 任务通知精确唤醒)
延迟一拍协议:
第 N 次 CS 事务读回的是第 N-1 次
命令的响应。GD32 在 CS 释放 ISR
内 µs 级装载下一响应,ESP32 发完
命令后由 poll 状态机按 DATA_READY
边沿驱动补读真实响应(两步法)。
CS 建立/保持加长:cs_ena_pretrans=64
cs_ena_posttrans=32;事务间隔 ≥1ms
图 5-2 五线时序与延迟一拍 :所以 transact 分两步——先发命令(丢弃本笔陈旧 RX),再 poll 等真实响应;CS 事务间最小 1ms 防 GD32 EXTI re-arm 窗口被撞(0.4.5 撕裂根因修复)
5.3 信用流控(Credit)与背压
ESP32 发送门
remoteCredit = 对端帧头 CREDIT
初值 8 · 随每帧更新
= 0 时禁发新 REQUEST
GD32 接收窗
响应缓冲 → 处理 → 释放
credit 随每帧帧头回传
空闲后不再主动发帧
数据/请求(credit 字段 = 本端剩余接收能力)
credit=0 → ESP32 循环 waitSpiDataReady(5ms) 等 PB6 / CREDIT_UPDATE(0x7E)
死锁破除:PB6 高 = GD32 无待发数据 = 响应缓冲已释放 → 陈旧 credit=0,强制 m_remoteCredit=1 放行
credit 更新必须在 session 校验之后(失配帧的 credit 不可信);半会话终止用 resetCredit() 复位 8/8(§11)
图 5-3 信用流控 :每帧帧头互报剩余接收能力,credit=0 触发发送背压;两处已知死锁(陈旧锁存 / 失配帧污染)各有针对性破除
5.4 HELLO 建链与自动重同步
HELLO(TYPE 0x00)
SESSION_ID = esp_random()|1(非零)
FLAGS = REQUEST|RESET_SESSION
seq/credit 全零 · role_hint 载荷 1B
HELLO_ACK 校验
type=0x00 · RESPONSE · session 回显一致
payload[4]=role(1=APP 2=BOOT)
payload[5]=对端 credit_max
链路生效
v2Active=true · credit=8/8
APP 角色解析 16B 身份块②
0xA3 缓存失效 · resyncs++
连发 2 次
② 身份块 16B(HELLO_ACK 附加 / 0xA1 PING 同布局):
[0..3] APP版本 major.minor.patch.build · [4..7] BOOT版本 · [8..11] uptime_ms · [12] feat_map_ver · [13..14] feature_flags · [15] init_state
feature_flags:bit0 ADC反馈 · bit1 UART温度打印 · bit2 SPI_APP_LOG · bit3 CAN_BRS · bit4/5/6 CAN 通道0/1/2 · init_state:0=OK / 1=DEGRADED / 2=CRIT_FAIL / 0xFF=UNKNOWN
自动重同步三触发:
① 收到 session=0 的响应(GD32 重启后 v2 未激活)→ 置 resyncPending;② worker 空闲期发现 resyncPending(5s 冷却)→ HELLO(role=any)
③ GD32 复位导致 LINK_ERROR(0x7F) 后由业务重试自然触发 — 每次成功 HELLO 计入 spi_link.resyncs
角色粘滞纠正:见 APP 独有 TYPE(0xC1/0xC3~0xC6/0xD0~0xD2)即把卡在 boot 的角色纠正回 APP,防止读预算用错 128/256
图 5-4 HELLO 建链 :延迟一拍连发两次取回 ACK;session 不匹配的 ACK 视为旧帧重发即可;ENTER_BOOT 前后各有专门的 HELLO/静默窗口
5.5 transact 两步法与透明重试
步骤 1:发送命令
credit 背压检查 · 本笔 RX 丢弃
5ms 静默豁免判定
isAppIsrFastRespType → 免静默,边沿唤醒
步骤 2:poll 响应
DATA_READY 优先 · 盲读受 no_blind 约束
快速类型白名单(GD32 APP ISR µs 级装载):0xA1 0xA3 0xC0 0xC1 0xC2 0xC3 0xC4 0xC5 0xC7 0xD1 0xF7
仅 BOOT 主循环处理的 OTA 命令(0x01/0x02/0x03)保留 5ms 静默
——过早盲读会打断 BOOT 的 EXTI/DMA 时序(实测 CHALLENGE 超时)
poll 规则
· no_blind(快速类型):禁盲读,纯等 PB6 电平
· 否则:前 2 轮盲读 → 每 8 轮一次 → deadline 末强制
· 读预算 = min(短响应 96B, 角色预算 128/256B)
盲读为何危险(0.4.4 实测)
PB6 未低时强打 256 时钟 0xFF,若 GD32 CS EXTI 被
ECAT/CAN 中断压住未 re-arm → RX DMA mid-pulse 重装
→ 撕裂 0x82 0xE8 → 迟到帧被偷读 → 偷读风暴
透明重试 kTxRetryMax=3(1 首发 + 2 重发,同 TX_SEQ):仅当「未读到任何有效响应」时重发
· 失败路径 seq 不推进 → GD32 按 DUP-SEQ 去重缓存回放,杜绝重复执行 · 读到任何帧(WAIT/ACK/ERROR/陈旧)即返回,GD32 明确拒绝(0x82)不重试
· 耗尽 → tx_fail++ / tx_fail_last_type=type / 熔断计数(3 连败 → 链路离线,2s 冷却后放一笔探针;HELLO/OTA 关键命令不受限)
0xC0 发送另有陈旧 WAIT 排空:读到 WAIT(0x81) 必为上轮电机 OTA 残留,循环最多 4 次直到真 ACK/ERROR,防「失败一次就再也发不出」
图 5-5 transact 流程 :发送 →(静默豁免)→ poll →(透明重试);所有业务共用此路径,调用方无感知
5.6 序号 / 会话失配与「偷读」修复
机制 规则
seq 推进 发送成功且非陈旧读 → m_txSeq++;poll 读到 ACK/WAIT/ERROR 且 ack_seq==lastSent → 推进;WAIT 也必须推进 (否则同 seq 重发被判重,feedTask 卡首 chunk)
迟到响应偷读 poll 超时放弃后 GD32 迟到响应拉低 PB6 被 worker 按铃读走(帧已上时钟无法退回)→ 同样必须推进 seq (0.4.2 修复,否则下条命令复用旧序号被 GD32 判重吞掉,业务永远读到旧 0x82)
失配帧处理 CRC+session 合法的 RESPONSE 一律交调用方(帧已被读走、GD32 已清 pending,丢弃会死等),由 m_lastRxAckMatched 标记归属,业务以结构化 payload 终判
session 失配 EVENT 帧放行;session=0 的响应放行(GD32 重启语义)并置重同步;其余拒收且不更新 credit
撕裂诊断 ring_steal_diag:偷读计数/最后类型/ack 归属/payload 签名 [rxLen, rb0, rb1](0xE8 撕裂时 rb0=A5 rb1=5A 为期望值,255=未找到 SOF);0x82 归因 rsp_err_last_req/p0..p5,全部进 /system/status
6. 安全握手与加密算法(GD32 OTA 数据面)
GD32 固件转发采用预共享主密钥 + 挑战-应答 派生会话密钥,会话内数据 AES-128-ECB 加密。实现于 OtaRelay::doChallengeAndAuth() 与 Protocol(nonce 搅拌生成 / 会话密钥管理)。
ESP32(OtaRelay)
GD32 Bootloader
前置:进入 BOOT(ENTER_BOOT 0xB0,ACK=0xB5,最多 3 次 + 整链重同步 + 100ms 静默)
nonce_server = scrambleGenerate(tickcount, &栈地址, 0x12345678) ⊗ 每次开机硬件 RNG 的 SCRAMBLE_CONST[16]
① 0x00 0xAC(握手请求)
重试 20 次×1.6s · 每 5 次 HELLO 重同步
② Challenge 36B
[0..11] GD32 UID 12B(禁全 0/全 FF)
[12..19] nonce_server[8..15] 回显 8B
[20..35] AES-ECB(MASTER_KEY, K_session) 16B
③ 本地验证与派生
K_session = AES-128-ECB_encrypt(MASTER_KEY, nonce_server)
重算 AES(MASTER_KEY, K_session) 与 [20..35] 比对;nonce 片段绑定本会话防重放
④ CONFIRM:AES(K_session, nonce^0x55)
transact 3s · poll 1s×8+8
⑤ 明文 ACK 0xBA 0xC0
会话生效 · 数据包 K_session 加密
任一步失败 → ERR_TIMEOUT / ERR_AUTH → 状态机 ERROR;会话结束/复位时 volatile 逐字节擦除密钥(抗优化清零)
图 6-1 挑战-应答握手(字节级) :MASTER_KEY 预共享(当前为占位值,§19),K_session 每次 OTA 重新派生,nonce 片段回显绑定会话
Nonce 搅拌算法(scrambleGenerate) :非标准 KDF,仅作握手 nonce——以 tick 计数、栈地址、固定种子 0x12345678 三输入混合每次开机由硬件 RNG 随机生成的 16B 扰码常数,输出 16B。协议实现另有 PC 端 golden-vector 自测(SPI_LINK_V2_SELFTEST)交叉锁定帧字节。
7. OTA 体系(协调器 / 中继 / 本机 / 存储)
7.1 OtaCoordinator — 系统级 9 状态协调器
单例 + FreeRTOS 互斥(100ms 锁超时),是 WiFi/BLE 传输层到 OTA 执行体的唯一权威入口 ;同时承担忙态看门狗自愈、epoch 会话保护、BLE 射频共存通知、CAN 反馈暂停与电机 OTA 互斥。
OTA_SYS_IDLE
GD32_STORING
收流写 Flash(忙)
GD32_STORED
GD32_RELAYING
SPI 转发中(忙)
GD32_COMPLETE
notifyRelayComplete(true)
ESP32_RECEIVING
收流写 AB 分区(忙)
ESP32_FINALIZING
SHA+end+set_boot(忙)
ESP32_REBOOTING
OTA_SYS_ERROR
任意阶段失败 → ERROR(isIdleState 不含它,但可被新请求/软清恢复)
· 忙态看门狗 150s (>中继预算 120s+余量):STORING/RELAYING/RECEIVING/FINALIZING 超时 → abort 自愈回 IDLE;FINALIZING 假占用(core 已空闲)直接复位
· epoch :每次 begin 递增;上传失败清理仅当 epoch 匹配才执行,防旧会话清理误杀新会话 · 开机检测 NVS 完整固件自动进 STORED
· 入口互斥:电机 OTA 活跃时拒绝一切 GD32/ESP32 OTA(gd32_ota 分区共用);REBOOTING 不入看门狗且拒绝 abort(防与重启竞态)
图 7-1 OtaCoordinator 9 状态 :GD32 与 ESP32 两条 OTA 路径互斥推进,任意异常收敛到 ERROR/IDLE 并自愈
7.2 OtaRelay — GD32 转发事件驱动状态机(7 状态)
独立任务(栈 2560B)+ dispatch/result 队列(深度 4)。收到调度信号先 setSuppressRingService(true)(防 SPI worker 把 OTA 响应当 EVENT 偷读——线上事故根因),全程 RAII 持有抑制引用。
RELAY_IDLE
HANDSHAKING 进BOOT+握手+认证
SIZE_NEG 发大小·GD32擦Flash
TRANSFERRING 逐包加密转发
FINISHING FINISH+APP验证
RELAY_DONE
RELAY_ERROR
ERROR/DONE 经 EVT_START 复位回 IDLE;所有失败路径 abortRemoteAndReset(true)(先 SPI CANCEL 0x03 清 GD32 半会话,失败仅告警)
关键时序:握手 20×1.6s(每 5 次 HELLO 重同步)· CHALLENGE 5s · SIZE poll 500ms×120+40(擦 Flash 长 WAIT)
转发预算 10s+25ms/包 上限 120s · 卡死检测 10s 无成功帧 · FINISH 10s×10 重试 · APP 存活 = 静默 500ms + PING 1000ms×20 轮(禁 HELLO 快路径,必须 PING 实证)
图 7-2 OtaRelay 状态机 :6 事件(START/RESP_OK/RESP_ERROR/TIMEOUT/HEALTH_CRIT/ABORT)驱动;结果码 -1~-7(TIMEOUT/CRC/AUTH/STORAGE/GD32/HEALTH/USER_ABORT)
7.3 64 字节 OTA 数据包与单包重试
session 4B =nonce[0..3]
seq 2B LE
保留 2B
AES-128-ECB(K_session, plain48) 48B
3 块连加密 · 末包 0xFF 填充
if_end 4B 末包 FFFFFFFF
CRC32 4B LE 覆盖 K_session+plain48
SPI_OTA_CMD_DATA(0x02) 载荷 = 64B(GD32_OTA_PACKET_SIZE=48 有效载荷,key 混入 CRC)
明文来源:Gd32FlashStore 按需读 gd32_ota 分区(读失败 → ERR_STORAGE);总包数 = ceil(total/48);双 64B tx 槽流水线预组下一包隐藏读 Flash+AES 延迟
单包重试(sendRelayPacketWithRetry):
DATA(seq=T) → WAIT(0x81)「从机已收」→ ACK(0xBAC0+seq=T+1);50ms 未到 ACK → 重发同 seq DUP-DATA
(GD32 去重缓存回放最终 ACK,重发安全);每包最多 5 次 DATA · 末包期望 ACK seq=0xF0F0F0F0
结构化错误帧:0xBAC0+seq+err——0x04=CRC 错立即放弃整包;其他错误允许带错重发;ACK 每 32 包触发健康检查
图 7-3 数据包结构与重试 :GD32 对已写入包号去重(只回 ACK 不二次写 Flash),使「见 WAIT 后重发」安全——旧逻辑 ACK 丢失即永久丢失
7.4 ESP32 本机 OTA(WiFi + BLE 共用)
步骤 实现 要点
begin 选非当前启动分区(优先 ota_0)· esp_ota_begin(OTA_WITH_SEQUENTIAL_WRITES) 边写边擦 整区预擦 1.25MB 会堵死 HTTP TCP 窗口(进度卡开头);begin 同时 setSpiPaused(true)——写 Flash 时 cache 反复失效会拖慢 SPI worker 甚至 Cache error;使能回滚时先 invalidate_inactive_ota_data_slot()
write esp_ota_write 循环 + 喂狗不做增量 SHA;httpd 侧 4KB RAII 接收缓冲(会话内 malloc),堆紧时 pacing(<8KB 减速)
finalize 分区读回算 SHA-256(1024B 静态缓冲,每 16 块喂狗)→ 与前端 SHA 大小写不敏感比对 → esp_ota_end → set_boot_partition 不匹配返回 ESP_ERR_INVALID_VERSION 并回传设备 SHA 供前端核对;m_ended 标志防 end 后误 abort
回滚保护 任务启动完成后 esp_ota_mark_app_valid_cancel_rollback() 新固件标记前崩溃 → bootloader 自动回退上一有效分区
7.5 Gd32FlashStore — 存储层
能力 实现细节
按需擦除(水位) beginWriteSession(false) 复位水位;ensureErasedForWrite 仅从水位向高地址顺序擦 4KB 扇区(跳空一并擦),禁止重擦已写扇区;每扇区擦后喂狗+yield 让 WiFi/LWIP 有机会运行
整区强制擦除 电机 OTA 上传 beginWriteSession(true) 全擦 252KB——NOR「只能 1→0」与残留固件叠加会造成静默单 bit 翻转(实测 0x19→0x18 根因)
NVS 元数据 命名空间 ota_meta:total_size / stored_size / last_seq / checksum(blob 32B);读-改-写全程持锁防丢失更新;仅 ACK checkpoint 持久化(NVS commit 毫秒级,不能每包一次)
SHA-256 收流收尾统一 metaFinalizeChecksum;relay 门控:checksum 非全零则信任跳过二次全盘 SHA;全零则兜底 metaVerifyChecksum(全零不容忍,防 meta 被清零掩盖损坏)
对齐缓冲 OTA 会话 begin 时 calloc(4KB+8),end/abort 释放——消除每非对齐块 malloc/free 堆抖动,非 OTA 期零占用
7.6 包级防呆:固件身份校验(0.3.81)
SHA-256 只能证明「传对了」,不能证明「传的是对的包 」。把 BOOT 包(或 ESP32 镜像/任意文件)中继进 GD32 APP 区会擦掉好固件。validateStoredImageIdentity() 读分区头 8B Cortex-M 向量表:初始 MSP 必须落在 GD32 SRAM(0x20000000~0x2001FFFF);复位向量(清 Thumb 位)落在 APP 区 → kApp 放行;落在 BOOT 区(0x08000000~0x08011FFF)→ kBootloader 拒绝 ;判据与 GD32 BOOT 侧 validateApplication() 完全一致(两侧需同步修改)。/ota/relay 与 BLE 0xF7 都强制 kApp 才放行(BLE 错误码 0x87 ERR_FW_IDENTITY)。
8. BLE OTA(NimBLE)
8.1 GATT 服务
UUID 特征 属性 用途 / 格式
0xFFFF OTA Service Primary 服务主入口(设备名 ESP32-OTA-RELAY · MTU 512 · 不做 bonding)
0x0001 CTRL Write 命令字:0xF7 触发 GD32 中继(≥35B:SHA32+session2)· 0xF8 ESP32 收尾(≥33B:SHA32)· 0xFA 重启 · 0xFB 中止(RELAYING/FINALIZING 拒绝)· 0xF9 SHA 预检仅定义未实现
0x0002 DATA_IN Write + WriteNoRsp 固件包:cmd 1B(0x00 GD32/0x01 ESP32)+ seq u16 LE + total_size u32 LE(仅 seq=0 有效)+ payload ≤480B;无 CRC(BLE 链路层已有)
0x0003 DATA_OUT Notify 0xA1 窗口 ACK 8B(cmd+seq+completed_bytes)· 0xB1 FINISH 6B(stage+value)· 0xC1 SHA-256 35B
0x0004 VERSION Read 固件版本 + 构建时间字符串
FINISH stage:0x02 DONE · 0x07 REBOOT_NOW · 0x10 RELAY_START · 0x11 RELAY_DONE · 0x20 ABORTED;错误码 0x80 BUSY / 0x81 ERR_FINALIZE / 0x83 ERR_MUTEX / 0x84 ERR_GD32_SIZE / 0x85 ERR_SHA_MISMATCH / 0x86 ERR_GD32_RELAY / 0x87 ERR_FW_IDENTITY。连接参数:高速分支 7.5~10ms 间隔 + 2M PHY 请求,Windows 兼容回退 10~12.5ms。
8.2 窗口 ACK、乱序重排与内存池
前端发送窗口(BLE_OTA_ACK_WINDOW = 64)与设备端乱序重排(REORDER_WINDOW = 64)
0✓
1✓
2✓
5暂存
6暂存
3缺口
7暂存
4缺口
…64
窗口内乱序包暂存 slot(lazy malloc ≤480B),3 到达后 flush 连续补齐并 free
· seq<expected 重复丢弃(静默,ACK 字节不增长会被前端误判丢包);seq==expected 写 Flash + 连续 flush;seq 超窗 → 丢弃 + ACK 报缺口
· ACK 内容 = expected-1(最近连续落盘序号)+ 累计字节;触发:跨 64 边界 / 尾包(<480B)/ seq=0 首包就绪 / flush 跨界
· 续传:前端 resumeSeq = ceil(completed_bytes / 480);负 ACK(丢包/队列满)节流 64 包或 200ms 一条,ackSeq=expected-1 供续传
· 合并包:NimBLE 可将多次 writeWithoutResponse 合并进同一 mbuf,4096B 静态缓冲内循环解析(≤8 包/批)
DATA_IN 内存池 = 8 槽 × 512B = 4KB
一次 calloc 切分;曾为 32KB 致 OTA 收尾 0xF8 OOM
(0xF8 收尾需 image verify),缩至 4KB 并修池复用竞态
数据队列深度 144(窗口×2+16)· worker prio 12 栈 2560
收包即入队,Flash 操作全部在 worker(NimBLE host 栈仅
4096B,阻塞会断连);>512B 合并批回退 malloc
GD32 数据路径(cmd=0x00)无 reorder、严格按序写:
乱序丢弃 + 节流 ACK;ACK checkpoint(seq==0 / 每 64 包边界 / 尾包)同时是 NVS meta 持久化点
收完自动推 0xC1 设备 SHA,CTRL 0xF7 才触发中继(前端 SHA 三方比对 + 身份校验 → RELAY_START/DONE/0x86)
ESP32 路径(cmd=0x01)收尾:0xF8+SHA → 补发最终窗口 ACK → 150ms → finalizeForReboot → 0xC1 SHA 通知 → DONE 0x02 → REBOOT_NOW 0x07
图 8-1 BLE 窗口传输机制 :writeWithoutResponse 即发即忘 + 窗口 ACK 兜底 + reorder 吸收乱序;负 ACK 支持断点续传
8.3 BLE/WiFi 射频共存(ESP32-C3 单射频)
方向 机制
WiFi OTA 开始 → 挂起 BLE 软挂起 suspendStack :停广播 → 断连接(等 30ms)→ CTRL/DATA_IN 拒写(ATT 错误)→ 清数据队列;不 deinit 协议栈 (硬 deinit 会打挂共存导致上传失联);挂起期 CONNECT/ADV_COMPLETE 被拒绝/抑制
OTA 中继结束/中止 → 恢复 BLE resumeStack:重新开广播;仅曾 hard-deinit 才完整 serverStart。/ota/relay 失败路径也会 resume
BLE OTA 期间 → WiFi 让步 保持 WIFI_PS_NONE,仅暂停 RSSI 轮询 (射频活动干扰 BLE 连接事件);标志防重入;断连按系统状态清理会话(GD32_RELAYING/REBOOTING 保留等自然结束,WiFi 通道会话不误杀)
9. WiFi 管理与 HTTP API
9.1 STA → AP 自动降级
STA 优先(编译期)
SSID「004舰桥」· FAST_SCAN
国家码 CN(信道 1-13)· WPA2
重连任务(独立,防死锁)
冷启动 retry≤3:退避 2s×retry
否则 1s 固定 · 5 次耗尽 → 放弃
30s 连接超时
waitStaConnected 每 1s 轮询
耗尽立即返回超时
AP 模式
ESP32_OTA_RELAY
12345678 · 192.168.31.1
信道 6 · ≤4 台
超时
降级切换:esp_wifi_stop → deinit → init → 重新注册 WIFI/IP 事件回调 (deinit 会丢失)→ initAp()
GOT_IP 经信号量通知(DHCP);AP 模式先停 DHCP server、设静态 192.168.31.1/24 再启动
事件回调仅 xTaskNotifyGive 唤醒重连任务(回调里直接 esp_wifi_connect 会死锁);RSSI 每 10s 喂健康监控(BLE OTA 期间暂停);WIFI_PS_NONE 保吞吐
冷启动(POWERON/BROWNOUT):统一等 400ms 再初始化 NVS/WiFi(STA 路径 esp_wifi_start 前再等 300ms)
清 NVS nvs.net80211 连接缓存,欠压复位额外清 phy 强制重校准;NVS 损坏自动擦除重建
扫描用 WIFI_FAST_SCAN(扫到即连)——全信道扫描会让 OTA 重启后页面恢复多等 3~8s;listen_interval=3;无 mDNS,访问靠固定 IP
图 9-1 WiFi 模式管理 :STA 优先 + 有界退避重连 + 30s 超时降级 AP;事件驱动 + 独立重连任务避免死锁
9.2 HTTP 服务器与路由表(22 条路由 / 21 个唯一路径)
httpd 端口 80 · httpd 栈 7168B · max_uri_handlers 48(22 条 handler 路由 / 21 个唯一路径,另为每个 path 注册 OPTIONS CORS)· max_open_sockets 6(约束 ≤ LWIP 12 − 内部 3)· LRU 踢除 · recv/send 5s 超时 + keep-alive 探针。无任何认证 (见 §19);CORS 全放开(前端本地托管跨源调用)。错误响应统一 {"status":"error","code":"snake_case","message":...}。
方法 路径 功能
POST /ota/upload固件上传(JSON 头 {"type":"esp32"|"gd32","total_size":N} + 二进制流式);GD32 路径流式写 gd32_ota + NVS meta + 收尾算 SHA;chunked 拒绝
POST /ota/updateESP32 OTA 收尾 {"sha256","verify_only"};SHA 不匹配回 error 并带 device SHA;成功后 100ms 重启
POST /ota/relay触发 GD32 中继:校验前端 SHA-256 + 身份 kApp → 投递 dispatch;失败恢复 BLE
POST /restart重启 ESP32
GET /logs日志环 JSON chunked 流式(512B 栈缓冲攒块,替代 4KB malloc;堆 <4KB 返回 [])
GET /system/status全量状态(?lite=1 轻量、?layout=1 加内存布局);堆极紧自动降级简包
GET /system/probe单次 GD32 APP 存活探测(HELLO/PING),返回版本/uptime
GET /system/heap_low_log最近一条「堆创新低」快照(alloc hook 最后一击归因)
POST /can/send_batchCAN 发送唯一入口,1~128 帧,512B 滚动缓冲流式解析;单帧 1200ms / 多帧 80ms/帧;SPI 失败或 0xE2/0xE3 致命中止并报 skipped
POST /can/send_burst0xC7 跨通道突发(≤24 帧,按容量分块背靠背;id=(node<<16)|suffix)
GET /can/feedback0xC1 按需查询三路统计+ID 快照(?lite=1 轻量)
GET/POST /can/mode查询/切换接收模式 0 全收 / 1 过滤 / 2 CAN0→1/2 桥接
POST /can/silent0xC8 通道收发器 Silent 只听不发(OTA/send_batch 独占 SPI 期间拒绝)
POST /spi/send原始 SPI 帧透传 {"type","data","timeout_ms"}
GET /motor/state电机状态缓存(0xC5;?bin=1 二进制协议 12B 头 + 92B/节点,省 66% 体积)
GET /motor/version电机版本缓存(0xC6 事件推送)
POST /motor/ota电机固件上传(?ch=N;整区强制擦除;SHA 随响应返回)
POST /motor/ota/relay触发电机 OTA(SPI 0xD0 START + feedTask)
GET /motor/ota/status电机 OTA 状态机进度(0xD2 缓存)
POST /motor/ota/cancel取消(异步 cancel task,先 resetCredit 再 CANCEL)
GET /spi/speedtestSPI 性能测试(0xA1 PING 探针,RTT 直方图/5 段细分;临时功能标记可删)
共享 JSON 工作区 :3KB .bss 缓冲 + 惰性互斥供 /system/status、/can/feedback、/motor/state(JSON) 等串行复用,避免峰值堆叠 malloc;/system/status 分 4 块 chunked 发送。motor.html 三条轮询链路(/motor/state、/can/feedback、/system/status)服务端保证互不阻塞。
10. CANFD 桥接(0xC0~0xC8)
ESP32 不直接接触 CAN 总线:HTTP/BLE 请求翻译为 SPI 命令,GD32 驱动三路 TCAN1057AV 收发器。帧约定 [通道 1B][扩展 ID 4B 大端][数据 0~64B]。
10.1 命令族
TYPE 名称 载荷 / 行为
0xC0 CAN 单帧发送 req = ch + id(4B BE) + data(最短 5B,DLC=0);GD32 回 ACK 或错误码 E0~E7(payload[0])
0xC1 统计 + ID 快照页 req=[ch,page];resp = 28B 固定头(含 mode/snapshot_seq/rx/tx/rx_drop/error/tec/rec/bus_state )+ 条目 (8+dlc)=id 4LE+dlc+flags(FD/BRS/ESI)+rsv+data;快照上限 16 条,page0=条目 0~7、page1=8~15;GD32 亦可 1Hz 主动推送(现默认请求驱动,§12.2)
0xC2 设置接收模式 0=全收 · 1=按 ID/掩码过滤 · 2=CAN0→CAN1/CAN2 桥接 (每帧按原格式自动转发);回实际生效模式
0xC3 ID 快照完整版 固定头 4B + 条目 (16+dlc),含 seq/rx_count;响应可长,不进短响应读预算白名单
0xC7 跨通道突发发送 多电机同步:4B 头 [count][dlc][suffix_hi][suffix_lo] + count×(chnode(1B=(ch<<4)|node)+data);单次 ≤16 帧(容量 = min(16,(236−4)/(1+dlc)))
0xC8 收发器 Silent req=[ch, silent 0/1];回 [ch 回显][actual][ch_en];错误 E0 过短/E1 通道非法/E2 S 脚回读不符(标志已回滚)
10.2 0xC7 突发同步原理(多电机同发)
ESP32
一条 0xC7 装载 N 帧
只等「一个」ACK
GD32 CS 释放 ISR 内
对每帧调非阻塞 transmitFrame(仅装载邮箱)
全部装载完成后才回一个 ACK
ACK payload = 实际 count + 每帧 CanTxResult
CAN0 邮箱 → 上线
CAN1 邮箱 → 上线(偏差 <1ms)
CAN2 邮箱 → 上线
组内近同时 · 组间背靠背
HTTP 层 /can/send_burst:单请求 ≤24 帧,超出单次容量按 canBurstCapacity(dlc) 分块、块间背靠背 SPI 事务(<1ms);CAN ID = (node<<16)|suffix(CMD=0x0200)。
单帧总线故障(0xE2 BUS_OFF / 0xE3 TEC>96)视为 fatal 中止剩余帧,由 skipped/abort_code 告知前端;0xE4 邮箱满按帧透传状态由前端冷却重试。
图 10-1 0xC7 跨通道突发 :装载在 ISR 内完成 + 单 ACK 确认,是「组内近同时」的保证;对比逐帧 0xC0 有帧间调度抖动
10.3 0xC0 错误码(GD32 细分)
码 含义 ESP32/HTTP 处理
E0 payload 过短(<5B) 参数错误
E1 通道非法 参数错误
E2 BUS_OFF fatal :批量/突发中止剩余帧
E3 TEC>96(对端 ACK 失败率高) fatal :同上
E4 邮箱满 按帧透传,前端冷却重试
E5 参数无效 / 电机 OTA 会话残留(0xD0 语境) motorOtaStart 收到 E5 自动 CANCEL→resetCredit→重试 START 自愈
E6 通道处于 Silent(入口即拒) 不计错误计数
E7 通道被电机 OTA 独占(跨总线排他) OTA 结束自动恢复,不计错误计数
10.4 0xC8 收发器 Silent(只听不发,0.5.2)
GD32 GPIO(S 脚)
CAN0=PA8 · CAN1=PB14 · CAN2=PB9
S=1 驱动器关断 / S=0 Normal
S 内部上拉:MCU 复位即 Silent
TCAN1057AV
TXD 关断(驱动器禁发)
RXD 仍镜像总线(接收照常)
退 Silent 忙等 tMODE=20µs 再放行
语义与状态
静默通道一切发送回 E6(不计错)
0xC1 接收上报不受影响
bus_state bit5=tx_silent · 不持久化(上电 Normal)
进 Silent 前 GD32 先 abort 本通道 PENDING 邮箱(驱动器已关,留着只涨 TEC);GD32 在 CS EXTI ISR 内直接回 ACK/ERROR 不回 WAIT
图 10-2 Silent 模式原理 :收发器级只听(总线无负载注入),用于总线监听/故障排查;HTTP 端点 POST /can/silent {"ch":0,"on":1}
11. 电机 OTA 中继(0xD0~0xD2)
电机节点固件(≤48KB)经 GD32 的 CAN 总线二次烧写。两段式:前端先上传到 gd32_ota 分区(与 GD32 固件共用,上传前 abortCurrent + 清 meta + 整区强制擦除),再触发 relay 由 feedTask 经 SPI 喂给 GD32。
① POST /motor/ota?ch=N
流式写 gd32_ota + meta
② /motor/ota/relay
0xD0 START{cmd,ch,fw_size}
③ feedTask(prio 7,栈 4096)
0xD1 {offset + data ≤232B} 逐块喂
④ GD32 → CAN 烧写电机 → 0xD2 上报
12B:state/err/acked/total/fw_size/used
feedTask 韧性设计
· 整体超时 120s 强制退出(防永久占用 motorOtaIsActive)
· 喂料期间 suppress ring(否则吞吐 5→1.5 chunks/s);代价是
0xD2 不更新,改主动探测:8 次 SPI 失败 / 150 次 WAIT → QUERY
偏移幂等与握手门控
· 帧带绝对 offset → SPI 超时重传幂等,GD32 重复追加不刷坏
· 每 8 个 ACK QUERY 一次,确认进 ERASE/TRANSFER 才标「传输中」
(握手阶段 GD32 也收预填充数据,防 UI 误显示 N/912)
credit 死锁与修复(线上事故沉淀)
死锁机理:取消的典型时机恰是传输卡住——最后一帧合法响应 credit=0 被锁存;GD32 resetToIdle 转空闲后不再发帧,
relay 又跑在 APP(无 session=0 帧)→ 没有入口能把 credit 送回 → 后续一切 transact 被背压拒绝 → 网页消息全断。
修复:SpiMaster::resetCredit() 双侧复位 8/8。三处先复位再动:① motorOtaCancel(resetCredit → 0xD0 CANCEL → 失败重试 → 收尾再对齐)
② motorOtaStart 收 0xE5 残留会话(resetCredit→CANCEL→resetCredit→重试 START);③ OOM 降级同步取消路径
取消异步化:motorOtaCancel 立即返回(前端不卡 "Failed to fetch"),耗时收尾由 ota_cancel task(prio 4)执行,等 feedTask 退出上限 10s,
超时强删并补 setSuppressRingService(false)(否则 ring 永久关闭、收不到任何推送)。
GD32 侧状态机:IDLE→握手×3→ERASE→TRANSFER→FINISH_OTA→(等新 APP 自主发 0x0A 0xAC 运行确认)→FINISHED;另含 PRE_CANCEL/WAIT_APP_RUN
图 11-1 电机 OTA 中继 :fire-and-forget START(ACK 常被延迟一拍吞掉但 GD32 已收,重发会被 0xE5 拒)+ 幂等喂数 + credit 自愈
12. 监控与可观测性
12.1 健康监控(HealthMonitor)
SYS_NORMAL (0) 允许一切 OTA
SYS_WARNING (1) 允许(带警告)
SYS_CRITICAL (2) ESP32 OTA 禁止
SAFE_SHUTDOWN (3) 一切 OTA 禁止·不可逆
恢复:RSSI ≥ -80 逐级回 NORMAL(任意状态可恢复)
触发阈值:温度 ≥80°C → WARNING;RSSI <-80dBm → WARNING / <-85 → CRITICAL;温度 ≥100°C → SAFE_SHUTDOWN(不可逆);堆 <10KB 告警
巡检周期:温度 esp_timer 1s(量程 20~80°C)· 主循环 5s(栈溢出标志/堆低水位快照/任务栈告警 20%)· RSSI 由 WiFi 任务 10s 喂入
OTA 门控:checkBeforeOta() 全检(SAFE_SHUTDOWN/CRITICAL 拒;RSSI ≤15s 时效 <-88 拒);checkBeforeOtaRelay() 仅温度(中继不依赖 WiFi)
堆 alloc hook:armed 后 malloc 只减不增记近似空闲,破新低记「最后一击」(size/caller/类别),供 heap_low_log 归因「碎片 vs 系统级紧」
图 12-1 健康状态机 :四级状态 + OTA 门控;温度超 100°C 不可逆关机保护
12.2 GD32 遥测(can_fb 任务,请求驱动架构)
数据源 SPI 命令 机制 内容
电机状态 0xC5 ESP32 每 250ms 逐页查询 ×14 页(page0 头部 active_count&0x7F 在线数、bit7 boot_active) 9 节点 × 3 通道 = 27 槽(node×ch 双键);每条目 92B:STATUS data[48](字节 32~35=InitStatus)+ CONFIG data[24] + rx/seq 计数
GD32 ADC 0xC4 ESP32 每 1s 查询 20B:temp×100 / vdda_mV / vin_mV / raw 等;VCC = vin_adc × (47k+4.7k)/4.7k = ×11 分压反推;5s 内算 fresh
电机版本 0xC6 GD32 事件推送(收到电机 0x00FF 应答后按铃)→ can_fb 任务 takeEvent 消费 29B/条目 ×9 节点
电机 OTA 进度 0xD2 GD32 ring push 事件 → 同上消费 12B 进度帧
GD32 初始化状态 0xA3 身份块 [15] 惰性查询,10s 失败退避 13 模块各 1B 故障码 + module_mask/critical_fail_mask
为什么改为请求驱动(2026-08) :原 GD32 主动按铃推送(0xC5 250ms/0xC4 1Hz)在 OTA 期间会撞上 ESP32 的 ring 抑制窗口,产生「按铃超时取消」刷屏。改为 ESP32 周期查询后按铃只剩真正的事件(0xC6/0xD2),OTA 期间 can_fb 任务被 pauseCanFeedbackTask 暂停,电机 OTA 期间也自动暂停避让 0xD1。0xC1 CAN 反馈则完全由 HTTP 请求时按需查询(APP 查询 0 时保持静默)。
12.3 日志系统(DebugLog)三路输出
DebugLog::log()
级别 ERR/WARN/INFO/DBG
单条 ≤128B · 堆门控分级丢弃
ERR/WARN 永不丢弃
① UART0 直写(TX21/RX20)
3Mbps · uart_tx_chars 非阻塞,满则丢
② UDP 单播 192.168.31.3:6666
socket 延迟创建 · 8 连败销毁 + 10s 冷却重建
③ 环形缓冲 64 槽→开机后缩 16 槽
满环覆盖优先牺牲 I/D 保 ERR/WARN
网页 /logs 面板
HTTP 轮询 · chunked 512B 栈缓冲
UART0 RX 泵任务(prio 1):把 GPIO20 收到的外部串口行(GD32 日志并线 H1/H2)按行转进 ring → 网页可见
图 12-2 日志三路 :历史教训——UART 同步阻塞写 5-8ms/条曾拖爆 OTA ACK 窗口(12/777 重发),现全部非阻塞/有界
12.4 其它诊断模块
模块 功能
cpu_stats 1Hz FreeRTOS runtime stats:总占用率 + 每任务 delta 占用(≤20 任务);httpd 只读静态快照,不在 httpd 线程遍历任务表
task_stack 10 项任务栈水位表(创建大小/high watermark/峰值);栈溢出 hook 硬写 UART「STACK OVERFLOW」后 abort(OTA 末尾溢出若不 abort 前端会停在 100%)
gd32_adc_feedback 0xC4 快照缓存(单快照覆盖写 + recv_ms 年龄);互斥懒创建,取锁 20ms 超时不阻塞查询节奏
motor_state / motor_version 同上缓存模型;motor_state 整表拷贝在锁内一次完成,读方永远拿到一致快照(防撕裂)
spi_speed_test GET /spi/speedtest:0xA1 PING 探针走真实 worker 路径,RTT 直方图 7 档 + 单帧 5 段细分(提交/发送/等应答/读响应/唤醒)+ 等效吞吐;文件头注明删除步骤
build_info 构建系统自动生成编译时间(fwBuildTime()),旧手写 BUILD_TIME 曾被误判为 OTA 回退
13. 诊断与容错机制总表
层级 机制 参数 针对的问题(事故沉淀)
SPI 链路 透明重试 kTxRetryMax=3,同 TX_SEQ,间隔 3ms 瞬时无响应;DUP-SEQ 去重保安全
链路熔断 3 连败 → 离线,2s 冷却放一笔探针 SPI 不通时周期查询死循环阻塞/耗尽
HELLO 重同步 session=0 触发 + 5s 冷却 GD32 重启后全命令超时死循环
credit 自愈 PB6 高强制置 1;resetCredit() credit=0 锁存死锁(§11)
撕裂防护 CS 间隔 ≥1ms · 快速类型禁盲读 · 读预算收紧 RX DMA mid-pulse 重装 → 0x82 0xE8 → 偷读风暴
OTA 协调 忙态看门狗 150s(>中继预算 120s) 异常退出导致系统永久「忙」
epoch 会话保护 begin 递增,清理校验 epoch 旧上传失败清理误杀新会话
半会话清理 abort 必发 SPI CANCEL 0x03 GD32 停在 WAITING_* 半会话
数据完整性 SHA-256 双端比对 前端算 / 设备读回算 / meta 存 传输错误静默落盘
包身份校验 向量表区间判定 kApp BOOT 包刷进 APP 区(§7.6)
NOR 全擦 电机 OTA 整区预擦 残留位叠加单 bit 翻转
资源保护 池化/会话内分配 BLE 池 4KB · 上传 4KB RAII · 对齐缓冲 OTA 期 calloc BLE 收尾 0xF8 OOM(曾 32KB 池)
栈水位收敛 S ≈ 峰值 + 1.5~2KB OTA 末尾 httpd 栈溢出假死
14. 关键配置常量速查
类别 常量 = 值
SPI CONFIG_DCU_SPI_CLOCK_HZ=13333333 · Mode 0 · 队列 4 · SPI_OTA_FRAME_MAX=256 · BOOT/APP TX 缓冲 128/256 · V2_MAX_PAYLOAD=236 · 读预算 96(短响应)· credit 8/8 · kTxRetryMax=3 · 熔断 3 败/2s · CS 间隔 1ms · poll backoff 1ms
GD32 OTA OTA_MAX_FIRMWARE_SIZE=216KB · 包载荷 48B(帧 64B)· OTA_BLOCK_SIZE=4096(RAII)· 每 32 包健康检查 · 预算 10s+25ms/包 ≤120s · 卡死 10s · 协调器看门狗 150s
电机 OTA MOTOR_OTA_MAX_FW_SIZE=48KB(0xC000) · 帧 ≤236B(4B offset + ≤232B data)· feed 120s 整体超时 · 8 败/150 WAIT → QUERY · 退出轮询 8s/150ms
BLE MTU 512 · 载荷 480B · ACK 窗口 64 · reorder 64 槽 · DATA_IN 池 8×512B · 队列 144 · 高速连接 7.5~10ms(回退 10~12.5ms)· 中继等待 120s · 负 ACK 节流 64 包/200ms
WiFi/HTTP STA 超时 30s · 重试 5(冷启动退避 2s×retry≤3,否则 1s)· AP 192.168.31.1/6/4 台 · 端口 80 · httpd 栈 7168 · sockets 6/LWIP 12 · TCP_WND 8192 / SND 5760 · RSSI 采样 10s
健康 温度 80/100°C · RSSI -80/-85/-88dBm(15s 时效)· 堆 10KB · 栈告警 20% · 巡检 5s · 温度采样 1s
日志 UART0 3Mbps(TX21/RX20) · UDP 192.168.31.3:6666 · 环 64→16 槽 ×128B · 门控 6KB/10KB/4KB · CONFIG_SPI_DEBUG_LOG_LEVEL=2(编译期裁 SPI DBG/INFO)
任务栈 http_server 7168 / wifi_ota 3072 / ota_relay 2560 / spi_worker 5120 / ble_ota 2560 / ble_worker 2560(p12) / health_mon 3072 / cpu_stats 3072 / wifi_reconnect 2560 / can_fb 4096 / motor_feed 4096 / ota_cancel 3072 / uart_rx 3072
欠压/Flash Brownout LVL7(~2.8V) · Flash DIO(禁 QIO)· FreeRTOS runtime stats 开启
15. 目录结构
ESP32_COMM/
├── CMakeLists.txt # project(ESP32_COMM)
├── sdkconfig(.defaults) # SPI 时钟/欠压 LVL7/LWIP/NimBLE/FreeRTOS stats 等
├── config/partitions.csv # 4MB 分区表(§2.2)
├── main/
│ ├── include/ # app_config.h(统一常量)/ version.h(0.5.2)/ build_info.h
│ ├── core/ # main.cpp 启动编排 · system_boot.cpp 16 位自检 · build_info
│ ├── server/ # wifi_ota(STA/AP+路由表)· http_handlers(22 端点)· ble_ota*(GATT/窗口/reorder/池)
│ ├── ota/ # ota_coordinator(9 态单例)· esp32_ota_core(本机 OTA)
│ ├── relay/ # ota_relay(7 态事件状态机 + 安全握手 + 逐包转发)
│ ├── storage/ # gd32_flash_store(分区读写/擦除水位/NVS meta/SHA/身份校验)
│ ├── transport/ # spi_master(Link v2 链路+worker)· protocol(AES/nonce)· canfd_bridge(0xC0~C8 桥)
│ └── monitor/ # health_monitor · debug_log(3 路+RX泵)· can_feedback_task(0xC4/C5 周期查询)
│ # motor_ota_relay(电机 OTA feed)· motor_state/motor_version/gd32_adc_feedback
│ # cpu_stats · task_stack · spi_speed_test · spi_ring_cache.h
├── components/transport/ # 组件封装(CMakeLists)
├── docs/ # can_protocol.md · frontend_architecture.md · 架构评审 · 本文件
├── spi_link/(兄弟目录) # 三工程共享 SPI Link v2 协议真源 + golden-vector 自测(../../../spi_link/)
└── tools/(已移出仓库) # 网页控制台;快照在 .cache/ws_era_frontend_backup/(§17)
16. 构建与烧录
16.1 编译(ESP-IDF v6.0,Windows PowerShell)
# 设置工具链环境(IDF_TOOLS_PATH / IDF_PATH / PATH,见仓库历史 README 或 .devcontainer)
Set-Location "G:\Robot_v2\DCU\ESP32_COMM"
python "F:\.espressif\v6.0\esp-idf\tools\idf.py" build
# 产物:build/ESP32_COMM.bin + bootloader + partition-table(零 error/warning 基线)
版本发布流程:手动递增 main/include/version.h 的 MAJOR/MINOR/PATCH 与 FW_VERSION_STRING(并把变更摘要写进该文件注释),GIT_HASH 填 git rev-parse --short HEAD;编译时间自动生成。
16.2 三条烧录路径
路径 流程
串口(首次/恢复) idf.py -p COM15 flash monitor;固件反复重启等严重故障时唯一恢复手段
WiFi OTA(ESP32 自升级) 网页控制台选 build/ESP32_COMM.bin → 前端算 SHA → /ota/upload → /ota/update(SHA 比对→set_boot→重启)→ 前端按 banner「启动」正则判定恢复(uptime<120s 判重启完成)
BLE OTA Web Bluetooth(Chrome/Edge):DATA_IN 480B 包 ×64 窗口 → 0xF8+SHA 收尾重启;0xFB 可中止
GD32 中继 上传 .bin/.hex 到 gd32_ota → /ota/relay(或 BLE 0xF7)→ 六阶段转发 → PING 确认 APP 存活;可重复触发(STORED/COMPLETE 均可)
电机节点 /motor/ota?ch=N 上传 → /motor/ota/relay → 0xD0/0xD1 经 GD32 CAN 烧写 → /motor/ota/status 轮询
17. 网页控制台前端
托管方式 :Web 资源不嵌入固件,由 tools/serve.bat(python -m http.server 8080)本地托管,浏览器跨源调用 ESP32 REST API——改网页无需重编固件。零构建原生 ES Modules(ES module 有 file:// CORS 限制,必须经 http 访问)。tools/ 目录已移出本仓库 (历史快照在 .cache/ws_era_frontend_backup/),约定文档见 docs/frontend_architecture.md。
页面 功能
index.html(OTA 控制台) WiFi/BLE 双通道切换 · GD32/ESP32 目标选择(GD32 支持 .hex 自动转换)· 上传/SHA/阶段流程条驱动 · 设备状态(含启动自检面板 = SystemBoot 位图)· 运行指标(ESP32 温度/GD32 温度 0xC4/输入 VCC 分压反推/GD32 APP·BOOT 版本)· 日志面板(/logs 轮询 + 级别过滤)· 「堆与任务栈」抽屉(CPU 条形/水位/任务表)· CAN/电机页入口
motor.html(电机控制台) 独立 seed 主题;节点总览 1Hz · CMD 控制/周期重发 · CONFIG/PID · 版本查询 · NodeID 设置 · 电机 OTA relay(进度 0xD2)· 波形显示;三条轮询链路互不阻塞
can.html / can_monitor.html DBC 导入解析 · 报文/信号编辑 · 单发/批量发送 · 发送历史与会话持久化 · 接收监控(独立页/抽屉,0xC1 快照分页)
关键约定:JS import 不带 ?v=(版本戳只放 HTML 标签);统一 data-action 事件委托禁内联 onclick;js/ble/*(Web Bluetooth 时序)与 js/ota/*(OTA 状态机/window.* 总线)为红线区;重启完成判定 = banner 正则 + uptime<120s。
18. 遗留与临时项(补充 §1.2 功能一览)
项 状态 说明
BLE CTRL 0xF9(SHA 预检) 未实现 仅 app_config.h 定义常量,CTRL 回调无处理分支;WiFi 侧 /ota/update 有 verify_only 等价功能
0xC3 ID 快照完整版(ESP32 侧) 协议就绪/桥未主动调用 canfd_bridge.h 有常量与条目布局定义;GD32 侧已实现;ESP32 HTTP 无独立端点
0xC1 GD32 1Hz 主动推送 保留 协议支持但现架构默认请求驱动(§12.2)
/spi/speedtest 临时功能 性能剖析用,代码标注可整行删除(3 处挂接点)
HTTP 认证 / 凭据治理 未实现 见 §19
mDNS 不存在 历史 README 讹误;仅 lwIP 编译项支持 mDNS 解析
19. 安全审计与已知限制
2026-08-16 安全审计结论(待整改) :当前固件面向可信局域网设计,存在以下已知风险——
① 零认证 :HTTP 22 个端点全部无鉴权(含固件上传、重启、CAN 发送),仅靠局域网边界;Bearer token 方案已设计未实施。
② 凭据硬编码 :WiFi SSID/密码、MASTER_KEY 占位值(01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10)明文入仓,建议迁 NVS/Kconfig 并做 CI 扫描。
③ 无固件签名 :ESP32 侧仅 SHA-256 完整性校验(非来源认证);GD32 侧依赖预共享 MASTER_KEY 的挑战-应答(对称密钥,ESP32 泄露即整链泄露)。
④ AES-ECB 模式 :OTA 数据包逐 16B 块独立加密(确定性模式),相同明文块产生相同密文块。
缓解现状:BLE 需物理接近;GD32 固件包身份校验防误刷;nonce 每会话随机绑定防重放;诊断统计(0x82 归因/偷读签名/熔断计数)全部可观测。
其他工程性限制:SPI 13.33MHz 为实测上限(≥16MHz 全双工不可行;12M 与 13.33M 重发率相当,时钟非重发主因);Flash 必须 DIO (QIO 读回 SHA 必错);CANFD 数据字段 64B 不可压缩(用户明确使用长帧);GD32 包修复依赖三工程协议同步(spi_link/ 兄弟目录 + 两侧判据成对修改)。
2026-10-19 复核更新(专家评审,基线 af1f682 / v0.5.2) :上方 2026-08-16 四项结论原文保留,复核结果如下——
① 零认证:
仍存在 ,且发现
/spi/send 可原样透传任意 SPI 帧类型、
timeout_ms 无上限,升级为
致命 (评审 F-01 / S-12)。
② 凭据硬编码:
仍存在 ,且 STA 口令为真实口令、已进入 git 历史,需轮换(S-08)。
③ 无固件签名:
仍存在 ;另发现 ESP32 回滚保护取消过早、回滚开关所在
config/sdkconfig.defaults 未被构建引用(S-01 / S-02)。
④ AES-ECB:
仍存在 。
“缓解现状”中“nonce 每会话随机绑定防重放”
不成立 :nonce 仅 4 字节 tick 变化且明文写入日志(S-06)。新增:BLE GATT 无配对/加密(S-09)。
完整问题清单、既有评审跟踪与修复路线图见
docs/review/ESP32_COMM_专家评审_2026-10-19.html ;历史评审原文已恢复至
docs/review/archive/。
20. 版本演进摘要(0.3.x → 0.5.2)
版本 要点
0.3.81~0.3.85 GD32 OTA 包级防呆(向量表身份)· UART0 RX 泵(/logs 汇聚外部串口)· GD32 APP 初始化状态可视化(0xA3)· 模块级故障码 hex · CAN 反馈请求驱动改造(0xC5/0xC4 改周期查询)
0.4.2 serviceRing 偷读迟到 RESPONSE 的 seq 失配修复(「一直读到 0x82」根因)
0.4.3~0.4.8 0x82 量化归因插桩 · 快速类型禁盲读 · CS 事务间隔 1ms · ring_steal_diag 撕裂形态签名
0.4.9 tx_total/tx_retry/tx_fail 计数进 /system/status(透明重试不再静默)
0.5.0 SPI 链路稳健性 + OTA 内存(上传 4KB RAII)+ 状态诊断
0.5.1 SPI 调试日志编译期门 CONFIG_SPI_DEBUG_LOG_LEVEL(84 条 DBG/INFO 裁剪)
0.5.2 SPI 0xC8 通道收发器 Silent + POST /can/silent · 0xC1 bus_state 新增 bit5=tx_silent · 静默通道发送回 0xE6
21. 文档同步说明(HTML ↔ README)
本文件(docs/project_introduction.html)是项目的完整介绍白皮书 :算法细节、协议字节级定义、状态机与事故沉淀。根目录 README.md 是它的浓缩版 (快速上手 + 速查表),两者内容同源。
同步规则(请后续维护者遵守) :
① 任何影响「功能清单、协议/常量数值、状态机、端点表、实现状态」的代码变更,须同时更新两份文档 ;
② 变更摘要写入 main/include/version.h 注释,文档头部版本行随之更新;
③ HTML 偏重原理与完整参数,README 偏重速查与上手,细节以 HTML 为准;
④ 两文件顶部均带「文档同步版本」(当前 0.5.2 / 2026-10-11 ),修改后同步递增日期。
ESP32_COMM 项目全景介绍 · 固件 v0.5.2(git 770ae14)· 文档版本 2026-10-11 ·
与 README.md 同步维护 · 本文所有数据以源码为准(main/ · spi_link/ · config/)