feat(ethercat): 添加LAN9252睡眠挂起机制防止HBI访问冲突

添加了s_pmSuspended状态标志来管理LAN9252的电源管理模式,在芯片进入D1-D3
睡眠状态时阻止HBI访问以避免唤醒问题。同步修改了ethercat_sync中断处理程序,
在睡眠挂起期间跳过HBI相关操作确保芯片能够正常维持睡眠状态。

fix(spi): 提升SPI CS EXTI抢占优先级避免帧丢失

将SPI CS EXTI中断优先级从1提升至0,使其能够抢占PDI/SYNC1等关键中断,确
保SPI帧释放处理能够在微秒级完成,避免因延时导致的ESP32 CS间隔超时和帧
丢失问题。

feat(ssc): 实现OP输入新鲜度不变量监控机制

添加了输入映射序列号监控机制,当PDI路径中断时能够检测到输入数据冻结并
自动触发强制刷新,解决结构洞问题导致的输入数据停止更新现象。

chore(pdo): 更新PDO输入字节注释说明布局变更历史
This commit is contained in:
cbb
2026-09-13 18:59:57 +08:00
parent 6afaacd03a
commit 6136c2363f
12 changed files with 524 additions and 56 deletions
+23
View File
@@ -228,6 +228,7 @@ bool EscLan9252::s_busReady = false;
bool EscLan9252::s_coreReady = false;
bool EscLan9252::s_resetDone = false;
bool EscLan9252::s_pmPhyDown = false;
bool EscLan9252::s_pmSuspended = false;
void EscLan9252::hardwareReset()
{
@@ -254,6 +255,8 @@ void EscLan9252::hardwareReset()
printf("[ECAT][ESC] hardwareReset: RST# low %ums -> released, pin reads %s\r\n",
(unsigned)CONFIG_ESC_NRST_ASSERT_MS,
pinHigh ? "HIGH" : "*** LOW (net shorted to GND / pin damaged) ***");
/* RST# 复位清掉 PM 睡眠态(芯片回 D0),挂起门控随之解除(1.6.27) */
s_pmSuspended = false;
}
uint8_t EscLan9252::nrstDrive(uint8_t action)
@@ -266,6 +269,8 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESC_NRST_PIN);
gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN);
/* 复位释放后芯片重走上电流程回 D0,睡眠挂起随之解除(1.6.27) */
s_pmSuspended = false;
break;
case 1: /* 持续拉低(手动保持复位) */
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
@@ -275,6 +280,7 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
case 3: /* 释放:浮空输入,网络交给 R44 10k 上拉 */
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_IN_FLOATING, 0,
CONFIG_ESC_NRST_PIN);
s_pmSuspended = false;
break;
case 4: /* 只读回,不改驱动 */
break;
@@ -315,6 +321,12 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
* 本操作的语义 = "读状态(若在睡眠则主动打破睡眠)",非无侵入观测。 */
const uint32_t pmt = readReg32(LAN9252_REG_PMT_CTRL);
status = pmPackStatus(pmt);
/* 读到 D0(SLEEP_EN=0):芯片不在睡眠,若门控仍挂起(如 op4 唤醒超时后
* 芯片延迟自醒)则自愈解除,恢复主循环/ISR 的 HBI 访问(1.6.27)。 */
if ((status & 0x02U) == 0U && s_pmSuspended) {
s_pmSuspended = false;
printf("[ECAT][PM] status: chip in D0, pm suspend gate released\r\n");
}
printf("[ECAT][PM] status: PMT_CTRL=0x%08lX READY=%u SLEEP_EN=%u MODE=D%u\r\n",
(unsigned long)pmt,
(unsigned)((status >> 0) & 1U), (unsigned)((status >> 1) & 1U),
@@ -332,6 +344,11 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
/* PM_MODE 与 PM_SLEEP_EN 必须分两次写(DS 6.3.5.1 注:同一写中改 MODE 置 EN 无效)。 */
writeReg32(LAN9252_REG_PMT_CTRL, v);
writeReg32(LAN9252_REG_PMT_CTRL, v | PMT_CTRL_PM_SLEEP_EN);
/* 1.6.27:立即挂起全部 HBI 消费者(task_runner 停 MainLoop/守护/心跳,
* ethercat_sync 三 ISR 清 pending 后直接返回)——本函数运行于 CS EXTI ISR,
* 退出后主循环第一圈 ECAT_Main 读 0x220 就会把刚睡着的芯片叫醒;
* 此前置位窗口内本 ISR 是唯一运行者(maskEnter 已屏 EXTI0/1),无竞态。 */
s_pmSuspended = true;
/* 1.6.24 实测:进入睡眠后任何 HBI 访问(含读!)都会立即触发唤醒——
* 手册 6.3.4.2 只说 HBI "write" 唤醒,但芯片实际按主机 CS 活动判定,
* 写入后的读回验证会在 <50ms 内把芯片叫回 D0(板级复现:进 D1 后
@@ -372,6 +389,12 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
printf("[ECAT][PM] wake: PHY power-up %s (autoneg re-negotiating)\r\n",
restored ? "OK" : "FAIL (retry action 34)");
}
if (awake) {
/* 唤醒成功:解除 HBI 挂起,主循环/ISR 恢复运行(1.6.27)。
* 超时则保持挂起——芯片大概率仍睡着;若实际已醒,下一次 op0
* 状态查询读到 D0 会自愈解除。 */
s_pmSuspended = false;
}
status = awake ? pmPackStatus(readReg32(LAN9252_REG_PMT_CTRL)) : 0U;
printf("[ECAT][PM] wake: %s -> status=0x%02X (READY=%u SLEEP_EN=%u)\r\n",
awake ? "OK" : "TIMEOUT (10ms, poll action 30 / retry 34)",
+10
View File
@@ -101,6 +101,15 @@ public:
* 落空(AL Event Mask/邮箱 SM 配置写不进),调用方必须跳过 SSC 初始化。 */
static bool isCoreReady() { return s_coreReady; }
/** @brief ESC 已被 pmDrive 置入 D1-D3 睡眠、所有 HBI 消费者必须静默(1.6.27)。
* @note 1.6.24 实测睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。置位期间
* task_runner 停调 MainLoop()/AL 守护/AL 心跳,ethercat_sync 三个 EXTI
* ISR 清 pending 后直接返回——否则芯片进入睡眠后毫秒级就被主循环的
* ECAT_Main(读 0x220)叫醒,根本睡不住(2026-09-12 休眠 ~20s 即"复活")。
* 解除:pmDrive 唤醒/读到 D0、nrstDrive 释放复位、hardwareReset。
* ISR 上下文安全:读一个 bool 静态成员,无锁。 */
static bool pmSuspended() { return s_pmSuspended; }
private:
/** @brief 使能 LAN9252 的 IRQ 引脚输出(IRQ_CFG 0x054 / INT_EN 0x05C)。
* @note 1.5.43 新增,由 init() 在 PDI/核心就绪后调用。两寄存器复位默认全 0,
@@ -118,6 +127,7 @@ private:
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */
static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */
static bool s_pmPhyDown; /**< pmDrive 进 D3 时两 PHY 已被置 PowerDown,唤醒后需恢复。 */
static bool s_pmSuspended; /**< ESC 睡眠挂起:置位期间全固件 HBI 消费者静默(见 pmSuspended)。 */
};
#endif /* COMM_ESC_LAN9252_H */
+4 -33
View File
@@ -52,40 +52,11 @@
/**
* 输入过程数据字节数(从站→主站,SM3):75×64bit = 600B。
* 布局(1.6.21 密排):段1..2 0..543 · 段3 544..583 · 段5 584..595。
* 改长度须同步 bridge/dc_app/gen_eeprom/ESI/主站 decode,并烧 EEPROM 冷启动。
*
* ── 1.6.1 瘦身 640 → 496(2026-09-05)──
* 固件 DWT 探针(0x2000 sub9/sub10)实测:PDO_InputMapping 里 HW_EscWriteIsr
* 640B 写恒为 296µs/拍(29.7% CPU),且与 SYNC0 相位**无关**(主站 dc_bench
* 五形态对拍极差 0.04%)——PRAM 通道 ≈0.463µs/B 线性,写多少省多少,唯一的
* 减法就是减字节。真机 CAN 流量采样(6086 帧)DLC 只有 24/32 两档,批量接收
* 区每帧 64B 数据是纯富余:条目数据 64→32B(DLC 真值保留,超长帧截断、
* flags bit1 标记),诊断区前移,段 1/电机窗口/长帧快照全部不动。
*
* ── 1.6.15 扩到 600:批量接收改定长覆盖窗口(2026-09-08)──
* 瘦身后实测 496B 写 218.7µs/拍(21.9%)。批量区当时是"变长紧排 + 环形队列
* 排空",容量 3 帧/拍(=3000 帧/s),现场 4000 帧/s 下每拍恒丢 1 帧,且排空赤字
* 把队列长期压在饱和态——1.6.9~1.6.13 追的错标/拼接竞态正是饱和态喂出来的
* (判满用的 tail 一旦滞后,生产者就会写进消费者刚读过的槽)。
*
* 本版把批量区改成**定长覆盖窗口**:生产者按自己的游标写、绕圈覆盖,消费者
* 每拍整块取值,不再有 tail/判满/pending。代价是每槽要带一个 u32 seqlock
* (PDI_Isr 会抢占主循环写侧,没有它主站就能拿到"新 ID + 旧数据"的假帧),
* 所以 5 槽的预算是 5×44 而不是 5×40:
* 段1..3 0..359(不动) + 头 4B(360..363) + 5×44B(364..583)
* + 段5 诊断 12B(584..595) + 保留 4B = 600B
* 5 槽 = 5000 帧/s,对 4000 帧/s 留出 25% 余量;写侧 600×0.441 ≈ 265µs/拍
* (26.5%,仍远好于旧 640B 的 296µs,更是 4kHz 周期的 87.5% 占空不可行方案的
* 零头,见 tools/ecat/dc_bench.py 固化结论)。
*
* 覆盖窗口**不创造容量**:PDO 每拍能带几帧只由字节数决定,要接住 N 帧/拍,
* 槽数必须 ≥ N,否则一帧在两次拷贝之间就被覆盖掉(改完可看主站按 seq 空洞
* 算出的丢帧数与从站侧 OVF 是否一致)。
* 600 = 75×8;SM3 实占 3×600=1800B(0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,
* DPRAM 仍留 120B 余量。改这里必须同步重生成 SII(gen_eeprom.py)并断电冷启动。
*
* ── 1.6.21:总长仍 600B,内部布局改「电机整轮密排」──
* 段 2 扩到 32..543(头 8B + 最多 9×56B),段 3 前移到 544..583,段 5 仍 584。
* 删段 4 历史覆盖窗。长度不变 ⇒ SII/EEPROM 无需重刷;主站解码偏移必须同步。
* 注意:DWT sub10 量的是从站 PDI 写 ESC 耗时,**不是**主站 rx 段。
* 主站 rx 主要由线速往返决定(约 1KB @100Mbit ≈ 80µs 量级),两边 µs 不要对拍。
*/
#define ECAT_PDO_IN_BYTES 600
+19 -1
View File
@@ -7,6 +7,7 @@
* SYNC1 (EXTI2, preempt=1) → Sync1_Isr() : 预留
*/
#include "comm/ethercat_sync.h"
#include "comm/esc_lan9252.h" /* pmSuspended:ESC 睡眠挂起期 ISR 拒碰 HBI(1.6.27) */
#include "sys/config.h"
#include "main.h" /* g_app_uptime_sec */
#include "sys/cpu_load.h"
@@ -114,6 +115,12 @@ void EthercatSync::onSync0Exti()
/* 直接寄存器访问,绕过 GD32 库函数以消除可变抖动 */
if ((EXTI_PD & (uint32_t)CONFIG_ESC_SYNC0_EXTI) != RESET) {
EXTI_PD = (uint32_t)CONFIG_ESC_SYNC0_EXTI;
/* 1.6.27:ESC 睡眠挂起期,清了 pending 就走——Sync0_Isr 内的 PRAM/CSR
* 访问全是 HBI,读即唤醒。睡眠中 SYNC0 引脚本应无沿,此分支只兜
* 进入睡眠瞬间的残留 pending。 */
if (EscLan9252::pmSuspended()) {
return;
}
++s_sync0Count;
Sync0_Isr();
}
@@ -123,6 +130,10 @@ void EthercatSync::onEscIrqExti()
{
if (exti_interrupt_flag_get(CONFIG_ESC_IRQ_EXTI) != RESET) {
exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI);
/* 1.6.27:ESC 睡眠挂起期直接返回——PDI_Isr 会读 0x220/PRAM(HBI),读即唤醒 */
if (EscLan9252::pmSuspended()) {
return;
}
++s_escIrqCount;
/* PDI_Isr(preempt=1) 可被 Sync0_Isr(preempt=0) 抢占;HBI Indexed 是两步
* 事务(IDX 写 + DATA 读),被抢占即错址。此处屏蔽 EXTI0 实现互斥:
@@ -144,6 +155,10 @@ void EthercatSync::onSync1Exti()
{
if (exti_interrupt_flag_get(CONFIG_ESC_SYNC1_EXTI) != RESET) {
exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI);
/* 1.6.27:ESC 睡眠挂起期直接返回——Sync1_Isr 内为 HBI 访问 */
if (EscLan9252::pmSuspended()) {
return;
}
++s_sync1Count;
/* 同 onEscIrqExti:Sync1_Isr(preempt=1) 内的 HBI 访问需与 SYNC0 互斥 */
const uint32_t prevSync0 = NVIC_GetEnableIRQ(CONFIG_ESC_SYNC0_IRQn);
@@ -160,7 +175,10 @@ void EthercatSync::onTimerIrq()
#if (CONFIG_ETHERCAT_SYNC_MODE == 1)
if (timer_interrupt_flag_get(TIMER0, TIMER_INT_FLAG_UP) != RESET) {
timer_interrupt_flag_clear(TIMER0, TIMER_INT_FLAG_UP);
Sync0_Isr();
/* 1.6.27:ESC 睡眠挂起期跳过——Sync0_Isr 内为 HBI 访问 */
if (!EscLan9252::pmSuspended()) {
Sync0_Isr();
}
}
#else
/* EXTI 模式下 TIMER0 不使用,本函数为占位;CONFIG_ETHERCAT_SYNC_MODE=1 时激活。 */
+91
View File
@@ -149,6 +149,16 @@ extern volatile UINT32 g_dbg_appCyc;
/* [DIFF-STATS] 差分写统计窗口值(comm/ethercat_diff_stats.cpp),out[0..10] 对应 sub13..23 */
void EcatDiffStats_GetWindow(UINT32 *out);
/* 1.6.33:OP 输入新鲜度 — APPL_InputMapping 每次成功刷新就自增 */
static volatile UINT32 s_inMapSeq = 0U;
static UINT32 s_inMapSeqSeen = 0U;
static UINT32 s_inStaleBaseCyc = 0U;
static UINT32 s_opFreshRescues = 0U;
#include <stdint.h>
/* DFP 的 SystemCoreClock 是 uint32_t(=unsigned int);SSC 的 UINT32 是 unsigned long,
* 不能用 UINT32 重复声明。 */
extern uint32_t SystemCoreClock;
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。
* 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发"
* 收拢进 EthercatPdo 类,本文件退回纯 SSC 移植层:只留对象字典 + 三行转发。 */
@@ -437,6 +447,9 @@ void APPL_InputMapping(UINT16 *pInputData)
* SDO 读 0x6000 时看到与过程数据一致的内容。 */
EcatPdo_WriteInputs((UINT8 *)&s_inputObj.u64Data[0]);
memcpy(pInputData, &s_inputObj.u64Data[0], ECAT_PDO_IN_BYTES);
/* 1.6.33:OP 输入新鲜度不变量的活性计数。每次真正走到输入映射就自增,
* 供 EcatDbg_OpInputFreshness 判断 PDI 路径是否还活着。 */
++s_inMapSeq;
}
/*---------------------------------------------------------------------------
@@ -632,3 +645,81 @@ UINT32 EcatDbg_GuardAleventMask(void)
}
return actual;
}
/*---------------------------------------------------------------------------
* OP 输入新鲜度不变量 — 1.6.33
*
* 结构洞(用户实锤:1ms 也复现,复位 GD32 APP 才好):
* SM-Synchron 稳态下 MainLoop 的 free-run 分支被
* (!bEscIntEnabled || !bEcatFirstOutputsReceived) && !bDcSyncActive
* 整段跳过,输入 PDO **只**由 PDI_Isr 的 PDO_InputMapping 刷新。
* PDI 通路一停(门控/掩码/EXTI),ESC 仍答 WKC、AL 仍 OP,主站读到的是
* 冻住的旧缓冲 —— 网页绿灯+高 Hz、CPU/CAN 全像没解析。既有 1.5.60 只救
* "门关着",救不了"门开着但 EXTI 不来"。
*
* 根治:把「OP ⇒ 输入必须在 T ms 内被重写」做成不变量。
* - 正常路径:PDI_Isr 每拍自增 s_inMapSeq
* - 异常路径:seq 停涨超过阈值 → 主循环强制 PDO_InputMapping
* (屏中断防双写)+ 重开门/应答 SM2,主站侧 gseq/rx 立刻恢复前进
* 不依赖主站重拉、不依赖人工复位。MainLoop 若本身死锁则本函数也停 ——
* 那是另一类故障(心跳会停),不在本不变量覆盖范围。
*---------------------------------------------------------------------------*/
void EcatDbg_OpInputFreshness(void)
{
const UINT8 al = (UINT8)(nAlStatus & 0x0FU);
const UINT32 now = EcatDbg_Dwt();
const UINT32 seq = s_inMapSeq;
if (al != 8U || nPdInputSize == 0U) {
s_inMapSeqSeen = seq;
s_inStaleBaseCyc = now;
return;
}
if (seq != s_inMapSeqSeen) {
s_inMapSeqSeen = seq;
s_inStaleBaseCyc = now;
return;
}
/* seq 未推进:等满阈值再介入,避开两拍之间的正常空隙。
* 5ms @1ms 周期 = 连丢 ≥5 拍;@4ms 周期 = 连丢 1~2 拍,仍足够判死。 */
{
const UINT32 thresh = (SystemCoreClock / 1000U) * 5U;
if ((UINT32)(now - s_inStaleBaseCyc) < thresh) {
return;
}
}
/* 强制刷新:先重开门/首输出标志(门关着时 PDI 永远不会回来),
* 再在关中断窗口内做一次输入映射 —— 此路径极低频,关中断可接受。 */
{
UINT16 dummy = 0U;
const UINT16 evt = HW_GetALEventRegister();
if (!bEscIntEnabled) {
bEscIntEnabled = TRUE;
}
if (!bEcatFirstOutputsReceived) {
bEcatFirstOutputsReceived = TRUE;
HW_EscReadWord(dummy, nEscAddrOutputData);
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
}
__asm volatile ("cpsid i" ::: "memory");
PDO_InputMapping();
__asm volatile ("cpsie i" ::: "memory");
++s_opFreshRescues;
s_inMapSeqSeen = s_inMapSeq;
s_inStaleBaseCyc = EcatDbg_Dwt();
printf("[ECAT][FRESH] WARN: OP input frozen (seq=%lu) evt=0x%04X "
"(gate=%u first=%u dc=%u) -> forced PDO_InputMapping #%lu\r\n",
(unsigned long)seq, (unsigned)evt,
(unsigned)(bEscIntEnabled ? 1U : 0U),
(unsigned)(bEcatFirstOutputsReceived ? 1U : 0U),
(unsigned)(bDcSyncActive ? 1U : 0U),
(unsigned long)s_opFreshRescues);
}
}
UINT32 EcatDbg_OpFreshRescues(void)
{
return s_opFreshRescues;
}
+4
View File
@@ -32,6 +32,10 @@ void APPL_InputMapping(UINT16 *pInputData);
void APPL_OutputMapping(UINT16 *pOutputData);
void APPL_Application(void);
/* 1.6.33:OP 输入新鲜度不变量(task_runner 每圈调用;内部廉价,仅冻结时强制重写) */
void EcatDbg_OpInputFreshness(void);
UINT32 EcatDbg_OpFreshRescues(void);
#ifdef __cplusplus
}
#endif
+2 -2
View File
@@ -173,7 +173,7 @@ volatile UINT32 g_dbg_pdiCyc = 0; /* sub5: PDI_Isr 全函数累计周期 */
volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */
volatile UINT32 g_dbg_freerunCyc = 0; /* sub7: MainLoop free-run 分支(SM 同步下恒 0) */
volatile UINT32 g_dbg_pdiCnt = 0; /* sub8: PDI_Isr 触发次数(=每周期 1 次) */
volatile UINT32 g_dbg_inCyc = 0; /* sub9: PDO_InputMapping 全程(APPL+640B 写) */
volatile UINT32 g_dbg_inCyc = 0; /* sub9: PDO_InputMapping 全程(APPL+过程数据写) */
volatile UINT32 g_dbg_inWriteCyc = 0; /* sub10: 其中 HW_EscWriteIsr 640B 过程数据写 */
volatile UINT32 g_dbg_alCyc = 0; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */
volatile UINT32 g_dbg_appCyc = 0; /* sub12: ECAT_Application(桥 processOutput) */
@@ -200,7 +200,7 @@ BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is fini
*////////////////////////////////////////////////////////////////////////////////////////
void PDO_InputMapping(void)
{
/*ECAT_DBG(1.6.0 剖分:输入映射全程 + 640B 写细分)*/
/*ECAT_DBG(1.6.0 剖分:输入映射全程 + 过程数据写细分)*/
/* 1.6.22 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr
* 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。
* 1.6.25 差分写:在稀疏之上再跳过「与影子一致且 3-buffer 冲刷完成」的段。 */