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