前言

如果你做过几年嵌入式开发,迟早会撞到 CAN 总线这堵墙。它在汽车电子里几乎是默认标配,在工业控制、轨道交通、电梯、医疗设备、甚至是无人机机臂内部通信里,到处都有它的身影。我第一次和 CAN 打交道是在一个新能源 BMS 项目上——电池包要把每串单体电压、温度、SOC 上报给整车控制器(VCU),现场没有以太网,没有 RS485,PCB 上焊得整整齐齐就两根线:CAN_H 和 CAN_L。当时我天真地以为,无非就是另一个 UART,能收能发就完事。结果第一周就被仲裁机制、位时序、错误帧、Bus-Off 教育得明明白白。

写这篇文章的初衷,是想把这些年踩过的坑、读过的手册、调过的示波器波形,整理成一份能让后来人少走弯路的实战指南。我不会停留在「CAN 是一种串行通信总线」这种百度百科式的介绍,而是从协议本质讲起,一路下沉到 STM32 的 bxCAN 外设寄存器层,再上升到工程代码里那些「为什么我的滤波器配了却收不到包」的具体问题。

CAN 总线(Controller Area Network)由德国博世公司在 1986 年推出,最初的目标是给汽车上日益增多的电子控制单元(ECU)之间提供一条可靠、低成本的通信骨干。在 CAN 出现之前,每两个 ECU 之间都要拉一根专线,一辆中高端车里光线束就能有上百公斤重。CAN 把所有节点挂在同一条双绞线上,靠协议本身去解决冲突和优先级问题,线束重量瞬间砍掉一大半。这个设计哲学在今天看来依然非常先进——它把网络的复杂性下沉到了协议层,让物理层简单到极致。

放到 2026 年的视角,CAN 当然不算「新技术」。CAN FD(Flexible Data-rate)已经成为汽车主流,传输速率突破 5 Mbps,数据载荷扩展到 64 字节;CAN XL 已经在路上,可以飙到 10 Mbps、2048 字节负载;车载以太网(Automotive Ethernet)也在逐步蚕食骨干网的份额。但即便如此,传统 CAN 2.0B 在嵌入式工程里依然不可替代。原因很简单:它便宜、皮实、抗干扰能力恐怖、协议成熟到几乎不会出 bug,而且几乎每一颗工业级 MCU 都自带 CAN 控制器。

一、为什么是 CAN?四个无法替代的设计优势

要理解 CAN 为什么能屹立不倒近四十年,必须先看清它的核心设计。我把它总结成四点:

第一,多主架构(Multi-master)。 总线上没有「主机」和「从机」的区分,任何节点在总线空闲时都可以主动发起传输。这跟 SPI 那种「主机点名才能说话」的模式完全不同。在汽车里,发动机 ECU 和刹车 ECU 是平等的两个节点,谁需要谁就说。这种架构的好处是没有单点故障——主机挂了整条线就废这种事不会发生。

第二,非破坏性逐位仲裁(Non-destructive bitwise arbitration)。 这是 CAN 协议的精髓所在,也是最容易被新手忽略的点。当多个节点同时抢占总线时,CAN 通过比较报文 ID 的每一位来决定谁优先发送,而且——胜出的那一帧不会被损坏。技术上,CAN 总线物理层定义了「显性位」(Dominant,逻辑 0)和「隐性位」(Recessive,逻辑 1)。在线与(wired-AND)逻辑下,只要有一个节点输出 0,整条总线就是 0。每个节点在发送时会同时监听总线状态,一旦发现自己发送的是 1 但总线上是 0,立刻退出仲裁、转为接收方。ID 数值越小(高位 0 越多)的报文,优先级越高。

第三,差分信号传输。 CAN_H 和 CAN_L 是一对双绞线,传输的是差分电压。隐性状态下两根线电压都是 2.5V,差值为 0;显性状态下 CAN_H 升到 3.5V,CAN_L 降到 1.5V,差值 2V。这种差分方式对共模干扰具有天然的抑制能力,配合双绞线的扭绞,可以在强电磁环境下(比如电机控制柜里)稳定通信。这也是为什么工业现场宁可用 CAN,也不爱用单端的 UART。

第四,强大的错误检测与故障约束机制。 CAN 协议内置了五种错误检测:CRC 校验、ACK 应答、帧格式检查、位监听、位填充。任何一种错误被检测到,整条总线上的节点都会立即丢弃当前帧并进入错误处理。更关键的是 CAN 的「错误计数器」机制——每个节点都有 TEC(发送错误计数)和 REC(接收错误计数)寄存器,超过 127 进入被动错误状态,超过 255 进入 Bus-Off 状态自动脱离总线,避免一个坏节点把整条总线拖死。这个故障约束机制保证了系统级的鲁棒性。

CAN 总线拓扑结构

二、CAN 物理层:那些手册上不写的细节

讲完协议哲学,我们来看物理层。CAN 的物理层标准在 ISO 11898 里定义得很细,但工程实践中有几个点特别容易翻车:

终端电阻必须严格 120Ω,而且只能在总线两端各接一个。 我见过太多新手要么忘了接,要么在每个节点都接一个。终端电阻的作用是阻抗匹配,吸收信号反射。CAN 是分布参数的传输线,没有终端电阻,信号在线缆两端会发生反射,导致波形畸变、ACK 错误、误码率飙升。如果中间某个节点接了终端电阻,总线阻抗变成 60Ω 甚至 40Ω,差分电压幅度变小、收发器驱动能力不足,同样会出问题。

线缆长度与波特率成反比。 这是因为 CAN 的仲裁机制要求显性位的电平变化在一个位时间(Bit Time)内传遍整条总线,让所有节点都能采样到一致的电平。光在导线里传播大约 5ns/m,一个 1 Mbps 的位时间是 1μs,留给信号传播的余量大概只够 40 米。所以你会看到典型的对应关系:1 Mbps 对应 40 米、500 kbps 对应 100 米、250 kbps 对应 250 米、125 kbps 对应 500 米。超出这个范围就老老实实降速,别硬怼。

收发器(Transceiver)的选型很重要。 STM32 内部的 CAN 控制器输出的是 TTL 电平的 CAN_TX/CAN_RX 单端信号,必须外接收发器芯片转换成差分总线信号。汽车级常用 TJA1050、TJA1042,工业级用 SN65HVD230、MCP2551。要重点关注几个参数:通信速率(CAN FD 必须用 5Mbps 以上的收发器)、节点数量上限(一般 110 个左右)、待机功耗(电池供电场景)、隔离方式(高压环境用 ADM3053 这种带隔离的型号)。

三、CAN 数据帧:协议层的语法书

CAN 协议定义了四种帧:数据帧、远程帧、错误帧、过载帧。99% 的应用只用数据帧,剩下三种主要是协议层自动处理。数据帧又分两种格式:CAN 2.0A 的标准帧(11 位 ID)和 CAN 2.0B 的扩展帧(29 位 ID)。我们重点看标准数据帧。

CAN 2.0A 标准数据帧结构

一个完整的标准数据帧从 SOF(帧起始)的一个显性位开始,紧跟着 11 位 ID(仲裁段)、1 位 RTR(远程发送请求,数据帧为 0)、6 位控制段(包含 IDE 标识扩展位、保留位 r0、4 位 DLC 数据长度码)、0~64 位的数据段、15 位 CRC 加 1 位界定符、2 位 ACK 段、7 位 EOF 帧结束,最后是 3 位 IFS 帧间隔。算下来一个空帧 47 位、满载(8 字节)数据帧 111 位(不算位填充)。所以在 1 Mbps 速率下,理论最高每秒约 9000 帧,实际由于位填充和帧间隔会再低一些。

这里面**位填充(Bit Stuffing)**是个容易被忽视但非常重要的机制。CAN 没有独立的时钟线,发送方和接收方靠相邻位的跳变沿来重新同步时钟。如果连续出现 5 个相同的位,发送方会强行插入一个相反的位(接收方再自动去掉),保证总线上始终有足够的跳变。这个机制让 CAN 能在没有时钟线的情况下做到几乎 0 错位采样,但代价是同一份数据在总线上的实际长度是不固定的。在做总线带宽精确计算时要把位填充的开销算进去,一般按帧长的 20% 估算。

(第一部分完,约 3100 字)

四、STM32 bxCAN 外设:硬件结构剖析

STM32F1/F4/F7/H7 系列的 CAN 控制器叫做 bxCAN(Basic Extended CAN),全面支持 CAN 2.0A 和 2.0B 协议,最高速率 1 Mbps。STM32G4/H7 部分型号开始上 FDCAN,支持 CAN FD。我们先把 bxCAN 吃透,FDCAN 的差异主要在数据段长度和位时序,理解了 bxCAN 切过去问题不大。

bxCAN 内部最核心的三块东西:3 个发送邮箱(Tx Mailbox)2 个接收 FIFO(每个 3 级深度)14 个过滤器组(连接两路 CAN 的型号是 28 个,主从拆分使用)。

STM32 bxCAN 内部结构

发送邮箱是发送方向的核心。当应用层调用 HAL_CAN_AddTxMessage() 时,HAL 库会找一个空闲的 Tx Mailbox 把帧数据填进去,硬件自动负责后续的总线仲裁、CRC 计算、错误重传。三个邮箱之间默认按报文 ID 优先级排队发送(小 ID 先发),也可以通过 CAN_MCR_TXFP 位改成 FIFO 顺序发送。在高优先级周期帧(比如 10ms 的发动机扭矩)和低优先级事件帧(比如故障码上报)混合的场景里,三个邮箱设计得刚刚好——基本不会出现「关键帧排队等待非关键帧」的优先级反转。

接收 FIFO 是接收方向的核心。bxCAN 有两个独立的 FIFO(FIFO0 和 FIFO1),每个 3 级深度。一帧合法报文进来后,先过过滤器组,根据过滤器配置决定送进 FIFO0 还是 FIFO1。FIFO 满了之后默认会丢弃新到的报文并置 FOVR(FIFO Overrun)标志位,也可以配置成覆盖最旧的报文。在中断模式下,FIFO 非空触发 CAN_RX0/RX1 中断,应用层在中断里调用 HAL_CAN_GetRxMessage() 取出报文。

过滤器组是最容易让新手翻车的地方。它的作用是硬件层面提前过滤掉不感兴趣的报文,避免大量无关报文塞满 FIFO、把 CPU 拖死在中断里。在汽车 CAN 网络上每秒几千帧是常态,没有硬件过滤靠 CPU 软件判断根本扛不住。

五、过滤器组的工作原理:一次说清楚

过滤器组的配置有两个维度:位宽(16 位或 32 位)和模式(掩码模式 Mask Mode 或列表模式 List Mode)。两两组合就是四种工作模式:

  • 32 位 / 列表模式:一组可以精确匹配 2 个完整的 29 位扩展 ID
  • 32 位 / 掩码模式:一组可以匹配 1 个 ID + 1 个掩码(指定哪些位需要匹配)
  • 16 位 / 列表模式:一组可以精确匹配 4 个 11 位标准 ID
  • 16 位 / 掩码模式:一组可以匹配 2 组 ID + 掩码

掩码模式最常用,举个例子:你希望接收所有 ID 在 0x100~0x1FF 范围内的标准帧。配置 ID 为 0x100,掩码为 0x700(二进制 111 0000 0000),意思是「ID 的高 3 位必须等于 100 ,低 8 位可以是任意值」。这样硬件就只让 0x100~0x1FF 的报文进 FIFO。

列表模式适合接收的报文 ID 数量少且固定的场景,比如只接收 0x123、0x456、0x789 三个 ID,配置进列表里硬件就只放这三个进来。

实际工程里我的经验是:先用最宽松的掩码(ID=0, Mask=0,即接收所有报文)让通信跑通,再根据节点的报文表逐步收紧过滤器。 一上来就配精确过滤,调通信问题时根本不知道是没收到包还是被过滤掉了。

这里还有一个经典坑:HAL 库里 CAN_FilterTypeDef 结构体的 FilterIdHighFilterIdLow 字段是左移 5 位后的值!标准帧 11 位 ID 写在高 16 位的 [15:5]。比如你要过滤 ID = 0x123 的标准帧,要写 FilterIdHigh = 0x123 << 5 = 0x2460。这个坑我见过至少 5 个新手踩过,包括我自己第一次用 bxCAN 的时候。STM32CubeMX 也不会替你算,必须自己手动左移。

六、CAN 位时序:那个让人头大的 Tq

CAN 的波特率配置不是简单设个数字就完事,它涉及**位时序(Bit Timing)**的精细调整。一个位时间被划分成几段:

  • SyncSeg(同步段):固定 1 个 Tq(时间量子),用于跨节点同步
  • PropSeg(传播段):补偿物理传播延迟,1~8 Tq
  • PhaseSeg1(相位段 1):1~8 Tq
  • PhaseSeg2(相位段 2):1~8 Tq

STM32 把 PropSeg 和 PhaseSeg1 合并成一个 BS1(Time Segment 1),PhaseSeg2 叫 BS2。波特率的计算公式:

波特率 = CAN_Clock / (Prescaler × (1 + BS1 + BS2))

举个例子,STM32F4 的 APB1 时钟一般是 42 MHz,给 CAN 用。要配 500 kbps:

42 MHz / (Prescaler × (1 + BS1 + BS2)) = 500000
=> Prescaler × (1 + BS1 + BS2) = 84

一种选法是 Prescaler = 6, BS1 = 11 Tq, BS2 = 2 Tq,刚好等于 84,采样点在 (1+11)/14 ≈ 85.7%,落在推荐的 75%~87.5% 区间。采样点位置很关键,太靠前抗干扰差,太靠后留给同步调整的余量不够。汽车行业普遍推荐 75% 或 87.5%(CiA 标准)。

不想手动算?用在线工具 bittiming.can-wiki.info,输入 MCU 型号、时钟频率、目标波特率,直接出最优配置,比自己拍脑袋强。

七、CubeMX 初始化配置:一步一步来

打开 STM32CubeMX,选好芯片(以 STM32F407 为例):

  1. RCC 配置:HSE 接外部 8MHz 晶振,PLL 倍频到 168MHz(APB1 = 42MHz)
  2. 使能 CAN1:左侧 Connectivity → CAN1 → Activated 打勾,模式选 Master
  3. 引脚分配:CAN1_TX → PA12(或 PB9),CAN1_RX → PA11(或 PB8)。注意 PA12 和 USB_DP 复用,做 USB 设备时要避开
  4. 参数配置
    • Prescaler = 6
    • Time Quanta in Bit Segment 1 = 12 Times
    • Time Quanta in Bit Segment 2 = 2 Times
    • ReSynchronization Jump Width = 1 Time
    • Operating Mode = Normal(调试时可选 Loopback 自环测试)
    • Time Triggered Communication Mode = Disable
    • Automatic Bus-Off Management = Enable(重要!让 Bus-Off 后能自动恢复)
    • Automatic Wake-Up Mode = Enable
    • Automatic Retransmission = Enable
    • Receive Fifo Locked Mode = Disable
    • Transmit Fifo Priority = Disable
  5. NVIC 配置:使能 CAN1 RX0 interrupt(FIFO0 消息挂起中断)
  6. 生成代码

生成的 MX_CAN1_Init() 函数会包含所有参数。我们要做的事情通常是在 main() 里:

  • 配置过滤器组(HAL_CAN_ConfigFilter
  • 启动 CAN(HAL_CAN_Start
  • 使能 RX 中断(HAL_CAN_ActivateNotification
  • 在中断回调里取报文,在主循环里发报文

下一部分我们就把这套完整代码撸出来。

(第二部分完,约 2500 字)