Linux 驱动开发两种范式对比:platform driver 与 device tree 的优劣势分析与迁移策略
一、两种范式的历史演进
Linux 内核驱动模型经历了从"板级文件硬编码"到"设备树动态描述"的演变。platform driver 模型在 Linux 2.6 时期引入,解决了设备与驱动的分离问题;device tree(设备树)自 ARM Linux 3.x 起成为主流,将硬件描述从内核源码中彻底剥离。
两种范式的本质区别在于:硬件信息存放位置不同。platform driver 的硬件资源定义在 C 源码的 platform_device 结构体中;device tree 则通过 .dts 文件以树形结构描述外设。这一差异看似微小,却深刻影响了整个 BSP 开发流程。
二、架构对比
2.1 platform driver 的典型实现
/**
* platform driver 传统写法 – 硬件资源硬编码在 C 源码中
* 每新增一块板子需要复制粘贴板级文件,维护成本随板卡数量线性增长
*/
#include <linux/platform_device.h>
#include <linux/module.h>
#include <linux/interrupt.h>
/* 硬件资源定义:内存基址、中断号全部写死在代码中 */
static struct resource my_uart_resources[] = {
{
.start = 0x20200000, /* UART 寄存器基址 */
.end = 0x202000FF, /* UART 寄存器结束地址 */
.flags = IORESOURCE_MEM,
.name = "uart_regs",
},
{
.start = 32, /* 中断号硬编码 */
.end = 32,
.flags = IORESOURCE_IRQ,
.name = "uart_irq",
},
};
/* platform device – 板级文件中定义 */
static struct platform_device my_board_uart = {
.name = "my_uart", /* 与驱动 .name 匹配 */
.id = 0,
.num_resources = ARRAY_SIZE(my_uart_resources),
.resource = my_uart_resources,
.dev = {
.platform_data = NULL,
},
};
/* 驱动侧 – 通过 platform_get_resource 获取硬件信息 */
static int my_uart_probe(struct platform_device *pdev) {
struct resource *res;
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "[错误] 无法获取 MEM 资源\\n");
return -ENODEV;
}
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base)) {
dev_err(&pdev->dev, "[错误] IO 映射失败: %ld\\n", PTR_ERR(base));
return PTR_ERR(base);
}
/* 中断号也从资源表中提取 */
int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "[错误] 无法获取 IRQ 资源: %d\\n", irq);
return irq;
}
return devm_request_irq(&pdev->dev, irq, my_uart_handler,
0, "my_uart", pdev);
}
2.2 device tree 的等效实现
/**
* device tree 驱动 – 硬件信息来自 dtb,无需修改 C 源码
* 同一份驱动可适配不同板卡,只需修改 .dts 文件
*/
#include <linux/of.h>
#include <linux/of_device.h>
/* 兼容性列表 – 支持多个 SoC 变体 */
static const struct of_device_id my_uart_of_match[] = {
{ .compatible = "vendor,my-uart-v1", },
{ .compatible = "vendor,my-uart-v2", },
{ /* 哨兵节点 */ },
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);
static int my_uart_probe_dt(struct platform_device *pdev) {
struct device_node *np = pdev->dev.of_node;
void __iomem *base;
int irq, ret;
/* 从设备树节点自动获取 IRQ(无需硬编码中断号) */
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "[错误] 设备树中未定义中断: %d\\n", irq);
return irq;
}
ret = devm_request_irq(&pdev->dev, irq, my_uart_handler,
IRQF_TRIGGER_RISING, "my_uart", pdev);
if (ret) {
dev_err(&pdev->dev, "[错误] 中断注册失败: %d\\n", ret);
return ret;
}
/* 从设备树解析自定义属性 */
int baudrate;
if (of_property_read_u32(np, "default-baudrate", &baudrate)) {
dev_warn(&pdev->dev, "[警告] 未指定波特率,使用默认值 115200\\n");
baudrate = 115200;
}
dev_info(&pdev->dev, "UART 初始化完成,IRQ=%d,波特率=%d\\n", irq, baudrate);
return 0;
}
对应的 .dts 描述:
/* 设备树中的描述 – 独立于内核源码 */
&uart1 {
compatible = "vendor,my-uart-v1";
reg = <0x20200000 0x100>;
interrupts = <32>;
default-baudrate = <921600>; /* 自定义属性 */
status = "okay";
};
三、优劣势量化对比
| 板卡适配效率 | 每板一份 C 文件 | 每板一份 .dts(文本) | DT 效率高 3-5 倍 |
| 内核镜像通用性 | 一变全变,需重新编译 | 内核不变,换 dtb 即可 | DT 显著优势 |
| 学习曲线 | 直观(纯 C 思维) | 需学 dts 语法 + bindings | Platform 更友好 |
| 编译时错误检测 | 编译器能发现 | 需额外 dtc 检查 + 运行时 | Platform 更安全 |
| 上下游合作效率 | 驱动/BSP 耦合 | 驱动/BSP 解耦 | DT 大幅改善 |
| 引脚复用配置 | 代码中 pinctrl | pinctrl 节点独立配置 | DT 更灵活 |
| 运行时修改硬件配置 | 需重新编译 | 加载 dtbo 叠加层即可 | DT 支持动态加载 |
四、迁移策略与实践建议
关键迁移原则:
五、总结
Device tree 已是 ARM Linux 的事实标准,新项目不要再使用 platform device 硬编码方式。但迁移老项目需要理性评估:如果只有 1-2 块定制板卡且无上游化计划,迁移收益有限。当板卡数超过 3 块或需要与 SoC 厂商协作时,device tree 带来的 BSP 解耦收益是决定性的。驱动程序本身建议保留 of_match_table + 传统 platform_driver 双兼容模式,一次重构,兼容新旧两种硬件描述方式。


