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:
@@ -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}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user