在掌握了中断、内存、定时器等独立概念后,如何将这些知识点系统性地组织起来,编写出结构清晰的驱动代码?这就要引入 Linux 驱动开发中最经典的设计范式:总线 (Bus) - 设备 (Device) - 驱动 (Driver) 模型。在嵌入式及 SoC 开发领域,Platform 总线是最常被使用的一种抽象。
Platform 模型的核心思想在于解耦。它将驱动代码清晰地划分为两部分:
- Device (硬件描述):描述板载设备的硬件资源,例如地址、中断号等。在现代 Linux 内核中,这一角色通常由设备树 (Device Tree) 承担。
- Driver (软件逻辑):定义如何操作设备,其逻辑不依赖于硬件的具体物理地址,这些地址会在运行时动态获取。
- Platform Bus (总线):充当“红娘”,负责将匹配的 Device 与 Driver 关联起来。
我们可以用一个简单的比喻来理解:
- Device 在设备树中声明:“我叫
learn,my-led,我有寄存器和中断两个资源!”
- Driver 向内核注册时说:“我能驱动所有名叫
learn,my-led 的设备。”
- Bus 发现双方的“暗号”(即
compatible 属性字符串)一致,于是触发驱动的 probe 函数。这个 probe 函数是驱动真正的入口(类似于普通 C 程序的 main 函数),只有匹配成功时才会被执行。
下面通过一个完整的示例,展示 Platform 模型的代码实现。
一、硬件描述:设备树
硬件信息通常在 .dts 或 .dtsi 文件中描述,这是一个设备树节点示例:
/* 此节点描述了一个硬件设备及其资源 */
my_demo_device {
/* 匹配“暗号”:必须与驱动中的定义完全一致 */
compatible = "learn,platform-demo";
/* 资源1:寄存器物理地址 (基地址 0x10005000,长度 0x100) */
reg = <0x10005000 0x100>;
/* 资源2:中断号 (连接到 gpio1 控制器的第 5 号引脚) */
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
status = "okay";
};
理解设备树是深入嵌入式Linux开发的基石,更多关于系统配置和内核的知识可以在 网络/系统 板块找到。
以下是驱动源码 platform_demo_drv.c:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h> // 核心头文件
#include <linux/mod_devicetable.h> // 存放 of_device_id
#include <linux/io.h>
#include <linux/interrupt.h>
/* 驱动私有数据结构 */
struct my_data {
void __iomem *base_addr; // 映射后的虚拟地址
int irq; // 虚拟中断号
};
/*
* ============================================================
* 2. PROBE 函数:当硬件和驱动匹配成功时,内核自动调用
* ============================================================
*/
static int my_probe(struct platform_device *pdev)
{
struct my_data *data;
struct resource *res;
int ret;
dev_info(&pdev->dev, "【匹配成功】Probe 函数被调用!\n");
/* 1. 分配私有数据内存 (使用 devm 系列函数自动管理) */
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/* 将私有数据指针保存到 platform_device 中,便于后续取用 */
platform_set_drvdata(pdev, data);
/*
* 2. 获取并映射内存资源 (MMIO)
* 对应设备树中的 reg = <...>;
*/
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取第一个内存资源
// devm_ioremap_resource 自动完成 request_mem_region 和 ioremap
data->base_addr = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(data->base_addr)) {
return PTR_ERR(data->base_addr);
}
dev_info(&pdev->dev, "映射物理地址 %pr 到虚拟地址 %p\n", res, data->base_addr);
/*
* 3. 获取中断资源
* 对应设备树中的 interrupts = <...>;
*/
data->irq = platform_get_irq(pdev, 0); // 获取第一个中断号
if (data->irq < 0) {
return data->irq;
}
/*
* 4. 申请中断
* 注意:此处中断处理函数为 NULL 仅用于演示,实际开发中必须提供
*/
ret = devm_request_irq(&pdev->dev, data->irq,
NULL, // 此处应为中断处理函数指针
0, "my_platform_irq", data);
dev_info(&pdev->dev, "驱动初始化完成!\n");
return 0;
}
/*
* ============================================================
* 3. REMOVE 函数:当驱动卸载或设备被移除时调用
* ============================================================
*/
static int my_remove(struct platform_device *pdev)
{
// struct my_data *data = platform_get_drvdata(pdev);
dev_info(&pdev->dev, "驱动已卸载,Bye!\n");
// 注意:由于使用了 devm_ 系列函数,内存和中断资源会自动释放
// 若未使用,则需在此手动 iounmap 和 free_irq
return 0;
}
/*
* ============================================================
* 1. 匹配表 (Match Table)
* 这是驱动与设备树“接头”的凭证,内核据此进行匹配
* ============================================================
*/
static const struct of_device_id my_match_table[] = {
{ .compatible = "learn,platform-demo" }, // 必须与设备树中的 compatible 属性一致
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_match_table);
/*
* 4. 定义驱动结构体
*/
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove, // 注意:在新版本内核中,remove 可能返回 void
.driver = {
.name = "my_demo_drv",
.of_match_table = my_match_table, // 关联匹配表
},
};
/*
* 5. 模块初始化和退出宏
* module_platform_driver 宏自动生成 module_init 和 module_exit,
* 并在内部调用 platform_driver_register/Unregister
*/
module_platform_driver(my_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("CXY");
三、关键代码解析
-
module_platform_driver(my_driver):
这是一个非常实用的宏。它自动生成了 module_init 和 module_exit 函数,并在其中分别调用了 platform_driver_register(&my_driver) 和 platform_driver_unregister(&my_driver)。对于大多数标准的 Platform 驱动,直接使用此宏可以简化代码。
-
platform_get_resource:
此函数是驱动获取硬件资源的桥梁。它能够从 platform_device 中解析出设备树里定义的物理地址、中断等资源,使驱动无需硬编码这些值,极大地提高了代码的通用性和可移植性。
-
devm_ (Device Managed) 系列函数:
这是现代 Linux 驱动开发的“最佳实践”之一。devm_kzalloc、devm_request_irq、devm_ioremap_resource 等函数都带有自动资源管理功能。当驱动的 probe 函数执行失败,或 remove 函数被调用时,通过这些函数申请的资源会被内核自动释放。这能有效避免常见的内存泄漏、资源未释放等问题,显著提升驱动代码的健壮性。掌握这类资源管理技巧是高效 Linux 系统开发的重要部分,相关内容可在 运维/DevOps 专题中进一步学习。
通过以上剖析,可以看到 Platform 模型通过清晰的层级划分和自动匹配机制,使得 Linux 驱动开发更加模块化和规范化,尤其适合资源多样的嵌入式场景。