CAN总线协议完全指南
目录
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Ω
(两端)
关键要点:
- 使用双绞线传输(抗干扰)
- 差分信号:通过两根线的电压差表示0和1
- 总线两端需要120Ω终端电阻(防止信号反射)
- 最大总线长度取决于波特率
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 Mbps | 40米 | 高速短距离 |
| 500 kbps | 100米 | 汽车内部网络 |
| 250 kbps | 250米 | 工业控制 |
| 125 kbps | 500米 | 长距离通信 |
| 50 kbps | 1000米 | 超长距离 |
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位(扩展帧)
- 作用:
- 识别消息类型(不是设备地址!)
- 决定优先级(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位界定符
- 作用:检测传输错误
- 原理:
- 发送方根据前面的数据计算CRC值
- 接收方重新计算CRC并比对
- 不匹配则报告CRC错误
ACK(Acknowledgement)- 应答
- 长度:1位ACK槽 + 1位界定符
- 工作方式:
- 发送节点在ACK槽发送隐性位(1)
- 接收节点正确接收后,发送显性位(0)覆盖
- 发送节点检测到显性位,确认有节点收到
- 注意: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)
错误帧的作用:
- 通知所有节点当前帧有错误
- 破坏当前帧(6个连续显性位违反填充规则)
- 所有节点丢弃当前帧
- 发送节点自动重传
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的对比
| 特性 | DDS | CAN |
|---|---|---|
| 通信模式 | 发布-订阅 | 发布-订阅 |
| 数据标识 | Topic名称 | CAN ID |
| 广播性质 | 多播/广播 | 广播 |
| 解耦性 | 完全解耦 | 完全解耦 |
| 过滤机制 | Topic过滤 | ID过滤 |
| 优先级 | QoS策略 | ID值(小=高) |
| 网络层 | IP网络 | CAN总线 |
| 数据大小 | 可达MB | 8B/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):
| 波特率 | BRP | Prop_Seg | Phase_Seg1 | Phase_Seg2 | 总TQ |
|---|---|---|---|---|---|
| 1 Mbps | 2 | 13 | 2 | 1 | 16 |
| 500 kbps | 5 | 13 | 2 | 1 | 16 |
| 250 kbps | 11 | 13 | 2 | 1 | 16 |
| 125 kbps | 23 | 13 | 2 | 1 | 16 |
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
图形化工具
-
CANoe(Vector)
- 专业级CAN分析工具
- 支持仿真、测试、分析
- 商业软件
-
Busmaster
- 开源CAN分析工具
- 支持多种CAN硬件
- Windows平台
-
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协议要点总结
核心概念
-
CAN是什么
- 多主站、串行、差分信号的通信总线
- 发布-订阅通信模式
- 广播式,所有节点都能收到所有消息
-
显性位与隐性位
- 显性位(0)优先级高,能覆盖隐性位(1)
- 线与特性是仲裁的基础
-
报文结构
- SOF + ID + 控制 + 数据 + CRC + ACK + EOF
- ID表示消息类型,不是目标地址
- 数据长度0-8字节(CAN 2.0)或0-64字节(CAN FD)
-
仲裁机制
- 非破坏性位仲裁
- ID越小优先级越高
- 边发送边监听,失败者立即退出
-
错误检测
- 5种错误检测机制
- 错误计数器管理
- 故障节点自动隔离(Bus Off)
-
硬件与软件
- 仲裁、错误检测由CAN控制器硬件完成
- 驱动只负责配置和接口
- 硬件保证协议一致性
设计原则
-
ID分配
- 每个消息类型唯一ID
- 每个ID只有一个发送者
- 根据优先级分配ID(紧急消息用小ID)
-
网络设计
- 使用双绞线
- 两端120Ω终端电阻
- 根据距离选择波特率
- 避免星型拓扑,使用总线型
-
软件设计
- 使用过滤器减少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库
1659

被折叠的 条评论
为什么被折叠?



