嵌入式系统启动流程:从复位向量到main函数的完整旅程

芯片上电那一刻,到底发生了什么?

很多人写嵌入式代码好几年,却从没认真想过这个问题。我刚开始做开发时也一样,反正编译器帮我搞定了启动代码,我直接写main函数就完事了。直到有一次,板子死活跑不起来,我才被迫把启动流程从头到尾捋了一遍。

今天我们就来聊聊,从复位信号拉低到main函数第一行代码执行,中间到底走了多少步。

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

CPU上电后,PC指针指向哪里?

答案是:复位向量地址。对于ARM Cortex-M系列,这个地址通常是0x00000000。对于Cortex-A系列,可能是0x00000000或0xFFFF0000,取决于硬件设计。

核心要点:复位向量处存放的不是代码,而是两张表——中断向量表。第一项是栈顶指针(MSP),第二项才是复位中断服务函数的入口地址。

我记得有一次调试一块新板子,焊好芯片后怎么都跑不起来。用示波器量复位引脚,波形正常。后来用JTAG挂上去一看,PC指针指向了一个非法地址。查了半天,原来是Flash里根本没烧程序,复位向量处全是0xFF。嗯,这个坑我踩过,你们就别再踩了。

启动模式:芯片怎么知道从哪里启动?

大多数嵌入式芯片都支持多种启动介质。芯片怎么选?靠的是启动引脚(Boot Mode Pins)。

启动模式 典型引脚配置 启动介质 特点
NOR Flash启动 BOOT0=0, BOOT1=0 片外NOR Flash 支持XIP(就地执行),速度快
NAND Flash启动 BOOT0=1, BOOT1=0 片外NAND Flash 容量大,需搬运代码到RAM
SD卡启动 BOOT0=0, BOOT1=1 SD/MMC卡 方便调试和量产
UART启动 BOOT0=1, BOOT1=1 串口下载 用于空片烧录和恢复

个人建议:量产时尽量用NOR或NAND启动,稳定可靠。SD卡和UART启动更适合开发调试阶段。我见过有人量产时忘了切回Flash启动,结果一批板子全从SD卡启动,生产线直接停摆。

NOR Flash启动:XIP的便利与局限

NOR Flash支持XIP(eXecute In Place),意思是CPU可以直接在Flash上取指令执行,不需要先把代码搬到RAM里。

好处很明显:启动快,上电就能跑。坏处呢?NOR Flash写入慢,容量做不大,价格贵。

启动流程大致是这样:

  1. 上电复位,CPU从0x00000000取复位向量
  2. 跳转到复位中断服务函数
  3. 初始化堆栈指针、BSS段清零、数据段拷贝
  4. 调用SystemInit()配置时钟和基本外设
  5. 跳转到main()

这里有个细节:NOR Flash的读取速度通常比CPU主频慢。所以芯片内部会有一个Flash预取缓冲区和加速器。我调试过一个项目,发现程序跑着跑着就卡死,最后定位到是Flash等待周期配置不对。你想想看,CPU跑200MHz,Flash只能跑50MHz,不配等待周期能不出问题吗?

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

NAND Flash不支持XIP。为什么?因为NAND的接口是I/O复用的,读一个地址需要发命令、发地址、等数据,时序复杂,没法直接映射到CPU的地址空间。

所以NAND启动的套路是:芯片内部固化的BootROM先把NAND前几页的代码读到内部SRAM里,然后跳过去执行。这段代码通常是二级Bootloader,再由它把完整的程序搬到SDRAM里。

注意:NAND Flash有坏块问题。BootROM读NAND时通常不会做ECC校验,所以存放Bootloader的区域必须保证没有坏块。我遇到过一批NAND芯片,前几页正好有坏块,结果整批板子都启动不了。后来我们改方案,在NAND里多放几个备份区,BootROM读不到第一个就试第二个。

NAND启动的典型流程:

  • BootROM初始化NAND控制器
  • 读取NAND前4KB(或8KB)到内部SRAM
  • 跳转到SRAM执行二级Bootloader
  • 二级Bootloader初始化SDRAM
  • 把完整程序从NAND搬到SDRAM
  • 跳转到SDRAM中的main()

SD卡启动:量产的福音

SD卡启动在开发阶段特别好用。改完代码,把SD卡插到电脑上,拷个新固件,再插回板子,重启就行。不用接调试器,不用按烧录按钮。

SD卡启动的流程和NAND类似,也是分步加载:

  1. BootROM初始化SD/MMC控制器
  2. 读取SD卡特定扇区(通常是第0扇区)到SRAM
  3. 检查文件系统或裸数据格式
  4. 加载二级Bootloader到SRAM
  5. 二级Bootloader初始化DDR,加载完整程序

避坑指南:SD卡启动对卡的质量和格式很敏感。我曾经用了一张高速SD卡,结果板子死活读不出来。后来换成一张老旧的低速卡,反而正常了。原因是BootROM的SDIO控制器不支持高速模式。所以量产时,建议用同一批次、同一型号的SD卡,提前验证兼容性。

UART启动:最后的救命稻草

当Flash里没有程序,或者程序跑飞了连不上调试器,UART启动就是你的救命稻草。

UART启动的原理很简单:芯片内部的BootROM监听串口,等待主机发送数据。主机通过特定协议(比如XMODEM、YMODEM或厂商自定义协议)把固件发送过来,BootROM收到后写入RAM或Flash,然后跳转执行。

我印象最深的一次:客户现场的一台设备死机了,远程连不上,Flash里的程序被意外擦除了。我让现场工程师用一根串口线连上笔记本,用SecureCRT通过UART下载了一个临时Bootloader,然后远程指挥他把完整固件烧进去。设备活了,客户省了一趟差旅费。

从复位到main:一张图看懂全流程

下面这张图总结了不同启动模式下,从复位到main函数的完整路径:

嵌入式系统启动流程总览 上电复位 读取复位向量(0x00000000) 读取Boot Mode引脚,选择启动介质 NOR Flash:XIP直接执行 NAND Flash:搬运到SRAM SD卡:读取扇区到SRAM UART:串口下载 二级Bootloader初始化硬件 跳转到 main() 注意: 不同芯片的BootROM大小 和启动协议各不相同

启动过程中的关键初始化步骤

不管从哪种介质启动,复位中断服务函数里都会做几件固定的事:

  • 设置堆栈指针:给C语言运行时提供栈空间
  • BSS段清零:把未初始化的全局变量清零
  • 数据段拷贝:把初始化过的全局变量从Flash搬到RAM
  • SystemInit:配置系统时钟、PLL、Flash等待周期
  • 调用main:终于轮到你的代码上场了

我曾经踩过的坑:有一次我写了一个很大的全局数组,初始化时赋值了一堆数据。结果程序一运行就死机。查了半天,发现是数据段太大了,启动代码里默认的拷贝循环用的是16位计数器,数组超过65535字节就溢出了。从那以后,我写大数组都改用动态分配,或者在链接脚本里手动指定段位置。

不同启动模式的性能对比

启动模式 启动速度 代码执行位置 适用场景 可靠性
NOR Flash 快(微秒级) Flash内直接执行 产品量产 高
NAND Flash 中等(毫秒级) 搬运到RAM后执行 大容量存储产品 中(需处理坏块)
SD卡 慢(百毫秒级) 搬运到RAM后执行 开发调试、小批量 中(卡兼容性问题)
UART 最慢(秒级) 下载到RAM或Flash 空片烧录、现场恢复 低(依赖串口连接)

说白了,启动模式的选择就是一场权衡。你要速度,就选NOR。你要容量,就选NAND。你要方便,就选SD卡。你要救命,就留个UART接口。

我个人习惯是在产品设计阶段就把这四种启动方式都支持上,通过跳线或电阻切换。开发时用SD卡或UART,量产时切回NOR或NAND。万一哪天现场出了问题,还能远程指导客户用UART救回来。