欢迎光临
我们一直在努力

CAN通信:STM32F1xx_hal_can入门实战详解

详解针对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 为例)
芯片型号CAN1_TXCAN1_RX备注
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:新建项目,选择芯片

  • 打开 CubeMX,点击New Project,搜索STM32F103C8T6,选择对应芯片,点击Start Project;
  • 弹出Pinout & Configuration页面,默认即可,点击OK。
  • 步骤 2:配置系统时钟树(核心,F1 必须 72MHz)

  • 点击左侧Clock Configuration,进入时钟配置页面;
  • 外部高速晶振(HSE)选择Crystal/Ceramic Resonator(8MHz 晶振);
  • 配置分频系数(按图配置,最终系统时钟 72MHz,APB1=36MHz):
    • PLLMUX:HSE;
    • PLLMUL:x9(8M×9=72M);
    • AHB Prescaler:/1(72M);
    • APB1 Prescaler:/2(36M,满足 CAN 的 APB1 最大 36M 要求);
    • APB2 Prescaler:/1(72M);
  • 点击Apply,完成时钟配置。
  • 步骤 3:开启 CAN1 并配置核心参数

  • 回到Pinout & Configuration,左侧展开Connectivity,点击CAN1;
  • 模式选择Normal(正常模式,测试用;回环模式Loopback无需硬件,仅 MCU 自收自发,后续可测试);
  • 点击Parameter Settings,配置 CAN 核心参数(直接按以下值填,500Kbps 波特率):
    • Mode:Normal(正常模式);
    • Prescaler:18(波特率分频系数);
    • SJW:1tq(同步跳转宽度,默认);
    • BS1:1tq(时间段 1);
    • BS2:2tq(时间段 2);
    • 其余 MCR 参数(TTCM/ABOM/AWUM/NART/RFLM/TXFP):全部选DISABLE(入门默认,ABOM 开启可自动总线恢复,可选 ENABLE);
  • 配置完成后,CubeMX 会自动分配 CAN1 引脚(PA11=CAN_RX,PA12=CAN_TX),无需手动配置。
  • 步骤 4:配置 CAN 过滤器(关键,必须配)

  • 仍在 CAN1 的Parameter Settings,下拉找到Filter Configuration,配置过滤器(接收所有标准 ID,入门首选):
    • 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 表示不筛选,接收所有);
  • 点击OK,过滤器配置完成。
  • 步骤 5:(中断案例需加)开启 CAN1 中断并配置 NVIC

    若做中断模式收发,需开启 CAN1 的 FIFO0 中断,并配置中断优先级:

  • 左侧展开System Core,点击NVIC;
  • 找到CAN1 RX0 interrupts,勾选Enabled(开启 FIFO0 接收中断);
  • 配置中断优先级(入门无需严格配置,按默认即可):
    • Preemption Priority:1;
    • Sub Priority:0;
  • 点击Apply。
  • 步骤 6:生成工程代码

  • 点击右上角Project Manager,配置项目信息:
    • Project Name:CAN_Demo(自定义);
    • Project Location:选择保存路径;
    • Toolchain/IDE:MDK-ARM V5;
  • 点击Code Generator,勾选Generate peripheral initialization as .c/.h files(分文件生成代码,整洁);
  • 点击GENERATE CODE,生成完成后点击Open Project,自动打开 Keil5 工程。
  • 案例 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 库源码)
  • CanTxMsgTypeDef/CanRxMsgTypeDef:旧版 CAN 库的专属收发结构体,源码中HAL_CAN_Transmit()/HAL_CAN_Receive()均基于该结构体操作,核心成员:
    • StdId:标准 ID(11 位);
    • IDE:ID 类型(CAN_ID_STD/CAN_ID_EXT);
    • RTR:帧类型(CAN_RTR_DATA数据帧 /CAN_RTR_REMOTE远程帧);
    • DLC:数据长度(0~8);
    • Data[8]:收发的 8 字节数据;
  • hcan1.pTxMsg = &TxMsg:源码中HAL_CAN_Transmit()会通过hcan->pTxMsg获取发送数据,必须赋值,否则发送失败(初学者核心坑);
  • HAL_CAN_Transmit(&hcan1, HAL_MAX_DELAY):源码中该函数会先查询发送邮箱是否为空,然后填充数据并请求发送,超时时间HAL_MAX_DELAY表示一直等待直到发送成功;
  • HAL_CAN_Receive(&hcan1, CAN_FIFO0, 10):源码中该函数会查询 FIFO0 是否有未处理的帧,有则填充RxMsg并释放 FIFO,超时 10ms 表示 10ms 内无数据则返回超时,不阻塞主循环;
  • 过滤器分配 FIFO0:接收时必须指定CAN_FIFO0,与 CubeMX 中过滤器配置的 FIFO 一致,否则收不到数据。
  • 硬件测试步骤

  • 按前文连接硬件:STM32 PA11/PA12 → TJA1050(TX/RX 交叉接),TJA1050 开启 120Ω 终端电阻,5V 供电,共 GND;
  • TJA1050 的 CAN_H/CAN_L → CAN 分析仪的 CAN_H/CAN_L,CAN 分析仪接电脑,打开配套软件CANTest;
  • CAN 分析仪配置:波特率500Kbps,标准 ID,打开总线;
  • 下载代码到 STM32,打开串口助手(波特率 115200),同时观察 CAN 分析仪和串口助手:
    • 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 库源码)

  • HAL_CAN_Receive_IT(&hcan1, CAN_FIFO0):源码中该函数会开启 CAN1 的 FIFO0 中断,配置好中断优先级后,有数据接收时会触发CAN1 RX0 interrupts,并调用HAL_CAN_IRQHandler()中断服务函数;
  • HAL_CAN_IRQHandler():源码中该函数是 CAN 的中断总处理函数,会判断中断类型(接收 / 发送 / 错误),然后调用对应的私有函数(如CAN_Receive_IT()),最终触发用户重写的回调函数;
  • 重新开启接收中断:旧版 CAN 库的HAL_CAN_Receive_IT()是单次中断,接收一帧数据后会自动关闭中断,必须在回调函数中重新调用该函数,否则无法接收下一帧(初学者最大坑);
  • HAL_CAN_Transmit_IT(&hcan1):源码中该函数会提交发送请求,发送完成后触发中断,调用HAL_CAN_TxCpltCallback(),主循环无需等待,实现非阻塞发送;
  • 弱回调函数:源码中HAL_CAN_RxCpltCallback()/HAL_CAN_TxCpltCallback()被__weak修饰,用户在main.c中重写后,编译器会优先使用用户的函数,这是 HAL 库的核心设计思想。
  • 测试步骤

  • 硬件连接同案例 1,CAN 分析仪配置 500Kbps,标准 ID;
  • 下载代码到 STM32,串口助手打印 “开启 CAN 接收中断成功”,LED 开始闪烁(证明主循环无阻塞);
  • CAN 分析仪发送数据:向 STM32 发送标准 ID0x123的 8 字节数据,串口助手立即打印 “中断接收成功”,且能连续接收(回调中重新开启了中断);
  • 按键触发发送:按下 PA0 按键,STM32 通过中断发送数据,CAN 分析仪可收到,串口助手打印 “发送请求已提交”→“发送完成”;
  • 若 CAN 总线出错(如断开 CAN_H),串口助手会打印错误码,触发HAL_CAN_ErrorCallback()。
  • 案例 3:CAN 回环模式测试(无硬件,快速验证代码)

    若没有 TJA1050 和 CAN 分析仪,可使用CAN 回环模式(Loopback),STM32 内部自收自发,无需任何外部硬件,快速验证 CAN 配置和代码的正确性:

  • CubeMX 中修改 CAN 模式为Loopback(Normal→Loopback),重新生成代码;
  • 用案例 2 的中断代码,无需硬件连接,直接下载;
  • 按下按键触发发送,STM32 会自己接收自己发送的数据,串口助手同时打印 “发送完成” 和 “接收成功”,快速验证代码无问题。
  • 初学者常见问题与避坑技巧(按出现频率排序)

    问题 1:CAN 无法发送 / 接收,无任何反应

    原因 & 解决方案:

  • 未接 CAN 收发器(TJA1050):STM32 的 CAN 是控制器,必须接收发器才能接总线;
  • TX/RX 未交叉接:TJA1050 的 TXD 接 STM32 的 CAN_RX,RXD 接 CAN_TX,交叉是核心;
  • 未开启 120Ω 终端电阻:短接 TJA1050 的终端电阻跳线;
  • 波特率不匹配:通信双方波特率必须完全一致(如 STM32500K,CAN 分析仪也必须 500K);
  • 未配置 CAN 过滤器:CubeMX 中未配置过滤器,或过滤器参数错误,CAN 无法接收任何数据;
  • hcan1.pTxMsg未赋值:旧版库必须将发送结构体赋值给hcan->pTxMsg,否则发送失败。
  • 问题 2:CAN 只能接收一帧数据,后续无反应

    原因 & 解决方案:

    • 中断模式下未重新开启接收中断:旧版 CAN 库的HAL_CAN_Receive_IT()是单次中断,接收一帧后必须在HAL_CAN_RxCpltCallback()中重新调用该函数,才能接收下一帧(案例 2 已处理)。

    问题 3:CAN 发送成功,但接收不到数据

    原因 & 解决方案:

  • 过滤器配置错误:如过滤器模式选了列表模式,或掩码非 0x0000,导致过滤掉了数据,入门直接用掩码模式 + 全 0 掩码,接收所有帧;
  • 接收时 FIFO 编号错误:过滤器分配到 FIFO0,接收时却用了 FIFO1,改回 CAN_FIFO0;
  • 回环模式未关闭:若 CubeMX 中是 Loopback 模式,外部 CAN 分析仪发送的数据无法被接收,改回 Normal 模式。
  • 问题 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 个核心要点:

  • 硬件核心:STM32 CAN 控制器必须搭配 TJA1050 收发器,TX/RX 交叉接,开启 120Ω 终端电阻;
  • 配置核心:CubeMX 中必须配置时钟树(APB1=36MHz)+ CAN 波特率 + 过滤器(接收所有帧),过滤器是接收数据的前提;
  • 代码核心:
    • 轮询模式:定义CanTxMsgTypeDef/CanRxMsgTypeDef,赋值hcan->pTxMsg,调用HAL_CAN_Transmit()/HAL_CAN_Receive();
    • 中断模式:开启 CAN_RX0 中断,重写回调函数,接收完成后必须重新开启中断,实现连续接收。
  • 掌握这三个案例后,可轻松过渡到实际项目,如多节点 CAN 通信、扩展 ID 使用、过滤器精准筛选 ID 等,后续只需在现有代码基础上稍作修改即可。

    赞(0)
    未经允许不得转载:171主机测评 » CAN通信:STM32F1xx_hal_can入门实战详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址