FreeRTOSのヒープとは?heap_1〜heap_5と動的メモリ管理を初心者向けに解説

組み込み基礎

FreeRTOSでタスクやQueue、Semaphore、Mutex、Software Timerなどを作成すると、そのためのメモリが必要になります。

たとえば、次のようにタスクを作成したとします。

xTaskCreate(
    sensorTask,
    "Sensor",
    2048,
    NULL,
    1,
    NULL
);

このときFreeRTOSは、タスクを管理するための情報やタスクスタックなどに必要なメモリを確保します。

このように、プログラムの実行中に必要に応じてメモリを確保するために使われる領域がヒープ(Heap)です。

FreeRTOSにはヒープを管理する方法として、代表的にheap_1、heap_2、heap_3、heap_4、heap_5という複数の実装が用意されています。

この記事では、FreeRTOSのヒープとは何か、pvPortMalloc()とvPortFree()、heap_1〜heap_5の違い、メモリ断片化、ヒープ残量の確認方法まで順番に解説します。

FreeRTOSのヒープの仕組み。heap_1からheap_5の違い、動的メモリ確保、メモリ解放、断片化、ヒープ使用量の確認方法を示した図
FreeRTOSのヒープとheap_1〜heap_5による動的メモリ管理

ヒープとは?

ヒープとは、プログラムの実行中に必要に応じてメモリを確保・解放するために利用するメモリ領域です。

たとえばC言語では、一般的にmalloc()を使って動的にメモリを確保します。

int *data;

data = (int *)malloc(
    sizeof(int) * 100
);

必要なくなったらfree()で解放します。

free(data);

FreeRTOSにも同じような動的メモリ管理の仕組みがあります。

スタックとヒープの違い

ヒープを理解するときは、スタックとの違いを整理しておくと分かりやすくなります。

項目 スタック ヒープ
主な用途 関数呼び出し、ローカル変数など 実行中の動的なメモリ確保
FreeRTOSでの用途 各タスクの実行状態 タスクやQueueなどの動的生成
確保 関数呼び出しなどに応じて使用 必要なタイミングで確保
解放 関数終了などに伴い自動的に再利用 オブジェクト削除などに伴い解放
主な問題 スタックオーバーフロー ヒープ不足、断片化など

スタックについては「FreeRTOSのスタックとは?タスクスタックとスタックオーバーフローを初心者向けに解説」も参照してください。

FreeRTOSでは何にヒープを使う?

FreeRTOSでは、さまざまなカーネルオブジェクトを動的に作成できます。

  • Task
  • Queue
  • Semaphore
  • Mutex
  • Event Groups
  • Software Timer

たとえばxTaskCreate()を使ってタスクを動的に作成すると、タスクを管理するためのTCB(Task Control Block)やスタックなどに必要なメモリが確保されます。

FreeRTOS Heap
      ↓
xTaskCreate()
      ↓
┌────────────────┐
│ TCB            │
├────────────────┤
│ Task Stack     │
│                │
└────────────────┘

Queueを作成した場合も、Queueの管理情報やデータを格納するための領域が必要になります。

pvPortMalloc()とは?

FreeRTOSでは、動的メモリを確保するためにpvPortMalloc()という関数が用意されています。

void *memory;

memory = pvPortMalloc(
    100
);

この例では100バイトのメモリを要求しています。

確保に成功すると確保した領域へのポインタが返り、失敗するとNULLが返ります。

void *memory =
    pvPortMalloc(100);

if (memory == NULL) {

    // メモリ確保失敗

}

FreeRTOSの動的生成APIの内部でも、選択されたヒープ実装を通してメモリが確保されます。

vPortFree()とは?

確保したメモリが不要になった場合は、対応しているヒープ実装であればvPortFree()を使って解放できます。

void *memory =
    pvPortMalloc(100);

if (memory != NULL) {

    // memoryを使用

    vPortFree(memory);
}

ただし、後述するheap_1ではメモリの解放に対応していません。

なぜheap_1〜heap_5があるの?

組み込みシステムでは、すべての製品が同じメモリ管理を必要とするわけではありません。

たとえば、起動時に必要なタスクやQueueをすべて作成し、その後は削除しないシステムであれば、複雑なメモリ解放処理は必要ありません。

一方で、実行中にオブジェクトを何度も作成・削除するシステムでは、メモリを再利用できる仕組みが必要です。

FreeRTOSでは、このような用途の違いに対応するため、複数のヒープ管理方式が用意されています。

heap_1とは?

heap_1は、非常にシンプルなヒープ管理方式です。

メモリを順番に確保していきますが、一度確保したメモリを解放できません。

初期状態

┌──────────────────────┐
│        空き           │
└──────────────────────┘


Aを確保

┌────┬─────────────────┐
│ A  │      空き        │
└────┴─────────────────┘


Bを確保

┌────┬────┬────────────┐
│ A  │ B  │    空き     │
└────┴────┴────────────┘

AやBが不要になっても、その領域をvPortFree()で再利用することはできません。

その代わり仕組みが単純で、メモリの解放による断片化も発生しません。

起動時に必要なオブジェクトをすべて作成し、その後削除しないようなシステムに向いています。

heap_2とは?

heap_2では、確保したメモリを解放して再利用できます。

A   B   C   D

↓ Bを解放

A  空き  C   D

↓ 新しい領域を確保

A   E   C   D

ただし、heap_2では隣り合った空き領域を結合しません。

そのため、確保と解放を繰り返すと、細かな空き領域が増えてメモリ断片化が発生する可能性があります。

現在では、多くの用途ではheap_4などの方が扱いやすいでしょう。

メモリ断片化とは?

メモリ断片化(Fragmentation)とは、空きメモリが細かく分かれてしまう状態です。

使用中  空き  使用中  空き  使用中  空き

██████  ░░░   ██████  ░░   ██████  ░░░

たとえば空きメモリの合計が1000バイトあったとしても、

300byte
+
300byte
+
400byte

合計1000byte

のように分断されていれば、700バイトの連続した領域を確保できない可能性があります。

つまり、「空き容量の合計」だけではメモリ確保が成功するか判断できない場合があります。

heap_3とは?

heap_3は、FreeRTOS独自のヒープ領域を管理する代わりに、Cライブラリなどが提供するmalloc()とfree()を利用する方式です。

pvPortMalloc()
      ↓
   malloc()


vPortFree()
      ↓
    free()

FreeRTOS側では、必要に応じてスケジューラとの整合性を考慮した形でこれらを利用します。

実際のメモリ管理の特徴は、使用しているCライブラリやシステムのmalloc()実装に依存します。

heap_4とは?

heap_4は、メモリの確保と解放に対応し、さらに隣り合う空き領域を結合する仕組みを持っています。

A   空き   空き   D

       ↓

隣接する空き領域を結合

       ↓

A      大きな空き領域      D

これにより、heap_2と比較して外部断片化を抑えやすくなっています。

ただし、断片化の可能性そのものが完全になくなるわけではありません。

1つの連続したヒープ領域を管理する一般的な用途では、heap_4は有力な選択肢です。

heap_5とは?

heap_5は、基本的なメモリ管理方式はheap_4に近いですが、複数の離れたメモリ領域を1つのFreeRTOSヒープとして管理できる点が特徴です。

Memory Region 1
┌───────────────┐
│     RAM       │
└───────────────┘

Memory Region 2
┌───────────────┐
│     RAM       │
└───────────────┘

        ↓

     heap_5

        ↓

複数領域を
FreeRTOSヒープとして管理

マイコンによっては、RAMがアドレス空間上で複数の領域に分かれている場合があります。

heap_5は、このような複数領域を利用したい場合に使える方式です。

heap_1〜heap_5を比較

方式 解放 空き領域の結合 特徴
heap_1 不可 不要 非常にシンプル
heap_2 可能 しない 断片化しやすい
heap_3 可能 実装依存 標準ライブラリのmalloc/freeを利用
heap_4 可能 する 隣接空き領域を結合する
heap_5 可能 する 複数の非連続メモリ領域を扱える

heap_4とheap_5の違い

heap_4とheap_5はよく似ています。

大きな違いは管理するメモリ領域の数です。

heap_4

┌────────────────────┐
│ 1つのヒープ領域     │
└────────────────────┘


heap_5

┌──────────┐
│ Region A │
└──────────┘

┌──────────┐
│ Region B │
└──────────┘

┌──────────┐
│ Region C │
└──────────┘

1つの連続したメモリ領域だけを使うのであればheap_4で対応できます。

複数の非連続なRAM領域をFreeRTOSのヒープとしてまとめて利用したい場合はheap_5が候補になります。

heap_5ではメモリ領域を定義する

heap_5を使用する場合は、使用するメモリ領域をHeapRegion_tで定義し、vPortDefineHeapRegions()へ渡します。

HeapRegion_t regions[] =
{
    {
        region1,
        sizeof(region1)
    },

    {
        region2,
        sizeof(region2)
    },

    {
        NULL,
        0
    }
};

vPortDefineHeapRegions(
    regions
);

実際の領域の指定方法や配置条件は、使用しているマイコンやリンカ設定なども関係します。

FreeRTOSのヒープサイズはどこで決まる?

FreeRTOSの一般的なheap_1、heap_2、heap_4では、ヒープ領域の大きさにconfigTOTAL_HEAP_SIZEが関係します。

#define configTOTAL_HEAP_SIZE \
    (20 * 1024)

この例なら約20KBです。

ただし、実際の設定方法やどのヒープ実装を使用するかは、FreeRTOSを組み込んでいるSDKや開発環境によって異なる場合があります。

ESP32ではheap_4を選ぶの?

ここはESP32やKUMITATE-C3を使う場合に特に注意したいポイントです。

FreeRTOS単体ではheap_1〜heap_5からヒープ実装を選択できますが、ESP-IDFではEspressif独自のヒープ管理機構と統合されています。

そのため、ESP32-C3でFreeRTOSを使うときに「heap_4.cを選べばよい」と単純に考えるのは適切ではありません。

ESP-IDFでは、内部RAMなど複数のメモリ領域とその能力を考慮してメモリを割り当てる仕組みが用意されています。

つまり、heap_1〜heap_5はFreeRTOSそのもののヒープ管理方式を理解するための重要な知識ですが、ESP32で実際にどのようにメモリが管理されるかはESP-IDF側の仕様も確認する必要があります。

ヒープの空き容量を確認する

FreeRTOSには、ヒープの空き容量を取得するAPIがあります。

代表的なのがxPortGetFreeHeapSize()です。

size_t freeHeap;

freeHeap =
    xPortGetFreeHeapSize();

これにより、現在利用可能なヒープ容量を確認できます。

Serial.print(
    "Free Heap: "
);

Serial.println(
    xPortGetFreeHeapSize()
);

最小空きヒープ容量を確認する

現在の空き容量だけでなく、起動してから現在までで最も空きヒープが少なくなったときの値を確認することも重要です。

対応しているFreeRTOS環境では、xPortGetMinimumEverFreeHeapSize()を利用できます。

size_t minFreeHeap;

minFreeHeap =
    xPortGetMinimumEverFreeHeapSize();

これは、前回の記事で紹介したStack High Water Markと似た考え方です。

起動時
Free Heap = 100KB

       ↓

通常動作
Free Heap = 80KB

       ↓

最も使用した瞬間
Free Heap = 42KB

       ↓

現在
Free Heap = 70KB


Minimum Ever Free Heap
        =
       42KB

現在70KB空いていても、過去には42KBまで減ったことが分かります。

空きヒープだけでは断片化を判断できない

xPortGetFreeHeapSize()で100KB空いているからといって、必ず100KBのメモリを1回で確保できるとは限りません。

メモリが細かく分断されている可能性があるためです。

合計空き容量 100KB

30KB   20KB   25KB   25KB
 ↓      ↓      ↓      ↓
空き    空き    空き    空き

最大の連続空き領域
        =
       30KB

この状態では、たとえば50KBの連続した領域を要求すると確保できません。

断片化が問題になるシステムでは、合計空き容量だけでなく最大の連続空きブロックも重要です。

ESP32ではヒープを詳しく確認できる

ESP-IDFには、FreeRTOSの一般的なヒープAPIに加えて、ESP32のメモリ構成を考慮したヒープ情報を取得するAPIがあります。

たとえば、用途に応じたメモリ領域の空き容量や最大連続ブロックなどを確認できます。

KUMITATE-C3のようなESP32-C3搭載ボードでメモリ問題を詳しく調査するときは、FreeRTOSの一般的なAPIだけでなく、ESP-IDFのHeap Memory Allocation APIも役立ちます。

KUMITATE-C3でヒープ容量を確認してみよう

KUMITATE-C3で、現在の空きヒープ容量を確認してみましょう。

#include <Arduino.h>

void setup()
{
    Serial.begin(115200);
}

void loop()
{
    Serial.print(
        "Free Heap: "
    );

    Serial.println(
        ESP.getFreeHeap()
    );

    delay(1000);
}

Arduino-ESP32ではESP.getFreeHeap()を使って空きヒープ容量を簡単に確認できます。

シリアルモニターには、たとえば次のような値が表示されます。

Free Heap: 312456
Free Heap: 312456
Free Heap: 312456

実際の値は、使用しているArduino-ESP32のバージョン、ライブラリ、Wi-Fiなどの機能、プログラム構成によって変わります。

オブジェクトを作るとヒープはどう変わる?

学習用として、FreeRTOSオブジェクトを作る前後で空きヒープを比較してみると分かりやすいでしょう。

#include <Arduino.h>

QueueHandle_t queue;


void setup()
{
    Serial.begin(115200);

    delay(1000);

    Serial.print(
        "Before: "
    );

    Serial.println(
        ESP.getFreeHeap()
    );

    queue = xQueueCreate(
        10,
        sizeof(int)
    );

    Serial.print(
        "After : "
    );

    Serial.println(
        ESP.getFreeHeap()
    );
}


void loop()
{
}

Queueを作成すると、Queueの管理情報やデータ格納領域などが必要になるため、空きヒープが減ることを確認できます。

タスクを作るとヒープはどう変わる?

タスクでも同じ実験ができます。

void testTask(void *pvParameters)
{
    while (1) {

        vTaskDelay(
            pdMS_TO_TICKS(1000)
        );
    }
}


void setup()
{
    Serial.begin(115200);

    delay(1000);

    Serial.print(
        "Before: "
    );

    Serial.println(
        ESP.getFreeHeap()
    );

    xTaskCreate(
        testTask,
        "Test",
        2048,
        NULL,
        1,
        NULL
    );

    Serial.print(
        "After : "
    );

    Serial.println(
        ESP.getFreeHeap()
    );
}

タスクを作成すると、タスクスタックやタスク管理情報などにメモリが必要になるため、空きヒープが減ります。

前回学習したタスクスタックとヒープがここでつながります。

動的生成と静的生成

FreeRTOSでは、オブジェクトを動的に作成するだけでなく、アプリケーション側で必要なメモリを用意して静的に作成できるものもあります。

タスクであれば、

xTaskCreate()

が動的生成で、

xTaskCreateStatic()

が静的生成です。

QueueにもxQueueCreateStatic()などがあります。

静的生成のメリット

  • 必要なメモリ量を設計時に把握しやすい
  • 実行中の動的メモリ確保を減らせる
  • 動的確保に伴う断片化の影響を避けやすい
  • メモリ配置をアプリケーション側で管理できる

メモリ使用量や動作の予測可能性が特に重要な組み込みシステムでは、静的生成を検討する場合があります。

動的生成が悪いわけではない

「組み込みでは動的メモリを使ってはいけない」と単純に考える必要はありません。

重要なのは、システムの要求に合わせて使い分けることです。

動的生成 静的生成
柔軟性 高い 設計時に固定しやすい
実装 比較的簡単 メモリ領域を用意する必要がある
断片化 方式・使い方によって発生 動的確保由来の断片化を避けやすい
メモリ予測 実行状況に依存する場合がある 把握しやすい

メモリリークとは?

動的メモリを使用するときに注意したい問題の1つがメモリリークです。

メモリを確保したあと、不要になったにもかかわらず解放しない状態が繰り返されると、利用できるヒープが徐々に減っていきます。

起動時
Free Heap
100KB

 ↓

90KB

 ↓

80KB

 ↓

70KB

 ↓

60KB
   ...

最終的に
メモリ確保失敗

長時間稼働するIoT機器などでは、特に注意が必要です。

ヒープ不足が起きるとどうなる?

必要なサイズのメモリを確保できなくなると、動的メモリ確保は失敗します。

その結果、

  • タスクを作成できない
  • Queueを作成できない
  • Semaphoreを作成できない
  • Software Timerを作成できない
  • アプリケーション側の動的メモリ確保に失敗する

といった問題が発生します。

そのため、動的生成APIの戻り値を確認することが重要です。

xTaskCreate()の戻り値を確認する

BaseType_t result;

result = xTaskCreate(
    testTask,
    "Test",
    2048,
    NULL,
    1,
    NULL
);

if (result != pdPASS) {

    Serial.println(
        "Task creation failed"
    );
}

「普段は成功するから」と戻り値を無視していると、メモリ不足が発生したときに原因を追いにくくなります。

Malloc Failed Hookとは?

FreeRTOSでは、設定によって動的メモリ確保に失敗したときにvApplicationMallocFailedHook()を呼び出すことができます。

void vApplicationMallocFailedHook(void)
{
    // メモリ確保失敗時の処理
}

一般的なFreeRTOSでは、configUSE_MALLOC_FAILED_HOOKを有効にして利用します。

製品では、メモリ確保失敗時にログを残したり、安全な状態へ移行したりする設計も重要です。

ヒープをデバッグするときのポイント

  • 現在の空きヒープを確認する
  • 最小空きヒープを確認する
  • 長時間動作させて減少し続けないか確認する
  • 動的生成APIの戻り値を確認する
  • メモリを確保したら必要に応じて解放する
  • 空き容量だけでなく断片化にも注意する
  • 最大連続空き領域も確認する
  • 必要に応じて静的生成を検討する

スタック不足とヒープ不足を区別しよう

FreeRTOSのメモリ問題では、スタック不足とヒープ不足を混同しないことが重要です。

スタック不足 ヒープ不足
対象 主に個々のタスク システム全体の動的メモリ
代表的な問題 Stack Overflow 動的メモリ確保失敗
確認方法 Stack High Water Mark Free Heap / Minimum Free Heap
対策例 タスクスタックを調整 ヒープ使用量や動的生成を見直す

FreeRTOSのメモリを全体で考える

FreeRTOSを使ったシステムでは、単に「RAMが何KBあるか」だけでなく、どこでどれくらいメモリを使っているかを見る必要があります。

RAM
 │
 ├─ グローバル変数
 │
 ├─ 静的変数
 │
 ├─ FreeRTOS Heap
 │    │
 │    ├─ Task
 │    ├─ Task Stack
 │    ├─ Queue
 │    ├─ Semaphore
 │    ├─ Mutex
 │    ├─ Event Groups
 │    └─ Software Timer
 │
 └─ その他SDK・ライブラリなど

ESP32系ではSDKや通信機能などもメモリを利用するため、アプリケーションコードだけを見て判断しないことも重要です。

まとめ

FreeRTOSでは、タスクやQueueなどのオブジェクトを動的に作成するためにヒープが利用されます。

  • ヒープは実行中に動的なメモリ確保を行うための領域
  • FreeRTOSではpvPortMalloc()でメモリを確保できる
  • 対応する方式ではvPortFree()で解放できる
  • heap_1は確保のみで解放しない
  • heap_2は解放できるが隣接空き領域を結合しない
  • heap_3は標準ライブラリなどのmalloc/freeを利用する
  • heap_4は隣接した空き領域を結合する
  • heap_5は複数の非連続メモリ領域を管理できる
  • 確保と解放を繰り返すと断片化が問題になる場合がある
  • xPortGetFreeHeapSize()で空きヒープを確認できる
  • xPortGetMinimumEverFreeHeapSize()で過去の最小空き容量を確認できる環境がある
  • 空き容量の合計だけでなく最大連続空き領域も重要
  • FreeRTOSには動的生成と静的生成がある
  • メモリリークやメモリ確保失敗にも注意する
  • ESP32ではESP-IDF独自のヒープ管理機構も理解する必要がある

特に重要なのは、「FreeRTOSのヒープは、単にアプリケーションがmallocするためだけのものではなく、タスクやQueueなどのRTOSオブジェクトを動的に作成するためにも使われる」という点です。

また、前回学習したタスクスタックと今回のヒープは密接に関係しています。動的にタスクを作成すれば、そのタスクのスタック領域にもメモリが必要になります。

FreeRTOSで安定したシステムを作るには、Stack High Water Markとヒープの空き容量の両方を確認し、「どこでRAMを使っているのか」を把握することが重要です。

KUMITATE

読むだけでなく、
実際に動かして学びませんか?

クミタテは、組み込み開発や電子工作を 実践しながら学べる学習プラットフォームです。 ESP32を使ったプログラミングから、電子回路、センサー、 通信、基板設計まで、自分のペースで学習できます。

  • 無料で学べる実践的な教材を掲載
  • すぐに試せるサンプルプログラム付き
  • Googleアカウントですぐに登録可能
クミタテに無料登録する
組み込み基礎
スポンサーリンク