- 引言
- CANopenNode项目简介
- CANopenNode的移植接口
- CO_driver_target.h
- CO_driver.c
- eeprom.h & eeprom.c
- 基于FlexCAN适配CANopenNode
- 一个CANopenNode的应用样例
- 总结
- 参考文献
引言
CANopen是实现CAN设备组网的典型协议栈和规范,对应于软件系统中,有一些开源的软件组件,实现了CANopen协议栈,例如CANopenNode和CAN Festival。CANFestival和CANopenNode都是用于在嵌入式系统上实现CANopen协议通信的开源软件协议栈,但需要注意的是,它们使用了不同的开源协议:
- CANFestival使用LGPLv2开源协议,这意味着CANFestival的源代码是免费提供的,任何人都可以使用、修改和分发,但衍生作品使用相同的GPL许可证。如果一个公司在产品中使用CANFestival组件,他们也必须按照同样的LGPLv2开源协议提供其产品的源代码。
- CANopenNode使用 Apache v2.0开源协议。这是一个自由度比LGPLv2更为开发的一个开源协议,允许在使用软件方面有更大的灵活性。任何人都可以使用、修改和发布CANopenNode,甚至用于商业目的,而不需要发布其衍生作品的源代码。
表x CANopenNode vs CAN Festival
本文将以CANopenNode为例,讲解CANopen协议的一种实现,并在具体的微控制器平台上适配运行。
CANopenNode项目简介
CANopenNode是一个免费的开源CANopen协议栈的实现。CANopen协议栈是一个在嵌入式控制器上的基于CAN总线高层应用协议,遵循国际标准CiA 301(EN 50325-4)。CANopenNode实现了CANopen协议栈的绝大多数功能:
- 网络管理协议中,NMT从站状态机(启动、停止、复位设备)和简单的NMT主站。
- 错误控制协议中,心跳消息的生产者(发送方)和消费者(接收方)
- PDO连接和动态映射到过程变量的快速交互
- SDO快速传输、常规(分段)传输和块传输
- SDO主站
- 紧急消息
- 同步协议中的生产者(发送方)和消费者(接收方)
- 授时协议中的生产者(发送方)和消费者(接收方)
- 非易失性存储
- LSS服务的主站和从机,LSS快速扫描
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的移植接口
CANopenNode的源码目录中,专门为在具体微控制器平台上实现移植提供了源码模板,位于stack/drvTemplate
目录中。本节简要分析其中的代码结构,为后续基于具体目标平台实现移植奠定基础。
stack/drvTemplate
目录下包含四个源文件:CO_driver.h, CO_driver_target.h, eeprom.c和eeprom.h。
CO_driver_target.h
CO_driver_target.h源文件包含了支持如下功能的数据类型定义、函数原型和宏定义:
- 基本的数据类型
- CANopen消息的接收和发送缓冲区
- 同微控制器集成CAN外设模块的接口
- CAN外设的接收和发送中断函数的声明(将用于在移植过程中嵌入硬件中断服务程序中)
这个源文件定义了的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,就会执行其中的回调函数。
回调函数有两个传入参数:
- object - CO_CANrxBufferInit()函数注册的一个指向传输对象的指针
- msg - 一个指向CO_CANrxMsg_t类型CAN消息的指针
回调函数可以返回CO_ReturnError_t类型的状态值:
- CO_ERROR_NO
- CO_ERROR_RX_OVERFLOW
- CO_ERROR_RX_PDO_OVERFLOW
- CO_ERROR_RX_MSG_LENGTH
- CO_ERROR_RX_PDO_LENGTH
发送过程
在发送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被设计基于多个线程运行,不同系统平台对多线程的实现方式也不尽相同。在微控制器平台,可以使用不同优先级的中断服务程序实现多个线程。此时,需要将多个线程可能共同访问的资源保护起来。一种简单的实现,可以在中断服务程序或者后台的调度器使用这些共享资源时,禁用对方,或者使用信号量等同步机制。
部分函数可以在不同的线程被调用:
- CO_driver.h中的CO_CANsend()函数
- CO_Emergency.h中的CO_errorReport()函数和CO_errorReset()函数
通常只有两个线程会访问到对象字典变量:一个是主线程,另一个是定时器线程。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数据类型
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_CANrxMsg_t - 接收CAN帧结构体
- CO_CANrx_t - 接收消息对象
- CO_CANtx_t - 发送消息对象
- CO_CANmodule_t - CAN外设模块对象
以及最后声明了CO_CANinterrupt()函数,便于开发者在移植时嵌入中断服务程序:
void CO_CANinterrupt(CO_CANmodule_t *CANmodule);
CO_driver.c
CO_driver.c文件是CANopenNode对接微控制器的底层接口,在CO_driver.c文件的函数中,添加操作目标微控制器平台包含CAN外设模块在内的电路系统的代码,建立CANopenNode同具体微控制器平台的绑定。CO_driver.c文件中实现了一些对CAN外设驱动进行抽象的函数,如表x所示。
表x CANopenNode抽象的CAN外设驱动函数清单
在具体的目标微控制器平台上移植CANopenNode时,需要结合具体的硬件CAN外设模块,补充这些函数中对硬件的操作。
eeprom.h & eeprom.c
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);
基于FlexCAN适配CANopenNode
(未完待续)
一个CANopenNode的应用样例
(未完待续)
总结
(未完待续)
参考文献
作者:安德鲁苏
文章来源:安德鲁的设计笔记本
推荐阅读
- CAN 总线开发一本全(5) -CANopen 协议概述
- CAN总线开发一本全(4) - FlexCAN的驱动程序
- CAN总线开发一本全(3) - 微控制器集成的FlexCAN外设
- CAN总线开发一本全(2) - CAN总线基础概要
- 定制带U盘功能的bootloader实现拖拽下载固件
更多MM32F5系列资料请关注灵动MM32 MCU专栏。如想进行MM32相关芯片技术交流,请添加极术小姐姐微信(id:aijishu20)加入微信群。