一、引言

CAN(Controller Area Network)总线是汽车电子和工业控制领域最重要的通信协议之一。它诞生于 1983 年(Bosch 公司),至今已有 40 年历史,却仍然是现代汽车的核心通信骨架。从一辆普通家用轿车的 30+ 个 ECU(电子控制单元)到工业机器人关节的实时通信,CAN 总线的身影无处不在。

本文将全面解构 CAN 总线:

  • CAN 的物理层与差分信号

  • 仲裁机制与优先级策略

  • 帧格式(标准帧、扩展帧、CAN FD)

  • 位时序与波特率计算

  • 过滤器配置的深入理解

  • STM32 bxCAN 的实战配置

  • CAN 总线的工程陷阱与排查方法

平台:STM32F103(bxCAN,基本 CAN 控制器) 收发器:TJA1050 测试工具:CAN 分析仪(USB-CAN)


二、CAN 总线基础

2.1 为什么用 CAN?——对比其他总线

┌─────────────────────────────────────────────────────────┐
│          CAN vs 其他工业通信协议                          │
├──────────┬──────────┬──────────┬──────────┬─────────────┤
│  特性     │  CAN     │  RS-485  │  I2C     │  SPI        │
├──────────┼──────────┼──────────┼──────────┼─────────────┤
│ 通信方式  │ 多主     │ 主从     │ 主从     │ 主从        │
│ 错误检测  │ ★★★★★   │ ★☆☆☆☆   │ ★☆☆☆☆   │ ★☆☆☆☆      │
│ 最大节点  │ 无限制   │ 256      │ 127      │ 仅 1 从机   │
│ 实时性    │ ★★★★★   │ ★★☆☆☆   │ ★☆☆☆☆   │ ★★★★☆       │
│ 距离/速率 │ 40m@1M   │ 1200m@100k│ 1m@100k │ 0.1m@10M   │
│ 硬件成本  │ 中       │ 低       │ 极低     │ 极低        │
│ 适用场景  │ 汽车/工业│ 仪表     │ 板内通信 │ 板内高速    │
└──────────┴──────────┴──────────┴──────────┴─────────────┘
​
CAN 的核心优势:
  1. 多主模式——任何节点都可以主动发送,不需要主机轮询
  2. 仲裁机制——多个节点同时发送时,高优先级自动胜出
  3. 错误检测——5 种错误检测机制,错误率极低
  4. 帧内 ACK——所有接收方在帧内确认,无需额外应答帧

2.2 CAN 物理层——差分信号

  CAN_H ──══════════════════──
            ╲    ╱  ╲    ╱
             ╲  ╱    ╲  ╱     差分电压 = CAN_H - CAN_L
  CAN_L ─────╲╱──────╲╱──────
​
  显性位 (Dominant, 逻辑 0):
    CAN_H ≈ 3.5V, CAN_L ≈ 1.5V
    差分电压 ≈ 2.0V(驱动状态)
​
  隐性位 (Recessive, 逻辑 1):
    CAN_H ≈ 2.5V, CAN_L ≈ 2.5V
    差分电压 ≈ 0V(总线上所有节点释放驱动)
​
  ★ 关键原理:显性位覆盖隐性位!
    - 任何节点驱动显性位 → 总线为显性
    - 所有节点驱动隐性位 → 总线为隐性
    
    这是 CAN 仲裁机制的基础!

三、CAN 仲裁机制——最优雅的设计

3.1 位仲裁(Bit-wise Arbitration)

CAN 的核心设计:CSMA/CD-AMP(载波侦听多路访问/冲突检测-通过仲裁的优先级消息)
​
三个节点同时开始发送:
​
  起始  ID 位                          数据...
  ─────┬────┬────┬────┬────┬────┬────┬──
       │SOF │ ID10│ID9 │ID8 │ID7 │ID6 │ ...
  NodeA│ 0  │ 0  │ 0  │ 1  │ 1  │ 0  │ ...  (ID=0x0C...)
  NodeB│ 0  │ 0  │ 0  │ 1  │ 1  │ 1  │ ...  (ID=0x0E...)
  NodeC│ 0  │ 0  │ 1  │ 0  │ 0  │ 0  │ ...  (ID=0x20...)
       │    │    │    │    │    │    │
  总线  │ 0  │ 0  │ 0══│ 1  │ 1  │ 0══│ ...
       │    │    │ ║  │    │    │ ║  │
       │    │    │ ║  │    │    │ ║  │
       │    │    │NodeC 发送隐性(1)  │
       │    │    │但总线是显性(0)→   │
       │    │    │NodeC 检测到冲突→  │
       │    │    │★ NodeC 退出仲裁! │
       │    │    │                  NodeB 发送隐性(1)
       │    │    │                  但总线是显性(0)→
       │    │    │                  ★ NodeB 退出仲裁!
       │    │    │                  
       │    │    │                  ★ NodeA 赢得仲裁,继续发送
       │    │    │                  
仲裁结果:NodeA(ID=0x0C) > NodeB(ID=0x0E) > NodeC(ID=0x20)
         ID 越小,优先级越高!

3.2 仲裁的关键规则

规则 1:发送方同时监听总线
  → 发送隐性位(1)时,检测到总线为显性(0)
  → 说明有更高优先级的节点在发送 → 立即退出!
​
规则 2:仲裁失败方自动变为接收方
  → 不丢失数据,只是发送权被抢占
  → 等当前帧传输完毕后自动重试
​
规则 3:ID 越小,优先级越高
  → CAN ID 不是"地址",而是"优先级标识"
  → 设计系统时需要为紧急消息分配小 ID

四、CAN 帧格式详解

4.1 标准数据帧(11 位 ID)

  ┌────────────────────────── 标准数据帧 ──────────────────────────────┐
  │                                                                   │
  │ ┌──┐┌─────┐┌──┐┌──┐┌──────────┐┌──┐┌──┐┌──┐┌───────┐┌──┐┌──┐  │
  │ │SOF││ ID  ││RTR││IDE││ 控制字段  ││数据││CRC││ACK││EOF/IFS│  │
  │ │ 1 ││11位 ││ 1 ││ 1 ││  DLC(4)  ││0-8B││16 ││ 2 ││ 7+3  │  │
  │ └──┘└─────┘└──┘└──┘└──────────┘└──┘└──┘└──┘└───────┘└──┘└──┘  │
  │                                                                   │
  └───────────────────────────────────────────────────────────────────┘
​
各字段含义:
  SOF     — 起始位(Start Of Frame),固定为显性位(0),用于同步所有节点
  ID      — 11 位标识符(标准格式),同时用于仲裁和过滤
  RTR     — 远程帧请求位:0=数据帧,1=远程帧(请求数据)
  IDE     — 标识符扩展位:0=标准帧,1=扩展帧
  r0      — 保留位
  DLC     — 数据长度码(4 位,0~8,表示数据字节数)
  DATA    — 0~8 字节数据(长度由 DLC 指定)
  CRC     — 15 位 CRC + 1 位分隔符(校验从 SOF 到 DATA 的所有位)
  ACK     — 1 位 ACK 槽 + 1 位分隔符(接收方在 ACK 槽发显性位确认)
  EOF     — 7 位帧结束(End Of Frame)
  IFS     — 3 位帧间间隔(Inter Frame Space)

4.2 扩展帧(29 位 ID)vs CAN FD

标准帧 vs 扩展帧(关键差异):
  ┌──────────┬────────────┬──────────────┐
  │           │ 标准帧(2.0A)│ 扩展帧(2.0B) │
  ├──────────┼────────────┼──────────────┤
  │ ID 位数    │ 11 位      │ 29 位        │
  │ IDE 位     │ 0 (显性)   │ 1 (隐性)     │
  │ SRR 位     │ 无         │ 在 RTR 位置   │
  │ 最大 ID    │ 0x7FF      │ 0x1FFFFFFF   │
  │ 兼容性      │ 所有节点    │ 需要节点支持  │
  └──────────┴────────────┴──────────────┘
​
CAN FD (Flexible Data-rate):
  ┌──────────┬────────────┬──────────────┐
  │           │ CAN 2.0     │ CAN FD       │
  ├──────────┼────────────┼──────────────┤
  │ 数据速率    │ 最高 1Mbps  │ 仲裁段 1Mbps │
  │ 数据段      │ 同一个速率  │ ★ 最高 8Mbps │
  │ 数据长度    │ 0~8 字节   │ ★ 0~64 字节 │
  │ 兼容性      │ —          │ 需 FD 控制器  │
  └──────────┴────────────┴──────────────┘

五、CAN 位时序——正确计算波特率

5.1 位时间的组成

  ┌─────────────────────────────────────────────┐
  │           一个位时间 (Time Quanta)           │
  │                                             │
  │  ← SYNC_SEG →←── PROP_SEG ──→←── PHASE_SEG1 ──→← PHASE_SEG2 →│
  │     1 TQ        1~8 TQ         1~8 TQ         1~8 TQ         │
  │                                             │
  │  ←──────────────── 采样点 ──────────────────→│
  │                   (一般在 75%~87% 处)         │
  └─────────────────────────────────────────────┘
​
  CAN 位时间 = (1 + PROP_SEG + PHASE_SEG1 + PHASE_SEG2) × TQ
  TQ = BRP / CAN_CLK
​
  波特率 = 1 / 位时间

5.2 STM32F103 CAN 波特率计算

// CAN 波特率计算(APB1 = 36MHz)
// 目标:500kbps(工业控制最常用)
​
// CAN 时钟 = APB1 = 36MHz
// 1 TQ = (BRP + 1) / 36MHz
​
// 位时间 = (1 + BS1 + BS2) TQ
// 选取:BRP=3, BS1=11, BS2=4
// TQ = (3+1)/36MHz = 111.1ns
// 位时间 = (1+11+4) × 111.1ns = 16 × 111.1ns = 1.778μs
// 波特率 = 1/1.778μs ≈ 562kbps(接近 500kbps,满足需求)
​
// 精确 500kbps 的配置:
// BRP=5, BS1=6, BS2=4, SJW=1
// TQ = (5+1)/36MHz = 166.67ns
// 位时间 = (1+6+4) × 166.67ns = 12 × 166.67ns = 2.0μs
// 波特率 = 1/2.0μs = 500kbps ✓
​
void CAN_BaudRate_500k(void)
{
    CAN_InitTypeDef can;
    
    can.CAN_Prescaler     = 5;    // BRP
    can.CAN_Mode          = CAN_Mode_Normal;
    can.CAN_SJW           = CAN_SJW_1tq;    // 同步跳转宽度
    can.CAN_BS1           = CAN_BS1_7tq;    // BS1 = 7(包含 PROP_SEG)
    can.CAN_BS2           = CAN_BS2_4tq;    // BS2 = 4
    can.CAN_TTCM          = DISABLE;
    can.CAN_ABOM          = ENABLE;         // ★ 自动离线恢复
    can.CAN_AWUM          = ENABLE;         // ★ 自动唤醒
    can.CAN_NART          = DISABLE;        // 自动重传
    can.CAN_RFLM          = DISABLE;
    can.CAN_TXFP          = DISABLE;
    
    CAN_Init(CAN1, &can);
}

六、CAN 过滤器——理解才能用好

6.1 过滤器的工作原理

CAN 过滤器用于硬件级别的消息筛选——过滤发生在硬件层,CPU 完全不用处理不感兴趣的消息。

STM32F103 的 bxCAN 有 14 个过滤器组(28 个过滤器),
可在"标识符列表模式"和"掩码模式"之间选择。

掩码模式(Mask Mode):
  ┌─────────────────────────────────────────────┐
  │  滤波器寄存器 0 (CAN_FxR1):  期望的 ID 值    │
  │  滤波器寄存器 1 (CAN_FxR2):  掩码            │
  │                                             │
  │  掩码 bit=1 → 该位必须匹配                   │
  │  掩码 bit=0 → 该位不关心(Don't Care)       │
  │                                             │
  │  示例:接收 0x100~0x1FF 的所有帧             │
  │    FxR1 = 0x100 (期望 ID=0x100)             │
  │    FxR2 = 0xF00 (掩码,只关心高 4 位)        │
  │    → 匹配:0x100, 0x101, ..., 0x1FF         │
  └─────────────────────────────────────────────┘

标识符列表模式(List Mode):
  ┌─────────────────────────────────────────────┐
  │  滤波器寄存器 0 (CAN_FxR1):  ID 1           │
  │  滤波器寄存器 1 (CAN_FxR2):  ID 2           │
  │                                             │
  │  只接收这两个具体 ID 的帧                     │
  │  → 精确匹配,不处理无关帧                     │
  └─────────────────────────────────────────────┘

6.2 过滤器配置代码

// 场景:接收 ID=0x100, 0x200, 0x300~0x3FF 的帧
void CAN_Filter_Config(void)
{
    CAN_FilterInitTypeDef filter;
    
    // === 过滤器 0:精确匹配 ID=0x100 和 ID=0x200(列表模式)===
    filter.CAN_FilterNumber        = 0;
    filter.CAN_FilterMode          = CAN_FilterMode_IdList;   // 列表模式
    filter.CAN_FilterScale         = CAN_FilterScale_32bit;   // 32 位模式
    filter.CAN_FilterIdHigh        = (0x100 << 5);            // STDID[10:3]
    filter.CAN_FilterIdLow         = (0x200 << 5);
    filter.CAN_FilterMaskIdHigh    = 0;  // 列表模式不用掩码
    filter.CAN_FilterMaskIdLow     = 0;
    filter.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
    filter.CAN_FilterActivation    = ENABLE;
    CAN_FilterInit(&filter);
    
    // === 过滤器 1:接收 0x300~0x3FF(掩码模式)===
    filter.CAN_FilterNumber        = 1;
    filter.CAN_FilterMode          = CAN_FilterMode_IdMask;   // 掩码模式
    filter.CAN_FilterScale         = CAN_FilterScale_32bit;
    filter.CAN_FilterIdHigh        = (0x300 << 5);            // 期望 ID
    filter.CAN_FilterIdLow         = 0;
    filter.CAN_FilterMaskIdHigh    = (0xF00 << 5);            // 掩码:只关心高 4 位
    filter.CAN_FilterMaskIdLow     = 0;
    filter.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
    filter.CAN_FilterActivation    = ENABLE;
    CAN_FilterInit(&filter);
}

七、CAN 发送与接收实战

7.1 发送 CAN 消息

CanTxMsg tx_msg;

void CAN_SendMessage(uint32_t id, uint8_t *data, uint8_t len)
{
    tx_msg.StdId    = id;                 // 标准 ID(11 位)
    tx_msg.ExtId    = 0;                  // 扩展 ID(不用)
    tx_msg.IDE      = CAN_Id_Standard;    // 标准帧
    tx_msg.RTR      = CAN_RTR_Data;       // 数据帧
    tx_msg.DLC      = len;                // 数据长度
    
    for (int i = 0; i < len && i < 8; i++) {
        tx_msg.Data[i] = data[i];
    }
    
    // 选择空闲的发送邮箱
    uint8_t mailbox = CAN_Transmit(CAN1, &tx_msg);
    
    // 等待发送完成(可改为中断方式)
    while (CAN_TransmitStatus(CAN1, mailbox) != CAN_TxStatus_Ok);
}

7.2 中断方式接收

void CAN_RX_Init(void)
{
    CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);  // FIFO0 消息挂起中断
    NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);     // CAN1 RX0 中断
}

void USB_LP_CAN1_RX0_IRQHandler(void)
{
    CanRxMsg rx_msg;
    
    // 读取接收到的消息
    CAN_Receive(CAN1, CAN_FIFO0, &rx_msg);
    
    // 处理消息
    uint32_t msg_id = rx_msg.IDE == CAN_Id_Standard 
                      ? rx_msg.StdId : rx_msg.ExtId;
    
    CAN_MessageHandler(msg_id, rx_msg.Data, rx_msg.DLC);
}

八、CAN 工程陷阱与排查

8.1 常见问题速查

现象 原因 解决方法
发送失败,错误计数增加 总线无其他节点应答 ACK 至少需要 2 个节点才能通信
TEC > 255,进入 Bus-Off 严重错误累积 检查终端电阻、波特率、接线
接收数据乱码 波特率不匹配 用示波器测位时间,确认配置一致
偶尔丢帧 过滤器配置过严/CPU 太忙 加宽过滤器或提高中断优先级
CAN 一直报错 未连接终端电阻 120Ω 终端电阻必不可少!

8.2 终端电阻的物理原理

没有终端电阻时:

  CAN_H ──○──────────────○──
           │ 信号反射!   │
  CAN_L ──○──────────────○──

总线的两端:信号到达端点时发生反射!
反射信号与入射信号叠加 → 电平畸变 → 通信失败

加上 120Ω 终端电阻后:

  CAN_H ──○──────┬──────○──
           │    120Ω   │
  CAN_L ──○──────┴──────○──

120Ω 电阻匹配双绞线特性阻抗 → 无反射 → 信号干净

★ 规则:总线两端各接一个 120Ω 终端电阻(总计 60Ω 并联)

8.3 错误状态处理

// 监控 CAN 错误状态
void CAN_ErrorMonitor(void)
{
    uint8_t tec = (CAN1->ESR >> 16) & 0xFF;  // 发送错误计数器
    uint8_t rec = CAN1->ESR & 0xFF;           // 接收错误计数器
    
    if (tec > 96) {
        printf("CAN WARNING: TEC=%d (接近 Bus-Off)\r\n", tec);
    }
    
    // Bus-Off 恢复
    if (CAN_GetFlagStatus(CAN1, CAN_FLAG_BOF)) {
        printf("CAN BUS-OFF! 尝试恢复...\r\n");
        
        // 清除 Bus-Off 标志,重新初始化
        CAN1->MCR |= CAN_MCR_ABOM;  // 自动离线恢复
        // 或手动:CAN_Init(CAN1, &can_config);
    }
}

九、CAN 高层协议简介

CAN 本身只定义了物理层和数据链路层。实际应用中需要高层协议:

协议 应用领域 特点
CANopen 工业自动化、机器人 对象字典、PDO/SDO、NMT 状态机
J1939 商用车、柴油机 29 位 ID、PGN 参数组、多包传输
DeviceNet 工厂自动化 基于 CIP 协议
ISO 15765 (UDS) 汽车诊断 27/29 位 ID + 多帧传输
自定义协议 简单系统 固定 ID → 功能映射表

十、总结

要点 核心理解
仲裁 ID 越小优先级越高,硬件自动仲裁,无需主机
帧结构 从 SOF 到 EOF 的每一段都有明确功能
波特率 位时间 = (1+BS1+BS2)×(BRP+1)/CAN_CLK
过滤器 硬件过滤——CPU 只处理有意义的帧
终端电阻 总线两端各 120Ω——没有它 CAN 不工作!
错误处理 5 种错误检测 + 自动重传 + Bus-Off 保护

一句话总结: CAN 总线的核心价值在于实时性、可靠性和多主通信。理解仲裁机制是理解 CAN 的钥匙,用好过滤器是工程实践的关键。


Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐