μT-Kernel 3.0とFreeRTOS

UCTで移植して、UCTのGitから公開しているインフィニオンのマイコン用のμT-Kernel 3.0を移植した時の経験をもとにしたチュートリアルです。

μT-Kernel 3.0から見たFreeRTOSの詳細仕様

μT-Kernelプログラマ向けFreeRTOS用モジュール移植技術

機器制御目的でリアルタイムOSを採用する場合、ITRONからの流れを汲みシリーズのシェアが高く、国際的スタンダードIEEE 2050-2018に準拠の最新版μT-Kernel 3.0が第一候補となろう。一方、AmazonがReal Time Engineerから買収したFreeRTOSも目につくようになった。

本解説は、FreeRTOSベースに作られたモジュールをμT-Kernel 3.0上に移植する場合のノウハウとして、リアルタイムOS両者の差異やコンセプトの違いについて述べている。

今回の事例では、インフィニオンの評価ボードCY8CKIT-062S2-43012にμT-Kernel 3.0を搭載。その上にインフィニオンが提供しているWi-Fiサンプルアプリを移植しようというものである。このサンプルアプリにはFreeRTOSが含まれていたので、結果としてFreeRTOS用に作られたコードをμT-Kernel 3.0版に書き換えるにはどうすればよいかという話になっている。

FreeRTOS → μT-Kernel 3.0

さて、FreeRTOSをμT-Kernel 3.0に変更しなければならいわけだが、どうすればいいのか?

調査した結果、COMPONENT_FREERTOS/ というディレクトリがあり、その中でアプリケーションやドライバ、ライブラリが必要としている関数がFreeRTOSのAPIを利用して実装されていた。
結局、この並びにCOMPONENT_MTKERNEL3/を作り、必要な機能(関数)をμT-Kernel 3.0で実装する必要がある。

μT-Kernel 3.0もFreeRTOSもプリエンプティブなマルチタスクRTOSだが、FreeRTOSとμT-Kernel 3.0とは細かな違いがいろいろとあり、右から左へとはいかない。

1. 比較表:μT-Kernel 3.0とFreeRTOSの違い

今回の目的は「FreeRTOS(のAPI)をμT-Kernel 3.0(のAPI)に置き換える」ことなので、FreeRTOSにはあるがμT-Kernel 3.0にはない機能が問題となる。μT-Kernel 3.0にない機能は別の機能で置き換える必要があることを確認する目的でFreeRTOSの機能を元に両OSの機能を比較すると以下のようになる。

機能FreeRTOSμT-Kernel 3.0備考
Task Creationタスク生成、削除
Task Control実行待ち、強制待ち/再開など
Task Utilities基本機能は同じ。
RTOS Kernel Control
Direct To Task Notificationstk_slp_tsk/tk_wup_tskに類似の機能
Queues×メッセージバッファで類似の機能を実装可能
Queue Sets×Queueの拡張機能
Stream Buffers×
Message BuffersFreeRTOSはStream Buffersベースなのでその制限を引き継ぐ
Semaphore / MutexesμT-Kernel 3.0に再帰ミューテックスはない。
Software Timers周期ハンドラ、アラームハンドラ
Event Groups (or ‘flags’)イベントフラグ
FreeRTOS-MPU Specific×
Co-routines×非プリエンプティブ方式のマルチタスク

この表の機能の項目はFreeRTOSのAPI Referenceをベースに作成したが、上の比較表に含まれてないμT-Kernel 3.0の機能をベースにして表の続きを作ると以下になる。
(項目としてはもっと多いが、主な機能のみのピックアップ)

機能FreeRTOSμT-Kernel 3.0備考
タスク例外処理機能×μT-Kernel 3.0も仕様のみ。実装はされていない。
メールボックス×
ランデブ×μT-Kernel 3.0では仕様範囲外。実装には含まれる。
固定長メモリプール×
割込み管理機能×
システム状態管理機能×
サブシステム管理機能×μT-Kernel 3.0も仕様のみ。実装はされていない。
デバッグサポート機能×(*注)

(*注) FreeRTOSにはタスクのリストを取得するvTaskList()といったAPIが用意されているが、 キューやセマフォ、メッセージバッファなどに対しては、このようなAPIは用意されておらず、 統一的な機能ではない。
このため、vTaskList()はデバッグサポート機能ではなく単なるタスク専用のユーティリティ であると判断して×にした。  

また、前半の比較表については、以下も考慮する必要がある。

  • Queues と Queue Sets、Stream Buffers と Message Buffers
    QueuesとQueue Setsが別になっているが、これは項目としては1つとみなせる。
    また、Stream BuffersとMessage Buffersが別になっているが、Message BuffersはStream Buffersを使っているので機能としてまとめてもおかしくない。
    そうするとμT-Kernel 3.0の × の数は必然的に減っていくことになる。
  • Semaphore と Mutexes
    Semaphore / Mutexes は別の機能ではないのか?
    別にすればSemaphoreはμT-Kernel 3.0でも ○ になる。
  • Queue はメッセージバッファで代替可能
    μT-Kernel 3.0にはQueueがないので、ここは × となる。
    ただ、この機能はメールボックスやメッセージバッファで同じような機能を作ることができる。
    しかも、FreeRTOSのMessage BuffersはStream Buffersベースであるがための制限があるが、μT-Kernel 3.0のメッセージバッファにはそういった制限がない。
  • Co-routines
    Co-routines は非プリエンプティブ方式のマルチタスク用の機能であり、「プリエンプティブ」を謳っているRTOSで積極的に利用するものでもない。つまり、今回はこの項目の ○ / × は無意味である。

結局、比較表というのは参考程度のものと考えるべきである。
アプリケーションとして「この機能を使いたい」という機能があれば、それを中心に考えて両OSの仕様を詳細に比較する方がよいだろう。

FreeRTOSを使って作られていた機能をμT-Kernel 3.0を使って置き換える際には『比較表では表されないような細かな違い』が非常に重要である。
以下、機能毎にμT-Kernel 3.0とFreeRTOSの機能を比較しながら説明する。

(アプリケーションに提供すべき)機能によってはFreeRTOSにない機能やμT-Kernel 3.0にない機能も存在する。このような機能についてはそれぞれ別の機能を使って置き換えている。別の機能を使っている場合はAPIや機能の比較はできないので説明はしていない。

2. 対象ファイルと比較対象機能

ModusToolboxでは、アプリケーションやドライバ、ライブラリが必要としている関数がCOMPONENT_FREERTOS/ の下にまとめられている。それを参考に COMPONENT_MTKERNEL3/ にμT-Kernel 3.0用のファイルを作成する。
実際に作成したファイルは以下のとおりである。

  • mtb_shared/abstraction-rtos/release-v1.5.0/source/COMPONENT_MTKERNEL3/cyabs_rtos_mtkernel3.c
  • mtb_shared/abstraction-rtos/include/COMPONENT_MTKERNEL3/cyabs_rtos_impl.h
  • mtb_shared/wifi-mw-core/release-v3.4.0/lwip-whd-port/COMPONENT_MTKERNEL3/arch/sys_arch.c
  • mtb_shared/wifi-mw-core/release-v3.4.0/lwip-whd-port/COMPONENT_MTKERNEL3/arch/cc.h
  • mtb_shared/wifi-mw-core/release-v3.4.0/lwip-whd-port/COMPONENT_MTKERNEL3/arch/sys_arch.h
  • mtb_shared/clib-support/release-v1.2.0/COMPONENT_MTKERNEL3/cy_mutex_pool.c

※各実装にはリビジョンが複数あったが、今回はWi-FiのTCP Clientサンプルで利用しているCOMPONENT_FREERTOS/ の並びにCOMPONENT_MTKERNEL3/ を配置した。

各ファイルの概要は以下のとおりである。

(1) cyabs_rtos_mtkernel3.c  

RTOSの機能を汎用化してModusToolboxのサンプルアプリケーションに提供するためのラッパーらしい。
このファイルではμT-Kernel 3.0とFreeRTOSの以下の機能を利用しているので、これらの差異について説明する。

この他に、キューも用意されているが、μT-Kernel 3.0にはキューが含まれていないので、メッセージバッファで代用した。FreeRTOSとμT-Kernel 3.0で別の機能を用いて実装しているのではキューという機能の比較にならないのでこの機能については省略する。

(2) sys_arch.c

lwIPに対して排他制御機構などを提供するためのファイルであり、以下の機能が利用されている。

  • ミューテックス
  • セマフォ
  • メールボックス

cyabs_rtos_mtkernel3.c にない機能としてはメールボックスが含まれている。
メールボックス機能の差異について比較したいところだが、FreeRTOSにはメールボックスがない(Queueで代用)。このため機能や個別のAPIの比較はできないのでこれも省略する。

(3) cy_mutex_pool.c

clib用に提供するRTOS機能であり、ミューテックスが利用されている。ミューテックスの差異については既に説明しているので、ここでは省略する。

(4) ヘッダファイル

移植作業ではヘッダファイルの追加も必要であるが、本記事の主眼であるμT-Kernel 3.0とFreeRTOSの差異の説明とは直接関係しないので、これらの説明は省略する。

3. 機能毎の比較

cyabs_rtos_mtkernel3.cには、RTOSの機能を汎用化してModusToolboxのサンプルアプリケーションに提供するためのラッパー関数が集められている。

ここには以下の機能が実装されている。

以下、各機能を実装するために利用されている各OSのAPIとその違いについて説明する。

※キューは比較対象外
FreeRTOSにはキューの機能があるのでそれを利用して実装されている。
μT-Kernel 3.0にはキューの機能がないので、メッセージバッファで代用している。
直接対応する機能がなく、個別のAPIの比較はできないので説明は省略する。

3-1. タスク制御

タスクの生成と開始

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_create_thread()xTaskCreateStatic()tk_cre_tsk(), tk_sta_tsk()

タスクを生成し、実行を開始する。
xTaskCreateStatic()をtk_cre_tsk()とtk_sta_tsk()に置き換えることで同じ機能を実現できる。

FreeRTOSではxTaskCreateStatic()で生成と実行が同時に行われる。
“Create”とあるので、生成だけかと思ったのだがそうではなく、生成と同時に起動されている。確かにFreeRTOSのタスク状態遷移図を見ると、(μT-Kernel 3.0の)「休止状態(DORMANT)」がないので、生成すればREADY状態(またはRUNNING状態)になってしまう。このため、μT-Kernel 3.0ではtk_cre_tsk()でタスクを生成した後で、tk_sta_tsk()によってタスクを開始することで同じ動作にしている。

図1 FreeRTOSのタスク状態遷移図

図2 μT-Kernelのタスク状態遷移図

なお、FreeRTOSではcy_rtos_create_thread()でタスク用のメモリを確保してスタックとしてAPIに渡していた。このメモリ領域を解放するタイミングを調整する必要があるので少々面倒な処理が入っており、以下のAPIも合わせて利用されている。

  • pvPortMalloc() … メモリ確保
  • xSemaphoreCreateBinary() … バイナリセマフォの生成

μT-Kernelではその必要はないので、単純にtk_cre_tsk()とtk_sta_tsk()を呼び出すだけで済んでいる。
μT-Kernelにもタスクのスタック領域をアプリケーション側に指定させる機能はあるが、特殊な事情でもない限りこの機能を利用することはない。
また、FreeRTOSにもアプリケーションでメモリを用意する必要のないxTaskCreate()もある。

タスクの終了

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_exit_thread()vTaskDelete()tk_exd_tsk()

タスクの終了と削除はFreeRTOSであればvTaskDelete()、μT-Kernel 3.0であればtk_exd_tsk()である。
ただし、FreeRTOSの方はタスクの生成と開始で説明したとおり、cy_rtos_create_thread()でメモリの確保も行っているのでこのメモリの解放が必要となる。しかも、解放するタイミングの調整が必要らしく、若干面倒な処理が追加されていた。(これもxTaskCreate()を使えば不要になるのだろう。)

一方、μT-Kernel 3.0ではtk_exd_tsk()のみでタスクの終了と削除が実行できる。
また、μT-Kernel 3.0ではタスクを削除せずに残しておくこともできる(タスク状態遷移図を参照)。
タスクを削除せずに休止状態にする場合はtk_ext_tsk()で終了する。この場合、tk_sta_tsk()でタスクを再度起動(タスクの最初から実行)することができる。

  • tk_ext_tsk() … タスクの終了 (exit)
  • tk_exd_tsk() … タスクの終了と削除 (exit & delete)

タスクの強制終了

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_terminate_thread()vTaskDelete()tk_ter_tsk(),tk_del_tsk()

FreeRTOSにはタスクを強制終了(Terminate)する機能はないので、vTaskDelete()で削除している。また、先に述べたとおりタスクを終了する場合はライブラリ内で確保したメモリの解放処理が必要である。

μT-Kernel 3.0ではそのような処理は必要ないので、tk_ter_tsk()とtk_del_tsk()を連続して呼び出すだけで済む。
当然、μT-Kernel 3.0であればタスクを強制終了するだけで削除しないでおけばタスクを再度起動させることも可能であるが、機能的にFreeRTOS版と同じ動作にする必要があるのでタスクの削除まで実行している。

μT-Kernel 3.0ではtk_ter_tsk()は原則として使用しないことを推奨しているので注意が必要である。
μT-Kernel 3.0に限った話ではないが、強制的にタスクを終了させるとシステムに異常をきたす可能性がある。
単純には確保したメモリやカーネルオブジェクトを解放せずに残してしまうことがある。
ただ、実際には強制的に終了しなければならない場合もあるので、このようなAPIも用意されている。

タスクの実行状態を確認

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_is_thread_running()eTaskGetState()tk_ref_tsk()

指定されたタスクが実行中かどうかを判定している。
FreeRTOSではeTaskGetState()で実装するが、μT-Kernel 3.0ではそのような単機能なAPIは用意していないので、タスクの各種情報を一括で取得できるtk_ref_tsk()を用いて実装した。

タスク識別子の取得

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_get_thread_handle()xTaskGetCurrentTaskHandle()tk_get_tid()

このAPIは両OSで対応しているAPIがあるのでそのまま置き換えた。

タスクの待ちとタスクの待ち解除

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_wait_thread_notification()ulTaskNotifyTake()tk_slp_tsk(), tk_can_wup()
cy_rtos_set_thread_notification()xTaskNotifyGive(),
vTaskNotifyGiveFromISR(),
portEND_SWITCHING_ISR()
tk_wup_tsk()

タスクの待ちと待ち解除の機能である。
FreeRTOSではこの機能のためにいろいろなAPI(やマクロ)が用意されているようだが、本機能はその中でも単純なAPIを利用して実装されていた。  
FreeRTOSとμT-Kernel 3.0のAPIの基本的な対応は以下になる。

機能FreeRTOSμT-Kernel 3.0
待ちulTaskNotifyTake()tk_slp_tsk()
待ち解除xTaskNotifyGive()tk_wup_tsk()

本機能でのFreeRTOSとμT-Kernel 3.0の大きな違いは待ち解除要求をキューイングするかどうかだろう。μT-Kernel 3.0では、タスクが待っていない状態でtk_wup_tsk()を呼び出すと待ち解除要求の数が追加(インクリメント)されていく。tk_slp_tsk()では、待ち解除要求の数が1以上であれば待ちに入らない(デクリメントして処理を継続する)ので、待ち解除の要求数が0になるまでは、tk_slp_tsk()を発行しても待ちにはならない。一方、FreeRTOSでは待ち解除の要求はキューイングされない(0から1にはなるが、2以上にはならない)。
このため、単純に実装すると両者の挙動に違いがでるので、μT-Kernel 3.0ではtk_slp_tsk()の待ちが解除された後で起床要求のキューイング数をクリアする処理を追加してある。

ところで、FreeRTOSではタスクから呼び出すAPIと割込みハンドラ(ISR)から呼び出すAPIが別になっている。ISRから呼び出すAPIは ~FromISR() という名称になっており、こちらを呼び出さなければならない。このため、cy_rtos_wait_thread_notification() には呼び出したコンテキストがタスクかISRかを呼び出し元に指定させるための引数が用意されている。
一方、μT-Kernel 3.0ではタスクでもISR(μT-Kernel 3.0では「タスク独立部」と呼ぶ)でも同じAPIを利用可能である。呼び出してよいかどうかはRTOS側で判定し、呼び出せない場合はエラーが返される。しかも、原則として待ちが発生しないAPIであれば「タスク独立部」から呼出可能であり、FreeRTOSのように待ち解除用のAPIで呼び出し側のコンテキストを意識する必要はない。

ちなみに、FreeRTOSでは、相手側のタスクが待ち状態でない場合に待ち解除のAPIを発行する側のタスクを待ち状態にする機能もタスク通知機能の中に用意されている。μT-Kernel 3.0のtk_slp_tsk() / tk_wup_tsk()にこのような機能はないが、バッファサイズを0にして生成したメッセージバッファで同じような機能を実現可能である。

タスクの遅延

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_delay_milliseconds()vTaskDelay()tk_dly_tsk()

タスクを指定された時間だけ待ち状態に移行させる。機能としては同じである。

ただし、FreeRTOSのAPIでは待ち時間をティックに変換する必要がある。マクロは用意されているが、これはいささか面倒である。
しかも、”Mastering the FreeRTOS Real Time Kernel” のp.62には以下のように書かれている。

Note: It is not recommended to specify times in ticks directly within the application, but instead to use the pdMS_TO_TICKS() macro to specify times in milliseconds, and in so doing, ensuring times specified within the application do not change if the tick frequency is changed.

注:アプリケーション内で直接ティック単位で時間を指定することは推奨しません。その代わりに pdMS_TO_TICKS() マクロを使用してミリ秒単位で時間を指定することを推奨します。そうすることで、ティック周波数が変更されてもアプリケーションで指定された時間が変更されなくなります。

ティックでタイムアウトを指定することのデメリットを理解しているからこのように書いているのであろう。であれば、「タイムアウトにはミリ秒を指定する」と仕様で決めてくれてもいいような気がする。この処理を仕様に持って行った(仕様として決めた)のがITRONであり、μT-Kernelであるとも言える。

3-2. ミューテックス

ミューテックスの生成

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_init_mutex2()xSemaphoreCreateMutex(),
xSemaphoreCreateRecursiveMutex()
tk_cre_mtx()

本関数ではミューテックスを生成する。
FreeRTOSでは(通常の)ミューテックスと再帰ミューテックスが利用可能である。
一方のμT-Kernel 3.0には(通常の)ミューテックスはあるが、再帰ミューテックスはない。(必要であれば通常のミューテックスとタスクIDを取得するtk_get_tid()、カウンタ変数などを組み合わせて実装することになる。)

現在インストールしてある(FreeRTOSを利用している)サンプルはWi-Fiだけだが、 このコードの中を確認するとcy_rtos_init_mutex2()は再帰的ミューテックスを生成する設定で呼び出されている。 ただし、アプリケーション側の実装はシンプルなcy_rtos_get_mutex()~cy_rtos_set_mutex()の組みになっており、 再帰ミューテックスを必要としている箇所はないようであった。

この他に優先度逆転を防ぐための機構にも違いがある。FreeRTOSのミューテックスでは優先度継承プロトコルしか利用できないが、μT-Kernel 3.0は優先度継承プロトコルと優先度上限プロトコルに対応している。

ミューテックス機能FreeRTOSμT-Kernel 3.0
(通常の)ミューテックス
再帰ミューテックス×
優先度継承プロトコル
優先度上限プロトコル×

ミューテックスの削除、ロック、アンロック

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_deinit_mutex()vSemaphoreDelete()tk_del_mtx()
cy_rtos_get_mutex()xSemaphoreTake(),
xSemaphoreTakeRecursive()
tk_loc_mtx()
cy_rtos_set_mutex()xSemaphoreGive(),
xSemaphoreGiveRecursive()
tk_unl_mtx()

これらはそれぞれに対応したAPIが用意されているので、特に問題なく移植できる。
ただ、FreeRTOSでは呼出しコンテキストの区別が必要(FromISRのない関数を呼ぶか、FromISRのある関数を呼ぶが)であり、このようなラッパーを用意する場合には実装が面倒になっている。

3-3. セマフォ

セマフォについてはそれぞれに対応したAPIが用意されているので、特に問題なく移植できる。
ただ、FreeRTOSでは「呼出しコンテキストの区別が必要」な点、「待ちの時間指定をティックで指定する」といった点がやはり面倒くさい。(このあたりはμT-Kernelに慣れているせいもあるのだろう。)

セマフォの生成、削除

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_init_semaphore()xSemaphoreCreateCounting()tk_cre_sem()
cy_rtos_deinit_semaphore()vSemaphoreDelete()tk_del_sem()

セマフォの生成、削除という意味では同じ機能である。
両OS共にバイナリセマフォとカウンティングセマフォを生成可能である。
ただし、μT-Kernel 3.0ではタスクの待ち解除方式としてFIFOだけでなくタスク優先度順も選択することができるので、セマフォの応用の幅が広がる。一方、FreeRTOSの待ち解除方式はFIFOのみである。

セマフォ資源の獲得、解放

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_get_semaphore()xSemaphoreTake(),
xSemaphoreTakeFromISR()
tk_wai_sem()
cy_rtos_set_semaphore()xSemaphoreGive(),
xSemaphoreGiveFromISR(),
portYIELD_FROM_ISR()
tk_sig_sem()

セマフォ資源の獲得/解放という機能としては同じである。
ただし、FreeRTOSではカウンティングセマフォであっても操作できる資源数が1に制限されている。
μT-Kernel 3.0では任意の資源数を指定可能であり、一度に複数個の資源を獲得/解放することも可能である。

また、FreeRTOSでは待ちが発生しない場合はタスク独立部からもセマフォ資源獲得を呼び出せる。
専用のAPI xSemaphoreGiveFromISR()が用意されていて、資源があれば獲得することもできる(資源がなければ即時エラー終了する)。
一方、μT-Kernel 3.0の実装ではタスク独立部からセマフォ資源獲得のAPI tk_wai_sem()は発行できない。タイムアウトに待ちを発生しないTMO_POLを指定したとしてもE_CTXのエラーになる。

μT-Kernel 3.0でもカウント数を確認することはできるのでセマフォ資源の有無を確認して挙動を変えることも可能ではある。ただし、その場合も資源の操作(デクリメント)はできないのでFreeRTOSと同じ動作にはならない。このため、タスク独立部から呼び出した場合は単純にエラーとした。

さて、実は上記の2点よりも、大きな違いがある。
FreeRTOSでは割込みハンドラでセマフォを制御した結果としてタスクの切り換えが必要になった場合、開発者が明示的にportYIELD_FROM_ISR()を呼び出さなければならない。
このAPIを呼び出さない場合、割込みハンドラからタスクコンテキストに戻ってもタスク切り替えが発生せず、次回明示的にタスクスイッチが呼ばれた時にタスクが切り替わることになる。これはFreeRTOSの仕様であり、他のCommunication Objectでも同様の処理が必要となる。  
μT-Kernel 3.0ではこのような処理は一切不要であり、割込みハンドラを終了する時点で自動的にディスパッチされる仕様になっている。これを「遅延ディスパッチ(delayed dispatching)の原則」という。
この仕様については「μT-Kernel 3.0仕様書」の「2.5.2 タスク独立部と準タスク部」に説明がある。
T-KernelやITRONを使い慣れた者としてこれが当たり前になっているので、その前提でFreeRTOSを使い始めると発見がかなり難しいバグを作り込んでしまう危険性が高い。(タイミングが異なるだけで動作はするわけだし…)  
ディスパッチが必要になったのはOSも把握しているのだから自動的に切り替えてくれればいいのではないかと思うのだが、FreeRTOSはそれをやらない。切り替えのタイミングを開発者がコントロールすることで無駄な処理が発生しないようにできるということかもしれない。
メリットがあることもわかるのだが、余計な不具合を作り込んでしまいそうで、個人的にはいまいち納得できない仕様である。

3-4. イベントフラグ、イベントグループ

イベントフラグの生成

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_init_event()xEventGroupCreate()tk_cre_flg()

FreeRTOSの「イベントグループ」がμT-Kernel 3.0の「イベントフラグ」に相当する。ここでは「イベントフラグ」で説明する。

イベントフラグ自体は類似の機能ではあるが、μT-Kernel 3.0とFreeRTOSには以下の違いがある。

イベントフラグの機能FreeRTOSμT-Kernel 3.0
フラグのビット数8ビット or 24ビットを選択処理系に依存(unsigned int)
待ちタスクのキューイングFIFOのみFIFO、または優先度順を選択可能
待ちタスク数複数タスクの待ちが可能1個、または複数個を選択可能

フラグのビット数はμT-Kernel 3.0ではunsigned int型になっているので、CPUのビット幅(処理系)によって自動的に決まる。これは効率的に処理できるようにするための配慮である。
一方のFreeRTOSではconfigUSE_16_BIT_TICKSの設定によって使用可能なビット数が8ビットか24ビットとなる。 configUSE_16_BIT_TICKSはTickType_t型を定義するためのマクロだが、なぜかこれに依存するらしい。
コードでは以下のように定義されており、

typedef TickType_t           EventBits_t;

TickType_tはconfigUSE_16_BIT_TICKSに従って16ビット or 32ビットと定義されている。

#if ( configUSE_16_BIT_TICKS == 1 )
        typedef uint16_t     TickType_t;
#else
        typedef uint32_t     TickType_t;
#endif

μT-Kernel 3.0がunsigned int型を使っているのと同じようなことかもしれないが、configUSE_16_BIT_TICKSの前にもうワンクッション入れるべきではないかと思う。

イベントフラグの削除

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_deinit_event()vEventGroupDelete()tk_del_flg()

両OS共にイベントフラグを削除する機能であり、特に違いはない。

イベントフラグのセット

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_setbits_event()xEventGroupSetBits(),
xEventGroupSetBitsFromISR()
tk_set_flg()

指定されたビットをセットする機能としては両OSとも同じである。
ただし、FreeRTOSではフラグのセットについても呼び出すコンテキストによってAPIが異なるので、アプリケーション側でコンテキストを区別しておかなければならない。μT-Kernel 3.0ではコンテキストによってフラグをセットするAPIを区別する必要はない。

なお、セマフォでも説明したとおり、FreeRTOSでは割込みハンドラでセマフォを制御した結果としてタスクの切り換えが必要になった場合、開発者が明示的に portYIELD_FROM_ISR() を呼び出さなければならない。この点も注意が必要である。

※元の(FreeRTOSの)cy_rtos_setbits_event()には、なぜかportYIELD_FROM_ISR()を呼び出す処理が含まれていなかったが…。

イベントフラグのクリア

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_clearbits_event()xEventGroupClearBits(),
xEventGroupClearBitsFromISR()
tk_clr_flg()

イベントのビットをクリアする機能としては両OSとも同じである。
ただし、FreeRTOSでは1のビットがクリアされ、μT-Kernel 3.0では0のビットがクリアされる。仕様としてどちらが良いとも悪いとも言えないが、移植だけでなく開発時にも注意が必要な点である。

イベントフラグの確認

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_getbits_event()xEventGroupGetBits()tk_ref_flg()

現在のイベントフラグのビットを取得する。機能としては同じである。

イベントフラグ待ち

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_waitbits_event()xEventGroupWaitBits()tk_wai_flg()

指定されたビットが1になるのを待つ。複数の待ちビットが指定されていた場合、全てのビットがセットされるのを待つか、いずれか1つのビットがセットされるのを待つのかを指定できる点も同じである。
条件成立時にビットをクリアするか否かを指定できる点も同じである。  
ただし、FreeRTOSでは、待ち条件として指定されたビットをクリアするか否かを指定できるだけであるが、μT-Kernel 3.0では全ビットをクリアすることも可能である。

ビットクリア方法FreeRTOSμT-Kernel 3.0
ビットをクリアしない
待ちビットをクリアする
全ビットをクリアする×

また、FreeRTOSでは戻値がビットパターンになっている点(以下)も注意が必要である。

The value of the event group at the time either the event bits being waited for became set, or the block time expired.

このため、タイムアウトが発生したかどうかは戻値をビットパターンと比較して検証しなければならない。
これに対してμT-Kernel 3.0では戻値でエラーか否かを判定できる仕様になっているので、APIからリターンした時の処理が簡単になる。ただし、エラー発生時はフラグのビットが読み出されない(不定値となる)ので、ビットパターンを利用したい場合は注意が必要である。

3-5. タイマ

どちらもアラームハンドラと周期ハンドラの機能があるので、両OS方とも機能としては○となる。
ただし、アラームハンドラではμT-Kernel 3.0とFreeRTOSで時間のパラメータを指定するタイミングが異なる。
コールバック関数(タイマハンドラ)を呼び出す時間を指定するタイミングは以下のとおりである。

機能FreeRTOSμT-Kernel 3.0
アラームハンドラ生成時開始時
周期ハンドラ生成時生成時

新規に開発するのであれば特に問題はないが、相互に移植するとなると結構面倒なことになるかもしれない。

また、タイマハンドラが実行されるコンテキストも異なる。
μT-Kernel 3.0ではタスク独立部(割込みハンドラ)として実行される。このため、基本的にタスクより優先して実行されることが保証されている。  

一方、FreeRTOSではコールバック関数がタイマ専用のタスクから呼び出されてタスクコンテキストで実行される。  

Timer callback functions execute in the context of the timer service task.

タイマサービスタスクは他のタスクと同様に扱われるので、優先度によっては(時間が来ても)必ずしもタイマが動作するとは限らないので注意が必要である。
なお、タイマサービスタスクのタスク優先度はconfigTIMER_TASK_PRIORITYで指定可能となっていて、多くのシステムは以下のように定義されている。
(https://www.freertos.org/ からFreeRTOSのソースコードをダウンロードして確認)

#define configTIMER_TASK_PRIORITY                  ( configMAX_PRIORITIES - 1 )

configMAX_PRIORITIESはタスク優先度の数であり、FreeRTOSのタスク優先度は 0:最低 ~ (configMAX_PRIORITIES-1):最高 である。つまり、多くのシステムではタイマサービスタスクの優先度は最高に設定されている。ただし、同じ優先度のタスクを作ることも可能なので、必ずしも全てのタスクに優先されることにはならない。
一方で、configTIMER_TASK_PRIORITYが即値で指定されている構成も多数あった。その場合、注意しないとタイマサービスタスクのブロックが多発するのではないだろうか? (何か理由があるのかもしれないが、そこまでは確認していない。)

更に、タイマの起動はこのタイマサービスタスクのキューに対してコマンドを送ることで実現している。このため、キューが既にいっぱいであれば、タイマの起動をかけたタスクが待たされることもあるし、「タイマの起動で」タイムアウトエラーが発生することもある。
μT-Kernel 3.0では、操作リクエストが溜まってエラーになるようなことはない。

タイマの生成

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_init_timer()xTimerCreate()tk_cre_alm(), tk_cre_cyc()

タイマを生成する。
μT-Kernel 3.0にはアラームハンドラと周期ハンドラの2つの機能がある。FreeRTOSではソフトウェアタイマとして1つの機能としてまとめられているが、オプションの指定でワンショットか繰り返して動作するかを指定できる。ワンショットのタイマがμT-Kernel 3.0のアラームハンドラ、繰り返しタイマが周期ハンドラに相当する。
機能としてはほぼ同じだが、以下のような違いがあるので実装(移植)の際には注意が必要である。

  • μT-Kernel 3.0では生成直後の起動、周期ハンドラ起動タイミングの指定が可能
    μT-Kernel 3.0の周期ハンドラでは生成と同時にタイマを起動することもできる。また、周期ハンドラを最初に起動する時間を指定することも可能である。FreeRTOSには、このような細かい設定をする機能はない。
  • FreeRTOSでは起動時刻の変更が可能
    FreeRTOSには、タイマの時間を変更するための xTimerChangePeriod() というAPIが用意されている。μT-Kernel 3.0の周期ハンドラでは生成時に起動時刻を指定する必要があり、後からは変更はできない。
    なお、μT-Kernel 3.0のアラームハンドラは開始時に時間を指定するので、このAPIは必要ない。
  • コールバック関数(タイマハンドラ)の引数が異なる。
    μT-Kernel 3.0では生成時に指定した拡張情報(void *exinf)がコールバック関数(タイマハンドラ)の引数として渡されるが、FreeRTOSではタイマ生成時に決定されるタイマハンドル(TimerHandle_t型の変数)が渡される。

タイマの開始

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_start_timer()xTimerChangePeriod(), xTimerStart()tk_sta_alm(), tk_sta_cyc()

タイマを開始するという機能としては同じである。
ただし、「タイマの生成」で説明したとおり、FreeRTOSではタイマの開始時に時間を設定する。
μT-Kernel 3.0のアラームハンドラは起動時間をAPI指定することができるので機能としては同じになる。一方、μT-Kernel 3.0の周期ハンドラでは起動時に時間を変更することはできない(生成時に指定する)。

また、先に説明したとおりFreeRTOSのタイマ機能は、タイマサービスタスクによって提供されており、タイマを制御するAPIは、このタスクにコマンドキューを介して操作リクエストを送信している。このため、キューがいっぱいになるとエラーが発生することもあるので注意が必要である。  

タイマの停止

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_stop_timer()xTimerStop()tk_stp_alm(), tk_stp_cyc()

タイマを停止する。機能としては同じである。

タイマの起動状態の確認

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_is_running_timer()xTimerIsTimerActive()tk_ref_alm(), tk_ref_cyc()

タイマの起動状態を取得する。
FreeRTOSにはこの機能専用のAPIがあるが、μT-Kernel 3.0ではtk_ref_alm()、tk_ref_cyc()によって起動状態を含めたタイマの各種情報を一括して取得する。

タイマの削除

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_deinit_timer()xTimerDelete()tk_del_alm(), tk_del_cyc()

タイマを削除する。機能としては同じである。

FreeRTOSには上記の他に、タイマのリセット、タイマ種類の変更などのいろいろなAPIが用意されている。
並べてみるとこんな感じである(~FromISR() は省略)。

FreeRTOSのタイマ関連のAPI
xTimerCreate()
xTimerCreateStatic()
xTimerIsTimerActive()
xTimerStart()
xTimerStop()
xTimerChangePeriod()
xTimerDelete()
xTimerReset()
pvTimerGetTimerID()
vTimerSetReloadMode()
vTimerSetTimerID()
xTimerGetTimerDaemonTaskHandle()
xTimerPendFunctionCall()
pcTimerGetName()
xTimerGetPeriod()
xTimerGetExpiryTime()
xTimerGetReloadMode()

一方のμT-Kernel 3.0では以下のとおりであり、μT-Kernel 3.0のAPIがいかにシンプルかがよくわかる(~_u()は省略)。

周期ハンドラアラームハンドラ機能
tk_cre_cyctk_cre_alm生成
tk_del_cyctk_del_alm削除
tk_sta_cyctk_sta_alm開始
tk_stp_cyctk_stp_alm停止
tk_ref_cyctk_ref_alm参照

FreeRTOSのように機能が多いのは別に悪いこととは思わない(細かく制御できるのは個人的には好きだ)が、これだけ数が多いと(関数名が長いこともあって)ちょっとわかりにくいと思う。

システム稼働時間の取得

ラッパー関数FreeRTOSのAPIμT-Kernel 3.0のAPI
cy_rtos_get_time()xTaskGetTickCount()tk_get_otm()

FreeRTOSのAPIでは時間はティック数で処理されているので、取得される経過時間もティック数となる。このため、アプリケーションでは必要に応じてミリ秒に変換する必要がある。
μT-Kernel 3.0ではAPIで取得する時間はミリ秒と規定されているのでこの処理は必要ない。

4. その他の違い

コンポーネントの差し替え作業時に確認したμT-Kernel 3.0とFreeRTOSの差異は上記で説明したとおりである。
その他にもいろいろと違いがあったので、以下にその点についてまとめておく。

4-1. FreeRTOSにはラウンドロビンの処理が含まれている!?

FeeRTOSでは実行可能状態のタスクが複数あり、それらが同一優先度の場合はラウンドロビンで動作するらしい。  

FreeRTSOのユーザーガイドに以下の説明がある。

これは、同時に実行する準備ができている場合に、等しい優先順位のタスク間で処理時間を共有することを意味します。

最初に読んだ時に意味がわからず、思わず英文を確認した。

This requires sharing processing time between tasks of equal priority if they are ready to run simultaneously. 

そのまま。特に変な訳にはなっていない。
だが、「同時に実行」とは何だ?「等しい優先順位のタスク間で処理時間を共有」とは何だ?
参照したのはAWSのドキュメントサイトなのだが、いまいち納得がいかないので、FreeRTOSについて解説してくれている他のサイトを確認してみると「ラウンドロビン」「タイムスライシング」と説明している人もいる。
やはりそういう意味で間違いないらしい。
…RTOSとしてこの仕様はどうなのだろう?
ITRONから始まって、TRON系のRTOSを使い続けている人間としては理解できないのだが、「ラウンドロビン方式をRTOSの基本設計に取り入れる」という考え方もあるのだろう(…か?)
ITRONやT-Kernelを説明する時に、「ラウンドロビン方式のOSではリアルタイムな処理にならない」といった説明をしてきたので、これは衝撃だった。
FreeRTSOもコンフィギュレーションの設定でラウンドロビンを無効にすることも可能ではあるが、デフォルトの設定ではラウンドロビンが有効なので、やはり前提となる設計思想がμT-Kernel 3.0とは異なるのだろう。

ちなみに、μT-Kernelでもラウンドロビン的な機能を実装することは可能である。
周期ハンドラを使ってtk_rot_rdq()を発行すれば同じような動作になる。もしくは、タスクから明示的にtk_rot_rdq()を発行するのでもよい。tk_rot_rdq()を利用する方式だとある程度処理が完了してからタスクをスイッチすることができるので、実行中のタスクへの影響は限定的にできる。

4-2. 資料は原則英語

μT-Kernel 3.0は原本が日本語である。 
一方のFreeRTOSは原則英語
AWSが公開している日本語のページにも、先頭に以下のように書かれている。

翻訳は機械翻訳により提供されています。
提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

以前の機械翻訳と比べれば格段に読みやすくなっている。だが、込み入った説明になってくると日本語の意味がわからないこともままある。こんな時はやはり直接英文を確認しなければならない。

4-3. 固定長メモリプールがない

FreeRTOSには固定長メモリプールがない。
組込機器ではメモリプールは以下の理由で、固定長を使用するべきだろう。

  • 単純だから取得も返却も処理が軽い。
  • フラグメンテーションが発生する心配がない。

RAMが乏しいシステムでは少しでもメモリ使用量を削りたいという気持ちはよくわかるが、管理領域を含めて考えると可変長メモリプールは結構無駄が多い。トレードオフになるとは思うが、最近のシステムはRAMも大きいので、バッファとして確保する場合は多少大きくてもいいのではないだろうか?  

4-4. OSの起動方式

FreeRTOSは、main()関数からユーザーアプリケーションが始まっていて、その中でFreeRTOSのスケジューラをキックしてRTOSを起動している。
一方、μT-Kernel 3.0ではアプリケーションは初期タスクから始まる。main()関数も含まれてはいるが、それはアプリ開発者には見せていない。
これは好みだろう。
RTOS上のアプリケーションの開発者に対してあまり下回りを見せるのは感心しない気もするが、最初に「C言語のプログラムはmain()関数から始まる」と教わった人にはmain()関数から始まってくれるのはわかりやすいかもしれない。特に、Non-OSからOSを導入する段階では理解しやすいだろう。ただ、RTOSに慣れてくるとこの部分は冗長に感じられてくる。

4-5. タスク用とISR用でAPIが異なる

既に説明したが、FreeRTOSではAPIがタスク用とISR(Interrupt Service Routine)用とに分けられている。慣れかもしれないが、これはすごく使いにくい。FreeRTOSはこういった細かい対応を開発者に任せている気がする。
自由度が高いともいえるが、注意事項が多く、その分開発者に負担になるのではないだろうか。

4-6. セマフォの初期値

μT-Kernel 3.0ではセマフォの生成時に初期値を指定できる。
一方の、FreeRTOSでは初期値は0に固定。生成後に資源を返却することで設定する。
FreeRTOSでは細かいAPIが別々に用意されていて、μT-Kernel 3.0ではある程度機能がまとめて整理されている。
APIのわかりやすさとしてはμT-Kernel 3.0の方が優れていると言えると思う。

4-7. データキュー

μT-Kernel 3.0にはデータキューはない。
μT-Kernel 3.0のメッセージバッファやメールボックスで代替可能だが、先頭に挿入するという機能はない。ただし、メールボックスでは属性の指定でメッセージの並び順をメッセージの優先度順にすることが可能であり、全く同じではないにしても近い動作を実現することはできるだろう。

4-8. タスク優先度

FreeRTOSのタスク優先度は0が最低で、(configMAX_PRIORITIES-1)が最高となっている。μT-Kernel 3.0は1が最高で、CNF_MAX_TSKPRI が最低である。
これは慣れの問題だとも思うが、「最も高いのが1」と即値で決められている方が理解しやすいのではないだろうか?

4-9. 戻値

FreeRTOSでは生成したオブジェクトの識別子(戻値)が構造体だったり(Queue, Semaphore, Mutex, …)、void* (Message Buffer) だったり、整数値(Task)だったりして統一されていない。
例えば、以下になっている。

  • xTaskCreate() の戻値は、pdPASS または errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY である。
  • xTaskCreateStatic() の戻値は、失敗ならNULLで、成功ならそれ以外となる。

FreeRTOSではAPIの先頭に戻値の型を示す文字が追加されてはいる(下記)が、これはコードを読む側にとってはわかりやすいかもしれないが、実装する側にとっては使いにくい。

先頭の文字
pcchar*
pvvoid*
vvoid
uxUBaseType_t
xBaseType_t, TaskHandle_t, …

一方、μT-Kernel 3.0を含めてTRON系のOSでは以下が原則である。

  • 戻値 < 0 … エラー
  • 戻値 ≧ 0 … 正常

教育を重視して設計されていることもあり、このあたりはμT-Kernel 3.0の方が開発しやすい。

4-10. アイドルタスク

FreeRTOSには自動的にアイドルタスクが用意される。
省電力機能はアイドルタスクに実装するらしいので、開発者にとってはとっつきやすいかもしれない。
一方、μT-Kernel 3.0の省電力機能はlow_pow()に実装することになっている。この関数は実行すべきタスクがなくなった場合にディスパッチャが直接呼び出しているので、省電力を実装する目的でアイドルタスクを生成する必要はない。

4-11. メッセージバッファ

FreeRTOSにもμT-Kernel 3.0にもメッセージバッファがある。ただ、FreeRTOSのメッセージバッファはストリームバッファを利用して実装されているため以下のような制限事項がある。

IMPORTANT NOTE: Uniquely among FreeRTOS objects, the stream buffer implementation (so also the message buffer implementation, as message buffers are built on top of stream buffers) assumes there is only one task or interrupt that will write to the buffer (the writer), and only one task or interrupt that will read from the buffer (the reader). It is safe for the writer and reader to be different tasks or interrupts, but, unlike other FreeRTOS objects, it is not safe to have multiple different writers or multiple different readers. If there are to be multiple different writers then the application writer must place each call to a writing API function (such as xStreamBufferSend()) inside a critical section and use a send block time of 0. Likewise, if there are to be multiple different readers then the application writer must place each call to a reading API function (such as xStreamBufferReceive()) inside a critical section and use a receive block time of 0.

だいたい以下のような意味である。

  • メッセージバッファはストリームバッファを利用して実装されているので、使い方にはストリームバッファと同じ制限がある。
  • ストリームバッファは以下の前提で実装してある。
    • Writer(バッファに書き込むタスクまたは割り込み)は1つだけである。
    • Reader(バッファから読み出すタスクまたは割り込み)は1つだけである。
  • WriterとReaderが別に存在するのは問題ない。
  • 複数のWriterや複数のReaderがあるのは安全ではない(not safe)。
  • 複数のWriterが必要な場合は、書き込み用のAPIをクリティカルセクション内に配置して、送信ブロック時間を0にして実装する必要がある。
  • 複数のReaderが必要な場合は、読み出し用のAPIをクリティカルセクション内に配置して、受信ブロック時間を0にして実装する必要がある。

メッセージバッファとしてAPIを用意しているのだから最後の2項目はメッセージバッファのAPIに組み込んでくれていてもよさそうな気もするがそうはなっていない。(WriterとReaderが1つずつしかない場合に無駄になるからだろうか?)  

このあたりは基本的な設計思想によるものかもしれないが、FreeRTOSはアプリケーション開発者に対する注文が多い気がする。
なお、μT-Kernel 3.0には当然ながらこのような制限事項はないので、気にせずに実装することができる。


以上、FreeRTOSで実装されていた機能を実際にμT-Kernel 3.0に置き換えた際に確認したμT-Kernel 3.0とFreeRTOSの差異について説明した。
この他にもいろいろな違いがあると思うが、今回確認した内容はここまでである。
RTOS選びや移植の際の参考になれば幸いである。

上記はUCTで移植して、UCTのGitから公開しているインフィニオンのマイコン用のμT-Kernel 3.0を移植した時の経験をもとにしたチュートリアルです。移植の御相談はUCTの問い合わせ窓口にお寄せください。