时序交互图说明:
该图展现了EtherCAT实时控制程序的完整执行流程,包括:
-
程序启动阶段
- 内存锁定、主站请求、域创建
-
设备配置阶段
- 配置总线耦合器、数字输出模块和计数器模块
-
循环执行阶段
- 1000Hz的实时控制循环,包含数据接收、处理、发送和时钟同步

代码架构总结图说明:
该图全面总结了程序的关键特性:
-
网络拓扑
: 展示EtherCAT主站与从站设备的连接关系
-
实时性能
: 强调1ms周期、实时调度、精确定时等关键特征
-
数据流处理
: 说明输入输出数据的处理路径
-
技术特点
: 突出分布式时钟、PDO映射、热插拔等重要功能
这是一个典型的工业自动化EtherCAT实时控制系统,具有高精度定时、确定性响应和分布式同步等特点,适用于需要精确协调控制的多轴运动控制应用。

这是一个在用户空间 运行的单线程 实时 EtherCAT 示例,它通过标准的 Linux 实时特性(SCHED_FIFO
调度策略和高精度定时器)来实现周期性任务,并且集成了分布式时钟 (Distributed Clocks, DC) 的同步功能。
cpp
#include <errno.h> // 包含C标准库头文件,用于错误码处理#include <signal.h> // 包含信号处理头文件#include <stdio.h> // 包含标准输入输出头文件#include <string.h> // 包含字符串处理头文件#include <sys/resource.h> // 包含系统资源操作头文件#include <sys/time.h> // 包含时间相关头文件#include <sys/types.h> // 包含基本系统数据类型头文件#include <unistd.h> // 包含 POSIX 标准 API 头文件#include <time.h> // 包含高精度时间相关头文件,如 clock_gettime#include <sys/mman.h> // 包含内存管理声明头文件,用于 mlockall#include <malloc.h> // 包含内存分配头文件#include <sched.h> /* sched_setscheduler() */ // 包含调度策略头文件,并注释其用途/****************************************************************************/ // 分隔注释#include "ecrt.h" // 包含 IgH EtherCAT 主站的核心应用层 API 头文件/****************************************************************************/ // 分隔注释// Application parameters // 区域注释:应用程序参数#define FREQUENCY 1000 // 宏定义:周期性任务的频率为 1000 Hz#define CLOCK_TO_USE CLOCK_MONOTONIC // 宏定义:选择使用的时钟类型为单调时钟 (不受系统时间修改影响)#define MEASURE_TIMING // 宏定义:启用时间测量代码块/****************************************************************************/ // 分隔注释#define NSEC_PER_SEC (1000000000L) // 宏定义:每秒的纳秒数 (L表示长整型)#define PERIOD_NS (NSEC_PER_SEC / FREQUENCY) // 宏定义:每个周期的纳秒数 (1ms)#define DIFF_NS(A, B) (((B).tv_sec - (A).tv_sec) * NSEC_PER_SEC + \ // 宏定义:计算两个 timespec 结构体之间的时间差(纳秒) (B).tv_nsec - (A).tv_nsec)#define TIMESPEC2NS(T) ((uint64_t) (T).tv_sec * NSEC_PER_SEC + (T).tv_nsec) // 宏定义:将 timespec 结构体转换为64位纳秒时间戳/****************************************************************************/ // 分隔注释// EtherCAT // 区域注释:EtherCAT 相关全局变量static ec_master_t *master = NULL; // 声明一个静态的 EtherCAT 主站对象指针static ec_master_state_t master_state = {}; // 声明一个静态的主站状态结构体变量static ec_domain_t *domain1 = NULL; // 声明一个静态的 EtherCAT 过程数据域指针static ec_domain_state_t domain1_state = {}; // 声明一个静态的域状态结构体变量/****************************************************************************/ // 分隔注释// process data // 区域注释:过程数据static uint8_t *domain1_pd = NULL; // 声明一个静态的指向过程数据内存区域的指针#define BusCouplerPos 0, 0 // 宏定义:总线耦合器的位置#define DigOutSlavePos 0, 1 // 宏定义:数字量输出从站的位置#define CounterSlavePos 0, 2 // 宏定义:计数器从站的位置#define Beckhoff_EK1100 0x00000002, 0x044c2c52 // 宏定义:倍福 EK1100 的厂商/产品ID#define Beckhoff_EL2008 0x00000002, 0x07d83052 // 宏定义:倍福 EL2008 的厂商/产品ID#define IDS_Counter 0x000012ad, 0x05de3052 // 宏定义:IDS 计数器从站的厂商/产品ID// offsets for PDO entries // 注释:PDO条目的偏移量static int off_dig_out; // 静态整型,存储数字量输出的偏移量static int off_counter_in; // 静态整型,存储计数器输入值的偏移量static int off_counter_out; // 静态整型,存储计数器输出值的偏移量static unsigned int counter = 0; // 静态无符号整型,用作通用计数器static unsigned int blink = 0; // 静态无符号整型,用作闪烁标志static unsigned int sync_ref_counter = 0; // 静态无符号整型,用作参考时钟同步的计数器const struct timespec cycletime = {0, PERIOD_NS}; // 静态常量 timespec 结构体,表示一个周期的时长/****************************************************************************/ // 分隔注释struct timespec timespec_add(struct timespec time1, struct timespec time2) // 定义一个函数,用于将两个 timespec 结构体相加{ struct timespec result; // 声明一个结果结构体 if ((time1.tv_nsec + time2.tv_nsec) >= NSEC_PER_SEC) { // 如果纳秒部分相加后超过1秒 result.tv_sec = time1.tv_sec + time2.tv_sec + 1; // 秒部分相加并进位 result.tv_nsec = time1.tv_nsec + time2.tv_nsec - NSEC_PER_SEC; // 纳秒部分减去1秒 } else { // 如果没有超过1秒 result.tv_sec = time1.tv_sec + time2.tv_sec; // 秒部分直接相加 result.tv_nsec = time1.tv_nsec + time2.tv_nsec; // 纳秒部分直接相加 } return result; // 返回计算结果}/****************************************************************************/ // 分隔注释void check_domain1_state(void) // 定义一个函数,用于检查并打印域的状态变化{ ec_domain_state_t ds; // 声明一个局部的域状态结构体变量 ecrt_domain_state(domain1, &ds); // 调用 ecrt API,获取 domain1 的当前状态 if (ds.working_counter != domain1_state.working_counter) // 比较当前工作计数器(WC)与上次记录的WC printf("Domain1: WC %u.\n", ds.working_counter); // 如果不一致,打印新的WC值 if (ds.wc_state != domain1_state.wc_state) // 比较当前工作计数器状态与上次记录的状态 printf("Domain1: State %u.\n", ds.wc_state); // 如果不一致,打印新的WC状态 domain1_state = ds; // 将当前状态赋值给全局变量,用于下次比较}/****************************************************************************/ // 分隔注释void check_master_state(void) // 定义一个函数,用于检查并打印主站的状态变化{ ec_master_state_t ms; // 声明一个局部的主站状态结构体变量 ecrt_master_state(master, &ms); // 调用 ecrt API,获取主站的当前状态 if (ms.slaves_responding != master_state.slaves_responding) // 比较当前响应的从站数量 printf("%u slave(s).\n", ms.slaves_responding); // 如果不一致,打印新的从站数量 if (ms.al_states != master_state.al_states) // 比较当前应用层(AL)状态 printf("AL states: 0x%02X.\n", ms.al_states); // 如果不一致,以十六进制格式打印新的AL状态 if (ms.link_up != master_state.link_up) // 比较当前链路连接状态 printf("Link is %s.\n", ms.link_up ? "up" : "down"); // 如果不一致,打印链路状态 master_state = ms; // 将当前状态赋值给全局变量,用于下次比较}/****************************************************************************/ // 分隔注释void cyclic_task() // 定义周期性任务函数,此函数将在主循环中被反复调用{ struct timespec wakeupTime, time; // 声明时间结构体变量#ifdef MEASURE_TIMING // 如果启用了时间测量 struct 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 // 结束 #ifdef // get current time // 注释:获取当前时间 clock_gettime(CLOCK_TO_USE, &wakeupTime); // 获取当前单调时钟的时间,作为首次唤醒时间的基准 while(1) { // 进入一个无限循环 wakeupTime = timespec_add(wakeupTime, cycletime); // 计算下一个周期的绝对唤醒时间 clock_nanosleep(CLOCK_TO_USE, TIMER_ABSTIME, &wakeupTime, NULL); // 精确睡眠直到指定的绝对时间 // Write application time to master // 注释:将应用时间写入主站 // // It is a good idea to use the target time (not the measured time) as // 注释:最好使用目标时间(而不是测量到的时间)作为应用时间 // application time, because it is more stable. // 因为它更稳定 // ecrt_master_application_time(master, TIMESPEC2NS(wakeupTime)); // 将目标唤醒时间戳告知 EtherCAT 主站,用于 DC 同步#ifdef MEASURE_TIMING // 如果启用了时间测量 clock_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; // 保存本次开始时间,用于下次计算 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 // 结束 #ifdef // receive process data // 注释:接收过程数据 ecrt_master_receive(master); // 从网络接口接收数据帧 ecrt_domain_process(domain1); // 处理域的数据 // check process data state (optional) // 注释:检查过程数据状态(可选) check_domain1_state(); // 调用函数检查并打印域的状态变化 if (counter) { // 如果计数器不为0 counter--; // 计数器减1 } else { // do this at 1 Hz // 否则(每秒执行一次) counter = FREQUENCY; // 重置计数器为频率值 (1000) // check for master state (optional) // 注释:检查主站状态(可选) check_master_state(); // 调用函数检查并打印主站的状态变化#ifdef MEASURE_TIMING // 如果启用了时间测量 // output timing stats // 注释:输出计时统计 printf("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; // 重置最小延迟为最大值#endif // 结束 #ifdef // calculate new process data // 注释:计算新的过程数据 blink = !blink; // 对 blink 变量取反,实现闪烁逻辑 } // write process data // 注释:写入过程数据 EC_WRITE_U8(domain1_pd + off_dig_out, blink ? 0x66 : 0x99); // 写入闪烁模式 EC_WRITE_U8(domain1_pd + off_counter_out, blink ? 0x00 : 0x02); // 向计数器从站写入输出数据 if (sync_ref_counter) { // 如果参考时钟同步计数器不为0 sync_ref_counter--; // 计数器减1 } else { // 否则 sync_ref_counter = 1; // sync every cycle // 重置计数器为1(表示每个周期都同步) clock_gettime(CLOCK_TO_USE, &time); // 获取当前时间 ecrt_master_sync_reference_clock_to(master, TIMESPEC2NS(time)); // 将主站时钟同步到参考从站 } ecrt_master_sync_slave_clocks(master); // 命令所有从站与参考从站同步 // send process data // 注释:发送过程数据 ecrt_domain_queue(domain1); // 将过程数据放入发送队列 ecrt_master_send(master); // 发送 EtherCAT 帧#ifdef MEASURE_TIMING // 如果启用了时间测量 clock_gettime(CLOCK_TO_USE, &endTime); // 记录周期结束时间#endif // 结束 #ifdef }}/****************************************************************************/ // 分隔注释int main(int argc, char **argv) // C程序的入口主函数{ ec_slave_config_t *sc; // 声明一个从站配置对象指针 if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) { // 锁定当前和未来所有分配的内存 perror("mlockall failed"); // 如果失败,打印系统错误信息 return -1; // 并退出 } master = ecrt_request_master(0); // 请求主站实例 if (!master) // 检查是否成功 return -1; // 失败则退出 domain1 = ecrt_master_create_domain(master); // 创建过程数据域 if (!domain1) // 检查是否成功 return -1; // 失败则退出 // Create configuration for bus coupler // 注释:为总线耦合器创建配置 sc = ecrt_master_slave_config(master, BusCouplerPos, Beckhoff_EK1100); // 为 EK1100 创建配置 if (!sc) // 检查是否成功 return -1; // 失败则退出 if (!(sc = ecrt_master_slave_config(master, // 为 EL2008 创建配置 DigOutSlavePos, Beckhoff_EL2008))) { fprintf(stderr, "Failed to get slave configuration.\n"); // 打印错误 return -1; // 失败则退出 } // 注册 EL2008 的第一个输出通道的 PDO 条目 off_dig_out = ecrt_slave_config_reg_pdo_entry(sc, 0x7000, 1, domain1, NULL); if (off_dig_out < 0) // 如果失败 return -1; // 则退出 if (!(sc = ecrt_master_slave_config(master, // 为计数器从站创建配置 CounterSlavePos, IDS_Counter))) { fprintf(stderr, "Failed to get slave configuration.\n"); // 打印错误 return -1; // 失败则退出 } // 注册计数器输入 PDO 条目 off_counter_in = ecrt_slave_config_reg_pdo_entry(sc, 0x6020, 0x11, domain1, NULL); if (off_counter_in < 0) // 如果失败 return -1; // 则退出 // 注册计数器输出 PDO 条目 off_counter_out = ecrt_slave_config_reg_pdo_entry(sc, 0x7020, 1, domain1, NULL); if (off_counter_out < 0) // 如果失败 return -1; // 则退出 // configure SYNC signals for this slave // 注释:为该从站配置 SYNC 信号 ecrt_slave_config_dc(sc, 0x0700, PERIOD_NS, 4400000, 0, 0); // 配置DC参数:SYNC0/1, 周期, 偏移等 printf("Activating master...\n"); // 打印提示信息 if (ecrt_master_activate(master)) // 激活主站 return -1; // 失败则退出 if (!(domain1_pd = ecrt_domain_data(domain1))) { // 获取过程数据内存指针 return -1; // 失败则退出 } /* Set priority */ // 注释:设置优先级 struct sched_param param = {}; // 声明一个 sched_param 结构体 param.sched_priority = sched_get_priority_max(SCHED_FIFO); // 获取 SCHED_FIFO 策略下的最高可用优先级 printf("Using priority %i.\n", param.sched_priority); // 打印正在使用的优先级 if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) { // 将当前进程的调度策略设置为 SCHED_FIFO 并应用最高优先级 perror("sched_setscheduler failed"); // 如果失败,打印系统错误信息 } printf("Starting cyclic function.\n"); // 打印提示信息 cyclic_task(); // 调用周期性任务函数,该函数将进入无限循环 return 0; // 理论上不会执行到这里}/****************************************************************************/
这是一个在用户空间 运行的单线程 实时 EtherCAT 示例,其核心是利用标准的 Linux 实时特性(需要 PREEMPT_RT
内核补丁以获得硬实时保证)来实现分布式时钟 (Distributed Clocks, DC) 的同步功能。
核心架构与功能:
- 单线程实时模型:
-
与之前的单线程示例类似,整个程序运行在
main
函数所在的单个线程中。 -
通过
sched_setscheduler()
将进程的调度策略提升为SCHED_FIFO
并赋予最高优先级,使其成为一个实时进程。 -
通过
mlockall()
锁定内存,防止因页面交换产生的延迟。 -
使用
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...)
在一个while(1)
循环中实现精确的、无累积误差的周期性调度。
-
分布式时钟 (DC) - 主站时钟同步到从站 (
Master-to-Reference
模式): -
-
应用时间戳
:在每个实时周期的开始,程序将目标唤醒时间
wakeupTime
通过ecrt_master_application_time()
告知 EtherCAT 主站核心。使用目标时间而非实际唤醒时间可以提供一个更稳定、无抖动的时间基准。 -
参考时钟同步
:在
cyclic_task
循环中,程序以高频率(在此示例中是每个周期)调用ecrt_master_sync_reference_clock_to()
。此函数将主站的当前本地时钟(clock_gettime()
)写入 EtherCAT 帧,发送给参考从站(隐式选择的第一个支持DC的从站,通常是 EK1100),命令参考从站将自己的时钟设置为这个值。 -
从站间同步
:紧接着调用
ecrt_master_sync_slave_clocks()
,命令网络中所有其他的 DC 从站都与参考从站的时钟对齐。 -
模式
:此示例采用了**"主站作为时间源"**的同步模式。Linux 系统的高精度单调时钟是整个系统的基准,它被周期性地"注入"到 EtherCAT 网络中,同步所有从站的时钟。
-
-
DC 同步信号配置 (
ecrt_slave_config_dc
): -
- 在初始化阶段,对一个特定的从站(IDS Counter)调用了
ecrt_slave_config_dc()
。这用于配置该从站的 SYNC0/1 硬件信号,使其能够基于被同步过来的本地时钟,在精确的时刻触发事件(如数据采样或执行器动作)。
- 在初始化阶段,对一个特定的从站(IDS Counter)调用了
-
实时性能测量:
-
-
延迟 (Latency)
:实际唤醒时间与目标唤醒时间的差值,反映了调度器的抖动。
-
周期 (Period)
:两次连续唤醒之间的时间间隔,反映了周期的稳定性。
-
执行时间 (Execution Time)
:上一个周期从开始到结束所花费的时间。
-
通过
#define MEASURE_TIMING
宏,代码中集成了一套详细的性能测量逻辑。 -
它在每个周期内测量并记录三个关键指标:
-
每秒钟,它会打印出这些指标在过去一秒内的最小值和最大值,为评估系统实时性能提供了宝贵的数据。
-
-
程序终止:与之前的单线程示例一样,这个版本也没有实现优雅的退出机制(如信号处理)。它会一直运行,直到被外部强制杀死。
-
总结 :此示例是一个高级的用户空间实时应用。它不仅展示了如何利用标准的 Linux 实时特性来构建一个确定性的周期性任务,更重要的是,它详细演示了如何实现 EtherCAT 的分布式时钟同步,将 Linux 系统时间作为整个网络的统一时间基准。同时,其内置的性能测量功能使其成为一个非常有用的工具,用于评估和调试
PREEMPT_RT
内核下的实时系统性能。