13-CAN通信协议与bxCAN外设.md 24 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — 第1章:CAN + 配套代码01~04" author: "尚硅谷研究院" date: 2026-07-15

created: 2026-07-15

CAN通信协议与bxCAN外设

用生活理解:CAN 总线就像一场没有主持人的圆桌会议——谁都可以发言(多主),但发言时先报自己的"优先级数字"(ID)。数字小的优先级高,即使两个人同时开口,优先级低的那位会自动闭嘴让优先级高的先说。这就是 CAN 的无损仲裁——不会因为冲突而浪费任何数据。


CAN 协议基础

CAN = Controller Area Network(控制器局域网),由 Bosch 公司 1986 年发明,主要用于汽车电子通信。

特性 说明
总线类型 多主从、广播式(所有节点同时接收)
物理层 差分信号(CAN_H / CAN_L),抗干扰强
速率 最高 1Mbps(40m 以内)
距离 5Kbps 时可达 10km
仲裁 无损位仲裁(ID 小的优先级高)
可靠性 CRC 校验 + 错误重传 + 错误节点自动脱离

参考https://www.kvaser.cn/about-can/can-protocol-tutorial/(CAN 协议教程中文版)

物理层 — 差分信号

CAN_H = 3.5V   CAN_L = 1.5V  →  显性 = 逻辑 0
CAN_H = 2.5V   CAN_L = 2.5V  →  隐性 = 逻辑 1

显性电平会"覆盖"隐性电平(仲裁机制的基础)

总线两端需要各接一个 120Ω 终端电阻(匹配阻抗,防止信号反射)。

协议层 — 5 种帧类型

帧类型 用途 软件控制?
数据帧 发送数据
远程帧 请求具有相同 ID 的节点发送数据
错误帧 检测到错误时通知所有节点 ❌ 硬件自动
过载帧 接收方未准备好时延迟发送 ❌ 硬件自动
帧间隔 分隔数据帧/远程帧 ❌ 硬件自动

数据帧结构(标准格式 11 位 ID)

┌──┬──────────┬───┬───┬──┬────┬──────────┬─────────────┬───────┬──┬──────────┐
│SOF│ ARBITRATION │CTRL│DATA(0~64)│    CRC    │ACK│    EOF   │
│ 1 │  ID 11+RTR1│ 6  │  0~8B   │ 15+1(del)│ 2  │    7     │
└──┴──────────┴───┴───┴──┴────┴──────────┴─────────────┴───────┴──┴──────────┘

逐段详解

字段 位数 说明
SOF 1 帧起始,显性电平(0),同步所有节点
ID 11 标识符,决定优先级(值越小优先级越高)
RTR 1 0=数据帧,1=远程帧
IDE 1 0=标准格式(11位ID),1=扩展格式(29位ID)
r0 1 保留位,显性
DLC 4 数据长度(0~8),单位字节
DATA 0~64 实际数据(0~8 字节,DLC 指定长度)
CRC 15 循环冗余校验(含 CRC 界定符 1 位)
ACK 2 应答:ACK 槽 + ACK 界定符。接收节点在 ACK 槽拉低(显性)= 正常收到
EOF 7 帧结束,全部隐性

扩展格式:29 位 ID(11 位基本 ID + 18 位扩展 ID),IDE=1。

CAN 总线仲裁

仲裁原则:谁先发 SOF → 谁先发 ID → ID 小的持续发显性(0) → ID 大的在隐性位(1)时丢失仲裁 → 自动转为接收。

示例(两个节点同时发送)

节点A: ID = 0x066  (二进制 000 0110 0110)
节点B: ID = 0x077  (二进制 000 0111 0111)

仲裁过程:
  位1-5: A=00001, B=00001 → 相同,继续
  位6:   A=1(隐性), B=1(隐性) → 相同,继续
  位7:   A=0(显性), B=1(隐性) → A 输出显性,B 输出隐性
         总线被 A 拉为显性 → B 读到总线和自身不同 → B 失去仲裁
         立即停止发送,转为接收

结果: 节点A 胜出,继续发送。节点B 等总线空闲自动重试。

CAN 的位时序

1 位 = Tq × (SYNC_SEG + BS1 + BS2)
     = Tq × (1 + TS1[3:0] + TS2[2:0])

STM32 把传播时间段合并到 BS1:

     ┌─────── SYNC_SEG ──────┐
     │   BS1(传播+相位缓冲1)  │
     │       BS2             │
     └───────────────────────┘

波特率计算

Baud = 1 / 位时间 = fAPB / BRP / (1 + BS1 + BS2)

fAPB = 36MHz (CAN1 在 APB1)
BRP = 35 → Tq = (35+1) / 36MHz = 1us
BS1 = 3 Tq, BS2 = 5 Tq → 位 = (1+3+5) × 1us = 9us
Baud = 1/9us ≈ 111Kbps

STM32 bxCAN 外设

bxCAN = Basic Extended CAN,支持 CAN 2.0A(标准帧)和 2.0B Active(标准+扩展帧)。

主要特性

特性
支持标准 CAN 2.0A + 2.0B Active
最高速率 1Mbps
发送邮箱 3 个(每个可缓存 1 帧)
接收 FIFO 2 个(每个 3 级深度,共 6 帧缓存)
过滤器组 14 个(0~13)
DMA ❌ 不支持
自动重发 ✅ 可配置

bxCAN 功能框图

主动内核(配置/控制/状态寄存器)
    ↓
发送邮箱[0~2] → CAN 内核(发送调度) → Tx 引脚
                                       ↓
接收滤波器(14组) ← CAN 内核(接收) ← Rx 引脚
    ↓
接收 FIFO[0~1] (各3级深度) → CPU 通过寄存器读取

3 种工作模式

模式 如何进入 特点
初始化 MCR.INRQ=1, 等待 MSR.INAK=1 可配置寄存器(波特率、过滤器等)
正常 MCR.INRQ=0, 等待 MSR.INAK=0 正常收发
睡眠 MCR.SLEEP=1 低功耗,检测到 CAN 总线活动自动唤醒

3 种测试模式

模式 BTR 配置 用途
静默 SILM=1 Rx 正常 → 可监听总线但不发送("只听不说的第三者")
环回 LBKM=1 Tx 内部回连到 Rx → 自收发不影响总线
环回静默 SILM=1 + LBKM=1 自收发且不影响总线(推荐调试用)

接收过滤器

14 个过滤器组(0~13),每组由 2 个 32 位寄存器(FR1, FR2)组成。

模式 说明 类比
掩码模式(32位) FR1=ID, FR2=掩码。掩码位=1 必须匹配,=0 不关心 模糊搜索
列表模式(32位) FR1 和 FR2 各存一个 ID,精确匹配 白名单
掩码模式(16位) FR1 存 2 个 ID,FR2 存 2 个掩码 双通道模糊搜索
列表模式(16位) FR1 和 FR2 各存 2 个 ID,共 4 个精确匹配 4 个白名单

参考:参考手册 §22(bxCAN 寄存器描述)


软件设计(寄存器版 — 环回静默测试)

项目路径stm32/01_can_test_register

文件:stm32/01_can_test_register/Hardware/CAN/can.h

/*
 * @Author: wushengran
 * @Date: 2024-12-21 16:52:20
 * @Description: 
 * 
 * Copyright (c) 2024 by atguigu, All Rights Reserved. 
 */
#ifndef __CAN_H
#define __CAN_H

#include "stm32f10x.h"

// 接收数据结构,保存接收到的报文信息
typedef struct
{
    uint16_t stdID;
    uint8_t data[8];
    uint8_t len;
} RxMsg;

// 初始化
void CAN_Init(void);

// 发送报文
void CAN_SendMsg(uint16_t stdID, uint8_t * data, uint8_t len);

// 接收报文
void CAN_ReceiveMsg(RxMsg rxMsg[], uint8_t * msgCount);

#endif

文件:stm32/01_can_test_register/Hardware/CAN/can.c

/*
 * @Author: wushengran
 * @Date: 2024-12-21 16:52:16
 * @Description: 
 * 
 * Copyright (c) 2024 by atguigu, All Rights Reserved. 
 */
#include "can.h"

static void CAN_FilterConfig(void);

// 初始化
void CAN_Init(void)
{
    // 1. 开启时钟:CAN、GPIO和AFIO
    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;

    // 2. 重映射PB8和PB9
    AFIO->MAPR |= AFIO_MAPR_CAN_REMAP_1;
    AFIO->MAPR &= ~AFIO_MAPR_CAN_REMAP_0;

    // 3. GPIO配置模式:
    // PB8 - 浮空输入,MODE 00,CNF 01
    GPIOB->CRH &= ~GPIO_CRH_MODE8;
    GPIOB->CRH &= ~GPIO_CRH_CNF8_1;
    GPIOB->CRH |= GPIO_CRH_CNF8_0;
    
    // PB9 - 复用推挽输出,MODE 11,CNF 10
    GPIOB->CRH |= GPIO_CRH_MODE9;
    GPIOB->CRH |= GPIO_CRH_CNF9_1;
    GPIOB->CRH &= ~GPIO_CRH_CNF9_0;

    // 4. CAN 初始化配置
    // 4.1 进入初始化模式
    CAN1->MCR |= CAN_MCR_INRQ;
    while ((CAN1->MSR & CAN_MSR_INAK) == 0)
    {}

    // 4.2 退出睡眠模式
    CAN1->MCR &= ~CAN_MCR_SLEEP;
    while ((CAN1->MSR & CAN_MSR_SLAK) != 0)
    {}

    // 4.3 自动离线管理
    CAN1->MCR |= CAN_MCR_ABOM;

    // 4.4 自动唤醒管理
    CAN1->MCR |= CAN_MCR_AWUM;

    // 4.5 设置环回静默模式
    CAN1->BTR |= CAN_BTR_SILM;
    CAN1->BTR |= CAN_BTR_LBKM;

    // 4.6 设置位时序
    // 4.6.1 设置波特率分频系数:35,Tq = 1us
    CAN1->BTR &= ~CAN_BTR_BRP;
    CAN1->BTR |= (35 << 0);

    // 4.6.2 设置BS1和BS2时间长度
    CAN1->BTR &= ~CAN_BTR_TS1;
    CAN1->BTR |= (2 << 16);

    CAN1->BTR &= ~CAN_BTR_TS2;
    CAN1->BTR |= (5 << 20);

    // 4.6.3 配置同步跳转宽度
    CAN1->BTR &= ~CAN_BTR_SJW;
    CAN1->BTR |= (1 << 24);

    // 4.7 退出初始化模式
    CAN1->MCR &= ~CAN_MCR_INRQ;
    while ((CAN1->MSR & CAN_MSR_INAK) != 0)
    {}

    // 5. CAN过滤器配置
    CAN_FilterConfig();
}

// 定义静态过滤器配置函数
static void CAN_FilterConfig(void)
{
    // 1. 进入初始化模式
    CAN1->FMR |= CAN_FMR_FINIT;

    // 2. 设置过滤器组工作模式:0 - 掩码位模式
    CAN1->FM1R &= ~CAN_FM1R_FBM0;

    // 3. 设置位宽:1 - 32位
    CAN1->FS1R |= CAN_FS1R_FSC0;

    // 4. 设置过滤器关联FIFO:FIFO0
    CAN1->FFA1R &= ~CAN_FFA1R_FFA0;

    // 5. 设置过滤器组0的ID寄存器:FR1
    CAN1->sFilterRegister[0].FR1 = 0x06e << 21;

    // 6. 设置过滤器组0的掩码寄存器:FR2 = 0,所有位都匹配
    CAN1->sFilterRegister[0].FR2 = 0x7f1 << 21;

    // 7. 激活过滤器组0
    CAN1->FA1R |= CAN_FA1R_FACT0;

    // 8. 退出初始化模式
    CAN1->FMR &= ~CAN_FMR_FINIT;
}

// 发送报文:标准ID、数据、长度,使用发送邮箱0
void CAN_SendMsg(uint16_t stdID, uint8_t * data, uint8_t len)
{
    // 1. 等待发送邮箱0为空
    while ( (CAN1->TSR & CAN_TSR_TME0) == 0)
    {
    }

    // 2. 组装要发送的数据帧
    // 2.1 设置标准ID
    CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_STID;
    CAN1->sTxMailBox[0].TIR |= stdID << 21;

    // 2.2 设为标准帧
    CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_IDE;

    // 2.3 设为数据帧
    CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_RTR;

    // 2.4 设置数据长度
    CAN1->sTxMailBox[0].TDTR &= ~CAN_TDT0R_DLC;
    CAN1->sTxMailBox[0].TDTR |= len << 0;

    // 2.5 写入数据
    // 清空寄存器
    CAN1->sTxMailBox[0].TDLR = 0;
    CAN1->sTxMailBox[0].TDHR = 0;

    // 循环写入每一个字节
    for (uint8_t i = 0; i < len; i++)
    {
        // 判断前4个字节,放到TDLR
        if (i < 4)
        {
            CAN1->sTxMailBox[0].TDLR |= data[i] << (i * 8);
        } 
        // 后4个字节,放到TDHR
        else
        {
            CAN1->sTxMailBox[0].TDHR |= data[i] << ((i - 4) * 8);
        }
    }

    // 3. 请求发送该帧
    CAN1->sTxMailBox[0].TIR |= CAN_TI0R_TXRQ;

    // 4. 等待发送完成
    while ((CAN1->TSR & CAN_TSR_TXOK0) == 0)
    {
    }
}

// 接收报文:结构体数组,数组长度,从FIFO0读取
void CAN_ReceiveMsg(RxMsg rxMsg[], uint8_t * msgCount)
{
    // 1. 获取FIFO0的报文个数,通过指针返回
    * msgCount = (CAN1->RF0R & CAN_RF0R_FMP0) >> 0;

    // 2. 循环取出每一个报文
    for (uint8_t i = 0; i < *msgCount; i++)
    {
        // 定义指针,指向当前保存报文的数据对象
        RxMsg * msg = &rxMsg[i];

        // 2.1 获取ID
        msg->stdID = (CAN1->sFIFOMailBox[0].RIR >> 21) & 0x7ff;

        // 2.2 获取数据长度
        msg->len = (CAN1->sFIFOMailBox[0].RDTR >> 0) & 0x0f;
        
        // 2.3 获取数据
        uint32_t low = CAN1->sFIFOMailBox[0].RDLR;
        uint32_t high = CAN1->sFIFOMailBox[0].RDHR;

        for (uint8_t j = 0; j < msg->len; j++)
        {
            // 如果是前4个字节,就从RDLR读取
            if (j < 4)
            {
                msg->data[j] = (low >> (8 * j)) & 0xff;
            }      
            // 如果是后4个字节,就从RDHR读取
            else
            {
                msg->data[j] = (high >> (8 * (j - 4))) & 0xff;
            }
        }

        // 2.4 释放FIFO0的邮箱,以便读取下一个报文
        CAN1->RF0R |= CAN_RF0R_RFOM0;
    }
}

文件:stm32/01_can_test_register/User/main.c

/*
 * @Author: wushengran
 * @Date: 2024-09-13 16:29:39
 * @Description:
 *
 * Copyright (c) 2024 by atguigu, All Rights Reserved.
 */

#include "usart.h"
#include "can.h"
#include <string.h>

int main(void)
{
	// 初始化
	USART_Init();
	CAN_Init();

	printf("尚硅谷CAN通讯实验:环回静默模式测试,寄存器版...\n");

	// 1. 发送三帧数据
	uint16_t stdID = 0x066;
	uint8_t * data = "abcdefg";
	CAN_SendMsg(stdID, data, strlen((char *)data));

	stdID = 0x068;
	data = "123";
	CAN_SendMsg(stdID, data, strlen((char *)data));

	stdID = 0x067;
	data = "xyz";
	CAN_SendMsg(stdID, data, strlen((char *)data));

	// 2. 开始接收
	RxMsg rxMsg[3];
	uint8_t msgCount;

	CAN_ReceiveMsg(rxMsg, &msgCount);

	printf("报文接收完毕,count = %d\n", msgCount);

	// 3. 打印接收到的数据
	for (uint8_t i = 0; i < msgCount; i++)
	{
		printf("stdID = %#X, len = %d, data = %.*s\n",
			 rxMsg[i].stdID, rxMsg[i].len, rxMsg[i].len, rxMsg[i].data);
	}

	while (1)
	{
	}
}

软件设计(寄存器版 — 双机收发通信)

项目路径stm32/03_can_tx_register(发送节点)、stm32/03_can_rx_register(接收节点)

与 01 项目相比,关键差别:

  • 不设置环回静默模式(SILM=0, LBKM=0 → 正常模式)
  • 过滤器掩码 FR2=0x00(接受所有报文,而非只匹配 0x06e)

can.c 差异(vs 01)

// 03 项目注释掉了环回静默配置(can.c:52-54)
// CAN1->BTR |= CAN_BTR_SILM;
// CAN1->BTR |= CAN_BTR_LBKM;

// 03 项目过滤器掩码设为 0(接受所有 ID)
CAN1->sFilterRegister[0].FR2 = 0x00;   // 01 项目为 0x7f1 << 21

文件:stm32/03_can_tx_register/User/main.c — 发送节点,每 1s 发一帧

#include "delay.h"
#include "usart.h"
#include "can.h"
#include <string.h>

int main(void)
{
    USART_Init();
    CAN_Init();

    printf("尚硅谷CAN通讯实验:双机收发测试 - 发送节点,寄存器版...\n");

    uint16_t stdID = 0x066;
    uint8_t * data = "abc";
    uint8_t buffer[10];
    uint32_t i = 0;

    while (1)
    {
        sprintf((char *)buffer, "%s %d", data, ++i);
        CAN_SendMsg(stdID, buffer, strlen((char *)buffer));
        printf("报文发送完毕...\n");
        Delay_ms(1000);
    }
}

文件:stm32/03_can_rx_register/User/main.c — 接收节点,持续轮询

#include "usart.h"
#include "can.h"
#include <string.h>

int main(void)
{
    USART_Init();
    CAN_Init();

    printf("尚硅谷CAN通讯实验:双机收发测试 - 接收节点,寄存器版...\n");

    RxMsg rxMsg[3];
    uint8_t msgCount;

    while (1)
    {
        CAN_ReceiveMsg(rxMsg, &msgCount);
        for (uint8_t i = 0; i < msgCount; i++)
        {
            printf("stdID = %#X, len = %d, data = %.*s\n",
                   rxMsg[i].stdID, rxMsg[i].len, rxMsg[i].len, rxMsg[i].data);
        }
    }
}

软件设计(HAL 库版 — 环回静默测试)

项目路径stm32/02_can_test_hal

CubeMX 生成 MX_CAN_Init,用户在 can.c 的 USER CODE 区添加过滤器和收发函数。

文件:stm32/02_can_test_hal/Core/Src/can.c — HAL 初始化与收发

CAN_HandleTypeDef hcan;

void MX_CAN_Init(void)
{
    hcan.Instance = CAN1;
    hcan.Init.Prescaler = 36;                      // 与寄存器版 BRP=35 对应
    hcan.Init.Mode = CAN_MODE_SILENT_LOOPBACK;     // 环回静默模式
    hcan.Init.SyncJumpWidth = CAN_SJW_2TQ;
    hcan.Init.TimeSeg1 = CAN_BS1_3TQ;
    hcan.Init.TimeSeg2 = CAN_BS2_6TQ;
    hcan.Init.TimeTriggeredMode = DISABLE;
    hcan.Init.AutoBusOff = ENABLE;
    hcan.Init.AutoWakeUp = ENABLE;
    hcan.Init.AutoRetransmission = DISABLE;
    hcan.Init.ReceiveFifoLocked = DISABLE;
    hcan.Init.TransmitFifoPriority = DISABLE;
    if (HAL_CAN_Init(&hcan) != HAL_OK)
        Error_Handler();
}

// CAN 过滤器配置
void CAN_FilterConfig(void)
{
    CAN_FilterTypeDef filterConfig;
    filterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
    filterConfig.FilterBank = 0;
    filterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
    filterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
    filterConfig.FilterIdHigh = 0x0000;
    filterConfig.FilterIdLow = 0x0000;
    filterConfig.FilterMaskIdHigh = 0x0000;
    filterConfig.FilterMaskIdLow = 0x0000;
    filterConfig.FilterActivation = ENABLE;
    HAL_CAN_ConfigFilter(&hcan, &filterConfig);
}

// 发送报文
void CAN_SendMsg(uint16_t stdID, uint8_t * data, uint8_t len)
{
    while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0) {}

    CAN_TxHeaderTypeDef txHeader;
    txHeader.StdId = stdID;
    txHeader.IDE = CAN_ID_STD;
    txHeader.RTR = CAN_RTR_DATA;
    txHeader.DLC = len;

    uint32_t txMailBox;
    HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &txMailBox);

    while (__HAL_CAN_GET_FLAG(&hcan, CAN_FLAG_TXOK0) == 0) {}
}

// 接收报文
void CAN_ReceiveMsg(RxMsg rxMsg[], uint8_t * msgCount)
{
    *msgCount = HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_RX_FIFO0);

    CAN_RxHeaderTypeDef rxHeader;
    for (uint8_t i = 0; i < *msgCount; i++)
    {
        HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &rxHeader, rxMsg[i].data);
        rxMsg[i].stdID = rxHeader.StdId;
        rxMsg[i].len = rxHeader.DLC;
    }
}

HAL 库的 CAN_TxHeaderTypeDef / CAN_RxHeaderTypeDef 等效于寄存器版的手动拼装 TIR/TDTR/TDLR/TDHR。CAN_FilterTypeDef 等效于操作 sFilterRegister[].FR1/FR2。

文件:stm32/02_can_test_hal/Core/Src/main.c — 环回测试逻辑(与 01 寄存器版效果相同)

#include "main.h"
#include "can.h"
#include "usart.h"
#include "gpio.h"
#include <string.h>

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_CAN_Init();
    MX_USART1_UART_Init();

    HAL_CAN_Start(&hcan);          // 启动 CAN 模块
    CAN_FilterConfig();            // 配置过滤器

    printf("尚硅谷CAN通讯实验:环回静默模式测试,HAL版...\n");

    // 发送三帧
    uint16_t stdID = 0x066;
    uint8_t *data = "abcdefg";
    CAN_SendMsg(stdID, data, strlen((char *)data));

    stdID = 0x068;
    data = "123";
    CAN_SendMsg(stdID, data, strlen((char *)data));

    stdID = 0x067;
    data = "xyz";
    CAN_SendMsg(stdID, data, strlen((char *)data));

    // 接收
    RxMsg rxMsg[3];
    uint8_t msgCount;
    CAN_ReceiveMsg(rxMsg, &msgCount);

    printf("报文接收完毕,count = %d\n", msgCount);
    for (uint8_t i = 0; i < msgCount; i++) {
        printf("stdID = %#X, len = %d, data = %.*s\n",
               rxMsg[i].stdID, rxMsg[i].len, rxMsg[i].len, rxMsg[i].data);
    }

    while (1) {}
}

软件设计(HAL 库版 — 双机收发通信)

项目路径stm32/04_can_tx_hal(发送节点)、stm32/04_can_rx_hal(接收节点)

与 02 项目的唯一区别是 MX_CAN_InitMode = CAN_MODE_NORMAL(而非 SILENT_LOOPBACK),其余 can.c 完全一致。

can.c 差异(vs 02)

hcan.Init.Mode = CAN_MODE_NORMAL;    // 02 项目为 CAN_MODE_SILENT_LOOPBACK

文件:stm32/04_can_tx_hal/Core/Src/main.c — HAL 发送节点

#include "main.h"
#include "can.h"
#include "usart.h"
#include "gpio.h"
#include <string.h>

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_CAN_Init();
    MX_USART1_UART_Init();

    HAL_CAN_Start(&hcan);
    CAN_FilterConfig();

    printf("尚硅谷CAN通讯实验:双机收发测试 - 发送节点,HAL版...\n");

    uint16_t stdID = 0x066;
    uint8_t * data = "abc";
    uint8_t buffer[10];
    uint32_t i = 0;

    while (1)
    {
        sprintf((char *)buffer, "%s %d", data, ++i);
        CAN_SendMsg(stdID, buffer, strlen((char *)buffer));
        printf("报文发送完毕...\n");
        HAL_Delay(1000);
    }
}

文件:stm32/04_can_rx_hal/Core/Src/main.c — HAL 接收节点

#include "main.h"
#include "can.h"
#include "usart.h"
#include "gpio.h"
#include <string.h>

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_CAN_Init();
    MX_USART1_UART_Init();

    HAL_CAN_Start(&hcan);
    CAN_FilterConfig();

    printf("尚硅谷CAN通讯实验:双机收发测试 - 接收节点,HAL版...\n");

    RxMsg rxMsg[3];
    uint8_t msgCount;

    while (1)
    {
        CAN_ReceiveMsg(rxMsg, &msgCount);
        for (uint8_t i = 0; i < msgCount; i++)
        {
            printf("stdID = %#X, len = %d, data = %.*s\n",
                   rxMsg[i].stdID, rxMsg[i].len, rxMsg[i].len, rxMsg[i].data);
        }
    }
}

双机实验需两块开发板,一块烧 TX(04_can_tx_hal),一块烧 RX(04_can_rx_hal),通过 CAN_H/CAN_L 对接并各接 120Ω 终端电阻。


HAL 库版 CAN

HAL 库使用 CAN_HandleTypeDef 管理 CAN 外设,CubeMX 图形化配置波特率/过滤器/工作模式。

// CubeMX 生成: MX_CAN1_Init() + 过滤器配置
CAN_HandleTypeDef hcan1;

// HAL 库发送
CAN_TxHeaderTypeDef tx_header;
uint8_t tx_data[8] = "Hello";
uint32_t tx_mailbox;

tx_header.StdId = 0x066;            // 标准 ID
tx_header.ExtId = 0;                 // 扩展 ID(标准帧不用)
tx_header.IDE = CAN_ID_STD;          // 标准帧
tx_header.RTR = CAN_RTR_DATA;        // 数据帧
tx_header.DLC = 5;                   // 数据长度

// 添加报文到发送邮箱(非阻塞)
HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, &tx_mailbox);

// HAL 库接收(轮询)
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8];
if (HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &rx_header, rx_data) == HAL_OK)
{
    printf("Rx ID=0x%03X, Data=%s\r\n", rx_header.StdId, rx_data);
}

// HAL 库中断方式:使能接收中断并实现回调
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

// 中断回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rx_header;
    uint8_t rx_data[8];
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);
    // 处理收到的报文
}

核心速查表

操作 寄存器操作 HAL 库
初始化模式 `MCR = INRQ; while(!MSR.INAK)`
设置波特率 BTR = BRP + (TS1<<16) + (TS2<<20) + (SJW<<24) 结构体 CAN_InitTypeDef
环回静默 `BTR = SILM + LBKM`
发送 邮箱 TIR/TDTxR/TDxR + TXRQ HAL_CAN_AddTxMessage()
接收 读 FIFO 邮箱寄存器 HAL_CAN_GetRxMessage()
配置过滤器 FINIT=1 → 设 FR1/FR2 → FINIT=0 CAN_FilterTypeDef 结构体
中断接收 EXTI + NVIC HAL_CAN_ActivateNotification()
回调 HAL_CAN_RxFifo0MsgPendingCallback()

常见问题与避坑

  1. CAN 初始化卡死在 INAK 等待 → 检查 CAN1 时钟是否开启(APB1ENR.CAN1EN)、引脚配置是否正确
  2. 环回正常但双机不通信 → 检查终端电阻(120Ω×2)、波特率一致、退出环回模式(清除 SILM/LBKM)
  3. 过滤器不生效 → 必须在 FINIT=1 模式下配置过滤器,激活后 FINIT=0
  4. 发送不出去 → 3 个邮箱全满(检查 TSR.TME[2:0])、未请求发送(TXRQ=0)
  5. 接收不到数据 → FIFO 溢出(RFOMR.FOVR)、过滤器未匹配、中断未使能
  6. CAN 错误状态 → 查询 ESR(错误状态寄存器),REC/TEC 计数(>127 进入错误被动,>255 脱离总线)