(笔记)CanOpen协议【CanFestival】移植⽅法⽀持VC、QT、
STM32
转⾃bbs.21ic/icview-878522-1-1.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;
stm32怎么使用printf
//重新计数的起始值
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);
//接收处理
if(RxMessage.RTR == CAN_RTR_REMOTE)
<=1;
else if(RxMessage.RTR == CAN_RTR_DATA)
<=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总线。