
简介本资源是一套面向嵌入式开发初学者与物联网项目实践者的STM32F103单片机CAN总线通信实战例程聚焦工业现场常用的CAN收发功能实现适用于课程设计、毕业设计及小型工控节点开发。压缩包共174个文件含90个头文件定义外设寄存器、函数接口与配置宏、76个C源文件涵盖HAL库底层驱动、CAN初始化、中断接收、定时发送及错误处理逻辑另有构建脚本bat、工程配置uvprojx/uvoptx、固件镜像hex及说明文档txt整体体积仅1.01MB轻量易导入KEIL MDK环境。已有574人学习下载代码全程采用ST官方HAL库编写结构清晰、注释详尽关键引脚定义、时钟配置与CAN滤波器设置均在源码中标明配套keilkilll.bat一键清理工程残留并预置I2C、UART、SPI、ADC等常用外设驱动文件便于扩展多传感器接入。1. 项目背景与CAN总线入门最近在整理一个老项目的代码发现里面用到了STM32的CAN总线。当时为了快速实现直接用了标准库代码结构比较混乱移植和维护都挺麻烦。正好手头有个STM32F103C8T6的核心板就想着用现在主流的HAL库重新写一遍CAN的收发例程把整个流程理清楚。这个“STM32F103单片机HAL库例程-CAN口收发通讯实验”的压缩包就是我这次折腾的成果。如果你也在用CubeMX和HAL库想搞懂CAN总线怎么从零开始配置和收发数据那这篇笔记应该能帮到你。CAN总线在工业控制、汽车电子这些领域用得非常多因为它抗干扰能力强能实现多主机通信一根线上可以挂很多个设备。STM32F103这个经典的“蓝桥杯”芯片内置了bxCAN控制器功能挺全的。但说实话HAL库对CAN的封装层次比较高光看函数名有时候不太清楚底层到底干了啥配置项也多容易配错。这次我就从CubeMX配置开始一步步带你过一遍初始化、发送和接收的代码中间会穿插我实际调试时遇到的坑和解决办法。比如为什么过滤器配了却没收到数据发送函数卡住了怎么办这些细节文档里往往不会细说。2. 开发环境搭建与CubeMX工程创建工欲善其事必先利其器。第一步是把开发环境搭好。我用的主控是STM32F103C8T6也就是常说的“蓝色药丸”最小系统板。它自带了一个CAN控制器但注意芯片上的CAN引脚PA11, PA12需要外接一个CAN收发器芯片比如TJA1050才能连接到物理CAN总线上。软件方面你需要安装好Keil MDK-ARM或者IAR、STM32CubeIDE以及STM32CubeMX。CubeMX是ST官方出的图形化配置工具能自动生成HAL库的初始化代码大大节省了时间。打开CubeMX新建一个工程在芯片选择里输入“STM32F103C8”选中“STM32F103C8Tx”。第一步是配置时钟。对于CAN总线来说时钟一定要配准因为CAN的波特率计算依赖APB1的时钟频率。在“RCC”配置里高速外部时钟HSE选择“Crystal/Ceramic Resonator”。然后转到“Clock Configuration”标签页我的习惯是把系统时钟源设为HSE然后通过PLL倍频到72MHz这是F103的常用主频。确认一下APB1的时钟它应该是36MHz系统时钟72MHz的一半。这个APB1时钟就是CAN外设的时钟源记下这个值后面算波特率要用。接下来配置CAN外设本身。在“Pinout Configuration”标签页的左侧找到“Connectivity” - “CAN1”。首先把模式Mode从“Disable”改成“Activate”。这时下方的引脚图会自动把PA11和PA12分配为CAN_RX和CAN_TX这和我们芯片的引脚定义是对应的。然后点击“Parameter Settings”子标签页这里才是配置的核心。工作模式Operating Mode对于基础的收发测试选择“Normal”模式就行。如果你想先只听总线上的数据不主动发送避免干扰可以选“Silent”模式如果想自发自收做回环测试可以选“Loopback”模式。这里我们先选“Normal”。位时序Bit Timings这是配置CAN波特率的关键也是最容易出错的地方。它由几个参数共同决定Prescaler (分频系数) 这个值将APB1时钟分频后得到CAN的时间单元Time Quantum, tq。比如APB1是36MHzPrescaler设为9那么tq的频率就是 36MHz / 9 4MHz即一个tq是250ns。Time Segment 1 (BS1) 定义了采样点之前的时间段包含同步段固定1个tq和传播时间段。它的值是以tq为单位的整数。Time Segment 2 (BS2) 定义了采样点之后的时间段。Synchronization Jump Width (SJW) 用于同步的宽度通常设为1。波特率 APB1时钟频率 / (Prescaler * (1 BS1 BS2))举个例子我要配置一个500kbps的经典CAN波特率。APB1时钟是36MHz。经过计算和尝试设置Prescaler9 BS15 BS23。那么总的时间段长度是 1539个tq。tq的周期是 9 / 36MHz 250ns。一个位的时间就是 9 * 250ns 2.25us。对应的波特率就是 1 / 2.25us ≈ 444.44kbps接近500k。如果想更精确可能需要调整APB1的时钟或者分频系数。这里为了演示我们就用这个接近的值。在实际项目中必须和总线上的其他节点严格一致。过滤器Filter这是CAN的精华也是难点。CAN控制器通过过滤器来决定哪些报文可以进入接收FIFO。HAL库的过滤器配置有点绕。在“Parameter Settings”页往下拉找到“Filter Settings”。首先“Filter Activate”要选“Enable”。然后关键的是“Filter Mode”和“Filter Scale”。Filter Mode 有“Mask mode”掩码模式和“List mode”列表模式。掩码模式更常用它允许你设定一个ID和一個掩码掩码为1的位表示必须和ID对应位匹配为0的位表示不关心。Filter Scale 有“16-bit”和“32-bit”。这指的是过滤器的位宽。对于标准帧11位ID用16位模式就够了对于扩展帧29位ID通常用32位模式。假设我们只接收标准帧ID为0x123的报文。可以这样配Filter Mode选“Mask mode” Filter Scale选“16-bit”。在“Filter Bank”里比如用Bank0“Filter ID High”填0x123 5。为什么左移5位因为标准帧ID只有11位在16位的过滤器寄存器里它存储在高位需要左移对齐。对应的“Filter Mask High”填0x7FF 5即0x7FF11位全1左移5位表示这11位都必须严格匹配。低16位Filter ID Low 和 Filter Mask Low在标准帧模式下通常设为0。最后“Filter FIFO Assignment”选择接收到匹配报文后存入哪个FIFOCAN有RX FIFO0和FIFO1选“FIFO0”就行。“Filter Activation”当然选“Enable”。注意CubeMX的过滤器配置界面有时不会直接显示移位后的值你输入0x123它可能内部会处理。但直接写代码时这个移位操作必须自己完成这是第一个坑点。配置好CAN后我们还需要一个串口来打印调试信息。在“Connectivity”里再启用一个USART比如USART1模式选“Asynchronous”波特率115200。引脚会自动分配到PA9(TX)和PA10(RX)。最后在“Project Manager”标签页设置好工程名、路径、选择你用的IDE比如MDK-ARM在“Code Generator”里勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”这样每个外设的代码会单独生成结构更清晰。点击“GENERATE CODE”CubeMX就会生成完整的工程了。3. HAL库CAN驱动代码深度解析打开生成的Keil工程你会发现CubeMX已经帮我们生成了can.c和can.h。但里面的代码只是初始化了硬件具体的发送接收逻辑需要我们自己在main.c或其他文件中补充。我们先来啃最硬的骨头——理解HAL库的CAN驱动结构。在can.h里你会看到一个重要的结构体类型定义CAN_HandleTypeDef。这个句柄handle是HAL库操作CAN外设的核心它包含了CAN寄存器基地址、初始化结构体、状态锁、错误码以及指向发送和接收邮箱FIFO的指针。我们所有对CAN的操作比如发送、接收、配置过滤器都需要通过这个句柄来进行。生成的MX_CAN1_Init函数里主要做了两件事填充了一个CAN_FilterTypeDef结构体并调用了HAL_CAN_ConfigFilter函数来配置我们之前在CubeMX里设置的过滤器。填充了CAN_HandleTypeDef句柄hcan1并调用了HAL_CAN_Init。这个函数内部会根据我们配置的参数模式、位时序去写CAN的控制寄存器。初始化完成后CAN外设还处于停止状态。要开始工作必须调用HAL_CAN_Start(hcan1)。这个函数会设置CAN_MCR寄存器的INRQ位让CAN从初始化模式进入正常工作模式。对应的停止CAN函数是HAL_CAN_Stop。接下来是重头戏发送和接收。HAL库提供了阻塞阻塞式和非阻塞中断式、DMA式两种方式。为了理解流程我们先从最简单的阻塞式发送开始。阻塞式发送函数是HAL_CAN_AddTxMessage和HAL_CAN_GetTxMailboxesFreeLevel。CAN控制器有3个发送邮箱。发送前最好先检查一下有没有空闲的邮箱if(HAL_CAN_GetTxMailboxesFreeLevel(hcan1) 0)。然后准备一个CAN_TxHeaderTypeDef结构体来定义要发送的报文头CAN_TxHeaderTypeDef TxHeader; TxHeader.StdId 0x123; // 标准帧ID TxHeader.ExtId 0; // 扩展帧ID标准帧时设为0 TxHeader.IDE CAN_ID_STD; // 标识符类型标准帧 TxHeader.RTR CAN_RTR_DATA; // 帧类型数据帧远程帧是CAN_RTR_REMOTE TxHeader.DLC 8; // 数据长度0-8 TxHeader.TransmitGlobalTime DISABLE; // 是否使用全局时间戳一般不用再定义一个uint8_t TxData[8]数组存放数据。最后调用发送函数uint32_t TxMailbox; // 用于返回使用的是哪个发送邮箱 if (HAL_CAN_AddTxMessage(hcan1, TxHeader, TxData, TxMailbox) ! HAL_OK) { // 发送失败处理 }这个函数是阻塞的它会等待直到报文被成功放入发送邮箱或者超时超时时间在hcan1.Init.Timeout中定义但通常我们更关心发送完成回调。报文放入邮箱后CAN控制器会在总线空闲时自动发送出去。中断式接收这才是实际项目中最常用的方式。我们不可能一直用轮询去检查有没有新数据。配置中断接收需要几步启动CAN在初始化并配置好过滤器后调用HAL_CAN_Start(hcan1)。激活接收FIFO中断调用HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING)。这个函数使能了FIFO0有新报文挂起时产生中断。你也可以对FIFO1做同样操作。实现回调函数当中断发生时HAL库会调用弱定义的回调函数。我们需要在main.c或其他用户文件里重写Override它。对于FIFO0这个函数是HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)。void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef RxHeader; uint8_t RxData[8]; // 从FIFO0中读取报文 if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, RxHeader, RxData) HAL_OK) { // 成功接收到数据在这里处理 // RxHeader.StdId 包含了发送方的ID // RxHeader.DLC 是数据长度 // RxData 数组里就是数据 // 可以在这里将数据通过串口打印出来或者设置一个标志位供主循环处理 } }别忘了开启全局中断在main函数的初始化部分在启动CAN和激活通知后需要调用__HAL_CAN_ENABLE_IT(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING)吗其实HAL_CAN_ActivateNotification内部已经帮我们做了这件事它封装了中断使能和NVIC配置。我们只需要确保在stm32f1xx_it.c中的CAN中断服务函数CAN1_RX0_IRQHandler和CAN1_RX1_IRQHandler正确调用了HAL_CAN_IRQHandler(hcan1)即可而CubeMX生成的代码通常已经做好了。这里有一个巨大的坑过滤器配置必须在CAN启动HAL_CAN_Start之前完成如果你在CAN启动后才去修改过滤器配置需要先调用HAL_CAN_Stop停止CAN再配置过滤器最后重新HAL_CAN_Start。很多人在调试时发现收不到数据问题就出在这里。4. 实战构建一个完整的CAN收发测试工程理论说了一大堆现在我们来动手搭建一个可以实际跑起来的测试工程。目标很简单让STM32F103每1秒通过CAN总线发送一帧数据ID: 0x123 数据: 0x01,0x02,0x03,0x04同时随时准备接收来自总线的数据并将接收到的ID和数据通过串口打印出来。第一步完善CubeMX生成的基础工程按照第二章的步骤用CubeMX生成一个包含CAN1Normal模式500kbps 过滤器配好和USART1的工程。在“Project Manager” - “Advanced Settings”里确保为CAN和USART生成了对应的中断服务函数默认是勾选的。生成代码打开工程。第二步添加用户代码在main.c的/* USER CODE BEGIN PV */区域定义我们需要的变量/* USER CODE BEGIN PV */ CAN_TxHeaderTypeDef TxHeader; uint8_t TxData[8]; uint32_t TxMailbox; CAN_RxHeaderTypeDef RxHeader; uint8_t RxData[8]; uint8_t uart_buf[64]; // 用于串口打印的缓冲区 /* USER CODE END PV */在/* USER CODE BEGIN 0 */区域重写CAN接收回调函数和串口发送函数如果HAL库的串口发送不好用可以自己写一个简单的/* USER CODE BEGIN 0 */ #include stdio.h // 用于sprintf // 重写FIFO0接收回调函数 void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { if (hcan-Instance CAN1) { if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, RxHeader, RxData) HAL_OK) { // 将接收到的信息格式化成字符串 int len sprintf((char*)uart_buf, CAN Recv: ID0x%03X, DLC%d, Data, RxHeader.StdId, RxHeader.DLC); for (int i 0; i RxHeader.DLC; i) { len sprintf((char*)uart_buf len, %02X , RxData[i]); } len sprintf((char*)uart_buf len, \r\n); // 通过串口1发送出去 HAL_UART_Transmit(huart1, uart_buf, len, 100); } } } // 一个简单的非阻塞延时用于主循环定时 uint32_t last_tick 0; void user_delay(uint32_t interval) { uint32_t current_tick HAL_GetTick(); if ((current_tick - last_tick) interval) { last_tick current_tick; // 可以在这里执行定时任务比如发送CAN报文 } } /* USER CODE END 0 */第三步在主函数中完成初始化和主循环在main函数中/* USER CODE BEGIN 2 */区域进行外设启动和初始化/* USER CODE BEGIN 2 */ // 配置发送报文头只需配置一次 TxHeader.StdId 0x123; TxHeader.ExtId 0; TxHeader.IDE CAN_ID_STD; TxHeader.RTR CAN_RTR_DATA; TxHeader.DLC 4; // 我们发送4个字节 TxHeader.TransmitGlobalTime DISABLE; // 填充要发送的数据 TxData[0] 0x01; TxData[1] 0x02; TxData[2] 0x03; TxData[3] 0x04; // 启动CAN外设 if (HAL_CAN_Start(hcan1) ! HAL_OK) { Error_Handler(); } // 激活FIFO0接收中断通知 if (HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) ! HAL_OK) { Error_Handler(); } // 启动串口可选如果只是发送不一定要启动接收中断 // HAL_UART_Start(huart1); HAL_UART_Transmit(huart1, (uint8_t*)CAN Test Start\r\n, 16, 100); /* USER CODE END 2 */在/* USER CODE BEGIN WHILE */区域的无限循环中实现定时发送/* USER CODE BEGIN WHILE */ while (1) { // 每秒发送一次 if (HAL_GetTick() - last_tick 1000) { last_tick HAL_GetTick(); // 检查是否有空闲的发送邮箱 if (HAL_CAN_GetTxMailboxesFreeLevel(hcan1) 0) { if (HAL_CAN_AddTxMessage(hcan1, TxHeader, TxData, TxMailbox) HAL_OK) { // 发送成功可以通过串口打印提示调试用正式代码可去掉 HAL_UART_Transmit(huart1, (uint8_t*)CAN Sent\r\n, 10, 10); } else { HAL_UART_Transmit(huart1, (uint8_t*)CAN Send Failed\r\n, 17, 10); } } } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */第四步硬件连接与测试将STM32F103的PA11(CAN_RX)和PA12(CAN_TX)连接到CAN收发器芯片如TJA1050的RXD和TXD。CAN收发器的CANH和CANL连接到CAN总线需要接120欧姆的终端电阻如果总线只有两个节点通常在总线两端各接一个。将USB转串口模块的RX连接到STM32的PA9(UART_TX)以便在电脑上查看打印信息。给板子上电打开串口调试助手如XCOM 波特率115200。编译下载程序。如果一切正常你应该在串口助手看到“CAN Test Start”然后每秒看到一次“CAN Sent”。要测试接收你需要另一个CAN节点比如另一个STM32开发板或者一个USB-CAN适配器如周立功的CANalyst-II、PCAN等向ID 0x123发送数据。当STM32收到数据后串口会打印出类似“CAN Recv: ID0x456, DLC8, Data11 22 33 44 AA BB CC DD”的信息。5. 调试过程中遇到的典型问题与解决方案第一次做CAN通讯几乎没有一次成功的。下面是我在调试这个例程时踩过的几个坑以及解决办法。问题一根本收不到任何数据发送似乎也没成功。排查步骤检查硬件这是第一步也是最容易忽略的一步。用万用表测量CANH和CANL之间的电阻如果总线上只有两个节点且都接了120欧姆终端电阻并联后应该是60欧姆左右。如果电阻无穷大说明总线断了或者终端电阻没接。确保TJA1050的VCC和地连接正确。检查引脚配置确认CubeMX里CAN的引脚确实是PA11和PA12并且没有被其他外设比如USB复用。F103的CAN引脚是固定的一般不会错。检查波特率这是软件层面最常见的问题。确保发送方和接收方的波特率设置完全一致包括分频系数、BS1、BS2。哪怕有一个参数对不上双方就无法同步。可以用示波器测量CAN_TX引脚PA12的波形看一个位的时间长度是否和你计算的相符500kbps对应2us一位。检查过滤器确认过滤器配置正确且在CAN启动前配置。一个快速验证的方法是将过滤器配置成“接收所有报文”模式。对于32位掩码模式可以将Filter ID High/Low和Filter Mask High/Low都设为0。这样任何报文都应该能收到。如果这样能收到了说明之前的过滤器配得太窄了。检查中断确保接收FIFO的中断已经激活HAL_CAN_ActivateNotification并且对应的中断服务函数CAN1_RX0_IRQHandler存在并调用了HAL_CAN_IRQHandler。我的踩坑记录我当时就是栽在过滤器上。我按照手册把标准帧ID 0x123左移5位得到0x2460然后把这个值填进了“Filter ID High”。结果死活收不到。后来发现CubeMX的过滤器配置界面在你选择“16-bit”和“Mask mode”后它提供的输入框“Filter ID High”和“Filter ID Low”指的是过滤器寄存器FXR的高16位和低16位。对于标准帧ID应该放在高16位的[15:5]位置。所以直接输入0x123CubeMX会在生成代码时帮你左移。但我自己写代码调用HAL_CAN_ConfigFilter时结构体里的FilterIdHigh和FilterMaskHigh就需要我手动左移5位了。这个不一致性让我排查了很久。问题二发送函数HAL_CAN_AddTxMessage返回HAL_ERROR或者一直阻塞。原因分析CAN控制器只有3个发送邮箱。如果连续快速调用发送函数而之前的报文还没发送完成总线忙或者仲裁失败重试邮箱会被占满。HAL_CAN_AddTxMessage会等待一个空闲邮箱如果所有邮箱都满且超时由hcan.Init.Timeout决定默认是HAL_MAX_DELAY它就会一直阻塞。解决方案发送前检查邮箱就像我在示例代码里做的先调用HAL_CAN_GetTxMailboxesFreeLevel检查空闲邮箱数量。使用发送完成回调更优雅的方式是使用中断非阻塞发送。调用HAL_CAN_AddTxMessage后函数会立即返回并告诉你报文放入了哪个邮箱TxMailbox参数。然后使能发送完成中断HAL_CAN_ActivateNotification(hcan1, CAN_IT_TX_MAILBOX_EMPTY)。当某个邮箱发送成功变为空闲时会触发中断并调用HAL_CAN_TxMailboxCompleteCallback回调函数。你可以在这个回调里设置标志位通知主程序可以发送下一帧了。这种方式效率更高适合连续发送。处理发送错误使能错误中断CAN_IT_ERROR在错误回调函数HAL_CAN_ErrorCallback中处理仲裁丢失、错误被动等情况。问题三接收到的数据乱码或者ID不对。排查步骤检查数据字节序CAN总线上的数据是按字节顺序发送的。发送方和接收方对多字节数据如uint16_t, int32_t的解释要一致即大小端问题。在发送端最好把数据按字节拆解到uint8_t数组里再发送。在接收端也按字节数组接收后再组装。检查DLC确保发送方设置的DLC数据长度码和实际填充的数据字节数一致。DLC最大是8。区分标准帧和扩展帧发送方发的是标准帧11位ID但接收方过滤器配成了接收扩展帧29位ID或者IDE位没设对都会导致过滤失败或解析错误。在CAN_RxHeaderTypeDef里检查IDE字段看收到的是什么帧。总线干扰如果布线很长或者环境恶劣可能会引入干扰。确保使用双绞线并正确接地。CAN收发器如TJA1050本身有很强的抗干扰能力但硬件设计不当也会影响。问题四代码运行一段时间后CAN好像“死”了不再收发数据。可能原因CAN控制器进入了“总线关闭”状态。当发送错误计数器TEC超过255时CAN控制器会进入总线关闭状态自动与总线断开停止发送和接收。解决方案监控错误状态定期或在错误中断中调用HAL_CAN_GetError函数获取错误码。如果错误码包含HAL_CAN_ERROR_BOFBus-Off说明进入了总线关闭。恢复总线检测到总线关闭后需要软件干预来恢复。标准的流程是调用HAL_CAN_Stop(hcan1)停止CAN延时一小段时间比如100ms然后调用HAL_CAN_Start(hcan1)重新启动CAN。HAL库的HAL_CAN_Start函数内部会执行从初始化模式到正常模式的切换这个过程会清零错误计数器使CAN重新同步到总线。你可以把这个恢复逻辑放在主循环或者一个定时器任务里。调试CAN逻辑分析仪或者专业的CAN总线分析仪是神器。它们能直观地看到总线上的每一帧报文包括ID、数据、帧类型甚至错误帧。没有硬件工具的话就只能靠“printf大法”一点点打印状态信息了所以前面提到的串口调试信息非常重要。6. 从基础例程到实际项目的进阶思考跑通这个点对点的收发例程只是万里长征第一步。要把CAN用到实际项目中还有很多问题需要考虑。这里分享几个我从项目实践中总结的经验点。1. 过滤器配置策略一个复杂的CAN网络可能有几十上百个节点每个节点只关心特定的ID。过滤器的合理配置能极大减轻CPU负担避免处理无关中断。STM32F103的bxCAN有14个过滤器组Filter Bank每个组可以配置成不同的模式。我的策略是精确过滤对于需要快速响应的关键指令如急停信号ID固定使用掩码模式进行精确匹配。范围过滤对于一组连续的ID如传感器1~8的数据可以利用掩码模式。例如ID从0x100到0x107它们的二进制高8位都是0x01低3位变化。可以设置Filter ID 0x100 5 Filter Mask 0x1F8 5 (二进制1111111000)。这样低3位掩码为0不关心高8位必须匹配0x100的高8位就实现了范围接收。分组管理将相关的ID分配到同一个过滤器组并指向同一个接收FIFO。比如将所有控制指令的ID配置到Bank0~Bank3指向FIFO0将所有状态反馈ID配置到Bank4~Bank7指向FIFO1。这样在中断回调里通过判断是哪个FIFO触发的就能快速知道报文类型。2. 高效的数据处理与协议设计在HAL_CAN_RxFifo0MsgPendingCallback回调函数里不宜做复杂的、耗时的处理比如浮点运算、大量字符串格式化。中断服务函数的原则是快进快出。我的做法是设置标志位数据拷贝在回调函数里只将接收到的报文ID和数据拷贝到一个自定义的环形缓冲区结构体数组中并设置一个“新数据到达”的标志位volatile变量。主循环处理在主循环中不断检查这个标志位如果置位就从环形缓冲区里取出数据进行后续的解析、计算、显示等操作。这样就把耗时任务从中断转移到了主循环。应用层协议CAN一帧只能传8字节。对于更长的数据比如一段配置参数、一个文件需要定义自己的应用层协议。常见的做法是定义“帧头数据包序号数据校验”的结构。发送端分包发送接收端按序号重组并校验。协议里还要考虑超时重发、应答机制等。3. 使用DMA进行批量数据收发对于需要高速、连续收发CAN数据的场景如电机控制、数据采集频繁的中断仍然会给CPU带来负担。STM32的CAN支持与DMA联动可以将发送邮箱和接收FIFO映射到DMA通道。DMA接收可以配置为当FIFO收到新报文时自动通过DMA将数据搬运到指定的内存数组中。你只需要在DMA传输完成中断或半传输中断里去处理一批数据而不是每帧一次中断效率高很多。DMA发送可以将要发送的多帧报文先准备好放在一个数组里然后启动CAN发送并关联DMADMA会自动按顺序将数据从内存搬运到CAN的发送邮箱。这在需要发送连续、周期性报文时非常有用。 HAL库提供了HAL_CAN_Start和HAL_CAN_Stop函数但DMA的配置相对复杂需要仔细阅读参考手册中关于CAN和DMA控制器交互的章节。4. 总线负载管理与错误处理一个健康的CAN网络总线负载率一般建议低于30%。你可以通过计算单位时间内收到的帧数*帧长度来估算负载。如果负载过高会导致延迟增加甚至丢帧。错误处理除了之前提到的总线关闭恢复还要关注其他错误标志如错误警告Error Warning、错误被动Error Passive。可以在错误中断回调函数里读取错误状态寄存器记录错误日志对于持续性错误如某个节点持续发送错误帧可以考虑将其从逻辑上隔离。心跳与超时在重要的主从通信中通常会让从节点定期发送“心跳”或状态帧。主节点监控这个心跳如果超时未收到则认为该节点离线采取安全措施如停止相关输出。最后关于HAL库和标准库的选择我的看法是对于新项目尤其是用到CubeMX图形化配置和中间件如FreeRTOS, USB, FATFS时HAL库是更优选择它封装统一移植方便。但对于资源极其紧张或对时序要求极其苛刻的场景标准库甚至直接寄存器操作可能更能满足需求。不过对于STM32F103和CAN通信这类常见应用HAL库的稳定性和性能已经完全足够。关键是要理解它背后的机制这样无论用什么库出了问题你都知道该从哪里下手排查。这个例程的代码我整理后放到了开头的压缩包里希望能成为一个扎实的起点。本文还有配套的精品资源点击获取