资讯详情

STM32F103 HAL库CAN通信实战:从初始化到收发中断的完整指南

📅 2026/10/26 2:33:19 | 华诺云谱 👁 阅读
STM32F103 HAL库CAN通信实战:从初始化到收发中断的完整指南
简介这份资源面向嵌入式开发初学者与需要快速上手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等编译产物完整保留了从外设配置到构建输出的工程结构。资源涵盖CAN时钟与GPIO配置、1Mbps波特率设置、标准帧与扩展帧收发、滤波器配置及收发回调等关键环节读者可据此理解HAL_CAN_Init、HAL_CAN_Transmit、HAL_CAN_Receive与HAL_CAN_ConfigFilter的调用方式并借助逻辑分析仪或串口输出验证总线数据。目前已有4449人学习下载适合作为CAN入门与工程移植的参考模板。1. 拿到一块 F103 板子想跑 CAN最省事的起点是什么手上有一块 STM32F103RBT6 的核心板外挂一个 TJA1050 收发器想跟另一台设备做 CAN 通信结果 CubeMX 里点完 CAN 外设、生成代码、烧进去串口一行日志都没有总线也静默——这种场景我见过太多次。这份资源就是给这种时刻准备的一套基于 HAL 库、在 STM32F103RBT6 上已经调通的 CAN 开发代码包含初始化、过滤器配置、发送、接收中断这几块能直接跑的最小闭环。它不适合想研究 CANopen 协议栈的人适合的是手里有板子、有收发器、想先把「一帧数据发出去、再收回来」这件事跑通然后在此基础上做二次开发的工程师和学生。HAL 库把寄存器操作包了一层好处是移植快代价是很多人只记住了HAL_CAN_Start却不知道过滤器没配对时接收中断永远不会来这份代码的价值就在于把这些容易翻车的点都摆在了明面上。2. 工程结构与 CAN 外设初始化从时钟到波特率的完整链路2.1 目录组织与文件职责这套代码沿用的是 CubeMX 生成工程的骨架没有另起炉灶所以拿到手之后不需要重新理解一套目录约定。核心文件集中在Core/Src和Core/Inc下CAN 相关的逻辑被拆成初始化、发送、接收三块主循环里只留调用。下面是我整理后的典型结构实际工程里文件名可能略有差异但职责划分基本一致CAN_F103_HAL/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── can.h // CAN 句柄与函数声明 │ │ └── stm32f1xx_hal_conf.h │ └── Src/ │ ├── main.c // 时钟、GPIO、主循环 │ ├── can.c // 初始化、过滤器、收发封装 │ ├── stm32f1xx_it.c // CAN_RX0_IRQHandler 等中断入口 │ └── stm32f1xx_hal_msp.c// CAN 引脚与时钟使能 ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ // HAL 库源码 └── MDK-ARM/ 或 STM32CubeIDE/ // 工程文件can.c是二次开发时改动最频繁的文件发送和接收的封装都放在这里主循环只负责组织业务数据。stm32f1xx_hal_msp.c里的HAL_CAN_MspInit负责打开 CAN 时钟和配置 PA11/PA12 复用功能这一块如果漏了后面所有配置都是空谈。2.2 时钟与引脚容易被跳过的一步STM32F103RBT6 的 CAN 挂在 APB1 总线上APB1 默认最高 36MHz。CAN 波特率的计算依赖这个时钟所以第一步必须确认SystemClock_Config里 APB1 的分频没被改乱。常见做法是系统时钟 72MHz、APB1 二分频得到 36MHz然后 CAN 位时序按36MHz / (Prescaler × (1 BS1 BS2))去凑 500kbps。引脚方面CAN 默认复用 PA11CAN_RX和 PA12CAN_TX配置成复用推挽输出接收脚可以加上拉外部收发器一侧再按芯片手册接终端电阻。void HAL_CAN_MspInit(CAN_HandleTypeDef *hcan) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_CAN1_CLK_ENABLE(); // 打开 CAN1 外设时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // PA11/PA12 所在端口时钟 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); 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); }这段代码的关键在于两个时钟使能缺一不可__HAL_RCC_CAN1_CLK_ENABLE漏掉时HAL_CAN_Init会返回错误但很多人不看返回值。接收脚配成上拉输入是稳妥做法因为收发器的 RXD 在空闲时输出高电平浮空输入容易在总线无设备时读到抖动。发送脚必须是复用推挽配成普通输出的话 CAN 控制器根本接管不了引脚。2.3 位时序参数怎么定初始化里最容易出错的是CAN_InitTypeDef的四个时序参数。以 36MHz 的 APB1 时钟、目标 500kbps 为例分频系数取 4则时间量子为 1/9MHz一个位需要 18 个量子BS1 取 15、BS2 取 2、SJW 取 1加起来正好 18。这套参数在多数 500k 总线上都能稳定工作采样点落在 87.5% 附近属于偏保守但兼容性好的配置。CAN_HandleTypeDef hcan; void 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 hcan.Init.TimeSeg2 CAN_BS2_2TQ; // 相位段2 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(); // 初始化失败必须停下来查 } }AutoBusOff打开后节点因为错误计数超限进入 Bus-Off 状态时硬件会自动尝试恢复省掉手动干预。AutoRetransmission在调试阶段建议保留否则一帧发不出去就丢了排查时很难判断是总线问题还是代码问题。Mode字段在只有一块板子、没有第二个节点的时候可以临时改成CAN_MODE_LOOPBACK自发自收能验证收发链路是否通这个技巧在手上只有一个收发器时特别有用。3. 过滤器、发送与接收中断让一帧数据真正流动起来3.1 过滤器组配置接收中断不来的头号原因HAL 库的 CAN 接收依赖过滤器过滤器没配或者配错HAL_CAN_GetRxMessage永远拿不到数据中断也不会触发。F103 的 CAN1 有 14 个过滤器组每个组可以配成屏蔽位模式或标识符列表模式。最省事的做法是开一组、配成屏蔽位模式、掩码全 0等于接收所有帧先把链路跑通再收紧。void CAN_Filter_Config(void) { CAN_FilterTypeDef filter; filter.FilterBank 0; // 使用过滤器组 0 filter.FilterMode CAN_FILTERMODE_IDMASK; // 屏蔽位模式 filter.FilterScale CAN_FILTERSCALE_32BIT; // 32 位尺度 filter.FilterIdHigh 0x0000; // 标识符高 16 位 filter.FilterIdLow 0x0000; filter.FilterMaskIdHigh 0x0000; // 掩码全 0全部放行 filter.FilterMaskIdLow 0x0000; filter.FilterFIFOAssignment CAN_RX_FIFO0; // 关联到 FIFO0 filter.FilterActivation ENABLE; filter.SlaveStartFilterBank 14; // 单 CAN 时无实际作用 if (HAL_CAN_ConfigFilter(hcan, filter) ! HAL_OK) { Error_Handler(); } }掩码全 0 意味着「不关心任何位」所有帧都进 FIFO0。等业务稳定后把FilterIdHigh/Low填成目标标识符、掩码填成需要匹配的位就能只收关心的帧。这里有个细节32 位尺度下标识符要左移 5 位再填因为标准帧的 11 位 ID 在寄存器里是从 bit5 开始对齐的直接填原始 ID 会导致匹配不上这是很多人第一次配过滤器时踩的坑。3.2 发送一帧标准数据帧发送流程是「填报文头 → 填数据 → 调发送函数 → 等邮箱空」。HAL 库的HAL_CAN_AddTxMessage会把报文放进三个发送邮箱之一返回 OK 只代表进了邮箱不代表已经发到总线上真正发完要看HAL_CAN_GetTxMailboxesFreeLevel或者等发送完成回调。uint8_t CAN_SendStd(uint32_t id, uint8_t *data, uint8_t len) { CAN_TxHeaderTypeDef txHeader; uint32_t txMailbox; txHeader.StdId id; // 标准帧 11 位标识符 txHeader.ExtId 0; txHeader.IDE CAN_ID_STD; // 标准帧 txHeader.RTR CAN_RTR_DATA; // 数据帧 txHeader.DLC len; // 数据长度 0~8 txHeader.TransmitGlobalTime DISABLE; if (HAL_CAN_AddTxMessage(hcan, txHeader, data, txMailbox) ! HAL_OK) { return 1; // 邮箱满或总线异常 } return 0; }DLC超过 8 会被硬件截断经典 CAN 一帧最多 8 字节这是协议本身的限制不是 HAL 的问题。txMailbox是输出参数告诉你这帧进了哪个邮箱调试时可以打印出来确认三个邮箱的占用情况。如果连续发送时返回失败多半是三个邮箱都满了说明总线速率跟不上发送节奏要么降发送频率要么检查总线上有没有节点一直不 ACK。3.3 接收中断与回调接收走中断方式时需要在初始化后调用HAL_CAN_ActivateNotification打开CAN_IT_RX_FIFO0_MSG_PENDING然后在stm32f1xx_it.c里确保CAN_RX0_IRQHandler调用了HAL_CAN_IRQHandler最后在HAL_CAN_RxFifo0MsgPendingCallback里取数据。这条链路任何一环断了数据都到不了应用层。void CAN_StartReceiveIT(void) { HAL_CAN_Start(hcan); // 必须先 Start 才能开中断 HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING); } void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData) ! HAL_OK) { return; } // rxHeader.StdId 为标识符rxHeader.DLC 为实际长度 // 业务处理放在这里注意不要做耗时操作 }回调里只做取数据和轻量处理耗时逻辑丢到主循环用标志位触发否则中断里拖太久会影响后续帧的接收。HAL_CAN_Start必须在开中断之前调用顺序反了中断使能了但外设没启动同样收不到数据。4. 避坑与排查CAN 调不通时按这个顺序查4.1 现象发送返回 OK但总线上测不到波形原因通常是收发器没工作或者引脚复用没生效。先量 TJA1050 的 VCC 和 STB 引脚STB 拉低才是正常模式再确认 PA12 是否真的被 CAN 外设接管用示波器看发送瞬间有没有电平跳变。如果收发器供电正常、引脚也对检查HAL_CAN_MspInit是否真的被调用——有时候 CubeMX 重新生成代码会把用户手写的 MspInit 覆盖掉。4.2 现象接收中断一次都不进按顺序查三处过滤器是否FilterActivation ENABLE、HAL_CAN_Start是否在ActivateNotification之前、中断向量表里CAN_RX0_IRQHandler是否指向了HAL_CAN_IRQHandler。这三处任意一处缺失都会导致中断静默。还有一种情况是过滤器配了但标识符对齐错了帧被硬件过滤掉表现和没配过滤器一样。4.3 现象两块板子单独都能发接一起就互相收不到十有八九是波特率不一致或者终端电阻缺失。CAN 总线两端各需要 120 欧终端电阻只接一块板子做回环测试时不需要但两个节点通信时缺了终端电阻信号反射会让采样出错。用万用表量 CAN_H 和 CAN_L 之间的电阻两个节点都接上时应该接近 60 欧只有 120 欧说明只接了一端。4.4 现象跑一段时间后发送全部失败查HAL_CAN_GetError的返回值如果是 Bus-Off说明错误计数超限常见于总线上有节点频繁发送错误帧或者波特率偏差过大。AutoBusOff打开后硬件会尝试恢复但根本原因还得从布线和波特率上找。另外注意AutoRetransmission打开时一帧一直发不出去会反复占用邮箱表现就是三个邮箱全满、后续发送全部返回失败。5. 二次开发把最小闭环改成能用的业务代码拿到这套代码之后真正要做的改造集中在三个方向。第一是过滤器收紧把掩码全 0 改成按业务标识符匹配避免无关帧占用中断第二是发送侧加队列主循环里不要直接调HAL_CAN_AddTxMessage而是把待发帧塞进环形缓冲区用一个发送任务按邮箱空闲情况逐帧发出这样总线繁忙时不会丢帧第三是接收侧做协议解析回调里只把StdId和DLC连同数据拷进接收队列解析放到主循环避免中断里做分支判断。下面这个发送队列的骨架是我一般会先搭的改动量不大但能明显提升稳定性typedef struct { uint32_t id; uint8_t data[8]; uint8_t len; } CanFrame_t; #define TX_QUEUE_SIZE 16 static CanFrame_t txQueue[TX_QUEUE_SIZE]; static volatile uint8_t txHead 0, txTail 0; // 主循环里周期调用邮箱有空位就发一帧 void CAN_TxPoll(void) { if (txHead txTail) return; // 队列空 if (HAL_CAN_GetTxMailboxesFreeLevel(hcan) 0) return; // 无空闲邮箱 CanFrame_t *f txQueue[txTail]; if (CAN_SendStd(f-id, f-data, f-len) 0) { txTail (txTail 1) % TX_QUEUE_SIZE; // 发送成功才出队 } }队列长度 16 是按常见控制场景估的帧率高的时候可以加大但要注意 RAM 占用F103RBT6 只有 20KB SRAM队列别开太大。txTail只在发送成功后推进失败时下一轮重试配合AutoRetransmission能覆盖大部分瞬时拥塞。验证改造是否有效我习惯用两步先用CAN_MODE_LOOPBACK自发自收跑一遍队列逻辑确认收发和解析都对再切回CAN_MODE_NORMAL接真实节点用逻辑分析仪抓总线看帧间隔和错误帧计数。回环模式能排除硬件和接线因素把问题锁定在软件层这个习惯帮我省过不少排查时间。还有一个容易被忽略的点是时间戳。HAL 的接收回调里拿不到硬件时间戳如果业务需要判断帧的到达顺序或者做超时检测得在回调里用HAL_GetTick()打一个软件时间戳一起入队。精度到毫秒级对大多数控制场景够用要求更高就得开 CAN 的定时器触发模式那是另一个复杂度了。从那以后我每次拿到一份「已调通」的 CAN 代码都强制先跑一遍回环、再量一遍终端电阻、最后才接真实总线这三步走完再谈业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑