feat(motor-ota): 改进电机OTA中继的握手确认和重试机制
- 添加握手确认逻辑,避免在握手阶段就标记TRANSFER状态 - 实现GD32 auto-retry检测机制,当连续WAIT超限时查询真实状态 - 修复预填充缓冲区数据重复问题,移除旧的offset重置逻辑 - 优化SPI响应等待机制,允许在WAIT后重发DATA以恢复丢失的ACK - 添加连续SPI失败和WAIT计数监控,超过阈值时主动查询GD32状态 perf(can): 提升CAN批量发送性能和可靠性 - 将CAN帧间延迟从5ms减少到1ms以提升传输速度 - 将单批最大帧数从32增加到64,并将批次间隔从200ms减少到100ms - 添加手动超时控制器防止fetch请求永久挂起 - 实现批量发送完成后的兜底渲染机制确保结果正确显示 refactor(spi): 优化SPI通信的事件驱动机制 - 实现基于信号量的事件驱动等待机制,替换原有的轮询方式 - 将SPI时钟频率从4MHz提升至8MHz以提高传输速度 - 优化OTA发送后的静默期处理,不同类型的命令采用不同的延迟策略 - 添加临时SPI测速端点用于性能测试 chore(version): 更新固件版本至0.3.74 - 版本号从0.3.72提升到0.3.74 - 更新前端资源版本号以确保缓存刷新
This commit is contained in:
@@ -67,7 +67,13 @@ static void feedTask(void *arg)
|
||||
* 连续 SPI 失败计数:超过阈值时主动 QUERY GD32 真实状态,避免盲重试空转到 120s。 */
|
||||
uint32_t consec_fail = 0U;
|
||||
constexpr uint32_t kQueryAfterFails = 8U; /* 连续 8 次失败(~2s)后 QUERY 一次 */
|
||||
uint32_t consec_wait = 0U;
|
||||
constexpr uint32_t kQueryAfterWaits = 150U; /* 连续 150 次 WAIT(~300ms)后 QUERY:
|
||||
* 正常 buffer-full WAIT <100 次即恢复;
|
||||
* 超限说明 GD32 可能因 auto-retry 重置了
|
||||
* s_fw_fed=0,当前 offset 被 gap 拒收。 */
|
||||
uint32_t ack_n = 0U; /* 成功 ACK 计数,用于降频写 s_status */
|
||||
bool handshake_confirmed = false; /* GD32 已进 kTransfer/kErase,可标 TRANSFER */
|
||||
|
||||
while (s_feed_running && s_fw_offset < s_fw_size) {
|
||||
if (nowMs() >= deadline_ms) {
|
||||
@@ -88,10 +94,10 @@ static void feedTask(void *arg)
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* 注:旧版有 "GD32 回到握手阶段→重置 offset" 的逻辑,
|
||||
* 但 v1.4.38+ 已移除握手重试(0x05 直接中止不重握手),
|
||||
* 且 v1.4.58 feedData 在握手期间就接受数据(预填充缓冲)。
|
||||
* 若在此重置 offset,会导致缓冲中数据重复。已移除。 */
|
||||
/* GD32 auto-retry 检测(v1.5.5+):GD32 SPI 断流/电机超时后自动重新握手,
|
||||
* 重置 s_fw_fed=0。ESP32 侧通过连续 WAIT→QUERY 检测到 state!=kTransfer 后
|
||||
* 重置 s_fw_offset=0 从头喂(见下方 kRspWait 分支)。
|
||||
* 注:v1.4.38~v1.5.4 曾移除此逻辑(当时无 auto-retry,握手失败即中止)。 */
|
||||
|
||||
const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
const uint32_t data_len = (remaining < MOTOR_OTA_CHUNK_DATA)
|
||||
@@ -154,33 +160,98 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
continue; /* 不推进 offset,同 offset 重发 */
|
||||
}
|
||||
consec_fail = 0U;
|
||||
consec_wait = 0U;
|
||||
|
||||
if (resp_type == kRspAck) {
|
||||
s_fw_offset += data_len;
|
||||
/* 进度:每 8 chunk 写 s_status;state 在 suppress 下不会收 0xD2,
|
||||
* 喂数一开始就标 TRANSFER,避免网页卡在「开始握手」。 */
|
||||
const bool touch_status = ((++ack_n & 7U) == 0U) || (s_fw_offset >= s_fw_size);
|
||||
if (touch_status || ack_n == 1U) {
|
||||
const uint16_t fed_pkts = static_cast<uint16_t>((s_fw_offset + 47U) / 48U);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(2)) == pdTRUE) {
|
||||
if (s_status.state < MOTOR_OTA_TRANSFER ||
|
||||
s_status.state == MOTOR_OTA_PRE_CANCEL ||
|
||||
s_status.state == MOTOR_OTA_HANDSHAKE_REQ ||
|
||||
s_status.state == MOTOR_OTA_HANDSHAKE_NONCE ||
|
||||
s_status.state == MOTOR_OTA_HANDSHAKE_CONFIRM ||
|
||||
s_status.state == MOTOR_OTA_ERASE) {
|
||||
s_status.state = MOTOR_OTA_TRANSFER;
|
||||
if (resp_type == kRspAck) {
|
||||
s_fw_offset += data_len;
|
||||
/* 进度:每 8 chunk 写 s_status。
|
||||
* ★ 不再喂数据一成功就强标 TRANSFER:GD32 feedData() 在握手阶段
|
||||
* (kPreCancel/kHandshakeReq 等) 也接收预填充数据,若此刻强标
|
||||
* TRANSFER,无电机的空通道会让网页误显示"数据传输 N/912"而非
|
||||
* "握手中…→超时"。改为:只在 GD32 真正进 kErase/kTransfer 后
|
||||
* 才标 TRANSFER;握手未完成时保持握手态显示。
|
||||
* 握手未确认时每 8 个 ACK 重新 QUERY 一次(GD32 握手+擦除
|
||||
* 通常 1~3s,8 个 ACK ~16 个 chunk ≈ 3700B <1s,需多轮确认)。 */
|
||||
++ack_n;
|
||||
const bool touch_status = ((ack_n & 7U) == 0U) || (s_fw_offset >= s_fw_size);
|
||||
|
||||
/* 握手未确认时定期 QUERY:确认 GD32 已过握手阶段才标 TRANSFER */
|
||||
if (!handshake_confirmed && (ack_n & 7U) == 0U) {
|
||||
uint8_t qcmd[1] = {2U};
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK
|
||||
&& qt == kRspAck && ql >= 1U) {
|
||||
motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
if (qst == MOTOR_OTA_ERASE || qst == MOTOR_OTA_TRANSFER ||
|
||||
qst == MOTOR_OTA_FINISH_OTA || qst == MOTOR_OTA_WAIT_APP_RUN) {
|
||||
handshake_confirmed = true;
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(2)) == pdTRUE) {
|
||||
s_status.state = MOTOR_OTA_TRANSFER;
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
}
|
||||
/* 终态(ERROR/IDLE/FINISHED)交给下方终态检测处理 */
|
||||
}
|
||||
}
|
||||
if (touch_status) {
|
||||
const uint16_t fed_pkts = static_cast<uint16_t>((s_fw_offset + 47U) / 48U);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(2)) == pdTRUE) {
|
||||
/* 握手未确认时不标 TRANSFER、不更新 packets_acked:
|
||||
* 此刻 fed_pkts 是喂给 GD32 环缓冲的字节数(预填充),
|
||||
* 不是电机真实 ACK,握手未成时显示会让用户误以为在传输。 */
|
||||
if (handshake_confirmed) {
|
||||
s_status.state = MOTOR_OTA_TRANSFER;
|
||||
s_status.packets_acked = fed_pkts;
|
||||
}
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
}
|
||||
} else if (resp_type == kRspWait) {
|
||||
/* 缓冲满 / gap 拒收:不推进 offset,稍候重试。
|
||||
* WAIT 路径 SPI 层已推进 seq,避免同 seq 重发被 GD32 当重复请求死锁。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
/* 连续 WAIT 超限:可能 GD32 auto-retry 重置了 s_fw_fed=0,
|
||||
* 当前 offset 被 gap 拒收(永远不会变 ACK)。QUERY 确认真实状态。 */
|
||||
if (++consec_wait >= kQueryAfterWaits) {
|
||||
consec_wait = 0U;
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK
|
||||
&& qt == kRspAck && ql >= 2U) {
|
||||
const motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
const uint8_t qerr = qp[1];
|
||||
if (qst != MOTOR_OTA_TRANSFER) {
|
||||
/* GD32 不在传输态(auto-retry 回了 kPreCancel/握手):
|
||||
* 重置 offset 从头喂,配合 GD32 重新擦除+握手。 */
|
||||
ESP_LOGW(TAG, "GD32 retry detected: state=%u err=0x%02X, reset offset %u->0",
|
||||
(unsigned)qst, (unsigned)qerr, (unsigned)s_fw_offset);
|
||||
s_fw_offset = 0U;
|
||||
ack_n = 0U;
|
||||
handshake_confirmed = false; /* 重新握手,需重新确认 */
|
||||
} else {
|
||||
handshake_confirmed = true; /* GD32 在 transfer,确认握手完成 */
|
||||
}
|
||||
/* 终态仍 break */
|
||||
if (qst == MOTOR_OTA_IDLE || qst == MOTOR_OTA_FINISHED ||
|
||||
qst == MOTOR_OTA_ERROR) {
|
||||
ESP_LOGW(TAG, "QUERY(WAIT) 终态 0x%02X err=0x%02X,停止",
|
||||
(unsigned)qst, (unsigned)qerr);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = qerr;
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
s_status.packets_acked = fed_pkts;
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
}
|
||||
} else if (resp_type == kRspWait) {
|
||||
/* 缓冲满:不推进 offset,稍候用新 TX_SEQ 再发同一 offset。
|
||||
* WAIT 路径 SPI 层已推进 seq(见 otaPollResponseUnlocked),
|
||||
* 避免同 seq 重发被 GD32 当重复请求死锁。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
} else {
|
||||
/* 非 ACK/WAIT(含 ERROR/陈旧 type):不 break 整次 OTA。
|
||||
* 曾因 poll 读到 EVENT/错 type 直接 break → fed 卡首 chunk。 */
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @file spi_speed_test.cpp
|
||||
* @brief 【临时功能 SPI-SPEEDTEST】SPI 链路测速端点 GET /spi/speedtest
|
||||
*
|
||||
* 用 0xA1 APP PING(无副作用、GD32 在 onCsRelease ISR 内 µs 级应答)作探针,
|
||||
* 经 SpiMaster::transact() 走与业务完全相同的 worker 串行路径,
|
||||
* 测量事件驱动握手(ready 下降沿唤醒)下的真实 RTT 与等效吞吐。
|
||||
*
|
||||
* 用法:
|
||||
* GET /spi/speedtest # 默认 200 帧 × 236B payload
|
||||
* GET /spi/speedtest?frames=500&payload=0 # 500 帧空载(纯 RTT)
|
||||
* 返回中文纯文本: 帧数/成功/失败、总耗时、往返延迟 平均/最快/最慢、等效吞吐
|
||||
*
|
||||
* ── 删除本临时功能的步骤(共 3 处挂接点,全部以 [SPI-SPEEDTEST] 标记)──
|
||||
* 1. 删除本文件与 monitor/spi_speed_test.h
|
||||
* 2. main/CMakeLists.txt 删除 "monitor/spi_speed_test.cpp" 一行
|
||||
* 3. server/wifi_ota.cpp 删除 include 与 kHttpRoutes 中各一行
|
||||
* (spi_master.cpp 中 0xA1 零盲等白名单为正式优化,保留不删)
|
||||
*/
|
||||
#include "spi_speed_test.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_timer.h"
|
||||
#include "spi_master.h"
|
||||
#include "http_handlers.h" /* http_set_cors_headers */
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kProbeType = 0xA1U; /* APP PING:无副作用探针 */
|
||||
constexpr uint32_t kFramesDefault = 200U;
|
||||
constexpr uint32_t kFramesMax = 2000U; /* ~3s 上限,避免占死 HTTP/worker */
|
||||
constexpr size_t kPayloadDefault = 236U; /* 满帧:等效 0xD1 电机 OTA 数据节奏 */
|
||||
constexpr size_t kPayloadMax = 236U; /* APP kTxBufSize=256 - V2_OVERHEAD */
|
||||
constexpr uint32_t kFrameTimeoutMs = 500U;
|
||||
|
||||
/** @brief 从 query 里取无符号整型参数,缺失/非法时返回默认值。 */
|
||||
uint32_t queryU32(httpd_req_t *req, const char *key, uint32_t defval)
|
||||
{
|
||||
char qbuf[64];
|
||||
char vbuf[16];
|
||||
if (httpd_req_get_url_query_str(req, qbuf, sizeof(qbuf)) != ESP_OK) {
|
||||
return defval;
|
||||
}
|
||||
if (httpd_query_key_value(qbuf, key, vbuf, sizeof(vbuf)) != ESP_OK) {
|
||||
return defval;
|
||||
}
|
||||
char *end = nullptr;
|
||||
const unsigned long v = strtoul(vbuf, &end, 10);
|
||||
return (end && *end == '\0') ? (uint32_t)v : defval;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
esp_err_t spi_speedtest_handler(httpd_req_t *req)
|
||||
{
|
||||
uint32_t frames = queryU32(req, "frames", kFramesDefault);
|
||||
if (frames < 1U) { frames = 1U; }
|
||||
if (frames > kFramesMax) { frames = kFramesMax; }
|
||||
size_t payload_len = (size_t)queryU32(req, "payload", (uint32_t)kPayloadDefault);
|
||||
if (payload_len > kPayloadMax) { payload_len = kPayloadMax; }
|
||||
|
||||
/* 探针 payload:递增填充(CRC32 覆盖,顺带校验数据路径完整性) */
|
||||
uint8_t probe[kPayloadMax];
|
||||
for (size_t i = 0; i < payload_len; ++i) {
|
||||
probe[i] = (uint8_t)i;
|
||||
}
|
||||
|
||||
uint32_t n_ok = 0U;
|
||||
uint32_t n_fail = 0U;
|
||||
uint64_t rtt_sum_us = 0U;
|
||||
uint64_t rtt_min_us = UINT64_MAX;
|
||||
uint64_t rtt_max_us = 0U;
|
||||
/* RTT 分布直方图:<2 / 2-3 / 3-4 / 4-5 / >=5 ms,用于判断抖动形态
|
||||
* (双峰=离散 +1ms 碰撞;均匀=tick 相位量化) */
|
||||
uint32_t histo[5] = {0U, 0U, 0U, 0U, 0U};
|
||||
|
||||
DEBUG_INFO("SPI", "[SPEEDTEST] start frames=%u payload=%u",
|
||||
(unsigned)frames, (unsigned)payload_len);
|
||||
const int64_t t_start = esp_timer_get_time();
|
||||
|
||||
for (uint32_t i = 0U; i < frames; ++i) {
|
||||
uint8_t resp_type = 0U;
|
||||
uint8_t resp_payload[64];
|
||||
size_t resp_len = 0U;
|
||||
|
||||
const int64_t t0 = esp_timer_get_time();
|
||||
const esp_err_t ret = SpiMaster::instance().transact(
|
||||
kProbeType, probe, payload_len,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
kFrameTimeoutMs);
|
||||
const int64_t t1 = esp_timer_get_time();
|
||||
|
||||
if (ret == ESP_OK && resp_type == 0x80U) {
|
||||
const uint64_t rtt = (uint64_t)(t1 - t0);
|
||||
++n_ok;
|
||||
rtt_sum_us += rtt;
|
||||
if (rtt < rtt_min_us) { rtt_min_us = rtt; }
|
||||
if (rtt > rtt_max_us) { rtt_max_us = rtt; }
|
||||
const uint32_t ms_bucket = (uint32_t)(rtt / 1000U);
|
||||
if (ms_bucket < 2U) { ++histo[0]; }
|
||||
else if (ms_bucket < 3U) { ++histo[1]; }
|
||||
else if (ms_bucket < 4U) { ++histo[2]; }
|
||||
else if (ms_bucket < 5U) { ++histo[3]; }
|
||||
else { ++histo[4]; }
|
||||
} else {
|
||||
++n_fail;
|
||||
}
|
||||
}
|
||||
|
||||
const int64_t elapsed_us = esp_timer_get_time() - t_start;
|
||||
const uint32_t elapsed_ms = (uint32_t)(elapsed_us / 1000);
|
||||
const uint64_t rtt_avg_us = (n_ok > 0U) ? (rtt_sum_us / n_ok) : 0U;
|
||||
/* 吞吐语义:payload 字节 / 完整"发送+应答"周期,等效 0xD1 单缓冲喂速上限 */
|
||||
double tput_kbps = 0.0;
|
||||
if (elapsed_us > 0 && n_ok > 0U) {
|
||||
tput_kbps = ((double)n_ok * (double)payload_len) / ((double)elapsed_us / 1000000.0) / 1024.0;
|
||||
}
|
||||
if (rtt_min_us == UINT64_MAX) { rtt_min_us = 0U; }
|
||||
|
||||
DEBUG_INFO("SPI", "[SPEEDTEST] done ok=%u fail=%u avg=%uus tput=%.1fKB/s",
|
||||
(unsigned)n_ok, (unsigned)n_fail, (unsigned)rtt_avg_us, tput_kbps);
|
||||
|
||||
char text[480];
|
||||
const int len = snprintf(text, sizeof(text),
|
||||
"SPI 测速结果\n"
|
||||
"帧数: %u 成功: %u 失败: %u%s\n"
|
||||
"载荷: %u B/帧 总耗时: %u ms\n"
|
||||
"往返延迟: 平均 %.1f ms 最快 %.1f ms 最慢 %.1f ms\n"
|
||||
"延迟分布: <2ms:%u 2-3ms:%u 3-4ms:%u 4-5ms:%u >=5ms:%u\n"
|
||||
"等效吞吐: %.1f KB/s\n",
|
||||
(unsigned)frames, (unsigned)n_ok, (unsigned)n_fail,
|
||||
(n_fail > 0U) ? " <-- 存在失败帧, 链路异常!" : "",
|
||||
(unsigned)payload_len, (unsigned)elapsed_ms,
|
||||
(double)rtt_avg_us / 1000.0, (double)rtt_min_us / 1000.0,
|
||||
(double)rtt_max_us / 1000.0,
|
||||
(unsigned)histo[0], (unsigned)histo[1], (unsigned)histo[2],
|
||||
(unsigned)histo[3], (unsigned)histo[4], tput_kbps);
|
||||
|
||||
http_set_cors_headers(req);
|
||||
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
||||
return httpd_resp_send(req, text, (len > 0) ? len : 0);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @file spi_speed_test.h
|
||||
* @brief 【临时功能 SPI-SPEEDTEST】SPI 链路测速端点声明。
|
||||
* 删除方法见 spi_speed_test.cpp 文件头。
|
||||
*/
|
||||
#ifndef SPI_SPEED_TEST_H
|
||||
#define SPI_SPEED_TEST_H
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
/** @brief GET /spi/speedtest — 0xA1 探针测 SPI RTT/吞吐(临时功能)。 */
|
||||
esp_err_t spi_speedtest_handler(httpd_req_t *req);
|
||||
|
||||
#endif // SPI_SPEED_TEST_H
|
||||
Reference in New Issue
Block a user