MCU拆解室------你的变量住在哪里?
配套视频 :
p02_memory_map_1080p30.mp4(1920×1080@30,7 分 22 秒,8 章)参考芯片 :STM32L552(Cortex-M33,512 KB Flash / 256 KB SRAM)
注意 :系列从下一期(编译与链接)起改用经典 STM32F103(64 KB / 20 KB)。概念完全一致,只是数字不同 ;阅读时把 512/256 换成 64/20 即可。
本文定位:对照阅读版。视频里出现的每个数字(12340 字节、七个变量、1540 字节堆栈)都来自真实工程;本文补上视频受时长限制没展开的推导与调试方法。
阅读指南
| 章节 | 对应视频 | 时间码 | 一句话 |
|---|---|---|---|
| 0 | --- | --- | 三个问题 |
| 1 | L1 开场提问 | 00:00 | 一盏灯动了三块地皮 |
| 2 | L2 两块地盘 | 00:56 | Flash / SRAM / 外设,以及段 |
| 3 | L3 编译流水线 | 01:46 | 段是在声明时定下的 |
| 4 | L4 变量安置 | 02:26 | 七个变量的户口 |
| 5 | L5 生命周期×作用域 | 03:22 | 活多久、谁能看见 |
| 6 | L6 上电那一刻 | 04:20 | 搬 .data、清 .bss、跳 main |
| 7 | L7 三关键字 | 05:17 | const / static / volatile |
| 8 | L8 总结 | 06:26 | map 文件与两个恒等式 |
第 0 章 三个问题
const uint8_t SEQ[] = {...}放在哪?为什么不占 RAM?volatile uint32_t g_ms;为什么必须 volatile?不加会怎样?int seq_copy[4];在函数里,它什么时候出生、什么时候消失?
一句话立意:变量进哪个段,看声明;活多久、谁能碰,也看声明。

图 0-1 点灯的三句话动了三种资源(视频 00:20)
第 1 章 一盏灯,三块地皮
视频用点灯代码引出内存话题。一段最普通的点灯循环,其实同时用了三个地方:
c
const uint8_t SEQ[] = {1,0,0,0}; // ① 只读常量 → Flash
for (;;) {
GPIOA->BSRR = SEQ[i] << 5; // ② 写外设寄存器 → 外设空间
for (volatile uint32_t g = 0; g < 200000; g++); // ③ 循环变量 → 栈/寄存器
}
| 句子 | 资源 | 特性 |
|---|---|---|
读 SEQ |
Flash(只读) | 掉电不丢,读不花 RAM |
写 BSRR |
外设寄存器空间 | 不经过内存,直接到 GPIO |
| 循环变量 | 栈 / 寄存器 | 掉电即失,函数返回即消失 |
编译器给的账单(真实工程):Flash 用了 12340 字节,RAM 1680 字节。这两个数字,就是"地图"的入口。
为什么先讲这个? 因为初学者常以为"程序 = 代码 + 变量",其实中间还有一层:变量被分门别类放进不同的段 ,段再被链接脚本放到不同的地址。不理解这一层,
HardFault、RAM 溢出、volatile 失效都只能靠猜。
第 2 章 三块地盘与七个段

图 2-1 芯片版图:左边内核,右上 Flash,右下 SRAM,右下角外设(视频 01:15)
2.1 三块地盘
| 地盘 | 起始地址 | 本集容量 | 掉电 | 放什么 |
|---|---|---|---|---|
| Flash | 0x0800_0000 |
512 KB | 不丢 | 向量表、代码、只读常量、.data 初值模板 |
| SRAM | 0x2000_0000 |
256 KB | 丢 | .data 运行副本、.bss、堆、栈 |
| 外设 | 0x4000_0000 |
--- | --- | GPIO/USART/SPI 等寄存器 |
地址就是门牌号:CPU 不区分"内存"和"外设",它只往地址上读写;是总线译码器把地址分发到对应设备。
2.2 Flash 里的段
| 段 | 内容 | 说明 |
|---|---|---|
.isr_vector |
向量表 | 复位后 SP/PC 从这里读 |
.text |
函数代码 | 只读、可执行 |
.rodata |
const 常量、字符串 |
只读 |
.data(LMA) |
有初值变量的初值模板 | 运行时会被搬到 SRAM |
2.3 SRAM 里的段
| 段 | 内容 | 何时初始化 |
|---|---|---|
.data(VMA) |
有初值的全局/静态变量 | 启动时从 Flash 拷贝 |
.bss |
初值为 0 的全局/静态变量 | 启动时整片清零 |
| 堆 | malloc 用 |
运行时向上长 |
| 栈 | 局部变量、调用现场 | 运行时向下长 |
关键认识 :.data 在 Flash 和 SRAM 各有一份(模板 + 运行副本),这正是第 6 章"上电为什么要搬"的原因。
第 3 章 段是在"声明"时定下的

图 3-1 .c → .o → 链接:段在编译期确定,地址在链接期确定(视频 02:35)
- 编译阶段:每个
.o里已经有.text/.rodata/.data/.bss这些小段,但地址是相对/未定的。 - 链接阶段:链接脚本把同名的段合并,分配最终地址(VMA),
.data还会另给一个装载地址(LMA)。
一句话 :变量落在哪个段,在你写下声明的那一刻就定了;具体地址,链接器说了算。
这也是为什么"同样的代码,换个链接脚本,地址就全变了"。调试时看到的
0x2000_0060,是链接脚本 + 当前编译结果的产物,不是语言规定。
第 4 章 七个变量的户口
视频里逐个安置了七个变量。它们的声明(节选):
c
const uint8_t SEQ[4] = {1,0,0,0}; // ① 只读查表
volatile uint32_t g_ms; // ② 中断里减、主循环读
uint32_t g_err_cnt; // ③ 全局计数
static uint8_t s_seq_idx; // ④ 文件级 static
static uint8_t s_ps; // ⑤ 函数级 static(在 SysTick 里)
void f(void) { int seq_copy[4]; ... } // ⑥ 局部数组
void g(void) { volatile uint32_t *bsrr; }// ⑦ 局部指针
| # | 变量 | 段 | 寿命 | 可见范围 |
|---|---|---|---|---|
| ① | SEQ |
.rodata(Flash) |
全程 | 本文件(const+static 语义) |
| ② | g_ms |
.data(有初值)/.bss(初值 0) |
全程 | 全工程 |
| ③ | g_err_cnt |
.bss |
全程 | 全工程 |
| ④ | s_seq_idx |
.bss |
全程 | 本文件 |
| ⑤ | s_ps |
.bss |
全程 | 本函数 |
| ⑥ | seq_copy[4] |
栈 | 仅函数调用期 | 本代码块 |
| ⑦ | bsrr(指针本身) |
栈 | 仅函数调用期 | 本代码块 |
注意三个细节:
static不改变位置,只收窄可见范围(①④⑤ 都在"全程"那一列)。- 显式写
= 0也进.bss,不进.data------编译器知道 0 不用存。 - 局部数组在栈上:编译器只决定"去栈上",具体地址运行时由 sp 决定。
4.1 为什么 g_ms 必须 volatile
g_ms 被两个执行流共享:SysTick 中断里减,主循环里等它到 0。不开 volatile 时,编译器会把 g_ms 缓存进寄存器(它"看不到"中断会改它),主循环就再也读不到新值。
这是第 7 章的主题,也是下一期 GPIO 的伏笔。
第 5 章 生命周期 × 作用域

图 5-1 左边代码、右边四个同心圈:代码块 / 函数 / 文件 / 全工程(视频 03:55)
5.1 两个维度
- 寿命(lifetime):变量从什么时候存在到什么时候消失。
- 作用域(scope):代码的哪些位置能"看见"这个名字。
两者不是一回事。典型例子:
c
void f(void) {
static int s = 0; // 寿命:全程;作用域:仅 f()
int a = 1; // 寿命:函数调用期;作用域:本代码块
}
5.2 四层可见范围
| 层 | 谁能看见 | 例子 |
|---|---|---|
代码块 {} |
块内 | for 里的 i |
| 函数 | 函数内 | 形参、局部变量 |
| 文件 | 本 .c 内 |
加 static 的全局变量 |
| 全工程 | 所有 .o |
不加 static 的全局变量(外部链接) |
static的两个含义要分清 :修饰全局变量/函数 时是"收窄可见范围到本文件";修饰局部变量时是"把寿命拉长到全程"。同一个关键字,两个方向。
5.3 栈变量的"生死"
seq_copy[4] 在进入函数时随 sub sp 出现,返回时随 pop 消失(第 5 章的栈帧)。返回后它的地址就失效了------这就是"不能返回局部变量地址"的原因。
第 6 章 上电那一刻:谁把变量放到位置上

图 6-1 启动代码:搬 .data、清 .bss、跳 main(视频 04:45)
复位后的顺序(真实启动文件):
- 硬件 :从
[0x0000_0000]读栈顶 → 装 SP;从[0x0000_0004]读复位向量 → 装 PC。 - Reset_Handler :调
SystemInit(配时钟)。 - 搬
.data:从 Flash 的 LMA(_sidata)拷到 SRAM 的 VMA(_sdata..._edata)。 - 清
.bss:_sbss..._ebss整片写 0。 bl main:PC 跳到main。
为什么
.data要搬? 因为初值必须"保留"(Flash 不丢),而运行时要"能改"(必须可写 RAM)。Flash 不可写,所以只能复制一份到 SRAM。
这一段和 P04《启动》是同一件事,那期会逐行拆启动文件;这里只要记住"三件事:搬、清、跳"。
第 7 章 三个关键字

图 7-1 -O2 下两版汇编:无 volatile 只读一次内存,有 volatile 每圈重读(视频 05:50)
7.1 const
- 全局
const→.rodata(Flash),省 RAM; - 但局部
const不一定进.rodata:它可能仍在栈上,只是"你不能改"。
7.2 static
- 全局加
static:外部文件看不见(避免命名冲突); - 函数内加
static:只初始化一次,寿命全程; - 静态局部变量的初始化发生在"第一次执行到声明处之前"(C 标准里是程序启动时,实现上由启动代码清零 + 首次进入时赋值),不是每次调用都初始化。
7.3 volatile
c
while (g_ms) { } // 忙等
| 编译 | 无 volatile | 有 volatile |
|---|---|---|
-O0 |
每圈都 ldr |
每圈都 ldr |
-O2 |
只读一次,cbz 后死循环 |
每圈 ldr + cbz |
边界(重要):
volatile只保证"每次都真的访问内存",不保证原子;- 不是内存屏障(编译器和 CPU 都可能重排其他访问);
- 读-改-写(
g_ms++)在中断并发下仍需临界区/原子指令。
这一条是嵌入式面试高频题:
volatile解决"看不见的修改",不解决"竞争"。
第 8 章 总结:map 文件与两个恒等式

图 8-1 map 文件、两个恒等式、三个翻车现场(视频 06:50)
两个恒等式:
Flash 占用 = .isr_vector + .text + .rodata + .data(LMA)
RAM 占用 = .data(VMA) + .bss + 堆 + 栈
三个真实翻车现场:
| 现象 | 原因 | 定位 |
|---|---|---|
HardFault 上电即死 |
局部大数组撑爆栈 | 看 SP 与 _estack 距离 |
region RAM overflowed |
.bss 太大 |
看 map 文件 |
while(g_ms==0); 卡死 |
没加 volatile |
看 -O2 反汇编 |
五句话:
- Flash 存不变的(代码、常量、初值模板)。
- RAM 分四块(
.data、.bss、堆、栈)。 - 变量进哪段看声明:
const→Flash,初值 0→.bss,局部→栈。 static管"谁能看见"(全局)或"活多久"(局部)。volatile管"每次都真读",不管原子、不管顺序。
自测(8 题)
- 为什么
.data在 Flash 和 SRAM 各有一份? - 显式写
= 0的全局变量进哪个段? - 函数内
static int s;的寿命和作用域? - 局部的
const int a = 5;一定在 Flash 吗? .bss不占 Flash,为什么?- 不加
volatile,-O2下忙等为什么可能死循环? volatile能保证g_ms++原子吗?- 栈和堆在链接脚本里没有显式分配时,会发生什么?
参考答案
- Flash 存初值模板(掉电不丢),SRAM 存运行副本(可写)。
.bss(编译器知道 0 不用存)。- 寿命全程;作用域仅该函数。
- 不一定;局部
const常在栈上,只是不可改。 .bss全 0,启动时整片清零即可,不需要在 Flash 存 0。- 编译器把
g_ms缓存进寄存器,循环条件永远用旧值。 - 不能;需要关中断/原子指令。
- 栈堆共享剩余 RAM,向中间生长,撞上就 HardFault 或踩坏数据。
附录 本期关键数字
| 项 | 值 |
|---|---|
| Flash 占用 | 12340 B |
| RAM 占用 | 1680 B |
| 栈/堆共享剩余 | 1540 B(后续 F103 工程实测) |
| 七个变量 | SEQ / g_ms / g_err_cnt / s_seq_idx / s_ps / seq_copy / bsrr |
| 段 | .isr_vector / .text / .rodata / .data / .bss / heap / stack |
成片与本文数值均来自同工程;换芯片/换链接脚本后数字会变,方法不变。