一、CAN 总线简介
CAN 总线(Controller Area Network,控制器局域网)是一种广泛应用于汽车、工业控制等领域的串行通信协议,由德国博世(Bosch)公司在 1980 年代开发,主要用于设备间的实时数据交换,当一个设备发送信息给 CAN 总线上,当这个信息通过了一些接收设备的过滤器,则接收设备就会收到发送设备的信息,如果多个接收设备都符合接收的条件,则有多个接收设备;下面是 CAN 总线的特征:
- 两根通信线(CAN_H、CAN_L),线路少
- 差分信号通信,抗干扰能力强
- 高速 CAN(ISO11898):125k ~ 1Mbps, < 40m;低速 CAN(ISO11519):10k ~ 125kbps, < 1km
- 异步通信,无需时钟线,通信速率由设备各自约定
- 半双工,可挂载多设备,多设备同时发送数据时通过仲裁判断先后顺序
- 11位/29位报文ID,用于区分消息功能,同时决定优先级
- 可配置1~8字节的有效载荷
- 可实现广播式和请求式两种传输方式
- 应答、CRC校验、位填充、位同步、错误处理等特性
下面表格是主流通信协议的对比:
| 名称 | 引脚 | 双工 | 时钟 | 电平 | 设备 | 应用场景 |
|---|---|---|---|---|---|---|
| UART | TX、RX | 全双工 | 异步 | 单端 | 点对点 | 两个设备互相通信 |
| I2C | SCL、SDA | 半双工 | 同步 | 单端 | 多设备 | 一个主控外挂多个模块 |
| SPI | SCK、MOSI、MISO、SS | 全双工 | 同步 | 单端 | 多设备 | 一个主控外挂多个模块(费导线,但传输速度快) |
| CAN | CAN_H、CAN_L | 半双工 | 异步 | 差分 | 多设备 | 多个主控互相通信 |
1.1.差分信号
差分信号是一种用两根信号线传输一个数据的技术。这两根线(如 CAN_H 和 CAN_L)始终传输大小相等、极性相反的电压信号,接收端通过计算两者的差值来判断逻辑状态。关键特点:
- 抗干扰能力强
- 抑制共模噪声
- 更低的电压摆幅,更高的速度
高速 CAN:
逻辑1(隐性):CAN_H = 2.5V,CAN_L = 2.5V → 差值 = 0V
逻辑0(显性):CAN_H = 3.5V,CAN_L = 1.5V → 差值 = +2V

低速 CAN:
逻辑1(隐性):CAN_H = 3.25V,CAN_L = 1.75V → 差值 = -1.5V
逻辑0(显性):CAN_H = 4V, CAN_L = 1V → 差值 = 3V

为什么逻辑 0 是显性电平,逻辑 1 是显性电平?因为该协议具有“线与”的特性,0 和 0 相与为 0;0 和 1 相与也为 0。这样的好处也可以区分出优先级,当多个设备同时发送消息,这些消息就会在仲裁段数据里开始比对,谁的数据不一致先出现了 1,那输掉仲裁,优先处理胜出那一个。
1.2.CAN 硬件电路
每个设备通过 CAN 收发器挂载在 CAN 总线网络上,CAN 控制器引出的 TX 和 RX 与 CAN 收发器相连,CAN 收发器引出的 CAN_H 和 CAN_L 分别与总线的 CAN_H 和 CAN_L 相连,CAN 硬件电路的连接方式不同,添加的电阻阻值不同,也分为两种类型:
- 高速 CAN:CAN_H 和 CAN_L 两端添加 120Ω 的终端电阻
- 低俗 CAN:CAN_H 和 CAN_L 其中一端添加 2.2kΩ 的终端电阻
如下图所示:

CAN 总线上的两个电阻类似 I2C 上的上拉电阻,如果 CAN 总线上没有设备发送显性电平“拉开”总线,那就保持默认的隐性电平。
下图是 CAN 总线物理层的特性:

二、CAN 总线帧格式
下面内容均以高速 CAN 为例子。
| 帧类型 | 用途 |
|---|---|
| 数据帧 | 发送设备主动发送数据(广播式) |
| 遥控帧 | 接收设备主动请求发送数据(请求式) |
| 错误帧 | 某个设备检测出错误时向其他设备通知错误 |
| 过载帧 | 接收设备通知发送设备,接收设备还没做好接收准备 |
| 帧间隔 | 用于将数据帧及遥控帧与前面的帧分开 |
2.1.数据帧
数据帧由 7 个不同的位场组成:
- 帧起始:当 CAN 总线空闲时,也就是处于隐性电平时,发送设备首先“拉开”电平,和各个设备说“我要发数据了”,起到通知作用,这个段只有 1 个位数据,也就是逻辑 0
- 仲裁段:用于解决多个节点同时发送数据时的冲突问题,确保优先级高的报文优先传输,而无需中央控制器协调。其核心作用是实现非破坏性逐位仲裁;该段分为标准格式和扩展格式,在 IDE 位上写入逻辑 1,即为扩展格式,之后的 r1 就是原来的 IDE
- 控制段:控制段控制前面的仲裁段是否为扩展格式,也控制后面数据段的长度
- 数据段:填写数据,数据段的长度可以为 0
- CRC 段:循环冗余校验,校验数据是否正确
- 应答段(ACK):发送设备将信息发送之后将总线控制权交给接收设备,需要接收设备回应逻辑 0
- 帧结束:以 7 个逻辑 1 为结束帧

- SOF(Start of Frame):帧起始,表示后面一段波形为传输的数据位
- ID(Identify):标识符,区分功能,同时决定优先级
- RTR(Remote Transmission Request ):远程请求位,区分数据帧和遥控帧
- IDE(Identifier Extension):扩展标志位,区分标准格式和扩展格式
- SRR(Substitute Remote Request):替代RTR,协议升级时留下的无意义位
- r0/r1(Reserve):保留位,为后续协议升级留下空间
- DLC(Data Length Code):数据长度,指示数据段有几个字节
- Data:数据段的1~8个字节有效数据
- CRC(Cyclic Redundancy Check):循环冗余校验,校验数据是否正确
- ACK(Acknowledgement):应答位,判断数据有没有被接收方接收
- CRC/ACK界定符:为应答位前后发送方和接收方释放总线留下时间
- EOF(End of Frame ):帧结束,表示数据位已经传输完毕
2.2.遥控帧
遥控帧与数据帧相比,只是将仲裁段的 RTR 位写入逻辑 1,没有了数据段,遥控帧用于请求其他设备发送消息,如下图所示:

2.3.错误帧
总线上所有设备都会监督总线的数据,一旦发现“位错误”或“填充错误”或“CRC 错误”或“格式错误”或“应答错误” ,这些设备便会发出错误帧来破坏数据,同时终止当前的发送设备,下图是错误帧:

错误帧的错误种类共有图下几种:

错误状态有三种:
- 主动错误状态:(初始状态)主动错误状态的设备正常参与通信并在检测到错误时发出主动错误帧,6 个或以上的逻辑 0,处于主动错误状态的设备在 CAN 总线上具有强大的破坏数据的能力,可以破坏别的设备数据(根据“线与”特性),如果某个设备接收或发送错误的计数值超过 127,那就将该设备转变为被动错误状态
- 被动错误:被动错误状态的设备正常参与通信但检测到错误时只能发出被动错误帧,6 个或以上的逻辑 1,处于被动错误状态中的设备只能发出逻辑 1 来破坏自己的数据,不能影响其他设备所发出的信息,如果处于被动错误状态中的设备,减少了发送或接收错误的计数值并 <= 127,该设备就有能够回到主动错误状态;如果继续错误,发送错误的计数值 > 255,就将该设备转变为关闭总线状态
- 关闭总线状态:总线关闭状态的设备不能参与通信,该设备只能“闭嘴”,等待在总线上检测到 128 次连续个位的逻辑 1 才能转变为主动错误状态(初始状态),也就是总线处于空闲
下图是错误状态:

下面表格是错误计数器的计数规则,由下表可知,容错率是比较低的:

2.4.过载帧
当接收方收到大量数据而无法处理时,其可以发出过载帧,延缓发送方的数据发送,以平衡总线负载,避免数据丢失。

2.5.间隔帧
将数据帧和遥控帧与前面的帧分离开,如下图所示:

三、位填充和位同步
3.1.位填充
位填充规则:发送方每发送 5 个相同电平后,自动追加一个相反电平的填充位,接收方检测到填充位时,会自动移除填充位,恢复原始数据。

位填充作用:
- 增加波形的定时信息,利于接收方执行“再同步”,防止波形长时间无变化,导致接收方不能精确掌握数据采样时机
- 将正常数据流与“错误帧”和“过载帧”区分开,标志“错误帧”和“过载帧”的特异性
- 保持CAN总线在发送正常数据流时的活跃状态,防止被误认为总线空闲
3.2.位同步
3.2.1.接收方采样的理想状态和实际可能遇到的状态
CAN总线没有时钟线,总线上的所有设备通过约定波特率的方式确定每一个数据位的时长,发送方以约定的位时长每隔固定时间输出一个数据位,接收方以约定的位时长每隔固定时间采样总线的电平,输入一个数据位,理想状态下,接收方能依次采样到发送方发出的每个数据位,且采样点位于数据位中心附近,如下图所示:

上图是理想的采样,如果没有约定某个波特率,可能会出现以下情况:
- 接收方以约定的位时长进行采样,但是采样点没有对齐数据位中心附近

- 接收方刚开始采样正确,但是时钟有误差,随着误差积累,采样点逐渐偏离

3.2.2.位时序
为了灵活调整每个采样点的位置,使采样点对齐数据位中心附近,CAN 总线对每一个数据位(每一个逻辑 0 和 1)的时长进行了更细的划分,分为同步段(SS)、传播时间段(PTS)、相位缓冲段1(PBS1)和相位缓冲段2(PBS2),每个段又由若干个最小时间单位(Tq)构成,如下图所示:

- SS = 1Tq
- PTS = 1~8Tq
- PBS1 = 1~8Tq
- PBS2 = 2~8Tq
将 SS 移到边沿变化的位置,即可采样,采样点通常位于 PBS1 和 PBS2 的交界处,如果采样点比较靠前,可以通过增加 PBS1 的 Tp ,使得采样点靠后走;如果采样点比较靠后,可以通过增加 PBS2 的 Tp ,使得采样点靠前走。
3.2.3.硬同步
发生在打破宁静的帧起始(SOF)的下降沿,所有节点强制调整自己的位时序,使 SS 段对齐起始帧的下降沿。如下图所示:

- 每个设备都有一个位时序计时周期,当某个设备(发送方)率先发送报文,其他所有设备(接收方)收到 SOF 的下降沿时,接收方会将自己的位时序计时周期拨到 SS 段的位置,与发送方的位时序计时周期保持同步
- 硬同步只在帧的第一个下降沿(SOF下降沿)有效
- 经过硬同步后,若发送方和接收方的时钟没有误差,则后续所有数据位的采样点必然都会对齐数据位中心附近
3.2.3.再同步
上面小点说到了:经过硬同步后,若发送方和接收方的时钟没有误差,则后续所有数据位的采样点必然都会对齐数据位中心附近。如果有误差,随着误差积累,数据位边沿逐渐偏离 SS 段,则此时接收方根据再同步补偿宽度值(SJW=1~4Tq)通过加长 PBS1 段,或缩短 PBS2 段,以调整同步;再同步可以发生在第一个下降沿之后的每个数据位跳变边沿。

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

四、资源分配规则
CAN 总线支持多信息(报文)交替传输,但同一时间只能有一个节点发送数据,CAN总线像一条单车道高速公路,所有车辆(报文)按交通规则(仲裁)有序通过。如果有一个设备正在发送数据,中间不允许插入另一个设备发送数据打断该数据,必须等该数据发送完毕,下一个设备才能发送信息;又如果有多个设备想同时发送信息,那就要比对仲裁段里的 ID 了,一开始的数据都一样,谁先出现隐性电平,谁就仲裁失利,等待下一次发送。
4.1.先占先得
先占先得的规矩如下:
- 若当前已经有设备正在操作总线发送数据帧/遥控帧,则其他任何设备不能再同时发送数据帧/遥控帧(可以发送错误帧/过载帧破坏当前数据)
- 任何设备检测到连续11个隐性电平,即认为总线空闲,只有在总线空闲时,设备才能发送数据帧/遥控帧
- 一旦有设备正在发送数据帧/遥控帧,总线就会变为活跃状态,必然不会出现连续11个隐性电平,其他设备自然也不会破坏当前发送
- 若总线活跃状态其他设备有发送需求,则需要等待总线变为空闲,才能执行发送需求
4.2.非破坏性仲裁
若多个设备的发送需求同时到来或因等待而同时到来,则 CAN 总线协议会根据 ID 号(仲裁段)进行非破坏性仲裁,**ID 号小的(优先级高)**取到总线控制权,**ID 号大的(优先级低)**仲裁失利后将转入接收状态,等待下一次总线空闲时再尝试发送。
实现非破坏性仲裁需要两个要求:
- 线与特性:总线上任何一个设备发送显性电平0时,总线就会呈现显性电平0状态;只有当所有设备都发送隐性电平1时,总线才呈现隐性电平1状态,即:0 & X & X = 0,1 & 1 & 1 = 1
- 回读机制:每个设备发出一个数据位后,都会读回总线当前的电平状态,以确认自己发出的电平是否被真实地发送出去了,根据线与特性,发出0读回必然是0,发出1读回不一定是1

当数据帧和遥控帧的 ID 一样时,数据帧的优先级高于遥控帧,因为 RTR 位需要逻辑 1 才能表示遥控帧:

标准格式 11 位 ID 号和扩展格式 29 位 ID 号的高 11 位一样时,标准格式的优先级高于扩展格式(SRR 必须始终为 1,以保证此要求),原因和上面同理。

五、STM32F103C8T6 上的 CAN 外设
5.1.简介
STM32 内置 bxCAN 外设(CAN控制器),支持 CAN2.0A 和 2.0B,可以自动发送 CAN 报文和按照过滤器自动接收指定 CAN 报文,程序只需处理报文数据而无需关注总线的电平细节,该芯片上的 CAN 外设具有下面特点:
- 波特率最高可达1兆位/秒
- 3个可配置优先级的发送邮箱
- 2个3级深度的接收FIFO
- 14个过滤器组(互联型28个)
- 时间触发通信、自动离线恢复、自动唤醒、禁止自动重传、接收FIFO溢出处理方式可配置、发送优先级可配置、双CAN模式
5.2.CAN 网拓扑结构
下图是各个结点与 CAN 总线相连:

下面 PA11 和 PA12 是默认复用的:

下面 PB8 和 PB9 是重定义:

5.3.CAN收发器电路


5.4.CAN 的基本结构

如上图所示,CPU 写入数据是由发送邮箱装载的,这三个发送邮箱是由用户来决定谁先被发送,可以是先进先出,或是根据优先级来排队;CPU 读取数据:引脚读取数据,并由接受控制器传入到接收过滤器,经过过滤器,确定是设备想要的信息,就把该数据存放在接收 FIFO0 或 FIFO1 里面,如果短时间读取到很多数据,两个接收 FIFO 就起到分流的作用了,也能够起到保存重要信息的作用,重要的信息放在 FIFO0 或 FIFO1,不重要的就放在另一个队伍里,如果实在不够放,用户可以开启 FIFO 的锁定状态,就会使新的数据直接丢弃,如果没开启,则新的数据占据邮箱 2 的位置,成为新的数据。
5.4.1.发送过程
选择一个空置邮箱→写入报文 →请求发送:

看图和了解每个缩写的意思即可:
- RQCP(Request completed):请求完成
- TXOK(Transmission OK):发送成功
- TME(Transmit mailbox empty):发送邮箱空
- TXRQ(Transmit mailbox request):发送请求控制位
- NART(No automatic retransmission):禁止自动重传
- ABRQ(Abort request):中止发送
5.4.2.接收过程
接收到一个报文→匹配过滤器后进入 FIFO0 或 FIFO1 → CPU 读取:(下面图纠正,表示 2 应该使用二进制,表示 3 也一样)

- FMP(FIFO message pending):报文数目
- FOVR(FIFO overrun):FIFO 溢出
- RFOM(Release FIFO output mailbox):释放邮箱
注意,当邮箱处于溢出状态时,释放邮箱之后会变成挂号 2 状态,因为溢出状态与挂号 3 状态是一样的。
5.5.过滤器
CAN 总线过滤器(Filter)是 CAN 控制器中的一个硬件功能,用于筛选接收到的报文,只让符合特定条件的报文进入接收缓冲区,从而减少 CPU 处理无关数据的负担。这些过滤器分为两大类:
- 标识符屏蔽
- 列表模式
下面是一个过滤器的寄存器介绍,本次实验的开发板共有 14 个过滤器,也就是有 14 个下面的寄存器,每个过滤器的核心由两个 32 位寄存器组成:R1[31:0] 和 R2[31:0]:
- FSC(Filter scale configuration):位宽设置,置 0:16 位;置 1:32 位
- FBM(Filter mode):模式设置:置 0:屏蔽模式;置 1:列表模式
- FFA(Filter FIFO assignment):关联设置:置 0:关联到 FIFO0;置 1:关联到 FIFO1
- FACT(Filter active) :激活设置:置 0:禁用;置 1:启用
5.5.1.标识符屏蔽模式
标识符屏蔽模式将 FBM 寄存器置 0,该模式常用于接收报文多于过滤器的数量,通过锁定特定位,来区分能否通过该过滤器,可以部分匹配 ID 的机制。通过配置 FSC 寄存器又可以分为 32 位和 16 位的过滤器
- 32 位的过滤器:通常用于过滤扩展格式的 ID,R1 用来填写目标 ID,需要过滤的位一般置 0;R2 用来匹配用户需要的位,写入的码叫掩码,置 1,报文匹配 R1 寄存器的位才能通过,置 0,则不匹配的不能通过

- 16 位的过滤器:通常用于过滤标准格式的 ID,将 R1 和 R2 分为两个过滤器;R1 的低 16 位用于填写目标 ID,需要过滤的位一般置 0,高 16 位用于匹配需要的位,写入的码叫掩码,置 1,报文匹配 R1 寄存器的位才能通过,置 0,则不匹配的不能通过,16 位的过滤器这样设计,提高了资源利用率,减少了资源浪费

例子:下图是扩展格式的 ID,需要的 ID 是 0x12345600 - 0x123456FF,这些 ID 有规律,保留前面 0x123456xx,这种情况使用 32 位的屏蔽模式最合适,R1 寄存器写入目标 ID:0x12345600,后面两个 0 是随便填写的,左移 3 位跨过 IDE 位、RTR 位和最后的置 0 位,或上 0x4,是说明 IDE 置 1,报文是扩展格式;R2 寄存器写入掩码,前面 29 位都置 1,说明有匹配前面要求才能通过,左移 3 位与上面说明一致,或上 0x4,是说明只有扩展格式才能通过过滤器,再或上 0x2 是用于区分数据帧还是遥控帧,这里是锁定了只有数据帧才能通过过滤器。

例子:下图是标准格式的 ID,需要的 ID 是 0x200 - 0x2FF 和 0x320 - 0x32F,符合 0x2 开头的和 0x3 开头的 ID 才能通过该过滤器,过程和 32 位的一致,只是一个寄存器的低 16 位用于填写目标 ID,高 16 位用于填写掩码:

5.5.1.列表模式
列表模式用于精确匹配特定 ID 的场景,R1 和 R2 都可以填写用户想要的目标 ID,其中 16 位的过滤器可以填写 4 个标志格式的目标 ID。


例子:下面 ID 中,只想要 0x123 和 0x12345678,就需要用到 32 位过滤器的 R1 和 R2,用 32 位寄存器写入标准格式的 ID,只需要写完前 11 位的 ID 之后,后面补 0 即可。

5.6.测试模式
当一个设备想要测试自己发送或接收 CAN 总线上的数据是否正常,又不影响别的设备在 CAN 总线上的运行,这里有三个模式用来测试:
- 静默模式:用于分析 CAN 总线的活动,不会对总线造成影响

- 环回模式:用于自测试,同时发送的报文可以在 CAN_TX 引脚上检测到

- 环回静默模式:用于热自测试,自测的同时不会影响 CAN 总线

5.7.工作模式
- 初始化模式:用于配置CAN外设,禁止报文的接收和发送
- 正常模式:配置CAN外设后进入正常模式,以便正常接收和发送报文
- 睡眠模式:低功耗,CAN外设时钟停止,可使用软件唤醒或者硬件自动唤醒
- AWUM:置1,自动唤醒,一旦检测到CAN总线活动,硬件就自动清零SLEEP,唤醒CAN外设;置0,手动唤醒,软件清零SLEEP,唤醒CAN外设
一个 CAN 外设一开始就要进入初始化模式进行配置,配置结束之后才能进入正常模式,如果在正常模式初始化,还没配置完成就在错误的参数下运行。

- AWUM(Automatic wakeup mode):自动唤醒模式
- SLAK(Sleep ack):睡眠应答
- INAK(Inti ack):初始化应答
- INRQ(Init request):请求初始化
六、实验
6.1.单个设备环回测试
本实验目的是测试该设备在 CAN 总线的发送和接收是否正常,按下按键显示本设备的 ID 信息,再按下按键数据就自增,配置 CAN 的时候,将模式配置成CAN_Mode_LoopBack;,下面是 MyCAN.c 代码:
#include "stm32f10x.h" // Device header
//配置GPIO口的PA11和PA12;配置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; //波特率 = 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(uint32_t ID, uint8_t Length, uint8_t *Data)
{
CanTxMsg TxMessage;
TxMessage.StdId = ID;
TxMessage.ExtId = ID;
TxMessage.IDE = CAN_Id_Standard; //CAN_ID_STD
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);
uint32_t Timeout = 0;
while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok)
{
Timeout ++;
if (Timeout > 100000)
{
break;
}
}
}
//接收标志
uint8_t MyCAN_ReceiveFlag(void)
{
if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0)
{
return 1;
}
return 0;
}
//接收数据
void MyCAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *Data)
{
CanRxMsg RxMessage;
CAN_Receive(CAN1, CAN_FIFO0, &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
{
//...
}
}
下面是头文件:
#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
下面是 main.c 代码:
#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);
}
}
}
6.2.三个设备互相通信
基于上面的 MyCAN.c,在 CAN 配置里,CAN 工作模式换成CAN_Mode_Normal;,其他不需要更改,然后用到三个 STM32 开发板,插上一个更改 ID 再按下按键保存 ID,另外两个也这样操作,最后按下其中一个开发板的按键,另外两个就会收到它的数据。
1万+

被折叠的 条评论
为什么被折叠?



