STM32CubeMX教程21 CAN - 双机通信
1、准备材料
开发板(
正点原子stm32f407探索者开发板V2.4
)
STM32CubeMX软件(
Version 6.10.0
)
keil µVision5 IDE(
MDK-Arm
)
CH340G Windows系统驱动程序(
CH341SER.EXE
)
2、实验目标
使用STM32CubeMX软件配置
两台STM32F407开发板的CAN1模块实现双机通信
3、实验流程
3.0、前提知识
3.0.1、CAN总体概述
STM32F407内部有两个CAN控制器,其中CAN1做为主CAN拥有所有的权限,而CAN2做为从CAN不能单独设置验证筛选器,每个CAN都有3个发送邮箱和两个接收FIFO,每个接收FIFO可以存储三条完整消息,具体的CAN框图如下图所示
(注释1)
CAN的总线网络结构有开环和闭环两种形式
闭环网络结构下,两根信号线H/L组成一个环路,在网络环路的两端连接120欧姆的电阻,这种网络是一种高速、短距离的CAN网络,通信速率最高1Mbit/s;
开环网络结构下,两根信号线H/L各自独立,在两根信号线上各自串联一个2.2千欧的电阻,这种网络是一种低速、远距离的CAN网络,通信速率最高125kbit/s;
如下图所示为开环/闭环的CAN总线网络结构
(注释2)
不管是开环还是闭环CAN网络结构都可以挂载多个节点,
CAN网络上的每个节点由CAN控制器和CAN收发器组成
,STM32F407内部集成的是CAN控制器,因此硬件设计时需要外部搭载CAN收发器才可以组成一个完整的CAN节点,如下图所示为笔者使用的开发板上搭载的CAN收发器芯片硬件原理图
这里CAN收发器芯片的TX/RX引脚并没有直接与单片机CAN1控制器的TX/RX引脚相连接,而是通过了一个跳线帽来进行调节,
读者在做该实验的时候应该注意USB/CAN排针座应该利用跳线帽将CAN_TX/RX引脚与USB_D+/D-进行短接
,具体硬件原理图如下图所示
3.0.2、CAN位时序和波特率
CAN通信是一种异步通信
,异步通信的收发双方无时钟同步,因此需要确保收发双方发送/接收一帧数据的帧格式和波特率保持一致,这样才能保证收发双方正确的进行通信
CAN1/2挂载在APB1最高42MHz的时钟总线上,其一个
时间片
的长度由PCLK1频率和CAN分频参数决定,假设PCLK1频率为25MHz,CAN分频参数位5,则一个时间片的长度为5/25Mhz=0.0000002s=0.0002ms=0.2us=200ns
CAN网络上一个节点采集一个位数据的时序叫做
位时序
,位时序由同步段(SYNC_SEG)、位段 1(BS1)和位段2(BS2)三段组成,其中同步段固定为一个时间片,在该段总线上应该发生一次位信号的跳变;位段1定义了采样点的位置,其可以是1-16个时间片;位段2定义了发送点的位置,其可以是1-8个时间片;
除了上面几个可调节的参数外,CAN还有一个再同步跳转宽度(SJW)参数可以调节,其取值可以是1-4个时间片,通过调节该参数长短决定了CAN再同步时自动调节位段1和位段2长度缩短/加长的上限,此处笔者未深究该参数
通过调节
时钟分频、位段1、位段2和再同步跳转宽度SJW
四个参数,就可以确定CAN通信的波特率,如下图所示位位序数的结构图
(注释1)
3.0.3、CAN帧格式
CAN通信过程中共有数据帧、遥控帧、错误帧、过载帧和帧间空间五种不同用途的帧
,其中数据帧和遥控帧又有标准格式的帧和扩展格式的帧两种,数据帧可以理解为CAN网络上的节点发送消息ID+要发送的数据,遥控帧可以理解为CAN网络上某个节点需要另外一个节点的数据,收到遥控帧的节点就发送对应的数据给请求数据的节点,这里不详细介绍每个帧的格式,想要知道具体帧格式的读者请阅读其他文章,如下图所示为CAN数据/遥控帧一帧的结构图
(注释1)
在HAL库中有一个CAN发送消息头结构体CAN_TxHeaderTypeDef,以下为结构体内主要定义参数
- IDE(帧格式):可选参数CAN_ID_STD(标准格式帧)和CAN_ID_EXT(扩展格式帧)
- StdId(标准格式帧ID):可选值范围0-0x7FF(11位)
- ExtId(扩展格式帧ID):可选值范围0-0x1FFF FFFF(29位,其中标准11位+扩展18位)
- RTR(帧类型):可选参数CAN_RTR_DATA(数据帧)和CAN_RTR_REMOTE(遥控帧)
- DLC(发送数据的长度):可选值范围0-8
- TransmitGlobalTime(传输时间戳使能):ENABLE/DISABLE
3.0.4、CAN验收筛选器
CAN网络上的所有节点没有地址的概念,因此当某个节点发送了特定ID的一条数据帧的时候,所有节点都会收到该帧消息,但是该帧应该只被需要接收该帧的节点接收,而其他不需要接收该数据帧的节点应该自动筛除掉该消息,减少资源浪费,
那一个节点如何判断是否应该接收该帧呢?
配置CAN控制器的验收筛选器
,STM32F407的CAN提供了28个可调整/可配置的标识符筛选器组,注意CAN1/2共用这个标识符筛选器组,而且CAN2不能够单独直接配置,需要使用CAN1来配置,这里的筛选功能为硬件筛选功能,无需软件筛选,可以节省软件筛选所需的CPU资源
筛选器可配置为掩码模式或标识符列表模式
,在掩码模式下,标识符寄存器与掩码寄存器关联,用以指示标识符的哪些位“必须匹配”,哪些位“无关”
在HAL库中有一个CAN过滤器配置结构体CAN_FilterTypeDef用于配置筛选器,以下为结构体内主要定义参数
- FilterMode(筛选器模式):可选参数CAN_FILTERMODE_IDMASK(掩码模式)和CAN_FILTERMODE_IDLIST(标识符列表模式)
- FilterBank(筛选器组):指定将初始化的筛选器组,单CAN时参数范围0-13,双CAN时参数范围0-27
- FilterFIFOAssignment(分配给筛选器的FIFO):指定分配给过筛选器的FIFO0/1,可选参数CAN_FILTER_FIFO0(FIFO0)和CAN_FILTER_FIFO1(FIFO1)
- FilterScale(筛选器宽度):CAN_FILTERSCALE_16BIT(两个16位)和CAN_FILTERSCALE_32BIT(一个32位)
- FilterActivation(筛选器使能):CAN_FILTER_DISABLE(不使能)和CAN_FILTER_ENABLE(使能)
- FilterIdHigh(CAN_FxR1 的高16位):在32位的屏蔽位模式下,用于指定这些位的标准值
- FilterIdLow(CAN_FxR1 的低16位):在32位的屏蔽位模式下,用于指定这些位的标准值
- FilterMaskIdHigh(CAN_FxR2的高16位):在32位的屏蔽位模式下,用于指定需要关心哪些位
- FilterMaskIdLow(CAN_FxR2的低16位):在32位的屏蔽位模式下,用于指定需要关心哪些位
这一部分很重要,不配置筛选器则CAN不能正常接收数据,具体配置请阅读本实验3.2.3小节程序,这里笔者自认为讲的不是很到位,读者可以阅读“
STM32 CAN 过滤器、滤波屏蔽器配置总结
”文章
3.0.5、CAN工作模式
CAN1/2有工作模式和测试模式两种模式,工作模式中又包括初始化模式、正常模式和睡眠模式三种,测试模式中包括静默模式、环回模式和环回与静默组合模式三种,测试模式主要用于测试单个CAN是否工作正常,本实验主要实现双机通信,因此CAN工作在正常模式即可,由于内容太多这里不再详细介绍,具体内容可参考STM32F4xx 参考手册 RM009,如下图所示为测试模式下结构示意图
(注释1)
3.0.6、CAN发送和接收流程
CAN1/2均有3个邮箱可以发送数据,当用户发送数据时只需要
利用CAN_TxHeaderTypeDef结构体生成要发送的帧(具体可阅读本实验3.0.3小节),然后使用HAL_CAN_AddTxMessage()函数将生成的帧添加到邮箱即可
,如果此时有空闲邮箱那么邮箱就会被挂起,当该邮箱具有最高优先级的时候就会安排发送出去,发送出去的过程完全由硬件实现,用户只需按要求生成帧,然后放入空闲邮箱即可,如下图所示为发送邮箱状态流程图
(注释1)
CAN1/2均有两个接收FIFO,每个接收FIFO都有三级深度,通俗理解就是有三个邮箱,可以接收三条信息,当发送的信息通过某个CAN网络上的节点验收筛选器成为一条有效信息时,那么该消息就会被该节点的CAN接收FIFO接收,同时该FIFO的接收0会被挂起,如果持续收到消息,该FIFO的接收1/2也会被挂起,直到三个邮箱全部用完,如果开启了CAN RX接收中断,那么当FIFO接收0/1/2/被挂起时会进入对应的中断服务回调函数中,
当使用HAL_CAN_GetRxMessage()函数读取掉接收FIFO0/1的某级深度的消息时,该级别邮箱将会被释放
,方便接收下一条消息,如下图所示为接收FIFO状态流程图
(注释1)
值得提醒的是,
在本实验中由于笔者使用了两个一摸一样的开发板,因此下面配置的一套程序可以直接烧录到两个开发板上就可以通信
,但是如果读者使用了不一样的开发板,每个板子的配置流程和下述流程一模一样,但请注意调节时钟树和CAN的参数配置时将两个开发板的CAN通信波特率调节为一致,两个开发板的连接应该如下图所示连接
(注释4)
另外就是尽量使用CAN1作为通信CAN,因为在STM32F407的两个CAN中CAN1为主CAN,本实验使用CAN2可能在接收信息时存在问题(大概率笔者没有彻底了解到应该对CAN2如何正确配置),读者可以自行尝试
3.1、CubeMX相关配置
3.1.0、工程基本配置
打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示
开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示
详细工程建立内容读者可以阅读“
STM32CubeMX教程1 工程建立
”
3.1.1、时钟树配置
本实验时钟树建议按照下图所示将MCU时钟频率配置为100MHz,APB1时钟频率配置为25MHz,这样设置是为了能够得到一个整数的时间片,当然也可以和之前的实验类似,将所有总线频率均设置为最高频率
3.1.2、外设参数配置
本实验需要初始化开发板上KEY2用户按键做普通输入,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应
”
本实验需要需要初始化USART2作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信
”
单击Pinout & Configuration页面左边Connectivity/CAN1,在Mode中勾选Activated激活CAN1,在其下方的参数配置栏目中按照图示参数配置即可,位时序参数详解可以阅读本实验“3.0.2、CAN位时序和波特率”小节
3.1.3、外设中断配置
在Pinout & Configuration页面左边System Core/NVIC中
勾选CAN1 TX interrupts和CAN1 RX0 interrupts发送接收两个中断
,然后选择合适的中断优先级即可,具体如配置下图所示
3.2、生成代码
3.2.0、配置Project Manager页面
单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示
详细Project Manager配置内容读者可以阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节
3.2.1、外设初始化调用流程
在生成的工程代码中新增加了MX_CAN1_Init()函数,该函数对CAN1的参数进行了配置,并调用了CAN初始化函数HAL_CAN_Init()
在该CAN初始化函数HAL_CAN_Init()中调用了HAL_CAN_MspInit()函数对外设CAN1所需要的时钟使能,引脚复用和中断进行了配置
CAN1具体初始化调用流程如下图所示
3.2.2、外设中断调用流程
在STM32CubeMX中勾选CAN1的TX中断和RX0中断后,会在生成的工程代码stm32f4xx_it.c中新增CAN1_TX_IRQHandler()和CAN1_RX0_IRQHandler()中断服务函数
这两个中断服务函数均调用了HAL库的CAN中断统一处理函数HAL_CAN_IRQHandler(),在该函数中
当CAN1邮箱0发送完成消息后会调用HAL_CAN_TxMailbox0CompleteCallback()函数,当CAN1FIFO0消息挂起时会调用HAL_CAN_RxFifo0MsgPendingCallback()函数
,这两个函数均为虚函数,需要用户重新实现
CAN1接收/发送中断具体调用流程如下图所示
3.2.3、添加其他必要代码
在can.c中添加FIFO0的消息筛选器函数CAN_SetFilters(),然后添加CAN发送数据测试函数CAN1_Send_Test(),具体源代码如下所示
(注释3)
//设置筛选器,要在完成CAN初始化之后调用此函数
HAL_StatusTypeDef CAN_SetFilters(void)
{
CAN_FilterTypeDef canFilter; //筛选器结构体变量
// Configure the CAN Filter
canFilter.FilterBank = 0; //筛选器组编号
canFilter.FilterMode = CAN_FILTERMODE_IDMASK; //ID掩码模式
canFilter.FilterScale = CAN_FILTERSCALE_32BIT; //32位长度
//设置1:接收所有帧
// canFilter.FilterIdHigh = 0x0000; //CAN_FxR1 的高16位
// canFilter.FilterIdLow = 0x0000; //CAN_FxR1 的低16位
// canFilter.FilterMaskIdHigh = 0x0000; //CAN_FxR2的高16位。所有位任意
// canFilter.FilterMaskIdLow = 0x0000; //CAN_FxR2的低16位,所有位任意
//设置2:只接收stdID为奇数的帧
canFilter.FilterIdHigh = 0x0020; //CAN_FxR1 的高16位
canFilter.FilterIdLow = 0x0000; //CAN_FxR1 的低16位
canFilter.FilterMaskIdHigh = 0x0020; //CAN_FxR2的高16位
canFilter.FilterMaskIdLow = 0x0000; //CAN_FxR2的低16位
canFilter.FilterFIFOAssignment = CAN_RX_FIFO0; //应用于FIFO0
canFilter.FilterActivation = ENABLE; //使用筛选器
canFilter.SlaveStartFilterBank = 14; //从CAN控制器筛选器起始的Bank
HAL_StatusTypeDef result=HAL_CAN_ConfigFilter(&hcan1, &canFilter);
return result;
}
/*CAN发送数据测试函数*/
void CAN1_Send_Test(uint32_t msgid, uint8_t *data)
{
TxMessage.IDE = CAN_ID_STD; //设置ID类型
TxMessage.StdId = msgid; //设置ID号
TxMessage.RTR = CAN_RTR_DATA; //设置传送数据帧
TxMessage.DLC = 4; //设置数据长度
if(HAL_CAN_AddTxMessage(&hcan1, &TxMessage, data, &TxMailbox) != HAL_OK)
{
printf("CAN send test data fail!\r\n");
Error_Handler();
}
}
在can.c中重新实现CAN接收/发送中断处理函数,具体源代码如下所示
/*CAN接收FIFO0挂起中断处理函数*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
uint8_t data[8];
HAL_StatusTypeDef status;
if(hcan == &hcan1)
{
status = HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxMessage, data);
if (HAL_OK == status)
{
printf("--->Data Receieve!\r\n");
printf("RxMessage.StdId is %#x\r\n", RxMessage.StdId);
printf("data[0] is 0x%02x\r\n", data[0]);
printf("data[1] is 0x%02x\r\n", data[1]);
printf("data[2] is 0x%02x\r\n", data[2]);
printf("data[3] is 0x%02x\r\n", data[3]);
printf("<---\r\n");
}
}
}
/*CAN发送完成中断处理函数*/
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan)
{
printf("--->Into TxMailbox0CompleteCallback Function!\r\n");
printf("--->CAN send test data success!\r\n\r\n");
}
在main.c主函数中设置CAN接收筛选器,启动CAN,使能中断,然后再主循环中实现按键控制,每当按键KEY2按下时就调用CAN1_Send_Test()函数发送数据
具体源代码如下所示
/*主循环外程序*/
printf("----- CAN Test Board #1 -----\r\n");
//设置筛选器
if (CAN_SetFilters() == HAL_OK)
printf("ID Filter: Only Odd IDs\r\n");
//启动CAN1模块
if (HAL_CAN_Start(&hcan1) == HAL_OK)
printf("CAN is started\r\n");
//启用CAN发送/接收中断
if(HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING | CAN_IT_TX_MAILBOX_EMPTY) != HAL_OK)
{
printf("CAN_IT_RX_FIFO0_MSG_PENDING Enable Fail\r\n");
Error_Handler();
}
uint32_t msg_id=0;
uint8_t data[4] = {0x01, 0x02, 0x03, 0x04};
/*主循环内程序*/
/*按键KEY2按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
HAL_Delay(50);
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
for(uint16_t i =0;i<4;i++)
data[i]++;
CAN1_Send_Test(msg_id++,data);
while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
}
}
最后将在can.c中定义的消息筛选器函数、发送数据测试函数在can.h中声明即可,具体源代码如下所示
/*can.h中函数声明*/
void CAN1_Send_Test(uint32_t msgid, uint8_t *data);
HAL_StatusTypeDef CAN_SetFilters(void);
4、常用函数
/*CAN开始通信*/
HAL_StatusTypeDef HAL_CAN_Start(CAN_HandleTypeDef *hcan)
/*CAN停止通信*/
HAL_StatusTypeDef HAL_CAN_Stop(CAN_HandleTypeDef *hcan)
/*获取当前空闲邮箱数量*/
uint32_t HAL_CAN_GetTxMailboxesFreeLevel(const CAN_HandleTypeDef *hcan)
/*请求发送相应的邮箱内容*/
HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, const CAN_TxHeaderTypeDef *pHeader,const uint8_t aData[], uint32_t *pTxMailbox)
/*获取FIFO中挂起的消息数*/
uint32_t HAL_CAN_GetRxFifoFillLevel(const CAN_HandleTypeDef *hcan, uint32_t RxFifo)
/*读取FIFI中挂起的消息信息并释放邮箱*/
HAL_StatusTypeDef HAL_CAN_GetRxMessage(CAN_HandleTypeDef *hcan, uint32_t RxFifo,CAN_RxHeaderTypeDef *pHeader, uint8_t aData[])
/*CAN发送完成中断处理函数*/
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan)
/*CAN接收中断处理函数*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
5、烧录验证
烧录程序,开发板1/2上电后均显示CAN初始化成功可以开始通信
当第一次按下开发板1的KEY2按键,此时串口输出进入CAN发送完成中断处理函数中并成功发送信息的提示,但是开发板2并没有接收消息(因为开发板1/2均设置了只接收stdID为奇数的帧)
当第二次按下开发板1的KEY2按键时,可以发现开发板2收到了消息,并将接收到的消息打印了出来,具体实验现象如下图所示(左边为开发板1,右边为开发板2)
6、注释详解
注释1
:图片来源 STM32F4xx 中文参考手册 RM009
注释2
:图片来源
通信——CAN总线基础介绍
注释3
:在CAN发送测试函数末尾不要使用printf输出,如果非要使用请在使用前进行1ms延时,否则可能进不去发送完成函数HAL_CAN_TxMailbox0CompleteCallback中
注释4
:图片来源
STM32CubeMX | 36 - 使用CAN总线进行双板通信(TJA1050)
参考资料
更多内容请浏览
STM32CubeMX+STM32F4系列教程文章汇总贴