目次
  1. はじめに:なぜSTM32にRTOSが必要か
  2. FreeRTOSの基本概念
  3. STM32CubeMXでFreeRTOSを有効化
  4. タスクの作成と管理
  5. キューによるタスク間通信
  6. セマフォ・ミューテックスによる同期
  7. 割り込みからのFreeRTOS API呼び出し
  8. よくある落とし穴と対策
  9. まとめ

はじめに:なぜSTM32にRTOSが必要か

STM32マイコンでセンサー収集・UART通信・LCD表示・モーター制御などを1つのwhileループ(スーパーループ)で書いていると、機能が増えるたびに「センサー読み込みが遅れて制御がガタつく」「UART受信中にLCD更新が止まる」といった問題が必ず起こります。

こうした問題を根本解決するのがFreeRTOSです。FreeRTOSはオープンソースの組込みリアルタイムOSで、STM32CubeMXに標準で組み込まれているため、追加コストなしで導入できます。タスクごとに処理を分離し、優先度に応じて自動的に切り替えてくれるため、複雑な制御系も見通しよく書けます。

本記事ではSTM32CubeMXで生成されるCMSIS-OS v2(FreeRTOSのCMSISラッパー)をベースに、最低限知っておくべき4つのAPIを解説します。本記事の前提として、UART/I2C/SPI/CANの基本実装は STM32マイコンのUART・I2C・SPI・CAN通信 実装ガイド をご覧ください。

弊社での活用実績

弊社では STM32F4・STM32H7 + FreeRTOS の構成で、産業用通信ゲートウェイ・センサーデータロガー・モーター制御装置など 多数の組込み開発実績 があります。本記事はその実案件で得たノウハウをもとに作成しています。

FreeRTOSの基本概念

FreeRTOSを使いこなす前に、3つの基本概念を押さえます。

1. タスク(Task)

独立した処理単位です。スーパーループの代わりに、機能ごとに「センサー読み込みタスク」「LCD更新タスク」「UART通信タスク」のように分割します。各タスクは 専用のスタック を持ち、独立に実行されます。

2. スケジューラ(Scheduler)

どのタスクをいつ実行するかを決める司令塔です。FreeRTOSはデフォルトでプリエンプティブ優先度ベースです。優先度の高いタスクが Ready 状態(実行可能)になった瞬間、現在のタスクを中断して切り替えます。同じ優先度のタスクはタイムスライス(デフォルト1ms)でラウンドロビン実行されます。

3. タスクの状態遷移

各タスクは4つの状態を行き来します。

  • Running(実行中):CPUを獲得して実行中。常に1つだけ
  • Ready(実行可能):実行待ち。スケジューラがCPU割り当てを判断
  • Blocked(待機中):イベント待ち(delay、キュー受信、セマフォ取得など)。CPUを使わない
  • Suspended(停止):明示的に停止された状態
「リアルタイム」とは

RTOSの「リアルタイム」は「高速」という意味ではなく、「決められた時間内に必ず処理が終わる保証がある」という意味です。優先度設計を間違えなければ、高優先度タスクは必ず期限内に実行されます。これが Linux などの汎用OSとの決定的な違いです。

STM32CubeMXでFreeRTOSを有効化

STM32CubeMXでFreeRTOSを有効化する手順は非常にシンプルです。

  1. 左側ペインの 「Middleware and Software Packs」→「FREERTOS」 を選択
  2. 「Interface」プルダウンで CMSIS_V2 を選択(新規プロジェクトはV2推奨)
  3. 「Tasks and Queues」タブでデフォルトタスク(defaultTask)が自動生成されることを確認
  4. 「Config parameters」タブで TOTAL_HEAP_SIZE を必要量に設定(デフォルト3072バイト、最低でも8192バイトを推奨)
  5. 「Generate Code」で初期化コードを自動生成
重要:SysTick の置き換え

FreeRTOS有効時はHALのタイムベースに SysTick以外のタイマー(例:TIM6)を使う必要があります。CubeMXの「Pinout & Configuration」→「SYS」→「Timebase Source」で TIM6 等を選択してください。これを忘れるとHAL_Delayが正しく動作しません。

生成される主なファイル

MyProject/
├── Core/
│   ├── Src/
│   │   ├── main.c             // main()、各タスク関数の定義
│   │   ├── freertos.c         // FreeRTOS初期化、defaultTask
│   │   └── stm32xxxx_it.c     // 割り込みハンドラ(SysTick等)
│   └── Inc/
│       └── FreeRTOSConfig.h   // FreeRTOSの全設定
└── Middlewares/Third_Party/FreeRTOS/
    └── Source/                // FreeRTOSカーネルソース

最小構成のmain.c

CubeMXが生成するmain.cの末尾は概ね次のような形になります。

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART2_UART_Init();

    /* FreeRTOSの初期化 */
    osKernelInitialize();

    /* タスク作成(CubeMXで定義したものが自動生成される) */
    MX_FREERTOS_Init();

    /* スケジューラ起動(ここで戻ってこない) */
    osKernelStart();

    while (1) { }  // ここには到達しない
}

タスクの作成と管理

FreeRTOSの中核です。CMSIS-OS v2 では osThreadNew() でタスクを作成します。

タスクの作成

/* タスク関数の定義 */
void LedBlinkTask(void *argument)
{
    for (;;) {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        osDelay(500);  // 500ms 待機(この間 Blocked 状態でCPUを解放)
    }
}

/* タスク属性 */
const osThreadAttr_t led_task_attr = {
    .name       = "LedBlink",
    .stack_size = 256 * 4,           // 1KB(CMSIS-OS v2 はバイト単位)
    .priority   = osPriorityNormal,  // 優先度
};

/* タスク作成(main()やMX_FREERTOS_Init() 内で実行) */
osThreadId_t ledTaskHandle = osThreadNew(LedBlinkTask, NULL, &led_task_attr);

優先度設計の基本ルール

FreeRTOSの優先度は数値が大きいほど高優先度です。CMSIS-OS v2 では以下の定義済み定数を使います。

定数用途
osPriorityIdleアイドル時のみ動作する最低優先度(CPU使用率測定など)
osPriorityLowログ出力、LCD更新など、遅延が許容される処理
osPriorityNormalセンサー読み込み、通常の制御処理
osPriorityAboveNormalUART/SPI受信処理など、応答性が必要な処理
osPriorityHighモーター制御、リアルタイム性が重要な処理
osPriorityRealtime緊急停止、安全に関わる処理
設計指針

優先度は「応答性が必要なものほど高く」が原則。長時間CPUを占有する処理は低優先度にして、緊急処理に割り込まれるよう設計します。すべてHighにすると逆にプリエンプションが効かず、設計の意味がなくなります。

スタックサイズの目安

各タスクは独立したスタックを持つため、ヒープから割り当てられます。スタック不足はHardFaultやSegmentation Fault的なバグの温床になります。

  • シンプルな処理(LED、簡単なセンサー):128〜256ワード(512B〜1KB)
  • 中程度の処理(UART通信、状態管理):512ワード(2KB)
  • printfやfloat演算を含む処理:1024ワード以上(4KB〜)
  • FATFS / LwIP などのミドルウェア:1024〜2048ワード(4〜8KB)

実機で uxTaskGetStackHighWaterMark() を呼ぶと、各タスクの未使用スタック量が確認できます。余裕が256バイト未満なら危険信号です。

タスクの削除と停止

/* タスクの削除(自己削除も可能) */
osThreadTerminate(ledTaskHandle);

/* 一時停止 / 再開 */
osThreadSuspend(ledTaskHandle);
osThreadResume(ledTaskHandle);

/* 待機(自タスクをBlockedに) */
osDelay(1000);                       // 1000ms 待機
osDelayUntil(osKernelGetTickCount() + 100);  // 周期実行(時間ドリフトなし)

キューによるタスク間通信

タスクは独立して動くため、データを安全に受け渡すにはキュー(メッセージキュー)を使います。グローバル変数を直接読み書きすると、競合状態(Race Condition)が発生します。

キューの作成

/* キュー:センサー値(float型)を最大10個まで保持 */
osMessageQueueId_t sensorQueueHandle;

const osMessageQueueAttr_t sensor_queue_attr = {
    .name = "SensorQueue"
};

void MX_FREERTOS_Init(void)
{
    sensorQueueHandle = osMessageQueueNew(
        10,                  // キューサイズ(最大10要素)
        sizeof(float),       // 1要素のサイズ
        &sensor_queue_attr
    );
}

送信側タスク(センサー読み込み)

void SensorTask(void *argument)
{
    float temperature;
    for (;;) {
        /* I2Cセンサーから温度を読み込み */
        temperature = read_temperature_i2c();

        /* キューに送信(キュー満杯なら最大100msまで待機) */
        osMessageQueuePut(sensorQueueHandle, &temperature, 0, 100);

        osDelay(100);  // 100ms周期
    }
}

受信側タスク(ログ・通信)

void LoggerTask(void *argument)
{
    float received_value;
    char tx_buf[64];
    for (;;) {
        /* キューから受信(データが来るまで永久待機) */
        if (osMessageQueueGet(sensorQueueHandle, &received_value, NULL, osWaitForever) == osOK) {
            snprintf(tx_buf, sizeof(tx_buf), "Temp: %.2f C\r\n", received_value);
            HAL_UART_Transmit(&huart2, (uint8_t*)tx_buf, strlen(tx_buf), 100);
        }
    }
}
キュー設計のコツ
  • 1対1だけでなく多対多でも使える:複数の送信タスクから1つの受信タスクへ集約する設計が典型例
  • キューサイズはバッファとして設計:送信側のバースト性を吸収できる程度に余裕を持たせる(10〜32個が目安)
  • 大きな構造体はポインタ渡し:キューに大きな構造体を直接入れると重い。ポインタを渡してヒープを共有する設計が高速

セマフォ・ミューテックスによる同期

共有リソース(UART、I2C、SPIなどのペリフェラル)に複数タスクが同時アクセスすると競合します。これを防ぐのがセマフォミューテックスです。

ミューテックスによる排他制御

UARTを複数タスクから使う典型例です。ミューテックスを取得しないと他のタスクは待たされます。

osMutexId_t uartMutexHandle;

void MX_FREERTOS_Init(void)
{
    const osMutexAttr_t mutex_attr = { .name = "UartMutex" };
    uartMutexHandle = osMutexNew(&mutex_attr);
}

/* タスクA: ログ出力 */
void LogTask(void *argument)
{
    char buf[64];
    for (;;) {
        osMutexAcquire(uartMutexHandle, osWaitForever);  // 取得
        snprintf(buf, sizeof(buf), "[LOG] tick=%lu\r\n", osKernelGetTickCount());
        HAL_UART_Transmit(&huart2, (uint8_t*)buf, strlen(buf), 100);
        osMutexRelease(uartMutexHandle);                 // 解放
        osDelay(500);
    }
}

/* タスクB: エラー出力 */
void ErrorTask(void *argument)
{
    for (;;) {
        if (error_occurred) {
            osMutexAcquire(uartMutexHandle, osWaitForever);
            HAL_UART_Transmit(&huart2, (uint8_t*)"[ERR] FAULT\r\n", 13, 100);
            osMutexRelease(uartMutexHandle);
            error_occurred = 0;
        }
        osDelay(100);
    }
}

バイナリセマフォによる割り込み通知

「割り込みが発生したらタスクを起こす」という非同期通知の定番パターンです。タスク側は osSemaphoreAcquire で待機し、ISRから osSemaphoreRelease で起こします。

osSemaphoreId_t uartRxSemHandle;

void MX_FREERTOS_Init(void)
{
    uartRxSemHandle = osSemaphoreNew(1, 0, NULL);  // 最大1、初期0
}

/* UART受信タスク:セマフォが解放されるまで待つ */
void UartRxTask(void *argument)
{
    uint8_t rx_byte;
    HAL_UART_Receive_IT(&huart2, &rx_byte, 1);  // 1バイト受信割り込み開始

    for (;;) {
        if (osSemaphoreAcquire(uartRxSemHandle, osWaitForever) == osOK) {
            /* 受信処理 */
            process_uart_byte(rx_byte);
            HAL_UART_Receive_IT(&huart2, &rx_byte, 1);  // 次の受信
        }
    }
}

/* 受信完了割り込み(ISR) */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART2) {
        osSemaphoreRelease(uartRxSemHandle);  // タスクを起こす
    }
}
ミューテックス vs セマフォ
  • ミューテックス:排他制御専用。「優先度継承」機能があり、優先度逆転を防ぐ
  • バイナリセマフォ:イベント通知用。ISR→タスクの「合図」に使う
  • カウンティングセマフォ:複数の同種リソース管理に使う(例:プール内のバッファ数)

共有リソースの排他制御には必ずミューテックスを使ってください。バイナリセマフォで代用すると、優先度逆転が起こって高優先度タスクが止まる事故が起こります。

割り込みからのFreeRTOS API呼び出し

割り込みハンドラ(ISR)の中でFreeRTOS APIを呼ぶには、専用の「FromISR」版APIを使う必要があります。通常版APIを使うと予期せぬクラッシュが起こります。

ISR内で使えるCMSIS-OS v2 API

CMSIS-OS v2 ではAPI名が同じで内部的に ISR を判定してくれるものが多い反面、明示的にISR専用関数を使うべき場面もあります。ネイティブFreeRTOS APIの場合は明確に分離されています。

/* CMSIS-OS v2(osXxx)はISR/タスクの両方から呼べる
   ただし osWaitForever などのブロッキングはISRから使ってはいけない */

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == BUTTON_Pin) {
        /* タイムアウト 0 を指定 = ブロックしない */
        osSemaphoreRelease(buttonSemHandle);

        /* キュー送信もタイムアウト 0 で使う */
        uint32_t event = EVENT_BUTTON_PRESSED;
        osMessageQueuePut(eventQueueHandle, &event, 0, 0);
    }
}

割り込み優先度の重要な設定

STM32の NVIC 優先度と FreeRTOSの優先度設定は必ず連動させる必要があります。FreeRTOSConfig.h の以下の設定を確認してください。

/* FreeRTOSConfig.h */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY  5  // この値以上の優先度で FromISR API が使える
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY      15

/* CubeMXで割り込みを設定するときの NVIC Priority Group は 4 ビット プリエンプション に設定すること */
よくあるトラブル

UART受信割り込みなどのNVIC優先度を 0 や 1(最高)に設定すると、その割り込み内からFreeRTOS APIを呼んだ瞬間にHardFaultします。STM32の割り込みは configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(デフォルト5)以上の値 に設定する必要があります(数値が大きい=優先度が低い)。

よくある落とし穴と対策

症状原因対策
起動直後にHardFault SysTickがHALタイムベースのままでFreeRTOSと衝突 CubeMXで Timebase Source を TIM6 等の汎用タイマーに変更
タスクが時々止まる / 暴走 スタックオーバーフロー configCHECK_FOR_STACK_OVERFLOW を2に設定。uxTaskGetStackHighWaterMark() で実測。スタックを増量
高優先度タスクが応答しない 優先度逆転(低優先度タスクが共有リソースを保持中) 排他制御にはセマフォではなく ミューテックス を使う(優先度継承機能で逆転を回避)
osThreadNewでNULLが返る FreeRTOSヒープ(TOTAL_HEAP_SIZE)不足 FreeRTOSConfig.hconfigTOTAL_HEAP_SIZE を増量(8KB→16KB等)
ISR内でAPI呼出時にHardFault NVIC優先度が configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY より高い 該当割り込みのNVIC優先度を5以上に設定(CubeMXのNVICタブで確認)
キュー受信タスクが永久に待つ 送信側がキューに入れていない / 別のキューハンドルを使っている ハンドル変数のスコープ・初期化順序を確認。osMessageQueueGetCount() でデバッグ
printfが出力されない / 出力が混ざる 複数タスクからUARTに同時アクセス UART送信を必ずミューテックスで保護する。ログ専用タスクを1つ作ってキュー経由に統一するのも良い
デバッグの定石
  • STM32CubeIDE の FreeRTOS-aware デバッグ:「Window → Show View → FreeRTOS Task List」で全タスクの状態・スタック使用量がリアルタイム表示できる
  • configUSE_TRACE_FACILITY を 1 に:タスクごとの実行時間・CPU使用率が取得できるようになる
  • Tracealyzer (Percepio):商用ツールだがタスクスイッチを可視化できる。難しい競合バグはこれで一気に見える化

まとめ

この記事のまとめ

  • FreeRTOSはSTM32CubeMXに標準搭載されており、追加コストなしで導入できる
  • 「タスク」「キュー」「セマフォ/ミューテックス」の3つを押さえれば実用的なRTOSアプリが書ける
  • 優先度設計は「応答性が必要なほど高く」が原則。全部Highにすると意味が無い
  • 共有リソースの排他制御は必ずミューテックスを使う(優先度逆転を回避)
  • ISRからAPIを呼ぶときはNVIC優先度を5以上にすること(HardFault対策)

STM32 + FreeRTOSを使った組込みシステム開発でお困りのことがあれば、弊社にお気軽にご相談ください。設計から実装・量産対応まで、マイコン開発歴20年超のエンジニアが一貫してサポートします。

STM32 + FreeRTOS の試作・量産設計、既存スーパーループ機器の RTOS 化、タイミング問題の調査まで 組込み制御・マイコン開発 として対応しています。

/この内容の受託開発・PoC相談はこちら\

STM32 + FreeRTOS の受託開発・量産対応

RTOSタスク設計・割込み連携・低消費電力化・量産デバッグまで、STM32開発歴20年超のエンジニアが一貫対応します。試作のPoCから量産フェーズまでお任せください。

よくある質問(FAQ)

STM32でFreeRTOSはどうやって導入しますか?

STM32CubeMXのMiddlewareでFreeRTOS(CMSIS-OS v2)を有効化すると、初期化コードとタスク生成のひな型が自動生成されます。追加ライブラリの購入は不要で、STM32CubeIDEに標準で含まれています。

タスク・キュー・セマフォはどう使い分けますか?

タスクは独立した処理単位(センサー読み込み・LCD更新など)、キューはタスク間で安全にデータを受け渡す仕組み、セマフォ/ミューテックスは共有リソースの排他制御や割り込みとの同期に使います。複数処理の並行実行はタスク、データ授受はキュー、競合防止はミューテックスが基本です。

スーパーループからFreeRTOSに移行するメリットは?

機能が増えても処理の遅延や取りこぼしが起きにくくなります。優先度設計により応答性が必要な処理を確実に実行でき、コードもタスク単位で見通しよく保守できます。実際にセンサー監視・通信・表示・制御を並行させる構築例は 実践編:STM32+FreeRTOSでリアルタイムセンサー監視システムを構築する で紹介しています。

割り込みハンドラからFreeRTOSのAPIを呼んでも大丈夫ですか?

ISRからは末尾が FromISR のAPI(xQueueSendFromISR() 等)のみを使用し、NVIC優先度を configMAX_SYSCALL_INTERRUPT_PRIORITY 以上(数値が大きい=低優先)に設定する必要があります。これを守らないとHardFaultの原因になります。

次に読む|STM32 シリーズ記事