はじめに:なぜ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を有効化する手順は非常にシンプルです。
- 左側ペインの 「Middleware and Software Packs」→「FREERTOS」 を選択
- 「Interface」プルダウンで CMSIS_V2 を選択(新規プロジェクトはV2推奨)
- 「Tasks and Queues」タブでデフォルトタスク(defaultTask)が自動生成されることを確認
- 「Config parameters」タブで TOTAL_HEAP_SIZE を必要量に設定(デフォルト3072バイト、最低でも8192バイトを推奨)
- 「Generate Code」で初期化コードを自動生成
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); // タスクを起こす
}
}
- ミューテックス:排他制御専用。「優先度継承」機能があり、優先度逆転を防ぐ
- バイナリセマフォ:イベント通知用。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 化、タイミング問題の調査まで 組込み制御・マイコン開発 として対応しています。
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の原因になります。