CAN通信知识梳理及在Stm32上的应用(HAL库)
文章目录
一.CAN通信简介
CAN是控制器局域网络(Controller Area Network)的简称,1986年被德国研发和生产汽车电子产品著称的BOSCH公司
所开发,并最终成为国际标准(ISO11898),是国际上应用最广泛的现场总线之一。 在北美和西欧,CAN总线协议已经成为汽车计算机控制系统和嵌入式工业控制局域网的标准总线。
CAN是国际标准化的串行通信协议
,采用数据块编码的方式,数据块根据帧的类型,能够让挂载在总线上的不同节点接收到相同的数据,再根据每个节点的配置对信息进行选择性处理(接收or丢弃)。
二.硬件连接
1. 一个节点一般包含3个部分:微控制器
,CAN控制器
和CAN收发器
。典型的组合有:51单片机+SJA1000+PCA82C250(5V)。
2. 有的微控制器内部已经包含了CAN控制器,只需要外加CAN收发器,收发器一般都是8个引脚的芯片。 比如STM32:STM32+SN65HVD230(3.3V)。
3. 所有节点通过两条线连接起来。两条线分别称为CAN_H
和CAN_L
。如果要求不高,一般用带屏蔽的双绞线就可以了。
4. 网络的两端必须有120Ω的终端电阻
。所以在设计线路板的时候都要有一个120欧的电阻。通过跳线或者拨码开关选择是否使用这个电阻。为什么是120Ω,因为电缆的特性阻抗为120Ω,为了模拟无限远的传输线
三.CAN总线上的电平信号
-
CANBUS
上的总线电平称为隐性电平
和显性电平
:名称 性质 逻辑 CAN_H CAN_L 两条线上的电压差 Recessive 隐性 逻辑1 2.5V 2.5V 0V Dominant 显性 逻辑0 3.5V 1.5V 2V 注: CAN标准有两个,即IOS11898和IOS11519,两者差分电平特性不同。这里讲的是IOS11898
-
如果CAN控制器发送逻辑1时,CAN收发器使CAN_H和CAN_L都为2.5V,这时,两条线上的电压差为0V。总线上称为
隐性电平
。 -
如果CAN控制器发送逻辑0时,CAN收发器使CAN_H为3.5V,CAN_L为1.5V,这时,两条线上的电压差为2V。总线上称为
显性电平
。 -
多个节点同时开始发送时,会涉及到
总线仲裁
(也就是谁可以继续发送)。详情看后面内容。
四.CAN帧的种类
• 每一个节点都可以主动发送帧。(帧是CAN协议规定的发送或接收的单位) 。帧由段组成,段由二进制位组成。
序号 | 名称 | 帧用途 |
---|---|---|
1 | 数据帧 | 用于发送单元向接收单元传送数据的帧。 |
2 | 遥控帧 | 用于接收单元向具有相同 ID 的发送单元请求数据的帧。 |
3 | 错误帧 | 用于当检测出错误时向其它单元通知错误的帧。 (硬件自动完成) |
4 | 过载帧 | 当一个节点正忙于处理接收的信息,可以通知其它节点暂缓发送新报文。(硬件自动完成) |
5 | 帧间隔 | 用于将数据帧及遥控帧与前面的帧分离开来的帧(硬件自动完成) |
帧的种类有很多,其中错误帧
、过载帧
、帧间隔
都是由硬件完成的,没有办法用软件来控制。对于一般使用者来说,只需要掌握数据帧
与遥控帧
。数据帧和遥控帧有标准格式
与扩展格式
。标准格式有11位
标识符,扩展格式有29位
标识符。
(1)遥控帧
数据帧和远程帧结构上差不多,最大的区别就是远程帧没有数据段。 数据帧也是最复杂的帧,所以我们重点解析下面要讲的数据帧。
(2)数据帧
序号 | 名称 | 描述 |
---|---|---|
1 | 帧起始 | 表示帧的开始,产生一个bit的显性电平。 |
2 | 仲裁段 | 表示帧的优先级, 由标识符(ID)和传送帧类型(RTR)组成。 |
3 | 控制端 | 表示数据的字节数,由6个bit构成 |
4 | 数据段 | 数据的具体内容,可发送0~8 个字节的数据。 |
5 | CRC段 | 用于校验传输是否正确。 |
6 | ACK段 | 表示确认是否正常接收。 |
7 | 帧结束 | 表示此帧结束。 |
这里着重讲仲裁段、控制段与数据段。
(1)仲裁段。 仲裁段分为标准格式(11位)以及扩展格式(29位)。仲裁段里含有CAN通信中一项非常重要的信息(ID),你可以把它当成是每一个报文的名字,每个CAN device可以发送多条不同的ID报文。每一个总线节点的CAN控制器中都有一个东西叫过滤器,通过设置相关寄存器能够配置它,一旦设置好,CAN控制器会根据你的设置,自动去判断要不要接收报文,这部分完全由硬件实现,这个判断要不要接收的过程,也叫过滤,判断的依据就是每个报文的ID。扩展格式的ID具有比标准格式更多的位数,从而有更高的ID容量。
- 标准格式的标识符长度的是11位,紧随其后的是RTR位,用于表明此帧是数据帧还是远程帧。IDE位用于表明此帧是标准帧还是扩展帧。
- 扩展格式的标识符长度的是29位,紧随其后的是RTR位,用于表明此帧是数 据帧还是远程帧。IDE位用于表明此帧是标准帧还是扩展帧。
(2)控制段。 控制段表示数据段的字节数。标准格式和扩展格式的构成有所不同。 控制段含有保留位与数据长度码(DLC)。
- 保留位(r0、r1) 必须全部以显性电平发送。但接收方可以接收显性、隐性及其任意组合的电平。
- 数据长度码(DLC) 与数据的字节数的对应关系下表所示。数据的字节数必须为 0~8字节。但接收方对 DLC=9~15的情况并不视为错误。
(3)数据段。 数据段就是你需要发送的数据,可能包含字符0~8个字节数据,CAN控制器有对应的寄存器,只需要把数据直接填进去就可以了。
五.CAN的仲裁方法
1. 在总线空闲时,最先开始发送的节点获得发送权,一旦开始发送,不会被其他节点抢占。
2. 多个节点同时开始发送时,各发送节点从仲裁段的第一位开始进行仲裁。连续输出显性电平最多的节点可继续发送。(Dominant :显性优先)
3. 具有相同ID的数据帧和遥控帧在总线上竞争时,仲裁段的最后一位(RTR)为显性位的数据帧具有优先权可继续发送。
3. 标准格式ID与具有相同ID的遥控帧或者扩展格式的数据帧在总线上竞争时,标准格式的RTR 位为显性位的具有优先权可继续发送。
六.CAN在Stm32上的应用
上面梳理了一下CAN通信协议的知识,内容虽然很多,但是很多东西都是硬件自动实现的。对于只需要做软件的人,只需要配置好CAN的相关寄存器,其他的事情只需要交由硬件自己去做就可以了。对于实现CAN通信,我把它分为三个步骤:
(1) CubeMX上配置CAN
(2) 配置CAN的过滤器
(3) 报文的发送、接受
开发环境:Keil 、 CubeMX
Vesion : 5.28 、 5.4.0
开发语言:C
(1)CubeMX上CAN的配置
(1) 首先需要配置CAN通信的波特率。 以我自己为例,我用的是STM32F405,分频系数设为3后,cubeMX会自动完成Time Quantum(TQ)
的计算,将得到的TQ乘以TBS1
、TBS2
、SJY
之和刚好就是1us,对应的波特率是1M,这是CAN总线支持的最高通讯频率。通信频率需要根据自己需要设置,注意的是同一条CAN总线上的设备的波特率需要一致。
71.42857142857143ns*(10+3+1)=1000ns=1us
即公式:
CAN波特率=TQ*(TBS1+TBS2+SJY)
也能够直接用公式:
CAN波特率=APB总线频率/分频系数/(TBS1+TBS2+SJY)
(2) 其次就是打开CAN的接受中断。 打开中断是为了CPU能够及时接受和处理放在 “接受邮箱” 中的报文。STM32F103系列单片机中CAN中具有两个“接受邮箱”,即FIFO0
与FIFO1
,每一个邮箱又有三层,每一层都可以存放一条报文,与过滤器匹配的报文会被放入FIFO邮箱中。CAN中受到的消息不是直接送入CPU,而是以报文的形式存在邮箱中,CPU需要时再去邮箱里取,读取时只能读到最先收到的报文,等这个读完之后,才能读下一个报文。每一个接受邮箱接受数据端都有一个过滤器,它会根据使用者的配置来判断每一条报文的ID,决定它是接受还是丢弃。这里我只打开FIFO0的中断(RX0),也能够同时打开RX0和RX1,以提高接受报文的容量。
既然讲到“接受邮箱”那么也讲讲“发送邮箱”。 发送邮箱是用于CAN总线数据发送。同样以STM32F103系列单片机为例,总共有3个,并且存在优先级关系。优先级越高表示其里面的数据会被优先发送。数据在发送前都会被送到优先级最高且空闲的发送邮箱,然后依次发送。最后说明一点:“发送邮箱有3个,且每个邮箱只能装一个报文”。
(2)配置CAN过滤器
1. CAN过滤器有两种工作模式:****列表模式
与 掩码模式
。
-
列表模式: 列出ID名字,过滤器通过判断报文ID与其是否一致来决定是接受还是舍弃这份报文。这种列表的方式受到列表容量大小的限制,实际上,bxCAN的一个过滤器若工作在列表模式下,scale为32时,每个过滤器的列表只能写入两个报文ID,若scale为16时,每个过滤器的列表最多可写入4个CAN ID 。
-
掩码模式: 通过确定ID特定位的值来判断报文的接受与丢弃。就像你想筛选出国内生日为1999年9月29日的人,你只需要选出身份证号码的第7~14位为19990929的人,其他位则不用关心。CAN通信中,掩码模式的实现主要是通过设定
屏蔽码
与验证码
的方式来实现筛选出特定位上特定数值的ID报文。掩码模式不受容量大小的局限。
2. 对于配置过滤器,还需知道三个重要的寄存器:****CAN_FS1R
、CAN_FxR1
和CAN_FxR2
。
- CAN_FS1R寄存器。 我们知道标准ID位11位,而扩展CAN ID有29位。对于标准的ID来说,我们用一个16位的寄存器来处理他足够了,而扩展CAN ID,我们就必须使用32位的寄存器来处理它。有些时候,我们自始至终都只需要处理标准ID,所以出于对资源最有效利用,Stm32设立了CAN_FS1R寄存器来表示ID位宽,标志是否需要处理32位的ID。依据模式与位数的不同也就出现了4种组合:
32位宽的列表模式
、16位宽的列表模式
、32位宽掩码模式
、16位宽的掩码模式
。 - CAN_FxR1寄存器和CAN_FxR2寄存器。 每个过滤器都存在这么两个寄存器CAN_FxR1和CAN_FxR2,这两个寄存器都是32位的,他们的定义并非固定的,针对不同的工作模式组合它们的定义是不一样的。
工作模式 | CAN_FxR1寄存器和CAN_FxR2寄存器定义 |
---|---|
32位宽的列表模式 | 两寄存器的定义一样,都用来存储某个期望通过的CAN ID,这样就可以存入2个期望通过的CAN ID(标准CAN ID和扩展CAN ID均可)。 |
16位宽的列表模式 | 两寄存器定义一样,且各自拆成两个,则总共可以写入4个标准CAN ID。 |
32位宽掩码模式 | CAN_FxR1用做32位宽的验证码,而CAN_FxR2则用作32位宽的屏蔽码。 |
16位宽的掩码模式 | 可以当做2对验证码+屏蔽码组合来用,但它只能对标准CAN ID进行过滤。 |
32位列表模式
/**
* @brief 32位列表模式过滤器配置(这里举一个标准ID一个扩展ID)
* @param hcan CAN的句柄
* @param StdId 标准ID
* @param ExtId 扩展ID
*/
void CANFilterConfig_Scale32_IdList(CAN_HandleTypeDef * hcan,uint32_t StdId,uint32_t ExtId,uint8_t Filter_Nunber )
{
CAN_FilterConfTypeDef sFilterConfig;
sFilterConfig.FilterNumber = Filter_Nunber ; //使用过滤器编号
sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST; //设为列表模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; //配置为32位宽
sFilterConfig.FilterIdHigh = StdId<<5; //基本ID放入到STID中
sFilterConfig.FilterIdLow = 0|CAN_ID_STD; //设置IDE位为0
sFilterConfig.FilterMaskIdHigh = ((ExtId<<3)>>16)&0xffff;
sFilterConfig.FilterMaskIdLow = (ExtId<<3)&0xffff|CAN_ID_EXT;//设置IDE位为1
sFilterConfig.FilterFIFOAssignment = 0; //接收到的报文放入到FIFO0中
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.BankNumber = 14;
HAL_CAN_ConfigFilter(hcan, &sFilterConfig) ;
}
123456789101112131415161718192021
在HAL库中,CAN_FxR1
与CAN_FxR2
寄存器分别被拆成两段,CAN_FxR1寄存器的高16位对应着上面代码中的FilterIdHigh
,低16位对应着FilterIdLow
,而CAN_FxR2寄存器的高16位对应着FilterMaskIdHigh
,低16位对应着FilterMaskIdLow
。
CAN_FilterConfTypeDef
的的4个成员FilterIdHigh
,FilterIdLow
,FilterMaskIdHigh
,FilterMaskIdLow
,不应该单纯看其名字,被其误导。
16位列表模式
/**
* @brief 16位列表模式过滤器配置
* @param hcan CAN的句柄
* @param StdId1~StdId4 4个标准ID
*/
void CANFilterConfig_Scale16_IdList(CAN_HandleTypeDef * hcan,uint32_t StdId1,uint32_t StdId2,uint32_t StdId3,uint32_t StdId4,uint8_t Filter_Nunber)
{
CAN_FilterConfTypeDef sFilterConfig;
sFilterConfig.FilterNumber = Filter_Nunber; //使用过滤器1
sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST; //设为列表模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_16BIT; //位宽设置为16位
sFilterConfig.FilterIdHigh = StdId1<<5; //4个标准CAN ID分别放入到4个存储中
sFilterConfig.FilterIdLow = StdId2<<5;
sFilterConfig.FilterMaskIdHigh = StdId3<<5;
sFilterConfig.FilterMaskIdLow = StdId4<<5;
sFilterConfig.FilterFIFOAssignment = 0; //接收到的报文放入到FIFO0中
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.BankNumber = 14;
HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);
}
1234567891011121314151617181920
32位掩码模式
/**
* @brief 32位掩码模式过滤器配置
* @param hcan CAN的句柄
* @param ID 验证码
* @param Mask 屏蔽码
* @param ID_Type ID类型(标准帧为0,其他则为扩展帧)
*/
void CANFilterConfig_Scale32_IdMask(CAN_HandleTypeDef * hcan,uint32_t ID,uint32_t Mask,uint8_t ID_Type,uint8_t Filter_Nunber)
{
if(ID_Type==0) //标准ID
{
CAN_FilterConfTypeDef sFilterConfig;
sFilterConfig.FilterNumber = Filter_Nunber; //使用过滤器2
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; //配置为掩码模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; //设置为32位宽
sFilterConfig.FilterIdHigh =ID<<5;
sFilterConfig.FilterIdLow =0;
sFilterConfig.FilterMaskIdHigh =(Mask<<5);
sFilterConfig.FilterMaskIdLow =0|0x02; //只接收数据帧
sFilterConfig.FilterFIFOAssignment = 0; //设置通过的数据帧进入到FIFO0中
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.BankNumber = 14;
HAL_CAN_ConfigFilter(hcan, &sFilterConfig);
}
else //扩展ID
{
CAN_FilterConfTypeDef sFilterConfig;
sFilterConfig.FilterNumber = Filter_Nunber; //使用过滤器3
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; //配置为掩码模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; //设为32位宽
sFilterConfig.FilterIdHigh =((ID<<3) >>16) &0xffff;
sFilterConfig.FilterIdLow =((ID<<3)&0xffff) | CAN_ID_EXT;
sFilterConfig.FilterMaskIdHigh = (Mask>>16)&0xffff;
sFilterConfig.FilterMaskIdLow = (Mask&0xffff)|0x02; //只接收数据帧
sFilterConfig.FilterFIFOAssignment = 0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.BankNumber = 14;
HAL_CAN_ConfigFilter(hcan, &sFilterConfig);
}
}
123456789101112131415161718192021222324252627282930313233343536373839404142
(3)CAN数据的发送与接收
/**
* @brief 发送标准ID的数据帧
* @param hcan CAN的句柄
* @param ID 数据帧ID
* @param pData 数组指针
* @param Len 字节数0~8
*/
uint8_t CANx_SendStdData(CAN_HandleTypeDef* hcan,uint16_t ID,uint8_t *pData,uint16_t Len)
{
static CAN_TxHeaderTypeDef Tx_Header;
Tx_Header.StdId=ID;
Tx_Header.ExtId=0;
Tx_Header.IDE=0;
Tx_Header.RTR=0;
Tx_Header.DLC=Len;
/*找到空的发送邮箱,把数据发送出去*/
if(HAL_CAN_AddTxMessage(hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX0) != HAL_OK) //
{
if(HAL_CAN_AddTxMessage(hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX1) != HAL_OK)
{
HAL_CAN_AddTxMessage(hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX2);
}
123456789101112131415161718192021222324
/**
* @brief 发送扩展ID的数据帧
* @param hcan CAN的句柄
* @param ID 数据帧ID
* @param pData 数组指针
* @param Len 数据长度0~8
*/
uint8_t CANx_SendExtData(CAN_HandleTypeDef* hcan,uint32_t ID,uint8_t *pData,uint16_t Len)
{
static CAN_TxHeaderTypeDef Tx_Header;
Tx_Header.RTR=0;
Tx_Header.DLC=Len;
Tx_Header.StdId=0;
Tx_Header.ExtId=ID;
Tx_Header.IDE=CAN_ID_EXT;
/*找到空的发送邮箱,把数据发送出去*/
if(HAL_CAN_AddTxMessage(&hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX0) != HAL_OK) //
{
if(HAL_CAN_AddTxMessage(&hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX1) != HAL_OK)
{
HAL_CAN_AddTxMessage(&hcan, &Tx_Header, pData, (uint32_t*)CAN_TX_MAILBOX2);
}
}
12345678910111213141516171819202122232425
/**
* @brief CAN FIFO0的中断回调函数,在里面完成数据的接收
* @param hcan CAN的句柄
*/
uint8_t date_CAN1[8];//设为全局变量,用于接收CAN1数据
uint8_t date_CAN2[8];//设为全局变量,用于接收CAN2数据
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
if(hcan->Instance ==CAN1)
{
CAN_RxHeaderTypeDef RxHeader; //接受句柄
HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &RxHeader, date_CAN1); //接收,CAN邮箱为0
return ;
}
else if(hcan->Instance == CAN2)
{
CAN_RxHeaderTypeDef RxHeader; //接受句柄
HAL_CAN_GetRxMessage(&hcan2, CAN_RX_FIFO0, &RxHeader, date_CAN2); //接收,CAN邮箱为0
}
}
12345678910111213141516171819202122
在启用CAN时不要忘记了进行初始化。
void CAN_Start(CAN_HandleTypeDef *hcan)
{
HAL_CAN_ActivateNotification(hcan ,CAN_IT_RX_FIFO0_MSG_PENDING);
HAL_CAN_Start(hcan);
}
12345