本文把"在电脑上编译 → 烧进 eMMC → 上电自动跑起 Linux"这条完整链路讲清楚,并理清几个最容易混淆的概念。
一、一句话解释:
用一套工具(交叉编译工具链),造出几样东西(U-Boot / 内核 / 设备树 / rootfs),烧进 eMMC 的不同位置,上电后逐级接力把 Linux 跑起来。
-
编译阶段(在 PC 上):用交叉工具链,编出引导程序、内核、设备树、根文件系统
-
烧录阶段:把这几样按固定布局写进 eMMC(或 SD 卡)
-
启动阶段(板子上电):BootROM → U-Boot → 内核 + 设备树 → 挂载 rootfs → 系统起来
下面逐阶段展开讲解。
二、编译阶段:
2.1交叉编译工具链
为什么需要"交叉"编译?因为 PC 是 x86 架构,而板子是 ARM64(aarch64)架构,指令集完全不同。x86 编出来的二进制 ARM 根本不认识。于是要在 PC 上用一套"专门生成 ARM 代码"的特殊编译器——这就是交叉编译:编译用的机器(x86)和运行的目标机器(ARM)是两种不同架构。
使用时通过两个参数告诉各项目的构建系统"请给 ARM 编、用这套交叉编译器":
sudo apt install gcc-aarch64-linux-gnu
-
ARCH=arm64—— 目标架构 -
CROSS_COMPILE=aarch64-linux-gnu-—— 交叉工具链前缀
前缀本身就编码了关键信息:aarch64(架构)- linux(系统)- gnu(用 glibc)。
2.2真正要编译的,其实只有三部分
1. U-Boot(引导程序)
└─ 单独编 ──→ u-boot.itb / idbloader.img
2. 内核(一条 make 三个产物,设备树和驱动都在这里)
├─ Image ──→ 内核本体
├─ dtb ────→ 设备树
└─ ko ─────→ 驱动模块(配成 =m 的驱动)
3. rootfs(根文件系统)
└─ Buildroot 编 / 或下现成的(下现成就不用编)(把编好的 .ko 拷进 /lib/modules/ ← 驱动在这被放进 rootfs)
2.2.1U-Boot(引导程序)
负责初始化 DDR 内存、读分区、把内核和设备树加载进内存。
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rk3588_defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
2.2.2内核
内核,设备树和模块不是和内核分开编的独立项目,它们都在内核源码树里,用同一份源码、同一个配置、同一条 make 命令编出来。
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image dtbs modules -j8
# └─┬─┘ └─┬─┘ └──┬──┘
# 内核本体 设备树 模块
-
Image—— 内核本体(arch/arm64/boot/Image) -
*.dtb—— 设备树二进制,描述本块板子的硬件 -
*.ko—— 内核模块,即可动态加载的那部分驱动
2.2.3根文件系统 rootfs
内核起来后第一件事是挂载根文件系统(/ 目录树,含 /bin、/etc、/lib 和 1 号进程 init/systemd)。没有它内核会直接 panic。来源有三:
-
下现成的 Ubuntu/Debian ARM64 rootfs —— 不算"编译",拿来即用,开发期推荐
-
Buildroot —— 自动交叉编译出一个精简 rootfs,适合产品
-
Yocto —— 工业级,量产常用
三、烧录阶段:东西怎么进板子
eMMC 里的固定布局
eMMC(板子的"硬盘")
├─ 前端固定偏移 ──→ U-Boot
├─ boot 分区 ────→ Image + dtb
└─ rootfs 分区 ──→ 根文件系统
└─ /lib/modules/ 下放着驱动模块 .ko
四、启动阶段:上电后的自动接力
烧好后,启动完全自动,你只要上电,接力链自己跑:
上电
│
① BootROM(固化在 SoC 内,出厂不可改)
│ 按优先级找介质(SD?eMMC?),到固定偏移处找到 U-Boot
│ 搬进 SRAM(此时 DDR 还没起来,只能用片内那点 SRAM)→跳过去
▼
② U-Boot(二级引导)
│ 核心任务:初始化 DDR 内存(分水岭,内存一起来后面才有大片空间)
│ 按 bootcmd 去 boot 分区读 Image 和 .dtb 加载进 DDR
│ 设置 bootargs(告诉内核 rootfs 在哪个分区)→跳转到内核
▼
③ Linux Kernel + 设备树
│ 内核解压运行,读 .dtb 认识硬件,初始化各驱动
│ 按 bootargs 指定位置挂载根文件系统
▼
④ init / systemd
│ 启动用户空间 → 登录提示符,系统起来了
关于 BootROM 选介质:它的自主权很小,要么由硬件 strap 引脚(拨码/电阻)定死,要么按内置固定优先级(RK 大致是 SD → eMMC → SPI → USB)逐个试,谁先有合法引导就用谁。
上面简单的描述了编译阶段,烧录阶段和启动阶段的大概流程。下面就简单的描述一下其他的东西。
五、.config、设备树、模块:三个最容易混的概念
.config 管"编译期编译哪些模块",设备树管"运行期现场有什么硬件、该取哪个驱动"
5.1 .config —— 决定"编什么、怎么编"(编译期)
.config 是内核源码根目录下的一个文本文件,记录每个功能/驱动的状态,可以自己选择编译哪些驱动:
CONFIG_I2C=y # 编进内核 Image(built-in)
CONFIG_SENSORS_MPU6050=m # 编成独立 .ko 模块
# CONFIG_SOME_DRIVER is not set # 根本不编
每个驱动有三种归宿:
-
=y→ 编进内核 Image,开机自带,改它要重编整个内核 -
=m→ 编成独立.ko,运行时动态加载,改它只需单独编这一个 -
not set→ 不编
.config 从哪来? 它不是天生的,需要生成,最常见三种来源:
# 来源一:厂商提供的板级基线(99% 会用这个)
make ARCH=arm64 rk3588_defconfig # 展开 defconfig 生成 .config
# 来源二:在基线上用菜单微调(加你自己的驱动)
make ARCH=arm64 menuconfig
# 来源三:从正在运行的系统抠出来(想复现现有内核配置时)
zcat /proc/config.gz > .config # 在板子上执行
defconfig 与 .config 的区别:defconfig 是精简版(只记和默认不同的项,适合存 git),.config 是它展开后的完整版(几千行,编译真正读的那个)。
5.2设备树 —— 描述"有哪些硬件"(运行期)
设备树描述的是"硬件",不是"驱动模块"
例如:
&i2c1 {
mpu6050@68 {
compatible = "invensense,mpu6050"; // 这颗芯片是什么
reg = <0x68>; // I2C 地址 0x68
};
};
它只说"板子上物理存在这么一颗芯片",完全不提"用哪个驱动""编了哪些模块"。它甚至不关心这颗芯片有没有驱动。
5.3内核启动时的配合 —— 把硬件和驱动连起来
那"硬件"和"驱动"是怎么对上的?靠内核启动时的配对,双方都写同一个 compatible 字符串:
设备树说:有个芯片,compatible = "invensense,mpu6050"
│
│ 内核拿这个字符串去"驱动登记表"里找
▼
找哪个驱动声明"我支持 invensense,mpu6050"(驱动里的 of_match_table)
│
┌─────────┴─────────┐
找到了 找不到
加载它,驱动这颗芯片 这颗芯片没人管,不工作
六、加一个驱动,需要做什么呢?
分两种情况:
情况 A:配成模块 =m(开发期)
只需单独编这一个模块,不用重编内核 Image,不用重编设备树(除非同时接了新硬件),不用重编其他模块,不用重编 rootfs——只是往它的 /lib/modules/ 里拷一个 .ko 文件,这是往文件夹放文件,不是重编文件系统。
情况 B:配成 built-in =y
要重编内核 Image,烧上去,重启才生效
七、总结
1. 编译阶段(PC 上):用交叉工具链,把 x86 代码编成 ARM 能跑的
├─ 编引导程序 U-Boot ──────────────→ u-boot.itb / idbloader.img
├─ 编内核(一条 make 三个产物)
│ ├─ Image ─────────────────────→ 内核本体
│ ├─ dtb ───────────────────────→ 设备树(描述本板硬件)
│ └─ ko ────────────────────────→ 驱动模块(配成 =m 的驱动在这编出)
└─ 备 rootfs(下现成 或 Buildroot 编)
└─ 把编好的 .ko 拷进 /lib/modules/ ← 驱动在这被放进 rootfs
注:驱动若配成 =y,则不生成 .ko,直接链进上面的 Image
2. 烧录阶段:把这几样按固定布局写进 eMMC(或先写 SD 卡)
├─ eMMC 前端固定偏移 ─────────────→ U-Boot
├─ boot 分区 ─────────────────────→ Image + dtb
└─ rootfs 分区 ───────────────────→ 根文件系统
└─ 里面就带着 /lib/modules/ 下的那些 .ko ← 驱动跟 rootfs 一起进板子
3. 启动阶段(上电):逐级接力,系统全自动跑起来
① BootROM ──→ 固化在 SoC,搬运下一级(它本身不算 bootloader)
② U-Boot ───→ 初始化 DDR 内存,加载 Image + dtb 进内存
③ 内核+dtb ─→ 内核读设备树认硬件
│ └─ 配成 =y 的驱动已在 Image 里,此刻自带运行
④ 挂 rootfs ─→ /lib/modules/ 此刻才可见
│ └─ 内核/udev 按设备树里的硬件,加载匹配的 .ko ← 驱动被加载
⑤ 系统起来 ──→ init/systemd 启动用户空间,登录提示符
用一套交叉工具链,造出四样东西(U-Boot / 内核 / 设备树 / rootfs),烧进 eMMC 的固定布局,上电后 BootROM → U-Boot → 内核+设备树 → 挂 rootfs 逐级接力跑起 Linux。其中 .config 决定编译期生产什么、设备树描述运行期有什么硬件、内核启动时靠 compatible 把两者整合。