1. Bootloader概述:嵌入式系统的“第一把火”

大家好,我是你们这门课的老朋友。今天咱们聊聊Bootloader。说白了,它就是嵌入式系统上电后运行的第一个程序。你想想看,CPU一上电,内存还是空的,Flash里虽然存了系统,但CPU自己可不知道怎么把系统搬进内存。这时候就需要一个“引路人”——Bootloader。

我刚开始做BSP的时候,总觉得Bootloader就是个“跳板”,随便弄弄就行。后来在一个项目里,因为Bootloader分区没规划好,导致整个产线返工。嗯,从那以后我再也不敢小看它了。

1.1 什么是Bootloader

Bootloader,直译就是“引导加载程序”。它负责初始化硬件、建立内存映射,然后把操作系统内核加载到内存里,最后跳转过去执行。没有它,你的Linux、RTOS都只是一堆躺在Flash里的二进制数据。

核心定义:Bootloader是嵌入式系统上电后第一个执行的程序,负责硬件初始化、系统引导和启动流程控制。

我习惯把Bootloader分成两个阶段:

  • 第一阶段(Stage 1):汇编写的,极小。初始化CPU、关闭看门狗、设置时钟、初始化DDR。这部分跟硬件绑死,移植性差。
  • 第二阶段(Stage 2):C语言写的,功能丰富。支持文件系统、网络、命令行交互,负责加载内核。

为什么会这样分?因为第一阶段代码受限于CPU内部SRAM大小,通常只有4KB-64KB。你想想看,这点空间连个完整的文件系统驱动都塞不下,更别提网络协议栈了。所以必须拆成两段。

1.2 Bootloader在嵌入式系统中的作用

Bootloader的作用,我总结为四个字:承上启下。具体来说:

作用 说明 我踩过的坑
硬件初始化 设置时钟、初始化DDR、配置GPIO/中断控制器 有一次DDR时序配错,系统死活起不来,查了三天
系统引导 从Flash/网络/串口加载内核到内存 分区偏移算错,内核加载到错误地址,直接死机
更新恢复 支持固件升级、系统恢复 产线上刷错分区,整批板子变砖,还好Bootloader有备份
调试诊断 提供命令行、内存读写、寄存器查看 没有串口输出时,调试全靠点LED,痛苦

避坑指南:我曾经在一个项目里,Bootloader和内核的分区大小没对齐Flash的擦除块大小。结果每次升级都要多擦一块,导致分区边界数据被误擦。后来我强制要求:所有分区起始地址和大小必须对齐Flash的物理擦除块(通常是64KB或128KB)。

1.3 常见的Bootloader方案

市面上Bootloader方案不少,但真正在生产中大规模用的,就那么几个。我按自己的经验排个序:

U-Boot(Das U-Boot)

这玩意儿是嵌入式Linux的“事实标准”。支持ARM、RISC-V、x86、MIPS……几乎你能想到的所有架构。功能极其丰富:网络启动、TFTP下载、NFS挂载、Flash读写、环境变量、脚本支持。

我个人习惯用U-Boot做主力。为什么?社区活跃,遇到问题Google一下基本有答案。而且它的分区管理机制很成熟——通过mtdparts命令定义分区表,内核和设备树都能共用同一套分区信息。

# U-Boot中查看分区表
=> mtdparts

device nand0 <NAND 512MiB 3,3V 8-bit>, # parts = 4
 #: name                size            offset          mask_flags
 0: spl                 0x00040000      0x00000000      0
 1: uboot               0x001c0000      0x00040000      0
 2: env                 0x00040000      0x00200000      0
 3: kernel              0x00800000      0x00240000      0
 4: rootfs              0x1d800000      0x00a40000      0

小技巧:U-Boot的环境变量默认保存在Flash里。我建议把环境变量单独划一个分区,大小至少两个擦除块。这样万一写坏一个,还有备份可以恢复。

Barebox

Barebox是U-Boot的一个“现代派”替代品。它借鉴了Linux内核的驱动模型和设备树机制,代码结构更清晰。如果你做过Linux驱动开发,看Barebox的代码会感觉很亲切。

我记得有一次给一个新平台移植Bootloader,U-Boot的板级代码太乱,各种宏定义嵌套。换成Barebox后,设备树直接描述硬件,驱动用Linux风格写,移植速度快了一倍。

特性 U-Boot Barebox
代码风格 自有框架,宏多 类Linux内核,清晰
设备树支持 较晚加入,兼容性一般 原生支持,与内核一致
分区管理 mtdparts + 环境变量 设备树描述 + 分区表
社区活跃度 极高,资料多 中等,但增长快
适用场景 几乎所有嵌入式Linux 新平台、对代码质量要求高的项目

其他方案

  • RedBoot:eCos生态的Bootloader,现在基本没人用了。我十年前用过一次,后来再也没见过。
  • GRUB:x86 PC领域的标准,嵌入式里很少用。除非你做的是x86工控机。
  • SPL/TPL:TI、NXP等厂商的二级引导方案。SPL是第一级,TPL是第二级,最终加载U-Boot。适合资源受限的芯片。

1.4 Bootloader的核心工作流程

说了这么多,咱们用一张图把Bootloader的启动流程串起来。这张图我画了很多次,每次给新人培训都用它。

Bootloader 启动流程 上电 / 复位 Stage 1:汇编初始化 初始化DDR / 时钟 Stage 2:C语言入口 加载内核到内存 跳转执行内核 Stage 1 通常 < 4KB 放在CPU内部SRAM中执行 Stage 2 支持文件系统 可从Flash/网络/串口加载 跳转前关闭MMU和Cache 设置内核启动参数

这张图里有个细节我特别想强调:跳转执行内核前,一定要关闭MMU和Cache。为什么?因为内核启动时有自己的页表建立流程,如果Bootloader开着MMU,内核一上来就懵了——虚拟地址和物理地址对不上。我曾经在这个坑里摔过一次,调试了整整两天才发现是MMU没关。

1.5 分区整合:为什么这门课叫这个名字

你可能会问:Bootloader就Bootloader,为什么还要强调“分区整合”?

原因很简单。在实际产品中,Bootloader不是孤立的。它要和内核分区、设备树分区、文件系统分区、配置分区、日志分区……协同工作。分区大小怎么定?起始地址怎么对齐?升级时怎么保证原子性?这些才是BSP开发中最头疼的问题。

我见过太多项目,Bootloader跑起来了,内核也能加载,但一到量产就出问题:

  • 分区太小,内核版本升级后放不下
  • 分区没对齐,Flash擦除时把邻居数据擦没了
  • 没有备份分区,升级到一半断电,板子变砖

所以这门课的核心,就是教你如何把Bootloader和各个分区当成一个整体来设计。从分区规划、地址映射、升级策略到容错机制,每一步都有讲究。

一句话总结:Bootloader是嵌入式系统的“第一把火”,而分区整合是让这把火能持续燃烧的“燃料系统”。两者缺一不可。