From 1b50ce16570b44ac7ebea0f779bdbe3842a12605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99=E8=99=BE?= Date: Sat, 10 Oct 2026 02:25:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(can):=20=E6=96=B0=E5=A2=9ECAN=E9=80=9A?= =?UTF-8?q?=E9=81=93=E6=94=B6=E5=8F=91=E5=99=A8Silent=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增SPI 0xC8命令用于切换CAN通道收发器Silent模式(只听不发) - 在canFdSetTxSilent函数中实现静默模式控制逻辑 - 在HTTP API中添加POST /can/silent端点 - 更新CAN协议文档,记录新命令和错误码E6 - 修改bus_state位图,添加bit5表示静默状态 fix(can): 优化CAN反馈任务内存使用和链路离线处理 - 将任务栈上的临时缓冲区改为静态分配,防止栈溢出 - 添加链路在线检测,在离线状态下进入休眠退避 - 修复queryMotorStatePages和queryAdcFeedback函数返回值类型 - 优化UDP日志发送的冷却退避机制 chore(config): 更新调试日志配置和版本信息 - 在VSCode设置中添加ESP-IDF串口配置 - 添加SPI调试日志级别编译期开关CONFIG_SPI_DEBUG_LOG_LEVEL - 更新固件版本至0.5.2,记录相关变更内容 - 调整SPI和CAN反馈任务的栈大小配置 --- .vscode/settings.json | 3 +- docs/can_protocol.md | 38 +++++++-- main/include/app_config.h | 14 +++- main/include/version.h | 4 +- main/monitor/can_feedback_task.cpp | 54 ++++++++---- main/monitor/can_feedback_task.h | 3 + main/monitor/debug_log.cpp | 11 ++- main/monitor/task_stack.cpp | 4 + main/server/http_handlers.cpp | 69 +++++++++++++++- main/server/http_handlers.h | 2 + main/server/wifi_ota.cpp | 1 + main/transport/canfd_bridge.cpp | 60 +++++++++++++- main/transport/canfd_bridge.h | 20 ++++- main/transport/spi_master.cpp | 128 +++++++++++++++++++++++++++-- main/transport/spi_master.h | 27 +++++- 15 files changed, 391 insertions(+), 47 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 953ffdd..7387b1d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,6 @@ "--query-driver=**", "--compile-commands-dir=g:\\Robot_v2\\DFU\\ESP32_COMM\\build" ], -"idf.flashType": "UART" +"idf.flashType": "UART", +"idf.port": "COM15" } diff --git a/docs/can_protocol.md b/docs/can_protocol.md index c4db22a..d9f14d0 100644 --- a/docs/can_protocol.md +++ b/docs/can_protocol.md @@ -1,6 +1,6 @@ # CAN 通信协议文档 -> 更新日期:2026-07-16 +> 更新日期:2026-10-10 > 涉及固件:GD32 DCU_APP + ESP32 ESP32_COMM > SPI:**Link v2 only** · 时钟 4 MHz > APP 最大业务 payload:`kTxBufSize(256) - V2_OVERHEAD(20) = 236` @@ -10,7 +10,7 @@ ## 1. 概述 -ESP32 通过 SPI Link v2 与 GD32 通信,实现三路 CAN/CANFD 的收发控制。CAN 相关 SPI 命令占用 `0xC0` ~ `0xC3`。 +ESP32 通过 SPI Link v2 与 GD32 通信,实现三路 CAN/CANFD 的收发控制。CAN 相关 SPI 命令占用 `0xC0` ~ `0xC3`、`0xC7`、`0xC8`。 | 帧类型 | 名称 | 方向 | 用途 | |--------|------|------|------| @@ -18,6 +18,8 @@ ESP32 通过 SPI Link v2 与 GD32 通信,实现三路 CAN/CANFD 的收发控 | `0xC1` | kTypeCanFeedback | 双向 | 通道统计 + ID 快照页;GD32 亦可 1Hz 主动推送 | | `0xC2` | kTypeCanSetMode | ESP32 → GD32 | 切换三路 CAN 接收过滤模式 | | `0xC3` | kTypeIdSnapshot | ESP32 → GD32 | 完整 ID 条目(含 seq/rx_count) | +| `0xC7` | kTypeCanBurstSend | ESP32 → GD32 | 跨通道突发发送(一次事务背靠背入多邮箱) | +| `0xC8` | kTypeCanSetSilent | ESP32 → GD32 | 切换指定通道收发器 Silent(只听不发) | --- @@ -52,7 +54,7 @@ ESP32 通过 SPI Link v2 与 GD32 通信,实现三路 CAN/CANFD 的收发控 ### 应答 payload ``` -[0] 0x00(ACK)或错误码 E0~E5 +[0] 0x00(ACK)或错误码 E0~E6 ``` | 码 | 含义 | @@ -63,6 +65,7 @@ ESP32 通过 SPI Link v2 与 GD32 通信,实现三路 CAN/CANFD 的收发控 | E3 | TEC 过高(>96,对端 ACK 失败率高) | | E4 | 邮箱满 | | E5 | 参数无效 | +| E6 | 通道处于收发器 Silent(只听不发),入口即拒,不计错误计数 | --- @@ -95,7 +98,7 @@ GD32 可 1Hz 主动 `requestEsp32Read(0xC1)` 推送;ESP32 亦支持被动查 [21..24] error_count LE [25] tec [26] rec -[27] bus_state bit0=init … bit3=bus_off bit4=EW +[27] bus_state bit0=init bit1=控制器睡眠 bit2=EP bit3=bus_off bit4=EW bit5=只听不发(Silent) [28..] 条目 ``` @@ -120,6 +123,30 @@ rsp[0] actual 生效 mode --- +## 5.1 0xC8 — 收发器 Silent(只听不发) + +GD32 把该通道 TCAN1057AV 的 **S 脚**(CAN0=PA8 / CAN1=PB14 / CAN2=PB9)置高: +驱动器关断,**接收机照常工作、RXD 仍镜像总线**(手册 Table 8-4),所以静默期间接收与 +0xC1 上报完全不受影响。S 脚内部上拉,MCU 复位/浮空即为 Silent。 + +``` +req[0] channel 0/1/2 +req[1] silent 1=进 Silent(只听不发) 0=回 Normal +rsp[0] channel 回显 +rsp[1] actual 实际生效(1=Silent;与 GD32 发送门控同一个值) +rsp[2] ch_en 该通道是否使能 +``` + +- ERROR:`E0`=payload 过短 · `E1`=通道非法/未使能 · `E2`=S 脚回读与意图不符(内部标志已回滚,不会留下"报了静默其实没静默"的状态) +- 静默生效期间,该通道所有发送入口(0xC0 / 0xC7 / 桥接 / 电机 relay)回 **E6**,且不推进任何错误计数 +- 进 Silent 前 GD32 先 abort 本通道 PENDING 邮箱(驱动器已关,留着只会白涨 TEC) +- 退 Silent 时 GD32 忙等收发器模式建立时间 `tMODE = 20 µs`(手册 6.9)再放开发送门 +- **不持久化**:上电 init 一律回 Normal +- 状态读取走 0xC1 的 `bus_state` bit5,ESP32 侧不另开轮询端点 +- ESP32 HTTP:`POST /can/silent` body `{"ch":0,"on":1}`;OTA / send_batch 独占 SPI 期间返回 `{"ok":false,"err":"paused"}` + +--- + ## 6. 0xC3 — ID 快照(完整字段) 固定头 4B + 条目 `(16 + dlc)`(含 id/seq/rx_count/dlc/flags/rsv/data)。 @@ -131,7 +158,8 @@ rsp[0] actual 生效 mode | 端 | 文件 | |----|------| -| GD32 0xC1/C2 | `DCU_APP/.../app_spi_cmd_can_feedback.cpp` | +| GD32 0xC1/C2/C8 | `DCU_APP/.../app_spi_cmd_can_feedback.cpp`(0xC8 复用同一 handler) | +| GD32 S 脚/发送门控 | `DCU_APP/.../device/canfd.cpp` → `setTxSilent()` / `isTxSilent()` | | GD32 0xC3 | `app_spi_cmd_id_snapshot.cpp` | | GD32 0xC0 | `app_spi_cmd_can_bridge.cpp` | | ESP32 解析/桥 | `ESP32_COMM/main/transport/canfd_bridge.*` | diff --git a/main/include/app_config.h b/main/include/app_config.h index eeea527..9d4bc1b 100644 --- a/main/include/app_config.h +++ b/main/include/app_config.h @@ -57,13 +57,13 @@ inline constexpr uint16_t HTTP_SERVER_PORT = 80; ///< HTTP 服 inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 7168; ///< httpd:peak~5.2KB + ~2KB → 7KB(原 8/10KB) inline constexpr uint32_t TASK_STACK_WIFI_OTA = 3072; ///< wifi_ota:peak~1.2KB + ~1.8KB → 3KB(原 4/8KB) inline constexpr uint32_t TASK_STACK_OTA_RELAY = 2560; ///< ota_relay:2.5KB(轻载 peak~1.3KB;转发突发不砍) -inline constexpr uint32_t TASK_STACK_SPI_WORKER = 4096; ///< spi_worker:peak~1.9KB + ~2KB → 4KB(原 5KB) +inline constexpr uint32_t TASK_STACK_SPI_WORKER = 5120; ///< spi_worker:5KB(覆盖超时重试+日志深层调用栈安全余量) inline constexpr uint32_t TASK_STACK_BLE_OTA = 2560; ///< ble_ota:peak~0.9KB + ~1.5KB → 2.5KB(原 3KB) inline constexpr uint32_t TASK_STACK_BLE_WORKER = 2560; ///< ble_ota_worker:peak 1004B + ~1.5KB → 2.5KB(原 3KB) inline constexpr uint32_t TASK_STACK_HEALTH_MON = 3072; ///< health_mon:peak~1.8KB,余量~1.2KB,不砍 inline constexpr uint32_t TASK_STACK_CPU_STATS = 3072; ///< cpu_stats:peak~1.8KB,余量~1.2KB,不砍 inline constexpr uint32_t TASK_STACK_WIFI_RECONNECT = 2560; ///< wifi_reconnect:轻载,2.5KB 保持 -inline constexpr uint32_t TASK_STACK_CAN_FB = 3072; ///< can_fb:被动 takeEvent(0xC1) +inline constexpr uint32_t TASK_STACK_CAN_FB = 4096; ///< can_fb:4KB(覆盖 0xC4/0xC5 周期查询超时与日志调用栈) inline constexpr uint32_t TASK_STACK_BLE_FINISH = 4096; ///< 按需:SHA finalize(保持) inline constexpr uint32_t TASK_STACK_BLE_GD32_WAIT = 3072; ///< 按需:GD32 中继等待 inline constexpr uint32_t TASK_STACK_BLE_RESTART = 2048; ///< 按需:延时重启 @@ -108,6 +108,16 @@ inline constexpr size_t SPI_OTA_FRAME_MAX = 256; ///< 单 CS 最大帧 inline constexpr size_t SPI_BOOT_TX_BUF_SIZE = 128; ///< BOOT TX DMA 满缓冲(读预算用) inline constexpr size_t SPI_APP_TX_BUF_SIZE = 256; ///< APP TX DMA 满缓冲(读预算用) +// ------------------------------------------------------------ +// SPI 调试日志级别(编译期裁剪,改完重编生效;用法见 spi_master.cpp 顶部门) +// 3 = 全开(逐事务 DBG + INFO 明细,SPI 联调用) +// 2 = 只留 ERR/WARN(★默认:真实故障仍可见,心跳/HELLO/INIT_STATUS 明细静默) +// 1 = 只留 ERR 0 = 全静默 +// GD32 侧对应开关:DCU_APP/GD32E508_DCU_APP/sys/config.h +// CONFIG_SPI_APP_LOG_ENABLE / CONFIG_SPI_DIAG_PERIODIC_PRINT +// ------------------------------------------------------------ +#define CONFIG_SPI_DEBUG_LOG_LEVEL 2 + // ============================================================ // OTA 协调器 // ============================================================ diff --git a/main/include/version.h b/main/include/version.h index 86aa6ba..50c58a8 100644 --- a/main/include/version.h +++ b/main/include/version.h @@ -14,8 +14,8 @@ // 固件版本 #define FW_VERSION_MAJOR 0 #define FW_VERSION_MINOR 5 -#define FW_VERSION_PATCH 0 -#define FW_VERSION_STRING "0.5.0" // 内存:s_ota_upload_recv 从常驻静态 4KB 改为 OTA 会话内 RAII malloc,稳态省 4KB 内部 DRAM(httpd 单线程+协调器互斥,结束/失败自动 free)。0.4.9 tx_total/tx_retry/tx_fail/tx_fail_last_type 计数进 /system/status:透明重试(kTxRetryMax=3)掩盖真实失败率,DBG 日志被 CONFIG_LOG_MAXIMUM_LEVEL=3 编译期裁掉→"默默失败无打印"。0.4.8 ring_steal_diag=[rxLen,rb0,rb1] 撕裂形态签名 // 0.4.5 CS 事务间最小间隔 1ms(transferClockedFlags 单点):GD32 CS EXTI re-arm 被 ECAT/CAN 中断延迟时,ESP32 背靠背 CS 撞进 re-arm 窗口使 RX DMA mid-pulse 重装→查询帧撕裂→0x82 0xE8→迟到偷读风暴。0.4.4 快速响应类型禁用盲读。0.4.3 0x82 量化归因插桩。0.4.2 serviceRing 偷读迟到 RESPONSE 的 seq 失配修复:poll 超时放弃后按铃读把响应当 EVENT 偷走且不推进 m_txSeq→下一条命令复用旧序号被 GD32 判重吞掉回放缓存旧 0x82(命令不再执行根因)。0.3.85 INIT_STATUS fault_codes hex;0.3.84 模块级故障码;0.3.83 GD32 APP 初始化状态可视;0.3.82 UART0 RX 泵→/logs;0.3.81 GD32 OTA 包级防呆 +#define FW_VERSION_PATCH 2 +#define FW_VERSION_STRING "0.5.2" // 0.5.2 CAN 通道收发器 Silent(只听不发):新增 SPI 0xC8 桥接 canFdSetTxSilent + POST /can/silent,0xC1 bus_state 增 bit5=tx_silent;静默通道发送回 0xE6。0.5.1 SPI 调试日志编译期门:app_config.h 新增 CONFIG_SPI_DEBUG_LOG_LEVEL=2,spi_master.cpp 的 84 条 DEBUG_*("SPI") 中 DBG/INFO 被裁掉(心跳/HELLO/INIT_STATUS 明细静默),ERR/WARN 保留 // 内存:s_ota_upload_recv 从常驻静态 4KB 改为 OTA 会话内 RAII malloc,稳态省 4KB 内部 DRAM(httpd 单线程+协调器互斥,结束/失败自动 free)。0.4.9 tx_total/tx_retry/tx_fail/tx_fail_last_type 计数进 /system/status:透明重试(kTxRetryMax=3)掩盖真实失败率,DBG 日志被 CONFIG_LOG_MAXIMUM_LEVEL=3 编译期裁掉→"默默失败无打印"。0.4.8 ring_steal_diag=[rxLen,rb0,rb1] 撕裂形态签名 // 0.4.5 CS 事务间最小间隔 1ms(transferClockedFlags 单点):GD32 CS EXTI re-arm 被 ECAT/CAN 中断延迟时,ESP32 背靠背 CS 撞进 re-arm 窗口使 RX DMA mid-pulse 重装→查询帧撕裂→0x82 0xE8→迟到偷读风暴。0.4.4 快速响应类型禁用盲读。0.4.3 0x82 量化归因插桩。0.4.2 serviceRing 偷读迟到 RESPONSE 的 seq 失配修复:poll 超时放弃后按铃读把响应当 EVENT 偷走且不推进 m_txSeq→下一条命令复用旧序号被 GD32 判重吞掉回放缓存旧 0x82(命令不再执行根因)。0.3.85 INIT_STATUS fault_codes hex;0.3.84 模块级故障码;0.3.83 GD32 APP 初始化状态可视;0.3.82 UART0 RX 泵→/logs;0.3.81 GD32 OTA 包级防呆 // Git 信息(手动填写) #define GIT_HASH "770ae14" diff --git a/main/monitor/can_feedback_task.cpp b/main/monitor/can_feedback_task.cpp index b36c922..a9cfe59 100644 --- a/main/monitor/can_feedback_task.cpp +++ b/main/monitor/can_feedback_task.cpp @@ -43,51 +43,57 @@ constexpr size_t kPayloadMax = SPI_OTA_FRAME_MAX; ///< 与变长读一致, constexpr const char *kTag = "CAN_FB"; static volatile bool s_paused = false; +static TaskHandle_t s_taskHandle = nullptr; + +/* 任务独占静态响应缓冲(消除每次在任务栈上分配 256B 大数组,防栈溢出) */ +static uint8_t s_motor_resp_buf[kPayloadMax] = {}; +static uint8_t s_adc_resp_buf[kPayloadMax] = {}; +static uint8_t s_push_ring_buf[kPayloadMax] = {}; /** * @brief 按页查询 GD32 电机状态(0xC5 请求驱动,替代 GD32 250ms 推送)。 * 任一页失败立即中断本轮:GD32 未就绪/瞬时错误时不空耗 6×200ms,下轮再试; * page0 会刷新全局字段(active_count/boot_active),部分页成功即部分更新。 */ -static void queryMotorStatePages(void) +static bool queryMotorStatePages(void) { uint8_t req[1] = {0U}; for (uint8_t page = 0U; page < MOTOR_TOTAL_PAGES; ++page) { req[0] = page; uint8_t resp_type = 0U; - uint8_t resp_buf[kPayloadMax] = {}; size_t resp_len = 0U; esp_err_t err = SpiMaster::instance().transact( kSpiMotorStateType, req, sizeof(req), - &resp_type, resp_buf, sizeof(resp_buf), &resp_len, 200U); + &resp_type, s_motor_resp_buf, sizeof(s_motor_resp_buf), &resp_len, 200U); if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < MOTOR_PAGE_HEADER) { - return; + return false; } - (void)onMotorStateEvent(resp_buf, resp_len); + (void)onMotorStateEvent(s_motor_resp_buf, resp_len); } + return true; } /** * @brief 查询 GD32 ADC 调试快照(0xC4 请求驱动,替代 GD32 1Hz 推送)。 * GD32 0xC4 handler 回自身类型 0xC4(异于 0xC1/0xC5 的 0x80),两者皆接受。 */ -static void queryAdcFeedback(void) +static bool queryAdcFeedback(void) { uint8_t req[1] = {0U}; uint8_t resp_type = 0U; - uint8_t resp_buf[kPayloadMax] = {}; size_t resp_len = 0U; esp_err_t err = SpiMaster::instance().transact( kSpiAdcFeedbackType, req, sizeof(req), - &resp_type, resp_buf, sizeof(resp_buf), &resp_len, 200U); + &resp_type, s_adc_resp_buf, sizeof(s_adc_resp_buf), &resp_len, 200U); if (err != ESP_OK || resp_len < 1U) { - return; + return false; } if (resp_type != kSpiAdcFeedbackType && resp_type != SPI_OTA_RSP_ACK) { - return; + return false; } - (void)onGd32AdcFeedbackEvent(resp_buf, resp_len); + (void)onGd32AdcFeedbackEvent(s_adc_resp_buf, resp_len); + return true; } /** @@ -97,17 +103,16 @@ static void queryAdcFeedback(void) static void drainPushEvents(uint32_t wait_ms) { uint8_t ring_type = 0U; - uint8_t ring_payload[kPayloadMax] = {}; size_t ring_len = 0U; - if (!SpiMaster::instance().takeEvent(&ring_type, ring_payload, - sizeof(ring_payload), &ring_len, + if (!SpiMaster::instance().takeEvent(&ring_type, s_push_ring_buf, + sizeof(s_push_ring_buf), &ring_len, wait_ms)) { return; } if (ring_type == kSpiMotorVersionType) { - (void)onMotorVersionEvent(ring_payload, ring_len); + (void)onMotorVersionEvent(s_push_ring_buf, ring_len); } else if (ring_type == kSpiMotorOtaStatusType) { - onMotorOtaStatusPush(ring_payload, ring_len); + onMotorOtaStatusPush(s_push_ring_buf, ring_len); } } @@ -139,6 +144,16 @@ static void taskLoop(void *) continue; } + /* 链路在线自适应退避:若 SPI 处于离线状态,进入休眠退避,避免死循环重试和日志风暴 */ + if (!SpiMaster::instance().isLinkOnline()) { + vTaskDelay(pdMS_TO_TICKS(2000)); + /* 尝试轻量查询一次作为探活 */ + (void)queryMotorStatePages(); + last_motor_q = xTaskGetTickCount(); + last_adc_q = xTaskGetTickCount(); + continue; + } + /* 周期查询:到期才执行(时间差对比,暂停恢复后立即补一次) */ const TickType_t now = xTaskGetTickCount(); if ((now - last_motor_q) >= pdMS_TO_TICKS(kQueryMotorStateMs)) { @@ -157,6 +172,11 @@ static void taskLoop(void *) } // namespace +TaskHandle_t getCanFeedbackTaskHandle() +{ + return s_taskHandle; +} + /** * @brief 启动事件监听任务(幂等)。 * @return true 表示任务已存在或创建成功。 @@ -172,7 +192,7 @@ bool startCanFeedbackTask() TASK_STACK_CAN_FB, nullptr, 4, - nullptr, + &s_taskHandle, 0); if (xRet != pdPASS) { DEBUG_ERR(kTag, "任务创建失败"); diff --git a/main/monitor/can_feedback_task.h b/main/monitor/can_feedback_task.h index 3bc3035..46484d9 100644 --- a/main/monitor/can_feedback_task.h +++ b/main/monitor/can_feedback_task.h @@ -15,6 +15,9 @@ void pauseCanFeedbackTask(bool pause); * 请求驱动的 SPI 查询(canFdQueryFeedback)在暂停期间应跳过。 */ bool canFeedbackPaused(); +/** @brief 获取 can_fb 任务句柄(用于栈水位监控)。 */ +TaskHandle_t getCanFeedbackTaskHandle(); + #ifdef __cplusplus extern "C" { #endif diff --git a/main/monitor/debug_log.cpp b/main/monitor/debug_log.cpp index 0b86570..319721b 100644 --- a/main/monitor/debug_log.cpp +++ b/main/monitor/debug_log.cpp @@ -676,9 +676,13 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len) uart_ret = uartSend(data, len); } - /* UDP:堆紧 / 无 IP 时跳过 sendto */ + /* UDP:堆紧 / 无 IP 时跳过 sendto;增加关闭后 10s 冷却退避,防连续失败时反复创建/销毁 socket 造成堆抖动 */ + static TickType_t s_lastUdpCloseTick = 0; + const TickType_t now_tick = xTaskGetTickCount(); + const bool udp_in_cooloff = (s_lastUdpCloseTick != 0) && + ((now_tick - s_lastUdpCloseTick) < pdMS_TO_TICKS(10000)); if (m_initialized && free_now >= kSkipUdpFree && udpNetifHasIp()) { - if (m_udpSock < 0) { + if (m_udpSock < 0 && !udp_in_cooloff) { (void)ensureUdpSocket(); } @@ -703,12 +707,13 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len) } if (m_udpFailStreak >= kUdpFailRecreate) { closeUdpSocket(); + s_lastUdpCloseTick = xTaskGetTickCount(); m_udpFailStreak = 0; const TickType_t now = xTaskGetTickCount(); if (m_lastUdpFailWarnTick == 0 || (now - m_lastUdpFailWarnTick) >= warnIntervalTicks()) { m_lastUdpFailWarnTick = now; - static const char kMsg[] = "[W][LOG] UDP 连续失败,已重建 socket\n"; + static const char kMsg[] = "[W][LOG] UDP 连续失败,暂停发送 10s 后重建\n"; ringPush(kMsg, (int)sizeof(kMsg) - 1); } } diff --git a/main/monitor/task_stack.cpp b/main/monitor/task_stack.cpp index 0c765dd..ef916f5 100644 --- a/main/monitor/task_stack.cpp +++ b/main/monitor/task_stack.cpp @@ -15,6 +15,7 @@ #include "wifi_ota.h" #include "ble_ota.h" #include "spi_master.h" +#include "can_feedback_task.h" #include "driver/uart.h" #include "esp_system.h" @@ -77,6 +78,9 @@ size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max) if (n < max) { out[n++] = { "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), TASK_STACK_BLE_WORKER }; } + if (n < max) { + out[n++] = { "can_fb", getCanFeedbackTaskHandle(), TASK_STACK_CAN_FB }; + } return n; } diff --git a/main/server/http_handlers.cpp b/main/server/http_handlers.cpp index daa21b6..eb250d7 100644 --- a/main/server/http_handlers.cpp +++ b/main/server/http_handlers.cpp @@ -783,7 +783,7 @@ esp_err_t can_feedback_handler(httpd_req_t *req) for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) { can_feedback_result_t result = {}; bool valid = false; - if (ch_en[i] && !spi_paused) { + if (ch_en[i] && !spi_paused && SpiMaster::instance().isLinkOnline()) { uint8_t entryIdx = 0; valid = (canFdQueryFeedback(i, 0, &result, &entryIdx) == 0); } @@ -881,7 +881,7 @@ esp_err_t can_feedback_handler(httpd_req_t *req) /* 按需查询:page 0 重置 + 统计,page 1 追加 entries(OTA 暂停期间跳过) */ can_feedback_result_t result = {}; bool valid = false; - if (ch_en[i] && !spi_paused) { + if (ch_en[i] && !spi_paused && SpiMaster::instance().isLinkOnline()) { uint8_t entryIdx = 0; int rc0 = canFdQueryFeedback(i, 0, &result, &entryIdx); if (rc0 == 0) { @@ -1070,6 +1070,64 @@ esp_err_t can_mode_handler(httpd_req_t *req) httpd_resp_send(req, resp, len); return ESP_OK; } + +// ============================================================ +// POST /can/silent — 切换指定 CAN 通道收发器 Silent(只听不发,SPI 0xC8) +// 当前状态不在本端点上报:由 /can/feedback 的 bus_state bit5 单一出处提供, +// 前端据此渲染开关样式,避免本地乐观状态与后端不一致。 +// ============================================================ +esp_err_t can_silent_handler(httpd_req_t *req) +{ + http_set_cors_headers(req); + + char buf[128] = {0}; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败"); + return ESP_FAIL; + } + buf[ret] = '\0'; + + JsonView root = jsonParse(buf); + JsonView ch_item = jsonGet(&root, "ch"); + JsonView on_item = jsonGet(&root, "on"); + if (!root.valid || !jsonIsNumber(&ch_item) || !jsonIsNumber(&on_item)) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 ch/on 字段"); + return ESP_FAIL; + } + + const int ch = (int)jsonGetNumberValue(&ch_item); + const bool on = (jsonGetNumberValue(&on_item) != 0); + + char resp[128]; + int len; + + /* OTA / send_batch 独占 SPI 且正在用 CAN 发帧:此时翻静默会让 relay 半途 + * 拒发(0xE6),现场只会看到"OTA 莫名失败"。直接拒绝,不抢这条时间线。 */ + if (canFeedbackPaused()) { + len = snprintf(resp, sizeof(resp), + "{\"ok\":false,\"ch\":%d,\"on\":%d,\"err\":\"paused\"}", + ch, on ? 1 : 0); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, resp, len); + return ESP_OK; + } + + uint8_t actual = 0U; + int rc = canFdSetTxSilent(static_cast(ch), on, &actual, 2000); + if (rc != 0) { + len = snprintf(resp, sizeof(resp), + "{\"ok\":false,\"ch\":%d,\"on\":%d,\"err\":%d}", + ch, on ? 1 : 0, rc); + } else { + len = snprintf(resp, sizeof(resp), + "{\"ok\":true,\"ch\":%d,\"requested\":%d,\"actual\":%u}", + ch, on ? 1 : 0, (unsigned)actual); + } + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, resp, len); + return ESP_OK; +} /** * @brief POST /can/send_batch 流式 body 读取器。 * @@ -2021,9 +2079,10 @@ esp_err_t system_status_handler(httpd_req_t *req) "\"resyncs\":%lu,\"max_resp_latency_us\":%lu," "\"rsp_err_0x82\":%lu,\"rsp_err_last_req\":\"0x%02X\",\"rsp_err_last_p0\":\"0x%02X\"," "\"rsp_err_last_p1\":%u,\"rsp_err_last_p2\":\"0x%02X\",\"rsp_err_last_p3\":\"0x%02X\"," + "\"rsp_err_last_p4\":%u,\"rsp_err_last_p5\":%u," "\"ring_steals\":%lu,\"ring_steal_last_type\":\"0x%02X\",\"ring_steal_last_ack\":%u," "\"ring_steal_last_p0\":\"0x%02X\",\"ring_steal_last_req\":\"0x%02X\"," - "\"ring_steal_diag\":[%u,\"0x%02X\",\"0x%02X\"]," + "\"ring_steal_diag\":[%u,\"0x%02X\",\"0x%02X\",%u,%u]," "\"tx_total\":%lu,\"tx_retry\":%lu,\"tx_fail\":%lu,\"tx_fail_last_type\":\"0x%02X\"}", (unsigned)ls.version, role_str, @@ -2045,6 +2104,8 @@ esp_err_t system_status_handler(httpd_req_t *req) (unsigned)ls.stats.rsp_err_last_p1, (unsigned)ls.stats.rsp_err_last_p2, (unsigned)ls.stats.rsp_err_last_p3, + (unsigned)ls.stats.rsp_err_last_p4, + (unsigned)ls.stats.rsp_err_last_p5, (unsigned long)ls.stats.ring_steals, (unsigned)ls.stats.ring_steal_last_type, (unsigned)ls.stats.ring_steal_last_ack, @@ -2053,6 +2114,8 @@ esp_err_t system_status_handler(httpd_req_t *req) (unsigned)ls.stats.ring_steal_last_p1, (unsigned)ls.stats.ring_steal_last_p2, (unsigned)ls.stats.ring_steal_last_p3, + (unsigned)ls.stats.ring_steal_last_p4, + (unsigned)ls.stats.ring_steal_last_p5, (unsigned long)ls.stats.tx_total, (unsigned long)ls.stats.tx_retry, (unsigned long)ls.stats.tx_fail, diff --git a/main/server/http_handlers.h b/main/server/http_handlers.h index 362a476..717cd8e 100644 --- a/main/server/http_handlers.h +++ b/main/server/http_handlers.h @@ -32,6 +32,8 @@ esp_err_t system_heap_low_log_handler(httpd_req_t *req); // CAN 反馈快照(被动 0xC1)与接收模式 esp_err_t can_feedback_handler(httpd_req_t *req); esp_err_t can_mode_handler(httpd_req_t *req); +// CAN 通道收发器 Silent(0xC8,只听不发) +esp_err_t can_silent_handler(httpd_req_t *req); // 多电机状态(被动 0xC5) esp_err_t motor_state_handler(httpd_req_t *req); diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index 5f7efe3..849e616 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -481,6 +481,7 @@ esp_err_t WifiOta::httpServerStart() {"/can/feedback", HTTP_GET, can_feedback_handler}, {"/can/mode", HTTP_GET, can_mode_handler}, {"/can/mode", HTTP_POST, can_mode_handler}, + {"/can/silent", HTTP_POST, can_silent_handler}, {"/spi/send", HTTP_POST, spi_send_handler}, {"/motor/state", HTTP_GET, motor_state_handler}, {"/motor/version", HTTP_GET, motor_version_handler}, diff --git a/main/transport/canfd_bridge.cpp b/main/transport/canfd_bridge.cpp index 22deb33..f1f76b4 100644 --- a/main/transport/canfd_bridge.cpp +++ b/main/transport/canfd_bridge.cpp @@ -5,6 +5,7 @@ * 0xC0 发 CAN 帧(canFdSend) * 0xC1 反馈页解析(canFdParseFeedbackPage;由 GD32 主动推送,本文件不轮询) * 0xC2 设 RX 模式(canFdSetRxMode) + * 0xC8 设通道收发器 Silent(canFdSetTxSilent,只听不发) * * 0xC1 线格式与 GD32 app_spi_cmd_can_feedback.cpp 对齐: * 固定头 28B + 条目 id(4LE)+dlc(1)+flags(1)+rsvd(2)+data(dlc) @@ -22,6 +23,7 @@ namespace { constexpr uint8_t kSpiCanFrameType = 0xC0U; constexpr uint8_t kSpiCanBurstType = 0xC7U; constexpr uint8_t kSpiCanSetModeType = 0xC2U; +constexpr uint8_t kSpiCanSetSilentType = 0xC8U; constexpr uint8_t kFeedbackFixedHeader = 28U; constexpr uint8_t kFeedbackMaxPage = 1U; /* GD32 仅 page 0/1 */ @@ -403,8 +405,13 @@ int canFdQueryFeedback(uint8_t channel, uint8_t page, &resp_type, resp_buf, sizeof(resp_buf), &resp_len, 200U); if (ret != ESP_OK) { - DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u SPI失败: %s", - (unsigned)channel, (unsigned)page, esp_err_to_name(ret)); + static TickType_t s_lastFailWarnTick = 0; + const TickType_t now_tick = xTaskGetTickCount(); + if (s_lastFailWarnTick == 0 || (now_tick - s_lastFailWarnTick) >= pdMS_TO_TICKS(5000)) { + s_lastFailWarnTick = now_tick; + DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u SPI失败: %s (限频5s)", + (unsigned)channel, (unsigned)page, esp_err_to_name(ret)); + } return -1; } if (resp_type == SPI_OTA_RSP_ERROR) { @@ -489,3 +496,52 @@ int canFdSetRxMode(can_rx_mode_t mode, (unsigned)resp_len); return -1; } + +/** + * @brief 切换指定 CAN 通道收发器 Silent(SPI 0xC8):只听不发。 + * @return 0 成功;-1 参数非法或 SPI/应答失败;-2 GD32 返回 ERROR。 + * @note 与 canFdSetRxMode 同款单步事务:GD32 在 CS EXTI ISR 内直接回 ACK/ERROR, + * 不会回 WAIT(区别于 0xC0 那条要排空 OTA 残留 WAIT 的路径)。 + * ACK payload = [0]channel [1]实际 silent [2]通道使能位。 + */ +int canFdSetTxSilent(uint8_t channel, + bool silent, + uint8_t *actual, + uint32_t timeout_ms) +{ + if (channel > 2U) { + DEBUG_WARN("CAN", "SET_SILENT 非法 ch=%u", (unsigned)channel); + return -1; + } + + uint8_t req[2] = { channel, static_cast(silent ? 1U : 0U) }; + uint8_t resp_buf[3] = {0}; + uint8_t resp_type = 0U; + size_t resp_len = 0U; + + esp_err_t ret = SpiMaster::instance().transact( + kSpiCanSetSilentType, + req, sizeof(req), + &resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms); + + if (ret == ESP_OK && resp_type == SPI_OTA_RSP_ACK && resp_len >= 2U) { + if (actual != nullptr) { + *actual = resp_buf[1]; + } + return 0; + } + + if (ret == ESP_OK && resp_type == SPI_OTA_RSP_ERROR) { + DEBUG_WARN("CAN", "SET_SILENT ch=%u want=%u GD32 err=0x%02X", + (unsigned)channel, (unsigned)(silent ? 1U : 0U), + (resp_len >= 1U) ? (unsigned)resp_buf[0] : 0U); + return -2; + } + + DEBUG_WARN("CAN", "SET_SILENT ch=%u want=%u %s r=0x%02X n=%u", + (unsigned)channel, (unsigned)(silent ? 1U : 0U), + esp_err_to_name(ret), + (unsigned)resp_type, + (unsigned)resp_len); + return -1; +} diff --git a/main/transport/canfd_bridge.h b/main/transport/canfd_bridge.h index d4a46bf..1233cbd 100644 --- a/main/transport/canfd_bridge.h +++ b/main/transport/canfd_bridge.h @@ -43,7 +43,7 @@ typedef struct { * @param recv_buf 接收数据缓冲区(可为 nullptr)。 * @param recv_max 接收缓冲区最大长度。 * @param timeout_ms 超时时间(毫秒)。 - * @param gd32_err_out 可选输出,GD32 返回的细分错误码(0xE0/0xE1/0xE2/0xE3/0xE4/0xE5); + * @param gd32_err_out 可选输出,GD32 返回的细分错误码(0xE0/0xE1/0xE2/0xE3/0xE4/0xE5/0xE6=通道静默拒发); * 仅在返回 -3 时有效,其它情况清零。可为 nullptr。 * @return 成功时返回接收数据长度,失败返回 -1/-2/-3。 */ @@ -137,7 +137,7 @@ typedef struct { uint32_t error_count; uint8_t tec; /**< 硬件发送错误计数器 (0~255) */ uint8_t rec; /**< 硬件接收错误计数器 (0~255) */ - uint8_t bus_state; /**< 总线状态位图:bit0=init,bit1=sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning */ + uint8_t bus_state; /**< 总线状态位图:bit0=init,bit1=控制器sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning,bit5=tx_silent(收发器Silent,只听不发) */ } can_channel_feedback_t; /** @@ -196,4 +196,20 @@ int canFdSetRxMode(can_rx_mode_t mode, can_rx_mode_t *actual, uint32_t timeout_ms); +/** + * @brief 切换指定 CAN 通道收发器的 Silent 模式(SPI 0xC8):只听不发。 + * GD32 侧把该通道 TCAN1057AV 的 S 脚置高 → 驱动器关断、**接收机照常工作** + * (手册 Table 8-4:RXD 仍镜像总线),所以静默期间接收与 0xC1 上报不受影响。 + * 静默期间该通道所有发送(0xC0/0xC7/桥接/relay)回 0xE6=kErrSilent,不计错误。 + * @param channel 目标通道 0/1/2。 + * @param silent true=进 Silent,false=回 Normal。 + * @param actual 可选输出,GD32 实际生效状态(1=Silent,与其发送门控同一个值)。 + * @param timeout_ms SPI 事务超时。 + * @return 0 成功,<0 失败(-1 参数/SPI 失败,-2 GD32 返回 ERROR)。 + */ +int canFdSetTxSilent(uint8_t channel, + bool silent, + uint8_t *actual, + uint32_t timeout_ms); + #endif // CANFD_BRIDGE_H diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index 154fe90..7db6ad0 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -28,6 +28,32 @@ #include "app_config.h" #include "monitor/debug_log.h" +/* ============================================================ + * SPI 调试日志编译期门(app_config.h: CONFIG_SPI_DEBUG_LOG_LEVEL) + * + * 本文件内 DEBUG_xxx("SPI", ...) 的级别若高于门限,展开为不执行的空语句: + * 用 `if (false)` 保留对实参的引用,避免"变量只用于日志"触发 + * -Wunused/-Werror,同时运行时不格式化、不写 UART/UDP/环。 + * 恢复 SPI 联调明细(与改动前完全一致):把 CONFIG_SPI_DEBUG_LOG_LEVEL 改回 3。 + * ============================================================ */ +#if (CONFIG_SPI_DEBUG_LOG_LEVEL < 4) +#define SPI_LOG_MUTED(tag, fmt, ...) \ + do { if (false) DebugLog::instance().log(DEBUG_LOG_DBG, (tag), (fmt), ##__VA_ARGS__); } while (false) +#undef DEBUG_DBG +#define DEBUG_DBG(tag, fmt, ...) SPI_LOG_MUTED(tag, fmt, ##__VA_ARGS__) +#endif +#if (CONFIG_SPI_DEBUG_LOG_LEVEL < 3) +#undef DEBUG_INFO +#define DEBUG_INFO(tag, fmt, ...) SPI_LOG_MUTED(tag, fmt, ##__VA_ARGS__) +#endif +#if (CONFIG_SPI_DEBUG_LOG_LEVEL < 2) +#undef DEBUG_WARN +#define DEBUG_WARN(tag, fmt, ...) SPI_LOG_MUTED(tag, fmt, ##__VA_ARGS__) +#endif +#if (CONFIG_SPI_DEBUG_LOG_LEVEL < 1) +#undef DEBUG_ERR +#define DEBUG_ERR(tag, fmt, ...) SPI_LOG_MUTED(tag, fmt, ##__VA_ARGS__) +#endif static constexpr size_t kSpiTransferMaxBytes = 256U; @@ -391,6 +417,8 @@ esp_err_t SpiMaster::serviceRingOnce() m_stats.ring_steal_last_p1 = (found_len > 1U) ? found_payload[1] : 0U; /* 0.4.8 */ m_stats.ring_steal_last_p2 = (found_len > 2U) ? found_payload[2] : 0U; m_stats.ring_steal_last_p3 = (found_len > 3U) ? found_payload[3] : 0U; + m_stats.ring_steal_last_p4 = (found_len > 4U) ? found_payload[4] : 0U; + m_stats.ring_steal_last_p5 = (found_len > 5U) ? found_payload[5] : 0U; static uint32_t s_steal_logs = 0U; if ((++s_steal_logs % 16U) == 1U) { DEBUG_WARN("SPI", "serviceRing 偷读迟到 RESPONSE t=0x%02X p0=0x%02X " @@ -641,6 +669,13 @@ esp_err_t SpiMaster::submitJob(Job &job) /* 尚未执行:标 abandoned,worker dequeue 时跳过 executeJob—— * 本函数即将返回,out_* 将指向已消亡栈帧,再执行即 use-after-return。 */ pool_job->abandoned = true; + /* ★ 立即清除输出缓冲指针,彻底杜绝 worker 迟到执行时踩坏已消亡栈帧 */ + pool_job->out_type = nullptr; + pool_job->out_payload = nullptr; + pool_job->out_len = nullptr; + pool_job->out_uptime = nullptr; + pool_job->out_version = nullptr; + pool_job->out_init_status = nullptr; } taskEXIT_CRITICAL(&s_spiJobMux); @@ -676,6 +711,13 @@ esp_err_t SpiMaster::submitJob(Job &job) taskENTER_CRITICAL(&s_spiJobMux); if (!pool_job->finished) { pool_job->abandoned = true; + /* ★ 立即清除输出缓冲指针,杜绝写入消亡栈帧 */ + pool_job->out_type = nullptr; + pool_job->out_payload = nullptr; + pool_job->out_len = nullptr; + pool_job->out_uptime = nullptr; + pool_job->out_version = nullptr; + pool_job->out_init_status = nullptr; } taskEXIT_CRITICAL(&s_spiJobMux); return ESP_ERR_TIMEOUT; @@ -1651,6 +1693,8 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p m_stats.rsp_err_last_p1 = (*resp_len > 1U) ? resp_payload[1] : 0U; m_stats.rsp_err_last_p2 = (*resp_len > 2U) ? resp_payload[2] : 0U; m_stats.rsp_err_last_p3 = (*resp_len > 3U) ? resp_payload[3] : 0U; + m_stats.rsp_err_last_p4 = (*resp_len > 4U) ? resp_payload[4] : 0U; + m_stats.rsp_err_last_p5 = (*resp_len > 5U) ? resp_payload[5] : 0U; } m_linkState.exitBusy(); return ESP_OK; @@ -1661,15 +1705,65 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p m_linkState.exitBusy(); m_stats.ready_timeouts++; - /* 短切片超时是 OTA 正常紧循环,勿 WARN 刷爆 ring/网页 */ + /* 短切片超时是 OTA 正常紧循环,勿 WARN 刷爆 ring/网页;限频 5 秒避免 SPI 不通时日志风暴耗尽堆栈 */ if (timeout_ms >= 200U) { - DEBUG_WARN("SPI", "OTA response timeout: %lu ms", (unsigned long)timeout_ms); + static TickType_t s_lastTimeoutWarnTick = 0; + const TickType_t now_tick = xTaskGetTickCount(); + if (s_lastTimeoutWarnTick == 0 || + (now_tick - s_lastTimeoutWarnTick) >= pdMS_TO_TICKS(5000)) { + s_lastTimeoutWarnTick = now_tick; + DEBUG_WARN("SPI", "OTA response timeout: %lu ms (限频5s)", (unsigned long)timeout_ms); + } } else { DEBUG_DBG("SPI", "OTA poll slice timeout: %lu ms", (unsigned long)timeout_ms); } return ESP_ERR_TIMEOUT; } +/** + * @brief 检查当前是否允许发起常规业务 SPI 事务。 + * 离线熔断机制:SPI 连续失败达到阈值后进入冷却期,冷却期内快速失败, + * 避免周期性业务查询(ADC/电机/CAN反馈)死循环重试阻塞 CPU 与排队破坏堆栈。 + */ +bool SpiMaster::allowRegularTransact(uint8_t type) const +{ + /* 关键链路/握手命令(HELLO、ENTER_BOOT、OTA 等)不受熔断限制 */ + if (type == V2_TYPE_LINK_HELLO || type == SPI_OTA_CMD_REQ || + type == SPI_OTA_CMD_DATA || type == SPI_OTA_CMD_CANCEL || + type == SPI_APP_ENTER_BOOT_CMD) { + return true; + } + if (m_linkOnline) { + return true; + } + /* 处于离线熔断状态:若已过冷却期,放行一笔作为探针尝试恢复 */ + const TickType_t now = xTaskGetTickCount(); + if ((now - m_lastFailureTick) >= pdMS_TO_TICKS(kCircuitBreakerCooloffMs)) { + return true; + } + return false; +} + +void SpiMaster::recordSpiSuccess() +{ + m_consecutiveFailures = 0; + if (!m_linkOnline) { + m_linkOnline = true; + DEBUG_INFO("SPI", "SPI 链路恢复在线 (circuit breaker reset)"); + } +} + +void SpiMaster::recordSpiFailure() +{ + m_consecutiveFailures = m_consecutiveFailures + 1; + m_lastFailureTick = xTaskGetTickCount(); + if (m_consecutiveFailures >= kCircuitBreakerFailThreshold && m_linkOnline) { + m_linkOnline = false; + DEBUG_WARN("SPI", "SPI 连续失败 %lu 次,触发熔断降频 (circuit breaker trip, 冷却 %lums)", + (unsigned long)m_consecutiveFailures, (unsigned long)kCircuitBreakerCooloffMs); + } +} + /** * @brief ESP32→GD32 发送命令 + 轮询读取真实响应(两步法,原子持锁)。 * @@ -1684,6 +1778,10 @@ esp_err_t SpiMaster::transact(uint8_t type, const uint8_t *payload, size_t paylo if (payload_len > SPI_OTA_FRAME_MAX) { return ESP_ERR_INVALID_SIZE; } + if (!allowRegularTransact(type)) { + /* 离线熔断保护:快速返回超时,避免排队阻塞与无休止重试 */ + return ESP_ERR_TIMEOUT; + } if (!onWorker() && m_workerTask != nullptr) { Job job{}; job.kind = JobKind::Transact; @@ -1805,14 +1903,20 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t if (send_ret != ESP_OK && send_ret != ESP_ERR_INVALID_RESPONSE) { if (attempt + 1U < kTxRetryMax) { m_stats.tx_retry++; /* 0.4.9:发送级失败→重发(原静默) */ - DEBUG_DBG("SPI", "TX retry(%u/%u) send fail type=0x%02X %s", - (unsigned)(attempt + 1U), (unsigned)kTxRetryMax, - type, esp_err_to_name(send_ret)); + static TickType_t s_lastSendRetryTick = 0; + const TickType_t now_tick = xTaskGetTickCount(); + if (s_lastSendRetryTick == 0 || (now_tick - s_lastSendRetryTick) >= pdMS_TO_TICKS(5000)) { + s_lastSendRetryTick = now_tick; + DEBUG_DBG("SPI", "TX retry(%u/%u) send fail type=0x%02X %s (限频5s)", + (unsigned)(attempt + 1U), (unsigned)kTxRetryMax, + type, esp_err_to_name(send_ret)); + } vTaskDelay(pdMS_TO_TICKS(3)); continue; } m_stats.tx_fail++; /* 0.4.9:耗尽重试仍失败 */ m_stats.tx_fail_last_type = type; + recordSpiFailure(); return send_ret; } @@ -1831,20 +1935,28 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t esp_err_t poll_ret = otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, poll_timeout, /*expected_type=*/0xFFU); if (poll_ret == ESP_OK) { + recordSpiSuccess(); return ESP_OK; /* 已读到有效响应帧(ACK/ERROR/陈旧) */ } if (attempt + 1U < kTxRetryMax) { m_stats.tx_retry++; /* 0.4.9:poll 无响应→重发(原静默) */ - DEBUG_DBG("SPI", "TX retry(%u/%u) no resp type=0x%02X %s", - (unsigned)(attempt + 1U), (unsigned)kTxRetryMax, - type, esp_err_to_name(poll_ret)); + static TickType_t s_lastPollRetryTick = 0; + const TickType_t now_tick = xTaskGetTickCount(); + if (s_lastPollRetryTick == 0 || (now_tick - s_lastPollRetryTick) >= pdMS_TO_TICKS(5000)) { + s_lastPollRetryTick = now_tick; + DEBUG_DBG("SPI", "TX retry(%u/%u) no resp type=0x%02X %s (限频5s)", + (unsigned)(attempt + 1U), (unsigned)kTxRetryMax, + type, esp_err_to_name(poll_ret)); + } vTaskDelay(pdMS_TO_TICKS(3)); continue; } m_stats.tx_fail++; /* 0.4.9:耗尽重试仍失败 */ m_stats.tx_fail_last_type = type; + recordSpiFailure(); return poll_ret; } + recordSpiFailure(); return ESP_ERR_TIMEOUT; /* 不可达:循环必有返回 */ } diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index 99477ca..b30754c 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -210,8 +210,10 @@ public: uint8_t rsp_err_last_req; ///< 最近一笔 0x82 对应的主动请求 TYPE uint8_t rsp_err_last_p0; ///< 最近一笔 0x82 的 payload[0](GD32 错误码) uint8_t rsp_err_last_p1; ///< 0xE8 时=GD32 收到的 rxLen - uint8_t rsp_err_last_p2; ///< 0xE8 时=GD32 rxBuf[0](期望 SOF0=0xE2?) - uint8_t rsp_err_last_p3; ///< 0xE8 时=GD32 rxBuf[1](期望 SOF1) + uint8_t rsp_err_last_p2; ///< 0xE8 时=GD32 rxBuf[0](期望 SOF0=0xA5) + uint8_t rsp_err_last_p3; ///< 0xE8 时=GD32 rxBuf[1](期望 SOF1=0x5A) + uint8_t rsp_err_last_p4; ///< 0xE8 时=GD32 SpiParseResult 解析失败原因枚举 + uint8_t rsp_err_last_p5; ///< 0xE8 时=GD32 SOF offset (0=正常, >0=漂移, 255=未找到) uint32_t ring_steals; ///< serviceRing 偷读迟到 RESPONSE 总数 uint8_t ring_steal_last_type; ///< 最近一笔偷读帧的 TYPE(0.4.5 归因) uint8_t ring_steal_last_ack; ///< 最近一笔偷读帧 ack_seq 是否匹配 lastSent @@ -220,6 +222,8 @@ public: uint8_t ring_steal_last_p1; ///< 0xE8 时=GD32 诊断 rxLen(0.4.8) uint8_t ring_steal_last_p2; ///< 0xE8 时=GD32 诊断 rxBuf[0](0.4.8) uint8_t ring_steal_last_p3; ///< 0xE8 时=GD32 诊断 rxBuf[1](0.4.8) + uint8_t ring_steal_last_p4; ///< 0xE8 时=GD32 诊断 parse_res + uint8_t ring_steal_last_p5; ///< 0xE8 时=GD32 诊断 sof_offset /* 0.4.9 诊断:透明重试会掩盖真实失败率(DBG 被编译裁掉、无计数)。 * tx_total 进入事务;tx_retry 走到重发;tx_fail 耗尽重试仍失败返回。 */ uint32_t tx_total; ///< otaSendAndPoll 事务总数 @@ -244,6 +248,15 @@ public: /** @brief v2 session 是否已激活。 */ bool v2Active() const { return m_v2Active; } + /** @brief SPI 链路是否判定在线(连续失败未触发熔断,防 SPI 不通时死循环阻塞与堆栈耗尽)。 */ + bool isLinkOnline() const { return m_linkOnline; } + + /** @brief 当前连续失败次数。 */ + uint32_t consecutiveFailures() const { return m_consecutiveFailures; } + + /** @brief 检查当前是否允许发起常规业务 SPI 事务(离线熔断冷却期内快速失败)。 */ + bool allowRegularTransact(uint8_t type) const; + /** @brief 最近一次 HELLO 得到的 GD32 角色(0=unknown, 1=app, 2=boot)。 */ uint8_t gd32Role() const { return m_gd32Role; } @@ -384,6 +397,16 @@ private: volatile bool m_resyncPending = false; /** 上次 HELLO 重同步时刻(冷却 5s,防 GD32 不响应时反复 HELLO)。 */ volatile uint32_t m_lastResyncTick = 0; + /** 链路在线状态及熔断状态:防 SPI 不通时死循环阻塞与堆栈耗尽 */ + volatile bool m_linkOnline = true; + volatile uint32_t m_consecutiveFailures = 0; + volatile uint32_t m_lastFailureTick = 0; + static constexpr uint32_t kCircuitBreakerFailThreshold = 3U; ///< 连续 3 次失败触发熔断 + static constexpr uint32_t kCircuitBreakerCooloffMs = 2000U; ///< 熔断后冷却 2 秒 + + void recordSpiSuccess(); + void recordSpiFailure(); + /** 最近一次主动请求类型(otaTransferUnlocked 发送时记录;poll 读预算按它收紧)。 */ volatile uint8_t m_lastReqType = 0xFFU; /** SPI worker 任务句柄(ISR 唯一通知目标)。 */