第二章 嵌入式系统启动流程:从复位向量到操作系统启动的完整流程

大家好,我是你们的嵌入式系统讲师。今天我们来聊聊嵌入式系统启动这件事。说实话,我见过不少工程师,写应用代码一把好手,但一提到启动流程就含糊其辞。这其实是个大问题——你想想看,系统怎么从零开始跑起来的,这直接决定了你后续所有代码的可靠性。

我个人习惯把启动流程分成三个阶段:硬件初始化Bootloader加载操作系统启动。咱们一个一个来拆解。

2.1 复位向量:一切从这里开始

芯片上电后,CPU会去一个固定的地址取第一条指令。这个地址就叫复位向量。不同的架构,这个地址不一样:

  • ARM Cortex-M系列:复位向量在0x00000000,存放的是栈顶指针;0x00000004存放复位中断服务函数地址。
  • ARM Cortex-A系列:复位向量在0x00000000或0xFFFF0000(取决于配置)。
  • RISC-V:复位向量通常在0x80000000或0x00000000。
  • x86:复位向量在0xFFFFFFF0(没错,靠近4GB地址空间的顶端)。

重要提醒:复位向量里放的必须是有效的指令或跳转地址。我曾经见过一个项目,芯片焊上去死活不跑,查了两天才发现是复位向量表被意外擦除了。嗯,从那以后我每次烧录完都会先读一遍复位向量确认一下。

2.2 启动模式:NOR Flash、NAND Flash、SD卡

芯片怎么知道从哪里启动?这取决于启动模式。大多数芯片都有几个BOOT引脚,通过高低电平组合来选择启动源。

启动模式 特点 典型应用
NOR Flash启动 支持XIP(就地执行),代码可直接在Flash中运行 小容量、快速启动场景
NAND Flash启动 容量大、成本低,但需要先加载到RAM才能执行 大容量存储、嵌入式Linux
SD卡启动 灵活方便,适合开发和调试 开发板、树莓派等
UART/USB启动 通过串口或USB下载代码到RAM执行 工厂烧录、紧急恢复

2.3 NOR Flash启动:最直接的方式

NOR Flash支持XIP,说白了就是CPU可以直接从Flash里取指令执行,不需要先把代码拷贝到RAM里。这有什么好处?启动速度快啊!上电就能跑。

但代价是什么?NOR Flash写入慢、容量小、价格贵。所以一般只用来放Bootloader,或者一些对启动速度要求极高的场景。

实战技巧:如果你用NOR Flash启动,记得把中断向量表重映射到Flash的起始地址。我刚开始做ARM开发时,忘了做这一步,结果中断一触发就死机。查了半天才发现是向量表地址不对。

2.4 NAND Flash启动:先搬运再执行

NAND Flash就不一样了。它不能XIP,因为它的接口是页读取的,不是线性地址映射。所以芯片内部会有一个固化的ROM代码(叫BootROM),它负责做以下几件事:

  1. 从NAND Flash的固定位置(通常是第0块)读取一小段代码。
  2. 把这段代码拷贝到芯片内部的SRAM里。
  3. 跳转到SRAM执行这段代码。
  4. 这段代码(通常是SPL或MLO)再负责初始化DDR,然后把完整的Bootloader从NAND加载到DDR。

这个过程有点像俄罗斯套娃——小代码加载大代码,大代码再加载更大的代码。

注意:NAND Flash有坏块问题。Bootloader必须放在好块里,否则系统直接变砖。我建议在量产时做坏块检测和备份,至少保留两份Bootloader副本。

2.5 SD卡启动:开发者的好朋友

SD卡启动在开发阶段特别方便。你想想看,不用烧录器,不用JTAG,把SD卡插上就能跑新程序。树莓派、BeagleBone这些板子都用这种方式。

SD卡启动的流程一般是:

  • 芯片上电,BootROM检查SD卡是否存在。
  • 读取SD卡的主引导记录(MBR),找到第一个分区。
  • 从分区中读取Bootloader文件(比如u-boot.img)。
  • 加载到RAM并执行。

我个人习惯在SD卡上放三个分区:一个FAT32放Bootloader和内核,一个ext4放根文件系统,一个swap分区。这样调试起来特别顺手。

2.6 Bootloader的核心任务

不管从哪种介质启动,Bootloader最终都要完成这几件事:

  1. 初始化硬件:时钟、DDR、串口、存储控制器等。
  2. 建立内存映射:设置MMU(如果需要的话)。
  3. 加载操作系统镜像:从Flash/SD卡/网络读取内核。
  4. 传递启动参数:告诉内核内存大小、控制台设备、根文件系统位置等。
  5. 跳转到内核入口:把控制权交给操作系统。

这里有个细节很多人会忽略——关闭中断和缓存。在跳转到内核之前,一定要关掉所有中断,清空缓存。否则内核启动时可能会被残留的中断信号干扰,或者读到脏数据。

避坑指南:我曾经在移植U-Boot时,忘了在跳转前关掉MMU和D-cache。结果内核启动到一半就挂了,打印信息乱七八糟。后来用JTAG单步跟踪才发现是缓存没清空,内核读到了过时的页表项。从那以后,我每次写Bootloader都会在跳转前加一段清理代码。

2.7 操作系统启动:从汇编到C语言

当Bootloader把控制权交给内核后,内核会从入口函数开始执行。以Linux为例,启动流程大致是:

  • 汇编阶段:设置页表、使能MMU、初始化CPU模式。
  • start_kernel():C语言入口,初始化调度器、内存管理、中断系统等。
  • init进程:启动第一个用户态进程(PID=1),然后加载init程序。
  • 用户空间初始化:启动服务、挂载文件系统、启动shell。

整个过程就像搭积木——底层硬件先准备好,然后一层层往上搭。每一层都依赖下面一层提供的服务。

2.8 总结与建议

好了,我们来捋一捋今天的内容:

  • 复位向量是系统的起点,必须保证它正确。
  • 启动模式决定了代码从哪里来,怎么执行。
  • NOR Flash适合快速启动,NAND Flash适合大容量,SD卡适合开发调试。
  • Bootloader是硬件和操作系统之间的桥梁,它的核心工作是初始化硬件、加载内核、传递参数。
  • 操作系统启动是从汇编到C语言,从内核态到用户态的过程。

最后给大家一个建议:动手做一遍。找一块开发板,从零开始写一个最小的Bootloader,让它点亮一个LED,然后加载一个简单的裸机程序。这个过程会让你对启动流程的理解深入好几个层次。

下一章我们会深入Bootloader的代码实现,手把手教你写一个能用的Bootloader。到时候见!