MCU堆栈与内存资源(RAM、ROM)关系深度解析

目录

一、ROM和RAM区别

二、堆栈、堆、栈区别

1、栈(Stack)

2、堆(Heap)

3、栈的运行过程

4、堆的运行过程

三、MCU的启动过程


一、ROM和RAM区别

MCU总体内存
┌───────────────────────────────────┐
│              ROM(Flash)          │
│  ┌──────────────────────────────┐ │
│  │  .text 段(程序代码)           │ │  程序指令,烧录进去,运行时从这里取
│  │  .rodata 段(常量数据)         │ │  const定义的常量,掉电不丢
│  │  .data初值(初始化全局/静态变量) │ │  存储初始化的变量的初值,启动时搬到RAM
│  │  中断向量表                    │ │  定义中断服务函数的入口地址。
│  │  启动代码                      │ │  初始化栈指针(SP)、堆的起始地址、堆栈大小
│  └──────────────────────────────┘ │  
│                                   │
├───────────────────────────────────┤
│              RAM(SRAM)           │
│  ┌──────────────────────────────┐ │
│  │  .data段(初始化后的全局变量)    │ │ 运行时的全局变量/静态变量(有初值)
│  │  .bss段(未初始化的全局/静态变量)│  │ 运行时的全局变量/静态变量(无初值,默认0)
│  │  堆区(heap,动态分配内存)      │ │  动态申请空间,如malloc的内容
│  │  栈区(stack,局部变量/返回地址) │ │  函数局部变量、中断保存、子程序返回地址
│  └──────────────────────────────┘ │
└───────────────────────────────────┘  

注:有初值的全局变量在ROM和RAM同时分配空间

二、堆栈、堆、栈区别

严格意义上来说,堆栈 = 栈(Stack)。只是在中文口语里,经常把“栈”叫成“堆栈”。其中,

解决 “程序怎么管理临时变量、保证函数跳回来” 的问题。

解决 “程序运行中怎么动态使用可变大小的内存” 的问题。

1、栈(Stack)

特点:

  • 自动内存管理区,用于存储函数调用时的返回地址、局部变量、函数参数等。

  • 自动管理:由编译器生成指令,硬件(栈指针SP)自动调整。

  • 高效但容量小:存取速度快,但大小固定(需在编译时确定)。

  • 生命周期:随函数调用开始,函数返回时自动释放。

  • 作用:

    作用 说明
    保存函数调用返回地址 函数执行完了知道跳回哪里继续执行
    保存局部变量 每次函数调用,都有自己独立的一份局部变量空间
    保存中断现场 进入中断时,把当前CPU状态保存到栈里,回来再恢复
    实现递归 支持函数自己调用自己,每一层都有自己的局部数据

    总结一句话: 栈负责短期、自动、快速、临时的数据保存。

    2、堆(Heap)

    特点:

  • 动态内存分配区,用于存储程序运行时动态申请的内存(如mallocnew)。

  • 手动管理:需要程序员显式分配(malloc)和释放(free),否则会导致内存泄漏。

  • 碎片化风险:频繁分配/释放可能产生内存碎片。

  • 生命周期:从分配开始到手动释放结束。

  • 作用:

    作用 说明
    动态内存管理 程序运行时,动态申请需要多大用多大
    支持复杂数据结构 比如链表、树、图这些,节点数量变化时,堆很灵活
    长期保存运行中生成的数据 只要你不free掉,堆里的数据就一直在,跨函数也能用
    实现大对象或不确定大小的对象 比如一个不确定长度的字符串、一个动态数组

    总结一句话: 堆负责长期、灵活、手动控制的数据保存。

    3、栈的运行过程

    栈运行遵循先进后出(FILO)原则,什么叫先进后出,举个例子: 搬家时装书,先把一本大书放进去,然后放一本小书,最后再放一本笔记本,一层一层的往上叠, 搬到新家后,把这个要把书重新拿出来,就需要先拿笔记本(因为它在最上面),再拿小书,最后拿大书。

    那么问题来了,为什么栈要设计先进后出这个原则,有什么用? 很简单,当然是为了自然的支持函数调用和返回!而且效率最高!再举一个例子:

    函数调用就像出门办事,比如你出门,事情的发展是这样的: 1、你本来在家里。 2、接到一个电话,叫你去超市买菜。 3、在超市买菜时,突然又接到电话,要你顺便去取快递。 4、在取快递时,又接到电话,让你去理发店剪头发

    此时的顺序是:家—>超市—>快递—>理发店 ,那么办完事回家的顺序应该是:先剪完头发(最后的事) ,再取快递,然后再买菜,最后再回家,这个过程就是先进后出(最后干的事,最先完成),栈就是用来记这种“办事顺序”的

    说了那么多,再举一个函数运行的例子,具体过程如下: 1、函数调用时:

  • 返回地址(调用后的下一条指令)压入栈

  • 函数参数压入栈(部分架构通过寄存器传递)。

  • 局部变量在栈中分配空间。

  • 2、函数返回时:

  • 局部变量自动释放。

  • 返回地址弹出,程序跳转回调用点。

  • void func1() {
        int a = 10;  // 局部变量a入栈
        func2();     // 返回地址压栈,跳转到func2
    }
    ​
    void func2() {
        int b = 20;  // 局部变量b入栈
    }                // b出栈,返回func1
    ​
    int main() {
        func1();     // 返回地址压栈,跳转到func1
        return 0;
    }
    ​
    栈变化:
    | main()返回地址 | → 调用func1时压栈
    | a = 10        | → func1的局部变量
    | func1返回地址  | → 调用func2时压栈
    | b = 20        | → func2的局部变量
    | (func2返回后)  | → b和func1返回地址出栈
    | (func1返回后)  | → a和main返回地址出栈

    4、堆的运行过程

    我们从上面的例子可以知道,栈是为了自动管理函数调用的过程,解决程序执行的跳转、临时数据保存、现场恢复问题。问题来了,那么堆是为了解决什么问题? 简单的来说,堆是为了解决程序运行中,内存需求“大小不确定、数量不确定、生命周期不确定”的问题。

    应用场景 为什么用堆? 举个例子
    1. 动态数组 数组大小运行时才能确定 用户输入需要存1000个数
    2. 链表、树、图 节点个数变化灵活,需要动态分配 增加或删除链表节点
    3. 大对象存储 栈太小放不下,堆能放很大 分配1MB缓存区
    4. 动态内存池管理 频繁创建销毁对象,提高效率 内存池分配任务对象
    5. 多线程栈切换 有些线程的栈是从堆上动态申请的 RTOS系统中创建线程
    6. 动态分配字符串 字符串长度变化,不定长 动态调整字符串大小
    7. 数据缓存和缓冲区 临时存数据,大小根据实际情况变化 网络接收大文件时动态缓冲

    1、动态数组

    int n = 1000;
    int *arr = (int*)malloc(n * sizeof(int)); // 动态分配n个int
    // 使用arr[0]~arr[n-1]
    free(arr); // 用完释放

    2、链表节点

    struct Node {
        int val;
        struct Node *next;
    };
    ​
    struct Node *p = (struct Node*)malloc(sizeof(struct Node)); // 创建新节点
    p->val = 10;
    p->next = NULL;
    // 使用完成后 free(p); 链表节点数量不固定,只能用堆动态开。

    3、大对象存储

    char *big_buffer = (char *)malloc(1024 * 1024); // 1MB缓存
    // 使用big_buffer处理数据
    free(big_buffer);
    

    4、动态内存池(简易内存池)

    // 一次性申请一大片堆内存
    void* pool = malloc(1000);
    ​
    // 比如,从pool中偏移100字节作为某对象
    void* obj = (char*)pool + 100;
    ​
    // 最后统一释放整个pool
    free(pool);
    

    5、多线程栈切换

    #include <pthread.h>
    #include <stdlib.h>
    #include <stdio.h>
    ​
    // 线程执行的函数
    void *thread_func(void *arg) {
        printf("线程开始运行!\n");
        return NULL;
    }
    ​
    int main() {
        pthread_t thread;
        pthread_attr_t attr;
        size_t stacksize = 1024 * 1024; // 栈大小1MB
    ​
        // 1. 初始化线程属性对象
        pthread_attr_init(&attr);
    ​
        // 2. 动态分配栈内存(在堆上)
        void *stack = malloc(stacksize);
        if (!stack) {
            perror("malloc failed");
            return -1;
        }
    ​
        // 3. 设置线程使用的栈
        pthread_attr_setstack(&attr, stack, stacksize);
    ​
        // 4. 创建线程,指定用我们准备好的栈
        pthread_create(&thread, &attr, thread_func, NULL);
    ​
        // 5. 等待线程结束
        pthread_join(thread, NULL);
    ​
        // 6. 释放分配的堆栈内存
        free(stack);
    ​
        // 7. 销毁属性对象
        pthread_attr_destroy(&attr);
    ​
        return 0;
    }
    

    6、动态分配字符串

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    ​
    int main() {
        char *str = (char*)malloc(10 * sizeof(char));  // 初始分配10字节
        strcpy(str, "Hello");
    ​
        // 扩展字符串(追加" World!")
        str = (char*)realloc(str, 20 * sizeof(char));  // 重新分配20字节
        if (str == NULL) {
            printf("内存分配失败!\n");
            return 1;
        }
        strcat(str, " World!");  // 拼接字符串
        printf("结果: %s\n", str);  // 输出: Hello World!
    ​
        free(str);
        return 0;
    }
    

    7、数据缓存和缓冲区

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    ​
    // 模拟网络接收函数(返回实际读取的字节数)
    size_t network_receive(char *buffer, size_t max_chunk_size) {
        // 假设每次接收1KB数据块(实际中从socket读取)
        const char *mock_data = "这是一个模拟的数据块...";
        size_t data_len = strlen(mock_data) + 1; // +1为字符串结尾\0
        memcpy(buffer, mock_data, data_len);
        return data_len;
    }
    ​
    int main() {
        size_t total_received = 0;   // 已接收数据总长度
        size_t buffer_size = 4096;   // 初始缓冲区大小(4KB)
        char *dynamic_buffer = (char*)malloc(buffer_size); // 从堆分配初始缓冲区
        if (!dynamic_buffer) {
            perror("初始分配失败");
            return 1;
        }
    ​
        while (1) {  // 模拟循环接收数据
            // 检查剩余空间是否足够(假设每次接收最多1KB)
            size_t remaining_space = buffer_size - total_received;
            if (remaining_space < 1024) {
                // 扩展缓冲区:每次扩大2倍(策略可调整)
                buffer_size *= 2;
                char *new_buffer = (char*)realloc(dynamic_buffer, buffer_size);
                if (!new_buffer) {
                    perror("扩展缓冲区失败");
                    free(dynamic_buffer);
                    return 1;
                }
                dynamic_buffer = new_buffer;
                printf("缓冲区扩展至 %zu 字节\n", buffer_size);
            }
    ​
            // 接收数据到缓冲区当前写入位置
            size_t received = network_receive(
                dynamic_buffer + total_received, // 写入位置偏移
                1024                             // 单次最多接收1KB
            );
            
            if (received == 0) break; // 接收完成
            total_received += received;
        }
    ​
        // 接收完成后处理数据(例如保存到文件)
        printf("总计接收 %zu 字节\n", total_received);
        
        // 释放堆内存
        free(dynamic_buffer);
        return 0;
    }
    

    三、MCU的启动过程

    1、上电(Power-On)

  • MCU 通电后,电源管理模块(PWR)开始工作,芯片内部电压稳定。

  • 通常有个复位电路,比如上电复位(POR, Power-On Reset),确保芯片不会在电压不稳定时乱跑。

  • 2、硬件复位(Reset)

  • MCU检测到复位信号,内部自动初始化:

  • 寄存器清零或置为默认值

  • 时钟系统复位

  • 外设模块关闭/待命

  • 确保 MCU 是在一个确定的初始状态开始工作的。

  • 3、Boot ROM 启动(Bootloader)

  • MCU会跳转到内部固化的启动代码区(Boot ROM),执行一段出厂烧录的程序,主要作用是:

  • 判断启动模式(比如:从主闪存启动?串口下载?USB启动?)

  • 配置基本的系统时钟(Oscillator、PLL)

  • 最后跳转到用户应用程序(通常是 Flash 中的某个地址,比如 0x08000000)。

  • 有的芯片可以通过引脚(BOOT引脚)、寄存器,或特殊命令来选择启动源。如下,是cs32的启动模式

    4、加载中断向量表(Vector Table)

  • 中断向量表通常是 MCU Flash 开头的一块区域。前两项特别重要:

  • 第0项:主堆栈指针(MSP)初始化值。

  • 第1项:复位中断处理函数(Reset Handler)的地址。

  • MCU会从向量表读取 MSP 和 Reset_Handler 地址,并跳转到 Reset_Handler 开始正式执行用户代码。

  • 5、执行Reset_Handler(启动文件)

    Reset_Handler 是启动文件(startup.s 或 startup.c)里的一个函数,它负责:

  • 关闭中断(防止初始化时被打断)

  • 配置中断向量表位置(SCB->VTOR)

  • 初始化时钟(有些MCU是早就初始化了,有些需要自己设)

  • 初始化内存:

  • .data 区(已初始化的全局变量)从 Flash 拷贝到 RAM

  • 清零 .bss 区(未初始化的全局变量、静态变量)

  • 调用系统库的初始化函数(比如标准C库的 SystemInit

  • 最后调用 main() 函数 —— 你的主程序正式开始运行。

  • 6、进入主程序(main)

  • 这个时候 MCU 的硬件基本配置好了,RAM正确,堆栈正确,中断向量表正确。

  • 运行你写在 main() 里的应用逻辑,比如点灯、通讯、控制等等。

  • 作者:o山山而川o

    物联沃分享整理
    物联沃-IOTWORD物联网 » MCU堆栈与内存资源(RAM、ROM)关系深度解析

    发表回复