# FreeRTOS入門｜STM32で学ぶタスク・キュー・セマフォの基本【サンプルコード付き】｜テクノスフィア

> FreeRTOS入門者向けに、STM32CubeMXでの有効化からタスク・キュー・セマフォ・ミューテックスの使い方までRTOS開発の基本を解説。CMSIS-OS v2ベースのCサンプルコードを多数掲載。組込み開発歴20年超のエンジニアが実務目線で解説します。

URL: https://technosphere.co.jp/blog/stm32-freertos-intro

** 目次
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通信 実装ガイド](https://technosphere.co.jp/blog/stm32-uart-i2c-can) をご覧ください。  ** 弊社での活用実績

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

## 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` | センサー読み込み、通常の制御処理 |
| `osPriorityAboveNormal` | UART/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.h` の `configTOTAL_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 化、タイミング問題の調査まで [組込み制御・マイコン開発](https://technosphere.co.jp/embedded) として対応しています。   ／この内容の受託開発・PoC相談はこちら＼

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

RTOSタスク設計・割込み連携・低消費電力化・量産デバッグまで、STM32開発歴20年超のエンジニアが一貫対応します。試作のPoCから量産フェーズまでお任せください。  [組込み開発サービスを見る](https://technosphere.co.jp/embedded) [RTOS設計・量産相談](https://technosphere.co.jp/contact)

## よくある質問（FAQ）   ** STM32でFreeRTOSはどうやって導入しますか？

STM32CubeMXのMiddlewareでFreeRTOS（CMSIS-OS v2）を有効化すると、初期化コードとタスク生成のひな型が自動生成されます。追加ライブラリの購入は不要で、STM32CubeIDEに標準で含まれています。   ** タスク・キュー・セマフォはどう使い分けますか？

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

機能が増えても処理の遅延や取りこぼしが起きにくくなります。優先度設計により応答性が必要な処理を確実に実行でき、コードもタスク単位で見通しよく保守できます。実際にセンサー監視・通信・表示・制御を並行させる構築例は [実践編：STM32+FreeRTOSでリアルタイムセンサー監視システムを構築する](https://technosphere.co.jp/de-blog/2026-04-24-stm32-freertos-sensor-monitoring) で紹介しています。   ** 割り込みハンドラからFreeRTOSのAPIを呼んでも大丈夫ですか？

ISRからは末尾が `FromISR` のAPI（`xQueueSendFromISR()` 等）のみを使用し、NVIC優先度を `configMAX_SYSCALL_INTERRUPT_PRIORITY` 以上（数値が大きい＝低優先）に設定する必要があります。これを守らないとHardFaultの原因になります。      # STM32 # FreeRTOS # RTOS # CMSIS-OS # タスク # セマフォ # マイコン開発 # 組込み制御

##   次に読む｜STM32 シリーズ記事
-  [STM32 シリーズ STM32マイコンのUART・I2C・SPI・CAN通信 実装ガイド HALライブラリで通信ペリフェラルを使いこなす定番ガイド](https://technosphere.co.jp/blog/stm32-uart-i2c-can)
-  [STM32 シリーズ STM32 DMA 完全ガイド｜UART・SPI・ADCとの連携 CPU負荷を最小化するDMA活用の決定版](https://technosphere.co.jp/blog/stm32-dma-complete-guide)
-  [開発実績 組込み制御・マイコン開発 実績紹介 FreeRTOS採用案件含む多数の受託開発事例](https://technosphere.co.jp/embedded-achievements)
-  [STM32 シリーズ STM32 低消費電力モード完全ガイド バッテリー駆動を年単位で実現する設計ノウハウ](https://technosphere.co.jp/blog/stm32-low-power-battery)
    [技術コラム一覧に戻る](https://technosphere.co.jp/blog/)
