14 CAN编程(一)

14 CAN编程

14.1 CAN介绍

14.1.1 CAN是什么?

​ CAN,全称为“Controller Area Network”,即控制器局域网,是国际上应用最广泛的现场总线之一。

最初,CAN 被设计作为汽车环境中的微控制器通讯,在车载各电子控制装置 ECU 之间交换信息,形成汽车

电子控制网络。比如:发动机管理系统、变速箱控制器、仪表装备、电子主干系统中,均嵌入 CAN 控制装

置。

​ 一个由 CAN 总线构成的单一网络中,理论上可以挂接无数个节点。实际应用中,节点数目受网络硬件

的电气特性所限制。例如,当使用 Philips P82C250 作为 CAN 收发器时,同一网络中允许挂接 110 个节点。

CAN 可提供高达 1Mbit/s 的数据传输速率,这使实时控制变得非常容易。另外,硬件的错误检定特性也增

强了 CAN 的抗电磁干扰能力。

14.1.2 CAN的起源

​ CAN 最初出现在 80 年代末的汽车工业中,由德国 Bosch 公司最先提出。当时,由于消费者对于汽车功

能的要求越来越多,而这些功能的实现大多是基于电子操作的,这就使得电子装置之间的通讯越来越复杂,

同时意味着需要更多的连接信号线。提出 CAN 总线的最初动机就是为了解决现代汽车中庞大的电子控制装

置之间的通讯,减少不断增加的信号线。于是,他们设计了一个单一的网络总线,所有的外围器件可以被

挂接在该总线上。1993 年,CAN 已成为国际标准 ISO11898(高速应用)和 ISO11519(低速应用)。

CAN 是一种多主方式的串行通讯总线,基本设计规范要求有高的位速率,高抗电磁干扰性,而且能够检

测出产生的任何错误。当信号传输距离达到 10Km 时,CAN 仍可提供高达 50Kbit/s 的数据传输速率。

由于 CAN 总线具有很高的实时性能,因此,CAN 已经在汽车工业、航空工业、工业控制、安全防护等领

域中得到了广泛应用。

14.1.3 CAN传输模型

​ CAN 通讯协议主要描述设备之间的信息传递方式。CAN 层的定义与开放系统互连模型(OSI)一致。每

一层与另一设备上相同的那一层通讯。实际的通讯发生在每一设备上相邻的两层,而设备只通过模型物理

层的物理介质互连。CAN 的规范定义了模型的最下面两层:数据链路层和物理层。下表中展示了 OSI 开放

式互连模型的各层。应用层协议可以由 CAN 用户定义成适合特别工业领域的任何方案。已在工业控制和制

造业领域得到广泛应用的标准是 DeviceNet,这是为 PLC 和智能传感器设计的。在汽车工业,许多制造商

都应用他们自己的标准。

表格 OSI开发系统互联模型
序号 层次 描述
7 应用层 最高层。用户、软件、网络终端等之间用来进行信息交换。
6 表示层 将两个应用不同数据格式的系统信息转化为能共同理解的格式
5 会话层 依靠低层的通信功能来进行数据的有效传递。
4 传输层 两通讯节点之间数据传输控制。操作如:数据重发,数据错误修复
3 网络层 规定了网络连接的建立、维持和拆除的协议。如:路由和寻址
2 数据链路层 规定了在介质上传输的数据位的排列和组织。如:数据校验和帧结构
1 物理层 规定通讯介质的物理特性。如:电气特性和信号交换的解释

​ 虽然CAN传输协议参考了OSI 七层模型,但是实际上CAN协议只定义了两层“物理层”和“数据链路层”,因此出现了各种不同的“应用层”协议,比如用在自动化技术的现场总线标准DeviceNet,用于工业控制的CanOpen,用于乘用车的诊断协议OBD、UDS(统一诊断服务,ISO14229),用于商用车的CAN总线协议SAEJ1939.

表格 CAN的
序号 层次 描述
7 应用层 主要定义CAN应用层。
2 数据链路层 数据链路层分为逻辑链接控制子层LLC和介质访问控制子层MAC。
MAC 子层是 CAN 协议的核心。它把接收到的报文提供给 LLC 子
层,并接收来自 LLC 子层的报文。 MAC 子层负责报文分帧、仲
裁、应答、错误检测和标定。MAC 子层也被称作故障界定的管理
实体监管 LLC 子层涉及报文滤波、过载通知、以及恢复管理。
LLC = Logical Link Control MAC = Medium Access Control
1 物理层 物理层,为物理编码子层PCS. 该层定义信号是如何实际地传输
的,因此涉及到位时间、位编码、同步。

14.1.4 CAN网络拓扑

​ CAN总线是一种分布式的控制总线。

​ CAN总线作为一种控制器局域网,和普通以太网一样,它的网络很多CAN节点构成。

其网络拓扑结构如下图所示:

​ CAN网络的每个节点非常简单,均由一个MCU(微控制器)、一个CAN控制器和一个CAN收发器构成,然后使用双绞线连接到CAN网络中。

14.1.5 CAN物理特性

​ CAN总线遵循国际标准ISO11898,如ISO11898-1,ISO11898-2,ISO11898-3,ISO11898-4标准。

序号 标准 描述
1 ISO11898-1 数据链路层和物理层信号
2 ISO11898-2 高速接入单元
3 ISO11898-3 低速容错接入单元
4 ISO11898-4 时间触发通讯
5 ISO11898-5 低功耗的接入单元
6 ISO11898-6 选择性唤醒的高速接入单元

CAN 能够使用多种物理介质,例如双绞线、光纤等。最常用的就是双绞线。

信号使用差分电压传送,两条信号线被称为“CAN_H”和“CAN_L”。

静态时CAN_H和CAN_L均是 2.5V 左右,此时状态表示为逻辑“1”,也可以叫做 “隐性”。

用 CAN_H 比 CAN_L 高表示逻辑“0”,称为“显形”,此时,通常电压值为:CAN_H = 3.5V 和 CAN_L

= 1.5V 。

目前实际常用的CAN收发器有如下几种型号:

序号 型号 描述
1 PCA82C250 高速 CAN 收发器
2 PCA82C251 高速 CAN 收发器
3 PCA82C252 容错 CAN 收发器
4 TJA1040 高速 CAN 收发器
5 TJA1041 高速 CAN 收发器
6 TJA1042 高速 CAN 收发器
7 TJA1043 高速 CAN 收发器
8 TJA1050 高速 CAN 收发器
9 TJA1053 容错 CAN 收发器
10 TJA1054 容错 CAN 收发器

目前实际常用的CAN控制器有如下几种型号:

序号 型号 描述
1 SJA1000 独立CAN控制器
2 MCU内部控制器 目前市面上如STM32系列,S32K系列,IMX6系列等等很多单片机均内部集成了CAN控制。

14.1.6 CAN报文帧

14.1.6.1 CAN报文格式

标准 CAN 的标志符长度是 11 位,而扩展格式 CAN 的标志符长度可达 29 位。

CAN 协议的 2.0A 版本 规定 CAN 控制器必须有一个 11 位的标志符。

同时,在 2.0B 版本中规定,CAN 控制器的标志符长度可以是 11 位或 29 位。

遵循 CAN2.0B 协议的 CAN 控制器可以发送和接收 11 位标识符的标准格式报文或 29 位标识符的扩展格式报文。

标准帧&扩展帧对比
帧格式 标准帧 扩展帧
规范 CAN2.0A CAN2.0B
CAN ID(标识符)长度 11 bits 29 bits
CAN ID(标识符)范围 0x000~0x7FF 0x00000000~0x1FFFFFFF

14.1.6.2 CAN报文帧类型

CAN报文类型又分如5种帧类型:

数据帧:主要用于发送方向接收方传输数据的帧;

遥控帧:主要用于接收方向具有相同ID的发送方请求数据的帧;

错误帧:主要用于当检测出错误时向其他节点通知错误的帧。

过载帧:主要用于接收方通知其他尚未做好接收准备的帧。

间隔帧:主要用于将数据帧及遥控帧与前一帧分隔开来的帧。

其中数据帧是使用最多的帧类型,这里重点介绍以下数据帧。

数据帧如下图所示:

由上图所示,数据帧包括:

(1)帧起始。表示数据帧开始的段。

(2)仲裁段。表示该帧优先级的段。

(3)控制段。表示数据的字节数及保留位的段。

(4)数据段。数据的内容,一帧可发送0~8个字节的数据。

(5)CRC段。检查帧的传输错误的段。

(6)ACK段。表示确认正常接收的段。

(7)帧结束。表示数据帧结束的段。

具体介绍可以查看”CAN2.0A”、”CAN2.0B”详细介绍。

我们主要关注我们编程所需要关注的几个段:

ID: CAN报文ID;

IDE: 为0是标准帧,为1是扩展帧;

RTR: 为0是数据帧,为1是远程帧;

DLC: CAN报文数据长度,范围0~8字节;

Data:数据,0~8个字节;

14.2 CAN编程框架创建

当前我们所学习的是应用编程,为了以后CAN编程框架的通用性和可移植性,我们创建一个抽象的CAN应用编程框架,此框架可以适用于单片机应用编程,也可以适用于linux应用编程。

因此,根据CAN总线编程的通用属性,我们抽象出如下属性:

属性 属性描述 说明
CAN端口号 描述CAN端口,如CAN1,CAN2,CAN3,与具体硬件外设有关。
CAN收发器配置 描述CAN收发器模式设置,收发器模式有Normal,Stanby,
Sleep,ListenOnly等模式; 本章节所使用的收发器是硬件默
认配置,因此不需要配置。
CAN控制器配置 描述CAN收发器配置,如CAN波特率配置,采样率设置,过
滤器设置等;
CAN中断配置 描述CAN中断接收函数配置
读取CAN报文 描述CAN读取报文实现
发送CAN报文 描述CAN发送报文实现

根据上面表格所描述的属性,创建CAN应用编程框架如下:

typedef struct _CAN_COMM_STRUCT
{
    /* CAN硬件名称 */
    char name[10];
    /* CAN端口号,裸机里为端口号;linux应用里作为socket套接口 */
    int  can_port;                                
    /* CAN控制器配置函数,返回端口号赋值给can_port */
    int  (*can_set_controller)( void );                  
    /* CAN接口中断创建,在linux中对应创建接收线程 */
    void (*can_set_interrput)( int can_port , pCanInterrupt callback );             
    /* CAN读取报文接口 */
    void (*can_read)( int can_port , CanRxMsg* recv_msg);   
    /* CAN发送报文接口*/
    void (*can_write)( int can_port , CanTxMsg send_msg);   
}CAN_COMM_STRUCT, *pCAN_COMM_STRUCT;

此框架可以用类比套用在单片机上,也可以使用在linux socketcan应用编程上。

14.3 STM32 CAN应用编程

本节主要使用14.2中的应用编程框架,在单片机上试验框架的可行性,以一个基本的接收和发送的案例来做讲解;

14.3.1 STM32 CAN接口电路

如下图所示,为本章STM32例程所使用的开发板STM32最小系统和CAN收发器接口电路。


14.3.2 STM32 CAN应用编程步骤

下面我们按照CAN通信的编程框架来一步一步实现基于STM32的CAN应用编程。

STM32 CAN应用编程,步骤如下:

14.3.2.1 准备STM32工程模版

请参见第14章节代码“01_stm32f407_can”例程;

所使用的开发环境为:MDK 5.24.

打开MDK工程后,如下图所示:


上图中目录CMSIS, STM32F407_LIB,main均为STM32运行的基础框架。

目录app_can为CAN应用编程所需要的文件。

14.3.2.2 编写CAN抽象框架的实现函数

(1)定义CAN端口号

见第14章节代码“01_stm32f407_can_addline”中“can_controller.h”文件。

主要根据STM32硬件的CAN有多路,依次定义为CAN_PORTCAN1, CAN_PORT_CAN2等,从“14.3.1 STM32 CAN接口电路”可知道,当前使用的CAN1.

25 /* CAN端口号定义*/
26 enum
27 {
28     CAN_PORT_NONE = 0,
29     CAN_PORT_CAN1,
30     CAN_PORT_CAN2,
31     CAN_PORT_MAX
32 };

(2)配置CAN控制器

配置CAN控制器有3个部分:GPIO(CAN_TX,CAN_RX管脚)配置,CAN波特率配置,CAN过滤器配置。

见第14章节代码“01_stm32f407_can_addline”中“can_controller.c”文件int CAN_Set_Controller( void )函数。

A.GPIO(CAN_TX,CAN_RX管脚)配置

配置GPIO代码如下:

96     /*************************************************************/
97     /*CAN相关GPIO配置,此处为:CAN_TX, CAN_RX*/
98
99     /*使能GPIO时钟*/
100     RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
101     /*初始化管脚配置*/
102     GPIO_InitStructure.GPIO_Pin     = GPIO_Pin_0 ;
103     GPIO_InitStructure.GPIO_Mode    = GPIO_Mode_AF;
104     GPIO_InitStructure.GPIO_Speed   = GPIO_Speed_50MHz;
105     GPIO_InitStructure.GPIO_OType   = GPIO_OType_PP;
106     GPIO_InitStructure.GPIO_PuPd    = GPIO_PuPd_UP;
107     GPIO_Init(GPIOD, &GPIO_InitStructure);
108
109     GPIO_InitStructure.GPIO_Pin     = GPIO_Pin_1;
110     GPIO_InitStructure.GPIO_Mode    = GPIO_Mode_AF;
111     GPIO_InitStructure.GPIO_Speed   = GPIO_Speed_50MHz;
112     GPIO_InitStructure.GPIO_OType   = GPIO_OType_PP;
113     GPIO_InitStructure.GPIO_PuPd    = GPIO_PuPd_UP;
114     GPIO_Init(GPIOD, &GPIO_InitStructure);
115     /*将GPIO设置为CAN复用模式*/
116     GPIO_PinAFConfig(GPIOD, GPIO_PinSource0, GPIO_AF_CAN1);
117     GPIO_PinAFConfig(GPIOD, GPIO_PinSource1, GPIO_AF_CAN1);

B.配置波特率,工作模式

按照如下代码,使能CAN外设,设置CAN工作模式为Normal,设置波特率为500kbps。

119     /*************************************************************/
120     /*CAN控制器相关配置,此处为波特率,采样率等*/
121
122     /* 使能CAN时钟 */
123     RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
124
125     /* 初始化CAN控制器工作模式*/
126     CAN_DeInit(CAN1);
127     CAN_StructInit(&CAN_InitStructure);
128     CAN_InitStructure.CAN_TTCM = DISABLE;
129     CAN_InitStructure.CAN_ABOM = DISABLE;
130     CAN_InitStructure.CAN_AWUM = DISABLE;
131     CAN_InitStructure.CAN_NART = DISABLE;
132     CAN_InitStructure.CAN_RFLM = DISABLE;
133     CAN_InitStructure.CAN_TXFP = DISABLE;
134     CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;//CAN工作模式
135
136     /* 初始化CAN波特率 */
137     CAN_Baud_Process(500,&CAN_InitStructure);
138     CAN_Init(CAN1, &CAN_InitStructure);

其中配置波特率的函数是一个自定义函数,这里可以不了解,只需要知道是配置波特率即可,如果需要使用本章代码,可以查看具体的源码工程。

C. 配置CAN过滤器

如下代码为配置过滤器:

141     /*************************************************************/
142     /* 初始化CAN过滤器 */
143     CAN_FilterInitStructure.CAN_FilterNumber = 0;                       /* CAN1滤波器号从0到13 */
144     CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;     /* 滤波屏蔽模式 */
145     CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
146     CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;
147     CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
148     CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;      /* 不屏蔽任何ID */
149     CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;           /* 不屏蔽任何ID */
150     CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0;
151
152     CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
153     CAN_FilterInit(&CAN_FilterInitStructure);
154
155     /*************************************************************/
156     /* 设置完CAN后,返回当前设置的CAN的端口号,此处主要类比linux socketcan中的套接口 */

此处我们设置过滤器不屏蔽任何报文ID,这里只是了解单片机下的一些过程。

(3)配置CAN接收中断

CAN总线支持发送中断和接收中断,此处仅仅使用了接收中断。

见第14章节代码“01_stm32f407_can_addline”中“can_controller.c”文件void CAN_Set_Interrupt(int can_port, pCanInterrupt callback)函数。

CAN中断配置代码如下:

163 /**********************************************************************
164 * 函数名称: void CAN_Set_Interrupt(int can_port,  pCanInterrupt callback)
165 * 功能描述: 使能CAN中断处理,并传入应用的的回调函数,回调函数主要处理应用层的功能
166 * 输入参数: can_port,端口号
167 *            callback: 中断具体处理应用功能的回调函数
168 * 输出参数: 无
169 * 返 回 值: 无
170 * 修改日期             版本号        修改人           修改内容
171 * -----------------------------------------------
172 * 2020/05/13         V1.0             bert            创建
173 ***********************************************************************/
174 void CAN_Set_Interrupt(int can_port,  pCanInterrupt callback)
175 {
176     NVIC_InitTypeDef NVIC_InitStructure;
177
178     /* 根据CAN端口号配置中断 */
179     switch( can_port )
180     {
181         case CAN_PORT_CAN1:
182         {
183             /* 初始化回调接口函数 */
184             if ( NULL != callback )
185             {
186                 g_pCanInterrupt = callback;
187             }
188
189             /* 使用CAN0_RX中断,在linux socket can中类似创建接收线程 */
190             NVIC_InitStructure.NVIC_IRQChannel = CAN1_RX0_IRQn;
191             NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
192             NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
193             NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
194             NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
195             NVIC_Init(&NVIC_InitStructure);
196             CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
197         }
198         break;
199
200         default:
201             break;
202
203     }
204     return ;
205 }

CAN接收中断函数如下:

275 /**********************************************************************
276 * 函数名称: void CAN1_RX0_IRQHandler(void)
277 * 功能描述: CAN接收中断函数
278 * 输入参数: 无
279 * 输出参数: 无
280 * 返 回 值: 无
281 * 修改日期             版本号        修改人           修改内容
282 * -----------------------------------------------
283 * 2020/05/13         V1.0             bert            创建
284 ***********************************************************************/
285 void CAN1_RX0_IRQHandler(void)
286 {
287     /* 如果回调函数存在,则执行回调函数 */
288     if( g_pCanInterrupt != NULL)
289     {
290         g_pCanInterrupt();
291     }
292
293     /* 清除挂起中断 */
294     CAN_ClearITPendingBit(CAN1,CAN_IT_FMP0);
295 }

此处CAN中断通过回调函数g_pCanInterrupt()函数将应用层需要的代码分层到应用层,此处为驱动部分通用接口。

(4)CAN报文读取函数

当CAN接收中断产生,通过CAN报文读取函数从FIFO中读取已经接收到的CAN报文。

见第14章节代码“01_stm32f407_can_addline”中“can_controller.c”文件void CAN_Read(int can_port, CanRxMsg* recv_msg)函数。

CAN报文读取函数如下:

208 /**********************************************************************
209 * 函数名称: void CAN_Read(int can_port, CanRxMsg* recv_msg)
210 * 功能描述: CAN读取接收寄存器,取出接收到的报文
211 * 输入参数: can_port,端口号
212 * 输出参数: recv_msg:接收报文
213 * 返 回 值: 无
214 * 修改日期             版本号        修改人           修改内容
215 * -----------------------------------------------
216 * 2020/05/13         V1.0             bert            创建
217 ***********************************************************************/
218 void CAN_Read(int can_port, CanRxMsg* recv_msg)
219 {
220     switch( can_port )
221     {
222         case CAN_PORT_CAN1:
223         {
224             /* 从FIFO中读取CAN报文 */
225             CAN_Receive(CAN1,CAN_FIFO0, recv_msg);
226         }
227         break;
228
229         default:
230             break;
231     }
232     return ;
233 }

(5)CAN报文发送函数

当需要发送CAN报文时,通过向CAN发送邮箱填充数据,启动发送报文。

见第14章节代码“01_stm32f407_can_addline”中“can_controller.c”文件void CAN_Write(int can_port, CanTxMsg send_msg)函数。

CAN报文读取函数如下:

235 /**********************************************************************
236 * 函数名称: void CAN_Write(int can_port, CanTxMsg send_msg)
237 * 功能描述: CAN报文发送接口,调用发送寄存器发送报文
238 * 输入参数: can_port,端口号
239 * 输出参数: send_msg:发送报文
240 * 返 回 值: 无
241 * 修改日期             版本号        修改人           修改内容
242 * -----------------------------------------------
243 * 2020/05/13         V1.0             bert            创建
244 ***********************************************************************/
245 void CAN_Write(int can_port, CanTxMsg send_msg)
246 {
247     unsigned char i;
248     uint8_t transmit_mailbox = 0;
249     CanTxMsg TxMessage;
250
251     switch( can_port )
252     {
253         case CAN_PORT_CAN1:
254         {
255             TxMessage.StdId = send_msg.StdId;     // 标准标识符为0x000~0x7FF
256             TxMessage.ExtId = 0x0000;             // 扩展标识符0x0000
257             TxMessage.IDE   = CAN_ID_STD;         // 使用标准标识符
258             TxMessage.RTR   = CAN_RTR_DATA;       // 设置为数据帧
259             TxMessage.DLC   = send_msg.DLC;       // 数据长度, can报文规定最大的数据长度为8字节
260
261             for(i=0; i<TxMessage.DLC; i++)
262             {
263                 TxMessage.Data[i] = send_msg.Data[i];
264             }
265             transmit_mailbox = CAN_Transmit(CAN1,&TxMessage);  /* 返回这个信息请求发送的邮箱号0,1,2或没有邮箱申请发送no_box */
266         }
267         break;
268
269         default:
270             break;
271     }
272     return ;
273 }

(6)CAN抽象结构体框架初始化

定义一个can1通信结构实例CAN_COMM_STRUCT can1_controller;

使用(1)~(5)步骤实现的函数,初始化can1_controller,构成与应用层关联的一个连接点。

298 /**********************************************************************
299 * 名称:     can1_controller
300 * 功能描述: CAN1结构体初始化
301 * 修改日期             版本号        修改人           修改内容
302 * -----------------------------------------------
303 * 2020/05/13         V1.0             bert            创建
304 ***********************************************************************/
305 CAN_COMM_STRUCT can1_controller = {
306     .name                   = "can0",
307     .can_port               = CAN_PORT_CAN1,
308     .can_set_controller     = CAN_Set_Controller,
309     .can_set_interrput      = CAN_Set_Interrupt,
310     .can_read               = CAN_Read,
311     .can_write              = CAN_Write,
312 };

14.3.2.3 编写CAN应用层代码

根据14.3.2.2 已经将具体的CAN硬件操作已经实现,并且已经抽象实例化了CAN编程框架。

但是我们现在还没关联到应用层,应用层并不知道调用哪个接口。

(1)CAN应用层注册实例

在应用层编写一个通用的实例化注册函数。

见第14章节代码“01_stm32f407_can_addline”中“app_can.c”文件int register_can_controller(const pCAN_COMM_STRUCT p_can_controller)函数。

代码实现如下:

62 /**********************************************************************
63 * 函数名称: int register_can_controller(const pCAN_COMM_STRUCT p_can_controller)
64 * 功能描述: 应用层进行CAN1结构体注册
65 * 输入参数: p_can_controller,CAN控制器抽象结构体
66 * 输出参数: 无
67 * 返 回 值: 无
68 * 修改日期             版本号        修改人           修改内容
69 * -----------------------------------------------
70 * 2020/05/13         V1.0             bert            创建
71 ***********************************************************************/
72 int register_can_controller(const pCAN_COMM_STRUCT p_can_controller)
73 {
74     /* 判断传入的p_can_controller为非空,目的是确认这个结构体是实体*/
75     if( p_can_controller != NULL )
76     {
77         /* 将传入的参数p_can_controller赋值给应用层结构体gCAN_COMM_STRUCT */
78
79         /*端口号,类比socketcan套接口*/
80         gCAN_COMM_STRUCT.can_port              = p_can_controller->can_port;
81         /*CAN控制器配置函数*/
82         gCAN_COMM_STRUCT.can_set_controller    = p_can_controller->can_set_controller;
83         /*CAN中断配置*/
84         gCAN_COMM_STRUCT.can_set_interrput     = p_can_controller->can_set_interrput;
85         /*CAN报文读函数*/
86         gCAN_COMM_STRUCT.can_read              = p_can_controller->can_read;
87         /*CAN报文发送函数*/
88         gCAN_COMM_STRUCT.can_write             = p_can_controller->can_write;
89         return 1;
90     }
91      return 0;
92 }

然后通过调用register_can_controller( &can1_controller );将实例can1_controller注册给应用的4 static CAN_COMM_STRUCT gCAN_COMM_STRUCT;

之后应用层只需要调用应用层自己的gCAN_COMM_STRUCT实例即可操作CAN通信功能。

315 /**********************************************************************
316 * 函数名称: void CAN1_contoller_add(void)
317 * 功能描述: CAN结构体注册接口,应用层在使用can1_controller前调用
318 * 输入参数: 无
319 * 输出参数: 无
320 * 返 回 值: 无
321 * 修改日期             版本号        修改人           修改内容
322 * -----------------------------------------------
323 * 2020/05/13         V1.0             bert            创建
324 ***********************************************************************/
325 void CAN1_contoller_add(void)
326 {
327     /*将can1_controller传递给应用层*/
328     register_can_controller( &can1_controller );
329 }

(2)CAN应用层初始化

CAN应用层初始化代码如下;

94 /**********************************************************************
95 * 函数名称: void app_can_init(void)
96 * 功能描述: CAN应用层初始化
97 * 输入参数: 无
98 * 输出参数: 无
99 * 返 回 值: 无
100 * 修改日期             版本号        修改人           修改内容
101 * -----------------------------------------------
102 * 2020/05/13         V1.0             bert            创建
103 ***********************************************************************/
104 void app_can_init(void)
105 {
106     /**
107     * 应用层进行CAN1结构体注册
108     */
109     CAN1_contoller_add();
110
111     /*
112     *调用can_set_controller进行CAN控制器配置,
113     *返回can_port,类比linux socketcan中的套接口,单片机例程中作为自定义CAN通道
114     */
115     gCAN_COMM_STRUCT.can_port = gCAN_COMM_STRUCT.can_set_controller();
116     /**
117     * 调用can_set_interrput配置CAN接收中断,类比socketcan中的接收线程
118     */
119     gCAN_COMM_STRUCT.can_set_interrput( gCAN_COMM_STRUCT.can_port, CAN_RX_IRQHandler_Callback );
120 }

(3)设计一个简单的周期发送报文功能

CAN周期发送报文的功能代码实现如下:

123 /**********************************************************************
124 * 函数名称: void app_can_tx_test(void)
125 * 功能描述: CAN应用层报文发送函数,用于测试周期发送报文
126 * 输入参数: 无
127 * 输出参数: 无
128 * 返 回 值: 无
129 * 修改日期             版本号        修改人           修改内容
130 * -----------------------------------------------
131 * 2020/05/13         V1.0             bert            创建
132 ***********************************************************************/
133 void app_can_tx_test(void)
134 {
135     // 以10ms为基准,运行CAN测试程序
136
137     unsigned char i=0;
138
139     /* 发送报文定义 */
140     CanTxMsg TxMessage;
141
142     /* 发送报文中用一个字节来作为计数器 */
143     static unsigned char tx_counter = 0;
144
145     /* 以10ms为基准,通过timer计数器设置该处理函数后面运行代码的周期为1秒钟*/
146     static unsigned int timer =0;
147     if(timer++>100)
148     {
149         timer = 0;
150     }
151     else
152     {
153         return ;
154     }
155
156     /* 发送报文报文数据填充,此报文周期是1秒 */
157     TxMessage.StdId = TX_CAN_ID;          /* 标准标识符为0x000~0x7FF */
158     TxMessage.ExtId = 0x0000;             /* 扩展标识符0x0000 */
159     TxMessage.IDE   = CAN_ID_STD;         /* 使用标准标识符 */
160     TxMessage.RTR   = CAN_RTR_DATA;       /* 设置为数据帧  */
161     TxMessage.DLC   = 8;                  /* 数据长度, can报文规定最大的数据长度为8字节 */
162
163     /* 填充数据,此处可以根据实际应用填充 */
164     TxMessage.Data[0] = tx_counter++;       /* 用来识别报文发送计数器 */
165     for(i=1; i<TxMessage.DLC; i++)
166     {
167         TxMessage.Data[i] = i;
168     }
169
170     /*  调用can_write发送CAN报文 */
171     gCAN_COMM_STRUCT.can_write(gCAN_COMM_STRUCT.can_port, TxMessage);
172
173 }

(4)设计一个简单的接收报文功能

220 /**********************************************************************
221 * 函数名称: void CAN_RX_IRQHandler_Callback(void)
222 * 功能描述: CAN1接收中断函数;在linux中可以类比用线程,或定时器去读CAN数据
223 * 输入参数: 无
224 * 输出参数: 无
225 * 返 回 值: 无
226 * 修改日期             版本号        修改人           修改内容
227 * -----------------------------------------------
228 * 2020/05/13         V1.0             bert            创建
229 ***********************************************************************/
230 void CAN_RX_IRQHandler_Callback(void)
231 {
232     /* 接收报文定义 */
233     CanRxMsg RxMessage;
234
235     /* 接收报文清零 */
236     memset( &RxMessage, 0, sizeof(CanRxMsg) );
237
238     /* 通过can_read接口读取寄存器已经接收到的报文 */
239     gCAN_COMM_STRUCT.can_read(gCAN_COMM_STRUCT.can_port, &RxMessage);
240
241     /* 将读取到的CAN报文存拷贝到全局报文结构体g_CAN1_Rx_Message */
242     memcpy(&g_CAN1_Rx_Message, &RxMessage, sizeof( CanRxMsg ) );
243
244     /* 设置当前接收完成标志,判断当前接收报文ID为RX_CAN_ID,则设置g_CAN1_Rx_Flag=1*/
245     if( g_CAN1_Rx_Message.StdId == RX_CAN_ID )
246     {
247         g_CAN1_Rx_Flag = 1;
248     }
249 }
176 /**********************************************************************
177 * 函数名称: void app_can_rx_test(void)
178 * 功能描述: CAN应用层接收报文处理函数,用于处理中断函数中接收的报文
179 * 输入参数: 无
180 * 输出参数: 无
181 * 返 回 值: 无
182 * 修改日期             版本号        修改人           修改内容
183 * -----------------------------------------------
184 * 2020/05/13         V1.0             bert            创建
185 ***********************************************************************/
186 void app_can_rx_test(void)
187 {
188     unsigned char i=0;
189
190     /* 发送报文定义 */
191     CanTxMsg TxMessage;
192
193     /* 发送报文中用一个字节来作为计数器 */
194     static unsigned char rx_counter = 0;
195
196
197     if( g_CAN1_Rx_Flag == 1)
198     {
199         g_CAN1_Rx_Flag = 0;
200
201         /* 发送报文报文数据填充,此报文周期是1秒 */
202         TxMessage.StdId = RX_TO_TX_CAN_ID;    /* 标准标识符为0x000~0x7FF */
203         TxMessage.ExtId = 0x0000;             /* 扩展标识符0x0000 */
204         TxMessage.IDE   = CAN_ID_STD;         /* 使用标准标识符 */
205         TxMessage.RTR   = CAN_RTR_DATA;       /* 设置为数据帧  */
206         TxMessage.DLC   = 8;                  /* 数据长度, can报文规定最大的数据长度为8字节 */
207
208         /* 填充数据,此处可以根据实际应用填充 */
209         TxMessage.Data[0] = rx_counter++;      /* 用来识别报文发送计数器 */
210         for(i=1; i<TxMessage.DLC; i++)
211         {
212             TxMessage.Data[i] = g_CAN1_Rx_Message.Data[i];
213         }
214
215         /*  调用can_write发送CAN报文 */
216         gCAN_COMM_STRUCT.can_write(gCAN_COMM_STRUCT.can_port, TxMessage);
217     }
218 }

14.3.2.4 STM32 CAN案例测试

在前面几个章节将代码编写完成之后,我们做个测试;

测试工具使用的是:英特蓓斯的Valuecan3(CAN协议盒),Vehicle Vspy3(电脑端软件)。

也可以在淘宝上购买便宜的USB转CAN的工具即可。

测试步骤如下:

Step1:将已经完成的STM32 CAN测试程序下载到实际开发板上;

Step2:通过CAN测试工具Vehicle Vspy3发送报文ID为0X201的报文;

Step3:观察CAN测试软件显示如下:

报文ID为0x101的报文是按照1秒周期进行发送,如图14.3.2.4-1。

报文ID为0x201的报文是Vehicle Spy3按照周期500ms发送给STM32开发板,如图14.3.2.4-1

报文ID为0x301的报文是在接收到报文ID为0x201的报文后,然后转发出报文ID为0x301的报文,如图14.3.2.4-2。