feat(canfd): 添加收发器静默模式支持和SPI命令接口
添加了CAN FD收发器静默模式功能,允许通过硬件引脚控制发送使能: - 新增setTxSilent/isTxSilent接口控制TCAN1057收发器Silent模式 - 静默时驱动器关断但接收机继续工作,支持只听不发场景 - 添加SPI命令0xC8用于ESP32远程控制静默模式切换 - 在总线状态位图中增加bit5表示静默状态 - 新增canBusyWaitUs函数用于ISR安全的微秒延时 fix(esc_lan9252): 改进warm jump后LAN9252探测逻辑 修复warm jump导致的LAN9252初始化问题: - 识别warm jump后芯片非POR状态(idx0!=0x1234)的情况 - 自动补发硬件复位对齐到POR状态后再进行正常探测 - 改进调试信息输出,区分POR状态和warm jump残值情况 - 修正失败路径的复位标志初始化逻辑 refactor(spi): 整理SPI命令文件结构 调整SPI命令相关文件的组织结构: - 移动诊断相关文件到更合适的位置 - 重新整理SPI命令处理文件的分组
This commit is contained in:
@@ -70,6 +70,8 @@ project:
|
||||
- file: ./device/spi/app_spi_cmd_motor_version.cpp
|
||||
- file: ./device/spi/app_spi_cmd_motor_ota.cpp
|
||||
- file: ./device/spi/app_spi_cmd_exmc_diag.cpp
|
||||
- file: ./device/spi/app_spi_diag.h
|
||||
- file: ./device/spi/app_spi_cmd_spi_diag.cpp
|
||||
- group: spi_link
|
||||
files:
|
||||
- file: ../../spi_link/spi_link_v2.cpp
|
||||
|
||||
@@ -427,16 +427,42 @@ bool EscLan9252::init()
|
||||
uint16_t idx0 = 0U;
|
||||
uint32_t byteTest = 0U;
|
||||
|
||||
/* 第 0 轮:不复位直接探。warm 残值下 BOOT 刚写过 HBI,BYTE_TEST 应立刻可读。 */
|
||||
/* 第 0 轮:不复位直接探。warm 残值下 BOOT 刚写过 HBI,BYTE_TEST 应立刻可读。
|
||||
* BYTE_TEST 只证明通路;idx0==0x1234 才证明「芯片处于 POR 默认态」。
|
||||
* warm jump 后 idx0 常为 0x0054(BOOT 索引事务残值)——与刚上电不同。
|
||||
* 为了「跳转 = 上电」,残值时补一发 RST# 回到 POR,再走与 cold 相同的探活。 */
|
||||
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
|
||||
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
|
||||
if (byteTest == LAN9252_BYTE_TEST_VALUE) {
|
||||
printf("[ECAT][ESC] probe: BYTE_TEST ok without RST# (idx0=0x%04X) — 总线已活,跳过复位\r\n",
|
||||
const bool porLike = (byteTest == LAN9252_BYTE_TEST_VALUE) && (idx0 == 0x1234U);
|
||||
if (porLike) {
|
||||
printf("[ECAT][ESC] probe: BYTE_TEST ok without RST# (idx0=0x%04X) — POR 态,跳过复位\r\n",
|
||||
(unsigned)idx0);
|
||||
s_resetDone = true;
|
||||
} else {
|
||||
} else if (byteTest == LAN9252_BYTE_TEST_VALUE) {
|
||||
/* 通路活但不是 POR(典型 warm jump:idx0=0x0054)。补 RST# 对齐上电。 */
|
||||
printf("[ECAT][ESC] probe: BYTE_TEST ok but idx0=0x%04X != 0x1234 — warm 残值,"
|
||||
"补 RST# 对齐上电 POR\r\n", (unsigned)idx0);
|
||||
hardwareReset();
|
||||
s_resetDone = true;
|
||||
if (!EscExmcBus::init()) {
|
||||
printf("[ECAT][ESC] init: FAIL, EXMC re-init after RST# (warm residual)\r\n");
|
||||
return false;
|
||||
}
|
||||
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
|
||||
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
|
||||
if (byteTest != LAN9252_BYTE_TEST_VALUE) {
|
||||
printf("[ECAT][ESC] probe: BYTE_TEST=0x%08lX after warm RST#, fall back to multi-round\r\n",
|
||||
(unsigned long)byteTest);
|
||||
} else {
|
||||
printf("[ECAT][ESC] probe: warm RST# ok (idx0=0x%04X, BYTE_TEST=0x87654321) — 与上电同态\r\n",
|
||||
(unsigned)idx0);
|
||||
}
|
||||
}
|
||||
|
||||
if (byteTest != LAN9252_BYTE_TEST_VALUE) {
|
||||
printf("[ECAT][ESC] probe: BYTE_TEST=0x%08lX idx0=0x%04X, 补 RST# 最多 3 轮\r\n",
|
||||
(unsigned long)byteTest, (unsigned)idx0);
|
||||
s_resetDone = false; /* 失败路径首轮强制 RST#,与上电后探不通时一致 */
|
||||
bool alive = false;
|
||||
for (uint8_t attempt = 0U; attempt < 3U; ++attempt) {
|
||||
if (!s_resetDone || attempt > 0U) {
|
||||
|
||||
Binary file not shown.
@@ -19,6 +19,27 @@ static inline void spinDelay(uint32_t loops) {
|
||||
for (volatile uint32_t d = 0; d < loops; ++d) { }
|
||||
}
|
||||
|
||||
/** @brief DWT CYCCNT 忙等微秒(ISR 安全)。
|
||||
* @note setTxSilent 退 Silent 时要等收发器 tMODE=20µs;本函数可能在 SPI CS
|
||||
* EXTI ISR 内被 0xC8 handler 调用,而 SysTickTimer::delayMs 依赖 SysTick
|
||||
* 中断计数,在该上下文会被冻死(同 esc_lan9252.cpp 的 pmBusyWaitUs)。
|
||||
* **不得复位 CYCCNT**:cpu_load / ethercat_diff_stats / SSC 软件定时器都拿它
|
||||
* 当时间基,清零会让它们的窗口计算瞬时报错。只在未使能时补使能位。 */
|
||||
static inline void canBusyWaitUs(uint32_t us)
|
||||
{
|
||||
if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) == 0U) {
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
}
|
||||
const uint32_t start = DWT->CYCCNT;
|
||||
const uint32_t cycles = us * (SystemCoreClock / 1000000U);
|
||||
/* 有界自旋:DWT 因故没在跑时不能变成无限等(最多多转 ~1ms 的圈数) */
|
||||
const uint32_t spin_cap = cycles + (SystemCoreClock / 1000U);
|
||||
for (uint32_t i = 0U; i < spin_cap; ++i) {
|
||||
if ((DWT->CYCCNT - start) >= cycles) break;
|
||||
}
|
||||
}
|
||||
|
||||
/* DLENC>8 的长度映射,与厂商库 g_can_fdlength_table 一致 */
|
||||
constexpr uint8_t kFdLenTable[7] = { 12U, 16U, 20U, 24U, 32U, 48U, 64U };
|
||||
|
||||
@@ -308,6 +329,7 @@ uint32_t CanFd::s_feedbackSeq = 0U;
|
||||
CanFd::IdSnapshotTable CanFd::s_idSnapshot[3]{};
|
||||
bool CanFd::s_channelHwOk[3]{};
|
||||
bool CanFd::s_brsSuppress[3]{};
|
||||
volatile uint8_t CanFd::s_txSilent[3]{};
|
||||
|
||||
/* TX 错误事件标志(ARD=1 单发丢帧通知):TX 完成 ISR 置位,主循环
|
||||
* consumeTxError() 读清;单写单读,无锁。 */
|
||||
@@ -492,7 +514,8 @@ void CanFd::initChannelHardware(uint8_t i)
|
||||
}
|
||||
s_channelHwOk[i] = false;
|
||||
rcu_periph_clock_enable(ch.canRcu);
|
||||
gpio_bit_reset(ch.slpPort, ch.slpPin); /* STB=0 -> Normal */
|
||||
gpio_bit_reset(ch.slpPort, ch.slpPin); /* S=0 → Normal(静默不跨重启,上电一律放开发送) */
|
||||
s_txSilent[i] = 0U;
|
||||
const uint8_t rxLevelBefore = (gpio_input_bit_get(ch.rxPort, ch.rxPin) != RESET) ? 1U : 0U;
|
||||
const uint8_t stbLevel = (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) ? 1U : 0U;
|
||||
|
||||
@@ -1096,6 +1119,13 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
|
||||
if (dlc > 0U && data == nullptr) {
|
||||
return CanTxResult::kErrInvalid;
|
||||
}
|
||||
/* 收发器 Silent(只听不发):本端主动配置的发送禁能,不是总线状态。
|
||||
* 与下面「BUS_OFF/TEC 不做软件拦截」的约定不冲突——那两条是粘性总线侧标志会误判,
|
||||
* 这条是 s_txSilent 单一真源,且驱动器已关断:硬发只会让控制器白涨 TEC。
|
||||
* 拒绝路径不碰任何计数/诊断,静默期间的拒发不是错误。 */
|
||||
if (s_txSilent[channelIndex] != 0U) {
|
||||
return CanTxResult::kErrSilent;
|
||||
}
|
||||
|
||||
CanTxConcurrencyGuard guard(channelIndex);
|
||||
|
||||
@@ -1211,6 +1241,12 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
|
||||
if (dlc > 0U && data == nullptr) {
|
||||
return CanTxResult::kErrInvalid;
|
||||
}
|
||||
/* 静默通道在入队口就拒掉:否则帧会进软件队列,等 TX 完成中断 drainTxQueueIsr
|
||||
* 调 transmitFrame 时才被拒,那一轮会把 head/count 推进并静默丢帧,
|
||||
* 表面上"已受理"实则必丢。 */
|
||||
if (s_txSilent[channelIndex] != 0U) {
|
||||
return CanTxResult::kErrSilent;
|
||||
}
|
||||
|
||||
TxQueue &q = s_txQueues[channelIndex];
|
||||
|
||||
@@ -1351,6 +1387,49 @@ void CanFd::setChannelBrsSuppress(uint8_t channelIndex, bool suppress)
|
||||
CAN_LOGI("[CAN] CH%u set brs_suppress=%u\r\n", (unsigned)channelIndex, (unsigned)suppress);
|
||||
}
|
||||
|
||||
bool CanFd::setTxSilent(uint8_t channelIndex, bool silent)
|
||||
{
|
||||
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
|
||||
return false;
|
||||
}
|
||||
const auto &ch = kChannels[channelIndex];
|
||||
|
||||
if (silent) {
|
||||
/* 先立旗再动脚:并发路径(主循环/其它 ISR)调 transmitFrame 立刻被拒,
|
||||
* 不会在关驱动器的当口又塞一帧进邮箱。 */
|
||||
s_txSilent[channelIndex] = 1U;
|
||||
/* 驱动器即将关断:残留 PENDING 邮箱发不出也等不到 ACK,只会白涨 TEC,
|
||||
* 进静默前先把它们停掉。 */
|
||||
abortPendingTx(channelIndex);
|
||||
gpio_bit_set(ch.slpPort, ch.slpPin); /* S=1 → TCAN1057 Silent(驱动关断,接收照常) */
|
||||
} else {
|
||||
gpio_bit_reset(ch.slpPort, ch.slpPin); /* S=0 → Normal */
|
||||
/* 收发器模式建立时间 tMODE=20µs(手册 6.9):等够再放开发送门,
|
||||
* 否则紧随其后的第一帧会被驱动器忽略。 */
|
||||
canBusyWaitUs(20U);
|
||||
s_txSilent[channelIndex] = 0U;
|
||||
}
|
||||
|
||||
const uint8_t pinHigh = (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) ? 1U : 0U;
|
||||
CAN_LOGI("[CAN] CH%u tx_silent=%u (S pin=%u)\r\n", (unsigned)channelIndex,
|
||||
(unsigned)silent, (unsigned)pinHigh);
|
||||
/* 回读确认脚电平与意图一致;不一致就把标志回滚成原状态再报错,
|
||||
* 否则 isTxSilent()(发送门控 + 0xC1 bit5)会与物理脚电平各说一套。 */
|
||||
if (pinHigh != (silent ? 1U : 0U)) {
|
||||
s_txSilent[channelIndex] = silent ? 0U : 1U;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CanFd::isTxSilent(uint8_t channelIndex)
|
||||
{
|
||||
if (channelIndex >= kChannelCount) {
|
||||
return false;
|
||||
}
|
||||
return s_txSilent[channelIndex] != 0U;
|
||||
}
|
||||
|
||||
uint32_t CanFd::readChannelErrorReg(uint8_t channelIndex)
|
||||
{
|
||||
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
|
||||
@@ -1826,14 +1905,17 @@ void CanFd::refreshFeedbackSnapshot()
|
||||
/* 写入硬件层错误状态供 SPI 反馈使用 */
|
||||
s_feedback[i].stats.tec = static_cast<uint8_t>(hw_tec[i]);
|
||||
s_feedback[i].stats.rec = static_cast<uint8_t>(hw_rec[i]);
|
||||
/* 总线状态位图:bit0=init, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning
|
||||
* init/sleep 来自 CAN_STAT,error_passive/bus_off/error_warning 来自 CAN_ERR */
|
||||
/* 总线状态位图:bit0=init, bit1=控制器sleep, bit2=error_passive, bit3=bus_off,
|
||||
* bit4=error_warning, bit5=本端收发器 Silent(只听不发)
|
||||
* bit0~1 来自 CAN_STAT,bit2~4 来自 CAN_ERR;bit5 来自 s_txSilent(唯一真源,
|
||||
* 与发送门控同一个变量),别拿 bit1 当收发器状态——两者不是一回事。 */
|
||||
uint8_t busState = 0U;
|
||||
if (hw_stat[i] & CAN_STAT_IWS) busState |= (1U << 0); /* 初始化模式 */
|
||||
if (hw_stat[i] & CAN_STAT_SLPWS) busState |= (1U << 1); /* 睡眠模式 */
|
||||
if (hw_stat[i] & CAN_STAT_SLPWS) busState |= (1U << 1); /* 控制器睡眠模式 */
|
||||
if (hw_err[i] & CAN_ERR_PERR) busState |= (1U << 2); /* 错误被动 */
|
||||
if (hw_err[i] & CAN_ERR_BOERR) busState |= (1U << 3); /* 总线关闭 */
|
||||
if (hw_err[i] & CAN_ERR_WERR) busState |= (1U << 4); /* 错误警告 */
|
||||
if (s_txSilent[i] != 0U) busState |= (1U << 5); /* 收发器 Silent */
|
||||
s_feedback[i].stats.bus_state = busState;
|
||||
s_idSnapshot[i].seq = s_feedbackSeq;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ public:
|
||||
/**
|
||||
* @brief CAN 发送结果枚举,枚举值即 SPI 错误响应码,便于 handler 直接透传。
|
||||
* @note 0xE0/0xE1 保留给 handler 入口校验(payload 过短/通道非法),
|
||||
* 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因。
|
||||
* 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因;
|
||||
* 0xE6=通道处于收发器 Silent(只听不发),在入口即拒绝,不计任何错误计数。
|
||||
*/
|
||||
enum class CanTxResult : uint8_t {
|
||||
kOk = 0x00U, /**< 发送成功,已进入硬件邮箱。 */
|
||||
@@ -41,6 +42,7 @@ public:
|
||||
kErrTecHigh = 0xE3U, /**< TEC > 96,暂停发送保护通道。 */
|
||||
kErrMailbox = 0xE4U, /**< 3 个硬件邮箱全 PENDING,abort+重投仍失败。 */
|
||||
kErrInvalid = 0xE5U, /**< 入口参数无效(channelIndex/dlc/data 等)。 */
|
||||
kErrSilent = 0xE6U, /**< 通道收发器处于 Silent(驱动器关断),拒绝发送。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -89,7 +91,7 @@ public:
|
||||
uint8_t last_rx_data[CANFD_FEEDBACK_LAST_DATA_MAX];
|
||||
uint8_t tec; /**< 最近一次采样时硬件发送错误计数器 (0~255)。 */
|
||||
uint8_t rec; /**< 最近一次采样时硬件接收错误计数器 (0~255)。 */
|
||||
uint8_t bus_state; /**< 总线状态位图:bit0=init_mode, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning。 */
|
||||
uint8_t bus_state; /**< 总线状态位图:bit0=init_mode, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning, bit5=tx_silent(收发器 Silent,只听不发)。bit0~4 是控制器/总线侧状态,bit5 是本端 S 脚状态。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -187,6 +189,23 @@ public:
|
||||
* 电机 OTA relay 经 enterState 钩子与 setChannelAutoRetrans 同点切换,OTA 结束必恢复。 */
|
||||
static void setChannelBrsSuppress(uint8_t channelIndex, bool suppress);
|
||||
|
||||
/** @brief 切换指定通道收发器(TCAN1057AV)的 Silent 模式:只听不发。
|
||||
* S 脚(CAN0=PA8/CAN1=PB14/CAN2=PB9)置高 → 驱动器关断、**接收机照常工作**
|
||||
* (手册 Table 8-4:RXD 仍镜像总线),所以静默期间接收与上报完全不受影响;
|
||||
* 置低 → 回 Normal。这是硬件级禁止发送,S 脚内部上拉,MCU 复位/浮空即为 Silent。
|
||||
* @note 进 Silent 前先 abort 本通道 PENDING 邮箱:驱动器已关,残留邮箱只会让
|
||||
* 控制器因收不到 ACK 而白涨 TEC。
|
||||
* 退 Silent 时忙等收发器模式建立时间 tMODE=20µs 再返回,否则紧跟的第一帧
|
||||
* 驱动不上线。用 DWT CYCCNT 忙等,可在 CS EXTI ISR 上下文调用
|
||||
* (SysTickTimer::delayMs 在该上下文会死等)。
|
||||
* 与 canfd.cpp 内 s_chanQuiet(空通道上报节流)无关,也别混为一谈。
|
||||
* 静默不持久化:上电 init 一律回 Normal。
|
||||
* @retval true 通道合法且已生效;false 通道非法。 */
|
||||
static bool setTxSilent(uint8_t channelIndex, bool silent);
|
||||
|
||||
/** @brief 查询指定通道是否处于收发器 Silent(唯一真源 s_txSilent,0xC1 bus_state bit5 用它)。 */
|
||||
static bool isTxSilent(uint8_t channelIndex);
|
||||
|
||||
/** @brief 读取指定通道 CAN_ERR 寄存器原始值(事件时刻只读快照,不依赖 1Hz 采样)。
|
||||
* 解码:TEC=bit16-23,REC=bit24-31,ERRN=bit4-6;无效通道返回 0。仅供诊断日志,禁用于发送门控。 */
|
||||
static uint32_t readChannelErrorReg(uint8_t channelIndex);
|
||||
@@ -320,6 +339,7 @@ private:
|
||||
static volatile Mode s_mode;
|
||||
static ChannelStats s_channelStats[3];
|
||||
static bool s_brsSuppress[3]; /**< TX BRS 压制(电机 OTA 期间该通道 DLC>8 帧禁 BRS) */
|
||||
static volatile uint8_t s_txSilent[3]; /**< 收发器 Silent(只听不发):1=S 脚已置高,TX 入口拒绝 */
|
||||
static ChannelFeedback s_feedback[3];
|
||||
/** 各通道硬件 init(时序+退出 init mode)是否成功 */
|
||||
static bool s_channelHwOk[3];
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* [21..24] error_count (LE)
|
||||
* [25] tec 硬件发送错误计数器 (0~255)
|
||||
* [26] rec 硬件接收错误计数器 (0~255)
|
||||
* [27] bus_state 总线状态位图:bit0=init,bit1=sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning
|
||||
* [27] bus_state 总线状态位图:bit0=init,bit1=控制器sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning,bit5=tx_silent(收发器Silent)
|
||||
* [28..] 条目逐条拼接:
|
||||
* id(4LE) + dlc(1) + flags(1) + reserved(2) + data(dlc)
|
||||
* 每条 = 8 + dlc 字节
|
||||
@@ -31,6 +31,16 @@
|
||||
* [0] mode 0=全部接收, 1=按 ID 过滤;其它值返回 kRspError。
|
||||
* 应答 payload:
|
||||
* [0] 实际生效的 mode。
|
||||
*
|
||||
* 3) kTypeCanSetSilent (0xC8):ESP32 切换指定通道收发器 Silent(只听不发)。
|
||||
* 请求 payload:
|
||||
* [0] channel 0/1/2
|
||||
* [1] silent 1=进 Silent(S 脚置高:驱动关断、接收机照常), 0=回 Normal
|
||||
* 应答 payload:
|
||||
* [0] channel [1] 实际生效的 silent(与发送门控同一个值) [2] 该通道使能位
|
||||
* ERROR:0xE0=payload 过短, 0xE1=通道非法/未使能, 0xE2=S 脚回读与意图不符(标志已回滚)。
|
||||
* 生效期间该通道所有发送入口(0xC0/0xC7/桥接/relay)回 0xE6=kErrSilent,
|
||||
* 不计错误计数;静默不持久化,上电 init 一律回 Normal。
|
||||
*/
|
||||
|
||||
#include "device/spi/spi_command_handler.h"
|
||||
@@ -138,18 +148,19 @@ static uint16_t buildFeedbackPayload(uint8_t channel, uint8_t page,
|
||||
|
||||
namespace {
|
||||
|
||||
/** @brief SPI 0xC1 反馈查询 + 0xC2 模式切换处理器。 */
|
||||
/** @brief SPI 0xC1 反馈查询 + 0xC2 模式切换 + 0xC8 收发器 Silent 开关处理器。 */
|
||||
class SpiCanFeedbackHandler final : public ISpiCommandHandler {
|
||||
public:
|
||||
/** @brief 是否处理 0xC1 / 0xC2。 */
|
||||
/** @brief 是否处理 0xC1 / 0xC2 / 0xC8。 */
|
||||
bool canHandle(uint8_t frame_type) const override
|
||||
{
|
||||
return (frame_type == SpiDefs::kTypeCanFeedback)
|
||||
|| (frame_type == SpiDefs::kTypeCanSetMode);
|
||||
|| (frame_type == SpiDefs::kTypeCanSetMode)
|
||||
|| (frame_type == SpiDefs::kTypeCanSetSilent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 分发到 handleFeedback / handleSetMode。
|
||||
* @brief 分发到 handleFeedback / handleSetMode / handleSetSilent。
|
||||
* @param bus 命令总线
|
||||
* @param ctx 请求上下文
|
||||
*/
|
||||
@@ -157,6 +168,8 @@ public:
|
||||
{
|
||||
if (ctx.frame_type == SpiDefs::kTypeCanFeedback) {
|
||||
handleFeedback(bus, ctx);
|
||||
} else if (ctx.frame_type == SpiDefs::kTypeCanSetSilent) {
|
||||
handleSetSilent(bus, ctx);
|
||||
} else {
|
||||
handleSetMode(bus, ctx);
|
||||
}
|
||||
@@ -237,6 +250,58 @@ private:
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &actual, 1U);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 0xC8:切换指定通道收发器 Silent(只听不发,S 脚控制)。
|
||||
* @param bus 命令总线
|
||||
* @param ctx 请求上下文(payload[0]=ch, [1]=silent 1/0)
|
||||
* @note 运行在 SPI CS EXTI ISR 上下文:只做 GPIO + 标志位;退 Silent 的 20µs
|
||||
* 收发器建立时间由 CanFd::setTxSilent 内部用 DWT 忙等,禁止 SysTick delayMs。
|
||||
*/
|
||||
void handleSetSilent(AppSpiCommand &bus, const SpiCommandContext &ctx)
|
||||
{
|
||||
if (ctx.payload_len < 2U) {
|
||||
uint8_t err = 0xE0U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t channel = ctx.payload[0];
|
||||
const bool silent = (ctx.payload[1] != 0U);
|
||||
|
||||
if (!CanFd::isChannelEnabled(channel)) {
|
||||
uint8_t err = 0xE1U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool ok = CanFd::setTxSilent(channel, silent);
|
||||
const uint8_t actual = CanFd::isTxSilent(channel) ? 1U : 0U;
|
||||
|
||||
if (!ok) {
|
||||
/* S 脚回读与意图不符(被别处抢改 / GPIO 配置未生效):如实报错,不报成功 */
|
||||
uint8_t err = 0xE2U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[AppSpiCommand::kLogBufSize] = {};
|
||||
(void)snprintf(log_msg, sizeof(log_msg),
|
||||
"CAN_SILENT ch=%u set=%u -> %u",
|
||||
static_cast<unsigned>(channel),
|
||||
static_cast<unsigned>(silent ? 1U : 0U),
|
||||
static_cast<unsigned>(actual));
|
||||
bus.postLog(log_msg);
|
||||
#endif
|
||||
|
||||
const uint8_t rsp[3] = { channel, actual, 1U };
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, rsp, 3U);
|
||||
}
|
||||
};
|
||||
|
||||
static SpiCanFeedbackHandler s_handler;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @file app_spi_cmd_spi_diag.cpp
|
||||
* @brief SPI 0xA4 链路全量诊断命令处理器。
|
||||
*
|
||||
* 供主机 (ESP32 / 调试脚本) 查询 GD32 侧 SPI0 从机实时运行诊断指标与异常现场快照:
|
||||
* payload[0]=sub_cmd:
|
||||
* 0 (或空载荷) -> 读取 ~88 字节全量诊断快照 (ACK)
|
||||
* 1 -> 复位清零所有诊断计数器 (ACK 1B: 0x00)
|
||||
* 2 -> 立即输出完整 ASCII 格式化报告到 GD32 USART0 串口 (ACK 1B: 0x00)
|
||||
*/
|
||||
#include "device/spi/spi_command_handler.h"
|
||||
#include "device/spi/app_spi_command.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/spi/app_spi_diag.h"
|
||||
#include "gd32e50x_gpio.h"
|
||||
#include "sys/config.h"
|
||||
#include <cstring>
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
|
||||
namespace {
|
||||
|
||||
class SpiDiagHandler final : public ISpiCommandHandler {
|
||||
public:
|
||||
bool canHandle(uint8_t frame_type) const override
|
||||
{
|
||||
return frame_type == SpiDefs::kTypeSpiDiag;
|
||||
}
|
||||
|
||||
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
|
||||
{
|
||||
uint8_t sub_cmd = 0U;
|
||||
if (ctx.payload_len >= 1U && ctx.payload != nullptr) {
|
||||
sub_cmd = ctx.payload[0];
|
||||
}
|
||||
|
||||
if (sub_cmd == 1U) {
|
||||
// 复位清零诊断计数器
|
||||
resetSpiDiagStats();
|
||||
uint8_t status = 0x00U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &status, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub_cmd == 2U) {
|
||||
// 立即向串口输出全量 ASCII 诊断报告
|
||||
dumpSpiDiag();
|
||||
uint8_t status = 0x00U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &status, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
// 默认 / sub_cmd == 0: 返回二进制全量指标快照
|
||||
const SpiDiagStats &st = getSpiDiagStats();
|
||||
uint8_t buf[88] = {};
|
||||
|
||||
buf[0] = 0x00U; // status OK
|
||||
buf[1] = bus.isV2Active() ? 1U : 0U;
|
||||
writeU32Le(&buf[2], bus.getSessionId());
|
||||
buf[6] = bus.getLocalCredit();
|
||||
buf[7] = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
|
||||
buf[8] = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
|
||||
buf[9] = bus.isCsActive() ? 1U : 0U;
|
||||
buf[10] = bus.isRingPending() ? 1U : 0U;
|
||||
writeU16Le(&buf[11], st.last_spi_stat);
|
||||
|
||||
writeU32Le(&buf[13], st.total_cs_assert);
|
||||
writeU32Le(&buf[17], st.total_cs_release);
|
||||
writeU32Le(&buf[21], st.valid_v2_frames);
|
||||
writeU32Le(&buf[25], st.parse_fail_count);
|
||||
writeU32Le(&buf[29], st.err_crc_mismatch);
|
||||
|
||||
writeU16Le(&buf[33], (uint16_t)st.err_no_sof);
|
||||
writeU16Le(&buf[35], (uint16_t)st.err_sof_drift);
|
||||
writeU16Le(&buf[37], (uint16_t)st.err_bad_len);
|
||||
writeU16Le(&buf[39], (uint16_t)st.rx_overrun_count);
|
||||
writeU16Le(&buf[41], (uint16_t)st.short_rx_len_count);
|
||||
writeU16Le(&buf[43], (uint16_t)st.zero_rx_len_count);
|
||||
writeU16Le(&buf[45], (uint16_t)st.all_ff_count);
|
||||
writeU16Le(&buf[47], (uint16_t)st.all_00_count);
|
||||
writeU16Le(&buf[49], (uint16_t)st.session_mismatch);
|
||||
writeU16Le(&buf[51], (uint16_t)st.seq_jump_count);
|
||||
writeU16Le(&buf[53], (uint16_t)st.ring_timeout_count);
|
||||
writeU16Le(&buf[55], (uint16_t)st.unknown_type_count);
|
||||
writeU16Le(&buf[57], (uint16_t)st.handler_empty_count);
|
||||
|
||||
buf[59] = st.last_rx_type;
|
||||
buf[60] = st.last_tx_type;
|
||||
buf[61] = st.last_error_code;
|
||||
buf[62] = st.last_error_reason;
|
||||
writeU16Le(&buf[63], (uint16_t)st.last_rx_len);
|
||||
writeU16Le(&buf[65], (uint16_t)st.last_error_rx_len);
|
||||
memcpy(&buf[67], st.last_error_bytes, 8);
|
||||
writeU32Le(&buf[75], st.last_error_calc_crc);
|
||||
writeU32Le(&buf[79], st.last_error_expected_crc);
|
||||
writeU32Le(&buf[83], st.last_error_uptime_ms);
|
||||
buf[87] = st.last_error_sof_offset;
|
||||
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, buf, sizeof(buf));
|
||||
}
|
||||
};
|
||||
|
||||
static SpiDiagHandler s_handler;
|
||||
|
||||
} // namespace
|
||||
|
||||
void registerSpiDiagCommand(AppSpiCommand &bus)
|
||||
{
|
||||
bus.registerHandler(&s_handler);
|
||||
}
|
||||
|
||||
#else /* CONFIG_SPI_DIAG_ENABLE == 0 */
|
||||
|
||||
void registerSpiDiagCommand(AppSpiCommand & /*bus*/) {}
|
||||
|
||||
#endif
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "device/spi/app_spi_command.h"
|
||||
#include "device/spi/spi_command_handler.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/spi/app_spi_diag.h"
|
||||
#include "spi_link_v2.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
|
||||
@@ -168,7 +169,113 @@ static uint16_t s_lastRespLen = 0;
|
||||
static bool s_hasCachedResp = false;
|
||||
|
||||
/* ============================================================
|
||||
* v2 帧构建 / 帧解析(委托 spi_link_v2.h,三工程共用)
|
||||
* SPI 诊断统计与现场快照
|
||||
* ============================================================ */
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
#define SPI_DIAG_RECORD(...) do { __VA_ARGS__; } while (0)
|
||||
|
||||
static SpiDiagStats s_diag = {};
|
||||
|
||||
const SpiDiagStats& getSpiDiagStats()
|
||||
{
|
||||
s_diag.current_local_credit = (uint8_t)g_localCredit;
|
||||
s_diag.expected_rx_seq = g_expectedRxSeq;
|
||||
return s_diag;
|
||||
}
|
||||
|
||||
void resetSpiDiagStats()
|
||||
{
|
||||
memset(&s_diag, 0, sizeof(s_diag));
|
||||
}
|
||||
|
||||
const char* spiParseResultToString(SpiParseResult res)
|
||||
{
|
||||
switch (res) {
|
||||
case SPI_PARSE_OK: return "OK";
|
||||
case SPI_PARSE_ERR_TOO_SHORT: return "TOO_SHORT";
|
||||
case SPI_PARSE_ERR_ALL_FF: return "ALL_FF";
|
||||
case SPI_PARSE_ERR_ALL_00: return "ALL_00";
|
||||
case SPI_PARSE_ERR_NO_SOF: return "NO_SOF";
|
||||
case SPI_PARSE_ERR_BAD_VERSION: return "BAD_VERSION";
|
||||
case SPI_PARSE_ERR_BAD_LEN: return "BAD_LEN";
|
||||
case SPI_PARSE_ERR_CRC_MISMATCH: return "CRC_MISMATCH";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#else
|
||||
#define SPI_DIAG_RECORD(...) ((void)0)
|
||||
#endif
|
||||
|
||||
bool AppSpiCommand::isV2Active() const { return g_v2Active; }
|
||||
uint32_t AppSpiCommand::getSessionId() const { return g_sessionId; }
|
||||
uint8_t AppSpiCommand::getLocalCredit() const { return (uint8_t)g_localCredit; }
|
||||
void AppSpiCommand::dumpDiag() const { dumpSpiDiag(); }
|
||||
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
void dumpSpiDiag()
|
||||
{
|
||||
const SpiDiagStats &st = getSpiDiagStats();
|
||||
const uint32_t now = SysTickTimer::getMs();
|
||||
const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
|
||||
const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
|
||||
const uint16_t stat = (uint16_t)SPI_STAT(SPI0);
|
||||
|
||||
printf("\r\n==================== [GD32 SPI0 LINK DIAGNOSTICS] ====================\r\n");
|
||||
printf("Status : v2_active=%d session_id=0x%08lX local_credit=%u uptime=%lums\r\n",
|
||||
AppSpiCommandBus.isV2Active() ? 1 : 0, (unsigned long)AppSpiCommandBus.getSessionId(),
|
||||
(unsigned)AppSpiCommandBus.getLocalCredit(), (unsigned long)now);
|
||||
printf("Pins & HW: CS(PA15)=%u DATA_READY(PB6)=%u ring_pending=%d cs_active=%d\r\n",
|
||||
cs_pin, rdy_pin, AppSpiCommandBus.isRingPending() ? 1 : 0, AppSpiCommandBus.isCsActive() ? 1 : 0);
|
||||
printf("SPI_STAT : 0x%04X (TRANS:%d TBE:%d RBNE:%d OVR:%d FERR:%d)\r\n",
|
||||
stat, (stat & SPI_FLAG_TRANS) ? 1 : 0, (stat & SPI_FLAG_TBE) ? 1 : 0,
|
||||
(stat & SPI_FLAG_RBNE) ? 1 : 0, (stat & SPI_FLAG_RXORERR) ? 1 : 0,
|
||||
(stat & SPI_FLAG_FERR) ? 1 : 0);
|
||||
printf("CS Stats : assert=%lu release=%lu missed=%lu short(<20B)=%lu zero_len=%lu\r\n",
|
||||
(unsigned long)st.total_cs_assert, (unsigned long)st.total_cs_release,
|
||||
(unsigned long)st.missed_cs_assert, (unsigned long)st.short_rx_len_count,
|
||||
(unsigned long)st.zero_rx_len_count);
|
||||
printf("Frames : valid_v2=%lu parse_fail=%lu all_FF=%lu all_00=%lu\r\n",
|
||||
(unsigned long)st.valid_v2_frames, (unsigned long)st.parse_fail_count,
|
||||
(unsigned long)st.all_ff_count, (unsigned long)st.all_00_count);
|
||||
printf("ParseErr : CRC_err=%lu no_SOF=%lu SOF_drift=%lu bad_ver=%lu bad_len=%lu\r\n",
|
||||
(unsigned long)st.err_crc_mismatch, (unsigned long)st.err_no_sof,
|
||||
(unsigned long)st.err_sof_drift, (unsigned long)st.err_bad_version,
|
||||
(unsigned long)st.err_bad_len);
|
||||
printf("Hw Errors: RXORERR(ovr)=%lu FERR(fmt)=%lu\r\n",
|
||||
(unsigned long)st.rx_overrun_count, (unsigned long)st.format_error_count);
|
||||
printf("Session : HELLO_cnt=%lu mismatch=%lu seq_ok=%lu seq_jump=%lu seq_dup=%lu\r\n",
|
||||
(unsigned long)st.hello_count, (unsigned long)st.session_mismatch,
|
||||
(unsigned long)st.seq_ok_count, (unsigned long)st.seq_jump_count,
|
||||
(unsigned long)st.seq_dup_count);
|
||||
printf("Sequences: last_rx_seq=0x%02X exp_rx_seq=0x%02X last_tx_seq=0x%02X\r\n",
|
||||
st.last_rx_seq, st.expected_rx_seq, st.last_tx_seq);
|
||||
printf("Commands : last_rx_type=0x%02X last_tx_type=0x%02X error_rsp(0x82)=%lu (last_code=0x%02X)\r\n",
|
||||
st.last_rx_type, st.last_tx_type, (unsigned long)st.error_rsp_count, st.last_error_code);
|
||||
printf("Routing : unknown_type=%lu (last=0x%02X) handler_empty=%lu (last=0x%02X)\r\n",
|
||||
(unsigned long)st.unknown_type_count, st.last_unknown_type,
|
||||
(unsigned long)st.handler_empty_count, st.last_empty_type);
|
||||
printf("Ring Push: req=%lu timeout=%lu collision=%lu\r\n",
|
||||
(unsigned long)st.ring_request_count, (unsigned long)st.ring_timeout_count,
|
||||
(unsigned long)st.ring_collision_count);
|
||||
if (st.last_error_uptime_ms > 0U) {
|
||||
printf("LastErr : at %lums, rxLen=%u, reason=%s(0x%02X), SOF_off=%u\r\n",
|
||||
(unsigned long)st.last_error_uptime_ms, (unsigned)st.last_error_rx_len,
|
||||
spiParseResultToString((SpiParseResult)st.last_error_reason),
|
||||
st.last_error_code, (unsigned)st.last_error_sof_offset);
|
||||
printf("LastBytes: [%02X %02X %02X %02X %02X %02X %02X %02X]\r\n",
|
||||
st.last_error_bytes[0], st.last_error_bytes[1], st.last_error_bytes[2], st.last_error_bytes[3],
|
||||
st.last_error_bytes[4], st.last_error_bytes[5], st.last_error_bytes[6], st.last_error_bytes[7]);
|
||||
if (st.last_error_reason == SPI_PARSE_ERR_CRC_MISMATCH) {
|
||||
printf("LastCRC : calc=0x%08lX exp=0x%08lX\r\n",
|
||||
(unsigned long)st.last_error_calc_crc, (unsigned long)st.last_error_expected_crc);
|
||||
}
|
||||
}
|
||||
printf("======================================================================\r\n\r\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ============================================================
|
||||
* v2 帧构建 / 诊断帧解析(委托 spi_link_v2.h,三工程共用)
|
||||
* ============================================================ */
|
||||
|
||||
/* 构建响应帧;上层接口(type/payload)保持不变,handler 无需改动。
|
||||
@@ -192,36 +299,120 @@ static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t paylo
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 解析 v2 帧;委托 v2_parse_frame,扫描 SOF 并校验 VERSION/CRC32。
|
||||
* 仅 v2:v2_parse_frame 扫 SOF0/SOF1 + CRC32;禁止再加 v1 CRC8 分支。 */
|
||||
static bool spi_try_parse_frame(const uint8_t *frame, uint16_t frame_len,
|
||||
uint8_t *type, const uint8_t **payload, uint16_t *payload_len,
|
||||
v2_header_t *out_hdr = nullptr)
|
||||
/* 细粒度诊断帧解析:逐项分析失败原因(无SOF/相位漂移/版本不符/长度截断/CRC位错)。 */
|
||||
static bool spi_diagnose_and_parse_frame(
|
||||
const uint8_t *buf, size_t buf_len,
|
||||
uint8_t *out_type, const uint8_t **out_payload, uint16_t *out_payload_len,
|
||||
v2_header_t *out_hdr,
|
||||
SpiParseResult *out_res,
|
||||
uint32_t *out_calc_crc, uint32_t *out_exp_crc,
|
||||
uint8_t *out_sof_offset)
|
||||
{
|
||||
if (!frame || !type || !payload || !payload_len) { return false; }
|
||||
if (!buf || buf_len == 0U) {
|
||||
if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT;
|
||||
if (out_sof_offset) *out_sof_offset = 0xFFU;
|
||||
return false;
|
||||
}
|
||||
|
||||
v2_header_t hdr;
|
||||
const uint8_t *pload = nullptr;
|
||||
const bool ok = v2_parse_frame(frame, frame_len, &hdr, &pload);
|
||||
if (!ok) { return false; }
|
||||
if (isAllBytesValue(buf, (uint16_t)buf_len, 0xFFU)) {
|
||||
if (out_res) *out_res = SPI_PARSE_ERR_ALL_FF;
|
||||
if (out_sof_offset) *out_sof_offset = 0xFFU;
|
||||
return false;
|
||||
}
|
||||
|
||||
*type = hdr.type;
|
||||
*payload = pload;
|
||||
*payload_len = hdr.payload_len;
|
||||
if (out_hdr) { *out_hdr = hdr; }
|
||||
return true;
|
||||
if (isAllBytesValue(buf, (uint16_t)buf_len, 0x00U)) {
|
||||
if (out_res) *out_res = SPI_PARSE_ERR_ALL_00;
|
||||
if (out_sof_offset) *out_sof_offset = 0xFFU;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buf_len < V2_OVERHEAD) {
|
||||
if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT;
|
||||
if (out_sof_offset) *out_sof_offset = 0xFFU;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool found_sof = false;
|
||||
size_t first_sof_offset = 0xFFU;
|
||||
SpiParseResult best_err = SPI_PARSE_ERR_NO_SOF;
|
||||
|
||||
for (size_t i = 0; i + V2_OVERHEAD <= buf_len; ++i) {
|
||||
if (buf[i + V2_OFF_SOF0] != V2_SOF0 || buf[i + V2_OFF_SOF1] != V2_SOF1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!found_sof) {
|
||||
found_sof = true;
|
||||
first_sof_offset = i;
|
||||
}
|
||||
|
||||
const uint8_t version = buf[i + V2_OFF_VERSION];
|
||||
if (version != V2_VERSION) {
|
||||
best_err = SPI_PARSE_ERR_BAD_VERSION;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint16_t plen = v2_get_u16le(&buf[i + V2_OFF_LEN]);
|
||||
if (plen > V2_MAX_PAYLOAD) {
|
||||
best_err = SPI_PARSE_ERR_BAD_LEN;
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t total = V2_OVERHEAD + plen;
|
||||
if (i + total > buf_len) {
|
||||
best_err = SPI_PARSE_ERR_BAD_LEN;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t expect = v2_get_u32le(&buf[i + V2_HEADER_LEN + plen]);
|
||||
const uint32_t calc = v2_crc32(&buf[i], V2_HEADER_LEN + plen);
|
||||
if (expect != calc) {
|
||||
best_err = SPI_PARSE_ERR_CRC_MISMATCH;
|
||||
if (out_calc_crc) *out_calc_crc = calc;
|
||||
if (out_exp_crc) *out_exp_crc = expect;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (out_hdr) {
|
||||
out_hdr->version = version;
|
||||
out_hdr->flags = buf[i + V2_OFF_FLAGS];
|
||||
out_hdr->session_id = v2_get_u32le(&buf[i + V2_OFF_SESSION]);
|
||||
out_hdr->tx_seq = buf[i + V2_OFF_TX_SEQ];
|
||||
out_hdr->ack_seq = buf[i + V2_OFF_ACK_SEQ];
|
||||
out_hdr->credit = buf[i + V2_OFF_CREDIT];
|
||||
out_hdr->type = buf[i + V2_OFF_TYPE];
|
||||
out_hdr->payload_len = plen;
|
||||
out_hdr->frame_offset = i;
|
||||
}
|
||||
if (out_type) *out_type = buf[i + V2_OFF_TYPE];
|
||||
if (out_payload) *out_payload = (plen > 0U) ? &buf[i + V2_HEADER_LEN] : nullptr;
|
||||
if (out_payload_len) *out_payload_len = plen;
|
||||
if (out_res) *out_res = SPI_PARSE_OK;
|
||||
if (out_sof_offset) *out_sof_offset = (uint8_t)i;
|
||||
if (out_calc_crc) *out_calc_crc = calc;
|
||||
if (out_exp_crc) *out_exp_crc = expect;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (out_res) *out_res = best_err;
|
||||
if (out_sof_offset) *out_sof_offset = (uint8_t)first_sof_offset;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 调试日志辅助
|
||||
* 调试日志辅助(环形队列,避免事件并发时丢日志)
|
||||
* ============================================================ */
|
||||
|
||||
void AppSpiCommand::postLog(const char *msg)
|
||||
{
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
if (!msg) return;
|
||||
snprintf(m_logBuf, kLogBufSize, "[SPI-APP] %s", msg);
|
||||
m_logPending = true;
|
||||
uint8_t next = (uint8_t)((m_logHead + 1U) % kLogSlotCount);
|
||||
if (next != m_logTail) {
|
||||
strncpy(m_logQueue[m_logHead], msg, kLogSlotLen - 1U);
|
||||
m_logQueue[m_logHead][kLogSlotLen - 1U] = '\0';
|
||||
m_logHead = next;
|
||||
}
|
||||
#else
|
||||
(void)msg;
|
||||
#endif
|
||||
@@ -231,14 +422,29 @@ void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val)
|
||||
{
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
if (!fmt) return;
|
||||
snprintf(m_logBuf, kLogBufSize, fmt, val);
|
||||
m_logPending = true;
|
||||
char tmp[kLogSlotLen];
|
||||
snprintf(tmp, sizeof(tmp), fmt, val);
|
||||
postLog(tmp);
|
||||
#else
|
||||
(void)fmt;
|
||||
(void)val;
|
||||
#endif
|
||||
}
|
||||
|
||||
void AppSpiCommand::postLogFmt32(const char *fmt, uint32_t v1, uint32_t v2)
|
||||
{
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
if (!fmt) return;
|
||||
char tmp[kLogSlotLen];
|
||||
snprintf(tmp, sizeof(tmp), fmt, (unsigned long)v1, (unsigned long)v2);
|
||||
postLog(tmp);
|
||||
#else
|
||||
(void)fmt;
|
||||
(void)v1;
|
||||
(void)v2;
|
||||
#endif
|
||||
}
|
||||
|
||||
void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size,
|
||||
uint16_t *tx_len, uint8_t rsp_type,
|
||||
const uint8_t *payload, uint16_t payload_len)
|
||||
@@ -402,18 +608,20 @@ extern void registerSpiAdcFeedbackCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiMotorStateCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiMotorOtaCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiExmcDiagCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiDiagCommand(AppSpiCommand &bus);
|
||||
AppSpiCommand AppSpiCommandBus;
|
||||
void AppSpiCommand::init()
|
||||
{
|
||||
m_csActive = false;
|
||||
m_rxLen = 0U;
|
||||
m_txLen = 0U;
|
||||
m_logPending = false;
|
||||
m_logHead = 0U;
|
||||
m_logTail = 0U;
|
||||
m_commandCount = 0U;
|
||||
m_ringPending = false;
|
||||
m_ringDeadline = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
memset(m_logBuf, 0, sizeof(m_logBuf));
|
||||
memset(m_logQueue, 0, sizeof(m_logQueue));
|
||||
memset(m_commands, 0, sizeof(m_commands));
|
||||
|
||||
registerSpiBootRequestCommand(AppSpiCommandBus);
|
||||
@@ -428,6 +636,7 @@ void AppSpiCommand::init()
|
||||
registerSpiMotorStateCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */
|
||||
registerSpiMotorOtaCommand(AppSpiCommandBus);
|
||||
registerSpiExmcDiagCommand(AppSpiCommandBus);
|
||||
registerSpiDiagCommand(AppSpiCommandBus);
|
||||
|
||||
configureGpio();
|
||||
configureDma();
|
||||
@@ -530,47 +739,122 @@ void AppSpiCommand::processCommand()
|
||||
const uint8_t *payload = nullptr;
|
||||
uint16_t payload_len = 0U;
|
||||
v2_header_t hdr;
|
||||
SpiParseResult parse_res = SPI_PARSE_OK;
|
||||
uint32_t calc_crc = 0U;
|
||||
uint32_t exp_crc = 0U;
|
||||
uint8_t sof_offset = 0xFFU;
|
||||
|
||||
if (!spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr)) {
|
||||
postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]);
|
||||
/* 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..5]=rxBuf 前 4 字节。
|
||||
* ESP32 /system/status rsp_err_last_p0..p3 直读,无需串口即可判定损坏形态
|
||||
* (全FF=DMA 未装/CS 丢失;SOF 错位=相位漂移;SOF 对但 CRC 败=帧内位错)。 */
|
||||
const bool parse_ok = spi_diagnose_and_parse_frame(
|
||||
m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr,
|
||||
&parse_res, &calc_crc, &exp_crc, &sof_offset);
|
||||
|
||||
if (!parse_ok) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.parse_fail_count++;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
s_diag.last_error_rx_len = m_rxLen;
|
||||
s_diag.last_error_calc_crc = calc_crc;
|
||||
s_diag.last_error_expected_crc = exp_crc;
|
||||
s_diag.last_error_sof_offset = sof_offset;
|
||||
s_diag.last_error_reason = (uint8_t)parse_res;
|
||||
s_diag.last_error_code = 0xE8U;
|
||||
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
|
||||
);
|
||||
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0 && CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
switch (parse_res) {
|
||||
case SPI_PARSE_ERR_CRC_MISMATCH: {
|
||||
SPI_DIAG_RECORD(s_diag.err_crc_mismatch++);
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] CRC mismatch: calc=0x%08lX exp=0x%08lX rxLen=%u",
|
||||
(unsigned long)calc_crc, (unsigned long)exp_crc, (unsigned)m_rxLen);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
case SPI_PARSE_ERR_NO_SOF: {
|
||||
SPI_DIAG_RECORD(s_diag.err_no_sof++);
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] No SOF: rxLen=%u raw=[%02X %02X %02X %02X]",
|
||||
(unsigned)m_rxLen, m_rxBuf[0], m_rxBuf[1], m_rxBuf[2], m_rxBuf[3]);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
case SPI_PARSE_ERR_BAD_VERSION: {
|
||||
SPI_DIAG_RECORD(s_diag.err_bad_version++);
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] Bad ver=0x%02X (exp 0x02) SOF_off=%u rxLen=%u",
|
||||
(sof_offset < m_rxLen - 2) ? m_rxBuf[sof_offset + 2] : 0xFFU,
|
||||
(unsigned)sof_offset, (unsigned)m_rxLen);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
case SPI_PARSE_ERR_BAD_LEN: {
|
||||
SPI_DIAG_RECORD(s_diag.err_bad_len++);
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] Bad len at SOF_off=%u rxLen=%u",
|
||||
(unsigned)sof_offset, (unsigned)m_rxLen);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] Parse fail res=%d rxLen=%u",
|
||||
(int)parse_res, (unsigned)m_rxLen);
|
||||
postLog(log_msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (sof_offset > 0U && sof_offset != 0xFFU) {
|
||||
SPI_DIAG_RECORD(s_diag.err_sof_drift++);
|
||||
}
|
||||
|
||||
/* 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..3]=rxBuf 前 2 字节, [4]=parse_res, [5]=sof_offset */
|
||||
uint16_t tx_len = 0U;
|
||||
uint8_t diag[8];
|
||||
diag[0] = 0xE8U;
|
||||
diag[1] = (m_rxLen > 255U) ? 255U : (uint8_t)m_rxLen;
|
||||
diag[2] = (m_rxLen > 0U) ? m_rxBuf[0] : 0xFFU;
|
||||
diag[3] = (m_rxLen > 1U) ? m_rxBuf[1] : 0xFFU;
|
||||
diag[4] = (m_rxLen > 2U) ? m_rxBuf[2] : 0xFFU;
|
||||
diag[5] = (m_rxLen > 3U) ? m_rxBuf[3] : 0xFFU;
|
||||
diag[4] = (uint8_t)parse_res;
|
||||
diag[5] = sof_offset;
|
||||
diag[6] = 0x5AU;
|
||||
diag[7] = 0xA5U;
|
||||
/* 非v2帧时 hdr 未初始化:禁止缓存/推进序号(垃圾缓存会被 dup 重放) */
|
||||
SPI_DIAG_RECORD(s_diag.error_rsp_count++);
|
||||
spi_build_frame(kRspError, diag, sizeof(diag), m_txBuf, &tx_len);
|
||||
m_txLen = tx_len;
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
}
|
||||
|
||||
/* 正常帧不打日志(避免 UART 刷屏);CONFIG_SPI_APP_LOG_ENABLE
|
||||
* 只覆盖异常路径(非v2帧/未知type/SEQ跳变/session不匹配),正常态静默。 */
|
||||
// 解析合法 v2 帧成功
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.valid_v2_frames++;
|
||||
s_diag.last_rx_type = frame_type;
|
||||
s_diag.last_rx_seq = hdr.tx_seq;
|
||||
);
|
||||
if (sof_offset > 0U && sof_offset != 0xFFU) {
|
||||
SPI_DIAG_RECORD(s_diag.err_sof_drift++);
|
||||
}
|
||||
|
||||
g_pendingAckSeq = hdr.tx_seq;
|
||||
|
||||
if (frame_type == V2_TYPE_LINK_HELLO) {
|
||||
/* 合法 HELLO 已到:停掉起机补清,避免 reset 打断建连 */
|
||||
SPI_DIAG_RECORD(s_diag.hello_count++);
|
||||
Esp32En::cancelRelinkFromSpi();
|
||||
g_sessionId = hdr.session_id;
|
||||
g_txSeq = 0;
|
||||
g_expectedRxSeq = 0;
|
||||
g_v2Active = true;
|
||||
s_hasCachedResp = false;
|
||||
|
||||
/* 与 requestEsp32Read() 一致(L5 8bc01dd):建帧前 credit=1,
|
||||
* 避免 TX 占用时 g_localCredit=0 写入 HELLO_ACK,导致主机 remoteCredit=0 拒发 PING。 */
|
||||
g_localCredit = 1U;
|
||||
/* 建连 + 开机身份一次完成:HELLO_ACK = 6B 链路 + 16B 身份(版本/开关) */
|
||||
|
||||
uint8_t identity[V2_APP_IDENTITY_LEN];
|
||||
fillGd32AppIdentityPayload(identity);
|
||||
size_t built_len = 0U;
|
||||
@@ -583,6 +867,10 @@ void AppSpiCommand::processCommand()
|
||||
memcpy(s_lastRespBuf, m_txBuf, m_txLen);
|
||||
s_lastRespLen = m_txLen;
|
||||
s_hasCachedResp = true;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_tx_type = V2_TYPE_LINK_HELLO;
|
||||
s_diag.last_tx_seq = 0;
|
||||
);
|
||||
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
@@ -590,42 +878,77 @@ void AppSpiCommand::processCommand()
|
||||
|
||||
if (g_v2Active) {
|
||||
if (hdr.session_id != g_sessionId) {
|
||||
postLogFmt("[SPI-APP] session 不匹配 0x%02X -> LINK_ERROR",
|
||||
(uint8_t)(hdr.session_id & 0xFFU));
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.session_mismatch++;
|
||||
s_diag.last_error_code = V2_ERR_SESSION;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
s_diag.error_rsp_count++;
|
||||
);
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] Session mismatch: rx=0x%08lX != cur=0x%08lX",
|
||||
(unsigned long)hdr.session_id, (unsigned long)g_sessionId);
|
||||
postLog(log_msg);
|
||||
#endif
|
||||
|
||||
size_t built_len = 0U;
|
||||
v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq,
|
||||
(uint8_t)g_localCredit, V2_ERR_SESSION,
|
||||
(uint8_t)g_expectedRxSeq, m_txBuf, &built_len);
|
||||
m_txLen = (uint16_t)built_len;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_tx_type = V2_TYPE_LINK_ERROR;
|
||||
s_diag.last_tx_seq = g_txSeq;
|
||||
);
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t dist = v2_seq_distance(hdr.tx_seq, g_expectedRxSeq);
|
||||
if (dist == 0U) {
|
||||
SPI_DIAG_RECORD(s_diag.seq_ok_count++);
|
||||
g_expectedRxSeq = (uint8_t)(g_expectedRxSeq + 1U);
|
||||
} else if (dist >= 128U) {
|
||||
SPI_DIAG_RECORD(s_diag.seq_dup_count++);
|
||||
if (s_hasCachedResp && hdr.tx_seq == s_lastReqSeq) {
|
||||
postLogFmt("[SPI-APP] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq);
|
||||
postLogFmt("[SPI-DIAG][INFO] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq);
|
||||
memcpy(m_txBuf, s_lastRespBuf, s_lastRespLen);
|
||||
m_txLen = s_lastRespLen;
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
postLogFmt("[SPI-APP] SEQ jump rx=0x%02X", hdr.tx_seq);
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.seq_jump_count++;
|
||||
s_diag.last_error_code = V2_ERR_SEQ_JUMP;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][WARN] SEQ jump: rx=0x%02X exp=0x%02X (gap=%u)",
|
||||
hdr.tx_seq, g_expectedRxSeq, (unsigned)dist);
|
||||
postLog(log_msg);
|
||||
#endif
|
||||
|
||||
g_expectedRxSeq = (uint8_t)(hdr.tx_seq + 1U);
|
||||
|
||||
if (frame_type != kTypeBootReq) {
|
||||
SPI_DIAG_RECORD(s_diag.error_rsp_count++);
|
||||
size_t built_len = 0U;
|
||||
v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq,
|
||||
(uint8_t)g_localCredit, V2_ERR_SEQ_JUMP,
|
||||
(uint8_t)g_expectedRxSeq, m_txBuf, &built_len);
|
||||
m_txLen = (uint16_t)built_len;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_tx_type = V2_TYPE_LINK_ERROR;
|
||||
s_diag.last_tx_seq = g_txSeq;
|
||||
);
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
}
|
||||
postLog("SEQ jump but BOOT_REQ still handled");
|
||||
postLog("[SPI-DIAG] SEQ jump but BOOT_REQ still handled");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,19 +973,46 @@ void AppSpiCommand::processCommand()
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.unknown_type_count++;
|
||||
s_diag.last_unknown_type = frame_type;
|
||||
s_diag.last_error_code = 0xE9U;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
s_diag.error_rsp_count++;
|
||||
);
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] 未知 type=0x%02X -> ERROR 0xE9", frame_type);
|
||||
postLog(log_msg);
|
||||
#endif
|
||||
uint8_t err = 0xE9U; /* E9=无 handler 认领 */
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
}
|
||||
|
||||
if (tx_len == 0U) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.handler_empty_count++;
|
||||
s_diag.last_empty_type = frame_type;
|
||||
s_diag.last_error_code = 0xEAU;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
s_diag.error_rsp_count++;
|
||||
);
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
char log_msg[128];
|
||||
snprintf(log_msg, sizeof(log_msg),
|
||||
"[SPI-DIAG][ERR] handler t=0x%02X tx_len=0 -> ERROR 0xEA", frame_type);
|
||||
postLog(log_msg);
|
||||
#endif
|
||||
uint8_t err = 0xEAU; /* EA=handler 命中但未产出响应 */
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
/* handler 命中但没产出响应(典型=buildResponse 构帧失败/业务拒绝)走此兜底错误路径。 */
|
||||
postLogFmt("[SPI-APP] handler t=0x%02X tx_len=0 -> ERROR", frame_type);
|
||||
}
|
||||
|
||||
m_txLen = tx_len;
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_tx_type = m_txBuf[V2_OFF_TYPE];
|
||||
s_diag.last_tx_seq = g_txSeq;
|
||||
);
|
||||
|
||||
if (g_v2Active) {
|
||||
s_lastReqSeq = hdr.tx_seq;
|
||||
@@ -728,11 +1078,15 @@ void AppSpiCommand::resetEsp32Link()
|
||||
armSpiDmasForNextCs();
|
||||
updateDataReady();
|
||||
configureExti();
|
||||
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
|
||||
printf("[SPI-APP] resetEsp32Link: SPI0/DMA/EXTI re-init done\r\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len)
|
||||
{
|
||||
SPI_DIAG_RECORD(s_diag.ring_request_count++);
|
||||
|
||||
/* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */
|
||||
if (m_ringPending) {
|
||||
return false;
|
||||
@@ -765,39 +1119,61 @@ bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload,
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 主循环轮询:打印延迟日志 + 按铃超时看护
|
||||
* 主循环轮询:打印延迟日志 + 按铃超时看护 + 周期/异常诊断报告
|
||||
* ============================================================ */
|
||||
|
||||
void AppSpiCommand::poll()
|
||||
{
|
||||
/* 打印 ISR 推迟的日志 */
|
||||
char log_copy[kLogBufSize] = {};
|
||||
bool has_log = false;
|
||||
|
||||
uint32_t save = RT_CRITICAL_SAVE();
|
||||
if (m_logPending) {
|
||||
memcpy(log_copy, m_logBuf, sizeof(log_copy));
|
||||
m_logPending = false;
|
||||
has_log = true;
|
||||
}
|
||||
RT_CRITICAL_RESTORE(save);
|
||||
|
||||
if (has_log) {
|
||||
printf("%s\r\n", log_copy);
|
||||
/* 1. 打印 ISR 推迟的日志队列(环形缓冲,避免丢弃) */
|
||||
while (m_logTail != m_logHead) {
|
||||
printf("%s\r\n", m_logQueue[m_logTail]);
|
||||
m_logTail = (uint8_t)((m_logTail + 1U) % kLogSlotCount);
|
||||
}
|
||||
|
||||
/* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
|
||||
/* 2. 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
|
||||
* 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */
|
||||
if (m_ringPending) {
|
||||
uint32_t save2 = RT_CRITICAL_SAVE();
|
||||
if (m_ringPending) {
|
||||
if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) {
|
||||
printf("[SPI-APP] 按铃超时取消 (ESP32 未响应, 释放 SPIready 重试)\r\n");
|
||||
SPI_DIAG_RECORD(s_diag.ring_timeout_count++);
|
||||
#if (CONFIG_SPI_DIAG_PERIODIC_PRINT != 0)
|
||||
printf("[SPI-DIAG][WARN] 按铃超时取消 (ESP32 未在 %lums 内响应, 释放 SPIready 重试)\r\n",
|
||||
(unsigned long)kRingTimeoutMs);
|
||||
#endif
|
||||
cancelRing();
|
||||
}
|
||||
}
|
||||
RT_CRITICAL_RESTORE(save2);
|
||||
}
|
||||
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0 && CONFIG_SPI_DIAG_PERIODIC_PRINT != 0)
|
||||
/* 3. 定期 / 增量打印 SPI 诊断摘要 */
|
||||
static uint32_t s_lastDiagPrintMs = 0;
|
||||
static uint32_t s_lastDiagErrCount = 0;
|
||||
const uint32_t now = SysTickTimer::getMs();
|
||||
const uint32_t totalErrs = s_diag.parse_fail_count + s_diag.rx_overrun_count +
|
||||
s_diag.short_rx_len_count + s_diag.session_mismatch +
|
||||
s_diag.seq_jump_count + s_diag.ring_timeout_count +
|
||||
s_diag.unknown_type_count + s_diag.handler_empty_count;
|
||||
|
||||
const bool hasNewError = (totalErrs > s_lastDiagErrCount && (now - s_lastDiagPrintMs >= 2000U));
|
||||
const bool periodicCheck = (s_diag.total_cs_assert > 0U && (now - s_lastDiagPrintMs >= 5000U));
|
||||
|
||||
if (hasNewError || periodicCheck) {
|
||||
s_lastDiagPrintMs = now;
|
||||
s_lastDiagErrCount = totalErrs;
|
||||
const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
|
||||
const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
|
||||
printf("[SPI-DIAG] CS:assert=%lu rls=%lu ok_v2=%lu | err[CRC:%lu noSOF:%lu short:%lu ovr:%lu sessMis:%lu jmp:%lu tmo:%lu] pins[CS:%d RDY:%d STAT:0x%02X]\r\n",
|
||||
(unsigned long)s_diag.total_cs_assert, (unsigned long)s_diag.total_cs_release,
|
||||
(unsigned long)s_diag.valid_v2_frames,
|
||||
(unsigned long)s_diag.err_crc_mismatch, (unsigned long)s_diag.err_no_sof,
|
||||
(unsigned long)s_diag.short_rx_len_count, (unsigned long)s_diag.rx_overrun_count,
|
||||
(unsigned long)s_diag.session_mismatch, (unsigned long)s_diag.seq_jump_count,
|
||||
(unsigned long)s_diag.ring_timeout_count, cs_pin, rdy_pin, (unsigned)(s_diag.last_spi_stat & 0xFFU));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -806,6 +1182,7 @@ void AppSpiCommand::poll()
|
||||
|
||||
void AppSpiCommand::onCsAssert()
|
||||
{
|
||||
SPI_DIAG_RECORD(s_diag.total_cs_assert++);
|
||||
if (m_csActive) {
|
||||
return;
|
||||
}
|
||||
@@ -817,9 +1194,16 @@ void AppSpiCommand::onCsAssert()
|
||||
|
||||
void AppSpiCommand::onCsRelease()
|
||||
{
|
||||
SPI_DIAG_RECORD(s_diag.total_cs_release++);
|
||||
if (!m_csActive) {
|
||||
/* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */
|
||||
m_rxLen = stopSpiDmas(kRxBufSize);
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.missed_cs_assert++;
|
||||
s_diag.last_rx_len = m_rxLen;
|
||||
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
|
||||
);
|
||||
postLogFmt("[SPI-DIAG][WARN] CS release without assert (got %u B)", (uint8_t)m_rxLen);
|
||||
drainSpiRxHw();
|
||||
if (m_txLen > 0U) {
|
||||
m_txLen = 0U;
|
||||
@@ -839,18 +1223,15 @@ void AppSpiCommand::onCsRelease()
|
||||
|
||||
/* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable,否则计数器被清) */
|
||||
m_rxLen = stopSpiDmas(kRxBufSize);
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.last_rx_len = m_rxLen;
|
||||
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
|
||||
);
|
||||
drainSpiRxHw();
|
||||
|
||||
/* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧,
|
||||
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。
|
||||
*
|
||||
* 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 v2 协议帧(如 PING/OTA/0xB0)。
|
||||
* 若 GD32 正在按铃时 ESP32 发协议帧,会因 m_ringPending=true 被误吞,
|
||||
* 导致命令无法处理。检测到 v2 SOF 时取消按铃并走正常命令处理。 */
|
||||
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 */
|
||||
if (m_ringPending) {
|
||||
/* 判断本次收到的数据是否为 v2 协议帧(扫描 SOF0 SOF1)。
|
||||
* 取代旧 containsSyncSof,直接用 V2_SOF0/V2_SOF1。
|
||||
* 0xB0 进 BOOT 现已走标准 v2 帧 handler,不再单独检测裸字节。 */
|
||||
bool is_v2_frame = false;
|
||||
if (m_rxLen >= 2U) {
|
||||
const uint16_t last = (m_rxLen < 16U) ? (uint16_t)(m_rxLen - 1U) : 16U;
|
||||
@@ -862,9 +1243,8 @@ void AppSpiCommand::onCsRelease()
|
||||
}
|
||||
}
|
||||
if (is_v2_frame) {
|
||||
/* ESP32 发的是 v2 协议帧(如 PING/OTA/0xB0 等),不是按铃响应。
|
||||
* 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。
|
||||
* processCommand() 末尾由 updateDataReady() 依据新 m_txLen 驱动 PB6。 */
|
||||
SPI_DIAG_RECORD(s_diag.ring_collision_count++);
|
||||
postLog("[SPI-DIAG][INFO] Ring collision: host sent v2 frame while ring pending");
|
||||
m_ringPending = false;
|
||||
m_txLen = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
@@ -883,6 +1263,42 @@ void AppSpiCommand::onCsRelease()
|
||||
}
|
||||
|
||||
if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) {
|
||||
if (m_rxLen == 0U) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.zero_rx_len_count++;
|
||||
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT;
|
||||
s_diag.last_error_rx_len = 0U;
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
postLog("[SPI-DIAG][WARN] Ignored 0-byte transaction (SCK did not toggle)");
|
||||
} else if (isAllBytesValue(m_rxBuf, m_rxLen, 0xFFU)) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.all_ff_count++;
|
||||
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_FF;
|
||||
s_diag.last_error_rx_len = m_rxLen;
|
||||
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
postLogFmt("[SPI-DIAG][WARN] Ignored all-0xFF transaction (%u bytes)", (uint8_t)m_rxLen);
|
||||
} else if (isAllBytesValue(m_rxBuf, m_rxLen, 0x00U)) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.all_00_count++;
|
||||
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_00;
|
||||
s_diag.last_error_rx_len = m_rxLen;
|
||||
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
postLogFmt("[SPI-DIAG][WARN] Ignored all-0x00 transaction (%u bytes)", (uint8_t)m_rxLen);
|
||||
} else {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.short_rx_len_count++;
|
||||
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT;
|
||||
s_diag.last_error_rx_len = m_rxLen;
|
||||
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
|
||||
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
|
||||
);
|
||||
postLogFmt("[SPI-DIAG][WARN] Ignored short transaction (%u bytes < 20B)", (uint8_t)m_rxLen);
|
||||
}
|
||||
m_rxLen = 0U;
|
||||
/* 起机毛刺/相位漂移后仅 re-arm DMA 不够:SPI0 可能已 OVR 卡死。
|
||||
* 顺手重配 SPI0+DMA(不动 EXTI/session),下一笔 HELLO 才能收进来。 */
|
||||
@@ -894,12 +1310,6 @@ void AppSpiCommand::onCsRelease()
|
||||
|
||||
m_txLen = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
|
||||
/* ★ 关键:在 processCommand 之前同步 g_localCredit。
|
||||
* 此时 m_txLen=0, m_ringPending=false → g_localCredit=1。
|
||||
* 若不做,buildResponse 会读到上一次 pushStatus/requestEsp32Read 留下的
|
||||
* g_localCredit=0 → 响应帧携带 credit=0 → ESP32 m_remoteCredit=0 → 后续发送死锁。
|
||||
* 这是电机 OTA 吐量只有 1 chunk/2s 的根因。 */
|
||||
updateDataReady();
|
||||
|
||||
/* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式),
|
||||
@@ -920,12 +1330,20 @@ void AppSpiCommand::onInterrupt()
|
||||
/* RX 已改走 DMA0_CH1:RBNE 逐字节中断已禁用,本 ISR 只保留错误标志清理。
|
||||
* CS 边沿处理全部在 onCsExtiInterrupt 的 EXTI15 ISR 里完成。 */
|
||||
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
|
||||
/* RXORERR 理论上不应再出现(RX 走 DMA),但兜底清理避免标志卡死。
|
||||
* 读 SPI_DATA + SPI_STAT 清除 OVR 标志。 */
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.rx_overrun_count++;
|
||||
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
|
||||
);
|
||||
postLog("[SPI-DIAG][ERR] SPI0 RXORERR (RX Overrun Error)!");
|
||||
(void)spi_i2s_data_receive(SPI0);
|
||||
(void)SPI_STAT(SPI0);
|
||||
}
|
||||
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
|
||||
SPI_DIAG_RECORD(
|
||||
s_diag.format_error_count++;
|
||||
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
|
||||
);
|
||||
postLog("[SPI-DIAG][ERR] SPI0 FERR (Frame Format Error)!");
|
||||
spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include "sys/config.h"
|
||||
#include "device/spi/app_spi_diag.h"
|
||||
|
||||
class ISpiCommandHandler;
|
||||
|
||||
@@ -51,6 +52,9 @@ public:
|
||||
/** @brief 记录一条带 1 字节参数的待输出日志。 */
|
||||
void postLogFmt(const char *fmt, uint8_t val);
|
||||
|
||||
/** @brief 记录一条带双 32 位参数的待输出日志。 */
|
||||
void postLogFmt32(const char *fmt, uint32_t v1, uint32_t v2);
|
||||
|
||||
/**
|
||||
* @brief 将应答帧构建到指定发送缓冲区。
|
||||
* @param[out] tx_buf 发送缓冲区。
|
||||
@@ -85,6 +89,21 @@ public:
|
||||
/** @brief 查询当前是否有未读按铃(ESP32 尚未来读)。 */
|
||||
bool isRingPending() const { return m_ringPending; }
|
||||
|
||||
/** @brief 查询 v2 会话是否已建立。 */
|
||||
bool isV2Active() const;
|
||||
|
||||
/** @brief 查询当前 v2 会话 ID。 */
|
||||
uint32_t getSessionId() const;
|
||||
|
||||
/** @brief 查询当前 local credit。 */
|
||||
uint8_t getLocalCredit() const;
|
||||
|
||||
/** @brief 查询 CS 是否当前处于激活(低电平)状态。 */
|
||||
bool isCsActive() const { return m_csActive; }
|
||||
|
||||
/** @brief 打印完整 ASCII SPI 链路诊断报告。 */
|
||||
void dumpDiag() const;
|
||||
|
||||
/** @brief 取消未完成的按铃(释放 SPIready、清 m_ringPending)。
|
||||
* 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */
|
||||
void cancelRing();
|
||||
@@ -111,7 +130,12 @@ private:
|
||||
/** 本笔 CS 实际收到的字节数(CS 上升沿时由 RX DMA CHCNT 反算得到)。 */
|
||||
volatile uint16_t m_rxLen = 0U;
|
||||
volatile uint16_t m_txLen = 0U;
|
||||
volatile bool m_logPending = false;
|
||||
/** 日志环形缓冲区:避免高频/多事件并发时单缓冲覆盖丢日志。 */
|
||||
static constexpr uint8_t kLogSlotCount = 8U;
|
||||
static constexpr uint8_t kLogSlotLen = 128U;
|
||||
char m_logQueue[kLogSlotCount][kLogSlotLen] = {};
|
||||
volatile uint8_t m_logHead = 0U;
|
||||
volatile uint8_t m_logTail = 0U;
|
||||
/** RX DMA 接收缓冲(替代旧 RBNE 逐字节累积的 m_rxBuf)。
|
||||
* CS 上升沿时 m_rxLen 为本笔实际收到的字节数,本缓冲前 m_rxLen 字节有效。 */
|
||||
uint8_t m_rxBuf[kRxBufSize] = {};
|
||||
|
||||
@@ -35,6 +35,10 @@ static constexpr uint8_t kTypeExmcDiag = 0xA2U;
|
||||
* [12..24] fault_codes[13](每模块 1B 故障码,0=成功;编码见 sys/initial.h,
|
||||
* canfd = 原因码|(首个失败通道<<4))。位定义与 sys/initial.h 一致。 */
|
||||
static constexpr uint8_t kTypeInitStatus = 0xA3U;
|
||||
/** @brief SPI 链路全量诊断查询与控制(ESP32→GD32):
|
||||
* payload[0]=sub_cmd: 0=读取全量诊断快照 (ACK ~88B), 1=复位诊断计数器, 2=打印完整报告到 GD32 串口。
|
||||
* 若 payload 为空,默认执行 sub_cmd=0。 */
|
||||
static constexpr uint8_t kTypeSpiDiag = 0xA4U;
|
||||
static constexpr uint8_t kTypeBootReq = 0xB0U;
|
||||
static constexpr uint8_t kTypeCanSend = 0xC0U;
|
||||
static constexpr uint8_t kTypeCanFeedback = 0xC1U; /**< ESP32 -> GD32 轮询 CAN 反馈快照 */
|
||||
@@ -58,6 +62,14 @@ static constexpr uint8_t kTypeMotorVersion = 0xC6U;
|
||||
* 响应 ACK: count(1) + 每条目 1 字节状态(CanTxResult 码)。 */
|
||||
static constexpr uint8_t kTypeCanBurstSend = 0xC7U;
|
||||
|
||||
/** @brief CAN 通道收发器 Silent 开关(ESP32→GD32):只听不发(驱动关断、接收照常)。
|
||||
* 请求 payload: [0] channel 0/1/2, [1] silent 1=进 Silent / 0=回 Normal。
|
||||
* 应答 ACK: [0] channel, [1] 实际生效的 silent(与发送门控同一个值),[2] 该通道是否使能。
|
||||
* ERROR: 0xE0=payload 过短, 0xE1=通道未使能/非法, 0xE2=S 脚回读与意图不符(标志已回滚)。
|
||||
* 生效期间该通道所有发送入口(0xC0/0xC7/桥接/relay)返回 0xE6=kErrSilent,
|
||||
* 不计错误计数;静默不持久化,上电 init 一律回 Normal。 */
|
||||
static constexpr uint8_t kTypeCanSetSilent = 0xC8U;
|
||||
|
||||
/** @brief 电机 OTA relay 命令(ESP32→GD32):start / cancel / query。
|
||||
* payload[0]=cmd(0=start,1=cancel,2=query), start 时 payload[1]=ch, payload[2..5]=fw_size LE */
|
||||
static constexpr uint8_t kTypeMotorOtaCmd = 0xD0U;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#ifndef DEVICE_APP_SPI_DIAG_H
|
||||
#define DEVICE_APP_SPI_DIAG_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "sys/config.h"
|
||||
|
||||
/**
|
||||
* @file app_spi_diag.h
|
||||
* @brief SPI 链路全量硬件与协议诊断模块声明。
|
||||
*
|
||||
* 用于快速量化与定位 SPI 通信故障根因:
|
||||
* - 物理/时序层:CS 边沿跳变、丢失下降沿、0 字节事务(SCK未走/DMA落后)、短包(<20B)、全FF/全00
|
||||
* - 硬件状态:SPI0 RXORERR (接收FIFO溢出)、FERR (格式错误)、SPI_STAT 寄存器值
|
||||
* - 协议/帧层:SOF丢失、相位偏移(SOF drift)、版本错误、超长/截断、CRC32位跳变(附带期望值与计算值)
|
||||
* - 状态机/会话:Session ID脱节、序号跳变(丢包)、序号重复(重发)、未处理命令TYPE、空响应
|
||||
* - 握手/流控:PB6 DATA_READY 引脚电平、PA15 CS 引脚电平、主动按铃超时、按铃竞争碰撞
|
||||
* - 现场快照:记录最近一次异常发生时刻、实际接收长度、前导原始字节及 CRC 对比
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief SPI 帧解析失败原因细分
|
||||
*/
|
||||
enum SpiParseResult : uint8_t {
|
||||
SPI_PARSE_OK = 0U, ///< 成功解析合法 v2 帧
|
||||
SPI_PARSE_ERR_TOO_SHORT = 1U, ///< 事务长度小于 v2 最小帧头 (rxLen < 20)
|
||||
SPI_PARSE_ERR_ALL_FF = 2U, ///< 接收数据全为 0xFF (从机浮空/DMA未就绪/MISO未连接)
|
||||
SPI_PARSE_ERR_ALL_00 = 3U, ///< 接收数据全为 0x00 (地电平短路或总线死锁)
|
||||
SPI_PARSE_ERR_NO_SOF = 4U, ///< 未扫描到 SOF 同步字 (0xA5 0x5A)
|
||||
SPI_PARSE_ERR_BAD_VERSION = 5U, ///< 找到 SOF,但 VERSION != 0x02
|
||||
SPI_PARSE_ERR_BAD_LEN = 6U, ///< 长度异常 (payload_len > 236 或帧总长超出实际收到的 rxLen)
|
||||
SPI_PARSE_ERR_CRC_MISMATCH = 7U, ///< CRC32 校验失败 (物理信号干扰/位跳变)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief SPI 链路全量诊断计数器与状态快照
|
||||
*/
|
||||
struct SpiDiagStats {
|
||||
/* 1. CS 边沿与事务统计 */
|
||||
uint32_t total_cs_assert; ///< CS 下降沿次数
|
||||
uint32_t total_cs_release; ///< CS 上升沿次数
|
||||
uint32_t missed_cs_assert; ///< 丢失 CS 下降沿异常次数
|
||||
uint32_t zero_rx_len_count; ///< RX 收到 0 字节 (SCK 未时钟化或 DMA 启动严重落后)
|
||||
uint32_t short_rx_len_count; ///< RX 收到 1~19 字节 (<20B 残包)
|
||||
uint32_t all_ff_count; ///< 全 0xFF 事务数
|
||||
uint32_t all_00_count; ///< 全 0x00 事务数
|
||||
|
||||
/* 2. 硬件外设与状态 */
|
||||
uint32_t rx_overrun_count; ///< SPI0 RXORERR (接收 FIFO 溢出)
|
||||
uint32_t format_error_count; ///< SPI0 FERR (帧格式错误)
|
||||
uint16_t last_spi_stat; ///< 最近一笔事务结束时的 SPI_STAT 寄存器值
|
||||
|
||||
/* 3. 帧解析统计 */
|
||||
uint32_t valid_v2_frames; ///< 成功解析的合法 v2 帧总数
|
||||
uint32_t parse_fail_count; ///< 帧解析失败总数
|
||||
uint32_t err_no_sof; ///< 未找到 SOF0(0xA5)+SOF1(0x5A)
|
||||
uint32_t err_sof_drift; ///< SOF 偏移不在索引 0 (前导毛刺或字节时序偏移)
|
||||
uint32_t err_bad_version; ///< 版本不匹配
|
||||
uint32_t err_bad_len; ///< 长度字段错误或事务截断
|
||||
uint32_t err_crc_mismatch; ///< CRC32 校验失败
|
||||
|
||||
/* 4. 会话与序号流控 */
|
||||
uint32_t hello_count; ///< 收到 LINK_HELLO 次数
|
||||
uint32_t session_mismatch; ///< Session ID 不匹配 (对端重置或会话脱节)
|
||||
uint32_t seq_ok_count; ///< 序号严格递增正常次数
|
||||
uint32_t seq_jump_count; ///< 序号跳变 (丢帧)
|
||||
uint32_t seq_dup_count; ///< 序号重复 (重发包)
|
||||
uint16_t last_rx_len; ///< 最近收到的 rx_len (字节数)
|
||||
uint8_t last_rx_seq; ///< 最近收到的 rx_seq
|
||||
uint8_t expected_rx_seq; ///< 当前期望的 rx_seq
|
||||
uint8_t last_tx_seq; ///< 最近发送的 tx_seq
|
||||
|
||||
/* 5. 命令路由与处理 */
|
||||
uint8_t last_rx_type; ///< 最近收到的命令 TYPE
|
||||
uint8_t last_tx_type; ///< 最近构建的响应 TYPE
|
||||
uint32_t unknown_type_count; ///< 未知命令 TYPE (无 handler 注册)
|
||||
uint8_t last_unknown_type; ///< 最近未知的 TYPE 值
|
||||
uint32_t handler_empty_count; ///< Handler 执行但返回 tx_len == 0
|
||||
uint8_t last_empty_type; ///< 最近产出 0 字节的 TYPE 值
|
||||
uint32_t error_rsp_count; ///< GD32 主动返回 0x82 ERROR 响应次数
|
||||
uint8_t last_error_code; ///< 最近一次返回 0x82 的错误码 (0xE8/0xE9/0xEA/V2_ERR_*)
|
||||
|
||||
/* 6. 握手引脚与流控 */
|
||||
uint32_t ring_request_count; ///< 主动按铃请求次数
|
||||
uint32_t ring_timeout_count; ///< 按铃超时 (ESP32 未在期限内读走)
|
||||
uint32_t ring_collision_count; ///< 按铃与 ESP32 主动发帧竞争碰撞次数
|
||||
uint8_t current_local_credit; ///< 当前 local credit (0 或 1)
|
||||
|
||||
/* 7. 最近异常快照 (用于定位具体错误现场) */
|
||||
uint32_t last_error_uptime_ms; ///< 异常发生时刻 SysTick 毫秒数
|
||||
uint16_t last_error_rx_len; ///< 异常事务收到的字节数
|
||||
uint8_t last_error_bytes[8]; ///< 异常事务前 8 字节原始数据
|
||||
uint32_t last_error_calc_crc; ///< CRC 错误时: 计算出的 CRC
|
||||
uint32_t last_error_expected_crc;///< CRC 错误时: 帧内携带的 CRC
|
||||
uint8_t last_error_sof_offset; ///< SOF 所在的偏移位置 (255 表示未找到)
|
||||
uint8_t last_error_reason; ///< SpiParseResult 错误原因枚举
|
||||
};
|
||||
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
/** @brief 获取当前全局 SPI 诊断统计快照引用 */
|
||||
const SpiDiagStats& getSpiDiagStats();
|
||||
|
||||
/** @brief 复位清零所有 SPI 诊断统计计数器 */
|
||||
void resetSpiDiagStats();
|
||||
|
||||
/** @brief 将解析结果枚举转换为可读字符串 */
|
||||
const char* spiParseResultToString(SpiParseResult res);
|
||||
|
||||
/** @brief 打印完整 SPI 链路诊断报告到串口 (ASCII 表格) */
|
||||
void dumpSpiDiag();
|
||||
#else
|
||||
inline const SpiDiagStats& getSpiDiagStats()
|
||||
{
|
||||
static const SpiDiagStats s_empty{};
|
||||
return s_empty;
|
||||
}
|
||||
inline void resetSpiDiagStats() {}
|
||||
inline const char* spiParseResultToString(SpiParseResult /*res*/) { return "DISABLED"; }
|
||||
inline void dumpSpiDiag() {}
|
||||
#endif
|
||||
|
||||
#endif /* DEVICE_APP_SPI_DIAG_H */
|
||||
@@ -48,8 +48,28 @@ constexpr uint8_t CONFIG_MOTOR_MAX_NODES = 9U;
|
||||
/** @brief 电机离线判定超时 (ms)。超过此时间未收到 STATUS 帧则标记 offline。 */
|
||||
constexpr uint16_t CONFIG_MOTOR_OFFLINE_MS = 3000U;
|
||||
/** @brief SPI 命令逐帧日志(#if)。开启后仅异常路径输出
|
||||
* (非v2帧/未知type/SEQ跳变/session不匹配),正常状态 UART 静默。 */
|
||||
#define CONFIG_SPI_APP_LOG_ENABLE 1
|
||||
* (非v2帧/未知type/SEQ跳变/session不匹配),正常状态 UART 静默。
|
||||
* @note 2026-10-09 置 0:SPI 联调已结束,USART0 不再打印逐帧明细。
|
||||
* 排查时改回 1;异常现场数据仍可通过 0xA4 快照读回(check_spi_diag.py),
|
||||
* 因为 CONFIG_SPI_DIAG_ENABLE 仍为 1,计数器与现场快照照常记录。 */
|
||||
#define CONFIG_SPI_APP_LOG_ENABLE 0
|
||||
/**
|
||||
* @brief SPI 链路全量硬件与协议诊断总开关 (0=发布/关闭, 1=开启)。
|
||||
* @note 【一键发布开关】:
|
||||
* - 0 (发布模式): 彻底裁剪 0xA4 诊断指令与 Handler 注册,清空诊断内存占用,
|
||||
* 关闭中断与事务中的所有计数/现场快照记录,完全静默 USART0 诊断输出(零 CPU/RAM/Flash 开销)。
|
||||
* - 1 (调试/诊断模式): 开启 CS/时序/硬件寄存器/SOF偏移/CRC错误统计、现场快照及 0xA4 查询支持。
|
||||
* 发布固件时,将此处改为 0 即可一键关闭全部诊断功能并重新编译。
|
||||
* 当前保持 1:只静默串口输出(见下 PERIODIC_PRINT),保留 0xA4 远程查询能力。
|
||||
*/
|
||||
#define CONFIG_SPI_DIAG_ENABLE 1
|
||||
/**
|
||||
* @brief SPI 诊断 USART0 周期打印开关 (仅在 CONFIG_SPI_DIAG_ENABLE=1 时生效)。
|
||||
* @note 0=静默串口周期输出 (仍保留 0xA4 远程查询与现场快照,不刷屏);
|
||||
* 1=每 5s 周期性或异常发生时在 USART0 打印 [SPI-DIAG] 单行摘要。
|
||||
* 2026-10-09 置 0:按铃超时取消告警同样按本开关裁剪(计数器仍经 SPI_DIAG_RECORD 记账,可 0xA4 读回)。
|
||||
*/
|
||||
#define CONFIG_SPI_DIAG_PERIODIC_PRINT 0
|
||||
/** @brief CAN_SEND 成功帧详细日志(#if)。 */
|
||||
#define CONFIG_SPI_CAN_SEND_VERBOSE_LOG 0
|
||||
/** @brief CAN-FD BRS 使能(canfd.cpp #if)。
|
||||
@@ -155,8 +175,8 @@ constexpr uint32_t CONFIG_CAN_QUIET_AFTER_SEC = 10U;
|
||||
* APP 固件版本号
|
||||
* ============================================================ */
|
||||
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
|
||||
constexpr uint32_t APP_FW_VERSION_MINOR = 6U;
|
||||
constexpr uint32_t APP_FW_VERSION_PATCH = 51U; /* 1.6.50: CAN 空通道自动识别与降噪 */
|
||||
constexpr uint32_t APP_FW_VERSION_MINOR = 7U;
|
||||
constexpr uint32_t APP_FW_VERSION_PATCH = 2U; /* 1.7.2: CAN 通道收发器 Silent(只听不发)——S 脚 API + 0xC8 命令 + 0xC1 bus_state bit5;静默期发送入口回 0xE6 且不计错误 */
|
||||
constexpr uint32_t APP_FW_VERSION_BUILD = 0U;
|
||||
|
||||
|
||||
@@ -209,11 +229,17 @@ constexpr uint16_t CONFIG_SPI_LOG_BUF_SIZE = 256U;
|
||||
constexpr uint32_t CONFIG_SPI_RING_TIMEOUT_MS = 50U;
|
||||
/** @brief SPI 命令 handler 注册表容量。
|
||||
*
|
||||
* 当前 init 注册 12 个:boot_request / ota_wait / ping / init_status / can_bridge /
|
||||
* can_burst / can_feedback / id_snapshot / adc_feedback / motor_state / motor_ota / exmc_diag。
|
||||
* 当前 init 注册 13 个:boot_request / ota_wait / ping / init_status / can_bridge /
|
||||
* can_burst / can_feedback / id_snapshot / adc_feedback / motor_state / motor_ota /
|
||||
* exmc_diag / spi_diag。1.7.2 的 0xC8(CAN Silent)复用 can_feedback handler
|
||||
* 多加一个 frame_type,不占新槽位。
|
||||
* registerHandler 表满时静默丢弃,motor_ota 曾因容量=8 被丢弃(0xD0 回 0xEE)。
|
||||
* 取 14 留余量;新增 handler 时须确认本容量足够。 */
|
||||
* 新增 handler 时须确认本容量足够。 */
|
||||
#if (CONFIG_SPI_DIAG_ENABLE != 0)
|
||||
constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 16U;
|
||||
#else
|
||||
constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 14U;
|
||||
#endif
|
||||
/** @brief GD32 APP 版本(HELLO_ACK/PING 身份块;改功能后请递增便于现场核对)。 */
|
||||
/* 版本号在 app_spi_cmd_ping.cpp fillGd32AppIdentityPayload 维护 */
|
||||
/** @brief 每通道不重复 ID 快照表最大条目数(与 ESP32 CANFD_ID_SNAPSHOT_MAX 对齐)。 */
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
feature: ecat-wkc-periodic-dip
|
||||
status: delivered
|
||||
updated: 2026-09-27
|
||||
branch: ecat-dcu-flash-crypto-audit (主工作区共享;本文件不提交)
|
||||
commits: 28d8426..uncommitted # 仅本报告,无生产代码提交
|
||||
---
|
||||
|
||||
# EtherCAT WKC / 丢帧规律性波动 — 根因定位
|
||||
|
||||
## Report
|
||||
|
||||
**What was built** — 仅定位报告(无生产代码改动)。结论:EtherCAT WKC/丢帧在「几十~几百 ms」上的规律起伏,**默认首选根因 B**——输出侧 `PDO_OutputMapping` 探针跳过 + `process` 内每 **256 拍**强制全量读,若「12B 探针+末字」不能释放 SM2 3-buffer,则写侧 WKC 会按 256×T 周期性塌陷/回升(0.5–2 ms PDO → 128–512 ms,与现象带重合)。**并列根因 A**——输入侧稀疏/多段 PRAM 写是否真正提交 SM3 3-buffer,固件只保证「计划触达 end address」(F1 测试),硬件 WKC 未验证;但 `fillDiagTail` 的 `s_diagTick` 使段 3+5 在 `process()` 正常时永变,**不能**把「差分 skip 窗口」当作固定时间轴(残余形态为 A1 提交语义 / A2 重入陈旧 skip / A3 写长摆动)。**C**(250 ms GuardAleventMask + 0xC5 SPI)可把凹坑对齐到 250 ms 并加深。**D**(PD 看门狗回读失败)签名是 100 ms 全错,仅作鉴别。完整证据链、锚点与 S2.7 对拍步骤见正文。
|
||||
|
||||
**Verification** — 静态:`rg` 核对 `CONFIG_ECAT_SPARSE/DIFF_ESC_WRITE=1`、`s_lastForceReadSeq>=256`、`flush=2`、末 2B 写、250 ms guard、`dc_app.c` WKC 注释、PD watchdog 1s 回读、`tests/tier1_unit/test_f1_esc_write_plan.cpp` + `TEST_READY.md` F1 均存在且与正文一致。独立复审两轮:首轮 2 critical(tests 误报已驳回并补精确引用;A 权重过高已接受并重排为 B 主 A 并列),复审 clean。无 ARM 交叉编译,未上机。工作区并行 dirty(warm-jump 1.7.0 / flash-crypto 审计)未触碰。
|
||||
|
||||
**Journey log**
|
||||
1. 先验把差分 skip 的「内容变化节奏」当主时间轴,被 `fillDiagTick` 每拍自增证伪——稳态进不了全 skip。
|
||||
2. 首轮 reviewer 误报 `tests/` 不存在;`Test-Path` + `TEST_READY.md` L18/L150 驳回。分析报告引用测试时必须落到真实路径与断言名。
|
||||
3. 输出侧 256 拍兜底是唯一与内容无关的机械时钟,与「总是、规律」同构,故升主因。
|
||||
4. `esc_exmc_bus.h` 注释写 EXTI0+1,实现是 EXTI0/1/2——以实现为准。
|
||||
5. T6 留给现场 A/B 对拍;未对拍前不写补丁。
|
||||
|
||||
---
|
||||
|
||||
## [S1] Problem
|
||||
|
||||
用户现象:**EtherCAT(E卡特)总是出现规律性波动**,观察面为 **WKC / 丢帧**,节奏大约 **几十 ms 到几百 ms**。
|
||||
|
||||
WKC(Working Counter)在过程数据里表达「本拍从站是否提供了/吃掉了新缓冲」:
|
||||
|
||||
- 输入侧(SM3,从站→主站):主站读到**未被从站重写**的 3-buffer 槽时 **不累加 WKC**(仓库内已有明确注释,`dc_app.c` APPL_StartInputHandler)。
|
||||
- 输出侧(SM2,主站→从站):若从站长期不完整读走缓冲,3-buffer 被占满后主站写侧 WKC 同样会掉。
|
||||
|
||||
因此「WKC 规律性掉」本质是:**某一侧的 SM 缓冲没有按主站期望的节奏完成提交/释放**,且触发恢复的事件本身有几十~几百 ms 的周期。
|
||||
|
||||
本次交付为**仅定位报告**(用户明确不改生产代码),给出根因排序、代码证据、以及可上机对拍的验证步骤。
|
||||
|
||||
## [S2] Design / 根因分析
|
||||
|
||||
### S2.0 周期从站稳态路径(判断前提)
|
||||
|
||||
| 模式 | 过程数据节拍 | 输入写 ESC | 输出读 ESC |
|
||||
|------|--------------|------------|------------|
|
||||
| SM-Synchron / SM2 | `PDI_Isr`(EXTI1) | 每拍 `PDO_InputMapping`(`u16SyncType` 为 SM/SM2 时) | `PDO_OutputMapping` |
|
||||
| DC Sync0 | `Sync0_Isr`(EXTI0, preempt=0) | `ECAT_Application` 每拍;`PDO_InputMapping` 仅当 `LatchInputSync0Value` 与计数匹配(`ecatappl.c` Sync0 分支) | PDI 未映射时由 Sync0 补 `PDO_OutputMapping` |
|
||||
| free-run | `MainLoop` → `ECAT_Application` | 同上 | 同上 |
|
||||
|
||||
输入写路径(`ecatappl.c` `PDO_InputMapping`):
|
||||
|
||||
1. `APPL_InputMapping` → `EcatPdo_WriteInputs` 整填对象存储 `aPdInputData`
|
||||
2. `EcatPdo_EscWritePlan` 产出**多段**写计划(有副作用:更新影子/flush)
|
||||
3. 逐段 `HW_EscWriteIsr` → `EscLan9252::pramWrite`(每段一次独立 PRAM FIFO 事务)
|
||||
|
||||
当前编译开关(`sys/config.h`):
|
||||
|
||||
- `CONFIG_ECAT_SPARSE_ESC_WRITE = 1` — 跳过密排空洞
|
||||
- `CONFIG_ECAT_DIFF_ESC_WRITE = 1` — 三区差分,稳定且冲刷完则跳过;全跳过时**只写末 2 字节**「触发 SM3 结束地址提交」
|
||||
|
||||
### S2.1 根因 B(主因,高)— 输出侧:探针跳过 + 256 拍强制全量读,SM2 缓冲释放可能不足
|
||||
|
||||
**为何升为主因**:这是唯一**与内容无关、固定节拍**的恢复时钟,与「总是、规律性、几十~几百 ms」最同构。输入侧差分写在稳态下几乎不会进入「全 skip」(见 S2.2 的 `s_diagTick` 约束),不能单独解释固定节奏。
|
||||
|
||||
**机制**
|
||||
|
||||
`PDO_OutputMapping`(`ecatappl.c`)每拍:
|
||||
|
||||
1. 读 12B 头探针(SEQ@0 / WIN@8 / FLAGS@3)
|
||||
2. `EcatPdo_OutputProbe` 假(应用层 seq 无变化沿)→ **只读 SM2 末字** 2B
|
||||
3. 为真 → 至少读 52B 前缀;`count@48>0` 才读满 384B
|
||||
4. 兜底:`EthercatPdo::process` 内每 **256 个 process 拍**强制一次输出全量读(`s_lastForceReadSeq`)
|
||||
|
||||
SM2 同样是 **3-buffer**。若「首字探针 + 末字」**不能**释放读缓冲,而完整 `readOutputs` 才释放一格,则:
|
||||
|
||||
- 主站应用 seq 不变(空闲/保持命令)时,从站几乎不释放 SM2
|
||||
- 3 格写满后主站写侧 **WKC 下跌**
|
||||
- 每到 256 拍强制全量读释放一格(或若干格)→ **WKC 周期性回升**
|
||||
|
||||
**周期算术与「几十~几百 ms」对齐**
|
||||
|
||||
| 过程数据周期 | 256 拍强制读间隔 |
|
||||
|--------------|------------------|
|
||||
| 0.5 ms | 128 ms |
|
||||
| 1 ms | 256 ms |
|
||||
| 2 ms | 512 ms |
|
||||
| 4 ms | 1024 ms(偏慢,除非还有其他恢复源) |
|
||||
|
||||
1–2 ms 周期下强制读间隔正落在「几十~几百 ms」。这是**独立于内容变化**的固定时钟。
|
||||
|
||||
**代码锚点**
|
||||
|
||||
| 位置 | 内容 |
|
||||
|------|------|
|
||||
| `comm/ssc/src/ecatappl.c` `PDO_OutputMapping` | 12B 探针;skip 只补末字 |
|
||||
| `comm/ethercat_pdo.cpp` `process` 末 256 拍兜底 | `seqNow - s_lastForceReadSeq >= 256` |
|
||||
| `comm/ethercat_can_bridge.cpp` `peekOutput` | 仅应用 seq 沿 / FLAGS bit0 才要求全量 |
|
||||
|
||||
### S2.2 根因 A(并列,高)— 输入侧:多段/稀疏 PRAM 写与 LAN9252 3-buffer「提交」语义可能不对齐
|
||||
|
||||
**机制**
|
||||
|
||||
SM3 为 **3-buffer**。主站侧 WKC 只在「从站本轮真正交出一块新缓冲」时 +1。交出条件在 ESC 硬件上通常是:
|
||||
|
||||
- 写侧字节计数达到 `SM.Length`(顺序填满当前写缓冲),或
|
||||
- 访问到缓冲末地址触发 buffer 切换(部分 ESC 如此)
|
||||
|
||||
固件假设是后者(`ethercat_pdo.cpp` 注释;`TEST_READY.md` F1「SM3 End Address Commit Guarantee」;`tests/tier1_unit/test_f1_esc_write_plan.cpp` 的 `touchesSm3EndAddress` / `none_changed_fallback_guarantees_end_addr`):
|
||||
|
||||
```text
|
||||
无论各段是否变化,必须保证每周期必定写入 SM3 结束地址 (kPdoInBytes-1),
|
||||
以触发 LAN9252 3-buffer 提交换冲,否则主站将读到冻结的旧缓冲。
|
||||
全段跳过时返回「末 2 字节」一段以完成 SM 事件。
|
||||
```
|
||||
|
||||
未闭环点:
|
||||
|
||||
1. **F1 / `Audit_F1_SM3_End_Address_Guarantee` 只断言写计划触达 end address**(软件计划层)。**没有**验证 LAN9252 在「非顺序、多段、总长常 < 600B」的 PRAM 写之后是否旋转 3-buffer、主站 WKC 是否 +1。测试文件在树内(`tests/tier1_unit/test_f1_esc_write_plan.cpp` 等),断言对象是计划几何,不是硬件 WKC。
|
||||
2. **稀疏写**只写「段1+段2有效区」与「段3+段5」,中间空洞不写。每次 `pramWrite` 是独立的 `ADDR_LEN+CMD` 事务,SM 写指针/字节计数是否跨事务累计,固件侧**无回读观测**。
|
||||
3. 仓库内 WKC 失败模式描述与「缓冲未被重写」一致(`dc_app.c` `APPL_StartInputHandler` 注释),本项目踩过同类坑。
|
||||
|
||||
**与「几十~几百 ms」的关系(约束后)**
|
||||
|
||||
`fillDiagTail` 每拍 `++s_diagTick` 写入段 5(584..595),落在段 3+5 差分区间(544..599)。因此 **`process()` 正常跑满时,段 3+5 每拍都判变化,不会进入「全 skip → 只写末 2B」**。不能把「差分 skip 窗口 = 内容变化周期」当作主时间轴。
|
||||
|
||||
更站得住的 A 形态是:
|
||||
|
||||
| 形态 | 节奏 | 是否像本现象 |
|
||||
|------|------|----------------|
|
||||
| A1 多段/稀疏 PRAM 无法提交 3-buffer(即便写了 end) | 常态偏低 + 在「偶发 600B 整包」时回升 | 偏慢性;除非整包/分段交替 |
|
||||
| A2 `process()` 重入门挡下后 `InputMapping` 仍走 skip(陈旧镜像) | 跟随抢占/救援,可能呈簇状 | 可与 C 叠加 |
|
||||
| A3 稀疏 `count` 变化导致写长在「有效区/整段」间摆动 | 跟随电机插拔/离线 | 不如 B 固定 |
|
||||
|
||||
**代码锚点**
|
||||
|
||||
| 位置 | 内容 |
|
||||
|------|------|
|
||||
| `comm/ethercat_pdo.cpp` `diffNeedWrite` / `escWritePlan` / `fillDiagTail` | 三区差分、flush=2、末 2B 假设;`s_diagTick` 使段 3+5 稳态永变 |
|
||||
| `comm/ssc/src/ecatappl.c` `PDO_InputMapping` | 多段 `HW_EscWriteIsr`,nseg==0 才 600B 整包 |
|
||||
| `comm/esc_lan9252.cpp` `pramWrite` | 每段独立 PRAM 事务,无 SM 字节计数回读 |
|
||||
| `comm/ssc/port/dc_app.c` `APPL_StartInputHandler` | 权威注释:未重写缓冲 ⇒ WKC 不涨 |
|
||||
| `tests/tier1_unit/test_f1_esc_write_plan.cpp` + `TEST_READY.md` F1 | 只测 end-address 触达,不测硬件 WKC |
|
||||
| `sys/config.h` `CONFIG_ECAT_SPARSE_ESC_WRITE` / `CONFIG_ECAT_DIFF_ESC_WRITE` | 均为 1(省字节路径全开) |
|
||||
|
||||
### S2.3 根因 C(中)— 250 ms 周期任务挤占 HBI 窗口,输入刷新相对主站读偏晚
|
||||
|
||||
`dc_app.c` 已写明:输入刷新若落在主站读之后,主站读到未重写缓冲 ⇒ **几乎每周期 WKC 偏低**。
|
||||
主循环上有两处 **250 ms** 量级的 HBI/SPI 周期工作:
|
||||
|
||||
1. **`EcatDbg_GuardAleventMask`**(`task_runner.cpp` `ethercatAleMaskGuard`,250 ms)
|
||||
连续读 SM0/1 几何、0x204、0x220、看门狗状态;`HW_EscRead*` 会走 `EscExmcBus::maskEnter`(屏蔽 EXTI0/1/2)。
|
||||
屏蔽期内 SYNC0/PDI 的 HBI 事务被推迟,单次守护可达数十~上百 µs。每 250 ms 打一次节拍孔 → **WKC 尖峰/凹坑周期 ≈ 250 ms**。
|
||||
|
||||
2. **0xC5 多电机状态 SPI 推送**(`CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS = 2500` = 250 ms)
|
||||
与 ESP32 SPI 会话耦合;若 SPI 命令处理仍在较高抢占级或与 EXMC 交错,会形成同量级阻塞(历史审计 `VULN-01` 曾量化过 SPI 突发对 SYNC0 的 160 µs 级冲击;本轮是否完全异步化需上板确认)。
|
||||
|
||||
**代码锚点**:`sys/task_runner.cpp` 289–315、627–630;`comm/ssc/port/dc_app.c` 603–728、730–801;`sys/config.h` 0xC5 周期。
|
||||
|
||||
### S2.4 根因 D(低~中,签名不同)— 过程数据看门狗 100 ms 调试周期
|
||||
|
||||
`EscLan9252::configPdWatchdog` 注释:
|
||||
|
||||
> 表现为「100ms 周期全 WKC 错、10/50ms 正常」
|
||||
|
||||
已把 PD 看门狗从默认 100 ms 延到 1 s。若回读失败(WARN 路径),主站用 100 ms 调试周期时会 **整窗 WKC 全错**,节奏精确 100 ms。
|
||||
与「总是规律性起伏(非全错)」不完全同构,列为**鉴别项**而非主因。
|
||||
|
||||
### S2.5 已排除 / 历史同族(避免误归因)
|
||||
|
||||
| 项 | 为何降权 |
|
||||
|----|----------|
|
||||
| `APPL_StartInputHandler` 覆写 `pIntMask` | 注释已标明不可覆写;若仍覆写会是「几乎每周期」WKC 低,不是几十 ms 节奏 |
|
||||
| SPI ISR 内 16 帧 CAN 突发 / 0xA2 10 ms 忙等 | 审计后改为快照+异步;若未上线该修复,症状会更接近 Sync0 Lost / SAFEOP,而非纯 WKC 起伏 |
|
||||
| 批量 PRAM 弹 DWORD | 1.6.2 已回退,与 WKC 无直接关系 |
|
||||
| 电机 STATUS 1 kHz | `s_diagTick` 已使段 3+5 稳态永变;STATUS 稀密只影响段 2 写长,不是 skip 主因 |
|
||||
| 主站 `expected_wkc` / 多从站拓扑配错 | 可能造成恒定偏低或错预期,但**不会**自己产生几十 ms 机械节拍;排除靠主站配置截图/拓扑,不在本仓库 |
|
||||
|
||||
### S2.6 综合判断(对用户现象的最佳匹配)
|
||||
|
||||
```text
|
||||
现象:WKC/丢帧 · 几十~几百 ms · 总是存在
|
||||
│
|
||||
├─ 严格等周期、空闲也抖(0.5–2ms PDO → 128–512ms)→ 根因 B(256 拍)★ 默认首选
|
||||
├─ 与整包写/分段写切换或 process 重入救援相关 → 根因 A(提交语义 / 陈旧 skip)
|
||||
├─ 若约 250 ms 对齐、串口/SPI 活动时加重 → 根因 C
|
||||
└─ 若 100 ms 且 WKC 全错 → 根因 D(看门狗回读失败)
|
||||
```
|
||||
|
||||
**默认主假设(未上板前)**:**B 为主**(固定 256 拍恢复时钟),**A 为并列机制缺口**(多段 PRAM 是否提交 3-buffer 未验证,可造成常态偏低或与 B 叠加的毛刺),**C** 可把凹坑对齐到 250 ms 并加深。
|
||||
|
||||
### S2.7 建议的上机对拍(不改代码也能做)
|
||||
|
||||
1. **输出侧(先做,验证根因 B)**
|
||||
- 临时把 `process` 内 256 拍强制全量读改成 1(或主站持续递增 seq),看输出 WKC 起伏是否消失/周期是否拉长 → 对齐 256×T 即实锤 B。
|
||||
|
||||
2. **输入侧开关 A/B(验证根因 A)**
|
||||
- 组1:`CONFIG_ECAT_DIFF_ESC_WRITE=0` `CONFIG_ECAT_SPARSE_ESC_WRITE=0`(恒 600B 整包写)
|
||||
- 组2:现网 `1/1`
|
||||
同主站周期看 WKC **基线**是否抬高、毛刺是否变缓 → 验证 A1。同时盯 `reentryCount` / `s_opFreshRescues` 是否与凹坑同相(A2)。
|
||||
|
||||
3. **250 ms 孔**
|
||||
- 临时让 `ethercatAleMaskGuard` 空转(或把 250 ms 改到 2 s),看凹坑周期是否离开 250 ms → 验证根因 C。
|
||||
|
||||
4. **仪表(已有,SDO 0x2000)**
|
||||
- `sub5..12`:PDI/Sync0/InWrite 周期累计(1s 差分)— InWrite 在 skip 时应显著变小
|
||||
- `sub13..23`:DIFF-STATS(需 `CONFIG_ECAT_DIFF_STATS=1`)— 看 changed/runs 是否与 WKC 凹坑同相
|
||||
- `EcatPdo::reentryCount` / `s_opFreshRescues`:重入与新鲜度救援是否在凹坑时上升
|
||||
- 主站侧:WKC 时间序列与电机 STATUS 到达时刻对齐
|
||||
|
||||
5. **看门狗鉴别**
|
||||
- 确认串口启动行 `PD watchdog = 1s (0x0420=...)`;若只有 WARN,则单独怀疑根因 D。
|
||||
|
||||
## [S3] Out of Scope
|
||||
|
||||
- 不修改生产代码(用户指定仅定位报告)。
|
||||
- 不触碰工作区并行改动(`ecat-dcu-flash-crypto-audit` 分支上的 warm-jump/1.7.0 与 flash-crypto 审计文档);它们不改变 `CONFIG_ECAT_*_WRITE` 开关,不影响本结论。
|
||||
- 不重刷 SII/EEPROM、不改 PDO 布局或主站解码。
|
||||
- 不在此文档给出最终补丁设计(待对拍锁定 A/B/C 后另开实现 spec)。
|
||||
- 不审计 BOOT/OTA 路径(与本次 WKC 节奏无直接证据链)。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] T1: 梳理稳态 PDO 节拍与写/读 ESC 路径 — acceptance: S2.0 与代码一致 (covers: S2.0)
|
||||
- [x] T2: 定位输入侧 WKC 与 3-buffer 提交的机制缺口 — acceptance: S2.2 含证据链;明确 `s_diagTick` 约束 (covers: S2.2)
|
||||
- [x] T3: 定位输出侧探针/256 拍强制读与 SM2 释放缺口 — acceptance: S2.1 含周期算术并升为主因 (covers: S2.1)
|
||||
- [x] T4: 列出 250 ms HBI 孔与看门狗鉴别项 — acceptance: S2.3/S2.4 可执行 (covers: S2.3; S2.4)
|
||||
- [x] T5: 给出上机对拍步骤 — acceptance: S2.7 可逐步执行且能区分 A/B/C/D (covers: S2.1; S2.2; S2.3; S2.4)
|
||||
- [ ] T6: (待现场)按 S2.7 完成 A/B 对拍并回填 Report — acceptance: Report 记录开关矩阵与 WKC 曲线结论 (covers: S2.6)
|
||||
Reference in New Issue
Block a user