feat(ota): 电机 OTA 支持 EtherCAT 模式(CoE 邮箱进料 + 通道独占锁)

进料口从「只有 ESP32 SPI 一条」扩成两条,中继引擎不变:主站经 CoE 0x2511 数据窗
把电机固件明文灌进 GD32 的 48KB SRAM 暂存区,再由 comm/ecat_motor_ota 泵进原有
4KB 环缓;AES 握手、CAN 单发停等、重发、0AAC 运行确认全部沿用 MotorOtaRelay。
纯网线(ESP32 WiFi 关断、/motor/ota 打不通)场合因此也能刷电机。
偏移/幂等判据仍只有一份:feedData 拆出 feedAt,两个进料口共用。

同时把审计报告 VULN-12/13/14 落地(此前只存在于文档,一行未实现):start() 第一条
语句在 PRIMASK 内原子置位 s_channel_lock_mask,EtherCAT 桥接 sendBatch/processOutput
与 SPI 0xC0/0xC7 对锁定通道回新码 0xE7=「被电机 OTA 独占」,不冒充「邮箱满」0xE4。
反向互斥一并补上:电机 OTA 活跃时 requestEnterBoot() 直接拒绝——SPI 0xB0、CoE 0x2502、
CAN 0x320 三条交接入口共用这一个判据,避免复位把刷写腰斩在 erase 之后。

完整性口径按链路分开,不互相冒充:块级 CRC32 + 整镜像 CRC32 都在从站核,且核过才
让电机进 BOOT——失败时电机仍跑旧 APP、擦除没开始,不存在「先擦再验」的变砖路径。
CRC 为 CRC-32/B(poly 0x04C11DB7,初值 0xFFFFFFFF,无反射无末尾异或),与 v2_crc32
同算法(用公开向量 123456789=0x0376E6E7 对过)。

块长取 1024 而不是 BOOT 用的 4096:SSC 的分段下载缓冲是全工程唯一的 malloc 用户,
而 APP 的 __HEAP_SIZE 只有 3KB,4096 会被从站当场回 abort 0x05040005 Out of memory
(BOOT 的堆是 16KB 才撑得住 4096——块长能抄,堆抄不了)。这是跨仓常量,须与
tools/ecat 的 MFO_CHUNK_OBJ 同步。

APP 1.7.4(RAM ZI 97156)。已实测:0x2510 参数写与一块 1016B 数据写均被从站接受
(staged=1016、phase bit4);端到端电机刷写待复测(native 块长修复后未跑完整一轮)。
This commit is contained in:
cbb
2026-10-11 02:47:19 +08:00
parent f3d7fc0c30
commit d8b3c9767c
14 changed files with 888 additions and 76 deletions
+97
View File
@@ -0,0 +1,97 @@
/**
* @file ecat_motor_ota.h
* @brief EtherCAT(CoE 邮箱)电机 OTA 进料通路 —— 主站把电机固件灌进 DCU 本地暂存区,
* 再由本模块泵进 MotorOtaRelay 现有的 4KB 环缓。
*
* 为什么要有它(不是重复造轮子):电机 OTA 的引擎一直是 MotorOtaRelay
* (AES 握手 + CAN 停等 + 单发模式),只有一条进料路:ESP32 经 SPI 0xD1 喂。
* 纯 EtherCAT 场合(只插网线、ESP32 WiFi 关断)那条 HTTP 打不通,
* 于是前端把能力标成 supportsOta=false。本模块只换**进料口**,不换引擎:
* - 分块写入仍走 MotorOtaRelay::feedAt(),offset/幂等/环缓满的判据只有一份;
* - 加密、停等、重发、终态判定全部沿用原状态机;
* - 通道独占锁同样沿用(EtherCAT 自己的 PDO 运动帧也被它挡在 OTA 之外)。
*
* 与 BOOT 的 0x2500/0x2501(刷 DCU 自身)的区别:那边落 flash,这边只落 SRAM 暂存区,
* 全程不碰 fmc_*,因此不需要关中断擦写那套优雅关停。
*
* 对象(APP 字典,表项与钩子包装在 comm/ssc/port/dc_app.c):
* 0x2510 会话控制(REC of UINT32,读写钩子返回实时值)
* 0x2511 数据窗(VAR OCTETSTRING,4096B,载荷 [offset:u32 LE][crc32:u32 LE][data...])
*
* 本头刻意只用 <stdint.h> 类型:dc_app.c 是 C 且靠相对路径取 ecat_def.h,
* 而 comm/ 下的 C++ 拿不到那套 SSC 类型。SDO abort 码的映射留在 dc_app.c 那一层。
*/
#ifndef COMM_ECAT_MOTOR_OTA_H
#define COMM_ECAT_MOTOR_OTA_H
#include <stdint.h>
/**
* ── 尺寸常量(宏:C 与 C++ 两侧共用同一真源) ──
* 0x2511 对象字节数上限,含 8 字节自描述头。
* 单条目 BitSize 是 UINT16 ⇒ 8191 字节是天花板,但**真正的上限是 APP 的堆**:
* SSC 的分段下载缓冲是 `ALLOCMEM(ROUNDUPBYTE2WORD(size))` 即 malloc(size)
* (comm/ssc/src/sdoserv.c:751,池不够就回 abort 0x05040005 Out of memory),
* 而 APP 的 `__HEAP_SIZE` 只有 0xC00=3072B(BOOT 是 0x4000,所以 BOOT 的
* 0x2501 能用 4096 —— 照抄块长不照抄堆就会当场失败,2026-10-11 实测)。
* 取 1024:48KB 镜像 = 47 块,每块 1 initiate + 4 段 ≈ 5 个邮箱帧,
* 总邮箱流量仍在亚秒级,且给 malloc 留出 3 倍余量。
* 改这里必须同步 tools/ecat/ecat_native.c 的 MFO_CHUNK_OBJ(两边各自断言不了对方)。
*/
#define ECAT_MOTA_CHUNK_OBJ_BYTES 1024U
#define ECAT_MOTA_CHUNK_HDR_BYTES 8U
/** 电机固件上限:必须与 MotorOtaRelay::kMaxFwSize(48KB)一致,C++ 侧有 static_assert 守着。 */
#define ECAT_MOTA_IMG_MAX_BYTES 0xC000U
/** 0x2510 的子索引个数(sub1..sub11)。 */
#define ECAT_MOTA_CTL_SUBS 11U
/**
* ── 会话结果码 ──
* 与 MotorOtaRelay 的协议错误码(0x01..0x3x,来自电机 BOOT)刻意分段:
* 0x8x 永远是「DCU 本端把料挡了」,不是电机报错。看到 0x8x 查链路与参数,
* 看到 0x0x/0x1x/0x3x 查电机与 CAN。同一份码既写进 0x2510:10 高字节(主站可读),
* 也作为钩子的返回值(dc_app.c 再换成 SDO abort 码)。
*/
#define ECAT_MOTA_OK 0x00U
#define ECAT_MOTA_E_BADLEN 0x81U /* 声明总长或块长非法 */
#define ECAT_MOTA_E_CHUNKCRC 0x82U /* 块 CRC32 不符:邮箱搬坏了,主站重发该块 */
#define ECAT_MOTA_E_GAP 0x83U /* 块偏移越界/留洞:主站发料顺序错 */
#define ECAT_MOTA_E_IMGCRC 0x84U /* 整镜像 CRC32 不符:拒绝进 BOOT,一个字节都没发 */
#define ECAT_MOTA_E_BUSY 0x85U /* relay 已在跑(另一条链路或上一会话未结束) */
#define ECAT_MOTA_E_NOTARMED 0x86U /* 没声明总长就写数据窗 */
#define ECAT_MOTA_E_STARTED 0x87U /* 已 start,暂存区冻结,不再收块 */
#define ECAT_MOTA_E_PARAM 0x88U /* 通道/节点参数非法 */
#define ECAT_MOTA_E_BOOTPEND 0x89U /* DCU 自己正在交接回 BOOT,此时不许刷电机 */
#ifdef __cplusplus
namespace EcatMotorOta {
/** 主循环每圈调用:把暂存区泵进环缓,并在会话结束时收标志。 */
void process(void);
/** 本模块的会话是否活跃(已 start 且 relay 未进终态)。 */
bool sessionActive(void);
} /* namespace EcatMotorOta */
#define ECAT_MOTA_LINKAGE extern "C"
#else
#define ECAT_MOTA_LINKAGE extern
#endif /* __cplusplus */
/*---------------------------------------------------------------------------
* SDO 钩子(dc_app.c 的对象表引用这三个,实现在 ecat_motor_ota.cpp)
*
* 返回 ECAT_MOTA_OK 或上面的 ECAT_MOTA_E_*;dc_app.c 把非 0 换成 SDO abort 码,
* 主站既拿得到 abort,也能从 0x2510:10 读到具体是哪一条,不用猜。
*---------------------------------------------------------------------------*/
/** @brief 0x2510 写入:sub1=cmd(1=start / 3=abort)、sub2=总长、sub3=通道、sub4=node、sub5=整镜像 CRC32。 */
ECAT_MOTA_LINKAGE uint8_t EcatMotorOtaCtlWrite(uint8_t subindex, uint32_t dataSize,
const uint8_t *bytes);
/** @brief 0x2510 读取:返回该 sub 的实时值(进度不落地缓存,避免读到旧数)。 */
ECAT_MOTA_LINKAGE uint32_t EcatMotorOtaCtlRead(uint8_t subindex);
/** @brief 0x2511 写入:一块镜像(自描述 offset + 块 CRC32),失败即 SDO abort。 */
ECAT_MOTA_LINKAGE uint8_t EcatMotorOtaChunkWrite(uint32_t dataSize, const uint8_t *bytes);
#endif /* COMM_ECAT_MOTOR_OTA_H */