STM32F103RBT6 HAL库CAN通信工程实战:初始化、收发与避坑指南 简介一套基于STM32F103RBT6微控制器、HAL库的CAN通信工程代码适合正在学习或开发CAN总线的嵌入式工程师覆盖从CubeMX配置到Keil工程编译、下载、调试以及报文收发验证的完整环节。压缩包共586个文件约6.01MB其中C源代码与头文件分别有337个和104个同时包含Keil工程配置、CubeMX工程文件、链接脚本、编译生成的映像文件与映射文件等目录结构稳定工程可直接打开适合仔细阅读源码逻辑和在此基础上做二次开发。代码已经实际调通CAN通信速率设为1Mbps实现了控制器初始化、滤波器配置、发送与接收回调函数、标准帧和扩展帧的处理并对消息标识符过滤给出示例针对所用的PB8、PB9引脚也提供了时钟使能与GPIO复用配置。鉴于CAN总线机制具有一定的学习曲线这套代码中明确展示了标识符过滤、中断回调等关键点。目前已有4447人学习下载对需要把CAN功能移植到同系列单片机或快速搭建CAN测试节点的开发者是一份值得参考的工程范例。1. 已调通的 CAN 工程换芯片换库都不怕拿这份 HAL 代码直接改手里有个设备对接需求对方给的协议是标准 CAN 2.0A2.5 年前的代码跑在另一颗 F103 上用的是标准外设库。现在要求换成 stm32f103rbt6 这颗料库也要统一到 HAL 库这意味着所有外设接口全部重写。我找了一份已经调通的 HAL 库 CAN 工程作为起点省去了从零翻参考手册的时间把收发链路、滤波器、波特率、邮箱管理全部打通剩下的是业务层的事。这篇笔记就把这份工程的结构、核心代码、跑通步骤和踩过的坑完整记录下来。适合三种人看一是刚接触 HAL 库想跑 CAN 的新手二是从标准库迁到 HAL 的老手三是想在 F103RBT6 上快速搭一套 CAN 通信骨架做二次开发的工程师。2. 工程结构CubeMX 生成的主干和你需要关心的文件2.1 硬件资源盘点RBT6 这颗芯片上有什么STM32F103RBT6 是 64 引脚的中容量芯片Flash 128KBSRAM 20KB内部带一个 bxCAN 控制器CAN1 的收发引脚固定在 PA11RX和 PA12TX没有第二个 CAN 外设。这个配置决定了你只能挂一路 CAN 总线如果需要双通道就得换 RCT6 或更高型号。硬件上还需要外接一颗 CAN 收发器比如 TJA1050 或者 SN65HVD230把芯片的 TTL 电平转成差分信号。还有一个容易被忽略的点总线两端需要各接一个 120Ω 终端电阻。如果只是两块板子短距离调试不接终端电阻也能通但波形会变差距离一长就容易偶发错误帧。我这次调试用的是一块自带收发器的开发板省了不少事但如果你是自己画的最小系统板这两样都得检查。2.2 CubeMX 里的配置项时钟树和 CAN 参数用 STM32CubeMX 生成工程时关键配置有四处。第一是时钟树外部晶振 8MHz 经过 PLL 倍频到 72MHzAPB1 分频得到 36MHzCAN 外设挂在 APB1 上CAN 时钟最高就是 36MHz这是 F103 的硬件限制不能超。第二是 CAN1 的引脚模式默认走 PA11/PA12不需要改。第三是中断CAN 接收建议打开 RX0 中断挂到 NVIC 上优先级在调试阶段可以给到最高后面再根据系统实时性调整。第四是 CAN 的具体参数这部分最容易出错会在下一节展开。生成工程后真正的用户代码改动范围其实很小main.c 里做初始化stm32f1xx_it.c 里不做任何修改HAL 库的回调机制会自动调用业务逻辑集中在可以单独拆出的 can.c / can.h 两个文件里。想让这份工程具备可复用性我一般会把 CAN 初始化、过滤器配置、发送封装、接收回调全部放进独立的 can.c这样换项目时只改 can.c 就行。2.3 生成后的目录结构与改动范围├─ Core/ │ ├─ Inc/ // main.h、stm32f1xx_hal_conf.h │ ├─ Src/ │ │ ├─ main.c // 系统初始化、外设句柄定义 │ │ ├─ stm32f1xx_it.c // 中断服务函数一般不改 │ │ └─ system_stm32f1xx.c ├─ Drivers/ │ ├─ CMSIS/ │ └─ STM32F1xx_HAL_Driver/ │ └─ Src/ │ ├─ stm32f1xx_hal_can.c // HAL 库 CAN 驱动 │ └─ stm32f1xx_hal_msp.c // 引脚时钟和中断配置 └─ myApp/ ├─ can.c // 本项目实际业务代码重点 └─ can.h这份工程能快速跑通核心在于 HAL 库把寄存器操作全部封装成了状态机你只需要关注四个 API初始化、发送、注册中断回调、启动接收。下面逐个拆。3. 核心代码拆解初始化、发送、接收三块3.1 波特率计算Prescaler、BS1、BS2 怎么配CAN 波特率只和一个公式有关波特率 PCLK1 / (Prescaler × (1 BS1 BS2))。PCLK1 在 F103 上是 36MHz这个值要记住。CubeMX 界面里 BS1 和 BS2 的单位是 tq实际配置时是时间份额的个数千万别理解成微秒或纳秒。比如要跑工业上最常见的 500kbps按公式套36MHz 除以 500k 得到总分频倍数 72。把 72 拆成Prescaler × (1 BS1 BS2)我常用的一组是 Prescaler4BS113BS24这样4 × (1 13 4) 72波特率刚好 500k。也可以换一组 Prescaler2BS116BS28不行BS1 最大 16BS2 最大 81 16 8 252 × 25 50算出来是 720k不对。所以 BS1 和 BS2 的取值必须保证等式成立。常见波特率参数如下PCLK1 36MHz 的前提下波特率PrescalerBS1BS2实际波特率采样点500k4134500k77.8%250k8134250k77.8%125k16134125k77.8%1M21341M77.8%采样点 77.8% 是个比较中庸的值对线缆长度和收发器延时不那么敏感。你如果用的是不同的 PCLK1比如外部晶振是 12MHz 或者开了 USB 预分频表里的参数就要重算。3.2 CAN 初始化时钟、引脚、模式、滤波器初始化部分我习惯分成两个函数CAN_Init()负责时钟和 GPIOCAN_ConfigFilter()负责滤波器。HAL 库的HAL_CAN_Init要求先配置好hcan1.Init结构体然后调用之后还要手动调HAL_CAN_Start容易漏的是这步。void CAN_Init(void) { // 1. 时钟和引脚使能 __HAL_RCC_CAN1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; // PA11: CAN_RX 上拉输入 GPIO_InitStruct.Pin GPIO_PIN_11; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PA12: CAN_TX 复用推挽输出 GPIO_InitStruct.Pin GPIO_PIN_12; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 2. CAN 参数配置 hcan1.Instance CAN1; hcan1.Init.Prescaler 4; // 500kbps见波特率表 hcan1.Init.Mode CAN_MODE_NORMAL; // 调试完成后改回 NORMAL hcan1.Init.SyncJumpWidth CAN_SJW_1TQ; hcan1.Init.TimeSeg1 CAN_BS1_13TQ; hcan1.Init.TimeSeg2 CAN_BS2_4TQ; hcan1.Init.TimeTriggeredMode DISABLE; hcan1.Init.AutoBusOff DISABLE; hcan1.Init.AutoWakeUp DISABLE; hcan1.Init.AutoRetransmission ENABLE; // 失败自动重发实测很有用 hcan1.Init.ReceiveFifoLocked DISABLE; hcan1.Init.TransmitFifoPriority DISABLE; HAL_CAN_Init(hcan1); HAL_CAN_Start(hcan1); // 这步漏了后面一切 API 都无效 // 3. 滤波器配置 CAN_FilterTypeDef sFilterConfig {0}; sFilterConfig.FilterBank 0; sFilterConfig.FilterMode CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh 0x0000; sFilterConfig.FilterIdLow 0x0000; sFilterConfig.FilterMaskIdHigh 0x0000; sFilterConfig.FilterMaskIdLow 0x0000; sFilterConfig.FilterFIFOAssignment CAN_RX_FIFO0; sFilterConfig.FilterActivation CAN_FILTER_ENABLE; HAL_CAN_ConfigFilter(hcan1, sFilterConfig); }AutoRetransmission开成 ENABLE 是血泪经验默认 DISABLE 时如果总线上有错误帧发一次就丢上层完全不知道。开了自动重发后硬件会一直重发直到成功或总线关闭应用层感受到的错误率会明显下降。滤波器那段配置是全接收模式FilterIdHigh/Low和FilterMaskIdHigh/Low全零意思是不管什么 ID 都收。3.3 发送链路HAL_CAN_AddTxMessage 与邮箱管理发送函数封装成下面这样调用方只需要给 ID、数据指针和长度。注意 HAL 库的发送是异步的函数返回只代表数据进了邮箱不代表已经上总线更不代表对端已经收到。uint8_t CAN_SendFrame(uint32_t id, uint8_t *data, uint8_t dlc) { CAN_TxHeaderTypeDef txHeader {0}; uint32_t mailbox 0; txHeader.StdId id; // 标准帧 ID txHeader.IDE CAN_ID_STD; // 标准帧格式 txHeader.RTR CAN_RTR_DATA; // 数据帧 txHeader.DLC dlc; // 数据长度 0~8 txHeader.ExtId 0; // 返回 HAL_OK 表示成功进入邮箱 if (HAL_CAN_AddTxMessage(hcan1, txHeader, data, mailbox) ! HAL_OK) { return 0; // 三个邮箱都满了或者 CAN 还没启动 } return 1; }使用上要注意邮箱有三种状态HAL_CAN_AddTxMessage会帮你挑一个空邮箱但如果连续快速调用超过三次且发送优先级低、总线繁忙第四个帧就会被拒绝返回HAL_ERROR。这时候要么把发送重试逻辑交给上层要么把TransmitFifoPriority配成 ENABLE按发送顺序排优先级。实际项目里我更倾向上层维护一个待发队列而不是靠底层死等。3.4 接收链路FIFO 中断与回调拿数据接收端用中断方式HAL 库在stm32f1xx_it.c的CAN1_RX0_IRQHandler里会自动查中断源最终回调到用户代码里。但有一个坑你必须在初始化后手动调用HAL_CAN_ActivateNotification去使能接收中断这一步 CubeMX 不会帮你生成。void CAN_StartReceive(void) { // 使能 FIFO0 消息挂起中断 HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING); } // 这个回调函数由 HAL 库在中断上下文自动调用 void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader {0}; uint8_t rxData[8] {0}; if (hcan-Instance CAN1) { // 从 FIFO0 取出一帧读完后硬件自动释放 FIFO 空间 HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData); // 业务处理标准帧 数据帧ID 是 0x123 if (rxHeader.IDE CAN_ID_STD rxHeader.RTR CAN_RTR_DATA) { if (rxHeader.StdId 0x123) { // 把 rxData 拷贝到全局变量置标志位交给主循环处理 } } } }回调里尽量别做耗时处理比如别调用printf除非重定向到中断安全的串口别做浮点运算因为这是中断上下文。正确做法是回调里只做“拷贝数据 置标志位”真正的解析放到主循环里。如果 F103 同时开了很多中断CAN 回调被长时间打断FIFO 溢出会造成丢帧这个问题在后面避坑里讲。3.5 环回模式验证不上总线也能自测拿到一个新板子我第一件事不是接总线而是把CAN1.Init.Mode改成CAN_MODE_LOOPBACK也就是环回模式。这个模式下芯片的内部发送路径直接连到接收路径不经过外部引脚发出去的帧自己就能收到。用来验证时钟、波特率、中断链路非常快。改法把上面初始化代码里的CAN_MODE_NORMAL换成CAN_MODE_LOOPBACK然后发送一帧看回调是否触发。如果环回模式能收发说明软件链路没问题再接上外部收发器和总线把模式改回 NORMAL问题就只可能出在硬件上。我习惯把这个切换做成宏定义调试阶段置 1发布前翻成 0不用手动改代码。4. 跑通实操接线、编译、验证三步走4.1 接线清单假设你手上是“STM32F103RBT6 最小系统板 外部 TJA1050 模块”的组合接线按下面这张表MCU 引脚信号方向收发器模块说明PA11RX → 输入TXDMCU 接收接到收发器的 RXD 输出PA12TX → 输出RXDMCU 发送接到收发器的 TXD 输入3V3/5V电源VCC收发器供电注意模块逻辑电平GND公共地GND必须共地否则差分信号会飘无差分CANH / CANL总线上两端各并 120Ω 终端电阻TJA1050 的供电有 3.3V 和 5V 两个版本买模块时看清楚。如果模块是 5V 供电而 MCU 是 3.3V 逻辑需要确认模块上有电平转换电路不然长期运行有烧引脚的风险。4.2 编译与下载推荐直接用 STM32CubeIDE开箱即用不需要额外配编译器。工程导入后直接 Build如果没有任何错误用 ST-Link 下载。命令行烧录也可以比如用 st-flash 工具st-flash write build/stm32f103rbt6_can.bin 0x08000000下载完复位运行用示波器或者 USB-CAN 分析仪看 PA12 上有没有波形。没有分析仪也没关系代码里反复发送对端用一个 USB 转 CAN 模块接到 PC配合candump就能看到帧。4.3 验证期待结果验证三步走第一步环回模式自测代码里每 500ms 发一帧 ID 0x123 的 8 字节数据预期回调里能收到同样的帧第二步改成 NORMAL 模式外接 USB-CAN 分析仪PC 端能收到 MCU 发出的帧第三步反向发送用 PC 端cansend给 MCU 发一帧预期 MCU 串口打印出对应 ID 和数据。三步全通说明这份工程的初始化、发送、接收全链路没有硬件问题可以开始写业务逻辑。5. 避坑记录CAN 调试里最常见的五个坑5.1 HAL_CAN_AddTxMessage 一直返回 HAL_BUSY现象调用发送函数返回值永远不是 HAL_OK调试时发现函数内部状态机卡住。原因最常见的是初始化时漏了HAL_CAN_Start(hcan1)。HAL 库的 Init 只是做参数配置Start 才把 CAN 控制器从初始化状态切到正常工作状态。邮箱虽然存在但控制器没工作AddTxMessage 只会报 BUSY。解决在HAL_CAN_Init之后立即调用HAL_CAN_Start并在发送前用HAL_CAN_GetState(hcan1)断言状态是HAL_CAN_STATE_READY。5.2 回调不执行接收中断进了 IRQ 却出不来现象发送正常开环回能看到发送成功但HAL_CAN_RxFifo0MsgPendingCallback一次都不进。原因漏了HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING)。HAL 库的设计是 Init 和 Start 只负责启动控制器不会自动使能接收中断必须显式调用这个函数。另一个原因是中断优先级配得太低被其他高频中断堵死。解决初始化完成后立即调用 ActivateNotificationNVIC 里 CAN 中断优先级至少比调试串口高一级。我遇到过一次是串口在狂打日志CAN 中断优先级低导致 FIFO 溢出回调永远进不了数据处理。5.3 500k 波特率实际跑成了 562.5k现象两端都是“500k”配置但通信时好时坏用逻辑分析仪抓波形发现位时间比预期的 2μs 短。原因网上大量 F103 老代码用的是Prescaler4, BS19, BS26这套参数在标准库年代广泛传播但按 36MHz PCLK1 算36M / (4 × (196)) 562.5k。协议双方如果都按这套参数配互相是能通的但如果一端是严谨的 500k 设备另一端是 562.5k错误率会高到让你怀疑人生。解决必须用公式算不要复制网上的参数。回到 3.1 的表500k 对应Prescaler4, BS113, BS24。排查办法是在 CubeMX 里看波特率显示它对任意参数组合算出的实际值都会直接显示在界面上自己核对一遍。5.4 两块板子互相收不到但单独接 PC 分析仪都正常现象每块板子和 USB-CAN 分析仪通信都正常板子之间互发就是收不到。原因三个原因按概率排序。第一CANH、CANL 接反了总线没有差分信号第二两端都没接 120Ω 终端电阻长线反射导致位错误第三两边的波特率参数组不同一个 500k 一个 250k。解决先把两边代码的总线参数截图对比确认一致然后用万用表量 CANH 和 CANL 之间的电压静默态应该在 2.5V 左右最后在总线上加终端电阻。这步做完大概率能通。5.5 偶发丢帧重发又正常现象高速连续发送 1000 帧中间偶尔丢 35 帧单帧调试时怎么试都对。原因FIFO 溢出。接收中断里处理耗时太长下一帧进 FIFO 时上一帧还没被读出硬件直接丢弃新帧。另一种原因是发送端开了一帧一确认模式没有做响应超时处理。解决把回调里的处理逻辑压到最小只做拷贝和置标志位同时把ReceiveFifoLocked打开弃新保旧保证旧数据不丢。如果要实现可靠传输需要在应用层加帧计数对端检测缺失就请求重传这是 CAN 协议的普遍做法。6. 二次开发从回环到落地的改造思路6.1 改滤波器从全收变成只收你要的 ID全收模式适合调试正式用的时候总线上一堆无关帧会持续打扰 CPU。改成掩码模式只对特定 ID 放行// 只接收标准帧 ID 0x123 sFilterConfig.FilterMode CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh (0x123 5) 8; sFilterConfig.FilterIdLow (0x123 5) 0xFF; sFilterConfig.FilterMaskIdHigh 0xFFFF; // 掩码为1的位必须完全匹配 sFilterConfig.FilterMaskIdLow 0xFFFF;16 位标准 ID 在 32 位滤波器里左移 5 位这是 HAL 库内部布局决定的绕晕了不少人。想省心的话直接设成掩码全 0回到全收然后在回调里用if (rxHeader.StdId 0x123)做软件过滤代价是回调被无关帧打扰但代码直观。6.2 补上错误处理和状态机生产环境里不能等 CAN 总线挂了才发现。改造思路启动时开启错误中断在HAL_CAN_ErrorCallback里读HAL_CAN_GetError根据错误码累积计数连续错误超过阈值就置总线关闭标志上层收到后切到降级模式。还有一个实用技巧把自动离线恢复打开AutoBusOff配成 ENABLE总线恢复后 CAN 控制器会自动重新同步不需要手动重启。6.3 移植到其他 F1 系列这颗 RBT6 的代码换到 F103C8T6 或者 F105 时唯一要变的是hcan1.Instance的取值和引脚。F105 有双 CAN如果用到 CAN2 需要使能时钟并配置不同的滤波器 Bank 范围。Pin 映射上CAN1 始终是 PA11/PA12CAN2 默认是 PB12/PB13。这份工程的初始化、发送、接收封装是通用的直接搬即可。从那以后我每次换芯片平台都强制先跑一遍环回模式再碰别的宁可多花十分钟验证链路也绝不在总线上对着错误帧发呆。这份 HAL 库 CAN 代码应该是你接手新板子时最快能跑起来的一份工程。希望帮到你。本文还有配套的精品资源点击获取