最近搞EtherCAT。EtherCAT协议栈目前无非就是几种选择,要么花钱买商用的,要么IGH或者SOEM。IGH算是实现得比较全,不过只能在Linux操作系统上运行。Linux大家都知道,并非实时操作系统,于是就有了一些实时补丁,比较典型的Xenomi,就是独立于Linux的一个内核。移植Xenomai,倒也是一个思路。不过毕竟在Linux的底子上,想要进一步提升总线周期的稳定性,恐怕要从Linux内核层入手进行深度改造了。好消息是暂时并不需要把功能做得比较全,这样简单一些得SOEM在MCU上裸跑也是个不错(偷懒)的选择。网上关于SOEM移植的文章很多,参照官方例程依葫芦画瓢也问题不大。倒是详细读一下SOEM的源代码的文章比较少。所以,借着这个机会,做些笔记,一方面整理一下自己的思路,另一方面也希望对大家有点小帮助。
SOEM版本:1.4.0.
只是声明两点:
1 纯个人理解,未必正确,欢迎各位斧正;
2 第一次写,而且是想到哪里写到哪里,条理可能不够清晰,更加不可能面面俱到;
3 个人创作,转载请注明出处。
SOEM,官方下来的源代码主要在SOEM, OSAL,OSHW三个文件夹中。其中SOEM是协议栈的内核,OSAL主要提供一些定时器,互斥锁,线程管理等操作系统的支持。OSHW提供以太网的硬件支持。一般来说,移植主要从OSAL,OSHW两个文件夹入手,不过本文重点不在移植,而在代码解读,侧重点自然会有所区别。
那么,从哪里开始呢?一般来说,这种情况最好的是官方的DOC,但是可惜,官方的DOC似乎没有比较详细的说明,不过,还好有例程。
官方提供的simpletest就很好,可以拿它作为打开这扇大门的钥匙。simpletest基本上刚开始就是如下这一段:
/* create thread to handle slave error handling in OP */
osal_thread_create(&thread1, 128000, &ecatcheck, (void*)&ctime);
strcpy(ifbuf, argv[1]);
/* start cyclic part */
simpletest(ifbuf);
这一段很简单,先开线程,几乎所有操作系统都可以做到。在Windows上,直接就是调用的CreateThread:
int osal_thread_create(void **thandle, int stacksize, void *func, void *param)
{
*thandle = CreateThread(NULL, stacksize, func, param, 0, NULL);
if(!thandle)
{
return 0;
}
return 1;
}
在RTK上也是类似:
int osal_thread_create(void *thandle, int stacksize, void *func, void *param)
{
thandle = task_spawn ("worker", func, 6,stacksize, param);
if(!thandle)
{
return 0;
}
return 1;
}
如此说来,最后传的void* param在这个例子当中并没有用到,不过,想要利用起来也没有问题,无非就是看想传什么参数了。
这个例子里面,是这样的:
OSAL_THREAD_FUNC ecatcheck(void* lpParam)
{
int slave;
while (1)
{
if (inOP && ((wkc < expectedWKC) || ec_group[currentgroup].docheckstate))
{
……
}
osal_usleep(10000);
}
return (void)0;
}
关于细节以后有需要再说。不过整体上可以看到,无非就是周期性扫描一下各个slave的状态,必要的时候进行一些恢复性的设置。这个例子当中,是先开线程后初始化设备,其实没必要。
需要注意的是,这里并没有直接进行过程数据的收发,总线的周期并不是由这个Thread来定的。所以,这个优先级不高,且对实时性要求也不高。
当然,没有操作系统也无所谓,一个周期性定时器中断也可以,这样的话,osal_usleep(10000)这个调用也省了。不过需要注意一点是互斥处理,不建议频繁开关中断(尤其是处理过程数据的中断)的方式。
开了线程之后,调用simpletest函数,并把网卡名作为输入参数。对于自己做的板卡,操作的网卡是哪一个自己知道,所以这个网卡名也不是必须的。
接下来要进入正题了。进入simpletest,第一步是调用ec_init函数:
int ec_init(const char * ifname)
{
return ecx_init(&ecx_context, ifname);
}
这个ifname是为了区分不同的网卡,在windows上有用,如果是自己的板子上做移植,就不需要,传个空指针就可以。这里需要注意一下ecx_context:
这个结构体定义如下:
struct ecx_context
{
/** port reference, may include red_port */
ecx_portt *port;
/** slavelist reference */
ec_slavet *slavelist;
/** number of slaves found in configuration */
int *slavecount;
/** maximum number of slaves allowed in slavelist */
int maxslave;
/** grouplist reference */
ec_groupt *grouplist;
/** maximum number of groups allowed in grouplist */
int maxgroup;
/** internal, reference to eeprom cache buffer */
uint8 *esibuf;
/** internal, reference to eeprom cache map */
uint32 *esimap;
/** internal, current slave for eeprom cache */
uint16 esislave;
/** internal, reference to error list */
ec_eringt *elist;
/** internal, reference to processdata stack buffer info */
ec_idxstackT *idxstack;
/** reference to ecaterror state */
boolean *ecaterror;
/** reference to last DC time from slaves */
int64 *DCtime;
/** internal, SM buffer */
ec_SMcommtypet *SMcommtype;
/** internal, PDO assign list */
ec_PDOassignt *PDOassign;
/** internal, PDO description list */
ec_PDOdesct *PDOdesc;
/** internal, SM list from eeprom */
ec_eepromSMt *eepSM;
/** internal, FMMU list from eeprom */
ec_eepromFMMUt *eepFMMU;
/** registered FoE hook */
int (*FOEhook)(uint16 slave, int packetnumber, int datasize);
/** registered EoE hook */
int (*EOEhook)(ecx_contextt * context, uint16 slave, void * eoembx);
/** flag to control legacy automatic state change or manual state change */
int manualstatechange;
/** userdata, promotes application configuration esp. in EC_VER2 with multiple
* ec_context instances. Note: userdata memory is managed by application, not SOEM */
void *userdata;
};
typedef struct ecx_context ecx_contextt;
于是,变量ecx_context又把总线运行的一系列信息放在了这里:
ecx_contextt ecx_context = {
&ecx_port, // .port =
&ec_slave[0], // .slavelist =
&ec_slavecount, // .slavecount =
EC_MAXSLAVE, // .maxslave =
&ec_group[0], // .grouplist =
EC_MAXGROUP, // .maxgroup =
&ec_esibuf[0], // .esibuf =
&ec_esimap[0], // .esimap =
0, // .esislave =
&ec_elist, // .elist =
&ec_idxstack, // .idxstack =
&EcatError, // .ecaterror =
&ec_DCtime, // .DCtime =
&ec_SMcommtype[0], // .SMcommtype =
&ec_PDOassign[0], // .PDOassign =
&ec_PDOdesc[0], // .PDOdesc =
&ec_SM, // .eepSM =
&ec_FMMU, // .eepFMMU =
NULL, // .FOEhook()
NULL, // .EOEhook()
0, // .manualstatechange
NULL, // .userdata
};
这里面初步的介绍,注释上都有说明,具体内容后续涉及到的时候再深究。不过这里吐槽一下SOEM的代码风格,里面用了很多类似这种方式:
&ec_group[0];
或者
int64 *DCtime;
有的地方甚至指针套了好几层,总觉得没必要,而且容易出错。
再然后,ecx_init调用了ecx_setupnic。
int ecx_init(ecx_contextt *context, const char * ifname)
{
return ecx_setupnic(context->port, ifname, FALSE);
}
这里三层外三层的,简直是个俄罗斯套娃。不过好在,我们终于找到正主了:
int ecx_setupnic(ecx_portt *port, const char *ifname, int secondary)
{
int i, rval;
pcap_t **psock;
rval = 0;
if (secondary)
{
/* secondary port stuct available? */
if (port->redport)
{
/* when using secondary socket it is automatically a redundant setup */
psock = &(port->redport->sockhandle);
*psock = NULL;
port->redstate = ECT_RED_DOUBLE;
port->redport->stack.sock = &(port->redport->sockhandle);
port->redport->stack.txbuf = &(port->txbuf);
port->redport->stack.txbuflength = &(port->txbuflength);
port->redport->stack.tempbuf = &(port->redport->tempinbuf);
port->redport->stack.rxbuf = &(port->redport->rxbuf);
port->redport->stack.rxbufstat = &(port->redport->rxbufstat);
port->redport->stack.rxsa = &(port->redport->rxsa);
ecx_clear_rxbufstat(&(port->redport->rxbufstat[0]));
}
else
{
/* fail */
return 0;
}
}
else
{
InitializeCriticalSection(&(port->getindex_mutex));
InitializeCriticalSection(&(port->tx_mutex));
InitializeCriticalSection(&(port->rx_mutex));
port->sockhandle = NULL;
port->lastidx = 0;
port->redstate = ECT_RED_NONE;
port->stack.sock = &(port->sockhandle);
port->stack.txbuf = &(port->txbuf);
port->stack.txbuflength = &(port->txbuflength);
port->stack.tempbuf = &(port->tempinbuf);
port->stack.rxbuf = &(port->rxbuf);
port->stack.rxbufstat = &(port->rxbufstat);
port->stack.rxsa = &(port->rxsa);
ecx_clear_rxbufstat(&(port->rxbufstat[0]));
psock = &(port->sockhandle);
}
/* we use pcap socket to send RAW packets in windows user mode*/
*psock = pcap_open(ifname, 65536, PCAP_OPENFLAG_PROMISCUOUS |
PCAP_OPENFLAG_MAX_RESPONSIVENESS |
PCAP_OPENFLAG_NOCAPTURE_LOCAL, -1, NULL , errbuf);
if (NULL == *psock)
{
printf("interface %s could not open with pcap\n", ifname);
return 0;
}
for (i = 0; i < EC_MAXBUF; i++)
{
ec_setupheader(&(port->txbuf[i]));
port->rxbufstat[i] = EC_BUF_EMPTY;
}
ec_setupheader(&(port->txbuf2));
return 1;
}
这个函数在oshw文件夹下的nicdrv.c文件中,不同的硬件,这里肯定不一样。上面的例子是基于win32的。对比一下rtk的版本:
int ecx_setupnic(ecx_portt *port, const char *ifname, int secondary)
{
int i;
int rVal;
int *psock;
port->getindex_mutex = mtx_create();
port->tx_mutex = mtx_create();
port->rx_mutex = mtx_create();
rVal = bfin_EMAC_init((uint8_t *)priMAC);
if (rVal != 0)
return 0;
if (secondary)
{
/* secondary port stuct available? */
if (port->redport)
{
/* when using secondary socket it is automatically a redundant setup */
psock = &(port->redport->sockhandle);
*psock = -1;
port->redstate = ECT_RED_DOUBLE;
port->redport->stack.sock = &(port->redport->sockhandle);
port->redport->stack.txbuf = &(port->txbuf);
port->redport->stack.txbuflength = &(port->txbuflength);
port->redport->stack.tempbuf = &(port->redport->tempinbuf);
port->redport->stack.rxbuf = &(port->redport->rxbuf);
port->redport->stack.rxbufstat = &(port->redport->rxbufstat);
port->redport->stack.rxsa = &(port->redport->rxsa);
ecx_clear_rxbufstat(&(port->redport->rxbufstat[0]));
}
else
{
/* fail */
return 0;
}
}
else
{
port->getindex_mutex = mtx_create();
port->tx_mutex = mtx_create();
port->rx_mutex = mtx_create();
port->sockhandle = -1;
port->lastidx = 0;
port->redstate = ECT_RED_NONE;
port->stack.sock = &(port->sockhandle);
port->stack.txbuf = &(port->txbuf);
port->stack.txbuflength = &(port->txbuflength);
port->stack.tempbuf = &(port->tempinbuf);
port->stack.rxbuf = &(port->rxbuf);
port->stack.rxbufstat = &(port->rxbufstat);
port->stack.rxsa = &(port->rxsa);
ecx_clear_rxbufstat(&(port->rxbufstat[0]));
psock = &(port->sockhandle);
}
/* setup ethernet headers in tx buffers so we don't have to repeat it */
for (i = 0; i < EC_MAXBUF; i++)
{
ec_setupheader(&(port->txbuf[i]));
port->rxbufstat[i] = EC_BUF_EMPTY;
}
ec_setupheader(&(port->txbuf2));
return 1;
}
两者的处理大同小异。简单来说,就是分配收发缓冲区地址,打开硬件,再把数据包头写到每一个发送缓冲区首部,免得后续每次都写。另外初始化了一些保护关键代码段的互斥锁。如果是裸跑的话,在保护关键代码的时候可能要考虑用开关中断来实现了。再一个,可以看到,这里实际上是可以打开第二个网口的。两个网口,一个作为输出,一个作为输入。这个按实际情况来做吧,目前见到的应用,多数是只用一个网口的。
这里的重点在于这个port。可以看到,实际上这里是用了在ethercatmain.c文件中定义的全局变量:
ecx_portt ecx_port;
还记得之前说的那个ecx_context吗?对,这个ecx_port就是ecx_context里面的。
在nicdrv.h文件中,这个exc_portt定义如下:
typedef struct
{
ec_stackT stack;
pcap_t *sockhandle;
/** rx buffers */
ec_bufT rxbuf[EC_MAXBUF];
/** rx buffer status */
int rxbufstat[EC_MAXBUF];
/** rx MAC source address */
int rxsa[EC_MAXBUF];
/** temporary rx buffer */
ec_bufT tempinbuf;
/** temporary rx buffer status */
int tempinbufs;
/** transmit buffers */
ec_bufT txbuf[EC_MAXBUF];
/** transmit buffer lenghts */
int txbuflength[EC_MAXBUF];
/** temporary tx buffer */
ec_bufT txbuf2;
/** temporary tx buffer length */
int txbuflength2;
/** last used frame index */
int lastidx;
/** current redundancy state */
int redstate;
/** pointer to redundancy port and buffers */
ecx_redportt *redport;
CRITICAL_SECTION getindex_mutex;
CRITICAL_SECTION tx_mutex;
CRITICAL_SECTION rx_mutex;
} ecx_portt;
这个nicdrv.h和具体的硬件和平台有关,上面这一段是win32的。再来看看rtk的:
typedef struct
{
ec_stackT stack;
int sockhandle;
/** rx buffers */
ec_bufT rxbuf[EC_MAXBUF];
/** rx buffer status */
int rxbufstat[EC_MAXBUF];
/** rx MAC source address */
int rxsa[EC_MAXBUF];
/** temporary rx buffer */
ec_bufT tempinbuf;
/** temporary rx buffer status */
int tempinbufs;
/** transmit buffers */
ec_bufT txbuf[EC_MAXBUF];
/** transmit buffer lenghts */
int txbuflength[EC_MAXBUF];
/** temporary tx buffer */
ec_bufT txbuf2;
/** temporary tx buffer length */
int txbuflength2;
/** last used frame index */
int lastidx;
/** current redundancy state */
int redstate;
/** pointer to redundancy port and buffers */
ecx_redportt *redport;
mtx_t * getindex_mutex;
mtx_t * tx_mutex;
mtx_t * rx_mutex;
} ecx_portt;
依然是大同小异。不同的是sockhandle的定义,以及几个前文提到的互斥量。也只是形式上的不同而异。
当然,这里还包括这个ec_stackT结构体类型,win32下,其定义如下:
typedef struct
{
/** socket connection used */
pcap_t **sock;
/** tx buffer */
ec_bufT (*txbuf)[EC_MAXBUF];
/** tx buffer lengths */
int (*txbuflength)[EC_MAXBUF];
/** temporary receive buffer */
ec_bufT *tempbuf;
/** rx buffers */
ec_bufT (*rxbuf)[EC_MAXBUF];
/** rx buffer status fields */
int (*rxbufstat)[EC_MAXBUF];
/** received MAC source address (middle word) */
int (*rxsa)[EC_MAXBUF];
} ec_stackT;
在rtk的定义如下:
typedef struct
{
/** socket connection used */
int *sock;
/** tx buffer */
ec_bufT (*txbuf)[EC_MAXBUF];
/** tx buffer lengths */
int (*txbuflength)[EC_MAXBUF];
/** temporary receive buffer */
ec_bufT *tempbuf;
/** rx buffers */
ec_bufT (*rxbuf)[EC_MAXBUF];
/** rx buffer status fields */
int (*rxbufstat)[EC_MAXBUF];
/** received MAC source address (middle word) */
int (*rxsa)[EC_MAXBUF];
} ec_stackT;
除了sock之外,一模一样。这里EC_MAXBUF是ethercattype.h文件当中的宏定义:
#define EC_MAXBUF 16
该文件在SOEM文件夹当中。还有ec_bufT的定义
#define EC_MAXECATFRAME 1518
#define EC_BUFSIZE EC_MAXECATFRAME
typedef uint8 ec_bufT[EC_BUFSIZE];
这个1518数字看着眼熟。看看EtherCAT数据帧格式就一目了然了:
请原谅,图当然是别处找的。
这个ec_bufT就是收发缓冲区。同样的,吐槽一下类似这样的方式:
ec_bufT (*rxbuf)[EC_MAXBUF];
这个直接拿来用可能不觉得怎样,如果要修改的话,挺别扭的。
读到这里,其实如果有必要,可以对ecx_portt进行一些自己的改造,譬如说,不希望编译器自动给缓冲区分配地址,就把对应的缓冲区声明为指针,然后在初始化时,对ecx_port当中对应的指针赋初值,使其指向需要的地址。
顺便也说一下这个ecx_redportt结构体:
typedef struct
{
ec_stackT stack;
pcap_t *sockhandle;
/** rx buffers */
ec_bufT rxbuf[EC_MAXBUF];
/** rx buffer status */
int rxbufstat[EC_MAXBUF];
/** rx MAC source address */
int rxsa[EC_MAXBUF];
/** temporary rx buffer */
ec_bufT tempinbuf;
} ecx_redportt;
依然是在nicdrv.h当中定义。这就是一个只有接收功能的ecx_portt的阉割版本,没有发送缓冲区及其状态标志,因为收发过程中的关键代码保护互斥量已经有了,所以这里连互斥量都省了。细心的你可能在ecx_portt当中发现一个问题:
/** temporary rx buffer */
ec_bufT tempinbuf;
/** temporary rx buffer status */
int tempinbufs;
/** transmit buffers */
ec_bufT txbuf[EC_MAXBUF];
/** transmit buffer lenghts */
int txbuflength[EC_MAXBUF];
/** temporary tx buffer */
ec_bufT txbuf2;
收发缓冲区明明已经定义了,为什么又多出来了临时收发缓冲区?而且这两个缓冲区在ecx_setupnic函数里面是单独进行初始化的。整个放在一起来处理不是更清晰方便?这个问题要留在收发处理当中去解答了。这里要吐槽一下这个tempinbuf和txbuf2的命名,真的有点混乱。
还有这两个函数:
void ec_setupheader(void *p)
{
ec_etherheadert *bp;
bp = p;
bp->da0 = htons(0xffff);
bp->da1 = htons(0xffff);
bp->da2 = htons(0xffff);
bp->sa0 = htons(priMAC[0]);
bp->sa1 = htons(priMAC[1]);
bp->sa2 = htons(priMAC[2]);
bp->etype = htons(ETH_P_ECAT);
}
static void ecx_clear_rxbufstat(int *rxbufstat)
{
int i;
for(i = 0; i < EC_MAXBUF; i++)
{
rxbufstat[i] = EC_BUF_EMPTY;
}
}
这里是win32版本的,没什么好深究的。这次先写到这里吧。这次只涉及到点皮毛,下次写数据收发,缓冲和FMMU。