灵动微电子 · 2023年08月01日

CAN总线开发一本全(6) - CANopenNode组件(1)

  • 引言
  • 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
image.png

本文将以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版本,这也是目前最新的相对稳定的发布版本。

image.png
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所示。

image.png
figure-conode-workflow
图x CANopenNode的工作流程

其中,CAN总线接收线程和定时器周期执行线程,可以在微控制器的中断服务程序中实现,主线线程可以在main()函数的主循环中实现。至于SDO客户端和LSS(一个配置CANopen节点ID和比特率的服务)客户端,可以在应用层的用户程序中根据需要调用。

CANopenNode的源码文件组织结构,如图x所示。

image.png
figure-conode-source-files
图x CANopenNode源码文件的组织结构

CANopenNode项目的stack目录下分别实现了CANopen协议中的对象(通信过程),并封装在各自独立的源文件中。特别地,在stack/drvTemplate目录中,为开发者提供了一个向具体目标平台移植CANopenNode的源码模板,同时还提供了在多种不同微控制器平台上移植CANopenNode的范例,例如stack/STM32stack/LPC1768stack/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外设驱动函数清单
image.png

在具体的目标微控制器平台上移植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的应用样例

(未完待续)

总结

(未完待续)

参考文献

CANopen LSS服务解析

作者:安德鲁苏
文章来源:安德鲁的设计笔记本

推荐阅读

更多MM32F5系列资料请关注灵动MM32 MCU专栏。如想进行MM32相关芯片技术交流,请添加极术小姐姐微信(id:aijishu20)加入微信群。
推荐阅读
关注数
6144
内容数
276
灵动MM32 MCU相关技术知识,欢迎关注~
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息