目录
[一:为什么需要CAN总线 **Controller Area Network**](#一:为什么需要CAN总线 Controller Area Network)
[1:为什么需要 CAN?](#1:为什么需要 CAN?)
[1:SOF:Start Of Frame,帧起始](#1:SOF:Start Of Frame,帧起始)
[3:RTR (1 bit):Remote Transmission Request要求帧](#3:RTR (1 bit):Remote Transmission Request要求帧)
[4:IDE:Identifier Extension标识符扩展位](#4:IDE:Identifier Extension标识符扩展位)
[6:DLC(4bit):Data Length Code数据长度码](#6:DLC(4bit):Data Length Code数据长度码)
[8:CRC(15 bit)+ (1bit):循环冗余校验](#8:CRC(15 bit)+ (1bit):循环冗余校验)
[9:ACK (2 bit),应答位](#9:ACK (2 bit),应答位)
[10:EOF(7bit):End Of Frame 帧结束](#10:EOF(7bit):End Of Frame 帧结束)
[5:ReceiveFifoLocked:FIFO 满了以后,锁住,不再接收新的消息](#5:ReceiveFifoLocked:FIFO 满了以后,锁住,不再接收新的消息)
[6:TransmitFifoPriority:按 ID 优先级发](#6:TransmitFifoPriority:按 ID 优先级发)
[7:Mode:CAN 工作在哪种模式](#7:Mode:CAN 工作在哪种模式)
[11:Prescaler: 时钟分频](#11:Prescaler: 时钟分频)
[4:CAN_SetMsg --- 装一帧扩展帧](#4:CAN_SetMsg — 装一帧扩展帧)
[5:CAN_Filter_Config硬件过滤器(CAN Filter)](#5:CAN_Filter_Config硬件过滤器(CAN Filter))
一:为什么需要CAN总线 **Controller Area Network**
CAN = 多节点共享总线 + 报文 ID + 硬件仲裁 + 广播通信 + 完善的错误检测机制(可靠传输)。
UART / SPI / I²C 解决的是"板内或短距离、主从、点对点/少节点"的通信;CAN 解决的是"总线上几十上百个节点、长距离、强电磁干扰、多主竞争、要出错自动重传和故障节点自动退出"的现场级分布式控制问题。 它们不是谁替代谁,而是层次和场景不同。
1:为什么需要 CAN?
UART(异步串口),I²C ,SPI
它们都是"安静实验室/板内"的好接口,放到汽车发动机舱、工程机械、产线 PLC 那种"几十个 ECU、几米长的双绞线、旁边是点火线圈/继电器/电机"的环境里,扛不住。
你之前的 UART/SPI/I2C 都是点对点或一主多从,但工厂里几十个设备(变频器、电机、传感器、PLC)怎么同时通信?
CAN 也是差分,但电平定义和叫法不同:
CAN 用两根线:CAN_H 和 CAN_L(对应 485 的 A 和 B)
隐性电平(逻辑 1,总线空闲):
CAN_H ≈ 2.5V
CAN_L ≈ 2.5V
压差 ≈ 0V → 表示 1
显性电平(逻辑 0,有数据):
CAN_H ≈ 3.5V
CAN_L ≈ 1.5V
压差 ≈ 2V → 表示 0
工厂车间 = 一条长长的双绞线(CAN 总线)
│
├── 节点 1:变频器 ── 随时发:电机转速 1200rpm
├── 节点 2:温度传感器 ── 随时发:温度超阈值报警
├── 节点 3:PLC ── 随时发:紧急停机命令
└── 节点 4:你的 STM32 ── 随时收/发控制指令
所有节点挂在同两根线上,谁都能发,谁都能收。
2:CAN解决了什么问题
CAN 为了补这些短板,专门做了什么
//差分总线 + 强抗共模干扰
CAN_H / CAN_L 两根线差分传输,显性/隐性靠压差判断;
对电磁干扰(EMI)、地电位漂移容忍度高,典型车内几米到几十米、125 k~1 M bps 仍稳。
//多主 + 非破坏性仲裁(基于 ID)
任何节点随时可发,不用等 Master 点名;
两节点同时发,ID 小的继续、ID 大的自动退避,不丢数据、不重发整帧到撞坏,这就是你前面学的 11 位 ID 第一用途;
仲裁完接着发,总线利用率比“主从轮询”高得多。
//基于消息(Message-based),不是基于地址(Address-based)
你前面纠结过的“ID=0x123 发给谁?”——答案是广播给全总线,谁关心谁收;
这天然适合“一个传感器广播,多个控制器同时消费”(比如车速同时给仪表、VCU、TCU、ABS 用),而 UART/SPI/I²C 做不到这一点不绕一大圈应用层。
//硬件级错误检测 + 自动重传 + 节点自治
有 CRC、ACK、位监控、填充规则、格式检查五重错误检测;
一发错自动重传;
某节点疯发错误帧/短路,TEC 涨到 256 自动 Bus Off 自己下线,不拖死整网——这是 SPI/I²C 没有的“故障隔离”。
//确定性实时性
通过 ID 优先级把“刹车信号 > 空调面板”这种实时等级写死在帧里,仲裁即优先级调度;
不像 UART+RS485 多机靠软件令牌/轮询,最坏延迟算不准。
3:一些基本规则
CAN 协议本身并没有规定“最多 127 个节点”。实际能挂多少节点,主要受:
CAN 收发器的单位负载
总线长度
波特率
终端电阻
电气环境
节点收发器类型
影响。
CAN 的经典经验值确实经常看到:
1 Mbps ≈ 40 m
500 kbps ≈ 100 m
250 kbps ≈ 250 m
125 kbps ≈ 500 m
50 kbps ≈ 1000 m
因为实际距离还和:
CAN 收发器
线缆
拓扑
信号质量
采样点
系统时序
有关。
因此:通信速率越高,可支持的总线长度通常越短;典型 1 Mbps 约几十米,125 kbps 可达数百米量级。
CAN 的可靠性不只是 CRC + ACK
CAN 真正厉害的是这一整套:
CAN 可靠性
│
┌────────┼─────────┐
↓ ↓ ↓
CRC ACK 位监视
│ │ │
└────────┼─────────┘
↓
错误检测机制
↓
自动重发
↓
错误计数器 / Bus-Off
位监视:
你发 1(隐性)→ 总线上却是 0(显性)
→ 说明有更高优先级的节点同时发 0,"压住了"
→ 你的 1 被吃掉 → 你输了,立即退出,让别人优先
你发 0(显性)→ 总线上也是 0
→ 继续发下一位,还没"输"
这就是仲裁能自动运行的原因——每个节点发的每一 bit 都在跟总线上其他节点比较。比输了自动退出,全程不用 CPU 管,CAN 硬件自己做。物理层上 0 是强驱动的信号(能压住 1),1 是弱驱动的信号(被 0 压住就丢了)。
//错误计数器 / Bus-Off
CAN 的错误处理机制——"故障自愈":
每个 CAN 节点内部有两个计数器,出一次错 +一部分,恢复一次 -一部分。超过阈值自动把自己从总线上踢掉:
TEC (发送错误计数器) REC (接收错误计数器)
0~127: 正常模式 0~127: 正常模式
128~255: 被动错误模式 128~255: 被动错误模式
256: Bus-Off!掉线 256: 不可能达到
状态切换:
正常运行(Error Active)
错误多了 → 告警模式(TEC 或 REC > 96),还能发,但带个警告
更多了 → 被动模式(TEC 或 REC > 127),只能小声发,不敢抢显性了
再错 → Bus-Off(TEC > 255),自己拔网线!CAN 控制器自动失能,不再碰总线
被动模式下连续没问题 → 回到正常模式
Bus-Off 后要软件重新初始化才能恢复
减法和加法规则很简单——一次发送成功不撤销减 1,一次失败增 8。故意这么设计的:进入错误快、恢复等待。
场景:你板子的 CAN 收发器坏了、线断了或者波特率不匹配——反复发送失败被拒绝 → TEC 会狂涨 → 256 自动离线,避免一个人喊叫影响整台网络。
4:CAN几个不同协议
CAN 2.0A + 2.0B 都学,实际工控用的是 2.0B。
| 版本 | 工控实际使用 |
|---|---|
| CAN 2.0A (11位ID) | CANopen 协议用这个,够了 |
| CAN 2.0B (29位ID) | J1939(卡车/工程机械)用这个,也常见 |
| CAN FD | 新车在切,工控还没普及,你的 F429 不支持 |
| CAN XL | 太新了,十年后再看 |
F429 的 bxCAN 外设同时支持 11 位和 29 位 ID,你配个寄存器就能切换。先学 2.0A 的标准帧,11 位 ID 能区分 2048 个报文,大部分工控场景够了。后面看 J1939 协议的时候再切 29 位 ID 就行。
A 和 B 区别就一个------ID 长度:
CAN 2.0A(标准帧):
ID = 11 位 → 最多 2048 种报文
┌────┬──────────┬──────┬─────┐
│SOF │ ID(11位) │ CTRL │ DATA│
└────┴──────────┴──────┴─────┘
CAN 2.0B(扩展帧):
ID = 29 位 → 最多 ~5.37 亿种报文
┌────┬───────────────────┬──────┬─────┐
│SOF │ ID(29位) │ CTRL │ DATA│
└────┴───────────────────┴──────┴─────┘
你回环实验用的就是 CAN 2.0B------代码里 TxHeader.IDE = CAN_ID_EXT(扩展帧)+ TxHeader.ExtId = 0x1314(29 位 ID)。CAN 2.0A/B 不是两个不同的学完的东西------同一个 bxCAN 外设,硬件原生支持两种帧混用,代码只差一个 IDE 标志位。
5:CAN的来源
CAN = Controller Area Network(控制器局域网络)
三个词拆开:
-
Controller 控制器------最早是博世(Bosch)给汽车里的 ECU(电子控制单元)设计的
-
Area 局域------限定在车内/机器内这种局部范围
-
Network 网络------多节点互联
诞生背景:1986 年德国博世公司开发,为了让汽车里越来越多的电子模块(发动机、刹车、车窗、空调...)能用两根线互相通信,替代原来一堆乱糟糟的点对点线束。
RS485 和 CAN 这对工控兄弟,底层都是"差分抗干扰",但 CAN 多了一层硬件仲裁能力(显性压隐性),所以能实现多主同时抢总线而不冲突。485 没这能力,只能靠 Modbus 这类软件协议做一问一答。
二:CAN(A)一帧完整数据长这个样:
┌────┬─────────┬─────┬─────┬────┬──────┬──────────┬──────────┬──────┬─────┐
│SOF │ ID 11bit│ RTR │ IDE │ r0 │ DLC │ DATA │ CRC │ ACK │ EOF │
│1b │ │ 1b │ 1b │1b │ 4b │ 0~8 Byte │15b + 1b │ 2b │ 7b │
└────┴─────────┴─────┴─────┴────┴──────┴──────────┴──────────┴──────┴─────┘
1:SOF:Start Of Frame,帧起始
SOF = 1 bit 帧的开始标志
CAN总线处于空闲idle状态时,SOF为高电平 1
发送节点首先发送一个:SOF = 0 低电平,SOF 会把总线从空闲状态拉到显性状态,此时一帧CAN开始了。
2:ID:Identifier,标识符
标准 CAN(A):ID = 11 bit ,例如 ID = 0x123 转换成二进制:001 0010 0011 , CAN 会把这个 11 bit 发到总线上。
【12 位是十六进制表示让你产生了错觉。11 位是能表示 0~2047 的二进制位宽。
0x123 转换成二进制:1 0010 0011 = 只用 9 位,远没满 11 位。11 位最大能表示 0x7FF = 000 0111 1111 1111 = 2047。
0x 开头只是写法,不等于 12 位。0x123 存进 11 位寄存器时高位自动补零:0 0001 0010 0011。别把十六进制位数跟二进制位宽搞混。】
作用1:标识消息
例如你可以规定:
0x100 → 电机控制
0x200 → 电机转速
0x300 → 温度
0x400 → 电压
所以:ID = 0x200
不是说:发给 0x200 号设备
而是:这是编号为 0x200 的一种消息。
作用2:决定优先级
例如:
节点A:ID = 0x100
节点B:ID = 0x200
两个ID同时发送,所有节点同时收到,自己判断要不要处理。
二进制:
0x100 = 001 0000 0000
0x200 = 010 0000 0000
前面几个 bit 就会产生不同。
CAN 进行逐 bit 仲裁。
最终:
0x100 获胜
0x200 退出
CAN ID 数值越小,优先级越高。
3:RTR (1 bit):Remote Transmission Request要求帧
用于区分:数据帧 和 远程帧
RTR = 0 ,表示:这是数据帧,我后面带真正的数据。
RTR = 1,表示:这是远程帧,"我想请求某个 ID 的数据,你给我发回来。"不过实际现代 CAN 应用中,远程帧已经很少使用,基本重点学习数据帧即可。
/*把 CAN 总线想成公司的大喇叭广播:
某同事(ECU_A)对着喇叭喊:“0x123 号通知:转速 3000!”
办公室里所有人(所有节点)耳朵都听到了;
但只有桌牌上写了“我关心 0x123”的人(滤波匹配)会掏出本子记下来;
其他人当没听见,继续干活;
没人会问“这声广播是不是叫我”——因为本来就是叫全场的,只是你愿不愿意理。
这就解释了前面讲远程帧时那句“谁有 0x123 的数据,回一帧”:回出来的那一帧也是广播的,不是单独塞回给请求者;只是一般工程上只有“持有 0x123 数据源的那个节点”才会被配置成“收到 0x123 远程帧→回 0x123 数据帧”,别的节点即使也听到远程帧,也没数据可回、或者被滤波/软件忽略。
4:IDE:Identifier Extension标识符扩展位
IDE = 1 bit
它用来区分:
标准帧:11 bit ID IDE=0
扩展帧:29 bit ID IDE=1
对于我们现在讲的标准 CAN 数据帧:IDE = 0
5:r0保留位
在 CAN 2.0A 标准数据帧 的控制场里,IDE 位之后、DLC(4 位数据长度码)之前,那 1 个 bit 就是 r0(reserve bit 0,保留位 0);
在 CAN 2.0B 扩展数据帧 里则常见 r1 + r0 两个保留位,都在 DLC 前面。
r0 现在是"占坑位",协议当前版本不给它分配任何功能,只是预留给未来扩展用。
6:DLC(4bit):Data Length Code数据长度码
它告诉接收节点:后面的 DATA 有多少个字节
| DLC(二进制) | DLC(十六进制) | 数据场实际字节数 |
|---|---|---|
| 0000 | 0x0 | 0 字节 |
| 0001 | 0x1 | 1 字节 |
| 0010 | 0x2 | 2 字节 |
| 0011 | 0x3 | 3 字节 |
| 0100 | 0x4 | 4 字节 |
| 0101 | 0x5 | 5 字节 |
| 0110 | 0x6 | 6 字节 |
| 0111 | 0x7 | 7 字节 |
| 1000 | 0x8 | 8 字节 |
| 1001--1111 | 0x9--0xF | 经典 CAN 中非法 / 通常按 8 字节处理或报格式错 |
7:DATA(D0~D7):真正的数据
经典 CAN(CAN 2.0A/B) :Data 段长度为 0 到 8 个字节 。这取决于前面提到的 DLC 值。
Data 段没有固定的格式规定。它可以是:
-
整型数据:如发动机转速(2 字节)、水温(1 字节)。
-
浮点数据:如电压值(4 字节 float)。
-
布尔标志:如车门开关状态(1 字节,每一位代表一个门)。
字节数是一个非常关键的工程细节:
-
Intel 格式(小端) :低字节存放在低地址(先存低位)。例如数值
0x1234,先存0x34,再存0x12。 -
Motorola 格式(大端) :高字节存放在低地址(先存高位)。例如数值
0x1234,先存0x12,再存0x34。 -
注意:CAN 协议本身不强制规定字节序,完全取决于发送和接收双方事先约定好的协议矩阵(DBC文件)。
数据对齐:
如果实际数据长度不足 DLC 声明的长度(例如 DLC=8,但只发了 2 字节数据),发送节点通常会用 0x00 填充剩余的位置,以保证帧的总长度符合规范。
在帧结构中,Data 场紧跟在 DLC(数据长度码) 之后,位于 CRC(校验场) 之前。它是最容易被用户应用程序修改的部分,也是解析 CAN 报文时最核心的区域。
8:CRC(15 bit)+ (1bit):循环冗余校验
检查这一帧在传输过程中有没有发生错误,硬件自动算,错了自动丢弃。
CRC Delimiter(CRC 界定符)------专门隔开 CRC 和后一段的 1 个 bit,纯粹是个"格式分隔符 / 边界位",不属于 CRC 校验值本身,也不参与 CRC 计算。
CRC 场内部可能因为位填充(Bit Stuffing)规则出现连续同极性 bit;
如果 CRC 序列最后刚好连续好几个显性位,接收节点可能分不清"CRC 到哪结束、ACK 从哪开始";
插一个固定为隐的 CRC Delimiter,就像在书上画一条横线:前面都是 CRC,下一拍起进入 ACK 场(ACK 槽是显性 0 才能被应答),边界清清楚楚。
┌───────────────┬──────────────┐
│ CRC Sequence │ CRC Delimiter│
│ 15 bit │ 1 bit │
└───────────────┴──────────────┘
9:ACK (2 bit),应答位
ACK Slot(应答槽,第 1 个 bit)------真正干活的"我收到了"
**发送节点在这 1 bit 里做什么**:主动把总线驱动成 **隐性(1)**,也就是“我先让出来,看有没有人替我拉成显性”。、
**接收节点在这 1 bit 里做什么**:只要至少有一个接收节点**成功收完 CRC 且校验通过、格式也对**,就在这一位把自己的发送器打开,把总线拉成 **显性(0)**。
按 CAN “显性压隐性”规则:只要有 1 个节点应答,总线上这一位就变成显性 0。
所以这 1 位本质上是个**“广播式确认”**:不是“某个特定节点回我了”,而是“总线上至少有一个合格接收者收到了这帧”。
这也解释了 CAN 的一个特性:
一帧数据发出来,就算 5 个节点都收对了,ACK Slot 也只会被拉一次(显性)
反过来,如果这条总线上**没有任何节点**打开过滤器、或所有节点都 CRC 失败,那这一位就一直是隐性 → 发送节点判定 **ACK Error**。
ACK Delimiter(ACK 界定符,第 2 个 bit)------又是"画横线"的
固定为 **隐性(1)**
不参与应答,也不被任何节点抢成显性(抢了就是 Form Error);
作用就是像前面 CRC Delimiter 一样:把“ACK 槽”和后面的 EOF(帧结束,7 bit 隐性)干净分开,告诉所有节点“应答阶段结束了,下面进入收尾”。
如果 ACK 没收到:
发送错误计数器(TEC)加 8;
这一帧会在总线空闲后**自动重传**;
若反复 ACK 失败(比如接收节点掉线、终端电阻坏了、ID 被全滤掉、线缆断了一边),TEC 涨到 128 进“被动错误”,再涨到 256 就“Bus Off”自己离线
所以你看老 CAN 网络里“某节点一直重发同一帧、最后自己 Bus Off”,十有八九是先卡在 ACK 这一步——不是仲裁输,也不是 CRC 错,而是**根本没人给它回这一声“收到”**。
10:EOF(7bit):End Of Frame 帧结束
最后:EOF = 7 bit 全部为1 时候 1111111 ,明确告诉所有节点:这一帧结束了。
之后总线重新进入:Idle,随后下一帧又可以开始。
三:CAN总线硬件
1:电平转换
STM32F429 的 CAN 外设实际上提供的是:
STM32
┌──────────────┐
│ CAN控制器 │
│ │
│ CAN_TX ──────┼──→
│ CAN_RX ←─────┼──
└──────────────┘
这是逻辑电平信号。
而 CAN 总线需要:
CAN_H
CAN_L
这种差分物理信号。
所以中间需要:CAN电平转换芯片
例如:
TJA1050
SN65HVD230
SN65HVD232
TJA1051
2:节点
双机通讯 = 两台 STM32 + 两根线:
你的板子 A 你的板子 B(或另一块)
┌─────────────┐ ┌─────────────┐
│ F429 │ │ F429 │
│ bxCAN1 │ │ bxCAN1 │
│ PB8 TX ──→ │ TJA1042T(1) │ TJA1042T(1)│ ←── PB8 RX
│ PB9 RX ←── │ │ │ │ │ ←── PB9 TX
└─────────────┘ │ └──────┼──────┘
CAN_H ───────── CAN_H
CAN_L ───────── CAN_L
两根双绞线
两个stm32搭配两个芯片TJA1050,就是CAN总线实现双节点通讯了。
3:跳线帽接线
CAN/485 模式
Pin1(3V3) ●─────● CAN/485_3V3
485_TX ●─────● 485_RX
USART2_TX ●─────● USART2_RX
T1INT ●─────● R1OUT (给RS232的)
●─────●表示:两个引脚之间可以用跳线帽短接。
跳线帽插上 = 两个点导通。
跳线帽拔掉 = 两个点断开。
所以它其实就是一个人工控制的开关
你的板子 CAN 硬件链路齐了:
STM32 F429 SIT1042AQT 螺丝端子
PB9(TX) ────→ TXD ─→ CAN_H ────→ 拧上双绞线
PB8(RX) ←──── RXD ←─ CAN_L ←──── 接对面板子



USB-CAN 分析仪是挂在总线上的另一个节点,你的 STM32 仍然需要板载 SIT1042AQT 把自己发的 TTL 信号转成差分信号才能上总线。
你的板子 USB-CAN 分析仪
STM32 → PB9(TX) → SIT1042AQT → CAN_H ────→ CAN_H
→ CAN_L ────→ CAN_L
PB8(RX) ← SIT1042AQT ←───────←
↑ 需要供电!
J40 Pin1-Pin2 接上跳帽
不接 Pin1-Pin2 → SIT1042AQT 没电 → STM32 发的信号到不了总线上 → USB-CAN 分析仪收不到。Pin1-Pin2 就是 CAN 芯片的总开关,接上就行。
四:CAN回环实验1
回环模式下的数据流:
CPU 写数据到 CAN 发送邮箱
│
▼
┌──── bxCAN 外设(在 STM32 芯片内部)────┐
│ │
│ 发送引擎 ──→ 仲裁器 ──→──┐ │
│ │ │
│ ← 内部绕回 ←┘ │
│ ↓ │
│ 接收引擎 → CRC 校验通过 │
│ ↓ │
│ 匹配过滤器?→ 丢进 FIFO0 │
│ ↓ │
│ 触发 RX0 中断 │
└────────────────────────────────────────┘
│
▼
进入 CAN1_RX0_IRQHandler
PB8/PB9 引脚 ← 悬空,没人碰
TJA1042T ← 没电,不工作
SIT1042AQT ← 同上
跳线帽 ← 没接
螺丝端子 ← 空的
回环模式的数据永远不出 STM32 芯片——连 GPIO 引脚都不经过,在 bxCAN 的数字逻辑内部就绕回来了。
CAN初始化
↓
CAN位时序 / 波特率
↓
★ 构造一帧 CAN 报文
↓
★ 发送 CAN 报文
↓
接收 CAN 报文
↓
过滤器
↓
两个节点真正通信
1:CAN_MODE_LOOPBACK
不要把它理解成"CAN 正常通信模式"。
它是:内部回环测试模式。
正常 CAN:
STM32
│
│ CAN_TX
↓
CAN收发器
│
│ CAN_H / CAN_L
↓
CAN总线
│
↓
另一个节点
而 Loopback:自己发给自己
STM32
┌──────────────────┐
│ │
│ CAN发送 ──────┐ │
│ ↓ │
│ CAN接收│
│ │
└──────────────────┘
STM32
↓
CAN_AddTxMessage()
↓
CAN内部回环
↓
CAN接收FIFO
↓
CAN_GetRxMessage()
不需要 CAN 收发器、不需要 CAN_H/CAN_L、不需要第二块 STM32。
Loopback回环模式主要用于:验证 STM32 的 CAN 控制器和软件配置有没有问题。
CAN配置
发送
接收
FIFO
过滤器
这些基本工作正常。
但是Loopback 测不出来真实 CAN 总线的物理层问题。
比如:
-
CAN_H/CAN_L 接反
-
120Ω 终端电阻
-
CAN 收发器损坏
-
两个节点波特率不一致
-
总线干扰
这些 Loopback 都测不到。
所以实验一般是:
第一步:
Loopback
↓
验证 MCU CAN 软件配置
第二步:
Normal Mode
↓
两个 STM32 + CAN收发器
↓
真正 CAN 总线通信
2:1Mbps怎么得到的
APB1 = 45MHz / Prescaler = 5 / (1 + TimeSeg1 + TimeSeg2) = 1 + 5 + 3 = 9TQ → 1Mbps
这句话其实是在算:
CAN 一秒钟到底发送多少个 bit。
CAN 不是简单地:45 MHz→1 Mbps
而是:CAN时钟→Prescaler分频→得到 Time Quantum(TQ)→一个 CAN bit = 很多个 TQ→最终得到波特率
CAN 波特率 来源
第一步:APB1 = 45 MHz
第二步:Prescaler = 5,CAN时钟45 MHz ÷ 5=9MHZ ,得到一个TQ时间 = 1 / 9 MHz ≈111 ns
可以把所以你可以把:9 MHz理解为CAN 的 TQ 时钟。
这里的 TQ(Time Quantum,时间量子 / 时间份额) 基本可以确定是在说 CAN 总线位时序里的最小时间单位,不是操作系统里“时间片”。
第三步:一个 CAN bit 不是 1 个 TQ
一个 CAN bit 被划分成几个时间段:
一个 CAN Bit
┌──────┬───────────────┬───────────┐
│ Sync │ TimeSeg1 │ TimeSeg2 │
│ 1TQ │ 5TQ │ 3TQ │
└──────┴───────────────┴───────────┘
所以一个 bit:为1+5+3=9TQ
我们知道1 TQ ≈ 111 ns
因此9TQ≈ 1 μs
也就是一个 CAN bit ≈ 1 μs
1 s ÷ 1 μs
= 1,000,000 bit
CAN 波特率 = 1 Mbps
3:CAN初始化字段
CAN_Init
│
├── ① CAN 工作行为
│ ├── TimeTriggeredMode
│ ├── AutoBusOff
│ ├── AutoWakeUp
│ ├── AutoRetransmission
│ ├── ReceiveFifoLocked
│ ├── TransmitFifoPriority
│ └── Mode
│
└── ② CAN 位时序 / 波特率
├── SyncJumpWidth
├── TimeSeg1
├── TimeSeg2
└── Prescaler
你可以先把这两类分开,前面一组是在决定 CAN“怎么干活”,后面一组是在决定 CAN“以多快的节奏干活”。
static void CAN_Mode_Config(void)
{
CAN_CLK_ENABLE(); /* 开 CAN1 时钟 */
Can_Handle.Instance = CANx; /* CAN1 */
Can_Handle.Init.TimeTriggeredMode = DISABLE; /* 不用时间触发 */
Can_Handle.Init.AutoBusOff = ENABLE; /* 自动离线恢复 */
Can_Handle.Init.AutoWakeUp = ENABLE; /* 自动唤醒 */
Can_Handle.Init.AutoRetransmission = DISABLE; /* 禁止自动重发 */
Can_Handle.Init.ReceiveFifoLocked = DISABLE; /* FIFO 满不锁,新数据覆盖旧数据写入FIFO */
Can_Handle.Init.TransmitFifoPriority = DISABLE; /* 按 ID 优先级发 */
Can_Handle.Init.Mode = CAN_MODE_LOOPBACK; /* 回环模式 */
Can_Handle.Init.SyncJumpWidth = CAN_SJW_1TQ; /* SJW = 1TQ */
Can_Handle.Init.TimeSeg1 = CAN_BS1_5TQ; /* BS1 = 5TQ */
Can_Handle.Init.TimeSeg2 = CAN_BS2_3TQ; /* BS2 = 3TQ */
Can_Handle.Init.Prescaler = 5; /* 45÷5=9MHz → 9÷9=1Mbps */
HAL_CAN_Init(&Can_Handle);
}
1:TimeTriggeredMode:时间触发通信模式
普通 CAN:有数据→发送
普通 CAN vs 时间触发 CAN(TTCAN)
-
普通 CAN(事件触发) 多个节点都是"我数据准备好了→我就去仲裁抢总线"。优点是灵活,缺点是:某一时刻几个节点同时抢,会有重试、延迟抖动,最坏延迟不确定------这对汽车动力总成、底盘控制这类"必须 X µs 内响应"的场景不太友好。
-
时间触发 CAN(TTCAN,ISO 11898‑4) 先在总线上约定一个参考报文(Reference Message),所有节点都拿它来对齐自己的本地时间;然后把一个大的周期拆成若干个时槽(Time Slot / 基本周期 Matrix Cycle)。 例如一个矩阵周期 = 8 个基本周期,节点 A 只在第 1 个时槽发心跳+控制量,节点 B 只在第 3 个时槽发传感器数据,节点 C 在第 5 个时槽发......到点之外即使有数据也憋着等下一轮。 这样每条报文什么时候出现在总线上是可预测的,冲突几乎为零,最坏响应时间是确定的。
现在的阶段目标是:先让两块板子用 500kbps、标准帧、ID 匹配、带 120Ω 终端电阻,A 发 B 收成功 ------ 这时候上面这些"特殊行为"一个都不需要,全关着就是最稳的基线。
等你哪天出现"自动重传刷屏干扰调试""Bus-Off 后想自动恢复""多传感器要严格排时槽"这类具体痛点,再回来一个个开,每开一个就验证一次,记忆会非常牢。
2:AutoBusOff:总线关闭
假设一个 CAN 节点一直出错:
发送
↓
错误
↓
重发
↓
又错误
↓
又重发
↓
……
如果一个坏节点一直往总线上捣乱,会影响整个 CAN 网络。
所以 CAN 有错误计数器:错一次+8,发对1次重新+1,减到256就退出了这个节点主动退出 CAN 总线。"我好像坏了,我先闭嘴。"
AutoBusOff = ENABLE ,表示:允许硬件自动进行 Bus-Off 恢复。
所以:CAN出错→Bus-Off→满足恢复条件 →自动恢复通信
AutoBusOff = DISABLE,需要软件参与恢复,硬件躺平不动了 → 必须你软件手动调 HAL_CAN_Start() 或 HAL_CAN_Init() 才能恢复
工控场景一般开 ENABLE------节点偶尔因为干扰出错自动离线后自己能回来。关掉 DISABLE 适用于你知道出故障了必须人工检查的场景(比如车载制动控制器,离线了不能自己偷摸回来,必须确认问题解决了)。
3:AutoWakeUp:自动唤醒
这个跟低功耗有关,如果 CAN 节点进入休眠,此后总线上出现活动
CAN总线
↓
出现通信
↓
唤醒节点
就是:允许 CAN 外设检测到总线活动后自动唤醒
4:AutoRetransmission:自动重发
如果发送失败:自动重发,这符合 CAN 的典型工作方式。
5:ReceiveFifoLocked:FIFO 满了以后,锁住,不再接收新的消息
CAN 收到报文之后:
CAN总线
↓
CAN接收
↓
硬件过滤器
↓
FIFO
↓
CPU
FIFO 是一个队列:
┌──────────────────────┐
│ Msg1 │ Msg2 │ Msg3 │
└──────────────────────┘
假设 FIFO 满了,又来一个消息
如果
ReceiveFifoLocked=ENABLE:FIFO 满了以后,锁住,不再接收新的消息,已有消息保持不变。
ReceiveFifoLocked=DISABLE:FIFO 满了以后,不锁住,FIFO 满时允许新报文覆盖已有报文。
6:TransmitFifoPriority:按 ID 优先级发
两个层面,一内一外:
┌── STM32 内部排队(TransmitFifoPriority 管这个)──┐
│ │
│ 你的代码连发三帧: │
│ HAL_CAN_AddTxMessage(ID=0x300) // 先调 │
│ HAL_CAN_AddTxMessage(ID=0x100) // 第二 │
│ HAL_CAN_AddTxMessage(ID=0x200) // 最后 │
│ │
│ 三帧都在 STM32 自己的发送邮箱里排队: │
│ │
│ DISABLE(FIFO 模式): ENABLE(优先级模式): │
│ ┌────┬────┬────┐ ┌────┬────┬────┐ │
│ │300 │100 │200 │ │100 │200 │300 │ │
│ └────┴────┴────┘ └────┴────┴────┘ │
│ 按进队顺序发 按 ID 优先级发 │
│ 先发 300 ID 越小越先发 │
│ 再发 100 先发 100 │
│ 最后 200 再发 200 │
│ 最后 300 │
└────────────────────────────────────────────────────┘
│
▼ STM32 把下一帧放到总线上去
│
┌── 总线上多节点抢(CAN 硬件仲裁管这个,跟上面无关)──┐
│ │
│ 你的 STM32 发 ID=0x300 │
│ 别人的 STM32 发 ID=0x100 ← 同时抢 │
│ │
│ CAN 总线硬件仲裁 → 0x100 赢(ID 小) │
│ 你的 STM32 检测到自己输了,自动退避,等下次重发 │
└────────────────────────────────────────────────────┘
一句话:TransmitFifoPriority 决定你自己三个孩子谁先出门,CAN 仲裁决定出门后谁先过独木桥。
7:Mode:CAN 工作在哪种模式
CAN_MODE_LOOPBACK:回环模式自己发给自己。
CAN_MODE_NORMAL :正常跟另一个 STM32 通信
一个 CAN bit 并不是一个完整的时间单位,而是:
一个 CAN Bit = 9 TQ,TQ 就是 CAN 位时间的基本时间单位。
时间 →
┌──────┬──────────────────────┬──────────────┐
│ Sync │ BS1 │ BS2 │
│ 1TQ │ 5TQ │ 3TQ │
└──────┴──────────────────────┴──────────────┘
↑
采样点
采样点就在 BS1 结束的位置
所以在一个 bit 的 6/9 ≈ 66.7% 处采样。
8:SyncJumpWidth:同步bit发送
Sync = 1TQ 到底在干嘛?
这个可以理解为CAN发送每一个 bit 的起跑线,因为STM32作为CAN总线上的不同节点时,晶振不可能绝对一样。
↓ 从这里开始
↓
┌───────┬───────────────────────┐
│ Sync │ 后面的时间 │
│ 1TQ │ │
└───────┴───────────────────────┘
你可以把它想成:发令枪响了:"所有人从这里开始跑这一圈。
9:TimeSeg1:等待采样数据平稳
重点:
假设节点 A 发送一个 bit:
A
│
│ CAN_H/CAN_L
│
└────────────────────→
但是信号不是瞬间到达接收节点的,现实世界里存在:
发送器延迟
+
导线传播延迟
+
接收器延迟
+
信号边沿建立时间
所以:
发送节点:
0 ────────→ 1
接收节点:
0 ─────────────→ 1
↑
晚一点
这就是为什么不能刚开始就采样,给信号留时间,我们的配置是,Sync = 1TQ和BS1 = 5TQ
因此
起点 采样点
↓ ↓
┌──────┬───────────────────────────┐
│ Sync │ BS1 │
│ 1TQ │ 5TQ │
└──────┴───────────────────────────┘
前面一共:所以第 6TQ 结束附近进行采样,也就是说,在一个 bit 的前 66.7% 处采样。
比“50% 中点采样”稳一些;
但比工业上 500 k / 1 M CAN 常用的 75%~87.5%• 偏早;
所以在短距离、低速、教学板、收发器响应一般的场景下能通;真上汽车/长线缆/高波特率就可能开始掉帧、重传增多。
BS1=5 而不是 2 或 10,也是折中:太小→采样点太早、抗干扰差;太大→采样点太晚、留给 BS2 的余量不够、还可能拖慢波特率上限。
9:TimeSeg2:时序修正缓冲段
那 BS2 是干什么的?
采样之后到达BS2:
采样点
↓
┌──────────────┐
│ BS2 │
│ 3TQ │
└──────────────┘
为什么采样完还要留时间?
因为 CAN 不仅需要知道什么时候采样,还需要知道,如果本次采样不同步,下次该如何调整,BS2 就给后面的时序变化留出了空间。
一个 bit:
Sync BS1 BS2
↓ ↓ ↓
┌──────┬─────────────────┬─────────┐
│ 1TQ │ 5TQ │ 3TQ │
└──────┴─────────────────┴─────────┘
↑
采样点
比如发送节点认为:
bit 边界
↓
|-------------------------|
接收节点自己的时钟却认为:
bit 边界
↓
|---------------------------|
慢慢地,两个节点的"尺子"就有一点错位
假设接收节点预计:
边沿应该在这里
↓
──────────────│────────────
实际边沿:
───────────────│───────────
↑
晚了一点
CAN 就会说:"哎,实际边沿比我预想的晚,那我得把自己的时间轴往后调整一点。",这就是重新同步 Re-synchronization
10:SJW:最多允许调整多少
假设SJW = 1TQ,意思不是:"永远调整 1TQ。"
而是:"这次重新同步,最多允许调整 1TQ。"
比如实际只差:0.3TQ,那么SJW可以修正,
如果理论上差:2TQ,那么SJW无法修正,因为一次最多只能修 1TQ。
实际调整中:SJW 是调整采样点附近时序的"活动范围"。
原来:
Sync |──── BS1 ────| BS2
|──── 5TQ ────|──3TQ──
↑
采样点
Sync |──── BS1 ─────| BS2
|──── 6TQ ─────|─2TQ─
↑
采样点后移
此时
//BS1 延长一点,BS2 缩短一点
Sync |──── BS1 ─────| BS2
|──── 6TQ ─────|─2TQ─
↑
采样点前移
此时
//BS1 缩短一点,BS2 延长一点
不是改寄存器:这是 CAN 控制器在当前 bit 的时序运行过程中,临时调整时间长度。
11:Prescaler: 时钟分频
第一步:APB1 = 45 MHz
第二步:Prescaler = 5,CAN时钟45 MHz ÷ 5=9MHZ ,得到一个TQ时间 = 1 / 9 MHz ≈111 ns
可以把所以你可以把:9 MHz理解为CAN 的 TQ 时钟。
这里的 TQ(Time Quantum,时间量子 / 时间份额) 基本可以确定是在说 CAN 总线位时序里的最小时间单位,不是操作系统里“时间片”。
第三步:一个 CAN bit 不是 1 个 TQ
一个 CAN bit 被划分成几个时间段:
一个 CAN Bit
┌──────┬───────────────┬───────────┐
│ Sync │ TimeSeg1 │ TimeSeg2 │
│ 1TQ │ 5TQ │ 3TQ │
└──────┴───────────────┴───────────┘
所以一个 bit:为1+5+3=9TQ
我们知道1 TQ ≈ 111 ns
因此9TQ≈ 1 μs
也就是一个 CAN bit ≈ 1 μs
1 s ÷ 1 μs
= 1,000,000 bit
CAN 波特率 = 1 Mbps
4:CAN_SetMsg --- 装一帧扩展帧
void CAN_SetMsg(void)
{
TxHeader.StdId = 0; /* 标准 ID 不用 */
TxHeader.ExtId = 0x1314; /* ★扩展 ID */
TxHeader.IDE = CAN_ID_EXT; /* 扩展帧 */
TxHeader.RTR = CAN_RTR_DATA; /* 数据帧 */
TxHeader.DLC = 8; /* 8 字节 */
TxHeader.TransmitGlobalTime = DISABLE;
for (uint8_t i = 0; i < 8; i++) TxData[i] = i; /* Data = {0,1,2,3,4,5,6,7} */
uint32_t mailbox;
HAL_CAN_AddTxMessage(&Can_Handle, &TxHeader, TxData, &mailbox);
}
1:TxHeader.StdId
TxHeader.StdId = 0; // 标准帧 11 位 ID,范围 0x000~0x7FF(0~2047)
// 本例用扩展帧,这个字段填 0,硬件忽略
2:TxHeader.ExtId
TxHeader.ExtId = 0x1314; // 扩展帧 29 位 ID,范围 0x00000000~0x1FFFFFFF
// 0~约 5.37 亿,够整个汽车厂所有 ECU 各分配几千个 ID
3:TxHeader.IDE
TxHeader.IDE = CAN_ID_EXT; // 帧类型:
// CAN_ID_STD (0x00000000) = 标准帧,11 位 ID
// CAN_ID_EXT (0x00000004) = 扩展帧,29 位 ID
4:TxHeader.RTR
TxHeader.RTR = CAN_RTR_DATA; // 帧用途:
// CAN_RTR_DATA (0x00000000) = 数据帧,我带了数据
// CAN_RTR_REMOTE(0x00000002) = 远程帧,请求别人发(基本不用)
5;TxHeader.DLC
TxHeader.DLC = 8; // 数据长度 0~8 字节,填几就发几个字节
// 实际缓存区只有 4字节时 → 这里填的8,那么只发前 4 个字节,后 4 个被硬件忽略
6:TxHeader.TransmitGlobalTime
TxHeader.TransmitGlobalTime = DISABLE; // 时间戳功能,F429 基本不用:
// DISABLE = 不管
// ENABLE = 硬件在帧末尾自动附加时间戳(F7/H7 系列支持)
5:CAN_Filter_Config硬件过滤器(CAN Filter)
static void CAN_Filter_Config(void)
{
CAN_FilterTypeDef f = {0}; /* 创建FilterTypeDef 结构体*/
f.FilterBank = 0; /* 过滤器 0 */
f.FilterMode = CAN_FILTERMODE_IDMASK; /* 掩码模式 */
f.FilterScale = CAN_FILTERSCALE_32BIT; /* 32 位宽 */
{
uint32_t id = ((uint32_t)0x1314 << 3) | CAN_ID_EXT | CAN_RTR_DATA;
f.FilterIdHigh = (id >> 16) & 0xFFFF; /* ID 高 16 位 */
f.FilterIdLow = id & 0xFFFF; /* ID 低 16 位 */
}
f.FilterMaskIdHigh = 0xFFFF; /* 全部位严格匹配 */
f.FilterMaskIdLow = 0xFFFF;
f.FilterFIFOAssignment = CAN_FILTER_FIFO0; /* 放 FIFO0 ,有FIFO0和FIFO1*/
f.FilterActivation = ENABLE; //打开这个过滤器。
f.SlaveStartFilterBank = 0;
HAL_CAN_ConfigFilter(&Can_Handle, &f);
}
假设总线上有:
ID=0x100 → 电机转速
ID=0x200 → 温度
ID=0x300 → 电压
ID=0x400 → 故障
ID=0x500 → 位置
ID=0x600 → PLC命令
你的 STM32 只关心:ID = 0x1314
CAN 的特点是:总线上的所有节点都可以看到报文。
但是你肯定不希望:
CAN总线
↓
STM32
↓
CPU
↓
“这个ID我要不要?”
每一条消息都让 CPU 判断。所以 STM32 CAN 外设里面有:硬件过滤器
变成:
CAN总线
↓
┌──────────────┐
│ CAN硬件过滤器 │
└──────┬───────┘
│
├── 不匹配 → 丢弃
│
↓
FIFO0
↓
CPU
这就是你这个函数存在的意义。
CAN_FilterTypeDef 结构体:
过滤器编号
过滤器模式
过滤器宽度
过滤ID
过滤Mask
FIFO
是否启用
...
我要告诉 CAN 硬件过滤器怎么工作
1:FilterBank:过滤器
f.FilterBank = 0;使用 过滤器 0
STM32F4 的 CAN 有很多个过滤器,可以配置成0~13其中一个,14 个独立的硬件过滤器,每个可以独立配置"收什么样的报文"。
2:FilterMode:掩码模式和列表模式
有两种模式可选:
IDMASK(掩码模式)—— "要求某几位必须匹配"
f.FilterMaskIdHigh = 0xFFFF; //高16位 /* 全部位严格匹配 */
f.FilterMaskIdLow = 0xFFFF; //低16位
IDLIST(列表模式)—— "给你两个具体的 ID,只有这两个收" //给你一张“精确 ID 清单”,只有完全等于清单里某一个 ID 的帧才收
一个 Filter Bank 有两个32位寄存器,FxR1、FxR2,
所以:1 个 32 位 Filter Bank 在列表模式 = 最多放行 2 个标准/扩展 ID,一次读取2个ID
在 16 位尺度下,一个 Filter Bank 拆成 4 个 16 位条目,列表模式 = 最多放行 4 个标准 ID,16 位模式根本检查不了完整的扩展帧 ID。
所以你的选择很简单:
帧类型 用什么模式
标准帧(11 位 ID) 16BIT 或 32BIT 都行
扩展帧(29 位 ID) 必须 32BIT
3:FilterScale:32位过滤还是16位过滤
过滤器的位宽配置:
32BIT 模式(你用的):
一个过滤器 = 1 个 32 位槽位,能装 1 个完整的扩展帧 ID(29 位全用上)
┌──────────────────────┬──────────────────────┐
│ FilterIdHigh │ FilterIdLow │
│ (32bit 高 16 位) │ (32bit 低 16 位) │
└──────────────────────┴──────────────────────┘
一次比较 32 位,用于扩展帧(29 位 ID + 3 位标志)
16BIT 模式:
一个过滤器 = 2 个独立的 16 位槽位,能装 2 个标准帧 ID(11 位)
┌──────────────────────┬──────────────────────┐
│ 槽 0: 一个 16bit ID │ 槽 1: 另一个 16bit ID │
│ 匹配标准帧 ID │ 匹配标准帧 ID │
└──────────────────────┴──────────────────────┘
一个过滤器可以同时匹配两个不同的标准帧 ID
16BIT 32BIT
每个过滤器几个槽 2 个 1 个
每个槽多少位 16 位 32 位
适合帧类型 标准帧(11 位 ID) 扩展帧(29 位 ID)
14 个过滤器能识别 最多 28 个标准帧 ID 最多 14 个扩展帧 ID
你用扩展帧(CAN_ID_EXT)+ 29 位 ID,所以必须 32BIT 模式。
4:SlaveStartFilterBank:分配过滤器
双 CAN 时才用到------CAN1 和 CAN2 共享 28 个过滤器,SlaveStartFilterBank 划一条线:
SlaveStartFilterBank = 0(你现在的设置):
28 个过滤器全归 CAN1,CAN2 一个都没有
SlaveStartFilterBank = 14:
┌─────────────┬─────────────┐
│ 0~13 = CAN1 │ 14~27 = CAN2 │
└─────────────┴─────────────┘
各分 14 个
你只用 CAN1,填 0 就行。用不到这个字段。
5:构建32位bit
uint32_t id = ((uint32_t)0x1314 << 3) | CAN_ID_EXT | CAN_RTR_DATA;
f.FilterIdHigh = (id >> 16) & 0xFFFF; /* ID 高 16 位 */
f.FilterIdLow = id & 0xFFFF;
把 ExtId=0x1314 的扩展帧组装成 CAN 硬件能比较的 32 位格式:
CAN 扩展帧在硬件寄存器里的布局(32 位):
bit31 ───────────────────────────────────────────── bit0
┌────────────┬──────┬──────┬────┬────┬───────────┐
│ EXTID[28:0] (29位)│ 保留 │RTR │IDE │ 保留 │
└────────────┴──────┴──────┴────┴────┴───────────┘
↑
↑ bit2=IDE
bit1=RTR
第一步:构建完整的 32 位对比值
uint32_t id = ((uint32_t)0x1314 << 3) // ExtId 左移 3 位,留出 RTR/IDE 的空间
| CAN_ID_EXT // bit2=1 → 扩展帧
| CAN_RTR_DATA; // bit1=0 → 数据帧
结果 id = 0x000098A4:
┌──────────┬──┬──┬──┐
│ 0x1314<<3│ 0│ 0│ 1│ ← IDE=1 在 bit2
└──────────┴──┴──┴──┘
第二步:拆成高 16 位和低 16 位,填进过滤器的两个寄存器
f.FilterIdHigh = (id >> 16) & 0xFFFF; // = 0x0000(高 16 位,本例全是 0)
f.FilterIdLow = id & 0xFFFF; // = 0x98A4(低 16 位,含 ID+RTR+IDE)
硬件比较时:
收到一帧 → 把帧的 ExtId 也按同样格式拼成 32 位
→ 逐位比较 FilterIdHigh/Low + FilterMaskIdHigh/Low
→ 全部匹配 → 收进 FIFO0 ✅
就是"把你的 ExtId 转成硬件能理解的二进制格式,再拆成两个 16 位塞进寄存器"。
6:FIFO中断触发
HAL_CAN_ActivateNotification(&Can_Handle, CAN_IT_RX_FIFO0_MSG_PENDING); /* FIFO0 中断 */
触发源头
/* Receive Interrupts */
#define CAN_IT_RX_FIFO0_MSG_PENDING ((uint32_t)CAN_IER_FMPIE0) /*!< FIFO 0 message pending interrupt */
#define CAN_IT_RX_FIFO0_FULL ((uint32_t)CAN_IER_FFIE0) /*!< FIFO 0 full interrupt */
#define CAN_IT_RX_FIFO0_OVERRUN ((uint32_t)CAN_IER_FOVIE0) /*!< FIFO 0 overrun interrupt */
#define CAN_IT_RX_FIFO1_MSG_PENDING ((uint32_t)CAN_IER_FMPIE1) /*!< FIFO 1 message pending interrupt */
#define CAN_IT_RX_FIFO1_FULL ((uint32_t)CAN_IER_FFIE1) /*!< FIFO 1 full interrupt */
#define CAN_IT_RX_FIFO1_OVERRUN ((uint32_t)CAN_IER_FOVIE1) /*!< FIFO 1 overrun interrupt */
1:CAN_IT_RX_FIFO0_MSG_PENDING
触发条件 :FIFOx 里有 ≥1 条报文 等待读取,每当你配置的过滤器匹配成功,一帧报文被写入 FIFOx 后,如果 FIFO 非空,就会触发这个中断。
读走一帧后,如果 FIFO 里还有帧,这个中断不会自动消失,硬件会再次触发,直到 FIFO 空。
2:CAN_IT_RX_FIFOx_FULL
触发条件:bxCAN 的 FIFO 深度是 3 级,当第 3 帧写进来、FIFO 变成"满"的那一瞬间触发一次。
FIFO 满后,新来的帧仍然会被硬件丢掉(除非你开了锁定模式),但 FULL 中断只触发一次。
读走至少一帧后,FIFO 不再满,下次再满才会再次触发。
3:CAN_IT_RX_FIFO0_OVERRUN
触发条件:FIFOx 发生数据覆盖(溢出),FIFO 已经满了(3 帧),第四帧来,且发生覆盖
默认情况下(非锁定模式),新帧会覆盖 FIFO 里最老的一帧,覆盖发生的那一瞬间,触发 OVERRUN 中断。
和 FULL 不同:FULL 是"快满了",OVERRUN 是"已经丢了"。
6条各三条对应FIFO0和FIFO1
FIFO 中断三选一对比表(在第六节末尾补):
| 中断源 | 触发条件 | 适用场景 |
|--------|---------|----------|
| MSG_PENDING | FIFO 有 ≥1 帧新报文 | 正常接收,来一帧取一帧(你用的) |
| FULL | FIFO 攒满 3 帧 | 批量处理,但第一帧已经等了很久 |
| OVERRUN | FIFO 满时又来新帧,旧帧被覆盖 | 错误处理,数据已经丢了 |
五:双机实验
1:改了三样东西
| 改动 | 回环实验 | 双机实验 | 性质 |
|---|---|---|---|
| Mode | CAN_MODE_LOOPBACK |
CAN_MODE_NORMAL |
⭐ 核心,唯一必须改 |
| AutoRetransmission | DISABLE |
ENABLE |
次要(双机更稳) |
| 注释/打印文字 | "回环测试/回环收到" | "双机通讯/CAN 收到" | 装饰(不影响功能) |
2:所以准确说
-
核心就是改模式 ------
LOOPBACK→NORMAL,这是"回环 vs 双机"的本质区别 -
我还顺手改了一个
AutoRetransmission(禁止自动重发 → 自动重发),原因是:-
回环:自己发自己收,一定成功,禁止重发无所谓
-
双机:发到总线上,可能仲裁失败/冲突 ,开自动重发(ENABLE)能自动重试,更稳(野火双机也是这么配的)
-
-
剩下全是注释和打印文字,从"回环"改成"双机通讯",跟功能没关系
3:一句话总结
功能上:改了「模式」+「自动重发」两处;其余(引脚、波特率、过滤器、报文 0x1314、中断)一个没动。
所以你的直觉"就是模式变了"------对,核心就是那一行 CAN_MODE_NORMAL,其他只是为了让双机更稳、让打印文字对上号的小调整 😎