首页 / 驱动算法 / CANopen专题

SocketCAN 入门:Linux CAN 通信编程

做嵌入式 Linux 与上位机联调时,最常问的一句是"CAN 口在 Linux 上怎么开?"。SocketCAN 给出了标准答案:把 CAN 总线做成一个网络接口 can0,用 socket() 收发帧。本文从架构、帧结构、接口配置、工具链讲到内核过滤器,配一个帧布局与过滤器匹配的交互演示和一份可编译的 C 代码。

1. 为什么需要 SocketCAN:Linux 访问 CAN 的进化

在 SocketCAN 出现之前,Linux 访问 CAN 是"各自为政":每家芯片/板卡厂商自己写一个字符设备(/dev/can0)和一套 ioctl 接口,应用代码跟驱动强绑定,换一块 CAN 卡就要改一遍业务逻辑;上层协议(CANopen、J1939、诊断)更是每厂一套,无法移植。

2002 年起,Volkswagen Research 的 Oliver Hartkopp 发起 SocketCAN 项目,核心思想是:把 CAN 总线当作 Linux 网络协议栈的一等公民——像 eth0 一样注册成一个网络接口 can0,用标准的 socket() / bind() / read() / write() 读写,上层协议(CAN_RAW / CAN_BCM / CAN_ISOTP / CAN_J1939)全部内核化。2008 年 SocketCAN 进入内核主线(2.6.25),今天几乎所有主流发行版默认支持。

✅ 一句话记住 SocketCAN

SocketCAN = "CAN 也是网络接口"can0 是网卡,socket(AF_CAN, SOCK_RAW, CAN_RAW) 是打开 CAN 的方式,struct can_frame 是每一帧的样子。

2. SocketCAN 架构:分层看一条帧的旅程

一帧数据从应用层到总线,经过三层:应用通过 AF_CAN 协议族调用内核 CAN 协议(协议层),协议层把帧交给 CAN 设备驱动(驱动层),驱动通过控制器(CAN controller)收发。整条路径都在内核里,应用只看到一个 socket。

层次组成职责
应用层用户程序 / can-utils 工具socket() 打开、bind() 绑定接口、read/write 收发、setsockopt 设过滤器
协议层(AF_CAN)CAN_RAW / CAN_BCM / CAN_ISOTP / CAN_J1939帧格式解析、过滤、广播管理、诊断分段重组、透明传输
驱动层can-dev 框架 + 控制器驱动(MCP251x、SJA1000、peak_usb、gs_usb…)帧收发、波特率配置、错误状态上报(bus-off/passive)
虚拟层vcan(虚拟总线)无硬件调试:多进程/多接口共享一条虚拟总线

AF_CAN 协议族下共五种协议,按需选择:

协议类型值用途典型场景
CAN_RAW1原始帧收发,最常用自定义协议、CANopen 底层承载
CAN_BCM2广播管理:周期发送、内容变化发送、内核侧接收过滤周期状态上报、少 CPU 占用
CAN_ISOTP4ISO 15765-2 诊断传输层(单帧/多帧分包)UDS/OBD 车载诊断(内核 3.15+)
CAN_J19395SAE J1939 卡车/工程机械协议农用机械、商用车(内核 4.19+)
ℹ️ CANopen 与 SocketCAN 的关系

CANopen 是建立在 CAN 之上、寻址到"索引:子索引"的高层协议(见本站 CANopen 专题)。SocketCAN 恰好是它在 Linux 上的"传输底座":PDO/SDO 报文最终都是通过 CAN_RAW 帧发出去的。先懂 CAN 帧,再学 CANopen,顺序正好。

3. CAN 帧结构:can_frame 与 CAN FD

内核用 struct can_frame 表示一帧经典 CAN(CAN 2.0),用 struct canfd_frame 表示 CAN FD 帧。它们是编程的核心数据结构(定义在 <linux/can.h>):

/* 经典 CAN 帧:最多 8 字节(16 字节对齐结构) */ struct can_frame { canid_t can_id; /* 32 位:11 位标准ID / 29 位扩展ID + EFF/RTR/ERR 标志位 */ __u8 can_dlc; /* 数据长度 0..8(经典帧) */ __u8 __pad; __u8 __res0; __u8 __res1; __u8 data[8] __attribute__((aligned(8))); }; /* CAN FD 帧:最多 64 字节(72 字节结构) */ struct canfd_frame { canid_t can_id; /* 同 can_frame */ __u8 len; /* 数据长度 0..64 */ __u8 flags; /* CANFD_BRS(0x01) 数据段高波特率 / CANFD_ESI(0x02) 错误状态指示 */ __u8 __res0; __u8 __res1; __u8 data[64] __attribute__((aligned(8))); };

can_id 不是普通整数,高 3 位是标志:

含义
CAN_EFF_FLAG0x80000000置 1 = 扩展帧(29 位 ID),低 29 位是 ID
CAN_RTR_FLAG0x40000000置 1 = 远程帧(RTR,请求对端发数据)
CAN_ERR_FLAG0x20000000置 1 = 错误帧(内核上报总线错误)
CAN_SFF_MASK0x000007FF标准帧 ID 掩码(11 位)
CAN_EFF_MASK0x1FFFFFFF扩展帧 ID 掩码(29 位)

在总线上,一帧经典标准帧(8 字节数据)编码为约 111 位(无位填充):

经典标准帧位时间估算

SOF(1) + ID(11) + RTR(1) + IDE(1) + r0(1) + DLC(4) + 数据(8×8=64) + CRC(15) + Delimiter(1) + ACK(2) + EOF(7) + IFS(3) = 111 位

位填充规则:连续 5 个相同电平插入 1 个反相填充位,最多约 130 位 → 500kbps 下 222~260 µs

经典 CAN 与 CAN FD 的差异,一张表说清:

特性经典 CAN(2.0)CAN FD
单帧数据最多 8 字节最多 64 字节
波特率全程单一(如 500kbps)仲裁段/数据段双波特率(BRS 位切换)
CRC15 位17 / 21 位(覆盖更多数据,检错更强)
内核结构体can_framecanfd_frame(收发前需开 FD 选项)
兼容性CAN FD 节点能收经典帧纯 2.0 节点不理解 FD 帧(混跑需 FD 帧隔离/掩码)

4. 接口配置:ip link 与 vcan

SocketCAN 接口用 ip 命令配置(老工具 canconfig 已废弃):

# 配置波特率并启动(500kbps) sudo ip link set can0 type can bitrate 500000 sudo ip link set can0 up # 查看接口详情:波特率、错误状态、报文计数 ip -details link show can0 # 无硬件调试:创建虚拟总线 vcan0 sudo modprobe vcan sudo ip link add dev vcan0 type vcan sudo ip link set vcan0 up # CAN FD:仲裁段 500k + 数据段 2M,fd on sudo ip link set can0 type can bitrate 500000 dbitrate 2000000 fd on # 总线关闭自动恢复:每 100ms 尝试恢复(默认不自动恢复) sudo ip link set can0 type can restart-ms 100

看到 NO-CARRIER(接口 up 但没有总线上电)是正常的,CAN 控制器探测到总线后状态变为 UP

5. can-utils 工具链:先会看帧,再写代码

调试 SocketCAN 第一件事是装 can-utilsapt install can-utils),它是 SocketCAN 的官方用户态工具集:

工具用途示例
candump抓取总线帧candump can0candump can0 -n 10 抓 10 帧退出
cansend发送一帧cansend can0 123#DEADBEEF;远程帧 123#R;FD 帧 123##1DEADBEEF
cangen周期/随机压帧cangen can0 -g 10 -L 8 每 10ms 发 8 字节随机帧
cansequence丢帧/乱序检测对端配对运行,统计丢帧率
canfdtestCAN FD 吞吐/压力测试canfdtest can0
cansniffer按 ID 筛选的嗅探器交互式看特定 ID 数据变化
canplayer回放 candump 日志canplayer -I candump.log

can-utils 全部基于 AF_CAN 编程接口实现——看懂 candump 的源码,就是最好的 SocketCAN 入门教材。

6. 交互式演示:CAN 帧布局与内核过滤器匹配

上半部分画出经典标准帧/扩展帧的位级布局(这就是 can_frame 在总线上编码的样子);下半部分演示内核过滤器规则 (接收ID & mask) == (过滤器ID & mask)——改一个掩码位,看匹配结果如何变化。

CAN 帧布局与接收过滤器匹配演示
帧布局:标准帧 · 111 位基础 · 过滤器计算 (123 & 7FF) == (120 & 7FF) → 不匹配(接收丢弃)

观察要点:掩码为 0 时过滤器全放行;掩码为 7FF(11 位全 1)时要求 ID 逐位相等。内核在驱动层完成过滤,不匹配的帧根本不会进入应用 socket——这就是"过滤下推"。

7. 代码实现(C语言,完整可编译)

下面是一份完整的 SocketCAN 示例:打开 can0、设置过滤器、发送一帧、接收循环打印,末尾附 CAN FD 发送。gcc 直接编译:

/* socketcan_demo.c —— SocketCAN 原始帧收发示例 * 编译:gcc -o socketcan_demo socketcan_demo.c * 运行:sudo ./socketcan_demo can0 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <net/if.h> #include <linux/can.h> #include <linux/can/raw.h> /* 打开并绑定 CAN 接口(返回 socket 句柄) */ static int can_open(const char *ifname) { struct sockaddr_can addr; struct ifreq ifr; int s = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (s < 0) { perror("socket"); return -1; } /* 接口名 -> ifindex */ strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl"); close(s); return -1; } addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); close(s); return -1; } return s; } /* 发送一帧标准数据帧 */ static int can_send(int s, canid_t id, const uint8_t *data, uint8_t dlc) { struct can_frame frame; memset(&frame, 0, sizeof(frame)); frame.can_id = id; /* 标准帧 11 位;扩展帧要 | CAN_EFF_FLAG */ frame.can_dlc = dlc; /* 0..8 */ memcpy(frame.data, data, dlc); return (int)write(s, &frame, sizeof(frame)); } int main(int argc, char **argv) { const char *ifname = argc > 1 ? argv[1] : "can0"; int s = can_open(ifname); if (s < 0) return 1; printf("bound to %s, socket=%d\n", ifname, s); /* 过滤器:只接收 ID=0x123 的标准帧(内核侧过滤) */ struct can_filter rfilter[1]; rfilter[0].can_id = 0x123; rfilter[0].can_mask = CAN_SFF_MASK; setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter)); /* 发送一帧 0x123#DEADBEEF */ uint8_t tx[] = { 0xDE, 0xAD, 0xBE, 0xEF }; can_send(s, 0x123, tx, sizeof(tx)); /* 接收循环 */ for (;;) { struct can_frame rx; ssize_t n = read(s, &rx, sizeof(rx)); if (n < 0) { perror("read"); break; } if (n < (ssize_t)sizeof(rx)) continue; /* 短帧(如错误帧)忽略 */ if (rx.can_id & CAN_ERR_FLAG) { /* 错误帧:打印并继续 */ printf("ERR frame: 0x%08X dlc=%u\n", rx.can_id, rx.can_dlc); continue; } printf("RX %s 0x%03X [%u]", (rx.can_id & CAN_EFF_FLAG) ? "EFF" : "SFF", rx.can_id & CAN_SFF_MASK, rx.can_dlc); for (int i = 0; i < rx.can_dlc; i++) printf(" %02X", rx.data[i]); printf("\n"); } close(s); return 0; }

7.1 CAN FD 发送

CAN FD 帧要用 canfd_frame,且必须先通过 setsockopt 打开 FD 支持,否则内核会拒绝:

/* CAN FD 发送:打开接口的 FD 支持并发送 64 字节帧 */ int canfd_open_send(const char *ifname) { int s = socket(PF_CAN, SOCK_RAW, CAN_RAW); int on = 1; setsockopt(s, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &on, sizeof(on)); /* 必须! */ struct canfd_frame f; memset(&f, 0, sizeof(f)); f.can_id = 0x123; f.len = 64; /* 0..64 */ f.flags = CANFD_BRS; /* 数据段切换高波特率(总线需支持 FD) */ for (int i = 0; i < 64; i++) f.data[i] = (uint8_t)i; return (int)write(s, &f, sizeof(f)); /* 72 字节 */ }

8. 工程要点与常见误区

要点 1:先用 vcan 把逻辑调通

没有硬件也能开发:vcan0 让两个 socket 像挂在同一总线上一样通信。协议逻辑、过滤器、CANopen 栈移植先用 vcan 验证,再接真硬件。

要点 2:过滤器要"下推"到内核

CAN_RAW_FILTER 在驱动/协议层过滤,不匹配的帧不进应用,省 CPU 且不丢负载。在用户态自己 if (id==xx) 判断是浪费——尤其是高负载总线上。

要点 3:错误状态必须监控

总线上电、线缆问题、波特率不匹配会让控制器进入 ERROR-PASSIVE 甚至 BUS-OFF。默认不自动恢复,要么配 restart-ms,要么在用户态监听错误帧(CAN_ERR_FLAG)主动恢复并告警。看到"发送失败 errno=ENETDOWN",先查接口是不是 bus-off 了。

要点 4:SocketCAN 不是硬实时通道

帧路径经过中断 + softirq + 协议层,有调度抖动。做"每 1ms 同步伺服"请走 CANopen 同步 PDO + 高优先级进程/实时内核;SocketCAN 本身保证顺序与完整性,不保证微秒级时延。

误区 1:扩展帧忘了置 CAN_EFF_FLAG

29 位 ID 必须 can_id = CAN_EFF_FLAG | id29,否则内核当成 11 位标准帧,ID 被截断——接收端怎么都对不上。

误区 2:can_frame 和 canfd_frame 混用

两种结构体大小不同、长度字段不同(can_dlc vs len),收发要严格配套;发 FD 帧不开 CAN_RAW_FD_FRAMES 会被内核拒绝。

误区 3:以为"8 字节"是 CAN 的极限

经典 CAN 是 8 字节,CAN FD 单帧 64 字节、双波特率,整包传输效率大幅提升;选型时如果数据量大(如 32 字节固件升级块),优先考虑 FD。

9. 小结

  1. SocketCAN 把 CAN 总线变成网络接口 can0,标准 socket API 读写,驱动层过滤
  2. 协议族五种:CAN_RAW(通用)/ CAN_BCM(广播管理)/ CAN_ISOTP(诊断)/ CAN_J1939(工程机械)
  3. can_frame = can_id(含 EFF/RTR/ERR 标志)+ can_dlc + data[8];CAN FD 用 canfd_frame 最高 64 字节
  4. 配置走 ip link set can0 type can bitrate ...,调试先上 vcan + candump/cansend
  5. 内核过滤器规则 (rx_id & mask) == (filter_id & mask),过滤务必下推内核
  6. CANopen/J1939 等高层协议都跑在 SocketCAN 之上——CAN 帧学明白,上层协议就剩语义了

下一篇进入 CAN FD 双波特率:采样点、数据域与仲裁段——为什么仲裁段不能提速、数据域却能跑 2M,位时间结构和采样点怎么配。