首页 > 编程语言 >八万字解析CAN总线协议·从入门到实战保姆级教学(源码可直接移植使用)

八万字解析CAN总线协议·从入门到实战保姆级教学(源码可直接移植使用)

时间:2024-11-21 13:42:45浏览次数:3  
标签:八万 总线 发送 源码 InitStructure RTR GPIO ID

目录

1.  简介

2.  CAN物理层特性

2.1  CAN总线拓扑图

2.2  CAN硬件电路

2.3  CAN电平标准

2.4  CAN收发器-TJA1050(高速CAN)

2.5  ISO 11898与11519-2对比

3.  CAN总线数据帧格式

3.1  数据帧

3.1.1  帧起始

3.1.2  仲裁段

3.1.3  控制段

3.1.4  数据段

3.1.5  CRC 段

3.1.6  ACK段

3.1.7  帧结束

3.1.8  总结

3.2  遥控帧

3.3  错误帧

3.3.1  错误标志

3.3.2  错误界定符

3.4  过载帧

3.5  帧间隔

3.6  位填充

4.  位同步

4.1  位时序

4.2  硬同步

4.3  再同步

4.4  波特率计算

5.  仲裁规则

5.1  先占先得

5.2  非破坏性仲裁

6.  有关STM32的CAN外设简介

6.1  CAN网拓扑结构

6.2  引脚定义

6.3  CAN框图

6.4  发送流程

6.5  接收流程

6.6  标识符过滤器

6.6.1  2个32位过滤器-标识符列表

6.6.2  4个16位过滤器-标识符列表

6.6.3  1个32位过滤器-标识符屏蔽

6.6.4  2个16位过滤器-标识符屏蔽

6.7  测试模式

6.7.1  静默模式

6.7.2  环回模式

6.7.3  环回静默模式

6.8  工作模式

6.8.1  初始化模式

6.8.2  正常模式

6.8.3  睡眠模式

6.9  位时间特性

6.10  中断

6.10.1  发送中断

6.10.2  FIFO 0中断

6.10.3  FIFO 1中断

6.10.4  错误和状态变化中断

6.11  错误处理和离线恢复

7.  CAN外设简介

7.1  CAN网拓扑结构

7.2  引脚定义

7.3  CAN框图

7.4  发送流程

7.5  接收流程

7.6  标识符过滤器

1.6.1  2个32位过滤器-标识符列表

1.6.2  4个16位过滤器-标识符列表

1.6.3  1个32位过滤器-标识符屏蔽

1.6.4  2个16位过滤器-标识符屏蔽

1.7  测试模式

1.7.1  静默模式

1.7.2  环回模式

1.7.3  环回静默模式

1.8  工作模式

1.8.1  初始化模式

1.8.2  正常模式

1.8.3  睡眠模式

1.9  位时间特性

1.10  中断

1.10.1  发送中断

1.10.2  FIFO 0中断

1.10.3  FIFO 1中断

1.10.4  错误和状态变化中断

7.11  错误处理和离线恢复

8.  STM32库函数解读

8.1  初始化相关

8.2  发送相关

8.3  接收相关

8.4  工作模式转换相关

8.5  错误管理相关

8.6  中断相关

9.  代码编写

9.1  环回测试代码编写

9.1.1  GPIO口初始化

9.1.2  模式配置

9.1.3  过滤器配置

9.1.4  初始化整合

9.1.5  发送函数

9.1.6  接收函数

9.1.7  主函数

9.2  设备间相互通讯代码编写

9.3  接收不同帧格式代码编写

9.3.1  初始化

9.3.2  发送函数

9.3.3  接收函数

9.3.4  主函数

9.4  标识符过滤器代码编写

9.4.1  16位列表模式

9.4.2  16位屏蔽模式

9.4.3  32位列表模式

9.4.4  32位屏蔽模式

9.5  中断接收代码编写


1.  简介

        CAN总线(Controller Area Network Bus)控制器局域网总线,是由BOSCH公司开发的一种简洁易用、传输速度快、易扩展、可靠性高的串行通信总线,广泛应用于汽车、嵌入式、工业控制等领域。下图是车载网络的构想示意图:

2.  CAN物理层特性

2.1  CAN总线拓扑图

        CAN 控制器根据两根通信线(CAN_H、CAN_L)上的电位差来判断总线电平(差分信号)。总线电平分为显性电平(逻辑0)隐性电平(逻辑1),二者必居其一。发送方通过使总线电平发生变化,将消息发送给接收方。

        差分信号(Differential Signal)是一种信号传输方式,通过两条信号线(通常称为“正”和“负”信号线)来传输信息。信号在这两条线上的电压变化是相反的,分别称为 +V 和 -V(相对于地或参考电平),接收端通过比较这两条信号线之间的电压差来恢复信号。

        对于显性电平(逻辑0)和隐性电平(逻辑1)所对应的逻辑电平为什么相反,在本章2.3小节会有详细介绍。

        CAN总线是:异步,半双工。其无需时钟线,通信速率由设备各自约定,并且由于其半双工通信可挂载多设备,多设备同时发送数据时通过仲裁判断先后顺序。


插点别的东西:

其中对于同步和异步的区别:

同步时钟:指的是在执行操作时,各个参与者(如线程、设备或系统)在同一时刻进行协调。所有的操作都需要在一个共同的时序框架下进行。

异步时钟:步指的是操作之间没有强制的时间协调,各个参与者可以独立地进行操作,不必等待其他操作完成。

对于单工,半双工,全双工的区别:

单工:数据传输仅能沿一个方向,不能实现反向传输,只有一条通信路线;

半双工:数据传输可以沿两个方向,但需要分时进行,也只有一条通信线路;

全双工:数据可以同时进行双向传输,具有两条通信线路。


        CAN总线的传输方式:广播式请求式

        广播式传输方式,你可以理解为一个人拿着大喇叭分配工作(发送方发送数据),其他人都可以听到这个喇叭喊话的声音(接收方接收数据),如果此时喇叭喊“张三”(报文ID),那么张三会站出来(响应)。

        请求式传输方式,也有一个人才分配工作,不过这个人不想拿着大喇叭主动吆喝别人过来(不主动发送数据),而是假如李四过来问能不能给我分配一个工作(接收方先请求),分配工作的人说可以,即可对李四分配工作(发送方发送数据)。

2.2  CAN硬件电路

        在了解CAN总线的硬件电路之前我们先回顾一下其他通信协议:

名称引脚双工时钟电平设备
USARTTX、RX全双工异步单端点对点
IICSCL、SDA半双工同步单端多设备
SPISCLK、MOSI、MISO、CS全双工同步单端多设备
CANCAN_H、CAN_L半双工异步差分多设备
USBDP、DM半双工异步差分点对点

串口硬件连接电路:

IIC硬件连接电路:

SPI硬件连接电路:

        对于CAN总线的硬件电路,每个设备通过CAN收发器挂载在CAN总线网络上,CAN控制器引出的TX和RX与CAN收发器相连,CAN收发器引出的CAN_H和CAN_L分别与总线的CAN_H和CAN_L相连。CAN总线可分为高速CAN总线和低速CAN总线。

        高速CAN使用闭环网络,CAN_H和CAN_L两端添加120Ω的终端电阻,数据传输速率125k~1Mbps,传输距离小于40m:

        其中两个120Ω终端电阻的作用:

①防止回波反射,使波形更平稳;若是不加中断电阻,反射波形可能如褐色所示,加入中断电阻如紫色所示:

②在没有设备操作时,使电压“收紧”,使其电压一致。这里的原理可以类比IIC通信的上拉电阻

嵌入式面试八股文(一)·define和const的区别以及IIC为什么要加上拉电阻,为什么使用开漏输出-CSDN博客

         CAN总线是差分信号,所以终端电阻可以在没有设备操作时将两根线“收紧”至电压一致状态,代表就是默认的逻辑电平“1”,当设备想要“0”时,就去把总线拉开,当设备想要“1”时,就不去碰总线,在终端电阻的作用下,自动恢复到“1”:

        低速CAN使用开环网络,CAN_H和CAN_L其中一端添加2.2kΩ的终端电阻,数据传输速率10k~125kbps,传输距离小于1Km:

2.3  CAN电平标准

        CAN总线采用差分信号,即两线电压差(V_{CAN_H}​-V_{CAN_L}​)传输数据位,前面我们也了解了,CAN总线的信号分为高速和低速两种:

对于高速CAN规定:压差为0V时表示逻辑1(隐性电平)     

                                 电压差为2V时表示逻辑0(显性电平)

         对于这里的显隐性对应的逻辑状态可能会有人不解,因为通常情况下,我们应该显性对应1,隐性对应0,但是这里为什么反着来了呢?

        这里主要是因为显性和隐性所表示的是总线的收紧状态,前面我们了解到CAN总线的默认状态是CAN_L和CAN_H两条总线收紧状态,不需要设备干预所以是隐性电平,又知道总线收紧时其所对应的电平状态为逻辑电平“1”,而总线张开,需要设备干预所以为显性电平,又知道总线张开时所对应的逻辑电平“0”,因此会出现隐性电平和逻辑“1”绑定,显性电平和逻辑“0”绑定。

        简单点来说,显性和隐性表示的是总线的状态,并不是逻辑电平状态。

对于低速CAN规定:电压差为-1.5V时表示逻辑1(隐性电平)     

                                 电压差为3V时表示逻辑0(显性电平)

2.4  CAN收发器-TJA1050(高速CAN)

​         其数据手册引脚介绍: 

​        简单翻译一下:

名称引脚描述
TXD1传输数据输入,从CAN控制器读入数据到总线驱动器
GND2电源地
VCC3电源电压
RXD4传输数据输出,从CAN控制器读出数据到总线驱动器
Vref5参考电压输入
CAN_L6低电平CAN总线
CAN_H7高电平CAN总线
S8选择输入高速/静默模式(相当于一个开关,可不用)

         其外部电路:

        其芯片内部框图如下:

​工作原理:

        首先是CANH和CANL,他们是CAN总线的两个差分信号线,黄色框住的部分是个接收器,可以时刻检测总线电压差,并输出“1”或者“0”到左边那根线,如果总线有电压差就会输出“1”,如果没有电压差就会输出“0”(这里没有错,这里的压差是芯片内部的压差,走过芯片会有反向,下面就介绍了):

        然后输出的“1”或者“0”会通过两个场效应管的输出驱动器,输出到RXD引脚,当输入为“1”时,上管断开,下关导通,输出为“0”,如下图蓝色所示:

        当输入为“0”时,上管导通,下关断开,输出为“1”,如下图绿色所示:


        对于场效应管,可以简单理解为,源极是电流输入端,漏极是电流输出端,栅极是控制源极与漏极之间电流流动的电极,我们通过控制栅极的电压,来确定二者的导通:

        这里只做一个简单说明,想要了解更多,可以搜一下场效应管的工作原理。


         当TXD给“1”时,驱动器会将后面两个场效应管断开,相当于不对总线进行任何操作,总线在终端电阻的收紧作用下,默认隐性电平:

        这里需要注意一点的是,芯片内部在CANL和CANH两条差分信号线之间有两个25KΩ的电阻:

        通过这两个电阻可以将CANL和CANH的电平拉倒0.5Vcc,也就是2.5V左右:

         当TXD给“0”时,驱动器会将后面两个场效应管导通,这样上面的Vcc会将CANH拉高,而下面的GND会将CANL拉低,这样就会产生电压差,呈现显性电平的状态:

        对于下图框住的部分,其起到类似上拉电阻的作用,当TXD悬空,通过其上拉,可以防止输入引脚不确定造成误操作:

2.5  ISO 11898与11519-2对比

物理层ISO 11898(High speed)ISO 11519-2(Low speed)
通信速度最高1Mbps最高125kbps
总线最大长度40m/1Mbps1km/40kbps
总线单元最大30最大20
总线拓扑隐性显性隐性显性
MinNomMaxMinNomMaxMinNomMaxMinNomMax
CAN_High(V)2.002.503.002.753.504.501.601.751.903.854.005.00
CAN_Low(V)2.002.503.000.501.502.253.103.253.400.001.001.15
电位差(H-L)(V)-0.500.051.52.03.0-0.3-1.5-0.33.00-

双绞线(屏蔽/非屏蔽)

闭环总线

阻抗(Z): 120Ω (Min.85Ω Max.130Ω)

总线电阻率(r): 70mΩ/m

总线延迟时间: 5ns/m

终端电阻: 120Ω (Min.85Ω Max.130Ω)

双绞线(屏蔽/非屏蔽)

开环总线

阻抗(Z):120Ω (Min.85Ω Max.130Ω)

总线电阻率(Г): 90mΩ/m

总线延迟时间: 5ns/m

终端电阻: 2.20kΩ (Min.2.09kΩ

Max.2.31kΩ)

CAN_L与GND间静电容量 30pF/m

CAN_H与GND间静电容量 30pF/m

CAN_L与GND间静电容量 30pF/m

3.  CAN总线数据帧格式

帧类型用途
数据帧发送设备主动发送数据
遥控帧接收设备主动请求数据(请求式)
错误帧某个设备检测出错误时向其他设备通知错误
过载帧接收设备通知其尚未做好接收准备
帧间隔用于将数据帧及遥控帧与前面的帧分离开

3.1  数据帧

        数据帧由 7 个不同的位场组成:帧起始、仲裁场、控制场、数据场、CRC 场、应答场、帧结尾。数据场的长度可以为 0。

        上图D和R分别代表:显性(0):Dominant(D),隐性(1):Recessive(R)。

        上图表示在灰色部分只能发显性电平“0”,紫色部分可以放显性电平“0”或者隐性电平“1”,白色部分表示只能发隐性电平“1”。

        其中黄框框住的部分表示的是这一段波形占多少位,例如1,表示1位,11表示11位,数组并不代表高低电平,电平是通过颜色来区分的:

扩展格式是后来新增的,扩展格式增加了ID位数,能承载更多种类的ID, 

3.1.1  帧起始

        它标志数据帧和远程帧的起始,由一个单独的“显性”位组成。只在总线空闲时,才允许站开始发送(信号)。

        这里帧起始为显性电平(逻辑0)的原因,我们之前也说过CAN总线默认电平位逻辑1,若是此时起始帧也是隐性电平(逻辑1),那么并不知道什么时候开始发送的数据,因为你的默认状态和帧起始状态一致都是隐性电平,因此帧起始必须是显性电平(逻辑0),作用是打破总线空闲,开始一帧数据:

3.1.2  仲裁段

        仲裁段包括识别符和远程发送请求位(RTR)。

        识别符:识别符的长度为 11 位。这些位的发送顺序是从 ID-10 到 ID-0。最低位是 ID-0。最高的 7 位(ID-10 到 ID-4)必须不能全是“隐性”。

        RTR 位:该位在数据帧里必须为“显性”,而在远程帧(遥控帧)里必须为“隐性”。主要是为了区分数据帧和遥控帧。

        同种帧格式下的ID不能相同,例如数据帧已经有一个ID为0x111,那么同数据帧格式下,就不能在取0x111,不过遥控帧还能取0x111,可以通过RTR位进行区分。

        不过需要注意,相同ID的数据帧和遥控帧,数据帧的优先级大于遥控帧。

        除此之外,仲裁段的表现形式,在标准格式和扩展格式下也不同:

         仲裁段的ID主要是为了区分数据,并且还用于区分优先级,当多个设备同时发送时,根据仲裁规则,ID小的报文优先发送,ID打的报文等待下一次总线空闲在重试发送,

        标准格式的 ID 有 11 个位。从 ID28 到 ID18 被依次发送。禁止高 7 位都为隐性。(禁止设定:ID=1111111XXXX)
        扩展格式的 ID 有 29 个位。基本 ID 从 ID28 到 ID18,扩展 ID 由 ID17 到 ID0 表示。基本 ID 和标准格式的 ID 相同。禁止高 7 位都为隐性。(禁止设定:基本 ID=1111111XXXX) 

3.1.3  控制段

        控制场由 6 个位组成,包括数据长度代码和两个将来作为扩展用的保留位。所发送的保留位必须为“显性”。接收器可以接收由“显性”和“隐性”任意组合在一起的电平。

        这里的两个保留位,其实已经被用掉一个了,那就是IDE,IDE原来是r1位,不过后来升级给使用掉了,现在IDE位主要是为了区分升级前的版本还是升级后的版本。

        同样的,控制段在标准格式和扩展格式的构成也有所不同:

        数据长度代码:数据长度代码指示了数据场中字节数量。数据长度字节数必须为0~8字节,但接收方对 DLC = 9~15 的情况并不视为错误。

数据字节数数据长度码
DLC3DLC2DLC1DLC0
0显性电平显性电平显性电平显性电平
1显性电平显性电平显性电平隐性电平
2显性电平显性电平隐性电平显性电平
3显性电平显性电平隐性电平隐性电平
4显性电平隐性电平显性电平显性电平
5显性电平隐性电平显性电平隐性电平
6显性电平隐性电平隐性电平显性电平
7显性电平隐性电平隐性电平隐性电平
8隐性电平显性电平显性电平显性电平

3.1.4  数据段

        数据段可包含 0~8 个字节的数据。从 MSB(最高位)开始输出。

3.1.5  CRC 段

        CRC 段是检查帧传输错误的帧。由 15 个位的 CRC 顺序和 1 个位的 CRC 界定符(用于分隔的位)构成。

3.1.6  ACK段

        ACK 段用来确认是否正常接收。由 ACK 槽(ACK Slot)和 ACK 界定符 2 个位构成。

        ACK槽的作用就是应答,发送方发送一帧数据,但他如何判断到底有没有设备收到呢?这需要靠ACK位来实现,发送方在发完一帧的主要内容后,在应答这一位时发送方释放总线,总线回归默认状态(隐性1),如果接收方收到数据,就会在ACK槽这一位主动出击,把总线在拉开使总线呈现显性0的状态。

        发送方那个释放总线后,在ACK槽会读取总线状态,如果发送方读取显性0,那就证明有接收方接收数据,把总线拉开了,呈现显性0的状态;如果发送方读取隐性1,发送方就会认为没人读取到总线数据,说明发送失败,发送方可以配置自动重发,也可以不管,总之发送方需要知道发送状态就行。

        发送单元的 ACK 段:发送单元在 ACK 段发送 2 个位的隐性位。
        接收单元的 ACK 段:接收到正确消息的单元在 ACK 槽(ACK Slot)发送显性位,通知发送单元正常接收结束。这称作“发送 ACK”或者“返回 ACK”。

发送 ACK 的是在既不处于总线关闭态也不处于休眠态的所有接收单元中,接收到正常消息的单元 (发送单元不发送 ACK)。所谓正常消息是指不含填充错误、格式错误、CRC 错误的消息。

3.1.7  帧结束

        帧结束是表示该该帧的结束的段。由 7 个位的隐性位构成。

3.1.8  总结

帧起始:数据帧开始的段(默认隐性电平“0”)。

仲裁段:区分数据功能,相当于一个数据的 ID 号,根据 ID 区分不同功能数据,并且根据大小区分帧优先级(帧ID小的优先级最高;不同类型数据同ID,数据帧大于遥控帧)。

               RTR:远程请求位,用于区分数据帧还是遥控帧。

               SRR:替代RTR,协议升级留下的无意义的位。


        在总线空闲态,最先开始发送消息的单元获得发送权。

        多个单元同时开始发送时,各发送单元从仲裁段的第一位开始进行仲裁。连续输出显性电平(逻辑0)最多的单元可继续发送。(这也就是为什么最小的优先级最高的原因,详细请看本章5)

数据帧和遥控帧的优先级:具有相同 ID 的数据帧和遥控帧在总线上竞争时,仲裁段的最后一位(RTR)为显性位的数据帧具有优先权,可继续发送。

标准格式和扩展格式的优先级:标准格式 ID 与具有相同 ID 的遥控帧或者扩展格式的数据帧在总线上竞争时,标准格式的 RTR 位为显性位的具有优先权,可继续发送。


控制段:用于指定数据段的长度,并且留下两个保留位(IDE,r0)。

               IDE:扩展标志位,用于区分扩展格式还是标准格式。

               r0/r1:保留位。

               DLC:指示数据有几个字节。

数据段:数据内容,可发送0~8个字节的数据。

CRC段:用于检查帧是否在传输过程发生错误,校验数据帧是否正确。

ACK段:判断是否正常接收。

               CRC/ACK界定符:为应答位前后发送方和接收方释放总线留下时间。

帧结束:数据帧结束的段。

        这里说一下扩展格式和标准格式的区别,扩展格式的出现,是因为标准格式仲裁段的11位ID不够用了,因此进行扩展出了18位的ID,到时为了保证协议不会出现大的出入,因此对RTR位和IDE位进行了修改:

        其中的SRR位,是替代的标准格式的RTR位(这里插一下,下面会想起介绍仲裁,这里简单提一下,仲裁是先比较ID,后比较RTR,所以RTR必须在所有ID的后面),这里因为又加了18位的ID,因此RTR需要移到这18位ID的后面,这样就空出来了SRR的位置且此位置必须是隐性电平1(为了保证标准格式的优先级高于扩展格式的设计)。

        IDE位,也就是控制段留下的两个保留位中的一位,这时就排上了用场,以前的控制段时r0和r1,此时将r1位拿出,赋予其新功能IDE位,用于区分是标准格式还是扩展格式,标准格式为显性0,扩展格式为隐性1。这样当数据走到IDE位,就知道后续是按照标准格式去判断还是按照扩展格式去判断(这里需要注意的一点是扩展格式的RTR位以后保留位已经恢复的r1,个人认为这一位相当于为后续如果扩展ID也不够用了的时候,进行进一步拓展保留的,继续套娃)。

3.2  遥控帧

        接收单元向发送单元请求发送数据所用的帧。遥控帧由6个段组成,遥控帧没有数据帧的数据段,RTR为隐性电平1,其他部分与数据帧相同。

        遥控帧相比于数据帧,数据帧是发送方,主动、连续、有间隔的向外广播数据,不过这样会出现要是发送方一直广播,但是接收方只是隔一会才要一个数据,这样会造成资源的浪费。

        而遥控帧则是接收方主动请求发送方发送广播数据,接收方进行接收,不过这也会出现一个问题就是发送方需要等接收方给它发送请求,才会发送,要是需要高速处理的数据,会占用大量CPU资源。

        因此遥控帧多适用于数据频率较慢的情况,数据帧多适用于发送方数据发送频率较快的情况。

3.3  错误帧

        用于在接收和发送消息时检测出错误通知错误的帧。一旦发现“位错误”或“填充错误”或“CRC错误”或“格式错误”或“应答错误” ,这些设备便会发出错误帧来破坏数据,同时终止当前的发送设备。错误帧由错误标志和错误界定符构成:

错误的种类错误的内容错误的检测帧(段)检测单元
位错误比较输出电平和总线电平(不含填充位),当两电平不一样时所检测到的错误。• 数据帧(SOF∼EOF)
• 遥控帧(SOF∼EOF)
• 错误帧
• 过载帧

发送单元

接收单元

填充错误在需要位填充的段内,连续检测到 6 位相同的电平时所检测到的错误。• 数据帧(SOF∼CRC 顺序)
• 遥控帧(SOF∼CRC 顺序)

发送单元

接收单元

CRC错误从接收到的数据计算出的 CRC 结果与接收到的 CRC 顺序不同时所检测到的错误。• 数据帧(CRC 顺序)
• 遥控帧(CRC 顺序)
接收单元
格式错误检测出与固定格式的位段相反的格式时所检测到的错误。• 数据帧
(CRC 界定符、ACK 界定符、EOF)
• 遥控帧
(CRC 界定符、ACK 界定符、EOF)
• 错误界定符
• 过载界定符
接收单元
应答错误发送单元在ACK槽(ACK Slot)中检测出隐性电平时所检测到的错误(ACK没被传送过来时所检测到的错误)。• 数据帧(ACK 槽)
• 遥控帧(ACK 槽)
发送单元

3.3.1  错误标志

        包括主动错误标志被动错误标志两种。

主动错误标志:

        处于主动错误状态的单元检测出错误时输出的错误标志。设备默认处于出于主动错误状态。

        当设备检测到发生错误,会连续发送6个性位“0”,显性是拉开总线,总选又一个被拉开,就必然处于显性状态(“线与”特性,“0”和“1”相遇时,肯定显示“0”),这样就会破坏掉总线数据,其他设备检测到数据错误就会抛弃当前数据(所有数据全抛弃),若是主动错误产生太过频繁,说明这个设备不太可靠,设备就会进入被动错误状态。

被动错误标志:

        处于被动错误状态的单元检测出错误时输出的错误标志。

        当设备检测到发生错误,会连续发送6个性位。隐性就是不去碰总线,那么总线状态就不会发生变化,就不会总线上别人发的数据,只会破坏自己发的数据(仅抛弃自己的数据)。

3.3.2  错误界定符

        由8个隐性位构成。

3.4  过载帧

        过载帧是用于接收单元通知其尚未完成接收准备的帧。当接收方收到大量数据而无法处理时,其可以发出过载帧,延缓发送方的数据发送,以平衡总线负载,避免数据丢失。过载帧由过载标志和过载界定符构成:

过载标志:6 个位的显性位。过载标志的构成与主动错误标志的构成相同。
过载界定符:8 个位的隐性位。过载界定符的构成与错误界定符的构成相同。

3.5  帧间隔

        帧间隔是用于分隔数据帧和遥控帧的帧。数据帧和遥控帧可通过插入帧间隔将本帧与前面的任何帧(数据帧、遥控帧、错误帧、过载帧)分开。过载帧和错误帧前不能插入帧间隔。

间隔:3 个位的隐性位。
总线空闲:隐性电平,无长度限制(0 亦可)。本状态下,可视为总线空闲,要发送的单元可开始访问总线。
延迟传送(发送暂时停止):8 个位的隐性位。只在处于被动错误状态的单元刚发送一个消息后的帧间隔中包含的段。

3.6  位填充

        位填充是为防止突发错误而设定的功能。发送方每发送5个相同电平后,自动追加一个相反电平的填充位,接收方检测到填充位时,会自动移除填充位,恢复原始数据。

发送单元的工作:在发送数据帧和遥控帧时,SOF~CRC 段间的数据,相同电平如果持续 5 位,在下一个位(第 6 个位)则要插入 1 位与前 5 位反型的电平。
接收单元的工作:在接收数据帧和遥控帧时,SOF~CRC 段间的数据,相同电平如果持续 5 位,需要删除下一个位(第 6 个位)再接收。如果这个第 6 个位的电平与前 5 位相同,将被视为错误并发送错误帧。

举个例子:

举例一举例二举例三
即将发送100000110 100000111100111111111110
实际发送10000011101000001111100011111011111010
实际接收10000011101000001111100011111011111010
移除填充后100000110100000111100111111111110

位填充的作用:

        增加波形的定时信息,利于接收方执行“再同步”,防止波形长时间无变化,导致接收方不能精确掌握数据采样时机。

举个例子,下面一句话“啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊”你告诉别人把听到的“啊”个数写下来,可能会出现多写或者少写的情况,但是“啊啊啊三啊啊啊三啊啊啊三啊啊啊三啊啊啊三”这样就能更加准去的知道实际发送的“啊”的数量,实际接收把“三”去掉就行了。

        将正常数据流与“错误帧”和“过载帧”区分开,标志“错误帧”和“过载帧”的特异性。

        保持CAN总线在发送正常数据流时的活跃状态,防止被误认为总线空闲。

4.  位同步

        CAN总线没有时钟线,总线上的所有设备通过约定波特率的方式确定每一个数据位的时长,发送方以约定的位时长每隔固定时间输出一个数据位接收方以约定的位时长每隔固定时间采样总线的电平,输入一个数据位,理想状态下,接收方能依次采样到发送方发出的每个数据位,且采样点位于数据位中心附近。接收方数据采样需要注意的是:

        问题一:接收方以约定的位时长进行采样,但是采样点没有对齐数据位中心附近如绿色所示部分,假如刚好对应跳变沿,可能就无法区分此时信号是“0”还是“1”,解决方法也很简单,首先这个问题的关键,就是采样点的初始位置没对齐,如果我们以第一次跳变沿位信号,延迟半个数据位左右的时间,进行第一次的数据采样,这样后续就会正常,这样的解决方法也叫“硬同步”(下面会有详细介绍):

        问题二:接收方刚开始采样正确,但是时钟有误差,随着误差积累,采样点逐渐偏离。开始的采样点正常,但是二者的时钟有偏差,这样将会导致开始的时候数据还能正常采集,但是随着时间的累计,偏差越来越大,这将会导致采样点严重偏离预定位置,解决方法,我们可以看到如果数据偏离太过严重,这一次的采样时间,距离数据跳变过于久了,那么我们可以根据这缩短采样时间,同理要是采样时间过快造成的偏差,我们也可以延长采样时间(再同步):

4.1  位时序

        为了灵活调整每个采样点的位置,使采样点对齐数据位中心附近,CAN总线对每一个数据位的时长进行了更细的划分,分为:同步段(SS)

                                                      传播时间段(PTS)

                                                      相位缓冲段1(PBS1)

                                                      相位缓冲段2(PBS2)

        每个段又由若干个最小时间单位(Tq)构成。

段名称段的作用Tq数
同步段(SS: Synchronization Segment)多个连接在总线上的单元通过此段实现时序调整,同步进行接收和发送的工作。由隐性电平到显性电平的边沿或由显性电平到隐性电平边沿最好出现在此段中。1Tq8~25Tq
传播时间段(PTS: Propagation Time Segment)

用于吸收网络上的物理延迟的段。

所谓的网络的物理延迟指发送单元的输出延迟、总线上信号的传播延迟、接收单元的输入延迟。
这个段的时间为以上各延迟时间的和的两倍。

1~8Tq
相位缓冲段 1 (PBS1: Phase Buffer Segment 1)当信号边沿不能被包含于 SS 段中时,可在此段进行补偿。
由于各单元以各自独立的时钟工作,细微的时钟误差会累积起来,PBS 段可用于吸收此误差。
通过对相位缓冲段加减 SJW 吸收误差。SJW 加大后允许误差加大,但通信速度下降。
1~8Tq
相位缓冲段 2 (PBS2: Phase Buffer Segment 2)2~8Tq
再同步补偿宽度(SJW: reSynchronization Jump Width)因时钟频率偏差、传送延迟等,各单元有同步误差。SJW 为补偿此误差的最大值。1~4Tq

采样点:所谓采样点是读取总线电平,并将读到的电平作为位值的点。位置在 PBS1 结束处。

4.2  硬同步

        每个设备都有一个位时序计时周期,当某个设备(发送方)率先发送报文,其他所有设备(接收方)收到SOF的下降沿时,接收方会将自己的位时序计时周期拨到SS段的位置,与发送方的位时序计时周期保持同步,硬同步只在帧的第一个下降沿(SOF下降沿)有效,经过硬同步后,若发送方和接收方的时钟没有误差,则后续所有数据位的采样点必然都会对齐数据位中心附近。

        这个可以理解为谍战剧中,进行秘密行动前的对表工作,无论发送方还是接收方都有一个秒表(时钟),开始行动了发送方和接收方进行校时,如上图,接收方发现发送方说出(SS)时,自己不是,自己马上将秒表纠正,当发送方说开始行动(SS位置)跳变数据开始新的一位(新的动作),接收方在自己秒表转到PBS1和PBS2之间进行采样数据: 

        不过再好的表,也会出现随着时间的推移,不准的情况,这就需要进行再次校准(再同步)。

4.3  再同步

        若发送方或接收方的时钟有误差,随着误差积累,数据位边沿逐渐偏离SS段,则此时接收方根据再同步补偿宽度值(SJW)通过加长PBS1段,或缩短PBS2段,以调整同步,再同步可以发生在第一个下降沿之后的每个数据位跳变边沿

        这里需要注意的是,如下图,SJW=2所代表的不是每次出现误差都需要补偿2Tq,这里的2代表补偿的最大值:

        实际补偿大小是误差大小和SJW共同决定的。如上图,SJW=2Tq,刚好误差也是2Tq,因此刚好补偿2Tq;如果这里误差只有1Tq,且SJW=2Tq,这时实际上只会补偿1Tq;若是误差3Tq,且SJW=2Tq,那么也只会补偿2Tq(只会补偿SJW指定的Tq数)。

4.4  波特率计算

波特率 = 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

5.  仲裁规则

        CAN总线只有一对差分信号线,同一时间只能有一个设备操作总线发送数据,若多个设备同时有发送需求,该如何分配总线资源?

        解决问题的思路:制定资源分配规则,依次满足多个设备的发送需求,确保同一时间只有一个设备操作总线。

        对于多设备发送也有不同的表现形式,首先若是一个数据先发送,已经发送一半了,突然又又一个发送方想要发送数据,要如何解决这个问题呢?

5.1  先占先得

        我们可以规定先占先得,若当前已经有设备正在操作总线发送数据帧/遥控帧,则其他任何设备不能再同时发送数据帧/遥控帧(不过这里需要注意的是,根据错误帧/过载帧的特性,别的发送方也是可以发送错误帧/过载帧破坏当前数据)。

        任何设备检测到连续11个隐性电平,即认为总线空闲,只有在总线空闲时,设备才能发送数据帧/遥控帧。一旦有设备正在发送数据帧/遥控帧,总线就会变为活跃状态,必然不会出现连续11个隐性电平,其他设备自然也不会破坏当前发送。(根据位填充的作用,总线在正常状态下,根本不可能出现11连续的隐性电平,可以看上面有关位填充的介绍)

        若总线活跃状态其他设备有发送需求,则需要等待总线变为空闲,才能执行发送需求。

        这只是错位多个设备发送,若是多个设备同时发送又应当如何解决呢?

5.2  非破坏性仲裁

        若多个设备的发送需求同时到来或因等待而同时到来,则CAN总线协议会根据ID号(仲裁段)进行非破坏性仲裁,ID号小的(优先级高)取到总线控制权,ID号大的(优先级低)仲裁失利后将转入接收状态,等待下一次总线空闲时再尝试发送。

实现非破坏性仲裁需要两个要求:

线与特性:总线上任何一个设备发送显性电平0时,总线就会呈现显性电平0状态,只有当所有设备都发送隐性电平1时,总线才呈现隐性电平1状态,即:0 & X & X = 0,1 & 1 & 1 = 1

回读机制:每个设备发出一个数据位后,都会读回总线当前的电平状态,以确认自己发出的电平是否被真实地发送出去了,根据线与特性,发出0读回必然是0,发出1读回不一定是1

6.  有关STM32的CAN外设简介

        前面我们已经详细介绍了CAN总线,下面我们开始在STM32单片机上对其进行使用,在使用前我们先来了解一下STM32的一些CAN外设。

        STM32内置bxCAN外设(CAN控制器),支持CAN2.0A和2.0B,可以自动发送CAN报文和按照过滤器自动接收指定CAN报文,程序只需处理报文数据而无需关注总线的电平细节。更详细的参数可以看参考手册,这里截了一点其主要特点:

6.1  CAN网拓扑结构

        这里的每个CAN结点都挂载在CAN总线上,对于其中的一个CAN结点,他们是由控制芯片MCU、CAN控制器以及CAN收发器组成,一般情况下CAN控制器都集成在MCU里面,STM32就集成了CAN控制器,STM32引出了CAN_Rx和CAN_Tx引脚,与CAN收发器相连,CAN收发器通过引出的CAN_H和CAN_L与CAN总线相连:

6.2  引脚定义

        这里我们使用的是STM32F103C8T6芯片,其上CAN资源为CAN1,我们查找数据手册,其相关引脚分别为:

6.3  CAN框图

        下面是STM32的CAN外设的框图,其中上面部分是CAN1的,也就是主CAN,下面部分是CAN2的(只有互联型设备才有CAN2),也就是从CAN,我们使用的STM32F103C8T6只有CAN1的外设:

6.4  发送流程

        对于CAN在STM32内部的工作,首先对于发送,当我们想要发送一个报文时,我们只需要将想要发送的报文的各个参数,通过CPU写入到邮箱中,然后给出请求发送的命令,然后发送和接收控制器就会等待总线空闲,然后自动把这个报文广播到总线上:

        这里我们可以看出,其实一个邮箱就能完成上述功能,但是为什么要创建三个呢?内置三个邮箱邮箱的原因,实际上是为了防止总线繁忙造成发送拥堵,举个例子:

        假如你写入了一个报文存储到邮箱0,让控制器将数据发出去,但是此时总线一直处于繁忙状态,控制器一直等不到总线空闲,数据无法发出会滞留在邮箱内,当CPU想发出下一个数据,就会发现邮箱满了没地方存储,这样CPU也会处于等待状态,这样就会造成拥堵:

        STM32内置3个邮箱,当总线拥堵,邮箱0数据未发送出去,但是CPU想继续发送数据,这时就可以在邮箱1或者邮箱2中任意找一个邮箱进行存储数据,等待总线空闲进行发出:

        这里可能会说万一邮箱全满了怎么办?一般情况下总线不会出现如此拥堵的情况,如果出现了只能要么CPU等待,要么CPU放弃本次数据存储。

        当两个或者三个邮箱都有数据要发送,那么会先发哪一个呢?这里可以进行配置:现请求先发送或者按照ID号优先级发送。

        下面是数据手册的一个基本流程,选择一个空置邮箱→写入报文 →请求发送:

大概查了一下意思:

  • RQCP(Request completed)请求完成,=X表示任意值 
  • TXOK(Transmission OK)发送成功,=X表示任意值 
  • TME(Tranmsit mailbox empty)发送邮箱空,=1表示邮箱空置状态 
  • TXRQ(Tranmsit mailbox request)发送请求控制位,=1表示产生发送请求
  • NART(No automatic retransmission)禁止自动重传,=0表示使用,=1表示禁止,CAN硬件在发送报文失败时会一直自动重传直到发送成功
  • ABRQ(Abort request)终止发送

        大概流程就是,首先邮箱处于空置状态,此时TXRQ=1,产生一个发送请求,进入挂号状态(在三个邮箱随便找一个进去):

        此时判断邮箱是否处于最高优先级,若是最高优先级进入预定状态,若是未处于则继续处于挂号状态:

        或者终止发送ABRQ=1,回到空置状态:

        数据进入预定状态也可以ABRQ=1,回到空置状态:

        或者等待总线空闲CAN总线=IDLE,进入发送状态:

        数据发送成功,回到空置状态:

        若是发送失败,若是NART=0,表示使用自动重传,回到预定状态:

        若是NART=1,表示不使用自动重传,回到空置状态:

6.5  接收流程

        对于数据的接收,当CAN总线出现任何一个数据帧或者遥控帧的报文波形时,控制器就会将报文收下,但是总线上的报文不一定就是自己需要的,需要通过接收过滤器进行筛选,接收过滤器可以根据ID号对报文进行过滤,如果ID不是我们想要的,就不会通过过滤器,数据就不会进行后续的传递,默认条件下不去配置过滤器,过滤器处于失能的状态,无法使用,想要什么ID就可以对过滤器进行配置。

        过滤器过滤完的数据,会存储在FIFO中,在STM32总共配置了两个FIFO,每个FIFO配备3个邮箱,过滤器过滤完的数据可以指定进入那个FIFO :

        假设配备FIFO 0接收过滤器0过滤的数据,假设过滤器通过了三个CAN总线发出的数据,将FIFO 0的三个邮箱全占满了:

        这里可以配置FIFO锁定,假如配置了FIFO锁定,在FIFO满了以后,新的报文就会丢弃;

        假如配置了FIFO不锁定,在FIFO满了以后,新报文会把邮箱2的数据踢出去:

        此时若是CPU读取数据,邮箱0的数据会被读取,那么邮箱1的数据,就会往前进入到邮箱0中,依次类推:

        至于为什么会设立两个FIFO,可以理解为医院就诊,患者就是数据,通过过滤器将急诊患者和普通患者区分开,急诊患者的队伍(基数较少,数据紧急的队伍),普通患者的队伍(基数较大,数据不紧急的队伍):

注意,这里FIFO 0和FIFO 1自己不区分优先级,默认优先级是相同,需要自己进行区分二者的优先级。

        下面是他手册上的一个流程,接收到一个报文→匹配过滤器后进入FIFO 0或FIFO 1→CPU读取:

        这里流程和上面介绍的差不多,就不做重复介绍了,不过需要注意的是,这里应该是二进制的01、10、11表示1、2、3,这里用的十六进制应该表示错了:

6.6  标识符过滤器

        下面数据手册上的有关过滤器的配置,每个过滤器的核心由两个32位寄存器组成:R1[31:0]和R2[31:0]:

图中一些参数:

  • FSCx:位宽设置     置0,16位;置1,32位
  • FBMx:模式设置     置0,屏蔽模式;置1,列表模式
  • FFAx:关联设置     置0,FIFO 0;置1,FIFO 1(相当于选择数据进入哪个FIFO)
  • FACTx:激活设置     置0,禁用;置1,启用(相当于给过滤器加个开关,一般我们不可能把所有过滤器全用上,关掉不用的防止误操作)

其中x表示0~13那个过滤器里的,例如过滤器1里的FSC就表示FSC1,过滤器8里的FBM表示FBM8,依次类推。

由上图通过FSCx和FBMx二者的组合,可以出现四种模式:

FBMx
10
FSCx12个32位过滤器-标识符列表1个32位过滤器-标识符屏蔽
04个16位过滤器-标识符列表2个16位过滤器-标识符屏蔽

6.6.1  2个32位过滤器-标识符列表

        32位列表模式,这个是最简单的模式,该模式下,R1和R2两个寄存器,都写入的是目标ID,我们可以把想要的ID写入到R1和R2两个寄存器,总共可以写两个目标的ID,过滤器通过对比这两个ID,来让匹配同样的ID的数据通过:

        那么要如何进行配置ID呢?首先,我们看下面这个映像,在其高11位需要存入的是标准格式的ID(STID)号:

        然后后面需要存入的这18位,是扩展格式的ID(EXID)号(注意实际上扩展ID是29位,若是写成扩展ID,标准ID将作为扩展ID的高11位):

        这里可以参考之前将帧格式的内容:

CAN总线数据帧格式详细介绍-CSDN博客

        扩展格式的ID实际上是标准格式的11位ID号加上扩展格式的18位ID号组成,简单来说,如果你想要写入一个标准格式的ID号,只需要写入前11位的STID就行,后面18位EXID就用不了了,全部写0就行;如果想写一个扩展ID,需要将标准ID(STID)作为EXID的高11位,然后加上后面18位。

        那么如何区分是标准ID还是扩展ID?这就需要通过IDE位来进行判定,若是IDE=0,那就表示是标准ID,若是IDE=1,那就表示是扩展ID,这里需要注意,若是你写的是扩展ID,但是你的IDE位确实写0,那么就会只取扩展ID的高11位作为标准ID来看。

        后面的RTR位是看你想要过滤遥控帧还是数据帧,RTR=1就是过滤遥控帧,如果RTR=0就是过滤数据帧。

        最后一位是保留位,没实际作用,默认0:

        这里我们可以看出一点,我们过滤器可能既需要过滤扩展ID,也需要过滤标准ID,但也有很多情况我们的过滤器只需要过滤标准ID,不需要过滤扩展ID,那么我们就会发现我们此时在使用32位列表模式,就会发现,扩展ID的18位,处于闲置浪费的状态。

6.6.2  4个16位过滤器-标识符列表

        为了解决闲置浪费,我们可以配置“4个16位过滤器-标识符列表”模式:

        该模式下R1和R2都被拆开,拆成4个16位的寄存器,每个寄存器写入一个标准格式的目标ID,这样一个过滤器就可以写入4个目标ID,极大地利用资源。

        该模式下的映像也是前11位写入标准ID:

        后面的RTR位也是过滤遥控帧还是数据帧,RTR=1就是过滤遥控帧,如果RTR=0就是过滤数据帧:

        因为没有扩展帧,因此IDE位默认写“0”:

        多出来的三位,没啥用写“0”即可:

        这里我们又需要思考,即便是一个过滤器能过滤4种标准ID,14个过滤器全部使用最高也只能过滤56个ID,面对我们平常使用不会出现问题,但是有些时候可能上百个传感器去监听进行数据传递,例如我们使用多个温度传感器,其ID是0x100~0x1FF和多个湿度传感器其ID是0x200~0x2FF,我们要如何才能将所有的数据都检测到呢?

        这里我们就引入了屏蔽,如果我们屏蔽掉后两位,认为取0x1开头的ID是温度数据,以0x2开头的是湿度数据,不就有效解决这个问题了吗。

6.6.3  1个32位过滤器-标识符屏蔽

        比如说R1写ID号,R2决定哪些ID是必须一样,哪些ID是任意的,这样就能完美满足需求:

这里R2的作用类似于掩码。

延续上面温湿度的例子:

        假设我们要过滤出0x1开头的所有ID号,11位的二进制表示就是:

ID号:001 xxxx xxxx

        首先我们需要把这个ID号,写入到R1寄存器里,EXID不需要全部写“0”,因为我们是标准ID,所有IDE位必须写“0”,RTR根据需求来:

        如果现在是列表模式,那么我们就只能过滤掉一个ID(0x100),但现在是屏蔽模式,那么我们就可以在R2里标明,在过滤时哪些位是必须匹配的,哪些是无关的,这样我们就需要给R2的高3位给1,这样就表示我们ID高3位必须为001才能通过过滤器,除此之外,屏蔽位的IDE位必须给1,不然就表示标准格式和扩展格式都无所谓了,这样可能使某些扩展ID进来,就不符合需求了:

STIDEXIDIDERTR保留位
ID001x xxxx  xxx 0 0000 0000 0000  0000  00X0
屏蔽1110 0000 0000 0000 0000 0000  0000  01X0

        其中RTR位根据需求:

RTRRTRRTR
ID011/0
屏蔽110
必须匹配数据帧必须匹配遥控帧数据帧/遥控帧都可以

6.6.4  2个16位过滤器-标识符屏蔽

        如果只需要过滤标准ID的话,可以使用下面这个,可以过滤两组屏蔽位的ID,使用方法和上面一样:

综合上面的四种情况句几个例子:

总线上存在的ID想要接收的ID过滤器模式R1[31:0]配置值R2[31:0]配置值
0x123, 0x234, 0x345, 0x456, 0x567, 0x6780x234, 0x345, 0x56716位/列表

ID: R1[15:0]=0x234<<5

ID: R1[31:16]=0x345<<5

ID: R2[15:0]=0x567<<5

ID: R2[31:16]=0x000<<5

0x100~0x1FF, 0x200~0x2FF, 0x310~0x31F, 0x320~0x32F0x200~0x2FF, 0x320~0x32F16位/屏蔽

ID: R1[15:0]=0x200<<5

Mask: R1[31:16]= (0x700<<5)|0x10|0x8

ID: R2[15:0]=0x320<<5

Mask: R2[31:16]= (0x7F0<<5)|0x10|0x8

0x123, 0x234, 0x345, 0x456, 0x12345678, 0x0789ABCD0x123, 0x1234567832位/列表ID: R1[31:0]= 0x123<<21ID: R2[31:0]= (0x12345678<<3)|0x4
0x12345600~ 0x123456FF, 0x0789AB00~ 0x0789ABFF0x12345600~ 0x123456FF32位/屏蔽ID: R1[31:0]= (0x12345600<<3)|0x4Mask: R2[31:0]= (0x1FFFFF00<<3)|0x4|0x2
任意ID只要遥控帧32位/屏蔽ID: R1[31:0]=0x2Mask: R2[31:0]=0x2
任意ID所有帧32位/屏蔽ID: R1[31:0]=随意Mask: R2[31:0]=0

6.7  测试模式

6.7.1  静默模式

        静默模式:用于分析CAN总线的活动,不会对总线造成影响:

        在静默模式下,bxCAN可以正常地接收数据帧和远程帧,但只能发出隐性位,而不能真正发送报文。如果bxCAN需要发出显性位(确认位、过载标志、主动错误标志),那么这样的显性位在内部被接回来从而可以被CAN内核检测到,同时CAN总线不会受到影响而仍然维持在隐性位状态。显性位(确认位、错误帧)不会真正发送到总线上。

6.7.2  环回模式

        环回模式:用于自测试,同时发送的报文可以在CAN_TX引脚上检测到,在环回模式下,bxCAN把发送的报文当作接收的报文并保存(如果可以通过接收过滤)在接收邮箱里。

        在环回模式下CAN内核忽略确认错误(在数据/远程帧的确认位时刻,不检测是否有显性位)。在环回模式下,bxCAN在内部把Tx输出回馈到Rx输入上,而完全忽略CANRX引脚的实际状态。

6.7.3  环回静默模式

        环回静默模式:用于热自测试,自测的同时不会影响CAN总线。即可以像环回模式那样测试bxCAN,但却不会影响CANTX和CANRX所连接的整个CAN系统。在环回静默模式下,CANRX引脚与CAN总线断开,同时CANTX引脚被驱动到隐性位状态。

6.8  工作模式

        bxCAN有3个主要的工作模式:初始化、正常和睡眠模式。在硬件复位后,bxCAN工作在睡眠模式以节省电能,同时CANTX引脚的内部上拉电阻被激活。软件通过对CAN_MCR寄存器的INRQ或SLEEP位置“1”,可以请求bxCAN进入初始化或睡眠模式。一旦进入了初始化或睡眠模式,bxCAN就对CAN_MSR寄存器的INAK或SLAK位置“1”来进行确认,同时内部上拉电阻被禁用。当INAK和SLAK位都为“0”时,bxCAN就处于正常模式。在进入正常模式前,bxCAN必须跟CAN总线取得同步;为取得同步,bxCAN要等待CAN总线达到空闲状态,即在CANRX引脚上监测到11个连续的隐性位。

SLAK(Sleep ack)睡眠确认状态位 ,=1表示硬件已经确认进入睡眠模式了

INAK(Init ack)初始化确认位,=0表示硬件没有确认进入初始化模式

6.8.1  初始化模式

        初始化模式:用于配置CAN外设,禁止报文的接收和发送。

6.8.2  正常模式

        正常模式:配置CAN外设后进入正常模式,以便正常接收和发送报文

6.8.3  睡眠模式

        睡眠模式:低功耗,CAN外设时钟停止,可使用软件唤醒或者硬件自动唤醒,通过AWUM:置1,自动唤醒,一旦检测到CAN总线活动,硬件就自动清零SLEEP,唤醒CAN外设;置0,手动唤醒,软件清零SLEEP,唤醒CAN外设。

        这三种模式详细想要了解怎么工作的,可以看数据手册,都是一些寄存器的配置,回头讲代码的时候,边用边讲,下面是他的一个手册相关部分截图:

6.9  位时间特性

        位时间特性逻辑通过采样来监视串行的CAN总线,并且通过与帧起始位的边沿进行同步,及通过与后面的边沿进行重新同步,来调整其采样点。这个在之前讲位同步是已经讲过,不过需要有一点注意的是:

         可以看出在CAN协议的位同步中,SS和PBS1之间还有个PTS,但是STM32是没有PTS段的,这里可能是因为,PBS1和PTS二者之间没啥事要干,STM32设计者讲二者融合到一起了,还能少设计个参数。

上图公式可写为:

 波特率 = APB1时钟频率 / 分频系数 / 一位的Tq数量                

             = 36MHz / (BRP[9:0]+1) / (1 + (TS1[3:0]+1) + (TS2[2:0]+1))

        同步段(SYNC_SEG):通常期望位的变化发生在该时间段内。其值固定为1个时间单元(1 x tCAN)。

        时间段1(BS1):定义采样点的位置。它包含CAN标准里的PROP_SEG和PHASE_SEG1。其值可以编程为1到16个时间单元,但也可以被自动延长,以补偿因为网络中不同节点的频率差异所造成的相位的正向漂移。

        时间段2(BS2):定义发送点的位置。它代表CAN标准里的PHASE_SEG2。其值可以编程为1到8个时间单元,但也可以被自动缩短以补偿相位的负向漂移。

        重新同步跳跃宽度(SJW)定义了,在每位中可以延长或缩短多少个时间单元的上限。其值可以编程为1到4个时间单元。

        有效跳变被定义为,当bxCAN自己没有发送隐性位时,从显性位到隐性位的第1次转变。

下面是各种CAN帧:

6.10  中断

        bxCAN占用4个专用的中断向量。通过设置CAN中断允许寄存器(CAN_IER),每个中断源都可以单独允许和禁用。

6.10.1  发送中断

        数据手册上表述的是:

简单来说,就是发送中断就是发送邮箱空时产生的中断。

6.10.2  FIFO 0中断

        数据手册上表述的是:

FIFO 0中断:收到一个报文/FIFO 0满/FIFO 0溢出时产生

6.10.3  FIFO 1中断

        数据手册上表述的是:

FIFO 1中断:收到一个报文/FIFO 1满/FIFO 1溢出时产生

6.10.4  错误和状态变化中断

        数据手册上表述的是:

状态改变错误中断:出错/唤醒/进入睡眠时产生

6.11  错误处理和离线恢复

        CAN协议描述的出错管理,完全由硬件通过发送错误计数器(CAN_ESR寄存器里的TEC域),和接收错误计数器(CAN_ESR寄存器里的REC域)来实现,其值根据错误的情况而增加或减少。

        当TEC大于255时,bxCAN就进入离线状态,同时CAN_ESR寄存器的BOFF位被置’1’。在离线状态下,bxCAN无法接收和发送报文。

        根据CAN_MCR寄存器中ABOM位的设置,bxCAN可以自动或在软件的请求下,从离线状态恢复(变为错误主动状态)。在这两种情况下,bxCAN都必须等待一个CAN标准所描述的恢复过程(CAN RX引脚上检测到128次11个连续的隐性位)。

如果ABOM位为’1’,bxCAN进入离线状态后,就自动开启恢复过程。

如果ABOM位为’0’,软件必须先请求bxCAN进入然后再退出初始化模式,随后恢复过程才被开启。

注: 在初始化模式下,bxCAN不会监视CAN RX引脚的状态,这样就不能完成恢复过程。为了完成恢复过程,bxCAN必须工作在正常模式。

7.  CAN外设简介

        前面我们已经详细介绍了CAN总线:

CAN总线_时光の尘的博客-CSDN博客

        下面我们开始在STM32单片机上对其进行使用,在使用前我们先来了解一下STM32的一些CAN外设。

        STM32内置bxCAN外设(CAN控制器),支持CAN2.0A和2.0B,可以自动发送CAN报文和按照过滤器自动接收指定CAN报文,程序只需处理报文数据而无需关注总线的电平细节。更详细的参数可以看参考手册,这里截了一点其主要特点:

7.1  CAN网拓扑结构

        这里的每个CAN结点都挂载在CAN总线上,对于其中的一个CAN结点,他们是由控制芯片MCU、CAN控制器以及CAN收发器组成,一般情况下CAN控制器都集成在MCU里面,STM32就集成了CAN控制器,STM32引出了CAN_Rx和CAN_Tx引脚,与CAN收发器相连,CAN收发器通过引出的CAN_H和CAN_L与CAN总线相连:

7.2  引脚定义

        这里我们使用的是STM32F103C8T6芯片,其上CAN资源为CAN1,我们查找数据手册,其相关引脚分别为:

7.3  CAN框图

        下面是STM32的CAN外设的框图,其中上面部分是CAN1的,也就是主CAN,下面部分是CAN2的(只有互联型设备才有CAN2),也就是从CAN,我们使用的STM32F103C8T6只有CAN1的外设:

7.4  发送流程

        对于CAN在STM32内部的工作,首先对于发送,当我们想要发送一个报文时,我们只需要将想要发送的报文的各个参数,通过CPU写入到邮箱中,然后给出请求发送的命令,然后发送和接收控制器就会等待总线空闲,然后自动把这个报文广播到总线上:

        这里我们可以看出,其实一个邮箱就能完成上述功能,但是为什么要创建三个呢?内置三个邮箱邮箱的原因,实际上是为了防止总线繁忙造成发送拥堵,举个例子:

        假如你写入了一个报文存储到邮箱0,让控制器将数据发出去,但是此时总线一直处于繁忙状态,控制器一直等不到总线空闲,数据无法发出会滞留在邮箱内,当CPU想发出下一个数据,就会发现邮箱满了没地方存储,这样CPU也会处于等待状态,这样就会造成拥堵:

        STM32内置3个邮箱,当总线拥堵,邮箱0数据未发送出去,但是CPU想继续发送数据,这时就可以在邮箱1或者邮箱2中任意找一个邮箱进行存储数据,等待总线空闲进行发出:

        这里可能会说万一邮箱全满了怎么办?一般情况下总线不会出现如此拥堵的情况,如果出现了只能要么CPU等待,要么CPU放弃本次数据存储。

        当两个或者三个邮箱都有数据要发送,那么会先发哪一个呢?这里可以进行配置:现请求先发送或者按照ID号优先级发送。

        下面是数据手册的一个基本流程,选择一个空置邮箱→写入报文 →请求发送:

大概查了一下意思:

  • RQCP(Request completed)请求完成,=X表示任意值 
  • TXOK(Transmission OK)发送成功,=X表示任意值 
  • TME(Tranmsit mailbox empty)发送邮箱空,=1表示邮箱空置状态 
  • TXRQ(Tranmsit mailbox request)发送请求控制位,=1表示产生发送请求
  • NART(No automatic retransmission)禁止自动重传,=0表示使用,=1表示禁止,CAN硬件在发送报文失败时会一直自动重传直到发送成功
  • ABRQ(Abort request)终止发送

        大概流程就是,首先邮箱处于空置状态,此时TXRQ=1,产生一个发送请求,进入挂号状态(在三个邮箱随便找一个进去):

        此时判断邮箱是否处于最高优先级,若是最高优先级进入预定状态,若是未处于则继续处于挂号状态:

        或者终止发送ABRQ=1,回到空置状态:

        数据进入预定状态也可以ABRQ=1,回到空置状态:

        或者等待总线空闲CAN总线=IDLE,进入发送状态:

        数据发送成功,回到空置状态:

        若是发送失败,若是NART=0,表示使用自动重传,回到预定状态:

        若是NART=1,表示不使用自动重传,回到空置状态:

7.5  接收流程

        对于数据的接收,当CAN总线出现任何一个数据帧或者遥控帧的报文波形时,控制器就会将报文收下,但是总线上的报文不一定就是自己需要的,需要通过接收过滤器进行筛选,接收过滤器可以根据ID号对报文进行过滤,如果ID不是我们想要的,就不会通过过滤器,数据就不会进行后续的传递,默认条件下不去配置过滤器,过滤器处于失能的状态,无法使用,想要什么ID就可以对过滤器进行配置。

        过滤器过滤完的数据,会存储在FIFO中,在STM32总共配置了两个FIFO,每个FIFO配备3个邮箱,过滤器过滤完的数据可以指定进入那个FIFO :

        假设配备FIFO 0接收过滤器0过滤的数据,假设过滤器通过了三个CAN总线发出的数据,将FIFO 0的三个邮箱全占满了:

        这里可以配置FIFO锁定,假如配置了FIFO锁定,在FIFO满了以后,新的报文就会丢弃;

        假如配置了FIFO不锁定,在FIFO满了以后,新报文会把邮箱2的数据踢出去:

        此时若是CPU读取数据,邮箱0的数据会被读取,那么邮箱1的数据,就会往前进入到邮箱0中,依次类推:

        至于为什么会设立两个FIFO,可以理解为医院就诊,患者就是数据,通过过滤器将急诊患者和普通患者区分开,急诊患者的队伍(基数较少,数据紧急的队伍),普通患者的队伍(基数较大,数据不紧急的队伍):

注意,这里FIFO 0和FIFO 1自己不区分优先级,默认优先级是相同,需要自己进行区分二者的优先级。

        下面是他手册上的一个流程,接收到一个报文→匹配过滤器后进入FIFO 0或FIFO 1→CPU读取:

        这里流程和上面介绍的差不多,就不做重复介绍了,不过需要注意的是,这里应该是二进制的01、10、11表示1、2、3,这里用的十六进制应该表示错了:

7.6  标识符过滤器

        下面数据手册上的有关过滤器的配置,每个过滤器的核心由两个32位寄存器组成:R1[31:0]和R2[31:0]:

图中一些参数:

  • FSCx:位宽设置     置0,16位;置1,32位
  • FBMx:模式设置     置0,屏蔽模式;置1,列表模式
  • FFAx:关联设置     置0,FIFO 0;置1,FIFO 1(相当于选择数据进入哪个FIFO)
  • FACTx:激活设置     置0,禁用;置1,启用(相当于给过滤器加个开关,一般我们不可能把所有过滤器全用上,关掉不用的防止误操作)

其中x表示0~13那个过滤器里的,例如过滤器1里的FSC就表示FSC1,过滤器8里的FBM表示FBM8,依次类推。

由上图通过FSCx和FBMx二者的组合,可以出现四种模式:

FBMx
10
FSCx12个32位过滤器-标识符列表1个32位过滤器-标识符屏蔽
04个16位过滤器-标识符列表2个16位过滤器-标识符屏蔽

1.6.1  2个32位过滤器-标识符列表

        32位列表模式,这个是最简单的模式,该模式下,R1和R2两个寄存器,都写入的是目标ID,我们可以把想要的ID写入到R1和R2两个寄存器,总共可以写两个目标的ID,过滤器通过对比这两个ID,来让匹配同样的ID的数据通过:

        那么要如何进行配置ID呢?首先,我们看下面这个映像,在其高11位需要存入的是标准格式的ID(STID)号:

        然后后面需要存入的这18位,是扩展格式的ID(EXID)号(注意实际上扩展ID是29位,若是写成扩展ID,标准ID将作为扩展ID的高11位):

        这里可以参考之前将帧格式的内容:

CAN总线数据帧格式详细介绍-CSDN博客

        扩展格式的ID实际上是标准格式的11位ID号加上扩展格式的18位ID号组成,简单来说,如果你想要写入一个标准格式的ID号,只需要写入前11位的STID就行,后面18位EXID就用不了了,全部写0就行;如果想写一个扩展ID,需要将标准ID(STID)作为EXID的高11位,然后加上后面18位。

        那么如何区分是标准ID还是扩展ID?这就需要通过IDE位来进行判定,若是IDE=0,那就表示是标准ID,若是IDE=1,那就表示是扩展ID,这里需要注意,若是你写的是扩展ID,但是你的IDE位确实写0,那么就会只取扩展ID的高11位作为标准ID来看。

        后面的RTR位是看你想要过滤遥控帧还是数据帧,RTR=1就是过滤遥控帧,如果RTR=0就是过滤数据帧。

        最后一位是保留位,没实际作用,默认0:

        这里我们可以看出一点,我们过滤器可能既需要过滤扩展ID,也需要过滤标准ID,但也有很多情况我们的过滤器只需要过滤标准ID,不需要过滤扩展ID,那么我们就会发现我们此时在使用32位列表模式,就会发现,扩展ID的18位,处于闲置浪费的状态。

1.6.2  4个16位过滤器-标识符列表

        为了解决闲置浪费,我们可以配置“4个16位过滤器-标识符列表”模式:

        该模式下R1和R2都被拆开,拆成4个16位的寄存器,每个寄存器写入一个标准格式的目标ID,这样一个过滤器就可以写入4个目标ID,极大地利用资源。

        该模式下的映像也是前11位写入标准ID:

        后面的RTR位也是过滤遥控帧还是数据帧,RTR=1就是过滤遥控帧,如果RTR=0就是过滤数据帧:

        因为没有扩展帧,因此IDE位默认写“0”:

        多出来的三位,没啥用写“0”即可:

        这里我们又需要思考,即便是一个过滤器能过滤4种标准ID,14个过滤器全部使用最高也只能过滤56个ID,面对我们平常使用不会出现问题,但是有些时候可能上百个传感器去监听进行数据传递,例如我们使用多个温度传感器,其ID是0x100~0x1FF和多个湿度传感器其ID是0x200~0x2FF,我们要如何才能将所有的数据都检测到呢?

        这里我们就引入了屏蔽,如果我们屏蔽掉后两位,认为取0x1开头的ID是温度数据,以0x2开头的是湿度数据,不就有效解决这个问题了吗。

1.6.3  1个32位过滤器-标识符屏蔽

        比如说R1写ID号,R2决定哪些ID是必须一样,哪些ID是任意的,这样就能完美满足需求:

这里R2的作用类似于掩码。

延续上面温湿度的例子:

        假设我们要过滤出0x1开头的所有ID号,11位的二进制表示就是:

ID号:001 xxxx xxxx

        首先我们需要把这个ID号,写入到R1寄存器里,EXID不需要全部写“0”,因为我们是标准ID,所有IDE位必须写“0”,RTR根据需求来:

        如果现在是列表模式,那么我们就只能过滤掉一个ID(0x100),但现在是屏蔽模式,那么我们就可以在R2里标明,在过滤时哪些位是必须匹配的,哪些是无关的,这样我们就需要给R2的高3位给1,这样就表示我们ID高3位必须为001才能通过过滤器,除此之外,屏蔽位的IDE位必须给1,不然就表示标准格式和扩展格式都无所谓了,这样可能使某些扩展ID进来,就不符合需求了:

STIDEXIDIDERTR保留位
ID001x xxxx  xxx 0 0000 0000 0000  0000  00X0
屏蔽1110 0000 0000 0000 0000 0000  0000  01X0

        其中RTR位根据需求:

RTRRTRRTR
ID011/0
屏蔽110
必须匹配数据帧必须匹配遥控帧数据帧/遥控帧都可以

1.6.4  2个16位过滤器-标识符屏蔽

        如果只需要过滤标准ID的话,可以使用下面这个,可以过滤两组屏蔽位的ID,使用方法和上面一样:

综合上面的四种情况句几个例子,在后续代码编写时有介绍:

总线上存在的ID想要接收的ID过滤器模式R1[31:0]配置值R2[31:0]配置值
0x123, 0x234, 0x345, 0x456, 0x567, 0x6780x234, 0x345, 0x56716位/列表

ID: R1[15:0]=0x234<<5

ID: R1[31:16]=0x345<<5

ID: R2[15:0]=0x567<<5

ID: R2[31:16]=0x000<<5

0x100~0x1FF, 0x200~0x2FF, 0x310~0x31F, 0x320~0x32F0x200~0x2FF, 0x320~0x32F16位/屏蔽

ID: R1[15:0]=0x200<<5

Mask: R1[31:16]= (0x700<<5)|0x10|0x8

ID: R2[15:0]=0x320<<5

Mask: R2[31:16]= (0x7F0<<5)|0x10|0x8

0x123, 0x234, 0x345, 0x456, 0x12345678, 0x0789ABCD0x123, 0x1234567832位/列表ID: R1[31:0]= 0x123<<21ID: R2[31:0]= (0x12345678<<3)|0x4
0x12345600~ 0x123456FF, 0x0789AB00~ 0x0789ABFF0x12345600~ 0x123456FF32位/屏蔽ID: R1[31:0]= (0x12345600<<3)|0x4Mask: R2[31:0]= (0x1FFFFF00<<3)|0x4|0x2
任意ID只要遥控帧32位/屏蔽ID: R1[31:0]=0x2Mask: R2[31:0]=0x2
任意ID所有帧32位/屏蔽ID: R1[31:0]=随意Mask: R2[31:0]=0

1.7  测试模式

1.7.1  静默模式

        静默模式:用于分析CAN总线的活动,不会对总线造成影响:

        在静默模式下,bxCAN可以正常地接收数据帧和远程帧,但只能发出隐性位,而不能真正发送报文。如果bxCAN需要发出显性位(确认位、过载标志、主动错误标志),那么这样的显性位在内部被接回来从而可以被CAN内核检测到,同时CAN总线不会受到影响而仍然维持在隐性位状态。显性位(确认位、错误帧)不会真正发送到总线上。

1.7.2  环回模式

        环回模式:用于自测试,同时发送的报文可以在CAN_TX引脚上检测到,在环回模式下,bxCAN把发送的报文当作接收的报文并保存(如果可以通过接收过滤)在接收邮箱里。

        在环回模式下CAN内核忽略确认错误(在数据/远程帧的确认位时刻,不检测是否有显性位)。在环回模式下,bxCAN在内部把Tx输出回馈到Rx输入上,而完全忽略CANRX引脚的实际状态。

1.7.3  环回静默模式

        环回静默模式:用于热自测试,自测的同时不会影响CAN总线。即可以像环回模式那样测试bxCAN,但却不会影响CANTX和CANRX所连接的整个CAN系统。在环回静默模式下,CANRX引脚与CAN总线断开,同时CANTX引脚被驱动到隐性位状态。

1.8  工作模式

        bxCAN有3个主要的工作模式:初始化、正常和睡眠模式。在硬件复位后,bxCAN工作在睡眠模式以节省电能,同时CANTX引脚的内部上拉电阻被激活。软件通过对CAN_MCR寄存器的INRQ或SLEEP位置“1”,可以请求bxCAN进入初始化或睡眠模式。一旦进入了初始化或睡眠模式,bxCAN就对CAN_MSR寄存器的INAK或SLAK位置“1”来进行确认,同时内部上拉电阻被禁用。当INAK和SLAK位都为“0”时,bxCAN就处于正常模式。在进入正常模式前,bxCAN必须跟CAN总线取得同步;为取得同步,bxCAN要等待CAN总线达到空闲状态,即在CANRX引脚上监测到11个连续的隐性位。

SLAK(Sleep ack)睡眠确认状态位 ,=1表示硬件已经确认进入睡眠模式了

INAK(Init ack)初始化确认位,=0表示硬件没有确认进入初始化模式

1.8.1  初始化模式

        初始化模式:用于配置CAN外设,禁止报文的接收和发送。

1.8.2  正常模式

        正常模式:配置CAN外设后进入正常模式,以便正常接收和发送报文

1.8.3  睡眠模式

        睡眠模式:低功耗,CAN外设时钟停止,可使用软件唤醒或者硬件自动唤醒,通过AWUM:置1,自动唤醒,一旦检测到CAN总线活动,硬件就自动清零SLEEP,唤醒CAN外设;置0,手动唤醒,软件清零SLEEP,唤醒CAN外设。

        这三种模式详细想要了解怎么工作的,可以看数据手册,都是一些寄存器的配置,回头讲代码的时候,边用边讲,下面是他的一个手册相关部分截图:

1.9  位时间特性

        位时间特性逻辑通过采样来监视串行的CAN总线,并且通过与帧起始位的边沿进行同步,及通过与后面的边沿进行重新同步,来调整其采样点。

        这个在之前讲位同步是已经讲过:

CAN总线位同步的使用以及总线仲裁规则详解-CSDN博客

        不过需要有一点注意的是:

         可以看出在CAN协议的位同步中,SS和PBS1之间还有个PTS,但是STM32是没有PTS段的,这里可能是因为,PBS1和PTS二者之间没啥事要干,STM32设计者讲二者融合到一起了,还能少设计个参数。

上图公式可写为:

 波特率 = APB1时钟频率 / 分频系数 / 一位的Tq数量                

             = 36MHz / (BRP[9:0]+1) / (1 + (TS1[3:0]+1) + (TS2[2:0]+1))

        同步段(SYNC_SEG):通常期望位的变化发生在该时间段内。其值固定为1个时间单元(1 x tCAN)。

        时间段1(BS1):定义采样点的位置。它包含CAN标准里的PROP_SEG和PHASE_SEG1。其值可以编程为1到16个时间单元,但也可以被自动延长,以补偿因为网络中不同节点的频率差异所造成的相位的正向漂移。

        时间段2(BS2):定义发送点的位置。它代表CAN标准里的PHASE_SEG2。其值可以编程为1到8个时间单元,但也可以被自动缩短以补偿相位的负向漂移。

        重新同步跳跃宽度(SJW)定义了,在每位中可以延长或缩短多少个时间单元的上限。其值可以编程为1到4个时间单元。

        有效跳变被定义为,当bxCAN自己没有发送隐性位时,从显性位到隐性位的第1次转变。

下面是各种CAN帧:

1.10  中断

        bxCAN占用4个专用的中断向量。通过设置CAN中断允许寄存器(CAN_IER),每个中断源都可以单独允许和禁用。

1.10.1  发送中断

        数据手册上表述的是:

简单来说,就是发送中断就是发送邮箱空时产生的中断。

1.10.2  FIFO 0中断

        数据手册上表述的是:

FIFO 0中断:收到一个报文/FIFO 0满/FIFO 0溢出时产生

1.10.3  FIFO 1中断

        数据手册上表述的是:

FIFO 1中断:收到一个报文/FIFO 1满/FIFO 1溢出时产生

1.10.4  错误和状态变化中断

        数据手册上表述的是:

状态改变错误中断:出错/唤醒/进入睡眠时产生

7.11  错误处理和离线恢复

        CAN协议描述的出错管理,完全由硬件通过发送错误计数器(CAN_ESR寄存器里的TEC域),和接收错误计数器(CAN_ESR寄存器里的REC域)来实现,其值根据错误的情况而增加或减少。

        当TEC大于255时,bxCAN就进入离线状态,同时CAN_ESR寄存器的BOFF位被置’1’。在离线状态下,bxCAN无法接收和发送报文。

        根据CAN_MCR寄存器中ABOM位的设置,bxCAN可以自动或在软件的请求下,从离线状态恢复(变为错误主动状态)。在这两种情况下,bxCAN都必须等待一个CAN标准所描述的恢复过程(CAN RX引脚上检测到128次11个连续的隐性位)。

如果ABOM位为’1’,bxCAN进入离线状态后,就自动开启恢复过程。

如果ABOM位为’0’,软件必须先请求bxCAN进入然后再退出初始化模式,随后恢复过程才被开启。

注: 在初始化模式下,bxCAN不会监视CAN RX引脚的状态,这样就不能完成恢复过程。为了完成恢复过程,bxCAN必须工作在正常模式。

8.  STM32库函数解读

        在编写代码前,我们需要先了解STM32为我们封装的函数都是什么意思,不然也不知道调用来干嘛的,我们在stm32f10x_can.h文件划到最下方,看一下:

8.1  初始化相关

        首先,初始化CAN控制器,配置指定CAN控制器(CANx)的初始化参数。CAN_InitStruct 包含了CAN控制器的初始化配置:

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

        初始化CAN滤波器,配置CAN滤波器。CAN_FilterInitStruct 包含CAN滤波器的相关参数:

void CAN_FilterInit(CAN_FilterInitTypeDef* CAN_FilterInitStruct);

        初始化CAN配置结构体, 将CAN_InitTypeDef结构体中的所有成员初始化为默认值,用于后续CAN初始化:

void CAN_StructInit(CAN_InitTypeDef* CAN_InitStruct);

        下面这三个函数我们这里用不到,大概意思分别是:1. 启动CAN从设备的滤波器

                                                                                           2. 控制CAN调试冻结模式

                                                                                           3. 控制CAN时间触发通信模式

void CAN_SlaveStartBank(uint8_t CAN_BankNumber); //1
void CAN_DBGFreeze(CAN_TypeDef* CANx, FunctionalState NewState);//2
void CAN_TTComModeCmd(CAN_TypeDef* CANx, FunctionalState NewState);//3

8.2  发送相关

        发送CAN消息, 将指定的CAN消息(TxMessage)通过指定的CAN控制器(CANx)进行传输。函数返回发送的状态:

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

        获取CAN消息发送状态, 获取指定发送邮箱(TransmitMailbox)的消息传输状态。返回的状态可以用来确认消息是否已经成功发送或正在发送中:

uint8_t CAN_TransmitStatus(CAN_TypeDef* CANx, uint8_t TransmitMailbox);

        取消CAN消息发送, 取消通过指定的发送邮箱(Mailbox)发送的CAN消息。这通常用于停止消息的传输,尤其是在传输过程中遇到错误或不再需要该消息时:

void CAN_CancelTransmit(CAN_TypeDef* CANx, uint8_t Mailbox);

8.3  接收相关

        接收CAN消息,从指定的接收FIFO(FIFONumber)中接收CAN消息,并将接收到的消息存储在RxMessage中。CANx表示使用的CAN控制器:

void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage);

        释放CAN接收FIFO, 释放指定FIFO(FIFONumber)中已处理的消息,允许该FIFO准备接收新的消息:

void CAN_FIFORelease(CAN_TypeDef* CANx, uint8_t FIFONumber);

        检查CAN接收FIFO是否有待处理的消息,检查指定FIFO(FIFONumber)中是否有待处理的消息。如果有,函数返回消息的数量;如果没有,返回0:

uint8_t CAN_MessagePending(CAN_TypeDef* CANx, uint8_t FIFONumber);

8.4  工作模式转换相关

        请求CAN操作模式, 请求将CAN控制器切换到指定的操作模式(CAN_OperatingMode)。例如,可以切换到正常模式、睡眠模式或初始化模式:

uint8_t CAN_OperatingModeRequest(CAN_TypeDef* CANx, uint8_t CAN_OperatingMode);

        使CAN进入睡眠模式,将CAN控制器设置为睡眠模式,以减少功耗。通常用于低功耗应用:

uint8_t CAN_Sleep(CAN_TypeDef* CANx);

        唤醒CAN控制器,从睡眠模式唤醒CAN控制器,使其恢复正常工作:

uint8_t CAN_WakeUp(CAN_TypeDef* CANx);

8.5  错误管理相关

        获取CAN控制器的最后错误代码,获取CAN控制器上一次发生的错误代码。返回的错误码可以用于诊断故障:

uint8_t CAN_GetLastErrorCode(CAN_TypeDef* CANx);

        获取接收错误计数器,返回CAN接收过程中的错误计数。该计数器指示接收到的错误帧的数量:

uint8_t CAN_GetReceiveErrorCounter(CAN_TypeDef* CANx);

        获取LSB发送错误计数器,返回CAN发送过程中的错误计数,特别是低位字节(LSB)部分的发送错误计数:

uint8_t CAN_GetLSBTransmitErrorCounter(CAN_TypeDef* CANx);

8.6  中断相关

        配置CAN中断,启用或禁用指定的CAN中断(CAN_IT)。NewState参数决定是否使能中断:

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

        获取CAN标志状态,返回指定CAN标志(CAN_FLAG)的状态,指示该标志是否已被设置:

FlagStatus CAN_GetFlagStatus(CAN_TypeDef* CANx, uint32_t CAN_FLAG);

        清除CAN标志,清除指定的CAN标志(CAN_FLAG)。该操作通常在处理中断或完成某个任务后执行:

void CAN_ClearFlag(CAN_TypeDef* CANx, uint32_t CAN_FLAG);

        获取CAN中断状态,返回指定CAN中断(CAN_IT)的状态,指示该中断是否已被触发:

ITStatus CAN_GetITStatus(CAN_TypeDef* CANx, uint32_t CAN_IT);

        清除CAN中断挂起标志,清除指定的CAN中断挂起标志,通常用于处理中断后,重置中断标志以便下次触发:

void CAN_ClearITPendingBit(CAN_TypeDef* CANx, uint32_t CAN_IT);

9.  代码编写

9.1  环回测试代码编写

9.1.1  GPIO口初始化

        根据数据手册我们可以了解到在STM32上,CAN的引脚有两对,首先是PA11和PA12引脚:

        配置就是正常引脚配置,不过这里要注意开启CAN1的时钟:

void CAN_GPIO_Config(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
}

        对于另外两个引脚PB8和PB9,可以看出CAN的功能需要重映射: 

        因此需要使能AFIO,以及进行端口重映射: 

void CAN_GPIO_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);//使能CAN1

	//重映射引脚,端口映射
	GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE);

    //CAN_RX	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);	
	
	//CAN_TX
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}

        以上两种初始化方法皆可使用,这里我们使用PA11和PA12两个引脚。

9.1.2  模式配置

        这里主要对CAN控制器,进行一个初始化操作,其中CAN_InitTypeDef所包含的内容,可以再stm32f10x_can.h中找到:

         简单翻译一下:

typedef struct
{
  uint16_t CAN_Prescaler;   /*!< 指定时间量子的长度。
                                 该值的范围是从 1 到 1024。 */

  uint8_t CAN_Mode;         /*!< 指定CAN操作模式。
                                 该参数可以是以下枚举值之一:@ref CAN_operating_mode */

  uint8_t CAN_SJW;          /*!< 指定CAN硬件允许用来进行重新同步的最大时间量子数。
                                 该值可以是以下枚举值之一:@ref CAN_synchronisation_jump_width */

  uint8_t CAN_BS1;          /*!< 指定位段1中的时间量子数。
                                 该参数可以是以下枚举值之一:@ref CAN_time_quantum_in_bit_segment_1 */

  uint8_t CAN_BS2;          /*!< 指定位段2中的时间量子数。
                                 该参数可以是以下枚举值之一:@ref CAN_time_quantum_in_bit_segment_2 */
  
  FunctionalState CAN_TTCM; /*!< 启用或禁用时间触发通信模式。此参数可以设置为 ENABLE 或 DISABLE。 */
  
  FunctionalState CAN_ABOM;  /*!< 启用或禁用自动总线关闭管理模式。此参数可以设置为 ENABLE 或 DISABLE。 */

  FunctionalState CAN_AWUM;  /*!< 启用或禁用自动唤醒模式。此参数可以设置为 ENABLE 或 DISABLE。 */

  FunctionalState CAN_NART;  /*!< 启用或禁用无自动重传模式。此参数可以设置为 ENABLE 或 DISABLE。 */

  FunctionalState CAN_RFLM;  /*!< 启用或禁用接收FIFO锁定模式。此参数可以设置为 ENABLE 或 DISABLE。 */

  FunctionalState CAN_TXFP;  /*!< 启用或禁用发送FIFO优先级模式。此参数可以设置为 ENABLE 或 DISABLE。 */
} CAN_InitTypeDef;

        对于 uint8_t CAN_Mode; 其代表的是,以下模式:

        这里我们使用的是环回模式进行代码测试,因此需要选择 CAN_Mode_LoopBack :

CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;

        波特率相关对应的是:

上图公式可写为:

 波特率 = APB1时钟频率 / 分频系数 / 一位的Tq数量                

             = 36MHz / (BRP[9:0]+1) / (1 + (TS1[3:0]+1) + (TS2[2:0]+1))

详解可以参考:CAN总线位同步的使用以及总线仲裁规则详解-CSDN博客

        对于BS1和BS2这里又一个加一的操作,那么我们在代码声明时需不需要进行减一的操作来陪着这里呢?实际上是不需要的,因为STM32的库函数已经帮我们把这一步封装过了我们只需要进行直接带入即可:

 波特率 = APB1时钟频率 / 分频系数 / 一位的Tq数量                

             = 36MHz / Prescaler/ (1 + PBS1 + PBS2)

        这里需要注意对于PBS1,PBS2和SJW这三个值是有取值范围的,其中PBS1取值范围是1~16Tq:

#define CAN_BS1_1tq                 ((uint8_t)0x00)  /*!< 1 time quantum */
#define CAN_BS1_2tq                 ((uint8_t)0x01)  /*!< 2 time quantum */
#define CAN_BS1_3tq                 ((uint8_t)0x02)  /*!< 3 time quantum */
#define CAN_BS1_4tq                 ((uint8_t)0x03)  /*!< 4 time quantum */
#define CAN_BS1_5tq                 ((uint8_t)0x04)  /*!< 5 time quantum */
#define CAN_BS1_6tq                 ((uint8_t)0x05)  /*!< 6 time quantum */
#define CAN_BS1_7tq                 ((uint8_t)0x06)  /*!< 7 time quantum */
#define CAN_BS1_8tq                 ((uint8_t)0x07)  /*!< 8 time quantum */
#define CAN_BS1_9tq                 ((uint8_t)0x08)  /*!< 9 time quantum */
#define CAN_BS1_10tq                ((uint8_t)0x09)  /*!< 10 time quantum */
#define CAN_BS1_11tq                ((uint8_t)0x0A)  /*!< 11 time quantum */
#define CAN_BS1_12tq                ((uint8_t)0x0B)  /*!< 12 time quantum */
#define CAN_BS1_13tq                ((uint8_t)0x0C)  /*!< 13 time quantum */
#define CAN_BS1_14tq                ((uint8_t)0x0D)  /*!< 14 time quantum */
#define CAN_BS1_15tq                ((uint8_t)0x0E)  /*!< 15 time quantum */
#define CAN_BS1_16tq                ((uint8_t)0x0F)  /*!< 16 time quantum */

#define IS_CAN_BS1(BS1) ((BS1) <= CAN_BS1_16tq)

        PBS2取值范围是1~8Tq:

#define CAN_BS2_1tq                 ((uint8_t)0x00)  /*!< 1 time quantum */
#define CAN_BS2_2tq                 ((uint8_t)0x01)  /*!< 2 time quantum */
#define CAN_BS2_3tq                 ((uint8_t)0x02)  /*!< 3 time quantum */
#define CAN_BS2_4tq                 ((uint8_t)0x03)  /*!< 4 time quantum */
#define CAN_BS2_5tq                 ((uint8_t)0x04)  /*!< 5 time quantum */
#define CAN_BS2_6tq                 ((uint8_t)0x05)  /*!< 6 time quantum */
#define CAN_BS2_7tq                 ((uint8_t)0x06)  /*!< 7 time quantum */
#define CAN_BS2_8tq                 ((uint8_t)0x07)  /*!< 8 time quantum */

        SJW取值范围是1~4Tq:

#define CAN_SJW_1tq                 ((uint8_t)0x00)  /*!< 1 time quantum */
#define CAN_SJW_2tq                 ((uint8_t)0x01)  /*!< 2 time quantum */
#define CAN_SJW_3tq                 ((uint8_t)0x02)  /*!< 3 time quantum */
#define CAN_SJW_4tq                 ((uint8_t)0x03)  /*!< 4 time quantum */

#define IS_CAN_SJW(SJW) (((SJW) == CAN_SJW_1tq) || ((SJW) == CAN_SJW_2tq)|| \
                         ((SJW) == CAN_SJW_3tq) || ((SJW) == CAN_SJW_4tq))

        对于剩下的参数:

TTCM:置1,开启时间触发通信功能;置0,关闭时间触发通信功能。CAN外设内置一个16位的计数器,用于记录时间戳,TTCM置1后,该计数器在每个CAN位的时间自增一次,溢出后归零。

ABOM:置1,开启离线自动恢复,进入离线状态后,就自动开启恢复过程;置0,关闭离线自动恢复,软件必须先请求进入然后再退出初始化模式,随后恢复过程才被开启。

AWUM:置1,自动唤醒,一旦检测到CAN总线活动,硬件就自动清零SLEEP,唤醒CAN外设;置0,手动唤醒,软件清零SLEEP,唤醒CAN外设。


NART:置1,关闭自动重传,CAN报文只被发送1次,不管发送的结果如何(成功、出错或仲裁丢失);置0,自动重传,CAN硬件在发送报文失败时会一直自动重传直到发送成功。

        该函数询问是否关闭自动重传,因为STM32默认自动重传,我们不想关闭,因此置0。


RFLM:置1,接收FIFO锁定,FIFO溢出时,新收到的报文会被丢弃;置0,禁用FIFO锁定,FIFO溢出时,FIFO中最后收到的报文被新报文覆盖。

        根据自己需求,这里也置0了。


TXFP:置1,优先级由发送请求的顺序来决定,先请求的先发送;置0,优先级由报文标识符来决定,标识符值(ID号)小的先发送(标识符值相等时,邮箱号小的报文先发送)。

        根据需求,这里我们选择ID号小的先发送,因此置0。 


        下面是完整的模式配置初始化代码:

void CAN_Mode_Config(void)
{
	CAN_InitTypeDef CAN_InitStructure;	

	CAN_DeInit(CAN1);
	CAN_StructInit(&CAN_InitStructure);

	CAN_InitStructure.CAN_TTCM=DISABLE;			 //MCR-TTCM  关闭时间触发通信模式使能  ENABLE
	CAN_InitStructure.CAN_ABOM=DISABLE;			 //MCR-ABOM  关闭自动离线管理   
	CAN_InitStructure.CAN_AWUM=DISABLE;			 //MCR-AWUM  关闭自动唤醒模式  
	CAN_InitStructure.CAN_NART=DISABLE;			 //MCR-NART  禁止报文自动重传       
	CAN_InitStructure.CAN_RFLM=DISABLE;			 //MCR-RFLM  接收FIFO锁定模式    DISABLE  溢出时新报文会覆盖原有报文
	CAN_InitStructure.CAN_TXFP=DISABLE;			 //MCR-TXFP  发送FIFO优先级      DISABLE  优先级取决于报文标识符ID
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;  //CAN_Mode_LoopBack  回环工作模式     CAN_Mode_Normal   正常工作模式

	//波特率相关
    //ss1=1 bs1=9 bs2=8 位时间宽度为(1+9+8) 波特率即为时钟周期tq*(1+9+8)	
	//采样点位于PBS1和PBS2之间
	CAN_InitStructure.CAN_SJW=CAN_SJW_1tq;	 //BTR-SJW  重新同步跳跃宽度 1个时间单元  
	CAN_InitStructure.CAN_BS1=CAN_BS1_9tq;	 //BTR-TS1  时间段1  占用9个时间单元  
	CAN_InitStructure.CAN_BS2=CAN_BS2_8tq;	 //BTR-TS2  时间段2  占用8个时间单元	
	//(CAN时钟频率APB1 = 36 MHz)
	CAN_InitStructure.CAN_Prescaler =4;		   //BTR-BRP  波特率分频器  定义了时间单元的时间长度 36000/[(1+9+8)*4]=500Kbps

    CAN_Init(CAN1, &CAN_InitStructure);    	
}

9.1.3  过滤器配置

        这里主要对CAN过滤器,进行一个初始化操作,其中CAN_FilterInitTypeDef所包含的内容,可以再stm32f10x_can.h中找到:

        简单翻译一下:

typedef struct
{
  uint16_t CAN_FilterIdHigh;         /*!< 指定过滤器标识符的高16位(对于32位配置中的高16位,16位配置中的第一个标识符)。
                                           该参数的值范围是 0x0000 到 0xFFFF。 */

  uint16_t CAN_FilterIdLow;          /*!< 指定过滤器标识符的低16位(对于32位配置中的低16位,16位配置中的第二个标识符)。
                                           该参数的值范围是 0x0000 到 0xFFFF。 */

  uint16_t CAN_FilterMaskIdHigh;     /*!< 指定过滤器掩码的高16位或标识符(根据模式,32位配置中的高16位,16位配置中的第一个掩码)。
                                           该参数的值范围是 0x0000 到 0xFFFF。 */

  uint16_t CAN_FilterMaskIdLow;      /*!< 指定过滤器掩码的低16位或标识符(根据模式,32位配置中的低16位,16位配置中的第二个掩码)。
                                           该参数的值范围是 0x0000 到 0xFFFF。 */

  uint16_t CAN_FilterFIFOAssignment; /*!< 指定将分配给过滤器的FIFO(0 或 1)。
                                           该参数的值可以是 @ref CAN_filter_FIFO 的一个值。 */

  uint8_t CAN_FilterNumber;          /*!< 指定将要初始化的过滤器。范围从 0 到 13。 */

  uint8_t CAN_FilterMode;            /*!< 指定要初始化的过滤器模式。
                                           该参数可以是 @ref CAN_filter_mode 中的一个值。 */

  uint8_t CAN_FilterScale;           /*!< 指定过滤器的规模(16位或32位)。
                                           该参数可以是 @ref CAN_filter_scale 中的一个值。 */

  FunctionalState CAN_FilterActivation; /*!< 启用或禁用过滤器。
                                             该参数可以设置为 ENABLE 或 DISABLE。 */
} CAN_FilterInitTypeDef;

        这里可以参考上面的过滤器章节:

        下面这四个函数分别代表R1高16位和低16位,R2高16位和低16位: 

uint16_t CAN_FilterIdHigh;         /*!< 指定过滤器标识符的高16位(对于32位配置中的高16位,16位配置中的第一个标识符)。
                                           该参数的值范围是 0x0000 到 0xFFFF。 */

uint16_t CAN_FilterIdLow;          /*!< 指定过滤器标识符的低16位(对于32位配置中的低16位,16位配置中的第二个标识符)。
                                           该参数的值范围是 0x0000 到 0xFFFF。 */

uint16_t CAN_FilterMaskIdHigh;     /*!< 指定过滤器掩码的高16位或标识符(根据模式,32位配置中的高16位,16位配置中的第一个掩码)。
                                           该参数的值范围是 0x0000 到 0xFFFF。 */

uint16_t CAN_FilterMaskIdLow;      /*!< 指定过滤器掩码的低16位或标识符(根据模式,32位配置中的低16位,16位配置中的第二个掩码)。
                                           该参数的值范围是 0x0000 到 0xFFFF。 */

        对于模式的选择:

#define CAN_FilterMode_IdMask       ((uint8_t)0x00)  /*!< identifier/mask mode标识符屏蔽位模式 */
#define CAN_FilterMode_IdList       ((uint8_t)0x01)  /*!< identifier list mode列表模式 */

#define IS_CAN_FILTER_MODE(MODE) (((MODE) == CAN_FilterMode_IdMask) || \
                                  ((MODE) == CAN_FilterMode_IdList))

         对于位宽:

#define CAN_FilterScale_16bit       ((uint8_t)0x00) /*!< Two 16-bit filters */
#define CAN_FilterScale_32bit       ((uint8_t)0x01) /*!< One 32-bit filter */

#define IS_CAN_FILTER_SCALE(SCALE) (((SCALE) == CAN_FilterScale_16bit) || \
                                    ((SCALE) == CAN_FilterScale_32bit))

        关联的过滤器选择:

#define CAN_Filter_FIFO0             ((uint8_t)0x00)  /*!< Filter FIFO 0 assignment for filter x */
#define CAN_Filter_FIFO1             ((uint8_t)0x01)  /*!< Filter FIFO 1 assignment for filter x */
#define IS_CAN_FILTER_FIFO(FIFO) (((FIFO) == CAN_FilterFIFO0) || \
                                  ((FIFO) == CAN_FilterFIFO1))

        过滤器初始化完整的代码配置:

//CAN的过滤器配置
void CAN_Filter_Config(void)
{
	CAN_FilterInitTypeDef  CAN_FilterInitStructure;

	CAN_FilterInitStructure.CAN_FilterNumber=0;//过滤器组0						
	CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;//	标识符屏蔽位模式     IdList 标识符列表模式 
	CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit;//过滤器位宽为单个32位

	CAN_FilterInitStructure.CAN_FilterIdHigh= 0;//要筛选的ID高位		
	CAN_FilterInitStructure.CAN_FilterIdLow= 0;//要筛选的ID低位 
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh= 0;			
	CAN_FilterInitStructure.CAN_FilterMaskIdLow= 0;	
		

	CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0 ;//过滤器被关联到FIFO0			
	CAN_FilterInitStructure.CAN_FilterActivation=ENABLE;//使能过滤器			
	CAN_FilterInit(&CAN_FilterInitStructure);

}

9.1.4  初始化整合

        对于以上3个小节就是完整的CAN初始化操作,只需要创建一个函数将三者整合:

//CAN完整功能配置
void CAN_Config(void)
{
  CAN_GPIO_Config();
  CAN_Mode_Config();
  CAN_Filter_Config();   
}

        主函数初始化即可,这是可能会有人说,将所有的初始化操作写的一个函数不就行了,也是可以的,以下是整合在一起的代码:

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
	CAN_InitStructure.CAN_Prescaler = 48;		//波特率 = 36M / 48 / (1 + 2 + 3) = 125K
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

9.1.5  发送函数

        想要发送数据,你需要先了解一帧数据都包含了哪些内容,可以参考:

CAN总线数据帧格式详细介绍-CSDN博客

        这里是STM32的一些有关发送函数的结构体定义:

​        翻译一下:

typedef struct
{
  uint32_t StdId;  /*!< 指定标准标识符。
                        该参数的值范围从 0 到 0x7FF。 */

  uint32_t ExtId;  /*!< 指定扩展标识符。
                        该参数的值范围从 0 到 0x1FFFFFFF。 */

  uint8_t IDE;     /*!< 指定将要传输的消息的标识符类型。
                        该参数可以是 @ref CAN_identifier_type 中的一个值。 */

  uint8_t RTR;     /*!< 指定将要传输的帧类型。
                        该参数可以是 @ref CAN_remote_transmission_request 中的一个值。 */

  uint8_t DLC;     /*!< 指定将要传输的帧的长度。
                        该参数的值范围从 0 到 8。 */

  uint8_t Data[8]; /*!< 包含将要传输的数据。数据范围从 0 到 0xFF。 */
} CanTxMsg;

        那么我们可以先创建一个函数,其中ID是消息的表示符,若是我们使用标准ID,则IDE位需要声明CAN_Id_Standard,要是需要使用扩展ID,则IDE位需要声明CAN_Id_Extended:

        对于RTR位,根据自己需求看是需要遥控帧还是数据帧,这里我们想要使用数据帧,因此声明CAN_RTR_Data:

        然后我们需要知道数据长度,声明一个值Length,方便修改,然后生命一个数组Data存储想要发送的具体数据:

void MyCAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *Data)
{
	CanTxMsg TxMessage;
	TxMessage.StdId = ID;
	TxMessage.ExtId = ID;
	TxMessage.IDE = CAN_Id_Standard;		
	TxMessage.RTR = CAN_RTR_Data;
	TxMessage.DLC = Length;
	for (uint8_t i = 0; i < Length; i ++)
	{
		TxMessage.Data[i] = Data[i];
	}
	
	CAN_Transmit(CAN1, &TxMessage);
	
}

        走后通过调用CAN_Transmit函数,就会将想要发送的报文写入发送邮箱,并有管理员发送。对于发送邮箱内部的工作,这里就不进行过多的描述可以参考:

        代码部分可以go to看一下:

uint8_t CAN_Transmit(CAN_TypeDef* CANx, CanTxMsg* TxMessage)
{
  uint8_t transmit_mailbox = 0;
  /* Check the parameters */
  assert_param(IS_CAN_ALL_PERIPH(CANx));
  assert_param(IS_CAN_IDTYPE(TxMessage->IDE));
  assert_param(IS_CAN_RTR(TxMessage->RTR));
  assert_param(IS_CAN_DLC(TxMessage->DLC));

  /* Select one empty transmit mailbox */
  if ((CANx->TSR&CAN_TSR_TME0) == CAN_TSR_TME0)
  {
    transmit_mailbox = 0;
  }
  else if ((CANx->TSR&CAN_TSR_TME1) == CAN_TSR_TME1)
  {
    transmit_mailbox = 1;
  }
  else if ((CANx->TSR&CAN_TSR_TME2) == CAN_TSR_TME2)
  {
    transmit_mailbox = 2;
  }
  else
  {
    transmit_mailbox = CAN_TxStatus_NoMailBox;
  }

  if (transmit_mailbox != CAN_TxStatus_NoMailBox)
  {
    /* Set up the Id */
    CANx->sTxMailBox[transmit_mailbox].TIR &= TMIDxR_TXRQ;
    if (TxMessage->IDE == CAN_Id_Standard)
    {
      assert_param(IS_CAN_STDID(TxMessage->StdId));  
      CANx->sTxMailBox[transmit_mailbox].TIR |= ((TxMessage->StdId << 21) | \
                                                  TxMessage->RTR);
    }
    else
    {
      assert_param(IS_CAN_EXTID(TxMessage->ExtId));
      CANx->sTxMailBox[transmit_mailbox].TIR |= ((TxMessage->ExtId << 3) | \
                                                  TxMessage->IDE | \
                                                  TxMessage->RTR);
    }
    
    /* Set up the DLC */
    TxMessage->DLC &= (uint8_t)0x0000000F;
    CANx->sTxMailBox[transmit_mailbox].TDTR &= (uint32_t)0xFFFFFFF0;
    CANx->sTxMailBox[transmit_mailbox].TDTR |= TxMessage->DLC;

    /* Set up the data field */
    CANx->sTxMailBox[transmit_mailbox].TDLR = (((uint32_t)TxMessage->Data[3] << 24) | 
                                             ((uint32_t)TxMessage->Data[2] << 16) |
                                             ((uint32_t)TxMessage->Data[1] << 8) | 
                                             ((uint32_t)TxMessage->Data[0]));
    CANx->sTxMailBox[transmit_mailbox].TDHR = (((uint32_t)TxMessage->Data[7] << 24) | 
                                             ((uint32_t)TxMessage->Data[6] << 16) |
                                             ((uint32_t)TxMessage->Data[5] << 8) |
                                             ((uint32_t)TxMessage->Data[4]));
    /* Request transmission */
    CANx->sTxMailBox[transmit_mailbox].TIR |= TMIDxR_TXRQ;
  }
  return transmit_mailbox;
}

        不过需要注意,邮箱发送不一定成功,因此我们需要检查一下邮箱发送是否成功,我们可以调用库函数解读时的这个函数:

//获取CAN消息发送状态, 获取指定发送邮箱(TransmitMailbox)的消息传输状态。返回的状态可以用来确认消息是否已经成功发送或正在发送中
uint8_t CAN_TransmitStatus(CAN_TypeDef* CANx, uint8_t TransmitMailbox);
void MyCAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *Data)
{
	CanTxMsg TxMessage;
	TxMessage.StdId = ID;
	TxMessage.ExtId = ID;
	TxMessage.IDE = CAN_Id_Standard;		
	TxMessage.RTR = CAN_RTR_Data;
	TxMessage.DLC = Length;
	for (uint8_t i = 0; i < Length; i ++)
	{
		TxMessage.Data[i] = Data[i];
	}
	
    uint8_t TransmitMailbox = CAN_Transmit(CAN1, &TxMessage);
    while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok)
	
}

        不过这样可能会出现一直发送不成功,程序卡死的现象,因此我们可以加一个自增,用于退出一直发送不成功退出循环:

void MyCAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *Data)
{
    CanTxMsg TxMessage;               // 创建一个 CAN 传输消息结构体

    TxMessage.StdId = ID;             // 设置标准标识符(StdId),这里假设使用的是标准标识符
    TxMessage.ExtId = ID;             // 设置扩展标识符(ExtId),此处与标准标识符相同
    TxMessage.IDE = CAN_Id_Standard;  // 设置标识符类型为标准标识符(CAN_ID_STD)
    TxMessage.RTR = CAN_RTR_Data;     // 设置帧类型为数据帧(CAN_RTR_Data,非远程请求帧)
    TxMessage.DLC = Length;           // 设置数据长度代码(DLC),即数据字段的长度
    
    // 将传入的数据填充到 TxMessage 的 Data 数组中
    for (uint8_t i = 0; i < Length; i++)
    {
        TxMessage.Data[i] = Data[i];  // 把数据内容逐字节复制到 TxMessage.Data 中
    }
    
    // 发送CAN消息并获取发送邮箱编号
    uint8_t TransmitMailbox = CAN_Transmit(CAN1, &TxMessage);

    uint32_t Timeout = 0;  // 定义一个超时计数器
    // 等待直到消息成功发送或超时
    while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok)
    {
        Timeout++;  // 增加超时计数器
        if (Timeout > 100000)  // 如果超过100000次循环,则退出(认为超时)
        {
            break;  // 超时,退出循环
        }
    }
}

        以上代码就是完整的发送部分。

9.1.6  接收函数

        接收的方式有两种:一种中断接收,一种查询接收。这里我们先讲查询接收,中断下面讲。

        首先我们需要判断邮箱(我们之前配置的是FIFO 0 )是否接受到消息:

uint8_t MyCAN_ReceiveFlag(void)
{
    // 检查CAN1 FIFO0队列中是否有待处理的消息
    if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0)
    {
        return 1;  // 如果有消息,返回1
    }
    return 0;      // 如果没有消息,返回0
}

        这里参考上面库函数解读的代码:

// 检查CAN接收FIFO是否有待处理的消息,检查指定FIFO(FIFONumber)中是否有待处理的消息。如果有,函数返回消息的数量;如果没有,返回0
uint8_t CAN_MessagePending(CAN_TypeDef* CANx, uint8_t FIFONumber);

         然后我们需要接收CAN消息:

CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

        这里参考上面库函数解读的代码:

//接收CAN消息,从指定的接收FIFO(FIFONumber)中接收CAN消息,并将接收到的消息存储在RxMessage中。CANx表示使用的CAN控制器
void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage);

        然后对消息类型和帧格式进行判断:

	if (RxMessage.IDE == CAN_Id_Standard)
	{
		*ID = RxMessage.StdId;
	}
	else
	{
		*ID = RxMessage.ExtId;
	}
	
	if (RxMessage.RTR == CAN_RTR_Data)
	{
		*Length = RxMessage.DLC;
		for (uint8_t i = 0; i < *Length; i ++)
		{
			Data[i] = RxMessage.Data[i];
		}
	}
	else
	{
		//...
	}

        完整代码如下:

uint8_t MyCAN_ReceiveFlag(void)
{
    // 检查CAN1 FIFO0队列中是否有待处理的消息
    if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0)
    {
        return 1;  // 如果有消息,返回1
    }
    return 0;      // 如果没有消息,返回0
}

void MyCAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *Data)
{
    CanRxMsg RxMessage;  // 创建接收消息结构体RxMessage
    CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);  // 从CAN1的FIFO0队列接收一条消息

    // 根据接收到的消息类型设置ID
    if (RxMessage.IDE == CAN_Id_Standard)
    {
        *ID = RxMessage.StdId;  // 如果是标准标识符(11位),将StdId赋值给ID
    }
    else
    {
        *ID = RxMessage.ExtId;  // 如果是扩展标识符(29位),将ExtId赋值给ID
    }

    // 如果接收到的数据帧(非远程请求帧)
    if (RxMessage.RTR == CAN_RTR_Data)
    {
        *Length = RxMessage.DLC;  // 设置数据长度为DLC字段的值
        for (uint8_t i = 0; i < *Length; i++)
        {
            Data[i] = RxMessage.Data[i];  // 将接收到的数据复制到Data数组中
        }
    }
    else
    {
        // 远程请求帧的处理(如果需要处理的话)
        // 此处可以根据需求进行扩展,或者留空
    }
}

9.1.7  主函数

        再开始前我们先将以上代码在.h文件进行声明,方便调用:

#ifndef __MYCAN_H
#define __MYCAN_H

void MyCAN_Init(void);
void MyCAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *Data);
uint8_t MyCAN_ReceiveFlag(void);
void MyCAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *Data);

#endif

        因为现在是环回模式,因此自发自收,我们创建一个数组,每次点击按键数组数据加一,向外发送数据,然后自己饿就会接收:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"

uint8_t KeyNum;
uint32_t TxID = 0x555;
uint8_t TxLength = 4;
uint8_t TxData[8] = {0x11, 0x22, 0x33, 0x44};

uint32_t RxID;
uint8_t RxLength;
uint8_t RxData[8];

int main(void)
{
	OLED_Init();
	Key_Init();
	MyCAN_Init();
	
	OLED_ShowString(1, 1, "TxID:");
	OLED_ShowHexNum(1, 6, TxID, 3);
	OLED_ShowString(2, 1, "RxID:");
	OLED_ShowString(3, 1, "Leng:");
	OLED_ShowString(4, 1, "Data:");
	
	while (1)
	{
		KeyNum = Key_GetNum();
		
		if (KeyNum == 1)
		{
			TxData[0] ++;
			TxData[1] ++;
			TxData[2] ++;
			TxData[3] ++;
			
			MyCAN_Transmit(TxID, TxLength, TxData);
		}
		
		if (MyCAN_ReceiveFlag())
		{
			MyCAN_Receive(&RxID, &RxLength, RxData);
			
			OLED_ShowHexNum(2, 6, RxID, 3);
			OLED_ShowHexNum(3, 6, RxLength, 1);
			OLED_ShowHexNum(4, 6, RxData[0], 2);
			OLED_ShowHexNum(4, 9, RxData[1], 2);
			OLED_ShowHexNum(4, 12, RxData[2], 2);
			OLED_ShowHexNum(4, 15, RxData[3], 2);
		}
	}
}

完整代码:

基于STM32F1系列CAN总线环回测试.zip资源-CSDN文库

9.2  设备间相互通讯代码编写

        想要实现设备间的相互通讯,代码和环回测试的代码只有一个地方的区别,找到换回测试代码的初始化整合代码,只需要将下面注释部分进行更改即可:

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;//CAN_Mode_LoopBack  环回工作模式     CAN_Mode_Normal   正常工作模式
	CAN_InitStructure.CAN_Prescaler = 48;		
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

        除此之外,我们在主函数中,也可以对发送ID进行更改:

uint32_t TxID = 0x555;//更改0x666,0x777
uint8_t TxLength = 4;
uint8_t TxData[8] = {0x11, 0x22, 0x33, 0x44};

        这样可以使现象更明显,并且当发送冲突时,也可以进行仲裁,否则可能造成数据损坏。对于仲裁规则可参考:

CAN总线位同步的使用以及总线仲裁规则详解-CSDN博客

        其他代码,完全参考环回测试的代码。

9.3  接收不同帧格式代码编写

9.3.1  初始化

        该小节也是在环回测试的基础上进行更改,初始化代码不用进行更改,直接照抄即可(详细想要了解看环回测试小节即可):

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
	CAN_InitStructure.CAN_Prescaler = 48;		//波特率 = 36M / 48 / (1 + 2 + 3) = 125K
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

9.3.2  发送函数

        在环回测试的代码,我们只能对ID,数据长度以及数据进行更改,并且只能写一个发送函数,不利于我们观察实验现象,若是我们将IDE和RTR也设成可调的,这样就会有五个参数,并且我们对CanTxMsg的封装也没有意义了。

        更改方法,我们直接给函数的形参换成CanTxMsg *TxMessage,结构体用地址传递,提高效率:

// 定义一个用于CAN消息发送的函数
void MyCAN_Transmit(CanTxMsg *TxMessage)
{
    // 调用CAN_Transmit函数将TxMessage消息传输到CAN总线,并返回一个发送邮箱编号
    uint8_t TransmitMailbox = CAN_Transmit(CAN1, TxMessage);
    
    // 定义一个超时计数器
    uint32_t Timeout = 0;
    
    // 在此循环中,检查发送邮箱的传输状态,直到CAN消息发送成功
    // CAN_TxStatus_Ok表示消息成功发送
    while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok)
    {
        Timeout++;  // 增加超时计数器
        
        // 如果超时计数器超过100000,则退出循环,避免死循环
        if (Timeout > 100000)
        {
            break;  // 超时退出
        }
    }
}

9.3.3  接收函数

        判断邮箱(我们之前配置的是FIFO 0 )是否接受到消息函数不需要进行变动:

uint8_t MyCAN_ReceiveFlag(void)
{
	if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0)
	{
		return 1;
	}
	return 0;
}

        但对于数据接收,我们同样的,需要将参数改为CanRxMsg *RxMessage,然后之前写入的一些内部赋值可以删去,更改完的代码:

void MyCAN_Receive(CanRxMsg *RxMessage)
{
	CAN_Receive(CAN1, CAN_FIFO0, RxMessage);
}

9.3.4  主函数

        再开始前我们需要将以上代码在.h文件重新进行声明,方便调用:

#ifndef __MYCAN_H
#define __MYCAN_H

void MyCAN_Init(void);
void MyCAN_Transmit(CanTxMsg *TxMessage);
uint8_t MyCAN_ReceiveFlag(void);
void MyCAN_Receive(CanRxMsg *RxMessage);

#endif

        对于主函数,之前我们发送数据用的单个变量,把他们删掉,我们重新定义结构体变量,创建四个参数分别是标准格式数据帧、扩展格式数据帧、标准格式遥控帧、扩展格式遥控帧:

CanTxMsg TxMsgArray[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]          */
	{0x555, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x12345678, CAN_Id_Extended, CAN_RTR_Data,   4, {0xAA, 0xBB, 0xCC, 0xDD}},
	{0x666, 0x00000000, CAN_Id_Standard, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
	{0x000, 0x0789ABCD, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
};

        然后我们在创建一个索引:

uint8_t pTxMsgArray = 0;

        通过索引,用来循环调用上方数组数据,进行循环发送: 

// 判断是否按下了第一个按键
if (KeyNum == 1)
{
    // 调用 MyCAN_Transmit 函数,传输当前的 CAN 消息
    MyCAN_Transmit(&TxMsgArray[pTxMsgArray]);
    
    // 更新发送消息的索引,指向下一个待发送的消息
    pTxMsgArray ++;
    
    // 如果已经到达消息数组的最后一个元素,则重置索引为 0
    if (pTxMsgArray >= sizeof(TxMsgArray) / sizeof(CanTxMsg))
    {
        pTxMsgArray = 0;
    }
}

        同理接收数据也进行修改,因为我们在接收函数的更改,在接收函数内只放了接收函数,对于数据的处理放到了主函数这边,首先我们先声明变量:

CanRxMsg RxMsg;

        因为现在我们设的是环回测试模式,因此自发自收,我们可以创建函数进行分辨发送函数的四种帧格式,分别对不同帧格式进行接收判断,做出不同的显示:

// 检查是否有接收到 CAN 消息
if (MyCAN_ReceiveFlag())
{
    // 如果有接收到消息,调用 MyCAN_Receive 函数接收数据
    MyCAN_Receive(&RxMsg);
    
    // 判断接收到的消息是否是标准格式(Standard ID)
    if (RxMsg.IDE == CAN_Id_Standard)
    {
        // 在 OLED 屏幕上显示 "Std" 字样,表示标准格式的 CAN 消息
        OLED_ShowString(1, 6, "Std");
        
        // 显示接收到的标准 ID(标准帧的 ID),以 8 位十六进制形式显示
        OLED_ShowHexNum(2, 6, RxMsg.StdId, 8);
    }
    // 判断接收到的消息是否是扩展格式(Extended ID)
    else if (RxMsg.IDE == CAN_Id_Extended)
    {
        // 在 OLED 屏幕上显示 "Ext" 字样,表示扩展格式的 CAN 消息
        OLED_ShowString(1, 6, "Ext");
        
        // 显示接收到的扩展 ID(扩展帧的 ID),以 8 位十六进制形式显示
        OLED_ShowHexNum(2, 6, RxMsg.ExtId, 8);
    }
    
    // 判断接收到的消息是否为数据帧(Data Frame)
    if (RxMsg.RTR == CAN_RTR_Data)
    {
        // 在 OLED 屏幕上显示 "Data" 字样,表示是数据帧
        OLED_ShowString(1, 10, "Data  ");
        
        // 显示接收到的消息数据长度(DLC),以 1 位十六进制形式显示
        OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);
        
        // 显示数据帧中的前四个数据字节
        OLED_ShowHexNum(4, 6, RxMsg.Data[0], 2);
        OLED_ShowHexNum(4, 9, RxMsg.Data[1], 2);
        OLED_ShowHexNum(4, 12, RxMsg.Data[2], 2);
        OLED_ShowHexNum(4, 15, RxMsg.Data[3], 2);
    }
    // 判断接收到的消息是否为远程帧(Remote Frame)
    else if (RxMsg.RTR == CAN_RTR_Remote)
    {
        // 在 OLED 屏幕上显示 "Remote" 字样,表示是远程帧
        OLED_ShowString(1, 10, "Remote");
        
        // 显示接收到的消息数据长度(DLC),以 1 位十六进制形式显示
        OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);
        
        // 远程帧没有数据,所以显示四个 0
        OLED_ShowHexNum(4, 6, 0x00, 2);
        OLED_ShowHexNum(4, 9, 0x00, 2);
        OLED_ShowHexNum(4, 12, 0x00, 2);
        OLED_ShowHexNum(4, 15, 0x00, 2);
    }
}

完整主函数:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"

uint8_t KeyNum;

CanTxMsg TxMsgArray[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]          */
	{0x555, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x12345678, CAN_Id_Extended, CAN_RTR_Data,   4, {0xAA, 0xBB, 0xCC, 0xDD}},
	{0x666, 0x00000000, CAN_Id_Standard, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
	{0x000, 0x0789ABCD, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
};

uint8_t pTxMsgArray = 0;

CanRxMsg RxMsg;

int main(void)
{
	OLED_Init();
	Key_Init();
	MyCAN_Init();
	
	OLED_ShowString(1, 1, " Rx :");
	OLED_ShowString(2, 1, "RxID:");
	OLED_ShowString(3, 1, "Leng:");
	OLED_ShowString(4, 1, "Data:");
	
	while (1)
	{
		KeyNum = Key_GetNum();
		
		if (KeyNum == 1)
		{
			MyCAN_Transmit(&TxMsgArray[pTxMsgArray]);
			
			pTxMsgArray ++;
			if (pTxMsgArray >= sizeof(TxMsgArray) / sizeof(CanTxMsg))
			{
				pTxMsgArray = 0;
			}
		}
		
		if (MyCAN_ReceiveFlag())
		{
			MyCAN_Receive(&RxMsg);
			
			if (RxMsg.IDE == CAN_Id_Standard)
			{
				OLED_ShowString(1, 6, "Std");
				
				OLED_ShowHexNum(2, 6, RxMsg.StdId, 8);
			}
			else if (RxMsg.IDE == CAN_Id_Extended)
			{
				OLED_ShowString(1, 6, "Ext");
				
				OLED_ShowHexNum(2, 6, RxMsg.ExtId, 8);
			}
			
			if (RxMsg.RTR == CAN_RTR_Data)
			{
				OLED_ShowString(1, 10, "Data  ");
				
				OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);
				
				OLED_ShowHexNum(4, 6, RxMsg.Data[0], 2);
				OLED_ShowHexNum(4, 9, RxMsg.Data[1], 2);
				OLED_ShowHexNum(4, 12, RxMsg.Data[2], 2);
				OLED_ShowHexNum(4, 15, RxMsg.Data[3], 2);
			}
			else if (RxMsg.RTR == CAN_RTR_Remote)
			{
				OLED_ShowString(1, 10, "Remote");
				
				OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);
				
				OLED_ShowHexNum(4, 6, 0x00, 2);
				OLED_ShowHexNum(4, 9, 0x00, 2);
				OLED_ShowHexNum(4, 12, 0x00, 2);
				OLED_ShowHexNum(4, 15, 0x00, 2);
			}
		}
	}
}

完整工程代码:基于STM32的CAN总线环回测试接收不同帧格式代码.zip资源-CSDN文库

9.4  标识符过滤器代码编写

        我们之前那举的例子,现在代码实现一下:

总线上存在的ID想要接收的ID过滤器模式R1[31:0]配置值R2[31:0]配置值
0x123, 0x234, 0x345, 0x456, 0x567, 0x6780x234, 0x345, 0x56716位/列表

ID: R1[15:0]=0x234<<5

ID: R1[31:16]=0x345<<5

ID: R2[15:0]=0x567<<5

ID: R2[31:16]=0x000<<5

0x100~0x1FF, 0x200~0x2FF, 0x310~0x31F, 0x320~0x32F0x200~0x2FF, 0x320~0x32F16位/屏蔽

ID: R1[15:0]=0x200<<5

Mask: R1[31:16]= (0x700<<5)|0x10|0x8

ID: R2[15:0]=0x320<<5

Mask: R2[31:16]= (0x7F0<<5)|0x10|0x8

0x123, 0x234, 0x345, 0x456, 0x12345678, 0x0789ABCD0x123, 0x1234567832位/列表ID: R1[31:0]= 0x123<<21ID: R2[31:0]= (0x12345678<<3)|0x4
0x12345600~ 0x123456FF, 0x0789AB00~ 0x0789ABFF0x12345600~ 0x123456FF32位/屏蔽ID: R1[31:0]= (0x12345600<<3)|0x4Mask: R2[31:0]= (0x1FFFFF00<<3)|0x4|0x2

9.4.1  16位列表模式

        该代码是在接收不同帧格式代码基础上进行修改的,首先是对主函数的帧数据进行修改,按照上方举例的6位列表模式将ID列出:

CanTxMsg TxMsgArray[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]          */
	{0x123, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x234, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x345, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x456, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x567, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x678, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
};

        主函数其他地方数据不需要动。

        然后是CAN总线配置,接收和发送函数不需要动,对初始化配置需要进行一些修改,主要是过滤器的修改(注释部分):

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
	CAN_InitStructure.CAN_Prescaler = 48;		
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;//过滤器0

    //这3个ID不分先后顺序,最后一个不需要补0
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x234 << 5;
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x345 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x567 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x000 << 5;

	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit;//这里修改为16位
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList;//	IdMask 标识符屏蔽位模式     IdList 标识符列表模式 
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

        至于ID为什么需要左移5位,这里就需要看一下,数据手册:

​​

​​

        由于映像的ID在数据是右对齐,而赋值数据是左对齐,因此需要将ID数据进行左移,又因为我们现在是16位列表模式,发送的是数据帧因此RTR=0,IDE=0,且后三位为空,补零,若是遥控帧,则左移后还需要或0x10,让RTR位等于1,代码如下:

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
	CAN_InitStructure.CAN_Prescaler = 48;		
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;//过滤器0

    //这3个ID不分先后顺序,最后一个不需要补0
	CAN_FilterInitStructure.CAN_FilterIdHigh = (0x234 << 5) | 0x10;//这样就是0x234的遥控帧
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x345 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x567 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x000 << 5;

	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit;//这里修改为16位
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList;//	IdMask 标识符屏蔽位模式     IdList 标识符列表模式 
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

        至此数据更改完成,完整代码如下:

基于STM32的CAN总线16位列表模式.zip资源-CSDN文库

拓展:

        如果想要更多的过滤ID,可以配备不同的过滤器,以及可以让他们进不同的FIFO:

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
	CAN_InitStructure.CAN_Prescaler = 48;		
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;//过滤器0
    //这3个ID不分先后顺序,最后一个不需要补0
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x234 << 5;
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x345 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x567 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x000 << 5;
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit;//这里修改为16位
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList;//	IdMask 标识符屏蔽位模式     IdList 标识符列表模式 
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);

	CAN_FilterInitStructure.CAN_FilterNumber = 1;//过滤器1
    //这3个ID不分先后顺序,最后一个不需要补0
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x123 << 5;
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x333 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x444 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x555 << 5;
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit;//这里修改为16位
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList;//	IdMask 标识符屏蔽位模式     IdList 标识符列表模式 
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO1;//不同的FIFO
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

        如果更多的ID怎么办呢?那就需要引出下面 16位屏蔽模式。

9.4.2  16位屏蔽模式

        我们先在主函数创建一堆ID,其他部分不做修改:

CanTxMsg TxMsgArray[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]          */
	{0x100, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x101, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x1FE, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x1FF, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	
	{0x200, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x201, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x2FE, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x2FF, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	
	{0x310, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x311, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x31E, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x31F, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	
	{0x320, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x321, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x32E, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x32F, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
};

        再到,CAN初始化函数进行修改:

#include "stm32f10x.h"                  // Device header

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
	CAN_InitStructure.CAN_Prescaler = 48;		
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;
	
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x200 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (0x700 << 5) | 0x10 | 0x8;
//	也可以写作(0xF00 << 5) | 0x10 | 0x8;	
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x320 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = (0x7F0 << 5) | 0x10 | 0x8;
	
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit;
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

        举个例子:

	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x200 << 5;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (0x700 << 5) | 0x10 | 0x8;

        因为是16位屏蔽位模式,这0x200和0x700分别表示:

​​

        经过左移5位后可以发现0x700和0xF00的高3位是一样的,进行运算:

​​

        这样我们就能保证RTR位为1,又因为我们是标准格式,因此IDE位为1。至于这里为什么RTR位和IDE位必须为1,若是为零,那么掩码这两位将会不生效,因为不论ID的RTR和IDE为多少,与上0解为0。

完整代码:基于STM32的CAN总线16位屏蔽模式.zip资源-CSDN文库 

9.4.3  32位列表模式

        和之前一样我们对主函数创建一堆ID,其他部分不做修改:

CanTxMsg TxMsgArray[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]          */
	{0x123, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x234, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x345, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x456, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	
	{0x000, 0x12345678, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x0789ABCD, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
};

        继续对初始化函数,标识符这里进行修改:

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
	CAN_InitStructure.CAN_Prescaler = 48;		
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;
	
	uint32_t ID1 = 0x123 << 21;
	CAN_FilterInitStructure.CAN_FilterIdHigh = ID1 >> 16;
	CAN_FilterInitStructure.CAN_FilterIdLow = ID1;
	
	uint32_t ID2 = (0x12345678u << 3) | 0x4;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = ID2 >> 16;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = ID2;
	
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList;
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

至于为什么这样,举个标准ID的例子:

	uint32_t ID1 = 0x123 << 21;
	CAN_FilterInitStructure.CAN_FilterIdHigh = ID1 >> 16;
	CAN_FilterInitStructure.CAN_FilterIdLow = ID1;

        因为 CAN_FilterIdHigh 和 CAN_FilterIdLow 都为 uint16_t类型的,因此只去低16位。

        由于一个32位过滤器,其ID的组成是CAN_FilterIdHigh代表的高16加上CAN_FilterIdLow代表的低16位组合而成,因此最终ID的形式如上图。

       

        如果是扩展ID,则只需要左移3位即可,需要注意IDE位等于1,否则会识别成标准ID。

完整代码:基于STM32的CAN总线32位列表模式.zip资源-CSDN文库

9.4.4  32位屏蔽模式

        和之前一样我们对主函数创建一堆ID,其他部分不做修改:

CanTxMsg TxMsgArray[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]          */
	{0x000, 0x12345600, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x12345601, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x123456FE, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x123456FF, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	
	{0x000, 0x0789AB00, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x0789AB01, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x0789ABFE, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x0789ABFF, CAN_Id_Extended, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
};

        继续对初始化函数,标识符这里进行修改:

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
	CAN_InitStructure.CAN_Prescaler = 48;		//波特率 = 36M / 48 / (1 + 2 + 3) = 125K
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;
	
	uint32_t ID = (0x12345600u << 3) | 0x4;
	CAN_FilterInitStructure.CAN_FilterIdHigh = ID >> 16;
	CAN_FilterInitStructure.CAN_FilterIdLow = ID;
	
	uint32_t Mask = (0x1FFFFF00u << 3) | 0x4 | 0x2;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = Mask >> 16;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = Mask;
	
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

        原因可以参考16位屏蔽,想要什么样的头,置多少位为1,注意RTR和IDE位为1。

完整代码:基于STM32的CAN总线32位屏蔽模式.zip资源-CSDN文库

9.5  中断接收代码编写

        根据库函数介绍,找到中断小节,可以看到中断使能函数,这里我们使用环回测试的代码进行更改,我们先跳转中断使能函数:

        简单翻译一下:

@brief 启用或禁用指定的 CANx 中断。

@param CANx: 选择 CAN 外设,其中 x 可以是 1 或 2,表示选择 CAN1 或 CAN2 外设。

@param CAN_IT: 指定要启用或禁用的 CAN 中断源。

该参数可以是以下值之一:

    CAN_IT_TME(传输邮件框空标志)
    CAN_IT_FMP0(接收 FIFO 0 消息挂起标志)
    CAN_IT_FF0(接收 FIFO 0 完整标志)
    CAN_IT_FOV0(接收 FIFO 0 溢出标志)
    CAN_IT_FMP1(接收 FIFO 1 消息挂起标志)
    CAN_IT_FF1(接收 FIFO 1 完整标志)
    CAN_IT_FOV1(接收 FIFO 1 溢出标志)
    CAN_IT_EWG(错误警告标志)
    CAN_IT_EPV(错误被动标志)
    CAN_IT_LEC(最后错误代码标志)
    CAN_IT_ERR(错误中断)
    CAN_IT_WKU(唤醒中断)
    CAN_IT_SLK(同步跳变中断)

@param NewState: CAN 中断的新状态。

该参数可以是:

    ENABLE(启用)
    DISABLE(禁用)

@retval None: 无返回值。

        这里我们使能FIFO 0,因此:

	CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);

        然后配置NVIC:

	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);

        根据上面的中断图,我们知道其有四个CAN终端通道,分别是:

        我们想要进行接收中断,因此:

	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;

        接收函数也要进行相应的修改:

void USB_LP_CAN1_RX0_IRQHandler(void)
{
	if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) == SET)
	{
		CAN_Receive(CAN1, CAN_FIFO0, &MyCAN_RxMsg);
		MyCAN_RxFlag = 1;
	}
}

        CAN相关配置完整代码:

#include "stm32f10x.h"                  // Device header

CanRxMsg MyCAN_RxMsg;
uint8_t MyCAN_RxFlag;

void MyCAN_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	
	CAN_InitTypeDef CAN_InitStructure;
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
	CAN_InitStructure.CAN_Prescaler = 48;		//波特率 = 36M / 48 / (1 + 2 + 3) = 125K
	CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
	CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
	CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
	CAN_InitStructure.CAN_NART = DISABLE;
	CAN_InitStructure.CAN_TXFP = DISABLE;
	CAN_InitStructure.CAN_RFLM = DISABLE;
	CAN_InitStructure.CAN_AWUM = DISABLE;
	CAN_InitStructure.CAN_TTCM = DISABLE;
	CAN_InitStructure.CAN_ABOM = DISABLE;
	CAN_Init(CAN1, &CAN_InitStructure);
	
	CAN_FilterInitTypeDef CAN_FilterInitStructure;
	CAN_FilterInitStructure.CAN_FilterNumber = 0;
	CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;
	CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
	CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
	CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
	CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
	CAN_FilterInit(&CAN_FilterInitStructure);
}

void MyCAN_Transmit(CanTxMsg *TxMessage)
{
	uint8_t TransmitMailbox = CAN_Transmit(CAN1, TxMessage);
	
	uint32_t Timeout = 0;
	while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok)
	{
		Timeout ++;
		if (Timeout > 100000)
		{
			break;
		}
	}
}

void USB_LP_CAN1_RX0_IRQHandler(void)
{
	if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) == SET)
	{
		CAN_Receive(CAN1, CAN_FIFO0, &MyCAN_RxMsg);
		MyCAN_RxFlag = 1;
	}
}

        主函数也换成MyCAN_RxFlag == 1进行判断:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"

uint8_t KeyNum;

CanTxMsg TxMsgArray[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]          */
	{0x555, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},
	{0x000, 0x12345678, CAN_Id_Extended, CAN_RTR_Data,   4, {0xAA, 0xBB, 0xCC, 0xDD}},
	{0x666, 0x00000000, CAN_Id_Standard, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
	{0x000, 0x0789ABCD, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
};

uint8_t pTxMsgArray = 0;

int main(void)
{
	OLED_Init();
	Key_Init();
	MyCAN_Init();
	
	OLED_ShowString(1, 1, " Rx :");
	OLED_ShowString(2, 1, "RxID:");
	OLED_ShowString(3, 1, "Leng:");
	OLED_ShowString(4, 1, "Data:");
	
	while (1)
	{
		KeyNum = Key_GetNum();
		
		if (KeyNum == 1)
		{
			MyCAN_Transmit(&TxMsgArray[pTxMsgArray]);
			
			pTxMsgArray ++;
			if (pTxMsgArray >= sizeof(TxMsgArray) / sizeof(CanTxMsg))
			{
				pTxMsgArray = 0;
			}
		}
		
		if (MyCAN_RxFlag == 1)
		{
			MyCAN_RxFlag = 0;
			
			if (MyCAN_RxMsg.IDE == CAN_Id_Standard)
			{
				OLED_ShowString(1, 6, "Std");
				
				OLED_ShowHexNum(2, 6, MyCAN_RxMsg.StdId, 8);
			}
			else if (MyCAN_RxMsg.IDE == CAN_Id_Extended)
			{
				OLED_ShowString(1, 6, "Ext");
				
				OLED_ShowHexNum(2, 6, MyCAN_RxMsg.ExtId, 8);
			}
			
			if (MyCAN_RxMsg.RTR == CAN_RTR_Data)
			{
				OLED_ShowString(1, 10, "Data  ");
				
				OLED_ShowHexNum(3, 6, MyCAN_RxMsg.DLC, 1);
				
				OLED_ShowHexNum(4, 6, MyCAN_RxMsg.Data[0], 2);
				OLED_ShowHexNum(4, 9, MyCAN_RxMsg.Data[1], 2);
				OLED_ShowHexNum(4, 12, MyCAN_RxMsg.Data[2], 2);
				OLED_ShowHexNum(4, 15, MyCAN_RxMsg.Data[3], 2);
			}
			else if (MyCAN_RxMsg.RTR == CAN_RTR_Remote)
			{
				OLED_ShowString(1, 10, "Remote");
				
				OLED_ShowHexNum(3, 6, MyCAN_RxMsg.DLC, 1);
				
				OLED_ShowHexNum(4, 6, 0x00, 2);
				OLED_ShowHexNum(4, 9, 0x00, 2);
				OLED_ShowHexNum(4, 12, 0x00, 2);
				OLED_ShowHexNum(4, 15, 0x00, 2);
			}
		}
	}
}

完整代码:基于STM32的CAN总线中断式接收.zip资源-CSDN文库

CAN总线_时光の尘的博客-CSDN博客

标签:八万,总线,发送,源码,InitStructure,RTR,GPIO,ID
From: https://blog.csdn.net/MANONGDKY/article/details/143571413

相关文章

  • 前端Uin打包校园App小程序免费教程【源码】
    前端部分1、下载uniapp开发工具,导入圈子前端源码。根目录下有个siteinfo.js。所有的配置参数都在此管理,进去看说明修改为你的即可。打开manifest.json。获取一下,在uniapp里会为你自动增加一个应用。视频下载教程:https://gitee.com/DKcui/qz打包APP1、去uniapp官网申......
  • 计算机毕业设计源码 python基于爬虫的毕业生兴趣与求职实时智能数据分析
    标题:python基于爬虫的毕业生兴趣与求职实时智能数据分析设计一个基于Python和爬虫技术的毕业生兴趣与求职实时智能数据分析系统,可以帮助高校、企业和毕业生更好地了解就业市场趋势、毕业生兴趣和求职需求,从而优化招聘策略和个人职业规划。以下是一个典型的数据分析系统的主......
  • 计算机毕设设计项目源码 python基于Spark的淘宝服装数据分析系统的设计与实现
    标题:python基于Spark的淘宝服装数据分析系统的设计与实现设计一个基于Python和Spark的淘宝服装数据分析系统,可以帮助商家和平台更好地了解市场趋势、消费者偏好和销售情况,从而优化库存管理和营销策略。以下是一个典型的数据分析系统的主要功能模块:1.系统概述•目标:为淘宝......
  • 视频号下载项目源码分享含助手和小程序源码
     下载过视频号的人都知道,转发视频给机器人助手就可以得到视频下载链接,然后进入小程序内下载分享助手和源码给你们玩玩比如下方图片这个助手专门做视频下载视频提取,视频号下载这个需求量是很大了,有流量渠道的朋友很适合这个项目,特别是做教育教学视频剪辑的,这方面用户很......
  • Java项目实战II基于微信小程序的课堂助手(开发文档+数据库+源码)
    目录一、前言二、技术介绍三、系统实现四、文档参考五、核心代码六、源码获取全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。一、前言在信息化教学日益普及的今天,如何更高效地辅助课堂学习,成为了教育技术......
  • Java项目实战II基于微信小程序的书橱系统(开发文档+数据库+源码)
    目录一、前言二、技术介绍三、系统实现四、文档参考五、核心代码六、源码获取全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末一、前言随着数字化时代的到来,阅读方式虽日趋多元......
  • Java项目实战II基于微信小程序的高校寻物平台(开发文档+数据库+源码)
    目录一、前言二、技术介绍三、系统实现四、文档参考五、核心代码六、源码获取全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末一、前言在快节奏的校园生活中,物品遗失与寻找成为......
  • Java项目实战II基于微信的设备故障报修管理系统(开发文档+数据库+源码)
    目录一、前言二、技术介绍三、系统实现四、文档参考五、核心代码六、源码获取全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末一、前言在现代企业管理中,设备故障报修的及时性和......
  • cornerstone中delayed_task,timer_task及scheduler源码解析
    1.概述delayed_task在cornerstone中充当一个base类,其延伸出的子类timer_task才是cornerstone用于实现定时器的类。而scheduler是用于实现延时效果的调度器。我们将按照delayed_task->timer_task->scheduler的顺序解析源码。2.delayed_task源码解析2.1成员变量分析classdel......
  • 基于springboot+vue的Java的航空飞机票务预约购票出行服务系统设计与实现(源码+文档+
    课题简介基于SpringBoot+Vue的航空飞机票务预约购票出行服务系统,为旅客提供一站式航空出行解决方案。后端采用SpringBoot构建核心服务,SpringDataJPA与MySQL数据库交互,存储航班信息、用户数据、订单详情等,SpringSecurity确保系统安全,进行严格的用户认证与......