【正点原子STM32连载】第38章:CAN通讯实验(摘自【正点原子】STM32F103 战舰开发指南V1.2)

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#

第三十八章 CAN通讯实验

本章我们将向大家介绍如何使用 STM32自带的CAN控制器来实现CAN的收发功能,并将结果显示在TFTLCD模块上。本章分为如下几个部分:
38.1 CAN简介:
38.2 硬件设计
38.3 程序设计
38.4 下载验证

38.1 CAN总线简介
38.1.1 CAN简介
CAN是Controller Area Network的缩写(以下称为CAN),是ISO国际标准化的串行通信协议。在当前的汽车产业中,出于对安全性、舒适性、方便性、低公害、低成本的要求,各种各样的电子控制系统被开发了出来。由于这些系统之间通信所用的数据类型及对可靠性的要求不尽相同,由多条总线构成的情况很多,线束的数量也随之增加。为适应“减少线束的数量”、“通过多个LAN,进行大量数据的高速通信”的需要,1986年德国电气商博世公司开发出面向汽车的CAN通信协议。此后,CAN通过ISO11898及ISO11519进行了标准化,现在在欧洲已是汽车网络的标准协议。
现在,CAN的高性能和可靠性已被认同,并被广泛地应用于工业自动化、船舶、医疗设备、工业设备等方面。现场总线是当今自动化领域技术发展的热点之一,被誉为自动化领域的计算机局域网。它的出现为分布式控制系统实现各节点之间实时、可靠的数据通信提供了强有力的技术支持。
CAN协议具有一下特点:
①多主控制。在总线空闲时,所有单元都可以发送消息(多主控制),而两个以上的单元同时开始发送消息时,根据标识符(Identifier以下称为ID)决定优先级。ID并不是表示发送的目的地址,而是表示访问总线的消息的优先级。两个以上的单元同时开始发送消息时,对各消息ID的每个位进行逐个仲裁比较。仲裁获胜(被判定为优先级最高)的单元可继续发送消息,仲裁失利的单元则立刻停止发送而进行接收工作。
②系统的柔软性。与总线相连的单元没有类似于“地址”的信息。因此在总线上增加单元时,连接在总线上的其它单元的软硬件及应用层都不需要改变。
③通信速度较快,通信距离远。最高1Mbps(距离小于40M),最远可达10KM(速率低于5Kbps)。
④具有错误检测、错误通知和错误恢复功能。所有单元都可以检测错误(错误检测功能),检测出错误的单元会立即同时通知其他所有单元(错误通知功能),正在发送消息的单元一旦检测出错误,会强制结束当前的发送。强制结束发送的单元会不断反复地重新发送此消息直到成功发送为止(错误恢复功能)。
⑤故障封闭功能。CAN可以判断出错误的类型是总线上暂时的数据错误(如外部噪声等)还是持续的数据错误(如单元内部故障、驱动器故障、断线等)。由此功能,当总线上发生持续数据错误时,可将引起此故障的单元从总线上隔离出去。
⑥连接节点多。CAN总线是可同时连接多个单元的总线。可连接的单元总数理论上是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。
正是因为CAN协议的这些特点,使得CAN特别适合工业过程监控设备的互连,因此,越来越受到工业界的重视,并已公认为最有前途的现场总线之一。
CAN协议经过ISO标准化后有两个标准:ISO11898标准(高速CAN)和ISO11519-2标准(低速CAN)。其中ISO11898是针对通信速率为125Kbps~1Mbps的高速通信标准,而ISO11519-2是针对通信速率为125Kbps以下的低速通信标准。
本章,我们使用的是ISO11898标准,也就是高速CAN,其拓扑图如图38.1.1.1所示。

图38.1.1 高速CAN拓扑结构图
从上图可知,高速CAN总线呈现的是一个闭环结构,总线是由两根线CAN_High和CAN_Low组成,且在总线两端各串联了120Ω的电阻(用于阻抗匹配,减少回波反射),同时总线上可以挂载多个节点。每个节点都有CAN收发器以及CAN控制器,CAN控制器通常是MCU的外设,集成在芯片内部;CAN收发器则是需要外加芯片转换电路。
CAN类似RS485也是通过差分信号传输数据。根据CAN总线上两根线的电位差来判断总线电平。总线电平分为显性电平和隐性电平,二者必居其一。这是属于物理层特征,ISO11898物理层特性如图38.1.1.2所示:

图38.1.1.2 ISO11898物理层特性
从该特性可以看出,显性电平对应逻辑0,CAN_H和CAN_L之差为2 V左右。而隐性电平对应逻辑1,CAN_H和CAN_L之差为0V。在总线上显性电平具有优先权,只要有一个单元输出显性电平,总线上即为显性电平。而隐形电平则具有包容的意味,只有所有的单元都输出隐性电平,总线上才为隐性电平(显性电平比隐性电平更强)。
38.1.2 CAN协议
CAN协议是通过以下5种类型的帧进行的:
数据帧
遥控帧
错误帧
过载帧
间隔帧
另外,数据帧和遥控帧有标准格式和扩展格式两种格式。标准格式有11个位的标识符(ID),扩展格式有29个位的ID。各种帧的用途如表38.1.2.1所示:

表38.1.2.1 CAN协议各种帧及其用途
由于篇幅所限,我们这里仅对数据帧进行详细介绍,数据帧一般由7个段构成,即:
帧起始。表示数据帧开始的段。
仲裁段。表示该帧优先级的段。
控制段。表示数据的字节数及保留位的段。
数据段。数据的内容,一帧可发送0~8个字节的数据。
CRC段。检查帧的传输错误的段。
ACK段。表示确认正常接收的段。
帧结束。表示数据帧结束的段。
数据帧的构成如图38.1.2.1所示:

图38.1.2.1数据帧的构成
图中D表示显性电平,R表示隐形电平(下同)。
帧起始,这个比较简单,标准帧和扩展帧都是由1个位的显性电平表示帧起始。
仲裁段,表示数据优先级的段,标准帧和扩展帧格式在本段有所区别,如图38.1.2.2所示:

图38.1.2.2 数据帧仲裁段构成
标准格式的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个位构成,表示数据段的字节数。标准帧和扩展帧的控制段稍有不同,如图38.1.2.3所示:

图38.1.2.3 数据帧控制段构成
上图中,r0和r1为保留位,必须全部以显性电平发送,但是接收端可以接收显性、隐性及任意组合的电平。DLC段为数据长度表示段,高位在前,DLC段有效值为08,但是接收方接收到915的时候并不认为是错误。
数据段,该段可包含0~8个字节的数据。从最高位(MSB)开始输出,标准帧和扩展帧在这个段的定义都是一样的。如图图38.1.2.4所示:

图38.1.2.4 数据帧数据段构成
CRC段,该段用于检查帧传输错误。由15个位的CRC顺序和1个位的CRC界定符(用于分隔的位)组成,标准帧和扩展帧在这个段的格式也是相同的。如图38.1.2.5所示:

图38.1.2.5 数据帧CRC段构成
此段CRC的值计算范围包括:帧起始、仲裁段、控制段、数据段。接收方以同样的算法计算CRC值并进行比较,不一致时会通报错误。
ACK段,此段用来确认是否正常接收。由ACK槽(ACKSlot)和ACK界定符2个位组成。标准帧和扩展帧在这个段的格式也是相同的。如图38.1.2.6所示:

图38.1.2.6 数据帧CRC段构成
发送单元的ACK,发送2个位的隐性位,而接收到正确消息的单元在ACK槽(ACKSlot)发送显性位,通知发送单元正常接收结束,这个过程叫发送ACK/返回ACK。发送ACK的是在既不处于总线关闭态也不处于休眠态的所有接收单元中,接收到正常消息的单元(发送单元不发送ACK)。所谓正常消息是指不含填充错误、格式错误、CRC错误的消息。
帧结束,这个段也比较简单,标准帧和扩展帧在这个段格式一样,由7个位的隐性位组成。至此,数据帧的7个段就介绍完了,其他帧的介绍,请大家参考光盘的《CAN入门书.pdf》相关章节。接下来,我们再来看看CAN的位时序。
由发送单元在非同步的情况下发送的每秒钟的位数称为位速率。一个位可分为4段。
同步段(SS)
传播时间段(PTS)
相位缓冲段1(PBS1)
相位缓冲段2(PBS2)
这些段又由可称为Time Quantum(以下称为Tq)的最小时间单位构成。
1位分为4个段,每个段又由若干个Tq构成,这称为位时序。
1位由多少个Tq构成、每个段又由多少个Tq构成等,可以任意设定位时序。通过设定位时序,多个单元可同时采样,也可任意设定采样点。各段的作用和Tq数如表38.1.2.2所示:

表38.1.2.2 一个位各段及其作用
1个位的构成如图38.1.2.7所示:

图38.1.2.7 一个位的构成
上图的采样点,是指读取总线电平,并将读到的电平作为位值的点。位置在PBS1结束处。根据这个位时序,我们就可以计算CAN通信的波特率了。具体计算方法,我们等下再介绍,前面提到的CAN协议具有仲裁功能,下面我们来看看是如何实现的。
在总线空闲态,最先开始发送消息的单元获得发送权。当多个单元同时开始发送时,各发送单元从仲裁段的第一位开始进行仲裁。连续输出显性电平最多的单元可继续发送。实现过程,如图38.1.2.8所示:

图38.1.2.8 CAN总线仲裁过程
上图中,单元1和单元2同时开始向总线发送数据,开始部分他们的数据格式是一样的,故无法区分优先级,直到T时刻,单元1输出隐性电平,而单元2输出显性电平,此时单元1仲裁失利,立刻转入接收状态工作,不再与单元2竞争,而单元2则顺利获得总线使用权,继续发送自己的数据。这就实现了仲裁,让连续发送显性电平多的单元获得总线使用权。
通过以上介绍,我们对CAN总线有了个大概了解(详细介绍参考光盘的:《CAN入门书.pdf》),接下来我们介绍下STM32F1的CAN控制器。
STM32F1自带的是bxCAN,即基本扩展CAN。它支持CAN协议2.0A和2.0B。CAN2.0A只能处理标准数据帧,扩展帧的内容会识别错误;CAN2.0B Active可以处理标准数据帧和扩展数据帧;而CAN2.0B passive只能处理标准数据帧,扩展帧的内容会忽略。它的设计目标是,以最小的CPU负荷来高效处理大量收到的报文。它也支持报文发送的优先级要求(优先级特性可软件配置)。对于安全紧要的应用,bxCAN提供所有支持时间触发通信模式所需的硬件功能。
STM32F1的bxCAN的主要特点有:
支持CAN协议2.0A和2.0B主动模式
波特率最高达1Mbps
支持时间触发通信
具有3个发送邮箱
具有3级深度的2个接收FIFO
可变的过滤器组(最多28个)
在STM32互联型产品中,带有2个CAN控制器,而我们使用的STM32F103ZET6属于增强型,不是互联型,只有1个CAN控制器。双CAN的框图如图38.1.2.9所示:

图38.1.2.9双CAN框图
从图中可以看出两个CAN都分别拥有自己的发送邮箱和接收FIFO,但是他们共用28个过滤器。通过CAN_FMR寄存器的设置,可以设置过滤器的分配方式。
STM32的标识符过滤比较复杂,它的存在减少了CPU处理CAN通信的开销。STM32的过滤器组最多有28个(互联型),但是STM32F103ZET6只有14个(增强型),每个滤波器组x由2个32为寄存器,CAN_FxR1和CAN_FxR2组成。
STM32F1每个过滤器组的位宽都可以独立配置,以满足应用程序的不同需求。根据位宽的不同,每个过滤器组可提供:
1个32位过滤器,包括:STDID[10:0]、EXTID[17:0]、IDE和RTR位
2个16位过滤器,包括:STDID[10:0]、IDE、RTR和EXTID[17:15]位
此外过滤器可配置为,屏蔽位模式和标识符列表模式。
在屏蔽位模式下,标识符寄存器和屏蔽寄存器一起,指定报文标识符的任何一位,应该按照“必须匹配”或“不用关心”处理。
而在标识符列表模式下,屏蔽寄存器也被当作标识符寄存器用。因此,不是采用一个标识符加一个屏蔽位的方式,而是使用2个标识符寄存器。接收报文标识符的每一位都必须跟过滤器标识符相同。
通过CAN_FMR寄存器,可以配置过滤器组的位宽和工作模式,如图38.1.2.10所示:

图38.1.2.10 过滤器组位宽模式设置
为了过滤出一组标识符,应该设置过滤器组工作在屏蔽位模式。
为了过滤出一个标识符,应该设置过滤器组工作在标识符列表模式。应用程序不用的过滤器组,应该保持在禁用状态。
过滤器组中的每个过滤器,都被编号为(叫做过滤器号,图38.1.2.10中的n)从0开始,到某个最大数值-取决于过滤器组的模式和位宽的设置。
举个简单的例子,我们设置过滤器组0工作在:1个32为位过滤器-标识符屏蔽模式,然后设置CAN_F0R1=0XFFFF0000,CAN_F0R2=0XFF00FF00。其中存放到CAN_F0R1的值就是期望收到的ID,即我们希望收到的映像(STID+EXTID+IDE+RTR)最好是:0XFFFF0000。而0XFF00FF00就是设置我们需要必须关心的ID,表示收到的映像,其位[31:24]和位[15:8]这16个位的必须和CAN_F0R1中对应的位一模一样,而另外的16个位则不关心,可以一样,也可以不一样,都认为是正确的ID,即收到的映像必须是0XFFxx00xx,才算是正确的(x表示不关心)。这里需要注意的是标识符选择位IDE和帧类型RTR需要一致。其情况如下图38.1.2.11所示:

图38.1.2.11 过滤器举例图
关于标识符过滤的详细介绍,请参考《STM32F10xxx参考手册_V10(中文版).pdf》的22.7.4节(431页)。
接下来,我们看看STM32的CAN发送和接收的流程。
CAN 发送流程
CAN发送流程为:程序选择1个空置的邮箱(TME=1)→设置标识符(ID),数据长度和发送数据→设置CAN_TIxR的TXRQ位为1,请求发送→邮箱挂号(等待成为最高优先级)→预定发送(等待总线空闲)→发送→邮箱空置。整个流程如图38.1.2.12所示:

图38.1.2.12 发送流程图
上图中,还包含了很多其他处理,不强制退出发送(ABRQ=1)和发送失败处理等。通过这个流程图,我们大致了解了CAN的发送流程,后面的数据发送,我们基本就是按照此流程来走。接下来再看看CAN的接收流程。
CAN接收流程
CAN接收到的有效报文,被存储在3级邮箱深度的FIFO中。FIFO完全由硬件来管理,从而节省了CPU的处理负荷,简化了软件并保证了数据的一致性。应用程序只能通过读取FIFO输出邮箱,来读取FIFO中最先收到的报文。这里的有效报文是指那些正确被接收的(直到EOF都没有错误)且通过了标识符过滤的报文。前面我们知道CAN的接收有2个FIFO,我们每个过滤器组都可以设置其关联的FIFO,通过CAN_FFA1R的设置,可以将滤波器组关联到FIFO0/FIFO1。
CAN接收流程为:FIFO空→收到有效报文→挂号_1(存入FIFO的一个邮箱,这个由硬件控制,我们不需要理会)→收到有效报文→挂号_2→收到有效报文→挂号_3→收到有效报文溢出。
这个流程里面,我们没有考虑从FIFO读出报文的情况,实际情况是:我们必须在FIFO溢出之前,读出至少1个报文,否则下个报文到来,将导致FIFO溢出,从而出现报文丢失。每读出1个报文,相应的挂号就减1,直到FIFO空。CAN接收流程如图38.1.2.13所示:

图38.1.2.13 FIFO接收报文流程图
FIFO接收到的报文数,我们可以通过查询CAN_RFxR的FMP寄存器来得到,只要FMP不为0,我们就可以从FIFO读出收到的报文。
接下来,我们简单看看STM32的CAN位时间特性,STM32的CAN位时间特性和之前我们介绍的CAN协议中,稍有点区别。STM32把传播时间段和相位缓冲段1(STM32称之为时间段1)合并了,所以STM32的CAN一个位只有3段:同步段(SYNC_SEG)、时间段1(BS1)和时间段2(BS2)。STM32的BS1段可以设置为1~16个时间单元,刚好等于我们上面介绍的传播时间段和相位缓冲段1之和。STM32的CAN位时序如图38.1.2.14所示:

图38.1.2.14 STM32 CAN位时序
图中还给出了CAN波特率的计算公式,我们只需要知道BS1和BS2的设置,以及APB1的时钟频率(一般为36Mhz),就可以方便的计算出波特率。比如设置TS1=8、TS2=7和BRP=3,在APB1频率为36Mhz的条件下,即可得到CAN通信的波特率=36000/[(9+8+1)*4]=500Kbps。
38.1.3 CAN寄存器
接下来,我们介绍一下本章需要用到的一些比较重要的寄存器。
 CAN的主控制寄存器(CAN_MCR)
CAN的主控制寄存器(CAN_MCR)各位描述如图38.1.3.1所示:

图38.1.3.1 寄存器CAN_MCR各位描述
该寄存器负责管理CAN的工作模式。这里我们仅介绍INRQ位,该位用来控制初始化请求。在CAN初始化的时候,先要设置该位为1,然后进行初始化(尤其是CAN_BTR的设置,该寄存器必须在CAN正常工作之前设置),之后再设置该位为0,让CAN进入正常工作模式。
该寄存器的详细描述,请参考《STM32F10xxx参考手册_V10(中文版).pdf》的439页。
 CAN位时序寄存器(CAN_BTR)
CAN位时序寄存器各位描述如图38.1.3.2所示:

图38.1.3.2 寄存器CAN_BTR各位描述
CAN位时序寄存器用于设置分频、Tbs1、Tbs2以及Tsjw等非常重要的参数,直接决定了CAN的波特率。
另外该寄存器还可以设置CAN的测试模式,STM32提供了三种测试模式,环回模式、静默模式和环回静默模式。这里我们简单介绍下环回模式。在环回模式下,bxCAN把发送的报文当作接收的报文并保存(如果可以通过接收过滤器组)在接收FIFO的输出邮箱里。也就是环回模式是一个自发自收的模式,如图38.1.3.3所示:

图38.1.3.3 CAN回环模式
环回模式可用于自测试。为了避免外部的影响,在环回模式下CAN内核忽略确认错误(在数据/远程帧的确认位时刻,不检测是否有显性位)。在环回模式下,bxCAN在内部把Tx输出回馈到Rx输入上,而完全忽略CANRX引脚的实际状态。发送的报文可以在CANTX引脚上检测到。
 CAN发送邮箱标识符寄存器(CAN_TIxR)
CAN发送邮箱标识符寄存器(CAN_TIxR)(x=0~3),该寄存器各位描述如图38.1.3.4所示:

图38.1.3.4 寄存器CAN_TIxR各位描述
该寄存器主要用来设置标识符(包括扩展标识符),另外还可以设置帧类型,通过TXRQ(位0)值1,来请求邮箱发送。因为有3个发送邮箱,所以寄存器CAN_TIxR有3个。
 CAN发送邮箱数据长度和时间戳寄存器(CAN_TDTxR)
CAN发送邮箱数据长度和时间戳寄存器(CAN_TDTxR)(x=0~2),该寄存器我们本章仅用来设置数据长度,即最低4个位。低4位的描述如图38.1.3.5所示。

图38.1.3.5 寄存器CAN_TDTxR各位描述
 CAN发送邮箱低字节数据寄存器(CAN_TDLxR)
CAN发送邮箱低字节数据寄存器(CAN_TDLxR)(x=0~2),该寄存器各位描述如图38.1.3.6所示:

图38.1.3.6 寄存器CAN_TDLxR各位描述
该寄存器用来存储将要发送的数据,这里只能存储低4个字节,另外还有一个寄存器CAN_TDHxR,该寄存器用来存储高4个字节,这样总共就可以存储8个字节。CAN_TDHxR的各位描述同CAN_TDLxR类似,我们就不单独介绍了。
 CAN接收FIFO邮箱标识符寄存器(CAN_RIxR)
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类似,这里我们就不单独一一介绍了。详细介绍,请参考《STM32F10xxx参考手册_V10(中文版).pdf》22.9.3节(447页)。
 CAN过滤器模式寄存器(CAN_FM1R)
CAN过滤器模式寄存器(CAN_FM1R),该寄存器各位描述如图38.1.3.7所示:

图38.1.3.7 寄存器CAN_FM1R各位描述
该寄存器用于设置各过滤器组的工作模式,对28个过滤器组的工作模式,都可以通过该寄存器设置,不过该寄存器必须在过滤器处于初始化模式下(CAN_FMR的FINIT位=1),才可以进行设置。对STM32F103ZET6来说,只有[13:0]这14个位有效。
 CAN过滤器位宽寄存器(CAN_FS1R)
CAN过滤器位宽寄存器(CAN_FS1R),该寄存器各位描述如图38.1.3.8所示:

图38.1.3.8 寄存器CAN_FS1R各位描述
该寄存器用于设置各过滤器组的位宽,对28个过滤器组的位宽设置,都可以通过该寄存器实现。该寄存器也只能在过滤器处于初始化模式下进行设置。对STM32F103ZET6来说,同样只有[13:0]这14个位有效。
 CAN过滤器FIFO关联寄存器(CAN_FFA1R)
CAN过滤器FIFO关联寄存器(CAN_FFA1R),该寄存器各位描述如图38.1.3.9所示:

图38.1.3.9 寄存器CAN_FFA1R各位描述
该寄存器设置报文通过过滤器组之后,被存入的FIFO,如果对应位为0,则存放到FIFO0;如果为1,则存放到FIFO1。该寄存器也只能在过滤器处于初始化模式下配置。
 CAN过滤器激活寄存器(CAN_FA1R)
CAN过滤器激活寄存器(CAN_FA1R),该寄存器各位对应过滤器组和前面的几个寄存器类似,这里就不列出了,把对应位置1,即开启对应的过滤器组;置0则关闭该过滤器组。
 CAN的过滤器组i的寄存器x(CAN_FiRx)
CAN的过滤器组i的寄存器x(CAN_FiRx)(互联产品中i=027,其它产品中i=013;x=1/2)。该寄存器各位描述如图38.1.3.10所示:

图38.1.3.10 寄存器CAN_FiRx各位描述
每个过滤器组的CAN_FiRx都由2个32位寄存器构成,即:CAN_FiR1和CAN_FiR2。根据过滤器位宽和模式的不同设置,这两个寄存器的功能也不尽相同。关于过滤器的映射,功能描述和屏蔽寄存器的关联,请参见前面的图38.1.2.10(过滤器组位宽模式设置)的说明。
关于CAN的介绍,就到此结束了。
38.2 硬件设计

  1. 例程功能
    通过KEY_UP按键(即WK_UP按键)选择CAN的工作模式(正常模式/环回模式),然后通过KEY0控制数据发送,接着查询是否有数据接收到,假如接收到数据,就将接收到的数据显示在LCD模块上。如果是环回模式,我们不需要2个开发板。如果是正常模式,我们就需要2个战舰开发板,并且将他们的CAN接口对接起来,然后一个开发板发送数据,另外一个开发板将接收到的数据显示在LCD模块上。
    2.硬件资源
    1) LED灯
    LED0–PB5
    2) 独立按键
    KEY0–PE4
    KEY_UP–PA0
    3) 正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    4) STM32自带CAN控制器
    5) CAN收发芯片TJA1050/SIT1050T
  2. 原理图
    STM32有CAN的控制器,但要实现CAN通讯的差分电平,我们还需要借助外围电路来实现,根据我们需要实现的程序功能,我们设计电路原理如下:

图38.2.2 CAN连接原理设计
从上图可以看出:STM32F1的CAN通过P9的设置,连接到TJA1050/SIT1050T收发芯片,然后通过接线端子(CAN)同外部的CAN总线连接。图中可以看出,在战舰STM32F103开发板上面是带有120Ω的终端电阻的,如果我们的开发板不是作为CAN的终端的话,需要把这个电阻去掉,以免影响通信。另外,需要注意:CAN和USB共用了PA11和PA12,所以他们不能同时使用。
这里还要注意,我们要设置好开发板上P9排针的连接,通过跳线帽将PA11和PA12分别连接到CAN_RX和CAN_TX上面,如图38.2.2所示。

图38.2.2 CAN实验需要跳线连接的位置
最后,我们用2根导线将两个开发板CAN端子的CAN_L和CAN_L,CAN_H和CAN_H连接起来。这里注意不要接反了(CAN_L接CAN_H),接反了会导致通讯异常!!
38.3 程序设计
38.3.1 CAN的HAL库驱动
CAN在HAL库中的驱动代码在stm32f1xx_hal_can.c文件(及其头文件)中。

  1. HAL_CAN_Init函数
    要使用一个外设首先要对它进行初始化,所以先看CAN的初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_CAN_Init(CAN_HandleTypeDef *hcan);
    函数描述:
    用于CAN控制器的初始化。
    函数形参:
    形参1是CAN的控制句柄,结构体类型是CAN_HandleTypeDef,其定义如下:
typedef struct __CAN_HandleTypeDef
{
  CAN_TypeDef                 		*Instance;    	/* CAN控制寄存器基地址 */
  CAN_InitTypeDef             	Init;        	/* 初始化参数结构体 */
  __IO HAL_CAN_StateTypeDef   	State;        	/* CAN通讯状态 */
  __IO uint32_t               		ErrorCode;    	/* CAN通讯结果编码 */
} CAN_HandleTypeDef;

1)Instance:指向CAN寄存器基地址。可以根据F103的寄存器的偏移量定义找到各个配置CAN的寄存器并对其进行操作。
2)Init:can初始化结构体,用于配置CAN的工作模式等等。它的定义也在stm32f1xx_hal_can.h中有列出。

typedef struct
{
  uint32_t Prescaler;      	/* 分频值,可以配置为1~1024间的任意整数 */
  uint32_t Mode;           	/* can操作模式,有效值参考CAN_operating_mode的描述 */
  uint32_t SyncJumpWidth;	/* CAN硬件的最大超时时间 */
  uint32_t TimeSeg1;     	/* CAN_time_quantum_in_bit_segment_1 */
  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;
调用CAN的初始化函数时,我们主要也是对这个结构体赋值,配置CAN的工作模式。

3)State:CAN操作状态,主要用于HAL库中的函数。
4)ErrorCode:CAN错误操作信息。CAN定义了多个错误返回值,可以方便查找通讯异常的可能原因,可以看到CAN总线的容错能力要大于串口。
函数返回值:
HAL_StatusTypeDef枚举类型的值,有4个,分别是HAL_OK表示成功,HAL_ERROR表示错误,HAL_BUSY表示忙碌,HAL_TIMEOUT为超时。
调用初始化函数之后,同样我们需要重定义HAL_CAN_MspInit来初始化跟底层硬件相关的配置,我们后面编写初始化函数时用到。
2. HAL_CAN_ConfigFilter函数
CAN的接收过滤器是属于硬件,可以根据软件的设置,在接收报文的时候,可以过滤出符合过滤器配置条件的报文ID,大大节省了CPU的开销,过滤器配置函数定义如下:
HAL_StatusTypeDef HAL_CAN_ConfigFilter(CAN_HandleTypeDef *hcan, CAN_FilterTypeDef *sFilterConfig)
函数描述:
用于配置CAN的接收过滤器。
函数形参:
形参1是CAN的控制句柄指针,初始化函数已经介绍过它的结构了,这里不重复了。
形参2是过滤器的结构体,这个是根据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;
	我们通过配置过滤器,通过过滤器组的报文,即可从关联的FIFO的输出邮箱中获取。

函数返回值:
我们只关注HAL_OK的情况。
3. HAL_CAN_Start函数
使能CAN控制器以接入总线进行数据收发处理。
HAL_StatusTypeDef HAL_CAN_Start(CAN_HandleTypeDef *hcan)
函数描述:
按需要配置完CAN总线后,使能CAN控制器以接入总线进行数据收发处理。
函数形参:
形参1是CAN的控制句柄指针,初始化函数已经介绍过它的结构了,这里不重复了。
函数返回值:
我们只关注HAL_OK的情况。
4. HAL_CAN_ActivateNotification函数
使能CAN的各种中断。
HAL_StatusTypeDef HAL_CAN_ActivateNotification(CAN_HandleTypeDef *hcan, uint32_t ActiveITs)
函数描述:
CAN定义了多种传输中断以满足多种需求,我们只需要在ActiveITs中填入相关中断即可,中断源可以在CAN_IER寄存器中找到。
函数形参:
形参1是CAN的控制句柄指针,初始化函数已经介绍过它的结构了,这里不重复了。
函数返回值:
我们只关注HAL_OK的情况。
5. HAL_CAN_AddTxMessage函数
发送报文函数。
HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan,
CAN_TxHeaderTypeDef *pHeader, uint8_t aData[], uint32_t *pTxMailbox)
函数描述:
该函数用于向发送邮箱添加发送报文,并激活发送请求
函数形参:
形参1是CAN的控制句柄指针,初始化函数已经介绍过它的结构了,这里不重复了。
形参2是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~8byte */
  FunctionalState TransmitGlobalTime;   /* 时间戳是否在开始时捕获 */

} CAN_TxHeaderTypeDef;
这里需要注意的是:当标识符选择位IDE为CAN_ID_STD时,表示本报文是标准帧,使用StdId成员存储报文ID;当它的值为CAN_ID_EXT时,表示本报文是扩展帧,使用ExtId成员存储报文ID。其他成员可以对照发送邮箱寄存器相关位进行理解。

形参3是报文的内容。
形参4是发送邮箱编号,可选三个发送邮箱之一。
6. HAL_CAN_GetRxMessage函数
接收消息函数。
HAL_StatusTypeDef HAL_CAN_GetRxMessage(CAN_HandleTypeDef *hcan, uint32_t RxFifo, CAN_RxHeaderTypeDef *pHeader, uint8_t aData[])
函数描述:
该函数可从接收FIFO里面的输出邮箱获取到消息报文。
函数形参:
形参1是CAN的控制句柄指针,初始化函数已经介绍过它的结构了,这里不重复了。
形参2是接收FIFO,具体是FIFO0/1 ,得看过滤器组关联的FIFO。
形参3是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~8byte */
  uint32_t Timestamp;   			/* 在帧接收开始时开始捕获的时间戳 */
  uint32_t FilterMatchIndex;	/* 过滤器匹配序号 */
} CAN_RxHeaderTypeDef;
在发送结构体中,同样的,也是通过IDE位确认该消息报文的标识符类型,该结构体不同于发送结构体还有一个过滤器匹配序号成员,可以查看到是此报文是通过哪里过滤器到达接收FIFO。其他成员可以对照发送邮箱寄存器相关位进行理解。

形参4是接收报文的内容。
CAN的初始化配置步骤
1)CAN参数初始化(工作模式、波特率等)
HAL库通过调用CAN初始化函数HAL_CAN_Init完成对CAN参数初始化,详见例程源码。
注意:该函数会调用:HAL_CAN_MspInit函数来完成对CAN底层的初始化,包括:CAN以及GPIO时钟使能、GPIO模式设置、中断设置等。
2)开启CAN和对应管脚时钟,配置CAN_TX和CAN_RX的复用功能输出
首先开启CAN的时钟,然后配置CAN相关引脚为复用功能(对应的引脚可查看中文参考手册P117)。本实验中CAN_TX对应的是PA12,CAN_RX对应的是PA11。他们的时钟开启方法如下:

__HAL_RCC_CAN1_CLK_ENABLE();            /* 使能CAN1 */
__HAL_RCC_GPIOA_CLK_ENABLE();           /* 开启GPIOA时钟 */

IO口复用功能是通过函数HAL_GPIO_Init来配置的。
3)设置过滤器
HAL库通过调用HAL_CAN_ConfigFilter完成CAN的过滤器相关参数初始化。
4)CAN数据接收和发送
通过调用HAL_CAN_AddTxMessage函数进行发送消息。
通过调用HAL_CAN_GetRxMessage函数进行接收数据。
至此,CAN就可以开始正常工作了。如果用到中断,就还需要进行中断相关的配置。本实验也提供CAN接收中断,详看例程源码,这里就不作介绍了。
38.3.2 程序流程图

图38.3.2.1 CAN通讯实验程序流程图
38.3.3 程序解析
我们要使用LED、LCD、按键这些功能,直接复制上一个485实验的代码,把rs485的代码从工程中移除,并在Drivers/BSP目录下新建一个《CAN》文件夹,与之前一样,新建can.c/can.h文件并把它们加入到工程中。

  1. can.c函数
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。CAN驱动相关源码包括两个文件:can.c和can.h。
    我们根据《STM32F10xxx参考手册_V10(中文版).pdf》第22章的内容,我们利用前面介绍的HAL库函数来配置CAN的接收时钟及模式等参数,配置过滤器以使能硬件自动过滤功能,最后使能CAN以开始CAN控制器的工作,编写CAN初始化函数。
/**
 * @brief    	CAN初始化
 * @param       	tsjw  	: 重新同步跳跃时间单元.范围: 1~3;
 * @param       	tbs2    	: 时间段2的时间单元.范围: 1~8;
 * @param       	tbs1    	: 时间段1的时间单元.范围: 1~16;
 * @param       	brp     	: 波特率分频器.范围: 1~1024;
 * @note      	以上4个参数, 在函数内部会减1, 所以, 任何一个参数都不能等于0
 *              	CAN挂在APB1上面, 其输入时钟频率为 Fpclk1 = PCLK1 = 36Mhz
 *              	tq     = brp * tpclk1;
 *              	波特率 = Fpclk1 / ((tbs1 + tbs2 + 1) * brp);
 *              	我们设置 can_init(1, 8, 9, 4, 1), 则CAN波特率为:
 *              	36M / ((8 + 9 + 1) * 4) = 500Kbps
 * @param       	mode    : CAN_MODE_NORMAL,  正常模式;
                          	  CAN_MODE_LOOPBACK,回环模式;
 * @retval      	0,  初始化成功; 其他, 初始化失败;
 */
uint8_t can_init(uint32_t tsjw, uint32_t tbs2, uint32_t tbs1, uint16_t brp, uint32_t mode)
{
  g_canx_handler.Instance = CAN1;
  g_canx_handler.Init.Prescaler = brp;	/* 分频系数(Fdiv)为brp+1 */
g_canx_handler.Init.Mode = mode;		/* 模式设置 */
/* 重新同步跳跃宽度(Tsjw)为tsjw+1个时间单位 CAN_SJW_1TQ~CAN_SJW_4TQ */
g_canx_handler.Init.SyncJumpWidth = tsjw; 
  g_canx_handler.Init.TimeSeg1 = tbs1;	/* tbs1范围CAN_BS1_1TQ~CAN_BS1_16TQ */
  g_canx_handler.Init.TimeSeg2 = tbs2;	/* tbs2范围CAN_BS2_1TQ~CAN_BS2_8TQ */
  g_canx_handler.Init.TimeTriggeredMode = DISABLE;    	/* 非时间触发通信模式 */
  g_canx_handler.Init.AutoBusOff = DISABLE;          	/* 软件自动离线管理 */  g_canx_handler.Init.AutoWakeUp = DISABLE;           	/* 通过软件唤醒睡眠模式 */
  g_canx_handler.Init.AutoRetransmission = ENABLE;   	/* 禁止报文自动传送 */
  g_canx_handler.Init.ReceiveFifoLocked = DISABLE;   	/* 报文不锁定,新的覆盖旧的 */
  g_canx_handler.Init.TransmitFifoPriority = DISABLE;	/* 优先级由报文标识符决定 */

  if (HAL_CAN_Init(&g_canx_handler) != HAL_OK)
  {
    return 1;
  }

#if CAN_RX0_INT_ENABLE
  /* 使用中断接收,FIFO0消息挂号中断允许 */
  __HAL_CAN_ENABLE_IT(&g_canx_handler, CAN_IT_RX_FIFO0_MSG_PENDING); 
  HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);          	/* 使能CAN中断 */
  HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 1, 0); 	/* 抢占优先级1,子优先级0 */
#endif

  CAN_FilterTypeDef sFilterConfig; 		
  /*配置CAN过滤器*/
  sFilterConfig.FilterBank = 0;                          	/* 过滤器0 */
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
  sFilterConfig.FilterIdHigh = 0x0000;                 	/* 32位ID */
  sFilterConfig.FilterIdLow = 0x0000;
  sFilterConfig.FilterMaskIdHigh = 0x0000;            	/* 32位MASK */
  sFilterConfig.FilterMaskIdLow = 0x0000;
  sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;	/* 过滤器0关联到FIFO0 */
  sFilterConfig.FilterActivation = ENABLE;             	/* 激活滤波器0 */
  sFilterConfig.SlaveStartFilterBank = 14;

  if (HAL_CAN_ConfigFilter(&g_canx_handler, &sFilterConfig) != HAL_OK)
  { /* 过滤器配置 */
    return 2;
  }
  
  if (HAL_CAN_Start(&g_canx_handler) != HAL_OK)
  { /* 启动CAN外围设备 */
    return 3;
  }

  return 0;
}
调用HAL_CAN_Init后会调用HAL_CAN_MspInit,我们重定义这个函数,在函数中初始化我们用于控制CAN的收发引脚:
void HAL_CAN_MspInit(CAN_HandleTypeDef *hcan)
{
  if (CAN1 == hcan->Instance)
  {
    CAN_RX_GPIO_CLK_ENABLE();       	/* CAN_RX脚时钟使能 */
    CAN_TX_GPIO_CLK_ENABLE();       	/* CAN_TX脚时钟使能 */
    __HAL_RCC_CAN1_CLK_ENABLE();    	/* 使能CAN1时钟 */

    GPIO_InitTypeDef gpio_initure;

    gpio_initure.Pin = CAN_TX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_PP;
    gpio_initure.Pull = GPIO_PULLUP;
    gpio_initure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(CAN_TX_GPIO_PORT, &gpio_initure); /* CAN_TX设置成复用推挽输出 */

    gpio_initure.Pin = CAN_RX_GPIO_PIN;
    gpio_initure.Mode = GPIO_MODE_AF_INPUT;
    HAL_GPIO_Init(CAN_RX_GPIO_PORT, &gpio_initure); /* CAN_RX设置成复用输入模式*/
  }
}

初始化函数就编写完成了,我们要设置它的工作波特率为500Kbps,设置工作模式为环回模式,参照各个参数的意义,我们最后用用以下的配置来完成初始化设置:
can_init(CAN_SJW_1TQ, CAN_BS2_8TQ, CAN_BS1_9TQ, 4, CAN_MODE_LOOPBACK);
要与其它的CAN节点设备通讯,我们需要编写CAN相关的收发函数。首先是发送函数,发送报文是有ID,所以我们在发送时需要设定ID,故需要设计一个形参为ID号,我们利用HAL库的发送函数封装一个更方便我们使用的函数,代码如下:

/**
 * @brief       	CAN 发送一组数据
 * @note      	发送格式固定为: 标准ID, 数据帧
 * @param       	id      : 标准ID(11位)
 * @retval      	发送状态 0, 成功; 1, 失败;
 */
uint8_t can_send_msg(uint32_t id, uint8_t *msg, uint8_t len)
{
  uint32_t TxMailbox = CAN_TX_MAILBOX0;
  g_canx_txheader.StdId = id;         	/* 标准标识符 */
  g_canx_txheader.ExtId = id;          	/* 扩展标识符(29位) */
  g_canx_txheader.IDE = CAN_ID_STD;  	/* 使用标准帧 */
  g_canx_txheader.RTR = CAN_RTR_DATA;	/* 数据帧 */
g_canx_txheader.DLC = len;

if (HAL_CAN_AddTxMessage(&g_canx_handler, &g_canx_txheader, 
msg, &TxMailbox) != HAL_OK) /* 发送消息 */
  {
    return 1;
}

/* 等待发送完成,所有邮箱为空(3个邮箱) */
while (HAL_CAN_GetTxMailboxesFreeLevel(&g_canx_handler) != 3); 

  return 0;
}

在CAN初始化时,我们对于过滤器的配置是不过滤任何报文ID,也就是说可以接收全部报文。但是我们可以编写接收函数时,使用软件的方式过滤报文ID,通过形参来跟接收到的报文ID进行匹配。接收函数代码具体如下:

/**
 * @brief       	CAN 接收数据查询
 * @note      	接收数据格式固定为: 标准ID, 数据帧
 * @param       	id      : 要查询的 标准ID(11位)
 * @param       	buf     : 数据缓存区
 * @retval     	接收结果
 * @arg       	0   , 无数据被接收到;
 * @arg       	其他, 接收的数据长度
 */
uint8_t can_receive_msg(uint32_t id, uint8_t *buf)
{
  if (HAL_CAN_GetRxFifoFillLevel(&g_canx_handler, CAN_RX_FIFO0) != 1)
  {
    return 0;
  }

if (HAL_CAN_GetRxMessage(&g_canx_handler, CAN_RX_FIFO0, &g_canx_rxheader,
 buf) != HAL_OK)
  {
    return 0;
  }

/* 接收到的ID不对 / 不是标准帧 / 不是数据帧 */
  if (g_canx_rxheader.StdId!= id || g_canx_rxheader.IDE != CAN_ID_STD ||       g_canx_rxheader.RTR != CAN_RTR_DATA)
  {
    return 0;    
  }
  return g_canx_rxheader.DLC;
}

最后,我们可以把can_send_msg函数加到USMART接口中,就可以方便地用串口来调试CAN接口了。
2. main.c代码
在main.c里面编写如下代码:

int main(void)
{
    uint8_t key;
    uint8_t i = 0, t = 0;
    uint8_t cnt = 0;
    uint8_t canbuf[8];
    uint8_t rxlen = 0;
    uint8_t res;
    uint8_t mode = 1; /* CAN工作模式: 0,正常模式; 1,环回模式 */

    HAL_Init();                               	/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);  	/* 设置时钟, 72Mhz */
    delay_init(72);                            	/* 延时初始化 */
    usart_init(115200);                        	/* 串口初始化为115200 */
    usmart_dev.init(72);                     	/* 初始化USMART */
    led_init();                             		/* 初始化LED */
    lcd_init();                                	/* 初始化LCD */
    key_init();                                 	/* 初始化按键 */
/* CAN初始化, 环回模式, 波特率500Kbps */
can_init(CAN_SJW_1TQ, CAN_BS2_8TQ, CAN_BS1_9TQ, 4, CAN_MODE_LOOPBACK); 

    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30, 70, 200, 16, 16, "CAN TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "LoopBack Mode", RED);
    lcd_show_string(30, 130, 200, 16, 16, "KEY0:Send KEK_UP:Mode", RED); 
    lcd_show_string(30, 150, 200, 16, 16, "Count:", RED);      /* 显示当前计数值 */
    lcd_show_string(30, 170, 200, 16, 16, "Send Data:", RED);  /* 提示发送的数据 */
    lcd_show_string(30, 230, 200, 16, 16, "Receive Data:", RED);/*提示接收的数据*/

    while (1)
    {
        key = key_scan(0);
        if (key == KEY0_PRES) /* KEY0按下,发送一次数据 */
        {
            for (i = 0; i < 8; i++)
            {
                canbuf[i] = cnt + i; /* 填充发送缓冲区 */
                if (i < 4)
                {   /* 显示数据 */
                    lcd_show_xnum(30 + i * 32, 190, canbuf[i], 3, 16, 0X80, BLUE);
                }
                else
                {   /* 显示数据 */
                    lcd_show_xnum(30 + (i-4)*32,210, canbuf[i], 3, 16, 0X80, BLUE);
                }
            }

            res = can_send_msg(0X12, canbuf, 8); /* ID = 0X12, 发送8个字节 */
            if (res)
            {   /* 提示发送失败 */
                lcd_show_string(30 + 80, 170, 200, 16, 16, "Failed", BLUE); 
            }
            else
            {   /* 提示发送成功 */
                lcd_show_string(30 + 80, 170, 200, 16, 16, "OK    ", BLUE);
            }
        }
        else if (key == WKUP_PRES) /* WK_UP按下,改变CAN的工作模式 */
        {
            mode = !mode;
            if (mode == 0) /* 正常模式,需要2个开发板 */
            {/* CAN模式初始化, 正常模式, 波特率500Kbps */
              can_init(CAN_SJW_1TQ, CAN_BS2_8TQ, CAN_BS1_9TQ, 4, CAN_MODE_NORMAL);
              lcd_show_string(30, 110, 200, 16, 16, "Nnormal Mode ", RED);
            }
            else /* 回环模式,一个开发板就可以测试了. */
            {/* CAN模式初始化, 回环模式, 波特率500Kbps */
             can_init(CAN_SJW_1TQ, CAN_BS2_8TQ, CAN_BS1_9TQ,4,CAN_MODE_LOOPBACK);
             lcd_show_string(30, 110, 200, 16, 16, "LoopBack Mode", RED);
            }
        }

        rxlen = can_receive_msg(0X12, canbuf); /* CAN ID = 0X12, 接收数据查询 */
        if (rxlen) /* 接收到有数据 */
        {
            lcd_fill(30, 270, 130, 310, WHITE); /* 清除之前的显示 */
            for (i = 0; i < rxlen; i++)
            {
                if (i < 4)
                {/* 显示数据 */
                 lcd_show_xnum(30 + i * 32, 250, canbuf[i], 3, 16, 0X80, BLUE); 
                }
                else
                {/* 显示数据 */
                 lcd_show_xnum(30 + (i-4) * 32, 270, canbuf[i], 3, 16,0X80, BLUE); 
                }
            }
        }

        t++;
        delay_ms(10);
        if (t == 20)
        {
            LED0_TOGGLE(); /* 提示系统正在运行 */
            t = 0;
            cnt++;
            lcd_show_xnum(30 + 48, 150, cnt, 3, 16, 0X80, BLUE); /* 显示数据 */
        }
    }
}

main函数的执行过程与程序流程图是一致的,这里需要注意:在选择正常模式的情况下,要使两个开发板通信成功,必须保持一致的波特率。其它细节,参考光盘资料中的源码即可。
38.4 下载验证
在代码编译成功之后,我们通过下载代码到开发板上,得到如图38.4.1所示:

图38.4.1 程序运行效果图
伴随LED0的不停闪烁,提示程序在运行。默认设置的回环模式,按下KEY0就可以在LCD模块上面看到自发自收的数据(如上图所示),如果我们选择正常模式(KEY_UP按键切换),就必须连接两个开发板的CAN接口,然后就可以互发数据了。如图38.4.2和图38.4.3所示:

图38.4.2 CAN正常模式发送数据

图38.4.3 CAN正常模式接收数据
图38.4.2来自开发板A,发送了8个数据,图37.4.3来自开发板B,收到了来自开发板A的个数据。另外,利用USMART测试的部分,我们这里就不做介绍了,大家可自行验证下。

物联沃分享整理
物联沃-IOTWORD物联网 » 【正点原子STM32连载】第38章:CAN通讯实验(摘自【正点原子】STM32F103 战舰开发指南V1.2)

发表评论