STM32CubeMX实战:分块内存管理在嵌入式系统中的应用

FreeRTOS(STM32CubeMX) FreeRTOS(Real-Time Operating System)是一款开源的实时操作系统内核,专为嵌入式系统和微控制器设计。FreeRTOS提供了一套功能丰富的实时调度和任务管理机制,可用于在嵌入式系统中实现多任务处理。FreeRTOS的主要目标是为嵌入式应用程序提供一个轻量级、高效和可移植的实时操作系统。它被广泛应用于各种嵌入式设备和应用领域,如工业自动化、物联网设备、消费电子、汽车控制系统、医疗设备等。 阅读详情

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)。

  1. 管理员拿着账本(内存管理表),从最后一项开始往前翻(从顶到底分配,这个策略有助于减少碎片)。为什么从后往前?这就像停车场从里往外停,先把里面的车位用上,门口的车位留出来方便调度。
  2. 挨个检查账目上的数字。目标是找到连续3个值都为0的条目。
  3. 找到了!比如找到第120、121、122项都是0。
  4. 管理员在这3项的账目上都写上数字3(标记为被占用,且连续长度为3)。注意,有些实现只在第一项(第120项)写3,其他两项写一个特殊值(如0xFFFF)表示已被占用,但属于“连续块的一部分”,这样释放时逻辑更清晰。我们采用更常见的“都写3”的方式。
  5. 管理员计算出第122号车位(最后一个被占用的块)的实际地址,把这个地址交给指针p。为什么返回最后一个块的地址? 这是为了分配算法实现的方便。因为我们是倒着找的,找到的连续空块的“尾部”就是第122块,返回它的地址,再加上块大小的偏移,就能定位到这片连续内存的起始地址用于实际使用。当然,你也可以设计成返回起始地址,算法上稍作调整即可。

释放内存(myfree): 过了一阵,p用完了内存,来还车位了。

  1. p告诉管理员:“我要还车,这是我的停车卡(内存地址)”。
  2. 管理员一看地址,就能算出这个地址对应的是账本上的第几号条目(通过 (目标地址 - 内存池基地址) / 块大小 得到偏移索引)。
  3. 管理员翻到那个条目,比如是第122项,一看上面的数字是3。他立刻就明白了:“哦,你这是从第120项开始,连续占了3个车位”。
  4. 于是,管理员把第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(上拉),这样按键未按下时读到的就是高电平。
  • 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); // 简单的延时
}

这段代码实现了一个完整的交互测试:

  1. KEY_RIGHT:按下后,调用mymalloc从当前选中的内存池(内部或外部SRAM)申请2KB内存。如果成功,会立即向这块内存写入一个带编号的字符串。
  2. KEY_DOWN:如果内存已分配,就更新这块内存里的字符串内容,并在LCD上显示出来。这模拟了实际使用中向动态内存写入数据的操作。
  3. KEY_LEFT:释放当前分配的内存,并将指针置为NULL。LCD上对应的数据展示区会被清空。
  4. KEY_UP:在内部SRAM和外部SRAM两个内存池之间切换。切换时,如果之前的内存池有分配内存,会先将其释放,避免内存泄漏。
  5. 实时显示:LCD上会动态显示当前操作的是哪个内存池、两个内存池的实时使用率(百分比)、最近一次成功分配的内存地址(以十六进制显示)、以及内存中存储的数据内容。
  6. 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了,试试自己动手实现一个分块管理器,你会发现它既没有想象中那么难,又能给你的系统带来实实在在的稳健性提升。

STM32H7下CubeMX配置FreeRTOS的完整指南 深入讲解STM32H7环境下如何使用CubeMX配置FreeRTOS,涵盖初始化设置、任务创建与调度等关键步骤,帮助开发者高效实现多任务处理,提升系统实时性与稳定性。 阅读详情

相关推荐

Linux Swap 子系统:架构、内存管理与虚拟化 Swap

Linux Swap机制通过将不活跃内存页移至磁盘或压缩内存,扩展系统有效内存容量,缓解内存压力。传统架构中,逻辑Swap槽与物理存储绑定,导致资源浪费,尤其在使用Zswap等压缩技术时。为此,Vswap与Xswap提出虚拟化方案:前者以XArray实现灵活映射,支持多后端迁移;后者采用稀疏映射,简化管理但容量固定。二者在架构设计、资源记账与动态扩展上各有权衡,反映了内核开发中对性能、可维护性与灵活性的深层考量。

mounter625的专栏 216

STM32CubeMX配置FreeRTOS时所有参数详解

STM32CubeMX 中配置 FreeRTOS 时各参数的详细含义与作用

2303_76316997的博客 1988

uCOSII在STM32上的移植与应用详解

uCOSII(MicroC/OS-II)是一个为嵌入式应用设计的实时操作系统(RTOS),由Jean J. Labrosse创立。该系统因其高度可裁剪性、可预测性和免费的源代码而广受工程师们的欢迎。uCOSII能够支持多任务,并且能够为任务调度、时间管理和同步提供支持。它为开发者提供了一种简便的方式,以管理复杂且要求实时响应的应用程序。STM32微控制器系列是由STMicroelectronics(意法半导体)开发的高性能ARM Cortex-M系列处理器。

weixin_42612804的博客 848

STM32CubeMX开发之路—8Flash读写

STM32CubeMX(Keil5)开发之路——8Flash读写 运行环境 Windows10 STM32CubeMX___Version 5.0.0 Keil5(MDK5)___Version 5.15 简介 本例程主要讲解如何对芯片内的Flash进行读写 STM32CubeMx基本配置 基础配置过程请参考 STM32CubeMx(Keil5)开发之路—配置第一个项目 printf重定...

iot小胡的博客 1万+

STM32CubeMX配置串口与DMA数据采集实战实验

STM32CubeMX是ST官方推出的图形化配置工具,集芯片选型、外设配置、时钟树规划与代码生成于一体。其核心价值在于通过可视化界面降低寄存器配置复杂度,自动生成符合HAL库规范的初始化代码,显著提升开发效率并减少人为错误。波特率(Baud Rate)是指每秒传输的符号数,在UART通信中,由于每个符号代表一位(bit),因此波特率等于比特率(bit/s)。它直接决定了通信速度,也影响着系统的实时性和稳定性。

weixin_36427956的博客 922

新手DIY无人机,从电机到飞控,这份配件清单和避坑指南请收好

本文为无人机DIY新手提供从电机到飞控的完整配件清单和避坑指南。详细解析电机KV值选择、电调匹配、飞控系统调试等核心问题,帮助初学者避开常见雷区,顺利完成首飞。特别推荐FPV竞速机和航拍机的配件组合方案,并附赠实用调试命令和故障排查表。

weixin_27032989的博客 232

STM32CUBEMX+FREERTOS使用软件定时器osTimerStart

一、配置STM32CUBEMX 1、开启FreeRTOS 新建一个 CubeMX 工程,在配置好时钟后,点击 Middleware -> 选择 FreeRTOS -> 下拉框选择 V1 版本 CMSIS 2、配置界面 配置项 功能 Tasks and Queues 任务与队列,用于配置任务体以及消息队列; Timers and Semaphores 软件定时器与信号量,用于配置内核对象 (软件定时器和信号量) Mutexes 互斥量,用于配置内核对象(互斥量) Eve

yongridada的博客 3937

【开源训练数据集2】10个最适合机器学习的股票市场数据集

​随着加密货币在世界各地的兴起,人们现在有比以往更多的投资方式。然而,这并不像低买高卖那么简单。如果你能准确预测股市,你就会成为地球上最富有的人之一。因此,之前已经有关于如何利用情绪分析来预测股市的研究。对于那些希望构建类似预测模型的人,本文将介绍 10 个用于机器学习的股票市场和加密货币数据集。

u010542847的博客 2138

Chord在嵌入式系统中的应用:STM32CubeMX配置指南

本文介绍了如何在星图GPU平台上自动化部署Chord视频时空理解工具镜像,赋能边缘智能分析。通过平台化部署,用户可快速构建轻量级视频理解环境,典型应用于嵌入式设备中的实时运动检测、行为识别与视频流时空特征提取,显著降低AI落地门槛。

weixin_33308579的博客 274

手把手教你用Keil5和Proteus8给51单片机跑个流水灯(附完整源码+安装包)

本文详细介绍了如何使用Keil5和Proteus8为51单片机实现流水灯效果,包括开发环境配置、汇编代码编写、电路设计及仿真调试。通过完整的源码和安装包,帮助初学者快速掌握C51单片机开发的核心流程,适合嵌入式开发入门学习。

weixin_42632434的博客 369

A2B音频总线在智能座舱中的应用

车载A2B链路音频是智能座舱的核心功能,涵盖车载音响、语音识别、e-Call、消噪及回声消除等应用,随着汽车智能网联化的发展,对音频的开发要求也越来越高,需要加入更先进、功能丰富的信息娱乐功能以满足消费者的用户体验。但传统的模拟并行音频信号传输方式,难以在功能增加与整车轻量化(线缆的重量及成本减少)之间取得均衡。这时候就需要A2B音频总线了。A2B音频总线用于连接车辆中的音频设备,如麦克风(MIC)、功放(AMP)、多媒体主机(Head Unit)和T-BOX等。通过一条低成本的非屏蔽双绞线UTP,进行跨距

怿星科技的博客 3933

STM32CubeMx配置FreeRTOS工程

本文介绍了使用STM32CubeMX配置FreeRTOS工程的注意事项和具体步骤。重点指出FreeRTOS时基源不要使用SysTick,建议切换至其他时钟源以保证时间准确性。文章对比了中断优先级和任务优先级的区别,并详细说明了FreeRTOS的特点,包括任务管理、内存管理等。配置步骤包括时基时钟源选择和FreeRTOS参数设置,通过图形化界面简化了实时操作系统的移植过程。该配置方法适用于STM32系列芯片,能有效提高系统实时性和可靠性。

宁静致远的博客 1547

手把手移植st7701s驱动:MT6763+安卓8.0屏幕适配保姆级教程

本文详细介绍了在MT6763平台上移植ST7701S驱动的完整流程,涵盖从LK层到Kernel层的适配方案,特别针对安卓8.0系统的特殊要求。通过MIPI转RGB屏幕适配的实战教程,帮助开发者解决常见显示问题,包括无显示、色彩异常和屏幕错位等,并提供高级调试技巧如彩条测试模式和闪屏解决方案。

weixin_29186249的博客 156

使用STM32CubeMX创建基于FreeRTOS的lwIP工程:项目推荐

使用STM32CubeMX创建基于FreeRTOS的lwIP工程:项目推荐 【下载地址】使用STM32CubeMX创建基于FreeRTOS的lwIP工程 本教程详细介绍了如何使用STM32CubeMX创建基于FreeRTOS的lwIP工程,帮助开发者快速实现STM32微控制器的网络通信功能。教程涵盖了工程创建、FreeR...

gitblog_06703的博客 915

深度学习实战35-图卷积神经网络GCN模型的搭建与应用

大家好,我是微学AI,今天给大家介绍一下深度学习实战35-图卷积神经网络GCN模型的搭建与应用,GCN是一种用于处理图结构数据的深度学习模型,它可以捕捉节点之间的拓扑关系和局部特征,并进行图级别的预测和分类任务。下面将介绍如何搭建并应用GCN模型。

微学AI的博客 2224

把 Claude Code 接进 ABAP 世界,vibing-steampunk 到底能做什么

《Vibing Steampunk:AI Agent 与 ABAP 开发的工程化桥梁》摘要 Vibing Steampunk(vsp)是一个连接 AI Agent 与 SAP ABAP 开发环境的开源工具,通过 Model Context Protocol(MCP)将 ABAP 开发能力标准化。它解决了传统 ABAP 开发中 AI 辅助工具的局限性,使 Claude Code 等 AI 编程助手能够直接访问 ADT API,实现代码读写、调试、测试等完整开发流程。该工具采用 Go 语言开发,提供 147 个

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1932

FreeRTOS在STM32上的高效内存管理:CubeMX配置与实战优化技巧

本文深入探讨了FreeRTOS在STM32平台上的高效内存管理策略,重点解析了如何通过STM32CubeMX进行关键配置与实战优化。内容涵盖FreeRTOS内存模型、堆栈分配、Heap管理方案选择,并提供了栈溢出检测、动态内存监控等高级调试技巧,旨在帮助开发者解决内存碎片、溢出等常见问题,构建更稳定、高效的嵌入式系统。

5f4d3s2a1q的博客 902

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

STM32CubeMX实战:FreeRTOS内存管理与任务堆栈的深度优化

本文深入探讨了在STM32CubeMX环境下使用FreeRTOS进行内存管理与任务堆栈优化的实战技巧。通过分析Heap_1到Heap_5五种内存分配方案的特点,结合STM32F103RCT6硬件平台,提供了任务堆栈计算、内存监控实现及高级优化策略,帮助开发者解决嵌入式系统中的资源管理难题。

weixin_29165465的博客 320

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

STM32Cubemx HAL库 移植FreeRTOS源码

Free和RTOS,Free 就是免费的、自由的、不受约束的意思,RTOS 全称是 Real Time Operating System,中文名就是实时操作系统。可以看出 FreeROTS 就是一个免费的 RTOS 类系统,FreeRTOS 是 RTOS 系统的一种,FreeRTOS 十分的小巧,可以在资源有限的微控制器中运行,当然了,FreeRTOS 不仅局限于在微控制器中使用。但从文件数量上来看 FreeRTOS 要比UCOSII 和 UCOSIII 小的多。

m0_51274562的博客 2141

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

STM32CubeMX学习笔记30---FreeRTOS内存管理

FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种内存分配算法(分配策略),但是上层接口(API)却是统一的。这样做可以增加系统的灵活性:用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不同的内存分配策略。

H2z1220的博客 1696

如何用STM32CubeMX配置FreeRTOS系统?

本文介绍如何在STM32上深度整合FreeRTOS,实现多任务调度、任务间通信、外设驱动与实时控制。涵盖CubeMX工程配置、时基源选择、任务堆栈分配、消息队列、信号量及低功耗优化等关键技术,提升嵌入式系统的实时性与稳定性。

android的专栏 1212

STM32CubeMX配置:嵌入式系统中的TranslateGemma集成

本文介绍了如何在星图GPU平台上自动化部署TranslateGemma: Matrix Engine镜像,实现嵌入式设备的离线多语言实时翻译功能。该方案通过STM32CubeMX配置硬件资源,将轻量化AI模型集成到微控制器,适用于智能翻译笔等需要本地化、低延迟翻译的嵌入式应用场景。

weixin_30789053的博客 406

STM32CubeMX配置黄山派外设完整流程

本文深入探讨基于STM32H7的黄山派开发板与STM32CubeMX工具的协同应用,涵盖GPIO、中断、时钟、UART、ADC等核心外设配置,并扩展至以太网、TFT显示、USB OTG和SDIO等高级功能,结合RTOS与低功耗优化,构建高性能嵌入式系统。

jump7的博客 999

【实战指南】STM32CubeMX+FreeRTOS任务调度与内存管理详解

本文详细解析了如何使用STM32CubeMX图形化工具配置FreeRTOS实时操作系统,涵盖任务调度、内存管理及多任务同步等核心内容。通过实战案例演示外设控制与系统优化,帮助开发者高效实现基于STM32和ARM Cortex-M的嵌入式应用开发。

palm99的专栏 667

STM32CubeMX使用:深度学习项目训练环境中的嵌入式开发

本文介绍了如何在星图GPU平台上自动化部署深度学习项目训练环境镜像,实现嵌入式AI应用的快速开发。该镜像集成了STM32CubeMX等工具链,支持将训练好的深度学习模型高效部署到STM32嵌入式设备,典型应用于图像分类、语音识别等边缘计算场景,显著提升开发效率。

weixin_35753431的博客 767
上一篇: 利用setEcoMode优化innovus中的ecoAddRepeater批量操作效率
下一篇: ESXI6.7.0安装全记录:戴尔R740服务器+USB3.0优盘实战教程
mango
博客等级 码龄27年 427粉丝 1710原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值