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 三层架构 用户空间 应用程序(candump, cansend, 自研App) 内核空间 - SocketCAN核心 协议族 AF_CAN / PF_CAN 协议实现:CAN_RAW | CAN_BCM | VxCAN | CAN_ISOTP 硬件抽象层 - CAN设备驱动 struct net_device + struct can_priv SocketCAN 将 CAN 总线抽象为网络设备,应用程序通过标准 Socket API 操作 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的典型流程:

  1. socket(AF_CAN, SOCK_RAW, CAN_RAW)——创建原始CAN套接字
  2. bind()——绑定到具体的CAN接口(比如can0)
  3. setsockopt()——设置过滤规则、接收超时等选项
  4. 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 开发的基石。