提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
对于刚接触canopen协议不久的我来说,这个简单的主站测试实现起来还是有一定困难,所以摸索当中记录下canopen主站一些功能的测试过程,比如NMT网络管理、SDO读写、PDO接收发送等功能,很多原理性的内容我不会多写,大佬们都写了很多,推荐周立功的《canopen轻松入门》,我主要结合自己的例程讲下简单的功能实现;通过在VS上新建一个空的应用台项目作为测试平台,canfestival源码移植到VS上已经做过说明,测试过没有问题,有需要可以去看看。
canfestival源码移植到VS上
一、对象字典
对象字典描述了应用对象和 CANopen 报文之间的关系,是canopen协议最核心的部分;主站和从站通信时的SDO、PDO通信参数,映射参数、心跳报文等都是在这里配置;同时当和从站通信时,需要知道从站的对象字典来配置主站的相关参数,否则无法通信,
1.对象字典环境安装:参考下面大佬的文章
对象字典环境安装链接
2.创建一个空的主站节点后,可以看到对象字典索引区域,每个索引又包含子索引区域,当我们使用SDO或者PDO时,本质上就是根据这些索引在访问从站对象字典的每个值;点击文件——保存,命名最好和节点名称一样,然后选择文件——建立词典,这时会生成三个文件TestMaster.h、TestMaster.c、TestMaster.od,将.c文件改为.cpp文件,然后把TestMaster.h、TestMaster.cpp添加到VS的项目中;
二、NMT网络通信
要使主站可以发送网络管理报文控制从站状态至少需要三部分:
1.定时器驱动:定时器驱动使用的是源码里面的timer_win32.cpp文件中的TimerInit()函数和 StartTimerLoop()函数;
2.can接口驱动:can驱动使用的是USB-CAN分析仪的接口函数;
3.主站节点初始化:setNodeId()函数设置主站id,setState()函数设置主站状态,此时主站便可以发送网络报文了,masterSendNMTstateChange(&TestMaster_Data, nodeId, NMT_Start_Node)便是通过网络报文控制id为nodeid的从站进入启动状态(操作状态)。
通过另外一个can分析仪监控网络数据,发现以上代码在can总线如下所示:COB-ID:700和701分别代表主站id为0和从站id为1的NMT节点上线报文,000为NMT网络管理报文,含义可以对照上面的节点状态切换命令理解;
如果从站设置了心跳报文,主站也可以监控到这个报文,索引1017是心跳时间,主站会按照索引1016设置的值检测来检查心跳报文
三、接收数据线程
在VS上没有can中断服务函数,接收数据只能通过设计一个接收线程来实现,我是直接在一个线程回调函数中的while循环中不断查询can-usb接收缓存区是否有数据;GetReceiceNum()是can分析仪的接口函数,就是判断can通道是否有数据,
DWORD WINAPI receiveCanData(LPVOID lpParam) {//接收线程回调函数
while (1) {
if (GetReceiveNum(nDeviceType, nDeviceInd, nCANInd) > 0) {
receiveCan();
}
}
return 0;
}
void receiveCan(void) {
UNS8 i = 0;
UNS8 rxbuf[8] = { 0 };
Message RxMSG;
int rec = Receive(nDeviceType, nDeviceInd, nCANInd, &can_rx_header, 8, 100);
if (rec > 0) {
for (i = 0; i < can_rx_header.DataLen; i++)
rxbuf[i] = can_rx_header.Data[i];
if (can_rx_header.RemoteFlag == 1) {
RxMSG.rtr = 1;
}
else {
RxMSG.rtr = 0;
}
RxMSG.cob_id = can_rx_header.ID;
RxMSG.len = can_rx_header.DataLen;
for (i = 0; i < 8; i++) {
RxMSG.data[i] = rxbuf[i];
}
EnterMutex();//上锁
canDispatch(&TestMaster_Data, &(RxMSG));//源码里面解包函数,需要阻塞接收,根据不同的COB-ID进行分类处理
LeaveMutex();//解锁
if (can_rx_header.ID == 0x180 + nodeId)
{
printf("cMotorStatus = %x, cActualSpeed = %x, cActualPosition = %x, cActualCurrent = %x\r\n", cMotorStatus, cActualSpeed, cActualPosition, cActualCurrent);
}
if (can_rx_header.ID == 0x280 + nodeId)
{
printf("cActualMotorTemp = %x, cActualBusVoltage = %x, cActualMosTemp = %x\r\n", cActualMotorTemp, cActualBusVoltage, cActualMosTemp);
}
if (can_rx_header.ID == 0x580 + nodeId)
{
printf("SDO返回数据成功\r\n");
}
}
else {
printf("读取数据失败\r\n");
}
}
void CreateReceiveTask(CAN_HANDLE fd0, TASK_HANDLE* Thread, void* ReceiveLoopPtr)
{
unsigned long thread_id = 0;
//线程结构体地址 用来继承
//线程堆栈大小
//线程起始地址 线程 函数名
//线程函数的参数
//创建方式
//线程id
*Thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ReceiveLoopPtr, fd0, 0, &thread_id);
}
主函数中调用:
CreateReceiveTask(NULL, &hThread1, receiveCanData);//创建接收任务线程
四、主站SDO通信
1.主站SDO读(上传服务)
在主站对象字典添加客户端SDO参数,在从站添加服务端SDO参数
通过SDO读从站对象字典主要用了源码的两个接口函数:
(1)发送读从站节点对象字典的指令
(2)成功发送并收到响应后,接收函数中会把接收到的报文输送给canDispatch()进行解析,然后调用getReadResultNetworkDict()函数,可以把值读取出来
UNS8 getReadResultNetworkDict (CO_Data* d, UNS8 nodeId, void* data, UNS32 *size, UNS32 * abortCode);
代码如下:
//sdo读canopen从站数据
UNS8 ReadSDO(UNS8 nodeId, UNS16 index, UNS8 subIndex, UNS8 dataType, void* data, UNS32* size)
{
UNS32 abortCode = 0;
UNS8 res = SDO_UPLOAD_IN_PROGRESS;
// Read SDO
UNS8 err = readNetworkDict(&TestMaster_Data, nodeId, index, subIndex, dataType, 0);
if (err)
return 0xFF;
for (;;)
{
res = getReadResultNetworkDict(&TestMaster_Data, nodeId, data, size, &abortCode);
printf("sendsdo res = %x\n", res);
if (res != SDO_UPLOAD_IN_PROGRESS)
break;
sleep_proc(1);
continue;
}
closeSDOtransfer(&TestMaster_Data, nodeId, SDO_CLIENT);
if (res == SDO_FINISHED)
return 0;
return 0xFF;
}
下面为主函数调用代码,这个是参考win32例程里面的,可以修改主站SDO对象字典的值,针对不同的从站比较方便,避免每次在对象字典里面修改;当然不加这些代码也是可以的,直接调用上面的readSDO(),只要在配置客户端SDO参数时,直接写上对应的COB-ID,比如我从站id是1,子索引1,2,3就写0x601和0x581和0x01即可。
UNS32 COB_ID_Client_to_Server_Transmit_SDO = 0x600 + nodeId;//客户端(主站)发送的COB-ID
UNS32 COB_ID_Server_to_Client_Receive_SDO = 0x580 + nodeId;//服务器端(从站)返回的COB-ID
UNS8 Node_ID_of_the_SDO_Server = nodeId;//从站ID
UNS32 ExpectedSize = sizeof(UNS32);
UNS32 ExpectedSizeNodeId = sizeof(UNS8);
写SDO 索引1280 子索引1,2,3的参数
if (OD_SUCCESSFUL == writeLocalDict(&TestMaster_Data, 0x1280, 1, &COB_ID_Client_to_Server_Transmit_SDO, &ExpectedSize, RW)
&& OD_SUCCESSFUL == writeLocalDict(&TestMaster_Data, 0x1280, 2, &COB_ID_Server_to_Client_Receive_SDO, &ExpectedSize, RW)
&& OD_SUCCESSFUL == writeLocalDict(&TestMaster_Data, 0x1280, 3, &Node_ID_of_the_SDO_Server, &ExpectedSizeNodeId, RW))
{
UNS32 size;
UNS8 res;
printf("\nnode_id: %d (%xh) info\n", (int)nodeId, (int)nodeId);
printf("********************************************\n");
size = sizeof(cNode_ID);
res = ReadSDO(nodeId, 0x2003, 0x00, uint16, &cNode_ID, &size);
printf("res = %x,cNode_ID: %x\n", res, cNode_ID);
size = sizeof(cActualMosTemp);
res = ReadSDO(nodeId, 0x2028, 0x00, uint16, &cActualMosTemp, &size);
printf("res = %x,cActualMosTemp: %x\n", res, cActualMosTemp);
/* UNS32 abortCode = 0;
UNS8 sendsdodata[4] = { 0 };
sendsdodata[0] = 0x33;
sendsdodata[1] = 0x22;
sendsdodata[2] = 0x00;
sendsdodata[3] = 0x00;
sleep_proc(10);
WriteSDO(nodeId, 0x2001, 0x00, 2, uint16, &sendsdodata, 0);*/
printf("********************************************\n");
}
else
{
printf("ERROR: Object dictionary access failed\n");
}
结果如下:可以看到我们读从站id为1,索引为2003和2028的值的指令为601,而服务端(从站)也会返回一个581的数据帧,每个字节含义如图,快速SDO最多只能读写四个数据。注意:读写从站的对象字典的自定义的变量一定要根据从站在主站对象字典也要添加对应参数,如图。
2.主站SDO写(下载服务)
对从站对象字典进行写数据和读类似,调用接口函数writeNetworkDict()向从站对象字典对应索引出写数据,通过getWriteResultNetworkDict()读取是否写成功。
UNS8 WriteSDO(UNS8 nodeId, UNS16 index, UNS8 subIndex, UNS32 count, UNS8 dataType, void* data, UNS8 useBlockMode)
{
UNS32 abortCode = 0;
UNS8 res = SDO_DOWNLOAD_IN_PROGRESS;
// Write SDO
UNS8 err = writeNetworkDict(&TestMaster_Data, nodeId, index, subIndex, count, dataType, data, useBlockMode);
if (err)
return 0xFF;
for (;;)
{
res = getWriteResultNetworkDict(&TestMaster_Data, nodeId, &abortCode);
printf("write res = %x\n", res);
if (res != SDO_DOWNLOAD_IN_PROGRESS)
break;
sleep_proc(1);
continue;
}
closeSDOtransfer(&TestMaster_Data, nodeId, SDO_CLIENT);
if (res == SDO_FINISHED)
return 0;
return 0xFF;
}
主函数中调用:
UNS32 abortCode = 0;
UNS8 sendsdodata[4] = { 0 };
sendsdodata[0] = 0x33;
sendsdodata[1] = 0x22;
sendsdodata[2] = 0x00;
sendsdodata[3] = 0x00;
sleep_proc(10);
WriteSDO(nodeId, 0x2001, 0x00, 2, uint16, &sendsdodata, 0);
结果如下:向从站对象字典索引为2001处写两个字节的数据0x2233,从站返回写成功应答0x60。
五、主站PDO通信
1.PDO接收从站数据
这个主要是根据从站配置,我的从站设备会定时发送两个PDO数据包,所以主站配置两个接收PDO,并根据从站的映射数据来配置主站的映射数据;
如图:RPDO1的索引为0x1400,子索引0x01是发送PDO数据的从站COB-ID:0x180+id,不同的从站的id不同;总线上配置了接收PDO的节点,将根据COB-ID来接收对应节点数据,其他子索引可以默认,下图我是直接按照从站来配置的;
接收PDO映射参数必须和从设备的映射参数一一对应,即8个字节的PDO数据中从站是映射了几个变量,占几个字节,主站必须相同;配置完成后只要从站发送数据到总线,接收函数解析了对应的COB-ID后会把数据传到对应的PDO映射变量上;
结果如下:
2.PDO发送控制指令
我需要主站实时发送几个控制数据到从站用来控制从设备,使用SDO一次只能发送四个字节,再多就需要分段发送,不稳定,所以想要使用PDO发送;查看PDO发送类型,发现传输类型FF或FE,可以通过事件触发PDO传输(我从设备发送PDO采用的定时发送也是一种事件触发),即通过改变PDO映射参数的状态从而触发PDO传输,例如,我想控制电机使能,只需要将电机使能的变量映射到PDO发送上,然后在调用sendPDOEvent(),这时我一旦在上位机上改变了电机使能这个变量的值,就是自动发送一个PDO到总线上,只要对应的从设备配置一个接收PDO,并和主站一样映射电机使能参数即可。
我映射了四个变量一共8个字节,只要这四个变量有任何一个发送值改变,就会触发PDO发送;
增加了一个线程一直调用sendPDOevent()函数来检测值是否发生变化(也可以在值改变前调用),
DWORD WINAPI PDOEvent_Change_Of_State(LPVOID lpParam) {//监控PDO映射参数状态是否发生改变,若改变,发送一个PDO
while (1) {
sendPDOevent(&TestMaster_Data);
}
return 0;
}
在主函数中创建PDO触发线程,并将发送线程的优先级设为最高
CreatePDOEventTask(&hThread2, PDOEvent_Change_Of_State);//创建PDO传输事件触发线程
SetThreadPriority(&hThread2, THREAD_PRIORITY_HIGHEST);
主函数while循环中改变变量cMotorOperation和cReferenceSpeed的值;
cMotorOperation = 0x02;
printf("cMotorOperation: %x\n", cMotorOperation);
sleep_proc(1000);
cReferenceSpeed = 0xc489;
printf("cReferenceSpeed: %x\n", cReferenceSpeed);
cMotorOperation = 0x01;
printf("cMotorOperation: %x\n", cMotorOperation);
sleep_proc(1000);
cReferenceSpeed = 0xc089;
printf("cReferenceSpeed: %x\n", cReferenceSpeed);
结果如下:两个变量的值改变时,主站循环发送PDO
从站也接收到这个值
以上功能是我主站需要的功能,所以只写了这么多,我也是刚接触canopen,导师需要,写的不是很严谨,搞这个也遇到不少坑,主要是基于PC端的主站这方面的资料太少了,大多数都是基于MCU开发的,记录一下我自己的成果,希望能帮到后面的朋友。