嵌入式Linux设备树(DTS)深度解析——从硬件描述到驱动绑定

📅 2026/7/6 20:03:53 👁️ 阅读次数 📝 编程学习
嵌入式Linux设备树(DTS)深度解析——从硬件描述到驱动绑定

文章目录

    • 每日一句正能量
    • 一、引言:为什么需要设备树
    • 二、设备树架构与编译流程
      • 2.1 设备树源文件类型
      • 2.2 编译工具链
      • 2.3 内核驱动绑定流程
    • 三、设备树节点层次结构
      • 3.1 节点命名规则
      • 3.2 节点引用与别名
      • 3.3 常见标准节点
    • 四、属性详解与数据类型
      • 4.1 标准属性
      • 4.2 属性数据类型
    • 五、compatible 属性与驱动绑定
      • 5.1 匹配机制详解
      • 5.2 匹配优先级
    • 六、设备树覆盖层 (Overlay)
      • 6.1 覆盖层源码结构
      • 6.2 编译与加载
      • 6.3 覆盖层应用场景
    • 七、中断与时钟属性深度解析
      • 7.1 中断属性
      • 7.2 时钟属性
    • 八、设备树调试与验证工具链
      • 8.1 常用调试命令
      • 8.2 驱动调试技巧
    • 九、设备树绑定文档 (Binding) 规范
      • 9.1 YAML 绑定文档结构
      • 9.2 验证绑定文档
    • 十、实战:从零编写一个完整设备树
      • 10.1 硬件假设
      • 10.2 SoC 级定义 (soc.dtsi)
      • 10.3 板级定义 (board.dts)
      • 10.4 驱动实现
    • 十一、总结与最佳实践
      • 11.1 设备树编写原则
      • 11.2 常见错误排查

每日一句正能量

个人选择的路,自己扛,不跟世界要安慰,只跟自身要答案。
这是成年人的底色:你选的路,结果好坏都由自己承担。向外索求同情或理解,往往徒增失望;向内追问“我该如何调整、如何负责”,才是解决之道。自立,从停止扮演受害者开始。


一、引言:为什么需要设备树

在早期的嵌入式 Linux 开发中,硬件信息(寄存器地址、中断号、引脚配置等)直接硬编码在内核源码中(如arch/arm/mach-xxx/board-xxx.c)。这种架构存在严重问题:

  • 内核膨胀:每新增一块开发板,都需要修改内核源码
  • 维护困难:硬件变更需要重新编译整个内核
  • 可移植性差:同一 SoC 的不同板级配置无法复用

设备树(Device Tree)的引入彻底解决了这些问题。它通过一种独立的、声明式的数据结构描述硬件,实现了内核与硬件的解耦

  • SoC 厂商:提供.dtsi文件描述通用硬件(CPU、中断控制器、时钟等)
  • 板级厂商:提供.dts文件描述具体外设连接
  • 内核:解析设备树,动态加载匹配的驱动

本文将从设备树架构、节点与属性、驱动绑定机制、覆盖层技术、调试工具等方面,深入解析设备树的完整技术栈。


二、设备树架构与编译流程

2.1 设备树源文件类型

文件类型扩展名用途示例
设备树源码.dts板级硬件描述board.dts
设备树包含.dtsiSoC 通用定义soc.dtsi
设备树二进制.dtb编译后的内核加载格式output.dtb
覆盖层源码.dts(含/plugin/)运行时动态修改overlay.dts
覆盖层二进制.dtbo覆盖层编译输出output.dtbo

2.2 编译工具链

# 安装设备树编译器sudoapt-getinstalldevice-tree-compiler# DTS → DTB (编译)dtc-Idts-Odtb-ooutput.dtb board.dts# DTB → DTS (反编译,用于调试)dtc-Idtb-Odts-oboard.dts output.dtb# 语法检查dtc-Idts-Odtb board.dts2>&1|head-20# 查看 DTB 信息fdtdump output.dtb|head-50

2.3 内核驱动绑定流程

设备树与内核驱动的绑定遵循严格的匹配机制:

/** * 内核启动时设备树解析流程 */// 1. 早期启动:展开 DTBvoid__initsetup_arch(char**cmdline){// unflatten_device_tree() 将扁平 DTB 展开为 device_node 树unflatten_device_tree();}// 2. 平台设备注册void__initof_platform_populate(structdevice_node*root,conststructof_device_id*matches,structdevice_node*parent){// 遍历所有设备节点,为每个节点创建 platform_devicefor_each_child_of_node(root,child){of_platform_device_create(child,NULL,parent);}}// 3. 驱动匹配staticintplatform_match(structdevice*dev,structdevice_driver*drv){structplatform_device*pdev=to_platform_device(dev);structplatform_driver*pdrv=to_platform_driver(drv);// 3.1 尝试 OF (Open Firmware) 风格匹配if(of_driver_match_device(dev,drv))return1;// 3.2 回退到 id_table 匹配if(pdrv->id_table)returnplatform_match_id(pdrv->id_table,pdev)!=NULL;// 3.3 最后尝试名称匹配returnstrcmp(pdev->name,drv->name)==0;}

三、设备树节点层次结构

3.1 节点命名规则

/* 节点命名: <name>[@<unit-address>] */ /soc/i2c@40020000/eeprom@50 /* 名称 (name): 设备类型标识 */ i2c, uart, gpio, eeprom /* 单元地址 (unit-address): 寄存器基地址或设备地址 */ @40020000 /* 寄存器地址 */ @50 /* I2C 设备地址 */ @0 /* SPI CS 片选号 */

3.2 节点引用与别名

/ { /* 别名:简化路径引用 */ aliases { serial0 = &uart0; i2c0 = &i2c0; spi0 = &spi0; }; /* 标签 (label):用于 & 引用 */ i2c0: i2c@40020000 { compatible = "vendor,i2c"; reg = <0x40020000 0x1000>; status = "disabled"; }; }; /* 使用 & 引用并修改节点 */ &i2c0 { status = "okay"; clock-frequency = <400000>; eeprom@50 { compatible = "atmel,24c256"; reg = <0x50>; pagesize = <64>; }; };

3.3 常见标准节点

节点路径用途典型属性
/根节点#address-cells,#size-cells,model,compatible
/cpusCPU 描述device_type = "cpu",reg,clock-frequency
/memory内存布局device_type = "memory",reg
/chosen启动参数bootargs,stdout-path
/aliases别名定义serial0,i2c0
/socSoC 外设包含所有片上外设节点

四、属性详解与数据类型

4.1 标准属性

/ { /* 模型标识 */ model = "MyCompany MyBoard"; /* 兼容字符串:用于驱动匹配 */ compatible = "mycompany,myboard", "mycompany,myboard-v1"; /* 地址单元格大小 */ #address-cells = <2>; /* 64位地址需要2个cells */ #size-cells = <2>; /* 64位大小需要2个cells */ soc { /* 子节点继承或覆盖 */ #address-cells = <1>; #size-cells = <1>; uart0: serial@40020000 { /* 寄存器地址: <base length> */ reg = <0x40020000 0x1000>; /* 中断: <type number flags> */ interrupts = <GIC_SPI 24 IRQ_TYPE_LEVEL_HIGH>; /* 时钟引用 */ clocks = <&clk 12>; clock-names = "uartclk"; /* 状态控制 */ status = "okay"; /* "okay" 或 "disabled" */ }; }; };

4.2 属性数据类型

类型表示示例读取 API
字符串"..."compatible = "vendor,device"of_property_read_string()
32位整数<value>reg = <0x40020000>of_property_read_u32()
数组<v1 v2 v3>reg = <0x4000 0x1000>of_property_read_u32_array()
布尔属性存在即真interrupt-controllerof_property_read_bool()
引用<&label>clocks = <&clk 12>of_parse_phandle()
字符串列表"a", "b"compatible = "a", "b"of_property_read_string_array()

五、compatible 属性与驱动绑定

compatible是设备树中最重要的属性,它是连接硬件描述与软件驱动的桥梁。

5.1 匹配机制详解

/* 设备树中的 compatible */ eeprom@50 { /* 优先匹配第一个,失败则回退到第二个 */ compatible = "atmel,24c256", "atmel,24c"; reg = <0x50>; };
/* 驱动中的 of_match_table */staticconststructof_device_idat24_of_match[]={/* 精确匹配: 设备树 "atmel,24c256" → 驱动 at24_data_24c256 */{.compatible="atmel,24c256",.data=&at24_data_24c256},{.compatible="atmel,24c512",.data=&at24_data_24c512},{.compatible="atmel,24c",.data=&at24_data_generic},{/* sentinel */}};MODULE_DEVICE_TABLE(of,at24_of_match);staticstructi2c_driverat24_driver={.driver={.name="at24",.of_match_table=at24_of_match,},.probe=at24_probe,.remove=at24_remove,};staticintat24_probe(structi2c_client*client){conststructof_device_id*match;conststructat24_chip_data*chip;/* 获取匹配信息 */match=of_match_device(at24_of_match,&client->dev);if(!match)return-ENODEV;/* 获取设备特定数据 */chip=match->data;dev_info(&client->dev,"Matched: %s, pagesize=%d\n",match->compatible,chip->page_size);/* 继续初始化... */}

5.2 匹配优先级

设备树: compatible = "vendor,device-v2", "vendor,device"; 匹配顺序: 1. 驱动 of_match_table 中 "vendor,device-v2" → 精确匹配 2. 驱动 of_match_table 中 "vendor,device" → 回退匹配 3. 驱动 id_table 匹配 4. 驱动 name 匹配 最佳实践: - 新设备: "vendor,device-v2" - 兼容旧驱动: "vendor,device"

六、设备树覆盖层 (Overlay)

覆盖层允许在运行时动态修改设备树,无需重新编译完整 DTB。

6.1 覆盖层源码结构

// myoverlay.dts /dts-v1/; /plugin/; /* 声明为覆盖层 */ &i2c0 { /* 引用已有节点 */ status = "okay"; clock-frequency = <400000>; /* 新增子节点 */ eeprom@50 { compatible = "atmel,24c256"; reg = <0x50>; pagesize = <64>; }; }; /* 也可以新增根节点 */ / { leds { compatible = "gpio-leds"; led0 { gpios = <&gpio0 5 GPIO_ACTIVE_LOW>; label = "status"; }; }; };

6.2 编译与加载

# 编译覆盖层dtc-Idts-Odtb-omyoverlay.dtbo myoverlay.dts# 方法一: 通过 configfs 加载 (Linux 4.10+)mkdir-p/sys/kernel/config/device-tree/overlays/myoverlaycatmyoverlay.dtbo>/sys/kernel/config/device-tree/overlays/myoverlay/dtbo# 方法二: 通过 U-Boot 加载 (启动时)setenv bootargs"${bootargs}dtoverlay=myoverlay"# 或在 U-Boot 命令行:fdt addr${fdt_addr}fdt resize8192load mmc0:1${loadaddr}myoverlay.dtbo fdt apply${loadaddr}

6.3 覆盖层应用场景

场景说明
HAT/扩展板Raspberry Pi 等平台的硬件扩展
** cape/子卡**BeagleBone 的 cape 扩展
动态外设热插拔设备(如 USB 转 I2C)
产品变体同一主板的不同 SKU(有无 WiFi/蓝牙)

七、中断与时钟属性深度解析

7.1 中断属性

/* GIC 中断控制器定义 */ gic: interrupt-controller@40041000 { compatible = "arm,cortex-a7-gic"; #interrupt-cells = <3>; /* 声明后续 cells 数量 */ interrupt-controller; /* 标记为中断控制器 */ reg = <0x40041000 0x1000>, <0x40042000 0x2000>; }; /* 设备使用中断 */ serial0: serial@40020000 { compatible = "ns16550a"; reg = <0x40020000 0x1000>; /* interrupts = <类型 中断号 触发方式> */ interrupts = <GIC_SPI 24 IRQ_TYPE_LEVEL_HIGH>; /* 中断父节点 */ interrupt-parent = <&gic>; };

中断 cells 含义(ARM GIC):

Cell说明
[0]GIC_SPI (0)共享外设中断
GIC_PPI (1)私有外设中断
[1]0-1019中断号
[2]IRQ_TYPE_LEVEL_HIGH高电平触发
IRQ_TYPE_EDGE_RISING上升沿触发
IRQ_TYPE_EDGE_FALLING下降沿触发
IRQ_TYPE_LEVEL_LOW低电平触发

7.2 时钟属性

/* 时钟控制器 */ clk: clock-controller@40000000 { compatible = "vendor,clk"; #clock-cells = <1>; /* 声明时钟 ID 需要 1 个 cell */ reg = <0x40000000 0x1000>; }; /* 设备使用时钟 */ serial0: serial@40020000 { compatible = "ns16550a"; /* clocks = <&控制器 时钟ID> */ clocks = <&clk 12>; clock-names = "uartclk"; };

驱动中获取时钟:

staticintserial_probe(structplatform_device*pdev){structclk*clk;/* 方法一: 通过名称获取 */clk=devm_clk_get(&pdev->dev,"uartclk");if(IS_ERR(clk))returnPTR_ERR(clk);/* 方法二: 通过索引获取 */clk=devm_clk_get(&pdev->dev,NULL);/* 第一个时钟 *//* 使能时钟 */clk_prepare_enable(clk);/* 获取时钟频率 */unsignedlongrate=clk_get_rate(clk);dev_info(&pdev->dev,"Clock rate: %lu Hz\n",rate);/* 禁用时钟 */clk_disable_unprepare(clk);return0;}

八、设备树调试与验证工具链

8.1 常用调试命令

# ========== 编译时调试 ==========# 检查 DTS 语法dtc-Idts-Odtb board.dts2>&1# 查看展开后的完整设备树(包含所有 include)cpp-nostdinc-Iinclude-Iarch/arm/boot/dts-xassembler-with-cpp\-D__DTS__board.dts|dtc-Idts-Odts -# ========== 运行时调试 ==========# 查看设备树节点ls/proc/device-tree/ls/proc/device-tree/soc/# 查看节点属性cat/proc/device-tree/soc/i2c0/compatiblecat/proc/device-tree/soc/i2c0/statuscat/proc/device-tree/soc/i2c0/reg|xxd# 查找特定设备find/proc/device-tree-name"*eeprom*"# 查看设备绑定情况ls/sys/bus/platform/devices/cat/sys/bus/platform/devices/soc\:i2c0/uevent# ========== U-Boot 调试 ==========# 查看设备树fdt print /soc/i2c0 fdt print /soc/i2c0/eeprom@50# 修改设备树(内存中)fdtset/soc/i2c0 status"okay"fdtset/soc/i2c0 clock-frequency<400000># 保存修改后的设备树fdt addr${fdt_addr}fdt save

8.2 驱动调试技巧

/* 打印设备树节点信息 */staticvoiddebug_device_node(structdevice_node*np){structproperty*prop;pr_info("Node: %s\n",np->full_name);for_each_property_of_node(np,prop){pr_info(" Property: %s\n",prop->name);}}/* 检查属性存在性 */if(of_property_read_bool(np,"optional-feature")){/* 启用可选功能 */}/* 安全读取属性(带默认值) */u32 value=1000;/* 默认值 */of_property_read_u32(np,"custom-delay",&value);

九、设备树绑定文档 (Binding) 规范

从 Linux 5.2 开始,内核引入 YAML 格式的绑定文档,用于自动化验证设备树。

9.1 YAML 绑定文档结构

# Documentation/devicetree/bindings/i2c/atmel,24c.yaml$schema:http://devicetree.org/meta-schemas/core.yaml#title:Atmel AT24 EEPROMmaintainers:-Bartosz Golaszewski <bgolaszewski@baylibre.com>description:|This binding describes the Atmel AT24 series of I2C EEPROMs.properties:compatible:enum:-atmel,24c00-atmel,24c01-atmel,24c02-atmel,24c04-atmel,24c08-atmel,24c16-atmel,24c32-atmel,24c64-atmel,24c128-atmel,24c256-atmel,24c512-atmel,24c1024reg:maxItems:1description:I2C device addresspagesize:$ref:/schemas/types.yaml#/definitions/uint32enum:[1,8,16,32,64,128,256]description:EEPROM page size for page writesize:$ref:/schemas/types.yaml#/definitions/uint32description:Total EEPROM size in bytesaddress-width:$ref:/schemas/types.yaml#/definitions/uint32enum:[8,16]default:8description:Number of address bitsrequired:-compatible-regadditionalProperties:falseexamples:-|i2c { #address-cells = <1>; #size-cells = <0>;eeprom@50{compatible = "atmel,24c256"; reg = <0x50>; pagesize = <64>;};};

9.2 验证绑定文档

# 使用 dt-schema 工具验证pip3installdtschema dt-validate Documentation/devicetree/bindings/# 验证具体设备树dt-validate board.dts-pDocumentation/devicetree/bindings/

十、实战:从零编写一个完整设备树

10.1 硬件假设

假设我们有一块基于 ARM Cortex-A7 的板卡,包含:

  • 1 个 UART (0x40020000)
  • 1 个 I2C (0x40030000),连接 AT24C256 EEPROM
  • 1 个 GPIO 控制的 LED

10.2 SoC 级定义 (soc.dtsi)

// arch/arm/boot/dts/vendor-soc.dtsi / { #address-cells = <1>; #size-cells = <1>; cpus { #address-cells = <1>; #size-cells = <0>; cpu@0 { device_type = "cpu"; compatible = "arm,cortex-a7"; reg = <0>; clock-frequency = <800000000>; /* 800MHz */ }; }; soc { compatible = "simple-bus"; #address-cells = <1>; #size-cells = <1>; ranges; uart0: serial@40020000 { compatible = "ns16550a"; reg = <0x40020000 0x1000>; interrupts = <GIC_SPI 24 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clk 12>; clock-names = "uartclk"; status = "disabled"; }; i2c0: i2c@40030000 { compatible = "vendor,i2c"; reg = <0x40030000 0x1000>; interrupts = <GIC_SPI 25 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clk 13>; clock-names = "i2cclk"; #address-cells = <1>; #size-cells = <0>; status = "disabled"; }; gpio0: gpio@40040000 { compatible = "vendor,gpio"; reg = <0x40040000 0x1000>; interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clk 14>; clock-names = "gpioclk"; gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>; status = "disabled"; }; }; };

10.3 板级定义 (board.dts)

// arch/arm/boot/dts/vendor-board.dts /dts-v1/; #include "vendor-soc.dtsi" / { model = "Vendor MyBoard"; compatible = "vendor,myboard", "vendor,myboard-v1"; chosen { stdout-path = "serial0:115200n8"; bootargs = "root=/dev/mmcblk0p2 rw rootfstype=ext4 console=ttyS0,115200n8"; }; memory@80000000 { device_type = "memory"; reg = <0x80000000 0x10000000>; /* 256MB */ }; leds { compatible = "gpio-leds"; led0: status { gpios = <&gpio0 5 GPIO_ACTIVE_LOW>; label = "status"; default-state = "on"; }; }; }; &uart0 { status = "okay"; }; &i2c0 { status = "okay"; clock-frequency = <400000>; eeprom@50 { compatible = "atmel,24c256"; reg = <0x50>; pagesize = <64>; }; }; &gpio0 { status = "okay"; };

10.4 驱动实现

// drivers/i2c/eeprom/my_eeprom.c#include<linux/module.h>#include<linux/i2c.h>#include<linux/of.h>#include<linux/of_device.h>structmy_eeprom_data{intpage_size;intsize;};staticconststructof_device_idmy_eeprom_of_match[]={{.compatible="atmel,24c256",.data=&(structmy_eeprom_data){64,32768}},{.compatible="atmel,24c512",.data=&(structmy_eeprom_data){128,65536}},{}};MODULE_DEVICE_TABLE(of,my_eeprom_of_match);staticintmy_eeprom_probe(structi2c_client*client){conststructmy_eeprom_data*data;structdevice_node*np=client->dev.of_node;u32 pagesize=0;/* 获取匹配数据 */data=of_device_get_match_data(&client->dev);if(!data)return-EINVAL;/* 读取设备树属性(覆盖默认值) */of_property_read_u32(np,"pagesize",&pagesize);if(pagesize==0)pagesize=data->page_size;dev_info(&client->dev,"EEPROM: size=%d, pagesize=%d\n",data->size,pagesize);return0;}staticintmy_eeprom_remove(structi2c_client*client){return0;}staticstructi2c_drivermy_eeprom_driver={.driver={.name="my_eeprom",.of_match_table=my_eeprom_of_match,},.probe=my_eeprom_probe,.remove=my_eeprom_remove,};module_i2c_driver(my_eeprom_driver);MODULE_LICENSE("GPL");MODULE_AUTHOR("Your Name");MODULE_DESCRIPTION("My EEPROM Driver");

十一、总结与最佳实践

11.1 设备树编写原则

  1. DRY 原则:公共定义放在.dtsi,板级差异放在.dts
  2. 兼容字符串:使用"vendor,model"格式,包含版本信息
  3. 状态控制:默认禁用外设 (status = "disabled"),板级启用
  4. 文档同步:修改设备树时同步更新绑定文档

11.2 常见错误排查

现象原因解决方法
驱动未加载compatible 不匹配检查字符串拼写
寄存器访问错误reg 地址错误核对 datasheet
中断不触发interrupts 配置错误检查中断号/触发方式
时钟不工作clocks 未指定确认时钟控制器和 ID
设备树编译失败语法错误使用dtc检查

转载自:https://blog.csdn.net/u014727709/article/details/162641210
欢迎 👍点赞✍评论⭐收藏,欢迎指正