第二章 嵌入式系统启动流程:从复位向量到操作系统启动的完整流程
大家好,我是你们的嵌入式系统讲师。今天我们来聊聊嵌入式系统启动这件事。说实话,我见过不少工程师,写应用代码一把好手,但一提到启动流程就含糊其辞。这其实是个大问题——你想想看,系统怎么从零开始跑起来的,这直接决定了你后续所有代码的可靠性。
我个人习惯把启动流程分成三个阶段:硬件初始化、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),它负责做以下几件事:
- 从NAND Flash的固定位置(通常是第0块)读取一小段代码。
- 把这段代码拷贝到芯片内部的SRAM里。
- 跳转到SRAM执行这段代码。
- 这段代码(通常是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最终都要完成这几件事:
- 初始化硬件:时钟、DDR、串口、存储控制器等。
- 建立内存映射:设置MMU(如果需要的话)。
- 加载操作系统镜像:从Flash/SD卡/网络读取内核。
- 传递启动参数:告诉内核内存大小、控制台设备、根文件系统位置等。
- 跳转到内核入口:把控制权交给操作系统。
这里有个细节很多人会忽略——关闭中断和缓存。在跳转到内核之前,一定要关掉所有中断,清空缓存。否则内核启动时可能会被残留的中断信号干扰,或者读到脏数据。
避坑指南:我曾经在移植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。到时候见!