2026年2月5日木曜日

μiTRONプログラマーがZephyrに挑戦! その7

前回からの続きです。

このテーマを最初からご覧になる場合はこちらからどうぞ。


μITRONとZephyrの比較

TOPPERS/ASPのサンプルプロジェクトをZephyrに移植し、動かして見てみることで両者を比較し、Zephyrを学ぼう!という今回の趣旨、ここまでお付き合いくださいまして誠にありがとうございました。

また、この数ヶ月、本業で初めてZephyrを使用した製品の開発を完了させることができました。

今回は、その過程において私のようなμITRONを使用しているプログラマーの視点から、Zephyrを使う上で感じた個人的な違和感をメモに残しておきたいと思います。

例えば、μITRONのプログラムをZephyrに移植する際に障害となった問題(ていうかハマった部分)の回避策も一部紹介させていただきます。

理由は後述しますが、今後自分が関わるプロジェクトにおいて、もう二度とZephyrを使うことはないでしょう。

しかし、万が一、μiTRONからZephyrに戻る様な状況(考えたくはないけど、不具合修正とか?)になった時のための備忘録です。

Zephyr vs μITRON


スリープのシステムコールの違い

TOPPERS/ASPのサンプルプロジェクトを移植する中で、一番最初に違和感を感じたのは、スリープのシステムコールです。

要するに、一定期間タスク/スレッドを待ち状態(スリープ)にし、自分よりも優先度の低いものも含め、他のタスク/スレッドへ処理を一時的に明け渡すという最もシンプルな仕組みです。

μITRONでは「tslp_tsk(<待ち時間>)」、一方、Zephyrで同じような働きをするシステムコールは「k_sleep(<待ち時間>)」です。

Zephyrの「k_sleep()」には、μITRONの「tslp_tsk()」では備わっていた以下3つの重要な機能がありません。


1.Zephyrの「k_sleep()」は、その起床原因がタイムアウトなのか、ウェイクアップ要求によるものかの区別がない

μITRONの「tslp_tsk()」は、以下の3つの起床原因を返します。


◯他のタスクや割り込みハンドラからの起床命令による起床(「E_OK」)

◯タイムアウトによる起床(「E_TMOUT」)

◯他のタスクや割り込みハンドラからの待ち状態(スリープ)強制解除による起床(「E_RLWAI」)


一度スリープに入ったタスクは、指定した時間が経過(タイムアウト)するか、または他のタスクや割り込みハンドラから叩き起こされる(ウェイクアップ)かによって起床します。

上記の通り、前者の場合は「E_TMOUT」、後者の場合は「E_OK」が関数の戻り値として返るため、起床原因が判別できます。

E_RLWAI」については、後述します。

一方、Zephyrの「k_wakeup()」の場合は、タイムアウトだろうが、他者からのウェイクアップだろうが、戻り値としては「0」しか返らず、起床原因が判りません。

これが困るのは、以下のような処理のケースです。

デバイスからの割り込みをタスクで処理するためのμITRONのデバイスドライバか何かの一部だと思ってください。

  1. /*
  2.  * メインタスク(MAIN_TASK)
  3.  */
  4. void main_task(intptr_t exinf)
  5. {
  6.     ER ercd;
  7.     
  8.     while (1) {
  9.         /*
  10.          * 5秒間割り込み待ち
  11.          */
  12.         ercd = tslp_tsk(5000);
  13.         if (ercd == E_TMOUT) {
  14.             /*
  15.              * 以降タイムアウトの場合のエラー処理
  16.              */
  17.             ...
  18.             continue;
  19.         }
  20.         /*
  21.          * 時間以内に割り込みが発生!
  22.          * 以降割り込みに応じた処理
  23.          */
  24.         ...
  25.     }
  26. }
  27. /*
  28.  * 割り込みハンドラ
  29.  */
  30. void int_handler(void)
  31. {
  32.     /*
  33.      * 割り込み発生!
  34.      * 割り込みハンドラからタスクへ処理を渡す
  35.      */
  36.     iwup_tsk(MAIN_TASK);
  37. }


タスクは開始されて間もなく「tslp_tsk()」で5秒間割り込みハンドラからの起床命令を待っています。

一方、割り込みハンドラには、タスクをウェイクアップさせるための「iwup_tsk(<タスクID>)」が配置されています。

割り込みハンドラに長々と処理を記述するのは、リアルタイム性を損なうのでご法度です。

ですので、これは何らか割り込みが発生した場合、割り込みハンドラからタスクへ処理が渡されるという非常にありがちな処理です。

前述の通り、Zephyrの場合は、ウェイクアップの原因が正常な割り込み処理の結果なのか?それとも、タイムアウト…すなわち、異常なのか?の区別が付かないことが問題です。

この問題の回避策は、後ほど提示します。


2.Zephyrの「k_sleep()」は、待ち状態期間中の「k_wakeup()」のキューイングを行わない

あるタスク(スレッド)に対し、それがスリープ状態ではない時に起床命令を実行した場合の違いです。

たとえは、以下は普通のμITRONの場合です。

STM32 Nucleo-144( NUCLEO-F207ZG)という評価ボードで動いています。

こもターゲットをパソコンに接続しTeraTermで見ている状態です。

TeraTerm - 1


ここで、以下の操作を行います。

すでに今起床している(スリープ状態ではない)「task1」に対して起床命令を発行します。

これには、ターミナルに対し「1」、「w」と順に入力します。

TeraTerm - 2


この後、スリープ命令を「s」で発行すると、以下のように「task1」はスリープになりません。

もし「task1」がスリープに入ったのなら「task2」に処理が切り替わるはずです。

でも、引き続き「task1」が続行していることが判ります。

TeraTerm - 3


もう一度、スリープ命令を「s」で1回発行すると、ようやく「task1」はスリープし、代わりに「task2」に処理が移ります。

TeraTerm - 4


このように、μITRONにおいては、そのタスクがスリープ状態ではない時でも自分に対して発行された起床命令を覚えていて(キューイング)、その後のスリープ命令を無視します。

これを「タスク起動要求キューイング」と呼んだりします。

キューイングの数はμITRONの実装によって異なりますが、TOPPERS/ASPの場合は通常1回です。


一方のZephyrのスリープ/起床命令には、この仕組みがありません。

以下は今回の「zephyr-sample」の場合です。

ターゲットをパソコンに接続しTeraTermで見てみましょう。

TeraTerm - 5


今起床している(スリープ状態ではない)「thread1」に対して起床命令を発行します。

これには、ターミナルに対し「1」、「w」と入力します。

TeraTerm - 6


この後、スリープ命令を「s」で1回発行すると…

TeraTerm - 7


…このように、即座に「thread2」に処理が移ります。

つまり、スリープ状態でなかった時に発行された起床命令はガン無視です。

このことは、μITRONからZephyrへソフトウェアを移植する際に非常に発見しづらいバグを生む可能性があります。

タスク起動要求キューイングの目的は、起床命令を取りこぼさないことです。

前述したようなデバイスドライバの処理などにおいて、割り込み割り込みハンドラからの起床命令を取りこぼすことは致命的です。

割り込みハンドラからの起床命令が発行された時点で、それを待つスレッドが必ずしもスリープ状態であるとは限りません。

割り込み頻度が高いデバイスであれば尚更です。

特に、そのデバイスドライバのソースコードが十分に検証されたμITRON用のものであり、それをZephyrに移植しようとした場合、これを知らないと「来るはずの割り込みが来ない!!」と言って、大いに焦ることになります。

…はい、私はその罠に見事にかかりました!

かなり長時間にわたり悩み続けましたよ…。

この問題も、どうやって回避したのかを後ほど提示します。


3.Zephyrの「k_sleep()」は、起床原因として「待ち状態の強制解除」がない

これは、前の2つに比べて重要性はあまり高くないものの、μITRONでは使っている人が多い機能だと思います。

おさらいですが、μITRONの「tslp_tsk()」は、以下の3つの起床原因を返します。


◯他のタスクや割り込みハンドラからの起床命令による起床(「E_OK」)

◯タイムアウトによる起床(「E_TMOUT」)

◯他のタスクや割り込みハンドラからの待ち状態(スリープ)強制解除による起床(「E_RLWAI」)


3つ目の「待ち状態(スリープ)強制解除」の説明です。

待ち状態(スリープ)強制解除は、μITRONでは「(i)wup_tsk(<タスクID>)」の代わりに「(i)rel_wai(<タスクID>)」をコールすることにより発行できます。

共にスリープしているタスクを起床させる機能を持つシステムコールですが、「(i)wup_tsk()」と「(i)rel_wai()」では何が違うのでしょう?

トロンフォーラムの配布する「µITRON4.0仕様」には、これについては以下のような違いが説明されています。

  • rel_waiとwup_tskには次のような違いがある。

  • 1. wup_tskは起床待ち状態からのみ待ち解除するが、rel_waiは任意要因による待ち状態から待ち解除する。

  • 2. 起床待ち状態になっていたタスクから見ると、wup_tskによる待ち解除は正常終了(E_OK)であるのに対して、rel_waiによる強制的な待ち解除はエラー(E_RLWAI)である。

  • 3. wup_tskの場合は、対象タスクが起床待ち状態でない場合には、要求がキューイングされる。それに対してrel_waiの場合は、対象タスクが待ち状態でない場合にはE_OBJエラーとなる。

μITRON4.0仕様 - WG024-S001-04.03.03 - P.124「rel_wai/irel_wai 待ち状態の強制解除」より抜粋


1番目の「任意要因による待ち状態」とは、スリープ=起床待ち以外の待ち状態のことです。

起床待ちは「slp_tsk()」や「tslp_tsk()」でタスクが「待ち」に入った状態です。

それ以外の「待ち」とは、セマフォの資源待ちシステムコールである「wai_sem(<セマフォID>)」やイベント待ちシステムコール「wai_flg(フラグID)」などにより、それぞれの=任意の要因でタスクが「待ち」に入った状態です。

つまり「wup_tsk()」は起床待ち=スリープだけを解除するけれども、一方「rel_wai()」は、それ以外の全ての待ち状態を強制的に解除します…と言っています。

2番目は「slp_tsk()」や「tslp_tsk()」で入った起床待ち=スリープ状態が待ち状態解除された後、これらのシステムコールが返り値を返しますが、その要因が「wup_tsk()」と「rel_wai()」の場合で異なりますよ…と言っています。

「wup_tsk()」の場合は「E_OK」で正常終了、一方「rel_wai()」の場合は「E_RLWAI」がエラーとして返ることを説明しています。

3番目のキューイングに関しては、既に前述の通りです。

「rel_wai()」は、タスク起動要求キューイングをされません。

つまり、該当のタスクが「待ち」状態でない時点でコールしてもキューイングはされず、「E_OBJ」エラーが返ることを説明しています。


一方のZephyrでは、この「待ち状態(スリープ)強制解除」に類するシステムコールが見当たりません。

μITRONの待ち状態(スリープ)強制解除は、意図しない待ち解除が発生するため、強力ですが危険です。

上手く使えばデッドロック防止やエラー処理などにおいて便利な場合がありますが、そもそも、そのような設計をするべきではない…というZephyrの設計思想なのでしょう。

確かにその通りですが、キューイングの有り無しは別として、起床原因の切り分けをするために便利なので(「E_OK」なのか?「E_RLWAI」なのか?)、よく使っているμITRONプログラマーもいるかもしれません。

私は、結構気軽に使っちゃってます。

μITRONのプログラムをZephyrに移植する際に、もし元ソースで「rel_wai()」が使われていたり、スリープが「E_RLWAI」で待っているような処理を見かけた場合、残念ながらこの問題に対するスマートな解決策はありません。

今回の「zephyr-sample」では、起床命令発行時にグローバル変数を使用して起床原因を設定し、起床した直後のスレッドでそれを判断するという、スマートじゃない方法で回避してます…。

すんません。


さて、μITRONの「tslp_tsk()」が持っているのに、Zephyrの「k_sleep()」にはない3つの機能を紹介させていただきました。

3つ目の待ち状態(スリープ)強制解除に関しては、今のところ良い回避策は思い付きませんが、1つ目の起床原因の区別と、2つ目のキューイングに関しては回避策があります。

どうすれば良いか?ですが…。

結論を言ってしまえば、その回避策とはスリープをセマフォに置き換えることです。

今回の「zephyr-sample」では「k_sleep()」で起床待ち状態に入ったタスクを「k_wakeup()」で起床させています。

それを「k_sem_take()」でセマフォ資源待ち状態に入ったタスクを「k_sem_give()」で起床(正しくはセマフォ資源を返す)させるようにします。

具体的に「zephyr-sample」を改造してみましょう。


…というところで一旦切りましょう。

このスリープの問題、開発していた時に最後までハマった部分なので、詳細に書いておきたいと思いました。

よくよく調べれば簡単な話だったんですけどね。


<続く>

μiTRONプログラマーがZephyrに挑戦! その7

前回からの続き です。 このテーマを最初からご覧になる場合は こちら からどうぞ。 μITRONとZephyrの比較 TOPPERS/ASPのサンプルプロジェクトをZephyrに移植し、動かして見てみることで両者を比較し、Zephyrを学ぼう!という今回の趣旨、ここまでお付き合い...