嵌入式Linux设备树实战:从原理到SAM9X60定制开发

📅 2026/6/22 11:20:23
嵌入式Linux设备树实战:从原理到SAM9X60定制开发
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 设备树源文件结构与语法精要设备树源文件主要有两种.dtsDevice Tree Source和.dtsiDevice 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; memory20000000 { // 子节点描述内存 device_type memory; reg 0x20000000 0x8000000; // 起始地址0x20000000大小128MB }; };节点Node 用花括号{}定义比如上面的memory20000000。节点可以嵌套形成父子关系。属性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控制器及挂载其上的flash0节点。SD卡槽: 通过SDMMC0控制器连接是主要的外部存储和文件系统载体。网络: 一个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 }; memory20000000 { 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属性引用了pioAGPIO控制器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-sensor48 { compatible nxp,lm75; // 匹配内核中的LM75驱动 reg 0x48; // I2C设备地址 // 可以添加其他属性例如中断引脚如果传感器支持 // interrupts-extended pioA 12 IRQ_TYPE_EDGE_FALLING; }; };实操心得clock-frequency属性很重要一些I2C设备对速度有要求必须匹配。子节点的名字temperature-sensor48主要是给人看的内核不关心。关键是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-phy1 { 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. 编译、调试与验证设备树配置写好了不编译成二进制格式.dtbDevice Tree Blob内核是读不懂的。编译和调试的过程也是验证配置是否正确的最重要环节。4.1 设备树的编译流程在内核源码目录下有专门的Makefile来编译设备树。假设你已经在linux-at91目录下配置好交叉编译工具链如arm-linux-gnueabi-。生成.dtb文件# 在内核源码根目录执行 make ARCHarm CROSS_COMPILEarm-linux-gnueabi- at91-sam9x60_curiosity.dtb这条命令会编译出arch/arm/boot/dts/at91-sam9x60_curiosity.dtb。一键编译内核镜像zImage和设备树make ARCHarm CROSS_COMPILEarm-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/i2cf8010000 # 查看i2c0节点 fdt set /soc/i2cf8010000 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_XXXy/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.dtb这样执行make 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_devicef0000000 { 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的“硬件地图”。