/** * @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 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 */