3. 设备驱动模型(LDM):总线、设备、驱动模型介绍、platform总线、设备树(Device Tree)基础

说实话,我刚入行那会儿,写驱动的方式特别原始。直接在板级文件里硬编码寄存器地址、中断号、GPIO引脚。换一个板子,代码就得重写一遍。后来接触了Linux设备驱动模型(LDM),才明白什么叫“优雅”。

LDM的核心思想很简单:把硬件资源、驱动代码、连接方式三者解耦。你想想看,一个USB网卡插到不同主板上,驱动不用改,为什么?因为USB总线帮你把设备枚举好了。LDM就是要把这种“总线-设备-驱动”的模型推广到所有外设上。

3.1 三要素:总线、设备、驱动

模型里就三个角色,我习惯用一句话记:驱动匹配设备,总线负责牵线。

角色 内核结构体 职责
总线 struct bus_type 定义匹配规则(match函数)、管理设备和驱动的链表
设备 struct device 描述硬件实体:地址、中断、时钟、DMA通道
驱动 struct device_driver 实现操作函数:probe、remove、suspend、resume

流程是这样的:内核启动时,总线会遍历所有注册的设备,对每个设备调用驱动的match函数。匹配上了,就调用驱动的probe。我在项目里遇到过一个问题:两个驱动声称支持同一个设备ID,结果probe顺序乱了,设备初始化一半就崩了。后来我养成了习惯——驱动ID表一定要做唯一性审查。

核心机制:设备与驱动的匹配完全由总线管理。驱动不直接操作设备结构体,设备也不关心谁在驱动它。这种分层设计让代码复用率大幅提升。

3.2 platform总线:虚拟总线的妙用

问题来了:SoC内部集成的I2C控制器、SPI控制器、GPIO模块,它们不挂在PCIe、USB、I2C这类物理总线上。怎么办?内核造了一条虚拟总线——platform总线。

platform总线对应的设备叫platform_device,驱动叫platform_driver。匹配规则默认就是比较名字。举个例子:

// 设备端
static struct platform_device my_device = {
    .name = "my-uart",
    .id = -1,
    .resource = (struct resource[]) {
        {
            .start = 0x10000000,
            .end   = 0x10000fff,
            .flags = IORESOURCE_MEM,
        },
        {
            .start = 42,
            .flags = IORESOURCE_IRQ,
        },
    },
};

// 驱动端
static struct platform_driver my_driver = {
    .driver = {
        .name = "my-uart",
    },
    .probe = my_uart_probe,
    .remove = my_uart_remove,
};

匹配上了,probe函数里就能拿到resource,直接ioremap。我记得第一次用platform框架重写一个老驱动,代码量减少了40%,而且移植到新芯片只改了资源表。嗯,这就是LDM的魅力。

个人习惯:写platform驱动时,我总会在probe函数开头加一句dev_info(&pdev->dev, "probe success\n");。调试时一眼就能看到哪个设备被谁驱动了,省去很多查日志的时间。

3.3 设备树(Device Tree)基础

platform_device虽然好,但每个板子都要写一堆C文件定义资源。ARM社区受够了这种“板级文件地狱”,于是设备树(DT)成了标准做法。

设备树说白了就是用文本描述硬件,编译成dtb文件,内核启动时解析它,自动生成platform_device。这样换板子只需要换dtb,内核不用重新编译。

来看一个典型的设备树节点:

/ {
    soc {
        uart@10000000 {
            compatible = "vendor,my-uart";
            reg = <0x10000000 0x1000>;
            interrupts = <0 42 4>;
            clocks = <&clk 15>;
            status = "okay";
        };
    };
};

关键字段解释:

  • compatible:匹配驱动的“身份证”,格式推荐"厂商,设备名"
  • reg:地址和长度,对应IORESOURCE_MEM
  • interrupts:中断号、触发类型,对应IORESOURCE_IRQ
  • status:设为"disabled"可以临时屏蔽设备,调试时很有用

驱动端只需要在of_match_table里声明compatible值:

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-uart" },
    { /* sentinel */ }
};

static struct platform_driver my_driver = {
    .driver = {
        .name = "my-uart",
        .of_match_table = my_of_match,
    },
    .probe = my_uart_probe,
};

我曾经踩过的坑:设备树里reg属性写了<0x10000000 0x1000>,驱动里用platform_get_resource(pdev, IORESOURCE_MEM, 0)拿到的地址范围是0x10000000~0x10000fff。但芯片手册说UART寄存器只占0x100个字节,我多映射了0xf00字节。虽然没出问题,但这是坏习惯——reg的长度一定要和硬件实际地址空间一致。

3.4 从设备树到platform_device的完整链路

为了让你更直观地理解,我画了一张流程图:

设备树到platform_device的解析流程 设备树 → platform_device 解析流程 .dts 源文件 dtc 编译 .dtb 二进制 of_platform_populate() 解析 platform_device 关键函数:of_platform_populate() 遍历DT节点,为每个 compatible 匹配的节点创建 platform_device

流程很清楚:.dts → dtc编译 → .dtb → 内核解析 → platform_device。内核启动时,of_platform_populate()会递归遍历设备树,为每个状态为"okay"且包含compatible属性的节点创建platform_device。然后platform总线开始匹配驱动,走我们前面说的probe流程。

3.5 调试技巧:怎么看设备树是否生效

我调试时最常用的三板斧:

  1. /proc/device-tree:挂载后能看到设备树的内存视图,直接cat可以检查节点属性是否被正确解析
  2. dmesg | grep platform:看platform总线匹配过程,有没有probe失败
  3. ls /sys/bus/platform/devices/:列出所有platform设备,确认设备节点是否生成

小技巧:如果设备树里status设为"disabled",内核不会为该节点创建platform_device。调试时想临时屏蔽某个外设,改status比改驱动方便得多。我经常用这招来隔离问题——先禁用所有外设,再逐个启用,定位哪个设备导致系统挂死。