当前位置: 首页 > news >正文

【原创】IgH EtherCAT主站详解(二十三)--DC 同步实战

DC 同步实战

8.3 — 分布式时钟配置与测量

概览

DC 同步要达到什么效果?

EtherCAT 分布式时钟 (DC) 的核心目标是让总线上所有从站的内部时钟保持纳秒级同步。在配置正确的系统中,从站间同步抖动可控制在亚微秒级(通常小于 100 ns),从而实现精确的同步采样与输出更新。

本教程基于 IgH EtherCAT Master 1.5.2 提供的 dc_user 示例程序,演示如何在实际应用中配置和使用 DC 同步功能。

DC 同步的 4 个关键 API 调用

1. ecrt_slave_config_dc() — 配置从站 DC 参数(周期、偏移、激活字)
2. ecrt_master_application_time() — 每周期写入主站应用时间
3. ecrt_master_sync_reference_clock_to() — 同步参考时钟到指定时间
4. ecrt_master_sync_slave_clocks() — 同步所有从站时钟(漂移补偿)

示例程序运行效果

dc_user 示例以 1000 Hz(1 ms 周期)运行,启用 MEASURE_TIMING 宏后,每秒输出一次时序统计:

period     ...    1000147
exec       ...     987234
latency    ...      12453

时序测量指标

period (周期): 两次循环启动的实际间隔,理想值为 1000000 ns (1 ms)
exec (执行时间): 单次循环的有效执行耗时
latency (延迟): 唤醒时刻到实际启动的时刻差,反映调度抖动

技术详情

DC 配置完整流程

以下流程图展示了从创建主站到进入 DC 同步循环的完整步骤:

[流程图 - 结构化描述]

步骤 描述
1 步骤 1 ecrt_request_master(0) 请求主站
2 步骤 2 ecrt_master_create_domain() 创建过程数据域
3 步骤 3 ecrt_master_slave_config() 获取从站配置句柄
4 步骤 4 ecrt_slave_config_dc() 配置 DC 参数
5 步骤 5 ecrt_slave_config_reg_pdo_entry() 注册 PDO 入口
6 步骤 6 ecrt_master_activate() 激活主站
7 步骤 7 ecrt_domain_data() 获取过程数据指针
8 步骤 8 进入 cyclic_task() DC 同步循环
9 每周期循环
10 clock_nanosleep() 绝对时间休眠
11 ecrt_master_application_time() 写入应用时间
12 ecrt_master_receive()
13 ecrt_domain_process()
14 处理过程数据
15 ecrt_master_sync_reference_clock_to() 同步参考时钟
16 ecrt_master_sync_slave_clocks() 同步从站时钟
17 ecrt_domain_queue()
18 ecrt_master_send()

ecrt_slave_config_dc() 参数详解

这是 DC 配置的核心函数,在激活主站之前调用:

ecrt_slave_config_dc(sc, 0x0700, PERIOD_NS, 4400000, 0, 0);
参数 示例值 说明
assign_activate 0x0700 DC 激活字,写入从站寄存器 0x0981。
0x0700 = SYNC0 + SYNC1 均激活 (bit8=1, bit9=1)
0x0300 = 仅 SYNC0 激活 (bit8=1, bit9=0)
具体值取决于从站 ESI 文件中的 DcSync 配置
sync0_cycle PERIOD_NS
(1000000 ns)
SYNC0 脉冲周期时间(纳秒)。本例为 1 ms,对应 1000 Hz 循环频率
sync0_shift 4400000
(4.4 ms)
SYNC0 相对于周期起始的偏移时间(纳秒)。用于补偿传播延迟,确保从站在数据到达后才触发采样
sync1_cycle 0 SYNC1 脉冲周期时间(纳秒)。设为 0 表示不使用 SYNC1
sync1_shift 0 SYNC1 相对于 SYNC0 的偏移时间(纳秒)。设为 0 表示不使用 SYNC1

AssignActivate 激活字说明

assign_activate 的值决定了 DC 同步模式。常见值:
0x0300: 仅 SYNC0 激活 — 适用于大多数简单同步场景
0x0700: SYNC0 + SYNC1 同时激活 — 适用于需要双同步信号的高级场景(如需要区分采样和输出时刻)

该值通常来自从站 ESI (EtherCAT Slave Information) XML 文件中的 <Dc> 配置节点。

完整代码走读: dc_user/main.c

1. 宏定义与常量

#define FREQUENCY 1000              // 循环频率 1000 Hz
#define CLOCK_TO_USE CLOCK_MONOTONIC // 使用单调时钟,不受系统时间跳变影响
#define MEASURE_TIMING               // 启用时序测量#define NSEC_PER_SEC (1000000000L)   // 1 秒 = 10^9 纳秒
#define PERIOD_NS (NSEC_PER_SEC / FREQUENCY) // 周期 = 1000000 ns = 1 ms// 时间差计算宏
#define DIFF_NS(A, B) (((B).tv_sec - (A).tv_sec) * NSEC_PER_SEC + \(B).tv_nsec - (A).tv_nsec)// timespec 转 64 位纳秒
#define TIMESPEC2NS(T) ((uint64_t) (T).tv_sec * NSEC_PER_SEC + (T).tv_nsec)

2. 主站初始化与 DC 配置

int main(int argc, char **argv)
{ec_slave_config_t *sc;// 锁定内存,防止页面换出导致延迟抖动if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {perror("mlockall failed");return -1;}// 步骤 1: 请求主站master = ecrt_request_master(0);// 步骤 2: 创建过程数据域domain1 = ecrt_master_create_domain(master);// 步骤 3: 配置从站(总线耦合器 EK1100)sc = ecrt_master_slave_config(master, BusCouplerPos, Beckhoff_EK1100);// 步骤 3: 配置数字量输出从站 EL2008sc = ecrt_master_slave_config(master, DigOutSlavePos, Beckhoff_EL2008);// 注册 PDO 入口off_dig_out = ecrt_slave_config_reg_pdo_entry(sc, 0x7000, 1, domain1, NULL);// 步骤 3: 配置计数器从站sc = ecrt_master_slave_config(master, CounterSlavePos, IDS_Counter);off_counter_in = ecrt_slave_config_reg_pdo_entry(sc, 0x6020, 0x11, domain1, NULL);off_counter_out = ecrt_slave_config_reg_pdo_entry(sc, 0x7020, 1, domain1, NULL);// 步骤 4: 配置 DC 同步 —— 关键!// 激活 SYNC0+SYNC1, 周期 1ms, 偏移 4.4msecrt_slave_config_dc(sc, 0x0700, PERIOD_NS, 4400000, 0, 0);// 步骤 5: 激活主站ecrt_master_activate(master);// 步骤 6: 获取过程数据内存映射domain1_pd = ecrt_domain_data(domain1);// 设置实时调度优先级 (SCHED_FIFO)struct sched_param param = {};param.sched_priority = sched_get_priority_max(SCHED_FIFO);sched_setscheduler(0, SCHED_FIFO, &param);// 步骤 7: 进入 DC 同步循环cyclic_task();return 0;
}

3. 时间辅助函数

// 精确的 timespec 加法,正确处理纳秒进位
struct timespec timespec_add(struct timespec time1, struct timespec time2)
{struct timespec result;if ((time1.tv_nsec + time2.tv_nsec) >= NSEC_PER_SEC) {result.tv_sec = time1.tv_sec + time2.tv_sec + 1;result.tv_nsec = time1.tv_nsec + time2.tv_nsec - NSEC_PER_SEC;} else {result.tv_sec = time1.tv_sec + time2.tv_sec;result.tv_nsec = time1.tv_nsec + time2.tv_nsec;}return result;
}

4. DC 同步循环 (cyclic_task)

void cyclic_task()
{struct timespec wakeupTime, time;
#ifdef MEASURE_TIMINGstruct timespec startTime, endTime, lastStartTime = {};uint32_t period_ns = 0, exec_ns = 0, latency_ns = 0,latency_min_ns = 0, latency_max_ns = 0,period_min_ns = 0, period_max_ns = 0,exec_min_ns = 0, exec_max_ns = 0;
#endif// 获取当前时间作为首次唤醒基准clock_gettime(CLOCK_TO_USE, &wakeupTime);while(1) {// 计算下一个绝对唤醒时间 = 当前唤醒时间 + 周期wakeupTime = timespec_add(wakeupTime, cycletime);// 使用绝对时间休眠 —— 确定性的周期唤醒// TIMER_ABSTIME: 休眠到绝对时刻,避免累积误差clock_nanosleep(CLOCK_TO_USE, TIMER_ABSTIME, &wakeupTime, NULL);// ====== DC 关键调用 1: 写入应用时间 ======// 使用目标时间(非测量时间),因为更稳定ecrt_master_application_time(master, TIMESPEC2NS(wakeupTime));#ifdef MEASURE_TIMINGclock_gettime(CLOCK_TO_USE, &startTime);latency_ns = DIFF_NS(wakeupTime, startTime);    // 唤醒延迟period_ns = DIFF_NS(lastStartTime, startTime);   // 实际周期exec_ns = DIFF_NS(lastStartTime, endTime);       // 执行时间lastStartTime = startTime;// 更新 min/max 统计if (latency_ns > latency_max_ns) latency_max_ns = latency_ns;if (latency_ns < latency_min_ns) latency_min_ns = latency_ns;if (period_ns > period_max_ns)   period_max_ns = period_ns;if (period_ns < period_min_ns)   period_min_ns = period_ns;if (exec_ns > exec_max_ns)       exec_max_ns = exec_ns;if (exec_ns < exec_min_ns)       exec_min_ns = exec_ns;
#endif// 接收过程数据ecrt_master_receive(master);ecrt_domain_process(domain1);// 可选: 检查域状态check_domain1_state();// 1 Hz 统计输出if (counter) {counter--;} else {counter = FREQUENCY;check_master_state();
#ifdef MEASURE_TIMINGprintf("period     %10u ... %10u\n", period_min_ns, period_max_ns);printf("exec       %10u ... %10u\n", exec_min_ns, exec_max_ns);printf("latency    %10u ... %10u\n", latency_min_ns, latency_max_ns);// 重置统计period_max_ns = 0;    period_min_ns = 0xffffffff;exec_max_ns = 0;      exec_min_ns = 0xffffffff;latency_max_ns = 0;   latency_min_ns = 0xffffffff;
#endifblink = !blink;}// 写入过程数据EC_WRITE_U8(domain1_pd + off_dig_out, blink ? 0x66 : 0x99);EC_WRITE_U8(domain1_pd + off_counter_out, blink ? 0x00 : 0x02);// ====== DC 关键调用 2: 同步参考时钟 ======if (sync_ref_counter) {sync_ref_counter--;} else {sync_ref_counter = 1; // 每周期同步一次clock_gettime(CLOCK_TO_USE, &time);ecrt_master_sync_reference_clock_to(master, TIMESPEC2NS(time));}// ====== DC 关键调用 3: 同步所有从站时钟 ======ecrt_master_sync_slave_clocks(master);// 发送过程数据ecrt_domain_queue(domain1);ecrt_master_send(master);#ifdef MEASURE_TIMINGclock_gettime(CLOCK_TO_USE, &endTime);
#endif}
}

时序测量输出分析

启用 MEASURE_TIMING 后,程序每秒输出三项时序统计的最小/最大值:

指标 理想值 可接受范围 异常阈值 可能原因
period (周期) 1000000 ns 999000 ~ 1001000 ns 偏差 > 10 us 调度优先级不足、非 RT 内核、其他高优先级中断
exec (执行时间) 取决于负载 < 周期的 80% 接近或超过周期 PDO 过多、printf 输出阻塞、内存未锁定
latency (延迟) 0 ns < 50 us > 100 us 内核未开启 PREEMPT_RT、未使用 SCHED_FIFO、GC 或电源管理干扰

常见问题与解决方案

现象 可能原因 解决方案
周期不稳定,period 波动大 未使用实时调度策略 调用 sched_setscheduler() 设置 SCHED_FIFO;使用 PREEMPT_RT 或 Xenomai 内核
latency 偶尔出现尖峰 内存页面换出、中断干扰 调用 mlockall(MCL_CURRENT | MCL_FUTURE) 锁定内存;关闭 CPU 频率调节
从站 AL 状态无法进入 OP DC 参数不匹配(周期或激活字错误) 对照从站 ESI 文件检查 ecrt_slave_config_dc() 参数;确认 assign_activate
WC (Working Counter) 不等于 3 从站未响应或帧丢失 检查物理连接;确认从站地址和位置正确;检查网线质量
DC 同步报错:时钟偏差过大 参考时钟未正确同步 确认参考时钟从站存在且有 DC 能力;检查 ecrt_master_sync_reference_clock_to() 是否每周期调用
printf 输出影响实时性能 标准 I/O 引起阻塞 在生产代码中移除或异步化 printf 调用;使用无锁环形缓冲记录日志

深入源码

DC 同步时序: 循环内部序列

下图展示了 cyclic_task() 单个循环周期中各步骤的时间关系:

[时序图 - 参与者]

标识 名称
SLEEP clock_nanosleep
APP 应用程序
MASTER ec_master (内核)
REF 参考时钟从站
SLAVES 其他从站

ecrt_master_application_time() 内部机制

该函数将应用程序的当前时间通知给主站内核模块,用于 DC 参考时钟同步。

[流程图 - 结构化描述]

步骤 描述
1 用户空间 ecrt_master_application_time()
2 ioctl EC_IOCTL_APP_TIME
3 内核空间 ec_ioctl_app_time()
4 master->app_time = time64 master->app_time_set = 1
5 下一次 ecrt_master_send() 时 ref_sync_datagram 自动携带该时间
6 FPWR 0x0910
7 参考时钟从站 系统时间寄存器被更新

为什么用目标时间而非测量时间?

dc_user 示例使用 wakeupTime(目标唤醒时间)而非 startTime(实际启动时间)来调用 ecrt_master_application_time()。这是因为:

1. 稳定性: 目标时间是精确计算的,不受调度抖动影响
2. 单调性: 目标时间严格递增(每次加固定周期),而实际时间可能因调度延迟出现波动
3. 确定性: 参考时钟从站收到的应该是"应该到达的时间",而不是"实际到达的时间"

ecrt_master_sync_reference_clock_to() 内部机制

该函数将主站应用时间同步到 DC 参考时钟从站,使用 ARMW (Auto Increment Read Multiple Write) 命令:

// ecrt_master_sync_reference_clock_to() 对应的 ioctl 处理
static ATTRIBUTES int ec_ioctl_sync_ref(ec_master_t *master, void *arg, size_t size)
{uint64_t app_time = *((uint64_t *) arg);// 将应用时间写入 ref_sync_datagram 的数据区// ref_sync_datagram 类型: FPWR (Configured Address Physical Write)// 目标寄存器: 0x0910 (DC 系统时间, 64 位)EC_WRITE_U64(master->ref_sync_datagram.data, app_time);master->ref_sync_datagram.state = EC_DATAGRAM_INITIALIZED;return 0;
}

[流程图 - 结构化描述]

步骤 描述
1 ecrt_master_sync_reference_clock_to(master, time)
2 ioctl EC_IOCTL_SYNC_REF
3 EC_WRITE_U64 ref_sync_datagram.data 写入 64 位时间
4 下次 send() 时 ref_sync_datagram 发送
5 FPWR 0x0910
6 参考时钟从站 ESC_DCSYSTIM 被写入

ecrt_master_sync_slave_clocks() 内部机制

该函数执行全网从站时钟的漂移补偿。它通过 FRMW (Configured Address Read, Multiple Write) 广播命令实现:

[流程图 - 结构化描述]

步骤 描述
1 ecrt_master_sync_slave_clocks(master)
2 ioctl EC_IOCTL_SYNC_SLAVES
3 标记 sync64_datagram 需要发送
4 下次 send() 时 sync64_datagram 发送
5 ARMW/FRMW 0x0910
6 读取参考时钟 64 位系统时间
7 帧沿总线返回时 每个从站自动锁存 并写入本地 0x0910
8 各从站 DC 硬件 自动计算偏移并补偿
9 从站内部漂移补偿
10 读取 0x092C 系统时间差
11 更新 0x0920 系统时间偏移
12 调整本地时钟速度 (0x0930 速度计数器)

ARMW vs FRMW 的区别

ARMW (Auto Increment Read Multiple Write): 使用自增地址,读取参考从站数据并广播写入后续从站。适用于首次同步。

FRMW (Configured Address Read Multiple Write): 使用配置地址,读取指定从站数据并广播写入其他从站。适用于运行时周期性漂移补偿。

IgH 主站在运行时通常使用 FRMW,地址指向参考时钟从站的配置地址。

clock_nanosleep 与确定性周期

dc_user 使用 clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...) 实现确定性周期唤醒,其关键在于 TIMER_ABSTIME 标志:

特性 相对时间 (默认) 绝对时间 (TIMER_ABSTIME)
唤醒基准 从调用时刻起延迟指定时间 休眠到指定的绝对时刻
累积误差 每次循环会累积处理时间的误差 不会累积,始终对齐到精确周期
唤醒公式 now + period start + N * period
适用场景 非实时应用 实时周期任务(推荐)
// 初始化: 获取当前时间
clock_gettime(CLOCK_TO_USE, &wakeupTime);while (1) {// 计算下一个绝对唤醒时刻wakeupTime = timespec_add(wakeupTime, cycletime);// 休眠到绝对时刻 —— 无累积误差clock_nanosleep(CLOCK_TO_USE, TIMER_ABSTIME, &wakeupTime, NULL);// ... 周期任务 ...
}

深入了解

  • 分布式时钟 (DC) 原理 — DC 系统时间、SYNC 信号、漂移补偿的完整原理
  • 启动配置流程 — DC 配置在主站启动序列中的位置
  • Slave Config FSM — DC 配置状态机的实现细节
  • Xenomai 实时平台 — 使用 Xenomai 进一步降低延迟抖动
http://www.jsqmd.com/news/643797/

相关文章:

  • 分享孙盈舞蹈场地设施、音乐搭配、成人舞蹈教学,性价比高不高 - 工业设备
  • GLM-TTS快速体验:开箱即用,免费生成高质量语音
  • 有实力的长安跨越V5二手车购买平台推荐,新能源二手车值得购买吗分析 - 工业推荐榜
  • 如何用 port.start 开启共享子线程与主页面的长连接通道
  • 从吐槽到规则:Karpathy 如何给 AI 编程立规矩
  • 033.API服务优化:异步处理、批处理、GPU内存管理
  • MTools企业应用:客服中心智能语音质检系统
  • a 进制转 b 进制(2~16)
  • AIVideo新手入门:手把手教你配置镜像,快速开启AI视频创作
  • SAP MM DIEN类型服务料号的采购
  • 3分钟解密网易云音乐NCM文件:ncmdump技术解析与应用指南
  • C++ 继承与派生入门:从基础到实践
  • 有实力的预制直埋保温管厂家探讨,生产工艺成熟度深度剖析 - mypinpai
  • ncmdumpGUI完全指南:5分钟掌握网易云音乐NCM文件转换技巧
  • 番茄小说下载器终极指南:一键将在线小说转为EPUB电子书
  • 3分钟快速上手:智慧树自动刷课插件的完整使用指南
  • 财务主管警示:企业云支出浪费比例或高达30%
  • NBTExplorer:Minecraft玩家的终极NBT编辑器完全指南
  • 八股(四)JVM
  • Window Resizer完整指南:免费工具解决Windows窗口无法调整的难题
  • 2026年带压封堵施工公司哪家性价比高,这些品牌值得考虑 - myqiye
  • 3个实用技巧:如何用ncmdump轻松解密网易云音乐NCM文件
  • 5个高级配置技巧:如何深度优化NVIDIA Profile Inspector
  • 高效提升游戏性能:开源帧率优化工具完整指南
  • RAG系统必看!混合检索、关键词、语义一次讲清,生产级方案选型指南
  • ffmpeg的安装与配置
  • Joy-Con手柄修复指南:3个高效技巧彻底解决漂移和连接问题
  • 2026年3月优秀的波纹管源头厂家推荐,七孔梅花管/MPP电力管/PE硅芯管/雄安硅芯管,波纹管源头厂家推荐 - 品牌推荐师
  • 8分钟掌握网易云音乐NCM解密:免费工具让你的音乐随处播放
  • Qwen3-ASR-1.7B实操手册:如何导出SRT/VTT字幕文件用于Premiere剪辑