由于在实习期间需要用到CAN协议进行通讯,那么就浅浅做一下笔记叭。使用芯片为STM32F429。
目录
0x01 物理层
正常我们的设备中,将片上的外设与主机通讯,正常是是使用IIC、SPI、USART等通讯协议,而CAN称为控制器局域网络,这种总线是一种可以在无主机情况下实现微处理器或者设备之间相互通信的总线标准。
CAN总线在发送数据时,并不会像SPI一样拉低片选信号或者像IIC一样发送设备地址,而是根据你的设备ID来进行区分,数据包会直接流通在总线上,设备会自己主动读取。
CAN是一种半双工的模式,它在一个时刻只可以收或者发,不可以同时进行,收与发都共用一个总线。与I2C、SPI等具有时钟信号的同步通讯方式不同,CAN通讯并不是以时钟信号来进行同步的,它是一种异步通讯,只具有CAN_High和CAN_Low两条信号线,共同构成一组差分信号线,以差分信号的形式进行通讯。那么为什么要用两根线?
CAN物理层的形式主要分为闭环总线及开环总线网络两种,一个适合于高速通讯,一个适合于远距离通讯。
(一)闭环总线网络
CAN闭环通讯网络是一种遵循ISO11898标准的高速、短距离网络,它的总线最大长度为40m,通信速度最高为1Mbps,总线的两端各要求有一个120欧的电阻。
(二)开环总线网络
CAN开环总线网络是遵循ISO11519-2标准的低速、远距离网络,它的最大传输距离为1km,最高通讯速率为125kbps,两根总线是独立的、不形成闭环,要求每根总线上各串联有一个2.2千欧的电阻。
(三)通讯节点
CAN总线上可以挂载多个通讯节点,节点之间的信号经过总线传输,实现节点间通讯。由于CAN通讯协议不对节点进行地址编码,而是对数据内容进行编码,所以网络中的节点个数理论上不受限制,只要总线的负载足够即可,可以通过中继器增强负载。
CAN通讯节点由一个CAN控制器及CAN收发器组成,控制器与收发器之间通过CAN_Tx及CAN_Rx信号线相连,收发器与CAN总线之间使用CAN_High及CAN_Low信号线相连。其中CAN_Tx及CAN_Rx使用普通的类似TTL逻辑信号,而CAN_High及CAN_Low是一对差分信号线,使用比较特别的差分信号。
当CAN节点需要发送数据时,控制器把要发送的二进制编码通过CAN_Tx线发送到收发器,然后由收发器把这个普通的逻辑电平信号转化成差分信号,通过差分线CAN_High和CAN_Low线输出到CAN总线网络。而通过收发器接收总线上的数据到控制器时,则是相反的过程,收发器把总线上收到的CAN_High及CAN_Low信号转化成普通的逻辑电平信号,通过CAN_Rx输出到控制器中。
(四)差分信号
差分信号又称差模信号,与传统使用单根信号线电压表示逻辑的方式有区别,使用差分信号传输时,需要两根信号线,这两个信号线的振幅相等,相位相反,通过两根信号线的电压差值来表示逻辑0和逻辑1。
为什么要这么表示呢,相比于单信号线,其使用差分信号传输具有下面的优点:
-
抗干扰能力强,当外界存在噪声干扰时,几乎会同时耦合到两条信号线上,而接收端只关心两个信号的差值,所以外界的共模噪声可以被完全抵消。
-
能有效抑制它对外部的电磁干扰,同样的道理,由于两根信号的极性相反,他们对外辐射的电磁场可以相互抵消,耦合的越紧密,泄放到外界的电磁能量越少。
-
时序定位精确,由于差分信号的开关变化是位于两个信号的交点,而不像普通单端信号依靠高低两个阈值电压判断,因而受工艺,温度的影响小,能降低时序上的误差,同时也更适合于低幅度信号的电路。
由于差分信号线具有这些优点,所以在USB协议、485协议、以太网协议及CAN协议的物理层中,都使用了差分信号传输。
那回来看看CAN协议的差分信号:
以高速CAN协议为例,当表示逻辑1时(隐性电平),CAN_High和CAN_Low线上的电压均为2.5v,即它们的电压差VH-VL=0V;而表示逻辑0时(显性电平),CAN_High的电平为3.5V,CAN_Low线的电平为1.5V,即它们的电压差为VH-VL=2V。
在CAN总线中,必须使它处于隐性电平(逻辑1)或显性电平(逻辑0)中的其中一个状态。假如有两个CAN通讯节点,在同一时间,一个输出隐性电平,另一个输出显性电平,类似I2C总线的线与特性将使它处于显性电平状态,显性电平的名字就是这样来的,即可以认为显性具有优先的意味。
0x02 协议层
CAN的协议层规定了通讯逻辑,实现了很强的抗干扰能力。
(一)CAN的波特率及位同步
由于CAN属于异步通讯,没有时钟信号线,连接在同一个总线网络中的各个节点会像串口异步通讯那样,节点间使用约定好的波特率进行通讯,特别地,CAN还会使用位同步的方式来抗干扰、吸收误差,实现对总线电平信号进行正确的采样,确保通讯正常。
(二)位时序分解
为了实现位同步,CAN协议把每一个数据位的时序分解成SS段、PTS段、PBS1段、PBS2段,这四段的长度加起来即为一个CAN数据位的长度。为什么要分成这么多段,到了下面就知道了,目的是为了更好的抗干扰,可以更好地同步还有吸收数据:
分解后最小的时间单位是Tq,而一个完整的位由8~25个Tq组成。
图中表示的是CAN通讯信号每一个数据位的长度为19Tq,其中SS段占1Tq,PTS段占6Tq,PBS1段占5Tq,PBS2段占7Tq。信号的采样点位于PBS1段与PBS2段之间,通过控制各段的长度,可以对采样点的位置进行偏移,以便准确地采样。可以对采样点进行位置的偏移,就是抗干扰的一个手段。
-
SS段(SYNC SEG)
SS译为同步段,若通讯节点检测到总线上信号的跳变沿被包含在SS段的范围之内,则表示节点与总线的时序是同步的,当节点与总线同步时,采样点采集到的总线电平即可被确定为该位的电平。SS段的大小固定为1Tq。
-
PTS段(PROP SEG)
PTS译为传播时间段,这个时间段是用于补偿网络的物理延时时间。是总线上输入比较器延时和输出驱动器延时总和的两倍。PTS段的大小可以为1~8Tq。
-
PBS1段(PHASE SEG1)
PBS1译为相位缓冲段,主要用来补偿边沿阶段的误差,它的时间长度在重新同步的时候可以加长。PBS1段的初始大小可以为1~8Tq。
-
PBS2段(PHASE SEG2)
PBS2这是另一个相位缓冲段,也是用来补偿边沿阶段误差的,它的时间长度在重新同步时可以缩短。PBS2段的初始大小可以为2~8Tq。
(三)通讯的波特率
总线上的各个通讯节点只要约定好1个Tq的时间长度以及每一个数据位占据多少个Tq,就可以确定CAN通讯的波特率。
例如,假设上图中的1Tq=1us,而每个数据位由19个Tq组成,则传输一位数据需要时间T1bit =19us,从而每秒可以传输的数据位个数为:1x10^6/19 = 52631.6 (bps)
这个每秒可传输的数据位的个数即为通讯中的波特率。
(四)CAN的报文种类及结构
当使用CAN协议进行通讯时,需要对数据、操作命令(如读/写)以及同步信号进行打包,打包后的这些内容称为报文。
报文的种类有如下:
帧 | 帧用途 |
---|---|
数据帧 | 用于节点向外传送数据 |
遥控帧 | 用于向远端节点请求数据 |
错误帧 | 用于向远端节点通知校验错误,请求重新发送上一个数据 |
过载帧 | 用于通知远端节点:本节点尚未做好接收准备 |
帧间隔 | 用于将数据帧及遥控帧与前面的帧分离开来 |
-
数据帧
在原始数据段的前面加上传输起始标签、片选(识别)标签和控制标签,在数据的尾段加上CRC校验标签、应答标签和传输结束标签,把这些内容按特定的格式打包好,就可以用一个通道表达各种信号,各种各样的标签就如同SPI中各种通道上的信号,起到了协同传输的作用。
当整个数据包被传输到其它设备时,只要这些设备按格式去解读,就能还原出原始数据,这样的报文就被称为CAN的“数据帧”。
数据帧以一个显性位(逻辑0)开始,以7个连续的隐性位(逻辑1)结束,在它们之间,分别有仲裁段、控制段、数据段、CRC段和ACK段。
帧起始:
SOF段(Start Of Frame),译为帧起始,帧起始信号只有一个数据位,是一个显性电平,它用于通知各个节点将有数据传输,其它节点通过帧起始信号的电平跳变沿来进行硬同步。
仲裁段:
当同时有两个报文被发送时,总线会根据仲裁段的内容决定哪个数据包能被传输,这也是它名称的由来。
仲裁段的内容主要为本数据帧的ID信息(标识符),数据帧具有标准格式和扩展格式两种,区别就在于ID信息的长度,标准格式的ID为11位,扩展格式的ID为29位,它在标准ID的基础上多出18位。在CAN协议中,ID起着重要的作用,它决定着数据帧发送的优先级,也决定着其它节点是否会接收这个数据帧。CAN协议不对挂载在它之上的节点分配优先级和地址,对总线的占有权是由信息的重要性决定的,即对于重要的信息,可给它打包上一个优先级高的ID,使它能够及时地发送出去。
那么具体怎么仲裁呢:
报文的优先级,是通过对ID的仲裁来确定的。根据前面对物理层的分析我们知道如果总线上同时出现显性电平和隐性电平,总线的状态会被置为显性电平,CAN正是利用这个特性进行仲裁。也就是线与特性,让其能够仲裁的条件。
若两个节点同时竞争CAN总线的占有权,当它们发送报文时,若首先出现隐性电平,则会失去对总线的占有权,进入接收状态。在开始阶段,两个设备发送的电平一样,所以它们一直继续发送数据。到了图中箭头所指的时序处,节点单元1发送的为隐性电平,而此时节点单元2发送的为显性电平,由于总线的“线与”特性使它表达出显示电平,因此单元2竞争总线成功,这个报文得以被继续发送出去。
仲裁段ID的优先级影响着接收设备对报文的反应。
因为在CAN总线上数据是以广播的形式发送的,所有连接在CAN总线的节点都会收到所有其它节点发出的有效数据,因而CAN控制器大多具有根据ID过滤报文的功能,它可以控制自己只接收某些ID的报文。
RTR位(Remote Transmission Request Bit),译作远程传输请求位,它是用于区分数据帧和遥控帧的,当它为显性电平时表示数据帧,隐性电平时表示遥控帧。
IDE位(Identifier Extension Bit),译作标识符扩展位,它是用于区分标准格式与扩展格式,当它为显性电平时表示标准格式,隐性电平时表示扩展格式。
SRR位(Substitute Remote Request Bit),只存在于扩展格式,它用于替代标准格式中的RTR位。由于扩展帧中的SRR位为隐性位,RTR在数据帧为显性位,所以在两个ID相同的标准格式报文与扩展格式报文中,标准格式的优先级较高。
控制段:
在控制段中的r1和r0为保留位,默认设置为显性位。它最主要的是DLC段(Data Length Code),译为数据长度码,它由4个数据位组成,用于表示本报文中的数据段含有多少个字节,DLC段表示的数字为0~8。
数据段:
数据段为数据帧的核心内容,它是节点要发送的原始信息,由0~8个字节组成,MSB先行。
CRC段:
为了保证报文的正确传输,CAN的报文包含了一段15位的CRC校验码,一旦接收节点算出的CRC码跟接收到的CRC码不同,则它会向发送节点反馈出错信息,利用错误帧请求它重新发送。CRC部分的计算一般由CAN控制器硬件完成,出错时的处理则由软件控制最大重发数。
在CRC校验码之后,有一个CRC界定符,它为隐性位,主要作用是把CRC校验码与后面的ACK段间隔起来。
ACK段:
ACK段包括一个ACK槽位,和ACK界定符位。类似I2C总线,在ACK槽位中,发送节点发送的是隐性位,而接收节点则在这一位中发送显性位以示应答。在ACK槽和帧结束之间由ACK界定符间隔开。
帧结束:
EOF段(End Of Frame),译为帧结束,帧结束段由发送节点发送的7个隐性位表示结束。
掌握了上面的那些报文,其实其他报文的原理也是一样的:
-
其他报文
(五)同步信号分析
同步过程可以分为硬同步以及重新同步:
根据对段的应用方式差异,CAN 的数据同步分为硬同步和重新同步。其中硬同步只是 当存在“帧起始信号”时起作用,无法确保后续一连串的位时序都是同步的,而重新同步方式可解决该问题,这两种方式具体介绍如下:
(1)硬同步
若某个 CAN 节点通过总线发送数据时,它会发送一个表示通讯起始的信号(即下一小 节介绍的帧起始信号),该信号是一个由高变低的下降沿。而挂载到 CAN 总线上的通讯节 点在不发送数时,会时刻检测总线上的信号。
当某个节点检测到总线的帧起始信号不在节点内部时序的SS段范围内,那么就可以判断为它自己内部的时序与总线不同步,因而这个状态的采样点采集得的数据是不正确的。所以节点以硬同步的方式调整,把自己的位时序中的SS段平移至总线出现下降沿的部分,获得同步,之后采样点就可以采集得正确的数据了。
(2)重新同步
前面的硬同步只是当存在帧起始信号时才起作用,如果在一帧很长的数据内,节点信号与总线信号相位有偏移时,这种同步方式也就无能为力了。因而需要引入重新同步方式,它利用普通数据位的高至低电平的跳变沿来同步(帧起始信号是特殊的跳变沿)。重新同步与硬同步方式相似的地方是它们都使用SS段来进行检测,同步的目的都是使节点内的SS段把跳变沿包含起来。
重新同步的方式分为超前和滞后两种情况,以总线跳变沿与SS段的相对位置进行区分。
超前:
滞后:
在重新同步时,PBS1和PBS2中增加或减少的这段时间长度被定义为“重新同步补偿带宽SJW”。一般来说 CAN 控制器会限定SJW 的最大 值,如限定了最大 SJW=3Tq 时,单次同步调整的时候不能增加或减少超过 3Tq 的时间长度,若有需要,控制器会通过多次小幅度调整来实现同步。当控制器设置的 SJW 极限值较大时,可以吸收的误差加大,但通讯的速度会下降。
(六)CAN中的优先级规则总结
数据帧和遥控帧的仲裁段用于多个节点竞争总线时进行仲裁,优先级高的帧获得在总线上发送的权利。优先级的确认总结为以下几条法则:
-
在总线空闲时,最先开始发送消息的节点获得发送权。
-
多个节点同时开始发送时,从仲裁段的第一位开始进行仲裁,第一次出现各节点的位电平互异时,输出显性电平的节点获得发送权。
-
相同ID和格式的数据帧和遥控帧,数据帧具有更高的优先级,因为数据帧的RTR位是显性电平,而遥控帧的RTR位是隐性电平。
-
对于11位标准ID相同的标准数据帧和扩展数据帧,标准数据帧具有更高的优先级,因为标准数据帧的IDE位是显性电平,而扩展数据帧的IDE是隐性电平。
0x03 STM32中CAN的使用
首先先看看数据手册中STM32的CAN外设简介:
STM32的芯片中具有bxCAN控制器 (Basic Extended CAN),它支持CAN协议2.0A和2.0B标准。
该CAN控制器支持最高的通讯速率为1Mb/s;可以自动地接收和发送CAN报文,支持使用标准ID和扩展ID的报文;外设中具有3个发送邮箱,发送报文的优先级可以使用软件控制,还可以记录发送的时间;具有2个3级深度的接收FIFO,可使用过滤功能只接收或不接收某些ID号的报文;可配置成自动重发;不支持使用DMA进行数据收发。
那么CAN的框架如下:
STM32的有两组CAN控制器,其中CAN1是主设备,框图中的“存储访问控制器”是由CAN1控制的,CAN2无法直接访问存储区域,所以使用CAN2的时候必须使能CAN1外设的时钟。
CAN由CAN控制内核(1)、CAN发送邮箱(2)、CAN接收FIFO(3)、验收筛选器(4)、整体控制逻辑(5)组成。
(一)CAN控制内核
框图中标号1处的CAN控制内核包含了各种控制寄存器及状态寄存器,其中的主控制寄存器CAN_MCR及位时序寄存器CAN_BTR。
主控制寄存器CAN_MCR负责管理CAN的工作模式,它使用以下寄存器位实现控制:
-
DBF调试冻结功能
DBF(Debug freeze)调试冻结,使用它可设置CAN处于工作状态或禁止收发的状态,禁止收发时仍可访问接收FIFO中的数据。这两种状态是当STM32芯片处于程序调试模式时才使用的,平时使用并不影响。
-
TTCM时间触发模式
TTCM(Time triggered communication mode)时间触发模式,它用于配置CAN的时间触发通信模式,在此模式下,CAN使用它内部定时器产生时间戳,并把它保存在CAN_RDTxR、CAN_TDTxR寄存器中。内部定时器在每个CAN位时间累加,在接收和发送的帧起始位被采样,并生成时间戳。利用它可以实现ISO 11898-4 CAN标准的分时同步通信功能。
-
ABOM自动离线管理
ABOM(Automatic bus-off management) 自动离线管理,它用于设置是否使用自动离线管理功能。当节点检测到它发送错误或接收错误超过一定值时,会自动进入离线状态,在离线状态中,CAN不能接收或发送报文。处于离线状态的时候,可以软件控制恢复或者直接使用这个自动离线管理功能,它会在适当的时候自动恢复。
-
AWUM自动唤醒
AWUM(Automatic bus-off management),自动唤醒功能,CAN外设可以使用软件进入低功耗的睡眠模式,如果使能了这个自动唤醒功能,当CAN检测到总线活动的时候,会自动唤醒。
-
NART自动重传
NART(No automatic retransmission)报文自动重传功能,设置这个功能后,当报文发送失败时会自动重传至成功为止。若不使用这个功能,无论发送结果如何,消息只发送一次。
-
RFLM锁定模式
RFLM(Receive FIFO locked mode)FIFO锁定模式,该功能用于锁定接收FIFO。锁定后,当接收FIFO溢出时,会丢弃下一个接收的报文。若不锁定,则下一个接收到的报文会覆盖原报文。
-
TXFP报文发送优先级的判定方法
TXFP(Transmit FIFO priority)报文发送优先级的判定方法,当CAN外设的发送邮箱中有多个待发送报文时,本功能可以控制它是根据报文的ID优先级还是报文存进邮箱的顺序来发送。
(二)CAN的工作模式
STM32的CAN提供了测试模式,配置位时序寄存器CAN_BTR的SILM及LBKM寄存器位可以控制使用正常模式、静默模式、回环模式及静默回环模式。
-
正常模式
正常模式下就是一个正常的CAN节点,可以向总线发送数据和接收数据。
-
静默模式
静默模式下,它自己的输出端的逻辑0数据会直接传输到它自己的输入端,逻辑1可以被发送到总线,所以它不能向总线发送显性位(逻辑0),只能发送隐性位(逻辑1)。输入端可以从总线接收内容。由于它只可发送的隐性位不会强制影响总线的状态,所以把它称为静默模式。这种模式一般用于监测,它可以用于分析总线上的流量,但又不会因为发送显性位而影响总线。
-
回环模式
回环模式下,它自己的输出端的所有内容都直接传输到自己的输入端,输出端的内容同时也会被传输到总线上,即也可使用总线监测它的发送内容。输入端只接收自己发送端的内容,不接收来自总线上的内容。使用回环模式可以进行自检。
-
回环静默模式
回环静默模式是以上两种模式的结合,自己的输出端的所有内容都直接传输到自己的输入端,并且不会向总线发送显性位影响总线,不能通过总线监测它的发送内容。输入端只接收自己发送端的内容,不接收来自总线上的内容。这种方式可以在“热自检”时使用,即自我检查的时候,不会干扰总线。
(三)位时序及波特率
STM32定义的位时序与前面讲的有些不一样,他只有三段,把一些段给合并了:
STM32的CAN外设位时序中只包含3段,分别是同步段SYNC_SEG、位段BS1及位段BS2,采样点位于BS1及BS2段的交界处。其中SYNC_SEG段固定长度为1Tq,而BS1及BS2段可以在位时序寄存器CAN_BTR设置它们的时间长度,它们可以在重新同步期间增长或缩短,该长度SJW也可在位时序寄存器中配置。
理解STM32的CAN外设的位时序时,可以把它的BS1段理解为是由CAN标准协议中PTS段与PBS1段合在一起的,而BS2段就相当于PBS2段。
之后就可以配置波特率了:
举个例子,配一个1Mbps的方式:
参数 | 说明 |
---|---|
SYNC_SE段 | 固定为1Tq |
BS1段 | 设置为5Tq (实际写入TS1[3:0]的值为4) |
BS2段 | 设置为3Tq (实际写入TS2[2:0]的值为2) |
TPCLK | APB1按默认配置为F=45MHz,TPCLK=1/45M |
CAN外设时钟分频 | 设置为5分频(实际写入BRP[9:0]的值为4) |
1Tq时间长度 | Tq = (BRP[9:0]+1) x TPCLK = 5 x 1/45M=1/9M |
1位的时间长度 | T1bit =1Tq+TS1+TS2 = 1+5+3 = 9Tq |
波特率 | BaudRate = 1/N Tq = 1/(1/9M x 9)=1Mbps |
(四)CAN发送邮箱
从图中可以得到CAN外设一共有三个发送邮箱,最多可以缓存三个待发送的报文。
每个发送邮箱中包含有标识符寄存器CAN_TIxR、数据长度控制寄存器CAN_TDTxR及2个数据寄存器CAN_TDLxR、CAN_TDHxR,它们的功能如下:
寄存器名 | 功能 |
---|---|
标识符寄存器CAN_TIxR | 存储待发送报文的ID、扩展ID、IDE位及RTR位 |
数据长度控制寄存器CAN_TDTxR | 存储待发送报文的DLC段 |
低位数据寄存器CAN_TDLxR | 存储待发送报文数据段的Data0-Data3这四个字节的内容 |
高位数据寄存器CAN_TDHxR | 存储待发送报文数据段的Data4-Data7这四个字节的内容 |
当要使用CAN外设发送报文时,把报文的各个段分解,按位置写入到这些寄存器中,并对标识符寄存器CAN_TIxR中的发送请求寄存器位TMIDxR_TXRQ置1,即可把数据发送出去。
其中标识符寄存器CAN_TIxR中的STDID寄存器位比较特别。CAN的标准标识符的总位数为11位,而扩展标识符的总位数为29位的。当报文使用扩展标识符的时候,标识符寄存器CAN_TIxR中的STDID[10:0]等效于EXTID[18:28]位,它与EXTID[17:0]共同组成完整的29位扩展标识符。注意的是,这个时候是从第18位开始了。
(五)CAN接收FIFO
当你有很多个报文的时候,我们需要一个缓存的地方来暂时保管我们的报文:
CAN外设一共有2个接收FIFO,每个FIFO中有3个邮箱,即最多可以缓存6个接收到的报文。当接收到报文时,FIFO的报文计数器会自增,而STM32内部读取FIFO数据之后,报文计数器会自减,通过状态寄存器可获知报文计数器的值,而通过前面主控制寄存器的RFLM位,可设置锁定模式,锁定模式下FIFO溢出时会丢弃新报文,非锁定模式下FIFO溢出时新报文会覆盖旧报文。
跟发送邮箱类似,每个接收FIFO中包含有标识符寄存器CAN_RIxR、数据长度控制寄存器CAN_RDTxR及2个数据寄存器CAN_RDLxR、CAN_RDHxR,其功能如下:
寄存器名 | 功能 |
---|---|
标识符寄存器CAN_RIxR | 存储收到报文的ID、扩展ID、IDE位及RTR位 |
数据长度控制寄存器CAN_RDTxR | 存储收到报文的DLC段 |
低位数据寄存器CAN_RDLxR | 存储收到报文数据段的Data0-Data3这四个字节的内容 |
高位数据寄存器CAN_RDHxR | 存储收到报文数据段的Data4-Data7这四个字节的内容 |
(六)验收筛选器
当我们收到了很多报文的时候,我们需要确定这个报文是不是发送给我们的,如果是的话,我们再让他进入我们的FIFO,我们需要去验证一下ID,这个时候就需要筛选器。
CAN外设的验收筛选器,一共有28个筛选器组,每个筛选器组有2个寄存器,CAN1和CAN2共用的筛选器的。
在 CAN 协议中,消息的标识符与节点地址无关,但与消息内容有关。因此,发送节点将报文广播给所有接收器时,接收节点会根据报文标识符的值来确定软件是否需要该消息,为了简化软件的工作,STM32的CAN外设接收报文前会先使用验收筛选器检查,只接收需要的报文到FIFO中。筛选器工作的时候,可以调整筛选ID的长度及过滤模式。
那么如何筛选?
根据筛选ID长度来分类有以下的两种形式:
-
检查 STDID[10:0]、 EXTID[17:0]、 IDE 和 RTR 位,一共31位。
-
检查STDID[10:0]、 RTR、 IDE 和 EXTID[17:15],一共16位。
而根据过滤的方法可以分为以下两种模式:
-
标识符列表模式,它把要接收报文的ID列成一个表,要求报文ID与列表中的某一个标识符完全相同才可以接收,可以理解为白名单管理。
-
掩码模式,它把可接收报文ID的某几位作为列表,这几位被称为掩码,可以把它理解成关键字搜索,只要掩码(关键字)相同,就符合要求,报文就会被保存到接收FIFO。
那么按照上面的筛选,我们可以得到四种筛选模式:
每组筛选器包含2个32位的寄存器,分别为CAN_FxR1和CAN_FxR2,它们用来存储要筛选的ID或掩码,各个寄存器位代表的意义与图中两个寄存器下面“映射”的一栏一致,各个模式的说明如下:
模式 | 说明 |
---|---|
32位掩码模式 | CAN_FxR1存储ID,CAN_FxR2存储哪个位必须要与CAN_FxR1中的ID一致,2个寄存器表示1组掩码。 |
32位标识符模式 | CAN_FxR1和CAN_FxR2各存储1个ID,2个寄存器表示2个筛选的ID |
16位掩码模式 | CAN_FxR1高16位存储ID,低16位存储哪个位必须要与高16位的ID一致; CAN_FxR2高16位存储ID,低16位存储哪个位必须要与高16位的ID一致 2个寄存器表示2组掩码。 |
16位标识符模式 | CAN_FxR1和CAN_FxR2各存储2个ID,2个寄存器表示4个筛选的ID |
那么对于有掩码的筛选器要如何使用,可以看看如下:
筛选器设置举例:
ID | 1 | 0 | 1 | 1 | 1 | 0 | 1 | … |
---|---|---|---|---|---|---|---|---|
掩码 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | … |
筛选的ID | 1 | 0 | 1 | x | x | 0 | x | … |
如在掩码模式时,第一个寄存器存储要筛选的ID,第二个寄存器存储掩码,掩码为1的部分表示该位必须与ID中的内容一致,筛选的结果为表中第三行的ID值,它是一组包含多个的ID值,其中x表示该位可以为1可以为0。
如工作在标识符模式时,2个寄存器存储的都是要筛选的ID,它只包含2个要筛选的ID值(32位模式时)。如果使能了筛选器,且报文的ID与所有筛选器的配置都不匹配,CAN外设会丢弃该报文,不存入接收FIFO。
0x04 代码
CAN初始化结构体:
/**
* @brief CAN 初始化结构体
*/
typedef struct {
uint16_t CAN_Prescaler; /*配置 CAN 外设的时钟分频,可设置为 1-1024*/
uint8_t CAN_Mode; /*配置 CAN 的工作模式,回环或正常模式*/
uint8_t CAN_SJW; /*配置 SJW 极限值 */
uint8_t CAN_BS1; /*配置 BS1 段长度*/
uint8_t CAN_BS2; /*配置 BS2 段长度 */
FunctionalState CAN_TTCM; /*是否使能 TTCM 时间触发功能*/
FunctionalState CAN_ABOM; /*是否使能 ABOM 自动离线管理功能*/
FunctionalState CAN_AWUM; /*是否使能 AWUM 自动唤醒功能 */
FunctionalState CAN_NART; /*是否使能 NART 自动重传功能*/
FunctionalState CAN_RFLM; /*是否使能 RFLM 锁定 FIFO 功能*/
FunctionalState CAN_TXFP; /*配置 TXFP 报文优先级的判定方法*/
} CAN_InitTypeDef;
其中的FunctionalState为:
typedef enum {DISABLE = 0, ENABLE = !DISABLE} FunctionalState;
配置完这些结构体成员后,调用库函数CAN_Init即可把这些参数写入到CAN控制寄存器中,实现CAN的初始化。
-
CAN_Prescaler
本成员设置CAN外设的时钟分频,它可控制时间片Tq的时间长度,这里设置的值最终会减1后再写入BRP寄存器位,即前面介绍的Tq计算公式:
Tq = (BRP[9:0]+1) x TPCLK
等效于:Tq = CAN_Prescaler x TPCLK
-
CAN_Mode
本成员设置CAN的工作模式,可设置为正常模式(CAN_Mode_Normal)、回环模式(CAN_Mode_LoopBack)、静默模式(CAN_Mode_Silent)以及回环静默模式(CAN_Mode_Silent_LoopBack)。
-
CAN_SJW
本成员可以配置SJW的极限长度,即CAN重新同步时单次可增加或缩短的最大长度,它可以被配置为1-4Tq(CAN_SJW_1/2/3/4tq)。
-
CAN_BS1
本成员用于设置CAN位时序中的BS1段的长度,它可以被配置为1-16个Tq长度(CAN_BS1_1/2/3…16tq)。
-
CAN_BS2
本成员用于设置CAN位时序中的BS2段的长度,它可以被配置为1-8个Tq长度(CAN_BS2_1/2/3…8tq)。
SYNC_SEG、BS1段及BS2段的长度加起来即一个数据位的长度,即前面介绍的原来计算公式:
T1bit =1Tq+TS1+TS2 =1+ (TS1[3:0] + 1)+ (TS2[2:0] + 1)
等效于:T1bit = 1Tq+CAN_BS1+CAN_BS2
-
CAN_TTCM
本成员用于设置是否使用时间触发功能(ENABLE/DISABLE),时间触发功能在某些CAN标准中会使用到。
-
CAN_ABOM
本成员用于设置是否使用自动离线管理(ENABLE/DISABLE),使用自动离线管理可以在节点出错离线后适时自动恢复,不需要软件干预。
-
CAN_ AWUM
本成员用于设置是否使用自动唤醒功能(ENABLE/DISABLE),使能自动唤醒功能后它会在监测到总线活动后自动唤醒。
-
CAN_NART
本成员用于设置是否使用自动重传功能(ENABLE/DISABLE),使用自动重传功能时,会一直发送报文直到成功为止。
-
CAN_RFLM
本成员用于设置是否使用锁定接收FIFO(ENABLE/DISABLE),锁定接收FIFO后,若FIFO溢出时会丢弃新数据,否则在FIFO溢出时以新数据覆盖旧数据。
-
CAN_TXFP
本成员用于设置发送报文的优先级判定方法(ENABLE/DISABLE),使能时,以报文存入发送邮箱的先后顺序来发送,否则按照报文ID的优先级来发送。
CAN发送与接收结构体:
在发送或接收报文时,需要往发送邮箱中写入报文信息或从接收 FIFO 中读取报文信息:
/**
* @brief CAN Tx message structure definition
* 发送结构体
*/
typedef struct {
uint32_t StdId; /*存储报文的标准标识符 11 位,0-0x7FF. */
uint32_t ExtId; /*存储报文的扩展标识符 29 位,0-0x1FFFFFFF. */
uint8_t IDE; /*存储 IDE 扩展标志 */
uint8_t RTR; /*存储 RTR 远程帧标志*/
uint8_t DLC; /*存储报文数据段的长度,0-8 */
uint8_t Data[8]; /*存储报文数据段的内容 */
}CanTxMsg;
/**
* @brief CAN Rx message structure definition
* 接收结构体
*/
typedef struct {
uint32_t StdId; /*存储了报文的标准标识符 11 位,0-0x7FF. */
uint32_t ExtId; /*存储了报文的扩展标识符 29 位,0-0x1FFFFFFF. */
uint8_t IDE; /*存储了 IDE 扩展标志 */
uint8_t RTR; /*存储了 RTR 远程帧标志*/
uint8_t DLC; /*存储了报文数据段的长度,0-8 */
uint8_t Data[8]; /*存储了报文数据段的内容 */
uint8_t FMI; /*存储了 本报文是由经过筛选器存储进 FIFO 的,0-0xFF */
}CanRxMsg;
-
StdId
本成员存储的是报文的11位标准标识符,范围是0-0x7FF。
-
ExtId
本成员存储的是报文的29位扩展标识符,范围是0-0x1FFFFFFF。ExtId与StdId这两个成员根据下面的IDE位配置,只有一个是有效的。
-
IDE
本成员存储的是扩展标志IDE位,当它的值为宏CAN_ID_STD时表示本报文是标准帧,使用StdId成员存储报文ID;当它的值为宏CAN_ID_EXT时表示本报文是扩展帧,使用ExtId成员存储报文ID。
-
RTR
本成员存储的是报文类型标志RTR位,当它的值为宏CAN_RTR_Data时表示本报文是数据帧;当它的值为宏CAN_RTR_Remote时表示本报文是遥控帧,由于遥控帧没有数据段,所以当报文是遥控帧时,下面的Data[8]成员的内容是无效的。
-
DLC
本成员存储的是数据帧数据段的长度,它的值的范围是0-8,当报文是遥控帧时DLC值为0。
-
Data[8]
本成员存储的就是数据帧中数据段的数据。
-
FMI
本成员只存在于接收结构体,它存储了筛选器的编号,表示本报文是经过哪个筛选器存储进接收FIFO的,可以用它简化软件处理。
当需要使用CAN发送报文时,先定义一个上面发送类型的结构体,然后把报文的内容按成员赋值到该结构体中,最后调用库函数CAN_Transmit把这些内容写入到发送邮箱即可把报文发送出去。
接收报文时,通过检测标志位获知接收FIFO的状态,若收到报文,可调用库函数CAN_Receive把接收FIFO中的内容读取到预先定义的接收类型结构体中,然后再访问该结构体即可利用报文。
CAN筛选器结构体
/**
* @brief CAN filter init structure definition
* CAN 筛选器结构体
*/
typedef struct {
uint16_t CAN_FilterIdHigh; /*CAN_FxR1 寄存器的高 16 位 */
uint16_t CAN_FilterIdLow; /*CAN_FxR1 寄存器的低 16 位*/
uint16_t CAN_FilterMaskIdHigh; /*CAN_FxR2 寄存器的高 16 位*/
uint16_t CAN_FilterMaskIdLow; /*CAN_FxR2 寄存器的低 16 位 */
uint16_t CAN_FilterFIFOAssignment; /*设置经过筛选后数据存储到哪个接收 FIFO
*/
uint8_t CAN_FilterNumber; /*筛选器编号,范围 0-27*/
uint8_t CAN_FilterMode; /*筛选器模式 */
uint8_t CAN_FilterScale; /*设置筛选器的尺度 */
FunctionalState CAN_FilterActivation; /*是否使能本筛选器*/
} CAN_FilterInitTypeDef;
-
CAN_FilterIdHigh
CAN_FilterIdHigh成员用于存储要筛选的ID,若筛选器工作在32位模式,它存储的是所筛选ID的高16位;若筛选器工作在16位模式,它存储的就是一个完整的要筛选的ID。
-
CAN_FilterIdLow
类似地,CAN_FilterIdLow成员也是用于存储要筛选的ID,若筛选器工作在32位模式,它存储的是所筛选ID的低16位;若筛选器工作在16位模式,它存储的就是一个完整的要筛选的ID。
-
CAN_FilterMaskIdHigh
CAN_FilterMaskIdHigh存储的内容分两种情况,当筛选器工作在标识符列表模式时,它的功能与CAN_FilterIdHigh相同,都是存储要筛选的ID;而当筛选器工作在掩码模式时,它存储的是CAN_FilterIdHigh成员对应的掩码,与CAN_FilterIdLow组成一组筛选器。
-
CAN_FilterMaskIdLow
类似地,CAN_FilterMaskIdLow存储的内容也分两种情况,当筛选器工作在标识符列表模式时,它的功能与CAN_FilterIdLow相同,都是存储要筛选的ID;而当筛选器工作在掩码模式时,它存储的是CAN_FilterIdLow成员对应的掩码,与CAN_FilterIdLow组成一组筛选器。
-
CAN_FilterFIFOAssignment
本成员用于设置当报文通过筛选器的匹配后,该报文会被存储到哪一个接收FIFO,它的可选值为FIFO0或FIFO1(宏CAN_Filter_FIFO0/1)。
-
CAN_FilterNumber
本成员用于设置筛选器的编号,即本过滤器结构体配置的是哪一组筛选器,CAN一共有28个筛选器,所以它的可输入参数范围为0-27。
-
CAN_FilterMode
本成员用于设置筛选器的工作模式,可以设置为列表模式(宏CAN_FilterMode_IdList)及掩码模式(宏CAN_FilterMode_IdMask)。
-
CAN_FilterScale
本成员用于设置筛选器的尺度,可以设置为32位长(宏CAN_FilterScale_32bit)及16位长(宏CAN_FilterScale_16bit)。
-
CAN_FilterActivation
本成员用于设置是否激活这个筛选器(宏ENABLE/DISABLE)。
可能对上面的那些列表模式,掩码模式看起来会有点头晕,可以看看下面这个表格:
模式 | CAN_FilterIdHigh | CAN_FilterIdLow | CAN_FilterMaskIdHigh | CAN_FilterMaskIdLow |
---|---|---|---|---|
32位列表模式 | ID1的高16位 | ID1的低16位 | ID2的高16位 | ID2的低16位 |
16位列表模式 | ID1的完整数值 | ID2的完整数值 | ID3的完整数值 | ID4的完整数值 |
32位掩码模式 | ID1的高16位 | ID1的低16位 | ID1掩码的高16位 | ID1掩码的低16位 |
16位掩码模式 | ID1的完整数值 | ID2的完整数值 | ID1掩码的完整数值 | ID2掩码完整数值 |
那么总结下来,要使用CAN,我们需要下面这四个结构体:
-
初始化结构体:CAN_InitTypeDef
-
发送及接收结构体:CanTxMsg及CanRxMsg
-
筛选器结构体:CAN_FilterInitTypeDef
上面是CAN的外设芯片,相当于是一个收发器。那么当两个板子相连时,可以如下接线:
首先是对结构体进行初始化,按照下面这张图配置CAN总线:
先初始化GPIO:
static void CAN_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* Enable GPIO clock */
RCC_AHB1PeriphClockCmd(CAN_TX_GPIO_CLK|CAN_RX_GPIO_CLK, ENABLE);
/* Connect CAN pins to AF9 */
GPIO_PinAFConfig(CAN_TX_GPIO_PORT, CAN_RX_SOURCE, CAN_AF_PORT);
GPIO_PinAFConfig(CAN_RX_GPIO_PORT, CAN_TX_SOURCE, CAN_AF_PORT);
/* Configure CAN TX pins */
GPIO_InitStructure.GPIO_Pin = CAN_TX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(CAN_TX_GPIO_PORT, &GPIO_InitStructure);
/* Configure CAN RX pins */
GPIO_InitStructure.GPIO_Pin = CAN_RX_PIN ;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_Init(CAN_RX_GPIO_PORT, &GPIO_InitStructure);
}
之后初始化一下CAN工作模式:
static void CAN_Mode_Config(void)
{
CAN_InitTypeDef CAN_InitStructure;
/* Enable CAN clock */
RCC_APB1PeriphClockCmd(CAN_CLK, ENABLE);
/*CAN寄存器初始化*/
CAN_DeInit(CAN1);
CAN_StructInit(&CAN_InitStructure);
/*CAN单元初始化*/
CAN_InitStructure.CAN_TTCM=DISABLE; //MCR-TTCM 关闭时间触发通信模式使能
CAN_InitStructure.CAN_ABOM=ENABLE; //MCR-ABOM 自动离线管理
CAN_InitStructure.CAN_AWUM=ENABLE; //MCR-AWUM 使用自动唤醒模式
CAN_InitStructure.CAN_NART=DISABLE; //MCR-NART 禁止报文自动重传 DISABLE-自动重传
CAN_InitStructure.CAN_RFLM=DISABLE; //MCR-RFLM 接收FIFO 锁定模式 DISABLE-溢出时新报文会覆盖原有报文
CAN_InitStructure.CAN_TXFP=DISABLE; //MCR-TXFP 发送FIFO优先级 DISABLE-优先级取决于报文标示符
CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack; //回环工作模式
CAN_InitStructure.CAN_SJW=CAN_SJW_2tq; //BTR-SJW 重新同步跳跃宽度 2个时间单元
/* ss=1 bs1=5 bs2=3 位时间宽度为(1+5+3) 波特率即为时钟周期tq*(1+3+6) */
CAN_InitStructure.CAN_BS1=CAN_BS1_5tq; //BTR-TS1 时间段1 占用了6个时间单元
CAN_InitStructure.CAN_BS2=CAN_BS2_3tq; //BTR-TS1 时间段2 占用了3个时间单元
/* CAN Baudrate = 1 MBps (1MBps已为stm32的CAN最高速率) (CAN 时钟频率为 APB 1 = 45 MHz) */
CAN_InitStructure.CAN_Prescaler =5; BTR-BRP 波特率分频器 定义了时间单元的时间长度 45/(1+5+3)/5=1 Mbps
CAN_Init(CANx, &CAN_InitStructure);
}
之后初始化CAN的接收中断通道:
static void CAN_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* Configure one bit for preemption priority */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
/*中断设置*/
NVIC_InitStructure.NVIC_IRQChannel = CAN_RX_IRQ; //CAN1 RX0中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级为0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
那么在以上就实现了CAN的所有初始化了。
第二步我们就需要配置一下筛选器,在STM32上有两个FIFO:
static void CAN_Filter_Config(void)
{
CAN_FilterInitTypeDef CAN_FilterInitStructure;
/*CAN筛选器初始化*/
CAN_FilterInitStructure.CAN_FilterNumber=0; //筛选器组0
CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask; //工作在掩码模式
CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit; //筛选器位宽为单个32位。
/* 使能筛选器,按照标志的内容进行比对筛选,扩展ID不是如下的就抛弃掉,是的话,会存入FIFO0。 */
//先左移三位,得到一个32位的数据
//之后取出高十六位 右移16位放置
//在Can.h中有几个宏定义是拿来区分是否远程帧:CAN_RTR_Data 、 CAN_RTR_Remote
//在Can.h中有几个宏定义是拿来区分是否为标准帧:CAN_ID_STD、CAN_ID_RXT(扩展)
CAN_FilterInitStructure.CAN_FilterIdHigh= ((((u32)0x1314<<3)|CAN_ID_EXT|CAN_RTR_DATA)&0xFFFF0000)>>16; //要筛选的ID高位
CAN_FilterInitStructure.CAN_FilterIdLow= (((u32)0x1314<<3)|CAN_ID_EXT|CAN_RTR_DATA)&0xFFFF; //要筛选的ID低位
CAN_FilterInitStructure.CAN_FilterMaskIdHigh= 0xFFFF; //筛选器高16位每位必须匹配
CAN_FilterInitStructure.C AN_FilterMaskIdLow= 0xFFFF; //筛选器低16位每位必须匹配
CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0 ; //筛选器被关联到FIFO0
CAN_FilterInitStructure.CAN_FilterActivation=ENABLE; //使能筛选器
CAN_FilterInit(&CAN_FilterInitStructure);
/*CAN通信中断使能*/
CAN_ITConfig(CANx, CAN_IT_FMP0, ENABLE);
}
接下来配置发送以及接收,可以使用按键操作,当按下按键后,发送一个报文,要指定接收的设备ID还有上面结构体所要求的所有信息:
void CAN_SetMsg(CanTxMsg *TxMessage)
{
uint8_t ubCounter = 0;
TxMessage->ExtId=0x1314; //使用的扩展ID
TxMessage->IDE=CAN_ID_EXT; //扩展模式
TxMessage->RTR=CAN_RTR_DATA; //发送的是数据
TxMessage->DLC=8; //数据长度为8字节
/*设置要发送的数据0-7*/
for (ubCounter = 0; ubCounter < 8; ubCounter++)
{
TxMessage->Data[ubCounter] = ubCounter;
}
}
if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON)
{
LED_BLUE;
CAN_SetMsg(&TxMessage);
CAN_Transmit(CANx, &TxMessage);
can_delay(10000);//等待发送完毕,可使用CAN_TransmitStatus查看状态
LED_GREEN;
printf("\r\n已使用CAN发送数据包!\r\n");
printf("\r\n发送的报文内容为:\r\n");
printf("\r\n 扩展ID号ExtId:0x%x \r\n",TxMessage.ExtId);
CAN_DEBUG_ARRAY(TxMessage.Data,8);
}
那么也把接收的结构体也初始化了:
void Init_RxMes(CanRxMsg *RxMessage)
{
uint8_t ubCounter = 0;
/*把接收结构体清零*/
RxMessage->StdId = 0x00;
RxMessage->ExtId = 0x00;
RxMessage->IDE = CAN_ID_STD;
RxMessage->DLC = 0;
RxMessage->FMI = 0;
for (ubCounter = 0; ubCounter < 8; ubCounter++)
{
RxMessage->Data[ubCounter] = 0x00;
}
}
接下来是编写中断服务函数,需要注意的是这个函数名一定要跟入口函数对应。使用一个flag来判断是否接收到数据。
void CAN_RX_IRQHandler(void)
{
/*从邮箱中读出报文*/
CAN_Receive(CANx, CAN_FIFO0, &RxMessage);
/* 比较ID是否为0x1314 */
if((RxMessage.ExtId==0x1314) && (RxMessage.IDE==CAN_ID_EXT) && (RxMessage.DLC==8) )
{
flag = 1; //接收成功
}
else
{
flag = 0; //接收失败
}
}
之后在主程序中就可以看到接收的数据了:
if(flag==1)
{
LED_GREEN;
printf("\r\nCAN接收到数据:\r\n");
CAN_DEBUG_ARRAY(RxMessage.Data,8);
flag=0;
}
上面是一个单片机实现的回环检测的编写,使用的是掩码格式。使用STM32的标准库实现起来还是没有那么难的,主要是要指定好设备ID,以及配置四个很重要的结构体,之后在中断中判断ID然后接收就可以了。这种方式不会通过CAN的收发器,不会去干扰到CAN总线。
那么如何在两个设备间通讯呢?
首先把上面的CAN_Mode_Config函数中的CAN_Mode改为:CAN_Mode_Normal就可以了。之后按照原理图,使用线进行连接,两个板子需要配置一样的波特率,CAN_L一起连接,CAN_H一起连接,两个板子可以下载一样的程序,一个板子按下按键后,另外一个板子就可以打印出它收到的消息了。
最近的事情实在是太多啦,又是实习,又是需要准备秋招的东西T T,所以很多类似于像算法一样的博客就没有时间继续更新了,但是还是会好好抓紧时间的,最近也收到了地平线寄过来的板子,旭日派X3,这几天计划着先在上面做好开发叭!学习的效率真的大不如前了,快点振作起来!!