3. Linux 设备驱动模型:platform总线、设备树与匹配机制
在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 函数进行设备和驱动的匹配。其匹配顺序如下:
- 设备树(DT)匹配:比较驱动中
of_match_table的compatible属性与设备树节点中的compatible属性。 - ACPI匹配:在支持ACPI的系统中,比较ACPI ID。
- 名称匹配:比较驱动的
id_table中的名称与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 设备名称 | 简单或遗留设备 |
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总线上,等待匹配的驱动。
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 流程时序图
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)
*/
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()是驱动与硬件交互的入口和出口。