CAN 收发器:MCU 从没碰过 CAN 那两根线(以 STM32F4 为例)
上一篇 CAN 总线 用环回模式把整个协议层跑通了,收发器被一句话打发:「真要接物理总线时,再加一颗 CAN 收发器(3.3V 的 SN65HVD230 或 5V 的 TJA1050)和一根双绞线」。这篇就来还这笔债——但先看一个会让你愣一下的事实。
环回跑通的那天,你把示波器挂上 PB9(CAN1_TX),想看看传说中的 CAN 长什么样。看到的是老朋友:0 和 3.3V 之间的方波,跟 串口篇 的 TX 没有任何区别。再挂到 CAN_H 上——环回模式下一片死寂,什么都看不见(环回在控制器内部拐弯,根本不出引脚)。等接上收发器、真正把帧发上总线,CAN_H 上倒是动起来了,但你逐位对表会发现:PB9 上的波形和 CAN_H 上的波形,逻辑相同,电平却是两个世界——一边是 0/3.3V 的单端逻辑,一边是围绕 2.5V 摆动的差分信号。
结论有点反直觉:MCU 里的 bxCAN 根本不会说 CAN 的物理语言。它一辈子只说一种很土的方言——「TXD 拉低 = 我想把总线拽成显性;松手 = 我松手」,然后把 RXD 读回来的东西当作全总线的裁决结果。差分、2.5V 偏置、线与、抗共模——这些真正定义了「CAN 长什么样」的东西,MCU 一根引脚都没碰过。它们全住在旁边那颗几块钱的八脚芯片里。
这篇文章分三层:L1 把收发器接上去、跑起来,看代码改动小到什么程度;L2 搞清两个方向的「翻译」各是怎么发生的——以及为什么说 RXD 不是「你发的」,是「全网说的」;L3 想明白为什么物理层非要住在 MCU 外面那颗单独的芯片里——答案和上一篇「把主持权下沉到物理层」是同一件事的另一半。
L1:先让它跑起来——八根脚、零个寄存器
你在第一层。目标:把那颗芯片接上去,让上一篇的代码跑在真总线上。
先摆正位置:CAN 系统里有两个角色。协议控制器(bxCAN,住在 MCU 里)负责帧格式、位时序、仲裁、错误账本这些「动脑」的事;收发器(transceiver,住在 MCU 外面)负责「动手」——把控制器的单端意图变成差分总线上的电流,再把总线上的合力变回单端电平。TJA1050 的数据手册第一句话就是这么自我介绍的:它是「CAN 协议控制器与物理总线之间的接口」。
TJA1050 的八根脚(数据手册):
| 引脚 | 名字 | 方向 | 干什么 |
|---|---|---|---|
| 1 | TXD | 输入 | 收控制器的「意图」:低 = 显性,高 = 隐性 |
| 2 | GND | 地 | |
| 3 | VCC | 电源(5V) | |
| 4 | RXD | 输出 | 总线现状:低 = 总线显性,高 = 总线隐性 |
| 5 | Vref | 输出 | VCC/2 参考电压(分裂终端用,可悬空) |
| 6 | CANL | I/O | 总线低线 |
| 7 | CANH | I/O | 总线高线 |
| 8 | S | 输入 | 接地 = 高速模式(推荐明确接地);接 VCC = 静音模式 |
第一眼就可能踩到反直觉:收发器的 TXD 是个输入引脚。TX 是「发送」的意思,怎么是输入?因为这个名字是站在总线系统的视角取的——TXD 线上跑的是「待发送的数据」,从控制器流进收发器。同理 RXD 是输出,把总线现状汇报给控制器。方向别接反,接反了的现象是「发得出去、收不回来」或干脆不通。
3.3V 的 SN65HVD230(TI 数据手册)引脚几乎同构:1=D(TXD)、2=GND、3=VCC(3.3V)、4=R(RXD)、5=Vref、6=CANL、7=CANH、8=RS。区别在第八脚:RS 接地是高速模式;通过电阻接地是斜率控制模式(下面 L2 讲);拉高是待机模式。和 STM32 同用 3.3V 电源域、直接对接 MCU 引脚,是新手最省事的选择。如果用 5V 的 TJA1050,它的 RXD 输出高电平摆到 5V,进 3.3V 的 MCU 前得确认引脚耐压——更干净的办法是选带 VIO 引脚的型号(比如 TJA1051 家族的 /3 后缀变体,VIO 单独接 3.3V 伺候逻辑侧),或者干脆整条总线都用 3.3V 收发器。
接线清单(以 HVD230 为例):
1
2
3
4
5
6
7
PB9 (CAN1_TX) ──→ D (TXD)
PB8 (CAN1_RX) ←── R (RXD)
3.3V ──→ VCC GND ──→ GND
RS ──→ GND (高速模式)
CANH ──→ 双绞线 ── CANH CANL ──→ 双绞线 ── CANL
两端各一个 120Ω 终端电阻(CANH↔CANL),中间节点不挂
Vref 悬空
然后是代码——这是全文最短的段落,因为改动只有一个位。把上一篇 can_init() 里 BTR 的 LBKM 位删掉,环回模式变成正常模式:
1
2
3
4
CAN_BTR = (10u << 16) /* BS1 = 11 tq */
| (1u << 20) /* BS2 = 2 tq */
| (3u - 1); /* 预分频 3:42MHz/3,1Mbps */
/* 唯一的差别:没有了 (1u << 30) 的 LBKM */
收发器这边没有任何初始化代码——它没有寄存器,是一颗纯电气器件,上电就工作。对软件完全透明,这正是它「动手不动脑」的岗位定义。接上 USB-CAN 分析仪,上一篇那三条命令照旧,就能在 candump 里看到自己的帧从真总线上回来。
到这一步,你「会用」收发器了。但欠着一串解释:TXD 上一个「0」,是怎么变成 CAN_H/CAN_L 上那两个电压的?为什么 PB9 和 CAN_H 逻辑相同电平不同?以及最要紧的——MCU 发出去的东西,是从哪条路「读回来」的?
L2:懂原理——两个方向的翻译
到这一层,问题变了:这颗八脚芯片里到底发生了什么?
一、发:TXD 低电平是一次「拽」
收发器的驱动级就两支输出管,一支对着 CANH、一支对着 CANL。TXD 为低(控制器想发显性 0)时,两支管子同时开通:CANH 被拽向高、CANL 被拽向低——两线拉开约 2V 的差。TXD 为高(隐性 1)时,两支管子一起关断,输出端彻底松手,两线靠弱偏置悬停在半空。TJA1050 手册给的数字:隐性时 CANH 和 CANL 都是典型 2.5V(2.0~3.0V 都算合格),显性时差分输出 1.5~3V。合成一张表(5V 收发器典型值):
| 隐性位(逻辑 1) | 显性位(逻辑 0) | |
|---|---|---|
| CAN_H | ≈ 2.5V | ≈ 3.5V |
| CAN_L | ≈ 2.5V | ≈ 1.5V |
| 两线差值 | ≈ 0V | ≈ 2V |
| 驱动级状态 | 松手(高阻) | 拽线(一抬一压) |
注意「松手」这两个字,这里藏着 CAN 和 UART 的分水岭。串口篇 的空闲是主动驱动的高电平,两根 TX 拼一起会打架;CAN 的隐性是不驱动——高阻。多个收发器的输出级并联在同一对总线上,每个人都能拽、没有任何人能强迫抬高,于是组合逻辑只能是线与:显性碰隐性,拽的赢;显性叠显性,还是显性;全员松手,才是隐性。上一篇那句「0 是强势的、无一例外」,电气实现就在这两支管子的开与关上——显性是动作,隐性是不动作。0/1 在 CAN 里不是对称的两种驱动,而是「拽」与「不拽」。
二、收:RXD 不是「你发的」,是「全网说的」
接收侧是一个差分比较器,盯着 CANH−CANL 的差值(阈值是 ISO 11898-2 的标准值,HVD230 手册里原样可查):差 ≥ 0.9V → RXD 拉低(总线显性);差 ≤ 0.5V → RXD 放高(总线隐性);两者之间是不确定区,靠迟滞和设计躲开。总线开路时判隐性——失效安全,断了线顶多「听不见」,不会产生幻听。
关键来了:TXD 和 RXD 在物理上是两条独立的通路。TXD 从控制器进驱动级,RXD 从比较器出回控制器——不是一根线自己短路绕回来的。平时单节点自说自话,TXD 和 RXD 的波形看起来一模一样,那只是「全网只有你一个人说话时,总线恰好等于你」。一旦有第二个节点抢线:你 TXD 发隐性 1(松手),别人拽成显性,比较器如实汇报,RXD 读回 0——和你的 TXD 分道扬镳。
这个分道扬镳就是上一篇所有魔法的物理入口。仲裁(「我发 1 读回 0,我输了」)、位错误检测(「我发 0 读回 1」)、ACK 表决(「我在 ACK 槽松手后读回 0,说明有人活着」)——bxCAN 干的每一件聪明事,原料都只有一样:把 RXD 和 TXD 逐位比对。控制器拿两个信号做异或就能知道「总线是否服从了我」,收发器的职责是保证 RXD 永远如实反映总线、绝不反映「你应该发的」。也顺带解释了环回模式为什么不需要收发器:它在控制器内部把发送数据抄进接收 FIFO,模拟了「总线服从你」的理想情形。
三、它是时序盲人
收发器不知道波特率、不知道位时序、不知道什么是帧、什么是仲裁。TXD 电平一变它就动,传播延迟几百纳秒量级(TJA1050 手册里 TXD→RXD 一类路径的规格就在 250ns 这一档),仅此而已。位时间的三个段、采样点、SJW,全在 bxCAN 里拿 tq 排账——上一篇 BTR 那笔账没有一个子花在收发器上。反过来说,选收发器时你看的全是电气参数(驱动能力、共模范围、显性超时),和你的波特率表基本无关。唯一的例外是斜率控制模式:HVD230 的 RS 脚通过电阻接地时,输出边沿被刻意放慢——边沿陡则高频分量多、EMI 大,短线上可以拿速度换安静。
四、它自带三份保险
翻手册翻到功能清单,会看到三条「说明书正文里几乎不提、出事才知道值钱」的保护:
- TXD 显性超时:MCU 死机把 TXD 钉死在低电平怎么办?没有这功能,这个节点会把总线永久按在显性,全网哑巴——一颗芯片的软件 bug 勒死整条总线。TJA1050 内置一个计时器:TXD 持续为低超过约 250~750µs(手册
t_dom(TXD)参数),发送器自动关断、总线回到隐性;TXD 恢复翻转即复位计时器。看清楚这个设计的哲学:收发器不信 MCU——上一篇说「任何人都可能坏,包括负责判定坏不坏的那个」,这句车规格言在物理层同样成立:MCU 就是那个可能坏掉、需要被提防的角色。注意 SN65HVD230 没有这个功能(我核过手册,连 timeout 这个词都搜不到)——拿它做产品前,这个缺口要自己心里有数。 - 断电不上总线:一个没上电的节点,输出级保持高阻,不会把别人的总线拖死——热插拔、分批上电的网络都能活。
- 总线脚抗浪涌:TJA1050 的 CANH/CANL 容忍 −27V~+40V,HVD230 手册写共模 −2V~+7V、瞬时共模 ±25V。共模范围还有一层含义:CAN 的各个节点不需要严格共地,地电位差在 ±2V 内都能正常收发——差分信号的红利,地「大致平」就够,单端信号做不到。
到这里,芯片内部的事齐了。但回头想想一个更根本的问题:这颗芯片为什么要存在?把它的功能并进 bxCAN 一起做进 MCU,省一颗料、省一段线、省一个故障点,不好吗?
L3:想得透——物理层为什么住在芯片外面
到这一层,问题只剩一个:分立收发器这个布局,防的是什么、换来了什么?
本质
去掉所有术语,收发器到底是什么?它是 MCU 的嗓门和耳朵。TXD 上是意图(「我想拽」「我想松」),CANH/CANL 上是行动(真实的电流和电压),RXD 上是全场公投的结果。协议的智能——位时序、仲裁、错误账本——全部住在硅片深处那个一纳米级工艺的控制器里;物理世界的蛮力——几十毫安的驱动电流、抗浪涌的粗脖子、断电时的体面(高阻退场)——全部住在旁边这颗老工艺的八脚芯片里。一个动口,一个动手,中间用两条单端线对暗号。
三个被否决的方案
被否决的方案一:把收发器并进 MCU。 市面上确有这么做的产品(印象中以电机驱动为主的车规 SoC 会把 CAN 收发器揉进单芯片,具体型号我没核实过,欢迎指正),但主流世界四十年如一日地分立。第一笔账是保险丝:总线上的电气事故(短路、搭错线、雷击浪涌)最先打死的是挂在线上的那颗芯片——分立布局里,死的是几块钱的收发器,换一颗就完;集成布局里,死的可能是整颗带程序带数据的 MCU。第二笔账是工艺:抗干扰的模拟功率器件喜欢落后但皮实的工艺,数字逻辑追逐最先进的线宽,两者硬挤在同一片硅上谁都别扭。第三笔账是换代:CAN FD 把数据段速率拉到 2Mbps 以上后,老收发器的传播延迟和斜率跟不上了——分立世界里你换一颗新的 SOT-23,集成世界里你换 MCU。
被否决的方案二:让 MCU 直接开漏拽总线(I2C 思路)。 单端、开漏、上拉——机制上也能实现「拽与松」,为什么非要差分?死因还是那辆车:点火线圈和电机把整车电气环境搅成共模地狱,单端信号在地噪声面前毫无还手之力;MCU 引脚直接裸奔在总线上,浪涌一来就阵亡。上一篇说差分是「CAN 能活在汽车里的根本原因」,这个根本原因的载体就是收发器——MCU 躲在它身后,只跟它一个人打交道。
被否决的方案三(最深的一个):学 RS-485,双态都强驱动。 RS-485 也是差分对,凭什么它只能主从半双工点名,CAN 却能对等抢线仲裁?因为 485 的驱动器是推挽的——逻辑 0 和逻辑 1 都主动驱动。两个 485 节点同时开口,一个往正拉一个往负拉,输出级直接对撞短路,除了烧管子不产生任何信息。所以 485 的世界必须先回答「现在轮到谁说」,答案只能是软件层的主从协议。CAN 的答案在电气层:隐性是松手。既然没有人能强迫总线变隐性,冲突就不再是对撞,而是单向覆盖——拽的人赢、松的人输,且双方都毫发无损。同一个「差分双绞线」的物理基底,485 用满力气换带宽,CAN 用半边力气换来了无主机仲裁。「把主持权下沉到物理层」这句话的完整版是:下沉到一颗故意只出一半力气的收发器里。
一段顺手的历史
1986 年博世公布的 CAN 规范只定义到协议层——帧格式、仲裁、错误处置,物理层几乎留白(哪根线、什么电平,悉听尊便)。填上这个空白的是飞利浦半导体:早年它家的 82C250 是 CAN 网络的标配物理层,后来的 TJA 家族(TJA1050/1040/1051……)一路演进到今天在 NXP/Nexperia 门下。今天你买任何一颗「CAN 收发器」,往上数都是这条血脉——一颗没有寄存器、不懂协议、只认两个电平的八脚芯片,四十年没变过模样。
动手环节:让总线亲手教你错误处理
这次的实验只需要一块板子加一个收发器(或者干脆裸跑也行,见文末注)——你要亲手触发一次上一篇只在纸面上见过的 bus-off。
实验一:单节点自杀。 先预测:总线上只有你一个节点,你发一帧,这一帧的命运是什么?
答案:上一篇留过话——「总线上只剩你自己,或者大家都没听清,立刻报错」。现在你是这局唯一的玩家:你的帧完好地发上总线,CRC 也没人怀疑,但 ACK 槽是你发的一个隐性位,你松手,全世界没有人拽——ACK 错误,错误帧拉响,重传。而重传的结果一模一样。每一次失败,TEC 加 8(上一篇立的规矩)。越过 127,你进错误被动;再往上,32 次左右(256 > 255)——bus-off,你被自己放逐了。代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define CAN_ESR (*(volatile unsigned int*)(CAN1_BASE + 0x018)) /* 错误状态寄存器 */
/* can_init():同上一篇,唯一改动是 BTR 去掉 LBKM 位。
uart_init/uart_putc 来自串口篇。 */
int main(void)
{
uart_init();
can_init();
can_send(0x123, (unsigned char[8]){1,2,3,4,5,6,7,8});
while (1) {
unsigned int esr = CAN_ESR;
uart_hex2(esr >> 8); /* TEC:发送错误计数 */
uart_putc(esr & (1u << 2) ? '!' : ' ');
/* BOFF 位亮了就打 '!':bus-off 到了 */
for (volatile int i = 0; i < 300000; i++) ; /* 慢镜头 */
}
}
以下为示意输出(本机没有 STM32 硬件,数值按「每次重传 TEC +8、越过 255 触发 bus-off」的规则推演,未实测):
1
2
08 10 18 20 28 30 38 40 48 50 58 60 68 70 78 80
88 90 98 A0 A8 B0 B8 C0 C8 D0 D8 E0 E8 F0 F8 !
注意三件事:一帧从发出到重传的间隔只有几百微秒(帧 + 错误帧 + 3 位间歇),32 次失败全程不到 10ms——肉眼看就是「打了一帧,然后总线安静了」;bxCAN 缺省 ABOM=0,bus-off 后不会自动复活,得复位或置 ABOM——这也是个值得亲手改一位再跑一遍的实验(置 1 后你会看到它自己爬起来、再撞、再趴下的循环)。另外本实验其实不接收发器也能做:正常模式下 TXD 在 PB9 上空转、RXD 悬空读隐性,同样凑出「永远没人 ACK」的世界——但如果想用示波器看总线侧的电压,就需要收发器和终端电阻了。
实验二:两个世界对表。 双通道示波器,一路挂 PB9(TXD),一路挂 CAN_H,发一帧。先预测:两路波形什么时候长得像、什么时候不像?
答案:逻辑同相,电平两个世界。TXD 低(显性)的每一处,CAN_H 正好抬起(2.5→3.5V);TXD 高的每一处,CAN_H 落回 2.5V——同一件事的两种记法。不像的地方才是重点:TXD 的「高」是 3.3V,CAN_H 的「高」是 2.5V;TXD 围绕地摆,CAN_H 围绕半电源摆。如果再挂第三路 RXD(PB8),你会看到它和 TXD 几乎重合——单节点自说自话,总线服从你。等你有了第二块板子同时开口,再看 TXD 和 RXD 在仲裁位上分叉——那一刻你在示波器上看到的,就是上一篇纸面上推演的整场仲裁。
三个自测问题,能答上说明这三层你真爬过了:
- 收发器的 TXD 为什么是输入引脚?TXD 和 RXD 各自承载的「事实」有什么区别?(答案在 L1 引脚表与 L2 二:名字站在总线视角;TXD 是意图,RXD 是全网公投的结果,两者物理独立,仲裁时分道扬镳。)
- 隐性位为什么必须「松手」,不能像 UART 空闲那样主动驱动成高?(答案在 L2 一与 L3 方案三:线与的前提是无人能强迫总线变隐性;双态强驱动下冲突即对撞短路,只能主从点名。)
- MCU 死机把 TXD 钉在低电平,TJA1050 和 SN65HVD230 各会发生什么?(答案在 L2 四:TJA1050 约 250~750µs 后发送器自动关断、总线回隐性;HVD230 没有这功能,总线被钉死——选型时这是一个真实的差别。)
收尾地图
一张「症状 → 查什么」的速查表收尾:
| 症状/场景 | 查什么 | 一句话理由 |
|---|---|---|
| TXD 有波形、CAN_H 不动 | 收发器供电、TXD 是否接反 | 收发器没上电或引脚接反(TXD 是输入!) |
| 总线电平钉死显性 | TXD 是否被钉低;芯片有无显性超时 | MCU 死机会勒死总线,收发器的超时是救命阀 |
| 发得出、收不到 | RXD 通路、过滤器(上一篇的头号坑) | 收发器透明,问题在接收侧 |
| 3.3V MCU 配 5V 收发器 | RXD 电平是否超 MCU 引脚耐压 | 选带 VIO 的变体,或全 3.3V 方案 |
| EMI 超标 | HVD230 的 RS 改斜率控制模式 | 边沿放慢,高频分量减少 |
| 总线随机错误 | 终端电阻(两端各 120Ω)、共地 | 上一篇的老坑 + 共模 −2~7V 的边界 |
| 新项目选型 | HVD230(3.3V 一体化)/ TJA1050(5V 皮实带超时)/ TJA1051 家族(VIO 变体) | 按电源域和超时需求分 |
最后留一个我自己没找到干净答案的真问题:分裂终端(split termination,两个 60Ω 电阻中点接电容到地)通常推荐接 Vref 辅助偏置,但总线的两个终端节点各自有一颗收发器、各有一个 Vref——两个 2.5V 精度都在 ±5% 左右的基准挂在一起,会不会互相灌电流打架?手册们都说自己可以接,没人说两颗一起接会怎样。如果你在真实项目里验证过,我很想听。
下一站回到上一篇岔出的那条系统集成之路:现在的收发代码全靠轮询,while (!can_recv(...)) 空转烧 CPU;而接收 FIFO 只有 3 级——500kbps 下一帧标准数据帧约 222µs,3 级满打满算 666µs 的余量,一次比一次长的中断风暴就能把它灌爆。轮询路线在真实机器人上活不过第一版:把 CAN 接收改成中断、再塞进 《FreeRTOS 队列》,这个 666µs 的死线就是下一篇要拆的炸弹。CANopen(ID 表之上给报文编语义)仍在更远的岔口等着。