Linux CAN子系统:SocketCAN架构、CAN设备驱动模型、VxCAN与CAN_RAW协议
好,咱们今天来啃一块硬骨头——Linux CAN子系统。说实话,我刚接触这块的时候也懵过。SocketCAN、CAN设备驱动、VxCAN、CAN_RAW……一堆名词砸过来,容易让人找不着北。但别急,我带你一层层拆开看。
先给你个全景图。Linux CAN子系统,说白了就是三部分:
- 最上层——用户空间的Socket接口,你写应用程序调的就是它
- 中间层——协议族和协议实现,比如CAN_RAW、CAN_BCM、VxCAN
- 最下层——CAN设备驱动,直接跟硬件打交道
嗯,这个分层结构,跟网络协议栈的思路很像。你想想看,TCP/IP也是这么干的——应用层、传输层、网络层、链路层。SocketCAN就是把CAN总线当成一个网络设备来管理。
SocketCAN架构——为什么它比老式驱动好?
在SocketCAN出现之前,Linux上的CAN驱动各自为政。每家芯片厂商自己搞一套字符设备驱动,应用程序得用不同的ioctl。那叫一个乱。
SocketCAN的牛逼之处在于:它把CAN设备当成网络设备来管理。你操作CAN总线,就像操作一个以太网卡一样。打开socket,bind,read/write,send/recv——全是标准POSIX接口。
核心概念:SocketCAN 在 Linux 内核中注册为 AF_CAN 协议族(PF_CAN)。它位于 net/can/ 目录下,核心文件包括 af_can.c、raw.c、bcm.c、gw.c 等。
我个人习惯,调试CAN总线时第一件事就是看 /proc/net/can/stats。这个文件能告诉你内核CAN子系统的收发统计,比如发送了多少帧、接收了多少帧、丢了多少帧。有一次我在现场排查丢帧问题,就是靠这个文件定位到是硬件FIFO溢出,而不是软件问题。
CAN设备驱动模型——从硬件到SocketCAN的桥梁
CAN设备驱动,说白了就是让SocketCAN能跟你的CAN控制器对话。Linux内核里,CAN驱动遵循标准的网络设备驱动模型。
驱动需要实现的核心结构体有两个:
| 结构体 | 作用 | 关键字段 |
|---|---|---|
struct net_device |
网络设备抽象,SocketCAN用它来收发数据 | netdev_ops, flags, mtu, dev_addr |
struct can_priv |
CAN控制器私有数据,包含硬件配置 | state, ctrlmode, bittiming, do_set_mode |
驱动开发中,你主要实现这几个回调函数:
- ndo_open——打开设备,配置CAN控制器,设置波特率
- ndo_stop——关闭设备,停止CAN控制器
- ndo_start_xmit——发送CAN帧,把数据写入硬件发送FIFO
- 中断处理函数——接收CAN帧,从硬件读取数据,调用
netif_rx()上报
我的经验:写CAN驱动时,can_priv 里的 do_set_mode 回调特别容易踩坑。这个回调负责切换CAN控制器的状态(比如从停止到运行)。我曾经在一个项目中,因为没处理好状态切换的时序,导致CAN控制器在高速通信时频繁进入总线关闭状态。排查了两天才发现是驱动里少了一个延时。
来看一个简化的驱动初始化流程:
static int can_probe(struct platform_device *pdev)
{
struct net_device *ndev;
struct can_priv *priv;
// 1. 分配 net_device,can_priv 作为私有数据
ndev = alloc_candev(sizeof(struct can_priv), TX_ECHO_SKB_MAX);
if (!ndev)
return -ENOMEM;
priv = netdev_priv(ndev);
// 2. 设置网络设备操作函数
ndev->netdev_ops = &can_netdev_ops;
// 3. 设置CAN协议相关操作
priv->do_set_mode = can_set_mode;
priv->do_get_state = can_get_state;
priv->do_set_bittiming = can_set_bittiming;
// 4. 注册网络设备
register_candev(ndev);
return 0;
}
嗯,这里要注意:alloc_candev 和 register_candev 是SocketCAN提供的辅助函数。它们帮你处理了很多底层细节,比如echo skb的管理、CAN状态机的维护。我建议你写驱动时尽量用这些标准接口,别自己造轮子。
CAN_RAW协议——最常用的SocketCAN协议
CAN_RAW,顾名思义,就是原始CAN帧协议。它不做任何协议解析,直接把CAN帧从用户空间传到硬件,或者从硬件传到用户空间。
应用程序用CAN_RAW的典型流程:
socket(AF_CAN, SOCK_RAW, CAN_RAW)——创建原始CAN套接字bind()——绑定到具体的CAN接口(比如can0)setsockopt()——设置过滤规则、接收超时等选项read()/write()或send()/recv()——收发CAN帧
代码示例:
#include <linux/can.h>
#include <linux/can/raw.h>
int s;
struct sockaddr_can addr;
struct can_frame frame;
// 创建原始CAN套接字
s = socket(AF_CAN, SOCK_RAW, CAN_RAW);
// 绑定到 can0 接口
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));
// 设置过滤规则:只接收 ID 为 0x123 的帧
struct can_filter rfilter;
rfilter.can_id = 0x123;
rfilter.can_mask = CAN_SFF_MASK;
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
// 发送一帧数据
frame.can_id = 0x456;
frame.can_dlc = 4;
frame.data[0] = 0x11;
frame.data[1] = 0x22;
frame.data[2] = 0x33;
frame.data[3] = 0x44;
write(s, &frame, sizeof(frame));
注意:CAN_RAW 的过滤规则是 与操作 匹配的。也就是说,can_id & can_mask 必须等于 rfilter.can_id & rfilter.can_mask。如果你想要接收所有帧,把 can_mask 设为 0 即可。
VxCAN——虚拟CAN总线,调试利器
VxCAN,全称是 Virtual CAN。它是个纯软件实现的虚拟CAN设备,不依赖任何硬件。
为什么需要它?你想想看,做CAN开发时,硬件不一定随时在手边。或者你写了个应用程序,想先做单元测试,不想每次都接上真实的CAN总线。这时候VxCAN就派上用场了。
加载VxCAN模块:
# 加载虚拟CAN驱动
modprobe can
modprobe can_raw
modprobe vxcan
# 创建一对虚拟CAN设备
ip link add dev vcan0 type vxcan
ip link add dev vcan1 type vxcan
# 启用设备
ip link set vcan0 up
ip link set vcan1 up
VxCAN设备是成对出现的。vcan0和vcan1之间有一条虚拟的CAN总线。你在vcan0上发的帧,vcan1能收到;反过来也一样。这跟虚拟以太网对(veth pair)的原理很像。
我的习惯:每次开始一个新项目,我都会先搭一套VxCAN环境。用candump监听vcan0,用cansend往vcan1发数据,验证应用程序的逻辑是否正确。等应用层调通了,再连真实硬件。这样能省下大量调试时间。
我曾经在一个项目中,客户反馈CAN通信偶尔丢帧。我一开始怀疑是硬件问题,但用VxCAN模拟了同样的应用逻辑,发现丢帧现象在虚拟环境下也能复现。最后定位到是应用程序里多线程并发访问同一个socket导致的竞态问题。如果没有VxCAN,这个bug在真实硬件上排查会困难得多。
CAN_RAW vs VxCAN——什么时候用哪个?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 应用层开发调试 | VxCAN | 无需硬件,快速验证逻辑 |
| 驱动开发 | 真实硬件 + CAN_RAW | 需要验证硬件时序和中断处理 |
| 协议栈测试 | VxCAN + CAN_RAW | VxCAN模拟总线,CAN_RAW收发帧 |
| 性能压测 | 真实硬件 | VxCAN无法模拟硬件延迟和总线仲裁 |
说白了,VxCAN是你的调试左膀,CAN_RAW是你的应用右臂。两者配合使用,能覆盖大部分开发场景。
避坑指南——我踩过的那些坑
做CAN开发这些年,我踩过不少坑。挑几个典型的跟你说说:
- 波特率不匹配——CAN总线上所有节点的波特率必须一致。我曾经在调试时,一个节点设了250k,另一个设了500k,结果总线一直报错。用
ip -details link show can0查看当前波特率,能快速定位问题。 - CAN帧格式混淆——标准帧(11位ID)和扩展帧(29位ID)不能混用。发送时
can_id要设置CAN_EFF_FLAG标志位。接收时也要检查这个标志位。 - 回环模式——调试时记得关闭回环模式,否则自己发的帧自己也能收到,容易造成逻辑混乱。用
ip link set can0 type can loopback off关闭。 - 中断上下文不能睡眠——CAN驱动的中断处理函数里,千万别调用
msleep()或mutex_lock()。我见过一个驱动在中断里调了msleep(1),结果系统直接死锁。
一句话总结:SocketCAN 把 CAN 总线抽象成网络设备,CAN_RAW 提供原始帧收发接口,VxCAN 提供虚拟调试环境。这三者构成了 Linux CAN 开发的基石。