以下轉載自安富萊電子: http://forum.armfly.com/forum.php
本章節為大家講解 FreeRTOS 動態內存管理,動態內存管理是 FreeRTOS 非常重要的一項功能,前面
章節講解的任務創建、 信號量、 消息隊列、 事件標志組、 互斥信號量、 軟件定時器組等需要的 RAM 空間
都是通過動態內存管理從 FreeRTOSConfig.h 文件定義的 heap 空間中申請的。
動態內存管理介紹
FreeRTOS 支持 5 種動態內存管理方案,分別通過文件 heap_1,heap_2,heap_3,heap_4 和 heap_5
實現,這 5 個文件在 FreeRTOS 軟件包中的路徑是:FreeRTOS\Source\portable\MemMang。 用戶創
建的 FreeRTOS 工程項目僅需要 5 種方式中的一種。
下面將這 5 種動態內存管理方式分別進行講解。
動態內存管理方式一 heap_1
heap_1 動態內存管理方式是五種動態內存管理方式中最簡單的,這種方式的動態內存管理一旦申請
了相應內存后,是不允許被釋放的。 盡管如此,這種方式的動態內存管理還是滿足大部分嵌入式應用的,
因為這種嵌入式應用在系統啟動階段就完成了任務創建、 事件標志組、 信號量、 消息隊列等資源的創建,
而且這些資源是整個嵌入式應用過程中一直要使用的,所以也就不需要刪除,即釋放內存。 FreeRTOS 的
動態內存大小在 FreeRTOSConfig.h 文件中進行了定義:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //單位字節
用戶通過函數 xPortGetFreeHeapSize 就能獲得 FreeRTOS 動態內存的剩余,進而可以根據剩余情況優化
動態內存的大小。 heap_1 方式的動態內存管理有以下特點:
項目應用不需要刪除任務、 信號量、 消息隊列等已經創建的資源。
具有時間確定性,即申請動態內存的時間是固定的並且不會產生內存碎片。
確切的說這是一種靜態內存分配,因為申請的內存是不允許被釋放掉的。
動態內存管理方式二 heap_2
與 heap_1 動態內存管理方式不同,heap_2 動態內存管理利用了最適應算法,並且支持內存釋放。
但是 heap_2 不支持內存碎片整理,動態內存管理方式四 heap_4 支持內存碎片整理。 FreeRTOS 的動態
內存大小在 FreeRTOSConfig.h 文件中進行了定義:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //單位字節
用戶通過函數 xPortGetFreeHeapSize 就能獲得 FreeRTOS 動態內存的剩余,但是不提供動態內存是
如何被分配成各個小內存塊的信息。 另外,就是用戶可以根據剩余情況優化動態內存的大小。 heap_2 方
式的動態內存管理有以下特點:
不考慮內存碎片的情況下,這種方式支持重復的任務、 信號量、 事件標志組、 軟件定時器等內部資源
的創建和刪除。
如果用戶申請和釋放的動態內存大小是隨機的,不建議采用這種動態內存管理方式,比如:
項目應用中需要重復的創建和刪除任務,如果每次創建需要動態內存大小相同,那么 heap_2 比
較適合,但每次創建需要動態內存大小不同,那么方式 heap_2 就不合適了,因為容易產生內存
碎片,內存碎片過多的話會導致無法申請出一個大的內存塊出來,這種情況使用 heap_4 比較合
適。
項目應用中需要重復的創建和刪除消息隊列,也會出現類似上面的情況,這種情況下使用 heap_4
比較合適。
直接的調用函數 pvPortMalloc() 和 vPortFree()也容易出現內存碎片。 如果用戶按一定順序成
對的申請和釋放,基本沒有內存碎片的,而不按順序的隨機申請和釋放容易產生內存碎片。
如果用戶隨機的創建和刪除任務、 消息隊列、 事件標志組、 信號量等內部資源也容易出現內存碎片。
heap_2 方式實現的動態內存申請不具有時間確定性,但是比 C 庫中的 malloc 函數效率要高。
大部分需要動態內存申請和釋放的小型實時系統項目可以使用 heap_2。 如果需要內存碎片的回收機
制可以使用 heap_4。
動態內存管理方式三 heap_3
這種方式實現的動態內存管理是對編譯器提供的 malloc 和 free 函數進行了封裝,保證是線程安全的。
heap_3 方式的動態內存管理有以下特點:
需要編譯器提供 malloc 和 free 函數。
不具有時間確定性,即申請動態內存的時間不是固定的。
增加 RTOS 內核的代碼量。
另外要特別注意一點,這種方式的動態內存申請和釋放不是用的 FreeRTOSConfig.h 文件中定義的
heap空間大小,而是用的編譯器設置的heap空間大小或者說STM32啟動代碼中設置的heap空間大小,
比如 MDK 版本的 STM32F103 工程中 heap 大小就是在這里進行的定義: 
動態內存管理方式四 heap_4
與 heap_2 動態內存管理方式不同,heap_4 動態內存管理利用了最適應算法,且支持內存碎片的回
收並將其整理為一個大的內存塊。 FreeRTOS 的動態內存大小在 FreeRTOSConfig.h 文件中進行了定義:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) ) //單位字節
heap_4 同時支持將動態內存設置在指定的 RAM 空間位置。
用戶通過函數 xPortGetFreeHeapSize 就能獲得 FreeRTOS 動態內存的剩余,但是不提供動態內存是
如何被分配成各個小內存塊的信息。 使用函數 xPortGetMinimumEverFreeHeapSize 能夠獲取從系統啟
動到當前時刻的動態內存最小剩余,從而用戶就可以根據剩余情況優化動態內存的大小。 heap_4 方式的
動態內存管理有以下特點:
可以用於需要重復的創建和刪任務、 信號量、 事件標志組、 軟件定時器等內部資源的場合。
隨機的調用 pvPortMalloc() 和 vPortFree(),且每次申請的大小都不同,也不會像 heap_2 那樣產
生很多的內存碎片。
不具有時間確定性,即申請動態內存的時間不是確定的,但是比 C 庫中的 malloc 函數要高效。
heap_4 比較實用,本教程配套的所有例子都是用的這種方式的動態內存管理,用戶的代碼也可以直
接調用函數 pvPortMalloc() 和 vPortFree()進行動態內存的申請和釋放。
動態內存管理方式五 heap_5
有時候我們希望 FreeRTOSConfig.h 文件中定義的 heap 空間可以采用不連續的內存區,比如我們希
望可以將其定義在內部 SRAM 一部分,外部 SRAM 一部分,此時我們就可以采用 heap_5 動態內存管理
方式。另外,heap_5 動態內存管理是在 heap_4 的基礎上實現的。
heap_5 動態內存管理是通過函數 vPortDefineHeapRegions 進行初始化的,也就是說用戶在創建任
務 FreeRTOS 的內部資源前要優先級調用這個函數 vPortDefineHeapRegions,否則是無法通過函數
pvPortMalloc 申請到動態內存的。
函數 vPortDefineHeapRegions 定義不同段的內存空間采用了下面這種結構體: 
定義的時候要注意兩個問題,一個是內存段結束時要定義 NULL。另一個是內存段的地址是從低地址到高
地址排列。
用戶通過函數 xPortGetFreeHeapSize 就能獲得 FreeRTOS 動態內存的剩余,但是不提供動態內存是
如何被分配成各個小內存塊的信息。 使用函數 xPortGetMinimumEverFreeHeapSize 能夠獲取從系統啟
動到當前時刻的動態內存最小剩余,從而用戶就可以根據剩余情況優化動態內存的大小。
五種動態內存方式總結
五種動態內存管理方式簡單總結如下,實際項目中,用戶根據需要選擇合適的:
heap_1:五種方式里面最簡單的,但是申請的內存不允許釋放。
heap_2:支持動態內存的申請和釋放,但是不支持內存碎片的處理,並將其合並成一個大的內存塊。
heap_3:將編譯器自帶的 malloc 和 free 函數進行簡單的封裝,以支持線程安全,即支持多任務調
用。
heap_4:支持動態內存的申請和釋放,支持內存碎片處理,支持將動態內存設置在個固定的地址。
heap_5:在 heap_4 的基礎上支持將動態內存設置在不連續的區域上。
動態內存和靜態內存比較
靜態內存方式是從 FreeRTOS 的 V9.0.0 版本才開始有的,而我們本次教程使用的版本是 V8.2.3。所
以靜態內存方式我們暫時不做講解,等 FreeRTOS 教程版本升級時再做講解。 關於靜態內存方式和動態內
存方式的優缺點可以看官方的此貼說明:點擊查看
(制作此教程的時候,官方的 FreeRTOS V9.0.0 正式版本還沒有發布,所以采用的是當前最新的 V8.2.3)
動態內存 API 函數
動態內存的 API 函數在官方的在線版手冊上面沒有列出,其實使用也比較簡單,類似 C 庫的 malloc
和 free 函數,具體使用參看下面的實例說明。
實驗練兵場:
聲明一個結構類型:
typedef struct Msg { uint8_t ucMessageID; uint16_t usData[2]; uint32_t ulData[2]; }MSG_T;
消息隊列發送任務:
static void vTaskWork(void *pvParameters) { MSG_T *ptMsg; uint8_t ucCount = 0; while(1) { if (key1_flag==1) { key1_flag=0; } /* K2鍵按下,向xQueue1發送數據 */ if(key2_flag==1) { key2_flag=0; printf("=================================================\r\n"); printf("當前動態內存大小 = %d\r\n", xPortGetFreeHeapSize()); ptMsg = (MSG_T *)pvPortMalloc(sizeof(MSG_T)); // ptMsg = (MSG_T *)pvPortMalloc(32); printf("申請動態內存后剩余大小 = %d\r\n", xPortGetFreeHeapSize()); ptMsg->ucMessageID = ucCount++; ptMsg->ulData[0] = ucCount++; ptMsg->usData[0] = ucCount++; /* 使用消息隊列實現指針變量的傳遞 */ if(xQueueSend(xQueue1, /* 消息隊列句柄 */ (void *) &ptMsg, /* 發送結構體指針變量ptMsg的地址 */ (TickType_t)10) != pdPASS ) { /* 發送失敗,即使等待了10個時鍾節拍 */ printf("K2鍵按下,向xQueue2發送數據失敗,即使等待了10個時鍾節拍\r\n"); vPortFree(ptMsg); printf("釋放申請的動態內存后大小 = %d\r\n", xPortGetFreeHeapSize()); } else { /* 發送成功 */ printf("K2鍵按下,向xQueue2發送數據成功\r\n"); /* 由於是低優先級任務向高優先級任務發送消息隊列,如果成功的話說明高優先級任務已經執行。 並獲得了消息隊列中的數據,所以我們可以在此處釋放動態內存,不會出現高優先級任務還沒有 獲得消息隊列數據,我們就將動態內存釋放掉了。 */ vPortFree(ptMsg); printf("釋放申請的動態內存后大小 = %d\r\n", xPortGetFreeHeapSize()); } // TIM_Mode_Config(); } vTaskDelay(200); } }
接收任務:
void vTaskBeep(void *pvParameters) { MSG_T *ptMsg; BaseType_t xResult; const TickType_t xMaxBlockTime = pdMS_TO_TICKS(500); /* 設置最大等待時間為500ms */ while(1) { xResult = xQueueReceive(xQueue1, /* 消息隊列句柄 */ (void *)&ptMsg, /* 這里獲取的是結構體的地址 */ (TickType_t)xMaxBlockTime);/* 設置阻塞時間 */ if(xResult == pdPASS) { /* 成功接收,並通過串口將數據打印出來 */ printf("接收到消息隊列數據ptMsg->ucMessageID = %d\r\n", ptMsg->ucMessageID); printf("接收到消息隊列數據ptMsg->ulData[0] = %d\r\n", ptMsg->ulData[0]); printf("接收到消息隊列數據ptMsg->usData[0] = %d\r\n", ptMsg->usData[0]); } else { /* 超時 */ BEEP_TOGGLE; } } }
實驗現象展示:

那么問題就來了:
typedef struct Msg
{
uint8_t ucMessageID;
uint16_t usData[2];
uint32_t ulData[2];
}MSG_T;
這個結構體類型無論是在4字節對齊還是8字節對齊的編譯器上,輸出都是16字節。keil默認4字節對齊。
申請之前,顯示:當前動態內存大小 = 23480
申請之后,顯示:申請動態內存后剩余大小 = 23456
奇怪了,怎么會減少了24個字節呢?明明只申請了16字節啊。
heap_4文件也就是我們所有實驗使用的堆內存文件,它使用一個鏈表結構來跟蹤記錄空閑內存塊。結構體定義為:
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; /*指向列表中下一個空閑塊*/
size_t xBlockSize; /*當前空閑塊的大小,包括鏈表結構大小*/
} BlockLink_t;
與第二種內存管理策略一樣,空閑內存塊也是以單鏈表的形式組織起來的,BlockLink_t類型的局部靜態變量xStart表示鏈表頭,但第四種內存管理策略的鏈表尾保存在內存堆空間最后位置,並使用BlockLink_t指針類型局部靜態變量pxEnd指向這個區域(第二種內存管理策略使用靜態變量xEnd表示鏈表尾),如下圖所示。
第四種內存管理策略和第二種內存管理策略還有一個很大的不同是:第四種內存管理策略的空閑塊鏈表不是以內存塊大小為存儲順序,而是以內存塊起始地址大小為存儲順序,地址小的在前,地址大的在后。這也是為了適應合並算法而作的改變。

整個有效空間組成唯一一個空閑塊,在空閑塊的起始位置放置了一個鏈表結構,用於存儲這個空閑塊的大小和下一個空閑塊的地址。由於目前只有一個空閑塊,所以空閑塊的pxNextFreeBlock指向指針pxEnd指向的位置,而鏈表xStart結構的pxNextFreeBlock指向空閑塊。xStart表示鏈表頭,pxEnd指向位置表示鏈表尾。
當申請x字節內存時,實際上不僅需要分配x字節內存,還要分配一個BlockLink_t類型結構體空間,用於描述這個內存塊,結構體空間位於空閑內存塊的最開始處。當然,申請的內存大小和BlockLink_t類型結構體大小都要向上擴大到對齊字節數的整數倍。
這個擴展怎么理解呢?我們的keil默認是4字節對齊的,那么內存地址開始處,其值一定要能被4整除,例如一個地址現在是0x01,那么我們存放一個int四字節的變量,並不能從地址0x01處開始,而必須地址擴展到0x04,這樣才可以整除4.這個在C語言中已經做過分析。有了這個之后,我們看源碼知道,還需要把BlockLink_t類型的結構放在我們申請的內存開始處,這就證明了,我們的消耗的堆內存,是等於字節對齊要求之后,BlockLink_t類型結構占用的字節 + 申請的字節數。
BlockLink_t類型結構的元素,第一個是個指針,在keil編譯器中,一個指針4個字節,第二個是個size_t類型的元素,siez_t在我們使用的環境下,是unsigned int的別名,也是占用4個字節,這樣,相當於我們實際消耗的堆內存,是申請的內存經過字節對齊之后,再加上8個字節的大小的。
現在舉例說明:
現在我把申請ptMsg = (MSG_T *)pvPortMalloc(sizeof(MSG_T));換成:
ptMsg = (MSG_T *)pvPortMalloc(30);
輸出如下:

不是說申請內存加8字節碼?這里為什么還差2字節,申請前23480,申請后23440.注意,我前面說的是內存對齊之后,再加上8字節,我申請30字節的內存,會被擴展成32字節,這樣才能滿足四字節對齊的要求。
我們再測試,把申請的內存換成ptMsg = (MSG_T *)pvPortMalloc(32);這樣的輸出,必然和上面一樣:

