ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RK3568设备树实战:从dts编写到U-Boot/Petalinux调试

RK3568设备树实战:从dts编写到U-Boot/Petalinux调试 很多刚接触嵌入式Linux的人会把设备树源文件dts当成一种“配置语法”去背结果遇到真实板子依旧无从下手。我最初写设备树时也这样手里是一块自主设计的瑞芯微RK3568底板原理图上明明接了触摸屏、GPIO按键和I2C风扇厂商BSP里却偏偏没有对应dts文件Linux起来之后什么都认不到。后来把dts、dtsi、dtb之间的逻辑理顺再把几类核心属性弄明白才意识到设备树不过是“用数据描述硬件长什么样”的一门手艺跟写业务代码完全是两回事。这篇文章会从设备树的来源与结构讲起落到如何在RK3568上新增一个I2C触摸节点、如何在U-Boot 2018与Petalinux环境下维护设备树、以及编译调试过程中真正容易踩的坑。适合正准备自己写板级设备树或者常改dts完成外设移植的嵌入式开发者看完可以直接拿着你的原理图动笔。1. 设备树到底是什么一份给内核的硬件物品清单在动手写dts之前必须先接受一个观点设备树描述的是硬件的“现状”而不是CPU或外设要执行的“逻辑”。这是一个数据文件不是程序。1.1 为什么需要设备树从mach文件到统一描述早期ARM Linux里每一块开发板都在内核源码arch/arm/mach-xxx目录下有自己的机器描述结构CPU型号、内存基址、外设资源、板级初始化函数全写死在.c文件里。换一块底板就得改内核源码、重新编译整个镜像而且内核版本升级时这部分代码几乎没有兼容性可言同一个SoC的不同开发板之间也基本无法复用。设备树就是来终结这种状态的。它把“这块板子上有哪些设备”“它们接在哪个总线地址”“中断怎么连”“GPIO是高有效还是低有效”这些信息从驱动代码和板级代码里彻底剥离出来变成一份独立于内核源码的文本数据。驱动只需要根据节点里的属性向内核索取资源而不需要关心自己跑在哪块板子上。这样内核可以同一份二进制适配一大批硬件设备厂商也能不改内核就支持新板。1.2 dts、dtsi与dtb三者到底是什么关系很多教程会把dts、dtsi、dtb混着说其实它们就和对源码与可执行文件的关系一样dtsDevice Tree Source设备树源文件通常一块板子一个描述板级差异部分比如开发板型号、外设是否使能、某个外设的专属配置。dtsiDevice Tree Source Include相当于设备树头文件通常一个SoC一个描述芯片内部公共资源比如RK3568的CPU核心、GIC中断控制器、UART、I2C、DMA等节点都在rk3568.dtsi里预先定义好。dtbDevice Tree Blob由dts经过编译器生成的二进制文件是内核启动时真正解析的数据。dts文件里会通过#include rk3568.dtsi引入芯片级描述然后在下面覆盖或追加板级内容。这种设计很像C语言的头文件机制让SoC厂商只需要维护一份dtsi所有下游板卡厂商在dts里增量修改即可。编译流程也比想象中简单先用C预处理器展开#include和宏定义再用dtcDevice Tree Compiler把展开后的dts文本编译成dtb。后文第5章会给出具体命令。1.3 从U-Boot到内核dtb在启动流程中的位置设备树不是直接被内核从SD卡里读出来的它要经过引导程序。典型流程是U-Boot从存储介质eMMC、SD或网络tftp加载dtb到内存某个地址启动内核时把这个地址通过寄存器传给内核内核用unflatten_device_tree将dtb解包成一颗由struct device_node组成的节点树再根据根节点的compatible字符串去匹配内核里的machine_desc匹配成功后就按照节点树创建platform设备和各种总线设备。这里有一个新手常忽略的点U-Boot传给内核的dtb不一定是编译内核时生成的那一份。U-Boot可能自己维护一套dtb也可能在启动过程中用fdt命令对dtb做修改。所以当你发现内核看到的设备树和你写的dts不一致时先检查U-Boot有没有动手脚这个坑我在第4章会专门展开。1.4 compatible设备和驱动之间的暗号设备树里每个设备节点几乎都有一行compatible属性格式是厂商名,设备型号。比如触摸屏节点可以是compatible goodix,gt1157串口节点可以是compatible snps,dw-apb-uart。驱动侧通过of_match_table声明自己能处理哪些字符串内核在枚举设备时拿节点里的compatible去匹配驱动表匹配上了就完成设备和驱动的绑定。所以一个设备不被识别时我最先查的不是中断和地址而是compatible是否和驱动完全一致。很多代理芯片用的是同类型主控但厂商型号不同这时直接在节点里加一个额外的compatible字符串让它去匹配通用驱动往往比改驱动更省事。2. 设备树文件的基本语句节点、属性、地址与引用设备树由节点node和属性property构成。节点是树形结构里的目录属性是键值对。最简单的dts长这样/dts-v1/; / { model My RK3568 Board; compatible my,rk3568-board, rockchip,rk3568; chosen { bootargs consolettyS2,1500000n8 root/dev/mmcblk0p1 rw; }; memory0 { device_type memory; reg 0x0 0x0 0x0 0x80000000; }; };/是根节点chosen用于传递启动参数memory节点描述内存。不过这些在大部分情况下都由SoC的dtsi和U-Boot代劳了我们日常写得多的是外设节点和它们的地址、中断、GPIO描述。2.1 地址编码机制由父节点决定孩子怎么读reg这是设备树里最容易让人一头雾水的部分。每个总线节点比如SoC内部总线都会声明#address-cells和#size-cells分别表示子节点reg属性中“地址”占几个u32、“长度”占几个u32。父节点决定了子节点地址的编码规则。比如RK3568内部总线通常是soc { compatible simple-bus; #address-cells 2; #size-cells 2; };那么在它的子节点里写uart2: serialfe660000 { reg 0x0 0xfe660000 0x0 0x100; };0x0 0xfe660000拼起来是一个64位地址0xfe6600000x0 0x100表示长度0x100。千万不要只写reg 0xfe660000 0x100那样解析出来的地址会变成0x00000000fe660000之外的东西或者越界读取。还有一个容易错的地方对一条I2C总线来说子节点是挂在I2C上的从设备这时reg不再表示物理地址而是I2C从设备地址且父节点I2C控制器的#address-cells 1、#size-cells 0。所以触摸屏节点写reg 0x5d是地址不写长度因为I2C从机地址就一个cell。2.2 ranges地址空间的翻译官ranges属性描述的是“子总线地址”到“父总线地址”的映射关系。空ranges;表示子地址和父地址一一对应没有这个属性则表示子地址空间与父地址空间完全隔离父节点不能直接访问。最常见的写法是三组数字每组含义是子地址、父地址、映射长度。例如ranges 0x0 0x0 0xfe000000 0x1000000;意思是子总线地址0x0映射到父节点视角的0xfe000000映射范围0x1000000。如果子节点里的寄存器地址是0x0 0x10000翻译成实际物理地址就是0xfe010000。在添加自定义外设时如果这块外设直接挂在SoC总线上且地址已经由dtsi定义你一般不需要碰ranges。但如果你在板级dts里通过FPGA或CPLD桥接了新的子地址空间那么ranges就是内核能否正确访问这段地址的关键。2.3 中断描述interrupt-parent、interrupts与控制器中断连到哪里由interrupt-parent指定中断怎么表达由中断控制器的#interrupt-cells决定。以ARM GIC为例#interrupt-cells 3三个cell分别是中断类型0为SPI共享外设中断1为PPI私有外设中断、中断编号、触发标志。写成interrupt-parent gic; interrupts 0 41 IRQ_TYPE_LEVEL_HIGH;内核头文件里IRQ_TYPE_LEVEL_HIGH之类宏可以直接用于dts因为预处理器会把#include dt-bindings/interrupt-controller/irq.h引入的宏展开。如果节点没有显式写interrupt-parent内核会沿设备树向上查找拥有interrupt-controller;属性的祖先节点把这个节点当作中断父节点。所以当你在板级文件里挪动了某个设备节点或发现中断没有生效时别忘了看看父节点是否仍然指向正确的GIC。2.4 GPIO描述gpio-controller、gpio-cells与gpios与中断类似GPIO控制器节点要声明gpio-controller;和#gpio-cells 2。用GPIO时写irq-gpios gpio0 RK_PB4 GPIO_ACTIVE_LOW;其中RK_PB4这种宏在dt-bindings/pinctrl/rockchip.h里定义展开后就是bank内引脚编号GPIO_ACTIVE_LOW在dt-bindings/gpio/gpio.h里定义表示低有效。这两个宏的值会被预处理器算成数字最终编进dtb。需要注意的是GPIO控制器bank的编号和SoC全局GPIO编号往往不一致比如RK3568有GPIO0到GPIO4每个bank默认32个pin。原理图上标的是“GPIO0_C6”之类的代号对应到dts里要换算成bank内pin index直接照抄别人的写法很容易拉错引脚。2.5 label、phandle与覆盖增量修改的基础节点前面写的uart2:这种叫label它只是为了方便在dts其他位置用uart2引用。真正起连接作用的是编译时自动生成的phandle属性一个节点一个唯一数字。我们在源码层写uart2 { ... };编译器会自动找到对应节点的phandle并填入引用位置。这种机制催生了设备树最常见的玩法dtsi里把所有外设都定义好但置为status disabled板级dts通过i2c0 { status okay; ... }打开外设并在大括号内追加子节点或修改属性。同一个节点被引用多次时后面的配置会对前面做增量合并这让我们可以在不修改SoC厂商dtsi的前提下完成整板适配。3. 实操从RK3568底板原理图到一段能用的I2C触摸节点概念讲再多不落到代码上都白搭。下面我用一块常见底板场景走一遍完整流程触摸屏通过I2C0接到RK3568中断脚接GPIO0_B4复位脚接GPIO0_B5。原理图上芯片丝印是GT1157。3.1 第一步在dtsi里确认目标总线打开rk3568.dtsi搜索i2c0通常能看到类似这样的定义i2c0: i2cfe650000 { compatible rockchip,rk3568-i2c, rockchip,rk3399-i2c; reg 0x0 0xfe650000 0x0 0x200; interrupts GIC_SPI 87 IRQ_TYPE_LEVEL_HIGH; #address-cells 1; #size-cells 0; clock-names i2c; clocks cru CLK_I2C0; pinctrl-names default; pinctrl-0 i2c0_xfer; status disabled; };注意最后一行status disabled这说明SoC内部已经把I2C控制器描述好了但未开启。我们不用重新发明节点只需在板级dts里引用它并打开。3.2 第二步编写触摸节点在板级dts里追加#include dt-bindings/interrupt-controller/irq.h #include dt-bindings/gpio/gpio.h #include dt-bindings/pinctrl/rockchip.h i2c0 { status okay; clock-frequency 400000; touchscreen5d { compatible goodix,gt1157; reg 0x5d; interrupt-parent gpio0; interrupts RK_PB4 IRQ_TYPE_LEVEL_LOW; irq-gpios gpio0 RK_PB4 GPIO_ACTIVE_LOW; reset-gpios gpio0 RK_PB5 GPIO_ACTIVE_LOW; pinctrl-names default; pinctrl-0 touch_int_pin touch_rst_pin; status okay; }; };这里有几个细节需要解释。关于reg 0x5dGT1157的I2C从设备地址是0x5D7位地址形式原理图手册上如果写的是8位地址0xBA那么右移一位后就是0x5D。I2C子节点的父节点是i2c0而i2c0的#address-cells 1、#size-cells 0所以地址只占一格无长度。关于interrupts的编号由于中断脚直接连到GPIO0 bank的B4引脚且RK3568的GPIO控制器本身实现了中断控制器功能所以这里用interrupt-parent gpio0编号RK_PB4是GPIO bank内引脚位置。如果原理图上中断脚连的是SoC的ext interrupt专用引脚写法就要换成GIC的中断编号务必对照SoC datasheet。关于同时写interrupts和irq-gpios很多touch驱动既支持设备树中断属性也支持独立GPIO属性。前者由内核通用irq domain解析后者由驱动通过GPIO子系统申请后再转中断。两个都写通常无害但必须保证都指向同一个物理引脚。我个人更推荐驱动优先读取irq-gpios再gpio_to_irq()拿中断号因为这样可以绕开GPIO控制器与GIC之间的中断路由差异。3.3 第三步配置pinctrl解决引脚复用冲突RK系列SoC的引脚是全能引脚I2C引脚也可能被复用成GPIO或UART。pinctrl节点负责把引脚切到正确功能。在板级dts里定义pinctrl { touch { touch_int_pin: touch-int { rockchip,pins 0 RK_PB4 RK_FUNC_GPIO pcfg_pull_up; }; touch_rst_pin: touch-rst { rockchip,pins 0 RK_PB5 RK_FUNC_GPIO pcfg_pull_up; }; }; };RK的pinctrl属性格式是bank号、pin index、功能模式、上下拉配置。RK_FUNC_GPIO表示把引脚切成GPIO功能pcfg_pull_up引用dtsi里定义好的上拉配置拉上还是拉下需要看触摸屏中断脚的电平要求GT1157的INT通常是开漏低有效外部有上拉所以这里用上拉。如果漏写了pinctrl可能表现为触摸屏设备已经在I2C总线上枚举出来了但中断怎么都触发不了或者触发后马上又丢失。原因是引脚还在复用成别的功能GPIO输入采样不到正确电平。这种问题dmesg看不到明显报错非常难排查所以每次新增外设节点我都会把pinctrl当成必写项而不是可选项。3.4 编译验证在Linux内核源码目录下单编dtb非常快make ARCHarm64 rk3568-myboard.dtb或者直接全编dtbs。如果平台不是RK就换成对应的dts文件名。编译通过后把dtb替换到启动分区上电后先用i2cdetect探测i2cdetect -y 0能看到5d地址出现说明I2C链路通。再查看中断有没有注册cat /proc/interrupts | grep touch加上触摸动作如果驱动正常这里会出现对应的中断计数累加。设备树如果写错最常见的结果不是内核panic而是某个设备“消失”或驱动defer这时候就要靠第5章的调试手段来定位了。4. U-Boot 2018与Petalinux场景下设备树的维护差异设备树不止存在于内核。近些年的U-Boot也大量依赖dts描述板级硬件Petalinux这种构建系统也把设备树当成一等公民。这里的坑和内核不完全一样值得单独说。4.1 U-Boot 2018里的dts谁来改改哪份从U-Boot 2016到2018这几年大量板级配置从board/xxx/xxx.c搬到arch/arm/dts/xxx.dts。U-Boot启动时会先利用自己的dtb初始化串口、DDR、时钟再把另一份dtb传给内核。这两份dtb可以是同一个文件也可以完全不同取决于板级构建脚本。很多人在U-Boot里找不到内核dts其实是没搞清楚“U-Boot自己的设备树”和“传给内核的设备树”之间的差异。以RK平台为例U-Boot的dts通常位于U-Boot源码arch/arm/dts/rk3568-evb.dts而内核dts位于内核源码arch/arm64/boot/dts/rockchip/rk3568-evb.dts。修改时要先确认改的是哪一个别盲目搜索同名文件。U-Boot默认环境下有个重要变量fdtfile它决定了U-Boot去哪个分区/目录加载哪个dtb。如果改了内核dtb文件名但忘了改fdtfile启动时依旧加载旧文件这是设备树修改不生效的最常见原因之一。4.2 在U-Boot命令行里手工改设备树U-Boot里可以动态修改传给内核的dtb适合调试不适合当长期方案。常用命令流程setenv fdt_addr_r 0x40000000 load mmc 0:1 0x40000000 rk3568-myboard.dtb fdt addr 0x40000000 fdt list /soc i2c0 fdt set /soc i2c0 status disabled bootm 0x40080000 - 0x40000000这种做法的价值在于硬件改动时不用反复编译整个镜像先在U-Boot里把节点改一改试出可行的配置再回到dts源码固化。但它对用户要求高而且fdt命令对属性名、节点路径非常敏感写错一个斜杠就提示找不到节点。所以我的习惯是U-Boot里只验证不维护长期改动全部回到源码里去。4.3 Petalinux里改设备树配方与层Petalinux构建系统本质上是Yocto设备树不直接放在某个固定目录而是由配方管理系统收集。最常见的做法是在项目工程里创建或编辑meta-user/recipes-bsp/device-tree/files/system-user.dtsi然后确保device-tree配方包含这个文件构建时它会被追加到主dts里。Petaliux把用户自定义设备树分散到system-user.dtsi而不是直接让你改kernel-source/arch/arm64/boot/dts/...是因为Yocto构建时会把内核源码下载到临时目录手工改动会在下一次clean或升级时被冲掉。只有把修改放进meta层才能保证可重复构建。具体操作大致是把自定义内容写进system-user.dtsi然后重新运行petalinux-build -c device-tree petalinux-build -c kernel petalinux-package --boot --dtb注意petalinux构建顺序很重要device-tree配方先编译生成dtbkernel配方后编译最后打包时把新的dtb和内核镜像一起生成BOOT.BIN。如果只重编内核不重编device-tree很可能打包的还是旧的dtb。4.4 一份dtb两处使用的注意点RK平台的U-Boot和内核经常共用同一份dtb。U-Boot会根据自己的需要更新chosen节点里的bootargs也可能会修改memory节点里的DDR容量。尤其是DDR容量不同或DDR频率训练结果不同时U-Boot要动态调整内存节点否则内核按错误的内存布局初始化轻则内存变小重则启动崩溃。所以如果你在板级dts里写了memory节点却发现启动后free -m显示的容量和硬件不一致先别怀疑设备树写法多半是U-Boot启用了固件传递内存信息的机制覆盖了dts里的声明。内核启动参数里可以加mem临时限制内存大小做验证但治本的办法是让U-Boot正确更新记忆节点或者在内核config里关闭不合适的固件内存处理逻辑。5. 编译dtb、反编译与运行期验证三板斧设备树写完了怎么确认它真的是内核看到的那一棵答案不是“相信编译生成了什么”而是看“启动时实际生效了什么”。5.1 单文件编译与反编译在Linux源码目录下最省事的编译命令是make ARCHarm64 dtbs如果只想编某一个文件make ARCHarm64 rockchip/rk3568-myboard.dtb但有时候你拿到的是一份脱离内核源码的dts想单独用dtc编译可以这样dtc -I dts -O dtb -o myboard.dtb myboard.dts前提是dts里没有宏和#include否则需要先用cpp预处理。内核对设备树编译有更完整的预处理器规则所以一般还是回源码树里编最稳。反编译是反向操作dtc -I dtb -O dts -o myboard_restored.dts myboard.dtb反编译出来的dts和源码不完全一样属性顺序、标签名都会被简化但它能直观显示这个dtb里到底有哪些节点、哪些节点被置为disabled是排查“源码看起来没问题但实际设备没有”的首选手段。5.2 运行时设备树查看一切以/sys为准Linux内核启动后你已经无法直接看到dtb文件了但内核把解析后的设备树暴露在了/sys/firmware/devicetree/base目录下。这个目录的目录结构就是设备树节点结构每个属性是一个文件。快速确认某个外设属性是否存在ls /sys/firmware/devicetree/base/soc/i2cfe650000 cat /sys/firmware/devicetree/base/soc/i2cfe650000/status注意属性值文件里可能带有结尾的空字符直接cat会看到正常内容但末尾可能有奇怪的显示。用hexdumphexdump -C /sys/firmware/devicetree/base/model就能看清原始字节。很多定位工作其实可以完全靠这根“活设备树”完成先确认运行时的节点属性再反推源码哪里写错省去反复重启的时间。5.3 内核日志里的设备树错误当设备树节点有问题内核往往会在启动早期打印一些容易忽略的提示。比较典型的包括OF: fdt: Error -22 processing node ...表示某个节点的属性格式不合法常见引起原因是reg的cell个数与父节点的#address-cells不匹配。invalid resource或failed to get resource说明驱动向节点索要reg或interrupt资源时解析失败。unable to handle kernel paging request at ...如果驱动把reg里的错误值当成寄存器基址访问就会在访问时崩溃。遇到这类日志不要一头扎进驱动源码先反编译dtb检查节点再看驱动里的of_match和platform_get_resource调用。设备和硬件都对通常问题就在某个属性少写或多写了一个cell。5.4 设备树overlay调试如果是模块化外设不想动不动改主dtb可以用设备树overlay。编译overlay需要保留符号所以dtc要加-参数或者内核里通过make dtbs生成对应dtbo后运行时用configfs加载mkdir /configfs/device-tree/overlays/myoverlay cat myoverlay.dtbo /configfs/device-tree/overlays/myoverlay/dtbo这个思路很适合调试“临时挂一个外部设备”的场景不用反复改启动分区。但overlay要求主dtb本身带符号信息频谱芯片、FPGA桥、自定义板级设备都可以受益。注意生产环境尽量别用overlay因为它的合并规则和启动时静态dtb并不完全一致偶尔会出现phandle冲突这类问题。6. 那些绕不过去的坑最后这部分是我实际踩过、也帮别人定位过的典型问题几乎每一条都浪费过至少半个工作日。6.1 cells写错一格整个地址链全崩最常见的是子节点reg多写或少写一个cell。父节点#address-cells 2子节点只写了一个32位地址内核解析时会把相邻属性值吞进来当作地址的一部分结果资源地址变成一串莫名其妙的值。最要命的是这种错误在编译期不报错只有运行期驱动去ioremap的时候才崩溃。规避办法是在写任何带reg的节点前先向上找两三层确认父节点及祖父节点的#address-cells和#size-cells再数一下自己的reg格式。6.2 status disabled 被二次覆盖同一个节点可能在多个dtsi里都被引用。比如SoC厂商的dtsi里把i2c0 { status disabled; }定了你的板级dts里覆盖成okay但如果某个中间层级的dtsi在include顺序上晚于你的板级dts它又写了一次disabled最终合并结果就是关闭状态。这类问题在运行时的/sys/firmware/devicetree/base里一查便知。所以我在排查外设不工作时第一步永远是查runtime status而不是反复检查源码。6.3 引脚复用和GPIO功能互掐RK平台尤其明显。同一个引脚既能当GPIO也能当I2C/SPI/PWM最终状态由pinctrl决定。设备树里写GPIOgpios属性并不代表引脚就是GPIO了还要在对应节点或子系统中配置pinctrl把引脚切到GPIO功能。反过来也一样某个外设A占用了引脚复用外设B的GPIO就完全失效两个设备节点都写对了但互相打架。建议每新增一个使用引脚的设备都先在dtsi里搜索这个引脚有没有被其他节点引用。芯片引脚复用表密密麻麻但设备树里都能查到多这一步能省大量debug时间。6.4 phandle冲突与overlay加载失败启动时静态解析的dtb一般不出现phandle冲突但overlay场景就会出现。每次加载一个dtbo如果它引用了一个主dtb里不存在的label内核会拒绝合并如果两个overlay都试图给同一个节点加不同子节点也可能发生覆盖混乱。所以overlay的dtbo编译一定要用-并且加载顺序要稳定。调试时如果overlay加载一半失败用/sys/kernel/debug/device-tree/下的信息看不出问题可以直接看dmesg里of_overlay_fdt_apply的内核打印它会把失败原因说得比较清楚。6.5 别用文本编辑器改dtb这个看起来像废话但我真的见过有人用十六进制编辑器把status从“disabled”改短到“okay”然后整个板子启动失败。dtb的二进制格式是设计给内核解析的节点对齐、字符串表、phandle映射都是结构化的手工改动一个字节都可能破坏整棵树。所有修改必须回到dts源码重新编译或者用U-Boot fdt命令在内存里改。写设备树文件这件事其实最考验的不是语法记忆而是对硬件细节的耐心。每次新增外设我建议只改一个节点、编译一次、启动验证一次确认没问题再继续下一个。如果一次改了一堆节点出了故障你根本不知道是哪个引脚复用错了还是哪个reg少写了一个cell。保持最小改动是所有底层硬件调试的通用法则设备树也不例外。现在拿到你的板子原理图挑一个最不起眼的外设练手比如一颗GPIO控制的LED。会写LED节点就等于摸清了设备树的半套门道剩下的更像填空题。
返回列表