0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

CANopenNode的移植接口详解

CHANBAEK 来源:安德鲁的设计笔记本 作者:安德鲁苏 2023-06-23 15:49 次阅读

引言

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

图片

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/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的应用样例

(未完待续)

总结

(未完待续)

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 嵌入式
    +关注

    关注

    5017

    文章

    18496

    浏览量

    293139
  • 接口
    +关注

    关注

    33

    文章

    7965

    浏览量

    149224
  • 移植
    +关注

    关注

    1

    文章

    371

    浏览量

    27906
  • 开源
    +关注

    关注

    3

    文章

    3040

    浏览量

    41829
  • CANopen
    +关注

    关注

    8

    文章

    206

    浏览量

    43159
收藏 人收藏

    评论

    相关推荐

    基于MM32G5330的FlexCAN实现CANopenNode协议栈移植

    本文将介绍如何基于灵动MM32G5330的FlexCAN实现CANopenNode协议栈的移植,并使用灵动官方提供的开发板Mini-G5333进行验证。
    发表于 04-12 09:15 751次阅读
    基于MM32G5330的FlexCAN实现<b class='flag-5'>CANopenNode</b>协议栈<b class='flag-5'>移植</b>

    串行通讯接口详解

    本帖最后由 eehome 于 2013-1-5 09:52 编辑 串行通讯接口详解
    发表于 08-08 11:20

    uCOS-II如何在STM32上的移植详解

    下载用uCOS-II在STM32上的移植详解.pdf (1.64 MB )
    发表于 05-26 02:13

    STM32程序的移植详解步骤

    一,概括程序的移植包括以下几步1.观察待移植程序调用了哪些文件,将这些文件放入移植的工程当中2.在keil当中添加这些文件,并且添加.h文件的路径3.处理头文件及软件版本匹配的问题二,详解
    发表于 08-23 07:27

    RTAI分析及在s3c4510上的移植详解

    RTAI分析及在s3c4510上的移植详解
    发表于 03-28 09:52 24次下载

    uCOSII在LPC2210上的移植详解

    uCOSII在LPC2210上的移植详解:嵌入式实时操作系统在目标处理器平台上的移植是嵌入式软件开发的基础和前提。论文实现了源码公开的嵌入式实时操作系统μC/OS- II 在ARM7 微控制器LPC2
    发表于 12-31 15:11 144次下载

    嵌入式Linux内核移植详解(顶嵌)

    嵌入式内核移植步骤详解 含配置含义及内容等方面
    发表于 11-20 16:00 19次下载

    SDI数字分量串行接口详解_王杰

    SDI数字分量串行接口详解
    发表于 05-18 14:26 19次下载

    LCD接口类型详解

    LCD接口类型详解--大盛唐电子,专业代理啊ALLANCEMXICISOCOMEXARHKCATO天马屏!
    发表于 06-06 14:48 0次下载

    几种串行通信接口标准详解

    几种串行通信接口标准详解
    发表于 01-03 11:34 0次下载

    网络接口跨平台移植扩展控件设计

    网络接口跨平台移植扩展控件设计_魏惠茹
    发表于 01-07 21:39 0次下载

    《Linux设备驱动开发详解》第23章、Linux设备驱动的移植

    《Linux设备驱动开发详解》第23章、Linux设备驱动的移植
    发表于 10-27 10:58 9次下载
    《Linux设备驱动开发<b class='flag-5'>详解</b>》第23章、Linux设备驱动的<b class='flag-5'>移植</b>

    Uboot移植步骤详解

    Uboot移植步骤详解
    发表于 10-30 08:46 21次下载
    Uboot<b class='flag-5'>移植</b>步骤<b class='flag-5'>详解</b>

    uCOS_ARM移植要点详解

    uCOS_ARM移植要点详解
    发表于 10-31 11:25 11次下载
    uCOS_ARM<b class='flag-5'>移植</b>要点<b class='flag-5'>详解</b>

    基于FlexCAN适配CANopenNode

    总结在微控制器平台上移植CANopenNode,需要根据具体硬件条件,适配2个源文件。
    的头像 发表于 06-23 15:51 1142次阅读
    基于FlexCAN适配<b class='flag-5'>CANopenNode</b>