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命令处理文件的分组
This commit is contained in:
cbb
2026-10-10 02:25:56 +08:00
parent a777be1cb4
commit f3d7fc0c30
13 changed files with 1247 additions and 105 deletions
+227
View File
@@ -0,0 +1,227 @@
---
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)