【IgH EtherCAT】 一个基于RTAI实时系统的EtherCAT分布式时钟示例程序,实现了高精度的主从站时钟同步

主要功能:

这是一个基于RTAI实时系统的EtherCAT分布式时钟示例程序,实现了高精度的主从站时钟同步。

核心组件:

  1. RTAI实时层

    : 提供1000Hz的周期性实时任务,确保确定性的时序控制

  2. EtherCAT主站

    : 管理整个EtherCAT网络,处理从站配置和数据交换

  3. 从站设备

    : 包括EK1100总线耦合器、EL2008数字输出模块和计数器模块

关键特性:

  • 分布式时钟同步

    : 通过SYNC信号实现纳秒级时钟同步

  • 实时性保证

    : 使用信号量和中断抑制机制保证实时性

  • 过程数据映射

    : 通过PDO实现高效的数据交换

  • 状态监控

    : 实时监控主站和域的工作状态

工作流程:

程序以1000Hz频率运行实时任务,每个周期进行数据接收、处理、同步时钟和数据发送,同时每10个周期进行一次参考时钟同步,确保整个网络的时间一致性。

时序图展现了完整的初始化到运行再到清理的过程,架构图则详细说明了各组件的功能和数据流向。

这段使用了 RTAI 并集成了分布式时钟 (Distributed Clocks, DC) 功能的内核模块示例代码。这是之前 RTAI 示例的进阶版,重点在于如何利用 DC 来实现高精度的同步操作。

cpp 复制代码
// Linux // 区域注释:Linux 内核头文件#include <linux/module.h> // 包含 Linux 内核模块编程所需的核心头文件#include <linux/err.h> // 包含内核错误处理相关的头文件// RTAI // 区域注释:RTAI 头文件#include <rtai_sched.h> // 包含 RTAI 调度器相关的头文件#include <rtai_sem.h> // 包含 RTAI 信号量相关的头文件// EtherCAT // 区域注释:EtherCAT 头文件#include "../../include/ecrt.h" // 包含 IgH EtherCAT 主站的实时接口头文件/****************************************************************************/ // 分隔注释// Module parameters // 区域注释:模块参数#define FREQUENCY 1000 // task frequency in Hz // 宏定义:任务频率为 1000 Hz#define INHIBIT_TIME 20 // 宏定义:抑制时间为 20 (单位可能是微秒,用于回调函数)#define TIMERTICKS (1000000000 / FREQUENCY) // 宏定义:每个周期的纳秒数 (1ms)#define NUM_DIG_OUT 1 // 宏定义:使用的数字量输出从站的数量#define PFX "ec_dc_rtai_sample: " // 宏定义:内核日志消息的前缀/****************************************************************************/ // 分隔注释// 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 = {}; // 声明一个静态的域状态结构体变量// RTAI // 区域注释:RTAI 相关全局变量static RT_TASK task; // 声明一个 RTAI 任务结构体变量static SEM master_sem; // 声明一个 RTAI 信号量结构体变量static cycles_t t_last_cycle = 0, t_critical; // 声明 RTAI 的时间戳变量 (CPU周期数)/****************************************************************************/ // 分隔注释// process data // 区域注释:过程数据static uint8_t *domain1_pd; // process data memory // 声明一个指向过程数据内存区域的指针#define DigOutSlavePos(X) 0, (1 + (X)) // 宏定义:计算数字量输出从站的位置,X从0开始#define CounterSlavePos   0, 2 // 宏定义:计数器从站的位置#define Beckhoff_EK1100 0x00000002, 0x044c2c52 // 宏定义:倍福 EK1100 的厂商/产品ID#define Beckhoff_EL2008 0x00000002, 0x07d83052 // 宏定义:倍福 EL2008 的厂商/产品ID#define IDS_Counter     0x000012ad, 0x05de3052 // 宏定义:IDS 计数器从站的厂商/产品IDstatic int off_dig_out[NUM_DIG_OUT]; // 静态整型数组,存储数字量输出的偏移量static int off_counter_in; // 静态整型,存储计数器输入值的偏移量static int off_counter_out; // 静态整型,存储计数器输出值的偏移量static unsigned int counter = 0; // 静态无符号整型,用作通用计数器static unsigned int blink_counter = 0; // 静态无符号整型,用作闪烁逻辑的计数器static unsigned int blink = 0; // 静态无符号整型,用作闪烁标志static u32 counter_value = 0U; // 静态32位无符号整型,存储从计数器从站读取的值/****************************************************************************/ // 分隔注释// 以下是为 EL2008 从站进行 SII (从站信息接口) 覆盖配置所需的数据结构static ec_pdo_entry_info_t el2008_channels[] = { // 定义 EL2008 的 PDO 条目信息    {0x7000, 1, 1}, // 通道1    {0x7010, 1, 1}, // 通道2    {0x7020, 1, 1}, // 通道3    {0x7030, 1, 1}, // 通道4    {0x7040, 1, 1}, // 通道5    {0x7050, 1, 1}, // 通道6    {0x7060, 1, 1}, // 通道7    {0x7070, 1, 1}  // 通道8};static ec_pdo_info_t el2008_pdos[] = { // 定义 EL2008 的 PDO 信息 (将条目分组到PDO)    {0x1600, 1, &el2008_channels[0]}, // RxPDO for Channel 1    {0x1601, 1, &el2008_channels[1]}, // RxPDO for Channel 2    {0x1602, 1, &el2008_channels[2]}, // RxPDO for Channel 3    {0x1603, 1, &el2008_channels[3]}, // RxPDO for Channel 4    {0x1604, 1, &el2008_channels[4]}, // RxPDO for Channel 5    {0x1605, 1, &el2008_channels[5]}, // RxPDO for Channel 6    {0x1606, 1, &el2008_channels[6]}, // RxPDO for Channel 7    {0x1607, 1, &el2008_channels[7]}  // RxPDO for Channel 8};static ec_sync_info_t el2008_syncs[] = { // 定义 EL2008 的同步管理器配置    {0, EC_DIR_OUTPUT, 8, el2008_pdos}, // Sync Manager 0: 输出类型, 关联8个PDO    {1, EC_DIR_INPUT}, // Sync Manager 1: 空的输入SM    {0xff} // 结束标志};/****************************************************************************/ // 分隔注释void check_domain1_state(void) // 定义一个函数,用于检查并打印域的状态变化{    ec_domain_state_t ds; // 声明一个局部的域状态结构体变量    rt_sem_wait(&master_sem); // 等待(获取)RTAI信号量    ecrt_domain_state(domain1, &ds); // 调用 ecrt API,获取 domain1 的当前状态    rt_sem_signal(&master_sem); // 发送(释放)RTAI信号量    if (ds.working_counter != domain1_state.working_counter) // 比较当前工作计数器(WC)与上次记录的WC        printk(KERN_INFO PFX "Domain1: WC %u.\n", ds.working_counter); // 如果不一致,打印新的WC值    if (ds.wc_state != domain1_state.wc_state) // 比较当前工作计数器状态与上次记录的状态        printk(KERN_INFO PFX "Domain1: State %u.\n", ds.wc_state); // 如果不一致,打印新的WC状态    domain1_state = ds; // 将当前状态赋值给全局变量,用于下次比较}/****************************************************************************/ // 分隔注释void check_master_state(void) // 定义一个函数,用于检查并打印主站的状态变化{    ec_master_state_t ms; // 声明一个局部的主站状态结构体变量    rt_sem_wait(&master_sem); // 等待 RTAI 信号量    ecrt_master_state(master, &ms); // 调用 ecrt API,获取主站的当前状态    rt_sem_signal(&master_sem); // 释放 RTAI 信号量    if (ms.slaves_responding != master_state.slaves_responding) // 比较当前响应的从站数量与上次记录的数量        printk(KERN_INFO PFX "%u slave(s).\n", ms.slaves_responding); // 如果不一致,打印新的从站数量    if (ms.al_states != master_state.al_states) // 比较当前应用层(AL)状态与上次记录的状态        printk(KERN_INFO PFX "AL states: 0x%02X.\n", ms.al_states); // 如果不一致,以十六进制格式打印新的AL状态    if (ms.link_up != master_state.link_up) // 比较当前链路连接状态与上次记录的状态        printk(KERN_INFO PFX "Link is %s.\n", ms.link_up ? "up" : "down"); // 如果不一致,打印链路是 "up" 还是 "down"    master_state = ms; // 将当前状态赋值给全局变量,用于下次比较}/****************************************************************************/ // 分隔注释void run(long data) // RTAI 实时任务的主体函数{    int i; // 声明循环变量    struct timeval tv; // 声明一个 timeval 结构体,用于时间转换    unsigned int sync_ref_counter = 0; // 声明一个计数器,用于控制参考时钟的同步频率    while (1) { // 进入一个无限循环        t_last_cycle = get_cycles(); // 使用 RTAI 函数获取当前 CPU 周期数,记录周期开始时间        // 将 RTAI 的实时时间转换为 timeval 结构,再转换为纳秒,并设置为主站的应用时间        count2timeval(nano2count(rt_get_real_time_ns()), &tv); // RTAI时间 -> timeval        ecrt_master_application_time(master, EC_TIMEVAL2NANO(tv)); // 设置主站应用时间,用于DC同步        // receive process data // 注释:接收过程数据        rt_sem_wait(&master_sem); // 获取信号量        ecrt_master_receive(master); // 从网络接口接收数据帧        ecrt_domain_process(domain1); // 处理域的数据        rt_sem_signal(&master_sem); // 释放信号量        // check process data state (optional) // 注释:检查过程数据状态(可选)        check_domain1_state(); // 调用函数检查并打印域的状态变化        if (counter) { // 如果计数器不为0            counter--; // 计数器减1        } else { // 否则(每秒执行一次)            u32 c; // 声明一个32位无符号整型            counter = FREQUENCY; // 重置计数器为频率值 (1000)            // check for master state (optional) // 注释:检查主站状态(可选)            check_master_state(); // 调用函数检查并打印主站的状态变化            c = EC_READ_U32(domain1_pd + off_counter_in); // 从过程数据中读取32位计数器输入值            if (counter_value != c) { // 如果值发生变化                counter_value = c; // 更新本地存储的值                printk(KERN_INFO PFX "counter=%u\n", counter_value); // 打印新的计数值            }        }        if (blink_counter) { // 如果闪烁计数器不为0            blink_counter--; // 计数器减1        } else { // 否则(每10个周期执行一次)            blink_counter = 9; // 重置计数器            // calculate new process data // 注释:计算新的过程数据            blink = !blink; // 对 blink 变量取反,实现闪烁逻辑        }        // write process data // 注释:写入过程数据        for (i = 0; i < NUM_DIG_OUT; i++) { // 遍历所有数字量输出从站            EC_WRITE_U8(domain1_pd + off_dig_out[i], blink ? 0x66 : 0x99); // 写入闪烁模式 (0b01100110 或 0b10011001)        }        EC_WRITE_U8(domain1_pd + off_counter_out, blink ? 0x00 : 0x02); // 向计数器从站写入输出数据        rt_sem_wait(&master_sem); // 获取信号量        if (sync_ref_counter) { // 如果参考时钟同步计数器不为0            sync_ref_counter--; // 计数器减1        } else { // 否则(每10个周期执行一次)            sync_ref_counter = 9; // 重置计数器            count2timeval(nano2count(rt_get_real_time_ns()), &tv); // 获取当前 RTAI 实时时间            // 将主站时钟(RTAI时间)同步到参考从站            ecrt_master_sync_reference_clock_to(master, EC_TIMEVAL2NANO(tv));        }        ecrt_master_sync_slave_clocks(master); // 命令所有从站与参考从站同步        ecrt_domain_queue(domain1); // 将过程数据放入发送队列        ecrt_master_send(master); // 发送 EtherCAT 帧        rt_sem_signal(&master_sem); // 释放信号量        rt_task_wait_period(); // RTAI 函数:使当前任务睡眠,直到下一个周期性调度点    }}/****************************************************************************/ // 分隔注释void send_callback(void *cb_data) // 定义一个发送回调函数{    ec_master_t *m = (ec_master_t *) cb_data; // 将 void* 指针转换为 master 指针    // too close to the next real time cycle: deny access... // 注释:离下一个实时周期太近:拒绝访问...    if (get_cycles() - t_last_cycle <= t_critical) { // 如果当前时间与上个周期开始时间的差值小于临界值        rt_sem_wait(&master_sem); // 获取信号量        ecrt_master_send_ext(m); // 调用扩展的发送函数        rt_sem_signal(&master_sem); // 释放信号量    }}/****************************************************************************/ // 分隔注释void receive_callback(void *cb_data) // 定义一个接收回调函数{    ec_master_t *m = (ec_master_t *) cb_data; // 将 void* 指针转换为 master 指针    // too close to the next real time cycle: deny access... // 注释:离下一个实时周期太近:拒绝访问...    if (get_cycles() - t_last_cycle <= t_critical) { // 如果当前时间与上个周期开始时间的差值小于临界值        rt_sem_wait(&master_sem); // 获取信号量        ecrt_master_receive(m); // 调用接收函数        rt_sem_signal(&master_sem); // 释放信号量    }}/****************************************************************************/ // 分隔注释int __init init_mod(void) // 内核模块的初始化函数{    int ret = -1, i; // 声明返回值和循环变量    RTIME tick_period, requested_ticks, now; // 声明 RTAI 的时间变量    ec_slave_config_t *sc; // 声明一个从站配置对象指针    printk(KERN_INFO PFX "Starting...\n"); // 在内核日志中打印启动信息    rt_sem_init(&master_sem, 1); // 初始化 RTAI 信号量    // 计算一个临界时间值,用于回调函数    t_critical = cpu_khz * 1000 / FREQUENCY - cpu_khz * INHIBIT_TIME / 1000;    master = ecrt_request_master(0); // 请求主站实例    if (!master) { // 检查是否成功        ret = -EBUSY; // 设置返回值为设备忙        printk(KERN_ERR PFX "Requesting master 0 failed!\n"); // 打印错误        goto out_return; // 跳转到返回处    }    ecrt_master_callbacks(master, send_callback, receive_callback, master); // 注册回调函数    printk(KERN_INFO PFX "Registering domain...\n"); // 打印信息    if (!(domain1 = ecrt_master_create_domain(master))) { // 创建过程数据域        printk(KERN_ERR PFX "Domain creation failed!\n"); // 打印错误        goto out_release_master; // 跳转到释放主站处    }    printk(KERN_INFO PFX "Configuring PDOs...\n"); // 打印信息    // create configuration for reference clock FIXME // 注释:为参考时钟创建配置 (FIXME 表示这里可能需要修改)    if (!(sc = ecrt_master_slave_config(master, 0, 0, Beckhoff_EK1100))) { // 为 EK1100 创建配置 (隐式作为参考时钟)        printk(KERN_ERR PFX "Failed to get slave configuration.\n"); // 打印错误        goto out_release_master; // 跳转到释放主站处    }    for (i = 0; i < NUM_DIG_OUT; i++) { // 遍历所有数字量输出从站        if (!(sc = ecrt_master_slave_config(master, // 为 EL2008 创建配置                        DigOutSlavePos(i), Beckhoff_EL2008))) {            printk(KERN_ERR PFX "Failed to get slave configuration.\n"); // 打印错误            goto out_release_master; // 跳转到释放主站处        }        if (ecrt_slave_config_pdos(sc, EC_END, el2008_syncs)) { // 为 EL2008 配置 PDO            printk(KERN_ERR PFX "Failed to configure PDOs.\n"); // 打印错误            goto out_release_master; // 跳转到释放主站处        }        // 注册 EL2008 的第一个输出通道的 PDO 条目到域中        off_dig_out[i] = ecrt_slave_config_reg_pdo_entry(sc,                0x7000, 1, domain1, NULL);        if (off_dig_out[i] < 0) // 如果注册失败            goto out_release_master; // 跳转到释放主站处    }    // 为计数器从站创建配置    if (!(sc = ecrt_master_slave_config(master,                    CounterSlavePos, IDS_Counter))) {        printk(KERN_ERR PFX "Failed to get slave configuration.\n"); // 打印错误        goto out_release_master; // 跳转到释放主站处    }    // 注册计数器输入 PDO 条目    off_counter_in = ecrt_slave_config_reg_pdo_entry(sc,            0x6020, 0x11, domain1, NULL);    if (off_counter_in < 0) // 如果失败        goto out_release_master; // 跳转    // 注册计数器输出 PDO 条目    off_counter_out = ecrt_slave_config_reg_pdo_entry(sc,            0x7020, 1, domain1, NULL);    if (off_counter_out < 0) // 如果失败        goto out_release_master; // 跳转    // configure SYNC signals for this slave // 注释:为该从站配置 SYNC 信号    ecrt_slave_config_dc(sc, 0x0700, 1000000, 440000, 0, 0); // 配置DC参数:SYNC0/1, 周期, 偏移等    printk(KERN_INFO PFX "Activating master...\n"); // 打印信息    if (ecrt_master_activate(master)) { // 激活主站        printk(KERN_ERR PFX "Failed to activate master!\n"); // 打印错误        goto out_release_master; // 跳转    }    // Get internal process data for domain // 注释:获取域的内部过程数据    domain1_pd = ecrt_domain_data(domain1); // 获取过程数据内存指针    printk(KERN_INFO PFX "Starting cyclic sample thread...\n"); // 打印信息    requested_ticks = nano2count(TIMERTICKS); // 将周期纳秒数转换为 RTAI 时钟节拍数    tick_period = start_rt_timer(requested_ticks); // 启动 RTAI 实时定时器    printk(KERN_INFO PFX "RT timer started with %i/%i ticks.\n", // 打印定时器信息           (int) tick_period, (int) requested_ticks);    if (rt_task_init(&task, run, 0, 2000, 0, 1, NULL)) { // 初始化 RTAI 任务        printk(KERN_ERR PFX "Failed to init RTAI task!\n"); // 打印错误        goto out_stop_timer; // 跳转    }    now = rt_get_time(); // 获取当前 RTAI 时间    if (rt_task_make_periodic(&task, now + tick_period, tick_period)) { // 将任务设置为周期性        printk(KERN_ERR PFX "Failed to run RTAI task!\n"); // 打印错误        goto out_stop_task; // 跳转    }    printk(KERN_INFO PFX "Initialized.\n"); // 打印初始化成功信息    return 0; // 返回成功 out_stop_task: // 清理标签:删除任务    rt_task_delete(&task); // 删除 RTAI 任务 out_stop_timer: // 清理标签:停止定时器    stop_rt_timer(); // 停止 RTAI 实时定时器 out_release_master: // 清理标签:释放主站    printk(KERN_ERR PFX "Releasing master...\n"); // 打印信息    ecrt_release_master(master); // 释放主站资源 out_return: // 清理标签:返回    rt_sem_delete(&master_sem); // 删除 RTAI 信号量    printk(KERN_ERR PFX "Failed to load. Aborting.\n"); // 打印加载失败信息    return ret; // 返回错误码}/****************************************************************************/ // 分隔注释void __exit cleanup_mod(void) // 内核模块的退出函数{    printk(KERN_INFO PFX "Stopping...\n"); // 打印停止信息    rt_task_delete(&task); // 删除 RTAI 任务    stop_rt_timer(); // 停止 RTAI 实时定时器    ecrt_release_master(master); // 释放主站资源    rt_sem_delete(&master_sem); // 删除 RTAI 信号量    printk(KERN_INFO PFX "Unloading.\n"); // 打印卸载信息}/****************************************************************************/ // 分隔注释MODULE_LICENSE("GPL"); // 宏:声明模块的许可证为 GPLMODULE_AUTHOR("Florian Pose <fp@igh.de>"); // 宏:声明模块的作者MODULE_DESCRIPTION("EtherCAT distributed clocks sample module"); // 宏:声明模块的描述module_init(init_mod); // 宏:将 init_mod 函数注册为模块的初始化函数module_exit(cleanup_mod); // 宏:将 cleanup_mod 函数注册为模块的退出函数/****************************************************************************/

这是一个在 Linux 内核空间 运行的、基于 RTAI硬实时 EtherCAT 主站示例,其核心功能是演示分布式时钟 (Distributed Clocks, DC) 的应用。与之前不带 DC 的 RTAI 示例相比,它增加了与时间同步相关的关键操作。

核心架构与功能:

  1. 内核模块与 RTAI 硬实时
  • 这是一个标准的 Linux 内核模块,通过 insmodrmmod 管理生命周期。

  • 它完全依赖 RTAI 子系统来实现硬实时。通过 rt_task_init 创建实时任务,通过 start_rt_timerrt_task_make_periodic 将该任务与 RTAI 的高精度定时器绑定,实现精确的周期性调度。

  • 分布式时钟 (DC) - 主站时钟同步到从站 (Master-to-Reference 模式):

    • 应用时间戳

      :在每个实时周期的开始,程序获取当前的 RTAI 实时时间 (rt_get_real_time_ns()),并通过 ecrt_master_application_time() 将这个时间戳告知 EtherCAT 主站核心。这个时间戳是后续所有 DC 同步计算的基础。

    • 参考时钟同步

      :代码中并没有像上一个 DC 示例那样显式选择参考时钟,而是隐式地让第一个支持 DC 的从站(EK1100)成为参考。在 run 函数中,它以较低的频率(每10个周期)调用 ecrt_master_sync_reference_clock_to()。这个函数的意义是将主站的当前应用时间(RTAI 时间)写入 EtherCAT 帧,发送给参考从站,命令参考从站将自己的时钟设置为这个值

    • 从站间同步

      :在每次调用 ecrt_master_sync_reference_clock_to() 之后,紧接着调用 ecrt_master_sync_slave_clocks()。这个函数会广播命令,让网络中所有其他的 DC 从站都与参考从站的时钟对齐。

    • 结论

      :这个示例采用的是**"主站作为时间源"**的同步模式。RTAI 的高精度时间是整个系统的基准,它被周期性地"注入"到 EtherCAT 网络中,同步所有从站的时钟。

  • DC 同步信号配置 (ecrt_slave_config_dc)

    • 在初始化阶段,代码对一个特定的从站(IDS Counter)调用了 ecrt_slave_config_dc()

    • 这个函数用于配置从站的 SYNC0 和 SYNC1 信号。这些信号是硬件信号,可以由从站的本地 DC 时钟以极高的精度(纳秒级)在指定的周期和偏移时间触发。

    • 这通常用于需要精确同步采样(如ADC)或驱动(如伺服电机)的应用。例如,配置 SYNC0 信号可以使所有电机在完全相同的物理时刻锁存位置命令。

  • 实时循环 (run)

    • 由 RTAI 周期性调度,执行标准的 EtherCAT I/O 流程。

    • 除了周期性的 PDO 数据交换(闪烁控制、读写计数器),它还嵌入了 DC 同步的逻辑

    • 它以 10 Hz 的频率(每10个周期)更新参考时钟,并在每个周期(1000 Hz)命令所有从站进行同步。

    总结:此代码是一个高级的硬实时内核模块,它不仅利用 RTAI 实现了高精度的周期性控制,更核心的是展示了如何使用 IgH Master 的 DC 功能来主导和控制整个 EtherCAT 网络的时钟同步。它通过将 RTAI 的高精度时间作为基准,周期性地同步网络中的参考从站,并命令所有其他从站跟随,从而在整个分布式系统中建立起一个统一、精确的时间基准。这对于实现多轴同步运动控制等高性能应用至关重要。