第二章 启动流程剖析:从按下电源到运行main函数

大家好,我是你们的Bootloader移植实战讲师。今天我们来聊聊嵌入式系统上电后,到底发生了什么。说实话,很多工程师做了好几年开发,对启动流程还是一知半解。我记得刚入行时,第一次调试板子,上电后啥反应没有,折腾了两天才发现是启动模式配置错了。嗯,从那以后我就养成了一个习惯——先搞清楚启动流程,再动手写代码。

2.1 嵌入式系统上电启动流程

一个典型的嵌入式系统,从按下电源到运行你的应用程序,大致要经历这么几个阶段:

  • 上电复位:电源稳定后,复位信号释放,CPU开始执行第一条指令
  • ROM Code执行:芯片内部固化的启动代码开始运行
  • BootROM阶段:初始化基本硬件,检测启动模式
  • 一级启动:加载并执行第一阶段的Bootloader
  • 二级启动:初始化更复杂的硬件,加载操作系统或应用程序

说白了,这个过程就像接力赛。每一棒只做自己该做的事,然后把控制权交给下一棒。我见过不少新手,一上来就想在Bootloader里做所有事,结果搞得一团糟。其实没必要,各司其职就好。

核心要点:启动流程的本质是「从简单到复杂,从固化到灵活」。ROM Code是芯片出厂就写死的,Bootloader是可编程的,而应用程序则是最终要运行的目标。

2.2 ROM Code与BootROM

ROM Code,也叫BootROM,是芯片制造商固化在芯片内部的一段代码。你没法修改它,也没法擦除它。它的任务很简单:

  1. 初始化最基本的硬件(比如CPU、时钟、片内SRAM)
  2. 检测启动引脚的电平状态
  3. 根据启动模式,从指定设备加载下一级代码

举个例子,我手头有一块STM32MP157的开发板。它的ROM Code会先检查BOOT0、BOOT1、BOOT2这三个引脚的电平组合。如果检测到是从SD卡启动,它就会去读取SD卡的特定扇区,把里面的代码加载到片内SRAM中。

个人经验:我曾经在一个项目里,因为启动引脚的上拉电阻焊错了,导致ROM Code死活检测不到正确的启动设备。排查了整整一天,最后用万用表一量,才发现是电阻值不对。所以啊,硬件设计阶段就要把启动引脚的电路仔细核对一遍。

ROM Code的局限性也很明显——它只能做最简单的事。为什么呢?因为片内SRAM通常很小,可能只有几十KB甚至几KB。你想想看,这么点空间,能跑多复杂的代码?所以ROM Code的任务就是「把下一级代码加载进来,然后跳转过去」。仅此而已。

2.3 一级启动与二级启动

一级启动(Stage 1)和二级启动(Stage 2)是Bootloader的两个阶段。为什么要分两级?说白了,还是空间限制的问题。

一级启动(Stage 1):

  • 通常放在片内SRAM中运行
  • 代码量极小,一般只有几百字节到几KB
  • 主要任务:初始化DDR内存、复制二级启动代码到DDR

二级启动(Stage 2):

  • 运行在DDR内存中
  • 代码量较大,可以是几十KB甚至几百KB
  • 主要任务:初始化外设(如网卡、USB、文件系统)、加载操作系统

我习惯把一级启动比作「开锁人」,二级启动比作「管家」。开锁人只需要一把钥匙打开大门,剩下的布置房间、迎接客人这些事,交给管家去做就行了。

避坑指南:我曾经在一个项目中,一级启动代码里做了太多事情——初始化了串口、GPIO、定时器,甚至还尝试检测按键。结果代码体积超过了片内SRAM的大小,直接导致启动失败。记住,一级启动的原则是「能少做就少做」,只做必须做的事。

2.4 链接脚本与内存布局

链接脚本(Linker Script)是Bootloader移植中最容易出错的地方之一。它告诉链接器:你的代码应该放在内存的哪个位置,数据段怎么安排,堆栈设在哪里。

一个典型的链接脚本长这样:

/* 一级启动链接脚本示例 */
MEMORY
{
    SRAM (rwx) : ORIGIN = 0x2FFC0000, LENGTH = 256K
    DDR  (rwx) : ORIGIN = 0xC0000000, LENGTH = 512M
}

SECTIONS
{
    .text : {
        *(.text*)
    } > SRAM

    .data : {
        *(.data*)
    } > SRAM

    .bss : {
        *(.bss*)
    } > SRAM
}

这里要注意几个关键点:

  • ORIGIN:内存的起始地址,必须和芯片手册一致
  • LENGTH:内存的大小,不能超过实际物理内存
  • 段分配:代码段、数据段、BSS段要放在合适的位置

我见过最离谱的一个错误,是有人把一级启动的代码段放在了DDR地址上。你想想看,DDR都还没初始化,代码怎么运行?结果就是板子直接死掉,连串口都打印不出任何信息。

内存布局的核心原则:

  1. 一级启动代码必须放在片内SRAM中
  2. 二级启动代码可以放在DDR中,但前提是DDR已经初始化
  3. 堆栈地址要避开代码段和数据段,防止覆盖
  4. 中断向量表要放在正确的位置(通常是内存起始地址)

下面这张图展示了典型的启动流程和内存布局:

嵌入式系统启动流程与内存布局 上电复位 ROM Code执行 一级启动 二级启动 片内SRAM (256KB) 中断向量表 (4KB) 一级启动代码 (16KB) 数据段 + BSS段 (8KB) 堆栈 (4KB) DDR内存 (512MB) 二级启动代码 (256KB) 应用程序代码 (1MB+) 堆 + 栈 (动态分配) 启动流程:上电 → ROM Code检测启动模式 → 加载一级启动到SRAM → 一级启动初始化DDR → 加载二级启动到DDR → 跳转到应用程序 注意:不同芯片的内存地址和大小可能不同,请参考具体芯片手册

调试技巧:我习惯在链接脚本里加上一个特殊的段标记,比如叫.debug_info,用来存放一些调试信息。这样在启动过程中,可以通过串口打印出当前执行到了哪个阶段,方便定位问题。

最后,我想强调一点:链接脚本不是写一次就完事的。每次修改Bootloader的功能,都要重新审视内存布局是否合理。比如你加了一个文件系统支持,二级启动代码变大了,那就要检查DDR里预留的空间够不够。不够的话,要么优化代码,要么调整链接脚本。

好了,这一章的内容就到这里。启动流程是Bootloader移植的基础,搞清楚了这些,后面的工作才能顺利进行。下一章我们会深入讨论具体的移植步骤,到时候我会拿一个实际的项目案例来讲解。

本章小结:

  • 启动流程分为上电复位、ROM Code、一级启动、二级启动四个阶段
  • ROM Code是芯片固化的,负责检测启动模式和加载一级启动
  • 一级启动运行在SRAM中,只做最核心的初始化工作
  • 二级启动运行在DDR中,负责加载操作系统或应用程序
  • 链接脚本决定了代码和数据的存放位置,必须与硬件内存布局一致

可可精品资料库