優音OS (μITRON版)


優音は組込みCPUボードだ !

 MZ-MLで優音の話題が出るたびに、頭に描くのはITRONのこと。坂村教授がTRONを提唱した当時は興味もなくまともに取り合う気もなかったのですが、いわゆるDOS/VマシンでBTRONが走るようになってどんどんそちらに興味が向いてきました。情報に接しないうちはTRONは失敗したものと思いこんでいたわけですが、実はTRONが市場を制覇していたなんて、目からうろこが落ちる思いでした。
 それに、その仕様書は本屋で買える。WindowsならAPIの解説書は手に入りますが、あくまで外から見た「動き」なのであって設計図とは違うんです。OSが自分で作れる?わくわくするじゃないですか。


私の理解するITRON

 TRONというのはTRONプロジェクト全体を指すキーワードで、実際にはそれがいくつかにわかれています。そのうち、ITRONはIndustrial TRONの略、つまり工業用組み込みOSの領域を担うものです。工業用といっても別に工場で使うものというのではなくて、工場で生産されるあらゆる製品に組み込めるものという意味なのです。有名なところではカシオのデジタルカメラ(最新のものはITRONでなくなったようですが)とかRolandの電子楽器、東京ガスのガスメーターに搭載されています。ITRONは年間5億コピーもの数が出荷されているといわれていて、数だけならWindowsやLinuxなんて敵ではないのです。って比べても仕方ないんですが。
 どうしてこれだけの量のITRONが出荷されているのにそれを知っている人が圧倒的に少ないのかというと、「これこれのOSを使用しています」という文句が宣伝にならないからです。ITRONを使っているからといって、その搭載製品を購入した人に購入後のメリットなんて何もありません(製品の値段が抑えられるというメリットはあります)。ITRONの採用そのものが戦略なので公表することもできないという事情が絡む場合もあります。組み込みOSのメリットはそれを採用するメーカーの側に最大のメリットがあるのです。
 ITRONの特徴は、「弱い標準化」にあると言って良いと思います。例えば、Windows98と呼ばれる製品にはWindows98で規定されているAPIをすべて搭載する必要があります。もしスタンドアロンで使うマシンにWin98を載せたとして、ネットワークは使わないとわかっていてもネットワーク機能は装備されたままです。パソコンだからどんなにメモリやディスクを圧迫しても「足らないメモリは買い足してください」「足らないHDDは交換してください」と殿様商売できますけれど、まさかカメラやエアコンで「メモリ買い足せ」なんて言えるわけがありません。では最初からそのメモリを載せておくかといえば、コストの点で「うん」とは言えないでしょう。
 そこでITRONでは、「使わない機能は搭載しなくてよい」ということになっています。もしそれでメモリが節約されるのなら、どんどん機能を削っていいのです。ぶっちゃけた話、「プログラムが走れて」「イベントがあるまで待てて」「プログラムが再始動できる」ならこれでITRONと名乗ってもいいことになっています。実際にはいろいろな機能、タスク間通信とかセマフォとか実装していかないと使えるものにはならないのですが、それだけの自由度があるのは確かなのです。
 機能を削っていいのと同じ理由で独自の機能を追加しても構いません。メーカーそれぞれで設計されるプロセッサにはそれぞれの特徴がありますので、それを生かした機能を追加することが可能になります。これによって、共通の仕様に従いながら独自色を打ち出せるという二律背反の命題をクリアしているのです。


優音搭載ITRONの仕様

 これまで何度か優音へのμITRONの移植を検討してきました。実際に着手したこともあります。ですが、その度に細かい部分での具体性に欠け、挫折してきました。しかし、今度は違います。シリアル-LANアダプタのために作ったオリジナルのμITRONがあります。基本的にはこれを移植すれば目的を達成できるはずです。ということで、ここから下をごっそり書きかえることにしました。

 まずはメモリ。LANアダプタ版ではCPUがKL5C16030ということもあって、Z80互換コードが走りながら16MBのメモリ空間がリニアにアクセスできるので、カーネルもタスクも比較的自由に配置できました。しかしながら、優音はHD64180、MMUを用いバンクを切り替える必要があります。そこで、論理空間をこのように分割することにします。

コモンエリア0
共有メモリ

バンクエリア
割り込みベクタ
カーネル
データキュー

コモンエリア1
システムタスク
スタック


ユーザータスク
スタック
その1


ユーザータスク
スタック
その2

 物理メモリはこんな感じ。アドレスは一例です。


共有メモリ

00800

空き


81000

割り込みベクタ
カーネル
データキュー


88000

システムタスク
スタック



90000

ユーザータスク
スタック
その1


98000

ユーザータスク
スタック
その2


A0000

メッセージバッファ

 システムタスクとは、シリアルポートや共有メモリをアクセスするデバイスドライバのようなものです。論理空間としては同じ領域を他のタスクと共有することになります。タスク間のメッセージ交換に用いる領域は、データ量の少なそうなデータキューはカーネルと同じ領域に、転送単位が大きくなるメッセージバッファはバンクに入れないでDMAで直接転送するようにします。
 開発にはイエローソフトのYC80を使用します。YC80にはユーザー関数を分割したバンクに振り分ける機能があり、これを用いるとバンクを自動的に切り替えて呼び出せるようコンパイルできるのですが、今回はバンク振り分けのみ使用します。YC80そのままだとスタックがコモンエリア1に置かれるようなのですが、これもバンクエリアに置くようにします。バンク切り替えでスタックが見えなくなるように思えますが、バンク切り替えとタスクディスパッチとは同時に行うことにすれば問題はなさそうです。その代わり、本来ある「別バンクの関数呼び出し」は禁止にする必要はありますね。

 一時考えていた「ネットワーク接続機能によるMZとの通信」は、μITRON4.0にその機能の定義がないので当分見送り。まぁ気にせず取り込んでしまえばいいのもμITRONではありますがね。その代わり単純に割り込みを受けたら共有メモリから特定の領域を読み出してメッセージバッファへ送り出すタスクと、メッセージバッファから共有メモリにデータを書き込んでMZに送信を知らせるタスクは作っておくことにします。

 割り込みについてはHD64180内部で起こりうるものとZ80PIOについてベクタをあらかじめ設定しておき、あとからこれをいじらないといけないような状態にはしません。その割り込みによって起動されるタスクはシステムタスクに入れて、いわゆるユーザーは割り込みに対応したタスクは作成しなくてもよい(機能に応じたメッセージバッファなどで受けるようにすればいい)ということにします。

 というのがYC80のマニュアルやインストールされたファイルをざっと眺めて考えたところ。それではこれから、コンパイラやリンカを騙す算段を考えましょうか。


セグメント定義ファイル

 リンカをバンク対応で処理させるには、コンパイラに/Bn(nはバンク番号)というオプションを付加すればいいようです。すると、アセンブルリストにBANKnというセグメント名を定義して出力します。リンカは、「セグメント定義ファイル」という各セグメント名とアドレス、そしてロードアドレスを記述したファイルを参照してHEXファイルを出力します。ドキュメントには、バンク対応のセグメント定義ファイルはこんな感じで書けばいいとあります。

;SEG.DEF
#NOCROSS
;segment start - end , rom
main $1000 , $1000
TEXT - $7FFF
BANK1 $8000 - $BFFF , $8000
BANK2 $8000 - $BFFF , $18000 ; バンク2のオフセット加算
BANK3 $8000 - $BFFF , $28000 ; バンク3のオフセット加算
DATA_CONST $C000 , $C000
DATA
HEAP -  $FC00

 start,endとあるのは論理アドレスの範囲で、それぞれのセグメントに含まれるコードがそこにあることで意味を持つということになります。romとあるのが物理アドレスで、転送時・リセット時にそれぞれのセグメントが存在するアドレスになります。優音の場合は全てRAMなので、単純にロードアドレスだと思えばいいでしょう。
 例ではメモリが0番地近くから配置されているという想定になっているようです。ROMが0番地にあって、RAMが1000番地から…ということですね。優音にはROMはありませんし、RAMは80000番地からありますので、かなり様子が変わりそうです。
 先ほどの表からすると、DATA_CONSTとかDATAのセグメントはカーネルの直後、TEXTセグメントの次にないといけなさそうです。HEAPはmallocとか使わなければ必要ないはずなので、これは省略。大量にメモリが必要になるようなら、そのためのAPIを定義してバンク外に確保するようにしましょう。
 とすれば、こんな感じなのでしょうかね。

;SEG.DEF
#NOCROSS
;segment start - end , rom
main $1000 , $81000
TEXT
DATA_CONST
DATA - $7FFF
BANK1 $8000 - $FFFF , $88000
BANK2 $8000 - $FFFF , $90000 ; バンク2のオフセット加算
BANK3 $8000 - $FFFF , $98000 ; バンク3のオフセット加算

 例ではバンクが64KB単位になっていてかなりの無駄があるのですけど、4KB単位でバンク設定可能な64180なら物理メモリに32KB単位のバンクを刻んでも問題ないはずです。この境界とかサイズとかは用途によって可変として、ユーザーに決めてもらうことにしましょう。

 あ、書き忘れてましたが基本的にこのOSはソース供給・ユーザーアプリはカーネルとまとめてコンパイルという形式をとります。だからリンカのオプション設定の一部となるセグメント定義ファイルの詳細の設定を「ユーザーに任せる」と言えるのです。組み込みOSとしては自然な形ですが、普通のOSを想像する人には奇異に見えるでしょうね。ただ、問題は使用するコンパイラが市販品のものであり、それを皆に買わせるのは問題あるだろうということ。当面はYC80専用で組みますが、今後は対策が必要です。


スタートアップルーチン

 スタートアップルーチンには周辺の初期設定と割り込みベクタを書きます。MMU設定のうちスタートアップルーチンが存在するエリアのマッピングまでは共有メモリに書くプログラムに仕込みます。

 割り込みベクタは、YIDEに自動生成する機能があるのでそれを使用することにします。ベクタ番号とそのベクタでジャンプする関数の対応をひとつずつ入力すると、INTVECT.DEFというファイルが生成されるようです。スタートアップルーチンにはこれをインクルードすればいいとのこと。但し、

という注意点はあります。
 で優音の割り込みですが、外部からの割り込みのうちINT0はなし、INT1はZ80PIOから、INT2は外部コネクタということで、CPU外部からベクタを受け取る必要はありません。内部で自動生成するベクタに対応するベクタテーブルだけ定義すればいいはずです。
 自動生成するベクタの下位8ビットのうち下5ビットは固定、上3ビットはレジスタ設定となりますが固定コードは"00000"から振られています。ところが、YIDEのベクタテーブル自動生成機能ではベクタ番号0は作成できないということらしいので、レジスタ設定する部分は"001"をあてることで(ベクタは16から始まる)しのぎます。

この後は準備中。タイトルも柔軟に変わるかも。


LANアダプタ版からの変更点

 まず、タスクをバンクに振り分けることにしましたので、TCBの構造体にそのタスクが含まれるバンク番号というのを追加します。タスクの作成(cre_tsk)でこれを定義する必要がありますが、タスク作成用の構造体には特別なメンバーを追加せずに定義済みだが未使用のexinf(拡張情報)を使用することにします。
 バンクはタスクディスパッチの際に切り替わりますが、指定を簡易にするためにchgbank()なる関数を定義。バンクは物理メモリの88000hから始まりますので、コモンベースレジスタには88h+バンク番号×8を設定すれば良いことになります(これを書いてて現在修正中のソースの間違いに気づいたぞ)。デフォルトのスタックポインタを自動設定する際、スタックを管理している変数を参照して要求するサイズのスタックを切り出しますが、よく考えたらこの変数はバンク毎に必要ですね。各バンクに変数を配置するかとも考えましたが、ここは単純にバンクの数だけの配列変数を用意します。
 バンク切り替え関数をまずはタスクの起動(act_tsk)で使用。起動時にスタックにダミーのコンテキストとタスクの先頭番地を仕込むために、事前に該当タスクのあるバンクに切り替える必要があるわけですね。一方、同様のルーチンを含むタスクの終了(ext_tsk)は自タスクからしか呼ばれないので、実行時はすでにバンクは切り替え済みとなります。
 バンク切り替えは肝心要のディスパッチにも使います。スタックポインタを再設定する直前に切り替えることにします。で、ディスパッチといえばコンテキストの保存/復帰なんですが、KC160とは細かい点でレジスタ構成が違っていますのでそれに合わせて書換えが必要です。といっても具体的にはYIYを保存/復帰していたところをIYに書換えるだけですがね。それにつれてタスクの起動の際のダミーコンテキストのサイズも調整。KC160では18バイト用意していたところを、(1)YIY→IY、(2)タスクアドレス、の計2バイトが減るので16バイトに変更します。

 割り込みも変わります。まずは簡単なところで、割り込みの許可と禁止。KC160では全て割り込みコントローラで制御できましたが64180では各ペリフェラル毎に禁止/許可の制御が散らばっています。これを割り込み番号に従ってswitch文で振り分けることにします。割り込み番号はベクタ16を0として割り当てます。そうそう、割り込みハンドラも割り込みの数が減っているので削らないといけないですね。
 割り込みハンドラからは割り込みサービスルーチンが呼び出されますが、割り込み自体がどういうバンク状態で発生するか予想できませんので、割り込みサービスルーチン呼び出し直前に現在のバンクを保存し、サービスルーチン毎にバンクを切り替え、割り込みハンドラを抜ける直前に元のバンクに戻しておくという操作が必要になります。ですので割り込みサービスルーチン作成時にexinfにバンク番号を設定し、割り込みサービスルーチンコントロールブロックの構造体にバンク番号を追加します。
 割り込みハンドラのレジスタ保存/復帰もディスパッチ同様の修正が必要ですが、それに加えてRETIの処理も追加しないといけません。KC160ではEnd Of Interrupt発生用の特別なレジスタがあったのですが64180ではRETIを使用しないといけません。RETIでは割り込み発生前の箇所に飛んでいってしまうのであらかじめRETIの次のアドレスをスタックにPUSHしておいて戻り先をごまかすようにします。

 あとはタスク等の初期化を作りこめばOS本体は出来あがるのかな…?


MZ用ホストプログラム

 先ほども述べましたが、一時期考えていた「ネットワーク接続機能によるMZとの通信」とか、「MZのプログラムをタスクとみなして通信する」というのはボツにします。まぁデバイスドライバが噛むということでデータをやりとりするタスクにはほとんど同じように見えるのだろうとは思いますがね。MZにはできるだけ単純な機能を載せて、あっさり作れるようにしましょう。

 ホスト側のOSはBASIC…が手軽ですがここは私らしくS-OSを選びましょう。BASICを選ばないのは割り込みを使いたいからというのもあります。BASICでもできるとは思いますがね。
 割り込みを使うにあたってちょっと調べてみました。調べたのはOh!MZに掲載された「共通I/Oポート」とその先につながる「RS-232Cボード」。これらの記事によるとMZ-80K/C/1200ではモード1割り込み、その他はモード2割り込みを使用することとなっています。優音を使うにあたってはMZ-80K/C/1200やX1は関係ないので、あのサイズのボードが装着できるマシンはどれもモード2割り込みが使えそうです。記事ではMZ-2500のことは語られてなかったのですが、元々モード2を使っているのは明白です。
 S-OSではそもそも割り込みを使用していないので、MZ-2500以外の機種ではベクタを設定しモード2割り込みを許可する初期設定があれば使えそうです。MZ-700/1500では8253の割り込みを止めておく必要はあります。MZ-2500ではIOCSを利用する関係上割り込みはすでに使用されているのですが、S-OS"SWORD"のソースを見ると使用されているベクタを書換えてスタックポインタの状態に合わせて飛ばし方を変えるような、そんなパッチが入っています。IOCSで使用されているベクタは5つなのにパッチのエントリは6つあるので、この6つめを使わせてもらうことにしましょう。実際に動作するS-OSで確かめてもここは未使用になっていましたしね。


システムタスク


ユーザータスク

 なんかいいのないっすかぁ。


戻る