Skip to content

从上电到 main:Flash、SRAM 与启动流程

上一节我们已经知道,一个裸机程序运行时并不是所有内容都放在同一种内存里。

程序代码通常保存在 Flash,而 .data.bss、Stack 等运行时数据主要使用 SRAM。

但这里还有一个一直没有回答的问题:

程序刚刚下载进 STM32 时明明只是“躺在 Flash 里”,上电之后 CPU 又是怎么一步一步把运行环境建立起来,最后进入 main() 的?

这一章我们就沿着这条时间线把它串起来。


1. 为什么同时需要 Flash 和 SRAM

以前我们可能只是记住:

text
Flash → 存程序

SRAM → 存变量

这个结论没有错,但更重要的是理解原因。

1.1 Flash:负责“断电以后还记得”

Flash 是非易失性存储器。

也就是说:

text
程序烧录进入 Flash

断电

Flash 中的数据仍然存在

再次上电仍然能够启动

所以:

text
程序机器指令
中断向量表
只读常量
.data 的初始值

都可以长期保存在 Flash 中。

如果程序只存在 SRAM:

text
程序运行

断电

SRAM内容丢失

下一次上电连程序都没了

显然无法作为 MCU 的正常程序存储器。

因此可以先记:

Flash 的核心价值是长期保存程序和固定数据。


1.2 SRAM:负责“运行时干活”

SRAM 和 Flash 不一样。

SRAM:

text
读写速度快

允许频繁修改

但是断电后内容丢失

例如:

c
uint32_t count = 0;

while (1)
{
    count++;
}

count 在程序运行过程中会被不断修改。

这种数据更适合放在 SRAM,而不是不断擦写 Flash。

所以可以暂时建立:

text
Flash

适合长期保存


SRAM

适合程序运行时频繁读写

也可以用一句比较好记的话:

Flash 负责“记住”,SRAM 负责“干活”。

这句话并不是严格定义,但是作为现阶段理解非常合适。


2. 一个简单程序到底放在哪里

来看:

c
uint32_t g_a = 100;

uint32_t g_b;

const uint32_t table[] = {1, 2, 3};

int main(void)
{
    uint32_t temp = 10;

    while (1)
    {
    }
}

程序编译以后,可以先建立这样的模型。


2.1 Flash 中主要有什么

text
Flash

├── Vector Table

├── .text
│    └── main()、各种函数的机器指令

├── .rodata
│    └── const只读数据

└── .data初始值镜像
     └── 例如 g_a 最初的 100

2.2 SRAM 中主要有什么

程序真正运行以后:

text
SRAM

├── .data
│    └── g_a = 100

├── .bss
│    └── g_b = 0

├── Heap

└── Stack
     └── 局部变量、函数调用现场等

但是这里马上出现一个问题。


3. .data 为什么 Flash 和 SRAM 都涉及

比如:

c
uint32_t g_a = 100;

g_a 在运行过程中可能变成:

c
g_a = 200;

所以它真正运行时必须位于:

text
SRAM

但是 SRAM 一旦断电:

text
g_a 的内容

丢失

那么下一次上电的时候:

CPU 怎么知道 g_a 初始应该等于 100?

所以必须提前在:

text
Flash

保存一份:

text
100

这就是 .data 的初始值镜像。

可以理解成:

text
程序烧录完成

Flash
└── g_a 的初始值 = 100


STM32上电

Flash中的100

      ↓ 复制

SRAM中的g_a


进入main()以后

g_a = 100

因此:

.data 的运行地址在 SRAM,但是它的初始值需要保存在 Flash。


4. .bss 为什么不用在 Flash 存一堆 0

例如:

c
uint32_t g_b;

这种未显式初始化的全局变量,在程序进入 main() 之前应该被初始化为:

text
0

那么是不是需要在 Flash 里专门保存:

text
00 00 00 00

呢?

没必要。

假设:

c
uint8_t buffer[10000];

如果为了表示它初始值全为 0,就真的在 Flash 里面保存一万个 0:

text
0 0 0 0 0 0 0 0 ...

会非常浪费 Flash。

所以 .bss 使用另一种方式:

text
Flash不用保存这些0

STM32启动阶段

直接找到.bss对应的SRAM区域

全部清零

因此可以这样记:

text
.data

Flash保存初始值

启动时复制到SRAM


.bss

不保存大量0

启动时直接在SRAM中清零

5. 程序下载完成以后,并不会直接进入 main()

现在把时间拨回:

text
Keil编译

生成程序

Download

写入Flash

此时程序只是:

已经存储在 Flash 里。

CPU还没有真正运行起来。

当我们:

text
上电

或者

按下Reset

之后,Cortex-M 才开始启动。

而 CPU 启动以后并不是:

text
直接执行main()

甚至也不是:

text
直接把0x00000000当第一条机器指令执行

它还有一套启动流程。


6. 以“从 Main Flash 启动”为例

STM32 可以存在不同启动来源。

例如:

text
Main Flash

System Memory

SRAM

这一章我们只研究日常开发最常见的:

从 Main Flash 启动

正常情况下,主 Flash 地址从:

text
0x08000000

开始。

但是 Cortex-M 复位启动时会从:

text
0x00000000

这个启动地址获取最初的信息。

所以 STM32 会把 Main Flash:

text
0x08000000

映射到:

text
0x00000000

这个启动区域。

可以先理解成:

text
CPU访问

0x00000000

实际看到

Flash开头


CPU访问

0x00000004

实际看到

Flash + 4

注意:

Flash 并没有真的从 0x08000000 搬走。

0x08000000 仍然能够正常访问 Flash。

这里只是让启动地址 0x00000000 也能够看到 Flash。


7. Vector Table:CPU启动首先读取的两个值

Flash 最前面存放着:

中断向量表

Vector Table。

其中最开始两个值尤其重要:

text
Vector Table

+0x00

Initial MSP


+0x04

Reset_Handler地址

假设 Flash 从:

text
0x08000000

开始。

那么可以理解成:

text
0x08000000

Initial MSP


0x08000004

Reset_Handler地址

由于启动时 Flash 被映射到了 0x00000000

text
0x00000000

Initial MSP


0x00000004

Reset_Handler地址

8. CPU不是执行 0x00000000,而是读取它

这是一个很容易理解错的地方。

Cortex-M 复位以后,并不是:

text
PC = 0x00000000

然后执行这里的机器指令

而是先:

text
读取0x00000000中的32位数据

装入MSP

然后:

text
读取0x00000004中的32位数据

装入PC

可以简化成:

text
MSP = VectorTable[0]

PC  = VectorTable[1]

9. 为什么第一个值是 MSP

MSP:

text
Main Stack Pointer

也就是主栈指针。

程序真正开始执行以前,CPU必须先知道:

我的栈在哪里?

因为后续:

text
函数调用

保存现场

异常

局部运行数据

都可能需要 Stack。

所以 Cortex-M 的设计思想是:

text
第一步
先把栈准备好

第二步
再开始运行代码

因此:

text
VectorTable[0]

Initial MSP

10. 第二个值为什么是 Reset_Handler

CPU知道 Stack 在哪里以后,还必须知道:

第一段真正要运行的代码在哪里?

所以:

text
VectorTable[1]

Reset_Handler地址

写入PC

PC:

text
Program Counter

保存 CPU 接下来要执行的指令地址。

因此:

text
PC = Reset_Handler

以后:

text
CPU开始执行Reset_Handler

这才是真正开始运行程序代码。


11. Reset_Handler 为什么不是 main()

这是另一个常见疑问。

很多初学者会觉得:

text
程序入口不就是main()吗?

但是对于 MCU 来说:

main() 是 C 程序准备完成后的入口,并不是 CPU 复位后的第一段代码。

因为进入 main() 之前还有很多事情必须完成。

例如:

text
系统初始化

准备C语言运行环境

初始化.data

清零.bss

这些工作需要先完成。

所以整个关系更像:

text
Reset_Handler

完成基础启动工作

准备C语言运行环境

main()

12. SystemInit() 做什么

在 STM32 的启动过程中,经常会看到:

c
SystemInit();

它主要负责:

芯片系统级硬件的早期初始化。

例如:

text
时钟相关基础设置

FPU相关设置

向量表相关配置

芯片系统级初始化

具体内容会随着芯片和工程配置变化。

现阶段不需要深入每一行。

只需要知道:

SystemInit() 主要偏硬件系统初始化。


13. C运行环境初始化

接下来还要做:

text
.data复制

.bss清零

例如:

c
uint32_t g_a = 100;

uint32_t g_b;

启动过程中:

text
Flash

g_a初始值100

      │ copy

SRAM

.data
g_a = 100

同时:

text
SRAM

.bss

全部清零

g_b = 0

这样进入:

c
main()

以后,我们才能放心认为:

text
g_a == 100

g_b == 0

14. Keil 工程中为什么经常看到 __main

我们使用 Keil 时,在启动文件中可能看到类似:

text
Reset_Handler

SystemInit

__main

这里:

text
__main

不是我们自己写的:

c
int main(void)

它通常属于 C 运行库的一部分。

可以简单理解成:

text
__main

帮助准备C运行环境

最终调用我们真正写的main()

因此不要混淆:

text
__main

和:

text
main()

现阶段知道这一点即可,不需要深入 C Runtime 源码。


15. 整个启动流程串起来

现在终于可以把整个流程完整画出来:

text
         Keil编译/链接

          生成程序镜像

           烧录进Flash

              断电

        Flash内容依然存在

        STM32上电 / Reset

       选择Main Flash启动

Flash映射到启动地址0x00000000

CPU读取0x00000000

         Initial MSP

        Main Stack准备好

CPU读取0x00000004

    得到Reset_Handler地址

       PC = Reset_Handler

      执行Reset_Handler

          SystemInit()

       准备C语言运行环境
          ↓             ↓
       .data           .bss
   Flash → SRAM      SRAM清零
          ↓             ↓
          └──────┬──────┘

               main()

        正式进入我们的程序

这就是:

一个躺在 Flash 里的程序,从 STM32 上电以后逐渐“活起来”的过程。


16. 再用之前的代码走一次

代码:

c
uint32_t g_a = 100;

uint32_t g_b;

int main(void)
{
    uint32_t temp = 10;

    while (1)
    {
    }
}

下载完成后

Flash:

text
Vector Table

.text
└── main()机器指令

.data初始化镜像
└── 100

SRAM 此时不需要认为内容有效。


上电启动

首先:

text
Initial MSP

Main Stack准备完成

然后:

text
Reset_Handler

开始运行。

接着:

text
Flash中的100

复制

SRAM .data

g_a = 100

然后:

text
SRAM .bss

清零

g_b = 0

进入 main()

直到:

c
int main(void)

真正开始以后:

c
uint32_t temp = 10;

这个局部变量才开始拥有实际的运行意义。

所以:

text
g_a

进入main前已经初始化完成


g_b

进入main前已经清零


temp

执行main以后才出现

17. APP_BASE + 0 和 APP_BASE + 4

如果以后做 Bootloader / OTA,我们还会经常看到:

text
APP_BASE + 0

APP_BASE + 4

例如:

text
Bootloader
0x08000000


Application
0x08010000

那么:

text
APP_BASE = 0x08010000

APP 自己的向量表最前面仍然是:

text
APP_BASE + 0

APP的初始MSP


APP_BASE + 4

APP的Reset_Handler地址

所以 Bootloader 跳转 APP 时,经常会读取这两个值。

这一部分以后真正学 Bootloader / OTA 时再深入。

现在只需要知道:

每一个能够独立启动的 Cortex-M 程序,都需要准备自己的向量表和启动入口。


18. 本章暂时不继续深究什么

启动过程其实还能继续钻非常深,例如:

text
启动文件汇编

链接脚本

Scatter File

Load Address / Execution Address

VTOR

Thumb状态

Boot0 / Boot1详细逻辑

C Runtime内部实现

但是对于我们现在学习 FreeRTOS 内存管理来说:

没有必要现在全部研究。

目前真正需要掌握的是:

text
① Flash掉电保存,SRAM运行时读写

② .data运行在SRAM,但初始值来自Flash

③ .bss启动阶段被清零

④ Cortex-M复位先读取Initial MSP

⑤ 再读取Reset_Handler地址

⑥ Reset_Handler完成启动准备

⑦ 最后才进入main()

能够把这七件事情讲明白,这一章的目标就已经完成。


19. 下一步回到 FreeRTOS 内存

现在我们的底层链路已经逐渐完整:

text
CPU怎么访问内存

Memory Map

Flash / SRAM

裸机内存布局

STM32启动流程

.data / .bss / Stack建立起来

下一步就可以重新回到:

FreeRTOS 的 Task Stack 与 Heap

这一次我们再看到:

text
TCB

Task Stack

ucHeap[]

xTaskCreate()

xTaskCreateStatic()

就不会把它们当成凭空出现的新内存。

因为已经知道:

FreeRTOS最终仍然只是在 STM32 原本的 SRAM 上,对不同区域进行更加系统化的组织和管理。

FreeRTOS 内核与工程实践