F429-HAL-Can总线(回环及双机实验)(2026/8/15)

目录

[一:为什么需要CAN总线 **Controller Area Network**](#一:为什么需要CAN总线 Controller Area Network)

[1:为什么需要 CAN?](#1:为什么需要 CAN?)

2:CAN解决了什么问题

3:一些基本规则

4:CAN几个不同协议

5:CAN的来源

二:CAN(A)一帧完整数据长这个样:

[1:SOF:Start Of Frame,帧起始](#1:SOF:Start Of Frame,帧起始)

2:ID:Identifier,标识符

作用1:标识消息

作用2:决定优先级

[3:RTR (1 bit):Remote Transmission Request要求帧](#3:RTR (1 bit):Remote Transmission Request要求帧)

[4:IDE:Identifier Extension标识符扩展位](#4:IDE:Identifier Extension标识符扩展位)

5:r0保留位

[6:DLC(4bit):Data Length Code数据长度码](#6:DLC(4bit):Data Length Code数据长度码)

7:DATA(D0~D7):真正的数据

[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 帧结束)

三:CAN总线硬件

1:电平转换

2:节点

3:跳线帽接线

四:CAN回环实验1

1:CAN_MODE_LOOPBACK

2:1Mbps怎么得到的

3:CAN初始化字段

1:TimeTriggeredMode:时间触发通信模式

2:AutoBusOff:总线关闭

3:AutoWakeUp:自动唤醒

4:AutoRetransmission:自动重发

[5:ReceiveFifoLocked:FIFO 满了以后,锁住,不再接收新的消息](#5:ReceiveFifoLocked:FIFO 满了以后,锁住,不再接收新的消息)

[6:TransmitFifoPriority:按 ID 优先级发](#6:TransmitFifoPriority:按 ID 优先级发)

[7:Mode:CAN 工作在哪种模式](#7:Mode:CAN 工作在哪种模式)

8:SyncJumpWidth:同步bit发送

9:TimeSeg1:等待采样数据平稳

9:TimeSeg2:时序修正缓冲段

10:SJW:最多允许调整多少

[11:Prescaler: 时钟分频](#11:Prescaler: 时钟分频)

[4:CAN_SetMsg --- 装一帧扩展帧](#4:CAN_SetMsg — 装一帧扩展帧)

1:TxHeader.StdId

2:TxHeader.ExtId

3:TxHeader.IDE

4:TxHeader.RTR

5;TxHeader.DLC

6:TxHeader.TransmitGlobalTime

[5:CAN_Filter_Config硬件过滤器(CAN Filter)](#5:CAN_Filter_Config硬件过滤器(CAN Filter))

1:FilterBank:过滤器

2:FilterMode:掩码模式和列表模式

3:FilterScale:32位过滤还是16位过滤

4:SlaveStartFilterBank:分配过滤器

5:构建32位bit

6:FIFO中断触发

1:CAN_IT_RX_FIFO0_MSG_PENDING

2:CAN_IT_RX_FIFOx_FULL

3:CAN_IT_RX_FIFO0_OVERRUN

五:双机实验

1:改了三样东西

2:所以准确说

3:一句话总结


一:为什么需要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:所以准确说

  1. 核心就是改模式 ------ LOOPBACKNORMAL,这是"回环 vs 双机"的本质区别

  2. 我还顺手改了一个 AutoRetransmission(禁止自动重发 → 自动重发),原因是:

    • 回环:自己发自己收,一定成功,禁止重发无所谓

    • 双机:发到总线上,可能仲裁失败/冲突 ,开自动重发(ENABLE)能自动重试,更稳(野火双机也是这么配的)

  3. 剩下全是注释和打印文字,从"回环"改成"双机通讯",跟功能没关系

3:一句话总结

功能上:改了「模式」+「自动重发」两处;其余(引脚、波特率、过滤器、报文 0x1314、中断)一个没动。

所以你的直觉"就是模式变了"------对,核心就是那一行 CAN_MODE_NORMAL,其他只是为了让双机更稳、让打印文字对上号的小调整 😎

相关推荐
caimouse2 小时前
ReactOS 图形系统分析(38):区域子系统 — region.c
c语言
嵌入式阿蔡2 小时前
STM32时钟树详解:配错一个全乱套
c语言·stm32·单片机·嵌入式硬件
caimouse2 小时前
ReactOS 图形系统分析(37):路径子系统 — path.c
c语言·reactos
CarIise3 小时前
C语言基础课程笔记:硬件原理、数据类型、结构体与格式化输入输出详解
c语言·开发语言·笔记
GeekZHR3 小时前
C语言指针2:数组名、二级指针、指针数组,一次把“指针和数组“讲透
c语言·数据结构·算法·指针
zlinear数据采集卡3 小时前
数据采集卡从入门到精通(7):流水线型ADC——级级接力,高速与高精的平衡术
开发语言·arm开发·嵌入式硬件·fpga开发·c#
Dr.kangder3 小时前
嵌入式面试总结(二十一)——C语言关键字
c语言·开发语言·面试·职场和发展·架构·虚拟化
bobuddy3 小时前
CMW500信令测试关键配置要点
嵌入式硬件
花花鱼4 小时前
TinyML Agent:MCU 上的微型智能体,AI 智能体下沉的底层实现
人工智能·单片机·嵌入式硬件