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的完整链路
为了让你更直观地理解,我画了一张流程图:
流程很清楚:.dts → dtc编译 → .dtb → 内核解析 → platform_device。内核启动时,of_platform_populate()会递归遍历设备树,为每个状态为"okay"且包含compatible属性的节点创建platform_device。然后platform总线开始匹配驱动,走我们前面说的probe流程。
3.5 调试技巧:怎么看设备树是否生效
我调试时最常用的三板斧:
- /proc/device-tree:挂载后能看到设备树的内存视图,直接cat可以检查节点属性是否被正确解析
- dmesg | grep platform:看platform总线匹配过程,有没有probe失败
- ls /sys/bus/platform/devices/:列出所有platform设备,确认设备节点是否生成
小技巧:如果设备树里status设为"disabled",内核不会为该节点创建platform_device。调试时想临时屏蔽某个外设,改status比改驱动方便得多。我经常用这招来隔离问题——先禁用所有外设,再逐个启用,定位哪个设备导致系统挂死。