3. 设备驱动模型与设备树:从混乱到有序
说实话,我刚入行那会儿,Linux驱动开发还没设备树这东西。那时候板级文件里全是硬编码的寄存器地址、中断号,换个引脚就得改代码重新编译。嗯,那叫一个痛苦。后来设备树来了,整个开发方式都变了。今天我就跟你聊聊这个转变背后的核心——设备驱动模型和设备树。
3.1 Linux设备驱动模型:bus、device、driver
先说说驱动模型。你想想看,一个嵌入式系统里有多少设备?I2C、SPI、USB、GPIO……如果每个驱动都自己管自己的资源,那代码会乱成一锅粥。Linux内核为了解决这个问题,搞了一套抽象模型。
这套模型的核心就三个概念:总线(bus)、设备(device)、驱动(driver)。说白了,总线是桥梁,设备是硬件实体,驱动是操作逻辑。它们仨怎么配合呢?
- 总线:负责匹配设备和驱动。比如platform总线,它不关心具体硬件,只管把设备和驱动牵线搭桥。
- 设备:描述硬件资源。比如寄存器基地址、中断号、时钟频率等。
- 驱动:实现操作逻辑。比如怎么初始化、怎么读写、怎么处理中断。
我在项目中遇到过一个问题:一个I2C触摸屏驱动死活加载不上。查了半天,发现是设备树里i2c节点的地址写错了,导致驱动匹配不到设备。你看,模型再漂亮,数据不对也白搭。
3.2 设备树(Device Tree)基本语法
设备树是什么?它就是一个描述硬件信息的文本文件。内核启动时解析它,然后动态创建设备。这样,同一份内核镜像可以适配不同硬件,只要换设备树文件就行。
设备树的文件后缀是.dts,编译后变成.dtb。基本语法很简单,我直接给你看个例子:
/dts-v1/;
/ {
compatible = "vendor,board";
#address-cells = <1>;
#size-cells = <1>;
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0x0>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
注意几个关键点:
/dts-v1/;是版本声明,必须写在第一行。- 根节点
/包含所有设备。 compatible属性用于匹配驱动,格式是"厂商,型号"。reg描述地址和大小,#address-cells和#size-cells决定它的格式。
compatible属性想清楚。它就像身份证,驱动靠它找到你。我曾经因为compatible写了个空格,导致驱动匹配失败,排查了整整一下午。
3.3 设备树节点与属性
设备树的节点就是硬件设备,属性就是设备的参数。常见的属性有:
| 属性名 | 作用 | 示例 |
|---|---|---|
| compatible | 驱动匹配标识 | "ti,am335x-gpio" |
| reg | 地址和大小 | <0x4804c000 0x1000> |
| interrupts | 中断号 | <0 29 4> |
| clocks | 时钟源 | <&clk_uart0> |
| status | 设备状态 | "okay" 或 "disabled" |
节点可以嵌套。比如一个I2C控制器下面挂多个从设备:
&i2c0 {
status = "okay";
clock-frequency = <100000>;
touch@38 {
compatible = "goodix,gt911";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <15 2>;
};
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
这里&i2c0是引用,表示在别处定义好的节点上追加内容。这种写法很常见,我建议你多用。
<0x38>这种写法,它还是十六进制。别搞混了。
3.4 在驱动中解析设备树
设备树写好了,驱动怎么读呢?内核提供了一套API。我拿一个简单的platform驱动举例:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct resource *res;
void __iomem *base;
int irq;
u32 val;
// 获取寄存器地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 获取中断号
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 读取自定义属性
if (of_property_read_u32(np, "my-custom-value", &val) == 0)
dev_info(dev, "custom value: %d\n", val);
return 0;
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,my-device" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct platform_driver my_driver = {
.probe = my_probe,
.driver = {
.name = "my_device",
.of_match_table = my_of_match,
},
};
module_platform_driver(my_driver);
这段代码里,of_match_table是关键。它告诉内核:这个驱动支持哪些compatible的设备。当设备树里有匹配的节点时,内核就会调用probe函数。
常用的解析函数还有:
of_property_read_string():读字符串属性of_get_named_gpio():读GPIO引脚of_parse_phandle():解析引用(比如时钟、中断控制器)
重点提醒:解析设备树时,一定要检查返回值。比如of_property_read_u32返回非0,说明属性不存在。我见过有人不检查,直接拿未初始化的变量去配置硬件,结果设备工作异常。
嗯,设备树和驱动模型就这些核心内容。说白了,设备树是硬件的「身份证」,驱动模型是内核的「婚姻介绍所」。两者配合,让驱动开发变得灵活又规范。你写驱动时,先把设备树理清楚,再写代码,会顺畅很多。
下一章我们聊聊字符设备驱动,那是驱动开发的基础,也是你第一个真正动手写的驱动。