【IgH EtherCAT】一个单线程、用户空间的 EtherCAT 实时控制示例

SVG图总结了程序的整体架构,包括:

主要组件

  • EtherCAT主站

    :负责通信协议和数据传输

  • 域(Domain1)

    :管理PDO映射和过程数据

  • 实时调度

    :SCHED_FIFO调度和内存管理

  • 过程数据处理

    :传感器读取和输出控制

从站设备

  • EK1100

    :总线耦合器,提供以太网接口

  • EL2032

    :数字输出模块,控制LED闪烁

  • EL3102

    :2通道模拟输入模块,读取传感器数据

  • EL4102

    :2通道模拟输出模块,输出控制信号

实时循环流程:每1ms执行一次的6步处理流程,确保确定性的实时控制性能。

这个程序是一个典型的EtherCAT实时控制应用,具有高精度定时、确定性通信和实时数据处理能力。

序列图展示了EtherCAT程序的执行流程,包括:

  1. 初始化阶段

    :请求主站、创建域、配置从站设备

  2. 配置阶段

    :设置PDO映射、激活主站、配置实时调度

  3. 循环执行阶段

    :1MHz频率的实时周期任务

这是一个单线程、用户空间的 EtherCAT 实时控制示例。它不使用多线程(如 Pthread)或专门的实时操作系统扩展(如 Xenomai、RTAI),而是依赖于标准 Linux 提供的实时特性(通常需要 PREEMPT_RT 内核补丁才能达到硬实时效果)来实现其功能。

核心架构与流程:

  1. 单线程模型

    :整个程序运行在 main 函数所在的单个线程中。没有创建任何新的线程。

  2. 实时化配置

    :在进入主循环之前,程序通过一系列系统调用将自己“实时化”:

  • 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, &param) == -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; // 返回程序最终的退出码}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值