From 86c0ec730bc3c25868c76c3dc8e2abb413a2cdd8 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Thu, 25 Jun 2026 01:11:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(canfd):=20=E6=B7=BB=E5=8A=A0CANFD=E5=8F=91?= =?UTF-8?q?=E9=80=81=E9=82=AE=E7=AE=B1=E5=8D=A0=E7=94=A8=E5=A4=84=E7=90=86?= =?UTF-8?q?=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当CAN控制器因总线无ACK导致邮箱卡死时,实现两段式恢复策略: 1) 短自旋轮询邮箱状态,大多数场景下可避免触发abort而不丢帧 2) 若仍全部PENDING则只中止第一个占用邮箱,限制最坏丢帧数量为1帧 添加了tx_abort_count诊断计数器用于监控该情况,并引入SysTick延时功能 refactor(spi-cmd): 优化SPI命令日志输出格式 将CAN发送相关日志改为统一的格式化函数输出,增加数据字段十六进制显示功能, 并扩大日志缓冲区大小从128到256字节以支持更详细的日志信息 chore(spi-command): 添加SPI帧类型分类和非刷写帧日志记录 新增isFlashRelatedFrame函数区分刷写相关帧类型,在处理命令时对非刷写帧进行日志记录 perf(diagnostic): 在诊断打印中添加发送中止计数统计 更新诊断信息打印格式,增加tx_abort计数项,便于监控CAN发送异常情况 --- GD32E508_DCU_APP/device/CANFD.cpp | 40 ++++++++++++++++++- .../device/app_spi_cmd_can_bridge.cpp | 36 +++++++++++++++-- GD32E508_DCU_APP/device/app_spi_cmd_ping.cpp | 1 - GD32E508_DCU_APP/device/app_spi_command.cpp | 17 ++++++++ GD32E508_DCU_APP/device/app_spi_command.h | 2 +- GD32E508_DCU_APP/device/canfd.h | 1 + GD32E508_DCU_APP/sys/config.h | 2 +- GD32E508_DCU_APP/sys/task_runner.cpp | 3 +- 8 files changed, 93 insertions(+), 9 deletions(-) diff --git a/GD32E508_DCU_APP/device/CANFD.cpp b/GD32E508_DCU_APP/device/CANFD.cpp index 7b2dea4..a9e9232 100644 --- a/GD32E508_DCU_APP/device/CANFD.cpp +++ b/GD32E508_DCU_APP/device/CANFD.cpp @@ -7,6 +7,7 @@ #include "device/canfd.h" #include "sys/config.h" #include "sys/task_manager.h" +#include "sys/systick.h" namespace { struct TxFrame { @@ -463,7 +464,44 @@ bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, tx_msg.tx_data[i] = data[i]; } - const uint8_t mailbox = can_message_transmit(kChannels[channelIndex].canPeriph, &tx_msg); + const uint32_t canPeriph = kChannels[channelIndex].canPeriph; + uint8_t mailbox = can_message_transmit(canPeriph, &tx_msg); + + /* 邮箱全占时的恢复处理: + * 当目标控制器初始化时 auto_retrans = ENABLE,且总线上没有其它节点回 ACK, + * 已入邮箱的帧会无限自动重传,TME 位永不置回,导致后续帧一律 CAN_NOMAILBOX。 + * + * 改良策略(两段式,尽量不丢帧): + * 1) 先短自旋 ~2 ms 轮询邮箱状态。健康总线上 1 Mbps + 8 字节帧的实际发送 + * 仅约 130 µs,正常重传几百 µs 内即可 drain,绝大多数“偶发 NOMAILBOX” + * 场景到这一步就已经恢复,不会触发 abort,也就不会丢帧。 + * 2) 若 2 ms 后仍全部 PENDING(典型:物理层不通、TCAN1057A 没焊或 CAN_L/H + * 断线导致 ACK Error 死循环),只 abort 第一个 PENDING 邮箱腾一个位置, + * 最坏丢 1 帧而不是 3 帧,并通过 s_diag.tx_abort_count 暴露给串口 diag, + * 上层(含 SPI 桥)仍能把后续帧正常下发。 */ + if (mailbox == CAN_NOMAILBOX) { + constexpr uint8_t kDrainPolls = 2U; + for (uint8_t poll = 0U; poll < kDrainPolls && mailbox == CAN_NOMAILBOX; ++poll) { + SysTickTimer::delayMs(1U); + mailbox = can_message_transmit(canPeriph, &tx_msg); + } + } + if (mailbox == CAN_NOMAILBOX) { + /* 只 abort 一个 PENDING 邮箱:找到首个 stop 成功的就停手, + * 不再扫描后续邮箱,把丢帧上限收敛到 1。stop 失败的邮箱 + * (硬件正在物理发送)继续尝试下一个,避免一次 abort 都没成功。 */ + for (uint8_t mb = CAN_MAILBOX0; mb <= CAN_MAILBOX2; ++mb) { + if (can_transmit_states(canPeriph, mb) != CAN_TRANSMIT_PENDING) { + continue; + } + if (can_transmission_stop(canPeriph, mb) == SUCCESS) { + s_diag.tx_abort_count++; + break; + } + } + mailbox = can_message_transmit(canPeriph, &tx_msg); + } + if (mailbox != CAN_NOMAILBOX) { s_diag.tx_count++; s_diag.last_tx_id = ext_id; diff --git a/GD32E508_DCU_APP/device/app_spi_cmd_can_bridge.cpp b/GD32E508_DCU_APP/device/app_spi_cmd_can_bridge.cpp index 6f947c8..231029a 100644 --- a/GD32E508_DCU_APP/device/app_spi_cmd_can_bridge.cpp +++ b/GD32E508_DCU_APP/device/app_spi_cmd_can_bridge.cpp @@ -2,9 +2,33 @@ #include "device/app_spi_command.h" #include "device/app_spi_command_defs.h" #include "device/canfd.h" +#include namespace { +constexpr uint8_t kCanLogMaxDlc = 64U; + +void postCanSendLog(AppSpiCommand &bus, const char *result, + uint8_t can_ch, uint32_t can_id, + const uint8_t *can_data, uint8_t can_dlc) +{ + const uint8_t logged_dlc = (can_dlc <= kCanLogMaxDlc) ? can_dlc : kCanLogMaxDlc; + char data_hex[(kCanLogMaxDlc * 2U) + 1U] = {}; + for (uint8_t i = 0U; i < logged_dlc; ++i) { + (void)snprintf(&data_hex[i * 2U], sizeof(data_hex) - (i * 2U), "%02X", can_data[i]); + } + + char log_msg[AppSpiCommand::kLogBufSize] = {}; + (void)snprintf(log_msg, sizeof(log_msg), + "CAN_SEND CH%u ID=0x%08lX DLC=%u DATA=%s -> %s", + static_cast(can_ch), + static_cast(can_id), + static_cast(can_dlc), + (logged_dlc > 0U) ? data_hex : "-", + result ? result : "UNKNOWN"); + bus.postLog(log_msg); +} + class SpiCanBridgeHandler final : public ISpiCommandHandler { public: bool canHandle(uint8_t frame_type) const override @@ -15,7 +39,11 @@ public: void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override { if (ctx.payload_len < 6U) { - bus.postLog("[SPI-APP] CAN_SEND 载荷太短 → ERROR"); + char log_msg[AppSpiCommand::kLogBufSize] = {}; + (void)snprintf(log_msg, sizeof(log_msg), + "CAN_SEND payload too short: len=%u -> ERROR", + static_cast(ctx.payload_len)); + bus.postLog(log_msg); uint8_t err = 0xE0U; bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, SpiDefs::kRspError, &err, 1U); @@ -31,7 +59,7 @@ public: const uint8_t can_dlc = static_cast(ctx.payload_len - 5U); if (can_ch >= 3U) { - bus.postLogFmt("[SPI-APP] CAN_SEND 通道非法: CH%d → ERROR", can_ch); + postCanSendLog(bus, "ERROR(channel)", can_ch, can_id, can_data, can_dlc); uint8_t err = 0xE1U; bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, SpiDefs::kRspError, &err, 1U); @@ -40,12 +68,12 @@ public: const bool ok = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc); if (ok) { - bus.postLogFmt("[SPI-APP] CAN_SEND CH%d → OK", can_ch); + postCanSendLog(bus, "OK", can_ch, can_id, can_data, can_dlc); const uint8_t ack = 0x00U; bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, SpiDefs::kRspAck, &ack, 1U); } else { - bus.postLogFmt("[SPI-APP] CAN_SEND CH%d → BUSY", can_ch); + postCanSendLog(bus, "BUSY", can_ch, can_id, can_data, can_dlc); uint8_t err = 0xE2U; bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, SpiDefs::kRspError, &err, 1U); diff --git a/GD32E508_DCU_APP/device/app_spi_cmd_ping.cpp b/GD32E508_DCU_APP/device/app_spi_cmd_ping.cpp index 320490a..d6d03e4 100644 --- a/GD32E508_DCU_APP/device/app_spi_cmd_ping.cpp +++ b/GD32E508_DCU_APP/device/app_spi_cmd_ping.cpp @@ -16,7 +16,6 @@ public: void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override { - uint8_t alive_payload[19]; std::memset(alive_payload, 0, sizeof(alive_payload)); alive_payload[0] = 0xA1U; /* magic */ diff --git a/GD32E508_DCU_APP/device/app_spi_command.cpp b/GD32E508_DCU_APP/device/app_spi_command.cpp index ea9e37f..79fff37 100644 --- a/GD32E508_DCU_APP/device/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/app_spi_command.cpp @@ -92,6 +92,19 @@ static bool shouldIgnoreTransaction(const uint8_t *data, uint8_t len) !(len == 1U && data[0] == CONFIG_SPI_APP_OTA_BOOT_CMD); } +static bool isFlashRelatedFrame(uint8_t frame_type) +{ + switch (frame_type) { + case kTypeOtaReq: + case kTypeOtaData: + case kTypeOtaCancel: + case kTypeBootReq: + return true; + default: + return false; + } +} + /* ============================================================ * CRC8 / 帧构建 / 帧解析(与 BOOT SpiOta 一致) * ============================================================ */ @@ -328,6 +341,10 @@ void AppSpiCommand::processCommand() uint16_t payload_len = 0U; if (spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len)) { + if (!isFlashRelatedFrame(frame_type)) { + postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type); + } + /* 合法帧:按 TYPE 分发给已注册命令处理器 */ uint16_t tx_len = 0U; bool handled = false; diff --git a/GD32E508_DCU_APP/device/app_spi_command.h b/GD32E508_DCU_APP/device/app_spi_command.h index e5a3713..d7edadd 100644 --- a/GD32E508_DCU_APP/device/app_spi_command.h +++ b/GD32E508_DCU_APP/device/app_spi_command.h @@ -56,7 +56,7 @@ public: static constexpr uint16_t kRxBufSize = 128U; static constexpr uint16_t kTxBufSize = 64U; - static constexpr uint16_t kLogBufSize = 128U; + static constexpr uint16_t kLogBufSize = 256U; private: static constexpr uint8_t kMaxCommands = 8U; diff --git a/GD32E508_DCU_APP/device/canfd.h b/GD32E508_DCU_APP/device/canfd.h index 67ba852..4a7c97f 100644 --- a/GD32E508_DCU_APP/device/canfd.h +++ b/GD32E508_DCU_APP/device/canfd.h @@ -24,6 +24,7 @@ public: uint32_t rx_fifo_full_count; /**< 硬件 FIFO0 进入满状态的次数。 */ uint32_t rx_fifo_overfull_count; /**< 硬件 FIFO0 发生 overfull 的次数。 */ uint32_t tx_drop_count; /**< 发送 FIFO 满导致丢弃的报文计数。 */ + uint32_t tx_abort_count; /**< 因邮箱全占主动中止占用邮箱的次数(典型:总线无 ACK 导致自动重传卡死)。 */ uint32_t last_rx_id; /**< 最近一次接收报文的标识符。 */ uint32_t last_tx_id; /**< 最近一次发送报文的标识符。 */ uint8_t last_rx_channel; /**< 最近一次接收报文所在的 CAN 通道号(0/1/2)。 */ diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index 1a9bff4..d12311c 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -127,7 +127,7 @@ /** @brief APP 补丁版本号。 */ #define APP_FW_VERSION_PATCH 1U /** @brief APP 构建编号。 */ -#define APP_FW_VERSION_BUILD 0U +#define APP_FW_VERSION_BUILD 2U /* ============================================================ * BOOT 版本在 OTA 配置页中的 Flash 存储地址 diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index 688bae5..59f71ac 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -241,7 +241,7 @@ void TaskRunner::taskDiagnosticPrint() static_cast(vin_adc_mV / 1000U), static_cast(vin_adc_mV % 1000U)); #else - printf("[APP] uptime=%lus valid=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V, rx_ch=%u, rx_id=0x%08lX, rx_len=%u, tx_ch=%u, can_tx=%lu, can_rx=%lu, sw_drop=%lu, hw_full=%lu, hw_over=%lu, tx_drop=%lu, can_err=%lu\r\n", + printf("[APP] uptime=%lus valid=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V, rx_ch=%u, rx_id=0x%08lX, rx_len=%u, tx_ch=%u, can_tx=%lu, can_rx=%lu, sw_drop=%lu, hw_full=%lu, hw_over=%lu, tx_drop=%lu, tx_abort=%lu, can_err=%lu\r\n", (unsigned long)g_app_uptime_sec, static_cast(s.valid), static_cast(temp_x100 / 100), @@ -260,6 +260,7 @@ void TaskRunner::taskDiagnosticPrint() static_cast(diag->rx_fifo_full_count), static_cast(diag->rx_fifo_overfull_count), static_cast(diag->tx_drop_count), + static_cast(diag->tx_abort_count), static_cast(diag->error_count)); #endif }