3. 设备树绑定与驱动:匹配机制与资源读取
设备树和驱动怎么对上号?这是很多刚接触嵌入式 Linux 的朋友会问的问题。说白了,设备树就是硬件的“身份证”,驱动则是“操作手册”。内核得先找到身份证,再翻手册,才能把活儿干好。
今天我们就聊聊这个匹配过程,以及怎么从设备树里把 GPIO、中断、时钟这些资源读出来。嗯,这部分内容我几乎每个项目都会用到,踩过的坑也不少。
3.1 platform 驱动匹配机制
内核里大部分总线设备(比如 I2C、SPI)都有自己的匹配方式。但那些直接挂在 CPU 总线上的设备——比如 GPIO 控制器、时钟控制器、内存映射的外设——它们走的是 platform 总线。
匹配的核心就一句话:设备树中的 compatible 属性 vs 驱动中的 of_match_table。
匹配流程(简化版)
- 内核解析 DTB,为每个节点创建 platform_device
- 驱动注册时,内核遍历其 of_match_table
- 比对节点中的 compatible 字符串
- 匹配成功 → 调用 probe 函数
举个例子。设备树里这么写:
my_device: my-device@40001000 {
compatible = "vendor,my-device-v1", "vendor,my-device";
reg = <0x40001000 0x100>;
interrupts = <0 31 4>;
};
驱动里对应的匹配表:
static const struct of_device_id my_device_of_match[] = {
{ .compatible = "vendor,my-device-v1", },
{ .compatible = "vendor,my-device", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_device_of_match);
static struct platform_driver my_device_driver = {
.probe = my_device_probe,
.remove = my_device_remove,
.driver = {
.name = "my_device",
.of_match_table = my_device_of_match,
},
};
内核会按顺序匹配。先查 "vendor,my-device-v1",没有就查 "vendor,my-device"。我个人习惯把最具体的版本号放前面,通用的放后面——这样兼容性好,也方便后续扩展。
小技巧:compatible 字符串的命名规范是 "厂商,型号"。别乱写,否则内核社区 review 时会让你改的。
3.2 of 操作 API 详解
匹配成功后,probe 函数会拿到一个 struct platform_device *pdev。这个结构体里藏着设备树节点的全部信息。怎么读?靠 of_ 系列 API。
我整理了一份常用 API 清单,基本覆盖了日常开发需求:
| API 函数 | 作用 | 返回值 |
|---|---|---|
of_match_device() |
获取匹配到的 of_device_id 条目 | 指针 / NULL |
of_property_read_u32() |
读取 32 位整数属性 | 0 成功 / 负值失败 |
of_property_read_string() |
读取字符串属性 | 0 成功 / 负值失败 |
of_get_named_gpio() |
获取 GPIO 编号(已废弃,建议用新 API) | GPIO 编号 / 负值错误 |
of_irq_get() |
获取中断号 | 中断号 / 负值错误 |
of_iomap() |
将 reg 地址映射到虚拟空间 | 映射后的地址 / NULL |
举个例子,读取 reg 和中断:
static int my_device_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct resource *res;
void __iomem *base;
int irq, ret;
u32 val;
// 1. 获取 IO 内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 2. 获取中断号
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 3. 读取自定义属性
ret = of_property_read_u32(dev->of_node, "vendor,some-value", &val);
if (ret)
dev_warn(dev, "missing vendor,some-value, using default\n");
// ... 注册中断、初始化硬件等
return 0;
}
这里有个细节:platform_get_resource 和 platform_get_irq 其实是封装了 of_ 函数。内核帮你把设备树信息转成了标准的 resource 结构体。我个人更推荐用 platform_ 系列,代码更简洁。
注意:of_iomap() 虽然方便,但它不检查资源冲突。生产代码建议用 devm_ioremap_resource(),它会在映射前做资源校验,出错了还会打印清晰的错误信息。
3.3 从设备树读取 GPIO
GPIO 在设备树里通常用 gpios 或 xxx-gpios 属性描述。比如:
my_device: my-device@40002000 {
compatible = "vendor,my-device";
reg = <0x40002000 0x100>;
enable-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio2 3 GPIO_ACTIVE_LOW>;
};
驱动里怎么读?我建议用 GPIO descriptor 接口,这是目前内核推荐的方式:
struct gpio_desc *enable_gpio, *reset_gpio;
enable_gpio = devm_gpiod_get(dev, "enable", GPIOD_OUT_LOW);
if (IS_ERR(enable_gpio))
return PTR_ERR(enable_gpio);
reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(reset_gpio))
return PTR_ERR(reset_gpio);
// 使用
gpiod_set_value(enable_gpio, 1);
gpiod_set_value(reset_gpio, 0);
注意 devm_gpiod_get 的第二个参数是 con_id,它会自动去掉属性名里的 "-gpios" 后缀。比如 "enable-gpios" 对应 con_id "enable"。这个设计挺巧妙的,少写不少代码。
我曾经在一个项目里用过老式的 of_get_named_gpio(),结果 GPIO 编号还要自己转成 IRQ,麻烦不说,还容易出错。后来全部换成 descriptor 接口,清爽多了。
避坑指南:GPIO_ACTIVE_HIGH / GPIO_ACTIVE_LOW 标志位会影响 gpiod_set_value 的行为。如果你设了 ACTIVE_LOW,gpiod_set_value(1) 实际输出的是低电平。别搞反了,否则硬件不干活。
3.4 读取中断资源
中断的读取方式取决于设备树怎么写。常见的有两种:
- 标准 interrupts 属性:用
platform_get_irq() - 命名中断:用
platform_get_irq_byname()
设备树示例:
my_device: my-device@40003000 {
compatible = "vendor,my-device";
reg = <0x40003000 0x100>;
interrupt-parent = <&gic>;
interrupts = <0 42 4>, <0 43 4>;
interrupt-names = "tx", "rx";
};
驱动里:
int tx_irq, rx_irq;
tx_irq = platform_get_irq_byname(pdev, "tx");
if (tx_irq < 0)
return tx_irq;
rx_irq = platform_get_irq_byname(pdev, "rx");
if (rx_irq < 0)
return rx_irq;
// 注册中断
ret = devm_request_irq(dev, tx_irq, tx_handler, 0, "my_device_tx", priv);
ret = devm_request_irq(dev, rx_irq, rx_handler, 0, "my_device_rx", priv);
用命名中断的好处是代码可读性强,而且不依赖中断在数组里的顺序。你想想看,如果哪天硬件工程师调整了中断顺序,你只要改设备树,驱动代码不用动。
3.5 读取时钟资源
时钟在设备树里用 clocks 和 clock-names 属性描述。内核提供了 clk 框架来操作。
设备树示例:
my_device: my-device@40004000 {
compatible = "vendor,my-device";
reg = <0x40004000 0x100>;
clocks = <&clk_axi>, <&clk_apb>;
clock-names = "axi", "apb";
};
驱动里:
struct clk *axi_clk, *apb_clk;
axi_clk = devm_clk_get(dev, "axi");
if (IS_ERR(axi_clk))
return PTR_ERR(axi_clk);
apb_clk = devm_clk_get(dev, "apb");
if (IS_ERR(apb_clk))
return PTR_ERR(apb_clk);
// 使能时钟
clk_prepare_enable(axi_clk);
clk_prepare_enable(apb_clk);
这里有个容易忽略的点:clk_prepare_enable 是 clk_prepare 和 clk_enable 的组合。在原子上下文中不能调用 clk_prepare(可能睡眠),得分开操作。我一般在 probe 里 prepare,在 runtime PM 里 enable/disable。
时钟操作最佳实践
- probe 中:clk_prepare_enable() 或分两步 prepare + enable
- remove 中:clk_disable_unprepare()
- runtime PM 中:clk_enable / clk_disable(假设已 prepare)
- 错误路径:记得按相反顺序释放已获取的资源
3.6 知识体系总览
下面这张图总结了设备树与驱动交互的核心流程。我画的时候特意把匹配、资源读取、probe 三个环节串在了一起,方便你理解整体脉络。
从这张图可以看得很清楚:设备树节点经过内核解析,通过 compatible 匹配到驱动,然后 probe 函数用 of_ 系列 API 把各种资源读出来,最后完成设备初始化。整个过程环环相扣,缺一不可。
3.7 常见问题与避坑
最后分享几个我实际项目中踩过的坑:
- compatible 写错:大小写、标点符号都不能错。我曾经因为少写了一个逗号,排查了整整半天。
- 忘记加 sentinel:of_match_table 数组最后必须有一个空条目
{ /* sentinel */ },否则内核遍历时会越界。 - GPIO 编号 vs descriptor:别再用手动编号了。Descriptor 接口会自动处理 GPIO 控制器映射、引用计数、设备树解析,省心又安全。
- 中断触发类型:设备树里写
IRQ_TYPE_EDGE_RISING,驱动里 request_irq 的 flags 参数要传 0,否则会冲突。内核会优先使用设备树里指定的类型。 - 时钟未使能就访问寄存器:有些 SoC 的模块时钟默认是关闭的。不使能时钟直接读写寄存器,轻则读回全 F,重则挂死总线。
重要提醒:调试设备树问题时,先检查 /sys/firmware/devicetree/base/ 下的内容,确认 DTB 是否被正确加载。很多时候问题出在 bootloader 没更新 DTB,而不是驱动代码有 bug。
好了,设备树与驱动的匹配机制、资源读取方法就讲到这里。这些 API 和流程我每天都在用,熟练了之后你会发现,设备树其实是个很优雅的设计——它把硬件描述和驱动代码彻底解耦了。下次维护代码时,你只需要改 dts 文件,不用动 C 代码,那种感觉真的很爽。