【原创】EtherCAT主站IgH解析(一)--主站初始化、状态机与EtherCAT报文

版权声明:本文为本文为博主原创文章,转载请注明出处。如有问题,欢迎指正。博客地址:https://www.cnblogs.com/wsg1100/

0 获取源码

IgH EtherCAT Master现已迁移到gitlab:https://gitlab.com/etherlab.org/ethercat 可以使用以下命令克隆存储库:

git clone https://gitlab.com/etherlab.org/ethercat.git
git checkout stable-1.5

上面的是igh官方的仓库,下面是其他Ethercat主站:
https://github.com/ribalda/ethercat 基于官方,功能更为全面的igh etehrcat主站。
https://github.com/leducp/KickCAT 一个C++写的全新etehrcat主站,目前功能不完善。
https://github.com/ethercrab-rs/ethercrab 一个纯rust语言编写的全新etehrcat主站,目前功能不完善。

本文主要讲igh。

1 启动脚本

igh通过脚本来启动,可以是systemd、init.d或sysconfig。分别位于源码script目录下:

image-20210219114206819

对于systemd方式,编译时由ethercat.service.in文件生成ethercat.serviceethercat.service中指定了执行文件为ethercatctl.ethercatctl文件由``ethercatctl.in`生成。init.d和sysconfig类似,都是生成一个可执行脚本,且脚本完成的工作一致,主要完成加载主站模块、网卡驱动、给主站内核模块传递参数、卸载模块等操作。

ethercat.conf共同的配置文件,配置主站使用的网卡、驱动等信息。下面看脚本start和stop所做的工作。

1.1 start

  1. 加载ec_master.ko

    模块参数:

    • main_devices :主网卡MAC地址,多个main_devices 表示创建多个主站,MAC参数个数master_count。
    • backup_devices :备用网卡MAC地址,多个backup_devices 表示创建多个主站,MAC参数个数backup_count。
    • debug_level :调试level,调试信息输出级别。
    • eoe_interfaces eoe接口,eoe_count表示eoe_interfaces 的个数。
    • eoe_autocreate 是否自动创建eoe handler。
    • pcap_size Pcap buffer size。
  2. 遍历配置文件中/etc/sysconfig/ethercat的环境变量DEVICE_MODULES,位于ethercat.conf中。

  3. 在每个DEVICE_MODULES前添加前缀ec_替换,如DEVICE_MODULES为igb的话,添加前缀后为ec_igb

  4. modinfo检查该模块是否存在。

  5. 对于非genericrtdm的驱动,需要先将网卡与当前驱动unbindunbind后的网卡才能被新驱动接管

  6. 加载该驱动。

start加载了两个内核模块,ec_master.ko和网卡驱动ec_xxx.ko,ec_master先根据内核参数(网卡MAC)来创建主站实例,此时主站处于Orphaned phase。后续加载网卡驱动ec_xxx.ko,执行网卡驱动probe,根据MAC地址将网卡与主站实例匹配,此时主站得到操作的网卡设备,进入Idle phase。详细过程见后文。

ecdev-offfer

1.2 stop

卸载内核模块ec_master.ko和网卡驱动ec_xxx.ko。

  1. 遍历配置文件中的环境变量DEVICE_MODULES。
  2. 在每个DEVICE_MODULES前添加前缀‘ec_’替换。
  3. lsmod检查该模块是否被加载。
  4. 卸载模块。

后文“主站”和”master“均表示主站或主站实例对象,slave和从站表示从站或从站对象,中英混用,不刻意区分。

2 主站实例创建

一个使用IgH的控制器中可以有多个EtherCAT主站,每个主站可以绑定了一个主网卡和一个备用网卡,一般备用网卡不使用,线缆冗余功能时才使用备用网卡(文中假设只有一个主网卡)。

image-20210523100016531

start过程中执行insmod ec_master.ko,这个时候,先调用的就是 module_init 调用的初始化函数ec_init_module()。先根据参数main_devices 的个数master_count,每个master需要一个设备节点来与应用程序交互,所以master_count决定需要创建多少个matser和多少个字符设备;

这里先分配注册master_count个字符设备的主次设备号device_number和名称,这样每个master对应的设备在文件系统中就是/dev/EtherCAT0/dev/EtherCAT1...(Linux下)。

if (master_count) {
        if (alloc_chrdev_region(&device_number,
                    0, master_count, "EtherCAT")) {
            EC_ERR("Failed to obtain device number(s)!\n");
...
        }
    }

 class = class_create(THIS_MODULE, "EtherCAT");

解析模块参数得到MAC地址,保存到数组macs中。

 for (i = 0; i < master_count; i++) {
        ret = ec_mac_parse(macs[i][0], main_devices[i], 0);

        if (i < backup_count) {
            ret = ec_mac_parse(macs[i][1], backup_devices[i], 1);
        }
    }

分配master_count个主站对象的内存,调用ec_master_init()初始化这些实例。

    if (master_count) {
        if (!(masters = kmalloc(sizeof(ec_master_t) * master_count,
                        GFP_KERNEL))) {
            ...
        }
    }

    for (i = 0; i < master_count; i++) {
        ret = ec_master_init(&masters[i], i, macs[i][0], macs[i][1],
                    device_number, class, debug_level);
       ...
    }

2.1 Master Phases

igh中,状态机是其核心思想,一切操作基于状态机来执行,对创建的每个EtherCAT主站实例都需要经过如下阶段转换(见图2.3),主站各阶段操作如下:

image-20200917142438559

Orphaned phase 此时主站实例已经分配初始化,正在等待以太网设备连接,即还没有与网卡驱动联系起来,此时无法使用总线通讯。

Idle phase 当主站与网卡绑定后,Idle线程ec_master_idle_thread开始运行,主站处于IDLE状态,ec_master_idle_thread主要完成从站拓扑扫描、配置站点地址等工作。该阶段命令行工具能够访问总线,但无法进行过程数据交换,因为还缺少总线配置。

Operation phase 应用程序请求主站提供总线配置并激活主站后,主站处于operation状态,ec_master_idle_thread停止运行,内核线程变为ec_master_operation_thread,之后应用可周期交换过程数据。

具体的后面会说到。

2.2 数据报与状态机

继续看master初始化代码ec_master_init前,我们先了解数据报与状态机的关系,这对后续理解很有帮助。

数据报

EtherCAT是以以太网为基础的现场总线系统,EtherCAT使用标准的IEEE802.3以太网帧,在主站一侧使用标准的以太网控制器,不需要额外的硬件。并在以太网帧头使用以太网类型0x88A4来和其他以太网帧相区别(EtherCAT数据还可以通过UDP/IP 来传输,本文已忽略),标准的IEEE802.3以太网帧中数据部分为EtherCAT的数据,标准的IEEE802.3以太网帧与EtherCAT数据帧关系如下:

ethercat-fram-format

EtherCAT数据位于以太网帧数据区,EtherCAT数据由EtherCAT头若干EtherCAT数据报文组成。其中EtheRCAT头中记录了EtherCAT数据报的长度、和类型,类型为1表示与从站通讯。EtherCAT数据报文内包含多个子报文,每个子报文又由子报文头、数据和WKC域组成。子报文结构含义如下。

datagram-hy

整个EtherCAT网络形成一个环状,主站与从站之间是通过EtherCAT数据报来交互,一个EtherCAT报文从网卡TX发出后,从站ESC芯片EtherCAT报文进行交换数据,最后该报文回到主站。网上有个经典的EtherCAT动态图(刷新后动态图重新播放).

认识EtherCAT数据帧结构后,我们看IgH内是如何表示一个EtherCAT数据报文的?EtherCAT数据报文在igh中用对象ec_datagram_t表示。

typedef struct {
    struct list_head queue; /**< 发送和接收时插入主站帧队列. */
    struct list_head sent; /**< 已发送数据报的主站列表项. */
    ec_device_index_t device_index; /**< 发送/接收数据报的设备。 */
	
    ec_datagram_type_t type; /**< 帧类型 (APRD, BWR, etc.). */
    uint8_t address[EC_ADDR_LEN]; /**< Recipient address. */
    uint8_t *data; /**< 数据. */
    ec_origin_t data_origin; /**< 数据保存的地方. */
    size_t mem_size; /**< Datagram \a data memory size. */
    size_t data_size; /**< Size of the data in \a data. */
    uint8_t index; /**< Index (set by master). */
    uint16_t working_counter; /**< 工作计数. */
    ec_datagram_state_t state; /**数据帧状态 */
#ifdef EC_HAVE_CYCLES
    cycles_t cycles_sent; /**< Time, 数据报何时发送. */
#endif
    unsigned long jiffies_sent; /**< Jiffies,数据报何时发送. */
#ifdef EC_HAVE_CYCLES
    cycles_t cycles_received; /**< Time, 何时被接收. */
#endif
    unsigned long jiffies_received; /**< Jiffies,何时被接收. */
    unsigned int skip_count; /**< 尚未收到的重新排队数. */
    unsigned long stats_output_jiffies; /**< Last statistics output. */
    char name[EC_DATAGRAM_NAME_SIZE]; /**< Description of the datagram. */
} ec_datagram_t;

可以看到上面子报文中各字段大都在ec_datagram_t中有表示了,不过多介绍,其它几个成员简单介绍下,

device_index表示这个数据报是属于哪个网卡设备发送接收的,IgH中一个master实例可以使用多个多个网卡设备来发送/接收EtherCAT数据帧,device_index就是表示master下的网络设备的。

data_origin表示每个master管理着多个空闲的ec_datagram_t,这些ec_datagram_t分成了三类,给不同的状态机使用,具体的后文马上会细说,data_origin就是表示这个ec_datagram_t是属于哪类的。

index表示该数据报是EtherCAT数据区的第index个子报文,在master发送一个完整的EtherCAT数据报时,组装以太网数据帧时使用。

data这个指向子报文的数据内存区,由于每个子报文交换数据不同,其大小不同,所以数据区为动态分配,mem_size表示的就是分配的大小。

data_size表示数据报操作的数据大小,比如该数据报用于读从站的某个寄存器,该值就是这个寄存器的大小。

jiffies_sent、jiffies_received、cycles_sent、cycles_received使用不同时钟方式是记录数据帧发送接收时间的,用于统计。

ec_datagram_state_t表示数据报的状态,每个数据报(ec_datagram_t)也是基于状态来处理,有6种状态:

  • EC_DATAGRAM_INIT :数据报已经初始化
  • EC_DATAGRAM_QUEUED :插入发送队列准备发送
  • EC_DATAGRAM_SENT :已经发送(还存在队列中)
  • EC_DATAGRAM_RECEIVED:该数据报已接收,并从发送队列删除
  • EC_DATAGRAM_TIMED_OUT :该数据报发送后,接收超时,从发送队列删除
  • EC_DATAGRAM_ERROR :发送和接收过程中出错(从队列删除),校验错误、不匹配等。

M位在master发送时组装EtherCAT数据帧时确定,接收时也根据该位判断后面还有没有子报文。

数据报对象初始化由函数ec_datagram_init()完成:

void ec_datagram_init(ec_datagram_t *datagram /**< EtherCAT datagram. */)
{
    INIT_LIST_HEAD(&datagram->queue); // mark as unqueued
    datagram->device_index = EC_DEVICE_MAIN;  /*默认主设备使用*/
    datagram->type = EC_DATAGRAM_NONE;		/*数据报类型*/
    memset(datagram->address, 0x00, EC_ADDR_LEN);  /*数据报地址清零*/
    datagram->data = NULL;
    datagram->data_origin = EC_ORIG_INTERNAL; 	/*默认内部数据*/
    datagram->mem_size = 0;
    datagram->data_size = 0;
    datagram->index = 0x00;
    datagram->working_counter = 0x0000;
    datagram->state = EC_DATAGRAM_INIT; /*初始状态*/
#ifdef EC_HAVE_CYCLES
    datagram->cycles_sent = 0;
#endif
    datagram->jiffies_sent = 0;
#ifdef EC_HAVE_CYCLES
    datagram->cycles_received = 0;
#endif
    datagram->jiffies_received = 0;
    datagram->skip_count = 0;
    datagram->stats_output_jiffies = 0;
    memset(datagram->name, 0x00, EC_DATAGRAM_NAME_SIZE);
}

状态机

说完IgH数据报对象,我们来看有限状态机(fsm),有限状态机(fsm):表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。说起状态机,相信大家大学时候都有用过吧,不管是单片机、FPGA,用它写按键、菜单、协议处理、控制器什么的爽的一塌糊涂。其实我们使用的计算机就是本就是基于状态机作为计算模型的,它对数字系统的设计具有十分重要的作用。另外Linux TCP协议也是由状态机实现。

同样igh主站内部机制使用有限状态机来实现,IgH内状态机的基本表示如下:

struct ec_fsm_xxx {
    ec_datagram_t *datagram; /**< 主站状态机使用的数据报对象 */
    void (*state)(ec_fsm_master_t *); /**< 状态函数 */
    ....
};

IgH EtherCAT协议栈几乎所有功能通过状态机实现,每个状态机管理着某个对象的状态、功能实现的状态装换,而这些状态转换是基于EtherCAT数据报来进行的,如状态机A0状态函数填充datagram,经EtherCAT数据帧发出后,经过slave ESC处理,回到网卡接收端接收后,交给状态机A1状态的下一个状态函数解析处理。所以每个状态机内都包含有指向该状态机操作的数据报对象指针datagram和状态执行的状态函数void (*state)(ec_fsm_master_t *)

总结一句话:状态机是根据数据报的状态来执行,每个状态机都需要操作一个数据报对象

现在知道了状态机与数据报的关系,下面介绍IgH EtherCAT协议栈中有哪些状态机,及状态机使用的数据报对象是从哪里分配如何管理的。

master状态机

前面说到主站具有的三个阶段,当主站与网卡设备attach后进入Idle phase,处于Idle phase后,开始执行主站状态机。

主站状态机包含1个主状态机和许多子状态机,matser状态机主要目的是:

  • Bus monitoring 监控EtherCAT总线拓扑结构,如果发生改变,则重新扫描。

  • Slave con fguration 监视从站的应用程序层状态。如果从站未处于其应有的状态,则从站将被(重新)配置

  • Request handling 请求处理(源自应用程序或外部来源),主站任务应该处理异步请求,例如:SII访问,SDO访问或类似。

主状态机ec_fsm_master_t结构如下:

struct ec_fsm_master {
    ec_master_t *master; /**< master the FSM runs on */
    ec_datagram_t *datagram; /**< 主站状态机使用的数据报对象 */
    unsigned int retries; /**< retries on datagram timeout. */

    void (*state)(ec_fsm_master_t *); /**< master state function */
    ec_device_index_t dev_idx; /**< Current device index (for scanning etc.).
                                */
    int idle; /**< state machine is in idle phase */
    unsigned long scan_jiffies; /**< beginning of slave scanning */
    uint8_t link_state[EC_MAX_NUM_DEVICES]; /**< Last link state for every
                                              device. */
    unsigned int slaves_responding[EC_MAX_NUM_DEVICES]; /**<每个设备的响应从站数。*/
    unsigned int rescan_required; /**< A bus rescan is required. */
    ec_slave_state_t slave_states[EC_MAX_NUM_DEVICES]; /**< AL states of
                                                         responding slaves for
                                                         every device. */
    ec_slave_t *slave; /**< current slave */
    ec_sii_write_request_t *sii_request; /**< SII write request */
    off_t sii_index; /**< index to SII write request data */
    ec_sdo_request_t *sdo_request; /**< SDO request to process. */

    ec_fsm_coe_t fsm_coe; /**< CoE state machine */
    ec_fsm_soe_t fsm_soe; /**< SoE state machine */
    ec_fsm_pdo_t fsm_pdo; /**< PDO configuration state machine. */
    ec_fsm_change_t fsm_change; /**< State change state machine */
    ec_fsm_slave_config_t fsm_slave_config; /**< slave state machine */
    ec_fsm_slave_scan_t fsm_slave_scan; /**< slave state machine */
    ec_fsm_sii_t fsm_sii; /**< SII state machine */
};

可以看到,主站状态机结构下还有很多子状态机,想象一下如果主站的所有功能通过一个状态机来完成,那么这个状态机的状态数量、各状态之间的联系会有多恐怖,复杂性级别将会提高到无法管理的水平。为此,IgH中,将EtherCAT主状态机的某些功能用子状态机完成。这有助于封装相关工作流,并且避免“状态爆炸”现象。这样当主站完成coe功能时,可以由子状态机fsm_coe去完成。具体各功能是如何通过状态机完成的,文章后面会介绍。

slave状态机

slave状态机管理着每个从站的状态,所以位于从站对象(ec_slave_t)内:

struct ec_slave
{
    ec_master_t *master; /**< Master owning the slave. */
.....
    ec_fsm_slave_t fsm; /**< Slave state machine. */
.....
};
struct ec_fsm_slave {
    ec_slave_t *slave; /**< slave the FSM runs on */
    struct list_head list; /**< Used for execution list. */
    ec_dict_request_t int_dict_request; /**< Internal dictionary request. */

    void (*state)(ec_fsm_slave_t *, ec_datagram_t *); /**< State function. */
    ec_datagram_t *datagram; /**< Previous state datagram. */
    ec_sdo_request_t *sdo_request; /**< SDO request to process. */
    ec_reg_request_t *reg_request; /**< Register request to process. */
    ec_foe_request_t *foe_request; /**< FoE request to process. */
    off_t foe_index; /**< Index to FoE write request data. */
    ec_soe_request_t *soe_request; /**< SoE request to process. */
    ec_eoe_request_t *eoe_request; /**< EoE request to process. */
    ec_mbg_request_t *mbg_request; /**< MBox Gateway request to process. */
    ec_dict_request_t *dict_request; /**< Dictionary request to process. */

    ec_fsm_coe_t fsm_coe; /**< CoE state machine. */
    ec_fsm_foe_t fsm_foe; /**< FoE state machine. */
    ec_fsm_soe_t fsm_soe; /**< SoE state machine. */
    ec_fsm_eoe_t fsm_eoe; /**< EoE state machine. */
    ec_fsm_mbg_t fsm_mbg; /**< MBox Gateway state machine. */
    ec_fsm_pdo_t fsm_pdo; /**< PDO configuration state machine. */
    ec_fsm_change_t fsm_change; /**< State change state machine */
    ec_fsm_slave_scan_t fsm_slave_scan; /**< slave scan state machine */
    ec_fsm_slave_config_t fsm_slave_config; /**< slave config state machine. */
};

slave状态机和master状态机类似,slave状态机内还包含许多子状态机。slave状态机主要目的是:

  • 主站管理从站状态
  • 主站与从站应用层(AL)通讯。比如具有EoE功能的从站,主站通过该从站下的子状态机fsm_eoe来管理主站与从站应用层的EOE通讯。

数据报对象的管理

上面简单介绍了IgH内的状态机,状态机输入输出的对象是datagram,fsm对象内只有数据报对象的指针,那fsm工作过程中的数据报对象从哪里分配?

由于每个循环周期都需要操作数据报对象,IgH为减少datagram的动态分配操作,提高主站性能,在master初始化的时候预分配了主站运行需要的所有datagram对象。在master实例我们可以看到下面的数据报对象:

struct ec_master {
    ...
    ec_datagram_t fsm_datagram; /**< Datagram used for state machines. */
    ...
    ec_datagram_t ref_sync_datagram; /**< Datagram used for synchronizing the
                                       reference clock to the master clock.*/
    ec_datagram_t sync_datagram; /**< Datagram used for DC drift
                                   compensation. */
    ec_datagram_t sync_mon_datagram; /**< Datagram used for DC synchronisation
                                       monitoring. */
    ...
    ec_datagram_t ext_datagram_ring[EC_EXT_RING_SIZE]; 
}

这些数据报对象都是已经分配内存的,但由于报文不同,报文操作的数据大小不同,所以datagram数据区大小随状态机的具体操作而变化,在具体使用时才分配数据区内存。
以上数据报对象给状态机使用,别忘了还有过程数据也需要数据报对象,所以IgH中数据报类型分为以下四类:
分为三类(非常重要):

数据报对象 用途
Datagram_pairs 过程数据报
fsm_datagram[] Fsm_master及子状态机专用的数据报对象。
ext_datagram_ring[] 动态分配给fsm_slave及其子fsm。
ref_sync_datagram
sync_datagram
sync64_datagram
sync_mon_datagram
应用专用数据报用于时钟同步。

其中fsm_datagram为master状态机及master下的子状态机执行过程中操作的对象。

ext_datagram_ring[]是一个环形队列,当fsm_slave从站状态机处于ready状态,可以开始处理与slave相关请求,如配置、扫描、SDO、PDO等,这时会从ext_datagram_ring[]中给该fsm_slave分配一个数据报,并运行fsm_slave状态机检查并处理请求。

应用专用数据报用于时钟同步,与时钟强相关,它们比较特殊,它们的数据区大小是恒定的,所以其数据区在主站初始化时就已分配内存,应用调用时直接填数据发送,避免linux的内存分配带来时钟的偏差。

数据报数据区(data)内存通过ec_datagram_prealloc()来分配.

int ec_datagram_prealloc(
        ec_datagram_t *datagram, /**< EtherCAT datagram. */
        size_t size /**< New payload size in bytes. */
        )
{
    if (datagram->data_origin == EC_ORIG_EXTERNAL
            || size <= datagram->mem_size)
        return 0;
	......

    if (!(datagram->data = kmalloc(size, GFP_KERNEL))) {
	......
    }

    datagram->mem_size = size;
    return 0;
}

数据区的大小为一个以太网帧中单个Ethercat数据报的最大数据大小EC_MAX_DATA_SIZE

/** Size of an EtherCAT frame header. */
#define EC_FRAME_HEADER_SIZE 2

/** Size of an EtherCAT datagram header. */
#define EC_DATAGRAM_HEADER_SIZE 10

/** Size of an EtherCAT datagram footer. */
#define EC_DATAGRAM_FOOTER_SIZE 2

/** Size of the EtherCAT address field. */
#define EC_ADDR_LEN 4

/** Resulting maximum data size of a single datagram in a frame. */
#define EC_MAX_DATA_SIZE (ETH_DATA_LEN - EC_FRAME_HEADER_SIZE \
                          - EC_DATAGRAM_HEADER_SIZE - EC_DATAGRAM_FOOTER_SIZE)

由于以太网帧的大小有限,因此数据报的最大大小受到限制,即以太网帧长度 1500 - ethercat头2byte- ethercat子数据报报头10字节-WKC 2字节,如图:

image-20210222222428543

如果过程数据镜像的大小超过该限制,就必须发送多个帧,并且必须对映像进行分区以使用多个数据报。 Domain自动进行管理。

2.3 master状态机及数据报初始化

对状态机及数据报对象有初步认识后,我们回到ec_master.ko模块入口函数ec_init_module()主站实例初始化ec_master_init(),主要完成主站状态机初始化及数据报:

    // init state machine datagram
    ec_datagram_init(&master->fsm_datagram); /*初始化数据报对象*/
    snprintf(master->fsm_datagram.name, EC_DATAGRAM_NAME_SIZE, "master-fsm");
    ret = ec_datagram_prealloc(&master->fsm_datagram, EC_MAX_DATA_SIZE);

    // create state machine object
    ec_fsm_master_init(&master->fsm, master, &master->fsm_datagram); /*初始化master fsm*/

其中ec_fsm_master_init初始化master fsm和子状态机,并指定了master fsm使用的数据报对象fsm_datagram

void ec_fsm_master_init(
        ec_fsm_master_t *fsm, /**< Master state machine. */
        ec_master_t *master, /**< EtherCAT master. */
        ec_datagram_t *datagram /**< Datagram object to use. */
        )
{
    fsm->master = master;
    fsm->datagram = datagram;
    ec_fsm_master_reset(fsm);

    // init sub-state-machines
    ec_fsm_coe_init(&fsm->fsm_coe);
    ec_fsm_soe_init(&fsm->fsm_soe);
    ec_fsm_pdo_init(&fsm->fsm_pdo, &fsm->fsm_coe);
    ec_fsm_change_init(&fsm->fsm_change, fsm->datagram);
    ec_fsm_slave_config_init(&fsm->fsm_slave_config, fsm->datagram,
            &fsm->fsm_change, &fsm->fsm_coe, &fsm->fsm_soe, &fsm->fsm_pdo);
    ec_fsm_slave_scan_init(&fsm->fsm_slave_scan, fsm->datagram,
            &fsm->fsm_slave_config, &fsm->fsm_pdo);
    ec_fsm_sii_init(&fsm->fsm_sii, fsm->datagram);
}

初始化外部数据报队列

外部数据报队列用于从站状态机,每个状态机执行期间使用的数据报从该区域分配,下面是初始化ext_datagram_ring中每个结构:

     for (i = 0; i < EC_EXT_RING_SIZE; i++) {
        ec_datagram_t *datagram = &master->ext_datagram_ring[i];
        ec_datagram_init(datagram);
        snprintf(datagram->name, EC_DATAGRAM_NAME_SIZE, "ext-%u", i);
    }

非应用数据报队列链表,如EOE数据报会插入该队列后发送。

INIT_LIST_HEAD(&master->ext_datagram_queue);

同样初始化几个时钟相关数据报对象,它们功能固定,所以数据区大小固定,就不贴代码了,比如sync_mon_datagram,它的作用是用于同步监控,获取从站系统时间差,所以是一个BRD数据报,在此直接将数据报操作偏移地址初始化,使用时能快速填充发送。

    ec_datagram_init(&master->sync_mon_datagram);
	......
    ret = ec_datagram_brd(&master->sync_mon_datagram, 0x092c, 4);
地址 名称 描述 复位值
0x092c~0x092F 0~30 系统时间差 本地系统时间副本与参考时钟系统时间值之差 0
31 符号 0:本地系统时间≥参考时钟时间
1:本地系统时间<参考时钟时间
0

另外比较重要的是将使用的网卡MAC地址放到macs[]中,在网卡驱动probe过程中根据MAC来匹配主站使用哪个网卡。

    for (dev_idx = EC_DEVICE_MAIN; dev_idx < EC_MAX_NUM_DEVICES; dev_idx++) {
        master->macs[dev_idx] = NULL;
    }

    master->macs[EC_DEVICE_MAIN] = main_mac;

2.4 初始化EtherCAT device

master协议栈主要完成EtherCAT数据报的解析和组装,然后需要再添加EtherNet报头和FCS组成一个完整的以太网帧,最后通过网卡设备发送出去。为与以太网设备驱动层解耦,igh使用ec_device_t来封装底层以太网设备,一般来说每个master只有一个ec_device_t,这个编译时配置决定,若启用线缆冗余功能,可指定多个网卡设备:

struct ec_device
{
    ec_master_t *master; /**< EtherCAT master */
    struct net_device *dev; /**< 使用的网络设备 */
    ec_pollfunc_t poll; /**< pointer to the device's poll function */
    struct module *module; /**< pointer to the device's owning module */
    uint8_t open; /**< true, if the net_device has been opened */
    uint8_t link_state; /**< device link state */
    struct sk_buff *tx_skb[EC_TX_RING_SIZE]; /**< transmit skb ring */
    unsigned int tx_ring_index; /**< last ring entry used to transmit */
#ifdef EC_HAVE_CYCLES
    cycles_t cycles_poll; /**< cycles of last poll */
#endif
#ifdef EC_DEBUG_RING
    struct timeval timeval_poll;
#endif
    unsigned long jiffies_poll; /**< jiffies of last poll */

    // Frame statistics
    u64 tx_count; /**< 发送的帧数 */
    u64 last_tx_count; /**<上次统计周期发送的帧数。 */
    u64 rx_count; /**< 接收的帧数 */
    u64 last_rx_count; /**< 上一个统计周期收到的帧数。 */
	
    u64 tx_bytes; /**< 发送的字节数 */
    u64 last_tx_bytes; /**< 上一个统计周期发送的字节数。 */
    u64 rx_bytes; /**< Number of bytes received. */
    u64 last_rx_bytes; /**< Number of bytes received of last statistics cycle.
                        */
    u64 tx_errors; /**< Number of transmit errors. */
    s32 tx_frame_rates[EC_RATE_COUNT]; /**< Transmit rates in frames/s for
                                         different statistics cycle periods.
                                        */
    s32 rx_frame_rates[EC_RATE_COUNT]; /**< Receive rates in frames/s for
                                         different statistics cycle periods.
                                        */
    s32 tx_byte_rates[EC_RATE_COUNT]; /**< Transmit rates in byte/s for
                                        different statistics cycle periods. */
    s32 rx_byte_rates[EC_RATE_COUNT]; /**< Receive rates in byte/s for
                                        different statistics cycle periods. */

......
};

成员*master表示改对象属于哪个master,*dev指向使用的以太网设备net_device,poll该网络设备poll函数,tx_skb[]以太网帧发送缓冲区队列,需要发送的以太网帧会先放到该队里,tx_ring_index管理tx_skb[],以及一些网络统计变量,下面初始化ec_device_t对象:

/*\master\master.c*/
    for (dev_idx = EC_DEVICE_MAIN; dev_idx < ec_master_num_devices(master);
            dev_idx++) {
        ret = ec_device_init(&master->devices[dev_idx], master);
        if (ret < 0) {
            goto out_clear_devices;
        }
    }
/*\master\device.c*/
int ec_device_init(
        ec_device_t *device, /**< EtherCAT device */
        ec_master_t *master /**< master owning the device */
        )
{
    int ret;
    unsigned int i;
    struct ethhdr *eth;
....

    device->master = master;
    device->dev = NULL;
    device->poll = NULL;
    device->module = NULL;
    device->open = 0;
    device->link_state = 0;
    for (i = 0; i < EC_TX_RING_SIZE; i++) {
        device->tx_skb[i] = NULL;
    }
......

    ec_device_clear_stats(device);
......
    for (i = 0; i < EC_TX_RING_SIZE; i++) {
        if (!(device->tx_skb[i] = dev_alloc_skb(ETH_FRAME_LEN))) {
            ......
        }

        // add Ethernet-II-header
        skb_reserve(device->tx_skb[i], ETH_HLEN);
        eth = (struct ethhdr *) skb_push(device->tx_skb[i], ETH_HLEN);
        eth->h_proto = htons(0x88A4);
        memset(eth->h_dest, 0xFF, ETH_ALEN);
    }
.....
}

主要关注分配以太网帧发送队列内存tx_skb[],并填充Ethernet报头中的以太网类型字段为0x88A4,目标MAC地址0xFFFFFFFF FFFF,对于源MAC地址、sk_buff所属网络设备、ec_device_t对象使用的网络设备net_device,将在网卡驱动初始化与master建立联系过程中设置。

2.5 设置IDLE 线程的发送间隔:

ec_master_set_send_interval(master, 1000000 / HZ);

根据网卡速率计算:

void ec_master_set_send_interval(
        ec_master_t *master, /**< EtherCAT master */
        unsigned int send_interval /**< Send interval */
        )
{
    master->send_interval = send_interval;  //发送间隔 us
    master->max_queue_size =
        (send_interval * 1000) / EC_BYTE_TRANSMISSION_TIME_NS;
    master->max_queue_size -= master->max_queue_size / 10;
}

100Mbps网卡发送一字节数据需要的时间EC_BYTE_TRANSMISSION_TIME_NS: 1/(100 MBit/s / 8 bit/byte) = 80 ns/byte.

2.6 初始化字符设备

由于主站位于内核空间,用户空间应用与主站交互通过字符设备来交互;
创建普通字符设备,给普通linux应用和Ethercat tool使用。若使用xenomai或RTAI,则再创建实时字符设备,提供给实时应用使用。

	......
	master->class_device = device_create(class, NULL,
            MKDEV(MAJOR(device_number), master->index), NULL,
            "EtherCAT%u", master->index);
    ......
#ifdef EC_RTDM
    // init RTDM device
    ret = ec_rtdm_dev_init(&master->rtdm_dev, master);
   ...
#endif

到这里明白了IgH中的状态机与数据报之间的关系,主站对象也创建好了,但是主站还没有网卡设备与之关联,主站也还没有工作,下面简单看一下ecdev_offer流程。

关于网卡驱动代码详细解析推荐这两篇文章:

Monitoring and Tuning the Linux Networking Stack: Sending Data

Monitoring and Tuning the Linux Networking Stack: Receiving Data

3 网卡

  1. 网卡probe

    static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
    {	
        ......
    	adapter->ecdev = ecdev_offer(netdev, ec_poll, THIS_MODULE);
    	if (adapter->ecdev) {	/*注册打开ec_net设备*/
    		err = ecdev_open(adapter->ecdev);
    		.....
    		adapter->ec_watchdog_jiffies = jiffies;
    	} else { /*注册普通网络设备*/
    		......
    		err = register_netdev(netdev);
    		......
    	}
        ......
    }
    
  2. 给主站提供网络设备:ecdev_offer

    根据MAC地址找到master下的ec_device_t对象

        device->dev = net_dev;
        device->poll = poll;
        device->module = module;
    

    上面我们只设置了ec_device_t->tx_skb[]sk_buff的以太网类型和目的地址,现在继续填充源MAC地址为网卡的MAC地址、sk_buff所属的net_device:

        for (i = 0; i < EC_TX_RING_SIZE; i++) {
            device->tx_skb[i]->dev = net_dev;
            eth = (struct ethhdr *) (device->tx_skb[i]->data);
            memcpy(eth->h_source, net_dev->dev_addr, ETH_ALEN);
        }
    
  3. 调用网络设备接口打开网络设备

int ec_device_open(
        ec_device_t *device /**< EtherCAT device */
        )
{
    int ret;
.....
    ret = device->dev->open(device->dev);
    if (!ret)
        device->open = 1;
....
    return ret;
}
  1. 当master下的所有的网络设备都open后,master从ORPHANED转到IDLE阶段
int ec_master_enter_idle_phase(
        ec_master_t *master /**< EtherCAT master */
        )
{
    int ret;
    ec_device_index_t dev_idx;
	......
    master->send_cb = ec_master_internal_send_cb;
    master->receive_cb = ec_master_internal_receive_cb;
    master->cb_data = master;

    master->phase = EC_IDLE;  /*更新master状态*/

    // reset number of responding slaves to trigger scanning
    for (dev_idx = EC_DEVICE_MAIN; dev_idx < ec_master_num_devices(master);
            dev_idx++) {
        master->fsm.slaves_responding[dev_idx] = 0;
    }

    ret = ec_master_nrthread_start(master, ec_master_idle_thread,
            "EtherCAT-IDLE");
    ....
    return ret;
}

其中主要设置master发送和接收回调函数,应用通过发送和接收数据时,将通过这两接口直接发送和接收。创建master idle线程ec_master_idle_thread

4 IDLE阶段内核线程

综上,状态机操作对象是datagram,datagram发送出去后回到主站交给状态机的下一个状态处理,所以主站需要循环地执行状态机、发送EtherCAT数据帧、接收EtherCAT数据帧、执行状态机、发送EtherCAT数据帧、……来驱动状态机运行,这个循环由内核线程来完成。

image-20210523100034721

当主站与网卡绑定后,应用还没有请求主站,主站处于IDLE状态,这时循环由内核线程ec_master_idle_thread来完成,主要完成从站拓扑扫描、配置站点地址等工作。

static int ec_master_idle_thread(void *priv_data)
{
    ec_master_t *master = (ec_master_t *) priv_data;
    int fsm_exec;
#ifdef EC_USE_HRTIMER
    size_t sent_bytes;
#endif

    // send interval in IDLE phase
    ec_master_set_send_interval(master, 250000 / HZ);
    
    while (!kthread_should_stop()) {
        // receive
        ecrt_master_receive(master);  /*接收上个循环发送的数据帧*/
		......
        // execute master & slave state machines
        ......
        fsm_exec = ec_fsm_master_exec(&master->fsm); /*执行master状态机*/

        ec_master_exec_slave_fsms(master); /*为从站状态机分配datagram,并执行从站状态机*/
		......
        if (fsm_exec) {
            ec_master_queue_datagram(master, &master->fsm_datagram); /*将master状态机处理的datagram插入发送链表*/
        }
        // send
        ecrt_master_send(master); /*组装以太网帧并调用网卡发送*/
        
        sent_bytes = master->devices[EC_DEVICE_MAIN].tx_skb[
            master->devices[EC_DEVICE_MAIN].tx_ring_index]->len;
        up(&master->io_sem);

        if (ec_fsm_master_idle(&master->fsm)) {
            ec_master_nanosleep(master->send_interval * 1000);
            set_current_state(TASK_INTERRUPTIBLE);
            schedule_timeout(1);
        } else {
            ec_master_nanosleep(sent_bytes * EC_BYTE_TRANSMISSION_TIME_NS);
        }
    }

    EC_MASTER_DBG(master, 1, "Master IDLE thread exiting...\n");

    return 0;
}

整个过程简单概述如下。

4.1 数据报发送

下面介绍IgH中状态机处理后数据报的发送流程(ecrt_master_send())。

image-20210523100210574

master使用一个链表datagram_queue来管理要发送的子报文对象datagram,需要发送的子报文对象会先插入该链表中,统一发送时,分配一个sock_buff,从datagram_queue上取出报文对象,设置indexindex是发送后接收回来与原报文对应的标识之一),将一个个报文对象按EtherCAT数据帧结构填充到sock_buff中,最后通过网卡设备驱动函数hard_start_xmit,将sock_buff从网卡发送出去。

image-20210523100225964

4.2 数据报接收

image-20210523100246660

接收数据时,通过网卡设备驱动ec_poll函数取出Packet得到以太网数据,然后解析其中的EtherCAT数据帧,解析流程如下:

  1. 得到子报文index,遍历发送链表datagram_queue找到index对应的datagram

  2. 将子报文数据拷贝到datagram数据区。

  3. 将以太网帧内子报文中的WKC值复制到datagram中的WKC。

  4. datagram从链表datagram_queue删除。

  5. 根据子报文头M位判断还有没有子报文,有则跳转1继续处理下一个子报文,否则完成接收。

接收完成后,进入下一个循环,内核线程运行状态机或周期应用进行下一个周期,处理接收的Ethercat报文。

先简单介绍到这,敬请关注后续文章。。。。

posted @ 2021-02-22 23:39  沐多  阅读(13580)  评论(6编辑  收藏  举报