进料口从「只有 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 块长修复后未跑完整一轮)。
128 lines
5.6 KiB
C++
128 lines
5.6 KiB
C++
/**
|
||
* @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 */
|