# STM32 + FreeRTOSでリアルタイムセンサー監視システムを構築する方法

URL: https://technosphere.co.jp/de-blog/2026-04-24-stm32-freertos-sensor-monitoring

## はじめに：なぜFreeRTOSが必要なのか？

組込みシステムで複数のセンサーを同時に監視したい——そんなとき、ベアメタル（OSなし）で書いたコードでは限界が出てきます。

たとえば、温度センサーの読み取り中にCAN通信の受信を取りこぼしたり、LCD表示の更新中にアラーム判定が遅れたり。**複数の処理を確実に並行実行するには、リアルタイムOS（RTOS）が有効**です。

本記事では、STM32マイコンとFreeRTOSを使って、リアルタイムセンサー監視システムを構築する方法を、実装コード付きで解説します。

> **FreeRTOSの基礎から学びたい方へ**：タスク・キュー・セマフォ・ミューテックスといったFreeRTOSの基本概念は、技術コラムの [STM32 + FreeRTOS 入門｜タスク・キュー・セマフォを使ったRTOS実装ガイド](/blog/stm32-freertos-intro) で体系的に解説しています。本記事はその実践編にあたります。

---

## 本記事で構築するシステム

### 構成図

```
[温度センサー] ──I2C──┐
[湿度センサー] ──I2C──┤
[振動センサー] ──ADC──┤── STM32F4 + FreeRTOS ──UART──→ [ログ出力]
[CAN Bus]     ──CAN──┤                      ──CAN───→ [上位システム]
                      └── LCD表示
```

### タスク構成

| タスク名 | 優先度 | 周期 | 役割 |
|---------|--------|------|------|
| SensorReadTask | High | 100ms | I2C/ADCセンサー読み取り |
| AlarmCheckTask | High | 200ms | 閾値判定・アラーム発報 |
| CANCommTask | Medium | 50ms | CAN通信の送受信 |
| DisplayTask | Low | 500ms | LCD画面更新 |
| LogTask | Low | 1000ms | UARTシリアルログ出力 |

---

## 環境構築

### 必要なもの

- **マイコンボード**: STM32F407 Discovery（またはNucleo-F446RE等）
- **開発環境**: STM32CubeIDE 1.15+
- **ミドルウェア**: FreeRTOS（CMSIS-RTOS v2 API）
- **センサー**: BME280（温湿度・気圧）、ADXL345（振動）など

### STM32CubeMXの設定

1. **Middleware → FREERTOS** を有効化（CMSIS_V2を選択）
2. **I2C1** を有効化（センサー接続用）
3. **ADC1** を有効化（振動センサー用）
4. **CAN1** を有効化（上位システム通信用）
5. **USART2** を有効化（デバッグログ用）

FreeRTOSの設定で、タスクのスタックサイズは各タスク最低256ワード（1KB）を確保しておきます。

---

## 実装のポイント

### 1. タスク間データ共有にはキュー（Queue）を使う

グローバル変数でのデータ共有は競合状態（Race Condition）の原因になります。FreeRTOSのキューを使えば、タスク間のデータ受け渡しをスレッドセーフに行えます。

```c
// センサーデータ構造体
typedef struct {
    float temperature;
    float humidity;
    float pressure;
    float vibration;
    uint32_t timestamp;
} SensorData_t;

// キューハンドル
osMessageQueueId_t sensorQueueHandle;

// キュー作成（main.c の初期化部分）
const osMessageQueueAttr_t sensorQueue_attr = {
    .name = "sensorQueue"
};
sensorQueueHandle = osMessageQueueNew(16, sizeof(SensorData_t), &sensorQueue_attr);
```

### 2. センサー読み取りタスク

```c
void SensorReadTask(void *argument)
{
    SensorData_t data;

    for (;;)
    {
        // BME280から温湿度・気圧を取得（I2C）
        data.temperature = BME280_ReadTemperature(&hi2c1);
        data.humidity    = BME280_ReadHumidity(&hi2c1);
        data.pressure    = BME280_ReadPressure(&hi2c1);

        // ADXL345から振動値を取得（ADC）
        HAL_ADC_Start(&hadc1);
        HAL_ADC_PollForConversion(&hadc1, 10);
        uint32_t raw = HAL_ADC_GetValue(&hadc1);
        data.vibration = (float)raw * 3.3f / 4096.0f;

        // タイムスタンプ
        data.timestamp = osKernelGetTickCount();

        // キューに送信（アラームタスク・表示タスクが受信）
        osMessageQueuePut(sensorQueueHandle, &data, 0, 0);

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

### 3. アラーム判定タスク

```c
#define TEMP_THRESHOLD_HIGH  40.0f  // 40℃以上で警告
#define VIBRATION_THRESHOLD  2.5f   // 2.5V以上で異常振動

void AlarmCheckTask(void *argument)
{
    SensorData_t data;

    for (;;)
    {
        // キューからデータ受信（最大200ms待機）
        if (osMessageQueueGet(sensorQueueHandle, &data, NULL, 200) == osOK)
        {
            // 温度異常チェック
            if (data.temperature > TEMP_THRESHOLD_HIGH)
            {
                CAN_SendAlarm(ALARM_TEMP_HIGH, data.temperature);
                HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_SET);
            }

            // 振動異常チェック
            if (data.vibration > VIBRATION_THRESHOLD)
            {
                CAN_SendAlarm(ALARM_VIBRATION, data.vibration);
                HAL_GPIO_WritePin(LED_ORANGE_GPIO_Port, LED_ORANGE_Pin, GPIO_PIN_SET);
            }
        }
    }
}
```

### 4. ミューテックスでI2Cバスを保護する

複数タスクが同じI2Cバスにアクセスする場合、ミューテックスで排他制御が必要です。

```c
osMutexId_t i2cMutexHandle;

// 初期化
const osMutexAttr_t i2cMutex_attr = {
    .name = "i2cMutex",
    .attr_bits = osMutexRecursive | osMutexPrioInherit
};
i2cMutexHandle = osMutexNew(&i2cMutex_attr);

// I2Cアクセス時にミューテックス取得
float BME280_ReadTemperature_Safe(I2C_HandleTypeDef *hi2c)
{
    float temp = 0.0f;
    if (osMutexAcquire(i2cMutexHandle, osWaitForever) == osOK)
    {
        temp = BME280_ReadTemperature(hi2c);
        osMutexRelease(i2cMutexHandle);
    }
    return temp;
}
```

`osMutexPrioInherit`（優先度継承）を指定することで、**優先度逆転問題**を防止できます。

---

## よくあるトラブルと対策

### スタックオーバーフロー

FreeRTOSタスクのスタックサイズが不足すると、HardFault例外が発生します。

**対策：**
- `configCHECK_FOR_STACK_OVERFLOW` を `2` に設定してオーバーフロー検出を有効化
- `uxTaskGetStackHighWaterMark()` で使用量を確認し、余裕を持ったサイズを設定

```c
// FreeRTOSConfig.h
#define configCHECK_FOR_STACK_OVERFLOW 2

// コールバック関数
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
    printf("Stack overflow in task: %s\n", pcTaskName);
    // LED点滅などでエラー通知
    while(1);
}
```

### タスク優先度の設計ミス

優先度が高いタスクがCPUを占有し続けると、低優先度タスクが実行されません。

**対策：**
- センサー読み取り・アラーム判定は**High**、表示やログは**Low**
- 必ず`osDelay()`や`osMessageQueueGet()`でブロッキング状態を作り、CPUを解放する
- `vTaskDelayUntil()`で正確な周期実行を実現

### ヒープメモリ不足

キューやミューテックスの生成に失敗する場合は、ヒープサイズが不足しています。

```c
// FreeRTOSConfig.h
#define configTOTAL_HEAP_SIZE ((size_t)32768)  // 32KB に拡張
```

---

## ベアメタルとFreeRTOSの比較

| 観点 | ベアメタル | FreeRTOS |
|------|----------|----------|
| 複数処理の並行実行 | 割り込み+フラグで手動管理 | タスクとして自然に分離 |
| タイミング制御 | タイマー割り込みで実装 | osDelay/vTaskDelayUntilで簡潔 |
| データ共有 | グローバル変数（競合リスクあり） | キュー/セマフォで安全 |
| コードの保守性 | 処理が密結合しがち | タスク単位でモジュール化 |
| デバッグ | 困難（タイミング依存バグ） | タスク状態の可視化が可能 |
| メモリ使用量 | 最小限 | +10〜20KB程度 |

センサー2〜3個程度のシンプルな構成ならベアメタルで十分ですが、**4つ以上の並行処理や、CAN通信のようなリアルタイム性が求められる場合はFreeRTOSの導入メリットが大きい**です。

---

## まとめ

STM32 + FreeRTOSによるリアルタイムセンサー監視システムの構築方法を解説しました。

**ポイントの振り返り：**
1. **タスク間データ共有はキュー（Queue）** を使い、グローバル変数を避ける
2. **共有リソース（I2Cバス等）はミューテックス**で保護する
3. **優先度継承**を有効にして優先度逆転を防止する
4. **スタックサイズは余裕を持って設定**し、オーバーフロー検出を有効化する

テクノスフィアでは、STM32をはじめとするマイコンの組込み開発を20年以上手がけてきました。FreeRTOSを活用したリアルタイムシステムの設計・実装も多数の実績があります。

組込みシステムの開発でお困りのことがあれば、[お気軽にご相談ください](/contact)。

**関連記事：**
- [STM32 + FreeRTOS 入門｜タスク・キュー・セマフォを使ったRTOS実装ガイド](/blog/stm32-freertos-intro) — FreeRTOSの基礎を体系的に解説
- [STM32マイコンのUART・I2C・SPI・CAN通信 実装ガイド](/blog/stm32-uart-i2c-can) — 通信ペリフェラルの使い分けと実装
- [組込み制御・マイコン開発サービス](/embedded) — STM32受託開発のご相談
