嵌入式Linux设备树(DTS)深度解析——从硬件描述到驱动绑定
📅 2026/7/6 20:03:53
👁️ 阅读次数
📝 编程学习
文章目录
- 每日一句正能量
- 一、引言:为什么需要设备树
- 二、设备树架构与编译流程
- 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 |
| 设备树包含 | .dtsi | SoC 通用定义 | 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-502.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 |
/cpus | CPU 描述 | device_type = "cpu",reg,clock-frequency |
/memory | 内存布局 | device_type = "memory",reg |
/chosen | 启动参数 | bootargs,stdout-path |
/aliases | 别名定义 | serial0,i2c0等 |
/soc | SoC 外设 | 包含所有片上外设节点 |
四、属性详解与数据类型
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-controller | of_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 save8.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 设备树编写原则
- DRY 原则:公共定义放在
.dtsi,板级差异放在.dts - 兼容字符串:使用
"vendor,model"格式,包含版本信息 - 状态控制:默认禁用外设 (
status = "disabled"),板级启用 - 文档同步:修改设备树时同步更新绑定文档
11.2 常见错误排查
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 驱动未加载 | compatible 不匹配 | 检查字符串拼写 |
| 寄存器访问错误 | reg 地址错误 | 核对 datasheet |
| 中断不触发 | interrupts 配置错误 | 检查中断号/触发方式 |
| 时钟不工作 | clocks 未指定 | 确认时钟控制器和 ID |
| 设备树编译失败 | 语法错误 | 使用dtc检查 |
转载自:https://blog.csdn.net/u014727709/article/details/162641210
欢迎 👍点赞✍评论⭐收藏,欢迎指正
编程学习
技术分享
实战经验