引言
在嵌入式 Linux 开发中,硬件资源的描述至关重要。驱动代码负责操作设备,而设备结构体(struct device)则描述硬件资源。问题在于,这些硬件信息如何进入内核并与驱动配对?
在单片机裸机编程中,硬件地址硬编码在代码里。但在 Linux 中,为了保证驱动的通用性,避免在代码中出现具体硬件地址是核心原则。因此,需要一种机制将硬件信息从驱动代码中剥离。
设备树用于解决 ARM 架构下硬件描述碎片化问题,替代了硬编码的 board file 模式。通过 DTS 源码编译为 DTB 二进制文件,由 Bootloader 传入内核。内核解析 DTB 生成 device_node 并转换为 platform_device,实现驱动与硬件的匹配。驱动通过 of_系列函数读取设备树中的标准资源(reg, interrupts)及自定义属性。设备树使内核镜像能支持多种硬件,提升了可维护性。

在嵌入式 Linux 开发中,硬件资源的描述至关重要。驱动代码负责操作设备,而设备结构体(struct device)则描述硬件资源。问题在于,这些硬件信息如何进入内核并与驱动配对?
在单片机裸机编程中,硬件地址硬编码在代码里。但在 Linux 中,为了保证驱动的通用性,避免在代码中出现具体硬件地址是核心原则。因此,需要一种机制将硬件信息从驱动代码中剥离。
这就是本文的主题——设备树。
2000 年代末期,ARM 架构迎来井喷式发展。相比 x86 架构的标准化,ARM 呈现碎片化状态。各家 SOC 厂商(高通、三星、TI 等)推出不同芯片,同一款 SOC 也可能有不同的外设配置。Linux 想统一支持所有设备,但当时 ARM 设备基本靠硬编码告诉内核硬件信息。
这导致 Linux 内核 arch/arm 目录下代码量急速膨胀。支持新板子需添加专属文件。
早期 ARM Linux 采用 board file(板级支持文件模式),即每个设备板子都有独立的 board-xxx.c 文件。文件中硬编码了内存映射、时钟配置、GPIO 引脚复用等信息。
这种模式在 2010 年左右暴露出问题:
Linus Torvalds 曾批评 ARM 社区的碎片化,要求推动根本性变革。
社区最终借鉴 PowerPC 架构的 Open Firmware 机制。核心想法是把硬件描述从 C 源码中剥离出来,用独立的数据文件描述硬件,由 bootloader 传入内核,让内核在运行时动态解析。
使用 .dts(Device Tree Source)文件描述硬件树状结构,编译成 .dtb(Device Tree Blob)文件传给内核。
好处包括:删除重复代码,一个内核镜像支持多种硬件,易读性和可维护性提升,便于上游合并。
设备树是由节点和属性构成的树。
Linux 采用分层和包含机制。以 NXP i.MX6ULL 芯片为例。
由芯片原厂提供,如 imx6ull.dtsi,描述芯片内部固有的硬件资源。物理地址和中断号不会变。
文件体现继承与重写策略。例如引用父类标签只修改需要的部分,或删除不存在的节点。
开发板厂商编写描述文件,如 imx6ull-14x14-evk.dts。文件开头引用 SOC 级文件,开发者只需关注板子上的外设配置。
通常在最外层使用 &标签名方式引用节点开启默认关闭的外设,或在根节点内部添加外接新设备。
在没有设备树时,platform_device 名字必须和 platform_driver 名字一样。
在设备树世界里,匹配规则主角变成了 compatible 属性。只要设备树节点和驱动程序的 compatible 属性相同即可匹配。
compatible 属性通常是字符串列表,格式为'厂商,芯片型号 - 模块名'。
驱动程序中会定义相应的 compatible 属性数组,Platform 总线据此匹配。
内核在启动过程中充当中间人,把设备树从文本变成结构体。
Bootloader(U-Boot)把内核镜像和 .dtb 文件加载到内存。启动内核时,把 .dtb 文件的内存首地址通过寄存器传给内核。
内存里的 .dtb 是二进制数据。内核在 setup_arch 阶段执行 unflatten_device_tree 函数,解析二进制数据,把它们展开成内核里的结构体链表。
为设备树中的每一个节点创建一个 struct device_node 结构体。
内核调用核心函数 of_platform_populate(),遍历 device_node 树,把看起来像是设备的节点转换成 Linux 驱动模型中的 struct platform_device。
只有挂载在根节点或 simple-bus 下的子节点才会被转换。
内核解析 reg 属性转换成 IORESOURCE_MEM,interrupts 属性转换成 IORESOURCE_IRQ,最后调用 platform_device_register 注册到总线上。
在 probe 函数里,通常需要获取硬件资源和设备树里面的一些自定义配置。Linux 内核提供了一套以 of_开头的函数。
reg 和 interrupts 已由 of_platform_populate 自动转换。直接用 Platform 子系统的标准接口。
// 在驱动的 probe 函数中
static int my_driver_probe(struct platform_device *pdev)
{
struct resource *res;
int irq;
// 获取寄存器地址,第三个参数 0 表示第 0 组 reg
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENOMEM;
// 拿到物理地址后,要把它映射成虚拟地址才能用
void __iomem *base_addr = devm_ioremap_resource(&pdev->dev, res);
// 获取中断号,参数 0 表示第 0 个中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
//......
}
获取寄存器地址函数原型:
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);
获取中断号函数原型:
int platform_get_irq(struct platform_device *dev, unsigned int num);
对于自定义属性,内核没有自动转换,需要手动查表,函数都在 <linux/of.h> 中。
struct device_node *np = pdev->dev.of_node;
u32 delay_time;
if (of_property_read_u32(np, "my-delay-ms", &delay_time) == 0) {
printk("get delay time: %d\n", delay_time);
} else {
printk("未找到该属性\n");
}
const char *str;
of_property_read_string(np, "my-name", &str);
u32 data_array[3];
of_property_read_u32_array(np, "my-data", data_array, 3);
现代内核推荐使用更高级的 GPIO Descriptor (gpiod) 接口。
struct gpio_desc *rst_gpio;
rst_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_LOW);
struct device_node *target_node;
target_node = of_find_node_by_path("/backlight/lcd-backlight");
if (target_node) {
// 找到后,就可以用 of_property_read_xxx 去读它的属性了
}
Linux 内核通过 procfs 文件系统展现解析好的设备树结构。
设备树在文件系统中的实体位于 /proc/device-tree。实际上这是一个软链接,指向/sys/firmware/devicetree/base。
可以使用 tree 命令查看设备树层级。
在 DTS 中添加节点后,可在 /proc/device-tree 下找到该节点。注意 name 属性即使未定义也会自动存在。
对于数值型属性(reg, interrupts, gpios 等),需用 hexdump 命令按十六进制打印,因为 cat 命令可能因空字符显示异常。
在 /sys/bus/platform/devices/目录下执行 ls 命令,能看到熟悉的设备名。这说明设备树不仅解析成功,而且已经成功注册为板载设备。
设备树的引入彻底结束了 ARM 社区代码脏乱差的时代。对于驱动开发者来说,它让代码变得更加纯粹——驱动只管逻辑,硬件交给设备树。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online