
SVG图表总结了整个系统的架构,包括:
三个主要层次:
- 应用层
:主线程负责初始化和管理
- 实时层
:1ms周期的实时控制线程
- 网络层
:EtherCAT总线和设备
硬件配置:
- EK1100
:总线耦合器(位置0,0)
- EL2004
:4通道数字输出模块(位置0,1)
技术特性:
使用SCHED_FIFO调度策略,优先级82
内存锁定确保实时性
1ms精确周期控制
5Hz频率的输出闪烁控制
这个程序是一个典型的工业自动化实时控制系统,使用EtherCAT协议实现高精度的设备控制。

此 C 语言示例程序演示了如何在 Xenomai 环境下,使用其 POSIX API 仿真层 (POSIX skin) 来实现一个基于 IgH EtherCAT Master 的硬实时用户空间应用。
程序的关键功能和流程如下:
系统初始化与配置:
main函数执行标准的 EtherCAT 初始化流程,包括请求主站、创建域、配置从站和 PDO、激活主站等。这些步骤与非 Xenomai 环境下的 IgH 应用完全相同。同时,它也设置了信号处理和内存锁定。创建 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, ¶m); // 应用上面设置的调度参数(优先级)。 // 核心:调用 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; // 程序正常退出} /****************************************************************************/
2385

被折叠的 条评论
为什么被折叠?



