3、Linux内核CAN子系统:SocketCAN架构、CAN协议栈、CAN设备驱动模型

好,咱们今天来聊聊Linux内核里的CAN子系统。说实话,我刚接触这块的时候,也被那一堆层次结构搞得有点晕。但后来我发现,只要抓住SocketCAN这个核心,其他东西就都顺了。

SocketCAN,说白了就是把CAN总线抽象成了一个网络设备。你想想看,我们平时用socket发TCP数据包,现在用同样的接口来发CAN帧,是不是很优雅?我个人觉得,这是Linux内核设计最巧妙的地方之一。

3.1 SocketCAN架构概览

SocketCAN不是凭空冒出来的。在它出现之前,Linux上的CAN驱动基本都是各玩各的——每个芯片厂商提供一套自己的字符设备接口,应用程序得针对不同硬件写不同的代码。那叫一个乱。

SocketCAN把CAN设备纳入了网络子系统,统一用PF_CAN协议族。这样一来,应用程序只需要打开一个socket,剩下的交给内核去调度。我在项目中遇到过好几次,从SJA1000换到MCP2515,应用层代码一行都不用改,这就是架构的力量。

核心思想: CAN设备 → 网络设备 → Socket接口 → 应用层

整个架构分三层:

  • 应用层: 用户程序通过socket()、bind()、read()/write()操作CAN数据
  • 协议层: 内核中的CAN协议栈,负责帧封装、过滤、协议解析
  • 驱动层: 具体硬件驱动,负责收发CAN帧,处理中断

嗯,这里要注意,SocketCAN不是简单地把CAN帧塞进网络协议栈,它有自己的地址族和协议类型。你打开一个CAN socket时,用的是AF_CAN(或者PF_CAN),而不是AF_INET。

3.2 CAN协议栈:从帧到socket

协议栈这部分,我建议你把它理解成一个加工流水线。原始CAN帧从硬件上来,经过协议栈的层层处理,最后变成应用程序能读的数据。

先看一个最基本的CAN帧结构:

struct can_frame {
    canid_t can_id;   /* CAN ID,32位,包含EFF/RTR/ERR标志 */
    __u8    can_dlc;  /* 数据长度,0-8 */
    __u8    __pad;    /* 填充字节 */
    __u8    __res0;   /* 保留 */
    __u8    __res1;   /* 保留 */
    __u8    data[8] __attribute__((aligned(8)));
};

这个结构体,你写驱动的时候天天见。can_id里藏了很多信息——标准帧还是扩展帧、远程帧还是数据帧、错误帧。我刚开始做的时候,就踩过一个坑:直接拿can_id去比较,结果把扩展帧的标志位也算进去了。

避坑指南: 我曾经在过滤CAN ID时,忘了屏蔽EFF标志位,导致标准帧和扩展帧匹配混乱。正确的做法是用can_id & CAN_EFF_MASK提取纯ID值。

协议栈里还有几个关键组件:

  • RAW socket: 最常用,直接收发原始CAN帧,不做任何协议解析
  • BCM socket: 广播管理器,适合周期性发送和接收过滤
  • ISOTP: 基于CAN的ISO-TP协议,处理长数据分包
  • CAN网关: 内核中的路由转发,实现CAN网桥

你想想看,如果没有SocketCAN,这些功能都得应用层自己实现,那得多痛苦。

3.3 CAN设备驱动模型

驱动模型这块,我直接给你画个图,比说一万句话都管用。

SocketCAN设备驱动模型架构 SocketCAN 设备驱动模型架构 应用层 用户程序 · socket(AF_CAN, SOCK_RAW, CAN_RAW) · read() / write() CAN 协议层 (net/can/) RAW协议 · BCM协议 · ISOTP · CAN网关 · 路由过滤 核心:af_can.c · raw.c · bcm.c · gw.c CAN 设备驱动层 (drivers/net/can/) net_device · can_priv · can_berr_counter · can_rx_offload 核心接口:open() · close() · start_xmit() · do_set_mode() 硬件抽象层 SPI适配器 · 内存映射IO · 中断处理 · DMA传输 物理硬件 MCP2515 · SJA1000 · FlexCAN · CCAN 应用层无需关心 底层硬件差异 协议栈提供 统一Socket接口 驱动层实现 net_device_ops

这张图你看明白了吗?从上到下,每一层都只跟相邻层打交道。应用层不知道你用的是SPI接口的MCP2515还是内存映射的SJA1000,它只管发数据。

驱动层里,最核心的是struct net_device_ops。你写驱动时,主要就是填充这个结构体:

static const struct net_device_ops can_netdev_ops = {
    .ndo_open       = can_open,
    .ndo_stop       = can_close,
    .ndo_start_xmit = can_start_xmit,
    .ndo_change_mtu = can_change_mtu,
};

这四个回调函数,基本就是驱动的主干。ndo_open里做硬件初始化、申请中断;ndo_stop里关中断、停硬件;ndo_start_xmit负责把上层传下来的skb发出去。

个人经验: 我建议你在ndo_open里先调用open_candev(),这个函数会帮你做很多通用检查,比如设备是否已经在运行、时钟是否有效。别自己从头写,内核已经帮你铺好路了。

3.4 关键数据结构一览

写CAN驱动,有几个数据结构你必须烂熟于心。我整理了一个表格,方便你对照:

数据结构 所在文件 作用 关键字段
struct can_priv include/linux/can/dev.h CAN设备私有数据,驱动核心 state, ctrlmode, bittiming, data_bittiming
struct can_frame include/uapi/linux/can.h CAN帧数据结构,用户态和内核态共用 can_id, can_dlc, data[8]
struct net_device include/linux/netdevice.h 网络设备通用结构,CAN设备继承它 netdev_ops, mtu, flags, dev_addr
struct sk_buff include/linux/skbuff.h 网络缓冲区,CAN帧封装在其中 data, len, protocol, dev
struct can_bittiming include/linux/can/dev.h CAN位时序参数 bitrate, sample_point, tq, prop_seg, phase_seg1, phase_seg2, sjw

这里我要特别说一下struct can_priv。这个结构体是CAN驱动的"户口本",你所有的硬件状态、配置参数都放在里面。我习惯在驱动probe函数里,用alloc_candev()分配它,而不是自己手动malloc。

3.5 注册与注销流程

驱动加载和卸载的流程,我总结成几个步骤,你照着这个顺序写,基本不会出错:

  1. probe阶段: 调用alloc_candev()分配设备,设置netdev_ops和can_priv参数,最后register_candev()注册
  2. open阶段: 上电、配置位时序、启动硬件、申请中断、调用netif_start_queue()
  3. 发送阶段: ndo_start_xmit里把skb里的can_frame取出来,写入硬件发送缓冲区,发送完成后调用can_put_echo_skb()和can_get_echo_skb()
  4. 接收阶段: 中断处理函数里读取硬件接收缓冲区,构造skb,调用netif_rx()或napi_gro_receive()
  5. close阶段: 关中断、停硬件、调用netif_stop_queue()
  6. remove阶段: unregister_candev()注销,free_candev()释放

注意: 发送完成中断里,一定要调用can_put_echo_skb()来管理回显skb。我曾经漏掉这一步,结果导致发送完成后系统崩溃——因为内核以为skb还在飞,实际上硬件已经发完了。

3.6 位时序与波特率配置

CAN总线的波特率配置,说难不难,说简单也不简单。它取决于三个参数:TQ(时间量子)、位段分割和采样点。

内核里用struct can_bittiming来描述这些参数。你可以在驱动里硬编码,也可以通过ip link set can0 bitrate 500000让内核自动计算。我个人建议用后者,省心。

但自动计算不是万能的。我记得有一次,客户要求采样点必须在75%,默认算法算出来是70%。最后我只能手动指定phase_seg1和phase_seg2的值,才把采样点调上去。

/* 手动配置位时序示例 */
struct can_bittiming bt = {
    .bitrate = 500000,
    .sample_point = 75,
    .tq = 125,        /* 时间量子,单位ns */
    .prop_seg = 2,
    .phase_seg1 = 3,
    .phase_seg2 = 2,
    .sjw = 1,
};

你想想看,如果不懂这些参数背后的原理,遇到这种问题就只能干瞪眼。

3.7 错误处理与状态管理

CAN驱动里,错误处理是重头戏。硬件随时可能报错——总线关闭、错误被动、ACK错误、填充错误……你的驱动得能处理这些情况。

内核提供了can_change_state()函数来统一管理设备状态。状态机是这样的:

  • CAN_STATE_ERROR_ACTIVE: 正常状态,可以正常收发
  • CAN_STATE_ERROR_WARNING: 错误计数超过96,但还能工作
  • CAN_STATE_ERROR_PASSIVE: 错误计数超过127,只能接收不能发送
  • CAN_STATE_BUS_OFF: 错误计数超过255,完全离线
  • CAN_STATE_STOPPED: 手动停止
  • CAN_STATE_SLEEPING: 休眠模式

我建议你在中断处理函数里,每次收发完成后都检查一下硬件错误寄存器,然后调用can_change_state()更新状态。这样上层协议栈才能及时知道总线状况。

小技巧: 总线关闭后,不要立即重新启动。硬件需要等待128个11个隐性位(Bus-Off Recovery Sequence)。你可以用can_restart()配合定时器来实现自动恢复。