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:
@@ -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)",
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 时激活。 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 冲刷完成」的段。 */
|
||||
|
||||
@@ -372,7 +372,14 @@ void AppSpiCommand::configureExti()
|
||||
exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH);
|
||||
exti_interrupt_enable(EXTI_15);
|
||||
exti_interrupt_flag_clear(EXTI_15);
|
||||
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U);
|
||||
/* 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
|
||||
* (µs 级)同级排队,release 处理确定性 µs 级完成。代价:0xA2 pmDrive 的
|
||||
* 忙等(≤10ms,手动 PM 诊断才用)会阻塞 SYNC0——诊断窗口,可接受。 */
|
||||
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U);
|
||||
}
|
||||
|
||||
|
||||
@@ -523,17 +530,28 @@ 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 字节。
|
||||
* ESP32 /system/status rsp_err_last_p0..p3 直读,无需串口即可判定损坏形态
|
||||
* (全FF=DMA 未装/CS 丢失;SOF 错位=相位漂移;SOF 对但 CRC 败=帧内位错)。 */
|
||||
uint16_t tx_len = 0U;
|
||||
uint8_t err = 0xEEU;
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
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[6] = 0x5AU;
|
||||
diag[7] = 0xA5U;
|
||||
/* 非v2帧时 hdr 未初始化:禁止缓存/推进序号(垃圾缓存会被 dup 重放) */
|
||||
spi_build_frame(kRspError, diag, sizeof(diag), m_txBuf, &tx_len);
|
||||
m_txLen = tx_len;
|
||||
finishResponseAndReady();
|
||||
return;
|
||||
}
|
||||
|
||||
if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) {
|
||||
postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type);
|
||||
}
|
||||
/* 1.6.29:逐帧"收到非刷写"行已移除(UART 刷屏);CONFIG_SPI_APP_LOG_ENABLE
|
||||
* 现在只覆盖下方异常路径(非v2帧/未知type/SEQ跳变/session不匹配),正常态静默。 */
|
||||
|
||||
g_pendingAckSeq = hdr.tx_seq;
|
||||
|
||||
@@ -628,13 +646,16 @@ void AppSpiCommand::processCommand()
|
||||
|
||||
if (!handled) {
|
||||
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
|
||||
uint8_t err = 0xEEU;
|
||||
uint8_t err = 0xE9U; /* 1.6.29: E9=无 handler 认领 */
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
}
|
||||
|
||||
if (tx_len == 0U) {
|
||||
uint8_t err = 0xEEU;
|
||||
uint8_t err = 0xEAU; /* 1.6.29: EA=handler 命中但未产出响应 */
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
/* 1.6.29:这条兜底原本完全静默——0x82 0xEE 的三大来源里唯一不可见的。
|
||||
* handler 命中但没产出响应(典型=buildResponse 构帧失败/业务拒绝)。 */
|
||||
postLogFmt("[SPI-APP] handler t=0x%02X tx_len=0 -> ERROR", frame_type);
|
||||
}
|
||||
|
||||
m_txLen = tx_len;
|
||||
|
||||
@@ -45,8 +45,10 @@
|
||||
constexpr uint8_t CONFIG_MOTOR_MAX_NODES = 9U;
|
||||
/** @brief 电机离线判定超时 (ms)。超过此时间未收到 STATUS 帧则标记 offline。 */
|
||||
constexpr uint16_t CONFIG_MOTOR_OFFLINE_MS = 3000U;
|
||||
/** @brief SPI 命令逐帧日志(#if)。 */
|
||||
#define CONFIG_SPI_APP_LOG_ENABLE 0
|
||||
/** @brief SPI 命令逐帧日志(#if)。1.6.29 诊断:开启后仅异常路径会输出
|
||||
* (非v2帧/未知type/SEQ跳变/session不匹配),逐帧"收到非刷写"行已在
|
||||
* app_spi_command.cpp 关闭,正常状态 UART 静默。定位完改回 0。 */
|
||||
#define CONFIG_SPI_APP_LOG_ENABLE 1
|
||||
/** @brief CAN_SEND 成功帧详细日志(#if)。 */
|
||||
#define CONFIG_SPI_CAN_SEND_VERBOSE_LOG 0
|
||||
/** @brief CAN-FD BRS 使能(canfd.cpp #if)。
|
||||
@@ -91,7 +93,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
|
||||
* run 碎片有多严重、比对自身要多少钱)。临时仪表,取证完置 0 即与改造前逐字节等价。
|
||||
* @note 开启时每拍在 PDI_Isr 内多一次 600B 字粒度比对 + 一次快照拷贝(约 10µs 量级,
|
||||
* 实测见 sub23),因此 **sub9/sub10 的写侧耗时会包含这部分**,不能直接与历史
|
||||
* 296µs/265µs 对拍——要看差分潜力就只看 sub13..22,要看真实写侧耗时请置 0 重测。 */
|
||||
* 不能与历史写侧基准对拍——要看差分潜力就只看 sub13..22,要看真实写侧耗时请置 0 重测。 */
|
||||
#define CONFIG_ECAT_DIFF_STATS 0
|
||||
/**
|
||||
* @brief 输入 PDO 稀疏写 ESC(1.6.22 P1):按密排 count 只写「段1+段2有效区」与「段3+段5」,
|
||||
@@ -147,12 +149,12 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
|
||||
* ============================================================ */
|
||||
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
|
||||
constexpr uint32_t APP_FW_VERSION_MINOR = 6U;
|
||||
constexpr uint32_t APP_FW_VERSION_PATCH = 27U; /* A/B 结束: 恢复 1.6.25 稀疏+差分写(用户裁定, 主站侧已有后台恢复兜底) */ /* A/B C组: SPARSE=0+DIFF=0, 回 1.6.21 恒全量 600B 写 ESC(查开机楔死) */ /* 1.6.25:P1 输入 PDO 差分写 ESC(三区:段1 桥状态/段2 电机密排/段3+5 快照诊断;变化后 3-buffer 冲刷协议,稳定段跳过;全跳过写末 2 字节完成 SM)。P2 csrRead 分级:verify 参数,SM/AL/邮箱默认双读,诊断/MII busy/init 轮询单读。开关 CONFIG_ECAT_DIFF_ESC_WRITE。 */ /* 1.6.24:pmDrive 进 D1/D2/D3 移除读回验证。板级实测:进入睡眠后任何 HBI 访问(含读,手册 6.3.4.2 只说写唤醒,芯片实际按主机 CS 活动判定)都会 <50ms 触发唤醒回 D0——读回验证自证失败,改为置信构造返回值(READY=0|SLEEP_EN=1|MODE=op);「读状态」语义改为"若在睡眠则主动打破睡眠"。 */ /* 1.6.23:LAN9252 芯片级电源管理诊断路径。EscLan9252::pmDrive()(0xA2 action 30~34:30 读状态/31 进 D1/32 进 D2/33 进 D3/34 手动唤醒),按 DS 6.3 写 PMT_CTRL(0x084) PM_MODE+PM_SLEEP_EN(分两次写);D3 前置两 PHY General Power Down(经 ESC 核心 MII 管理 0x0510-0x0514,PHY A 地址 0/2 运行期判定),唤醒=HBI 写 BYTE_TEST 后轮询 BYTE_TEST/READY(DWT CYCCNT 忙等,50µs 步进、上限 10ms——0xA2 在 CS EXTI ISR 上下文执行,禁用 SysTickTimer::delayMs 会死等;HBI 段 maskEnter 屏 EXTI0/1 防 SYNC0 突入)。唤醒后自动恢复 PHY 上电(自协商需数秒)。休眠期间 EtherCAT 通信中断属预期,主站 wkc 报错。 */ /* 1.6.22:P1 稀疏写 ESC。PDO_InputMapping 在密排布局下按 count 只写「段1+段2有效区(0..40+count*56)」与「段3+段5(544..599)」,跳过空槽区,省 HW_EscWriteIsr 的 PRAM 字节。开关 CONFIG_ECAT_SPARSE_ESC_WRITE=1;非密排/count 撑满自动回退整包。总长 600B/SII/主站解码不变。A/B:置 0 重编即 1.6.21 写侧。 */ /* 1.6.21:段 2 改「一帧=总线电机整轮状态」。用户裁定旧需求不对:不要 win_base 滑窗扫 27 槽,也不要段 4 历史帧覆盖窗反复重发。实测病理:接 3 个电机网页却看到 5 个状态帧——固定 5 槽里空槽 node_id=0 也占位,主站不滤就多报。本版:(1) 段 2 密排,count=本拍实际电机数(0..9),空节点不占位,槽序按 node_id 升序,同号多通道取 seq 最新;主站 `for i in 0..count` 按槽内 node_id 自动识别,无需滑窗。(2) 删段 4 覆盖窗口(fillRxWindow/RxWinSlot 整套),非 STATUS 仍走段 1/3 最近一帧。(3) 差分填充:节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽 48B 拷贝。(4) 输入 PDO 总长仍 600B,SII 无需重刷;段 2 扩到 32..543、段 3 前移到 544、段 5 仍 584。**主站解码必须同步**(decode_input/transport-ecat.js:count 密排 + 段 3 偏移 320→544)。 */
|
||||
constexpr uint32_t APP_FW_VERSION_PATCH = 33U; /* 1.6.33: OP 输入新鲜度不变量(EcatDbg_OpInputFreshness)——SM-Synchron 下输入 PDO 只靠 PDI_Isr,通路一停主站读到冻缓冲(WKC/AL 仍正常=假连接);现 OP 内输入停更>5ms 由主循环强制 PDO_InputMapping+重开门。根治"连接看起来正常但 CPU/CAN 不更新、复位 GD32 才好"。 */ /* 1.6.32: SPI CS EXTI 提到抢占级0(见 app_spi_command configureExti),根治 CS release 被 PDI/RT_CRITICAL 延迟导致的 RX 撕裂(0xE8) *//* /* 1.6.28:ESC 睡眠挂起门控(EscLan9252::pmSuspended)。pmDrive 进 D1-D3 置位后全固件 HBI 消费者静默——task_runner 停调 MainLoop/AL 守护/AL 心跳(MainLoop 每圈 HBI 读 0x220,睡眠中任何 HBI 访问含读都把芯片打回 D0,2026-09-12 实测休眠 ~20s 即被主站"复活":芯片毫秒级被 GD32 自己叫醒、SM 失效后主站 wedge 重建 OP);ethercat_sync 三 EXTI ISR 与 timer Sync0 路径清 pending 即返。解除:pmDrive 唤醒成功/op0 读到 D0 自愈/nrstDrive 释放复位/hardwareReset。配套主站侧 ecat_native /api/pm/hold 楔死看门狗豁免。 */ /* A/B 结束: 恢复 1.6.25 稀疏+差分写(用户裁定, 主站侧已有后台恢复兜底) */ /* A/B C组: SPARSE=0+DIFF=0, 回 1.6.21 恒全量 600B 写 ESC(查开机楔死) */ /* 1.6.25:P1 输入 PDO 差分写 ESC(三区:段1 桥状态/段2 电机密排/段3+5 快照诊断;变化后 3-buffer 冲刷协议,稳定段跳过;全跳过写末 2 字节完成 SM)。P2 csrRead 分级:verify 参数,SM/AL/邮箱默认双读,诊断/MII busy/init 轮询单读。开关 CONFIG_ECAT_DIFF_ESC_WRITE。 */ /* 1.6.24:pmDrive 进 D1/D2/D3 移除读回验证。板级实测:进入睡眠后任何 HBI 访问(含读,手册 6.3.4.2 只说写唤醒,芯片实际按主机 CS 活动判定)都会 <50ms 触发唤醒回 D0——读回验证自证失败,改为置信构造返回值(READY=0|SLEEP_EN=1|MODE=op);「读状态」语义改为"若在睡眠则主动打破睡眠"。 */ /* 1.6.23:LAN9252 芯片级电源管理诊断路径。EscLan9252::pmDrive()(0xA2 action 30~34:30 读状态/31 进 D1/32 进 D2/33 进 D3/34 手动唤醒),按 DS 6.3 写 PMT_CTRL(0x084) PM_MODE+PM_SLEEP_EN(分两次写);D3 前置两 PHY General Power Down(经 ESC 核心 MII 管理 0x0510-0x0514,PHY A 地址 0/2 运行期判定),唤醒=HBI 写 BYTE_TEST 后轮询 BYTE_TEST/READY(DWT CYCCNT 忙等,50µs 步进、上限 10ms——0xA2 在 CS EXTI ISR 上下文执行,禁用 SysTickTimer::delayMs 会死等;HBI 段 maskEnter 屏 EXTI0/1 防 SYNC0 突入)。唤醒后自动恢复 PHY 上电(自协商需数秒)。休眠期间 EtherCAT 通信中断属预期,主站 wkc 报错。 */ /* 1.6.22:P1 稀疏写 ESC。PDO_InputMapping 在密排布局下按 count 只写「段1+段2有效区(0..40+count*56)」与「段3+段5(544..599)」,跳过空槽区,省 HW_EscWriteIsr 的 PRAM 字节。开关 CONFIG_ECAT_SPARSE_ESC_WRITE=1;非密排/count 撑满自动回退整包。总长 600B/SII/主站解码不变。A/B:置 0 重编即 1.6.21 写侧。 */ /* 1.6.21:段 2 改「一帧=总线电机整轮状态」。用户裁定旧需求不对:不要 win_base 滑窗扫 27 槽,也不要段 4 历史帧覆盖窗反复重发。实测病理:接 3 个电机网页却看到 5 个状态帧——固定 5 槽里空槽 node_id=0 也占位,主站不滤就多报。本版:(1) 段 2 密排,count=本拍实际电机数(0..9),空节点不占位,槽序按 node_id 升序,同号多通道取 seq 最新;主站 `for i in 0..count` 按槽内 node_id 自动识别,无需滑窗。(2) 删段 4 覆盖窗口(fillRxWindow/RxWinSlot 整套),非 STATUS 仍走段 1/3 最近一帧。(3) 差分填充:节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽 48B 拷贝。(4) 输入 PDO 总长仍 600B,SII 无需重刷;段 2 扩到 32..543、段 3 前移到 544、段 5 仍 584。**主站解码必须同步**(decode_input/transport-ecat.js:count 密排 + 段 3 偏移 320→544)。 */
|
||||
/* 1.6.19:[DIFF-STATS] 仪表成本终于测对了,并推翻 1.6.18 自己的判断。1.6.18 认定"是 -Oz 把 memcpy(&a,p,4) 编成函数调用",改成手拼 4 字节移位,结果反汇编出**每字 8 条 ldrb.w**、比开销从 21.3µs/拍涨到 41µs/拍 —— 方向完全反了。真根因不在这里:alignas(4) 标在数组声明上只到调用点为止,而 observe(const uint8_t*) 的形参签名不带对齐信息,优化器无从证明指针 4 字节对齐,两版都只能逐字节走。修法是对两个入参各做一次 __builtin_assume_aligned(p,4) 再走 memcpy —— 反汇编确认循环体已缩成 ldr.w/ldr.w/cmp/bne 每字 4 条 (150 字 ≈ 750 cycle ≈ 4.2µs/拍,与最初估算同量级),且整个函数 bl 归零(600B 快照拷贝也被折成字搬运)。教训:**测量工具的开销会直接变成结论**,"差分判断很贵"这句话差一点就被我自己造的假象坐实。被测量(变化字节数 25%、run 峰值 12)不受此影响,两版读数一致,仍然有效。 */
|
||||
/* 1.6.18:修 [DIFF-STATS] 仪表自己的假象 —— 比对循环里 `memcpy(&a,p,4)` 在 -Oz 下被编成**函数调用**而非单条 LDR,首轮实测 compare 开销虚报成 21.3µs/拍(≈300 次调用×995 拍),若拿它当"差分判断很贵"的证据就结论反了。改成小端 4 字节移位拼装:合法、不碰严格别名、且是 Clang 认得折 LDR 的模式。顺带把 alignas(4) 的理由改成"服务整块 memcpy",比对本身不再依赖它。Code 52716→52768(+52B),代价是体积不是速度。 */
|
||||
/* 1.6.17:新增 [DIFF-STATS] 临时仪表,为"输入 PDO 改差分写"取证,**零行为变更**。动机:差分写的收益完全取决于每拍真正变了多少字节、变化点碎成几个 run,而 run 碎片数决定它会不会把一次 600B 整块写退化成几百次带 PRAM_AVAIL 轮询的小写——这两个数今天没有实测依据,所以先只上仪表再决定做不做。实现:comm/ethercat_diff_stats.{h,cpp},每拍在 PDI_Isr 里与上一份已发布镜像做 u32 粒度比对,按 1s 窗口(DWT 周期,同 cpu_load 手法)滚动,getter 只回报"上一个完整窗口"以免主站读到半累积值;量为 cycles / changedMax / runsTotal / runsMax / 段1..段5 各自变化字节 / 比对自身 DWT 开销。经对象字典 0x2000 sub13..23 上报(新增 11 个只读 UINT32)。**只加 SDO 子索引、PDO 长度与映射逐字节不动 ⇒ 无需重生成 SII、无需断电冷启动**,可与主站独立升级。整模块由 CONFIG_ECAT_DIFF_STATS 门控,置 0 后与 1.6.16 等价;挂接点统一 [DIFF-STATS] 标记,删除=删 2 文件+删标记行。已知副作用:开启时 sub9/sub10 写侧耗时含比对开销,不能直接与历史 296µs/265µs 对拍,真实写侧基准须置 0 重测。 */
|
||||
/* 1.6.17:新增 [DIFF-STATS] 临时仪表,为"输入 PDO 改差分写"取证,**零行为变更**。动机:差分写的收益完全取决于每拍真正变了多少字节、变化点碎成几个 run,而 run 碎片数决定它会不会把一次 600B 整块写退化成几百次带 PRAM_AVAIL 轮询的小写——这两个数今天没有实测依据,所以先只上仪表再决定做不做。实现:comm/ethercat_diff_stats.{h,cpp},每拍在 PDI_Isr 里与上一份已发布镜像做 u32 粒度比对,按 1s 窗口(DWT 周期,同 cpu_load 手法)滚动,getter 只回报"上一个完整窗口"以免主站读到半累积值;量为 cycles / changedMax / runsTotal / runsMax / 段1..段5 各自变化字节 / 比对自身 DWT 开销。经对象字典 0x2000 sub13..23 上报(新增 11 个只读 UINT32)。**只加 SDO 子索引、PDO 长度与映射逐字节不动 ⇒ 无需重生成 SII、无需断电冷启动**,可与主站独立升级。整模块由 CONFIG_ECAT_DIFF_STATS 门控,置 0 后与 1.6.16 等价;挂接点统一 [DIFF-STATS] 标记,删除=删 2 文件+删标记行。已知副作用:开启时 sub9/sub10 写侧耗时含比对开销,不能直接与旧基线对拍,真实写侧基准须置 0 重测。 */
|
||||
/* 1.6.16:修段 1/段 3 快照 drop 的计数口径(纯判据修复,PDO 布局逐字节不动)。病理:`onCanFrame` 里判的是 `if (s_rxValid) ++s_dropCount`,而 s_rxValid 置位后**永不清零**(只有 flags bit0 清统计会归零计数,不动 valid),于是开机收到第一帧之后**每一帧都算一次丢弃**,drop ≡ rx−1 (mod 2^16)。实测 period=1ms、CAN 侧 6000 帧/s 时 drop 同样 6000/s,与 rx_count 逐拍 1:1(Δ 完全相等),(rx−1) mod 65536 与上报值精确吻合。后果:主站 `/api/motor/state` 的 bridge.drop 被 transport-ecat.js 当状态栏 **ERR** 显示、且它是桥全局值三通道同数 → 网页"ERR 多得离谱"其实只是收帧速率,零灵敏度、纯假仪表;calibrate_ecat_metrics 一类拿 drop 做分子分母的判据恒得 100%。本版加 s_rxPublished(写侧置 false / buildInput 段 1 发布分支置 true,与段 4 的 s_rxWinSeen 同一套"未被任何一拍取走才算丢"约定),判据改 `s_rxValid && !s_rxPublished`;计数落在 xrxSeq 奇数窗口内,消费者判奇会跳过段 1/段 3 故不会并发置位,读+自增对消费者等效原子。flags bit0 清统计时一并置 published=true,免得清零后的第一帧被误计。s_rxValid 语义不变("有过接收",flags bit0 与 backlog 取通道都依赖它常驻),故 ecat.html 短快照、daemon 的 xrx 分派、ESP32 侧 0xC1 全部零改动。**修完不会归零**:段 1/段 3 是单槽、每拍只能发布 1 帧,6000 帧/s @1ms 下仍约 5000/s(≈rx 的 0.83),这个数自此真正的含义是"快照槽位跟不上收帧速率",不是总线错误;真·CAN 错误仍看 ESP32 `/can/feedback` 的 err/TEC/REC,真·入窗丢帧看段 4 的 OVF(362) 与 seq 空洞。 */
|
||||
/* 1.6.15:批量接收改**定长覆盖窗口**,输入 PDO 496→600(75×64bit)。动机:1.6.1 瘦身后批量区是"环形队列 + 变长紧排排空",容量 3 帧/拍(=3000 帧/s),现场 4000 帧/s 下每拍恒丢 1 帧(can_cadence lost=512/drain FAIL);更糟的是排空赤字把队列长期压在饱和态,而 1.6.9~1.6.13 追的错标/拼接竞态正是饱和态喂出来的(判满用的 tail 一旦滞后,生产者就写进消费者刚读过的槽)。本版按用户裁定改成覆盖式:生产者(onCanFrame)按自己游标写、绕圈覆盖,消费者(fillRxWindow)每拍整块取值 —— 没有 tail/判满/pending,那个滞后握手的载体直接不存在。定长(5×44B@364)让主站不必推游标,1.6.10 那类边界越界写诊断区的错在结构上不可能再犯。**每槽必须带 u32 seqlock**:buildInput 跑在 PDI_Isr(preempt=0) 会抢占主循环写侧,没有它主站能拿到"新 ID + 旧数据"拼出的假帧(不是"差一个周期",是现实中不存在但看起来合法的帧,与刚修的拼接同形)。seq 兼作主站去重与丢帧判据(空洞数=被覆盖未取的帧),与从站侧新加的 OVF("未取先覆"次数) 互为对账仪表。语义变更:批量区从"保证不丢的事件流"变成"最近 N 帧窗口"——同一帧会连续几拍重复出现,低频一次性帧(版本/节点应答)在突发下可能被静默覆盖;主站 daemon 层按 seq 去重后归一成事件流,故 motor.js/ecat.html/can_cadence.py 判据不变、前端零改动。容量:5 槽=5000 帧/s@1ms,对 4000 帧/s 留 25% 余量(STATUS 自 1.6.9 起不入窗,实际入窗量是 CONFIG 量级);覆盖窗口**不创造容量**,槽数 < 帧/拍 仍会静默丢帧。写侧 600×0.441≈265µs/拍(26.5%),仍优于旧 640B 的 296µs,更是"提到 4kHz"方案 87.5% 占空不可行结论(dc_bench 固化)的零头。段1..3(0..359) 逐字节不动,段5 诊断区 484→584。三端同步:ethercat_pdo_size.h / bridge.h+cpp / dc_app.c(62→75) / gen_eeprom.py / DCU_ESI.xml / ecat_native.c decode_input(新增 600..639 分支,向后自适应 496 与 640)。必须重烧固件 + 烧 EEPROM + 断电冷启动,只烧一边 SAFEOP 报 AL 0x001A/0x0011。 */
|
||||
/* 1.6.15:批量接收改**定长覆盖窗口**,输入 PDO 496→600(75×64bit)。动机:1.6.1 瘦身后批量区是"环形队列 + 变长紧排排空",容量 3 帧/拍(=3000 帧/s),现场 4000 帧/s 下每拍恒丢 1 帧(can_cadence lost=512/drain FAIL);更糟的是排空赤字把队列长期压在饱和态,而 1.6.9~1.6.13 追的错标/拼接竞态正是饱和态喂出来的(判满用的 tail 一旦滞后,生产者就写进消费者刚读过的槽)。本版按用户裁定改成覆盖式:生产者(onCanFrame)按自己游标写、绕圈覆盖,消费者(fillRxWindow)每拍整块取值 —— 没有 tail/判满/pending,那个滞后握手的载体直接不存在。定长(5×44B@364)让主站不必推游标,1.6.10 那类边界越界写诊断区的错在结构上不可能再犯。**每槽必须带 u32 seqlock**:buildInput 跑在 PDI_Isr(preempt=0) 会抢占主循环写侧,没有它主站能拿到"新 ID + 旧数据"拼出的假帧(不是"差一个周期",是现实中不存在但看起来合法的帧,与刚修的拼接同形)。seq 兼作主站去重与丢帧判据(空洞数=被覆盖未取的帧),与从站侧新加的 OVF("未取先覆"次数) 互为对账仪表。语义变更:批量区从"保证不丢的事件流"变成"最近 N 帧窗口"——同一帧会连续几拍重复出现,低频一次性帧(版本/节点应答)在突发下可能被静默覆盖;主站 daemon 层按 seq 去重后归一成事件流,故 motor.js/ecat.html/can_cadence.py 判据不变、前端零改动。容量:5 槽=5000 帧/s@1ms,对 4000 帧/s 留 25% 余量(STATUS 自 1.6.9 起不入窗,实际入窗量是 CONFIG 量级);覆盖窗口**不创造容量**,槽数 < 帧/拍 仍会静默丢帧。段1..3(0..359) 逐字节不动,段5 诊断区 484→584。三端同步:ethercat_pdo_size.h / bridge.h+cpp / dc_app.c(62→75) / gen_eeprom.py / DCU_ESI.xml / ecat_native.c decode_input(新增 600..639 分支,向后自适应 496 与 640)。必须重烧固件 + 烧 EEPROM + 断电冷启动,只烧一边 SAFEOP 报 AL 0x001A/0x0011。 */
|
||||
/* 1.6.14:CAN-FD 数据段 2Mbps→5Mbps(CONFIG_CAN_DATA_PRESCALER 9→3、CONFIG_CAN_DATA_BS1 3TQ→4TQ ⇒ 90MHz/(3×6TQ),采样点 80%→83.3%;TDC filter/offset 4→12 tq 维持约 400ns,因 TDCF/TDCO 以数据段 tq 为单位)。动机:电机 STATUS48B+CONFIG24B×2 节点@1kHz 加 1kHz 下行 CMD,2Mbps 下负载约 120%(实测总线帧数被钉在 ~4993 帧/s),高频下发时 RX FIFO 拥塞。仲裁段仍 1Mbps 不变。配套:电机 APP 与 BOOT 的 data_prescaler 同步改 2(60MHz/(2×6TQ)=5Mbps)——FD 数据段速率必须全总线一致,节点 TX 都带 fd_brs。 */
|
||||
/* 1.6.13:根治 —— 1.6.12 的 PDI 入口预抽实测(60s 正弦指令负载,链路活性已断言):overfull 50/s→0 但指纹违规仍 ~92/s、torn 在无溢出阶段 1-7/s 持续涨 —— 判决:溢出只是共犯之一,**357µs 的 PDI 阻塞窗口本身**才是病理母体(窗口内 FIFO 满溢毁窗口 + 被冻结的取帧路径留着不依赖溢出的换帧路径,MI/MP 只抓到少数)。本版釜底抽薪:CAN RX ISR 提到 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX,与 SYNC0 同级互不抢占)—— RX 不再被 PDI/SYNC1/SCHED 压住,FIFO 深度 ≤2、溢出与冻结同时消失,receiveFrameChecked 的多字取数自此真正原子;TX ISR 留在 2(PDI 上下文有 TX 提交,提 0 会引入提交/释放竞争)。代价:SYNC0/PDI 偶发 µs 级延迟(RX ISR ~3µs/帧×8kHz≈2.4% 占空),相对 1ms 周期是零头。PDI 预抽 pumpAllChannels 随之撤除(失去意义且引入 PDI/ISR 双消费者风险)。保留 1.6.12 的诚实管道(校验只计不丢)与诊断布局(overfull@490、relto@492、seq u16@488)作为验证仪表:修好 = reject/torn/overfull 恒 0。 */
|
||||
/* 1.6.12:源头修复 —— 用户裁定"过滤是遮羞布,修采集源头,前端只许诚实反馈"。1.6.11 上线后实测(repro_cmdload 正弦负载)reject@494 仍以 ~384/s 增长:RFOD=DISABLE 挡不住溢出毁窗口(本 IP 满溢时 DISCARD 语义存疑,或毁帧发生在满态下的读并发路径),内容校验丢弃只是把坏数据拦在计数前。本版两处:(1) **源头**:CanFd::pumpAllChannels() 挂进 onEscIrqExti 的 PDI_Isr 入口 —— 在 buildInput 357µs 长阻塞(CAN ISR preempt=2 全程被压)开始之前把三路 FIFO 抽干,阻塞窗口起点深度=0、窗口内到达 ≤2-3 帧,3 深度 FIFO 从"必然打满"变成"必然不满";preempt=1 + SYNC0 已关的上下文保证与 CAN ISR 取帧互斥,与主循环批量队列生产零交集。(2) **诚实管道**:内容校验改为只计不丢(reject@494 恒 0 = 源头修好;还在涨 = 源头未愈且页面如实显示),诊断区加挂判决性测量:seq 488 拆 u16、overfull(RFO0)@490、release_timeout@492(原重入计数位,恒 0 让出)。reject 与 overfull 是否同涨 = "残余拼接是否仍源自溢出"的判决。 */
|
||||
|
||||
@@ -197,7 +197,7 @@ void OtaBootRequest::process()
|
||||
if(!ok) {
|
||||
s_spiFrozenForBoot = false;
|
||||
nvic_irq_enable(SPI0_IRQn, 2U, 0U);
|
||||
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U);
|
||||
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); /* 1.6.32: 与 configureExti 一致提到抢占级 0 */
|
||||
/* requestEnterBoot 失败时 s_rebootPending 仍为 false */
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "comm/ethercat_sync.h"
|
||||
#include "comm/ethercat_can_bridge.h"
|
||||
#include "comm/esc_exmc_bus.h"
|
||||
#include "comm/esc_lan9252.h"
|
||||
|
||||
extern "C" {
|
||||
void MainLoop(void);
|
||||
@@ -284,6 +285,8 @@ bool TaskRunner::copyAdcSample(Adc::Sample *out)
|
||||
extern "C" uint32_t EcatDbg_ReadAlDiag(void);
|
||||
/* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c, 1.5.48):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */
|
||||
extern "C" uint32_t EcatDbg_GuardAleventMask(void);
|
||||
/* 1.6.33:OP 输入新鲜度不变量 — 输入 PDO 停更则主循环强制重写(根治假连接) */
|
||||
extern "C" void EcatDbg_OpInputFreshness(void);
|
||||
|
||||
/**
|
||||
* @brief 1.5.48/1.5.49 AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 1.5.52 邮箱
|
||||
@@ -303,7 +306,9 @@ extern "C" uint32_t EcatDbg_GuardAleventMask(void);
|
||||
static void ethercatAleMaskGuard()
|
||||
{
|
||||
static uint32_t s_lastGuardMs = 0U;
|
||||
if (!EthercatSync::isActive()) {
|
||||
if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
|
||||
/* 1.6.27:睡眠挂起期一并静默——守护内部读 SM 起止/长度与 0x204/0x220
|
||||
* 全是 HBI 访问,会把睡着的芯片叫醒 */
|
||||
return;
|
||||
}
|
||||
const uint32_t nowMs = g_scheduler.tick_1ms;
|
||||
@@ -321,7 +326,8 @@ static void ethercatAleMaskGuard()
|
||||
*/
|
||||
static void ethercatAlHeartbeat()
|
||||
{
|
||||
if (!EthercatSync::isActive()) {
|
||||
if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
|
||||
/* 1.6.27:睡眠挂起期静默——读 0x0130/0x0220 即 HBI 访问,读即唤醒 */
|
||||
return;
|
||||
}
|
||||
const uint32_t alDiag = EcatDbg_ReadAlDiag();
|
||||
@@ -594,10 +600,19 @@ void TaskRunner::runPending()
|
||||
|
||||
/* SSC 协议栈主循环:AL 状态机 + 邮箱处理 + FreeRun PDO。
|
||||
* 每轮无条件调用,SSC 内部自行判断是否有待处理事件。
|
||||
* DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。 */
|
||||
MainLoop();
|
||||
* DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。
|
||||
* 1.6.27:ESC 睡眠(D1-D3)挂起期整段停调——MainLoop 每圈必读 0x220(AL 事件
|
||||
* 寄存器,经 HBI),而睡眠中任何 HBI 访问(含读)都会把芯片打回 D0,
|
||||
* 2026-09-12 实测休眠后 ~20s 即被"复活"。 */
|
||||
if (!EscLan9252::pmSuspended()) {
|
||||
MainLoop();
|
||||
/* 1.6.33:OP 输入新鲜度不变量。与 MainLoop 同拍调用——正常时
|
||||
* PDI_Isr 已在刷新,这里只做廉价 seq 比对;停更超 5ms 才强制重写。
|
||||
* 睡眠期与 MainLoop 一并跳过(读 HBI 会唤醒芯片)。 */
|
||||
EcatDbg_OpInputFreshness();
|
||||
}
|
||||
|
||||
/* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 1s 节流。
|
||||
/* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 250ms 节流。
|
||||
* 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率,
|
||||
* OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */
|
||||
ethercatAleMaskGuard();
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
feature: tiered-pdo-mapping
|
||||
status: designed
|
||||
updated: 2026-09-12
|
||||
branch: master (DCU_APP) + main (tools/ecat) — 无 worktree,沿用工作树
|
||||
commits: <pending delivery>
|
||||
---
|
||||
|
||||
# Tiered PDO Mapping(按电机数分档收缩 SM3)
|
||||
|
||||
## Report
|
||||
|
||||
(交付后填写)
|
||||
|
||||
---
|
||||
|
||||
## [S1] Problem
|
||||
|
||||
输入 PDO SM3 恒为 **600B**,与本拍实际密排电机数无关:
|
||||
|
||||
- 3 电机时仍占用 600B SM 与 ~984B 过程数据帧,100Mbps 上单帧线路时钟地板约 **81µs**。
|
||||
- 从站稀疏写(1.6.22)只省 PRAM 搬运,**不减线长**。
|
||||
- 用户场景:电机数长期固定(常见 3 台),偶发插拔;希望档内无感、跨档自动重映射,且重映射 **<500ms**、不 NRST、不撞 1s 过程数据看门狗。
|
||||
|
||||
现链路硬约束:
|
||||
|
||||
1. SSC `PREOP→SAFEOP` 调 `APPL_GenerateMapping` 得 `nPdInputSize`,`CheckSmSettings` 要求 **ESC SM3.Length == nPdInputSize**(`ecatslv.c:509`)。
|
||||
2. SOEM `Ibytes` 来自 CoE PDO 映射位数,**不是**从站运行时 tier;主站不改写 SM/FMMU/`Ibytes` 则线帧不变。
|
||||
3. 完整 `do_op_start`(重枚举 + 250×4ms 喂帧 + 最长数秒等 OP)无法满足 <500ms。
|
||||
4. `decode_input` 密排魔数 `@35=0xA5` 已可区分布局,但 **段3@544 / 段5@584 写死**,短 SM 下会读越界/错位。
|
||||
|
||||
---
|
||||
|
||||
## [S2] Design
|
||||
|
||||
### S2.1 容量档与线格式
|
||||
|
||||
唯一真源(从站头 + 主站常量,数值必须一致):
|
||||
|
||||
```
|
||||
ECAT_PDO_IN_MAX = 600
|
||||
ECAT_PDO_IN_PREFIX = 40 // 段1 32B + 段2 头 8B,槽区起点 MOTW_OFF_SLOTS=40
|
||||
ECAT_PDO_IN_XRX = 40 // 段3 长快照
|
||||
ECAT_PDO_IN_DIAG = 16 // 段5 诊断(实际用 12B,对齐到 8)
|
||||
ECAT_MOT_SLOT_SIZE = 56
|
||||
|
||||
in_bytes(cap) = 40 + cap*56 + 40 + 16 = 96 + 56*cap
|
||||
```
|
||||
|
||||
| 档 | count 范围 | cap | in_bytes | 线过程数据 out+in | 约线路时钟* |
|
||||
|----|------------|-----|----------|-------------------|-------------|
|
||||
| A | 0–3 | 3 | **264** | 384+264=648 | ~52µs |
|
||||
| B | 4–6 | 6 | **432** | 384+432=816 | ~65µs |
|
||||
| C | 7–9 | 9 | **600** | 384+600=984 | ~81µs(现状)|
|
||||
|
||||
\*仅 eth 过程数据字节的 100Mbps 线时钟粗算,不含头/IFG 的完整帧模型见 `ECAT_BEAT_TAIL_REPORT`。
|
||||
|
||||
**布局语义(密排,魔数 `@35=0xA5` 不变):**
|
||||
|
||||
```
|
||||
[0..31] 段1 桥状态(与现网一致)
|
||||
[32..39] 段2 头:count/active/boot/magic/gseq
|
||||
[40 .. 40+n*56) 密排槽,n = count ≤ cap
|
||||
[40+cap*56 .. +40) 段3 xrx
|
||||
[+40 .. +16) 段5 诊断(load/torn/seq/overfull/reltimeout/reject)
|
||||
```
|
||||
|
||||
- **档内**(count 在同一 cap 槽位预算内变化):只更新 `count` 与槽内容,**不改 SM、不 PREOP**。
|
||||
- **跨档**:走 S2.3 轻量重映射;两端同时切到目标 `in_bytes`。
|
||||
- 非密排(magic≠0xA5 或 ilen≥640 旧布局)路径保持原样,本特性不碰。
|
||||
|
||||
主站由 `Ibytes` 反推 cap(与 tier 一一对应):
|
||||
|
||||
```
|
||||
cap = (Ibytes - 96) / 56 // 264→3, 432→6, 600→9
|
||||
xrx_off = 40 + cap*56
|
||||
diag_off = xrx_off + 40
|
||||
```
|
||||
|
||||
禁止再写死 544/584。
|
||||
|
||||
### S2.2 从站契约(DCU_APP,目标版本 **1.6.34**)
|
||||
|
||||
1. **SII/EEPROM 不改**:`gen_eeprom.py` 仍 `PD_IN_SIZE=600`,冷启动 SM3 默认 600,与旧主站兼容。
|
||||
2. **`APPL_GenerateMapping`**(`dc_app.c`,`PREOP→SAFEOP` 时调用):
|
||||
- 读 ESC 当前 SM3 `Length`(`GetSyncMan(PROCESS_DATA_IN)`)。
|
||||
- 若 `Length` ∈ {264, 432, 600} → `*pInputSize = Length`(从站跟随主站)。
|
||||
- 否则 → `*pInputSize = ECAT_PDO_IN_MAX`(600,保底)。
|
||||
- `*pOutputSize` 仍 384,不变。
|
||||
3. **`APPL_InputMapping`**:`memcpy(pInputData, …, nPdInputSize)`,不再写死 600。
|
||||
4. **密排填充 / 稀疏写 / 差分写**(`ethercat_can_bridge.cpp` + diff/sparse 开关路径):
|
||||
- 段3/段5 物理偏移改为 `40 + cap*56` 起,其中 `cap = (nPdInputSize - 96) / 56`(在 Safe/OP 内 `nPdInputSize` 已被 GenerateMapping 更新)。
|
||||
- 稀疏写范围同步:段A=`0 .. 40+count*56`,段B=`xrx_off .. nPdInputSize`。
|
||||
- `count > cap` 时本拍只打包前 `cap` 个(并保持现有“撑满回退整包”仅在 cap=9 时语义不变);正常调度不应出现,出现时记诊断。
|
||||
5. **对象字典 0x6000**:仍按 600B 定义(SDO 可读全镜像);过程数据长度以 SM/`nPdInputSize` 为准。不引入多 TxPDO 映射子索引(v1 范围外)。
|
||||
6. **版本**:`config.h` patch → **34**,注释写明 tiered SM3 跟随主站。
|
||||
|
||||
**从站不主动改 SM 长度**,避免与主站写顺序竞态。
|
||||
|
||||
### S2.3 主站契约(tools/ecat / `ecat_native.c`)
|
||||
|
||||
#### 常量与状态
|
||||
|
||||
```
|
||||
tier_in_bytes[] = {264, 432, 600} // cap 3/6/9
|
||||
g_tier_cap // 当前会话生效 cap,默认 9
|
||||
g_tier_seen_count / g_tier_since // 滞回:观察到的新档 count 与起始时刻
|
||||
g_tier_last_remap / g_tier_remaps // 诊断
|
||||
```
|
||||
|
||||
`count` 取密排头 `g_head.wcount`(或 `inp[32]`),**不用** `active`(离线电机仍占槽报最后一轮,属于“槽预算”)。
|
||||
|
||||
#### 首次进 OP
|
||||
|
||||
- `do_op_start` 仍走完整 `config_init` + `config_map`(CoE 得 Ibytes=600)→ **首拍恒 Tier C**。
|
||||
- 首拍 decode 得到 `count` 后进入滞回观察;不与 SAFEOP→OP 竞态。
|
||||
|
||||
#### 跨档轻量重映射 `do_tier_remap(target_cap)`
|
||||
|
||||
前置:`g_bus_open && g_op && 循环已 loop_stop(调用方持 g_bus_lock)`。
|
||||
|
||||
```
|
||||
1. 记录 DC 形态 (dc_cycle_us, dc_shift_us) —— 与 wedge 恢复同源
|
||||
2. OP → PREOP|ACK(不 INIT、不 config_init 重枚举)
|
||||
- statecheck PREOP ≤400ms;失败则回退完整 do_op_start 一次
|
||||
3. 写 0x0420 = 10000(1s WD,与 do_op_start 同款)
|
||||
4. 改写 SM3:
|
||||
- ec_slave[1].SM[3].SMlength = htoes(target_in_bytes) // StartAddr 仍 0x1880,flags 不动(3-buffer)
|
||||
- ecx_FPWR / ec_FPWRw 写 ESC SM3 寄存器(与 config_map 写 SM 同基址)
|
||||
5. 改写输入 FMMU 逻辑长度(仅输入 FMMU,LogStart 不变,LogLength = target_in_bytes)
|
||||
6. 同步 SOEM 进程数据尺寸:
|
||||
- ec_slave[1].Ibytes / Ibits = target_in_bytes / *8
|
||||
- ec_group[0].Ibytes 调整,保证 send/receive_processdata 帧长 = O+new_I
|
||||
- g_expected_wkc 重算(inputsWKC 对短映射通常仍 1,需实读确认)
|
||||
7. PREOP → SAFEOP|ACK(≤400ms)—— 此时从站 GenerateMapping 读回新 SM3 长度
|
||||
8. 短喂帧:1ms 节拍 pump ~150 次(≈150ms,≪1s WD),再请求 OP
|
||||
9. OP|ACK,继续 pump 直到 AL=OP,总预算 ≤400ms;失败一次则完整 do_op_start
|
||||
10. g_tier_cap = target_cap;g_rxb_layout 保持 dense(3);清空槽表按 do_op_start 会话语义
|
||||
(可选:保留 g_slots,避免 remap 窗口 UI 闪空——v1 与 do_op_start 一致选择清空+重建)
|
||||
```
|
||||
|
||||
**目标耗时:** PREOP/SAFEOP/OP 状态检查各一次成功路径通常 10–50ms,加 150ms 喂帧,合计 **远低于 500ms**;全失败回退完整路径仍可恢复(不保证 <500ms,记日志)。
|
||||
|
||||
**禁止:** NRST、`ec_close`、改邮箱 SM、改输出 SM2 长度。
|
||||
|
||||
#### 滞回与触发点
|
||||
|
||||
- 仅在 **循环运行且 OP** 时评估;放在 `wedge_worker` 100ms 节拍(或 pdo_worker 每 N 拍采样后置标志,由 wedge 线程统一处理——**不得在 pdo_worker 内 loop_stop 自己**)。
|
||||
- 规则:
|
||||
- `need_cap = cap_for_count(count)`
|
||||
- 若 `need_cap == g_tier_cap` → 清观察状态
|
||||
- 若 `need_cap != g_tier_cap`:
|
||||
- 首次记下 `g_tier_seen_count` 与 `g_tier_since`
|
||||
- **同一 need_cap 连续 ≥500ms** 才执行 remap
|
||||
- 期间 count 又回到原档 → 清观察
|
||||
- `pm/hold` 置位期间不 remap(与楔死看门狗一致)。
|
||||
- remap 进行中 `g_tier_busy`:wedge 楔死恢复、`/api/motor/start`、SDO 独占路由互斥(复用 `g_bus_lock`)。
|
||||
|
||||
#### `decode_input` 密排分支
|
||||
|
||||
- 魔数判定不变。
|
||||
- `cap = (ilen - 96) / 56`,非法(不能整除或 cap∉{3,6,9})时:按 cap=9 且 `off+size>ilen` 则 break 的现有保护,**不崩溃**;记 layout 异常。
|
||||
- 段3/段5 偏移用 `xrx_off/diag_off`,替换 `XRX_DENSE_OFF_*` / `DIAG_OFF_*_WIN` 常量中的 544/584(常量改为宏函数或运行时变量)。
|
||||
|
||||
#### API / 观测
|
||||
|
||||
- `/api/motor/state` 或 busKv 增加:`tier_cap`、`tier_in_bytes`、`tier_remaps`、`tier_since`(可选)。
|
||||
- `/api/pdo/tier`:`{"cap":3|6|9}` 手动钉档(立即 remap,不走 500ms 滞回);`{"auto":true}` 恢复自动。v1 至少实现手动钉档读写。
|
||||
- `/api/open` 启动日志:`输入 %dB` 打印真实 `Ibytes`(remap 后),去掉写死「600B」文案。
|
||||
|
||||
#### 楔死恢复 / start / sdo
|
||||
|
||||
- `wedge_worker`、`/api/motor/state` 快路径、`/api/motor/start`:重拉 `do_op_start` 后 **重置 `g_tier_cap=9`**,再允许滞回把档缩回去(简单、可预期)。
|
||||
- `/api/sdo` 仍 `loop_stop` 不恢复——tier 状态在下次 start 时回到 9,可接受。
|
||||
|
||||
### S2.4 前端 / 文档(次要)
|
||||
|
||||
- `lab/spi_test.html`:bus 总线卡展示 `tier in_bytes`(若 API 提供);layout=dense 分支的段5 注释改为“偏移随 cap”。
|
||||
- `lab/pdo_layout.html`:统计卡从「600 B SM3」改为「264/432/600 分档」;**不阻塞固件交付**,可同 PR 或 follow-up。
|
||||
- 注释清理:避免再写“线速两布局同为 600B”这类在 tier 生效后错误的句子。
|
||||
|
||||
### S2.5 错误与回退
|
||||
|
||||
| 情况 | 行为 |
|
||||
|------|------|
|
||||
| SM3 改写 FPWR 失败 | abort remap,保持旧 Ibytes,日志 WARN;下一滞回窗重试 |
|
||||
| SAFEOP 失败(SSC 拒 SM) | 读 ALstatuscode;完整 `do_op_start`(回到 600)并记 `tier_remap_fail` |
|
||||
| OP 失败 | 同上 |
|
||||
| count 突然 > cap(从站 bug/撕裂) | decode 已有越界 break;从站侧 count>cap 截断;**不**立即 remap,等滞回 |
|
||||
| 首拍 Ibytes 仍 600 且 count=3 | 正常进入滞回,500ms 后缩到 264 |
|
||||
| 主站新、从站旧(GenerateMapping 恒 600) | 主站写 SM=264 后 SSC CheckSm 失败 → 回退 600,功能与今日等价 |
|
||||
| 从站新、主站旧 | SM 仍被旧主站写 600,从站跟随 600,与今日等价 |
|
||||
|
||||
### S2.6 不做(Out of Scope 见 S3)但需预留
|
||||
|
||||
- 输出 SM2 分档
|
||||
- 多 TxPDO 赋值(0x1C13 子索引选映射)
|
||||
- EEPROM/ESI 为每档发一份 SII
|
||||
- 改 1-buffer 换更大窗口
|
||||
|
||||
### S2.7 文件级改动清单(Implement 对照表)
|
||||
|
||||
> 行号以 2026-09-12 工作树为准,实现时以符号为准、行号仅作导航。
|
||||
|
||||
#### A. 从站 DCU_APP
|
||||
|
||||
| # | 文件 | 改什么 |
|
||||
|---|------|--------|
|
||||
| A1 | `GD32E508_DCU_APP/comm/ethercat_pdo_size.h` | 新增分档纯宏(C/C++ 共用):`ECAT_PDO_IN_TIERS {264,432,600}`、`ECAT_PDO_TIER_CAPS {3,6,9}`、`ECAT_PDO_IN_PREFIX=40`、`ECAT_PDO_IN_XRX=40`、`ECAT_PDO_IN_DIAG=16`、`ECAT_PDO_IN_BYTES_FOR_CAP(cap)=(96+56*(cap))`、`ECAT_PDO_CAP_FOR_IN_BYTES(ib)`。静态断言:三档均 ≤600 且 `%8==0`,cap=9 时等于现行 `ECAT_PDO_IN_BYTES`。`ECAT_PDO_IN_BYTES` 保留为 **max=600**(数组/EEPROM 上限),语义注释改为「上限,运行期可小于」。 |
|
||||
| A2 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.h` | 段3/段5 **枚举保持 cap=9 数值**(544/584…)作默认/静态断言基准;新增运行时偏移接口:`uint16_t ecatMotXrxOff(uint8_t cap)` / `ecatMotDiagOff(uint8_t cap)` / `ecatPdoInBytesForCap(uint8_t cap)`(inline 或声明+cpp)。`static_assert` 改为只约束 cap=9 与 max 的首尾相接。注释标明:短档时段3/段5 前移,主站必须按 Ibytes 解。 |
|
||||
| A3 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp` `buildInput` @286–333 | 段3 填充:`ECAT_XRX_OFF_*` → `xrx = ecatMotXrxOff(cap)` 相对写。`memset(in,0,kEcatPdoInBytes)` 可改为只清 `in_bytes`(或仍清 600,多清无害但稀疏/diff 会多比——**建议只清 in_bytes**)。`cap` 来源见 A5。段1/段2 偏移不变(0–39 + slots)。 |
|
||||
| A4 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp` `fillMotorRound` @119+ | 逻辑不变;若 `count > cap` 截断到 cap(防越界写到段3)。影子区仍按 max 分配。 |
|
||||
| A5 | `GD32E508_DCU_APP/comm/ssc/port/dc_app.c` `APPL_GenerateMapping` @355–360 | 读 `GetSyncMan(PROCESS_DATA_IN)`(channel=3)`Length`;∈{264,432,600} 则 `*pInputSize=Length`,否则 600。随后调用新增 `EcatPdo_OnPdoInBytes(*pInputSize)`(或写 `EthercatPdo` 静态),通知填充层当前 in_bytes/cap。输出仍 384。 |
|
||||
| A6 | `GD32E508_DCU_APP/comm/ssc/port/dc_app.c` `APPL_InputMapping` @408–453 | `EcatPdo_WriteInputs` 之后:`memcpy(pInputData, …, nPdInputSize)`(替换写死 `ECAT_PDO_IN_BYTES`)。 |
|
||||
| A7 | `GD32E508_DCU_APP/comm/ethercat_pdo.h` / `.cpp` | 新增静态 `s_inBytes`/`s_cap`(默认 600/9)+ `OnPdoInBytes(uint16)`。`fillDiagTail` @114–155:`ECAT_DIAG_OFF_*` → `diag=ecatMotDiagOff(cap)` 相对。`sparseEscWritePlan` @166–194:`ECAT_XRX_OFF_CH`/`kPdoInBytes` → 运行时 `xrx`/`in_bytes`;「撑满」条件改为 `headEnd >= xrx`。`escSeg2WriteLen` @199–216、diff 三区 @258–279:同改。影子数组 `s_escSh2/s_escSh3` **保持按 600 上限分配**,只比较/写 `in_bytes` 内长度。 |
|
||||
| A8 | `GD32E508_DCU_APP/sys/config.h` @152 | `APP_FW_VERSION_PATCH` 33→**34**,追加 1.6.34 注释:SM3 分档跟随主站(GenerateMapping 读回 SM3.Length)。 |
|
||||
| A9 | `GD32E508_DCU_APP/comm/ssc/port/gen_eeprom.py` | **不改** PD_IN_SIZE=600(v1 不刷 EEPROM)。可在文件头加一行注释:「运行期主站可把 SM3 改为 264/432,SII 仍 600」。 |
|
||||
| A10 | `ethercat_diff_stats.cpp` | 若 `kInBytes` 宏绑死 600:改为读 `EthercatPdo::inBytes()` 或仍按 max 快照——**差分仪表在短档下勿拿 600 去比 in_bytes 内容**;实现时优先 `observe` 用当前 in_bytes。 |
|
||||
|
||||
#### B. 主站 tools/ecat
|
||||
|
||||
| # | 文件 | 改什么 |
|
||||
|---|------|--------|
|
||||
| B1 | `ecat_native.c` 顶部常量 @70–133 | 新增:`TIER_IN_BYTES[]`、`tier_cap_for_count(n)`、`tier_in_bytes(cap)`、`dense_xrx_off(ilen)`、`dense_diag_off(ilen)`(`cap=(ilen-96)/56`)。**保留** `XRX_DENSE_OFF_*`/`DIAG_OFF_*_WIN` 仅作文档/默认 cap=9;decode 路径改走函数。 |
|
||||
| B2 | `ecat_native.c` 状态 @190–226 附近 | 新增:`g_tier_cap`(默认9)、`g_tier_auto`(默认1)、`g_tier_busy`、`g_tier_need_cap`、`g_tier_since`、`g_tier_remaps`、`g_tier_remap_fail`。 |
|
||||
| B3 | `ecat_native.c` `decode_input` @450–530 | 密排分支:`xch/xda/diag` 用 `dense_*_off(ilen)`。若 `(ilen-96)%56!=0` 或 cap∉{3,6,9}:仍按魔数密排处理但 `off+size>ilen` 则 break(已有),并置 layout 异常标志。段5 读取位置同步改(@559–560 现用 `DIAG_OFF_LOAD_WIN`)。 |
|
||||
| B4 | `ecat_native.c` 新函数 `do_tier_remap(int target_cap, …)` | 放在 `do_op_start`/`do_op_stop` 附近。实现 S2.3 十步;内部用 `ecx_FPWR`/`ec_FPWRw`:SM3 长度字 @ ESC `0x081A`(`ECT_REG_SM0+3*8+2`),整段 SM 重写可抄 `ethercatconfig.c` `ecx_map_sm` 的 FPWR 模式;输入 FMMU:读当前 FMMU 表,只改输入那条的 `LogLength=target_in`,FPWR 回去;`ec_slave[1].Ibytes=Ibits/8`、`ec_group[0].Ibytes` 收缩/回扩;`g_expected_wkc` 重算。成功:`g_tier_cap=target`;失败:`do_op_start` 回落并 `g_tier_cap=9`。 |
|
||||
| B5 | `ecat_native.c` `wedge_worker` @3057–3100 | 在 `need` 楔死分支**之前**增加 tier 滞回评估(仅当 `g_loop_running && g_op && !g_tier_busy && !pm_hold && g_tier_auto`):用最新 `g_head.wcount`;同一 `need_cap≠g_tier_cap` 持续 ≥0.5s → `loop_stop` + `do_tier_remap` + `loop_start`。楔死恢复成功路径:`g_tier_cap=9`、清观察。 |
|
||||
| B6 | `ecat_native.c` `do_op_start` 成功出口 @1178–1207 | `g_tier_cap=9`、清滞回观察。日志 `输入 %dB` 已打真实 Ibytes(@1180)——确认 remap 后再 start 也正确。 |
|
||||
| B7 | `ecat_native.c` `/api/motor/start` @1987–2046 | 成功 `loop_start` 后同样 `g_tier_cap=9`(若走的是未 remap 的 do_op_start)。日志去掉写死「输入 600B」文案 @2039。 |
|
||||
| B8 | `ecat_native.c` `/api/motor/state` @2365–2403 | 快路径楔死恢复成功后 `g_tier_cap=9`。`build_state_json` 增加 `tier_cap`/`tier_in_bytes`/`tier_remaps`(函数体搜 `build_state_json`)。 |
|
||||
| B9 | `ecat_native.c` 新路由 `/api/pdo/tier` | GET:返回 `{ok, auto, tier_cap, tier_in_bytes, remaps, last_ms}`。POST body `{"cap":3\|6\|9}` 或 `{"auto":false\|true}`:钉档时持 `g_bus_lock`、`loop_stop`、`do_tier_remap`、`loop_start`;`auto` 只改标志。 |
|
||||
| B10 | `ecat_native.c` `pdo_worker` @848–852 | 无需改 Ibytes 来源(已读 `ec_slave[1].Ibytes`)。**禁止**在此 remap。可选:每 N 拍把 `g_head.wcount` 时间戳写给 wedge(直接读 `g_head` 已无锁跨线程——与现状一致,保持)。 |
|
||||
| B11 | `build_new.bat` / 工程 | 无新源文件则不用改;若 `do_tier_remap` 拆到新 `.c` 再加入 bat。 |
|
||||
|
||||
#### C. 前端 tools/ESP32(不阻塞固件)
|
||||
|
||||
| # | 文件 | 改什么 |
|
||||
|---|------|--------|
|
||||
| C1 | `lab/spi_test.html` | 总线/KV 区:若 API 返回 `tier_in_bytes` 则显示「SM3 264/432/600 · 档」。dense 段5 注释 @1198 改为「偏移随 cap」。CSS/JS 引用若外置需 `?v=`(本页自包含则内联即可)。 |
|
||||
| C2 | `lab/pdo_layout.html` | 统计卡 @243–244、图例 @819/@841、说明 @264:由「600 B SM3」改为分档说明;图可标三档长度。 |
|
||||
| C3 | `ECAT_BEAT_TAIL_REPORT.html` 等历史报告 | **不改**(已归档结论);避免改写历史数字。 |
|
||||
|
||||
#### D. 明确不改
|
||||
|
||||
- `ESP32_COMM/**`
|
||||
- `gen_eeprom.py` 的 PD_IN_SIZE、`DCU_ESI.xml`
|
||||
- `soem-src/**`(只调用,不改库;FMMU/SM 用现成 FPWR)
|
||||
- 输出 SM2、邮箱 SM0/1
|
||||
|
||||
#### E. 实现顺序(文件依赖)
|
||||
|
||||
```
|
||||
A1 → A2 → A5/A6(GenerateMapping+InputMapping)
|
||||
→ A3/A4/A7(填充与稀疏/差分,依赖 A5 通知 in_bytes)
|
||||
→ A8 版本
|
||||
B1 → B3 decode → B4 remap → B5/B6/B7 滞回与重置 → B8/B9 API
|
||||
C1/C2 可后置
|
||||
```
|
||||
|
||||
双端可并行:A 与 B 仅共享数值表;联调以 V3–V8 为准。
|
||||
|
||||
---
|
||||
|
||||
## [S3] Out of Scope
|
||||
|
||||
- ESP32_COMM 固件(不解析 EtherCAT PDO)
|
||||
- 输出 PDO 384B 收缩
|
||||
- 刷 EEPROM / 冷启动依赖的 SII 多映射
|
||||
- 电机 10+ 节点
|
||||
- 用 tier 直接调 PDO 周期(0.25ms 等)
|
||||
- 提交/合并(用户明确不自动 commit)
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] T1: A1+A2+A5+A6 从站长度宏 + GenerateMapping 跟随 SM3 + InputMapping 按 nPdInputSize —— acceptance: 静态断言 264/432/600;主站手写 SM3=264 后 PREOP→SAFEOP 不报 0x001A/INVALIDSMINCFG;`nPdInputSize` 变 264 (covers: S2.2 S2.7-A)
|
||||
- [ ] T2: A3+A4+A7 从站密排/稀疏/差分偏移 cap 相对 —— acceptance: 264B SM 下段3/段5 不越界,主站能读 load/torn;count≤3 稀疏写两段正确 (covers: S2.2 S2.7-A; depends: T1)
|
||||
- [ ] T3: A8 版本 1.6.34 —— acceptance: PATCH=34 + 注释 (covers: S2.2; depends: T1)
|
||||
- [ ] T4: B1+B3 主站 decode cap 相对 —— acceptance: ilen=264+magic 槽/段3/段5 正确;ilen=600 回归一致 (covers: S2.1 S2.3 S2.7-B)
|
||||
- [ ] T5: B4 do_tier_remap + Ibytes/FMMU/group —— acceptance: 600→264→600 各一次 AL=OP、wkc 正、成功路径日志 <500ms (covers: S2.3; depends: T4)
|
||||
- [ ] T6: B5+B6+B7 滞回 + start/wedge 重置 —— acceptance: 稳定 3 电机 0.5–1.0s 进 264;4 电机进 432;楔死后回 9 再缩回 (covers: S2.3; depends: T5)
|
||||
- [ ] T7: B8+B9 `/api/pdo/tier` 与 state 字段 —— acceptance: POST cap 立即 remap;GET 含 tier_* (covers: S2.3; depends: T5)
|
||||
- [ ] T8: V1–V10 双端编译+台架 —— acceptance: cbuild/build_native 通过;见 Verification (covers: S2.2 S2.3; depends: T1–T7)
|
||||
- [ ] T9: C1+C2 前端分档展示 —— acceptance: 页面显示当前档;不阻塞 T8 (covers: S2.4; depends: T7)
|
||||
|
||||
---
|
||||
|
||||
## Verification(交付证据)
|
||||
|
||||
| 步骤 | 命令/操作 | 期望 |
|
||||
|------|-----------|------|
|
||||
| V1 | DCU cbuild(既有 Release+GD32E508VE) | 0 error |
|
||||
| V2 | `build_new.bat` / VS2022 cl 原生主站 | 产出 `ecat_native.new.exe` |
|
||||
| V3 | 冷启动 600 进 OP(3 电机) | 与今日一致,layout=dense |
|
||||
| V4 | 等待 ≥1s | 自动 remap 到 264,日志含耗时 |
|
||||
| V5 | `/api/motor/state` + spi_test | Ibytes/tier=264,count=3,wkc 稳定 |
|
||||
| V6 | rx 线时(粗) | 过程数据字节 648 vs 984,rx 段应下降(允许 ±15µs 噪声) |
|
||||
| V7 | 手动 `/api/pdo/tier` cap=9 | 回到 600 |
|
||||
| V8 | 拔/加第 4 台(或模拟 count) | 滞回后 432 |
|
||||
| V9 | 旧从站 + 新主站 | SAFEOP 失败路径回 600,总线仍可用 |
|
||||
| V10 | 楔死/PM hold | hold 中不 remap;恢复后 tier 策略符合 S2.3 |
|
||||
|
||||
---
|
||||
|
||||
## Journey log(设计期)
|
||||
|
||||
1. 线时收益来自 **SM3 变短**,稀疏写不够——与 1.6.22/1.6.25 结论一致。
|
||||
2. SSC 长度双源校验 ⇒ 必须主从同数;v1 选「主站写 SM,从站 GenerateMapping 读回」,避免 EEPROM 与多 PDO 映射。
|
||||
3. 首拍恒 600 是为了兼容 config_map/CoE,用 500ms 滞回消化一次 remap,比改 CoE 映射表风险低。
|
||||
4. 重映射不能走完整 `do_op_start`(重枚举+长喂帧)——独立轻量路径 + 失败回退完整路径。
|
||||
5. 段3/段5 偏移写死是主站解码的隐雷;Ibytes 分支(ilen<600→slim)会把 264 误判成瘦身布局——必须先用魔数 0xA5 拦下,再按 cap 排偏移。
|
||||
Reference in New Issue
Block a user