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 = "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_RAW | 1 | 原始帧收发,最常用 | 自定义协议、CANopen 底层承载 |
| CAN_BCM | 2 | 广播管理:周期发送、内容变化发送、内核侧接收过滤 | 周期状态上报、少 CPU 占用 |
| CAN_ISOTP | 4 | ISO 15765-2 诊断传输层(单帧/多帧分包) | UDS/OBD 车载诊断(内核 3.15+) |
| CAN_J1939 | 5 | SAE J1939 卡车/工程机械协议 | 农用机械、商用车(内核 4.19+) |
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_id 不是普通整数,高 3 位是标志:
| 宏 | 值 | 含义 |
|---|---|---|
CAN_EFF_FLAG | 0x80000000 | 置 1 = 扩展帧(29 位 ID),低 29 位是 ID |
CAN_RTR_FLAG | 0x40000000 | 置 1 = 远程帧(RTR,请求对端发数据) |
CAN_ERR_FLAG | 0x20000000 | 置 1 = 错误帧(内核上报总线错误) |
CAN_SFF_MASK | 0x000007FF | 标准帧 ID 掩码(11 位) |
CAN_EFF_MASK | 0x1FFFFFFF | 扩展帧 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 位切换) |
| CRC | 15 位 | 17 / 21 位(覆盖更多数据,检错更强) |
| 内核结构体 | can_frame | canfd_frame(收发前需开 FD 选项) |
| 兼容性 | CAN FD 节点能收经典帧 | 纯 2.0 节点不理解 FD 帧(混跑需 FD 帧隔离/掩码) |
4. 接口配置:ip link 与 vcan
SocketCAN 接口用 ip 命令配置(老工具 canconfig 已废弃):
看到 NO-CARRIER(接口 up 但没有总线上电)是正常的,CAN 控制器探测到总线后状态变为 UP。
5. can-utils 工具链:先会看帧,再写代码
调试 SocketCAN 第一件事是装 can-utils(apt install can-utils),它是 SocketCAN 的官方用户态工具集:
| 工具 | 用途 | 示例 |
|---|---|---|
| candump | 抓取总线帧 | candump can0;candump can0 -n 10 抓 10 帧退出 |
| cansend | 发送一帧 | cansend can0 123#DEADBEEF;远程帧 123#R;FD 帧 123##1DEADBEEF |
| cangen | 周期/随机压帧 | cangen can0 -g 10 -L 8 每 10ms 发 8 字节随机帧 |
| cansequence | 丢帧/乱序检测 | 对端配对运行,统计丢帧率 |
| canfdtest | CAN 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)——改一个掩码位,看匹配结果如何变化。
观察要点:掩码为 0 时过滤器全放行;掩码为 7FF(11 位全 1)时要求 ID 逐位相等。内核在驱动层完成过滤,不匹配的帧根本不会进入应用 socket——这就是"过滤下推"。
7. 代码实现(C语言,完整可编译)
下面是一份完整的 SocketCAN 示例:打开 can0、设置过滤器、发送一帧、接收循环打印,末尾附 CAN FD 发送。gcc 直接编译:
7.1 CAN FD 发送
CAN FD 帧要用 canfd_frame,且必须先通过 setsockopt 打开 FD 支持,否则内核会拒绝:
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. 小结
- SocketCAN 把 CAN 总线变成网络接口
can0,标准 socket API 读写,驱动层过滤 - 协议族五种:CAN_RAW(通用)/ CAN_BCM(广播管理)/ CAN_ISOTP(诊断)/ CAN_J1939(工程机械)
can_frame= can_id(含 EFF/RTR/ERR 标志)+ can_dlc + data[8];CAN FD 用canfd_frame最高 64 字节- 配置走
ip link set can0 type can bitrate ...,调试先上vcan+candump/cansend - 内核过滤器规则
(rx_id & mask) == (filter_id & mask),过滤务必下推内核 - CANopen/J1939 等高层协议都跑在 SocketCAN 之上——CAN 帧学明白,上层协议就剩语义了
下一篇进入 CAN FD 双波特率:采样点、数据域与仲裁段——为什么仲裁段不能提速、数据域却能跑 2M,位时间结构和采样点怎么配。