FreeRTOSのスタックとは?タスクスタックとスタックオーバーフローを初心者向けに解説

組み込み基礎

FreeRTOSでタスクを作成するとき、必ず考える必要があるものの1つがスタック(Stack)です。

たとえばFreeRTOSのxTaskCreate()では、次のようにタスク作成時にスタックサイズを指定します。

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

この2048は何を意味しているのでしょうか。

また、スタックサイズを小さくしすぎるとStack Overflow(スタックオーバーフロー)が発生し、プログラムが突然停止したり、リセットしたり、原因の分かりにくい不具合につながったりすることがあります。

この記事では、FreeRTOSのタスクスタックとは何か、なぜタスクごとにスタックが必要なのか、スタックサイズの決め方、High Water Markによる確認方法、スタックオーバーフローの検出方法まで順番に解説します。

FreeRTOSのタスクスタックの仕組み。タスクごとのスタック、スタックサイズ、スタックオーバーフロー、High Water Markによる確認方法を示した図
FreeRTOSのタスクスタックとスタックオーバーフロー、High Water Markによる確認

スタックとは?

スタックとは、プログラムの実行中に一時的なデータを保存するために使われるメモリ領域です。

代表的には、次のような情報がスタックに保存されます。

  • 関数の戻り先に関する情報
  • ローカル変数
  • 関数呼び出しに必要な情報
  • CPUレジスタなど、タスクの実行状態を保存するための情報

たとえば、次のように関数が呼び出されたとします。

main()
  ↓
funcA()
  ↓
funcB()
  ↓
funcC()

funcC()が終了したらfuncB()へ戻り、さらにfuncA()、main()へ戻っていく必要があります。

このような関数呼び出しの情報を管理するためにスタックが利用されます。

一般的なスタックとヒープの違いについては「スタックとヒープとは?組み込み開発のメモリの仕組みを初心者向けに解説」も参照してください。

FreeRTOSではタスクごとにスタックを持つ

FreeRTOSでは、複数のタスクを切り替えながら実行します。

Task A
 ↓
Task B
 ↓
Task C
 ↓
Task A
 ↓
...

ここで重要なのが、各タスクが独立したスタックを持っているということです。

Task A
┌──────────────┐
│   Task A     │
│   Stack      │
└──────────────┘

Task B
┌──────────────┐
│   Task B     │
│   Stack      │
└──────────────┘

Task C
┌──────────────┐
│   Task C     │
│   Stack      │
└──────────────┘

Task Aが関数を実行している途中でTask Bへ切り替わったとしても、Task Aの関数呼び出しやローカル変数などの状態を残しておかなければなりません。

そのため、それぞれのタスクに専用のスタック領域が必要になります。

なぜタスクごとにスタックが必要なの?

たとえばTask Aが次の関数を実行している途中だったとします。

Task A

taskA()
  ↓
func1()
  ↓
func2()
  ↓
func3()

このタイミングでFreeRTOSのスケジューラがTask Bへ切り替えます。

Task A
func3()実行中
   ↓
タスク切り替え
   ↓
Task B
funcB()実行

その後Task Aへ戻ったときには、Task Aは途中から処理を再開する必要があります。

Task AとTask Bで同じスタック領域を使ってしまうと、お互いの関数呼び出しやローカルデータを壊してしまいます。

そのためFreeRTOSでは、タスクごとに独立したスタックを用意します。

タスク作成時にスタックサイズを指定する

FreeRTOSでタスクを作成するときは、xTaskCreate()などでスタックサイズを指定します。

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

ここで指定するスタックサイズの単位には注意が必要です。

標準的なFreeRTOSのxTaskCreate()では、スタック深さはバイト数ではなくStackType_tの個数として指定されます。

つまり、標準FreeRTOSでStackType_tが32bitなら、

2048 × 4byte
= 8192byte
= 約8KB

となります。

ESP32ではスタックサイズの単位に注意

ESP32を使用している場合は、ここが特に注意点です。

ESP-IDFで使用されるFreeRTOSはEspressifによる変更が加えられており、xTaskCreate()のulStackDepthはバイト単位として扱われます。

そのため、ESP32系で次のように指定した場合、

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

ESP-IDF系のFreeRTOSでは、基本的に2048バイトのスタックという意味になります。

標準FreeRTOSとESP-IDFでは単位が異なるため、他のマイコン向けFreeRTOSのサンプルコードをESP32へ移植するときは注意してください。

スタックには何が保存される?

タスクのスタック使用量は、そのタスク内でどのような処理を行うかによって変わります。

  • ローカル変数
  • 関数呼び出し
  • 関数の引数などに関する情報
  • 保存が必要なCPUレジスタ
  • ライブラリ関数内部で必要になるデータ

特に大きなローカル配列には注意が必要です。

void sensorTask(void *pvParameters)
{
    char buffer[2048];

    while (1) {

        // 処理

    }
}

このような大きな配列をローカル変数として確保すると、それだけでスタックを大きく消費する可能性があります。

関数を深く呼び出すとスタック使用量が増える

関数呼び出しが深くなることでも、スタック使用量が増える可能性があります。

task()
 ↓
funcA()
 ↓
funcB()
 ↓
funcC()
 ↓
funcD()

それぞれの関数でローカル変数を使用していれば、その分のスタック領域も必要になります。

再帰呼び出しを使う場合は、呼び出しが繰り返されるたびにスタックを消費するため、組み込みシステムでは特に注意が必要です。

スタックオーバーフローとは?

タスクが割り当てられたスタック領域を超えて使用してしまうことをStack Overflow(スタックオーバーフロー)と呼びます。

正常

┌─────────────┐
│     空き     │
│-------------│
│             │
│ 使用中      │
│             │
└─────────────┘


Stack Overflow

┌─────────────┐
│ 使用中 ↑    │
│ 使用中 ↑    │
│ 使用中 ↑    │
│ 使用中 ↑    │
└─────────────┘
      ↑
割り当て領域を超える

スタックの境界を超えると、別のメモリ領域を破壊してしまう可能性があります。

スタックオーバーフローが怖い理由

スタックオーバーフローの厄介なところは、単純に「スタックが足りません」と表示されるとは限らないことです。

メモリを破壊した結果として、

  • 突然リセットする
  • タスクが停止する
  • 別のタスクがおかしくなる
  • 変数の値が突然変わる
  • 例外やクラッシュが発生する
  • 特定条件でしか発生しない不具合になる

といった、一見スタックとは関係なさそうな症状として現れることがあります。

スタックオーバーフローが発生しやすい処理

  • 大きなローカル配列を使う
  • 大きな構造体をローカル変数として使う
  • 関数呼び出しが深い
  • 再帰関数を使用する
  • スタックを多く使うライブラリを呼び出す
  • printf()など比較的重い処理を使用する
  • タスク作成時のスタックサイズが小さすぎる

スタックを大きくすればいい?

「スタックオーバーフローが怖いなら、すべてのタスクに大きなスタックを割り当てればいい」と考えるかもしれません。

しかし、組み込みシステムではRAM容量が限られています。

Task A  8KB
Task B  8KB
Task C  8KB
Task D  8KB

合計 32KB

実際には各タスクが2KB程度しか必要としていないのに8KBずつ割り当てれば、多くのRAMを無駄にしてしまいます。

そこで、実際にどれくらいスタックを使用しているかを確認することが重要になります。

High Water Markとは?

FreeRTOSには、タスクのスタックにどれくらい余裕が残っているかを確認するための仕組みがあります。

それがStack High Water Markです。

High Water Markは、タスクが実行されてから現在までの間で、最もスタックを多く使用した時点でも残っていた最小の空き量を表します。

スタック容量

████████████████

最大使用時

████████████░░░░
            ↑
     最低でも残っていた領域

        High Water Mark

つまり、この値を見ることで「今までの実行で、スタックに最低どれくらい余裕があったか」を確認できます。

uxTaskGetStackHighWaterMark()で確認する

FreeRTOSではuxTaskGetStackHighWaterMark()を使ってHigh Water Markを取得できます。

UBaseType_t highWaterMark;

highWaterMark =
    uxTaskGetStackHighWaterMark(
        NULL
    );

NULLを指定すると、呼び出したタスク自身のHigh Water Markを取得します。

Serial.print(
    "High Water Mark: "
);

Serial.println(
    highWaterMark
);

値が小さくなっている場合は、スタックの余裕が少なくなっている可能性があります。

ESP32ではHigh Water Markの単位にも注意

標準FreeRTOSではHigh Water Markの単位はスタックのエントリ数として扱われます。

一方、ESP-IDF版FreeRTOSでは、uxTaskGetStackHighWaterMark()などのスタックサイズ関連APIがバイト単位になるよう変更されているものがあります。

ESP32-C3などで実際の値を判断するときは、標準FreeRTOSの説明だけでなく、使用しているESP-IDFのAPIリファレンスも確認してください。

KUMITATE-C3でHigh Water Markを確認してみよう

KUMITATE-C3で簡単なタスクを作成し、High Water Markをシリアルモニターへ表示してみます。

#include <Arduino.h>


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

        UBaseType_t highWaterMark =
            uxTaskGetStackHighWaterMark(
                NULL
            );

        Serial.print(
            "High Water Mark: "
        );

        Serial.println(
            highWaterMark
        );

        vTaskDelay(
            pdMS_TO_TICKS(1000)
        );
    }
}


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

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


void loop()
{
    vTaskDelay(
        pdMS_TO_TICKS(1000)
    );
}

このプログラムでは、1秒ごとにタスクのHigh Water Markを表示します。

タスク内の処理を増やしながら値を確認すると、どのような処理でスタック使用量が増えるのかを観察できます。

大きなローカル配列を追加して比較する

学習用として、タスク内に大きなローカル配列を追加してHigh Water Markの変化を見る方法もあります。

void testTask(void *pvParameters)
{
    volatile char buffer[512];

    buffer[0] = 0;

    while (1) {

        Serial.println(
            uxTaskGetStackHighWaterMark(
                NULL
            )
        );

        vTaskDelay(
            pdMS_TO_TICKS(1000)
        );
    }
}

配列を追加する前と後でHigh Water Markを比較すると、ローカル変数とスタックの関係を理解しやすくなります。

ただし、コンパイラの最適化によって未使用の変数が削除されることがあります。学習用の実験では、実際に配列へアクセスするなど、最適化の影響にも注意してください。

スタックオーバーフローを検出する

FreeRTOSには、スタックオーバーフローを検出するための仕組みがあります。

代表的な設定がconfigCHECK_FOR_STACK_OVERFLOWです。

#define configCHECK_FOR_STACK_OVERFLOW 2

設定を有効にすると、FreeRTOSがタスクのスタック異常を検出するためのチェックを行います。

具体的なチェック方法や設定方法はFreeRTOSのポートや使用している開発環境によって異なるため、対象環境の設定も確認してください。

vApplicationStackOverflowHook()とは?

スタックオーバーフロー検出を有効にした環境では、異常が検出されたときにvApplicationStackOverflowHook()を呼び出すよう設定できます。

void vApplicationStackOverflowHook(
    TaskHandle_t xTask,
    char *pcTaskName
)
{
    // スタックオーバーフロー時の処理
}

ここで、

  • デバッグ情報を残す
  • LEDでエラーを知らせる
  • 安全な状態へ移行する
  • システムを停止する

などの処理を行う設計が考えられます。

スタックオーバーフロー検出だけに頼らない

スタックオーバーフロー検出を有効にしたからといって、あらゆるスタック破壊を完全に防止できるわけではありません。

スタック不足が疑われる場合は、

  • High Water Markを確認する
  • ローカル変数のサイズを確認する
  • 関数呼び出しの深さを確認する
  • 再帰処理がないか確認する
  • スタックサイズを増やして症状が変わるか確認する
  • クラッシュログや例外情報を確認する

といった複数の方法で調査することが重要です。

スタックサイズはどう決める?

必要なスタックサイズは、タスクの処理内容によって異なります。

そのため、「すべてのタスクは2048にする」といった固定的な決め方よりも、実際の動作を確認しながら調整する方が適切です。

① 余裕を持ってスタックを確保
          ↓
② 実際の処理を動かす
          ↓
③ High Water Markを確認
          ↓
④ 最悪条件でも十分な余裕があるか確認
          ↓
⑤ 必要に応じてサイズ調整

「普段の動作」だけで判断しない

High Water Markを確認するときは、通常状態だけでなく最もスタックを使う条件を実行することが重要です。

  • エラー処理
  • 通信再接続
  • 大量データ受信
  • ログ出力
  • 複雑な表示処理
  • JSONなどの解析
  • 異常時の処理

普段は十分な余裕があっても、特定の処理を実行した瞬間だけスタック使用量が大きく増えることがあります。

スタックとヒープの関係

FreeRTOSで動的にタスクを作成する場合、タスクの管理情報やスタック領域などのために動的メモリが使用されます。

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

そのため、タスクのスタックサイズを大きくすると、システム全体で利用できるメモリもその分減ります。

「スタック不足を防ぐこと」と「RAMを無駄にしないこと」のバランスが重要です。

スタックとQueue・Semaphore・Mutexは別物

FreeRTOSでは、Queue、Semaphore、Mutex、Event Groupsなどさまざまなオブジェクトを利用します。

これらとタスクスタックは役割が異なります。

機能 主な役割
Task Stack タスクの関数呼び出しやローカルデータなどを保持
Queue タスク間でデータを受け渡す
Semaphore イベント通知・同期
Mutex 共有リソースの排他制御
Task Notification 特定タスクへ軽量に通知
Event Groups 複数イベントの状態管理・待ち合わせ

タスクが増えるとスタックも増える

FreeRTOSでタスクを増やすと、そのタスクごとにスタック領域が必要になります。

Sensor Task
  Stack 3KB

Display Task
  Stack 3KB

WiFi Task
  Stack 5KB

Logger Task
  Stack 3KB

----------------
合計 14KB + 管理領域など

そのため、「処理ごとに何でもタスクを作る」という設計はRAM使用量を増やす原因にもなります。

Software Timerや既存タスクへのTask Notificationなどを利用することで、専用タスクを増やさずに実装できる場合もあります。

スタック不足を疑う症状

次のような現象が発生した場合は、スタック不足も確認項目の1つです。

  • 処理を追加したら突然リセットするようになった
  • 特定の関数を呼ぶとクラッシュする
  • ログを増やしたら不安定になった
  • タスクを増やしたら動作がおかしくなった
  • 特定条件でのみ例外が発生する
  • 原因不明のメモリ破壊のような症状が出る

ただし、これらの症状はスタック不足以外の原因でも発生します。High Water Markやクラッシュ情報などを確認して原因を切り分けることが重要です。

FreeRTOSのスタックをデバッグするときのポイント

  • タスクごとのスタックサイズを把握する
  • High Water Markを確認する
  • 大きなローカル変数を確認する
  • 関数呼び出しの深さを確認する
  • 再帰処理を避ける、または最大深度を明確にする
  • スタックオーバーフロー検出を利用する
  • 最悪条件でテストする
  • スタックサイズをむやみに削りすぎない

まとめ

FreeRTOSでは、それぞれのタスクが独立したスタックを持っています。

  • スタックは関数呼び出しやローカル変数などに利用される
  • FreeRTOSではタスクごとに独立したスタックが必要
  • タスク切り替え後も各タスクの実行状態を維持できる
  • xTaskCreate()でタスクのスタックサイズを指定する
  • 標準FreeRTOSではスタック深さはStackType_t単位
  • ESP-IDF版FreeRTOSではスタックサイズ関連APIの単位が標準FreeRTOSと異なる場合がある
  • 大きなローカル変数や深い関数呼び出しはスタック使用量を増やす
  • 割り当てたスタックを超えるとStack Overflowが発生する
  • Stack Overflowはクラッシュやメモリ破壊につながる可能性がある
  • High Water Markでスタックの最小空き量を確認できる
  • uxTaskGetStackHighWaterMark()で確認できる
  • configCHECK_FOR_STACK_OVERFLOWで検出機能を設定できる
  • 実際の最悪条件を動かしてスタックサイズを評価することが重要

FreeRTOSのスタックで特に重要なのは、「タスクを1つ作るたびに、そのタスク専用のスタック領域も必要になる」という点です。

スタックサイズを小さくしすぎるとシステムが不安定になり、大きくしすぎると限られたRAMを無駄にします。

そのため、最初はある程度余裕を持って確保し、実際の処理を動かしたうえでHigh Water Markを確認し、十分な安全マージンを残して調整するのが基本です。

ここまで理解できると、FreeRTOSで「タスクを作ったら動いた」という段階から、各タスクがどのくらいRAMを使い、なぜスタック不足でシステムが不安定になるのかまで考えられるようになります。

KUMITATE

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

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

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