EtherNET/IP开源从站OpENer单次通讯功能开发记录
本文介绍了基于EtherNet/IP协议实现上下位机单次通讯的开发过程。通过分析协议中的Class、Instance和Attribute关系,设计了一种通过读写单个属性实现通讯的方案。具体实施包括:在Assembly Class中注册Get/Set服务、创建自定义Instance并添加Attribute,并解决了内存分配和参数限制等调试问题。验证表明该方案能成功实现数据的读写操作,为上下位机通讯提
目录
背景
因很多场合需要上下位机单次通讯,因此,基于opener对上述功能进行了开发调试。
思路
首先想到的是采用Tag读写功能于是翻开opener的cip服务对象表查看,发现仅有0x4d,没有0x4c。因此需要自己插入0x4c的功能及实现。
typedef enum {
/* Start CIP common services */
kGetAttributeAll = 0x01,
kSetAttributeAll = 0x02,
kGetAttributeList = 0x03,
kSetAttributeList = 0x04,
kReset = 0x05,
kStart = 0x06,
kStop = 0x07,
kCreate = 0x08,
kDelete = 0x09,
kMultipleServicePacket = 0x0A,
kApplyAttributes = 0x0D,
kGetAttributeSingle = 0x0E,
kSetAttributeSingle = 0x10,
kFindNextObjectInstance = 0x11,
kRestore = 0x15,
kSave = 0x16,
kNoOperation = 0x17,
kGetMember = 0x18,
kSetMember = 0x19,
kInsertMember = 0x1A,
kRemoveMember = 0x1B,
kGroupSync = 0x1C,
kGetConnectionPointMemberList = 0x1D,
/* End CIP common services */
/* Start CIP object-specific services */
kEthLinkGetAndClear = 0x4C, /**< Ethernet Link object's Get_And_Clear service */
kForwardOpen = 0x54,
kLargeForwardOpen = 0x5B,
kForwardClose = 0x4E,
kUnconnectedSend = 0x52,
kGetConnectionOwner = 0x5A,
kGetConnectionData = 0x56,
kSearchConnectionData = 0x57
/* End CIP object-specific services */
} CIPServiceCode;
但是在这个表格中看到了熟悉的两个功能,即读写单个属性:
kGetAttributeSingle = 0x0E,
kSetAttributeSingle = 0x10,
那也就是说,是否可以通过上位机下发读写单个内部属性的请求来实现上述功能呢?
这样做上位机不用再定义Tag的字符串了,直接按协议双方规定好哪个属性是干什么的就可以,并且扩展跟实现都比较方便。
这里科普下在eip协议中,class,instance,attribute的关系。
官网文档中进行了解释,比如 class = human,instance = Mary, attribute = age。
也就是类,类的实例,实例内的属性的概念。
eip也规定了在每个Class中,那些instance号的规范是什么,这里贴个class id =0x04的例子,其instance的id号的划分规则。
经过上述的分析与背景知识储备,我们可以这样定义下:
对于单次通讯需求A,可以定义下列方案用于通讯
Class id =0x65 ,instance id =0x70 , attribute =0x65
对于单次通讯需求B,可以定义下列方案用于通讯
Class id =0x65 ,instance id =0x70 , attribute =0x66
。。。。。。
Scanner发送GetAttributeSingle(0x65,0x70,0x65)时,Adapter把对应实例的数据塞到报文中回复Scanner。
Scanner发送etAttributeSingle(0x65,0x70,0x65)时,Adapter把对应实例的数据写到内部变量中。
实施
首先,需要在opener_init(netif);中,确认要使用的Class注册了SetAttributeSingleGetAttributeSingle的Service没有,调用链如下:
Main(void){
…
opener_init(netif);
…
}
->
void opener_init(struct netif *netif) {
….
EipStatus eip_status = CipStackInit(unique_connection_id);
….
}
->
EipStatus CipStackInit(const EipUint16 unique_connection_id) {
…..
eip_status = CipAssemblyInitialize();
….
}
->
EipStatus CipAssemblyInitialize(void) {
return ( NULL != CreateAssemblyClass() ) ? kEipStatusOk : kEipStatusError;
}
->
CipClass *CreateAssemblyClass(void) {
/* create the CIP Assembly object with zero instances */
CipClass *assembly_class = CreateCipClass(kCipAssemblyClassCode, 0, /* # class attributes*/
7, /* # highest class attribute number*/
1, /* # class services*/
2, /* # instance attributes*/
4, /* # highest instance attribute number*/
2, /* # instance services*/
0, /* # instances*/
"assembly", /* name */
2, /* Revision, according to the CIP spec currently this has to be 2 */
NULL); /* # function pointer for initialization*/
if(NULL != assembly_class) {
InsertService(assembly_class,
kGetAttributeSingle,
&GetAttributeSingle,
"GetAttributeSingle");
InsertService(assembly_class,
kSetAttributeSingle,
&SetAttributeSingle,
"SetAttributeSingle");
InsertGetSetCallback(assembly_class, AssemblyPreGetCallback, kPreGetFunc);
InsertGetSetCallback(assembly_class, AssemblyPostSetCallback, kPostSetFunc);
}
return assembly_class;
}
从上述代码中可以看到,class id =kCipAssemblyClassCode 初始化时注册了GetAttributeSingle SetAttributeSingle的服务。
然后,在opener线程启动时,需要插入 用于单次通讯的instance,调用链如下:
#define DEMO_ASSEMBLY_EXPLICT (0x70)
Main(void){
…
opener_init(netif);
…
}
->
void opener_init(struct netif *netif) {
….
EipStatus eip_status = CipStackInit(unique_connection_id);
….
}
->
EipStatus CipStackInit(const EipUint16 unique_connection_id) {
…..
eip_status = ApplicationInitialization();
….
}
->
EipStatus ApplicationInitialization(void) {
CreateAssemblyObject(DEMO_ASSEMBLY_EXPLICT, g_assembly_data_explicit, sizeof(g_assembly_data_explicit));
}
CipInstance *CreateAssemblyObject(const CipInstanceNum instance_id,
EipByte *const data,
const EipUint16 data_length) {
CipClass *assembly_class = GetCipClass(kCipAssemblyClassCode);
if(NULL == assembly_class) {
assembly_class = CreateAssemblyClass();
}
if(NULL == assembly_class) {
return NULL;
}
CipInstance *const instance = AddCipInstance(assembly_class, instance_id); /* add instances (always succeeds (or asserts))*/
CipByteArray *const assembly_byte_array = (CipByteArray *) CipCalloc(1,
sizeof(
CipByteArray) );
if(assembly_byte_array == NULL) {
return NULL; /*TODO remove assembly instance in case of error*/
}
assembly_byte_array->length = data_length;
assembly_byte_array->data = data;
InsertAttribute(instance,
3,
kCipByteArray,
EncodeCipByteArray,
DecodeCipAssemblyAttribute3,
assembly_byte_array,
kSetAndGetAble | kPreGetFunc | kPostSetFunc);
/* Attribute 4 Number of bytes in Attribute 3 */
InsertAttribute(instance, 4, kCipUint, EncodeCipUint,
NULL, &(assembly_byte_array->length), kGetableSingle);
return instance;
}
上述代码表示,在assembly class中,插入了DEMO_ASSEMBLY_EXPLICT的instance,并且创建了两个attribute,分别是attribute 3 ,attribute 4。
后续,只需在DEMO_ASSEMBLY_EXPLICT 内插入单次通讯的attribute即可,示例如下:
#define ATTRIBUTE_NUM_OF_A 0x65
uint8_t A;
void init_assmble_ex_attribute(void){
CipInstance *instance = GetCipInstance(GetCipClass(kCipAssemblyClassCode), DEMO_ASSEMBLY_EXPLICT);
CipAttributeStruct* attr;
InsertAttribute(instance,ATTRIBUTE_NUM_OF_A,kCipUsint,EncodeCipByte,DecodeCipByte,(void *)&A,kSetAndGetAble);
attr = GetCipAttribute(instance, ATTRIBUTE_NUM_OF_A;
if(attr != NULL) {printf("Found attr %x, type=%x, encode=%p,decode=%p,data=%p\n",attr->attribute_number, attr->type, attr->encode,attr->decode,attr->data);}
}
上述代码表示,将A插入到instance = DEMO_ASSEMBLY_EXPLICT内,A的attribute id =ATTRIBUTE_NUM_OF_A
如果插入成功,则打印相关信息。
后续Scanner只需调用GetAttributeSingle SetAttributeSingle 便可对attribute id =ATTRIBUTE_NUM_OF_A的变量进行读写了。
验证
如,Scanner读取Class =0x04 instance = 0x9a attribute = 0x11的1byte数据
MCU中初始化该变量为0xaa,Scanner的log显示读取的数据为0xaa,验证通过。
Scanner写Class =0x04 instance = 0x9a attribute = 0x12的1byte数据
Scanner的log显示成功,观察MCU内部数据已被成功修改。
[DEBUG] Opened TCP socket fd=3
Local Port is20675
[DEBUG] Connecting to 192.168.2.2:44818
[INFO] Registered session 1
[INFO] Send request: service=0xe epath=[classId=4 objectId=154 attributeId=17]
[INFO] get_value is 0xaa
[INFO] Send request: service=0x10 epath=[classId=4 objectId=154 attributeId=18]
[INFO] Writing is successful
[INFO] Unregistered session 1
[DEBUG] Close TCP socket fd=3
对应的wireshark报文如下:
调试问题记录
内存问题
插入Attribute时会失败,其中一个原因就是ram不足。
因为MCU ram有限,插入instance 和 attribute会有一些内存需求,当内存申请失败时会引起运行不稳定。
另外,opener在内存操作的实现时,采用的是c的calloc,free方法。这里申请的是系统堆内的内存,是否成功跟系统堆分配的大小有关,这里改成了FreeRTOS的实现方法,申请的是RTOS内的堆空间。
//void*
//CipCalloc(size_t number_of_elements,
// size_t size_of_element) {
// return calloc(number_of_elements, size_of_element);
//}
//void CipFree(void *data) {
// free(data);
//}
#include "FreeRTOS.h"
#include "task.h"
void* CipCalloc(size_t number_of_elements, size_t size_of_element) {
size_t total_size = number_of_elements * size_of_element;
void* ptr = pvPortMalloc(total_size);
if (ptr != NULL) {
memset(ptr, 0, total_size);
}
return ptr;
}
void CipFree(void *data) {
if (data != NULL) {
vPortFree(data);
}
}
-
Eip打印拒绝新建instance等问题
因为在Assembly初始化时,传参了对该assembly内部多少个instance,attribute等的限制参数,因此要根据自身的需求修改assembly的初始化参数以下是一个示例:
CipClass *CreateAssemblyClass(void) {
/* create the CIP Assembly object with zero instances */
CipClass *assembly_class = CreateCipClass(kCipAssemblyClassCode, 0, /* # class attributes*/
7, /* # highest class attribute number*/
1, /* # class services*/
60, /* # instance attributes*/
0xFF, /* # highest instance attribute number*/
2, /* # instance services*/
0, /* # instances*/
"assembly", /* name */
2, /* Revision, according to the CIP spec currently this has to be 2 */
NULL); /* # function pointer for initialization*/
if(NULL != assembly_class) {
InsertService(assembly_class,
kGetAttributeSingle,
&GetAttributeSingle,
"GetAttributeSingle");
InsertService(assembly_class,
kSetAttributeSingle,
&SetAttributeSingle,
"SetAttributeSingle");
InsertGetSetCallback(assembly_class, AssemblyPreGetCallback, kPreGetFunc);
InsertGetSetCallback(assembly_class, AssemblyPostSetCallback, kPostSetFunc);
}
return assembly_class;
}
其中用到的CreateCipClass函数原型如下:
/** @ingroup CIP_API
* @brief Allocate memory for new CIP Class and attributes
*
* The new CIP class will be registered at the stack to be able
* for receiving explicit messages.
*
* @param class_code class code of the new class
* @param number_of_class_attributes number of class attributes
* @param highest_class_attribute_number Highest attribute number from the set of implemented class attributes
* @param number_of_class_services number of class services
* @param number_of_instance_attributes Number of implemented instance attributes
* @param highest_instance_attribute_number Highest attribute number from the set of implemented instance attributes
* @param number_of_instance_services number of instance services
* @param number_of_instances number of initial instances to create
* @param name class name (for debugging class structure)
* @param revision class revision
* @param initializer For non-standard implementation of
* class attributes, function realizes specific implementation
* @return pointer to new class object
* 0 on error
*/
CipClass *CreateCipClass(const CipUdint class_code,
const int number_of_class_attributes,
const EipUint32 highest_class_attribute_number,
const int number_of_class_services,
const int number_of_instance_attributes,
const EipUint32 highest_instance_attribute_number,
const int number_of_instance_services,
const CipInstanceNum number_of_instances,
const char *const name,
const EipUint16 revision,
InitializeCipClass initializer);
更多知识分享:
b站,知乎同名:沧海一条狗
咸鱼ID:tb764914262
更多推荐
所有评论(0)