目录
1. CAN简介
CAN的全称是:Controller Area Network,简称CAN,也称为控制器区域网络,是ISO国际标准化的串行通信协议。
CAN协议的提出最早是为了解决当时汽车行业面临的 “减少线束的数量”、“通过多个 LAN ,进行大量数据的高速通信” 的需要。该协议可以帮助汽车行业提高对安全性、舒适性、方便性、低公害、低成本的要求。1986 年,德国电气商博世公司开发出面向汽车的 CAN 通信协议。此后,CAN 通过 ISO11898 及 ISO11519 进行了标准化。CAN 的出现为分布式控制系统实现各节点之间实时、可靠的数据通信提供了强有力的技术支持。CAN 总线协议已经成为汽车计算机控制系统和嵌入式工业控制局域网的标准总线,并且拥有以 CAN 为底层协议专为大型货车和重工机械车辆设计的 J1939 协议。
CAN控制器根据两条线上的电位差来判断总线电平。总线电平分为显性电平和隐形电平,二者必居其一。发送方通过使总线电平发生变化的方式,将消息发送给接收方。
针对于CAN协议,我们还是从其物理层和协议层来对其进行学习。
2. CAN协议的物理层
CAN 和 IIC、SPI 等具有时钟信号的同步通信方式不同,CAN通讯并不是以时钟信号来进行同步的,它是一种异步通讯。(IIC有SCL时钟总线,SPI有SCK时钟引脚)。CAN 只具有CAN_High和CAN_Low两条信号线,共同构成一组差分信号线,以差分信号的形式进行通讯。
CAN物理层的形式主要分为闭环总线及开环总线网络两种,一种适合于高速通讯,一个适合于远距离通讯。
闭环总线网络:
CAN 闭环通讯网络是一种遵循 ISO11898 标准的高速、短距离网络,它的总线最大长度为40m,通信速度最高为1Mbps,总线的两端各要求有一个120Ω的电阻。(该电阻称作终端匹配电阻(该电阻的作用是:减少通讯过程中产生的噪音干扰,防止通讯设备处于空闲状态时破坏程序))
之所以称作闭环总线网络,是因为两根总线 CAN_High 和 CAN_Low 两端各接了一个120Ω的终端匹配电阻,总线形成了一个闭环。
开环总线网络:
CAN开环总线网络是遵循 ISO11519-2 标准的低速、远距离网络,它的最大传输距离为1Km,最高通讯速率为125kbps,两根总线是独立的,不形成闭环,要求每根总线上各串联有一个 “2.2千欧” 的电阻。
通讯节点:
通讯节点就是CAN总线上可以挂载的控制器个数,IIC 的通讯节点数受限于其7位地址或者10位地址,因为IIC是通过主机寻址的方式寻找从机的;SPI 的通讯节点受限于其片选引脚个数,因为SPI是通过主机的片选信号线CS进行主从机通讯的。而CAN总线上可以挂载多个通讯节点;节点之间的信号经过总线传输,实现节点间通讯。由于CAN通讯协议不对节点进行地址编码,而是对数据内容进行编码,所以网络中的节点个数理论上不受限制,只要总线的负载足够即可,可以通过中继器增强负载。
CAN通讯的过程实际上和RS485通讯过程相似:
CAN 通讯节点由一个 CAN 控制器及 CAN 收发器组成,控制器与收发器之间通过 CAN_TX 和 CAN_RX 信号线相连,收发器与 CAN 总线之间使用 CAN_High 及 CAN_Low 信号线相连。其中 CAN_TX 及 CAN_RX 使用普通的类似 TTL 逻辑信号,而 CAN_High 及 CAN_Low 是一对差分信号线,使用比较特殊的差分信号。
这里简要提及一下什么叫差分信号?
差分信号就是由两条信号线表示同一个信号,比如我们家用的网线就是如此,IIC、SPI都是一条信号线传输一个信号,输出高电平就是1,输出低电平就是0;而差分信号通过两条信号线的电平差表示逻辑上的1与0;比方说:CAN_High=0V,CAN_Low=-5V,此时可以表示为逻辑上的1;CAN_High=5V,CAN_Low=0V,此时可以表示为逻辑上的0;
由于CAN是由差分信号进行传输的,所以CAN也是半双工通讯,也就是不能同时控制发送和接收。
差分信号的优点:
1. 抗干扰能力强,当外界存在噪声干扰时,几乎会同时耦合到两条信号线上,但是接收端只关心两条信号线的差值(也就是说哪怕是有干扰,影响我两根线是同时干扰的,要么两根线的电平同时上升,要么两根线的电平同时下降,其差值是不变的。)所以外界的共模干扰可以完全抵消。
2. 能有效的抑制它对外界的电磁干扰。两根信号线是极性相反的信号线,对外辐射的电磁场可以相互抵消,耦合的越紧密,泄放到外界的电磁能量越少。
3. 时序定位精准。
由于差分信号线具有这些优点,所以在USB协议、485协议、以太网协议及CAN协议的物理层中,都使用了差分信号进行传输。
当CAN节点需要发送数据时,CAN控制器首先把需要发送的二进制编码通过CAN_TX发送给CAN收发器,CAN收发器会把发送的TTL信号转换为差分信号,然后通过差分线CAN_High和CAN_Low输出到CAN总线网络。
通过收发器接收总线上的数据到控制器时,过程是恰好相反的。收发器把总线上收到的CAN_High及CAN_Low信号转换成普通的逻辑电平信号,通过CAN_RX输出到控制器中。
2.1 CAN协议中的差分信号
CAN协议中对其所使用的 CAN_High 和 CAN_Low 表示的差分信号做出了明确的规定:
以高速CAN协议为例,当表示逻辑1时(也就是隐形电平),CAN_High和CAN_Low线上的电压均为2.5V,他们的电压差-=0V;
当表示逻辑0时(也就是显性电平),CAN_High的电平为3.5V,CAN_Low的电平为1.5V,他们的电压差-=2V。
注:高速CAN协议是在闭环总线网络下,低速CAN协议是在开环总线网络下。
在CAN总线中,必须使它处于隐形电平(逻辑1)或显性电平(逻辑0)中的其中一个状态。假设有两个CAN通讯节点,在同一时间,一个输出隐形电平,一个输出显性电平,类似于IIC总线的 “线与” 特性将使它处于显性电平状态,也可以认为显性具有优先的意义。
线与就是说:在IIC通讯中,想要呈现高电平,那么需要上拉电阻将电平拉高,使总线呈现高阻态。
想要呈现低电平,那么总线会使所有设备都成低电平0,也可以说是只要有一个设备想要成为低电平,其他所有设备都要跟着一起成为低电平。
1. 由于CAN总线协议的物理层只有1对差分线,在一个时刻只能表示一个信号,所以对通讯节点来说,CAN通讯是半双工的,收发数据需要分时进行。
2. 在CAN的通讯网络中,因为共用总线,在整个网络中同一时刻只能有一个通讯节点发送信号,其余的节点在该时刻都只能接收。(意思就是说在CAN协议中,不同于IIC、SPI协议,需要明确的规定通讯的主机和从机,CAN协议因为各个节点都是挂载在总线下的,没有规定必须哪个节点是主机,哪个节点必须是从机,也就可以说一个节点发送信号时,其余节点均只能接收信号;因为CAN是半双工的)
3. CAN协议的协议层
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段之间,通过控制各段的长度,可以对采样点的位置进行偏移,以便准确的进行采样。
一般情况下认为在采样点的电平信号是比较稳定的。也就是找到采样点,然后计算出CAN_High-CAN_Low的值,然后表示逻辑1或者逻辑0;就以采样点采集的电平值表示这一位时序的逻辑电平值。
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,而每个数据位设置的是19Tq,则传输一位数据需要时间=19us,从而每秒可以传输的数据位个数为:
= 52631.6(bqs)
每秒可以传输的数据位的个数就是通讯中的波特率。
3.1 CAN的报文种类及结构
当使用 CAN 协议进行通讯时,需要对数据、操作命令(如读/写)以及同步信号进行打包,打包后的这些内容称为报文。
报文的种类:
在原始数据段的前面加上传输起始标签、片选(识别)标签和控制标签,在数据的尾段加上CRC校验标签、应答标签和传输结束标签,把这些特定的内容按照特定的格式打包好,就可以用一个通道表达各种信号,各种各样的标签就如同 SPI 中各种通道上的信号,起到了协同传输的作用。
当整个数据包被传输到其他设备时,只要这些设备按照相应格式去解读,就能还原出原始的数据,这样的报文就被称为CAN的 “数据帧”。
为了能更有效的控制通讯,CAN一共规定了5种类型的帧:
数据帧:
数据帧 以一个显性位(逻辑0)开始,以7个连续的隐形位(逻辑1)结束,在它们之前,分别有仲裁段、控制段、数据段、CRC段和ACK段。
SOF段(Start of Frame):译为帧起始段,帧起始信号只有一个数据位,是一个显性电平,它用于通知各个节点将有数据传输,其他节点通过帧起始信号的电平跳边沿来进行硬同步。
仲裁段:当同时有两个报文被发送时,总线会根据仲裁段的内容决定哪个数据包能被传输,这也是它名称的由来。
仲裁段的内容主要是本数据帧的ID信息(标识符),数据帧具有标准格式和扩展格式两种,区别就在于ID信息的长度,标准格式的ID为11位,扩展格式的ID为29位,它在标准ID的基础上多出18位。
我们都知道IIC主从机是通过主机按照从机地址进行寻址的;SPI的主从机是根据主机的片选信号线是否置低电平0来选择的;那么CAN通信呢?
事实上,CAN通讯时是通过ID的重要性,也就是ID优先级来决定数据帧发送的,也决定着其他节点是否会接收这个数据帧。CAN协议不对挂载在它之上的节点分配优先级和地址,对总线的占有权是根据信息的重要性决定的,也就是说对于重要的信息,可以给他打包一个优先级高的ID,使它能够及时的发送出去。
同时也正是因为CAN通讯这样的优先级分配原则,使得CAN的扩展性大大增强,在总线上增加或减少节点并不影响其他设备。
节点优先级:
报文的优先级,是通过对ID的仲裁来确定的。根据前面对物理层的分析,我们知道如果总线上同时出现显性电平0和隐形电平1,总线的状态会被置为显性电平,CAN正是利用这个特性进行仲裁。
倘若两个节点同时竞争 CAN 总线的占有权,当它们发送报文时,若首先出现隐性电平,隐性电平和显性电平同时存在时,会显示为显性电平,隐性电平就会失去对总线的占有权,进入接收状态(因为 CAN 协议中只能一个结点发送数据,只要有一个节点在发送数据,那么其他的节点只能够在接收数据,就像是 IIC 的 线与 特性)
在开始阶段,两个设备发送的电平一样,所以他们一直继续发送数据。到了上图中箭头所指示的位置,节点 1 发送隐性电平 1,就会失去对总线的占有权,转为接收。而节点 2 是显性电平0,由于总线的 线与 特性使它表达出显性电平,因此节点2竞争成功,这个报文得以被继续发送出去。
RTR位(Remote Transmission Request Bit):译为远程传输请求位,它是用来 区别数据帧和遥控帧 的。当它为显性电平0时表示数据帧,隐形电平1时表示遥控帧。
IDE位(Identifier Extension Bit):译为标识符扩展位,它是用于区分标准格式和扩展格式,当它为显性电平时表示为标准格式,隐性电平时表示扩展格式。
SRR位(Substitute Remote Request Bit):只存在于扩展模式,它用于替代标准格式中的 RTR 位。
控制段:
在控制段中 r1 和 r0 为保留位,默认设置为显性位,显性位就是0。他最主要的是 DLC段(Data Length Code),译为数据长度码它由四个数据位组成,用于表示本报文中的数据段含有多少个字节,DLC 段表示的数字为 0~8。
数据段(0~8Byte Data):
数据段为数据帧的核心内容,它是节点要发送的原始信息,也可以说是还原打包以后的报文得到的数据,由0~8个字节组成,MSB也就是高位先行。
CRC校验段:
为了保证报文的正确传输,CAN的报文包含了一段15位的CRC校验码,一旦接收节点算出的CRC码跟接收到的CRC码不同,则它会向发送节点反馈出错误信息,利用错误帧请求重新发送。
CRC部分的计算一般由CAN控制器硬件完成,出错时的处理则由软件控制最大重发数。
在CRC校验码之后,有一个CRC界定符,CRC界定符为隐性位1,主要作用是把 CRC 校验符与后面的 ACK 段间隔起来。
ACK段:
ACK段包括一个ACK槽位,和ACK界定符位。类似于IIC总线,在ACK槽位中,发送节点发送的是隐性位1,而接受节点则在这一位中发送显性位以示应答。在ACK槽和帧结束之间由ACK界定符间隔开。
EOF段(End of Frame):
EOF,译为帧结束,帧结束段由发送节点发送7个隐性位1表示结束。
4. STM32的CAN外设
STM32 的芯片中具有 bxCAN 控制器(Basic Extended CAN),它支持 CAN 协议 2.0A 和 2.0B 标准。
STM32 的 CAN 控制器支持最高的通讯速率为 1Mb/s;可以自动的接收和发送 CAN 报文,支持使用标准 ID 和拓展 ID 的报文;外设中具有 3 个发送邮箱(发送邮箱就是真正发送报文的时候,首先将报文放到邮箱中,发送的时候发送的是邮箱;类似于收发缓冲区),发送报文的优先级可以使用软件控制,还可以记录发送时间;具有 2 个 3 级深度的接收 FIFO,可以使用过滤功能只接收或不接收某些 ID 号的报文;可配置成自动重发;不支持使用 DMA 进行数据收发。
4.1 CAN框图
①:CAN控制内核
CAN控制内核包含了各种控制寄存器及状态寄存器,着重介绍其中的主控制寄存器CAN_MCR及位时序寄存器CAN_BTR。
主控制寄存器CAN_MCR 负责管理CAN的工作模式:
位 16 DBF:调试冻结 (Debug freeze)
0:调试期间 CAN 处于工作状态。
1:调试期间 CAN 处于接收/发送冻结状态。接收 FIFO 仍可正常访问/控制。
位 7 TTCM:时间触发通信模式 (Time triggered communication mode)
0:禁止时间触发通信模式。
1:使能时间触发通信模式。
其他位的功能也很重要,这里不作一一介绍了。
位时序寄存器CAN_BTR:
位 31 SILM:静默模式(调试)(Silent mode (debug))
0:正常工作
1:静默模式
位 30 LBKM:环回模式(调试)(Loop back mode (debug))
0:禁止环回模式
1:使能环回模式
这两位用来设置工作模式。
工作模式:
为了方便调试,STM32的CAN提供了测试模式,通过配置位时序寄存器CAN_BTR的SILM及LBKM寄存器位可以控制使用正常模式、静默模式、回环模式及静默回环模式。
正常模式:
正常模式就是一个正常的CAN节点,可以向总线发送数据和接收数据。
静默模式:
由下图,输出段的逻辑0会直接发送给输入端,而输出端的逻辑1可以发送给总线,也就是说,静默模式下,只能向总线发送隐性电平1,显性电平0会回到自己的输入端;静默模式下,输入正常。
这种模式一般用于监测,他可以用于分析总线上的流量,但又不会因为发送显性位影响总线。(因为显性电平和隐性电平相遇时,由于线与的特性,会呈现显性电平0)
回环模式:
通过上图,回环模式下,输出端的所有内容都可以发送给总线,同时输出端的所有内容也会发送给自己的输入端,但是输入端只能接收自己输出的内容,总线的内容是无法进入输入端的。
使用回环模式也可以进行自检。即使用总线监测自己的发送内容。
回环静默模式:
回环静默模式是回环模式和静默模式的组合。即回环静默模式下,输入端只接收自己输出端的内容,不接受来自总线的内容。同时也不会向总线发送显性电平干扰总线。这样一来,回环静默模式是无法检测自己发送端的内容的。
这种方式可以在 “热自检” 时使用,即进行自我检查的时候,不会干扰到总线。
位时序:
STM32的CAN外设位时序只包含3段,相对于CAN协议规定的4段少了一段,其实STM32的位时序并不是少了一段,而是原本的PTS段和PBS1段合并成了BS1(其余的STM32的位时序和CAN协议规定的相同)。采样点位于BS1和BS2的交界处。
其中SYNC_SEG段的固定长度为1Tq,而BS1和BS2段可以在位时序寄存器CAN_BTR设置他们的时间长度,他们可以在重新同步期间增长或者缩短,该长度SJW(SJW就是重新同步时增长或者缩短的距离)也可以在位时序寄存器中配置。
波特率:
通过配置位时序寄存器CAN_BTR的TS1[3:0]及TS2[2:0]寄存器位设定BS1及BS2段的长度后,就可以确定每个CAN数据位的时间:
BS1段时间:
=Tq*(TS1[3:0]+1)
BS2段时间:
=Tq*(TS2[2:0]+1)
根据位时序图分成的三段:
=1Tq++=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)*
其中的PCLK指APB1时钟,默认值为36MHz。
最终可以计算出CAN通信的波特率:
BaudRate=1/N*Tq
配置波特率为1Mbps:
②:CAN发送邮箱
CAN外设一共有3个发送邮箱,即最多可以缓存3个待发送的报文。
每个发送邮箱中都包含有标识符寄存器CAN_TIxR、数据长度控制寄存器CAN_TDTxR及2个数据寄存器CAN_TDLxR、CAN_TDHxR
当要使用CAN外设发送报文时,把报文的各个段分解,按位置写入到这些寄存器中,并对标识符寄存器CAN_TIxR中的发送请求寄存器位TMIDxR_TXRQ置1,即可把数据发送出去。
③:CAN接收FIFO
CAN外设中一共两个接收FIFO,每个FIFO中有3个邮箱,即最多可以缓存6个接收到的报文。当接收到报文时,FIFO的报文计数器会自增,而STM32内部读取FIFO数据之后,报文计数器会自减,通过状态寄存器可获知报文计数器的值。
每个接收FIFO中都包含有标识符寄存器CAN_RIxR、数据长度控制寄存器CAN_RDTxR及2个数据寄存器CAN_RDLxR、CAN_RDHxR
④:验收筛选器
CAN外设的验收筛选器,一共有28个筛选器组(切记:是组),每个筛选器组有2个寄存器,CAN1和CAN2共用筛选器。
首先,在CAN协议中,消息的标识符和地址无关,也就是不是通过寻址的方式来选择CAN通信的。CAN协议是按照ID优先级来选择通讯的,与消息的内容有关。所以,每当多个发送消息进行CAN通信时,如果没有筛选器,那么每个消息都需要进行对比,ID优先级高的优先处理。为了解决这一问题,简化软件的工作,STM32的CAN外设接收报文前会先使用验收筛选器,只接收需要的报文到FIFO中。
筛选器工作的时候,可以调整筛选的ID和过滤模式。
根据筛选ID长度分类:
1. 可以分别检查STDID[10:0]、EXTID[17:0]、IDE、RTR这些位,如果这些位都相同,我才允许报文进入FIFO中。
2. 可以分别检查STDID[10:0]、RTR、IDE、EXTID[17:15]这些位,如果这些位都相同,我才允许报文进入FIFO中。
根据过滤的方法:
1. 标识符列表,就是把ID列成一个表,表中ID和标识符都相同才可以接收。
2. 掩码模式,把报文的某几位找出来列成一个表,这几位称为掩码,这几位相同才允许报文进入FIFO。也可以理解为关键字查找。
举个简单的例子:
我们设置过滤器组0工作在:1个32位过滤器-标识符屏蔽模式,然后设置CAN_F0R1=0XFFFF0000,CAN_F0R2=0XFF00FF00。其中存放到CAN_F0R1的值就是期望收到的ID,即我们希望收到的 ID(STID+EXTID+IDE+RTR)--->(这四个也就对应于筛选器结构体的成员变量)最好是:0XFFFF0000。而0XFF00FF00就是设置我们需要必须关心的ID,表示收到的ID中位[31:24]和位[15:8]这 16 个 位的必须和 CAN_F0R1 中对应的位一模一样,而另外的 16 个位则不关心。(这类似于我们筛选,只关心其中重要的几个位),即收到的 ID 必须是 0XFFxx00xx,才算是正确的(x 表示不关心)。
⑤:整体控制逻辑
CAN2外设的结构与CAN1外设是一样,他们共用筛选器,且由于存储访问控制器由CAN1控制,所以要使用CAN2的时候必须先使能CAN1的时钟。
5. CAN相关结构体
//CAN初始化结构体 typedef struct { uint16_t CAN_Prescaler; //配置CAN外设的时钟分频,可设置为1~1024 uint8_t CAN_Mode; //配置CAN工作模式,可设置为回环或者正常 uint8_t CAN_SJW; //配置SJW极限值,也就是SS段发生偏移的最大值 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;
CAN_Prescaler:本成员设置CAN外设的时钟分频,它可以控制时间片Tq的时间长度。也就是前面提及的计算波特率的公式。
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的长度,可以配置为(CAN_BS1_1/2/3-16tq)
CAN_BS2:本成员用于设置CAN时序中BS2的长度,可以配置为(CAN_BS2_1/2/3-8tq)
CAN_TTCM:是否使能TTCM时间触发功能
CAN_ABOM:是否使能ABOM自动离线管理功能
CAN_AWUM:是否使能AWUM自动唤醒功能
CAN_NART:是否使能NART自动重传功能
CAN_RFLM:是否使能RFLM锁定FIFO功能
CAN_TXFP:配置TXFP报文优先级的判定方法 (以上是否使能的这些诸多结构体成员,都是ENABLE或者DISABLE,用到该功能时使能即可)
//发送结构体 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;
stdId 和 ExtId 都是存储报文标识符的ID的,两个只有其中一个有效,要么使用标准标识符,要么使用扩展标识符。
后续的不管是IDE/RTR/DLC还是Data[8],都是报文包装过程的时序。
当需要使用CAN发送报文时,先定义一个上面的结构体,然后把报文的内容根据需要赋值到该结构体中,最后调用库函数CAN_Transmit把这个内容写到发送邮箱即可把报文发出去。
CAN的发送流程:程序选择一个空置的邮箱(TME=1)----->设置标识符(ID)、数据长度和发送数据 ---->设置CAN_TIxR的TXRQ位为1,请求发送 ---->邮箱挂号(等待成为最高优先级) ---->预定发送(等待总线空闲) ---->发送-邮箱空置。
//接收结构体 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;
FMI:本成员只存在于接收结构体中,它存储了筛选器的编号,表示本报文是经过哪个筛选器存储进的FIFO,可以用它简化软件处理。
接收报文时,通过检测标志位获得接收FIFO的标志,若收到报文,可以调用库函数CAN_Receive将接收到的报文存储到已经定义好的接收报文结构体中,然后通过访问结构体即可利用该报文。
在介绍CAN接收流程之前,首先要明白接收到有效报文之后,会被存储在3级邮箱的FIFO。FIFO由硬件处理,减少CPU处理负荷。CAN接收的话,必须保证报文是有效的,才会被接收。这里的有效报文是指那些正确被接收的(直到 EOF都没有错误)且通过了标识符过滤的报文。
CAN接收流程:FIFO空----->收到有效的报文----->挂号1(存入FIFO的一个邮箱,由硬件控制)----->收到有效报文----->挂号2----->收到有效报文----->挂号3----->收到有效报文----->溢出。
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 uint16_t CAN_FilterNumber; //筛选器编号,范围0-27 uint16_t CAN_FilterMode; //筛选器模式 uint16_t CAN_FilterScale; //设置筛选器尺度 FunctionalState CAN_FilterActivation; //是否使能本筛选器 }CAN_FilterInitTypeDef;
CAN_FilterIdHigh:用于存储要筛选的ID,若筛选器工作在32位模式,它存储的是ID的高16位;若筛选器工作在16位模式,它存储的是整个ID。
6. CAN相关寄存器
CAN主控制寄存器:CAN_MCR
位0 INRQ:
软件对该位清0,可使CAN从初始化模式进入正常工作模式:当CAN在接收引脚检测到连续的11个隐性位后,CAN就达到了同步,并为接收和发送做好了准备。硬件相应的对CAN_MSR寄存器的INAK位清0。
软件对该位置1,可使CAN从正常工作模式进入初始化模式:一旦当前CAN的发送和接收结束,CAN就进入了初始化模式。硬件相应地对CAN_MSR寄存器的INAK位置1。
也就是说:我们在对CAN进行初始化时,首先要将该位置1,进入初始化模式,然后进入初始化,之后设置该位为0,让CAN进入正常工作模式。
注:CAN_MSR是CAN主状态寄存器。
CAN位时序寄存器:CAN_BTR
位31 SILM:静默模式 / 位30 LBKM:回环模式
该两位是用来设置CAN的工作模式的。
该寄存器通过其他位可以设置分频、BS1、BS2、SJW等重要参数,直接决定了CAN的波特率。
CAN发送邮箱标识符寄存器:CAN_TIxR(x=0~3)
位0 TXRQ:
发送邮箱请求,由软件置1,用于请求发送相应邮箱的内容。
通过该寄存器可以设置标准标识符或者扩展标识符,另外还可以设置帧类型。通过TXRQ位值1,来请求邮箱发送。因为有3个发送邮箱,所以寄存器CAN_TIxR有3个。
CAN发送邮箱数据长度和时间戳寄存器:CAN_TDTxR(x=0~2)
位3:0 DLC:设置数据长度代码。
该寄存器用来设置数据长度,即最低4个位。
CAN发送邮箱低字节数据寄存器:CAN_TDLxR(x=0~2)
该寄存器是32位寄存器,每8个位表示一个字节。该寄存器用来存储将要发送的数据,低字节数据寄存器:CAN_TDLxR用来存储低4个字节。
另外还有一个寄存器CAN_TDHxR,该寄存器用来存储高 4 个字节,这样总共就可以存储 8 个字节
CAN接收FIFO邮箱标识符寄存器:CAN_RIxR(x=0/1)
该寄存器用于保存接收到的报文标识符等信息,可以通过读该寄存器获取相关信息。
CAN过滤器模式寄存器:CAN_FM1R
位27:0 FBMx:筛选器模式
筛选器x的寄存器模式
0:筛选器存储区x的两个32位寄存器处于标识符屏蔽模式。
1:筛选器存储区x的两个32位寄存器处于标识符列表模式。
该寄存器用来设置各滤波器组的工作模式。
注意:该寄存器必须在过滤器处于初始化模式下才可以进行设置。CAN_FMR 的 FINIT 位=1表示处于初始化模式下。
CAN过滤器位宽寄存器:CAN_FS1R
该寄存器用于设置各滤波器的位宽。
注意:该寄存器必须在过滤器处于初始化模式下才可以进行设置。CAN_FMR 的 FINIT 位=1表示处于初始化模式下。
CAN过滤器FIFO关联寄存器:CAN_FFA1R
该寄存器设置报文通过滤波器组之后,被存入的FIFO。
如果对应位为0,则存放到FIFO0;
如果对应位为1,则存放到FIFO1;
注意:该寄存器必须在过滤器处于初始化模式下才可以进行设置。CAN_FMR 的 FINIT 位=1表示处于初始化模式下。
CAN过滤器激活寄存器:CAN_FA1R
该寄存器用于激活对应的滤波器组。
对应位置1,开启对应的滤波器组;
对应位置0,关闭对应的滤波器组;
7. 硬件设计
因为我手上使用的是STM32F4xx系列的开发板,所以这里主要来讲解STM32F4的CAN的硬件原理图。
首先STM32的CAN的收发引脚CAN1_RX和CAN1_TX为引脚PA11和PA12的复用功能。USB也是该引脚进行控制的,所以使用时注意区别。
CAN的原理图和 RS485 相似,RS485 使用收发芯片 SP3485 进行 TTL 电平和差分信号之间的转换。
CAN使用 TJA1050 芯片进行 TTL 电平和差分信号之间的转换。
在开发板上使用CAN时需要将跳线帽接在图中红色的区域,当使用USB功能时,将跳线帽接在PA11、PA12和D+、D-上。
7.1 TJA1050芯片
TJA1050芯片在原理图的电路如下图所示:
D/R//RS:官方的引脚定义是DC voltage at TXD, RXD, Vref and S,表示TXD, RXD, Vref和S处的直流电压,所以CAN1_TX会接到D上,CAN1_RX会接到R上,而不是像串口那样一收一发。
GND:地。
VCC:支持电压。-0.3V~+5.25V。
CANL:CAN_Low总线。
CANH:CAN_High总线。实现两个开发板通讯时,CAN_High和另外一个开发板的CAN_High相连。CAN_Low和另外一个开发板的CAN_Low相连。
注:引脚上的箭头表示流向。
C75电容:滤波电容,使得芯片接收到较为稳定的电压,滤掉其中的杂志。
R51电阻:匹配终端电阻。通常120Ω。如果我们的开发板不是作为CAN的终端的话,去掉这个电阻以免影响通讯。
两个开发板使用CAN进行通讯时,每个开发板相互收发信息,则两个开发板都有作为终端的过程。当两个开发板的CAN_High和CAN_Low相连时,会形成一个闭环回路,回路的起始和终端都有一个120Ω的终端电阻,该电阻的作用是可以滤掉噪音。同时形成闭环回路后,会形成高速CAN通讯模式,加快通讯的速率。
8. 库函数配置CAN初始化
1. 配置相关引脚的复用功能AF9,使能CAN时钟
CAN1_RX和CAN1_TX使用时是通过跳线帽和USB D+和USB D-来进行使用的。接在引脚PA11和PA12上。
//使能相关时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);//使能 PORTA 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);//使能 CAN1 时钟
//初始化 GPIO
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11| GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化 PA11,PA12
//引脚复用映射配置
GPIO_PinAFConfig(GPIOA,GPIO_PinSource11,GPIO_AF_CAN1);//PA11 复用为 CAN1
GPIO_PinAFConfig(GPIOA,GPIO_PinSource12,GPIO_AF_CAN1);//PA12 复用为 CAN1
2. 设置CAN工作模式及波特率
首先设置CAN_MCR寄存器的 INRQ 位,让 CAN 进入初始化模式。然后设置CAN_MCR 的其他相关控制位。再通过 CAN_BTR 设置波特率和工作模式(正常模式/环回模式) 等信息。 最后设置 INRQ 为 0,退出初始化模式。
uint8_t CAN_Init(CAN_TypeDef* CANx, CAN_InitTypeDef* CAN_InitStruct); //初始化CAN
typedef struct
{
uint16_t CAN_Prescaler;
uint8_t CAN_Mode;
uint8_t CAN_SJW;
uint8_t CAN_BS1;
uint8_t CAN_BS2;
FunctionalState CAN_TTCM;
FunctionalState CAN_ABOM;
FunctionalState CAN_AWUM;
FunctionalState CAN_NART;
FunctionalState CAN_RFLM;
FunctionalState CAN_TXFP;
} CAN_InitTypeDef;
CAN_InitStructure.CAN_TTCM=DISABLE; //非时间触发通信模式
CAN_InitStructure.CAN_ABOM=DISABLE; //软件自动离线管理
CAN_InitStructure.CAN_AWUM=DISABLE; //睡眠模式通过软件唤醒
CAN_InitStructure.CAN_NART=ENABLE; //禁止报文自动传送
CAN_InitStructure.CAN_RFLM=DISABLE; //报文不锁定,新的覆盖旧的
CAN_InitStructure.CAN_TXFP=DISABLE; //优先级由报文标识符决定
CAN_InitStructure.CAN_Mode= CAN_Mode_LoopBack; //模式设置 1,回环模式;
CAN_InitStructure.CAN_SJW=CAN_SJW_1tq;//重新同步跳跃宽度为个时间单位
CAN_InitStructure.CAN_BS1=CAN_BS1_8tq; //时间段 1 占用 8 个时间单位
CAN_InitStructure.CAN_BS2=CAN_BS2_7tq;//时间段 2 占用 7 个时间单位
CAN_InitStructure.CAN_Prescaler=5; //分频系数(Fdiv)
CAN_Init(CAN1, &CAN_InitStructure); // 初始化 CAN1
3. 设置滤波器
void CAN_FilterInit(CAN_FilterInitTypeDef* CAN_FilterInitStruct) //初始化滤波器函数
typedef struct
{
uint16_t CAN_FilterIdHigh;
uint16_t CAN_FilterIdLow;
uint16_t CAN_FilterMaskIdHigh;
uint16_t CAN_FilterMaskIdLow;
uint16_t CAN_FilterFIFOAssignment;
uint8_t CAN_FilterNumber;
uint8_t CAN_FilterMode;
uint8_t CAN_FilterScale;
FunctionalState CAN_FilterActivation;
} CAN_FilterInitTypeDef;
CAN_FilterInitStructure.CAN_FilterNumber=0; //过滤器 0
CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit; //32 位
CAN_FilterInitStructure.CAN_FilterIdHigh=0x0000;32 位 ID
CAN_FilterInitStructure.CAN_FilterIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh=0x0000;//32 位 MASK
CAN_FilterInitStructure.CAN_FilterMaskIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0;// FIFO0
CAN_FilterInitStructure.CAN_FilterActivation=ENABLE; //激活过滤器 0
CAN_FilterInit(&CAN_FilterInitStructure);//滤波器初始化
4. 发送接受消息
uint8_t CAN_Transmit(CAN_TypeDef* CANx, CanTxMsg* TxMessage); //发送消息函数
void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage);//接收消息函数
5. CAN状态获取
CAN_TransmitStatus() //发送消息状态
CAN_MessagePending() //挂起消息数目
CAN_GetFlagStatus() //CAN获取状态位函数
9. 实验代码
实验现象:
本次程序通过按键选择 CAN的工作模式(正常模式/回环模式)。
然后通过KEY0控制数据发送,并通过查询的办法,将接收到的数据显示在 LCD 模块上。
如果是 回环模式(发送的数据直接到输入,不可能从总线接收数据),我们用一个开发板即可测试。如果是 正常模式,我们就需要 2 个 STM32F4(板载CAN的即可) 开发板,并且将他们的 CAN 接口对接起来,然后一个开发板发送数据,另外一个开发板将接收到的数据显示在 LCD 模块上。
9.1 main.c
#include "stm32f4xx.h"
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "Key.h"
#include "usmart.h"
#include "CAN.h"
//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
led_set(sta);
}
int main(void)
{
u8 key;
u8 i=0;
u8 t=0;
u8 CNT=0; //CNT是一个累加数,一旦KEY0按下,就以这个数位基准连续发送8个数据。
u8 CANBuf[8]; //CAN存储区,可存储8位字节大小的数据
u8 res; //获得返回值,判断是否发送或者接收成功
u8 Mode=1; //CAN工作模式:0,普通模式;1,回环模式
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //因为有中断,所以需要设置中断优先级分组2
delay_init(168);
uart_init(115200);
LED_Init();
LCD_Init();
Key_Init();
CAN1_Mode_Init(CAN_SJW_1tq,CAN_BS2_6tq,CAN_BS1_7tq,6,CAN_Mode_LoopBack);//初始化CAN 模式也可以设置为正常模式CAN_Mode_Normal,通过两个开发板进行测试
//重新同步的时间跳跃单位为1Tq,BS2时间长度为6Tq,BS1时间长度为7Tq,波特率分频器值为6,模式设置为回环模式
//通过该配置得到的波特率值为500Kbqs
POINT_COLOR=RED;
LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
LCD_ShowString(30,70,200,16,16,"CAN Test");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,"2023/06/21");
LCD_ShowString(30,130,200,16,16,"LoopBack Mode");
LCD_ShowString(30,150,200,16,16,"KEY0:Send WK_UP:Mode"); //显示按键的提示信息
POINT_COLOR=BLUE; //颜色设置为蓝色
LCD_ShowString(30,170,200,16,16,"Count:"); //显示当前的计数值
LCD_ShowString(30,190,200,16,16,"Send Data:"); //提示发送的数据
LCD_ShowString(30,250,200,16,16,"Receive Data:"); //提示接收到的数据
while(1)
{
key=KEY_Scan(0);
if(key==1)//KEY0按下 发送数据
{
//发送的第一步,先将数据填充到发送缓冲区中,准备发送
for(i=0;i<8;i++)
{
CANBuf[i]=i+CNT;//填充发送缓冲区,CNT是一个累加数,一旦KEY0按下,就以这个数为基准连续发送8个数据。
//循环每进行一次,我发送一位数据,下一次发送数据都要建立在上一次发送数据位的基础之上,所以是i+CNT
if(i<4) //因为是要一次按键发送8个数据,这里if判断的意思是8个数据分开放,放在LCD两行显示
{
LCD_ShowxNum(30+i*4*8,210,CANBuf[i],3,16,0x80); //显示前四个数据,30+i*4*8表示每两个数据之间空4个字节的长度
//3表示显示数据为3位,字节大小16,0x80表示填充
}
else
LCD_ShowxNum(30+(i-4)*4*8,230,CANBuf[i],3,16,0x80);//显示后四个数据,30+(i-4)*4*8表示后四个数据和前四个数据的排列方式一样
}
res=CAN1_Send_Msg(CANBuf,8);//调用CAN发送字节函数,发送8个字节,res接收函数的返回值,若返回1,发送失败;返回0,发送成功
if(res) //res为真,意味着返回值为1,发送失败
{
LCD_ShowString(30+8*10,190,200,16,16,"Failed"); //提示发送失败
//30+8*10的意思是:调整坐标使发送失败的信息正好显示在Send Data:后面
}
else
{
LCD_ShowString(30+8*10,190,200,16,16,"OK "); //提示发送成功
}
}
else if(key==4) //KEY_UP按下,表示切换模式
{
//u8 Mode=1; CAN工作模式:0,正常模式;1,回环模式
Mode=!Mode; //切换CAN模式
CAN1_Mode_Init(CAN_SJW_1tq,CAN_BS2_6tq,CAN_BS1_7tq,6,Mode);//CAN普通模式初始化,普通模式,波特率500Kbps
//最初初始化Mode等于1,然后在下面第一次初始化CAN,设置的是回环模式,就默认Mode=1表示回环模式
//Mode取反,Mode=0;切换为普通模式
//#define CAN_Mode_Normal ((uint8_t)0x00)
//#define CAN_Mode_LoopBack ((uint8_t)0x01) CAN头文件定义的,正常模式为0,回环模式为1
POINT_COLOR=RED;
if(Mode==0) //普通模式,此时需要两个开发板
{
LCD_ShowString(30,130,200,16,16,"Normal Mode");
}
else //回环模式,一个开发板自收自发
{
LCD_ShowString(30,130,200,16,16,"LoopBack Mode");
}
POINT_COLOR=BLUE;
}
key=CAN1_Receive_Msg(CANBuf); //将CAN接收函数CAN1_Receive_Msg的返回值给到key
//CAN1_Receive_Msg该函数如果接收成功,返回接收到数据的长度;如果接收失败,返回0;
if(key) //key只要为真,就意味着接收到了数据,不管字节长多少,判断语句都为真
{
LCD_Fill(30,270,160,310,WHITE); //清除的范围是x:30-160 y:270-310 这样坐标下的一个矩形
for(i=0;i<key;i++) //因为此时key的值是发送数据的字节长,所以要一个字节一个字节的把接收到的数据显示出来
{
if(i<4)
{
LCD_ShowxNum(30+i*32,270,CANBuf[i],3,16,0X80); //分两行显示,显示前4个数据
}
else
{
LCD_ShowxNum(30+(i-4)*32,290,CANBuf[i],3,16,0X80); //显示后4个数据
}
}
}
t++;
delay_ms(10);
if(t==20)
{
LED0=!LED0;
t=0;
CNT++;//CNT是一个累加数,一旦KEY0按下,就以这个数位基准连续发送8个数据。
//CNT++就是发送数据的基准以CNT值为基础进行发送。也就是程序只要运行起来,Count值开始++,
//按键按下时,Count值是多少,在基础上发送后面8个数,这也是前面CANBuf[i]=i+CNT中i+CNT的意思。
LCD_ShowxNum(30+6*8,170,CNT,3,16,0x80); //显示Count值
}
}
}
9.2 CAN.c
#include "stm32f4xx.h"
#include "CAN.h"
//CAN初始化
//SJW:重新同步跳跃的时间单元 CAN_SJW_1tq~ CAN_SJW_4tq
//TSB1:时间段1的时间单元 CAN_BS1_1tq ~CAN_BS1_16tq
//TSB2:时间段2的时间单元 CAN_BS2_1tq ~CAN_BS2_8tq
//Bound_Previous_Devide:波特率分频器 范围:1~1024; tq=(Bound_Previous_Devide)*tpclk1
//Bound波特率=Fpclk1/((TSB1+1+TSB2+1+1)*Bound_Previous_Devide);
//Mode:CAN_Mode_Normal,正常模式;CAN_Mode_LoopBack,回环模式;
//Fpclk1的时钟在初始化的时候设置为42M,如果设置CAN1_Mode_Init(CAN_SJW_1tq,CAN_BS2_6tq,CAN_BS1_7tq,6,CAN_Mode_LoopBack);
//则波特率为:42M/((6+7+1)*6)=500Kbps(这里解释一下为什么和公式中的不一样,只加了一个1。)
//TSB1+1和TSB2+1在设置时设置的数值是包含这个+1的。也就是说设置的SB2=6,SB1=7,6和7里面是包含这两个1的。另外加的那个1是SS段固定的1Tq。这个1Tq是固定不变的。
//返回值:0,初始化OK;
// 其他,初始化失败;
u8 CAN1_Mode_Init(u8 SJW,u8 TSB2,u8 TSB1,u8 Bound_Previous_Devide,u8 Mode)
{
#if CAN1_RX0_INT_ENABLE //如果使用中断功能,则开启中断优先级NVIC
NVIC_InitTypeDef NVIC_InitStructure;
#endif
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟,因为要使用PA11和PA12的引脚复用功能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1,ENABLE); //使能CAN1时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF; //引脚复用开启
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP; //推挽输出
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_11|GPIO_Pin_12;
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP; //上拉
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA11、PA12
GPIO_PinAFConfig(GPIOA,GPIO_PinSource11,GPIO_AF_CAN1); //PA11复用为CAN1
GPIO_PinAFConfig(GPIOA,GPIO_PinSource12,GPIO_AF_CAN1); //PA12复用为CAN1
//初始化CAN
CAN_InitTypeDef CAN_InitStructure;
CAN_InitStructure.CAN_ABOM=DISABLE; //软件自动离线管理
CAN_InitStructure.CAN_AWUM=DISABLE; //睡眠模式自动唤醒
CAN_InitStructure.CAN_BS1=TSB1;
CAN_InitStructure.CAN_BS2=TSB2;
CAN_InitStructure.CAN_Mode=Mode; //模式选择回环模式
CAN_InitStructure.CAN_NART=ENABLE; //禁止报文自动传送
CAN_InitStructure.CAN_Prescaler=Bound_Previous_Devide;
CAN_InitStructure.CAN_RFLM=DISABLE; //报文不锁定,新的覆盖旧的
CAN_InitStructure.CAN_SJW=SJW;
CAN_InitStructure.CAN_TTCM=DISABLE; //非时间触发通信模式
CAN_InitStructure.CAN_TXFP=DISABLE; //优先级由报文标识符决定
CAN_Init(CAN1,&CAN_InitStructure);
//配置CAN过滤器
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterIdHigh=0x0000; //32位ID
CAN_FilterInitStructure.CAN_FilterIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh=0x0000; //32位Mask
CAN_FilterInitStructure.CAN_FilterMaskIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask; //过滤模式使用ID掩码模式
CAN_FilterInitStructure.CAN_FilterNumber=0; //使用过滤器0
CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit; //过滤器位数使用32位
CAN_FilterInitStructure.CAN_FilterActivation=ENABLE; //激活过滤器0
CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0; //过滤器0关联到FIFO0,这里可供选择的只有CAN_Filter_FIFO0/CAN_Filter_FIFO1
CAN_FilterInit(&CAN_FilterInitStructure);
#if CAN1_RX0_INT_ENABLE
CAN_ITConfig(CAN1,CAN_IT_FMP0,ENABLE); //FIFO0 挂号中断允许
NVIC_InitStructure.NVIC_IRQChannel=CAN1_RX0_IRQn; //CAN1中断通道RX0
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1; //主优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
#endif
return 0;
}
#if CAN1_RX0_INT_ENABLE //使能RX0中断
//中断服务函数
void CAN1_RX0_IRQHandler(void)
{
// 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;
CanRxMsg RxMessage; //CanRxMsg是STM32库函数定义的接收结构体,该代码的意思是设置结构体的结构体变量
int i=0;
CAN_Receive(CAN1,0,&RxMessage);//void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage);接收消息函数
for(i=0;i<8;i++)
{
printf("rxbuf[%d]:%d\r\n",i,RxMessage.Data[i]);
}
}
#endif
//CAN发送一组数据(固定格式:ID为0x12,标准帧,数据帧)
//len:数据长度(最大为8)
//message:数据指针,最大8个字节
//返回值:0,成功;
// 其他,失败;
u8 CAN1_Send_Msg(u8 *message,u8 len)
{
u8 mBox; //定义存储信息的邮箱,当报文被包装好以后,会优先发送给邮箱,然后在发送出去;邮箱类似于发送缓冲区
u16 i=0;
// typedef struct
// {
// uint32_t StdId;
// uint32_t ExtId;
// uint8_t IDE;
// uint8_t RTR;
// uint8_t DLC;
// uint8_t Data[8];
// } CanTxMsg;
CanTxMsg TxMessage; //定义结构体变量 CanTxMsg是STM32库定义接收结构体
TxMessage.StdId=0x12; //标准标识符0
TxMessage.ExtId=0x12; //扩展标识符(29位)
TxMessage.IDE=0; //存储IDE扩展标识,使用扩展标识符0
TxMessage.RTR=0; //存储RTR远程帧标志,这里不使用远程帧,而使用数据帧,一帧8位
TxMessage.DLC=len; //存储报文数据段的长度
for(i=0;i<len;i++)
{
TxMessage.Data[i]=message[i]; //将数据一位一位的存储到报文中
}
mBox=CAN_Transmit(CAN1,&TxMessage); //把已经接收到的这一帧数据存储到邮箱中,邮箱类似于发送缓冲区
i=0; //当一帧数据存放到邮箱后,被校验没有发送错误帧,就将i=0,发送下一帧的数据
while((CAN_TransmitStatus(CAN1,mBox)==CAN_TxStatus_Failed)&&(i<0xFFF))//判断这一帧是否错误
{
i++;//获取状态位,判断这一帧的数据是否有错误
//while判断条件,如果这一帧数据有错误,那么i++,跳过这一帧数据,发送下一帧数据
}
if(i>=0xFFF)
return 1; //发送失败
return 0;//否则return 0,发送成功。
}
//CAN接收数据查询
//Buf:数据缓存区,接收的数据会存储在数据缓冲区中
//返回值:0,无数据被收到;
// 其他,接收的数据长度;
u8 CAN1_Receive_Msg(u8 *Buf)
{
u32 i;
// 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;
CanRxMsg RxMessage;//设置接收结构体变量
if(CAN_MessagePending(CAN1,CAN_FIFO0)==0) //CAN_MessagePending表示挂起消息的数目,每接收一个数据,挂起位就会++;接收数据的个数是有上限的,超过上限就会数据溢出
{
return 0;//表示没有接收数据,直接退出
}
//跳出if判断语句,表示接收到了数据。接下来进行接收数据的读取
CAN_Receive(CAN1,CAN_FIFO0,&RxMessage); //上面是通过结构体变量发送数据的,这里我也通过结构体变量来接收数据
for(i=0;i<RxMessage.DLC;i++) //每一位数据进行一帧一帧的读取,存储到缓冲区中
{
Buf[i]=RxMessage.Data[i];
}
return RxMessage.DLC; //返回接收的数据长度
}
9.3 CAN.h
#ifndef _CAN__H_
#define _CAN__H_
//CAN1接收中断,这次程序不使用中断,所以设置为0
#define CAN1_RX0_INT_ENABLE 0 //0:不使能 1:使能
u8 CAN1_Mode_Init(u8 SJW,u8 TSB2,u8 TSB1,u8 Bound_Previous_Devide,u8 Mode);
u8 CAN1_Send_Msg(u8 *message,u8 len);
u8 CAN1_Receive_Msg(u8 *Buf);
#endif