CAN总线详解
笔记 / 嵌入式
引言
UART 只能点对点,I2C 和 SPI 只能由一个主机带一圈从机(见 嵌入式通信协议UART、I2C、SPI对比详解)。要在一台设备里挂几十个节点、允许任意节点主动发送、还得在电机旁边稳定工作,这三种都做不到。CAN 用一对差分线同时解决了三件事:多主竞争有仲裁,节点出错有隔离,短帧自带 CRC。汽车上几十个 ECU 挂在同一对线上,工业和电机控制也大量采用。
下面按四层展开:电气层(差分与线与)、仲裁层(谁先发送)、报文层(帧的字段)、时序层(采样点位置),最后是 bxCAN 外设的配置。
一、拓扑与电气

每个节点由三部分组成:MCU 内的 CAN 控制器(bxCAN,负责协议)、收发器(TJA1050、SN65HVD230 等,负责电平转换)、以及连接 CAN_H/CAN_L 的两根总线。控制器输出 TX/RX 逻辑电平,收发器把它转换成总线上的差分电压。
CAN 没有时钟线,也没有主从,只靠两个电平约定工作:
| 总线状态 | CAN_H | CAN_L | 差分电压 | 逻辑值 |
|---|---|---|---|---|
| 显性(dominant) | ≈ 3.5 V | ≈ 1.5 V | ≈ 2 V | 0 |
| 隐性(recessive) | ≈ 2.5 V | ≈ 2.5 V | ≈ 0 V | 1 |
关键在于显性电平压过隐性电平:只要有一个节点输出 0,总线就是 0。可以把总线理解为一根被所有节点并联下拉的线,任何一个节点把它拉到低电平,整根线就是低电平。多个节点同时发送时,总线电平等于所有输出的线与。这个物理特性是下一节仲裁的基础。
差分带来的好处是共模抑制:两根线绞在一起,干扰同时作用于两根线,接收端取差值后共模噪声被抵消。这也是 CAN 线要求绞接、并且能延伸到几十米到一公里的原因(1 Mbps 约 40 m,降到 40 kbps 约 1 km,硬约束是信号往返延迟)。
信号沿电缆传播到末端时,如果负载阻抗与电缆的特性阻抗不匹配,一部分能量会反射回来,叠加在后续的位电平上。手册的拓扑图没有画终端电阻,实物中必须有:总线两端各接一只 120 Ω 电阻,跨接在 CAN_H 与 CAN_L 之间,让末端呈现电缆期望的阻抗,从而匹配特性阻抗、吸收反射。两端都接好后,万用表测得总线电阻约 60 Ω(两只 120 Ω 并联),这是上电调试前的例行检查。漏接终端电阻会出现信号振铃和偶发 CRC 错误;中间多接一只则负载加重、显性电平变弱,整条总线都不稳定。
二、仲裁:ID 小者优先
多主意味着可能发生冲突:两个节点同时发送怎么办。CAN 的规则只有一条:节点在发送隐性位(1)的同时回读总线,如果读到显性位(0),说明有其它节点在抢占,于是立即退出发送并转为接收。
由于总线是线与,ID 数值小的帧必然在某个位上先发出 0,把对方压成”读到 0”。因此:
- ID 越小优先级越高(0x001 胜过 0x100)
- 仲裁失败的节点不会损坏数据,也不重发垃圾帧;它自动转为接收,等总线空闲后再次尝试,获胜的那一帧无损通过。这称为非破坏性仲裁
- 工程上把电机电流环报文的 ID 排在诊断报文之前,实时性就由仲裁结果决定
代价是优先级设计没有回旋余地:高优先级节点持续发送会饿死低优先级节点。CAN 没有公平调度机制,优先级分配本身就是系统设计的一部分。
三、报文格式

以标准数据帧为主干,逐字段说明:
| 字段 | 位数 | 作用 |
|---|---|---|
| SOF | 1 | 显性位,硬同步,帧起始 |
| ID | 11 | 仲裁 + 报文身份(标识”什么报文”,不是”发给谁”) |
| RTR | 1 | 0=数据帧,1=遥控帧 |
| IDE | 1 | 0=标准格式,1=后面还有 18 位 ID |
| r0/r1 | 1 | 保留,显性 |
| DLC | 4 | 数据长度 0–8 字节 |
| Data | 0–64 | 有效载荷 |
| CRC | 15 | 校验序列 + 1 位界定符 |
| ACK | 2 | 发送方发隐性,任何一个正确接收的节点把它改成显性——没有 ACK 位被拉低,发送方就知道无人接收 |
| EOF | 7 | 全隐性,帧结束 |
四个变体中,有三点需要区分:
- 扩展帧:29 位 ID,仲裁段拆成 11 位基 ID + SRR + IDE + 18 位扩展 ID,前段按标准帧规则仲裁——同 ID 时标准帧优先
- 遥控帧:RTR=1、无数据。用于向网络请求”谁有 0x201 的数据,发一份”
- 错误帧:任何节点检测到错误就发 6 个连续显性位——这故意违反位填充规则,全网立刻知道这帧作废
位填充(bit stuffing)贯穿 SOF 到 CRC:连续 5 个同电平位后自动插入一个反相位。NRZ 编码在电平不变时没有任何边沿,接收端无法校正自己的采样时钟——UART 靠每个字符的起始位重新对齐,位填充起的是同样的作用。它同时让错误帧的 6 个连续显性位成为明确的违规信号。计算帧长时不能忽略填充位,最高速率下它会占用约 20% 的标称带宽。
CAN 是广播总线,帧里没有源地址和目标地址,ID 标识的是报文内容而不是接收方,是否接收由各节点的过滤器决定,见第五节。
四、位时序

UART 只需要收发双方约定相同的波特率,CAN 的要求更严:仲裁要求全网节点在同一个位时间内的同一时刻采样,时钟误差会直接影响仲裁和 ACK 的正确性。每个位被切成三段:
| 段 | 长度 | 作用 |
|---|---|---|
| SYNC_SEG | 固定 1 tq | 同步缓冲,预期跳变落在这里 |
| BS1 | 1–16 tq | 跳变后传播 + 相位缓存,采样点在 BS1 结束处 |
| BS2 | 1–8 tq | 采样后的缓冲,也给重同步留调整空间 |
时间量子 tq = (BRP + 1) × tPCLK,于是波特率 = 1 / [(1 + BS1 + BS2) × tq]。采样点位置 = (1 + BS1) / (1 + BS1 + BS2),工程惯例放在 75%–87.5%,给信号传播和晶振误差留余量。
以 500 kbps 为例(PCLK1 = 36 MHz,即 时钟树与RCC配置详解 里的 72/36/72 配置):
- 位时间 = 2 µs;取 tq = 250 ns → BRP+1 = 9,每位 8 tq
- 切分 1 + 5 + 2:采样点 = 6/8 = 75%
- 寄存器:
Prescaler=9, TS1=4, TS2=1(段长减一写进寄存器)
SJW(重同步宽度,1–4 tq)是最后一个参数:接收方发现跳变没落在 SYNC_SEG,就把 BS1 拉长或 BS2 缩短最多 SJW 个 tq 追回去。SJW 不能超过 BS1/BS2 的最小值,通常配 1 个 tq。
节点间晶振误差的预算大约 ±1.4%(容忍度取决于采样点位置),因此 CAN 节点应当使用晶体。HSI 的 ±1% 精度在温度变化后不足以覆盖这个预算。
五、bxCAN:邮箱、FIFO 与过滤器
F1 的 bxCAN 把协议处理硬件化,CPU 需要操作的只有三部分:
- 发送:3 个发送邮箱,装好帧后写请求寄存器即可,硬件负责排队、仲裁、重发
- 接收:2 条 FIFO、各 3 帧深,收到的帧先落 FIFO;应用层按需读取
- 过滤:28 组过滤器(双 CAN 时一分为二),每组可选 32 位掩码/列表或 16 位掩码/列表模式
过滤器是 CAN 驱动的关键:总线上同时存在大量 ID,若全部触发中断,CPU 无法承受。掩码模式回答”这一类我都要”(ID + mask,mask 的 0 位是无关位);列表模式回答”精确这几个”(每组精确匹配 2 个 32 位 ID)。过滤器命中的帧进入 FIFO0 或 FIFO1,映射关系可以自由安排,例如电机报文进 FIFO0 走较高优先级中断、诊断报文进 FIFO1 慢慢取。
错误管理同样由硬件完成。每个节点维护发送/接收错误计数器(TEC/REC):
| 状态 | 条件 | 行为 |
|---|---|---|
| Error Active | 计数 < 128 | 正常收发,出错发主动错误帧 |
| Error Passive | TEC 或 REC > 127 | 受限:出错只发被动错误标志,且发完要等 8 个隐性位 |
| Bus Off | TEC > 255 | 物理上离线,控制器不再收发 |
Bus-off 并不意味着节点损坏,而是被总线隔离了:它干扰总线的次数超过阈值。恢复条件是检测到 128 次连续 11 个隐性位(也可以由软件发出退出恢复模式的指令来加速)。产品中必须监控 bus-off 状态:现场出现接线短路或终端电阻缺失时,设备静默通常就是这个原因。
六、HAL 配置示例
500 kbps、FIFO0 收一帧中断通知:
CAN_HandleTypeDef hcan;
void CAN_Config(void)
{
__HAL_RCC_CAN1_CLK_ENABLE(); // 先开时钟
hcan.Instance = CAN1;
hcan.Init.Prescaler = 9; // tq = 250 ns @ PCLK1 36 MHz
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_5TQ; // 1+5+2=8tq → 500 kbps
hcan.Init.TimeSeg2 = CAN_BS2_2TQ; // 采样点 75%
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = ENABLE; // bus-off 后硬件自动恢复
hcan.Init.AutoWakeUp = ENABLE;
hcan.Init.AutoRetransmission = ENABLE; // 仲裁输/出错自动重发
hcan.Init.ReceiveFifoLocked = DISABLE; // FIFO 满时新帧覆盖旧帧
hcan.Init.TransmitFifoPriority= DISABLE; // 邮箱按 ID 优先级而不是请求顺序
HAL_CAN_Init(&hcan);
// 过滤器 0:32 位掩码,只收 ID=0x201 一类(掩码全 1 = 精确匹配)
CAN_FilterTypeDef f = {0};
f.FilterBank = 0;
f.FilterMode = CAN_FILTERMODE_IDMASK;
f.FilterScale = CAN_FILTERSCALE_32BIT;
f.FilterIdHigh = 0x201 << 5; // 32 位过滤寄存器:STD_ID 在 [31:21]
f.FilterIdLow = 0x0000;
f.FilterMaskIdHigh = 0x7FF << 5; // 低 11 位全比对
f.FilterMaskIdLow = 0x0000;
f.FilterFIFOAssignment = CAN_RX_FIFO0;
f.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan, &f);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
HAL_CAN_Start(&hcan);
}
// FIFO0 有帧待取 → 回调(在 ISR 上下文,快取快走)
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan_)
{
CAN_RxHeaderTypeDef hdr;
uint8_t data[8];
HAL_CAN_GetRxMessage(hcan_, CAN_RX_FIFO0, &hdr, data);
// ISR 里不做业务:拷走数据,置信号量/队列,业务在任务里做
// 中断到任务的交接方式见 [[freertos-semaphore-callback]]
}
两个工程习惯:AutoRetransmission 保持 ENABLE,失败自动重发是协议承诺的可靠性;发送用 HAL_CAN_AddTxMessage 前查 HAL_CAN_GetTxMailboxesFreeLevel,三只邮箱都满时等中断通知,不要轮询死等。
F1 的 CAN 没有 DMA 请求线(DMA1 的映射表里没有 CAN),所以数据搬运只能靠 FIFO 深度和中断的及时性。串口、SPI 这类有 DMA 通道的外设另见 DMA直接存储器访问详解。
七、常见问题与注意事项
| 问题 | 原因 | 解决 |
|---|---|---|
| 完全无 ACK、发送一直失败 | 总线单节点 / 终端电阻缺失 / 收发器没焊 | 万用表量总线 60 Ω,示波器看 CAN_H/L 摆幅 |
| 偶发 CRC 错误、误帧 | 位时序参数错、节点间晶振偏差大 | 统一 tq 划分,采样点 75–87.5% |
| 只在高负载时丢帧 | FIFO 满溢出 | 中断里快取;过滤器收窄;看 HAL_CAN_GetRxFifoFillLevel |
| 通信偶发中断后自行恢复 | 节点进入 bus-off 后自动恢复 | 监控 ESR,查接线/终端/干扰源 |
| 点对点不通,更换节点后正常 | 一端误配静默/环回模式 | 检查 CAN_MODE_NORMAL,别用上次的 LOOPBACK |
| 实验室通信正常,量产失败 | 低负载时误差预算没用完 | 按 87.5% 采样点 + 最坏晶振误差重算预算 |
| 需要发送超过 8 字节的数据 | 经典 CAN 上限 8 字节 | 拆帧 + 序号重组,或改用 CAN FD(单帧到 64 字节) |
总结
CAN 的四层各负责一件事:差分电气层保证信号能到达,线与仲裁决定谁先发送,短帧加 CRC 与 ACK 保证数据正确,位时序保证全网同步采样。bxCAN 把这四层都做成了硬件,软件需要处理的只有三处——位时序参数计算、过滤器划分、中断到任务的数据交接。与 UART 的点对点、I2C/SPI 的主从编址相比,CAN 的总线模型没有主节点、没有地址,只有优先级和报文内容。