この記事は がじぇるねGR Advent Calendar 2021 の14日目です。
GR-ROSEでは、FreeRTOSとAzure RTOSという2つのRTOSをお手軽に使うことができます。
この記事ではGR-ROSEでこの2つのRTOSを比較します。
GR-ROSEでFreeRTOSとAzure RTOS
GR-ROSEの開発環境はWebコンパイラ、e2studio、IDE for GRの3種があり、どれもFreeRTOSベースのArduino互換SDKが基本です。Arduino互換のAPIが使えるので、FreeRTOSをまったく意識しなくともプログラムできますが、FreeRTOSのAPIを使用すればマルチタスクなプログラミングも可能です。
#include <Arduino.h>
#include <FreeRTOS.h>
#include <task.h>
#include <semphr.h>
#include <queue.h>
いっぽう、Azure RTOSに関してはWebコンパイラとe2studioでAzure IoT Central用のサンプルプロジェクトが提供されています。こちらもArduino互換SDKですが、Azure RTOSのAPIを使用すればマルチタスクなプログラミングも可能です。IDE for GRはサポートしてません。
#include <Arduino.h>
#include <tx_api.h>
まずはRTOSの根幹であるマルチタスクから。(FreeRTOSではタスク、Azure RTOSではスレッドという用語を使いますが、方言の差くらいに捉えておきます。)
FreeRTOSでは次のように書きます。タスク関数へ渡す引数やハンドルの取得は不要ならNULLを渡します。
結果 = xTaskCreate(
タスク関数, タスクの名前, スタックサイズ,
タスク関数へ渡す引数のポインタ, 優先度, ハンドルを返すポインタ );
Azure RTOSでは次のように書きます。スタックに用いるメモリは予め確保した領域を渡します。プリエンプション閾値とタイムスライス値については後述します。自動スタートは、スレッドをすぐに開始するならTX_AUTO_STARTを、保留状態にするならTX_DONT_STARTを渡します。
結果 = tx_thread_create(
ハンドルを返すポインタ, スレッドの名前, スレッド関数,
スレッド関数へ渡す引数, スタック先頭へのポインタ, スタックサイズ,
優先度, プリエンプション閾値, タイムスライス値, 自動スタート);
優先度の数字に注意してください。FreeRTOSでは数字の大きいほうが優先度が高くなるのに対し、Azure RTOSでは数字の小さいほうが優先度が高くなります。
GR-ROSEのメインタスク( setup()とloop() )は、FreeRTOS環境では優先度3、スタック512バイトに設定されています。(FreeRTOS/src/amazon_freertos_common/freertos_start.c)
いっぽう、Azure RTOS環境では優先度4、スタック4キロバイトに設定されています。(src/main.c)
マルチタスクで並列処理をすれば、たいていの場合はミューテックスなどを使った排他制御が必要になります。
FreeRTOSでは次のように書きます。最大待ち時間はtick数で指定します。後述しますが、GR-ROSEでは tick = 1msec となっています。portMAX_DELAYを指定すると永久に待ちます。
SemaphoreHandle_t mutex;
mutex = xSemaphoreCreateMutex();
結果 = xSemaphoreTake(mutex, 最大待ち時間);
結果 = xSemaphoreGive(mutex);
Azure RTOSでは次のように書きます。優先度継承については後述しますが、 TX_INHERITを指定すると優先度継承が有効となり、TX_NO_INHERITを指定すると無効になります。待ちオプションは、TX_NO_WAITを指定すると待たずに即座に戻り、TX_WAIT_FOREVER を指定すると永久に待ち、それ以外の値の場合は指定したtick数まで待ちます。GR-ROSEでは tick = 10msec となっています。
TX_MUTEX mutex;
結果 = tx_mutex_create(&mutex, ミューテックスの名前, 優先度継承);
結果 = tx_mutex_get(&mutex, 待ちオプション);
結果 = tx_mutex_put(&mutex);
優先度継承(Priority Inheritance)とは、スレッド間で待ち合いをする場合にそれらのスレッドの中の最高の優先度でクリティカルセクションを実行するというものです。優先度の高いスレッドが優先度の低いスレッドに待たされて遅れるケース(優先度の逆転)を緩和する狙いがあります。
FreeRTOSではミューテックスは必ず優先度継承になります。タスク間の同期など優先度継承をすべきでない場合にはミューテックスではなくバイナリーセマフォを用います。
タスク間通信
マルチタスクでもう一つよく使うのが、キューを使ったタスク間のメッセージのやりとりです。
FreeRTOSでは次のように書きます。最大待ち時間は前述のミューテックスの場合と同様です。
QueueHandle_t queue;
queue= xQueueCreate(キューの長さ, 要素のサイズ);
結果= xQueueSend(queue, 要素へのポインタ, 最大待ち時間);
結果 = xQueueReceive(queue, 要素を返すポインタ, 最大待ち時間);
Azure RTOSでは次のように書きます。要素のサイズは32ビットワード単位であることに注意してください。指定できる値は1~16です。キューに用いるメモリは予め確保した領域を渡します。キューのサイズはトータルのバイト数を指定します。待ちオプションは前述のミューテックスの場合と同様です。
TX_QUEUE queue;
結果 = tx_queue_create(&queue, キューの名前, 要素のサイズ, キューの先頭へのポインタ, キューのサイズ);
結果 = tx_queue_send(&queue, 要素へのポインタ, 待ちオプション);
結果 = tx_queue_receive(&queue, 要素を返すポインタ, 待ちオプション);
プリエンプション
プリエンプションとは、現在実行中のタスクより優先度の高いタスクが実行可能である場合にOSが強制的にタスクスイッチすることです。プリエンプションが無いと、タスクが自発的に待ち状態に入らないかぎりタスクスイッチは起こりません。これを協調的マルチタスクと言います。
【FreeRTOSの場合】
FreeRTOSはプリエンプティブマルチタスクにも協調的マルチタスクにも設定できます。GR-ROSEのFreeRTOSはプリエンプティブマルチタスク、同一優先度タスクのタスクスイッチ(タイムスライス)あり、システム割り込み周期(tick)は1msecに設定されています。
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configTICK_RATE_HZ (1000)
【Azure RTOSの場合】
Azure RTOSは基本的にプリエンプティブマルチタスクですが、スレッドごとに生成時にtx_thread_createの引数で優先度の他にプリエンプション閾値、タイムスライス値が設定できます。また TX_DISABLE_PREEMPTION_THRESHOLD を定義することでプリエンプションを無効にも設定できるようです。
プリエンプション閾値より優先度の高いスレッドのみがプリエンプションの対象になります。したがって、プリエンプション閾値は優先度以下の値を指定します。プリエンプション閾値に0を設定するとプリエンプションはおこなわれません。
タイムスライス値は、同じ優先度の実行可能状態のスレッドがあるときにタスクスイッチをおこなう時間をtick数で指定します。優先度とプリエンプション閾値に同じ値を設定したときのみ有効です。タイムスライス値にTX_NO_TIME_SLICE (=0)を設定するとタイムスライスが無効になります。つまり同じ優先度のスレッド間ではプリエンプションがおこなわれなくなります。
GR-ROSEのAzure RTOSでは、メインスレッドは優先度=プリエンプション閾値、タイムスライス値はTX_NO_TIME_SLICEに設定されています。
※ 現状、GR-ROSEのAzure RTOSではプリエンプションがうまく機能していないようです。この件はRulzにて問い合わせ中です。