P02_你的变量住在哪里_教学文档

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 章 三个问题

  1. const uint8_t SEQ[] = {...} 放在哪?为什么不占 RAM?
  2. volatile uint32_t g_ms; 为什么必须 volatile?不加会怎样?
  3. 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(指针本身) 栈 仅函数调用期 本代码块

注意三个细节:

  1. static 不改变位置,只收窄可见范围(①④⑤ 都在"全程"那一列)。
  2. 显式写 = 0 也进 .bss ,不进 .data------编译器知道 0 不用存。
  3. 局部数组在栈上:编译器只决定"去栈上",具体地址运行时由 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)

复位后的顺序(真实启动文件):

  1. 硬件 :从 [0x0000_0000] 读栈顶 → 装 SP;从 [0x0000_0004] 读复位向量 → 装 PC。
  2. Reset_Handler :调 SystemInit(配时钟)。
  3. 搬 .data :从 Flash 的 LMA(_sidata)拷到 SRAM 的 VMA(_sdata..._edata)。
  4. 清 .bss :_sbss..._ebss 整片写 0。
  5. 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 反汇编

五句话:

  1. Flash 存不变的(代码、常量、初值模板)。
  2. RAM 分四块(.data、.bss、堆、栈)。
  3. 变量进哪段看声明:const→Flash,初值 0→.bss,局部→栈。
  4. static 管"谁能看见"(全局)或"活多久"(局部)。
  5. volatile 管"每次都真读",不管原子、不管顺序。

自测(8 题)

  1. 为什么 .data 在 Flash 和 SRAM 各有一份?
  2. 显式写 = 0 的全局变量进哪个段?
  3. 函数内 static int s; 的寿命和作用域?
  4. 局部的 const int a = 5; 一定在 Flash 吗?
  5. .bss 不占 Flash,为什么?
  6. 不加 volatile,-O2 下忙等为什么可能死循环?
  7. volatile 能保证 g_ms++ 原子吗?
  8. 栈和堆在链接脚本里没有显式分配时,会发生什么?

参考答案

  1. Flash 存初值模板(掉电不丢),SRAM 存运行副本(可写)。
  2. .bss(编译器知道 0 不用存)。
  3. 寿命全程;作用域仅该函数。
  4. 不一定;局部 const 常在栈上,只是不可改。
  5. .bss 全 0,启动时整片清零即可,不需要在 Flash 存 0。
  6. 编译器把 g_ms 缓存进寄存器,循环条件永远用旧值。
  7. 不能;需要关中断/原子指令。
  8. 栈堆共享剩余 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

成片与本文数值均来自同工程;换芯片/换链接脚本后数字会变,方法不变。

相关推荐
叼烟扛炮1 小时前
Linux 学习笔记(三十一):线程概念、底层分页机制与初体验
linux·运维·服务器·笔记·学习·1024程序员节
星恒随风1 小时前
Linux Socket UDP编程(一):手写UDP回显服务器与客户端,从socket、bind、recvfrom到网络字典服务器完整实战
linux·服务器·网络·笔记
凉、介1 小时前
深入理解 ARMv7-M | 异常/中断处理
笔记·学习·嵌入式·arm
蒸蒸yyyyzwd1 小时前
工具、力扣、中间件学习笔记
笔记·八股
深圳老胡1 小时前
液面探测功能的硬件方案对比:原理、优缺点与选型建议
笔记·单片机
zhangzhangkeji1 小时前
stm32, HAL 库,RTC:
stm32
成成爱质评1 小时前
Windows 任务计划程序 + 唤醒定时器:让电脑按时开机干活
windows·单片机·电脑
凉、介1 小时前
深入理解 ARMv7-M|处理器模式与寄存器
笔记·学习·嵌入式·arm
AMASENSYS艾马斯2 小时前
低压变频器输入输出电抗器选型指南
驱动开发·单片机·机器学习·硬件工程·精益工程