【IgH EtherCAT】 Xenomai +POSIX 实现一个基于EtherCAT的硬实时用户空间应用

SVG图表总结了整个系统的架构,包括:

三个主要层次:

  1. 应用层

    :主线程负责初始化和管理

  2. 实时层

    :1ms周期的实时控制线程

  3. 网络层

    :EtherCAT总线和设备

硬件配置:

  • EK1100

    :总线耦合器(位置0,0)

  • EL2004

    :4通道数字输出模块(位置0,1)

技术特性:

  • 使用SCHED_FIFO调度策略,优先级82

  • 内存锁定确保实时性

  • 1ms精确周期控制

  • 5Hz频率的输出闪烁控制

这个程序是一个典型的工业自动化实时控制系统,使用EtherCAT协议实现高精度的设备控制。

此 C 语言示例程序演示了如何在 Xenomai 环境下,使用其 POSIX API 仿真层 (POSIX skin) 来实现一个基于 IgH EtherCAT Master 的硬实时用户空间应用。

程序的关键功能和流程如下:

  1. 系统初始化与配置main 函数执行标准的 EtherCAT 初始化流程,包括请求主站、创建域、配置从站和 PDO、激活主站等。这些步骤与非 Xenomai 环境下的 IgH 应用完全相同。同时,它也设置了信号处理和内存锁定。

  2. 创建 Xenomai 实时任务 (通过 POSIX API)

  • 程序使用标准的 pthread_create 函数来创建一个新的执行流。

  • 关键区别在于

    :由于程序是在 Xenomai 环境下编译和运行的,Xenomai 的 POSIX skin 会拦截这个 API 调用。因此,创建的不是一个普通的 Linux 线程,而是一个由 Xenomai 内核直接管理的、具有硬实时保证的实时任务

  • 通过 pthread_attr_setschedpolicy 设置的 SCHED_FIFO 调度策略和优先级,会被直接应用到 Xenomai 的实时调度器上。

  • 实时循环 (my_thread)

    • 这个被 Xenomai 托管的实时任务进入一个周期为 1ms 的循环。

    • 周期的精确性由 clock_nanosleep 函数保证。同样,这个 POSIX API 调用被 Xenomai 拦截,并由 Xenomai 的高精度、无抖动的定时器服务来执行,从而实现了微秒级的确定性定时。

    • 在每个精确的周期内,任务执行标准的 EtherCAT I/O 流程:接收、处理、执行应用逻辑(闪烁控制)、发送。

  • 程序终止

    • main

       线程通过 sched_yield() 等待,而异步的信号处理器在接收到 SIGINT/SIGTERM 时,将全局 run 标志置为 0。

    • 这个标志的改变会使实时任务和主线程的 while 循环都退出。

    • 最后,main 线程通过 pthread_join 等待实时任务(Xenomai 任务)完全结束,然后执行资源清理。

    结论:这个示例的精髓在于展示了 Xenomai POSIX skin 的强大之处——它允许开发者使用标准、可移植的 POSIX 代码来编写享受 Xenomai 硬实时内核服务的应用程序,极大地降低了实时开发的门槛,并提高了代码的可维护性和可移植性。

    #include <errno.h> // 包含C标准库头文件,用于错误码处理#include <mqueue.h> // 包含消息队列头文件,虽然在此代码中未直接使用,但常用于实时通信#include <signal.h> // 包含信号处理头文件,用于捕获中断信号#include <pthread.h> // 包含 POSIX 线程库头文件。在 Xenomai 环境下,这些 API 会被 Xenomai 的 POSIX skin 映射到实时内核服务。#include <stdio.h> // 包含标准输入输出头文件#include <stdlib.h> // 包含标准库头文件#include <string.h> // 包含字符串处理头文件#include <unistd.h> // 包含 POSIX 标准 API 头文件#include <limits.h> // 包含限制性常量头文件#include <sys/ioctl.h> // 包含I/O控制操作头文件#include <sys/mman.h> // 包含内存管理声明头文件,用于 mlockall 函数#include <time.h> // 包含时间相关头文件。在 Xenomai 环境下,时间函数会被映射到 Xenomai 的高精度时钟。
    #ifndef XENOMAI_API_V3 // C预处理器指令,检查是否没有定义 XENOMAI_API_V3 宏#include <rtdm/rtdm.h> // 包含实时驱动模型(RTDM)头文件,提供 rt_printf 等功能#include <rtdk.h> // 包含 Xenomai 的实时打印(rt_printf)头文件#endif // 结束 #ifndef 条件编译块
    #include "ecrt.h" // 包含 IgH EtherCAT 主站的核心应用层 API 头文件
    #define NSEC_PER_SEC 1000000000 // 宏定义:每秒的纳秒数,用于时间计算
    static unsigned int cycle_us = 1000; /* 1 ms */ // 声明一个静态无符号整型变量,定义实时任务的周期为1000微秒(1毫秒)
    static pthread_t cyclic_thread; // 声明一个 pthread_t 类型的变量,用于存储被 Xenomai 托管的实时线程的ID
    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; // 声明一个静态的指向过程数据(Process Data)内存区域的指针
    static ec_slave_config_t *sc_dig_out_01 = NULL; // 声明一个静态的从站配置对象指针,用于数字量输出从站
    /****************************************************************************/
    // process data // 区域注释:过程数据定义#define BusCoupler01_Pos  0, 0 // 宏定义:总线耦合器(Bus Coupler)的位置#define DigOutSlave01_Pos 0, 1 // 宏定义:数字量输出从站(Digital Out Slave)的位置
    #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; // 声明一个静态无符号整型变量,用于存储PDO在过程数据域中的字节偏移量
    // process data // 区域注释:过程数据注册表const static ec_pdo_entry_reg_t domain1_regs[] = { // 定义一个静态常量数组,用于注册需要映射到域的PDO条目   {DigOutSlave01_Pos, Beckhoff_EL2004, 0x7000, 0x01, &off_dig_out0, NULL}, // 注册 EL2004 的第一个输出通道 (Index 0x7000, Sub-index 0x01)   {} // 数组结束标志};
    /****************************************************************************/
    /* Slave 1, "EL2004" // 注释:从站1 "EL2004" 的详细信息 * Vendor ID:       0x00000002 // 厂商ID * Product code:    0x07d43052 // 产品代码 * Revision number: 0x00100000 // 修订号 */// 以下是为 EL2004 从站进行 SII (从站信息接口) 覆盖配置所需的数据结构const 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输出)};
    const ec_pdo_info_t slave_1_pdos[] = { // 定义一个静态常量数组,描述 EL2004 从站的 PDO 配置 (将条目分组到PDO)   {0x1600, 1, slave_1_pdo_entries + 0}, /* Channel 1 */ // PDO 1: RxPDO 0x1600,包含1个条目   {0x1601, 1, slave_1_pdo_entries + 1}, /* Channel 2 */ // PDO 2: RxPDO 0x1601,包含1个条目   {0x1602, 1, slave_1_pdo_entries + 2}, /* Channel 3 */ // PDO 3: RxPDO 0x1602,包含1个条目   {0x1603, 1, slave_1_pdo_entries + 3}, /* Channel 4 */ // PDO 4: RxPDO 0x1603,包含1个条目};
    const ec_sync_info_t slave_1_syncs[] = { // 定义一个数组,描述 EL2004 从站的同步管理器(Sync Manager)配置   {0, EC_DIR_OUTPUT, 4, slave_1_pdos + 0, EC_WD_ENABLE}, // Sync Manager 0: 类型为输出(OUTPUT), 关联4个PDO, 启用看门狗(Watchdog)   {0xff} // 数组结束标志};
    /***************************************************************************** * Realtime task // 区域注释:实时任务相关函数 (由Xenomai内核调度) ****************************************************************************/
    void rt_check_domain_state(void) // 定义一个函数,用于检查并打印域的状态变化{    ec_domain_state_t ds = {}; // 声明一个局部的域状态结构体变量 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; // 声明一个局部的主站状态结构体变量 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"); // 如果不一致,输出链路是 "up" 还是 "down"    }
        master_state = ms; // 将当前状态赋值给全局变量,用于下次比较}
    /****************************************************************************/
    void *my_thread(void *arg) // 定义实时线程的主体函数。此函数将由一个被 Xenomai 托管的实时任务执行。{    struct timespec next_period; // 声明一个 timespec 结构体,用于存储下一个周期唤醒的绝对时间    int cycle_counter = 0; // 定义一个整型变量,用作周期计数器unsigned int blink = 0; // 定义一个无符号整型变量,用作闪烁标志
        // 核心:此调用被 Xenomai 拦截,获取的是 Xenomai 实时时钟的时间    clock_gettime(CLOCK_REALTIME, &next_period); // 获取当前实时时钟的时间,作为周期计算的起点
        while (run) { // 循环,只要全局变量 run 为1就一直执行        // 计算下一个周期的绝对唤醒时间        next_period.tv_nsec += cycle_us * 1000; // 将纳秒部分加上一个周期的时间(微秒 * 1000)        while (next_period.tv_nsec >= NSEC_PER_SEC) { // 处理纳秒进位到秒			next_period.tv_nsec -= NSEC_PER_SEC; // 纳秒部分减去1秒			next_period.tv_sec++; // 秒部分加1		}
            // 核心:此调用被 Xenomai 拦截,由 Xenomai 的高精度定时器执行,确保精确唤醒,抖动极小        clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &next_period, NULL); // 精确睡眠直到指定的绝对时间 (TIMER_ABSTIME)
            cycle_counter++; // 周期计数器加1
            // 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; // 对 blink 变量取反(0变1,1变0),实现闪烁		}
            // 将数据写入过程数据区,控制 EL2004 的4个输出通道        EC_WRITE_U8(domain1_pd + off_dig_out0, blink ? 0x0 : 0x0F); // 如果 blink 为 true, 输出0x00; 否则输出0x0F (全开)
            // send process data // 注释:发送过程数据        ecrt_domain_queue(domain1); // 将要发送的域数据放入发送队列        ecrt_master_send(master); // 将数据帧发送到 EtherCAT 总线    }
        return NULL; // 线程函数返回 NULL,表示正常退出}
    /***************************************************************************** * 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; // 声明一个局部的从站配置对象指针    int ret; // 声明一个整型变量,用于存储函数返回值
        signal(SIGTERM, signal_handler); // 注册信号处理器,当收到终止信号(SIGTERM)时调用 signal_handler    signal(SIGINT, signal_handler); // 注册信号处理器,当收到中断信号(SIGINT, Ctrl+C)时调用 signal_handler
        mlockall(MCL_CURRENT | MCL_FUTURE); // 锁定当前和未来所有分配的内存,防止被交换到磁盘,确保实时性
        printf("Requesting master...\n"); // 在控制台打印提示信息    master = ecrt_request_master(0); // 请求索引为0的 EtherCAT 主站实例    if (!master) { // 检查主站请求是否成功        return -1; // 如果失败,程序返回-1并退出    }
        domain1 = ecrt_master_create_domain(master); // 在主站上创建一个过程数据域    if (!domain1) { // 检查域创建是否成功        return -1; // 如果失败,程序返回-1并退出    }
        printf("Creating slave configurations...\n"); // 在控制台打印提示信息
        // 为 EK1100 总线耦合器创建配置    sc = ecrt_master_slave_config(master, BusCoupler01_Pos, Beckhoff_EK1100); // 为位置(0,0)的EK1100创建从站配置    if (!sc) { // 检查配置创建是否成功        return -1; // 如果失败,程序返回-1并退出    }
        // 为 EL2004 数字量输出从站创建配置    sc_dig_out_01 =        ecrt_master_slave_config(master, DigOutSlave01_Pos, Beckhoff_EL2004); // 为位置(0,1)的EL2004创建配置    if (!sc_dig_out_01) { // 检查配置创建是否成功        fprintf(stderr, "Failed to get slave configuration.\n"); // 打印错误信息        return -1; // 如果失败,程序返回-1并退出    }
        // 为 EL2004 从站配置 PDO    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; // 并返回-1退出    }
        // 将 PDO 条目注册到域中    if (ecrt_domain_reg_pdo_entry_list(domain1, domain1_regs)) { // 将定义的 PDO 注册列表注册到域        fprintf(stderr, "PDO entry registration failed!\n"); // 如果注册失败,打印错误信息        return -1; // 并返回-1退出    }
        printf("Activating master...\n"); // 在控制台打印提示信息    if (ecrt_master_activate(master)) { // 激活主站,开始总线通信        return -1; // 如果激活失败,返回-1退出    }
        // 获取域过程数据区的指针    if (!(domain1_pd = ecrt_domain_data(domain1))) { // 获取域的过程数据内存区指针        fprintf(stderr, "Failed to get domain data pointer.\n"); // 如果获取失败,打印错误信息        return -1; // 并返回-1退出    }
        /* Create cyclic RT-thread */ // 注释:创建周期性实时线程 (将被 Xenomai 托管)    struct sched_param param = { .sched_priority = 82 }; // 声明并设置调度参数,优先级为82。此优先级将被 Xenomai 调度器使用。
        pthread_attr_t thattr; // 声明 Pthread 属性对象。    pthread_attr_init(&thattr); // 初始化线程属性对象为默认值。    pthread_attr_setdetachstate(&thattr, PTHREAD_CREATE_JOINABLE); // 设置线程为可加入状态,主线程可以等待其结束。    pthread_attr_setinheritsched(&thattr, PTHREAD_EXPLICIT_SCHED); // 设置调度属性不从父线程继承,而是显式指定。    pthread_attr_setschedpolicy(&thattr, SCHED_FIFO); // 核心:设置调度策略为 SCHED_FIFO。Xenomai 将此映射为其硬实时调度策略。    pthread_attr_setschedparam(&thattr, &param); // 应用上面设置的调度参数(优先级)。
        // 核心:调用 POSIX API 创建线程,但此调用被 Xenomai 拦截,实际在 Xenomai 内核创建了一个硬实时任务。    ret = pthread_create(&cyclic_thread, &thattr, &my_thread, NULL); // 创建新线程,执行 my_thread 函数。    if (ret) { // 检查线程创建是否成功        fprintf(stderr, "%s: pthread_create cyclic task failed\n", // 如果失败,打印错误信息                strerror(-ret)); // strerror(-ret) 将错误码转为字符串return 1; // 程序返回1并退出    }
    while (run) { // 主线程进入一个低功耗等待循环sched_yield(); // 主动让出 CPU,让高优先级的实时任务运行	}
        pthread_join(cyclic_thread, NULL); // 等待实时线程执行结束,并回收其资源
        printf("End of Program\n"); // 打印程序结束信息    ecrt_release_master(master); // 释放 EtherCAT 主站资源
        return 0; // 程序正常退出}
    /****************************************************************************/
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值