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
+146
View File
@@ -21,6 +21,7 @@
#include "objdef.h"
#include "dc_app.h"
#include "../../ethercat_pdo_size.h" /* ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES */
#include "../../ecat_motor_ota.h" /* 0x2510/0x2511:电机 OTA 邮箱进料的尺寸、会话码与钩子 */
#include <string.h>
#include <stddef.h> /* offsetof:下方布局守卫断言使用 */
@@ -391,6 +392,141 @@ static UINT8 ObjEsp32EnWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
return 0U;
}
/*---------------------------------------------------------------------------
* 0x2510 / 0x2511 电机 OTA 邮箱进料通路(主站 → APP)
*
* 为什么要有它:电机 OTA 的引擎在 GD32(MotorOtaRelay:AES 握手 + CAN 停等),
* 但固件明文只有一条进料路 —— ESP32 经 SPI 0xD1 喂。只插网线的场合那条 HTTP
* 打不通,于是前端把电机 OTA 标成「本链路不支持」。这两个对象把进料口换成
* EtherCAT 邮箱,引擎一行没动:数据窗的写回调最终调的还是 MotorOtaRelay::feedAt()。
*
* 形状完全照 BOOT 已验证的 0x2500/0x2501(comm/ecat/boot_ecat_app.cpp):
* - REC 的条目统一 UINT32,偏移才和 SSC「si0 占 2 字节 + 按 BitLength 紧排」一致;
* - 数据窗建成 VAR(不是 ARR):VAR 没有 sub0 计数字段,主站按 `:00` 整块写;
* VAR 的 maxSubindex 恒为 0 ⇒ 写 `:1` 会直接 ABORTIDX_SUBINDEX_NOT_EXISTING;
* complete access 对 VAR 一律 UNSUPPORTED ⇒ 主站必须传 CA=FALSE;
* - 条目描述必须带写权限位,objdef.c:1325 的通用写权限门早于 1352 的回调分发,
* 少了它回调一次都不会进(BOOT 的 0x2500 当初就这么静默躺平过)。
* 业务逻辑(暂存、CRC、泵、终态锁存)在 comm/ecat_motor_ota.cpp,本层只做
* SDO 字节 ↔ 纯整型参数的翻译。
*---------------------------------------------------------------------------*/
typedef struct OBJ_STRUCT_PACKED_START {
UINT16 bSubIndex; /**< sub0:条目数,SSC 按 UINT16 读 */
UINT32 u32Cmd; /**< sub1:W 1=start 3=abort;R 最近一次命令 */
UINT32 u32Total; /**< sub2:镜像总字节数 */
UINT32 u32Channel; /**< sub3:目标 CAN 通道 0..2 */
UINT32 u32Node; /**< sub4:目标节点 1..9 */
UINT32 u32ImgCrc; /**< sub5:整镜像 CRC32(MPEG-2,与 SPI 链路同款) */
UINT32 u32Staged; /**< sub6:已连续暂存字节数(只读) */
UINT32 u32RelayState; /**< sub7:MotorOtaRelay 状态(只读) */
UINT32 u32PkAcked; /**< sub8:已确认包数(只读) */
UINT32 u32PkTotal; /**< sub9:总包数(只读) */
UINT32 u32Err; /**< sub10:高字节=本模块会话码 0x8x,低字节=电机协议错误码 */
UINT32 u32Phase; /**< sub11:位图,见 ecat_motor_ota.cpp */
}
OBJ_STRUCT_PACKED_END
TOBJ2510;
_Static_assert(offsetof(TOBJ2510, u32Cmd) == 2, "TOBJ2510 未紧排:检查 OBJ_STRUCT_PACKED_START");
_Static_assert(sizeof(TOBJ2510) == 2 + 4 * 11, "TOBJ2510 尺寸应为 2+44");
static TOBJ2510 s_motorOtaCtl = {ECAT_MOTA_CTL_SUBS, 0U, 0U, 0U, 0U, 0U,
0U, 0U, 0U, 0U, 0U, 0U};
OBJCONST TSDOINFOENTRYDESC OBJMEM asMotorOtaCtlEntryDesc[] = {
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* sub0 : 子索引个数 = 11 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub1 : cmd(必须带写位,否则回调进不来) */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub2 : 镜像总长 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub3 : CAN 通道 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub4 : 节点号 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub5 : 整镜像 CRC32 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub6 : 已暂存字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub7 : relay 状态 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub8 : 已 ACK 包数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub9 : 总包数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub10 : 会话码<<8 | 协议错误码 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub11 : phase 位图 */
};
static OBJCONST UCHAR aName0x2510[] = "Motor OTA control";
OBJCONST TSDOINFOENTRYDESC OBJMEM asMotorOtaChunkEntryDesc[] = {
/* 唯一条目 = 一整块 [offset:u32][crc32:u32][data...]。
* 写回调在 objdef.c:1352 早分发,SSC 的默认拷贝路径不会执行。 */
{DEFTYPE_OCTETSTRING, (UINT16)(ECAT_MOTA_CHUNK_OBJ_BYTES * 8U), ACCESS_READWRITE},
};
static OBJCONST UCHAR aName0x2511[] = "Motor firmware chunk";
/** 数据窗的落地缓冲:写路径不依赖它(回调自己取字节),保留是给默认读路径的落点。 */
static UINT8 s_motorOtaChunk[ECAT_MOTA_CHUNK_OBJ_BYTES];
static UINT8 ObjMotorOtaCtlWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT8 rc = EcatMotorOtaCtlWrite(subindex, dataSize, (const UINT8 *)pData);
(void)index;
(void)bCompleteAccess;
if (subindex == 0U) {
return ABORTIDX_SUBINDEX_NOT_EXISTING; /* sub0 是条目数,不可写 */
}
if (subindex >= 2U + ECAT_MOTA_CTL_SUBS) {
return ABORTIDX_SUBINDEX_NOT_EXISTING;
}
if (rc != ECAT_MOTA_OK) {
/* 具体原因不靠 abort 码分类:主站读 0x2510:10 的高字节拿真码。 */
return ABORTIDX_DATA_CANNOT_BE_READ_OR_STORED;
}
return 0U;
}
/** 0x2510 读取:逐 subindex 从 ecat_motor_ota 取**实时**值,不读 s_motorOtaCtl 的镜像。 */
static UINT8 ObjMotorOtaCtlRead(UINT16 index, UINT8 subindex, UINT32 objSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT32 v = EcatMotorOtaCtlRead(subindex);
UINT8 *p8 = (UINT8 *)pData;
(void)index;
(void)objSize;
(void)bCompleteAccess;
if (pData == NULL) {
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
}
if (subindex == 0U) {
/* sub0 是 UINT8(条目数),只占载荷一个字节 */
p8[0] = (UINT8)v;
} else {
p8[0] = (UINT8)(v & 0xFFU);
p8[1] = (UINT8)((v >> 8) & 0xFFU);
p8[2] = (UINT8)((v >> 16) & 0xFFU);
p8[3] = (UINT8)((v >> 24) & 0xFFU);
}
return 0U;
}
static UINT8 ObjMotorOtaChunkWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT8 rc = EcatMotorOtaChunkWrite(dataSize, (const UINT8 *)pData);
(void)index;
(void)bCompleteAccess;
if (subindex != 0U) {
return ABORTIDX_SUBINDEX_NOT_EXISTING; /* VAR 只有 :00 */
}
if (rc == ECAT_MOTA_E_GAP || rc == ECAT_MOTA_E_PARAM || rc == ECAT_MOTA_E_BADLEN) {
return ABORTIDX_VALUE_EXCEEDED;
}
if (rc != ECAT_MOTA_OK) {
return ABORTIDX_DATA_CANNOT_BE_READ_OR_STORED;
}
return 0U;
}
/*---------------------------------------------------------------------------
* Object Names
*---------------------------------------------------------------------------*/
@@ -444,6 +580,16 @@ TOBJECT OBJMEM ApplicationObjDicList[] = {
{NULL, NULL, 0x2503, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)},
sEntryDesc0x2503, aName0x2503, &s_esp32EnVal, NULL, ObjEsp32EnWrite, 0x0000},
/* 0x2510: 电机 OTA 会话控制(写 sub2 声明总长 → 反复写 0x2511 灌块 → 写 sub1=1 启动) */
{NULL, NULL, 0x2510, {DEFTYPE_RECORD, ECAT_MOTA_CTL_SUBS | (OBJCODE_REC << 8)},
asMotorOtaCtlEntryDesc, aName0x2510, &s_motorOtaCtl,
ObjMotorOtaCtlRead, ObjMotorOtaCtlWrite, 0x0000},
/* 0x2511: 电机固件数据窗(VAR OCTETSTRING,主站分段下载攒完才回调一次) */
{NULL, NULL, 0x2511, {DEFTYPE_OCTETSTRING, 0 | (OBJCODE_VAR << 8)},
asMotorOtaChunkEntryDesc, aName0x2511, &s_motorOtaChunk,
NULL, ObjMotorOtaChunkWrite, 0x0000},
/* 终止条目 */
{NULL, NULL, 0xFFFF, {0, 0}, NULL, NULL, NULL, NULL, NULL, 0x000}
};