主站诊断
建议通过 事件 驱动异常处理,而非自行轮询。 直接读取诊断属性适用于 UI 显示等场景。
单个从站的状态诊断、链路质量请参考 从站诊断。
功能概览
| 功能 | 说明 |
|---|---|
| 通信与性能统计 | 帧计数、丢包、抖动、PDO 丢帧、网口状态、拓扑 |
| DC 同步 | 同步窗口阈值、DCSyncLost 事件 |
| 冗余状态 | 冗余激活、故障点检测(断线 + CRC 故障定位) |
| 诊断控制 | 启停数据采集、重置统计 |
通信与性能统计
| 类别 | 函数 | 类型 | 读写 | 说明 |
|---|---|---|---|---|
| 帧计数 | GetPacketLossRate(mi) | float | 只读 | 丢包率(0.0~1.0),TX vs RX 5 秒滑窗 |
| 帧计数 | GetLateFrameRate(mi) | float | 只读 | 过慢帧率(0.0~1.0),idx 出 8 帧窗 stale |
| WKC | GetExpectedWKC(mi) / SetExpectedWKC(mi, wkc) | uint16_t | 读写 | 期望工作计数器 |
| WKC | GetPrimaryWKC() | uint16_t | 只读 | 主网口 WKC(冗余模式独立跟踪) |
| WKC | GetSecondaryWKC() | uint16_t | 只读 | 副网口 WKC(冗余模式独立跟踪) |
| WKC 镜像 | GetWkcActualMirror(mi) | uint16_t | 只读 | 总线实测工作计数器镜像(actual WKC)— 反映此刻哪些从站真的在响应。R1:如实值永不篡改。薄读零帧,实时 |
| WKC 镜像 | GetWkcExpectedMirror(mi) | uint16_t | 只读 | 期望工作计数器镜像(expected WKC)— 配置期/进 OP 确定的固定真值。R1:永不动态下调迁就劣化总线。薄读零帧,实时 |
| WKC 镜像 | GetWcDeficit(mi) | uint16_t | 只读 | WKC 缺额 = expected − actual(>0 表示有映射从站此刻没贡献 WKC,疑似掉站/热插拔)。从站恢复后自动归 0。薄读零帧,实时 |
| WKC 镜像 | GetMappedSlaveCount(mi) | uint16_t | 只读 | 已映射(参与 WKC 计算)的从站数。薄读零帧,实时 |
| WKC 镜像 | GetWcStateSeq(mi) | uint64_t | 只读 | 内核 WcState 缓存序列号(每次刷新自增)。判断 per-slave 镜像是否在两次读取间更新过。薄读零帧,实时 |
| 从站异常 | GetSlaveLinkQuality(mi, si) | int16_t | 只读 | 从站链路质量 0-100% |
| 网口帧计数 | GetWdkPrimaryFrameTx(mi) / GetWdkPrimaryFrameRx(mi) | uint32_t | 只读 | 主网卡发送/接收累计帧数 |
| 网口帧计数 | GetWdkSecondaryFrameTx(mi) / GetWdkSecondaryFrameRx(mi) | uint32_t | 只读 | 副网卡发送/接收累计帧数(冗余有效) |
| 网口帧计数 | GetWdkPdoIdxDropCount(mi) | uint32_t | 只读 | PDO idx 槽位被覆盖丢弃次数 |
| 详细诊断 | GetDetailedDiagnostics(mi) | void* | 只读 | 详细诊断数据指针(无需释放) |
| 通信统计 | GetCommunicationStats(mi) / ResetCommunicationStats(mi) | void* | 读写 | 通信统计指针 + 重置 |
详细诊断数据结构:
typedef struct {
uint32_t FrameErrors;
uint32_t LostFrames;
uint32_t ChecksumErrors;
uint32_t TimeoutFrames;
uint32_t RxErrorCount[EC_MAXSLAVE];
uint32_t TxErrorCount[EC_MAXSLAVE];
uint16_t LostLinkCount[EC_MAXSLAVE];
uint16_t InvalidFrameCount[EC_MAXSLAVE];
uint16_t WorkingCounterErrors;
uint16_t ConsecutiveWkcErrors;
uint32_t TotalWkcMismatches;
uint32_t PdoLostFrames[EC_MAXGROUP];
uint32_t PdoConsecutiveLost[EC_MAXGROUP];
uint32_t PdoMaxConsecutiveLost[EC_MAXGROUP];
int16_t LinkQualityPercent[EC_MAXSLAVE];
uint32_t PrimaryPortTxCount;
uint32_t PrimaryPortRxCount;
uint32_t SecondaryPortTxCount;
uint32_t SecondaryPortRxCount;
uint32_t PrimaryPortErrors;
uint32_t SecondaryPortErrors;
} ec_diagnostics_data_t;
PDO 丢帧统计
void GetPDOFrameLossStats(uint16_t master_index, uint8_t group,
uint32_t* total_lost, uint32_t* consecutive_lost,
uint32_t* max_consecutive_lost);
void ResetPDOFrameLossStats(uint16_t master_index, uint8_t group);
按组(0-7)查询 PDO 丢帧统计:累计丢帧、当前连续丢帧、历史最大连续丢帧。
WKC 镜像(掉站可观测)
uint16_t GetWkcActualMirror(uint16_t master_index); /* 实测 WKC 镜像 */
uint16_t GetWkcExpectedMirror(uint16_t master_index); /* 期望 WKC 镜像 (固定真值) */
uint16_t GetWcDeficit(uint16_t master_index); /* 缺额 = expected - actual */
uint16_t GetMappedSlaveCount(uint16_t master_index); /* 已映射从站数 */
uint64_t GetWcStateSeq(uint16_t master_index); /* WcState 缓存序列号 (自增) */
WKC 镜像是内核 per-slave WcState 诊断缓存的主站级聚合镜像,全部薄读零帧(不缓存、不需刷新——内核每周期、WKC 异常时立即维护,DLL 保证读到即此刻真实总线现实)。配合每个从站的 GetSlaveWcState(mi, si) 可定位到具体掉了哪个从站。
R1 可观测性约定(不可违反):
GetWkcExpectedMirror(mi)= 配置期 / 进 OP 时确定的固定真值,拓扑固定它就固定,概念上不可变,永不动态下调迁就劣化总线。GetWkcActualMirror(mi)= 总线实测值,如实反映此刻哪些从站真的在响应,永不篡改。GetWcDeficit(mi) = expected − actual。> 0不是 master 故障,而是有映射从站此刻没贡献 WKC(疑似掉站 / 热插拔恢复中);从站恢复后自动归 0,无需任何重置调用。
示例:
uint16_t act = dll.GetWkcActualMirror(master);
uint16_t exp = dll.GetWkcExpectedMirror(master);
printf("WKC: %u / %u (映射从站 %u 个)\n", act, exp, dll.GetMappedSlaveCount(master));
uint16_t deficit = dll.GetWcDeficit(master);
if (deficit > 0) {
printf("WKC 缺额 %u — 有从站没在响应, 逐个从站定位:\n", deficit);
for (int si = 1; si <= dll.GetSlaveCount(master); si++) {
if (dll.GetSlaveWcState(master, si) == 0) /* 0 = 未贡献 */
printf(" 从站 %d 未贡献 WKC\n", si);
}
}
uint64_t seq = dll.GetWcStateSeq(master); /* 序列号判断镜像是否更新过 (无需轮询帧) */
热插拔重建
/* [roundHOTSWAP1] 运行中一次性热插拔重建拓扑: 重扫从站 + 重建拓扑图 + 恢复 OP, 不停总线.
* 返回 int 错误码: 0=成功, 负值=失败. */
int HotSwapRebuild(uint16_t master_index);
运行中一次性热插拔重建拓扑:任意状态下调用一次,在线重扫总线 + 重建拓扑图 + 重配 PDO + 恢复到运行态(OP)。适用于现场拔掉 / 换上一个模块后,不停总线地把网络重新带回运行态。它不是 EcConfigInit() 那种"释放全网从头扫"的重建——是面向热插拔的在线增量恢复。
返回值(错误码语义):
| 返回码 | 名称 | 含义与处理 |
|---|---|---|
0 | OK | 成功:从站全部回到 OP |
-20 | BUSY | 另一操作正在进行——稍后重试 |
-21 | RESCAN_0 | 重扫到 0 个从站——已停在 PreOp 安全态,检查物理链路 / 供电 |
-22 | SDO_ABORT | 重配被从站 SDO abort 拒绝——检查 PDO 映射 / Startup 参数 |
-23 | NO_OP | 部分从站未达 OP——已回滚停安全态,查各从站 AL Status Code |
-24 | TIMEOUT | 重建超时 |
-25 | IDX_FULL | 帧索引池饱和 |
-1 | (通用失败) | DLL 未导出该函数等的通用失败码 |
行为 / 约束(R1 如实可观测):
- 在 OP 状态调用会短暂中断 PDO(内部降 PreOp 重配再升回 OP),调用方需容忍这一周期的过程数据空窗。
- 失败时返错误码不掩盖——绝不靠篡改内部计数让结果"看起来对"。
- 即使最终 0 个从站在 OP,PDO 循环也照常运行(周期不掉、帧不停),期望 WKC 不动态下调迁就劣化总线。
示例:
int rc = dll.HotSwapRebuild(master);
switch (rc) {
case 0:
printf("热插拔重建成功, 从站已回到 OP\n");
break;
case -20:
printf("有操作进行中, 稍后重试\n");
break;
case -21:
printf("重扫到 0 站, 已停 PreOp 安全态——检查链路/供电\n");
break;
case -23:
printf("部分从站未达 OP, 已回滚安全态——逐个查 AL Status Code:\n");
for (int si = 1; si <= dll.GetSlaveCount(master); si++) {
uint16_t al = dll.GetSlaveALStatusCode(master, si);
if (al) printf(" 从站 %d: AL=0x%04X\n", si, al);
}
break;
default:
printf("热插拔重建失败: %d\n", rc);
break;
}
DC 同步
自动监控(ETG.1500 5.13.3),每秒检查各从站时间偏差。超出 SyncWindowThreshold 阈值时触发 DCSyncLost 回调。
void SetSyncWindowThreshold(uint16_t master_index, int threshold_ns);
int GetSyncWindowThreshold(uint16_t master_index);
threshold_ns 默认 1000ns。
单个从站的同步状态请使用 GetSlaveSyncWindowStatus(),详见 从站 DC 同步。
冗余状态
int GetBreakPoints(uint16_t master_index,
uint16_t* out_slaves, uint8_t* out_ports,
uint8_t* out_types, uint16_t max_results);
int GetRingMode(uint16_t master_index);
BOOL GetSecondaryLinkStatus(uint16_t master_index);
GetBreakPoints() 检测当前所有故障点,统一覆盖两类物理故障:
| FaultType | 类型 | 检测方式 | 典型场景 |
|---|---|---|---|
| 0 | 断线 | DL Status 端口物理链路丢失 | 拔线、线缆断裂 |
| 1 | CRC 故障 | 端口级 RxError + InvalidFrame 持续增长 | 接触不良、线缆老化 |
GetRingMode() 返回值: 0=Inactive(未激活), 1=Dual(双向冗余), 2=Degraded(secondary 链路不可用,仅 primary 工作)。
故障线缆段定位: 当相邻从站对向端口(如从站 N 的 P1 与从站 N+1 的 P0)同时报故障,说明线缆段有问题;仅单侧报故障则定位到该端口连接器。
单个从站的冗余状态请参考 从站诊断 - 冗余诊断。
诊断快照
BOOL GetDiagnosticsSnapshot(uint16_t master_index, ec_diagnostics_snapshot_t* snapshot);
一次调用获取所有诊断数据的一致快照(内部加锁,线程安全),避免多次属性访问导致的数据不一致和性能开销。适合 UI 刷新和日志记录场景。
typedef struct {
int frequency; /* 每秒帧数 (Hz) */
uint32_t error_count; /* 每秒错误数 */
float packet_loss_rate; /* 丢包率 (0.0-1.0) */
float late_frame_rate; /* 过慢帧率 (0.0-1.0) */
double mailbox_latency_avg_us; /* 邮箱收发延迟 - 平均 (微秒) */
double mailbox_latency_us; /* 邮箱收发延迟 - 最大 (微秒) */
int cycle_time_us; /* 实际周期时间 (微秒) */
uint16_t wkc_actual; /* 当前 WKC */
uint16_t wkc_expected; /* 期望 WKC */
uint32_t bus_cycle_hz; /* 总线频率 (Hz) */
double bus_max_jitter_us; /* 总线最大抖动 (微秒) */
double bus_avg_jitter_us; /* 总线平均抖动 (微秒) */
double bus_roundtrip_us; /* 总线往返延迟 (微秒) */
double bus_load_percent; /* 通讯负载 (%) */
uint32_t smi_count; /* SMI 次数 */
double smi_peak_us; /* SMI 峰值 (微秒) */
int primary_port_ok; /* 主端口正常 (BOOL) */
int secondary_port_ok; /* 副端口正常 (BOOL) */
int redundancy_active; /* 冗余激活 (BOOL) */
} ec_diagnostics_snapshot_t;
mailbox_latency_us / mailbox_latency_avg_us 反映邮箱(CoE/SoE/FoE/EoE/AoE/VoE 等非周期、请求-响应)事务的收发往返时延 —— 从请求发出到响应返回的耗时(最近 1 秒结算的最大值 / 平均值)。
PDO 过程数据已是纯内核 RT 收发 + 内核共享内存指针零拷贝,不存在"用户态每周期通知"环节,旧的"应用抖动"指标失去意义,故改为统计邮箱事务时延。总线时序仍由 bus_max_jitter_us / bus_avg_jitter_us 反映。
示例:
ec_diagnostics_snapshot_t snap;
if (dll.GetDiagnosticsSnapshot(master, &snap)) {
printf("频率: %d Hz, 邮箱收发延迟: %.2f us, WKC: %d/%d, 丢包: %.4f\n",
snap.frequency, snap.mailbox_latency_us,
snap.wkc_actual, snap.wkc_expected, snap.packet_loss_rate);
}
诊断控制
void SetDiagnosticsEnabled(uint16_t master_index, BOOL enable);
BOOL GetDiagnosticsEnabled(uint16_t master_index);
void ResetDiagnostics(uint16_t master_index);
SetDiagnosticsEnabled() 启用/禁用诊断数据采集(默认关闭,启用后周期性采样)。ResetDiagnostics() 一次性重置所有诊断统计:基础诊断统计、PDO 丢帧统计、DC 同步窗口统计、所有从站的端口错误计数器。只清空主站层面的统计数据, 不影响 FSoE 安全连接状态机。
AL 错误分类
/* 定义于 master/diagnostics_ext.h, 按 AL Status Code 高位段分类 */
typedef enum {
AL_ERR_NONE = 0,
AL_ERR_GENERAL = 1, /* 0x0001-0x000F 通用错误 */
AL_ERR_STARTUP = 2, /* 0x0010-0x001F 启动 (PDO 映射 / SM 配置) */
AL_ERR_MAILBOX = 3, /* 0x0020-0x002F 邮箱 */
AL_ERR_WATCHDOG = 4, /* 0x0030-0x003F 看门狗 */
AL_ERR_SYNC = 5, /* 0x0040-0x004F 同步 */
AL_ERR_DC = 6, /* 0x0050-0x005F DC */
AL_ERR_MBX_EOE = 7, /* 0x0060-0x006F EoE 邮箱 */
AL_ERR_UNKNOWN = 99
} DarraAlErrorClass;
DarraAlErrorClass diagnostics_classify_al_error(uint16_t alStatusCode);
对从站 AL Status Code 进行分类,帮助快速判断错误性质和处理策略。分类依据 AL Status Code 的高位段。
| 分类 | 处理建议 |
|---|---|
| AL_ERR_GENERAL | 通用错误,查阅 ETG.1000 或从站手册 |
| AL_ERR_STARTUP | 检查 PDO 映射、SM 配置、Startup 参数 |
| AL_ERR_MAILBOX | 检查邮箱协议配置 (CoE/FoE 等) |
| AL_ERR_WATCHDOG | PDO 周期过慢或通信中断,检查丢帧 |
| AL_ERR_SYNC | 同步错误,检查 SYNC0 周期配置 |
| AL_ERR_DC | DC 同步配置问题,检查 DC 启动时间 |
| AL_ERR_MBX_EOE | EoE 邮箱错误 |
| AL_ERR_UNKNOWN | 未在已知段内,查阅 ETG.1000 或从站手册 |
0x001E无效输入映射 — AL_ERR_STARTUP0x001D无效输出映射 — AL_ERR_STARTUP0x0011无效邮箱配置 — AL_ERR_GENERAL0x002D同步错误 — AL_ERR_MAILBOX0x0032DC 同步超时 — AL_ERR_WATCHDOG0x0052DC PLL 错误 — AL_ERR_DC
打印从站状态时, C 可用 ec_state_name(state) inline 把 EC_STATE_INIT/PRE_OP/SAFE_OP/OP/BOOT 直接转字符串, 屏蔽错误位 (& 0x0F); 同样 ec_link_name(ec_link(mi)) 输出 "UP" / "DOWN" / "REDUNDANCY". 详见 inline 包装.
从站错误计数器
BOOL ResetSlavePortErrorCounters(uint16_t master_index, uint16_t slave_index);
BOOL ReadSlavePortErrorCounters(uint16_t master_index, uint16_t slave_index,
uint8_t* rx_error, uint8_t* invalid_frame, uint8_t* lost_link);
int ReadAllSlavePortErrorCounters(uint16_t master_index);
void* GetSlavePortErrorStats(uint16_t master_index, uint16_t slave_index);
读取/重置从站 ESC 端口错误计数器(ETG.1000.4)。rx_error/invalid_frame/lost_link/fwd_rx_error 各为 4 字节数组,对应 P0-P3 端口(4 类 4 字节数组)。
示例:
uint8_t rx[4], inv[4], lost[4];
if (dll.ReadSlavePortErrorCounters(master, 1, rx, inv, lost)) {
for (int p = 0; p < 4; p++) {
if (rx[p] || inv[p] || lost[p])
printf("从站1 P%d: rx=%u inv=%u lost=%u\n", p, rx[p], inv[p], lost[p]);
}
}
配置预检查
int ec_validate_config(uint16_t master_index);
Build 前预检查主站配置是否完整(网络适配器、从站发现、PDO 映射等)。返回 0 表示配置有效,非 0 表示配置问题数量。
主站身份与诊断对象
BOOL GetMasterIdentity(uint16_t master_index, ec_master_identity_t* identity);
BOOL GetMasterDiagData(uint16_t master_index, ec_master_diag_data_t* diag);
读取主站 ETG.1510 标准对象:身份对象 0x1018(VendorID/ProductCode/RevisionNumber/SerialNumber)和诊断数据对象 0xF120。
诊断消息 (CoE 0x10F3 诊断历史)
从站对象 0x10F3(诊断历史对象,ETG.1020)是标准对象字典对象,应用层用通用 SDO 读写原语 dx_sdo_read / dx_sdo_write 直接读取其各子索引即可(0x10F3:04 是否有新消息、0x10F3:02 最新编号、0x10F3:05 标志、0x10F3:06..FF 各条消息、0x10F3:03 写编号确认已读)。完整子索引布局与读取流程示例见 CoE 诊断历史。
并非所有从站都支持 0x10F3 诊断历史对象。不支持的从站读取时 dx_sdo_read_ex 回填的 out_abort_code 为对象不存在错误。此对象通过 SDO 读取,不建议在实时路径中高频调用。
紧急控制与中止
void AbortScan(void);
void ResetScanAbort(void);
void AbortNetwork(void);
void ResetAbortNetwork(void);
void EmergencyCloseNics(void);
终止正在进行的网络扫描或紧急关闭网卡,避免长操作卡死调用方。
EmergencyCloseNics 不走标准 Stop / Dispose 流程, 仅在死锁兜底场景使用. 正常退出请用 Stop + Dispose + EcClose.
完整示例
#define DYNAMIC_LOAD
#include "ethercat.h"
#include <stdio.h>
int main(void) {
dll_t dll;
LOAD_DLL(&dll, "DarraEtherCAT.dll");
uint16_t master = dll.Initialize();
dll.SetNetwork(master, "\\Device\\NPF_{GUID}", "");
dll.SetStateSequence(master, EC_STATE_OPERATIONAL, 10000);
dll.Start(master);
dll.SetDiagnosticsEnabled(master, TRUE);
/* 一致快照 */
ec_diagnostics_snapshot_t snap;
if (dll.GetDiagnosticsSnapshot(master, &snap)) {
printf("频率: %d Hz, 邮箱收发延迟: %.2f us, 丢包: %.4f\n",
snap.frequency, snap.mailbox_latency_us, snap.packet_loss_rate);
}
/* PDO 丢帧 */
uint32_t total, cur, max;
dll.GetPDOFrameLossStats(master, 0, &total, &cur, &max);
if (cur > 10) printf("警告: PDO 连续丢帧 %u\n", cur);
/* 故障点 */
uint16_t bp_s[8]; uint8_t bp_p[8], bp_t[8];
int n = dll.GetBreakPoints(master, bp_s, bp_p, bp_t, 8);
for (int i = 0; i < n; i++) {
printf("故障: 从站%d P%d %s\n", bp_s[i], bp_p[i],
bp_t[i] == 0 ? "断线" : "CRC故障");
}
/* AL 错误分类 */
uint16_t al = dll.GetSlaveALStatusCode(master, 1);
if (al) {
DarraAlErrorClass c = diagnostics_classify_al_error(al);
printf("从站1 错误 0x%04X: 分类=%d\n", al, c);
}
dll.ResetDiagnostics(master);
dll.Stop(master);
dll.Dispose(master);
UNLOAD_DLL(&dll);
return 0;
}
按问题分块的诊断字段指南 (C 风格)
GetDetailedDiagnostics(mi) 返回 ec_diagnostics_data_t*(字段名见上文 详细诊断数据结构,全部 PascalCase)。总线 RT 性能指标走 GetDiagnosticsSnapshot() 输出的 ec_diagnostics_snapshot_t。
| 现场症状 | C 接口 |
|---|---|
| 是不是丢包? | diag->LostFrames + GetPacketLossRate(mi) |
| 是偶发还是持续? | diag->ConsecutiveWkcErrors (≥5 = 持续) |
| WKC 累计不匹配? | diag->TotalWkcMismatches / diag->WorkingCounterErrors |
| CRC 问题? | diag->InvalidFrameCount[i] (ESC 0x0304) |
| 哪根网线断了? | GetBreakPoints(mi, ...) |
| 总线抖动 / 周期? | snap.bus_max_jitter_us / snap.bus_cycle_hz (GetDiagnosticsSnapshot) |
1. 帧错误 (TX vs RX 对比)
ec_diagnostics_data_t 字段:
- LostFrames: 帧根本没回来
- TimeoutFrames: PDO 周期未响应
- WorkingCounterErrors: WKC 不匹配
- ConsecutiveWkcErrors: 连续 WKC 错误 (核心区分偶发/持续)
- TotalWkcMismatches: 累计 WKC 不匹配总数
- FrameErrors: 派生总和
- ChecksumErrors: 物理层 CRC (从 ESC 派生)
2. 端口收发
PrimaryPortTxCount / PrimaryPortRxCount, SecondaryPortTxCount / SecondaryPortRxCount, PrimaryPortErrors / SecondaryPortErrors (5 秒滑窗)
3. per-slave 物理层 (ESC 寄存器, 数组 1-based)
RxErrorCount[i] (ESC 0x0300) | InvalidFrameCount[i] (ESC 0x0304 CRC) | LostLinkCount[i] (ESC 0x0310) | LinkQualityPercent[i]
4. PDO 丢帧 (每组)
PdoLostFrames[g] (累计丢帧) | PdoConsecutiveLost[g] (当前连续) | PdoMaxConsecutiveLost[g] (历史最大连续)
5. 总线 RT 性能
通过 GetDiagnosticsSnapshot(mi, &snap) 一致读 ec_diagnostics_snapshot_t:
- bus_cycle_hz / bus_max_jitter_us / bus_avg_jitter_us / bus_roundtrip_us
- frequency / mailbox_latency_us / mailbox_latency_avg_us / cycle_time_us / packet_loss_rate
完整诊断流程示例 (C)
ec_diagnostics_data_t* diag = (ec_diagnostics_data_t*)dll.GetDetailedDiagnostics(mi);
// 1. 持续 vs 偶发
if (diag->ConsecutiveWkcErrors >= 5)
printf("持续 WKC 错误\n");
// 2. 丢包率
float loss = dll.GetPacketLossRate(mi);
if (loss > 0.01f) printf("丢包率 %.2f%% 严重\n", loss * 100.0f);
// 3. 故障定位
uint16_t bp_slaves[16]; uint8_t bp_ports[16]; uint8_t bp_types[16];
int bpn = dll.GetBreakPoints(mi, bp_slaves, bp_ports, bp_types, 16);
for (int i = 0; i < bpn; i++)
printf("故障点: 从站 %d P%d (类型=%d)\n", bp_slaves[i], bp_ports[i], bp_types[i]);
// 4. 最不稳定从站 (按 ESC CRC 错误计数)
int worst = 0; uint16_t worstCnt = 0;
for (int i = 1; i < EC_MAXSLAVE; i++)
if (diag->InvalidFrameCount[i] > worstCnt)
{ worstCnt = diag->InvalidFrameCount[i]; worst = i; }
if (worstCnt > 0) printf("CRC 错误最多的从站: #%d (%u 次)\n", worst, worstCnt);
// 5. 总线 RT 性能
ec_diagnostics_snapshot_t snap;
if (dll.GetDiagnosticsSnapshot(mi, &snap))
printf("总线 %u Hz, 最大抖动 %.2f us\n",
snap.bus_cycle_hz, snap.bus_max_jitter_us);