CanFestival协议栈从站实现和源码简析
2019-07-13 20:20发布
生成海报
(转自我的新浪博客
http://blog.sina.com.cn/s/blog_b315f69b0102yhji.html)
一、先谈谈CanFestival
在2018年的一年中,项目接二连三紧紧跟随,刚完成工业级现场总线Profibus-DP通信协议,接着开始Canopen协议通信开发;当然,作为一个刚一涉入嵌入式行业时就兴奋似火的两年多sophomore(辣鸡)来说,能在项目中学习新的通信技术,也是一种巨大的诱惑。强大的实力是张牙舞爪的资本。
CanFestival协议栈是用于嵌入式Canopen通信开发的常用协议源码之一,使用过的软件开发人员评价都还不错,架构和逻辑层次比较清晰,因此方便开发人员进行后期应用扩展和源代码设计;
本文主要介绍CanFestival协议从站的开发流程,以及根据开发流程的顺序进行的简单源码介绍;希望能为后来的开发者们解决一些困惑;介绍的基础是ARM的MCU,基于FreeRTOS操作系统的开发过程;
二、谈CanFestival协议栈开发和源码前必要的操作要懂
首先是移植CanFestival协议栈源码,源码和相关文档下载路径https://canfestival.org/,移植过程参考http://www.cnblogs.com/tdyizhen1314/p/4348725.html,可百度一下,网上移植的文章内容基本相同;
在使用操作系统时,驱动整个协议栈运行的两个接口函数timerCanFestival()和canDispatch(&_Data, &m)可用如下方式调用:
timerCanFestival()需要创建一个软件定时器定时调用(也可用一个硬件定时器定时中断进行调用)。
canDispatch(&_Data, &m)需要创建一个信号量(或邮箱)和一个任务,在任务中阻塞获取信号量调用(硬件CAN中断中释放信号量或邮箱发送消息)。
协议栈实现逻辑框图可参考CanFestival源码目录CanFestival-3-8bfe0ac00cdbobjdictgendoc下的manual_en.pdf文档 Pag.10。如下图:
其次是对象字典的编辑和使用,CanFestival协议栈CanFestival-3-8bfe0ac00cdbobjdictgen目录下提供了对象字典编辑器objdictedit.py,此工具打开方式可百度“canfestival中对象字典编辑器的打开”;
对象字典编辑器提供的编辑功能个人觉得并不完全,若对对象字典的编辑比较全面,设计更深入可考虑使用Vector的EDS文件编辑器CANeds,然后使用对象字典编辑器导入由CANeds设计完成EDS文件,再保存为.od文件;缺点是CANeds编辑的EDS文件中有些字符为Unicode编码,不能使用objdictedit.py建立字典(即生成.c和.h文件),,,,伤人,,,;如下图CANeds工具:
(编辑时,若新增或修改的对象内容符合规范,则不用在意下面检查结果栏的警告和错误)
若要创建适用于自己项目的对象字典,可使用协议栈提供的对象字典例子文件,在路径CanFestival-3-8bfe0ac00cdbexamplesAVRSlave目录下的ObjDict.od文件,进行编辑和修改(也可导出为EDS文件供CANeds进行编辑和修改);如下图对象字典编辑器:
(通过对象字典编辑器可建立对象字典.c和.h文件用于工程编译,图中框出部分后面描述)
三、简析CanFestival协议栈源码
1、对象字典.c和.h文件
上面提到的对象字典编辑器建立的对象字典.c文件中,以变量和数组的方式列出了DS301标准的每一个必须的对象、设计者定义的可选对象、对象的数据内容; 通过索引和子索引进行管理和访问,并提供了管理和访问的相关接口,供协议栈调用。
如自定义的索引为0x2001的可选对象如下图:
其中对象子索引的数据内容以变量形式定义,即_highestSubIndex_obj2001,Device_Data_Channel1_Num,Device_Data_Channel2_Num,Device_Data_Channel3_Num,分别在对象和文件的开头处已定义,如下图所示(对象索引和子索引数据名称为.od文件中定义的名称):
其中的ODCallback_t Device_Data_callbacks[]数组变量为在上个图中对象字典编辑器中对该对象进行编辑时勾选“有回访”选项后生成的变量,若未勾选此选项,将不存在此变量;
勾选“有回访”默认生成的ODCallback_t Device_Data_callbacks[]数组中,其数组元素为ODCallback_t函数类型的变量,类型定义为(objdictdef.h文件中):
typedef UNS32 (*ODCallback_t)(CO_Data* d, const indextable *, UNS8 bSubindex);
该对象的每个子索引对应每个数组成员变量;当Canopen协议通信访问这些对象结束时(此处访问指写访问,即通信中向对象写数据,读访问也可实现但无必要,读访问实现回调见后文描述),将进行回调这些函数;若Canopen协议通信只访问这些对象的子索引时,访问结束时将只回调该子索引对应的回调函数。
这些回调函数可由程序设计人员自行决定回调函数入口,如本文例程中的回调函数入口如下:
其中Device_Data_Channel1_Num_ODCallback等函数将上图中的NULL进行替换,其函数的原型为:UNS32 Device_Data_Channel1_Num_ODCallback(CO_Data* d, const indextable *ptrTable, UNS8 bSubindex),其实现的功能由程序设计人员自行确定。(不知CanFestival协议栈的源码设计者初衷是不是如此,在这里进行回调函数替换NULL……0^0)
(事隔半年后记:哈哈,回头看这篇文章时,发现CanFestival协议栈的源码设计者初衷就是如此,在应用层设计回调函数,并且在objacces.c文件中已经提供回调函数注册接口RegisterSetODentryCallBack())
2、对象字典.c文件中对象数据的访问
在Canopen通信中对象的访问方式分为:
对象读访问;
对象写访问;
如上所讲,对象字典.c文件中以变量和数组的方式表示一个对象的索引或子索引的数据内容,因此程序设计人员可在程序的应用层中动态修改这些索引和子索引对应变量或数组的值,以达到应用程序与CanFestival协议栈数据交互的目的(一般要达到通信数据交互功能,只需修改对象字典中TPDO映射的对象数据或者厂商自定义对象数据)。
观察objacces.c文件中:
UNS32 _setODentry()函数用于Canopen通信对象写访问;
UNS32 _getODentry()函数则用于Canopen通信对象读访问;
但函数_getODentry中未执行回调函数,初步推断CanFestival协议栈的源码设计者预留这些Callback函数用于执行对象的读写访问以后,以作他用。
例如:写对象数据后,将对象数据更新到系统需要使用的其他地方;
读对象数据后,将系统数据做读取标记等;
回调函数的执行,上面谈到的回调函数,其执行过程在objacces.c文件的UNS32 _setODentry()函数中,在进行Canopen通信时,对象写访问时会调用到此函数,将通信中的数据写入访问对象结束后,执行回调函数,如下图:
回调函数的通信对象写访问时,也能达到通信数据交互功能,若要实现在通信对象读访问时也达到数据交互功能,则需要修改CanFestival协议栈源码,这是开发设计中所不建议的,但不影响功能。具体的修改方法如下图(仿造_setODentry()函数在_getODentry()中增加函数回调):
3、功能和目录结构
功能和文件目录结构可参考以下两篇文章:
https://blog.csdn.net/qq_34071268/article/details/78476943;
https://blog.csdn.net/iamplane/article/details/49944491;
4、协议栈工作逻辑流程简析
首先是CanFestival event scheduling,此部分内容想详细了解的可参考学习可参考CanFestival源码目录CanFestival-3-8bfe0ac00cdbobjdictgendoc下的manual_en.pdf文档 Pag.11,流程图如下:
其详细设计源代码可查看timer.c文件,其核心为void TimeDispatch(void)函数,用于操作协议栈工作过程中使用到的存放在s_timer_entry timers[MAX_NB_TIMER]中的定时器;在第二节移植协议栈源码时提到,此函数需要用定时器进行定时调度;
其次是作为一个从站设备初始化过程,作为代码级别的初始化过程,应分别调用:
setNodeId(&_Data, 1);
setState(&_Data, Initialisation);
// Automatic transition - No break statement !
// Transition from Initialisation to Pre_operational
// is automatic as defined in DS301.
// App don't have to call SetState(d, Pre_operational)
//setState(&_Data, Pre_operational);
setState(&_Data, Operational);
第一步设置从站设备的节点ID,否则无法正常工作运行,此时也会将ID写入对象字典中需要使用到的地方;
第二步将协议栈设置为初始化状态,此时若协议栈的配置文件config.h文件中未定义LSS(即自动波特率)功能,将不执行任何操作;
协议栈设置为初始化状态后会自动转化为预操作状态,并执行bootup等相关动作;(如注释部分英文解释)
第三步为将协议栈设置为操作状态,即正常运行状态,此时可进行正的数据交换;(此步骤需要根据主站的能力和需求决定是否执行,即从站可由主站发送命令的方式决定是否进入操作状态)
接着是协议栈的核心处理函数canDispatch(&_Data, &m),在第二节移植协议栈源码时提到,此函数需要在任务中进行阻塞式调度,其源代码如下:
此函数通过内部调用实现了CANopen协议的服务数据对象(sdo.c)、过程数据对象(pdo.c)、同步对象(sync.c)、自动波特率对象(lss.c)、节点守护和心跳(lifegrd.c)、网络管理对象(本例使用nmtSlave.c)等所有数据对象。
接着时间戳对象的处理,需要软件设计者自行实现,用于日历同步等功能;其日历数据格式按照DS301规范解析即可,如下图所示(起点时间为1984年1月1日):
接着PDO的传输类型与数据传输,在canDispatch(&_Data, &m)函数的调用中调用了proceedPDO (d, m)函数,其中对PDO的所有传输类型进行处理;
如:TPDO1的传输类型为1(“synchron” Transmissiontype 1)时,对象字典的编辑图如下:
在Canopen通信时,proceedPDO(...)函数将根据Transmission type 的类型进行处理,本例是在每个SYNC同步帧到来时准备好数据进行发送。
最后SDO的快速下载和分段下载(正常下载)说明,在canDispatch(&_Data, &m)函数的调用中调用了proceedSDO (d, m)函数,其中对SDO的快速下载和分段下载协议进行处理,具体的协议内容需要看DS301规范;
需要说明的是,分段下载协议(即ccs=0)在进行数据传输的时候所有的数据先进行接收,然后缓存在对象字典的变量d->transfers[line]中(其中line个数在config.h中目默认定义为1),直到分段传输结束后,调用SDOlineToObjdict(d, line)函数将缓存的数据写入到SDO下载的指定的对象中去。本例中使用分段下载协议进行软件升级,其实现过程参考SDO分段下载协议,如图所示:
第一步:
第二步到最后一步:
协议详细数据帧格式解析可参考:https://blog.csdn.net/ethercat_i7/article/category/7838923,此参考文章无SDO分段下载(正常下载)的详细帧说明,哈哈哈。
因此所有疑问还是得需详细学习和理解DS301规范文档,多看,多理解,多看,多理解,多看,多理解。
^-^ ^-^ ^-^
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮