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)
特点:
动态内存分配区,用于存储程序运行时动态申请的内存(如malloc
、new
)。
手动管理:需要程序员显式分配(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