--- 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`** ```c #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`** ```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 图形化配置波特率/过滤器/工作模式。 ```c // 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)` | CubeMX 自动处理 | | 设置波特率 | `BTR = BRP + (TS1<<16) + (TS2<<20) + (SJW<<24)` | 结构体 `CAN_InitTypeDef` | | 环回静默 | `BTR |= SILM + LBKM` | `CAN_MODE_LOOPBACK` | | 发送 | 邮箱 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 脱离总线)