ARTICLE DETAIL

资讯详情

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

Linux设备树从入门到实战:dts/dtsi/dtb解析与RK3568驱动开发

Linux设备树从入门到实战:dts/dtsi/dtb解析与RK3568驱动开发 从裸机思维到设备树思维几乎是每个Linux驱动开发者都要迈过的一道坎。我自己当年从写单片机驱动程序转过来时面对dtb、dts、compatible这些概念也是一头雾水花了很长时间才把整个链路理顺。这篇内容就是想把设备树和驱动之间那层窗户纸捅破从为什么需要设备树讲起一直说到驱动代码里怎么解析设备树节点、RK3568这种主流平台上的设备树怎么选型和修改最后再分享一些我实际调试中踩过的坑。无论你是准备入门嵌入式Linux还是已经开始接触设备树但总感觉隔着一层这篇都值得耐心看完。1. 设备树到底在解决什么问题——为什么越新的内核越离不开它先说一个最本质的问题设备树不是什么高深莫测的魔法它本质上就是一份描述硬件信息的配置文件。它解决的核心痛点是Linux内核曾经被各种板级硬件信息搞得乌烟瘴气。1.1 从硬编码到配置化的演进逻辑在早期的ARM Linux内核里每一块开发板想要跑起来都要在arch/arm/mach-xxx/目录下写一大堆board-xxx.c文件。这些C文件里塞满了平台设备结构体、GPIO编号、中断号、寄存器地址、时钟频率等硬件信息。比如你想让内核知道某块板子上有一颗I2C总线上挂着温度传感器就需要在C代码里构造一个platform_device然后用platform_add_devices()把它注册进内核。代码长这样static struct i2c_board_info __initdata xxx_i2c_devices[] { { I2C_BOARD_INFO(lm75, 0x48), .platform_data xxx_lm75_data, }, }; static int __init xxx_board_init(void) { platform_add_devices(xxx_devices, ARRAY_SIZE(xxx_devices)); i2c_register_board_info(0, xxx_i2c_devices, ARRAY_SIZE(xxx_i2c_devices)); return 0; }这种做法最致命的问题就是每一个芯片厂商、每一块开发板都要往内核里灌入自己的私有代码。社区每发布一版内核都要维护成千上万个board文件上游维护者和下游板卡厂商都苦不堪言。而且这些硬件描述和驱动代码强耦合——换一颗Sensor、改一个GPIO都要重新编译内核。设备树出现以后这个局面被彻底扭转。硬件描述从C文件里抽离出来变成一份独立的数据文件DTS内核启动时由bootloader把这份数据以二进制DTB的形式传递给内核。驱动代码不再关心“哪块板子上有什么硬件”只关心“我匹配到什么样的硬件描述”所有的板级差异都被隔离到设备树文件里。驱动编译一次换块板子只需要换DTB内核主线和板级BSP的耦合度大大降低。1.2 设备树文件家族dts/dtsi/dtb/dtc各司其职很多新手面对设备树时会被dts、dtsi、dtb、dtc这几个后缀搞得晕头转向。它们其实分属两个阶段源码阶段和编译阶段。dtsDevice Tree Source设备树的源码文件描述一块具体板卡上有哪些设备、设备挂在哪个总线、基地址和中断是多少。一个dts文件对应一种具体型号的板子。dtsiDevice Tree Source Include设备树公共头文件一般用来描述SoC级公共的内容比如CPU核心信息、SoC内置外设控制器UART、I2C、SPI、DMA、中断控制器等的默认配置。dts可以通过#include语法把它包含进来。dtbDevice Tree Blobdts被设备树编译器编译后生成的二进制文件是bootloader实际加载并传递给内核的东西。内核解析的就是它。dtcDevice Tree Compiler把dts编译成dtb的工具。在Ubuntu上安装device-tree-compiler包就能拿到dtc命令内核源码的scripts/dtc/目录下也有一份。编译链路上最常用的命令是# 编译生成dtb-o指定输出文件名 dtc -I dts -O dtb -o xxx.dtb xxx.dts # 反编译把dtb还原成可读的dts格式 dtc -I dtb -O dts -o xxx.dts xxx.dtb在实际的项目里我从来不会手动一条条dtc命令去编设备树基本都是通过内核的make命令来编。比如你要编译arch/arm64/boot/dts/rockchip/rk3568-evb.dtb只需要在Linux内核源码根目录执行make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- rk3568-evb.dtb它会自动处理所有#include的dtsi和头文件非常省心。不过手动用dtc反编译dtb去确认bootloader实际传递的硬件配置这个习惯倒是值得保留。1.3 设备树的硬件描述层次从SoC到板级理解设备树的另一个关键视角是它的层次化描述模型。看一棵典型的设备树你会发现它长得非常像一颗SoC的“地图”从根节点/开始逐级细化。一个简化的示例/ { compatible rockchip,rk3568-evb, rockchip,rk3568; aliases { serial0 uart0; mmc0 sdmmc0; }; cpus { #address-cells 2; #size-cells 0; cpu0: cpu0 { compatible arm,cortex-a55; reg 0x0 0x0; enable-method psci; }; }; soc { compatible simple-bus; #address-cells 2; #size-cells 2; ranges; uart0: serialfdd50000 { compatible rockchip,rk3568-uart, snps,dw-apb-uart; reg 0x0 0xfdd50000 0x0 0x100; interrupts GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH; clocks cru SCLK_UART0, cru PCLK_UART0; clock-names baudclk, apb_pclk; status disabled; }; }; };根节点下面的cpus描述处理器核心soc节点里的每个子节点对应SoC内部的一个外设控制器。reg描述寄存器地址interrupts描述中断号clocks描述时钟来源。这一层通常放在dtsi里因为同一颗SoC不管放在哪块板卡上这部分内容基本是固定的。板级差异则体现在dts文件中。同一个SoC的同一颗UART在SoC层面可能默认是disabled的到了你的板子上外设被引出来了、接到了调试串口芯片上你就把它的status改成okay再挂上引脚复用信息。这种“SoC描述在dtsi、板级描述在dts”的分层设计就是设备树工程化的基石。2. 驱动侧如何“反向解析”设备树——从匹配到资源获取的完整链路设备树只是描述了硬件长什么样真正干活的还是驱动。那么驱动拿到设备树节点后到底怎么找到对应的设备、怎么拿中断号、怎么拿寄存器基地址、怎么获取时钟这一整套“反向解析”的机制就是设备树驱动开发的核心。2.1 驱动与节点的匹配机制compatible的优先级与板级设备实例化驱动代码和设备树节点建立联系的第一道门是compatible字符串。在内核里一个platform_driver定义一个of_match_table里面列出它支持的compatible值static const struct of_device_id xxx_of_match[] { { .compatible rockchip,rk3568-uart, }, { /* sentinel */ } }; static struct platform_driver xxx_uart_driver { .probe xxx_uart_probe, .remove xxx_uart_remove, .driver { .name xxx-uart, .of_match_table xxx_of_match, }, }; module_platform_driver(xxx_uart_driver);当内核遍历设备树节点时发现某个节点的compatible属性里有“rockchip,rk3568-uart”就会把这个节点和该驱动匹配上然后调用驱动的probe函数。匹配逻辑优先遍历of_match_table里的compatible字符串如果驱动和设备树节点都带有设备ID表i2c_device_id、spi_device_id这种也可以用ID表匹配platform总线还允许通过driver.name与platform_device.name进行匹配。这里有一个新手容易踩的坑compatible第二个字符串“snps,dw-apb-uart”是通用IP核的名字在rockchip,rk3568-uart第一次匹配失败时内核还会尝试用后面的通用字符串去匹配snps那个通用驱动。所以同一个硬件IP核既可以被芯片厂商的专用驱动接管也可以被上游通用驱动接管取决于谁的of_match_table命中优先级更高。实际调试时如果你加载了一个专有驱动却发现probe根本没被调用先查一下是不是另一个通用驱动提前把你“截胡”了。匹配成功后驱动的probe函数接收一个struct platform_device *pdev。这个pdev就是内核根据设备树节点抽象出来的平台设备。驱动后续拿所有资源都是通过pdev-dev.of_node指回设备树节点的。2.2 从probe函数中“挖”出硬件资源设备树驱动的probe函数像一把瑞士军刀内核提供了大量of_开头的API让你从设备树节点中提取信息。最常用的几个我列一下static int xxx_uart_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct device_node *np dev-of_node; struct resource *res; struct xxx_uart *uart; void __iomem *base; int irq; u32 baudrate 0; uart devm_kzalloc(dev, sizeof(*uart), GFP_KERNEL); if (!uart) return -ENOMEM; // 1. 获取寄存器地址通过reg属性自动转换 res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(dev, res); if (IS_ERR(base)) return PTR_ERR(base); // 2. 获取中断号通过interrupts属性 irq platform_get_irq(pdev, 0); if (irq 0) return irq; // 3. 从自定义属性读取参数 of_property_read_u32(np, baudrate, baudrate); // 4. 获取时钟 uart-clk devm_clk_get(dev, baudclk); if (IS_ERR(uart-clk)) return PTR_ERR(uart-clk); platform_set_drvdata(pdev, uart); return 0; }这里面有几处细节值得展开。platform_get_resource和platform_get_irq内部就是通过of_iomap、of_irq_get这些底层接口实现的它们帮你省掉了手动解析address-cells、interrupt-cells的烦恼。devm_ioremap_resource会检查地址资源是否有冲突并自动映射成虚拟地址也不用担心释放问题这属于现代设备树驱动的标准做法。读取自定义属性时要注意数据类型匹配。设备树里0x100这种尖括号里的数字默认就是u32用of_property_read_u32去读如果想读字符串用of_property_read_string如果属性后面跟的是一个字符串数组或者一组数字用of_property_count_elems_of_size先数一下个数再逐项读取。参数的类型、个数都得和dts里写的严格对应否则读出来的值完全不可控。2.3 从零实现一个会读设备树的字符设备驱动为了把整个链路串起来我写过一个非常简单的demo驱动它的作用就是从设备树节点里读取两个属性然后创建一个misc设备用户态通过read就能拿到这些参数。这个例子虽然小但五脏俱全非常适合入门。设备树片段/ { demo_device: demo-device0 { compatible demo,chardev; reg 0x0 0x100; demo,msg hello from device tree; demo,count 5; status okay; }; };驱动代码#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/miscdevice.h #include linux/uaccess.h struct demo_dev { char msg[64]; int count; }; static struct demo_dev demo_data; static ssize_t demo_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { char out[128]; int len; len snprintf(out, sizeof(out), msg%s, count%d\n, demo_data.msg, demo_data.count); if (*ppos len) return 0; if (count len - *ppos) count len - *ppos; if (copy_to_user(buf, out *ppos, count)) return -EFAULT; *ppos count; return count; } static const struct file_operations demo_fops { .owner THIS_MODULE, .read demo_read, }; static struct miscdevice demo_miscdev { .minor MISC_DYNAMIC_MINOR, .name demo_device, .fops demo_fops, }; static int demo_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct device_node *np dev-of_node; const char *str NULL; if (of_property_read_string(np, demo,msg, str)) { dev_err(dev, failed to get demo,msg\n); return -EINVAL; } strscpy(demo_data.msg, str, sizeof(demo_data.msg)); if (of_property_read_u32(np, demo,count, demo_data.count)) { dev_err(dev, failed to get demo,count\n); return -EINVAL; } dev_info(dev, probed, msg%s, count%d\n, demo_data.msg, demo_data.count); return misc_register(demo_miscdev); } static int demo_remove(struct platform_device *pdev) { misc_deregister(demo_miscdev); return 0; } static const struct of_device_id demo_of_match[] { { .compatible demo,chardev, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, demo_of_match); static struct platform_driver demo_driver { .probe demo_probe, .remove demo_remove, .driver { .name demo_chardev, .of_match_table demo_of_match, }, }; module_platform_driver(demo_driver); MODULE_LICENSE(GPL);编译成ko后先确保设备树节点状态是okay且compatible字符串完全一致再insmod你就会在dmesg里看到probe成功打印然后/dev/demo_device节点就出现了。cat /dev/demo_device就能看到设备树里那两个属性被读了出来。这个流程完整跑通后你对设备树的“树到驱动到用户态”的整个链路就会非常清晰。3. RK3568设备树实战——多版本设备树区分、修改与编译烧写搜索热点里好几次出现“瑞芯微rk3568设备树”“openharmony的rk3568有许多设备树到底咋选”“ubuntu如何修改rk3568的设备树”说明很多朋友已经进入实战阶段被具体板型选型卡住了。这节就针对RK3568展开聊。3.1 dts和dtsi的分工rk3568.dtsi与板级dts的差异瑞芯微的BSP包和官方内核里RK3568的设备树文件组织得非常有代表性。arch/arm64/boot/dts/rockchip/目录下你通常能看到两层文件rk3568.dtsi描述RK3568这颗SoC的全部公共资源。CPU核心、GIC中断控制器、DMA、CRU时钟、PMU、I2C、SPI、UART、SD/MMC控制器、GPU、VPU、NPU等等全部在这里定义。节点大多带有status disabled并附带完整的外设资源描述。rk3568-evb.dts对应官方EVB板包含了DDR容量型号、具体外设接法、引脚的pinctrl配置、哪些控制器被使能等板级信息。这种分层带来一个直接好处想搞明白“某一块板子的串口为什么是ttyS0而不是ttyS3”你需要看的是板级dts而不是去翻SoC的datasheet逐个核对。以调试串口为例rk3568.dtsi里定义了uart0到uart9共10个串口节点其中uart2通常被Rockchip官方默认配置为调试串口。你需要在板级dts里找到类似下面的片段uart2 { status okay; pinctrl-names default; pinctrl-0 uart2m0_xfer; };这里status改okay是使能串口pinctrl-0里的uart2m0_xfer则指定了串口引脚复用为m0组。如果这行pinctrl写错或者被别的设备占用串口就完全没法工作。3.2 板级dts的选型同样一颗RK3568为什么dtb不能随便乱用很多新手拿到市面上各种RK3568开发板下载OpenHarmony或者Linux镜像后发现SD卡烧进去起不来或者起来后屏幕不亮、网口不通第一反应是镜像坏了。实际上绝大多数情况是dtb和板型不匹配。为什么同一颗SoC会有那么多dtb因为RK3568的管脚和功能非常灵活一块板子的硬件设计决定了它只能用特定的dtb。同样是rk3568的evb板evb1、evb2可能在DDR型号、LCD面板接口、PHY地址上就有差异配置文件自然不同。瑞芯微官方发布的内核里arch/arm64/boot/dts/rockchip/目录下可能同时有rk3568-evb.dtb、rk3568-evb2-lp4x-v10.dtb、rk3568-nvr-demo.dtb等十几种产物分别对应EVB、NVR、平板等不同产品形态。选型时优先看板卡厂商提供的BSP包和烧录工具。Rockchip的SDK比如Linux SDK、OpenHarmony SDK里一般都会把当前支持板的dtb单独列出来。如果你手头是第三方开发板老老实实用厂商提供的dtb不要自己去内核编译目录里碰运气。判断当前系统运行的是哪个dtb可以进系统后执行# 查看加载的设备树文件路径一般指向根文件系统里的dtb cat /proc/device-tree/model # 查看compatible会包含板级型号信息 cat /proc/device-tree/compatiblemodel节点里会写明板卡型号比如“Rockchip RK3566 EVB2 LP4X V10 Board”compatible里也会带上具体板级字符串。如果和你手上的板卡对不上恭喜你找到问题了。3.3 Ubuntu下修改并重新编译RK3568设备树的完整步骤当你确定了需要修改的设备树源文件接下来就是在Ubuntu主机上完成修改、编译、烧录。以Ubuntu 20.04/22.04环境为例我常用的流程如下第一步准备交叉编译工具链和内核源码。如果是Rockchip官方Linux SDK它的目录里已经预置了交叉编译器比如export PATH/opt/arm-gcc/bin:$PATH export CROSS_COMPILEaarch64-linux-gnu- export ARCHarm64如果你的SDK里没有编译器也可以手动安装sudo apt-get install gcc-aarch64-linux-gnu device-tree-compiler第二步修改dts文件。假设我们要把I2C4总线上挂一颗外部RTC芯片在板级dts里追加i2c4 { status okay; pinctrl-names default; pinctrl-0 i2c4m1_xfer; clock-frequency 100000; pcf8563: pcf856351 { compatible nxp,pcf8563; reg 0x51; status okay; }; };注意几处细节i2c4要okaypinctrl要指定正确复用组clock-frequency明确I2C速率子节点地址0x51来自RTC芯片手册里的设备地址。reg的单位是7位I2C地址不是8位地址。第三步重新编译dtb。在内核源码根目录执行make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- dtbs或者只编单个目标make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- rk3568-evb.dtb编译完的dtb在arch/arm64/boot/dts/rockchip/rk3568-evb.dtb。如果只想验证语法正确性不牵涉内核编译也可以直接用dtc编译但强烈建议用内核的编译体系因为dts里大量引用dtsi中的宏定义、头文件和标签dtc裸编译常常会因为找不到符号报一堆错。第四步替换boot分区里的dtb。不同板子的dtb存放位置不同——有些是独立dtb分区有些打包在boot.img里有些则由uboot从FIT镜像里加载。参考你板卡厂商的烧录文档把dtb放到对应位置。第五步验证。重新烧录启动后进入系统检查节点状态# 查看I2C4下的设备是否注册成功 ls /sys/bus/i2c/devices/ # 查看pcf8563是否probe成功 dmesg | grep pcf8563 # 如果驱动支持rtc直接读时间 hwclock -r如果dmesg里没有任何报错i2c设备列表里也出现了4-0051说明整条链路已经打通。3.4 OpenHarmony环境下的设备树选择逻辑适配OpenHarmony的RK3568时设备树选择逻辑和标准Linux没有本质区别但有一个额外难点OpenHarmony的设备树往往和hdf驱动框架耦合得更紧。OpenHarmony的drivers/hdf目录下很多外设驱动的probe流程不仅要解析设备树compatible还要去匹配hcsHDF Configuration Source配置文件。实际操作里最常见的问题是同一个RK3568开发板OpenHarmony官方仓库里可能有标准系统、小型系统、轻量系统三套代码每套代码里的设备树都不太一样而且rk3568相关的board目录下会有ohos/rk3568-evb.dts、rk3568-evb.dts等多个文件交叉引用。我的经验是先分清你编译的是哪套OpenHarmony版本、哪个产品形态然后到device/board/对应的目录里找dts。绝对不要从别的仓库或者别的版本里复制一个dtb直接拿来用。如果板载外设和默认设备树不匹配优先修改该产品目录下的dts并重新编译而不是在uboot层面强制加载别的dtb。4. 设备树调试手段与避坑经验——这些年我踩过的坑这一节的核心是帮你建立一套“出问题时先查哪里”的思维链路。设备树问题最大的特点是现象千奇百怪根因往往就那么几个。多数时候不是驱动代码写错而是设备树描述和硬件实际情况不一致。4.1 设备树问题背后的隐蔽错误与排查链路我遇到过最夸张的一次是一块板子启动后emmc完全无法识别。dmesg里报的是mmc0的timeout很多人第一反应是驱动问题但仔细看设备树发现emmc的pinctrl配置里sdmmc0引脚组和uart1的引脚组发生了复用冲突。两个节点都写了status okay都配置了自己的pinctrl但复用寄存器只有一组最后的结果是两者抢引脚谁都不正常。排查这类问题最有效的工具是pinctrl和gpio的debugfs节点# 查所有引脚当前复用状态 cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins # 查gpio申请情况看谁占用了有冲突的引脚 cat /sys/kernel/debug/gpiopinmux-pins会列出每个引脚当前的function对照原理图看该引脚应该是什么功能立刻就能定位是谁抢了谁的。如果发现被其它设备复用改掉其中一个节点的pinctrl-0指向即可。另一种高频问题是不规则的中断配置。RK3568的GIC中断类型很多设备树中interrupts GIC_SPI x IRQ_TYPE_LEVEL_HIGH这种写法GIC_SPI是共享外设中断相对于PPIx是中断号最后一个是触发类型。如果你把电平触发写成了边沿触发中断就会丢或者重复触发。这种问题在调试触摸屏、传感器时特别常见系统表现是中断风暴或干脆没反应。4.2 错误property导致的驱动失败——一个血压传感器的故事讲一个我印象特别深的实战经历。客户需要用I2C读取血压传感器模块数据手册上明确写寄存器地址是0x25I2C从机地址是0x57。这时候在设备树里敲下reg 0x57结果驱动probe之后一读寄存器返回值全为0xFF完全不像传感器该有的数据。查I2C总线抓波形才发现地址线上传输地址是高位的0x57地址但是驱动里报的却是0x2B。原因是设备树reg属性表示的是7位地址而传感器手册上写的是8位地址0xAE——手册把方向和地址位都包含了。把reg改成0x57后读取就正常了。这个坑提醒两件事第一设备树里I2C子节点的reg必须是7位从机地址不要直接把8位地址写进去第二芯片手册如果写的是8位地址换算方法就是右移一位。这个错误在初学者里出现频率非常高值得特别记住。4.3 动态加载dtbo和overlay调试的实用技巧内核提供设备树overlay机制可以让你在不重新编译整个dtb的情况下动态修改设备树。这在调试阶段特别好用。实现方法是在bootloader阶段加载一个dtbo文件或者在运行阶段通过configfs动态应用覆盖。以运行阶段为例先把overlay支持编译进内核CONFIG_OF_OVERLAYy CONFIG_CONFIGFS_FSy启动后加载configfssudo mount -t configfs configfs /config sudo mkdir /config/device-tree/overlays/uart-test然后往overlay的dtbo里写入编译好的dtbo文件echo -n uart-test.dtbo /config/device-tree/overlays/uart-test/path应用成功后查看对应节点是否生效即可。overlay在调试阶段的价值非常大。比如你要验证某个I2C外设的reg是不是写错了可以先overlay一个只有正确地址的节点上去验证通了再改dts固化下来。比改dts重新编译烧录快好几个来回。不过overlay用的dts写法和普通dts略有差异必须带__overlay__标签/dts-v1/; /plugin/; i2c4 { status okay; pcf8563: pcf856351 { compatible nxp,pcf8563; reg 0x51; status okay; }; };编译时用dtc - -I dts -O dtb -o uart-test.dtbo uart-test.dts其中-参数是为了保留symbol信息没有它overlay就没法定位i2c4引用。4.4 地址长度、时钟频率、设备名称这些低级但致命的细节最后这部分把几个人人都会犯但踩一次就能记一辈子的小问题集中起来提醒一下第一个是address-cells和size-cells不一致。RK3568默认soc节点下是#address-cells 2、#size-cells 2即地址和长度都是64位。many新手抄外设节点的reg时只写一个0xfdd50000 0x100内核解析出来的地址就完全错了。正确写法是reg 0x0 0xfdd50000 0x0 0x100。如果你不确定怎么填最稳的办法是模仿同款SoC官方dtsi里同名节点。第二个是时钟名字不匹配。devm_clk_get(dev, baudclk)里的“baudclk”必须和设备树clocks里clock-names的字符串完全一致。写错后驱动probe会返回-ENOENT而且有时错误信息非常隐晦不仔细看可能以为时钟系统坏了。这类问题通常在驱动源码里搜clock-names就能找到答案不必去猜。第三个是驱动名称和内核其它子系统存在命名冲突。我自己遇到过misc设备名字注册失败原因是/dev下已经有同名设备节点了。用cat /proc/misc可以查看已注册的misc设备提前避开常见名称是一种很实用的习惯。第四个是关于节点status字段。很多新写设备树的人子节点里少了status okay或者父节点没开都会导致设备不可见。查找问题第一步永远是用ls /sys/bus/platform/devices/或ls /proc/device-tree/确认节点是否有效。设备树这东西本质上是“用数据驱动驱动”。把dts当成一份需要严格校验的契约——你写的每一个属性、每一个数字都要和硬件实物、芯片手册、驱动代码三方对齐。这样思考问题后调试思路会清晰很多。最后一个经验是修改设备树之前养成先备份原dtb的习惯以及在改动最少的情况下做验证。很多莫名其妙的驱动器问题回头发现都是dtb被刷成别的版本造成的。
返回列表