正式 scripts 仅保留 README 自测清单;根目录报告迁入 docs/reports; lab 采样 dump 出库;CSS 注明 btn/toast 所有权并 bump ?v=。
21 KiB
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 |
| 板载固件版本 | — | 无法确认(设备不回报版本字) |
三点后果,均已在结论中按保守口径处理:
- 台账哈希与任何在盘产物都不匹配,且产物时间戳(02:31)晚于台账记录 —— 无法证明板上跑的 就是当前源码编出的那一版。本报告不复用任何历史固件结论(含"1.6.5 已修 seqlock 撕裂")。
- 测量期间主站构建又更新过(02:39/02:40),无法证明 02:32 启动的进程内存里就是盘上那份 exe。
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:1615queue.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处理,封顶即报"下界"而非精确值。
四、曲线与解读
① 主站循环率 在 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% | 不可引用 | — |
三条读数:
- CAN 的负担是常量、不是变量:三档 Δ 稳定在 8.7–9.3pp。所以 CAN 既不是上限的成因, 也不随频率张开 —— 带 CAN 那轮的快档塌陷不是 CAN 挤出来的。
- 纯 ECAT 到 2kHz 毫无问题(68.0%,达成率 100.0%,错拍 17.1 次/s)。
- 上限仍没测到:加压到 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 周期无关 → 提速不改善它。
六、局限
- 单台单次,未做重复性与温漂验证。
- λ≈3993/s 是本次台子的 CAN 工况。λ 变则分界变,本报告给的是相对关系, 0.5ms 这个具体数字不可直接搬到别的 CAN 流量下。
- 板载固件版本无法确认(见第二节),历史固件结论一律未采信;
rx_torn/pdo_reentry恒零属无检出功效,只能表述为"本次测不到",不得读成"无撕裂/已修好"。 cyc_us.late/cyc_us.lost恒为 0(ecat_native.c里是 Python 时代遗留占位),未接线,未使用。- 扫描用 HTTP 以 10Hz 打 native,对被测对象有扰动;但主机 CPU 已恒 100% 单核, 且 native 另有 HTTP 线程,扰动量级判断为小 —— 未单独做无负载对照实验。
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 快档读数一律不采信,也未写入本报告。)- 每拍固定开销 F≈323µs 是从 4 个负载点反推的,不是直接计时测得;两项模型在 2ms 档残差 +10.5 个百分点,说明低速档还有未建模的开销项。F 只能当量级用。
七、待验证(下一步该做的判据)
drop语义二选一(第五节的 (a)/(b)):从 ESP32/WiFi 侧读固件那份 u32rx_drop_count(app_spi_cmd_can_feedback.cpp:99,CAN 通道队列满丢弃的权威口径), 与 ECAT 侧 u16bridge.drop对速率。若两侧同为 ~4000/s → (a) 成立,丢帧发生在入窗之前, 与 ECAT 无关;若 WiFi 侧接近 0 而 ECAT 侧 4000/s → (b) 成立,字段读错偏移, 本报告的故障率轴需要整体重做。这是唯一还悬着的承重结论。- 从站侧健康度目前不可读:
OVF在fillRxWindow里每拍清零,只能取到单拍增量, 而单拍增量落在 u8 分辨率以下即恒 0 —— 等于从站没有可用的累计丢帧仪表。 要判窗口是否真的健康,需要固件改成累计值或主站侧累加(属改动,需另行批准)。 - 每拍固定开销 F 直接计时:给
fillRxWindow加计时(属改动,需另行批准)。 目前 F≈323µs 是从 4 个负载点反推的;直接量到 F 就能把「上限 2.7kHz」从拟合 变成算式,也才能判断减字节能否真的换到速度。 - 模型已被证伪(保守方向):F/T+R 预测 0.4ms 约 92% 可行,但 0.4ms/0.35ms 的复测因 电机掉线作废,上限究竟在 0.4 还是 0.5ms 仍未定。恢复电机后需重跑这两档; 在此之前,建议值只写"0.5ms 已验证可跑",不要外推到 0.4ms。
- 重复性:同一档跑 3 次看 λ / GD32 负载 / 电机刷新率 / online 标志 的散布, 确认 0.5ms 与 0.25ms 的分界不是单次偶然,也不是电机掉线的副产物。
- 0.5ms 档实跑功能验证:在 0.5ms 下开电机页跑一段完整命令序列,确认前端
drainRx()与波形在 2kHz 下不积压(本调研只测了计数器,没测界面实际可用性)。
