13-CAN通信协议与bxCAN外设.md 16 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/stm32/01_can_test_register

文件:stm32/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/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/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)
	{
	}
}

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 脱离总线)