开源EtherCAT主站SOEM入门使用
文章目录
目的
去年工作上用到了EtherCAT,EtherCAT同学是分主从的,作为学习测试来说主站最常用的是TwinCAT。不过TwinCAT支持的网卡信号有限,又没法应用到各种平台上。所以实际使用时又找了一些主站方案,适应性最广的就是SOEM库(Simple Open EtherCAT Master Library)。
项目地址:https://github.com/OpenEtherCATsociety/SOEM
文档地址:https://docs.rt-labs.com/soem/
SOEM库目前主要分为1.4.0版本和2.0.0版本,前者仓库中自带文档,后者文档需要去RT-Labs官网看(需要注册账号),本文将使用2.0.0版本:
本文测试时EtherCAT从站使用了LAN9252和AX58100,找那种支持DIO独立工作的板子:

这两个从站芯片都支持不依赖外部的控制器独立工作在DIO模式下。LAN9252支持总共16通道DIO,AX58100支持总共32路DIO。
上手体验
Releases中包含了已经编译好的示例和静态库,示例可以直接拿来体验:
需要注意的是Windows上需要安装Npcap才可以(如果有问题可以尝试安装WinPcap):
https://npcap.com/
https://www.winpcap.org/
对于EtherCAT主站来说其实就是收发特定格式的以太网报文而已,但是Windows上默认是不支持收发以太网原始帧的,需要借助诸如Npcap等库。
slaveinfo 示例可以读取EEPROM的数据。直接运行会提示用法:
指定网卡运行:
simple_ng 示例是个最简单的读写信号示例:
基础使用
SOEM库提供的是基础的EtherCAT数据帧的收发,应用层的功能和网络管理都是需要用户写代码手动处理的:
SOEM is a library that lets your application send and receive EtherCAT frames. It does not contain any logic for maintaining the EtherCAT network in an operational state; however, it has all the building blocks for doing so. In SOEM, it is the user application’s responsibility to handle these tasks:Reading and writing process data.Keeping local I/O data synchronized with the global I/O map.Detecting errors reported by SOEM.Managing errors reported by SOEM.
SOEM库使用有多种方式,这里在项目中直接使用SOEM源码。项目结构图下:
CMakeLists.txt 文件内容如下:
cmake_minimum_required(VERSION 3.28) project(soem_demo VERSION 1.0.1) add_subdirectory(SOEM-2.0.0) add_executable(soem_demo main.cpp) target_link_libraries(soem_demo soem)main.cpp 文件内容如下:
#include<iostream>#include"SOEM-2.0.0/include/soem/soem.h"usingnamespace std;#defineMAX_ADAPTERS_QTY(32)#defineMAX_ADAPTER_NAME(128)intbasic_examples(char*ifname){printf("SOEM baisc examples start: \n"); ecx_contextt ctx; uint8 IOmap[4096]={0};int expectedWKC;/************************* Init BUS *************************/if(!ecx_init(&ctx, ifname)){printf("Init adapter failed.\n");return-1;}if(ecx_config_init(&ctx)<=0){printf("Enum and init slaves failed.\n");return-1;}else{for(size_t i =0; i < ctx.slavecount; i++){printf("Slave %03d: ", i);printf("Name %s, ", ctx.slavelist[i].name);printf("Output %d bytes %d bits", ctx.slavelist[i].Obytes, ctx.slavelist[i].Obits);printf("Input %d bytes %d bits", ctx.slavelist[i].Ibytes, ctx.slavelist[i].Ibits);printf("Delay %d ns, Has DC %d\n", ctx.slavelist[i].pdelay, ctx.slavelist[i].hasdc);}}ecx_config_map_group(&ctx, IOmap,0); expectedWKC = ctx.grouplist[0].outputsWKC *2+ ctx.grouplist[0].inputsWKC;ecx_configdc(&ctx);ecx_statecheck(&ctx,0, EC_STATE_SAFE_OP, EC_TIMEOUTSTATE *4);ecx_send_processdata(&ctx);ecx_receive_processdata(&ctx, EC_TIMEOUTRET);// using slave 0, which will broadcast the request to all slaves on the network ctx.slavelist[0].state = EC_STATE_OPERATIONAL;ecx_writestate(&ctx,0);for(size_t i =0; i <10; i++){ecx_send_processdata(&ctx);ecx_receive_processdata(&ctx, EC_TIMEOUTRET);ecx_statecheck(&ctx,0, EC_STATE_OPERATIONAL, EC_TIMEOUTSTATE /10);if(ctx.slavelist[0].state == EC_STATE_OPERATIONAL)break;}if(ctx.slavelist[0].state != EC_STATE_OPERATIONAL){printf("Set operational state failed.\n");return-1;}/************************* Main loop *************************/for(size_t turn =0; turn <10000; turn++){ ec_timet start, end, diff;osal_usleep(5000);/********* Change output data if need *********/// change by slavefor(size_t i =0; i < ctx.slavecount; i++){for(size_t j =0; j < ctx.slavelist[i].Obytes; j++){ ctx.slavelist[i].outputs[j]= turn %256;}}// change by group// for (size_t i = 0; i < ctx.grouplist[0].Obytes; i++)// {// ctx.grouplist[0].outputs[i] = turn % 256;// } start =osal_current_time();ecx_send_processdata(&ctx);int wkc =ecx_receive_processdata(&ctx, EC_TIMEOUTRET); end =osal_current_time();osal_time_diff(&start,&end,&diff);printf("Iteration %05d: ", turn +1);printf("%08d usec, WKC %d",(int)(diff.tv_sec *1000000+ diff.tv_nsec /1000), wkc);printf(", O:");for(size_t n =0; n < ctx.grouplist[0].Obytes;++n){printf(" %02X", ctx.grouplist[0].outputs[n]);}printf(", I:");for(size_t n =0; n < ctx.grouplist[0].Ibytes;++n){printf(" %02X", ctx.grouplist[0].inputs[n]);}printf(", T: %lld",(longlong)ctx.DCtime);if(((turn +1)%1000))printf("\r");elseprintf("\n");if(wkc != expectedWKC)break;// error: stop or reinit slave}/************************* Stop BUS *************************/ ctx.slavelist[0].state = EC_STATE_INIT;ecx_writestate(&ctx,0);ecx_close(&ctx);printf("SOEM baisc examples end.\n");return0;}char*find_and_select_adapters(void){char adapter_name[MAX_ADAPTERS_QTY][MAX_ADAPTER_NAME]={0};int index =0; ec_adaptert *adapter =NULL; ec_adaptert *head =NULL;printf("\nAvailable adapters:\n"); head = adapter =ec_find_adapters();while(adapter !=NULL){printf("%02d - %s (%s)\n", index, adapter->name, adapter->desc);strncpy(adapter_name[index], adapter->name, MAX_ADAPTER_NAME); index++; adapter = adapter->next;}ec_free_adapters(head);printf("Please input the network adapter number: ");scanf("%d",&index);staticchar selected_name[MAX_ADAPTER_NAME];strncpy(selected_name, adapter_name[index], MAX_ADAPTER_NAME);return selected_name;}intmain(){char*ifname =find_and_select_adapters();basic_examples(ifname);return0;}使用情况如下:
测试过程中output数据是在代码中不断改变的,input数据是板子上接了开关的IO口,拨动开关时数据会显示变化。
资料和例程
本文所设计的一些EtherCAT_SOEM相关资料可以从下面下载:
https://pan.baidu.com/s/1lDRk10TK2AKJm_mdseNO9Q?pwd=39qh
https://download.ZEEKLOG.net/download/Naisu_kun/92573410
其中LAN9252和AX58100烧录的ESI(EtherCAT SubDevice Information)文件分别如下:
- AX58100_EVB_DIO_ESI_00010200_00000001_20220323.xml
- Microchip EVB-LAN9252-DIGIO-8IN-8OUT.xml
烧录EEPROM可以使用安装倍福的SSC(EtherCAT Slave Stack Code Tool)工具时自带的 EEPROM Programmer 工具进行烧录:
上面工具比TwinCAT好的一点是不挑网卡,USB网卡等的都能用。
总结
SOEM总体使用是比较简单的,对于EtherCAT主站来说除了基本的读写,可能更多的挑战会在于对于时间稳定性上的处理。
最近发现类似的CherryECAT库也不错:
https://github.com/cherry-embedded/CherryECAT
https://cherryecat.readthedocs.io/