CAN(控制器局域网)工业协议教学文档(一)
文章目录
- 1. 概述与环境背景
- 1.1 什么是CAN?
- 1.2 核心特性与优势
- 1.3 典型网络环境与硬件
- 2. 通讯结构与数据帧
- 2.1 OSI模型中的CAN
- 2.2 帧类型
- 2.3 数据帧结构 (CAN 2.0A 标准帧)
- 3. 数据传输过程
- 4. Go语言Demo演练
- 4.1 环境准备 (Linux)
- 4.2 Go示例代码 (使用 `eliasnaur.com/can`)
- 4.3 运行
- 5. Rust语言Demo演练
- 5.1 环境准备 (Linux)
- 5.2 Rust示例代码
- 5.3 运行
- 6. 总结
1. 概述与环境背景
1.1 什么是CAN?
CAN(Controller Area Network)即控制器局域网,是一种高性能、高可靠性、高实时性的串行通信协议,最初由德国博世公司(BOSCH)于1980年代开发,主要用于解决汽车内部众多电子控制单元(ECU)之间的数据交换问题。
由于其卓越的特性,CAN总线如今已广泛应用于汽车电子、工业自动化(传感器、执行器、PLC)、船舶、医疗设备、航空航天等领域,是现场总线家族中至关重要的一员。
1.2 核心特性与优势
- 多主结构:网络上任何节点都可以在总线空闲时主动向其他节点发送消息,无主从之分。
- 基于优先级的总线仲裁:标识符(ID)决定了报文的优先级。ID值越小,优先级越高。当多个节点同时发送时,高优先级的报文会无损地继续传输,低优先级的则主动退出发送,大大提高了总线利用率。
- 高可靠性:
- CRC校验:强大的循环冗余校验保证数据完整性。
- 帧格式校验:自动检查报文格式错误。
- 应答机制:发送节点会确认是否至少有一个节点正确接收了报文。
- 通信灵活:支持广播(所有节点接收)和点对点(通过硬件过滤)两种模式。
- 强抗干扰能力:采用差分信号(CAN_H和CAN_L)传输,能有效抑制共模干扰,适合恶劣的电磁环境。
1.3 典型网络环境与硬件
一个典型的CAN网络环境包括:
- CAN节点:带有CAN控制器(常集成在MCU中)的设备,如ECU、传感器、工控机等。
- CAN收发器:将CAN控制器的逻辑电平转换为CAN总线的差分信号(反之亦然),如TJA1050、MCP2551等。
- 物理介质:通常是双绞线(CAN_H和CAN_L)。
- 终端电阻:在总线两端(最远距离处)各接一个120Ω的电阻,用于阻抗匹配,消除信号反射。
连接方式:所有节点通过主干线(Trunk Line) 和支线(Drop Line) 以总线形式并联在一起。
2. 通讯结构与数据帧
2.1 OSI模型中的CAN
CAN协议主要定义了OSI模型中的物理层和数据链路层(包括逻辑链路控制子层LLC和介质访问控制子层MAC)。
2.2 帧类型
CAN协议有四种帧类型:
- 数据帧:用于发送节点向接收节点传输数据。
- 远程帧:用于请求发送具有相同标识符的数据帧。
- 错误帧:当节点检测到错误时,向总线发送错误帧,通知其他节点。
- 过载帧:用于在数据帧或远程帧之间提供额外的延时。
其中最核心的是数据帧。
2.3 数据帧结构 (CAN 2.0A 标准帧)
一个标准数据帧由以下7个字段组成,最多可传输8字节数据。
字段 | 长度(bit) | 说明 |
---|---|---|
SOF (Start Of Frame) | 1 | 帧起始,显性位(0),用于同步。 |
Arbitration Field (仲裁场) | 12 | ID (11bit) + RTR (1bit)。ID决定优先级,RTR(远程传输请求)用于区分数据帧(0)和远程帧(1)。 |
Control Field (控制场) | 6 | IDE (1bit, 显性0表示标准帧) + r0 (保留位) + DLC (4bit, 数据长度码,0-8)。 |
Data Field (数据场) | 0-64 (0-8字节) | 实际要传输的数据。 |
CRC Field (CRC场) | 16 | CRC序列 (15bit) + CRC界定符 (1bit, 隐性1),用于接收方校验数据错误。 |
ACK Field (应答场) | 2 | ACK Slot (1bit, 发送方发隐性1,接收方用显性0覆盖以示应答) + ACK界定符 (1bit, 隐性1)。 |
EOF (End Of Frame) | 7 | 帧结束,7个连续的隐性位(1)。 |
总线仲裁过程:在发送仲裁场(ID)时,发送节点同时监听总线电平。如果发送的是隐性(1)而监测到的是显性(0),说明有更高优先级的报文正在发送,该节点立即退出发送转为接收模式。这个过程不会损坏高优先级报文的任何位。
3. 数据传输过程
- 空闲检测:所有节点持续监听总线。当总线空闲(连续检测到11个隐性位)时,任何节点都可以开始发送。
- 仲裁:多个节点同时开始发送时,通过仲裁场(ID)进行“线与”仲裁。优先级高的报文赢得总线使用权。
- 数据传输与应答:赢得仲裁的节点继续完成数据帧的发送。所有接收节点进行CRC校验。校验正确的节点会在ACK Slot位期间发送一个显性位(0)来应答发送方。
- 错误处理:如果发送节点未收到应答(ACK错误),或CRC校验失败,或其他格式错误,该节点会发送一个错误帧,并自动重发报文(除非错误计数过高导致节点进入“Bus-Off”状态)。
- 帧结束:发送EOF字段,总线恢复空闲状态。
4. Go语言Demo演练
在Go语言中,我们可以使用第三方库(如github.com/golang/mock/gomock
,但更常用的是如github.com/brutella/can
或eliasnaur.com/can
)来操作SocketCAN(Linux下的CAN接口抽象)。
以下示例假设你使用的是Linux系统,并且已经配置好了虚拟或真实的CAN接口(vcan0
)。
4.1 环境准备 (Linux)
# 加载vcan内核模块
sudo modprobe vcan# 创建虚拟CAN接口vcan0
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0# 检查接口
ip link show vcan0
4.2 Go示例代码 (使用 eliasnaur.com/can
)
首先初始化Go模块并安装依赖:
go mod init can-demo
go get eliasnaur.com/can
代码:can_send_go.go
package mainimport ("context""fmt""log""time""eliasnaur.com/can"
)func main() {// 打开CAN接口 "vcan0"bus, err := can.OpenBus("vcan0")if err != nil {log.Fatalf("Failed to open CAN bus: %v", err)}defer bus.Close()// 创建一个要发送的帧// ID: 0x123, 数据: "HelloCAN!" (前8字节)frame := can.Frame{ID: 0x123,Length: 8,Data: can.Data{'H', 'e', 'l', 'l', 'o', 'C', 'A', 'N'},}ticker := time.NewTicker(500 * time.Millisecond)defer ticker.Stop()ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()for {select {case <-ticker.C:// 发送帧err := bus.Send(frame)if err != nil {log.Printf("Failed to send frame: %v", err)} else {fmt.Printf("Sent frame: ID=%03X, Data=%s\n", frame.ID, string(frame.Data[:frame.Length]))}case <-ctx.Done():fmt.Println("Sender stopped.")return}}
}
代码:can_recv_go.go
package mainimport ("fmt""log""eliasnaur.com/can"
)func main() {// 打开CAN接口 "vcan0"bus, err := can.OpenBus("vcan0")if err != nil {log.Fatalf("Failed to open CAN bus: %v", err)}defer bus.Close()fmt.Println("Listening for CAN frames on vcan0...")// 持续读取总线上的帧for {var frame can.Frameerr := bus.Recv(&frame)if err != nil {log.Fatalf("Failed to receive frame: %v", err)}// 打印接收到的帧信息fmt.Printf("Received: ID=%03X, DLC=%d, Data=%X | ASCII: %s\n",frame.ID,frame.Length,frame.Data[:frame.Length],string(frame.Data[:frame.Length]),)}
}
4.3 运行
打开两个终端窗口。
终端1(接收):
go run can_recv_go.go
终端2(发送):
go run can_send_go.go
你将在接收终端看到每秒2条来自发送终端的数据。
5. Rust语言Demo演练
在Rust中,常用的CAN库是socketcan
。
5.1 环境准备 (Linux)
同Go demo,确保vcan0
接口已设置并启动。
5.2 Rust示例代码
首先创建新的Rust项目并添加依赖:
cargo new can-demo-rs
cd can-demo-rs
# 在 Cargo.toml 的 [dependencies] 下添加
# socketcan = "2.2.0"
# tokio = { version = "1", features = ["full"] } # 用于异步示例
代码:src/main.rs
(同步版本)
use socketcan::{CanFrame, CanSocket, Socket};
use std::time::Duration;fn main() -> Result<(), Box<dyn std::error::Error>> {// 打开CAN接口 "vcan0"let mut sock = CanSocket::open("vcan0")?;// 创建一个要发送的帧// ID: 0x123, 数据: "HelloRST!" (前8字节)let data = [b'H', b'e', b'l', b'l', b'o', b'R', b'S', b'T'];let frame = CanFrame::new(0x123, &data, false, false)?; // 标准帧,非远程帧loop {// 发送帧sock.write_frame(&frame)?;println!("Sent frame: ID={:03X}, Data={:?}", frame.id(), std::str::from_utf8(&data)?);// 等待1秒std::thread::sleep(Duration::from_secs(1));}
}
代码:接收端 src/bin/can_recv.rs
use socketcan::{CanSocket, Socket};fn main() -> Result<(), Box<dyn std::error::Error>> {// 打开CAN接口 "vcan0"let sock = CanSocket::open("vcan0")?;println!("Listening for CAN frames on vcan0...");loop {// 阻塞读取一帧let frame = sock.read_frame()?;println!("Received: ID={:03X}, DLC={}, Data={:02X?} | ASCII: {}",frame.id(),frame.dlc(),frame.data(),String::from_utf8_lossy(frame.data()));}
}
需要在Cargo.toml
中配置为多二进制项目:
[[bin]]
name = "can_recv"
path = "src/bin/can_recv.rs"[[bin]]
name = "can_send"
path = "src/main.rs" # 假设发送代码在main.rs
5.3 运行
编译并运行两个程序。
终端1(接收):
cargo run --bin can_recv
终端2(发送):
cargo run --bin can_send
你将在接收终端看到每秒1条来自发送终端的数据。
6. 总结
本教程系统地介绍了CAN协议的核心概念:
- 背景与环境:理解了CAN诞生的原因、其卓越特性及典型应用场景和硬件组成。
- 通讯结构:深入学习了数据帧的构成,理解了标识符(ID)决定优先级这一核心机制。
- 传输过程:掌握了从仲裁、传输、应答到错误处理的完整数据流。
- 实战演练:通过Go和Rust两种现代语言的Demo,学会了如何使用代码在Linux系统上通过SocketCAN接口进行最基本的CAN报文收发。
下一步:
- 探索扩展帧(29位ID)。
- 学习CAN高层协议,如CANopen或J1939,它们定义了设备对象字典、通信参数等,是实际工业应用的基础。
- 使用CAN分析仪(如PCAN, USB-CAN) 连接真实硬件,捕获和分析总线数据。
- 深入研究错误状态和故障 confinement机制。