Files
DCU_APP/docs/compose/spec/ecat-wkc-periodic-dip.md
cbb f3d7fc0c30 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命令处理文件的分组
2026-10-10 02:25:56 +08:00

228 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)