CAN 是 Controller Area Network 的缩写(以下称为 CAN),是 ISO 国际标准化的串行通信协议。在当前的汽车产业中,出于对安全性、舒适性、方便性、低公害、低成本的要求,各种各样的电子控制系统被开发了出来。为适应“减少线束的数量”、“通过多个 LAN,进行大量数据的高速通信”的需要, 1986 年德国电气商博世公司开发出面向汽车的 CAN 通信协议。此后, CAN 通过 ISO11898 及ISO11519进行了标准化,现在在北美和西欧已是汽车网络的标准协议。
如今, CAN 的高性能和可靠性已被认同,并被广泛地应用于工业自动化、船舶、医疗设备、工业设备等方面。现场总线是当今自动化领域技术发展的热点之一,被誉为自动化领域的计算机局域网。它的出现为分布式控制系统实现各节点之间实时、可靠的数据通信提供了强有力的技术支持。
目录
前言
在学习stm32通信的过程中,USART、I2C、SPI有很多优秀的视频和文章供大家学习和应用,而CAN却不容易掌握。本文章是作者在学习CAN的过程中查阅大量资料后,将知识点融汇贯通,内容较多,但相信读者在仔细阅读和认真思考后会有醍醐灌顶的感觉。此外,若文章有错误或者需要改进的地方,请评论或私信作者。望共勉!
一、CAN的数据指标和特点
1.数据指标
-
有一个CAN总线(stm32f103c8t6 )
-
可以发送和接收11位标识符的标准帧
-
可以发送和接收29位标识符的扩展帧
-
3个发送邮箱
-
2个FIFO
-
3级14个可调节的滤波器
2.特点
-
多主控制 总线空闲时,所有单元都可发送消息,而两个以上的单元同时开始发送消息时,根据标识符(ID,非地址)决定优先级。两个以上的单元同时开始发送消息时,对各消息ID的每个位进行逐个仲裁比较。仲裁获胜(优先级最高)的单元可继续发送消息,仲裁失利的单元立刻停止发送而进行接收工作
-
系统柔软性 连接总线的单元没有类似“地址”的信息(地址需要分配),因此,在总线上添加单元时,已连接的其他单元的软硬件和应用层都不需要做改变
-
速度快距离远 最高1Mbps(距离 < 40m),最远可达10km(速率 < 5kbps)
-
具有错误检测、错误通知和错误恢复功能 所有单元都可以检测错误(错误检测功能),检测出错误的单元会立即同时通知其他所有单元(错误通知功能),正在发送消息的单元一旦检测出错误,会强制结束当前的发送。强制结束发送的单元会不断反复地重新发送此消息直到成功发送为止(错误恢复功能)
-
故障封闭功能 CAN可以判断出错误的类型是总线上暂时的数据错误(如外部噪声等)还是持续的数据错误(如单元内部故障、驱动器故障、断线等)。由此功能,当总线上发生持续数据错误时,可将引起此故障的单元从总线上隔离出去
-
连接节点多 CAN总线是可同时连接多个单元的总线。可连接的单元总线理论上是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电器负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,可连接的单元数减少。
-
波特率 由于 CAN 属于异步通讯,没有时钟信号线,连接在同一个总线网络中的各个节点会像串口异步通讯那样,节点间使用约定好的波特率进行通讯,特别地, CAN 还会使用“位同步”的方式来抗干扰、吸收误差,实现对总线电平信号进行正确的采样,确保通讯正常。
CAN总线是基于相同波特率通信的,所以设备介入前要知道总线上的波特率是多少
波特率 = 1 / 一个数据位的时长 = 1 / (TSS + TPTS + TPBS1 + TPBS2)下文会详细介绍
例:SS = 1Tq,PTS = 3Tq,PBS1 = 3Tq,PBS2 = 3Tq Tq = 0.5us波特率 = 1 / (0.5us + 1.5us + 1.5us + 1.5us)= 200kbps
-
数据数量 一次最多只能发送8个字节的数据,多于8个的需要第二次再发送,或者做一个上层的连续多数据发送的函数
3.其他功能
- 工作模式:正常,睡眠,测试
- 测试模式中包括:静默、环回、环回静默
- 时间接触通信模式
- 寄存器访问保护
- 中断
- 记录接收SOF时刻的时间戳
这些功能在CAN总线基本的收发之外,提供了更多的应用方式
二、结构
1.控制器与收发器
可以看到,CPU如果想要通过CAN总线发送和接收消息其实还需要两个“帮手”——CAN控制器和CAN收发器 。如果是使用8051单片机则需要连接SIA1000等CAN控制器,而stm32则已经集成了CAN的控制器,只需要接TJA1050(ISO 11898标准)或其他CAN收发器即可。
TJA1050采用的是ISO11898
控制器与收发器之间通过 CAN_Tx 及CAN_Rx 信号线相连,收发器与 CAN 总线之间使用 CAN_High 及 CAN_Low 信号线相连。其中CAN_Tx 及 CAN_Rx 使用普通的类似 TTL 逻辑信号,而 CAN_High 及 CAN_Low 是一对差分信号线(抗干扰能力强)。
CAN 控制器与 CAN收发器的关系如同 TTL 串口与 MAX3232 电平转换芯片的关系, MAX3232 芯片把 TTL 电平的串口信号转换成 RS-232 电平的串口信号,CAN 收发器的作用则是把 CAN 控制器的 TTL 电平信号转换成差分信号 (或者相反) 。
2.闭环总线网络
CAN 物理层的形式主要有两种,分别为闭环总线网络和开环总线网络
图中的 CAN 通讯网络是一种遵循 ISO11898 标准的高速、短距离“闭环网络”,它的总线最大长度为 40m,通信速度最高为 1Mbps,总线的两端各要求有一个“120 欧”的电阻
120Ω终端电阻作用:
- 用来做阻抗匹配,以减少回波反射
- 在没有设备操作总线的时候,将两根线的电压拉到同一水平。
当某个设备想发送0时,他就会操作总线,把总线“拉开”,也就是产生电压差,使其呈现0状态;当设备想发送1时,就不去碰总线,总线在终端电阻的收缩下,自动归位默认状态1。(与I2C的上拉电阻相似)
3.开环总线网络
图中的CAN 通讯网络是遵循 ISO11519-2 标准的低速、远距离“开环网络”,它的最大传输距离为 1km,最高通讯速率为 125kbps,两根总线是独立的、不形成闭环,要求每根总线上各串联有一个“2.2千欧”的电阻。
4.差异
- 总线电平 = CAN_H的电压 - CAN_L的电压
- 显性电平对应逻辑0 = 总线电平为2V左右
- 隐性电平对应逻辑1 = 总线电平为0V左右
- 显性电平具有优先权,只要有一个单元输出显性电平,总线上即为显性电平
- 隐形电平则具只有所有的单元都输出隐形电平,总线上才为隐形电平
- 显性电平比隐形电平更强,优先级高
三、通信过程
通过CAN总线发送和接收的是报文。CPU先将报文存放在发送邮箱中再进行发送,发送邮箱有三个。当发送节点想要发送新数据但发送邮箱已被其他数据占用时,它会不断尝试发送数据,直到发送成功或达到了预设的重传次数。这种重传机制使得即使发送邮箱繁忙,数据也不会被丢弃,而是会被推迟发送,直到发送邮箱可用。而在接收过程中,报文会先被过滤器筛选,只有通过过滤器的报文才会被存储到FIFO0或者FIFO1接收邮箱中。
1.空闲状态
在CAN协议中,当总线上的上出现连续的11位隐性电平时,表示总线就处于空闲状态。也就是说对于任意一个节点而言,只要它监听到总线上连续出现了11位隐性电平,那么该节点就会认为总线当前处于空闲状态。
怎么让总线连续出现11位隐形电平呢?由于显性电平的高优先级特性,必须所有CAN主机都连续发送11个隐性电平,或者不发送时,总线才能出现连续11个隐性电平,即处于空闲状态。
通过后续报文的学习,我们可以知道,11位隐性电平其实是1位ACK界定符+7位帧结束EOF段+3位帧间隔
2.发送
蓝色虚线框表示单片机内部的CAN总线控制器
在发送过程中首先需要了解几个名词,后续会有详细介绍
-
报文(数据包/帧):CAN设备一次发送出去的完整数据信息
-
邮箱:用于发送报文的发送调度器
-
帧种类:不同用途的报文种类。数据帧、遥控帧、错误帧、过载帧、帧间隔
-
数据帧:用于发送单元向接收单元传送数据的帧(广播式)
-
遥控帧/远程帧:用于接收单元向具有相同的ID的发送单元请求数据的帧(请求式)
-
错误帧:用于当检测出错误时向其他单元通知错误的帧
-
过载帧:用于接收单元通知其尚未做好接收准备的帧
-
帧间隔:用于将数据帧及遥控帧于前面的帧分离开来的帧
-
用户控制的只有前两个
-
-
帧格式:一个报文里包含的内容格式
-
标识符:CAN总线上的设备可以用此判断数据是不是发给自己的
数据帧和遥控帧有标准格式和扩展格式两种格式
3.接收
-
所有设备都会接收报文,但标识符不符的报文会被过滤器删除
-
接收邮箱于发送邮箱有所不同
接收邮箱
-
FIFO:表面的意思是“先入先出”,是指有层级深度的接收邮箱
-
STM32F103系列单片机上有2个FIFO邮箱,每个FIFO有3层深度。即可以存放6组数据
-
与过滤器匹配的报文会被放入FIFO邮箱
四、报文
在 SPI 通讯中,片选、时钟信号、数据输入及数据输出这 4 个信号都有单独的信号线,I2C 协议包含有时钟信号及数据信号 2 条信号线,异步串口包含接收与发送 2 条信号线,这些协议包含的信号都比 CAN 协议要丰富,它们能轻易进行数据同步或区分数据传输方向。而 CAN 使用的是两条差分信号线,只能表达一个信号,简洁的物理层决定了 CAN 必然要配上一套更复杂的协议,如何用一个信号通道实现同样、甚至更强大的功能呢?CAN 协议给出的解决方案是对数据、操作命令 (如读/写) 以及同步信号进行打包,打包后的这些内容称为报文。
在原始数据段的前面加上传输起始标签、片选 (识别) 标签和控制标签,在数据的尾段加上 CRC校验标签、应答标签和传输结束标签,把这些内容按特定的格式打包好,就可以用一个通道表达各种信号了,各种各样的标签就如同 SPI 中各种通道上的信号,起到了协同传输的作用。当整个数据包被传输到其它设备时,只要这些设备按格式去解读,就能还原出原始数据,这样的报文就被称为 CAN 的“数据帧”。
数据帧的报文格式:
数据帧以一个显性位 (逻辑 0) 开始,以 7 个连续的隐性位 (逻辑 1) 结束,在它们之间,分别有仲裁段、控制段、数据段、CRC 段和 ACK 段。
扩展格式ID中间夹的两位是为了兼容标准格式
- 帧起始
SOF 段 (Start OfFrame),译为帧起始,帧起始信号只有一个数据位,是一个显性电平,它用于通知各个节点将有数据传输,其它节点通过帧起始信号的电平跳变沿来进行硬同步。 - 仲裁段
当同时有两个报文被发送时,总线会根据仲裁段的内容决定哪个数据包能被传输,这也是它名称的由来。- ID
本数据帧的 ID 信息, ID 信息的作用:① 如果同时有多个节点发送数据时,作为优先级依据(仲裁机制);② 目标节点通过 ID 信息来接受数据(验收滤波技术) - RTR
RTR标识是否是远程帧(0,数据帧;1,远程帧)
- ID
- 控制段
- IDE
IDE用于区分标准格式与扩展格式,在标准格式中 IDE 位为显性(‘b0),在扩展格式里 IDE 位为隐性(’b1) - r0
1bit保留位,固定为显性位 - DLC
由 4 位组成,MSB 先行(高位先行),它的二进制编码用于表示本报文中的数据段含有多少个字节,DLC 段表示的数字为0到8,若接收方接收到 9~15 的时候并不认为是错误
- IDE
- 数据段
数据段为数据帧的核心内容,它是节点要发送的原始信息,由 0~8 个字节组成,MSB 先行。 - CRC段
为了保证报文的正确传输,CAN 的报文包含了一段 15 位的 CRC 校验码,一旦接收节点算出的CRC 码跟接收到的 CRC 码不同,则它会向发送节点反馈出错信息,利用错误帧请求它重新发送。CRC 部分的计算一般由 CAN 控制器硬件完成,出错时的处理则由软件控制最大重发数。
CRC 界定符:发送方释放总线,将控制权交给接收方,如果接受成功,接收方会在ACK槽拉开总线 - ACK段
- ACK槽
在 ACK 槽位中,发送端发送的为隐性位,而接收端则在这一位中发送显性位以示应答;发送 ACK/返回 ACK这个过程使用到回读机制,即发送方先在 ACK 槽发送隐性位后,回读到的总线上的电平为显性0,发送方才知道它发送成功了,不用重发 - ACK界定符
隔开 ACK 槽和帧结束
- ACK槽
- 帧结束
EOF 段 (End Of Frame),译为帧结束,帧结束段由发送节点发送的 7 个隐性位表示结束。
五、仲裁机制
节点1和节点2同时向节点3发送数据的时候,如何判定先后呢?答案是采用非破坏性位仲裁机制,即对各个消息的标识符(即ID号)进行逐位仲裁(比较),如果某个节点发送的消息仲裁获胜,那么这个节点将获取总线的发送权,仲裁失败的节点则立即停止发送并转变为监听(接收)状态。
实现非破坏性仲裁需要两个要求:
线与特性:总线上任何一个设备发送显性电平0时,总线就会呈现显性电平0状态,只有当所有设备都发送隐形电平1时,总线才呈现隐形电平1状态
回读机制:每个设备发出一个数据位后,都会读回总线当前的电平状态,以确认自己发出的电平是否被真实的发送出去,根据线与特性,发出0读回必然是0,发出1读回不一定是1(当发送与读回不同时,即仲裁失利)
填充位不会影响仲裁
显性的优先级高于隐性,即仲裁比较的就是哪个ID中的0多,0最多的那个就可以获得发送权,比如 000000 00010 就比 000000 00011 的优先级要高,仲裁的过程由硬件实现;同时要注意,仲裁段除了报文 ID 外,还有 RTR、IDE、SRR 位(拓展模式中),也就是说当ID全都一样时,会继续比较接下来的几位。
至于如何做到“0多即胜”,可以理解为一种回读和线与机制,即显性能够将隐性覆盖,将自己要比较的位与总线上的状态相与,只有线与的结果与本身一致时,仲裁才能够通过。
其实在报文发送上去的过程,采用的是广播的方式,在节点1和节点2仲裁的同时,总线上所有的节点都能够监听到它们的ID号,只不过也在同时进行验收滤波,只有监听到的ID号存在ID表中,该节点才会选择继续监听后面的报文。
六、同步
1.位时序
由于 CAN 没有时钟信号线,而且它的报文中并没有包含用于同步的标志,要怎么做才能对总线的电平进行正确的采样呢?比如我节点1发送3个位出去了,节点2应该在什么时候接收才能保证此时此刻它所接收到的就是第3位或者接收到的电平是正确的? CAN中提出了位同步的方式来确保通讯时序。
CAN总线通讯协议的每一个数据帧可以看作一连串的电平信号,每一个电平信号代表一位(一个字节8位的位),所以一帧中包含了很多个位,由发送单元在非同步的情况下发送的每秒钟的位数称为位速率。 一位又分为4段, 同步段(SS)、传播时间段(PTS)、相位缓冲段 1(PBS1)、相位缓冲段 2(PBS2)。分解后最小的时间单位是 Tq,而一个完整的位由 8~25 个 Tq 组成。
-
1 位分为 4 个段,每个段又由若干个 Tq 构成,这称为位时序。
-
1 位由多少个 Tq 构成、每个段又由多少个 Tq 构成等,可以任意设定位时序。通过设定位时序,多个单元可同时采样,也可任意设定采样点。
-
采样点在PBS1和PBS2中间
SS 段(SYNC SEG):同步段,比如当总线上出现帧起始信号(SOF)时,其它节点上的控制器根据总线上的这个下降沿,对自己的位时序进行调整,把该下降沿包含到 SS 段内,这样根据起始帧来进行同步的方式称为硬同步。其中 SS 段的大小固定为 1Tq。总线上信号的跳变沿被包含在节点的 SS 段的范围之内,则表示节点与总线的时序是同步的,采样点采集到的总线电平即可被确定为该位的电平。
PTS 段(PROP SEG):传播时间段,这个时间段是用于补偿网络的物理延时时间,包括发送单元的输出延迟、总线上信号的传播延迟、接收单元的输入延迟,这个段的时间为以上各延迟时间的和的两倍。大小可以为 1~8Tq。
PBS1 段(PHASE SEG1):相位缓冲段,主要用来补偿边沿阶段的误差,它的时间长度在重新同步的时候可以加长。 PBS1 段的初始大小可以为 1~8Tq。
PBS2 段(PHASE SEG2):另一个相位缓冲段,也是用来补偿边沿阶段误差的,它的时间长度在重新同步时可以缩短。 PBS2 段的初始大小可以为 2~8Tq。
(对于PBS段而言,当信号边沿不能被包含于 SS 段中时,可在此段进行补偿,以及可以吸收时钟误差,即再同步。)
设定PBS1和PBS2大小可以灵活指定采样点靠前还是靠后
2.硬同步
-
作用:使接收方第一个采样点与波形的第一位对齐。
-
将发送和接受的时间”对齐“
-
每个设备都有一个位时序计时周期,当某个设备(发送方)率先发送报文,其他所有设备(接收方)收到SOF的下降沿时,接收方会将自己的位时序计时周期拨到SS段的位置,与发送方的位时序计时周期保持同步
-
硬同步只在帧的第一个下降沿(SOF下降沿)有效
-
经过硬同步后,若发送方和接收方的时钟没有误差,则后续所有数据位的采样点必然都会对齐数据位中心附近
3.再同步
-
作用:解决接收方开始采样正确(开始采样正确依靠硬同步),但时钟有误差,随着误差积累,采样点逐渐偏离的问题(补偿误差)
-
在检测到总线上的时序与节点使用的时序有相位差时(即总线上的跳变沿不在节点时序的 SS 段范围),则此时接收方根据再同步补偿宽度值(SJW)通过延长 PBS1 段或缩短 PBS2 段,来获得同步。
-
再同步可以发生在第一个下降沿之后的每个数据位跳变边沿
-
SJW (reSynchronization Jump Width):重新同步补偿宽度,即在重新同步的时候,PBS1 和 PBS2 段的允许加长或缩短的时间长度,SJW 加大后允许误差加大,但通信速度下降。SJW 为补偿此误差的最大值(即每一次误差补偿都不能超过这个值,1~4Tq)。
第一个是接收方时钟快于发送方,第二个是接收方时钟慢于发送方
注意:不可以每次不同步时都暴力使用硬同步的原因:如果有抖动,这一个数据位就需要多次置换SS位
七、 位填充
位填充规则:发送方每发送5个相同电平后,自动追加一个相反电平的填充位,接收方检测倒填充位时,会自动移除填充位,恢复原始数据
例如:
即将发送: 100000110 10000011110 0111111111110
实际发送: 1000001110 1000001111100 011111011111010
实际接收: 1000001110 1000001111100 011111011111010
移除填充后:100000110 10000011110 0111111111110
位填充作用:
-
增加波形的定时信息,利于接收方执行“再同步”,防止波形长时间无变化,导致接收方不能精确掌握数据采样时机
-
将正常数据流与“错误帧”和“过载帧”区分开(这两个帧会填充在数据帧之后,并且有连续六个相同电平),标志“错误帧”和“过载帧”的特异性(帧结束同样不会进行位填充)
-
保持CAN总线在发送正常数据流时的活跃状态,以防止被误认为总线空闲(连续的11位隐性电平)
八、过滤器
1.特点
-
可由硬件判断报文中的标识符,过滤掉标识符(ID)不匹配的报文,只有与过滤器匹配的报文才需要软件处理
-
STM32F103系列单片机中的CAN总线控制器提供了14个过滤器组
-
如果有两个过滤器,CAN1和CAN2共享28个,如果有CAN3,则CAN3有独立的14个过滤器
-
过滤器总长度为64位,可以分成2个32位单元或4个16位单元,只有32位单元可以过滤拓展ID
-
ID号对齐最高位(MSB)——需要移位操作
-
有掩码和列表两种模式
-
一个筛选器组共有两个32位的寄存器 (CAN_FiRx) ,可将筛选码填入这两个寄存器中。而一个筛选组具有四种筛选模式,不同模式下的两个筛选寄存器中的内容会有不同的效果:
1.32位掩码模式
此模式下筛选寄存器1 (CAN_FiR1) 用来存放ID,筛选寄存器2 (CAN_FiR2) 用来存放此ID的掩码。掩码为1的位一定要匹配ID所对应的位,掩码为0的位则ID此为为1还是0都可以。如果收到匹配成功的ID,则放入接收FIFO。
2.32位列表模式
列表模式就相当于白名单,只有收到与白名单一模一样的ID时才能通过匹配。此模式下筛选寄存器1存储一条ID,筛选寄存器2存储另一条ID。也就是说此模式可以存放两条32位的白名单。
3.16位掩码模式
此模式与前面的32位掩码模式类似,只不过筛选码从原来的32位变成了16位,对扩展ID筛选能力变弱了。但这种模式可以存放两条ID及两条掩码。
4.16位列表模式
此模式可以存放4条16位的ID。
初学者一定不要望图生畏,望字生畏,其实只要仔细看下去你就会柳暗花明又一村。同时要说明一点,这个图最好结合下文代码中配置的过滤器结构体对照来学,相信你会对过滤器有更进一步的掌握。
现在解释一下如何看这个图:
表中1,2,2,4是说能过滤的ID数,对应模式下分别是1个2个2个4个。设置不同的过滤方式产生不同的过滤数量。知道个数代表什么意思以后就可以忽略他。
此外要明白,图中FSCx、FBMx、CAN_FxR1和CAN_FxR2都是代表寄存器。
FSCx为过滤器组位宽,即选择32位还是16位;FBMx为过滤器组模式,即列表模式还是掩码(屏蔽)模式;他们两个在过滤器结构体中分别表示为FilterScale与FilterMode。
后两个需要注意,CAN_FxR1和CAN_FxR2,他们两个即为过滤器组中的两个32位寄存器,根据位数和模式的不同,寄存器中存储的数据不同。在过滤器结构体中FilterIdHigh/low以及FilterMaskIdHigh/low,即为此处的ID与屏蔽寄存器的高16位与低16位
例如当选择了32位掩码模式时,CAN_FxR1寄存器32位存储的是ID,CAN_FxR2寄存器32位存储的是掩码;当选择了16位列表模式,CAN_FxR1寄存器高16位存储一个ID,低16位存储一个ID,CAN_FxR2寄存器同理,这样就存储了四个ID,即只可以过滤出四组报文
最后我们需要关注“映像”,可以看到他将两个32位寄存器分出了不同位去存储某些数据。例如当选择32位掩码模式后,CAN_FxR1和CAN_FxR2【31:24】高8位存储的是STID的高8位;【23:16】位分成了两部分,高三位存储STID低3位,低5位存储EXID高5位;【15:8】位存储EXID【12:5】8位;【7:0】则分成了四部分,【7:3】存储EXID【4:0】,第二位存储IDE,第一位存储RTR,第0位存储数据0。(对于STID、EXID(拓展ID)、IDE、RTR不熟悉的话需要复习报文知识)其余三种模式同理。由此我们可以明白在配置结构体成员FilterIdHigh/low以及FilterMaskIdHigh/low为什么要移位,并且知道该如何移位。
过滤器编号会在下文介绍
至此我们就明白了过滤器中的寄存器的作用以及该如何配置过滤器。这部分内容较多,希望读者可以静下心来仔细阅读,同时与过滤器的结构体对照学习。
这里仅用8位数据举例,实际的过滤器为16位或32位
2.过滤器编号
- 过滤器组(FilterBank)和过滤器编号(FilterMatchIndex)不一样
- 过滤器编号即接收结构体成员FilterMatchIndex
- 通过过滤器编号可以判断放入邮箱中的报文是通过哪一个过滤器被过滤的(存放在CAN_RDT0/1R寄存器中)
3.优先级
当同一个ID能通过多个过滤器时,需要优先级来决定存放到哪个邮箱
-
位宽为32位的过滤器,优先级高于位宽为16位的过滤器
-
对于位宽相同的过滤器,标识符列表模式的优先级高于屏蔽位模式
-
位宽和模式都相同的过滤器,优先级由过滤器号决定,过滤器号小的优先级高
4.双接收中断
对照下文函数中的中断回调函数
-
每个CAN有2个接收中断,对应两组接收邮箱
-
每个过滤器可以绑定一个CAN接收中断
-
经过过滤器过滤的帧会进入该过滤器绑定的接收中断对应的邮箱
九、遥控帧
适合使用频率低,但偶尔又需要集中使用几次的数据
使用频率高的直接使用广播式,一直发即可
十、波形实例
十一、配置波特率
常用为250kbps和500kbps
RX0中断是FIFO0收到数据的中断,RX1是FIFO1收到数据的中断
FIFO0和FIFO1可以一个装重要消息一个装不重要消息等等多种应用
可以下载对应厂家的波特率计算器,如STM32_CANBaudRate等
配置完成后生成工程即可。
十二、相关函数
1.使能CAN
生成工程后,在进行其他操作之前要先调用下面这个函数(只需在初始化过程中调用一次即可):
HAL_StatusTypeDef HAL_CAN_Start(CAN_HandleTypeDef *hcan);
因为CubeMX生成的代码是没有帮我们使能CAN的。
2.发送
1> 发送所用结构体
typedef struct
{
uint32_t StdId; //标准ID
uint32_t ExtId; //扩展ID
uint32_t IDE; //用来决定报文是使用标准ID还是扩准ID
uint32_t RTR; //用来决定报文是数据帧要是遥控帧
uint32_t DLC; //数据长度,取值为0-8
FunctionalState TransmitGlobalTime;
//最后这个是时间触发模式用的,开启后会自动把时间戳添加到最后两字节的数据中。目前没有用到,选择 DISABLE
} CAN_TxHeaderTypeDef;
成员:
StdId :如果将要发送的报文使用标准ID,那么这个成员便记录标准ID的值 取值: 0x0 ~ 0x7FF
ExtId :如果将要发送的报文使用扩展ID,那么这个成员便记录扩展ID的值 取值: 0x0 ~ 0x1FFFFFFF
IDE :用来决定报文使用标准ID还是扩准ID 取值: CAN_ID_STD 或 CAN_ID_EXT
RTR :用来决定报文是数据帧要是遥控帧 取值: CAN_RTR_DATA 或 CAN_RTR_REMOTE
DLC :用来记录数据帧的数据长度,单位字节(如果要发送的是遥控帧,该成员中的内容不起作用) 取值:0 ~ 8
TransmitGlobalTime :ENABLE 或 DISABLE
结构体中的 .IDE 成员是用来决定报文是使用标准ID还是扩准ID,如果这个成员等于 CAN_ID_STD ,也就是使用标准ID,此时 .ExtId 中的内容就不起作用了;反之亦然。
2>发送函数
HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, CAN_TxHeaderTypeDef *pHeader, uint8_t aData[], uint32_t *pTxMailbox);
参数:
*hcan :can的句柄,由CubeMX自动帮我们定义
*pHeader :发送结构体 aData[] :要发送的数据
*pTxMailbox :发送这条报文用的是哪个邮箱,这个作为输出参数。(因为STM32的CAN外设拥有三个发送邮箱,库函数会帮我们找到空的邮箱并把一帧报文装进去,这个参数可以记录用到的邮箱号)即定义一个变量存放邮箱
返回值:
HAL_StatusTypeDef:HAL库定义的几种状态,如果本次CAN发送成功,则返回HAL_OK
3>获取发送邮箱可用数量
HAL_CAN_GetTxMailboxesFreeLevel(&hcan)
如果有可用的邮箱,则继续执行发送操作;如果没有可用的邮箱,则可能需要等待之前的发送操作完成或者采取其他措施来处理这种情况。这种机制确保了CAN通信的可靠性和效率,避免了资源冲突和不必要的等待时间
4>示例
#include "stdio.h"
#include "string.h"
CAN_TxHeaderTypeDef sCAN_Tx;
uint32_t StdID = 0x123; //标准ID,便于修改
uint32_t ExtID = 0; //拓展ID,便于修改
uint32_t Mailbox; //发送邮箱
uint8_t SendBuffer[5] = {"hello"}; //要发送的数据
uint32_t DLC = sizeof(SendBuffer) / sizeof(SendBuffer[0]); //发送数据长度
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_CAN_Init();
//使能CAN失败就执行处理错误的函数
if(HAL_CAN_Start(&hcan) != HAL_OK)
{
Error_Handler(); //不自行编写则为一个死循环
}
sCAN_Tx.StdId = StdID ;
sCAN_Tx.ExtId = ExtID ;
sCAN_Tx.IDE = CAN_ID_STD;
sCAN_Tx.RTR = CAN_RTR_DATA;
sCAN_Tx.DLC = DLC ;
sCAN_Tx.TransmitGlobalTime = DISABLE;
while (1)
{
//如果可用发送邮箱不为空则发送数据
if(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) != 0)
{
//发送数据
HAL_CAN_AddTxMessage(&hcan,&sCAN_Tx,SendBuffer,&Mailbox);
HAL_Delay(1000);
}
}
}
效果:每隔一秒发送一次hello
3.过滤器
必须配置过滤器才可以接收到消息,即使是没有限制的过滤器。同时,如果使用多个CAN,无论是否需要接收消息都需要配置过滤器(不配置CAN2结构体的SlaveStartFilterBank参数会出问题)
1>结构体说明
typedef struct
{
uint32_t FilterActivation; //使能或失能
uint32_t FilterBank; //此次配置用的是哪个筛选器。用单CAN的取值为0-13
uint32_t FilterScale; //32位或16位
uint32_t FilterMode; //掩码模式或列表模式
uint32_t FilterFIFOAssignment; //通过筛选器的报文存在FIFO0还是FIFO1中
uint32_t FilterIdHigh; //CAN_FiR1寄存器的高16位
uint32_t FilterIdLow; //CAN_FiR1寄存器的低16位
uint32_t FilterMaskIdHigh; //CAN_FiR2寄存器的高16位
uint32_t FilterMaskIdLow; //CAN_FiR2寄存器的低16位
uint32_t SlaveStartFilterBank; //CAN1和CAN2一起用的时候,为CAN2分配筛选器的个数
} CAN_FilterTypeDef;
2>成员
-
FilterActivation :使能或失能此筛选器。 取值:CAN_FILTER_DISABLE 或 CAN_FILTER_ENABLE
-
FilterBank :本次配置的筛选器号。如果有两个CAN第二个一般是14 取值:对于单CAN为 0 ~ 13;对于双CAN为 0 ~ 27
-
FilterScale :筛选码大小,16位或32位。 取值:CAN_FILTERSCALE_16BIT 或 CAN_FILTERSCALE_32BIT
-
FilterMode :筛选模式,掩码模式或列表模式。 取值:CAN_FILTERMODE_IDMASK(掩码) 或 CAN_FILTERMODE_IDLIST(列表)
-
FilterFIFOAssignment :通过筛选器的报文存在FIFO0还是FIFO1中 取值:CAN_FILTER_FIFO0 或 CAN_FILTER_FIFO1
-
FilterIdHigh :CAN_FiR1寄存器的高16位,用于填写筛选码。具体的格式要根据16位、32位;掩码模式、列表模式来确定。 取值: 0x0 ~ 0xFFFF
-
FilterIdLow :CAN_FiR1寄存器的低16位
-
FilterMaskIdHigh :CAN_FiR2寄存器的高16位
-
FilterMaskIdLow :CAN_FiR2寄存器的低16位
-
SlaveStartFilterBank :为从CAN(CAN2)分配的筛选器个数。如果只使用单个CAN,可忽略此成员;如果是多个一般是14 取值:0 ~ 27
3>配置过滤器
HAL_CAN_ConfigFilter(&hcan,&sFilterConfig);
4>示例
配置不过滤任何ID的过滤器
CAN_FilterTypeDef sFilterConfig; //s表示结构体(
void CAN_ConfigFileter()
{
CAN_FilterTypeDef sFilterConfig;
sFilterConfig.FilterActivation = CAN_FILTER_ENABLE;
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterFIFOAssignment = CAN_FilterFIFO0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x00;
sFilterConfig.FilterIdLow = 0x00;
sFilterConfig.FilterMaskIdHigh = 0x00;
sFilterConfig.FilterMaskIdLow = 0x00;
sFilterConfig.SlaveStartFilterBank = 0;
//如果配置过滤器失败就执行处理错误的函数(如果自己不写的话仅仅是一个死循环)
if(HAL_CAN_ConfigFilter(&hcan,&sFilterConfig) != HAL_OK)
{
Error_Handler();
}
}
4.中断
1>激活中断
HAL_CAN_ActivateNotification(&hcan, ActiveITs);
参数 :
hcan: 这是CAN总线的句柄,指向CAN_HandleTypeDef
结构体的指针,用于标识特定的CAN总线实例。
ActiveITs: 这是一个整数参数,用来指定要激活的中断或回调通知类型。通常是通过位掩码来指定多个中断或回调通知的组合。可以使用预定义的宏来设置这些位,例如CAN_IT_TX_MAILBOX_EMPTY
表示发送邮箱为空中断,CAN_IT_RX_FIFO0_MSG_PENDING
表示FIFO0接收消息挂起中断等等。
一般设置的是2和3
-
发送邮箱空中断:
-
宏:
CAN_IT_TX_MAILBOX_EMPTY
-
描述:当CAN发送邮箱为空时触发中断或回调。
-
-
接收FIFO0消息挂起中断:
-
宏:
CAN_IT_RX_FIFO0_MSG_PENDING
-
描述:当CAN接收FIFO0中有消息挂起时触发中断或回调。
-
-
接收FIFO1消息挂起中断:
-
宏:
CAN_IT_RX_FIFO1_MSG_PENDING
-
描述:当CAN接收FIFO1中有消息挂起时触发中断或回调。
-
-
总线状态变化中断:
-
宏:
CAN_IT_BUSOFF
-
描述:当CAN总线进入总线关闭状态时触发中断或回调。
-
-
接收FIFO0溢出中断:
-
宏:
CAN_IT_RX_FIFO0_OVERRUN
-
描述:当CAN接收FIFO0溢出时触发中断或回调。
-
-
接收FIFO1溢出中断:
-
宏:
CAN_IT_RX_FIFO1_OVERRUN
-
描述:当CAN接收FIFO1溢出时触发中断或回调。
-
-
错误中断:
-
宏:
CAN_IT_ERROR
-
描述:当CAN总线发生错误时触发中断或回调。
-
-
硬件错误中断:
-
宏:
CAN_IT_ERROR_PASSIVE
-
描述:当CAN总线进入错误被动状态时触发中断或回调。
-
-
仲裁丢失中断:
-
宏:
CAN_IT_LAST_ERROR_CODE
-
描述:当CAN总线最后一个错误代码中包含仲裁丢失时触发中断或回调。
-
这些宏通常作为位掩码组合在一起,可以通过按位或操作来同时激活多个中断或回调通知类型。例如,可以使用CAN_IT_TX_MAILBOX_EMPTY | CAN_IT_RX_FIFO0_MSG_PENDING
来同时激活发送邮箱为空和FIFO0消息挂起的中断或回调通知。
2>中断回调函数
由于FIFO0和FIFO1用到的中断函数是独立的,因此这里的回调函数也是不一样的,要看清楚是FIFO0的还是1的
FIFO0中断回调函数
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
//有多个CAN才需要判断
if(hcan == &hcan1)
{
HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &can_Rx, recvBuf);
/*下面是你用来处理收到数据的代码,
可以通过串口把内容发送出来,
也可以用来控制某些外设*/
}
}
FIFO1中断回调函数
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
if(hcan == &hcan1)
{
HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &can_Rx, recvBuf);
/*下面是你用来处理收到数据的代码,
可以通过串口把内容发送出来,
也可以用来控制某些外设*/
}
}
5.接收
1>接收所用结构体
typedef struct
{
uint32_t StdId;
uint32_t ExtId;
uint32_t IDE;
uint32_t RTR;
uint32_t DLC;
uint32_t Timestamp;
uint32_t FilterMatchIndex;
} CAN_RxHeaderTypeDef;
-
Timestamp :只有使能了时间触发模式才有用,记录时间戳
-
FilterMatchIndex:过滤器编号
其余与发送结构体类型相同,但是接收结构体不需要赋值,而是作为接收函数的输出参数
2>接收函数
HAL_StatusTypeDef HAL_CAN_GetRxMessage(CAN_HandleTypeDef *hcan, uint32_t RxFifo, CAN_RxHeaderTypeDef *pHeader, uint8_t aData[]);
参数:
-
*hcan :can的句柄
-
RxFifo:接收FIFO号。参数:
CAN_RX_FIFO0
或CAN_RX_FIFO1
-
pHeader :接收结构体,这里作为输出参数
-
aData[] :接收数组,这里作为输出参数
3>示例
#include "stdio.h"
#include "string.h"
CAN_TxHeaderTypeDef sCAN_Tx; //定义发送所用结构体
CAN_RxHeaderTypeDef sCAN_Rx; //定义接收所用结构体
uint32_t StdID = 0x123; //标准ID,便于修改
uint32_t ExtID = 0; //拓展ID,便于修改
uint32_t pTxMailbox; //发送邮箱
uint8_t DataBuffer[] = {"hello"}; //发送数据
uint32_t DLC = sizeof(DataBuffer) / sizeof(DataBuffer[0]); //发送数据长度
uint8_t RxBuffer[8]; //存放接收数据的数组
int flag = 0; //按钮标志
int res = 0; //判断是否点灯
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &sCAN_Rx, RxBuffer);
for(uint8_t i = 0;i <= 7;i++)
{
if(RxBuffer[i] != DataBuffer[i])
{
res = 0;
break;
}
res = 1;
}
if(res == 1)
{
HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);
}else
{
HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_CAN_Init();
//配置发送结构体
sCAN_Tx.IDE = StdID;
sCAN_Tx.ExtId = ExtID;
sCAN_Tx.RTR = CAN_RTR_DATA;
sCAN_Tx.IDE = CAN_ID_STD;
sCAN_Tx.DLC = DLC;
sCAN_Tx.TransmitGlobalTime = DISABLE;
CAN_ConfigFileter();
while (1)
{
//按下按钮发送数据
if(HAL_GPIO_ReadPin(Key_GPIO_Port,Key_Pin) == 0 && flag == 0)
{
flag = 1;
//printf("%d\n",flag);
//如果可用发送邮箱不为0则发送数据
if(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) != 0)
{
//发送数据
HAL_CAN_AddTxMessage(&hcan,&sCAN_Tx,DataBuffer,&pTxMailbox);
HAL_Delay(1000);
}
else
{
Error_Handler();
}
}else if(HAL_GPIO_ReadPin(Key_GPIO_Port, Key_Pin) == 1 && flag == 1)
{
flag = 0;
}
}
}
//配置过滤器
void CAN_ConfigFileter()
{
CAN_FilterTypeDef sFilterConfig;
sFilterConfig.FilterActivation = CAN_FILTER_ENABLE;
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterFIFOAssignment = CAN_FilterFIFO0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x00;
sFilterConfig.FilterIdLow = 0x00;
sFilterConfig.FilterMaskIdHigh = 0x00;
sFilterConfig.FilterMaskIdLow = 0x00;
sFilterConfig.SlaveStartFilterBank = 0;
if(HAL_CAN_ConfigFilter(&hcan,&sFilterConfig) != HAL_OK)
{
Error_Handler();
}
//使能CAN
if(HAL_CAN_Start(&hcan) != HAL_OK)
{
Error_Handler();
}
//激活中断
if(HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_RX_FIFO1_MSG_PENDING) != HAL_OK)
{
Error_Handler();
}
}
总结
为实现CAN通信,我们首先需要约定波特率,而这一步由波特率计算器和CubeMX帮我们配置。在使用发送函数之前,我们需要配置结构体CAN_TxHeaderTypeDef,为完成这一步我们必须清楚报文的组成,而在使用接收函数之前,我们需要明白FIFO邮箱的工作原理,同时配置过滤器,激活中断,以处理不同情况。
必不可少的,我们需要清楚CAN的特点、结构、仲裁机制以及CAN为实现同步运用了哪些方法等知识。
至此,基本的CAN通信我们已经了解。文章较长,作者期望可以通过一篇文章为初学者省去查阅大量资料的难题。同时希望读者不要望而生畏。最后,纸上得来终觉浅,绝知此事要躬行,学习过程中必须多多动手实践,不断碰到问题、解决问题,才可以将所学掌握。