diff --git a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml index 3d1c1df..c8c4182 100644 --- a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml +++ b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml @@ -18,6 +18,10 @@ project: - file: ./sys/task_runner.cpp - file: ./sys/ota_boot_request.h - file: ./sys/ota_boot_request.cpp + - file: ./sys/aes128.h + - file: ./sys/aes128.cpp + - file: ./sys/motor_ota_relay.h + - file: ./sys/motor_ota_relay.cpp - file: ./sys/cpp_stubs.cpp - file: ./sys/config.h - group: device @@ -32,6 +36,10 @@ project: - file: ./device/usart0.cpp - file: ./device/watchdog.h - file: ./device/watchdog.cpp + - file: ./device/motor_state.h + - file: ./device/motor_state.cpp + - file: ./device/motor_version.h + - file: ./device/motor_version.cpp # GigaDevice DFP 未把 EXMC 注册为 CMSIS 组件(仅有 FMC=片内 Flash), # 但外设源文件存在于 Pack;LAN9252 并行总线需要 EXMC,故本地编入。 - group: device_exmc @@ -51,6 +59,9 @@ project: - file: ./device/spi/app_spi_cmd_can_feedback.cpp - file: ./device/spi/app_spi_cmd_id_snapshot.cpp - file: ./device/spi/app_spi_cmd_adc_feedback.cpp + - file: ./device/spi/app_spi_cmd_motor_state.cpp + - file: ./device/spi/app_spi_cmd_motor_version.cpp + - file: ./device/spi/app_spi_cmd_motor_ota.cpp - group: spi_link files: - file: ../../spi_link/spi_link_v2.cpp diff --git a/GD32E508_DCU_APP/comm/esc_exmc_bus.h b/GD32E508_DCU_APP/comm/esc_exmc_bus.h index ec1b064..b9de51f 100644 --- a/GD32E508_DCU_APP/comm/esc_exmc_bus.h +++ b/GD32E508_DCU_APP/comm/esc_exmc_bus.h @@ -1,7 +1,7 @@ /** * @file esc_exmc_bus.h * @brief LAN9252 ESC 的 EXMC 16-bit 并行总线抽象接口。 - * @note 通过 EXMC NOR/SRAM Region1(NE1,基址 0x60000000)以内存映射方式访问 LAN9252。 + * @note 通过 EXMC NOR/SRAM Region1(NE1,基址 0x64000000)以内存映射方式访问 LAN9252。 * 16-bit 数据总线 + A16 地址线 + NOE/NWE/NE1 控制线。 */ #ifndef COMM_ESC_EXMC_BUS_H diff --git a/GD32E508_DCU_APP/device/CANFD.cpp b/GD32E508_DCU_APP/device/CANFD.cpp index 8fc3301..725be1e 100644 --- a/GD32E508_DCU_APP/device/CANFD.cpp +++ b/GD32E508_DCU_APP/device/CANFD.cpp @@ -267,7 +267,7 @@ bool CanFd::initNominalTiming(uint32_t canPeriph) can_parameter.time_triggered = DISABLE; can_parameter.auto_bus_off_recovery = ENABLE; can_parameter.auto_wake_up = DISABLE; - can_parameter.auto_retrans = ENABLE; + can_parameter.auto_retrans = ENABLE; /* 默认自动重发兜底偶发 ACK 丢失;OTA 停等协议期间由 setChannelAutoRetrans(ch,false) 临时关闭,避免重发打穿电机单缓冲 RX */ can_parameter.rec_fifo_overwrite = ENABLE; can_parameter.trans_fifo_order = ENABLE; can_parameter.working_mode = CAN_NORMAL_MODE; @@ -936,40 +936,19 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, return CanTxResult::kErrInvalid; } - /* 总线健康检查:BUS_OFF 或 TEC 过高时跳过发送,防止测试帧把通道推向 BUS_OFF */ + /* 诊断:捕获任何通过 transmitFrame 发 0x100 DLC=16 的帧(排查幽灵帧来源) */ + if (ext_id == 0x00000100U && dlc == 16U) { + printf("[CAN-DIAG] TX 0x100 DLC=16 via transmitFrame ch=%u\r\n", + (unsigned)channelIndex); + } + + /* BUS_OFF 不再软件拦截——CAN_ERR_BOERR 是粘性标志,控制器恢复后仍置位, + * 导致总线正常时仍拒绝发送(用户确认其他设备能收到 GD32 帧)。 + * CAN 硬件自带恢复机制(128×11隐性位后自动回 Normal); + * 若确实发不出,can_message_transmit 会返回 NOMAILBOX → kErrMailbox。 */ const uint32_t canPeriph = kChannels[channelIndex].canPeriph; - const uint32_t errReg = CAN_ERR(canPeriph); - if (errReg & CAN_ERR_BOERR) { - static uint32_t s_lastBusOffLogSeq[kChannelCount] = {0xFFFFFFFFU, 0xFFFFFFFFU, 0xFFFFFFFFU}; - if (s_lastBusOffLogSeq[channelIndex] != s_feedbackSeq) { - printf("[CAN-TX-REJECT] CH%u BUS_OFF ERR=0x%08lX ERRN=%lu(%s) TEC=%lu REC=%lu TSTAT=0x%08lX\r\n", - static_cast(channelIndex), - static_cast(errReg), - static_cast((errReg >> 4U) & 0x07U), - canLastErrorName(errReg), - static_cast((errReg >> 16U) & 0xFFU), - static_cast((errReg >> 24U) & 0xFFU), - static_cast(CAN_TSTAT(canPeriph))); - s_lastBusOffLogSeq[channelIndex] = s_feedbackSeq; - } - return CanTxResult::kErrBusOff; /* 通道处于 BUS_OFF,不发送 */ - } - const uint32_t tec = (errReg >> 16) & 0xFFU; - if (tec > 96U && !periodic) { - static uint32_t s_lastTecLogSeq[kChannelCount] = {0xFFFFFFFFU, 0xFFFFFFFFU, 0xFFFFFFFFU}; - if (s_lastTecLogSeq[channelIndex] != s_feedbackSeq) { - printf("[CAN-TX-REJECT] CH%u TEC_HIGH ERR=0x%08lX ERRN=%lu(%s) TEC=%lu REC=%lu TSTAT=0x%08lX\r\n", - static_cast(channelIndex), - static_cast(errReg), - static_cast((errReg >> 4U) & 0x07U), - canLastErrorName(errReg), - static_cast(tec), - static_cast((errReg >> 24U) & 0xFFU), - static_cast(CAN_TSTAT(canPeriph))); - s_lastTecLogSeq[channelIndex] = s_feedbackSeq; - } - return CanTxResult::kErrTecHigh; /* TEC 过高(>75% 错误被动阈值),暂停发送保护通道 */ - } + /* CAN 硬件自有 error-passive(TEC>127 自动加间隔) + * 和 bus-off(TEC=256 → 自动恢复) 保护。软件不再做任何发送拦截。 */ /* 周期发送只允许一个邮箱保持自动重发。无 ACK 时同一帧会持续产生 * 总线波形;不要每 1ms 再填满其余邮箱,更不能主动 abort 旧帧,否则 @@ -1045,6 +1024,44 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, return CanTxResult::kErrMailbox; } +void CanFd::abortPendingTx(uint8_t channelIndex) +{ + if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) { + return; + } + const uint32_t canPeriph = kChannels[channelIndex].canPeriph; + for (uint8_t mb = CAN_MAILBOX0; mb <= CAN_MAILBOX2; ++mb) { + if (can_transmit_states(canPeriph, mb) == CAN_TRANSMIT_PENDING) { + (void)can_transmission_stop(canPeriph, mb); + } + } +} + +void CanFd::setChannelAutoRetrans(uint8_t channelIndex, bool enable) +{ + if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) { + return; + } + const uint32_t canPeriph = kChannels[channelIndex].canPeriph; + /* ARD 位仅在 init mode 可写:进 init mode → 改 ARD → 退 init mode。 */ + CAN_CTL(canPeriph) |= CAN_CTL_IWMOD; + for (volatile uint32_t t = 0U; t < 200000U; ++t) { + if (CAN_STAT(canPeriph) & CAN_STAT_IWS) break; + } + if (enable) { + CAN_CTL(canPeriph) &= ~CAN_CTL_ARD; /* 自动重发 ON(业务默认) */ + } else { + CAN_CTL(canPeriph) |= CAN_CTL_ARD; /* 自动重发 OFF(单发,停等式 OTA 用) */ + } + CAN_CTL(canPeriph) &= ~CAN_CTL_IWMOD; + for (volatile uint32_t t = 0U; t < 200000U; ++t) { + if ((CAN_STAT(canPeriph) & CAN_STAT_IWS) == 0U) break; + } + printf("[CAN] CH%u set auto_retrans=%u -> ARD=%u\r\n", + (unsigned)channelIndex, (unsigned)enable, + (CAN_CTL(canPeriph) & CAN_CTL_ARD) ? 1U : 0U); +} + /** * @brief 获取 CANFD 诊断统计信息只读指针。 * @param[in] 无。 @@ -1319,13 +1336,16 @@ void CanFd::refreshFeedbackSnapshot() } s_feedbackSeq++; - /* 每 5 秒打印一次 seq 心跳;诊断期间同时输出实际错误通道的最后错误类型。 */ + /* 每 5 秒采样硬件错误态;正常心跳不打印(统计走 SPI 0xC1)。 + * 仅当 TEC/REC 或告警位非零时输出 [CAN-HW-ERR];全量 seq 心跳见 CONFIG_UART_DIAG_LEVEL。 */ if (s_feedbackSeq % 5U == 0U) { +#if (CONFIG_UART_DIAG_LEVEL != 0) printf("[CAN-FB] refresh seq=%lu rx0=%lu rx1=%lu rx2=%lu\r\n", (unsigned long)s_feedbackSeq, (unsigned long)s_channelStats[0].rx_count, (unsigned long)s_channelStats[1].rx_count, (unsigned long)s_channelStats[2].rx_count); +#endif for (size_t i = 0; i < kChannelCount; ++i) { if (hw_tec[i] == 0U && hw_rec[i] == 0U && (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) == 0U) { diff --git a/GD32E508_DCU_APP/device/canfd.h b/GD32E508_DCU_APP/device/canfd.h index 1959a9e..d12fb68 100644 --- a/GD32E508_DCU_APP/device/canfd.h +++ b/GD32E508_DCU_APP/device/canfd.h @@ -144,6 +144,12 @@ public: static bool consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel = nullptr); static CanTxResult transmitFrame(uint8_t channelIndex, uint32_t ext_id, const uint8_t *data, uint8_t dlc, bool periodic = false); + + /** @brief 立即停掉指定通道所有 PENDING 发送邮箱(阻止 auto_retrans 残留重发,供停等式协议用)。 */ + static void abortPendingTx(uint8_t channelIndex); + + /** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */ + static void setChannelAutoRetrans(uint8_t channelIndex, bool enable); static const Diag *diag(); /** diff --git a/GD32E508_DCU_APP/device/motor_state.cpp b/GD32E508_DCU_APP/device/motor_state.cpp new file mode 100644 index 0000000..502c644 --- /dev/null +++ b/GD32E508_DCU_APP/device/motor_state.cpp @@ -0,0 +1,148 @@ +/** + * @file motor_state.cpp + * @brief 多电机状态表实现 — CAN STATUS 帧识别、内存缓存、SPI 推送序列化。 + * + * 所有函数仅在主循环上下文调用(taskCanRxParse / taskHeartbeat / pushStep), + * 无 ISR 竞态,不需要临界区保护。 + */ + +#include "device/motor_state.h" +#include "device/spi/app_spi_command_defs.h" +#include "main.h" +#include + +/* ── 全局状态表实例 ── */ +static MotorStateTable s_motorTable = {}; + +namespace MotorState { + +MotorStateTable &table() +{ + return s_motorTable; +} + +void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len) +{ + /* BOOT 心跳 0x103(固定 ID,不带节点号):电机在 Bootloader IDLE 每 ~10ms 发一次。 + * 记录最近收到时刻与通道,供网页显示独立 BOOT 状态(不归属任何 Nx)。 + * 注意:必须在 STATUS 掩码过滤之前拦截,否则 0x103 会被下面的掩码判断丢弃。 */ + if (id == kOtaBootHeartbeatId) { + s_motorTable.boot_hb_sec = g_app_uptime_sec; + s_motorTable.boot_channel = channel; + s_motorTable.boot_active = 1U; + /* 0x103 仅更新 boot_active 状态,不再串口打印(高频心跳会刷屏)。 */ + return; + } + + /* 掩码匹配 STATUS ID: 0x000N0100 */ + if ((id & kMotorStatusIdMask) != kMotorStatusIdMatch) { + return; + } + + const uint8_t node = static_cast((id >> 16) & 0x0FU); + if (node < 1U || node > kMotorMaxNodes) { + return; + } + + MotorSlot &slot = s_motorTable.slots[node - 1U]; + + slot.node_id = node; + slot.channel = channel; + slot.online = 1U; + slot.dlc = (len <= kMotorStatusDlc) ? len : kMotorStatusDlc; + slot.rx_count++; + slot.last_rx_sec = g_app_uptime_sec; + slot.seq = ++s_motorTable.global_seq; + + /* 拷贝原始 payload */ + std::memcpy(slot.data, data, slot.dlc); + if (slot.dlc < kMotorStatusDlc) { + std::memset(&slot.data[slot.dlc], 0, kMotorStatusDlc - slot.dlc); + } +} + +void checkOffline() +{ + const uint32_t now = g_app_uptime_sec; + uint8_t active = 0U; + + for (uint8_t i = 0U; i < kMotorMaxNodes; ++i) { + MotorSlot &slot = s_motorTable.slots[i]; + if (slot.node_id == 0U) { + continue; /* 空槽 */ + } + if ((now - slot.last_rx_sec) >= kMotorOfflineSec) { + slot.online = 0U; + } + if (slot.online != 0U) { + active++; + } + } + s_motorTable.active_count = active; + + /* BOOT 心跳超时:>kBootHeartbeatTimeoutSec 无 0x103 → 电机已退出 BOOT。 */ + if (s_motorTable.boot_active != 0U && + (now - s_motorTable.boot_hb_sec) >= kBootHeartbeatTimeoutSec) { + s_motorTable.boot_active = 0U; + } +} + +uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen) +{ + if (page >= kMotorTotalPages || buf == nullptr) { + return 0U; + } + + /* 需要的最小空间: header + 至少 1 entry */ + const uint16_t needed = kMotorPageHeaderLen + kMotorEntrySize; + if (maxLen < needed) { + return 0U; + } + + const uint8_t startIdx = static_cast(page * kMotorEntriesPerPage); + uint8_t count = 0U; + + /* Header (4B) */ + buf[0] = page; + buf[1] = kMotorTotalPages; + buf[2] = 0U; /* entry_count, 稍后回填 */ + /* bit[6:0]=在线电机数,bit7=BOOT 心跳(0x103)活跃标志(供网页显示独立 BOOT 状态)。 + * active_count 上限 9,bit7 空闲可复用,旧解析方读到会多算 128,故 ESP32 侧需掩 0x7F。 */ + buf[3] = static_cast((s_motorTable.active_count & 0x7FU) + | (s_motorTable.boot_active ? kMotorActiveBootBit : 0U)); + + uint16_t offset = kMotorPageHeaderLen; + + for (uint8_t i = 0U; i < kMotorEntriesPerPage; ++i) { + const uint8_t slotIdx = static_cast(startIdx + i); + if (slotIdx >= kMotorMaxNodes) { + break; + } + + const MotorSlot &slot = s_motorTable.slots[slotIdx]; + if (slot.node_id == 0U) { + continue; /* 跳过空槽 */ + } + + /* 检查剩余空间 */ + if (offset + kMotorEntrySize > maxLen) { + break; + } + + buf[offset + 0U] = slot.node_id; + buf[offset + 1U] = slot.channel; + buf[offset + 2U] = slot.online; + buf[offset + 3U] = slot.dlc; + writeU32Le(&buf[offset + 4U], slot.rx_count); + writeU32Le(&buf[offset + 8U], slot.seq); + std::memcpy(&buf[offset + 12U], slot.data, kMotorStatusDlc); + + offset += kMotorEntrySize; + count++; + } + + buf[2] = count; + return offset; +} + +} // namespace MotorState diff --git a/GD32E508_DCU_APP/device/motor_state.h b/GD32E508_DCU_APP/device/motor_state.h new file mode 100644 index 0000000..138fc09 --- /dev/null +++ b/GD32E508_DCU_APP/device/motor_state.h @@ -0,0 +1,87 @@ +/** + * @file motor_state.h + * @brief 多电机状态表 — CAN STATUS 帧识别、内存缓存、SPI 推送序列化。 + * + * 设计要点: + * - 仅在主循环上下文 (taskCanRxParse / taskHeartbeat / pushStep) 中读写, + * 无 ISR 竞态,不需要临界区。 + * - GD32 不做浮点解码,只缓存原始 32B STATUS payload,由前端按 DBC 解码。 + * - 离线判定:3s 无 STATUS 帧 → online=0。 + */ + +#ifndef DEVICE_MOTOR_STATE_H +#define DEVICE_MOTOR_STATE_H + +#include +#include +#include "sys/config.h" + +/* ── CAN ID 匹配规则 (29-bit 扩展帧) ── + * 物理 ID = 0x000N_TT_SS + * N = 节点号 (1-9), 位于 bits[19:16] + * TT = 消息类型: 01=STATUS, 02=CMD, 03=CONFIG + * SS = 子类型 + * STATUS = 0x000N0100 + */ +static constexpr uint32_t kMotorStatusIdMask = 0xFFF0FFFFU; /* 屏蔽 node nibble [19:16] */ +static constexpr uint32_t kMotorStatusIdMatch = 0x00000100U; /* STATUS 类型码 */ + +static constexpr uint8_t kMotorMaxNodes = CONFIG_MOTOR_MAX_NODES; +static constexpr uint8_t kMotorStatusDlc = 32U; +static constexpr uint16_t kMotorOfflineSec = CONFIG_MOTOR_OFFLINE_MS / 1000U; + +/* ── BOOT 心跳(0x103)── + * 电机在 Bootloader IDLE 每 ~10ms 发一次 0x00000103(固定 ID,不带节点号)。 + * 网页据此显示一个独立 BOOT 状态(不归属任何 Nx)。 */ +static constexpr uint32_t kOtaBootHeartbeatId = 0x00000103U; +static constexpr uint32_t kBootHeartbeatTimeoutSec = 3U; /* >3s 无 0x103 → 视为退出 BOOT */ +static constexpr uint8_t kMotorActiveBootBit = 0x80U; /* 0xC5 page0 header active 字节 bit7 = boot_active */ + +/* ── 序列化常量 (0xC5 payload) ── */ +static constexpr uint8_t kMotorEntrySize = 44U; /* 1+1+1+1+4+4+32 */ +static constexpr uint8_t kMotorPageHeaderLen = 4U; /* page+total+count+active */ +static constexpr uint8_t kMotorEntriesPerPage = 5U; /* (236-4)/44 = 5.27 → 5 */ +static constexpr uint8_t kMotorTotalPages = 2U; /* ceil(9/5) = 2 */ + +/// 单电机状态槽 +struct MotorSlot { + uint8_t node_id; ///< 1-9, 0=空槽 + uint8_t channel; ///< 最后收到帧的 CAN 通道 (0/1/2) + uint8_t online; ///< 1=在线, 0=离线 + uint8_t dlc; ///< 最后帧 DLC (正常=32) + uint32_t rx_count; ///< 累计收到 STATUS 帧数 + uint32_t last_rx_sec; ///< 最后收到帧时的 g_app_uptime_sec + uint32_t seq; ///< 更新序号 (每次刷新 +1) + uint8_t data[kMotorStatusDlc]; ///< 原始 STATUS payload +}; + +/// 全局电机状态表 +struct MotorStateTable { + MotorSlot slots[kMotorMaxNodes]; ///< 按 node_id-1 索引 + uint32_t global_seq; ///< 全局刷新序号 + uint32_t last_push_sec; ///< 上次推送完成时间 + uint8_t active_count; ///< 当前在线电机数 + uint32_t boot_hb_sec; ///< 最后收到 0x103 BOOT 心跳时的 uptime(0=从未) + uint8_t boot_channel; ///< 最后 0x103 所在 CAN 通道 + uint8_t boot_active; ///< 1=最近 kBootHeartbeatTimeoutSec 内有 0x103 +}; + +/// 电机状态管理命名空间 +namespace MotorState { + +/// 获取全局状态表引用 +MotorStateTable &table(); + +/// CAN 帧到达时调用 (主循环上下文, taskCanRxParse 内) +void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len); + +/// 1Hz 离线检测 (taskHeartbeat 内) +void checkOffline(); + +/// 序列化一页电机状态到 payload 缓冲区 +/// @return 实际写入字节数, 0=参数错误 +uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen); + +} // namespace MotorState + +#endif /* DEVICE_MOTOR_STATE_H */ diff --git a/GD32E508_DCU_APP/device/motor_version.cpp b/GD32E508_DCU_APP/device/motor_version.cpp new file mode 100644 index 0000000..c476c12 --- /dev/null +++ b/GD32E508_DCU_APP/device/motor_version.cpp @@ -0,0 +1,105 @@ +/** + * @file motor_version.cpp + * @brief 多电机版本缓存实现 — CAN 版本应答帧(0x00FF)识别、缓存、0xC6 序列化。 + * + * 所有函数仅在主循环上下文调用(taskCanRxParse / pushStep),无 ISR 竞态。 + * 与 motor_state.cpp 不同:版本是事件驱动(收到应答才缓存+推送),无周期任务、 + * 无离线判定;pushStep 在 app_spi_cmd_motor_version.cpp(含 SPI 依赖)。 + */ + +#include "device/motor_version.h" +#include "device/spi/app_spi_command_defs.h" +#include + +/* ── 全局版本表实例 ── */ +static MotorVersionTable s_verTable = {}; + +namespace MotorVersion { + +MotorVersionTable &table() +{ + return s_verTable; +} + +void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len) +{ + (void)channel; + + /* 掩码匹配版本应答 ID: 0x000N00FF */ + if ((id & kMotorVersionIdMask) != kMotorVersionIdMatch) { + return; + } + + const uint8_t node = static_cast((id >> 16) & 0x0FU); + if (node < 1U || node > kMotorVersionMaxNodes) { + return; + } + if (data == nullptr || len < 1U) { + return; + } + + MotorVersionSlot &slot = s_verTable.slots[node - 1U]; + + slot.node_id = node; + slot.seq = ++s_verTable.global_seq; + + /* 拷贝原始 payload(不足补 0,超长截断) */ + const uint8_t n = (len <= kMotorVersionDlc) ? len : kMotorVersionDlc; + std::memcpy(slot.version, data, n); + if (n < kMotorVersionDlc) { + std::memset(&slot.version[n], 0, kMotorVersionDlc - n); + } + + slot.dirty = 1U; /* 标记待推送 */ +} + +bool hasPending() +{ + for (uint8_t i = 0U; i < kMotorVersionMaxNodes; ++i) { + if (s_verTable.slots[i].dirty != 0U) { + return true; + } + } + return false; +} + +uint16_t buildPayload(uint8_t *buf, uint16_t maxLen) +{ + if (buf == nullptr || maxLen < kMotorVersionHeaderLen) { + return 0U; + } + + /* Header (2B): count 稍后回填, rsvd=0 */ + buf[0] = 0U; + buf[1] = 0U; + + uint16_t offset = kMotorVersionHeaderLen; + uint8_t count = 0U; + + for (uint8_t i = 0U; i < kMotorVersionMaxNodes; ++i) { + MotorVersionSlot &slot = s_verTable.slots[i]; + if (slot.dirty == 0U) { + continue; + } + /* 检查剩余空间;装不下则保留 dirty 待下轮(不丢) */ + if (offset + kMotorVersionEntrySize > maxLen) { + break; + } + + buf[offset + 0U] = slot.node_id; + writeU32Le(&buf[offset + 1U], slot.seq); + std::memcpy(&buf[offset + 5U], slot.version, kMotorVersionDlc); + + offset += kMotorVersionEntrySize; + count++; + slot.dirty = 0U; /* 已打包进本次推送 */ + } + + if (count == 0U) { + return 0U; + } + buf[0] = count; + return offset; +} + +} // namespace MotorVersion diff --git a/GD32E508_DCU_APP/device/motor_version.h b/GD32E508_DCU_APP/device/motor_version.h new file mode 100644 index 0000000..aed491c --- /dev/null +++ b/GD32E508_DCU_APP/device/motor_version.h @@ -0,0 +1,68 @@ +/** + * @file motor_version.h + * @brief 多电机版本缓存 — CAN 版本应答帧(0x00FF)识别、内存缓存、SPI 0xC6 推送序列化。 + * + * 设计要点(与 motor_state 同款模型): + * - 仅在主循环上下文 (taskCanRxParse / pushStep) 中读写,无 ISR 竞态,不需要临界区。 + * - GD32 不做语义解码,只缓存原始 24B 版本 payload,由前端解码。 + * - 事件驱动:电机回报版本(0x000N00FF)时缓存并置 dirty,pushStep 经 ring 推 0xC6。 + * 版本是按需查询、低频数据,不进 250ms 周期推送(0xC5),避免占用周期带宽。 + */ + +#ifndef DEVICE_MOTOR_VERSION_H +#define DEVICE_MOTOR_VERSION_H + +#include +#include +#include "sys/config.h" + +/* ── CAN ID 匹配规则 (29-bit 扩展帧) ── + * 版本应答 = 0x000N00FF(DBC CANFD_GET_VERSION,发送方 Motor,与请求同 ID)。 + * 电机 APP 仅在收到 base_id=0x00FF 时回报(CANFD_VERSION_ID)。 + */ +static constexpr uint32_t kMotorVersionIdMask = 0xFFF0FFFFU; /* 屏蔽 node nibble [19:16] */ +static constexpr uint32_t kMotorVersionIdMatch = 0x000000FFU; /* 版本应答类型码 */ + +static constexpr uint8_t kMotorVersionMaxNodes = CONFIG_MOTOR_MAX_NODES; +static constexpr uint8_t kMotorVersionDlc = 24U; + +/* ── 序列化常量 (0xC6 payload) ── + * Header 2B: count + rsvd;每 entry 29B: node(1)+seq(4)+version(24)。 + * (236-2)/29 = 8.06 → 单帧最多 8 个节点,版本按需查询、通常单节点,足够。 */ +static constexpr uint8_t kMotorVersionHeaderLen = 2U; +static constexpr uint8_t kMotorVersionEntrySize = 29U; /* 1+4+24 */ + +/// 单电机版本槽 +struct MotorVersionSlot { + uint8_t node_id; ///< 1-9, 0=空槽 + uint8_t dirty; ///< 1=已缓存待推送, 0=已推送/空 + uint8_t rsvd[2]; + uint32_t seq; ///< 更新序号 (每次收到新应答 +1) + uint8_t version[kMotorVersionDlc]; ///< 原始版本 payload +}; + +/// 全局电机版本表 +struct MotorVersionTable { + MotorVersionSlot slots[kMotorVersionMaxNodes]; ///< 按 node_id-1 索引 + uint32_t global_seq; ///< 全局更新序号 +}; + +/// 电机版本管理命名空间 +namespace MotorVersion { + +/// 获取全局版本表引用 +MotorVersionTable &table(); + +/// CAN 帧到达时调用 (主循环上下文, taskCanRxParse 内);识别 0x00FF 并缓存 +void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len); + +/// 是否有待推送的版本更新 (任一槽 dirty) +bool hasPending(); + +/// 序列化所有待推送版本到 payload 缓冲区,并清除已打包槽的 dirty。 +/// @return 实际写入字节数, 0=无待推送/参数错误 +uint16_t buildPayload(uint8_t *buf, uint16_t maxLen); + +} // namespace MotorVersion + +#endif /* DEVICE_MOTOR_VERSION_H */ diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_adc_feedback.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_adc_feedback.cpp index 40a3613..54794fd 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_adc_feedback.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_adc_feedback.cpp @@ -29,6 +29,7 @@ #include "device/adc.h" #include "sys/config.h" #include "sys/ota_boot_request.h" +#include "sys/motor_ota_relay.h" #include "sys/task_runner.h" #include "main.h" #include @@ -130,6 +131,11 @@ void adcFeedbackPushStep() s_needPush = false; return; } + /* 电机 OTA relay 活跃期间停止 ADC 反馈推送,避免按铃干扰 0xD1 数据流 + * (详见 canFeedbackPushStep 同款注释)。 */ + if (MotorOtaRelay::isActive()) { + return; + } /* 与 CAN 反馈共用 ring:未读走则等下一主循环,不抢占 */ if (AppSpiCommandBus.isRingPending()) { return; diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp index 3ab98cc..d9d14aa 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp @@ -39,6 +39,7 @@ #include "device/canfd.h" #include "sys/config.h" #include "sys/ota_boot_request.h" +#include "sys/motor_ota_relay.h" #include "spi_link_v2.h" #include #include @@ -281,6 +282,15 @@ void canFeedbackPushStep() return; } + /* 电机 OTA relay 活跃期间停止 CAN 反馈主动推送: + * OTA 数据流(0xD1)在 SPI 上是紧循环 stop-and-wait,本推送会拉低 PB6 按 + * 铃、占用 m_txBuf,与 0xD1 的 ACK 响应争用单缓冲;ESP32 的 send/poll + * 两步法会把 0xC1 EVENT 帧当作 0xD1 响应误读,导致 feedTask 误判失败。 + * OTA 结束后 s_roundActive 由 taskHeartbeat 重新激活。 */ + if (MotorOtaRelay::isActive()) { + return; + } + /* 1Hz 节流:当前轮次未激活则等待 taskHeartbeat() 触发 */ if (!s_roundActive) { return; diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_ota.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_ota.cpp new file mode 100644 index 0000000..83c4a02 --- /dev/null +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_ota.cpp @@ -0,0 +1,141 @@ +/** + * @file app_spi_cmd_motor_ota.cpp + * @brief SPI 命令处理:电机 OTA relay(0xD0 命令 / 0xD1 数据)。 + * + * 0xD0 payload: [cmd(1B)] + 参数 + * cmd=0 (START): [ch(1B) + fw_size(4B LE)] + * cmd=1 (CANCEL): 无参数 + * cmd=2 (QUERY): 无参数 → 返回 12B 状态 + * + * 0xD1 payload: [offset:u32 LE][固件明文…](总长≤236,数据≤232), + * GD32 按绝对 offset 幂等写入环缓后本地加密发 CAN。 + */ +#include "device/spi/spi_command_handler.h" +#include "device/spi/app_spi_command.h" +#include "device/spi/app_spi_command_defs.h" +#include "sys/motor_ota_relay.h" +#include +#include + +namespace { + +class SpiMotorOtaHandler final : public ISpiCommandHandler { +public: + bool canHandle(uint8_t frame_type) const override + { + return (frame_type == SpiDefs::kTypeMotorOtaCmd || + frame_type == SpiDefs::kTypeMotorOtaData); + } + + void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override + { + if (ctx.frame_type == SpiDefs::kTypeMotorOtaCmd) { + handleCmd(bus, ctx); + } else { + handleData(bus, ctx); + } + } + +private: + void handleCmd(AppSpiCommand &bus, const SpiCommandContext &ctx) + { + if (ctx.payload_len < 1U) { + const uint8_t err = 0xE0U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + const uint8_t cmd = ctx.payload[0]; + switch (cmd) { + case 0U: { /* START */ + if (ctx.payload_len < 6U) { + const uint8_t err = 0xE0U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + const uint8_t ch = ctx.payload[1]; + const uint32_t fw_size = + static_cast(ctx.payload[2]) + | (static_cast(ctx.payload[3]) << 8) + | (static_cast(ctx.payload[4]) << 16) + | (static_cast(ctx.payload[5]) << 24); + + printf("[MOTOR-OTA] SPI 0xD0 START recv ch=%u fw=%lu\r\n", + (unsigned)ch, (unsigned long)fw_size); + if (MotorOtaRelay::start(ch, fw_size)) { + const uint8_t ack = 0x00U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, &ack, 1U); + } else { + const uint8_t err = 0xE5U; /* 已在进行中或参数无效 */ + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + } + break; + } + case 1U: { /* CANCEL */ + MotorOtaRelay::cancel(); + const uint8_t ack = 0x00U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, &ack, 1U); + break; + } + case 2U: { /* QUERY */ + const MotorOtaRelay::Status st = MotorOtaRelay::getStatus(); + uint8_t resp[12]; + resp[0] = static_cast(st.state); + resp[1] = st.error_code; + resp[2] = static_cast(st.packets_acked & 0xFF); + resp[3] = static_cast(st.packets_acked >> 8); + resp[4] = static_cast(st.total_packets & 0xFF); + resp[5] = static_cast(st.total_packets >> 8); + resp[6] = static_cast(st.fw_size & 0xFF); + resp[7] = static_cast((st.fw_size >> 8) & 0xFF); + resp[8] = static_cast((st.fw_size >> 16) & 0xFF); + resp[9] = static_cast((st.fw_size >> 24) & 0xFF); + resp[10] = 0; /* reserved */ + resp[11] = 0; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, resp, 12U); + break; + } + default: { + const uint8_t err = 0xE1U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + break; + } + } + } + + void handleData(AppSpiCommand &bus, const SpiCommandContext &ctx) + { + if (ctx.payload_len == 0U) { + const uint8_t ack = 0x00U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, &ack, 1U); + return; + } + + if (MotorOtaRelay::feedData(ctx.payload, ctx.payload_len)) { + const uint8_t ack = 0x00U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, &ack, 1U); + } else { + /* 缓冲满或状态不对:返回 WAIT,ESP32 稍后重试 */ + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspWait, nullptr, 0U); + } + } +}; + +static SpiMotorOtaHandler s_handler; + +} // namespace + +void registerSpiMotorOtaCommand(AppSpiCommand &bus) +{ + bus.registerHandler(&s_handler); +} diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_state.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_state.cpp new file mode 100644 index 0000000..9ee0c45 --- /dev/null +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_state.cpp @@ -0,0 +1,139 @@ +/** + * @file app_spi_cmd_motor_state.cpp + * @brief 多电机状态 250ms 主动推送 (TYPE 0xC5) 与被动查询处理器。 + * + * 与 0xC1 CAN 反馈 / 0xC4 ADC 反馈同款模型: + * - 主循环 pushStep 非阻塞;ring 未读走则跳过。 + * - 优先级:CAN feedback > Motor state > ADC(runPending 调用顺序决定)。 + * - 启动节拍:taskMotorStatePush()(250ms 独立任务位,与 1Hz heartbeat 解耦)。 + * - 总开关:sys/config.h 中 CONFIG_MOTOR_STATE_ENABLE + * + * payload 格式 (每页): + * [0] page 页号 (0/1) + * [1] total_pages 总页数 (2) + * [2] entry_count 本页有效电机数 + * [3] active_count 全局在线电机数 + * [4..] 每个 entry 44B: + * [0] node_id, [1] channel, [2] online, [3] dlc, + * [4..7] rx_count (LE), [8..11] seq (LE), + * [12..43] data[32] (原始 STATUS payload) + */ + +#include "device/spi/spi_command_handler.h" +#include "device/spi/app_spi_command.h" +#include "device/spi/app_spi_command_defs.h" +#include "device/motor_state.h" +#include "sys/config.h" +#include "sys/ota_boot_request.h" +#include "sys/motor_ota_relay.h" +#include "spi_link_v2.h" +#include + +#if (CONFIG_MOTOR_STATE_ENABLE != 0) + +/* ---- 被动查询 handler ---- */ + +namespace { + +static constexpr uint16_t kMotorPayloadMax = + static_cast(AppSpiCommand::kTxBufSize - V2_OVERHEAD); + +class SpiMotorStateHandler final : public ISpiCommandHandler { +public: + bool canHandle(uint8_t frame_type) const override + { + return frame_type == SpiDefs::kTypeMotorState; + } + + void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override + { + /* 请求 payload[0] = page (可选, 默认 0) */ + const uint8_t page = (ctx.payload_len >= 1U) ? ctx.payload[0] : 0U; + + uint8_t payload[kMotorPayloadMax] = {}; + const uint16_t n = MotorState::buildPayload(page, payload, sizeof(payload)); + + if (n == 0U) { + uint8_t err = 0xE1U; /* 非法页号 */ + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, payload, n); + } +}; + +static SpiMotorStateHandler s_handler; + +} // namespace + +/* ---- 主动推送状态机 ---- */ + +static volatile bool s_roundActive = true; /* 首次允许启动,无需等 1s */ +static uint8_t s_page = 0U; + +/** + * @brief 激活新一轮 0xC5 推送。 + * @note 若上一轮仍在进行中(s_roundActive=true),不重置 s_page, + * 避免在 250ms 节拍 < 单轮 P0+P1 排空时间时反复打断、 + * 导致 P1 永远没机会推(前端只看到 P0 的 5 个节点)。 + * 新一轮请求会被合并到当前轮次,推完 P1 后自然进入下一轮。 + * 这与 0xC1 canFeedbackStartRound() 的语义不同:CAN 反馈 1Hz + * 远大于排空时间,不会发生自打断;0xC5 在 250ms 节拍下需要这层保护。 + */ +void motorStateStartRound() +{ + s_roundActive = true; + /* 仅在轮次已结束时重置页号;进行中保持 s_page 不动 */ + if (s_page >= kMotorTotalPages) { + s_page = 0U; + } +} + +void motorStatePushStep() +{ + if (!s_roundActive) { + return; + } + if (OtaBootRequest::isRebootPending()) { + s_roundActive = false; + return; + } + /* 电机 OTA relay 活跃期间停止 motor state 推送,避免按铃干扰 0xD1 数据流 + * (详见 canFeedbackPushStep 同款注释)。 */ + if (MotorOtaRelay::isActive()) { + return; + } + if (AppSpiCommandBus.isRingPending()) { + return; + } + + uint8_t payload[kMotorPayloadMax] = {}; + const uint16_t n = MotorState::buildPayload(s_page, payload, sizeof(payload)); + + if (n > 0U) { + (void)AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeMotorState, payload, n); + } + + /* 推进页号;到末页后结束本轮 */ + s_page++; + if (s_page >= kMotorTotalPages) { + s_page = 0U; + s_roundActive = false; + } +} + +void registerSpiMotorStateCommand(AppSpiCommand &bus) +{ + bus.registerHandler(&s_handler); +} + +#else /* CONFIG_MOTOR_STATE_ENABLE == 0 */ + +void motorStateStartRound() {} +void motorStatePushStep() {} +void registerSpiMotorStateCommand(AppSpiCommand & /*bus*/) {} + +#endif diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_version.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_version.cpp new file mode 100644 index 0000000..c54a159 --- /dev/null +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_version.cpp @@ -0,0 +1,68 @@ +/** + * @file app_spi_cmd_motor_version.cpp + * @brief 多电机版本 0xC6 事件驱动推送(pushStep)。 + * + * 与 0xC1/0xC4/0xC5 同款 ring 按铃模型,但触发方式不同: + * - 0xC5 是 250ms 周期轮次;0xC6 是事件驱动——电机回报版本(0x00FF)时 + * MotorVersion::onCanFrame() 缓存并置 dirty,本 pushStep 在主循环发现 + * 有待推送(dirty)且 ring 空闲时,打包一帧 0xC6 按铃推给 ESP32。 + * - 版本是按需查询的低频数据,推送优先级最低(runPending 中排在 0xC1/0xC5/0xC4 之后)。 + * - 总开关:sys/config.h 中 CONFIG_MOTOR_VERSION_ENABLE + * + * payload 格式(单帧,Header 2B + 每 entry 29B): + * [0] count 本帧节点数 + * [1] rsvd 保留 + * [2..] 每个 entry 29B: + * [0] node_id, [1..4] seq (LE), [5..28] version[24] (原始版本 payload) + * + * 仅推送(无被动查询 handler):ESP32 收到后自行缓存,Web 从 ESP32 缓存读取。 + */ + +#include "device/spi/app_spi_command.h" +#include "device/spi/app_spi_command_defs.h" +#include "device/motor_version.h" +#include "sys/config.h" +#include "sys/ota_boot_request.h" +#include "sys/motor_ota_relay.h" +#include "spi_link_v2.h" +#include + +#if (CONFIG_MOTOR_VERSION_ENABLE != 0) + +namespace { +static constexpr uint16_t kVerPayloadMax = + static_cast(AppSpiCommand::kTxBufSize - V2_OVERHEAD); +} + +void motorVersionPushStep() +{ + /* 无待推送 → 直接返回(事件驱动,绝大多数主循环迭代在此短路) */ + if (!MotorVersion::hasPending()) { + return; + } + if (OtaBootRequest::isRebootPending()) { + return; + } + /* 电机 OTA relay 活跃期间停止版本推送,避免按铃干扰 0xD1 数据流 + * (详见 canFeedbackPushStep 同款注释)。dirty 保留不丢,OTA 结束后继续推。 */ + if (MotorOtaRelay::isActive()) { + return; + } + if (AppSpiCommandBus.isRingPending()) { + return; /* 上一帧未被 ESP32 读走,下轮再试(dirty 保留不丢) */ + } + + uint8_t payload[kVerPayloadMax] = {}; + const uint16_t n = MotorVersion::buildPayload(payload, sizeof(payload)); + if (n > 0U) { + /* ring 空闲时 requestEsp32Read 必成功(单线程主循环,检查与调用之间无并发); + * buildPayload 已清掉已打包槽的 dirty。 */ + (void)AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeMotorVersion, payload, n); + } +} + +#else /* CONFIG_MOTOR_VERSION_ENABLE == 0 */ + +void motorVersionPushStep() {} + +#endif diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp index 8d5651d..597c949 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp @@ -386,6 +386,8 @@ extern void registerSpiCanBridgeCommand(AppSpiCommand &bus); extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus); extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus); extern void registerSpiAdcFeedbackCommand(AppSpiCommand &bus); +extern void registerSpiMotorStateCommand(AppSpiCommand &bus); +extern void registerSpiMotorOtaCommand(AppSpiCommand &bus); AppSpiCommand AppSpiCommandBus; void AppSpiCommand::init() { @@ -407,6 +409,8 @@ void AppSpiCommand::init() registerSpiCanFeedbackCommand(AppSpiCommandBus); registerSpiIdSnapshotCommand(AppSpiCommandBus); registerSpiAdcFeedbackCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */ + registerSpiMotorStateCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */ + registerSpiMotorOtaCommand(AppSpiCommandBus); configureGpio(); configureDma(); @@ -467,7 +471,14 @@ void AppSpiCommand::armSpiDmasForNextCs() void AppSpiCommand::registerHandler(ISpiCommandHandler *handler) { - if (!handler || m_commandCount >= kMaxCommands) { + if (!handler) { + return; + } + if (m_commandCount >= kMaxCommands) { + /* 表满静默丢弃会导致命令无声失效(motor_ota 曾因此 0xD0 回 0xEE)。 + * init 在任务上下文,printf 走 USART0 中断环,启动日志可见。 */ + printf("[SPI-APP] registerHandler: 注册表满(%u),handler 被丢弃! 请增大 CONFIG_SPI_MAX_COMMANDS\r\n", + static_cast(kMaxCommands)); return; } m_commands[m_commandCount++] = handler; @@ -811,6 +822,13 @@ void AppSpiCommand::onCsRelease() m_txLen = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + /* ★ 关键:在 processCommand 之前同步 g_localCredit。 + * 此时 m_txLen=0, m_ringPending=false → g_localCredit=1。 + * 若不做,buildResponse 会读到上一次 pushStatus/requestEsp32Read 留下的 + * g_localCredit=0 → 响应帧携带 credit=0 → ESP32 m_remoteCredit=0 → 后续发送死锁。 + * 这是电机 OTA 吐量只有 1 chunk/2s 的根因。 */ + updateDataReady(); + /* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式), * 消除主循环延迟。处理完成后 processCommand() 末尾再 re-arm DMA + 释放 SPIready。 */ processCommand(); diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.h b/GD32E508_DCU_APP/device/spi/app_spi_command.h index c9b0bfe..c09e6b4 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.h @@ -173,6 +173,21 @@ void adcFeedbackPushStep(); /** @brief 激活一次 ADC 调试反馈推送(1Hz taskHeartbeat)。 */ void adcFeedbackStartRound(); +/** + * @brief 多电机状态(0xC5)主动推送一步。 + * @note CONFIG_MOTOR_STATE_ENABLE=0 时为空操作。 + */ +void motorStatePushStep(); + +/** @brief 激活新一轮多电机状态推送(1Hz taskHeartbeat)。 */ +void motorStateStartRound(); + +/** + * @brief 多电机版本(0xC6)事件驱动推送步进:有 dirty 版本且 ring 空闲时按铃推一帧。 + * @note CONFIG_MOTOR_VERSION_ENABLE=0 时为空操作。优先级低于 0xC1/0xC5/0xC4。 + */ +void motorVersionPushStep(); + /** @brief 全局 SPI 命令总线实例。 */ extern AppSpiCommand AppSpiCommandBus; diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h index aac7674..4366769 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h @@ -27,6 +27,21 @@ static constexpr uint8_t kTypeIdSnapshot = 0xC3U; /**< ESP32 -> GD32 拉取不 /** @brief GD32 ADC 调试反馈(温度/VDDA/VIN);1Hz 主动 EVENT 或被动查询。 * 由 CONFIG_SPI_ADC_FEEDBACK_ENABLE 门控,调试用可随时关。 */ static constexpr uint8_t kTypeAdcFeedback = 0xC4U; +/** @brief 多电机状态推送;GD32 1Hz 主动 EVENT,将 CAN STATUS 帧缓存表推给 ESP32。 + * 由 CONFIG_MOTOR_STATE_ENABLE 门控。 */ +static constexpr uint8_t kTypeMotorState = 0xC5U; +/** @brief 多电机版本推送;GD32 收到电机版本应答(0x00FF)后事件驱动 EVENT, + * 将缓存的 24B 版本推给 ESP32(按需查询、低频,不进周期推送)。 + * 由 CONFIG_MOTOR_VERSION_ENABLE 门控。 */ +static constexpr uint8_t kTypeMotorVersion = 0xC6U; + +/** @brief 电机 OTA relay 命令(ESP32→GD32):start / cancel / query。 + * payload[0]=cmd(0=start,1=cancel,2=query), start 时 payload[1]=ch, payload[2..5]=fw_size LE */ +static constexpr uint8_t kTypeMotorOtaCmd = 0xD0U; +/** @brief 电机 OTA relay 数据(ESP32→GD32):固件明文块,GD32 本地加密后发 CAN。 */ +static constexpr uint8_t kTypeMotorOtaData = 0xD1U; +/** @brief 电机 OTA relay 状态推送(GD32→ESP32 ring push):12B 进度。 */ +static constexpr uint8_t kTypeMotorOtaStatus = 0xD2U; /* 响应类型 */ static constexpr uint8_t kRspAck = 0x80U; diff --git a/GD32E508_DCU_APP/main.h b/GD32E508_DCU_APP/main.h index 8d4f8c7..bf700d3 100644 --- a/GD32E508_DCU_APP/main.h +++ b/GD32E508_DCU_APP/main.h @@ -16,8 +16,9 @@ enum system_task_t { TASK_ADC_TEMP_SAMPLE, /**< 采样内部温度通道原始值任务。 */ TASK_ADC_PROCESS, /**< 对 ADC 原始值进行电压与温度计算任务。 */ TASK_CAN_RX_PARSE, /**< 解析并消费已接收 CANFD 帧任务。 */ - TASK_CAN_TX_SEND, /**< CANFD 周期发送任务,每 1ms 每通道发送一帧。 */ + TASK_CAN_TX_SEND, /**< CANFD 周期发送任务,每 10ms 每通道发送一帧。 */ TASK_HEARTBEAT, /**< 系统心跳维护任务。 */ + TASK_MOTOR_STATE_PUSH, /**< 多电机状态 0xC5 主动推送启动任务(250ms)。 */ TASK_DIAGNOSTIC_PRINT, /**< 诊断信息打印任务。 */ TASK_ETHERCAT_PROCESS, /**< EtherCAT 周期/事件处理任务(非实时应用层 PDO 映射)。 */ TASK_COUNT /**< 任务总数,用于边界或统计场景。 */ @@ -41,10 +42,12 @@ struct task_scheduler_t { volatile uint16_t div_temp_sample; /** @brief 调度分频计数:CAN 接收处理。 */ volatile uint16_t div_can_rx; - /** @brief 调度分频计数:CAN 周期发送(1ms)。 */ + /** @brief 调度分频计数:CAN 周期发送(10ms)。 */ volatile uint16_t div_can_tx; /** @brief 调度分频计数:心跳(1s)。 */ volatile uint16_t div_heartbeat; + /** @brief 调度分频计数:多电机状态推送(250ms)。 */ + volatile uint16_t div_motor_state; /** @brief 调度分频计数:APP 状态/诊断打印(5s)。 */ volatile uint16_t div_diag_print; }; diff --git a/GD32E508_DCU_APP/sys/aes128.cpp b/GD32E508_DCU_APP/sys/aes128.cpp new file mode 100644 index 0000000..5cf927d --- /dev/null +++ b/GD32E508_DCU_APP/sys/aes128.cpp @@ -0,0 +1,183 @@ +/** + * @file aes128.cpp + * @brief AES-128 ECB 加解密实现(移植自电机 BOOT 工程 sys/aes128.cpp)。 + */ +#include "sys/aes128.h" + +/* S盒 */ +static const uint8_t sbox[256] = { + 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, + 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, + 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, + 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, + 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, + 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, + 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, + 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, + 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, + 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, + 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, + 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, + 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, + 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, + 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, + 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 +}; + +/* 逆S盒 */ +static const uint8_t inv_sbox[256] = { + 0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, + 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, + 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, + 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, + 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, + 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, + 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, + 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, + 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, + 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, + 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, + 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, + 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, + 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, + 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, + 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d +}; + +/* 轮常量 */ +static const uint8_t rcon[10] = {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36}; + +static uint8_t xtime(uint8_t x) { return static_cast((x << 1) ^ (((x >> 7) & 1) * 0x1b)); } + +static uint8_t gf_mul(uint8_t a, uint8_t b) { + uint8_t p = 0; + while (b) { + if (b & 1) p ^= a; + const uint8_t hi = a & 0x80; + a <<= 1; + if (hi) a ^= 0x1b; + b >>= 1; + } + return p; +} + +static void add_round_key(uint8_t *state, const uint8_t *rk) { + for (uint8_t i = 0; i < 16; i++) state[i] ^= rk[i]; +} + +static void sub_bytes(uint8_t *state) { + for (uint8_t i = 0; i < 16; i++) state[i] = sbox[state[i]]; +} + +static void inv_sub_bytes(uint8_t *state) { + for (uint8_t i = 0; i < 16; i++) state[i] = inv_sbox[state[i]]; +} + +static void shift_rows(uint8_t *s) { + uint8_t t; + t=s[1]; s[1]=s[5]; s[5]=s[9]; s[9]=s[13]; s[13]=t; + t=s[2]; s[2]=s[10]; s[10]=t; t=s[6]; s[6]=s[14]; s[14]=t; + t=s[3]; s[3]=s[15]; s[15]=s[11]; s[11]=s[7]; s[7]=t; +} + +static void inv_shift_rows(uint8_t *s) { + uint8_t t; + t=s[13]; s[13]=s[9]; s[9]=s[5]; s[5]=s[1]; s[1]=t; + t=s[2]; s[2]=s[10]; s[10]=t; t=s[6]; s[6]=s[14]; s[14]=t; + t=s[7]; s[7]=s[11]; s[11]=s[15]; s[15]=s[3]; s[3]=t; +} + +static void mix_columns(uint8_t *s) { + for (uint8_t c = 0; c < 4; c++) { + const uint8_t i = c * 4; + const uint8_t a0=s[i], a1=s[i+1], a2=s[i+2], a3=s[i+3]; + s[i] = xtime(a0) ^ xtime(a1) ^ a1 ^ a2 ^ a3; + s[i+1] = a0 ^ xtime(a1) ^ xtime(a2) ^ a2 ^ a3; + s[i+2] = a0 ^ a1 ^ xtime(a2) ^ xtime(a3) ^ a3; + s[i+3] = xtime(a0) ^ a0 ^ a1 ^ a2 ^ xtime(a3); + } +} + +static void inv_mix_columns(uint8_t *s) { + for (uint8_t c = 0; c < 4; c++) { + const uint8_t i = c * 4; + const uint8_t a0=s[i], a1=s[i+1], a2=s[i+2], a3=s[i+3]; + s[i] = gf_mul(0x0e,a0)^gf_mul(0x0b,a1)^gf_mul(0x0d,a2)^gf_mul(0x09,a3); + s[i+1] = gf_mul(0x09,a0)^gf_mul(0x0e,a1)^gf_mul(0x0b,a2)^gf_mul(0x0d,a3); + s[i+2] = gf_mul(0x0d,a0)^gf_mul(0x09,a1)^gf_mul(0x0e,a2)^gf_mul(0x0b,a3); + s[i+3] = gf_mul(0x0b,a0)^gf_mul(0x0d,a1)^gf_mul(0x09,a2)^gf_mul(0x0e,a3); + } +} + +static void key_expansion(aes128_context *ctx) { + memcpy(ctx->round_keys[0], ctx->key, 16); + for (uint8_t i = 1; i <= 10; i++) { + uint8_t temp[4]; + temp[0] = ctx->round_keys[i-1][12]; + temp[1] = ctx->round_keys[i-1][13]; + temp[2] = ctx->round_keys[i-1][14]; + temp[3] = ctx->round_keys[i-1][15]; + const uint8_t t = temp[0]; + temp[0]=temp[1]; temp[1]=temp[2]; temp[2]=temp[3]; temp[3]=t; + temp[0]=sbox[temp[0]]; temp[1]=sbox[temp[1]]; + temp[2]=sbox[temp[2]]; temp[3]=sbox[temp[3]]; + temp[0] ^= rcon[i-1]; + ctx->round_keys[i][0] = ctx->round_keys[i-1][0] ^ temp[0]; + ctx->round_keys[i][1] = ctx->round_keys[i-1][1] ^ temp[1]; + ctx->round_keys[i][2] = ctx->round_keys[i-1][2] ^ temp[2]; + ctx->round_keys[i][3] = ctx->round_keys[i-1][3] ^ temp[3]; + for (uint8_t j = 1; j < 4; j++) { + ctx->round_keys[i][j*4+0] = ctx->round_keys[i][(j-1)*4+0] ^ ctx->round_keys[i-1][j*4+0]; + ctx->round_keys[i][j*4+1] = ctx->round_keys[i][(j-1)*4+1] ^ ctx->round_keys[i-1][j*4+1]; + ctx->round_keys[i][j*4+2] = ctx->round_keys[i][(j-1)*4+2] ^ ctx->round_keys[i-1][j*4+2]; + ctx->round_keys[i][j*4+3] = ctx->round_keys[i][(j-1)*4+3] ^ ctx->round_keys[i-1][j*4+3]; + } + } +} + +void aes128_set_key(aes128_context *ctx, const uint8_t *key) { + memcpy(ctx->key, key, AES128_KEY_LEN); + key_expansion(ctx); +} + +void aes128_encrypt_block(aes128_context *ctx, const uint8_t *input, uint8_t *output) { + memcpy(ctx->state, input, AES_BLOCK_SIZE); + add_round_key(ctx->state, ctx->round_keys[0]); + for (uint8_t r = 1; r <= 9; r++) { + sub_bytes(ctx->state); + shift_rows(ctx->state); + mix_columns(ctx->state); + add_round_key(ctx->state, ctx->round_keys[r]); + } + sub_bytes(ctx->state); + shift_rows(ctx->state); + add_round_key(ctx->state, ctx->round_keys[10]); + memcpy(output, ctx->state, AES_BLOCK_SIZE); +} + +void aes128_decrypt_block(aes128_context *ctx, const uint8_t *input, uint8_t *output) { + memcpy(ctx->state, input, AES_BLOCK_SIZE); + add_round_key(ctx->state, ctx->round_keys[10]); + for (uint8_t r = 9; r >= 1; r--) { + inv_shift_rows(ctx->state); + inv_sub_bytes(ctx->state); + add_round_key(ctx->state, ctx->round_keys[r]); + inv_mix_columns(ctx->state); + } + inv_shift_rows(ctx->state); + inv_sub_bytes(ctx->state); + add_round_key(ctx->state, ctx->round_keys[0]); + memcpy(output, ctx->state, AES_BLOCK_SIZE); +} + +void aes128_ecb_encrypt_48bytes(aes128_context *ctx, const uint8_t *input, uint8_t *output) { + aes128_encrypt_block(ctx, input, output); + aes128_encrypt_block(ctx, input + 16, output + 16); + aes128_encrypt_block(ctx, input + 32, output + 32); +} + +void aes128_ecb_decrypt_48bytes(aes128_context *ctx, const uint8_t *input, uint8_t *output) { + aes128_decrypt_block(ctx, input, output); + aes128_decrypt_block(ctx, input + 16, output + 16); + aes128_decrypt_block(ctx, input + 32, output + 32); +} diff --git a/GD32E508_DCU_APP/sys/aes128.h b/GD32E508_DCU_APP/sys/aes128.h new file mode 100644 index 0000000..bac5740 --- /dev/null +++ b/GD32E508_DCU_APP/sys/aes128.h @@ -0,0 +1,28 @@ +/** + * @file aes128.h + * @brief AES-128 ECB 加解密(纯软件实现,移植自电机 BOOT 工程)。 + * 用于电机 OTA relay 中对固件数据加密及密钥协商。 + */ +#ifndef SYS_AES128_H +#define SYS_AES128_H + +#include +#include +#include + +static constexpr uint8_t AES128_KEY_LEN = 16U; +static constexpr uint8_t AES_BLOCK_SIZE = 16U; + +struct aes128_context { + uint8_t key[AES128_KEY_LEN]; + uint8_t round_keys[11][16]; + uint8_t state[AES_BLOCK_SIZE]; +}; + +void aes128_set_key(aes128_context *ctx, const uint8_t *key); +void aes128_encrypt_block(aes128_context *ctx, const uint8_t *input, uint8_t *output); +void aes128_decrypt_block(aes128_context *ctx, const uint8_t *input, uint8_t *output); +void aes128_ecb_encrypt_48bytes(aes128_context *ctx, const uint8_t *input, uint8_t *output); +void aes128_ecb_decrypt_48bytes(aes128_context *ctx, const uint8_t *input, uint8_t *output); + +#endif /* SYS_AES128_H */ diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index 8d4a270..e39915e 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -14,31 +14,62 @@ * 功能开关(调试/特性总闸 — 改这里即可开关,改完重编) * 0/false=关,1/true=开。需要 #if 的保留 #define。 * ============================================================ */ -/** @brief 周期温度/电压 UART 打印(5s,task_manager #if)。 */ -#define CONFIG_UART_TEMP_PERIODIC_PRINT 1 -/** @brief UART 诊断详细级别(task_runner #if;0=精简 1=含 CAN 统计)。 */ -#define CONFIG_UART_DIAG_LEVEL 1 +/** + * @brief 5s 诊断任务是否输出全量 UART 摘要(task_runner)。 + * @note 任务本身始终 5s 调度:0=默认仅状态变化/错误增量;1=周期打印 ADC/CAN/ECAT 全量行。 + * 现场抓吞吐/温度时改回 1 后重编即可。 + */ +#define CONFIG_UART_TEMP_PERIODIC_PRINT 0 +/** + * @brief 全量摘要详细级别(仅 PERIODIC_PRINT=1 时生效;0=ADC 精简 1=含 CAN 统计)。 + * @note 默认 0;需要 can_tx/rx/drop 等列时与 PERIODIC_PRINT 一并打开。 + */ +#define CONFIG_UART_DIAG_LEVEL 0 /** * @brief SPI 调试反馈 0xC4:GD32 ADC 温度/VDDA/VIN 1Hz 主动推送。 * @note 调试用;0=不注册 handler、不推送。与 ESP32 GD32_ADC_FEEDBACK_ENABLE 配对。 */ #define CONFIG_SPI_ADC_FEEDBACK_ENABLE 1 +/** + * @brief 多电机状态表 0xC5:CAN STATUS 帧缓存 + 250ms 主动推送。 + * @note 0=不注册 handler、不推送、不解析 STATUS 帧。与 ESP32 端 motor_state_task 配对。 + */ +#define CONFIG_MOTOR_STATE_ENABLE 1 +/** + * @brief 多电机版本 0xC6:CAN 版本应答帧(0x00FF)缓存 + 事件驱动推送。 + * @note 0=不识别版本帧、不推送。与 ESP32 端 motor_version 缓存配对。 + * 版本为按需查询的低频数据,独立于 0xC5 周期推送。 + */ +#define CONFIG_MOTOR_VERSION_ENABLE 1 +/** @brief 最大电机节点数 (node ID 1-9)。 */ +constexpr uint8_t CONFIG_MOTOR_MAX_NODES = 9U; +/** @brief 电机离线判定超时 (ms)。超过此时间未收到 STATUS 帧则标记 offline。 */ +constexpr uint16_t CONFIG_MOTOR_OFFLINE_MS = 3000U; /** @brief SPI 命令逐帧日志(#if)。 */ #define CONFIG_SPI_APP_LOG_ENABLE 0 /** @brief CAN_SEND 成功帧详细日志(#if)。 */ #define CONFIG_SPI_CAN_SEND_VERBOSE_LOG 0 -/** @brief CAN-FD BRS 使能(canfd.cpp #if)。 */ -#define CONFIG_CAN_FD_BRS_ENABLE 1U +/** @brief CAN-FD BRS 使能(canfd.cpp #if)。 + * 关闭 BRS: 数据段用仲裁段同速率,牢牢性更高。 + * 电机 BOOT 无法正确接收 BRS 高速段→nonce(16B FD+BRS)被拒绝→握手失败。 */ +#define CONFIG_CAN_FD_BRS_ENABLE 0U /** @brief 启动时总线探测帧;正常固件关。 */ #define CONFIG_CAN_STARTUP_BUS_TEST_ENABLE 0 /** @brief 启动时内部回环自检;正常固件关。 */ #define CONFIG_CAN_STARTUP_LOOPBACK_TEST_ENABLE 0 /** @brief CAN 启动详细诊断 dump;默认关。 */ #define CONFIG_CAN_STARTUP_DIAG_VERBOSE 0 -/** @brief CAN0/1/2 通道使能。 */ +/** + * @brief 周期测试发送任务(taskCanTxSend,发 0x18FF5000 64B FD 帧)。 + * @note 调试用:该 PGN 无业务接收节点 ACK。auto_retrans=ENABLE 时无 ACK 被 + * 无限重发吞进 TEC(TEC 一直涨);DISABLE 时单发失败即 MTEn→error_count++。 + * 电机拓扑下无对端应答,默认关以消除持续错误帧。需要回放测试波形时打开。 + */ +#define CONFIG_CAN_PERIODIC_TEST_TX_ENABLE 0 +/** @brief CAN0/1/2 通道使能。CH1/CH2 未接设备,关闭避免持续 CAN 错误 + 占 SPI 带宽。 */ constexpr bool CONFIG_CAN_CH0_ENABLE = true; -constexpr bool CONFIG_CAN_CH1_ENABLE = true; -constexpr bool CONFIG_CAN_CH2_ENABLE = true; +constexpr bool CONFIG_CAN_CH1_ENABLE = false; +constexpr bool CONFIG_CAN_CH2_ENABLE = false; /** @brief 主循环 WFI(main.cpp #if)。 */ #define CONFIG_MAIN_LOOP_WFI_ENABLE 0 /** @brief EtherCAT SYNC0 同步模式(#if)。 */ @@ -48,13 +79,13 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true; * APP 固件版本号 * ============================================================ */ constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; -constexpr uint32_t APP_FW_VERSION_MINOR = 3U; -constexpr uint32_t APP_FW_VERSION_PATCH = 0U; +constexpr uint32_t APP_FW_VERSION_MINOR = 4U; +constexpr uint32_t APP_FW_VERSION_PATCH = 75U; /* 1.4.75:尾段fed>=fw_size旁路水位门控,修复末6包死锁 */ constexpr uint32_t APP_FW_VERSION_BUILD = 0U; /** @brief USART0 默认波特率。 */ -constexpr uint32_t CONFIG_USART0_BAUDRATE = 115200U; +constexpr uint32_t CONFIG_USART0_BAUDRATE = 3000000U; /* 3M:诊断打印高速输出,printf 阻塞极小、不拖慢 relay */ /** @brief ADC 外设实例。 */ constexpr uint32_t CONFIG_ADC_INSTANCE = ADC0; @@ -100,8 +131,13 @@ constexpr uint16_t CONFIG_SPI_RX_BUF_SIZE = 256U; constexpr uint16_t CONFIG_SPI_LOG_BUF_SIZE = 256U; /** @brief 主动按铃超时:ESP32 未在该时间内来读则 cancelRing(ms)。 */ constexpr uint32_t CONFIG_SPI_RING_TIMEOUT_MS = 50U; -/** @brief SPI 命令 handler 注册表容量。 */ -constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 8U; +/** @brief SPI 命令 handler 注册表容量。 + * + * 当前 init 注册 9 个:boot_request / ota_wait / ping / can_bridge / + * can_feedback / id_snapshot / adc_feedback / motor_state / motor_ota。 + * registerHandler 表满时静默丢弃,motor_ota 曾因容量=8 被丢弃(0xD0 回 0xEE)。 + * 取 12 留余量;新增 handler 时须确认本容量足够。 */ +constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 12U; /** @brief 每通道不重复 ID 快照表最大条目数(与 ESP32 CANFD_ID_SNAPSHOT_MAX 对齐)。 */ constexpr uint8_t CONFIG_CANFD_ID_SNAPSHOT_MAX = 16U; /** @brief 快照条目最大数据字节数(CAN FD DLC 上限)。 */ @@ -176,10 +212,15 @@ constexpr uint32_t CONFIG_TASK_PERIOD_VREF_SAMPLE_TICKS = 5U; constexpr uint32_t CONFIG_TASK_PERIOD_TEMP_SAMPLE_TICKS = 10U; /** @brief 调度基准节拍:10ms 计数周期,单位 100us tick。 */ constexpr uint32_t CONFIG_TASK_PERIOD_CAN_RX_TICKS = 100U; -/** @brief 调度基准节拍:CAN 周期发送任务周期,10 tick = 1ms。 */ -constexpr uint32_t CONFIG_TASK_PERIOD_CAN_TX_TICKS = 10U; +/** @brief 调度基准节拍:CAN 周期发送任务周期,100 tick = 10ms。 + * @note 原 1ms/帧过密,改为 10ms/帧以降低 CAN 总线负载与 GD32 邮箱压力。 */ +constexpr uint32_t CONFIG_TASK_PERIOD_CAN_TX_TICKS = 100U; /** @brief 调度基准节拍:心跳周期,单位 100us tick。 */ constexpr uint32_t CONFIG_TASK_PERIOD_HEARTBEAT_TICKS = 10000U; +/** @brief 调度基准节拍:多电机状态 0xC5 主动推送启动周期,2500 tick = 250ms。 + * @note 独立于 1Hz heartbeat,避免连带加速 OTA Boot 轮询 / CAN 反馈快照刷新 / + * ADC 推送 / checkOffline 离线判定(3s 阈值)。前端 age 应稳定在 ~120ms。 */ +constexpr uint32_t CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS = 2500U; /** @brief 调度基准节拍:APP 状态/诊断打印周期,50000 tick = 5s。 */ constexpr uint32_t CONFIG_TASK_PERIOD_DIAG_PRINT_TICKS = 50000U; @@ -225,8 +266,9 @@ constexpr uint32_t CONFIG_ESC_PDI_OUTPUT_OFFSET = 0x1200U; constexpr size_t CONFIG_ETHERCAT_PDO_SIZE = 32U; /* ---------- EXMC 引脚配置(GD32E508 EXMC 标准映射) ---------- */ -constexpr uint32_t CONFIG_ESC_EXMC_BASE_ADDR = 0x60000000U; -/* Region1 = NE1 @ 0x60000000;数值与 gd32e50x_exmc.h 中 EXMC_BANK0_NORSRAM_REGION1 一致 */ +constexpr uint32_t CONFIG_ESC_EXMC_BASE_ADDR = 0x64000000U; +/* Region1 = NE1 @ 0x64000000(GD32E50x 用户手册 Rev2.2 图25-3: Bank0 四 Region 各 64MB, + * HADDR[27:26]=01 选中 NE1)。LAN9252 CS 接 PD7=EXMC_NE1,故基址为 0x64000000。 */ #ifndef EXMC_BANK0_NORSRAM_REGION1 #define EXMC_BANK0_NORSRAM_REGION1 ((uint32_t)0x00000001U) #endif diff --git a/GD32E508_DCU_APP/sys/motor_ota_relay.cpp b/GD32E508_DCU_APP/sys/motor_ota_relay.cpp new file mode 100644 index 0000000..a24c69d --- /dev/null +++ b/GD32E508_DCU_APP/sys/motor_ota_relay.cpp @@ -0,0 +1,1055 @@ +/** + * @file motor_ota_relay.cpp + * @brief 电机 OTA relay 状态机实现。 + */ +#include "sys/motor_ota_relay.h" +#include "sys/aes128.h" +#include "sys/config.h" +#include "sys/systick.h" +#include "sys/task_manager.h" +#include "main.h" +#include "device/canfd.h" +#include "device/spi/app_spi_command.h" +#include "device/spi/app_spi_command_defs.h" +#include +#include + +namespace MotorOtaRelay { + +/* ═══════════════════ 内部常量 ═══════════════════ */ + +/* 与电机 BOOT 工程一致的 K_master(预置主密钥) */ +static const uint8_t K_MASTER[16] = { + 0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF, + 0xFE,0xDC,0xBA,0x98,0x76,0x54,0x32,0x10 +}; + +/* 固件环形缓冲:ESP32 喂入明文,状态机消耗加密后发 CAN。 + * 2KB≈42 包:CAN 5ms/包时约 210ms 余量;ESP32 SPI 欠载时会出现 ~500ms CAN 空洞。 + * 提到 4KB≈85 包(~420ms)给 WiFi/HTTP 抖动更大缓冲。 */ +static constexpr uint16_t kBufSize = 4096U; + +/* ═══════════════════ 内部状态 ═══════════════════ */ + +static State s_state = State::kIdle; +static uint8_t s_error = 0; +static uint8_t s_channel = 0; +static uint32_t s_fw_size = 0; + static uint32_t s_fw_fed = 0; /* 已接受的固件字节数(绝对流位置,防 0xD1 重传重复追加) */ + static uint16_t s_total_packets = 0; + static uint16_t s_packets_acked = 0; +static uint32_t s_session_num = 0; +static bool s_boot_heartbeat_seen = false; /* kPreCancel 期间是否收到 0x103 心跳 */ + +/* AES */ +static aes128_context s_aes_ctx; +static uint8_t s_k_session[16]; +static uint8_t s_nonce[16]; + +/* ACK 捕获(由 onCanFrame 在 taskCanRxParse 上下文写入) */ +static volatile bool s_ack_pending = false; +static uint8_t s_ack_data[64]; +static uint8_t s_ack_len = 0; + +/* 超时 / 发送节奏 */ +static uint32_t s_state_enter_ms = 0; +/* 上次发送时刻;0 = 当前步骤尚未首发(enterState 置 0,process() 负责首发)。 */ +static uint32_t s_last_send_ms = 0; +/* 当前步骤已补发次数(进入新步骤 / 弹出新数据包时清零)。 */ +static uint8_t s_retry_count = 0; +/* CRC 错误后待重发标志:置位后由 onTxTick() 按 kTransferGapMs 间隔重发当前包(不立即重发)。 */ +static bool s_crc_resend_pending = false; +/* 数据发送节奏完全由主循环 onTxTick() + SysTick 钟表(s_last_send_ms)驱动, + * 不再用跨 ISR/主循环的计数器(消除 s_gap_div_100us 竞争)。 */ + +/* 固件环形缓冲 */ +static uint8_t s_buf[kBufSize]; +static volatile uint16_t s_buf_head = 0; /* 写入位置(ESP32 feed) */ +static volatile uint16_t s_buf_tail = 0; /* 读取位置(状态机消耗) */ + +/* 当前待发包的明文暂存 + "当前包是否已从缓冲弹出"标志。 + * CRC 错误时 BOOT 要求重发同一包,不能再 bufPop 新数据。 + * s_pkt_pending=false 表示当前没有待发包数据(需弹新包); + * s_pkt_pending=true 表示 s_pkt_plain 已含待发数据,可重发。 */ +static uint8_t s_pkt_plain[kPacketPayload]; +static bool s_pkt_pending = false; +/* 末包 padding 诊断:sendNextPacket 弹末包时记录有效字节数(0xFFFF=非末包/已打印), + * 供 sendCurrentPacket 在 TX 日志前一次性打印,避免 padding 日志被紧接的 TX 行吞掉。 */ +static uint16_t s_last_pad_valid = 0xFFFFU; + +/* ═══════════════════ 工具函数 ═══════════════════ */ + +static inline uint16_t bufUsed() { + return static_cast((s_buf_head - s_buf_tail + kBufSize) % kBufSize); +} +static inline uint16_t bufFree() { + return static_cast(kBufSize - 1U - bufUsed()); +} + +static void bufPush(const uint8_t *data, uint16_t len) { + for (uint16_t i = 0; i < len; i++) { + s_buf[s_buf_head] = data[i]; + s_buf_head = static_cast((s_buf_head + 1U) % kBufSize); + } +} + +static bool bufPop(uint8_t *out, uint16_t len) { + if (bufUsed() < len) return false; + for (uint16_t i = 0; i < len; i++) { + out[i] = s_buf[s_buf_tail]; + s_buf_tail = static_cast((s_buf_tail + 1U) % kBufSize); + } + return true; +} + +/* 简单伪随机 nonce(SysTick + 未初始化 ADC 噪声足够区分会话) */ +static void generateNonce(uint8_t *out) { + uint32_t seed = SysTickTimer::getMs(); + for (uint8_t i = 0; i < 16; i++) { + seed = seed * 1103515245U + 12345U; + out[i] = static_cast((seed >> 16) & 0xFF); + } +} + +/* keyed-CRC32:与电机 BOOT calculate_crc32_with_key 查表实现完全一致。 + * ★ 必须用查表法:BOOT 侧 (crc ^ key[i]) & 0xFF(8位 XOR 再查表), + * bit-by-bit 用 crc ^= key[i](32位 XOR),两者不等价,会导致每包 CRC 校验失败。 */ +/* CRC32 查找表(标准多项式 0xEDB88320)。 */ +static const uint32_t kCrcTable[256] = { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, + 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, + 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, + 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, + 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, + 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, + 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, + 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, + 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, + 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, + 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, + 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, + 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, + 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, + 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, + 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D +}; + +static void keyedCrc32(const uint8_t *key, const uint8_t *data, uint8_t *output) { + uint32_t crc = 0xFFFFFFFFU; + + /* 16 字节会话密钥先喂入(与 BOOT 完全一致:查表法) */ + for (int i = 0; i < 16; ++i) { + crc = kCrcTable[(crc ^ key[i]) & 0xFFU] ^ (crc >> 8); + } + + /* 48 字节有效数据再喂入 */ + for (int i = 0; i < (int)kPacketPayload; ++i) { + crc = kCrcTable[(crc ^ data[i]) & 0xFFU] ^ (crc >> 8); + } + + /* 取反得到最终结果 */ + crc = ~crc; + + /* 小端格式输出 */ + output[0] = static_cast(crc & 0xFFU); + output[1] = static_cast((crc >> 8) & 0xFFU); + output[2] = static_cast((crc >> 16) & 0xFFU); + output[3] = static_cast((crc >> 24) & 0xFFU); +} + +/* ═══════════════════ CAN 发送辅助 ═══════════════════ */ + +/* OTA 帧发送:periodic 控制是否允许硬件自动重传。 + * 所有帧统一用 periodic=false:发一次即止,不靠 CAN 硬件重传。 + * - 握手/控制帧:重传由 relay 软件 process() 按 kStepTimeoutMs 超时补发。 + * - 数据帧:靠 relay 软件 stop-and-wait(onTxTick)补发。 + * 否则电机跳 BOOT 期间 CAN 短暂断开时硬件持续重传,恢复后重复帧到达 BOOT + * 被判为非法握手 → INVALID_HANDSHAKE(0x05),或数据帧重复 → CRC 错误(0x06)。 */ +static bool canSend(uint32_t id, const uint8_t *data, uint8_t dlc, bool periodic = false) { + /* 诊断:kTransfer 期间只应发 0x101,若出现 0x100 则为幽灵帧 */ + if (s_state == State::kTransfer && id == kOtaReqId) { + printf("[MOTOR-OTA] BUG! canSend(0x100) in kTransfer! dlc=%u\r\n", (unsigned)dlc); + } + const CanFd::CanTxResult r = CanFd::transmitFrame(s_channel, id, data, dlc, periodic); + return (r == CanFd::CanTxResult::kOk); +} + +static void enterState(State st) { + const State prev = s_state; + s_state = st; + s_state_enter_ms = SysTickTimer::getMs(); + /* 进入新步骤:清补发计数;s_last_send_ms=0 令 process() 先“首发”再进入等待/补发节奏。 */ + s_retry_count = 0; + s_last_send_ms = 0; + s_crc_resend_pending = false; + + /* auto_retrans 切换:OTA 是应用层停等协议,硬件自动重发会打穿电机单缓冲 RX + * (重发的前一帧被当成新帧处理 → receive_packet_num 错位 → CRC 错)。 + * 进入 kHandshakeReq(start() 唯一入口)即关 ARD 切单发; + * 回到 kIdle/kFinished/kError 即恢复 ARD,避免异常路径漏恢复导致全局卡在单发。 + * 注:setChannelAutoRetrans 内部按 init-mode 改 ARD 位,开销在几十 µs 量级。 */ + const bool otaActive = (st == State::kPreCancel || st == State::kHandshakeReq + || st == State::kHandshakeNonce || st == State::kHandshakeConfirm + || st == State::kErase || st == State::kTransfer + || st == State::kFinishOta); + const bool wasActive = (prev == State::kPreCancel || prev == State::kHandshakeReq + || prev == State::kHandshakeNonce || prev == State::kHandshakeConfirm + || prev == State::kErase || prev == State::kTransfer + || prev == State::kFinishOta); + if (otaActive && !wasActive) { + CanFd::setChannelAutoRetrans(s_channel, false); /* OTA 开始:单发 */ + } else if (!otaActive && wasActive) { + CanFd::setChannelAutoRetrans(s_channel, true); /* OTA 结束:恢复重发 */ + } +} + +static bool stateTimedOut(uint32_t timeout_ms) { + return (SysTickTimer::getMs() - s_state_enter_ms) >= timeout_ms; +} + +static void pushStatus(); /* 前置声明:setError 里要用,定义在下方 */ + +static void setError(uint8_t code) { + s_error = code; + enterState(State::kError); + /* 任何失败都立即把 kError 推给 ESP32:否则 ESP32 feedTask 看不到错误态, + * 会一直喂到 120s 超时才退出。"失败了就报错退出"要求即时上报。 */ + pushStatus(); +} + +/* 进度推送给 ESP32(0xD2 ring push)。 + * 传输期 ESP32 suppress ring:push 只会占 PB6/m_txBuf 50ms,干扰 0xD1 喂数。 + * kTransfer 内禁止 push;终态/错误/握手阶段仍推。 */ + static void pushStatus() { + if (s_state == State::kTransfer) { + return; + } + uint8_t payload[12]; + payload[0] = static_cast(s_state); + payload[1] = s_error; + payload[2] = static_cast(s_packets_acked & 0xFF); + payload[3] = static_cast(s_packets_acked >> 8); + payload[4] = static_cast(s_total_packets & 0xFF); + payload[5] = static_cast(s_total_packets >> 8); + payload[6] = static_cast(s_fw_size & 0xFF); + payload[7] = static_cast((s_fw_size >> 8) & 0xFF); + payload[8] = static_cast((s_fw_size >> 16) & 0xFF); + payload[9] = static_cast((s_fw_size >> 24) & 0xFF); + payload[10] = static_cast(bufUsed() & 0xFF); + payload[11] = static_cast(bufUsed() >> 8); + AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeMotorOtaStatus, payload, 12); + } + +/* ═══════════════════ 握手子步骤 ═══════════════════ */ + +/* 单步等对应反馈的超时:到点仍无反馈就补发。100ms 加快握手节奏(电机 BOOT 主循环~10ms 足够响应)。 */ +static constexpr uint32_t kStepTimeoutMs = 100U; +/* 补发次数上限:首发后最多再补发 kMaxResends 次;仍无反馈则判该步失败并打印。 */ +static constexpr uint8_t kMaxResends = 2U; +/* 数据传输每包间隔(ms):正式发固件时每包也留间隔,防电机单缓冲 RX 高速下丢包/CRC 错。 + * 现行:5ms/包(用户指定)。SPI 喂数必须能跟上;跟不上时靠水位门控暂停 CAN。 */ + static constexpr uint32_t kTransferGapMs = 5U; + /* 开传前环缓最低水位:至少约 1KB(~21 包 / ~100ms)再发首包, + * 避免擦除后立刻按 5ms 抽干仅有的 232B 首 chunk 导致电机超时。 */ + static constexpr uint16_t kStartWaterBytes = 1024U; + /* 运行中低水位:低于此值暂停发新包,等 ESP32 回填(已在途包仍等 ACK)。 */ + static constexpr uint16_t kRunWaterBytes = 256U; +/* 擦除后首包等待(ms):电机发 erase ACK = 擦除已完成, + * 100ms 仅为电机内部状态转换留余量(BOOT 主循环~10ms)。 + * 之前 500ms 导致电机 RECEIVING_FIRMWARE 超时(~2s)预算被压缩。 */ +static constexpr uint32_t kEraseWaitMs = 100U; +/* 预取消等待(ms):发 CANCEL 后等电机 BOOT 主循环处理完(复位到 IDLE),再进握手。 + * 电机 BOOT 主循环周期 ~1-10ms,100ms 留足余量。 */ +static constexpr uint32_t kBootSettleMs = 100U; /* 检测到0x103心跳后再等这么久,让电机APP残留[00FA]CAN帧先消耗完 + * 原 2000ms→800ms→100ms:0x103 出现即电机已在 BOOT IDLE, + * 残帧实测 <4 帧(~40ms),100ms 足够。 */ + +/* 步骤名,用于“xxx 未收到”日志,让用户/上位机知道卡在协议哪一步。 */ +static const char *stepName(State st) { + switch (st) { + case State::kPreCancel: return "WAIT_BOOT(0x103)"; + case State::kHandshakeReq: return "HANDSHAKE_REQ(0x100:00AC)"; + case State::kHandshakeNonce: return "HANDSHAKE_NONCE(0x100:16B)"; + case State::kHandshakeConfirm: return "HANDSHAKE_CONFIRM(0x100:16B)"; + case State::kErase: return "ERASE(0x101:fw_size)"; + case State::kTransfer: return "TRANSFER(0x101:data)"; + case State::kFinishOta: return "FINISH_OTA(0x100:0AAC)"; + default: return "?"; + } +} + +static void sendNonce() { + /* 生成 nonce 并切状态;首发由 ACK handler 触发(不走 process 首发路径)。 */ + generateNonce(s_nonce); + enterState(State::kHandshakeNonce); +} + +/* 统一重发当前步骤的帧(超时重发 + kHandshakeReq 首发) */ +static void sendCurrentStep(uint32_t now_ms) { + printf("[MOTOR-OTA] TX %s try=%u t=%lu\r\n", + stepName(s_state), (unsigned)s_retry_count, (unsigned long)now_ms); + switch (s_state) { + case State::kHandshakeReq: { + const uint8_t req[2] = {0x00, 0xAC}; + canSend(kOtaReqId, req, 2); + break; + } + case State::kHandshakeNonce: + canSend(kOtaReqId, s_nonce, 16); + break; + case State::kHandshakeConfirm: { + uint8_t plain[16]; + for (uint8_t i = 0; i < 16; i++) plain[i] = s_nonce[i] ^ 0x55; + uint8_t confirm[16]; + aes128_set_key(&s_aes_ctx, s_k_session); + aes128_encrypt_block(&s_aes_ctx, plain, confirm); + canSend(kOtaReqId, confirm, 16); + break; + } + case State::kErase: { + uint8_t size_le[4]; + size_le[0] = static_cast(s_fw_size & 0xFF); + size_le[1] = static_cast((s_fw_size >> 8) & 0xFF); + size_le[2] = static_cast((s_fw_size >> 16) & 0xFF); + size_le[3] = static_cast((s_fw_size >> 24) & 0xFF); + canSend(kOtaDataId, size_le, 4); + break; + } + case State::kFinishOta: { + const uint8_t finish_cmd[2] = {0x0A, 0xAC}; + canSend(kOtaReqId, finish_cmd, 2); + break; + } + default: + break; + } + s_last_send_ms = now_ms; +} + +/* ═══════════════════ 数据传输 ═══════════════════ */ + +/* 将 s_pkt_plain 组装成 64B CAN 帧并发送。 + * 不弹缓冲,只做加密+CRC+组帧+CAN 发送。 + * 供 sendNextPacket(弹新包后发)和 CRC 重发(发同一包)共用。 */ +static void sendCurrentPacket() { + const uint32_t pkt_num = s_packets_acked; + const bool is_last = (pkt_num + 1U >= s_total_packets); + + /* AES-ECB 加密 48B */ + uint8_t encrypted[kPacketPayload]; + aes128_set_key(&s_aes_ctx, s_k_session); + aes128_ecb_encrypt_48bytes(&s_aes_ctx, s_pkt_plain, encrypted); + + /* keyed CRC32 */ + uint8_t crc[4]; + keyedCrc32(s_k_session, s_pkt_plain, crc); + + /* 组装 64B 帧: session(4) + pkt_num(4) + encrypted(48) + IFEND(4) + CRC(4) + * 帧布局对齐 BOOT 工程 OTA.cpp: + * data[0-3] session, [4-7] pkt_num, [8-55] encrypted(48B), + * [56-59] IFEND (extract_u32_from_4bytes(data+56)), + * [60-63] CRC (data+60) */ + uint8_t frame[kFrameDlc]; + memset(frame, 0, sizeof(frame)); + /* session_num LE */ + frame[0] = static_cast(s_session_num & 0xFF); + frame[1] = static_cast((s_session_num >> 8) & 0xFF); + frame[2] = static_cast((s_session_num >> 16) & 0xFF); + frame[3] = static_cast((s_session_num >> 24) & 0xFF); + /* packet_num LE */ + frame[4] = static_cast(pkt_num & 0xFF); + frame[5] = static_cast((pkt_num >> 8) & 0xFF); + frame[6] = static_cast((pkt_num >> 16) & 0xFF); + frame[7] = static_cast((pkt_num >> 24) & 0xFF); + /* encrypted payload */ + memcpy(&frame[8], encrypted, kPacketPayload); + /* IFEND @56 (BOOT 在 data+56 读 IFEND) */ + if (is_last) { + frame[56] = 0xFF; frame[57] = 0xFF; frame[58] = 0xFF; frame[59] = 0xFF; + } + /* CRC @60 (BOOT 在 data+60 读 CRC) */ + memcpy(&frame[60], crc, 4); + + s_ack_pending = false; + /* periodic=false:数据帧只发一次,不靠 CAN 硬件自动重传。 + * 硬件重传会在 10ms 间隔下持续重发同一包,电机 BOOT 处理期间重复收到 + * 同一包 → 时序错乱 → CRC 错误(06)/握手错误(05)。 + * 超时重发由 onTxTick()(CRC 错误置 s_crc_resend_pending)负责。 */ + /* 末包 padding 日志在 TX 前打印(sendNextPacket 设 s_last_pad_valid)。 + * 用最短格式 + 一次性打印(打印后置 0xFFFF 防重发重复打印)。 */ + if (s_last_pad_valid != 0xFFFFU) { + printf("[MOTOR-OTA] PAD v=%u p=%u\r\n", + (unsigned)s_last_pad_valid, + (unsigned)(kPacketPayload - s_last_pad_valid)); + s_last_pad_valid = 0xFFFFU; + } + /* 逐包 TX 日志在 5ms 节奏下会拖主循环;仅末包/每 64 包打一行。 */ + if (is_last || ((pkt_num & 0x3FU) == 0U)) { + printf("[MOTOR-OTA] TX pkt=%lu/%u%s t=%lu\r\n", + (unsigned long)pkt_num, (unsigned)s_total_packets, + is_last ? " LAST" : "", (unsigned long)SysTickTimer::getMs()); + } + canSend(kOtaDataId, frame, kFrameDlc, /*periodic=*/false); +} + +static void sendNextPacket() { + /* 从缓冲取明文到 s_pkt_plain */ + const uint16_t used = bufUsed(); + const bool is_last = (s_packets_acked + 1U >= s_total_packets); + /* 末包真实有效字节:fw_size % 48;整除时末包也是满 48B。 */ + const uint16_t last_valid = static_cast( + (s_fw_size % kPacketPayload) == 0U + ? kPacketPayload + : (s_fw_size % kPacketPayload)); + + if (is_last) { + /* ★ 末包无论 used 是否 ≥48,都只取 last_valid 再 0xFF pad。 + * 旧逻辑 used≥48 时 bufPop(48) 会把环缓里多喂的重复字节一并发出 + * (BLF:末包 padding 变成向量表而非 0xFF)。 */ + if (used < last_valid) { + static uint32_t s_last_tail_ms = 0; + const uint32_t now = SysTickTimer::getMs(); + if (now - s_last_tail_ms >= 200U) { + printf("[MOTOR-OTA] 末包欠载,等尾字节 pkt=%u used=%u need=%u fed=%lu t=%lu\r\n", + (unsigned)s_packets_acked, (unsigned)used, + (unsigned)last_valid, (unsigned long)s_fw_fed, + (unsigned long)now); + s_last_tail_ms = now; + } + return; + } + for (uint16_t i = 0U; i < last_valid; ++i) { + s_pkt_plain[i] = s_buf[s_buf_tail]; + s_buf_tail = static_cast((s_buf_tail + 1U) % kBufSize); + } + for (uint16_t i = last_valid; i < kPacketPayload; ++i) { + s_pkt_plain[i] = 0xFFU; + } + s_last_pad_valid = last_valid; + } else { + if (used < kPacketPayload) { + static uint32_t s_last_empty_ms = 0; + const uint32_t now = SysTickTimer::getMs(); + if (now - s_last_empty_ms >= 200U) { + printf("[MOTOR-OTA] 缓冲空,等ESP32喂 pkt=%u used=%u fed=%lu/%lu t=%lu\r\n", + (unsigned)s_packets_acked, (unsigned)used, + (unsigned long)s_fw_fed, (unsigned long)s_fw_size, + (unsigned long)now); + s_last_empty_ms = now; + } + return; + } + if (!bufPop(s_pkt_plain, kPacketPayload)) { + return; + } + s_last_pad_valid = 0xFFFFU; + } + s_pkt_pending = true; + s_retry_count = 0; /* 新数据包:重置补发计数 */ + s_state_enter_ms = SysTickTimer::getMs(); /* 新数据包:重置整体超时 backstop */ + /* 末包 padding 日志在 sendCurrentPacket 的 TX 行之前打印,且用最短格式, + * 避免 10ms 高频 TX 期间被 UART 吞掉(之前放在这里会被紧接的 TX pkt= 行挤掉)。 */ + sendCurrentPacket(); + s_last_send_ms = SysTickTimer::getMs(); +} + +/* BOOT 错误码(与 OTA.h ota_error_t 对齐,仅列出实际处理的) */ +static constexpr uint8_t kBootErrHandshake = 0x05; /* INVALID_HANDSHAKE:直接中止(不重握手) */ +static constexpr uint8_t kBootErrCrc = 0x06; /* CRC 错误:重发同一包 */ + +/* CRC 错误重试上限:BOOT 对 CRC 失败的包做 receive_packet_num -= 1 并 + * 发 1 字节 error frame(0x06),期望上位机重发同一包。超过此限判定为 + * 真实数据损坏,报错中止,避免无限循环。 */ +static constexpr uint8_t kMaxCrcRetries = 3; +static uint8_t s_crc_retry_count = 0; +static uint16_t s_last_acked_pkt = 0xFFFFU; /* 上次确认的包号,用于识别重复 ACK */ +static uint32_t s_dup_ack_count = 0; /* 累计忽略的重复/未前进 ACK 帧数(诊断重传风暴) */ +static uint32_t s_crc_err_count = 0; /* 累计收到的 0x06 CRC 错误帧数(含电机重传) */ + +static void processTransferAck(const uint8_t *data, uint8_t len) { + /* 擦除等待窗口(首包尚未发出:s_packets_acked==0 且 !s_pkt_pending):电机重传的旧 + * [BA C0] / 杂散帧一律忽略,绝不能推进 s_packets_acked——否则 onTxTick 的 + * need=(s_packets_acked==0?kEraseWaitMs:kTransferGapMs) 会提前用 30ms 抢跑首包, + * 且首包被错标成非 0 号(实测首包变 pkt=5) → 从一开始就包号/数据错位,每包 0x06。 */ + if (s_packets_acked == 0U && !s_pkt_pending) { + s_dup_ack_count++; + return; + } + /* BOOT 错误帧:1 字节 [error_code],发到 0x102 ID。 + * CRC 错误(0x06):BOOT 做 receive_packet_num -= 1,期望重发同一包, + * 不改变 ota_state,仍处于 RECEIVING_FIRMWARE。 + * 其他错误码:BOOT 已进 IDLE,不可恢复。 */ + if (len == 1) { + if (data[0] == kBootErrCrc) { + s_crc_err_count++; + /* 去重:已排定重发时,电机 auto_retrans 重传的重复 0x06 不再计数, + * 避免一次 CRC 错的多份重传帧把重试计数提前耗尽 → 假性 0x23。 */ + if (s_crc_resend_pending) { + return; + } + if (s_crc_retry_count < kMaxCrcRetries) { + s_crc_retry_count++; + /* 不立即重发:置标志,由 onTxTick() 按 kTransferGapMs 间隔重发当前包, + * 避免"挨太近→又 CRC 错"死循环。s_pkt_plain 仍含上次明文,s_pkt_pending 保持。 */ + s_crc_resend_pending = true; + return; + } + printf("[MOTOR-OTA] CRC 重试耗尽 pkt=%u/%u dup=%lu crc=%lu\r\n", + (unsigned)s_packets_acked, (unsigned)s_total_packets, + (unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count); + setError(0x23); /* CRC 重试耗尽 */ + return; + } + if (data[0] == kBootErrHandshake) { + /* INVALID_HANDSHAKE(0x05):电机 BOOT 已退回 IDLE。按用户要求不再做 + * 「发 CANCEL + 从包 0 重握手」的自动重试(会白扔进度、坏总线下反复失败), + * 直接中止,由上位机决定是否整体重刷。 */ + printf("[MOTOR-OTA] HANDSHAKE 错误(0x05),中止(不重握手) pkt=%u/%u\r\n", + (unsigned)s_packets_acked, (unsigned)s_total_packets); + setError(0x05); + return; + } + /* 其他错误码:BOOT 已中止,relay 也报错 */ + setError(0x24); /* BOOT 报致命错误 */ + return; + } + + if (len < 6) { setError(0x21); return; } + /* ACK: [0xBA, 0xC0, next_pkt(4B LE)] */ + if (data[0] != 0xBA || data[1] != 0xC0) { setError(0x22); return; } + + const uint32_t next_pkt = static_cast(data[2]) + | (static_cast(data[3]) << 8) + | (static_cast(data[4]) << 16) + | (static_cast(data[5]) << 24); + + /* BOOT 协议:最后一包(IFEND=0xFFFFFFFF)后,BOOT 进 FINISHED 并回 + * ACK(0xF0F0F0F0) 表示"全部写入完成,等 FINISH_OTA"。 + * 上位机须再发 [0x0A, 0xAC] 到 REQ ID,BOOT 才真正跳转 APP。 + * 旧实现误将 0xF0F0F0F0 当普通包号处理,导致 relay 永远等不到 0xFFFFFFFF, + * 既不进 kFinished 也不发 FINISH_OTA,BOOT 卡在 FINISHED 状态不跳转。 */ + /* BOOT 协议(已对照电机 BOOT OTA.cpp line 430-436 确认): + * 最后一包(IFEND=0xFFFFFFFF)后,BOOT 发 ACK(0xFFFFFFFF) 并立即 jumpToApplication() + * 自行跳转 APP,不需要上位机再发 FINISH_OTA。 + * 旧注释里"0xF0F0F0F0 表示等 FINISH_OTA"是错误假设——电机 BOOT 代码里根本 + * 没有 0xF0F0F0F0,尾帧 ACK 就是 0xFFFFFFFF,发完即跳转。 + * 因此收到 0xFFFFFFFF 即表示电机已跳转 APP,relay 直接进 kFinished。 + * 若再发 [0x0A,0xAC] 反而会被已运行的电机 APP 当作握手开始信号干扰。 */ +if (next_pkt == 0xFFFFFFFFU) { + /* 末包已被电机确认并跳转:把进度钉到 total,便于 QUERY/网页识别 100%。 */ + s_packets_acked = s_total_packets; + s_last_acked_pkt = s_total_packets; + s_pkt_pending = false; + printf("[MOTOR-OTA] FINISHED pkt=%u/%u dup=%lu crc=%lu t=%lu\r\n", + (unsigned)s_packets_acked, (unsigned)s_total_packets, + (unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count, + (unsigned long)SysTickTimer::getMs()); + enterState(State::kFinished); + pushStatus(); + return; + } + + /* 兼容:若未来 BOOT 改为回 0xF0F0F0F0 等待 FINISH_OTA,再发 [0x0A,0xAC]。 + * 当前电机 BOOT 不会走到这里,保留分支以防协议变体。 */ + if (next_pkt == 0xF0F0F0F0U) { + const uint8_t finish_cmd[2] = {0x0A, 0xAC}; + canSend(kOtaReqId, finish_cmd, 2); + s_last_send_ms = SysTickTimer::getMs(); + enterState(State::kFinishOta); + return; + } + + const uint16_t next16 = static_cast(next_pkt); + + /* 重复/未前进 ACK:BOOT auto_retrans=ENABLE,relay 侧 CAN 层没回 ACK 时会重传 + * “同一个 next_pkt”的 ACK 帧。next_pkt 未前进时必须整帧忽略——绝不能清 + * s_pkt_pending:否则会把刚发出、尚未真正确认的在途包误判为已确认,下一次 + * onTxTick 多 bufPop 一包却仍沿用旧包号发送 → 包号/数据错位、CRC 错累积, + * 正是“看着有很多重复发送”的根源。忽略后在途包的真确认(next_pkt 前进)自然到来。 */ + if (s_last_acked_pkt != 0xFFFFU && next16 <= s_last_acked_pkt) { + /* 重复/未前进 ACK:只累计计数,不逐帧打印(量极大会打爆 UART 扰乱时序)。 + * 汇总见每 32 包进度行的 dup=。 */ + s_dup_ack_count++; + return; + } + + s_packets_acked = next16; + s_last_acked_pkt = next16; + s_crc_retry_count = 0; /* 正常前进 ACK,清零 CRC 重试计数 */ + s_crc_resend_pending = false; + s_pkt_pending = false; /* 本包确认;下一包由主循环 onTxTick 按钟表间隔发出 */ + + /* 每 32 包只打串口汇总,不再 pushStatus(0xD2)。 + * 传输期 ESP32 suppress ring,0xD2 读不到还占 spi_ready/ring, + * 与 0xD1 喂数抢总线,加重缓冲欠载(CANlog 可见周期性 ~500ms 空洞)。 + * 进度由 ESP32 feedTask 按已喂字节估算;终态再 push。 */ + if ((s_packets_acked & 0x1F) == 0) { + printf("[MOTOR-OTA] 进度 pkt=%u/%u dup=%lu crc=%lu t=%lu\r\n", + (unsigned)s_packets_acked, (unsigned)s_total_packets, + (unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count, + (unsigned long)SysTickTimer::getMs()); + } +} + +/* ═══════════════════ 公开接口 ═══════════════════ */ + +void init() { + /* 无额外硬件初始化;CAN 已由 CanFd 模块管理 */ +} + +/* 数据发送推进:由主循环 TaskRunner 每圈调用(不再走 ISR 计时 + TASK 标志)。 + * 全部间隔判定用 SysTick 钟表(s_last_send_ms),与任务调度器的定时器分频解耦, + * 从根上消除 s_gap_div_100us 被 ISR(++)与主循环(=0)同时读写的竞争。 + * 首包等 BOOT 擦除完成(kEraseWaitMs,从进 kTransfer 起算),后续包按 kTransferGapMs。 */ +void onTxTick() { + if (s_state != State::kTransfer) return; + + const uint32_t now = SysTickTimer::getMs(); + + /* ACK 超时重发:包已发出(s_pkt_pending=true)但超过 500ms 未收到 ACK,重发。 + * 场景:CAN 总线 ERR_PASSIVE(REC>128) 导致电机 ACK 丢失, + * GD32 等 ACK、电机等下一包 → 双方死锁。 + * ★ 注意:检查 s_pkt_pending(包已发等ACK),不是 s_ack_pending(ACK已到等处理)! + * 重发后电机幂等处理(同包号只 ACK 不重复写 flash)。 */ + if (s_pkt_pending && !s_ack_pending && !s_crc_resend_pending) { + if (now - s_last_send_ms >= 500U) { + if (s_retry_count < kMaxResends) { + s_retry_count++; + printf("[MOTOR-OTA] ACK 超时重发 pkt=%u retry=%u\r\n", + (unsigned)s_packets_acked, (unsigned)s_retry_count); + sendCurrentPacket(); + s_last_send_ms = SysTickTimer::getMs(); + } + /* 超过最大重试次数后由 process() 的整体超时处理 */ + } + return; + } + + /* CRC 错误重发:发同一包(s_pkt_plain 不变、不 bufPop),按 kTransferGapMs 节流; + * 优先于 s_pkt_pending 门,否则忽略重复 ACK 后 s_pkt_pending 长期为真会堵死重发。 */ + if (s_crc_resend_pending) { + if (now - s_last_send_ms < kTransferGapMs) return; + s_crc_resend_pending = false; + sendCurrentPacket(); + s_last_send_ms = SysTickTimer::getMs(); + return; + } + +if (s_pkt_pending) return; /* 当前包待确认,不发新包 */ + + /* 首包(s_packets_acked==0)等 kEraseWaitMs 让 BOOT 擦完,后续包按 kTransferGapMs。 */ + const uint32_t need = (s_packets_acked == 0U) ? kEraseWaitMs : kTransferGapMs; + if (now - s_last_send_ms < need) return; + + /* 水位门控:SPI 喂数跟不上时暂停 CAN,避免抽干环缓后电机 1s 超时。 + * 首包要求更高水位(开传缓冲);运行中维持低水位即可。 */ + const uint16_t water = (s_packets_acked == 0U) ? kStartWaterBytes : kRunWaterBytes; + const uint16_t used = bufUsed(); + /* 末包:只剩 last_valid 字节时放行 */ + const bool is_last = (s_packets_acked + 1U >= s_total_packets); + const uint16_t last_need = static_cast( + (s_fw_size % kPacketPayload) == 0U ? kPacketPayload + : (s_fw_size % kPacketPayload)); + /* 尾段:SPI 已喂完全部固件(fed>=fw_size)时缓冲里就是剩余全部数据, + * 剩余字节可能 < water(如 248<256),必须旁路水位门控,否则尾段死锁 + * → 电机 BOOT 1s 无包超时。 */ + const bool all_fed = (s_fw_fed >= s_fw_size); + if (!is_last && !all_fed && used < water) { + static uint32_t s_last_water_ms = 0; + if (now - s_last_water_ms >= 200U) { + printf("[MOTOR-OTA] 等SPI水位 used=%u need=%u fed=%lu pkt=%u\r\n", + (unsigned)used, (unsigned)water, + (unsigned long)s_fw_fed, (unsigned)s_packets_acked); + s_last_water_ms = now; + } + return; + } + if (is_last && used < last_need) { + return; /* sendNextPacket 内也会等,这里省一次调用 */ + } + sendNextPacket(); /* 内部设 s_last_send_ms */ + } + +void process() { + if (s_state == State::kIdle || s_state == State::kFinished || s_state == State::kError) { + return; + } + + /* kPreCancel:等待电机 BOOT 心跳(0x103)确认已进入 Bootloader。 + * 电机 BOOT IDLE 每 ~10ms 发一次 0x103;收到即证明电机已在 BOOT IDLE。 + * 但不立即握手——电机 APP 跳 BOOT 前可能在 CAN TX 队列残留 [00 FA](fail)帧, + * 这帧延迟发到总线会被 BOOT 的 handleLinkMessage catch-all 命中 + * → 重置状态机 → 0x05。等 kBootSettleMs 让残帧在 BOOT IDLE 期间消耗完, + * 再开始握手就不会被干扰。 */ + if (s_state == State::kPreCancel) { + if (s_boot_heartbeat_seen && stateTimedOut(kBootSettleMs)) { + printf("[MOTOR-OTA] 0x103 心跳确认 + %ums 残帧窗口已过,开始握手\r\n", + (unsigned)kBootSettleMs); + enterState(State::kHandshakeReq); + pushStatus(); + } else if (stateTimedOut(kHandshakeTimeoutMs)) { + printf("[MOTOR-OTA] 等待 0x103 超时,电机未进入 BOOT\r\n"); + setError(0x02); /* 0x02 = 电机未进入 BOOT */ + } + return; + } + + /* 超时检查 */ + const uint32_t timeout = (s_state == State::kTransfer || s_state == State::kFinishOta) + ? kAckTimeoutMs : kHandshakeTimeoutMs; + if (stateTimedOut(timeout)) { + setError(0x01); /* 超时 */ + pushStatus(); + return; + } + + /* ===== ACK 优先处理 ===== */ + if (s_ack_pending) { + s_ack_pending = false; + uint8_t ack_data[64]; + uint8_t ack_len; + memcpy(ack_data, s_ack_data, 64); + ack_len = s_ack_len; + + /* 日志:握手步骤全打印;传输期错误帧限频 */ + bool rx_do_log = (s_state != State::kTransfer); + if (s_state == State::kTransfer && ack_len == 1) { + static uint32_t s_last_rxerr_ms = 0; + const uint32_t rnow = SysTickTimer::getMs(); + if (rnow - s_last_rxerr_ms >= 200U) { rx_do_log = true; s_last_rxerr_ms = rnow; } + } + if (rx_do_log) { + printf("[MOTOR-OTA] RX %s pkt=%u/%u len=%u b0=%02X b1=%02X t=%lu\r\n", + stepName(s_state), (unsigned)s_packets_acked, (unsigned)s_total_packets, + (unsigned)ack_len, + (ack_len > 0) ? ack_data[0] : 0xFFU, (ack_len > 1) ? ack_data[1] : 0xFFU, + (unsigned long)SysTickTimer::getMs()); + } + + switch (s_state) { + case State::kHandshakeReq: + if (ack_len >= 2 && ack_data[0] == 0x00 && ack_data[1] == 0xAC) { + /* echo 正确 —— 立即发 nonce(与 PC 工具一致,不给干扰帧窗口) */ + generateNonce(s_nonce); + enterState(State::kHandshakeNonce); + canSend(kOtaReqId, s_nonce, 16); + s_last_send_ms = SysTickTimer::getMs(); + printf("[MOTOR-OTA] TX HANDSHAKE_NONCE(0x100:16B) [ACK-driven] t=%lu\r\n", + (unsigned long)s_last_send_ms); + } + /* 不匹配的 ACK 靠掉 —— 忽略,等超时重发 REQ */ + break; + + case State::kHandshakeNonce: + if (ack_len < 36) { + /* 短帧(0x05/干扰) —— 忽略,等真正 48B 或超时重发 */ + printf("[MOTOR-OTA] NONCE ACK 忽略短帧 len=%u b0=0x%02X\r\n", + (unsigned)ack_len, (ack_len > 0) ? ack_data[0] : 0xFFU); + break; + } + if (memcmp(ack_data + 12, s_nonce + 8, 8) != 0) { + /* nonce echo 不匹配 —— 电机收到了错误的帧当 nonce */ + printf("[MOTOR-OTA] NONCE ACK echo mismatch, ignore\r\n"); + break; + } + /* ✔ nonce 响应正确 —— 派生 K_session,立即发 confirm */ + aes128_set_key(&s_aes_ctx, K_MASTER); + aes128_encrypt_block(&s_aes_ctx, s_nonce, s_k_session); + s_session_num = static_cast(s_nonce[0]) + | (static_cast(s_nonce[1]) << 8) + | (static_cast(s_nonce[2]) << 16) + | (static_cast(s_nonce[3]) << 24); + { + enterState(State::kHandshakeConfirm); + uint8_t plain[16]; + for (uint8_t i = 0; i < 16; i++) plain[i] = s_nonce[i] ^ 0x55; + uint8_t confirm[16]; + aes128_set_key(&s_aes_ctx, s_k_session); + aes128_encrypt_block(&s_aes_ctx, plain, confirm); + canSend(kOtaReqId, confirm, 16); + s_last_send_ms = SysTickTimer::getMs(); + printf("[MOTOR-OTA] TX HANDSHAKE_CONFIRM(0x100:16B) [ACK-driven] t=%lu\r\n", + (unsigned long)s_last_send_ms); + } + break; + + case State::kHandshakeConfirm: + if (ack_len >= 2 && ack_data[0] == 0xBA && ack_data[1] == 0xC0) { + /* ✔ 握手成功 —— 立即发 fw_size */ + enterState(State::kErase); + uint8_t size_le[4]; + size_le[0] = static_cast(s_fw_size & 0xFF); + size_le[1] = static_cast((s_fw_size >> 8) & 0xFF); + size_le[2] = static_cast((s_fw_size >> 16) & 0xFF); + size_le[3] = static_cast((s_fw_size >> 24) & 0xFF); + canSend(kOtaDataId, size_le, 4); + s_last_send_ms = SysTickTimer::getMs(); + printf("[MOTOR-OTA] TX ERASE(0x101:fw_size) [ACK-driven] t=%lu\r\n", + (unsigned long)s_last_send_ms); + } + /* 非 [BA C0] —— 忽略(可能是 0x03/0x05 干扰帧) */ + break; + + case State::kErase: + if (ack_len >= 6 && ack_data[0] == 0xBA && ack_data[1] == 0xC0) { + /* ✔ 擦除完成 —— 进入传输态,等 kEraseWaitMs 后由 onTxTick 发首包 */ + s_packets_acked = 0; + enterState(State::kTransfer); + pushStatus(); + /* 清空 CAN TX 邮箱:握手期间快速连发4帧(REQ+nonce+confirm+fw_size), + * 可能有帧卡在邮箱未发出,进入 kTransfer 后才发出 → 电机收到 + * 过时的 0x100 帧 → catch-all → [05] 杀死 OTA。强制 abort 降低风险。 */ + CanFd::abortPendingTx(s_channel); + s_last_send_ms = SysTickTimer::getMs(); + printf("[MOTOR-OTA] ERASE ACK 收到,等 %ums 再发首包\r\n", + (unsigned)kEraseWaitMs); + } + /* 非 [BA C0 ...] —— 忽略,等超时重发 fw_size */ + break; + + case State::kTransfer: + processTransferAck(ack_data, ack_len); + break; + + case State::kFinishOta: + if (ack_len >= 2 && ack_data[0] == 0x0A && ack_data[1] == 0xAC) { + enterState(State::kFinished); + pushStatus(); + } else { + /* BOOT 可能回 WAIT 表示还在处理,重置超时 */ + s_state_enter_ms = SysTickTimer::getMs(); + } + break; + + default: + break; + } + return; /* ACK 已处理,本轮结束 */ + } + + /* ===== 无 ACK —— 首发或超时重发 ===== */ + if (s_state == State::kTransfer) { + return; /* kTransfer 由 onTxTick 驱动 */ + } + + const uint32_t now_ms = SysTickTimer::getMs(); + + if (s_last_send_ms == 0U) { + /* 首发(仅 kHandshakeReq 初始进入时走这里,其余步骤由 ACK handler 触发发帧) */ + sendCurrentStep(now_ms); + } else if ((now_ms - s_last_send_ms) >= kStepTimeoutMs) { + /* 超时重发 */ + if (s_retry_count >= kMaxResends) { + printf("[MOTOR-OTA] %s 未收到应答(已发送 %u 次无响应),中止刷写\r\n", + stepName(s_state), (unsigned)(kMaxResends + 1U)); + setError(static_cast(0x30U + static_cast(s_state))); + pushStatus(); + return; + } + s_retry_count++; + sendCurrentStep(now_ms); + } +} + +bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len) { + if (s_state == State::kIdle || s_state == State::kFinished || s_state == State::kError) { + return false; + } + if (channel != s_channel) return false; + + /* BOOT 的 0x103 帧语义修正: + * 本电机 BOOT 仅在 IDLE 状态每 ~10ms 发一次 0x103 心跳(见 OTA.cpp process()), + * 擦除/传输忙时并不发 0x103。握手阶段(kHandshakeReq/Nonce/Confirm)收到 0x103 + * 只说明电机仍停在 IDLE、尚未接受握手,此时绝不能重置超时——否则握手失败时 + * 超时会被 IDLE 心跳无限重置 → relay 静默死循环、UI 永远卡在"握手中"且无错误码。 + * 仅在 kErase/kTransfer/kFinishOta(电机确在忙、WAIT 合法)才重置超时, + * 使握手失败能在 kHandshakeTimeoutMs 后正常超时报错、可被取消/重试。 */ + if (id == kOtaWaitId) { + /* kPreCancel 期间:0x103 = 电机 BOOT IDLE 心跳,证明电机已进入 Bootloader */ + if (s_state == State::kPreCancel) { + s_boot_heartbeat_seen = true; + } + if (s_state == State::kErase || s_state == State::kTransfer || + s_state == State::kFinishOta) { + s_state_enter_ms = SysTickTimer::getMs(); + } + return true; /* 已消费,不再传递给其他 handler */ + } + + if (id != kOtaAckId) return false; + + /* kPreCancel 期间丢弃残留 ACK(电机可能对上次 OTA 的帧补发 ACK), + * 不置 s_ack_pending,确保进入 kHandshakeReq 时从干净状态开始。 */ + if (s_state == State::kPreCancel) return true; + + /* 捕获 ACK:总是覆盖(处理最新帧)。 + * 无需 s_last_send_ms guard——新模型里 ACK handler 触发后立即发下一帧, + * s_last_send_ms 在任何 ACK 到达前已被设置。 */ + + /* 捕获 ACK */ + const uint8_t copy_len = (len <= 64) ? len : 64; + memcpy(s_ack_data, data, copy_len); + s_ack_len = copy_len; + s_ack_pending = true; + return true; /* 已消费,不再传递给其他 handler */ +} + +bool start(uint8_t can_channel, uint32_t fw_size) { + if (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError) { + return false; /* 已在进行中 */ + } + if (fw_size == 0 || fw_size > kMaxFwSize) return false; + if (can_channel >= 3) return false; + +s_channel = can_channel; + s_fw_size = fw_size; + s_fw_fed = 0U; + s_total_packets = static_cast((fw_size + kPacketPayload - 1U) / kPacketPayload); + s_packets_acked = 0; + s_error = 0; + s_session_num = 0; /* 由 processNonceAck 根据 s_nonce 前4字节设置 */ + s_buf_head = 0; + s_buf_tail = 0; + s_ack_pending = false; + s_last_send_ms = 0; /* 0=尚未首发,enterState(kHandshakeReq) 也会置 0,由 process() 首发 00AC */ + s_crc_retry_count = 0; + s_last_acked_pkt = 0xFFFFU; + s_pkt_pending = false; + s_dup_ack_count = 0; + s_crc_err_count = 0; + s_last_pad_valid = 0xFFFFU; + + /* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。 + * 先进 kPreCancel 等待电机 0x103 心跳确认已进 BOOT,再由 process() 异步推进到握手。 */ + printf("[MOTOR-OTA] START ch=%u fw=%lu total=%u\r\n", + (unsigned)can_channel, (unsigned long)fw_size, (unsigned)s_total_packets); + s_boot_heartbeat_seen = false; + enterState(State::kPreCancel); + return true; +} + +bool feedData(const uint8_t *data, uint16_t len) { + /* 接受任何活跃状态的数据(包括握手期间)——让 ESP32 在 kPreCancel/握手 + * 期间就能预填充缓冲,进入 kTransfer 后立即有数据可发。 + * + * payload: [offset:u32 LE][chunk...]。offset 为固件绝对字节偏移。 + * ★ BLF 根因:无 offset 纯流时,SPI 超时重传同一 chunk,GD32 再次追加, + * CAN 上出现「前 236B 循环 185 次」→ CRC 全过但固件内容全错。 */ + if (s_state == State::kIdle || s_state == State::kError || + s_state == State::kFinished) { + return false; + } + if (data == nullptr || len < 4U) { + return false; + } + + const uint32_t offset = static_cast(data[0]) + | (static_cast(data[1]) << 8) + | (static_cast(data[2]) << 16) + | (static_cast(data[3]) << 24); + const uint8_t *chunk = data + 4U; + uint16_t chunk_len = static_cast(len - 4U); + if (chunk_len == 0U) { + return true; + } + if (s_fw_size == 0U) { + return false; + } + if (offset >= s_fw_size) { + return true; /* 越界尾:幂等 ACK */ + } + /* 截到固件末尾 */ + if (offset + chunk_len > s_fw_size) { + chunk_len = static_cast(s_fw_size - offset); + } + + if (offset + chunk_len <= s_fw_fed) { + /* 完全已收过:幂等 ACK,不写环缓 */ + return true; + } + if (offset > s_fw_fed) { + /* 缺口:拒收,ESP32 按 s_fw_offset 重发正确位置 */ + static uint32_t s_last_gap_ms = 0; + const uint32_t now = SysTickTimer::getMs(); + if (now - s_last_gap_ms >= 200U) { + printf("[MOTOR-OTA] feed 缺口 off=%lu fed=%lu len=%u\r\n", + (unsigned long)offset, (unsigned long)s_fw_fed, + (unsigned)chunk_len); + s_last_gap_ms = now; + } + return false; + } + + /* 部分重叠:只追加 [s_fw_fed, offset+chunk_len) */ + const uint32_t skip = s_fw_fed - offset; + const uint16_t add_len = static_cast(chunk_len - skip); + if (bufFree() < add_len) { + return false; /* 环缓满 → SPI WAIT */ + } + bufPush(chunk + skip, add_len); + s_fw_fed += add_len; + return true; + } + +void cancel() { + if (s_state == State::kIdle) return; + printf("[MOTOR-OTA] CANCEL received in state %s\r\n", stepName(s_state)); + /* ★ kFinished/kError 时电机已跳转 APP 或已中止,绝不能再发 CAN 取消帧! + * kOtaCancelId=0x00000000 与电机 APP 的 CANFD_BOOT_ID(base_id=0x0000) 冲突, + * 电机 APP 收到会当作 BOOT 命令 → enter_silent_mode 10s,APP 看似卡死。 + * 仅在活跃传输态(kPreCancel..kFinishOta)发取消帧通知电机 BOOT 中止。 */ + const bool was_active = (s_state != State::kFinished && s_state != State::kError); + if (was_active) { + canSend(kOtaCancelId, nullptr, 0); + } +enterState(State::kIdle); + s_buf_head = 0; + s_buf_tail = 0; + s_fw_fed = 0U; + s_pkt_pending = false; + s_crc_retry_count = 0; + pushStatus(); + } + +Status getStatus() { + Status st; + st.state = s_state; + st.error_code = s_error; + st.packets_acked = s_packets_acked; + st.total_packets = s_total_packets; + st.fw_size = s_fw_size; + return st; +} + +bool isActive() { + return (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError); +} + +} /* namespace MotorOtaRelay */ diff --git a/GD32E508_DCU_APP/sys/motor_ota_relay.h b/GD32E508_DCU_APP/sys/motor_ota_relay.h new file mode 100644 index 0000000..1fcbd2a --- /dev/null +++ b/GD32E508_DCU_APP/sys/motor_ota_relay.h @@ -0,0 +1,84 @@ +/** + * @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 */ +}; + +struct Status { + State state; + uint8_t error_code; + uint16_t packets_acked; + uint16_t total_packets; + uint32_t fw_size; +}; + +/* 初始化(注册 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 命令接口 */ + bool start(uint8_t can_channel, uint32_t fw_size); + /** + * @brief 喂入固件明文。payload 布局:[offset:u32 LE][data...] + * offset 为固件绝对字节偏移;重复/重叠区间幂等 ACK,缺口拒绝(→SPI WAIT)。 + * 无 offset 的旧纯流会在 SPI 超时重传时把同一 chunk 追加进环缓(BLF 见前 236B 循环)。 + */ + bool feedData(const uint8_t *data, uint16_t len); + void cancel(); + +/* 状态查询 */ +Status getStatus(); +bool isActive(); + +} /* namespace MotorOtaRelay */ + +#endif /* SYS_MOTOR_OTA_RELAY_H */ diff --git a/GD32E508_DCU_APP/sys/task_manager.cpp b/GD32E508_DCU_APP/sys/task_manager.cpp index 5b406b5..5c0a8bc 100644 --- a/GD32E508_DCU_APP/sys/task_manager.cpp +++ b/GD32E508_DCU_APP/sys/task_manager.cpp @@ -32,6 +32,7 @@ void TaskManager::init() g_scheduler.div_can_rx = 0U; g_scheduler.div_can_tx = 0U; g_scheduler.div_heartbeat = 0U; + g_scheduler.div_motor_state = 0U; g_scheduler.div_diag_print = 0U; } @@ -95,7 +96,6 @@ void TaskManager::tick100us() if(++g_scheduler.div_can_tx >= CONFIG_TASK_PERIOD_CAN_TX_TICKS) { g_scheduler.div_can_tx = 0U; - g_scheduler.tick_1ms++; set(1UL << TASK_CAN_TX_SEND); } @@ -106,12 +106,18 @@ void TaskManager::tick100us() set(1UL << TASK_HEARTBEAT); } -#if (CONFIG_UART_TEMP_PERIODIC_PRINT != 0) + /* 多电机状态 0xC5 推送:250ms 独立节拍。 + * 不挂在 1Hz heartbeat 上,避免连带加速 OTA Boot 轮询/CAN快照/ADC推送/checkOffline。 */ + if(++g_scheduler.div_motor_state >= CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS) { + g_scheduler.div_motor_state = 0U; + set(1UL << TASK_MOTOR_STATE_PUSH); + } + + /* 始终 5s 调度诊断任务:默认静默检查错误增量,PERIODIC_PRINT=1 时打全量摘要 */ if(++g_scheduler.div_diag_print >= CONFIG_TASK_PERIOD_DIAG_PRINT_TICKS) { g_scheduler.div_diag_print = 0U; set(1UL << TASK_DIAGNOSTIC_PRINT); } -#endif } /** diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index ce36e21..aee20b9 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -8,8 +8,11 @@ #include "device/watchdog.h" #include "device/spi/app_spi_command.h" #include "device/canfd.h" +#include "device/motor_state.h" +#include "device/motor_version.h" #include "sys/config.h" #include "sys/ota_boot_request.h" +#include "sys/motor_ota_relay.h" #include "sys/task_manager.h" #include "sys/task_runner.h" #include "comm/ethercat_slave.h" @@ -134,7 +137,7 @@ void TaskRunner::taskAdcProcess() } /** - * @brief 执行 CAN 周期发送任务:每 1ms 在每个使能通道上发送一帧 CANFD 测试帧。 + * @brief 执行 CAN 周期发送任务:每 10ms 在每个使能通道上发送一帧 CANFD 测试帧。 * @param[in] 无。 * @param[out] 无。 * @retval 无返回值。 @@ -150,6 +153,7 @@ void TaskRunner::taskAdcProcess() */ void TaskRunner::taskCanTxSend() { +#if (CONFIG_CAN_PERIODIC_TEST_TX_ENABLE != 0) static uint32_t seq = 0U; for (uint8_t ch = 0U; ch < 3U; ++ch) { @@ -176,6 +180,11 @@ void TaskRunner::taskCanTxSend() seq++; } } +#else + /* CONFIG_CAN_PERIODIC_TEST_TX_ENABLE=0:测试帧无业务接收节点 ACK, + * 单发模式下会持续产生 MTEn 错误帧,整任务空跑。 */ + (void)0; +#endif } /** @@ -199,6 +208,25 @@ void TaskRunner::taskCanRxParse() OtaBootRequest::process(); return; } + if(MotorOtaRelay::onCanFrame(s_runtime.last_rx_channel, + s_runtime.last_rx_id, + s_runtime.last_rx_data, + s_runtime.last_rx_len)) { + consumed++; + continue; + } +#if (CONFIG_MOTOR_STATE_ENABLE != 0) + MotorState::onCanFrame(s_runtime.last_rx_channel, + s_runtime.last_rx_id, + s_runtime.last_rx_data, + s_runtime.last_rx_len); +#endif +#if (CONFIG_MOTOR_VERSION_ENABLE != 0) + MotorVersion::onCanFrame(s_runtime.last_rx_channel, + s_runtime.last_rx_id, + s_runtime.last_rx_data, + s_runtime.last_rx_len); +#endif consumed++; } @@ -221,6 +249,22 @@ void TaskRunner::taskHeartbeat() canFeedbackStartRound(); /* ADC 调试反馈 1Hz:CONFIG_SPI_ADC_FEEDBACK_ENABLE=0 时为空操作 */ adcFeedbackStartRound(); +#if (CONFIG_MOTOR_STATE_ENABLE != 0) + MotorState::checkOffline(); +#endif +} + +/** + * @brief 启动多电机状态 0xC5 推送轮次(250ms 独立节拍)。 + * @note 与 taskHeartbeat 解耦:不连带加速 OTA Boot 轮询 / CAN 快照 / ADC 推送 / + * checkOffline 离线判定(3s 阈值)。仅触发本轮 P0/P1 分页推送, + * 实际 SPI 传输由 motorStatePushStep() 在主循环按铃协议推进。 + */ +void TaskRunner::taskMotorStatePush() +{ +#if (CONFIG_MOTOR_STATE_ENABLE != 0) + motorStateStartRound(); +#endif } /** @@ -238,60 +282,62 @@ bool TaskRunner::copyAdcSample(Adc::Sample *out) } /** - * @brief 输出当前温度、电压与 CAN 诊断信息。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 由 5 秒节拍触发,通过 UART 打印当前温度、电压;采用定点整数格式,避免依赖浮点 `printf` 支持。 + * @brief 5s 诊断任务:默认仅状态变化/错误增量;可选全量 UART 摘要。 + * @note CONFIG_UART_TEMP_PERIODIC_PRINT=1 时打印 ADC/CAN/ECAT 全量行(调试用)。 + * 默认=0:相对上次快照有 CAN 错误计数增量、ADC valid 跳变、ECAT ready 变化或 ISR 超阈时才打印。 + * 采用定点整数格式,避免依赖浮点 printf。 */ void TaskRunner::taskDiagnosticPrint() { const auto *diag = CanFd::diag(); const auto &s = s_runtime.sample; - const int32_t temp_x100 = static_cast(s.temperature * 100.0f); - const uint32_t vdda_mV = static_cast(s.vdda * 1000.0f); - const uint32_t vin_adc_mV = static_cast(s.vin_adc_v * 1000.0f); + +#if (CONFIG_UART_TEMP_PERIODIC_PRINT != 0) + /* ---- Debug 全量摘要:周期刷 ADC / CAN 吞吐 / ECAT ISR 统计 ---- */ + { + const int32_t temp_x100 = static_cast(s.temperature * 100.0f); + const uint32_t vdda_mV = static_cast(s.vdda * 1000.0f); + const uint32_t vin_adc_mV = static_cast(s.vin_adc_v * 1000.0f); #if (CONFIG_UART_DIAG_LEVEL == 0) - printf("[APP] uptime=%lus valid=%u, temp_raw=%u, vref_raw=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V\r\n", - (unsigned long)g_app_uptime_sec, - static_cast(s.valid), - static_cast(s.temp_raw), - static_cast(s.vref_raw), - static_cast(temp_x100 / 100), - static_cast((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100), - static_cast(vdda_mV / 1000U), - static_cast(vdda_mV % 1000U), - static_cast(vin_adc_mV / 1000U), - static_cast(vin_adc_mV % 1000U)); + printf("[APP] uptime=%lus valid=%u, temp_raw=%u, vref_raw=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V\r\n", + (unsigned long)g_app_uptime_sec, + static_cast(s.valid), + static_cast(s.temp_raw), + static_cast(s.vref_raw), + static_cast(temp_x100 / 100), + static_cast((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100), + static_cast(vdda_mV / 1000U), + static_cast(vdda_mV % 1000U), + static_cast(vin_adc_mV / 1000U), + static_cast(vin_adc_mV % 1000U)); #else - printf("[APP] uptime=%lus valid=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V, rx_ch=%u, rx_id=0x%08lX, rx_len=%u, tx_ch=%u, can_tx=%lu, can_rx=%lu, sw_drop=%lu, hw_full=%lu, hw_over=%lu, tx_abort=%lu, can_err=%lu\r\n", - (unsigned long)g_app_uptime_sec, - static_cast(s.valid), - static_cast(temp_x100 / 100), - static_cast((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100), - static_cast(vdda_mV / 1000U), - static_cast(vdda_mV % 1000U), - static_cast(vin_adc_mV / 1000U), - static_cast(vin_adc_mV % 1000U), - static_cast(s_runtime.last_rx_channel), - static_cast(s_runtime.last_rx_id), - static_cast(s_runtime.last_rx_len), - static_cast(diag->last_tx_channel), - static_cast(diag->tx_count), - static_cast(diag->rx_count), - static_cast(diag->rx_drop_count), - static_cast(diag->rx_fifo_full_count), - static_cast(diag->rx_fifo_overfull_count), - static_cast(diag->tx_abort_count), - static_cast(diag->error_count)); + printf("[APP] uptime=%lus valid=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V, rx_ch=%u, rx_id=0x%08lX, rx_len=%u, tx_ch=%u, can_tx=%lu, can_rx=%lu, sw_drop=%lu, hw_full=%lu, hw_over=%lu, tx_abort=%lu, can_err=%lu\r\n", + (unsigned long)g_app_uptime_sec, + static_cast(s.valid), + static_cast(temp_x100 / 100), + static_cast((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100), + static_cast(vdda_mV / 1000U), + static_cast(vdda_mV % 1000U), + static_cast(vin_adc_mV / 1000U), + static_cast(vin_adc_mV % 1000U), + static_cast(s_runtime.last_rx_channel), + static_cast(s_runtime.last_rx_id), + static_cast(s_runtime.last_rx_len), + static_cast(diag->last_tx_channel), + static_cast(diag->tx_count), + static_cast(diag->rx_count), + static_cast(diag->rx_drop_count), + static_cast(diag->rx_fifo_full_count), + static_cast(diag->rx_fifo_overfull_count), + static_cast(diag->tx_abort_count), + static_cast(diag->error_count)); #endif - { if (EthercatSlave::isEscReady()) { uint32_t maxCyc = 0U, avgCyc = 0U, cnt = 0U; EthercatSlave::getIsrStats(&maxCyc, &avgCyc, &cnt); - /* 180MHz: cycles / 180 = µs;用 *1000/180 获得毫微秒精度避免浮点 */ + /* 180MHz: cycles / 180 = µs;用 *1000/180 获得亚微秒精度避免浮点 */ const uint32_t maxUs_x1000 = maxCyc * 1000U / 180U; const uint32_t avgUs_x1000 = avgCyc * 1000U / 180U; printf("[ECAT] periodicIsr cnt=%lu max=%lu.%03luus avg=%lu.%03luus\r\n", @@ -303,6 +349,88 @@ void TaskRunner::taskDiagnosticPrint() (unsigned long)EthercatSlave::getChipId()); } } +#else + /* ---- 默认静默路径:仅错误增量 / 状态变化,无 uptime 心跳 ---- */ + { + static uint32_t s_prev_rx_drop = 0U; + static uint32_t s_prev_rx_fifo_full = 0U; + static uint32_t s_prev_rx_fifo_overfull = 0U; + static uint32_t s_prev_tx_abort = 0U; + static uint32_t s_prev_error = 0U; + static uint8_t s_prev_adc_valid = 0xFFU; /* 0xFF=尚未采样 */ + static bool s_prev_esc_ready = false; + static bool s_esc_ready_inited = false; + static uint32_t s_prev_ecat_max_cyc = 0U; + + const uint32_t rx_drop = diag->rx_drop_count; + const uint32_t rx_full = diag->rx_fifo_full_count; + const uint32_t rx_over = diag->rx_fifo_overfull_count; + const uint32_t tx_abort = diag->tx_abort_count; + const uint32_t can_err = diag->error_count; + + if (rx_drop > s_prev_rx_drop || rx_full > s_prev_rx_fifo_full + || rx_over > s_prev_rx_fifo_overfull || tx_abort > s_prev_tx_abort + || can_err > s_prev_error) { + printf("[CAN-DIAG] uptime=%lus err=%lu(+%lu) drop=%lu(+%lu) full=%lu(+%lu) over=%lu(+%lu) abort=%lu(+%lu)\r\n", + (unsigned long)g_app_uptime_sec, + (unsigned long)can_err, + (unsigned long)(can_err - s_prev_error), + (unsigned long)rx_drop, + (unsigned long)(rx_drop - s_prev_rx_drop), + (unsigned long)rx_full, + (unsigned long)(rx_full - s_prev_rx_fifo_full), + (unsigned long)rx_over, + (unsigned long)(rx_over - s_prev_rx_fifo_overfull), + (unsigned long)tx_abort, + (unsigned long)(tx_abort - s_prev_tx_abort)); + s_prev_rx_drop = rx_drop; + s_prev_rx_fifo_full = rx_full; + s_prev_rx_fifo_overfull = rx_over; + s_prev_tx_abort = tx_abort; + s_prev_error = can_err; + } + + if (s_prev_adc_valid == 0xFFU) { + s_prev_adc_valid = s.valid; + } else if (s.valid != s_prev_adc_valid) { + printf("[APP] adc valid %u -> %u (uptime=%lus)\r\n", + static_cast(s_prev_adc_valid), + static_cast(s.valid), + (unsigned long)g_app_uptime_sec); + s_prev_adc_valid = s.valid; + } + + const bool esc_ready = EthercatSlave::isEscReady(); + if (!s_esc_ready_inited) { + s_prev_esc_ready = esc_ready; + s_esc_ready_inited = true; + } else if (esc_ready != s_prev_esc_ready) { + printf("[ECAT] ESC ready %u -> %u chip_id=0x%08lX (uptime=%lus)\r\n", + static_cast(s_prev_esc_ready ? 1U : 0U), + static_cast(esc_ready ? 1U : 0U), + (unsigned long)EthercatSlave::getChipId(), + (unsigned long)g_app_uptime_sec); + s_prev_esc_ready = esc_ready; + } + + if (esc_ready) { + uint32_t maxCyc = 0U, avgCyc = 0U, cnt = 0U; + EthercatSlave::getIsrStats(&maxCyc, &avgCyc, &cnt); + /* 阈值单位 µs → CPU 周期(180 MHz) */ + const uint32_t threshCyc = CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD * 180U; + if (maxCyc > threshCyc && maxCyc > s_prev_ecat_max_cyc) { + const uint32_t maxUs_x1000 = maxCyc * 1000U / 180U; + printf("[ECAT] ISR max over threshold: max=%lu.%03luus thr=%luus cnt=%lu (uptime=%lus)\r\n", + (unsigned long)(maxUs_x1000 / 1000U), + (unsigned long)(maxUs_x1000 % 1000U), + (unsigned long)CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD, + (unsigned long)cnt, + (unsigned long)g_app_uptime_sec); + s_prev_ecat_max_cyc = maxCyc; + } + } + } +#endif } /** @@ -359,12 +487,23 @@ void TaskRunner::runPending() OtaBootRequest::process(); } + /* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。 + * 非活跃时立即返回,开销可忽略。 */ + MotorOtaRelay::process(); + /* 数据发送推进:主循环每圈调用,按 SysTick 钟表控制每包间隔 + * (替代原靠 tick100us 计数 + TASK_MOTOR_OTA_TX 的方式,消除计数器竞争)。 */ + MotorOtaRelay::onTxTick(); + /* CAN 反馈主动推送:GD32 APP 循环推送 3 通道 × 2 页反馈帧到 ESP32, * 替代 ESP32 主动轮询。非阻塞:上一帧未读走时立即返回。 * 每次主循环迭代调用一次,ESP32 读走一帧后 CS 释放中断唤醒主循环继续推送。 */ canFeedbackPushStep(); + /* 多电机状态(0xC5):优先级介于 CAN 反馈与 ADC 之间;ring 忙则跳过。 */ + motorStatePushStep(); /* ADC 调试反馈(0xC4):优先级低于 CAN;ring 忙则跳过。可 config 关闭。 */ adcFeedbackPushStep(); + /* 多电机版本(0xC6):事件驱动、低频,优先级最低;无 dirty 版本时立即返回。 */ + motorVersionPushStep(); /* 周期路径( SYNC0 ISR preempt=0 )用 s_periodicSeq 序列号通知主循环, * 不再走 task_flags,避免 BASEPRI 临界区与 preempt=0 的 RMW 竞态。 @@ -385,6 +524,7 @@ void TaskRunner::runPending() if(pending & (1UL << TASK_CAN_RX_PARSE)) taskCanRxParse(); if(pending & (1UL << TASK_CAN_TX_SEND)) taskCanTxSend(); if(pending & (1UL << TASK_HEARTBEAT)) taskHeartbeat(); + if(pending & (1UL << TASK_MOTOR_STATE_PUSH)) taskMotorStatePush(); if(pending & (1UL << TASK_DIAGNOSTIC_PRINT)) taskDiagnosticPrint(); if(pending & (1UL << TASK_ETHERCAT_PROCESS)) taskEthercatProcess(); } diff --git a/GD32E508_DCU_APP/sys/task_runner.h b/GD32E508_DCU_APP/sys/task_runner.h index 9d62285..2ac3d59 100644 --- a/GD32E508_DCU_APP/sys/task_runner.h +++ b/GD32E508_DCU_APP/sys/task_runner.h @@ -46,10 +46,12 @@ private: static void taskAdcProcess(); /** @brief 解析并消费接收帧。 */ static void taskCanRxParse(); - /** @brief CAN 周期发送任务;每 1ms 每通道发送一帧测试报文。 */ + /** @brief CAN 周期发送任务;每 10ms 每通道发送一帧测试报文。 */ static void taskCanTxSend(); /** @brief 心跳维护任务。 */ static void taskHeartbeat(); + /** @brief 多电机状态 0xC5 主动推送启动(250ms 独立节拍)。 */ + static void taskMotorStatePush(); /** @brief 诊断打印任务。 */ static void taskDiagnosticPrint(); /** @brief EtherCAT 周期/事件处理(非实时应用层 PDO 映射)。 */