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
+2
View File
@@ -35,6 +35,7 @@ public:
* @note 0xE0/0xE1 保留给 handler 入口校验(payload 过短/通道非法),
* 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因;
* 0xE6=通道处于收发器 Silent(只听不发),在入口即拒绝,不计任何错误计数。
* 0xE7=通道被电机 OTA 独占锁定(EtherCAT/SPI 跨总线排他),同样不计错误计数。
*/
enum class CanTxResult : uint8_t {
kOk = 0x00U, /**< 发送成功,已进入硬件邮箱。 */
@@ -43,6 +44,7 @@ public:
kErrMailbox = 0xE4U, /**< 3 个硬件邮箱全 PENDING,abort+重投仍失败。 */
kErrInvalid = 0xE5U, /**< 入口参数无效(channelIndex/dlc/data 等)。 */
kErrSilent = 0xE6U, /**< 通道收发器处于 Silent(驱动器关断),拒绝发送。 */
kErrOtaLocked = 0xE7U, /**< 通道被电机 OTA 独占(跨总线排他),拒绝发送,不计错误计数。 */
};
/**
@@ -3,6 +3,7 @@
#include "device/spi/app_spi_command_defs.h"
#include "device/canfd.h"
#include "sys/config.h"
#include "sys/motor_ota_relay.h"
#include <cstdio>
namespace {
@@ -76,6 +77,17 @@ public:
return;
}
/* 通道正被电机 OTA 独占(WiFi 与 EtherCAT 两条链路可能同时在跑,锁是共用的):
* 这一帧挤进同一通道会 abort 掉在途的 0x101 固件包,单发模式下永不重传。
* 回 0xE7,上位机据此知道「是被 OTA 挡了」而不是「邮箱满」。 */
if (MotorOtaRelay::isChannelLocked(can_ch)) {
postCanSendLog(bus, "LOCKED(motor-OTA)", can_ch, can_id, can_data, can_dlc);
const uint8_t err = static_cast<uint8_t>(CanFd::CanTxResult::kErrOtaLocked);
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
/* 0xC0 单帧发送改走 enqueueTx:
* 1) allow_abort=false 杜绝硬件邮箱被后续发送主动 abort 抹杀;
* 2) 电机高频上报抢占仲裁导致邮箱全占时,帧自动进入 13 深度软件队列,
@@ -3,6 +3,7 @@
#include "device/spi/app_spi_command_defs.h"
#include "device/canfd.h"
#include "sys/config.h"
#include "sys/motor_ota_relay.h"
namespace {
@@ -81,6 +82,11 @@ public:
resp[1U + i] = 0xE1U; /* 通道非法:跳过该帧,其余照常发送 */
continue;
}
if (MotorOtaRelay::isChannelLocked(ch)) {
/* 电机 OTA 独占该通道:整帧拒绝,不挤邮箱(0xE7)。 */
resp[1U + i] = static_cast<uint8_t>(CanFd::CanTxResult::kErrOtaLocked);
continue;
}
const CanFd::CanTxResult r =
CanFd::enqueueTx(ch, id, (dlc > 0U) ? &p[1] : nullptr, dlc);
resp[1U + i] = static_cast<uint8_t>(r);