1. 为什么你的STM32项目需要自己动手管内存?
大家好,我是老张,在嵌入式这行摸爬滚打十几年了。今天想和大家聊聊一个很多新手朋友觉得“高大上”但又绕不开的话题——内存管理。尤其是在用STM32这类资源受限的单片机时,这个话题就更实际了。
你可能用过STM32CubeMX,它帮你把时钟、外设初始化得明明白白,但生成代码后,当你开始写应用逻辑,尤其是涉及到动态创建对象、处理变长数据时,是不是经常遇到内存不够用,或者程序跑着跑着就“死”了的情况?我当年可是踩过不少坑。比如,一个数据采集项目,需要临时缓存一批传感器数据,数据量时大时小。一开始图省事,直接用了标准C库里的 malloc 和 free,结果项目现场运行几天后,系统就卡死了。后来一查,是内存碎片化太严重,虽然总内存还有不少,但都是零零碎碎的小块,申请一个稍大点的连续空间就失败了。
这就是为什么在嵌入式系统里,我们不能完全依赖编译器自带的内存管理。标准的动态内存分配,虽然灵活,但存在几个致命问题:分配时间不确定(你不知道它要找多久)、容易产生内存碎片、以及缺乏边界保护容易踩内存。在实时性要求高的嵌入式环境里,这些都是“定时炸弹”。
所以,我们需要一种更可控、更高效、更“嵌入式”的内存管理方法。分块内存管理就是其中一种非常经典且实用的方案。它不像操作系统里的虚拟内存那么复杂,核心思想就一句话:把一大块连续内存(内存池)预先切成大小固定的“块”,然后通过一个“账本”(管理表)来记录每一块是空闲还是被占用。听起来是不是很像你去停车场停车?停车场(内存池)有固定数量的车位(内存块),管理员(管理表)手里有个本子,记录着哪个车位空着,哪个车位被占了,以及被占的车位是不是连续好几个(比如一辆大巴车占了三个车位)。
这种方法的优势非常明显:分配和释放的速度极快(时间复杂度几乎是O(1)),完全避免了外部碎片(因为块大小固定,虽然可能有内部碎片浪费),而且实现简单,自己就能写,完全可控。接下来,我就手把手带你,在STM32CubeMX生成的项目框架下,从零搭建一套分块内存管理系统,并把它用起来。
2. 分块内存管理的“五脏六腑”:原理深度拆解
上一节我们打了停车场的比方,现在我们来深入看看这个“停车场”和“管理员账本”具体是怎么运作的。理解了原理,写代码就是水到渠成的事。
2.1 核心组件:内存池与内存管理表
想象一下,你有一大块空地(比如外部SRAM的128KB),这就是你的内存池。为了方便管理,你决定把它划分成许多个大小相同的车位,比如每个车位1KB。那么128KB就能划出128个车位。这个“1KB”就是内存块大小,你可以根据你项目里最常申请的内存大小来定,比如512字节、1KB、2KB都行。块越小,内部碎片浪费越少,但管理表会变大;块越大,管理越简单,但可能浪费更多。这需要你根据实际情况权衡。
光有停车场还不行,你得有个管理员和它的账本。这个账本就是内存管理表。它是一个数组,数组里的元素数量,正好等于内存池里块的数量(我们这个例子就是128)。数组里的每一个元素,就对应一个内存块的状态。
那么,这个状态怎么表示呢?这里的设计非常巧妙:
- 如果这个元素的值为0:恭喜,它对应的那个内存块是空的,随时可以停车(分配)。
- 如果这个元素的值是一个大于0的数,比如5:这就有意思了。它不仅仅表示自己对应的这个块被占了,还表示从它自己开始,连续有5个内存块都被同一个“车主”(某个指针)占用了。
这就完美解决了“大巴车占多个车位”的问题。管理员不需要为连续占用的每个车位都记一条复杂的记录,只需要在第一个车位的账目上写个“5”,后面4个车位甚至可以不特殊记录(或者也标记为已占用,但核心信息在第一个)。释放的时候,只要找到第一个车位,看到数字5,就知道要把连续5个车位都清空。
2.2 分配与释放的“行动指南”
原理清楚了,我们看看管理员(我们的代码)具体怎么干活。
分配内存(mymalloc):
假设我们的指针p来申请一块能停“大巴车”的内存,需要连续3个块(m=3)。
- 管理员拿着账本(内存管理表),从最后一项开始往前翻(从顶到底分配,这个策略有助于减少碎片)。为什么从后往前?这就像停车场从里往外停,先把里面的车位用上,门口的车位留出来方便调度。
- 挨个检查账目上的数字。目标是找到连续3个值都为0的条目。
- 找到了!比如找到第120、121、122项都是0。
- 管理员在这3项的账目上都写上数字3(标记为被占用,且连续长度为3)。注意,有些实现只在第一项(第120项)写3,其他两项写一个特殊值(如0xFFFF)表示已被占用,但属于“连续块的一部分”,这样释放时逻辑更清晰。我们采用更常见的“都写3”的方式。
- 管理员计算出第122号车位(最后一个被占用的块)的实际地址,把这个地址交给指针p。为什么返回最后一个块的地址? 这是为了分配算法实现的方便。因为我们是倒着找的,找到的连续空块的“尾部”就是第122块,返回它的地址,再加上块大小的偏移,就能定位到这片连续内存的起始地址用于实际使用。当然,你也可以设计成返回起始地址,算法上稍作调整即可。
释放内存(myfree):
过了一阵,p用完了内存,来还车位了。
- p告诉管理员:“我要还车,这是我的停车卡(内存地址)”。
- 管理员一看地址,就能算出这个地址对应的是账本上的第几号条目(通过
(目标地址 - 内存池基地址) / 块大小得到偏移索引)。 - 管理员翻到那个条目,比如是第122项,一看上面的数字是3。他立刻就明白了:“哦,你这是从第120项开始,连续占了3个车位”。
- 于是,管理员把第120、121、122项账目上的数字全部擦掉,改成0。这3个车位又重新可用了。
你看,整个过程没有复杂的查找和合并算法,就是简单的算术和数组操作,速度非常快,而且完全可预测。这就是它适合嵌入式实时系统的原因。
3. 实战准备:硬件连接与CubeMX工程配置
理论说得再多,不如动手做一遍。我们用一个具体的硬件场景来演示:STM32F103ZE核心板,板载64KB内部SRAM,我们通过FSMC总线外扩了一片128KB的SRAM芯片(比如常用的IS62WV12816)。我们要实现的目标是,对内部SRAM和外部SRAM分别建立两个独立的内存池,都可以用我们自己的分块内存管理来分配。
3.1 硬件清单与电路要点
你需要准备以下东西(或者在你的开发板上找到对应资源):
- STM32F103ZET6开发板:这是我们的主控。
- 外部SRAM模块:IS62WV12816,128K x 16位,通过FSMC接口连接。这是我们扩大“停车场”的关键。
- TFT LCD屏幕:用于直观显示内存使用状态、分配到的地址、写入的数据等。通过FSMC的Bank1 NOR/PSRAM 4连接。
- 几个按键:我们用来模拟内存的申请、释放、写入数据、切换内存池等操作。比如:
- KEY_UP: 切换当前操作的内存池(内部SRAM / 外部SRAM)
- KEY_DOWN: 向已分配的内存中写入测试数据
- KEY_LEFT: 释放当前分配的内存
- KEY_RIGHT: 申请一块新内存
- 一个LED(比如PC0连接的D1):让它闪烁,告诉我们程序在跑。
- 串口1(USART1):作为另一个信息输出窗口,方便调试。
这些硬件的电路连接,在大多数STM32F103的开发板教程里都是标准配置。关键是FSMC的接线:数据线(FSMC_D[0:15])、地址线(FSMC_A[0:18])、控制线(FSMC_NE4、NOE、NWE、NBL0/1)要正确连接到外部SRAM和LCD的对应引脚。如果用的是现成的开发板,这部分通常已经做好了。
3.2 在STM32CubeMX中“画”出系统蓝图
打开STM32CubeMX,新建工程,选择你的芯片型号(STM32F103ZETx)。
第一步:时钟树配置 在RCC设置里,将高速外部时钟(HSE)选择为“Crystal/Ceramic Resonator”。然后进入Clock Configuration标签页,把系统时钟(SYSCLK)通过PLL倍频到72MHz。这是F1系列的一个经典主频,性能足够。
第二步:关键外设引脚配置
-
FSMC(Flexible Static Memory Controller):这是驱动外部SRAM和TFT LCD的核心。
- 在
Connectivity下找到FSMC。 - 启用
FSMC,并选择NOR/PSRAM 4(因为我们的LCD接在Bank1的NE4上)。 - 在
FSMC的配置界面,我们需要配置两个“设备”,因为SRAM和LCD共用FSMC总线但时序不同。 - 先配SRAM:
Memory type选PSRAM,Data选16 bits。时序参数可以先按保守的设,比如Address setup time=2,Data setup time=5。后面在代码里可以细调。 - 再配LCD(以ILI9341为例):需要在
User-defined中添加一个新设备。Memory type选LCD Interface。时序参数同样先设一个大概值。 - CubeMX会自动帮你分配好FSMC相关的所有GPIO(数据线、地址线、控制线),检查一下是否和你板子的实际连接一致。
- 在
-
GPIO:
- 给LED(PC0)配置为
GPIO_Output,默认输出高电平(如果LED是低电平点亮,则输出低)。 - 给几个按键(PA0, PE3, PE4等)配置为
GPIO_Input,并根据硬件电路设置上拉或下拉。比如按键一端接地,另一端接MCU引脚,则MCU引脚应配置为Pull-up(上拉),这样按键未按下时读到的就是高电平。
- 给LED(PC0)配置为
-
USART1:
- 在
Connectivity下找到USART1,模式选Asynchronous。 - 波特率设为115200,数据位8,停止位1,无校验。
- 引脚会自动分配到PA9(TX)和PA10(RX)。
- 在
第三步:工程生成设置
转到Project Manager标签页:
- 给你的工程起个名字,比如
STM32_Memory_Management。 - 选择好工程路径(切记不要有中文路径!)。
Toolchain / IDE选择MDK-ARM V5。- 关键一步:在
Code Generator部分,务必勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per IP”。这个选项太有用了!它会把每个外设(如FSMC、USART1)的初始化代码单独放在一对.c/.h文件里,而不是全部堆在main.c。这样代码结构清晰,你后续添加自己的驱动文件时不会和生成的代码混在一起,维护起来方便得多。
最后,点击右上角的GENERATE CODE,让CubeMX为你生成完整的初始化代码框架。用MDK-ARM(Keil)打开生成的工程。
4. 编写驱动:构建内存管理的“基础设施”
CubeMX帮我们搭好了舞台,现在该我们自己的代码上场了。我们需要创建几个关键的驱动文件。
4.1 底层硬件驱动:SRAM与LCD
首先,把外部SRAM和TFTLCD的底层读写驱动准备好。这些驱动函数通常不复杂,但要求对FSMC时序和器件手册有了解。
-
sram.c / sram.h:外部SRAM驱动。- 核心函数就是一个:
FSMC_SRAM_WriteBuffer和FSMC_SRAM_ReadBuffer。实际上,因为FSMC将外部SRAM映射到了STM32的固定地址段(比如0x68000000),我们可以直接像操作内部数组一样操作这个地址。但为了规范,我们封装成函数。 - 关键点:FSMC时序调试。如果读写外部SRAM数据不对,首先要检查FSMC的时序配置(
FSMC_BCR和FSMC_BTR寄存器设置)是否与你的SRAM芯片手册要求匹配。在CubeMX生成的fsmc.c中调整FSMC_NORSRAM_TimingTypeDef和FSMC_NORSRAM_InitTypeDef结构体的参数。
- 核心函数就是一个:
-
tftlcd.c / tftlcd.h:TFT LCD驱动。- 同样,LCD也被映射到另一个固定地址(比如0x6C000000)。驱动主要包含初始化序列发送、设置显示窗口、写GRAM数据等函数。
- 我们需要用到显示字符串、画矩形、显示数字等上层函数,这些在之前的LCD例程中都有,可以直接移植过来。
-
key.c / key.h:按键扫描驱动。- 实现一个简单的按键扫描函数
KEY_Scan,支持单击检测和连按检测(本例中简单处理即可)。它需要读取对应GPIO引脚的电平,并进行消抖处理。
- 实现一个简单的按键扫描函数
把这些文件添加到你的MDK工程中,并确保包含正确的头文件路径。
4.2 灵魂所在:分块内存管理算法实现
现在,我们来写最核心的部分——内存管理驱动malloc.c和malloc.h。这里我会把关键代码和设计思路都讲清楚。
首先,在malloc.h里定义一些宏和结构体:
#ifndef __MALLOC_H
#define __MALLOC_H
#include "main.h"
// 定义两个内存池
#define SRAM_IN 0 // 内部内存池
#define SRAM_EX 1 // 外部内存池
#define SRAM_BANK_NUM 2 // 内存池总数
// 每个内存池的属性结构体
typedef struct {
uint8_t *membase; // 内存池的基地址(起始地址)
uint32_t *memmap; // 内存管理表指针
uint32_t memsize; // 内存池总大小(字节)
uint32_t blocksize; // 每个内存块的大小(字节)
uint32_t blocknum; // 内存块的总数量
uint32_t freeblocks; // 剩余空闲内存块数量
} _malloc_cortol;
// 声明全局内存管理控制器
extern _malloc_cortol malloc_cortol[SRAM_BANK_NUM];
// 用户函数声明
void my_mem_init(uint8_t memx); // 初始化内存池
uint32_t my_mem_malloc(uint8_t memx, uint32_t size); // 核心分配函数(返回偏移地址)
uint8_t my_mem_free(uint8_t memx, uint32_t offset); // 核心释放函数
uint8_t my_mem_perused(uint8_t memx); // 获取内存使用率
void my_mem_cpy(void *des, void *src, uint32_t n); // 内存拷贝(用于realloc)
void *mymalloc(uint8_t memx, uint32_t size); // 用户调用:分配内存
void myfree(uint8_t memx, void *paddr); // 用户调用:释放内存
void *myrealloc(uint8_t memx, void *paddr, uint32_t size); // 用户调用:重新分配
#endif
接下来是malloc.c的实现,我们聚焦最核心的两个函数:
// 假设我们已经定义好了内部和外部SRAM的基地址
#define SRAM_IN_BASE (0x20000000) // 内部SRAM起始地址(需根据实际调整)
#define SRAM_EX_BASE (0x68000000) // 外部SRAM映射起始地址
// 定义内存池参数
#define MEM_BLOCK_SIZE 1024 // 内存块大小为1KB
#define MEM_IN_SIZE (64 * 1024) // 内部SRAM 64KB
#define MEM_EX_SIZE (128 * 1024) // 外部SRAM 128KB
// 计算每个内存池需要的管理表大小(项数)
#define MEM_IN_BLOCK_NUM (MEM_IN_SIZE / MEM_BLOCK_SIZE)
#define MEM_EX_BLOCK_NUM (MEM_EX_SIZE / MEM_BLOCK_SIZE)
// 管理表本身也需要占用内存!我们把它放在内部SRAM中。
uint32_t mem_in_map[MEM_IN_BLOCK_NUM]; // 内部SRAM管理表
uint32_t mem_ex_map[MEM_EX_BLOCK_NUM]; // 外部SRAM管理表
// 全局内存控制器数组初始化
_malloc_cortol malloc_cortol[SRAM_BANK_NUM] = {
{ (uint8_t*)SRAM_IN_BASE, mem_in_map, MEM_IN_SIZE, MEM_BLOCK_SIZE, MEM_IN_BLOCK_NUM, MEM_IN_BLOCK_NUM }, // SRAM_IN
{ (uint8_t*)SRAM_EX_BASE, mem_ex_map, MEM_EX_SIZE, MEM_BLOCK_SIZE, MEM_EX_BLOCK_NUM, MEM_EX_BLOCK_NUM }, // SRAM_EX
};
// 内存池初始化:将管理表全部清零
void my_mem_init(uint8_t memx) {
_malloc_cortol *ctrl = &malloc_cortol[memx];
memset(ctrl->memmap, 0, ctrl->blocknum * sizeof(uint32_t)); // 清零管理表
ctrl->freeblocks = ctrl->blocknum; // 空闲块数等于总块数
}
// 核心分配函数:从指定内存池分配size字节的内存
// 成功返回分配内存相对于内存池基地址的偏移量,失败返回0xFFFFFFFF
uint32_t my_mem_malloc(uint8_t memx, uint32_t size) {
_malloc_cortol *ctrl = &malloc_cortol[memx];
uint32_t blocks_need; // 需要多少块
uint32_t offset = 0; // 偏移量
uint32_t i, j;
if (size == 0) return 0xFFFFFFFF; // 申请大小不能为0
// 计算需要的连续内存块数(向上取整)
blocks_need = (size + ctrl->blocksize - 1) / ctrl->blocksize;
if (blocks_need > ctrl->freeblocks) {
return 0xFFFFFFFF; // 剩余块数不足
}
// 从管理表末尾开始,向前搜索连续的空闲块
for (i = ctrl->blocknum - 1; i >= blocks_need - 1; i--) {
if (ctrl->memmap[i] == 0) { // 找到一个空闲块
// 检查它前面(blocks_need-1)个块是否也都空闲
for (j = 0; j < blocks_need; j++) {
if (ctrl->memmap[i - j] != 0) {
break; // 中间有被占用的块,不连续
}
}
if (j == blocks_need) { // 找到了连续的blocks_need个空闲块!
// 标记这些块为已占用
for (j = 0; j < blocks_need; j++) {
ctrl->memmap[i - j] = blocks_need; // 都标记为需要的块数
}
ctrl->freeblocks -= blocks_need; // 更新空闲块计数
// 计算偏移量:最后一个被占用块的地址偏移
offset = (i - (blocks_need - 1)) * ctrl->blocksize;
return offset;
}
}
}
return 0xFFFFFFFF; // 找不到连续的块
}
my_mem_malloc 函数是整个分配过程的核心。它严格按照我们之前讲的“从后往前找连续空块”的逻辑。这里返回的 offset 是第一个被占用块相对于内存池基地址的字节偏移量。这是更常见的做法,因为计算起始地址更直观。
对应的释放函数 my_mem_free:
// 核心释放函数:释放从指定偏移量开始的内存
uint8_t my_mem_free(uint8_t memx, uint32_t offset) {
_malloc_cortol *ctrl = &malloc_cortol[memx];
uint32_t block_index; // 内存块索引
uint32_t blocks_occupied; // 被连续占用的块数
uint32_t i;
// 检查偏移量是否合法
if (offset >= ctrl->memsize) {
return 1; // 错误:偏移超出范围
}
block_index = offset / ctrl->blocksize; // 计算是第几块
// 检查该块是否确实被分配了
blocks_occupied = ctrl->memmap[block_index];
if (blocks_occupied == 0) {
return 2; // 错误:该块未被分配,重复释放?
}
// 释放连续的内存块
for (i = 0; i < blocks_occupied; i++) {
ctrl->memmap[block_index + i] = 0; // 清零管理表项
}
ctrl->freeblocks += blocks_occupied; // 更新空闲块计数
return 0; // 释放成功
}
最后,我们封装用户友好的接口函数:
// 用户调用:分配内存,返回内存指针
void *mymalloc(u8 memx, u32 size) {
u32 offset;
offset = my_mem_malloc(memx, size);
if(offset == 0xFFFFFFFF) {
return NULL;
} else {
// 将偏移量转换为实际的内存地址
return (void*)((u32)malloc_cortol[memx].membase + offset);
}
}
// 用户调用:释放内存
void myfree(u8 memx, void *paddr) {
u32 offset;
if(paddr == NULL) return; // 空指针直接返回
// 计算指针地址相对于内存池基地址的偏移量
offset = (u32)paddr - (u32)malloc_cortol[memx].membase;
my_mem_free(memx, offset); // 调用核心释放函数
}
这样,一套完整的分块内存管理驱动就完成了。它提供了与标准C库 malloc/free 类似的接口,但内部机制完全不同,更简单、更快速、更确定。
5. 在main函数中集成与测试:让系统跑起来
驱动都准备好了,现在我们在main.c里把它们用起来,做一个直观的测试程序。
在main.c的/* USER CODE BEGIN 2 */和/* USER CODE END 2 */之间,添加我们的硬件初始化和界面绘制代码:
/* USER CODE BEGIN 2 */
// 初始化硬件
TFTLCD_Init(); // 初始化LCD
FSMC_SRAM_Init(); // 初始化外部SRAM接口(FSMC时序配置)
// 初始化两个内存池
my_mem_init(SRAM_IN); // 初始化内部SRAM内存池
my_mem_init(SRAM_EX); // 初始化外部SRAM内存池
// 在LCD上绘制一个简单的UI界面
FRONT_COLOR = BROWN;
LCD_DrawRectangle(25, 25, 215, 135); // 画一个框
FRONT_COLOR = RED;
LCD_ShowString(30, 30, 200, 16, 16, "STM32 Memory Manager");
LCD_ShowString(30, 50, 200, 16, 16, "Block Memory Demo");
FRONT_COLOR = BLACK;
LCD_ShowString(30, 90, 200, 16, 16, "KEY_R: Malloc");
LCD_ShowString(30, 110, 200, 16, 16, "KEY_L: Free");
LCD_ShowString(30, 130, 200, 16, 16, "KEY_U: Toggle SRAM");
LCD_ShowString(30, 150, 200, 16, 16, "KEY_D: Write Data");
FRONT_COLOR = BLUE;
LCD_ShowString(30, 170, 200, 16, 16, "Current Pool: SRAMIN");
LCD_ShowString(30, 190, 200, 16, 16, "SRAMIN Used: %");
LCD_ShowString(30, 210, 200, 16, 16, "SRAMEX Used: %");
LCD_ShowString(30, 230, 200, 16, 16, "Alloc Addr: 0x--------");
LCD_ShowString(30, 250, 200, 16, 16, "Data: (none)");
/* USER CODE END 2 */
接下来,在while (1)主循环中,我们实现按键交互逻辑:
uint8_t key, i = 0;
uint8_t *p = NULL; // 指向当前分配的内存
uint8_t *last_p = NULL; // 记录上一次的指针,用于判断是否变化
uint8_t sramx = SRAM_IN; // 当前操作的内存池,默认内部SRAM
char disp_buf[50]; // 显示缓冲区
while (1) {
key = KEY_Scan(0); // 扫描按键,0为不支持连按
switch(key) {
case KEY_RIGHT_PRES: // 申请内存
// 尝试分配2KB内存
p = mymalloc(sramx, 2048);
if(p != NULL) {
// 分配成功,在分配的内存中写入一些数据
sprintf((char*)p, "Alloc Test #%03d", i);
i++;
} else {
// 分配失败,可以在LCD上显示错误信息
// 或者通过串口打印
printf("Malloc failed! Not enough continuous blocks.\r\n");
}
break;
case KEY_DOWN_PRES: // 写入/更新数据
if(p != NULL) {
// 向已分配的内存写入新数据
sprintf((char*)p, "Updated Data #%03d", i);
i++;
// 在LCD上显示写入的数据
LCD_ShowString(30, 250, 200, 16, 16, (char*)p);
}
break;
case KEY_LEFT_PRES: // 释放内存
myfree(sramx, p);
p = NULL; // 指针置空,防止误用
LCD_Fill(30, 250, 239, 266, WHITE); // 清空数据显示区域
break;
case KEY_UP_PRES: // 切换内存池
sramx = !sramx; // 在SRAM_IN(0)和SRAM_EX(1)之间切换
if(sramx == SRAM_EX) {
LCD_ShowString(30, 170, 200, 16, 16, "Current Pool: SRAMEX");
} else {
LCD_ShowString(30, 170, 200, 16, 16, "Current Pool: SRAMIN");
}
// 切换内存池时,最好也释放当前池的内存(如果已分配)
if(p != NULL) {
myfree(!sramx, p); // 释放之前内存池的内存
p = NULL;
LCD_Fill(30, 250, 239, 266, WHITE);
}
break;
}
// 如果分配的内存地址发生了变化,更新LCD上的地址显示
if(last_p != p) {
last_p = p;
if(p) {
sprintf(disp_buf, "Alloc Addr: 0x%08X", (uint32_t)p);
} else {
sprintf(disp_buf, "Alloc Addr: (freed)");
}
LCD_ShowString(30, 230, 200, 16, 16, disp_buf);
}
// 每隔一段时间(如500ms)更新内存使用率
static uint32_t tick = 0;
if(HAL_GetTick() - tick > 500) {
tick = HAL_GetTick();
// 显示内部SRAM使用率
sprintf(disp_buf, "%3d", my_mem_perused(SRAM_IN));
LCD_ShowString(30+112, 190, 30, 16, 16, disp_buf); // 覆盖显示百分比数字
// 显示外部SRAM使用率
sprintf(disp_buf, "%3d", my_mem_perused(SRAM_EX));
LCD_ShowString(30+112, 210, 30, 16, 16, disp_buf);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // LED闪烁,指示系统运行
}
HAL_Delay(10); // 简单的延时
}
这段代码实现了一个完整的交互测试:
- KEY_RIGHT:按下后,调用
mymalloc从当前选中的内存池(内部或外部SRAM)申请2KB内存。如果成功,会立即向这块内存写入一个带编号的字符串。 - KEY_DOWN:如果内存已分配,就更新这块内存里的字符串内容,并在LCD上显示出来。这模拟了实际使用中向动态内存写入数据的操作。
- KEY_LEFT:释放当前分配的内存,并将指针置为NULL。LCD上对应的数据展示区会被清空。
- KEY_UP:在内部SRAM和外部SRAM两个内存池之间切换。切换时,如果之前的内存池有分配内存,会先将其释放,避免内存泄漏。
- 实时显示:LCD上会动态显示当前操作的是哪个内存池、两个内存池的实时使用率(百分比)、最近一次成功分配的内存地址(以十六进制显示)、以及内存中存储的数据内容。
- LED闪烁:通过LED的周期性闪烁,你可以直观地确认程序正在正常运行,没有卡死。
编译、下载到开发板。上电后,你应该能看到LCD上显示出界面,LED开始闪烁。尝试按不同的按键,观察LCD上地址、数据和使用率的变化。特别是反复进行“分配->释放”操作,观察使用率是否能正确回到初始值。你也可以尝试申请一个非常大的内存(比如超过总内存大小),看看是否会返回NULL,并在串口助手中看到错误提示。
6. 进阶思考与项目实战优化建议
通过上面的实战,你已经成功实现了一个基础的分块内存管理器。但在真实的项目中,我们还需要考虑更多。这里分享几个我踩过坑后总结的优化方向。
第一,块大小的选择是个艺术。 我们例子中固定用1KB。如果你的项目里频繁申请几十字节的小变量,用1KB的块就太浪费了,内部碎片严重。怎么办?一个常见的策略是多内存池。你可以创建多个内存池,每个池的块大小不同。比如:
- 池A:块大小32字节,用于分配小结构体、短字符串。
- 池B:块大小256字节,用于分配中等缓冲区。
- 池C:块大小1024字节,用于分配较大的数据块。 在分配时,根据请求的大小,自动选择最合适(浪费最少)的池子。这需要更复杂的管理器,但能极大提升内存利用率。
第二,加入内存越界与泄漏检测。 我们现在的实现非常“裸奔”。如果用户写数据时超出了分配的内存块边界,就会破坏相邻块的管理信息,导致系统崩溃。可以在每个内存块的头部和尾部加入“魔术数字”(Magic Number)或校验和。在分配时写入特定值,在释放或定期检查时验证这些值是否被篡改。一旦发现篡改,立即通过串口输出错误地址,帮助你快速定位是哪段代码写飞了。
第三,myrealloc的优化实现。 我们提供了myrealloc的函数原型,它的基本思路是:分配一块新的、更大的内存,把旧数据拷贝过去,然后释放旧内存。但在分块管理下,如果旧内存块后面紧接着的空闲块足够大,我们可以尝试一种更高效的“原地扩容”:直接标记后面的空闲块为已占用,并更新管理表。这避免了数据拷贝的开销,但实现逻辑会更复杂,需要仔细处理边界情况。
第四,考虑多任务环境。 如果你的项目跑在了RTOS(如FreeRTOS)上,多个任务可能同时调用mymalloc和myfree。这就涉及到线程安全问题。最简单的办法是在这些函数内部加上互斥锁(mutex),确保同一时间只有一个任务在操作内存管理表。虽然会引入一点开销,但保证了数据一致性。
第五,监控与统计。 除了显示使用率,还可以记录更多信息,比如:总分配次数、总释放次数、当前最大连续空闲块大小、历史最大内存使用量等。这些数据对于分析项目运行期的内存行为、评估块大小设置是否合理、预判内存瓶颈非常有价值。
嵌入式内存管理没有银弹,分块式管理是在确定性、性能和实现复杂度之间一个非常好的折中。它特别适合那些内存需求模式相对固定、对实时性要求高的场景。希望这个从原理到代码的完整梳理,能帮你建立起自己项目内存管理的“安全感”。下次当你的STM32项目需要动态内存时,别再犹豫是否用malloc了,试试自己动手实现一个分块管理器,你会发现它既没有想象中那么难,又能给你的系统带来实实在在的稳健性提升。
216



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



