本篇介绍如何使用STM32的标准库与HAL库对CAN总线的使用,由于我那块普中的STM32F103ZET6开发板送人了,因此我这边用STM32F103C8最小系统板,进行简单的演示一下功能,最主要的还是需要理解CAN总线的原理,在本篇中,我会尽量把原理简化,便于理解,实在觉得困难的话就多看几遍,并且在结合一下其他的视频教程
CAN 是控制器局域网络 (Controller Area Network) 的简称,它是由研发和生产汽车电子产品著称的德国 BOSCH 公司开发的,并最终成为国际标准(ISO11519以及ISO11898,我简称519,898),是国际上应用最广泛的现场总线之一。其差异点如下图所示:
CAN 通讯是一种异步通讯,只具有 CAN_High 和 CAN_Low 两条信号线,共同构成一组差分信号线,以差分信号的形式进行通讯。其连接示意图如下:
可以看到,CPU使用 CAN控制器 将数据通过 Tx线 发送给 CAN收发器,CAN收发器将信号转化为差分信号传递到网络,
打个比方,串口将数据转换为TTL信号通过Tx端将信号发送出去,一般如果我们电脑要接收这些数据的话,需要一个转换芯片(CH340)将TTL信号转化为电脑可以识别的信号。同样我们也需要一个芯片(CAN收发器)将Tx端发送过来的信号转化为差分信号发送到 CAN总线网络,对于接收信号也是一样。
也就是说CAN控制器是集成在芯片内部的,以引脚的方式引出发送端(Tx)和接收端(Rx),CAN收发器需要自己接,一般的开发板上都会有CAN收发器,但本次使用的最小系统板没有。
下图中的 CAN 通讯网络是遵循 ISO11898 标准的高速、短距离 的闭环网络,它的总线最大长度为 40m,通信速度最高为 1Mbps,总线的两端各要求有一个“120 欧”的电阻。
下图中的是遵循 ISO11519-2 标准的低速、远距离“开环网络”,它的最大传输距离为 1km,最高通讯速率为 125kbps,两根总线是独立的、不形成闭环,要求每根总线上各串联有一个“2.2千欧”的电阻。
是闭环还是开环,看自己的开发板原理图。
从上面就可以看到,CAN总线上是一个个通信节点间的通信,CAN 通信节点由一个 CAN 控制器 及 CAN 收发器组成,各个节点间使用差分信号相互通信。
差分信号需要两根信号线,通过两根信号线的电压差值来表示 逻辑 0 和 逻辑 1。
差分信号有 抗干扰能力强,少电磁干扰的优点。
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总线差分信号对比,主要是电压不同。
由于 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。
为了使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 极限值较大时,可以吸收的误差加大,但通讯的速度会下降。
而 CAN 使用的是两条差分信号线,只能表达一个信号,简洁的物理层决定了 CAN 必然要配上一套更复杂的协议,CAN总线协议将数据、操作命令 (如读/写) 以及同步信号进行打包,打包后的这些内容称为报文或者说帧。
为了更有效地控制通讯,CAN 一共规定了 5 种类型的帧,它们的类型及用途说明如表
数据帧是在 CAN 通讯中最主要、最复杂的报文,了解清楚清楚了数据帧,那么对其他种类报文的理解就会简单许多,我们来了解它的结构,如下图所示。
数据帧以一个显性位 (逻辑 0) 开始,以 7 个连续的隐性位 (逻辑 1) 结束,在它们之间,分别有仲裁段、控制段、数据段、CRC 段和 ACK 段。下面讲解各个段的内容。
仲裁段的内容主要为本数据帧的 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 相同的标准格式报文与扩展格式报文中,标准格式的优先级较高。
控制段
在控制段中的 r1 和 r0位 为保留位,默认设置为显性位。它最主要的是 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 个隐性位表示结束。
STM32 的芯片中具有 bxCAN 控制器,它支持 CAN 协议 2.0A 和 2.0B 标准。该 CAN 控制器支持最高的通讯速率为 1Mb/s;可以自动地接收和发送 CAN 报文,支持使用标准ID 和扩展 ID 的报文;外设中具有 3 个发送邮箱,发送报文的优先级可以使用软件控制,还可以记录发送的时间;具有 2 个 3 级深度的接收 FIFO,可使用过滤功能只接收或不接收某些 ID 号的报文;可配置成自动重发;不支持使用 DMA 进行数据收发。框架示意图如下:
STM32 的有两组 CAN 控制器,其中 CAN1 是主设备,框图中的“存储访问控制器”是由 CAN1控制的,CAN2 无法直接访问存储区域。
CAN 控制内核包含了各种控制寄存器及状态寄存器,我们主要讲解其中的主控制寄存器 CAN_MCR 及位时序寄存器 CAN_BTR,想要详细了解的朋友可以查看STM32F1中文参考手册。
主控制寄存器 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’。
CAN 外设中的位时序寄存器 CAN_BTR 用于配置测试模式、波特率以及各种位内的段参数。其各个位的功能如下。
0:正常工作
1:静默模式
位30 LBKM:环回模式(调试)
0:禁止环回模式
1:使能环回模式
STM32 的 CAN 提供了测试模式,配置位时序寄存器 CAN_BTR 的 SILM 及 LBKM寄存器位可以控制使用正常模式、静默模式、回环模式及静默回环模式,其各个模式的功能如下图所示。
位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通信波特率计算器
回到图 中的 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位扩展标识符。
图 中的 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 的内存中
在 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的位可以不同。
(1)使能CAN时钟,将对应引脚复用映射为CAN功能
(2)设置CAN工作模式、波特率等
(3)设置CAN筛选器
(4)选择CAN中断类型,开启中断(可选)
(5)CAN发送和接收消息
(6)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的值,可得到波特率,根据波特率计算器得到
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)。
void CAN_ITConfig(CAN_TypeDef* CANx, uint32_t CAN_IT, FunctionalState NewState);
发送消息的函数
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
关于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;
}
}
延时和位带我前面的文章有这里就不列举了
(1) 打开cubemx,新建工程,选择自己的芯片。
(5) 配置LED。
我的是最小系统板,LED是PC13,根据自己的开发板来
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 的初始化
#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
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控制器的回环检测正常,实验结束。