STM32で始めるFreeRTOS:リアルタイムOSの基礎と実践的な使い方
組み込みシステム開発の世界へようこそ。家電、自動車、産業機器など、私たちの身の回りには無数の組み込みシステムが存在します。これらのシステムは、限られたリソースの中で、複雑な処理を効率的に、そして定められた時間内に実行する必要があります。特に、複数の機能を同時に処理したり、外部からのイベントに迅速に対応したりする場合、プログラムの設計は複雑になりがちです。
このような課題を解決するために利用されるのが、リアルタイムOS(RTOS)です。RTOSは、タスク管理、タスク間の通信と同期、資源の共有といった機能を提供し、組み込みアプリケーションの開発を強力にサポートします。中でもFreeRTOSは、軽量かつ高性能、そして豊富なハードウェアへのポーティング実績を持つオープンソースのRTOSとして、世界中のエンジニアに広く利用されています。
そして、組み込み開発で最もよく使われるマイクロコントローラーの一つが、STMicroelectronicsのSTM32シリーズです。高性能なARM Cortex-Mコアを搭載し、豊富なペリフェラルを備えたSTM32は、FreeRTOSを動作させるプラットフォームとして非常に適しています。
この記事では、STM32マイコンをターゲットに、FreeRTOSの基礎から実践的な使い方までを詳細に解説します。RTOSとは何かという基本的な概念から始まり、FreeRTOSの主要機能、STM32CubeIDEを使ったFreeRTOSプロジェクトの作成方法、タスク管理、タスク間通信と同期の実現方法、さらには応用的なトピックやトラブルシューティングまで、FreeRTOSを使った組み込み開発に必要な知識を網羅します。
この記事を読み終える頃には、STM32上でFreeRTOSを使ったマルチタスクプログラミングの基礎を習得し、より複雑な組み込みアプリケーションを設計・実装できるようになっているはずです。
さあ、FreeRTOSとSTM32の世界へ一歩踏み出しましょう。
第1章: 組み込みシステムとRTOSの基礎
1.1 組み込みシステムの特徴と課題
組み込みシステムは、特定の機能を実現するために機器に組み込まれたコンピュータシステムです。その特徴として、以下の点が挙げられます。
- リソース制約: CPU性能、メモリ容量、ストレージ容量などが限られていることが多い。
- リアルタイム性: 外部からのイベントに対して、定められた時間内に応答する必要がある(ハードリアルタイム、ソフトリアルタイム)。
- 電力制約: バッテリー駆動の場合など、低消費電力が求められる。
- 信頼性・安全性: 長期間安定して動作することが求められ、故障や誤動作が許されない場合が多い。
- 多様なペリフェラル: GPIO、UART、SPI、I2C、ADC、DACなど、様々なハードウェアペリフェラルを制御する必要がある。
これらの特徴を持つ組み込みシステムにおいて、複数の異なる処理(例:センサーからのデータ取得、ユーザーインターフェースの操作受付、通信処理、制御アルゴリズムの実行)を同時に実行したいという要求はよく発生します。
1.2 ベアメタルプログラミングの限界
RTOSを使用しない「ベアメタルプログラミング」では、通常、一つの大きなwhile(1)
ループの中で、ポーリングや状態遷移マシンを駆使して複数の処理を切り替えながら実行します。割り込みハンドラを使って非同期イベントに対応することも一般的です。
しかし、処理が増えたり複雑になったりすると、ベアメタルプログラミングでは以下のような問題が発生しやすくなります。
- コードの複雑化: 複数の処理を協調させるためのフラグ管理や状態管理が煩雑になる。
- 応答性の問題: 一つの処理に時間がかかると、他の処理や外部イベントへの応答が遅れる(特にポーリングの場合)。
- 機能追加・変更の困難さ: 後から機能を追加したり、既存の機能を変更したりする際に、全体への影響が大きく、デバッグが困難になる。
- 開発効率の低下: 低レベルなハードウェア制御とアプリケーションロジックが混在し、開発効率が上がらない。
1.3 RTOSの役割とメリット
リアルタイムOS (RTOS) は、これらのベアメタルプログラミングの限界を克服するために設計されています。RTOSの主な役割は以下の通りです。
- タスク管理: アプリケーションを複数の独立した「タスク」に分割し、それぞれのタスクを実行・管理します。
- スケジューリング: どのタスクをいつ実行するかを決定し、CPU時間を割り当てます。これにより、複数のタスクがあたかも同時に実行されているかのように見えます。
- タスク間通信 (IPC – Inter-Process Communication): タスク間でデータや情報を安全にやり取りする仕組み(キューなど)を提供します。
- タスク間同期: 複数のタスクが特定のイベントを待ったり、処理順序を調整したりするための仕組み(セマフォ、イベントフラグなど)を提供します。
- 資源管理: 複数のタスクが共有するハードウェア資源やソフトウェア資源(グローバル変数など)へのアクセスを制御し、競合を防ぎます(ミューテックスなど)。
- タイマー管理: 一定時間後の処理実行や、周期的な処理実行をサポートします(ソフトウェアタイマー)。
RTOSを導入することで、以下のようなメリットが得られます。
- 構造化された設計: アプリケーションを論理的に分割されたタスクとして実装できるため、コードの見通しが良くなり、設計が容易になります。
- 開発生産性の向上: RTOSがタスク管理や通信・同期の低レベルな部分を担うため、開発者はアプリケーションロジックの実装に集中できます。
- 応答性の向上: スケジューラがタスクの優先度や待ち状態に基づいてCPUを割り当てるため、重要なタスクや待ち状態から解放されたタスクが迅速に実行されます。
- 保守性・再利用性の向上: 各タスクが独立性が高いため、機能の追加・変更が容易になり、また特定のタスクを他のプロジェクトで再利用しやすくなります。
- デバッグの容易さ: RTOSが提供する情報(タスクリスト、タスクの状態など)を活用することで、システムの挙動を把握しやすくなり、デバッグ効率が向上します。
1.4 FreeRTOSの基本的な概念
FreeRTOSを始めるにあたって、まず理解しておくべき基本的な概念をいくつか紹介します。
- タスク (Task): RTOSにおける実行単位です。アプリケーションの各機能は通常、一つのタスクとして実装されます。各タスクは独自のスタックを持ち、独立した実行コンテキストを持ちます。
- スケジューラ (Scheduler): どのタスクにCPUの実行時間を割り当てるかを決定・管理するFreeRTOSの中核部分です。FreeRTOSは通常、優先度ベースのプリエンプティブスケジューリングを行います。
- カーネル (Kernel): スケジューリングやタスク管理、タスク間通信・同期などのRTOSの基本機能を提供する中核コードです。
- キュー (Queue): タスク間でデータやメッセージを安全に受け渡すための最も一般的な機構です。FIFO(First-In, First-Out)のバッファとして機能します。
- セマフォ (Semaphore): タスク間の同期や、リソースへのアクセス数を制限するために使用されます。バイナリセマフォ(0または1の状態)は、イベント通知やミューテックスの簡易版として使われます。カウンティングセマフォは、複数のリソースインスタンスへのアクセス制御などに使われます。
- ミューテックス (Mutex): 共有リソース(グローバル変数、ペリフェラルなど)への排他制御を行うために使用されます。一度に一つのタスクのみがミューテックスを獲得でき、リソースへのアクセスを保護します。優先度逆転問題を回避するための優先度継承メカニズムを持つのが一般的です。
- ソフトウェアタイマー (Software Timer): ハードウェアタイマーとは異なり、FreeRTOSカーネルによって管理されるタイマーです。指定した時間後に一度だけ(ワンショットタイマー)、または周期的に(周期タイマー)、登録されたコールバック関数を実行します。
これらの基本的な概念を理解することが、FreeRTOSを使ったアプリケーション開発の第一歩となります。
第2章: FreeRTOSのアーキテクチャとコア機能
FreeRTOSは、その軽量さと移植性の高さが特徴です。これは、カーネルの設計がシンプルであることと、ハードウェア依存の部分が「ポーティング層 (Port Layer)」として分離されていることによります。
2.1 FreeRTOSの全体像
FreeRTOSは大きく以下の要素から構成されます。
- カーネル (Kernel): FreeRTOSの中核であり、タスク管理、スケジューリング、タスク間通信・同期などの汎用的な機能を提供します。この部分はハードウェアに依存しないように記述されています。
- ポーティング層 (Port Layer): FreeRTOSカーネルを特定のCPUアーキテクチャやコンパイラ、ハードウェア(特にSysTickタイマーなどのシステムタイマー)に適応させるための層です。コンテキストスイッチの具体的な実装や、システムティックタイマーの設定などが含まれます。STM32はARM Cortex-Mコアを搭載しており、FreeRTOSはARM Cortex-M用のポーティング層を提供しています。
- FreeRTOSConfig.h: アプリケーション固有の設定(タスクの最大優先度、ヒープサイズ、ティックレートなど)を定義するヘッダーファイルです。FreeRTOSの動作をカスタマイズするために非常に重要です。
- オプションコンポーネント: ソフトウェアタイマー、イベントグループ、ストリームバッファ、メッセージバッファなど、基本的なカーネル機能に追加される機能です。
2.2 タスク (Tasks)
タスクはFreeRTOSの基本的な実行単位です。各タスクは以下を持ちます。
- タスク関数: タスクとして実行されるC言語の関数です。通常は無限ループ (
while(1)
) を含み、終了しない設計になります。 - スタック: タスク関数内のローカル変数、関数呼び出し時のリターンアドレスなどが格納されるメモリ領域です。タスクごとに独立したスタックが必要です。
- 優先度: スケジューラがタスクの実行順序を決定する際に使用します。数値が大きいほど優先度が高いことが一般的です(FreeRTOSでは
0
が最低優先度)。 - タスク制御ブロック (TCB – Task Control Block): FreeRTOSカーネルがタスクの状態(実行中、待機中、準備完了など)、スタックポインタ、優先度などの情報を管理するために使用するデータ構造です。
タスクの状態遷移:
タスクは以下のいずれかの状態にあります。
- Running (実行中): CPU上で現在実行されているタスク。常に1つのタスクのみがこの状態です(SMP構成を除く)。
- Ready (準備完了): 実行可能であるが、より優先度の高いタスクが実行中であるか、または同じ優先度のタスクにCPU時間が割り当てられていないため、実行待ちの状態にあるタスク。
- Blocked (待機中): イベント(キューからのデータ受信、セマフォの獲得、指定時間の経過など)が発生するまで実行を中断しているタスク。待機条件が満たされると、Ready状態に遷移します。
- Suspended (中断):
vTaskSuspend()
によって明示的に中断されたタスク。スケジューラによって管理されず、再開するにはvTaskResume()
またはxTaskResumeFromISR()
を呼び出す必要があります。
タスクの生成と削除:
タスクは通常、xTaskCreate()
APIを使って生成されます。
c
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // タスク関数へのポインタ
const char * const pcName, // タスク名 (デバッグ用)
configSTACK_DEPTH_TYPE usStackDepth, // スタックサイズ (word単位)
void * const pvParameters, // タスク関数に渡すパラメータ
UBaseType_t uxPriority, // タスクの優先度
TaskHandle_t * const pxCreatedTask // 生成されたタスクのハンドルを格納する変数へのポインタ (NULLでも可)
);
タスクがその役割を終えた場合、vTaskDelete()
APIを使って削除できます。しかし、多くの組み込みアプリケーションではタスクは無限ループで実行され、明示的に削除されることは少ないです。
2.3 スケジューラ (Scheduler)
FreeRTOSのスケジューラは、通常、固定優先度ベースのプリエンプティブスケジューリングを行います。
- 固定優先度: 各タスクには静的に定義された優先度があります。
- プリエンプティブ: より優先度の高いタスクがReady状態になると、現在実行中のタスク(たとえ処理途中であっても)は中断され、より優先度の高いタスクが即座に実行されます。
- ラウンドロビン: 同じ優先度のタスクが複数Ready状態にある場合、通常はタイムスライシングによって、一定時間ごとにタスクが切り替わりながら実行されます。これは
configUSE_PREEMPTION
とconfigUSE_TIME_SLICING
の設定に依存します。
スケジューラの動作は、システムティックタイマーによって定期的に発生する割り込み(ティック割り込み)や、タスクが待機状態に遷移する、あるいはタスクの優先度が変更されるといったイベントによって駆動されます。
スケジューラは vTaskStartScheduler()
APIを呼び出すことで開始されます。この関数は通常、最初のタスクへのコンテキストスイッチを行い、アプリケーションの制御をFreeRTOSに渡します。スケジューラが開始されると、vTaskStartScheduler()
は戻りません(エラー時を除く)。
2.4 タスク間の通信と同期 (IPC)
複数のタスクが協調して動作するためには、情報のやり取りや処理タイミングの調整が必要です。FreeRTOSは様々なIPC/同期メカニズムを提供します。
-
キュー (Queue):
- タスク間で固定長のメッセージをコピーして送受信するための機能です。
- FIFO (First-In, First-Out) 形式でデータを出し入れします。
- 複数のタスクから同じキューに送信したり、同じキューから受信したりできます。
- 割り込みハンドラからも安全に使用できるAPIが提供されています (例:
xQueueSendFromISR
)。 - 使い方:
xQueueCreate()
で生成、xQueueSend()
で送信、xQueueReceive()
で受信。
-
セマフォ (Semaphore):
- タスク間の同期や、共有リソースへのアクセス数のカウントに使用されます。
- FreeRTOSでは、バイナリセマフォ(カウンタが0または1)とカウンティングセマフォ(カウンタが0から任意の値)が提供されます。
- バイナリセマフォ: イベントの通知(例: 割り込みハンドラからタスクへ)、あるいはミューテックスの簡易版として使用されます。
xSemaphoreGive()
でセマフォを放出し(カウンタ+1)、xSemaphoreTake()
でセマフォを獲得します(カウンタ-1)。カウンタが0の時にxSemaphoreTake()
を呼び出すと、タスクはブロックされます。 - カウンティングセマフォ: 複数の同一リソースへのアクセスを管理する場合などに使用されます。
- 使い方:
xSemaphoreCreateBinary()
またはxSemaphoreCreateCounting()
で生成、xSemaphoreGive()
で放出、xSemaphoreTake()
で獲得。ISRからはxSemaphoreGiveFromISR()
を使用。
-
ミューテックス (Mutex):
- 共有リソースへの排他制御(一度に一つのタスクのみがアクセスできるようにする)を目的とした特殊なバイナリセマフォです。
- 所有権の概念があり、ミューテックスを獲得したタスクのみがそれを解放できます。
- 優先度継承 (Priority Inheritance) メカニズムを持つことが多いです(FreeRTOSの設定
configUSE_MUTEXES
とconfigUSE_PRIORITY_INHERITANCE
による)。これは、優先度の低いタスクがミューテックスを保持している間に、より優先度の高いタスクがそのミューテックスを待ってブロックされた場合、一時的に低い優先度のタスクの優先度を高いタスクと同じに引き上げることで、優先度逆転問題を回避する仕組みです。 - 使い方:
xSemaphoreCreateMutex()
で生成、xSemaphoreTake()
で獲得、xSemaphoreGive()
で解放。
-
イベントグループ (Event Groups):
- 複数のイベント(ビットフラグ)の組み合わせをタスクが待つための機能です。
- 例えば、「イベントAとイベントBの両方が発生したら処理を行う」といった条件同期を実現できます。
- 使い方:
xEventGroupCreate()
で生成、xEventGroupSetBits()
でビットフラグをセット(イベント発生を通知)、xEventGroupWaitBits()
で指定したビットフラグの組み合わせが発生するまで待機。ISRからもxEventGroupSetBitsFromISR()
を使用。
これらのIPC/同期オブジェクトは、タスクを効率的に連携させるために不可欠です。どのメカニズムを選択するかは、タスク間で何を共有・同期したいかによって異なります。データの受け渡しならキュー、イベント通知ならバイナリセマフォまたはイベントグループ、排他制御ならミューテックスが適しています。
2.5 メモリ管理
FreeRTOSは、タスクスタックやキュー、セマフォなどのRTOSオブジェクトを動的に確保するためにメモリ(ヒープ領域)を使用します。FreeRTOSはいくつかの異なるメモリ管理スキームを提供しています。
- Heap_1:
malloc
/free
なしの非常にシンプルな静的割り当て。FreeRTOSカーネル開始前に全メモリを割り当てます。 - Heap_2:
free
を持たないシンプルな動的割り当て。メモリは割り当てられるだけで解放されません。 - Heap_3: 標準Cライブラリの
malloc
/free
を使用します。スレッドセーフではないmalloc
/free
を使用する場合、FreeRTOSがミューテックスで保護します。 - Heap_4: 断片化を防ぐための非常に基本的な「ファーストフィット」アルゴリズムと、結合(coalescing)機能を持ちます。最もよく使われるスキームの一つです。リンク時に複数のメモリ領域を指定することも可能です。
- Heap_5: Heap_4に似ていますが、複数の離散的なメモリ領域を動的なヒープとして使用できます。STM32のように内部SRAMが複数領域に分かれている場合に便利です。
どのスキームを使用するかは、FreeRTOSConfig.h
のconfigHEAP_ALLOCATION_TYPE
またはconfigUSE_HEAPx
マクロで設定します。STM32CubeIDEでFreeRTOSを設定する際には、Heap_4またはHeap_5を選択するのが一般的です。
また、各タスクが必要とするスタックサイズ (configMINIMAL_STACK_SIZE
) を適切に見積もることも重要です。スタックサイズが不足すると、スタックオーバーフローが発生し、システムの誤動作やクラッシュの原因となります。
2.6 タイムアウトと遅延
タスクは、指定した時間だけ実行を中断することができます。
vTaskDelay(TickType_t xTicksToDelay)
: 指定したティック数だけタスクをBlocked状態にします。この関数が呼び出された時点からの相対的な遅延を指定します。vTaskDelayUntil(TickType_t * const pxPreviousWakeTime, const TickType_t xTimeToWake)
: 指定したティック時刻になるまでタスクをBlocked状態にします。周期的なタスク実行に便利です。前回の起床時刻を記録しておく変数へのポインタと、次回の起床時刻(絶対時刻)を指定します。これにより、周期的な処理のジッターを減らすことができます。
2.7 ソフトウェアタイマー (Software Timers)
ソフトウェアタイマーは、指定した時間が経過した後に、定義されたコールバック関数を実行する機能です。これは、ハードウェアタイマーの代わりに、あるいはハードウェアタイマーのリソースを節約するために使用できます。
- ワンショットタイマー: 一度だけコールバック関数を実行します。
- 周期タイマー: 指定した間隔で繰り返しコールバック関数を実行します。
ソフトウェアタイマーの処理は、FreeRTOSカーネルによって管理される専用のソフトウェアタイマーサービス/デーモンタスクによって実行されます。したがって、タイマーコールバック関数内では、長い時間のかかる処理を実行したり、タスクをブロックする可能性のあるFreeRTOS API(例: xQueueReceive()
に無限待ちを指定するなど)を呼び出したりするべきではありません。ISR SafeなAPI (FromISR
サフィックスを持つもの) のみを使用できる割り込みハンドラとは異なり、ソフトウェアタイマーコールバック関数はタスクコンテキストで実行されますが、タイマーサービス/デーモンタスク自身の優先度内で実行されるため、ここでのブロッキング処理は他のタイマーの実行に影響を与える可能性があります。
使い方: xTimerCreate()
でタイマーを生成、xTimerStart()
で開始、xTimerStop()
で停止。
2.8 割り込みハンドリングとFreeRTOS
組み込みシステムでは、外部イベント(ボタンプレス、データ受信など)に割り込みによって応答することが一般的です。FreeRTOSを使用する場合、割り込みハンドラ内からFreeRTOSのAPIを呼び出すことがあります(例: 割り込みをトリガーとしてタスクを起床させる、割り込みで受信したデータをキューに送信するなど)。
割り込みハンドラ内から呼び出せるFreeRTOS APIは限られています。これらのAPIは通常、関数名の末尾にFromISR
または_FromISR
が付きます(例: xQueueSendFromISR()
, xSemaphoreGiveFromISR()
, xTaskResumeFromISR()
, xEventGroupSetBitsFromISR()
)。
割り込みとFreeRTOSカーネルの連携において最も重要な設定の一つが、FreeRTOSConfig.h
で定義される configMAX_SYSCALL_INTERRUPT_PRIORITY
です。これは、FreeRTOS APIを安全に呼び出せる割り込みの最大優先度を指定します。ARM Cortex-Mの場合、これはNVIC (Nested Vectored Interrupt Controller) の割り込み優先度と関連付けられます。FreeRTOSカーネルが使用する割り込み(SysTickなど)よりも高い(つまり数値が小さい)優先度を持つ割り込みハンドラ内からは、FreeRTOS APIを呼び出すべきではありません。configMAX_SYSCALL_INTERRUPT_PRIORITY
で指定された優先度レベル以下の割り込みハンドラ内でのみ、FromISR
APIを安全に使用できます。
ISR内でFromISR
APIを呼び出すと、通常、pxHigherPriorityTaskWoken
というパラメータが渡されます。このパラメータは、APIの実行結果として、現在実行中のタスクよりも高い優先度のタスクがReady状態になったかどうかを示します。もしpdTRUE
が格納された場合、割り込みハンドラの終了時に即座にコンテキストスイッチを実行して、より高い優先度のタスクにCPUを明け渡す必要があることをFreeRTOSに伝えます。これは、ポーティング層によって提供される特定のメカニズム(ARM Cortex-Mの場合はportYIELD_FROM_ISR
マクロなど)を使用して行われます。
第3章: STM32開発環境の準備
FreeRTOSを使ったSTM32開発を行うためには、いくつかのハードウェアとソフトウェアが必要です。ここでは、最も一般的な開発環境であるSTM32CubeIDEを前提に説明します。
3.1 必要なハードウェア
- STM32開発ボード: プログラムを書き込んで実行するためのSTM32マイコンを搭載したボード。初心者には、STMicroelectronics純正のNucleoシリーズやDiscoveryシリーズがお勧めです。これらのボードにはデバッガ(ST-Link)が内蔵されているため、別途デバッガを用意する必要がありません。例えば、Nucleo-F401RE や Discovery-STM32F407G などが一般的です。
- USBケーブル: 開発ボードとPCを接続し、電源供給、プログラム書き込み、デバッグ、仮想COMポート通信などに使用します。
3.2 必要なソフトウェア
- STM32CubeIDE: STMicroelectronicsが提供する統合開発環境 (IDE)。GCCコンパイラ、デバッガ、ビルドツール、そして後述するSTM32CubeMXの機能が統合されています。無償で利用できます。
- STM32CubeMX (STM32CubeIDEに統合): グラフィカルな設定ツール。マイコンのピン設定、クロックツリー設定、ペリフェラル設定、ミドルウェア(FreeRTOS、USB、TCP/IPスタックなど)の設定をGUIで行い、対応する初期化コードを生成します。
- ST-Linkドライバー: PCと開発ボードのST-Linkデバッガを通信させるためのドライバーソフトウェア。STM32CubeIDEをインストールする際に一緒にインストールされることが多いですが、別途インストールが必要な場合もあります。
- ターミナルソフト (オプション): UARTなどのシリアル通信を使ってデバッグメッセージなどを表示したい場合に必要です(Tera Term, PuTTYなど)。
3.3 STM32CubeIDEの基本的な使い方
STM32CubeIDEはEclipseベースのIDEです。基本的な開発フローは以下のようになります。
- プロジェクト作成: 新規プロジェクトを作成し、使用するSTM32マイコンまたは開発ボードを選択します。
- STM32CubeMX設定: プロジェクト作成時に自動的に、または手動でSTM32CubeMXを開き、マイコンの各種設定を行います。ここでピン機能割り当て、クロック設定、ペリフェラル設定、そしてFreeRTOSを含むミドルウェアの設定を行います。
- コード生成: STM32CubeMXの設定を保存すると、設定に基づいた初期化コードやHALライブラリ、ミドルウェアのソースコードが自動的に生成されます。
- アプリケーションコード記述: 生成されたプロジェクトのソースファイル(特に
main.c
)に、FreeRTOSのタスク定義やアプリケーションロジックを記述します。 - ビルド: プロジェクトをビルドし、実行可能なバイナリファイル(
.elf
や.hex
ファイル)を生成します。 - デバッグ/実行: 開発ボードをPCに接続し、生成されたバイナリファイルをマイコンに書き込みます。STM32CubeIDEのデバッグ機能を使って、ステップ実行、ブレークポイント設定、変数監視などを行いながらプログラムの動作を確認します。
3.4 FreeRTOSをプロジェクトに追加する方法 (STM32CubeIDE)
STM32CubeIDEを使ったFreeRTOSプロジェクトの作成は非常に簡単です。これはSTM32CubeMXの機能を利用して行います。
- 新しいプロジェクトを作成し、ターゲットとなるMCUやボードを選択します。
- プロジェクト設定画面で、Middleware カテゴリを開き、FreeRTOS にチェックを入れます。
- FreeRTOSにチェックを入れると、FreeRTOSの設定オプションが表示されます。ここでカーネルの各種設定(後述)を行います。
- 必要なペリフェラル(GPIO、UARTなど)やその他の設定も行います。
- クロック設定を行います。FreeRTOSのTickタイマーの精度に影響するため重要です。
- System CoreのSYS設定で、Debug設定をSerial Wireに変更します(デバッグに必要)。また、Timebase SourceがFreeRTOSによって利用されるペリフェラル(SysTickなど)と競合しないように設定されているか確認します。通常、FreeRTOSを選択すると自動的にSysTickがFreeRTOSのTimebaseとして設定されます。
- 設定が完了したら、コード生成を行います。
main.c
やFreeRTOS関連のソースファイル、そして重要な設定ファイルであるFreeRTOSConfig.h
が生成されます。
次の章では、この手順をさらに具体的に見ていきます。
第4章: STM32CubeIDEを使ったFreeRTOSプロジェクトの作成手順
ここでは、STM32CubeIDEを使って、FreeRTOSを組み込んだ基本的なプロジェクトを作成する具体的な手順を解説します。例として、LEDを点滅させるタスクを作成するプロジェクトを想定します。
使用ツール: STM32CubeIDE (バージョン 1.0 以降を推奨)
ターゲットボード: STM32F4シリーズ搭載ボード (例: NUCLEO-F401RE) ※他のSTM32シリーズでも基本的な手順は同じです。
-
STM32CubeIDEを起動し、新しいプロジェクトを作成:
- File -> New -> STM32 Project を選択。
- “Target Selection” ウィンドウで、使用するMCUまたは開発ボードを選択します。Boards Selector タブで NUCLEO-F401RE を選択し、Nextをクリックします。
- プロジェクト名を決定します (例:
MyFreeRTOS_LED_Toggle
)。Project Type は “STM32Cube” のまま、Targeted Project Type は “Empty” を選択するのが一般的です。Nextをクリックします。 - Firmware Package Infoを確認し、Finishをクリックします。はい、と聞かれたらはいを選択します。
-
STM32CubeMXによる設定:
- プロジェクトが作成され、
.ioc
ファイルが開かれ、マイコンのピン配置図が表示されます。 - Pinout & Configuration タブ:
- System Core -> SYS:
Debug
:Serial Wire
を選択します。デバッグに必要です。Timebase Source
: FreeRTOSを使用する場合、SysTickが自動的にFreeRTOSのTimebaseとして割り当てられることが多いです。他の用途でSysTickを使用している場合は競合に注意が必要ですが、通常はデフォルト設定で問題ありません。
- System Core -> RCC:
- High Speed Clock (HSE) や Low Speed Clock (LSE) の設定を行います。ここではボードのデフォルト設定(例えば、HSEをCrystal/Ceramic Resonatorにするなど)に従います。
- GPIO:
- LEDが接続されているピンを設定します。例えば、NUCLEO-F401REの場合、LD2 (Green LED) は PA5 に接続されています。PA5ピンをクリックし、
GPIO_Output
を選択します。ラベル名(例:LED2_Pin
)を付けることも可能です。
- LEDが接続されているピンを設定します。例えば、NUCLEO-F401REの場合、LD2 (Green LED) は PA5 に接続されています。PA5ピンをクリックし、
- Middleware -> FreeRTOS:
Mode
:CMSIS_V1
またはCMSIS_V2
を選択できます。CMSIS-RTOS APIを使用する場合はこれらを選択しますが、FreeRTOSネイティブAPIを直接使用する場合はInterface
でFreeRTOS
を選択します。この記事ではFreeRTOSネイティブAPIを使う前提で説明を進めますので、Interface
でFreeRTOS
を選択してください。Configuration
: FreeRTOSの設定が表示されます。Kernel Parameters
:Heap Management
: 通常はHeap_4
またはHeap_5
を選択します。ここではHeap_4
を選択します。Total Heap Size
: FreeRTOSが動的にメモリを割り当てるためのヒープ領域のサイズを指定します。初期値は小さいことが多いので、アプリケーションの要件に合わせて増やします。例えば、8192
(8KB) など。Use Mutexes
:Enable
にチェックを入れるとミューテックス関連のAPIが有効になります。Use Recursive Mutexes
: 必要に応じて有効にします。Use Counting Semaphores
: 必要に応じて有効にします。Use Timers
:Enable
にチェックを入れるとソフトウェアタイマー関連のAPIが有効になります。Use Event Groups
: 必要に応じて有効にします。
Tasks and Queues
:- ここでは初期タスクやキューなどをGUIで定義することもできますが、通常はコード中で動的に生成する方が柔軟性が高く一般的です。なので、ここではタスクの追加は行いません。
Config Parameters
:FreeRTOSConfig.h
で定義される各種パラメータが表示されます。CPU Clock configuration [Hz]
: STM32のクロック周波数です。正しい値が設定されているか確認します。Tick Rate [Hz]
: システムティック割り込みの周期 (1秒あたりのティック数) を設定します。通常は1000
(1ms周期) とします。Number of priorities
: 使用するタスク優先度の最大値を設定します。デフォルトは5や7などですが、必要に応じて増やします。Minimal stack size [words]
:vTaskCreate()
で指定するスタックサイズの最小値のデフォルト値です。この値自体がタスクのスタックサイズを決めるわけではありませんが、タスク作成時の推奨値として利用されます。- その他の設定項目(Idle Hook, Tick Hook, Assertなど)もここで有効/無効を設定できます。
- System Core -> SYS:
- プロジェクトが作成され、
-
Clock Configuration タブ:
- システムのクロック周波数を設定します。HCLK (AHB bus clock) の周波数がCPUの動作周波数であり、FreeRTOSの
configCPU_CLOCK_HZ
と一致する必要があります。PLL設定などを適切に行い、最大の動作周波数に設定するのが一般的です。ここではツールの自動解決機能を使って適切な設定を行います。
- システムのクロック周波数を設定します。HCLK (AHB bus clock) の周波数がCPUの動作周波数であり、FreeRTOSの
-
Project Manager タブ:
- Project Name, Toolchain/IDE (STM32CubeIDE), Project Locationなどを確認します。
- Code Generator セクションで、
Generate peripheral initialization as a pair of '.c/.h' files per each peripheral
にチェックを入れると、ペリフェラルごとにファイルが分割されて見通しが良くなります(必須ではありません)。
-
コード生成:
- Project -> Generate Code またはツールバーの歯車アイコンをクリックしてコード生成を行います。はい、と聞かれたらはいを選択します。
- コード生成後、
main.c
やFreeRTOSConfig.h
などのファイルがプロジェクトに追加されます。
-
生成されたコードの確認:
- main.c:
main()
関数が生成されています。HALライブラリの初期化、システムクロックの設定、ペリフェラルの初期化などが自動生成されています。MX_FREERTOS_Init()
関数が呼び出され、FreeRTOSの初期化処理が含まれています。重要なのは、自動生成された領域 (user code begin/end コメントの間) にコードを記述することです。これにより、CubeMXで再生成を行った際に記述したコードが消されるのを防ぎます。 - FreeRTOSConfig.h: FreeRTOSの動作設定がマクロ定義として記述されています。CubeMXで設定した内容が反映されています。後から手動で編集することも可能ですが、CubeMXで設定した項目はCubeMX側で管理されるため注意が必要です。
- Middlewares/Third_Party/FreeRTOS: FreeRTOSカーネルのソースコード(
croutine.c
,event_groups.c
,list.c
,queue.c
,tasks.c
,timers.c
など)と、ポーティング層のソースコード(ARM_CM4F用のport.c
など)が配置されています。
- main.c:
これで、FreeRTOSを組み込んだ基本的なSTM32CubeIDEプロジェクトが作成されました。次は、実際にタスクを実装していきます。
第5章: 基本的なFreeRTOSアプリケーションの実装 (STM32CubeIDE)
前の章で作成したプロジェクトをベースに、FreeRTOSを使ってLEDを点滅させるシンプルなアプリケーションを実装します。
5.1 main関数の構造
STM32CubeIDEで生成されたmain.c
の構造を確認します。
“`c
/ Includes ——————————————————————/
include “main.h” // HALライブラリや生成された各種ヘッダーファイルを含む
include “cmsis_os.h” // FreeRTOSを使う場合 (CMSIS API用, ネイティブAPIでもインクルードされることがある)
/ Private includes ———————————————————-/
/ USER CODE BEGIN Includes /
// ここにユーザー独自のインクルードを追加
/ USER CODE END Includes /
/ Private typedef ———————————————————–/
/ USER CODE BEGIN PTD /
// ここにユーザー独自のtypedefを追加
/ USER CODE END PTD /
/ Private define ————————————————————/
/ USER CODE BEGIN PD /
// ここにユーザー独自のdefineを追加 (例: LEDピンの定義)
define LED2_PIN GPIO_PIN_5
define LED2_GPIO_PORT GPIOA
/ USER CODE END PD /
/ Private macro ————————————————————-/
/ USER CODE BEGIN PM /
/ USER CODE END PM /
/ Private variables ———————————————————/
/ USER CODE BEGIN PV /
// ここにユーザー独自のグローバル変数を追加
/ USER CODE END PV /
/ Private function prototypes ———————————————–/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
// static void MX_USART2_UART_Init(void); // 使用する場合
void MX_FREERTOS_Init(void); // FreeRTOS初期化関数
/ USER CODE BEGIN PFP /
// ここにユーザー独自の関数プロトタイプを追加
void StartLedToggleTask(void argument); // LED点滅タスク関数プロトタイプ
/ USER CODE END PFP */
/ Private user code ———————————————————/
/ USER CODE BEGIN 0 /
// ここにユーザー独自の関数や定義を追加
/ USER CODE END 0 /
/
* @brief The application entry point.
* @retval int
/
int main(void)
{
/ USER CODE BEGIN 1 /
/ USER CODE END 1 */
/ MCU Configuration——————————————————–/
/ Reset of all peripherals, Initializes the Flash interface and the Systick. /
HAL_Init();
/ USER CODE BEGIN Init /
/ USER CODE END Init /
/ Configure the system clock /
SystemClock_Config();
/ USER CODE BEGIN SysInit /
/ USER CODE END SysInit /
/ Initialize all configured peripherals /
MX_GPIO_Init();
// MX_USART2_UART_Init(); // 使用する場合
/ USER CODE BEGIN 2 /
/ USER CODE END 2 /
/ Init scheduler /
MX_FREERTOS_Init();
/ We should never get here as control is now taken by the scheduler /
/ Infinite loop /
/ USER CODE BEGIN WHILE /
while (1)
{
/ USER CODE END WHILE /
/* USER CODE BEGIN 3 */
}
/ USER CODE END 3 /
}
/
* @brief FreeRTOS Initialization
* @param None
* @retval None
/
void MX_FREERTOS_Init(void) {
/ USER CODE BEGIN RTOS_MUTEX /
/ add mutexes, … /
/ USER CODE END RTOS_MUTEX */
/ USER CODE BEGIN RTOS_SEMAPHORES /
/ add semaphores, … /
/ USER CODE END RTOS_SEMAPHORES /
/ USER CODE BEGIN RTOS_TIMERS /
/ start timers, add new ones, … /
/ USER CODE END RTOS_TIMERS /
/ USER CODE BEGIN RTOS_QUEUES /
/ add queues, … /
/ USER CODE END RTOS_QUEUES /
/ USER CODE BEGIN RTOS_TASKS /
/ add tasks, … /
/ USER CODE END RTOS_TASKS /
/ Start scheduler /
osKernelStart(); // CMSIS APIの場合
// vTaskStartScheduler(); // FreeRTOSネイティブAPIの場合 (MX_FREERTOS_Init内ではなくmainの最後に記述することが多い)
/ We should never get here as control is now taken by the scheduler /
/ USER CODE BEGIN RTOS_INMOUNTABLES /
/ USER CODE END RTOS_INMOUNTABLES /
}
/ USER CODE BEGIN Header_StartDefaultTask /
// デフォルトタスクのヘッダーコメント (CubeMXでデフォルトタスクを生成した場合)
/
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
/
/ USER CODE END Header_StartDefaultTask /
// void StartDefaultTask(void argument); // CubeMXで生成されたデフォルトタスク関数プロトタイプ
// MX_FREERTOS_Init 関数と StartDefaultTask 関数は、CubeMXでRTOS設定時に自動生成される場合があります。
// 今回は手動でタスクを作成するため、デフォルトタスクは削除または無視します。
// vTaskStartScheduler() は main 関数の最後で呼び出すようにします。
“`
main()
関数内で、HAL_Init()
、SystemClock_Config()
、MX_GPIO_Init()
などのハードウェア初期化が行われた後、FreeRTOS関連の初期化が行われ、最後にスケジューラが開始されます。
5.2 最初のタスクの作成と起動
LEDを点滅させるタスクを作成します。
-
タスク関数の定義: タスクとして実行される関数を定義します。この関数は
void StartLedToggleTask(void *argument)
のシグネチャを持つ必要があります。argument
はタスク作成時に渡されるパラメータです。“`c
/ Private user code ———————————————————/
/ USER CODE BEGIN 0 /
// LED点滅タスク関数
void StartLedToggleTask(void argument)
{
/ USER CODE BEGIN StartLedToggleTask */
// タスクの初期化処理があればここに記述/ Infinite loop /
for(;;) // あるいは while(1)
{
// LEDをONにする
HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_PIN, GPIO_PIN_SET);
// 500ms待つ
vTaskDelay(pdMS_TO_TICKS(500)); // pdMS_TO_TICKS はミリ秒をティック数に変換するマクロ// LEDをOFFにする HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_PIN, GPIO_PIN_RESET); // 500ms待つ vTaskDelay(pdMS_TO_TICKS(500)); // タスクが終了しないように無限ループになっていることを確認
}
/ USER CODE END StartLedToggleTask /
}
/ USER CODE END 0 /
“` -
タスクの生成:
main()
関数内のMX_FREERTOS_Init()
のUSER CODE BEGIN RTOS_TASKS
セクションで、タスクを生成します。“`c
void MX_FREERTOS_Init(void) {
// … 他のRTOSオブジェクト生成 (今回はなし) …/ USER CODE BEGIN RTOS_TASKS /
// タスクハンドルの宣言 (オプション, タスクを削除したり操作したりする場合に必要)
TaskHandle_t xLedToggleTaskHandle = NULL;// LED点滅タスクの生成
// pvTaskCode: タスク関数へのポインタ (StartLedToggleTask)
// pcName: タスク名 (“LED_Toggle”)
// usStackDepth: スタックサイズ (word単位、STM32F4では1 word = 4 bytes。256 words = 1024 bytes)
// pvParameters: タスク関数に渡すパラメータ (今回はNULL)
// uxPriority: タスクの優先度 (tskIDLE_PRIORITY は最も低い優先度)
// pxCreatedTask: 生成されたタスクのハンドルを格納する変数へのポインタ (NULLでも可)
BaseType_t xResult = xTaskCreate(
StartLedToggleTask, // タスク関数
“LED_Toggle”, // タスク名
configMINIMAL_STACK_SIZE,// スタックサイズ (FreeRTOSConfig.hで定義された最小値を使用)
NULL, // パラメータ
tskIDLE_PRIORITY + 1, // 優先度 (IDLEタスクより少し高く)
&xLedToggleTaskHandle); // タスクハンドル// タスク生成が成功したか確認 (オプション)
if( xResult != pdPASS )
{
// タスク生成に失敗した場合の処理 (メモリ不足など)
// エラーハンドリングコードをここに記述
// 例: assert_failed() を呼び出す、無限ループで停止するなど
}
/ USER CODE END RTOS_TASKS /// スケジューラ開始 (main 関数の最後で行うのが一般的)
// osKernelStart(); // CMSIS APIの場合
// vTaskStartScheduler(); // FreeRTOSネイティブAPIの場合
}
``
MX_FREERTOS_Init
**注意**: STM32CubeIDEのFreeRTOS設定でCMSIS_V1またはV2を選択した場合、内に
osKernelStart()が自動生成されます。FreeRTOSネイティブAPIを選択した場合でも
osKernelStart()が生成されることがありますが、その場合は
vTaskStartScheduler()に置き換える必要があります。あるいは、
MX_FREERTOS_Init内でタスク生成までを行い、
main関数の最後に
vTaskStartScheduler()を呼び出す形にする方が、FreeRTOSの資料と一致して分かりやすいかもしれません。ここでは後者の形を推奨します。
main.c`を以下のように修正します。“`c
int main(void)
{
// … (HAL初期化、クロック設定、ペリフェラル初期化) …/ USER CODE BEGIN 2 /
/ END USER CODE 2 // Init scheduler /
// MX_FREERTOS_Init(); // この中でタスク生成のみ行うように修正/ USER CODE BEGIN 3 /
/ END USER CODE 3 /// — タスク生成をmain関数内に移動 —
TaskHandle_t xLedToggleTaskHandle = NULL;
BaseType_t xResult = xTaskCreate(
StartLedToggleTask,
“LED_Toggle”,
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
&xLedToggleTaskHandle);if( xResult != pdPASS )
{
// タスク生成失敗
while(1); // 無限ループで停止など
}
// — タスク生成ここまで —/ Start scheduler /
vTaskStartScheduler(); // スケジューラ開始/ We should never get here as control is now taken by the scheduler /
while (1)
{
// ここには通常到達しない
}
}// MX_FREERTOS_Init はそのまま残しておくか、または削除します。
// CubeMXで生成されたRTOS_TASKSセクションは main 関数内の USER CODE BEGIN 3 に移動させます。
// あるいは、MX_FREERTOS_Init 関数内でタスク生成のみ行い、main 関数から呼び出し、main 関数の最後に vTaskStartScheduler() を呼び出す形でも良いです。
// ここでは、タスク生成を main 関数のスケジューラ開始直前に記述する方法を採用します。
“` -
ビルドと実行: プロジェクトをビルドし、STM32ボードに書き込んで実行します。LEDが約1秒周期で点滅することを確認してください。
5.3 vTaskDelayによるタスクの同期
上の例では、vTaskDelay(pdMS_TO_TICKS(500))
を使ってタスクを500msだけ遅延させています。vTaskDelay
を呼び出したタスクは指定された時間だけBlocked状態となり、その間CPUは他のReady状態のタスクに明け渡されます。指定時間が経過すると、タスクはReady状態に戻り、スケジューラによって再度実行可能になった時点で実行されます。
5.4 FreeRTOSConfig.h の設定項目の解説
FreeRTOSConfig.h
はFreeRTOSの動作を詳細に設定するための非常に重要なファイルです。CubeMXで設定した内容がここに反映されます。主な設定項目をいくつか紹介します。
configCPU_CLOCK_HZ
: CPUのシステムクロック周波数(Hz)。ポーティング層が時間計算などに使用します。configTICK_RATE_HZ
: システムティック割り込みの周波数(Hz)。通常は1000 (1ms周期) に設定します。この周期でスケジューラがタスクの切り替えやタイムアウトの管理を行います。configMAX_PRIORITIES
: アプリケーションで使用するタスク優先度の最大値。0からconfigMAX_PRIORITIES - 1
までの優先度が使用可能です。configMINIMAL_STACK_SIZE
:xTaskCreate
でスタックサイズを指定する際の最小値のデフォルト値(word単位)。実際のタスクに必要なスタックサイズは、そのタスク関数内で使用されるローカル変数や、呼び出される関数によって異なります。configTOTAL_HEAP_SIZE
: FreeRTOSが動的にメモリを割り当てるためのヒープ領域の合計サイズ(byte単位)。これはconfigHEAP_ALLOCATION_TYPE
(またはconfigUSE_HEAPx
)で指定されたメモリ管理スキームで使用されます。十分なサイズを確保しないと、タスク生成やキュー生成などに失敗します。configUSE_PREEMPTION
: プリエンプティブスケジューリングを有効にするか (1) 無効にするか (0)。通常は1に設定します。configUSE_TIME_SLICING
: 同じ優先度のタスク間でタイムスライシングを有効にするか (1) 無効にするか (0)。configUSE_PREEMPTION
が1の場合に有効です。通常は1に設定します。configUSE_TICKLESS_IDLE
: 低消費電力機能であるTickless Idleを有効にするか (1) 無効にするか (0)。有効にすると、アイドルタスクが実行され、かつ次に発生するティック割り込みまたはイベントまで時間がある場合に、システムティック割り込みを停止してマイコンを低消費電力モードに移行させます。configUSE_IDLE_HOOK
: アイドルタスクのフック関数 (Idle Hook) を使用するか (1) 無効にするか (0)。アイドルタスクは、実行可能な他のタスクがない場合にFreeRTOSによって自動的に実行される最も優先度の低いタスクです。Idle Hookはアイドルタスク内で呼び出され、低消費電力モードへの移行処理などに利用できます。configUSE_TICK_HOOK
: システムティック割り込みのフック関数 (Tick Hook) を使用するか (1) 無効にするか (0)。Tick Hookはシステムティック割り込みハンドラの終了時に呼び出され、周期的な処理などに利用できますが、割り込みコンテキストで実行されるため、処理時間は短くする必要があります。configUSE_MALLOC_FAILED_HOOK
:pvPortMalloc
(ヒープ割り当て)が失敗した場合のフック関数を使用するか (1) 無効にするか (0)。メモリ不足のデバッグに役立ちます。configUSE_DAEMON_TASK_STARTUP_HOOK
: デーモンタスク(ソフトウェアタイマーサービスなど)が開始された後に呼び出されるフック関数を使用するか (1) 無効にするか (0)。他のタスクを生成する前に、ソフトウェアタイマーなどのRTOS機能が利用可能になったことを確認するのに使用できます。configMAX_SYSCALL_INTERRUPT_PRIORITY
: FreeRTOS API (FromISR
関数) を安全に呼び出せる割り込みの最大優先度。NVICの割り込み優先度設定と密接に関連します。この設定を誤ると、割り込みからのRTOS API呼び出しがシステムクラッシュを引き起こす可能性があります。詳細はFreeRTOSのドキュメントやポーティング層のソースコードを参照してください。
これらの設定値を適切に構成することで、アプリケーションの要件に合わせたFreeRTOSの動作を実現できます。特に、configTOTAL_HEAP_SIZE
とタスクのスタックサイズの見積もりは、メモリ関連のトラブルを防ぐ上で重要です。
5.5 フック関数 (Hook Functions)
FreeRTOSは、特定のイベント発生時にユーザー定義関数を呼び出す仕組みとしてフック関数を提供します。
- Idle Hook:
configUSE_IDLE_HOOK
を1に設定すると有効になります。アイドルタスクのループ内で呼び出される関数 (void vApplicationIdleHook(void)
) を定義できます。CPU使用率が100%でない限り頻繁に呼び出されます。主に低消費電力モードへの移行処理などに利用されます。アイドルタスクはFreeRTOSカーネルによって自動的に生成され、Ready状態の他のタスクがない場合に実行されます。最も優先度が低いです。 - Tick Hook:
configUSE_TICK_HOOK
を1に設定すると有効になります。システムティック割り込みハンドラの終了時に呼び出される関数 (void vApplicationTickHook(void)
) を定義できます。周期的な処理(例: ソフトタイマーの進捗)に利用できますが、ISRコンテキストで実行されるため、処理時間はごく短くする必要があります。 - Malloc Failed Hook:
configUSE_MALLOC_FAILED_HOOK
を1に設定すると有効になります。pvPortMalloc
(FreeRTOSのヒープ割り当て関数)がメモリ不足で失敗した場合に呼び出される関数 (void vApplicationMallocFailedHook(void)
) を定義できます。メモリ不足のデバッグに非常に役立ちます。通常は無限ループなどでシステムを停止させ、デバッガで状態を確認できるようにします。 - Stack Overflow Hook:
configCHECK_FOR_STACK_OVERFLOW
を1または2に設定すると有効になります。タスクのスタックオーバーフローが検出された場合に呼び出される関数 (void vApplicationStackOverflowHook( TaskHandle_t xTask, signed char *pcTaskName )
) を定義できます。スタックオーバーフローは組み込みシステムでよくある問題であり、このフック関数を有効にすることで早期に問題を検出できます。
これらのフック関数は、システムの動作監視やデバッグ、低消費電力化などに活用できます。
第6章: タスク間通信と同期の実践
実際の組み込みアプリケーションでは、複数のタスクが互いに連携する必要があります。ここでは、FreeRTOSが提供する主要なタスク間通信・同期メカニズムであるキュー、セマフォ、ミューテックス、イベントグループの使い方を具体的な例と共に解説します。
6.1 キュー (Queue)
キューは、タスク間で固定サイズのデータを安全に受け渡すための最も一般的な手段です。FIFO(First-In, First-Out)バッファとして機能します。
特徴:
* データを「コピー」して送受信します。ポインタを渡す場合は、データの生存期間に注意が必要です。
* 送信時にはキューが満杯の場合、受信時にはキューが空の場合に、タスクを待機(ブロック)させることができます。タイムアウトを指定することも可能です。
* 割り込みハンドラからも安全に使用できるAPI (FromISR
サフィックス付き) が提供されています。
使用例: UARTで受信したデータを処理タスクに渡す
UART受信割り込みハンドラ内で受信データをキューに入れ、別のタスクがそのデータをキューから取り出して処理する、というシナリオを考えます。
-
キューハンドルの宣言: グローバル変数としてキューハンドルを宣言します。
c
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
QueueHandle_t xUartQueue;
/* USER CODE END PV */ -
キューの生成:
MX_FREERTOS_Init()
(またはタスク生成を行う場所)でキューを生成します。“`c
void MX_FREERTOS_Init(void) {
/ USER CODE BEGIN RTOS_QUEUES /
// 1バイトのメッセージを10個保持できるキューを生成
xUartQueue = xQueueCreate(10, sizeof(uint8_t));if( xUartQueue == NULL )
{
// キュー生成失敗
while(1);
}
/ USER CODE END RTOS_QUEUES /
// … (タスク生成など) …
}
“` -
UART受信割り込みハンドラでのデータ送信: UART受信完了割り込み(例:
HAL_UART_RxCpltCallback
)内で、受信した1バイトのデータをキューに送信します。“`c
// HAL_UART_RxCpltCallback 関数の実装例
// (stm32f4xx_hal_uart.c などにweak属性で定義されているものをオーバーライド)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2) // 使用するUARTに合わせて変更
{
static uint8_t rx_data; // 受信データ格納用
BaseType_t xHigherPriorityTaskWoken = pdFALSE;// 受信データをキューに送信 (割り込み安全なAPIを使用) // 第三引数はキューが満杯だった場合の待機時間ですが、ISRからはブロックできないため常に0 xQueueSendFromISR(xUartQueue, &rx_data, &xHigherPriorityTaskWoken); // キュー送信によってより高い優先度のタスクが起床した場合、コンテキストスイッチを要求 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 次の受信に備えてUART受信を再開 HAL_UART_Receive_IT(huart, &rx_data, 1);
}
}// main関数または適切な場所で最初のUART受信を開始しておく必要がある
// 例: HAL_UART_Receive_IT(&huart2, &rx_data, 1);
“` -
データ処理タスクでのデータ受信: キューからデータを取り出して処理するタスクを作成します。
“`c
/ Private user code ———————————————————/
/ USER CODE BEGIN 0 /
// UART受信データ処理タスク関数
void StartUartProcessTask(void argument)
{
/ USER CODE BEGIN StartUartProcessTask */
uint8_t received_data;for(;;)
{
// キューからデータを受信
// キューが空の場合は、データが到着するまで無限に待機 (portMAX_DELAY)
if( xQueueReceive(xUartQueue, &received_data, portMAX_DELAY) == pdPASS )
{
// データを受信できた場合の処理
// 例: 受信データを別のタスクに送信する、LEDを点滅させるなど
// printf(“Received: %c\n”, received_data); // デバッグ出力など
HAL_GPIO_TogglePin(LED2_GPIO_PORT, LED2_PIN); // 例としてLEDをトグル
}
// キューが空でタイムアウトした場合 (ここでは無限待機なので発生しない) はelseブロック
}
/ USER CODE END StartUartProcessTask /
}
/ USER CODE END 0 /
“` -
データ処理タスクの生成:
main()
関数などで、このタスクを生成します。“`c
int main(void)
{
// … (ハードウェア初期化、キュー生成) …/ USER CODE BEGIN 3 /
TaskHandle_t xUartProcessTaskHandle = NULL;
BaseType_t xResult = xTaskCreate(
StartUartProcessTask,
“UART_Process”,
configMINIMAL_STACK_SIZE * 2, // スタックサイズは処理内容に合わせて調整
NULL,
tskIDLE_PRIORITY + 2, // LEDタスクより少し高くするなど
&xUartProcessTaskHandle);if( xResult != pdPASS ) { while(1); }
// … (他のタスク生成) …
vTaskStartScheduler(); // スケジューラ開始
// …
}
“`
この構成により、UART受信割り込みは可能な限り短く(キューへのデータ送信のみ)、実際のデータ処理は専用のタスクで行うことができるため、割り込み応答性を損なわずに複雑なデータ処理を実現できます。
6.2 セマフォ (Semaphore)
セマフォは、イベントの通知やリソース数の管理に使用されます。FreeRTOSではバイナリセマフォとカウンティングセマフォがあります。ミューテックスも内部的にはセマフォの一種ですが、所有権と優先度継承の機能が追加されています。
特徴:
* データを保持しません。信号の受け渡しのみを行います。
* xSemaphoreGive()
で信号を放出し(カウンタ+1)、xSemaphoreTake()
で信号を獲得します(カウンタ-1)。
* カウンタが0の時にxSemaphoreTake()
を呼び出すとタスクはブロックされます。
* 割り込みハンドラからも安全に使用できるAPI (FromISR
サフィックス付き) があります。
使用例: ボタン押下イベントをタスクに通知する
外部割り込み(GPIOピンの変化)によって発生するボタン押下イベントを、タスクに通知するシナリオを考えます。
-
セマフォハンドルの宣言: グローバル変数としてセマフォハンドルを宣言します。
c
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
SemaphoreHandle_t xButtonSemaphore;
/* USER CODE END PV */ -
セマフォの生成:
MX_FREERTOS_Init()
などでバイナリセマフォを生成します。“`c
void MX_FREERTOS_Init(void) {
/ USER CODE BEGIN RTOS_SEMAPHORES /
// バイナリセマフォを生成 (最初は利用可能状態(1)で生成)
xButtonSemaphore = xSemaphoreCreateBinary();if( xButtonSemaphore == NULL )
{
// セマフォ生成失敗
while(1);
}
/ USER CODE END RTOS_SEMAPHORES /
// … (タスク生成など) …
}
``
xSemaphoreCreateBinary()は初期値を0で生成します。すぐに使用可能にするには
xSemaphoreGive(xButtonSemaphore)を一度呼び出す必要があります。または
xSemaphoreCreateBinary()は内部で
xSemaphoreCreateMutex()と同じ関数を呼ぶため、ミューテックスのように初期値1で作成され、すぐに
xSemaphoreTake()` で取得できると解釈することもできます(ただし所有権はありません)。ここではイベント通知用途なので、通常は初期値0で作成し、イベント発生時に1にする使い方をします。 -
GPIO外部割り込みハンドラでのセマフォ放出: ボタンに接続されたGPIOピンの外部割り込みハンドラ内で、セマフォを放出します。HALライブラリでは
HAL_GPIO_EXTI_Callback
関数が使用されます。“`c
// HAL_GPIO_EXTI_Callback 関数の実装例
// (stm32f4xx_hal_gpio.c などにweak属性で定義されているものをオーバーライド)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == B1_Pin) // 使用するボタンピンに合わせて変更 (例: NUCLEO-F401REのB1ボタンはPC13)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;// セマフォを放出 (割り込み安全なAPIを使用) // セマフォが既に利用可能な場合でも、バイナリセマフォではカウンタは1より大きくならない xSemaphoreGiveFromISR(xButtonSemaphore, &xHigherPriorityTaskWoken); // セマフォ放出によってより高い優先度のタスクが起床した場合、コンテキストスイッチを要求 portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
“` -
ボタン処理タスクでのセマフォ獲得: セマフォを獲得してボタン押下イベントを処理するタスクを作成します。
c
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
// ボタン処理タスク関数
void StartButtonTask(void *argument)
{
/* USER CODE BEGIN StartButtonTask */
for(;;)
{
// セマフォを獲得
// セマフォが利用可能になるまで無限に待機 (portMAX_DELAY)
if( xSemaphoreTake(xButtonSemaphore, portMAX_DELAY) == pdPASS )
{
// セマフォを獲得できた = ボタンが押された
// ボタン押下時の処理をここに記述
// 例: LEDの状態を反転させるなど
HAL_GPIO_TogglePin(LED2_GPIO_PORT, LED2_PIN);
// ボタンのチャタリング対策として、短い遅延を入れる場合がある
// vTaskDelay(pdMS_TO_TICKS(50));
}
}
/* USER CODE END StartButtonTask */
}
/* USER CODE END 0 */ -
ボタン処理タスクの生成:
main()
関数などで、このタスクを生成します。“`c
int main(void)
{
// … (ハードウェア初期化、セマフォ生成) …/ USER CODE BEGIN 3 /
TaskHandle_t xButtonTaskHandle = NULL;
BaseType_t xResult = xTaskCreate(
StartButtonTask,
“Button_Process”,
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 2, // 適切な優先度を設定
&xButtonTaskHandle);if( xResult != pdPASS ) { while(1); }
// … (他のタスク生成) …
vTaskStartScheduler(); // スケジューラ開始
// …
}
“`
この例では、GPIO割り込みハンドラは最小限の処理(セマフォ放出)に留まり、時間のかかる可能性のある処理はタスクコンテキストで行われます。
6.3 ミューテックス (Mutex)
ミューテックスは、共有リソースへの排他制御を行うために使用されます。一度に一つのタスクのみがミューテックスを獲得でき、リソースへのアクセスを保護します。
特徴:
* セマフォと似ていますが、「所有権」の概念があります。ミューテックスを獲得したタスクだけがそれを解放できます。
* 優先度逆転問題を緩和するための優先度継承メカニズムを持つことができます (configUSE_PRIORITY_INHERITANCE
を1に設定した場合)。
* 通常、割り込みハンドラから直接ミューテックスを獲得/解放することはできません(ミューテックスはタスクコンテキストで実行されるブロッキングAPIを使用するため)。
使用例: 複数のタスクから同じUARTポートを使ってメッセージを出力する
複数のタスクがデバッグ目的でprintfなどを使ってUARTにメッセージを出力する場合、同時に出力しようとするとメッセージが混ざってしまいます。これを防ぐために、UARTリソースをミューテックスで保護します。
-
ミューテックスハンドルの宣言: グローバル変数としてミューテックスハンドルを宣言します。
c
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
SemaphoreHandle_t xUartMutex; // ミューテックスもSemaphoreHandle_t型で扱われます
/* USER CODE END PV */ -
ミューテックスの生成:
MX_FREERTOS_Init()
などでミューテックスを生成します。“`c
void MX_FREERTOS_Init(void) {
/ USER CODE BEGIN RTOS_MUTEX /
// ミューテックスを生成 (優先度継承対応版を推奨)
xUartMutex = xSemaphoreCreateMutex();if( xUartMutex == NULL )
{
// ミューテックス生成失敗
while(1);
}
/ USER CODE END RTOS_MUTEX /
// … (他のRTOSオブジェクト、タスク生成) …
}
“` -
共有リソースへのアクセス保護: UARTを使用する各タスクで、ミューテックスを獲得してから使用し、使い終わったら解放します。
“`c
// タスクAの関数例
void StartTaskA(void *argument)
{
for(;;)
{
// UARTミューテックスを獲得
if( xSemaphoreTake(xUartMutex, portMAX_DELAY) == pdPASS )
{
// ミューテックスを獲得できたので、安全にUARTを使用できる
printf(“Hello from Task A!\r\n”); // printfはUARTに出力する場合
// UART出力処理が終わったらミューテックスを解放
xSemaphoreGive(xUartMutex);
}
vTaskDelay(pdMS_TO_TICKS(1000)); // 1秒待機
}
}// タスクBの関数例
void StartTaskB(void *argument)
{
for(;;)
{
// UARTミューテックスを獲得
if( xSemaphoreTake(xUartMutex, portMAX_DELAY) == pdPASS )
{
// ミューテックスを獲得できたので、安全にUARTを使用できる
printf(“Hello from Task B!\r\n”);
// UART出力処理が終わったらミューテックスを解放
xSemaphoreGive(xUartMutex);
}
vTaskDelay(pdMS_TO_TICKS(1500)); // 1.5秒待機
}
}
“` -
タスクの生成:
main()
関数などでこれらのタスクを生成します。“`c
int main(void)
{
// … (ハードウェア初期化、ミューテックス生成) …
// UARTの初期化も必要
MX_USART2_UART_Init(); // 例/ USER CODE BEGIN 3 /
TaskHandle_t xTaskAHandle = NULL;
BaseType_t xResultA = xTaskCreate(
StartTaskA, “TaskA”, configMINIMAL_STACK_SIZE * 2, NULL, tskIDLE_PRIORITY + 1, &xTaskAHandle);
if( xResultA != pdPASS ) { while(1); }TaskHandle_t xTaskBHandle = NULL;
BaseType_t xResultB = xTaskCreate(
StartTaskB, “TaskB”, configMINIMAL_STACK_SIZE * 2, NULL, tskIDLE_PRIORITY + 1, &xTaskBHandle);
if( xResultB != pdPASS ) { while(1); }vTaskStartScheduler(); // スケジューラ開始
// …
}
“`
この構成により、Task AとTask Bが同時にUARTを使用しようとしても、ミューテックスによってアクセスが制御されるため、メッセージが混ざることなく順番に出力されます。
6.4 イベントグループ (Event Groups)
イベントグループは、複数のイベント(ビットフラグ)の組み合わせをタスクが待つための機能です。
特徴:
* イベントを個別のフラグ(ビット)として表現します。
* タスクは、特定のビットの組み合わせがセットされるまで待機できます(論理積または論理和)。
* イベントをセットするタスクは、イベントを待つタスクとは無関係に動作できます。
* 割り込みハンドラからも安全に使用できるAPI (FromISR
サフィックス付き) があります。
使用例: 複数の異なるセンサーからのデータが揃うのを待って処理を開始する
センサーAとセンサーBからデータが取得できたことをそれぞれイベントとして通知し、両方のイベントが発生した後にデータ処理タスクを開始するシナリオを考えます。
-
イベントグループハンドルの宣言: グローバル変数としてイベントグループハンドルを宣言します。
“`c
/ Private variables ———————————————————/
/ USER CODE BEGIN PV /
EventGroupHandle_t xSensorEventGroup;
/ イベントフラグのビット定義 /define BIT_SENSOR_A_READY (1 << 0) // ビット0
define BIT_SENSOR_B_READY (1 << 1) // ビット1
/ USER CODE END PV /
“` -
イベントグループの生成:
MX_FREERTOS_Init()
などでイベントグループを生成します。“`c
void MX_FREERTOS_Init(void) {
/ USER CODE BEGIN RTOS_EVENT_GROUPS /
// イベントグループを生成
xSensorEventGroup = xEventGroupCreate();if( xSensorEventGroup == NULL )
{
// イベントグループ生成失敗
while(1);
}
/ USER CODE END RTOS_EVENT_GROUPS /
// … (他のRTOSオブジェクト、タスク生成) …
}
“` -
イベントのセット: 各センサーからデータが取得できた時点で、対応するビットをセットします。これは、センサーデータの取得を行うタスクや、センサーの割り込みハンドラ内で行われます。
“`c
// センサーAデータ取得タスク/関数内でイベントをセット
void SensorA_DataReady(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// センサーAのデータ準備完了ビットをセット
xEventGroupSetBitsFromISR(
xSensorEventGroup, // イベントグループハンドル
BIT_SENSOR_A_READY, // セットするビット
&xHigherPriorityTaskWoken ); // より高い優先度のタスクが起床したかportYIELD_FROM_ISR( xHigherPriorityTaskWoken ); // ISRの場合 // または、タスクコンテキストからの場合 // xEventGroupSetBits(xSensorEventGroup, BIT_SENSOR_A_READY);
}
// センサーBデータ取得タスク/関数内でイベントをセット
void SensorB_DataReady(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// センサーBのデータ準備完了ビットをセット
xEventGroupSetBitsFromISR(
xSensorEventGroup,
BIT_SENSOR_B_READY,
&xHigherPriorityTaskWoken );portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); // ISRの場合 // または、タスクコンテキストからの場合 // xEventGroupSetBits(xSensorEventGroup, BIT_SENSOR_B_READY);
}
“` -
イベントの待機: データ処理タスクで、必要なイベント(ここではBIT_SENSOR_A_READY と BIT_SENSOR_B_READY の両方)が発生するのを待ちます。
“`c
/ Private user code ———————————————————/
/ USER CODE BEGIN 0 /
// データ処理タスク関数
void StartDataProcessTask(void argument)
{
/ USER CODE BEGIN StartDataProcessTask */
EventBits_t uxBits;
const EventBits_t uxBitsToWaitFor = (BIT_SENSOR_A_READY | BIT_SENSOR_B_READY); // 待つビットの組み合わせfor(;;)
{
// 指定したビットの組み合わせがセットされるまで待機
// xClearOnExit: 待機終了時に対応するビットをクリアするか (pdTRUE = クリアする)
// xWaitForAllBits: 全てのビットがセットされるのを待つか (pdTRUE = AND条件, pdFALSE = OR条件)
// xTicksToWait: 待機時間 (portMAX_DELAY = 無限待機)
uxBits = xEventGroupWaitBits(
xSensorEventGroup, // イベントグループハンドル
uxBitsToWaitFor, // 待機するビットマスク
pdTRUE, // 待機終了時にビットをクリアする
pdTRUE, // 全てのビットがセットされるのを待つ (AND)
portMAX_DELAY ); // 無限待機// 待機条件が満たされた(指定したビットが全てセットされた) if( (uxBits & uxBitsToWaitFor) == uxBitsToWaitFor ) { // データ処理ロジックをここに記述 // 例: 両方のセンサーデータが揃ったので計算処理を開始するなど printf("Both sensors ready, starting data processing.\r\n"); // データ処理が終わったら、再度イベント待ちに戻る } // タイムアウトした場合の処理 (ここでは無限待機なので発生しない) // else { ... }
}
/ USER CODE END StartDataProcessTask /
}
/ USER CODE END 0 /
“` -
データ処理タスクの生成:
main()
関数などでこのタスクを生成します。“`c
int main(void)
{
// … (ハードウェア初期化、イベントグループ生成) …/ USER CODE BEGIN 3 /
TaskHandle_t xDataProcessTaskHandle = NULL;
BaseType_t xResult = xTaskCreate(
StartDataProcessTask,
“Data_Process”,
configMINIMAL_STACK_SIZE * 3, // 処理内容に合わせてスタックサイズ調整
NULL,
tskIDLE_PRIORITY + 3, // 適切な優先度を設定
&xDataProcessTaskHandle);if( xResult != pdPASS ) { while(1); }
// … (他のタスク生成, 例: センサーA/Bのデータ取得タスク) …
vTaskStartScheduler(); // スケジューラ開始
// …
}
“`
この例では、データ処理タスクはセンサーデータの準備が完了するまでCPUを消費せずに待機し、全ての準備が整ってから実行を開始するため、システムリソースを効率的に使用できます。
キュー、セマフォ、ミューテックス、イベントグループは、FreeRTOSを使ったマルチタスク開発における重要なツールです。これらのメカニズムを適切に使い分けることで、タスク間の連携を安全かつ効率的に実現できます。
第7章: 応用トピックと高度な機能
基本的なFreeRTOSアプリケーションの実装に慣れてきたら、さらに進んだ機能や応用トピックについて学ぶことで、より洗練された、高性能な組み込みシステムを開発できるようになります。
7.1 ソフトウェアタイマーの詳細
第2章で触れましたが、ソフトウェアタイマーはFreeRTOSカーネルによって管理されるタイマーです。ハードウェアタイマーとは異なり、正確なタイミングを保証するものではなく、主にタスクコンテキストでの定期的な処理や遅延実行に使用されます。
- タイマーサービス/デーモンタスク: ソフトウェアタイマーの処理は、専用の優先度の高いタスク(タイマーサービス/デーモンタスク)によって実行されます。タイマーの生成や開始、停止要求はキューを介してこのタスクに送られ、このタスクがタイマーの管理とコールバック関数の実行を行います。
- コールバック関数: タイマーが満了したときに実行される関数です。この関数はタイマーサービス/デーモンタスクのコンテキストで実行されるため、ブロッキングAPIを呼び出すべきではありません。時間のかかる処理が必要な場合は、コールバック関数内から別のタスクに処理を委譲(例: キューにメッセージを送る、セマフォを放出するなど)する必要があります。
“`c
// ソフトウェアタイマーのコールバック関数例
void vMyTimerCallback( TimerHandle_t xTimer )
{
// タイマー満了時に実行される処理
// この関数はタイマーサービス/デーモンタスクのコンテキストで実行される
// 例: LEDをトグルする (HAL関数は通常安全に使用可能)
HAL_GPIO_TogglePin(LED2_GPIO_PORT, LED2_PIN);
// 例: 別のタスクにイベントを通知する (ISR safe APIを使用)
// BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// xSemaphoreGiveFromISR(xSomeSemaphore, &xHigherPriorityTaskWoken);
// portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // コンテキストスイッチ要求
}
// ソフトウェアタイマーの生成と開始例
TimerHandle_t xMyTimer = NULL;
// タイマー名、周期(ティック数)、周期タイマーか(pdTRUE)、タイマーID(NULL可)、コールバック関数
xMyTimer = xTimerCreate(“MyTimer”, pdMS_TO_TICKS(1000), pdTRUE, NULL, vMyTimerCallback);
if( xMyTimer != NULL )
{
// タイマー開始 (開始までの待機時間は0)
xTimerStart(xMyTimer, 0);
}
“`
ソフトウェアタイマーは、定期的なポーリング処理やタイムアウト監視などに便利です。
7.2 メモリ管理の選択肢
第2章で触れたヒープ管理スキーム (Heap_1
からHeap_5
) に加えて、FreeRTOSではいくつかの高度なメモリ管理手法が利用可能です。STM32の場合、Heap_4
またはHeap_5
がよく使われます。
- Heap_4: 単一の連続したメモリブロックをヒープとして使用します。動的な割り当てと解放が可能で、ある程度の断片化対策が施されています。STM32CubeIDEでFreeRTOSを設定する際のデフォルトとして選択されることが多いです。
- Heap_5: 複数の不連続なメモリブロックをヒープとして使用できます。STM32のSRAM1、SRAM2などが物理的に離れている場合に、これらをまとめてFreeRTOSの動的ヒープとして使用したい場合に便利です。
FreeRTOSConfig.h
でヒープ領域のアドレスとサイズを配列で指定します。
適切なヒープサイズ (configTOTAL_HEAP_SIZE
) の見積もりは重要です。vApplicationMallocFailedHook
を有効にして、メモリ割り当て失敗を検出できるようにしておくと良いでしょう。また、FreeRTOS+Traceなどのツールを使うと、ヒープの使用状況や断片化の様子を視覚的に確認できます。
7.3 デバッグ手法
RTOSを使ったマルチタスクアプリケーションのデバッグは、ベアメタルプログラミングに比べて複雑になることがあります。 FreeRTOSはデバッグを支援するための機能を提供しています。
- タスクリストの表示:
vTaskList()
またはvTaskGetRunTimeStats()
APIを使用すると、現在のタスクの状態、優先度、スタック残りサイズ、実行時間統計などを文字列として取得できます。これをUARTなどで出力することで、システムの状況を把握できます。これらの機能を使用するには、FreeRTOSConfig.h
でconfigUSE_TRACE_FACILITY
やconfigUSE_STATS_FORMATTING_FUNCTIONS
などを有効にする必要があります。また、実行時間統計を取得するには、別途ハードウェアタイマーを使って高分解能なカウンタを用意する必要があります。 - スタックオーバーフロー検出:
configCHECK_FOR_STACK_OVERFLOW
を1または2に設定すると、タスク切り替え時などにスタックオーバーフローが検出された場合にvApplicationStackOverflowHook
フック関数が呼び出されます。スタックオーバーフローは組み込みシステムで最も多い問題の一つであり、この機能は非常に重要です。 - トレース機能: FreeRTOS+Trace (Percepio Tracealyzer) や Segger SystemView のような商用ツールを使用すると、RTOSイベント(タスク切り替え、API呼び出し、ISR実行など)の発生タイミングを時系列で記録し、グラフィカルに表示できます。これにより、システムの実行状況を詳細に分析し、タイミング問題やデッドロック、優先度逆転などを効率的にデバッグできます。これらのツールを使用するには、FreeRTOSConfig.hでトレース関連の設定を有効にし、ポーティング層の実装が必要な場合があります。STM32CubeIDEはSegger SystemViewとの連携機能を持っています。
- デバッガ: STM32CubeIDEに統合されたデバッガ (GDB) を使用して、ブレークポイントの設定、ステップ実行、変数監視、レジスタ確認などを行います。特に、RTOSプラグインが利用できる場合(STM32CubeIDEにはFreeRTOSに対応したState Viewerプラグインなどがあります)、タスクリストやキュー/セマフォの状態をリアルタイムに確認でき、デバッグ効率が大幅に向上します。
7.4 割り込みとFreeRTOSの連携 (configMAX_SYSCALL_INTERRUPT_PRIORITY
)
第2章でも触れましたが、割り込みハンドラ内からFreeRTOS API (FromISR
関数) を安全に呼び出すためには、configMAX_SYSCALL_INTERRUPT_PRIORITY
の設定が非常に重要です。
ARM Cortex-MマイコンのNVICでは、割り込み優先度は数値が小さいほど高い優先度になります(Priority Groupingの設定によります)。FreeRTOSカーネルが使用するシステムティック割り込みの優先度(通常は最低優先度に近い設定)よりも高い優先度を持つ割り込みハンドラは、FreeRTOSカーネルの処理を中断する可能性があります。このような高い優先度の割り込みハンドラ内からは、FreeRTOSカーネルの整合性を保つために、FromISR
サフィックス付きのAPIであっても呼び出してはなりません。
configMAX_SYSCALL_INTERRUPT_PRIORITY
は、FreeRTOS APIを安全に呼び出せる「ユーザー定義の」割り込みハンドラの最大優先度を示します。この値よりも高い優先度を持つ割り込みハンドラ内からは、FreeRTOS APIを呼び出してはいけません。
STM32CubeMXでFreeRTOSを設定する際に、NVIC設定で割り込み優先度を設定しますが、この際に configMAX_SYSCALL_INTERRUPT_PRIORITY
を考慮する必要があります。通常、CubeMXはデフォルトで安全な設定(FreeRTOSが使用する割り込みよりも低い優先度でユーザー定義の割り込みを許可するなど)を行います。しかし、手動でNVIC設定を変更する際は注意が必要です。優先度設定は、FreeRTOSのポーティング層と使用しているARM Cortex-MプロセッサのInterrupt Controllerのドキュメントをよく確認して行ってください。
7.5 低消費電力機能との連携 (Tickless Idle)
バッテリー駆動の機器などでは、消費電力の削減が重要になります。FreeRTOSは、アイドルタスクが実行され、かつ次のイベント(タスクの起床、ソフトウェアタイマーの満了など)まである程度の時間がある場合に、システムティック割り込みを一時的に停止し、マイコンを低消費電力モード(Sleepモードなど)に移行させるTickless Idle機能 (configUSE_TICKLESS_IDLE
を1に設定) をサポートしています。
Tickless Idleが有効な場合、アイドルタスクは次に発生するイベントまでの時間を計算し、システムティックタイマーを停止して、その時間が経過するか、または別の割り込みが発生するまでCPUを低消費電力モードにします。イベントが発生したり指定時間が経過したりすると、システムは通常モードに復帰し、FreeRTOSは停止していた間のティック数を補正します。
Tickless Idleを効果的に利用するには、アプリケーションの設計において、タスクを頻繁に起床させたり、短い遅延を多用したりしないように工夫が必要です。また、Tickless Idleの実装はポーティング層に依存するため、使用するSTM32シリーズやHALライブラリのバージョンによって動作が異なる場合があります。CubeMXでTickless Idleを有効にした際に生成されるコードや、STM32 HALライブラリの低消費電力関連API (HAL_PWR_EnterSLEEPMode
, HAL_PWR_EnterSTOPMode
など) と連携して動作するIdle Hookの実装を確認することが重要です。
第8章: よくある問題とその解決策
FreeRTOSを使った開発では、特有の問題に遭遇することがあります。ここでは、よく発生する問題とその一般的な解決策を紹介します。
8.1 スタックオーバーフロー
問題: タスクのスタックサイズが不足し、スタック領域を超えてメモリにアクセスしてしまう問題。システムのクラッシュ、予期しない動作、変数破壊などを引き起こします。FreeRTOSを使った開発で最も頻繁に遭遇する問題の一つです。
原因:
* タスク関数内で大きなローカル変数を宣言している。
* 深い関数呼び出しのネストがある。
* 関数内で再帰呼び出しを行っている。
* 割り込みが頻繁に発生し、割り込みハンドラ内で使用されるスタック領域がタスクスタックを圧迫している(FreeRTOSカーネルによっては、タスクスタックとは別にメインスタックやシステムスタックが使用される場合もあります)。
解決策:
* スタックサイズの増加: xTaskCreate
関数で指定するスタックサイズを増やします。必要なスタックサイズは正確に見積もるのが難しいですが、経験則やデバッグツールを使って調整します。
* ローカル変数の削減: 大きな配列などは、可能であればグローバル変数やヒープからの動的割り当て(ただしヒープの断片化に注意)に置き換えます。
* 関数呼び出し構造の見直し: 深いネストや再帰を避けるようにコードをリファクタリングします。
* スタックオーバーフロー検出機能の利用: FreeRTOSConfig.h
のconfigCHECK_FOR_STACK_OVERFLOW
を1または2に設定し、vApplicationStackOverflowHook
フック関数を実装します。これにより、スタックオーバーフローが発生した際にそれを検出し、デバッグ情報を取得できます。
* デバッグツールによる監視: FreeRTOS対応のデバッガプラグインやトレースツール(Segger SystemView, Percepio Tracealyzerなど)は、各タスクのスタック使用状況をリアルタイムに表示する機能を持っていることがあります。これを利用して、タスクの最大スタック使用量を確認し、適切なサイズを設定します。CubeIDEのRTOS State Viewerもスタック使用率を表示できます。
8.2 デッドロック (Deadlock)
問題: 複数のタスクがお互いが保持しているリソースの解放を待ち続け、全てのタスクが永久に待機状態になってしまう問題。システム全体が応答しなくなります。
原因:
* タスクAがリソースXを保持し、リソースYを獲得しようとして待機。
* 同時に、タスクBがリソースYを保持し、リソースXを獲得しようとして待機。
* 結果として、どちらのタスクも必要なリソースを獲得できず、永久に待機してしまう。
解決策:
* リソース獲得順序の統一: 複数のリソースを獲得する場合、全てのタスクで同じ順序で獲得するように規約を定めます。
* タイムアウトの使用: リソースを獲得する際に無限待機 (portMAX_DELAY
) を使用せず、タイムアウトを指定します。タイムアウトが発生した場合は、獲得済みのリソースを解放してエラー処理を行うなどのリカバリロジックを実装します。
* ミューテックスの使用: 排他制御には、可能な限り優先度継承機能を持つミューテックス (xSemaphoreCreateMutex
) を使用します。これにより、優先度逆転に起因するデッドロック(単純なバイナリセマフォで発生しうる)を防ぐことができます。
* 設計の見直し: そもそも複数のタスクで同じリソースを共有する必要があるか、より良い設計方法はないか検討します。例えば、リソースへのアクセスを専用のタスクに集約し、他のタスクはそのタスクにキュー経由で要求を出す、といった設計も考えられます。
8.3 優先度逆転 (Priority Inversion)
問題: 優先度の高いタスクが、優先度の低いタスクが保持している共有リソース(ミューテックスなどで保護されている)を獲得しようとしてブロックされた結果、中間の優先度のタスクが実行されてしまい、あたかも高い優先度のタスクの優先度が下がったかのように見える問題。システムのリアルタイム性を損なう可能性があります。
原因:
* 優先度の低いタスクが共有リソース(例: ミューテックス)を獲得する。
* より優先度の高いタスクが同じリソースを獲得しようとしてブロックされる。
* この間、優先度の低いタスクがリソースを解放する前に、中間の優先度のタスクが実行されてしまう。
解決策:
* ミューテックスの優先度継承: FreeRTOSConfig.h
でconfigUSE_MUTEXES
とconfigUSE_PRIORITY_INHERITANCE
を1に設定し、xSemaphoreCreateMutex()
で生成したミューテックスを使用します。優先度継承が有効な場合、優先度の高いタスクがミューテックスを待ってブロックされると、そのミューテックスを保持している優先度の低いタスクは、一時的に高いタスクと同じ優先度に昇格します。これにより、低いタスクが迅速に実行されてミューテックスを解放し、優先度の高いタスクがすぐに実行を再開できるようになります。
* リソース獲得時間の最小化: 共有リソースを獲得している時間は可能な限り短くします。
* ミューテックスを使用しない設計: 可能であれば、共有リソースへのアクセスを専用タスクに集約するなどの設計変更を検討します。
8.4 タイミング問題
問題: 期待したタイミングでタスクが実行されない、イベントに反応しない、デッドライン(締め切り時間)を満たせないなどの問題。
原因:
* 不適切なタスク優先度設定。
* タスクが無限ループ内でvTaskDelay
を使用せず、CPUを占有している。
* タスクが長時間かかる処理を実行している間に、より高い優先度のタスクがReadyになる。
* IPCメカニズム(キュー、セマフォなど)でのブロッキングが長すぎる。
* システムティックレート (configTICK_RATE_HZ
) の設定が適切でない。
* 割り込み優先度とFreeRTOSカーネルの連携設定 (configMAX_SYSCALL_INTERRUPT_PRIORITY
) の誤り。
解決策:
* 適切な優先度設定: システムのリアルタイム要件に基づいて、タスクの優先度を慎重に設定します。重要な処理や応答性の高い処理を行うタスクには高い優先度を与えます。
* タスクの分割: 長時間かかる処理は、複数のタスクに分割するか、定期的にvTaskDelay
やイベント待ちを入れてCPUを開放するようにします。
* ブロッキングAPIの使用: タスクが何かを待つ必要がある場合は、ポーリングではなく、キュー、セマフォなどのブロッキングAPI (xQueueReceive(..., portMAX_DELAY)
, xSemaphoreTake(..., portMAX_DELAY)
) を使用して、待機中はタスクがBlocked状態になりCPUを他のタスクに明け渡すようにします。
* タイムアウトの利用: 無限待機ではなく、適切なタイムアウトを指定することで、問題発生時にもシステムが完全に停止するのを防ぎます。
* トレースツールによる分析: トレースツールを使用してタスクの実行タイミング、状態遷移、イベント発生などを詳細に分析することで、タイミング問題の原因を特定できます。
8.5 FreeRTOSConfig.h の設定ミス
問題: FreeRTOSConfig.h
の設定値が不適切であるために、FreeRTOSカーネルが正しく初期化されなかったり、予期しない動作をしたりする問題。
原因:
* configCPU_CLOCK_HZ
や configTICK_RATE_HZ
の設定ミス。
* configTOTAL_HEAP_SIZE
が不足している。
* configMAX_PRIORITIES
が小さすぎる。
* configMINIMAL_STACK_SIZE
が小さすぎる(これはタスク作成時の推奨値ですが、タスクによってはこの値では不足します)。
* configMAX_SYSCALL_INTERRUPT_PRIORITY
の設定ミス。
* 使用しない機能(例: ソフトウェアタイマー)関連の設定が有効になっている、あるいはその逆。
解決策:
* 設定値の確認: STM32CubeMXで生成された設定値が、実際のハードウェア構成(特にクロック周波数)やアプリケーションの要件と一致しているか慎重に確認します。
* ドキュメント参照: 各設定項目の意味や制約については、FreeRTOSの公式ドキュメントを必ず参照します。
* 必要な機能のみ有効化: 使用しない機能 (configUSE_TIMERS
, configUSE_EVENT_GROUPS
など) は無効化 (0
) することで、コードサイズとRAM使用量を削減できます。
* エラー検出機能の利用: vApplicationMallocFailedHook
や vApplicationStackOverflowHook
などのエラー検出機能を有効にしておくことで、メモリ関連の問題を早期に検出できます。
* CubeMXの再生成: CubeMXの設定を変更した場合は、必ずコードを再生成して設定が反映されていることを確認します。手動でFreeRTOSConfig.h
を編集した場合、CubeMXで再生成すると上書きされる可能性があるため注意が必要です。CubeMXで設定可能な項目は可能な限りCubeMXで設定し、それ以外の設定は手動で追加編集するのが良い方法です。
これらの問題は、FreeRTOSを使った開発でよく直面するものですが、原因と対策を理解しておけば、より効率的にデバッグを進めることができます。
第9章: まとめと次のステップ
この記事では、STM32マイコンを対象に、リアルタイムOSであるFreeRTOSの基礎から実践的な使い方までを詳細に解説しました。
9.1 FreeRTOSを使った開発のメリットの再確認
RTOS、特にFreeRTOSを導入することで、組み込みシステム開発は以下の点で改善されます。
- 複雑さの管理: アプリケーションを独立したタスクに分割することで、コードの構造化と管理が容易になります。
- 応答性の向上: スケジューラにより、重要なタスクやイベント応答処理が優先的に実行されます。
- 開発効率の向上: タスク管理やタスク間通信・同期といった低レベルな処理をRTOSが肩代わりするため、開発者はアプリケーションロジックに集中できます。
- 保守性と再利用性: モジュール化されたタスクは、変更や他のプロジェクトへの移植が容易になります。
9.2 STM32とFreeRTOSの連携の重要性
STM32は高性能なARM Cortex-Mプロセッサを搭載し、豊富なペリフェラルを備えています。FreeRTOSはARM Cortex-M向けに高度に最適化されたポーティング層を提供しており、STM32の性能を最大限に引き出しながら、リアルタイムなアプリケーションを効率的に開発できます。STM32CubeIDEとSTM32CubeMXの組み合わせは、FreeRTOSのプロジェクト設定とコード生成を容易にし、開発の敷居を大幅に下げています。
9.3 さらに学ぶためのリソース
この記事でFreeRTOSの基本的な使い方を習得しましたが、FreeRTOSは非常に多機能なRTOSです。さらに深く学ぶためには、以下のリソースを活用することをお勧めします。
- FreeRTOS公式ドキュメント: FreeRTOSのウェブサイト (www.freertos.org) には、APIリファレンス、ユーザーガイド、移植ガイドなど、豊富なドキュメントが公開されています。最新かつ正確な情報を得るために、定期的に参照することをお勧めします。
- 書籍: FreeRTOSに関する専門書籍も出版されています。体系的に学びたい場合に役立ちます。
- FreeRTOSコミュニティ: フォーラムやQ&Aサイト (Stack Overflowなど) で、他のFreeRTOSユーザーや開発者に質問したり、情報を共有したりできます。
- STM32 HAL/LLドライバ: STM32のハードウェアペリフェラルを制御するために、STMicroelectronicsが提供するHAL (Hardware Abstraction Layer) またはLL (Low-Layer) ドライバライブラリの使い方を習得する必要があります。
- STM32CubeIDE/CubeMXのドキュメント: STMicroelectronicsが提供する開発環境やツールに関するドキュメントも参照してください。
9.4 応用例と次のステップ
FreeRTOSの基本的な使い方をマスターしたら、より複雑な応用例に挑戦してみましょう。
- ネットワーク機能: FreeRTOS+TCPや、LwIPのようなサードパーティ製TCP/IPスタックをFreeRTOS上で動作させ、ネットワーク通信機能を実装します。
- ファイルシステム: FatFsのようなファイルシステムライブラリをFreeRTOS上で動作させ、SDカードなどにファイルを読み書きする機能を実装します。
- USB: USBホストまたはデバイス機能をFreeRTOS上で実装します。
- GUI: グラフィカルユーザーインターフェースライブラリ(例: LittlevGL/LVGL)をFreeRTOS上で動作させ、ディスプレイ表示機能を実装します。
- より複雑なタスク連携: センサーデータ処理、モーター制御、通信プロトコル処理など、複数の機能をRTOSタスクとして実装し、キュー、セマフォ、イベントグループなどを組み合わせてタスク間の複雑な連携を実現します。
- パフォーマンス最適化: タスク優先度の調整、スタックサイズの最適化、Tickless Idleの活用などにより、システムのパフォーマンスや消費電力を最適化します。
これらの応用例に取り組むことで、FreeRTOSとSTM32を使った組み込みシステム開発スキルをさらに向上させることができるでしょう。
リアルタイムOSは組み込みシステム開発における強力なツールであり、FreeRTOSは特にその軽量さと柔軟性から多くのプロジェクトで採用されています。STM32という高性能なハードウェアと組み合わせることで、様々な要求に応える高機能な組み込みデバイスを開発することが可能です。
この記事が、あなたがSTM32上でFreeRTOSを使った開発を始めるための一助となれば幸いです。 Happy Coding!