摘要:本文是一篇面向嵌入式初学者的 CAN 总线实操教程,从汽车电子场景出发,体系化梳理了 CAN 的核心知识与 STM32 开发实战。全文共四章:第 1 章介绍 CAN 的起源与标准化;第 2 章详解物理层特性(串行/异步/半双工)、差分信号原理及速率分级;第 3 章深入解析数据帧、遥控帧等 5 种帧类型及其结构;第 4 章聚焦 STM32 bxCAN 控制器,涵盖内部框图、位时序、波特率计算,并给出 HAL 库完整收发代码示例与接线验证。文章以“理论为骨架、代码为血肉、截图验证效果”的思路编排,旨在帮助读者动手搭建 CAN 通信链路。
1. CAN是什么
CAN,全称 Controller Area Network,是一种被 ISO 标准化的串行通信协议。
它最初是为汽车而生。一辆车里塞满了各种电子控制系统,彼此都要通信,早期只能"一个系统拉一组线",结果线束越堆越多、又重又乱。为了解决"用更少的线,让多个节点高速互通"这个矛盾,博世(Bosch)于 1986 年提出了 CAN,后经 ISO 11898 / ISO 11519 标准化,成为汽车网络的事实标准。
凭借高可靠性,CAN 早已走出汽车,广泛用于工业自动化、医疗设备、船舶等领域。
车载网络的构想示意图。CAN 等通信协议的开发,使多种 LAN 通过网关进行数据交换得以实现。
CAN 的应用示例
2. CAN 总线物理层:差分信号、总线组成与速率
2.1 通信基础特性:串行、异步、半双工
串行:一位一位传输,节省线束,适合车载环境
异步:无独立时钟线,靠位时序同步(与 SPI 不同)
半双工:同一时刻只能发或收,不能同时双向
与 USB / HDMI / 以太网 / RS-485 的简单对比(用一句话表格区分)
特性
LIN(UART)
USB
HDMI
以太网(100BASE-T1)
RS-485
传输方式
串行
串行(差分)
串行(差分,多 Lane)
串行(差分)
串行(差分)
同步方式
异步(无时钟线)
同步(内嵌时钟)
同步(TMDS 编码)
同步(内嵌时钟)
异步 / 同步均可
双工模式
半双工(单线)
全双工
全双工
全双工
半双工(2 线)/ 全双工(4 线)
典型速率
1–20 kbps
480 Mbps – 40 Gbps
4.95 – 48 Gbps
100 Mbps – 1 Gbps
最高 10 Mbps
拓扑
单主多从(≤16 节点)
星型(Host-Device)
点对点
星型 / 总线
总线(≤32 / 256 节点)
一句话定位
车内低速传感器/执行器控制
通用外设高速互联
音视频高带宽传输
车载/工业高速网络骨干
工业长距离多点通信
2.2 总线拓扑与组成:一条总线上挂多个节点
CAN 控制器根据两根线上的电位差来判断总线电平。总线电平分为显性电平和隐性电平,二者必居其一。发送方通过使总线电平发生变化,将消息发送给接收方。
CAN连接图
CAN收发器
2.3 差分信号:显性电平 vs 隐性电平
ISO11898、ISO11519-2 的物理层特征
这里介绍的是IOS11898的电特性
性质
逻辑
CAN_H
CAN_L
电压差
隐形
逻辑1
2.5V
2.5V
0V
显性
逻辑0
3.5V
1.5V
2V
为什么差分信号抗干扰强?(共模干扰同时抬升/拉低两根线,差值不变)
仲裁依赖显性覆盖隐性(逻辑 0 优先级高于逻辑 1)
2.4 速率分级:低速 / 高速 / CAN FD
通信速度和最大总线长度
类型
速率
传输距离
应用场景
低速 CAN
10–125 kbps
可达 1000 m
车身控制、门窗等
高速 CAN
125 kbps–1 Mbps
< 40 m
动力域、底盘、刹车
CAN FD
最高 8 Mbps
< 40 m(典型)
诊断、刷写、大带宽场景
3. CAN 帧的种类与用途
CAN 总线上的信息以“帧”为单位进行传输。根据功能不同,CAN 协议定义了 5 种帧类型。了解它们,是看懂总线活动和编写代码的基础。
序号
名称
帧用途
1
数据帧
用于发送单元向接收单元传送数据的帧。
2
遥控帧
用于接收单元向具有相同 ID 的发送单元请求数据的帧。
3
错误帧
用于当检测出错误时向其它单元通知错误的帧。(硬件自动完成)
4
过载帧
当一个节点正忙于处理接收的信息,可以通知其它节点暂缓发送新报文。(硬件自动完成)
5
帧间隔
用于将数据帧及遥控帧与前面的帧分离开来的帧。(硬件自动完成)
对于开发者而言,数据帧和遥控帧是需要主动操作和理解的,而错误帧、过载帧和帧间隔通常由硬件自动处理。下面我们重点看前两种。
3.1 数据帧:承载实际数据的“货车”
数据帧是 CAN 总线上最常用的帧,用于发送单元把数据交给接收单元。一帧由 7 个段按固定顺序拼接而成,可以记成七个动作:帧起始、仲裁、控制、数据、CRC、应答、帧结束。
标准帧与扩展帧的段顺序略有不同,为避免混淆,下面先完整讲标准帧,再完整讲扩展帧,最后用一张表对比二者差异。
数据帧的构成
3.1.1、标准帧(11 位 ID)
SOF(1) → 仲裁段[ ID(11) + RTR(1) ] → 控制段[ IDE(1) + r0(1) + DLC(4) ] → 数据段[ 0~64 位 ] → CRC段[ CRC(15) + 界定符(1) ] → ACK段[ ACK(1) + 界定符(1) ] → EOF(7)
字段
所属段
位宽
取值
作用
SOF
帧起始
1
恒为 0
总线由空闲转入活动,所有节点在此对齐起点
ID
仲裁
11
由代码赋值
标识报文;数值越小,优先级越高
RTR
仲裁
1
数据帧=0,遥控帧=1
区分数据帧与遥控帧,并参与仲裁
IDE
控制
1
恒为 0(标准格式下)
帧格式标识;标准帧此位为 0
空闲位r0
控制
1
恒为 0
保留位
数据长度DLC
控制
4
由代码赋值
数据段的字节数,经典 CAN 取 0~8
数据段
数据
0~64
由 DLC 决定
真正传输的字节;DLC=0 时无此段
CRC 段
CRC
15+1
校验值由硬件算;界定符=1
校验前面所有位(含位填充位)
ACK 段
ACK
1+1
发送=1,接收拉 0;界定符=1
接收方拉低,表示已正确收到
EOF
帧结束
7
全为 1
连续 7 个隐性位,标志本帧结束
仲裁:显性 0 压隐性 1 → ID 越小越优先;ID 撞车时,数据帧(RTR=0)压遥控帧(RTR=1)。
应答:发送方在 ACK 位松手发 1,任一正确收到的节点拉成 0;发送方回读到 0 = 至少一人收到。
长度:经典 CAN 中 DLC = 字节数(0~8;0 = 只发通知、不带数据)。CAN FD 的 DLC 9~15 跳变,不再线性对应。
提示:上表不含位填充——连续 5 个相同电平,硬件自动插 1 个反极性位(也计入 CRC)。故标准帧满载 108 位、扩展帧 128 位 只是"没算填充"的长度,真上总线还会更长。
3.1.2、扩展帧(11+18=29 位 ID)
SOF(1) → 仲裁段[ ID(11) + SRR(1) + IDE(1) + EID(18) + RTR(1) ] → 控制段[ r1(1) + r0(1) + DLC(4) ] → 数据段[ 0~64 位 ] → CRC段[ CRC(15) + 界定符(1) ] → ACK段[ ACK(1) + 界定符(1) ] → EOF(7)
字段
所属段
位宽
取值
作用
SOF
帧起始
1
恒为 0
同标准帧,对齐起点
ID
仲裁
11
由代码赋值
标识报文的高 11 位(Base ID)
SRR
仲裁
1
恒为 1
替代位,扩展帧占位用,标准帧无
IDE
仲裁
1
恒为 1(扩展格式下)
帧格式标识;扩展帧此位为 1,且位于仲裁段
EID
仲裁
18
由代码赋值
扩展 ID 的低 18 位,标准帧无
RTR
仲裁
1
数据帧=0,遥控帧=1
同标准帧
r1
控制
1
恒为 0
保留位,标准帧无
r0
控制
1
恒为 0
保留位
DLC
控制
4
由代码赋值
同标准帧
数据段
数据
0~64
由 DLC 决定
同标准帧
CRC 段
CRC
15+1
同标准帧
同标准帧
ACK 段
ACK
1+1
同标准帧
同标准帧
EOF
帧结束
7
全为 1
同标准帧
提示:除上表所列的结构差异外,扩展帧的仲裁、应答、校验、位填充等机制与标准帧完全相同,故不再重复。
标准帧与扩展帧的区别
对比维度
标准帧
扩展帧
ID 总位数
11
29(11 + 18)
仲裁段组成
ID + RTR
ID + SRR + IDE+ EID + RTR
仲裁段位宽
12
32
IDE 所在段
控制段
仲裁段
是否含 SRR / EID / r1
无
有
控制段组成
IDE + r0 + DLC
r1 + r0 + DLC
控制段位宽
6
6
固定开销(不含数据与填充)
44 位
64 位
一句话记忆:扩展帧把 IDE 从控制段挪进了仲裁段,并多出 SRR、EID、r1 三个位,使仲裁段由 12 位增至 32 位;而控制段二者同为 6 位,其余各段顺序完全一致。
3.2 遥控帧:主动索要数据的“订单”
遥控帧不携带数据,它的作用是向总线上某个节点“下单”,请求它发送具有特定 ID 的数据帧。
遥控帧的构成
差别项
数据帧
遥控帧
RTR
0
1
数据段
有,0~8 字节
无
DLC 的含义
本帧实际携带的字节数
希望对方回送的字节数
遥控帧与数据帧结构非常相似,关键区别在于:
RTR 位:在仲裁段,遥控帧的 RTR 位为隐性1(数据帧为显性0)。
数据段:遥控帧的 DLC 表示期望收到的数据长度,但其数据段为空。
工作流程:
节点 A 需要节点 B 的数据(ID=0x100),于是 A 向总线发送一个 ID=0x100、 RTR=1 (隐性)的遥控帧。
节点 B 监听到这个对自己 ID 的请求,便会将对应的数据(ID=0x100)以数据帧形式发送出来。
节点 A 以及其他监听该 ID 的节点都能收到这份数据。
应用场景:常用于初始化和诊断,例如,诊断仪(Tester)向 ECU 请求特定故障码或数据。
3.3 其他帧类型(硬件自动处理,了解即可)
这三种帧不用你写代码,硬件自己收发,知道"何时出现、干嘛的"就够:
错误帧:谁发现错(位错 / 格式错 / CRC 错)谁广播,发送方随后自动重发。
过载帧:接收方忙不过来时发,让发送方缓一缓。
帧间隔:两帧之间的"休息符",总线回隐性,给节点喘口气。
4. CAN 控制器
到这里我们将会进入芯片内部查看结构框图,然后进行收发代码的编写
4.1 内部框图:邮箱、FIFO 与滤波器
两条路径一对比就能看出 bxCAN 的设计哲学:CPU 永远只在邮箱和 FIFO 这两头插手,中间的位级收发和门卫过滤全是硬件。这也是你以后写收发代码时,脑子里该有的画面。
CAN框图
型号提醒:只有 互联型产品(F105 / F107) 才有双 CAN;普通 F103 只有单 CAN。若你用的是 F103,图中下半截 CAN2(从)整块以及 CAN2SB 相关说明均不存在,其中我使用的F103系列滤波器只有14个。
关键数字速记
资源
数量
说明
发送邮箱
3 个 / 每 CAN
可同时挂 3 条待发报文
接收 FIFO
2 组 / 每 CAN
FIFO0、FIFO1
每个 FIFO 深度
3 帧
CPU 晚取也不立刻丢
接收滤波器
28 个(0~27)
双 CAN 共享,分界由 CAN2SB 配;单 CAN 全归 CAN1(14个)
邮箱 / FIFO 存储
512 字节 SRAM
挂在 CAN1(主)侧
4.2 位时序:一个 bit 里的四个段
CAN 异步、无时钟线,各节点拿自己的时钟数节拍。但时钟有偏差、信号有延迟——怎么让大家在同一瞬间读电平、读出的 bit 还一致?办法是把一个 bit 切成 4 段,靠段对齐"采样点"。
一个 bit 由若干 Tq(Time Quantum,时间份额,最小时间单位) 拼成,切成 4 段,每段占几个 Tq 可配:
SS 同步段:bit 开头,对齐起点(固定 1Tq)。
PTS 传播段:吸收信号在导线上的传播延迟。
PBS1 / PBS2 相位缓冲段:吸收各节点时钟偏差,并用来挪采样点。
下图是同一 bit = 10Tq 的三种分法,PBS1、PBS2 配比一变,红箭头(采样点)就前后挪:
1 个位的构成
可见想让采样点靠后,就调大 PBS1、调小 PBS2;工程上常取一个 bit 的 70%~80%,读电平最稳。
各段的 Tq 取值范围及其作用
与代码的对应:HAL / CubeMX 把四段合并成三个参数——SS 固定为 1 不让配,PTS + PBS1 合成 BS1,PBS2 即 BS2。所以配置界面只看到 BS1 / BS2,对不上"四段"别慌,是合并关系。
4.3 波特率计算:从时钟树到 500 kbps
位时序参数最终要落到 CubeMX 的 Bit Timings 配置里,而波特率就是这些参数算出来的"结果"。
1、查时钟来源。 看时钟树可知 CAN 挂在 APB1 上,F103 的 APB1 = 36 MHz。
2、把配置项对应回位时序的段。 上图中 Prescaler = 4、BS1 = 9、BS2 = 8、SJW = 1,对应关系如下(注意 BS1 已含 PTS + PBS1):
3、套公式算波特率。 一个 bit 的 Tq 总数 = 1(SS) + BS1 + BS2 = 1 + 9 + 8 = 18,于是:
波特率 = APB1 频率 / 分频系数 / (1 + BS1 + BS2)
= 36 MHz / 4 / 18
= 0.5 MHz = 500 kbps
换个角度从 Tq 反推,结果一样:分频后 9 MHz → 1 Tq ≈ 0.111 μs → 18 Tq = 2 μs → 1 / 2 μs = 500 kbps,与界面的 Baud Rate = 500000 吻合。
进阶:采样点 = (1 + BS1) / (1 + BS1 + BS2) = 10 / 18 ≈ 55.6%,偏前,工程常取 70%~80%。短距离这组参数已够稳;要上长总线,保持波特率不变,调小 Prescaler、加大 BS1 把采样点后移即可。
4.4、代码编写
4.4.1配置过滤器,启动 CAN
//过滤器函数原型:
HAL_CsAN_ConfigFilter(CAN_HandleTypeDef *hcan, //句柄
const CAN_FilterTypeDef *sFilterConfig); //结构体
//启动CAN原型:
HAL_StatusTypeDef HAL_CAN_Start(CAN_HandleTypeDef *hcan);
//应用实例:
CAN_HandleTypeDef hcan; // CAN外设句柄,用于管理CAN的所有配置和状态
/* CAN过滤器配置:决定哪些CAN报文可以被接收 */
CAN_FilterTypeDef Can_FilterConfig;
Can_FilterConfig.FilterBank = 0; // 使用过滤器组 0(共14/28个,取决于芯片)
Can_FilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 过滤器模式:标识符掩码模式
Can_FilterConfig.FilterScale = CAN_FILTERSCALE_16BIT; // 过滤器位宽:16位模式
Can_FilterConfig.FilterIdHigh = 0x0; // 待匹配的ID高16位(全0表示接收所有ID)
Can_FilterConfig.FilterIdLow = 0x0; // 待匹配的ID低16位
Can_FilterConfig.FilterMaskIdHigh = 0x0; // 掩码高16位(全0表示不过滤,接收所有报文)
Can_FilterConfig.FilterMaskIdLow = 0x0; // 掩码低16位
Can_FilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0; // 匹配的报文存入FIFO0
Can_FilterConfig.FilterActivation = CAN_FILTER_ENABLE; // 使能该过滤器
Can_FilterConfig.SlaveStartFilterBank = 14; // 从CAN的起始过滤器组编号(仅双CAN模式有效)
/* 应用过滤器配置,失败则进入错误处理 */
if (HAL_CAN_ConfigFilter(&hcan, &Can_FilterConfig) != HAL_OK)
{
Error_Handler();
}
HAL_CAN_Start(&hcan); //启动CAN
4.4.2 接收模式和发送模式
//接收函数原型:从FIFO读取一条CAN报文
HAL_StatusTypeDef HAL_CAN_GetRxMessage(
CAN_HandleTypeDef *hcan, // CAN外设句柄
uint32_t RxFifo, // 读取哪个FIFO
CAN_RxHeaderTypeDef *pHeader, // 输出>接收报文头(ID、DLC、RTR、IDE、时间戳)
uint8_t aData[]); // 输出>接收到的8字节数据缓冲区
//发送函数原型:将报文放入发送邮箱
HAL_StatusTypeDef HAL_CAN_AddTxMessage(
CAN_HandleTypeDef *hcan, // CAN外设句柄
const CAN_TxHeaderTypeDef *pHeader, // 输入>发送报文头(需填充StdId、IDE、RTR、DLC)
const uint8_t aData[], // 输入>要发送的8字节数据
uint32_t *pTxMailbox); // 输出>返回使用的发送邮箱编号(0/1/2)
//应用实例:
uint8_t Tx_Data[] = {7, 8, 5, 24, 11, 2, 13, 9}; // CAN发送数据缓冲区(测试用,可自定义内容)
uint8_t Rx_Data[8]; // CAN接收数据缓冲区(8字节,CAN一帧最多8字节)
uint32_t Tx_Mail = CAN_TX_MAILBOX0; // 发送邮箱编号(0/1/2,用于查询发送状态)
#if 0 // 1: ZET6使用CAN接收模式 0: C8T6使用CAN发送模式
/* CAN接收报文头结构体,包含ID、DLC、RTR等信息 */
CAN_RxHeaderTypeDef RxMessage;
/* 从FIFO0读取一条CAN报文 */
HAL_CAN_GetRxMessage(&hcan, CAN_FILTER_FIFO0, &RxMessage, Rx_Data);
/* 打印接收到的报文信息:标准ID、数据长度、8字节数据 */
printf("recv id 0x%x:\r\n", RxMessage.StdId);
printf("recv length %u:\r\n", RxMessage.DLC);
printf("recv:");
for(int i = 0; i < 8; i++)
printf(" %u ", Rx_Data[i]);
printf("\r\n");
/* 清空接收缓冲区,验证memset是否生效 */
memset(Rx_Data, 0, 8);
printf("recv memset:");
for(int i = 0; i < 8; i++)
printf(" %u ", Rx_Data[i]);
printf("\r\n");
#else //可以看看3.1关于数据帧的介绍
/* CAN发送报文头结构体 */
CAN_TxHeaderTypeDef TxMessage;
TxMessage.StdId = 0x12; // 标准帧ID: 0x12
TxMessage.IDE = CAN_ID_STD; // 标准帧格式(非扩展帧)
TxMessage.DLC = 8; // 数据长度: 8字节
TxMessage.RTR = CAN_RTR_DATA; // 数据帧(非远程帧)
/* 将报文放入发送邮箱,触发CAN发送 */
HAL_CAN_AddTxMessage(&hcan, &TxMessage, Tx_Data, &Tx_Mail);
printf("Send ok!\r\n");
#endif
4.4.3 接线框图与效果演示
1、接线框图
2、效果演示
ZET6 打印出的 7 8 5 24 … 与 C8T6 的 Tx_Data 完全一致,ID = 0x12、长度 = 8 也对得上;memset 后全 0,说明一帧 8 字节完整收妥、缓冲区也清干净——链路通了。
资料参考:
CAN通信知识梳理及在Stm32上的应用(HAL库)
CAN总线入门书.pdf
STM32中文参考手册_V10.pdf(P423页)