feat(comm): 1.6.51 CAN 空通道自动静默 + PDO 诊断 + OTA 布局共用头

变更点:
1. CAN-FD(device/CANFD.cpp、sys/config.h)
   - 发送统一 FD+BRS(DLC≤8 不再回退经典 CAN);OTA 期仍按通道压制 BRS
   - 空通道自动识别:连续 5 个 1Hz 窗口无有效帧且有错误 → 静默上报;有帧即恢复
   - 三路 CAN 默认全开;RX parse budget 48→64
2. EtherCAT 桥 / PDO
   - 段1 增 txtxid/rx00ff;版本应答 0x00FF 粘滞发布,避免被 CONFIG 冲掉
   - STATUS 禁止写入 xrx 快照;processSeq 新鲜度救援
3. OTA / 配置
   - sys/config.h include 仓库内 shared/ota_layout.h(与 BOOT 共用,不再依赖仓外路径)
   - ota_boot_request 对齐配置页 word 布局
4. docs/audit_ecat_spi_wifi_ota_compatibility.md:ECAT/SPI/WiFi/OTA 兼容性审计
5. .gitignore:忽略 .trae/、docs/traework-import、评审过程稿
This commit is contained in:
cbb
2026-09-20 22:58:09 +08:00
parent 63af85e7be
commit 5fd986299c
32 changed files with 2592 additions and 781 deletions
+83 -50
View File
@@ -36,13 +36,10 @@ static inline bool releaseFifo0(uint32_t canPeriph)
CAN_RFIFO0(canPeriph) = CAN_RFIFO0_RFD0;
for (uint32_t n = 0U; n < 2000U; ++n) {
if ((CAN_RFIFO0(canPeriph) & CAN_RFIFO0_RFD0) == 0U) {
/* 1.6.11 注:这里曾加过 128/2048 拍的 settle 延迟,假设"自清后 MDATA
* 还在搬运"是装载时间竞态 —— 实测无效(正弦指令负载下拼接率不变)。
* 节点指纹实验证明拼接是 FIFO 缓冲指针与帧边界失同步(node3 的
* STATUS 帧头 24B = node2 的完整 CONFIG,错位恰为 CONFIG 帧长 6 字),
/* 节点指纹实验表明拼接是 FIFO 缓冲指针与帧边界失同步
* (STATUS 帧头 24B = 完整 CONFIG,错位恰为 CONFIG 帧长 6 字),
* 不是延迟能挡的;结构矛盾的帧由 handleRxInterrupt 的内容校验计数
* (1.6.12 起只计不丢 —— 管道必须诚实;源头修复见 1.6.13 的
* CONFIG_IRQ_PREEMPT_CAN_RX=0)。 */
* (只计不丢 —— 管道必须诚实;源头修复见 CONFIG_IRQ_PREEMPT_CAN_RX)。 */
return true;
}
}
@@ -55,7 +52,7 @@ static inline bool releaseFifo0(uint32_t canPeriph)
* 地址 CAN_RFIFOMDATA0 连读 dlen/4 次才取完(48B = 12 次),加上头部两次,一条
* 帧就是十几次有先后依赖的总线访问,整个过程**不是原子的**。若这期间硬件把下
* 一帧搬进邮箱窗口,就会读出"前几个字属于上一帧、后几个字属于这一帧"的拼接
* 体,而 id/dlc 取自最开始那次读 —— 实测表现为 STATUS 帧的 ID 配上 CONFIG 帧
* 体,而 id/dlc 取自最开始那次读 —— 表现为 STATUS 帧的 ID 配上 CONFIG 帧
* 的前 12~24 字节,且混合边界 100% 落在 4 字节边界上。
*
* 判据用 MP 寄存器:TS[31:16] 是硬件接收时间戳、DLENC[3:0] 是长度,同一帧内两
@@ -67,8 +64,8 @@ static bool receiveFrameChecked(uint32_t canPeriph, can_receive_message_struct *
{
/* 整段取数放进 BASEPRI 临界区:屏蔽 preempt>=1,把 EtherCAT 的 ESC(1)/SYNC1(1)
* 挡在外面。取数是十几次有先后依赖的寄存器读,被抢占就会读出跨帧拼接体。
* 1.6.13 起本 ISR 自身就是 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX),与 SYNC0
* 同级互不抢占,取数自此真正原子;MI/MP 回读校验保留,作为对硬件自发换帧
* 本 ISR 自身是 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX),与 SYNC0
* 同级互不抢占,取数真正原子;MI/MP 回读校验保留,作为对硬件自发换帧
* (dequeue 异步完成等)的兜底观测 —— rx_torn 计数即"该病理是否还在发生"。 */
const uint32_t basepri = RT_CRITICAL_SAVE();
@@ -109,7 +106,7 @@ static bool receiveFrameChecked(uint32_t canPeriph, can_receive_message_struct *
/* 取数后回读 MI+MP:任一变化 ⇒ 邮箱窗口在取数期间被换帧,这一帧是拼接体。
* 必须连 MI(完整 ID)一起比 —— 只比 MP 的 TS/DLENC 会漏:TS 是 16 位计数器,
* 相邻两帧可能同值,DLENC 也常常相同(都是 48),于是"ID 报 STATUS、数据却是
* CONFIG"这种错配能从校验里溜过去(实测占坏帧的绝大多数)。ID 变了就一定是
* CONFIG"这种错配能从校验里溜过去。ID 变了就一定是
* 换了帧,这条判据没有巧合空间。 */
const uint32_t keep = CAN_RFIFOMP_TS | CAN_RFIFOMP_DLENC;
const uint32_t mp1 = CAN_RFIFOMP(canPeriph, CAN_FIFO0);
@@ -419,13 +416,11 @@ bool CanFd::initNominalTiming(uint32_t canPeriph)
can_parameter.auto_bus_off_recovery = ENABLE;
can_parameter.auto_wake_up = DISABLE;
can_parameter.auto_retrans = ENABLE; /* 默认自动重发兜底偶发 ACK 丢失;OTA 停等协议期间由 setChannelAutoRetrans(ch,false) 临时关闭,避免重发打穿电机单缓冲 RX */
/* 1.6.11 真根因:这里曾是 ENABLE —— FIFO 满时**覆盖最旧邮箱**,而最旧邮箱
* 正是 CPU 正在读取的输出窗口。buildInput 在 PDI_Isr(preempt1) 占 ~357µs
* 会压住 CAN ISR(preempt2),发指令时电机 CONFIG 翻倍到 2kHz/节点、总线
* ~8kHz,阻塞窗口内到达 ~3 帧恰好打满 FIFO 深度 → 覆盖进行到一半被读走
* = 跨节点/跨类型拼接帧(指纹实验实锤:node2 的 CONFIG 条目带 node3 的
* 速度限 150 与反相角度;settle 延迟/重取/MI-MP 校验都挡不住,因为
* MI/MP 是覆盖序列里最后更新的一环)。DISABLE = 溢出丢新帧并置 RFO0
/* rec_fifo_overwrite = DISABLE:FIFO 满时丢新帧而非覆盖最旧邮箱。
* 覆盖最旧会伤及 CPU 正在读取的输出窗口。buildInput 在 PDI_Isr(preempt1)
* 占 ~357µs 会压住 CAN ISR(preempt2),发指令时电机 CONFIG 翻倍到 2kHz/节点、
* 总线 ~8kHz,阻塞窗口内到达 ~3 帧恰好打满 FIFO 深度 → 覆盖进行到一半被读走
* = 跨节点/跨类型拼接帧。DISABLE 后溢出丢新帧并置 RFO0
* (rx_fifo_overfull_count 已在统计):STATUS/CONFIG 均周期量,丢一拍下拍
* 即补;覆盖递上来的才是网页乱码。与本文件软件队列"满丢新不覆旧"同则。 */
can_parameter.rec_fifo_overwrite = DISABLE;
@@ -579,7 +574,7 @@ void CanFd::initChannelHardware(uint8_t i)
CAN_TSTAT(ch.canPeriph) = CAN_TSTAT_MTF0 | CAN_TSTAT_MTF1 | CAN_TSTAT_MTF2;
can_interrupt_enable(ch.canPeriph, CAN_INT_TME | CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0);
nvic_irq_enable(ch.txIrq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i));
/* 1.6.13:RX 提到 preempt=0 —— 见 sys/config.h CONFIG_IRQ_PREEMPT_CAN_RX 注释。 */
/* RX 提到 preempt=0 —— 见 sys/config.h CONFIG_IRQ_PREEMPT_CAN_RX 注释。 */
nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN_RX, static_cast<uint8_t>(i));
s_rxQueues[i].head = 0U;
@@ -946,7 +941,7 @@ void CanFd::init()
/**
* @brief L0 轻量自检:无总线发帧,仅校验使能通道控制器自身状态(GD32 侧故障)。
* @note 1.5.35:不再检查 BUS_OFF/TEC/REC——未接从站的通道 RX 浮空噪声会累积 REC,
* @note 不检查 BUS_OFF/TEC/REC——未接从站的通道 RX 浮空噪声会累积 REC,
* 「没接设备」不等于错误;总线侧健康由 0xC1 反馈帧运行期上报。
*/
bool CanFd::selfTestL0(uint8_t *fault_code, uint8_t *fault_channel)
@@ -1136,11 +1131,13 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
tx_msg.tx_ft = CAN_FT_DATA;
tx_msg.tx_dlen = dlc;
#ifdef GD32E508
tx_msg.fd_flag = (dlc > 8U) ? ENABLE : DISABLE;
/* 只要通道硬件已启用 CAN-FD(3路均由 initFdMode 开启),统一发 FD 帧;
* 避免 DLC<=8 时回退经典 CAN 导致对端电机 DMA / 报文解析异常。 */
tx_msg.fd_flag = ENABLE;
#if (CONFIG_CAN_FD_BRS_ENABLE != 0)
/* 电机 BOOT 收不了 BRS 数据段,OTA 期间 relay 对该通道压制(setChannelBrsSuppress),
* DLC>8 帧回仲裁段速率(FD-no-BRS,OTA 长期验证过的组合)。 */
tx_msg.fd_brs = (dlc > 8U && !s_brsSuppress[channelIndex]) ? ENABLE : DISABLE;
* 压制中回退仲裁段速率(FD-no-BRS,OTA 长期验证过的组合)。日常发送统一启用 BRS 高速段。 */
tx_msg.fd_brs = (!s_brsSuppress[channelIndex]) ? ENABLE : DISABLE;
#else
tx_msg.fd_brs = DISABLE;
#endif
@@ -1467,24 +1464,18 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
/* 自己取帧并校验(见 receiveFrameChecked):厂商库不校验也不等 RFD 自清,
* 会把"半条上一帧 + 半条本帧"的拼接体当正常帧交上来。
*
* 1.6.10:**校验失败只丢不重取**。1.6.7 起这里是"原地重取最多 3 次",
* 实测被证伪为撕裂的直接制造者:RFIFOMDATA0 是"重复读同一地址、硬件读
* 指针自动前进"(厂商驱动证实),attempt1 失败时读指针已随取数前进若干
* 字,重取的 attempt2 从错位指针开读 —— MI/MP 回读比对的是"窗口身份",
* **校验失败只丢不重取**。RFIFOMDATA0 是"重复读同一地址、硬件读
* 指针自动前进",取数失败时读指针已随取数前进若干字,
* 重取会从错位指针开读 —— MI/MP 回读比对的是"窗口身份",
* 管不住"指针相对窗口起点的错位",于是校验通过、内容却是"上一帧的尾巴
* 拼下一帧的开头"。自测实锤:重取版 1.6.9 在电机活跃时错标率 9.155%
* (56835/620838),而 rx_torn 全程恒 0 —— 拼接恰好都从校验里溜过去。
* 丢帧无害:STATUS/CONFIG 都是 1kHz 周期量,下拍即补;递上去才会变乱码。
* CAN ISR 是 preempt=2,EtherCAT 的 SYNC(0)/ESC(1)/SYNC1(1) 都能抢占它,
* 这正是取数被打断的来源,也解释了为什么只有 EtherCAT 链路上看得到拼接、
* WiFi 链路干净。 */
* 拼下一帧的开头"。
* 丢帧无害:STATUS/CONFIG 都是 1kHz 周期量,下拍即补;递上去才会变乱码。 */
const bool intact = receiveFrameChecked(ch.canPeriph, &rx_message);
if (!releaseFifo0(ch.canPeriph)) {
s_diag.rx_release_timeout_count++;
}
if (!intact) {
/* 校验判为拼接:整帧作废,不计 rx_count、不入队、不转发。
* 本版起 rx_torn_count 恢复真实语义 —— 它恒 0 说明校验没在挡东西。 */
/* 校验判为拼接:整帧作废,不计 rx_count、不入队、不转发。 */
s_diag.rx_torn_count++;
continue;
}
@@ -1494,28 +1485,25 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
const uint32_t rxId = (rx_message.rx_ff == CAN_FF_STANDARD)
? rx_message.rx_sfid : rx_message.rx_efid;
/* ── 1.6.11:内容级拼接帧拦截 ──
* MI/MP 回读校验管不住 FIFO 缓冲指针与帧边界的失同步(1.6.10 的
* settle 延迟实测无效,指纹实验证明 node3 的 STATUS 帧头 24B 可以是
* node2 的完整 CONFIG —— 错位恰为 CONFIG 帧长 6 字,与混合帧长交错
* 相关,是 CAN IP 的指针病理,软件无法从取数时序上根除)。
* 改为在入队前做结构自洽校验:拼接帧的 (id, data) 必矛盾,而真帧必自洽。
/* ── 内容级拼接帧拦截 ──
* MI/MP 回读校验管不住 FIFO 缓冲指针与帧边界的失同步
* (指针病理,软件无法从取数时序上根除)。
* 在入队前做结构自洽校验:拼接帧的 (id, data) 必矛盾,而真帧必自洽。
* STATUS(0x000N0100, dlc48):CONFIG 头灌入必然把 MCU/Motor 温度推出
* [-40,125]℃ 物理量程(CONFIG B0-1 = 03+限压低字节 → 123~250℃ 或
* 负值),母线电压同步失序;
* CONFIG(0x000N0101, dlc24):DBC v0.74 规定 B15-23 未使用恒 0,
* STATUS 体灌入几乎必非 0;模式字节限 0..7(电机模式表最大 7)。
* 命中即计数(1.6.12 起不再丢帧——管道必须诚实,计数仅做"源头是否
* 还在产拼接帧"的测量):源头修复生效后本计数应恒 0,非 0 即失效告警。
* 计数经段5@494 上报。
* 残余风险:限压字节恰使温度落回量程内的理论组合(24V 台架实测限压
* ≥15V → 对应"温度"≥153℃,仍在量程外),可接受。 */
* 命中即计数(不丢帧——管道必须诚实,计数仅做"源头是否
* 还在产拼接帧"的测量):源头修复生效后本计数应恒 0,非 0 即失效告警。
* 计数经段5@494 上报。
* 残余风险:限压字节恰使温度落回量程内的理论组合
* (24V 台架实测限压 ≥15V → 对应"温度"≥153℃,仍在量程外),可接受。 */
{
const uint16_t idLo = (uint16_t)(rxId & 0xFFFFU);
bool reject = false;
/* 上一帧已接受的温度/电压(按节点):拼接帧的 CONFIG 字节可能恰好落进
* 静态量程(实测 vel=150 指纹时 0301→2.59℃、5a00→0.9℃、cace→52.9V
* 全部"合法"漏过),但绝不可能在 1ms 帧间隔里物理地跳过去 —— 斜率判据
* 静态量程,但绝不可能在 1ms 帧间隔里物理地跳过去 —— 斜率判据
* 是静态量程补不上的洞。阈值留 3 个数量级余量:温度 20℃/帧(实际
* ≤0.005℃),电压 5V/帧(实际 ≤0.05V)。 */
static int16_t s_lastMcu[16];
@@ -1540,7 +1528,7 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
if (dmcu < 0) { dmcu = (int16_t)-dmcu; }
if (dmot < 0) { dmot = (int16_t)-dmot; }
/* 只比温度斜率:拼接帧的 CONFIG 头必然把 mcu/mot 推出 20℃/帧
* (实测最小跳变 35℃,而真实温度 ≤0.005℃/帧,4 个数量级余量)。
* (实际温度 ≤0.005℃/帧,4 个数量级余量)。
* 电压不比斜率 —— 正弦换向/堵转时母线真实跌落可超 5V/帧,
* 误杀会让显示冻结;拼接的电压特征(≈48V)已由量程+温度斜率兜住。 */
if (dmcu > 2000 || dmot > 2000) {
@@ -1564,8 +1552,8 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
}
}
if (reject) {
/* 1.6.12:只计不丢 —— 数据管道必须诚实(用户裁定:过滤是遮羞布,
* 修的是采集源头)。网页/ESP32/主站看到的就是源头产出的原始字节,
/* 只计不丢 —— 数据管道必须诚实。
* 网页/ESP32/主站看到的就是源头产出的原始字节,
* 本计数退化为纯测量仪表:源头修好 = 恒 0;还在涨 = 源头问题仍在,
* 且网页如实显示着,不存在"看着干净其实坏了"。 */
s_diag.rx_content_reject_count++;
@@ -1737,6 +1725,15 @@ void CanFd::handleTxInterrupt(uint8_t channelIndex)
drainTxQueueIsr(channelIndex);
}
/* ── 空通道自动识别状态(2026-09-18)──
* 无对端的通道 RX 浮空 → 持续 STUFF、REC 冲 ~130 停在 ERR_PASSIVE,每 5s 刷一行
* [CAN-HW-ERR](config.h 老注释"CH1/CH2 未接设备,关闭避免持续 CAN 错误 + 占 SPI
* 带宽")。改为运行时自动判定:连续 CONFIG_CAN_QUIET_WINDOW 个窗口无有效帧增量
* 且有错误 → 静默上报;出现帧增量立刻恢复。只抑制上报,硬件保持 NORMAL 接收。 */
static uint8_t s_chanQuiet[3] = {0U, 0U, 0U}; /* 1=该通道已自动静默上报 */
static uint32_t s_rxCountPrev[3] = {0U, 0U, 0U}; /* 上一拍末尾的 rx_count */
static uint8_t s_quietMiss[3] = {0U, 0U, 0U}; /* 连续"无帧且有错"的窗口数 */
/**
* @brief 刷新三路 CAN 通道的 1Hz 反馈快照。
* @note 在临界区内拷贝实时计数与最近一帧信息到 s_feedback;保证 SPI 命令读到一致快照。
@@ -1766,6 +1763,37 @@ void CanFd::refreshFeedbackSnapshot()
}
s_feedbackSeq++;
/* 空通道自动识别:本拍无有效帧增量且有错误 → 记一次 miss,连续达到窗口长度才静默
* (加滞回,避免偶发丢拍/慢流量误判);一旦有帧增量立刻恢复上报。
* 时间基 = s_feedbackSeq(本函数 1Hz,见下面 %5 的 5s 打印节拍)。 */
for (size_t i = 0; i < kChannelCount; ++i) {
const uint32_t rxNow = s_channelStats[i].rx_count;
const uint32_t rxDelta = rxNow - s_rxCountPrev[i];
s_rxCountPrev[i] = rxNow;
const bool hasErr = (hw_tec[i] != 0U || hw_rec[i] != 0U
|| (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) != 0U);
if (rxDelta > 0U) {
s_quietMiss[i] = 0U;
if (s_chanQuiet[i] != 0U) {
s_chanQuiet[i] = 0U;
CAN_LOGI("[CAN] CH%u 恢复上报:检测到有效帧 (rx=%lu)\r\n",
static_cast<unsigned>(i), static_cast<unsigned long>(rxNow));
}
} else if (hasErr) {
if (s_quietMiss[i] < 0xFFU) s_quietMiss[i]++;
if (s_chanQuiet[i] == 0U
&& s_feedbackSeq >= CONFIG_CAN_QUIET_AFTER_SEC
&& s_quietMiss[i] >= CONFIG_CAN_QUIET_WINDOW_SEC) {
s_chanQuiet[i] = 1U;
LOG_ERR("[CAN] CH%u 无有效帧且错误累积 → 自动静默上报(本通道无设备/无对端;"
"硬件保持接收,插上设备有帧即自动恢复;TEC=%lu REC=%lu)\r\n",
static_cast<unsigned>(i),
static_cast<unsigned long>(hw_tec[i]), static_cast<unsigned long>(hw_rec[i]));
}
} else {
s_quietMiss[i] = 0U;
}
}
/* 每 5 秒采样硬件错误态;正常心跳不打印(统计走 SPI 0xC1)。
* 仅当 TEC/REC 或告警位非零时输出 [CAN-HW-ERR];全量 seq 心跳见 CONFIG_UART_DIAG_LEVEL。 */
if (s_feedbackSeq % 5U == 0U) {
@@ -1781,6 +1809,7 @@ void CanFd::refreshFeedbackSnapshot()
&& (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) == 0U) {
continue;
}
if (s_chanQuiet[i] != 0U) continue; /* 已自动静默的空通道:不再刷错误 */
LOG_ERR("[CAN-HW-ERR] CH%u ERR=0x%08lX ERRN=%lu(%s) TEC=%lu REC=%lu TSTAT=0x%08lX\r\n",
static_cast<unsigned>(i),
static_cast<unsigned long>(hw_err[i]),
@@ -1814,6 +1843,10 @@ void CanFd::refreshFeedbackSnapshot()
for (size_t i = 0; i < kChannelCount; ++i) {
uint8_t cur = s_feedback[i].stats.bus_state;
if (cur != s_prevBusState[i]) {
if (s_chanQuiet[i] != 0U) { /* 静默通道的状态变化只记账不打印 */
s_prevBusState[i] = cur;
continue;
}
const char *stateStr = "NORMAL";
if (cur & (1U << 0)) stateStr = "INIT_MODE";
else if (cur & (1U << 3)) stateStr = "BUS_OFF";
+4 -4
View File
@@ -167,7 +167,7 @@ public:
/**
* @brief 直接向指定 CAN 通道发送一帧(绕过软件队列)。
* @param allow_abort 邮箱全占时是否允许 abort 一个 PENDING 邮箱并重投一次。
* - true(0xC0 单帧路径默认):非阻塞顶替,适合"最新命令优先"的单帧发送;
* - true(底层非阻塞顶替):适合最新单发覆盖场景;高总线负载下慎用(可能 abort 掉正在等仲裁的帧);
* - false(突发/队列路径):邮箱满直接返回 kErrMailbox,交由 enqueueTx
* 的软件队列缓存,由 TX 完成中断回灌——避免 abort 把同通道已装载的
* 旧帧静默丢弃(>3 帧同通道突发只剩最后 3 帧上线的根因)。
@@ -182,7 +182,7 @@ public:
/** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */
static void setChannelAutoRetrans(uint8_t channelIndex, bool enable);
/** @brief 运行时压制指定通道 TX 帧的 BRS:压制中 DLC>8 的 FD 帧数据段回退仲裁段速率。
/** @brief 运行时压制指定通道 TX 帧的 BRS:压制中 FD 帧数据段回退仲裁段速率。
* 电机 BOOT 无法正确接收 BRS 高速段(见 sys/config.h CONFIG_CAN_FD_BRS_ENABLE 注释),
* 电机 OTA relay 经 enterState 钩子与 setChannelAutoRetrans 同点切换,OTA 结束必恢复。 */
static void setChannelBrsSuppress(uint8_t channelIndex, bool suppress);
@@ -246,14 +246,14 @@ public:
/** @brief 软件 TX 队列当前积压帧数(统计/诊断用)。 */
static uint32_t txQueueBacklog(uint8_t channelIndex);
/** @brief 接收队列容量深度(固定为 kRxQueueDepth = 16)。 */
/** @brief 接收队列容量深度(2 的幂;8kHz PDO 下主循环间隙短,加深缓冲防 STATUS 溢出丢帧)。 */
static constexpr uint8_t rxQueueDepth() { return kRxQueueDepth; }
/** @brief 查询指定通道软件接收队列当前积压帧数(无锁 SPSC,由 tail 与 head 计算)。 */
static uint32_t rxQueueCount(uint8_t channelIndex);
private:
static constexpr uint8_t kRxQueueDepth = 16U;
static constexpr uint8_t kRxQueueDepth = 64U; /* 16→64:1kHz STATUS×3 在 PDI 风暴下不再轻易打满 */
static constexpr uint8_t kRxQueueMask = kRxQueueDepth - 1U;
/** 每通道软件突发队列深度:覆盖整块突发(CAN_BURST_HARD_MAX=16 帧)中同一通道的
* 全部超载帧(3 邮箱之外最多 13 帧 = 16-3),保证任意 DLC 的整块突发不因队列满丢帧。 */
@@ -76,7 +76,11 @@ public:
return;
}
const CanFd::CanTxResult result = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc);
/* 0xC0 单帧发送改走 enqueueTx:
* 1) allow_abort=false 杜绝硬件邮箱被后续发送主动 abort 抹杀;
* 2) 电机高频上报抢占仲裁导致邮箱全占时,帧自动进入 13 深度软件队列,
* 由 TX 中断 drainTxQueueIsr 持续回灌空闲邮箱发送,彻底消除丢帧。 */
const CanFd::CanTxResult result = CanFd::enqueueTx(can_ch, can_id, can_data, can_dlc);
if (result == CanFd::CanTxResult::kOk) {
postCanSendLog(bus, "OK", can_ch, can_id, can_data, can_dlc);
const uint8_t ack = 0x00U;
+13 -16
View File
@@ -370,17 +370,15 @@ void AppSpiCommand::configureExti()
{
gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15);
exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH);
/* 1.6.35:先清 STAT 再开 IMR。原 enable→clear 顺序在 warm jump 下有害——
* BOOT 关中断跳转(v1.1.0.15)前 ESP32 持续拉 CS,EXTI15 STAT 大概率残留;
* 本函数在 AppSpiCommandBus.init() 里跑,此刻 PRIMASK 已被 init 开头清零,
* 若先 enable IMR,那个残留位会立即回放成一次假 CS 沿进 NVIC。 */
/* 先清 STAT 再开 IMR。warm jump 下 BOOT 关中断跳转前 ESP32 持续拉 CS,
* EXTI15 STAT 大概率残留;若先 enable IMR,残留位会立即回放成一次假 CS
* 沿进 NVIC。 */
exti_interrupt_flag_clear(EXTI_15);
exti_interrupt_enable(EXTI_15);
/* 1.6.32:SPI CS EXTI 抢占级 1→0。原 1 与 PDI/SYNC1/SCHED 同级不抢占
* (1.6.13 实测 PDI 窗口 ~357µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽;
* release ISR 一旦拖过 ESP32 的 CS 间隔(0.4.4 起 1ms),re-arm 落空,
* 下一笔查询帧头部丢失→GD32 解析失败回 0x82 0xE8→迟到被 serviceRing
* 偷读("偷读迟到 RESPONSE 0xEE/E8"风暴根因)。提到 0 后仅与 SYNC0/CAN-RX
/* SPI CS EXTI 抢占级 0。抢占级 1 与 PDI/SYNC1/SCHED 同级不抢占
* (PDI 窗口可达数百 µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽;
* release ISR 一旦拖过 ESP32 的 CS 间隔(1ms),re-arm 落空,
* 下一笔查询帧头部丢失。提到 0 后仅与 SYNC0/CAN-RX
* (µs 级)同级排队,release 处理确定性 µs 级完成。代价:0xA2 pmDrive 的
* 忙等(≤10ms,手动 PM 诊断才用)会阻塞 SYNC0——诊断窗口,可接受。 */
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U);
@@ -534,7 +532,7 @@ void AppSpiCommand::processCommand()
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]);
/* 1.6.30: 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..5]=rxBuf 前 4 字节。
/* 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..5]=rxBuf 前 4 字节。
* ESP32 /system/status rsp_err_last_p0..p3 直读,无需串口即可判定损坏形态
* (全FF=DMA 未装/CS 丢失;SOF 错位=相位漂移;SOF 对但 CRC 败=帧内位错)。 */
uint16_t tx_len = 0U;
@@ -554,8 +552,8 @@ void AppSpiCommand::processCommand()
return;
}
/* 1.6.29:逐帧"收到非刷写"行已移除(UART 刷屏);CONFIG_SPI_APP_LOG_ENABLE
* 现在只覆盖下方异常路径(非v2帧/未知type/SEQ跳变/session不匹配),正常态静默。 */
/* 正常帧不打日志(避免 UART 刷屏);CONFIG_SPI_APP_LOG_ENABLE
* 只覆盖异常路径(非v2帧/未知type/SEQ跳变/session不匹配),正常态静默。 */
g_pendingAckSeq = hdr.tx_seq;
@@ -650,15 +648,14 @@ void AppSpiCommand::processCommand()
if (!handled) {
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
uint8_t err = 0xE9U; /* 1.6.29: E9=无 handler 认领 */
uint8_t err = 0xE9U; /* E9=无 handler 认领 */
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
if (tx_len == 0U) {
uint8_t err = 0xEAU; /* 1.6.29: EA=handler 命中但未产出响应 */
uint8_t err = 0xEAU; /* EA=handler 命中但未产出响应 */
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
/* 1.6.29:这条兜底原本完全静默——0x82 0xEE 的三大来源里唯一不可见的。
* handler 命中但没产出响应(典型=buildResponse 构帧失败/业务拒绝)。 */
/* handler 命中但没产出响应(典型=buildResponse 构帧失败/业务拒绝)走此兜底错误路径。 */
postLogFmt("[SPI-APP] handler t=0x%02X tx_len=0 -> ERROR", frame_type);
}
+2 -3
View File
@@ -15,8 +15,7 @@ uint8_t Watchdog::init()
{
/* FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零)。
* BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开——
* 直接配置会让 PUD/RUD 永远等不到清零(库超时返回 ERROR,1.5.34 起暴露为 init 失败;
* 此前静默:FWDGT 实际按复位默认值 DIV4/0xFFF 运行,超时就与配置不符)。
* 直接配置会让 PUD/RUD 永远等不到清零(库超时返回 ERROR)。
* 必须先手动开 LSI 并等就绪,再配置。 */
rcu_osci_on(RCU_IRC40K);
if (rcu_osci_stab_wait(RCU_IRC40K) != SUCCESS) {
@@ -24,7 +23,7 @@ uint8_t Watchdog::init()
}
/* PSC/RLD 配置函数自带 ErrStatus(等待 PUD/RUD 清零带超时);
* 此前返回值被忽略,写入失败会无声变成"看门狗实际未按预期运行"。 */
* 必须检查返回值,否则写入失败会让看门狗实际未按预期运行。 */
fwdgt_write_enable();
uint8_t fault = kFaultOk;
if (fwdgt_prescaler_value_config((uint16_t)CONFIG_FWDGT_PSC) != SUCCESS) {