linux系统上安装完成ethercat系统,配置好网卡MAC和驱动之后,执行sudo /etc/init.d/ethercat start即可启动ethercat系统,在这个脚本中本质上调用了两个模块,一个是主站模块ec_master.ko,一个是驱动模块ec_generic.ko(关于驱动模块部分视配置文件中的DEVICE_MODULES类型决定,这里为了通用选择generic)。
该模块的初始化函数为./master/module.c/ec_init_module,该模块就是ethercat的主站模块,主要负责存储一些ethercat从站信息,管理domains域,管理从站配置等有关于ethercat系统的资源,在/etc/sysconfig/ethercat中配置的MASTER0_DEVICE会作为该模块参数传入主站模块,最终成为主站模块的mac使用网络地址,即master->macs;顺序理解主站模块的初始化函数,主要有以下部分:
1.字符设备创建方面:创建字符设备的目的就是为了给应用层提供接口,便于应用层调用内核的一些功能。创建的字符设备就是/dev/EtherCATx(可以是/dev/EtherCAT0、/dev/EtherCAT1..有多少主站就有多少创建的字符设备):具体创建过程如下:
使用int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);动态申请设备号;
使用class_create和device_create以及上面申请好的设备好直接创建字符设备/dev/EtherCAT;
2.主站结构创建与初始化:
每个MAC地址对应一个主站空间,每个主站对应的结构体为./master/master.h的ec_master结构体,结构体如下:
struct ec_master
{
unsigned int index; /**< Index. 第几个主站(具体对应于加载ec_master时候的mac顺序) */
/* 主站是否被申请使用,在应用申请主站的时候,会将该变量置1 */
unsigned int reserved; /**< \a True, if the master is in use. */
ec_cdev_t cdev; /**< Master character device. 主站字符设备 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 26)
struct device *class_device; /**< Master class device. ec_master对应/dev下的设备节点 */
#else
struct class_device *class_device; /**< Master class device. */
#endif
#ifdef EC_RTDM
ec_rtdm_dev_t rtdm_dev; /**< RTDM device. */
#endif
struct semaphore master_sem; /**< Master semaphore. sema_init(&master->master_sem, 1); 初始化为互斥量 */
ec_device_t devices[EC_MAX_NUM_DEVICES]; /**< EtherCAT devices. EtherCAT系统的网络设备 */
const uint8_t *macs[EC_MAX_NUM_DEVICES]; /**< Device MAC addresses. 主站的mac地址, 0为主站 1为备份地址 */
#if EC_MAX_NUM_DEVICES > 1
unsigned int num_devices; /**< Number of devices. Access this always via
ec_master_num_devices(), because it may be
optimized!
主站mac地址个数,一个mac地址对应一个devices,如果有备用的mac就为2,没有则为1 */
#endif
struct semaphore device_sem; /**< Device semaphore. 初始化为互斥量,使用网卡时候的旗标 */
ec_device_stats_t device_stats; /**< Device statistics. 设备使用信息统计 */
/* 主站状态机,用于表示主站当前状态(孤儿阶段、空闲阶段和操作阶段),并通过设置不同的状态机执行函数实现不同状态下执行不同的函数实现(fsm->state)的目的。 */
ec_fsm_master_t fsm; /**< Master state machine. */
ec_datagram_t fsm_datagram; /**< Datagram used for state machines. 主站状态机使用数据包 */
/** Master phase. 当前主站阶段 */
ec_master_phase_t phase;
/* 主站是否激活,初始化为0,在应用进行主站激活的时候标志为激活状态,即置1 */
unsigned int active; /**< Master has been activated. */
/* 在激活主站的时候,对config_changed同样进行了置1,config_changed是有关于DC的,于主站中的configs没有关系 */
unsigned int config_changed; /**< The configuration changed. */
/* 主站初始化函数中初始化为0,主站激活的时候也会进行初始化 */
unsigned int injection_seq_fsm; /**< Datagram injection sequence number for the FSM side. FSM侧的数据报注入序列号。 */
/* 主站初始化函数中初始化为0,主站激活的时候也会进行初始化 */
unsigned int injection_seq_rt; /**< Datagram injection sequence number for the realtime side. */
/* 指向ethercat从站的指针,空闲阶段在需要重新扫描从站的时候,需要申请相对应的ethercat从站结构空闲,该指针就指向这片空间。在ec_master_init初始化阶段初始化为NULL,注意,该指针指向的是个ec_slave_t数组,而不是只有一个从站 */
ec_slave_t *slaves; /**< Array of slaves on the bus. */
/* 在ec_master_init初始化阶段初始化为0,主站使用的主设备和次设备(用到的话)上有相应的从站个数 */
unsigned int slave_count; /**< Number of slaves on the bus. */
/* Configuration applied by the application. 通过应用程序进行配置应用,在应用中使用ecrt_master_slave_config()进行从站配置的时候会将对应配置添加到configs链表中 */
/* 虽然在这个位置只是直接定义了list_head类型,但是巧妙之处也在于此,申请的空间却为ec_slave_config_t(作为configs的独立的项)类型,且ec_slave_config_t第一个元素即为list_head,从而实现内容跟随在链表头之后的目的,这里的配置是应用部分设置下来的配置 */
struct list_head configs; /**< List of slave configurations. */
struct list_head domains; /**< List of domains. 域列表(双向循环链表) */
/* 该时间在应用开启主站之后,使用ecrt_master_application_time()通过ioctl对该值赋值,赋值意义即为linux系统本身的系统时间,且每个周期都会发送更新,可参考examples/dc_user/main.c的cycle_task() */
u64 app_time; /**< Time of the last ecrt_master_sync() call. */
/* 应用层应用开启的时间 */
u64 app_start_time; /**< Application start time. */
/* 写入应用程序时间,一般写入主机时间后,之后会进行主机时间同步到参考时钟,然后再将主站系统时间同步到其余从站参考时钟 */
u8 has_app_time; /**< Application time is valid. */
/* master/master.c/ec_master_find_dc_ref_clock()中将该子报文设置为fpwr类型,对0x0910(4Byte)进行写入用于设置主站系统时间到参考时钟从站的系统时间位置处 */
ec_datagram_t ref_sync_datagram; /**< Datagram used for synchronizing the reference clock to the master clock. */
/* master/master.c/ec_master_find_dc_ref_clock()中将该子报文设置为frmw类型,目标操作从站为参考时钟从站,对0x0910(4Byte)进行写入 */
ec_datagram_t sync_datagram; /**< Datagram used for DC drift compensation. */
ec_datagram_t sync_mon_datagram; /**< Datagram used for DC synchronisation monitoring. 用于分布式时钟同步管理的数据帧 */
/* 主站使用那个从站作为系统参考时钟从站,如果没有设置,那么默认选择主站的从站列表中第一个支持分布式时钟的从站作为参考时钟从站 */
ec_slave_config_t *dc_ref_config; /**< Application-selected DC reference clock slave config. */
/* 主站使用的参考时钟从站,如果dc_ref_config有配置且对应从站支持DC功能,那该从站就指向dc_ref_config从站配置对应的从站,否则,使用ethercat系统中主站的从站连接链表的第一个支持DC功能的从站作为参考时钟从站,每个网络设备连接的 环路上都会设定一个参考时钟从站 */
ec_slave_t *dc_ref_clock; /**< DC reference clock slave. */
/* 表明当前主站正在进行从站扫描过程,且allow_scan量为时候允许从站扫描,受scan_sem信号量保护
从站扫描有以下过程:1.清除从站的站地址(从站在拓扑结构中的地址) */
unsigned int scan_busy; /**< Current scan state. */
/* 允许进行从站扫描,ec_master_init初始化过程中开启,进入操作阶段后停止扫描,主站初始化函数ec_master_init()中初始化为1,应用申请主站后,主站进入操作阶段,该量allow_scan赋值0,离开操作阶段ec_master_leave_operation_phase()时又会置1 */
unsigned int allow_scan; /**< \a True, if slave scanning is allowed. */
struct semaphore scan_sem; /**< Semaphore protecting the \a scan_busy
variable and the \a allow_scan flag. 互斥量,保护scan_busy和allow_scan两个量 */
wait_queue_head_t scan_queue; /**< Queue for processes that wait for slave scanning. 扫描从站的进程的等待队列 */
unsigned int config_busy; /**< State of slave configuration. */
struct semaphore config_sem; /**< Semaphore protecting the \a config_busy
variable and the allow_config flag. */
wait_queue_head_t config_queue; /**< Queue for processes that wait for
slave configuration. */
/**
* 主站发送数据包的时候的数据包队列。当数据帧需要被主站读取信息的时候,即数据帧拥有read属性,就需要将数据帧中的子报文信息提取并存储到ec_datagram_t->data中。主站中使用到的所有的数据帧都会存储到该子报文队列中(包括状态机中用到的数据帧); */
struct list_head datagram_queue; /**< Datagram queue. */
/* 主站初始化函数ec_master_init()中将该index初始化为0,在发送子报文的时候,会对该数值进行累加处理;
有个问题就是datagram_index类型为uint8_t,该类型最大值为255,这个在ethercat系统中是如何解决的呢? */
uint8_t datagram_index; /**< Current datagram index. */
struct list_head ext_datagram_queue; /**< Queue for non-application datagrams. */
struct semaphore ext_queue_sem; /**< Semaphore protecting the ext_datagram_queue. */
/* 该数据环报中会有一部分子报文排列到主站的子报文列表中,然后进行子报文发送, */
ec_datagram_t ext_datagram_ring[EC_EXT_RING_SIZE]; /**< External datagram ring. 子报文结构名称初始化为ext-xx*/
/* 关于这部分这两个变量是使用在主站的外部子报文环上的,也就是上面的ec_datagram_t数组,在应用层进行SDO申请时,首先会将申请部分的信息先存放在从站的sdo_requests SDO申请链表中,而后基于从站的状态机执行进行从站数据帧的组合发送,那么此时从站使用的数据报结构就是ec_datagram_t数组进行邮箱通信 */
unsigned int ext_ring_idx_rt; /**< Index in external datagram ring for RT side. */
unsigned int ext_ring_idx_fsm; /**< Index in external datagram ring for FSM side. */
/* 两次调用ecrt_master_send()的间隔(ms) */
unsigned int send_interval; /**< Interval(ms) between two calls to ecrt_master_send(). */
/* 这个是通过设置的间隔周期进行计算并设置的,代表了在设置的间隔情况下,每次网卡可以发送最大的数据量 */
size_t max_queue_size; /**< Maximum size of datagram queue */
/* 用于遍历从站状态机 */
ec_slave_t *fsm_slave; /**< Slave that is queried next for FSM exec. 接下来要向FSM程序查询的从站 */
/* 从站状态机执行链表,具体的类型为ethercat/master/fsm_slave.h/ec_fsm_slave_t类型,主站初始化的时候会将该链表清空,主站状态机广播扫描过程中检测到主站使用网络设备连接断开异常情况,也会清空该链表;主站状态机广播的时候检测到需要重新扫描从站的时候,也会清空该链表。 */
struct list_head fsm_exec_list; /**< Slave FSM execution list. 从站状态机,对应文件为master/fsm_slave.h/.c中 */
/* 从站状态机当前的执行个数,该量的清空和fsm_exec_list一致 */
unsigned int fsm_exec_count; /**< Number of entries in execution list. */
unsigned int debug_level; /**< Master debug level. */
ec_stats_t stats; /**< Cyclic statistics. */
/**
* 当前运行的主站线程,主站在空闲阶段的时候运行的函数为 ec_master_idle_thread() 函数;主站激活的时候会关闭空闲线程然后开启ec_master_operation_thread()操作阶段线程 */
struct task_struct *thread; /**< Master thread. 主站线程,master.c文件中初始化为NULL */
#ifdef EC_EOE
struct task_struct *eoe_thread; /**< EoE thread. */
struct list_head eoe_handlers; /**< Ethernet over EtherCAT handlers. */
#endif
struct semaphore io_sem; /**< Semaphore used in \a IDLE phase. 空闲阶段使用的信号量 */
// 回调函数接口,留给使用端进行自定义
/* 初始化为NULL,设置为master.c/ec_master_internal_send_cb回调函数 */
void (*send_cb)(void *); /**< Current send datagrams callback. */
/* 初始化为NULL,设置为master.c/ec_master_internal_receive_cb回调函数 */
void (*receive_cb)(void *); /**< Current receive datagrams callback. */
/* 初始化为NULL,设置为指向本身(ec_master类型)空间 */
void *cb_data; /**< Current callback data. */
void (*app_send_cb)(void *); /**< Application's send datagrams callback. 初始化为NULL */
void (*app_receive_cb)(void *); /**< Application's receive datagrams callback. 初始化为NULL */
void *app_cb_data; /**< Application callback data. 初始化为NULL */
struct list_head sii_requests; /**< SII write requests. 初始化为双向循环链表 */
wait_queue_head_t request_queue; /**< Wait queue for external requests from user space. 通过init_waitqueue_head()来进行初始化等待队列 */
};
然后使用./master/master.c/ec_master_init()函数进行单个主站结构体的初始化,初始化内容较为繁杂,最好还是结合之后的操作使用理解。本章节不再赘述。
该模块的初始化函数为./devices/generic.c/ec_gen_init_module()函数,在该函数中有一个init_net链表,这个链表是内核空间中的,指向所有网卡设备。筛选出来ARPHRD_ETHER以太网卡的netdev,然后将这些网卡信息提取与注册主站时使用的MAC地址进行匹配,匹配正常就将主站与网络设备相连接,即master->devices[0]->dev指向匹配的netdev结构体(且基本上主站相关的net_device类型结构体都指向该结构信息)。将主站使用网络设备与主站匹配之后,为了进行网络通信,还需要申请一个套接字,该套接字采用的是PF_PACKET协议族的原始套接字,原始套接字在进行数据帧收发的时候是个完整的数据帧,即收到的数据帧包含目标MAC+源MAC以及数据帧类型这样的信息。
加载ec_generic.ko模块,完成网络配置之后,主站就可以使用网络设备进行通信,在ec_generic.ko模块初始化的最后,设置呢主站的发送、接收的回调函数,并把主站切换到了空闲阶段(在这之前主站都是孤儿阶段);然后开启主站的空闲线程函数./master/master.c/ec_master_idle_thread()。