Linux 总线设备驱动模型学习笔记
1. 详细介绍总线设备驱动模型
1.1 产生背景:为什么要引入它?
在传统的字符设备驱动编写中,开发者通常将"硬件资源(引脚、地址)"和"软件逻辑(操作流程)"混写在一起。这种方式存在两个主要缺陷:
- 代码冗余:如果有多个相同的硬件(如 10 个相同的 LED 灯),开发者可能需要拷贝多份几乎相同的驱动代码。
- 可移植性差:一旦硬件引脚发生变动,必须修改并重新编译整个驱动程序。
为了解决这些问题,Linux 引入了 分离(Separation) 的思想,即"总线-设备-驱动"模型。
1.2 核心思想:分离与分层
总线设备驱动模型将一个驱动程序的核心任务拆分为两个独立的模块:设备(Device) 和 驱动(Driver) ,并通过 总线(Bus) 将它们撮合在一起。
① 设备 (platform_device):描述"有什么"
- 职责:负责提供硬件资源。
- 内容:包括硬件的基地址、中断号、GPIO 引脚编号等物理参数。
- 特点:当硬件更换引脚或地址时,只需要修改设备端的代码,而不需要动业务逻辑。
② 驱动 (platform_driver):描述"怎么做"
- 职责:负责实现具体的业务逻辑。
- 内容:包括如何初始化硬件(init)、如何点亮 LED(ctl)、如何处理文件操作(file_operations)等。
- 特点:驱动程序通过内核提供的接口动态地从"设备端"获取资源,从而实现"一套代码驱动多个板卡"的目标。
③ 总线 (Bus):负责"撮合"
- 职责:管理设备和驱动的匹配。
- 机制:总线维护着两张链表(设备链表和驱动链表)。每当一个新的设备或驱动注册到总线上时,总线就会自动触发"匹配(Match)"动作。
1.3 平台总线 (Platform Bus)
对于嵌入式系统中的 SoC 内部控制器(如 I.MX6ULL 的 GPIO、UART、I2C 等),它们通常直接挂在 CPU 内部总线上,并没有类似 USB 或 PCI 这种可以被物理感知的实际总线。
为此,Linux 内核虚拟出了一种总线,称为 平台总线 (Platform Bus)。
- 结构体 :内核中使用
struct platform_device来代表硬件资源,使用struct platform_driver来代表驱动逻辑。 - 匹配标志 :默认情况下,它们通过
.name属性(字符串)进行匹配。
1.4 模型的优势总结
| 维度 | 传统方式 | 总线驱动模型 |
|---|---|---|
| 代码结构 | 硬件资源与逻辑耦合 | 资源与逻辑完全分离 |
| 可移植性 | 差,换硬件需改源码编译 | 强,只需更换设备端资源 |
| 管理效率 | 低,容易造成驱动代码冗余 | 高,支持多实例匹配 |
2. 匹配规则
当我们在内核中注册一个 platform_device 或一个 platform_driver 时,系统会自动调用平台总线的匹配函数(通常是 platform_match)。匹配的目的是为了让驱动程序找到它能控制的硬件资源。
内核遵循一套严格的优先级顺序进行匹配,主要分为以下三个阶段:
2.1 第一优先级:强制选择 (driver_override)
这是优先级最高的一种方式,允许开发者手动"指定"匹配关系。
- 规则 :比较
platform_device.driver_override和platform_driver.driver.name。 - 原理 :如果
platform_device结构体中的driver_override成员被设置了某个驱动的名字,那么这个设备将无视其他匹配规则,只与名字相符的驱动进行绑定。 - 用途:常用于调试,或者在系统中有多个兼容驱动时,强制指定使用某一个驱动。
2.2 第二优先级:ID 表匹配 (id_table)
如果第一阶段没有匹配成功,内核会检查驱动程序是否支持一系列设备。
- 规则 :比较
platform_device.name和platform_driver.id_table[i].name。 - 原理 :
platform_driver结构体中包含一个id_table数组(类型为struct platform_device_id)。- 这个数组就像是一个"白名单",列出了该驱动能够支持的所有设备名称。
- 内核会遍历这个数组,如果发现某个
{.name = "xxx"}与设备的name一致,则匹配成功。
- 优势 :一个驱动程序可以支持多个名字不同的设备。例如,一个 LED 驱动可以同时支持名字为
"led_red"和"led_blue"的两个设备。 - 私有数据 :
id_table还可以携带driver_data,为不同的匹配项提供不同的私有配置数据。
2.3 第三优先级:名字匹配 (Name Match)
这是最常用、也是最简单的匹配方式,当 id_table 为空时使用。
- 规则 :比较
platform_device.name和platform_driver.driver.name。 - 原理 :直接比对设备结构体中的名字和驱动结构体(内嵌的
device_driver)中的名字。 - 代码体现 :
- 在设备端:
.name = "100ask_led"。 - 在驱动端:
.driver = { .name = "100ask_led" }。
- 在设备端:
- 局限性 :这种方式要求名字必须完全一致,且一个驱动通常只能对应一种名称的设备。
匹配规则优先级表
| 优先级 | 比较对象 A | 比较对象 B | 说明 |
|---|---|---|---|
| 1 (最高) | device.driver_override |
driver.name |
强制覆盖规则 |
| 2 (中等) | device.name |
driver.id_table[i].name |
支持多设备列表匹配 |
| 3 (最低) | device.name |
driver.driver.name |
最基础的同名匹配 |
2.4 特殊说明:设备树匹配 (Device Tree)
虽然你提到的规则主要针对物理代码定义的匹配,但现代内核(如 4.9.88)最常用的其实是 设备树匹配,它的优先级通常非常高:
- 规则 :比较设备树节点的
compatible属性与驱动的of_match_table列表。 - 位置:在上述三种规则之前或之中执行,取决于具体的内核版本实现。
这是 Linux 总线设备驱动模型学习笔记的第三部分,重点分析内核是如何将设备和驱动联结在一起并触发业务代码的。
3. 函数调用关系
在总线模型中,函数调用不再是线性的,而是由内核总线框架根据"注册"行为触发的事件驱动。
3.1 注册阶段的调用关系
① 驱动注册 (platform_driver_register)
当我们在 led_drv.c 的入口函数中调用 platform_driver_register 时,内核会发生以下调用:
platform_driver_register(drv): 驱动入口函数发起请求。driver_register: 进入内核通用驱动层。bus_add_driver: 将驱动添加到platform_bus的驱动链表中。driver_attach: 尝试在总线上寻找匹配的设备。bus_for_each_dev: 遍历总线上的设备链表,对每个设备调用__driver_attach。
② 设备注册 (platform_device_register)
在传统的设备端代码(如 led_dev.c)中注册设备时:
platform_device_register(pdev): 向内核声明硬件资源。device_add: 将设备添加到platform_bus的设备链表中。bus_probe_device: 尝试为该新设备寻找匹配的驱动。device_initial_probe->__device_attach: 遍历驱动链表进行匹配。
3.3 匹配与探测的核心:Match 与 Probe
无论是先加载驱动还是先加载设备,最终都会汇聚到 匹配 (Match) 和 探测 (Probe) 这两个核心步骤。
① 匹配 (Match) 过程
内核调用总线定义的 .match 函数(对于平台总线是 platform_match):
- 该函数会按照上一章提到的优先级(
driver_override->id_table->name)进行字符串比对。 - 如果返回 1:表示匹配成功,进入下一步。
- 如果返回 0:表示不匹配,继续查找下一个。
② 探测 (Probe) 过程
一旦 Match 成功,内核会启动 probe 调用链:
really_probe: 内核确认匹配后,准备调用驱动的具体实现。drv->probe(即imx6ull_led_probe) :- 获取资源 :驱动程序通过
platform_get_resource拿到设备定义的寄存器基地址。 - 地址映射 :执行
ioremap得到虚拟地址。 - 注册接口 :调用
register_chrdev并创建/dev下的设备节点。
- 获取资源 :驱动程序通过
3.4 卸载阶段的调用关系 (Remove)
当执行 rmmod 卸载驱动,或设备被拔出时:
platform_driver_unregister: 发起注销请求。driver_unregister->__device_release_driver: 找到已绑定的设备。drv->remove(即imx6ull_led_remove) :- 执行
iounmap释放虚拟地址。 - 销毁设备节点和类。
- 注销字符设备号。
- 执行
3.5 逻辑关系总结表
| 动作 | 触发函数 | 内核行为 | 驱动层响应 |
|---|---|---|---|
| 装载驱动 | platform_driver_register |
找设备名并匹配 | 匹配成功则执行 probe |
| 装载设备 | platform_device_register |
找驱动名并匹配 | 匹配成功则执行 probe |
| 匹配逻辑 | platform_match |
执行三级匹配规则 | (内核自动执行) |
| 卸载逻辑 | platform_driver_unregister |
解除绑定关系 | 执行 remove |
这是 Linux 总线设备驱动模型学习笔记的第四部分,重点解析驱动开发中频繁调用的内核 API。
4. 常用函数:注册、反注册与资源获取
在平台总线模型中,开发者不再直接操作底层的链表,而是通过内核提供的标准 API 来完成设备资源的声明和驱动逻辑的加载。
4.1 设备端常用函数 (Platform Device)
设备端代码(如 led_dev.c)的主要任务是定义 struct resource 并注册设备。
① 注册与反注册
int platform_device_register(struct platform_device *pdev)- 作用 :将定义好的平台设备注册到内核中。
- 行为 :将设备加入
platform_bus的设备链表,并触发总线的匹配过程。
void platform_device_unregister(struct platform_device *pdev)- 作用:注销设备。
- 行为 :从链表中移除设备,并触发已绑定驱动的
remove函数。
② 静态定义宏
struct platform_device_register_simple(...)- 作用:这是一个快捷函数,用于一次性完成分配、设置和注册一个简单的平台设备。
4.2 驱动端常用函数 (Platform Driver)
驱动端代码(如 led_drv.c)负责实现业务逻辑并提取资源。
① 注册与反注册
int platform_driver_register(struct platform_driver *drv)- 作用:将驱动程序注册到内核。
- 行为:将驱动加入总线的驱动链表,并搜索匹配的设备。
void platform_driver_unregister(struct platform_driver *drv)- 作用:注销驱动程序。
module_platform_driver(drv)- 作用 :宏定义。它可以代替
init和exit函数中冗长的注册/注销代码,是一个一键注册宏。
- 作用 :宏定义。它可以代替
② 获取资源 (Resource Management)
这是 probe 函数中最核心的操作:
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num)- 参数说明 :
dev:由内核传给probe的设备指针。type:资源类型(如IORESOURCE_MEM代表内存/寄存器地址,IORESOURCE_IRQ代表中断号)。num:同类资源的索引(比如有两组内存资源,第 0 组和第 1 组)。
- 示例:获取寄存器物理基地址。
- 参数说明 :
4.3 资源类型定义 (struct resource)
资源结构体是设备向驱动传递物理参数的载体。
c
struct resource {
resource_size_t start; // 资源的起始位置(如物理地址 0x020AC000)
resource_size_t end; // 资源的结束位置
const char *name; // 资源的名字
unsigned long flags; // 资源类型标志(IORESOURCE_MEM, IORESOURCE_IO, IORESOURCE_IRQ)
};
4.4 函数功能对照表
| 函数分类 | 函数名称 | 典型使用位置 | 核心目的 |
|---|---|---|---|
| 设备注册 | platform_device_register |
led_dev.c (init) |
声明硬件资源存在。 |
| 驱动注册 | platform_driver_register |
led_drv.c (init) |
声明操作逻辑就绪。 |
| 资源获取 | platform_get_resource |
probe 函数内部 |
动态获取硬件物理地址。 |
| 地址映射 | devm_ioremap_resource |
probe 函数内部 |
将获取的资源自动进行映射(更高级的 ioremap)。 |
| 驱动注销 | platform_driver_unregister |
led_drv.c (exit) |
停止驱动并释放资源。 |
这是 Linux 总线设备驱动模型学习笔记的最后一部分,将设备端、驱动端以及内核总线的交互逻辑完整串联。
5. 写程序的流程
编写一个基于总线模型的驱动程序,本质上是完成 "资源定义" 与 "逻辑实现" 的解耦。整个流程分为设备端(Resource)和驱动端(Logic)两个独立文件的编写。
5.1 设备端流程:分配 / 设置 / 注册 platform_device
目标:告诉内核硬件的物理参数,但不涉及任何控制逻辑。
- 定义资源 (
struct resource) :- 指定寄存器的起始地址、长度及类型(
IORESOURCE_MEM)。 - 指定中断号等其他资源(如果需要)。
- 指定寄存器的起始地址、长度及类型(
- 设置
platform_device结构体 :.name:指定设备名称(如"100ask_led"),这是与驱动匹配的唯一"暗号"。.resource:关联刚才定义的资源数组。.num_resources:资源的个数。
- 注册设备 :
- 在模块入口函数中调用
platform_device_register()。 - 在模块出口函数中调用
platform_device_unregister()。
- 在模块入口函数中调用
5.2 驱动端流程:分配 / 设置 / 注册 platform_driver
目标:编写通用的控制逻辑,动态地根据拿到的资源进行操作。
- 定义
file_operations结构体 :- 实现标准的
open,write等接口。
- 实现标准的
- 实现
probe函数(核心逻辑点) :- 获取资源 :调用
platform_get_resource()拿到设备端传来的物理地址。 - 地址映射 :调用
ioremap()将物理地址转为虚拟地址。 - 注册字符设备 :调用
register_chrdev()并创建类与设备节点。
- 获取资源 :调用
- 实现
remove函数 :- 执行
iounmap()释放地址映射。 - 注销字符设备并销毁设备节点。
- 执行
- 设置
platform_driver结构体 :.probe和.remove:指向刚才实现的函数。.driver.name:必须与设备端的.name完全一致,才能触发匹配。
- 注册驱动 :
- 在模块入口函数调用
platform_driver_register()。
- 在模块入口函数调用
5.3 整体运行逻辑串联表
| 步骤 | 模块 | 动作 | 内核行为 | 结果 |
|---|---|---|---|---|
| 1 | 设备端 (led_dev.ko) |
insmod |
将设备加入总线设备链表 | 等待驱动匹配 |
| 2 | 驱动端 (led_drv.ko) |
insmod |
将驱动加入总线驱动链表 | 触发匹配检查 |
| 3 | 总线系统 | 匹配 name |
发现 name 一致 |
调用驱动的 probe |
| 4 | 驱动端 (probe) |
获取资源并映射 | 注册字符设备驱动 | /dev/100ask_led0 出现 |
| 5 | 用户空间 | open / write |
经过系统调用到达驱动层 | LED 物理状态改变 |
| 6 | 驱动端 (remove) |
rmmod |
释放资源,注销设备 | 系统恢复清洁 |
5.4 为什么这样写更高效?
- 解耦 :如果你的 LED 从 GPIO5_3 换到了 GPIO3_3,你只需要修改并重新编译设备端文件,而处理逻辑复杂的驱动端 (
led_drv.c) 一行代码都不用动。 - 多实例 :如果你有两组 LED 硬件,只需注册两个
platform_device(使用相同名字),内核就会自动调用两次驱动的probe,为你生成两个设备节点。