【IgH EtherCAT】 利用标准的 Linux 实时特性(需 PREEMPT_RT 内核补丁)来实现分布式时钟的同步功能

发布于:2025-08-14 ⋅ 阅读:(17) ⋅ 点赞:(0)

时序交互图说明:

该图展现了EtherCAT实时控制程序的完整执行流程,包括:

  1. 程序启动阶段

     - 内存锁定、主站请求、域创建

  2. 设备配置阶段

     - 配置总线耦合器、数字输出模块和计数器模块

  3. 循环执行阶段

     - 1000Hz的实时控制循环,包含数据接收、处理、发送和时钟同步

代码架构总结图说明:

该图全面总结了程序的关键特性:

  • 网络拓扑

    : 展示EtherCAT主站与从站设备的连接关系

  • 实时性能

    : 强调1ms周期、实时调度、精确定时等关键特征

  • 数据流处理

    : 说明输入输出数据的处理路径

  • 技术特点

    : 突出分布式时钟、PDO映射、热插拔等重要功能

这是一个典型的工业自动化EtherCAT实时控制系统,具有高精度定时、确定性响应和分布式同步等特点,适用于需要精确协调控制的多轴运动控制应用。

这是一个在用户空间运行的单线程实时 EtherCAT 示例,它通过标准的 Linux 实时特性(SCHED_FIFO 调度策略和高精度定时器)来实现周期性任务,并且集成了分布式时钟 (Distributed Clocks, DC) 的同步功能。

#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, &param) == -1) { // 将当前进程的调度策略设置为 SCHED_FIFO 并应用最高优先级        perror("sched_setscheduler failed"); // 如果失败,打印系统错误信息    }    printf("Starting cyclic function.\n"); // 打印提示信息    cyclic_task(); // 调用周期性任务函数,该函数将进入无限循环    return 0; // 理论上不会执行到这里}/****************************************************************************/

这是一个在用户空间运行的单线程实时 EtherCAT 示例,其核心是利用标准的 Linux 实时特性(需要 PREEMPT_RT 内核补丁以获得硬实时保证)来实现分布式时钟 (Distributed Clocks, DC) 的同步功能。

核心架构与功能:

  1. 单线程实时模型

  • 与之前的单线程示例类似,整个程序运行在 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 硬件信号,使其能够基于被同步过来的本地时钟,在精确的时刻触发事件(如数据采样或执行器动作)。

  • 实时性能测量

    • 延迟 (Latency)

      :实际唤醒时间与目标唤醒时间的差值,反映了调度器的抖动。

    • 周期 (Period)

      :两次连续唤醒之间的时间间隔,反映了周期的稳定性。

    • 执行时间 (Execution Time)

      :上一个周期从开始到结束所花费的时间。

    • 通过 #define MEASURE_TIMING 宏,代码中集成了一套详细的性能测量逻辑。

    • 它在每个周期内测量并记录三个关键指标:

    • 每秒钟,它会打印出这些指标在过去一秒内的最小值和最大值,为评估系统实时性能提供了宝贵的数据。

  • 程序终止:与之前的单线程示例一样,这个版本也没有实现优雅的退出机制(如信号处理)。它会一直运行,直到被外部强制杀死。

  • 总结:此示例是一个高级的用户空间实时应用。它不仅展示了如何利用标准的 Linux 实时特性来构建一个确定性的周期性任务,更重要的是,它详细演示了如何实现 EtherCAT 的分布式时钟同步,将 Linux 系统时间作为整个网络的统一时间基准。同时,其内置的性能测量功能使其成为一个非常有用的工具,用于评估和调试 PREEMPT_RT 内核下的实时系统性能。



网站公告

今日签到

点亮在社区的每一天
去签到