切换主题
从上电到 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 最初的 1002.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 MSP10. 第二个值为什么是 Reset_Handler
CPU知道 Stack 在哪里以后,还必须知道:
第一段真正要运行的代码在哪里?
所以:
text
VectorTable[1]
↓
Reset_Handler地址
↓
写入PCPC:
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 == 014. 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初始化镜像
└── 100SRAM 此时不需要认为内容有效。
上电启动
首先:
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 = 0x08010000APP 自己的向量表最前面仍然是:
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 上,对不同区域进行更加系统化的组织和管理。