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 等用户态库的区别

特性SocketCANpython-can(用户态)
运行位置Linux 内核用户空间
语言绑定C(原生),其他语言通过 FFIPython
性能高(零拷贝)中(用户态到内核态切换)
跨平台仅 Linux跨平台
适用场景生产环境、高性能需求快速原型、测试工具

2. SocketCAN 内核架构

┌──────────────────────────────────────────────────┐
│                   用户空间                        │
│  ┌─────────────┐  ┌─────────────┐  ┌───────────┐ │
│  │ can_send.c  │  │ candump     │  │ cansend   │ │
│  └──────┬──────┘  └──────┬──────┘  └─────┬─────┘ │
│         │                │               │        │
│         └────────────────┼───────────────┘        │
│                    socket(AF_CAN)                 │
├─────────────────────────┼────────────────────────┤
│                   内核空间                        │
│         ┌───────────────┴───────────────┐        │
│         │        CAN_RAW 协议层         │        │
│         │   (过滤器、错误处理、回环)    │        │
│         └───────────────┬───────────────┘        │
│         ┌───────────────┴───────────────┐        │
│         │        CAN 核心层             │        │
│         │   (报文路由、定时器)          │        │
│         └───────────────┬───────────────┘        │
│  ┌──────────────┐  ┌────┴────┐  ┌─────────────┐ │
│  │ vcan (虚拟)  │  │ can-dev │  │ CAN FD      │ │
│  │              │  │ (硬件)  │  │             │ │
│  └──────────────┘  └────┬────┘  └─────────────┘ │
└─────────────────────────┼────────────────────────┘
                          │
                   ┌──────┴──────┐
                   │ CAN 控制器  │
                   │ MCP2515 等  │
                   └─────────────┘

三层结构:

  1. CAN_RAW 协议层:用户态 socket 的直接接口,负责报文过滤、回环控制、错误处理
  2. CAN 核心层:报文路由(在多个 CAN 设备间转发)、定时器管理
  3. 设备驱动层:与硬件 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_FLAG0x80000000U扩展帧
CAN_RTR_FLAG0x40000000U远程帧
CAN_ERR_FLAG0x20000000U错误帧
/* 标准帧:直接赋值 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_CANCAN 协议族
类型SOCK_RAW原始套接字,直接收发 CAN 帧
协议CAN_RAWCAN 原始协议

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 -DCANOpen 解码candump -D vcan0

cansend 语法:

cansend <interface> <can_id>#[data]
例:cansend vcan0 123#11223344
    cansend vcan0 000#0105        # NMT Start Node 5

6. 从 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(地址)
核心 APIsocket() → 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 设置

如果这篇文章对你有帮助,欢迎请我喝杯咖啡 ☕
Cygnus Yang 支付宝支付宝
Cygnus Yang 微信微信