本文讲述如何通过简单操作就可以把CanFestival库移植到STM32 F4芯片上,作为Slave设备。使用启明欣欣的工控板来做实验。
获取本文工程源码包,可以执行
gitclone https://github.com/happybruce/CanFestival.gitcdCanFestivalgitcheckout develop然后在examples\CM4\里即可找到,名字为My_CAN_Test.zip
一 硬件连接
观察CAN报文需要专门的设备,本人从某宝上买了一个兼容PCAN的开源小板子,二十几块钱,通过USB接到电脑后就可以使用PCAN-View打开进行监控,非常方便,同时还带开关控制的120欧姆终端电阻,缺点是有时不太稳定,
这个小板子还需要通过CAN线和启明欣欣板子上的CAN口进行连接,如下图,这里连接的是CAN2,
工控板需要单独供电,不能依靠下载器的电,否则无法正常运行。最后打开小板子上的120欧姆电阻,开关拨到ON那一端就可以了。
PS:PCAN-View可以去PeakCAN官网下载,是免费软件;CAN线得是双绞线,H对H,L对L进行连接。
二 搭建CAN基础工程
硬件搭建好之后,先搭建一个CAN的基础工程,验证是否可以收发CAN消息,为后续移植打下基础。这里使用STM32CubeMX来操作。
打开STM32CubeMX,芯片选择STM32F407ZGT6,选好后创建工程。
配置时钟
在新界面里的Pinout & Configuration里选择RCC,然后高速时钟和低速时钟都选择Crystal/Ceramic Resonator
PS:启明欣欣的板子,其外部接了2个晶振,一个8M,一个32.768K,前者用于高速时钟,后者用于低速时钟,如果低速时钟没有外接晶振,那么就选disable。
此时点击Clock Configuration,
然后按照红框里的步骤一个个更改,
第4步输入频率168后回车,CubeMX会自动调整,非常方便。另外,可以看到低速时钟LSE的输入频率是32.768K,这个也可以修改,需要结合实际。
这里要注意一下:配置完毕后APB1外设时钟是42M,APB1定时器时钟是84M,后面要用到。
设置SYS
回到Pinout & Configuration,然后点击SYS,右侧的选项按照如下进行选择,
PS:由于没有使用操作系统,所以这里的Timebase Source选择SysTick
开启CAN2
启明欣欣的CAN2对应的是PB12和PB13,其中PB12是RX,PB13是TX,先在Pinout view里右下角的搜索框中输入PB12,然后回车,就可以找到PB12,会闪烁,
点击该引脚,功能选择CAN2_RX,
同理找到PB13,将其设置为CAN2_TX,设置OK后会发现CAN2已经自动使能了。
这里设置一下CAN的通信速率,本教程使用500K,设置如上图红框,
- Prescaler设置为6
- Time Quanta in Bit Segment 1设置为7
- Time Quanta in Bit Segment 2设置为6
- ReSynchronization Jump Width设置为1
经过上述设置后,CAN的波特率就变成500K了。
设置原理:前面配置时钟时,APB1的外设时钟是42M,而CAN是属于APB1的外设,这样经过Prescaler的变频后就变成42M/6=7M,也就是7000K,然后除以(Time Quanta in Bit Segment 1+Time Quanta in Bit Segment 2+ReSynchronization Jump Width* 1),也就是7000K/14=500K
芯片手册上的波特率的计算原理如下,
可以看出上述配置并不是唯一选择,只要根据原理让波特率是500K就可以了。
最后是开启CAN2的接收中断,如下红框,勾选CAN2 RX0中断,
PS:RX0和RX1分别对应2个内部接收FIFO,这里选择FIFO0,后面会讲到如何设置
生成Keil工程
配置完毕,最后生成工程。
点击Project Manager,然后在Project里对红框标注的地方进行自定义修改
接着是Code Generator,按照如下勾选,也可以根据自己需要进行修改
设置完毕后点击右上角的GENERATE CODE来生成Keil工程
PS:Keil现在有社区版,个人使用是免费的,不用去破解了。
验证CAN通信
打开生成的Keil工程,然后先做以下配置,
编译器使用V6版本
如果使用的是老版的Keil,可能还得用V5版本的编译器
取消勾选Browse Information,节约编译时间
编译优化选项选择O1或O0,优化选项过高可能会造成调试时无法打断点
配置好之后,编译一下,保证工程可以顺利编译完成。
剩下是修改代码,主要参考这篇文章,不过该作者用的CAN1,这里再写一遍,在USER CODE BEGIN 4下面添加函数CANFilter_Config(),用于配置CAN2的过滤器,
voidCANFilter_Config(void){CAN_FilterTypeDef sFilterConfig;sFilterConfig.FilterBank=0;// CAN过滤器编号,范围0-27sFilterConfig.FilterMode=CAN_FILTERMODE_IDMASK;// CAN过滤器模式,掩码模式或列表模式,这里选择掩码模式sFilterConfig.FilterScale=CAN_FILTERSCALE_32BIT;// CAN过滤器尺度,16位或32位,这里选择32位sFilterConfig.FilterIdHigh=0x0000;// 32位下,存储要过滤ID的高16位sFilterConfig.FilterIdLow=0x0000;// 32位下,存储要过滤ID的低16位sFilterConfig.FilterMaskIdHigh=0x0000;// 掩码模式下,存储的是过滤器掩码的高16位sFilterConfig.FilterMaskIdLow=0x0000;// 掩码模式下,存储的是过滤器掩码的低16位sFilterConfig.FilterFIFOAssignment=CAN_FILTER_FIFO0;// 报文通过过滤器的匹配后,存储到哪个FIFO,这里选择FIFO0sFilterConfig.FilterActivation=CAN_FILTER_ENABLE;// 激活过滤器sFilterConfig.SlaveStartFilterBank=0;if(HAL_CAN_ConfigFilter(&hcan2,&sFilterConfig)!=HAL_OK){Error_Handler();}}选择FilterBank 0,配置成掩码模式,选择FIFO0来接收CAN报文(这个和前面选择RX0中断保持一致,如果使用FIFO1,那么前面就要勾选RX1中断)。掩码ID是全0,表示允许接收所有报文,芯片手册介绍如下,
PS:如果只想接收指定报文,那么就要配置掩码ID为非0,具体可以参考芯片手册。
然后添加CAN2的使能函数,
voidCAN_Start_Init(void){if(HAL_CAN_Start(&hcan2)!=HAL_OK){Error_Handler();}if(HAL_CAN_ActivateNotification(&hcan2,CAN_IT_RX_FIFO0_MSG_PENDING)!=HAL_OK){Error_Handler();}}还有发送函数,这个用来测试发送,
voidCAN2_Send_Test(void){uint32_tTxMailbox;uint8_tdata[4]={0x01,0x02,0x03,0x04};CAN_TxHeaderTypeDef TxMessage;TxMessage.IDE=CAN_ID_STD;// 设置ID类型,标准帧还是扩展帧,这里选择标准帧TxMessage.StdId=0x111;// 设置ID号TxMessage.RTR=CAN_RTR_DATA;// 设置传输数据帧TxMessage.DLC=4;// 设置数据长度if(HAL_CAN_AddTxMessage(&hcan2,&TxMessage,data,&TxMailbox)!=HAL_OK){Error_Handler();}}最后是CAN2接收中断的回调函数,用来测试接收,
voidHAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef*hcan){uint8_tdata[8]={0,0,0,0,0,0,0,0};HAL_StatusTypeDef status;CAN_RxHeaderTypeDef RxMessage;if(hcan==&hcan2){status=HAL_CAN_GetRxMessage(hcan,CAN_RX_FIFO0,&RxMessage,data);if(HAL_OK==status){;// to do something}}}添加好之后,在USER CODE BEGIN PFP下添加函数声明,接收中断的回调函数不用声明,
/* USER CODE BEGIN PFP */voidCANFilter_Config(void);voidCAN_Start_Init(void);voidCAN2_Send_Test(void);/* USER CODE END PFP */然后在main函数里调用这些函数,每隔1s发送一次CAN报文,
再次编译并烧录到板子里,然后重启运行,此时在PCAN-View上可以看到CAN报文,说明发送没问题,
接着验证接收,此时先进入debug模式,然后在CAN接收中断里打个断点,
使用PCAN-View来创建一条随意报文,数据是0x9988,
然后点击OK进行保存,最后选中创建的报文按空格进行发送,此时会进入断点,
把data数组添加到Keil的Watch窗口中,然后单步走一步,可以看到data数组里有数据了,如下,正是之前发送的报文数据,
三 移植CanFestival
有了第二节的基础工程后,本节讲述如何移植CanFestival,适用于STM32 F4系列芯片。
对象字典的配置
首先规划一下Slave设备的对象字典,具体如下,
- 每隔3s发送一次心跳报文
- 添加对象:0x2000_00 (u32类型), 0x2001_00 (i16类型), 0x2002_00 (u8类型), 0x2003_00 (u8类型),0x2004_00 (u16类型)
- 添加对象:0x3000_00 (u32类型), 0x3001_00 (u16类型), 0x3002_00 (u8类型), 0x3003_00 (u8类型),0x3004_00 (u16类型)
- RPDO1:映射0x2000_00和0x2004_00
- RPDO2:映射0x2001_00
- RPDO3:映射0x2002_00
- RPDO4:映射0x2003_00
- TPDO1:映射0x3000_00和0x3004_00,每隔500ms发一次
- TPDO2:映射0x3001_00,每隔1000ms发一次
- TPDO3:映射0x3002_00,每隔1500ms发一次
- TPDO4:映射0x3003_00,每隔2000ms发一次
这里使用objdictgen来进行编辑,其地址是https://github.com/happybruce/objdictgen,经过本人优化后可以使用Python3启动。
PS:这个编辑器原本是在CanFestival里自带的,本人把其独立出来了。
双击objdictedit.py打开编辑器,然后在File下点击New,在弹出的界面里进行以下设置,
点击OK,进入新的主界面,然后根据下述步骤进行配置,
添加SDO Server
因为这个是slave,用户会发送SDO请求来访问,属于被访问者,所以它就是一个SDO server
配置心跳报文
接着在0x1000-0x1029里找到0x1017,将其值设置为3000 (其单位是毫秒)
添加对象
在0x2000-0x5FFF里添加之前规划的对象,总共10个
配置RPDO1
设置RPDO1的映射,该对象索引是0x1600,如下,对应0x2000_00, 0x2004_00
设置RPDO1的通信参数,该对象索引是0x1400,这里只配置COB ID和Inhibit Time (10ms)
当设备收到COB ID的为NodeId+0x200的报文,就会把报文里包含的值存入对应的对象里。设备的NodeId设置后面会在main.c里通过参数设置。
同理,按照规划配置RPDO2,RPDO3,RPDO4,需要注意:
- RPDO2 COB ID:NodeId+0x300
- RPDO3 COB ID:NodeId+0x400
- RPDO4 COB ID:NodeId+0x500
当然,这些COB ID是可以随意修改的,这里只是按照推荐的ID来设置
配置TPDO1
设置TPDO1的映射,该对象索引是0x1A00,如下,对应0x3000_00和0x3004_00
设置TPDO1的通信参数,该对象索引是0x1800,如下,
COB ID是nodeid+0x180,传输类型是254,表示是事件触发或定时触发,这里使用定时触发,定时时间是500ms
同理,按照规划配置TPDO2,TPDO3,TPDO4,需要注意:
- TPDO2 COB ID:NodeId+0x280,传输类型254,定时时间1000ms
- TPDO3 COB ID:NodeId+0x380,传输类型254,定时时间1500ms
- TPDO4 COB ID:NodeId+0x480,传输类型254,定时时间2000ms
这样对象字典就配置好了。最后保存工程,并生成对应的代码,点击File->Build Dictionary生成slavedic.c和slavedic.h
这2个文件暂时留着备用。
开启定时器
打开CubeMX工程,然后开启TIM3,时钟源选择内部时钟,预分频系数输入839,
为什么是839呢?CanFestival对定时器的要求是counter每隔10us加1,也就是100KHz,而前面配置时钟时定时器的时钟频率是84MHz,如果以100KHz为单位,那么84M就等于840x100K, 那么为了达到100KHz的频率,预分频系数就是840-1=839
最后打开定时器中断,
移植
首先使用git clone下载CanFestival,
gitclone https://github.com/happybruce/CanFestival.gitcdCanFestivalgitcheckout develop这个Github地址是本人的仓库,已经对代码进行了优化。
在工程目录Core下添加以下目录结构,
然后做以下拷贝,
把CanFestival/src目录下的红框标注的文件拷贝到Core/CanFestival/src/下,红叉的不要拷贝
把CanFestival/include目录下的以下文件拷贝到Core/CanFestival/include/下,注意包含cm4目录,
把CanFestival/drivers/cm4目录下的cm4.c拷贝到Core/CanFestival/drv下
把对象字典的配置这节生成的slavedic.c和slavedic.h拷贝到Core/CanFestival/slavedic下
拷贝完毕后打开Keil工程,点击以下按钮,
然后添加三个组及其对应的源文件,注意这里不需要添加头文件,后面给头文件设置搜索目录即可
添加完毕后点击OK,然后添加搜索目录,如下图2个步骤,
在弹出界面里填入以下目录,
最后点击OK,这样工程就配置好了。
main.c内容如下,
/* USER CODE BEGIN Header *//** ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** * @attention * * Copyright (c) 2026 STMicroelectronics. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** *//* USER CODE END Header *//* Includes ------------------------------------------------------------------*/#include"main.h"#include"can.h"#include"tim.h"#include"gpio.h"/* Private includes ----------------------------------------------------------*//* USER CODE BEGIN Includes */#include"canfestival.h"#include"slavedic.h"/* USER CODE END Includes *//* Private typedef -----------------------------------------------------------*//* USER CODE BEGIN PTD *//* USER CODE END PTD *//* Private define ------------------------------------------------------------*//* USER CODE BEGIN PD *//* USER CODE END PD *//* Private macro -------------------------------------------------------------*//* USER CODE BEGIN PM *//* USER CODE END PM *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV *//* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/voidSystemClock_Config(void);/* USER CODE BEGIN PFP */voidCANFilter_Config(void);voidCAN_Start_Init(void);voidCAN2_Send_Test(void);/* USER CODE END PFP *//* Private user code ---------------------------------------------------------*//* USER CODE BEGIN 0 *//* USER CODE END 0 *//** * @brief The application entry point. * @retval int */intmain(void){/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_CAN2_Init();MX_TIM3_Init();/* USER CODE BEGIN 2 */selectTimer(&htim3);selectCAN(&hcan2);// baud rate is not needed here, because it is set in CubeMXcanInit(&slavedic_ObjDictData,UINT32_MAX);HAL_TIM_Base_Start_IT(&htim3);CANFilter_Config();CAN_Start_Init();setNodeId(&slavedic_ObjDictData,1);// Set CANopen node id to be 1setState(&slavedic_ObjDictData,Initialisation);// Set NMT state to InitialisationsetState(&slavedic_ObjDictData,Pre_operational);// Set NMT state to Pre_operationalsetState(&slavedic_ObjDictData,Operational);// Set NMT state to Operational/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while(1){HAL_Delay(1000);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */}/** * @brief System Clock Configuration * @retval None */voidSystemClock_Config(void){RCC_OscInitTypeDef RCC_OscInitStruct={0};RCC_ClkInitTypeDef RCC_ClkInitStruct={0};/** Configure the main internal regulator output voltage */__HAL_RCC_PWR_CLK_ENABLE();__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);/** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */RCC_OscInitStruct.OscillatorType=RCC_OSCILLATORTYPE_HSE;RCC_OscInitStruct.HSEState=RCC_HSE_ON;RCC_OscInitStruct.PLL.PLLState=RCC_PLL_ON;RCC_OscInitStruct.PLL.PLLSource=RCC_PLLSOURCE_HSE;RCC_OscInitStruct.PLL.PLLM=4;RCC_OscInitStruct.PLL.PLLN=168;RCC_OscInitStruct.PLL.PLLP=RCC_PLLP_DIV2;RCC_OscInitStruct.PLL.PLLQ=4;if(HAL_RCC_OscConfig(&RCC_OscInitStruct)!=HAL_OK){Error_Handler();}/** Initializes the CPU, AHB and APB buses clocks */RCC_ClkInitStruct.ClockType=RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;RCC_ClkInitStruct.AHBCLKDivider=RCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDivider=RCC_HCLK_DIV4;RCC_ClkInitStruct.APB2CLKDivider=RCC_HCLK_DIV2;if(HAL_RCC_ClockConfig(&RCC_ClkInitStruct,FLASH_LATENCY_5)!=HAL_OK){Error_Handler();}}/* USER CODE BEGIN 4 */voidCANFilter_Config(void){CAN_FilterTypeDef sFilterConfig;sFilterConfig.FilterBank=0;// CAN过滤器编号,范围0-27sFilterConfig.FilterMode=CAN_FILTERMODE_IDMASK;// CAN过滤器模式,掩码模式或列表模式,这里选择掩码模式sFilterConfig.FilterScale=CAN_FILTERSCALE_32BIT;// CAN过滤器尺度,16位或32位,这里选择32位sFilterConfig.FilterIdHigh=0x0000;// 32位下,存储要过滤ID的高16位sFilterConfig.FilterIdLow=0x0000;// 32位下,存储要过滤ID的低16位sFilterConfig.FilterMaskIdHigh=0x0000;// 掩码模式下,存储的是过滤器掩码的高16位sFilterConfig.FilterMaskIdLow=0x0000;// 掩码模式下,存储的是过滤器掩码的低16位sFilterConfig.FilterFIFOAssignment=CAN_FILTER_FIFO0;// 报文通过过滤器的匹配后,存储到哪个FIFO,这里选择FIFO0sFilterConfig.FilterActivation=CAN_FILTER_ENABLE;// 激活过滤器sFilterConfig.SlaveStartFilterBank=0;if(HAL_CAN_ConfigFilter(&hcan2,&sFilterConfig)!=HAL_OK){Error_Handler();}}voidCAN_Start_Init(void){if(HAL_CAN_Start(&hcan2)!=HAL_OK){Error_Handler();}if(HAL_CAN_ActivateNotification(&hcan2,CAN_IT_RX_FIFO0_MSG_PENDING)!=HAL_OK){Error_Handler();}}voidCAN2_Send_Test(void){uint32_tTxMailbox;uint8_tdata[4]={0x01,0x02,0x03,0x04};CAN_TxHeaderTypeDef TxMessage;TxMessage.IDE=CAN_ID_STD;// 设置ID类型,标准帧还是扩展帧,这里选择标准帧TxMessage.StdId=0x111;// 设置ID号TxMessage.RTR=CAN_RTR_DATA;// 设置传输数据帧TxMessage.DLC=4;// 设置数据长度if(HAL_CAN_AddTxMessage(&hcan2,&TxMessage,data,&TxMailbox)!=HAL_OK){Error_Handler();}}voidHAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef*hcan){if(hcan==&hcan2){handleCANPendingMessage();}}voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){if(htim==&htim3){handleTimerPeriodElapsed();}}/* USER CODE END 4 *//** * @brief This function is executed in case of error occurrence. * @retval None */voidError_Handler(void){/* USER CODE BEGIN Error_Handler_Debug *//* User can add his own implementation to report the HAL error return state */__disable_irq();while(1){}/* USER CODE END Error_Handler_Debug */}#ifdefUSE_FULL_ASSERT/** * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None */voidassert_failed(uint8_t*file,uint32_tline){/* USER CODE BEGIN 6 *//* User can add his own implementation to report the file name and line number, ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) *//* USER CODE END 6 */}#endif/* USE_FULL_ASSERT */注意:
- 使用selectTimer()和selectCAN()来选择开启的外设,老代码是在库里写死的,这里优化了下,由用户来选择,更加灵活
- 使用setNodeId()来设置自身的node id
- 重写HAL_CAN_RxFifo0MsgPendingCallback(),里面调用CanFestival提供的处理函数,老代码是在库里写好的,这里改成在外部由用户填写
- 重写HAL_TIM_PeriodElapsedCallback(),里面调用CanFestival提供的处理函数,老代码是在库里写好的,这里改成在外部由用户填写
验证
最后编译,烧录到板子上运行,如下,可以看到TPDO1每隔500毫秒发一次,TPDO2每隔1000毫秒发一次,TPDO3每隔1500毫秒发一次,TPDO4每隔2000毫秒发一次,
由前面配置可知,TPDO1对应0x3000_00和0x3004_00,这里通过PcanView来发送SDO去修改对象0x3000_00的值,
发送完后,可以看出TPDO1发出的值就变了,变成我们通过SDO写的值了,同理可以测试TPDO2,3,4
然后通过PcanView发一个RPDO1给板子,
CAN总线监控,
发完之后通过SDO去读取0x2000_00, 0x2004_00的值,如下,可以看出与RPDO1发出的值相等
同理,可以测试RPDO2,3,4
四 总结
本文讲述了如何把CanFestival移植到STM32 F4系列芯片上,由于本人优化了CanFestival库,所以移植起来比较简单。
了解过程后,对于master或移植到CM3、CM0都很容易了。