【STM32 + CubeMX】 CAN 通信



本篇,通过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 通信为例 :

  1. CAN 控制器:STM32芯片,芯片内部集成了 CAN控制器 功能;
  2. CAN 收发器:准确名称是 CAN 电平转换(模块);即下图中的 TJA1050。
  3. 总线: 狭义地理解(别杠字眼),就是两根导线;即下图中的 H、L 这两根导线。
  4. 终端电阻: 相距最远的两个节点上,各放置一个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进制显示模式),确认能正常接收该字符串。

该测试目的:

  1. 验证系统时钟配置正确
  2. 确认程序正常运行
  3. 检查printf重定向是否成功

步骤 4:新建用户 CAN 文件

在工程文件夹中,新建两个文件:bsp_CAN.c 和 **bsp_CAN.h。**文件用途:

  • 集中管理将要编写的3个核心函数 (发送函数、筛选器配置函数、回调函数)。
  • 便于跨型号移植,支持F103、F407、H750等多种芯片型号,减少重复工作。

具体操作:

  1. 在工程目录下,新建 Bsp 文件夹 (可自定义名称)
  2. 在 Bsp 文件夹内,新建 CAN 子文件夹
  3. 在 CAN 文件夹内新建 2个 文本文档,重命名为 bsp_CAN.cbsp_CAN.h(需显示文件扩展名,设置:文件夹菜单→文件→选项→查看→取消勾选 "隐藏已知文件类型的扩展名")

操作完成后,文件结构是这样子的:

提示说明:

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

步骤 5:Keil 中添加源文件

具体操作,如下图。

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

步骤 6:配置 Keil 头文件路径

作用:确保编译器能找到 bsp_CAN.h 。

具体操作,如下图。

步骤 7:添加头文件保护

新建的每个头文件,都需要添加头文件保护,以避免多文件包含时重复定义导致错误。

具体操作

  1. 在 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

具体操作:

  1. 在 Connectivity 分组中选择 CAN1
  2. 勾选 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,可以减少冲突。

具体操作:

  1. 单击 PB9 引脚,将其功能配置为 CAN1_TX
  2. 单击 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 接收中断

具体操作

  1. 进入 CAN1 NVIC Settings 标签页
  2. 勾选 CAN1 RX0 interrupts,启用接收 FIFO0 的中断功能
  3. 其它参数 默认

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:代码生成

完成上述配置后,即可生成配置代码。

具体操作

  1. 点击 CubeMX 右上角 Generate Code 按钮,CubeMX 将生成 CAN1 配置。

至此,CAN1模块的软件初始化配置工作已全部完成。

接下来可进入应用代码编写和功能实现阶段。



六、CAN 初始化 -- 启动外设

步骤 1:启动 CAN 外设

通过调用函数:HAL_CAN_Start(&hcan1),即可启动 CAN1 。

函数返回值为 HAL 状态(HAL_OK = 成功,其余 = 失败)。

对比UART,UART启动是在CubeMX生成的代码中被调用,因为会100%成功,无异常可言。

而CAN启动,需要用户手动调用,通过判断函数的返回状态, 才能知道CAN 启动是否成功。

(很多新手不习惯使用printf!调试出现故障时,完全靠盲猜,无法定位哪个节点的问题!)

具体操作

  1. main.c中,MX_CAN1_Init () 之后,while (1) 之前,编写以下代码,以启动CAN
  2. 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:调用示例

具体操作

  1. 在main.c 中,while(1)之前 / 之内 均可
  2. 编写这段发送测试代码
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.cwhile(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 数据

cpp 复制代码
while(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);
  • 中断回调函数中,将接收的canRxHeadercanRxData封装成结构体,发送至队列;
  • 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库 ]

若文中存在疏漏、章节衔接不畅,诚盼留言指正,将虚心聆听、斟酌修改,力求内容清晰通达。

感谢!~

相关推荐
__万波__2 小时前
STM32L475定时器实验
stm32·单片机·嵌入式硬件
80530单词突击赢2 小时前
C++服务程序自启动实战指南
stm32·单片机·嵌入式硬件
shansz202012 小时前
暂时无法解决的关于STM32F103的RTC日期更新问题
stm32·嵌入式硬件·实时音视频
独处东汉19 小时前
freertos开发空气检测仪之按键输入事件管理系统设计与实现
人工智能·stm32·单片机·嵌入式硬件·unity
小灰灰搞电子19 小时前
STM32/GD32 字节对齐详解
stm32·单片机·嵌入式硬件
xixixi7777719 小时前
基于零信任架构的通信
大数据·人工智能·架构·零信任·通信·个人隐私
良许Linux1 天前
DSP的选型和应用
后端·stm32·单片机·程序员·嵌入式
混分巨兽龙某某1 天前
基于STM32的嵌入式操作系统RT-Thread移植教学(HAL库版本)
stm32·嵌入式硬件·rt-thread·rtos