tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — 第1章:CAN + 配套代码01~04" author: "尚硅谷研究院" date: 2026-07-15
用生活理解:CAN 总线就像一场没有主持人的圆桌会议——谁都可以发言(多主),但发言时先报自己的"优先级数字"(ID)。数字小的优先级高,即使两个人同时开口,优先级低的那位会自动闭嘴让优先级高的先说。这就是 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Ω 终端电阻(匹配阻抗,防止信号反射)。
| 帧类型 | 用途 | 软件控制? |
|---|---|---|
| 数据帧 | 发送数据 | ✅ |
| 远程帧 | 请求具有相同 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。
仲裁原则:谁先发 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 等总线空闲自动重试。
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
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 | ❌ 不支持 |
| 自动重发 | ✅ 可配置 |
主动内核(配置/控制/状态寄存器)
↓
发送邮箱[0~2] → CAN 内核(发送调度) → Tx 引脚
↓
接收滤波器(14组) ← CAN 内核(接收) ← Rx 引脚
↓
接收 FIFO[0~1] (各3级深度) → CPU 通过寄存器读取
| 模式 | 如何进入 | 特点 |
|---|---|---|
| 初始化 | MCR.INRQ=1, 等待 MSR.INAK=1 | 可配置寄存器(波特率、过滤器等) |
| 正常 | MCR.INRQ=0, 等待 MSR.INAK=0 | 正常收发 |
| 睡眠 | MCR.SLEEP=1 | 低功耗,检测到 CAN 总线活动自动唤醒 |
| 模式 | 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_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() |