设备树和裁剪内核

6 阅读12分钟

大家好,我是良许。

在嵌入式 Linux 开发中,设备树(Device Tree)和内核裁剪是两个非常重要的概念。

很多刚入门的朋友可能会觉得这两个东西很抽象,不知道从何下手。

今天我就用通俗易懂的方式,给大家详细讲解一下这两个技术点,帮助大家在实际项目中更好地应用它们。

1. 设备树(Device Tree)详解

1.1 什么是设备树

设备树说白了就是一个描述硬件信息的文本文件。

在早期的 Linux 内核中,硬件信息都是直接写死在内核代码里的,这就导致每次硬件有变动,都要重新编译内核,非常麻烦。

设备树的出现就是为了解决这个问题,它把硬件信息从内核代码中剥离出来,放到一个独立的文件中进行描述。

打个比方,设备树就像是一份硬件说明书,告诉 Linux 内核:"我这块板子上有哪些外设,它们的地址是多少,使用哪些引脚,工作频率是多少"等等。

内核启动时会读取这份说明书,然后根据描述去初始化相应的硬件。

1.2 设备树的基本语法

设备树文件的后缀通常是 .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 表示中断号等。

1.3 设备树在实际项目中的应用

在我之前做汽车电子项目的时候,经常需要根据不同的车型配置不同的外设。

比如高配车型有倒车影像,低配车型没有;有的车型用 CAN 通信,有的用 LIN 通信。

如果把这些都写死在内核里,那每个车型都要编译一个内核,维护起来简直是噩梦。

有了设备树之后,我们只需要为不同车型准备不同的设备树文件就可以了,内核可以共用一个。

比如高配车型的设备树里会有摄像头节点:

camera@5000 {
    compatible = "vendor,camera-v1";
    reg = <0x5000 0x1000>;
    clocks = <&clk_camera>;
    status = "okay";
};

而低配车型的设备树里,这个节点的 status 就设置为 disabled,或者干脆不写这个节点。

这样同一个内核就可以适配不同的硬件配置了。

1.4 修改设备树的注意事项

修改设备树时要特别注意几点。

第一,地址和中断号一定要和硬件原理图对应上,否则驱动根本找不到硬件。

第二,compatible 属性要和驱动代码中的匹配字符串一致,否则驱动不会被加载。

第三,修改完设备树后要重新编译生成 .dtb 文件,并且替换掉板子上的旧文件。

我记得有一次调试一个 SPI 设备,怎么都读不到数据。

折腾了半天才发现,是设备树里的片选引脚配置错了,和实际硬件接线不一致。

所以说,设备树虽然只是个文本文件,但每个参数都要仔细核对,马虎不得。

2. 内核裁剪详解

2.1 为什么要裁剪内核

Linux 内核非常庞大,包含了对各种硬件的支持、各种文件系统、网络协议栈等等。

如果把所有功能都编译进去,内核镜像可能有几十兆甚至上百兆。

对于嵌入式设备来说,存储空间是很宝贵的,而且很多功能根本用不到。

比如一个简单的数据采集设备,不需要图形界面,不需要 USB 支持,不需要蓝牙,那这些模块就完全可以去掉。

裁剪内核不仅可以减小镜像大小,还能加快启动速度,降低内存占用。

我之前做过一个项目,要求系统启动时间不能超过 3 秒。

通过精简内核,去掉不必要的驱动和功能,最终把启动时间压缩到了 2.5 秒。

2.2 内核配置系统

Linux 内核使用 Kconfig 系统来管理配置选项。

我们可以通过 make menuconfig 命令打开一个图形化的配置界面,在里面选择需要的功能。

配置完成后会生成一个 .config 文件,记录了所有的配置选项。

配置选项有三种状态:y 表示编译进内核,m 表示编译成模块,n 表示不编译。

对于嵌入式系统,我一般倾向于把必需的功能编译进内核(y),而不是编译成模块。

因为模块需要额外的存储空间,而且加载模块也需要时间。

2.3 裁剪内核的实战步骤

2.3.1 确定硬件需求

首先要明确项目的硬件配置和功能需求。

列一个清单,写明需要哪些外设,需要哪些功能。比如:

  • CPU 架构:ARM Cortex-A7
  • 存储:256MB DDR3,128MB NAND Flash
  • 外设:UART、SPI、I2C、GPIO、以太网
  • 文件系统:ext4、JFFS2
  • 网络:TCP/IP、HTTP
  • 不需要:USB、蓝牙、WiFi、音频、显示

2.3.2 配置内核选项

进入内核源码目录,执行配置命令:

cd linux-5.10
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

在配置界面中,我们需要关注以下几个重要的配置项:

通用设置(General setup):

  • 关闭不需要的内核特性,比如审计支持(Auditing support)
  • 关闭内核调试符号(Kernel debugging symbols),可以大幅减小内核大小
  • 配置内核压缩方式,推荐使用 XZ 压缩,压缩率最高

处理器类型和特性(Processor type and features):

  • 选择正确的 CPU 类型
  • 配置浮点运算支持(如果 CPU 支持硬件浮点)

**设备驱动(Device Drivers):**这是裁剪的重点,需要仔细选择。比如:

  • 字符设备:保留串口驱动,去掉不用的字符设备
  • 块设备:保留需要的存储设备驱动
  • 网络设备:只保留以太网驱动,去掉 WiFi、蓝牙等
  • 输入设备:如果不需要键盘鼠标,可以全部去掉
  • USB 支持:如果不用 USB,整个 USB 子系统都可以关闭

文件系统(File systems):

  • 只保留需要的文件系统,比如 ext4、JFFS2、NFS
  • 去掉不用的文件系统,比如 NTFS、HFS 等

网络选项(Networking support):

  • 保留 TCP/IP 协议栈
  • 去掉不用的网络协议,比如 IPX、AppleTalk 等

2.3.3 编译和测试

配置完成后,保存配置并编译内核:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8 zImage
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

编译完成后,把生成的 zImage 和设备树文件烧录到板子上进行测试。

如果系统能正常启动,功能都正常,那就说明裁剪成功了。

如果启动失败或者某些功能不正常,就需要回过头检查配置,把缺失的驱动或功能加回来。

2.4 裁剪内核的经验总结

在实际裁剪过程中,我总结了几点经验。

第一,不要一次性裁剪太多,要循序渐进。

先从明显不需要的功能开始裁剪,比如 USB、蓝牙、音频等。裁剪一部分就测试一次,确保系统还能正常工作。

第二,有些配置项之间有依赖关系。

比如网络功能依赖于网络设备驱动,文件系统功能依赖于块设备驱动。

如果把被依赖的功能关闭了,依赖它的功能也会失效。

Kconfig 系统会自动处理一些依赖关系,但不是所有的依赖都能自动处理,所以要特别注意。

第三,保留一些调试功能在开发阶段是有必要的。

比如串口控制台、printk 日志等。等到产品定型了,再把这些调试功能去掉,进一步精简内核。

第四,可以参考芯片厂商提供的默认配置文件。

比如 STM32MP1 系列,ST 官方提供了 stm32mp1_defconfig,这个配置文件已经针对 STM32MP1 做了优化,我们可以在这个基础上进行裁剪,会省很多事。

3. 设备树与内核裁剪的配合使用

3.1 两者的关系

设备树和内核裁剪是相辅相成的。

设备树描述了硬件有哪些设备,而内核裁剪决定了支持哪些设备。

如果设备树里描述了一个 SPI 设备,但内核里没有编译 SPI 驱动,那这个设备就无法工作。

反过来,如果内核里编译了 USB 驱动,但设备树里没有 USB 节点,那这个驱动也不会被加载。

所以在实际项目中,我们要做到设备树和内核配置的一致性。

设备树里有的设备,内核里一定要有对应的驱动;内核里编译的驱动,最好是设备树里确实有对应的设备,否则就是浪费空间。

3.2 实战案例分享

我来分享一个实际案例。

之前做一个工业控制项目,硬件平台是 STM32MP157,需要用到以太网、CAN 总线、SPI Flash、I2C 传感器。我的工作流程是这样的:

首先,根据硬件原理图编写设备树文件,描述所有的硬件设备:

&ethernet0 {
    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 中:

  • 网络设备驱动:选择 STMMAC 以太网驱动
  • CAN 总线支持:选择 SocketCAN 和 M_CAN 驱动
  • SPI 驱动:选择 STM32 SPI 驱动
  • MTD 支持:选择 SPI-NOR Flash 支持
  • I2C 驱动:选择 STM32 I2C 驱动
  • 硬件监控:选择 TMP75 温度传感器驱动

同时,把不需要的功能全部关闭,比如 USB、音频、显示、蓝牙等。

最终编译出来的内核镜像只有 3.2MB,相比默认配置的 8MB 减小了 60%。

系统启动后,所有外设都能正常工作,启动时间也从 5 秒缩短到了 2.8 秒。

3.3 常见问题排查

在实际开发中,经常会遇到一些问题。

比如设备树配置正确,内核也编译了驱动,但设备就是不工作。这时候可以通过以下几个步骤排查:

第一步,检查内核启动日志。

通过串口查看内核的启动信息,看看驱动有没有被加载,有没有报错。如果看到类似"failed to probe"的错误,说明驱动加载失败了。

第二步,检查设备树是否被正确加载。

可以在 Linux 系统中查看 /proc/device-tree 目录,这里面是设备树的运行时表示。如果某个节点不存在或者属性不对,说明设备树有问题。

第三步,检查驱动的匹配字符串。

驱动代码中有一个 of_device_id 结构体,里面定义了驱动支持的 compatible 字符串。

这个字符串必须和设备树中的 compatible 属性完全一致,否则驱动不会被加载。

第四步,检查硬件连接。有时候问题不在软件,而是硬件接线错了,或者硬件本身有问题。

这时候需要用示波器或者逻辑分析仪来检查信号。

4. 总结与建议

设备树和内核裁剪是嵌入式 Linux 开发中的两项基本技能。

设备树让我们能够灵活地描述硬件配置,而不用修改内核代码。

内核裁剪让我们能够根据实际需求定制内核,减小镜像大小,提高系统性能。

对于初学者,我的建议是先从阅读现有的设备树文件开始,理解各个节点和属性的含义。

然后尝试修改一些简单的配置,比如改个 GPIO 引脚,改个波特率,看看效果。

在内核裁剪方面,不要一开始就大刀阔斧地裁剪,先从关闭明显不需要的功能开始,逐步积累经验。

最后,建议大家多看芯片厂商的文档和参考设计。

比如 ST、NXP、TI 这些厂商都会提供详细的设备树示例和内核配置文件,这些都是非常好的学习资料。

多动手实践,多总结经验,慢慢就能掌握这两项技能了。

在我的嵌入式开发生涯中,设备树和内核裁剪帮我解决了无数问题,也让我对 Linux 内核有了更深入的理解。

希望今天的分享能对大家有所帮助,如果有什么问题,欢迎留言交流。

更多编程学习资源