本篇,通过CubeMX + Keil,实现 STM32 的 CAN 通信 (发送数据+接收数据+筛选器配置) 。
只讲图解实操、不谈底层原理,新手可直接复刻!
若文中存在疏漏、章节衔接不畅,诚盼留言指正,将虚心聆听、斟酌修改,力求内容清晰通达。
目录
[一、CAN 通信 -- 核心特点 (与UART/RS485对比)](#一、CAN 通信 -- 核心特点 (与UART/RS485对比))
[二、CAN 通信 -- 硬件组成 (4大核心部件)](#二、CAN 通信 -- 硬件组成 (4大核心部件))
[三、CAN 通信 -- 实物接线图解 (核心板 / 开发板)](#三、CAN 通信 -- 实物接线图解 (核心板 / 开发板))
[四、工程准备 -- CubeMX / Keil 基础工程](#四、工程准备 -- CubeMX / Keil 基础工程)
[五、CubeMX 配置 -- CAN 引脚 / 波特率 / 中断](#五、CubeMX 配置 -- CAN 引脚 / 波特率 / 中断)
[六、CAN 初始化 -- 启动外设](#六、CAN 初始化 -- 启动外设)
[七、CAN 数据发送 -- 完善版函数](#七、CAN 数据发送 -- 完善版函数)
[八、CAN 接收数据 -- 核心三步(筛选器 / 使能接收 / 回调函数)](#八、CAN 接收数据 -- 核心三步(筛选器 / 使能接收 / 回调函数))
[九、CAN 接收进阶 -- 筛选器配置 + 机制优化](#九、CAN 接收进阶 -- 筛选器配置 + 机制优化)
[十、CAN 调试 -- 常见错误排查](#十、CAN 调试 -- 常见错误排查)
一、CAN 通信 -- 核心特点 (与UART/RS485对比)
通过下表对比,可快速理解 CAN 的适用场景 与核心优势。无需深究底层协议,记住关键特性即可。
| 特性 | UART | RS485 | CAN |
|---|---|---|---|
| 通信模式 | 点对点 (私聊) | 主从式 (领导开会) | 多主式 (像圆桌会议) |
| 硬件连接 | 1对1 | 总线型 (1主多从) | 总线型 (多主多从) |
| 信号类型 | 单端信号(TXD/RXD) | 差分信号(A/B) | 差分信号(H/L) |
| 抗干扰性 | 弱 | 强 (差分信号) | 极强(差分+硬件级错误处理) |
| 通信距离 | 短(通常<1米) | 远(可达数百米) | 中远(几十米) |
| 最大速率 | 较高(数Mbps) | 高(常用 115200 bps) | 高(最高1Mbps) |
| 数据长度 | 灵活可变 | 灵活可变 | 8字节 (固定) |
| 冲突处理 | 无仲裁,软件处理 | 主从轮询,无硬件处理 | 硬件仲裁,基于ID优先级 |
| 可靠性 | 奇偶校验(可选) | 依赖软件协议(如CRC) | 硬件级 CRC+ACK+错误帧 |
| 开发难度 | 最简单 | 中等 (常配合Modbus RTU) | 中等 |
| 硬件成本 | 极低 | 低(需收发器) | 中 (需收发器) |
| 优点 | 简单、通用、成本低 | 距离远、抗干扰、可组网 | 多主、高可靠、实时性强 |
| 缺点 | 无组网、距离短 | 主从模式、实时性差 | 协议复杂、单帧数据量小 |
| 应用场景 | 开发调试、模块直连 | 工业PLC、楼宇自动化 | 汽车、工业现场总线 |
二、CAN 通信 -- 硬件组成 (4大核心部件)
每个CAN通信节点的硬件,由4个部分组成。以 STM32 的 CAN 通信为例 :
- CAN 控制器:STM32芯片,芯片内部集成了 CAN控制器 功能;
- CAN 收发器:准确名称是 CAN 电平转换(模块);即下图中的 TJA1050。
- 总线: 狭义地理解(别杠字眼),就是两根导线;即下图中的 H、L 这两根导线。
- 终端电阻: 相距最远的两个节点上,各放置一个120R电阻,H与L之间阻值约60R。

1、CAN 控制器(STM32芯片内置)
STM32 芯片内部集成 CAN 控制器,硬件自动实现总线仲裁、错误管理、CRC 校验(对比 RS485,这些都需要软件写协议实现),开发者仅需调用 HAL 库函数即可。
不同的 STM32 型号所内置的CAN控制器稍有不同,F103(1 组、CAN 2.0)、F407(2 组、CAN1+CAN2、CAN 2.0)、H750(多组、CAN 2.0、CAN FD)。
引脚规律(建议熟记,避坑关键):
- CAN1:默认 RX_PA11 / TX_PA12,复用 RX_PB8 / TX_PB9(本文用此组)
- CAN2:默认 RX_PB5 / TX_PB6,复用 RX_PB12 / TX_PB13
- 注:F103 仅支持 CAN1,句柄为
hcan;F407 支持 CAN1/CAN2,句柄为hcan1/hcan2。
2、CAN 收发器(电平转换电路)
作用:
- 将 STM32 输出的3.3V TTL 信号 转换为 CAN 总线的差分信号(STM32 无法直接驱动物理总线),硬件自动完成,无需编程。
CAN 差分信号定义(了解即可):
- 显性电平(逻辑 0):CAN_H≈3.5V、CAN_L≈1.5V(差分电压≈2V,优先级更高)
- 隐性电平(逻辑 1):CAN_H≈2.5V、CAN_L≈2.5V(差分电压≈0V,总线空闲状态)
常用型号:
- TJA1050:性价比最高,支持 CAN2.0,目前市面流通最多、平替型号最多。新手首选。
- TJA1042:支持 CAN 2.0,抗干扰更强。
- TJA1051:支持 CAN 2.0 与 CAN FD(注意:STM32F407不支持FD)。
3、CAN 总线(两根导线)
狭义理解: 并联所有 CAN 节点的 CAN_H、CAN_L 两根线。
线材选择(按需使用,无需刻意追求专用线):
- 临时测试:杜邦线、硅胶线均可;新手可直接用杜邦线测试;
- 项目使用:铜芯双绞线(最常用);
- 强干扰场景(如伺服):带屏蔽层的双绞线;
- 线径参考:以500Kbps为例,40米内建议0.34mm²;80米内建议0.75mm²;130米内建议1.5mm²;换个思路,当大于100米时,不应考虑加粗线径,而是增加中继器或降低波特率。
4、终端电阻(两个120Ω,必配!)
作用: 消除总线末端的信号反射,避免波形畸变导致通信失败。
核心规则: 整个CAN网络有且只有两个120Ω电阻,接在物理总线最远的两个节点的 H 与 L 之间。
部分CAN分析仪、开发板内置终端电阻(跳线帽 / 软件启闭),组网时务必检查,避免多接 / 漏接。
若不确定网络中的电阻情况,可断电后用万用表测 CAN_H 与 CAN_L 之间的阻值,正常为60Ω 左右 (两个120Ω并联后的等效电阻);
5、共地
CAN 通信虽然是差分传输,理论上依赖电压差来传递信号,但实际应用中,共地仍然是必要的,尤其是在远距离或不同电源系统中。
- **短距离 / 同电源:**无需刻意共地;
- **远距离 / 不同电源:**建议将所有节点 GND 短接,避免地电位差过大导致差分信号失真。
- 带屏蔽层的双绞线: 屏蔽层必须采用单点接地!以避免接地环路形成新的干扰通道。接地点通常干扰源较强的一端
三、CAN 通信 -- 实物接线图解 (核心板 / 开发板)
1、接线要点
- **引脚匹配:**配置的 CAN 引脚,如 PB8/PB9,需与原理图、硬件实际接线一致。
- **总线连接:**H 接 H,L 接 L,无需交叉反接。
- 供电要求: 主流 CAN 收发器需5V供电。3.3V供电会导致收发器不工作。
- 电阻配置: 最远端、有且仅有两个 120Ω,中间节点禁用,实测 H-L 阻值≈60Ω;
2、场景一:核心板接线(需外接 TJA1050 模块)
首先,以核心板作为接线示范。
多数 STM32 核心板均未集成 CAN 收发器,需外接 CAN 收发器模块,如 TJA1050 等。
具体接线方法,参考下图:
- 供电: 核心板由USB的5V 供电,TJA1050 的 VCC 接核心板的5V,GND 接 GND。
- 信号:TJA1050 的 RX 接核心板 PB8,TX 接核心板 PB9。
- **组网:**TJA1050 的 H 接分析仪 CAN_H 线,L 接分析仪 CAN_L 线。
- 电阻: TJA1050 已带 120Ω ;CAN 分析仪启用 120Ω 开关。总线 H-L实测 ≈ 60Ω。
上图实验器材清单:
| 器材 | 技术说明与推荐建议 | 链接 |
|---|---|---|
| F407VE 核心板 | 板载DAP调试器和USB转TTL功能 | 【链接】 |
| TJA1050 模块 | 推荐升级为TJA1051等更优型号 | 【链接】 |
| CAN 分析仪 | 图款是公司很多年前买的 可使用平价型号, 当前市场几十元产品性能已满足需求 | 【链接】 |
| 杜邦线 | 母对母、适用于实验板间连接 | 【链接】 |
| CAN连接线 | 测试时可用杜邦线; 正式项目推荐使用屏蔽双绞线 | 【链接】 |
3、场景二:开发板接线(板载CAN收发器,无需外接模块)
市面大多数STM32开发板已集成 CAN 电平转换电路,如TJA1050,无需连接外部 CAN 收发器 。
具体接线,参考下图:
- 供电 :开发板由 USB 线供电(5V ), PCB设计布线时已给 TJA1050 电路 5V 供电;
- 信号: 用跳线帽**,**将开发板的 PB8 与 TJA1050 的 RX 短接,PB9 与 TJA1050 的 TX 短接;
- **组网:**开发板 CAN 接线端子的 H 接分析仪 H,L 接分析仪 L;
- 电阻: 开发板已带 120Ω ;分析仪启用 120Ω;其它节点禁用。
注意:图中最右侧的CAN收发器仅用于展示节点3的接线方式,并非节点1所需设备。
上图实验器材清单:
| 实验器材 | 备注说明 | 链接 |
|---|---|---|
| F407VE 开发板 | 板载CAN收发电路、DAP、USB转TTL | 参考链接 |
| TJA1050 模块 (可选) | 如需扩展节点可搭配使用 | 参考链接 |
| CAN 分析仪 | 可使用平价型号 | 参考链接 |
| 杜邦线 | 母对母、用于连接外接模块 | 参考链接 |
| CAN导线 | 测试时可用杜邦线; 项目建议使用屏蔽双绞线 | 参考链接 |
四、工程准备 -- CubeMX / Keil 基础工程
核心准备共3步:创建工程、实现 printf 串口输出(调试必备)、新建用户 CAN 文件(避免 CubeMX 重生成代码覆盖)。
新手按步骤操作即可实现基础工程。
步骤 1:创建工程
本教程基于现有CubeMX工程进行讲解,如需了解新建工程的基础操作,请参考:
步骤 2:配置 UART1 并实现 printf 重定向
通过printf函数将接收到的数据输出至串口助手。
这是调试 CAN 必备,无法省略。
如需了解printf的实现方法,请参考:
步骤 3:工程验证
实现printf功能后,请在工程中添加以下代码,完成编译烧录后进行测试:
c
printf( "Hello World! 你好吗?" );
打开串口助手(取消16进制显示模式),确认能正常接收该字符串。
该测试目的:
- 验证系统时钟配置正确
- 确认程序正常运行
- 检查printf重定向是否成功
步骤 4:新建用户 CAN 文件
在工程文件夹中,新建两个文件:bsp_CAN.c 和 **bsp_CAN.h。**文件用途:
- 集中管理将要编写的3个核心函数 (发送函数、筛选器配置函数、回调函数)。
- 便于跨型号移植,支持F103、F407、H750等多种芯片型号,减少重复工作。
具体操作:
- 在工程目录下,新建 Bsp 文件夹 (可自定义名称)
- 在 Bsp 文件夹内,新建 CAN 子文件夹
- 在 CAN 文件夹内新建 2个 文本文档,重命名为 bsp_CAN.c 和bsp_CAN.h(需显示文件扩展名,设置:文件夹菜单→文件→选项→查看→取消勾选 "隐藏已知文件类型的扩展名")
操作完成后,文件结构是这样子的:

提示说明:
- 也可以直接在Keil开发环境中新建这两个文件,效果等同。
- 强烈不建议(禁止):把用户函数直接写至CubeMX生成的 can.c 和 can.h 。原因: 当工程迁移至未启用CAN功能的新项目时,重新生成代码会删除文件can.c和can.h,将导致文件里的用户函数一并丢失。
步骤 5:Keil 中添加源文件
具体操作,如下图。

添加完成后,keil资源管理窗口,将是这个样子的:

步骤 6:配置 Keil 头文件路径
作用:确保编译器能找到 bsp_CAN.h 。
具体操作,如下图。

步骤 7:添加头文件保护
新建的每个头文件,都需要添加头文件保护,以避免多文件包含时重复定义导致错误。
具体操作
- 在 bsp_CAN.h 中,添加如下代码。
cpp#ifndef __BSP_CAN_H #define __BSP_CAN_H // 头文件引用、函数声明等内容 #endif /* __BSP_CAN_H */
步骤 8:bsp_CAN.h 添加头文件引用
具体操作 ,如下图。
注意,下图第10行是小编的UART文件,替换成你的文件,或取消。

步骤 9:bsp_CAN.c 添加头文件引用
具体操作,如下图。

步骤 10:main.c 添加头文件引用
具体操作,如下图。

准备工作已完成!
现在可以开始配置CAN模块!
五、CubeMX 配置 -- CAN 引脚 / 波特率 / 中断
步骤 1:启用 CAN1
具体操作:
- 在 Connectivity 分组中选择 CAN1
- 勾选 Activated选项以启用 CAN1
在启用 CAN1 时,CubeMX 将自动启用默认引脚 PA11+PA12, 无需手动配置引脚。
- CAN_RX = PA11
- CAN_TX = PA12
步骤 2:修改 CAN 引脚
检查开发板原理图,或开发板实物,确认使用哪组引脚连接CAN:PA11+PA12 或 PB8+PB9。
本篇开发板,使用的是 PB8+PB9,因此需要修改CAN引脚。
为什么不用 CAN 的默认引脚 PA11+PA12 ?
因为 PA11+PA12 也是 USB 默认引脚。如果工程中要同时用到USB和CAN,可以减少冲突。

具体操作:
- 单击 PB9 引脚,将其功能配置为 CAN1_TX
- 单击 PB8 引脚,将其功能配置为 CAN1_RX
修改 PB8+PB9 为CAN功能后,PA11+PA12 引脚会被 CubeMX 自动释放,无需手动释放。

步骤 3:配置 CAN 波特率
CAN 波特率由4个参数决定:
| 参数 | 说明 |
|---|---|
| Prescaler | 时钟分频系数 |
| BS1 | 时间段1 (传播段 + 相位缓冲段1) |
| BS2 | 时间段2 (相位缓冲段2) |
| SJW | 同步跳转宽度 |
波特率计算公式:(了解即可,无需手动计算)
CAN波特率 = APB1时钟频率 / [Prescaler × ( BS1 + BS2 + SJW ) ]
- F407:系统时钟 168MHz → APB1 时钟 42MHz;
- F103:系统时钟 72MHz → APB1 时钟 36MHz。
常用波特率参数(已验证可用。新手直接抄参数,避免计算错误)
| 芯片型号 | 波特率 | Prescaler | BS1 | BS2 | SJW |
|---|---|---|---|---|---|
| F407 | 250Kbps | 21 | 6 | 1 | 1 |
| F407 | 500Kbps | 6 | 11 | 2 | 1 |
| F407 | 1MKbps | 6 | 5 | 1 | 1 |
| F103 | 250Kbps | 9 | 13 | 2 | 1 |
| F103 | 500Kbps | 9 | 6 | 1 | 1 |
| F103 | 1MKbps | 9 | 2 | 1 | 1 |
配置示范:STM32F407、系统时钟 168MHz、APB1总线 42MHz、目标波特率 500Kbps
具体操作 :
- Prescaler (时钟分频器):6
- BS1 (时间段1,包含传播段和相位缓冲段1):11 Times
- BS2 (时间段2,即相位缓冲段2) :2 Times
- SJW (同步跳转宽度):1 Time
- 其它参数 默认
步骤 4:启用 CAN1 接收中断
具体操作
- 进入 CAN1 → NVIC Settings 标签页
- 勾选 CAN1 RX0 interrupts,启用接收 FIFO0 的中断功能
- 其它参数 默认
FIFO 简单说明 (了解即可,不深究)
CAN 控制器采用 FIFO(先进先出缓冲区)机制存储接收报文。
CAN 控制器共有两个接收缓冲区:FIFO0、FIFO1,各含 3 个邮箱,由硬件自动缓存接收报文;
CAN1、CAN2共用这两个缓存区。
两个接收缓冲区功能相同,可自行选择使用哪一个。
注意:在代码实现时,需根据使用的 FIFO 实现对应回调函数:
- 使用 FIFO0 时,实现中断回调函数:
HAL_CAN_RxFifo0MsgPendingCallback( ) - 使用 FIFO1 时,实现中断回调函数:
HAL_CAN_RxFifo1MsgPendingCallback( )
启用 FIFO0 中断后,接收到报文时系统自动触发HAL_CAN_RxFifo0MsgPendingCallback回调函数;
单个 FIFO0 即可满足 99% 的新手场景,无需启用 FIFO1,避免复杂。
步骤 5:代码生成
完成上述配置后,即可生成配置代码。
具体操作
- 点击 CubeMX 右上角 Generate Code 按钮,CubeMX 将生成 CAN1 配置。
至此,CAN1模块的软件初始化配置工作已全部完成。
接下来可进入应用代码编写和功能实现阶段。
六、CAN 初始化 -- 启动外设
步骤 1:启动 CAN 外设
通过调用函数:HAL_CAN_Start(&hcan1),即可启动 CAN1 。
函数返回值为 HAL 状态(HAL_OK = 成功,其余 = 失败)。
对比UART,UART启动是在CubeMX生成的代码中被调用,因为会100%成功,无异常可言。
而CAN启动,需要用户手动调用,通过判断函数的返回状态, 才能知道CAN 启动是否成功。
(很多新手不习惯使用printf!调试出现故障时,完全靠盲猜,无法定位哪个节点的问题!)
具体操作
- main.c中,MX_CAN1_Init () 之后,while (1) 之前,编写以下代码,以启动CAN
- F407/F103 通用。F103需替换句柄 &hcan1 为 &hcan
cpp/** 启动CAN,并检查返回值:成功-0、错误-1、忙错 误-2、超时-3 **/ if (HAL_CAN_Start(&hcan1) == HAL_OK) { printf("CAN1 启动成功! \r"); } else { printf("CAN1 启动失败! \r"); //Error_Handler(); // 错误处理 }
编写完成后,代码位置如下:

关键说明
这个位置 ,建议编译、烧录,运行后观察串口助手的输出。
- 烧录后,串口助手打印 "CAN1 启动成功",则说明 CAN 外设初始化正常。
- 启动失败原因:引脚配置错误、收发器未供电、引脚未正确连接收发器(导致总线无法进入隐性电平);
七、CAN 数据发送 -- 完善版函数
CAN 发送与 UART 不同,需 先配置帧参数 → 等待发送邮箱空闲 → 填入邮箱排队发送(硬件自动等待总线空闲后实际发送),新手先理解流程,再直接使用完善版函数。
核心概念
- 每帧数据,最大8 字节。超出部分会被忽略。
- 发送邮箱:CAN1 有 3 个发送邮箱,需等待至少 1 个空闲才能发送 。
- 帧类型:标准帧(11 位 ID,0x000~0x7FF)、扩展帧(29 位 ID,0x00000000~0x1FFFFFFF),均为数据帧(CAN_RTR_DATA)。
为了便于理解实现过程,本节分两步走,先编写基础发送功能,再完善为增强版发送函数。
基础发送函数(仅供流程理解,无需实际编写)
本函数以最小的步骤、完整地展示 CAN 发送数据的配置流程。
建议花1~2分钟细细阅读下面代码,这将帮助您快速理解发送流程。
cpp
void CAN1_SendData_Test(void)
{
CAN_TxHeaderTypeDef canTxHeader = {0}; // CAN发送消息结构体,定义了发送报文的关键参数
uint32_t txMailbox = 0; // 发送邮箱编号:0~2; 用于记录发送成功时所用的邮箱编号; 被发送函数HAL_CAN_AddTxMessage()赋值
uint8_t txData[8] = {5, 2, 0, 1, 3, 1, 4}; // 发送数据缓冲区
/* 1. 配置数据帧参数 */
canTxHeader.ExtId = 0x123 ; // 扩展帧ID; 值有效范围:0x00000000~0x1FFFFFFF
canTxHeader.IDE = CAN_ID_EXT; // 帧格式:扩展帧(CAN_ID_EXT); 此值是HAL库文件stm32f4xx_hal.h里的宏定义
canTxHeader.RTR = CAN_RTR_DATA; // 帧类型:数据帧
canTxHeader.DLC = 7; // 数据长度,单位:字节数
canTxHeader.TransmitGlobalTime = DISABLE; // 时间戳功能:禁用; 添加到最后两个字节:Data[6]、Data[7]
/* 2. 等待发送邮箱就绪 */
while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) == 0)
{
// 可在此处添加任务延时或超时退出逻辑
}
/* 3. 执行数据发送 */
HAL_CAN_AddTxMessage(&hcan1, &canTxHeader, txData, &txMailbox); // 返回发送状态; 成功-0、错误-1、忙错误-2、超时-3
}
步骤 1:发送函数
理解上面的发送函数后,我们把它稍作完善:
支持标准 / 扩展帧切换、超时保护、数据长度限制,适合项目开发 (可移植,直接复制)。
具体操作
- 在 bsp_CAN.c 文件中,编写(直接复制)下面发送函数:
cpp/****************************************************************************** * 函 数: CAN1_SendData * 功 能: CAN发送数据函数 * 参 数: uint32_t type 帧格式; 参数:CAN_ID_EXT、CAN_ID_STD * uint32_t ID 帧ID * uint8_t* msgData 需发送数据的地址 * uint8_t len 发送的字节数; 最大值:8 * 返回值: 发送状态; 成功-0、错误-1、忙错误-2、超时-3 * 备 注: CAN_TxHeaderTypeDef 是HAL库的CAN发送帧头部信息结构体,成员列表如下: uint32_t StdId 标准帧的ID, 11位, 值范围:0x000~0x7FF uint32_t ExtId 扩展帧的ID, 29位, 值范围:0x00000000~0x1FFFFFFF uint32_t DLC 接收到的字节数, 单位:byte, 值范围:0~8 uint32_t IDE 帧格式; 0_标准帧、4_扩展帧 uint32_t RTR 帧类型; 0_数据帧、2_遥控帧 uint32_t TransmitGlobalTime 使能时间戳,添加到Data[6]和Data[7] ******************************************************************************/ uint8_t CAN1_SendData(uint32_t type, uint32_t ID, uint8_t *msgData, uint8_t num) { // 定义两个变量 static CAN_TxHeaderTypeDef canTxHeader = {0}; // CAN发送消息的结构体,定义了发送报文的关键参数 static uint32_t txMailbox = 0; // 用于记录发送成功时所用的邮箱编号:0~2; 被发送函数HAL_CAN_AddTxMessage()赋值 /* 限制数据的字节数 */ if (num > 8) // 判断字节是否超过8字节 num = 8; // 如果超过8字节,只发送前8个字节 /* 1.配置帧信息 */ if (type == CAN_ID_EXT) // 扩展帧 { canTxHeader.ExtId = ID ; // 扩展帧ID; 值有效范围:0x00000000~0x1FFFFFFF canTxHeader.IDE = CAN_ID_EXT; // 帧格式:扩展帧(CAN_ID_EXT); 此值是HAL库文件stm32f4xx_hal.h里的宏定义 } else // 标准帧 { canTxHeader.StdId = ID; // 帧ID; 值有效范围:0x000~0x7FF canTxHeader.IDE = CAN_ID_STD; // 帧格式: 标准帧(CAN_ID_STD); 此值是HAL库文件stm32f4xx_hal.h里的宏定义 } canTxHeader.RTR = CAN_RTR_DATA; // 数据帧 canTxHeader.DLC = num; // 数据字节数 canTxHeader.TransmitGlobalTime = DISABLE; // 使能时间戳添加到最后两个字节:Data[6]、Data[7] /* 2.等待发送邮箱空闲 */ uint8_t times = 0; // 注意:如果发送超时失败,总线一直在忙的可能性极低,更大的可能是,CAN没有连接到收发器 while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) == 0) // 每次发送前,要等待至有发送邮箱空闲。共有3个发送邮箱。如果返回值为0,即没有发送邮箱空闲,继续等 { HAL_Delay(1); if (times++ >= 5) { times = 0; printf("\r发送失败:等待超时!检查CAN1的线路是否已连接到收发器\r"); return 3; } } /* 3.发送,并返回发送状态:成功-0、错误-1、忙错误-2、超时-3 */ return HAL_CAN_AddTxMessage(&hcan1, &canTxHeader, msgData, &txMailbox); // 返回发送状态; 成功-0、错误-1、忙错误-2、超时-3 }
步骤 2:添加函数声明 (bsp_CAN.h中)
具体操作
- 在 bsp_CAN.h 中,添加CAN1_SendData()的函数声明 ,如下行;
- uint8_t CAN1_SendData(uint32_t type, uint32_t ID, uint8_t *msgData, uint8_t num);
编写完成后,位置如下图:

步骤 3:调用示例
具体操作
- 在main.c 中,while(1)之前 / 之内 均可
- 编写这段发送测试代码
cpp// 示例1:发送标准帧(ID:0x123,4字节数据:0x01,0x02,0x03,0x04) uint8_t canData1[] = {0x01, 0x02, 0x03, 0x04}; CAN1_SendData(CAN_ID_STD, 0x123, canData1, 4); // 示例2:发送扩展帧(ID:0x123456,5字节字符串:Hello) uint8_t canData2[] = "Hello"; CAN1_SendData(CAN_ID_EXT, 0x123456, canData2, 5);
编写完成后,位置如下图:
步骤 4:功能验证
- 编译、烧录程序;
- CAN 分析仪连接总线,设置相同波特率 500Kbps;
- 分析仪能捕获到两帧报文,显示正确的 ID、字节数、数据,说明发送功能正常。
CAN分析仪捕获结果,如下图:

八、CAN 接收数据 -- 核心三步(筛选器 / 使能接收 / 回调函数)
CAN 接收必须完成三步 ,缺一不可:配置筛选器→使能接收中断→实现中断回调函数,新手先配置为「接收所有报文」,验证通信后再优化筛选规则。
核心概念
- CAN 筛选器:STM32 内置 28 组硬件筛选器,可筛选报文 ID,仅符合规则的报文会存入 FIFO 并触发中断。
- 不配置筛选器则无法接收任何报文(CubeMX 无法自动生成,必须手动编程)。
步骤 1:CAN 筛选器初始化函数
配置为掩码模式 + 32 位宽,接收所有标准 / 扩展数据帧,存入 FIFO0。
具体操作
- 复制下面函数到 bsp_CAN.c 文件;
- 在 bsp_CAN.h 头文件中添加函数声明:void CAN1_FilterInit(void) ;
cpp/****************************************************************************** * 函 数: CAN1_FilterInit * 功 能: CAN1筛选器初始化 * 备 注: 配置为接收所有数据帧(标准/扩展帧) * 参 数: 无 * 返回值: 无 ******************************************************************************/ void CAN1_FilterInit(void) { /* 1. 配置筛选器 */ CAN_FilterTypeDef sFilterConfig = {0}; // 过滤器配置结构体 sFilterConfig.FilterBank = 1; // 筛选器组编号 sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式 sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // 32位模式 sFilterConfig.FilterIdHigh = 0x0000; // ID(验证码)的高16位 sFilterConfig.FilterIdLow = 0x0000; // ID(验证码)的低16位 sFilterConfig.FilterMaskIdHigh = 0x0000; // 掩码高16位; 位匹配,位0-此位都通过、位1-此位需要与ID位相同 sFilterConfig.FilterMaskIdLow = 0x0000; // 掩码低16位;位匹配,位0-此位都通过、位1-此位需要与ID位相同 sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; // 分配到 FIFO 0 sFilterConfig.FilterActivation = ENABLE; // 使能筛选器 sFilterConfig.SlaveStartFilterBank = 14; // 设置CAN2的筛选器起始编号; CAN1、CAN2共用28个筛选器(0~27),默认情况下:CAN1用0~13、CAN2必须14~27。此值的设置范围14~27, 即必须14起步。因为CAN1、CAN2共用28个筛选器(0~27),设置后,CAN1可用筛选器为(0~值-1), CAN2可用筛选器为(值~27); 标准库没有这个选项,CAN1默认0~13, CAN2默认14~27; 当使用寄存器操作时最自由,CAN1可以0~27, CAN2必须14~27。 /* 2. 初始化筛选器 */ if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK) { printf("CAN1筛选器初始化失败!\r\n"); // Error_Handler(); } /* 3. 使能FIFO0接收中断 */ if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) { printf("CAN1接收中断使能失败!\r\n"); // Error_Handler(); } }
关于 开启接收中断
上面函数的第3步:使能FIFO0接收中断,调用了 HAL_CAN_ActivateNotification ( );
这个函数用于开启 CAN 各类事件的中断触发功能,如 接收、发送完成、错误等各类事件的中断。我们这里传入的参数是开启FIFO0接收中断。当FIFO0收到报文时,触发 CAN 中断,进而执行编写的中断回调函数。
网上很多教程,会将这个调用步骤独立出来,放在 main 函数中。
个人经验,更建议将使能接收中断的调用直接放在筛选器配置函数中,确保配置完成后立即调用 ,避免遗漏。
步骤 2:实现中断回调函数
FIFO0 接收到报文后,触发中断,系统继而调用FIFO0的回调函数。
**测试时,**我们在函数中直接读取 FIFO 数据,并处理( printf 报文详情,观察调试)。
具体操作
- 复制下面函数到 bsp_CAN.c 文件;
- 无需在h文件中为这个回调函数添加函数声明,因为它在HAL库文件中已有声明;
cpp/****************************************************************************** * 函 数: HAL_CAN_RxFifo0MsgPendingCallback * 功 能: CAN接收中断的回调函数 (FIFO0) * 注意:只有在筛选器配置中接收规则使用的是FiFO0,接收到新一帧数据时才会触发此回调函数 * 如果配置时使用的是FIFO1, 触发的就是HAL_CAN_RxFifo1MsgPendingCallback(),编写它的回调函数即可 * 参 数: CAN_HandleTypeDef *hcan CAN句柄指针,可用于区分 CAN1 或 CAN2 实例。 * 返回值: 无 ******************************************************************************/ void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { uint8_t rxNum = 0; // 存储报文中数据的字节数 uint8_t rxData[9] = {0}; // 存储接收的数据; CAN一帧数据最大8字节,数组开辟9个字节,是为了适配以字符串输出调试信息,最后的1字节是0,即'\0',是字符串结束符; CAN_RxHeaderTypeDef rxHeader = {0}; // 帧结构信息 // 1. 从FIFO0读取报文 HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData); // 把FIFO缓存里的报文,转存到结构体备用; 参数:帧信息:RxHeader,数据:RxData rxNum = rxHeader.DLC; // 字节数 // 2. 解析并打印报文详情(调试用) printf("\r****** CAN 接收到新报文数据 ******"); // 准备把CAN帧报文,详细地输出到串口软件,方便观察调试 printf("\r 帧类型:%s", rxHeader.RTR ? "遥控帧" : "数据帧"); // 帧类型 printf("\r 帧格式:%s", rxHeader.IDE ? "扩展帧" : "标准帧"); // 帧格式 printf("\r 帧 ID :0x%X", rxHeader.IDE ? rxHeader.ExtId : rxHeader.StdId); // 帧ID printf("\r 字节数:%d", rxHeader.DLC); // 字节数 printf("\r 筛选器匹配序号:%d \r", rxHeader.FilterMatchIndex); // 筛选器匹配序号; 和筛选器编号是不同的!从筛选器0组地址起计,被配置成32位模式的筛选器组+1,被配置成16位工作模式的筛选器组+2,没有被配置的筛选器默认16位宽+2; // 3. 打印数据字节(十六进制) for (uint8_t i = 0; i < rxNum; i++) // 逐个字节显示CAN的数据值 printf(" 0x%02X ", rxData[i]); // 格式:16进制显示 printf("\r"); // 换行,以使每帧的输出更清晰 }
提示说明
函数区分:本回调仅处理FIFO0中断。若在筛选器配置中将报文分配至 FIFO1,则应实现对应的 HAL_CAN_RxFifo1MsgPendingCallback 函数。
实例识别:因为CAN1 、CAN2 会共用此回调函数。通过hcan参数可区分CAN1/CAN2实例,如,在代码中增加 if (hcan->Instance == CAN1),即可区分。
性能建议:回调函数中包含了 printf 输出,仅用作调试阶段的测试、观察。在实际项目中,务必移除或简化串口打印,避免在中断中执行耗时操作,转而采用:设置标志位、使用数据队列等高效方式,在中断外部再处理,如在while主循环中进行判断标志后处理。
步骤 3: 调用筛选器初始化 (main.c中)
具体操作:
- 在main函数的初始化之后、while(1)之前
- 调用 CAN1_FilterInit(),
- 调用 HAL_CAN_Start(&can1),启动CAN1
cpp// 初始化CAN1筛选器(接收所有报文) CAN1_FilterInit(); // 启动CAN1 if (HAL_CAN_Start(&hcan1) == HAL_OK) { printf("CAN1 启动成功! \r\n"); } else { printf("CAN1 启动失败! \r\n"); }
编写完成后,位置如下图:

步骤 4:接收功能验证
- 编译烧录程序,串口助手打开 UART1(波特率115200);
- CAN 分析仪设置 500Kbps;发送任意报文(标准 / 扩展帧均可,数据 1~8 字节);
- 串口助手即能打印出完整的报文详情(ID、字节数、数据),说明接收功能正常。

串口助手,运行效果如下图:

至此,CAN数据接收,完成!
九、CAN 接收进阶 -- 筛选器配置 + 机制优化
1、CAN 筛选器配置
新手前期可先用「接收所有报文」的配置验证 CAN 通信是否正常;
当项目中需要 精准过滤 ID,避免无关报文占用 CPU、干扰业务逻辑时,需核心掌握 CAN 外设的两种核心筛选模式,也是实际项目中最常用的过滤方式:
-
掩码模式 :宏定义
CAN_FILTERMODE_IDMASK,属于模糊匹配;通过设置「基准 ID + 掩码 ID」按位过滤,掩码位为 1 时必须与基准 ID 严格匹配,掩码位为 0 时忽略对应位;常用于接收某一连续范围的 ID 报文。 -
列表模式 :宏定义
CAN_FILTERMODE_IDLIST,属于精确匹配;设置需要接收的 ID 白名单,仅与列表中 ID 完全一致的报文能通过筛选;用于只接收指定单个 / 多个 ID 的报文。
STM32 的 CAN 外设共有 28 组筛选器,CAN1 可用 0~13 组,CAN2 可用 14~27 组。
灵活配置单组 / 多组过滤规则,即可精准筛选出业务所需的 CAN 报文。
以下提供可直接复用的筛选器配置示例,直接替换工程bsp_CAN.c中的CAN1_FilterInit函数即可使用。示例分别示范 列表模式( 扩展帧精准匹配) 和 掩码模式(标准帧段匹配)。
示例 1:列表模式,仅接收扩展帧、指定单个 ID 的报文(ID=0x123456)
cpp
void CAN1_FilterInit(void)
{
CAN_FilterTypeDef sFilterConfig = {0};
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST; // 列表模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
// 配置扩展帧ID:0x123456,精确匹配
sFilterConfig.FilterIdHigh = ((0x123456 << 3) & 0xFFFF0000) >> 16;
sFilterConfig.FilterIdLow = ((0x123456 << 3) | CAN_ID_EXT | CAN_RTR_DATA) & 0xFFFF;
sFilterConfig.FilterMaskIdHigh = 0x0000; // 列表模式掩码无效,设0
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.SlaveStartFilterBank = 14;
if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK) Error_Handler();
if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) Error_Handler();
}
示例 2:掩码模式,仅接收标准帧、ID在 0x120~0x12F 段的报文
cpp
void CAN1_FilterInit(void)
{
CAN_FilterTypeDef sFilterConfig = {0};
// 定义变量,基准ID值、掩码值
uint32_t _FilterId = 0x120; // 基准ID; 标准帧ID是11位; 值范围:0x000~0x7FF; 寄存器位[31:21]
uint32_t _FilterMask = 0x7F0; // 掩码值; 掩码0x7F0:111 1111 0000(11位),最后4位为0,表示忽略ID的最后4位, 因此可以接收ID在0x120~0x12F段的报文
// 配置筛选结构体
sFilterConfig.FilterBank = 0; // 筛选器组编号; CAN1可用0~13,CAN2可用14~27
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // 32位模式
sFilterConfig.FilterIdHigh = (_FilterId << 21) >> 16; // 基准ID; 高16位; 需用二进制理解;
sFilterConfig.FilterIdLow = (CAN_ID_STD | CAN_RTR_DATA) & 0xFFFF; // 基准ID; 低16位; 需用二进制理解;
sFilterConfig.FilterMaskIdHigh = (_FilterMask << 21) >> 16; // 掩码; 高16位; 需用二进制理解; 位1-总线报文的ID位必须与基准ID位一致、位0-此位忽略
sFilterConfig.FilterMaskIdLow = 0xFFFF; // 掩码; 低16位; 需用二进制理解; 位1-总线报文的ID位必须与基准ID位一致、位0-此位忽略
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; // 分配到 FIFO 0
sFilterConfig.FilterActivation = ENABLE; // 使能筛选器
sFilterConfig.SlaveStartFilterBank = 14;
// 初始化筛选器
if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
{
printf("CAN 筛选器初始化: 失败! \r");
Error_Handler(); // 错误处理
}
// 使能中断-FIFO0接收中断
if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
printf("CAN 使能中断: 失败! \r");
Error_Handler(); // 错误处理
}
}
筛选器核心难点理解(FilterId + FilterMaskId)
筛选器是 CAN 通信入门的核心难点,
其中 FilterId(基准 ID) 和 FilterMaskId(掩码 ID) 的配合使用最易混淆,难的并非技术实现,而是概念理解 + 寄存器位匹配的结合。
列表模式的 "白名单" 匹配逻辑简单易懂,
但掩码模式直接通过十六进制的基准 ID 和掩码,很难直观理解为何能匹配某一 ID 段。
核心理解思路:
将十六进制的基准 ID、掩码,换算成二进制,再结合【掩码 1 = 严格匹配、0 = 忽略】,即可清晰理解过滤逻辑;
再者,代码中的移位操作(>>16、<<21、<<3)仅为适配 STM32 CAN 筛选器的寄存器位存储规则,属于底层实现细节,理解核心匹配规则后,移位仅需按规则套用即可。
以示例 2 的 掩码模式(基准 ID=0x120,掩码 = 0x7F0) 为例,标准帧为固定 11 位 ID,换算后解析如下:

- 基准 ID FilterId = 0x120 ,11 位二进制:
001 0010 0000 - 掩码 ID FilterMaskId = 0x7F0 ,11 位二进制:
111 1111 0000
掩码 FilterMaskId 的位 = 1:严格匹配位,总线上报文的 ID 对应位必须与基准 ID 完全一致;
掩码 FilterMaskId 的位 = 0:忽略位,总线上报文的 ID 对应位可以是 0 或 1,无需匹配。
本示例中,掩码111 1111 0000表示高 7 位为严格匹配位,低 4 位为忽略位,因此基准 ID0x120+ 掩码0x7F0的组合,仅允许 ID 为0x120~0x12F的标准帧通过筛选器,完美实现指定 ID 段的模糊匹配。
2、CAN 接收机制优化
我们在第八章的测试示范时,直接在中断回调函数内处理数据(如通过printf打印到串口助手),
这种方式仅适用于前期功能测试。
真实项目中严禁在中断回调内执行耗时操作,如复杂数据解析、延时函数、串口打印、文件操作等。
中断的核心要求是快进快出 ,耗时操作会阻塞中断响应,导致后续报文丢失、其他中断延迟触发等问题。工业级项目标准做法 :中断回调函数中仅完成【数据转存 + 标志置位】的轻量操作,将复杂的数据处理逻辑放到main.c的while(1)主循环中轮询执行;若使用 RTOS,可在回调中将数据发送至消息队列,由任务线程处理。
优化思路:
在bsp_CAN.c中定义全局缓存变量存放接收数据,回调函数仅完成数据转存;提供数据获取 / 标志清零的接口函数,在bsp_CAN.h中声明;最终在main.c的主循环中轮询接口、处理数据,实现中断与业务逻辑的解耦。
具体操作
步骤 1:修改 bsp_CAN.c ,增加缓存变量 + 重写回调函数 + 实现接口函数。
cpp// 定义三个变量,用于转存最后一帧接收的内容 static uint8_t canRxNum = 0; // 最后一帧的接收字节数 static uint8_t canRxData[9] = {0}; // 最后一帧的接收的数据; CAN一帧数据最大8字节,数组开辟9个字节,是为了适配以字符串输出调试信息,最后的1字节是0,即'\0',是字符串结束符; static CAN_RxHeaderTypeDef canRxHeader = {0}; // 最后一帧的头部信息 /****************************************************************************** * 函 数: HAL_CAN_RxFifo0MsgPendingCallback * 功 能: CAN接收中断的回调函数 * 注意:只有在筛选器配置中接收规则使用的是FiFO0,接收到新一帧数据时才会触发此回调函数 * 如果配置时使用的是FIFO1, 触发的就是HAL_CAN_RxFifo1MsgPendingCallback(),编写它的回调函数即可 * 参 数: CAN_HandleTypeDef *hcan * 返回值: 无 ******************************************************************************/ void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { memset(canRxData, 0, 9); // 数组清0; 存放接收有效数据的数组; CAN一帧数据最大有效负载8字节,数组中开辟9个字节,是为了适配以字符串输出调试信息,最后的1字节0='\0',是字符串结束符; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &canRxHeader, canRxData); // 把收到的数据存放到结构体备用; 参数:帧信息:RxHeader,数据:RxData canRxNum = canRxHeader.DLC; // 接收字节数、标志位; 外部判断此值接收字节数是否大于0,以判断是否接收到新一帧数据 } /****************************************************************************** * 函 数: CAN1_GetRxNum * 功 能: 获取最后一帧接收的字节数 * 参 数: 无 * 返回值: 0=没有接收到数据,非0=新一帧数据的字节数 ******************************************************************************/ uint8_t CAN1_GetRxNum(void) { return canRxNum ; // 返回最后一帧的接收字节数; 在接收到数据时,硬件触发HAL_CAN_RxFifo0MsgPendingCallback()函数,此值在函数内被重新赋值; } /****************************************************************************** * 函 数: CAN1_GetRxData * 功 能: 获取最后一帧接收的数据(地址) * 参 数: 无 * 返回值: 数据地址 ******************************************************************************/ uint8_t *CAN1_GetRxData(void) { return canRxData ; // 返回最后一帧的接收数据(地址); 在接收到数据时,硬件触发HAL_CAN_RxFifo0MsgPendingCallback()函数,此数值在函数内被重新赋值; } /****************************************************************************** * 函 数: CAN1_GetRxHeader * 功 能: 获取最后一帧的头部信息 * 参 数: 无 * 返回值: CAN_RxHeaderTypeDef 头部信息 * 备 注: CAN_RxHeaderTypeDef 是HAL库的CAN接收头部信息结构体,成员: * uint32_t StdId 标准帧的ID, 11位, 值范围:0x00~0x7FF * uint32_t ExtId 扩展帧的ID, 29位, 值范围:0x00~0x1FFFFFFF * uint32_t DLC 接收到的字节数, 单位:byte, 值范围:0~8 * uint32_t FilterMatchIndex 筛选器编号, 值范围:0x00~0xFF * uint32_t IDE 帧格式; 0_标准帧、4_扩展帧 * uint32_t RTR 帧类型; 0_数据帧、2_遥控帧 * uint32_t Timestamp 使用时间触发模式时,时间戳,值范围:0x00~0xFFFF ******************************************************************************/ CAN_RxHeaderTypeDef CAN1_GetRxHeader(void) { return canRxHeader; // 返回最后一帧的头部信息; 在接收到数据时,硬件触发HAL_CAN_RxFifo0MsgPendingCallback()函数,此结构体在函数内被重新赋值; } /****************************************************************************** * 函 数: CAN1_ClearRx * 功 能: 清0最后一帧的接收 * 主要是清0字节数,因为它是用来判断接收的标准 * 参 数: 无 * 返回值: 无 ******************************************************************************/ void CAN1_ClearRx(void) { canRxNum = 0 ; // 清0最后一帧的接收字节数; 它在外部文件中用于判断是否接收到新一帧数据; 在接收到数据时,硬件触发HAL_CAN_RxFifo0MsgPendingCallback()函数,此值在函数内被重新赋值; }步骤 2:修改 bsp_CAN.h,添加接口函数声明
cpp// CAN1筛选器初始化函数声明 void CAN1_FilterInit(void); // CAN1接收数据接口函数声明 uint8_t CAN1_GetRxNum(void); // 获取最后一帧接收的字节数 uint8_t *CAN1_GetRxData(void); // 获取最后一帧接收的数据(地址) void CAN1_ClearRx(void); // 清0接收标志(清0的是接收字节数) CAN_RxHeaderTypeDef CAN1_GetRxHeader(void); // 获取最后一帧接收的头部信息步骤 3:修改 main.c, 在 while(1) 主循环中处理 CAN 数据
cppwhile(1) { /** 检查CAN1是否收到数据 **/ if (CAN1_GetRxNum()) // 检查接收字节数是否>0,以判断是否接收到新一帧数据 { static uint16_t canRxCNT = 1; // 用于计算已接收了多少帧数据; 非必要; // 1. 获取数据 uint8_t rxNum = CAN1_GetRxNum(); // 获取最后一帧的字节数 uint8_t *rxData = CAN1_GetRxData(); // 获取最后一帧的数据(地址) CAN_RxHeaderTypeDef rxHeader = CAN1_GetRxHeader(); // 获取最后一帧的头部信息 // 2. 处理数据:把收到的数据,printf到串口助手观察 printf("\r****** CAN 接收到第%d帧新数据 ******", canRxCNT++); // 准备把CAN帧报文,详细地输出到串口软件,方便观察调试 printf("\r 帧类型:%s", rxHeader.RTR ? "遥控帧" : "数据帧"); // 帧类型 printf("\r 帧格式:%s", rxHeader.IDE ? "扩展帧" : "标准帧"); // 帧格式 printf("\r 标识符:0x%X", rxHeader.IDE ? rxHeader.ExtId : rxHeader.StdId); // 帧ID printf("\r 字节数:%d", rxHeader.DLC); // 字节数 printf("\r 筛选器匹配序号:%d", rxHeader.FilterMatchIndex); // 筛选器匹配序号; 和筛选器编号,是不一样的。大概:从筛选器0开始,每个16位宽筛选器+2, 32位宽+1, 没有被使用的筛选器,默认是16位宽,+2; printf("\r 显示数据(16进制):"); // 16进制方式显示数据,方便观察真实数据 for (uint8_t i = 0; i < rxNum; i++) // 逐个字节显示CAN的数据值 printf(" 0x%X ", rxData[i]); // 格式:16进制显示 printf("\r"); // 换行,以使每帧的输出更清晰 // 3. 清零接收标志 CAN1_ClearRx(); // 字节数清0; 每次处理完一帧数据,都需要清0接收标志(其实清0的是接收字节数),以方便下一轮的判断,避免重复处理同一帧数据; } }
核心优化点与关键注意事项
- 中断快进快出:回调函数仅保留数据转存和标志置位,移除所有耗时操作,添加
CAN1实例判断,避免多 CAN 外设干扰; - 数据安全设计:接收缓存变量添加
static修饰,仅本文件可见,通过接口函数对外提供访问,避免外部直接修改缓存数据; - 避免重复处理:处理完数据后必须调用
CAN1_ClearRx()清零接收标志,这是核心要点,否则主循环会一直检测到标志位为 1,重复处理同一帧数据;
扩展说明(RTOS 项目适配)
若项目使用 FreeRTOS 等实时操作系统,可将此方案进一步优化为队列通信,更符合 RTOS 的编程规范:
在 bsp_CAN.c中创建一个 CAN 数据队列(如osMessageQueueId_t can1Queue);- 中断回调函数中,将接收的
canRxHeader和canRxData封装成结构体,发送至队列; - 在
main.c中创建一个 CAN 数据处理任务,通过osMessageQueueGet从队列中读取数据并处理; - 优势:无需手动管理接收标志,由 RTOS 队列实现数据的异步传递,避免轮询占用 CPU,支持多帧数据缓存,防止报文丢失。
十、CAN 调试 -- 常见错误排查
CAN 调试对新手不友好。
多数问题并非代码错误,而是硬件接线、供电、配置问题,按以下顺序排查,99% 的问题能解决。
错误 1:供电问题 -- CAN 收发器未接 5V(最常见)
- 现象:程序运行正常,CAN 启动成功,却无法收发数据,串口无报错;
- 原因 :TJA1050 等收发器需5V 供电,若用 ST-Link/J-Link 仅给 STM32 供 3.3V,收发器不工作,无法转换电平;
- 解决:将收发器 VCC 接稳定 5V(核心板 / 开发板的 5V 引脚),GND 接 STM32 的 GND。
错误 2:引脚 / 接线 问题-- 引脚配置错误或 H/L 反接
- 现象:CAN 启动失败,或仅能发送不能接收,分析仪无报文;
- 原因 1:CubeMX 配置的 CAN 引脚(PA11+PA12 或 PB8+PB9)与硬件接线不一致,或引脚被其他外设(如 USB、串口)占用;
- 解决 1:查阅芯片手册、原理图、实物接线,确认 CAN 引脚无冲突,重新配置 CubeMX 并生成代码;
- 原因 2:CAN_H 与 CAN_L 反接,或 TX/RX 接反(STM32_TX 接收发器 TX,STM32_RX 接收发器 RX);
- 解决 2:按 "H 接 H、L 接 L、TX 接 TX、RX 接 RX" 重新接线,无交叉。
错误 3:程序运行后,无发送、无接收。.
- **原因1:**可能是系统时钟配置不对、CAN配置不对等。
- **解决1:**① 在程序上电后 printf 几行信息,如"Hello 你好"等,以便判断系统时钟配置是否正常。
- **原因2:**程序卡死
- 解决2: ① 在while中增加500ms规律闪灯,以便判断程序是否正常运行、卡死。 ② 如果是卡死了,在不同位置插入 printf, 看看哪个 printf 没有输出即可定位错误位置。
错误 4:终端电阻缺失
- **现象:**短距离(<1 米)、低速率(250Kbps)通信正常,距离稍长 / 速率提高后通信不稳定,误码率高;
- 原因: 总线末端缺少终端电阻,信号反射导致波形畸变;或多接电阻,总线等效电阻过小(<60Ω),信号幅度被拉低;
- 解决: 仅在总线最远的两个节点接 120Ω,断电后测 H-L 阻值≈60Ω,中间节点禁用电阻。
错误 5:仅能发送不能接收
- 现象:CAN 能正常发送(分析仪能捕获),但无法接收,回调函数不触发;
- **排查1:**线路问题。检查RX线路是否松支,是否接错,更换线材。
- **排查2:**中断没触发。在回调函数中 插入 printf,看看是否能触发中断调用。
- **排查3:**筛选器配置。必须配置筛选器,否则无法接收任何报文。筛选器配置错误。先配置为接收所有报文 ,验证接收后再精准过滤。
错误 6:无法进入中断或回调函数
- 现象:CAN 启动成功、筛选器配置正确,却无法接收,分析仪有发送报文;
- 原因 :① 未在 CubeMX 中启用 CAN1 RX0 中断;② 未实现对应的
HAL_CAN_RxFifo0MsgPendingCallback回调函数; - 解决:重新在 CubeMX 中启用 CAN1 RX0 中断并生成代码,确保回调函数正确实现(无语法错误,句柄判断正确)。
附:源文件链接
示例源文件下载链接:[ STM32F407 + CAN + HAL库 ]
若文中存在疏漏、章节衔接不畅,诚盼留言指正,将虚心聆听、斟酌修改,力求内容清晰通达。
感谢!~