Files
DCU_DesktopController/scripts/plot_ecat_nocan_vs_can.py
T

113 lines
5.1 KiB
Python
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.
#!/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()