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 整体流程概览

好了,我们把整个流程串起来,画一张图,大家就一目了然了。

从Bootloader到内核启动参数传递流程 从Bootloader到内核启动参数传递流程 1. ROM Code 芯片固化代码 2. SPL/MLO 一级引导 3. U-Boot 完整引导程序 4. 内核 参数传递的关键动作 ① U-Boot 从存储设备(eMMC/SD/NAND)加载内核镜像(zImage/Image)到内存 ② U-Boot 加载设备树(.dtb)到内存,并记录其地址 ③ U-Boot 设置内核命令行参数(bootargs),或通过设备树传递 ④ U-Boot 跳转到内核入口地址,同时将设备树地址放在指定寄存器(r2/r0)中 总结:Bootloader 是硬件与内核之间的桥梁 没有正确的参数传递,内核就像蒙着眼睛跑步——跑得再快也会撞墙

这张图把流程分成了四个阶段:

  1. ROM Code: 芯片出厂固化的代码,上电第一件事就是执行它。它通常很小,负责初始化最基本的硬件(比如从SD卡的第一扇区读取SPL)。
  2. SPL/MLO: 二级引导程序。因为ROM Code太小,干不了复杂的事。SPL负责初始化DDR,然后从存储设备加载完整的U-Boot。
  3. U-Boot: 完整的Bootloader。它提供命令行交互,可以加载内核、设备树,设置启动参数,最后跳转到内核。
  4. 内核: 拿到控制权后,解析设备树,初始化驱动,挂载根文件系统,最终启动用户空间的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的源码怎么改、内核怎么解析参数等等。

记住一句话:启动参数传递,是嵌入式系统稳定性的第一道关卡。 这道关把好了,后面就顺了。