【转】(笔记)CANopen协议【CANFestival】移植方法
一、背景
CAN组网就必须得要应用层协议,原因就在于
* 便于网络管理与控制
* 确认数据的收发
* 发送大于8个字节的数据块(CAN每帧数据传输大小为8字节)
* 为不同节点分配不同的报文标识符
* 定义帧报文的内容及含义(这在我看来是最主要的原因)
* 网络的监控,节点故障的诊断与标识
CAN上层协议有许多,用大家都公认的,便于产品的兼容,因此,CANopen成为备选项。
CANopen有个开源协议栈【CANFestival】,同时有一位大神已经做了移植并记录,在此就厚着脸皮转载过来以做备份。
转载地址:http://www.cnblogs.com/tdyizhen1314/p/4348725.html
二、正文:
前段时间学习了CanOpen协议,到网上下载的CanFestival3-10源码,移植到VC、QT、STM32等平台,由于网上的资源较少,走了不少弯路,移植好使用过程中才逐渐暴露出各种问题,比如OD字符串传输、心跳时间不准确等等,现在已经解决了遇到的所有问题,移植出来的工程能够完好支持CanOpen协议,花了点时间,整理出一个简单易用的移植方法说明,也写了一些比较实用的调试工具,本来还想整理SDO、PDO、EDS文件装载等相关知识的,可惜比较忙,等什么时候有空了再整理其他的吧!先把移植的贴上来,希望能帮到大家。
如果是第一次,整个移植过程还比较麻烦,需要有耐心,按照下面说的一步步来肯定可以的,移植成功一次后,再移植到其他平台就非常轻松了。
到网上下载CanFestival源码CanFestival-3-1041153c5fd2,解压出来,并将文件夹名字改为CanFestival-3-10,我们移植需要用到的源文件在CanFestival-3-10\src目录下,头文件在CanFestival-3-10\include目录下。
CanFestival-3-10\src下的文件如下图所示:
CanFestival-3-10\include下的文件如下图所示:
接下来开始移植: 步骤一: 在新建好的工程目录下新建文件夹CanFestival,再在CanFestival下新建文件夹driver、inc和src,再在inc文件夹下面新建 stm32文件夹(我这里主要以移植到stm32为例说明,如果是移植到VC或其他平台下,这里也可以命名为其他名字,如vc)。 步骤二: 将CanFestival-3-10\src目录下的dcf.c、emcy.c、lifegrd.c、lss.c、nmtMaster.c、nmtSlave.c、objacces.c、pdo.c、sdo.c、states.c、sync.c、timer.c共12个文件拷贝到CanFestival\src目录下; 将CanFestival-3-10\include目录下的所有.h文件共19个文件全部拷贝到CanFestival\inc目录下, 再把CanFestival-3-10\examples\AVR\Slave目录下的ObjDict.h文件拷贝过来,一共20个; 将CanFestival-3-10\include\AVR目录下的applicfg.h、canfestival.h、config.h、timerscfg.h共4个头文件拷贝到canfestival\inc\stm32目录下; 将CanFestival-3-10\examples\TestMasterSlave目录下的TestSlave.c、TestSlave.h、TestMaster.h、TestMaster.c拷贝到canfestival\driver目录下,并在该目录下新建stm32_canfestival.c文件。 步骤三: 将CanFestival\src目录下的所有.c文件添加到工程;将canfestival\driver目录下的stm32_canfestival.c文件添加到工程; 如果实现的是从设备,再将canfestival\driver目录下的TestSlave.c文件添加到工程,如果实现的是主设备,则将TestMaster.c文件添加到工程; 步骤四: 将文件目录canfestival\inc、canfestival\inc\stm32、canfestival\driver等路径添加到工程包含路径。 步骤五: 在stm32_canfestival.c中包含头文件#include "canfestival.h",并定义如下函数: void setTimer(TIMEVAL value) { } TIMEVAL getElapsedTime(void) { return 1; } unsigned char canSend(CAN_PORT notused, Message *m) { return 1; } 可以先定义一个空函数,等到编译都通过了之后,再往里面添加内容,这几个函数都是定义来供canfestival源码调用的,如果找不到这几个函数编译就会报错。 步骤六:通过以上几步,所有的文件都弄齐了,但是编译一定会出现报错,注释或删除掉config.h文件中的如下几行就能编译通过: #include <inttypes.h> #include <avr\io.h> #include <avr\interrupt.h> #include <avr/pgmspace.h> #include <avr\sleep.h> #include <avr\wdt.h> 如果还有其他报错,那有可能是因为不同源码版本、不同平台、不同人遇到的错误也会不相同,这里的过程只能做一定的参考,不一定完全相同,解决这些错误需要有一定的调试功底,需要根据编译出错提示来进行修改对应地方,一般都是有些函数没声明或者某个头文件没有包含或者包含了一些不必要的头文件而该文件不存在或者是一些变量类型不符合需定义之类的,如果能够摆平所有的编译出错,那么移植就算成功了,如果你被编译出错摆平了,那么游戏就结束,没得玩了。 步骤七: 解决了所有的编译错误后,接下来实现刚才定义的3个空函数: 函数void setTimer(TIMEVAL value)主要被源码用来定时的,时间到了就需要调用一下函数TimeDispatch(), 函数TIMEVAL getElapsedTime(void)主要被源码用来查询距离下一个定时触发还有多少时间, 函数unsigned char canSend(CAN_PORT notused, Message *m)主要被源码用来发一个CAN包的,需要调用驱动来将一个CAN包发出去。 我们在stm32_canfestival.c文件里定义几个变量如下: unsigned int TimeCNT=0;//时间计数 unsigned int NextTime=0;//下一次触发时间计数 unsigned int TIMER_MAX_COUNT=70000;//最大时间计数 static TIMEVAL last_time_set = TIMEVAL_MAX;//上一次的时间计数 setTimer和getElapsedTime函数实现如下: //Set the next alarm // void setTimer(TIMEVAL value) { NextTime=(TimeCNT+value)%TIMER_MAX_COUNT; } // Get the elapsed time since the last occured alarm // TIMEVAL getElapsedTime(void) { int ret=0; ret = TimeCNT> last_time_set ? TimeCNT - last_time_set : TimeCNT + TIMER_MAX_COUNT - last_time_set; last_time_set = TimeCNT; return ret; } 另外还要开一个1毫秒的定时器,每1毫秒调用一下下面这个函数。 void timerForCan(void) { TimeCNT++; if (TimeCNT>=TIMER_MAX_COUNT) { TimeCNT=0; } if (TimeCNT==NextTime) { TimeDispatch(); } } can发包函数canSend跟CAN驱动有关,CAN通道可以使用真实的CAN总线,也可以使用虚拟的CAN通道(如文件接口、网络通道等等)。 启动时初始化: 在初始化的文件里(比如main.c)添加以下几行代码 #include "TestSlave.h" unsigned char nodeID=0x21; extern CO_Data TestSlave_Data; 在调用函数(比如main函数)里调用以下代码初始化 setNodeId(&TestSlave_Data, nodeID); setState(&TestSlave_Data, Initialisation); // Init the state 其中T estSlave_Data在TestSlave.c中定义 然后开启调用TimerForCan()的1毫秒定时器,在接收CAN数据那里调用一下源码函数canDispatch(&TestSlave_Data, &m); canfestival源码就可以跑了,如果需要跟主设备联调,还要实现canSend函数,这个与平台的Can驱动相关。 Stm32平台下的驱动实现: 开启一个1毫秒定时器,可参考如下代码,调用一下函数TIM4_start();即可: /* TIM4 configure */ static void TIM4_Configuration(void) { /* 时钟及分频设置 */ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; /* Time Base configuration */ /* 72M / 72 = 1us */ // 这个就是预分频系数,当由于为0时表示不分频所以要减1 TIM_TimeBaseStructure.TIM_Prescaler =72-1; //72000 - 1; //计数模式:向上计数 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //这个就是自动装载的计数值,由于计数是从0开始的 //TIM_TimeBaseStructure.TIM_Period =0xffff;// TIM_TimeBaseStructure.TIM_Period =0x03e8;//1ms TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //重新计数的起始值 TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); // TIM IT enable TIM_ITConfig(TIM4, TIM_IT_CC1, ENABLE); //打开溢出中断 // TIM enable counter TIM_Cmd(TIM4, ENABLE);//计数器使能,开始工作 } static void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3); /* Enable the TIM4 global Interrupt */ NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } static void RCC_Configuration(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); /* TIM4 clock enable */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); /* clock enable */ RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA ,ENABLE); } void TIM4_start(void) { RCC_Configuration(); /* configure TIM4 for remote and encoder */ NVIC_Configuration(); TIM4_Configuration(); } void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_CC1) != RESET) { //printf("enter tim4"); TIM_ClearITPendingBit(TIM4, TIM_IT_CC1); } TimerForCan(); } canSend函数实现如下: unsigned char canSend(CAN_PORT notused, Message *m) { uint32_t i; CanTxMsg *ptx_msg=&TxMessage; ptx_msg->StdId = m->cob_id; if(m->rtr) { ptx_msg->RTR = CAN_RTR_REMOTE; } else { ptx_msg->RTR = CAN_RTR_DATA; } ptx_msg->IDE = CAN_ID_STD; ptx_msg->DLC = m->len; for(i = 0; i < m->len; i++) { ptx_msg->Data = m->data; } if( CAN_Transmit( CAN1, ptx_msg )==CAN_NO_MB) { return 0xff; } else { return 0x00; } } 其中CAN_Transmit为stm32提供的库函数,在stm32f10x_can.c中定义。 在使用stm32之前需要初始化一下CAN void CAN_Config(void) { /* CAN register init */ CAN_DeInit(CAN1); CAN_DeInit(CAN2); CAN_StructInit(&CAN_InitStructure); /* CAN1 cell init */ CAN_InitStructure.CAN_TTCM = DISABLE; CAN_InitStructure.CAN_ABOM = DISABLE; CAN_InitStructure.CAN_AWUM = DISABLE; CAN_InitStructure.CAN_NART = DISABLE; CAN_InitStructure.CAN_RFLM = DISABLE; CAN_InitStructure.CAN_TXFP = DISABLE; CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; //Fpclk=72M/2/CAN_Prescaler //BitRate=Fpclk/((CAN_SJW+1)*((CAN_BS1+1)+(CAN_BS2+1)+1)); //1M /*CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_3tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_5tq; CAN_InitStructure.CAN_Prescaler = 4;*/ //125K CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_8tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_7tq; CAN_InitStructure.CAN_Prescaler = 18; CAN_Init(CAN1, &CAN_InitStructure); CAN_Init(CAN2, &CAN_InitStructure); /* CAN1 filter init */ CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure); /* CAN2 filter init */ CAN_FilterInitStructure.CAN_FilterNumber = 14; CAN_FilterInit(&CAN_FilterInitStructure); } Can 接收中断实现: void CAN1_RX0_IRQHandler(void) { CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); //接收处理 m.cob_id=RxMessage.StdId; if(RxMessage.RTR == CAN_RTR_REMOTE) m.rtr=1; else if(RxMessage.RTR == CAN_RTR_DATA) m.rtr=0; m.len=RxMessage.DLC; for(i = 0; i < RxMessage.DLC; i++) m.data=RxMessage.Data; canDispatch(&TestSlave_Data, &m); } 移植到VC或其他C++平台说明: 由于源码全是c文件,如果要移植到C++平台,需要将以上所有涉及的.c文件改成.cpp文件, 如果是移植到MFC,则还要在cpp文件中包含头文件 #include "stdafx.h" 移植到VC等一些比较牛的编译器下面时,由于检查得更严格,所以编译还会出现一些指针不匹配的问题,如:pdo.cpp文件的145、216、332行就会报错, 只要强制转换一下指针就能通过,如将 pwCobId = d->objdict[offset].pSubindex[1].pObject; 改成 pwCobId = (unsigned long *)d->objdict[offset].pSubindex[1].pObject; 即可通过。 还有407行由于代码跨平台出现些乱码错误,将 MSG_ERR (0x1948, " Couldn't build TPDO n�", numPdo); 改成 MSG_ERR (0x1948, " Couldn't build TPDO \n", numPdo); 即可。 这时编译还不能通过,需修改除了dcf.h和canfestival.h以外的所有头文件,在开头加上 #ifdef __cplusplus extern "C" { #endif 头文件结尾加上 #ifdef __cplusplus }; #endif 例如:data.c改成data.cpp后,data.h中添加位置如下: #ifndef __data_h__ #define __data_h__ #ifdef __cplusplus extern "C" { #endif //省略掉中间内容 #ifdef __cplusplus }; #endif #endif /* __data_h__ */ 另外,源码文件文件还有一个错误,这个错误在keil里表现不出来,在VC里就会导致出错,花了些时间才找到这些错误。如下: 文件dcf.cpp第40行,将 extern UNS8 _writeNetworkDict (CO_Data* d, UNS8 nodeId, UNS16 index, UNS8 subIndex, UNS8 count, UNS8 dataType, void *data, SDOCallback_t Callback, UNS8 endianize); 改成 extern UNS8 _writeNetworkDict (CO_Data* d, UNS8 nodeId, UNS16 index, UNS8 subIndex, UNS32 count, UNS8 dataType, void *data, SDOCallback_t Callback, UNS8 endianize); 移植到VC或QT时,由于电脑没有CAN接口,这时要么用USB-CAN,要么得使用虚拟的CAN总线通道,Linux下面有虚拟的CAN总线, windows下没有,只能通过走文件接口或网口来虚拟CAN总线。
记录地点:深圳WZ
记录时间:2016年7月29日