4. 报文转发基本原理

好,咱们今天聊点硬核的——CAN驱动开发。很多工程师一听到“驱动”两个字就头大,觉得那是内核开发者的专利。其实不然,在Linux环境下,SocketCAN框架已经把大部分脏活累活都封装好了。你只需要搞清楚几个关键环节,就能让CAN设备跑起来。

我个人习惯把CAN驱动开发分成三层来看:设备注册层、网络协议层、应用接口层。今天咱们重点啃设备注册和数据收发API这两块。

核心观点:SocketCAN不是让你从零写驱动,而是让你在现成的框架里“填空”。你填对结构体,调对API,CAN报文就能在应用层和总线之间自由穿梭。

4.1 SocketCAN框架概览

SocketCAN是Linux内核为CAN总线专门打造的一套网络协议栈。它把CAN设备当作网络设备来管理,用套接字(socket)作为用户空间的访问接口。说白了,你操作CAN就像操作TCP/IP一样自然。

框架的核心组件包括:

  • CAN设备驱动:负责硬件初始化、中断处理、寄存器读写
  • CAN核心层:提供设备注册、协议族管理、过滤规则匹配
  • CAN协议族:AF_CAN,定义原始套接字、广播管理套接字等类型
  • CAN网络设备:以net_device结构体呈现,通过rtnetlink管理

我记得第一次接触SocketCAN时,最让我惊讶的是它居然用struct sock来管理CAN连接。你想想看,一个面向流媒体的网络栈,硬是被拿来处理CAN这种短帧、实时性要求高的总线——Linux内核社区的工程师们确实有两把刷子。

4.2 CAN设备注册流程

设备注册是整个驱动开发的起点。没有注册,内核就不知道你的CAN控制器存在。注册流程大致分四步:

  1. 分配net_device结构体:调用alloc_candev(),指定私有数据大小和TX队列深度
  2. 填充操作函数集:实现struct net_device_ops中的ndo_open、ndo_stop、ndo_start_xmit
  3. 设置硬件参数:配置时钟频率、位时序、中断号、DMA通道
  4. 注册到内核:调用register_candev(),让系统识别这个设备

来看一段典型的注册代码:

static int mycan_probe(struct platform_device *pdev)
{
    struct net_device *ndev;
    struct mycan_priv *priv;
    int err;

    // 1. 分配CAN网络设备
    ndev = alloc_candev(sizeof(struct mycan_priv), TX_ECHO_SKB_MAX);
    if (!ndev)
        return -ENOMEM;

    priv = netdev_priv(ndev);
    priv->ndev = ndev;

    // 2. 设置网络设备操作函数
    ndev->netdev_ops = &mycan_netdev_ops;
    ndev->flags |= IFF_ECHO;  // 需要硬件回显

    // 3. 设置硬件相关参数
    priv->can.clock.freq = 8000000;   // 8MHz时钟
    priv->can.bittiming.bitrate = 500000; // 500kbps
    priv->can.do_set_bittiming = mycan_set_bittiming;

    // 4. 注册CAN设备
    err = register_candev(ndev);
    if (err) {
        free_candev(ndev);
        return err;
    }

    platform_set_drvdata(pdev, ndev);
    return 0;
}

经验之谈:我曾经在某个项目里忘了设置IFF_ECHO标志,结果上层应用收不到自己发送的报文,排查了整整两天。这个标志告诉内核:硬件会回显发送的报文,不需要软件模拟。如果你的CAN控制器支持硬件回显,一定要加上。

4.3 数据收发API详解

设备注册好了,接下来就是收发数据。SocketCAN的数据收发API分为内核态和用户态两套。咱们先看内核态,再看用户态。

4.3.1 内核态发送:ndo_start_xmit

这是驱动发送报文的入口函数。当上层调用dev_queue_xmit()时,最终会回调到这里。你需要做的是:

  • 从sk_buff中提取CAN帧
  • 将帧数据写入硬件发送寄存器
  • 触发硬件发送
  • 处理发送完成中断
static netdev_tx_t mycan_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
    struct mycan_priv *priv = netdev_priv(ndev);
    struct can_frame *cf = (struct can_frame *)skb->data;
    u32 id, dlc, data[2];

    // 检查硬件是否空闲
    if (priv->tx_busy) {
        netdev_err(ndev, "TX buffer full\n");
        return NETDEV_TX_BUSY;
    }

    // 解析CAN帧
    id = cf->can_id;
    dlc = cf->can_dlc;
    data[0] = le32_to_cpu(*(u32 *)&cf->data[0]);
    data[1] = le32_to_cpu(*(u32 *)&cf->data[4]);

    // 写入硬件寄存器
    mycan_write_tx_regs(priv, id, dlc, data);

    // 触发发送
    mycan_start_tx(priv);

    // 记录回显skb
    can_put_echo_skb(skb, priv->ndev, 0);

    return NETDEV_TX_OK;
}

4.3.2 内核态接收:中断处理

接收通常由中断驱动。当硬件收到报文,触发中断,你在中断处理函数里读取数据,然后通过netif_rx()或napi_gro_receive()把sk_buff上交给协议栈。

static irqreturn_t mycan_rx_irq(int irq, void *dev_id)
{
    struct net_device *ndev = dev_id;
    struct mycan_priv *priv = netdev_priv(ndev);
    struct sk_buff *skb;
    struct can_frame *cf;
    u32 id, dlc, data[2];

    // 读取硬件接收寄存器
    mycan_read_rx_regs(priv, &id, &dlc, data);

    // 分配sk_buff
    skb = alloc_can_skb(priv->ndev, &cf);
    if (!skb) {
        netdev_err(ndev, "RX: OOM\n");
        return IRQ_HANDLED;
    }

    // 填充CAN帧
    cf->can_id = id;
    cf->can_dlc = dlc;
    *(u32 *)&cf->data[0] = cpu_to_le32(data[0]);
    *(u32 *)&cf->data[4] = cpu_to_le32(data[1]);

    // 上交协议栈
    netif_rx(skb);

    return IRQ_HANDLED;
}

注意:中断处理函数里不要做耗时操作。我曾经见过有人在中断里打印调试信息,结果CAN总线直接超载,丢帧率飙升到30%。中断里只做最必要的事——读数据、构造skb、上交,其他事情交给下半部或工作队列。

4.3.3 用户态收发API

用户态编程就简单多了。你只需要创建一个原始CAN套接字,绑定到具体设备,然后就能用read()/write()或send()/recv()来收发报文。

#include <linux/can.h>
#include <linux/can/raw.h>
#include <sys/socket.h>
#include <net/if.h>

int main()
{
    int s;
    struct sockaddr_can addr;
    struct ifreq ifr;
    struct can_frame frame;

    // 1. 创建套接字
    s = socket(PF_CAN, SOCK_RAW, CAN_RAW);

    // 2. 指定设备
    strcpy(ifr.ifr_name, "can0");
    ioctl(s, SIOCGIFINDEX, &ifr);

    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;

    // 3. 绑定
    bind(s, (struct sockaddr *)&addr, sizeof(addr));

    // 4. 发送
    frame.can_id = 0x123;
    frame.can_dlc = 4;
    frame.data[0] = 0xDE;
    frame.data[1] = 0xAD;
    frame.data[2] = 0xBE;
    frame.data[3] = 0xEF;
    write(s, &frame, sizeof(frame));

    // 5. 接收
    read(s, &frame, sizeof(frame));
    printf("Received ID: 0x%03X, DLC: %d\n", frame.can_id, frame.can_dlc);

    close(s);
    return 0;
}

这段代码看起来简单,但实际项目中要注意的点不少。比如,你需要在发送前设置CAN帧的can_id格式——标准帧用11位,扩展帧用29位,还要设置CAN_EFF_FLAG标志。我见过有人把扩展帧的ID直接塞进11位字段里,结果接收方死活解析不对。

4.4 核心数据结构对比

为了让你更清晰地理解,我把几个关键数据结构整理成了一张表:

数据结构 所在层级 主要用途 关键字段
struct net_device 内核网络层 表示一个网络设备,包含操作函数、状态、统计信息 netdev_ops, flags, mtu, dev_addr
struct can_priv CAN核心层 CAN设备私有数据,包含时钟、位时序、状态、中断 clock, bittiming, state, restart_ms
struct can_frame 协议层 CAN帧的抽象表示,用于用户态和内核态之间传递 can_id, can_dlc, data[8]
struct sk_buff 内核网络层 套接字缓冲区,承载网络层和驱动层之间的数据 data, len, dev, protocol

这张表我建议你保存下来。每次写驱动时对照着看,能少走很多弯路。

4.5 SocketCAN驱动架构图

下面这张SVG图展示了SocketCAN驱动的完整架构和数据流向。你可以看到用户空间、内核空间、硬件之间的协作关系。

SocketCAN驱动架构图 SocketCAN驱动架构与数据流向 用户空间 (User Space) 应用程序:socket(PF_CAN, SOCK_RAW, CAN_RAW) → bind → read/write 内核空间 (Kernel Space) AF_CAN 协议族 raw / bcm / isotp CAN核心层 设备注册 / 过滤 / 统计 net_device ndo_start_xmit / 中断 硬件层 (Hardware) CAN控制器 (MCP2515 / SJA1000 / FlexCAN) → CAN收发器 → CAN总线 说明:数据从用户空间经套接字进入内核,经过协议族和核心层处理后,由net_device驱动写入硬件寄存器,最终通过CAN总线发送出去。接收路径相反。

4.6 避坑指南

最后,分享几个我在实际项目中踩过的坑:

  • 回显skb管理:发送完成后一定要调用can_put_echo_skb()和can_get_echo_skb(),否则内核会内存泄漏。我曾经因为漏了can_put_echo_skb,系统跑了三天后OOM崩溃。
  • 中断上下文限制:中断里不能调用alloc_can_skb()?其实可以,但要用GFP_ATOMIC标志。用GFP_KERNEL会导致睡眠,内核直接报错。
  • 位时序配置:不要硬编码位时序参数。不同晶振、不同波特率下,采样点位置差异很大。我建议用can_calc_bittiming()辅助函数自动计算。
  • 设备命名冲突:如果系统里有多个CAN设备,注册顺序可能导致can0、can1混乱。建议在设备树或平台数据中指定别名。

调试技巧:用ip -details link show can0可以查看CAN设备的详细状态,包括错误计数器、位时序参数、状态寄存器。这个命令比任何调试器都好用。

嗯,关于CAN驱动开发的核心内容就这些。SocketCAN框架的设计思路很清晰——把CAN设备当作网络设备来管理,复用Linux内核成熟的网络栈。你只要掌握了设备注册、数据收发API、中断处理这三个环节,就能写出稳定可靠的CAN驱动。