1. 项目概述:为什么设备树是嵌入式Linux的“地图”
如果你玩过嵌入式Linux,尤其是像Microchip SAM9X60-Curiosity这样的ARM9开发板,那你一定绕不开一个东西——设备树。很多新手第一次接触它,感觉就像在看天书:一堆.dts、.dtsi文件,里面全是各种花括号和看不懂的属性。但我要告诉你,设备树其实就是你板子的“硬件地图”。没有这张地图,Linux内核就像一个盲人,根本不知道你的板子上挂了哪些硬件、这些硬件怎么连的、该用什么驱动去控制它们。
以前的内核,会把板子的硬件信息直接硬编码(Hardcode)在代码里。这意味着每换一块板子,哪怕只是改个LED灯的GPIO引脚,都得去重新编译内核,麻烦不说,还容易出错。设备树的出现,就是为了把硬件描述和内核代码解耦。它用一种结构化的文本(就是设备树源文件.dts)来描述硬件,内核启动时去读取并解析这个文件,就知道该怎么干活了。这就像给内核一本随板附送的说明书,而不是把说明书焊死在芯片里。
我这次拿Microchip的SAM9X60-Curiosity开发板来举例,原因很实在:这块板子用的是ARM9核心的AT91SAM9X60,在工控、物联网网关里很常见,资源丰富(像LCD、以太网、USB、SD卡都有),但它的设备树配置也相对典型和复杂,搞懂了它,你再去看其他类似架构的板子,比如NXP的i.MX系列或者ST的MP1系列,基本都能触类旁通。我们最终的目标,是让你能根据自己的硬件改动,独立修改和编译设备树,让内核正确识别你的定制板。
2. 设备树核心概念与SAM9X60硬件框架解析
在动手改代码之前,我们必须把几个核心概念和SAM9X60的硬件家底摸清楚。设备树不是玄学,它有一套自己的语法和逻辑。
2.1 设备树源文件结构与语法精要
设备树源文件主要有两种:.dts(Device Tree Source)和.dtsi(Device Tree Source Include)。.dts是描述具体某一块板子的顶层文件,而.dtsi则像是头文件,用来描述SoC(系统级芯片)共性的东西。对于SAM9X60来说,通常会有一个sama5d2.dtsi来描述SAM9X60这颗芯片内部的所有外设控制器(比如GPIO控制器、串口、I2C控制器等),然后我们的at91-sam9x60_curiosity.dts板级文件去引用它,并在此基础上添加板级特有的配置,比如哪个GPIO接了LED,哪个I2C总线上挂了EEPROM。
它的基本语法是树形结构:
/ { // 根节点 compatible = "microchip,sam9x60-curiosity", "microchip,sam9x60", "atmel,at91sam9"; model = "Microchip SAM9X60 Curiosity"; memory@20000000 { // 子节点,描述内存 device_type = "memory"; reg = <0x20000000 0x8000000>; // 起始地址0x20000000,大小128MB }; };- 节点(Node): 用花括号
{}定义,比如上面的memory@20000000。节点可以嵌套,形成父子关系。 - 属性(Property): 是键值对,比如
compatible = "microchip,sam9x60-curiosity"。这是最重要的属性之一,内核靠它来匹配驱动。 compatible属性: 这是设备的“身份证”。驱动代码里会声明自己支持哪些compatible字符串,内核在解析设备树时,会为每个设备节点寻找匹配的驱动。一个设备可以有多个compatible,按优先级排序,提供向后兼容。reg属性: 描述设备在内存或IO空间中的地址和范围。对于内存映射的设备(MMIO)来说,这是必须的。格式通常是<起始地址 长度>。status属性: 决定一个设备是否启用。常用值是"okay"(启用)和"disabled"(禁用)。你想临时关掉某个外设(比如某个不用的串口),改这里就行。
注意: 设备树里的地址通常是物理地址。但有些总线(如I2C、SPI)上的设备,
reg属性可能只是设备在该总线上的地址(如I2C的7位地址)。
2.2 SAM9X60-Curiosity开发板硬件资源盘点
要配置设备树,你得先知道板子上有什么。我们快速过一下SAM9X60-Curiosity的核心硬件,这决定了我们要在设备树里写什么:
- SoC: Microchip AT91SAM9X60。这是一颗ARM926EJ-S内核的芯片,主频600MHz,内置了DDR2控制器、大量外设。
- 内存: 板载128MB DDR2 SDRAM,映射在地址
0x20000000。 - 存储:
- QSPI Flash: 一片16MB的QSPI NOR Flash,用于存放U-Boot、设备树和内核。对应设备树中的
spi0控制器及挂载其上的flash@0节点。 - SD卡槽: 通过SDMMC0控制器连接,是主要的外部存储和文件系统载体。
- QSPI Flash: 一片16MB的QSPI NOR Flash,用于存放U-Boot、设备树和内核。对应设备树中的
- 网络: 一个10/100M以太网口,通过KSZ8081RNA PHY芯片连接至SoC的GMAC(以太网控制器)。
- 显示与触摸: 一个4.3寸LCD屏,接口为RGB565,连接SoC的LCD控制器。触摸屏通常是电阻式或电容式,通过ADC或I2C接口读取。
- 调试与通信:
- 调试串口: 通常是USART0(或DBGU),通过一个USB转串口芯片(如CP2102)连接到电脑,这是你最重要的调试窗口。
- 用户串口: 额外的USART接口,可用于连接其他设备。
- USB: 一个USB Host接口和一个USB Device接口。
- I2C: 至少一路I2C总线,可能连接着EEPROM、触摸控制器或环境传感器。
- SPI: 除了QSPI Flash占用的,可能还有额外的SPI接口。
- 用户IO: 用户按钮和LED灯,连接到特定的GPIO引脚。
这些硬件信息,一部分来自芯片数据手册(描述控制器本身),另一部分来自开发板原理图(描述控制器具体连到了哪个物理接口、哪个引脚)。原理图是你的终极依据。
3. 从零开始:解读与修改SAM9X60设备树
现在,我们进入实战环节。假设你已经拿到了Linux内核源码(比如从Microchip的GitHub仓库获取的linux-at91),并找到了设备树文件。它的路径通常在arch/arm/boot/dts/下。对于我们的板子,核心文件就是at91-sam9x60_curiosity.dts。
3.1 顶层设备树文件(.dts)结构剖析
我们打开at91-sam9x60_curiosity.dts,它的结构通常是这样的:
// SPDX-License-Identifier: GPL-2.0 /dts-v1/; #include "at91-sam9x60.dtsi" // 包含SoC级定义 #include "sam9x60-pinfunc.h" // 包含引脚复用定义 / { model = "Microchip SAM9X60 Curiosity"; compatible = "microchip,sam9x60-curiosity", "microchip,sam9x60", "atmel,at91sam9"; chosen { stdout-path = "serial0:115200n8"; // 指定内核控制台输出到串口0 }; memory@20000000 { device_type = "memory"; reg = <0x20000000 0x8000000>; // 128MB DDR2 }; // 板级特有的节点从这里开始添加,比如LED、按键、固定电压调节器等 leds { compatible = "gpio-leds"; led-blue { label = "blue"; gpios = <&pioA 10 GPIO_ACTIVE_HIGH>; // 使用PIOA的第10引脚,高电平点亮 linux,default-trigger = "heartbeat"; // 默认让它作为“心跳”指示灯闪烁 }; }; };关键点解读:
#include "at91-sam9x60.dtsi": 这行至关重要,它把SoC的所有内部资源(时钟、中断控制器、各种外设控制器节点)都引入了进来。这些节点在.dtsi里可能被标记为status = "disabled",需要在板级文件中按需启用。chosen节点: 这不是一个真实硬件,而是传递给内核的“运行时参数”。stdout-path指定了内核启动信息和控制台输出到哪个串口,波特率多少。如果启动时看不到串口打印,首先检查这里。memory节点: 必须和你的板载内存大小严格一致。如果换了内存芯片,这里一定要改。leds节点: 这是一个典型的、板级特有的“平台设备”节点。它通过compatible = "gpio-leds"匹配内核中的GPIO LED通用驱动。gpios属性引用了pioA(GPIO控制器A)的第10号引脚,并指定了有效电平。
3.2 关键外设节点配置详解
我们挑几个最常用、也最容易出问题的外设,看看在设备树里具体怎么配。
3.2.1 串口(UART)配置
串口是调试的生命线。在.dtsi文件中,USART0可能已经定义好了,但我们需要在板级文件中确保它被启用,并配置正确的引脚。
// 在 at91-sam9x60_curiosity.dts 中 &uart0 { // “&”符号表示引用已有的uart0节点,并对其进行覆盖/添加属性 pinctrl-names = "default"; pinctrl-0 = <&pinctrl_uart0_default>; // 指定引脚复用配置 status = "okay"; // 启用该设备 };这里引入了两个新概念:
pinctrl(引脚控制器): 这是现代Linux驱动中管理引脚复用的核心。一个引脚可能既可以做UART的TX,又可以做SPI的SCK。pinctrl-0指定了当前设备要使用哪一组预定义的引脚配置。这组配置pinctrl_uart0_default通常在同一个dts文件的末尾或专门的引脚配置头文件(如sam9x60-pinfunc.h)中定义。status: 从disabled改为okay,是启用一个设备最常见、最关键的一步。
3.2.2 I2C总线与设备添加
假设我们要在I2C0总线上添加一个温度传感器(例如,LM75,地址0x48)。
&i2c0 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c0_default>; status = "okay"; clock-frequency = <100000>; // 指定I2C总线速度为100kHz // 在i2c0节点下添加子节点,表示挂载的设备 temperature-sensor@48 { compatible = "nxp,lm75"; // 匹配内核中的LM75驱动 reg = <0x48>; // I2C设备地址 // 可以添加其他属性,例如中断引脚(如果传感器支持) // interrupts-extended = <&pioA 12 IRQ_TYPE_EDGE_FALLING>; }; };实操心得:
clock-frequency属性很重要,一些I2C设备对速度有要求,必须匹配。- 子节点的名字
temperature-sensor@48主要是给人看的,内核不关心。关键是compatible和reg。 - 如何知道设备的
compatible字符串?最好的方法是查阅内核文档Documentation/devicetree/bindings/。例如,Documentation/devicetree/bindings/hwmon/lm75.txt就会告诉你应该用"nxp,lm75"。
3.2.3 以太网(Ethernet)与PHY配置
SAM9X60的以太网(GMAC)需要外接PHY芯片。配置涉及两个部分:MAC控制器和PHY。
&macb0 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_macb0_default>; status = "okay"; phy-mode = "rmii"; // 或 "mii",取决于硬件连接。SAM9X60 Curiosity通常用RMII。 // 指定PHY,这里假设PHY在MDIO总线的地址是1 ethernet-phy@1 { reg = <0x1>; // PHY地址,看原理图确定 // 一些PHY需要额外的复位或配置,可以在这里指定 // reset-gpios = <&pioA 14 GPIO_ACTIVE_LOW>; // reset-assert-us = <1000>; // reset-deassert-us = <1000>; }; };避坑指南:
phy-mode必须和你的硬件连接方式(MII/RMII/RGMII)完全一致,否则网络不通。- PHY的地址(
reg)由PHY芯片上的引脚(如RXER, LED2)的上拉/下拉电阻决定,必须查阅原理图确认。地址不对是导致eth0: Cannot find PHY!错误的常见原因。
3.2.4 SD/MMC卡槽配置
SD卡是常见的存储和启动介质。
&sdmmc0 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_sdmmc0_default>; status = "okay"; bus-width = <4>; // 4位数据线 cd-gpios = <&pioA 25 GPIO_ACTIVE_LOW>; // 卡检测引脚,低电平表示有卡插入 // 如果需要写保护检测 // wp-gpios = <&pioA 26 GPIO_ACTIVE_HIGH>; disable-wp; // 如果硬件没有写保护引脚,建议禁用WP功能 };提示:
cd-gpios的配置非常关键。如果配置错误(比如电平反了),系统会一直认为有卡或无卡,导致无法挂载。务必用万用表或逻辑分析仪确认插入SD卡时该引脚的实际电平。
3.3 引脚复用(Pinctrl)配置实战
引脚复用是嵌入式Linux设备树配置中最繁琐但也最核心的一环。它决定了芯片的物理引脚在当前系统中扮演什么角色。
在SAM9X60的DTS中,你会看到一个大段的pinctrl节点,里面定义了多组配置。例如:
pinctrl { // ... uart0_default: uart0_default { pinmux = <PIN_PA26__URXD0>, // 引脚PA26复用为URXD0 <PIN_PA27__UTXD0>; // 引脚PA27复用为UTXD0 bias-disable; // 禁止内部上拉/下拉 }; i2c0_default: i2c0_default { pinmux = <PIN_PA4__TWD0>, // PA4复用为TWD0 (SDA) <PIN_PA5__TWCK0>; // PA5复用为TWCK0 (SCL) bias-disable; // 对于I2C,通常需要启用内部上拉,但具体看板子设计 // bias-pull-up; }; // ... };修改引脚复用的步骤:
- 查数据手册: 找到芯片的引脚功能复用表,确认你想要的引脚(例如PA30)支持哪些功能(例如,可以是SPI0_MISO,也可以是PWM0)。
- 查原理图: 确认该引脚在板子上实际连接到了什么外设。
- 修改DTS:
- 如果要更改一个已有外设的引脚,找到对应的
pinctrl_xxx_default组,修改pinmux中的宏。这些宏(如PIN_PA26__URXD0)通常在sam9x60-pinfunc.h头文件中定义。 - 如果要用一个全新的引脚功能组合,需要新建一组
pinctrl配置。
- 如果要更改一个已有外设的引脚,找到对应的
- 更新外设节点: 确保外设节点(如
&uart0)的pinctrl-0属性指向你修改或新建的配置组。
一个真实案例: 假设SAM9X60 Curiosity板上的用户LED原本接在PA10,但我的定制板改到了PB5。
- 第一步,确认PB5可以作为普通GPIO(功能B)。
- 第二步,修改LED的节点:
leds { compatible = "gpio-leds"; led-blue { label = "blue"; // gpios = <&pioA 10 GPIO_ACTIVE_HIGH>; // 原配置 gpios = <&pioB 5 GPIO_ACTIVE_HIGH>; // 新配置 linux,default-trigger = "heartbeat"; }; }; - 第三步,通常不需要修改pinctrl,因为GPIO功能通常是引脚的默认或备用功能,且GPIO驱动本身会处理引脚方向。但对于一些特殊功能(如PWM、外设片选),pinctrl配置是必须的。
4. 编译、调试与验证设备树
配置写好了,不编译成二进制格式(.dtb,Device Tree Blob),内核是读不懂的。编译和调试的过程,也是验证配置是否正确的最重要环节。
4.1 设备树的编译流程
在内核源码目录下,有专门的Makefile来编译设备树。假设你已经在linux-at91目录下配置好交叉编译工具链(如arm-linux-gnueabi-)。
生成
.dtb文件:# 在内核源码根目录执行 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- at91-sam9x60_curiosity.dtb这条命令会编译出
arch/arm/boot/dts/at91-sam9x60_curiosity.dtb。一键编译内核镜像(zImage)和设备树:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- zImage dtbsdtbs目标会编译所有在arch/arm/boot/dts/Makefile中定义的设备树文件。
编译依赖: 编译设备树需要设备树编译器(dtc)。如果你在编译内核,它通常会自动被编译。你也可以单独安装:sudo apt-get install device-tree-compiler。
4.2 设备树调试技巧与问题排查
设备树配置错误,会导致内核启动失败、外设无法识别或工作异常。掌握调试方法至关重要。
4.2.1 查看运行时设备树
系统启动后,你可以查看内核最终“看到”的设备树,这是最直接的调试手段。
# 将当前系统的设备树结构导出为文本格式(DTS) cat /proc/device-tree/ # 列出根节点下的内容(是目录结构) # 更常用的方法是使用 dtc 工具反编译 dtc -I fs -O dts /sys/firmware/devicetree/base > current.dts导出的current.dts文件包含了所有节点和属性的当前值,你可以和你编译的源文件进行对比,看看是否一致。
4.2.2 使用内核日志(dmesg)
内核在启动和驱动加载过程中,会打印大量关于设备树的信息。
- 搜索你的设备:
dmesg | grep -i “i2c0”或dmesg | grep -i “lm75”。看驱动是否成功匹配(probe),以及是否有错误信息。 - 常见错误信息:
OF: **ERROR** (phandle) in /soc/i2c@...: 设备树语法错误,比如引用了一个不存在的节点标签(&xxx)。[drm] Cannot find any crtc or sizes: 可能是显示相关的设备树配置(如LCD时序)错误。atmel_usba_udc: no vbus pin: USB设备控制器缺少必要的VBUS检测引脚定义。
4.2.3 在U-Boot中加载和测试设备树
在系统最终启动前,你可以在U-Boot阶段加载并预览设备树,这是一个安全的调试方式。
# 假设你把新的.dtb文件放在SD卡或tftp服务器上 # 1. 加载dtb到内存 fatload mmc 0:1 0x21000000 at91-sam9x60_curiosity.dtb # 或 tftp 0x21000000 at91-sam9x60_curiosity.dtb # 2. 用fdt命令查看和修改(U-Boot需要开启CONFIG_OF_LIBFDT) fdt addr 0x21000000 # 设置当前操作的dtb地址 fdt print /soc/i2c@f8010000 # 查看i2c0节点 fdt set /soc/i2c@f8010000 status "okay" # 临时启用i2c0(仅内存中修改) # 3. 用修改后的dtb启动内核 bootz 0x22000000 - 0x21000000 # zImage地址 - dtb地址4.3 常见问题速查与解决方案
这里整理了几个我踩过坑的典型问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 内核启动卡住,无串口输出 | 1. 串口引脚复用错误。 2. stdout-path指定的串口不对。3. 波特率不匹配。 | 1. 检查&uartX节点的pinctrl-0指向的配置组,确认pinmux宏正确。2. 确认 chosen节点的stdout-path值(如"serial0:115200n8")与硬件连接的串口一致。3. 确保PC端串口工具的波特率设置为115200。 |
ifconfig看不到eth0网卡 | 1. PHY地址 (reg) 错误。2. phy-mode(RMII/MII) 错误。3. 时钟或复位引脚未配置。 | 1.dmesg | grep -A5 -B5 phy查看PHY探测日志,确认地址。2. 对照原理图,确认MAC和PHY之间的接口类型,修改 phy-mode。3. 检查设备树中PHY的 reset-gpios等属性,确保PHY能正常复位。 |
| SD卡无法识别或挂载 | 1. 卡检测(CD)引脚电平配置反了。 2. 电源或时钟问题。 3. 总线宽度( bus-width)不匹配。 | 1. 用万用表测SD卡座CD引脚在插卡/不插卡时的电平,修正cd-gpios的<... GPIO_ACTIVE_LOW/HIGH>。2. dmesg | grep mmc看错误信息。有些板子需要配置vmmc-supply来提供卡电源。3. 确认是4位还是1位SD总线,修改 bus-width。 |
| I2C设备探测失败 | 1. I2C设备地址错误。 2. 总线上无设备或设备损坏。 3. 上拉电阻未接或I2C引脚被其他功能占用。 | 1. 用i2cdetect -y 0命令扫描I2C总线0,看目标地址(如0x48)是否出现。2. 检查硬件连接,用示波器看SCL/SDA波形。 3. 确认I2C引脚复用的pinctrl配置正确,且没有被其他驱动占用。 |
| 添加的自定义节点,驱动读不到 | 1. 节点位置不对,不在内核扫描的范围内。 2. compatible字符串与驱动不匹配。3. 驱动未编译进内核或模块未加载。 | 1. 确保节点放在根/或某个总线(如&i2c0)节点下。2. 核对内核源码中驱动的 of_device_id表里的字符串。3. 检查内核 .config,确认对应驱动已启用 (CONFIG_XXX=y/m)。 |
5. 进阶:为定制硬件创建新的设备树
当你基于SAM9X60设计了自己的板子,你就需要从头创建一个新的设备树文件。这听起来 daunting,但其实有章可循。
5.1 创建新DTS文件的步骤
- 复制最接近的模板: 在
arch/arm/boot/dts/目录下,找一个硬件最相似的现有dts文件(比如at91-sam9x60_curiosity.dts)作为模板,复制并重命名,例如at91-sam9x60_myboard.dts。 - 修改顶层信息:
注意/ { model = "My Company, My SAM9X60 Board"; compatible = "mycompany,my-sam9x60-board", "microchip,sam9x60", "atmel,at91sam9"; // ... 保留或修改 memory, chosen 等节点 };compatible字符串,第一个应该是你板子独有的ID。 - 根据原理图,逐项修改:
- 内存: 修改
memory节点的reg属性。 - LED和按键: 修改或重写
gpio-keys和gpio-leds节点,更新gpios属性。 - 外设启用/禁用: 用
&uart1 { status = "disabled"; };的方式禁用你板子上没有的外设控制器;启用并正确配置你有的外设。 - 引脚复用: 这是工作量最大的部分。你需要根据原理图,为每个使用的外设创建或修改对应的
pinctrl_xxx_default组。强烈建议在Excel或文本文件中先做好引脚分配表,避免冲突。
- 内存: 修改
- 更新Makefile: 编辑
arch/arm/boot/dts/Makefile,在dtb-$(CONFIG_SOC_SAM9X60)部分添加你的新dtb目标,例如:
这样,执行dtb-$(CONFIG_SOC_SAM9X60) += \ at91-sam9x60_curiosity.dtb \ at91-sam9x60_myboard.dtbmake dtbs时就会自动编译你的板子设备树。
5.2 设备树与驱动开发的联动
当你为自己设计的特殊硬件(比如一块自定义的FPGA桥接芯片)编写Linux驱动时,设备树是驱动获取硬件信息的主要途径。
在驱动代码中,你会这样使用设备树:
// 在驱动探测函数中 static int my_driver_probe(struct platform_device *pdev) { struct device_node *np = pdev->dev.of_node; const char *string_prop; u32 reg_data[2]; int irq_num; // 1. 获取字符串属性 of_property_read_string(np, "my-custom-string", &string_prop); // 2. 获取寄存器地址和长度 of_address_to_resource(np, 0, &res); // 获取第一个 reg 区域 // 3. 获取中断号 irq_num = platform_get_irq(pdev, 0); // 4. 获取GPIO struct gpio_desc *my_gpio; my_gpio = devm_gpiod_get(&pdev->dev, "enable", GPIOD_OUT_LOW); // ... 使用这些资源初始化硬件 }对应的设备树节点可能是:
my_custom_device@f0000000 { compatible = "mycompany,my-custom-device"; reg = <0xf0000000 0x1000>; // 驱动通过 of_address_to_resource 获取 interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>; // 驱动通过 platform_get_irq 获取 enable-gpios = <&pioA 15 GPIO_ACTIVE_HIGH>; // 驱动通过 devm_gpiod_get 获取 my-custom-string = "hello-from-dts"; // 驱动通过 of_property_read_string 获取 status = "okay"; };这种驱动与设备树的解耦,使得同一份驱动代码可以用于不同硬件平台,只需修改设备树即可,极大地提高了代码的复用性和可维护性。
设备树的配置,是一个从“照猫画虎”到“心中有图”的过程。一开始你可能会觉得它繁琐,但当你成功让内核识别出你亲手焊接的硬件时,那种成就感是无与伦比的。多看、多改、多编译、多测试,遇到问题善用dmesg和反编译工具,你很快就能掌握这张嵌入式Linux的“硬件地图”。