2. 启动流程分析:MCU上电启动流程、中断向量表与复位向量、从Flash到RAM的代码搬运
这一章,我们来聊聊MCU上电后到底发生了什么。很多初学者觉得启动流程很神秘,好像芯片一通电就自动跑main函数了。其实没那么玄乎,中间有一整套硬件和软件配合的流程。我当年刚入行时,就因为没搞懂启动流程,调试一个板子折腾了整整三天——后来发现是中断向量表放错了位置。
2.1 MCU上电启动流程
MCU上电后,硬件会做一系列固定动作。这些动作由芯片内部逻辑自动完成,不需要你写任何代码。说白了,就是芯片的"出厂设置"。
核心流程:上电复位 → 系统时钟初始化 → 硬件自检 → 加载复位向量 → 跳转执行
具体来说,上电后芯片会经历这几个阶段:
- 电源稳定:VDD上升到阈值电压,内部POR(上电复位)电路释放复位信号
- 时钟起振:内部RC振荡器开始工作,产生基础时钟
- 硬件自检:检查Flash校验和、SRAM完整性(部分芯片支持)
- 加载复位向量:从固定地址读取复位向量值
- 跳转执行:PC指针指向复位向量,开始执行第一条指令
嗯,这里要注意:不同厂商的MCU,上电流程细节有差异。比如STM32会先执行内部BootROM里的代码,再决定是从Flash启动还是从系统存储器启动。我曾在项目中遇到过客户把BOOT引脚焊错了,结果芯片一直进DFU模式,怎么都跑不起来。
个人经验:调试启动问题时,先检查复位引脚电平、电源纹波、时钟输出。这三个信号正常,芯片基本就能跑起来。我曾经用示波器抓过一块板子的复位引脚,发现复位信号毛刺太多,加了个10kΩ上拉电阻就解决了。
2.2 中断向量表与复位向量
中断向量表,是MCU启动的核心数据结构。它本质上是一个地址表,每个表项对应一个中断服务函数的入口地址。复位向量是这张表的第一个表项,也就是地址偏移0x00的位置。
为什么复位向量这么重要?因为芯片上电后,硬件会自动去固定地址读取复位向量,然后跳转过去。这个固定地址通常是Flash的起始地址,比如0x08000000(STM32)或0x00000000(Cortex-M默认)。
避坑指南:我曾经在移植Bootloader时,把中断向量表地址写错了。结果芯片能跑,但一触发中断就死机。调试了整整两天才发现,是SCB->VTOR寄存器没设置对。记住:如果你的代码里有中断,一定要确保向量表地址正确。
以Cortex-M系列为例,中断向量表的典型布局如下:
| 偏移地址 | 内容 | 说明 |
|---|---|---|
| 0x0000 0000 | 栈顶地址 | 主堆栈指针(MSP)初始值 |
| 0x0000 0004 | 复位向量 | 复位后第一条指令地址 |
| 0x0000 0008 | NMI向量 | 不可屏蔽中断入口 |
| 0x0000 000C | HardFault向量 | 硬件错误中断入口 |
| 0x0000 0010 | ... | 其他异常和中断向量 |
你想想看,如果复位向量指向的地址是无效的,或者栈顶地址是0,芯片上电后会发生什么?大概率是直接跑飞。所以,链接脚本里第一件事就是安排中断向量表的位置。
下面是一个典型的Cortex-M启动代码片段,展示了中断向量表的定义方式:
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
(void (*)(void))&_estack, // 0x0000 0000: 栈顶地址
Reset_Handler, // 0x0000 0004: 复位向量
NMI_Handler, // 0x0000 0008: NMI
HardFault_Handler, // 0x0000 000C: HardFault
MemManage_Handler, // 0x0000 0010: MemManage
BusFault_Handler, // 0x0000 0014: BusFault
UsageFault_Handler, // 0x0000 0018: UsageFault
// ... 其他向量
};
关键点:中断向量表必须放在Flash的起始地址,且对齐到256字节或512字节边界(具体看芯片手册)。Bootloader和应用程序各自维护一份向量表,通过VTOR寄存器切换。
2.3 从Flash到RAM的代码搬运
代码为什么要搬运?原因很简单:Flash读取速度慢,RAM快。尤其是需要频繁访问的代码段(比如中断服务函数、关键算法),放在RAM里能显著提升性能。
我习惯把搬运过程分为三个阶段:
- 确定搬运内容:哪些段需要从Flash搬到RAM?通常是.data段(已初始化全局变量)和.text段中的部分函数。
- 计算搬运地址:Flash中的加载地址(LMA)和RAM中的运行地址(VMA)。
- 执行搬运:在复位向量执行后,main函数之前,由启动代码完成搬运。
链接脚本中,这些信息通过SECTION命令定义:
SECTIONS
{
.isr_vector : ALIGN(4)
{
KEEP(*(.isr_vector))
} > FLASH
.text : ALIGN(4)
{
*(.text)
*(.text*)
} > FLASH
.data : ALIGN(4)
{
_sdata = .;
*(.data)
*(.data*)
_edata = .;
} > RAM AT> FLASH
.bss : ALIGN(4)
{
_sbss = .;
*(.bss)
*(.bss*)
_ebss = .;
} > RAM
}
注意看.data段后面的AT> FLASH,这表示数据在Flash中的加载地址。而> RAM表示运行地址。启动代码需要根据这两个地址做搬运。
启动代码中的搬运逻辑通常长这样:
void Reset_Handler(void)
{
// 1. 搬运.data段:从Flash复制到RAM
uint32_t *pSrc = &_sidata; // Flash中的加载地址
uint32_t *pDst = &_sdata; // RAM中的运行地址
while (pDst < &_edata) {
*pDst++ = *pSrc++;
}
// 2. 清零.bss段
pDst = &_sbss;
while (pDst < &_ebss) {
*pDst++ = 0;
}
// 3. 跳转到main
main();
}
个人习惯:我一般会在搬运完成后加一个内存屏障指令(DSB/ISB),确保所有数据写操作完成后再跳转main。曾经遇到过搬运后变量值不对的问题,就是少了这个屏障。
为什么Bootloader特别关注代码搬运?因为Bootloader本身可能也需要搬运自己的代码段,同时还要为后续的应用程序准备好运行环境。如果搬运逻辑写错了,应用程序可能根本跑不起来。
避坑指南:我曾经在做一个OTA升级项目时,Bootloader把应用程序的.data段搬运到了错误的RAM地址。结果应用程序能启动,但全局变量全是乱的。排查后发现是链接脚本里的_sidata符号定义错了。记住:_sidata是Flash中的加载地址,不是RAM地址。
2.4 本章知识结构图
下面这张SVG图,把MCU上电启动到代码搬运的完整流程串起来了。你可以对照着看,每个环节的输入输出是什么。
这张图把整个启动流程分成了六个阶段。你从上往下看,左边是硬件自动完成的,右边是软件需要处理的。我个人建议你把这张图打印出来贴在工位上,调试启动问题时对照着看,能省不少时间。
好了,这一章的内容就到这里。启动流程是Bootloader开发的基础,搞懂了这些,后面讲Flash分区、跳转逻辑、固件校验时,你就能理解为什么每一步都这么设计了。