2. 嵌入式系统启动流程:从上电到操作系统加载的完整链路

各位同学,今天我们来聊聊嵌入式系统上电后到底发生了什么。很多人觉得启动就是按个电源键,然后屏幕亮了。但在我们做底层的人眼里,这中间是一条非常精密的链路——从晶振起振到操作系统跑起来,每一步都踩在刀刃上。

我刚开始带项目时,遇到过一块板子死活起不来。查了三天,最后发现是ROM Code里一个时序配置跟外部Flash不匹配。嗯,从那以后,我对启动流程的每个环节都格外较真。

2.1 上电瞬间:芯片内部发生了什么?

按下电源键那一刻,芯片内部其实在忙活好几件事。你想想看,一个芯片刚上电,电压都不稳,怎么能直接跑代码?

  1. POR(Power-On Reset):内部复位电路检测到电压稳定后,释放复位信号。这个阈值各家芯片不同,一般在1.8V~3.3V之间。
  2. 时钟起振:内部RC振荡器先跑起来,给芯片提供基础时钟。外部晶振要等几百微秒才能稳定。
  3. 硬件自检:检查SRAM、寄存器、总线接口是否正常。有问题就挂死在复位状态。
  4. 启动源选择:根据BOOT引脚电平或efuse值,决定从哪加载代码——NAND、eMMC、SD卡还是UART。

关键点:上电后前几百微秒,芯片处于“裸奔”状态。没有中断,没有MMU,只有最原始的硬件逻辑在跑。

2.2 ROM Code:芯片出厂就刻好的“第一行代码”

ROM Code是芯片流片时就固化在ROM里的程序。用户改不了,也删不掉。它的任务很简单:把二级Bootloader从存储介质搬到SRAM或DDR里,然后跳过去执行。

我记得有一次调试一个国产芯片,发现ROM Code里有个bug——它读eMMC时用的时序参数偏保守,导致某些高速卡初始化失败。最后只能通过修改二级Bootloader来绕过去。所以说,ROM Code虽然“不可变”,但它的行为你得摸透。

特性 ROM Code 一级Bootloader 二级Bootloader
存储位置 芯片内部ROM 片上SRAM DDR/Flash
可修改性 不可修改 可更新(需特殊流程) 可自由更新
大小限制 几十KB 几十~几百KB 几百KB~几MB
主要任务 初始化硬件、加载一级Boot 初始化DDR、加载二级Boot 加载OS内核

2.3 一级Bootloader:承上启下的“桥梁”

ROM Code把一级Bootloader加载到SRAM后,就把控制权交出去了。一级Bootloader要做的事比ROM Code多得多:

  • 初始化DDR控制器:配置时序、频率、电压。这一步错了,后面全白搭。
  • 设置时钟树:把CPU、总线、外设的时钟配到目标频率。
  • 初始化存储控制器:让芯片能正常读写NAND、eMMC或SD卡。
  • 加载二级Bootloader:从存储介质把二级Bootloader读到DDR里。
  • 跳转执行:设置好栈指针和入口地址,跳过去。

个人经验:一级Bootloader的代码量要严格控制。我习惯把DDR初始化放在最前面,因为后面所有操作都依赖内存。曾经有个项目,DDR初始化代码里有个循环次数算错了,导致低温下启动失败——查了整整一周。

2.4 二级Bootloader:真正的“系统管家”

二级Bootloader就是我们常说的U-Boot、GRUB、Lk这类程序。它比一级Bootloader灵活得多,功能也更丰富:

  1. 文件系统支持:能识别FAT32、ext4、UBIFS等格式,从分区里读内核镜像。
  2. 网络启动:通过TFTP、NFS从服务器加载内核,适合开发调试。
  3. 环境变量管理:保存启动参数、内核命令行、设备树路径等配置。
  4. 设备树处理:解析DTB文件,把硬件信息传递给内核。
  5. 启动内核:设置好CPU模式、MMU、缓存,然后跳转到内核入口。

说白了,二级Bootloader就是操作系统的“引路人”。它把硬件环境准备好,把内核镜像放到内存里,然后说一句“剩下的交给你了”,就撒手不管了。

2.5 完整启动链路图

下面这张图把整个流程串起来了。我建议你把它存下来,调试时对照着看。

嵌入式系统完整启动链路 ① 上电复位 (POR) ② ROM Code 执行 ③ 一级 Bootloader ④ 二级 Bootloader ⑤ OS 内核启动 ⑥ 用户空间初始化 硬件阶段 • 电压稳定、时钟起振 • 复位释放、启动源选择 • 硬件自检 软件阶段 • ROM Code → 一级Boot • 一级Boot → 二级Boot • 二级Boot → OS内核 关键动作 • DDR初始化 • 设备树传递 • 内核解压与跳转

2.6 避坑指南:启动流程中常见的“坑”

做Bootloader移植调试,说白了就是在跟时序、配置、兼容性较劲。我把自己踩过的坑整理了一下:

我曾经遇到过:一块板子在常温下启动正常,放到高低温箱里就挂。查到最后,发现是DDR初始化代码里用的时序参数是常温值,没做温度补偿。从那以后,我所有项目的DDR初始化都加了温度检测逻辑。

  • 启动源选择冲突:BOOT引脚电平被外部电路拉偏,导致ROM Code从错误介质加载。建议在PCB设计时加10kΩ上下拉电阻。
  • DDR初始化失败:时序参数太紧或太松,导致内存读写不稳定。我习惯先跑一遍DDR训练程序,把最佳参数写进寄存器。
  • 二级Bootloader过大:一级Bootloader的SRAM空间有限,如果二级Bootloader超过限制,加载就会失败。建议控制在512KB以内。
  • 设备树不匹配:内核用的DTB跟实际硬件不一致,导致外设驱动加载失败。每次改硬件后,记得同步更新设备树。

2.7 小结

嵌入式系统的启动流程,说白了就是一条“接力跑”。ROM Code跑第一棒,一级Bootloader跑第二棒,二级Bootloader跑第三棒,最后把棒子交给操作系统。每一棒都有自己的任务,也都有自己的限制。

理解了这个链路,你就能回答很多面试题了——比如“系统启动时为什么先跑ROM Code而不是直接跑U-Boot?”、“DDR初始化为什么不能放在ROM Code里?”等等。嗯,这些问题的答案,其实都在上面这张图里。