feat(can): 优化批量发送性能并修复竞态问题

- 帧间添加5ms延时,暂停CAN反馈避免与批量SPI竞争导致偶发崩溃
- 单次批量容量从48帧提升至128帧,最大body大小从12KB增至48KB
- 单帧超时从400ms降至80ms,失败后不再拖慢整体速度
- 暂停0xC1反馈任务降低worker竞态和WDT风险
- 中途致命错误后跳过后续帧发送,避免每帧等待超时
- 添加详细的JSON错误响应格式
- 修复内存分配失败时的资源清理问题

refactor(wifi): 优化冷启动流程避免电源竞争

- 电源轨稳定等待策略调整:POWERON/BROWNOUT复位时等待200ms
- 仅BROWNOUT复位后清除PHY校准数据,避免每次上电全量校准
- 电源稳定等待期间等待400ms后再进行NVS/WiFi操作
- WiFi启动前延时从500ms调整为300ms
- BOD阈值适当放宽避免上电瞬间复位

perf(monitor): 调整堆内存监控策略

- 内存紧张时增大健康监控间隔:低于8KB时从3s增至10s
- 避免日志输出消耗更多堆内存造成雪崩效应

docs(readme): 更新冷启动优化说明

- 详细描述电源轨稳定等待机制
- 说明仅BROWNOUT后才强制PHY重校准的策略
- 解释避免与GD32抢电流的设计考虑

style(html): 更新前端资源版本号

- 更新CSS和JavaScript文件的版本参数
- 修复页面加载时的资源缓存问题

fix(ble): 修复阶段显示状态问题

- 终态阶段全部显示为完成状态(绿色)
- 优化状态栏样式更新逻辑

feat(ota): 优化SPI通信超时处理

- 将总超时拆分为发送和轮询两个阶段
- 禁止轮询单独占用500ms避免短超时批量卡顿
- 优化极短超时情况下的时间分配策略

refactor(can): 减少批量操作时的日志输出

- SPI失败和GD32拒绝时按类型聚合日志
- 每32次相同错误才输出一次完整日志
- 避免大量错误时UDP/格式化耗尽堆内存
This commit is contained in:
小龙虾
2026-07-17 00:24:09 +08:00
parent 5db3f66b8e
commit 6364eacfb1
19 changed files with 256 additions and 116 deletions
+22 -4
View File
@@ -64,8 +64,17 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
&resp_type, spi_rx, spi_rx_max, &resp_len, timeout_ms);
if (ret != ESP_OK) {
DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s",
(unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret));
/* 批量失败时勿每帧打日志:UDP/格式化会把堆打穿 */
static uint32_t s_spi_fail_n = 0U;
static int s_last_spi_err = 0;
const int err_i = static_cast<int>(ret);
if (s_spi_fail_n == 0U || err_i != s_last_spi_err || (s_spi_fail_n % 32U) == 0U) {
DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s (x%lu)",
(unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret),
(unsigned long)(s_spi_fail_n + 1U));
}
s_last_spi_err = err_i;
++s_spi_fail_n;
if (gd32_err_out) {
*gd32_err_out = 0U;
}
@@ -87,8 +96,17 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
if (gd32_err_out) {
*gd32_err_out = gd32_err;
}
DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X",
(unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err);
{
static uint32_t s_rej_n = 0U;
static uint8_t s_last_rej = 0xFFU;
if (s_rej_n == 0U || gd32_err != s_last_rej || (s_rej_n % 32U) == 0U) {
DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X (x%lu)",
(unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err,
(unsigned long)(s_rej_n + 1U));
}
s_last_rej = gd32_err;
++s_rej_n;
}
return -3;
}
+24 -9
View File
@@ -22,7 +22,6 @@
#include "esp_log.h"
#include "esp_random.h" // esp_random() for HELLO session_id
#include "esp_intr_alloc.h" // ESP_INTR_FLAG_IRAM
#include "spi_master.h"
#include "spi_link_v2.h"
#include "app_config.h"
@@ -1229,7 +1228,28 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
* ESP_OK - 读到过期帧(正常,忽略)
* ESP_ERR_INVALID_RESPONSE - 读到 0xFF(正常,延迟一拍)
* 其他 - SPI 真实错误 */
const uint32_t send_timeout = (timeout_ms > 500U) ? 500U : timeout_ms;
/* 将总超时切成 send + poll,禁止 poll 单独再给 500ms(短超时批量时会「失败卡很久」) */
uint32_t send_timeout = (timeout_ms > 500U) ? 500U : timeout_ms;
if (send_timeout == 0U) {
send_timeout = 1U;
}
/* 至少给 poll 留一点时间;总超时很短时 send/poll 各半 */
uint32_t poll_timeout;
if (timeout_ms > send_timeout) {
poll_timeout = timeout_ms - send_timeout;
} else if (timeout_ms >= 40U) {
send_timeout = timeout_ms / 2U;
if (send_timeout == 0U) {
send_timeout = 1U;
}
poll_timeout = timeout_ms - send_timeout;
} else {
poll_timeout = timeout_ms; /* 极短超时:发完立刻 poll,总预算仍受 deadline 约束 */
}
if (poll_timeout == 0U) {
poll_timeout = 1U;
}
esp_err_t send_ret = otaTransferUnlocked(type, payload, payload_len,
nullptr, nullptr, 0U, nullptr, send_timeout,
/*expect_stale=*/true);
@@ -1237,15 +1257,10 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
return send_ret;
}
/* 等 GD32 完成 onCsRelease;poll 内仅在 DATA_READY 低时变长读(不盲 CS)。 */
{
const TickType_t gap = (pdMS_TO_TICKS(1) > 0) ? pdMS_TO_TICKS(1) : 1;
vTaskDelay(gap);
}
/* 等 GD32 完成 onCsRelease 再 poll;5ms 兼顾「能发出」与批量节奏 */
vTaskDelay(pdMS_TO_TICKS(5));
/* 步骤 2: 轮询读取真实响应(变长读,不重发命令) */
const uint32_t poll_timeout = (timeout_ms > send_timeout)
? (timeout_ms - send_timeout) : 500U;
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, poll_timeout,
/*expected_type=*/0xFFU);
}