3、Linux内核定制与启动:内核源码获取、内核配置(menuconfig)、编译与烧录、U-Boot引导流程分析、设备树(Device Tree)基础

这一章,我们聊聊嵌入式Linux最核心的环节——内核定制与启动。说白了,就是让Linux内核在你的板子上跑起来,并且跑得稳、跑得快。我做了这么多年嵌入式,发现很多工程师把精力都花在应用层,却忽略了底层启动流程。其实,这里才是性能瓶颈和稳定性问题的根源。

3.1 内核源码获取:从哪里来,到哪里去

获取内核源码,第一反应肯定是去kernel.org下载主线版本。但做嵌入式,我建议你优先找芯片厂商(比如Intel、NXP、TI)提供的BSP(板级支持包)内核。为什么?因为主线内核虽然新,但可能没有针对你板子的补丁和驱动。

我个人习惯是这么做的:

  • 先看厂商SDK:比如Intel的linux-intel-lts分支,或者NXP的imx_4.14.y分支。这些分支已经集成了大量板级适配代码。
  • 再用git管理:把厂商内核作为上游,自己拉一个分支做定制。这样后续厂商更新补丁,你可以轻松合并。
小技巧:用git clone --depth=1只拉取最新一次提交,能省下大量下载时间。我刚开始做项目时,傻傻地全量克隆,结果等了半小时。后来发现,嵌入式开发根本不需要完整历史。

3.2 内核配置:menuconfig的玄机

配置内核,最常用的就是make menuconfig。它基于ncurses的图形界面,让你勾选各种功能。嗯,这里要注意:不要贪多。很多新手喜欢把所有驱动都编译进去,结果内核镜像大得吓人,启动慢,还容易出问题。

我的经验是:

  • 最小化原则:只选你板子必须的驱动。比如你的板子用eMMC,就别选SD卡驱动;用USB键盘,就别选PS/2键盘。
  • 模块化编译:把不常用的驱动编译成模块(M),而不是内置(Y)。这样内核镜像小,启动快,需要时再用modprobe加载。

举个例子,我在一个Intel Atom的工控项目里,发现默认配置编译出来的内核有8MB。经过裁剪,只保留串口、网卡、GPIO和I2C驱动,最后内核只有2.3MB。启动时间从12秒降到了4秒。你想想看,这差距有多大。

避坑指南:我曾经在配置内核时,不小心把CONFIG_DEVTMPFS给关了。结果系统启动后,/dev目录是空的,所有设备节点都找不到。折腾了两天才发现是这个问题。所以,devtmpfs一定要开启,除非你明确知道自己在做什么。

3.3 编译与烧录:从源码到二进制

配置完成后,就是编译。命令很简单:

make -j$(nproc)   # 用所有CPU核心并行编译
make modules      # 编译模块
make modules_install  # 安装模块到目标目录

这里有个关键点:交叉编译。如果你的开发机是x86,目标板是ARM,那必须指定交叉编译器。比如:

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
make -j4

烧录方式取决于你的板子。常见的有:

  • U-Boot的tftp下载:通过网络加载内核镜像,适合调试阶段。
  • 直接写入eMMC/NAND:用dd命令或厂商工具写入。
  • SD卡启动:把内核放到SD卡第一个分区,U-Boot从那里读取。

我个人习惯在调试阶段用tftp,因为改完内核不用反复插拔SD卡。等稳定了,再烧录到板载存储。

3.4 U-Boot引导流程分析:从复位到内核

U-Boot是嵌入式Linux的标配引导程序。它的流程大致是这样的:

  1. 第一阶段(SPL):芯片上电后,先从ROM加载SPL(Secondary Program Loader)。SPL很小,主要初始化DDR和时钟,然后加载完整的U-Boot。
  2. 第二阶段(U-Boot proper):完整的U-Boot启动,初始化更多外设(网卡、存储等),然后进入命令行或自动启动脚本。
  3. 加载内核:U-Boot从存储设备(eMMC、SD卡、网络)读取内核镜像和设备树,放到内存指定地址,然后跳转执行。

为什么要有SPL?因为芯片内部的SRAM通常只有几十KB,装不下完整的U-Boot。所以先跑一个小程序初始化DDR,再把大程序加载到DDR里运行。这个设计很巧妙,对吧?

注意:U-Boot的环境变量(env)存储在eMMC或SPI Flash的特定分区。如果你改了环境变量但没保存,下次启动还是老样子。用saveenv命令保存。我曾经因为忘了保存,反复调试了十几次才发现问题。

3.5 设备树(Device Tree)基础:硬件描述语言

设备树,说白了就是描述板子硬件信息的文件。它用.dts(Device Tree Source)格式编写,编译成.dtb(Device Tree Blob)供内核使用。

一个简单的设备树片段:

/dts-v1/;
#include "imx6ul.dtsi"

/ {
    model = "My Custom Board";
    compatible = "my,imx6ul-board";

    memory {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  // 256MB DDR
    };

    &uart1 {
        status = "okay";
        pinctrl-0 = <&pinctrl_uart1>;
    };
};

这里的关键点:

  • compatible:告诉内核这个板子兼容哪个型号。内核通过这个字符串匹配对应的驱动。
  • reg:描述地址和大小。比如内存的起始地址和大小。
  • status:控制外设是否启用。设为"okay"启用,"disabled"禁用。

我在项目中遇到过一个问题:板子上的以太网死活不通。查了半天,发现设备树里网口的phy-mode写成了"rgmii",但实际硬件是"rmii"。改过来就好了。所以,设备树一定要和硬件一一对应,差一点都不行。

调试技巧:用dtc -I dtb -O dts可以把编译好的.dtb反编译成.dts,方便检查内核实际加载的设备树内容。我经常用这个命令确认我的修改有没有生效。

好了,这一章的内容就到这里。内核定制和启动流程,是嵌入式Linux的基石。你把这些搞懂了,后面做驱动开发、性能优化,都会顺手很多。下一章,我们聊聊内核调试工具,比如kgdb和ftrace,敬请期待。