【IgH EtherCAT】如何利用 RTAI 提供的实时任务和调度机制来构建一个高精度、确定性的工业控制应用

SVG图展示了系统的分层架构:

  • RTAI实时层

    :包含RT_TASK、信号量和定时器

  • EtherCAT Master层

    :主站、域、从站配置和PDO映射

  • EtherCAT网络层

    :与实际硬件设备(EL3162模拟输入、EL2004数字输出)通信

关键特点:

  1. 实时性

    :使用RTAI确保2000Hz的精确定时

  2. 同步保护

    :通过信号量保护EtherCAT操作

  3. 状态监控

    :定期检查主站、域和从站状态

  4. 双向数据流

    :接收模拟输入数据,发送数字输出控制信号

这个系统典型用于工业自动化场景,需要高精度的实时控制和可靠的现场总线通信

这个序列图详细描绘了从模块加载到卸载的整个生命周期中,Linux 内核、RTAI 子系统、驱动逻辑以及 EtherCAT 主站之间的动态交互。它特别突出了 RTAI 任务的创建、周期性执行以及与信号量的交互。

时序图展示了EtherCAT RTAI程序的完整执行流程:

  • 模块初始化阶段

    :请求主站、创建域、配置从站、注册PDO等

  • 循环执行阶段

    :以2000Hz频率进行实时数据交换

  • 模块卸载阶段

    :清理资源

这段使用了 RTAI (Real-Time Application Interface) 的 IgH EtherCAT Master 示例代码。这是一个在内核空间运行的硬实时应用,通过 RTAI 提供的任务调度和定时器来实现高精度的周期性控制。

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 2000 // task frequency in Hz // 宏定义:任务频率为 2000 Hz#define INHIBIT_TIME 20 // 宏定义:抑制时间为 20 (单位可能是微秒,用于回调函数)#define TIMERTICKS (1000000000 / FREQUENCY) // 宏定义:每个周期的纳秒数// Optional features (comment to disable) // 区域注释:可选特性 (注释掉以禁用)#define CONFIGURE_PDOS // 宏定义:启用 PDO 配置代码块#define PFX "ec_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 = {}; // 声明一个静态的域状态结构体变量static ec_slave_config_t *sc_ana_in = NULL; // 声明一个静态的从站配置对象指针,用于模拟量输入从站static ec_slave_config_state_t sc_ana_in_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 AnaInSlavePos  0, 3 // 宏定义:模拟量输入从站的位置#define DigOutSlavePos 0, 2 // 宏定义:数字量输出从站的位置#define Beckhoff_EL2004 0x00000002, 0x07D43052 // 宏定义:倍福 EL2004 的厂商/产品ID#define Beckhoff_EL3162 0x00000002, 0x0C5A3052 // 宏定义:倍福 EL3162 的厂商/产品IDstatic unsigned int off_ana_in; // offsets for PDO entries // 静态无符号整型,存储模拟量输入值的偏移量static unsigned int off_dig_out; // 静态无符号整型,存储数字量输出的偏移量const static ec_pdo_entry_reg_t domain1_regs[] = { // 定义一个静态常量数组,用于注册需要映射到域的PDO条目    {AnaInSlavePos,  Beckhoff_EL3162, 0x3101, 2, &off_ana_in}, // 注册 EL3162 的值 PDO    {DigOutSlavePos, Beckhoff_EL2004, 0x3001, 1, &off_dig_out}, // 注册 EL2004 的输出 PDO    {} // 数组结束标志};static unsigned int counter = 0; // 静态无符号整型,用作通用计数器static unsigned int blink = 0; // 静态无符号整型,用作闪烁标志/****************************************************************************/ // 分隔注释#ifdef CONFIGURE_PDOS // 如果定义了 CONFIGURE_PDOS 宏// 以下是为从站进行 SII (从站信息接口) 覆盖配置所需的数据结构static ec_pdo_entry_info_t el3162_channel1[] = { // 定义 EL3162 通道1的 PDO 条目信息    {0x3101, 1,  8}, // status (状态,8位)    {0x3101, 2, 16}  // value (值,16位)};static ec_pdo_entry_info_t el3162_channel2[] = { // 定义 EL3162 通道2的 PDO 条目信息    {0x3102, 1,  8}, // status (状态,8位)    {0x3102, 2, 16}  // value (值,16位)};static ec_pdo_info_t el3162_pdos[] = { // 定义 EL3162 的 PDO 信息 (将条目分组)    {0x1A00, 2, el3162_channel1}, // TxPDO 0x1A00    {0x1A01, 2, el3162_channel2}  // TxPDO 0x1A01};static ec_sync_info_t el3162_syncs[] = { // 定义 EL3162 的同步管理器配置    {2, EC_DIR_OUTPUT}, // SM2 是一个空的输出 SM    {3, EC_DIR_INPUT, 2, el3162_pdos}, // SM3 是输入 SM,关联2个 PDO    {0xff} // 结束标志};static ec_pdo_entry_info_t el2004_channels[] = { // 定义 EL2004 的 PDO 条目信息    {0x3001, 1, 1}, // Value 1 (值1,1位)    {0x3001, 2, 1}, // Value 2 (值2,1位)    {0x3001, 3, 1}, // Value 3 (值3,1位)    {0x3001, 4, 1}  // Value 4 (值4,1位)};static ec_pdo_info_t el2004_pdos[] = { // 定义 EL2004 的 PDO 信息    {0x1600, 1, &el2004_channels[0]}, // RxPDO 0x1600    {0x1601, 1, &el2004_channels[1]}, // RxPDO 0x1601    {0x1602, 1, &el2004_channels[2]}, // RxPDO 0x1602    {0x1603, 1, &el2004_channels[3]}  // RxPDO 0x1603};static ec_sync_info_t el2004_syncs[] = { // 定义 EL2004 的同步管理器配置    {0, EC_DIR_OUTPUT, 4, el2004_pdos}, // SM0 是输出 SM,关联4个 PDO    {1, EC_DIR_INPUT}, // SM1 是一个空的输入 SM    {0xff} // 结束标志};#endif // 结束 #ifdef/****************************************************************************/ // 分隔注释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 check_slave_config_states(void) // 定义一个函数,用于检查并打印特定从站的配置状态变化{    ec_slave_config_state_t s; // 声明一个局部的从站配置状态结构体变量    rt_sem_wait(&master_sem); // 等待 RTAI 信号量    ecrt_slave_config_state(sc_ana_in, &s); // 调用 ecrt API,获取模拟量输入从站的当前配置状态    rt_sem_signal(&master_sem); // 释放 RTAI 信号量    if (s.al_state != sc_ana_in_state.al_state) // 比较当前应用层状态与上次记录的状态        printk(KERN_INFO PFX "AnaIn: State 0x%02X.\n", s.al_state); // 如果不一致,打印新的AL状态    if (s.online != sc_ana_in_state.online) // 比较当前在线状态与上次记录的状态        printk(KERN_INFO PFX "AnaIn: %s.\n", s.online ? "online" : "offline"); // 如果不一致,打印在线/离线状态    if (s.operational != sc_ana_in_state.operational) // 比较当前操作状态与上次记录的状态        printk(KERN_INFO PFX "AnaIn: %soperational.\n", // 如果不一致,打印是否进入操作状态                s.operational ? "" : "Not "); // 根据 operational 的值打印 "operational" 或 "Not operational"    sc_ana_in_state = s; // 将当前状态赋值给全局变量,用于下次比较}/****************************************************************************/ // 分隔注释void run(long data) // RTAI 实时任务的主体函数{    while (1) { // 进入一个无限循环        t_last_cycle = get_cycles(); // 使用 RTAI 函数获取当前 CPU 周期数,记录周期开始时间        // 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 { // do this at 1 Hz // 如果计数器为0 (即每秒执行一次)            counter = FREQUENCY; // 重置计数器为频率值 (2000)            // calculate new process data // 注释:计算新的过程数据            blink = !blink; // 对 blink 变量取反,实现闪烁逻辑            // check for master state (optional) // 注释:检查主站状态(可选)            check_master_state(); // 调用函数检查并打印主站的状态变化            // check for islave configuration state(s) (optional) // 注释:检查从站配置状态(可选)            check_slave_config_states(); // 调用函数检查并打印特定从站的状态变化        }        // write process data // 注释:写入过程数据        EC_WRITE_U8(domain1_pd + off_dig_out, blink ? 0x06 : 0x09); // 将数据写入过程数据区,控制数字量输出 (输出 0b0110 或 0b1001)        rt_sem_wait(&master_sem); // 获取信号量        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; // 声明并初始化返回值    RTIME tick_period, requested_ticks, now; // 声明 RTAI 的时间变量#ifdef CONFIGURE_PDOS // 如果定义了 CONFIGURE_PDOS 宏    ec_slave_config_t *sc; // 声明一个从站配置对象指针#endif // 结束 #ifdef    printk(KERN_INFO PFX "Starting...\n"); // 在内核日志中打印启动信息    rt_sem_init(&master_sem, 1); // 初始化 RTAI 信号量,初始值为1    // 计算一个临界时间值,用于回调函数中判断是否离下一个周期太近    t_critical = cpu_khz * 1000 / FREQUENCY - cpu_khz * INHIBIT_TIME / 1000;    master = ecrt_request_master(0); // 请求索引为0的 EtherCAT 主站实例    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; // 跳转到释放主站处    }    if (!(sc_ana_in = ecrt_master_slave_config( // 为模拟量输入从站创建配置                    master, AnaInSlavePos, Beckhoff_EL3162))) {        printk(KERN_ERR PFX "Failed to get slave configuration.\n"); // 打印错误        goto out_release_master; // 跳转到释放主站处    }#ifdef CONFIGURE_PDOS // 如果定义了 CONFIGURE_PDOS 宏    printk(KERN_INFO PFX "Configuring PDOs...\n"); // 打印信息    if (ecrt_slave_config_pdos(sc_ana_in, EC_END, el3162_syncs)) { // 为 EL3162 配置 PDO        printk(KERN_ERR PFX "Failed to configure PDOs.\n"); // 打印错误        goto out_release_master; // 跳转到释放主站处    }    if (!(sc = ecrt_master_slave_config(master, DigOutSlavePos, // 为数字量输出从站创建配置                    Beckhoff_EL2004))) {        printk(KERN_ERR PFX "Failed to get slave configuration.\n"); // 打印错误        goto out_release_master; // 跳转到释放主站处    }    if (ecrt_slave_config_pdos(sc, EC_END, el2004_syncs)) { // 为 EL2004 配置 PDO        printk(KERN_ERR PFX "Failed to configure PDOs.\n"); // 打印错误        goto out_release_master; // 跳转到释放主站处    }#endif // 结束 #ifdef    printk(KERN_INFO PFX "Registering PDO entries...\n"); // 打印信息    if (ecrt_domain_reg_pdo_entry_list(domain1, domain1_regs)) { // 将定义的 PDO 注册列表注册到域        printk(KERN_ERR PFX "PDO entry registration failed!\n"); // 如果注册失败,打印错误        goto out_release_master; // 跳转到释放主站处    }    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);    // 初始化 RTAI 任务    if (rt_task_init(&task, run, 0, 2000, 0, 1, NULL)) { // 参数:任务结构体, 函数, 参数, 栈大小, 优先级, 使用FPU, 信号        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 RTAI sample module"); // 宏:声明模块的描述module_init(init_mod); // 宏:将 init_mod 函数注册为模块的初始化函数module_exit(cleanup_mod); // 宏:将 cleanup_mod 函数注册为模块的退出函数/****************************************************************************/

这是一个在 Linux 内核空间 运行的、基于 RTAI (Real-Time Application Interface) 的硬实时 EtherCAT 主站示例模块。它展示了如何利用 RTAI 提供的实时任务和调度机制来构建一个高精度、确定性的工业控制应用。

核心架构与功能:

  1. 内核模块形态 :与 tty.c 示例类似,这是一个通过 insmod 加载、rmmod 卸载的完整内核模块,其生命周期由 module_initmodule_exit 函数管理。

  2. 硬实时环境 (RTAI):此示例的核心是 RTAI。它不依赖 Linux 自身的调度器或定时器,而是使用 RTAI 提供的、运行在 Linux 内核之下的实时微内核服务。

  • RTAI 任务

    :通过 rt_task_init 创建一个名为 task 的 RTAI 实时任务,并指定其执行函数为 run

  • RTAI 调度与定时

    :通过 start_rt_timer 启动 RTAI 的高精度实时定时器,并通过 rt_task_make_periodictask 设置为由该定时器驱动的周期性任务。周期的精确性由 RTAI 内核保证,抖动(jitter)非常小。

  • 周期性执行

    run 函数中的 rt_task_wait_period() 会精确地阻塞任务,直到下一个由 RTAI 定时器确定的调度点到来。

  • 并发保护 - RTAI 信号量

    • 为了保护对共享 EtherCAT 主站资源的访问,代码使用了 RTAI 提供的信号量 (rt_sem_init, rt_sem_wait, rt_sem_signal)。

    • run 函数和两个回调函数中,所有对 ecrt_* 函数的调用都被信号量加锁和解锁,确保了在 RTAI 的多任务环境下的数据一致性和原子性。

  • 周期性任务 (run)

    • 这是 RTAI 实时任务的主体,在一个无限循环中运行。

    • 每个周期开始时,它执行标准的 EtherCAT I/O 流程:接收、处理、应用逻辑、发送。

    • 应用逻辑

      :包含一个简单的 1Hz 闪烁逻辑,控制一个数字量输出模块。

    • 状态监控

      :以较低频率检查并打印主站、域和从站的状态。

    • 高精度时间戳

      :使用 get_cycles() 获取 CPU 周期数,用于高精度的时间测量,这在 send/receive_callback 中用于防止与实时周期冲突。

  • 可选的回调机制

    • 代码中注册了 send_callbackreceive_callback,这通常用于外部事件驱动的模式(例如,由网卡中断直接触发数据收发)。

    • 在这些回调函数中,通过比较当前 CPU 周期数与上一个实时周期开始的时间戳,来判断是否离下一个实时周期太近。如果太近,就不执行操作,这是为了避免外部事件干扰到由 RTAI 定时器驱动的主实时循环的确定性。

  • 生命周期管理

    • 加载 (insmod)

      : 执行 init_mod。完成 EtherCAT 配置、初始化 RTAI 信号量、启动 RTAI 实时定时器,并创建和启动 RTAI 周期性任务。

    • 运行

      : RTAI 调度器周期性地唤醒 run 任务,执行硬实时控制循环。

    • 卸载 (rmmod)

      : 执行 cleanup_mod。安全地删除 RTAI 任务,停止 RTAI 定时器,释放 EtherCAT 主站资源,并删除 RTAI 信号量。

    总结 :此代码是一个经典的硬实时内核空间 EtherCAT 控制器范例。它完全绕开了标准 Linux 的调度和定时机制,将所有实时关键操作都交由 RTAI 微内核处理,从而获得了微秒级的、确定性的性能。这是在需要极高实时性保证的工业自动化和机器人控制等领域中常见的架构。