Files
DCU_DesktopController/ECAT_RATE_TRADEOFF.md
T

292 lines
21 KiB
Markdown
Raw 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.
# EtherCAT 周期速度 × 故障率 × CPU 占用 调研
> **⚠ 本 Markdown 版含一条已作废的结论,权威版本是 `ECAT_RATE_TRADEOFF.html`。**
> 作废内容:第一节「`drop` ≡ λ → 丢帧率 100%、与周期无关」。**错因**:`drop`、
> `bridge.rx_count`、`head.global_seq` **各自都以约 4000/s 增长**,两两相除必然得到 100%,
> 那只是同速量之比、不是丢帧。**反证**(后续实测):两台电机 STATUS 以 4001 帧/s 正常刷新,
> 帧确实收到了;而 ECAT 批量窗口去重后的 CONFIG 事件流只有约 92 帧/s,与 `drop` 的 4000/s
> 完全不成比例。`drop` 的口径至今未查明,**不得用它做任何丢帧判断**。
> 仍然成立:速度上限、从站 CPU 负担、拔 CAN 对照、>2kHz 时 STATUS/CONFIG 一同塌陷。
调研日期:2026-09-08 · 台子:本机 I210 + 1 个 GD32E508 从站(双电机在线)
数据与脚本:`scripts/sweep_ecat_rate.py`(扫描)、`scripts/calibrate_ecat_metrics.py`(现场标定)、
`scripts/plot_ecat_sweep.py`(出图)、`scripts/plot_ecat_nocan_vs_can.py`(带/无 CAN 对比);
曲线 `lab/ecat_rate_sweep.png`、`lab/ecat_rate_sweep_nocan_vs.png`;原始数据
`lab/ecat_rate_sweep_withcan.csv`、`lab/ecat_rate_sweep_nocan.csv`、`lab/ecat_calib.json`
(`*.csv`/`*.jsonl` 按 `.gitignore` 约定不入库)。
本轮共两轮扫描:**带 CAN**(第四~五节主结论)与**拔掉 CAN 的补测**(第四 b 节)。
---
## 一、结论
**速度余量约 2×,且瓶颈在从站 GD32,不在主站。** 实测可用到 **0.5ms(2kHz)**;
0.25ms 起电机数据刷新率从 4000 帧/s 掉到 88 帧/s(带 CAN 那轮 8s/档定点实测,当场确认
`online=True`,故为真失效)。上限的**具体位置没测到** —— 只夹在 (0.25ms, 0.5ms] 之间。
**「加快 ECAT 周期能减少 CAN 丢帧」这个前提被本次数据推翻。** 在 4ms→0.5ms 整个区间,
桥接层丢帧 `bridge.drop` 与 CAN 到帧率 λ **完全相等**(丢帧率恒 100.0%),与周期无关。
该字段属 GD32 段1 桥接层的接收计数,其消费者是 GD32 主循环而非 ECAT PDO 填充路径,
所以 ECAT 周期快慢改不了它(字段的精确含义尚有一处未闭合矛盾,见第五节)。
**提速的实际收益与代价**:
| | 4ms | 2ms | 1ms(现行) | **0.5ms** | 0.25ms | 0.125ms |
|---|---|---|---|---|---|---|
| 实测循环率 | 249Hz | 499Hz | 998Hz | **1999Hz** | 3989Hz | 7780Hz |
| 达成率 | 99.7% | 99.7% | 99.8% | **100.0%** | 99.7% | 97.2% |
| GD32 负载 | 20.2% | 28.3% | 44.5% | **76.7%** | 99.7% | 99.3% |
| 主站错拍 skip/s | 0.02 | 0.08 | 1.1 | **11.9** | 98.6 | 5106.6 |
| 电机数据刷新 /s ¹ | 4000† | 4000† | 4000.2 | **4000.2** | 87.9 | 147.5 |
| CAN 到帧 λ /s | 3987 | 3990 | 3992 | **3995** | 159 | 190 |
| 桥接层丢帧 drop /s | 3987 | 3990 | 3992 | **3995** | 159 | 190 |
| 丢帧率 | 100.0% | 100.0% | 100.0% | **99.99%** | 100.0% | 100.0% |
| 主机 native CPU | 100.3% | 100.3% | 100.3% | **100.3%** | 100.3% | 87.5% |
¹ 电机刷新率只在 **1 / 0.5 / 0.25 / 0.125ms** 四档定点实测(8s/档)。标 † 的 4ms、2ms 两列
是按"λ 不随周期变化"推的,**未实测**。
建议:**要吃到这份余量就停在 0.5ms**(功能与 1ms 完全同速、达成率满分、CPU 76.7% 仍有余量,
代价是错拍从 1.1/s 升到 11.9/s)。0.25ms 不是"更快",是**功能损坏**。
**补测(拔掉全部 CAN)后新增两点**:
- CAN 的每拍负担是**常量 ≈9 个百分点**(2/1/0.5ms 三档 Δ=9.3/9.1/8.7pp),不随频率张开
→ 带 CAN 那轮的快档塌陷不是 CAN 挤出来的,上限由纯 ECAT 的过程数据交换决定。
- 纯 ECAT 到 2kHz 只需 68.0%,模型外推上限约 **3.5kHz**(带 CAN 约 2.7kHz)。
但 **0.25ms 那档没测到数**:加压时从站停止应答(`wkc=-1`、`load_perm` 成残值),
所以"3.5kHz"目前是外推而非实测,**不要据此设定期望值**。详见第四 b 节。
---
## 二、工况与数据身份(先声明,因为对账不上)
| 项 | 值 | 能否确认 |
|---|---|---|
| GD32 烧写台账 | `lab/_last_gd32_sha.txt` = `201832b5a8e258d6…`(未提交状态) | 台账本身可读 |
| 在盘固件产物 | 展平后哈希 `63d6ab9b…`(09-08 02:31)、`e6a6e75f…`(09-05) | **均不等于台账** |
| 主站构建 | `ecat_native.c` 改于 02:39、`ecat_native.exe` 改于 02:40,均有未提交改动 | 运行中 PID 29284 起于 02:32 |
| 板载固件版本 | — | **无法确认**(设备不回报版本字) |
三点后果,均已在结论中按保守口径处理:
1. 台账哈希与任何在盘产物都不匹配,且产物时间戳(02:31)**晚于**台账记录 —— 无法证明板上跑的
就是当前源码编出的那一版。**本报告不复用任何历史固件结论**(含"1.6.5 已修 seqlock 撕裂")。
2. 测量期间主站构建又更新过(02:39/02:40),无法证明 02:32 启动的进程内存里就是盘上那份 exe。
3. `pdo_reentry` 字段要固件 1.6.7+ 才有、`rx_torn` 要 1.6.6+。两者本次恒零,**既可能是"没发生",
也可能是"该字段根本没实现、恒读 0"** —— 因版本未知,二者无法区分。
---
## 三、判据口径(含三个当场失效/需修正的测法)
先做 Phase 0 标定(`scripts/calibrate_ecat_metrics.py`)再扫描,因为固件改过很多版,
不能假设任何历史判据仍然有效。标定过程中我自己有三个判据当场失效或被我误判,记录如下以免重犯:
| 测法 | 结果 | 处置 |
|---|---|---|
| 拿「seq 值域 − 观测到的 distinct seq」当丢帧率 | 报出 **99.686%** 丢帧 | **方向与 `drop`≈λ 一致,不是假信号**;但分母是全局 seq 值域(含 STATUS 全流量),口径不干净 → 不单独引用,只作 `drop` 的旁证。曾据 `OVF=0` 判它"被证伪",该否定**收回**(见下节 OVF 说明) |
| 用 HTTP 轮询看 `rx_batch.count` 当"每拍几帧" | 读出 cap=2,与源码 3 帧/拍不符 | 轮询欠采样。每拍口径必须取 native 的按拍环 `/api/can/cadence`(实测 count 众数=2,与轮询一致,但与 1.6.15 的 5 槽上限不是一回事) |
| 用 `rxraw` 的环快照列 `rxb_count_mode_per_cycle` / `lost` / `pending` | 快档读出 5、4、2 | 该环只存最近 512 拍,0.25ms 档 = 128ms、0.125ms 档 = 64ms,环没装满 —— 读到的是**行数**不是帧数。**这几列不入结论** |
最终采用的口径:
- **速度达成**:`cycles`(u32)增量 / 墙钟,**cycles 与时刻同取同舍**(沿用 `tools/ecat/rate_probe.py` 的纪律,其注释记过一次因分子分母不同源把 99.2% 误报成 96% 的实踩)。
- **CAN 到帧 λ**:`head.global_seq`(u32,固件 `ECAT_MOTW_OFF_GLOBAL_SEQ`「状态表全局刷新序号」)。
与电机槽 `seq` 同速(实测均 4000/s)—— 两条独立字段互证,λ 口径可信。
- **桥接层丢帧**:`bridge.drop`,固件出处 `ECAT_CAN_RX_OFF_DROP = 24`,注释明确为
「**投递时上一帧还没被取走**」的丢帧数(u16)—— 即窗口被覆盖而未送达主站的帧,
语义正对本题。自增另有 `canfd.h:53 rx_drop_count`(CAN RX 软件队列满,`canfd.cpp:1615`
`queue.count >= kRxQueueDepth`,队列深 `kRxQueueDepth=16`);两者都指向"消费不及"。
u16 在 4000/s 下 16 秒绕一圈 → 10Hz 逐次累加 + `(b-a) mod 65536`,单次增量 >32768 记为
绕圈作废。**本次作废点数 = 0**,即采样率足够。
- **不能当"无丢帧"证据的字段**:`rx_batch.lost` / cadence 的 `lost`/`pending`。它们对应固件
`OVF`,而 `fillRxWindow` 里 `in[...] = s_rxWinOvf; s_rxWinOvf = 0;` —— **每拍清零取增量**,
实测恒 0 只说明单拍增量小于分辨率,不代表没有丢帧。曾据它判 `drop` 为死字段,此推断**收回**。
- **功能存活(真故障轴)**:电机槽 `seq`(u32)前进速率。这是唯一直接回答"用户还能不能用"的指标。
- **CPU(从站)**:`load_perm`(千分比,1s 滚动窗,`ECAT_DIAG_OFF_LOAD`)。**它不是整机 CPU 占用率**
—— 固件 `sys/cpu_load.h:9-27` 定义收窄为「中断 + 非延迟处理段」占墙钟之比,并**刻意排除**
SSC MainLoop 轮询与 `AppSpiCommandBus.poll()`;且当前是 free-run(`native/health.dc=0`),
注释写明"free-run 调试模式下 PDO 应用跑在主循环轮询路径,**不计入**"。
因此:① 不能把"load→100%"读成"CPU 耗尽";② 但它仍随循环率强增(20.2%→76.7%),
这份增量只能来自**被计入的部分**(PDI/EXTI 等挂钩 ISR 每拍触发一次),
所以作为"每拍中断负担"的代理量仍然成立 —— 第五节据此建模,并把结论限定在这个口径内。
- **CPU(主机)**:`psutil` 按 PID 差分,同时报单核% 与整机%(`scripts/cpu_sample.ps1` 只报前者,
28 核机器上会把 3.6% 误读成 100%)。
- **撕裂/重入**:`rx_torn` / `pdo_reentry` 为 u16 **饱和**计数(到 65535 停),按
`delta_saturate` 处理,封顶即报"下界"而非精确值。
---
## 四、曲线与解读
![ECAT 速度-故障率-CPU 实测曲线](lab/ecat_rate_sweep.png)
**① 主站循环率** 在 4ms→0.25ms 全程贴着 1:1(≥99.7%),0.125ms 才降到 97.2%。
主站单拍耗时 `cyc_us.total.p50` 各档恒约 120–134µs、与设定周期无关(自旋钉核所致),
所以**主站侧根本不是限制项**。附带修正一条历史记录:`rate_probe.py` 写着"真机一送一回
≈165µs 物理墙",本次实测 0.125ms 档跑到 7780Hz,那道墙在当前 5 槽布局下已经不是限制。
**② CPU 占用** 是唯一陡峭的分界线。GD32 负载 20.2 → 28.3 → 44.5 → **76.7 → 99.7**%,
在 0.5ms 与 0.25ms 之间越顶。主机 native 全程 100.3%(自旋线程恒占一个逻辑核,
`/api/motor/spin` 已是空操作,CPU 降不下来)—— 有意思的是 0.125ms 档反而**降到 87.5%**,
因为从站已经不回数据、总线事务提前完成:**主机 CPU 掉下来的那一刻,恰恰是从站死掉的那一刻**。
这条曲线说明"提速免费"的判断只在一半意义上成立:不花主机成本,但花从站成本,而从站才是预算。
**③ 故障率** 是本次的核心图。λ 与 `drop` 两条线**重合到肉眼分不开**,横跨 4ms→0.5ms 全程,
然后一起从 4000/s 断崖式塌到 ~150/s。读法:
- 重合 = 到多少丢多少,**丢帧率 100% 且与周期无关** → 提速不改善它;
- 一起塌 = 0.25ms 起连 CAN 收帧本身都停了 → 提速会**打断**它。
电机数据刷新率(红)与 λ 同步塌陷,确认这不是测量假象而是功能失效。
**④ 主站错拍 `skip_total`** 是次级上限:0.02 → 11.9(0.5ms)→ 98.6(0.25ms)→ 5107/s(0.125ms)。
它在 GD32 越顶之前就已经指数上升,若只看主站视角,0.5ms 的 11.9 次/s 是可接受代价,
但 0.25ms 的 98.6 次/s 已说明主站也在勉强。
---
## 四b、补测:拔掉全部 CAN 设备后的纯 EtherCAT 速度
**动机**:带 CAN 的那轮里,快档同时出现「λ 塌陷」与「电机掉线」两种征象,二者在 λ/seq 上
长得一模一样,无法区分是**每拍过程数据交换太重**还是**CAN 流量挤掉了主循环**。
拔 CAN 是解这个混淆的判别性实验。
**结果**(同脚本同参数,`lab/ecat_rate_sweep_nocan.csv`,对比图 `lab/ecat_rate_sweep_nocan_vs.png`):
| 周期 | 循环率 | 带 CAN | 无 CAN | Δ |
|---|---|---|---|---|
| 2ms | 500Hz | 28.3% | **19.0%** | 9.3pp |
| 1ms | 1000Hz | 44.5% | **35.4%** | 9.1pp |
| 0.5ms | 2000Hz | 76.7% | **68.0%** | 8.7pp |
| 0.25ms | 4000Hz | 99.7% | **不可引用** | — |
**三条读数**:
1. **CAN 的负担是常量、不是变量**:三档 Δ 稳定在 8.7–9.3pp。所以 CAN 既不是上限的成因,
也不随频率张开 —— 带 CAN 那轮的快档塌陷**不是 CAN 挤出来的**。
2. **纯 ECAT 到 2kHz 毫无问题**(68.0%,达成率 100.0%,错拍 17.1 次/s)。
3. **上限仍没测到**:加压到 0.25ms 时**从站停止用有效数据应答** ——
`wkc=-1`(cadence 环里 512/512 拍全为 65535)、`bridge.valid=false`、`head.active=0`、
`app/load` 的 `age_ms=214338`,而主站 `cycles` 仍以 1004Hz 前进、`op=1`、`al_status=0`。
即总线在空转、从站不应答。**0.25ms 档的 99.2% 是从站停报后的 load_perm 残值,
本报告不引用。**
**用无 CAN 数据重拟合第五节的两项模型**:F=(0.680−0.190)/(2.0−0.25)≈**272µs**、
R≈4.0%,硬上限周期 F/(1−R)≈**284µs(3.5kHz)**。
对比带 CAN 的 F≈323µs → 2.7kHz。**拔 CAN 大约把上限从 2.7kHz 抬到 3.5kHz,
但 0.25ms=4kHz 按此模型仍不可行** —— 与实测"0.25ms 出事"方向一致,只是这次
出事的表征是从站不应答而非负载封顶,机理未定。
**扫描工具的两个缺陷由这轮暴露并已修**:
- 原脚本会把从站停报后的 `load_perm` 残值当实测写入结论 → 已加 `age_ms<1000` 闸,
并新增 `gd32_load_samples` / `gd32_load_stale` 两列,使"这个数有没有被测到"可见。
- 原脚本未记电机在线标志 → 已加 `motors_online` / `online_frac` / `motor_refresh_per_s`。
**台子现状**:从站仍停在 `wkc=-1`,恢复到 1ms 也不能自愈(native 的楔死看门狗未触发,`wr=0`)。
继续测快档前需要先断电重启板子。
---
## 五、机理:为什么是 2 倍而不是更多
从站每拍要写满输入 PDO。1.6.15 布局下输入 600B,`fillRxWindow` 在
PDI_Isr/Sync0_Isr(preempt=0)里跑,**每拍都要跑一次**,与 CAN 是否有新帧无关。
先否定一个想当然的模型:把「负载 × 周期」当作每拍固定开销,反推得到
808 / 566 / 445 / 384µs(4ms→0.5ms),**单调下降**——说明它量的是每拍总工作量,
里面还含一项与周期成正比的后台摊薄,不是常数。改成两项模型:
```
load(T) = F / T + R F = 每拍固定开销,R = 与周期无关的本底
```
用两端点(4ms/20.2% 与 0.5ms/76.7%)辨识:
```
F = (0.767 − 0.202) / (1/0.5 − 1/4) = 0.565 / 1.750 ≈ 323µs
R = 0.202 − 323/4000 ≈ 11.7%
```
用**未参与拟合**的中间两点检验:
| T | 模型 F/T+R | 实测 | 残差 |
|---|---|---|---|
| 2ms | 17.8% | 28.3% | +10.5 |
| 1ms | 44.0% | 44.5% | +0.5 |
| 0.25ms | **140.9%** | 99.7%(封顶) | 算术上 >100% → 不可行 |
| 0.125ms | **269.9%** | 99.3%(封顶) | 同上 |
模型在 1ms 处几乎精确、2ms 处偏低(低速档还有别的开销项),但关键预测成立:
**存在硬上限周期 F/(1−R) ≈ 367µs ≈ 2.7kHz**,实测分界正好落在它两侧
(0.5ms=2kHz 可跑、0.25ms=4kHz 不可跑)。这条是可证伪的:若下一次扫描在 0.4ms
也能满速运行且负载 <100%,本模型即被推翻。
用第四 b 节的**无 CAN** 数据独立重拟合,得 F≈272µs、R≈4.0%、上限 ≈284µs(3.5kHz)——
同一模型形式、两组独立数据、F 相差 16%(正是 CAN 那份负担),说明 F/T+R 这个结构站得住。
但**两组都没能直接测到上限本身**:带 CAN 时在 0.25–0.5ms 之间靠"功能塌陷"间接定位,
无 CAN 时 0.25ms 直接从站停报。所以上限目前是被**夹在** (0.25ms, 0.5ms] 区间内,
不是被测出来的。
结论:从站侧的极限周期由**每拍固定开销 F**决定,要突破只能减小每拍要搬的字节数
(当前 600B / 5 槽 / 56B 电机槽),**不是调主站周期能解决的**。主机侧完全有余量
(native 恒约 100% 单核,与周期无关),所以预算全在从站。
丢帧是另一条独立链路,与 ECAT 周期无关:`drop` 字段位于**段1(CAN RX 桥,偏移 24)**,
属 GD32 桥接层自己的接收/投递计数,其消费者是主循环(`sys/task_runner.cpp:248`)而非
ECAT PDO 填充路径 —— 所以 ECAT 周期快慢改不了它,实测四个档位恒等于 λ 也印证了这点。
但这里有一处**尚未闭合的矛盾**,不能糊过去:固件 `config.h:125` 的 1.6.15 设计说明写着
「STATUS 自 1.6.9 起不入窗,滤掉后入窗流量降到 CONFIG 量级」「5 槽=5000 帧/s 对 4000 帧/s
留 25% 余量」—— 按此设计意图,丢帧应当接近 0。实测却是 `drop` 以 3992/s 持续增长、
且恰好等于 λ(含 STATUS 的全流量)。两种解释都还活着:
(a) 该计数统计的是入窗**之前**的 CAN RX 队列/桥接层丢弃,STATUS 在入队环节就被丢,与窗口
容量无关(则 `drop`≈λ 完全合理,设计意图也没被违背);
(b) 字段在当前固件里已改义或读错偏移,`drop`≈λ 是巧合。
本次数据不足以区分二者,**列为待验证第 1 项**。无论哪种,都不影响主结论:
`drop` 与 ECAT 周期无关 → 提速不改善它。
---
## 六、局限
1. 单台单次,未做重复性与温漂验证。
2. λ≈3993/s 是本次台子的 CAN 工况。λ 变则分界变,本报告给的是**相对关系**,
0.5ms 这个具体数字不可直接搬到别的 CAN 流量下。
3. 板载固件版本无法确认(见第二节),历史固件结论一律未采信;`rx_torn` / `pdo_reentry`
恒零属**无检出功效**,只能表述为"本次测不到",**不得**读成"无撕裂/已修好"。
4. `cyc_us.late` / `cyc_us.lost` 恒为 0(`ecat_native.c` 里是 Python 时代遗留占位),未接线,未使用。
5. 扫描用 HTTP 以 10Hz 打 native,对被测对象有扰动;但主机 CPU 已恒 100% 单核,
且 native 另有 HTTP 线程,扰动量级判断为小 —— **未单独做无负载对照实验**。
6. `rxb_count_mode_per_cycle` / `rxb_lost_max` / `rxb_pending_max` 三列因环缓冲快照问题不可引用
(见第三节),CSV 中保留原值但结论未使用。
6b. **快档塌陷这条结论的证据来源要分清**:电机刷新率与 λ 的塌陷,取自扫描后另做的 8s/档
定点实测,那四次都当场确认 `online=True` 且 seq 在动,所以是**真功能失效**,不是电机
掉线造成的假象。但扫描 CSV 本身**没记在线标志**,事后无法复核。补该列前不要引用
CSV 的快档数字。
(2026-09-08 复测时电机已处于 `online=False`、`global_seq` 冻结状态 —— 总线本身正常,
cycles 仍以 1000Hz 前进、CAN 侧仍以 ~3700 帧/s 收帧,停的是 STATUS 上报。
该状态下的 0.4/0.35ms 快档读数一律不采信,也**未**写入本报告。)
7. 每拍固定开销 F≈323µs 是从 4 个负载点**反推**的,不是直接计时测得;两项模型在
2ms 档残差 +10.5 个百分点,说明低速档还有未建模的开销项。F 只能当量级用。
## 七、待验证(下一步该做的判据)
1. **`drop` 语义二选一**(第五节的 (a)/(b)):从 ESP32/WiFi 侧读固件那份 u32
`rx_drop_count`(`app_spi_cmd_can_feedback.cpp:99`,CAN 通道队列满丢弃的权威口径),
与 ECAT 侧 u16 `bridge.drop` 对速率。若两侧同为 ~4000/s → (a) 成立,丢帧发生在入窗之前,
与 ECAT 无关;若 WiFi 侧接近 0 而 ECAT 侧 4000/s → (b) 成立,字段读错偏移,
本报告的故障率轴需要整体重做。**这是唯一还悬着的承重结论。**
2. **从站侧健康度目前不可读**:`OVF` 在 `fillRxWindow` 里每拍清零,只能取到单拍增量,
而单拍增量落在 u8 分辨率以下即恒 0 —— 等于从站没有可用的累计丢帧仪表。
要判窗口是否真的健康,需要固件改成累计值或主站侧累加(属改动,需另行批准)。
3. **每拍固定开销 F 直接计时**:给 `fillRxWindow` 加计时(属改动,需另行批准)。
目前 F≈323µs 是从 4 个负载点反推的;直接量到 F 就能把「上限 2.7kHz」从拟合
变成算式,也才能判断减字节能否真的换到速度。
4. **模型已被证伪(保守方向)**:F/T+R 预测 0.4ms 约 92% 可行,但 0.4ms/0.35ms 的复测因
电机掉线作废,**上限究竟在 0.4 还是 0.5ms 仍未定**。恢复电机后需重跑这两档;
在此之前,建议值只写"0.5ms 已验证可跑",不要外推到 0.4ms。
5. **重复性**:同一档跑 3 次看 λ / GD32 负载 / 电机刷新率 / **online 标志** 的散布,
确认 0.5ms 与 0.25ms 的分界不是单次偶然,也不是电机掉线的副产物。
6. **0.5ms 档实跑功能验证**:在 0.5ms 下开电机页跑一段完整命令序列,确认前端
`drainRx()` 与波形在 2kHz 下不积压(本调研只测了计数器,没测界面实际可用性)。