OpenHarmony设备树DTS修改实战:从定位到编译验证全流程

OpenHarmony设备树DTS修改实战:从定位到编译验证全流程 鸿蒙开发越深入就越绕不开设备树。尤其是当你拿到一块新板子或者想在一个官方开发板上外接一个不太常见的传感器、屏幕、模组时十有八九最后都会落到同一个问题上DTS怎么改我见过不少人卡在这一步卡了很久——不是不会写驱动而是根本不知道怎么让内核认出新硬件。这篇就把我在OpenHarmony上改设备树DTS的完整思路和操作过程梳理一遍从文件在哪、怎么改、怎么编译到怎么验证一条龙讲清楚。1. 先搞明白DTS在OpenHarmony开发里到底管什么1.1 设备树的本质不是配置文件而是硬件的地图很多刚开始接触OpenHarmony的开发者会把设备树误解成一个普通的配置文件觉得它跟JSON、ini差不多改一改就能生效。这个理解方向不太对。设备树本质上是描述整块板子硬件资源的一张地图内核启动的时候需要靠这张地图找到内存有多大、有几个UART、GPIO哪些引脚被复用了、I2C控制器挂在哪个地址上、中断号是多少。在OpenHarmony的ARM和RISC-V平台上内核和硬件之间的信息沟通基本都靠设备树来完成。比如你写了一个HDF驱动驱动代码里并不会写死寄存器的物理地址是0x12340000而是通过设备树节点拿到这个地址。如果设备树里的信息是错的驱动代码写得再对也跑不起来。这也就是为什么修改DTS会成为OpenHarmony硬件适配中最常见的操作——因为所有硬件资源的定义都集中在这里不是散落在驱动代码里而是先在这里登记再由内核把资源分配给各个驱动。1.2 哪些场景必须动DTS我自己在实际项目里遇到的需要改DTS的场景大概有这么几类板级外设适配比如官方开发板只引出了一路SPI但你的项目需要两路SPI这时候就要去看第二路SPI的控制节点是否已经被复用成GPIO了如果是就得改引脚复用关系。屏幕点不亮调试LCD屏上电时序、背光引脚、reset引脚、分辨率时序参数这些全部定义在设备树节点里。很多屏点不亮不是驱动的问题而是DTS里某个GPIO编号写错了。内存和DDR参数调整外接大内存或者改DDR频率时需要动memory节点和dramc相关节点。传感器接入加速度计、陀螺仪这类外设通常驱动代码是现成的你只需要在DTS里加一个I2C子节点填上I2C地址和中断引脚就能用起来。调试引脚功能切换有时候想把某个默认的调试串口引脚释放出来当作普通GPIO使用也要改DTS。提示如果你在做x86平台的OpenHarmony适配情况会稍有不同——x86传统上偏向用ACPI描述硬件但在OpenHarmony生态里ARM和RISC-V芯片才是DTS的主力战场所以这篇的实操部分主要围绕这两种架构展开。2. 在工程里定位DTS目录结构、编译产物与板型对应关系2.1 按图索骥从内核仓库找到你的板型DTSOpenHarmony的工程目录和普通单片机工程不同它是仓库套仓库的结构。DTS文件不是统一放在某一个地方而是跟着内核版本走具体路径在kernel/linux/下面。比如你用的内核版本是5.10那么DTS一般会在这两个地方之一kernel/linux/linux-5.10/arch/arm64/boot/dts/ kernel/linux/linux-5.10/arch/arm/boot/dts/进到这个目录后你会发现DTS文件不是只有一份而是按厂商、板型分了多层目录。以全志、瑞芯微、海思这些常见厂商为例目录结构一般长这样arch/arm64/boot/dts/sunxi/ ├── sun55i-w3.dtsi ├── sun55i-aosp.dtsi └── boards/ ├── sun55i_awol_aosp.dts └── ...其中.dtsi是公共部分描述同一颗SoC内部的所有硬件资源比如CPU核数量、内置外设控制器、中断控制器这些对于所有使用同款芯片的板子基本是通用的。而.dts是板级文件描述具体某一款开发板上的差异部分比如板载了哪个型号的LCD、用的是哪颗Codec、某个GPIO接了什么按键。注意在OpenHarmony的很多开发板上编译时会把多个.dtsi叠加到.dts里最终多个文件的内容会被合并成一个完整体。所以你改一个引脚可能改的是.dtsi而不是.dts这得先定位清楚。2.2 dts、dtsi、dtb、dtbo之间的关系这四个后缀名我相信看晕过不少人。简单梳理一下.dts是设备树源文件给人看的。.dtsi也是设备树源文件但它是被包含的文件公共部分放这里可以理解成头文件。.dtb是编译后的二进制文件内核启动时实际加载的就是它。.dtbo是设备树覆盖层文件运行时动态加载用可以在不改主设备树的情况下叠加新硬件描述。在OpenHarmony的构建流程里.dts/.dtsi会被dtc编译成.dtb然后打包进boot_linux.img这个启动镜像里。这也是为什么很多人改了DTS后单独编译内核却还是老样子——因为你虽然改了源码但没有重新打包boot镜像设备树内容根本没进去。2.3 从.config和defconfig反查板型有些时候你手头的一个系统镜像对应的是哪块开发板、用的是哪个DTS光看目录是猜不出来的。这时候需要借助内核配置来看。在kernel目录下执行make ARCHarm64 xxx_defconfig grep CONFIG_DEFAULT_DEVICE_TREE .config执行完就能看到类似CONFIG_DEFAULT_DEVICE_TREEsun55i_awol_aosp这样的输出这就锁定了编进镜像的默认设备树是哪一个。这个方法在排错时特别有用能帮你快速定位我明明改了文件为什么编译产物不对的问题——大概率就是改了另一块板子的DTS。3. 按场景修改DTS引脚复用、外设节点与板级参数调整3.1 GPIO引脚复用最高频的修改点先讲最常遇到的情况一个引脚当前被分配给了某个外设但你想让它干别的活。比如某块板子的UART3_TX被默认复用成了GPIO导致串口根本发不出数据。在DTS里引脚复用通常体现在两个地方pinctrl节点下定义引脚的复用状态和上下拉配置外设节点中的pinctrl-0属性引用这个状态。一段典型配置长这样uart3 { pinctrl-names default; pinctrl-0 uart3_pins; status okay; }; pinctrl { uart3_pins: uart3_pins { pins PH0, PH1; function uart3; bias-pull-up; }; };这里每一行的含义分别是pins指定要配置的物理引脚比如PH0、PH1。function指定这个引脚充当的功能是UART3还是GPIO具体可填的值取决于芯片的pinctrl驱动。bias-pull-up设置内部上拉防止引脚悬空导致电平不稳定。status okay使能节点改成disabled则屏蔽对应外设。如果你想把这两个引脚改成普通GPIO输出只需要把function换掉并去掉外设节点对它们的引用然后在GPIO子系统里重新申请uart3 { status disabled; }; pinctrl { gpio_led_pins: gpio_led_pins { pins PH0, PH1; function gpio; }; };3.2 添加一个I2C外设节点从一颗传感器说起假设板子上有一颗BME280温湿度传感器挂在I2C0总线上I2C地址是0x76INT引脚接到了PH3。驱动代码如果有现成的你只需要在DTS里添加这么一个节点i2c0 { status okay; clock-frequency 400000; bme28076 { compatible bosch,bme280; reg 0x76; interrupt-parent pio; interrupts 7 3 IRQ_TYPE_LEVEL_LOW; status okay; }; };几个字段逐个拆开说bme28076节点名后面跟的是I2C从设备地址方便阅读也可以不写。compatible驱动匹配的关键字段。内核和HDF框架会拿这个字符串跟驱动里的of_device_id表做比对对上了才binding。regI2C设备地址必须是7位地址别把8位地址填进去这是个高频坑。interrupt-parent中断控制器通常指向pio。interrupts中断号、中断类型。具体数字含义需要查芯片手册不能凭空编。加完节点后如果驱动已经编进内核重启系统后在/proc/device-tree/目录下应该能看到bme28076这个子目录说明内核已经识别到节点了。3.3 调整内存大小和启动参数有些项目拿到板子会改内存比如原来的开发板焊了512MB DDR你换成了1GB。这时候如果不改DTS里的memory节点内核只会把512MB范围之外的区域当作不存在白白浪费一半内存。在DTS里通常是这样的memory40000000 { device_type memory; reg 0x0 0x40000000 0x0 0x40000000; };这行reg的含义是起始物理地址0x40000000大小0x40000000也就是1GB。如果你需要改成2GB按起始地址高32位 起始地址低32位 大小高32位 大小低32位的格式写成reg 0x0 0x40000000 0x0 0x80000000;另外chosen节点里的bootargs也经常会被改比如调整内核日志级别、关闭某些子系统、指定根文件系统分区等chosen { bootargs consolettyS0,115200 root/dev/mmcblk0p5 rw; };3.4 时钟与电源域这块需要边查手册边改DTS里还有一类修改涉及时钟频率和电源域控制。比如你想把某个外设的工作时钟从24MHz改成48MHz通常会在时钟控制器节点里配置assigned-clocks和assigned-clock-ratesspi1 { assigned-clocks ccu CLK_SPI1; assigned-clock-rates 48000000; status okay; };这里需要你查芯片手册确认时钟ID号CLK_SPI1这个宏定义在include/dt-bindings/clock/头文件里编译DTS的时候会用到。要是ID写错了编译不会报错但运行时spi时钟会乱数据收发全部错位——这种问题排查起来特别费劲我建议你在改之前先确认宏定义的真实数值。4. 编译、打包与烧录如何让修改真正生效4.1 单独编译dtb再验证语法设备树语法写错是家常便饭最常见的是少了分号、多写了逗号、或者引用了不存在的宏。在OpenHarmony的构建流程里可以单独编译一下dtb来快速验证语法是否正确cd kernel/linux/linux-5.10/ make ARCHarm64 sun55i_awol_aosp_defconfig make ARCHarm64 dtbs编译完成后产物在arch/arm64/boot/dts/对应目录下。如果你用的不是make直接编译而是hb构建整套流程会变成hb build -f -T kernel不管用哪种方式核心目的都是先确认DTS能不能被成功编译成.dtb。如果语法有错编译会直接中断并且报错信息里会精确指向某个文件某一行比如Error: arch/arm64/boot/dts/sunxi/xxx.dts:123.1-2 syntax error FATAL ERROR: Unable to parse input tree这种报错基本跟着行号改就行最怕的是编译能过、启动行不通的隐性错误后面专门讲。4.2 反编译dtb核对真实生效内容编译通过不等于你写的节点真的生效了。我有个习惯编译完dtb后再用dtc把它反编译成dts确认关键内容是不是真的按预期编进去了。这个习惯帮我避开过好几次改错文件的问题。dtc -I dtb -O dts -o out.dts arch/arm64/boot/dts/sunxi/sun55i_awol_aosp.dtb grep -n bme280 out.dts如果反编译结果里能看到你新增的节点说明它已经在这个dtb里面了后续只需要确保dtb被正确打包进boot镜像。4.3 重新打包boot镜像并烧录在OpenHarmony工程里dtb一般不会单独烧录而是和内核一起打包进boot_linux.img。因此修改完DTS并且编译出新的dtb后还得重新生成boot镜像。通常的顺序是hb build -f -T kernel hb build -f -T boot_linux_img然后把生成的新boot_linux.img用升级工具烧录到开发板的boot分区。这里有个非常容易踩的坑有些平台在烧录时会做镜像校验如果你只烧了boot而没有同步烧录其它配套镜像可能出现启动失败或者某个外设起不来。注意如果你的目标平台支持设备树overlay动态加载即.dtbo那就不需要重新烧录整个boot分区只需要在系统起来后把新的dtbo覆盖到/vendor或/data分区的指定目录下再重启即可。这种方式在调试阶段特别省时间强烈推荐。4.4 运行时验证进系统后怎么确认DTS生效烧录并开机后验证DTS是否生效有几种手段ls /proc/device-tree/内核会把设备树内容暴露在这个目录下能看到所有节点权限不够时文件会显示为r--r--r--看不全内容但节点结构是能看到的。cat /proc/device-tree/model如果这个文件的内容和你的板子型号一致说明加载的dtb是对的。dmesg | grep -i bme280\|i2c驱动加载的过程会留日志如果设备树匹配成功但驱动初始化失败这里能看到报错。通过HDF框架的调试接口OpenHarmony里很多外设驱动走的是HDF框架设备树节点即使匹配上了还需要看/sys/kernel/debug/hdf/下的调试信息才能确认驱动真的完成了初始化。这个验证环节千万不要省——很多时候你改完DTS自以为成功了实际上系统加载的还是一份旧的dtb原因可能是镜像缓存、烧录工具选了错误的分区或者编译时用了另一份设备树。5. 避坑实录这几个问题最容易让人卡壳5.1 同名节点被覆盖改了等于没改设备树允许在不同层级的.dtsi文件里定义同名节点后面解析的会把前面解析的覆盖掉。这意味着你可能在板级.dts里辛辛苦苦配了一堆引脚结果被SoC的.dtsi里某个同名的uart3节点里的status disabled给盖掉了看起来改了却完全没生效。排查方法也很直接反编译最终生成的dtb搜一下你关心的节点名看status字段的值到底是多少。记住你写的.dts源码并不是最终产物被合并、覆盖后的dtb才是内核真正看到的。5.2 pinctrl与GPIO子系统的打架这类问题在引脚的配置上很典型DTS里同时有GPIO控制节点和外设pinctrl节点都引用了同一个引脚结果就是系统启动时报类似pin PH0 already requested的警告或者外设功能时好时坏。我遇到过一个案例某块板子的LED用的是PH0但I2C1的引脚也被误配成了PH0结果一开I2C通信LED就乱闪。查了半天才发现是同一个引脚被两个子系统同时占用内核没有强制仲裁这种冲突只是打印一句警告就继续运行了。建议改DTS时先全局搜索一下目标引脚有没有被其他地方引用过。比如要使用PH0就搜PH0这个字符串所有引用它的地方都能看到一次性排查完再动手。5.3 改了DTS但是外设还挂死先查interrupt和regreg地址写错和interrupts中断号写错是驱动挂死的两大元凶。I2C从设备的地址尤其容易出错很多传感器数据手册里写的地址是8位格式比如0xEC但在设备树reg里必须填7位格式0x76直接把0xEC填进去会导致地址不匹配驱动一直报NACK。中断号的问题也一样不同厂商的中断控制器编号方式差别很大有的是全局中断号有的需要按bank计算。你宁可多花几分钟查芯片手册也别靠猜的——我之前就靠猜填错过一次中断号驱动一进中断就死循环最后只能在中断处理函数里加打印才定位出来。5.4 编译缓存导致昨天改的今天怎么还原了OpenHarmony的构建系统在某些情况下会对内核编译产物做缓存特别是当你用hb构建并且没有指定-f强制全量时可能出现源码改了、编译没执行的情况最终打包进镜像的还是旧dtb。我的习惯是改完DTS之后用小范围全量编译的方式强制刷新hb build -f -T kernel如果时间允许干脆把整个内核目录的.o文件清掉再编一次确保万无一失。别觉得这样浪费时间跟烧录后发现改了个寂寞相比重新编译节省的时间要多得多。5.5 怎么判断问题到底出在DTS还是驱动最后分享一个排查思路。很多人遇到外设不起来第一反应就是改驱动但改来改去也没用其实问题在DTS。反过来也有人反复调DTS其实驱动代码就有问题。区分这两者的一个有效方法在驱动初始化入口加一条打印或者用HDF框架自带的调试节点查看设备是否被成功match。一般来说如果你在/proc/device-tree/下能看到对应节点说明内核层面已经解析到设备树内容了如果驱动对应的compatible字符串能在/sys/bus/platform/devices/下找到同名设备说明设备树和驱动的匹配链路是通的。走到这一步还没反应问题大概率在驱动代码本身。这个判断顺序能省掉很多无用功。我个人在OpenHarmony上做硬件适配时一直把DTS当成“硬件的接口契约”来看待——它不光是驱动跑的依赖也是不同团队协作时对齐硬件信息的地方。改DTS本身并不难难的是搞清楚现状、看清合并规则、验证是否真正生效。把这套流程走熟了很多板级适配问题都能在几分钟内定位到根因。