113 lines
5.1 KiB
Python
113 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
||||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""对比「带 CAN」与「拔掉 CAN」两轮扫描,分离每拍负担的组成。
|
|||
|
|
|
|||
|
|
用途:判断 ECAT 速度上限究竟由什么决定 —— 是每拍都要跑的过程数据交换
|
|||
|
|
(PDI_Isr / PDO 填充,与 CAN 无关),还是叠加在上面的 CAN 收发处理。
|
|||
|
|
|
|||
|
|
判读:两条 load 曲线的**垂直间距**就是 CAN 在每个周期档上贡献的负担。
|
|||
|
|
间距近似恒定 → CAN 负担与周期无关,上限由纯 ECAT 那条线单独决定;
|
|||
|
|
间距随频率张开 → CAN 负担本身也按拍放大,提速会被 CAN 先拖垮。
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
python scripts/plot_ecat_nocan_vs_can.py \
|
|||
|
|
--nocan lab/ecat_rate_sweep_nocan.csv --withcan lab/ecat_rate_sweep_withcan.csv
|
|||
|
|
"""
|
|||
|
|
import argparse
|
|||
|
|
import csv
|
|||
|
|
import os
|
|||
|
|
|
|||
|
|
import matplotlib
|
|||
|
|
matplotlib.use("Agg")
|
|||
|
|
import matplotlib.pyplot as plt
|
|||
|
|
from matplotlib import rcParams
|
|||
|
|
|
|||
|
|
rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "DejaVu Sans"]
|
|||
|
|
rcParams["axes.unicode_minus"] = False
|
|||
|
|
rcParams["figure.dpi"] = 130
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load(path):
|
|||
|
|
out = {}
|
|||
|
|
for r in csv.DictReader(open(path, encoding="utf-8")):
|
|||
|
|
def num(k):
|
|||
|
|
v = r.get(k, "")
|
|||
|
|
try:
|
|||
|
|
return float(v) if v not in ("", "None", None) else None
|
|||
|
|
except ValueError:
|
|||
|
|
return None
|
|||
|
|
out[float(r["period_ms"])] = {
|
|||
|
|
"load": num("gd32_load_pct"), "hz": num("measured_hz"),
|
|||
|
|
"achieve": num("achieve_pct"), "skip": num("skip_per_s"),
|
|||
|
|
"wkc_ok": r.get("wkc") == r.get("expected_wkc"),
|
|||
|
|
"stale": num("gd32_load_stale")}
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
ap = argparse.ArgumentParser(description="带 CAN vs 无 CAN 每拍负担分离")
|
|||
|
|
d = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||
|
|
ap.add_argument("--nocan", default=os.path.join(d, "lab", "ecat_rate_sweep_nocan.csv"))
|
|||
|
|
ap.add_argument("--withcan", default=os.path.join(d, "lab", "ecat_rate_sweep_withcan.csv"))
|
|||
|
|
ap.add_argument("--out", default=None)
|
|||
|
|
a = ap.parse_args()
|
|||
|
|
|
|||
|
|
no, wc = load(a.nocan), load(a.withcan)
|
|||
|
|
common = sorted(set(no) & set(wc), reverse=True)
|
|||
|
|
out = a.out or os.path.splitext(a.nocan)[0] + "_vs.png"
|
|||
|
|
|
|||
|
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13.0, 5.2))
|
|||
|
|
fig.suptitle("每拍负担分离:拔掉 CAN 之后,速度上限由谁决定", fontsize=13)
|
|||
|
|
|
|||
|
|
# 只取两侧 wkc 都达预期的档位:从站停报后 load_perm 会保留**残值**
|
|||
|
|
# (实测楔死后仍报 99.2%),留在图里会被当成真数据、把 Δ 趋势带歪。
|
|||
|
|
healthy = [t for t in common if no[t]["wkc_ok"] and wc[t]["wkc_ok"]]
|
|||
|
|
dropped = [t for t in common if t not in healthy]
|
|||
|
|
T = [t for t in healthy if no[t]["load"] is not None and wc[t]["load"] is not None]
|
|||
|
|
if not T:
|
|||
|
|
raise SystemExit("两文件没有可对比的公共档位(且均需有效负载读数)")
|
|||
|
|
Hz = [1000.0 / t for t in T]
|
|||
|
|
ax1.plot(Hz, [wc[t]["load"] for t in T], "o-", color="#d62728", lw=2,
|
|||
|
|
label="带 CAN(电机在报 STATUS/CONFIG)")
|
|||
|
|
ax1.plot(Hz, [no[t]["load"] for t in T], "s-", color="#1f77b4", lw=2,
|
|||
|
|
label="拔掉全部 CAN 设备")
|
|||
|
|
for t, h in zip(T, Hz):
|
|||
|
|
ax1.annotate(f"Δ={wc[t]['load'] - no[t]['load']:.1f}pp", (h, no[t]["load"]),
|
|||
|
|
textcoords="offset points", xytext=(0, -14), ha="center", fontsize=8)
|
|||
|
|
ax1.axhline(100, ls=":", color="k", lw=1)
|
|||
|
|
ax1.set_xscale("log"); ax1.set_ylim(0, 112)
|
|||
|
|
ax1.set_xlabel("循环率 (Hz)"); ax1.set_ylabel("load_perm (%)")
|
|||
|
|
ax1.set_title("从站每拍负担(load_perm 口径,非整机 CPU)")
|
|||
|
|
ax1.legend(fontsize=8, loc="upper left"); ax1.grid(alpha=0.3)
|
|||
|
|
|
|||
|
|
ax2.plot(Hz, [wc[t]["skip"] for t in T], "o-", color="#d62728", lw=2, label="带 CAN")
|
|||
|
|
ax2.plot(Hz, [no[t]["skip"] for t in T], "s-", color="#1f77b4", lw=2, label="无 CAN")
|
|||
|
|
ax2.set_xscale("log"); ax2.set_yscale("symlog", linthresh=1)
|
|||
|
|
ax2.set_xlabel("循环率 (Hz)"); ax2.set_ylabel("skip_total (次/s)")
|
|||
|
|
ax2.set_title("主站错拍率:CAN 有无对节拍精度的影响")
|
|||
|
|
ax2.legend(fontsize=8, loc="upper left"); ax2.grid(alpha=0.3)
|
|||
|
|
|
|||
|
|
fig.tight_layout(rect=(0, 0, 1, 0.94))
|
|||
|
|
fig.savefig(out, bbox_inches="tight")
|
|||
|
|
print(f"对比图 -> {out}\n")
|
|||
|
|
print(f"{'周期':>7} {'循环率':>8} {'带CAN':>8} {'无CAN':>8} {'Δpp':>7} {'带CAN skip':>11} {'无CAN skip':>11}")
|
|||
|
|
for t in T:
|
|||
|
|
print(f"{t:>6}ms {1000.0/t:>7.0f}Hz {wc[t]['load']:>7.1f}% {no[t]['load']:>7.1f}% "
|
|||
|
|
f"{wc[t]['load']-no[t]['load']:>6.1f} {wc[t]['skip']:>10.2f} {no[t]['skip']:>10.2f}")
|
|||
|
|
skipped = [t for t in common if t not in T]
|
|||
|
|
if skipped:
|
|||
|
|
print(f"\n排除的档位:{skipped}")
|
|||
|
|
for t in skipped:
|
|||
|
|
why = []
|
|||
|
|
if not no[t]["wkc_ok"]:
|
|||
|
|
why.append("无CAN 侧 wkc 不达预期(从站已停报,load_perm 为残值)")
|
|||
|
|
if not wc[t]["wkc_ok"]:
|
|||
|
|
why.append("带CAN 侧 wkc 不达预期")
|
|||
|
|
if no[t]["load"] is None or wc[t]["load"] is None:
|
|||
|
|
why.append("缺有效负载读数")
|
|||
|
|
print(f" {t}ms: " + "; ".join(why or ["未知"]))
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|