3. SocketCAN 核心 API 编程:从 socket() 到 CAN ID 过滤

好,我们直接进入正题。SocketCAN 的编程接口,说白了就是一套标准 socket 操作加上 CAN 特有的数据结构。你如果写过网络 socket 程序,上手会非常快。但有几个坑,我当年踩得可不轻。

3.1 三个基础系统调用:socket() / bind() / connect()

这三个函数是 SocketCAN 程序的骨架。先看 socket() 怎么创建 CAN 套接字:

int s;
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);

注意协议族是 PF_CAN,不是 AF_INET。类型用 SOCK_RAW,协议用 CAN_RAW。我刚开始写的时候,顺手就写了 AF_INET,结果 bind 一直报错,查了半天才发现是协议族搞错了。

bind() 的作用是把套接字绑定到具体的 CAN 接口。比如你车上有两个 CAN 节点,一个 can0,一个 can1,你得告诉内核你想用哪个:

struct sockaddr_can addr;
struct ifreq ifr;

strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);

addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

bind(s, (struct sockaddr *)&addr, sizeof(addr));

这里有个细节:sockaddr_can 结构体里的 can_ifindex 必须通过 ioctl 获取。你不能直接写死接口索引,因为系统重启后索引可能变。我在一个车载项目里就吃过这个亏——写死了索引 0,结果系统升级后 can0 变成了 can1,程序直接罢工。

connect() 在 SocketCAN 里不是必须的。它主要用于 SOCK_DGRAM 模式,或者你想固定发送到某个 CAN ID。对于 SOCK_RAW,bind 之后直接用 sendto/recvfrom 就行。我个人习惯用 connect 来简化发送逻辑,这样后面可以直接用 write() 发送:

struct sockaddr_can addr;
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
addr.can_addr.tp.tx_id = 0x123;  // 固定发送 ID

connect(s, (struct sockaddr *)&addr, sizeof(addr));
// 之后直接用 write(s, &frame, sizeof(frame));

3.2 struct can_frame 与 struct canfd_frame

这两个结构体是 CAN 数据的载体。经典 CAN 用 can_frame,CAN FD 用 canfd_frame。区别在哪?看表:

特性 can_frame canfd_frame
最大数据长度 8 字节 64 字节
速率 固定 1 Mbps 数据段可高达 8 Mbps
结构体大小 16 字节 72 字节
兼容性 所有 CAN 控制器 需 CAN FD 控制器

看代码更直观:

struct can_frame {
    canid_t can_id;   /* 32 bit CAN_ID + EFF/RTR/ERR flags */
    __u8    can_dlc;  /* 数据长度 (0..8) */
    __u8    __pad;    /* 填充字节 */
    __u8    __res0;   /* 保留 */
    __u8    __res1;   /* 保留 */
    __u8    data[8] __attribute__((aligned(8)));
};

struct canfd_frame {
    canid_t can_id;   /* 32 bit CAN_ID + EFF/RTR/ERR flags */
    __u8    len;      /* 数据长度 (0..64) */
    __u8    flags;    /* CAN FD 标志位 */
    __u8    __res0;   /* 保留 */
    __u8    __res1;   /* 保留 */
    __u8    data[64] __attribute__((aligned(8)));
};

注意 can_frame 里数据长度字段叫 can_dlc,而 canfd_frame 里叫 len。这个命名差异我一开始没注意,用 canfd_frame 时还习惯性地写 frame.can_dlc = 8,结果数据死活发不出去。嗯,这里要记住。

我的习惯:在不确定硬件是否支持 CAN FD 时,先用 can_frame 做原型验证。等硬件确认支持 FD 了,再切换到 canfd_frame。这样能避免早期调试时被 CAN FD 的兼容性问题干扰。

3.3 CAN ID 过滤与 mask 设置

这是 SocketCAN 最实用的功能之一。你想想看,一个 CAN 总线上可能有几十个节点,每秒上千条报文。如果你的应用只关心其中几个 ID,不做过滤的话,CPU 全浪费在处理无关报文上了。

过滤的核心是 can_filter 结构体:

struct can_filter {
    canid_t can_id;
    canid_t can_mask;
};

工作原理很简单:接收到的报文 ID 与 can_id 按位与 can_mask 后,如果等于 can_id & can_mask,就接收。否则丢弃。

举个例子:

struct can_filter rfilter[2];

// 只接收 ID 0x123 和 0x456
rfilter[0].can_id   = 0x123;
rfilter[0].can_mask = CAN_SFF_MASK;  // 标准帧掩码 0x7FF
rfilter[1].can_id   = 0x456;
rfilter[1].can_mask = CAN_SFF_MASK;

setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

如果你想接收一组连续的 ID,比如 0x100 到 0x10F,可以用 mask 实现:

rfilter[0].can_id   = 0x100;
rfilter[0].can_mask = 0x7F0;  // 只比较高 7 位,低 4 位任意

这样 ID 0x100~0x10F 都会被接收。我在做电机控制器调试时,就用这个技巧一次性捕获了所有 16 个控制报文,省去了逐个配置过滤器的麻烦。

曾经踩过的坑:过滤规则是按顺序匹配的,匹配到第一条就停止。如果你把宽匹配规则放在前面,后面的精确匹配可能永远用不上。我曾经把 can_mask = 0(接收所有报文)放在第一条,结果后面的过滤规则全废了。

3.4 setsockopt() 选项详解

SocketCAN 通过 setsockopt() 提供了丰富的控制选项。我挑几个最常用的说:

选项 作用 典型值
CAN_RAW_FILTER 设置接收过滤规则 struct can_filter 数组
CAN_RAW_ERR_FILTER 设置错误帧过滤 can_err_mask_t
CAN_RAW_LOOPBACK 控制本地回环 1 开启 / 0 关闭
CAN_RAW_RECV_OWN_MSGS 是否接收自己发送的报文 1 接收 / 0 不接收
CAN_RAW_FD_FRAMES 启用 CAN FD 支持 1 启用

错误帧过滤这个选项,很多人会忽略。但实际项目中,总线错误是家常便饭。我建议至少把总线关闭和错误被动这两种错误打开:

can_err_mask_t err_mask = CAN_ERR_TX_TIMEOUT | CAN_ERR_BUSOFF | CAN_ERR_CRTL;
setsockopt(s, SOL_CAN_RAW, CAN_RAW_ERR_FILTER, &err_mask, sizeof(err_mask));

这样当总线出现严重错误时,你能第一时间收到通知,而不是等应用层超时才发现。

本地回环选项也值得注意。默认情况下,你发送的报文会回环到本机的其他套接字。如果你在同一个进程里既发又收,可能会收到自己发的报文。关闭回环可以避免这种干扰:

int loopback = 0;
setsockopt(s, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));

核心要点:SocketCAN 的 API 设计遵循了「简单接口 + 灵活选项」的思路。socket/bind/connect 是骨架,can_frame/canfd_frame 是血肉,过滤和 setsockopt 是灵魂。把这几个点吃透,CAN 通信的基本框架就搭起来了。

SocketCAN 核心 API 知识体系 应用层程序 socket() / bind() / connect() setsockopt() 选项配置 struct can_frame / struct canfd_frame 数据封装与解析 CAN ID 过滤与 mask 设置 can_filter 结构体 + setsockopt 配置 Linux CAN 驱动 / 硬件控制器 从应用层到硬件,SocketCAN 通过四层结构完成 CAN 通信的完整链路

这张图把整个 API 调用链路串起来了。从上到下,应用层通过 socket/bind/connect 建立通道,用 setsockopt 配置行为,然后通过 can_frame/canfd_frame 承载数据,经过过滤层筛选,最终到达内核驱动和硬件。每一层都有它的作用,缺一不可。