第4章:设备驱动模型——总线-设备-驱动与设备树实战
设备驱动模型,说白了就是Linux内核为了管理硬件而设计的一套“相亲系统”。总线是媒人,设备和驱动是男女双方。媒人负责牵线,双方各自提供自己的“择偶标准”,配对成功就结婚——驱动开始工作。
我刚开始接触这个模型时,觉得它太抽象了。直到有一次在项目中调试一个USB摄像头驱动,死活挂不上,最后发现是设备ID匹配错了。嗯,从那以后我再也不敢小看这套模型了。
4.1 总线-设备-驱动模型的核心逻辑
内核里这套模型,核心就三个角色:总线(Bus)、设备(Device)、驱动(Driver)。它们之间的关系,我用一张图给你讲清楚。
核心思想:驱动不直接操作硬件,设备不直接调用驱动。总线作为中间层,负责匹配和绑定。
你看,总线在中间,设备和驱动在两边。总线负责调用match()函数,看看设备和驱动能不能对上眼。对上了,就调用驱动的probe()函数,驱动开始干活。
4.2 platform总线——嵌入式开发的好帮手
在嵌入式世界里,很多设备不像PCI、USB那样有标准的枚举机制。比如I2C控制器、SPI控制器、GPIO按键——它们直接挂在CPU的内存总线上。这时候,platform总线就派上用场了。
我个人习惯把platform总线叫做“虚拟总线”。它专门用来管理那些“没有标准总线可挂”的设备。你想想看,一个简单的LED灯,它不需要PCI枚举,也不需要USB描述符,但它确实需要一个驱动。platform总线就是为这类设备准备的。
4.2.1 platform设备与驱动的匹配规则
匹配规则其实很简单,就三种方式:
- 设备树匹配:通过
compatible属性。这是目前最主流的方式。 - ACPI匹配:主要用于x86平台,嵌入式用得少。
- 名称匹配:通过
name字段。老代码里常见,现在不推荐了。
我的建议:新项目一律用设备树匹配。名称匹配容易冲突,我在一个项目里就踩过坑——两个驱动用了同一个名字,结果谁先加载谁说了算,完全不可控。
4.2.2 一个简单的platform驱动示例
下面这个例子,展示了一个platform驱动的基本骨架。代码不长,但包含了核心要素。
#include <linux/module.h>
#include <linux/platform_device.h>
static int my_driver_probe(struct platform_device *pdev)
{
pr_info("my_driver: probe success!\n");
// 在这里做硬件初始化
return 0;
}
static int my_driver_remove(struct platform_device *pdev)
{
pr_info("my_driver: remove\n");
// 在这里做清理工作
return 0;
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "mycompany,mydevice" },
{ /* 结束标记 */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct platform_driver my_driver = {
.probe = my_driver_probe,
.remove = my_driver_remove,
.driver = {
.name = "my_driver",
.of_match_table = my_of_match,
},
};
module_platform_driver(my_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
这段代码里,of_match_table指向设备树匹配表。当内核发现设备树里有一个compatible = "mycompany,mydevice"的节点时,就会调用这个驱动的probe()函数。
注意:MODULE_DEVICE_TABLE(of, my_of_match)这行不能少。少了它,内核在编译模块时就无法生成设备树别名,导致模块无法自动加载。我曾经因为这个疏忽,浪费了半天时间排查。
4.3 设备树(Device Tree)——硬件描述的艺术
设备树,说白了就是一份“硬件说明书”。它告诉内核:CPU是什么、内存多大、外设挂在哪里、中断号是多少。没有设备树之前,这些信息都硬编码在C代码里,换个板子就得重新编译内核。
我记得第一次接触设备树时,觉得它就是个配置文件。后来发现,它远比配置文件复杂——它有语法、有绑定规则、有覆盖机制。嗯,这里要注意,设备树不是万能药,但它确实解决了硬件描述与内核代码解耦的大问题。
4.3.1 设备树的基本语法
一个简单的设备树节点长这样:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "mycompany,myboard";
chosen {
bootargs = "console=ttyS0,115200";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 256MB内存
};
soc {
uart0: serial@10000000 {
compatible = "mycompany,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 5 4>;
clock-frequency = <24000000>;
};
};
};
这里的关键字段:
| 字段 | 含义 | 说明 |
|---|---|---|
compatible |
兼容性字符串 | 驱动匹配的核心依据,格式为"厂商,设备" |
reg |
寄存器地址和大小 | 格式为<地址 长度>,支持多段 |
interrupts |
中断描述 | 格式依赖中断控制器,通常为<中断类型 中断号 触发方式> |
status |
设备状态 | "okay"表示启用,"disabled"表示禁用 |
4.3.2 设备树在驱动中的使用
驱动里怎么读取设备树信息?看这个例子:
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
u32 val;
int ret;
// 读取reg信息
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENXIO;
// 读取中断号
int irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 读取自定义属性
ret = of_property_read_u32(np, "clock-frequency", &val);
if (ret)
dev_warn(dev, "missing clock-frequency\n");
return 0;
}
你看,platform_get_resource()和platform_get_irq()是platform总线提供的API,它们自动从设备树里解析reg和interrupts。自定义属性则用of_property_read_*系列函数。
小技巧:调试设备树时,可以在内核启动参数里加of_devtree或者通过/sys/firmware/devicetree/base查看实际解析结果。我经常用dtc -I fs -O dts /sys/firmware/devicetree/base把当前设备树反编译出来看。
4.4 sysfs与udev——用户空间与内核的桥梁
sysfs是一个虚拟文件系统,挂载在/sys目录下。它把内核里的设备、驱动、总线信息以文件形式暴露给用户空间。udev则是一个用户空间守护进程,它监听sysfs的变化,自动创建设备节点、加载固件、执行规则。
我刚开始用Linux时,觉得/sys目录下的东西很神秘。后来理解了,它就是内核的“窗户”——你想看什么,打开对应的文件就行。
4.4.1 sysfs的目录结构
sysfs的核心目录:
/sys/bus/:所有总线,每个总线下面有devices/和drivers/子目录。/sys/devices/:所有设备的真实层级结构。/sys/class/:按功能分类的设备视图,比如net/、input/、tty/。/sys/block/:块设备。
举个例子,你想看platform总线下的设备和驱动:
# 查看platform总线下的设备
ls /sys/bus/platform/devices/
# 查看platform总线下的驱动
ls /sys/bus/platform/drivers/
# 查看某个设备的具体属性
cat /sys/bus/platform/devices/serial@10000000/of_node/compatible
4.4.2 udev规则实战
udev规则文件通常放在/etc/udev/rules.d/目录下。规则格式是“键值对”,匹配条件满足后就执行动作。
我曾经遇到一个问题:一个USB转串口设备,每次插入后设备名都不一样,导致脚本无法正常工作。解决方案就是写一条udev规则,给它固定一个符号链接。
# /etc/udev/rules.d/99-usb-serial.rules
# 匹配USB转串口设备,创建固定符号链接
ACTION=="add", SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="my_usb_serial"
这条规则的意思是:当系统添加一个tty子系统设备时,如果它的vendor ID是0403(FTDI),product ID是6001,就创建一个/dev/my_usb_serial符号链接指向它。
避坑指南:udev规则匹配时,ATTRS会遍历设备的父节点,性能开销较大。能用ATTR(只匹配当前设备)就别用ATTRS。我曾经在一个项目里写了20多条ATTRS规则,结果设备插入后要等好几秒才出现节点——后来改成ATTR,瞬间就好了。
4.4.3 sysfs与驱动的交互
驱动也可以主动创建sysfs属性文件,让用户空间直接读写。比如一个LED驱动,可以暴露一个brightness文件:
static ssize_t brightness_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
int val = read_brightness(dev);
return sprintf(buf, "%d\n", val);
}
static ssize_t brightness_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
unsigned long val;
if (kstrtoul(buf, 10, &val))
return -EINVAL;
set_brightness(dev, val);
return count;
}
static DEVICE_ATTR_RW(brightness);
static int my_probe(struct platform_device *pdev)
{
// ... 初始化代码 ...
device_create_file(&pdev->dev, &dev_attr_brightness);
return 0;
}
这样,用户空间就可以通过echo 100 > /sys/devices/.../brightness来控制LED亮度了。这种交互方式,说白了就是“内核暴露接口,用户空间决定策略”。
4.5 本章小结
总线-设备-驱动模型是Linux内核设备管理的基石。platform总线解决了嵌入式设备“无标准总线可挂”的问题。设备树让硬件描述与内核代码解耦,换板子不再需要重新编译内核。sysfs和udev则打通了内核与用户空间的通道,让设备管理变得灵活可控。
我个人觉得,理解这套模型的关键在于“分层”和“解耦”这两个词。每一层只关心自己的事,层与层之间通过标准接口通信。你想想看,如果没有这套模型,每个驱动都要自己处理硬件枚举、资源分配、用户空间交互——那代码得乱成什么样?
嗯,这一章的内容就到这里。记住,多动手写代码、多查看/sys和/proc下的文件,比看十遍理论都有用。