目录
[2:为什么不能 free](#2:为什么不能 free)
[三:heap_2 --- 支持 free,但不合并碎片](#三:heap_2 — 支持 free,但不合并碎片)
[1:跟 heap_1 的区别](#1:跟 heap_1 的区别)
[四:heap_3 --- 标准库 malloc 的套壳](#四:heap_3 — 标准库 malloc 的套壳)
[五:heap_4 --- 工业首选](#五:heap_4 — 工业首选)
[六:heap_5 --- heap_4 的多内存区版](#六:heap_5 — heap_4 的多内存区版)
[七:rtos\23\ 改动清单](#七:rtos\23\ 改动清单)
一:Freertos内存管理
1:我们采用的内存
FreeRTOS 不直接依赖 C 标准库 malloc/free,而是自己实现了一套简单的内存管理器。
对应源码:
FreeRTOS/Source/portable/MemMang/
heap_1.c
heap_2.c
heap_3.c
heap_4.c
heap_5.c
你配置:
FreeRTOSConfig.h
#define configUSE_HEAP_4
然后工程里面加入:
heap_4.c
即可。
2:先理解动态内存管理的核心
假设 FreeRTOS 有一块堆:
SRAM
0x20000000
|
v
+-------------------------+
| |
| Free Heap |
| |
+-------------------------+
创建任务:xTaskCreate(...)
heap分配“
+---------+
| TCB |
+---------+
| stack |
+---------+
| free |
+---------+
删除任务:vTaskDelete()
释放 TCB
释放 stack,因此内存管理需要有申请和释放才合理。
二:heap_1:最简单,只分配不释放
1:工作原理
整个堆就是一块连续内存 + 一个指针 free_ptr,每次申请就把指针往后移:
static uint8_t ucHeap[configTOTAL_HEAP_SIZE]; // 堆本体
static size_t xNextFreeByte; // ★ 就一个指针,指向下一个空闲位置
// pvPortMalloc(100):
void *ptr = &ucHeap[xNextFreeByte]; // 从当前位置切一块
xNextFreeByte += 100; // 指针往后移
return ptr;
ucHeap ────────────────────────────────────────→
│ Task1栈+TCB │ Task2栈+TCB │ Queue数据区 │ ... │ ← 空闲
│ │ │ │ │
│ 已分配的 │ 已分配的 │ 已分配的 │ │
↑
xNextFreeByte(用多少走多少)
2:为什么不能 free
没有记录谁申请的、多大、在哪 。xNextFreeByte 只知道往前走,不知道哪块可以回收。
3:优缺点
| 优点 | 缺点 |
|---|---|
| 分配极快(O(1),就移一次指针) | 不能释放------内存只进不出 |
| 无碎片(永远连续分配) | 不能动态创建删除任务/队列 |
| 代码极小 | 不适合需要动态生命周期的场景 |
4:适合什么场景
启动阶段:
创建 task1 → 申请
创建 task2 → 申请
创建全部分队列 → 申请
启动调度器
运行阶段:
永不创建新任务
永不删除任务
稳定跑几年无故障重启
→ 工业控制、传感器节点、不重启的嵌入式设备
一句话:heap_1 = 只分配不释放的线性分配器。适合永远不删任务的场景,不适合 FreeRTOS 通用场合。我们一直用的是 heap_4(支持 free + 碎片合并)。
三:heap_2 --- 支持 free,但不合并碎片
1:跟 heap_1 的区别
heap_2 多了空闲链表 ,释放的内存会被挂回链表供后续使用。但相邻的空闲块不合并。
heap_1: heap_2:
只分配不释放 分配 + 释放(有碎片)
O(1) O(1)
无碎片 有碎片!不合并!
2:碎片怎么产生的
① 申请 3 块:
┌─────┬─────┬─────┐
│ A │ B │ C │
│100B │200B │100B │
└─────┴─────┴─────┘
② 释放 B:
┌─────┬─────┬─────┐
│ A │FREE │ C │ ← 中间空了 200B
│100B │200B │100B │
└─────┴─────┴─────┘
③ 有人要 300B:
空闲链表里最大块是 200B(B 那块),不够 300B
虽然 A+B+C 总空闲 > 300B,但它们不连续
→ 分配失败!
④ 如果释放了 A 和 B:
┌─────┬─────┬─────┐
│FREE │FREE │ C │ ← A 和 B 相邻但不会合并!
│100B │200B │100B │ 还是两块独立的 100B + 200B
└─────┴─────┴─────┘
3:为什么不会合并
heap_2 的算法只挂接vPortFree时归还的块,不检查相邻块是否也是空闲。所以空闲链表里永远是最初分配出去的原始块大小。
优缺点
| 优点 | 缺点 |
|---|---|
| 支持释放(比 heap_1 进了一步) | 碎片越来越严重(运行越久内存越散) |
| 分配释放都是 O(1) | 大块可能申请失败(被小碎片卡死) |
| 实现简单 | 不适合频繁 malloc/free 的场景 |
4:适合什么场景
任务和队列大多在创建后不删除,偶尔有小块临时内存的申请释放。运行周期不长、碎片不会累积到致命程度的应用。
一句话:heap_2 = 能用 free,但内存像揉碎的面包------中间留下的空位原地不动,不合并,碎片越用越多。我们用的 heap_4 解决了这个问题。
四:heap_3 --- 标准库 malloc 的套壳
void *pvPortMalloc(size_t size) { return malloc(size); }
void vPortFree(void *pv) { free(pv); }
就是把 C 标准库的 malloc/free 包了一层,堆管理甩给 libc。
| 优点 | 缺点 |
|---|---|
| 代码少 | 速度不确定(libc 可能搜链表、合并碎片) |
| 不用维护 | STM32 裸机不一定有 malloc |
| 线程安全依赖 libc 实现 |
适合:有完整 C 运行库的平台(Linux 上的 FreeRTOS 模拟器)。
五:heap_4 --- 工业首选
= heap_2 的分配释放 + 相邻空闲块自动合并。
跟 heap_2 的区别就一点
释放 A 再释放 B(A 和 B 相邻):
heap_2: heap_4:
┌──────┬──────┬─────┐ ┌──────┬──────┬─────┐
│ FREE │ FREE │ C │ │ FREE │ FREE │ C │
│ 100B │ 200B │100B │ │ 100B │ 200B │100B │
└──────┴──────┴─────┘ └──────┴──────┴─────┘
两小空洞,300B 申请失败 ↓ 检测到相邻 → 合并
┌──────────┬─────┐
│ FREE │ C │
│ 300B │100B │
└──────────┴─────┘
300B 的申请能通过了!
六:heap_5 --- heap_4 的多内存区版
跟 heap_4 算法完全一样,只是能从多处不连续的 RAM 里取内存:
// F429 有三块 RAM:
// SRAM1 @ 0x20000000, 112KB(主战场)
// SRAM2 @ 0x2001C000, 16KB
// CCM @ 0x10000000, 64KB(CPU 独享,DMA 用不了)
HeapRegion_t regions[] = {
{ (uint8_t *)0x20000000, 112 * 1024 }, // 区域1:SRAM1
{ (uint8_t *)0x10000000, 64 * 1024 }, // 区域2:CCM
{ NULL, 0 } // 结束标志
};
vPortDefineHeapRegions(regions); // 必须调在 vTaskStartScheduler 之前
heap_4 的堆: heap_5 的堆:
一块连续 RAM 多块不连续 RAM 拼成逻辑上的大一整块
┌────────────┐ ┌────────────┐
│ 全部堆 │ │ SRAM1 │
│ (36KB) │ │ 112KB │
│ │ ├────────────┤
└────────────┘ │ SRAM1 │
│ 16KB │
└────────────┘
│ CCM │
│ 64KB │
└────────────┘
同一套算法,从三块池子里划
我们不用它 ------configTOTAL_HEAP_SIZE = 36KB 完全在 SRAM1 内部,一块就够。需要管理外部 SDRAM 或者要把 CCM 也用起来时才切到 heap_5。
三块 ram都能划分给堆heap,数组里写几个区就管几个:
HeapRegion_t regions[] = {
{ (uint8_t *)0x20000000, 112 * 1024 }, // 区域1:SRAM1(112KB)
{ (uint8_t *)0x2001C000, 16 * 1024 }, // 区域2:SRAM2(16KB)
{ (uint8_t *)0x10000000, 64 * 1024 }, // 区域3:CCM(64KB)
{ NULL, 0 } // ★ 结束标志
};
三块不连续的物理内存 → heap_5 把它们当成一个逻辑大堆,算法跟 heap_4 完全一样。上限不限,只要有连续地址空间,写几块都行。
注意 CCM 的坑:CCM 是 CPU 独享的,DMA 访问不了。把任务栈放 CCM 没问题(CPU 用),DMA 缓冲区放 CCM 会挂。
七:rtos\23\ 改动清单
freertos_demo.c
| 行号 | 内容 |
|---|---|
| 99-107 | pvPortMalloc(30) --- KEY1 申请 30 字节 |
| 113-120 | vPortFree(buf) --- KEY2 释放 |
| 128-133 | xPortGetFreeHeapSize() --- 每 500ms 打印堆剩余 |
main.c
| 行号 | 内容 |
|---|---|
| 26 | printf 标题 → "FreeRTOS Memory Management Test!" |
不需要开任何新宏 ------pvPortMalloc / vPortFree 配合 heap_4.c 一直可用,configSUPPORT_DYNAMIC_ALLOCATION = 1 早开着。
运行效果:按 KEY1 → 申请 30 字节 → 堆剩余减少。连按多次多申请几块。按 KEY2 → 释放最近一块 → 堆剩余恢复。每 500ms 自动打印当前堆剩余。
实验现象:
堆剩余:36224 字节
堆剩余:36224 字节
申请内存成功!
堆剩余:36184 字节
堆剩余:36184 字节
堆剩余:36184 字节
堆剩余:36184 字节
30 字节是你要的,但 heap_4 内部还有管理开销:
你申请 30 字节,实际上切出来的是:
┌──────────────┬─────────────────┐
│ BlockLink_t │ 30 字节有效区 │
│ (8 字节) │ (给你的) │
└──────────────┴─────────────────┘
↑ 这 8 字节不算在 30 里
36184 - 36224 = -40 字节,多出来的 ~10 字节是块头 + 对齐(heap_4 按 8 字节对齐,30 会凑整到 32,加上 8 字节头 = 40)。
// heap_4.c 里的块结构体
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; // 4 字节:指向下一个空闲块
size_t xBlockSize; // 4 字节:块大小(含头 + 对齐)
} BlockLink_t; // 共 8 字节
每个分配的块前面都有这个 8 字节头,FreeRTOS 用它在链表里管理和合并。你拿到的是跳过这个头的地址。