49. CAN通信协议
一、CAN总线
1.1、CAN总线简介
CAN 是 Controller Area Network 控制器局域网 的缩写,是 ISO 国际标准化的 串行通信协议。CAN 总线由两根线(CANL 和 CANH)组成,允许挂载多个设备节点。CAN 协议经过 ISO 标准化后有两个标准:ISO11898 标准(高速 CAN)和 ISO11519-2 标准(低速 CAN)。其中 ISO11898 是针对通信速率为 125Kbps ~ 1Mbps 的高速通信标准,而 ISO11519-2 是针对通信速率为 125Kbps 以下的低速通信标准。
CAN 是一种功能丰富的车用总线标准,被设计用于在不需要主机(Host)的情况下,允许网络上的单片机和仪器相互通信。它基于 消息队列协议,设计之初在车辆上复用通信线缆,以降低铜线使用量,后来也被其它行业所使用。CAN 拥有良好的弹性调整能力,可以在现有网络中增加节点而不用在软、硬件上做出调整。处理之外,消息的传递不基于特殊种类的节点,增加了升级网络的便利性。
CAN 协议具有以下特点:
- 多主控制。在总线空闲时,所有单元都可以发送消息(多主控制),而两个以上的单元同时开始发送消息时,根据标识符(Identifier 以下称为 ID)决定优先级。ID 并不是表示发送的目的地址,而是表示访问总线的消息的优先级。两个以上的单元同时开始发送消息时,对各消息 ID 的每个位进行逐个仲裁比较。仲裁获胜(被判定为优先级最高)的单元可继续发送消息,仲裁失利的单元则立刻停止发送而进行接收工作。
- 系统的柔软性。与总线相连的单元没有类似于 “地址” 的信息。因此在总线上增加单元时,连接在总线上的其它单元的软硬件及应用层都不需要改变。
- 通信速度较快,通信距离远。最高 1Mbps(距离小于 40M),最远可达 10KM(速率低于 5Kbps)。
- 具有错误检测、错误通知和错误恢复功能。所有单元都可以检测错误(错误检测功能),检测出错误的单元会立即同时通知其他所有单元(错误通知功能),正在发送消息的单元一旦检测出错误,会强制结束当前的发送。强制结束发送的单元会不断反复地重新发送此消息直到成功发送为止(错误恢复功能)。
- 故障封闭功能。CAN 可以判断出错误的类型是总线上暂时的数据错误(如外部噪声等)还是持续的数据错误(如单元内部故障、驱动器故障、断线等)。由此功能,当总线上发生持续数据错误时,可将引起此故障的单元从总线上隔离出去。
- 连接节点多。CAN 总线是可同时连接多个单元的总线。可连接的单元总数理论上是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。
1.2、CAN拓扑结构图
从上图可知,高速 CAN 总线呈现的是一个闭环结构,总线是由两根线 CAN_High 和 CAN_Low 组成,且在总线两端各串联了 120Ω 的电阻(用于阻抗匹配,减少回波反射),同时总线上可以挂载多个节点。每个节点都有 CAN 收发器以及 CAN 控制器,CAN 控制器通常是 MCU 的外设,集成在芯片内部;CAN 收发器则是需要外加芯片转换电路。
CAN 使用 差分信号 传输数据。根据 CAN 总线上两根线的电位差来判断总线电平。总线电平分为显性电平和隐性电平,二者必居其一。
差分信号 是一种信号传输的技术,差分传输在这两根线上都传输信号,这两个信号的 振幅相同,相位相反。信号接收端比较这两个 电压的差值 来判断发送发送的逻辑状态。在电路板上,差分走线必须是 等长、等宽、紧密相连、且在 同一层面 的两根线。
差分信号的抗干扰能力强,干扰噪声一般会等值、同时的被加载到两根信号线上,而其插值为 0,即噪声信号会信号的逻辑意义不会产生影响。并且差分信号能有效移植电磁干扰。由于两根线靠的很近且信号幅值相等,这两根线与地线之间的耦合电磁场的幅值也相等,同时它们的信号极性相反,其电磁场将会相互抵消。因此对外界的电磁干扰也小。
从该特性可以看出,显性电平对应逻辑 0,CAN_H 和 CAN_L 之差为 2 V 左右。而隐性电平对应逻辑 1,CAN_H 和 CAN_L 之差为 0V。在总线上显性电平具有优先权,只要有一个单元输出显性电平,总线上即为显性电平。而隐形电平则具有包容的意味,只有所有的单元都输出隐性电平,总线上才为隐性电平(显性电平比隐性电平更强)。
1.3、CAN协议层
CAN 协议是通过以下 5 种类型的帧进行的:数据帧、遥控帧、错误帧、过载帧 和 间隔帧。另外,数据帧和遥控帧有标准格式和扩展格式两种格式。标准格式有 11 个位的标识符(ID),扩展格式有 29 个位的 ID。
数据帧 一般由 7 个段构成,即:帧起始、仲裁段、控制段、数据段、CRC 段、ACK 段 和 帧结束。
① 帧起始
表示数据帧开始的段。标准帧和扩展帧都是由 1 个位的显性电平表示帧起始。
② 仲裁段
表示该帧优先级的段。标准帧和扩展帧格式在本段有所区别。
标准格式的 ID 有 11 个位。禁止高 7 位都为隐性(禁止设定:ID=1111111XXXX)。扩展格式的 ID 有 29 个位。基本 ID 从 ID28 到 ID18,扩展 ID 由 ID17 到 ID0 表示。基本 ID 和标准格式的 ID 相同。禁止高 7 位都为隐性(禁止设定:基本 ID=1111111XXXX)。
其中 RTR 位用于标识是否是远程帧(0,数据帧;1,远程帧),IDE 位为标识符选择位(0,使用标准标识符;1,使用扩展标识符),SRR 位为代替远程请求位,为隐性位,它代替了标准帧中的 RTR 位。
③ 控制段
由 6 个位构成,表示数据的字节数及保留位的段。标准帧和扩展帧的控制段稍有不同。
上图中,r0 和 r1 为保留位,必须全部以显性电平发送,但是接收端可以接收显性、隐性及任意组合的电平。DLC 段为数据长度表示段,高位在前,DLC 段有效值为 0 ~ 8,但是接收方接收到 9 ~ 15 的时候并不认为是错误。
④ 数据段
该段可包含 0~8 个字节的数据。从最高位(MSB)开始输出,标准帧和扩展帧在这个段的定义都是一样的。
⑤ CRC 段
检查帧的传输错误的段。由 15 个位的 CRC 顺序和 1 个位的 CRC 界定符(用于分隔的位)组成,标准帧和扩展帧在这个段的格式也是相同的。
此段 CRC 的值计算范围包括:帧起始、仲裁段、控制段、数据段。接收方以同样的算法计算 CRC 值并进行比较,不一致时会通报错误。
⑥ ACK 段
此段用来确认是否正常接收。由 ACK 槽(ACKSlot)和 ACK 界定符 2 个位组成。标准帧和扩展帧在这个段的格式也是相同的。
发送单元的 ACK,发送 2 个位的隐性位,而接收到正确消息的单元在 ACK 槽(ACKSlot)发送显性位,通知发送单元正常接收结束,这个过程叫发送 ACK/返回 ACK。发送 ACK 的是在既不处于总线关闭态也不处于休眠态的所有接收单元中,接收到正常消息的单元(发送单元不发送 ACK)。所谓正常消息是指不含填充错误、格式错误、CRC 错误的消息。
⑦ 帧结束
表示数据帧结束的段,标准帧和扩展帧在这个段格式一样,由 7 个隐性信号构成。
1.4、CAN位时序
CAN总线以 “位同步” 机制,实现对电平的正确采样。位数据都由四段组成:同步段(SS)、传播时间段(PTS)、相位缓冲段 1(PBS1)和 相位缓冲段 2(PBS2)。这些段又由可称为 Time Quantum(以下称为 Tq)的最小时间单位构成。1 位分为 4 个段,每个段又由若干个 Tq 构成,这称为 位时序。
1 位由多少个 Tq 构成、每个段又由多少个 Tq 构成等,可以任意设定位时序。通过设定位时序,多个单元可同时采样,也可任意设定采样点。采样点是指读取总线电平,并将读到的电平作为位值的点。根据位时序,就可以计算CAN通信的波特率。
同步段(SS: Sycchronization Segment)的大小固定为 1Tq。若通信节点检测到总线上信号的跳变沿被包含在 SS 段的范围之内,则表示节点与总线的时序是同步的。
传播时间段(PTS: Propagation Time Seqment),CAN 总线上数据的传输会受到各种物理延迟,比如发送单元的发送延迟、总线上信号的传播延迟、接收单元的输入延迟等,PTS 段就是补偿这些因素产生的时间延迟。PTS 段长度至少为 1 个 Tq,一般为 1 ~ 8Tq。
相位缓冲段 1(PBS1: Phase Buffer Segment 1)主要补偿 边沿阶段 的误差。它的时间长度在 重新同步阶段 的时候可以被 自动加长。PBS1 段的初始长度可以为 1 ~ 8Tq。
相位缓冲段 2(PBS2: Phase Buffer Segment 2)另一个相位缓冲段,也是用来补偿 边沿阶段 误差的。它的时间长度在重新同步阶段时可以 缩短。PBS2 段的初始大小可以为 2 ~ 8Tq。
采样点位于 PBS1 和 PBS2 的交界处。
上图的采样点,是指读取总线电平,并将读到的电平作为位值的点。位置在 PBS1 结束处。根据这个位时序,我们就可以计算 CAN 通信的波特率了。
节点监测到总线上信号的跳变在SS段范围内,表示节点与总线的时序是同步,此时采样点的电平即该位的电平。
1.5、数据同步方式
CAN 为了实现对总线电平信号的正确采样,数据同步分为 硬件同步 和 再同步。
① 硬件同步
节点通过 CAN 总线发送数据,一开始发送帧起始信号。总线上其它节点会检测帧起始信号在不在位数据的 SS 段内,判断内部时序与总线是否同步。假如不在 SS 段内,这种情况下,采样点获得的电平状态是不正确的。所以,节点会使用硬件同步方式调整, 把自己的 SS 段平移到检测到边沿的地方,获得同步,同步情况下,采样点获得的电平状态才是正确的。
②、再同步
再同步利用普通数据位的边沿信号(帧起始信号是特殊的边沿信号)进行同步。再同步的方式分为两种情况:超前 和 滞后,即边沿信号与 SS 段的相对位置。再同步时,PSB1 和 PSB2 中增加或者减少的时间被称为 “再同步补偿宽度(SJW)”,其范围:1 ~ 4 Tq。限定了 SJW 值后,再同步时,不能增加限定长度的 SJW 值。SJW 值较大时,吸收误差能力更强,但是通讯速度会下降。
所有的同步都是由 CAN 控制器硬件自动完成的。
1.6、CAN总线仲裁
CAN 总线处于空闲状态,最先开始发送消息的单元获得发送权。而多个单元同时开始发送时,从仲裁段(报文 ID)的第一位开始进行仲裁。连续输出显性电平最多的单元可继续发送,即首先出现隐性电平的单元失去对总线的占有权变为接收(报文 ID 小的优先级高)。竞争失败单元,会自动检测总线空闲,在第一时间再次尝试发送。
二、CAN控制器
2.1、CAN控制器简介
STM32F407 自带的是 bxCAN,即基本扩展 CAN。它支持 CAN 协议 2.0A 和 2.0B。CAN2.0A只能处理标准数据帧,扩展帧的内容会识别错误;CAN2.0B Active 可以处理标准数据帧和扩展数据帧;而 CAN2.0B passive 只能处理标准数据帧,扩展帧的内容会忽略。它的设计目标是,以最小的 CPU 负荷来高效处理大量收到的报文。它也支持报文发送的优先级要求(优先级特性可软件配置)。对于安全紧要的应用,bxCAN 提供所有支持时间触发通信模式所需的硬件功能。
STM32F407 的 bxCAN 的主要特点有:
- 支持 CAN 协议 2.0A 和 2.0B 主动模式。
- 波特率最高达 1Mbps。
- 支持时间触发通信(CAN 的硬件内部定时器可以在 TX/RX 的帧起始位的采样点位置生成时间戳)。
- 具有 3 个发送邮箱。
- 具有 3 级深度的 2 个接收 FIFO。
- 可变的过滤器组(最多 28 个)。
CAN 控制器的工作模式有三种:初始化模式、正常模式 和 睡眠模式。
CAN 控制器的测试模式有三种:静默模式、环回模式 和 环回静默模式。这三种模式需要在 CAN 控制器的 初始化模式 下进行配置。
2.2、CAN控制器的框图
STM32 的 CAN 控制器框图如下:
从图中可以看出两个 CAN 都分别拥有自己的发送邮箱和接收 FIFO,但是他们共用 28 个滤波器。通过 CAN_FMR 寄存器的设置,可以设置滤波器的分配方式。STM32F407的过滤器组最多有 28 个(互联型),每个滤波器组 x 由 2 个 32 为寄存器,CAN_FxR1 和 CAN_FxR2 组成。
STM32 的 CAN 控制器的发送处理过程如下:程序选择 1 个空置的邮箱(TME=1) 设置标识符(ID),数据长度和发送数据,设置 CAN_TIxR 的 TXRQ 位为 1,请求发送 邮箱挂号(等待成为最高优先级) 预定发送(等待总线空闲) 发送 邮箱空置。
STM32 的 CAN 控制器的接收处理过程如下:FIFO 空 → 收到有效报文 → 挂号_1(存入 FIFO 的一个邮箱,这个由硬件控制,我们不需要理会) → 收到有效报文 → 挂号_2 → 收到有效报文 → 挂号_3 → 收到有效报文溢出。
CAN 接收到的有效报文,被存储在 3 级邮箱深度的 FIFO 中。FIFO 完全由硬件来管理,从而节省了 CPU 的处理负荷,简化了软件并保证了数据的一致性。应用程序只能通过读取 FIFO输出邮箱,来读取 FIFO 中最先收到的报文。这里的有效报文是指那些正确被接收的(直到 EOF 都没有错误)且通过了标识符过滤的报文。
这个流程里面,我们必须在 FIFO 溢出之前,读出至少 1 个报文,否则下个报文到来,将导致 FIFO 溢出,从而出现报文丢失。每读出 1 个报文,相应的挂号就减 1,直到 FIFO 空。
当总线上报文数据量很大时,总线上的设备会频繁获取报文,占用 CPU。过滤器的存在,可以选择性接收有效报文,减轻系统负担。每个过滤器组都有两个 32 位寄存器 CAN_FxR1 和 CAN_FxR2。根据过滤器组的工作模式不同,寄存器的作用不尽相同。STM32F407 每个过滤器组的位宽都可以独立配置,以满足应用程序的不同需求。根据位宽的不同,每个过滤器组可提供:
- 1 个 32 位过滤器,包括:STDID[10:0]、EXTID[17:0]、IDE 和 RTR 位。
- 2 个 16 位过滤器,包括:STDID[10:0]、IDE、RTR 和 EXTID[17:15] 位。
选择模式 可设置 屏蔽位模式 或 标识符列表模式,寄存器内容的功能就有所区别。
屏蔽位模式,可以选择出 一组符合条件 的报文。寄存器内容功能相当于是否符合条件。屏蔽位模式它把可接收报文 ID 的某几位作为列表,这几位就称为掩码,可以把它理解为关键字搜索,只要掩码(关键字)相同,就符合要求,报文就会保存到接收 FIFO。
标识符列表模式,可以选择出几个 特定 ID 的报文。寄存器内容功能就是标识符本身。它把要接收报文的 ID 列成一个表,要求报文 ID 与列表中的某一个标识符完全相同才可以接收,可以理解为白名单管理。
如果使能了筛选器,且报文的 ID 与所有筛选器的配置都不匹配,CAN 外设会丢弃该报文,不存入 FIFO。
REG 中 bit 值代表的是匹配与否:1 必须匹配 0 不用关心
在屏蔽位模式下,标识符寄存器和屏蔽寄存器一起,指定报文标识符的任何一位,应该按照 “必须匹配” 或 “不用关心” 处理。而在标识符列表模式下,屏蔽寄存器也被当作标识符寄存器用。因此,不是采用一个标识符加一个屏蔽位的方式,而是使用 2 个标识符寄存器。接收报文标识符的每一位都必须跟过滤器标识符相同。
2.3、CAN控制器的位时序
STM32 外设定义的 CAN 控制器的位时序与 CAN 标准的位时序有一点区别,STM32 它把传播时间段和相位缓冲段 1 做了合并。所以 STM32 的 CAN 一个位只有 3 段:同步段(SYNC_SEG)、时间段 1(BS1)和时间段 2(BS2)。STM32 的 BS1 段可以设置为 1 ~ 16 个时间单元,刚好等于我们上面介绍的传播时间段和相位缓冲段 1 之和。
我们只需要知道 BS1 和 BS2 的设置,以及 APB1 的时钟频率(一般为 42Mhz),就可以方便的计算出波特率。比如设置 TS1=6、TS2=5 和 BRP=5,在 APB1 频率为 42Mhz 的条件下,即可得到 CAN 通信的 \(波特率 = \frac{f_{APB1}}{(TS1 + 1) + (TS2 + 1) * (BKP + 1)} = \frac{42000}{[(7+6+1)*6]} = 500Kbps\)。
三、CAN常用的寄存器
3.1、CAN控制和状态寄存器
3.1.1、CAN主控制寄存器
该寄存器负责管理 CAN 的工作模式。位 0 INRQ 位,该位用来控制初始化请求。在 CAN 初始化的时候,先要设置该位为 1,然后进行初始化(尤其是 CAN_BTR 的设置,该寄存器必须在 CAN 正常工作之前设置),之后再设置该位为 0,让 CAN 进入正常工作模式。
3.1.2、CAN主状态寄存器
3.1.3、CAN位时序寄存器
CAN 位时序寄存器用于设置分频、Tbs1、Tbs2 以及 Tsjw 等非常重要的参数,直接决定了 CAN 的波特率。
另外该寄存器还可以设置 CAN 的测试模式,STM32 提供了三种测试模式,回环模式、静默模式和回环静默模式。在环回模式下,bxCAN 把发送的报文当作接收的报文并保存(如果可以通过接收过滤器组)在接收 FIFO 的输出邮箱里。也就是回环模式是一个自发自收的模式。
回环模式可用于自测试。为了避免外部的影响,在回环模式下 CAN 内核忽略确认错误(在数据/远程帧的确认位时刻,不检测是否有显性位)。在回环模式下,bxCAN 在内部把 Tx 输出回馈到 Rx 输入上,而完全忽略 CANRX 引脚的实际状态。发送的报文可以在 CANTX 引脚上检测到。
3.2、CAN邮箱寄存器
CAN 接收 FIFO 邮箱标识符寄存器 (CAN_RIxR)(x=0/1),该寄存器各位描述同 CAN_TIxR 寄存器几乎一模一样,只是最低位为保留位,该寄存器用于保存接收到的报文标识符等信息,我们可以通过读该寄存器获取相关信息。同样的,CAN 接收 FIFO 邮箱数据长度和时间戳寄存器 (CAN_RDTxR)、CAN 接收 FIFO 邮箱低字节数据寄存器(CAN_RDLxR)和 CAN 接收 FIFO 邮箱高字节数据寄存器 (CAN_RDHxR)分别和发送邮箱的:CAN_TDTxR、CAN_TDLxR 以及 CAN_TDHxR 类似
3.2.1、CAN发送邮箱标识符寄存器
该寄存器主要用来设置标识符(包括扩展标识符),另外还可以设置帧类型,通过 TXRQ(位 0)值 1,来请求邮箱发送。因为有 3 个发送邮箱,所以寄存器 CAN_TIxR 有 3 个。
3.2.2、CAN邮箱数据长度控制和时间戳寄存器
该寄存器位 DLC[3:0] 可以用来设置数据长度,即最低 4 个位。
3.2.3、CAN发送邮箱低字节数据寄存器
该寄存器用来存储将要发送的数据,这里只能存储低 4 个字节,另外还有一个寄存器CAN_TDHxR,该寄存器用来存储高 4 个字节,这样总共就可以存储 8 个字节。
3.2.4、CAN邮箱数据高位寄存器
3.3、CAN筛选器寄存器
3.3.1、CAN筛选器主寄存器
3.3.2、CAN筛选器模式寄存器
该寄存器用于设置各滤波器组的工作模式,对 28 个滤波器组的工作模式,都可以通过该寄存器设置,不过该寄存器必须在过滤器处于初始化模式下(CAN_FMR 的 FINIT 位 = 1),才可以进行设置。
3.3.2、CAN筛选器尺度寄存器
该寄存器用于设置各滤波器组的位宽,对 28 个滤波器组的位宽设置,都可以通过该寄存器实现。该寄存器也只能在过滤器处于初始化模式下进行设置。
3.3.4、CAN筛选器FIFO分配寄存器
该寄存器设置报文通过滤波器组之后,被存入的 FIFO,如果对应位为 0,则存放到 FIFO0;如果为 1,则存放到 FIFO1。该寄存器也只能在过滤器处于初始化模式下配置。
3.3.5、CAN筛选器激活寄存器
CAN 过滤器激活寄存器(CAN_FA1R),把对应位置 1,即开启对应的滤波器组;置 0 则关闭该滤波器组。
3.3.6、筛选器组i寄存器x
每个滤波器组的 CAN_FiRx 都由 2 个 32 位寄存器构成,即:CAN_FiR1 和 CAN_FiR2。根据过滤器位宽和模式的不同设置,这两个寄存器的功能也不尽相同。
四、IO引脚复用功能
【1】、CAN1引脚复用功能
功能引脚 | 复用引脚 |
---|---|
CAN1_TX | PA12 |
CAN1_RX | PA11 |
【2】、CAN2 引脚复用及其重映射功能
功能引脚 | 复用引脚 |
---|---|
CAN2_TX | PB13 |
CAN2_RX | PB12 |
五、CAN的配置步骤
5.1、使能CAN时钟和对应的GPIO时钟
使能 CAN 时钟:
#define __HAL_RCC_CAN1_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_CAN1EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_CAN1EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_CAN2_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_CAN2EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_CAN2EN);\
UNUSED(tmpreg); \
} while(0U)
使能对应的 GPIO 时钟:
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOB_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
UNUSED(tmpreg); \
} while(0U)
5.2、配置CAN工作参数
要使用一个外设需要对它进行初始化,其声明如下:
HAL_StatusTypeDef HAL_CAN_Init(CAN_HandleTypeDef *hcan);
形参 hcan 是 CAN 的控制句柄,结构体类型是 CAN_HandleTypeDef,其定义如下:
typedef struct
{
CAN_TypeDef *Instance; // AN 控制寄存器基地址
CAN_InitTypeDef Init; // AN 控制寄存器基地址
__IO HAL_CAN_StateTypeDef State; // CAN通讯状态
__IO uint32_t ErrorCode; // CAN 通讯结果编码
} CAN_HandleTypeDef;
Instance:指向 CAN 寄存器基地址,可选值如下:
#define CAN1 ((CAN_TypeDef *) CAN1_BASE)
#define CAN2 ((CAN_TypeDef *) CAN2_BASE)
Init:CAN 初始化结构体,用于配置 CAN 的工作模式、波特率等等,定义如下:
typedef struct
{
uint32_t Prescaler; // 分频值,可以配置为1~1024间的任意整数
uint32_t Mode; // CAN操作模式
uint32_t SyncJumpWidth; // CAN硬件的最大超时时间
uint32_t TimeSeg1; // CAN_time_quantum_in_bit_segment_
uint32_t TimeSeg2; // CAN_time_quantum_in_bit_segment_2
FunctionalState TimeTriggeredMode; // 启用或禁用时间触发模式
FunctionalState AutoBusOff; // 禁止/使能软件自动断开总线的功能
FunctionalState AutoWakeUp; // 禁止/使能CAN 的自动唤醒功能
FunctionalState AutoRetransmission; // 禁止/使能CAN的自动传输模式
FunctionalState ReceiveFifoLocked; // 禁止/使能CAN 的接收 FIFO
FunctionalState TransmitFifoPriority; // 禁止/使能CAN 的发送 FIFO
} CAN_InitTypeDef;
成员 Mode 设置 CAN 操作模式,可选值如下:
#define CAN_MODE_NORMAL (0x00000000U) /*!< Normal mode */
#define CAN_MODE_LOOPBACK ((uint32_t)CAN_BTR_LBKM) /*!< Loopback mode */
#define CAN_MODE_SILENT ((uint32_t)CAN_BTR_SILM) /*!< Silent mode */
#define CAN_MODE_SILENT_LOOPBACK ((uint32_t)(CAN_BTR_LBKM | CAN_BTR_SILM)) /*!< Loopback combined with silent mode */
成员 TimeSeg1 设置 时间段 1,可选值如下:
#define CAN_BS1_1TQ (0x00000000U) /*!< 1 time quantum */
#define CAN_BS1_2TQ ((uint32_t)CAN_BTR_TS1_0) /*!< 2 time quantum */
#define CAN_BS1_3TQ ((uint32_t)CAN_BTR_TS1_1) /*!< 3 time quantum */
#define CAN_BS1_4TQ ((uint32_t)(CAN_BTR_TS1_1 | CAN_BTR_TS1_0)) /*!< 4 time quantum */
#define CAN_BS1_5TQ ((uint32_t)CAN_BTR_TS1_2) /*!< 5 time quantum */
#define CAN_BS1_6TQ ((uint32_t)(CAN_BTR_TS1_2 | CAN_BTR_TS1_0)) /*!< 6 time quantum */
#define CAN_BS1_7TQ ((uint32_t)(CAN_BTR_TS1_2 | CAN_BTR_TS1_1)) /*!< 7 time quantum */
#define CAN_BS1_8TQ ((uint32_t)(CAN_BTR_TS1_2 | CAN_BTR_TS1_1 | CAN_BTR_TS1_0)) /*!< 8 time quantum */
#define CAN_BS1_9TQ ((uint32_t)CAN_BTR_TS1_3) /*!< 9 time quantum */
#define CAN_BS1_10TQ ((uint32_t)(CAN_BTR_TS1_3 | CAN_BTR_TS1_0)) /*!< 10 time quantum */
#define CAN_BS1_11TQ ((uint32_t)(CAN_BTR_TS1_3 | CAN_BTR_TS1_1)) /*!< 11 time quantum */
#define CAN_BS1_12TQ ((uint32_t)(CAN_BTR_TS1_3 | CAN_BTR_TS1_1 | CAN_BTR_TS1_0)) /*!< 12 time quantum */
#define CAN_BS1_13TQ ((uint32_t)(CAN_BTR_TS1_3 | CAN_BTR_TS1_2)) /*!< 13 time quantum */
#define CAN_BS1_14TQ ((uint32_t)(CAN_BTR_TS1_3 | CAN_BTR_TS1_2 | CAN_BTR_TS1_0)) /*!< 14 time quantum */
#define CAN_BS1_15TQ ((uint32_t)(CAN_BTR_TS1_3 | CAN_BTR_TS1_2 | CAN_BTR_TS1_1)) /*!< 15 time quantum */
#define CAN_BS1_16TQ ((uint32_t)CAN_BTR_TS1) /*!< 16 time quantum */
成员 TimeSeg2 设置 时间段 2,可选值如下:
#define CAN_BS2_1TQ (0x00000000U) /*!< 1 time quantum */
#define CAN_BS2_2TQ ((uint32_t)CAN_BTR_TS2_0) /*!< 2 time quantum */
#define CAN_BS2_3TQ ((uint32_t)CAN_BTR_TS2_1) /*!< 3 time quantum */
#define CAN_BS2_4TQ ((uint32_t)(CAN_BTR_TS2_1 | CAN_BTR_TS2_0)) /*!< 4 time quantum */
#define CAN_BS2_5TQ ((uint32_t)CAN_BTR_TS2_2) /*!< 5 time quantum */
#define CAN_BS2_6TQ ((uint32_t)(CAN_BTR_TS2_2 | CAN_BTR_TS2_0)) /*!< 6 time quantum */
#define CAN_BS2_7TQ ((uint32_t)(CAN_BTR_TS2_2 | CAN_BTR_TS2_1)) /*!< 7 time quantum */
#define CAN_BS2_8TQ ((uint32_t)CAN_BTR_TS2) /*!< 8 time quantum */
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
typedef enum
{
HAL_OK = 0x00U, // 成功
HAL_ERROR = 0x01U, // 错误
HAL_BUSY = 0x02U, // 忙碌
HAL_TIMEOUT = 0x03U // 超时
} HAL_StatusTypeDef;
5.3、CAN底层初始化
HAL 库中,提供 HAL_GPIO_Init() 函数用于配置 GPIO 功能模式,初始化 GPIO。该函数的声明如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
该函数的第一个形参 GPIOx 用来 指定端口号,可选值如下:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
第二个参数是 GPIO_InitTypeDef 类型的结构体变量,用来 设置 GPIO 的工作模式,其定义如下:
typedef struct
{
uint32_t Pin; // 引脚号
uint32_t Mode; // 模式设置
uint32_t Pull; // 上下拉设置
uint32_t Speed; // 速度设置
uint32_t Alternate; // 复用功能设置
}GPIO_InitTypeDef;
成员 Pin 表示 引脚号,范围:GPIO_PIN_0 到 GPIO_PIN_15。
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
成员 Mode 是 GPIO 的 模式选择,有以下选择项:
#define GPIO_MODE_AF_OD 0x00000002U // 开漏式复用
成员 Pull 用于 配置上下拉电阻,有以下选择项:
#define GPIO_NOPULL 0x00000000U // 无上下拉
#define GPIO_PULLUP 0x00000001U // 上拉
#define GPIO_PULLDOWN 0x00000002U // 下拉
成员 Speed 用于 配置 GPIO 的速度,有以下选择项:
#define GPIO_SPEED_FREQ_LOW 0x00000000U // 低速
#define GPIO_SPEED_FREQ_MEDIUM 0x00000001U // 中速
#define GPIO_SPEED_FREQ_HIGH 0x00000002U // 高速
#define GPIO_SPEED_FREQ_VERY_HIGH 0x00000003U // 极速
成员 Alternate 用于 配置具体的复用功能,不同的 GPIO 口可以复用的功能不同,具体可参考数据手册。
#define GPIO_AF9_CAN1 ((uint8_t)0x09) /* CAN1 Alternate Function mapping */
#define GPIO_AF9_CAN2 ((uint8_t)0x09) /* CAN2 Alternate Function mapping */
5.4、配置过滤器
CAN 的接收过滤器是属于硬件,可以根据软件的设置,在接收报文的时候,可以过滤出符合过滤器配置条件的报文 ID,大大节省了 CPU 的开销,过滤器配置函数定义如下:
HAL_StatusTypeDef HAL_CAN_ConfigFilter(CAN_HandleTypeDef *hcan, const CAN_FilterTypeDef *sFilterConfig);
形参 hcan 是 CAN 的控制句柄指针。
形参 sFilterConfig 是 过滤器的结构体,这个是根据 STM32 的 CAN 过滤器模式设置的一些配置参数,它的结构如下:
typedef struct
{
uint32_t FilterIdHigh; // 过滤器标识符高位
uint32_t FilterIdLow; // 过滤器标识符低位
uint32_t FilterMaskIdHigh; // 过滤器掩码号高位(列表模式下,也是属于标识符)
uint32_t FilterMaskIdLow; // 过滤器掩码号低位(列表模式下,也是属于标识符)
uint32_t FilterFIFOAssignment; // 与过滤器组关联的FIFO
uint32_t FilterBank; // 指定过滤器组,单CAN为0~13,双CAN可为0~27
uint32_t FilterMode; // 过滤器的模式,标识符屏蔽位模式/标识符列表模式
uint32_t FilterScale; // 过滤器的位宽,32位/16位
uint32_t FilterActivation; // 禁用或者使能过滤器
uint32_t SlaveStartFilterBank; // 双CAN 模式下,规定CAN 的主从模式的过滤器分配
} CAN_FilterTypeDef;
成员 FilterFIFOAssignment 设置 与过滤器组关联的 FIFO,可选值如下:
#define CAN_RX_FIFO0 (0x00000000U) /*!< CAN receive FIFO 0 */
#define CAN_RX_FIFO1 (0x00000001U) /*!< CAN receive FIFO 1 */
成员 FilterMode 设置 过滤器的模式,可选值如下:
#define CAN_FILTERMODE_IDMASK (0x00000000U) /*!< Identifier mask mode */
#define CAN_FILTERMODE_IDLIST (0x00000001U) /*!< Identifier list mode */
成员 FilterScale 设置 过滤器的位宽,可选值如下:
#define CAN_FILTERSCALE_16BIT (0x00000000U) /*!< Two 16-bit filters */
#define CAN_FILTERSCALE_32BIT (0x00000001U) /*!< One 32-bit filter */
成员 FilterActivation 设置 禁用或者使能过滤器,可选值如下:
#define CAN_FILTER_DISABLE (0x00000000U) /*!< Disable filter */
#define CAN_FILTER_ENABLE (0x00000001U) /*!< Enable filter */
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
5.5、启动CAN设备
使能 CAN 控制器以接入总线进行数据收发处理。
HAL_StatusTypeDef HAL_CAN_Start(CAN_HandleTypeDef *hcan);
形参 hcan 是 CAN 的控制句柄指针。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
5.6、使能CAN的各种中断
CAN 定义了多种传输中断以满足多种需求,我们只需要在形参 ActiveITs 中填入相关中断即可,中断源可以在 CAN_IER 寄存器中找到。
HAL_StatusTypeDef HAL_CAN_ActivateNotification(CAN_HandleTypeDef *hcan, uint32_t ActiveITs);
形参 hcan 是 CAN 的控制句柄指针。
形参 ActiveITs 是 CAN 的中断源,可选值如下:
/* Transmit Interrupt */
#define CAN_IT_TX_MAILBOX_EMPTY ((uint32_t)CAN_IER_TMEIE) /*!< Transmit mailbox empty interrupt */
/* Receive Interrupts */
#define CAN_IT_RX_FIFO0_MSG_PENDING ((uint32_t)CAN_IER_FMPIE0) /*!< FIFO 0 message pending interrupt */
#define CAN_IT_RX_FIFO0_FULL ((uint32_t)CAN_IER_FFIE0) /*!< FIFO 0 full interrupt */
#define CAN_IT_RX_FIFO0_OVERRUN ((uint32_t)CAN_IER_FOVIE0) /*!< FIFO 0 overrun interrupt */
#define CAN_IT_RX_FIFO1_MSG_PENDING ((uint32_t)CAN_IER_FMPIE1) /*!< FIFO 1 message pending interrupt */
#define CAN_IT_RX_FIFO1_FULL ((uint32_t)CAN_IER_FFIE1) /*!< FIFO 1 full interrupt */
#define CAN_IT_RX_FIFO1_OVERRUN ((uint32_t)CAN_IER_FOVIE1) /*!< FIFO 1 overrun interrupt */
/* Operating Mode Interrupts */
#define CAN_IT_WAKEUP ((uint32_t)CAN_IER_WKUIE) /*!< Wake-up interrupt */
#define CAN_IT_SLEEP_ACK ((uint32_t)CAN_IER_SLKIE) /*!< Sleep acknowledge interrupt */
/* Error Interrupts */
#define CAN_IT_ERROR_WARNING ((uint32_t)CAN_IER_EWGIE) /*!< Error warning interrupt */
#define CAN_IT_ERROR_PASSIVE ((uint32_t)CAN_IER_EPVIE) /*!< Error passive interrupt */
#define CAN_IT_BUSOFF ((uint32_t)CAN_IER_BOFIE) /*!< Bus-off interrupt */
#define CAN_IT_LAST_ERROR_CODE ((uint32_t)CAN_IER_LECIE) /*!< Last error code interrupt */
#define CAN_IT_ERROR ((uint32_t)CAN_IER_ERRIE) /*!< Error Interrupt */
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
5.7、发送消息
该函数用于向发送邮箱添加发送报文,并激活发送请求。
HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, const CAN_TxHeaderTypeDef *pHeader, const uint8_t aData[], uint32_t *pTxMailbox);
形参 hcan 是 CAN 的控制句柄指针。
形参 pHeader 是 CAN 发送的结构体,定义如下:
typedef struct
{
uint32_t StdId; // 标准标识符11位范围:0~0x7FF
uint32_t ExtId; // 扩展标识符29位范围:0~0x1FFFFFFF
uint32_t IDE; // 标识符类型CAN_ID_STD/CAN_ID_EXT
uint32_t RTR; // 帧类型 CAN_RTR_DATA/CAN_RTR_REMOTE
uint32_t DLC; // 帧长度 范围:0~8byt
FunctionalState TransmitGlobalTime; // 帧长度 范围:0~8byt
} CAN_TxHeaderTypeDef;
成员 IDE 设置 标识符类型,可选值如下:
#define CAN_ID_STD (0x00000000U) /*!< Standard Id */
#define CAN_ID_EXT (0x00000004U) /*!< Extended Id */
成员 RTR 设置 帧类型,可选值如下:
#define CAN_RTR_DATA (0x00000000U) /*!< Data frame */
#define CAN_RTR_REMOTE (0x00000002U) /*!< Remote frame */
形参 aData 是 报文的内容。
形参 pTxMailbox 是 发送邮箱编号,可选三个发送邮箱之一。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
5.8、等待发送完成
uint32_t HAL_CAN_GetTxMailboxesFreeLevel(const CAN_HandleTypeDef *hcan);
形参 hcan 是 CAN 的控制句柄指针。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
5.9、接收数据
HAL_StatusTypeDef HAL_CAN_GetRxMessage(CAN_HandleTypeDef *hcan, uint32_t RxFifo, CAN_RxHeaderTypeDef *pHeader, uint8_t aData[]);
形参 hcan 是 CAN 的控制句柄指针。
形参 RxFifo 是 报文的内容。
形参 pHeader 是 CAN 接收的结构体,定义如下:
typedef struct
{
uint32_t StdId; // 标准标识符11位范围:0~0x7FF
uint32_t ExtId; // 扩展标识符29位范围:0~0x1FFFFFFF
uint32_t IDE; // 标识符类型CAN_ID_STD/CAN_ID_EXT
uint32_t RTR; // 帧类型 CAN_RTR_DATA/CAN_RTR_REMOTE
uint32_t DLC; // 帧长度 范围:0~8byt
uint32_t Timestamp; // 时间戳
uint32_t FilterMatchIndex; // 过滤器编号
} CAN_RxHeaderTypeDef;
形参 aData 是 报文的内容。
5.10、等待接收完成
uint32_t HAL_CAN_GetRxFifoFillLevel(const CAN_HandleTypeDef *hcan, uint32_t RxFifo);
形参 hcan 是 CAN 的控制句柄指针。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
六、原理图
七、程序源码
CAN 初始化函数:
CAN_HandleTypeDef g_can1_handle;
/**
* @brief CAN1初始化函数
*
* @param mode CAN的工作模式
* @param BKP 分频值
* @param TS1 时间段1
* @param TS2 时间段2
* @param sjw 重新同步跳跃宽度
*/
void CAN_Init(CAN_HandleTypeDef *hcan, CAN_InitTypeDef *CANx, uint32_t mode, uint32_t BKP, uint32_t TS1, uint32_t TS2, uint32_t sjw)
{
hcan->Instance = CANx; // CAN的寄存器基地址
hcan->Init.Mode = mode; // CAN工作模式
// 波特率相关
hcan->Init.Prescaler = BKP + 1; // 分频系数
hcan->Init.TimeSeg1 = TS1; // 时间段1
hcan->Init.TimeSeg2 = TS2; // 时间段2
hcan->Init.SyncJumpWidth = sjw; // 重新同步跳跃宽度
// CAN功能设置
hcan->Init.AutoBusOff = DISABLE; // 禁止自动离线管理
hcan->Init.AutoRetransmission = DISABLE; // 禁止自动重发
hcan->Init.AutoWakeUp = DISABLE; // 禁止自动唤醒
hcan->Init.ReceiveFifoLocked = DISABLE; // 禁止自动接收FIFO锁定
hcan->Init.TimeTriggeredMode = DISABLE; // 禁止时间触发通信模式
hcan->Init.TransmitFifoPriority = DISABLE; // 禁止发送FIFO优先级
HAL_CAN_Init(hcan);
}
CAN 底层初始化函数:
/**
* @brief CAN底层初始化函数
*
* @param hcan CAN句柄
*/
void HAL_CAN_MspInit(CAN_HandleTypeDef *hcan)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (hcan->Instance == CAN1)
{
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF9_CAN1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
CAN 配置过滤器函数:
/**
* @brief CAN配置过滤器函数
*
* @param hcan CAN句柄
* @param filterMode 过滤器模式,可选值: [CAN_FILTERMODE_IDMASK, CAN_FILTERMODE_IDLIST]
* @param filterScale 过滤器位宽,可选值: [CAN_FILTERSCALE_16BIT, CAN_FILTERSCALE_32BIT]
* @param filterId 过滤器标识符
* @param filterMaskId 过滤器掩码号
* @param bank 过滤器组
* @param fifo FIFO选择,可选值: [CAN_RX_FIFO0, CAN_RX_FIFO1]
*/
void CAN_ConfigFilter(CAN_HandleTypeDef *hcan, uint32_t mode, uint32_t scale, uint32_t id, uint32_t maskId, uint32_t bank, uint32_t fifo)
{
CAN_FilterTypeDef CAN_FilterStruct = {0};
CAN_FilterStruct.FilterMode = mode; // 过滤器的模式
CAN_FilterStruct.FilterScale = scale; // 过滤器的位宽
CAN_FilterStruct.FilterIdHigh = id >> 16; // 过滤器标识符高位
CAN_FilterStruct.FilterIdLow = id & 0xFFFF; // 过滤器标识符低位
CAN_FilterStruct.FilterMaskIdHigh = maskId >> 16; // 过滤器掩码号高
CAN_FilterStruct.FilterMaskIdLow = maskId & 0xFFFF; // 过滤器掩码号低
CAN_FilterStruct.FilterBank = bank; // 指定过滤器组
CAN_FilterStruct.FilterFIFOAssignment = fifo; // 指定过滤器分配到哪个FIFO
CAN_FilterStruct.FilterActivation = CAN_FILTER_ENABLE; // 禁用或者使能过滤器
HAL_CAN_ConfigFilter(hcan, &CAN_FilterStruct);
}
CAN 发送数据函数:
/**
* @brief CAN发送数据函数
*
* @param hcan CAN句柄
* @param id 标识符
* @param mail 发送数据的邮箱,可选值: CAN_TX_MAILBOXx, x可选[0, 1, 2]
* @param data 数据
* @param length 数据长度
*/
void CAN_SendMessage(CAN_HandleTypeDef *hcan, uint32_t id, uint32_t mail, uint8_t *data, uint8_t length)
{
CAN_TxHeaderTypeDef CAN_TxHeaderStruct = {0};
CAN_TxHeaderStruct.StdId = id; // 标准标识符
CAN_TxHeaderStruct.DLC = length; // 数据长度
CAN_TxHeaderStruct.IDE = CAN_ID_STD; // 标识符类型
CAN_TxHeaderStruct.RTR = CAN_RTR_DATA; // 数据类型
HAL_CAN_AddTxMessage(hcan, &CAN_TxHeaderStruct, data, &mail);
while (HAL_CAN_GetTxMailboxesFreeLevel(hcan) != 3);
}
CAN 接收数据函数:
/**
* @brief CAN接收数据
*
* @param hcan CAN句柄
* @param fifo FIFO选择,可选值: CAN_RX_FIFOx, x可取[0, 1]
* @param data 保存接收的数据
* @return uint8_t 接收数据的长度
*/
uint8_t CAN_ReceiveMessage(CAN_HandleTypeDef *hcan, uint32_t fifo, uint8_t *data)
{
CAN_RxHeaderTypeDef CAN_RxHeaderStruct = {0};
if (HAL_CAN_GetRxFifoFillLevel(hcan, fifo) == 0)
{
return 0;
}
HAL_CAN_GetRxMessage(hcan, fifo, &CAN_RxHeaderStruct, data);
return CAN_RxHeaderStruct.DLC;
}
main() 函数:
int main(void)
{
char sendData[] = "Sakura";
char receiveData[8] = {0};
int length = 0;
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
UART_Init(&g_usart1_handle, USART1, 115200);
CAN_Init(&g_can1_handle, CAN1, CAN_MODE_LOOPBACK, 5, CAN_BS1_5TQ, CAN_BS2_5TQ, CAN_SJW_1TQ);
CAN_ConfigFilter(&g_can1_handle, CAN_FILTERMODE_IDMASK, CAN_FILTERSCALE_32BIT, 0, 0, 0, CAN_RX_FIFO0);
HAL_CAN_Start(&g_can1_handle);
while (1)
{
CAN_SendMessage(&g_can1_handle, CAN_TX_MAILBOX0,0x123, (uint8_t *)sendData, strlen(sendData));
HAL_Delay(1000);
length = CAN_ReceiveMessage(&g_can1_handle, CAN_RX_FIFO0, (uint8_t *)receiveData);
if (length)
{
printf("%s\r\n", receiveData);
memset(receiveData, 0, length);
}
}
return 0;
}