CAN协议完全指南

CAN总线协议完全指南

目录

  1. CAN协议简介
  2. CAN总线基础知识
  3. 显性位与隐性位详解
  4. CAN报文格式
  5. CAN总线仲裁机制
  6. CAN错误检测机制
  7. CAN节点状态管理
  8. 发布-订阅通信模式
  9. 硬件与软件分工
  10. 实际编程示例
  11. CAN配置与调试
  12. 常见问题与解决方案

1. CAN协议简介

1.1 什么是CAN?

CAN(Controller Area Network,控制器局域网络)是一种串行通信协议,由德国BOSCH公司在1980年代为汽车电子系统开发。它是一种多主站、广播式的通信总线,允许多个设备在同一条线路上进行实时通信。

核心特点:

  • 多主站:任何节点都可以发起通信
  • 广播式:所有节点都能接收到总线上的消息
  • 实时性强:最高速度可达1Mbps
  • 可靠性高:具有强大的错误检测和处理机制
  • 成本低:只需2根线就能连接所有设备

1.2 为什么需要CAN?

传统点对点连接的问题:

传统汽车布线:
  ECU ─────── 仪表盘
  ECU ─────── ABS
  ECU ─────── 安全气囊
  ...
  
问题:
  - 需要大量线束
  - 连接器多,故障点多
  - 扩展困难
  - 成本高

CAN总线方案:

CAN总线方案:
         ┌─── ECU
         │
  总线 ──┼─── 仪表盘
         │
         ├─── ABS
         │
         └─── 安全气囊
         
优势:
  - 只需2根线
  - 连接器少
  - 易于扩展
  - 成本低

1.3 应用场景

  • 汽车:发动机控制、ABS、安全气囊、车身电子、仪表盘
  • 工业自动化:机器人控制、PLC通信、传感器网络
  • 农业机械:拖拉机、收割机控制系统
  • 医疗设备:手术设备、监护仪器通信
  • 船舶:导航系统、发动机控制
  • 电梯:控制系统通信

2. CAN总线基础知识

2.1 物理层结构

CAN总线使用差分信号传输,由两根线组成:

网络拓扑:

设备1 ───┐
         │
设备2 ───┼─── CAN_H (高电平线)
         │    CAN_L (低电平线)
设备3 ───┤
         │
设备4 ───┘

终端电阻:120Ω     120Ω
        (两端)

关键要点:

  1. 使用双绞线传输(抗干扰)
  2. 差分信号:通过两根线的电压差表示0和1
  3. 总线两端需要120Ω终端电阻(防止信号反射)
  4. 最大总线长度取决于波特率

2.2 电气特性

显性位(Dominant,逻辑0)
  • CAN_H ≈ 3.5V
  • CAN_L ≈ 1.5V
  • 差分电压 ≈ 2V
  • 需要主动驱动总线
隐性位(Recessive,逻辑1)
  • CAN_H ≈ 2.5V
  • CAN_L ≈ 2.5V
  • 差分电压 ≈ 0V
  • 总线处于放松状态

重要规则:显性位优先级高于隐性位(0能覆盖1)

2.3 CAN标准

特性CAN 2.0A(标准帧)CAN 2.0B(扩展帧)CAN FD
标识符长度11位29位11/29位
可用ID数量2048个约5.37亿个同左
最大数据长度8字节8字节64字节
数据速率最高1Mbps最高1Mbps最高8Mbps
应用场景简单系统复杂系统高速大数据

2.4 波特率与距离

波特率最大距离应用场景
1 Mbps40米高速短距离
500 kbps100米汽车内部网络
250 kbps250米工业控制
125 kbps500米长距离通信
50 kbps1000米超长距离

3. 显性位与隐性位详解

3.1 基本概念

显性位和隐性位是CAN总线上表示逻辑0和逻辑1的两种电平状态,这是理解CAN协议的关键。

显性位(Dominant) = 逻辑 0
隐性位(Recessive) = 逻辑 1

3.2 命名由来

命名来自生物学的显隐性遗传概念:

  • 显性位(Dominant):具有"支配性",能够覆盖隐性位
  • 隐性位(Recessive):具有"隐退性",会被显性位覆盖

核心规则:当总线上同时出现显性位和隐性位时,显性位获胜!

3.3 电气特性详解

隐性位(逻辑1)
电平状态:
CAN_H: 2.5V ━━━━━━━━━━━━━━━━
                              差值 ≈ 0V
CAN_L: 2.5V ━━━━━━━━━━━━━━━━

特点:
  - 两根线电压相同
  - 差分电压接近0V
  - 总线处于放松状态
  - 节点不主动驱动总线(高阻态)
显性位(逻辑0)
电平状态:
CAN_H: 3.5V ━━━━━━━━━━━━━━━━
                              差值 ≈ 2V
CAN_L: 1.5V ━━━━━━━━━━━━━━━━

特点:
  - 两根线电压差明显
  - 差分电压约2V
  - 节点主动驱动总线
  - 需要能量维持

3.4 线与(Wired-AND)特性

CAN总线具有线与特性,这是实现仲裁的基础:

真值表:
节点A发送  节点B发送  总线实际状态  说明
   0         0          0          都发显性位
   0         1          0          显性位获胜
   1         0          0          显性位获胜
   1         1          1          都发隐性位

规则:只要有任何一个节点发送显性位(0),总线就是显性位(0)

3.5 物理实现原理

总线连接示意:

节点A                    节点B
  │                        │
  ├─ CAN_TX (发送)         ├─ CAN_TX
  ├─ CAN_RX (接收)         ├─ CAN_RX
  │                        │
  └─ CAN收发器             └─ CAN收发器
       │                        │
       └────────┬───────────────┘
                │
         CAN_H ─┴─ CAN_L
                │
            终端电阻 120Ω

发送隐性位时:
  - 节点不驱动总线(高阻态)
  - 总线被上拉/下拉电阻拉到中间电平

发送显性位时:
  - 节点主动驱动总线
  - CAN_H拉高,CAN_L拉低
  - 产生明显的差分电压

3.6 为什么这样设计?

优势1:实现非破坏性仲裁
传统冲突检测(如以太网):
  - 检测到冲突 → 停止发送
  - 随机等待
  - 重新发送
  - 效率低

CAN非破坏性仲裁:
  - 边发送边仲裁
  - 失败者立即停止
  - 获胜者继续发送
  - 无需重传,效率高
优势2:天然的优先级机制
ID越小 → 越早出现显性位(0) → 优先级越高

例如:
  ID=0x000:最高优先级(紧急消息)
  ID=0x100:高优先级(发动机数据)
  ID=0x200:中优先级(车速数据)
  ID=0x7FF:最低优先级(诊断信息)
优势3:容错性好
节点故障时:
  - 如果故障节点发送隐性位:不影响总线
  - 如果故障节点持续发送显性位:其他节点能检测到错误
  - 错误节点会被自动隔离(Bus Off机制)

3.7 记忆技巧

方法1:联想"强弱"

  • 显性位 = 强势 → 能覆盖别人 → 逻辑0
  • 隐性位 = 弱势 → 会被覆盖 → 逻辑1

方法2:联想"主动被动"

  • 显性位 = 主动驱动 → 需要能量 → 逻辑0
  • 隐性位 = 被动放松 → 不需能量 → 逻辑1

方法3:联想"说话"

  • 显性位 = 大声说话 → 能压过别人 → 逻辑0
  • 隐性位 = 保持安静 → 会被压过 → 逻辑1

4. CAN报文格式

4.1 标准帧结构(CAN 2.0A)

完整的CAN标准帧:
┌────┬─────┬─────┬─────┬────────┬──────┬─────┬─────┬─────┐
│SOF │ ID  │ RTR │ IDE │ r0 DLC │ DATA │ CRC │ ACK │ EOF │
│1位 │11位 │ 1位 │ 1位 │ 1+4位  │ 0-8B │ 16位│ 2位 │ 7位 │
└────┴─────┴─────┴─────┴────────┴──────┴─────┴─────┴─────┘

4.2 各字段详细说明

SOF(Start of Frame)- 帧起始
  • 长度:1位
  • :显性位(0)
  • 作用:标志一帧数据的开始,同步所有节点
标识符(Identifier)- ID
  • 长度:11位(标准帧)或29位(扩展帧)
  • 作用
    1. 识别消息类型(不是设备地址!)
    2. 决定优先级(ID值越小,优先级越高)
  • 示例
    • ID=0x100:发动机转速
    • ID=0x200:车速信息
    • ID=0x300:温度数据

重要理解:CAN的ID表示消息类型,不是目标地址

RTR(Remote Transmission Request)- 远程传输请求
  • 长度:1位
    • 0(显性)= 数据帧
    • 1(隐性)= 远程帧(请求数据)
IDE(Identifier Extension)- 标识符扩展位
  • 长度:1位
    • 0 = 标准帧(11位ID)
    • 1 = 扩展帧(29位ID)
r0/r1 - 保留位
  • 长度:1位
  • :显性位(0)
  • 作用:为将来扩展预留
DLC(Data Length Code)- 数据长度码
  • 长度:4位
  • :0-8(表示数据字节数)
  • 示例:DLC=5 表示有5个字节数据
数据域(Data Field)
  • 长度:0-8字节(CAN 2.0)或0-64字节(CAN FD)
  • 内容:实际要传输的数据
  • 字节序:通常使用大端序(Big-Endian)
  • 示例
    发动机转速:[0x12, 0x34] → 0x1234 = 4660 RPM
    温度数据:[0x5A] → 90°C
    多字节数据:[0x01, 0x02, 0x03, 0x04]
    
CRC(Cyclic Redundancy Check)- 循环冗余校验
  • 长度:15位CRC码 + 1位界定符
  • 作用:检测传输错误
  • 原理
    1. 发送方根据前面的数据计算CRC值
    2. 接收方重新计算CRC并比对
    3. 不匹配则报告CRC错误
ACK(Acknowledgement)- 应答
  • 长度:1位ACK槽 + 1位界定符
  • 工作方式
    1. 发送节点在ACK槽发送隐性位(1)
    2. 接收节点正确接收后,发送显性位(0)覆盖
    3. 发送节点检测到显性位,确认有节点收到
  • 注意:ACK不是回传数据,只是1位的确认信号
EOF(End of Frame)- 帧结束
  • 长度:7位
  • :7个连续隐性位(1111111)
  • 作用:标志帧结束
IFS(Interframe Space)- 帧间隔
  • 长度:3位
  • :3个隐性位
  • 作用:帧与帧之间的最小间隔时间

4.3 扩展帧结构(CAN 2.0B)

扩展帧格式:
┌────┬──────┬─────┬─────┬─────┬──────┬─────┬────────┬──────┬─────┬─────┬─────┐
│SOF │ID(11)│ SRR │ IDE │ID(18)│ RTR │ r1  │ r0 DLC │ DATA │ CRC │ ACK │ EOF │
│1位 │ 11位 │ 1位 │ 1位 │ 18位 │ 1位 │ 1位 │ 1+4位  │ 0-8B │ 16位│ 2位 │ 7位 │
└────┴──────┴─────┴─────┴─────┴─────┴─────┴────────┴──────┴─────┴─────┴─────┘

总ID长度:11 + 18 = 29位

扩展帧特点

  • ID空间更大(约5.37亿个ID)
  • 与标准帧可以共存
  • 标准帧优先级高于相同ID值的扩展帧

5. CAN总线仲裁机制

5.1 什么是仲裁?

当多个节点同时想要发送数据时,CAN使用非破坏性位仲裁机制决定谁先发送,无需检测冲突和重传。

5.2 仲裁原理

核心思想:边发送边监听,发现冲突立即退出

仲裁规则:
1. 所有节点同时开始发送
2. 每发送1位,同时监听总线
3. 如果发送隐性位(1)但检测到显性位(0)
   → 说明有其他节点在发送显性位
   → 仲裁失败,立即停止发送
4. ID值越小,越早出现显性位,优先级越高

5.3 仲裁过程详解

场景:三个节点同时发送
节点A:想发送 ID=0x100 = 0001 0000 0000
节点B:想发送 ID=0x200 = 0010 0000 0000
节点C:想发送 ID=0x150 = 0001 0101 0000
逐位仲裁过程
位序号  A发送  B发送  C发送  总线  A状态  B状态  C状态
────────────────────────────────────────────────────
SOF      0      0      0     0    继续   继续   继续
ID[10]   0      0      0     0    继续   继续   继续
ID[9]    0      0      0     0    继续   继续   继续
ID[8]    0      0      0     0    继续   继续   继续
ID[7]    1      1      1     1    继续   继续   继续
ID[6]    0      1      0     0    继续   失败✗  继续
         ↑      ↑      ↑     ↑           停止
ID[5]    0      -      1     0    继续    -    失败✗
         ↑             ↑     ↑                 停止
ID[4]    0      -      -     0    继续    -     -
...后续只有A在发送...

结果:节点A获胜,继续发送完整帧
      节点B、C转为接收模式

5.4 仲裁的关键时刻

节点B在ID[6]位的判断:
  1. 我要发送:1(隐性位)
  2. 我不驱动总线(高阻态)
  3. 我监听总线:检测到0(显性位)
  4. 结论:有其他节点在发送显性位
  5. 动作:仲裁失败,立即停止发送,转为接收模式

节点A在ID[6]位的判断:
  1. 我要发送:0(显性位)
  2. 我主动驱动总线为显性位
  3. 我监听总线:检测到0(显性位)
  4. 结论:符合预期,继续发送

5.5 仲裁的优势

优势1:无时间浪费
传统冲突检测(CSMA/CD):
  节点A ━━━发送━━━ 碰撞!停止 ━━等待━━━ 重新发送━━━
  节点B ━━━发送━━━ 碰撞!停止 ━━等待━━━ 重新发送━━━
                    ↑
                浪费时间!

CAN仲裁(CSMA/CR):
  节点A ━━━发送━━━━━━━━━━━━━━继续发送完成━━━
  节点B ━━━发送━━━ 检测到冲突,立即停止,转为接收
                    ↑
                无浪费!前面发的数据有效!
优势2:确定性优先级
优先级完全由ID决定:
  ID=0x000:最高优先级(紧急消息)
  ID=0x100:高优先级(发动机控制)
  ID=0x200:中优先级(车速信息)
  ID=0x7FF:最低优先级(诊断数据)

特点:
  - 优先级固定,可预测
  - 实时性有保障
  - 适合安全关键系统
优势3:高效利用带宽
无需:
  - 冲突检测时间
  - 随机等待时间
  - 重传开销

结果:
  - 带宽利用率高
  - 延迟可预测
  - 吞吐量大

5.6 仲裁失败后的处理

仲裁失败的节点:
  1. 立即停止发送
  2. 转为接收模式
  3. 接收获胜节点的完整帧
  4. 等待总线空闲
  5. 重新尝试发送
  6. 再次参与仲裁

注意:
  - 不需要随机等待
  - 下次仲裁时优先级不变
  - 如果ID相同会再次冲突

5.7 相同ID的问题

重要规则:禁止两个节点发送相同的ID!

如果两个节点发送相同ID:
  1. 仲裁阶段无法分出胜负
  2. 两个节点都认为自己赢了
  3. 都继续发送
  4. 在数据字段产生冲突
  5. 触发位错误
  6. 发送错误帧
  7. 循环往复,可能导致Bus Off

解决方案:
  - 系统设计时分配唯一ID
  - 每个消息类型只能由一个节点发送
  - 使用DBC文件管理ID分配

6. CAN错误检测机制

CAN具有5种错误检测机制和完善的错误管理,确保数据传输的可靠性。

6.1 五种错误检测机制

1. 位错误(Bit Error)
  • 检测方法:发送节点监听总线,发现发送的位与总线上的位不一致
  • 例外情况
    • 仲裁期间(正常现象)
    • ACK槽(接收节点会覆盖)
  • 示例
    发送节点发送:1(隐性位)
    总线实际状态:0(显性位)
    非仲裁期间 → 位错误!
    
2. 填充错误(Stuff Error)
  • 位填充规则:连续5个相同位后,必须插入1个相反的位
  • 检测方法:接收到连续6个相同位
  • 示例
    原始数据:  1 1 1 1 1 0 0 0 0 0
    填充后:    1 1 1 1 1 0 0 0 0 0 0 1
                          ↑插入     ↑插入
    
    如果收到:  1 1 1 1 1 1(6个连续1)
    → 填充错误!
    
  • 作用:保证足够的边沿用于时钟同步
3. CRC错误(CRC Error)
  • 检测方法:接收方计算的CRC与收到的CRC不匹配
  • 原因:数据在传输中被干扰或损坏
  • CRC多项式:x^15 + x^14 + x^10 + x^8 + x^7 + x^4 + x^3 + 1
4. 格式错误(Form Error)
  • 检测方法:固定格式的位出现错误值
  • 示例
    • EOF应该是7个隐性位,但出现了显性位
    • CRC界定符应该是隐性位,但是显性位
    • ACK界定符格式错误
5. 应答错误(ACK Error)
  • 检测方法:发送节点在ACK槽未检测到显性位
  • 原因
    • 没有其他节点在总线上
    • 所有接收节点都检测到错误
    • 总线故障

6.2 错误帧(Error Frame)

当节点检测到错误时,会发送错误帧:

错误帧结构:
┌──────────────┬────────────┐
│ 错误标志     │ 错误界定符  │
│ 6位相同位    │ 8个隐性位   │
└──────────────┴────────────┘

主动错误标志:6个连续显性位(000000)
被动错误标志:6个连续隐性位(111111)

错误帧的作用

  1. 通知所有节点当前帧有错误
  2. 破坏当前帧(6个连续显性位违反填充规则)
  3. 所有节点丢弃当前帧
  4. 发送节点自动重传

6.3 错误计数器

每个CAN节点维护两个错误计数器:

TEC(Transmit Error Counter):发送错误计数
REC(Receive Error Counter):接收错误计数

计数规则:
  发送错误:TEC +8
  接收错误:REC +1
  成功发送:TEC -1
  成功接收:REC -1(如果REC > 0)
  
注意:
  - 发送错误惩罚更重(+8)
  - 成功传输会减少计数器
  - 计数器不会小于0

7. CAN节点状态管理

7.1 三种节点状态

CAN节点根据错误计数器的值在三种状态间转换:

状态转换图:

        ┌─────────────┐
        │Error Active │ (主动错误状态)
        │  正常工作    │
        └──────┬───────┘
               │ TEC或REC > 127
               ▼
        ┌─────────────┐
        │Error Passive│ (被动错误状态)
        │  受限工作    │
        └──────┬───────┘
               │ TEC > 255
               ▼
        ┌─────────────┐
        │  Bus Off    │ (总线关闭)
        │   离线      │
        └─────────────┘

7.2 状态详解

Error Active(主动错误状态)
  • 条件:TEC ≤ 127 且 REC ≤ 127
  • 特点
    • 正常工作状态
    • 可以发送和接收数据
    • 检测到错误时发送主动错误标志(6个显性位)
    • 主动错误标志会破坏总线上的帧
Error Passive(被动错误状态)
  • 条件:TEC > 127 或 REC > 127(但TEC ≤ 255)
  • 特点
    • 受限工作状态
    • 可以发送和接收数据
    • 检测到错误时发送被动错误标志(6个隐性位)
    • 被动错误标志不会破坏总线(可能被其他节点覆盖)
    • 发送后需要额外等待时间(Suspend Transmission)
Bus Off(总线关闭)
  • 条件:TEC > 255
  • 特点
    • 节点完全离线
    • 不能发送或接收数据
    • 不参与总线活动
    • 需要手动或自动恢复

7.3 状态转换条件

Error Active → Error Passive:
  - TEC > 127 或 REC > 127

Error Passive → Error Active:
  - TEC ≤ 127 且 REC ≤ 127

Error Passive → Bus Off:
  - TEC > 255

Bus Off → Error Active:
  - 检测到128次11个连续隐性位(总线空闲)
  - 或软件复位

7.4 错误管理的意义

目的:
  1. 隔离故障节点
     - 频繁出错的节点自动进入Bus Off
     - 不影响其他节点正常工作
  
  2. 保护总线
     - 防止故障节点持续干扰总线
     - 维护网络稳定性
  
  3. 自我诊断
     - 通过错误计数器判断节点健康状况
     - 及时发现硬件或软件问题

8. 发布-订阅通信模式

8.1 CAN的通信模式

CAN采用**发布-订阅(Publish-Subscribe)**通信模式,类似于DDS、MQTT等现代通信协议。

传统点对点通信:
  发送者 ──→ 接收者A
  发送者 ──→ 接收者B
  发送者 ──→ 接收者C
  
  问题:
    - 发送者需要知道所有接收者
    - 扩展困难
    - 耦合度高

CAN发布-订阅:
  发送者 ──→ [ID: 0x100] ──→ 接收者A(订阅0x100)
                         ──→ 接收者B(订阅0x100)
                         ──→ 接收者C(订阅0x100)
                         ✗  接收者D(不订阅)
  
  优势:
    - 发送者不需要知道接收者
    - 易于扩展
    - 解耦

8.2 ID的真正含义

重要理解:CAN的ID不是"目标地址",而是"消息类型"!

错误理解:
  ID=0x100 表示"发给设备100"
  
正确理解:
  ID=0x100 表示"这是发动机转速数据"
  所有对发动机转速感兴趣的节点都会接收

8.3 广播特性

CAN是广播式通信:
  - 所有节点都能收到所有消息
  - 通过过滤器选择感兴趣的消息
  - 不关心的消息被硬件自动丢弃
示例:汽车CAN网络
场景:发动机ECU发送转速数据

发送:
  发动机ECU → ID=0x100, DATA=[转速数据]

接收:
  ✓ 仪表盘:需要显示转速,接收
  ✓ 诊断接口:需要记录数据,接收
  ✗ ABS模块:不关心转速,过滤掉
  ✗ 车身控制:不关心转速,过滤掉

实现:
  - 仪表盘和诊断接口配置过滤器接收ID=0x100
  - ABS和车身控制不配置此过滤器

8.4 过滤器机制

硬件过滤器
// 配置CAN过滤器
void setup_can_filter() {
    // 过滤器1:只接收ID=0x100
    CAN_FilterConfig filter1;
    filter1.id = 0x100;
    filter1.mask = 0x7FF;  // 精确匹配
    can_set_filter(1, &filter1);
    
    // 过滤器2:接收ID=0x200-0x20F
    CAN_FilterConfig filter2;
    filter2.id = 0x200;
    filter2.mask = 0x7F0;  // 匹配高8位
    can_set_filter(2, &filter2);
    
    // 其他ID的消息会被硬件自动丢弃
}
软件过滤
// 接收回调函数
void on_can_message(struct can_frame *frame) {
    switch(frame->can_id) {
        case 0x100:  // 发动机转速
            handle_engine_rpm(frame->data);
            break;
            
        case 0x200:  // 车速
            handle_vehicle_speed(frame->data);
            break;
            
        case 0x300:  // 温度
            handle_temperature(frame->data);
            break;
            
        default:
            // 不关心的消息,忽略
            break;
    }
}

8.5 发布-订阅的优势

优势1:灵活性
场景:增加新的显示屏

传统方式:
  - 需要修改发送节点代码
  - 需要配置目标地址
  - 系统需要重新测试

CAN方式:
  - 新显示屏配置过滤器接收需要的ID
  - 发送节点不需要任何改动
  - 即插即用
优势2:可靠性
场景:仪表盘损坏

传统点对点:
  - 发送给仪表盘的数据丢失
  - 诊断接口收不到数据
  - 系统功能受影响

CAN广播:
  - 仪表盘坏了不影响其他节点
  - 诊断接口仍然能收到数据
  - 系统继续工作
优势3:效率
场景:多个节点需要同一数据

传统方式:
  - 需要发送多次
  - 占用带宽
  - 延迟增加

CAN方式:
  - 发送一次
  - 所有感兴趣的节点都收到
  - 高效利用带宽

8.6 与DDS的对比

特性DDSCAN
通信模式发布-订阅发布-订阅
数据标识Topic名称CAN ID
广播性质多播/广播广播
解耦性完全解耦完全解耦
过滤机制Topic过滤ID过滤
优先级QoS策略ID值(小=高)
网络层IP网络CAN总线
数据大小可达MB8B/64B
应用场景分布式系统嵌入式实时系统

9. 硬件与软件分工

9.1 系统层次结构

CAN通信系统的层次:

┌─────────────────────────────────────┐
│  应用层(用户程序)                  │  ← 您的代码
│  - 决定发送什么数据                  │
│  - 处理接收到的数据                  │
│  - 业务逻辑                          │
└─────────────────────────────────────┘
              ↕ API调用
┌─────────────────────────────────────┐
│  驱动层(CAN驱动)                   │  ← 驱动软件
│  - 配置CAN控制器                     │
│  - 提供发送/接收接口                 │
│  - 处理中断                          │
│  - 错误处理                          │
└─────────────────────────────────────┘
              ↕ 寄存器操作
┌─────────────────────────────────────┐
│  CAN控制器(硬件芯片)               │  ← 硬件!
│  - 执行仲裁 ★                        │
│  - 位填充/去填充 ★                   │
│  - CRC计算/校验 ★                    │
│  - 错误检测 ★                        │
│  - 错误管理 ★                        │
│  - 自动重发 ★                        │
│  - ACK处理 ★                         │
└─────────────────────────────────────┘
              ↕ 数字信号
┌─────────────────────────────────────┐
│  CAN收发器(物理层芯片)             │  ← 硬件!
│  - 差分信号转换                      │
│  - 总线驱动                          │
│  - 电平转换                          │
└─────────────────────────────────────┘
              ↕
         CAN总线(物理线路)

9.2 仲裁是硬件完成的

关键理解:仲裁、错误检测等核心机制都是CAN控制器硬件自动完成的!

驱动的工作(软件):
  1. 把数据写入CAN控制器的发送缓冲区
  2. 设置ID、DLC等参数
  3. 触发发送命令
  4. 等待发送完成中断
  5. 读取接收到的数据
  
CAN控制器的工作(硬件自动完成):
  1. 等待总线空闲
  2. 开始逐位发送
  3. 同时监听总线(仲裁)★
  4. 发现冲突立即停止 ★
  5. 仲裁失败后自动重试 ★
  6. 自动位填充 ★
  7. 计算并发送CRC ★
  8. 检查ACK ★
  9. 检测各种错误 ★
  10. 管理错误计数器 ★
  11. 发送完成后通知驱动(中断)

9.3 CAN控制器的实现

CAN控制器是硬件逻辑电路,不是运行固件的处理器:

CAN控制器内部(简化):

┌─────────────────────────────────────┐
│ CAN控制器芯片(例如MCP2515)         │
│                                     │
│  ┌────────────────────────────┐    │
│  │ 硬件状态机                  │    │
│  │  - 仲裁状态机               │    │
│  │  - 发送状态机               │    │
│  │  - 接收状态机               │    │
│  │  - 错误管理状态机           │    │
│  └────────────────────────────┘    │
│                                     │
│  ┌──────┐  ┌──────┐  ┌──────┐     │
│  │位流   │  │CRC    │  │位填充│     │
│  │处理器 │  │计算器 │  │逻辑  │     │
│  └──────┘  └──────┘  └──────┘     │
│                                     │
│  所有都是数字电路,不是程序!        │
└─────────────────────────────────────┘

特点

  • 由门电路、触发器组成
  • 硬件状态机实现
  • 不需要CPU执行程序
  • 实时、并行、零延迟
  • 所有CAN控制器遵循相同标准

9.4 驱动能做什么?不能做什么?

驱动可以配置的
// 1. 波特率设置
can_set_bitrate(500000);  // 500kbps

// 2. 过滤器配置
can_set_filter(0x100, 0x7FF);  // 只接收ID=0x100

// 3. 中断使能
can_enable_interrupts(CAN_IT_RX | CAN_IT_TX);

// 4. 工作模式
can_set_mode(CAN_MODE_NORMAL);  // 正常模式/环回模式/静默模式

// 5. 错误处理策略
can_set_auto_retransmit(true);  // 自动重传
驱动无法控制的(硬件自动完成)
✗ 无法控制仲裁过程(硬件状态机)
✗ 无法修改CRC计算(硬件逻辑)
✗ 无法跳过错误检测(硬件强制)
✗ 无法改变位填充规则(硬件自动)
✗ 无法控制ACK应答(硬件自动)
✗ 无法修改协议时序(硬件固定)

9.5 驱动质量的影响

好的驱动:
  ✓ 正确配置波特率(匹配网络)
  ✓ 合理设置过滤器(减少CPU负担)
  ✓ 高效处理中断(避免丢失消息)
  ✓ 完善的错误处理(Bus Off恢复)
  ✓ 提供易用的API

差的驱动:
  ✗ 波特率配置错误 → 无法通信
  ✗ 过滤器配置不当 → 丢失消息或CPU负担重
  ✗ 中断处理慢 → 接收缓冲区溢出
  ✗ 没有错误恢复 → Bus Off后无法恢复

但是:
  ✓ 仲裁机制不受影响(硬件保证)
  ✓ 错误检测不受影响(硬件保证)
  ✓ 协议一致性不受影响(硬件保证)
  ✓ 不会影响其他节点的工作

9.6 为什么硬件实现这么重要?

实时性要求
CAN 1Mbps的时序要求:
  - 每位时间:1微秒
  - 需要在1微秒内:发送、监听、比较、判断
  
软件实现(假设):
  - 函数调用:几十纳秒
  - GPIO操作:几百纳秒
  - 循环判断:几十纳秒
  - 总延迟:可能超过1微秒
  - 无法满足要求!
  
硬件实现:
  - 组合逻辑:纳秒级
  - 并行执行
  - 轻松满足要求
可靠性要求
如果仲裁由软件实现:
  ✗ 不同驱动可能有bug
  ✗ 实现不一致导致互操作问题
  ✗ 软件延迟导致仲裁失败
  ✗ 系统不可靠

硬件实现:
  ✓ 所有厂商遵循ISO 11898标准
  ✓ 硬件逻辑保证一致性
  ✓ 无延迟,实时响应
  ✓ 系统可靠
  ✓ 不同厂商设备可以互操作

10. 实际编程示例

10.1 Linux SocketCAN示例

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

int main() {
    int s;
    struct sockaddr_can addr;
    struct ifreq ifr;
    struct can_frame frame;
    
    // 1. 创建socket
    s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (s < 0) {
        perror("socket");
        return 1;
    }
    
    // 2. 指定CAN接口
    strcpy(ifr.ifr_name, "can0");
    ioctl(s, SIOCGIFINDEX, &ifr);
    
    // 3. 绑定socket
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    bind(s, (struct sockaddr *)&addr, sizeof(addr));
    
    // 4. 发送CAN帧
    frame.can_id = 0x123;
    frame.can_dlc = 8;
    frame.data[0] = 0x11;
    frame.data[1] = 0x22;
    frame.data[2] = 0x33;
    frame.data[3] = 0x44;
    frame.data[4] = 0x55;
    frame.data[5] = 0x66;
    frame.data[6] = 0x77;
    frame.data[7] = 0x88;
    
    if (write(s, &frame, sizeof(struct can_frame)) != sizeof(struct can_frame)) {
        perror("write");
        return 1;
    }
    printf("发送CAN帧: ID=0x%03X, DLC=%d\n", frame.can_id, frame.can_dlc);
    
    // 5. 接收CAN帧
    if (read(s, &frame, sizeof(struct can_frame)) < 0) {
        perror("read");
        return 1;
    }
    
    printf("接收CAN帧: ID=0x%03X, 数据: ", frame.can_id);
    for(int i = 0; i < frame.can_dlc; i++) {
        printf("%02X ", frame.data[i]);
    }
    printf("\n");
    
    close(s);
    return 0;
}

10.2 Python示例

import can
import time

# 创建CAN总线接口
bus = can.interface.Bus(channel='can0', bustype='socketcan')

# 发送消息
def send_message():
    msg = can.Message(
        arbitration_id=0x123,
        data=[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88],
        is_extended_id=False
    )
    try:
        bus.send(msg)
        print(f"发送: ID=0x{msg.arbitration_id:03X}, 数据={msg.data.hex()}")
    except can.CanError:
        print("发送失败")

# 接收消息
def receive_message():
    msg = bus.recv(timeout=1.0)
    if msg:
        print(f"接收: ID=0x{msg.arbitration_id:03X}, 数据={msg.data.hex()}")
    else:
        print("接收超时")

# 主循环
try:
    while True:
        send_message()
        receive_message()
        time.sleep(1)
except KeyboardInterrupt:
    print("程序退出")
finally:
    bus.shutdown()

10.3 STM32 HAL库示例

#include "stm32f4xx_hal.h"

CAN_HandleTypeDef hcan1;

// CAN初始化
void CAN_Init(void) {
    // 配置CAN参数
    hcan1.Instance = CAN1;
    hcan1.Init.Prescaler = 6;
    hcan1.Init.Mode = CAN_MODE_NORMAL;
    hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
    hcan1.Init.TimeSeg1 = CAN_BS1_13TQ;
    hcan1.Init.TimeSeg2 = CAN_BS2_2TQ;
    hcan1.Init.TimeTriggeredMode = DISABLE;
    hcan1.Init.AutoBusOff = ENABLE;
    hcan1.Init.AutoWakeUp = DISABLE;
    hcan1.Init.AutoRetransmission = ENABLE;
    hcan1.Init.ReceiveFifoLocked = DISABLE;
    hcan1.Init.TransmitFifoPriority = DISABLE;
    
    if (HAL_CAN_Init(&hcan1) != HAL_OK) {
        Error_Handler();
    }
    
    // 配置过滤器
    CAN_FilterTypeDef filter;
    filter.FilterBank = 0;
    filter.FilterMode = CAN_FILTERMODE_IDMASK;
    filter.FilterScale = CAN_FILTERSCALE_32BIT;
    filter.FilterIdHigh = 0x0000;
    filter.FilterIdLow = 0x0000;
    filter.FilterMaskIdHigh = 0x0000;
    filter.FilterMaskIdLow = 0x0000;
    filter.FilterFIFOAssignment = CAN_RX_FIFO0;
    filter.FilterActivation = ENABLE;
    
    if (HAL_CAN_ConfigFilter(&hcan1, &filter) != HAL_OK) {
        Error_Handler();
    }
    
    // 启动CAN
    if (HAL_CAN_Start(&hcan1) != HAL_OK) {
        Error_Handler();
    }
    
    // 使能接收中断
    if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) {
        Error_Handler();
    }
}

// 发送CAN消息
void CAN_Send(uint32_t id, uint8_t *data, uint8_t len) {
    CAN_TxHeaderTypeDef txHeader;
    uint32_t txMailbox;
    
    txHeader.StdId = id;
    txHeader.ExtId = 0;
    txHeader.IDE = CAN_ID_STD;
    txHeader.RTR = CAN_RTR_DATA;
    txHeader.DLC = len;
    txHeader.TransmitGlobalTime = DISABLE;
    
    if (HAL_CAN_AddTxMessage(&hcan1, &txHeader, data, &txMailbox) != HAL_OK) {
        Error_Handler();
    }
}

// 接收中断回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    CAN_RxHeaderTypeDef rxHeader;
    uint8_t rxData[8];
    
    if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK) {
        // 处理接收到的数据
        printf("接收: ID=0x%03X, DLC=%d\n", rxHeader.StdId, rxHeader.DLC);
        
        // 根据ID处理不同的消息
        switch(rxHeader.StdId) {
            case 0x100:
                // 处理发动机转速
                handle_engine_rpm(rxData);
                break;
            case 0x200:
                // 处理车速
                handle_vehicle_speed(rxData);
                break;
            default:
                break;
        }
    }
}

// 主函数
int main(void) {
    HAL_Init();
    SystemClock_Config();
    CAN_Init();
    
    uint8_t data[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
    
    while (1) {
        CAN_Send(0x123, data, 8);
        HAL_Delay(1000);
    }
}

10.4 Arduino示例(MCP2515)

#include <SPI.h>
#include <mcp2515.h>

MCP2515 mcp2515(10);  // CS引脚连接到D10

void setup() {
    Serial.begin(115200);
    
    // 初始化MCP2515
    mcp2515.reset();
    mcp2515.setBitrate(CAN_500KBPS, MCP_8MHZ);
    mcp2515.setNormalMode();
    
    Serial.println("CAN初始化完成");
}

void loop() {
    // 发送CAN消息
    struct can_frame txFrame;
    txFrame.can_id = 0x123;
    txFrame.can_dlc = 8;
    txFrame.data[0] = 0x11;
    txFrame.data[1] = 0x22;
    txFrame.data[2] = 0x33;
    txFrame.data[3] = 0x44;
    txFrame.data[4] = 0x55;
    txFrame.data[5] = 0x66;
    txFrame.data[6] = 0x77;
    txFrame.data[7] = 0x88;
    
    mcp2515.sendMessage(&txFrame);
    Serial.println("发送CAN帧");
    
    // 接收CAN消息
    struct can_frame rxFrame;
    if (mcp2515.readMessage(&rxFrame) == MCP2515::ERROR_OK) {
        Serial.print("接收ID: 0x");
        Serial.print(rxFrame.can_id, HEX);
        Serial.print(" 数据: ");
        for (int i = 0; i < rxFrame.can_dlc; i++) {
            Serial.print(rxFrame.data[i], HEX);
            Serial.print(" ");
        }
        Serial.println();
    }
    
    delay(1000);
}

11. CAN配置与调试

11.1 波特率配置

CAN波特率由以下参数决定:

位时间 = 同步段(Sync_Seg) + 传播段(Prop_Seg) + 相位段1(Phase_Seg1) + 相位段2(Phase_Seg2)

每段由若干时间量子(TQ)组成
TQ = (BRP + 1) / CAN时钟频率

波特率 = CAN时钟频率 / (BRP + 1) / (1 + Prop_Seg + Phase_Seg1 + Phase_Seg2)

常用配置示例(假设CAN时钟48MHz):

波特率BRPProp_SegPhase_Seg1Phase_Seg2总TQ
1 Mbps2132116
500 kbps5132116
250 kbps11132116
125 kbps23132116

11.2 Linux下CAN配置

# 加载CAN驱动模块
sudo modprobe can
sudo modprobe can_raw
sudo modprobe vcan

# 创建虚拟CAN接口(用于测试)
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0

# 配置真实CAN接口
sudo ip link set can0 type can bitrate 500000
sudo ip link set can0 type can restart-ms 100  # 自动重启
sudo ip link set up can0

# 查看CAN接口状态
ip -details link show can0

# 查看CAN统计信息
ip -s link show can0

# 关闭CAN接口
sudo ip link set down can0

11.3 常用调试工具

命令行工具
# 1. cansend - 发送单个CAN帧
cansend can0 123#1122334455667788
cansend can0 123#R  # 发送远程帧

# 2. candump - 监听CAN总线
candump can0
candump can0,123:7FF  # 只显示ID=0x123
candump -L can0  # 记录到文件

# 3. cangen - 生成随机CAN流量
cangen can0 -I 100 -L 8 -D i -g 10  # 每10ms发送一帧

# 4. canplayer - 回放记录的CAN数据
canplayer -I candump.log

# 5. cansniffer - 实时显示变化的数据
cansniffer can0

# 6. cansequence - 测试丢包
cansequence can0

# 7. isotpdump - ISO-TP协议分析
isotpdump -s 123 -d 456 can0
图形化工具
  1. CANoe(Vector)

    • 专业级CAN分析工具
    • 支持仿真、测试、分析
    • 商业软件
  2. Busmaster

    • 开源CAN分析工具
    • 支持多种CAN硬件
    • Windows平台
  3. SavvyCAN

    • 开源CAN数据分析工具
    • 支持DBC文件
    • 跨平台

11.4 DBC文件管理

DBC(Database CAN)文件用于定义CAN网络的消息和信号:

VERSION ""

NS_ : 
    NS_DESC_
    CM_
    BA_DEF_
    BA_
    VAL_

BS_:

BU_: EngineECU Dashboard ABS

// 消息定义
BO_ 256 EngineSpeed: 8 EngineECU
 SG_ RPM : 0|16@1+ (1,0) [0|8000] "rpm" Dashboard
 SG_ Torque : 16|16@1+ (0.1,0) [0|500] "Nm" Dashboard

BO_ 257 EngineTemp: 8 EngineECU
 SG_ Temperature : 0|8@1+ (1,-40) [-40|215] "degC" Dashboard

BO_ 512 VehicleSpeed: 8 ABS
 SG_ Speed : 0|16@1+ (0.01,0) [0|250] "kph" Dashboard

// 信号值定义
VAL_ 256 RPM 0 "Engine_Off" 8000 "Max_RPM" ;

12. 常见问题与解决方案

12.1 总线关闭(Bus Off)

症状

  • 节点无法发送和接收数据
  • 错误计数器TEC > 255

原因

  • 硬件故障(线路断开、短路)
  • 波特率不匹配
  • 终端电阻不正确
  • 节点频繁发送错误

解决方案

// 1. 检查硬件连接
// 2. 确认波特率一致
// 3. 检查终端电阻(两端各120Ω)
// 4. 软件恢复Bus Off

void recover_from_bus_off() {
    // 方法1:软件复位CAN控制器
    HAL_CAN_Stop(&hcan1);
    HAL_CAN_Start(&hcan1);
    
    // 方法2:等待自动恢复
    // 需要检测到128次11个连续隐性位
    
    // 方法3:使能自动恢复
    hcan1.Init.AutoBusOff = ENABLE;
}

12.2 通信不稳定

症状

  • 偶尔丢失消息
  • CRC错误频繁
  • 错误计数器增加

原因

  • 电磁干扰
  • 线路过长
  • 终端电阻不正确
  • 波特率过高

解决方案

1. 使用屏蔽双绞线
2. 缩短总线长度或降低波特率
3. 检查终端电阻(两端各120Ω,不能多也不能少)
4. 远离干扰源(电机、继电器等)
5. 增加共模电感
6. 检查接地

12.3 ACK错误

症状

  • 发送节点报告ACK错误
  • 消息无法发送成功

原因

  • 只有一个节点在总线上(没有其他节点应答)
  • 其他节点未正确初始化
  • 其他节点都检测到错误

解决方案

1. 确保至少有2个节点在总线上
2. 检查其他节点的初始化状态
3. 使用环回模式测试(单节点测试)
4. 检查其他节点的过滤器配置
// 环回模式(用于单节点测试)
hcan1.Init.Mode = CAN_MODE_LOOPBACK;
HAL_CAN_Init(&hcan1);

12.4 无法接收消息

症状

  • 能发送,但收不到消息
  • 总线上有数据,但节点收不到

原因

  • 过滤器配置错误
  • 接收中断未使能
  • 接收缓冲区溢出

解决方案

// 1. 配置过滤器接收所有消息(调试用)
CAN_FilterTypeDef filter;
filter.FilterIdHigh = 0x0000;
filter.FilterIdLow = 0x0000;
filter.FilterMaskIdHigh = 0x0000;
filter.FilterMaskIdLow = 0x0000;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan1, &filter);

// 2. 使能接收中断
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

// 3. 及时读取接收缓冲区
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    CAN_RxHeaderTypeDef rxHeader;
    uint8_t rxData[8];
    
    // 立即读取,避免溢出
    while (HAL_CAN_GetRxFifoFillLevel(hcan, CAN_RX_FIFO0) > 0) {
        HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData);
        process_message(&rxHeader, rxData);
    }
}

12.5 相同ID冲突

症状

  • 总线频繁出现错误帧
  • 两个节点都无法成功发送

原因

  • 两个节点发送相同的ID
  • 仲裁无法分出胜负
  • 在数据字段产生冲突

解决方案

1. 系统设计时分配唯一ID
2. 每个ID只能由一个节点发送
3. 使用DBC文件管理ID分配
4. 开发阶段使用工具检查ID冲突

ID分配原则:
  - 每个消息类型唯一ID
  - 每个ID只有一个发送者
  - 多个接收者可以接收同一ID

12.6 调试技巧

# 1. 查看错误统计
ip -s -d link show can0

# 2. 监控错误帧
candump can0,0:0,#FFFFFFFF  # 显示所有帧包括错误帧

# 3. 测试环回
ip link set can0 type can loopback on
cansend can0 123#1122334455667788
candump can0  # 应该能收到自己发送的消息

# 4. 检查总线负载
canbusload can0@500000  # 显示总线负载百分比

# 5. 分析时序
# 使用逻辑分析仪或示波器
# 测量CAN_H和CAN_L的差分信号

附录A:CAN协议要点总结

核心概念

  1. CAN是什么

    • 多主站、串行、差分信号的通信总线
    • 发布-订阅通信模式
    • 广播式,所有节点都能收到所有消息
  2. 显性位与隐性位

    • 显性位(0)优先级高,能覆盖隐性位(1)
    • 线与特性是仲裁的基础
  3. 报文结构

    • SOF + ID + 控制 + 数据 + CRC + ACK + EOF
    • ID表示消息类型,不是目标地址
    • 数据长度0-8字节(CAN 2.0)或0-64字节(CAN FD)
  4. 仲裁机制

    • 非破坏性位仲裁
    • ID越小优先级越高
    • 边发送边监听,失败者立即退出
  5. 错误检测

    • 5种错误检测机制
    • 错误计数器管理
    • 故障节点自动隔离(Bus Off)
  6. 硬件与软件

    • 仲裁、错误检测由CAN控制器硬件完成
    • 驱动只负责配置和接口
    • 硬件保证协议一致性

设计原则

  1. ID分配

    • 每个消息类型唯一ID
    • 每个ID只有一个发送者
    • 根据优先级分配ID(紧急消息用小ID)
  2. 网络设计

    • 使用双绞线
    • 两端120Ω终端电阻
    • 根据距离选择波特率
    • 避免星型拓扑,使用总线型
  3. 软件设计

    • 使用过滤器减少CPU负担
    • 快速处理接收中断
    • 实现Bus Off恢复机制
    • 使用DBC文件管理消息定义

附录B:参考资源

标准文档

  • ISO 11898-1: CAN数据链路层和物理信令
  • ISO 11898-2: CAN高速物理层
  • CAN Specification 2.0 (Bosch)
  • CAN FD Specification (Bosch)

开发工具

  • 硬件:MCP2515、TJA1050、PCAN-USB、CANable
  • 软件:SocketCAN、CANoe、Busmaster、SavvyCAN
  • :python-can、socketcan、Arduino CAN库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值