CANopen(CiA301)

什么是CANopen

1986年,德国博世(Bosch)公司推出了 CAN 总线,全称 Controller Area Network(控制器局域网),成功解决了设备之间如何可靠的传输几个字节的问题。CAN 总线实时性高,可靠性好,一时间名声大振。但随着后续应用的深入,逐步暴露出了诸多问题:

CAN 总线只解决了物理层和数据链路层的问题,也就是说 CAN 总线只规定了怎么路由数据,而数据本身代表什么含义,怎么去理解统统没有定义。这就造成不同的设备厂商,各自持有一套自身的标准,相互之间移植性和通用性极差,有时候同一设备更换不同厂家的产品时甚至需要重写一套适配驱动。

为了解决这个混乱的问题,1992年 CiA(CAN in Automation)组织正式成立,负责专门把混乱的 CAN 应用层收编成一个统一的标准协议。1995年,CiA 发布了CiA DS-301(也就是现在的 CiA301 ),该协议定义了一套 CAN 应用层的标准规则,也就是我们后来所说的 CANopen。其核心设计理念就是:统一用对象字典(OD)来表示设备的所有参数,用标准的 SDO/PDO 来存取这些参数。

标准化的实现,使得不同厂商不需要再各自定义自身的私有协议,也简化了用户层面的压力:如果一台电机坏了,可以买任何支持 CANopen 的伺服替换,即插即用,而不必非得买原厂型号。

总而言之,CANopen 的本质就是一套应用层的规范协议。以经典网络四层模型来看,处于应用层。它不定义如何传输CAN 报文(这是 CAN 底层的事),而是表征了该帧报文的具体含义(参数读取/参数修改/节点状态切换)。


核心设计思想

CANopen 最精髓的地方在于每一个设备都维护了一套 OD 对象字典,该套字典本质上是一个巨大的内存地址表,内部存储了该设备的所有可配置参数。通过协议统一定义的通信模式以及通信数据元,使各个厂商的设备在同一条 CAN 总线或同一个 CAN 网络中高效的交换数据。这个被交换的数据就是 OD 字典内部的内存对象。

OD 字典

CANopen 通信的基础,整个设备所有的参数(温度/当前状态字/控制字等)全部放入了这张表中。每个参数都是独立的,并通过16位的主索引 + 8位的子索引来进行精准定位。

举个例子:某伺服电机的控制字在 0x6040 0x00 的位置,我们需要控制该电机,就可以通过 SDO 写入 0x40 0x60 0x00…来访问并修改这个状态字。这个例子中,0x6040便是16位的主索引,0x00便是8位的子索引。我们主控站不需要知道从站内部的具体结构,只需要知道我们要修改参数的地址即可。

以下是同毅IXL系列电机 OD 字典的控制字位置,可以看到也是 0x6040 0x00 的位置。这些伺服电机都是遵循 CiA 402 标准的,因此控制字都在这个位置。此处只是展示一下对象字典表,关于 CiA 402 标准本文不加以叙述。


SDO / PDO (服务/过程数据对象)

光有 OD 字典表是不够的,OD 字典只提供了数据的布局指导,还缺少读写接口来操纵这块内存表。CANopen 提供了两种类型通道,分别为 SDO (服务数据对象),PDO(过程数据对象)。二者可以简单理解为慢速配置通道(SDO)与实时配置通道(PDO)。

SDO:一问一答机制,也就是说每个 SDO 请求,其接收方都必须回复响应报文。且 SDO 报文每次通信时必须带上16位的主索引与8位子索引。这也是为啥 SDO 报文不适用于实时控制通信,开销太大且额外的 ACK 机制影响实时性。一般用于上电初始化、修改波特率、PDO 映射参数等低频操作。

PDO:没有应答机制,并且得益于地址映射,PDO通信时报文中无需携带参数地址信息,因此 PDO 报文用于高实时性控制,且能够一定程度上容忍丢包(如每1ms发送一次报文,某个时间节点的报文丢了并没什么影响)。

这里简单介绍了 CANopen 通信的两个基础数据元 SDO/PDO 。关于这些数据元更加深入的认识将在其余文章进行阐述。


NMT(Network Management) 网络管理

CiA 301 不仅规定了数据读写通道以及OD字典表,还定义了一套应用层的节点标准状态机,这个状态机的推动正是由 NMT 进行管理的。NMT 不携带用户数据,只负责让从站改变状态。

CiA 301标准定义了如下几种状态机:

  • 初始化(Initialisation)

设备上电后默认处于此状态。在该状态下设备执行上电自检,时间较短。

  • 预运行(Pre-Operational)

设备初始化完毕后进入该状态。在该状态下节点可以收发SDO,主站可通过SDO通信来修改节点的OD参数以及重映射PDO等等。该状态本质就是一个配置状态,不允许也不能够收发PDO。

  • 运行态(Operational)

运行态就是日常工作态,对于设备的运行实时控制在这个状态进行。这个状态可以收发PDO,也可以收发SDO(这里要注意:虽然该状态下可以收发SDO,但是OD字典中部分对象例如PDO映射参数等只能在 Pre-Operational 下进行更改)。

  • 停止态(Stopped)

除了 NMT 和心跳,其他通信全部切断,用于紧急制动。

CiA301标准中还有两个子状态:应用复位与通信复位。本文不做过多讲解,这里只做了解。


EMCY紧急报文/SYNC同步机制/节点守护

以上只是理想工况下的通信流程,实际工作中,需要更加健全的总线处突机制。CiA 301也给出了相应的健壮机制。

当总线上节点出现紧急情况时,故障节点会发送一帧 EMCY 报文,该报文优先级仅次于 NMT 命令以及 SYNC 同步命令。该紧急报文中涵盖了错误的具体分类,便于主站识别处理。

当需要多设备同步响应时,CiA301 提供了 SYNC 同步报文,以使多个节点在收到 SYNC(或达到SYNC阈值)时,在接下来一个响应窗口里面能够同时进行响应,保证了一致性。

同时,标准还规定了一套从机节点心跳检测的方法,当某节点后掉线后主机能够快速识别并进行对应动作。


本文目的只是作为一个 CANopen 的导览,很多地方细节并没有深入,更深入的部分将在其余文章中进行阐述。

ISO-TP 通信流程(上)

协议数据单元

在详细了解 ISO-TP 之前,首先需要了解 ISO-TP 的协议数据单元(PDU),这是整个协议栈的核心。ISO-TP 定义了 4 个协议数据单元,分别是:单帧(Single Frame),连续帧(Consecutive Frame),首帧(First Frame),流控帧(Flow Control)。

这四种 PDU 本质上都是一帧独立的 CAN 报文,区别在于报文的布局与含义。四种 PDU 协作共同完成单帧,多帧的传输动作。下面就来详细阐述下四种 PDU。


SF 单帧

iso-tp 支持单帧与多帧传输,SF 帧就是完成单帧传输的唯一 PDU 单元。在 Classic CAN 报文中,数据段最大8字节,其中SF 帧的数据段第一个字节为信息字节(N_PCI),你可以将其理解为一个控制头,内部携带了与传输相关的一些控制信息。

对于 SF 帧,PCI 字节主要是这么划分的:

  • 高 4 位:这是 PDU 类型信息头,对于 SF 固定为 0b0000。
  • 低 4 位:这是 SF_DL。也就是该单帧承载的数据长度。表示本次单帧通信报文中,数据段有多少个字节的数据。

除开 PCI 信息字节,其余部分均为数据负载。这里要注意:根据通信模式的选择不同(普通寻址 / 拓展寻址),数据负载大小分别为 7 / 6 字节,这是因为在拓展寻址中地址数据被放到了数据场第一个字节,PCI 被顺延到了第二个字节。协议开销占了两个字节,因此为6字节负载。关于通信模式,后续会讲述,只要记住拓展寻址地址会被放进数据段第一字节这一点即可。

以下是 Classic CAN 在普通寻址模式下的 PCI 布局方式,截自 <ISO 15765-2:2016[E]>

当然,标准对 CAN FD 也提供了支持。CAN FD 单帧最大数据负载能够达到64字节,因此原先4位的 SF_DL 布局已然不够使用。CAN FD 的 PCI 布局方式如下图所示:

第一字节高四位依然是类型识别码,这是固定的,所有 PDU 都是这么规定。SF_DL 被放到了第二字节,总共能表示 255 大小的数据,足够了。拓展寻址下,地址会被放入Byte1位置,此时 PCI 信息字节 与 SF_DL 顺势各自后延一个字节,这里简单了解一下就好。

这里有个细节要注意下:原文中 “Messages with CAN_DL > 8 shall use an escape sequence where the lower nibble of Byte #1 is set to 0 (invalid length). “ 也就是说只有当Byte1低4位全为0时,Byte2才被认作是有效的 SF_DL。这么规定是有原因的,因为 PCI 内部的识别码只能区分报文的类型,区分不了外部 CAN 控制器究竟是 Classic CAN 还是 CAN FD。标准实现内部在对 SF 进行处理时,首先会检查Byte1低4位是否为0,若不为0则按照 Classic CAN 处理;反之则按照 CAN FD 处理,继续读取后面第二字节作为 SF_DL。 如果你在尝试动手自行实现的话,这里尤其要注意。


FF 首帧

完成单帧通信,只需要一个 SF 帧即可。但是 iso-tp 重点不在于单帧报文通信,而是可靠的多帧传输与重组机制。而 FF 帧正是用于启动一次多帧传输的开关。FF 帧又叫首帧,其内包含了一个很重要的信息: FF_DL 本次多帧通信的数据长度。

启动一次多帧传输,首先必须要知道的一个元素就是本次通信的数据长度,对于发送方而言,这很好获取,要发多大的数据自身心里肯定有数;但对于接收方,这就是一个棘手的问题,接收方必须得知道数据有多长,以此来判断传输是否完毕,而数据总大小的来源只有从发送方获取,FF 帧正是被设计出来干这个事的。

PCI 布局上,固定高四位为类型识别码,FF 帧的类型识别码为 0b0001。对于数据大小 <= 4095 字节的多帧传输,FF_DL 占用了12位,而对于 > 4095 字节的多帧传输,协议支持将 FF_DL 映射到后续的连续4个字节,总共32位,理论数据量达到4Gb。

这里也要注意一个同样的问题:当传输数据大于4095时,原先12位 FF_DL 的位置必须被置0表示无效,具体原因参考上述协议栈对 SF帧 的处理。

这里我想讲一下,自行实现轻量化 iso-tp 时,对于常规开发,大于 4095 的情况可以选择性实现。因为对于常规MCU,我们通常是提供一整块缓冲去用于多帧接收,在一些性能中上的 MCU 中,拿出4kb左右的 RAM 做缓冲还是能够承受的,更何况大部分情况下可能用不到这么多。但是对于 > 4095 时,例如传输30kb数据,这在 iso-tp 看来是完全支持的,但是要在 MCU 中一次性开辟出30kb RAM 可是一笔不小的开销,在程序架构上就必须得使用流式传输。因此为了简化协议栈实现,我们可以只实现 < 4095 的情况,对于大于4kb的数据传输,采用分段切片传输即可。

FF 帧按标准是需要携带数据的!我们以普通寻址 Classic CAN 的标准8字节报文为例:构建一个 FF,PCI + FF_DL 共占2个字节,剩下6个字节要带上应用数据!这么设计的原因是保证总线利用率。当然,标准实现只是提供一个参考,你也可以直接发送一个2字节的短报文 FF 到对端,数据全部统一到 CF 中进行处理,这也是完全可以的。不过这些是属于各自实现的改造,我们本文只讨论标准实现。

至于 FF 帧如何启动一次多帧传输,此处暂时不展开。


CF 连续帧

FF 帧用于开启一次多帧传输,CF 帧则是多帧传输的主力军:携带数据包,作为通信的数据单元。不管是 CAN FD 还是 Classic CAN, 其 CF 帧的布局都是 一个 PCI 信息字节 + 数据负载。PCI 高4位位类型识别码:0b0010,低4位为 SN 序列码。SN 用于判断数据包到达的顺序,确保在多帧传输中及时发现丢包问题。

“that the SN shall start with zero for all segmented messages; the FirstFrame (FF) shall be assigned
the value zero; it does not include an explicit SequenceNumber in the N_PCI field but shall be treated
as the segment number zero; “

“that the SN of the first CF immediately following the FF shall be set to one; “

SN 序列码从0开始递增,在分段消息传输期间,每发送一个新的 CF,SN 递增 1,模16循环(数到15后回0,再从0开始到15循环)。首帧 FF 是隐含得有 SN = 0 这个概念的,所以 FF 之后的首个 CF 其 SN 码应该是从1开始,而不是0!之前我就没注意这个问题,虽然在发送端进行了正确处理,但是接收端却没有同步,发送方发来的第一个 CF 的 SN 为1,而我接收连接一直在等 SN = 0 的包,导致通信一直因为SN Dismatch出错。这里要注意。

注意:多帧传输过程,可能会涉及到 FC 帧的收发。FC 帧不参与 SN 的计数,SN 只表征 CF。

每一帧 CF (除开最后一帧)都必须填满数据段,中间不允许出现有空槽的包。最后一包数据可能已经塞不满一帧报文了,标准对于最后一帧 CF 报文允许出现短包。或者你也可以自行填充到8字节凑成一个完整包,填充到8字节对于逻辑实现比较简单,但是会浪费带宽,就看你怎么取舍了。这里我要强调一下,我是以 Classic CAN 为例子进行叙述的,如果你使用 CAN FD,那么得具体结合你一帧的负载总大小进行分析。


FC 流控帧

一次多帧传输事务中,流控机制是不得不考虑的因素。因为通信双方性能以及当前运行表现总是有差异的,若发送方不加控制的全速发送,接收方很可能由于处理跟不上导致出现丢包等异常情况;若发送方发送过慢,又会使通信效率打折,选择合适的流控参数是一次健壮高效通信事务的前提。FC 帧正是用于在开启一次多帧传输事务时,接收方向发送方传递流控参数。发送方必须严格遵守这个流控参数。

FC 帧的类型识别码为 0b0011。其 PCI 布局为高四位类型码 + 低四位 FS 码。FS 是流控状态,标准中定义了三个流控状态:

  • ContinueToSend (CTS):绿灯状态,由接收方->发送方,通知发送方可以继续 CF 帧的发送。
  • Wait (WAIT):黄灯状态,表示接收方当前正忙,请暂停一会发送(发送过程没有出现问题,只是接收方自身目前必须暂停接收来处理手上的急事)。 这里可以类比一下 TCP 的行为进行理解,在 TCP 通信中,当数据接收方通告窗口为0后,发送方会停止发送,直到对端窗口打开才会继续接下来的发送。iso-tp 也是类似行为,发送方收到 WAIT 后,会立即停止发送,并启动超时定时器。在该定时器超时之前收到 FC (除了OVFLW的任意状态)都能够重置定时器。当定时器超时后仍未收到 FC ,那么发送方将超时断开。当然,为了防止长时间被接收方阻塞,部分工程实现中还会增加一个计数器,每收到一个 WAIT 计数加1,若连续收到 WAIT 超过某个阈值,则也会主动断开连接。收到 CTS FC会解除 WAIT ,回到正常通信状态。
  • Overflow (OVFLW):红灯状态,接收方在收到一帧 FF 后,有权利拒绝本次通信的建立,这个权力便来源于此。FF 中携带有本次通信的数据总大小,当数据总大小 > 缓冲区大小时,可以发送 OVFLW 给对端终止通信,表示自身缓冲区将溢出,不能启动此次通信。当然规矩是死的,人是活的,对于流式传输架构,此处是否需要主动发送 OVFLW 就取绝于你的架构了。因为流式传输需要达到的目的本来就是小缓冲区复用来承接大数据,如果你的 FF_DL 含义是整个大数据块的总长的话,那么于此不应该也不能发送 OVFLW,因为 FF_DL > 缓冲区衡成立。

FC 流控帧除了流控状态码之外,还有两个核心参数:BS(Block Size),STmin(Separation Time min)。

  • BS:块大小,表示最大能够连续发送 CF 的数目。类似于 TCP 的滑动窗口,当窗口未被填满时,可以踩足马力发送,当窗口被填满时,必须停下来等待窗口重新开放。这里也是一样的,发送方发满 BS 后,会启动定时器并且等待下一个 FC.CTS 的到来,只有在正确收到 FC.CTS 后才会重新开始下一块 BS 的发送。
  • STmin:两个 CF 之间的最小时间间隔。可以填0表示以最大速率,帧之间背靠背发送。

注意:FC 帧不允许携带任何数据。

这两个流控参数以及流控状态码,共同构成了 iso-tp 的流控机制。以上4个 PDU 单元便是整个 iso-tp 的通信核心,必须熟悉掌握。其中 SF 负责单帧通信;FF + FC + CF 共同构建起了多帧传输。


通信寻址模式

iso-tp 寻址模式分为了 普通寻址模式 , 拓展寻址模式, 混合寻址模式,以及一个广播用途的 功能寻址模式。

各种寻址模式,本质上区别在于 N_AI(协议地址信息) 的布局模式。不同寻址模式下,部分地址会被映射到 CAN 报文中不同的位置。这里先来讲讲 N_AI 地址信息。

CAN 是端到端的通信,对于普通 CAN 报文,只要是经过了过滤器粗筛的报文,都会被控制器照单全收存进 FIFO 。而 CANTP 由于涉及多帧数据包的传输与重组,因此每次通信必须精准匹配一个连接,仅靠底层过滤器的粒度是远远达不到要求的(虽然过滤器能够设置列表模式精准匹配ID,但成本昂贵且数量少,在多设备网络中不合算)。因此协议栈必须定义一个自身的地址信息规则,用于双端两个通信连接之间的正确匹配。

The N_AI is used to identify the communicating peer entities of the network layer. The N_AI information received in the N_SDU (N_SA, N_TA, N_TAtype [and N_AE]) shall be copied and included in the N_PDU.

N_AI 内部包含了几个关键字段,分别是:

N_SA(Sender Address. 源地址/也即发送方地址),N_TA(Target Address. 目标地址),N_TAType(区分功能寻址),以及N_AE(地址拓展)。N_AI 就相当于是一块电子身份证,每一帧都需要携带 N_AI 信息以便协议栈正确识别。接下来从普通寻址模式开始简单讲讲各个模式。

普通寻址模式

普通寻址模式下,整个 N_AI 信息均被放置在了 CAN 报文的 ID 段。数据段完全留给 PCI 和实际应用数据,没有任何地址信息占用数据场,在 Classic CAN 场景下一个 SF 最多可带7字节负载数据。

在标准 ISO 15765-2:2016 中,并没有规定11位 ID 具体的 SA/TA 以及 TAType 的分配,只是强调了在 CAN 报文的11位 ID 段必须完整携带 N_AI 信息。具体的 N_AI 映射由上层应用或服务提供商自定义。

在我的应用中,通常将11位 ID 划分为:

bit 6~10 bit 1~5 bit0
N_SA N_TA N_TAType

也就是:SA / TA 各5位,总共最多可表示 32 个节点,0位用于表示这是一帧物理帧还是功能帧(广播)。如果你的节点数量较少且固定,那么使用普通寻址模式是最常见,最高效的寻址方式。


拓展寻址模式

拓展寻址模式与普通寻址模式,本质区别就是拓展寻址模式下,将 N_AI 拆散了。为了提升可用地址空间,标准将 N_AI 拆开,将其中的 N_TA 放到了数据段的第一个字节,数据段的 PCI 信息依次向后顺延一个字节。这也是为什么我前面说 SF 帧和 CF 帧的负载与寻址模式有关系,就是因为要考虑这个 N_TA 的存在与否。

拓展寻址模式下,我的布局是这样的:

ID bit 3~10 ID bit2 ID bit 0~1 data bytes1 8bit
N_SA(8bit) N_TAType(1bit) Reserved N_TA

这样来看,SA 和 TA 各占8位,目标 256 个,源 256 个,网络节点容量直接比起普通寻址,提升了8倍。当然,在这种模式下,所以帧负载都要减1用于 N_TA 的单独开销。


混合寻址模式

混合寻址模式综合了上述两者的特性:既在 ID 段中完整保留了 N_AI 信息,又在数据段中额外插入了 N_AE 作为地址拓展,共同组合而成的一个杂合模式。它的帧布局如下:

1
2
CAN ID [包含 N_SA + N_TA + N_TAtype]   ← 和普通寻址一样,完整的 N_AI 信息
Data [ N_AE ][ N_PCI ][ N_Data ... ] ← 数据场首字节塞的是 AE,而不是 TA

混合模式算是个比较偏门的模式,主要用于多网段之间的通信,在数据段多出来的这个 N_AE 就用于区分本地网络之外的子网。同时也达到了一个多网段中,地址全局且唯一的作用。

因为多个网段上,有很大概率会出现地址撞车的现象。例如A子网中有一台设备其地址为0x10,B子网也有一台设备地址为0x10。他们在本地网络中地址是没有重复的,但是一但涉及到跨网段通信,网关在转发时就会出现问题:两个都是0x10?那么究竟该转发给谁?

N_AE 正好解决了这个问题,N_AE 表示该帧报文要访问的是哪个子网中的 TA,不存在地址撞车问题(除非子网中仍有地址重复,不过这是典型的配置错误,与协议栈无关)。

这个模式一般使用较少,我也很少会用到该模式,此处只简单叙述下其工作模式与部分应用场景。


功能寻址模式

这里要强调下,不同于上述三种寻址模式,前三种解决的是 “地址信息(N_AI)该放在帧的哪个位置”(布局问题),而功能寻址解决的是 “这帧报文哪些小组能收”。

在功能寻址模式下,N_TA 指向的不再是某一个具体节点,而是一组节点(或全部节点),该模式布局与普通寻址模式是一致的,区别在于 N_TAType 被定义为了功能寻址。

举个例子:我主控设备发一帧 0x7DF(TA) 的报文表示需要知道目前都有谁上线,所有配置了该功能 ID 的设备协议栈内部都会处理该广播报文,并以物理寻址单帧回复自己的地址。值得注意的是:功能寻址只能用于 SF 单帧通信。这是显而易见的,如果通过功能寻址模式启动多帧传输,那么总线上所有接收到该 FF 的节点都可能尝试回复 FC 流控帧,造成总线冲突和堵塞。在自行实现协议栈时,对于该种非法情况,一定要尽早在源头处拦截掉,同时在接收处理逻辑中丢掉可能误发的功能 FF 报文。


以上便是 ISO-TP 的通信基础元与相关的通信模式。值得注意的是:ISO-TP 是支持29位拓展 CAN 报文形式的,不过由于应用过少本文便只叙述了最常用的11位标准 ID 模式,如需更多了解可查阅 <ISO 15765-2:2016> 标准协议。

CAN-TP (ISO15765-2)

什么是CANTP

CANTP 全称 Controller Area Network Transport Layer,也就是 CAN 传输层,是一种在CAN总线基础上实现的传输协议。Classic CAN 报文一帧最大也就携带8字节数据,就算是加强版的 CAN FD 一帧最大也只能携带64字节数据。因此若需要传输大数据,就需要一个可靠的传输协议,而 CANTP 也正是为此设计的。

CANTP 是一个统称,根据标准不同,有着不同的实现:例如 SAE J1939-21 标准(商用车/重型车辆领域),以及更通用的 ISO15765-2 ,本文的 CANTP 也是指基于 ISO 15765-2 标准的实现(也叫 iso-tp)。

为什么需要CANTP

  • CAN报文负载限制:标准CAN帧(CAN 2.0A/B)的数据场最多只能携带8字节。但在很多实际应用(如汽车诊断、ECU刷写、仪表显示大数据等)中,需要传输几十甚至几百字节的数据。必须有一个稳定可靠的协议,来指导将长数据分割成多个短帧发送,并在接收端正确重组。
  • 保证数据可靠传输:光能够把数据正确发出与重组还不够,通信双方还必须考虑一个合理的通信节奏。不能够太快(接收方处理不过来,缓冲区溢出),也不能够太慢(影响效率)。ISO-TP 引入了流控机制,管理发送和接收节奏,确保了多帧传输情况下的可靠性和稳定性。

CANTP 位于 CAN 底层之上,若以网络四层模型进行类比,CAN底层应属于数据链路层(第四层),涉及具体的物理收发操作;而CANTP 更类似于网络 IP 层与传输层的结合,负责数据的正确路由与可靠性;进而CANopen之类的应用层协议能够正确运作。

通信模式

ISO-TP 支持两种通信形式:单帧(Single Frame)通信与多帧(Consecutive Frame)通信。

单帧通信与 CAN 常规报文类似,根据寻址模式的不同,每一个单帧可以携带 6~7 字节负载数据。同时 ISO-TP 还在单帧通信中引入了功能寻址模式:它完全遵循单帧的负载规则,但是CAN ID有讲究,功能寻址通常使用特定的广播 ID,比如诊断报文中的 0x7DF。所有接收功能报文的节点都必须接收该帧,达到广播目的。

多帧通信,当该次通信所要传输的数据量大于该寻址模式下单帧的负载时,此时必须启动多帧传输。多帧传输分为两段:1. <= 4095 段;2. > 4095. 32位数据大小,最大可达 4Gb 。

关于 ISO-TP 详细的通信流程,将在其余文章中讲解。

应用场景

CANTP 常被应用于汽车领域,例如 UDS 诊断(读取 ECU 内部 RAM 或 Flash 中的大量数据,数据量可能达到几十甚至几百字节。),以及 ECU 的固件升级。在产线上,通过 OBD 口对车辆 ECU 进行初始固件烧录,这是 CAN TP 在汽车电子中非常普遍的应用场景。

在一些工业领域,CANTP 也会配合 CANopen 进行长数据的发送与接收,例如大数据量的 SDO 报文。

我在一个项目中也使用了 CANTP 作为传输层接收 OTA 升级固件,CANTP 不止可以用于汽车等领域,在常规开发中也很好用,协议核心也比较简单,完全可以自行实现一份轻量化的版本用于端到端的大数据通信。

关于 ISO-TP 就先介绍这么多,在后续文章中会详细阐述其通信流程与协议实现。

CANopen协议栈移植(CAN Festival)

什么是 CAN Festival

CAN Festival 就是一个用纯 C 编写的、开源的 CANopen 主/从站协议栈。由法国 LEC(嵌入式系统实验室)团队早期开发,后来完全开源,适用于在各大嵌入式平台上运行 CANopen 协议栈。其支持 CiA 301(通信行规)和 CiA 402(驱动行规),相比起CANopenNode / MicroCANopen 等主流协议栈,CAN Festival 的代码相对老旧,内存占用较大,且对 64 位数据(如某些位置编码器)的支持不如现代协议栈完善。

虽然新开源栈十分优秀,但是 CAN Festival 依然有一席之地。CAN Festival 的 OD 对象字典设计十分有意思,初见时会觉得很别扭,难以配置,但当熟络之后会发现 CAN Festival 的 OD 字典配置设计是很精妙的,而且全部是编译期静态分配的全局数组,没有动态内存分配,这也是它依然被很多老军工、老工业设备固件锁定的核心理由。

移植全景

本文主要阐述裸机环境下的 CAN Festival 协议栈(以下简称协议)移植,其移植的核心是协议开放出来的外部接口,协议内部的核心状态机无需改动,也不能够进行改动。

  • 硬件 CAN 接口:由于协议栈无法感知外部具体环境,因此具体的CAN报文收发等低层操作,需要用户根据自身移植环境进行提供。
    具体主要是实现报文发送接口 canSend() 以及报文接收接口 canDispatch()。以上是在使用 CAN ISR 的前提下我们需要对应实现的接口。同时协议也提供了 canReceive() 接口,若使用轮询的方式进行协议状态机推进,则需要实现该接口。
  • 定时器接口:CANopen 内部运转涉及到心跳报文以及超时检测,因此协议中也需要用户提供一个时基来维护协议栈內部状态的正常运转。以 STM32 为例,我们可以将基本定时器 TIM7 配置给协议栈用作时基,并实现 getElapsedTime() 以及 setTimer() 接口即可。
  • OD字典:对象字典是 CANopen 的灵魂所在,也是协议比较难以理解的地方。本文只阐述协议的移植步骤,因此会提供一份配置好的主/从机 OD 字典框架。关于字典的配置,在相关文章中再详细进行阐述。

协议移植

CAN 接口

首先应确保底层 CAN 被正确配置,这是协议运作的根基,很多时候移植好了却发不出去,抓不到波形,多半就是底层传输没跑通。

这里以 STM32F407VET6 为例子,配置 CAN1 作为本次移植的底层传输 CAN 控制外设。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
static CAN_HandleTypeDef hcan;
static CAN_FilterTypeDef hfilter;

void
HX_CAN_Init( void )
{
__HAL_RCC_CAN1_CLK_ENABLE();
CAN_GPIO_CLK_EN(); // do { __HAL_RCC_GPIOA_CLK_ENABLE(); } while(0)

GPIO_InitTypeDef can_gpio;
can_gpio.Alternate = GPIO_AF9_CAN1;
can_gpio.Mode = GPIO_MODE_AF_PP;
can_gpio.Pin = CAN_TX_PIN | CAN_RX_PIN;
can_gpio.Pull = GPIO_PULLUP;
can_gpio.Speed = GPIO_SPEED_FREQ_HIGH;

HAL_GPIO_Init( GPIOA, &can_gpio );

hcan.Instance = CAN1;
hcan.Init.AutoBusOff = DISABLE;
hcan.Init.AutoRetransmission = ENABLE;
hcan.Init.AutoWakeUp = DISABLE;

hcan.Init.Mode = CAN_MODE_LOOPBACK;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.ReceiveFifoLocked = DISABLE;

hcan.Init.Prescaler = 7;
hcan.Init.TimeSeg1 = CAN_BS1_9TQ;
hcan.Init.TimeSeg2 = CAN_BS2_2TQ;

hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.TransmitFifoPriority = ENABLE;

if ( HAL_CAN_Init( &hcan ) != HAL_OK )
{
printf( "CAN_Init Err.\n" );
while(1);
}

hfilter.FilterActivation = CAN_FILTER_ENABLE;
hfilter.FilterBank = 0;
hfilter.FilterFIFOAssignment = CAN_FILTER_FIFO0;

/* Only STD message can be received. */
hfilter.FilterIdHigh = 0;
hfilter.FilterIdLow = 0;
hfilter.FilterMaskIdHigh = 0;
hfilter.FilterMaskIdLow = 0x0004;

hfilter.FilterMode = CAN_FILTERMODE_IDMASK;
hfilter.FilterScale = CAN_FILTERSCALE_32BIT;
hfilter.SlaveStartFilterBank = 0;

if ( HAL_CAN_ConfigFilter( &hcan, &hfilter ) != HAL_OK )
{
printf( "Filter config err.\n" );
while(1);
}

if ( HAL_CAN_Start( &hcan ) != HAL_OK )
{
printf( "CAN start err.\n" );
while(1);
}

HAL_NVIC_EnableIRQ( CAN1_RX0_IRQn );
HAL_NVIC_SetPriority( CAN1_RX0_IRQn, 6, 0 );
HAL_CAN_ActivateNotification( &hcan, CAN_IT_RX_FIFO0_MSG_PENDING );
}

这里都是常规配置,注意一下即可。本次移植我们配置为 CAN1 500kbps / 32位掩码模式,只收标准报文(CANopen只走标准报文收发)。

接下来实现协议的报文出口,也就是发送接口 canSend()。接口原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
/** 
* @brief The CAN message structure
* @ingroup can
*/
typedef struct {
UNS16 cob_id; /**< message's ID */
UNS8 rtr; /**< remote transmission request. (0 if not rtr message, 1 if rtr message) */
UNS8 len; /**< message's length (0 to 8) */
UNS8 data[8]; /**< message's datas */
} Message;

UNS8 canSend( CAN_PORT notused, Message *m );

可以看到,当协议内部需要向外发送一帧报文时,会调用该接口,并将 m 传入,该结构内部包含了已经由协议内部组装好了的通信报文(例如 PDO/SDO 等),我们只需要使用 m 中所携带的参数,将报文发送到总线上即可。其参考实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
uint8_t 
canSend( CAN_PORT notused, Message *TxMessage )
{
if ( !TxMessage )
return 0;

CAN_TxHeaderTypeDef txHeader;
uint8_t i = 0;
uint8_t txBuf[8];
HAL_StatusTypeDef hReturn;
uint32_t mailbox;

txHeader.DLC = TxMessage->len;
txHeader.RTR = TxMessage->rtr;
txHeader.StdId = TxMessage->cob_id;
txHeader.IDE = CAN_ID_STD; // 注意:txHeader此处必须显示赋值为 CAN_ID_STD.

memcpy( txBuf, TxMessage->data, TxMessage->len );

do
{
hReturn = HAL_CAN_AddTxMessage( &hcan, &txHeader, txBuf, &mailbox );
if ( hReturn != HAL_OK )
{
i++;
DELAY(10);
}
} while( (hReturn != HAL_OK) && (i < 3) );

return (i >= 3) ? 0 : 1;
}

这里强调一下:txHeader 是被我们定义在局部栈空间中的,因此内部的值将是随机值(除非显示初始化为0),因此

txHeader.IDE = CAN_ID_STD;是必须的,不能够因为没用到就忽略!我在第一次移植时就没注意这个问题,导致波形抓出来全是拓展帧。

最后是中断处理,我们使用 CAN ISR,在 ISR 中收帧后直接喂给协议的状态机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void 
CAN1_RX0_IRQHandler( void )
{
HAL_CAN_IRQHandler( &hcan );
}


void
HAL_CAN_RxFifo0MsgPendingCallback( CAN_HandleTypeDef *hcan )
{
if ( hcan->Instance == CAN1 )
{
while( HAL_CAN_GetRxFifoFillLevel( hcan, CAN_RX_FIFO0 ) > 0 )
{
HAL_StatusTypeDef hReturn;
CAN_RxHeaderTypeDef rxHeader;
Message msg;
uint8_t rxData[8];

hReturn = HAL_CAN_GetRxMessage( hcan, CAN_RX_FIFO0, &rxHeader, rxData );
if ( hReturn != HAL_OK )
return;
msg.cob_id = rxHeader.StdId;
msg.len = rxHeader.DLC;
msg.rtr = rxHeader.RTR;
memcpy( msg.data, rxData, msg.len );

canDispatch( &ObjDict_Data, &msg ); // 将收到的帧喂给协议栈内部.
}
}
}

将收到的报文信息(COB-ID,dataLen,data等)通过 Message 类型打包后,连同具体数据一并通过 canDispatch() 派发给协议栈内部,推动协议栈内部状态机的运行。至此,CAN 部分协议所需的我们已经填充完毕。

定时器接口

我们使用基本定时器 TIM7 作为协议的时基来源,协议中要求时基配置为以 1 MHz 计数,也就是 1us。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void 
HX_TIM_Init( void )
{
__HAL_RCC_TIM7_CLK_ENABLE();

htim7.Instance = TIM7;
htim7.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
htim7.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim7.Init.CounterMode = TIM_COUNTERMODE_UP;
htim7.Init.Period = 65535; /* 占位,避免初始化后 1us 中断风暴 */
htim7.Init.Prescaler = 84 - 1;
htim7.Init.RepetitionCounter = 0;

if ( HAL_TIM_Base_Init( &htim7 ) != HAL_OK )
{
printf( "TIM init fail\n" );
while(1);
}

HAL_TIM_Base_Start_IT( &htim7 );
HAL_NVIC_EnableIRQ( TIM7_IRQn );
HAL_NVIC_SetPriority( TIM7_IRQn, 6, 0 );
}

这里我们 F4 主频配置到 168 Mhz,APB1 为 42 Mhz(4分频),TIM7 时钟为 42*2 = 84Mhz。PSC = 83 → 计数频率 = 84MHz / 84 = 1MHz,即 1 个计数 = 1us,满足协议栈要求。这里有一个细节问题:由于上电后协议栈内部第一次调用 setTimer() 就会将 ARR 改动并且正式接管 ARR 寄存器。因此在配置 Period 时我们可以将其设置很大,防止在上电到第一次 setTimer() 被调用期间频繁触发中断影响性能。

接下来,我们需要维护一个全局计数值,用于记录上一次的定时器设定值,供协议栈内部对超时等事件进行判断:

1
static TIMEVAL last_time_set = 0;

实现 setTimer() 用于协议内部重启定时,实现 getElapsedTime( void ) 用于协议内部获取当前的系统绝对时间。其原型与参考实现分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @ingroup timer
* @brief Set a timerfor a given time.
* @param value The time value.
*/
void setTimer( TIMEVAL value );

/**
* @ingroup timer
* @brief Get the time elapsed since latest timer occurence.
* @return time elapsed since latest timer occurence
*/
TIMEVAL getElapsedTime( void );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static TIM_HandleTypeDef htim7;

TIMEVAL
getElapsedTime( void )
{
/* 距上次中断经过的微秒 */
return htim7.Instance->CNT - last_time_set;
}

void
setTimer( TIMEVAL value )
{
/* value 微秒后触发 */
htim7.Instance->ARR = htim7.Instance->CNT + value;
}

接下来还需要在定时中断中驱动内部状态机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void 
TIM7_IRQHandler( void )
{
HAL_TIM_IRQHandler( &htim7 );
}

void
HAL_TIM_PeriodElapsedCallback( TIM_HandleTypeDef *htim )
{
if ( htim->Instance == TIM7 )
{
last_time_set = htim->Instance->CNT; /* 更新时间 */
TimeDispatch(); /* 驱动协议栈内部定时状态机. */
}
}

这里要注意的是:基本定时器 ARR 是16位的,也就是说单次定时最大只能到 65535us (合 65.5ms)。而在我的移植中,协议配置的
最大延时时长就是16位的,因此不会存在问题。

1
2
// The timer of the STM32 counts from 0000 to 0xFFFF
#define TIMEVAL_MAX 0xFFFF

若你的 TIMEVAL_MAX 超过了 16 位,则不能够使用基本定时器。应考虑 32 位变量软件计时,或使用通用定时器或高级定时器。

对象字典

此处给出主机 / 从机的 OD 字典配置。

主机配置(master):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#include "user_master_objdict.h"

//////////////////////////////////////SDO配置(客户端)///////////////////////////////////////////
/* 子索引个数 */
UNS8 ObjDict_highestSubIndex_obj1280 = 3;
/* 客户端->服务器用的COB-ID */
UNS32 ObjDict_obj1280_COB_ID_Client_to_Server_Transmit_SDO = 0x600 + 1; /* 1号节点 */
/* 服务器->客户端用的COB-ID */
UNS32 ObjDict_obj1280_COB_ID_Server_to_Client_Receive_SDO = 0x580 + 1; /* 1号节点 */
/* 对应服务器的节点号 */
UNS8 ObjDict_obj1280_Node_ID_of_the_SDO_Server = 0x1; /* 1号节点 */
subindex ObjDict_Index1280[] =
{
{RO, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1280},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1280_COB_ID_Client_to_Server_Transmit_SDO},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1280_COB_ID_Server_to_Client_Receive_SDO},
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1280_Node_ID_of_the_SDO_Server}
};
///////////////////////////////////接收PDO通讯参数配置(客户端)////////////////////////////////////////////
/* 子索引数 */
UNS8 ObjDict_highestSubIndex_obj1400 = 5;
/* 主站的接收PDO对应着从站的发送PDO,所以两边的COB-ID需要保持一致 */
UNS32 ObjDict_obj1400_COB_ID_used_by_PDO = 0x181; /* PDO1 1号节点 */
/* PDO类型,0表示同步非周期,1-240表示同步周期,252表示同步RTR,253表示异步RTR,254表示异步制造商特定事件、255表示异步设备子协议特定事件,主站不需要配置 */
UNS8 ObjDict_obj1400_Transmission_Type = 0x0;
/* 禁止时间,主站不需要配置 */
UNS16 ObjDict_obj1400_Inhibit_Time = 0x0; /* 0 单位10us */
/* 保留不使用 */
UNS8 ObjDict_obj1400_Compatibility_Entry = 0x0;
/* 从站作为事件定时周期,主站作为事件超时时间,这里实验用同步周期所以不进行配置 */
UNS16 ObjDict_obj1400_Event_Timer = 0x0;
subindex ObjDict_Index1400[] =
{
{RO, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1400},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1400_COB_ID_used_by_PDO},
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1400_Transmission_Type},
{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1400_Inhibit_Time},
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1400_Compatibility_Entry},
{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1400_Event_Timer}
};
/////////////////////////////////接收PDO映射参数配置(客户端)//////////////////////////////////////////
/* 子索引数 */
UNS8 ObjDict_highestSubIndex_obj1600 = 2;
/* bit0-7表示位数,bit8-15表示子索引,bit16-32表示索引 */
UNS32 ObjDict_obj1600[] =
{
0x20000108, /* 索引为2000,子索引为1,位数为8位 */
0x20000210, /* 索引为2000,子索引为2,位数为16位 */
};
subindex ObjDict_Index1600[] =
{
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1600},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1600[0]},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1600[1]},
};
////////////////////////被接收PDO映射的索引配置(客户端)////////////////////////////////////////
/* 子索引数 */
UNS8 ObjDict_highestSubIndex_obj2000 = 2;
/* 用于存储接收到的数据 */
UNS8 ControlWordAxis1 = 0;
/* 用于存储接收到的数据 */
UNS16 ControlWordAxis2 = 0;
subindex ObjDict_Index2000[] =
{
{RW, int8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj2000},
{RW, uint8, sizeof(UNS8), (void *)&ControlWordAxis1},
{RW, uint16, sizeof(UNS16), (void *)&ControlWordAxis2},
};
/* 第一个子索引接收到数据之后的回调函数 */
UNS32 ODCallback_t_Index2000_Subindex1(CO_Data *d, const indextable *index, UNS8 bSubindex)
{
printf("ControlWordAxis1 = %d\r\n", ControlWordAxis1);
return 0;
}
/* 第二个子索引接收到数据之后的回调函数 */
UNS32 ODCallback_t_Index2000_Subindex2(CO_Data *d, const indextable *index, UNS8 bSubindex)
{
printf("ControlWordAxis2 = %d\r\n", ControlWordAxis2);
return 0;
}
ODCallback_t ObjDict_Index2000_callbacks[] =
{
NULL,
ODCallback_t_Index2000_Subindex1,
ODCallback_t_Index2000_Subindex2,
};
/////////////////////////////////////////字典配置/////////////////////////////////////////////////////////
/* 字典的声明 */
const indextable ObjDict_objdict[] =
{
{NULL, 0, 0x0},
{(subindex *)ObjDict_Index1280, sizeof(ObjDict_Index1280) / sizeof(ObjDict_Index1280[0]), 0x1280},
{(subindex *)ObjDict_Index1400, sizeof(ObjDict_Index1400) / sizeof(ObjDict_Index1400[0]), 0x1400},
{(subindex *)ObjDict_Index1600, sizeof(ObjDict_Index1600) / sizeof(ObjDict_Index1600[0]), 0x1600},
{(subindex *)ObjDict_Index2000, sizeof(ObjDict_Index2000) / sizeof(ObjDict_Index2000[0]), 0x2000},
};

/* 通过索引查在字典中的下标 */
const indextable *ObjDict_scanIndexOD(UNS16 wIndex, UNS32 *errorCode, ODCallback_t **callbacks)
{
int i;
*callbacks = NULL;

switch(wIndex)
{
case 0x1280: i = 1; break;
case 0x1400: i = 2; break;
case 0x1600: i = 3; break;
case 0x2000: i = 4; *callbacks = ObjDict_Index2000_callbacks; break;
default:
*errorCode = OD_NO_SUCH_OBJECT;
return NULL;
}
*errorCode = OD_SUCCESSFUL;
return &ObjDict_objdict[i];
}

/* 在字典中的下标 */
const quick_index ObjDict_firstIndex = {
0, /* 第一个SDO服务器,主站不需要配置服务器 */
1, /* 第一个SDO客户端,有多少个从站就需要配置多少个客户端 */
2, /* 第一个PDO接收端通讯参数,这里配置1个接收端(最多配置4个) */
3, /* 第一个PDO接收端映射参数,这里配置1个接收端 */
0, /* 第一个PDO发送端通讯参数,主站不需要配置发送端 */
0 /* 第一个PDO发送端映射参数,主站不需要配置发送端 */
};

/* 在字典中的下标 */
const quick_index ObjDict_lastIndex = {
0, /* 最后一个SDO服务器,主站不需要配置服务器 */
1, /* 最后一个SDO客户端,有多少个从站就需要配置多少个客户端 */
2, /* 最后一个PDO接收端通讯参数,这里配置1个接收端(最多配置4个) */
3, /* 最后一个PDO接收端映射参数,这里配置1个接收端(最多配置4个) */
0, /* 最后一个PDO发送端通讯参数,主站不需要配置发送端 */
0 /* 最后一个PDO发送端映射参数,主站不需要配置发送端 */
};

/* 字典的大小 */
const UNS16 ObjDict_ObjdictSize = sizeof(ObjDict_objdict)/sizeof(ObjDict_objdict[0]);

/* 对各数据类型的范围进行检查合法性检查 */
UNS32 ObjDict_valueRangeTest(UNS8 typeValue, void *value)
{
switch(typeValue)
{
// case int8:
// if(*(INTEGER8 *)value != (INTEGER8)0) //
// return OD_VALUE_RANGE_EXCEEDED;
// break;

default:
break;
}
return 0;
}
///////////////////////////////////节点属性配置////////////////////////////////////////////////////////
/* ObjDict_iam_a_slave = 0表明该节点为主站 */
const UNS8 ObjDict_iam_a_slave = 0;
/* CanOpen规定主站节点号为0 */
UNS8 ObjDict_bDeviceNodeId = 0x00;
////////////////////////////////////心跳/节点保护报文配置//////////////////////////////////////////////
/* 心跳报文从站节点数,主站才需要配置,但是这里不使用心跳报文 */
UNS8 ObjDict_highestSubIndex_obj1016 = 0;
/* 心跳报文入口,其中存放了从站nod-id和倒计时时间,主站才需要配置,但是这里不使用心跳报文 */
UNS32 ObjDict_obj1016[1];
/* 心跳报文定时事件状态,主站才需要配置,但是这里不使用心跳报文 */
TIMER_HANDLE ObjDict_heartBeatTimers[1];
/* 从站上报心跳包间隔时间,从站才需要配置,但这里不使用心跳报文 */
UNS16 ObjDict_obj1017 = 0;
/* 节点保护报文,每60秒询问一次所有节点在线情况 */
UNS16 ObjDict_obj100C = 60000;
/* 节点保护报文,3次不回复则认为节点掉线 */
UNS8 ObjDict_obj100D = 3;

//////////////////////////////////////同步报文配置///////////////////////////////////////////////////////
/* bit31表示是否开启同步报文,低16位表示cob-id,主站才需要配置 */
UNS32 ObjDict_obj1005 = 0x40000080;
/* 同步报文发送事件间隔为60秒 */
UNS32 ObjDict_obj1006 = 60000000;

/////////////////////////////////紧急报文配置///////////////////////////////////////////////////////
/* 紧急报文,用于存放错误,主站不要配置 */
UNS32 ObjDict_obj1003[EMCY_MAX_ERRORS];
/* 错误个数,主站不需要配置 */
UNS8 ObjDict_highestSubIndex_obj1003 = 0;
/* 用于存放错误标志位,主站不需要配置 */
UNS8 ObjDict_obj1001 = 0x0;
/* 紧急报文cob-id,主站不需要配置,从站在setNodeId函数中也会初始化,所以也不用配置 */
UNS32 ObjDict_obj1014 = 0x80;

/////////////////////////////////PDO报文配置////////////////////////////////////////////////////
/* 主站只要配置接收PDO,所以不需要设置PDO状态 */
s_PDO_status ObjDict_PDO_status[1];

///////////////////////////////////字典定义//////////////////////////////////////////////
/* CANOPEN字典 */
CO_Data ObjDict_Data = CANOPEN_NODE_DATA_INITIALIZER(ObjDict);

从机配置(slave):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
#include "user_slave_objdict.h"

//////////////////////////////////////////////////SDO配置(服务器)/////////////////////////////////////
/* 子索引个数 */
UNS8 ObjDict_highestSubIndex_obj1200 = 3;
/* 客户端->服务器用的COB-ID */
UNS32 ObjDict_obj1200_COB_ID_Client_to_Server_Transmit_SDO = 0x600 + 1; /* 1号节点 */
/* 服务器->客户端用的COB-ID */
UNS32 ObjDict_obj1200_COB_ID_Server_to_Client_Receive_SDO = 0x580 + 1; /* 1号节点 */
/* 对应服务器的节点号 */
UNS8 ObjDict_obj1200_Node_ID_of_the_SDO_Server = 0x1; /* 1号节点 */
subindex ObjDict_Index1200[] =
{
{RO, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1200},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1200_COB_ID_Client_to_Server_Transmit_SDO},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1200_COB_ID_Server_to_Client_Receive_SDO},
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1200_Node_ID_of_the_SDO_Server}
};
////////////////////////////////////////////发送PDO通讯参数配置(客户端)//////////////////////////////////
/* 子索引数 */
UNS8 ObjDict_highestSubIndex_obj1800 = 5;
/* 从站的发送PDO对应着主站的接收PDO,所以两边的COB-ID需要保持一致 */
UNS32 ObjDict_obj1800_COB_ID_used_by_PDO = 0x181; /* PDO1 1号节点 */
/* PDO类型,0表示同步非周期,1-240表示同步周期,252表示同步RTR,253表示异步RTR,254表示异步制造商特定事件、255表示异步设备子协议特定事件,这里配置为同步一次 */
UNS8 ObjDict_obj1800_Transmission_Type = 0xFE;
/* 因为波特率为1MB/S,标准帧最长108位,所以禁止时间至少为108us,这里设置为1ms */
UNS16 ObjDict_obj1800_Inhibit_Time = 0x100; /* 0 单位10us */
/* 保留不使用 */
UNS8 ObjDict_obj1800_Compatibility_Entry = 0x0;
/* 从站作为事件定时周期,主站作为事件超时时间,这里实验用同步周期所以不进行配置 */
UNS16 ObjDict_obj1800_Event_Timer = 0x0;
subindex ObjDict_Index1800[] =
{
{RO, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1800},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1800_COB_ID_used_by_PDO},
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1800_Transmission_Type},
{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1800_Inhibit_Time},
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1800_Compatibility_Entry},
{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1800_Event_Timer}
};
ODCallback_t ObjDict_Index1800_callbacks[] =
{
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
};

/* ========== TPDO2 通讯参数 (0x1801) ========== */
/* 子索引数 */
UNS8 ObjDict_highestSubIndex_obj1801 = 5;
/* 从站的发送PDO对应着主站的接收PDO,所以两边的COB-ID需要保持一致 */
UNS32 ObjDict_obj1801_COB_ID_used_by_PDO = 0x281; /* PDO1 1号节点 */
/* PDO类型,0表示同步非周期,1-240表示同步周期,252表示同步RTR,253表示异步RTR,254表示异步制造商特定事件、255表示异步设备子协议特定事件,这里配置为同步一次 */
UNS8 ObjDict_obj1801_Transmission_Type = 0xFE;
/* 因为波特率为1MB/S,标准帧最长108位,所以禁止时间至少为108us,这里设置为1ms */
UNS16 ObjDict_obj1801_Inhibit_Time = 0x100; /* 0 单位10us */
/* 保留不使用 */
UNS8 ObjDict_obj1801_Compatibility_Entry = 0x0;
/* 从站作为事件定时周期,主站作为事件超时时间,这里实验用同步周期所以不进行配置 */
UNS16 ObjDict_obj1801_Event_Timer = 0x0;
subindex ObjDict_Index1801[] =
{
{RO, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1801},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1801_COB_ID_used_by_PDO},
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1801_Transmission_Type},
{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1801_Inhibit_Time},
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_obj1801_Compatibility_Entry},
{RW, uint16, sizeof(UNS16), (void *)&ObjDict_obj1801_Event_Timer}
};
ODCallback_t ObjDict_Index1801_callbacks[] =
{
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
};

////////////////////////////////////////////发送PDO映射参数配置(客户端)////////////////////////////////
/* 子索引数 */
UNS8 ObjDict_highestSubIndex_obj1A00 = 2;
/* bit0-7表示位数,bit8-15表示子索引,bit16-32表示索引 */
UNS32 ObjDict_obj1A00[] =
{
0x20000108, /* 索引为2000,子索引为1,位数为8位 */
0x20000210, /* 索引为2000,子索引为2,位数为16位 */
};
subindex ObjDict_Index1A00[] =
{
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1A00},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A00[0]},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A00[1]},
};


/* ========== TPDO2 映射参数 (0x1A01) ========== */
UNS8 ObjDict_highestSubIndex_obj1A01 = 2; /* 映射 2 个变量 */
UNS32 ObjDict_obj1A01[] =
{
0x20100110, /* 映射 0x2010-1 (16位) */
0x20100210 /* 映射 0x2010-2 (16位) */
};
subindex ObjDict_Index1A01[] =
{
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj1A01},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A01[0]},
{RW, uint32, sizeof(UNS32), (void *)&ObjDict_obj1A01[1]},
};

//////////////////////////////////////////被接收PDO映射的索引配置(客户端)///////////////////////////////////
/* 子索引数 */
UNS8 ObjDict_highestSubIndex_obj2000 = 2;
/* 用于存储将要发送的数据 */
UNS8 ControlWordAxis1 = 100;
/* 用于存储将要发送的数据 */
UNS16 ControlWordAxis2 = 1000;
subindex ObjDict_Index2000[] =
{
{RW, int8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj2000},
{RW, uint8, sizeof(UNS8), (void *)&ControlWordAxis1},
{RW, uint16, sizeof(UNS16), (void *)&ControlWordAxis2},
};

UNS8 ObjDict_highestSubIndex_obj2010 = 2;
UNS16 test_half_word1 = 1250;
UNS16 test_half_word2 = 1600;
subindex ObjDict_Index2010[] =
{
{RW, uint8, sizeof(UNS8), (void *)&ObjDict_highestSubIndex_obj2010},
{RW, uint16, sizeof(UNS16), (void *)&test_half_word1},
{RW, uint16, sizeof(UNS16), (void *)&test_half_word2},
};
///////////////////////////////////////////////字典配置//////////////////////////////////////////////////////
/* 字典的声明 */
const indextable ObjDict_objdict[] =
{
{NULL, 0, 0x0},
{(subindex *)ObjDict_Index1200, sizeof(ObjDict_Index1200) / sizeof(ObjDict_Index1200[0]), 0x1200},
{(subindex *)ObjDict_Index1800, sizeof(ObjDict_Index1800) / sizeof(ObjDict_Index1800[0]), 0x1800},
{(subindex *)ObjDict_Index1801, sizeof(ObjDict_Index1801) / sizeof(ObjDict_Index1801[0]), 0x1801},
{(subindex *)ObjDict_Index1A00, sizeof(ObjDict_Index1A00) / sizeof(ObjDict_Index1A00[0]), 0x1A00},
{(subindex *)ObjDict_Index1A01, sizeof(ObjDict_Index1A01) / sizeof(ObjDict_Index1A01[0]), 0x1A01},
{(subindex *)ObjDict_Index2000, sizeof(ObjDict_Index2000) / sizeof(ObjDict_Index2000[0]), 0x2000},
{(subindex *)ObjDict_Index2010, sizeof(ObjDict_Index2010) / sizeof(ObjDict_Index2010[0]), 0x2010},
};

/* 通过索引查在字典中的下标 */
const indextable *ObjDict_scanIndexOD(UNS16 wIndex, UNS32 *errorCode, ODCallback_t **callbacks)
{
int i;
*callbacks = NULL;

switch(wIndex)
{
case 0x1200: i = 1; break;
case 0x1800: i = 2; *callbacks = ObjDict_Index1800_callbacks; break;
case 0x1A00: i = 4; break;
case 0x1801: i = 3; *callbacks = ObjDict_Index1801_callbacks; break;
case 0x1A01: i = 5; break;
case 0x2000: i = 6; break;
case 0x2010: i = 7; break;
default:
*errorCode = OD_NO_SUCH_OBJECT;
return NULL;
}
*errorCode = OD_SUCCESSFUL;
return &ObjDict_objdict[i];
}

/* 在字典中的下标 */
const quick_index ObjDict_firstIndex = {
1, /* 第一个SDO服务器,每个从站配置一个服务器 */
0, /* 第一个SDO客户端,从站不需要配置客户端 */
0, /* 第一个PDO接收端通讯参数,从站不需要配置接收端 */
0, /* 第一个PDO接收端映射参数,从站不需要配置接收端 */
2, /* 第一个PDO发送端通讯参数,这里配置1个发送端(最多4个) */
4 /* 第一个PDO发送端映射参数,这里配置1个发送端(最多4个) */
};

/* 在字典中的下标 */
const quick_index ObjDict_lastIndex = {
1, /* 最后一个SDO服务器,每个从站配置一个服务器 */
0, /* 最后一个SDO客户端,从站不需要配置客户端 */
0, /* 最后一个PDO接收端通讯参数,从站不需要配置接收端 */
0, /* 最后一个PDO接收端映射参数,从站不需要配置接收端 */
3, /* 最后一个PDO发送端通讯参数,这里配置1个发送端(最多4个) */
5 /* 最后一个PDO发送端映射参数,这里配置1个发送端(最多4个) */
};

/* 字典的大小 */
const UNS16 ObjDict_ObjdictSize = sizeof(ObjDict_objdict)/sizeof(ObjDict_objdict[0]);

/* 对各数据类型的范围进行检查合法性检查 */
UNS32 ObjDict_valueRangeTest(UNS8 typeValue, void *value)
{
switch(typeValue)
{
// case int8:
// if(*(INTEGER8 *)value != (INTEGER8)0) //
// return OD_VALUE_RANGE_EXCEEDED;
// break;

default:
break;
}
return 0;
}
/////////////////////////////////////////////节点属性配置//////////////////////////////////////////
/* ObjDict_iam_a_slave = 1表明该节点为从站 */
const UNS8 ObjDict_iam_a_slave = 1;
/* 设置该从站节点号为1 */
UNS8 ObjDict_bDeviceNodeId = 0x01;
/////////////////////////////////////////////心跳/节点保护报文配置/////////////////////////////////////////////
/* 心跳报文从站节点数,主站才需要配置,但是这里不使用心跳报文 */
UNS8 ObjDict_highestSubIndex_obj1016 = 0;
/* 心跳报文入口,其中存放了从站nod-id和倒计时时间,主站才需要配置,但是这里不使用心跳报文 */
UNS32 ObjDict_obj1016[1];
/* 心跳报文定时事件状态,主站才需要配置,但是这里不使用心跳报文 */
TIMER_HANDLE ObjDict_heartBeatTimers[1];
/* 从站上报心跳包间隔时间,从站才需要配置,但这里不使用心跳报文 */
UNS16 ObjDict_obj1017 = 0;
/* 节点保护报文,每60秒询问一次所有节点在线情况 */
UNS16 ObjDict_obj100C = 60;
/* 节点保护报文,3次不回复则认为节点掉线 */
UNS8 ObjDict_obj100D = 3;

/////////////////////////////////////////////////同步报文配置/////////////////////////////////////////////////////
/* bit31表示是否开启同步报文,低16位表示cob-id,主站才需要配置 */
UNS32 ObjDict_obj1005 = 0;
/* 同步报文发送事件间隔时间,主站才需要配置 */
UNS32 ObjDict_obj1006 = 0;

/////////////////////////////////////////////////紧急报文配置/////////////////////////////////////////////////////
/* 紧急报文,用于存放错误信息 */
UNS32 ObjDict_obj1003[EMCY_MAX_ERRORS];
/* 错误个数 */
UNS8 ObjDict_highestSubIndex_obj1003 = 0;
/* 用于存放错误标志位,主站不需要配置 */
UNS8 ObjDict_obj1001 = 0x0;
/* 紧急报文cob-id,主站不需要配置,从站在setNodeId函数中也会初始化,所以也不用配置 */
UNS32 ObjDict_obj1014 = 0x80;

//////////////////////////////////////////////////PDO报文配置/////////////////////////////////////////////
/* 初始化PDO状态 */
s_PDO_status ObjDict_PDO_status[2] = {s_PDO_status_Initializer, s_PDO_status_Initializer};

/////////////////////////////////字典定义/////////////////////////////////////////////////////
/* CANOPEN字典 */
CO_Data ObjDict_Data = CANOPEN_NODE_DATA_INITIALIZER(ObjDict);

以上就是裸机移植 CAN Festival 的具体步骤,后续在主流程中,只需将两个 Init 初始化函数执行一遍将底层硬件正确配置,即可使用协议提供的 API 进行对应的控制。

移植过程中也要注意:整体 CAN 总线上的设备,波特率必须一致,同时根据使用需求,按需选择定时器的实现,注意定时器的位数。

音乐
0:000:00