【转】(笔记)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日

时间: 2024-10-25 18:40:42

【转】(笔记)CanOpen协议【CanFestival】移植方法的相关文章

(笔记)CanOpen协议【CanFestival】移植方法 支持VC、QT、STM32

转自http://bbs.21ic.com/icview-878522-1-1.html 前段时间学习了CanOpen协议,到网上下载的CanFestival3-10源码,移植到VC.QT.STM32等平台,由于网上的资源较少,走了不少弯路,移植好使用过程中才逐渐暴露出各种问题,比如OD字符串传输.心跳时间不准确等等,现在已经解决了遇到的所有问题,移植出来的工程能够完好支持CanOpen协议,花了点时间,整理出一个简单易用的移植方法说明,也写了一些比较实用的调试工具,本来还想整理SDO.PDO.

AM335x(TQ335x)学习笔记——WM8960声卡驱动移植

经过一段时间的调试,终于调好了TQ335x的声卡驱动.TQ335x采用的Codec是WM8960,本文来总结下WM8960驱动在AM335x平台上的移植方法.Linux声卡驱动架构有OSS和ALSA两种架构,目前最常用的架构是ALSA,本文也使用ALSA架构对WM8960驱动进行移植. ASoC是对ALSA驱动架构的进一步封装.ASoC将ALSA驱动中的各模块抽象为三部分:Platform.Codec和Machine.Platform主要是平台硬件驱动,包括SoC的IIS模块.DMA等,在本文中

CANopen协议介绍

1.CANopen协议简介 从OSI 网络模型的角度来看,CAN总线只定义了OSI网络模型的第一层(物理层) 和第二层(数据链路层),而在实际设计中,这两层完全由硬件实现,设计人员无需再为此开发相关软件或固件. 同时,CAN只定义物理层和数据链路层,没有规定应用层,本身并不完整,因此需要一个高层协议来定义CAN报文中的11/29位标识符和8字节数据的使用.而且,基于CAN总线的工业自动化应用中,越来越需要一个开放的.标准化的高层协议:这个协议支持各种CAN厂商设备的互用性.互换性,能够实现在CA

iOS复习笔记11:协议和代理

一 功能 可以在协议中声明方法(不能声明变量), 某个类只要遵守这个协议,就相当于拥有协议中所有的方法声明. 父类准守协议,子类也拥有协议中的方法. 协议也可以准守另一个协议. 基协议NSObject(同时也是基类),NSObject基类也准守基协议. 基协议中包含了常用的内存管理方法:release,retain方法等. 二 定义 1 协议定义 @protocal 协议名 <NSObject> // 声明方法 @end 2 遵守协议 2.1 类 @interface 类名 : 父类名 <

androidclient和站点数据交互的实现(基于Http协议获取数据方法)

androidclient一般不直接訪问站点数据库,而是像浏览器一样发送get或者post请求.然后站点返回client能理解的数据格式,client解析这些数据.显示在界面上.经常使用的数据格式是xml和json. 能够理解client事实上是一个你自定义标记语言的浏览器,一般浏览器能解析的是html+css的数据,而androidclient能解析的是xml和json(或者都不是而是你自定义的火星格式),服务端为了能满足client输出这样的数据格式的需求,不得不专门针对client开发不同

UITextFieldDelegate协议中各个方法的作用与Xcode中AppDelegate.m中自动生成各个方法的作用(iOS)

UITextFieldDelegate协议中各个方法的作用 控制当前输入框是否能被编辑 - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField 当输入框开始时触发(获得焦点触发) - (void)textFieldDidBeginEditing:(UITextField *)textField 询问输入框是否可以结束编辑(键盘是否可以收回) - (BOOL)textFieldShouldEndEditing:(UITextFiel

cocos2d-x 3.0游戏实例学习笔记 《跑酷》移植到android手机

说明:这里是借鉴:晓风残月前辈的博客,他是将泰然网的跑酷教程,用cocos2d-x 2.X 版本重写的,目前我正在学习cocos2d-X3.0 于是就用cocos2d-X 3.0重写,并做相关笔记 好吧,自从上次<跑酷>完结之后,就什么没做什么的,主要是修复了一点点bug ,也在相应的文章里面做出了相应的改动.不过,只能在window上看着玩又有什么太大意思呢!!!于是乎又尝试着弄到手机上试试--OK,初生牛犊不要脸,哦不!不怕喷.这里记录一下我的移植过程.(这里会用到新的大小的资源.待会也会

android客户端和网站数据交互的实现(基于Http协议获取数据方法)

android客户端一般不直接访问网站数据库,而是像浏览器一样发送get或者post请求,然后网站返回客户端能理解的数据格式,客户端解析这些数据,显示在界面上,常用的数据格式是xml和json. 可以理解客户端其实是一个你自己定义标记语言的浏览器,一般浏览器能解析的是html+css的数据,而android客户端能解析的是xml和json(或者都不是而是你自己定义的火星格式),服务端为了能满足客户端输出这种数据格式的需求,不得不专门针对客户端开发不同于浏览器访问的接口. 开发一个网站的客户端你需

嵌入式学习笔记104-uboot_1.1.6移植(4)

前面的4篇uboot博文基本概括了uboot的整体flow,现在使能支持启动linux,至此之前请先阅读<嵌入式学习笔记200-Linux kernel初体验>和<嵌入式学习笔记201-Linux kernel动起来>.准备kernel的镜像文件才可以立马检测uboot是否能够启动kernel.在u-boot-1.1.6\include\configs\tq2440.h 最后添加如下几行code: /****************** boot kernel setup ****