ARTICLE DETAIL

资讯详情

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

RK3399更新DTB全指南:编译、烧写与启动失败排查

RK3399更新DTB全指南:编译、烧写与启动失败排查 先说个真实经历。有次我在一块RK3399开发板上调整GPIO复用想多引出一路UART调试串口改完设备树源码、重新编译出新的dtb烧进板子重启结果U-Boot正常进内核却直接panic。整整查了一晚上最后发现根本不是dtb内容写错了而是U-Boot从ext4的boot分区加载dtb时路径和分区格式没对上。那次之后我算是搞明白了RK3399更新dtb这件事表面看起来就是把一个二进制文件拷进板子实际上背后牵扯到设备树编译、内核配置、分区布局、U-Boot启动参数这一整条链路的配合。这篇文章就把我在这条链路上反复踩过的坑和验证过的方法完整梳理一遍。不管你是做方案评估、驱动移植还是纯粹想给自己手头的RK3399板子换一套外设配置文中的操作思路和排查步骤都能直接拿去用。1. 为什么RK3399需要手动更新dtb三种典型场景先说清楚一个基本事实RK3399的系统启动流程里dtb不是一个可有可无的附属文件而是决定内核如何识别硬件、如何初始化外设的关键输入。U-Boot负责把dtb从存储介质读进内存然后在跳转内核时把物理地址传过去内核启动早期会解析这份二进制数据构建设备树。这个阶段一旦出错小则某个外设失灵大则直接启动失败。1.1 场景一内核升级或换用主线内核Rockchip官方发布的固件其内核源码和dtb都是配套的。如果你想把内核从4.4升到5.10或者干脆换成mainline主线内核那么原来的dtb大概率不能继续用。原因很简单新旧内核里设备树绑定的节点结构、compatible字符串、时钟和pinctrl描述方式都会变一份旧dtb喂给新内核解析阶段就可能拒绝识别更别提后续的外设初始化。我实际遇到过的情况是用4.4内核的dtb启动5.10内核系统能起来但以太网MAC地址读取失败eMMC识别成8GB实际是64GB调试串口完全静默。这类问题外表看起来像驱动bug根因却是dtb和内核版本不匹配。1.2 场景二板级外设改版或引脚重新规划开发阶段最常见的需求。比如一块RK3399核心板原来默认把I2C0留给摄像头但你的项目里摄像头不需要了想把I2C0的引脚空出来给触摸屏用又比如某个GPIO默认上拉你的硬件上需要下拉——这些配置都写在dts源文件里改完就要重新编译并更新dtb。这类需求还有一个隐蔽的问题同一个SoCRK3399往往有多个板级dts文件。Rockchip官方evb、NanoPC-T4、Rock Pi 4这些板子SoC一样但DDR颗粒、PMIC型号、网卡PHY、HDMI接口的电源控制引脚各不相同。拿别家板子的dtb烧到自己的板子上轻则个别外设不工作重则直接不开机。所以更新dtb之前第一步永远是确认你手里dts源文件确实对应你自己的板子。1.3 场景三从Android固件迁移到Linux固件RK3399有不少商用设备出厂装的是Android系统而Android固件里dtb通常是打包在resource分区里的。如果你要在这类设备上跑Debian/Ubuntu或者自己基于Buildroot做系统就可能需要把dtb从resource分区里提取出来或者重新编译一份适配Linux引导方式的dtb。注意Android boot.img的dtb存放格式和Linux extlinux引导方式并不完全一样直接拿来用还可能因为U-Boot版本差异导致加载失败。后面第4部分我会专门讲不同引导方式下dtb该怎么放。2. 从dts到dtb编译环境搭建与三种常用编译姿势dtb的全称是Device Tree Blob本质上是由dts源码经dtcDevice Tree Compiler编译而来的二进制数据。在RK3399平台上dts源文件分散在内核源码的arch/arm64/boot/dts/rockchip/目录里相关头文件在include/dt-bindings/下。所以编译dtb的最自然方式就是拿到对应的内核源码在里面执行编译命令。2.1 环境准备交叉编译工具链与依赖RK3399是arm64架构而你通常在x86_64的PC上开发所以需要aarch64交叉编译工具链。Ubuntu/Debian下直接装sudo apt install gcc-aarch64-linux-gnu device-tree-compiler libssl-dev flex bison有一个细节值得提醒如果你只是编译dtb理论上不需要完整工具链因为dtc是内核源码里自带的在scripts/dtc/dtc。但你总得先生成内核配置文件这个过程中Kbuild系统会检查gcc是否存在所以建议还是老老实实把交叉编译器装上避免出现一些奇怪的报错。2.2 拿到正确版本的内核源码Rockchip官方内核仓库地址是https://github.com/rockchip-linux/kernel分支名通常是develop-4.4、develop-4.19、develop-5.10这些。选分支时不要只看版本号还要留意你这个板子/方案商用用的内核版本。举例来说以前很多RK3399安卓设备出厂用4.4内核但Rock Pi 4官方Debian系统用5.10内核两者dts文件结构和可用的外设支持差很多。拉代码时建议加上--depth1省时间git clone --depth1 -b develop-5.10 https://github.com/rockchip-linux/kernel.git cd kernel2.3 编译dtb的三种姿势方式一直接编译指定的dtbexport ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make rockchip_defconfig make dtbs -j8执行完之后生成的dtb文件在arch/arm64/boot/dts/rockchip/目录下。如果只想编译某个具体的板级dtb可以指定目标文件名make rockchip/rk3399-evb.dtb注意目标路径是相对于arch/arm64/boot/dts/的。命令执行完同样在该子目录下找到rk3399-evb.dtb。方式二从Image编译脚本里把dtb带出来如果你平时烧写固件习惯用的是Rockchip官方的mkimage脚本会发现有些脚本里已经把dtb打包进boot.img。这时候你不需要单独拿dtb只要重新执行脚本生成boot.img烧写时一并烧掉就行。这种方式适合Android固件流程不适合Linux手动挂载分区的场景。方式三单独用dtc命令编译严格来说这种方式不是主流因为dts文件里大量宏定义和include需要内核Kbuild系统先做预处理你手动用dtc命令编译时还得处理那些头文件路径非常容易出错。我一般只在需要快速验证一个临时修改时才这么用而且也要先用cpp做预处理。正常开发还是推荐方式一。2.4 编译结果的快速自查拿到dtb之后不要急着烧先用fdtdump看一眼内容是否正常。dtc工具安装后自带fdtdumpfdtdump rk3399-evb.dtb | head -100正常情况下能看到model节点、cpus节点、内存节点、以及各种外设节点的描述信息。如果输出一堆乱码或者提示“failed to parse”说明dtb文件本身就是坏的烧进去大概率也起不来。还有一个更直接的自查手段在设备树源文件里加一个临时节点再编译然后确认编译产物里确实有这个节点的字符串strings rk3399-evb.dtb | grep my-test-node这一步是为了确认内核Kbuild系统确实重新编译了你的dts而不是用了缓存的旧产物。实际开发中我至少遇到三次“改了dts没生效”最后都发现是make dtbs没有重新编译目标文件。3. 改dts之前必须先搞懂的设备树依赖链很多新手一上来就打开dts文件找外设节点改完statusokay就完事了。这在简单场景下可能没问题但RK3399的设备树是分层结构的盲目修改很容易引发资源冲突。3.1 从rk3399.dtsi到板级dts的覆盖关系RK3399的设备树文件分两层soc级文件rk3399.dtsi定义了SoC内部所有控制器、中断号、时钟ID、引脚复用关系板级文件比如rk3399-evb.dts通过#include rk3399.dtsi引入基础描述然后再用uart0、i2c1这类语法对具体节点做覆盖。这种分层逻辑很像代码里的基类和派生类SoC级的东西尽量不动板级差异都在dts里覆盖。比如rk3399.dtsi里uart0默认statusdisabled板级文件里如果你想用就需要写uart0 { status okay; };如果某个引脚既被UART0用又被I2C0用而且配置在同一个dts里那么编译时不会报错但内核pinctrl子系统在初始化时会发现pin已经被claim后续申请的那个设备会静默失败。这类问题最难排查因为没有任何显性报错。3.2 compatible字符串与内核驱动的匹配机制内核里每一个设备驱动都会声明自己支持的compatible列表。dtb里某个节点必须包含对应的compatible字符串驱动才会尝试去绑定它。RK3399的板级dts里根节点下的compatible通常长这样compatible rockchip,rk3399-evb, rockchip,rk3399;前一个是板级后一个是SoC级。这里有个实际用途内核启动早期会打印“Machine model: Rockchip RK3399 Evaluation Board”这类信息。如果你烧了错误的dtb启动日志里的Machine model会暴露出来。这也是我后面要讲的验证手段之一。再深入一层同是RK3399的板子USB PHY、HDMI PHY、DP这些节点的驱动依赖的是SoC级的compatible所以换板级dtb一般不直接影响。真正容易出问题的是电源管理、DDR参数、以及由板级文件额外添加的节点比如某个GPIO控制的设备使能脚。3.3 引脚复用pinctrl的核心配置逻辑在RK3399上改配外设本质上改的是pinctrl。每个引脚可以工作在GPIO、UART、I2C等功能模式之一称为function这里举一个具体示例。假设要把UART4的TX/RX从默认引脚挪到GPIO2_C3和GPIO2_C4你需要先在板级dts里定义一组新的pinctrl配置pinctrl { uart4 { uart4_xfer_alt: uart4-xfer-alt { rockchip,pins 2 RK_PC3 2 pcfg_pull_up, 2 RK_PC4 2 pcfg_pull_none; }; }; }; uart4 { pinctrl-names default; pinctrl-0 uart4_xfer_alt; status okay; };关键在rockchip,pins那一行的三个参数第一个是GPIO bank号第二个是引脚号第三个是复用功能号。RK3399的GPIO bank 0到4每组有A/B/C/D四组每组8个引脚。RK_PC3就表示bank 2的C组第3脚。功能号要根据RK3399的TRMTechnical Reference Manual里的引脚复用表确定填错的话轻则引脚不工作重则驱动申请pin时直接报错。3.4 板级dts常见头文件的引用匹配RK3399的dts文件里经常能看到#include dt-bindings/pinctrl/rockchip.h和#include dt-bindings/gpio/gpio.h。前者定义了RK_PA0、RK_PB1这些宏后者定义了GPIO_ACTIVE_HIGH、GPIO_ACTIVE_LOW。如果你改完dts后编译器报宏找不到不用怀疑一定是少了头文件include。同样dt-bindings/clock/rk3399-cru.h定义了时钟IDdt-bindings/interrupt-controller/arm-gic.h定义了中断类型。反正只要你在dts里用了宏务必确认对应头文件已经在文件头部被包含否则编译到一半就断而且报错信息经常定位不到真正原因。4. 烧进设备不同启动介质和分区下的dtb更新操作这是整篇文章实操性最强的一部分。RK3399的启动介质可能是eMMC、TF卡、或者通过USB从主机烧录每种方式下dtb的存放位置和更新命令都不一样。下面按常见方案分别说。4.1 ext4 boot分区方案Rockchip官方Linux固件常用Rockchip官方Debian固件一般把boot信息放在一个ext4分区里分区内包含Image内核镜像、dtb文件、extlinux/extlinux.conf引导配置。更新dtb的步骤很直观# 假设TF卡或eMMC的boot分区是mmcblk0p2 sudo mount /dev/mmcblk0p2 /mnt/boot sudo cp rk3399-evb.dtb /mnt/boot/rk3399-evb.dtb sync sudo umount /mnt/boot注意拷贝完之后还要检查extlinux.conf里FDT那一行。文件内容通常长这样label kernel-5.10 kernel /Image initrd /initrd.img fdt /rk3399-evb.dtb append earlyconuart8250,mmio32,0xff1a0000 root/dev/mmcblk0p3 rw rootwaitFDT那一行必须指向你实际拷贝进去的dtb文件名。如果文件名对不上U-Boot会下载一个“couldnt find DTB”的提示或者干脆按默认路径找找不到就直接挂了。4.2 Android resource分区方案如果设备上是Android固件dtb通常不在ext4分区里而是在resource分区里。这种情况下直接mount是看不到dtb文件的需要先取得分区镜像再用工具解包或者直接写入。比较顺手的做法是在Linux主机上安装Rockchip提供的upgrade_tool工具然后通过USB连接设备到Loader模式烧写。以resource.img为例# 把dtb打包成resource.img需要Rockchip的resource_tool工具 ./resource_tool --dtbnamerk3399-evb.dtb --pack # 通过upgrade_tool写入 sudo upgrade_tool di -resource resource.img如果是Android系统已经能正常起来用adb也能写adb push rk3399-evb.dtb /data/local/tmp/ adb shell dd if/data/local/tmp/rk3399-evb.dtb of/dev/block/by-name/resource convfsyncdd操作完建议再执行一次reboot。注意dd写的是raw二进制所以dtb文件本来多大resource分区对应偏移处就会被改多大务必保证dtb文件没有超出resource分区的容量。4.3 TF卡启动时dtb放在哪里TF卡启动的RK3399系统其实还是要看引导方式。如果TF卡里放的是从官方SD卡镜像直接dd出来的那卡上会有多个分区boot相关文件在一个FAT或ext4分区里更新方法和4.1里的ext4方案一致。如果是你自己给人用Buildroot做的TF卡镜像要么用extlinux方式要么用U-Boot脚本配合boot.scr读取dtb。还有一种我自己常用的临时验证方式U-Boot的env支持动态加载在U-Boot命令行里手动指定dtb在TF卡FAT分区里的路径setenv fdtfile rk3399-evb.dtb load mmc 1:1 $fdt_addr_r /rk3399-evb.dtb bootm $kernel_addr_r - $fdt_addr_r这种用法适合临时验证不适合量产流程。量产还是要保证extlinux.conf或boot.scr里配置好默认加载路径。4.4 烧写前的备份与回滚准备这句很重要因为我第一次烧dtb时差点把板子变砖。最关键的一个原则是不要只更新dtb把内核镜像、U-Boot都留一套备份。对于ext4 boot分区方案备份很简单sudo cp /mnt/boot/rk3399-evb.dtb /mnt/boot/rk3399-evb.dtb.bak对于Android resource分区方案先把原resource分区dump出来adb shell dd if/dev/block/by-name/resource of/data/local/tmp/resource_orig.img adb pull /data/local/tmp/resource_orig.imgdtb更新失败最常见的表现是内核起不来或者某个外设挂了。如果只是外设问题还能救回来如果整个起不来手边又没有备份就只能重新找完整固件烧录时间成本高很多。5. 更新后启动失败三类典型故障的完整排查记录这一节把我在RK3399实际调试中遇到的失败经验分类拆解每一类都给出完整的排查链路。建议你遇到问题时不要急着重烧整包固件先从日志和现象判断根因。5.1 类型一U-Boot阶段就找不到dtb现象上电后串口停在U-Boot提示无法读取设备树或者直接进入U-Boot命令行没有继续引导内核。排查链路第一步看U-Boot环境变量和存储设备。在U-Boot命令行执行ls mmc 1:2 /确认boot分区是否存在、是否挂载成功、能否看到dtb文件。第二步检查extlinux.conf或boot.scr里的路径写没写对。这里有个容易忽略的点U-Boot对分区的编号是 mmc 1:2 这种格式第一个数字是设备号0通常是eMMC1通常是SD卡第二个是分区号。如果你板子上eMMC里也有系统TF卡里也有系统设备号顺序可能和你想的不一样。第三步确认U-Boot环境变量fdtfile是否被配置。有些U-Boot版本默认从环境变量里取dtb文件名而不是直接在extlinux.conf里写死路径。执行printenv fdtfile看看是否有值。没有的话setenv fdtfile rk3399-evb.dtb然后saveenv。5.2 类型二内核启动早期解析设备树报错现象U-Boot正常内核已经打印出Uncompressing Kernel...或Booting Linux但很快就出现类似“FDT: Failed to parse”或“unflatten: error”的报错卡死。这类问题的根因通常是dtb二进制本身有问题或者U-Boot传给内核的地址不对。排查链路第一步复查dtb文件大小。在PC上执行ls -l rk3399-evb.dtb正常RK3399的dtb大小在40KB到100KB之间。如果只有几KB八成是编译没完成或者文件被截断。第二步用fdtdump重新解析dtb文件确认其格式完整。第三步检查U-Boot加载dtb的内存地址。RK3399的U-Boot里常见变量是fdt_addr_r一般设为0x01f00000或类似地址。如果这个地址和内核镜像地址重叠内核在解压阶段可能会覆盖dtb内存区域导致后续解析失败。两个地址务必分开。5.3 类型三dtb是新的但某个外设静默失效现象系统正常启动但你要改的那个外设没起来dmesg里还没什么明显报错。一个典型的例子是调试串口静默。在RK3399设备树里调试串口不是靠statusokay就能解决的问题它还需要关注U-Boot里的earlycon地址。比如rk3399的uart0物理地址是0xff180000uart2是0xff1a0000。如果你在dts里把调试串口从uart2换成了uart0内核启动参数里的earlycon如果还写0xff1a0000那么内核启动早期的输出依然会走uart2结果就是uart0那边一片静默看起来像改失败了。另一个静默失效案例是GPIO申请失败。dmesg里可能出现类似“gpio: pin GPIO2_C3 already requested”的信息但不一定会打印警告级别以上很容易被忽略。排查时用dmesg | grep -i gpio dmesg | grep -i pinctrl翻一遍这些日志基本上能定位是哪个驱动或者节点抢占了引脚。5.4 排查思路总结一张表定位启动失败环节现象优先怀疑环节首查命令/手段U-Boot界面卡住找不到文件U-Boot配置、文件路径U-Boot执行ls查看分区内容内核解压后卡死解析设备树报错dtb格式、加载地址PC端fdtdump、U-Boot检查fdt_addr_r系统启动但外设失效pinctrl冲突、compatible不匹配dmesg | grep -i pinctrl启动earlycon无输出内核参数earlycon地址检查bootargs里的物理地址以太网/HDMI这类低速外设异常板级dts差异用strings比较dtb里的节点字符串这张表不是万能药但能帮你快速缩小范围。嵌入式调试最怕的就是乱试一定要靠日志和现象逐步收敛。6. 验证更新是否生效的几种可靠手段很多开发者烧完dtb重启后看一眼系统能起来就觉得完事了。但“能起来”和“dtb正确生效”是两回事。我总结了几个验证手段按效率排序。6.1 读取设备树的运行时表现内核启动后会把dtb解析成devicetree可以通过两个路径访问一个是/proc/device-tree另一个是/sys/firmware/devicetree/base。直接看model节点cat /proc/device-tree/model如果输出的是你板子的型号说明U-Boot加载的确实是这份dtb。接着查看你修改过的节点是否在运行时设备树里ls /proc/device-tree/比如你新增了一个节点叫my-test-node在/proc/device-tree/下应该能看到对应目录。这一步能确认内核确实解析到了你改的内容。6.2 确认pinctrl和gpio的实际状态修改过引脚复用后强烈建议检查内核的pinctrl调试接口。前提是内核开启了CONFIG_DEBUG_FS然后mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins输出里每个引脚的占用情况、当前function都会列出来。比如你期望GPIO2_C3处于UART4功能这里会显示Pin 83之类的编号对应uart4-xfer-alt。GPIO状态确认则用cat /sys/kernel/debug/gpio可以看到每个GPIO被谁占用、方向、逻辑电平。6.3 外设功能实测验证环节不能只停留在软件层。比如你改的是网卡PHY的复位引脚直接ping外部主机确认网络通不通改的是UART4调试口用USB转串口模块接上TX/RX做一次回环测试# 打开两个终端 cat /dev/ttyS4 echo test /dev/ttyS4能收到字符就说明UART4确实工作了。注意RK3399的UART设备节点名可能是ttyS0到ttyS4具体映射关系由内核早期的ttyS分配决定接到终端上看实际注册名最准确。6.4 避免“验证成功”的假象有一个常见的误判场景修改了某个设备的reg地址或irq号却还是能启动就以为dtb生效了。其实如果这个设备没有驱动匹配或者modaliases里没提示加载内核根本不会创建平台设备你看到的“正常启动”可能只是它本来就没加载。更稳妥的验证方式是临时删除一个设备节点观察内核是否报错或者修改一个非常明显的属性比如改成不存在的时钟频率看启动日志里是否有对应的警告。测试完再改回来这样能确保设备节点确实是被内核读取和使用的。说到底验证的本质是“改变要能被观测到”不是“看起来没变坏就是成功”。更新dtb是RK3399开发里很小的一件事但它在整个启动链路里牵一发而动全身。我个人的习惯是每次更新前先把原dtb备份到板子的另一个目录同时把编译用的内核源码仓库tag或commit号记下来方便回滚时知道对应的产物是哪一份。这个习惯帮我避免过多次无谓的返工。如果你在更新dtb过程中也遇到过哪些奇怪的bug欢迎留言交流我踩过的坑说不定正好能帮上你。
返回列表