3. Linux设备驱动模型

在Linux内核中,设备驱动模型为硬件设备与驱动程序之间提供了标准化的抽象层。对于网络接口卡(NIC)驱动开发而言,理解platform总线、设备树(Device Tree)以及驱动与设备的匹配机制是编写可移植、可维护驱动的基石。本章将深入剖析这些核心概念,并解析驱动模块的加载与卸载流程。

3.1 Linux设备驱动模型概述

Linux内核的设备驱动模型基于设备(device)、驱动(driver)和总线(bus)三个核心对象。总线负责将设备和驱动绑定在一起。当系统检测到一个新设备时,总线会遍历已注册的驱动列表,寻找能够管理该设备的驱动;反之,当注册一个新驱动时,总线会扫描已连接的设备,尝试进行匹配。

核心概念: 设备模型的核心数据结构包括 struct device、struct device_driver 和 struct bus_type。对于大多数SoC内部集成的NIC(如MAC控制器),它们通常挂载在platform总线上。

3.2 platform总线

platform总线是一种虚拟总线,用于管理那些不依附于传统物理总线(如PCI、USB、I2C)的片上系统(SoC)外设。在嵌入式系统中,绝大多数NIC控制器(如DesignWare EMAC、TI CPSW、Freescale FEC)都作为platform设备存在。

3.2.1 platform总线的工作机制

platform总线通过 platform_match 函数进行设备和驱动的匹配。其匹配顺序如下:

  1. 设备树(DT)匹配:比较驱动中 of_match_table 的 compatible 属性与设备树节点中的 compatible 属性。
  2. ACPI匹配:在支持ACPI的系统中,比较ACPI ID。
  3. 名称匹配:比较驱动的 id_table 中的名称与platform设备的名称。
  4. 设备名称回退匹配:直接比较驱动名称与设备名称。
platform总线匹配优先级
优先级 匹配方式 关键字段 典型应用场景
1(最高) 设备树(DT) of_match_table → .compatible ARM/ARM64/RISC-V嵌入式平台
2 ACPI acpi_match_table x86/ARM64服务器平台
3 ID表 id_table → .name 传统非DT平台(如旧版ARM)
4(最低) 名称回退 驱动名称 vs 设备名称 简单或遗留设备
开发提示: 在编写NIC驱动时,强烈建议使用设备树匹配方式。它提供了最佳的硬件描述解耦,使得同一份驱动二进制可以支持多个不同型号的控制器,只需更换设备树文件即可。

3.2.2 platform驱动结构

一个典型的platform NIC驱动框架如下:

/* 定义 of_match_table,用于设备树匹配 */
static const struct of_device_id my_nic_of_match[] = {
    { .compatible = "vendor,my-nic-v1", },
    { .compatible = "vendor,my-nic-v2", },
    { /* end of list */ }
};
MODULE_DEVICE_TABLE(of, my_nic_of_match);

/* probe 函数:当驱动与设备匹配成功时调用 */
static int my_nic_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;
    int irq;

    /* 获取设备树中的内存资源 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* 获取中断号 */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    /* 初始化硬件、注册net_device等 */
    // ...

    return 0;
}

/* remove 函数:当驱动卸载或设备移除时调用 */
static int my_nic_remove(struct platform_device *pdev)
{
    /* 释放资源、注销net_device */
    // ...
    return 0;
}

/* platform_driver 结构体 */
static struct platform_driver my_nic_driver = {
    .probe  = my_nic_probe,
    .remove = my_nic_remove,
    .driver = {
        .name = "my_nic",
        .of_match_table = my_nic_of_match,
    },
};

3.3 设备树(Device Tree)

设备树是一种描述硬件资源的数据结构,它通过文本文件(.dts)编译成二进制(.dtb),在系统启动时由bootloader传递给内核。设备树使得内核无需硬编码硬件信息,实现了代码与硬件描述的分离。

3.3.1 设备树节点与属性

一个NIC控制器在设备树中的典型描述如下:

/* my-nic.dts */
/ {
    soc {
        my_nic: ethernet@f0000000 {
            compatible = "vendor,my-nic-v1";
            reg = <0x0 0xf0000000 0x0 0x1000>;
            interrupts = <0 33 4>;
            interrupt-parent = <&gic>;
            phy-mode = "rgmii";
            phy-handle = <&phy0>;
            status = "okay";

            mdio {
                #address-cells = <1>;
                #size-cells = <0>;
                phy0: ethernet-phy@0 {
                    reg = <0>;
                };
            };
        };
    };
};
设备树关键属性说明
属性 含义 驱动获取方式
compatible 硬件兼容性字符串,用于驱动匹配 of_match_device()
reg 寄存器地址范围(起始地址+长度) platform_get_resource(pdev, IORESOURCE_MEM, 0)
interrupts 中断号、触发类型 platform_get_irq(pdev, 0)
phy-mode PHY接口模式(如RGMII、MII) of_get_phy_mode()
phy-handle 引用外部PHY节点 of_phy_connect()
status 设备状态("okay" 或 "disabled") 内核自动解析

3.3.2 设备树与platform总线的交互

当内核启动时,设备树中的每个节点都会被展开为一个 struct platform_device。这些设备被注册到platform总线上,等待匹配的驱动。

设备树 → platform设备 → 驱动匹配流程 设备树 (.dts/.dtb) compatible = "vendor,nic" reg = <0xf0000000 0x1000> interrupts = <0 33 4> 内核解析 platform_device name = "my_nic" num_resources = 2 dev.of_node = 指向DT节点 platform_match platform_driver of_match_table .probe() .remove() 匹配成功 → 调用 .probe() 匹配失败 → 继续等待驱动 说明:设备树节点由内核OF(Open Firmware)子系统解析,生成platform_device。 platform_match通过比较compatible属性与驱动的of_match_table完成匹配。

3.4 驱动与设备的匹配机制

匹配机制是设备模型的核心。当 platform_match 被调用时,它按照优先级顺序尝试匹配。对于NIC驱动,最常用的是设备树匹配。

3.4.1 匹配过程详解

匹配过程在内核源码 drivers/base/platform.c 的 platform_match 函数中实现。其核心逻辑如下:

static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* 1. 尝试设备树匹配 */
    if (of_driver_match_device(dev, drv))
        return 1;

    /* 2. 尝试ACPI匹配 */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* 3. 尝试ID表匹配 */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* 4. 回退到名称匹配 */
    return (strcmp(pdev->name, drv->name) == 0);
}
常见陷阱: 如果驱动中定义了 of_match_table 但设备树节点的 compatible 属性与表中任何条目都不匹配,驱动将永远不会被 probe。务必使用 MODULE_DEVICE_TABLE(of, ...) 导出匹配表,以便内核在模块加载时构建别名。

3.4.2 匹配成功后的生命周期

一旦匹配成功,内核会调用驱动的 probe 函数。在 probe 中,驱动需要完成以下关键步骤:

  • 资源获取:使用 platform_get_resource() 获取寄存器地址、DMA通道等。
  • 中断申请:使用 platform_get_irq() 获取中断号,并调用 request_irq() 或 devm_request_irq() 注册中断处理函数。
  • net_device分配与初始化:调用 alloc_etherdev() 分配网络设备,设置 net_device_ops。
  • 硬件初始化:复位MAC、配置DMA描述符、设置MAC地址等。
  • 注册网络设备:调用 register_netdev() 使设备可用。

3.5 module_init / module_exit 流程

驱动模块的加载与卸载由 module_init 和 module_exit 宏控制。对于platform驱动,标准做法是在模块初始化时注册platform驱动,在模块退出时注销。

3.5.1 标准入口与出口

/* 模块初始化函数 */
static int __init my_nic_init(void)
{
    int ret;

    ret = platform_driver_register(&my_nic_driver);
    if (ret)
        pr_err("Failed to register my_nic driver\n");

    return ret;
}

/* 模块退出函数 */
static void __exit my_nic_exit(void)
{
    platform_driver_unregister(&my_nic_driver);
}

module_init(my_nic_init);
module_exit(my_nic_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My NIC Platform Driver");

3.5.2 流程时序图

module_init / module_exit 完整流程 模块加载 (insmod) module_init → my_nic_init() platform_driver_register() platform_match() my_nic_probe() 模块卸载 (rmmod) module_exit → my_nic_exit() platform_driver_unregister() my_nic_remove() 注意:probe() 在驱动注册时立即执行(如果设备已存在),或在设备热插拔时执行。 remove() 在驱动卸载时执行,必须释放所有已申请的资源。

3.5.3 使用 module_platform_driver 宏

为了简化代码,内核提供了 module_platform_driver 宏,它自动生成 module_init 和 module_exit 函数:

/* 使用宏简化驱动注册 */
module_platform_driver(my_nic_driver);

/* 等价于手动编写:
 * module_init(my_nic_init);
 * module_exit(my_nic_exit);
 *
 * 其中 my_nic_init 调用 platform_driver_register(&my_nic_driver)
 * my_nic_exit 调用 platform_driver_unregister(&my_nic_driver)
 */
最佳实践: 对于大多数platform NIC驱动,推荐使用 module_platform_driver() 宏。它减少了样板代码,并确保错误处理路径正确。如果需要在初始化时执行额外的操作(如子模块初始化、全局状态设置),则手动编写 module_init 和 module_exit。

3.6 本章小结

本章涵盖了Linux设备驱动模型在NIC驱动开发中的核心应用:

  • platform总线是SoC内部NIC控制器的标准挂载点,通过 platform_driver_register() 注册。
  • 设备树提供了硬件描述与代码的分离,compatible 属性是驱动匹配的关键。
  • 匹配机制遵循DT → ACPI → ID表 → 名称的优先级顺序,确保灵活性和兼容性。
  • module_init/exit 流程管理驱动的生命周期,probe() 和 remove() 是驱动与硬件交互的入口和出口。