已更新完成,标准帧部分已补,前面can相关知识有时间再写吧,没时间不写了,不耽误配置,按照步骤走是可以直接用的

工作中经常用到,每次隔时间长就会忘,这次写个笔记留给自己,目前扩展帧已完善,标准帧后续会写,还有一些空出来的后续再写,配置写的挺清楚的,可以直接用,我用的芯片是f103rct,芯片不一样工程不能直接用   

通过网盘分享的文件:can.rar
链接: https://pan.baidu.com/s/1y5U1vkkf1eW3LUpLs3vuSQ?pwd=ifjj 提取码: ifjj 
--来自百度网盘超级会员v8的分享                    

介绍这部分是can的原理,可以不看,直接到配置部分

一、CAN总线介绍

1.物理层

只介绍高速CAN,闭环,总线最大长度为 40m,通信速度最高为 1Mbps,总线的两端各要求有一个“120 欧”的电阻

CAN 总线可以挂载多个通讯节点,且CAN 通讯协议不对节点进行地址编码,而是对数据内容进行编码,所以网络中的节点个数理论上不受限制,只要总线的负载足够即可。CAN 通讯节点由一个 CAN 控制器及 CAN 收发器组成,控制器与收发器之间通过 CAN_Tx 及CAN_Rx 信号线相连,收发器与 CAN 总线之间使用 CAN_High 及 CAN_Low 信号线相连。CAN_High 及 CAN_Low 是一对差分信号线。

CAN 协议中对它使用的 CAN_High 及 CAN_Low 表示的差分信号做了规定,以高速 CAN 协议为例,当表示逻辑 1 时 (隐性电平) , CAN_High 和 CAN_Low 线上的电压均为 2.5v,即它们的电压差 VH-V:sub:L=0V;而表示逻辑 0 时 (显性电平) , CAN_High 的电平为 3.5V, CAN_Low 线的电平为 1.5V,即它们的电压差为 VH-V:sub:L=2V。例如,当 CAN收发器从 CAN_Tx 线接收到来自 CAN 控制器的低电平信号时 (逻辑 0),它会使 CAN_High 输出3.5V,同时 CAN_Low 输出 1.5V,从而输出显性电平表示逻辑 0 。CAN只有一对差分线,同一时刻只能有一个通讯节点发送信号,半双工

当 CAN 节点需要发送数据时,控制器把要发送的二进制编码通过 CAN_Tx 线发送到收发器,然
后由收发器把这个普通的逻辑电平信号转化成差分信号,通过差分线 CAN_High 和 CAN_Low 线
输出到 CAN 总线网络。而通过收发器接收总线上的数据到控制器时,则是相反的过程,收发器
把总线上收到的 CAN_High 及 CAN_Low 信号转化成普通的逻辑电平信号,通过 CAN_Rx 输出到
控制器中。

2.协议层

2.1CAN 的报文种类及结构

CAN 一共规定了 5 种类型的帧

数据帧是在 CAN 通讯中最主要、最复杂的报文

数据帧以一个显性位 (逻辑 0) 开始,以 7 个连续的隐性位 (逻辑 1) 结束,在它们之间,分别有仲
裁段、控制段、数据段、 CRC 段和 ACK 段。

仲裁段:表示优先级,由报文 ID 、 RTR、 IDE 和 SRR 位组成

        ID:数据帧具有标准格式和扩展格式两种,标准格式的 ID 为 11 位,扩展格式的 ID 为 29 位,ID 决定数据帧发送的优先级,总线上同时出现显性电平和隐性电平,总线的状态会被置为显性电平,也就是显性电平优先级高,就意味着ID越小,优先级越高

        RTR:0:数据帧      1:遥控帧

        IDE: 0:标准格式   1:扩展格式

        SRR: 只存在于扩展格式,替代标准格式中的 RTR

控制段:主要是DLC,4 个数据位组成,表示本报文中的数据段含有多少个字节,范围0~8

数据段:数据内容,MSB先行

CRC段:校验

ACK 段:类似 I2C 总线,在 ACK 槽位中,发送节点发送的是隐性位,而接收节点则在这一位中发送显性位以示应答。

帧结束:7 个隐性位

2.2位时序

CAN 使用“位同步”的方式来抗干扰、吸收误差,实现对总线电平信号进行正确的采样,确保通讯正常。为了实现位同步, CAN 协议把每一个数据位的时序分解成如图所示的 SS 段、 PTS 段、PBS1 段、 PBS2 段,这四段的长度加起来即为一个 CAN 数据位的长度。

这里只需要知道

SS段:信号跳变沿被包含在 SS 段内,表示同步,大小固定为 1Tq。
PTS 段:大小为 1~8Tq
PBS1段:大小为 1~8Tq
PBS2段:大小为 2~8Tq
其中PBS1段和PBS2段是需要我们在cubemx里配置的,会和can总线的波特率有关

二、stm32CAN外设

1.工作模式

正常模式:一个正常的 CAN 节点,可以向总线发送数据和接收数据

静默模式:自己输出端的逻辑 0 数据会直接传输到它自己的输入端,逻辑 1 可以被发送到总线,不能向总线发送显性位 (逻辑 0),只能发送隐性位 (逻辑 1)。输入端可以从总线接收内容。由于只可发送的隐性位不会强制影响总线的状态,所以把它称为静默模式。这种模式一般用于监测,它可以用于分析总线上的流量,但又 不会因为发送显性位而影响总线。

回环模式:

回环静默模式:

STM32 外设定义的位时序与我们前面解释的 CAN 标准时序有一点区别

SYNC_SEG 段固定长度为 1Tq,BS1 段理解为是由前面介绍的 CAN 标准协议中 PTS 段与 PBS1 段合在一起的,而 BS2 段就相当于 PBS2 段。

CAN 发送流程
程序选择 1 个空置的邮箱(TME=1) →设置标识符(ID),数据长度和发送数据→设置 CAN_TIxR 的 TXRQ 位为 1,请求发送→邮箱挂号(等待成为最高优先级) →预定发送(等待总线空闲) →发送→邮箱空置。

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 空。
 

三、配置

3.1 stm32cubemx选配

 cubemx的配置还是很简单的,sys选serial wire

时钟RCC,选择外部高速时钟

时钟树,需要的时钟根据自己需求选择,我喜欢配最大,一定要是HSE和PLLCLK

增加一路串口用来打印调试信息,选择异步模式,注意波特率,我这里用的默认

can:

psc,bs1,bs2这三个俩都是用来算波特率的,只要这三个数在范围内,根据实际需要的波特率,想怎么配怎么配

计算波特率

bruad=PCLK1/Prescaler/(BS1+BS2+ReSynchronization)名字均与cubemx里对的上,自行对照

f1系列的can都挂载在APB1上

前面时钟树,APB1外设这条路上的时钟频率是36MHz

我实际需要400bit/s,所以braud=36000000/9/(5+4+1)=400

打开中断

文件名

勾选分文件编程,给每一个外设一个.c文件

生成工程,这样在cubemx的选配工作就完成了

3.2 代码

1.准备工作

新建班级支持包,加头文件

1.1 选库

打开工程文件,点击魔术棒选项,勾选库,不选库的话程序会卡在初始化,不能往下执行

接下来在debug界面,改一下下载器,默认是ST,我用的是CMSIS

我还喜欢勾选一下下载之后自动复位运行,这样就不用我按按键复位了,点击上一张图红框里的setting即可进入下面的界面,记得点击ok保存,然后编译一下

1.2 板级支持包

之前就说过了,板级支持包就是自己的头文件和源文件,如果不喜欢单独构建文件,也可以在工程文件的指定位置写,cubemx生成的代码每个源文件都留了自己编程的位置。

举例,在这两行中间写就行。在其他位置写的话,如果后续改动了cubemx里的配置,重新生成代码之后会被清空,所有要么写的时候注意位置,要么就构建板级支持包,班级支持包里的东西不会被清空

接下来是构建板级支持包的步骤

首先在工程文件任意位置,新建文件夹,一定要在工程文件里,我喜欢在工程文件的Drivers里,这个文件里原本就有两个生成的文件夹,名字可以随便起,不带中文就行

在里面新建源文件和头文件,因为是can,和生成的can区分开,我是这么命名的

在头文件加入条件编译,我习惯就在txt这写了,待会去keil里写也行,记得留出空行,不然会报错

回到keil,将刚才新建的文档加入工程,,按照一下步骤操作

添加完了如图所示,点击ok即可

在左侧project栏里可以看到添加成功的文件

接下来酱板级支持包的头文件添加到工程,不然编译的时候找不到,点击魔术棒选项,把文件夹添加进来,点击ok然后编译,不编译就识别不到

在源文件加入代码,编译无错,可以跳转到对应的头文件

最后,将串口打印重定向,用来调试,这段代码也可以放到自己的板级支持包里,但是这个工程并没有额外的串口功能要实现,所以放到usart.c里了,还需包含头文件,代码在下面,复制到对应位置

所需代码如下

/* USER CODE BEGIN 0 */
#include "stdio.h"

/* USER CODE END 0 */


/* USER CODE BEGIN 1 */
int fputc(int ch, FILE *f)
{
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);	
	
	return (ch);
}
/* USER CODE END 1 */

至此准备工作完成

main.c里同样要加stdio.h的头文件,不然printf用不了,在mian.c里加入打印看看代码能不能执行

2.代码

2.1 发送

发送是很简单的,现在就可以写发送的代码了,在bsp_can.c里写,记得添加头文件

扩展帧版

bsp_can.h

#ifndef _BSP_CAN_H_
#define _BSP_CAN_H_

#include "can.h"

void can_send(uint32_t id,uint8_t *buf,uint8_t len);

#endif



bsp_can.c

#include "bsp_can.h"

CAN_TxHeaderTypeDef  TxHeader;


void can_send(uint32_t id,uint8_t *buf,uint8_t len){
	uint32_t TxMailbox;
	TxHeader.StdId=id;
	TxHeader.ExtId=id;
	TxHeader.IDE=CAN_ID_EXT;
	TxHeader.RTR=CAN_RTR_DATA;
	TxHeader.DLC=len;	
	
	while(HAL_CAN_AddTxMessage(&hcan, &TxHeader, buf, &TxMailbox) != HAL_OK);
}


来到main.c,将我们自己的头文件包含进来

在对应位置,加入如下代码,别忘记打开can,打印是用来检测程序是否运行到这的

  /* USER CODE BEGIN 2 */
	HAL_CAN_Start(&hcan);
	printf("55555\n");
	
	uint8_t data[3]={0x1,0x2,0x3};
	can_send(0x10080000,data,3);
  /* USER CODE END 2 */

验证can能不能用有多种方法,可以自检,用回环模式,我这边采用了can分析仪,单独买的,很好用,我用的下面的,有客服会教怎么用

还有配套的软件,也有教程

代码写好以后,编译下载,可以看到发送的信息,发送函数完成

 标准帧

bsp_can.h和扩展帧一样

#include "bsp_can.h"

CAN_TxHeaderTypeDef  TxHeader;


void CAN_Send(uint32_t id, uint8_t* buf, uint8_t len)
{	    
	uint32_t TxMailbox;
	TxHeader.StdId = id;        
	TxHeader.IDE = CAN_ID_STD;    
	TxHeader.RTR = CAN_RTR_DATA;  
	TxHeader.DLC = len;       
	while(HAL_CAN_AddTxMessage(&hcan, &TxHeader, buf, &TxMailbox) != HAL_OK);//发送
}

main.c验证,我换id为0x123,可以看到能收到

2.2 接收
过滤器

can的关键就是搞懂过滤器配置,过滤器主要用于接收的时候过滤id,下面这张图,可以看到有四种模式,很明显前两个模式是留给扩展帧,后两个模式是留给标准帧的

过滤器的代码是需要自己写的,代码里圈出来的部分就对应以上四种模式,只要配置这四个成员就能满足以上四个模式,接下来介绍这四个模式 先从最简单的开始

4个16位标识符列表模式

选择这个模式以后, FilterIdHigh,FilterIdLow,FilterMaskIdHigh,FilterMaskIdLow这四个变量全部变为id,每一个变量代表一个你想过滤的id,只需填入对应的id即可

标准帧id为11位,可以看到只有搞11位是id, 因为是数据帧,RTR位是0,因为是为标准帧IDE位是0,EXID也是0,没有扩展id,所以低五位全是零,所以代码里直接将id左移五位填入了。

void CAN_Filter_Config(void)
{
	CAN_FilterTypeDef CAN_FilterInitStructure;
	
	CAN_FilterInitStructure.FilterBank=0;//筛选器0
	CAN_FilterInitStructure.FilterMode=CAN_FILTERMODE_IDLIST;//屏蔽模式
	CAN_FilterInitStructure.FilterScale=CAN_FILTERSCALE_16BIT;//位宽 32
	
	CAN_FilterInitStructure.FilterIdHigh=0x123<<5;
	CAN_FilterInitStructure.FilterIdLow=0x346;
	
	CAN_FilterInitStructure.FilterMaskIdHigh=0x0000;
	CAN_FilterInitStructure.FilterMaskIdLow=0x136<<5;
	
	CAN_FilterInitStructure.FilterFIFOAssignment=CAN_FILTER_FIFO0;
	CAN_FilterInitStructure.FilterActivation=ENABLE;
	CAN_FilterInitStructure.SlaveStartFilterBank = 14;
	
	HAL_CAN_ConfigFilter(&hcan,&CAN_FilterInitStructure);
	
	HAL_CAN_Start(&hcan);//开启CAN
	HAL_CAN_ActivateNotification(&hcan,CAN_IT_RX_FIFO0_MSG_PENDING);//开启CAN-FIFO0中断
}

2个16位标识符屏蔽模式

选择这个模式以后, FilterIdHigh,FilterIdLow,对应需要过滤的id,FilterMaskIdHigh,FilterMaskIdLow根据需要,将需要保留的位写为1即可

void CAN_Filter_Config(void)
{
	CAN_FilterTypeDef CAN_FilterInitStructure;
	
	CAN_FilterInitStructure.FilterBank=0;//筛选器0
	CAN_FilterInitStructure.FilterMode=CAN_FILTERMODE_IDLIST;//屏蔽模式
	CAN_FilterInitStructure.FilterScale=CAN_FILTERSCALE_16BIT;//位宽 32
	
	CAN_FilterInitStructure.FilterIdHigh=0x123<<5;//id1
	CAN_FilterInitStructure.FilterIdLow=0x346<<5;//id2
	
	CAN_FilterInitStructure.FilterMaskIdHigh=0x0000;//不过滤
	CAN_FilterInitStructure.FilterMaskIdLow=0x0000;//不过滤
	
	CAN_FilterInitStructure.FilterFIFOAssignment=CAN_FILTER_FIFO0;
	CAN_FilterInitStructure.FilterActivation=ENABLE;
	CAN_FilterInitStructure.SlaveStartFilterBank = 14;
	
	HAL_CAN_ConfigFilter(&hcan,&CAN_FilterInitStructure);
	
	HAL_CAN_Start(&hcan);//开启CAN
	HAL_CAN_ActivateNotification(&hcan,CAN_IT_RX_FIFO0_MSG_PENDING);//开启CAN-FIFO0中断
}

1个32位标识符屏蔽模式

选择这个模式以后,  FilterIdHigh,FilterIdLow,填入所需的id高位和低位,FilterMaskIdHigh,FilterMaskIdLow则成为掩码

掩码:有了掩码以后可以想保留哪位就保留哪位,将需与id一致的bit写1,不管的bit写0

举例:我选择扩展id=0x10241234,我想让id=0x102x1xxx的所有id进入,那么 FilterMaskIdHigh=0xfff0,FilterMaskIdLow=0x1000

这里有两个特例

FilterMaskIdHigh=0x0000,FilterMaskIdLow=0x0000;这两位都为0时,代表不过滤,所有id均可进入

FilterMaskIdHigh=0xffff,FilterMaskIdLow=0xffff;代表只有一个id可以进入,就是与FilterIdHigh,FilterIdLow一模一样的id才可以进入,在例子下就是只有0x10241234这个id可以进入

void CAN_Filter_Config(void)
{
	CAN_FilterTypeDef CAN_FilterInitStructure;
	
	CAN_FilterInitStructure.FilterBank=0;//筛选器0
	CAN_FilterInitStructure.FilterMode=CAN_FILTERMODE_IDMASK;//屏蔽模式
	CAN_FilterInitStructure.FilterScale=CAN_FILTERSCALE_32BIT;//位宽 32
	
	CAN_FilterInitStructure.FilterIdHigh=(0x10240000>>13) & 0xffff;//id高位
	CAN_FilterInitStructure.FilterIdLow=(0x10240000|CAN_ID_EXT|CAN_RTR_DATA) & 0xffff;//ID低位
	//我这个代码就是让高四位一样的所有id都进入
	CAN_FilterInitStructure.FilterMaskIdHigh=0xfff0;
	CAN_FilterInitStructure.FilterMaskIdLow=0x0000;
	
	CAN_FilterInitStructure.FilterFIFOAssignment=CAN_FILTER_FIFO0;
	CAN_FilterInitStructure.FilterActivation=ENABLE;
	CAN_FilterInitStructure.SlaveStartFilterBank = 14;
	
	HAL_CAN_ConfigFilter(&hcan,&CAN_FilterInitStructure);
	
	HAL_CAN_Start(&hcan);//开启CAN
	HAL_CAN_ActivateNotification(&hcan,CAN_IT_RX_FIFO0_MSG_PENDING);//开启CAN-FIFO0中断
}

2个32位标识符列表模式

选择这个模式以后, FilterIdHigh,FilterIdLow是一个32位的id,FilterMaskIdHigh,FilterMaskIdLow也是一个32位的id,只允许这里两个id进入

了解了过滤器之后,接着写接收的代码,用中断接收,前面已经选配好了,直接写代码,在刚才发送的基础上增加以下代码,我这里就是只要前四位一样就都接收进来

CAN_RxHeaderTypeDef  RxHeader;
uint8_t              RxData[8] = {0};


void CAN_Filter_Config(void)
{
	CAN_FilterTypeDef CAN_FilterInitStructure;
	
	CAN_FilterInitStructure.FilterBank=0;//筛选器0
	CAN_FilterInitStructure.FilterMode=CAN_FILTERMODE_IDMASK;//屏蔽模式
	CAN_FilterInitStructure.FilterScale=CAN_FILTERSCALE_32BIT;//位宽 32
	
	CAN_FilterInitStructure.FilterIdHigh=(0x10240000>>13) & 0xffff;//id高位
	CAN_FilterInitStructure.FilterIdLow=(0x10240000|CAN_ID_EXT|CAN_RTR_DATA) & 0xffff;//ID低位
	
	CAN_FilterInitStructure.FilterMaskIdHigh=0xffff;
	CAN_FilterInitStructure.FilterMaskIdLow=0x0000;
	
	CAN_FilterInitStructure.FilterFIFOAssignment=CAN_FILTER_FIFO0;
	CAN_FilterInitStructure.FilterActivation=ENABLE;
	CAN_FilterInitStructure.SlaveStartFilterBank = 14;
	
	HAL_CAN_ConfigFilter(&hcan,&CAN_FilterInitStructure);
	
	HAL_CAN_Start(&hcan);//开启CAN
	HAL_CAN_ActivateNotification(&hcan,CAN_IT_RX_FIFO0_MSG_PENDING);//开启CAN-FIFO0中断
}

//可以直接将这两行放到main里,我这里为了方便,额外写了这个函数
void CAN_Config(void){
	CAN_Filter_Config();
	HAL_CAN_Start(&hcan);
}

用的是中断接收,还要写中断回调函数,为了方便我也在写在bsp_can.c里,写在mian里其实也行,但是还得把接收的句柄extern过去,有点麻烦,中断函数如下,里面每一步的打印都是用来检测程序是否运行到这的

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
	printf("333\n");      
	HAL_CAN_GetRxMessage(hcan,CAN_RX_FIFO0,&RxHeader,RxData);//接收数据
	if(RxHeader.ExtId==0x10240000){
		printf("10240000\n");
		
	}

		
	__HAL_CAN_ENABLE_IT(hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
}

写好之后还是用测试软件发送,看看是否能按预期打印,可以看到,不符合过滤器的id连中断都进不了,在中断里可以根据id的不同进行不同的数据处理

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐