目录
-
- [1. 内存映射:统一编址的"城市地图"](#1. 内存映射:统一编址的“城市地图”)
-
- [1.1 别名区与位带操作](#1.1 别名区与位带操作)
- [2. Flash存储器:代码的"永久家园"](#2. Flash存储器:代码的“永久家园”)
-
- [2.1 Flash的物理特性](#2.1 Flash的物理特性)
- [2.2 零等待区域:性能的"快车道"](#2.2 零等待区域:性能的“快车道”)
- [2.3 Flash的读写保护](#2.3 Flash的读写保护)
- [3. SRAM:程序的"工作台"](#3. SRAM:程序的“工作台”)
-
- [3.1 SRAM的特性](#3.1 SRAM的特性)
- [3.2 内存布局:堆与栈](#3.2 内存布局:堆与栈)
-
- [3.2.1 .data段:已初始化的全局变量](#3.2.1 .data段:已初始化的全局变量)
- [3.2.2 .bss段:未初始化的全局变量](#3.2.2 .bss段:未初始化的全局变量)
- [3.2.3 堆(Heap):动态分配的内存](#3.2.3 堆(Heap):动态分配的内存)
- [3.2.4 栈(Stack):函数调用和局部变量](#3.2.4 栈(Stack):函数调用和局部变量)
- [3.3 栈的大小如何确定?](#3.3 栈的大小如何确定?)
- [4. 启动过程:从复位到main](#4. 启动过程:从复位到main)
-
- [4.1 第1步:从向量表获取初始值](#4.1 第1步:从向量表获取初始值)
- [4.2 第2步:执行复位处理函数](#4.2 第2步:执行复位处理函数)
- [4.3 启动流程图](#4.3 启动流程图)
- [5. 实际案例:从map文件看内存布局](#5. 实际案例:从map文件看内存布局)
- [6. 总结:存储器是程序的"安身之所"](#6. 总结:存储器是程序的“安身之所”)
在前两讲中,我们认识了MCU的整体架构,也深入了CPU内部的流水线和指令集。现在,我们把目光转向另一个至关重要的组成部分------存储器。
如果说CPU是MCU的"大脑",那么存储器就是"大脑"的记忆系统。没有它,CPU再强大的运算能力也毫无意义------就像一位天才数学家,如果没有纸和笔来记录中间结果,也无法完成任何复杂计算。
这一讲,我们将深入MCU的存储器世界,看看Flash和RAM是如何布局的,程序烧录后到底存放在哪里,以及堆、栈、全局变量这些抽象概念在物理内存中究竟对应什么。
1. 内存映射:统一编址的"城市地图"
在MCU中,所有可以被CPU访问的资源------包括Flash、RAM、外设寄存器------都被分配了唯一的地址。这被称为统一编址。
想象一下,这就像一个城市的门牌号系统:每条街道(Flash区域、RAM区域、外设区域)都有自己专属的门牌号范围。CPU通过地址总线发出一个门牌号,总线系统就会将CPU引导到对应的物理位置。
以典型的Cortex-M3/M4处理器为例(如STM32F103系列),其4GB的地址空间通常划分为:
| 地址范围 | 区域名称 | 用途 |
|---|---|---|
| 0x0000 0000 - 0x1FFF FFFF | Code区 | Flash存储器,存放程序代码和只读数据 |
| 0x2000 0000 - 0x3FFF FFFF | SRAM区 | 片内SRAM,存放变量、堆、栈 |
| 0x4000 0000 - 0x5FFF FFFF | 外设区 | GPIO、USART、定时器等外设的寄存器 |
| 0x6000 0000 - 0xDFFF FFFF | 扩展区 | 外部存储器接口(如果芯片支持) |
| 0xE000 0000 - 0xE00F FFFF | 系统区 | 内核内部寄存器,如NVIC、SysTick等 |
为什么需要内存映射? 因为它让CPU可以用同一套指令 来访问代码、变量和外设。无论目标是Flash还是GPIO,都用LDR(读)和STR(写)指令。这种设计极大简化了指令集和编程模型。
1.1 别名区与位带操作
Cortex-M3/M4还有一个有趣的设计------位带(Bit-Band)。它将一个地址位扩展为一个字(32位),实现了对单个位的原子操作。
位带别名地址 = 位带基址 + (字节偏移 × 32) + (位序号 × 4)
例如,要操作SRAM区地址0x2000 0000的第2位,可以直接写别名地址0x2200 0008(计算过程略),而不需要通过读-改-写三步。这在需要原子操作位变量的场景(如标志位、通信协议状态机)中非常有用。
2. Flash存储器:代码的"永久家园"
Flash是MCU中的非易失性存储器------断电后数据依然保留。它存储着程序代码、常量以及掉电后需要保留的数据。
2.1 Flash的物理特性
Flash的存储单元是基于浮栅晶体管实现的。通过往浮栅中注入或释放电子,来改变晶体管的阈值电压,从而区分"0"和"1"。
这种物理结构带来了几个关键特性:
| 特性 | 说明 |
|---|---|
| 非易失性 | 断电后数据可保存10年以上 |
| 读速度快 | 随机读取通常只需几十纳秒 |
| 写入慢 | 编程一个字节/字需要几微秒到几十微秒 |
| 擦除更慢 | 必须按"页"或"扇区"擦除,耗时毫秒级 |
| 寿命有限 | 典型擦写次数:1万次(低成本)到10万次(工业级) |
Flash擦除为什么不能按字节? 这是由物理结构决定的。浮栅晶体管的电子注入和释放会影响周围单元,因此需要以大块为单位进行操作来保证可靠性。
2.2 零等待区域:性能的"快车道"
这是一个在开发中经常被忽视、但对性能影响巨大的概念。
CPU从Flash取指令时,需要等待Flash的读取延迟。如果Flash的速度跟不上CPU的主频,就必须插入等待周期(Wait States)。
很多MCU在Flash的前几KB(通常是16KB或64KB)设置了零等待区域(Zero-Wait-Area):
- 这段区域使用高速缓冲区或并行读取结构,能以CPU全速运行。
- 超出此区域的Flash,每次读取需要插入1-3个等待周期。
部分高性能Mcu也会通过在Cpu与Flash之间设置cache解决此问题。
这意味着什么?
- 如果你的程序代码能够全部放在零等待区域内,执行效率最高。
- 如果代码超出零等待区域,关键代码(如中断服务函数、高频调用的算法)应该放在零等待区域内,而启动代码、初始化代码等可以放在外部区域。
2.3 Flash的读写保护
Flash还具备安全特性:
- 读保护:防止通过调试器(如JTAG/SWD)读取Flash内容,保护固件不被盗取。
- 写保护:锁定特定扇区,防止程序意外修改自身代码。
解除读保护通常需要执行全片擦除------这是MCU防抄板的基础手段之一。
3. SRAM:程序的"工作台"
SRAM(静态随机存取存储器)是MCU中的易失性存储器------断电即失。它是程序的"工作台",所有变量、堆、栈都在这里运行。
3.1 SRAM的特性
| 特性 | 说明 |
|---|---|
| 读写速度快 | 通常无需等待周期,与CPU速度匹配 |
| 无限寿命 | 无擦写次数限制 |
| 易失性 | 掉电数据全部丢失 |
| 密度低 | 单位面积容量比Flash小,成本更高 |
正因为SRAM速度极快且无写入寿命限制,所有运行时需要频繁修改的数据都放在这里。
3.2 内存布局:堆与栈
MCU的SRAM空间通常被划分为三个区域:
------------------ 高地址
| 栈 (Stack) | ← 向下增长(向低地址)
| ↓↓↓↓ |
| (空闲) |
| ↑↑↑↑ |
| 堆 (Heap) | ← 向上增长(向高地址)
| .bss段 | ← 未初始化的全局/静态变量
| .data段 | ← 已初始化的全局/静态变量
------------------ 低地址
3.2.1 .data段:已初始化的全局变量
c
int global_var = 100; // 存放在.data段
static int static_var = 50; // 也存放在.data段
3.2.2 .bss段:未初始化的全局变量
c
int global_uninit; // 存放在.bss段
注意:.bss段在程序启动时会被清零,所以未初始化的全局变量默认值是0。
3.2.3 堆(Heap):动态分配的内存
c
int *p = malloc(100 * sizeof(int)); // 从堆中分配
堆用于malloc()、calloc()等动态分配。在嵌入式开发中,堆的使用需要非常谨慎:
- 容易产生内存碎片
- 分配失败(返回NULL)难以处理
- 很多嵌入式编程规范(如MISRA C)禁止使用动态内存分配
3.2.4 栈(Stack):函数调用和局部变量
c
void func(void) {
int local_var = 10; // 存放在栈上
char buffer[256]; // 也存放在栈上
}
栈存储的是:
- 函数的局部变量
- 函数参数
- 返回地址(LR寄存器)
- 中断发生时保存的上下文
栈溢出 是嵌入式开发中最常见的问题之一。当函数嵌套过深、局部变量过大,或中断嵌套过多时,栈指针可能冲破边界,覆盖堆或.data段的数据,导致HardFault或难以排查的随机故障。
3.3 栈的大小如何确定?
栈大小通常由链接脚本(.ld文件)定义。如何合理配置?
一种常见方法是:编译后查看生成的.map文件,找到"栈最大使用深度",然后在基础上增加20%-50%的安全余量。
对于RTOS环境,每个任务都有独立的栈空间,更需要精确计算。
4. 启动过程:从复位到main
理解了Flash和SRAM的分工,现在我们可以串联起MCU的完整启动流程了。
当MCU上电或按下复位按钮时,硬件会执行以下步骤:
4.1 第1步:从向量表获取初始值
复位后,CPU自动从地址0x00000000读取:
- 初始主栈指针(MSP):从地址0x00000000读取
- 复位向量:从地址0x00000004读取,这是复位后要跳转的地址
4.2 第2步:执行复位处理函数
CPU跳转到复位向量指向的地址,通常是Reset_Handler(在启动文件中定义)。这个函数用汇编编写,完成:
- 设置栈指针(实际上第1步已经做了初步设置)
- 初始化
.data段:将Flash中的初始值拷贝到SRAM中 - 清零
.bss段:将SRAM中的.bss区域全部写0 - 调用
SystemInit():配置系统时钟 - 调用
__libc_init_array():初始化C库 - 跳转到
main():终于进入用户的应用程序
4.3 启动流程图
复位 → 读取向量表 → 设置MSP → 跳转到Reset_Handler
↓
初始化.data段(从Flash拷贝到SRAM)
↓
清零.bss段
↓
SystemInit()(配置时钟)
↓
初始化C库
↓
main() ← 用户代码开始执行
5. 实际案例:从map文件看内存布局
我们通过一个实际例子来直观感受内存布局。假设我们有如下代码:
c
#include <stdint.h>
int global_init = 0x1234; // .data段
int global_uninit; // .bss段
const int constant = 0x5678; // .rodata段(放在Flash)
void main(void) {
static int static_init = 0xABCD; // .data段
static int static_uninit; // .bss段
int local = 0xDEAD; // 栈上
while(1);
}
编译后查看.map文件(简化):
.data 0x20000000 0x0008
0x20000000 global_init
0x20000004 static_init
.bss 0x20000008 0x0008
0x20000008 global_uninit
0x2000000c static_uninit
.rodata 0x08001000 0x0004
0x08001000 constant
.text 0x08001004 0x0020
0x08001004 main
可以看到:
- Flash从
0x08000000开始,存放.text(代码)和.rodata(常量) - SRAM从
0x20000000开始,存放.data和.bss - 栈空间通常放在SRAM的末尾,向下增长
6. 总结:存储器是程序的"安身之所"
这一讲,我们深入了MCU的存储器世界:
- 内存映射让Flash、RAM、外设寄存器共享同一个地址空间,CPU用统一的指令访问所有资源。
- Flash 是程序的永久家园,存储代码和只读数据。零等待区域的存在意味着代码布局会影响性能,关键代码应放在高速区。
- SRAM是程序的工作台,全局变量、堆、栈都在这里运行。堆的使用需谨慎,栈溢出是嵌入式开发的大敌。
- 启动过程 是Flash和SRAM协同工作的典范:CPU从Flash读取向量表,初始化栈指针,将
.data段从Flash拷贝到SRAM,清零.bss段,最后进入main()。
理解了存储器架构,你就掌握了程序从烧录到运行的完整生命周期。无论是优化性能、排查内存问题,还是实现OTA升级,这些知识都是必不可少的基石。
下一讲预告 :我们将进入时钟系统------MCU的"心跳"。时钟不仅决定了程序跑多快,还深刻影响着功耗和外设的配置。我们会深入时钟树、PLL锁相环,以及低功耗模式下的时钟管理策略。
思考题 :当一个全局变量被声明为const,它会被放在哪个段?是Flash还是SRAM?为什么这样设计?这样的设计会带来什么好处和限制?欢迎带着这个问题,等待后续关于链接脚本和内存分配的深入探讨。