13、设备树与内核配置:Device Tree(DTS)、内核 defconfig 裁剪与配置
设备树和内核配置,是嵌入式Android系统开发中绕不开的两座山。我刚开始接触AOSP时,总觉得设备树就是一堆看不懂的dts文件,内核配置就是改几个开关。直到有一次,我因为漏配了一个GPIO节点,导致触摸屏在开机后完全没反应,排查了整整两天……嗯,从那以后,我再也不敢小看这两个东西了。
说白了,设备树就是告诉内核“你的硬件长什么样”,而内核配置就是告诉内核“你要支持哪些功能”。两者配合好了,你的ROM才能跑得稳、跑得快。
13.1 设备树(Device Tree)是什么?
设备树是一种描述硬件的数据结构。它用树形结构来描述CPU、内存、外设、中断控制器等硬件信息。内核在启动时解析设备树,就知道该初始化哪些设备、用哪个驱动。
你想想看,如果没有设备树,内核就得硬编码硬件信息。换一块板子就得重新编译内核。有了设备树,同一份内核镜像可以适配不同硬件,只要换一个dtb文件就行。
核心概念:
- DTS:设备树源文件,文本格式,人类可读
- DTC:设备树编译器,把dts编译成dtb
- DTB:设备树二进制文件,内核实际使用的格式
- DTBO:设备树叠加层,用于动态修改设备树
我在项目中遇到过最典型的问题:同一款SoC用在手机和平板上,外设完全不同。手机有摄像头、指纹,平板没有。这时候只需要准备两份dts文件,共用同一个内核,完美解决。
13.2 DTS文件结构详解
一个典型的dts文件长这样:
/dts-v1/;
#include "soc.dtsi"
/ {
model = "My Android Device";
compatible = "vendor,my-device", "vendor,soc";
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p5";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>; // 1GB内存
};
i2c@ff110000 {
compatible = "vendor,i2c-controller";
reg = <0xff110000 0x1000>;
interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
clock-frequency = <400000>;
touchscreen@38 {
compatible = "goodix,gt911";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
};
};
};
这里有几个关键点:
/dts-v1/:声明dts版本,必须写在第一行#include:可以包含头文件或dtsi文件,dtsi是公共部分compatible:匹配驱动的关键字符串,内核靠它找到对应驱动reg:描述地址和大小,格式是<起始地址 长度>interrupts:中断描述,包括中断类型、中断号、触发方式
个人经验:我建议把SoC公共部分放在dtsi文件中,板级差异放在dts中。这样多个项目可以复用同一个dtsi,改起来也方便。我曾经见过一个团队把所有东西都塞在一个dts里,结果维护起来简直是噩梦。
13.3 设备树编译与加载流程
设备树的编译流程其实很简单:
# 编译dts为dtb
dtc -I dts -O dtb -o my-device.dtb my-device.dts
# 反编译dtb为dts(调试用)
dtc -I dtb -O dts -o my-device.dts my-device.dtb
# 在AOSP中,设备树通常放在kernel目录下
# 编译时会自动生成dtb文件
make ARCH=arm64 my-device.dtb
内核启动时,bootloader(比如U-Boot或LK)会把dtb加载到内存中,然后把dtb的物理地址通过寄存器传递给内核。内核解析dtb后,开始初始化设备。
我记得有一次调试,内核死活不认我的触摸屏。后来发现是bootloader传的dtb地址不对,内核读到的全是乱码。嗯,这种问题最坑了,因为错误信息往往不明确。
13.4 内核defconfig裁剪与配置
内核配置,说白了就是决定内核要编译哪些模块、支持哪些功能。Android系统对内核大小有严格要求,太大的内核会影响启动速度和内存占用。
defconfig是内核的默认配置文件,放在arch/arm64/configs/目录下。比如:
# 查看当前配置
cat arch/arm64/configs/vendor_defconfig
# 修改配置
make ARCH=arm64 vendor_defconfig
make ARCH=arm64 menuconfig
# 保存新配置
make ARCH=arm64 savedefconfig
cp defconfig arch/arm64/configs/vendor_defconfig
注意:千万不要直接修改defconfig文件!正确的做法是用menuconfig修改,然后savedefconfig导出。直接改文件很容易漏掉依赖项,导致编译失败。
13.5 内核裁剪实战技巧
内核裁剪的核心原则:用不到的就不编译。但说起来容易做起来难,我见过有人为了追求极致大小,把USB驱动都裁掉了,结果adb连不上……
以下是我常用的裁剪策略:
| 模块 | 建议 | 说明 |
|---|---|---|
| 文件系统 | 只保留ext4、f2fs | Android主要用这两个,其他可以去掉 |
| 网络协议 | 保留TCP/IP、IPv4 | IPv6如果不需要可以关掉 |
| 驱动 | 只保留硬件需要的 | 用lsmod查看当前加载的模块 |
| 调试功能 | 发布版全部关闭 | 比如printk、debugfs、lockdep |
| 电源管理 | 保留suspend/resume | Android的睡眠唤醒依赖这个 |
我曾经接手过一个项目,内核镜像有40MB,启动要8秒。经过裁剪后降到12MB,启动时间缩短到3秒。怎么做到的?其实就是把那些调试用的、实验性的功能全部关掉,只保留生产环境需要的。
13.6 设备树与内核配置的协同
设备树和内核配置不是孤立的。你想想看,设备树里描述了一个I2C触摸屏,但内核配置里把I2C驱动编译成模块了,那触摸屏还是用不了。
所以,我建议的流程是:
- 先确定硬件有哪些外设
- 编写设备树,描述所有外设
- 根据设备树,配置内核,确保所有用到的驱动都编译进去
- 编译测试,验证每个外设都能正常工作
避坑指南:我曾经因为设备树里写了一个不存在的GPIO号,导致内核在启动时panic。排查方法很简单:打开内核的CONFIG_OF_DEBUG,启动时会有详细的设备树解析日志。
13.7 知识体系总览
下面这张图总结了设备树与内核配置的核心逻辑:
从这张图可以看出,设备树和内核配置是两条并行的线,最终在内核启动时交汇。设备树告诉内核“我有什么硬件”,内核配置告诉内核“我能支持什么硬件”。两者必须匹配,系统才能正常工作。
13.8 常见问题与排查方法
最后,分享几个我踩过的坑:
- 设备树节点没生效:检查
compatible字符串是否和驱动匹配,大小写敏感 - 内核配置改了没效果:记得
make clean再重新编译,有时候缓存会坑人 - 启动时卡在某个设备:打开
CONFIG_DEBUG_LL和CONFIG_EARLY_PRINTK,看串口输出 - dtb文件太大:检查是否包含了不必要的节点,比如调试用的节点可以去掉
重要提醒:设备树里的reg地址一定要和硬件手册一致。我曾经因为看错了数据手册,把I2C控制器的地址写错了,结果所有I2C设备都初始化失败。这种低级错误最要命,但也是最容易犯的。
嗯,设备树和内核配置就讲到这里。这两个东西看似基础,但真正用好需要大量实践。我建议你找一块开发板,从零开始写设备树、配内核,跑一遍完整的流程。只有亲手踩过坑,才能真正理解它们的重要性。