第三章 Linux内核编译与定制:从源码到启动

好,咱们进入第三章。这一章可以说是整个驱动开发的基础——你得先把内核跑起来,对吧?

我见过不少新手,拿到开发板第一件事就是找现成的镜像烧进去。这当然能亮机,但说实话,你什么都没学到。真正有价值的是——自己动手编译内核,定制功能,然后看着自己编译出来的系统在板子上跑起来。那种成就感,嗯,很不一样。

3.1 获取内核源码

先说源码从哪来。i.MX8M 系列用的内核,NXP 官方维护在 GitHub 上。我个人习惯直接从 NXP 的 linux-imx 仓库拉取。

官方仓库地址:

git clone https://github.com/nxp-imx/linux-imx.git
cd linux-imx
git checkout lf-5.15.y  # 我推荐这个分支,稳定

为什么选 lf-5.15.y?我在项目里踩过坑——太新的内核(比如 6.x)有些外设驱动还不完善,太老的又缺功能。5.15 是长期支持版,NXP 的 BSP 也主要基于这个分支,兼容性最好。

小技巧:如果你用的是某款具体的开发板(比如 i.MX8M Mini EVK),NXP 还提供了完整的 BSP 包,里面包含了内核、U-Boot、Yocto 构建脚本。但咱们做驱动开发,单独编译内核更灵活。

3.2 配置内核——menuconfig 实战

源码拉下来后,第一件事不是直接编译,而是配置。说白了,就是告诉内核:你要支持哪些硬件,要开启哪些功能。

i.MX8M 的默认配置在 arch/arm64/configs/ 目录下。以我常用的 i.MX8M Mini 为例:

make ARCH=arm64 defconfig        # 生成默认配置
make ARCH=arm64 menuconfig       # 图形化配置界面

menuconfig 界面一出来,你可能会有点懵——选项太多了。别慌,我教你几个实用操作:

  • 按 "/" 键:搜索功能。比如你想找 I2C 驱动,直接搜 "I2C",所有相关选项就出来了
  • 按 "?" 键:查看当前选项的帮助信息,里面有依赖关系和简要说明
  • 按 "Z" 键:显示所有选项(包括被依赖隐藏的)

注意:menuconfig 里选项有三种状态:[*] 表示编译进内核,[M] 表示编译成模块,[ ] 表示不编译。我建议把常用的驱动(比如 GPIO、I2C、UART)直接编译进内核,别搞成模块。为什么?因为模块加载需要文件系统支持,万一你文件系统还没起来,驱动就加载不了——死循环了。

我曾经在一个项目里,把 SD 卡驱动配成了模块。结果内核启动后,文件系统在 SD 卡上,但 SD 卡驱动还没加载...你想想看,这不就卡死了吗?从那以后,根文件系统相关的驱动我全编进内核。

3.3 编译内核与设备树

配置好了,接下来就是编译。i.MX8M 是 64 位 ARM 处理器,所以要用 aarch64 的交叉编译工具链。

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-

# 编译内核
make -j$(nproc) Image

# 编译设备树
make -j$(nproc) dtbs

# 编译内核模块(如果有)
make -j$(nproc) modules

这里说几个关键点:

  • -j$(nproc):用所有 CPU 核心并行编译,能快不少。我 8 核的机器编译一次大概 5 分钟
  • Image:这是未压缩的内核镜像,i.MX8M 的 U-Boot 可以直接加载
  • dtbs:设备树二进制文件,描述硬件信息。i.MX8M Mini 的默认设备树是 imx8mm-evk.dtb

编译产物位置:

文件 路径 说明
Image arch/arm64/boot/Image 内核镜像
设备树 arch/arm64/boot/dts/freescale/imx8mm-evk.dtb 硬件描述文件
内核模块 各驱动目录下的 .ko 文件 可动态加载的驱动

3.4 烧录与启动验证

编译好了,怎么烧到板子上?i.MX8M 支持多种启动方式:SD 卡、eMMC、NAND Flash、网络启动。我一般用 SD 卡,最方便。

步骤是这样的:

  1. 准备 SD 卡:分两个区,第一个区放内核和设备树(FAT32),第二个区放根文件系统(ext4)
  2. 复制文件:把 Image 和 dtb 文件拷到第一个分区
  3. 设置 U-Boot:启动时进入 U-Boot 命令行,设置启动参数

U-Boot 里的启动命令大概长这样:

# 从 SD 卡加载内核和设备树
fatload mmc 1:1 0x42000000 Image
fatload mmc 1:1 0x44000000 imx8mm-evk.dtb

# 设置启动参数
setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'

# 启动
booti 0x42000000 - 0x44000000

这里有个坑——内存地址。0x42000000 和 0x44000000 不是随便写的。i.MX8M Mini 的内存起始地址是 0x40000000,内核加载地址一般用 0x42000000(偏移 32MB),设备树用 0x44000000(再偏移 32MB)。不同型号可能不一样,你得查芯片手册。

验证方法:启动后,用 cat /proc/version 查看内核版本,用 dmesg | grep "Linux version" 确认编译时间。如果看到你编译时的用户名和时间戳,就说明成功了。

我记得有一次,烧录后板子死活起不来,串口输出停在 "Starting kernel ..."。折腾了半天,最后发现是设备树里内存大小配错了——我用的板子是 2GB 内存,但设备树里写的是 1GB。改过来就好了。所以,启动验证时一定要看串口日志,它会把问题说得明明白白。

3.5 避坑指南

最后,总结几个我踩过的坑,你遇到了可以少走弯路:

  • 工具链版本不匹配:太老的 gcc 编不了新内核。我建议用 Linaro 的 gcc 7.5 以上版本
  • 设备树里外设没使能:比如你加了 I2C 驱动,但设备树里 i2c 节点 status = "disabled",那驱动根本不会 probe。检查设备树!
  • 根文件系统格式不对:U-Boot 的 bootargs 里 root= 参数要跟实际分区对应。我习惯用 UUID 而不是设备名,因为设备名在不同内核版本里可能变
  • 缓存问题:如果改了设备树但没生效,试试 make clean && make dtbs。有时候增量编译会漏掉依赖

嗯,这一章的内容就这些。内核编译说难不难,说简单也不简单——关键是要理解整个流程:源码 -> 配置 -> 编译 -> 烧录 -> 验证。每一步都有它的道理。

下一章咱们开始写第一个驱动——字符设备驱动。到时候你会看到,内核编译的功底有多重要。