--- 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)