
SVG图总结了程序的整体架构,包括:
主要组件:
- EtherCAT主站
:负责通信协议和数据传输
- 域(Domain1)
:管理PDO映射和过程数据
- 实时调度
:SCHED_FIFO调度和内存管理
- 过程数据处理
:传感器读取和输出控制
从站设备:
- EK1100
:总线耦合器,提供以太网接口
- EL2032
:数字输出模块,控制LED闪烁
- EL3102
:2通道模拟输入模块,读取传感器数据
- EL4102
:2通道模拟输出模块,输出控制信号
实时循环流程:每1ms执行一次的6步处理流程,确保确定性的实时控制性能。
这个程序是一个典型的EtherCAT实时控制应用,具有高精度定时、确定性通信和实时数据处理能力。

序列图展示了EtherCAT程序的执行流程,包括:
- 初始化阶段
:请求主站、创建域、配置从站设备
- 配置阶段
:设置PDO映射、激活主站、配置实时调度
- 循环执行阶段
:1MHz频率的实时周期任务

这是一个单线程、用户空间的 EtherCAT 实时控制示例。它不使用多线程(如 Pthread)或专门的实时操作系统扩展(如 Xenomai、RTAI),而是依赖于标准 Linux 提供的实时特性(通常需要 PREEMPT_RT 内核补丁才能达到硬实时效果)来实现其功能。
核心架构与流程:
- 单线程模型
:整个程序运行在
main函数所在的单个线程中。没有创建任何新的线程。 - 实时化配置
:在进入主循环之前,程序通过一系列系统调用将自己“实时化”:
sched_setscheduler(0, SCHED_FIFO, ...): 将当前进程的调度策略设置为
SCHED_FIFO(先入先出),并赋予其该策略下所能获得的最高优先级。这使得它能抢占几乎所有其他非实时进程。mlockall(MCL_CURRENT | MCL_FUTURE): 锁定进程的所有内存(当前和未来分配的),防止它们被交换到磁盘,从而避免了因缺页中断(Page Fault)带来的巨大延迟。
stack_prefault(): 通过主动访问一大块栈内存,强制内核提前为栈分配物理内存页,进一步减少了实时循环中发生缺页中断的可能性。
- EtherCAT 配置
:程序按标准流程配置 IgH EtherCAT 主站。它为多个从站(包括模拟量输入、模拟量输出、数字量输出和总线耦合器)创建配置,设置 PDO 映射,并将需要周期性交换的 PDO 注册到一个域中。
- 实时主循环
:
程序使用一个
while(1)无限循环作为其实时主循环。- 精确周期控制
:循环的核心是
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...)。它使程序精确地睡眠到下一个周期的绝对时间点。使用CLOCK_MONOTONIC(单调时钟)和TIMER_ABSTIME(绝对时间)模式是实现无累积误差的精确周期的关键。 - 任务执行
:每次从
clock_nanosleep唤醒后,程序立即调用cyclic_task()函数。
- 周期性任务 (
cyclic_task):
在这个函数内部,执行一个完整的 EtherCAT I/O 周期:接收数据 -> 处理数据 -> 执行应用逻辑 -> 准备发送数据 -> 发送数据。
- 应用逻辑
:包含一个简单的闪烁逻辑 (
blink),每秒翻转一次状态,并根据状态向数字量输出模块写入不同的值。 - 状态监控
:它还会以较低的频率(1Hz)检查并打印主站、域和特定从站的状态,用于监控和调试。
- 程序终止
:这个版本的示例代码没有实现优雅的退出机制(如信号处理)。它会一直运行,直到被外部强制杀死(例如,在终端中按
Ctrl+C,但这不会触发清理流程)。 总结:此示例展示了在具备实时能力的 Linux 系统上,如何仅使用标准系统调用,将一个单线程程序提升为具有确定性周期的实时应用。它强调了通过设置调度策略、锁定内存和精确延时来实现实时性的技术,是理解 Linux 软实时/硬实时编程的基础。

#include <errno.h> // 包含C标准库头文件,用于错误码处理#include <signal.h> // 包含信号处理头文件,虽然在此代码中未直接使用,但通常用于实时应用#include <stdio.h> // 包含标准输入输出头文件#include <string.h> // 包含字符串处理头文件#include <sys/resource.h> // 包含系统资源操作头文件,虽然在此代码中未直接使用#include <sys/time.h> // 包含时间相关头文件#include <sys/types.h> // 包含基本系统数据类型头文件#include <unistd.h> // 包含 POSIX 标准 API 头文件#include <time.h> /* clock_gettime() */ // 包含时间相关头文件,并注释说明其用途是 clock_gettime()#include <sys/mman.h> /* mlockall() */ // 包含内存管理声明头文件,并注释说明其用途是 mlockall()#include <sched.h> /* sched_setscheduler() */ // 包含调度策略头文件,并注释说明其用途是 sched_setscheduler()/****************************************************************************/ // 分隔注释#include "ecrt.h" // 包含 IgH EtherCAT 主站的核心应用层 API 头文件/****************************************************************************/ // 分隔注释/** Task period in ns. */ // Doxygen 风格的注释:任务周期(纳秒)#define PERIOD_NS (1000000) // 宏定义:周期时间为 1,000,000 纳秒 (即 1 毫秒)#define MAX_SAFE_STACK (8 * 1024) /* The maximum stack size which is // 宏定义:最大安全栈大小为 8 KB guranteed safe to access without // 注释:保证可以安全访问而不会产生页面错误的栈大小 faulting */ // 注释结束/****************************************************************************/ // 分隔注释/* Constants */ // 区域注释:常量定义#define NSEC_PER_SEC (1000000000) // 宏定义:每秒的纳秒数#define FREQUENCY (NSEC_PER_SEC / PERIOD_NS) // 宏定义:频率,计算为 1,000,000,000 / 1,000,000 = 1000 Hz/****************************************************************************/ // 分隔注释// 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 = {}; // 声明一个静态的从站配置状态结构体变量/****************************************************************************/ // 分隔注释// process data // 区域注释:过程数据相关static uint8_t *domain1_pd = NULL; // 声明一个静态的指向过程数据内存区域的指针#define BusCouplerPos 0, 0 // 宏定义:总线耦合器的位置#define DigOutSlavePos 0, 2 // 宏定义:数字量输出从站的位置#define AnaInSlavePos 0, 3 // 宏定义:模拟量输入从站的位置#define AnaOutSlavePos 0, 4 // 宏定义:模拟量输出从站的位置#define Beckhoff_EK1100 0x00000002, 0x044c2c52 // 宏定义:倍福 EK1100 的厂商/产品ID#define Beckhoff_EL2004 0x00000002, 0x07d43052 // 宏定义:倍福 EL2004 的厂商/产品ID (此处未在domain1_regs中使用)#define Beckhoff_EL2032 0x00000002, 0x07f03052 // 宏定义:倍福 EL2032 的厂商/产品ID#define Beckhoff_EL3152 0x00000002, 0x0c503052 // 宏定义:倍福 EL3152 的厂商/产品ID (此处未使用)#define Beckhoff_EL3102 0x00000002, 0x0c1e3052 // 宏定义:倍福 EL3102 的厂商/产品ID#define Beckhoff_EL4102 0x00000002, 0x10063052 // 宏定义:倍福 EL4102 的厂商/产品ID// offsets for PDO entries // 注释:PDO条目的偏移量static unsigned int off_ana_in_status; // 静态无符号整型,存储模拟量输入状态的偏移量static unsigned int off_ana_in_value; // 静态无符号整型,存储模拟量输入值的偏移量static unsigned int off_ana_out; // 静态无符号整型,存储模拟量输出的偏移量static unsigned int off_dig_out; // 静态无符号整型,存储数字量输出的偏移量const static ec_pdo_entry_reg_t domain1_regs[] = { // 定义一个静态常量数组,用于注册需要映射到域的PDO条目 {AnaInSlavePos, Beckhoff_EL3102, 0x3101, 1, &off_ana_in_status}, // 注册 EL3102 的状态 PDO {AnaInSlavePos, Beckhoff_EL3102, 0x3101, 2, &off_ana_in_value}, // 注册 EL3102 的值 PDO {AnaOutSlavePos, Beckhoff_EL4102, 0x3001, 1, &off_ana_out}, // 注册 EL4102 的输出 PDO {DigOutSlavePos, Beckhoff_EL2032, 0x3001, 1, &off_dig_out}, // 注册 EL2032 的输出 PDO {} // 数组结束标志};static unsigned int counter = 0; // 静态无符号整型,用作通用计数器static unsigned int blink = 0; // 静态无符号整型,用作闪烁标志/****************************************************************************/ // 分隔注释// Analog in -------------------------- // 区域注释:模拟量输入从站(EL3102)的配置数据static const ec_pdo_entry_info_t el3102_pdo_entries[] = { // 定义 EL3102 的 PDO 条目信息 {0x3101, 1, 8}, // channel 1 status (通道1 状态,8位) {0x3101, 2, 16}, // channel 1 value (通道1 值,16位) {0x3102, 1, 8}, // channel 2 status (通道2 状态,8位) {0x3102, 2, 16}, // channel 2 value (通道2 值,16位) {0x6401, 1, 16}, // channel 1 value (alt.) (备用通道1 值,16位) {0x6401, 2, 16} // channel 2 value (alt.) (备用通道2 值,16位)};static const ec_pdo_info_t el3102_pdos[] = { // 定义 EL3102 的 PDO 信息 (将条目分组) {0x1A00, 2, el3102_pdo_entries}, // TxPDO 0x1A00 包含2个条目 {0x1A01, 2, el3102_pdo_entries + 2} // TxPDO 0x1A01 包含2个条目};static const ec_sync_info_t el3102_syncs[] = { // 定义 EL3102 的同步管理器配置 {2, EC_DIR_OUTPUT}, // Sync Manager 2 是一个空的输出 SM {3, EC_DIR_INPUT, 2, el3102_pdos}, // Sync Manager 3 是输入 SM,关联2个 PDO {0xff} // 结束标志};// Analog out ------------------------- // 区域注释:模拟量输出从站(EL4102)的配置数据static const ec_pdo_entry_info_t el4102_pdo_entries[] = { // 定义 EL4102 的 PDO 条目信息 {0x3001, 1, 16}, // channel 1 value (通道1 值,16位) {0x3002, 1, 16}, // channel 2 value (通道2 值,16位)};static const ec_pdo_info_t el4102_pdos[] = { // 定义 EL4102 的 PDO 信息 {0x1600, 1, el4102_pdo_entries}, // RxPDO 0x1600 包含1个条目 {0x1601, 1, el4102_pdo_entries + 1} // RxPDO 0x1601 包含1个条目};static const ec_sync_info_t el4102_syncs[] = { // 定义 EL4102 的同步管理器配置 {2, EC_DIR_OUTPUT, 2, el4102_pdos}, // Sync Manager 2 是输出 SM,关联2个 PDO {3, EC_DIR_INPUT}, // Sync Manager 3 是一个空的输入 SM {0xff} // 结束标志};// Digital out ------------------------ // 区域注释:数字量输出从站(EL2032/EL2004)的配置数据static const ec_pdo_entry_info_t el2004_channels[] = { // 定义数字量输出的 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 const ec_pdo_info_t el2004_pdos[] = { // 定义数字量输出的 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 const ec_sync_info_t el2004_syncs[] = { // 定义数字量输出的同步管理器配置 {0, EC_DIR_OUTPUT, 4, el2004_pdos}, // Sync Manager 0 是输出 SM,关联4个 PDO {1, EC_DIR_INPUT}, // Sync Manager 1 是一个空的输入 SM {0xff} // 结束标志};/****************************************************************************/ // 分隔注释void check_domain1_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 printf("Domain1: WC %u.\n", ds.working_counter); // 如果不一致,打印新的WC值 } if (ds.wc_state != domain1_state.wc_state) { // 比较当前工作计数器状态与上次记录的状态 printf("Domain1: State %u.\n", ds.wc_state); // 如果不一致,打印新的WC状态 } domain1_state = ds; // 将当前状态赋值给全局变量,用于下次比较}/****************************************************************************/ // 分隔注释void check_master_state(void) // 定义一个函数,用于检查并打印主站的状态变化{ ec_master_state_t ms; // 声明一个局部的主站状态结构体变量 ecrt_master_state(master, &ms); // 调用 ecrt API,获取主站的当前状态 if (ms.slaves_responding != master_state.slaves_responding) { // 比较当前响应的从站数量与上次记录的数量 printf("%u slave(s).\n", ms.slaves_responding); // 如果不一致,打印新的从站数量 } if (ms.al_states != master_state.al_states) { // 比较当前应用层(AL)状态与上次记录的状态 printf("AL states: 0x%02X.\n", ms.al_states); // 如果不一致,以十六进制格式打印新的AL状态 } if (ms.link_up != master_state.link_up) { // 比较当前链路连接状态与上次记录的状态 printf("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; // 声明一个局部的从站配置状态结构体变量 ecrt_slave_config_state(sc_ana_in, &s); // 调用 ecrt API,获取模拟量输入从站的当前配置状态 if (s.al_state != sc_ana_in_state.al_state) { // 比较当前应用层状态与上次记录的状态 printf("AnaIn: State 0x%02X.\n", s.al_state); // 如果不一致,打印新的AL状态 } if (s.online != sc_ana_in_state.online) { // 比较当前在线状态与上次记录的状态 printf("AnaIn: %s.\n", s.online ? "online" : "offline"); // 如果不一致,打印在线/离线状态 } if (s.operational != sc_ana_in_state.operational) { // 比较当前操作状态与上次记录的状态 printf("AnaIn: %soperational.\n", s.operational ? "" : "Not "); // 如果不一致,打印是否进入操作状态 } sc_ana_in_state = s; // 将当前状态赋值给全局变量,用于下次比较}/****************************************************************************/ // 分隔注释void cyclic_task() // 定义周期性任务函数,此函数将在主循环中被反复调用{ // receive process data // 注释:接收过程数据 ecrt_master_receive(master); // 从网络接口接收数据帧 ecrt_domain_process(domain1); // 处理域的数据,将数据解包到过程数据内存区 // check process data state // 注释:检查过程数据状态 check_domain1_state(); // 调用函数检查并打印域的状态变化 if (counter) { // 如果计数器不为0 counter--; // 计数器减1 } else { // 如果计数器为0 (即每秒执行一次,因为 counter 被初始化为 FREQUENCY) counter = FREQUENCY; // 重置计数器为频率值 (1000) // calculate new process data // 注释:计算新的过程数据 blink = !blink; // 对 blink 变量取反,实现闪烁逻辑 // check for master state (optional) // 注释:检查主站状态(可选) check_master_state(); // 调用函数检查并打印主站的状态变化 // check for slave configuration state(s) (optional) // 注释:检查从站配置状态(可选) check_slave_config_states(); // 调用函数检查并打印特定从站的状态变化 }#if 0 // 预处理指令,条件为0,此代码块不被编译 // read process data // 注释:读取过程数据 printf("AnaIn: state %u value %u\n", // 打印模拟量输入的状态和值 EC_READ_U8(domain1_pd + off_ana_in_status), // 读取8位状态 EC_READ_U16(domain1_pd + off_ana_in_value)); // 读取16位值#endif // 结束 #if 0#if 1 // 预处理指令,条件为1,此代码块被编译 // write process data // 注释:写入过程数据 EC_WRITE_U8(domain1_pd + off_dig_out, blink ? 0x06 : 0x09); // 将数据写入过程数据区,控制数字量输出 (输出 0b0110 或 0b1001)#endif // 结束 #if 1 // send process data // 注释:发送过程数据 ecrt_domain_queue(domain1); // 将要发送的域数据放入发送队列 ecrt_master_send(master); // 将数据帧发送到 EtherCAT 总线}/****************************************************************************/ // 分隔注释void stack_prefault(void) // 定义一个函数,用于预先分配和访问栈内存{ unsigned char dummy[MAX_SAFE_STACK]; // 在栈上声明一个 8KB 的数组 memset(dummy, 0, MAX_SAFE_STACK); // 对这个数组进行写操作(清零),这会强制操作系统分配物理内存页面,防止在实时循环中因缺页中断(Page Fault)而产生抖动}/****************************************************************************/ // 分隔注释int main(int argc, char **argv) // C程序的入口主函数{ ec_slave_config_t *sc; // 声明一个局部的从站配置对象指针 struct timespec wakeup_time; // 声明一个 timespec 结构体,用于存储下一个唤醒的绝对时间 int ret = 0; // 声明一个整型变量,用于存储函数返回值 master = ecrt_request_master(0); // 请求索引为0的 EtherCAT 主站实例 if (!master) { // 检查主站请求是否成功 return -1; // 如果失败,程序返回-1并退出 } domain1 = ecrt_master_create_domain(master); // 在主站上创建一个过程数据域 if (!domain1) { // 检查域创建是否成功 return -1; // 如果失败,程序返回-1并退出 } // 为 EL3102 模拟量输入从站创建配置 if (!(sc_ana_in = ecrt_master_slave_config( // 为位置 AnaInSlavePos 的 EL3102 创建配置 master, AnaInSlavePos, Beckhoff_EL3102))) { fprintf(stderr, "Failed to get slave configuration.\n"); // 如果失败,打印错误信息 return -1; // 并退出 } printf("Configuring PDOs...\n"); // 打印提示信息 if (ecrt_slave_config_pdos(sc_ana_in, EC_END, el3102_syncs)) { // 为 EL3102 配置 PDO 和同步管理器 fprintf(stderr, "Failed to configure PDOs.\n"); // 如果失败,打印错误信息 return -1; // 并退出 } // 为 EL4102 模拟量输出从站创建配置 if (!(sc = ecrt_master_slave_config( // 为位置 AnaOutSlavePos 的 EL4102 创建配置 master, AnaOutSlavePos, Beckhoff_EL4102))) { fprintf(stderr, "Failed to get slave configuration.\n"); // 如果失败,打印错误信息 return -1; // 并退出 } if (ecrt_slave_config_pdos(sc, EC_END, el4102_syncs)) { // 为 EL4102 配置 PDO fprintf(stderr, "Failed to configure PDOs.\n"); // 如果失败,打印错误信息 return -1; // 并退出 } // 为 EL2032 数字量输出从站创建配置 if (!(sc = ecrt_master_slave_config( // 为位置 DigOutSlavePos 的 EL2032 创建配置 master, DigOutSlavePos, Beckhoff_EL2032))) { fprintf(stderr, "Failed to get slave configuration.\n"); // 如果失败,打印错误信息 return -1; // 并退出 } if (ecrt_slave_config_pdos(sc, EC_END, el2004_syncs)) { // 为 EL2032 配置 PDO fprintf(stderr, "Failed to configure PDOs.\n"); // 如果失败,打印错误信息 return -1; // 并退出 } // Create configuration for bus coupler // 注释:为总线耦合器创建配置 sc = ecrt_master_slave_config(master, BusCouplerPos, Beckhoff_EK1100); // 为 EK1100 创建配置 if (!sc) { // 检查配置创建是否成功 return -1; // 如果失败则退出 } if (ecrt_domain_reg_pdo_entry_list(domain1, domain1_regs)) { // 将定义的 PDO 注册列表注册到域 fprintf(stderr, "PDO entry registration failed!\n"); // 如果注册失败,打印错误信息 return -1; // 并退出 } printf("Activating master...\n"); // 打印提示信息 if (ecrt_master_activate(master)) { // 激活主站,开始总线通信 return -1; // 如果激活失败则退出 } if (!(domain1_pd = ecrt_domain_data(domain1))) { // 获取域的过程数据内存区指针 return -1; // 如果获取失败则退出 } /* Set priority */ // 注释:设置优先级 struct sched_param param = {}; // 声明一个 sched_param 结构体 param.sched_priority = sched_get_priority_max(SCHED_FIFO); // 获取 SCHED_FIFO 调度策略下的最高可用优先级 printf("Using priority %i.\n", param.sched_priority); // 打印正在使用的优先级 if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) { // 将当前进程(0)的调度策略设置为 SCHED_FIFO 并应用最高优先级 perror("sched_setscheduler failed"); // 如果失败,打印系统错误信息 } /* Lock memory */ // 注释:锁定内存 if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) { // 锁定当前和未来所有分配的内存 fprintf(stderr, "Warning: Failed to lock memory: %s\n", // 如果失败,打印警告信息 strerror(errno)); // 将 errno 转换为可读字符串 } stack_prefault(); // 调用函数,预分配和访问栈内存,防止缺页中断 printf("Starting RT task with dt=%u ns.\n", PERIOD_NS); // 打印实时任务的周期信息 clock_gettime(CLOCK_MONOTONIC, &wakeup_time); // 获取单调时钟的当前时间 wakeup_time.tv_sec += 1; /* start in future */ // 将唤醒时间的秒数加1,使得循环在1秒后开始 wakeup_time.tv_nsec = 0; // 将纳秒部分清零 while (1) { // 进入一个无限循环,作为实时主循环 ret = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, // 使用单调时钟,以绝对时间模式睡眠 &wakeup_time, NULL); // 精确睡眠直到 wakeup_time if (ret) { // 如果睡眠被中断 fprintf(stderr, "clock_nanosleep(): %s\n", strerror(ret)); // 打印错误信息 break; // 跳出循环 } cyclic_task(); // 调用周期性任务函数,执行 EtherCAT 通信和应用逻辑 wakeup_time.tv_nsec += PERIOD_NS; // 计算下一个周期的唤醒时间(纳秒部分) while (wakeup_time.tv_nsec >= NSEC_PER_SEC) { // 处理纳秒进位到秒 wakeup_time.tv_nsec -= NSEC_PER_SEC; // 纳秒部分减去1秒 wakeup_time.tv_sec++; // 秒部分加1 } } return ret; // 返回程序最终的退出码}

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



