嵌入式Linux设备树配置实战:以SAM9X60-Curiosity开发板为例

嵌入式Linux设备树配置实战:以SAM9X60-Curiosity开发板为例

1. 项目概述与核心价值

最近在调试一块Microchip的SAM9X60-Curiosity开发板,想把一个定制的传感器模块挂上去。过程里最绕不开的,就是修改和编译设备树(Device Tree)。这东西对于刚接触嵌入式Linux的开发者来说,就像一堵无形的墙,文档散落在各处,概念抽象,改错一个地方系统可能就启动不了。网上很多教程要么太泛泛而谈,要么是针对特定平台语焉不详。所以,我想结合手头这块具体的板子,把设备树从配置、修改到编译、测试的完整流程,掰开揉碎了讲清楚。如果你正在使用基于ARM架构的嵌入式Linux开发板,尤其是Microchip的AT91系列或者类似需要手动配置设备树的平台,这篇指南应该能帮你避开我踩过的那些坑。

简单说,设备树就是一个描述硬件的数据结构。在以前,内核里要把每块板子的硬件信息(比如内存地址、中断号、外设连接)都硬编码进去,换块板子就得重新编译内核,非常麻烦。设备树解决了这个问题,它把硬件描述从内核代码中分离出来,变成一个可以单独编辑、编译的文本文件(.dts或.dtsi)。内核启动时,Bootloader(如U-Boot)会把这个编译好的二进制文件(.dtb)加载到内存并传递给内核,内核根据这个“地图”来识别和驱动硬件。对于SAM9X60-Curiosity这块板子,我们做驱动开发或者外设移植,核心工作就是正确地修改和生成这份“地图”。

2. 设备树基础与SAM9X60硬件框架解析

2.1 设备树的核心语法与结构

设备树源文件(.dts)是一种层级化的数据结构,语法类似JSON。理解几个基本概念是关键:

  1. 节点(Node):描述一个设备或总线,用花括号{}定义。最顶层是根节点/
  2. 属性(Property):描述节点的具体信息,是键值对。比如compatible = “microchip,sam9x60-curiosity”是最重要的属性之一,内核靠它来匹配对应的驱动程序。
  3. 兼容性(compatible):这是设备的“身份证”。值是一个字符串列表,优先匹配最具体的。例如,一个设备的compatible“microchip,sam9x60-gpio”, “microchip,at91sam9x5-gpio”, “atmel,at91rm9200-gpio”,内核会按顺序寻找匹配的驱动。
  4. 地址(reg):描述设备寄存器在总线地址空间中的位置和大小。通常包含多个<地址 长度>对。理解它需要结合#address-cells#size-cells这两个属性,它们定义了这个节点的子节点中reg属性的格式。

一个最简单的设备树片段看起来是这样的:

/ { compatible = “microchip,sam9x60-curiosity”, “microchip,sam9x60”; #address-cells = <1>; #size-cells = <1>; soc { compatible = “simple-bus”; #address-cells = <1>; #size-cells = <1>; ranges; usart0: serial@f801c000 { compatible = “microchip,sam9x60-usart”, “atmel,at91sam9260-usart”; reg = <0xf801c000 0x400>; interrupts = <23 IRQ_TYPE_LEVEL_HIGH 7>; pinctrl-names = “default”; pinctrl-0 = <&pinctrl_usart0_default>; status = “okay”; }; }; };

这段代码描述了一个串口设备usart0,它的寄存器起始地址是0xf801c000,长度0x400,使用的中断号是23。pinctrl-0引用了另一个节点(&pinctrl_usart0_default)来配置该串口所用到的具体引脚功能。

2.2 SAM9X60-Curiosity开发板硬件特点

SAM9X60是一款基于ARM926EJ-S内核的微处理器,运行频率可达600MHz。Curiosity开发板是其评估板,集成了丰富的外设接口。在修改设备树前,必须清楚板载资源:

  • 核心与内存:ARM926EJ-S核心,板载128MB DDR2 SDRAM。
  • 存储:128MB的QSPI Flash,一个MicroSD卡槽。
  • 网络:一个10/100 Mbps以太网控制器(GMAC)。
  • USB:一个USB Host和一个USB Device接口。
  • 串口:多个USART/UART,其中USART0通常作为调试串口。
  • 扩展接口:包括Arduino兼容接口和 mikroBUS 接口,方便连接各种外设模块。

我们的设备树修改,主要就是围绕这些外设,以及我们想要添加的额外模块(比如通过mikroBUS连接的传感器)来展开。关键是要查阅两个文档:SAM9X60的数据手册Curiosity开发板的原理图。数据手册告诉你处理器每个外设模块的寄存器基地址、中断号;原理图告诉你具体的外设连接到了处理器的哪个引脚。这是所有修改工作的基石。

注意:在修改任何内容前,务必备份原始的.dts文件。最好使用版本控制工具(如git)来管理你的设备树修改,这样你可以清晰地追踪每一次变更。

3. 设备树配置的完整实操流程

3.1 环境准备与源码获取

首先,你需要一个可以编译设备树的Linux环境。通常是在你的开发主机(PC)上进行交叉编译。

  1. 安装交叉编译工具链:对于ARM架构的SAM9X60,你需要ARM Linux GNU工具链。可以从Linaro或ARM官网下载,或者使用包管理器安装。

    # 例如,在Ubuntu上安装gcc-arm-linux-gnueabi sudo apt-get update sudo apt-get install gcc-arm-linux-gnueabi # 验证安装 arm-linux-gnueabi-gcc --version
  2. 获取Linux内核源码与设备树:你需要获取针对SAM9X60移植好的Linux内核源码。最直接的方式是从Microchip的官方GitHub仓库克隆。

    git clone https://github.com/linux4sam/linux-at91.git cd linux-at91 # 切换到与你的板卡系统镜像匹配的分支,例如 linux-5.10-at91 git checkout linux-5.10-at91

    设备树源文件通常位于arch/arm/boot/dts/目录下。对于SAM9X60-Curiosity,核心文件是at91-sam9x60_curiosity.dts

  3. 理解设备树的包含关系.dts文件不是孤立的。它通过#include语句包含其他.dtsi(设备树源包含文件)文件。这是一种层级化设计:

    • at91-sam9x60.dtsi:描述SAM9X60芯片共性的硬件信息(如CPU、内存控制器、所有外设模块的节点框架)。
    • at91-sam9x60_curiosity.dts:描述Curiosity这块特定板子的硬件信息(如哪些外设被启用、引脚复用配置、板载外设如EEPROM的地址等)。 我们大部分针对具体板卡的修改,都是在.dts文件中进行的。

3.2 修改设备树:以添加I2C传感器为例

假设我们要通过mikroBUS接口(其背后是I2C1总线)连接一个温湿度传感器(例如SHT30)。

  1. 第一步:确认硬件连接。查看Curiosity原理图,找到mikroBUS接口的I2C信号线连接到了处理器的哪个I2C控制器和哪个引脚。假设原理图显示连接到了TWD1TWCK1,即I2C1总线。

  2. 第二步:在设备树中启用并配置I2C1控制器。打开at91-sam9x60_curiosity.dts,查找i2c1相关的节点。它可能已经在at91-sam9x60.dtsi中定义,但在.dts文件中状态是“disabled”

    /* 在 at91-sam9x60.dtsi 中可能已有定义 */ i2c1: i2c@f8018000 { compatible = “microchip,sam9x60-i2c”; reg = <0xf8018000 0x400>; interrupts = <25 IRQ_TYPE_LEVEL_HIGH 7>; clocks = <&pmc PMC_TYPE_PERIPHERAL 25>; pinctrl-names = “default”; pinctrl-0 = <&pinctrl_i2c1_default>; status = “disabled”; };

    我们需要在板级.dts文件中将其状态改为“okay”,并确保引脚复用配置正确。

    /* 在 at91-sam9x60_curiosity.dts 的合适位置(例如在 &i2c1 节点处) */ &i2c1 { pinctrl-names = “default”; pinctrl-0 = <&pinctrl_i2c1_default>; status = “okay”; /* 可以在这里直接添加子设备节点,也可以保持为空,由内核动态探测 */ };

    同时,要检查pinctrl_i2c1_default这个引脚配置组是否已经在pinctrl节点中正确定义,且与原理图匹配。

  3. 第三步:添加具体的传感器设备节点。在&i2c1节点内部,我们可以静态添加传感器设备。

    &i2c1 { status = “okay”; clock-frequency = <100000>; /* 标准模式,100kHz */ sht30: temperature-sensor@44 { compatible = “sensirion,sht30”; reg = <0x44>; /* 其他可选属性,例如中断引脚配置 */ // interrupts-extended = <&pioA PIN_PBXX GPIO_ACTIVE_LOW>; }; };
    • sht30:是一个标签,方便其他地方引用。
    • temperature-sensor@44:节点名,@后跟设备的I2C从地址(十六进制0x44)。
    • compatible:必须与内核中SHT30驱动的of_device_id表里的字符串匹配。你需要确认内核是否编译了此驱动,或者以模块形式提供。
    • reg:设备的I2C从地址,即<0x44>

3.3 设备树的编译与生成

修改保存后,需要将其编译成二进制设备树Blob(.dtb)。

  1. 配置内核:确保内核配置包含了设备树支持以及你所用外设的驱动。

    # 在linux-at91源码根目录 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- at91_dt_defconfig # 如果需要,可以通过menuconfig进一步配置 # make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig

    menuconfig中,确保:

    • CONFIG_OF=y(设备树支持)
    • 对应的I2C驱动、SHT30传感器驱动被启用(=y)或编译为模块(=m)。
  2. 编译设备树

    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- dtbs

    这条命令会编译arch/arm/boot/dts/目录下所有.dts文件对应的.dtb。编译成功后,你需要的at91-sam9x60_curiosity.dtb文件会生成在arch/arm/boot/dts/目录下。

  3. 单独编译某个设备树(如果只想测试当前板子):

    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- at91-sam9x60_curiosity.dtb

3.4 设备树的部署与测试

将生成的.dtb文件部署到开发板上有多种方式,取决于你的Bootloader和启动方式。

方式一:通过U-Boot和TFTP加载(适用于调试阶段)

  1. at91-sam9x60_curiosity.dtb复制到TFTP服务器目录。
  2. 启动开发板,在U-Boot命令行中断启动过程。
  3. 使用U-Boot命令加载并启动:
    # 设置服务器IP和板子IP setenv serverip 192.168.1.100 setenv ipaddr 192.168.1.50 # 通过TFTP加载设备树文件到内存地址 0x21000000 tftp 0x21000000 at91-sam9x60_curiosity.dtb # 启动内核,并指定设备树地址 bootz 0x22000000 - 0x21000000
    这种方式非常灵活,无需刷写存储,适合快速迭代测试。

方式二:替换启动介质中的dtb文件(适用于固化)

  1. 如果系统从MicroSD卡启动,通常卡的第一个分区(FAT格式)存放着内核镜像(zImage)和设备树文件(at91-sam9x60_curiosity.dtb)。
  2. 将SD卡挂载到电脑,用新编译的.dtb文件覆盖原有的文件即可。

启动后验证: 系统启动后,登录开发板,通过以下命令检查设备树是否生效:

# 查看I2C1总线是否识别到设备 ls /sys/bus/i2c/devices/ # 应该能看到类似 “1-0044” 的目录,对应I2C1总线上的地址0x44设备 # 查看传感器设备是否成功绑定驱动 ls /sys/bus/i2c/devices/1-0044/ # 如果驱动加载成功,这里会有 driver -> ../../../bus/i2c/drivers/sht30 的链接 # 查看设备树中I2C1节点的状态 cat /proc/device-tree/soc/i2c@f8018000/status # 应该显示 “okay” # 更直观地,查看完整的设备树 apt-get install device-tree-compiler # 安装dtc工具 dtc -I fs /sys/firmware/devicetree/base -O dts | less # 在输出中搜索 “sht30” 或 “i2c1”,确认你的修改已存在

4. 常见问题排查与调试技巧实录

设备树配置出错,轻则外设无法工作,重则系统无法启动。以下是我在实际项目中积累的排查经验。

4.1 系统无法启动或外设不工作

  1. 检查语法错误:设备树编译器dtc非常严格。在编译前,可以用它做预检查。

    dtc -O dts -I dtb -o test.dts arch/arm/boot/dts/at91-sam9x60_curiosity.dtb 2>&1 # 或者直接编译dts看错误 dtc -I dts -O dtb -o test.dtb arch/arm/boot/dts/at91-sam9x60_curiosity.dts

    常见的语法错误包括缺少分号、括号不匹配、节点名格式错误等。

  2. 检查内核启动日志:这是最重要的调试信息源。通过串口控制台,查看内核启动早期的输出。

    [ 0.000000] OF: fdt: Ignoring memory range 0x0 - 0x100000 [ 0.000000] Machine model: Microchip SAM9X60-Curiosity [ 0.000000] …… [ 1.234567] i2c /dev entries driver [ 1.345678] at91_i2c f8018000.i2c: version 0x311 [ 1.345789] at91_i2c f8018000.i2c: using 16 bit data width [ 1.456789] at91_i2c f8018000.i2c: bus 1 at 100 kHz [ 1.567890] sht30 1-0044: chip found

    重点关注:

    • Machine model是否正确识别了你的板子。
    • 你的外设控制器(如at91_i2c f8018000.i2c)是否被成功探测和初始化。
    • 你的具体设备(如sht30 1-0044)是否被成功发现并绑定驱动。如果看到probe failed或类似的错误,就要根据错误码去查原因。
  3. 检查引脚复用冲突:这是最隐蔽的问题之一。SAM9X60的每个引脚功能都是复用的。如果你为I2C1配置了引脚,但同一个引脚组(PIO)里的另一个引脚被其他地方(比如另一个已启用的外设或GPIO)配置成了不同功能,就会冲突。务必仔细核对原理图和pinctrl配置。一个技巧是,在系统启动后,查看引脚复用状态:

    cat /sys/kernel/debug/pinctrl/pinctrl-handles # 或者查看具体的pinctrl状态 cat /sys/kernel/debug/pinctrl/*/pinmux-pins

4.2 设备树与驱动匹配问题

  1. compatible字符串不匹配:这是设备无法绑定驱动的首要原因。确保设备树节点中的compatible属性值,与内核驱动源码中of_device_id结构体数组里定义的字符串完全一致,包括大小写和标点。去内核源码drivers/目录下搜索你的驱动,核对字符串。

    // 例如在 drivers/iio/humidity/sht30.c 中 static const struct of_device_id sht30_dt_ids[] = { { .compatible = “sensirion,sht30” }, { } };
  2. 驱动未编译进内核:即使compatible匹配,如果驱动被配置为模块(=m)而模块未自动加载,或者驱动直接被禁用(=n),设备也不会工作。检查内核配置文件(.config):

    grep CONFIG_SHT30 .config # 或者在内核源码目录下 make ARCH=arm menuconfig # 然后搜索 SHT30

    如果驱动是模块,启动后需要手动modprobe sht30

4.3 设备树覆盖与动态修改

对于生产环境,有时我们希望在基础设备树之上进行小范围修改,而不重新编译整个DTB。U-Boot支持设备树覆盖(Device Tree Overlay)

  1. 创建覆盖文件(my-overlay.dts):
    /dts-v1/; /plugin/; / { fragment@0 { target = <&i2c1>; __overlay__ { new_device@55 { compatible = “vendor,new-device”; reg = <0x55>; }; }; }; };
  2. 编译覆盖文件
    dtc -O dtb -o my-overlay.dtbo -@ my-overlay.dts
  3. 在U-Boot中应用
    # 加载基础dtb和overlay dtbo tftp 0x21000000 at91-sam9x60_curiosity.dtb tftp 0x21100000 my-overlay.dtbo # 应用覆盖 fdt addr 0x21000000 fdt resize 8192 # 预留足够空间 fdt apply 0x21100000 # 然后正常启动内核 bootz 0x22000000 - 0x21000000
    这种方式非常适合在产线为不同配置的硬件加载不同的外设配置。

4.4 实用调试命令速查表

下表总结了在开发板上最常用的设备树相关调试命令:

命令作用示例/说明
ls /proc/device-tree/查看根节点下的所有属性/节点这是查看运行时设备树结构的起点
cat /proc/device-tree/model查看板卡型号应与设备树中根节点的model属性一致
hexdump /proc/device-tree/soc/i2c@f8018000/reg查看某个节点的reg属性原始值输出的是二进制数据,需结合#address-cells解析
dtc -I fs -O dts /sys/firmware/devicetree/base将运行时设备树导出为dts文本最强大的调试工具,可以查看内核实际使用的、经过所有处理后的完整设备树
find /sys/firmware/devicetree/base -name “*compatible*”查找所有包含compatible字符串的文件可以快速定位设备节点在sysfs中的路径
echo 1 > /sys/class/gpio/export导出GPIO进行测试用于验证引脚复用是否成功,如果导出失败,可能该引脚已被其他驱动占用
dmesg | grep -i “error|fail|probe”过滤内核日志中的错误和探测信息快速定位启动过程中的外设初始化问题

设备树的配置是一个需要耐心和细致的工作,它连接着硬件原理图和软件驱动。我的经验是,每次只做一处小的修改,然后编译、测试、确认,逐步推进。养成查阅芯片数据手册、板卡原理图和内核驱动源码的习惯,这三份文档交叉对照,能解决90%以上的配置问题。当系统按照你的“地图”正确识别出所有硬件时,那种成就感是实实在在的。