From 8e5da181000a5c812e39ff36bec3d0ad38d78adb Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Sat, 1 Aug 2026 04:15:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(motor-ota):=20=E6=B7=BB=E5=8A=A0SPI?= =?UTF-8?q?=E6=96=AD=E6=B5=81=E6=A3=80=E6=B5=8B=E5=92=8C=E4=BC=A0=E8=BE=93?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现SPI数据馈送断流超时检测,避免因WiFi/HTTP抖动导致的死锁 - 添加传输中途自动重试功能,支持SPI断流或电机会话丢失时的恢复 - 更新版本号至1.5.5,包含电机OTA relay的SPI断流超时处理 - 优化心跳超时逻辑,仅在有包在途时重置传输阶段超时 - 增加断流检测相关状态变量和错误码0x26处理 --- GD32E508_DCU_APP/sys/config.h | 2 +- GD32E508_DCU_APP/sys/motor_ota_relay.cpp | 90 ++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index a0aec46..faa799f 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -115,7 +115,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true; * ============================================================ */ constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; constexpr uint32_t APP_FW_VERSION_MINOR = 5U; -constexpr uint32_t APP_FW_VERSION_PATCH = 4U; /* 1.5.4:0xC5 推送周期运行时可调(子命令 0xFE 设周期,50~5000ms) */ +constexpr uint32_t APP_FW_VERSION_PATCH = 5U; /* 1.5.5:电机 OTA relay SPI 断流超时(0x26) + 0x103 心跳不再为无包在途的 kTransfer 续命 */ constexpr uint32_t APP_FW_VERSION_BUILD = 0U; diff --git a/GD32E508_DCU_APP/sys/motor_ota_relay.cpp b/GD32E508_DCU_APP/sys/motor_ota_relay.cpp index 9a2d118..dbc46a5 100644 --- a/GD32E508_DCU_APP/sys/motor_ota_relay.cpp +++ b/GD32E508_DCU_APP/sys/motor_ota_relay.cpp @@ -40,6 +40,10 @@ static uint32_t s_fw_size = 0; static uint16_t s_packets_acked = 0; static uint32_t s_session_num = 0; static bool s_boot_heartbeat_seen = false; /* kPreCancel 期间是否收到 0x103 心跳 */ +static uint32_t s_last_feed_ms = 0; /* 上次 feedData 成功接受新数据的时刻(SPI 断流检测) */ +static uint8_t s_auto_retry = 0; /* 传输中途自动重试计数(SPI 断流 / 电机会话丢失) */ +static uint16_t s_last_acked_pkt = 0xFFFFU; /* 上次确认的包号,用于识别重复 ACK */ +static uint8_t s_crc_retry_count = 0; /* 当前包 CRC 重试计数 */ /* AES */ static aes128_context s_aes_ctx; @@ -267,6 +271,40 @@ static void setError(uint8_t code) { AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeMotorOtaStatus, payload, 12); } +/* 传输中途自动重试:电机会话已死(超时退 IDLE)或 SPI 断流,但仍有重试预算。 + * 流程:CANCEL 电机 → 清环缓 → 重置喂数/包计数 → 回 kPreCancel 重新握手。 + * error_code 暂置 0xF0 推给 ESP32:含义="relay 正在重试,请从 offset 0 重新喂数"。 + * ESP32 feedTask 看到 state 回退到 kPreCancel + err=0xF0 → 重置 s_fw_offset=0。 */ +static void tryAutoRetry(const char *reason) { + s_auto_retry++; + LOG_ERR("[MOTOR-OTA] AUTO-RETRY #%u (%s) pkt=%u/%u fed=%lu/%lu\r\n", + (unsigned)s_auto_retry, reason, + (unsigned)s_packets_acked, (unsigned)s_total_packets, + (unsigned long)s_fw_fed, (unsigned long)s_fw_size); + + /* 通知电机 BOOT 中止当前会话(若已退 IDLE 则 CANCEL 无副作用) */ + canSend(kOtaCancelId, nullptr, 0); + + /* 清环缓 + 重置喂数位置:电机重新擦除后需要完整固件,旧缓冲数据已无效 */ + s_buf_head = 0; + s_buf_tail = 0; + s_fw_fed = 0U; + s_packets_acked = 0; + s_pkt_pending = false; + s_crc_resend_pending = false; + s_crc_retry_count = 0; + s_last_acked_pkt = 0xFFFFU; + s_ack_pending = false; + s_last_pad_valid = 0xFFFFU; + s_last_feed_ms = SysTickTimer::getMs(); /* 重试后重新起算 SPI 超时 */ + + /* 回 kPreCancel 等电机 0x103 心跳 → 重新握手 */ + s_boot_heartbeat_seen = false; + s_error = 0xF0U; /* 信号:非致命,正在重试 */ + enterState(State::kPreCancel); + pushStatus(); +} + /* ═══════════════════ 握手子步骤 ═══════════════════ */ /* 单步等对应反馈的超时:到点仍无反馈就补发。100ms 加快握手节奏(电机 BOOT 主循环~10ms 足够响应)。 */ @@ -297,6 +335,14 @@ static constexpr uint32_t kEraseWaitMs = 100U; * 自主发 [0A AC](刷写结束/APP 运行有效,见协议表 15/16 行)。 * 复位+初始化实测 <1s,3s 留足余量;超时 = 新 APP 未启动 → error 0x25。 */ static constexpr uint32_t kAppRunTimeoutMs = 3000U; +/* SPI 喂数断流超时(ms):ESP32 停止 feedData 超过此时限且固件尚未喂完, + * 判定 SPI 链路故障,主动报错退出,避免环缓耗尽后与电机 0x103 心跳形成死锁。 + * 8s 留足 WiFi/HTTP 抖动余量(正常 SPI 喂数间隔 <100ms)。 */ +static constexpr uint32_t kSpiFeedTimeoutMs = 8000U; +/* 传输中途 SPI 断流 / 电机会话丢失时的自动重试次数: + * 发 CANCEL 通知电机 → 清环缓 → 重新握手 → 推状态让 ESP32 从 offset 0 重喂。 + * 1 次重试覆盖"SPI 短暂抖动 + 电机超时退 IDLE"场景;仍失败则报错由上位机决策。 */ +static constexpr uint8_t kMaxAutoRetry = 1U; /* 残帧窗口 kBootSettleMs 已移除(2026-07-27):收到 0x103 即直接进握手。 * 旧担忧:电机 APP 跳转前 TX 队列残留 [00 FA] 帧延迟上总线被 BOOT catch-all 命中 → 0x05。 * 现行握手阶段对垃圾帧一律忽略,靠 kStepTimeoutMs=100ms 步进重发(≤2次)自愈, @@ -502,8 +548,6 @@ static constexpr uint8_t kBootErrCrc = 0x06; /* CRC 错误:重发同 * 发 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 uint32_t s_ack_resend_total = 0; /* 累计 ACK 超时重发次数(整场不清零,丢帧率基线) */ @@ -792,11 +836,35 @@ void process() { const uint32_t timeout = (s_state == State::kTransfer || s_state == State::kFinishOta) ? kAckTimeoutMs : kHandshakeTimeoutMs; if (stateTimedOut(timeout)) { - setError(0x01); /* 超时 */ - pushStatus(); + /* kTransfer 超时(电机无 ACK / 已退 IDLE):优先自动重试 */ + if (s_state == State::kTransfer && s_auto_retry < kMaxAutoRetry) { + tryAutoRetry("transfer-timeout"); + } else { + setError(0x01); /* 超时 */ + pushStatus(); + } return; } + /* SPI 喂数断流检测(kTransfer 专用):固件尚未喂完但 ESP32 已长时间无新数据。 + * 优先自动重试(重新握手 + 等 ESP32 重喂);重试耗尽才报错 0x26。 */ + if (s_state == State::kTransfer && s_fw_fed < s_fw_size) { + const uint32_t feed_silence = SysTickTimer::getMs() - s_last_feed_ms; + if (feed_silence >= kSpiFeedTimeoutMs) { + if (s_auto_retry < kMaxAutoRetry) { + tryAutoRetry("SPI-feed-starvation"); + } else { + LOG_ERR("[MOTOR-OTA] SPI feed starvation: no data for %lums, fed=%lu/%lu pkt=%u/%u retry-exhausted\r\n", + (unsigned long)feed_silence, + (unsigned long)s_fw_fed, (unsigned long)s_fw_size, + (unsigned)s_packets_acked, (unsigned)s_total_packets); + setError(0x26); /* 0x26 = SPI 喂数超时(重试耗尽) */ + pushStatus(); + } + return; + } + } + /* ===== ACK 优先处理 ===== */ if (s_ack_pending) { s_ack_pending = false; @@ -963,8 +1031,15 @@ bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len) if (s_state == State::kPreCancel) { s_boot_heartbeat_seen = true; } - if (s_state == State::kErase || s_state == State::kTransfer || - s_state == State::kFinishOta) { + if (s_state == State::kErase || s_state == State::kFinishOta) { + s_state_enter_ms = SysTickTimer::getMs(); + } + /* kTransfer 特殊处理:仅在有包在途(s_pkt_pending=true)时重置超时。 + * 电机 BOOT 在 RECEIVING_FIRMWARE 超时(~1-2s)后退回 IDLE 并恢复 0x103 心跳; + * 此时 GD32 若无包在途(环缓耗尽等 SPI 喂数),0x103 的含义是"电机已放弃传输" + * 而非"电机还在忙"。无条件重置超时 → 5s backstop 永远到不了 → 永久卡死。 + * 有包在途时重置仍合法:电机 flash 写慢确实会回 WAIT。 */ + if (s_state == State::kTransfer && s_pkt_pending) { s_state_enter_ms = SysTickTimer::getMs(); } return true; /* 已消费,不再传递给其他 handler */ @@ -1014,6 +1089,8 @@ s_channel = can_channel; s_ack_resend_total = 0; s_fast_resend_total = 0; s_last_pad_valid = 0xFFFFU; + s_last_feed_ms = SysTickTimer::getMs(); /* 从 start 起算 SPI 喂数超时 */ + s_auto_retry = 0; /* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。 * 先进 kPreCancel 等待电机 0x103 心跳确认已进 BOOT,再由 process() 异步推进到握手。 */ @@ -1084,6 +1161,7 @@ bool feedData(const uint8_t *data, uint16_t len) { } bufPush(chunk + skip, add_len); s_fw_fed += add_len; + s_last_feed_ms = SysTickTimer::getMs(); /* SPI 断流检测:记录有效喂数时刻 */ return true; }