CAN 总线:一根线上没有主持人的会议(以 STM32F4 为例)
《串口篇》 结尾岔出的总线之路,走过了 SPI(加一根时钟线)和 I2C(时钟线 + 地址 + 开漏),但你可能注意到它们有个共同点:都得有个主机。主机不点头,从机一个字都不能说。现在想象机器人上的场景:主控一块、六七个关节电机驱动各一块、IMU 一块、电池管理一块,全都挂在同一对线上,谁想说话谁开口——没有主机、没有点名、没有中央仲裁器。
更怪的是这件事:两个节点在同一纳秒同时开口,总线上居然没有乱码。输的那个立刻退场改听,赢的那个报文一字不差地被全体收到——从头到尾没人主持,赢家甚至不知道刚才发生过争抢。第一次听说”多设备抢一根线”的人,脑子里浮现的都是以太网那种碰撞后退避、退避再碰撞的混乱;CAN 用一个电气上的小花招,把冲突做成了协议的日常。
这条线就是 CAN(Controller Area Network,控制器局域网),汽车里几十个 ECU 的神经总线,也是机器人关节电机最常用的通信方式。这篇文章分三层拆开它:L1 用环回模式一块板子跑起来,L2 看”同时说话不打架”背后的电气与协议机制,L3 想明白为什么这套设计在汽车的地狱环境里活了四十年——以及它和 《电机篇》 埋的闭环之间,隔着什么。
L1:先让它跑起来——环回模式,一块板子都不用找
你在第一层。目标:用最小代码把一帧报文发出去、再把它原样收回来,对 CAN 外设长什么样有个地图感。
CAN 的麻烦是它天生不是一个人的游戏——一根线上至少俩节点。但 bxCAN(STM32F4 的 CAN 外设)准备了一个后门:环回模式(loopback),自己发的帧在芯片内部绕一圈回到自己的接收 FIFO,不用第二个节点、不用收发器,一块板子就够。真要接物理总线时,再加一颗 CAN 收发器(3.3V 的 SN65HVD230 或 5V 的 TJA1050)和一根双绞线,两端各挂一个 120Ω 终端电阻——记住是两端,中间节点不挂,这个坑下面还会念叨。
STM32F407 的 CAN1 在 APB1 上(42MHz),引脚用 PB8/PB9(AF9)。寄存器照旧直接怼地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#define RCC_AHB1ENR (*(volatile unsigned int*)0x40023830)
#define RCC_APB1ENR (*(volatile unsigned int*)0x40023840)
#define GPIOB_MODER (*(volatile unsigned int*)0x40020400)
#define GPIOB_AFRH (*(volatile unsigned int*)0x40020424)
#define CAN1_BASE 0x40006400u
#define CAN_MCR (*(volatile unsigned int*)(CAN1_BASE + 0x000))
#define CAN_MSR (*(volatile unsigned int*)(CAN1_BASE + 0x004))
#define CAN_TSR (*(volatile unsigned int*)(CAN1_BASE + 0x008))
#define CAN_RF0R (*(volatile unsigned int*)(CAN1_BASE + 0x00C))
#define CAN_BTR (*(volatile unsigned int*)(CAN1_BASE + 0x01C))
#define CAN_TIR0 (*(volatile unsigned int*)(CAN1_BASE + 0x180)) /* 发送邮箱0 */
#define CAN_TDTR0 (*(volatile unsigned int*)(CAN1_BASE + 0x184))
#define CAN_TDLR0 (*(volatile unsigned int*)(CAN1_BASE + 0x188))
#define CAN_TDHR0 (*(volatile unsigned int*)(CAN1_BASE + 0x18C))
#define CAN_RIR (*(volatile unsigned int*)(CAN1_BASE + 0x1B0)) /* 接收FIFO0 */
#define CAN_RDTR (*(volatile unsigned int*)(CAN1_BASE + 0x1B4))
#define CAN_RDLR (*(volatile unsigned int*)(CAN1_BASE + 0x1B8))
#define CAN_RDHR (*(volatile unsigned int*)(CAN1_BASE + 0x1BC))
#define CAN_FMR (*(volatile unsigned int*)(CAN1_BASE + 0x200)) /* 过滤器 */
#define CAN_FS1R (*(volatile unsigned int*)(CAN1_BASE + 0x20C))
#define CAN_FA1R (*(volatile unsigned int*)(CAN1_BASE + 0x21C))
#define CAN_FR1_1 (*(volatile unsigned int*)(CAN1_BASE + 0x240))
#define CAN_FR1_2 (*(volatile unsigned int*)(CAN1_BASE + 0x244))
void can_init(void)
{
RCC_AHB1ENR |= 1u << 1; /* GPIOB 时钟 */
RCC_APB1ENR |= 1u << 25; /* CAN1 时钟 */
GPIOB_MODER |= 2u << 16; /* PB8 -> 复用(CAN1_RX) */
GPIOB_MODER |= 2u << 18; /* PB9 -> 复用(CAN1_TX) */
GPIOB_AFRH |= 9u << 0; /* PB8 -> AF9 = CAN1 */
GPIOB_AFRH |= 9u << 4; /* PB9 -> AF9 = CAN1 */
CAN_MCR &= ~(1u << 1); /* 清 SLEEP:先把外设叫醒 */
CAN_MCR |= 1u << 0; /* INRQ:请求进初始化模式 */
while (!(CAN_MSR & 1u)) /* 等 INAK:硬件确认就位 */
;
CAN_BTR = (1u << 30) /* LBKM:环回,自己发自己收 */
| (10u << 16) /* BS1 = 11 tq */
| (1u << 20) /* BS2 = 2 tq */
| (3u - 1); /* 预分频 3:42MHz/3 = 14MHz */
CAN_FMR |= 1u << 0; /* 过滤器进入初始化模式 */
CAN_FS1R |= 1u << 0; /* 过滤器组0:单 32 位模式 */
CAN_FR1_1 = 0; /* ID:随便填 */
CAN_FR1_2 = 0; /* 掩码:全 0 = 32 位全都"无所谓" */
CAN_FA1R |= 1u << 0; /* 激活过滤器组0 */
CAN_FMR &= ~(1u << 0); /* 过滤器退出初始化模式 */
CAN_MCR &= ~(1u << 0); /* 离开初始化模式,上线 */
while (CAN_MSR & 1u)
;
}
void can_send(unsigned int id, const unsigned char d[8])
{
while (!(CAN_TSR & (1u << 26))) /* TME0:发送邮箱0空了没 */
;
CAN_TDLR0 = d[0] | (d[1] << 8) | (d[2] << 16) | ((unsigned)d[3] << 24);
CAN_TDHR0 = d[4] | (d[5] << 8) | (d[6] << 16) | ((unsigned)d[7] << 24);
CAN_TDTR0 = 8; /* DLC:8 字节 */
CAN_TIR0 = (id << 21) | 1u; /* 标准 ID 进高 11 位;TXRQ=1,走你 */
}
int can_recv(unsigned int *id, unsigned char d[8])
{
unsigned int i;
if (!(CAN_RF0R & 3u)) /* FMP:FIFO0 里有没有货 */
return 0;
*id = CAN_RIR >> 21;
for (i = 0; i < 4; i++)
d[i] = CAN_RDLR >> (8 * i);
for (i = 0; i < 4; i++)
d[4 + i] = CAN_RDHR >> (8 * i);
CAN_RF0R = 1u << 4; /* RFOM0:释放 FIFO 顶部(直写,别 |=) */
return 1;
}
int main(void)
{
unsigned char tx[8] = {1,2,3,4,5,6,7,8};
unsigned char rx[8] = {0};
unsigned int id = 0;
can_init();
can_send(0x123, tx);
while (!can_recv(&id, rx)) /* 环回也要走完一百多个位时间 */
; /* @1Mbps 就是上百微秒,别急 */
/* 到这里 id = 0x123、rx = 01..08:8 字节原样回来了,
配合串口篇的 uart_putc 就能打出来看 */
while (1)
;
}
烧进去,rx 里就是 01 02 ... 08,ID 0x123。先钉几个”看着眼生”的地方:
- INRQ/INAK 握手:CAN 外设和 《MCU 开发》 里那些”写完就生效”的外设不一样,它有一个正式的初始化状态——你请求(INRQ),它确认(INAK),你才能动那些”开车时不许碰”的寄存器(BTR 只在初始化模式里可写)。改波特率不改模式,写了等于没写,这是 CAN 排行榜前几名的坑。
CAN_BTR里那笔账:预分频 3 把 42MHz 分成 14MHz,一个时间份额(tq)71.4ns;位时间 = 1 + 11 + 2 = 14 个 tq = 1µs,也就是 1Mbps。这笔账和串口篇的BRR = 84000000 / 115200是同一门手艺,只是 CAN 把”一位”切成了三个段(同步段、BS1、BS2),为什么要切、采样点在哪,L2 细算。- 过滤器必须配:CAN 没有收件人——每个报文都是吼给全场的,你的收不收由门口保安(28 组过滤器)决定。F407 的过滤器缺省全关,一个都不激活,FIFO 里就永远是空的——”CAN 发得出去收不到”的头号嫌疑。这里配的是”全收”:掩码全 0,32 位每一位都无所谓。
(id << 21):标准 ID 是 11 位,放在 TIR 的高位段。11 位 ID 共 2048 个号,L2 会看到这串数字同时是优先级。- 邮箱和 FIFO:发送有 3 个邮箱(排队中、正在发、发完待清),接收有 2 个 FIFO 深度各 3 级。CPU 只管往里放、往外取,位时序、重传、仲裁全是硬件在跑。
到这一步你已经会收发了,真机上接两块板子或买一个几块钱到几十块钱的 USB-CAN 分析仪,Linux 下三条命令就能看到总线上的现场直播:
1
2
3
4
sudo ip link set can0 type can bitrate 500000
sudo ip link set can0 up
candump can0
# can0 123 [8] 01 02 03 04 05 06 07 08
但欠着一屁股解释:CAN_H/CAN_L 两根线怎么就”线与”了?两个节点同时发,凭什么输赢自动分出、赢家无损?帧里为什么有个专门说”谢谢”的位?
L2:懂原理——冲突怎么被物理层消化掉
到这一层,问题变了:不再问”怎么写”,而是问”底下发生了什么”。
一、电气层:0 是强势的
CAN 物理上是两根线(CAN_H、CAN_L)上的差分信号,只有两种状态:
| 隐性位(逻辑 1) | 显性位(逻辑 0) | |
|---|---|---|
| CAN_H | ≈ 2.5V | ≈ 3.5V |
| CAN_L | ≈ 2.5V | ≈ 1.5V |
| 两线差值 | ≈ 0V | ≈ 2V |
| 物理含义 | 所有节点”松手” | 至少一个节点在”拽线” |
关键在最下面那行的物理含义。收发器发隐性位时输出级松开(靠偏置停在 2.5V),发显性位时一头把 CAN_H 拽高、一头把 CAN_L 拽低。于是多节点的组合逻辑是线与:几个显性叠显性,还是显性(大家一起拽);显性碰上隐性,还是显性(拽的压过松手的);只有全员隐性,线才是隐性。0 赢 1,无一例外。
注意这和 《串口篇》 的哲学一脉相承又往前走了一步。串口空闲是高电平——沉默是被约定的;起始位是低——开口就是打破沉默。CAN 一样:总线空闲是隐性(沉默),帧以一个显性位(SOF)开场。区别在于,串口的”沉默”是两个人的默契,CAN 的沉默是全场共有的——一根线上挂着的所有节点,同处于一个沉默里,谁都可以随时用一个显性 0 打破它。而”多个 0 并存还是 0”这条电气规则,就是后面一切魔法的种子。
差分本身则是 CAN 能活在汽车里的根本原因:点火线圈、电机、继电器把整车电气环境搅成电磁地狱,干扰电压同时砸在 CAN_H 和 CAN_L 上(共模干扰),接收方只看两线差值——一起抖等于没抖。串口的单端信号在这件事上毫无还手之力。
二、仲裁:逐位对表,输家自己知道疼
现在让两个节点同时开口。前文埋的线收紧了:每个节点发送每一位的同时都在回读总线。我发隐性 1,读回来却是显性 0——说明有别人在线上拽——我输了,立刻闭嘴,改收。全部仲裁机制就这一句话,没有第三个角色。
看一场真实的对局。节点 A 发 ID 0x123,节点 B 发 ID 0x1FF,同拍起跑(SOF 显性位就是起跑枪,全场对齐)。ID 高位在先,逐位上总线:
1
2
3
4
5
6
7
线上的位序 SOF b10 b9 b8 b7 b6 b5 ...
A (0x123) 0 0 0 1 0 0 ...
B (0x1FF) 0 0 0 1 1 1 ...
总线实际值 0 0 0 1 0 0 ...
↑
b7:A 发 0(拽线),B 发 1(松手)
B 回读发现是 0 —— "有人比我强,我闭嘴"
前四位(SOF 到 b8)两边发的完全一样,线上也完全一样,谁都不知道有对手。到 b7 分出高下:B 发的 1 被线与吃掉了。注意两件事:赢家 A 全程毫无感知——它发的每一位都原样回到线上,好像线一直是它一个人的;输家 B 零损失退场——它不是报文撞残了才退,而是在第 4 个 ID 位就知道了结果,无缝转成接收方,把 A 的帧完整听完,等总线空闲(帧间 3 位隐性间歇)再重发自己的。
ID 是 11 位数,小的赢——而这恰好和电气规则咬合得天衣无缝:0 在电气上强势,0 在编号上也是优先。最小 ID 0x000 享有最高优先级。这不是巧合,是设计:让”物理上压得住”和”逻辑上该先说”变成同一个比特。
再往深一层:仲裁发生在 ID 段,所以优先级属于报文,不属于节点。同一个节点,报”急停”用低 ID、报”温度”用高 ID,急停就插到所有人前面。这是汽车和机器人分配实时性的标准做法:把 ID 段规划成一张优先级表。
三、帧解剖:一封 111 位的明信片
一帧标准数据帧的全貌(8 字节载荷时):
| 段 | 宽度 | 干什么 |
|---|---|---|
| SOF | 1 位 | 显性,起跑枪 |
| 仲裁段 | 11 位 ID + 1 位 RTR | 优先级 + 内容标签;RTR 区分数据帧/遥控帧 |
| 控制段 | 1 + 1 + 4 位 | IDE(标准/扩展)、保留位、DLC(载荷几字节) |
| 数据段 | 0–64 位 | 载荷,最多 8 字节 |
| CRC 段 | 15 + 1 位 | 校验 + 界定位 |
| ACK 段 | 1 + 1 位 | 全员表决(下面细讲) |
| EOF | 7 位 | 7 个连续隐性 = 帧结束 |
| 帧间隔 | 3 位 | 间歇,下一场仲裁的喘息 |
加起来:1 + 12 + 6 + 64 + 16 + 2 + 7 + 3 = 111 位(帧本体 108 位,再加 3 位帧间隔)驮 64 位载荷——零填充时效率约 58%,算上填充平均五成上下。1Mbps 满跑每秒七千五到九千帧(取决于数据赶不赶上最坏填充情况),有效吞吐 60~70 KB/s——和 《MQTT》 那种”跑在 TCP 上的应用层协议”比朴素得多,这是 1980 年代中期的硅片预算决定的,也是它能跑在几块钱收发器上的原因。
位填充(bit stuffing):发送方每连发 5 个同值位,就自动插一个反值位;接收方数到 5 自动剥掉。这套规则只管辖 SOF 到 CRC 序列这一段——后面的 ACK 槽、EOF 不参与,所以 EOF 连续 7 个隐性位是合法的。为什么要有它?串口没有时钟线,靠起始位每帧对一次表;CAN 也没有时钟线,但一帧 100 多位,只对一次表撑不住——接收方需要源源不断的电平翻转来持续重新同步。最坏情况线上出现连续 5 个同值位还没翻转,第 6 个插入位保证翻转至少每 6 个位来一次。位填充就是 CAN 版的”心跳”。顺便记住”填充区里最多 5 连同值”,第五节它还有戏份。
ACK 段是全场最妙的设计:发送方在 ACK 槽发一个隐性位——松手,然后等。总线上任何一个收到这帧且 CRC 校验通过的节点,都会把这一位拽成显性。发送方回读:显性 = 至少有一个人完整听到了;隐性 = 没人应答(总线上只剩你自己,或者大家都没听清),立刻报错。没有”接收方回一条 ACK 报文”这种两步走,确认就发生在这帧报文的身体里,一个位,全体投票。
四、位时序:采样点是一门手艺
回到 L1 的 BTR。CAN 的一个位被切成三段:同步段(1 个 tq,电平翻转被锁定在这里)+ BS1 + BS2,采样点在 BS1 结束、BS2 开始的地方。L1 的配置 1 + 11 + 2 = 14 tq,采样点在 12/14 ≈ 85.7% 处。
为什么采样点要这么靠后(行业惯例 75%~87.5%)?因为信号要和最远的节点打一个来回:你的电平翻转传到总线最远端、对端依此发出的电平再传回来,全都得赶在你采样之前完成。BS1 就是留给这一来一回在路上跑的时间。采样点越靠后,允许的线越长——这笔账动手环节实验三当面算。
至于 SJW(再同步跳转宽度,BTR 里 [25:24] 那 2 位),规定每次重新同步最多跳几个 tq,日常配置 1~2 个 tq 就行,先记住它是”对表时允许的步长上限”。
五、纠错:撒谎会被全体当场戳穿
CAN 的纠错是一套没有法官的司法系统。
怎么发现错:CRC 不通过(数据坏了)、位错误(我发显性读回隐性,或反之——只在不是仲裁段和 ACK 槽时才算错,那两处”读不回自己”是正常操作)、填充错误——就是前面让你记住的那条:填充区里最多 5 连同值,在 SOF 到 CRC 之间数到 6 个同值位,一定出事了。
怎么广播错误:出错节点立刻发”错误帧”——连续 6 个显性位。看懂这个设计的优雅之处:它本身是一次故意的位填充违规。全网所有节点都在执行同一条数数规则,任何人看到 6 连显性都会立刻判定总线出错,跟着一起发错误帧,把这一帧连同现场彻底搅碎,等大家安静下来重发。位填充这一个机制,平时是心跳,出错时是警报——一物两用。
怎么处置惯犯:每个节点揣着两本账——发送错误计数(TEC)和接收错误计数(REC),出错加账(发送错 +8),收发成功减账。账本把节点分成三个状态:
1
2
3
4
5
6
7
8
9
TEC>127 或 REC>128 TEC>255
┌──────────┐ ────────────────→ ┌──────────┐ ────────→ ┌──────────┐
│ 错误主动 │ │ 错误被动 │ │ 总线关闭 │
│ 数错就喊 │ ←──────────────── │ 出错只敢 │ │ 自我放逐 │
│ (主动标志)│ 两本账都跌回 ≤127 │ 小声嘀咕 │ │ 不许说话 │
└──────────┘ └──────────┘ └─────┬────┘
↑ │
└──────────── 恢复:两本账清零,重新做人 ←─────────────────┘
条件:连续观察到 128 组"11 个连续隐性位"
错误主动状态下你还能发主动错误标志(6 个显性,一嗓子喊醒全场);错误被动(账本过线)说明”要么你坏了,要么你那头的线路坏了”,只许发被动错误标志(6 个隐性,小声嘀咕,因为你自己都可能不可信,不许再搅扰总线);发送账本越过 255——你自己就是故障源,主动放逐出总线(bus-off),一个字都不许再说。想回来?观察 128 组 11 个连续隐性位——相当于缓刑观察期,看总线在你缺席时是否依然健康。bxCAN 里把 MCR 的 ABOM 位置 1,这套恢复是硬件自动的。
到这里机制都齐了。但回头看,每一条都透着一股执拗:不要主机、错误要全网联防、故障节点自己放逐自己。为什么非得这样设计?
L3:想得透——把主持权下沉到物理层
到这一层,问题只剩一个:这套设计在防什么?
本质
去掉所有术语,CAN 到底做了什么?它把”发言权分配”这道题,从任何中央角色手里拿走,下沉到了物理层。一根线与的差分线,加上”每个节点逐位回读自己发出的电平”,让”同时说话”从需要防止的事故,变成被协议消化的常态——分出胜负用的是一个位的时间,代价是确定的:编号大的排队,编号小的先行。
一个心智模型:一场没有主持人的圆桌会议,但话筒是特殊做的——低编号的话筒物理功率更大,两人同时开口时,高编号那位一听见自己的声音被盖住就自觉闭嘴、把对方的话听完。圆桌会议上人人可发言、谁都能听见(广播)、抢话自决、没有单点权威。
类比拆脚手架的时候到了,两处失效要交代清楚。第一,真人会议里被插话者会尴尬、赢家会得意——CAN 里没有任何”社会性”:赢家不知道被抢过,输家不觉得受辱,抢话只是一次电气事件。第二,真人会议里嗓门属于人,CAN 里优先级属于报文(ID),同一节点发不同 ID 优先级不同——把”话筒功率”想象成人的属性,就理解不了”急停报文永远插队”这件事。
三个被否决的方案
被否决的方案一:设一个主机轮询(I2C 模式)。 主机点名,谁也不许抢话,多干净。它死在单点故障上:主机挂了,全场哑巴。车里制动、安全气囊这些节点不能因为别的节点死了就不工作;CAN 的分布式仲裁里,任何一个节点退出游戏,总线照常运转。还有一笔账:主机轮询的延迟是 O(N) 起步,N 个节点都等点名,最坏等待时间随节点数线性涨;CAN 里一帧报文的最坏等待 = 手头正发的那帧(不可抢占,8 字节帧最坏一百三十多位)+ 排在它前面的更高优先级帧——后面这个数目不随全网节点数增长,只取决于系统设计里给多少人更高优先级。
被否决的方案二:破坏性碰撞 + 随机退避(老以太网 CSMA/CD 模式)。 检测到碰撞,双方都作废、各自随机等一段时间重试。问题是双输:赢家的报文也作废了,带宽白白烧掉一帧;更糟的是随机退避给出的是统计性延迟——高优先级报文也可能一直倒霉,最坏延迟没有上界。车规场景要的是确定性:制动报文必须保证 N 微秒内上总线,”平均很快”在安全工程里一文不值。CAN 仲裁的妙处正在于它是非破坏性的——赢家报文一字不损,冲突的代价全部由输家承担,而输家天然是低优先级、可以等的那个。
被否决的方案三:点对点星型(UART 阵列)。 主控开 7 个串口,扯 7 对线。这正是 CAN 诞生的导火索:1980 年代初,博世的工程师面对一辆车里越来越多的电控单元,每对通信都要专线,线束越滚越粗——成本、重量、故障点全在涨。而且星型没有广播:一条”全体急停”得发 7 遍,还得软件保证它们”差不多同时”到达。CAN 两根线全车共享,广播免费,节点即插即用(新节点挂上线就收得到一切,加电机不动任何旧线)。
顺手再嚼一个小设计:为什么数据段最多 8 字节——一个字都不多给。帧越短,单次占用总线的时间越短,低优先级报文被一帧长报文堵住的最坏时间就越短。8 字节装不下就拆包,拆包的重组开销丢给软件,把”最坏延迟有界”这条硬指标留给总线。1980 年代的硅片也乐意:FIFO 和过滤器按 8 字节做,便宜一个数量级。
一段顺手的历史
1986 年,博世正式公布 CAN;1993 年它成为 ISO 11898 标准。之后的四十年里它没有惊心动魄的版本战争,只是安静地渗透:今天从汽车 ECU 网络、发动机 OBD 诊断口(2008 年起美国市场强制新车诊断走 CAN),到工业现场的 CANopen、船舶的 NMEA 2000,到处是那两根双绞线。机器人圈更是重度用户——相当一部分关节电机驱动器的主接口就是 CAN,主控与电机之间的”转速指令/位置反馈”就跑在本文讲的 ID 和那 8 字节里。2012 年博世又推出 CAN FD(Flexible Data-rate),载荷扩到 64 字节、数据段可冲到 5Mbps 以上,帧格式和仲裁哲学原封未动——和串口活过六十年靠的是同一件事:根基的东西不动,扩容的东西再议。
一个反直觉收尾
回味一下 bus-off 那套设计:没有中心裁判,节点自己记账、自己降级、自己放逐自己、自己观察缓刑期满回队。整套司法系统里没有一个”别人”——因为如果需要一个中心节点来判定谁坏了,那个中心自己坏了怎么办?汽车安全设计的默认前提是:任何人都可能坏,包括负责判定坏不坏的那个。CAN 把判定权下放给每个节点的账本,把证据(电平)公开在总线上让所有人看见。分布式系统教材里反复打磨的同一类思想——不信任任何单点,包括负责判定故障的那个——CAN 在 1986 年,用一根双绞线和两个错误计数器,就把它开进了汽车。
动手环节:一张纸、一个计算器
这次的实验不需要第二块板子。
实验一:仲裁推演。 节点 A 发 ID 0x0FF,节点 B 发 ID 0x100,同时开口。先预测:谁赢?在第几个 ID 位分出胜负?赢家全程有没有察觉?
答案:0x0FF = 000 1111 1111,0x100 = 001 0000 0000。b10、b9 都是 0,势均力敌;到 b8(第 3 个 ID 位)A 发 0、B 发 1,B 回读到显性,当场退场。A 赢(ID 小),赢家全程无感知。加餐题:如果两个节点恰好同时发同一个 ID(比如两个温度计都报 0x100),仲裁段全程同值,谁都读回自己发的值,两个都以为自己是赢家——一路同行到数据段,载荷第一次出现分歧就当场拆穿:发隐性那位回读到显性,位错误,错误帧拉响;另一个的报文随即被错误帧搅碎,也报错。双双重试,再撞……所以”同 ID 同周期上报”在真实系统里是要专门避开的设计错误:ID 是地址,也是优先级,还是身份——一人一号。
实验二:位时序的账。 L1 里 42MHz 配出了 1Mbps(预分频 3,1 + 11 + 2 = 14 tq)。现在要配 500 kbps,还是这块板子。先预测:BTR 怎么改?
答案:位宽翻倍到 2µs,最优雅的改法是预分频翻倍(6),三段原封不动——还是 14 个 tq,只是每个 tq 从 71.4ns 变成 142.9ns,采样点依旧 85.7%。当然答案不唯一(预分频 7、12 个 tq 也行:1 + 9 + 2,采样点 83.3%),工程上的规矩是用”采样点对齐 87.5% 附近”把答案收窄——顺便记一条硬件边界:BTR 里 TS1 只有 4 位、TS2 只有 3 位,一位最多塞 1 + 16 + 8 = 25 个 tq,想全靠缩 tq 拉慢速率是有限度的,速率再低就得动预分频。看懂这一层,你就能读懂任何 CAN 位时序计算器:先定一个位里塞几个 tq,再用预分频把 tq 拉到目标宽度,最后用采样点惯例分三段。
实验三:线长的账。 信号在双绞线上约 5ns/m。仲裁的物理要求是一来一回:你的电平传到最远节点、对端回敬的电平再传回来,必须赶在你采样之前到齐——所以预算 = 采样点之前的位宽 − 收发器来回合计约 250ns,而这笔钱要付两倍线长的路费。先预测:1Mbps(位宽 1000ns,采样点按行业惯例的 87.5% 取整算,L1 实配 85.7% 同量级)理论上最远能跑多少米?再算 125 kbps 的,两次都和行业经验表(1Mbps → 40m,125 kbps → 500m)比一比。
答案:1Mbps:875 − 250 = 625ns,付两倍路费,625 ÷ 2 ÷ 5 ≈ 62m;125 kbps:(0.875 × 8000 − 250) ÷ 2 ÷ 5 ≈ 675m。对比经验表 40m 和 500m——物理上限之上留了三五成余量,不是白留的:线上的短截线(中间节点引出的小支线)反射、接插件阻抗跳变、晶体误差、收发器个体差异,每一项都在吃预算。这个实验真正想让你看见的是:“波特率×线长≈常数”不是玄学经验,是”信号要在一个位里跑完一个来回”这条物理定律的直接推论——想拉长线,就把位拉宽,也就是降波特率。
三个自测问题,能答上说明这三层你真爬过了:
- 电气上为什么 0 是强势位?”0 强势”和”ID 小优先”这两个设计是怎么咬合的?(答案在 L2 一、二:松手/拽线的线与;显性 0 物理压过隐性 1,编号小 = 高优先,同一个比特既是电气赢家又是逻辑赢家。)
- 仲裁输掉的节点,在输掉之后到重发之前,做了哪三件事?(答案在 L2 二:当场改收、把赢家的帧听完、帧间 3 位隐性后重试——全程无碰撞损失。)
- 数据段为什么死守 8 字节上限?这个上限买到了什么?(答案在 L3:单帧占用总线的时间有上界 → 低优先级报文的最坏等待有上界 → 高优先级延迟确定性;重组的麻烦丢给软件。)
收尾地图
把整篇收进一张速查表:
| 你想干什么 | 怎么做 | 一句话理由 |
|---|---|---|
| 两块板说话 | UART | 一对一,最便宜 |
| 一主多从、板内/板间短距 | I2C / SPI | 要主机,但简单快 |
| 多节点对等、要广播、抗干扰、几十米 | CAN | 本文:无主机、非破坏仲裁、差分 |
| 机器人主控 ↔ 关节电机 | CAN(多数驱动器原生支持) | 生态现成,实时性好 |
| 汽车诊断口 | OBD-II(底层就是 CAN) | 强制标准 |
| 无外设调试 CAN 代码 | 环回模式(L1) | 一块板子解决 |
| 真总线调试 | USB-CAN + candump | 总线现场直播 |
| 收不到任何报文 | 先查过滤器激活没有 | 头号坑:缺省全关 |
| 信号振铃、偶发错误 | 查终端电阻:只有物理两端各一个 120Ω | 多挂一个 = 60Ω 变 40Ω |
下一站有两条岔路。往协议上层走:CAN 只回答”这一帧谁发给谁”,不回答”ID 0x201 的 8 个字节到底是什么意思”——给报文编语义的 CANopen(对象字典、SDO/PDO)是机器人电机的现实世界,到那里你会发现今天学的 ID 表就是 CANopen 的地基。往系统集成走:把 CAN 接收从轮询升级成中断、再塞进 《FreeRTOS 队列》——多节点总线 + 消息队列,正是 那篇机器人科普 里”感知—决策—执行”跨机器协作的硬件底座。而 《电机篇》 结尾埋的闭环(编码器 + PID)也快到期了:电机怎么转、怎么报转速(走 CAN)、怎么稳在目标转速——三块拼图,就差最后一块。