目录
两个开发板(正点原子stm32f407探索者开发板V2.4)
STM32CubeMX软件(Version 6.10.0)
keil μVision5 IDE(MDK-Arm)
CH340G Windows系统驱动程序(CH341SER.EXE)
使用STM32CubeMX软件配置两台STM32F407开发板的CAN1模块实现双机通信
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通信是一种异步通信,异步通信的收发双方无时钟同步,因此需要确保收发双方发送/接收一帧数据的帧格式和波特率保持一致,这样才能保证收发双方正确的进行通信
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)
CAN通信过程中共有数据帧、遥控帧、错误帧、过载帧和帧间空间五种不同用途的帧,其中数据帧和遥控帧又有标准格式的帧和扩展格式的帧两种,数据帧可以理解为CAN网络上的节点发送消息ID+要发送的数据,遥控帧可以理解为CAN网络上某个节点需要另外一个节点的数据,收到遥控帧的节点就发送对应的数据给请求数据的节点,这里不详细介绍每个帧的格式,想要知道具体帧格式的读者请阅读其他文章,如下图所示为CAN数据/遥控帧一帧的结构图(注释1)
在HAL库中有一个CAN发送消息头结构体CAN_TxHeaderTypeDef,以下为结构体内主要定义参数
CAN网络上的所有节点没有地址的概念,因此当某个节点发送了特定ID的一条数据帧的时候,所有节点都会收到该帧消息,但是该帧应该只被需要接收该帧的节点接收,而其他不需要接收该数据帧的节点应该自动筛除掉该消息,减少资源浪费,那一个节点如何判断是否应该接收该帧呢?
配置CAN控制器的验收筛选器,STM32F407的CAN提供了28个可调整/可配置的标识符筛选器组,注意CAN1/2共用这个标识符筛选器组,而且CAN2不能够单独直接配置,需要使用CAN1来配置,这里的筛选功能为硬件筛选功能,无需软件筛选,可以节省软件筛选所需的CPU资源
筛选器可配置为掩码模式或标识符列表模式,在掩码模式下,标识符寄存器与掩码寄存器关联,用以指示标识符的哪些位“必须匹配”,哪些位“无关”
在HAL库中有一个CAN过滤器配置结构体CAN_FilterTypeDef用于配置筛选器,以下为结构体内主要定义参数
这一部分很重要,不配置筛选器则CAN不能正常接收数据,具体配置请阅读本实验3.2.3小节程序,这里笔者自认为讲的不是很到位,读者可以阅读“STM32 CAN 过滤器、滤波屏蔽器配置总结”文章
CAN1/2有工作模式和测试模式两种模式,工作模式中又包括初始化模式、正常模式和睡眠模式三种,测试模式中包括静默模式、环回模式和环回与静默组合模式三种,测试模式主要用于测试单个CAN是否工作正常,本实验主要实现双机通信,因此CAN工作在正常模式即可,由于内容太多这里不再详细介绍,具体内容可参考STM32F4xx 参考手册 RM009,如下图所示为测试模式下结构示意图(注释1)
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通信波特率调节为一致,两个开发板的连接应该如下图所示连接(注释)
另外就是尽量使用CAN1作为通信CAN,因为在STM32F407的两个CAN中CAN1为主CAN,本实验使用CAN2可能在接收信息时存在问题(大概率笔者没有彻底了解到应该对CAN2如何正确配置),读者可以自行尝试
请先阅读“STM32CubeMX教程1 工程建立”实验3.4.1小节配置RCC和SYS
本实验时钟树建议按照下图所示将MCU时钟频率配置为100MHz,APB1时钟频率配置为25MHz,这样设置是为了能够得到一个整数的时间片,当然也可以和之前的实验类似,将所有总线频率均设置为最高频率
本实验需要初始化开发板上KEY2用户按键做普通输入,具体配置步骤请阅读“STM32CubeMX教程3 GPIO输入 - 按键响应”
本实验需要需要初始化USART2作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”
单击Pinout & Configuration页面左边Connectivity/CAN1,在Mode中勾选Activated激活CAN1,在其下方的参数配置栏目中按照图示参数配置即可,位时序参数详解可以阅读本实验“3.0.2、CAN位时序和波特率”小节
在Pinout & Configuration页面左边System Core/NVIC中勾选CAN1 TX interrupts和CAN1 RX0 interrupts发送接收两个中断,然后选择合适的中断优先级即可,具体如配置下图所示
请先阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节配置Project Manager
单击页面右上角GENERATE CODE生成工程
在生成的工程代码中新增加了MX_CAN1_Init()函数,该函数对CAN1的参数进行了配置,并调用了CAN初始化函数HAL_CAN_Init()
在该CAN初始化函数HAL_CAN_Init()中调用了HAL_CAN_MspInit()函数对外设CAN1所需要的时钟使能,引脚复用和中断进行了配置
CAN1具体初始化调用流程如下图所示
在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接收/发送中断具体调用流程如下图所示
在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);
/*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)
“配置KEY2用户按键 -> 配置串口USART1 -> 激活CAN1并配置参数 -> 配置CAN1 TX/RX0中断 -> 生成的代码中实现筛选器配置 -> 实现CAN发送函数 -> 重新实现CAN发送 /接收中断函数 -> 主函数中启动CAN并激活中断 -> 主循环中实现按键控制CAN发送程序”
烧录程序,开发板1/2上电后均显示CAN初始化成功可以开始通信,当第一次按下开发板1的KEY2按键,此时串口输出进入CAN发送完成中断处理函数中并成功发送信息的提示,但是开发板2并没有接收消息(因为开发板1/2均设置了只接收stdID为奇数的帧),当第二次按下开发板1的KEY2按键时,可以发现开发板2收到了消息,并将接收到的消息打印了出来,具体实验现象如下图所示(左边为开发板1,右边为开发板2)
注释1:图片来源 STM32F4xx 中文参考手册 RM009
注释2:图片来源 通信——CAN总线基础介绍
注释3:在CAN发送测试函数末尾不要使用printf输出,如果非要使用请在使用前进行1ms延时,否则可能进不去发送完成函数HAL_CAN_TxMailbox0CompleteCallback中
注释4:图片来源?STM32CubeMX | 36 - 使用CAN总线进行双板通信(TJA1050)