ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32 CAN总线通信入门:从物理层原理到bxCAN配置实战

STM32 CAN总线通信入门:从物理层原理到bxCAN配置实战

我在帮一个同学调一套电机驱动板时,第一次正儿八经接触 CAN 总线。板子上主控是 STM32F103,驱动板上留了一组 CANH、CANL 接口。我当时想当然地以为它像串口一样,把 TX 接 CANH、RX 接 CANL 就能收发,结果数据全乱,通信完全不可用。后来才发现,CAN 和串口根本不是一回事——串口解决的是“两个设备之间怎么传字节”,CAN 解决的是“几十个设备之间怎么稳定地抢总线并保证高优先级消息不丢”。这个认知差,就是很多人学 CAN 卡住的根本原因。

这篇文章我按自己从入门到项目落地的路径来写:先讲 CAN 到底解决了什么问题,再把物理层和协议层一次性拆清楚,最后落到 STM32 的 bxCAN 外设上,给出一套能直接抄作业的 CubeMX 配置和 HAL 代码骨架,同时把我在多节点联调里踩过的坑全部列出来。适合正在学 STM32、准备接电机驱动、传感器模块、电池管理或者工业设备的开发者。不管你有没有接触过 CAN,看完应该都能上手。

1. CAN 总线到底解决了什么问题:从汽车线束到多主通信

CAN(Controller Area Network)本质是一根两根线串联所有节点的现场总线。它最早是为汽车电子设计的,目的非常朴素:车上的控制器太多了,发动机、变速箱、ABS、仪表盘、车窗,每个控制器都要互相传数据。如果全部点对点连接,线束会多到整车布线根本塞不下,而且重量和成本也完全不可接受。

所以 CAN 采取了一个极其聪明的思路:把所有的控制器挂在同一条总线上,就像一群人围在一个圆桌边开会,谁都能发言,但发言内容所有人都能听到。每个节点只关心自己感兴趣的消息,不需要建立专用通路。这样整车的线束从“蜘蛛网”直接简化成两条线,而且天然支持广播和多主通信。

1.1 从点对点通信到多主竞争

传统的串口 UART 是典型的点对点通信,一个 TX 只能对应一个 RX。I2C 和 SPI 虽然能挂多个设备,但必须由一个主机来调度,从机自己不能主动发起通信。这在工业现场和汽车里是很要命的场景——假设一个传感器突然检测到异常,它希望立刻上报,如果它是 I2C 从机,它只能等主机来轮询。假如主机刚好在处理别的任务,等轮询到它的时候,可能已经晚了。

CAN 去掉了“主机”的概念。任何节点只要检测到总线空闲,都可以主动发送报文。如果两个节点同时在发,总线会进入仲裁机制,按照报文 ID 的优先级决定谁能继续发,低优先级的节点自动退让。也就是说,CAN 是一个真正的多主总线,节点之间是平等关系,谁有紧急数据谁就能抢到总线。

这个能力意味着:你可以随时往总线上加一个新节点,不需要重新配置主机的调度逻辑,也不需要修改其它节点的代码,只要新节点不跟别人抢同一个 ID,整个网络就能正常工作。这在做扩展性要求高的项目时非常舒服。

1.2 和 UART、I2C、SPI、RS-485 放一起比

很多刚入门的人都会问一句:为什么不用现成的串口或者 I2C?我把它们放在一张表里对比,大家就清楚了。

对比项UARTI2CSPIRS-485CAN
连接方式点对点总线一主多从总线总线
多主能力不支持不支持不支持半支持原生支持
最高速率常见 115.2k 起步,可更高400k/1M几十 Mbps10M 左右1M(CAN FD 到更高)
传输距离板级,几米内板级,一般不超过 1 米板级,短距离可达千米级百米级,速率越低越远
仲裁与优先级无无无无有
错误处理极弱弱弱较弱CRC+ACK+错误状态机

看这张表你会发现,I2C 和 SPI 天生是给板内通信设计的,距离短、没有多节点竞争;RS-485 解决了距离和多点问题,但 RS-485 本身只是物理层标准,协议得自己写,多节点同时发送要靠上层软件来避让。CAN 则是把物理层 + 数据链路层全部做完了,甚至仲裁都由硬件完成,对开发者非常友好。所以只要是“多个设备跨板互联、距离超过几米、又要求通信可靠”的场景,CAN 几乎都是首选。

1.3 你在什么场景下会被迫用上 CAN

我自己的体会是,大多数嵌入式开发者接触 CAN,并不是因为课程要求,而是因为项目里接了一个 CAN 接口的设备。常见的几类:无人机飞控连接电调和数传,机器人关节里面的电机驱动,汽车后装仪表读取整车数据,AGV 小车底盘与主控通信,BMS 电池管理板内部各模块互联,工业现场的传感器和执行器。这些设备选择 CAN,是因为 CAN 的实时性和抗干扰能力在工业环境里非常稳定,而且协议简单,适合做轻量级数据交互。

我的建议是:如果你只是做板级小作品,I2C 和 SPI 完全够用,没必要非得用 CAN;但如果你的设备需要长距离通信、多节点互联、或者以后有扩展到更多节点的可能,那直接上 CAN 是稳妥的。接下来我们把它拆开看,CAN 能稳定通信靠的到底是什么。

2. 把 CAN 协议拆开看:物理层、帧结构与总线仲裁

CAN 给人难学的感觉,很大程度上因为它不是一个单纯的“协议”,而是物理层和数据链路层打包在一起的解决方案。这一节我尽量用大白话把底层逻辑讲清楚,不堆术语,但关键的位场结构必须看懂,因为后面 STM32 配置和调试都会用到这些概念。

2.1 物理层:差分信号、显性电平、隐性电平与终端电阻

CAN 的物理层由两条线组成:CAN_H 和 CAN_L。它采用差分信号传输,也就是说,逻辑值不是靠单根线的绝对电压表达,而是靠两根线之间的电压差来表达。这一点和 USB、RS-485 的思路一样,好处是对共模干扰不敏感,抗噪声能力特别强——尤其适合电机会产生强干扰的场合。

CAN 定义了两种总线状态:

  • 显性电平(Dominant):对应逻辑 0。此时 CAN_H 被拉高、CAN_L 被拉低,两根线之间存在一个明显的电压差。显性电平具有“覆盖”能力。
  • 隐性电平(Recessive):对应逻辑 1。此时两根线都处于一个近似等电位的高阻状态,电压差接近 0。隐性电平是“让位”状态。

最直观的记忆方式:显性是主动拉线,隐性是放手。如果总线上同时有节点输出显性、有节点输出隐性,最终表现是显性,就像一条电线,一个人用手拉低,另一个人不碰,结果是拉低。这个特性是整个总线仲裁机制的基础,后面会展开。

终端电阻也是 CAN 物理层里绕不开的坑。标准做法是在总线物理两端各接一个 120 欧姆电阻,目的是吸收信号反射。两端的 120 欧并联后,从总线上任意一点往里看,阻抗大约 60 欧姆。如果你量到 CANH 和 CANL 之间的电阻接近 60 欧姆,说明终端电阻是正常的;如果量到 120 欧姆,说明只在一端有电阻;如果是开路,说明两端都没有。这个测量方法在排查总线故障时极其实用,后面章节会再提到。

2.2 一帧报文的数据结构:从 SOF 到 EOF

CAN 报文分为标准帧和扩展帧。标准帧使用 11 位 ID,扩展帧使用 29 位 ID。STM32 的 bxCAN 两种都支持,但项目中最常用的还是标准帧。我们把标准帧拆开看,它就像一个“广播信封”:

字段长度作用
SOF1 bit起始位,固定为显性,标志一帧开始
ID11 bit报文标识符,同时用于仲裁
RTR1 bit远程请求标志,数据帧为显性 0
IDE1 bit标准帧/扩展帧标志,标准帧显性 0
DLC4 bit数据长度,0 到 8 字节
DATA0~8 字节实际负载数据
CRC15 bit循环冗余校验,检测传输错误
ACK2 bit应答:发送方发隐性,任意接收节点拉显性表示收到
EOF7 bit帧结束,全部隐性

这个结构里,ACK 位很有意思。它是一个“匿名确认”机制:发送方在 ACK slot 输出隐性电平,任何收到正确帧的节点都可以在这时候把总线拉成显性,表示“我收到了”。发送方发现 ACK 位不再是最初的隐性,就知道总线上至少有别的节点成功收下了这帧数据。如果一帧报文发出去没人应答,发送方就会判定出错并触发重传机制。这是 CAN 可靠性的第一道保障。

实际应用里,我们关心的是 ID、DLC 和 DATA 三个字段。ID 决定这帧数据的身份,比如 0x123 代表“电机转速指令”,DLC 告诉接收方这帧带了几字节数据。接收方主要靠硬件过滤器和软件解析来筛选自己关心的报文,其余的一律丢。

2.3 总线仲裁:为什么 CAN 多节点同时发送不冲突

在搞清楚仲裁之前,要先理解 CAN 的电平覆盖关系:显性永远压过隐性。如果节点 A 和节点 B 同时开始发送报文,它们都会从高位往低位逐位输出自己的 ID。由于接收方每发出一个位的同时也在监视总线电平,当某个位出现“自己发隐性、但总线上是显性”的情况,这个节点立刻知道自己仲裁失败,马上停止发送,转入接收状态。

举例来说:节点 A 要发 ID 0x100,节点 B 要发 ID 0x200。在仲裁过程中,两者从最高位开始逐位比较。由于 0x100 最高位是 0,0x200 最高位是 1(这里只为说明方便,实际要看二进制展开),0x100 先出现显性电平,0x200 的节点检测到总线电平与自己输出不一致,马上退出。最终 0x100 顺利发完整帧。

从这个机制可以推出一个结论:CAN 报文 ID 越小,优先级越高。因为 ID 数值越小,它在高位越早出现显性 0,越容易在仲裁中胜出。所以做项目时,关键的控制指令应该用较小的 ID,周期性低速数据可以用较大的 ID。这个原则在 CAN 的工程应用里几乎是铁律。

仲裁过程中,高优先级节点发送完全不受干扰,甚至感受不到仲裁发生过。这是 CAN 和以太网 CSMA/CD 的本质区别:以太网冲突后要退避重发,浪费时间;CAN 在冲突瞬间就完成了竞争,胜者继续发送,代价为零。所以 CAN 在实时控制场景里特别合适,保证确定性延迟。

2.4 帧过滤与常见帧类型

和多主总线配套的,是接收过滤机制。每个节点通常不需要处理总线上所有报文,所以 CAN 控制器硬件会提供滤波器。以 STM32 bxCAN 为例,你可以配置滤波器只放行某些 ID,其余的硬件直接丢弃,完全不进 CPU。这是一个非常高效的“减负”手段,也是 STM32 上大家经常配置错误的地方,后面我会专门讲。

常见的帧类型,除了我们上面讲的数据帧,还有遥控帧。遥控帧没有数据区,它只有一个 ID,作用是让某个节点主动请求其它节点发送数据。实际项目里遥控帧用得不多,大部分场景是各节点周期性主动上报数据,或者用单次数据帧做控制指令。另一样需要知道的,是 CAN FD 帧。它是 CAN 2.0 的扩展,最多携带 64 字节数据,并且数据段可以用更高的波特率传输。STM32F1 系列不支持 CAN FD,只有部分新系列支持,如果选型时确定以后要用 FD,需要专门确认 MCU 型号。

3. STM32 的 bxCAN 外设:从 CubeMX 配置到最小收发例程

理论部分到这里已经足够支撑实战了。这一节直接进入 STM32 的开发流程。我用的是 STM32CubeMX + HAL 库,代码量小,配置透明,对新手最友好。如果你想直接操作寄存器,理解了这一节的核心过程之后再去读参考手册,也会顺利很多。

3.1 bxCAN 外设的结构和关键资源

STM32 的 bxCAN 本质上是一套完整的 CAN 控制器,只需要外界接一个 CAN 收发器芯片,就能把电平转换成 CAN_H 和 CAN_L 的差分信号。bxCAN 内部有几个关键资源:

  • 两个 CAN 外设:CAN1 和 CAN2。CAN1 是主外设,CAN2 是从外设,两者共享一部分内存和过滤器组。STM32F1 系列里,CAN2 若要工作,必须先使能 CAN1,而且 CAN2 只能用挂在 CAN1 下的滤波器。这个设计非常隐蔽,很多人踩坑,后面细说。
  • 3 个发送邮箱:每个邮箱可以缓存一帧待发送报文,硬件会自动按优先级发送。邮箱状态可以通过 HAL 函数查询,满了就是发送失败。
  • 2 个接收 FIFO:FIFO0 和 FIFO1。接收到的报文先经过过滤器,再进入 FIFO,CPU 需要及时读取,否则 FIFO 满后新报文会直接丢弃。
  • 28 个滤波器组(不同型号数量不同):每组可以配置成屏蔽位模式或列表模式,用来决定哪些报文能进 FIFO。

理解了这几个资源,后面的代码就顺理成章。配置的本质就三件事:初始化波特率和模式,配置滤波器,然后在中断或轮询里收发。

3.2 用 CubeMX 配置 CAN 参数:波特率、采样点和工作模式

打开 CubeMX,在 Pinout 里找到 CAN1,勾选 RX 和 TX 引脚。默认情况下 CAN1 的引脚通常在 PA11(RX)和 PA12(TX),具体以你板子为准。接着在 Configuration 里打开 CAN1 的配置界面,需要设置几个关键参数:

  • 工作模式:Normal 用于真实总线通信;Loopback 用于单板自测,发送的数据自己接收;Silent 模式只接收不发送,可以配合 Loopback 使用。入门调试建议先用 Loopback 模式验证收发链路,再接双板。
  • 波特率:CAN1 挂载在 APB1 总线上。你需要先确认时钟树里 APB1 的时钟是多少 MHz,然后通过 Prescaler、BS1、BS2 三个参数组合出目标波特率。
  • 采样点:这是很多人忽略的。CAN 信号的采样点在每个位时间里的位置,推荐落在 80%~87.5% 之间。采样点太靠前,抗干扰能力差;太靠后,容错余量不足。

举个例子,我习惯把 APB1 配成 42MHz,然后设置 Prescaler=4,BS1=16 TQ,BS2=4 TQ。这样总时间量子是 4 * (1 + 16 + 4) = 84,42MHz 除以 84 正好是 500kbps。采样点位置是 (1 + 16) / (1 + 16 + 4) = 85.7%,落在推荐区间。

CubeMX 的时间段配置界面里,你可以直接看到波特率和采样点的计算值。我实测下来,CubeMX 的自动计算是可靠的,但前提是 APB1 时钟一定得确认清楚。有次我在项目里为了跑 USB,把 APB1 从 42MHz 改成了 36MHz,只改了时钟树没有同步调整 CAN 的分频参数,结果双方波特率差了几十 k,现象是偶尔能通、大部分时间报错。从那以后,我每次配置 CAN 都会先看时钟树,把 APB1 的最终频率写进计算器里算一遍,而不是只看 CubeMX 推荐的 Prescaler 下拉框。

工作模式里还有一个常见误区:新手拿到板子先拿 Loopback 模式测,发现收发自如,就以为硬件没问题。换成 Normal 模式接双板后突然不通,就开始怀疑代码。大概率不是代码问题,而是物理接线、终端电阻或者收发器供电问题。所以正式联调前,务必把 Loopback 和 Normal 分开测试,并且明白它们在验证不同层级。

3.3 滤波器配置:屏蔽位模式和列表模式

滤波器是 STM32 bxCAN 配置里概念最绕、但功能极其重要的部分。每个滤波器组有两种典型使用方式:

  • 列表模式:配置一组精确的 ID,只有这些 ID 能通过,类似门禁名单。
  • 屏蔽位模式:配置一个 ID 和一个 Mask。Mask 的每一位表示“这一位要不要比较”。Mask 位为 1,则 ID 对应位必须匹配;Mask 位为 0,则 ID 对应位不关心,任意值都算通过。

刚入门最容易犯的错,是把 Mask 理解反。有人想只接收 ID 0x123,结果把 Mask 配置成了 0,含义从“所有位都不比较”变成了“所有报文都能通过”,于是滤波器形同虚设,总线上乱七八糟的报文全进 FIFO。正确做法是:要精确匹配一个标准帧 ID,ID 寄存器填 0x123,Mask 填 0x7FF。7FF 是标准帧 11 位 ID 的全 1 掩码,表示 11 位全部比较。

如果你希望接收一整个范围的 ID,比如 0x120 到 0x12F,可以把掩码的最低位设为 0,让低 4 位不参与比较,ID 寄存器填 0x120,Mask 填 0x7F0。这样 0x120 到 0x12F 都能通过。这个技巧在实际项目中非常有用,可以把节点按功能分组。

在 CubeMX 里,滤波器配置在 CAN 外设的 Filter Configurator 中。可以选择 Filter Mode 为 Mask 或 List,Filter Type 为 Data 或 Remote,Filter Bank 编号对应硬件滤波器组,FIFO Assignment 选择通过后的报文进入 FIFO0 还是 FIFO1。我通常把控制类报文进 FIFO0 并开中断,把日志类报文进 FIFO1 用轮询读取,避免低优先级日志把控制报文拖慢。

提示:如果只是快速验证通信,可以先不使用滤波器,或者配一个全部通过的滤波器(Mask=0)。但正式项目里一定要开启精确过滤,否则总线负载一高,CPU 会被无效中断淹没。

3.4 最小收发代码骨架:HAL 库直接抄

配置完成后,CubeMX 会生成初始化代码。接下来是两个最基本的操作:发送和接收。发送的核心是填充CAN_TxHeaderTypeDef,然后调用HAL_CAN_AddTxMessage,例如发送一个标准帧 ID 0x123,8 字节测试数据:

CAN_TxHeaderTypeDef tx_header; uint8_t tx_data[8] = {0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x00, 0x00, 0x00}; uint32_t tx_mailbox; tx_header.StdId = 0x123; // 标准帧 ID tx_header.IDE = CAN_ID_STD; // 使用标准帧 tx_header.RTR = CAN_RTR_DATA; // 数据帧 tx_header.DLC = 8; // 数据长度 8 字节 if (HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, &tx_mailbox) != HAL_OK) { // 发送失败,检查邮箱状态和总线错误 }

需要注意,HAL_CAN_AddTxMessage是把报文放到发送邮箱里,真正的发送由硬件完成。如果三个发送邮箱全满,这个函数会返回失败。所以不要在紧耦合的控制循环里无脑发送,必须结合总线状态判断。

接收有两种方式:轮询和中断。中断最常用。你需要在 main 函数里调用HAL_CAN_ActivateNotification使能 FIFO0 消息挂起中断,然后在中断回调函数里读取:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; uint8_t rx_data[8]; if (hcan->Instance == CAN1) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data); // 解析 rx_header.StdId 和 rx_data } }

这个回调函数是在中断上下文里执行的,所以千万不要在回调里面做耗时操作,比如打印长日志、浮点运算、延时。正确的做法是把数据拷到自己的缓冲区,设置一个标志位,然后回到主循环再处理。我在项目里一般直接在回调里放一个环形缓冲区,主循环从缓冲区取数据解析,这样既不会丢数据,也不会拖慢中断。

提示:如果同时使能了多个 CAN 或多个 FIFO,要注意回调函数名称。FIFO0 对应HAL_CAN_RxFifo0MsgPendingCallback,FIFO1 对应HAL_CAN_RxFifo1MsgPendingCallback,别把回调配错了。

4. 实测多节点联调中最常见的坑与排查思路

代码能编译、单板回环能收发,并不代表实际总线上就能跑。我在多节点联调里踩过的坑,很多都是“看起来像软件问题,实际是硬件问题”,或者反过来。以下是我认为最高频的五类问题,按照排查顺序写出来。

4.1 终端电阻缺失导致的可靠性问题

CAN 总线的终端电阻有时候很“傲娇”:短距离、低速率的总线上,哪怕完全不接终端电阻,通信也可能正常,因为波形反射强度在距离短时不足以造成位错误。但只要距离拉长、节点增多、波特率提高,反射问题就会被放大,表现为总线偶发错误、错误计数器不断累积,严重时甚至完全断连。

我遇到过一个很典型的案例:两个开发板用 30cm 杜邦线连接,跑 125kbps,一切正常;我把通信改成 500kbps,偶尔收到错误帧;再把线延长到 2 米,几乎完全不通。排查了半天,先查代码,再查滤波器和波特率,最后用万用表量 CANH 和 CANL 之间的电阻,发现开路——两端的收发器板上根本没有焊终端电阻。

解决办法很简单:在总线两端分别并入一个 120 欧姆电阻。如果是临时测试,直接在其中一个节点的 CANH 和 CANL 之间跨接一个 120 欧电阻也可以,但正规项目必须按菊花链拓扑,在物理最远的两端各接一个。用万用表量总线两端之间的电阻,应该读到约 60 欧姆。如果读到 120 欧姆,说明只有一端有终端电阻;如果读到接近 0 欧姆,说明 CANH 和 CANL 之间短路了,优先检查线序和插头。

4.2 波特率参数不一致:报错但不明显

CAN 总线有一个很迷惑人的现象:波特率不匹配时,不一定完全静默,而是会出现“偶尔收到一帧成功、大部分时间报错”。原因是某些位序列恰好满足接收方的采样窗口,错误还没被检测出来,但更多情况下会在 CRC、ACK 或 bit stuffing 阶段被识破,表现为错误帧。如果你用 PCAN 之类的工具抓总线,能看到大量红色错误帧,那就该怀疑波特率了。

排查时先确认两边的目标波特率一致,再核对实际配置。STM32 的波特率由 APB1 时钟和分频参数决定,所以哪怕你在 CubeMX 里选了同一个波特率,只要两边的 APB1 时钟不同,实际波特率也不同。我建议每个节点都打印或者读取一下hcan1.Init里的参数,自己算一遍实际波特率,不要凭感觉。还有一种“差一点”的情况:两边名义波特率都是 500k,但因为分频组合不同导致采样点偏差很大,比如一边采样点在 70%,另一边在 90%。通信短距离能通,长距离或者干扰强一点就开始报错。采样点不一致的问题很难肉眼发现,建议统一把采样点配置在 80%~87.5% 区间。

4.3 滤波器配置错误:硬件把消息悄悄丢了

滤波器是另一种“查不出代码问题”的重灾区。如果你的接收中断一直没有触发,但发送端确认已经在发送,第一步应该检查的就是滤波器。常见错误有三种:

  1. 滤波器全部禁用:CubeMX 中如果没有配置滤波器,有些库版本默认是不接收任何报文的,你需要显式使能至少一个滤波器组。这会让新手怀疑自己代码写错,实际只是滤波配置缺失。
  2. Mask 配置不对:想精确接收某个 ID,Mask 却填了 0,导致所有报文都通过;或者反过来,把 Mask 填成 0xFFFFFFFF 这样的值,结果任何报文都进不来。标准帧精确匹配应该是 ID=0x123,Mask=0x7FF。
  3. 报文进入了 FIFO0 还是 FIFO1 搞混:滤波器里可以选择把报文分配到 FIFO0 或 FIFO1。你中断使能了 FIFO0,但滤波器把报文分配到了 FIFO1,自然永远不会触发 FIFO0 的回调。这种错误只需要把分配关系改成一致就行。

排查滤波器问题时,我一般会先临时配置一个 Mask=0 的全通滤波器,确认通信链路本身是通的,然后再逐步收紧过滤条件。这样可以区分“链路问题”和“过滤问题”。

4.4 CAN2 的隐藏规则:必须借道 CAN1 和共享滤波器

如果你用的是 STM32F1 这类双 CAN 型号,容易踩一个隐藏规则:CAN2 不是独立运行的,必须同时使能 CAN1。原因是 CAN2 的接收路径依赖 CAN1 的滤波器资源。即使你的业务逻辑只用 CAN2,初始化时也要先初始化 CAN1,并且在 CAN1 的滤波器里分配至少一个滤波器组给 CAN2 使用。CubeMX 的图形化配置里,这一步很容易被忽略,导致 CAN2 的发送正常,但接收永远无反应。

处理方式:在 CubeMX 中同时使能 CAN1 和 CAN2。CAN1 可以不接发送数据,但必须确保 CAN1 时钟使能、CAN1 初始化成功。然后在 CAN1 的滤波器配置界面里,把一个 Filter Bank 的 FIFO Assignment 指向 CAN2,这样 CAN2 收到的报文才能进入接收 FIFO。这属于 STM32 bxCAN 的架构性设计,参考手册里写得很隐晦,实战中我见过不止一个项目卡在这个地方。

4.5 回环模式正常、正常模式不通时的优先级排查

如果你已经做了双板测试但通信失败,建议按照下面的优先级排查,而不是重新翻代码:

  1. 用万用表检查 CANH 和 CANL 是否接反、是否短路。
  2. 检查所有节点的 GND 是否共地。CAN 收发器差分信号虽然抗共模干扰,但收发器本身工作的参考地必须一致。跨电源供电时,最好让两个板子的 GND 连在一起,否则极大的共模电压会直接损坏收发器。
  3. 确认收发器芯片供电正常。很多 CAN 收发器有 STBY 引脚或 RS 引脚,必须拉成正确电平才能进入工作模式。某些带 5V 和 3.3V 电平转换的模块,跳线帽错了也会导致通信失败。
  4. 确认终端电阻正确后,再抓总线波形。如果有示波器,可以直接看 CAN_TX 引脚上是否有波形,确认 MCU 正在发送;再看 CAN_H 和 CAN_L 之间的差分信号振幅是否符合收发器规格。
  5. 最后再检查代码里的波特率、滤波器和回调配置。

顺序很重要。我见过有人花了两天查代码,最后发现是收发器的 STBY 引脚悬空,收发器一直在待机模式。硬件问题优先排除,能省下大量时间。

5. 工程化选型建议与硬件布局心得

如果你已经跑通了双板通信,接下来的问题通常就是:怎么正式做项目?这里分享一些我在工程实践中的选型和硬件布局经验。

5.1 收发器芯片怎么选

STM32 的 bxCAN 输出的是 TX/RX 数字信号,不是差分信号,所以必须外接 CAN 收发器。选型时有几个关键点:

  • 供电电压:早期常见的收发器是 5V 供电,比如 TJA1050 这类。它直接出来的 CAN_H/CAN_L 电平是 5V 体系。而 STM32 的 TX/RX 是 3.3V,两者之间的逻辑电平匹配一般没问题,但信号电平上要注意共地。另一种方案是 3.3V 供电的收发器,比如 SN65HVD230 系列,它和 STM32 同电压体系,接线更直接,适合电池供电或低功耗产品。
  • 待机控制:很多收发器有 STBY 或 S 引脚,用于进入低功耗待机模式。这个引脚不能悬空,必须按手册要求接上拉或下拉,否则收发器可能一直在待机,通信完全不通。
  • 防护等级:工业现场有浪涌和静电风险,建议选内置 ESD 保护的型号,并在总线入口加 TVS 管和共模电感。汽车级应用还要考虑收发器的 EMI 特性。

如果你做的是小批量产品,直接买集成了收发器和保护电路的 CAN 模块即可;但量产时,还是单独设计收发器电路更可控。

5.2 波特率与总线长度的经验对照

CAN 总线的传输距离和速率不是无关的。速率越高,允许的物理距离越短,因为位时间越短,信号在长线缆上的反射和衰减越容易破坏位同步。下面是一组工程上常用的经验值:

波特率参考总线长度
1 Mbps约 40 米
500 kbps约 130 米
250 kbps约 270 米
125 kbps约 530 米
50 kbps约 1300 米

这个表格只做参考,线径、绞合、接头质量和节点数量都会影响实际效果。但我建议选型时遵循一个原则:先确定最远节点间的物理距离,再反推最大可用波特率。如果距离预算宽裕,尽量选低一档波特率,可靠性能大幅提升。

此外,总线拓扑建议采用菊花链,即每个节点从上一个节点引线到下一个节点,避免在某个节点引出很长的分支线。CAN 标准对支线长度有严格要求,支线过长会破坏信号完整性。如果现场必须星型连接,应该在中心点使用 CAN 集线器,而不是直接把线拧在一起。

5.3 代码封装、ID 规划与后续扩展

工程化不只是硬件,代码层面同样有讲究。我自己的做法是,把 CAN 操作封装成一个独立模块,提供几个基础接口:初始化和滤波器配置、发送接口、接收回调注册、总线错误处理。业务代码不直接操作 HAL 层的CAN_HandleTypeDef,全部通过模块接口调用。这样换 MCU 或换库的时候,只需要改底层适配,业务逻辑基本不动。

ID 规划也值得提前做。我习惯把 ID 按功能段划分:比如 0x000~0x07F 用于系统的控制指令,0x100~0x1FF 用于电机反馈,0x200~0x2FF 用于传感器数据,0x300~0x3FF 用于调试和日志。每个节点只能发送自己权限范围内的 ID,接收则通过滤波器精确匹配。这样当总线上有新节点加入时,不需要全局修改其它节点的代码,只在新节点里配置输入的 ID 规则即可。周期型数据和事件型数据也要分开:周期数据用定时器发送,事件数据用中断或任务触发,避免所有节点都在同一时刻抢总线造成抖动。

如果你在产品规划阶段就知道未来的数据量会增长,比如单帧 8 字节不够用、总线负载率超过 40%,那么在 MCU 选型时就要考虑支持 CAN FD 的型号。CAN FD 能把单帧数据扩展到 64 字节,数据段还能切换到更高波特率,总线利用率提升非常明显。STM32F1 全系不支持 CAN FD,如果你刚开始选型,建议直接把 F4 或 G4 系列纳入考虑,省得后期换平台。

从最初以为 CAN 是“高级串口”,到后来能在总线上同时挂十几个节点稳定跑几个月,我的最大体会是:CAN 真正的难度不在协议栈,而在于物理层细节和多节点协作。先把终端电阻、波特率、滤波器这三件事吃透,你的 CAN 项目基本就稳了一大半。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进