当前位置: 首页 > news >正文

【经验分享】CANOPEN协议驱动移植(基于CANfestival源码架构)

【经验分享】CANOPEN协议驱动移植(基于CANfestival源码架构)

  • 前言
  • 一、CANOPEN整体实现原理
  • 二、CANOPEN驱动收发
  • 三、Timer定时器
  • 四、Object Dictionary对象字典
  • 五、CANOPEN应用层接口
  • 六、CANOPEN 驱动移植经验
  • 总结


前言

本次CANOPEN移植基于CANfestival开源代码,整体参考了如下文章:
基于STM32F4的CANOpen移植教程(超级详细)
谈谈自己对CANOPEN协议的驱动移植理解。每个移植CANOPEN协议的请务必认真阅读《周立功CANopen 轻松入门》,其中的内容生动形象,对你移植CANOPEN代码会有很大帮助。

CANopen的难点在于需要掌握的知识点比较多,如果没有移植过类似于Ethercat等协议,对新手来说并不算容易。如果移植过协议类驱动,那入手相对容易一些。


一、CANOPEN整体实现原理

带OS(操作系统)的整体实现原理
在这里插入图片描述
不带OS的整体实现原理
在这里插入图片描述
不管是带OS还是不带OS,都需要注意三个要点
1、CAN驱动收发实现
2、Timer定时器实现
3、Object Dictionary对象字典实现
这三点贯穿CANOPEN驱动调试整个过程,实现成功基本就不会有太大问题了,后续详细讲解。
CANOPEN协议的报文格式和CAN的消息格式区别不大,唯一的区别在于COB-ID的区别,COB-ID由Funciton code(功能码)和NODE ID构成。
在这里插入图片描述
这部分了解即可,例如0x580+NODE ID为SDO接收,0x600+NODE ID为SDO发送。
在这里插入图片描述
由此引出三种模型
在这里插入图片描述
第一种是主从站模型,一主多从模型,网络管理基于此模型。
第二种是客户端/服务器模型,一般是主站作为客户端,从站作为服务器端,SDO传输基于此模型。
第三种是消费者/生产者模型,这在《周立功CANopen 轻松入门》中有很生动的解释,生产者数据发送之后,消费者只接收不回复,就像买菜一样,PDO运行基于此模型。


二、CANOPEN驱动收发

CANOPEN调用的接口
1、canSend(CAN_PORT notused, Message *m)
canSend是canfestival协议实现的关键底层接口函数,最终调用的是CAN的底层驱动发送接口。

查看Message结构体定义,如果原来就有CAN驱动发送接口,对接上即可,转换下并不难。

typedef struct {UNS16 cob_id;	/**< message's ID */UNS8 rtr;		/**< remote transmission request. (0 if not rtr message, 1 if rtr message) */UNS8 len;		/**< message's length (0 to 8) */UNS8 data[8]; /**< message's datas */
} Message;

如果原先没有CAN底层发送接口,那么建议先实现CAN收发,再来实现CANOPEN收发。AT91的实现如下

unsigned char canSend(CAN_PORT notused, Message *m)
/******************************************************************************
The driver send a CAN message passed from the CANopen stack
INPUT	CAN_PORT is not used (only 1 avaiable)Message *m pointer to message to send
OUTPUT	1 if  hardware -> CAN frame
******************************************************************************/
{unsigned int mask;AT91S_CAN_MB *mb_ptr = AT91C_BASE_CAN_MB0 + START_TX_MB;if ((AT91F_CAN_GetStatus(AT91C_BASE_CAN) & TX_INT_MSK) == 0)return 0;			// No free MB for sendingfor (mask = 1 << START_TX_MB;(mask & TX_INT_MSK) && !(AT91F_CAN_GetStatus(AT91C_BASE_CAN) & mask);mask <<= 1, mb_ptr++)	// Search the first free MB{}AT91F_CAN_CfgMessageIDReg(mb_ptr, m->cob_id, 0);	// Set cob id// Mailbox Control Register, set remote transmission request and data lenght codeAT91F_CAN_CfgMessageCtrlReg(mb_ptr, m->rtr ? AT91C_CAN_MRTR : 0 | (m->len << 16));	AT91F_CAN_CfgMessageDataLow(mb_ptr, *(UNS32*)(&m->data[0]));// Mailbox Data Low RegAT91F_CAN_CfgMessageDataHigh(mb_ptr, *(UNS32*)(&m->data[4]));// Mailbox Data High Reg// Start sending by writing the MB configuration register to transmitAT91F_CAN_InitTransferRequest(AT91C_BASE_CAN, mask);return 1;	// successful
}

2、canReceive(Message *m)
CANOPEN发送接口一般在CAN中断中实现,主要用来实现当收到CANOPEN消息后,进行CANOPEN的协议解析,协议解析的接口为canDispatch函数。

unsigned char canReceive(Message *m)
/******************************************************************************
The driver passes a received CAN message to the stack
INPUT	Message *m pointer to received CAN message
OUTPUT	1 if a message received
******************************************************************************/
{unsigned int mask;AT91S_CAN_MB *mb_ptr = AT91C_BASE_CAN_MB0;if ((AT91F_CAN_GetStatus(AT91C_BASE_CAN) & RX_INT_MSK) == 0)return 0;		// Nothing receivedfor (mask = 1;(mask & RX_INT_MSK) && !(AT91F_CAN_GetStatus(AT91C_BASE_CAN) & mask);mask <<= 1, mb_ptr++)	// Search the first MB received{}m->cob_id = AT91F_CAN_GetFamilyID(mb_ptr);m->len = (AT91F_CAN_GetMessageStatus(mb_ptr) & AT91C_CAN_MDLC) >> 16;m->rtr = (AT91F_CAN_GetMessageStatus(mb_ptr) & AT91C_CAN_MRTR) ? 1 : 0;*(UNS32*)(&m->data[0]) = AT91F_CAN_GetMessageDataLow(mb_ptr);*(UNS32*)(&m->data[4]) = AT91F_CAN_GetMessageDataHigh(mb_ptr);// Enable Reception on MailboxAT91F_CAN_CfgMessageModeReg(mb_ptr, AT91C_CAN_MOT_RX | AT91C_CAN_PRIOR);AT91F_CAN_InitTransferRequest(AT91C_BASE_CAN, mask);return 1;		// message received
}

3、can_irq_handler
can中断函数实现,当中断来临时判断,如果接收到消息就进行canopen协议解析。

void can_irq_handler(void)
/******************************************************************************
CAN Interrupt
******************************************************************************/
{volatile unsigned int status;static Message m = Message_Initializer;		// contain a CAN messagestatus = AT91F_CAN_GetStatus(AT91C_BASE_CAN) & AT91F_CAN_GetInterruptMaskStatus(AT91C_BASE_CAN);if(status & RX_INT_MSK){	// Rx Interruptif (canReceive(&m))			// a message receivedcanDispatch(&ObjDict_Data, &m);         // process it}
}

canDispatch原型如下:

void canDispatch(CO_Data* d, Message *m)
{UNS16 cob_id = UNS16_LE(m->cob_id);switch(cob_id >> 7){case SYNC:		/* can be a SYNC or a EMCY message */if(cob_id == 0x080)	/* SYNC */{if(d->CurrentCommunicationState.csSYNC)proceedSYNC(d);} else 		/* EMCY */if(d->CurrentCommunicationState.csEmergency)proceedEMCY(d,m);break;case TIME_STAMP:case PDO1tx:case PDO1rx:case PDO2tx:case PDO2rx:case PDO3tx:case PDO3rx:case PDO4tx:case PDO4rx:if (d->CurrentCommunicationState.csPDO)proceedPDO(d,m);break;case SDOtx:case SDOrx:if (d->CurrentCommunicationState.csSDO)proceedSDO(d,m);break;case NODE_GUARD:if (d->CurrentCommunicationState.csLifeGuard)proceedNODE_GUARD(d,m);break;case NMT:if (*(d->iam_a_slave)){proceedNMTstateChange(d,m);}break;
#ifdef CO_ENABLE_LSScase LSS:if (!d->CurrentCommunicationState.csLSS)break;if ((*(d->iam_a_slave)) && cob_id==MLSS_ADRESS){proceedLSS_Slave(d,m);}else if(!(*(d->iam_a_slave)) && cob_id==SLSS_ADRESS){proceedLSS_Master(d,m);}break;
#endif}
}

该函数对接收到的信息首先进行COB-ID判断是什么类型,然后进行相应的报文处理。

各函数实现可以参考CANfestival中examples中的实现,带OS和不带OS的都有。
在这里插入图片描述


三、Timer定时器

这是第二个重点,各驱动的实现各有不同,需要实现:
1、void initTimer(void) 初始化定时器
2、void setTimer(TIMEVAL value) 用于设置下一个定时器报警时间
3、TIMEVAL getElapsedTime(void) 该函数用于获取自上次调用以来所经过的时间。它通过复制运行中的计时器的值,然后计算当前计时器值与上次调用时计时器值之间的差异来实现。
4、void timer_can_irq_handler(void) 此函数处理定时器中断,定时时间到即处理,调用TimeDispatch函数更新栈中的时间信息。TimeDispatch函数原型如下:

void TimeDispatch(void)
{TIMER_HANDLE i;TIMEVAL next_wakeup = TIMEVAL_MAX; /* used to compute when should normaly occur next wakeup *//* First run : change timer state depending on time *//* Get time since timer signal */UNS32 overrun = (UNS32)getElapsedTime();TIMEVAL real_total_sleep_time = total_sleep_time + overrun;s_timer_entry *row;for(i=0, row = timers; i <= last_timer_raw; i++, row++){if (row->state & TIMER_ARMED) /* if row is active */{if (row->val <= real_total_sleep_time) /* to be trigged */{if (!row->interval) /* if simply outdated */{row->state = TIMER_TRIG; /* ask for trig */}else /* or period have expired */{/* set val as interval, with 32 bit overrun correction, *//* modulo for 64 bit not available on all platforms     */row->val = row->interval - (overrun % (UNS32)row->interval);row->state = TIMER_TRIG_PERIOD; /* ask for trig, periodic *//* Check if this new timer value is the soonest */if(row->val < next_wakeup)next_wakeup = row->val;}}else{/* Each armed timer value in decremented. */row->val -= real_total_sleep_time;/* Check if this new timer value is the soonest */if(row->val < next_wakeup)next_wakeup = row->val;}}}/* Remember how much time we should sleep. */total_sleep_time = next_wakeup;/* Set timer to soonest occurence */setTimer(next_wakeup);/* Then trig them or not. */for(i=0, row = timers; i<=last_timer_raw; i++, row++){if (row->state & TIMER_TRIG){row->state &= ~TIMER_TRIG; /* reset trig state (will be free if not periodic) */if(row->callback)(*row->callback)(row->d, row->id); /* trig ! */}}
}

该函数实现定时器调度功能:

计算自上次信号以来的时间偏移。
遍历所有定时器,根据是否已触发或周期到期更新状态和下次触发时间。
根据最近需触发的定时器设置系统睡眠时间和实际定时器值。
再次遍历并调用已触发定时器的回调函数。可以参考
CANopen补充–时间计算出错


四、Object Dictionary对象字典

对象字典是连接底层和应用层通信的重要桥梁,没有对象字典就无法解析SDO和PDO等报文。对象字典的生成依赖于如下python工具objdictedit,在canfestival源码里。
在这里插入图片描述
根据sdo和pdo的要求进行配置,注意主站需要配置成client,从站配置成server。pdo配置,主站的tpdo和从站的rpdo cob-id要一致,主站的rpdo和从站的tpdo要一致,否则无法获取到数据。
在这里插入图片描述
配置完成后点击建立词典即可生成Master.c和Master.h文件。
在这里插入图片描述
需要注意,生成后的文件可能还需要进行二次修改,不一定可以直接使用。Master.c最后一句是连接主函数和对象字典的关键。所以一定要匹配上。
main.c函数,引用对象字典Master_Data
在这里插入图片描述
Master.c函数Master_Data定义
在这里插入图片描述
Co_Data这个结构体包含了对象字典解析后的关键信息,在调试过程中也可以查看,比如是否存在越界,空指针等问题。详细的不再赘述,可以参考网上相关文章。

struct struct_CO_Data {/* Object dictionary */UNS8 *bDeviceNodeId;const indextable *objdict;s_PDO_status *PDO_status;TIMER_HANDLE *RxPDO_EventTimers;void (*RxPDO_EventTimers_Handler)(CO_Data*, UNS32);const quick_index *firstIndex;const quick_index *lastIndex;const UNS16 *ObjdictSize;const UNS8 *iam_a_slave;valueRangeTest_t valueRangeTest;/* SDO */s_transfer transfers[SDO_MAX_SIMULTANEOUS_TRANSFERS];/* s_sdo_parameter *sdo_parameters; *//* State machine */e_nodeState nodeState;s_state_communication CurrentCommunicationState;initialisation_t initialisation;preOperational_t preOperational;operational_t operational;stopped_t stopped;void (*NMT_Slave_Node_Reset_Callback)(CO_Data*);void (*NMT_Slave_Communications_Reset_Callback)(CO_Data*);/* NMT-heartbeat */UNS8 *ConsumerHeartbeatCount;UNS32 *ConsumerHeartbeatEntries;TIMER_HANDLE *ConsumerHeartBeatTimers;UNS16 *ProducerHeartBeatTime;TIMER_HANDLE ProducerHeartBeatTimer;heartbeatError_t heartbeatError;e_nodeState NMTable[NMT_MAX_NODE_ID]; /* NMT-nodeguarding */TIMER_HANDLE GuardTimeTimer;TIMER_HANDLE LifeTimeTimer;nodeguardError_t nodeguardError;UNS16 *GuardTime;UNS8 *LifeTimeFactor;UNS8 nodeGuardStatus[NMT_MAX_NODE_ID];/* SYNC */TIMER_HANDLE syncTimer;UNS32 *COB_ID_Sync;UNS32 *Sync_Cycle_Period;/*UNS32 *Sync_window_length;;*/post_sync_t post_sync;post_TPDO_t post_TPDO;post_SlaveBootup_t post_SlaveBootup;post_SlaveStateChange_t post_SlaveStateChange;/* General */UNS8 toggle;CAN_PORT canHandle;	scanIndexOD_t scanIndexOD;storeODSubIndex_t storeODSubIndex; /* DCF concise */const indextable* dcf_odentry;UNS8* dcf_cursor;UNS32 dcf_entries_count;UNS8 dcf_status;UNS32 dcf_size;UNS8* dcf_data;/* EMCY */e_errorState error_state;UNS8 error_history_size;UNS8* error_number;UNS32* error_first_element;UNS8* error_register;UNS32* error_cobid;s_errors error_data[EMCY_MAX_ERRORS];post_emcy_t post_emcy;#ifdef CO_ENABLE_LSS/* LSS */lss_transfer_t lss_transfer;lss_StoreConfiguration_t lss_StoreConfiguration;
#endif	
};

五、CANOPEN应用层接口

上述移植完成后驱动大部分内容已经完成,接下来就是应用层调用什么接口进行主从站的通信。在例子中可以看到,主站进入工作状态y以及设置NodeID,需要调用如下接口
1、setState设置状态

  setState(&ObjDict_Data, Initialisation);	// Init the statesetNodeId (&ObjDict_Data, 0x7F);setState(&ObjDict_Data, Operational);		// Put the master in operational mode

2、masterSendNMTstateChange设置从站状态

UNS8 masterSendNMTstateChange(CO_Data* d, UNS8 nodeId, UNS8 cs)
{Message m;MSG_WAR(0x3501, "Send_NMT cs : ", cs);MSG_WAR(0x3502, "    to node : ", nodeId);/* message configuration */m.cob_id = 0x0000; /*(NMT) << 7*/m.rtr = NOT_A_REQUEST;m.len = 2;m.data[0] = cs;m.data[1] = nodeId;return canSend(d->canHandle,&m);
}

可以发现最终都是调用canSend进行底层报文发送出去。
3、masterSendNMTnodeguard用来设置从站节点守护进程,设置成功后可以收到从站的心跳报文。
4、sendsdo用来发送服务数据对象sdo命令,直接调用即可。
5、过程数据对象发送pdo比较特殊,有好几种通信方式。选择FEh或者FFh后,再设置Event timer,tpdo就会自动发送。(注意:TPDO和RPDO是相对于自身来定义的,T发送,R接收)。
在这里插入图片描述
6、写字典和读字典setODentry和getODentry,可以用来改变对象字典中的参数,Pdo过程中的数据传递等,注意各输入参数的定义。


六、CANOPEN 驱动移植经验

1、timer定时器调试过程中需要注意时间溢出问题,避免出现定时不准,如果不重新开定时器也可以用系统时钟。把timer加入监控内容,调试过程中需要注意是否停止。
2、canfestival默认是开启串口Log的,可以借助串口工具进行开发。
在这里插入图片描述

研发阶段结束后需要关闭,避免打印的延时。

#define DEBUG_WAR_CONSOLE_ON
#define DEBUG_ERR_CONSOLE_ON

3、借助支持canopen协议的工具可以直接看到传输的协议内容,对于调试有很大帮助,建议备一个,如果实在没有就接串口。
在这里插入图片描述
4、有些canfestival源码可能存在bug,根据实际情况依然需要查看源码进行修改,不要觉得源码必然靠谱。
5、canSend转换到can底层传输一定要注意RTR,数据帧用的比较多,但是一定不能省。
6、想到再补充。


总结

CANOPEN协议移植主要调试时间花在timer定时器、can发送和中断实现和对象字典的实现上,其他接口都是统一通用的,只要知道调用哪个接口就可以实现。时间仓促讲的不是很详细,有什么问题可以留言。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Unity3D DOTS中ECS核心架构详解
  • 华为OD机试 - 数大雁(Java 2024 E卷 100分)
  • 指针的一些细节补充———C语言
  • Python 如何操作 Excel 文件(openpyxl, xlrd)
  • 基于STM32开发的智能农业监测与控制系统
  • 【深度学习】迭代次数 vs bs? 迭代次数 vs epoch
  • Vue.js 样式绑定
  • Systemc example based on VCS
  • 专家系统的核心要点解析|专家系统|人工智能|知识工程
  • 【中仕公考是骗子吗】公务员联考是什么意思?
  • 线性代数基础
  • 基于ssm+vue+uniapp的图书管理系统小程序
  • 关于武汉芯景科技有限公司的实时时钟芯片XJ8340开发指南(兼容DS1340)
  • 【微服务】springboot整合对象映射工具MapStruct使用详解
  • 力扣刷题--137. 只出现一次的数字 II【中等】
  • 《深入 React 技术栈》
  • Hibernate最全面试题
  • Idea+maven+scala构建包并在spark on yarn 运行
  • Java Agent 学习笔记
  • java 多线程基础, 我觉得还是有必要看看的
  • JavaScript 一些 DOM 的知识点
  • Median of Two Sorted Arrays
  • Mybatis初体验
  • Octave 入门
  • 基于webpack 的 vue 多页架构
  • 面试总结JavaScript篇
  • 前嗅ForeSpider教程:创建模板
  • 日剧·日综资源集合(建议收藏)
  • 使用 Node.js 的 nodemailer 模块发送邮件(支持 QQ、163 等、支持附件)
  • 使用 Xcode 的 Target 区分开发和生产环境
  • NLPIR智能语义技术让大数据挖掘更简单
  • ​埃文科技受邀出席2024 “数据要素×”生态大会​
  • ​插件化DPI在商用WIFI中的价值
  • ​虚拟化系列介绍(十)
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • #绘制圆心_R语言——绘制一个诚意满满的圆 祝你2021圆圆满满
  • $.extend({},旧的,新的);合并对象,后面的覆盖前面的
  • (day 2)JavaScript学习笔记(基础之变量、常量和注释)
  • (Java企业 / 公司项目)点赞业务系统设计-批量查询点赞状态(二)
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (SpringBoot)第二章:Spring创建和使用
  • (八)Spring源码解析:Spring MVC
  • (不用互三)AI绘画:科技赋能艺术的崭新时代
  • (附源码)php投票系统 毕业设计 121500
  • (附源码)计算机毕业设计ssm电影分享网站
  • (离散数学)逻辑连接词
  • (力扣)循环队列的实现与详解(C语言)
  • (算法)前K大的和
  • (原創) 博客園正式支援VHDL語法著色功能 (SOC) (VHDL)
  • (原創) 如何將struct塞進vector? (C/C++) (STL)
  • (原創) 是否该学PetShop将Model和BLL分开? (.NET) (N-Tier) (PetShop) (OO)
  • (转)树状数组
  • .class文件转换.java_从一个class文件深入理解Java字节码结构
  • .mp4格式的视频为何不能通过video标签在chrome浏览器中播放?
  • .NET 使用 JustAssembly 比较两个不同版本程序集的 API 变化