EtherCAT-4 主站SOEM
SOEM 是 Simple Open EtherCAT Master Library 的缩写,是瑞典 rt-lab 提供 的一个开源 EtherCAT 主站协议库 。 SOEM 库使用 C 语言编写,可以在 windows 以及 Linux 平台上运行,并也可以方便地移植到嵌入式平台上。
注:以下文章内容通过网站资料整理而来,如有侵权,抱歉联系删除~
参考:EtherCAT主站——SOEM(学习笔记1)-EtherCAT主站——SOEM(学习笔记1)-CSDN博客
SOEM 库架构分析
SOEM 是 Simple Open EtherCAT Master Library 的缩写,是瑞典 rt-lab 提供 的一个开源 EtherCAT 主站协议库 。 SOEM 库使用 C 语言编写,可以在 windows 以及 Linux 平台上运行,并也可以方便地移植到嵌入式平台上。
SOEM 支持 CoE , SoE , FoE 以及分布式时钟。 SOEM 直接将 EtherCAT 帧 通过 MAC 发送和接收,因此它不支持 EoE 。 SOEM 库由若干模块组成,最底层提供硬件和操作系统抽象层,从而可以方便地将 SOEM 库移植到不同的系统平台上。
SOEM 库架构如图 所示:

SOEM 库采用分层设计,并且提供了一个抽象层,将 SOEM 协议栈与具体操作系统和硬件分开,使得 SOEM 在理论上可以移植到任意操作系统和硬件平 台之上。
抽象层由 OSAL 和 OSHW 两个模块组成,OSAL 是操作系统抽象层,OSHW 是硬件抽象层,移植的主要内容就是对 OSAL 和 OSHW 具体 API 实现,在新的操作系统和硬件平台上的重写。注意,SOEM 可以不需要 OS 操作系统,直接与时间和定时器相关的 API 是必须实现的。
1、操作系统抽象层
主要是用于符合OSADL和实时进程创建。也就是说:发送EtherCAT数据包不能抖动太大,如果直接使用linux提供的原生线程,可能实时性无法满足。需要对Linux内核打上实时补丁,我们采用PREEMPT_RT。
注1:OSADL(Open Source Automation Development Lab):这是一个开源自动化开发实验室相关的组织或标准框架。在自动化系统中,特别是像工业控制这样的场景,对实时性要求很高。OSADL 的标准规定了如何创建和管理实时进程,以确保系统能够在精确的时间间隔内响应事件。
注2:PREEMPT_RT 是一种对 Linux 内核进行修改的实时补丁。它改变了 Linux 内核的调度方式,使得内核能够更好地支持实时进程。通过采用这种补丁,系统能够更及时地响应实时任务的请求,减少任务调度的延迟,从而降低 EtherCAT 数据包发送时间的抖动。具体来说,这个补丁可以使操作系统在运行时优先处理实时任务,将非实时任务暂时挂起,确保实时任务(如发送 EtherCAT 数据包)能够按照精确的时间要求执行。
2、硬件抽象层
硬件抽象层即OSHW 模块,网卡的接口封装,不同操作系统对网卡操作不一样,linux下我们要求网卡支持混杂模式。它为上层提供网络服务,是最重要的实现。 SOEM 移植的核心工作就是对此模块的移植,后文会详细介绍具体的实现。
OSHW 模块由 oshw.h/oshw.c 和 nicdrv.h/nicdrv.c 四个文件组成。 oshw.h/.c 主要实现网络小端和本地大小端的转换,nicdrv.h/.c 是网络驱动,主要实现 EtherCAT 帧的发送和接收, SOEM 的移植主要是对网络驱动的实现。 oshw.h/.c 主要提供字节顺序转换的服务。数据在网络上传输时,总是先传输高位比特,然后传低位比特。所以,网络采用的是大端方式。而具体一个硬 件平台,可能采用大端也可能是小端。
不同平台上 nicdrv 模块的逻辑结构基本相同,需要实现的是三个底层 API :
1) 网口的初始化
2)MAC 层帧发送
3)MAC 层帧接收
3、中间层
抽象层之上的模块(除了应用层)属于 SOEM 的中间层,是 EtherCAT 协议栈的具体实现,包含 BASE 模块、 MAIN 模块、 CONFIG 模块、 CONFIGDC 模 块、COE 等。
注1:
COE、SOE、EOE、FOE都是一种基于邮箱的协议,分别基于EtherCAT来兼容。
COE
全称:CANopen over EtherCAT。该协议是一个具有开放性的标准应用层协议。其中CANopen协议是基于CAN协议的链路层上实现的。EtherCAT协议在应用层支持CANopen协议,因此支持CANopen协议的从站可以被运用在EtherCAT协议上。
SOE
全称:Servo Drive over EtherCAT。SERCOS是世界首个应用于伺服控制的协议。EtherCAT协议在应用层接口上兼容了这个协议,简称为SOE。SERCOS应用层协议为主站设计了信息接口,可以通过配置EtherCAT过程数据报文,实现周期性传递伺服驱动器的数据。
EOE
全称:Ethernet over EtherCAT。该协议支持EtherCAT能分段传递标准的以太网数据报文,使得EtherCAT协议同样能支持TCP/IP、UDP/IP协议。
FOE
全称:File Access over EtherCAT。该协议可以使用EtherCAT总线上传、下载固件,刷新从站的固件。并且可以通过命令行工具加载或存储文件。
注2:
主站与从站进行数据交互的方式主要通过PDO和SDO,即过程数据和邮箱数据,其概念与CANOpen中的概念相同。
- PDO(过程数据对象):过程数据用来传输周期性的数据,PDO由三个数据缓冲区组成,类似于一个FIFO,从站写入第一个缓冲区,主站从第三个缓冲区读走。注意第二个缓冲区不可操作。从站发送PDO和接受PDO各自采用两个独立的数据缓冲区。有同步管理器来控制缓冲区,每一个同步管理器只负责一种功能,例如同步管理器2负责发送PDO,同步管理器3负责接受PDO。
- SDO(服务数据对象):邮箱通信用来发送非周期性的数据,邮箱通信只有一个数据缓冲区,通信方式采用握手的机制,确保主从之间的数据交互不丢失,而PDO由于采用FIFO的机制,可能会出现新值覆盖旧值或旧值被多次读走的情况。SDO也由同步管理器来进行管理,发送和接受邮箱独立控制,例如同步管理器0控制发送邮箱,同步管理器1控制接受邮箱。
注3:

SOEM主要文件及功能
ethercatbase.c
基本EtherCAT功能函数,主要包含的是一些EtherCAT通信服务命令函数以及EtherCAT数据帧组成的函数
ethercatmain.c
EtherCAT主要功能模块,包含了EtherCAT初始化、状态设置和读取、邮箱数据通信、从站EEPROM操作、从站信息接口SII读写和过程数据交换等函数,以及定义ec_slave[],所有从站信息都在这个结构体中
ethercatconfig.c
EtherCAT主站配置模块,包含的是EtherCAT主站对从站的基本配置函数
ethercatconfiglist.h
此文件包含的是一个列表,里面包含了一些已知产品参数的EtherCAT从站
ethercatdc.c
EtherCAT分布时钟功能,包含了EtherCAT分布时钟(Distributed Clock)相关配置的函数
ethercatcoe.c
CoE(CAN over EtherCAT)模块,包含的是应用层CoE协议相关的一些结构体和数据操作函数
ethercatprint.c
将EtherCAT错误转换成可读信息模块,SDO中止信息和应用层状态代码用于传递从站信息给用户应用层,所以这个文件里函数的主要功能是将二进制码转换成可读的文本信息
ethercattype.c
此文件主要是一些新的类型定义和宏定义,使用这些可以给具体的应用程序带来优化和便利
nicdrv.c
EtherCAT原始套接字驱动,主要包含了使用网卡NIC和socket通信来发送数据帧的函数
osal.c
定时器配置模块,主要包含了一些定时器设置和延时等函数
oshw.c
此文件中包含的函数主要用来识别现在这台机器上现有的网卡信息
simple_test.c
此文件是根据自己所需的EtherCAT主站开发所编写的程序,通过各种配置,数据发送和读取
simple_test.c解读
- 基于SOEM创建主站步骤
- 主要函数介绍
- ecx_init函数
- ecx_config_init函数
- ecx_config_map_group函数
- simple_test.c文件
1、基于SOEM创建主站步骤
参照EtherCAT状态机,可以为如下步骤:
- 初始化SOEM,将socket绑定到ifname,调用 ecx_init
- 枚举并初始化从站,调用ecx_config_init函数
- 建立从站pdo与IOMap映射,调用ecx_config_map_group函数
SOEM通过函数ec_config_map(&IOmap)完成逻辑地址和物理地址的映射,应用程序需要定义一个数组IOmap,例如:
char IOmap[4096];
完成映射后,应用程序就可以通过读写数组IOmap[]来完成与从站的数据交换。
映射关系
SOEM首先将所有从站的输出映射到IOmap[]的开始位置,然后是输入,如下图所示:

4. 等待所有从站运行到安全状态(SAFE_OP),调用ec_statecheck函数
5. 切换到运行状态(OP state),通过发送一个有效过程数据实现
2、主要函数介绍
ecx_init函数
功能:初始化SOEM,将socket绑定到ifname
ecx_init函数定义在ethercatmain.h文件,其有两种参数形式,由于大部分函数都是这两种格式,所以后文将不做区分处理。
int ec_init(const char * ifname);
int ecx_init(ecx_contextt *context, const char * ifname);
函数方法在ethercatmain.c中定义如下
/** Initialise lib in single NIC mode
* @param[in] context = context struct
* @param[in] ifname = Dev name, f.e. "eth0"
* @return >0 if OK
*/
int ecx_init(ecx_contextt *context, const char * ifname)
{ return ecx_setupnic(context->port, ifname, FALSE);
}
ecx_setupnic函数调用的oshw/linux/nicdrv.h文件,实现方法太长,简单来说:
1)就是分配收发缓冲区地址,打开硬件,再把数据包头写到每一个发送缓冲区首部,免得后续每次都写;
2)另外初始化了一些保护关键代码段的互斥锁。如果是裸跑的话,在保护关键代码的时候可能要考虑用开关中断来实现了。
3)再一个,这里实际上是可以打开第二个网口的。两个网口,一个作为输出,一个作为输入。这个按实际情况来做吧,目前见到的应用,多数是只用一个网口的。
/** Basic setup to connect NIC to socket.
* @param[in] port = port context struct
* @param[in] ifname = Name of NIC device, f.e. "eth0"
* @param[in] secondary = if >0 then use secondary stack instead of primary
* @return >0 if succeeded
*/
int ecx_setupnic(ecx_portt *port, const char *ifname, int secondary)
{...}
ecx_config_init函数
功能:枚举并初始化从站,返回发现从站的个数
定义在ethercatconfig.h文件,实现方法如下
/** Enumerate and init all slaves.
*
* @param[in] context = context struct
* @param[in] usetable = TRUE when using configtable to init slaves, FALSE otherwise
* @return Workcounter of slave discover datagram = number of slaves found
*/
int ecx_config_init(ecx_contextt *context, uint8 usetable)
{...}
ecx_config_map_group函数
功能:将所有从站pdo映射到IOmap
定义在ethercatconfig.h文件,实现方法如下
/** Map all PDOs in one group of slaves to IOmap with Outputs/Inputs
* in sequential order (legacy SOEM way).
*
* @param[in] context = context struct
* @param[out] pIOmap = pointer to IOmap
* @param[in] group = group to map, 0 = all groups
* @return IOmap size
*/
int ecx_config_map_group(ecx_contextt *context, void *pIOmap, uint8 group)
{...}
ecx_SDOwrite函数
功能:写入SDO数据
定义在ethercatcoe.h文件,实现方法如下
/** CoE SDO write, blocking. Single subindex or Complete Access.
*
* A "normal" download request is issued, unless we have
* small data, then a "expedited" transfer is used. If the parameter is larger than
* the mailbox size then the download is segmented. The function will split the
* parameter data in segments and send them to the slave one by one.
*
* @param[in] context = context struct
* @param[in] Slave = Slave number
* @param[in] Index = Index to write
* @param[in] SubIndex = Subindex to write, must be 0 or 1 if CA is used.
* @param[in] CA = FALSE = single subindex. TRUE = Complete Access, all subindexes written.
* @param[in] psize = Size in bytes of parameter buffer.
* @param[out] p = Pointer to parameter buffer
* @param[in] Timeout = Timeout in us, standard is EC_TIMEOUTRXM
* @return Workcounter from last slave response
*/
int ecx_SDOwrite(ecx_contextt *context, uint16 Slave, uint16 Index, uint8 SubIndex,
boolean CA, int psize, void *p, int Timeout)
{...}
ecx_SDOread函数
功能:读取SDO数据
定义在ethercatcoe.h文件,实现方法如下
/** CoE SDO read, blocking. Single subindex or Complete Access.
*
* Only a "normal" upload request is issued. If the requested parameter is <= 4bytes
* then a "expedited" response is returned, otherwise a "normal" response. If a "normal"
* response is larger than the mailbox size then the response is segmented. The function
* will combine all segments and copy them to the parameter buffer.
*
* @param[in] context = context struct
* @param[in] slave = Slave number
* @param[in] index = Index to read
* @param[in] subindex = Subindex to read, must be 0 or 1 if CA is used.
* @param[in] CA = FALSE = single subindex. TRUE = Complete Access, all subindexes read.
* @param[in,out] psize = Size in bytes of parameter buffer, returns bytes read from SDO.
* @param[out] p = Pointer to parameter buffer
* @param[in] timeout = Timeout in us, standard is EC_TIMEOUTRXM
* @return Workcounter from last slave response
*/
int ecx_SDOread(ecx_contextt *context, uint16 slave, uint16 index, uint8 subindex,
boolean CA, int *psize, void *p, int timeout)
{...}
ecx_statecheck函数
功能:检测从站状态
定义在ethercatmain.h文件,实现方法如下
/** Check actual slave state.
* This is a blocking function.
* To refresh the state of all slaves ecx_readstate()should be called
* @param[in] context = context struct
* @param[in] slave = Slave number, 0 = all slaves (only the "slavelist[0].state" is refreshed)
* @param[in] reqstate = Requested state
* @param[in] timeout = Timeout value in us
* @return Requested state, or found state after timeout.
*/
uint16 ecx_statecheck(ecx_contextt *context, uint16 slave, uint16 reqstate, int timeout)
{...}
simple_test.c文件
/** \file
* \brief Example code for Simple Open EtherCAT master
*
* Usage : simple_test [ifname1]
* ifname is NIC interface, f.e. eth0
*
* This is a minimal test.
*
* (c)Arthur Ketels 2010 - 2011
*/
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "ethercat.h"
#define EC_TIMEOUTMON 500
char IOmap[4096]; //映射空间
OSAL_THREAD_HANDLE thread1;
int expectedWKC;
boolean needlf;
volatile int wkc;
boolean inOP;
uint8 currentgroup = 0;
void simpletest(char *ifname)
{
int i, j, oloop, iloop, chk;
needlf = FALSE;
inOP = FALSE;
printf("Starting simple test\n");
/* initialise SOEM, bind socket to ifname */
if (ec_init(ifname)) //初始化soem,将主站绑定到网口
{
printf("ec_init on %s succeeded.\n",ifname);
/* find and auto-config slaves */
if ( ec_config_init(FALSE) > 0 ) //配置从站
{
printf("%d slaves found and configured.\n",ec_slavecount);
ec_config_map(&IOmap); //主站内存空间与从站内存空间映射
ec_configdc(); //配置DC时钟参数
printf("Slaves mapped, state to SAFE_OP.\n");
/* wait for all slaves to reach SAFE_OP state */
ec_statecheck(0, EC_STATE_SAFE_OP, EC_TIMEOUTSTATE * 4); //运行到安全运行状态
//将主站输入输出字节上下限设置为1和8
oloop = ec_slave[0].Obytes;
if ((oloop == 0) && (ec_slave[0].Obits > 0)) oloop = 1;
if (oloop > 8) oloop = 8;
iloop = ec_slave[0].Ibytes;
if ((iloop == 0) && (ec_slave[0].Ibits > 0)) iloop = 1;
if (iloop > 8) iloop = 8;
printf("segments : %d : %d %d %d %d\n",ec_group[0].nsegments ,ec_group[0].IOsegment[0],ec_group[0].IOsegment[1],ec_group[0].IOsegment[2],ec_group[0].IOsegment[3]);
//工作计数
printf("Request operational state for all slaves\n");
expectedWKC = (ec_group[0].outputsWKC * 2) + ec_group[0].inputsWKC;
printf("Calculated workcounter %d\n", expectedWKC);
ec_slave[0].state = EC_STATE_OPERATIONAL;
//激活从站输出
/* send one valid process data to make outputs in slaves happy*/
ec_send_processdata();
//接收数据
ec_receive_processdata(EC_TIMEOUTRET);
/* request OP state for all slaves */
//写入状态
ec_writestate(0);
chk = 200;
//等待所有从站进入运行状态
/* wait for all slaves to reach OP state */
do
{
ec_send_processdata();
ec_receive_processdata(EC_TIMEOUTRET);
ec_statecheck(0, EC_STATE_OPERATIONAL, 50000);
}
while (chk-- && (ec_slave[0].state != EC_STATE_OPERATIONAL));
//进入运行状态
if (ec_slave[0].state == EC_STATE_OPERATIONAL )
{
printf("Operational state reached for all slaves.\n");
inOP = TRUE;
/* cyclic loop */
for(i = 1; i <= 10000; i++)
{
ec_send_processdata();
wkc = ec_receive_processdata(EC_TIMEOUTRET);
if(wkc >= expectedWKC)
{
printf("Processdata cycle %4d, WKC %d , O:", i, wkc);
for(j = 0 ; j < oloop; j++)
{
printf(" %2.2x", *(ec_slave[0].outputs + j));
}
printf(" I:");
for(j = 0 ; j < iloop; j++)
{
printf(" %2.2x", *(ec_slave[0].inputs + j));
}
printf(" T:%"PRId64"\r",ec_DCtime);
needlf = TRUE;
}
osal_usleep(5000);
}
inOP = FALSE;
}
//未进入运行状态
else
{
printf("Not all slaves reached operational state.\n");
ec_readstate(); //读取状态
for(i = 1; i<=ec_slavecount ; i++)
{
if(ec_slave[i].state != EC_STATE_OPERATIONAL)
{
printf("Slave %d State=0x%2.2x StatusCode=0x%4.4x : %s\n",
i, ec_slave[i].state, ec_slave[i].ALstatuscode, ec_ALstatuscode2string(ec_slave[i].ALstatuscode));
}
}
}
printf("\nRequest init state for all slaves\n");
ec_slave[0].state = EC_STATE_INIT;
//初始化所有从站
/* request INIT state for all slaves */
ec_writestate(0);
}
else
{
printf("No slaves found!\n");
}
printf("End simple test, close socket\n");
/* stop SOEM, close socket */
ec_close();
}
else
{
printf("No socket connection on %s\nExecute as root\n",ifname);
}
}
//检测
OSAL_THREAD_FUNC ecatcheck( void *ptr )
{
int slave;
(void)ptr; /* Not used */
while(1)
{
if( inOP && ((wkc < expectedWKC) || ec_group[currentgroup].docheckstate))
{
if (needlf)
{
needlf = FALSE;
printf("\n");
}
/* one ore more slaves are not responding */
ec_group[currentgroup].docheckstate = FALSE;
ec_readstate();
for (slave = 1; slave <= ec_slavecount; slave++)
{
if ((ec_slave[slave].group == currentgroup) && (ec_slave[slave].state != EC_STATE_OPERATIONAL))
{
ec_group[currentgroup].docheckstate = TRUE;
if (ec_slave[slave].state == (EC_STATE_SAFE_OP + EC_STATE_ERROR))
{
printf("ERROR : slave %d is in SAFE_OP + ERROR, attempting ack.\n", slave);
ec_slave[slave].state = (EC_STATE_SAFE_OP + EC_STATE_ACK);
ec_writestate(slave);
}
else if(ec_slave[slave].state == EC_STATE_SAFE_OP)
{
printf("WARNING : slave %d is in SAFE_OP, change to OPERATIONAL.\n", slave);
ec_slave[slave].state = EC_STATE_OPERATIONAL;
ec_writestate(slave);
}
else if(ec_slave[slave].state > EC_STATE_NONE)
{
if (ec_reconfig_slave(slave, EC_TIMEOUTMON))
{
ec_slave[slave].islost = FALSE;
printf("MESSAGE : slave %d reconfigured\n",slave);
}
}
else if(!ec_slave[slave].islost)
{
/* re-check state */
ec_statecheck(slave, EC_STATE_OPERATIONAL, EC_TIMEOUTRET);
if (ec_slave[slave].state == EC_STATE_NONE)
{
ec_slave[slave].islost = TRUE;
printf("ERROR : slave %d lost\n",slave);
}
}
}
if (ec_slave[slave].islost)
{
if(ec_slave[slave].state == EC_STATE_NONE)
{
if (ec_recover_slave(slave, EC_TIMEOUTMON))
{
ec_slave[slave].islost = FALSE;
printf("MESSAGE : slave %d recovered\n",slave);
}
}
else
{
ec_slave[slave].islost = FALSE;
printf("MESSAGE : slave %d found\n",slave);
}
}
}
if(!ec_group[currentgroup].docheckstate)
printf("OK : all slaves resumed OPERATIONAL.\n");
}
osal_usleep(10000);
}
}
int main(int argc, char *argv[])
{
printf("SOEM (Simple Open EtherCAT Master)\nSimple test\n");
if (argc > 1)
{
/* create thread to handle slave error handling in OP */
// pthread_create( &thread1, NULL, (void *) &ecatcheck, (void*) &ctime);
osal_thread_create(&thread1, 128000, &ecatcheck, (void*) &ctime);
/* start cyclic part */
simpletest(argv[1]);
}
else
{
printf("Usage: simple_test ifname1\nifname = eth0 for example\n");
}
printf("End program\n");
return (0);
}
更多推荐




所有评论(0)