4. 内核编译与启动:从配置到跑起来

各位同学,今天我们来聊聊嵌入式Linux开发中最核心的一环——内核编译与启动。说实话,我见过太多工程师在这块栽跟头了。我自己刚入行那会儿,也曾经因为一个配置选项没选对,折腾了整整三天。

嗯,咱们今天就把这块彻底讲透。从内核配置开始,一直到U-Boot引导,最后看启动日志。你跟着我走一遍,以后遇到类似问题心里就有底了。

4.1 内核配置(menuconfig)—— 别怕,就是个图形界面

很多人第一次看到menuconfig,觉得这东西很神秘。其实说白了,它就是Linux内核的“开关面板”。你想让内核支持什么功能,就在这里勾选。

我个人习惯的做法是:先找一个接近的默认配置,然后在此基础上修改。比如我们做车规级芯片,一般会从 defconfig 开始。

# 以i.MX8为例
make ARCH=arm64 defconfig
make ARCH=arm64 menuconfig

进入menuconfig后,你会看到这样的界面:

  • General setup — 通用设置,比如内核版本号、压缩方式
  • Enable loadable module support — 模块支持,建议选上
  • Device Drivers — 设备驱动,我们最常打交道的地方
  • File systems — 文件系统支持

这里有个避坑指南:我曾经在某个项目中,因为没选 CONFIG_EXT4_FS,导致根文件系统挂载失败。你想想看,内核都跑起来了,结果找不到文件系统,多尴尬。

我的小技巧: 每次修改配置后,记得把 .config 文件备份一下。我习惯命名为 config_$(date +%Y%m%d),方便回退。

4.2 编译内核与设备树 —— 两条腿走路

配置好了,接下来就是编译。这里我要强调一点:内核镜像和设备树是分开编译的。很多人以为只编一个 zImage 就完事了,其实不对。

# 编译内核
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j4

# 编译设备树
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs

编译完成后,你会得到两个关键文件:

文件 路径 说明
Image / zImage arch/arm64/boot/ 内核镜像本体
.dtb 文件 arch/arm64/boot/dts/ 设备树二进制文件

我记得有一次,客户反馈说网卡不工作。我查了半天,最后发现是设备树里GPIO的pinmux配置错了。嗯,设备树这东西,错一个引脚,整个外设就废了。

注意: 设备树编译时,dts 源文件里如果引用了 dtsi 头文件,一定要确保路径正确。我见过有人把 #include 写成了 #include "../soc.dtsi",结果编译出来的dtb完全不对。

4.3 U-Boot引导流程分析 —— 内核是怎么被叫醒的

U-Boot,说白了就是内核的“闹钟”。它负责把内核从Flash里读到内存,然后跳转过去执行。整个过程分几步:

  1. 初始化硬件 — 时钟、DDR、串口等
  2. 加载内核和设备树 — 从存储介质读到内存
  3. 设置启动参数 — 比如 bootargs
  4. 跳转到内核入口 — 把控制权交给内核

这里我贴一段典型的U-Boot启动脚本:

setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
load mmc 1:1 0x80000000 Image
load mmc 1:1 0x83000000 imx8mp-evk.dtb
booti 0x80000000 - 0x83000000

你可能会问:为什么内核地址是 0x80000000?这个地址其实是芯片厂商规定的。对于i.MX8MP来说,DDR的起始地址就是 0x80000000。嗯,这个你得看芯片手册。

关键点: U-Boot的 booti 命令用于启动ARM64内核。如果是32位ARM,用 bootzbootm。别搞混了,我见过有人用 bootz 启动64位内核,结果卡死在启动阶段。

4.4 内核启动日志解读 —— 学会看“天书”

内核启动日志,很多人觉得密密麻麻看不懂。其实它是有规律的。我一般按时间线来读:

  • 早期启动 — 打印内核版本、编译信息
  • CPU和内存初始化 — 检测CPU核数、内存大小
  • 设备驱动加载 — 每个驱动probe时打印的信息
  • 文件系统挂载 — 最后一步,挂载根文件系统

举个例子,这是我从实际项目中截取的一段日志:

[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034]
[    0.000000] Linux version 5.15.32 (user@host) (gcc 9.3.0) #1 SMP PREEMPT
[    0.000000] Memory: 2048MB available
[    0.500000] mmc1: new high speed SD card at address 0x1234
[    0.800000] EXT4-fs (mmcblk1p2): mounted filesystem with ordered data mode

看到 [ 0.000000] 了吗?这是内核启动后的时间戳,单位是秒。如果某个驱动卡住了,你看时间戳就能定位到问题。

我曾经遇到一个案例:日志停在 [ 1.200000] 就不动了。后来发现是某个I2C设备在probe时没响应,导致内核超时等待。嗯,这种问题,看日志比猜快多了。

调试建议: 如果内核启动到一半就挂了,可以加 earlyprintk 参数。这样在串口驱动还没初始化之前,就能看到早期打印信息。我靠这招救过好几次急。

4.5 常见问题与排查思路

最后,我总结几个我实际工作中遇到的高频问题:

现象 可能原因 排查方法
内核启动后无控制台 console参数错误或串口驱动未编译 检查bootargs中的console=ttyXXX
挂载根文件系统失败 文件系统类型不支持或设备节点错误 确认CONFIG_EXT4_FS等选项已开启
设备树加载后外设不工作 设备树中地址或中断号配置错误 对比芯片手册,检查reg和interrupts属性

好了,这一章的内容就到这里。内核编译与启动这块,说白了就是“配置-编译-引导-调试”四个步骤。你多练几次,自然就熟了。下一章我们聊聊设备树的具体写法,到时候我会拿一个真实的车规级外设来举例。