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 机制完成。简单来说:

  1. 设备树节点通过 interrupts 属性描述中断源(比如 <0 31 4> 表示中断类型、中断号、触发方式)。
  2. 中断控制器驱动注册一个 irq_domain,负责把设备树里的中断描述翻译成Linux IRQ number。
  3. 驱动调用 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的完整链路:

设备树 → platform驱动匹配与资源获取流程 设备树节点 compatible + reg + interrupts 内核OF核心 解析设备树,创建platform_device platform_device dev.of_node = 节点指针 of_match_table匹配 匹配成功 probe() 被调用 of_property_read_*() platform_get_resource() platform_get_irq() 驱动获得:寄存器基地址 + 中断号 + 自定义配置参数 说明:整个流程从设备树节点开始,经过OF核心解析、匹配、probe,最终驱动拿到所有硬件资源

嗯,这张图把整个链路串起来了。你写驱动的时候,脑子里要有这个流程:设备树节点 → 内核解析 → 匹配驱动 → probe → 读取资源。每一步都有对应的API和数据结构,搞清楚了,写驱动就是填表格的事。