SocketCAN 入门 -- Linux 下的 CAN 开发环境
目录
1. 什么是 SocketCAN
SocketCAN 是 Linux 内核提供的 CAN 网络子系统,由 Volkswagen 贡献。它将 CAN 总线抽象为标准网络设备(如同 eth0),开发者可以使用熟悉的 socket API 进行 CAN 通信编程。
1.1 为什么用 Socket?
传统的 CAN 开发使用字符设备(/dev/can0),API 设计不统一。SocketCAN 采用 socket 方式,带来以下优势:
- 统一的 API:与 TCP/UDP 编程接口一致,降低学习成本
- 多路复用:多个进程可以同时监听同一 CAN 总线
- 过滤器:内核级报文过滤,减少用户态处理负担
- 网络工具兼容:可以使用
ip link、ifconfig等标准工具管理 CAN 设备
1.2 与 Python-can 等用户态库的区别
| 特性 | SocketCAN | python-can(用户态) |
|---|---|---|
| 运行位置 | Linux 内核 | 用户空间 |
| 语言绑定 | C(原生),其他语言通过 FFI | Python |
| 性能 | 高(零拷贝) | 中(用户态到内核态切换) |
| 跨平台 | 仅 Linux | 跨平台 |
| 适用场景 | 生产环境、高性能需求 | 快速原型、测试工具 |
2. SocketCAN 内核架构
┌──────────────────────────────────────────────────┐
│ 用户空间 │
│ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ can_send.c │ │ candump │ │ cansend │ │
│ └──────┬──────┘ └──────┬──────┘ └─────┬─────┘ │
│ │ │ │ │
│ └────────────────┼───────────────┘ │
│ socket(AF_CAN) │
├─────────────────────────┼────────────────────────┤
│ 内核空间 │
│ ┌───────────────┴───────────────┐ │
│ │ CAN_RAW 协议层 │ │
│ │ (过滤器、错误处理、回环) │ │
│ └───────────────┬───────────────┘ │
│ ┌───────────────┴───────────────┐ │
│ │ CAN 核心层 │ │
│ │ (报文路由、定时器) │ │
│ └───────────────┬───────────────┘ │
│ ┌──────────────┐ ┌────┴────┐ ┌─────────────┐ │
│ │ vcan (虚拟) │ │ can-dev │ │ CAN FD │ │
│ │ │ │ (硬件) │ │ │ │
│ └──────────────┘ └────┬────┘ └─────────────┘ │
└─────────────────────────┼────────────────────────┘
│
┌──────┴──────┐
│ CAN 控制器 │
│ MCP2515 等 │
└─────────────┘三层结构:
- CAN_RAW 协议层:用户态 socket 的直接接口,负责报文过滤、回环控制、错误处理
- CAN 核心层:报文路由(在多个 CAN 设备间转发)、定时器管理
- 设备驱动层:与硬件 CAN 控制器通信,或使用虚拟设备(vcan)
3. 核心数据结构
SocketCAN 编程的核心是以下数据结构(定义在 <linux/can.h> 中):
3.1 struct can_frame(标准帧)
struct can_frame {
canid_t can_id; /* CAN 标识符 + 标志位 */
__u8 can_dlc; /* 数据长度码 (0-8) */
__u8 __pad; /* 填充 */
__u8 __res0; /* 保留 */
__u8 len; /* 实际数据长度 */
__u8 data[8]; /* 数据载荷 */
};can_id 标志位:
| 标志 | 值 | 含义 |
|---|---|---|
CAN_EFF_FLAG | 0x80000000U | 扩展帧 |
CAN_RTR_FLAG | 0x40000000U | 远程帧 |
CAN_ERR_FLAG | 0x20000000U | 错误帧 |
/* 标准帧:直接赋值 ID */
frame.can_id = 0x123;
/* 扩展帧:设置 EFF_FLAG */
frame.can_id = 0x12345678 | CAN_EFF_FLAG;3.2 struct can_filter(接收过滤器)
struct can_filter {
canid_t can_id; /* 过滤器 ID */
canid_t can_mask; /* 掩码 */
};过滤规则:(received_id & mask) == (filter_id & mask) 的帧被接收。
/* 只接收 ID = 0x123 的帧 */
struct can_filter f = {
.can_id = 0x123,
.can_mask = CAN_SFF_MASK /* 0x7FF */
};
/* 接收所有帧 */
// 不设置过滤器即可
3.3 struct sockaddr_can(套接字地址)
struct sockaddr_can {
sa_family_t can_family; /* AF_CAN */
int can_ifindex; /* 接口索引 */
union {
/* 传输层协议特定数据 */
} can_addr;
};4. 核心 API
4.1 创建 CAN 套接字
int sock = socket(AF_CAN, SOCK_RAW, CAN_RAW);
if (sock < 0) {
perror("socket");
return -1;
}| 参数 | 值 | 说明 |
|---|---|---|
| 协议族 | AF_CAN | CAN 协议族 |
| 类型 | SOCK_RAW | 原始套接字,直接收发 CAN 帧 |
| 协议 | CAN_RAW | CAN 原始协议 |
4.2 绑定到网络设备
struct sockaddr_can addr;
struct ifreq ifr;
strcpy(ifr.ifr_name, "vcan0");
ioctl(sock, SIOCGIFINDEX, &ifr);
memset(&addr, 0, sizeof(addr));
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(sock, (struct sockaddr *)&addr, sizeof(addr));4.3 发送帧
struct can_frame frame;
frame.can_id = 0x123;
frame.can_dlc = 4;
frame.data[0] = 0x11;
frame.data[1] = 0x22;
frame.data[2] = 0x33;
frame.data[3] = 0x44;
write(sock, &frame, sizeof(frame));4.4 接收帧
struct can_frame frame;
int nbytes = read(sock, &frame, sizeof(frame));
if (nbytes > 0) {
/* 处理接收到的帧 */
}4.5 设置过滤器
struct can_filter rfilter[1];
rfilter[0].can_id = 0x123;
rfilter[0].can_mask = CAN_SFF_MASK;
setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FILTER,
&rfilter, sizeof(rfilter));5. 常用工具(can-utils)
can-utils 是 Linux 下标准的 CAN 用户空间工具集:
| 命令 | 用途 | 示例 |
|---|---|---|
candump | 接收并显示 CAN 帧 | candump vcan0 |
cansend | 发送单个 CAN 帧 | cansend vcan0 123#11223344 |
cangen | 生成随机 CAN 帧(测试) | cangen vcan0 -v |
canplayer | 重放 CAN 日志 | canplayer -I candump.log |
cansequence | 发送递增序列(测试连通性) | cansequence vcan0 |
candump -D | CANOpen 解码 | candump -D vcan0 |
cansend 语法:
cansend <interface> <can_id>#[data]
例:cansend vcan0 123#11223344
cansend vcan0 000#0105 # NMT Start Node 56. 从 SocketCAN 到 CANOpen
重要理解:SocketCAN 只处理 CAN 的物理层和数据链路层。它不知道什么是 CANOpen。
┌─────────────────────────────────────────────┐
│ CANOpen 应用层 │ ← 本系列教程的核心
│ (NMT, SDO, PDO, 对象字典, 设备行规) │
├─────────────────────────────────────────────┤
│ SocketCAN (Linux 内核) │ ← 本章内容
│ (CAN_RAW, 报文路由, 过滤器) │
├─────────────────────────────────────────────┤
│ CAN 2.0A/B (硬件) │ ← 模块 1 前半部分
│ (帧格式, 仲裁, 位填充) │
└─────────────────────────────────────────────┘理解这一点至关重要:
- SocketCAN 是传输管道 —— 它负责将 CAN 帧从一端送到另一端
- CANOpen 是语言 —— 它定义了 CAN 帧的含义、格式和行为规则
- 学习 SocketCAN 是编写 CANOpen 上位机(主站、调试工具)的前提
7. 关键知识点总结
| 知识点 | 要点 |
|---|---|
| SocketCAN 是什么 | Linux 内核 CAN 网络子系统,socket API 风格 |
| 核心结构体 | can_frame(帧)、can_filter(过滤)、sockaddr_can(地址) |
| 核心 API | socket() → bind() → write()/read() → setsockopt() |
| 标志位 | CAN_EFF_FLAG(扩展帧)、CAN_RTR_FLAG(远程帧)、CAN_ERR_FLAG(错误帧) |
| 工具集 | candump、cansend、cangen 等 |
| 与 CANOpen 关系 | SocketCAN = 底层管道,CANOpen = 上层协议 |
★ Insight ─────────────────────────────────────
- SocketCAN 的设计哲学:将 CAN 总线视为"网络设备",而非特殊硬件。这使得 CAN 编程与网络编程的体验一致。
- 过滤器是内核级功能 —— 在内核层面丢弃不需要的帧,避免用户态程序被海量报文淹没。这对于高总线负载的场景尤为重要。
CAN_RAW协议是"最原始"的 CAN 接口,类似于 TCP 的SOCK_RAW。后续的 BCM(Broadcast Manager)和 ISOTP 协议在 RAW 之上提供了更高级的功能。─────────────────────────────────────────────────
上一步 | 下一步
← 02-can-frame-format.md —— CAN 帧格式 → 04-vcan-setup.md —— 虚拟 CAN 设置
如果这篇文章对你有帮助,欢迎请我喝杯咖啡 ☕
支付宝
微信