ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

设备树与Linux驱动:从DTS语法到匹配流程与RK3568实战

设备树与Linux驱动:从DTS语法到匹配流程与RK3568实战 搞嵌入式Linux有一段时间的人应该都经历过第一次打开dts文件的懵明明像个配置文件里面全是树状的节点和属性可驱动代码翻遍整个工程也找不到设备树在哪里“被使用”。尤其用RK3568这类SoC的时候原厂SDK里躺着几十个dts文件OpenHarmony、Linux、Buildroot各一套选了a文件启动不了换了b文件串口没输出到底该用哪个更别提驱动一直报probe失败最后发现只是设备树里一个reg地址写错。这篇文章不打算从设备树规范第一行讲起而是从一个驱动开发者的角度把设备树和驱动的协作机制、语法关键点、匹配流程、改树与排查的完整链路聊透。适用人群很明确刚转Linux驱动的新人面试被问“设备树怎么匹配驱动”答不上来的求职者以及正在被RK3568之类SoC设备树折磨的工程师。1. 设备树是干什么的从板级文件泛滥说起1.1 没有设备树的年代改个引脚要重编内核很多人不理解为什么Linux要引入设备树觉得它是个“多此一举的中间层”。要回答这个问题得先回到ARM Linux那段混乱的历史。在设备树普及之前ARM平台的内核里每个开发板都有一个对应的mach-xxx文件放在arch/arm/目录下。这块板子的内存基址是多少UART0寄存器地址在哪I2C控制器挂在哪条总线上GPIO哪个引脚接了LED全都在C代码里写死。内核启动的时候通过这些板级文件里的machine_desc结构体告诉内核当前跑在什么硬件上。听起来挺合理但问题很快就爆发了一个SoC往往有几十上百个评估板、客户定制板每个板子都要复制一份板级文件改几个宏定义和初始化函数然后编进内核。一个芯片厂商的内核分支里mach-xxx文件能堆出几十上百个大部分内容重复只有引脚配置和寄存器地址不同。更要命的是这种硬件信息散落在各处新板卡bringup的时候工程师要在海量代码里翻找、对比、修改效率极低。最典型的场景就是做单片机转Linux开发的人经常遇到的情况我在一个项目里只改了一个GPIO的拉高拉低逻辑结果需要重新编译整个内核再烧写整个内核镜像。因为板级信息是编译期就固化进内核镜像的运行期没有任何机制让你单独改硬件配置。这种“改一行配置、重编一次内核”的模式在ARM平台多板卡并行开发的现实下是不可持续的。1.2 设备树的核心思路把“硬件配置”从代码里拆出来设备树Device Tree的核心思路其实特别朴素把板级硬件配置从内核源码中彻底剥离出来用一种独立于内核的、结构化的文本格式来描述硬件。你完全可以把它看成是一份“硬件描述清单”。这块板子叫什么名字modelCPU是什么架构compatible内存起始地址和大小是多少reg哪里挂了I2C控制器、上面接了哪些外设哪里挂了SPI哪些引脚被复用成UART功能全都用树形节点和属性表达清楚。内核不再关心“我是跑在哪块具体的板子上”它只负责在启动的时候拿到这份清单解析出内存范围、中断控制器、时钟树、总线拓扑把清单里描述的硬件设备一个个创建出来然后去匹配对应的驱动。这样做的好处非常明显硬件变化不再需要改内核代码只需要改设备树源文件dts重新编译生成dtb甚至可以在bootloader阶段直接指定加载哪份dtb完全不用动内核镜像。一个内核镜像通过搭配不同的dtb就能适配多个板卡。这也是为什么现在SoC厂商发布SDK时经常是同一个内核镜像加一整套dts文件。1.3 DTS、DTC、DTB搞不清这三个文件就别往下看设备树涉及三种形态的文件很多新手在第一步就被这几个后缀绕晕了文件后缀全称说明.dtsDevice Tree Source设备树源文件纯文本给人看和改的.dtsiDevice Tree Source Include公共的、可被include的设备树片段一般是SoC级别的通用描述.dtbDevice Tree Blob源文件经DTC编译生成的二进制文件内核启动时实际使用.dtboDevice Tree Overlay增量补丁式的二进制设备树运行时叠加到主dtb上用.dts文件类似于C语言的.c文件.dtsi类似于.h头文件。SoC厂商会把CPU、中断控制器、时钟、串口、I2C控制器这些“整个SoC都一样的部分”抽到.dtsi里而把内存大小、外设连接、引脚复用这些“每块板子都不同”的部分放到具体板子的.dts里。编译的时候dtcDevice Tree Compiler工具会把include进来的内容合并最终生成.dtb。在RK3568官方SDK里你经常会看到rk3568-evb1-ddr4-v10.dts、rk3568-evb2-lp4-v10.dts、rk3568-nas.dts等一堆文件。它们本质上都是include了rk3568.dtsi这个SoC级描述再加自己的板级差异。选择哪个文件取决于你的板子DDR类型、硬件版本。很多人在OpenHarmony的RK3568项目里不知道选哪个设备树其实就是这一步卡住了先把内存颗粒DDR版本、硬件版本号核对清楚再找对应的dts文件名就不会选错。2. 读懂DTS语法与属性速查拿RK3568实际节点拆一遍2.1 一个最小设备树长什么样设备树源文件的语法很简洁整个文件就是一个树形结构最外层是根节点“/”。每个节点可以包含子节点和属性属性就是“key value”的形式。字符串用双引号数字可以用十进制或十六进制多个值用尖括号包裹。一个最小的、能启动的设备树大概是这个样子/dts-v1/; / { model Rockchip RK3568 EVB1 DDR4 V10 Board; compatible rockchip,rk3568-evb1-ddr4-v10, rockchip,rk3568; chosen { stdout-path serial2:1500000n8; bootargs earlycon consolettyS2,1500000n8 root/dev/mmcblk0p5 rw; }; memory200000 { device_type memory; reg 0x00000000 0x200000 0x00000000 0x8000000; }; leds { compatible gpio-leds; work_led: work { label green:work; gpios gpio4 21 GPIO_ACTIVE_HIGH; default-state on; }; }; };model属性只是一行人类可读的描述内核真正用来做匹配判断的是compatible。compatible字符串的格式一般约定为“厂商,型号”比如“rockchip,rk3568”内核启动时会用这个值和存储的machine描述符做匹配。chosen节点用于传递内核启动参数stdout-path告诉内核早期串口日志输出到哪个串口这些内容在调试最前面阶段非常重要。2.2 节点属性reg/status/pinctrl/时钟中断怎么配刚刚接触设备树的人最容易在reg、status、pinctrl、interrupt这几个属性上卡壳。逐个拆开看其实每个都有明确用途。reg属性用来描述设备在总线上的地址它和父节点的#address-cells、#size-cells配合使用。这两个属性定义地址和大小分别用几个32位整数表示。比如#address-cells2代表地址用两个32位数字拼成的64位地址#size-cells2代表大小也是64位后面的reg 0x0 0x200000 0x0 0x8000000就是在说内存起始物理地址是0x200000大小是0x8000000。status属性只有几个可选值最常用的是“okay”和“disabled”。okay代表节点启用disabled代表停用。驱动开发中最常见的坑之一就是节点配置好驱动也写了但忘了把status从disabled改成okay导致整个设备根本没有被创建。原厂dtsi里很多外设节点默认都是disabled靠板级dts开启这个设计留到了第5节细说。pinctrl属性用来描述引脚的复用和电气属性。它和pinctrl-0、pinctrl-names配合使用。pinctrl-namesdefault表示默认状态的引脚配置系统在设备prob时自动应用对应的pinctrl-0里的配置。在RK3568上SPI引脚如果用错复用组功能就会完全失效这是做板级适配时最需要小心的点。interrupt相关属性描述中断从哪里来。interrupt-parent指向中断控制器节点interrupts则指定中断号和触发方式。在I2C触摸屏等外设节点里这两组属性几乎是标配。用生活化的类比来说compatible是设备的身份证号驱动凭这个找设备reg是地址门牌号告诉驱动去哪里访问硬件status是“营业状态”牌子pinctrl是“闸机通道配置”。四个属性合在一起决定了设备能不能被内核正确识别和访问。2.3 dtsi与dts原厂给你的文件结构怎么组织很多新手第一次打开RK3568 SDK时面对几十个dts文件完全无从下手。这里有个很有用的工作习惯先打开板级dts文件头部看它include了哪些dtsi。典型结构是这样#include rk3568.dtsi #include rk3568-evb.dtsi #include rk3568-linux.dtsi #include dt-bindings/gpio/gpio.h #include dt-bindings/pinctrl/rockchip.hrk3568.dtsi是SoC级描述包含CPU、中断、时钟、串口、I2C、SPI等控制器信息芯片厂商维护一般不去动。rk3568-evb.dtsi则是开发板级描述包含DDR配置、SD卡/eMMC、以太网PHY、USB等等。板级dts最后再针对具体的EVB1、EVB2等硬件版本做微调。这种组织方式最大的好处是层次清晰SoC不可变的部分、评估板通用的部分、具体硬件版本独有的部分各归其位。我们实际开发中新增一个模块只要在板级dts里通过“节点名”的语法对某个节点做追加或覆盖即可。比如要在I2C0上挂一个触摸屏不需要修改rk3568.dtsi直接在板级dts里追加i2c0 { status okay; touchscreen38 { compatible myvendor,ts; reg 0x38; interrupt-parent gpio3; interrupts RK_PA5 IRQ_TYPE_LEVEL_LOW; }; };i2c0的写法表示引用dtsi中已有的i2c0节点后面覆盖或追加属性。这种“引用覆盖”的机制是设备树让板级适配变得轻量的关键。3. 驱动和设备树怎么“牵手”compatible匹配与of_系列API3.1 内核匹配过程设备树节点如何变成设备理解设备树和驱动的关系核心是搞明白一个问题设备树里写了一个节点驱动代码是通过什么路径被调起来的很多人以为驱动会主动去搜索设备树里的某个节点这是个误解。实际流程是这样的内核启动时设备树被解析后里面的节点会被转换成platform_device等设备结构体挂到对应的总线上。比如我们写一个SPI子设备节点内核会通过SPI控制器节点的信息创建出对应的spi_device。而驱动的注册则是向内核登记一个platform_driver或spi_driver结构体。在驱动注册和设备注册这两条线都完成之后内核的总线逻辑会自动做匹配遍历总线上的设备链表对每个设备检查已注册的驱动列表中是否存在compatible字段或name字段与设备匹配的驱动。匹配成功就调用驱动的probe函数。所以更准确的说法是驱动不需要知道设备树节点长什么样它只需要提供一张“我支持哪些设备”的清单也就是of_match_table然后坐在那里等内核来匹配。匹配上了系统就调probe匹配不上驱动就是个“空盒子”什么事都不发生。3.2 驱动侧怎么写of_match_table与probe对应到代码上一个最典型的platform_driver框架是这样的#include linux/module.h #include linux/platform_device.h #include linux/of.h static const struct of_device_id my_led_of_match[] { { .compatible myvendor,myled }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_led_of_match); static int my_led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct device_node *np dev-of_node; u32 index; dev_info(dev, my led probed\n); if (of_property_read_u32(np, reg-idx, index)) { dev_err(dev, no reg-idx property\n); return -EINVAL; } dev_info(dev, reg-idx is %u\n, index); return 0; } static int my_led_remove(struct platform_device *pdev) { return 0; } static struct platform_driver my_led_driver { .probe my_led_probe, .remove my_led_remove, .driver { .name my_led, .of_match_table my_led_of_match, }, }; module_platform_driver(my_led_driver); MODULE_LICENSE(GPL);of_match_table里的compatible字符串和设备树节点里的compatible属性必须完全相等包括厂商前缀和大小写。内核在匹配时的规则是设备树节点compatible属性里通常有多个字符串从“最具体”到“最通用”排列驱动侧的of_device_id表里只要有一个能和其中一个字符串相等就算匹配成功。一个值得注意的细节是驱动注册时还不一定“有设备在等”。设备树解析和驱动加载的先后顺序不是固定的所以匹配成功与否完全由内核总线的动态探测机制决定。这也是为什么驱动做成模块时insmod后经常能看到probe立刻被调起来因为总线在驱动加入时会重新扫描一遍设备链表。3.3 从树里拿数据of_property_read_xxx和gpiod_api设备树和设备“牵手”成功后probe函数里最常做的事就是从设备树节点里读出驱动运行所需的硬件参数。这一组API以of_或者devm_开头非常常用。API作用of_property_read_string(np, label, str)读取字符串属性of_property_read_u32/u64(np, prop, val)读取整数属性of_property_read_u32_array(np, reg, vals, cnt)读取一组整型属性of_n_addr_cells / of_n_size_cells获取父节点的地址/大小单元信息of_get_named_gpio(np, gpios, index)获取GPIO号legacy方式devm_gpiod_get(dev, led, GPIOD_OUT_HIGH)获取GPIO descriptor并处理反转逻辑devm_clk_get(dev, name)获取时钟引用devm_regulator_get(dev, vcc-supply)获取电源regulator引用platform_get_irq(pdev, 0)获取中断号这些API的共同特点是它们都围绕struct device_node工作而这个node就是设备树解析后在内存中的抽象。驱动拿到这些数据之后使用方法跟传统硬编码配置没有任何区别。我经常用这样的类比来解释设备树和驱动的分工compatible匹配就像两个人相亲先看介绍资料上的“标签”能不能对上一旦对上of_系列API就是婚后过日子从家里拿生活费、钥匙、门牌号都是通过这些API从设备树这个“家庭财产清单”里取。如果设备树里没写某个属性probe里又强行去读结果只会得到错误码或者默认值进而导致外设初始化失败而这类失败经常让新手误以为是驱动本身写错了。4. 一个能跑的案例RK3568上通过SPI设备树节点驱动ADC芯片4.1 目标与准备工作理论讲再多不如一个完整案例来得直观。这里用RK3568平台为例在SPI1总线上挂一颗MCP3008八通道ADC芯片写设备树节点和驱动逻辑实现读取模拟电压。选MCP3008的原因很简单它是非常典型的SPI字符设备通信协议简单时序容易理解适合演示设备树、SPI子系统、设备树属性读取三者的协作关系。在做任何修改之前先打开原厂SDK中对应的板级dts文件搜索spi1节点通常能看到这样一段具体命名以你的SDK为准spi1 { status disabled; pinctrl-0 spi1m0_cs0 spi1m0_pins; pinctrl-names default; };注意status是disabled所以第一步就是把节点打开然后根据MCP3008的数据手册在spi1下添加子节点指定SPI模式、最高频率、片选编号。4.2 在板级dts里新增设备树节点修改后的设备树片段如下spi1 { status okay; max-freq 20000000; pinctrl-0 spi1m0_cs0 spi1m0_pins; pinctrl-names default; mcp30080 { compatible microchip,mcp3008; reg 0; /* 片选CS0 */ spi-max-frequency 1000000; vref-supply vcc3v3_sys; }; };reg 0 代表这颗ADC挂在SPI1的片选0上。spi-max-frequency告诉SPI控制器和该设备通信时最高时钟频率是1MHz。有些芯片还要指定spi-mode属性来控制CPOL/CPHAMCP3008手册要求mode 0所以这里也不用额外写默认就是mode 0。这里特别强调两个容易被坑的地方一是reg值必须和实际接的片选引脚对应很多人在I2C设备那里习惯了reg就是设备地址到了SPI里还是顺着写一个地址值结果设备树解析后创建的spi_device首片选完全不对。二是pinctrl里的spi1m0_cs0代表使用M0组引脚复用配置如果硬件设计用的是M1组引脚必须改用spi1m1相关的pinctrl否则IO复用错乱SCLK和MOSI根本不会输出信号。4.3 驱动侧代码骨架匹配、初始化、读写MCP3008挂在SPI总线上所以驱动要注册为spi_driver而不是platform_driver。两者的区别在于platform_driver匹配总线上的platform_device而spi_driver匹配由SPI控制器枚举出来的spi_device。我们在设备树里写的子节点最终会由SPI控制器注册成spi_device因此spi_driver是正确选择。驱动代码骨架如下#include linux/module.h #include linux/spi/spi.h #include linux/of.h static const struct of_device_id mcp3008_of_match[] { { .compatible microchip,mcp3008 }, { } }; MODULE_DEVICE_TABLE(of, mcp3008_of_match); static int mcp3008_probe(struct spi_device *spi) { struct device *dev spi-dev; struct device_node *np dev-of_node; u32 max_freq; spi-mode SPI_MODE_0; spi-bits_per_word 8; spi_setup(spi); if (of_property_read_u32(np, spi-max-frequency, max_freq) 0) dev_info(dev, spi-max-frequency from DT: %u\n, max_freq); dev_info(dev, mcp3008 probed on CS %d\n, spi-chip_select); return 0; } static int mcp3008_remove(struct spi_device *spi) { return 0; } static struct spi_driver mcp3008_driver { .driver { .name mcp3008, .of_match_table mcp3008_of_match, }, .probe mcp3008_probe, .remove mcp3008_remove, }; module_spi_driver(mcp3008_driver); MODULE_LICENSE(GPL);实际读取MCP3008数据时需要先发一个控制字节起始位通道编号然后读回两个字节拼出10位ADC结果。这部分用spi_transfer和spi_sync接口即可流程是标准的SPI读写不是本文重点。真正要理解的是设备树里写的spi-max-frequency和reg经过SPI子系统解析后直接反映在spi_device的max_speed_hz和chip_select字段里probe里读属性只是为了演示of_API的用法很多驱动根本不重复读取直接用SPI框架已经解析好的字段就行。4.4 编译烧录dtb更新链路到底要经过哪几步设备树改完、驱动写完接下来的问题是怎么让板子跑起来RK平台的dtb并不总是一个独立分区有些SDK会把dtb打包进resource分区有些则跟kernel一起打包成boot.img。判断方法很简单看编译产物目录里的文件出现resource.img那就是dtb资源分区出现boot.img则通常是把kernel和dtb合并了。编译设备树的完整流程分两步。先编译设备树源文件make ARCHarm64 dtbs如果SDK自带构建脚本比如Rockchip的Linux SDK直接在kernel目录下执行./make.sh rk3568即可脚本会自动编译内核和dtb。编译完成后在产品固件目录下会生成新的resource.img或boot.img。烧录时可以用RK的upgrade_tool或rkdeveloptool直接烧写对应分区。比如resource镜像使用./upgrade_tool di -r resource.imgboot镜像则换成./upgrade_tool di -b boot.img很多人在这里翻车是因为烧错了dtbRK3568 EVB板有DDR4和LPDDR4两种内存版本烧录前必须确认自己的板子用的是哪种内存颗粒再选择对应的dtb编译产物。UBoot启动时加载哪个dtb有的SDK是在extlinux.conf里指定有的则自动扫描resource分区。如果打印出UBoot的启动日志能看到类似“Load fdt from resource partition”的信息从中就能确认实际加载的dtb文件名这一步是定位“为什么我改了设备树没反应”的关键前提。5. 改完设备树驱动不生效完整排查链路和几个高频坑5.1 第一步确认设备树真的进了内核驱动不生效最怕的不是问题本身而是根本没搞清楚问题在哪一层。所以我建议按链路一级一级排查从最底层的“设备树是否真的被内核加载”开始。先进入板子系统执行ls /proc/device-tree/ cat /proc/device-tree/model如果/proc/device-tree目录下的节点和属性跟你改的设备树一致说明当前内核已经加载了包含修改的设备树。另一个更直观的方法是反编译运行中的dtbcat /sys/firmware/fdt /tmp/fdt.dtb dtc -I dtb -O dts -o /tmp/fdt.dts /tmp/fdt.dtb然后打开fdt.dts搜索你新增的节点。如果节点不在说明要么你操作的不是内核加载的那份dtb要么编译烧录环节出了问题。这时候回看第4.4节提到的uboot日志和resource/boot分区确认有没有烧错文件。还需要特别注意一种情况uboot环境变量里可能写死了fdt_file比如“fdt_filerk3568-evb1-ddr4-v10.dtb”它会直接从boot分区或者FAT分区加载指定dtb覆盖resource分区自带的选择逻辑。你烧了新的resource.img但uboot根本没读它而是从boot分区取了旧文件这就会导致“明明编译了、烧录了却没有任何变化”的灵异现象。5.2 第二步节点在但probe不执行问题多半出在匹配或资源如果/proc/device-tree里能看到节点但驱动始终不跑先别急着看驱动代码按下面顺序来查。先看设备是否真的注册成功。platform设备可以查ls /sys/bus/platform/devices/SPI子设备则查ls /sys/bus/spi/devices/如果你的SPI节点reg 0应该能看到类似spi1.0这样的设备目录。没有这个目录说明设备没被创建那问题出在SPI控制器解析设备树子节点阶段。没有platform设备目录则可能是父节点没有被正确展开此时要检查父节点compatible是否包含“simple-bus”或类似驱动或者需要在内核代码中调用of_platform_populate显式展开。如果设备存在但probe没被调用再看驱动的of_match_table和设备树compatible是否完全一致。有一个很隐蔽的坑设备树里compatible可能写了多个字符串比如“microchip,mcp3008”和“mcp3008”驱动只匹配了后者而内核匹配顺序是从前往后第一个匹配失败后继续找第二个理论上也能匹配上但如果两个字符串在驱动里只填了其中一个另一个又拼错了就会失败。所以排查的时候直接把这行属性复制出来和驱动代码里对比不要肉眼可劲找直接复制粘贴最稳妥。最后看dmesg。probe调用之前的错误比如pinctrl申请失败、GPIO被占用、时钟获取失败都会在dmesg里有线索dmesg | grep -i mcp3008 dmesg | grep -i spi1 dmesg | grep -i pinctrlpinctrl申请失败在RK平台上尤其常见。原因是设备树里在同一组引脚上既被SPI节点用了又被某个GPIO节点或其他外设节点用了内核的pinctrl框架发现引脚已经被claim就直接返回-EBUSY导致probe阶段被中止。5.3 高频坑实例地址写错、status忘记改、address-cells不匹配这里整理几个我实际开发中踩过、也帮别人排查过的高频坑每个都值得单独记一笔。第一个是I2C地址写成SPI片选风格。RK3568上接I2C触摸屏设备树写成了touch0 { compatible myvendor,ts; reg 0x0; };但触摸屏I2C地址实际上是0x38reg 0x0导致I2C核心按地址0去探测设备数据手册上写的0x38完全对不上设备根本枚举不出来。I2C的reg就是芯片的I2C从地址SPI的reg才是片选号。这里没有捷径只能对照原理图和数据手册逐一核对。第二个是改完节点忘了改status。原厂dtsi里外设节点统一默认disabled板级dts通过“节点 { status okay; }”开启。很多人新增了一个子节点在子节点里写了status okay但父节点还是disabled最终功能依旧不生效。检查的时候要从根节点一路看到子节点所有层级的status都得是okay。第三个是#address-cells和#size-cells引起的解析错乱。这个坑藏得很深单看节点本身完全看不出问题。比如某DMA控制器的父节点声明#address-cells2但子设备节点里reg只写了一个地址值解析后地址的第二个word被当成零补上实际地址错位DMA操作直接访问错误内存。遇到这类问题时用dtc反编译运行时设备树查看实际解析后的reg值往往一眼就能发现问题。5.4 常用排查命令与内核开关整理一份我常用的排查命令速查表基本覆盖了设备树和驱动联调的大部分场景命令作用ls /proc/device-tree/查看已加载设备树的节点目录结构cat /proc/device-tree/xxx/name查看节点的name属性cat /sys/firmware/fdt导出运行时实际的FDT供反编译dtc -I dtb -O dts把dtb反编译回dtsls /sys/bus/platform/devices/查看已注册的platform设备ls /sys/bus/spi/devices/查看已注册的SPI设备cat /sys/kernel/debug/gpio查看GPIO申请状态确认引脚是否被占用ls /sys/kernel/debug/pinctrl/查看pinctrl引脚复用状态dmesg | grep -i of_查看设备树相关打印dmesg | grep -i probe查看probe相关错误内核编译时还要确保打开了设备树相关配置。绝大多数情况这些选项默认已开启但裁剪过的内核需要检查CONFIG_OF、CONFIG_OF_GPIO、CONFIG_OF_SPI、CONFIG_OF_IRQ这几个选项。驱动依赖了GPIO、时钟、regulator子系统时对应的CONFIG_GPIOLIB、CONFIG_COMMON_CLK、CONFIG_REGULATOR也都要在。6. 进阶话题与学习建议6.1 Device Tree Overlays为什么新平台都爱用dtbo传统设备树方案里每次改硬件配置都要重新编译整个dtb再烧录整个设备树分区。对产品开发阶段来说反复烧录的代价还不算高但在量产前的适配阶段、或者消费类产品需要同时支持多个外设配置时这种“全量替换”的方式就显得笨重了。Device Tree Overlay提供了另一种思路一个基础dtb描述固定的SoC平台多个dtbo描述可选的板级差异。启动时bootloader先把基础dtb加载进去再根据硬件识别结果叠加对应的dtbo补丁。有点像给手机装系统再打补丁包不需要因为换个摄像头模组就重新打整个系统镜像。RK平台在新版SDK里对overlay支持也比较完善很多硬件版本差异就是通过.dtbo来区分的。不过对刚开始学设备树的人来说我不建议一上来就搞overlay先彻底搞懂基础dtb怎么改、怎么烧、怎么验再去学overlay叠加机制才能理解它的价值到底在哪。6.2 可维护性写设备树也要“面向人类”设备树虽然叫“树”但它最终是给内核解析用的也是给其他工程师看的。我看到过不少产品代码设备树节点命名随意属性拼写错误注释几乎为零等到下一个工程师接手的时候根本不敢动这些节点生怕改坏某个隐式依赖。几个提升可维护性的建议节点命名遵循“名称地址”的规范比如i2c1a00000自定义外设的compatible一定要带厂商前缀不要裸写“touch”、“adc”这种通用词避免和其他厂商驱动冲突关键属性下面写清楚依据例如“spi-max-frequency 1000000; /* MCP3008 datasheet: max 1.35MHz */”不要直接在rk3568.dtsi这种SoC级文件里改板级内容所有板级差异都放到最外层的dts里否则整个SDK升级时你的修改会被覆盖得面目全非。另外如果开发环境支持可以用dt-validate工具配合内核源码里的schema文件对设备树做静态检查。这套校验体系在Linux内核里越来越完善很多语法错误和属性缺失在编译阶段就能被拦下来我们浪费在板子上的调试时间也会少很多。6.3 给新手的建议路线看过太多人接触设备树时直接去背语法或者去啃整本binding文档结果一个月过去能看懂每个属性是什么意思真拿到自己的板子改外设照样抓瞎。据我个人经验最快的学习路径是反着来的找一块RK或者其它常见SoC的开发板从原厂SDK里随便挑一个外设节点LED、按键、I2C触摸屏都行改掉它的compatible、reg、gpio引脚然后观察驱动probe是否变化再用设备树里的属性去驱动一个真实的模块。整个过程不需要读完整本规范但很快就会逼着你掌握compatible匹配、reg寻址、pinctrl复用、GPIO/中断属性这几个核心概念。面试普遍关注的考点也集中在这些地方设备树到驱动的匹配流程、compatible和name的关系、reg和address-cells的组合含义、pinctrl如何影响IO功能、字符设备驱动和设备树如何结合。把这些实操跑通面试时能讲清楚原理比背一百道八股题有用得多。最后再分享一个我自己的习惯拿到一个陌生BSP时第一件事不是翻代码而是把当前运行中的dtb反编译成dts先读懂“现在板子长什么样”再考虑“要改什么”。原因很简单文档可能滞后代码可能被裁剪但运行中的设备树一定和实际硬件配置一致。我的很多外设适配问题都是从这个习惯开始一步步追根因追出来的。
返回列表