13-CAN通信协议与bxCAN外设.md 15 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

#ifndef __CAN_H
#define __CAN_H

#include "stm32f10x.h"

// 接收数据结构
typedef struct {
    uint16_t stdID;       // 标准 ID
    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

#include "can.h"

// 过滤器配置(内部函数)
static void CAN_FilterConfig(void);

// CAN 初始化(环回静默模式,125Kbps)
void CAN_Init(void)
{
    /* ========== 1. 开启时钟 ========== */
    // CAN1 挂载在 APB1 总线上(36MHz)
    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;      // |=: CAN1时钟使能
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;       // |=: GPIOB时钟
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;       // |=: AFIO时钟(重映射用)

    /* ========== 2. 引脚重映射(PB8=RX, PB9=TX) ========== */
    // CAN 默认引脚是 PA11(RX)/PA12(TX)
    // 开发板使用重映射到 PB8(RX)/PB9(TX)
    // MAPR[25:24] = 10 → CAN 重映射到 PB8/PB9
    AFIO->MAPR |= AFIO_MAPR_CAN_REMAP_1;      // |=: 置位REMAP[1]
    AFIO->MAPR &= ~AFIO_MAPR_CAN_REMAP_0;     // &=~: 清零REMAP[0]

    /* ========== 3. GPIO 模式 ========== */
    // PB8(CAN_RX): 浮空输入 MODE=00, CNF=01
    GPIOB->CRH &= ~GPIO_CRH_MODE8;            // MODE8=00(输入)
    GPIOB->CRH &= ~GPIO_CRH_CNF8_1;           // CNF8[1]=0
    GPIOB->CRH |= GPIO_CRH_CNF8_0;            // CNF8[0]=1 → 浮空输入

    // PB9(CAN_TX): 复用推挽输出 MODE=11, CNF=10
    GPIOB->CRH |= GPIO_CRH_MODE9;             // MODE9=11(50MHz输出)
    GPIOB->CRH |= GPIO_CRH_CNF9_1;            // CNF9[1]=1
    GPIOB->CRH &= ~GPIO_CRH_CNF9_0;           // CNF9[0]=0 → 复用推挽

    /* ========== 4. CAN 初始化 ========== */
    // 4.1 进入初始化模式(INRQ=1, 等待 INAK=1)
    CAN1->MCR |= CAN_MCR_INRQ;               // |=: INRQ位置1
    while ((CAN1->MSR & CAN_MSR_INAK) == 0)  // 等待INAK=1确认
    {}

    // 4.2 退出睡眠模式(SLEEP=0, 等待 SLAK=0)
    CAN1->MCR &= ~CAN_MCR_SLEEP;             // &=~: SLEEP位清0
    while ((CAN1->MSR & CAN_MSR_SLAK) != 0)  // 等待SLAK=0
    {}

    // 4.3 自动离线管理
    CAN1->MCR |= CAN_MCR_ABOM;               // ABOM=1: 错误节点自动恢复

    // 4.4 自动唤醒管理
    CAN1->MCR |= CAN_MCR_AWUM;               // AWUM=1: 检测总线活动自动唤醒

    /* ========== 5. 配置位时序 ========== */
    // 5.1 测试模式:环回 + 静默
    CAN1->BTR |= CAN_BTR_SILM;               // SILM=1: 静默模式
    CAN1->BTR |= CAN_BTR_LBKM;               // LBKM=1: 环回模式

    // 5.2 波特率: APB1=36MHz, BRP=35 → Tq=1us
    // BS1=3, BS2=5 → 位时间=1+3+5=9us → Baud≈111Kbps
    CAN1->BTR &= ~CAN_BTR_BRP;               // 清除BRP位域
    CAN1->BTR |= (35 << 0);                   // BRP[9:0]=35
    CAN1->BTR &= ~CAN_BTR_TS1;               // 清除TS1位域
    CAN1->BTR |= (3 << 16);                   // TS1[3:0]=3 (BS1=4Tq)
    CAN1->BTR &= ~CAN_BTR_TS2;               // 清除TS2位域
    CAN1->BTR |= (5 << 20);                   // TS2[2:0]=5 (BS2=6Tq)
    CAN1->BTR &= ~CAN_BTR_SJW;               // 清除SJW位域
    CAN1->BTR |= (1 << 24);                   // SJW[1:0]=1

    // 5.3 退出初始化模式,进入正常模式
    CAN1->MCR &= ~CAN_MCR_INRQ;              // &=~: INRQ位清0
    while ((CAN1->MSR & CAN_MSR_INAK) != 0)  // 等待INAK=0确认
    {}

    /* ========== 6. 过滤器配置 ========== */
    CAN_FilterConfig();
}

static void CAN_FilterConfig(void)
{
    // 1. 进入过滤器初始化模式
    CAN1->FMR |= CAN_FMR_FINIT;              // FINIT=1: 允许配置过滤器

    // 2. 过滤器0: 掩码模式(32位)
    CAN1->FM1R &= ~CAN_FM1R_FBM0;            // FBM0=0: 掩码模式

    // 3. 过滤器位宽: 32位
    CAN1->FS1R |= CAN_FS1R_FSC0;             // FSC0=1: 32位宽

    // 4. 关联 FIFO: FIFO0
    CAN1->FFA1R &= ~CAN_FFA1R_FFA0;          // FFA0=0: 匹配帧→FIFO0

    // 5. 过滤器ID (FR1) = 0x06e ← 只接收 ID=0x06e 的帧
    CAN1->sFilterRegister[0].FR1 = 0x06e << 21;

    // 6. 过滤器掩码 (FR2) = 0x7f1 ← 高11位全部必须匹配
    CAN1->sFilterRegister[0].FR2 = 0x7f1 << 21;

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

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

// 发送报文(使用发送邮箱0)
void CAN_SendMsg(uint16_t stdID, uint8_t *data, uint8_t len)
{
    // 1. 等待邮箱0为空
    while ((CAN1->TSR & CAN_TSR_TME0) == 0)  // TME0=1 表示空
    {}

    // 2. 写入 ID
    CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_STID;       // 清除STID位域
    CAN1->sTxMailBox[0].TIR |= (uint32_t)stdID << 21; // 写入11位标准ID

    // 3. 写入 DLC(数据长度)
    CAN1->sTxMailBox[0].TDTR &= ~CAN_TDT0R_DLC;       // 清除DLC
    CAN1->sTxMailBox[0].TDTR |= len;                   // 写入长度

    // 4. 写入数据(最多8字节)
    for (uint8_t i = 0; i < len && i < 8; i++)
    {
        CAN1->sTxMailBox[0].TDLR = data[0];   // 低32位寄存器存data[0~3]
        if (len > 4)                           // 超过4字节的存入高32位
            CAN1->sTxMailBox[0].TDHR = *(uint32_t *)&data[4];
    }

    // 5. 请求发送(TXRQ=1)
    CAN1->sTxMailBox[0].TIR |= CAN_TI0R_TXRQ;
}

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