CANopen是实现CAN设备组网的典型协议栈和规范,对应于软件系统中,有一些开源的软件组件,实现了CANopen协议栈,例如CANopenNode和CAN Festival。CANFestival和CANopenNode都是用于在嵌入式系统上实现CANopen协议通信的开源软件协议栈,但需要注意的是,它们使用了不同的开源协议:
表x CANopenNode vs CAN Festival
本文将以CANopenNode为例,讲解CANopen协议的一种实现,并在具体的微控制器平台上适配运行。
CANopenNode是一个免费的开源CANopen协议栈的实现。CANopen协议栈是一个在嵌入式控制器上的基于CAN总线高层应用协议,遵循国际标准CiA 301(EN 50325-4)。CANopenNode实现了CANopen协议栈的绝大多数功能:
CANopenNode的源码在开源软件网站GitHub上发布,https://github.com/CANopenNode/CANopenNode,开发者可以直接下载完整的CANopenNode源码。此处需要注意,v1.3之后,持续开发的v2.0/4.0,在实现内容上有较大变化。本文选用已经标注“Verified”的v1.3版本,这也是目前最新的相对稳定的发布版本。
figure-conode-github-pack
图x 在GitHub上下载CANopenNode源码包CANopenNode组件是以ANSI C语言编写,可以作为一个标准的应用组件,方便地移植到不同的微控制器平台,或者是实时操作系统上。通过使用CANopenNode组件,可以在CANopen设备节点上创建一个对象字典(Object Dictionary),其中包含若干个变量(代表着配置信息),可以由本机直接通过C语言访问,也可以由别的CANopen节点通过CAN网络访问,以此来实现CAN总线网络系统中的信息交换,以及软件系统对硬件系统的控制。
CANopenNode组件本身并不是一个完整的应用程序,它包含一组实现CANopen协议栈的源码和基于本身的样例工程。如果要运行CANopenNode组件,还需要在一个具体的硬件平台上进行适配,例如文本中即将用到的集成FlexCAN外设模块的MM32F0140微控制器。
CANopenNode的开源站点上还开放了更多CANopen的功能组件,例如可以运行在Linux主机系统作为master的CANopenSocket项目,在多微控制器平台上实现演示用例和测试工具的CANopenDemo项目,可以编辑对象字典生成C源码文件的CAnopenEditor项目等等。
CANopenNode实现的工作流,如图x所示。
figure-conode-workflow
图x CANopenNode的工作流程其中,CAN总线接收线程和定时器周期执行线程,可以在微控制器的中断服务程序中实现,主线线程可以在main()函数的主循环中实现。至于SDO客户端和LSS(一个配置CANopen节点ID和比特率的服务)客户端,可以在应用层的用户程序中根据需要调用。
CANopenNode的源码文件组织结构,如图x所示。
figure-conode-source-files
图x CANopenNode源码文件的组织结构CANopenNode项目的stack
目录下分别实现了CANopen协议中的对象(通信过程),并封装在各自独立的源文件中。特别地,在stack/drvTemplate
目录中,为开发者提供了一个向具体目标平台移植CANopenNode的源码模板,同时还提供了在多种不同微控制器平台上移植CANopenNode的范例,例如stack/STM32
, stack/LPC1768
,stack/PIC32
等。
CANopenNode的源码目录中,专门为在具体微控制器平台上实现移植提供了源码模板,位于stack/drvTemplate
目录中。本节简要分析其中的代码结构,为后续基于具体目标平台实现移植奠定基础。
stack/drvTemplate
目录下包含四个源文件:CO_driver.h, CO_driver_target.h, eeprom.c和eeprom.h。
CO_driver_target.h源文件包含了支持如下功能的数据类型定义、函数原型和宏定义:
这个源文件定义了的CANopen的底层驱动程序,还定义了一些专用于优化协议执行过程的数据结构,它不再使用的CAN消息队列,而是直接将数据连接CANopen设备的对象(通信过程)上,尽量提高响应速度,并减少不必要的计算和内存开销。
CO_CANmodule_t结构类型中,包含了一组接收消息对象(CO_CANrx_t类型)和一组发送消息对象(CO_CANtx_t类型),每个CANopen通信对象都有自己专属的其中一个成员。例如,心跳消息生产者可以创建一个CANopen发送对象,它就需要在CO_CANtx_t数组中预留一个表项。同步模块可能产生一个同步对象或是接收一个同步的对象,它就需要在CO_CANtx_t数组或者CO_CANrx_t数组中预留一个表项。
在接收到CAN消息之前,CO_CANrx_t中的每个成员都必须被初始化,此时需要调用CO_CANrxBufferInit()函数,例如,在CO_HBconsumer中就使用了CO_CANrx_t中的多个成员(需要监控多个远程节点),就需要多次调用CO_CANrxBufferInit()函数,对每个CO_CANrx_t进行初始化。CO_CANrxBufferInit()函数的两个主要参数,一个是CAN ID,另一个是一个回调函数的指针,这两个参数将被写入到CO_CANrx_t数组中。其中的回调函数是根据具体功能模块实现的,用以处理接收的帧消息,将必要的数据搬运到合适的内存中,然后触发其他任务以继续处理接收数据。回调函数的程序必须要短小精悍,仅做少量必要的计算和数据搬运工作,以避免耽误后续接收帧的时机。
接收CAN帧的操作将在CAN外设模块的接收中断服务中进行。当在接收中断服务程序中捕获到CAN消息后,程序首先将它的CAN ID同CO_CANrx_t数组中的成员进行匹配,如果匹配到预先配置好的CO_CANrx_t,就会执行其中的回调函数。
回调函数有两个传入参数:
回调函数可以返回CO_ReturnError_t类型的状态值:
在发送CAN消息之前,CO_CANtx_t列表中的成员必须被CO_CANtxBufferInit()函数初始化。例如,心跳消息生产者就必须初始化它在CO_CANtx_t数组中的成员。CO_CANtxBufferInit()函数翻译一个指向CO_CANtx_t类型结构体的指针,其中包含了一个缓冲区,可以存放即将要发送帧的数据。之后,可以通过调用CO_CANsend()函数启动发送过程。如果恰巧微控制器硬件的发送缓冲区是可用的,就可以直接将发送消息从内存中搬运到CAN外设的硬件缓冲区中等待发送,否则,CO_CANsend()函数将设定_bufferFull_
标志位为True,之后将通过发送中断触发的硬件发送缓冲区可用事件,触发数据搬运过程并启动发送。CO_CANtx_t中的数据在通过CAN外设发送出去之前,是不可改动的。这里CO_CANtx_t队列中可能有多个成员的_bufferFull_
标志位为True,此时,编号更小的CO_CANtx_t将被优先发送出去。
CANopenNode被设计基于多个线程运行,不同系统平台对多线程的实现方式也不尽相同。在微控制器平台,可以使用不同优先级的中断服务程序实现多个线程。此时,需要将多个线程可能共同访问的资源保护起来。一种简单的实现,可以在中断服务程序或者后台的调度器使用这些共享资源时,禁用对方,或者使用信号量等同步机制。
部分函数可以在不同的线程被调用:
通常只有两个线程会访问到对象字典变量:一个是主线程,另一个是定时器线程。CANopenNode在主线程中运行CANopenNode的初始化过程和SDO服务端程序。PDO的程序运行在周期更短的定时器线程中,并且处理PDO的过程不能被主线程打断。主线程必须保护定时器线程同时在访问的对象字典变量,应用层的程序也是如此。需要注意的是,并不是所有的对象字典变量可以被映射到PDO,所以这些不被PDO操作的变量是不需要被保护起来的。SDO服务端操作是会保护操作对象字典中的变量。
CAN接收线程对接收到的CAN消息帧进行简单处理后,将它们写入对应的对象中,交由别的线程在后续继续处理。这个过程不需要保护任何关键区域。但有一个例外,当同步消息出现在CANopen的总线上时, 需要临时禁用CANrx(),直到所有的PDO都处理完毕。
这里需要开发者在移植CANopenNode到具体的微控制器平台上时,需要实现保护关键区的宏函数:
#define CO_LOCK_CAN_SEND() /**< Lock critical section in CO_CANsend() */
#define CO_UNLOCK_CAN_SEND()/**< Unlock critical section in CO_CANsend() */
#define CO_LOCK_EMCY() /**< Lock critical section in CO_errorReport() or CO_errorReset() */
#define CO_UNLOCK_EMCY() /**< Unlock critical section in CO_errorReport() or CO_errorReset() */
#define CO_LOCK_OD() /**< Lock critical section when accessing Object Dictionary */
#define CO_UNLOCK_OD() /**< Unock critical section when accessing Object Dictionary */
在接收CAN通信帧和处理消息的线程间同步消息缓冲区。当在中断服务程序中运行接收函数,则不需要进行任何同步操作,因为一旦中断发生,CPU的使用权会自动从其它处理消息帧的线程切换到中断服务程序。否则,需要使用一些同步机制,确保先接收到完整的CAN消息帧之后再处理它们。例如,使用GCC编译器时,可以使用GCC编译器内置的内存边界函数__sync_synchronize()
,此时,只要将CANrxMemoryBarrier()函数映射到这个内存边界函数即可。
#define CANrxMemoryBarrier() {__sync_synchronize();}
CO_driver_target.h文件中定义了一组内存同步相关的函数:
/** Memory barrier */
#define CANrxMemoryBarrier()
/** Check if new message has arrived */
#define IS_CANrxNew(rxNew) ((uintptr_t)rxNew)
/** Set new message flag */
#define SET_CANrxNew(rxNew) {CANrxMemoryBarrier(); rxNew = (void*)1L;}
/** Clear new message flag */
#define CLEAR_CANrxNew(rxNew) {CANrxMemoryBarrier(); rxNew = (void*)0L;}
CO_driver_target.h文件的后续,还定了一些基本数据类型:
/**
* @defgroup CO_dataTypes Data types
* @{
*
* According to Misra C
*/
/* int8_t to uint64_t are defined in stdint.h */
typedef unsigned char bool_t; /**< bool_t */
typedef float float32_t; /**< float32_t */
typedef long double float64_t; /**< float64_t */
typedef char char_t; /**< char_t */
typedef unsigned char oChar_t; /**< oChar_t */
typedef unsigned char domain_t; /**< domain_t */
/** @} */
以及CANopenNode在操作CAN硬件驱动时涉及到的结构体类型的定义,包括:
以及最后声明了CO_CANinterrupt()函数,便于开发者在移植时嵌入中断服务程序:
void CO_CANinterrupt(CO_CANmodule_t *CANmodule);
CO_driver.c文件是CANopenNode对接微控制器的底层接口,在CO_driver.c文件的函数中,添加操作目标微控制器平台包含CAN外设模块在内的电路系统的代码,建立CANopenNode同具体微控制器平台的绑定。CO_driver.c文件中实现了一些对CAN外设驱动进行抽象的函数,如表x所示。
表x CANopenNode抽象的CAN外设驱动函数清单
在具体的目标微控制器平台上移植CANopenNode时,需要结合具体的硬件CAN外设模块,补充这些函数中对硬件的操作。
eeprom.h和eeprom.c文件,绑定了读写EEPROM存储器的驱动程序,可以在CANopen协议运行的过程中,将对象字典保存在EEPROM存储器中。在基本的移植中,可以不实现将对象字典存储在外部存储器的功能。
/**
* Eeprom object.
*/
typedef struct{
uint8_t *OD_EEPROMAddress; /**< From CO_EE_init_1() */
uint32_t OD_EEPROMSize; /**< From CO_EE_init_1() */
uint8_t *OD_ROMAddress; /**< From CO_EE_init_1() */
uint32_t OD_ROMSize; /**< From CO_EE_init_1() */
uint32_t OD_EEPROMCurrentIndex; /**< Internal variable controls the OD_EEPROM vrite */
bool_t OD_EEPROMWriteEnable; /**< Writing to EEPROM is enabled */
}CO_EE_t;
/**
* First part of eeprom initialization. Called after microcontroller reset.
*
* @param ee This object will be initialized.
* @param OD_EEPROMAddress Address of OD_EEPROM structure from object dictionary.
* @param OD_EEPROMSize Size of OD_EEPROM structure from object dictionary.
* @param OD_ROMAddress Address of OD_ROM structure from object dictionary.
* @param OD_ROMSize Size of OD_ROM structure from object dictionary.
*
* @return #CO_ReturnError_t: CO_ERROR_NO, CO_ERROR_DATA_CORRUPT (Data in eeprom corrupt) or
* CO_ERROR_CRC (CRC from MBR does not match the CRC of OD_ROM block in eeprom).
*/
CO_ReturnError_t CO_EE_init_1(
CO_EE_t *ee,
uint8_t *OD_EEPROMAddress,
uint32_t OD_EEPROMSize,
uint8_t *OD_ROMAddress,
uint32_t OD_ROMSize);
/**
* Second part of eeprom initialization. Called after CANopen communication reset.
*
* @param ee - This object.
* @param eeStatus - Return value from CO_EE_init_1().
* @param SDO - SDO object.
* @param em - Emergency object.
*/
void CO_EE_init_2(
CO_EE_t *ee,
CO_ReturnError_t eeStatus,
CO_SDO_t *SDO,
CO_EM_t *em);
/**
* Process eeprom object.
*
* Function must be called cyclically. It strores variables from OD_EEPROM data
* block into eeprom byte by byte (only if values are different).
*
* @param ee This object.
*/
void CO_EE_process(CO_EE_t *ee);
(未完待续)
(未完待续)
(未完待续)
全部0条评论
快来发表一下你的评论吧 !