4、Linux内核移植(上):内核源码树结构、设备树(DTB)概念与语法、i.MX8QM设备树编写、时钟与电源管理框架
各位同学,欢迎来到《i.MX Android Automotive系统移植实战》的第四讲。今天我们要啃的这块骨头,是系统移植中最核心、也最容易让人头大的部分——Linux内核移植。不过别担心,我会把这几年的实战经验揉碎了讲给你听。
这一讲,我们聚焦在「上篇」:先把内核源码树的结构摸清楚,再把设备树(DTB)这个关键概念吃透,最后看看i.MX8QM的时钟与电源管理框架。说白了,这三件事搞定了,内核移植的骨架你就搭起来了。
4.1 内核源码树结构:别在迷宫里转晕
我第一次接触Linux内核源码时,面对那几百个文件夹,说实话,有点懵。但后来我发现,只要抓住几个关键目录,整个源码树就像一张地图一样清晰。
Linux内核源码的顶层目录,我习惯把它分成三大块:
- arch/:架构相关代码。我们做i.MX8QM,重点关注
arch/arm64/。这里面有启动代码、内存管理、中断处理等底层实现。 - drivers/:设备驱动。这是最大的目录,没有之一。我们的显示、音频、网络、GPU等驱动都在这里。
- kernel/、mm/、fs/:核心子系统。进程调度、内存管理、文件系统,这些是内核的「操作系统」部分。
嗯,这里有个小技巧:当你需要找某个外设的驱动时,先看drivers/下的子目录,比如drivers/gpu/drm/imx/就是i.MX的GPU驱动。我在项目中遇到过,有人把驱动放错了位置,结果编译时死活找不到符号——这种低级错误,其实只要熟悉目录结构就能避免。
make ARCH=arm64 defconfig,然后看看.config文件里哪些驱动被打开了。这比直接翻代码快得多。
4.2 设备树(DTB)概念与语法:硬件描述的「身份证」
设备树,说白了就是一份描述硬件信息的「身份证」。它告诉内核:你的CPU是什么?内存多大?外设挂在哪里?中断号是多少?
为什么要有设备树?你想想看,以前的内核,硬件信息都硬编码在C代码里。换一块板子,就得重新编译内核。有了设备树,内核和硬件配置就解耦了——同一份内核镜像,换不同的设备树文件,就能适配不同的硬件。
设备树的语法,其实不复杂。核心就是节点(node)和属性(property)。
/dts-v1/;
/ {
model = "NXP i.MX8QM EVK";
compatible = "fsl,imx8qm";
chosen {
stdout-path = &uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x80000000>;
};
soc {
#address-cells = <2>;
#size-cells = <2>;
ranges;
uart0: serial@5a060000 {
compatible = "fsl,imx8qm-uart", "fsl,imx6q-uart";
reg = <0x0 0x5a060000 0x0 0x10000>;
interrupts = <0 26 4>;
clocks = <&clk IMX8QM_UART0_CLK>;
status = "okay";
};
};
};
看到这个例子,你可能会问:#address-cells和#size-cells是干嘛的?这其实是告诉解析器,地址和大小用几个32位整数表示。i.MX8QM是64位SoC,所以用2个cell表示64位地址。
interrupts属性的第三个参数写错了——那个参数是中断触发类型,我写成了4(高电平触发),但实际硬件需要1(上升沿触发)。这种错误,编译时不会报错,运行时才暴露,特别坑。
4.3 i.MX8QM设备树编写:从参考板到定制板
编写i.MX8QM的设备树,我建议你从NXP官方提供的参考板设备树开始。通常路径是:arch/arm64/boot/dts/freescale/fsl-imx8qm.dtsi。这个文件定义了SoC内部的所有外设,比如UART、I2C、SPI、GPU等。
然后,你需要在板级设备树文件(比如fsl-imx8qm-mek.dts)中,#include这个SoC的dtsi文件,再添加你自己的板级差异。
举个例子,如果你的板子用了不同的以太网PHY,你只需要在板级dts里覆盖PHY节点:
&fec1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_fec1>;
phy-mode = "rgmii";
phy-handle = <ðphy0>;
status = "okay";
mdio {
#address-cells = <1>;
#size-cells = <0>;
ethphy0: ethernet-phy@0 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = <0>;
reset-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
};
};
};
这里要注意:pinctrl节点定义了引脚的复用功能。i.MX8QM的引脚配置非常灵活,但也非常复杂。我个人的做法是,先用NXP的pinmux tool生成配置,再手动核对一遍——工具生成的代码有时候会漏掉一些电源域设置。
4.4 时钟与电源管理框架:让芯片「活」起来
时钟和电源管理,是嵌入式系统里最容易被忽视、但又最关键的部分。没有时钟,芯片就是一块死硅片;电源管理不好,要么功耗爆炸,要么系统不稳定。
在Linux内核中,时钟框架由clk子系统管理。每个时钟源都是一个struct clk对象,通过clk_get()、clk_enable()等API来操作。
i.MX8QM的时钟树非常复杂,有多个PLL、分频器、门控。NXP在drivers/clk/imx/下提供了驱动。你不需要从头写,但需要理解设备树里怎么引用时钟:
clocks = <&clk IMX8QM_UART0_CLK>;
这个IMX8QM_UART0_CLK是一个宏,定义在dt-bindings/clock/imx8qm-clock.h里。它对应一个时钟ID,驱动通过这个ID去查找时钟树。
电源管理方面,i.MX8QM使用了genpd(Generic Power Domain)框架。每个电源域可以独立开关,比如GPU域、VPU域、DDR域。设备树里这样描述:
pd_gpu: power-domain@1 {
compatible = "fsl,imx8qm-pd";
reg = <0x1>;
#power-domain-cells = <0>;
domain-id = <SC_R_GPU_0_PID0>;
};
然后,GPU设备节点通过power-domains属性引用它:
&gpu0 {
power-domains = <&pd_gpu>;
status = "okay";
};
这样,当GPU驱动加载时,内核会自动打开对应的电源域。驱动卸载时,自动关闭。省电又省心。
clk_ignore_unused和pd_ignore_unused。这样内核不会关闭未使用的时钟和电源域,方便你排查是哪个域没配好。但注意,这只是调试手段,产品发布前一定要去掉。
4.5 本章知识体系总览
为了让你更直观地理解这一讲的内容,我画了一张图,把内核移植的骨架展示出来:
这张图把这一讲的核心脉络串起来了。你从内核源码树入手,找到对应的架构和驱动目录;然后通过设备树描述硬件;最后用时钟和电源管理框架让硬件动起来。这三步走完,内核移植的「上半场」就基本到位了。
好了,这一讲的内容就到这里。记住,设备树是硬件和内核之间的「翻译官」,写错了,内核就听不懂硬件在说什么。下一讲我们会深入内核移植的「下篇」,聊聊启动流程、内存映射和调试技巧。到时候见。
公众号:蓝海资料掘金营,微信deep3321