详解针对STM32F1 系列旧版 CAN HAL 库(stm32f1xx_hal_can.c,宏HAL_CAN_LEGACY_MODULE_ENABLED开启),全程贴合初学者视角,从硬件连接、CubeMX 可视化配置、代码编写、实际测试一步步落地,覆盖 ** 轮询模式收发(入门必学)、中断模式收发(项目常用)** 两个核心典型案例,代码可直接复制使用,同时结合你提供的 HAL 库源码解释关键函数的底层逻辑,解决初学者 “配了不会用、写了收不到” 的核心问题。
前置基础必看
1. 库版本说明
你提供的stm32f1xx_hal_can.c是STM32F1 专属的旧版 CAN HAL 库(Legacy),STM32 官方标注该库有 FIFO 管理限制,但胜在 API 简单、初学者易上手,适合入门;新库修复了限制,但 API 更复杂,入门先掌握旧版再过渡即可。
- 适用芯片:STM32F103x6/xB/xE/xG、F105xC、F107xC(如最常用的 F103ZET6/F103C8T6);
- 核心宏开启:需在stm32f1xx_hal_conf.h中开启#define HAL_CAN_LEGACY_MODULE_ENABLED,并关闭新版HAL_CAN_MODULE_ENABLED(否则会报编译错误,源码中已有该报错提示)。
2. CAN 硬件连接(缺一不可,初学者最易踩坑)
STM32 的 CAN 外设本身是CAN 控制器,无法直接接 CAN 总线,必须搭配CAN 收发器(最常用TJA1050,成本低、兼容性好),硬件连接是 CAN 通信的基础,接错则软件再对也无法通信。
核心器件:TJA1050 CAN 收发器模块
- 供电:多数 TJA1050 模块为5V 供电(也有 3.3V 版,按模块标注接),需和 STM32 共 GND;
- 关键引脚连接(TX/RX 交叉接,核心要点):
TJA1050 引脚STM32F1 CAN 引脚功能说明 TXD CAN_RX 收发器 TX 接控制器 RX,交叉连接! RXD CAN_TX 收发器 RX 接控制器 TX,交叉连接! VCC 5V/3.3V 模块供电 GND GND 共地,必须接! CAN_H CAN 总线 H 端 接外部 CAN 总线 CAN_L CAN 总线 L 端 接外部 CAN 总线 - 120Ω 终端电阻:CAN 总线两端需各接一个 120Ω 电阻(TJA1050 模块多数自带跳线,短接即可开启),作用是消除总线信号反射,短距离测试(如板级测试)可只开一个,长距离总线必须两端都接。
STM32F1 CAN 引脚(硬件固定,以 F103C8T6/F103ZET6 为例)
| STM32F103C8T6 | PA12 | PA11 | 无 CAN2,仅 CAN1 |
| STM32F103ZET6 | PA12/PD1 | PA11/PD0 | 可重映射,默认 PA11/PA12 |
初学者直接用默认引脚,无需重映射,减少配置复杂度。
3. CAN 关键基础概念(入门只需记这 3 个)
(1)CAN 波特率计算(核心,必须匹配)
通信双方的波特率必须完全一致,否则无法收发数据,旧版 HAL 库中波特率由 CubeMX 直接配置,初学者无需手动计算,只需记典型值配置,底层公式用于理解:CAN波特率=Prescaler×(1+BS1+BS2)APB1时钟频率
- 关键前提:STM32F1 的 CAN 挂载在APB1 总线,APB1 最大时钟频率为36MHz(CubeMX 中配置系统时钟时,APB1 预分频需设为 2,若系统时钟 72MHz,APB1 时钟即为 36MHz);
- 典型配置(500Kbps,项目最常用):Prescaler=18,BS1=1,BS2=2 → 36M/(18×(1+1+2))=500Kbps;
- 其他常用值(1Mbps):Prescaler=9,BS1=1,BS2=2 → 36M/(9×4)=1Mbps。
(2)标准 ID / 扩展 ID
- 标准 ID(CAN_ID_STD):11 位,范围0~0x7FF,入门首选,足够满足绝大多数场景;
- 扩展 ID(CAN_ID_EXT):29 位,范围0~0x1FFFFFFF,用于多节点复杂总线,入门暂不涉及。
(3)CAN 过滤器(必须配置,否则收不到数据)
STM32 的 CAN 有过滤器组,作用是筛选需要接收的 CAN 帧,过滤掉无关帧,减少 MCU 开销;未配置过滤器,CAN 无法接收任何数据(初学者核心坑)。
- 入门配置:过滤所有标准 ID(接收所有帧),方便测试,无需筛选;
- 核心参数:过滤器模式(掩码模式CAN_FILTERMODE_IDMASK)、过滤器尺度(32 位CAN_FILTERSCALE_32BIT)、FIFO 分配(FIFO0)。
4. 工具准备
- 硬件:STM32F1 开发板、TJA1050 模块、CAN 分析仪(USB 转 CAN,如 CANalyst-II,必备,用于测试收发);
- 软件:STM32CubeMX(6.0+)、MDK-ARM Keil5(支持 F1 系列)、CAN 分析仪配套软件(如 CANTest)。
通用前置步骤:STM32CubeMX 配置(两个案例共用)
所有 CAN 应用的基础是 CubeMX 配置,一键生成初始化代码,避开手动写寄存器,初学者按步骤操作即可,以下以STM32F103C8T6为例,系统时钟 72MHz,CAN1 波特率 500Kbps,标准 ID,接收所有帧。
步骤 1:新建项目,选择芯片
步骤 2:配置系统时钟树(核心,F1 必须 72MHz)
- PLLMUX:HSE;
- PLLMUL:x9(8M×9=72M);
- AHB Prescaler:/1(72M);
- APB1 Prescaler:/2(36M,满足 CAN 的 APB1 最大 36M 要求);
- APB2 Prescaler:/1(72M);
步骤 3:开启 CAN1 并配置核心参数
- Mode:Normal(正常模式);
- Prescaler:18(波特率分频系数);
- SJW:1tq(同步跳转宽度,默认);
- BS1:1tq(时间段 1);
- BS2:2tq(时间段 2);
- 其余 MCR 参数(TTCM/ABOM/AWUM/NART/RFLM/TXFP):全部选DISABLE(入门默认,ABOM 开启可自动总线恢复,可选 ENABLE);
步骤 4:配置 CAN 过滤器(关键,必须配)
- Filter Mode:Mask Mode(掩码模式);
- Filter Scale:32-bit(32 位尺度);
- Filter FIFO Assignment:FIFO 0(分配到 FIFO0);
- Filter Bank:0(过滤器组 0,F103C8T6 有 14 个过滤器组,选 0 即可);
- Filter Activation:Enabled(使能过滤器);
- Filter ID Low/High:0x0000/0x0000(过滤 ID,全 0 表示接收所有);
- Filter Mask ID Low/High:0x0000/0x0000(掩码,全 0 表示不筛选,接收所有);
步骤 5:(中断案例需加)开启 CAN1 中断并配置 NVIC
若做中断模式收发,需开启 CAN1 的 FIFO0 中断,并配置中断优先级:
- Preemption Priority:1;
- Sub Priority:0;
步骤 6:生成工程代码
- Project Name:CAN_Demo(自定义);
- Project Location:选择保存路径;
- Toolchain/IDE:MDK-ARM V5;
案例 1:轮询模式 CAN 收发(入门必学,最简单)
案例特点
- 轮询模式(查询模式):通过HAL_CAN_Transmit()主动发送,HAL_CAN_Receive()主动查询是否有数据接收,无需中断,代码最简单,易理解;
- 适用场景:简单的单节点通信、低频率数据收发(如每秒发送一次数据);
- 核心函数:从你提供的 HAL 库源码中调用HAL_CAN_Init()(初始化)、HAL_CAN_ConfigFilter()(过滤器配置,CubeMX 已生成)、HAL_CAN_Transmit()(轮询发送)、HAL_CAN_Receive()(轮询接收)。
代码编写(Keil5 中修改,仅需改main.c)
步骤 1:定义 CAN 收发结构体(全局 / 局部均可,全局更方便)
在main.c的用户代码区 1(/* USER CODE BEGIN 0 */)定义 CAN 发送和接收结构体,这两个结构体是旧版 CAN 库的核心,源码中所有收发函数都基于它们:
/* USER CODE BEGIN 0 */
#include "stdio.h" // 若用串口打印,需加
// CAN发送结构体(CanTxMsgTypeDef是旧版库专属)
CanTxMsgTypeDef TxMsg;
// CAN接收结构体(CanRxMsgTypeDef是旧版库专属)
CanRxMsgTypeDef RxMsg;
// 串口重定向(可选,用于打印接收数据,方便调试)
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}
/* USER CODE END 0 */
若用串口打印,需在 CubeMX 中开启 USART1,配置波特率 115200,引脚 PA9/PA10,CubeMX 会自动生成huart1句柄。
步骤 2:初始化 CAN 发送结构体(main 函数中,MX_CAN1_Init 之后)
在main()函数中,MX_CAN1_Init();之后的用户代码区 2初始化发送参数,标准 ID,8 字节数据,数据帧:
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
// 1. 系统初始化(CubeMX自动生成)
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_CAN1_Init(); // CAN1初始化(含过滤器配置)
/* USER CODE BEGIN 2 */
// 初始化CAN发送结构体
TxMsg.StdId = 0x123; // 标准ID,自定义(0~0x7FF),通信双方需一致
TxMsg.IDE = CAN_ID_STD; // 标准ID模式
TxMsg.RTR = CAN_RTR_DATA;// 数据帧(非远程帧)
TxMsg.DLC = 8; // 数据长度,0~8,这里发8字节
// 初始化发送数据(自定义,如0x01,0x02,…0x08)
TxMsg.Data[0] = 0x01;
TxMsg.Data[1] = 0x02;
TxMsg.Data[2] = 0x03;
TxMsg.Data[3] = 0x04;
TxMsg.Data[4] = 0x05;
TxMsg.Data[5] = 0x06;
TxMsg.Data[6] = 0x07;
TxMsg.Data[7] = 0x08;
// 关联CAN句柄的发送结构体(旧版库核心:必须将TxMsg赋值给hcan1.pTxMsg)
hcan1.pTxMsg = &TxMsg;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// ************************** CAN轮询发送 **************************
// HAL_CAN_Transmit(Can句柄, 超时时间):超时时间HAL_MAX_DELAY表示一直等待
if(HAL_CAN_Transmit(&hcan1, HAL_MAX_DELAY) == HAL_OK)
{
printf("CAN发送成功!数据:0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X\\r\\n",
TxMsg.Data[0],TxMsg.Data[1],TxMsg.Data[2],TxMsg.Data[3],
TxMsg.Data[4],TxMsg.Data[5],TxMsg.Data[6],TxMsg.Data[7]);
// 发送后修改数据(可选,方便观察)
for(uint8_t i=0; i<8; i++)
{
TxMsg.Data[i]++;
if(TxMsg.Data[i] > 0xFF) TxMsg.Data[i] = 0x00;
}
}
else
{
printf("CAN发送失败!\\r\\n");
}
// ************************** CAN轮询接收 **************************
// HAL_CAN_Receive(Can句柄, FIFO编号, 超时时间):FIFO0(过滤器分配的),超时10ms
if(HAL_CAN_Receive(&hcan1, CAN_FIFO0, 10) == HAL_OK)
{
// 接收成功,打印数据(RxMsg会被HAL库自动填充)
printf("CAN接收成功!ID:0x%03X,数据长度:%d,数据:", RxMsg.StdId, RxMsg.DLC);
for(uint8_t i=0; i<RxMsg.DLC; i++)
{
printf("0x%02X ", RxMsg.Data[i]);
}
printf("\\r\\n");
}
// 延时1秒,降低收发频率
HAL_Delay(1000);
}
/* USER CODE END 3 */
}
代码核心解释(结合 HAL 库源码)
- StdId:标准 ID(11 位);
- IDE:ID 类型(CAN_ID_STD/CAN_ID_EXT);
- RTR:帧类型(CAN_RTR_DATA数据帧 /CAN_RTR_REMOTE远程帧);
- DLC:数据长度(0~8);
- Data[8]:收发的 8 字节数据;
硬件测试步骤
- STM32 每秒发送一帧数据,CAN 分析仪可收到,串口助手打印 “发送成功”;
- 在 CAN 分析仪中手动发送标准 ID0x123的 8 字节数据,STM32 可接收,串口助手打印 “接收成功”。
回环模式测试(无需硬件,快速验证代码)
若没有 CAN 分析仪,可将 CubeMX 中 CAN 的模式改为Loopback(回环模式),STM32 会自收自发,无需 TJA1050 和 CAN 分析仪,下载代码后串口助手会同时打印 “发送成功” 和 “接收成功”,快速验证代码正确性。
案例 2:中断模式 CAN 收发(项目常用,非阻塞)
案例特点
- 中断模式:发送完成 / 接收数据时触发中断,通过回调函数处理收发逻辑,主循环无阻塞,CPU 可处理其他任务,是实际项目的主流用法;
- 核心函数:HAL_CAN_Receive_IT()(开启接收中断)、HAL_CAN_Transmit_IT()(中断发送),以及回调函数HAL_CAN_RxCpltCallback()(接收完成回调)、HAL_CAN_TxCpltCallback()(发送完成回调);
- 关键:旧版 CAN 库的中断接收需在回调函数中重新开启中断,否则只能接收一帧数据(初学者核心坑)。
代码编写(基于案例 1 的 CubeMX 配置,仅改main.c,开启了 CAN1 RX0 中断)
步骤 1:定义全局变量和收发结构体(中断中需访问,建议全局)
/* USER CODE BEGIN 0 */
#include "stdio.h"
// CAN收发结构体(全局,中断回调中访问)
CanTxMsgTypeDef TxMsg;
CanRxMsgTypeDef RxMsg;
// 串口重定向
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}
// 自定义发送标志(可选,按键触发发送,模拟实际项目)
uint8_t CanSendFlag = 0;
/* USER CODE END 0 */
步骤 2:main 函数初始化 + 开启接收中断 + 主循环处理
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
// 系统初始化(CubeMX自动生成)
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_CAN1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
// 初始化CAN发送结构体
TxMsg.StdId = 0x123;
TxMsg.IDE = CAN_ID_STD;
TxMsg.RTR = CAN_RTR_DATA;
TxMsg.DLC = 8;
TxMsg.Data[0] = 0x01;
TxMsg.Data[1] = 0x02;
TxMsg.Data[2] = 0x03;
TxMsg.Data[3] = 0x04;
TxMsg.Data[4] = 0x05;
TxMsg.Data[5] = 0x06;
TxMsg.Data[6] = 0x07;
TxMsg.Data[7] = 0x08;
// 关联发送结构体
hcan1.pTxMsg = &TxMsg;
// 关联接收结构体(旧版库:hcan1.pRxMsg指向FIFO0的接收结构体)
hcan1.pRxMsg = &RxMsg;
// 开启CAN1 FIFO0接收中断(核心,中断接收的入口)
if(HAL_CAN_Receive_IT(&hcan1, CAN_FIFO0) != HAL_OK)
{
printf("开启CAN接收中断失败!\\r\\n");
}
else
{
printf("开启CAN接收中断成功!等待接收数据…\\r\\n");
}
// (可选)配置按键GPIO(如PA0),下降沿触发,用于按键触发发送
// 此处省略GPIO配置,可在CubeMX中开启PA0为EXTI0中断,按键按下置位CanSendFlag
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// 按键触发CAN中断发送(模拟实际项目的外部触发)
if(CanSendFlag == 1)
{
CanSendFlag = 0; // 清标志
// 中断发送:HAL_CAN_Transmit_IT()
if(HAL_CAN_Transmit_IT(&hcan1) == HAL_OK)
{
printf("CAN中断发送请求已提交!\\r\\n");
// 预修改下一次发送的数据
for(uint8_t i=0; i<8; i++)
{
TxMsg.Data[i]++;
if(TxMsg.Data[i] > 0xFF) TxMsg.Data[i] = 0x00;
}
}
else
{
printf("CAN中断发送请求失败!\\r\\n");
}
}
// 主循环处理其他任务(如LED闪烁,证明无阻塞)
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);
}
/* USER CODE END 3 */
}
步骤 3:重写 CAN 中断回调函数(核心,用户自定义逻辑)
在main.c的用户代码区 4重写 HAL 库的弱回调函数(__weak修饰,源码中为空,用户可重写),中断触发后会自动调用这些函数:
/* USER CODE BEGIN 4 */
// ************************** CAN接收完成回调函数 **************************
// 接收一帧数据后触发,由HAL_CAN_IRQHandler()调用(源码中可见)
void HAL_CAN_RxCpltCallback(CAN_HandleTypeDef* hcan)
{
if(hcan->Instance == CAN1)
{
// 接收成功,打印数据(RxMsg已被HAL库填充)
printf("CAN中断接收成功!ID:0x%03X,长度:%d,数据:", RxMsg.StdId, RxMsg.DLC);
for(uint8_t i=0; i<RxMsg.DLC; i++)
{
printf("0x%02X ", RxMsg.Data[i]);
}
printf("\\r\\n");
// 核心:重新开启CAN接收中断!!!
// 旧版CAN库的中断接收为「单次中断」,接收一帧后需重新开启,否则无法接收下一帧
if(HAL_CAN_Receive_IT(&hcan1, CAN_FIFO0) != HAL_OK)
{
printf("重新开启CAN接收中断失败!\\r\\n");
}
}
}
// ************************** CAN发送完成回调函数 **************************
// 发送一帧数据后触发,由HAL_CAN_IRQHandler()调用(源码中可见)
void HAL_CAN_TxCpltCallback(CAN_HandleTypeDef* hcan)
{
if(hcan->Instance == CAN1)
{
printf("CAN中断发送完成!\\r\\n");
}
}
// ************************** CAN错误回调函数 **************************
// CAN通信出错时触发(如总线掉、波特率不匹配)
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef* hcan)
{
if(hcan->Instance == CAN1)
{
printf("CAN通信错误!错误码:0x%08X\\r\\n", HAL_CAN_GetError(hcan));
// 重新开启接收中断
HAL_CAN_Receive_IT(&hcan1, CAN_FIFO0);
}
}
// (可选)按键外部中断回调函数(PA0),触发发送
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0)
{
CanSendFlag = 1; // 置位发送标志,主循环处理
}
}
/* USER CODE END 4 */
代码核心解释(结合 HAL 库源码)
测试步骤
案例 3:CAN 回环模式测试(无硬件,快速验证代码)
若没有 TJA1050 和 CAN 分析仪,可使用CAN 回环模式(Loopback),STM32 内部自收自发,无需任何外部硬件,快速验证 CAN 配置和代码的正确性:
初学者常见问题与避坑技巧(按出现频率排序)
问题 1:CAN 无法发送 / 接收,无任何反应
原因 & 解决方案:
问题 2:CAN 只能接收一帧数据,后续无反应
原因 & 解决方案:
- 中断模式下未重新开启接收中断:旧版 CAN 库的HAL_CAN_Receive_IT()是单次中断,接收一帧后必须在HAL_CAN_RxCpltCallback()中重新调用该函数,才能接收下一帧(案例 2 已处理)。
问题 3:CAN 发送成功,但接收不到数据
原因 & 解决方案:
问题 4:Keil 编译报错,提示CanTxMsgTypeDef未定义
原因 & 解决方案:
- 未开启旧版 CAN 库宏:在stm32f1xx_hal_conf.h中开启#define HAL_CAN_LEGACY_MODULE_ENABLED,并关闭#define HAL_CAN_MODULE_ENABLED(否则冲突)。
问题 5:中断收发时,主循环阻塞
原因 & 解决方案:
- 回调函数中执行了耗时操作:中断回调函数应简洁高效,仅做数据接收 / 标志置位,耗时操作(如复杂计算、延时)放到主循环中处理。
关键函数总结(基于你提供的 HAL 库源码)
旧版stm32f1xx_hal_can.c的核心函数,初学者只需掌握以下几个,即可应对绝大多数场景:
| HAL_CAN_Init() | CAN 外设初始化(CubeMX 自动生成) | 所有 |
| HAL_CAN_ConfigFilter() | CAN 过滤器配置(CubeMX 自动生成) | 所有 |
| HAL_CAN_Transmit() | 轮询发送,阻塞直到发送完成 / 超时 | 轮询 |
| HAL_CAN_Receive() | 轮询接收,阻塞直到接收完成 / 超时 | 轮询 |
| HAL_CAN_Transmit_IT() | 中断发送,提交请求后立即返回,非阻塞 | 中断 |
| HAL_CAN_Receive_IT() | 开启中断接收,有数据时触发回调 | 中断 |
| HAL_CAN_IRQHandler() | CAN 中断总处理函数(CubeMX 自动生成,无需修改) | 中断 |
| HAL_CAN_RxCpltCallback() | 接收完成回调(用户重写) | 中断 |
| HAL_CAN_TxCpltCallback() | 发送完成回调(用户重写) | 中断 |
| HAL_CAN_ErrorCallback() | 错误回调(用户重写) | 所有 |
总结
STM32F1xx_hal_can(旧版)的入门核心是先掌握硬件连接 + CubeMX 配置,再从轮询模式入手,最后过渡到中断模式,初学者只需牢记以下 3 个核心要点:
- 轮询模式:定义CanTxMsgTypeDef/CanRxMsgTypeDef,赋值hcan->pTxMsg,调用HAL_CAN_Transmit()/HAL_CAN_Receive();
- 中断模式:开启 CAN_RX0 中断,重写回调函数,接收完成后必须重新开启中断,实现连续接收。
掌握这三个案例后,可轻松过渡到实际项目,如多节点 CAN 通信、扩展 ID 使用、过滤器精准筛选 ID 等,后续只需在现有代码基础上稍作修改即可。





