Files
cbb d8b3c9767c 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 块长修复后未跑完整一轮)。
2026-10-11 02:47:19 +08:00

128 lines
5.6 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file motor_ota_relay.h
* @brief 电机 OTA relay 状态机 — GD32 本地完成与电机 Bootloader 的全部交互。
*
* 职责:
* - AES-128 密钥协商(K_master 存本地 flash)
* - 固件数据 AES-ECB 加密 + keyed-CRC32
* - CAN 上逐包 stop-and-wait(发 0x101 → 等 0x102 ACK → 按包号继续)
* - 进度/完成/错误通过 SPI ring push 报 ESP32
*
* ESP32 通过 SPI 0xD0(start) / 0xD1(data) / 0xD3(cancel) 喂数据,
* GD32 通过 0xD2 ring push 报进度。
*/
#ifndef SYS_MOTOR_OTA_RELAY_H
#define SYS_MOTOR_OTA_RELAY_H
#include <cstdint>
namespace MotorOtaRelay {
/* 电机 Bootloader CAN ID(固定,非节点相关) */
static constexpr uint32_t kOtaReqId = 0x00000100U; /* Host→Motor 握手 */
static constexpr uint32_t kOtaDataId = 0x00000101U; /* Host→Motor 数据 */
static constexpr uint32_t kOtaAckId = 0x00000102U; /* Motor→Host ACK */
static constexpr uint32_t kOtaWaitId = 0x00000103U; /* Motor→Host IDLE心跳 */
static constexpr uint32_t kOtaCancelId = 0x00000000U; /* Host→Motor 取消 */
static constexpr uint32_t kPacketPayload = 48U; /* 每包有效载荷 */
static constexpr uint32_t kFrameDlc = 64U; /* CAN FD DLC */
static constexpr uint32_t kMaxFwSize = 0xC000U; /* 48 KB */
static constexpr uint32_t kAckTimeoutMs = 5000U; /* 单包 ACK 超时 */
static constexpr uint32_t kHandshakeTimeoutMs = 3000U;
enum class State : uint8_t {
kIdle = 0,
kHandshakeReq, /* 已发 [00 AC],等 ACK */
kHandshakeNonce, /* 已发 nonce,等 36B ACK */
kHandshakeConfirm, /* 已发 confirm,等 [BA C0] */
kErase, /* 已发 size,等擦除完成 ACK */
kTransfer, /* 逐包传输中 */
kFinishOta, /* 收到 0xF0F0F0F0,已发 [0A AC],等 FINISH ACK */
kFinished, /* 完成 */
kError, /* 错误 */
kPreCancel = 9, /* 等待电机 BOOT 心跳(0x103)确认已进入 Bootloader,然后进 kHandshakeReq */
kWaitAppRun = 10 /* 尾包 ACK 后电机已跳转,等新 APP 上电自主发的 [0A AC] 运行确认,收到才进 kFinished */
};
struct Status {
State state;
uint8_t error_code;
uint16_t packets_acked;
uint16_t total_packets;
uint32_t fw_size;
/** 已被环缓接受的明文字节数(流位置)。ECAT 暂存泵用它对账:
* 电机侧自动重试会把环缓与这个位置一起清零,泵必须跟着回退。
* 注意:只是内部 C++ 结构,SPI 0xD0 QUERY 的线格式没有改动。 */
uint32_t fw_fed;
};
/** 固件明文进料来源。同一时刻只允许一个来源,环缓不接受两路交叉写入。 */
enum class FeedSource : uint8_t {
kSpi = 0U, /* ESP32 经 SPI 0xD1 喂(WiFi/BLE 链路的既有通路) */
kEcat = 1U, /* EtherCAT 主站经 CoE 0x2511 灌进暂存区,再由 ecat_motor_ota 泵入环缓 */
};
/* 初始化(注册 CAN RX 回调等) */
void init();
/* 主循环推进(从 task_runner 周期调用,建议 1~5ms 一次) */
void process();
/* 数据发送推进:由主循环 TaskRunner 每圈调用,按 SysTick 钟表控制每包间隔
* (不再走 ISR 计时 + TASK 标志,消除跨上下文计数器竞争)。 */
void onTxTick();
/* CAN RX 分发入口:匹配 0x102 ACK 帧时消费,返回 true 表示已处理 */
bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len);
/* SPI 命令接口 */
/**
* @brief 启动一次电机 OTA。
* @param can_channel 目标 CAN 通道 0..2。
* @param fw_size 固件明文字节数。
* @param src 进料来源(SPI / ECAT 暂存泵)。
* @param node_id 1..9:由 GD32 自己在**已加锁**的通道上发 0x000N0000 进 BOOT 帧;
* 0:不进 BOOT(WiFi 链路沿用上位机先发帧的老顺序)。
* @note 入口第一条语句即置通道锁,早于任何状态清零与串口日志。
*/
bool start(uint8_t can_channel, uint32_t fw_size,
FeedSource src = FeedSource::kSpi, uint8_t node_id = 0U);
/**
* @brief 喂入固件明文。payload 布局:[offset:u32 LE][data...]
* offset 为固件绝对字节偏移;重复/重叠区间幂等 ACK,缺口拒绝(→SPI WAIT)。
* 无 offset 的旧纯流会在 SPI 超时重传时把同一 chunk 追加进环缓(BLF 见前 236B 循环)。
*/
bool feedData(const uint8_t *data, uint16_t len);
/**
* @brief 已解析过的进料写入(feedData 的核心,偏移/幂等/环缓满判据只有一份)。
* @note 供 ECAT 暂存泵调用:那边已按 offset 寻址,无需再拼 4 字节头,
* 也避免在仅 512B 的主栈上放临时头缓冲。
*/
bool feedAt(uint32_t offset, const uint8_t *chunk, uint16_t chunk_len);
/**
* @brief 环缓当前还能接受多少明文字节。
* @note 给进料方按空位切片用(否则一次投过大永远排不进队首)。不改变任何判据:
* feedAt 内部仍会自己再查一次空位。
*/
uint16_t feedFreeSpace(void);
void cancel();
/* 状态查询 */
Status getStatus();
bool isActive();
/**
* @brief 指定 CAN 通道当前是否被电机 OTA 独占。
* @note 单条 32 位位与,可在 preempt=0(EXTI0/SYNC0、EXTI10_15)里直接调用。
* EtherCAT 桥接与 SPI 桥接/突发在发送前必须查它,否则周期运动帧会
* abort 掉 OTA 的 0x101 包(单发模式 ARD=1 不重传),电机会回 0x05/0x06。
*/
bool isChannelLocked(uint8_t channel);
/** @brief 当前被锁定的通道号;-1 表示无锁。 */
int8_t lockedChannel();
} /* namespace MotorOtaRelay */
#endif /* SYS_MOTOR_OTA_RELAY_H */