feat(motor-ota): 添加SPI断流检测和传输重试机制
- 实现SPI数据馈送断流超时检测,避免因WiFi/HTTP抖动导致的死锁 - 添加传输中途自动重试功能,支持SPI断流或电机会话丢失时的恢复 - 更新版本号至1.5.5,包含电机OTA relay的SPI断流超时处理 - 优化心跳超时逻辑,仅在有包在途时重置传输阶段超时 - 增加断流检测相关状态变量和错误码0x26处理
This commit is contained in:
@@ -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_MAJOR = 1U;
|
||||||
constexpr uint32_t APP_FW_VERSION_MINOR = 5U;
|
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;
|
constexpr uint32_t APP_FW_VERSION_BUILD = 0U;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ static uint32_t s_fw_size = 0;
|
|||||||
static uint16_t s_packets_acked = 0;
|
static uint16_t s_packets_acked = 0;
|
||||||
static uint32_t s_session_num = 0;
|
static uint32_t s_session_num = 0;
|
||||||
static bool s_boot_heartbeat_seen = false; /* kPreCancel 期间是否收到 0x103 心跳 */
|
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 */
|
/* AES */
|
||||||
static aes128_context s_aes_ctx;
|
static aes128_context s_aes_ctx;
|
||||||
@@ -267,6 +271,40 @@ static void setError(uint8_t code) {
|
|||||||
AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeMotorOtaStatus, payload, 12);
|
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 足够响应)。 */
|
/* 单步等对应反馈的超时:到点仍无反馈就补发。100ms 加快握手节奏(电机 BOOT 主循环~10ms 足够响应)。 */
|
||||||
@@ -297,6 +335,14 @@ static constexpr uint32_t kEraseWaitMs = 100U;
|
|||||||
* 自主发 [0A AC](刷写结束/APP 运行有效,见协议表 15/16 行)。
|
* 自主发 [0A AC](刷写结束/APP 运行有效,见协议表 15/16 行)。
|
||||||
* 复位+初始化实测 <1s,3s 留足余量;超时 = 新 APP 未启动 → error 0x25。 */
|
* 复位+初始化实测 <1s,3s 留足余量;超时 = 新 APP 未启动 → error 0x25。 */
|
||||||
static constexpr uint32_t kAppRunTimeoutMs = 3000U;
|
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 即直接进握手。
|
/* 残帧窗口 kBootSettleMs 已移除(2026-07-27):收到 0x103 即直接进握手。
|
||||||
* 旧担忧:电机 APP 跳转前 TX 队列残留 [00 FA] 帧延迟上总线被 BOOT catch-all 命中 → 0x05。
|
* 旧担忧:电机 APP 跳转前 TX 队列残留 [00 FA] 帧延迟上总线被 BOOT catch-all 命中 → 0x05。
|
||||||
* 现行握手阶段对垃圾帧一律忽略,靠 kStepTimeoutMs=100ms 步进重发(≤2次)自愈,
|
* 现行握手阶段对垃圾帧一律忽略,靠 kStepTimeoutMs=100ms 步进重发(≤2次)自愈,
|
||||||
@@ -502,8 +548,6 @@ static constexpr uint8_t kBootErrCrc = 0x06; /* CRC 错误:重发同
|
|||||||
* 发 1 字节 error frame(0x06),期望上位机重发同一包。超过此限判定为
|
* 发 1 字节 error frame(0x06),期望上位机重发同一包。超过此限判定为
|
||||||
* 真实数据损坏,报错中止,避免无限循环。 */
|
* 真实数据损坏,报错中止,避免无限循环。 */
|
||||||
static constexpr uint8_t kMaxCrcRetries = 3;
|
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_dup_ack_count = 0; /* 累计忽略的重复/未前进 ACK 帧数(诊断重传风暴) */
|
||||||
static uint32_t s_crc_err_count = 0; /* 累计收到的 0x06 CRC 错误帧数(含电机重传) */
|
static uint32_t s_crc_err_count = 0; /* 累计收到的 0x06 CRC 错误帧数(含电机重传) */
|
||||||
static uint32_t s_ack_resend_total = 0; /* 累计 ACK 超时重发次数(整场不清零,丢帧率基线) */
|
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)
|
const uint32_t timeout = (s_state == State::kTransfer || s_state == State::kFinishOta)
|
||||||
? kAckTimeoutMs : kHandshakeTimeoutMs;
|
? kAckTimeoutMs : kHandshakeTimeoutMs;
|
||||||
if (stateTimedOut(timeout)) {
|
if (stateTimedOut(timeout)) {
|
||||||
setError(0x01); /* 超时 */
|
/* kTransfer 超时(电机无 ACK / 已退 IDLE):优先自动重试 */
|
||||||
pushStatus();
|
if (s_state == State::kTransfer && s_auto_retry < kMaxAutoRetry) {
|
||||||
|
tryAutoRetry("transfer-timeout");
|
||||||
|
} else {
|
||||||
|
setError(0x01); /* 超时 */
|
||||||
|
pushStatus();
|
||||||
|
}
|
||||||
return;
|
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 优先处理 ===== */
|
/* ===== ACK 优先处理 ===== */
|
||||||
if (s_ack_pending) {
|
if (s_ack_pending) {
|
||||||
s_ack_pending = false;
|
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) {
|
if (s_state == State::kPreCancel) {
|
||||||
s_boot_heartbeat_seen = true;
|
s_boot_heartbeat_seen = true;
|
||||||
}
|
}
|
||||||
if (s_state == State::kErase || s_state == State::kTransfer ||
|
if (s_state == State::kErase || s_state == State::kFinishOta) {
|
||||||
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();
|
s_state_enter_ms = SysTickTimer::getMs();
|
||||||
}
|
}
|
||||||
return true; /* 已消费,不再传递给其他 handler */
|
return true; /* 已消费,不再传递给其他 handler */
|
||||||
@@ -1014,6 +1089,8 @@ s_channel = can_channel;
|
|||||||
s_ack_resend_total = 0;
|
s_ack_resend_total = 0;
|
||||||
s_fast_resend_total = 0;
|
s_fast_resend_total = 0;
|
||||||
s_last_pad_valid = 0xFFFFU;
|
s_last_pad_valid = 0xFFFFU;
|
||||||
|
s_last_feed_ms = SysTickTimer::getMs(); /* 从 start 起算 SPI 喂数超时 */
|
||||||
|
s_auto_retry = 0;
|
||||||
|
|
||||||
/* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。
|
/* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。
|
||||||
* 先进 kPreCancel 等待电机 0x103 心跳确认已进 BOOT,再由 process() 异步推进到握手。 */
|
* 先进 kPreCancel 等待电机 0x103 心跳确认已进 BOOT,再由 process() 异步推进到握手。 */
|
||||||
@@ -1084,6 +1161,7 @@ bool feedData(const uint8_t *data, uint16_t len) {
|
|||||||
}
|
}
|
||||||
bufPush(chunk + skip, add_len);
|
bufPush(chunk + skip, add_len);
|
||||||
s_fw_fed += add_len;
|
s_fw_fed += add_len;
|
||||||
|
s_last_feed_ms = SysTickTimer::getMs(); /* SPI 断流检测:记录有效喂数时刻 */
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user