第4章:Linux内核移植(基础篇):内核源码获取与分支选择、架构相关代码(arch/arm64)与板级文件(mach-xxx)的修改、早期串口调试(earlycon)的启用
好,咱们进入内核移植的正题了。
说实话,内核移植这件事,很多刚入行的朋友一听就觉得头大。其实没那么玄乎。你想想看,Linux内核发展了这么多年,早就把硬件相关的部分抽象得很好了。我们要做的,说白了就是告诉内核:「嘿,我的板子长这样,你照着这个来初始化。」
我个人习惯把内核移植分成三步走:拿到源码、改架构相关代码、搞定早期调试。今天咱们就把这三步掰开揉碎了讲清楚。
4.1 内核源码获取与分支选择
先说说源码从哪来。
官方渠道当然是 kernel.org。但我建议你别直接去拉主线的最新版——那玩意儿变化太快,今天能编译通过,明天可能就挂了。我在项目中吃过这个亏,有一次跟进了主线的一个RC版本,结果某个驱动接口变了,整个BSP都得重改,那叫一个酸爽。
我的建议是:
- 长期支持版(LTS):比如5.10、5.15、6.1这些。车规级项目首选,稳定周期长,社区维护力度大。
- 芯片厂商的BSP分支:像NXP、TI、Rockchip这些厂商都会维护自己的内核分支。如果你用的是他们的SoC,直接拉他们的分支最省事。
- 不要追新:除非你有特殊需求,否则别用最新版。车规级产品讲究的是稳定,不是跑分。
获取源码的命令:
# 拉取LTS版本
git clone --depth 1 --branch v5.15.120 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
# 或者拉取厂商BSP
git clone https://github.com/nxp-imx/linux-imx.git -b lf-5.15.y
嗯,这里要注意:--depth 1 这个参数很有用。只拉取最新的一次提交,省带宽省时间。等你需要看历史记录的时候再 git fetch --unshallow 补全就行。
4.2 架构相关代码(arch/arm64)的修改
拿到源码之后,第一件事就是看 arch/arm64 目录。这是ARM64架构的核心代码所在。
你可能会问:「我改这里干嘛?芯片厂商不是都做好了?」
理论上是这样。但实际项目中,你总会遇到一些厂商没覆盖到的场景。比如我去年做一个项目,用的是一颗比较新的车规级SoC,厂商的BSP只支持了DDR4,但我们硬件设计用了LPDDR5。没办法,只能自己改内存控制器相关的初始化代码。
arch/arm64 目录下你需要关注的文件:
| 文件/目录 | 说明 | 常见修改点 |
|---|---|---|
Kconfig.platforms |
平台配置选项 | 添加你自己的板子配置项 |
Makefile |
编译规则 | 添加板级文件的编译入口 |
boot/dts/ |
设备树文件 | 这是最常改的地方,后面会细讲 |
kernel/head.S |
内核入口汇编 | 一般不动,除非你要改启动流程 |
mm/ |
内存管理相关 | 页表大小、内存映射等 |
我个人习惯是先看 arch/arm64/Kconfig.platforms。这里面定义了所有支持的平台。找到你的芯片对应的平台选项,确认它被选中了。如果没有,你就得自己加一个。
一个小技巧:
用 make menuconfig 的时候,在「Platform selection」下面看看你的平台有没有被勾上。很多移植问题都是因为平台没选对,导致某些关键代码没编译进去。
4.3 板级文件(mach-xxx)的修改
老版本内核里有个 arch/arm/mach-xxx 目录,里面放的是板级初始化代码。但从4.x内核开始,ARM64架构已经全面转向设备树(Device Tree)了。所以现在所谓的「板级文件修改」,其实主要就是改设备树。
设备树文件放在 arch/arm64/boot/dts/ 下面。每个芯片厂商一个子目录,比如 freescale/、rockchip/、ti/ 等等。
你需要创建或修改的文件:
- SoC级别的dtsi:比如
imx8mp.dtsi,描述芯片内部外设。 - 板级dts:比如
imx8mp-evk.dts,描述具体板子的硬件。 - 头文件:
include/dt-bindings/下面的宏定义。
举个例子,假设你要添加一个新的GPIO按键:
// 在板级dts中添加
gpio-keys {
compatible = "gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
power-key {
label = "Power Key";
linux,code = ;
gpios = <&gpio1 13 GPIO_ACTIVE_LOW>;
debounce-interval = <10>;
wakeup-source;
};
};
我曾经遇到过一个坑:设备树里引用的GPIO号跟实际原理图对不上。查了两天才发现,原来是芯片的GPIO bank编号从0开始,但设备树里某些宏定义是从1开始的。嗯,这种低级错误最折磨人。
避坑指南:
我曾经在设备树里漏掉了 pinctrl 配置,结果GPIO死活拉不高。后来才发现,没有pinctrl的话,内核根本不知道这个引脚要配成GPIO功能还是其他复用功能。所以,一定要确认pinctrl配置完整。
4.4 早期串口调试(earlycon)的启用
好了,代码改完了,编译通过了,烧进去……黑屏。
这时候你怎么办?
早期串口调试(earlycon)就是你的救命稻草。
什么是earlycon?
说白了,就是在内核初始化早期、还没有完整的串口驱动之前,就能输出打印信息。这样你就能看到内核到底卡在哪一步了。
启用方法:
- 内核配置:打开
CONFIG_SERIAL_EARLYCON和对应的串口驱动。 - 设备树中添加:在
chosen节点里指定earlycon参数。 - bootargs中传递:在U-Boot的bootargs里加上
earlycon。
具体来说,设备树里这样写:
/ {
chosen {
bootargs = "earlycon=uart8250,mmio32,0x30890000 console=ttyS0,115200";
stdout-path = &uart1;
};
};
这里 0x30890000 是UART控制器的基地址。你得查芯片手册确认这个地址对不对。
我的经验:
earlycon的格式是 earlycon=<驱动名>,<访问方式>,<基地址>。不同的串口IP,驱动名不一样。比如8250系列的用 uart8250,PL011的用 pl011。搞错了的话,earlycon不会报错,但也不会输出任何东西。
还有一个容易忽略的点:earlycon依赖时钟。如果串口模块的时钟还没配好,earlycon也出不来。我遇到过这种情况:内核在时钟框架初始化之前就调了earlycon,结果串口波特率完全不对,输出全是乱码。
怎么验证earlycon是否生效?
看启动日志。如果能看到类似这样的输出:
[ 0.000000] Early console on uart8250 at 0x30890000
[ 0.000000] bootconsole [uart8250] enabled
那就说明earlycon已经跑起来了。如果看不到,检查一下是不是内核配置没开,或者设备树里的地址不对。
4.5 本章小结
咱们今天聊了三个核心点:
- 源码获取:别追新,选LTS或厂商BSP。
- 架构代码修改:重点在
arch/arm64的配置和设备树。 - earlycon启用:这是调试的起点,一定要先搞定。
下一章咱们会深入设备树的细节,讲讲怎么从零开始写一个板级的设备树文件。到时候我会拿一个实际项目里的例子来拆解,保证你看完就能上手。
嗯,今天就到这儿。有问题欢迎在评论区交流。