Mcu架构以及原理——3.存储器架构

目录

    • [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. 设置栈指针(实际上第1步已经做了初步设置)
  2. 初始化.data段:将Flash中的初始值拷贝到SRAM中
  3. 清零.bss段:将SRAM中的.bss区域全部写0
  4. 调用SystemInit():配置系统时钟
  5. 调用__libc_init_array():初始化C库
  6. 跳转到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的存储器世界:

  1. 内存映射让Flash、RAM、外设寄存器共享同一个地址空间,CPU用统一的指令访问所有资源。
  2. Flash 是程序的永久家园,存储代码和只读数据。零等待区域的存在意味着代码布局会影响性能,关键代码应放在高速区。
  3. SRAM是程序的工作台,全局变量、堆、栈都在这里运行。堆的使用需谨慎,栈溢出是嵌入式开发的大敌。
  4. 启动过程 是Flash和SRAM协同工作的典范:CPU从Flash读取向量表,初始化栈指针,将.data段从Flash拷贝到SRAM,清零.bss段,最后进入main()

理解了存储器架构,你就掌握了程序从烧录到运行的完整生命周期。无论是优化性能、排查内存问题,还是实现OTA升级,这些知识都是必不可少的基石。

下一讲预告 :我们将进入时钟系统------MCU的"心跳"。时钟不仅决定了程序跑多快,还深刻影响着功耗和外设的配置。我们会深入时钟树、PLL锁相环,以及低功耗模式下的时钟管理策略。


思考题 :当一个全局变量被声明为const,它会被放在哪个段?是Flash还是SRAM?为什么这样设计?这样的设计会带来什么好处和限制?欢迎带着这个问题,等待后续关于链接脚本和内存分配的深入探讨。

相关推荐
殷紫川1 小时前
吃透 Spring Boot 3 + Spring Cloud 云原生新特性
spring boot·spring cloud·架构
weiyvyy1 小时前
嵌入式硬件接口的定义与作用
单片机·嵌入式硬件·信息与通信·信息化系统
heimeiyingwang1 小时前
【架构实战】Spring Cloud微服务实战入门
spring cloud·微服务·架构
senijusene2 小时前
依赖51 单片机的 Modbus 协议温度采集与外设控制系统的实现
c语言·单片机·嵌入式硬件·51单片机·keil
一叶飘零_sweeeet2 小时前
Redis 高可用全链路拆解:从主从复制到集群架构的原理与实践
redis·架构·redis高可用架构
JSMSEMI112 小时前
JSM1040T 1Mbps高速具有总线唤醒功能的CAN总线收发器
单片机·嵌入式硬件
SuniaWang2 小时前
《Spring AI + 大模型全栈实战》学习手册系列 · 专题八:《RAG 系统安全与权限管理:企业级数据保护方案》
java·前端·人工智能·spring boot·后端·spring·架构
zzh940772 小时前
GPT-4o与Gemini 3镜像站背后的算力与工程:大模型训练基础设施拆解
人工智能·深度学习·架构
jianqiang.xue2 小时前
ESP32-S3 运行 Linux 全指南:从 RISC-V 模拟器移植到 8 秒快速启动
linux·stm32·单片机·mongodb·risc-v·esp32s3