1. 课程导入

大家好,我是你们的讲师。做了十几年嵌入式底层,我始终觉得 Bootloader 是嵌入式系统里最容易被低估的角色。很多人觉得它不就是个引导程序嘛,把内核加载起来就完事了。但说实话,我见过太多项目因为 Bootloader 没配好,导致整块板子点不亮,最后查来查去,问题就出在 DDR 初始化时序或者设备树里一个 pinmux 配置错了。

这一章,我们先从宏观上把 Bootloader 的定位、启动流程和主流方案理清楚。你只有知道它“为什么存在”,后面做移植和调试时才能抓住重点。

1.1 Bootloader 在嵌入式系统中的角色

嵌入式系统上电后,CPU 会从一个固定的地址开始执行第一条指令。这个地址通常映射到片上的 ROM 或 Flash。但问题是——这时候 DDR 还没初始化,内核镜像还躺在 Flash 里,你总得有人先把硬件初始化好,再把内核从存储介质搬到内存里,最后跳过去执行。

这个“人”就是 Bootloader。

我习惯把 Bootloader 比作“系统看门人”。它负责三件事:

  • 硬件初始化:时钟、DDR、串口、存储控制器……这些外设在上电后都是“裸”的,Bootloader 得把它们伺候好。
  • 加载内核:从 Flash、SD 卡、网络等介质把内核镜像读到内存里。
  • 传递启动参数:把设备树、内核命令行、ATAG 等信息交给内核。

关键认知:Bootloader 不是操作系统的一部分,它是独立于内核的裸机程序。你换内核版本,Bootloader 通常不需要动;但你换板子,Bootloader 几乎必须重配。

1.2 典型启动流程:ROM Code → SPL → U-Boot → Kernel

现在的嵌入式 SoC 越来越复杂,启动流程也分成了多个阶段。我拿最典型的 ARM 平台举例,整个链条是这样的:

  1. ROM Code:芯片出厂固化的只读代码。它很小,通常只有几十 KB。ROM Code 负责从预设的启动介质(比如 SD 卡、eMMC、NAND Flash)里读取下一级程序到内部 SRAM 里。为什么不用 DDR?因为 DDR 还没初始化,内部 SRAM 是唯一能用的 RAM。
  2. SPL (Secondary Program Loader):也叫 MLO 或 x-loader。它由 ROM Code 加载到 SRAM 里执行。SPL 的任务很简单:初始化 DDR 时钟和控制器,然后把完整的 U-Boot 从存储介质拷贝到 DDR 里。
  3. U-Boot:完整的 Bootloader。它提供命令行、环境变量、网络协议栈、文件系统支持。U-Boot 加载设备树(.dtb)和内核镜像(zImage/Image),然后跳转到内核入口。
  4. Kernel:内核接管后,Bootloader 的使命就结束了。内核会重新初始化所有外设,挂载根文件系统,启动用户空间。

嗯,这里要注意:有些 SoC 没有 SPL 阶段,ROM Code 直接加载 U-Boot。但大多数现代 SoC(比如 TI AM335x、NXP i.MX 系列)都采用这种三级启动,因为内部 SRAM 太小,装不下完整的 U-Boot。

下面这张图把整个流程串起来了,你可以对照着看:

嵌入式系统典型启动流程 ROM Code 芯片固化代码 SPL 初始化DDR U-Boot 加载内核+设备树 Kernel 接管系统 存储介质:SD卡 / eMMC / NAND Flash / SPI NOR Flash ROM Code从介质读取SPL → SPL从介质读取U-Boot → U-Boot从介质读取Kernel 阶段1:硬件初始化 SRAM运行 阶段2:DDR就绪 DDR运行 阶段3:加载内核 DDR运行 阶段4:系统运行 DDR运行

我的经验:调试启动流程时,我习惯在 SPL 和 U-Boot 的串口输出里加一些自定义打印。比如在 DDR 初始化前后各打一个字符,这样能快速定位是 DDR 没配好,还是 U-Boot 根本没被加载进来。曾经有个项目,板子死活起不来,最后发现是 ROM Code 读取 SPL 时 CRC 校验失败——因为 SD 卡的分区表格式不对。

1.3 主流 Bootloader 对比:U-Boot vs Barebox vs RedBoot

市面上 Bootloader 不少,但真正在工业级产品里广泛使用的,也就那么几个。我列个表格,把它们的核心差异说清楚:

特性 U-Boot Barebox RedBoot
起源 PPCBoot → U-Boot,2000 年左右 U-Boot 分支,2010 年左右独立 eCos 生态,2000 年左右
社区活跃度 极高,几乎每款 SoC 都支持 中等,主要支持 ARM 和 RISC-V 低,基本停止维护
代码风格 C 语言 + 少量汇编,宏定义多 更接近 Linux 内核风格,清晰 C 语言,较老派
设备树支持 完整支持,从 2014 年起标配 完整支持,原生设计 不支持,需手动传参
命令行 功能强大,脚本支持好 类似 Linux shell,体验好 简单,功能有限
驱动模型 DM (Driver Model),2016 年后引入 原生驱动模型,更现代 无统一模型
适用场景 绝大多数嵌入式 Linux 产品 对代码质量要求高的项目 遗留系统、eCos 生态

你想想看,现在做新项目,基本没人会选 RedBoot 了。它连设备树都不支持,这在今天简直是不可想象的。Barebox 其实是个好东西,代码写得比 U-Boot 干净,但生态差太远——你随便买个开发板,厂商给的 BSP 里肯定是 U-Boot,不会是 Barebox。

避坑指南:我曾经在一个项目里试图把 Barebox 移植到一款 NXP i.MX6UL 的板子上。Barebox 的代码确实漂亮,驱动分层清晰,但问题在于——网上几乎找不到现成的板级配置,所有 DDR 参数、时钟树、pinmux 都得自己从参考手册里抠。折腾了两周,最后还是换回了 U-Boot。不是说 Barebox 不好,而是生态决定了你的开发效率。

1.4 为什么我选择 U-Boot 作为课程主线

这门课的所有实战内容,我都会基于 U-Boot 来展开。原因很简单:

  • 市场占有率:保守估计,80% 以上的嵌入式 Linux 产品在用 U-Boot。你出去面试,人家问的也是 U-Boot。
  • 设备树支持成熟:U-Boot 从 2014 年开始全面拥抱设备树,现在已经是标配。设备树配置和调试,正是这门课的核心。
  • 板级移植资料多:不管你是用 TI、NXP、Rockchip 还是 Allwinner 的 SoC,网上都能找到现成的 U-Boot 板级配置做参考。
  • 调试工具链完善:U-Boot 的网卡、TFTP、NFS 支持非常稳定,调试内核时特别方便。

当然,U-Boot 也有缺点——代码量巨大,宏定义满天飞,阅读体验不太好。但话说回来,做底层开发,哪有不啃代码的?