ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RK3588 U-Boot设备树修改不生效?深度解析启动链路与避坑指南

RK3588 U-Boot设备树修改不生效?深度解析启动链路与避坑指南 1. 先从一次翻车说起RK3588的U-Boot设备树为什么值得单独研究我接手RK3588平台的第一周就在U-Boot设备树上栽了个跟头。当时要做的事情本身不复杂——把某个MIPI-DSI屏的初始化参数从内核阶段提前到U-Boot阶段因为需要U-Boot启动时直接点亮屏幕显示Logo。按惯性思维我直接去改了内核的.dts文件测试时发现Logo依旧黑屏而内核日志里屏幕初始化又是成功的。折腾了半天才反应过来U-Boot阶段用的设备树和内核阶段用的设备树根本不是同一个东西。这个教训让我意识到RK3588上的U-Boot设备树有一套完全独立的编译、加载和解析链路很多刚从kernel开发转过来的工程师包括我自己都会默认设备树嘛改改dts就行了结果就是改了没生效或者改了之后U-Boot直接启动崩溃。先说清楚这篇文章覆盖的内容RK3588平台上U-Boot设备树文件的定位与作用、完整的修改与编译流程、以及我在实际项目中踩过的四个比较有代表性的坑。适合正在做RK3588包括RK3568这类同架构兄弟芯片底层开发、uboot移植、或者需要U-Boot阶段驱动外设的朋友参考。我会尽量按照实际调试顺序来讲而不是按手册目录来讲。需要提前说明的是这篇文章里的文件路径和操作步骤是基于Rockchip官方U-Boot仓库在RK3588上的常见布局写的。各家板卡厂商比如官方EVB、第三方核心板在具体文件组织和配置菜单上会有差异但底层的机制是通用的理解了原理换一块板子你也能快速定位。2. RK3588启动链路里U-Boot设备树到底卡在哪一环2.1 从BootROM到U-Boot设备树在这个阶段扮演什么角色先把RK3588的上电启动流程捋一遍这对理解后面的修改逻辑至关重要。RK3588芯片内部有一段固化的BootROM代码上电后先执行它。BootROM会根据启动引脚的电平状态从SPI-NOR Flash、eMMC、SD卡或者USB等介质中加载下一级引导程序。在Rockchip平台上这个下一级引导程序在较新的U-Boot版本中分两个阶段TPLTiny Program Loader和SPLSecondary Program Loader。TPL负责最基础的DDR初始化然后跳转到SPLSPL负责初始化存储控制器、时钟然后把完整的U-Boot主程序加载进内存。这里有一个很多新手会忽略的关键点TPL和SPL阶段各自都会用到一份设备树。是的U-Boot在RK3588上实际编译产出多个设备树二进制.dtb分别服务于不同的启动阶段。这就意味着如果你在U-Boot阶段要驱动某个外设比如点亮屏幕、初始化触摸、配置网口PHY必须搞清楚你的改动应该落在哪一个阶段的设备树上。很多情况下驱动外设的动作确实发生在U-Boot主程序阶段对应的就是u-boot.dtb但如果是DDR频率调整或者存储介质选择这类更底层的事情那就要动TPL/SPL对应的设备树了。从纯开发角度讲90%的日常修改集中在U-Boot主程序阶段的u-boot.dtb。这篇文章后面提到的修改U-Boot设备树默认就是指它。2.2 RK3588特有的资源映射为什么你改的节点没生效在修改设备树之前必须先建立一张资源地图。RK3588的外设资源映射在设备树里有一套成熟的命名规范和层级结构核心的几个大类包括系统级资源mapped内存节点定义了DDR的地址范围、保留内存比如给ISP、VPU、RGA用的CMA区域dmc节点DDR控制器参数包括频率表、电压域配置pmu节点电源管理单元涉及各域的上电时序外设控制器节点i2c、spi、uart等总线控制器pinctrl引脚功能复用配置这个在U-Boot阶段尤其容易出问题gpio通用输入输出常配合pinctrl一起配置regulator电源调节器U-Boot阶段初始化外设时经常需要先把对应的电源轨打开显示相关节点display-subsystem显示子系统总节点route_hdmi、route_dsi等路由节点决定显示链路怎么走panel节点屏幕面板参数时序、初始化序列等很多人在RK3588上改U-Boot设备树没生效原因往往不是节点写错了而是改错了映射层级。RK3588的设备树使用dtsiSoC级加dts板级的嵌套结构SoC级定义外设控制器的寄存器地址、中断号、时钟ID板级只负责定义这块板子上这个控制器接了什么东西。如果你在板级.dts里直接修改了一个在SoC级.dtsi里定义的属性并且没有通过节点的引用方式来覆盖那么你的改动很可能被下一层的默认值覆盖掉或者因为语法错误被直接忽略。2.3 pinctrl与regulatorU-Boot阶段最大的两个隐藏依赖我自己的经验是在U-Boot阶段调试设备树最容易踩进去的两个泥潭是pinctrl配置不完整和regulator依赖未满足。先讲pinctrl。内核阶段设备树里的pinctrl配置通常会非常完整因为内核驱动要求精确的引脚复用和上下拉状态。但U-Boot阶段对pinctrl的处理相对省事——U-Boot不会像内核那样对所有设备树节点做完整的pinctrl解析它只会在驱动初始化时查找并应用节点里显式引用的pinctrl状态。举例来说你想在U-Boot阶段把某个UART口用作调试串口输出日志。你以为只要在aliases节点里把serial2指过去就行结果发现U-Boot日志还是在原来的串口上输出。排查到最后发现这个UART对应的pinctrl-0属性里并没有配置TX/RX引脚的复用功能U-Boot的串口驱动初始化时根本没有把引脚切换到UART模式。内核之所以没问题是因为内核的pinctrl子系统会在驱动probe时自动应用pin function而U-Boot的驱动模型不够智能你不显式写清楚它就不管。再说regulator。U-Boot阶段的驱动通常只做够用的初始化但够用不等于不需要电源。比如你要驱动eMMCeMMC的供电轨vmmc如果没有在设备树里正确关联到一个regulator节点U-Boot的mmc驱动初始化时开不了电设备直接枚举失败。类似的问题在RK3588这种多电源域架构的芯片上尤其明显。所以我给的建议是在U-Boot阶段修改设备树前先把你要驱动的外设在完整设备树即内核设备树里的所有依赖梳理清楚特别是pinctrl和regulator然后逐项在U-Boot设备树里补齐。这不是偷懒能跳过的一步。3. 实战修改RK3588 U-Boot设备树的核心操作流程3.1 第一步在庞大的仓库里定位正确的dts/dtsi文件Rockchip的U-Boot仓库目录结构与上游U-Boot基本一致设备树文件主要分布在两个地方arch/arm/dts/存放绝大部分的设备树源文件board/rockchip/Rockchip板级相关代码也可能包含板级头文件在arch/arm/dts/下RK3588相关文件命名规律大致是rk3588s.dtsi # SoC级定义RK3588S是RK3588的衍生型号 rk3588-evb.dts # EVB板级定义 rk3588-evb1-lp4.dts # 搭配LPDDR4的EVB板子 rk3588-evb2-lp4.dts rk3588s-tablet.dts # 平板方案如果你用的是第三方核心板厂商一般会在自己的BSP里附带对应的.dts文件命名可能是rk3588-yourboard.dts。定位方法也很简单在U-Boot根目录执行grep -rn model arch/arm/dts/rk3588*.dts根据model属性里的板卡名称基本就能锁定目标文件。如果不是官方板直接去问板卡厂商要他们的dts从别家板子改起会走很多弯路。还有一个顺序问题值得说。RK3588的设备树结构里.dts文件通常通过#include引入了多个.dtsirk3588-evb.dts #include rk3588.dtsi # SoC通用定义 #include rk3588-evb.dtsi # 板级外设配置 #include rk3588-android.dtsi # Android相关配置取决于BSP版本rk3588-android.dtsi这种文件尤其容易坑人。它里面可能已经默认配置了很多与Android系统相关的节点比如chosen启动参数、firmware节点、一些特定的内存布局。如果你在板级.dts里修改了一个chosen属性但.dtsi里也有一个同属性定义那么编译时后#include进来的文件里的定义会覆盖之前的。所以改之前先搞清楚引用顺序不然改了等于没改。3.2 最小修改示例在U-Boot阶段开启一个额外的调试串口这个例子比较有代表性也是我实际项目里做过的事。需求U-Boot阶段默认日志输出在UART2上但板子硬件设计上调试串口接在了UART3需要把U-Boot日志改到UART3输出。完整修改分三处第一步在.dts里找到或添加UART3节点引用。一般需要这样一段uart3 { status okay; pinctrl-names default; pinctrl-0 uart3m1_xfer; };注意这里的uart3m1_xfer是引脚复用组的名称m1表示第1组复用。具体是哪一组要看rk3588.dtsi里pinctrl节点的定义。RK3588的UART3通常有m0、m1两组可选你需要确认硬件原理图上实际连的是哪一组引脚。接错m0/m1串口一样不出数据而且很难排查。第二步在chosen节点里修改stdout-path。找到chosen节点把标准输入输出路径指向UART3chosen { stdout-path uart3; };如果板子原本有stdout-path uart2;记得改过来。这一步的作用是告诉U-Boot的console框架默认控制台走哪个串口。第三步检查并确认时钟和电源依赖。RK3588在U-Boot阶段默认可能没有为UART3所在的电源域打开时钟但通常Rockchip BSP里会默认打开所有UART的时钟方便调试所以这一步多数情况下不需要改。但如果你发现UART3在U-Boot阶段依然没有输出时钟就要作为一个重要怀疑对象来排查我后面会详细讲。3.3 编译与烧录这一步比你想的更讲究修改完设备树文件之后进入编译环节。RK3588的U-Boot编译流程推荐使用官方脚本如果手动编译需要清楚以下几点。通常的第一步是配置defconfigmake rk3588_defconfig然后执行编译make -j$(nproc) CROSS_COMPILEaarch64-linux-gnu-设备树文件会在编译过程中被自动解析成u-boot.dtb并且会作为payload嵌入到最终的u-boot.bin中。这是理解改了没生效的关键之一在RK3588的启动流程中U-Boot主程序的设备树并不是一个独立文件被加载器单独加载的而是被编译链接进了u-boot.bin镜像里由一个特定符号指向。所以烧录的时候你需要烧录的是重新生成的u-boot.bin而不是单独去烧哪个.dtb。很多人不知道这一点改完dts后只烧了resource.img或者内核dtb那自然永远不生效。Rockchip平台的完整固件烧录用的是update.img或者loader.img等镜像组合。如果你用的是开发板的烧录工具如RKDevTool烧录时选择u-boot.bin对应的分区即可。需要特别检查烧录工具的配置确认它烧的是你重新编译出来的u-boot.bin而不是原始出厂镜像。我见过不止一次编译半天没问题结果烧录工具加载的是旧镜像路径白忙一场。另外RK3588上还有一个细节SPL和TPL阶段的依赖。因为完整U-Boot镜像中包含TPL和SPL部分修改设备树虽然不会直接影响TPL/SPL的代码逻辑但重新编译时最好做一次完整清理make clean make rk3588_defconfig make -j$(nproc) CROSS_COMPILEaarch64-linux-gnu-避免因为增量编译导致某些依赖没有重新生成尤其是在你动了dtsi文件的情况下。4. 高频踩坑点我替你交过的四种学费4.1 改的是dts编的却是别的东西依赖关系引发的无效修改这个坑我在文章开头已经提到但值得展开说说具体表现。有一次我的需求是修改U-Boot阶段的LCD初始化参数。改了rk3588-evb.dts里的panel节点重新编译烧录重启屏幕依然一片黑。当时我以为是参数问题来回调了好几轮timing参数结果都没用。后来冷静下来去arch/arm/dts/Makefile里查了一下发现这块板子默认编译用的并不是我改的那个.dts。RK3588 EVB板在U-Boot的Makefile里通常会定义一个默认的设备树目标但这个目标可能是rk3588-evb1-lp4.dtb而不是rk3588-evb.dtb。也就是说我改的文件根本不在编译目标列表里改得再正确也不会生效。排查方法很简单在U-Boot根目录执行grep -n rk3588 arch/arm/dts/Makefile查看实际编译目标和你的板子型号逐一比对。如果发现默认目标和你的板卡实际dts文件对不上就需要修改Makefile或者重新选择一个defconfig来指定正确的设备树。这个坑有多普遍我可以负责任地说在一个团队里几乎每个RK3588相关项目初期都会有人踩一次。因为厂商BSP一般会包含多个板型的dts而Makefile里的默认编译目标往往只有一两个。拿到板子第一件事千万别急着改代码先确认defconfig和Makefile目标可以省下一整天的冤枉时间。4.2 U-Boot里的regulator和内核regulator打架这个坑涉及到U-Boot和内核之间的状态交接是典型的改对了也埋雷的问题。场景是这样的U-Boot阶段需要给某个外设供电比如一个SDIO接口的WiFi模块它的供电轨是vcc_wifi_3v3。我在U-Boot设备树里给这个regulator接上了正确的GPIO控制U-Boot阶段电压正常外设初始化顺利。但是进入内核后WiFi模块偶尔会出现探测失败、时好时坏的诡异现象。排查了很久最后定位到问题出在regulator的状态交接上。内核的regulator驱动在上电时会对所有已知regulator做一次状态检查并根据regulator-boot-on、regulator-always-on这些属性来决定初始状态。我的U-Boot设备树里给这个regulator设置了regulator-boot-on表示启动阶段就要打开。这个属性本身没有错。问题在于U-Boot阶段驱动外设时可能把这个regulator的电压设置成了某个体值而内核驱动初始化时要求一个不同的电压值。如果两者之间缺少一次平滑的电压切换逻辑内核regulator驱动在重新配置电压时可能会出现瞬态跌落导致下游外设进入异常状态。经验教训是在U-Boot设备树里设置regulator相关属性时要提前想清楚这个外设进入内核后的工作电压是多少。如果你的U-Boot阶段需要给外设供的电压与内核驱动要求的电压不同比如U-Boot阶段只需要低功耗开启内核阶段才跑全速那最好在U-Boot阶段初始化完成后、跳转内核前把regulator恢复到内核期望的默认状态。在正式产品中这通常通过U-Boot的board后期代码来实现而不是简单依赖设备树。话说回来如果你的产品里U-Boot阶段根本不需要驱动这个外设那就别在U-Boot设备树里给它加regulator配置多一事不如少一事避免给内核埋坑。4.3 复位引脚时序不对外设总是初始化失败一个需要耐心排查的案例这个坑跟RK3588关系不算特别大但在实际调试嵌入式Linux设备树时很常见值得提一下。RK3588平台上一个PCIe设备NVMe SSD在U-Boot阶段能识别但进入内核后偶尔识别不到。通过串口日志配合pcie驱动的调试信息发现NVMe SSD的复位引脚PERST时序不满足规格书要求——复位信号释放到PCIe链路训练开始之间的时间间隔太短。内核设备树里通常有一个reset-gpios属性通过reset-deassert-us或reset-assert-us这样的属性来控制复位时序。但在U-Boot的设备树模型里这个属性支持情况不一有些版本并不解析。我当时的做法是在U-Boot阶段直接通过GPIO操作来手动控制PERST引脚的时序确保链路训练前有一个足够长的高电平窗口。如果你也遇到类似问题排查思路是先看U-Boot设备树里的PCIe节点是否有reset-gpios和时序相关属性如果没有确认U-Boot驱动是否支持解析这些属性如果驱动不支持只能退回到GPIO直接操作的方式。类似的场景在MIPI-DSI屏幕初始化上也很常见屏幕的复位引脚和供电时序不对直接表现就是屏幕闪烁或者点不亮。我的一个实操习惯是把外设规格书里的时序图截图放在手边然后在设备树里甚至U-Boot的board代码里精确计算每一段延时。RK3588平台本身的运行频率很高一个简单的mdelay(10)足够覆盖绝大多数时序要求但如果你是把延时设得刚好卡在规格书边缘那就等于埋了一颗定时炸弹量产批次一换可能就炸了。4.4 在U-Boot设备树里写了内核才有的节点导致启动流程异常这个坑比较隐蔽也比较能体现U-Boot设备树和内核设备树同源但不同用的本质。U-Boot的设备树在编译时会有一个CONFIG_OF_SPL_REMOVE_PROPS或者类似机制用于在U-Boot阶段的fdtgrep处理中剥离掉一些U-Boot不需要的属性。但是如果你直接修改了设备树源文件并添加了某个节点而这个节点在U-Boot的fdt处理逻辑中没有被显式允许就可能出现异常。最常见的情况是在设备树里给某个外设添加了一个较新的属性绑定比如某种新的compatible字符串这个绑定只有内核驱动认识U-Boot驱动不认识。U-Boot在解析设备树时如果发现某个节点状态是okay但对应的驱动不存在它通常会静默跳过不影响启动。这属于温和的情况。危险的情况是添加的节点包含U-Boot阶段也会尝试解析的错误属性比如错误的中断号格式、错误的clocks引用导致U-Boot在启动早期访问非法地址。尤其是访问不存在的时钟ID时U-Boot的clock驱动会尝试去操作对应的寄存器如果这个寄存器地址域不存在或已被其他控制器占用轻则挂死重则直接crash。我在一次调试中往U-Boot设备树里加了一组DDR频率表里的新频率档位结果U-Boot在初始化DMC时去读一个不存在的PLL配置直接卡死在DDR初始化阶段。这就是典型的内核思维带到了U-Boot的教训。结论U-Boot设备树和内核设备树是同源但不同命的两个世界。同源指的是它们的源文件可能来自同一个仓库或同一份基础dtsi不同命指的是它们的解析器、驱动支持范围、甚至属性语法支持程度都完全不同。修改U-Boot设备树时首先要确认目标驱动在U-Boot里存在其次要确认节点引用的资源在U-Boot的设备模型里是合法的。5. 验证修改是否生效的实用手段5.1 在U-Boot命令行里直接查看和设备树相关的信息修改设备树之后不用急着进系统在U-Boot命令行下就能做初步验证。重启开发板在U-Boot启动阶段快速按任意键进入命令行提示符。然后执行fdt addr $fdt_addr_r fdt print /chosenfdt addr命令指定当前内存中设备树的位置$fdt_addr_r是环境变量通常指向U-Boot加载设备树的内存地址fdt print可以查看指定节点当前的实际状态。如果你修改的是chosen节点里的stdout-path在这里就能直接确认它是否已经生效。如果你修改的是某个外设节点的status属性同样可以用fdt print去查看该节点。如果你所处环境里没有fdt命令有些裁剪过的U-Boot配置没有包含它也可以使用dm tree命令查看当前U-Boot设备模型下所有活动的设备列表dm tree这个命令会列出U-Boot驱动模型当前识别的所有设备包括那些probe成功的和失败的。如果你的外设节点存在但驱动没有绑定dm tree里就不会出现对应的条目或者会显示一个驱动bind失败的错误。这一步能帮你快速判断节点写对了没有驱动认不认这个节点。5.2 串口日志的逐段排查方法串口日志是验证设备树修改效果最直接的手段。以RK3588 U-Boot启动过程为例保留的串口日志大致可以分为几个阶段TPL阶段日志一般以U-Boot TPL开头会打印DDR初始化信息。如果你修改的是DDR相关的设备树配置这个阶段是你观察效果的最前端。SPL阶段日志以U-Boot SPL开头会打印存储介质初始化、加载U-Boot主程序的过程。U-Boot主程序日志打印版本号、编译时间、板级信息、设备树加载地址、外设初始化过程。当你怀疑某个外设的设备树配置有问题时建议打开U-Boot对应驱动的调试宏重新编译。以串口为例在drivers/serial/目录下通过DEBUG宏打开调试信息后可以打印出串口驱动初始化时读取的设备树参数包括时钟频率、baudrate、引脚配置等。一个很实用的技巧是在完整U-Boot日志中搜索关键字Device Tree或FDT。RK3588的U-Boot在加载设备树后会打印类似Using default environment或者Failed to get default environment的信息同时会报告设备树在内存中的加载地址。如果在日志里出现了设备树相关的解析错误通常会有ERROR或WARNING级别的提示。值得注意的是U-Boot阶段的串口日志输出级别默认可能不高有些信息不会打印出来。你可以通过设置环境变量bootargs中的earlycon或者console参数来增加调试输出但这个方法在U-Boot阶段未必生效更可靠的是在编译时通过CONFIG_LOGLEVEL配置来调整。5.3 确认内核阶段拿到的设备树是否被覆写这是很多人在RK3588调试中忽略的一个环节。RK3588的U-Boot在跳转内核之前会做一些设备树处理工作。具体来说U-Boot会把自己内存里的设备树就是我们前面修改并编译进去的那份传给内核同时会根据启动介质、硬件版本等信息对设备树做一些修改比如填充内存大小、进行fdt chosen处理、根据bootargs环境变量修改内核启动参数。所以在验证时你需要确认内核实际拿到的是不是你改的那份设备树。操作方法是进入内核后在设备树文件系统里查看实际生效的设备树cat /proc/device-tree/model cat /proc/device-tree/chosen/stdout-path如果你发现这里打印出来的值和你在U-Boot设备树里设置的不一致说明U-Boot在跳转前对它做了二次修改或者是启动脚本比如bootcmd、bootargs里使用了自定义的fdt操作命令覆盖了设备树。RK3588平台上还有一种常见情况有些板级配置里U-Boot会根据存储在eMMC特定分区的参数比如misc分区里的bootconfig信息动态修改设备树。如果你的改动总是被覆盖可以检查一下U-Boot的board代码里是否有类似ft_board_setup的函数这类hook函数经常被用来在跳转内核前动态调整设备树内容。5.4 对比法同时抓取U-Boot前后两个阶段的设备树差异最后说一下我常用的一个高效率验证方法——对比法。在U-Boot命令行里先把内存中的设备树保存到存储介质上fdt addr $fdt_addr_r fdt save 0x1000000 /dtb_uboot.dtb等内核启动后再把内核实际使用的设备树导出出来dtc -I fs -O dts /proc/device-tree /dtb_kernel.dts对比这两个文件你就能精确看到U-Boot在跳转前对设备树做了哪些修改。这个方法在排查U-Boot里改了、内核阶段表现却不对的疑难杂症时非常有效。我曾经靠这个方法定位到一个很隐蔽的问题U-Boot阶段把某个GPIO的默认状态设为高电平以解除外设复位但在跳转内核时U-Boot的GPIO驱动根据设备树里的gpio-hog配置把该引脚重置为了低电平导致外设在内核初始化时处于复位状态功能异常。对比两份dtb后一眼就发现了这个属性的差异。6. 一些经验体会RK3588的U-Boot设备树调试说难确实有一定门槛因为涉及BootROM、TPL、SPL、U-Boot主程序、内核这一整条链路的协作说简单其实只要理解了U-Boot是一套独立的系统设备树在它内部有自己的生命周期这个核心思想很多问题都能迎刃而解。我个人在调试中最受用的三个习惯也分享给大家第一永远先确认编译目标再动手改dts。花十分钟查Makefile和defconfig比改了半小时才发现文件不对要强得多。第二养成保存现场的习惯。U-Boot设备树出问题时往往是启动早期没有完整的日志系统唯一的现场就是串口输出和内存中的dtb。每次调试前固定好串口日志保存方式比如用script命令配合串口工具先把上一把的日志完整保存下来再去进行新的修改。很多时候反复出现问题最后就是靠对比前后几次日志的细微差异找到了问题时序。第三大胆使用源码级调试。不要在猜上花太多时间。RK3588的U-Boot源码完全开放驱动模型也比较规范遇到设备树不生效的问题直接在drivers/对应驱动源码里搜索设备树属性解析函数看它到底读取了哪些属性、有没有日志输出、在哪个环节走入了error路径。一套流程下来大多数问题在两个小时内可以定位到一个具体的代码分支。最后想说设备树改不好只是表象本质上是还没建立起对启动流程各阶段什么能用、什么不能用的判断力。这个判断力没有捷径就是多改、多试、多看日志把每个阶段的日志特征记在心里。希望这篇文章能让大家少走一些我走过的弯路。
返回列表