1、简介
MECM(Mini Ethercat Master),名字随便起的。已经学习了一段时间的Ethercat总线了,目前的想法就是自己简单实现一个Ethercat主站,没有太多的冗余功能,暂时不考虑太多的容错机制,仅实现目前用到的FOE、COE、过程数据通信这三个功能,仅用于学习和加深理解。主站的硬件是GD32F450Z_EVAL开发板,板载的以太网芯片是DP83848VV。
2、底层编写
从根本来讲,Ethercat主站就是用来读写从站的。因此驱动的最核心功能有三个:网卡初始化、写入据到从站、读取从站数据。还有一个非必要功能,就是系统时钟,用来超时计数,也算是个简单的容错。当然,如果你不做超时也可以,选择发送和接收时一直死等也可以。
1、网卡初始化
关于GD32的以太网理解和初始化流程,请看文章Ethercat学习-GD32以太网学习 下面是部分代码,代码的初始化流程也是按照文章中的流程来的。
/* 初始化代码片段 */
int enet_system_init(void)
{
ErrStatus reval_state = ERROR;
/* 初始化以太网模块时钟 */
enet_clock_init();
/* 初始化以太网引脚 RMII接口*/
enet_gpio_init();
/* 初始化以太网MAC和DMA */
reval_state = enet_mac_dma_config();
return reval_state;
}
/* MAC 和 DMA 的初始化 */
static int enet_mac_dma_config(void)
{
int i;
ErrStatus reval_state = ERROR;
/* 复位,并等待完成 */
enet_deinit();
reval_state = enet_software_reset();
if(ERROR == reval_state) {
return ERROR;
}
/* 以太网初始化 配置网卡自协商模式、使能接收端校验和检测功能、接收所有的广播帧*/
reval_state = enet_init(ENET_AUTO_NEGOTIATION, ENET_AUTOCHECKSUM_DROP_FAILFRAMES, ENET_BROADCAST_FRAMES_PASS);
if(ERROR == reval_state) {
return ERROR;
}
/* 设置MAC地址 */
enet_mac_address_set(ENET_MAC_ADDRESS0, mac);
/* 初始化收发的描述符,常规描述符,链结构*/
enet_descriptors_chain_init(ENET_DMA_TX);
enet_descriptors_chain_init(ENET_DMA_RX);
/* 设置接收描述符 RDES1的31位为0,接收完成后会立马产生中断*/
for(i=0; i<ENET_RXBUF_NUM; i++){
enet_rx_desc_immediate_receive_complete_interrupt(&rxdesc_tab[i]);
}
/* 使能硬件IP包头和数据域的校验和计算和插入 */
for(i=0; i < ENET_TXBUF_NUM; i++){
enet_transmit_checksum_config(&txdesc_tab[i], ENET_CHECKSUM_TCPUDPICMP_FULL);
}
// /* 正常中断汇总使能 接收中断属于正常中断 */
// enet_interrupt_enable(ENET_DMA_INT_NIE);
// /* 使能接收中断 */
// enet_interrupt_enable(ENET_DMA_INT_RIE);
/* 使能以太网 */
enet_enable();
return reval_state;
}
2、数据读写
数据的读写很简单,读数据采用的时轮询的方式,不是中断的方式。
/**
* @******************************************************************************:
* @func: [enet_buf_send]
* @description: 以太网数据发送
* @note:
* @author:
* @param [uint8_t] *buf 待发送数据的地址
* @param [uint32_t] length 待发送数据的长度
* @return [*]
* @==============================================================================:
*/
int enet_buf_send(uint8_t *buf,uint32_t length)
{
ErrStatus reval_state = ERROR;
reval_state = enet_frame_transmit(buf,length);
return reval_state;
}
/**
* @******************************************************************************:
* @func: [enet_buf_send]
* @description: 以太网数据接收
* @note:
* @author:
* @param [uint8_t] *buf 接收数据的地址
* @param [uint32_t] length 接收数据缓存的最大长度
* @return [*]
* @==============================================================================:
*/
int enet_buf_recv(uint8_t *buf,uint32_t length)
{
ErrStatus reval_state;
if(enet_rxframe_size_get()){
reval_state = enet_frame_receive(buf,length);
}else{
reval_state = 0;
}
return reval_state;
}
3、系统时钟
系统时钟的思路就是用滴答定时器,每1us记一次数,这个数就时我们的系统时钟,这个数我们只用来做超时检测。
/**
* @******************************************************************************:
* @func: [osal_current_time]
* @description: 返回系统时钟计数,单位us
* @note:
* @author:
* @return [*]
* @==============================================================================:
*/
uint64_t osal_current_time_us(void)
{
return timercount;
}
3、数据打包
1、Ethercat报文简介
基本接口已经写好,接下来就是按照Ethercat的数据包对数据进行打包,然后将发送出去。
如图所示,一个标准的ethercat包括了6部分:
名称 | 说明 | 长度 |
---|---|---|
Destination | 目的MAC地址,这个可以暂时忽略,在简单的连接中,这个是真的无关紧要 | 6byte |
Source | 本机MAC地址,这个可以暂时忽略,在简单的连接中,这个是也无关紧要 | 6byte |
EthercatType | 0x88A4 | 2byte |
EthercatHeader | 有11bit的length、1bit的预留、4bit的type组成 | 2byte |
Datagrams | 数据包 | 46~1500byte |
FCS | 帧校验序列,这个不需要我们去填写,自动生成 | 4byte |
这里我们关注两个部分EthercatHeader和Datagrams,其他的要么是固定的,要么是自动生成的,只有这两个需要我们字节计算。另外可以看到图中所示,ethercat报文最大长度写的是1522,这是因为它算上了VLAN的4byte,我们用的Ethercat总长度是1518。ethercat报文的最小长度为64,而Datagrams的最小长度就是64-6-6-2-4 = 46。如果我们的Datagrams小于44个长度,MAC子系统会自动将剩余的补齐。如下图所示,图中的pad bytes就是系统补齐的。
EthercatHeader中有一个length,这个长度就是Datagrams的长度,但是它不包含补齐的长度,也不包含FCS的长度,例如我们自己的数据是10个字节,然后系统又给我们填充了34个自己凑够了64字节,那么这个length的值依然是10。EthercatHeader中的type只能为0x01。
2、Datagrams报文简介
Datagrams可以只包含一个Datagram,也可以是好多个Datagram拼接起来的。如下图所示:
在一包Datagram中又分为了三部分,DatagramHeader、data、WKC。DatagramHeader固定长度为10,说明如下:
名称 | 说明 | 长度 |
---|---|---|
Cmd | Ethercat命令,用来表明该数据包的作用:LWR、BRD、FPRW、APRW等 | 1byte |
idx | 索引,我们字节生成的,不会被从机修改,可以判断数据是否重复或者丢失 | 1byte |
Address | 地址。除了逻辑地址是站四个字节外,其他地址都是一个从机地址(2byte)+内存地址(2byte) | 4byte |
Len | 数据包中的数据长度,也就是data的长度 | 11bit |
R | 预留,0 | 1bit |
C | 帧循环,由从机的处理单元自动置位,用来识别该帧是否已经循环了,例如从机下次又收到了相同的帧,但是帧循环这里已经置1了,说明该帧已经处理过一次了,由于某种原因在重复循环。 | 1bit |
M | 该数据包是否是最后一包,1表示后续还有数据包 | 1bit |
IRQ | 所有从站ethercat事件请求寄存器的OR,主机发送时写0就好了。值是在从站读取出来的。 | 2byte |
3、数据组包
int mecm_BRD_test()
{
int i = 0;
mecm_fram_t fram;
mecm_datagram_header_t dg_header;
/* 填充Destination、Source、EthercatType */
for(i = 0;i<MAXBUF;i++)
{
memcpy( fram.en_header.dst,dstMAC,6);
memcpy( fram.en_header.src,srcMAC,6);
fram.en_header.etype = 0x88A4;
}
/* 填充Ethercat header */
fram.ec_header.clehgth = 13;
fram.ec_header.reserve = 0;
fram.ec_header.ctype = 0x01;
/* 填充数据包的datagram header */
dg_header.cmd = CMD_BRD;
dg_header.idx = 1;
dg_header.addr.np.adp = 0x0001;
dg_header.addr.np.ado = 0x0508;
/* 数据长度为 1 */
dg_header.dlength = 1;
dg_header.irq = 0x0000;
memcpy((uint8_t *)fram.datagram,(uint8_t *)&dg_header,10);
/* 填充数据包 data */
fram.datagram[10] = 0x01;
/* 填充WCK 发送时置 0*/
uint16_t wck = 0;
memcpy((uint8_t *)&fram.datagram[11],(uint8_t *)&wck,2);
mecm_data_send((uint8_t *)&fram,29);
}
上电测试,开发板于电脑网口连接,用wireshark抓包,结果如下:
可以看到,虽然数据发出来了,但是wireshark识别的并不时Ethercat包,0x88A4 变成了0xA488。这里要把上面的0x88A4改为0xA488,主要是因为本地是小端存储,而网络字节序中在前的被认为是高字节,改完之后可以看到,ethercat包被正确识别出来了,结果如下:
4、其他
关于网络字节序的理解。我们用的单片机时小端存储,低字节的放在低地址处,数据在传输的时候是由低到高。因此我们的0x88A4,首先传输的时0xA4。但是到了对方网卡那里,它是按照网络字节序来识别的,而网络字节序统一是高字节在前,因此网卡在识别协议的时候先到达的0xA4被认为是高字节,所以就被识别为了0xA488。因此我们在传输前可以将网络协议部分的报文转换为网络字节序,例如MAC地址,以太网类型等报文,这样网卡在识别网络协议的时候可以识别出来。至于数据部分,我们可以不需要管,因为主机和从机都是小端存储,不会出错。当然,如果接收方是大端的话,数据部分需要转换为大端再传输,或者让接收方自己收到数据后自己转换。