第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?

说白了,就是在内核初始化早期、还没有完整的串口驱动之前,就能输出打印信息。这样你就能看到内核到底卡在哪一步了。

启用方法:

  1. 内核配置:打开 CONFIG_SERIAL_EARLYCON 和对应的串口驱动。
  2. 设备树中添加:在 chosen 节点里指定earlycon参数。
  3. 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启用:这是调试的起点,一定要先搞定。

下一章咱们会深入设备树的细节,讲讲怎么从零开始写一个板级的设备树文件。到时候我会拿一个实际项目里的例子来拆解,保证你看完就能上手。

嗯,今天就到这儿。有问题欢迎在评论区交流。