シリアル-LANアダプタ(その2)
仕切り直しはOSも視野に入れて
シリアル-LANアダプタを「とにかく作る」(まぁこの考え方が悪いんだろうけど)ことでがんばっていましたが、いろいろあって凍結してしまいました。その経緯は該当ページを読んでもらうとして、とりあえずのところまでは動いているのは心強いです。IPの送受信に関してはあまり心配せず、問題になってるスケジューリングだけを考えればいいんですからね。
さて、そのスケジューリングはいわゆるマルチタスクOSを採用することで解決することにします。私がマルチタスクOSと言えば、μITRONしかありません。4.0の仕様も発表されましたが、3.0だろうが2.0だろうが、まずはタスクが切り替わって、イベントがあったらタスクが起動されて、というものがあれば一応の目的が達成されそうな気がします。
OSを試作する
ずーっと前からITRONに関する文献を読んだりしていたのですが、ここへきてようやく具体的にどうすべきなのか理解できてきました。いろいろな部分で既成観念やら取り違いやらでなかなか理解できなかったのです。
実装に絡むμITRONの仕様・特徴は次のとおりとなります(用語を合わせるため、APIという単語を「サービスコール」に改めました)。
走行可能な(待ち条件が解消されいつでも走れる)タスクは、優先順位毎にレディキューにつながれます。今走っていたタスクが何らかの原因で止められるとディスパッチが起こるわけですが、その時には最も優先順位が高いレディキューの先頭につながれているタスクが選ばれて、切り替わります。先頭のタスクが選ばれるのも仕様で、これをFirst Come First Servedといいます。
割り込み処理を行う際にタスク切り替えを伴うサービスコールを呼び出すことはままあるわけですが、それをそのまま実行するとサービスコールから戻る直前にタスクが切り替わってしまうことが考えられます。しかし割り込み処理部はユーザープログラムのうちでもシステムに近い部分で、まだ処理が残っていることもあり得ます。そこで、割り込み処理を終わるときまでディスパッチをしてはいけないということになっています。多重に割り込みがかかったときも、割り込みのネストを数えておいて、一番外側の割り込み処理を抜けるときにディスパッチをするようにします。
タスクのスケジューリングはタスクがどこのキューのどの辺りにあるかで決まってきます。待ち条件が発生したらそのキューにタスクをつなぎ換えますし、解消すればレディキューにつなぎ換えます。ほとんどが、どの条件でどのキューにタスクをつなぎかえるのかを記述してあるのがサービスコールとも言えるわけです。
基本としてこれらを念頭に置いた上で、OSというよりは「タスク切り替えテストプログラム」を作ってみることにします。仕様はこんな感じです。
プログラムは基本的にCで組みます。ディスパッチだけはアセンブラにしないといけませんが、インラインアセンブラで対処しましょう。ディスパッチのときに保存するコンテキスト(レジスタ群ですな)はCの割り込みルーチンで自動的に保存することになっているものを対象にすれば大丈夫なはずです。実際のところ、これだけのものが動いてしまえばほとんど目的は達成されたも同然です。それだけに試作としては欲張ってるかなと思うんですが、これくらいないと効果的にタスクが切り替わっていることを確認できそうにないんですよね…。
どこが試作なんだか
まずはディスパッチ部分を作ります。ものの本によれば、おおよそ次のように実装すれば大丈夫のようです。
コンテキストの保存は、保存すべきレジスタをスタックにpushするだけというのが主流のようです。タスク毎に保存するのはスタックポインタだけで良くなるからです。この、「タスク毎に持っている保存領域」と「次に実行すべきタスクを探すための手がかり」をまとめて「タスクコントロールブロック(TCB)」といい、それぞれをひとまとめで扱うと都合がいいので構造体として定義するのが普通になっているようです。ですので、ディスパッチ部分でこれをどのように取り扱うかでTCBの構造が決まってきます。
「次に実行すべきタスクを探すための手がかり」というのは、リンクポインタという手法で実現するのが一般的のようです。例えばレディキューに実行可能なタスクが列をなしてぶら下がっているとき、リンクポインタにはその行列で次につながっているリンクポインタを含む構造体のアドレスが格納されます。リンクポインタを順にたどっていけば、システムで定義された順に構造体をサーチしていくことができます。行列の最後のリンクポインタは、先頭を指すようにしておけば途中から探し始めても必ず全部の構造体をサーチできます。なお、実際にはもう一組、行列の最後を操作するために「前を指すリンクポインタ」も用意します。これがないと、最後を探すために先頭からサーチしていかないといけなくなり、効率が悪いです。
ものの本によればリンクの作り方には二つあるらしく、ひとつはリンクの根元の情報が入るコントロールブロックにはリンクされるオブジェクトの先頭のIDが入るもの、もうひとつはコントロールブロックにリンクポインタが含まれているもののようです。リンクに何もつながっていないことを識別するには、前者ではIDに特定のコードを定義しておく、後者ではリンクポインタが自分を指すようにしておくようにします。両者であまりメリットの差を感じなかったのと、最初に参照した文献が後者を採用していたので、私も後者を採用しました。
そのリンクの根元はレディキューと呼ばれるもので、リンクポインタそのものなんですが、これが優先度の数だけ存在します。優先度はTCBに保存してあります。高い優先度のレディキューからサーチして最初に見つかったTCBを持つタスクに実行権が渡るわけです。
もしサーチした結果実行すべきタスクがないときはどうすればいいのでしょう?最初は内部にアイドルルーチンを持っているような構造にしようかと思ったのですが、最も優先度の低い「隠れタスク」のようなものを用意して(ユーザが指定する優先度よりさらに低い優先度を勝手に定義しておく)、待ち行列に何もないときはそれが走るようにしておくことで、ディスパッチ部に特別な細工をしないようにしました。
ディスパッチとTCB(の原形)ができたので、次に最初のサービスコールとしてslp_tskとwup_tskを実装します。
slp_tskは自タスクを起床待ちにするサービスコールです。今走行しているタスクの状態をRUNからWAITに変更して、レディキューから外し、ディスパッチ部に飛びます。ここで、TCBにタスクの状態を保存する場所が必要になることがわかります。ディスパッチ部ではこれをRUN状態に変更するので、ディスパッチ後RUN状態を持つタスクが複数あるということがないようにサービスコールの行列を操作する部分ではここをWAITに必ず変更するようにします。
wup_tskは指定のIDのタスクを起床するサービスコールです。指定のタスクがWAIT状態ならそれをレディキューにつなぎ、タスク状態をREADYに変更します。WAIT状態といっても他の要因で待っているものも皆WAITなので、slp_tskで待っているのかどうかという「待ち要因」もTCBに必要です。また、slp_tskによるWAITでない場合はwup_tskで起床されようとした回数を記録する(キューイング)のも必要です。これは、あるタスクが一連の動作を終えた後slp_tskで起床待ちに入るようになっていたとき(今回の試作用タスクもそうなんですが)、2ヶ所から起床要求が立て続けに出ても、キューイングされなければまとめて1回ということになり、どちらかの要求が捨てられることになるからです。
TCBが細かく決まってきたので、今度はタスクの起動を実装します。ものの本ではこれはsta_tskというサービスコールで実装していたのですが、パラメータにタスクに渡す「起動コード」というものがあって、具体的にどうしようか迷ったのです。しかしμITRON4.0の仕様書を見てみると、sta_tskの他にact_tskというものも定義されていて、これは起動コードがパラメータにないので実装が楽そうです。
act_tskはDORMANT状態のタスクをREADYにしたあと、指定優先度のレディキューにつなぎ、ディスパッチの結果タスクの起動番地に飛ぶように飛び先をスタックに格納してコンテキストの大きさだけスタックポインタをずらし、ディスパッチします。DORMANTとはタスク生成直後の状態を指し、この状態のタスクを対象にしますので、act_tskではタスクの生成を行わないことになります。まだ生成方法を具体的に考えていなかったりしますが、静的生成も規定に入ってますし、まぁいいかな、と…。なお、このサービスコールの採用で今回制作するOSはμITRON4.0仕様準拠ということになりました。
act_tskにはwup_tskのように「起動要求のキューイング」というものもありますので、そのための領域もTCBに用意しました。これに対応するサービスコールはext_tsk(タスクの終了)で、もしキューイングされている起動要求があればタスクを終了した後直ちに再起動することになっています。これもμITRON4.0の特色で、WAIT状態が必須から外れDORMANT状態が必須になったことに対応するものです。そもそも今回はWAIT状態が欲しいためにOSを制作しているのでext_tskは実装していません。むずかしくはないでしょうけど。
次はタスク間のデータのやり取りに関連したサービスコール。シリアルポートのデータを対象としているのですが、文字にするか文字列にするかデータの単位についてちょっと悩みました。結局I/Oに近いところでは文字単位でデータを取り扱う都合から文字列というのはやめたのですが、こんどはどのサービスコールを使うかで一考。しかしμITRON4.0の仕様書を読んでみると、それまでメールボックスとして定義されていたものからリングバッファを構成するものを切り離して「データキュー」として再定義されたことがわかり、これを採用することにしました。昔のメールボックスではバッファをリングバッファにするか線形バッファにするかは実装者の勝手だったのですが、分けたぶん使い勝手も良くなっている印象を受けます。以前は定義を厳密にするということでちょっと不安だったのですが、これなら納得ですね(サービスコールの数は増えちゃうんだけど)。
データキューの実装はリングバッファそのものなので今まで何度も作ったリングバッファそのままをサービスコール中に再現しました。まぁ前は2の倍数-1にしてオフセットの計算を楽にしていましたが今回は汎用性を持たせるということでもっと単純にif文で範囲の逸脱をチェックすることにしましたが。snd_dtqではパラメータに指定されたデータをバッファに格納し、rcv_dtqではパラメータに指定されたポインタに取り出したデータを格納し、格納バイト数を増減し、それぞれ読み出しや書き込みのオフセットを進め、範囲から外れたらリセットするという、リングバッファとして当たり前の実装しかしていません。
問題はバッファがいっぱいだったり空だったりしたときの処理。もちろん空きができたり何かデータが入ったりするまでWAITにならないといけないのは当然です。さらにrcv_dtqでWAITになったタスクがあるかsnd_dtqでチェックしたりその逆もあります。この場合はバッファに格納せずに直接データを渡したり、空きができたバッファに直ちに格納したりという処理の後、待っていたタスクをREADYに変更します。仕様書にはバッファの大きさが0である場合も規定されているのですが、バッファが空だったり満杯だったりした時の振る舞いを正しく記述すればある程度は自然と対応できます。
次に割り込み関係。割り込みには割り込みハンドラと割り込みサービスルーチンがあるのですが、どちらかを選択することになっています(最初は一体のものだと思ってた)。といっても、サービスコールとしては「呼び出して欲しい割り込みハンドラを登録する」か「指定する割り込み番号に対応するハンドラから呼んでほしいサービスルーチンを登録する」のどちらかなので、結局は
割り込み→割り込みハンドラ→割り込みサービスルーチン
という流れは外せないことになります。指針としては、どうも「単一の割り込みがあって割り込みルーチンで要因を判別する必要がある」機構を持つCPUはハンドラを内蔵してサービスルーチン登録を、「割り込みがベクタで管理されてあらかじめ要因別に飛び先が異なる」機構を持つCPUはハンドラを登録するようにするようです。が、私はここではサービスルーチンを登録する方を選択しました。というのは、確かにZ80の割り込みは最大128要因をベクタで管理できるようになってはいますが、KL5C16030では内蔵の割り込みコントローラが16ベクタに絞り込んでありますので、それくらいならハンドラが数だけあってもたいしたことありません。
さて、CコンパイラであるYC160では割り込みルーチンをCで書くための機構が用意してあります。理想としてはそれをそのまま使うのが一番いいんですが、コンテキストの操作が隠れてしまうので、コンテキストの復帰後にあるべきディスパッチを復帰前にしか定義できなくなってしまい、不都合が生じます。そこでインラインアセンブラを使って自前でコンテキストの操作を行うことで、割り込みハンドラを記述することにしました。もうひとつ、Z80は割り込み処理から抜けるのにRETIという特別なreturnを使用するのですが、これをそのまま使用して割り込みハンドラから抜けるとすると、その直前にあるディスパッチによってタスク切り替えが起こってしまうと切り替わった先がずーっと割り込み処理中のような状態になってしまいます。当然多重割り込みを許可してあっても、抜けてない割り込みより優先度が低い割り込みは受け付けられません。KL5C16030では特定ポートに対して適当なデータを出力してやると割り込み処理が終了するようにできているので、そのポート操作の後ディスパッチをすることにしました。純粋なZ80だとどうしましょうかねぇ。RETIの次のアドレスをスタックに入れておいて、RETIを実行させれば割り込みから抜けることが可能になりますかね。
あとは割り込みサービスルーチン。ポインタとして定義した変数に飛び先を入れて、それを関数のように書けば可変の関数呼び出しとして使うことができるのはわかりましたが(ちゃんと「JP
(HL)」のようにコンパイルされてたし)、登録した任意の箇所の飛び先をどのように管理するか…。結局、「割り込みサービスルーチンコントロールブロック」のようなものを作って、飛び先をリンクポインタでつないでいくという今回制作しているプログラム中ではいくつも書いてるテクニックを使うことに落ち着きました。なお、ひとつの割り込みハンドラに対して複数のサービスルーチンを登録できるのは、ひとつのサービスルーチンではひとつの割り込み要因について処理するというスタイルを想定しているのでしょう。
とりあえず実装したいと思っていた機能についてコーディングしたので、最後にそれらの機能の生成を行うサービスコールを作ります。一時は静的生成も考えていたのですが、その場合実際にはコンパイラに通す前に「コンフィグレータ」というものにソースを通し、最終的なソースを生成することになっています。フリーで配布されているμITRONの中にはコンフィグレータも作っている例もあるのですが、ちょっとそれをさぼりたかったのと、いろいろ考えてみると動的生成ができれば特に必要ではないという結論になったせいで、静的生成は実装しないことにしました。アプリケーションによる変動要因は必要なタスクやバッファの数の定数をヘッダファイルの先頭で書きかえ、リコンパイルすることで対応します。
生成のサービスコールはタスクやデータキューにつけるIDとともにその属性をまとめた構造体を渡して、使用するグローバル変数などを初期化するものだと思えばいいでしょう。どのような構造体を渡せばいいかは仕様書にあります。具体的にそれを使うかどうかが問題で、例えばタスク生成なら「属性」やら「拡張情報」などというのは今のところ使い道を思いつきません(属性というのはタスクがパラメータをスタックで渡すのかレジスタで渡すのかを定義するのですが、サービスコールはスタック渡しになってますし…。拡張情報は渡すとするとあらかじめスタックに積んでおくのか、それともTCBに入れるのかが判断つきません)。属性についてはデータキューだけ「送信待ち行列のリンク順」がFIFOかタスクの優先度順かという定義が含まれていますので、これは取り込むことにします。タスクの方には自動起動という属性があるので取り込みはしませんが生成の最後にact_tskを呼び出すことにします。
タスクのスタックやデータキューのバッファを自動定義する機能もあります。具体的にアドレスを与えないとOSが勝手に確保するのですが、ここではスタック領域から切り出して与えることにしました。機能としてはタスクなどの削除のサービスコールもあり、途中のオブジェクトを削除すると虫食いになってしまうのが気になりますが、今回は実装しないので考えないことにします。まじめにやるなら、ガベージコレクションでもしないといけないのでしょうが…削除と生成を繰り返すオブジェクトについては固定的に領域を与えるのが正義でしょうかね。
最後の最後に、μITRONとしての初期化ルーチンを付加します。初期化ということでやはり最初に呼び出して欲しいですから、ここを全体のmainと定義します。こうすればスタートアップルーチンから真っ先に飛んできてくれます。スタートアップルーチンを見ると別の名前でも構わないような気がしますが、そこまでの傍若無人はしないことにしましょう。
実際のところほとんど初期化する部分はないんですが、オブジェクトIDが有効かどうかの判定にあり得ないポインタなどを判別材料にしているので、その「無効の印」を書きこんでおくのと、サービスコール中で初期化されないリンクポインタがあるのでその初期化だけはしています。そして、ディスパッチのところで触れたアイドル用の「隠れタスク」の生成と起動もここでやります。タスクIDはたいていのサービスコールで「自タスク」を意味する"0"、つまりサービスコールではレディキューから外せないタスクということになります。ちょっと困ったのが、単純に起動するとそちらに制御が移ってしまい、しかも中身は永久ループですから抜け出せなくなって初期化の途中でシステムが止まってしまうことです。悩んだ末に思い出したのが、割り込み処理中はそのネスティングを数えておいて、ディスパッチをしない仕組み。そうか、ではネスティングのカウンタを1増やしておけばディスパッチせずに帰ってきてくれるじゃないか、ということでそのようにコーディングしたのですが、よ〜く考えてみると「ディスパッチの許可・不許可」のサービスコールというものがあるじゃないですか(^_^;)。仕様書を見てみると、dis_dspでディスパッチ禁止、ena_dspでディスパッチ許可となっていますが、何度dis_dspを実行しても一度のena_dspでディスパッチが許可されることになっています。一瞬、割り込みのネスティングと兼ねようかと思いましたが、割り込み処理中にena_dspを実行するとディスパッチが許可されてしまい、割り込み処理中にもかかわらずディスパッチされてしまうことになります。そこで割り込みのネスティングとは別に変数を用意してディスパッチ部ではそれも条件判断に入れることにしました。ついでに、CPUのロック機能も実装しました。
これらの初期化作業が終わったら、いよいよユーザの初期化ルーチンを呼び出します。名前はusr_mainで、ユーザがここにアプリケーションとしてのタスク生成・起動やデータキューの生成を記述するということになります。
ということで、これで「試作」としてはだいたいの完成です。あとはTCP/IPプロトコルスタックで使う「メッセージバッファ」を実装すれば当面私が使いたいμITRONの機能は揃ってしまいます。うーん、覚悟はしていたが、ほとんど完成品ではないか。サービスコールがありながら実装しない機能もいくつかあるけれど、ほとんど手抜きしないで作りこんじゃったからなぁ。
ユーザアプリは理想的なつくり
続いてユーザの初期化ルーチンとユーザタスクを作ります。初期化ルーチンは仕様書で決められている構造体をauto変数で定義して、必要事項を収めたらcre_xxxとかいうサービスコールを呼び出せばOK。タスクが3つあるわけですが、2つめまでのタスクの作成はディスパッチ禁止で、最後のタスク生成のときはディスパッチ許可にしておけば生成直後にディスパッチが起きてプログラムが本格的に始動することになるはずです。あ、もちろんタスク生成時の属性は「自動起動」にするんですが。
次にタスクと割り込みサービスルーチンの中身を作ります。タスク1とタスク2は文字の受信によって起床されるんですが、次のように作ります。
それぞれは文字が受信されるまでに起床待ちになっておいて、文字が受信されると起床される→起床待ちから戻ってメッセージを出力して、また起床待ちになるということですね。メッセージの出力は具体的にはサービスコールを使うのですが、文字を切り出してパラメータにセットして…とかいうのは煩雑になるので、専用のサブルーチンを用意します。これを拡張すればprintfみたいなものも実現できると思いますが無理はしません。
タスク3はシリアルポートへ文字列を出力するタスクです。次のように作ります。
最初に文字列を受け取るのはサービスコールなので、もし文字列がなければ受信待ちということでレディキューから外れます。タスク1か2から文字列が送られたところで、走行できる条件が整うことになります。シリアルポートに一文字送ったあと起床待ちになるのは送信割り込みを待つためです。なお、タスク3だけ優先度を高くしてあるので、文字列を受け取る段階ではまだどのタスクも文字列を送ってないので必ず受信待ちになります。文字列を受け取れば直ちにシリアルポートからの送信を開始しますが、一文字送り終えるまでまた待ちます。送れるようになったら直ちに続きにとりかかります。
割り込みサービスルーチンは受信割り込みと送信割り込みの2つがあります。受信割り込みではシリアルポートから受信した文字を受け取り、その内容によってタスク1と2を起床します。送信割り込みは単にタスク3を起床します。
さて、いろいろ書いてあるのを読むうちに、「セマフォはなくていいの?」という疑問をもたれた方もいるかもしれません。実はわざとセマフォを作っていないのです。ここではシリアルポートがハードウェアでの資源ということになるのでしょうが、あるタスクに専門的にアクセスさせることで排他制御をしなくてもシステムが作れるのです。つまり上記のユーザタスクは、タスク3というのはなくて、共通のシリアルポート出力ルーチンがあって、文字列が混ざってはいけないので出力ルーチンを呼ぶ前にセマフォを獲得するようにして、獲得できたタスクが出力ルーチンを呼べる、という作り方もあるというわけです。ですがマルチタスクモニタとしてはセマフォはあまり使うべきでない(完全にマルチタスク化できてない)という指摘もあります。上記のタスク3は「デバイスドライバ」と見ることもできますし、実際この作り方は「ITRON標準ガイドブック2」にも掲載されています。
と簡単に作ったように見えますが、実は「文字列の受け渡し」についてデータキューでは都合が悪いことがこの段階になって判明しました。データキューでは一文字単位にディスパッチが起こる可能性があって、タスクが2つあるわけですから文字単位に管理されてしまうと混ざってしまうかもしれないわけですね。まぁ今回は文字列リテラルだけなのでデータキューでポインタを渡してもいいのですが、せっかくなので、メッセージバッファも作ってしまいます。メッセージバッファもデータキューと同じくリングバッファで構成するのですが、サービスコール内でコピーを伴うのが特徴です。リングバッファにデータを入れる前にデータ長を入れてしまうなど、ますます「前に作ったリングバッファ」そのものですし、ディスパッチを伴うデータのやり取りというのはデータキューで形になってますので、それの応用でわりとやさしく作る事ができます(書きこみ・読み出しポインタの管理が面倒だったけど。全然スマートにならなかった)。
あとはスタートアップルーチン。割り込みベクタが入りますので、各割り込みハンドラのラベルを外部参照で宣言して、JP3命令で飛んでくるようにすればOK。シリアルポートなどの初期化はこちらに入れてしまえば、とりあえず形にはなりました。
デバッガ欲しい…
プログラムが揃ったところで、いざコンパイル…おお、エラーがまさに山のように(;_;)。YC160はコンパイルエラーを49個までしか報告しないので、とにかく見つけたエラーから順番に片付けて行かないと仕方ありません。文法エラーはともかくとして、多かったのが構造体とポインタの取り扱い。それぞれじゃなくて、複合的なものですね。構造体をポインタとして渡す時に勘違いがあったのでした。配列をポインタで渡すときは要素を取っ払った名前だけの状態にすれば、その配列の先頭アドレスを表現することができるわけですが、同じノリで構造体の要素を書かない状態にしてアドレスを表現しているつもりになっていたのです。良く考えればわかりそうなものですが…。
エラーで困ったのがスタックにタスクのスタートアドレスを仕込むところ。ディスパッチ部でレジスタを復帰していくと、RET3命令で自然に目的の番地に飛べるようにあらかじめそういう内容のスタックにしておくのですが、素直に代入できないのです。stackをスタックのポインタ、startを開始アドレスのポインタとして、stackがスタックの底を指しているとすると、最初は
*(stack - 3)=start;
と書いていました(stackから3引くのはアドレスが3バイトだからです)。良さそうな気がするのですが、実はそれぞれ
unsigned char *stack; void *start; |
として宣言してありました(正確にはtypedefで別名にしてある)。startはアドレスで渡そうとしているので型は関係なく、問題はstackの方にあります。初期スタックを作るところで1バイト単位でずらす操作が必要なので、他の型では宣言できないのです。ではstartの上中下位バイトをそれぞれ代入すれば…と三行に分けたのですが、ポインタに論理演算はできないということのようですのでそれもだめ。
よ〜く考えた結果、こうしました。代入する先もポインタである必要があるからエラーになるわけで、つまりはstackの指すアドレスにポインタがあるということにすればいいわけですね。stackの指すアドレス=ポインタですから、ポインタのポインタ、二重ポインタであると型変換してやればいいわけです。具体的にはこうなります。
*(unsigned char **)stack=start;
「ポインタの先にポインタがある変数の、先の方のポインタにポインタを代入する」ってこれじゃわからんか。あとでそのように代入されているのを確認してますし、これはこれであってます。
もうひとつ悩んだのが割り込みハンドラでの割り込みサービスルーチンの呼び出し。割り込みサービスルーチンコントロールブロックに収めてあるジャンプ先に飛ぶために
(* temp->jump)();
としてたのですが(tempはコントロールブロックのポインタ、jumpはそのメンバ)、プロトタイプ宣言もしてないのでエラーになりました。まぁ原因はすぐわかったのですがこれをそのままプロトタイプ宣言はできないような気がして、別の方法を考えました。結局、
void (* isr)(void); isr=temp->jump; (* isr)(); |
として、変数宣言と同時にプロトタイプ宣言して、呼び出し前に飛び先を代入することにしました。
エラーと共にワーニングも夥しく報告されました。ほとんどリンクポインタ関連のもので、リンクポインタを持つ構造体は先頭からリンクポインタを配してアドレスが正しく渡れば問題なくアクセスできるようにはしてあったのですが、あまりの数の多さに、ちゃんと対策してやることにしました。といっても単に構造体を型変換して要求通りの型に見せてるだけですが。
ということで、ようやくエラーもワーニングもきれいに消して、動かしてみます…動きません(;_;)。まぁ一発で動くなんて思ってもみませんでしたが。
システム初期化ルーチンやユーザ初期化ルーチンのあちこちにprintfやputsなんかを仕込んで、サービスコールから帰ってきているかチェック。メッセージバッファ生成も、割り込みサービスルーチン生成も、タスク生成もサービスコールで行ったままになってはいないようです。最初に起動されるタスク3にその旨を知らせるメッセージ出力を仕込んだものの表示されず。もしや…と思って、最後に生成直後ディスパッチされて帰ってこないはずのタスク3の生成のあとにメッセージを仕込むと…表示される(;_;)。ディスパッチされてないらしいので、ディスパッチ部の先頭でディスパッチ許可・不許可の変数をダンプすると…初期化してないらしい(;_;)。この他、タスクコントロールブロック、メッセージバッファコントロールブロックなど、あちらこちらで初期化してない変数などがあって、その始末に追われました。
初期化がなんとかなれば、今度はスタックの内容が合っているか確かめるために、任意のアドレスから任意のバイト数分8バイト毎にメモリダンプを表示するルーチンを作りました。その結果とディスアセンブル結果を突き合わせたところ、こちらもまだ間違いがありました。コンパイルされた関数は、先頭で
PUSH LD LD ADD LD |
IX IX,SP HL,-2 HL,SP SP,HL |
としてローカル変数を確保するようになっています。ディスパッチ部はここで設定された新たなスタックポインタにレジスタをPUSHすることになるわけですね。関数の最後では元のスタックポインタに戻すために
LD POP RET3 |
SP,IX IX |
としてますので、初期スタックに設定する飛び先=関数からの戻り先はIXの保存分も加味してセットしないといけないですし、この時点でIXにはスタックポインタが入っていなければなりません。そこで、初期スタックの頭にIXに入れるスタックのアドレスも仕込んでおくことにします(始めにIXからPOPするので)。
一応ディスパッチ部に制御が移り、スタックも狙い通りにPOPされてタスクに起動がかかるようになりました。最初に優先度の高いタスク3が起動し、メッセージバッファの受信待ちになってディスパッチし、次に優先度の高いタスク1が起動し、メッセージを送るとタスク3が直ちに受け取って文字列の送信を始める…ように見えますが文字列は送られませんし、ディスパッチのたびにスタックの内容をダンプさせているのですが、そのアドレスが変なのです。というのも、タスク3では受け取る文字列のために80バイトの配列を宣言しているのですが、ディスパッチ部で見るスタックアドレスはタスク3の起動時のものとまったく同じなのです。配列が宣言されてない?そんなばかな、ということで今度はタスク3の中でのスタックの内容を表示させると、そこは問題なさそうなアドレスになっています。ところがディスパッチ部では、なんとタスク3で表示されているアドレスと同じものが表示されるようになりました。ううむ、何してるのやら…デバッガ欲しい…。
スタックポインタはCPUレジスタですのでCから直接参照することはできません。そこでインラインアセンブラで一度グローバル変数に書き出してから、その変数を参照するようにしています。もちろんディスパッチ部でも同じなのですが、どうもそこを実行していないような気がしてきました。つまり、ディスパッチ直前のスタックポインタの値を保存せず、ずっとタスク生成時の初期設定値を使っているということです。でもCのソースにはそのような記述はありません。
まさかな、と思いながらアセンブルリストを作成し中を見てみると…なんと、インラインアセンブラの次に書いたCの構文に相当する場所にラベルがあって、インラインアセンブラの直前にあるディスパッチ不許可状態のチェックからそこに飛ぶようにコンパイルされていたのです。ううむ、バグか仕様か?!(Ver.2.00のバグだそうです。elseのないif文をインラインアセンブラの直前に配するとそのようにコンパイルしてしまうのだそうです)
しかし、どう対処しようか…意味のない命令なんてないし(あります、セミコロンだけ書いて「空文」にすればいいのです。忘れてました(^_^;))…NOPってインラインアセンブラで書いてもしかたないし…もしかしたら、Cでのラベルをさも意味ありげに書いてやれば、そこに飛んでくるかもしれない、ということで試してみると、思惑通りそのラベルに飛ぶようにコンパイルされました(対処がちょっと難しいので、当分空文かラベルで対処して欲しいとのことです)。
今度はスタックのアドレスだけ表示するようにちょっと変えて、ディスパッチ後のTCBのアドレスも表示するようにして制御の動きを追い始めました。何やら1バイトくらいは送信を始めたようですが、表示を見るとタスク3もタスク1も2回起動されています。スタックのアドレスを見ても2回目の起動時には完全に初期状態に戻っているようです。ううむ、何してるのやら…デバッガ欲しい…。
送信は始めていますから、送信割り込みが発生している可能性はあります。つまり割り込みハンドラで不都合が起こっているのかもしれません。またインラインアセンブラ絡みでコンパイルミスが起こっているのかも、とコンパイル結果のアセンブルリストから割り込みハンドラを探してみると…見つからない(?_?)。割り込みベクタを見てみると、どーもどのベクタも同じアドレスを指してるようです。ということで、次はスタートアップルーチン。そこの割り込みベクタから各割り込みハンドラに飛ぶところが、なんとどれもコメントアウトされてました(^_^;)。元々コメントだったジャンプ命令を修正したのに、コメントであることを忘れてたわけですね。その先にはプログラムの先頭アドレスがありますので、割り込みがかかると先頭アドレスに飛んでしまってたために、2回目の起動がかかったのです。
割り込みはかかるようになりましたが、MZ-2500の画面にはほんのちょっとしか表示がありません。半角文字で4文字くらいでしょうか?なんで途中で止まるのか、あちらこちらにputsなどを仕込んで確かめようとしたのですが、こんどはちゃんとタスク1が表示させようとした文字列がきっちり出てきました。でもタスク2の文字列はありません。タスク起動時のデバッグ用メッセージもタスク2のものは表示されないので、どうもタスク2の起動がかかってないようです。タスク1と2の生成順序を入れ替えるとタスク1が起動しなくなることから、タスクの構造が間違ってるというのはなさそうです(コピーして作ってるんだからあるはずないんだけど)。タスク1と3が起動しているだけに今更初期スタックがおかしいなんてのも考えにくいんですけど、一応スタックの頭をダンプしてみると…ディスパッチ部では確かにタスク2へ切り替わろうとしていますが、先頭の値は納得行かない数値になっています。念のためタスク起動時にスタックをダンプしてみると、当初想定どおりに初期スタックが形成されています。初期スタックを設定してからタスク2の起動までに、誰かがスタックを壊してるんです。ううむ、何してるのやら…デバッガ欲しい…。
デバッグ用の文字列が仕込んであると全部の文字列が表示されて、それがないと途中で表示が止まるのは、デバッグ用文字列の表示でプログラム全体の処理速度が落ち、割り込みの入るタイミングが変わったからだと予想されます。送信用レジスタに書きこんでからそれが空になって送信割り込みが入るまでの時間、デバッグ用の文字列がなければたくさん仕事ができますので、いつかタスク3も1も待ちに入る状態になるわけです。その時にタスク2への切り替えを失敗するので、暴走し表示が途中で止まったというわけです。デバッグ用の文字列があるときはタスク3が待ちに入る時間が短くなる、あるいはslp_tskを実行する前に割り込みがかかりwup_tskが実行されてキューイングされslp_tskで待ちに入らないという状態になり、文字列が最後まで表示されてやっとタスク2が起動されるが切り替えに失敗するということなのでしょう。
ディスパッチのたびにタスク2のスタックをダンプさせるようにしてみると、タスク3が起動しタスク1に切り替わるまでは変化はありませんが、タスク1から再びタスク3に制御が戻るときにはもう書き変わっています。タスク1のコンパイル結果を眺めてみますが、特に変なところはありません(すっかりコンパイル結果を信用してないみたいですが、コンパイラはタスクが変化してるなんて思いもよらないんです。タスクアドレスが変わると都合の悪いことがあるかもしれないからです)。ダンプを見てみると先頭の3バイト(先述した最終的にスタックを元に戻すためのIXをPOPする対象が含まれています)に変化があるのですが、そもそもどこから書き変わっているんでしょう?ということでスタックの頭から32バイトずらしたところからダンプしてみると、都合16バイトほどがまとまって書き変わっていることがわかりました。何かの文字列かと思いましたが、時々0が混じったり0x80以上だったりしてなんとも的を得ません。
実はデバッグ中に、スタックの中身がいかにも表示しようとしている文字列の一部であることに気づいて、各タスクに与えるスタックのアドレスをずらしたことがありました。コンパイラはスタックがひとつであるとの前提に立っていて、それが自由に使えるつもりでいます。printf関数の中で、表示するための文字列を生成するためにauto変数を宣言していて、それがタスクのスタックにぶつかったわけですね。この時はそれを見越した分のスタックを初期化ルーチンに残してやることで衝突を回避したのです。それと同じ事が起きているのではないかと言うことで、今回は「ぶつかっている」タスク2のスタックまでの分、つまりタスク2と1とアイドルタスクの分を加えてずらしてみました。が、やはりタスク2のスタックがつぶされてしまいます。
でもこれで答えが見えました。つぶされ方は少々変化がありましたが、ほぼ同じ量が(相対的に)同じ場所でつぶされているので、初期化ルーチンに残すためにずらした分というのは関係なくて、そのあとに積み上げたスタックを使っているプログラム…つまりタスクに問題があるわけです。しかもタスク1に。printfのワークとぶつかった経験から、タスク1のデバッグ用文字列を取っ払ってみました。すると、タスク2の起動が他のデバッグ用メッセージで確認できました。恐れてはいましたが、やはり標準関数がタスクのスタックをつぶしてしまうことが起こるんですね。できればデバッグ用に文字列を仕込むときの、その表示ルーチンはタスクで実現すべきなんでしょう。今回みたいな「根本的にタスクの遷移をデバッグする」目的には本末転倒なんですが…。そうでなければ、たんまりとスタックを与えてあげれば、他のタスクのスタックをつぶすこともないかな。
苦難はさらに続きます。これでバグは取れたものと思い、デバッグ用のメッセージを消して全速力で動かしてみると、タスク2のメッセージが表示されないのです。メッセージの場所をいろいろ変えて調べてみると、タスク2のメッセージを受け取ろうとしてrcv_mbfを実行したまま帰ってきてないようなのはわかりました。が、割り込み待ちのslp_tskの直後に適当なメッセージを仕込むと、タスク2のメッセージを受け取り、その後は期待通りの動作をするのです。つまり、またこれも割り込みのタイミングが影響しているというわけです。
どのように動いているのかを調べるためにメッセージを仕込むと、特にタスク3の送信レジスタ書き込み→割り込み許可→slp_tsk→送信レジスタ書きこみのループの中に入る場合はその障害が現れません。メッセージをたんまり書けるようにスタックの量を大幅に増やしたのですが、肝心のメッセージを必要最小限にしないといけないのです。そろそろディスパッチそのものについては問題も出尽くしたようではありますが、なんとも言えません。
メッセージバッファからの受け取りで止まっているのでそのあたりに問題があるのだろうといろいろ考えてみると、ひとつ気になるところが浮かび上がりました。デバッグ用メッセージによれば、動いているときはタスク1用の文字列をシリアルポートから出力しているときにはタスク3がslp_tskで待ちに入ってもキューイングされているのかディスパッチされずにタスク3に再び実行権が渡っています。つまりタスク2にはまだ起動がかけられてないわけですが、もし起動がかかって、snd_mbfでデータを転送している最中に、タスク3からの受信が実行されたらどうなるのでしょう(バッファにはあらかじめメッセージは入っていないものとします)?転送は終了してないですからバッファに格納されたバイト数はゼロです。なので受信待ちになり、ディスパッチされます。転送中の箇所に制御が戻りその転送が終わると、格納バイト数が更新され、転送していたタスクに戻りますが、受信待ちでディスパッチされたタスクには受信されたことが伝わりません。さらにバッファが満杯のときに受信しかけて、送信しようとしたタスクが割り込んで送信できなくてディスパッチし、受信が再開されて空きができても、送信待ちのタスクに空きができたことが伝わらないというのもあります。つまりサービスコールの先頭でしか待ちタスクの存在を確認してないためで、待ちタスクが待ちを解除される条件が整うならもう一度チェックする必要があるということですね。で、せめて転送が始まったら邪魔はされないように、転送部分だけ割り込み禁止にしてみましたが、実行結果は変わらず。関係ないんでしょうか?
実際にこの問題で不具合が出ているのかどうかを調べるためにどのタスクにディスパッチしているのかを確かめたいのですが、これまでどおりTCBのアドレスを表示するのでは時間がかかり現象を見ることができません。できるだけ処理を軽くするため、TCBにタスクIDを追加してそれをディスパッチ部で表示するようにしましたが、それでもだめ。数字だけにしてもだめ。改行をやめてもだめ。CPUのウェイトを減らしてみましたがそれでもだめ。そこで、TCBのタスクIDの格納場所にはその文字コードを入れることとし、printfは文字の表示を一文字だけすることにしました。これでようやく現象を確認できる軽さにすることができました。
タスクの遷移を見てみると、タスク2はタスク1の文字表示をするかなり早い時期にメッセージバッファへの送信を完了しているようで、タスク3が次のメッセージを受信しようという頃にはタスク2の姿はありません。ですので、先ほど考えた原因ではなさそうです(まずいのは確かなのであとで直しますが)。
そのタスクの遷移の最後を確認すると、タスク3がディスパッチされてアイドルタスクに制御が移っていました。タスク3でチェックしたときはrcv_mbfで行方不明になっていましたので、メッセージを受信しようとしてディスパッチしたのは間違いありません。そこで、rcv_mbfとsnd_mbfの内部に一文字だけ表示するデバッグ用メッセージを仕込んで、どこを走っているのかを確かめることにしました。動かしてみると確かにメッセージを受信しに行ったところでディスパッチしています。しかし、そこはメッセージが存在するので受信しようとしている箇所で、空きがなくて送信を待っているタスクがないかチェックしますが今はそんなのはなく、そのままサービスコールから戻ってくるはずです。…がよく見てみると、送信待ちタスクをチェックする箇所にはreturnがありますが、そのif文の外(メッセージがあるかをチェックするif文よりは内側)にはありません。これでは、メッセージを受け取った後受信待ちに入ってしまいます。ここのreturn文を移動してやると、ようやく動くようになりました。
…と思ったのですが、よく見ると、連続してシリアルポートから送信している文字列のうち、ひとつめの改行コードが送られていないようで、ふたつめの文字列と連結してしまっています。一旦動き出すと問題はないですし、rcv_mbfで受信する文字列は送っている文字数と同じだけあるようです。何かのきっかけで一文字減ることがないか調べるために文字列の長さを変え、できるだけ負担がかからないような文字数の表示を試みましたが、今度は二つ目の文字列の先頭が欠けてしまうという現象になりました。ううむ、何してるのやら…でもこんなところでデバッガなんていらないし、使えなさそう。
ふと、タスク1の文字列とタスク2の文字列との時間的な間隔が短いのでは?と思いつきました。いや、何がきっかけかよくわかりませんが、詰まりついでにこのページでも書いておこうかと夕食をはさみつらつら書いていたときに思い至りました。シリアルポートにすれば二つの文字列の区別はないはずだし、そもそもタスク3を書いたときには二つの文字列の時間的な間隔はもっとあると思いこんでました。というのは、さっきのタスク3の説明にあった、
の3と4がなんでこの順番なのかというところにかかるんですが、最後の文字の送信をセットして、もう次の文字がないなら、割り込みされて呼ばれる必要はなくさっさと次の文字列の受け取りに移るべきだと考えてたんですね。でも実は、1で受け取る文字列は既に用意されてて、3から1へ遷移してただちに2へ遷移するものだから、シリアルポートは最後の文字のひとつ前の文字を送信している最中で、その次の送るべき文字(改行コード)を次の文字列の一文字目が上書きしてしまい、結果文字列が連結されてしまったんです。タスク1と2の文字列の長さを変えたときは、その文字数を表示しようとしていた箇所の時間分だけ遅れ、ポートに書きこんでも無視される時間帯(シリアルポートの構造上、ほんのわずかな一瞬だと思うんですが…)にあたったので、先頭の文字が欠けたというわけなのです。いくら先に急ぎたいといってもアクセスしてはいけない瞬間にアクセスできる構造が問題なのですから、タスク3は
というようにする必要があるわけですね。これで本当に目標どおりの動作をするようになりました。あとはsnd_mbfとrcv_mbfの問題回避とext_tskを実装して、ついでにデータキューもデバッグしてしまえば、μITRON4.0の最小構成としては完成ですな。え、ext_tskは使わないんじゃないのかって?仕様書を見てみると最小構成として「休止(DORMANT)状態があること」とあるので、cre_tskでDORMANT状態のタスクはできますから、RUNやWAITのタスクがDORMANTになれる機構を用意すれば「μITRON4.0仕様準拠」と堂々と宣言できるのです。なぜするのかって?フフフ…。
ところで、問題が発生してその対処とかをすぐやってるように見えますが、実際には、問題発覚から解決の糸口をつかむまで半日とか丸一日とか二日とかかかってるんですよ。スタックがころころ移動するのでリモートデバッガはあてにならないかもしれない、割り込みはシミュレータでは正確に再現できない、という点でICEでもあれば良かったんですが…。
再びTCP/IP!
ということで今度は棚上げになっていたTCP/IPプロトコルスタックの実装。タイムアウトをサポートするサービスコールをOSに追加する必要があるかもしれませんが、それはそうなってからでいいでしょう。単純に実装していってもいいんですが、ITRON TCP/IP APIというのもありますので、それに準拠して作ってみようと思います。
と意気込むのもいいんですが、ちょおっと別件のネタがありますので、ここにとりかかるのはもうちょっと先。さらにその先に夢と野望は広がっていますので、ここで止めたりはしませんですよ。
…の前に、回り道
このμITRONにとりかかる直前、コンパイラの販売元イエローソフトのホームページに「当社製品による開発事例記事募集」なる企画が掲載されました。最終的に完成したら「こんなん作りました」って報告しようかと思ってたくらいだったのですが、なんだか渡りに船という感じです。本当は全て完成してから応募すべきなんでしょうが、応募期限と定められている9月末には到底間に合わないのでせめてμITRONの部分だけでもという方針で、制作に取りかかったのです。
幸いにして発表できるレベルのものが期限までに出来上がったので、予定通り応募しました。実はその直前にバグ関連で確かめておきたいことがあってメールしたのですが、そこに「応募予定」と書いたところ返事に「ぜひ応募してください。今応募したら即採用です。期限は延長予定なので気にしなくていいです」とあったので遅れても構わなかったんですがね。まぁ他にも着手予定のものがあったので無意味に期間を開ける必要はなかったのです。しかしそれよりも、どうも担当(というか社長さん?)はあまりの応募の少なさに困っていらっしゃったみたいなのが気になりました。まぁそれでもいくつかあるんじゃないのかと思ってたんですが…。
10月に入って程なく連絡があり、正式な記事執筆依頼を頂いたので、ホームページビルダーをワープロ代わりにゆっくり書いていきました。あまりにも書き心地がいいので、一太郎Ark
for Javaの購入を検討したくらい。まぁ私にはTeXがあるし。ってかなり忘れてるけど。内容はここの焼き直しだと面白いものができるかもしれませんが、どうも「開発事例」という位置付けからは離れたものになりそうで、他の人に参考になるものとなるかちょっと疑問です。ということで、いろいろ悩んでできたのがこの記事。
http://www.yellowsoft.com/contents/usersamp/user1/kiji.htm
読むと、偉そうな態度でいかにも経験者ヅラして書いてあったりするのが(^_^;)なところです。要は「μITRONを作りたいと思ったら何から手をつけたらいいのか?」という疑問に答えられる記事にしたいと思ったわけですね。というのも、特に参考になったインターフェース誌1993年12月号ってもう手に入らないと思われるからです。あの記事の代わりになる物が欲しい、という意味で拙作というのはあまりにもおこがましいものがありますが、まぁ意気込みだけは買ってくださいな。
ところで、URLを/usersamp以下を/userapli.htmに編集して表示するとわかりますが、まだ私一人の記事しかありません(^_^;)。もしかして私以外誰も応募しなかったんでしょうか?
これについては一部納得するところもあります。個人での応募も、会社としての応募も、どちらでもよかったのではありますが、会社としての応募だと開発している製品の一部を公表することになるという点で担当者やその上司が応募に踏み切れないという問題があります。また趣味でプログラムを組む人というのは世間一般で言われているほどの数はないはずです。「言われている」というのは、例えば最近だとH8やPICなんかが流行ってたりして雑誌記事などを見かけることもありますが、それを読む人がみなそういう方向に走るという時代でもなくなりました。我々が学生の頃は「パソコンを持っている」=「プログラムの知識が多少なりともある」、「パソコンを使っている」=「プログラムを作っている」という常識がありましたが、今はプログラムを書けるとなると下手すれば超天才扱いです(もちろんソフト制作とは関係のない場所での話です)。さらに、コンパイラなどの販売実績が2000を超えたくらいという数、また組み込みコンパイラを趣味で買うという酔狂なやつがどれだけいるのか、ということを考えると、あまり応募数は見込めそうにありません。トラ技の広告では募集しなかったのにも一因あるかもしれません。
# …と思ったら、12/22付けで3件ということになりました。おお、ちゃんと企業からの応募もあるじゃないか。
でもね、いいコンパイラですよ。これだけ安くて、ちゃんと性能が出て(他のものと比べてないのでよくわかりませんが)、さらにボードとセットで開発環境が完成してる商品がある。LSI-C80なんて約10万円ですもんね。そりゃあ各種80系のコンパイラが同梱してありますが、同等のものは6万円で手に入るわけですから、こちらは。もっと売れてもいいものだと思いますよ。
そろそろ続きを…
でも長くなったので続きは別ページにて。
参考文献
いわずと知れた原典です。
μITRON4.0の仕様について、3.0との比較を中心に簡単な解説をしています。仕様書をそのまま読むのは大変ですから、まずこれを見てどんな機能があるのか確認するのがいいでしょう。
Z80のアセンブラにて簡単なμITRONを実装した例が掲載されています。ディスパッチの仕組みやリンクポインタの使い方などはバージョンが変わってもITRONでなくても共通の技術ではないかと思いますので、そういう意味でも参考になります。
宮崎システム設計事務所の宮崎久則氏によるNORTi/86の実装事例が参考になります。ITRONや、リアルタイムOSとはなんぞやという人も勉強になる本です(過去にTRONWARE誌に掲載された入門記事などがまとまっています)。
本格的にμITRON4.0をお手本にするまでは読んでいました。ここのメールボックスやメッセージバッファの項目を読んでどういう形式にしようか悩んでいたときに、4.0の仕様書を読んで頭がすっきりしたせいで、あまり読まなくなりました。
LSI-C86で記述された、μITRON3.0仕様OSです。LSI-C80でのコンパイルも考慮してあって、特にCでディスパッチする場合の参考にさせていただきました。でも割り込み関係の記述があまりなかったような気がするな…。