42. CAN—通讯实验

本章参考资料:《STM32参考手册》、《STM32F103CDE增强型系列数据手册》、库帮助文档《stm32f10x_stdperiph_lib_um.chm》。

若对CAN通讯协议不了解,可先阅读《CAN总线入门》、《CAN-bus规范》文档内容学习。

关于实验板上的CAN收发器可查阅《TJA1050》文档了解。

42.1. CAN协议简介

CAN是控制器局域网络(Controller Area Network)的简称,它是由研发和生产汽车电子产品著称的德国BOSCH公司开发的,并最终成为国际标准(ISO11519),是国际上应用最广泛的现场总线之一。

CAN总线协议已经成为汽车计算机控制系统和嵌入式工业控制局域网的标准总线,并且拥有以CAN为底层协议专为大型货车和重工机械车辆设计的J1939协议。近年来, 它具有的高可靠性和良好的错误检测能力受到重视,被广泛应用于汽车计算机控制系统和环境温度恶劣、电磁辐射强及振动大的工业环境。

42.1.1. CAN物理层

与I2C、SPI等具有时钟信号的同步通讯方式不同,CAN通讯并不是以时钟信号来进行同步的,它是一种异步通讯,只具有CAN_High和CAN_Low两条信号线, 共同构成一组差分信号线,以差分信号的形式进行通讯。

42.1.1.1. 闭环总线网络

CAN物理层的形式主要有两种,图 CAN闭环总线通讯网络 中的CAN通讯网络是一种遵循ISO11898标准的高速、 短距离“闭环网络”,它的总线最大长度为40m,通信速度最高为1Mbps,总线的两端各要求有一个“120欧”的电阻。

CAN闭环总线通讯网络

42.1.1.2. 开环总线网络

CAN开环总线通讯网络 中的是遵循ISO11519-2标准的低速、远距离“开环网络”,它的最大传输距离为1km, 最高通讯速率为125kbps,两根总线是独立的、不形成闭环,要求每根总线上各串联有一个“2.2千欧”的电阻。

CAN开环总线通讯网络

42.1.1.3. 通讯节点

从CAN通讯网络图可了解到,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输出到控制器中。

例如,STM32的CAN片上外设就是通讯节点中的控制器,为了构成完整的节点,还要给它外接一个收发器,在我们实验板中使用型号为TJA1050的芯片作为CAN收发器。 CAN控制器与CAN收发器的关系如同TTL串口与MAX3232电平转换芯片的关系,MAX3232芯片把TTL电平的串口信号转换成RS-232电平的串口信号, CAN收发器的作用则是把CAN控制器的TTL电平信号转换成差分信号(或者相反)。

42.1.1.4. 差分信号

差分信号又称差模信号,与传统使用单根信号线电压表示逻辑的方式有区别,使用差分信号传输时,需要两根信号线,这两个信号线的振幅相等, 相位相反,通过两根信号线的电压差值来表示逻辑0和逻辑1。见图 差分信号 ,它使用了V+与V-信号的差值表达出了图下方的信号。

差分信号

相对于单信号线传输的方式,使用差分信号传输具有如下优点:

  • 抗干扰能力强,当外界存在噪声干扰时,几乎会同时耦合到两条信号线上,而接收端只关心两个信号的差值,所以外界的共模噪声可以被完全抵消。

  • 能有效抑制它对外部的电磁干扰,同样的道理,由于两根信号的极性相反,他们对外辐射的电磁场可以相互抵消,耦合的越紧密,泄放到外界的电磁能量越少。

  • 时序定位精确,由于差分信号的开关变化是位于两个信号的交点,而不像普通单端信号依靠高低两个阈值电压判断,因而受工艺,温度的影响小, 能降低时序上的误差,同时也更适合于低幅度信号的电路。

由于差分信号线具有这些优点,所以在USB协议、485协议、以太网协议及CAN协议的物理层中,都使用了差分信号传输。

42.1.1.5. CAN协议中的差分信号

CAN协议中对它使用的CAN_High及CAN_Low表示的差分信号做了规定,见表 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收发器从CAN_Tx线接收到来自CAN控制器的低电平信号时(逻辑0), 它会使CAN_High输出3.5V,同时CAN_Low输出1.5V,从而输出显性电平表示逻辑0。

CAN协议标准表示的信号逻辑 CAN的差分信号高速

在CAN总线中,必须使它处于隐性电平(逻辑1)或显性电平(逻辑0)中的其中一个状态。假如有两个CAN通讯节点,在同一时间,一个输出隐性电平, 另一个输出显性电平,类似I2C总线的“线与”特性将使它处于显性电平状态,显性电平的名字就是这样来的,即可以认为显性具有优先的意味。

由于CAN总线协议的物理层只有1对差分线,在一个时刻只能表示一个信号,所以对通讯节点来说,CAN通讯是半双工的,收发数据需要分时进行。 在CAN的通讯网络中,因为共用总线,在整个网络中同一时刻只能有一个通讯节点发送信号,其余的节点在该时刻都只能接收。

42.1.2. 协议层

以上是CAN的物理层标准,约定了电气特性,以下介绍的协议层则规定了通讯逻辑。

42.1.2.1. CAN的波特率及位同步

由于CAN属于异步通讯,没有时钟信号线,连接在同一个总线网络中的各个节点会像串口异步通讯那样,节点间使用约定好的波特率进行通讯, 特别地,CAN还会使用“位同步”的方式来抗干扰、吸收误差,实现对总线电平信号进行正确的采样,确保通讯正常。

位时序分解

为了实现位同步,CAN协议把每一个数据位的时序分解成如图 CAN位时序分解图 所示的SS段、 PTS段、PBS1段、PBS2段,这四段的长度加起来即为一个CAN数据位的长度。分解后最小的时间单位是Tq,而一个完整的位由8~25个Tq组成。 为方便表示,图 CAN位时序分解图 中的高低电平直接代表信号逻辑0或逻辑1(不是差分信号)。

CAN位时序分解图

该图中表示的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,从而每秒可以传输的数据位个数为:

1x106­/19 = 52631.6 (bps)

这个每秒可传输的数据位的个数即为通讯中的波特率。

同步过程分析

波特率只是约定了每个数据位的长度,数据同步还涉及到相位的细节,这个时候就需要用到数据位内的SS、PTS、PBS1及PBS2段了。

根据对段的应用方式差异,CAN的数据同步分为硬同步和重新同步。其中硬同步只是当存在“帧起始信号”时起作用,无法确保后续一连串的位时序都是同步的, 而重新同步方式可解决该问题,这两种方式具体介绍如下:

(1) 硬同步

若某个CAN节点通过总线发送数据时,它会发送一个表示通讯起始的信号(即下一小节介绍的帧起始信号),该信号是一个由高变低的下降沿。 而挂载到CAN总线上的通讯节点在不发送数据时,会时刻检测总线上的信号。

见图 硬同步过程图 ,可以看到当总线出现帧起始信号时, 某节点检测到总线的帧起始信号不在节点内部时序的SS段范围,所以判断它自己的内部时序与总线不同步,因而这个状态的采样点采集得的数据是不正确的。 所以节点以硬同步的方式调整,把自己的位时序中的SS段平移至总线出现下降沿的部分,获得同步,同步后采样点就可以采集得正确数据了。

硬同步过程图

(2) 重新同步

前面的硬同步只是当存在帧起始信号时才起作用,如果在一帧很长的数据内,节点信号与总线信号相位有偏移时,这种同步方式就无能为力了。 因而需要引入重新同步方式,它利用普通数据位的高至低电平的跳变沿来同步(帧起始信号是特殊的跳变沿)。重新同步与硬同步方式相似的地方是它们都使用SS段来进行检测,同步的目的都是使 节点内的SS段把跳变沿包含起来。

重新同步的方式分为超前和滞后两种情况,以总线跳变沿与SS段的相对位置进行区分。第一种相位超前的情况如图 相位超前时的重新同步 , 节点从总线的边沿跳变中,检测到它内部的时序比总线的时序相对超前2Tq,这时控制器在下一个位时序中的PBS1段增加2Tq的时间长度,使得节点与总线时序重新同步。

相位超前时的重新同步

第二种相位滞后的情况如图 相位滞后时的重新同步 ,节点从总线的边沿跳变中, 检测到它的时序比总线的时序相对滞后2Tq,这时控制器在前一个位时序中的PBS2段减少2Tq的时间长度,获得同步。

相位滞后时的重新同步

在重新同步的时候, PBS1和PBS2中增加或减少的这段时间长度被定义为“重新同步补偿宽度SJW (reSynchronization Jump Width)”。 一般来说CAN控制器会限定SJW的最大值,如限定了最大SJW=3Tq时,单次同步调整的时候不能增加或减少超过3Tq的时间长度,若有需要, 控制器会通过多次小幅度调整来实现同步。当控制器设置的SJW极限值较大时,可以吸收的误差加大,但通讯的速度会下降。

42.1.2.2. CAN的报文种类及结构

在SPI通讯中,片选、时钟信号、数据输入及数据输出这4个信号都有单独的信号线,I2C协议包含有时钟信号及数据信号2条信号线,异步串口包含接收与发送2条信号线, 这些协议包含的信号都比CAN协议要丰富,它们能轻易进行数据同步或区分数据传输方向。而CAN使用的是两条差分信号线,只能表达一个信号, 简洁的物理层决定了CAN必然要配上一套更复杂的协议,如何用一个信号通道实现同样、甚至更强大的功能呢?CAN协议给出的解决方案是对数据、 操作命令(如读/写)以及同步信号进行打包,打包后的这些内容称为报文。

报文的种类

在原始数据段的前面加上传输起始标签、片选(识别)标签和控制标签,在数据的尾段加上CRC校验标签、应答标签和传输结束标签,把这些内容按特定的格式打包好, 就可以用一个通道表达各种信号了,各种各样的标签就如同SPI中各种通道上的信号,起到了协同传输的作用。当整个数据包被传输到其它设备时, 只要这些设备按格式去解读,就能还原出原始数据,这样的报文就被称为CAN的“数据帧”。

为了更有效地控制通讯,CAN一共规定了5种类型的帧, 它们的类型及用途说明如表 帧的种类及其用途

帧的种类及其用途

数据帧的结构

数据帧是在CAN通讯中最主要、最复杂的报文,我们来了解它的结构,见图 数据帧的结构

数据帧的结构

数据帧以一个显性位(逻辑0)开始,以7个连续的隐性位(逻辑1)结束,在它们之间,分别有仲裁段、控制段、数据段、CRC段和ACK段。

  • 帧起始

SOF段(Start Of Frame),译为帧起始,帧起始信号只有一个数据位,是一个显性电平, 它用于通知各个节点将有数据传输,其它节点通过帧起始信号的电平跳变沿来进行硬同步。

  • 仲裁段

当同时有两个报文被发送时,总线会根据仲裁段的内容决定哪个数据包能被传输,这也是它名称的由来。

仲裁段的内容主要为本数据帧的ID信息(标识符),数据帧具有标准格式和扩展格式两种,区别就在于ID信息的长度,标准格式的ID为11位,扩展格式的ID为29位, 它在标准ID的基础上多出18位。在CAN协议中,ID起着重要的作用,它决定着数据帧发送的优先级,也决定着其它节点是否会接收这个数据帧。 CAN协议不对挂载在它之上的节点分配优先级和地址,对总线的占有权是由信息的重要性决定的,即对于重要的信息,我们会给它打包上一个优先级高的ID, 使它能够及时地发送出去。也正因为它这样的优先级分配原则,使得CAN的扩展性大大加强,在总线上增加或减少节点并不影响其它设备。

报文的优先级,是通过对ID的仲裁来确定的。根据前面对物理层的分析我们知道如果总线上同时出现显性电平和隐性电平,总线的状态会被置为显性电平,CAN正是利用这个特性进行仲裁。

若两个节点同时竞争CAN总线的占有权,当它们发送报文时,若首先出现隐性电平,则会失去对总线的占有权,进入接收状态。 见图 仲裁过程 ,在开始阶段,两个设备发送的电平一样, 所以它们一直继续发送数据。到了图中箭头所指的时序处,节点单元1发送的为隐性电平,而此时节点单元2发送的为显性电平, 由于总线的“线与”特性使它表达出显示电平,因此单元2竞争总线成功,这个报文得以被继续发送出去。

仲裁过程

仲裁段ID的优先级也影响着接收设备对报文的反应。因为在CAN总线上数据是以广播的形式发送的,所有连接在CAN总线的节点都会收到所有其它节点发出的有效数据, 因而我们的CAN控制器大多具有根据ID过滤报文的功能,它可以控制自己只接收某些ID的报文。

回看图 数据帧的结构 中的数据帧格式,可看到仲裁段除了报文ID外,还有RTR、IDE和SRR位。

(1) RTR位(Remote Transmission Request Bit),译作远程传输请求位, 它是用于区分数据帧和遥控帧的,当它为显性电平时表示数据帧,隐性电平时表示遥控帧。

(2) IDE位(Identifier Extension Bit),译作标识符扩展位, 它是用于区分标准格式与扩展格式,当它为显性电平时表示标准格式,隐性电平时表示扩展格式。

(3) 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报文结构,不再展开讲解,其主要内容见图 各种CAN报文的结构

各种CAN报文的结构

42.2. STM32的CAN外设简介

STM32的芯片中具有bxCAN控制器 (Basic Extended CAN),它支持CAN协议2.0A和2.0B标准。

该CAN控制器支持最高的通讯速率为1Mb/s;可以自动地接收和发送CAN报文,支持使用标准ID和扩展ID的报文;外设中具有3个发送邮箱, 发送报文的优先级可以使用软件控制,还可以记录发送的时间;具有2个3级深度的接收FIFO,可使用过滤功能只接收或不接收某些ID号的报文; 可配置成自动重发;不支持使用DMA进行数据收发。

42.2.1. STM32的CAN架构剖析

STM32的CAN外设架构图

STM32的CAN外设架构图 是 STM32F105/107系列互联型芯片的CAN外设架构图, 图里具有2组CAN控制器,其中CAN1是主设备,框图中的“存储访问控制器”是由CAN1控制的,CAN2无法直接访问存储区域,所以使用CAN2的时候必须使能CAN1外设的时钟。 框图中主要包含CAN控制内核、发送邮箱、接收FIFO以及验收筛选器。我们实验板中使用的STM32F103系列芯片跟上述框图类似,但该系列只包含1组CAN控制器,即它们不包含图中标号的部分。

下面对框图中的各个部分进行介绍。

42.2.1.1. CAN控制内核

框图中标号处的CAN控制内核包含了各种控制寄存器及状态寄存器,我们主要讲解其中的主控制寄存器CAN_MCR及位时序寄存器CAN_BTR。

主控制寄存器CAN_MCR

主控制寄存器CAN_MCR负责管理CAN的工作模式,它使用以下寄存器位实现控制。

(1) DBF调试冻结功能

DBF(Debug freeze)调试冻结,使用它可设置CAN处于工作状态或禁止收发的状态,禁止收发时仍可访问接收FIFO中的数据。 这两种状态是当STM32芯片处于程序调试模式时才使用的,平时使用并不影响。

(2) TTCM时间触发模式

TTCM(Time triggered communication mode)时间触发模式,它用于配置CAN的时间触发通信模式,在此模式下, CAN使用它内部定时器产生时间戳,并把它保存在CAN_RDTxR、CAN_TDTxR寄存器中。内部定时器在每个CAN位时间累加, 在接收和发送的帧起始位被采样,并生成时间戳。利用它可以实现ISO 11898-4CAN标准的分时同步通信功能。

(3) ABOM自动离线管理

ABOM(Automatic bus-off management)自动离线管理,它用于设置是否使用自动离线管理功能。 当节点检测到它发送错误或接收错误超过一定值时, 会自动进入离线状态,在离线状态中,CAN不能接收或发送报文。处于离线状态的时候,可以软件控制恢复或者直接使用这个自动离线管理功能,它会在适当的时候自动恢复。

(4) AWUM自动唤醒

AWUM(Automatic bus-off management),自动唤醒功能,CAN外设可以使用软件进入低功耗的睡眠模式, 如果使能了这个自动唤醒功能,当CAN检测到总线活动的时候,会自动唤醒。

(5) NART自动重传

NART(No automatic retransmission)报文自动重传功能,设置这个功能后,当报文发送失败时会自动重传至成功为止。 若不使用这个功能,无论发送结果如何,消息只发送一次。

(6) RFLM锁定模式

RFLM(Receive FIFO locked mode)FIFO锁定模式,该功能用于锁定接收FIFO。锁定后,当接收FIFO溢出时, 会丢弃下一个接收的报文。若不锁定,则下一个接收到的报文会覆盖原报文。

(7) TXFP报文发送优先级的判定方法

TXFP(Transmit FIFO priority)报文发送优先级的判定方法,当CAN外设的发送邮箱中有多个待发送报文时, 本功能可以控制它是根据报文的ID优先级还是报文存进邮箱的顺序来发送。

位时序寄存器(CAN_BTR)及波特率

CAN外设中的位时序寄存器CAN_BTR用于配置测试模式、波特率以及各种位内的段参数。

(1) 测试模式

为方便调试,STM32的CAN提供了测试模式,配置位时序寄存器CAN_BTR的SILM及LBKM寄存器位可以控制使用正常模式、静默模式、 回环模式及静默回环模式,见图 四种工作模式

四种工作模式

各个工作模式介绍如下:

  • 正常模式

正常模式下就是一个正常的CAN节点,可以向总线发送数据和接收数据。

  • 静默模式

静默模式下,它自己的输出端的逻辑0数据会直接传输到它自己的输入端,逻辑1可以被发送到总线,所以它不能向总线发送显性位(逻辑0), 只能发送隐性位(逻辑1)。输入端可以从总线接收内容。由于它只可发送的隐性位不会强制影响总线的状态,所以把它称为静默模式。这种模式一般用于监测,它可以用于分析总线上的流量,但 又不会因为发送显性位而影响总线。

  • 回环模式

回环模式下,它自己的输出端的所有内容都直接传输到自己的输入端,输出端的内容同时也会被传输到总线上,即也可使用总线监测它的发送内容。 输入端只接收自己发送端的内容,不接收来自总线上的内容。使用回环模式可以进行自检。

  • 回环静默模式

回环静默模式是以上两种模式的结合,自己的输出端的所有内容都直接传输到自己的输入端,并且不会向总线发送显性位影响总线, 不能通过总线监测它的发送内容。输入端只接收自己发送端的内容,不接收来自总线上的内容。这种方式可以在“热自检”时使用,即自我检查的时候,不会干扰总线。

以上说的各个模式,是不需要修改硬件接线的,例如,当输出直接连输入时,它是在STM32芯片内部连接的,传输路径不经过STM32的CAN_Tx/Rx引脚, 更不经过外部连接的CAN收发器,只有输出数据到总线或从总线接收的情况下才会经过CAN_Tx/Rx引脚和收发器。

(2) 位时序及波特率

STM32外设定义的位时序与我们前面解释的CAN标准时序有一点区别,见图 STM32中CAN的位时序

STM32中CAN的位时序

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段。

了解位时序后,我们就可以配置波特率了。通过配置位时序寄存器CAN_BTR的TS1[3:0]及TS2[2:0]寄存器位设定BS1及BS2段的长度后,我们就可以确定每个CAN数据位的时间:

BS1段时间:

TS1=Tq x (TS1[3:0] + 1),

BS2段时间:

TS2= Tq x (TS2[2:0] + 1),

一个数据位的时间:

T1bit =1Tq+TS1+TS2 =1+ (TS1[3:0] + 1)+ (TS2[2:0] + 1)= N Tq

其中单个时间片的长度Tq与CAN外设的所挂载的时钟总线及分频器配置有关,CAN1和CAN2外设都是挂载在APB1总线上的, 而位时序寄存器CAN_BTR中的BRP[9:0]寄存器位可以设置CAN外设时钟的分频值 ,所以:

Tq = (BRP[9:0]+1) x TPCLK

其中的PCLK指APB1时钟,默认值为36MHz。

最终可以计算出CAN通讯的波特率:

BaudRate = 1/N Tq

例如表 一种配置波特率为1Mbps的方式 说明了一种把波特率配置为1Mbps的方式。

一种配置波特率为1Mbps的方式

42.2.1.2. CAN发送邮箱

回到图 25‑5中的CAN外设框图,在标号处的是CAN外设的发送邮箱,它一共有3个发送邮箱,即最多可以缓存3个待发送的报文。

每个发送邮箱中包含有标识符寄存器CAN_TIxR、 数据长度控制寄存器CAN_TDTxR及2个数据寄存器CAN_TDLxR、CAN_TDHxR, 它们的功能见表 发送邮箱的寄存器

发送邮箱的寄存器

当我们要使用CAN外设发送报文时,把报文的各个段分解,按位置写入到这些寄存器中,并对标识符寄存器CAN_TIxR中的发送请求寄存器位TMIDxR_TXRQ置1,即可把数据发送出去。

其中标识符寄存器CAN_TIxR中的STDID寄存器位比较特别。我们知道CAN的标准标识符的总位数为11位,而扩展标识符的总位数为29位的。 当报文使用扩展标识符的时候,标识符寄存器CAN_TIxR中的STDID[10:0]等效于EXTID[18:28]位,它与EXTID[17:0]共同组成完整的29位扩展标识符。

42.2.1.3. CAN接收FIFO

STM32的CAN外设架构图 中的CAN外设框图,在标号处的是CAN外设的接收FIFO, 它一共有2个接收FIFO,每个FIFO中有3个邮箱,即最多可以缓存6个接收到的报文。当接收到报文时,FIFO的报文计数器会自增,而STM32内部读取FIFO数据之后, 报文计数器会自减,我们通过状态寄存器可获知报文计数器的值,而通过前面主控制寄存器的RFLM位,可设置锁定模式,锁定模式下FIFO溢出时会丢弃新报文, 非锁定模式下FIFO溢出时新报文会覆盖旧报文。

跟发送邮箱类似,每个接收FIFO中包含有标识符寄存器CAN_RIxR、数据长度控制寄存器CAN_RDTxR及2个数据寄存器CAN_RDLxR、CAN_RDHxR, 它们的功能见 表发送邮箱的寄存器

表发送邮箱的寄存器

通过中断或状态寄存器知道接收FIFO有数据后,我们再读取这些寄存器的值即可把接收到的报文加载到STM32的内存中。

42.2.1.4. 验收筛选器

STM32的CAN外设架构图 中的CAN外设框图,在标号处的是CAN外设的验收筛选器, 一共有28个筛选器组,每个筛选器组有2个寄存器,CAN1和CAN2共用的筛选器的。其中STM32F103系列芯片仅有14个筛选器组:0-13号。

在 CAN 协议中,消息的标识符与节点地址无关,但与消息内容有关。因此,发送节点将报文广播给所有接收器时, 接收节点会根据报文标识符的值来确定软件是否需要该消息, 为了简化软件的工作,STM32的CAN外设接收报文前会先使用验收筛选器检查,只接收需要的报文到FIFO中。

筛选器工作的时候,可以调整筛选ID的长度及过滤模式。根据筛选ID长度来分类有有以下两种:

(1) 检查 STDID[10:0]、 EXTID[17:0]、 IDE 和 RTR 位, 一共31位。

(2) 检查STDID[10:0]、 RTR、 IDE 和 EXTID[17:15], 一共16位。

通过配置筛选尺度寄存器CAN_FS1R的FSCx位可以设置筛选器工作在哪个尺度。

而根据过滤的方法分为以下两种模式:

(1) 标识符列表模式,它把要接收报文的ID列成一个表, 要求报文ID与列表中的某一个标识符完全相同才可以接收,可以理解为白名单管理。

(2) 掩码模式,它把可接收报文ID的某几位作为列表,这几位被称为掩码, 可以把它理解成关键字搜索,只要掩码(关键字)相同,就符合要求,报文就会被保存到接收FIFO。

通过配置筛选模式寄存器CAN_FM1R的FBMx位可以设置筛选器工作在哪个模式。

不同的尺度和不同的过滤方法可使筛选器工作在图 筛选器的4种工作状态 的4种状态。

筛选器的4种工作状态

每组筛选器包含2个32位的寄存器,分别为CAN_FxR1和CAN_FxR2,它们用来存储要筛选的ID或掩码, 各个寄存器位代表的意义与图中两个寄存器下面“映射”的一栏一致,各个模式的说明见表 筛选器的工作状态说明

筛选器的工作状态说明

例如下面的表格所示,在掩码模式时,第一个寄存器存储要筛选的ID,第二个寄存器存储掩码,掩码为1的部分表示该位必须与ID中的内容一致, 筛选的结果为表中第三行的ID值,它是一组包含多个的ID值,其中x表示该位可以为1可以为0。

ID

1

0

1

1

1

0

1

掩码

1

1

1

0

0

1

0

筛选的ID

1

0

1

x

x

0

x

而工作在标识符模式时,2个寄存器存储的都是要筛选的ID,它只包含2个要筛选的ID值(32位模式时)。

如果使能了筛选器,且报文的ID与所有筛选器的配置都不匹配,CAN外设会丢弃该报文,不存入接收FIFO。

42.2.1.5. 整体控制逻辑

回到图 STM32的CAN外设架构图 结构框图,图中的标号处表示的是CAN2外设的结构, 它与CAN1外设是一样的,他们共用筛选器且由于存储访问控制器由CAN1控制,所以要使用CAN2的时候必须要使能CAN1的时钟。其中STM32F103系列芯片不具有CAN2控制器。

42.3. CAN初始化结构体

从STM32的CAN外设我们了解到它的功能非常多,控制涉及的寄存器也非常丰富,而使用STM32标准库提供的各种结构体及库函数可以简化这些控制过程。 跟其它外设一样,STM32标准库提供了CAN初始化结构体及初始化函数来控制CAN的工作方式,提供了收发报文使用的结构体及收发函数, 还有配置控制筛选器模式及ID的结构体。这些内容都定义在库文件“stm32f10x_can.h”及“stm32f10x_can.c”中, 编程时我们可以结合这两个文件内的注释使用或参考库帮助文档。

首先我们来学习初始化结构体的内容,见代码清单:CAN-1。

代码清单:CAN-1 CAN初始化结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
* @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;

这些结构体成员说明如下,其中括号内的文字是对应参数在STM32标准库中定义的宏, 这些结构体成员都是“42.2.1 1CAN控制内核”小节介绍的内容,可对比阅读:

(1) CAN_Prescaler

本成员设置CAN外设的时钟分频,它可控制时间片Tq的时间长度,这里设置的值最终会减1后再写入BRP寄存器位,即前面介绍的Tq计算公式:

Tq = (BRP[9:0]+1) x TPCLK

等效于:Tq = CAN_Prescaler x TPCLK

(2) CAN_Mode

本成员设置CAN的工作模式,可设置为正常模式(CAN_Mode_Normal)、回环模式(CAN_Mode_LoopBack)、静默模式(CAN_Mode_Silent)以及回环静默模式(CAN_Mode_Silent_LoopBack)。

(3) CAN_SJW

本成员可以配置SJW的极限长度,即CAN重新同步时单次可增加或缩短的最大长度,它可以被配置为1-4Tq(CAN_SJW_1/2/3/4tq)。

(4) CAN_BS1

本成员用于设置CAN位时序中的BS1段的长度,它可以被配置为1-16个Tq长度(CAN_BS1_1/2/3…16tq)。

(5) 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

(6) CAN_TTCM

本成员用于设置是否使用时间触发功能(ENABLE/DISABLE),时间触发功能在某些CAN标准中会使用到。

(7) CAN_ABOM

本成员用于设置是否使用自动离线管理(ENABLE/DISABLE),使用自动离线管理可以在节点出错离线后适时自动恢复,不需要软件干预。

(8) CAN_ AWUM

本成员用于设置是否使用自动唤醒功能(ENABLE/DISABLE),使能自动唤醒功能后它会在监测到总线活动后自动唤醒。

(9) CAN_ABOM

本成员用于设置是否使用自动离线管理功能(ENABLE/DISABLE),使用自动离线管理可以在出错时离线后适时自动恢复,不需要软件干预。

(10) CAN_NART

本成员用于设置是否使用自动重传功能(ENABLE/DISABLE),使用自动重传功能时,会一直发送报文直到成功为止。

(11) CAN_RFLM

本成员用于设置是否使用锁定接收FIFO(ENABLE/DISABLE),锁定接收FIFO后,若FIFO溢出时会丢弃新数据,否则在FIFO溢出时以新数据覆盖旧数据。

(12) CAN_TXFP

本成员用于设置发送报文的优先级判定方法(ENABLE/DISABLE),使能时,以报文存入发送邮箱的先后顺序来发送,否则按照报文ID的优先级来发送。

配置完这些结构体成员后,我们调用库函数CAN_Init即可把这些参数写入到CAN控制寄存器中,实现CAN的初始化。

42.4. CAN发送及接收结构体

在发送或接收报文时,需要往发送邮箱中写入报文信息或从接收FIFO中读取报文信息,利用STM32标准库的发送及接收结构体可以方便地完成这样的工作, 它们的定义见 代码清单:CAN-2

代码清单:CAN-2 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
/**
* @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;

这些结构体成员都是“42.2.1 2CAN发送邮箱及CAN接收FIFO”小节介绍的内容,可对比阅读, 发送结构体与接收结构体是类似的,只是接收结构体多了一个FMI成员,说明如下:

(1) StdId

本成员存储的是报文的11位标准标识符,范围是0-0x7FF。

(2) ExtId

本成员存储的是报文的29位扩展标识符,范围是0-0x1FFFFFFF。ExtId与StdId这两个成员根据下面的IDE位配置,只有一个是有效的。

(3) IDE

本成员存储的是扩展标志IDE位,当它的值为宏CAN_ID_STD时表示本报文是标准帧,使用StdId成员存储报文ID; 当它的值为宏CAN_ID_EXT时表示本报文是扩展帧,使用ExtId成员存储报文ID。

(4) RTR

本成员存储的是报文类型标志RTR位,当它的值为宏CAN_RTR_Data时表示本报文是数据帧;当它的值为宏CAN_RTR_Remote时表示本报文是遥控帧, 由于遥控帧没有数据段,所以当报文是遥控帧时,下面的Data[8]成员的内容是无效的。

(5) DLC

本成员存储的是数据帧数据段的长度,它的值的范围是0-8,当报文是遥控帧时DLC值为0。

(6) Data[8]

本成员存储的就是数据帧中数据段的数据。

(7) FMI

本成员只存在于接收结构体,它存储了筛选器的编号,表示本报文是经过哪个筛选器存储进接收FIFO的,可以用它简化软件处理。

当需要使用CAN发送报文时,先定义一个上面发送类型的结构体,然后把报文的内容按成员赋值到该结构体中,最后调用库函数CAN_Transmit把这些内容写入到发送邮箱即可把报文发送出去。

接收报文时,通过检测标志位获知接收FIFO的状态,若收到报文,可调用库函数CAN_Receive把接收FIFO中的内容读取到预先定义的接收类型结构体中,然后再访问该结构体即可利用报文了。

42.5. CAN筛选器结构体

CAN的筛选器有多种工作模式,利用筛选器结构体可方便配置,它的定义见 代码清单:CAN-3

代码清单:CAN-3 CAN筛选器结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
* @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;

这些结构体成员都是“42.2.1 4验收筛选器”小节介绍的内容,可对比阅读,各个结构体成员的介绍如下:

(1) CAN_FilterIdHigh

CAN_FilterIdHigh成员用于存储要筛选的ID,若筛选器工作在32位模式,它存储的是所筛选ID的高16位; 若筛选器工作在16位模式,它存储的就是一个完整的要筛选的ID。

(2) CAN_FilterIdLow

类似地,CAN_FilterIdLow成员也是用于存储要筛选的ID,若筛选器工作在32位模式, 它存储的是所筛选ID的低16位;若筛选器工作在16位模式,它存储的就是一个完整的要筛选的ID。

(3) CAN_FilterMaskIdHigh

CAN_FilterMaskIdHigh存储的内容分两种情况,当筛选器工作在标识符列表模式时,它的功能与CAN_FilterIdHigh相同, 都是存储要筛选的ID;而当筛选器工作在掩码模式时,它存储的是CAN_FilterIdHigh成员对应的掩码,与CAN_FilterMaskIdLow组成一组筛选器。

(4) CAN_FilterMaskIdLow

类似地,CAN_FilterMaskIdLow存储的内容也分两种情况,当筛选器工作在标识符列表模式时,它的功能与CAN_FilterIdLow相同, 都是存储要筛选的ID;而当筛选器工作在掩码模式时,它存储的是CAN_FilterIdLow成员对应的掩码,与CAN_FilterMaskIdHigh组成一组筛选器。

上面四个结构体的存储的内容很容易让人糊涂,请结合前面的图 STM32中CAN的位时序 和下面的表 不同模式下各结构体成员的内容 理解, 如果还搞不清楚,再结合库函数CAN_FilterInit的源码来分析。

不同模式下各结构体成员的内容

对这些结构体成员赋值的时候,还要注意寄存器位的映射,即注意哪部分代表STID,哪部分代表EXID以及IDE、RTR位。

(5) CAN_FilterFIFOAssignment

本成员用于设置当报文通过筛选器的匹配后,该报文会被存储到哪一个接收FIFO,它的可选值为FIFO0或FIFO1(宏CAN_Filter_FIFO0/1)。

(6) CAN_FilterNumber

本成员用于设置筛选器的编号,即本过滤器结构体配置的是哪一组筛选器,CAN一共有28个筛选器,所以它的可输入参数范围为0-27(STM32F103系列芯片的可输入参数为0-13)。

(7) CAN_FilterMode

本成员用于设置筛选器的工作模式,可以设置为列表模式(宏CAN_FilterMode_IdList)及掩码模式(宏CAN_FilterMode_IdMask)。

(8) CAN_FilterScale

本成员用于设置筛选器的尺度,可以设置为32位长(宏CAN_FilterScale_32bit)及16位长(宏CAN_FilterScale_16bit)。

(9) CAN_FilterActivation

本成员用于设置是否激活这个筛选器(宏ENABLE/DISABLE)。

配置完这些结构体成员后,我们调用库函数CAN_FilterInit即可把这些参数写入到筛选控制寄存器中,从而使用筛选器。 我们前面说如果不理解那几个ID结构体成员存储的内容时,可以直接阅读库函数CAN_FilterInit的源代码理解,就是因为它直接对寄存器写入内容,代码的逻辑是非常清晰的。

42.6. CAN—双机通讯实验

本小节演示如何使用STM32的CAN外设实现两个设备之间的通讯,该实验中使用了两个实验板,如果您只有一个实验板, 也可以使用CAN的回环模式进行测试,不影响学习的。为此,我们提供了“CAN—双机通讯”及“CAN—回环测试”两个工程, 可根据自己的实验环境选择相应的工程来学习。这两个工程的主体都是一样的,本教程主要以“CAN—双机通讯”工程进行讲解。

42.6.1. 硬件设计

V1

双CAN通讯实验硬件连接图

V2

双CAN通讯实验硬件连接图 双CAN通讯实验硬件连接图

双CAN通讯实验硬件连接图_ 中的是两个实验板的硬件连接。 在单个实验板中,作为CAN控制器的STM32引出CAN_Tx和CAN_Rx两个引脚与CAN收发器TJA1050相连,收发器使用CANH及CANL引脚连接到CAN总线网络中。 为了方便使用,我们每个实验板引出的CANH及CANL都连接了1个120欧的电阻作为CAN总线的端电阻, 所以要注意如果您要把实验板作为一个普通节点连接到现有的CAN总线时,是不应添加该电阻的!

要实现通讯,我们还要使用导线把实验板引出的CANH及CANL两条总线连接起来,才能构成完整的网络。实验板之间CANH1与CANH2连接,CANL1与CANL2连接即可。

要注意的是,由于我们的实验板CAN使用的信号线与摄像头共用了,为防止干扰,平时我们默认是不给CAN收发器供电的, 使用CAN的时候一定要把CAN接线端子旁边的“C/4-5V”排针使用跳线帽与“5V”排针连接起来进行供电,并且把摄像头从板子上拔下来。

如果您使用的是单机回环测试的工程实验,就不需要使用导线连接板子了,而且也不需要给收发器供电,因为回环模式的信号是不经过收发器的。

42.6.2. 软件设计

为了使工程更加有条理,我们把CAN控制器相关的代码独立分开存储,方便以后移植。在“串口实验”之上新建“bsp_can.c”及“bsp_can.h”文件, 这些文件也可根据您的喜好命名,它们不属于STM32标准库的内容,是由我们自己根据应用需要编写的。

42.6.2.1. 编程要点

  1. 初始化CAN通讯使用的目标引脚及端口时钟;

  2. 使能CAN外设的时钟;

  3. 配置CAN外设的工作模式、位时序以及波特率;

  4. 配置筛选器的工作方式;

  5. 编写测试程序,收发报文并校验。

42.6.2.2. 代码分析

CAN硬件相关宏定义

我们把CAN硬件相关的配置都以宏的形式定义到 “bsp_can.h”文件中,见 代码清单:CAN-4

代码清单:CAN-4 CAN硬件配置相关的宏(bsp_can.h文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#define CANx                        CAN1
#define CAN_CLK                    RCC_APB1Periph_CAN1
#define CAN_RX_IRQ                 USB_LP_CAN1_RX0_IRQn
#define CAN_RX_IRQHandler         USB_LP_CAN1_RX0_IRQHandler

#define CAN_RX_PIN                 GPIO_Pin_8
#define CAN_TX_PIN                 GPIO_Pin_9
#define CAN_TX_GPIO_PORT          GPIOB
#define CAN_RX_GPIO_PORT          GPIOB
#define CAN_TX_GPIO_CLK        (RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOB)
#define CAN_RX_GPIO_CLK           RCC_APB2Periph_GPIOB

以上代码根据硬件连接,把与CAN通讯使用的CAN号、引脚号以及时钟都以宏封装起来,并且定义了接收中断的中断向量和中断服务函数, 我们通过中断来获知接收FIFO的信息。注意在GPIO时钟部分我们还加入了AFIO时钟, 这是为下面CAN进行复用功能重映射而设置的,当使用复用功能重映射时,必须开启AFIO时钟。

初始化CAN的 GPIO

利用上面的宏,编写CAN的初始化函数,见 代码清单:CAN-5

代码清单:CAN-5 CAN的GPIO初始化函数(bsp_can.c文件)
 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
/*
* 函数名:CAN_GPIO_Config
* 描述  :CAN的GPIO 配置
* 输入  :无
* 输出  : 无
* 调用  :内部调用
*/
static void CAN_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;

/* Enable GPIO clock */
RCC_APB2PeriphClockCmd(CAN_TX_GPIO_CLK|CAN_RX_GPIO_CLK, ENABLE);
//重映射引脚
GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE);

/* Configure CAN TX pins */
GPIO_InitStructure.GPIO_Pin = CAN_TX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;            // 复用推挽输

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

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_IPU;              // 上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(CAN_RX_GPIO_PORT, &GPIO_InitStructure);
}

与所有使用到GPIO的外设一样,都要先把使用到的GPIO引脚模式初始化,配置好复用功能,CAN的两个引脚都配置成通用推挽输出模式即可。根据CAN外设的要求, TX引脚要被配置成推挽复用输出、RX引脚要被设置成浮空输入或上拉输入。另外,由于本实验板中的CAN使用的PB8、PB9的默认复用功能不是CAN外设的引脚, 需要使用外设引脚的复用功能重映射才能正常使用,代码中使用GPIO_PinRemapConfig函数把CAN映射至PB8、PB9, 见表 CAN引脚的复用功能重映射

CAN引脚的复用功能重映射

配置CAN的工作模式

接下来我们配置CAN的工作模式,由于我们是自己用的两个板子之间进行通讯,波特率之类的配置只要两个板子一致即可。如果您要使实验板与某个CAN总线网络的通讯的节点通讯, 那么实验板的CAN配置必须要与该总线一致。我们实验中使用的配置见 代码清单:CAN-6

代码清单:CAN-6 配置CAN的工作模式(bsp_can.c文件)
 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
/*
* 函数名:CAN_Mode_Config
* 描述  :CAN的模式 配置
* 输入  :无
* 输出  : 无
* 调用  :内部调用
*/
static void CAN_Mode_Config(void)
{
    CAN_InitTypeDef        CAN_InitStructure;
    /************************CAN通信参数设置************************/
    /* 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  禁止报文自动重传
    CAN_InitStructure.CAN_RFLM=DISABLE;        //MCR-RFLM  接收FIFO 不锁定
                            // 溢出时新报文会覆盖原有报文
    CAN_InitStructure.CAN_TXFP=DISABLE; //MCR-TXFP  发送FIFO优先级 取决于报文标示符
    CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;  //正常工作模式
    CAN_InitStructure.CAN_SJW=CAN_SJW_2tq; //BTR-SJW 重新同步跳跃宽度 2个时间单元

    /* ss=1 bs1=5 bs2=3 位时间宽度为(1+5+3) 波特率即为时钟周期tq*(1+3+5)  */
    CAN_InitStructure.CAN_BS1=CAN_BS1_5tq; //BTR-TS1 时间段1 占用了5个时间单元
    CAN_InitStructure.CAN_BS2=CAN_BS2_3tq; //BTR-TS1 时间段2 占用了3个时间单元

/* CAN Baudrate = 1 MBps (1MBps已为stm32的CAN最高速率) (CAN 时钟频率为 APB 1 = 36 MHz) */
    ////BTR-BRP 波特率分频器  定义了时间单元的时间长度 36/(1+5+3)/4=1 Mbps
    CAN_InitStructure.CAN_Prescaler =4;
    CAN_Init(CANx, &CAN_InitStructure);
}

这段代码主要是把CAN的模式设置成了正常工作模式,如果您阅读的是“CAN—回环测试”的工程,这里是被配置成回环模式的,除此之外,两个工程就没有其它差别了。

代码中还把位时序中的BS1和BS2段分别设置成了5Tq和3Tq,再加上SYNC_SEG段,一个CAN数据位就是9Tq了,加上CAN外设的分频配置为4分频, CAN所使用的总线时钟fAPB1 = 36MHz,于是我们可计算出它的波特率:

1Tq = 1/(36M/4)=1/9 us

T1bit=(5+3+1) x Tq =1us

波特率=1/T1bit =1Mbps

配置筛选器

以上是配置CAN的工作模式,为了方便管理接收报文,我们还要把筛选器用起来,见 代码清单:CAN-7

代码清单:CAN-7 配置CAN的筛选器(bsp_can.c文件)
 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
/*IDE位的标志*/
#define CAN_ID_STD             ((uint32_t)0x00000000)  /*标准ID */
#define CAN_ID_EXT             ((uint32_t)0x00000004)  /*扩展ID */

/*RTR位的标志*/
#define CAN_RTR_Data            ((uint32_t)0x00000000)  /*数据帧 */
#define CAN_RTR_Remote          ((uint32_t)0x00000002)  /*远程帧*/

/*********************************************************************/
/*
* 函数名:CAN_Filter_Config
* 描述  :CAN的筛选器 配置
* 输入  :无
* 输出  : 无
* 调用  :内部调用
*/
static void CAN_Filter_Config(void)
{
    CAN_FilterInitTypeDef  CAN_FilterInitStructure;

    /*CAN筛选器初始化*/
    CAN_FilterInitStructure.CAN_FilterNumber=0; //筛选器组0
    //工作在掩码模式
    CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;
    //筛选器位宽为单个32位。
    CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit;

    /* 使能筛选器,按照标志符的内容进行比对筛选,
        扩展ID不是如下的就抛弃掉,是的话,会存入FIFO0。 */
    //要筛选的ID高位,第0位保留,第1位为RTR标志,第2位为IDE标志,从第3位开始是EXID
    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;
    //筛选器高16位每位必须匹配
    CAN_FilterInitStructure.CAN_FilterMaskIdHigh= 0xFFFF;
    //筛选器低16位每位必须匹配
    CAN_FilterInitStructure.CAN_FilterMaskIdLow= 0xFFFF;
    //筛选器被关联到FIFO0
    CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0 ;
    //使能筛选器
    CAN_FilterInitStructure.CAN_FilterActivation=ENABLE;

    CAN_FilterInit(&CAN_FilterInitStructure);
    /*CAN通信中断使能*/
    CAN_ITConfig(CANx, CAN_IT_FMP0, ENABLE);
}

这段代码把筛选器第0组配置成了32位的掩码模式,并且把它的输出连接到接收FIFO0,若通过了筛选器的匹配,报文会被存储到接收FIFO0。

筛选器配置的重点是配置ID和掩码,根据我们的配置, 这个筛选器工作在图 一个32位的掩码模式筛选器 中的模式。

一个32位的掩码模式筛选器

在该配置中,结构体成员CAN_FilterIdHigh和CAN_FilterIdLow存储的是要筛选的ID,而CAN_FilterMaskIdHigh和CAN_FilterMaskIdLow存储的是相应的掩码。 在赋值时,要注意寄存器位的映射,在32位的ID中,第0位是保留位,第1位是RTR标志,第2位是IDE标志,从第3位起才是报文的ID(扩展ID)。

因此在上述代码中我们先把扩展ID“0x1314”、IDE位标志“宏CAN_ID_EXT”以及RTR位标志“宏CAN_RTR_DATA”根据寄存器位映射组成一个32位的数据, 然后再把它的高16位和低16位分别赋值给结构体成员CAN_FilterIdHigh和CAN_FilterIdLow。

而在掩码部分,为简单起见我们直接对所有位赋值为1,表示上述所有标志都完全一样的报文才能经过筛选,所以我们这个配置相当于单个ID列表的模式, 只筛选了一个ID号,而不是筛选一组ID号。这里只是为了演示方便,实际使用中一般会对不要求相等的数据位赋值为0,从而过滤一组ID,如果有需要, 还可以继续配置多个筛选器组,最多可以配置28个,代码中只是配置了筛选器组0。

对结构体赋值完毕后调用库函数CAN_FilterInit把个筛选器组的参数写入到寄存器中。

配置接收中断

在配置筛选器代码的最后部分我们还调用库函数CAN_ITConfig使能了CAN的中断,该函数使用的输入参数宏CAN_IT_FMP0表示当FIFO0接收到数据时会引起中断, 该接收中断的优先级配置如下,见 代码清单:CAN-8

代码清单:CAN-8 配置CAN接收中断的优先级(bsp_can.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/*接收中断号*/
#define CAN_RX_IRQ                                  USB_LP_CAN1_RX0_IRQn
/*
* 函数名:CAN_NVIC_Config
* 描述  :CAN的NVIC 配置,第1优先级组,0,0优先级
* 输入  :无
* 输出  : 无
* 调用  :内部调用
*/
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;       //CAN RX中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

这部分与我们配置其它中断的优先级无异,都是配置NVIC结构体,优先级可根据自己的需要配置,最主要的是中断向量,上述代码中把中断向量配置成了CAN的接收中断。

设置发送报文

要使用CAN发送报文时,我们需要先定义一个发送报文结构体并向它赋值,见 代码清单:CAN-9

代码清单:CAN-9 设置要发送的报文(bsp_can.c文件)
 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
/*IDE位的标志*/
#define CAN_ID_STD             ((uint32_t)0x00000000)  /*标准ID */
#define CAN_ID_EXT             ((uint32_t)0x00000004)  /*扩展ID */

/*RTR位的标志*/
#define CAN_RTR_Data            ((uint32_t)0x00000000)  /*数据帧 */
#define CAN_RTR_Remote          ((uint32_t)0x00000002)  /*远程帧*/

/*
* 函数名:CAN_SetMsg
* 描述  :CAN通信报文内容设置,设置一个数据内容为0-7的数据包
* 输入  :无
* 输出  : 无
* 调用  :外部调用
*/
void CAN_SetMsg(CanTxMsg *TxMessage)
{
    uint8_t ubCounter = 0;

    //TxMessage.StdId=0x00;
    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;
    }
}

这段代码是我们为了方便演示而自己定义的设置报文内容的函数,它把报文设置成了扩展模式的数据帧,扩展ID为0x1314,数据段的长度为8, 且数据内容分别为0-7,实际应用中您可根据自己的需求发设置报文内容。当我们设置好报文内容后,调用库函数CAN_Transmit即可把该报文存储到发送邮箱, 然后CAN外设会把它发送出去:

CAN_Transmit(CANx, &TxMessage);

接收报文

由于我们设置了接收中断,所以接收报文的操作是在中断的服务函数中完成的,见 代码清单:CAN-10

代码清单:CAN-10 接收报文(stm32f10x_it.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/*接收中断服务函数*/
#define CAN_RX_IRQHandler                   USB_LP_CAN1_RX0_IRQHandler

extern __IO uint32_t flag ;      //用于标志是否接收到数据,在中断函数中赋值
extern CanRxMsg RxMessage;               //接收缓冲区
/********************************************************************/
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;                      //接收失败
    }
}

根据我们前面的配置,若CAN接收的报文经过筛选器匹配后会被存储到FIFO0中,并引起中断进入到这个中断服务函数中, 在这个函数里我们调用了库函数CAN_Receive把报文从FIFO复制到自定义的接收报文结构体RxMessage中,并且比较了接收到的报文ID是否与我们希望接收的一致, 若一致就设置标志flag=1,否则为0,通过flag标志通知主程序流程获知是否接收到数据。

要注意如果设置了接收报文中断,必须要在中断内调用CAN_Receive函数读取接收FIFO的内容,因为只有这样才能清除该FIFO的接收中断标志, 如果不在中断内调用它清除标志的话,一旦接收到报文,STM32会不断进入中断服务函数,导致程序卡死。

42.6.2.3. main函数

最后我们来阅读main函数,了解整个通讯流程,见 代码清单:CAN-11

代码清单:CAN-11 main函数
 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
_IO uint32_t flag = 0;       //用于标志是否接收到数据,在中断函数中赋值
CanTxMsg TxMessage;              //发送缓冲区
CanRxMsg RxMessage;              //接收缓冲区

/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main(void)
{
    LED_GPIO_Config();

    /*初始化USART1*/
    Debug_USART_Config();

    /*初始化按键*/
    Key_GPIO_Config();

    /*初始化can,在中断接收CAN数据包*/
    CAN_Config();

    printf("\r\n 欢迎使用野火  STM32 开发板。\r\n");
    printf("\r\n 野火CAN通讯实验例程\r\n");

    printf("\r\n 实验步骤:\r\n");

    printf("\r\n 1.使用导线连接好两个CAN讯设备\r\n");
    printf("\r\n 2.使用跳线帽连接好:5v --- C/4-5V \r\n");
    printf("\r\n 3.按下开发板的KEY1键,会使用CAN向外发送0-7的数据包,包的扩展ID为0x1314 \r\n");
    printf("\r\n 4.若开发板的CAN接收到扩展ID为0x1314的数据包,会把数据以打印到串口。 \r\n");
    printf("\r\n 5.本例中的can波特率为1MBps,为stm32的can最高速率。 \r\n");

    while (1)
    {
        /*按一次按键发送一次数据*/
        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);
        }
        if (flag==1)
        {
            LED_GREEN;
            printf("\r\nCAN接收到数据:\r\n");

            CAN_DEBUG_ARRAY(RxMessage.Data,8);

            flag=0;
        }
    }
}

在main函数里,我们调用了CAN_Config函数初始化CAN外设,它包含我们前面解说的GPIO初始化函数CAN_GPIO_Config、中断优先级设置函数CAN_NVIC_Config、 工作模式设置函数CAN_Mode_Config以及筛选器配置函数CAN_Filter_Config。

初始化完成后,我们在while循环里检测按键,当按下实验板的按键1时,它就调用CAN_SetMsg函数设置要发送的报文,然后调用CAN_Transmit函数把该报文存储到发送邮箱, 等待CAN外设把它发送出去。代码中并没有检测发送状态,如果需要,您可以调用库函数CAN_TransmitStatus检查发送状态。

while循环中在其它时间一直检查flag标志,当接收到报文时,我们的中断服务函数会把它置1,所以我们可以通过它获知接收状态, 当接收到报文时,我们把它使用宏CAN_DEBUG_ARRAY输出到串口。

42.6.3. 下载验证

下载验证这个CAN实验时,我们建议您先使用“CAN—回环测试”的工程进行测试,它的环境配置比较简单, 只需要一个实验板,用USB线使实验板“USB TO UART”接口跟电脑连接起来, 在电脑端打开串口调试助手,并且把编译好的该工程下载到实验板,然后复位。这时在串口调试助手可看到CAN测试的调试信息,按一下实验板上的KEY1按键, 实验板会使用回环模式向自己发送报文,在串口调试助手可以看到相应的发送和接收的信息。

使用回环测试成功后,如果您有两个实验板,需要按照“硬件设计”小节中的图例连接两个板子的CAN总线,并且一定要接上跳线帽给CAN收发器供电、把摄像头拔掉防止干扰。 用USB线使实验板“USB TO UART”接口跟电脑连接起来,在电脑端打开串口调试助手,然后使用“CAN—双机通讯”工程编译,并给两个板子都下载该程序, 然后复位。这时在串口调试助手可看到CAN测试的调试信息,按一下其中一个实验板上的KEY1按键,另一个实验板会接收到报文,在串口调试助手可以看到相应的发送和接收的信息。