FENIX Ver0.2 Copyright (C) 1991-1998 S.Uchida
fsh Ver0.2 Copyright (C) 1991-1998 S.Uchida
従来のBASIC上では困難だったマシン語による処理ができるわけですが、 いささか今までの環境とは異なるためにプログラムする際にとまどうことがあります。
そこで、本マニュアルではFENIXでアプリケーションを作ってみたいという人のために、各種のノウハウについて書いてみたいと思います。
Programmer's manual 及び System call manual を読んでからお読み下さい。
4000H 〜 FFFFH で動き、RET で終了するプログラムを作ればいいのです。 ただ、FENIXにおいてはメモリブロックの考えが強くなりますから、その点には注意して下さい。
例えば 5FFFH 〜 6000H の2バイトから成るプログラムの場合、これは二つのメモリブロックにまたがっていますから、なんとオブジェクトとしては16KBのメモリを消費してしまいます。
こんなプログラムを作る人はいないでしょうが、FENIXで重要なことは、複数のプログラムがオンメモリで幾つも存在できるということです。
メモリは各プログラムの共有資源だということを忘れずに、無駄なメモリを使わないように心がけましょう。
しかし、どんな小さなプログラムでも1ブロック、8KBも消費します。 と、すると下手に小さなツールを作るのもメモリの無駄ということになります。
ですから、何かツールを作ったら、8KBを使い切るまではできるだけ豪華な機能をつけてやりましょう。(笑)
では、どのようにメモリを使えばいいのでしょうか?
答:FXC .GETMB を使う。
…これでは答になっていませんね。 問題は、確保したメモリをどのように使っていくかというところにあります。
これには2つの方法があって、ひとつはマッピングして使う方法、もうひとつは統一アクセスで使う方法です。
統一アクセスは後で扱うことにして、ここではマッピングして使う方法をやってみましょう。
マッピングは、スワップが生じる関係上必ず FXC を使わなければなりません。 FXC .MAPMEM もしくは FXC .MAPMB1 ですね。
しかし .MAPMB1 の方は1ブロックずつマッピングするので使いやすいけれど、.MAPMEM は6ブロックを一度にマッピングしてしまうので使いにくいと思う人もいるのではないでしょうか。
6ブロックを一度に変更するとプログラムの部分のマッピングが変更される危険があるので、プログラム部分がどのLMBにあるのかいちいち調べておかないと .MAPMEM は使えないので確かに面倒臭いです。
ですが心配無用。 6バイトのマッピングデータを用意する必要は全くなく、必要な部分のデータは既に設定済みである6バイトのデータがカーネル内に存在しています。
オブジェクトは起動時に A レジスタにオブジェクトIDが入っていますから、起動時にそのまま
FXC .OBJTAD PUSH IX POP HL LD DE,26 ADD HL,DE LD (MAPAD),HLとでもして下さい。 何をしているかと言うと、オブジェクト管理テーブルから自分自身の情報を取り出し、そのマッピングデータの位置を求めているのです。
管理テーブル内のマッピングデータの情報は、オブジェクトを起動する際の初期マッピングデータとして使用されますから、プログラムがあるブロックのマッピングのデータは既に設定済みです。 ですからこのデータを用いて変更部分だけを書き換えれば、プログラムのあるブロックには影響を与えずにマッピングを行うことが可能です。
例えば
LD IX,(MAPAD) LD (IX+4),3 ← C000H 〜 DFFFH にLMB3 LD (IX+5),4 ← E000H 〜 FFFFH にLMB4 LD HL,(MAPAD) FXC .MAPMEMとでもするだけです。簡単でしょう?
ちなみに細かい話をしますと、複数のブロックをマッピングする場合は .MAPMB1 は .MAPMEM よりも遅いです。
さっきと同じ答ですね。しかし今度は勝手が違います。 どのマッピングモードを使うか、ということが大事なのです。
.GETMB を呼び出すときにどのモードかを指定するようになっていますが、この際の選択は以下のように行ってください。
プログラムを載せて走らせる ・・・ モード0
マッピングして使うデータ ・・・ モード1/2
統一アクセスでしか使わない ・・・ モード3
間違ってもただのデータを入れるためのメモリをモード0で確保し、そればかりかそれをスワップ不可にする、などということはやめましょう。 他のオブジェクトに対して迷惑です。
例えば fsh の ls では、
ls [-ahF1Cls]と多彩なオプション指定が可能です。 しかもこれは、
ls -a -h -Fと書いても
ls -ahFと書いても同じ動作をします。
いつの日か(ぉぃぉぃ)UNIXのcshなどに見られるエイリアス(別名置換)機能を使えるようにする予定です。 これをうまく活用するためには、コマンド側で上のような柔軟なオプション処理を行えるようになっているのが望ましいのです。
ひとつのオプションが複数の文字から成っている、という場合もあるでしょうが、まぁそれはそれ。オプションが一文字の場合は柔軟なものにしてやりましょう。
それではどうやってこんなオプション処理を行うか? 論より証拠。ls のオプション処理部分のソースを抜き出してみましょう。
LSOPLP: LD A,(DE) OR A JR Z,LSNODIR CP '-' JR NZ,LSFN INC DE LSOPLS1: LD A,(DE) INC DE OR A JR Z,LSOPLP CALL LSOPTS ; interpret option JR LSOPLS1 : : LSOPTS: CP 'a' ; check option JR NZ,LSOF LD A,(INWORK+1) OR 1 LD (INWORK+1),A RET LSOF: CP 'F' JR NZ,LSOH LD A,(INWORK+1) OR 2 LD (INWORK+1),A RET LSOH: CP 'h' JR NZ,LSOC LD A,(INWORK+1) OR 4 LD (INWORK+1),A RET : :コマンドが起動されたとき、(DE)〜にコマンド名や引数のデータが入っています。 この処理では、まず引数の最初が "-" であるかどうか調べています。
"-" であれば、その引数はオプション指定であると見なし、以降の各文字をオプションとしてチェックしていきます。 文字がオプションであればワークエリアのフラグを立てます。 "-" でなければそれはファイル名として、実際の処理に移ります。
これはオプション処理のひとつのパターンを示しています。 実際、fsh 中の複数のオプションを許すコマンドは全てこのパターンで作っています。 このパターンを基本にして、ぜひ複数オプション処理を極めましょう。
しかし、Programmer's manual でも書きましたが、実際にはワイルドカードは常に実際のファイル名に変換されるわけではありません。
そのワイルドカードにマッチするファイルがひとつしかない場合のみ変換が行われます。 そこで、コマンドラインからワイルドカードが指定され、しかもそれがシェルによって変換されなかった場合は、アプリケーション側で対処してやる必要があります。
もっとも、ワイルドカードが指定されたということは複数のファイルを指定した、ということですから、そのコマンドが複数のファイル名を受け付けないというならエラーと見なせばいいでしょう。
しかし複数のファイルを指定可能な場合、その指定の中でワイルドカードを用いていても正しく動いてほしいものです。
例えば fsh の chmod は、
chmod {+|-} filename・・・という書式です。これは例えば
chmod - a*とすると a で始まる名前の全てのファイルのプロテクトを解除します。
chmod + abc def ghiとすると abc,def,ghi の3つのファイルをプロテクトしますし、
chmod + a* def g*などと指定することもできます。
このような指定を許すのは一見ややこしそうな処理に見えますが、実は一定の形式にしてしまえば簡単になります。
以下は、chmod のワイルドカード処理部分です。
CHMLP0: LD A,C LD (CHMA),A CHMLP: PUSH BC ; 引数処理ループ CALL NXTWORD PUSH DE XOR A FXC .DIRLIST OR A JR NZ,CHMLP1 CALL CHMERR2 JR CHMLPE CHMLP1: CP 1 JR NZ,CHMWILD ; multi file --- wild card CALL CHMSUB ; chmod 1 file CHMLPE: POP DE POP BC DJNZ CHMLP RET ; ワイルドカード CHMWILD: LD B,A ; B...number of matched file LD C,0 CHMWLP: PUSH BC PUSH DE LD A,C FXC .SETWILD CALL CHMSUB POP DE POP BC INC C DJNZ CHMWLP JR CHMLPE : :その他、ワイルドカードを許すコマンドは、全部同じ形式を取っています。
簡単に説明すると、引数がひとつのファイル名を表しているなら直接処理ルーチンを呼び、ワイルドカードならマッチするファイル名を順次生成し、処理ルーチンを呼んでいる、というだけです。
このパターンはワイルドカード処理の基本となりますので、これを元にしてワイルドカード処理も極めましょう。
ユーザーが load したものは実行した後もオブジェクトとして残るわけですが、直接ファイルから起動すると実行後には消されてしまいます。
しかしアプリケーションによっては、勝手に消去されると困るということがあります。 主に後述のTSRの場合なのですが、こういう場合に基本となるのが常駐オブジェクトの考え方です。
作り方は簡単です。
LD A,自分のオブジェクトID FXC .OBJTAD SET 1,(IX+18)とすればできあがりです。
ここでのビットセットは自分自身の書き込み許可を取り消してしています。 これだけで、ファイルから起動した場合でもシェルに消されずに済みます。 ファイルから起動した場合は、シェルは実行終了後にそのオブジェクトを消そうとしますが、書き込み許可がないと消せませんのでそのままにするからです。
ではこれで終わりです。 と言いたいところですが、よく考えるとこのオブジェクトを消すにはどうしたらいいんだ、という問題にぶつかります。
これは、勝手に常駐するわけですから、そのオブジェクト側で勝手に消えてもらうのが妥当なところでしょう。 従って、常駐を解除するオプションなどを用意してオブジェクトが自分自身を消去するようにしましょう。 それは以下のような処理になります。
LD A,自分のオブジェクトID FXC .OBJTAD RES 1,(IX+18) FXC .KILLOBJ RET自分自身の書き込み許可を戻し、自分自身を殺して RET しています。
この、FXC .KILLOBJ と RET の間が、ゾンビオブジェクトという危険な状態です。 オブジェクトとしては死んでいるのに、プログラムとしては動いています。 不測の事態を避けるため、この例のように自分を殺したらすぐさま元のシステムに戻るようにして下さい。
しかしFENIXにはプロセスの概念がなく、常に常駐するオブジェクトの概念しかありません。 ですから、本当はTSRと呼ぶのは間違っているのかもしれません。 しかし、一般にTSRと言うと、常駐してシステムに作用するものを指すようですので、ここではそういうものをTSRという呼び方をします。
ではFENIXにおけるTSRとはどんなものでしょう?
これは割り込みを用いたもので、大きく分けて以下の3種類があります。
結局オブジェクトを呼び出しているのですが、FXC .EXECOBJ と異なる点は、
では実際にどのように使うか、について説明しましょう。
まず、ソフトウェア割り込みを登録するオブジェクトが必要です。 そしてその機能を利用するオブジェクトが必要です。 今、仮に前者をオブジェクトA、後者をオブジェクトBとします。
Aの方はまずソフトウェア割り込みを登録します。 これには FXC .TSRON を使用します。
LD A,8 ←ソフト割り込みはタイプ8 LD HL,ADDRESS ←割り込みで呼び出されるルーチンのアドレス FXC .TSRONこれで登録終了です。
この登録で何が行われるかといいますと、
さて、今度はこれをBから使ってみましょう。 まず、AのオブジェクトIDが分かっていればいいのですが、普通は分かりません。 しかしAの名前さえ分かっていればIDを取得することはできます。
LD DE,A_NAME FXC .CHKOBJ : : A_NAME: DEFB 'A',00Hといった具合です。FXC .CHKOBJ でIDが分かれば、次に
LD C,A ←オブジェクトID LD B,0 ←Aが持っている番号0の割り込み LD A,8 ←ソフト割り込みはタイプ8 FXC .TSRENTこれで HL に呼び出しアドレスが返ってきます。 このアドレスをコールすれば、Aが登録した機能を使うことができます。
そこで、このアドレスはどこにあるのか、マッピングを変更すると暴走の危険があるのではないか、と気にする方がいらっしゃるかもしれませんが、心配無用です。
TSR処理ルーチンは、全てコモンエリア内のスタック領域にあります。 ここならマッピングが変更される恐れはありません。 スタック領域は2KBもありますので、少々使っても大丈夫です。
そこでこの呼び出しの際ですが、まず、呼び出された時のレジスタの値を全て保存したまま実際の処理が呼び出されます。 そして、実際の処理が終了した時のレジスタの値を全て保存したままもとのルーチンに帰ります。 従って入出力パラメータにレジスタを自由に割り当てることができます。
ではこれで説明を終わり…おっとっと。 ソフト割り込みを登録したAは、もはや書き込み許可を与えても消去することはできません。 このままではオブジェクトが増える一方ですね。
じゃあ登録を取り消してみましょう。Aから
LD A,8 ←ソフト割り込みはタイプ8 LD B,0 ←Aが持っている番号0の割り込み FXC .TSROFFこれで登録が解除されます。 しかし書き込み許可は復活しませんので、自分で復活させます。 常駐オブジェクトの説明で行った通りです。
さっそく使ってみましょう。 今度はオブジェクトをひとつしか使いませんので楽です。
まずは登録。
LD A,9 ←タイマ割り込みはタイプ9 LD HL,ADDRESS ←割り込みで呼び出されるルーチンのアドレス FXC .TSRONソフト割り込みと変わりませんね。これで、
HL ... 呼び出しアドレスが返ってきます。 しかし HL の値は全く意味がありません。 呼び出すためのアドレス設定は既に済ませてあるからです。
DE ... コモンエリアのタイマ管理テーブルのアドレス
ここで重要なのは DE です。 このままま放っておいても、全く処理ルーチンは呼び出されません。 タイマテーブルを設定してやらなければならないのです。
コモンエリアのタイマテーブルは以下のような構成になっています。(計8バイト)
+4〜+7 は書き換える必要はないのですが、問題は +0〜+3 です。 最初に起動するときに設定しなければならないのはもちろんのこと、なんとこの部分のデータは処理を起動する度に0になってしまい、再設定しないとその処理ルーチンを起動することはできなくなってしまうのです。
+0 1でテーブル有効 +1〜+3 呼び出しインターバル。0.1秒ごとに1加えられ、0になると処理ルーチンが呼び出される。+1 が下位バイト。 +4〜+7 呼び出すルーチンのマッピングとアドレスのデータ。.TSRON で既に設定済み。
ですから、処理部分では
以下のような感じですね。
ADDRESS:CALL 実際の処理 LD HL,(TABLE_ADDRESS) ←.TSRON で DE に渡された値 LD (HL),1 ←テーブルを有効にする INC HL LD (HL),0FBH ←インターバル再設定 INC HL この場合は 0FFFFFBH = 0.5秒 LD (HL),0FFH INC HL LD (HL),0FFH RET従来のタイマ管理では1ブロックのマッピングしか行えませんでしたが、FENIXでは6ブロック全てマッピングが可能です。
登録解除はソフト割り込みとほぼ同じです。 なお、この割り込みの処理ルーチン側で、レジスタを保存する必要はありません。 また、SVC・FNC・FXCは基本的に使えないはずです。(暴走の危険あり)
しかし、多くのハードウェア割り込みは既にIOCSで利用されていて、うかつに使うとシステムが暴走する危険があります。 従って、注意して使う必要があります。
登録する際に注意すべきことは、.TSRON は内部テーブルに登録するだけで実際に割り込みのための設定などは行わないということです。 .TSROFF も同じく、割り込みを解除するような処理は行いません。 従って、アプリケーション側で割り込みの設定・解除を行う必要があるのです。
そして、もうひとつ注意すべきなのはレジスタです。 ハードウェア割り込みはその性質上全てのレジスタを保存する必要がありますが、呼び出しルーチン側では AF,BC,HL しか保存しません。 従ってそれ以外のレジスタを使う場合は、保存しておいて終了時に復帰させる必要があります。
更に言うと、SVC・FNC・FXCも使えません。(暴走の危険あり)
ハードウェア割り込みは割り込み源によって6種類のものがサポートされています。 順に使い方を説明していきましょう。
登録は例によって
LD A,10 ←PIO割り込みはタイプ10 LD HL,PTR_ADDRESS ←割り込みルーチンアドレス格納位置へのポインタ FXC .TSRONあれ?何か違いますね。 実はPIOはAポートとBポートの2種類の割り込み源から割り込みをかけることができるので、割り込みルーチンのアドレスが2つ必要なのです。 つまり
(HL),(HL+1) .... Aポート割り込み時の処理アドレス (HL+2),(HL+3) .... Bポート割り込み時の処理アドレスと、ワークエリアをセットしておいてから登録しないといけないのです。
すると HL と DE に処理アドレスへのポインタが入って返されますが、PIOの場合は HL=DE です。
(HL),(HL+1) .... Aポート割り込み時に呼び出されるアドレスとなっていますので、実際にPIOをプログラムして割り込みをセットする場合は、Aポートのベクタを HL に、Bポートのベクタを HL+2 に合わせて下さい。
(HL+2),(HL+3) .... Bポート割り込み時に呼び出されるアドレス
登録は、PIOと同じく
LD A,11 ←SIO割り込みはタイプ11 LD HL,PTR_ADDRESS ←割り込みルーチンアドレス格納位置へのポインタ FXC .TSRONとなります。
これで HL に呼び出しアドレステーブルへのポインタ、DE にIOCSが使っている割り込みベクタの格納アドレスが入って返されます。
具体的な設定方法を各モードについて説明します。
DI LD (ORIGADD),DE ←元の処理ルーチンアドレスの格納位置を保存 LD A,(DE) ←元の処理ルーチンのアドレスを保存 LD (ORIG),A INC DE LD A,(DE) LD (ORIG+1),A DEC DE LD C,(HL) ←処理ルーチンアドレスを読み出す INC HL LD B,(HL) LD (DE),C ←アドレスを登録 INC DE LD (DE),B EIといった具合にすればいいでしょう。 TSR管理テーブルにはステータス・アフェクツ・ベクトル用に8個のテーブルが用意されていますが、その最初のひとつを流用しているわけです。
(HL),(HL+1) .... ch.B トランスミッタバッファエンプティ処理アドレス (HL+2),(HL+3) .... ch.B 外部/ステータス割り込み処理アドレス : 中略 : (HL+14),(HL+15).... ch.A 特殊受信状態処理アドレスと、16バイトのワークエリアを設定してから .TSRON を呼ばなければなりません。
すると (HL)〜(HL+15) に各割り込み要因に対応した呼び出し先が登録されているテーブルが返されますので、割り込みベクトルを HL の示すアドレスに合わせます。 ちなみに HL=2D0H ですから、Iレジスタを気にする必要はありません。
それではいつ起動するのか、というと、スムーススクロールをする時です。 つまりスムーススクロールを実行する時だけ割り込み処理を起動するようにしてあって、スクロールが終了すると割り込みを解除してしまうのです。
ここが少々やっかいなところです。 割り込みの起動・解除までIOCSで管理されていては手が出ません。 スムーススクロールさえ生じなければIOCSは関与せず大丈夫なので、FENIXでこの割り込みを使用する場合はスムーススクロールモードでない場合に限る、ということになります。
それでも使いたいという場合は、以下のように使って下さい。
LD A,12 ←VBLANK割り込みはタイプ12 LD HL,ADDRESS ←割り込みで呼び出されるルーチンのアドレス FXC .TSRONこれで HL に呼び出しアドレス、DE に元々IOCSが使っていた割り込みベクタの格納アドレスが入って返されます。
DI LD (ORIGADD),DE ←元の処理ルーチンアドレスの格納位置を保存 LD A,(DE) ←元の処理ルーチンのアドレスを保存 LD (ORIG),A INC DE LD A,(DE) LD (ORIG+1),A DEC DE LD A,L ←割り込みベクタに新しいアドレスを設定 LD (DE),A INC DE LD A,H LD (DE),A LD A,(61EH) ←割り込み許可 OR 8 LD (61EH),A OUT (0C6H),A EI : :こんな具合に設定して下さい。 他の割り込みと異なり、割り込みが有効になるようにI/Oコントローラの設定をしなければなりません。
この点を除けば以降の3つの割り込みと大した違いはありません。 61EH は割り込みベクタマスクフラグを保存しているワークエリアです。 この値を用いてアクセスすれば、現在の割り込み設定を乱す恐れはありません。
解放時には逆のことを行わなければなりません。 つまり
DI LD A,(61EH) ←割り込み禁止 AND 7 LD (61EH),A OUT (0C6H),A LD DE,(ORIGADD) ←元の処理ルーチンのアドレスを復帰 LD A,(ORIG) LD (DE),A INC DE LD A,(ORIG+1) LD (DE),A EI : :といった具合です。
この割り込みを止めてしまうとシステムがハングアップしますので、これを使う場合は細心の注意が必要です。
登録は例によって
LD A,13 ←8253割り込みはタイプ13 LD HL,ADDRESS ←割り込みで呼び出されるルーチンのアドレス FXC .TSRONとなります。 ここで HL に呼び出しアドレス、DE にIOCSが使っていた割り込みベクタの格納アドレスが入って返されます。 設定は以下のように行って下さい。
DI LD A,(DE) ←元の処理ルーチンのアドレスをセット LD (ORIG+1),A INC DE LD A,(DE) LD (ORIG+2),A DEC DE LD A,L ←割り込みベクタに新しいアドレスを設定 LD (DE),A INC DE LD A,H LD (DE),A EI : : ADDRESS:CALL 実際の処理 ORIG: JP 0000 ←元の処理ルーチンへジャンプ
元の処理ルーチンを呼ばないとシステムがハングアップしてしまいます。 また、この割り込みはただでさえ多数の処理を行っていてかなり負担がかかるので、時間のかかる処理をさせたりしないで下さい。
登録時のタイプは14です。
登録時のタイプは15です。
登録時のタイプは16です。
作者としては「ないよりはマシだ」という程度にしか考えていませんので、別にFENIXだから統一アクセスしないといけない、ということはありません。 しかし簡単にプログラムを作れますので、さほど速度を要求されないところでは結構使いやすいと思います。
アクセスは以下のように行います。
全体としては、
指定→オープン→読み書き→クローズ
という流れになります。
確かに直接マッピングしてアクセスする方が速いのですが、統一アクセスにもそれなりの利点はあります。
プロセスのようでもありファイルのようでもある。 これがオブジェクトの面白いところです。 私は「ファイル以上プロセス未満」と呼んでいます。
こんな「オブジェクト」を使って、何か面白いことやってみませんか?
まず、当たり前ですが存在しないブロックにアクセスしようとしないこと。 そして、.GETMB で獲得したメモリにアクセスしようとする場合は自分のサイズを必ず変更しておいて下さい。
統一アクセスは、オブジェクト管理テーブルに記録されているサイズに関する情報を参照してアクセス可能な最終ブロックを決定しています。 .GETMB はサイズの変更など行いませんから、実際はアクセスできる部分は増えていてもアクセスしようとすると拒否されます。 (正確に言うと、一回アクセスするとEOFが返ってくる)
致命的なエラーになるということはありませんが、一貫性がとれていないとバグの原因にもなりかねませんのでサイズは常に修正して下さい。
アプリから変数の値を参照するとき注意しておかなければならないのは、変数には4通りの状態があるということです。それは、
LD A,0 LD C,FSH_VAR FXC .SHELLなどとすればできますが、ここでは A=0 にしない方がいいです。 なぜかと言いますと、このやり方だと上記の2〜4全ての場合にあてはまるため、2の状態だった場合、値が空ですから、(DE) には NULL(00) が入っていることになります。
そこで、値を読むときにいちいち (DE)=0 かどうか確かめなければなりません。 値は不要と言うならそれでもいいのですが、値が必要な場合はもっと賢いやり方があります。先の例で、
LD A,1にして呼び出すのです。 こうすると、値が定義されていない場合はエラーになります(指定ワードが存在しない)ので、(DE)=0 の確認が不要になるというわけです。
逆に、変数が定義されているかどうかだけ知りたい場合は最初のやり方の方がいいというわけです。
これから充実させていきたいと思いますので、質問、アイデアなどがありましたら聞かせて下さい。