4. Linux内核编译与配置:内核源码获取、内核配置系统(Kconfig/Makefile)、内核编译与烧写

好,咱们今天聊聊Linux内核的编译与配置。说实话,这是嵌入式Linux开发中最基础、也最容易踩坑的一环。我当年刚入行时,第一次编译内核就折腾了整整两天——不是缺工具链,就是配置选项选错了,最后烧进去系统起不来。嗯,这些教训让我后来养成了不少好习惯,今天一并分享给你。

4.1 内核源码获取

先说说源码从哪来。你可能会想,这还不简单?去kernel.org下载呗。对,但嵌入式开发没那么简单。

我个人习惯的做法是:

  • 官方主线内核:从 kernel.org 获取,适合学习或通用平台
  • 芯片厂商提供的BSP内核:比如NXP、TI、Rockchip等厂商会维护自己的内核分支,里面包含了板级支持包和驱动
  • SoC厂商的Git仓库:很多厂商在GitHub或Gitee上维护了内核仓库,比如 torvalds/linux 的某个分支
我的建议:刚开始学习时,直接用主线内核就好。等做到具体项目,再用厂商提供的BSP。我在一个项目中就吃过亏——用了主线内核,结果某个外设驱动死活调不通,后来换成厂商分支,问题迎刃而解。

获取源码的命令很简单:

# 从主线仓库克隆(以5.10 LTS为例)
git clone --depth=1 -b v5.10 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git

# 或者下载压缩包
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.tar.xz
tar -xf linux-5.10.tar.xz

为什么要加 --depth=1?说白了,就是只拉取最新的一次提交,省时间省空间。完整的历史记录动辄几个GB,你想想看,嵌入式开发板子的存储空间本来就紧张,没必要。

4.2 内核配置系统:Kconfig与Makefile

内核的配置系统,说白了就是一套「搭积木」的机制。Kconfig定义「有哪些积木块」,Makefile定义「怎么把这些积木块搭起来」。

4.2.1 Kconfig:配置选项的定义

Kconfig文件分布在各个子目录中,比如 drivers/i2c/Kconfig 里定义了I2C相关的配置选项。它的语法其实不难理解:

config I2C_CHARDEV
    tristate "I2C /dev entries support"
    depends on I2C
    help
      Say Y here to enable I2C device interface.
      This provides a /dev/i2c-N interface.

这里 tristate 表示三种状态:Y(编译进内核)、M(编译成模块)、N(不编译)。depends on 是依赖关系——你选了I2C字符设备,前提是I2C核心已经被选中。

关键点:Kconfig的依赖关系是链式的。我曾经在配置一个音频驱动时,选了某个编解码器,结果它依赖I2C、GPIO、DMA三个子系统。漏掉任何一个,编译就报错。所以配置时一定要看仔细了。

4.2.2 Makefile:编译规则

Makefile决定了哪些文件被编译,以及怎么编译。举个例子:

# drivers/i2c/Makefile
obj-$(CONFIG_I2C_CHARDEV) += i2c-dev.o
obj-$(CONFIG_I2C_ALGOBIT) += i2c-algo-bit.o

这里的 obj-$(CONFIG_I2C_CHARDEV) 会被展开成 obj-yobj-m 或空。如果配置成Y,就编译进内核;配置成M,就编译成模块;没配置,就不编译。

你想想看,这套机制是不是很巧妙?Kconfig负责「你要什么」,Makefile负责「怎么给」。两者配合得天衣无缝。

4.2.3 配置方法

内核提供了多种配置界面:

命令 说明 适用场景
make menuconfig 基于ncurses的文本菜单 最常用,SSH远程也适用
make xconfig 基于Qt的图形界面 桌面环境,操作直观
make defconfig 使用默认配置 快速开始,再手动微调
make savedefconfig 生成精简的配置文件 保存最小化配置

我个人最常用的是 make menuconfig。为什么?因为嵌入式开发经常在服务器或虚拟机上进行,没有图形界面。而且menuconfig支持搜索(按 / 键),找某个选项特别方便。

小技巧:配置完成后,配置信息保存在 .config 文件中。我建议你把这个文件备份一份,命名为 config_myboard。下次重新编译时,直接 cp config_myboard .config,省得从头配。

4.3 内核编译

配置好了,接下来就是编译。嗯,这一步其实最「无脑」,但也是最容易出问题的。

4.3.1 设置交叉编译工具链

嵌入式开发嘛,肯定是在PC上编译,在开发板上运行。所以需要指定交叉编译器:

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

ARCH 指定目标架构,CROSS_COMPILE 指定交叉编译器的前缀。这里要注意,你的工具链路径要加到 PATH 环境变量里。

注意:我曾经犯过一个低级错误——忘了设置 CROSS_COMPILE,结果编译出来的内核是x86架构的,烧到ARM板上当然起不来。折腾了半天才发现,真是欲哭无泪。

4.3.2 编译产物

编译完成后,主要生成以下文件:

  • arch/arm/boot/zImage:压缩后的内核镜像
  • arch/arm/boot/uImage:带U-Boot头部的镜像(如果配置了)
  • arch/arm/boot/dts/:设备树文件(.dtb)
  • vmlinux:未压缩的内核,用于调试

对于大多数嵌入式平台,我们最终需要的是 zImageuImage 加上对应的 .dtb 文件。

4.3.3 编译内核模块

如果有些驱动配置成了模块(M),还需要单独编译和安装模块:

make modules
make modules_install INSTALL_MOD_PATH=/path/to/rootfs

INSTALL_MOD_PATH 指定模块安装到根文件系统的路径。模块会按 /lib/modules/$(KERNEL_VERSION)/ 的目录结构安装。

4.4 内核烧写

编译好了,怎么烧到板子上?这取决于你的硬件平台和启动方式。

4.4.1 通过U-Boot烧写

大多数嵌入式Linux板子都用U-Boot作为bootloader。烧写步骤一般是:

  1. 将内核镜像和设备树文件放到TFTP服务器或SD卡上
  2. 在U-Boot命令行中设置启动参数
  3. 使用 tftpfatload 命令加载到内存
  4. 使用 bootmbootz 启动

举个例子:

# 从TFTP加载内核
tftp 0x82000000 zImage
tftp 0x83000000 myboard.dtb

# 启动内核
bootz 0x82000000 - 0x83000000

4.4.2 通过烧录工具烧写

有些平台(比如全志、瑞芯微)有专门的烧录工具,通过USB将镜像烧写到NAND Flash或eMMC中。这类工具通常需要把内核镜像打包成特定的格式。

经验之谈:我建议你在开发阶段用TFTP或NFS启动,这样修改内核后不用反复烧写,直接重启就能看到效果。等内核稳定了,再烧写到板载存储中。这个习惯能帮你节省大量时间。

4.4.3 烧写后的验证

烧写完成后,上电启动。如果系统能正常启动到shell,恭喜你,内核编译和烧写成功了。如果启动卡住了,别慌,看看串口输出的内核日志,通常能定位到问题。

常见的启动失败原因:

  • 设备树与硬件不匹配
  • 根文件系统路径或类型配置错误
  • 内核缺少关键驱动(比如存储控制器驱动)
  • 启动参数中的内存地址不对

4.5 小结

内核编译与配置,说白了就是三步:拿源码、配选项、编译烧写。但每一步都有不少细节。我建议你从一块成熟的开发板开始,比如BeagleBone Black或树莓派,它们的社区支持好,资料多。等熟悉了流程,再挑战自己画板子的项目。

嗯,今天就聊到这儿。下一章咱们会深入设备树,这可是嵌入式Linux开发的「硬骨头」,到时候咱们好好啃一啃。