STM32 CAN总线的理论到实操

STM32 CAN总线的理论到实操

摘要:本文是一篇面向嵌入式初学者的 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页)

相关探索

天天酷跑芯片哪个好 四大芯片实用分析对比
bt365体育在线

天天酷跑芯片哪个好 四大芯片实用分析对比

后羿应该什么样?
s365 2.2.3

后羿应该什么样?