一、嵌入式内存:和电脑完全不同的世界
电脑有操作系统帮你管内存,内存不够就加一条;但在MCU上,RAM用完了就是死,栈溢出、HardFault、malloc返回NULL都是家常便饭。嵌入式开发里,你写的每一个变量放在哪里,都得心里有数。
二、嵌入式内存四大板块
嵌入式内存主要分为四大板块,各有各的用途:
- Flash:存代码和常量,掉电不丢失,通常64KB-2MB
- SRAM:存变量、栈、堆,掉电丢失,通常8KB-512KB
- EEPROM:存配置参数,可字节读写,通常256B-64KB
- 外部存储:SPI Flash/SD卡,大容量扩展,通常1MB-32GB
2.1 内存映射:看懂你的内存地图
MCU的内存就像一张地图,每个区域都有固定的地址:
0x08000000 ---- Flash(代码/常量)
0x20000000 ---- SRAM(变量/栈/堆)
0x40000000 ---- 外设寄存器
0xE0000000 ---- Cortex-M 内部外设
4GB地址空间,但实际用到的可能不到1MB。
2.2 Flash:代码的安身之所
Flash里主要存三样东西:代码、常量、中断向量表。
.text段存函数.rodata段存常量字符串- 中断向量表放在Flash最前面,上电后CPU先读这里才知道复位后往哪跳
代码示例:常量放在Flash,不占RAM
// 正确:const修饰的常量放在Flash,不占RAM
const int LUT[256] = {1,2,3,...}; // 放在Flash里,不占RAM
// 错误:不加const,启动时会从Flash复制一份到RAM
int LUT[256] = {1,2,3,...}; // 启动时会从Flash复制一份到RAM,占用256*4=1024字节RAM
一个256项的查找表,差别就是0字节和1024字节的RAM消耗。
2.3 SRAM:变量的主战场
SRAM内部又分为三大区域:
.data段:已初始化的全局变量,启动时从Flash复制.bss段:未初始化的全局变量,启动时自动清零- 栈和堆:剩余空间给栈和堆,它们相向生长,碰头就是HardFault
代码示例:结构体对齐,省33%内存
// 未优化:12字节
struct Bad {
uint8_t flag; // 1字节 + 1字节padding
uint16_t count; // 2字节
uint32_t value; // 4字节
uint8_t chk; // 1字节 + 3字节padding
}; // 12 bytes
// 优化后:8字节(重排字段)
struct Good {
uint32_t value; // 4字节
uint16_t count; // 2字节
uint8_t flag; // 1字节
uint8_t chk; // 1字节
}; // 8 bytes -- 省33%
2.4 栈溢出:最容易被忽视的内存杀手
栈溢出的三大元凶:
- 调用大局部数组
- 中断嵌套
- 递归调用
代码示例:栈溢出的经典场景
// 错误:栈上开2KB数组,很容易溢出
void bad_function(void) {
char buffer[2048]; // 栈上开2KB数组
// ...
} // 每次调用吃2KB栈
// 错误:递归+大数组,必死无疑
void process(int n) {
double matrix[256]; // 栈:我谢谢你
process(n-1); // 无限递归,必死
}
2.5 堆:一把双刃剑
堆的优点是灵活,缺点也很明显:
- malloc/free有开销
- 忘记free会内存泄漏
- 频繁分配会碎片化
- 很多项目直接禁用堆
代码示例:使用静态分配替代动态分配
// 推荐:静态分配,零开销不碎片
#define BUFFER_SIZE 256
static uint8_t buffer[BUFFER_SIZE]; // 编译时确定大小
// 谨慎使用:动态分配,只在必要时用
#include <stdlib.h>
void init_system(void) {
uint8_t* data_buffer = (uint8_t*)malloc(1024);
if (data_buffer == NULL) {
// 处理分配失败
Error_Handler();
}
// ... 使用缓冲区
// 注意:这里不立即释放,避免碎片
}
三、内存优化六把刀
3.1 前三刀:零成本优化
- 加const:查找表、字符串放Flash,不占RAM
- 用static:限定作用域,编译器优化,还能防命名冲突
- 重排字段:大字段在前,消除padding,每结构省2-8B
3.2 后三刀:实战优化
- 栈→静态:大数组别放栈上,用static或malloc防栈溢出
- union复用:互斥使用的变量,用union共享内存,一块内存多种用途
- 变量瘦身:够用就行,uint8_t别用int,enum而非magic
代码示例:union复用内存
// 错误:分开定义,浪费内存
struct {
uint8_t startup_buf[1024];
uint8_t rx_frame[1024];
uint8_t tx_frame[1024];
} SharedRAM_t; // 3072字节
// 正确:union复用,只占1024字节
typedef union {
struct {
uint8_t startup_buf[1024];
} startup;
struct {
uint8_t rx_frame[1024];
uint8_t tx_frame[1024];
} runtime;
} SharedRAM_t; // 1024字节
四、嵌入式内存管理黄金法则
4.1 静态优先
全局/静态变量,编译期确定大小,零碎片风险,嵌入式首选。
4.2 池化分配
预分配固定大小池,从池取用再归还,无碎片O(1)。
代码示例:内存池实现
#define MEM1_BLOCK_SIZE 32
#define MEM1_MAX_SIZE 110*1024
#define MEM1_ALLOC_TABLE_SIZE MEM1_MAX_SIZE/MEM1_BLOCK_SIZE
__align(4) uint8_t mem1base[MEM1_MAX_SIZE]; // 内部SRAM内存池
uint16_t mem1map[MEM1_ALLOC_TABLE_SIZE]; // 内存管理表
// 内存池初始化
void mem_init(void) {
memset(mem1map, 0, MEM1_ALLOC_TABLE_SIZE*2);
memset(mem1base, 0, MEM1_MAX_SIZE);
}
// 内存分配
void *mymalloc(uint32_t size) {
uint32_t offset, cmemb = 0;
uint32_t nmemb = (size + MEM1_BLOCK_SIZE - 1) / MEM1_BLOCK_SIZE;
for(offset = MEM1_ALLOC_TABLE_SIZE - 1; offset >= 0; offset--) {
if(!mem1map[offset]) cmemb++;
else cmemb = 0;
if(cmemb == nmemb) {
for(uint32_t i = 0; i < nmemb; i++)
mem1map[offset + i] = nmemb;
return (void*)&mem1base[offset * MEM1_BLOCK_SIZE];
}
}
return NULL;
}
4.3 栈水线监控
定期查栈最高水位,uxTaskGetStackHighWaterMark。
代码示例:FreeRTOS栈水线监控
#include "FreeRTOS.h"
#include "task.h"
void vTask1(void *pvParameters) {
UBaseType_t stack_high_water_mark;
while(1) {
// 任务代码...
// 获取栈最高水位
stack_high_water_mark = uxTaskGetStackHighWaterMark(NULL);
printf("Task1 stack high water mark: %u words\n", stack_high_water_mark);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
五、三大内存大坑
5.1 第一大坑:栈溢出
不报错、不警告、不崩溃,只是你的变量突然变成随机数。
排查方法:
- 栈底放哨兵值0xDEADBEEF
- 定期检查
- FreeRTOS内置栈水线检测
- 永远留30%余量
5.2 第二大坑:野指针
局部变量的地址返回给调用者,调用者美滋滋地用,但那个地址上的内容早就被覆盖了。
代码示例:野指针的经典场景
// 错误:返回局部变量地址
int* bad_function(void) {
int value = 100;
return &value; // 函数返回后,value的地址会被覆盖
}
// 正确:使用static变量或动态分配
int* good_function(void) {
static int value = 100; // static变量生命周期是整个程序运行期
return &value;
}
5.3 第三大坑:内存碎片
你的系统跑了三天没问题,第四天malloc返回NULL。不是代码写错了,是碎片攒够了。
解决方案:
- 静态分配
- 内存池代替malloc
- FreeRTOS用heap_4支持碎片合并
六、调内存问题的三件利器
6.1 Map文件
编译生成.map,查看每个符号的地址和大小,谁占RAM最多一目了然。
6.2 栈水线
FreeRTOS API,uxTaskGetStackHighWaterMark,告诉你峰值用了多少。
6.3 调试器
HardFault时看栈帧,找到崩溃前最后调用的函数,定位野指针源头。
七、嵌入式内存管理自查清单
7.1 编译期
- 所有常量都加const了吗?
- 查找表、字符串放Flash了吗?
- 结构体字段按大小重排了吗?
- 能用uint8_t/uint16_t的地方用int了吗?
7.2 链接期
- map文件里谁占RAM最多?
- 栈和堆空间够吗?
- 有未使用的全局变量吗?
7.3 运行时
- 栈水线监控开了吗?
- 内存池用了吗?
- malloc/free有配对吗?
- 有野指针风险吗?
八、总结
嵌入式内存管理不是玄学,而是工程。从编译期到运行时,从静态分配到动态监控,每一步都有章可循。记住:在MCU的世界里,RAM不是资源,是命脉。用好每一字节,你的系统才能跑得稳、活得久。
375

被折叠的 条评论
为什么被折叠?



