这是一个基于RTAI(Real-Time Application Interface)的EtherCAT实时控制程序,主要功能是控制Beckhoff EL2004数字输出模块进行LED闪烁控制。
主要组件
- EtherCAT主站
: 管理整个EtherCAT网络,负责与从站设备通信
- 从站设备
: EK1100总线耦合器 + EL2004四路数字输出模块
- 实时任务
: 以1ms周期运行的硬实时任务
- 分布式时钟(DC)
: 确保整个系统的时间同步
核心工作流程
程序在1ms的硬实时周期内执行以下步骤:
等待下个周期到来
接收来自从站的数据
处理过程数据并检查系统状态
执行业务逻辑(控制LED闪烁)
同步分布式时钟
发送数据到从站并更新主站时钟
技术特点
使用RTAI提供硬实时保证
实现了复杂的分布式时钟同步算法
具有过载检测和错误处理机制
支持实时性能监控和状态报告
这个程序展示了工业自动化中典型的实时控制系统设计模式,特别适用于对时序要求极为严格的应用场景。
这是一个非常高级和复杂的示例,它在之前的 RTAI/LXRT 用户空间硬实时示例的基础上,增加了分布式时钟 (Distributed Clocks, DC) 的同步功能。这是实现 EtherCAT 网络中多个从站设备间微秒级同步的关键技术。
#include <sched.h> // 包含 Linux 调度策略相关的头文件#include <stdio.h> // 包含标准输入输出头文件#include <stdlib.h> // 包含标准库头文件#include <fcntl.h> // 包含文件控制操作头文件#include <signal.h> // 包含信号处理头文件#include <rtai_lxrt.h> // 包含 RTAI/LXRT 的核心头文件,用于用户空间硬实时#include <rtdm/rtdm.h> // 包含实时驱动模型(RTDM)的头文件#include "ecrt.h" // 包含 IgH EtherCAT 主站的核心应用层 API 头文件#define rt_printf(X, Y) // 宏定义:将 rt_printf 定义为空操作,禁用实时打印#define NSEC_PER_SEC 1000000000 // 宏定义:每秒的纳秒数RT_TASK *task; // 声明一个指向 RTAI 任务结构体的全局指针static unsigned int cycle_ns = 1000000; /* 1 ms */ // 声明一个静态无符号整型变量,定义实时任务的周期为1,000,000纳秒(1毫秒)static volatile sig_atomic_t run = 1; // 声明一个静态的、易失的、信号安全的整型变量 run,作为主循环的运行标志/****************************************************************************/ // 分隔注释// 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 = {}; // 声明一个静态的域状态结构体变量static uint8_t *domain1_pd = NULL; // 声明一个静态的指向过程数据内存区域的指针static ec_slave_config_t *sc_dig_out_01 = NULL; // 声明一个静态的从站配置对象指针,用于数字量输出从站/****************************************************************************/ // 分隔注释// EtherCAT distributed clock variables // 区域注释:EtherCAT 分布式时钟 (DC) 相关变量#define DC_FILTER_CNT 1024 // 宏定义:用于时钟漂移计算的滤波器样本数#define SYNC_MASTER_TO_REF 1 // 宏定义:同步模式开关。为1表示主站时钟同步到参考从站时钟。static uint64_t dc_start_time_ns = 0LL; // 静态64位无符号整型,记录DC开始的系统时间static uint64_t dc_time_ns = 0; // 静态64位无符号整型,记录当前周期的系统时间#if SYNC_MASTER_TO_REF // 如果启用主站同步到参考从站模式static uint8_t dc_started = 0; // 静态8位无符号整型,标志DC同步是否已开始static int32_t dc_diff_ns = 0; // 静态32位整型,记录主站时钟与参考从站时钟的当前差值static int32_t prev_dc_diff_ns = 0; // 静态32位整型,记录上一个周期的时钟差值static int64_t dc_diff_total_ns = 0LL; // 静态64位整型,用于累加时钟差值以计算平均值static int64_t dc_delta_total_ns = 0LL; // 静态64位整型,用于累加时钟差值的变化量(漂移)以计算平均值static int dc_filter_idx = 0; // 静态整型,滤波器样本计数器static int64_t dc_adjust_ns; // 静态64位整型,计算出的需要调整的时间量#endif // 结束 #ifstatic int64_t system_time_base = 0LL; // 静态64位整型,系统时间基准,用于动态调整主站本地时钟static uint64_t wakeup_time = 0LL; // 静态64位无符号整型,下一个周期的唤醒时间static uint64_t overruns = 0LL; // 静态64位无符号整型,记录任务超时的次数/****************************************************************************/ // 分隔注释// process data // 区域注释:过程数据定义#define BusCoupler01_Pos 0, 0 // 宏定义:总线耦合器的位置#define DigOutSlave01_Pos 0, 1 // 宏定义:数字量输出从站的位置#define Beckhoff_EK1100 0x00000002, 0x044c2c52 // 宏定义:倍福 EK1100 的厂商/产品ID#define Beckhoff_EL2004 0x00000002, 0x07d43052 // 宏定义:倍福 EL2004 的厂商/产品ID// offsets for PDO entries // 注释:PDO条目的偏移量static unsigned int off_dig_out0 = 0; // 静态无符号整型,存储数字量输出的偏移量// process data // 区域注释:过程数据注册表const static ec_pdo_entry_reg_t domain1_regs[] = { // 定义一个静态常量数组,用于注册需要映射到域的PDO条目 {DigOutSlave01_Pos, Beckhoff_EL2004, 0x7000, 0x01, &off_dig_out0, NULL}, // 注册 EL2004 的第一个输出通道 {} // 数组结束标志};/****************************************************************************/ // 分隔注释/* Slave 1, "EL2004" // 注释:从站1 "EL2004" 的详细信息 * Vendor ID: 0x00000002 // 厂商ID * Product code: 0x07d43052 // 产品代码 * Revision number: 0x00100000 // 修订号 */// 以下是为 EL2004 从站进行 SII (从站信息接口) 覆盖配置所需的数据结构ec_pdo_entry_info_t slave_1_pdo_entries[] = { // 定义 EL2004 的 PDO 条目信息 {0x7000, 0x01, 1}, /* Output */ // 条目1:索引0x7000,子索引0x01,位长度1 (通道1输出) {0x7010, 0x01, 1}, /* Output */ // 条目2:索引0x7010,子索引0x01,位长度1 (通道2输出) {0x7020, 0x01, 1}, /* Output */ // 条目3:索引0x7020,子索引0x01,位长度1 (通道3输出) {0x7030, 0x01, 1}, /* Output */ // 条目4:索引0x7030,子索引0x01,位长度1 (通道4输出)};ec_pdo_info_t slave_1_pdos[] = { // 定义 EL2004 的 PDO 信息 {0x1600, 1, slave_1_pdo_entries + 0}, /* Channel 1 */ // RxPDO 0x1600 {0x1601, 1, slave_1_pdo_entries + 1}, /* Channel 2 */ // RxPDO 0x1601 {0x1602, 1, slave_1_pdo_entries + 2}, /* Channel 3 */ // RxPDO 0x1602 {0x1603, 1, slave_1_pdo_entries + 3}, /* Channel 4 */ // RxPDO 0x1603};ec_sync_info_t slave_1_syncs[] = { // 定义 EL2004 的同步管理器配置 {0, EC_DIR_OUTPUT, 4, slave_1_pdos + 0, EC_WD_ENABLE}, // Sync Manager 0: 输出类型, 关联4个PDO, 启用看门狗 {0xff} // 数组结束标志};/***************************************************************************** * Realtime task // 区域注释:实时任务相关函数 ****************************************************************************//** Get the time in ns for the current cpu, adjusted by system_time_base. // Doxygen 注释:获取当前CPU的时间(纳秒),并根据 system_time_base 进行调整 * * \attention Rather than calling rt_get_time_ns() directly, all application // 注意:所有应用层的时间调用都应该使用此方法,而不是直接调用 rt_get_time_ns() * time calls should use this method instead. * * \ret The time in ns. // 返回值:时间(纳秒) */uint64_t system_time_ns(void) // 定义获取调整后系统时间的函数{ RTIME time = rt_get_time_ns(); // 调用 RTAI/LXRT API 获取原始的系统时间(纳秒) if (system_time_base > time) { // 如果时间基准大于当前时间(异常情况) rt_printk("%s() error: system_time_base greater than" // 打印错误信息 " system time (system_time_base: %lld, time: %llu\n", __func__, system_time_base, time); return time; // 返回原始时间 } else { // 正常情况 return time - system_time_base; // 返回减去基准偏移后的时间,这是“主站本地时间” }}/****************************************************************************/ // 分隔注释/** Convert system time to RTAI time in counts (via the system_time_base). // Doxygen 注释:将系统时间转换为 RTAI 的时钟节拍数(通过 system_time_base) */RTIME system2count( // 定义将“主站本地时间”转换为 RTAI 节拍数的函数 uint64_t time // 参数:主站本地时间(纳秒) ){ RTIME ret; // 声明返回值 if ((system_time_base < 0) && // 如果基准为负且其绝对值大于给定时间(异常情况) ((uint64_t) (-system_time_base) > time)) { rt_printk("%s() error: system_time_base less than" // 打印错误信息 " system time (system_time_base: %lld, time: %llu\n", __func__, system_time_base, time); ret = time; // 直接使用时间 } else { // 正常情况 ret = time + system_time_base; // 将主站本地时间加上基准偏移,得到原始的 RTAI 系统时间 } return nano2count(ret); // 将 RTAI 系统时间(纳秒)转换为时钟节拍数}/****************************************************************************/ // 分隔注释/** Synchronise the distributed clocks // Doxygen 注释:同步分布式时钟 */void sync_distributed_clocks(void) // 定义同步分布式时钟的函数{#if SYNC_MASTER_TO_REF // 如果启用主站同步到参考从站模式 uint32_t ref_time = 0; // 声明一个32位无符号整型,用于存储参考时钟时间 uint64_t prev_app_time = dc_time_ns; // 保存上一个周期的应用时间#endif // 结束 #if dc_time_ns = system_time_ns(); // 获取当前周期的应用时间(主站本地时间)#if SYNC_MASTER_TO_REF // 如果启用主站同步到参考从站模式 // get reference clock time to synchronize master cycle // 注释:获取参考时钟时间以同步主站周期 ecrt_master_reference_clock_time(master, &ref_time); // 获取参考从站的当前时间 dc_diff_ns = (uint32_t) prev_app_time - ref_time; // 计算主站本地时间与参考从站时间的差值#else // 否则(参考从站同步到主站模式) // sync reference clock to master // 注释:将参考时钟同步到主站 ecrt_master_sync_reference_clock_to(master, dc_time_ns); // 将当前应用时间写入 EtherCAT 帧,用于设置参考从站时钟#endif // 结束 #if // call to sync slaves to ref slave // 注释:调用此函数以将其他从站同步到参考从站 ecrt_master_sync_slave_clocks(master); // 在 EtherCAT 帧中写入命令,让所有从站与参考从站的时钟同步}/****************************************************************************/ // 分隔注释/** Return the sign of a number // Doxygen 注释:返回一个数的符号 * * ie -1 for -ve value, 0 for 0, +1 for +ve value // 例如:负数返回-1,0返回0,正数返回+1 * * \retval the sign of the value // 返回值:值的符号 */#define sign(val) \ // 宏定义:获取符号 ({ typeof (val) _val = (val); \ // 使用 GCC 扩展,创建一个与 val 类型相同的临时变量 _val ((_val > 0) - (_val < 0)); }) // 一个巧妙的技巧:如果_val>0,结果是1-0=1;如果_val<0,结果是0-1=-1;如果_val=0,结果是0-0=0/****************************************************************************/ // 分隔注释/** Update the master time based on ref slaves time diff // Doxygen 注释:根据参考从站的时间差更新主站时间 * * called after the ethercat frame is sent to avoid time jitter in // 注释:在发送 EtherCAT 帧之后调用,以避免 sync_distributed_clocks() 中的时间抖动 * sync_distributed_clocks() */void update_master_clock(void) // 定义更新主站时钟的函数{#if SYNC_MASTER_TO_REF // 如果启用主站同步到参考从站模式 // calc drift (via un-normalised time diff) // 注释:计算漂移(通过未归一化的时间差) int32_t delta = dc_diff_ns - prev_dc_diff_ns; // 计算当前差值与上次差值的变化量,即漂移 prev_dc_diff_ns = dc_diff_ns; // 保存当前差值,用于下一周期计算 // normalise the time diff // 注释:归一化时间差 dc_diff_ns = // 将时间差值归一化到 [-cycle_ns/2, +cycle_ns/2] 的范围内 ((dc_diff_ns + (cycle_ns / 2)) % cycle_ns) - (cycle_ns / 2); // only update if primary master // 注释:仅当是主 master 时才更新 if (dc_started) { // 如果 DC 同步已开始 // add to totals // 注释:累加到总和 dc_diff_total_ns += dc_diff_ns; // 累加归一化后的差值 dc_delta_total_ns += delta; // 累加漂移量 dc_filter_idx++; // 滤波器样本计数加1 if (dc_filter_idx >= DC_FILTER_CNT) { // 如果样本数达到滤波器大小 // add rounded delta average // 注释:加上四舍五入后的漂移平均值 dc_adjust_ns += // 计算平均漂移并累加到调整量中(PI控制器中的I项) ((dc_delta_total_ns + (DC_FILTER_CNT / 2)) / DC_FILTER_CNT); // and add adjustment for general diff (to pull in drift) // 注释:并为一般性差异添加调整(以拉回漂移) dc_adjust_ns += sign(dc_diff_total_ns / DC_FILTER_CNT); // 根据平均差值的符号,进行微调(PI控制器中的P项) // limit crazy numbers (0.1% of std cycle time) // 注释:限制异常值 (标准周期的0.1%) if (dc_adjust_ns < -1000) { // 如果调整量过小 dc_adjust_ns = -1000; // 限制最小值 } if (dc_adjust_ns > 1000) { // 如果调整量过大 dc_adjust_ns = 1000; // 限制最大值 } // reset // 注释:重置 dc_diff_total_ns = 0LL; // 重置差值累加器 dc_delta_total_ns = 0LL; // 重置漂移累加器 dc_filter_idx = 0; // 重置滤波器计数器 } // add cycles adjustment to time base (including a spot adjustment) // 注释:将周期调整量添加到时间基准(包括一个即时调整) system_time_base += dc_adjust_ns + sign(dc_diff_ns); // 更新 system_time_base,动态调整主站本地时钟 } else { // 如果 DC 同步尚未开始 dc_started = (dc_diff_ns != 0); // 检查是否收到了有效的差值,如果收到了则标记为已开始 if (dc_started) { // 如果刚刚开始 // output first diff // 注释:输出第一次的差值 rt_printk("First master diff: %d.\n", dc_diff_ns); // 打印首次差值 // record the time of this initial cycle // 注释:记录这个初始周期的时刻 dc_start_time_ns = dc_time_ns; // 保存开始时间 } }#endif // 结束 #if}/****************************************************************************/ // 分隔注释void rt_check_domain_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 rt_printf("Domain1: WC %u.\n", ds.working_counter); // 打印新的WC值 (已禁用) } if (ds.wc_state != domain1_state.wc_state) { // 比较当前工作计数器状态与上次记录的状态 rt_printf("Domain1: State %u.\n", ds.wc_state); // 打印新的WC状态 (已禁用) } domain1_state = ds; // 将当前状态赋值给全局变量}/****************************************************************************/ // 分隔注释void rt_check_master_state(void) // 定义一个函数,用于检查并打印主站的状态变化{ ec_master_state_t ms; // 声明一个局部的主站状态结构体变量 ecrt_master_state(master, &ms); // 调用 ecrt API,获取主站的当前状态 if (ms.slaves_responding != master_state.slaves_responding) { // 比较当前响应的从站数量 rt_printf("%u slave(s).\n", ms.slaves_responding); // 打印新的从站数量 (已禁用) } if (ms.al_states != master_state.al_states) { // 比较当前应用层(AL)状态 rt_printf("AL states: 0x%02X.\n", ms.al_states); // 打印新的AL状态 (已禁用) } if (ms.link_up != master_state.link_up) { // 比较当前链路连接状态 rt_printf("Link is %s.\n", ms.link_up ? "up" : "down"); // 打印链路状态 (已禁用) } master_state = ms; // 将当前状态赋值给全局变量}/****************************************************************************/ // 分隔注释/** Wait for the next period // Doxygen 注释:等待下一个周期 */void wait_period(void) // 定义等待周期的函数{ while (1) // 进入一个循环,直到成功睡眠 { RTIME wakeup_count = system2count(wakeup_time); // 将下一个唤醒时间(主站本地时间)转换为 RTAI 节拍数 RTIME current_count = rt_get_time(); // 获取当前的 RTAI 节拍数 if ((wakeup_count < current_count) // 如果唤醒时间已经过去 || (wakeup_count > current_count + (50 * cycle_ns))) { // 或者唤醒时间远在未来(异常) rt_printk("%s(): unexpected wake time!\n", __func__); // 打印警告 } switch (rt_sleep_until(wakeup_count)) { // 调用 RTAI/LXRT API,睡眠直到指定的绝对时间点(节拍数) case RTE_UNBLKD: // 如果被非预期地唤醒 rt_printk("rt_sleep_until(): RTE_UNBLKD\n"); // 打印信息 continue; // 继续循环,重新尝试睡眠 case RTE_TMROVRN: // 如果发生定时器超时(任务执行时间超过一个周期) rt_printk("rt_sleep_until(): RTE_TMROVRN\n"); // 打印信息 overruns++; // 超时计数器加1 if (overruns % 100 == 0) { // 每发生100次超时 // in case wake time is broken ensure other processes get // 注释:以防唤醒时间出错,确保其他进程能获得一些时间片 // some time slice (and error messages can get displayed) rt_sleep(cycle_ns / 100); // 短暂睡眠,让出CPU } break; // 跳出 switch default: // 默认情况(正常唤醒) break; // 跳出 switch } // done if we got to here // 注释:如果执行到这里,说明已成功唤醒 break; // 跳出 while 循环 } // set master time in nano-seconds // 注释:设置主站时间(纳秒) ecrt_master_application_time(master, wakeup_time); // 将下一个周期的应用时间戳告知 EtherCAT 主站,用于 DC 同步 // calc next wake time (in sys time) // 注释:计算下一个唤醒时间(在主站本地时间系统中) wakeup_time += cycle_ns; // 将唤醒时间增加一个周期}/****************************************************************************/ // 分隔注释void my_cyclic(void) // 定义周期性函数,这个函数的主体将在 RTAI 的硬实时上下文中执行{ int cycle_counter = 0; // 声明周期计数器 unsigned int blink = 0; // 声明闪烁标志 // oneshot mode to allow adjustable wake time // 注释:单次触发模式以允许可调整的唤醒时间 rt_set_oneshot_mode(); // 设置 RTAI 定时器为单次触发模式,而不是周期模式 // set first wake time in a few cycles // 注释:在几个周期后设置第一次唤醒时间 wakeup_time = system_time_ns() + 10 * cycle_ns; // 计算首次唤醒时间 // start the timer // 注释:启动定时器 start_rt_timer(nano2count(cycle_ns)); // 启动 RTAI 实时定时器,参数为周期长度(用于参考,但实际由 rt_sleep_until 控制) rt_make_hard_real_time(); // 将当前任务切换到硬实时模式 while (run) { // 循环,只要全局变量 run 为1就一直执行 // wait for next period (using adjustable system time) // 注释:等待下一个周期(使用可调整的系统时间) wait_period(); // 调用函数,精确睡眠到下一个周期 cycle_counter++; // 周期计数器加1 if (!run) { // 再次检查 run 标志,防止在睡眠期间被改变 break; // 如果为0,退出循环 } // receive EtherCAT // 注释:接收 EtherCAT 帧 ecrt_master_receive(master); // 从网络接口接收数据帧 ecrt_domain_process(domain1); // 处理域的数据 rt_check_domain_state(); // 调用函数检查域的状态 if (!(cycle_counter % 1000)) { // 每1000个周期 (1秒) rt_check_master_state(); // 调用函数检查主站的状态 } if (!(cycle_counter % 200)) { // 每200个周期 (200毫秒) blink = !blink; // 切换闪烁标志 } EC_WRITE_U8(domain1_pd + off_dig_out0, blink ? 0x00 : 0x0F); // 将数据写入过程数据区 // queue process data // 注释:将过程数据放入队列 ecrt_domain_queue(domain1); // 将要发送的域数据放入发送队列 // sync distributed clock just before master_send to set // 注释:在发送前同步分布式时钟,以设置最精确的主站时钟时间 // most accurate master clock time sync_distributed_clocks(); // 调用函数,执行 DC 同步操作 // send EtherCAT data // 注释:发送 EtherCAT 数据 ecrt_master_send(master); // 将数据帧发送到 EtherCAT 总线 // update the master clock // 注释:更新主站时钟 // Note: called after ecrt_master_send() to reduce time // 注意:在发送后调用,以减少 sync_distributed_clocks() 中的时间抖动 // jitter in the sync_distributed_clocks() call update_master_clock(); // 调用函数,根据 DC 差值调整主站本地时钟 } rt_make_soft_real_time(); // 将任务从硬实时模式切换回软实时模式 stop_rt_timer(); // 停止 RTAI 实时定时器}/***************************************************************************** * Signal handler // 区域注释:信号处理器 ****************************************************************************/void signal_handler(int sig) // 定义信号处理函数{ run = 0; // 将全局运行标志 run 设置为0}/***************************************************************************** * Main function // 区域注释:主函数 ****************************************************************************/int main(int argc, char *argv[]) // C程序的入口主函数{ ec_slave_config_t *sc_ek1100; // 声明一个指向 EK1100 从站配置的指针 int ret; // 声明返回值 signal(SIGTERM, signal_handler); // 注册终止信号处理器 signal(SIGINT, signal_handler); // 注册中断信号处理器 mlockall(MCL_CURRENT | MCL_FUTURE); // 锁定内存 printf("Requesting master...\n"); // 打印提示信息 master = ecrt_request_master(0); // 请求主站实例 if (!master) { // 检查是否成功 return -1; // 失败则退出 } domain1 = ecrt_master_create_domain(master); // 创建过程数据域 if (!domain1) { // 检查是否成功 return -1; // 失败则退出 } printf("Creating slave configurations...\n"); // 打印提示信息 // Create configuration for bus coupler // 注释:为总线耦合器创建配置 sc_ek1100 = // 为 EK1100 创建配置 ecrt_master_slave_config(master, BusCoupler01_Pos, Beckhoff_EK1100); if (!sc_ek1100) { // 检查是否成功 return -1; // 失败则退出 } sc_dig_out_01 = // 为 EL2004 创建配置 ecrt_master_slave_config(master, DigOutSlave01_Pos, Beckhoff_EL2004); if (!sc_dig_out_01) { // 检查是否成功 fprintf(stderr, "Failed to get slave configuration.\n"); // 打印错误 return -1; // 失败则退出 } if (ecrt_slave_config_pdos(sc_dig_out_01, EC_END, slave_1_syncs)) { // 为 EL2004 配置 PDO fprintf(stderr, "Failed to configure PDOs.\n"); // 打印错误 return -1; // 失败则退出 } if (ecrt_domain_reg_pdo_entry_list(domain1, domain1_regs)) { // 注册 PDO 条目到域 fprintf(stderr, "PDO entry registration failed!\n"); // 打印错误 return -1; // 失败则退出 } /* Set the initial master time and select a slave to use as the DC // 注释:设置初始主站时间,并选择一个从站作为 DC 参考时钟 * reference clock, otherwise pass NULL to auto select the first capable // 否则传递 NULL 将自动选择第一个有能力的从站 * slave. Note: This can be used whether the master or the ref slave will // 注意:无论主站还是参考从站作为系统主时钟,此方法都适用 * be used as the systems master DC clock. */ dc_start_time_ns = system_time_ns(); // 初始化 DC 开始时间 dc_time_ns = dc_start_time_ns; // 初始化 DC 当前时间 ret = ecrt_master_select_reference_clock(master, sc_ek1100); // 选择 EK1100 作为 DC 参考时钟 if (ret < 0) { // 检查是否成功 fprintf(stderr, "Failed to select reference clock: %s\n", // 打印错误 strerror(-ret)); return ret; // 失败则退出 } printf("Activating master...\n"); // 打印提示信息 if (ecrt_master_activate(master)) { // 激活主站 return -1; // 失败则退出 } if (!(domain1_pd = ecrt_domain_data(domain1))) { // 获取过程数据指针 fprintf(stderr, "Failed to get domain data pointer.\n"); // 打印错误 return -1; // 失败则退出 } /* Create cyclic RT-thread */ // 注释:创建周期性实时线程 struct sched_param param; // 声明调度参数结构体 param.sched_priority = sched_get_priority_max(SCHED_FIFO) - 1; // 设置为最高实时优先级减1 if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) { // 应用调度策略和优先级 puts("ERROR IN SETTING THE SCHEDULER"); // 打印错误 perror("errno"); // 打印系统错误 return -1; // 失败则退出 } task = rt_task_init(nam2num("ec_rtai_rtdm_example"), // 初始化 RTAI 任务 0 /* priority */, 0 /* stack size */, 0 /* msg size */); // 使用默认参数 my_cyclic(); // 调用周期性函数,该函数将接管执行流并进入硬实时模式 rt_task_delete(task); // 当 my_cyclic 退出后,删除 RTAI 任务 printf("End of Program\n"); // 打印结束信息 ecrt_release_master(master); // 释放主站资源 return 0; // 正常退出}/****************************************************************************/
这段代码是一个高度复杂的用户空间硬实时 EtherCAT 主站示例,它基于 RTAI/LXRT,并实现了分布式时钟 (Distributed Clocks, DC) 同步功能。其核心目标是在整个 EtherCAT 网络中实现所有从站的微秒级同步,并使主站的应用周期与该同步时钟对齐。
核心架构与功能:
用户空间硬实时 (LXRT):与上一个 RTAI 示例类似,它是一个用户空间程序,但通过
rt_make_hard_real_time()
等 LXRT API 将自身执行上下文切换到 RTAI 微内核进行硬实时调度,从而获得了确定性的性能。分布式时钟 (DC) 同步:这是此示例最核心的新增功能。
在每个实时周期,
sync_distributed_clocks()
函数会读取参考从站的当前时间。update_master_clock()
函数会计算主站的本地应用时钟与参考从站时钟之间的差值 (
dc_diff_ns
) 和漂移 (delta
)。它实现了一个简单的 PI 控制器,通过一个低通滤波器(累加
DC_FILTER_CNT
个样本)来计算出一个调整量 (dc_adjust_ns
)。这个调整量被用于动态地修改一个全局的时间基准
system_time_base
。
- 参考时钟选择
:在初始化阶段,通过
ecrt_master_select_reference_clock()
选择了第一个支持 DC 的从站(EK1100)作为整个网络的参考时钟。 - 主站同步到参考时钟
(
SYNC_MASTER_TO_REF = 1
): - 可调整的主站时钟
:所有与时间相关的操作都通过
system_time_ns()
函数进行。这个函数返回rt_get_time_ns() - system_time_base
。通过不断微调system_time_base
,程序实际上是在**“拉伸”或“压缩”**主站的本地时间轴,使其与 EtherCAT 网络上的参考时钟保持同步。 - 从站同步
:
ecrt_master_sync_slave_clocks()
函数会在每个周期发送命令,让网络中所有其他的 DC 从站都与这个参考时钟同步。
精确的周期唤醒 (
wait_period
):-
程序的周期性不再由 RTAI 的周期模式 (
rt_task_make_periodic
) 驱动,而是切换到了单次触发模式 (rt_set_oneshot_mode
)。wait_period
函数使用
rt_sleep_until()
睡眠到一个动态计算的唤醒时间wakeup_time
。这个
wakeup_time
是在经过system_time_base
调整后的“主站本地时间”系统中计算的。这意味着,当主站时钟被调整时,任务的唤醒时间点也会相应地提前或推后,从而实现了应用周期与网络同步时钟的对齐。ecrt_master_application_time()
函数将这个唤醒时间戳告知主站核心,主站会用这个时间来计算和补偿网络延迟,实现更精确的同步。
实时循环 (
my_cyclic
):-
循环的结构是:
wait_period()
->I/O
->sync_distributed_clocks()
->ecrt_master_send()
->update_master_clock()
。这个顺序非常关键:在发送数据帧之前执行
sync_distributed_clocks()
,可以将最新的时间信息写入帧中;在发送之后执行update_master_clock()
,可以利用刚刚完成的通信所获得的时间差来计算下一个周期的调整量,从而避免了将计算延迟引入到同步过程中。
总结:此示例是实现高精度、多轴同步运动控制等应用的基石。它不仅利用 RTAI/LXRT 实现了用户空间的硬实时,更进一步地,通过复杂的分布式时钟同步算法,将主站的应用周期与整个 EtherCAT 网络的物理时钟紧密耦合。通过动态调整本地时间基准,它创建了一个与远程参考时钟同步的“虚拟本地时钟”,并在此虚拟时钟的驱动下运行,从而实现了整个系统的确定性同步。