1. 课程导论:什么是Bootloader?为什么需要传递参数给内核?整体流程概览。
大家好,欢迎来到这门课。我是你们的老朋友,一个在嵌入式底层摸爬滚打了十几年的工程师。
咱们这门课,讲的是从Bootloader到内核的启动参数传递。听起来有点绕口是吧?说白了,就是搞清楚你的开发板或者手机,按下电源键之后,在操作系统界面亮起来之前,那短短几秒钟里,到底发生了什么。
我个人习惯把整个启动过程比作一场接力赛。Bootloader是第一棒,内核是第二棒。第一棒跑得稳不稳,交接棒顺不顺畅,直接决定了第二棒能不能跑起来。我们今天就来聊聊第一棒,以及交接棒这个关键动作。
1.1 什么是Bootloader?
Bootloader,直译过来就是“引导加载程序”。它的核心任务就两个:初始化硬件,然后加载并启动操作系统内核。
你想想看,芯片刚上电的时候,内存(DDR)还没初始化,时钟还是默认的慢速模式,连个像样的C语言运行环境都没有。这时候你让内核怎么跑?内核是个庞然大物,它需要在一个相对稳定的环境里才能运行。
Bootloader就是干这个脏活累活的。它就像一个先遣队,先把阵地(硬件)打扫干净,再把大部队(内核)接进来。
Bootloader的核心职责:
- 硬件初始化: 设置时钟、初始化DDR、初始化串口(方便我们调试)、初始化存储控制器(比如NAND Flash或eMMC)。
- 建立内存映射: 让CPU知道哪些地址是内存,哪些是外设。
- 加载内核镜像: 从Flash、SD卡或者网络(TFTP)上把内核镜像读到内存里。
- 传递启动参数: 把硬件信息、启动配置等关键数据告诉内核。
- 跳转到内核: 把CPU的控制权交给内核。
常见的Bootloader有哪些?我列个表,大家心里有个数。
| Bootloader | 适用场景 | 特点 |
|---|---|---|
| U-Boot | ARM、MIPS、RISC-V等 | 功能最全,社区活跃,几乎是嵌入式Linux的事实标准 |
| GRUB | x86 PC、服务器 | 支持多系统引导,配置灵活 |
| Barebox | ARM、x86 | 设计更现代,代码更清晰,但生态不如U-Boot |
| Coreboot | x86、ARM | 启动速度极快,主要用于Chromebook和服务器 |
我在项目中遇到过不少新手,觉得Bootloader就是U-Boot,U-Boot就是Bootloader。其实不是的。U-Boot只是最流行的一种。在一些资源受限的MCU上,我们甚至自己写一个几百行的汇编Bootloader就够了。
1.2 为什么需要传递参数给内核?
好,Bootloader把内核加载到内存了,然后呢?直接跳过去跑?不行。内核需要知道它跑在什么硬件上。
举个例子。你的开发板用的是512MB的DDR,但内核不知道啊。它可能默认只认256MB,那剩下的256MB就浪费了。或者你的板子用的是串口2作为调试口,但内核默认去初始化串口1,那你的调试信息就全丢了。
所以,Bootloader必须把硬件配置信息“告诉”内核。这个“告诉”的过程,就是传递启动参数。
传递参数的方式,主要有两种:
- ATAG(ARM Tagged List): 老式ARM Linux用的,一个链表结构,里面塞满了各种标签(内存大小、命令行参数、机器类型ID等)。
- 设备树(Device Tree,DTB): 现在的主流方式。把硬件信息写在一个.dts文件里,编译成.dtb二进制文件,Bootloader把它和内核镜像一起加载到内存,然后把地址告诉内核。
一个小技巧: 我个人习惯在调试阶段,把内核命令行参数(比如 console=ttyS0,115200 root=/dev/mmcblk0p2)写死在Bootloader的环境变量里。这样改起来方便,不用每次都重新编译内核。但产品发布时,一定要用设备树,把参数固化在硬件描述里,更规范,也更容易维护。
我曾经在一个项目里吃过亏。当时图省事,把内存大小写死在了Bootloader的代码里。后来换了更大内存的芯片,忘了改Bootloader,结果内核只认到一半内存,系统跑起来各种OOM(内存耗尽)。排查了半天才发现是参数没传对。从那以后,我再也不敢把硬件参数写死在代码里了,一律用设备树或者从硬件寄存器里动态读取。
1.3 整体流程概览
好了,我们把整个流程串起来,画一张图,大家就一目了然了。
这张图把流程分成了四个阶段:
- ROM Code: 芯片出厂固化的代码,上电第一件事就是执行它。它通常很小,负责初始化最基本的硬件(比如从SD卡的第一扇区读取SPL)。
- SPL/MLO: 二级引导程序。因为ROM Code太小,干不了复杂的事。SPL负责初始化DDR,然后从存储设备加载完整的U-Boot。
- U-Boot: 完整的Bootloader。它提供命令行交互,可以加载内核、设备树,设置启动参数,最后跳转到内核。
- 内核: 拿到控制权后,解析设备树,初始化驱动,挂载根文件系统,最终启动用户空间的init进程。
注意: 并不是所有平台都有SPL这个阶段。一些简单的MCU或者FPGA软核,可能ROM Code直接就把U-Boot加载了。但ARM Cortex-A系列的主流SoC(比如TI的AM335x、NXP的i.MX系列),基本都是三级启动:ROM Code → SPL → U-Boot。这个层级关系,大家心里要有数。
嗯,到这里,我们对Bootloader是什么、为什么要传参、整体流程怎么走,应该有了一个清晰的轮廓。后面的章节,我们会深入到每一个细节里去,比如设备树怎么写、U-Boot的源码怎么改、内核怎么解析参数等等。
记住一句话:启动参数传递,是嵌入式系统稳定性的第一道关卡。 这道关把好了,后面就顺了。