EtherCAT主站程序代码详解

2023-12-14 09:50:02

简介

EtherCAT主站是用于控制EtherCAT网络的中央组件,负责协调和管理整个EtherCAT网络上的从站设备。主站与从站之间通过实时以太网通信,形成了一个高性能、低延迟的实时控制网络。

主要功能和责任:

  1. 实时通信: 主站负责在EtherCAT网络上实现实时通信。它与每个从站建立通信,通过EtherCAT协议实时传输控制信息和数据。

  2. 网络配置: 主站负责配置EtherCAT网络的拓扑结构。它需要了解整个网络上从站设备的位置、类型和通信参数。

  3. 同步控制: 主站确保网络中所有从站设备的时钟同步,以便实现高精度的同步控制。

  4. 数据处理: 主站负责处理从站发送的实时数据,并将控制指令传送到各个从站。

  5. 错误处理: 主站能够检测并处理网络中的错误。它可以监控通信故障、从站错误以及其他可能影响系统性能的问题。

本文介绍EtherLab 公司开发IgH(EtherLab IgH)主站程序案例IgH主站程序案例

一、头文件

#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h> /* clock_gettime() */
#include <sys/mman.h> /* mlockall() */
#include <sched.h> /* sched_setscheduler() */

#include "ecrt.h"

二、宏定义

// 任务周期(以 ns 为单位)
#define PERIOD_NS   (1000000)

#define MAX_SAFE_STACK (8 * 1024) //保证安全访问而不会出现故障的最大堆栈大小

//常数
#define NSEC_PER_SEC (1000000000)
#define FREQUENCY (NSEC_PER_SEC / PERIOD_NS)
  1. PERIOD_NS: 定义了任务的周期,以纳秒(ns)为单位。在这里,周期被设置为 1000000 ns,即 1 毫秒。

  2. MAX_SAFE_STACK: 定义了最大的安全栈大小,单位是字节。在这里,最大安全栈大小被设置为 8 * 1024 字节,即 8 KB。这个值通常与实时系统中线程栈的大小相关,确保线程的栈不会溢出。

  3. NSEC_PER_SEC: 定义了一秒钟内的纳秒数,即 1000000000 ns。

  4. FREQUENCY: 定义了任务的执行频率,以 Hz 为单位。其用于配置实时任务的执行频率。

三、变量定义

?3.1?定义主站

static ec_master_t *master = NULL;
static ec_master_state_t master_state = {};
  • 声明了一个指向 ec_master_t 类型的指针 master,并初始化为 NULL
  • ec_master_t 通常代表 EtherCAT 主站的结构,用于表示主站的状态和配置信息。
  • 声明了一个 ec_master_state_t 类型的结构体变量 master_state,并初始化为零。
  • ec_master_state_t 通常包含了主站的当前状态信息,如主站是否启动、是否有错误等。

?3.2 定义域(domain)

static ec_domain_t *domain1 = NULL;
static ec_domain_state_t domain1_state = {};
  • 声明了一个指向 ec_domain_t 类型的指针 domain1,并初始化为 NULL
  • ec_domain_t 通常代表 EtherCAT 中的一个域,用于组织和管理从站的数据通信。
  • 声明了一个 ec_domain_state_t 类型的结构体变量 domain1_state,并初始化为零。
  • ec_domain_state_t 通常包含了域的当前状态信息,如域是否激活、是否有错误等。

?3.3 定义从站的配置

static ec_slave_config_t *sc_ana_in = NULL;
static ec_slave_config_state_t sc_ana_in_state = {};
  • 声明了一个指向 ec_slave_config_t 类型的指针 sc_ana_in,并初始化为 NULL
  • ec_slave_config_t 通常代表 EtherCAT 中的一个从站配置,包含了从站的地址、通信周期等信息。
  • 声明了一个 ec_slave_config_state_t 类型的结构体变量 sc_ana_in_state,并初始化为零。
  • ec_slave_config_state_t 通常包含了从站配置的当前状态信息,如从站是否准备好通信等。

3.4 定义过程数据

static uint8_t *domain1_pd = NULL;

domain1_pd?是用于指向 EtherCAT 域(ec_domain_t)的过程数据(process data)的缓冲区。在 EtherCAT 中,域是用于在主站和从站之间传输数据的逻辑单元。uint8_t 表示一个字节(8位),所以 domain1_pd 通常会被用来处理和存储域中的字节数据。

在 EtherCAT 应用程序中,会在初始化过程中动态分配内存给 domain1_pd,以便存储域的过程数据。这个指针会在 EtherCAT 通信周期内用于读取或写入从站的数据。

3.5 定义从站信息变量

3.5.1 定义从站的别名

#define BusCouplerPos  0, 0
#define DigOutSlavePos 0, 2
#define AnaInSlavePos  0, 3
#define AnaOutSlavePos 0, 4

这些宏定义了不同从站的位置信息,其中 0,0表示从站在总线上的第一个位置,第一个地址。

3.5.2 定义从站的信息

#define Beckhoff_EK1100 0x00000002, 0x044c2c52
#define Beckhoff_EL2004 0x00000002, 0x07d43052
#define Beckhoff_EL2032 0x00000002, 0x07f03052
#define Beckhoff_EL3152 0x00000002, 0x0c503052
#define Beckhoff_EL3102 0x00000002, 0x0c1e3052
#define Beckhoff_EL4102 0x00000002, 0x10063052

用于标识不同的硬件设备,包含从站产品的供应商ID,产品代码(注:要与从站配置文件中的定义一致

3.5.3 定义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;
  1. off_ana_in_status:表示模拟输入(Analog Input)状态的偏移量。

  2. off_ana_in_value:表示模拟输入值的偏移量。

  3. off_ana_out:表示模拟输出(Analog Output)的偏移量。

  4. off_dig_out:表示数字输出(Digital Output)的偏移量。

3.5.4 定义 EtherCAT 域(domain)中的对象

const static ec_pdo_entry_reg_t domain1_regs[] = {
    {AnaInSlavePos,  Beckhoff_EL3102, 0x3101, 1, &off_ana_in_status},
    {AnaInSlavePos,  Beckhoff_EL3102, 0x3101, 2, &off_ana_in_value},
    {AnaOutSlavePos, Beckhoff_EL4102, 0x3001, 1, &off_ana_out},
    {DigOutSlavePos, Beckhoff_EL2032, 0x3001, 1, &off_dig_out},
    {}
};

以{AnaInSlavePos, Beckhoff_EL3102, 0x3101, 1, &off_ana_in_status}为例

  • 对应于 EtherCAT 从站位置 AnaInSlavePos上的一个 PDO 条目。
  • 对应的硬件设备是 Beckhoff EL3102。
  • 对象字典索引为 0x3101
  • 子索引为 1
  • 偏移量 &off_ana_in_status 指向变量,用于表示该 PDO 条目的状态。

3.5.4 定义 PDO对象字典、结构和同步周期

static ec_pdo_entry_info_t el3102_pdo_entries[] = {
    {0x3101, 1,  8}, // channel 1 status
    {0x3101, 2, 16}, // channel 1 value
    {0x3102, 1,  8}, // channel 2 status
    {0x3102, 2, 16}, // channel 2 value
    {0x6401, 1, 16}, // channel 1 value (alt.)
    {0x6401, 2, 16}  // channel 2 value (alt.)
};

static ec_pdo_info_t el3102_pdos[] = {
    {0x1A00, 2, el3102_pdo_entries},
    {0x1A01, 2, el3102_pdo_entries + 2}
};

static ec_sync_info_t el3102_syncs[] = {
    {2, EC_DIR_OUTPUT},
    {3, EC_DIR_INPUT, 2, el3102_pdos},
    {0xff}
};
  1. el3102_pdo_entries[]

    • 这是一个包含 ec_pdo_entry_info_t 类型元素的数组,用于描述 PDO 条目的信息。
    • {0x3101, 1, 8} 表示第一个 PDO 条目,对象字典索引为 0x3101,子索引为 1,有 8 位(1 字节)的数据。
    • {0x3101, 2, 16} 表示第二个 PDO 条目,对象字典索引为 0x3101,子索引为 2,有 16 位(2 字节)的数据。
    • 后续的条目类似,描述了 EL3102 模块的状态和值的不同通道。
  2. el3102_pdos[]

    • 这是一个包含 ec_pdo_info_t 类型元素的数组,用于描述 PDO 配置信息。
    • {0x1A00, 2, el3102_pdo_entries} 表示第一个 PDO,对象字典索引为 0x1A00,有 2 个 PDO 条目。
    • {0x1A01, 2, el3102_pdo_entries + 2} 表示第二个 PDO,对象字典索引为 0x1A01,有 2 个 PDO 条目,其偏移量指向 el3102_pdo_entries 数组的第三个元素,即第三个通道的状态和值。
  3. el3102_syncs[]

    • 这是一个包含 ec_sync_info_t 类型元素的数组,用于描述同步信息。
    • {2, EC_DIR_OUTPUT} 表示第一个同步,其周期为 2 个 DCycle(同步周期),方向为输出。
    • {3, EC_DIR_INPUT, 2, el3102_pdos} 表示第二个同步,周期为 3 个 DCycle,方向为输入,包含了两个 PDO(通过 el3102_pdos 指定)。
    • {0xff} 表示同步信息数组的结束标志。

四、函数

4.1?检查 EtherCAT 域 domain1 的状态

void check_domain1_state(void)
{
    ec_domain_state_t ds;

    ecrt_domain_state(domain1, &ds);

    if (ds.working_counter != domain1_state.working_counter) {
        printf("Domain1: WC %u.\n", ds.working_counter);
    }
    if (ds.wc_state != domain1_state.wc_state) {
        printf("Domain1: State %u.\n", ds.wc_state);
    }

    domain1_state = ds;
}

此函数执行以下操作:?

  1. 获取域状态:

    使用 ecrt_domain_state 函数获取 domain1 的当前状态,将状态信息存储在 ec_domain_state_t 类型的结构体变量 ds 中。
  2. 比较状态变化:

    • 检查当前状态中的 working_counter 是否与之前存储的 domain1_state.working_counter 不同,如果不同,则打印相关信息表示 working_counter 的变化。
    • 检查当前状态中的 wc_state 是否与之前存储的 domain1_state.wc_state 不同,如果不同,则打印相关信息表示 wc_state 的变化。
  3. 更新域状态:

    将当前的域状态 ds 赋值给 domain1_state,以备下一次函数调用时进行比较。

? ? ? ? 此函数?来监测计数器WKC的状态,计数器?于数据帧的操作计数,放在帧内初始值为 0,成功写?+2,成功读取+1,所以会有0(发送、读取失败)、2(读取失败)、3(成功)三种情 况,频率过?会导致读取、写?失败,?般认为错误率低于0.1%即可

4.2 检查 EtherCAT 主站 master 的状态

void check_master_state(void)
{
    ec_master_state_t ms;

    ecrt_master_state(master, &ms);

    if (ms.slaves_responding != master_state.slaves_responding) {
        printf("%u slave(s).\n", ms.slaves_responding);
    }
    if (ms.al_states != master_state.al_states) {
        printf("AL states: 0x%02X.\n", ms.al_states);
    }
    if (ms.link_up != master_state.link_up) {
        printf("Link is %s.\n", ms.link_up ? "up" : "down");
    }

    master_state = ms;
}

这个函数执行以下操作:

  1. 获取主站状态:

    使用 ecrt_master_state 函数获取 master 的当前状态,将状态信息存储在 ec_master_state_t 类型的结构体变量 ms 中。
  2. 比较状态变化:

    • 检查当前状态中的 slaves_responding 是否与之前存储的 master_state.slaves_responding 不同,如果不同,则打印相关信息表示连接的从站数量的变化。
    • 检查当前状态中的 al_states 是否与之前存储的 master_state.al_states 不同,如果不同,则打印相关信息表示主站的 AL(Application Layer) 状态的变化。
    • 检查当前状态中的 link_up 是否与之前存储的 master_state.link_up 不同,如果不同,则打印相关信息表示主站连接的物理链路状态的变化。
  3. 更新主站状态:

    将当前的主站状态 ms 赋值给 master_state,以备下一次函数调用时进行比较。

4.3 检查与 EtherCAT 从站配置状态

void check_slave_config_states(void)
{
    ec_slave_config_state_t s;

    ecrt_slave_config_state(sc_ana_in, &s);

    if (s.al_state != sc_ana_in_state.al_state) {
        printf("AnaIn: State 0x%02X.\n", s.al_state);
    }
    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;
}

这个函数执行以下操作:

  1. 获取从站配置状态:

    使用 ecrt_slave_config_state 函数获取与 sc_ana_in 关联的从站配置的当前状态,将状态信息存储在 ec_slave_config_state_t 类型的结构体变量 s 中。
  2. 比较状态变化:

    • 检查当前状态中的 al_state 是否与之前存储的 sc_ana_in_state.al_state 不同,如果不同,则打印相关信息表示从站配置的 AL(Application Layer) 状态的变化。
    • 检查当前状态中的 online 是否与之前存储的 sc_ana_in_state.online 不同,如果不同,则打印相关信息表示从站配置的在线状态的变化。
    • 检查当前状态中的 operational 是否与之前存储的 sc_ana_in_state.operational 不同,如果不同,则打印相关信息表示从站配置的操作状态的变化。
  3. 更新从站配置状态:

    将当前的从站配置状态 s 赋值给 sc_ana_in_state,以备下一次函数调用时进行比较。

4.4 周期性任务

void cyclic_task()
{
    // receive process data
    ecrt_master_receive(master);
    ecrt_domain_process(domain1);

    // check process data state
    check_domain1_state();

    if (counter) {
        counter--;
    } else { // do this at 1 Hz
        counter = FREQUENCY;

        // calculate new process data
        blink = !blink;

        // check for master state (optional)
        check_master_state();

        // check for slave configuration state(s) (optional)
        check_slave_config_states();
    }

#if 0
    // read process data
    printf("AnaIn: state %u value %u\n",
            EC_READ_U8(domain1_pd + off_ana_in_status),
            EC_READ_U16(domain1_pd + off_ana_in_value));
#endif

#if 1
    // write process data
    EC_WRITE_U8(domain1_pd + off_dig_out, blink ? 0x06 : 0x09);
#endif

    // send process data
    ecrt_domain_queue(domain1);
    ecrt_master_send(master);
}

这个函数执行以下操作:

  1. 接收和处理过程数据:

    • 使用 ecrt_master_receive 函数接收主站的过程数据。
    • 使用 ecrt_domain_process 函数处理域 domain1 中的过程数据。
  2. 检查过程数据状态:

    调用 check_domain1_state 函数,检查并打印域 domain1 的状态变化。
  3. 周期性任务执行:

    • 使用 counter 计数器来确定何时执行每秒一次的任务。
    • 在每秒执行一次的任务中,计算新的过程数据,然后检查主站和从站配置的状态(这两步是可选的)。
  4. 读写过程数据:

    在每个循环中,通过 EC_WRITE_U8 函数写入一个字节的过程数据,根据 blink 的值设置不同的值。
  5. 发送过程数据:

    • 使用 ecrt_domain_queue 将域 domain1 中的过程数据放入队列。
    • 使用 ecrt_master_send 将过程数据发送到主站。

这个函数通常在 EtherCAT 主循环中被调用,用于处理周期性的 EtherCAT 任务。在每个循环中,它接收、处理、检查状态,并在每秒执行一次的任务中计算新的过程数据。最后,它写入和发送过程数据到 EtherCAT 从站。

4.5 预先分配和初始化堆栈空间

void stack_prefault(void)
{
    unsigned char dummy[MAX_SAFE_STACK];

    memset(dummy, 0, MAX_SAFE_STACK);
}

这个函数的作用是在程序启动时,通过创建一个名为 dummy 的数组,其大小为 MAX_SAFE_STACK,来分配一块固定大小的堆栈空间。然后,使用 memset 函数将整个数组初始化为零。

在实时控制系统或者需要确保稳定性和可预测性的应用中,预先分配和初始化堆栈空间是为了避免在程序运行过程中动态分配堆栈空间的不确定性。这可以帮助确保在程序执行的关键阶段,堆栈的空间分配是可靠且可控的。

4.6 主函数

int main(int argc, char **argv)
{
    ec_slave_config_t *sc;
    struct timespec wakeup_time;
    int ret = 0;

    master = ecrt_request_master(0);
    if (!master) {
        return -1;
    }

    domain1 = ecrt_master_create_domain(master);
    if (!domain1) {
        return -1;
    }

    if (!(sc_ana_in = ecrt_master_slave_config(
                    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)) {
        fprintf(stderr, "Failed to configure PDOs.\n");
        return -1;
    }

    if (!(sc = ecrt_master_slave_config(
                    master, AnaOutSlavePos, Beckhoff_EL4102))) {
        fprintf(stderr, "Failed to get slave configuration.\n");
        return -1;
    }

    if (ecrt_slave_config_pdos(sc, EC_END, el4102_syncs)) {
        fprintf(stderr, "Failed to configure PDOs.\n");
        return -1;
    }

    if (!(sc = ecrt_master_slave_config(
                    master, DigOutSlavePos, Beckhoff_EL2032))) {
        fprintf(stderr, "Failed to get slave configuration.\n");
        return -1;
    }

    if (ecrt_slave_config_pdos(sc, EC_END, el2004_syncs)) {
        fprintf(stderr, "Failed to configure PDOs.\n");
        return -1;
    }

    // Create configuration for bus coupler
    sc = ecrt_master_slave_config(master, BusCouplerPos, Beckhoff_EK1100);
    if (!sc) {
        return -1;
    }

    if (ecrt_domain_reg_pdo_entry_list(domain1, domain1_regs)) {
        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 = {};
    param.sched_priority = sched_get_priority_max(SCHED_FIFO);

    printf("Using priority %i.", param.sched_priority);
    if (sched_setscheduler(0, SCHED_FIFO, &param) == -1) {
        perror("sched_setscheduler failed");
    }

    /* Lock memory */

    if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
        fprintf(stderr, "Warning: Failed to lock memory: %s\n",
                strerror(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 */
    wakeup_time.tv_nsec = 0;

    while (1) {
        ret = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME,
                &wakeup_time, NULL);
        if (ret) {
            fprintf(stderr, "clock_nanosleep(): %s\n", strerror(ret));
            break;
        }

        cyclic_task();

        wakeup_time.tv_nsec += PERIOD_NS;
        while (wakeup_time.tv_nsec >= NSEC_PER_SEC) {
            wakeup_time.tv_nsec -= NSEC_PER_SEC;
            wakeup_time.tv_sec++;
        }
    }

    return ret;
}

这个主函数的关键部分包括:

  1. EtherCAT 主站和域的创建:

    • 使用 ecrt_request_master 请求 EtherCAT 主站。
    • 使用 ecrt_master_create_domain 创建 EtherCAT 域。
  2. 从站配置:

    • 使用 ecrt_master_slave_config 配置各个从站,包括 AnaInSlavePos、AnaOutSlavePos、DigOutSlavePos 和 BusCouplerPos。
    • 使用 ecrt_slave_config_pdos 配置每个从站的 PDO。
  3. PDO 条目的注册:

    • 使用 ecrt_domain_reg_pdo_entry_list 注册域的 PDO 条目。
  4. EtherCAT 主站的激活:

    • 使用 ecrt_master_activate 激活 EtherCAT 主站。
  5. 实时任务的设置:

    • 设置实时任务的优先级。
    • 锁定内存以提高实时性能。
  6. 主循环:

    • 进入主循环,其中通过 clock_nanosleep 在每个周期内休眠,等待下一个周期的开始。
    • 在每个周期内,执行 cyclic_task 函数来处理 EtherCAT 主循环中的周期性任务。

此程序实现了一个基本的 EtherCAT 主循环,用于实时控制应用。在主循环中,配置了 EtherCAT 主站、从站和域,执行周期性任务,同时使用实时任务的优先级和内存锁定来确保对实时性的要求。

?

文章来源:https://blog.csdn.net/m0_56451176/article/details/134912244
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。