一、CAN总线简介
1.1 CAN概述
CAN是Controller Area Network(控制区域网络) 的缩写,是ISO国际标准化的串行通信协议。由德国电气商博世公司在1986年率先提出。此后,CAN通过ISO11898及ISO11519进行了标准化。现在在欧洲已是汽车网络的标准协议。
CAN协议经过ISO标准化后有两个标准:ISO11898标准和ISO11519-2标准。其中ISO11898是针对通信速率为125Kbps~1Mbps的高速通信标准,总线最大长度*²达到40m/1Mbps,连接单元数最大支持到30;而ISO11519-2是针对通信速率为125Kbps以下的低速通信标准,总线最大长度*²达到1km/40kbps,连接单元数最大支持到20。
只有2个设备简单通信时,当成USART串口使用,多个设备同时通信时,遵循CAN协议,使用邮箱、识别符、过滤器功能:总线空闲时,所有单元都可发送消息,而两个以上的单元同时开始
发送消息时,根据标识符(ID,非地址)决定优先级。两个以上的单元同时开始发送消息时,对各消息ID 的每个位进行逐个仲裁比较。仲裁获胜(优先级最高)的单元可继续发送消息,仲裁失利的单元则立刻停止发送而进行接收工作。
1.2 CAN总线较其他串行通信优势
CAN总线相比起其他串行通信协议,具有以下优势:
【1】由于连接总线的单元,没有类似“地址”的信息,因此,在总线上添加单元时,已连接的其他单元的软硬件和应用层都不需要做改变。
【2】具有错误检测、错误通知和错误恢复功能:所有单元都可以检测错误(错误检测功能),检测出错误的单元会立即同时通知其他所有单元(错误通知功能),正在发送消息的
单元一旦检测出错误,会强制结束当前的发送。强制结束发送的单元会不断反复地重新
发送此消息直到成功发送为止(错误恢复功能)。
【3】故障封闭功能:CAN可以判断出错误的类型是总线上暂时的数据错误(如外部噪声等)
还是持续的数据错误(如单元内部故障、驱动器故障、断线等)。由此功能,当总线上发生持续数据错误时,可将引起此故障的单元从总线上隔离出去。
【4】连接节点多。CAN 总线是可同时连接多个单元的总线。可连接的单元总数理论上
是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。
1.3 CAN总线通信
CAN总线是基于相同波特率通信的,所以设备接入前要知道总线上的波特率是多少。在STM32CubeMX上,其波特率计算如下图所示:波特率=(pclk1/((1+8+7)*9)) = 36Mhz/16/9 = 250Kbits。
在数据传输方面,一次最多只能发送8个字节的数据,这是由CAN协议规定的。多于8个的需要第二次再发送,需要开发者自己实现连续多数据发送的函数来实现大于8字节数据量的应用。
同一条 CAN 总线的不同主机,不能同时发送相同 ID 的报文:如果使用 2 个及以上驱动器,连接在同一条 CAN 线上,必须设置为不同的编号,比如 0 组 1 号和 0 组 2 号,如果不设置为不同的编号,一旦让驱动器回传报文,报文 ID 则会相同,就会造成总线致命冲突,此时驱动器会进入硬件错误状态。
CAN接口兼容规范2.0A和2.0B(主动式),可以接收和发送11位标识符的标准帧,也可以接收和发送29位标识符的扩展帧。具有3个发送邮箱和2个接收FIFO(邮箱及FIFO可以看做是缓存区域),3级14个可调节滤波器。
”发送邮箱“是用于CAN总线数据发送的,总共有3个,每个邮箱只装一个报文,并且存在优先级关系。优先级越高表示其里面的数据会被优先发送。数据在发送前都会被送到优先级最高且空闲的发送邮箱,然后依次发送。如果所有邮箱都满了,会溢出标记为1,需要等待空邮箱。满邮箱按先后次序排队发送报文,发送完成后变成空邮箱。
FIFO:表面的意思是“先入先出”,是指有层级深度的接收邮箱。一般CAN有2个FIFO邮箱(FIFO0和FIFO1),每个FIFO有3层深度,3层深度是指每个邮箱可以接收3个报文,但读取时只能读到最先收到的报文报文处理完成后,再读取时则是下一个报文。
过滤器:可由硬件判断报文中的标识符,过滤掉标识符不匹配的报文(过滤掉不想要的ID,接收想要的ID)。CAN总线控制器通常提供了14个或28个过滤器组,每组2个32为寄存器。过滤器是由硬件实现的,只有与过滤器匹配的报文才需要软件处理。在接收报文时,数据先进入过滤器,与过滤器匹配的报文会被放入FIFO邮箱。过滤器根据下列优先级规则来确定:
1,位宽为32位的过滤器,优先级高于位宽为16位的过滤器
2,对于位宽相同的过滤器,标识符列表模式的优先级高于屏蔽位模式
3,位宽和模式都相同的过滤器,优先级由过滤器号决定,过滤器号小的优先级高
最终CAN总线通信如下图示意:
1.4 报文种类及格式
CAN报文包含以下几种,其中数据帧及遥控帧是重点常用格式:
【1】数据帧,用于发送单元向接收单元传送数据的帧。
【2】遥控帧或远程帧,用于接收单元向具有相同ID的发送单元请求数据的帧。
【3】错误帧,用于当检测出错误时向其它单元通知错误的帧。
【4】过载帧,用于接收单元通知其尚未做好接收准备的帧。
【5】帧间隔,用于将数据帧及遥控帧与前面的帧分离开来的帧。
每种类型的报文格式都不相同,数据帧和遥控帧格式如下:
数据帧和遥控帧有标准格式和扩展格式两种格式,类似于I2C协议里的7位和10位地址。CAN的数据帧和遥控帧的标识符(ID)的标准格式有11个位标识符,扩展格式有29个位标识符。设备可以用标识符(ID)判断数据是不是发给自己的。
CAN总线收发数据报文,根据标示符(也就是 ID)来决定优先级的。必须注意的是,在同一CAN网络中,所有单元必须设定成统一的通信速度,CAN设备可通过发送“遥控帧” 请求其他单元发送数据。CAN支持Normal 正常模式、Silent 静默模式、Loopback 环回模式测试、Silent_loopback 静默换回模式测试共四种模式,在CubeMX对应如下:
1.5 CAN应用
CAN具有很高的可靠性,广泛应用于:汽车电子、工业自动化、船舶、医疗设备、工业设备等方面。
二、CAN工程创建及配置
2.1 工程设计及原理框图
本文采用STM32L496VGTX+独立CAN 收发器(TJA1050)和STM32F103C8Tx开发板(已经集成了CAN 收发器-TJA1050)进行CAN通信。
STM32L496VGTX预留了扩展引脚PD0、PD1支持CAN1,然后通过扩展引脚接入TJA1050:
TJA1050原理框图,STM32L496VGTX扩展口PD0、PD1接下图的PA11、PA12。
STM32F103C8Tx开发板的CAN组件原理框图如下,PB8、PB9接MCU:
2.2 工程创建接配置
现以STM32L496VGTX为例创建CAN工程stm32L496VGT6_can,创建完成后,移植以前已经实现过的lpuart1通信功能、按键功能及LED灯功能,请参考:
cubeIDE开发, stm32调试信息串口通信输出显示_py_free的博客-CSDN博客_stm32串口显示
假设已经移植了串口调试功能并能在电脑上通过串口通信实现调试信息输出。
进入CAN配置阶段,双击.ioc打开CubeMX配置界面,设置CAN1功能开启,先在图形配置界面选择PD0、PD1为CAN_RX/TX模式,将自动开启CAN1功能,然后进入CAN页面配置参数,波特率设置为250Kbits,及本次CAN网络的统一波特率。
开启接收中断功能
完成后点击保存输出生成代码
三、代码设计
3.1 修改输出源文件can.c
在Core/src目录下,双击打开can.c文件,修改MX_CAN1_Init函数,增加过滤设置、CAN开启、中断支持设置等功能。
/* USER CODE BEGIN 0 */
//新增
#define CAN1_ID_H 0x0000 //32位基础ID设置(高16位)
#define CAN1_ID_L 0x0000 //32位基础ID设置(低16位)
#define CAN1_MASK_H 0x0000 //32位屏蔽MASK设置(高16位)
#define CAN1_MASK_L 0x0000 //32位屏蔽MASK设置(低16位)
/* USER CODE END 0 */
CAN_HandleTypeDef hcan1;
/* CAN1 init function */
void MX_CAN1_Init(void)
{
/* USER CODE BEGIN CAN1_Init 0 */
//新增
CAN_FilterTypeDef CAN1_sFilterConfig;
HAL_StatusTypeDef HAL_Status;
/* USER CODE END CAN1_Init 0 */
/* USER CODE BEGIN CAN1_Init 1 */
/* USER CODE END CAN1_Init 1 */
hcan1.Instance = CAN1;
hcan1.Init.Prescaler = 20;
hcan1.Init.Mode = CAN_MODE_NORMAL;
hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan1.Init.TimeSeg1 = CAN_BS1_8TQ;
hcan1.Init.TimeSeg2 = CAN_BS2_7TQ;
hcan1.Init.TimeTriggeredMode = DISABLE;
hcan1.Init.AutoBusOff = DISABLE;
hcan1.Init.AutoWakeUp = DISABLE;
hcan1.Init.AutoRetransmission = DISABLE;
hcan1.Init.ReceiveFifoLocked = DISABLE;
hcan1.Init.TransmitFifoPriority = DISABLE;
if (HAL_CAN_Init(&hcan1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN CAN1_Init 2 */
//新增
CAN1_sFilterConfig.FilterIdHigh = CAN1_ID_H;//32位基础ID设置(高16位)
CAN1_sFilterConfig.FilterIdLow = CAN1_ID_L;//32位基础ID设置(低16位)
CAN1_sFilterConfig.FilterMaskIdHigh = CAN1_MASK_H;//32位屏蔽MASK设置(高16位)
CAN1_sFilterConfig.FilterMaskIdLow = CAN1_MASK_L;//32位屏蔽MASK设置(低16位)
CAN1_sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO1;//接收到的报文放入FIFO1位置
CAN1_sFilterConfig.FilterBank = 0;//过滤器0
CAN1_sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;//设为IDLIST列表模式/IDMASK屏蔽模式
CAN1_sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;//过滤器位宽度
CAN1_sFilterConfig.FilterActivation = ENABLE;//ENABLE激活过滤器,DISABLE禁止过滤器
CAN1_sFilterConfig.SlaveStartFilterBank = 0;//过滤器组设置(单个CAN总线时无用)
HAL_Status = HAL_CAN_ConfigFilter(&hcan1, &CAN1_sFilterConfig);
if ( HAL_Status!= HAL_OK)//判断开启是否成功
{
/* Filter configuration Error */
Error_Handler();//开启CAN总线失败的处理程序
}
/* Start the CAN peripheral */
HAL_Status = HAL_CAN_Start(&hcan1);
if ( HAL_Status!= HAL_OK)//这个函数和下面的函数是cubemx没有给出的,需要手动添加
{
/* Start Error */
Error_Handler();//开启CAN总线失败的处理程序
}
/* Activate CAN RX notification */
//若不使用CAN中断,以下不需要
HAL_Status = HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO1_MSG_PENDING);
if ( HAL_Status!= HAL_OK)//选择fifo1中断
{
/* Notification Error */
Error_Handler();//开启CAN总线挂起中断失败的处理程序
}
/* USER CODE END CAN1_Init 2 */
}
3.2 自定义can应用驱动
在ICore目录创建文件夹can,并在该文件夹下创建can1.h和can1.c源文件。
can1.h
#ifndef CAN_CAN1_H_
#define CAN_CAN1_H_
#include "stm32l4xx_hal.h" //HAL库文件声明
extern CAN_HandleTypeDef hcan;//声明的HAL库结构体
#define CAN1_REC_LEN 256//定义CAN1最大接收字节数
extern uint8_t CAN1_RX_BUF[CAN1_REC_LEN];//接收缓冲,末字节为换行符
extern uint16_t CAN1_RX_STA;//接收状态标记
uint8_t CAN1_SendNormalData(CAN_HandleTypeDef* hcan,uint16_t ID,uint8_t *pData,uint16_t Len);//CAN发送函数
void CAN1_printf (char *fmt, ...);//CAN总线通信,使用CAN1,这是CAN专用的printf函数
#endif /* CAN_CAN1_H_ */
can1.c,该驱动通过CAN1_printf函数实现向CAN发送数据,通过重写stm32l4xx_hal_can.h/c文件中的HAL_CAN_RxFifo1MsgPendingCallback函数实现CAN数据接收。
#include "can1.h" //库文件声明
#include "main.h"
#include <string.h>//用于字符串处理的库
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
CAN_HandleTypeDef hcan;//声明的HAL库结构体
CAN_TxHeaderTypeDef TxMeg;//CAN发送设置相关结构体
CAN_RxHeaderTypeDef RxMeg;//CAN接收设置相关结构体
uint8_t CAN1_RX_BUF[CAN1_REC_LEN];//接收缓冲,最大CAN1_REC_LEN个字节.末字节为换行符
uint16_t CAN1_RX_STA;//接收状态标记
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan) //接收回调函数(函数名不可改)
{
uint8_t Data[8];//接收缓存数组
HAL_StatusTypeDef HAL_RetVal;//判断状态的枚举
HAL_RetVal=HAL_CAN_GetRxMessage(hcan,CAN_RX_FIFO1,&RxMeg,Data);//接收邮箱中的数据
if (HAL_OK==HAL_RetVal){//判断接收是否成功
//接收成功后的数据处理程序,写在此处。(数据在Data数组中)
//以下2行是采用简单的寄存器查寻方式处理接收数据,每次只接收1位。在实际项目中的复杂接收程序可自行编写。
CAN1_RX_BUF[0]=Data[0];//将接收到的数据放入缓存数组(因只用到1个数据,所以只存放在数据[0]位置)
CAN1_RX_STA++;//数据接收标志位加1
}
}
//CAN发送数据函数(参数:总线名,ID,数据数组,数量。返回值:0成功HAL_OK,1参数错误HAL_ERROR,2发送失败HAL_BUSY)
//示例:CAN1_SendNormalData(&hcan1,0,CAN_buffer,8);//CAN发送数据函数
uint8_t CAN1_SendNormalData(CAN_HandleTypeDef* hcan,uint16_t ID,uint8_t *pData,uint16_t Len)
{
HAL_StatusTypeDef HAL_RetVal;//判断状态的枚举
uint16_t SendTimes,SendCNT=0;
uint8_t FreeTxNum=0;
uint8_t FreeLevelCount=0;
uint32_t CAN_TX_BOX0;
TxMeg.StdId=ID;
TxMeg.IDE = CAN_ID_STD;//扩展帧标识(STD标准帧/EXT扩展帧)
TxMeg.RTR = CAN_RTR_DATA;//远程帧标识(DATA数据帧/REMOTE远程帧)
if(!hcan||!pData||!Len){
printf("\n\rCAN发送失败!\n\r"); //串口发送
return HAL_ERROR;//如果总线名、数据、数量任何一个为0则返回值为1
}
SendTimes=Len/8+(Len%8?1:0);
FreeTxNum=HAL_CAN_GetTxMailboxesFreeLevel(hcan);//得出空闲邮箱的数量
TxMeg.DLC=8;
while(SendTimes--){//循环判断分批发送是否结束
if(0==SendTimes){//如果分批发送结束
if(Len%8)TxMeg.DLC=Len%8;//则加入最后不足8个的数据内容
}
FreeLevelCount = 0;//防止死循环
while(0 == FreeTxNum&&FreeLevelCount<10){
FreeLevelCount++;
HAL_Delay(1);
FreeTxNum = HAL_CAN_GetTxMailboxesFreeLevel(hcan);
}
HAL_Delay(1);//延时防止速度过快导致的发送失败
//开始发送数据(参数:总线名,设置参数,数据,邮箱号)
HAL_RetVal=HAL_CAN_AddTxMessage(hcan,&TxMeg,pData+SendCNT,&CAN_TX_BOX0);
if(HAL_RetVal!=HAL_OK){
printf("\n\rCAN总线忙碌!\n\r"); //串口发送
return HAL_BUSY;//如果发送失败,则返回值为2
}
SendCNT+=8;
}
return HAL_OK;//如果发送成功结束,返回值为0
}
//CAN总线通信,使用CAN1,这是CAN专用的printf函数
//调用方法:CAN1_printf("123"); //向UART8发送字符123
void CAN1_printf (char *fmt, ...)
{
char buff[CAN1_REC_LEN+1]; //用于存放转换后的数据 [长度]
uint16_t i=0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buff, CAN1_REC_LEN+1, fmt, arg_ptr);//数据转换
i=strlen(buff);//得出数据长度
if(strlen(buff)>CAN1_REC_LEN)i=CAN1_REC_LEN;//如果长度大于最大值,则长度等于最大值(多出部分忽略)
CAN1_SendNormalData(&hcan,0x11,(uint8_t *)buff,i);//CAN发送数据函数(ID为0x11)
va_end(arg_ptr);
}
3.3 CAN调用实现
在main.c文件中,加入各驱动文件的头文件引用
/* USER CODE BEGIN Includes */
#include "../../ICore/key/key.h"
#include "../../ICore/led/led.h"
#include "../../ICore/print/print.h"
#include "../../ICore/usart/usart.h"
#include "../../ICore/can/can1.h"
/* USER CODE END Includes */
在main函数中,加入各驱动的初始化化实现,CAN的初始化以及生产代码自动调用
/* USER CODE BEGIN 2 */
ResetPrintInit(&hlpuart1);
HAL_UART_Receive_IT(&hlpuart1,(uint8_t *)&HLPUSART_NewData, 1); //再开启接收中断
HLPUSART_RX_STA = 0;
/* USER CODE END 2 */
在main函数循环体中,实现CAN调用
/* USER CODE BEGIN WHILE */
while (1)
{
if(HLPUSART_RX_STA&0xC000){//溢出或换行,重新开始
printf("%.*s\r\n",HLPUSART_RX_STA&0X0FFF, HLPUSART_RX_BUF);
HLPUSART_RX_STA=0;//接收错误,重新开始
HAL_Delay(100);//等待
}
if(CAN1_RX_STA!=0)//CAN判断中断接收标志位【处理从CAN外部设备接收的字符】
{
//CAN1_printf("%c",CAN1_RX_BUF[0]); //CAN总线发送
printf("CAN1 Receive:%c",CAN1_RX_BUF[0]); //lpuart1总线发送
CAN1_RX_STA=0;//清除标志位
}
if(KEY_1())//按下KEY1判断
{
CAN1_printf("A");//向CAN1发送字符A
}
if(KEY_2())//按下KEY2判断
{
CAN1_printf("B");//向CAN1发送字符B
}
/* USER CODE END WHILE */
四、编译及测试
4.1 编译及测试
编译工程,STM32L496VGx支持ST-Link,直接在CubeIDE配置运行及下载程序。
同样方法基于STM32F103C8Tx芯片创建工程、配置CAN及代码编译,其CAN配置如下
完成工程编译后,加载测试
4.2 测试
打开串口工具,连接STM32L496芯片的工程lpuart1串口,在STM32F103C8Tx开发板上按键KEY1、KEY2,查看串口输出情况。