Bootstrap

SOEM协议栈代码研读笔记(一)

        最近搞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。

;