第二章 启动流程剖析:从按下电源到运行main函数
大家好,我是你们的Bootloader移植实战讲师。今天我们来聊聊嵌入式系统上电后,到底发生了什么。说实话,很多工程师做了好几年开发,对启动流程还是一知半解。我记得刚入行时,第一次调试板子,上电后啥反应没有,折腾了两天才发现是启动模式配置错了。嗯,从那以后我就养成了一个习惯——先搞清楚启动流程,再动手写代码。
2.1 嵌入式系统上电启动流程
一个典型的嵌入式系统,从按下电源到运行你的应用程序,大致要经历这么几个阶段:
- 上电复位:电源稳定后,复位信号释放,CPU开始执行第一条指令
- ROM Code执行:芯片内部固化的启动代码开始运行
- BootROM阶段:初始化基本硬件,检测启动模式
- 一级启动:加载并执行第一阶段的Bootloader
- 二级启动:初始化更复杂的硬件,加载操作系统或应用程序
说白了,这个过程就像接力赛。每一棒只做自己该做的事,然后把控制权交给下一棒。我见过不少新手,一上来就想在Bootloader里做所有事,结果搞得一团糟。其实没必要,各司其职就好。
核心要点:启动流程的本质是「从简单到复杂,从固化到灵活」。ROM Code是芯片出厂就写死的,Bootloader是可编程的,而应用程序则是最终要运行的目标。
2.2 ROM Code与BootROM
ROM Code,也叫BootROM,是芯片制造商固化在芯片内部的一段代码。你没法修改它,也没法擦除它。它的任务很简单:
- 初始化最基本的硬件(比如CPU、时钟、片内SRAM)
- 检测启动引脚的电平状态
- 根据启动模式,从指定设备加载下一级代码
举个例子,我手头有一块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都还没初始化,代码怎么运行?结果就是板子直接死掉,连串口都打印不出任何信息。
内存布局的核心原则:
- 一级启动代码必须放在片内SRAM中
- 二级启动代码可以放在DDR中,但前提是DDR已经初始化
- 堆栈地址要避开代码段和数据段,防止覆盖
- 中断向量表要放在正确的位置(通常是内存起始地址)
下面这张图展示了典型的启动流程和内存布局:
调试技巧:我习惯在链接脚本里加上一个特殊的段标记,比如叫.debug_info,用来存放一些调试信息。这样在启动过程中,可以通过串口打印出当前执行到了哪个阶段,方便定位问题。
最后,我想强调一点:链接脚本不是写一次就完事的。每次修改Bootloader的功能,都要重新审视内存布局是否合理。比如你加了一个文件系统支持,二级启动代码变大了,那就要检查DDR里预留的空间够不够。不够的话,要么优化代码,要么调整链接脚本。
好了,这一章的内容就到这里。启动流程是Bootloader移植的基础,搞清楚了这些,后面的工作才能顺利进行。下一章我们会深入讨论具体的移植步骤,到时候我会拿一个实际的项目案例来讲解。
本章小结:
- 启动流程分为上电复位、ROM Code、一级启动、二级启动四个阶段
- ROM Code是芯片固化的,负责检测启动模式和加载一级启动
- 一级启动运行在SRAM中,只做最核心的初始化工作
- 二级启动运行在DDR中,负责加载操作系统或应用程序
- 链接脚本决定了代码和数据的存放位置,必须与硬件内存布局一致
可可精品资料库