【STM32】标准库与HAL库对照学习教程十四--CAN总线


一、前言

本篇介绍如何使用STM32的标准库与HAL库对CAN总线的使用,由于我那块普中的STM32F103ZET6开发板送人了,因此我这边用STM32F103C8最小系统板,进行简单的演示一下功能,最主要的还是需要理解CAN总线的原理,在本篇中,我会尽量把原理简化,便于理解,实在觉得困难的话就多看几遍,并且在结合一下其他的视频教程


二、准备工作

  • STM32开发板
  • STM32cubemx软件、keil5(MDK)
  • 开发板原理图(最小系统的话非必要)

三、CAN协议

3.1 简介

CAN 是控制器局域网络 (Controller Area Network) 的简称,它是由研发和生产汽车电子产品著称的德国 BOSCH 公司开发的,并最终成为国际标准(ISO11519以及ISO11898,我简称519,898),是国际上应用最广泛的现场总线之一。其差异点如下图所示:
在这里插入图片描述

3.2 CAN物理层

CAN 通讯是一种异步通讯,只具有 CAN_HighCAN_Low 两条信号线,共同构成一组差分信号线,以差分信号的形式进行通讯。其连接示意图如下:
在这里插入图片描述
可以看到,CPU使用 CAN控制器 将数据通过 Tx线 发送给 CAN收发器,CAN收发器将信号转化为差分信号传递到网络,
打个比方,串口将数据转换为TTL信号通过Tx端将信号发送出去,一般如果我们电脑要接收这些数据的话,需要一个转换芯片(CH340)将TTL信号转化为电脑可以识别的信号。同样我们也需要一个芯片(CAN收发器)将Tx端发送过来的信号转化为差分信号发送到 CAN总线网络,对于接收信号也是一样。

也就是说CAN控制器是集成在芯片内部的,以引脚的方式引出发送端(Tx)和接收端(Rx),CAN收发器需要自己接,一般的开发板上都会有CAN收发器,但本次使用的最小系统板没有。

部分转换芯片如下:
在这里插入图片描述

3.2.1 闭环总线网络

下图中的 CAN 通讯网络是遵循 ISO11898 标准的高速短距离 的闭环网络,它的总线最大长度为 40m,通信速度最高为 1Mbps,总线的两端各要求有一个“120 欧”的电阻。
在这里插入图片描述

3.2.2 开环总线网络

下图中的是遵循 ISO11519-2 标准的低速远距离“开环网络”,它的最大传输距离为 1km,最高通讯速率为 125kbps,两根总线是独立的、不形成闭环,要求每根总线上各串联有一个“2.2千欧”的电阻。
在这里插入图片描述
是闭环还是开环,看自己的开发板原理图。

从上面就可以看到,CAN总线上是一个个通信节点间的通信,CAN 通信节点由一个 CAN 控制器CAN 收发器组成,各个节点间使用差分信号相互通信。

3.3 差分信号

3.3.1 简介

差分信号需要两根信号线,通过两根信号线的电压差值来表示 逻辑 0逻辑 1
差分信号有 抗干扰能力强,少电磁干扰的优点。

3.3.2 CAN总线上的差分信号

CAN 协议中对它使用的 CAN_High 及** CAN_Low** 表示的差分信号做了规定,如下图所示。以高速 CAN 协议为例

当表示逻辑 1 时 (隐性电平) ,CAN_High 和 CAN_Low 线上的电压均为 2.5v,即它们的电压差 VH-V:sub:L=0V;

当表示逻辑 0 时 (显性电平) ,CAN_High 的电平为 3.5V,CAN_Low 线的电平为 1.5V,即它们的电压差为 VH-V:sub:L=2V。
在这里插入图片描述

有点晕没事,反复记一下就好了。下图是高速与低速CAN总线差分信号对比,主要是电压不同。
在这里插入图片描述

3.4 CAN 协议层

3.4.1 CAN 的波特率

由于 CAN 属于异步通讯,节点之间需要向串口一样,使用约定好的波特率进行通讯。但是CAN总线的波特率的计算有些不同。如下图所示。
在这里插入图片描述
上面的电平表示逻辑信号,不是差分信号,一个单位的电平持续时间会被分为ss段PTS段PBS1段PBS2段,分解后最小的时间单位是 Tq,一个位的持续时间大概在 8~25 个 Tq,如上图所示计算每一个数据位的长度为 19Tq。变个倒数就是波特率。各段的作用如介绍下:

  • ss段
    SS 译为同步段,就是电平跳变的时间,SS 段的大小固定为 1Tq。

  • PTS段
    PTS 译为传播时间段,这个时间段是用于补偿网络的物理延时时间。PTS 段的大小可以为 1~8Tq。

  • PBS1段
    PBS1 译为相位缓冲段,主要用来补偿边沿阶段的误差,它的时间长度在重新同步的时候可以加长。PBS1 段的初始大小可以为 1~8Tq。

  • PBS2段
    PBS2 这是另一个相位缓冲段,也是用来补偿边沿阶段误差的,它的时间长度在重新同步时可以缩短。PBS2 段的初始大小可以为 2~8Tq。

3.4.2 CAN 的同步

为了使CAN总线的数据电平始终维持在ss段PTS段PBS1段PBS2段上,需要对电平进行同步。CAN 的数据同步分为硬同步重新同步。其中硬同步只是当存在“帧起始信号”时起作用。
(1)硬同步
若某个 CAN 节点通过总线发送数据时,它会发送一个表示通讯起始的信号 (即下一小节介绍的帧起始信号),该信号是一个由高变低下降沿。硬同步如下图所示,某节点检测到总线的帧起始信号不在节点内部时序的 SS 段范围,所以判断它自己的内部时序与总线不同步,所以节点以硬同步的方式调整,把自己的位时序中的 SS 段平移至总线出现下降沿的部分,获得同步,同步后采样点就可以采集得正确数据了。
在这里插入图片描述

(2)重新同步
前面的硬同步只是当存在帧起始信号时才起作用,如果在一帧很长的数据内,节点信号与总线信号相位有偏移时,这种同步方式就无能为力了。因而需要引入重新同步方式。

重新同步的方式分为超前滞后两种情况,以总线跳变沿与 SS 段的相对位置进行区分。第一种相位超前的情况如图 ,节点从总线的边沿跳变中,检测到它内部的时序比总线的时序相对超前 2Tq,这时控制器在下一个位时序中的 PBS1 段增加 2Tq 的时间长度,使得节点与总线时序重新同步。
在这里插入图片描述
第二种相位滞后的情况如图 ,节点从总线的边沿跳变中,检测到它的时序比总线的时序相对滞后 2Tq,这时控制器在前一个位时序中的 PBS2 段减少 2Tq 的时间长度,获得同步。
在这里插入图片描述
在重新同步的时候,PBS1 和 PBS2 中增加或减少的这段时间长度被定义为“重新同步补偿宽度SJW (reSynchronization Jump Width)。一般来说 CAN 控制器会限定 SJW 的最大值,如限定了最大 SJW=3Tq 时,单次同步调整的时候不能增加或减少超过 3Tq 的时间长度,若有需要,控制器会通过多次小幅度调整来实现同步。当控制器设置的 SJW 极限值较大时,可以吸收的误差加大,但通讯的速度会下降。

3.5 CAN的通信帧

而 CAN 使用的是两条差分信号线,只能表达一个信号,简洁的物理层决定了 CAN 必然要配上一套更复杂的协议,CAN总线协议将数据、操作命令 (如读/写) 以及同步信号进行打包,打包后的这些内容称为报文或者说

为了更有效地控制通讯,CAN 一共规定了 5 种类型的帧,它们的类型及用途说明如表
在这里插入图片描述

3.6 数据帧的结构

数据帧是在 CAN 通讯中最主要、最复杂的报文,了解清楚清楚了数据帧,那么对其他种类报文的理解就会简单许多,我们来了解它的结构,如下图所示。
在这里插入图片描述
数据帧以一个显性位 (逻辑 0) 开始,以 7 个连续的隐性位 (逻辑 1) 结束,在它们之间,分别有仲裁段、控制段、数据段、CRC 段和 ACK 段。下面讲解各个段的内容。

  • 帧起始
    SOF 段 (Start OfFrame),译为帧起始,帧起始信号只有一个数据位,是一个显性电平,它用于通知各个节点将有数据传输,其它节点通过帧起始信号的电平跳变沿来进行硬同步。
  • 仲裁段
    在CAN总线上一般是一个节点发,其他节点收,那么当多个节点同时发数据该怎么办呢?总线会根据仲裁段的内容决定哪个数据包能被传输。

仲裁段的内容主要为本数据帧的 ID 信息 (标识符),数据帧具有标准格式扩展格式两种,区别就在于 ID 信息的长度,标准格式的 ID 为 11 位,扩展格式的 ID 为 29 位。

在 CAN 协议中, ID 起着重要的作用,它决定着数据帧发送的优先级,也决定着其它节点是否会接收这个数据帧(接收数据的ID不一样,就会丢掉这个数据)。

CAN总线如何进行仲裁呢?当两个节点同时竞争 CAN 总线的占有权,哪个节点的ID先出现隐性电平,那个节点就会失去对总线的占有权,进入接收状态。

如下图所示 ,在开始阶段,两个设备发送的电平一样,所以它们一直继续发送数据。到了图中箭头所指的时序处,节点单元 1 发送的为隐性电平,而此时节点单元 2 发送的为显性电平,由于节点1先出现隐形电平,节点1停止发送,改为接收模式。
在这里插入图片描述
仲裁段除了报文 ID 外,还有 RTR、IDE 和 SRR 位。

RTR位:区分数据帧和遥控帧的,当它为显性电平时表示数据帧,隐性电平时表示遥控帧。
IDE:区分标准格式与扩展格式,当它为显性电平时表示标准格式,隐性电平时表示扩展格式。
SRR:只存在于扩展格式,它用于替代标准格式中的 RTR位。由于扩展帧中的 SRR 位为隐性位,RTR 在数据帧为显性位,所以在两个 ID 相同的标准格式报文与扩展格式报文中,标准格式的优先级较高。

  • 控制段
    在控制段中的 r1r0位 为保留位,默认设置为显性位。它最主要的是 DLC 段,它由 4 个数据位组成,用于表示本报文中的数据段含有多少个字节, DLC 段表示的数字为 0~8。

  • 数据段
    数据段为数据帧的核心内容,它是节点要发送的原始信息,由 0~8 个字节组成。

  • *CRC 段
    为了保证报文的正确传输,CAN 的报文包含了一段 15 位的 CRC 校验码,一旦接收节点算出的CRC 码跟接收到的 CRC 码不同,则它会向发送节点反馈出错信息,利用错误帧请求它重新发送。CRC 部分的计算一般由 CAN 控制器硬件完成。
    在 CRC 校验码之后,有一个 CRC 界定符,它为隐性位,主要作用是把 CRC 校验码与后面的 ACK段间隔起来。

  • ACK 段
    ACK 段包括一个 ACK 槽位,和 ACK 界定符位。接收节点在这一位中发送显性位以示应答。在 ACK 槽和帧结束之间由 ACK 界定符间隔开。

  • EOF 段
    帧结束段由发送节点发送的 7 个隐性位表示结束。

3.7 其它报文的结构

在这里插入图片描述
在这里插入图片描述

四、STM32F103 CAN 控制器介绍

STM32 的芯片中具有 bxCAN 控制器,它支持 CAN 协议 2.0A 和 2.0B 标准。该 CAN 控制器支持最高的通讯速率为 1Mb/s;可以自动地接收和发送 CAN 报文,支持使用标准ID 和扩展 ID 的报文;外设中具有 3 个发送邮箱,发送报文的优先级可以使用软件控制,还可以记录发送的时间;具有 2 个 3 级深度的接收 FIFO,可使用过滤功能只接收或不接收某些 ID 号的报文;可配置成自动重发;不支持使用 DMA 进行数据收发。框架示意图如下:
在这里插入图片描述
STM32 的有两组 CAN 控制器,其中 CAN1 是主设备,框图中的“存储访问控制器”是由 CAN1控制的,CAN2 无法直接访问存储区域。

4.1 CAN 控制内核

CAN 控制内核包含了各种控制寄存器及状态寄存器,我们主要讲解其中的主控制寄存器 CAN_MCR位时序寄存器 CAN_BTR,想要详细了解的朋友可以查看STM32F1中文参考手册。

4.1.1 主控制寄存器 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-4 CAN 标准的分时同步通信功能。

(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 优先级还是报文存进邮箱的顺序来发送。

(8)SLEEP: 睡眠模式请求 (Sleep mode request)
软件对该位置’1’可以请求CAN进入睡眠模式,一旦当前的CAN活动(发送或接收报文)结束,CAN就进入睡眠。
软件对该位清’0’使CAN退出睡眠模式。

(9)INRQ: 初始化请求 (Initialization request)
软件对该位清’0’可使CAN从初始化模式进入正常工作模式:当CAN在接收引脚检测到连续的11
个隐性位后,CAN就达到同步,并为接收和发送数据作好准备了。为此,硬件相应地对
CAN_MSR寄存器的INAK位清’0’。
软件对该位置1可使CAN从正常工作模式进入初始化模式:一旦当前的CAN活动(发送或接收)结
束,CAN就进入初始化模式。相应地,硬件对CAN_MSR寄存器的INAK位置’1’。

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

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

在这里插入图片描述
位31 SILM:静默模式(调试)

0:正常工作

1:静默模式

位30 LBKM:环回模式(调试)

0:禁止环回模式

1:使能环回模式
STM32 的 CAN 提供了测试模式,配置位时序寄存器 CAN_BTR 的 SILM 及 LBKM寄存器位可以控制使用正常模式、静默模式、回环模式及静默回环模式,其各个模式的功能如下图所示。

  • 正常模式
    正常模式下就是一个正常的 CAN 节点,可以向总线发送数据和接收数据。
    在这里插入图片描述
  • 静默模式
    静默模式下,它自己的输出端的逻辑 0 数据会直接传输到它自己的输入端,逻辑 1 可以被发送到总线,可以正常接收数据。
    在这里插入图片描述
  • 回环模式
    回环模式下,它自己的输出端的所有内容都直接传输到自己的输入端,输出端的内容同时也会被传输到总线上,即也可使用总线监测它的发送内容。
    在这里插入图片描述
  • 回环静默模式
    回环静默模式是以上两种模式的结合,自己的输出端的所有内容都直接传输到自己的输入端,不发给总线内容,也不接收总线内容。
    在这里插入图片描述

位24-25 SJW 重新同步补偿宽度
上面讲过,就是补偿误差

位20-22 TS2 时间段
相当于上面讲的PBS2

位16-19 TS1 时间段
相当于上面讲的PTS 段与 PBS1段的结合

位0-9 BRP 分频器,用来计算Tq值
tq = (BRP[9:0]+1)/CAN总线时钟频率

STM32 的 CAN 外设位时序中只包含 3 段,分别是同步段 SYNC_SEG、位段 BS1 及位段 BS2,采样点位于 BS1 及 BS2 段的交界处。其中 SYNC_SEG 段固定长度为 1Tq,而 BS1 及 BS2 段可以在位时序寄存器 CAN_BTR 设置它们的时间长度。
在这里插入图片描述
因此,一个数据位的时间:T1bit =1Tq+TS1+TS2=1+ (TS1[3:0] + 1)+ (TS2[2:0] + 1)= N Tq
因此,CAN波特率 = 1/(N*Tq)
上面提到Tq = (BRP[9:0]+1)/CAN总线时钟频率,结合公式自己算吧

根据自己的芯片查看,CAN总线的时钟频率,例如我的STM32F103C8是36M
在这里插入图片描述
在这里插入图片描述
这里推荐大家自己下载一个波特率计算器,在CSDN上一搜就有例如我的。
在这里插入图片描述
STM32的CAN通信波特率计算器

4.2 CAN 发送邮箱

回到图 中的 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位扩展标识符。

4.3 CAN 接收 FIFO

图 中的 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 的内存中

4.4 验收筛选器

在 CAN 的总外设框图,在标号处的是 CAN 外设的验收筛选器,一共有 28 个筛选器组,每个筛选器组有 2 个寄存器,CAN1 和 CAN2 共用的筛选器的。

发送节点将报文广播给所有接收器时,接收节点会根据报文标识符(ID)的值来确定软件是否需要该消息,这就是筛选器的作用,对比ID。
根据筛选 ID 长度来分类有有以下两种:

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

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

过滤的方法分为以下两种模式:
(1) 标识符列表模式,它把要接收报文的 ID 列成一个表,要求报文 ID 与列表中的某一个标识符完全相同才可以接收。

(2) 掩码模式,它把可接收报文 ID 的某几位作为列表,这几位被称为掩码,只要掩码相同,就符合要求,报文就会被保存到接收 FIFO。通过配置筛选模式寄存器 CAN_FM1R 的 FBMx 位可以设置筛选器工作在哪个模式。例如下图,为掩码为1的位必须是相同,为0的位可以不同。
在这里插入图片描述


五、标准固件库配置CAN总线

5.1 配置步骤

(1)使能CAN时钟,将对应引脚复用映射为CAN功能
(2)设置CAN工作模式、波特率等
(3)设置CAN筛选器
(4)选择CAN中断类型,开启中断(可选)
(5)CAN发送和接收消息
(6)CAN状态获取

5.2 结构体成员功能介绍

5.2.1 CAN初始化函数

uint8_t CAN_Init(CAN_TypeDef* CANx, CAN_InitTypeDef* 
CAN_InitStruct);

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_Prescaler:用于设置CAN 外设的时钟分频,它可控制时间片tq 的时间长度,这里设置的值最终会加 1 后再写入 BRP 寄存器位。

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

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_Prescaler、CAN_BS1和CAN_BS2的值,可得到波特率,根据波特率计算器得到
在这里插入图片描述

5.2.2 设置CAN筛选器

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_FilterIdHigh:用于存储要筛选的 ID,若筛选器工作在 32 位模式,它存储的是所筛选 ID 的高 16 位;若筛选器工作在 16 位模式,它存储的就是一个完整的要筛选的 ID。

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

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

CAN_FilterMaskIdLow:同上一个成员一样,它也是用于存储要筛选的ID或掩码,只不过这里对应存储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)。

5.2.3 设置中断

void CAN_ITConfig(CAN_TypeDef* CANx, uint32_t CAN_IT, FunctionalState NewState);

在这里插入图片描述

5.2.4 CAN发送和接收消息

发送消息的函数

uint8_t CAN_Transmit(CAN_TypeDef* CANx, CanTxMsg* TxMessage);

typedef struct
{
    
    
  uint32_t StdId;  
  uint32_t ExtId;  
  uint8_t IDE;     
  uint8_t RTR;    
  uint8_t DLC;     
  uint8_t Data[8]; 
} CanTxMsg;

StdId:用于存储报文的 11 位标准标识符,范围是 0-0x7FF。

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

IDE:用于存储扩展标志 IDE 位的值,其值可配置为CAN_ID_STD和CAN_ID_EXT。如果为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]:用于存储数据帧中数据段的数据。

接收消息的函数是

void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage);
typedef struct
{
    
    
  uint32_t StdId;  
  uint32_t ExtId;  
  uint8_t IDE;     
  uint8_t RTR;     
  uint8_t DLC;     
  uint8_t Data[8]; 
  uint8_t FMI;   
} CanRxMsg;

跟发送一样,区别是多了一个FMI

5.3 CAN总线的完整配置

关于STM32固件库的文件创建,我前面已经讲了很多遍了,这里直接列出需要的文件。
工程结构
在这里插入图片描述
can.h

#ifndef _can_H
#define _can_H

#include "system.h"

#define CAN_RX0_INT_ENABLE 0   //不使用中断

void CAN_Mode_Init(u8 tsjw,u8 tbs2,u8 tbs1,u16 brp,u8 mode);

u8 CAN_Send_Msg(u8* msg,u8 len);						//发送数据

u8 CAN_Receive_Msg(u8 *buf);							//接收数据


#endif

can.c

#include "can.h"


//CAN初始化
//tsjw:重新同步跳跃时间单元.范围:CAN_SJW_1tq~ CAN_SJW_4tq
//tbs2:时间段2的时间单元.   范围:CAN_BS2_1tq~CAN_BS2_8tq;
//tbs1:时间段1的时间单元.   范围:CAN_BS1_1tq ~CAN_BS1_16tq
//brp :波特率分频器.范围:1~1024;  tq=(brp)*tpclk1
//波特率=Fpclk1/((tbs1+tbs2+1)*brp);
//mode:CAN_Mode_Normal,普通模式;CAN_Mode_LoopBack,回环模式;
//Fpclk1的时钟在初始化的时候设置为36M,如果设置CAN_Mode_Init(CAN_SJW_1tq,CAN_BS2_8tq,CAN_BS1_9tq,4,CAN_Mode_LoopBack);
//则波特率为:36M/((8+9+1)*4)=500Kbps
//返回值:0,初始化OK;
//    其他,初始化失败;
void CAN_Mode_Init(u8 tsjw,u8 tbs2,u8 tbs1,u16 brp,u8 mode)
{
    
    
	GPIO_InitTypeDef GPIO_InitStructure;
	CAN_InitTypeDef        CAN_InitStructure;
	CAN_FilterInitTypeDef  CAN_FilterInitStructure;
	
#if CAN_RX0_INT_ENABLE 
	NVIC_InitTypeDef  		NVIC_InitStructure;
#endif
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); //打开CAN1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   //PA端口时钟打开
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;		//PA11	   
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; 	 //上拉输入模式
	GPIO_Init(GPIOA, &GPIO_InitStructure);	

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;		//PA12	   
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 	 //复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	 //IO口速度为50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//CAN单元设置
   	CAN_InitStructure.CAN_TTCM=DISABLE;	//非时间触发通信模式   
  	CAN_InitStructure.CAN_ABOM=DISABLE;	//软件自动离线管理	  
  	CAN_InitStructure.CAN_AWUM=DISABLE;//睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位)
  	CAN_InitStructure.CAN_NART=ENABLE;	//使用报文自动传送 
  	CAN_InitStructure.CAN_RFLM=DISABLE;	//报文不锁定,新的覆盖旧的  
  	CAN_InitStructure.CAN_TXFP=DISABLE;	//优先级由报文标识符决定 
  	CAN_InitStructure.CAN_Mode= mode;	 //模式设置 
  	CAN_InitStructure.CAN_SJW=tsjw;	//重新同步跳跃宽度(Tsjw)为tsjw+1个时间单位 CAN_SJW_1tq~CAN_SJW_4tq
  	CAN_InitStructure.CAN_BS1=tbs1; //Tbs1范围CAN_BS1_1tq ~CAN_BS1_16tq
  	CAN_InitStructure.CAN_BS2=tbs2;//Tbs2范围CAN_BS2_1tq ~	CAN_BS2_8tq
  	CAN_InitStructure.CAN_Prescaler=brp;  //分频系数(Fdiv)为brp+1	
  	CAN_Init(CAN1, &CAN_InitStructure);   // 初始化CAN1
	
	//配置过滤器
 	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;//过滤器0关联到FIFO0
  	CAN_FilterInitStructure.CAN_FilterActivation=ENABLE; //激活过滤器0
  	CAN_FilterInit(&CAN_FilterInitStructure);//滤波器初始化
	
#if CAN_RX0_INT_ENABLE 
	CAN_ITConfig(CAN1,CAN_IT_FMP0,ENABLE);				//FIFO0消息挂号中断允许.		    

	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;     // 主优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;            // 次优先级为0
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
#endif
}

#if CAN_RX0_INT_ENABLE	//使能RX0中断
//中断服务函数			    
void USB_LP_CAN1_RX0_IRQHandler(void)
{
    
    
	/*参考代码
  CanRxMsg RxMessage;
	int i=0;
    CAN_Receive(CAN1, 0, &RxMessage);
	for(i=0;i<8;i++)
	printf("rxbuf[%d]:%d\r\n",i,RxMessage.Data[i]);
	*/
}
#endif

/*************************************************
*函数名:CAN_Send_Msg
*函数功能:can发送一组数据(固定格式:ID为0X12,标准帧,数据帧)
*输入:len:数据长度(最大为8),msg:数据指针,最大为8个字节.
*返回值:0,成功;其他,失败;
**************************************************/
u8 CAN_Send_Msg(u8* msg,u8 len)
{
    
    	
	u8 mbox;
	u16 i=0;
	CanTxMsg TxMessage;
	TxMessage.StdId=0x12;	 // 标准标识符为0
	TxMessage.ExtId=0x12;	 // 设置扩展标示符(29位)
	TxMessage.IDE=0;		  // 使用扩展标识符
	TxMessage.RTR=0;		  // 消息类型为数据帧,一帧8位
	TxMessage.DLC=len;							 // 发送两帧信息
	for(i=0;i<len;i++)
		TxMessage.Data[i]=msg[i];				 // 第一帧信息          
	mbox= CAN_Transmit(CAN1, &TxMessage);   
	i=0;
	while((CAN_TransmitStatus(CAN1, mbox)==CAN_TxStatus_Failed)&&(i<0XFFF))i++;	//等待发送结束
	if(i>=0XFFF)return 1;
	return 0;		
}

/*************************************************
*函数名:CAN_Receive_Msg
*函数功能:can口接收数据查询
*输入:buf:数据缓存区;
*返回值:0,无数据被收到;其他,接收的数据长度;
**************************************************/
u8 CAN_Receive_Msg(u8 *buf)
{
    
    		   		   
 	u32 i;
	CanRxMsg RxMessage;
    if( CAN_MessagePending(CAN1,CAN_FIFO0)==0)return 0;		//没有接收到数据,直接退出 
    CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);//读取数据	
    for(i=0;i<RxMessage.DLC;i++)
    buf[i]=RxMessage.Data[i];  
	return RxMessage.DLC;	
}

LED.h

#ifndef LED_H_
#define LED_H_

#include "stm32f10x.h"

/* 宏定义LED时钟端口、引脚的定义 */
#define LED0_GPIO_Port   GPIOC
#define LED0_Pin         GPIO_Pin_13
#define LED0_Port_RCC    RCC_APB2Periph_GPIOC

void LED_Init(void);  //LED初始化函数
void LED0_Set(int x); //LED控制函数

#endif

LED.c

#include "LED.h"


/*************************************************
*函数名:    LED_Init
*函数功能:  LED灯初始化函数
*输入:      无
*返回值:    无
**************************************************/
void LED_Init()
{
    
    
	GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量
	
	RCC_APB2PeriphClockCmd(LED0_Port_RCC,ENABLE); //使能时钟
	
	GPIO_InitStructure.GPIO_Pin=LED0_Pin;           //选择设置LED0的IO口
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;  //设置推挽输出模式
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;	//设置传输速率
	GPIO_Init(LED0_GPIO_Port,&GPIO_InitStructure);  /* 初始化GPIO */
	GPIO_SetBits(LED0_GPIO_Port, LED0_Pin);         //输出高电平,LED0熄灭
	
}

/*************************************************
*函数名:     LED0_Set
*函数功能:   LED的控制
*输入:       x:0灯亮,1灯灭
*返回值:     无
**************************************************/
void LED0_Set(int x)
{
    
    
	switch(x)
	{
    
    
		case 0:
			GPIO_ResetBits(LED0_GPIO_Port, LED0_Pin);break;
		case 1:
			GPIO_SetBits(LED0_GPIO_Port, LED0_Pin);break;
	}
}

main.c

#include "Delay.h"
#include "can.h"
#include "LED.h"

int main()
{
    
    
	u8 msg_send[1], msg_recv[1];
	u8 x = 0;
	
	SysTick_Init(72);
	SystemInit();
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //中断优先级分组 分2组
	LED_Init();
	CAN_Mode_Init(CAN_SJW_1tq,CAN_BS2_2tq,CAN_BS1_6tq,9,CAN_Mode_LoopBack);//500Kbps波特率,回环模式
	
	while(1)
	{
    
    
		/************1s循环,当接收的信息等于发送的信息时,LED灯取反否则常亮************/
		Delay_ms(1000);
		
		CAN_Send_Msg(msg_send,1);
		CAN_Receive_Msg(msg_recv);
		if(msg_send[0] == msg_recv[0]+1)
		{
    
    
			x ^= 1;
			LED0_Set(x);
		}
		else
			LED0_Set(0);
		
		msg_send[0] += 1;
	}
}

延时和位带我前面的文章有这里就不列举了


六、cubemx配置CAN总线

(1) 打开cubemx,新建工程,选择自己的芯片。

(2) 配置RCC,选择外部高速时钟
在这里插入图片描述


(3) 配置时钟树
在这里插入图片描述


(4) 配置SYS
在这里插入图片描述


(5) 配置LED
我的是最小系统板,LED是PC13,根据自己的开发板来
在这里插入图片描述


(6) 配置CAN
在这里插入图片描述
波特率我是根据波特率计算器设置的
在这里插入图片描述

Timer Triggered Communication Mode:否使用时间触发功能 (ENABLE/DISABLE),时间触发功能在某些CAN 标准中会使用到。

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

Automatic Wake-Up Mode:用于设置是否使用自动唤醒功能 (ENABLE/DISABLE),使能自动唤醒功能后它会在监测到总线活动后自动唤醒。

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

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

Transmit Fifo Priority:用于设置发送报文的优先级判定方法 (ENABLE/DISABLE),使能时,以报文存入发送邮箱的先后顺序来发送,否则按照报文 ID 的优先级来发送。配置完这些结构体成员后,我们调用库函数 HAL_CAN_Init 即可把这些参数写入到 CAN 控制寄存器中,实现 CAN 的初始化


(7) 工程文件配置并生成工程

在这里插入图片描述


在这里插入图片描述


工程内的程序结构
在这里插入图片描述
can_app.h

#ifndef _can_H
#define _can_H

#include "stm32f1xx_hal.h"
#include "can.h"

#define CAN_RX0_INT_ENABLE 0   //不使用中断

void CAN_Mode_Init(void);

uint8_t CAN_Send_Msg(uint8_t* msg,uint8_t len);						//发送数据

int16_t CAN_Receive_Msg(uint8_t *buf);							//接收数据

#endif

can_app.c

#include "can_app.h"

/*************************************************
*函数名:CAN_Mode_Init
*函数功能:can过滤器设置并打开can
*输入:无
*返回值:无
**************************************************/
void CAN_Mode_Init()
{
    
    
	CAN_FilterTypeDef filter = {
    
    0};
	filter.FilterActivation = ENABLE;
	filter.FilterMode = CAN_FILTERMODE_IDMASK;
	filter.FilterScale = CAN_FILTERSCALE_32BIT;
	filter.FilterBank = 0;
	filter.FilterFIFOAssignment = CAN_RX_FIFO0;
	filter.FilterIdLow = 0;
	filter.FilterIdHigh = 0;
	filter.FilterMaskIdLow = 0;
	filter.FilterMaskIdHigh = 0;
	filter.SlaveStartFilterBank = 14;
	HAL_CAN_ConfigFilter(&hcan, &filter);

	HAL_CAN_Start(&hcan);

	#if CAN_RX0_INT_ENABLE
	HAL_CAN_ActivateNotification(&hcan1,CAN_IT_RX_FIFO0_MSG_PENDING);
	#endif
}

/*************************************************
*函数名:CAN_Send_Msg
*函数功能:can发送一组数据(固定格式:ID为0X12,标准帧,数据帧)
*输入:len:数据长度(最大为8),msg:数据指针,最大为8个字节.
*返回值:0,成功;其他,失败;
**************************************************/
uint8_t CAN_Send_Msg(uint8_t* msg,uint8_t len)
{
    
    
	uint16_t i=0;
	uint32_t msg_box;  //存储发送函数的返回值
	uint8_t send_buf[8] = {
    
    0};
	
	CAN_TxHeaderTypeDef TxMessage;
	TxMessage.StdId=0X12;	 // 标准标识符为0
	TxMessage.ExtId=0X12;	 // 设置扩展标示符(29位)
	TxMessage.IDE=CAN_ID_STD;		  // 使用扩展标识符
	TxMessage.RTR=CAN_RTR_DATA;		  // 消息类型为数据帧,一帧8位
	TxMessage.DLC=len;							 // 发送两帧信息
	TxMessage.TransmitGlobalTime = DISABLE;
	for(i=0;i<len;i++)
		send_buf[i]=msg[i];				 // 第一帧信息          
	HAL_CAN_AddTxMessage(&hcan,&TxMessage,send_buf,&msg_box);
	return 0;		
}

/*************************************************
*函数名:CAN_Receive_Msg
*函数功能:can口接收数据查询
*输入:buf:数据缓存区;
*返回值:0,无数据被收到;-1,接收错误,其他,接收的数据长度;
**************************************************/
int16_t CAN_Receive_Msg(uint8_t *buf)
{
    
    
	uint32_t i;
	uint8_t recv_data[8];
	CAN_RxHeaderTypeDef RxMessage;
	
	while(HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_RX_FIFO0) != 0) //循环等待接收
	{
    
    
		if (__HAL_CAN_GET_FLAG(&hcan, CAN_FLAG_FOV0) != RESET) //接收超时退出
      return -1;
		
		HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &RxMessage, recv_data);
		for(i=0;i<RxMessage.DLC;i++)
			buf[i]=recv_data[i];
	}
 
	return RxMessage.DLC;
}

#if CAN_RX0_INT_ENABLE
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    
    
	//中断内容
}
#endif

main.c
在这里插入图片描述
在这里插入图片描述

CAN_Mode_Init();
uint8_t msg_send[1], msg_recv[1];

在这里插入图片描述

/************1s循环,当接收的信息等于发送的信息时,LED灯取反否则常亮************/
	HAL_Delay(1000);
	
	CAN_Send_Msg(msg_send,1);
	CAN_Receive_Msg(msg_recv);
	if(msg_send[0] == msg_recv[0]+1)
	{
    
    
		HAL_GPIO_TogglePin(GPIOC, LED_Pin);
	}
	else
		HAL_GPIO_WritePin(GPIOC,LED_Pin,GPIO_PIN_RESET);
	
	msg_send[0] += 1;

七、实验现象

采用ST-Link连接STM32F103烧入程序
在这里插入图片描述

程序的功能就是使用CAN的回环模式,当发送的数据与接收的数据相当等时,LED灯闪烁,否则LED等常亮,标准固件库与HAL库实现的功能都是一样的,因此现象也一样,具体现象如下。
在这里插入图片描述
LED灯正常闪烁,说明STM32CAN控制器的回环检测正常,实验结束。


在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_51447215/article/details/129915252