4. 设备树与内核驱动:platform驱动模型、of_match_table匹配、设备树API函数、中断映射
好,咱们进入第四章。这一章是设备树和内核驱动结合的关键环节。说白了,就是让内核知道:你设备树里描述的那个硬件,到底该由哪个驱动来管?驱动又该怎么从设备树里把配置信息读出来?
我刚开始接触这套东西的时候,也觉得有点绕。但搞明白之后你会发现,这套机制设计得非常巧妙。它把硬件描述和驱动代码彻底解耦了——换一块板子,改改设备树就行,驱动代码基本不用动。
4.1 platform驱动模型:设备与驱动的“媒人”
Linux内核里,设备驱动模型的核心思想就是“设备”和“驱动”分离。设备负责描述“我有什么硬件资源”,驱动负责说“我能操作什么硬件”。那谁来匹配它们呢?
对于挂载在PCIe、USB这类可枚举总线上的设备,总线本身会负责枚举和匹配。但咱们嵌入式系统里大量的设备——比如I2C控制器、SPI控制器、GPIO控制器、看门狗——它们直接挂在CPU的地址总线上,没法自动枚举。这些设备就叫 platform设备(平台设备)。
platform驱动模型就是专门为这类设备设计的。它包含两个核心结构体:
| 结构体 | 作用 | 典型定义位置 |
|---|---|---|
struct platform_device |
描述一个平台设备(资源、名称、设备树节点等) | 由内核在解析设备树时动态创建 |
struct platform_driver |
描述一个平台驱动(probe、remove、匹配表等) | 驱动开发者编写 |
匹配流程很简单:内核遍历所有注册的platform_driver,用driver里面的匹配表和每个platform_device的名字或ID进行比较。一旦匹配上,就调用驱动的 probe() 函数。你的驱动初始化代码,基本都写在probe里。
关键点:设备树中的每个节点,只要它的compatible属性匹配到了某个驱动,内核就会自动创建一个platform_device,然后触发匹配流程。你不需要手动注册设备。
4.2 of_match_table匹配:设备树和驱动的“暗号”
那驱动怎么告诉内核“我能支持哪些设备”呢?答案就是 of_match_table。这是platform_driver结构体里的一个成员,指向一个 of_device_id 数组。
每个 of_device_id 包含一个 .compatible 字符串。这个字符串就是和设备树节点里 compatible 属性对应的“暗号”。
举个例子。设备树里有个节点:
my_led: led-controller@1c20000 {
compatible = "vendor,my-led-driver";
reg = <0x1c20000 0x1000>;
interrupts = <0 31 4>;
};
那驱动里就要这么写:
static const struct of_device_id my_led_of_match[] = {
{ .compatible = "vendor,my-led-driver" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);
static struct platform_driver my_led_driver = {
.probe = my_led_probe,
.remove = my_led_remove,
.driver = {
.name = "my_led",
.of_match_table = my_led_of_match,
},
};
module_platform_driver(my_led_driver);
这里有个细节我提醒一下:MODULE_DEVICE_TABLE(of, ...) 这行不是摆设。它会把匹配表导出到模块的元数据里,这样内核在加载模块时就能知道这个驱动支持哪些设备,实现自动加载。我见过有人漏掉这行,结果模块死活不自动加载,排查了半天。
个人习惯:我写驱动时,of_match_table的最后一个元素总是留一个空结构体作为哨兵(sentinel),也就是上面代码里的 { /* sentinel */ }。内核遍历时遇到空结构体就会停止,防止越界。
4.3 设备树API函数:从设备树里“捞”数据
匹配成功之后,probe函数被调用。这时候你手里有一个 struct platform_device *pdev,它的 pdev->dev.of_node 就指向设备树中对应的节点。接下来,你需要用设备树API函数从这个节点里读取各种配置信息。
常用的API函数都在 #include <linux/of.h> 里。我列几个最常用的:
| 函数 | 作用 | 返回值 |
|---|---|---|
of_property_read_u32() |
读取一个32位整数属性 | 0成功,负值失败 |
of_property_read_u32_array() |
读取一个32位整数数组 | 0成功,负值失败 |
of_property_read_string() |
读取一个字符串属性 | 0成功,负值失败 |
of_property_read_bool() |
判断一个布尔属性是否存在 | true/false |
of_get_named_gpio() |
获取GPIO编号(旧API,建议用GPIO descriptor) | GPIO编号或负值错误 |
of_iomap() |
将reg属性中的地址映射到虚拟地址空间 | 映射后的虚拟地址 |
举个例子,读取上面那个LED节点的reg和中断:
static int my_led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct resource *res;
void __iomem *base;
u32 led_config;
int irq;
// 读取自定义属性 "led-config"
if (of_property_read_u32(dev->of_node, "led-config", &led_config)) {
dev_err(dev, "failed to read led-config\n");
return -EINVAL;
}
// 获取reg地址并映射
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;
// ... 后续初始化 ...
return 0;
}
注意看,这里我用了 platform_get_resource() 和 platform_get_irq()。这两个函数是platform驱动特有的,它们会自动从设备树解析出来的资源里提取信息。当然,你也可以直接用 of_iomap() 和 irq_of_parse_and_map(),但我个人更推荐用platform封装好的接口,代码更清晰。
我曾经踩过的坑:用 of_property_read_u32 读取属性时,如果设备树里这个属性没定义,函数会返回负值。很多新手不检查返回值,直接拿未初始化的变量去用,结果跑起来全是随机值。记住:所有设备树API的返回值都必须检查。
4.4 中断映射:从设备树到Linux中断号
设备树里描述中断的方式,和实际Linux内核里用的中断号是两套体系。设备树里用的是“中断控制器”视角的编号,而内核里用的是IRQ number(全局中断号)。中间需要一个映射过程。
这个过程由 irq_domain 机制完成。简单来说:
- 设备树节点通过
interrupts属性描述中断源(比如<0 31 4>表示中断类型、中断号、触发方式)。 - 中断控制器驱动注册一个
irq_domain,负责把设备树里的中断描述翻译成Linux IRQ number。 - 驱动调用
platform_get_irq()或irq_of_parse_and_map()时,内核自动完成映射。
对于大多数驱动开发者来说,你不需要自己实现irq_domain——那是中断控制器驱动的事。你只需要在probe里拿到IRQ number,然后调用 request_irq() 注册中断处理函数就行。
但有一点要注意:设备树里 interrupts 属性的格式,取决于它引用的中断控制器。最常见的是GIC(通用中断控制器),它的格式是:
interrupts = <中断类型 中断号 触发方式>;
- 中断类型:0表示SPI(共享外设中断),1表示PPI(私有外设中断)。
- 中断号:相对于中断控制器的编号。注意SPI的中断号是从32开始的(硬件上GIC的SPI从ID32开始)。
- 触发方式:4表示高电平触发,8表示上升沿触发,等等。
举个例子,<0 31 4> 表示:SPI类型,中断号31(对应GIC的ID 32+31=63),高电平触发。
避坑指南:不同厂商的SoC,中断控制器的实现可能不同。有的用GIC v2,有的用GIC v3,还有的用自家写的中断控制器。一定要看芯片手册里关于中断号映射的说明。我曾经在一个项目里,设备树写的中断号是31,但实际硬件的中断线是63,查了两天才发现是GIC的SPI基地址偏移没算对。
4.5 本章知识体系总览
为了让你对整个流程有个直观的印象,我画了一张图。它展示了从设备树节点到驱动probe的完整链路:
嗯,这张图把整个链路串起来了。你写驱动的时候,脑子里要有这个流程:设备树节点 → 内核解析 → 匹配驱动 → probe → 读取资源。每一步都有对应的API和数据结构,搞清楚了,写驱动就是填表格的事。