feat(canfd): 添加CANFD发送邮箱占用处理机制

当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发送异常情况
This commit is contained in:
cbb
2026-06-25 01:11:31 +08:00
parent dc91883f1e
commit 86c0ec730b
8 changed files with 93 additions and 9 deletions
+39 -1
View File
@@ -7,6 +7,7 @@
#include "device/canfd.h" #include "device/canfd.h"
#include "sys/config.h" #include "sys/config.h"
#include "sys/task_manager.h" #include "sys/task_manager.h"
#include "sys/systick.h"
namespace { namespace {
struct TxFrame { struct TxFrame {
@@ -463,7 +464,44 @@ bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
tx_msg.tx_data[i] = data[i]; 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) { if (mailbox != CAN_NOMAILBOX) {
s_diag.tx_count++; s_diag.tx_count++;
s_diag.last_tx_id = ext_id; s_diag.last_tx_id = ext_id;
@@ -2,9 +2,33 @@
#include "device/app_spi_command.h" #include "device/app_spi_command.h"
#include "device/app_spi_command_defs.h" #include "device/app_spi_command_defs.h"
#include "device/canfd.h" #include "device/canfd.h"
#include <cstdio>
namespace { 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<unsigned>(can_ch),
static_cast<unsigned long>(can_id),
static_cast<unsigned>(can_dlc),
(logged_dlc > 0U) ? data_hex : "-",
result ? result : "UNKNOWN");
bus.postLog(log_msg);
}
class SpiCanBridgeHandler final : public ISpiCommandHandler { class SpiCanBridgeHandler final : public ISpiCommandHandler {
public: public:
bool canHandle(uint8_t frame_type) const override bool canHandle(uint8_t frame_type) const override
@@ -15,7 +39,11 @@ public:
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
{ {
if (ctx.payload_len < 6U) { 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<unsigned>(ctx.payload_len));
bus.postLog(log_msg);
uint8_t err = 0xE0U; uint8_t err = 0xE0U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U); SpiDefs::kRspError, &err, 1U);
@@ -31,7 +59,7 @@ public:
const uint8_t can_dlc = static_cast<uint8_t>(ctx.payload_len - 5U); const uint8_t can_dlc = static_cast<uint8_t>(ctx.payload_len - 5U);
if (can_ch >= 3U) { 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; uint8_t err = 0xE1U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U); SpiDefs::kRspError, &err, 1U);
@@ -40,12 +68,12 @@ public:
const bool ok = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc); const bool ok = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc);
if (ok) { 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; const uint8_t ack = 0x00U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspAck, &ack, 1U); SpiDefs::kRspAck, &ack, 1U);
} else { } 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; uint8_t err = 0xE2U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U); SpiDefs::kRspError, &err, 1U);
@@ -16,7 +16,6 @@ public:
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
{ {
uint8_t alive_payload[19]; uint8_t alive_payload[19];
std::memset(alive_payload, 0, sizeof(alive_payload)); std::memset(alive_payload, 0, sizeof(alive_payload));
alive_payload[0] = 0xA1U; /* magic */ alive_payload[0] = 0xA1U; /* magic */
@@ -92,6 +92,19 @@ static bool shouldIgnoreTransaction(const uint8_t *data, uint8_t len)
!(len == 1U && data[0] == CONFIG_SPI_APP_OTA_BOOT_CMD); !(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 一致) * CRC8 / 帧构建 / 帧解析(与 BOOT SpiOta 一致)
* ============================================================ */ * ============================================================ */
@@ -328,6 +341,10 @@ void AppSpiCommand::processCommand()
uint16_t payload_len = 0U; uint16_t payload_len = 0U;
if (spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len)) { 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 分发给已注册命令处理器 */ /* 合法帧:按 TYPE 分发给已注册命令处理器 */
uint16_t tx_len = 0U; uint16_t tx_len = 0U;
bool handled = false; bool handled = false;
+1 -1
View File
@@ -56,7 +56,7 @@ public:
static constexpr uint16_t kRxBufSize = 128U; static constexpr uint16_t kRxBufSize = 128U;
static constexpr uint16_t kTxBufSize = 64U; static constexpr uint16_t kTxBufSize = 64U;
static constexpr uint16_t kLogBufSize = 128U; static constexpr uint16_t kLogBufSize = 256U;
private: private:
static constexpr uint8_t kMaxCommands = 8U; static constexpr uint8_t kMaxCommands = 8U;
+1
View File
@@ -24,6 +24,7 @@ public:
uint32_t rx_fifo_full_count; /**< 硬件 FIFO0 进入满状态的次数。 */ uint32_t rx_fifo_full_count; /**< 硬件 FIFO0 进入满状态的次数。 */
uint32_t rx_fifo_overfull_count; /**< 硬件 FIFO0 发生 overfull 的次数。 */ uint32_t rx_fifo_overfull_count; /**< 硬件 FIFO0 发生 overfull 的次数。 */
uint32_t tx_drop_count; /**< 发送 FIFO 满导致丢弃的报文计数。 */ uint32_t tx_drop_count; /**< 发送 FIFO 满导致丢弃的报文计数。 */
uint32_t tx_abort_count; /**< 因邮箱全占主动中止占用邮箱的次数(典型:总线无 ACK 导致自动重传卡死)。 */
uint32_t last_rx_id; /**< 最近一次接收报文的标识符。 */ uint32_t last_rx_id; /**< 最近一次接收报文的标识符。 */
uint32_t last_tx_id; /**< 最近一次发送报文的标识符。 */ uint32_t last_tx_id; /**< 最近一次发送报文的标识符。 */
uint8_t last_rx_channel; /**< 最近一次接收报文所在的 CAN 通道号(0/1/2)。 */ uint8_t last_rx_channel; /**< 最近一次接收报文所在的 CAN 通道号(0/1/2)。 */
+1 -1
View File
@@ -127,7 +127,7 @@
/** @brief APP 补丁版本号。 */ /** @brief APP 补丁版本号。 */
#define APP_FW_VERSION_PATCH 1U #define APP_FW_VERSION_PATCH 1U
/** @brief APP 构建编号。 */ /** @brief APP 构建编号。 */
#define APP_FW_VERSION_BUILD 0U #define APP_FW_VERSION_BUILD 2U
/* ============================================================ /* ============================================================
* BOOT 版本在 OTA 配置页中的 Flash 存储地址 * BOOT 版本在 OTA 配置页中的 Flash 存储地址
+2 -1
View File
@@ -241,7 +241,7 @@ void TaskRunner::taskDiagnosticPrint()
static_cast<unsigned long>(vin_adc_mV / 1000U), static_cast<unsigned long>(vin_adc_mV / 1000U),
static_cast<unsigned long>(vin_adc_mV % 1000U)); static_cast<unsigned long>(vin_adc_mV % 1000U));
#else #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, (unsigned long)g_app_uptime_sec,
static_cast<unsigned int>(s.valid), static_cast<unsigned int>(s.valid),
static_cast<long>(temp_x100 / 100), static_cast<long>(temp_x100 / 100),
@@ -260,6 +260,7 @@ void TaskRunner::taskDiagnosticPrint()
static_cast<unsigned long>(diag->rx_fifo_full_count), static_cast<unsigned long>(diag->rx_fifo_full_count),
static_cast<unsigned long>(diag->rx_fifo_overfull_count), static_cast<unsigned long>(diag->rx_fifo_overfull_count),
static_cast<unsigned long>(diag->tx_drop_count), static_cast<unsigned long>(diag->tx_drop_count),
static_cast<unsigned long>(diag->tx_abort_count),
static_cast<unsigned long>(diag->error_count)); static_cast<unsigned long>(diag->error_count));
#endif #endif
} }