大家好,我是良许。
在嵌入式 Linux 开发中,设备树(Device Tree)和内核裁剪是两个非常重要的概念。
很多刚入门的朋友可能会觉得这两个东西很抽象,不知道从何下手。
今天我就用通俗易懂的方式,给大家详细讲解一下这两个技术点,帮助大家在实际项目中更好地应用它们。
设备树说白了就是一个描述硬件信息的文本文件。
在早期的 Linux 内核中,硬件信息都是直接写死在内核代码里的,这就导致每次硬件有变动,都要重新编译内核,非常麻烦。
设备树的出现就是为了解决这个问题,它把硬件信息从内核代码中剥离出来,放到一个独立的文件中进行描述。
打个比方,设备树就像是一份硬件说明书,告诉 Linux 内核:"我这块板子上有哪些外设,它们的地址是多少,使用哪些引脚,工作频率是多少"等等。
内核启动时会读取这份说明书,然后根据描述去初始化相应的硬件。
设备树文件的后缀通常是
.dts(Device Tree Source),编译后会生成
.dtb(Device Tree Blob)二进制文件供内核使用。我们来看一个简单的设备树示例:
/ {
model = "STM32MP157C-DK2";
compatible = "st,stm32mp157c-dk2", "st,stm32mp157";
memory@c0000000 {
device_type = "memory";
reg = <0xc0000000 0x20000000>;
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
usart3: serial@4000f000 {
compatible = "st,stm32h7-uart";
reg = <0x4000f000 0x400>;
interrupts = <39>;
clocks = <&rcc USART3>;
status = "okay";
};
i2c1: i2c@40012000 {
compatible = "st,stm32mp15-i2c";
reg = <0x40012000 0x400>;
interrupts = <31>, <32>;
clocks = <&rcc I2C1>;
status = "okay";
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <16>;
};
};
};
};
这个设备树描述了一个 STM32MP157 的板子,包含了内存信息、串口、I2C 总线以及挂在 I2C 上的 EEPROM。
每个节点都有一些属性,比如
compatible 表示兼容的驱动,
reg 表示寄存器地址,
interrupts 表示中断号等。
在我之前做汽车电子项目的时候,经常需要根据不同的车型配置不同的外设。
比如高配车型有倒车影像,低配车型没有;有的车型用 CAN 通信,有的用 LIN 通信。
如果把这些都写死在内核里,那每个车型都要编译一个内核,维护起来简直是噩梦。
有了设备树之后,我们只需要为不同车型准备不同的设备树文件就可以了,内核可以共用一个。
比如高配车型的设备树里会有摄像头节点:
camera@5000 {
compatible = "vendor,camera-v1";
reg = <0x5000 0x1000>;
clocks = <&clk_camera>;
status = "okay";
};
而低配车型的设备树里,这个节点的
status 就设置为
disabled,或者干脆不写这个节点。
这样同一个内核就可以适配不同的硬件配置了。
修改设备树时要特别注意几点。
第一,地址和中断号一定要和硬件原理图对应上,否则驱动根本找不到硬件。
第二,
compatible 属性要和驱动代码中的匹配字符串一致,否则驱动不会被加载。
第三,修改完设备树后要重新编译生成
.dtb 文件,并且替换掉板子上的旧文件。
我记得有一次调试一个 SPI 设备,怎么都读不到数据。
折腾了半天才发现,是设备树里的片选引脚配置错了,和实际硬件接线不一致。
所以说,设备树虽然只是个文本文件,但每个参数都要仔细核对,马虎不得。
Linux 内核非常庞大,包含了对各种硬件的支持、各种文件系统、网络协议栈等等。
如果把所有功能都编译进去,内核镜像可能有几十兆甚至上百兆。
对于嵌入式设备来说,存储空间是很宝贵的,而且很多功能根本用不到。
比如一个简单的数据采集设备,不需要图形界面,不需要 USB 支持,不需要蓝牙,那这些模块就完全可以去掉。
裁剪内核不仅可以减小镜像大小,还能加快启动速度,降低内存占用。
我之前做过一个项目,要求系统启动时间不能超过 3 秒。
通过精简内核,去掉不必要的驱动和功能,最终把启动时间压缩到了 2.5 秒。
Linux 内核使用 Kconfig 系统来管理配置选项。
我们可以通过
make menuconfig 命令打开一个图形化的配置界面,在里面选择需要的功能。
配置完成后会生成一个
.config 文件,记录了所有的配置选项。
配置选项有三种状态:
y 表示编译进内核,
m 表示编译成模块,
n 表示不编译。
对于嵌入式系统,我一般倾向于把必需的功能编译进内核(
y),而不是编译成模块。
因为模块需要额外的存储空间,而且加载模块也需要时间。
首先要明确项目的硬件配置和功能需求。
列一个清单,写明需要哪些外设,需要哪些功能。比如:
进入内核源码目录,执行配置命令:
cd linux-5.10 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
在配置界面中,我们需要关注以下几个重要的配置项:
通用设置(General setup):
处理器类型和特性(Processor type and features):
设备驱动(Device Drivers):这是裁剪的重点,需要仔细选择。比如:
文件系统(File systems):
网络选项(Networking support):
配置完成后,保存配置并编译内核:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8 zImage make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
编译完成后,把生成的
zImage 和设备树文件烧录到板子上进行测试。
如果系统能正常启动,功能都正常,那就说明裁剪成功了。
如果启动失败或者某些功能不正常,就需要回过头检查配置,把缺失的驱动或功能加回来。
在实际裁剪过程中,我总结了几点经验。
第一,不要一次性裁剪太多,要循序渐进。
先从明显不需要的功能开始裁剪,比如 USB、蓝牙、音频等。裁剪一部分就测试一次,确保系统还能正常工作。
第二,有些配置项之间有依赖关系。
比如网络功能依赖于网络设备驱动,文件系统功能依赖于块设备驱动。
如果把被依赖的功能关闭了,依赖它的功能也会失效。
Kconfig 系统会自动处理一些依赖关系,但不是所有的依赖都能自动处理,所以要特别注意。
第三,保留一些调试功能在开发阶段是有必要的。
比如串口控制台、printk 日志等。等到产品定型了,再把这些调试功能去掉,进一步精简内核。
第四,可以参考芯片厂商提供的默认配置文件。
比如 STM32MP1 系列,ST 官方提供了
stm32mp1_defconfig,这个配置文件已经针对 STM32MP1 做了优化,我们可以在这个基础上进行裁剪,会省很多事。
设备树和内核裁剪是相辅相成的。
设备树描述了硬件有哪些设备,而内核裁剪决定了支持哪些设备。
如果设备树里描述了一个 SPI 设备,但内核里没有编译 SPI 驱动,那这个设备就无法工作。
反过来,如果内核里编译了 USB 驱动,但设备树里没有 USB 节点,那这个驱动也不会被加载。
所以在实际项目中,我们要做到设备树和内核配置的一致性。
设备树里有的设备,内核里一定要有对应的驱动;内核里编译的驱动,最好是设备树里确实有对应的设备,否则就是浪费空间。
我来分享一个实际案例。
之前做一个工业控制项目,硬件平台是 STM32MP157,需要用到以太网、CAN 总线、SPI Flash、I2C 传感器。我的工作流程是这样的:
首先,根据硬件原理图编写设备树文件,描述所有的硬件设备:
ðernet0 {
status = "okay";
phy-mode = "rgmii";
phy-handle = <&phy0>;
};
&can1 {
status = "okay";
pinctrl-0 = <&can1_pins>;
};
&spi4 {
status = "okay";
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <50000000>;
};
};
&i2c2 {
status = "okay";
temperature@48 {
compatible = "ti,tmp75";
reg = <0x48>;
};
};
然后,配置内核,确保相关驱动都被编译进去。在
menuconfig 中:
同时,把不需要的功能全部关闭,比如 USB、音频、显示、蓝牙等。
最终编译出来的内核镜像只有 3.2MB,相比默认配置的 8MB 减小了 60%。
系统启动后,所有外设都能正常工作,启动时间也从 5 秒缩短到了 2.8 秒。
在实际开发中,经常会遇到一些问题。
比如设备树配置正确,内核也编译了驱动,但设备就是不工作。这时候可以通过以下几个步骤排查:
第一步,检查内核启动日志。
通过串口查看内核的启动信息,看看驱动有没有被加载,有没有报错。如果看到类似"failed to probe"的错误,说明驱动加载失败了。
第二步,检查设备树是否被正确加载。
可以在 Linux 系统中查看
/proc/device-tree 目录,这里面是设备树的运行时表示。如果某个节点不存在或者属性不对,说明设备树有问题。
第三步,检查驱动的匹配字符串。
驱动代码中有一个
of_device_id 结构体,里面定义了驱动支持的
compatible 字符串。
这个字符串必须和设备树中的
compatible 属性完全一致,否则驱动不会被加载。
第四步,检查硬件连接。有时候问题不在软件,而是硬件接线错了,或者硬件本身有问题。
这时候需要用示波器或者逻辑分析仪来检查信号。
设备树和内核裁剪是嵌入式 Linux 开发中的两项基本技能。
设备树让我们能够灵活地描述硬件配置,而不用修改内核代码。
内核裁剪让我们能够根据实际需求定制内核,减小镜像大小,提高系统性能。
对于初学者,我的建议是先从阅读现有的设备树文件开始,理解各个节点和属性的含义。
然后尝试修改一些简单的配置,比如改个 GPIO 引脚,改个波特率,看看效果。
在内核裁剪方面,不要一开始就大刀阔斧地裁剪,先从关闭明显不需要的功能开始,逐步积累经验。
最后,建议大家多看芯片厂商的文档和参考设计。
比如 ST、NXP、TI 这些厂商都会提供详细的设备树示例和内核配置文件,这些都是非常好的学习资料。
多动手实践,多总结经验,慢慢就能掌握这两项技能了。
在我的嵌入式开发生涯中,设备树和内核裁剪帮我解决了无数问题,也让我对 Linux 内核有了更深入的理解。
希望今天的分享能对大家有所帮助,如果有什么问题,欢迎留言交流。
更多编程学习资源
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70049026/viewspace-3111644/ ,如需转载,请注明出处,否则将追究法律责任。