进料口从「只有 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 块长修复后未跑完整一轮)。
384 lines
14 KiB
C++
384 lines
14 KiB
C++
/**
|
||
* @file ecat_motor_ota.cpp
|
||
* @brief EtherCAT 邮箱进料通路实现(职责与时序前提见 ecat_motor_ota.h)。
|
||
*
|
||
* 上下文:三个 SDO 钩子只在 MainLoop(ECAT_Application 调用链)里被调,泵 process()
|
||
* 也在主循环,因此 48KB 暂存区没有并发写者。真正的硬保证仍是通道锁:即便主站循环
|
||
* 被别的路径(页面自愈 / 守护重连)拉回来,运动帧也进不了正在升级的通道。
|
||
*/
|
||
#include "comm/ecat_motor_ota.h"
|
||
|
||
#include "sys/config.h"
|
||
#include "sys/motor_ota_relay.h"
|
||
#include "sys/ota_boot_request.h"
|
||
#include "sys/systick.h"
|
||
|
||
#include "spi_link_v2.h" /* v2_crc32:CRC32 MPEG-2(poly 0x04C11DB7,初值 0xFFFFFFFF,无反射、无末尾异或) */
|
||
|
||
#include <cstring>
|
||
#include <cstdio> /* LOG_ERR 展开为 printf(sys/config.h) */
|
||
|
||
namespace {
|
||
|
||
/*============================================================================
|
||
* 尺寸常量与守卫
|
||
*==========================================================================*/
|
||
constexpr uint32_t kChunkObjBytes = ECAT_MOTA_CHUNK_OBJ_BYTES;
|
||
constexpr uint32_t kChunkHdrBytes = ECAT_MOTA_CHUNK_HDR_BYTES;
|
||
constexpr uint32_t kImgMaxBytes = ECAT_MOTA_IMG_MAX_BYTES;
|
||
constexpr uint8_t kCtlSubs = ECAT_MOTA_CTL_SUBS;
|
||
|
||
static_assert(kImgMaxBytes == MotorOtaRelay::kMaxFwSize,
|
||
"暂存区上限必须与 MotorOtaRelay::kMaxFwSize 一致");
|
||
static_assert(kChunkObjBytes * 8U <= 0xFFFFU,
|
||
"0x2511 块长超出 SDO 单条目 BitSize(UINT16)上限");
|
||
static_assert(kChunkObjBytes > kChunkHdrBytes, "数据窗要留出净载荷");
|
||
|
||
/** APP 的堆容量(RTE/Device/GD32E508VE/regions_GD32E508VE.h 的 __HEAP_SIZE)。
|
||
* SSC 每笔分段下载都要 malloc(块长)(sdoserv.c:751),堆不够就从站直接回
|
||
* abort 0x05040005「Out of memory」——2026-10-11 就是拿 4096 撞出来的。
|
||
* 这里留 3 倍余量:谁要把块长往上抬,先抬 __HEAP_SIZE 再来改这一行。 */
|
||
constexpr uint32_t kHeapBytes = 3072U;
|
||
static_assert(kChunkObjBytes * 3U <= kHeapBytes,
|
||
"块长受 APP 堆限制:malloc(块长) 必须留足余量,否则从站 OOM 拒收");
|
||
|
||
/** 单次 process() 最多搬运的字节数:把一圈的占用钳住,别在 MainLoop 里长时间拷 48KB。 */
|
||
constexpr uint16_t kPumpSlice = 512U;
|
||
|
||
/*============================================================================
|
||
* 会话状态
|
||
*==========================================================================*/
|
||
uint32_t s_cmd = 0U; /**< sub1: 最近一次执行的命令 */
|
||
uint32_t s_total = 0U; /**< sub2: 声明的镜像总字节数 */
|
||
uint32_t s_channel = 0U; /**< sub3: 目标 CAN 通道 */
|
||
uint32_t s_node = 0U; /**< sub4: 目标节点 1..9 */
|
||
uint32_t s_img_crc = 0U; /**< sub5: 整镜像期望 CRC32 */
|
||
|
||
uint32_t s_staged = 0U; /**< 已连续暂存字节数(水位,写块时前移) */
|
||
uint8_t s_sess_err = ECAT_MOTA_OK;
|
||
|
||
bool s_armed = false; /**< 已声明总长,可收块 */
|
||
bool s_started = false; /**< 已交给 relay(暂存区冻结,只剩泵) */
|
||
bool s_crc_ok = false; /**< 整镜像 CRC 已核对通过 */
|
||
bool s_last_chunk_ok = false;
|
||
bool s_end_logged = false;/**< 终态日志只打一次 */
|
||
|
||
/* 终态锁存:relay 会话结束后可能很快回到 kIdle(主站下一拍才来读),
|
||
* 不做锁存就会把「刷成功了」读成 kIdle,主站据此报的结论就成了猜的。 */
|
||
bool s_final_valid = false;
|
||
uint8_t s_final_state = 0U;
|
||
uint8_t s_final_err = 0U;
|
||
|
||
uint32_t s_pump_pos = 0U; /**< 泵在暂存区里的读游标 */
|
||
|
||
/** 48KB 暂存区(.bss:启动零耗时;APP 当前 RAM 用量 35KB/128KB,放得下)。 */
|
||
uint8_t s_img[kImgMaxBytes];
|
||
|
||
inline uint32_t loadU32(const uint8_t *p) {
|
||
return static_cast<uint32_t>(p[0])
|
||
| (static_cast<uint32_t>(p[1]) << 8)
|
||
| (static_cast<uint32_t>(p[2]) << 16)
|
||
| (static_cast<uint32_t>(p[3]) << 24);
|
||
}
|
||
|
||
void resetSession()
|
||
{
|
||
s_armed = false;
|
||
s_started = false;
|
||
s_crc_ok = false;
|
||
s_staged = 0U;
|
||
s_pump_pos = 0U;
|
||
s_total = 0U;
|
||
s_last_chunk_ok = false;
|
||
s_end_logged = false;
|
||
s_final_valid = false;
|
||
s_final_state = 0U;
|
||
s_final_err = 0U;
|
||
}
|
||
|
||
/** 只认「已连续暂存水位之内」的偏移:留洞会让泵卡住,而且说明主站发料顺序错了。 */
|
||
uint8_t acceptChunk(uint32_t offset, uint32_t crc, const uint8_t *data, uint32_t len)
|
||
{
|
||
if (!s_armed || s_total == 0U) {
|
||
s_sess_err = ECAT_MOTA_E_NOTARMED;
|
||
return s_sess_err;
|
||
}
|
||
if (s_started) {
|
||
s_sess_err = ECAT_MOTA_E_STARTED;
|
||
return s_sess_err;
|
||
}
|
||
if (offset > s_staged || offset + len > s_total) {
|
||
s_sess_err = ECAT_MOTA_E_GAP;
|
||
return s_sess_err;
|
||
}
|
||
if (len > 0U) {
|
||
/* 块级校验:EtherCAT 数据报**头**有 CRC,载荷没有;邮箱分段拼装还是软件路径。
|
||
* 这里挡一次,坏块就地重发,不会等到电机回 0x06 才发现(那时 flash 已擦)。 */
|
||
if (v2_crc32(data, len) != crc) {
|
||
s_sess_err = ECAT_MOTA_E_CHUNKCRC;
|
||
s_last_chunk_ok = false;
|
||
return s_sess_err;
|
||
}
|
||
std::memcpy(&s_img[offset], data, len);
|
||
if (offset + len > s_staged) {
|
||
s_staged = offset + len;
|
||
}
|
||
}
|
||
s_last_chunk_ok = true;
|
||
s_sess_err = ECAT_MOTA_OK;
|
||
return ECAT_MOTA_OK;
|
||
}
|
||
|
||
/** 整镜像核对 + 交给 relay。所有失败路径都必须在电机进 BOOT **之前**。 */
|
||
bool beginTransfer()
|
||
{
|
||
if (s_total == 0U || s_total > kImgMaxBytes) {
|
||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||
return false;
|
||
}
|
||
if (s_staged != s_total) {
|
||
s_sess_err = ECAT_MOTA_E_GAP;
|
||
return false;
|
||
}
|
||
if (s_channel >= 3U || s_node < 1U || s_node > 9U) {
|
||
s_sess_err = ECAT_MOTA_E_PARAM;
|
||
return false;
|
||
}
|
||
/* DCU 自己正在回 BOOT(配置页写 + 复位在路上)时绝不能起电机会话:
|
||
* 复位会把半程的刷写腰斩在 erase 之后。 */
|
||
if (OtaBootRequest::isRebootPending()) {
|
||
s_sess_err = ECAT_MOTA_E_BOOTPEND;
|
||
return false;
|
||
}
|
||
/* 整镜像 CRC 先行:这一刻电机还在 APP 正常跑、擦除没开始,校验不过就是零代价失败。
|
||
* 反过来「先进 BOOT 再验」才是变砖路径。 */
|
||
if (v2_crc32(s_img, s_total) != s_img_crc) {
|
||
s_sess_err = ECAT_MOTA_E_IMGCRC;
|
||
return false;
|
||
}
|
||
s_crc_ok = true;
|
||
|
||
if (!MotorOtaRelay::start(static_cast<uint8_t>(s_channel), s_total,
|
||
MotorOtaRelay::FeedSource::kEcat,
|
||
static_cast<uint8_t>(s_node))) {
|
||
s_sess_err = ECAT_MOTA_E_BUSY;
|
||
s_crc_ok = false;
|
||
return false;
|
||
}
|
||
s_started = true;
|
||
s_end_logged = false;
|
||
s_pump_pos = 0U;
|
||
s_final_valid = false;
|
||
s_sess_err = ECAT_MOTA_OK;
|
||
LOG_ERR("[ECAT-MOTA] start ch=%lu node=%lu size=%lu crc=%08lX\r\n",
|
||
(unsigned long)s_channel, (unsigned long)s_node,
|
||
(unsigned long)s_total, (unsigned long)s_img_crc);
|
||
return true;
|
||
}
|
||
|
||
} /* namespace */
|
||
|
||
/*============================================================================
|
||
* EcatMotorOta(C++ 侧:主循环钩子与查询)
|
||
*==========================================================================*/
|
||
namespace EcatMotorOta {
|
||
|
||
void process()
|
||
{
|
||
if (!s_started) {
|
||
return;
|
||
}
|
||
|
||
const MotorOtaRelay::Status st = MotorOtaRelay::getStatus();
|
||
|
||
/* 电机侧自动重试会清环缓并把 fw_fed 归零:泵必须跟着回退,否则会把
|
||
* 「后半段」当成已喂过,喂出一个前段缺失的镜像。 */
|
||
if (st.fw_fed < s_pump_pos) {
|
||
s_pump_pos = st.fw_fed;
|
||
}
|
||
|
||
/* 泵:按环缓空位切片,搬不动就下一圈再来(不阻塞、不忙等、不碰 CAN)。 */
|
||
while (s_pump_pos < s_total) {
|
||
const uint16_t freeSpace = MotorOtaRelay::feedFreeSpace();
|
||
if (freeSpace == 0U) {
|
||
break;
|
||
}
|
||
const uint32_t remain = s_total - s_pump_pos;
|
||
uint16_t n = static_cast<uint16_t>(remain < kPumpSlice ? remain : kPumpSlice);
|
||
if (n > freeSpace) {
|
||
n = freeSpace;
|
||
}
|
||
if (!MotorOtaRelay::feedAt(s_pump_pos, &s_img[s_pump_pos], n)) {
|
||
break;
|
||
}
|
||
s_pump_pos += n;
|
||
}
|
||
|
||
if (!MotorOtaRelay::isActive()) {
|
||
if (!s_end_logged) {
|
||
s_end_logged = true;
|
||
s_final_valid = true;
|
||
s_final_state = static_cast<uint8_t>(st.state);
|
||
s_final_err = st.error_code;
|
||
LOG_ERR("[ECAT-MOTA] end state=%u err=0x%02X acked=%u/%u fed=%lu/%lu\r\n",
|
||
static_cast<unsigned>(st.state), static_cast<unsigned>(st.error_code),
|
||
static_cast<unsigned>(st.packets_acked),
|
||
static_cast<unsigned>(st.total_packets),
|
||
(unsigned long)st.fw_fed, (unsigned long)st.fw_size);
|
||
}
|
||
s_started = false;
|
||
s_crc_ok = false;
|
||
/* 会话已结束:不再收块。主站要开下一轮必须重新写 0x2510:02 声明总长,
|
||
* 那一步复位全部会话状态(包括上面的终态锁存)。 */
|
||
s_armed = false;
|
||
s_last_chunk_ok = false;
|
||
}
|
||
}
|
||
|
||
bool sessionActive()
|
||
{
|
||
return s_started;
|
||
}
|
||
|
||
} /* namespace EcatMotorOta */
|
||
|
||
/*============================================================================
|
||
* SDO 钩子(extern "C":dc_app.c 的对象表直接引用)
|
||
*==========================================================================*/
|
||
extern "C" {
|
||
|
||
uint8_t EcatMotorOtaChunkWrite(uint32_t dataSize, const uint8_t *bytes)
|
||
{
|
||
if (bytes == nullptr) {
|
||
return ECAT_MOTA_E_BADLEN;
|
||
}
|
||
if (dataSize < kChunkHdrBytes) {
|
||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||
return s_sess_err;
|
||
}
|
||
if (dataSize > kChunkObjBytes) {
|
||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||
return s_sess_err;
|
||
}
|
||
|
||
const uint32_t offset = loadU32(&bytes[0]);
|
||
const uint32_t crc = loadU32(&bytes[4]);
|
||
const uint32_t len = dataSize - kChunkHdrBytes;
|
||
|
||
const uint8_t rc = acceptChunk(offset, crc, &bytes[kChunkHdrBytes], len);
|
||
if (rc != ECAT_MOTA_OK) {
|
||
/* 一次失败只打一行:主站会重发同一块,串口刷屏会拖慢 MainLoop。 */
|
||
LOG_ERR("[ECAT-MOTA] chunk reject off=%lu len=%lu err=0x%02X\r\n",
|
||
(unsigned long)offset, (unsigned long)len, (unsigned)rc);
|
||
}
|
||
return rc;
|
||
}
|
||
|
||
uint8_t EcatMotorOtaCtlWrite(uint8_t subindex, uint32_t dataSize, const uint8_t *bytes)
|
||
{
|
||
if (bytes == nullptr || dataSize < 4U) {
|
||
return ECAT_MOTA_E_BADLEN;
|
||
}
|
||
const uint32_t v = loadU32(bytes);
|
||
|
||
switch (subindex) {
|
||
case 1U: {
|
||
/* sub1 cmd:1 = 校验并启动,3 = 中止会话 */
|
||
s_cmd = v;
|
||
if (v == 1U) {
|
||
if (!beginTransfer()) {
|
||
return s_sess_err;
|
||
}
|
||
} else if (v == 3U) {
|
||
if (s_started) {
|
||
MotorOtaRelay::cancel();
|
||
LOG_ERR("[ECAT-MOTA] cancel by master\r\n");
|
||
}
|
||
resetSession();
|
||
s_sess_err = ECAT_MOTA_OK;
|
||
} else {
|
||
return ECAT_MOTA_E_PARAM;
|
||
}
|
||
return ECAT_MOTA_OK;
|
||
}
|
||
case 2U: {
|
||
/* sub2 总长:声明即复位会话(主站重开一轮不需要先 abort) */
|
||
if (v == 0U || v > kImgMaxBytes) {
|
||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||
return s_sess_err;
|
||
}
|
||
if (s_started) {
|
||
s_sess_err = ECAT_MOTA_E_STARTED;
|
||
return s_sess_err;
|
||
}
|
||
resetSession();
|
||
s_total = v;
|
||
s_armed = true;
|
||
s_sess_err = ECAT_MOTA_OK;
|
||
return ECAT_MOTA_OK;
|
||
}
|
||
case 3U:
|
||
if (v >= 3U) {
|
||
s_sess_err = ECAT_MOTA_E_PARAM;
|
||
return s_sess_err;
|
||
}
|
||
s_channel = v;
|
||
return ECAT_MOTA_OK;
|
||
case 4U:
|
||
if (v < 1U || v > 9U) {
|
||
s_sess_err = ECAT_MOTA_E_PARAM;
|
||
return s_sess_err;
|
||
}
|
||
s_node = v;
|
||
return ECAT_MOTA_OK;
|
||
case 5U:
|
||
s_img_crc = v;
|
||
return ECAT_MOTA_OK;
|
||
default:
|
||
/* sub0 是条目数;sub6..sub11 只读 */
|
||
return ECAT_MOTA_E_PARAM;
|
||
}
|
||
}
|
||
|
||
uint32_t EcatMotorOtaCtlRead(uint8_t subindex)
|
||
{
|
||
const MotorOtaRelay::Status st = MotorOtaRelay::getStatus();
|
||
/* live = 会话还在跑,或从没跑过(那就照实反映 relay 当前状态);
|
||
* 会话刚结束时返回锁存值,避免主站把「成功」读成 kIdle。 */
|
||
const bool live = (s_started || !s_final_valid);
|
||
|
||
switch (subindex) {
|
||
case 0U: return kCtlSubs;
|
||
case 1U: return s_cmd;
|
||
case 2U: return s_total;
|
||
case 3U: return s_channel;
|
||
case 4U: return s_node;
|
||
case 5U: return s_img_crc;
|
||
case 6U: return s_staged;
|
||
case 7U: return live ? static_cast<uint32_t>(st.state)
|
||
: static_cast<uint32_t>(s_final_state);
|
||
case 8U: return st.packets_acked;
|
||
case 9U: return st.total_packets;
|
||
case 10U: return (static_cast<uint32_t>(s_sess_err) << 8)
|
||
| (live ? static_cast<uint32_t>(st.error_code)
|
||
: static_cast<uint32_t>(s_final_err));
|
||
case 11U: {
|
||
/* phase 位图:只报能被板子真实触发的事实,不做任何推测性状态。
|
||
* bit0 已声明总长 · bit1 暂存齐 · bit2 整镜像 CRC 过 · bit3 会话在跑
|
||
* bit4 上一块被接受 · bit5 本会话目标通道当前持锁 · bit6 relay 活跃 */
|
||
uint32_t phase = 0U;
|
||
if (s_armed) { phase |= 0x0001U; }
|
||
if (s_total != 0U && s_staged == s_total) { phase |= 0x0002U; }
|
||
if (s_crc_ok) { phase |= 0x0004U; }
|
||
if (s_started) { phase |= 0x0008U; }
|
||
if (s_last_chunk_ok) { phase |= 0x0010U; }
|
||
if (MotorOtaRelay::isChannelLocked(static_cast<uint8_t>(s_channel))) { phase |= 0x0020U; }
|
||
if (MotorOtaRelay::isActive()) { phase |= 0x0040U; }
|
||
return phase;
|
||
}
|
||
default: return 0U;
|
||
}
|
||
}
|
||
|
||
} /* extern "C" */
|