☰
STM32F103 HAL库CAN开发实战:从配置到避坑指南
2026/10/26 10:56:48 网站建设 项目流程

简介:这份资源面向嵌入式开发初学者与需要快速上手CAN通信的工程师,提供基于STM32F103RBT6、STM32CubeMX与HAL库的完整CAN开发工程,代码已在Keil uVision 4环境下调试通过,可直接用于工业控制、汽车电子与自动化等场景的通信功能验证。压缩包共586个文件,约6.01MB,以337个c源文件与104个h头文件为主体,配合43个s启动文件、28个icf及1个sct链接脚本、1个uvproj工程文件与hex、axf等编译产物,构成从外设驱动到可烧录固件的完整结构。资源已有4449人学习下载,说明其配置思路与代码组织经过较多实践检验。工程覆盖CAN时钟源与1Mbps速率设置、PB8/PB9引脚分配、标准帧与扩展帧收发、滤波器配置及收发回调等关键环节,读者可据此对照理解CubeMX生成代码与HAL库API的衔接方式,并借助逻辑分析仪或串口输出验证通信结果,适合作为CAN入门与工程移植的参考模板。

1. 一块 F103 板子跑通 CAN,比想象中省事也比想象中坑

手头有个项目要接一条 CAN 总线,节点不多,速率 500k,主控用惯了 STM32F103RBT6,于是翻出一份已经调通的 HAL 库 CAN 开发代码当底子。这份代码的价值不在于多复杂,而在于它把「时钟、引脚、波特率、过滤器、收发中断」这几件最容易翻车的事一次性摆平了,拿过来改改引脚和波特率就能上自己的板子。适合两类人:一是刚接触 CAN、被过滤器配置和波特率计算绕晕的新手,二是想找个干净 HAL 工程做二次开发、不想从 CubeMX 重新点一遍的老手。下面按「这代码怎么组织、怎么跑起来、怎么改」的顺序拆,中间夹一段我踩过的坑,最后讲二次开发时我会怎么动它。

2. 工程结构与 CAN 初始化链路:从时钟到第一个报文

2.1 目录与文件职责

这份代码是典型的 HAL 裸机工程,没有 RTOS,结构清晰到可以直接对照 CubeMX 生成的骨架看。核心文件就那几个,其余是 HAL 库和启动文件:

CAN_F103/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── can.h // CAN 句柄声明、收发函数原型 │ │ └── stm32f1xx_hal_conf.h │ └── Src/ │ ├── main.c // 时钟、GPIO、CAN 初始化与主循环 │ ├── can.c // CAN 外设初始化、过滤器、收发实现 │ ├── stm32f1xx_it.c // CAN_RX0 / CAN_TX 中断入口 │ └── stm32f1xx_hal_msp.c // 引脚与时钟的 MSP 初始化 ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── MDK-ARM/ // Keil 工程文件 └── STM32F103RBTx_FLASH.ld

can.c是整份代码的重心,main.c只负责把系统时钟拉到 72MHz 然后调用初始化。stm32f1xx_hal_msp.c里做的是 GPIO 复用和 CAN 时钟使能,这部分很多人会漏,结果初始化返回 HAL_ERROR 却找不到原因。

2.2 时钟与引脚:CAN 挂在 APB1 上

F103 的 CAN 外设时钟来自 APB1,最高 36MHz,这一点直接决定后面波特率怎么算。引脚方面 RBT6 的 CAN 默认走 PA11(CAN_RX)和 PA12(CAN_TX),复用推挽输出、上拉输入:

void HAL_CAN_MspInit(CAN_HandleTypeDef *hcan) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if (hcan->Instance == CAN1) { __HAL_RCC_CAN1_CLK_ENABLE(); // 使能 CAN1 时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟 GPIO_InitStruct.Pin = GPIO_PIN_12; // CAN_TX GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_11; // CAN_RX GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); } }

这里两个细节值得说:TX 必须是复用推挽,配成普通输出发不出波形;RX 配输入上拉,是因为总线上没有节点驱动时靠上拉拉成隐性电平,否则可能收到一堆错误帧。MSP 函数由HAL_CAN_Init内部回调,不需要手动调用,但前提是你在can.c里正确传入了CAN1实例。

2.3 波特率与过滤器:两个最容易配错的地方

CAN 初始化里真正需要动脑的是Prescaler、TimeSeg1、TimeSeg2和SyncJumpWidth四个参数。以 APB1 = 36MHz、目标 500kbps 为例,常见配法是分频 4,得到 9MHz 的 tq 时钟,再让一个位时间等于 18 个 tq:

CAN_HandleTypeDef hcan; void MX_CAN_Init(void) { hcan.Instance = CAN1; hcan.Init.Prescaler = 4; // 36MHz / 4 = 9MHz hcan.Init.Mode = CAN_MODE_NORMAL; // 正常模式,调试可改 LOOPBACK hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan.Init.TimeSeg1 = CAN_BS1_15TQ; // 段1 15tq hcan.Init.TimeSeg2 = CAN_BS2_2TQ; // 段2 2tq hcan.Init.TimeTriggeredMode = DISABLE; hcan.Init.AutoBusOff = ENABLE; // 总线关闭自动恢复 hcan.Init.AutoWakeUp = DISABLE; hcan.Init.AutoRetransmission = ENABLE;// 发送失败自动重传 hcan.Init.ReceiveFifoLocked = DISABLE; hcan.Init.TransmitFifoPriority = DISABLE; if (HAL_CAN_Init(&hcan) != HAL_OK) { Error_Handler(); } }

位时间 = 1(SYNC) + 15(BS1) + 2(BS2) = 18tq,9MHz / 18 = 500kHz,采样点落在 (1+15)/18 ≈ 88.9%,这是 CAN 里比较稳妥的位置。参数不是随便填的,Prescaler改一位波特率就整体偏移,TimeSeg1/2决定采样点,采样点太靠前在长线缆上容易误判。

过滤器是第二个坑。F103 的 CAN1 有 14 组过滤器,代码里通常配一组 32 位掩码模式,只收指定 ID:

CAN_FilterTypeDef filter; filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式 filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterIdHigh = 0x123 << 5; // 标准 ID 左移 5 位 filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x7FF << 5; // 全比较,只放行 0x123 filter.FilterMaskIdLow = 0x0000; filter.FilterFIFOAssignment = CAN_RX_FIFO0; filter.FilterActivation = ENABLE; filter.SlaveStartFilterBank = 14; if (HAL_CAN_ConfigFilter(&hcan, &filter) != HAL_OK) { Error_Handler(); }

标准 ID 要左移 5 位再写进FilterIdHigh,这是寄存器布局决定的,不左移就会出现「明明发了却收不到」。掩码位为 1 表示该位参与比较,为 0 表示忽略,想收一段 ID 就把掩码相应位清零。

2.4 启动与收发:中断方式的最小闭环

初始化完还要HAL_CAN_Start并打开接收中断,否则 FIFO 里有数据也不会进回调:

HAL_CAN_Start(&hcan); HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING); // 发送一帧标准数据帧 uint8_t data[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; CAN_TxHeaderTypeDef txHeader; uint32_t txMailbox; txHeader.StdId = 0x123; txHeader.ExtId = 0; txHeader.IDE = CAN_ID_STD; txHeader.RTR = CAN_RTR_DATA; txHeader.DLC = 8; txHeader.TransmitGlobalTime = DISABLE; if (HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &txMailbox) != HAL_OK) { // 三个邮箱都满时会返回 HAL_ERROR,需要等发送完成中断 }

接收侧在HAL_CAN_RxFifo0MsgPendingCallback里取数据,这个回调由中断触发,函数名固定,HAL 内部弱定义,你在can.c里重写即可。取完一帧要判断是否还有待处理报文,避免 FIFO 溢出丢帧。

3. 编译、下载与总线联调:让第一帧真正发出去

3.1 工具链与工程打开方式

代码带的是 Keil MDK 工程,打开MDK-ARM下的工程文件即可。芯片选型确认是 STM32F103RB,Flash 128KB、RAM 20KB,启动文件用startup_stm32f103xb.s。如果手头是 IAR 或 GCC,把Core和Drivers两个目录拷进自己的工程,补上对应的启动文件和链接脚本就行,HAL 层代码是编译器无关的。

编译前检查stm32f1xx_hal_conf.h里HAL_CAN_MODULE_ENABLED是否打开,很多人编译报「HAL_CAN_xxx 未定义」就是这里被注释掉了。另外确认HSE_VALUE和你板子上的晶振一致,常见是 8MHz,配错会导致系统时钟和 CAN 波特率一起偏。

3.2 下载与硬件连接

下载用 ST-Link 或 DAP-Link 都行,SWD 四根线:3.3V、GND、SWDIO、SWCLK。CAN 侧接线要注意,F103 的 CAN 引脚是 3.3V 电平,不能直接挂到 5V 收发器以外的电路上,中间必须经过 CAN 收发器(如 TJA1050 一类),收发器再接到 CAN_H / CAN_L。总线两端各接一个 120Ω 终端电阻,这是硬件层的硬要求,少一个在短线上可能还能通,线一长就随机出错。

上电后用 USB-CAN 分析仪或者另一块板子做对端,先确认分析仪自身波特率设成 500k,再抓报文。如果分析仪一直报错误帧,先量 CAN_H 和 CAN_L 之间的差分电压,隐性时约 2.5V 附近、显性时拉开,电压不对说明收发器或接线有问题,跟代码无关。

3.3 用回环模式先排除硬件

第一次调不通时,我一般先把hcan.Init.Mode改成CAN_MODE_LOOPBACK,这样发送的帧不进总线、直接回环到自己的接收 FIFO。如果回环能收到,说明初始化、过滤器、收发流程都对,问题在外部接线或对端;如果回环都收不到,那就是代码或时钟配置的问题。这个二分法能省掉大量「到底是硬件还是软件」的纠结。

回环验证通过后再切回CAN_MODE_NORMAL,重新编译下载。注意切换模式后要重新HAL_CAN_Start,运行中改模式需要先HAL_CAN_Stop。

3.4 观察运行状态

调试时打开hcan.Instance->ESR寄存器看错误计数,TEC 和 REC 持续增长说明总线上有持续错误,常见是波特率不一致或终端电阻缺失。hcan.Instance->TSR能看三个发送邮箱的状态,判断是卡在邮箱满还是仲裁失败。这些寄存器比在回调里打断点直观,尤其是总线偶发错误时。

4. 避坑与排查:五个真实踩过的记录

4.1 现象:初始化返回 HAL_ERROR,串口打印停在 CAN 初始化

原因多半是 MSP 里没使能 CAN1 时钟,或者HAL_CAN_MspInit压根没被调用。HAL 的初始化流程是先调HAL_CAN_Init,内部回调 MSP 做引脚和时钟,如果工程里 MSP 函数写在别的文件却没被链接进来,时钟就是关的。解决方法是确认stm32f1xx_hal_msp.c参与编译,并在HAL_CAN_MspInit里显式__HAL_RCC_CAN1_CLK_ENABLE()。

4.2 现象:能发不能收,分析仪能看到自己发的帧

原因通常是过滤器没配对。标准 ID 写进FilterIdHigh前忘了左移 5 位,或者掩码设成全 0 导致「全部忽略」而不是「全部放行」。还有一种是把过滤器分配到了 FIFO1,但中断只开了 FIFO0。解决方法是先用掩码全 0(接收所有)验证链路,再逐步收紧到目标 ID,同时确认FilterFIFOAssignment和CAN_IT_RX_FIFO0_MSG_PENDING一致。

4.3 现象:波特率算出来对,实际通信随机出错

原因常见于采样点和晶振。HSE_VALUE与实际晶振不符会让 APB1 时钟偏移,波特率跟着偏;采样点设得太靠前(比如 BS1 很小)在长线缆上容错差。解决方法是核对晶振宏定义,把采样点调到 80%~90% 区间,并用示波器或分析仪看实际位宽。

4.4 现象:发送函数偶尔返回 HAL_ERROR

原因是三个发送邮箱都占满,HAL_CAN_AddTxMessage没有空邮箱可用。如果没开AutoRetransmission,一次仲裁失败也会占着邮箱。解决方法是开启自动重传,发送前判断HAL_CAN_GetTxMailboxesFreeLevel(),邮箱满时等HAL_CAN_TxMailbox0CompleteCallback再发,而不是死循环重试。

4.5 现象:跑一段时间后总线彻底不通,错误计数很高

原因是总线关闭(Bus-Off)后没有恢复。F103 的 CAN 在 TEC 超过 255 时进入 Bus-Off,如果AutoBusOff没开,外设就停在那。解决方法是初始化时AutoBusOff = ENABLE,并在HAL_CAN_ErrorCallback里记录错误类型,方便定位是位错误、格式错误还是应答错误。

5. 二次开发改造:把这份代码变成自己的节点

拿到能跑的代码只是起点,真正上手改的时候,我会按「配置层、协议层、应用层」三层去动,尽量不碰 HAL 生成的初始化骨架。

配置层只改三样:引脚、波特率、过滤器。引脚换到 PB8/PB9 时,MSP 里改 GPIO 端口和引脚号,同时确认这两个脚没有别的复用冲突;波特率按目标速率重算Prescaler和TimeSeg1/2,我习惯写个小表放在注释里,换速率时直接查;过滤器按实际要收的 ID 段配掩码,别图省事全放行,总线节点一多全放行会让 CPU 被中断淹没。

协议层建议在can.c外面再包一层,把「帧 ID 到业务字段」的映射抽出来,HAL 的CAN_TxHeaderTypeDef不要散落在业务代码里。比如定义一个发送结构体,内部填好 ID 和 DLC,再统一调HAL_CAN_AddTxMessage,这样以后换芯片或换库,业务层不用动。

应用层最关键的是收发解耦。接收回调里不要做耗时处理,把数据拷进环形缓冲区就返回,主循环里再解析;发送侧用一个发送队列,邮箱满时排队而不是阻塞。下面是我常用的接收缓冲写法:

#define CAN_RX_BUF_SIZE 32 typedef struct { uint32_t id; uint8_t data[8]; uint8_t dlc; } CanFrame; static CanFrame rxBuf[CAN_RX_BUF_SIZE]; static volatile uint16_t rxHead = 0, rxTail = 0; void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t data[8]; if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, data) != HAL_OK) return; uint16_t next = (rxHead + 1) % CAN_RX_BUF_SIZE; if (next != rxTail) { // 缓冲区未满才写入 rxBuf[rxHead].id = rxHeader.StdId; rxBuf[rxHead].dlc = rxHeader.DLC; for (uint8_t i = 0; i < rxHeader.DLC; i++) rxBuf[rxHead].data[i] = data[i]; rxHead = next; } // 缓冲区满则丢弃,避免在中断里阻塞 }

这段代码的要点是中断里只做搬运,rxHead和rxTail用volatile修饰,主循环里判断rxHead != rxTail再取帧解析。缓冲区大小按总线负载估,500k 满负载时中断频率不低,32 帧的余量对一般控制场景够用。

验证改造是否成功,我有个固定习惯:先回环跑一遍自发自收,确认协议层解析正确;再上真实总线,用分析仪对比发出的帧和预期是否一致;最后做一次拔线、重新插上的热插拔测试,看 Bus-Off 恢复是否正常。这三步走完,基本能排除大部分集成问题。

从那以后我每次改 CAN 相关代码,都强制先跑一遍回环再上总线,宁可多花五分钟,也不要在现场对着分析仪猜是线的问题还是代码的问题。希望这份拆解能帮你少走几个弯路,把这块 F103 的 CAN 节点稳稳跑起来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询