
主要功能:
这是一个基于RTAI实时系统的EtherCAT分布式时钟示例程序,实现了高精度的主从站时钟同步。
核心组件:
-
RTAI实时层
: 提供1000Hz的周期性实时任务,确保确定性的时序控制
-
EtherCAT主站
: 管理整个EtherCAT网络,处理从站配置和数据交换
-
从站设备
: 包括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 示例相比,它增加了与时间同步相关的关键操作。
核心架构与功能:
- 内核模块与 RTAI 硬实时:
-
这是一个标准的 Linux 内核模块,通过
insmod
和rmmod
管理生命周期。 -
它完全依赖 RTAI 子系统来实现硬实时。通过
rt_task_init
创建实时任务,通过start_rt_timer
和rt_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 的高精度时间作为基准,周期性地同步网络中的参考从站,并命令所有其他从站跟随,从而在整个分布式系统中建立起一个统一、精确的时间基准。这对于实现多轴同步运动控制等高性能应用至关重要。
-