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

花瓣小跑  金牌会员 | 2024-9-17 07:07:48 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 808|帖子 808|积分 2424


前言

本次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驱动发送接口,对接上即可,转换下并不难。
  1. typedef struct {
  2.   UNS16 cob_id;        /**< message's ID */
  3.   UNS8 rtr;                /**< remote transmission request. (0 if not rtr message, 1 if rtr message) */
  4.   UNS8 len;                /**< message's length (0 to 8) */
  5.   UNS8 data[8]; /**< message's datas */
  6. } Message;
复制代码
如果原先没有CAN底层发送接口,那么建议先实现CAN收发,再来实现CANOPEN收发。AT91的实现如下
  1. unsigned char canSend(CAN_PORT notused, Message *m)
  2. /******************************************************************************
  3. The driver send a CAN message passed from the CANopen stack
  4. INPUT        CAN_PORT is not used (only 1 avaiable)
  5.         Message *m pointer to message to send
  6. OUTPUT        1 if  hardware -> CAN frame
  7. ******************************************************************************/
  8. {
  9.   unsigned int mask;
  10.   AT91S_CAN_MB *mb_ptr = AT91C_BASE_CAN_MB0 + START_TX_MB;
  11.   if ((AT91F_CAN_GetStatus(AT91C_BASE_CAN) & TX_INT_MSK) == 0)
  12.     return 0;                        // No free MB for sending
  13.   for (mask = 1 << START_TX_MB;
  14.        (mask & TX_INT_MSK) && !(AT91F_CAN_GetStatus(AT91C_BASE_CAN) & mask);
  15.         mask <<= 1, mb_ptr++)        // Search the first free MB
  16.   {
  17.   }
  18.   AT91F_CAN_CfgMessageIDReg(mb_ptr, m->cob_id, 0);        // Set cob id
  19.   // Mailbox Control Register, set remote transmission request and data lenght code
  20.   AT91F_CAN_CfgMessageCtrlReg(mb_ptr, m->rtr ? AT91C_CAN_MRTR : 0 | (m->len << 16));       
  21.   AT91F_CAN_CfgMessageDataLow(mb_ptr, *(UNS32*)(&m->data[0]));// Mailbox Data Low Reg
  22.   AT91F_CAN_CfgMessageDataHigh(mb_ptr, *(UNS32*)(&m->data[4]));// Mailbox Data High Reg
  23.   // Start sending by writing the MB configuration register to transmit
  24.   AT91F_CAN_InitTransferRequest(AT91C_BASE_CAN, mask);
  25.   return 1;        // successful
  26. }
复制代码
2、canReceive(Message *m)
CANOPEN发送接口一般在CAN停止中实现,重要用来实现当收到CANOPEN消息后,进行CANOPEN的协议剖析,协议剖析的接口为canDispatch函数。
  1. unsigned char canReceive(Message *m)
  2. /******************************************************************************
  3. The driver passes a received CAN message to the stack
  4. INPUT        Message *m pointer to received CAN message
  5. OUTPUT        1 if a message received
  6. ******************************************************************************/
  7. {
  8.   unsigned int mask;
  9.   AT91S_CAN_MB *mb_ptr = AT91C_BASE_CAN_MB0;
  10.   if ((AT91F_CAN_GetStatus(AT91C_BASE_CAN) & RX_INT_MSK) == 0)
  11.     return 0;                // Nothing received
  12.   for (mask = 1;
  13.        (mask & RX_INT_MSK) && !(AT91F_CAN_GetStatus(AT91C_BASE_CAN) & mask);
  14.         mask <<= 1, mb_ptr++)        // Search the first MB received
  15.   {
  16.   }
  17.   m->cob_id = AT91F_CAN_GetFamilyID(mb_ptr);
  18.   m->len = (AT91F_CAN_GetMessageStatus(mb_ptr) & AT91C_CAN_MDLC) >> 16;
  19.   m->rtr = (AT91F_CAN_GetMessageStatus(mb_ptr) & AT91C_CAN_MRTR) ? 1 : 0;
  20.   *(UNS32*)(&m->data[0]) = AT91F_CAN_GetMessageDataLow(mb_ptr);
  21.   *(UNS32*)(&m->data[4]) = AT91F_CAN_GetMessageDataHigh(mb_ptr);
  22.   // Enable Reception on Mailbox
  23.   AT91F_CAN_CfgMessageModeReg(mb_ptr, AT91C_CAN_MOT_RX | AT91C_CAN_PRIOR);
  24.   AT91F_CAN_InitTransferRequest(AT91C_BASE_CAN, mask);
  25.   return 1;                // message received
  26. }
复制代码
3、can_irq_handler
can停止函数实现,当停止来临时判断,如果吸收到消息就进行canopen协议剖析。
  1. void can_irq_handler(void)
  2. /******************************************************************************
  3. CAN Interrupt
  4. ******************************************************************************/
  5. {
  6.   volatile unsigned int status;
  7.   static Message m = Message_Initializer;                // contain a CAN message
  8.                
  9.   status = AT91F_CAN_GetStatus(AT91C_BASE_CAN) & AT91F_CAN_GetInterruptMaskStatus(AT91C_BASE_CAN);
  10.   if(status & RX_INT_MSK)
  11.   {        // Rx Interrupt
  12.     if (canReceive(&m))                        // a message received
  13.       canDispatch(&ObjDict_Data, &m);         // process it
  14.   }
  15. }
复制代码
canDispatch原型如下:
  1. void canDispatch(CO_Data* d, Message *m)
  2. {
  3.         UNS16 cob_id = UNS16_LE(m->cob_id);
  4.          switch(cob_id >> 7)
  5.         {
  6.                 case SYNC:                /* can be a SYNC or a EMCY message */
  7.                         if(cob_id == 0x080)        /* SYNC */
  8.                         {
  9.                                 if(d->CurrentCommunicationState.csSYNC)
  10.                                         proceedSYNC(d);
  11.                         } else                 /* EMCY */
  12.                                 if(d->CurrentCommunicationState.csEmergency)
  13.                                         proceedEMCY(d,m);
  14.                         break;
  15.                 case TIME_STAMP:
  16.                 case PDO1tx:
  17.                 case PDO1rx:
  18.                 case PDO2tx:
  19.                 case PDO2rx:
  20.                 case PDO3tx:
  21.                 case PDO3rx:
  22.                 case PDO4tx:
  23.                 case PDO4rx:
  24.                         if (d->CurrentCommunicationState.csPDO)
  25.                                 proceedPDO(d,m);
  26.                         break;
  27.                 case SDOtx:
  28.                 case SDOrx:
  29.                         if (d->CurrentCommunicationState.csSDO)
  30.                                 proceedSDO(d,m);
  31.                         break;
  32.                 case NODE_GUARD:
  33.                         if (d->CurrentCommunicationState.csLifeGuard)
  34.                                 proceedNODE_GUARD(d,m);
  35.                         break;
  36.                 case NMT:
  37.                         if (*(d->iam_a_slave))
  38.                         {
  39.                                 proceedNMTstateChange(d,m);
  40.                         }
  41.             break;
  42. #ifdef CO_ENABLE_LSS
  43.                 case LSS:
  44.                         if (!d->CurrentCommunicationState.csLSS)break;
  45.                         if ((*(d->iam_a_slave)) && cob_id==MLSS_ADRESS)
  46.                         {
  47.                                 proceedLSS_Slave(d,m);
  48.                         }
  49.                         else if(!(*(d->iam_a_slave)) && cob_id==SLSS_ADRESS)
  50.                         {
  51.                                 proceedLSS_Master(d,m);
  52.                         }
  53.                         break;
  54. #endif
  55.         }
  56. }
复制代码
该函数对吸收到的信息首先进行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函数原型如下:
  1. void TimeDispatch(void)
  2. {
  3.         TIMER_HANDLE i;
  4.         TIMEVAL next_wakeup = TIMEVAL_MAX; /* used to compute when should normaly occur next wakeup */
  5.         /* First run : change timer state depending on time */
  6.         /* Get time since timer signal */
  7.         UNS32 overrun = (UNS32)getElapsedTime();
  8.         TIMEVAL real_total_sleep_time = total_sleep_time + overrun;
  9.         s_timer_entry *row;
  10.         for(i=0, row = timers; i <= last_timer_raw; i++, row++)
  11.         {
  12.                 if (row->state & TIMER_ARMED) /* if row is active */
  13.                 {
  14.                         if (row->val <= real_total_sleep_time) /* to be trigged */
  15.                         {
  16.                                 if (!row->interval) /* if simply outdated */
  17.                                 {
  18.                                         row->state = TIMER_TRIG; /* ask for trig */
  19.                                 }
  20.                                 else /* or period have expired */
  21.                                 {
  22.                                         /* set val as interval, with 32 bit overrun correction, */
  23.                                         /* modulo for 64 bit not available on all platforms     */
  24.                                         row->val = row->interval - (overrun % (UNS32)row->interval);
  25.                                         row->state = TIMER_TRIG_PERIOD; /* ask for trig, periodic */
  26.                                         /* Check if this new timer value is the soonest */
  27.                                         if(row->val < next_wakeup)
  28.                                                 next_wakeup = row->val;
  29.                                 }
  30.                         }
  31.                         else
  32.                         {
  33.                                 /* Each armed timer value in decremented. */
  34.                                 row->val -= real_total_sleep_time;
  35.                                 /* Check if this new timer value is the soonest */
  36.                                 if(row->val < next_wakeup)
  37.                                         next_wakeup = row->val;
  38.                         }
  39.                 }
  40.         }
  41.         /* Remember how much time we should sleep. */
  42.         total_sleep_time = next_wakeup;
  43.         /* Set timer to soonest occurence */
  44.         setTimer(next_wakeup);
  45.         /* Then trig them or not. */
  46.         for(i=0, row = timers; i<=last_timer_raw; i++, row++)
  47.         {
  48.                 if (row->state & TIMER_TRIG)
  49.                 {
  50.                         row->state &= ~TIMER_TRIG; /* reset trig state (will be free if not periodic) */
  51.                         if(row->callback)
  52.                                 (*row->callback)(row->d, row->id); /* trig ! */
  53.                 }
  54.         }
  55. }
复制代码
该函数实现定时器调理功能:
计算自前次信号以来的时间偏移。
遍历全部定时器,根据是否已触发或周期到期更新状态和下次触发时间。
根据近来需触发的定时器设置体系就寝时间和现实定时器值。
再次遍历并调用已触发定时器的回调函数。可以参考
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这个结构体包含了对象字典剖析后的关键信息,在调试过程中也可以检察,好比是否存在越界,空指针等问题。具体的不再赘述,可以参考网上相关文章。
  1. struct struct_CO_Data {
  2.         /* Object dictionary */
  3.         UNS8 *bDeviceNodeId;
  4.         const indextable *objdict;
  5.         s_PDO_status *PDO_status;
  6.         TIMER_HANDLE *RxPDO_EventTimers;
  7.         void (*RxPDO_EventTimers_Handler)(CO_Data*, UNS32);
  8.         const quick_index *firstIndex;
  9.         const quick_index *lastIndex;
  10.         const UNS16 *ObjdictSize;
  11.         const UNS8 *iam_a_slave;
  12.         valueRangeTest_t valueRangeTest;
  13.        
  14.         /* SDO */
  15.         s_transfer transfers[SDO_MAX_SIMULTANEOUS_TRANSFERS];
  16.         /* s_sdo_parameter *sdo_parameters; */
  17.         /* State machine */
  18.         e_nodeState nodeState;
  19.         s_state_communication CurrentCommunicationState;
  20.         initialisation_t initialisation;
  21.         preOperational_t preOperational;
  22.         operational_t operational;
  23.         stopped_t stopped;
  24.      void (*NMT_Slave_Node_Reset_Callback)(CO_Data*);
  25.      void (*NMT_Slave_Communications_Reset_Callback)(CO_Data*);
  26.      
  27.         /* NMT-heartbeat */
  28.         UNS8 *ConsumerHeartbeatCount;
  29.         UNS32 *ConsumerHeartbeatEntries;
  30.         TIMER_HANDLE *ConsumerHeartBeatTimers;
  31.         UNS16 *ProducerHeartBeatTime;
  32.         TIMER_HANDLE ProducerHeartBeatTimer;
  33.         heartbeatError_t heartbeatError;
  34.         e_nodeState NMTable[NMT_MAX_NODE_ID];
  35.         /* NMT-nodeguarding */
  36.         TIMER_HANDLE GuardTimeTimer;
  37.         TIMER_HANDLE LifeTimeTimer;
  38.         nodeguardError_t nodeguardError;
  39.         UNS16 *GuardTime;
  40.         UNS8 *LifeTimeFactor;
  41.         UNS8 nodeGuardStatus[NMT_MAX_NODE_ID];
  42.         /* SYNC */
  43.         TIMER_HANDLE syncTimer;
  44.         UNS32 *COB_ID_Sync;
  45.         UNS32 *Sync_Cycle_Period;
  46.         /*UNS32 *Sync_window_length;;*/
  47.         post_sync_t post_sync;
  48.         post_TPDO_t post_TPDO;
  49.         post_SlaveBootup_t post_SlaveBootup;
  50.     post_SlaveStateChange_t post_SlaveStateChange;
  51.        
  52.         /* General */
  53.         UNS8 toggle;
  54.         CAN_PORT canHandle;       
  55.         scanIndexOD_t scanIndexOD;
  56.         storeODSubIndex_t storeODSubIndex;
  57.        
  58.         /* DCF concise */
  59.     const indextable* dcf_odentry;
  60.         UNS8* dcf_cursor;
  61.         UNS32 dcf_entries_count;
  62.         UNS8 dcf_status;
  63.     UNS32 dcf_size;
  64.     UNS8* dcf_data;
  65.        
  66.         /* EMCY */
  67.         e_errorState error_state;
  68.         UNS8 error_history_size;
  69.         UNS8* error_number;
  70.         UNS32* error_first_element;
  71.         UNS8* error_register;
  72.     UNS32* error_cobid;
  73.         s_errors error_data[EMCY_MAX_ERRORS];
  74.         post_emcy_t post_emcy;
  75.        
  76. #ifdef CO_ENABLE_LSS
  77.         /* LSS */
  78.         lss_transfer_t lss_transfer;
  79.         lss_StoreConfiguration_t lss_StoreConfiguration;
  80. #endif       
  81. };
复制代码

五、CANOPEN应用层接口

上述移植完成后驱动大部分内容已经完成,接下来就是应用层调用什么接口进行主从站的通讯。在例子中可以看到,主站进入工作状态y以及设置NodeID,必要调用如下接口
1、setState设置状态
  1.   setState(&ObjDict_Data, Initialisation);        // Init the state
  2.   setNodeId (&ObjDict_Data, 0x7F);
  3.   setState(&ObjDict_Data, Operational);                // Put the master in operational mode
  4.        
复制代码
2、masterSendNMTstateChange设置从站状态
  1. UNS8 masterSendNMTstateChange(CO_Data* d, UNS8 nodeId, UNS8 cs)
  2. {
  3.   Message m;
  4.   MSG_WAR(0x3501, "Send_NMT cs : ", cs);
  5.   MSG_WAR(0x3502, "    to node : ", nodeId);
  6.   /* message configuration */
  7.   m.cob_id = 0x0000; /*(NMT) << 7*/
  8.   m.rtr = NOT_A_REQUEST;
  9.   m.len = 2;
  10.   m.data[0] = cs;
  11.   m.data[1] = nodeId;
  12.   return canSend(d->canHandle,&m);
  13. }
复制代码
可以发现最终都是调用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的,可以借助串口工具进行开发。

研发阶段结束后必要关闭,避免打印的延时。
  1. #define DEBUG_WAR_CONSOLE_ON
  2. #define DEBUG_ERR_CONSOLE_ON
复制代码
3、借助支持canopen协议的工具可以直接看到传输的协议内容,对于调试有很大帮助,建议备一个,如果着实没有就接串口。

4、有些canfestival源码大概存在bug,根据现实情况依然必要检察源码进行修改,不要觉得源码必然靠谱。
5、canSend转换到can底层传输肯定要注意RTR,数据帧用的比力多,但是肯定不能省。
6、想到再增补。

总结

CANOPEN协议移植重要调试时间花在timer定时器、can发送和停止实现和对象字典的实现上,其他接口都是同一通用的,只要知道调用哪个接口就可以实现。时间匆匆讲的不是很具体,有什么问题可以留言。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

花瓣小跑

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表