GD32 刷写流程对比:ESP32 中继(老)× EtherCAT 直灌(新)

2026-09-13 · BOOT v1.1.0.7(ER_ROM0 64,552 / 73,728 B)· 主站 ecat_native.exe 22:18 构建 · 每条数字带 文件:行号 · 镜像基准 = DCU_APP Debug .hex 展平后 124,992 B(本机实测)
2 604 → 527同张镜像的链路往返次数(结构硬数字)
48 → 241 B每帧净落 flash 的字节数
252 KB → 无中间存储:ESP32 缓存分区整个消失
0 次新流程的上板实测次数 —— 一条都没跑过

一句话结论

新流程不是"更快的刷法",是"少一层先决条件的刷法"。省下的 5 倍往返是副产品,真正的收益是:出厂首刷不再要求板上先有一颗刷好并配好网的 ESP32。两条路并存互为备份,而且刻意复用了同一个 flash 写入者、同一个提交顺序、同一条掉电不变式 —— 新流程没有引入第二套"刷坏了怎么回来"的语义。

逐维度对照

维度老:WiFi → ESP32 中继 → SPI新:PC 主站 → EtherCAT → SDO
先决条件板上必须已有 ① 刷好固件的 ESP32 ② 配好网的 ESP32 ③ 上位机知道设备 IP。esp32_server.py:830总线上有电、从站在 PREOP 即可。ESP32 可以完全没刷过。
参与部件浏览器 → 本机 2625(只发静态页)→ ESP32:80 → GD32。esp32_server.py:1-7浏览器 → 同一个 ecat_native.exe(页面也它发的)→ Npcap/I210 → GD32。ESP32 不在链路里。
镜像中间存储ESP32 flash 的 gd32_ota 分区 252 KB@0x11000,两段式:先整份存下算 SHA,/ota/relay 才起中继。partitions.csv:7-8;http_handlers.cpp:515;ota_relay.cpp:1128PC 内存 216 KB 静态缓冲 BOOT_DL_IMG_MAX 0x36000。不落任何中间设备。
浏览器→服务端请求1 次整文件 POST(/ota/upload)+ 1 次 /ota/relay + 轮询。无断点续传:每次从偏移 0 重写。upload-esp32.js:238-247;http_handlers.cpp:4406 次 push(每次 24,576 B → hex 49,152 字符,卡在 native 的 65,535 B body 上限内)+ 1 次 run。已冒烟:received 24576→49152→73728 精确累加
每帧净落 flash线上一帧 = v2 头 16 B + DATA 64 B + CRC 4 B;64 B 里只有 48 B 是固件。config.h:140-146;ota_relay.cpp:1135-11514 KB 块 = 17 个邮箱帧(1 initiate 带 240 B + 16 段×247 B)⇒ 每帧 241 B,是老的 5.0 倍。sdoserv.c:210,744
链路往返次数124,992 / 48 = 2 604 包,每包一次「发 → 等 ACK(50 ms 上限,最多重发 5 次)」。ota_relay.cpp:1181,1185124,992 / 4096 = 31 次 SDO 传输 = 527 帧,外加总长 / begin / commit 三个控制 SDO。
鉴权AES-128 三步双向握手,会话密钥作为前缀喂进每包 CRC32(防重放/篡改)。OTA.cpp:976-1246,1414-1442出厂形态零鉴权。唯一裁决点 ecatArmed() = 配置页令牌 或 A 区不可启动;commit 成功即收回令牌,用完即弃。OTA.cpp:792-812,905-915
主站复判身份?不存在该问题 —— ESP32 是唯一持有 AES 密钥的对端。不复判。读 0x1008 只当日志读数。APP 的对象字典里没有 0x2500/0x2501(实测只有 0x1600/0x1A00/0x1C12/0x1C13/0x2000/0x6000/0x7000),写上去只会 SDO abort,擦不到一个字节。
掉电不变式完全相同,且是刻意复用的。擦除前先把 A 区三面标志(CHECK/AVAL/EN)全标 NOT_AVAL ⇒ 任何时刻掉电都表现为「AVAL 未置 = 不可启动 = 留守 BOOT 可重灌」。OTA.cpp:635-637;config.h:37-43;main.cpp:229-402
提交顺序向量表校验 → calculateAppCrc+writeAppCrc → CHECK → AVAL → EN;跳转延后到 process() 排空 pending_app_jump。OTA.cpp:372-410逐字对齐同一条,只在 CRC 之后多插一步收回令牌。CRC 必须先于 AVAL —— verifyAppCrc() 对擦除态 0xFFFFFFFF 返回 true,真门控是三面标志。OTA.cpp:900-915
对在跑业务的影响SPI 中继是独立链路,不占 EtherCAT 环;但会话期间 BOOT 会丢弃 CAN OTA 帧。OTA.cpp:303-309;Can.cpp:174-179run 全程持 g_bus_lock + loop_stop() ⇒ PDO 循环冻结数秒。但目标本来就得停在 BOOT(进不了 SAFEOP/OP),实际没有循环可冻。
可观测性进度靠轮询 /system/status 的 relay_seq/relay_total_packets;每 32 包一次健康检查。upload-gd32.js:58,194;app_config.h:86从站 0x2500:03 实时报已落盘字节、:04 报 arm/会话位;主站 probe 一次读回,进度条两侧各一个独立读数。
刷 BOOT 自己两条都不行。BOOT 跑在 0x08000000,改不了自己正在执行的 flash ⇒ 永远只能 SWD/JTAG。新流程没改变这一点。
刷"正在跑 APP 的机器"可以 —— 这正是老流程的主用途(APP 收 OTA 请求、复位进 BOOT 再灌)。main.cpp:249-272还不行。A 区可启动 ⇒ 令牌是硬门,而置令牌的入口没接(ecatArmSet 目前只有 commit 里那个"收回"调用者)。要么 BOOT 加密会话加命令码(要花余下的 9,176 B),要么 APP 侧复位进 BOOT 前自己写 word8。
BOOT flash 代价基线 36,340 B+28,212 B ⇒ 64,552 B,余 9,176 B(用 87.6%);RAM 32,392 B(堆为分段下载放大到 16 KB,栈仍 1 KB)。GD32E50X_BOOT.axf.map
成熟度产线在用;有 CLI 与刷写台账 —— 只有 ESP32/scripts/flash_gd32.py 写 lab/_last_gd32_sha.txt,且要拿到"板上版本变了 / uptime 归零"的证据才写。scripts/flash_gd32.py:36,132-148代码接通、零上板。主站软件路径已冒烟(路由分发 / 缓冲累加 / 边界值全过),但 ESC 起板、0x1008 可读、SDO 字节序三条只有源码级推导。

关于"快了多少" —— 不能说

老流程 ESP32 侧注释有实测:正常 ~2 ms/包、"777 包正常仅 ~1.6 s"。ota_relay.cpp:356-368 按这个常数换算,124,992 B = 2 604 包 ≈ 5.2 s 纯传输。
新流程没有任何一次真机计时,这里填不出可比的数字 —— 5 倍往返差是代码结构决定的硬事实,端到端耗时不是。第一次上板要记的就是三件事:begin 实际阻塞多久、单块实际耗时、整轮总耗时。

为什么值得两条都留

  • 覆盖场景不重叠:出厂首刷只有新流程能做(那时还没有 ESP32 固件);刷在跑的机器目前只有老流程能做(有 AES 通道)。
  • 故障域独立:WiFi 配不上 / ESP32 没刷 / 网卡拉不起 / 从站没进 PREOP —— 任意一边坏,另一边还是条路。
  • 共用同一个落盘点:两条最终都调 eraseApplicationArea / writeApplicationData / validateApplication / OTA_SET;BOOT 里始终只有一个 flash writer,ecatBegin 在 SPI/CAN 会话活跃时直接拒绝并发。

新流程的未证清单

#待证判据(一条就够)
1BOOT 侧 ESC 起不起得来(EXMC/HBI/RST#/IRQ 这套移植层从未在 BOOT 跑过)串口出 [BOOT][ESC] PDI ok DL=… 邮箱可用;否则 FAIL PDI 未就绪 DL=… EEP=…
2主站认不认出它在 BOOTprobe 回 name="GD32E508 DCU BOOT" 且 armed=1
3SDO 分段载荷字节序(源码推得原序,未实测)用现成 /api/sdo 往 0x2501 写 00 01 02 … 0F,看落 flash 后回读是否逐字节同序;不对就只在 ObjFoeDlWrite 一处加转换
4begin 的整区预擦到底阻塞多久、8 s 超时够不够串口时间戳与主站 ec_SDOwrite 往返计时对表
5刷完 APP 真能起来重新连接后 probe 的设备名变回 "GD32E508 DCU" —— 从另一次启动读的,不自证

顺带查到的一个既有缺陷(老流程同样有)

writeApplicationData() 的循环条件是 i + 4 <= size ⇒ 不足 4 字节的尾巴被静默丢弃,注释里写的"用 0xFF 补齐一个 word"代码并没有做。OTA.cpp:762-782
老流程每包固定 48 B(4 对齐)所以碰不到,只有非 4 对齐的镜像总长会在最后一包丢 1~3 字节;新流程在主站侧补 0xFF 到 4 对齐并按补齐长度声明总量,绕开了这个坑,但没有去改已在产线跑的 SPI 路径。要修该单独一次改动、单独一次回归。

老流程:浏览器 → ESP32 → SPI → GD32 BOOT

浏览器算 SHA-256
整文件一次 POST
→
本机 2625只发静态页
不碰硬件
→
ESP32 :80/ota/upload
每 4 KB 写分区
→
ESP32 flashgd32_ota 252 KB
NVS 存元数据
→
/ota/relayAES 三步握手
逐包读回转发
→
GD32 BOOTSPI ISR 入队
主循环写 flash

红框 = 新流程里整个消失的一跳。

关键常量

项值出处
每帧总长 / 其中固件数据64 B / 48 Bconfig.h:140-146
SPI v2 帧开销20 B(16 头 + 4 CRC)spi_link_v2.h:34-36
SPI 时钟 / 注释实测每帧13.333 MHz / ≈96 µssdkconfig.defaults:20;spi_master.cpp:80
ISR→主循环 DATA 槽 / flash 写缓冲槽4 / 8config.h:178,180
ESP32 上传块 / 固件上限4 096 B / 216 KBapp_config.h:74,76
单包等 ACK / 最多重发 / 跨包停刷50 ms / 5 次 / 10 sota_relay.cpp:1181,1185,53
转发预算25 ms/包,总 10~120 sota_relay.cpp:356-368
BOOT 会话超时 / APP 延后启动窗口50 000(≈1 s) / 10 000 msconfig.h:168,169
握手各步超时(ESP32 侧)800 / 5000 / 3000 / 3000 msota_relay.cpp:942,1036,1062,1090

一帧 DATA 的字节排布

0–3会话号 LE
4–7包号 LE
8–5548 B AES-128 密文
56–59结束标志 IFEND
60–63CRC32(密钥参与)

外面再套 SPI v2 头:SOF0/SOF1/VER/FLAGS、SESSION@4、TX_SEQ@8、ACK_SEQ@9、CREDIT@10、TYPE@11、LEN@12,尾部 4 B MPEG-2 CRC32。spi_link_v2.h:34-51;OTA.cpp:1002-1018;ota_relay.cpp:1135-1151

握手三步(双向认证)

步上位机 → BOOTBOOT → 上位机出处
1魔术字 00 AC回显 ACK,转 WAITING_RANDOMOTA.cpp:976-988
216 B nonce_server36 B = UID(12)+nonce[8..15](8)+AES(K_master,K_session)(16),转 WAITING_CONFIRMOTA.cpp:1195-1219
3AES(K_session, nonce XOR 0x55) 16 B0xBA 0xC0,转 CHECKING_FIRMWAREOTA.cpp:1224-1246

状态机全序 IDLE → WAITING_RANDOM → WAITING_CONFIRM → CHECKING_FIRMWARE → RECEIVING_FIRMWARE → FINISHEDOTA.h:223-230。每包 CRC32(IEEE) 先把 16 B 会话密钥喂进去、再喂 48 B 明文,所以改一比特或重放旧包都对不上。OTA.cpp:1414-1442 主密钥两端硬编码同值。config.h:158-161;app_config.h:150-153

从收到字节到落 flash

失败与恢复分流

main.cpp run() 读三面标志分流:二者皆 AVAL 且 CRC 过 → 直接跳;CHECK AVAL / EN NOT_AVAL → 开 10 s 等 OTA 窗口;A 区坏但 B 区有效 → move_app_data() 整体搬回;都不行 → 长期留守等重下。FWDGT 复位会把 EN 标 NOT_AVAL,阻止反复跳进卡死的固件。main.cpp:204-210,229-402

代码里没有的东西(别当成有据)

  • SPI 中继链路的实测端到端耗时 / KB 每秒 —— 只有预算常数和注释里的 ~2 ms/包。
  • WiFi/HTTP 上传的断点续传:NVS 元数据预留了 stored_size/last_seq,HTTP 路径未实装。gd32_flash_store.h:23;ota_coordinator.cpp:246
  • BOOT 侧握手各步的独立毫秒超时:统一走 OTA_TIMEOUT 计数。OTA.cpp:431

同一后端的三条入口

浏览器、tools/flash_gd32.py(裸 BIN、不写台账)、tools/ESP32/scripts/flash_gd32.py(带 {"total_size":N} 头、写台账)三条都打 ESP32 固件 80 端口的同一组 /ota/upload + /ota/relay + /system/status,没有独立通道;设备侧对带不带 JSON 头都兼容。http_handlers.cpp:403-407;tools/flash_gd32.py:88;scripts/flash_gd32.py:98

新流程:PC 主站 → EtherCAT 邮箱 → BOOT 写 flash

浏览器ecat.html「BOOT 刷写」
页内自带 HEX→BIN
→
ecat_native :8090/api/ota/ecat
镜像攒在 PC 内存
→
I210 / Npcaploop_stop 独占
CoE SDO 分段下载
→
LAN9252SM0 邮箱 256 B
PRAM FIFO 逐 DWORD
→
GD32 BOOTSSC 攒满 4 KB 才回调
OTAHandler 写 flash

只有 5 跳,且 ESP32 完全不在里面。页面本身也是 ecat_native 发的,所以没有跨域、没有第二个服务。

操作时序(六个 op,只有后两个碰总线)

op做什么碰总线?要点
open声明镜像长度,清 PC 侧缓冲否超 221,184 B 直接拒。不抢 g_bus_lock、不 loop_stop,所以上传过程不会把正在跑的电机循环冻掉
push追加一段 hex 到镜像尾部否24,576 B/次 = hex 49,152 字符,卡在 handle_conn 的 65,535 B body 上限内
status / abort查已收字节 / 丢缓冲否abort 只清 PC 侧,总线一个字节都没动
probe读 0x1008 设备名 + 0x2500:03/:04是纯诊断读数,不是门槛
run真正的灌写:总长 → begin → 31 块 → commit是全程持 g_bus_lock + loop_stop();只有 commit 成功才不把 PDO 循环还回来(从站正在硬复位)

BOOT 侧只挂两个对象

索引类型条目语义
0x2500REC ×4
UINT32
:0 条目数 / :1 命令 / :2 总长 / :3 已收 / :4 状态 :1 写 1=begin、2=commit、3=abort;:4 bit0=允许刷写、bit1=会话活跃、高 16 位=错误码。:3/:4 只读,读的时候现取 ota.ecatReceived()
0x2501VAR
OCTETSTRING
4 096 B
只有 :00 数据通道。建成 VAR 而不是 ARR:VAR 的 maxSubindex 恒为 0,所以只能写 :00,且 complete access 一律 UNSUPPORTED_ACCESS ⇒ SOEM 侧必须传 CA=FALSE。objdef.c:1278,1295,1352

BitSize 是 UINT16 ⇒ 单条目天花板 8191 B,块长取 4096 就是这个原因。SSC 的分段下载攒满整块才调一次 OBJ_Write(最后一帧才分发,且分发前不做通用长度校验),所以从站侧不需要 begin/data/commit 拼包状态机。sdoserv.c:292-295;objdef.c:1352;sdoserv.c:714

一个 4 KB 块在总线上的样子

initiate带 240 B
段 1247 B
段 2247 B
…×14
段 16151 B

共 17 帧搬 4096 B。段长上限两端各算一次取小者:SOEM maxdata = mbx_l − 0x10 = 240(首帧带数),SSC 段 256 − 6 − 3 = 247。全图 124,992 B ⇒ 31 块 = 527 帧。ethercatcoe.c:346,450;sdoserv.c:210

放行判据只有一个执行点

ecatArmed() = 配置页 word8 == 0xACA70001 || !(CHECK==AVAL && EN==AVAL)

后半句和 main.cpp run() 决定"跳 APP"用的是同两个 flash 字,所以四种 BOOT 留守情形(空板首刷 / 下载中断电 / A 区 CRC 失败 / FWDGT 复位)天然放行,不需要任何先决条件;反过来 APP 正常在跑时两字皆 OTA_AVAL,令牌就是硬门 —— 而令牌写不进(SDO 摸不到配置页),所以刷不动一台正常机器。

ecatCommit() 成功路径里先 ecatArmSet(false) 收回令牌再置三面标志:用完即弃。此刻 CHECK/EN 还是 eraseApplicationArea 标的 NOT_AVAL,所以收回这一拍救砖分支照样为真,不影响本次提交。OTA.cpp:792-812,905-915

刻意复用老流程的部分(一条都没新写)

新增的移植层花了多少

项值备注
无 EtherCAT 基线36,340 BPhase 0 对照组:关掉唯一调用者后 --gc-sections 剥回此值
邮箱-only SSC(空移植层桩件)57,816 B净增 21,472 B —— 原方案声称的"12 KB"是假的
真移植层 + 会话(定版)64,552 B再 +6,736 B(EXMC/HBI/CSR 双读/PRAM/RST#/IRQ + 0x2500/0x2501 + arm 门控);余 9,176 B
RAM32,392 / 131,072 B堆 0xC00→0x4000(每笔分段下载 ALLOCMEM(块长) 走 malloc);栈仍 1 KB ⇒ SDO 回调里不许开块级大数组
生成:Qoder 会话 · 证据基线 GD32E50X_BOOT v1.1.0.7 / tools/ecat ecat_native 22:18 / ESP32_COMM 现网分支 · 本轮改动均未提交 git