組み込みプログラムが大きくなってくると、複数の処理を並行して動かしたくなることがあります。
たとえばIoT機器なら、
- センサーを読み取る
- LEDやモーターを制御する
- UARTで通信する
- Wi-Fiでデータを送信する
- ボタン入力を監視する
- 画面を更新する
といった処理を行うことがあります。
Arduinoのloop()へすべての処理を書くこともできますが、機能が増えるほどプログラム全体の管理が難しくなります。
そこで利用されるのがRTOS(Real-Time Operating System:リアルタイムOS)です。
RTOSでは、プログラムをタスク(Task)という単位に分割し、スケジューラが各タスクを切り替えながら実行します。
この記事では、RTOSとは何かという基本から、タスク・スケジューラ・優先度・タスクの状態、FreeRTOSを使ったESP32での実装例、さらにタスク間通信まで順番に解説します。
RTOSとは?
RTOSはReal-Time Operating Systemの略で、日本語では「リアルタイムOS」と呼ばれます。
組み込み機器で複数の処理を管理し、必要なタイミングで処理を実行するために利用されるOSです。
RTOSでは、処理をタスクという単位に分割できます。
センサー処理
↓
タスクA
通信処理
↓
タスクB
画面更新
↓
タスクC
モーター制御
↓
タスクD
そしてRTOSが、それぞれのタスクをいつ実行するか管理します。
「リアルタイム」とは高速という意味ではない
RTOSを理解するときに非常に重要なのが、リアルタイム=高速ではないという点です。
リアルタイムシステムで重要なのは、単純に平均処理速度が速いことではなく、必要な処理を要求された時間的制約の中で実行できることです。
たとえば、
センサー異常を検出
↓
決められた時間内に
↓
モーターを停止
といった処理では、「いつ実行されるか」が重要になります。
そのためRTOSでは、タスクの優先度や実行状態などを管理し、時間的な要求を満たせるようにシステムを設計します。
普通のOSとRTOSは何が違う?
WindowsやmacOS、Linuxなどの一般的なOSも複数のプログラムを動かしています。
RTOSも複数の処理を管理しますが、組み込み用途では特に処理の応答時間や実行タイミングを予測しやすくすることが重視されます。
| 一般的なOS | RTOS | |
|---|---|---|
| 主な用途 | PC・サーバーなど | 組み込み機器・制御機器など |
| 重視する点 | 操作性・処理能力・機能性など | 時間的制約・予測可能性など |
| タスク管理 | あり | あり |
| 優先度 | あり | リアルタイム制御で重要 |
| 代表例 | Windows・Linuxなど | FreeRTOSなど |
ただし、一般OSとRTOSを完全に二分できるわけではありません。Linuxにもリアルタイム性を高める仕組みがありますし、RTOSにもさまざまな設計があります。
タスクとは?
RTOSでは、実行する処理をタスク(Task)という単位に分割します。
たとえば環境センサーを搭載したIoT機器なら、
Sensor Task ・温度取得 ・湿度取得 Display Task ・OLED更新 Communication Task ・Wi-Fi通信 Button Task ・スイッチ監視
のように役割ごとにタスクを分けることができます。
プログラムを機能単位に分割できるため、大きな組み込みソフトウェアを整理しやすくなります。
スケジューラとは?
複数のタスクが存在すると、「次にどのタスクを実行するのか」を決める必要があります。
その役割を持つのがスケジューラ(Scheduler)です。
Task A ─┐ Task B ─┤ Task C ─┼→ スケジューラ → CPU Task D ─┘
スケジューラは、タスクの状態や優先度などを確認し、どのタスクへCPUの実行時間を与えるか決定します。
CPUが1つでも複数タスクを動かせる
CPUコアが1つの場合、基本的にある瞬間に実行できる命令列は1つです。
それでもRTOSでは、タスクを高速に切り替えることで複数の処理を進めることができます。
時間 → Task A ███ Task B ██ Task C ███ Task A ██ Task B ███ Task C ██
人間から見ると複数の処理が並行して進んでいるように見えます。
このように処理を切り替えながら進めることを並行処理(Concurrency)と呼びます。
複数CPUコアで実際に同時実行する並列処理(Parallelism)とは区別して考えることが重要です。
コンテキストスイッチとは?
CPUがTask AからTask Bへ切り替わるとき、Task Aを途中から再開できるように実行状態を保存する必要があります。
Task Aを実行
↓
CPUの状態を保存
↓
Task Bの状態を復元
↓
Task Bを実行
このようなタスクの切り替えをコンテキストスイッチ(Context Switch)と呼びます。
レジスタやスタックポインタなど、タスクを再開するために必要なCPU状態が切り替えられます。
コンテキストスイッチにも処理時間が必要なので、むやみにタスクを細かく分割すればよいわけではありません。
タスクにはそれぞれスタックが必要
RTOSでは通常、各タスクがそれぞれのスタックを持ちます。
Task A └─ Stack A Task B └─ Stack B Task C └─ Stack C
タスク内の関数呼び出しやローカル変数などに、このスタックが利用されます。
そのためRTOSでは、タスクを増やすほどRAM使用量も増える傾向があります。
スタックについては「スタックとヒープとは?組み込み開発のメモリの仕組みを初心者向けに解説」も参照してください。
タスクの3つの基本状態
RTOSのタスクには状態があります。
まずは次の3つを理解するとよいでしょう。
| 状態 | 意味 |
|---|---|
| Running | 現在CPUで実行されている |
| Ready | 実行可能でCPUが割り当てられるのを待っている |
| Blocked | 時間やイベントなどを待っている |
RTOSや説明の仕方によってはSuspendedなど、ほかの状態もあります。
Runningとは?
Runningは、現在CPU上で実際に実行されている状態です。
Ready ↓ スケジューラに選ばれる ↓ Running
シングルコアCPUなら、ある瞬間にRunningになれるタスクは基本的に1つです。
Readyとは?
Readyは「実行できる準備はできているが、まだCPUを使っていない」状態です。
複数のタスクがReadyなら、スケジューラが優先度などを見て実行するタスクを選択します。
Blockedとは?
Blockedは、何らかの条件を待っている状態です。
- 一定時間待つ
- Queueへデータが届くのを待つ
- Semaphoreを待つ
- イベントを待つ
などがあります。
Running ↓ イベント待ち ↓ Blocked ↓ イベント発生 ↓ Ready
タスクの優先度とは?
RTOSでは、タスクごとに優先度(Priority)を設定できます。
緊急停止 Task 優先度:高 通信 Task 優先度:中 画面更新 Task 優先度:低
FreeRTOSの一般的な設定では、数値が大きいタスクほど高い優先度を持ちます。
より高い優先度のタスクがReadyになると、現在実行中の低優先度タスクからCPUを切り替えることがあります。
このような方式をプリエンプティブスケジューリングと呼びます。
高優先度にすれば速くなるわけではない
初心者が間違えやすいのが、「重要な処理は全部高優先度にすればよい」という考え方です。
高優先度タスクがCPUを使い続けると、低優先度タスクがなかなか実行されない可能性があります。
高優先度Task
████████████████████
低優先度Task
実行したい……
↓
CPUが回ってこない
そのため、タスクの優先度はシステム全体の時間要件を考えて設計します。
vTaskDelay()でタスクを待たせる
FreeRTOSでは、タスクを一定時間待機させるためにvTaskDelay()を利用できます。
vTaskDelay(pdMS_TO_TICKS(1000));
この処理を実行すると、タスクは指定した時間が経過するまでBlocked状態になります。
Task A ↓ vTaskDelay() ↓ Blocked その間 ↓ 別のReadyタスクを実行
CPUを使いながら空回りして待つのではなく、他のタスクへ実行機会を渡せるのが重要なポイントです。
Arduinoのdelay()との違いは?
Arduinoではdelay()をよく使います。
delay(1000);
ESP32のArduino環境自体がFreeRTOS上で動作しているため、内部実装を含めて考えると単純に「delayはCPUを完全に止める、vTaskDelayは止めない」と二分するのは正確ではありません。
RTOSを直接扱うコードでは、タスクが一定時間処理を必要としないことを明示するためにvTaskDelay()を使う、と理解するとよいでしょう。
FreeRTOSとは?
FreeRTOSは、組み込み機器で広く利用されているRTOSの1つです。
タスク管理だけでなく、
- Queue
- Semaphore
- Mutex
- Software Timer
- Event Group
など、複数の処理を協調して動かすための機能を持っています。
ESP32ではFreeRTOSが使われている
ESP32シリーズの開発で使われるESP-IDFでは、FreeRTOSをベースとしたタスク管理機能が利用されています。
そのためKUMITATE-C3で使っているESP32-C3でも、RTOSの考え方を実際に試すことができます。
Arduino環境でESP32を使っている場合でも、その下ではFreeRTOSの仕組みが利用されています。
FreeRTOSでタスクを作ってみよう
ESP32のArduino環境では、FreeRTOSのAPIを利用してタスクを作成できます。
void ledTask(void *pvParameters)
{
const int LED_PIN = 0;
pinMode(LED_PIN, OUTPUT);
while (1) {
digitalWrite(LED_PIN, HIGH);
vTaskDelay(pdMS_TO_TICKS(500));
digitalWrite(LED_PIN, LOW);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
この関数がLEDを制御する1つのタスクになります。
xTaskCreate()でタスクを登録する
作成したタスクをFreeRTOSへ登録するには、xTaskCreate()を利用できます。
void setup()
{
Serial.begin(115200);
xTaskCreate(
ledTask,
"LED Task",
2048,
NULL,
1,
NULL
);
}
代表的な引数は次のような意味を持ちます。
| 引数 | 意味 |
|---|---|
ledTask | 実行するタスク関数 |
"LED Task" | タスク名 |
2048 | タスク用スタックサイズ |
NULL | タスクへ渡すパラメータ |
1 | 優先度 |
NULL | タスクハンドルの保存先 |
なお、スタックサイズ引数の単位や実際に必要なサイズは、使用するFreeRTOSのポートや開発環境を確認して決める必要があります。適当な値をそのまま実製品へ流用するのは避けましょう。
KUMITATE-C3で2つの処理を動かしてみる
KUMITATE-C3を使って、LEDタスクとシリアル出力タスクを動かしてみましょう。
const int LED_PIN = 0;
void ledTask(void *pvParameters)
{
pinMode(LED_PIN, OUTPUT);
while (1) {
digitalWrite(LED_PIN, HIGH);
vTaskDelay(pdMS_TO_TICKS(500));
digitalWrite(LED_PIN, LOW);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void messageTask(void *pvParameters)
{
while (1) {
Serial.println("Message Task");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void setup()
{
Serial.begin(115200);
xTaskCreate(
ledTask,
"LED",
2048,
NULL,
1,
NULL
);
xTaskCreate(
messageTask,
"MESSAGE",
2048,
NULL,
1,
NULL
);
}
void loop()
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
LEDの点滅とシリアル出力が、それぞれ別のタスクとして進みます。
この例では同じ優先度を設定しています。実際のスケジューリング動作はFreeRTOSの設定や各タスクの状態などにも依存します。
RTOSなら何でもタスクに分ければよい?
RTOSを使えるようになると、すべての処理を別々のタスクへ分けたくなるかもしれません。
しかし、タスクを増やすと、
- タスクごとのスタックが必要になる
- コンテキストスイッチが増える
- タスク間通信が必要になる
- 共有データの排他制御が必要になる
- 優先度設計が複雑になる
などのコストも増えます。
「処理を独立したタスクとして管理する意味があるか」を考えて分割することが重要です。
タスク同士でデータを渡すには?
複数のタスクを作ると、次に必要になるのがタスク間通信です。
たとえばセンサータスクで取得した温度を、表示タスクへ渡したい場合を考えます。
Sensor Task
↓
25.4℃
↓
Queue
↓
Display Task
↓
OLEDへ表示
このような用途で利用できる仕組みの1つがQueue(キュー)です。
Queueとは?
Queueは、タスク間でデータを受け渡すために利用できる仕組みです。
基本的には、先に入れたデータを先に取り出すFIFO(First In, First Out)として動作します。
送信
A → B → C
↓
Queue
取り出し
A → B → C
前に学習したリングバッファと似た考え方が登場しますが、FreeRTOSのQueueはRTOSが提供するタスク間通信機能として、待ち合わせや同期と組み合わせて利用できます。
リングバッファについては「リングバッファとは?仕組みとUART受信での使い方を初心者向けに解説」も参照してください。
Queueを作成する
QueueHandle_t sensorQueue;
void setup()
{
sensorQueue = xQueueCreate(
10,
sizeof(float)
);
}
この例では、float型のデータを最大10個保持できるQueueを作成しています。
Queueへデータを送信する
float temperature = 25.4f;
xQueueSend(
sensorQueue,
&temperature,
portMAX_DELAY
);
センサータスクからQueueへデータを送信できます。
Queueからデータを受信する
float temperature;
if (xQueueReceive(
sensorQueue,
&temperature,
portMAX_DELAY
) == pdTRUE) {
Serial.println(temperature);
}
データが届いていなければ、指定した待ち時間に応じてタスクをBlocked状態にできます。
このように、RTOSでは「データが来るまでCPUを使って何度も確認する」のではなく、必要なイベントが発生するまでタスクを待機させる設計ができます。
グローバル変数で共有するだけではダメ?
タスク同士でデータを共有するだけなら、グローバル変数を使う方法も考えられます。
float temperature;
しかし、複数のタスクが同じデータを同時に読み書きするようになると、競合を考える必要があります。
Task A
↓
共有データへ書き込み
← 同じタイミング →
Task B
↓
共有データを読み出し
このような問題を理解するうえで重要になるのが、次に学習するMutexやSemaphoreです。
volatileを付ければタスク間共有は安全?
ここで、以前学習したvolatileを思い出すかもしれません。
volatile int value;
しかし、volatileを付けるだけでは複数タスクからの同時アクセスを安全にはできません。
volatileは、アクセスの最適化などに関係する指定であり、Mutexのような排他制御を提供するものではありません。
詳しくは「constとvolatileとは?組み込み開発での意味と使い方を初心者向けに解説」も参照してください。
ステートマシンとRTOSはどちらを使う?
前回の記事ではステートマシンを紹介しました。
ステートマシンとRTOSは、どちらか一方しか使えないものではありません。
たとえば、
Communication Task
↓
通信処理のステートマシン
Motor Task
↓
モーター制御のステートマシン
UI Task
↓
画面遷移のステートマシン
のように、RTOSで機能ごとにタスクを分け、そのタスク内部をステートマシンで設計することもできます。
ステートマシンについては「ステートマシン(状態遷移)とは?組み込み開発での設計と実装方法を初心者向けに解説」も参照してください。
割り込みとRTOSの関係
RTOSを使っていても、ハードウェア割り込みは重要です。
UART受信 ↓ 割り込み ↓ RTOSへイベントを通知 ↓ 待機していたTaskがReady ↓ Taskで本格的な処理
割り込み処理そのものを長くするのではなく、必要な情報をタスクへ通知し、重い処理をタスク側で行う設計がよく使われます。
ただし、割り込みから呼び出せるRTOS APIには制約があります。FreeRTOSではFromISR系APIなど、割り込み用に用意された関数を使用する場面があります。
RTOSを使うメリット
- 複数の処理をタスクとして分離できる
- 処理ごとに優先度を設定できる
- イベント待ちの処理を効率よく記述できる
- Queueなどでタスク間通信ができる
- SemaphoreやMutexなどの同期機能を利用できる
- 機能が増えたプログラムを整理しやすい
- リアルタイム性が必要な処理を設計しやすい
RTOSを使うデメリット・注意点
- プログラムの仕組みが複雑になる
- タスクごとのスタック領域が必要になる
- 共有データの排他制御が必要になる
- 優先度の設計が必要になる
- デッドロックなど並行処理特有の問題が発生する
- 不具合がタイミング依存になり、再現しにくい場合がある
- コンテキストスイッチなどのオーバーヘッドがある
RTOSが必要ない場合もある
RTOSは便利ですが、すべての組み込みプログラムに必要なわけではありません。
たとえば、
入力確認 ↓ センサー取得 ↓ 状態更新 ↓ 出力更新 ↓ 最初へ戻る
という単純なメインループやステートマシンで十分に管理できるシステムもあります。
RTOSを導入するとタスク間同期やスタック管理など、新しい設計要素も増えます。
「RTOSを使えるから使う」のではなく、システムの規模や時間要件、処理の独立性などを考えて選択することが重要です。
RTOSが使われる場面
- IoT機器
- 産業機器
- ロボット
- 計測機器
- 通信機器
- 家電製品
- センサー端末
- モーター制御機器
- 複数の通信インターフェースを持つ機器
特に、「複数の独立した処理があり、それぞれ異なるタイミングで動作する」システムではRTOSが役立ちます。
RTOSを学ぶと組み込みソフトの見え方が変わる
ここまで学習してきた内容は、RTOSを理解すると1つにつながってきます。
GPIO
↓
ハードウェア制御
割り込み
↓
イベント発生
タイマー
↓
時間管理
スタック
↓
タスクごとの実行領域
関数ポインタ
↓
コールバック
リングバッファ
↓
データの一時保存
ステートマシン
↓
状態管理
↓
RTOS
↓
複数の処理をタスクとして管理
RTOSは突然登場する特殊な仕組みではなく、これまで学習してきた組み込み開発の基礎知識を組み合わせて、大きなソフトウェアを管理するための仕組みと考えると理解しやすくなります。
RTOSを理解したら次に学ぶこと
RTOSの基本を理解したら、次はタスク同士が安全に協調して動くための仕組みを学んでいきます。
RTOS ↓ Task ↓ Queue ↓ Semaphore ↓ Mutex ↓ 共有リソース ↓ 競合・排他制御 ↓ Deadlock ↓ Priority Inversion
特にQueue・Semaphore・Mutexは、FreeRTOSを使った実際の組み込み開発で頻繁に登場します。
まとめ
RTOSは、複数の処理をタスクとして管理し、必要なタイミングで実行するためのリアルタイムOSです。
- RTOSはReal-Time Operating Systemの略
- リアルタイムとは単に高速という意味ではない
- 処理をタスクという単位に分割できる
- スケジューラが実行するタスクを選択する
- タスクにはRunning・Ready・Blockedなどの状態がある
- タスクごとに優先度を設定できる
- タスク切り替えをコンテキストスイッチと呼ぶ
- 各タスクは通常それぞれのスタックを持つ
- vTaskDelay()などでタスクを待機状態にできる
- FreeRTOSは代表的な組み込み向けRTOSの1つ
- ESP32シリーズではFreeRTOSの仕組みを利用できる
- Queueを使ってタスク間でデータを渡せる
- volatileだけではタスク間の排他制御にはならない
- ステートマシンとRTOSを組み合わせることもできる
Arduinoのsetup()とloop()だけでも多くの機器を作れますが、処理が増えてくると「どの処理を、いつ、どの優先度で実行するのか」という問題が出てきます。
RTOSを理解すると、複数の処理をタスクへ分割し、それらを協調して動かすという、本格的な組み込みソフトウェアの設計へ進めるようになります。

