CanFestival移植到STM32 F4芯片(基于HAL库)

发布时间:2026/7/21 16:46:01
CanFestival移植到STM32 F4芯片(基于HAL库) 本文讲述如何通过简单操作就可以把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那一端就可以了。PSPCAN-View可以去PeakCAN官网下载是免费软件CAN线得是双绞线H对HL对L进行连接。二 搭建CAN基础工程硬件搭建好之后先搭建一个CAN的基础工程验证是否可以收发CAN消息为后续移植打下基础。这里使用STM32CubeMX来操作。打开STM32CubeMX芯片选择STM32F407ZGT6选好后创建工程。配置时钟在新界面里的Pinout Configuration里选择RCC然后高速时钟和低速时钟都选择Crystal/Ceramic ResonatorPS启明欣欣的板子其外部接了2个晶振一个8M一个32.768K前者用于高速时钟后者用于低速时钟如果低速时钟没有外接晶振那么就选disable。此时点击Clock Configuration然后按照红框里的步骤一个个更改第4步输入频率168后回车CubeMX会自动调整非常方便。另外可以看到低速时钟LSE的输入频率是32.768K这个也可以修改需要结合实际。这里要注意一下配置完毕后APB1外设时钟是42MAPB1定时器时钟是84M后面要用到。设置SYS回到Pinout Configuration然后点击SYS右侧的选项按照如下进行选择PS由于没有使用操作系统所以这里的Timebase Source选择SysTick开启CAN2启明欣欣的CAN2对应的是PB12和PB13其中PB12是RXPB13是TX先在Pinout view里右下角的搜索框中输入PB12然后回车就可以找到PB12会闪烁点击该引脚功能选择CAN2_RX同理找到PB13将其设置为CAN2_TX设置OK后会发现CAN2已经自动使能了。这里设置一下CAN的通信速率本教程使用500K设置如上图红框Prescaler设置为6Time Quanta in Bit Segment 1设置为7Time Quanta in Bit Segment 2设置为6ReSynchronization Jump Width设置为1经过上述设置后CAN的波特率就变成500K了。设置原理前面配置时钟时APB1的外设时钟是42M而CAN是属于APB1的外设这样经过Prescaler的变频后就变成42M/67M也就是7000K然后除以(Time Quanta in Bit Segment 1Time Quanta in Bit Segment 2ReSynchronization Jump Width* 1)也就是7000K/14500K芯片手册上的波特率的计算原理如下可以看出上述配置并不是唯一选择只要根据原理让波特率是500K就可以了。最后是开启CAN2的接收中断如下红框勾选CAN2 RX0中断PSRX0和RX1分别对应2个内部接收FIFO这里选择FIFO0后面会讲到如何设置生成Keil工程配置完毕最后生成工程。点击Project Manager然后在Project里对红框标注的地方进行自定义修改接着是Code Generator按照如下勾选也可以根据自己需要进行修改设置完毕后点击右上角的GENERATE CODE来生成Keil工程PSKeil现在有社区版个人使用是免费的不用去破解了。验证CAN通信打开生成的Keil工程然后先做以下配置编译器使用V6版本如果使用的是老版的Keil可能还得用V5版本的编译器取消勾选Browse Information节约编译时间编译优化选项选择O1或O0优化选项过高可能会造成调试时无法打断点配置好之后编译一下保证工程可以顺利编译完成。剩下是修改代码主要参考这篇文章不过该作者用的CAN1这里再写一遍在USER CODE BEGIN 4下面添加函数CANFilter_Config()用于配置CAN2的过滤器voidCANFilter_Config(void){CAN_FilterTypeDef sFilterConfig;sFilterConfig.FilterBank0;// CAN过滤器编号范围0-27sFilterConfig.FilterModeCAN_FILTERMODE_IDMASK;// CAN过滤器模式掩码模式或列表模式这里选择掩码模式sFilterConfig.FilterScaleCAN_FILTERSCALE_32BIT;// CAN过滤器尺度16位或32位这里选择32位sFilterConfig.FilterIdHigh0x0000;// 32位下存储要过滤ID的高16位sFilterConfig.FilterIdLow0x0000;// 32位下存储要过滤ID的低16位sFilterConfig.FilterMaskIdHigh0x0000;// 掩码模式下存储的是过滤器掩码的高16位sFilterConfig.FilterMaskIdLow0x0000;// 掩码模式下存储的是过滤器掩码的低16位sFilterConfig.FilterFIFOAssignmentCAN_FILTER_FIFO0;// 报文通过过滤器的匹配后存储到哪个FIFO这里选择FIFO0sFilterConfig.FilterActivationCAN_FILTER_ENABLE;// 激活过滤器sFilterConfig.SlaveStartFilterBank0;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.IDECAN_ID_STD;// 设置ID类型标准帧还是扩展帧这里选择标准帧TxMessage.StdId0x111;// 设置ID号TxMessage.RTRCAN_RTR_DATA;// 设置传输数据帧TxMessage.DLC4;// 设置数据长度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(hcanhcan2){statusHAL_CAN_GetRxMessage(hcan,CAN_RX_FIFO0,RxMessage,data);if(HAL_OKstatus){;// 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_00RPDO2映射0x2001_00RPDO3映射0x2002_00RPDO4映射0x2003_00TPDO1映射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的为NodeId0x200的报文就会把报文里包含的值存入对应的对象里。设备的NodeId设置后面会在main.c里通过参数设置。同理按照规划配置RPDO2RPDO3RPDO4需要注意RPDO2 COB IDNodeId0x300RPDO3 COB IDNodeId0x400RPDO4 COB IDNodeId0x500当然这些COB ID是可以随意修改的这里只是按照推荐的ID来设置配置TPDO1设置TPDO1的映射该对象索引是0x1A00如下对应0x3000_00和0x3004_00设置TPDO1的通信参数该对象索引是0x1800如下COB ID是nodeid0x180传输类型是254表示是事件触发或定时触发这里使用定时触发定时时间是500ms同理按照规划配置TPDO2TPDO3TPDO4需要注意TPDO2 COB IDNodeId0x280传输类型254定时时间1000msTPDO3 COB IDNodeId0x380传输类型254定时时间1500msTPDO4 COB IDNodeId0x480传输类型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-1839最后打开定时器中断移植首先使用git clone下载CanFestivalgitclone 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 ------------------------------------------------------------------*/#includemain.h#includecan.h#includetim.h#includegpio.h/* Private includes ----------------------------------------------------------*//* USER CODE BEGIN Includes */#includecanfestival.h#includeslavedic.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.OscillatorTypeRCC_OSCILLATORTYPE_HSE;RCC_OscInitStruct.HSEStateRCC_HSE_ON;RCC_OscInitStruct.PLL.PLLStateRCC_PLL_ON;RCC_OscInitStruct.PLL.PLLSourceRCC_PLLSOURCE_HSE;RCC_OscInitStruct.PLL.PLLM4;RCC_OscInitStruct.PLL.PLLN168;RCC_OscInitStruct.PLL.PLLPRCC_PLLP_DIV2;RCC_OscInitStruct.PLL.PLLQ4;if(HAL_RCC_OscConfig(RCC_OscInitStruct)!HAL_OK){Error_Handler();}/** Initializes the CPU, AHB and APB buses clocks */RCC_ClkInitStruct.ClockTypeRCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSourceRCC_SYSCLKSOURCE_PLLCLK;RCC_ClkInitStruct.AHBCLKDividerRCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDividerRCC_HCLK_DIV4;RCC_ClkInitStruct.APB2CLKDividerRCC_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.FilterBank0;// CAN过滤器编号范围0-27sFilterConfig.FilterModeCAN_FILTERMODE_IDMASK;// CAN过滤器模式掩码模式或列表模式这里选择掩码模式sFilterConfig.FilterScaleCAN_FILTERSCALE_32BIT;// CAN过滤器尺度16位或32位这里选择32位sFilterConfig.FilterIdHigh0x0000;// 32位下存储要过滤ID的高16位sFilterConfig.FilterIdLow0x0000;// 32位下存储要过滤ID的低16位sFilterConfig.FilterMaskIdHigh0x0000;// 掩码模式下存储的是过滤器掩码的高16位sFilterConfig.FilterMaskIdLow0x0000;// 掩码模式下存储的是过滤器掩码的低16位sFilterConfig.FilterFIFOAssignmentCAN_FILTER_FIFO0;// 报文通过过滤器的匹配后存储到哪个FIFO这里选择FIFO0sFilterConfig.FilterActivationCAN_FILTER_ENABLE;// 激活过滤器sFilterConfig.SlaveStartFilterBank0;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.IDECAN_ID_STD;// 设置ID类型标准帧还是扩展帧这里选择标准帧TxMessage.StdId0x111;// 设置ID号TxMessage.RTRCAN_RTR_DATA;// 设置传输数据帧TxMessage.DLC4;// 设置数据长度if(HAL_CAN_AddTxMessage(hcan2,TxMessage,data,TxMailbox)!HAL_OK){Error_Handler();}}voidHAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef*hcan){if(hcanhcan2){handleCANPendingMessage();}}voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){if(htimhtim3){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写的值了同理可以测试TPDO234然后通过PcanView发一个RPDO1给板子CAN总线监控发完之后通过SDO去读取0x2000_00, 0x2004_00的值如下可以看出与RPDO1发出的值相等同理可以测试RPDO234四 总结本文讲述了如何把CanFestival移植到STM32 F4系列芯片上由于本人优化了CanFestival库所以移植起来比较简单。了解过程后对于master或移植到CM3、CM0都很容易了。