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:
小龙虾
2026-08-01 04:14:51 +08:00
parent e9cb7342f6
commit 04a118219f
17 changed files with 572 additions and 90 deletions
+53 -17
View File
@@ -97,6 +97,11 @@ static int IRAM_ATTR readReadyLevelRaw(void)
* 但 serviceRingOnce / otaPollResponseUnlocked 等更早出现的方法需要它。 */
static esp_err_t waitSpiDataReady(uint32_t timeout_ms);
/** @brief DATA_READY 下降沿事件信号量(ISR give / waitSpiDataReady take)。
* 二值信号量:多余边沿饱和为 1,消费方醒来后必须重查电平,
* 陈旧 give 只多一次电平检查,无副作用。 */
static SemaphoreHandle_t s_readyEdgeSem = nullptr;
// ============================================================
// 单例
// ============================================================
@@ -220,12 +225,14 @@ esp_err_t SpiMaster::serviceRingOnce()
return ESP_ERR_NOT_FOUND;
}
/* 仅 worker 调用。有 pending 或 DATA_READY 低时读一帧 EVENT。 */
if (!m_gd32RequestPending) {
/* 无 ISR 标志时,若线已低仍尝试读(丢边沿兜底) */
if (readReadyLevelRaw() != 0) {
return ESP_ERR_NOT_FOUND;
}
/* 仅 worker 调用。有 pending 或 DATA_READY 低时读一帧 EVENT。
* ★ pending 也必须验电平:业务响应(PING/OTA ACK)拉低 PB6 时链路忙,
* ISR 误记 pending;响应被 poll 读走后 PB6 已回高,若不验电平会白等
* waitSpiDataReady(3ms) 超时,把每笔业务周期拖慢 ~3ms(测速直方图
* 79% 帧堆在 3-4ms 的根因)。真按铃时 PB6 在被读走前保持低,不误伤。 */
if (readReadyLevelRaw() != 0) {
m_gd32RequestPending = false;
return ESP_ERR_NOT_FOUND;
}
m_gd32RequestPending = false;
m_linkState.enterBusy(HandshakeState::SERVING_GD32_RING);
@@ -557,8 +564,11 @@ void SpiMaster::workerLoop()
* @brief GPIO 边沿中断回调(DATA_READY / PB6)。
*
* 仅处理下降沿:GD32 有待读数据(按铃)。上升沿不再使用(旧的"等上升沿
* ready"逻辑已删除,现由 waitSpiDataReady 做电平轮询)。
* ISR 内仅做标志翻转 + 任务通知,不做 SPI 传输。
* ready"逻辑已删除)。
* ISR 内仅做标志翻转 + 信号量/任务通知,不做 SPI 传输。
*
* 所有下降沿先 give s_readyEdgeSem:唤醒阻塞在 waitSpiDataReady 的任务
* (OTA 响应等待),把原来 1ms tick 轮询变成 µs 级事件驱动。
*
* 本端忙时(acceptRing()==false):记下 pending 但不 notify,避免抢断
* 当前 SPI 事务;事务结束 worker 下一轮会立即消费此标志,省去最多 25ms
@@ -572,6 +582,13 @@ void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg)
if (readReadyLevelRaw() != 0) {
return;
}
/* 无条件先 give 边沿事件:精确唤醒 waitSpiDataReady(响应已装载)。
* 无人等待时二值信号量饱和为 1,由消费方电平重查兜底。 */
if (s_readyEdgeSem != nullptr) {
BaseType_t sem_hp = pdFALSE;
xSemaphoreGiveFromISR(s_readyEdgeSem, &sem_hp);
portYIELD_FROM_ISR(sem_hp);
}
if (self->m_suppressRingService) {
/* OTA 中继:PB6 低是 WAIT/ACK 就绪,留给业务 poll,不按 EVENT 处理 */
return;
@@ -594,6 +611,8 @@ void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg)
/**
* @brief 等 GD32 拉低 PB6(有待读响应)。
* v2 DATA_READY:低 = 有数据;高 = 队列空。TX 前不等 HIGH(CS 即互斥)。
* 事件驱动:阻塞在 s_readyEdgeSem 上,由下降沿 ISR 精确唤醒(µs 级),
* 每次醒来重查电平以过滤陈旧 give;信号量不可用时退回 1ms tick 轮询。
* @return ESP_OK 已低;ESP_ERR_TIMEOUT 超时仍为高(调用方可再决定是否盲 poll)。
*/
static esp_err_t waitSpiDataReady(uint32_t timeout_ms)
@@ -602,11 +621,16 @@ static esp_err_t waitSpiDataReady(uint32_t timeout_ms)
if (timeout_ms == 0U) { return ESP_ERR_TIMEOUT; }
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
while (xTaskGetTickCount() < deadline) {
for (;;) {
if (readReadyLevelRaw() == 0) { return ESP_OK; }
vTaskDelay(pdMS_TO_TICKS(1) > 0 ? pdMS_TO_TICKS(1) : 1);
const TickType_t now = xTaskGetTickCount();
if (now >= deadline) { return ESP_ERR_TIMEOUT; }
if (s_readyEdgeSem != nullptr) {
(void)xSemaphoreTake(s_readyEdgeSem, deadline - now);
} else {
vTaskDelay(pdMS_TO_TICKS(1) > 0 ? pdMS_TO_TICKS(1) : 1);
}
}
return ESP_ERR_TIMEOUT;
}
// ============================================================
@@ -968,6 +992,14 @@ esp_err_t SpiMaster::init()
DEBUG_INFO("SPI", "DATA_READY 输入已配置: GPIO%d (← GD32 PB6,低=有数据)", SPI_READY_GPIO);
}
/* 边沿事件信号量必须先于 ISR 注册创建,避免 ISR 触发时句柄为空 */
if (s_readyEdgeSem == nullptr) {
s_readyEdgeSem = xSemaphoreCreateBinary();
if (s_readyEdgeSem == nullptr) {
DEBUG_WARN("SPI", "ready edge sem create failed, 退回轮询等待");
}
}
/* 安装 GPIO ISR 服务(全局仅安装一次)并注册边沿回调 */
static bool s_isrInstalled = false;
if (!s_isrInstalled) {
@@ -1487,12 +1519,16 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
return send_ret;
}
/* CS 释放后短延时,再 poll 应答。
* GD32 主控 OTA / HELLO 等:5ms(BOOT AES 路径需要)。
* 电机 OTA 数据 0xD1:1ms。每 chunk 固定 5ms 会把 SPI 喂速压到 ~47KB/s 边缘,
* CAN 5ms/包消费时环缓经常被掏空(日志「缓冲空,等ESP32喂」+ ACK 超时重发)。 */
const uint32_t post_cs_ms = (type == 0xD1U) ? 1U : 5U;
vTaskDelay(pdMS_TO_TICKS(post_cs_ms));
/* CS 释放后的静默期分型处理:
* - 0xD1(电机 OTA 数据)/ 0xA1(APP PING):GD32 APP 在 onCsRelease ISR
* 内 µs 级装好响应并拉低 PB6 → 零盲等,waitSpiDataReady 由下降沿 ISR
* 精确唤醒,旧 1ms 固定延时压制喂速~47KB/s,现由事件驱动提速。
* - 其他类型(GD32 relay/BOOT AES/HELLO 等):保留 5ms——BOOT 在主循环
* 处理 CHALLENGE/flash,poll 前 2 轮盲读 CS 过早打进去会打断其
* EXTI/DMA 时序(实测去掉后 CHALLENGE 超时)。 */
if (type != 0xD1U && type != 0xA1U) {
vTaskDelay(pdMS_TO_TICKS(5));
}
/* 步骤 2: 轮询读取真实响应(变长读,不重发命令) */
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, poll_timeout,