リアルタイム制御

タスク / スレッド

並行して動く処理の単位。

概要

タスク(Task)またはスレッド(Thread)は、CPUが独立して実行できる処理の最小単位です。複数のタスクがスケジューラによって管理され、時分割または優先度ベースで実行されることで、一つのCPUコアが複数の処理を「並行して」動かしているように見えます。

組み込み分野では、RTOSの文脈でよく「タスク」と呼ばれます。LinuxなどのOS文脈では「スレッド」や「プロセス」が使われますが、組み込みRTOSでは保護メモリ空間を持たないことが多く、全タスクが同一のアドレス空間を共有します。

各タスクは独自のスタック(ローカル変数・関数呼び出し履歴の格納域)、プログラムカウンタ(現在の実行位置)、レジスタの値を持ちます。タスクが切り替わるとき、これらの情報が保存・復元されます(コンテキストスイッチ)。

タスク設計は組み込みシステムの応答性・安定性に直結します。各タスクの役割を明確に分離し、優先度を適切に設定することがリアルタイム性確保の基本です。

歴史・背景

マルチタスクという概念はメインフレーム時代(1960年代)に生まれましたが、組み込みシステムへの応用は1980年代以降です。リソース制約の厳しいマイコンでは、シングルタスクのスーパーループ(無限ループで各処理を順次実行)が長らく主流でした。

1980年代にVxWorks(Wind River社)やpSOS(Software Components社)などの商用RTOSが登場し、優先度ベースのマルチタスク機能を組み込み分野にもたらしました。

1993年に策定されたTOPPERS/ITRON(日本発のRTOS仕様)は国産家電・自動車電子機器への普及に貢献。同年のPOSIX.1b(リアルタイム拡張)ではスレッドAPI(pthreads)が標準化されました。

2000年代以降はFreeRTOS(2003年)がオープンソースで公開され、コスト・ライセンス面から急速に普及。2017年にAmazonがFreeRTOSを取得しAWS IoTとの統合を進め、現在はIoT組み込みの事実上の標準となっています。

技術仕様

タスクの状態遷移

RTOSのタスクは以下の状態を遷移します。

         +-----------+
   作成   |  Ready    |  スケジューラが選択
  ------->| (実行可能) |---------------+
         +-----------+                |
               ^                     v
               |               +-----------+
               |               |  Running  |
               |               |(実行中)   |
               |               +-----------+
               |                     |
         +-----------+          [ブロック条件]
         |  Blocked  |<---------(待機・スリープ)
         |(待機中)  |
         +-----------+
               |  [条件成立:キュー受信・タイムアウト]
               +----> Ready へ

※Suspended(一時停止)状態を持つRTOSもある

FreeRTOSでのタスク仕様

項目詳細
優先度0(最低)〜configMAX_PRIORITIES-1(最高)
スタックサイズワード単位で指定(例:512ワード = 2048バイト on 32bit)
スタック割り当て静的(xTaskCreateStatic)または動的(xTaskCreate
タスク識別子TaskHandle_t
スケジューリング優先度ベース・プリエンプティブ(デフォルト)

スタックサイズの設計

タスクのスタックサイズが不足するとスタックオーバーフローが発生し、メモリ破壊や不正動作を引き起こします。

スタックサイズの決定に影響する要因:

  • ローカル変数のサイズ(配列・大きな構造体に注意)
  • 関数呼び出しの深さ(再帰は特に危険)
  • ISRからの通知による追加スタック
  • printf()等のライブラリ関数の内部スタック使用量(数百バイト)

FreeRTOSではuxTaskGetStackHighWaterMark()でスタックの使用済みピーク値を確認できます。

/* スタック使用量の確認 */
UBaseType_t remaining = uxTaskGetStackHighWaterMark(NULL);
/* remaining ワードが残り空き容量。小さすぎると危険 */

動作原理

タスクの作成と管理(FreeRTOS)

#include "FreeRTOS.h"
#include "task.h"

/* タスク関数の定義 */
void sensorTask(void *pvParameters) {
    /* 初期化処理 */
    TickType_t xLastWakeTime = xTaskGetTickCount();

    for (;;) { /* 無限ループ(タスクは終了しないこと) */
        float temp = readTemperature();
        xQueueSend(tempQueue, &temp, 0);

        /* 100ms周期で実行(ジッタを抑制) */
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(100));
    }
}

void controlTask(void *pvParameters) {
    float temp;
    for (;;) {
        if (xQueueReceive(tempQueue, &temp, pdMS_TO_TICKS(200)) == pdTRUE) {
            adjustHeater(temp);
        }
    }
}

int main(void) {
    SystemInit();
    tempQueue = xQueueCreate(10, sizeof(float));

    /* タスク作成:関数, 名前, スタック(ワード), 引数, 優先度, ハンドル */
    xTaskCreate(sensorTask,  "Sensor",  256, NULL, 2, NULL);
    xTaskCreate(controlTask, "Control", 512, NULL, 3, NULL);

    vTaskStartScheduler(); /* スケジューラ起動(戻らない) */
    for (;;);
}

タスク間通信

タスク同士はキュー・セマフォ・イベントグループなどのRTOSオブジェクトで通信します。

/* キューによるタスク間データ転送 */
QueueHandle_t dataQueue;

void producerTask(void *pvParameters) {
    uint32_t data = 0;
    for (;;) {
        data++;
        /* キューへ送信(満杯なら10ms待機後に諦める) */
        xQueueSend(dataQueue, &data, pdMS_TO_TICKS(10));
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

void consumerTask(void *pvParameters) {
    uint32_t received;
    for (;;) {
        /* キューからデータを受け取るまで無限ブロック */
        if (xQueueReceive(dataQueue, &received, portMAX_DELAY) == pdTRUE) {
            processData(received);
        }
    }
}

タスク優先度の設計指針

優先度は処理のタイムクリティカル度に基づいて割り当てます。

高優先度(優先度:高)
  ├ モーター制御タスク(1ms周期、制御ループ)
  ├ UART受信処理タスク(ISRから起床)
  ├ センサ読み取りタスク(10ms周期)

中優先度
  ├ データ処理・フィルタリングタスク
  ├ 通信プロトコル処理タスク

低優先度(優先度:低)
  ├ ログ記録タスク
  ├ 自己診断タスク
  └ アイドルタスク(スタック監視・省電力移行)

用途・ユースケース

産業機器

  • モーション制御: 軸制御タスク(最高優先度・1ms)、軌跡補間タスク(中)、HMI更新タスク(低)
  • PLCエミュレーション: IOポーリングタスク、ラダーロジック実行タスク、Modbus通信タスク

IoT機器

  • センサ収集: センサ読み取りタスク(1秒周期)、データ圧縮タスク、クラウド送信タスク(MQTT)
  • BLEデバイス: BLEスタックタスク(最高優先度)、アプリケーションタスク、省電力管理タスク

自動車ECU(AUTOSARタスク構造)

AUTOSARではOSタスクをRunnable(実行可能エンティティ)の集合として定義し、静的に周期・優先度を割り付けます。

HighFrequencyTask(1ms周期、優先度高)
  └ ComRead_Runnable, PIDControl_Runnable, ComWrite_Runnable

MediumFrequencyTask(10ms周期)
  └ SignalProcessing_Runnable, Diagnostic_Runnable

BackgroundTask(100ms周期、優先度低)
  └ NvWrite_Runnable, PowerManagement_Runnable

実装・開発のポイント

タスクのアンチパターン

  1. タスク数の過多 タスクが多すぎるとコンテキストスイッチの頻度が増え、スケジューラのオーバーヘッドが増大します。機能をまとめられるタスクは統合を検討します。

  2. グローバル変数での直接共有 タスク間をグローバル変数で直接通信すると、競合状態(Race Condition)が発生します。必ずキュー・セマフォミューテックスを経由します。

  3. スタックサイズの過小設定 スタックオーバーフローは最も発見しにくいバグの一つです。FreeRTOSのスタックオーバーフロー検出機能(configCHECK_FOR_STACK_OVERFLOW)を有効化します。

  4. vTaskDelay()による固定スリープ vTaskDelay(100ms)では、処理時間のばらつきで実効周期がずれます。vTaskDelayUntil()を使って周期を絶対時刻基準にします。

デバッグ支援

/* FreeRTOS デバッグ情報の表示 */
void printTaskInfo(void) {
    char buf[512];
    vTaskList(buf); /* タスク一覧(名前・状態・優先度・スタック空き) */
    printf("%s", buf);

    vTaskGetRunTimeStats(buf); /* タスクごとのCPU使用時間 */
    printf("%s", buf);
}

他技術との比較

観点RTOSタスクLinuxスレッド(pthread)コルーチン
メモリ保護なし(同一空間共有)あり(プロセス間は分離)なし
コンテキストスイッチコスト数μs数μs〜数十μs数ns〜数百ns
優先度の粒度粗(32〜256段階)細(99段階,SCHED_FIFO)ユーザ定義
スタック管理手動(固定サイズ)自動拡張(mmap)小スタック可
タイミング保証あり(RTOS保証)PREEMPT_RTで改善なし
向いている用途組み込みリアルタイムエッジAI・GUI・ネット高並行・省メモリ

タスク設計はリアルタイム性を達成するための根幹です。スケジューラプリエンプション・タスク間通信を正しく組み合わせることで、複雑な組み込みシステムを安全かつ保守性高く構築できます。

関連用語

参考リンク