全ての道はVDPへ通ず(大嘘)。
まずは問題。
次の二つの内、実行に要する時間が短いのはどちらでしょう?
実行環境はMSXturboR(FS-A1ST/GT)のR800モードです。
(1) 1ステート命令が先 | (2) 2ステート命令が先 |
---|---|
IN A,(C) SUB B ADD A,10H |
IN A,(C) ADD A,10H SUB B |
ちなみに、同じではありません。
ここで基礎知識をいくつか。
MSXturboRのR800は7.16MHzで動作します。この為、CPU外部と入出力を行うときは外部クロック3.58MHzと同期をとる必要があり、タイミングによっては余計な1clockが掛かります。ここでは外部と同期している時のタイミングをT0、同期していない時のタイミングをT1とします。
ちなみにI/O命令がT0のタイミングで実行されるとT1の場合より1wait多く入り、命令終了時には実行タイミングに関わらずT0のタイミングになります。つまり、上記の問題ではIN A,(C)の後の命令は必ずT0のタイミングで実行されます。
次、リフレッシュ。
R800モード時のリフレッシュは、CPUとは非同期に行われます。具体的には、29.3μsec毎にリフレッシュ要求が出され、17clock或いは25clockかけてリフレッシュが行われます。ここで注意しなければならないのは、周期的に行われるのはリフレッシュ要求であって、リフレッシュそのものではないという事です。つまり、何等かの命令を実行している最中にリフレッシュ要求が出された場合、その命令が終了するまでリフレッシュは開始されません。
なお、リフレッシュそのものは外部出力の一種なので、外部と同期を取る必要があります。例えばT1のタイミングでリフレッシュが開始されれば、リフレッシュの前に1wait挿入されます。T0の場合はNo waitです。
最後は簡単な話。
R800でのSUB B・ADD A,10Hの実行時間はそれぞれ1clock・2clockです。
さて謎解き。
上の問題は、実は実行に要する平均時間
の話だったりします。つまり、一方が速い事もあれば、同じ時間で実行される事もありますが、その平均ではやはり一方が速くなるとゆ〜わけです。
このような一種の不確定状況を作り出すのはリフレッシュです。リフレッシュ要求の周期はCPUと無関係ですが、実際にリフレッシュが発生するのは各命令が終わった後。そしてリフレッシュの実行タイミングがT0かT1かでwaitが入る可能性があります。結局、このwaitの発生確率の高い方が平均実行時間が大きいという話になります。
一般にはリフレッシュのタイミングがT0を取る確率もT1を取る確率も1/2ですから、平均実行時間は同じになります。しかし、上のプログラムでは最初にIN A,(C)がある為、その後にある命令の実行タイミングは次のように一意に定まります。
T0 T1 T0 T1 +-------- -----+-------+---------------+ (1) |IN A,(C) |SUB B |ADD A,10H | +-------- -----+-------+-------+-------+ (2) |IN A,(C) |ADD A,10H |SUB B | +-------- -----+---------------+-------+
最初のT0でリフレッシュが発生する確率は双方変わりませんが、それ以降は異なります。(1)では残る2回のタイミングともT1。つまり、SUB BやADD A,10Hの後でリフレッシュが発生すると、いずれの場合でも余計な1waitが発生します。
一方、(2)ではSUB Bの後のみがT1のタイミングとなっており、余計なwaitの発生する確率は(1)よりも低くなります。
とゆ〜わけで、正解は(2)の方が実行時間が短い
でした。
ちなみにVDP関連の測定をしていて気付いたネタでして、S1990VDP.LZHがその測定プログラムです。この測定の第二領域のグラフが階段曲線になるという予測の下、精度向上に躍起になっていた頃、その決定打となりました。で、階段曲線は無事出たわけですが、他の部分のデータ解析がまだ不十分なのでまだ公開出来ません。って事でお開き。
ふる〜い話で申し訳ないんだけど。
OTIRTIME.COMとゆ〜のがある。これは1/60sec割り込みを利用して1/60秒間にOTIRを何ループ実行出来るか測定し、そこから逆にOTIR 1ループに必要な時間をclock単位で調べるとゆ〜なんだかよく分からない代物である。
目的はVDPアクセスに掛るウェイトの測定だったのだが、その過程で、_KANJI3した状態ではOTIRの実行時間が減るように見えるという発見があった。減少分は僅かなものだったのだが、当時は全く説明出来なかったわけで。
しかし、最近になってV9938 MANUALに
ライン数 | 192lines(LN=0) | 212lines(LN=1) | ||||
---|---|---|---|---|---|---|
Interlace | Non-interlace | Interlace | Non-interlace | Interlace | ||
Field | 1st | 2nd | 1st | 2nd | ||
同期信号期間 帰線期間(1) 表示期間 帰線期間(2) |
3 39 192 28 |
3 39 192 28.5 |
3 39.5 192 28 |
3 29 192 18 |
3 29 192 18.5 |
3 29.5 192 18 |
同期期間 | 262 | 262.5 | 262.5 | 262 | 262.5 | 262.5 |
とゆ〜表を見つけた。注目は同期期間で、これは1画面描くのに要する時間、即ち1/60sec割り込みの周期を示している。これがInterlaceとNon-interlaceでは異なるというわけで。
つまり以前は絶対だと思っていた割り込み周期が、実は画面モードによって変化するため、割り込み周期一定と仮定したOTIRTIME.COMではInterlace modeによって値が異なるように見える。具体的には、
Mode | OTIRTIME.COMの出力 |
---|---|
Interlace mode | 23.97clock |
Non-interlace mode | 24.02clock |
は、
Mode | 割り込み周期 |
---|---|
Interlace mode | 262.5 line cycle |
Non-interlace mode | 262.0 line cycle |
によって説明される(筈)。Interlace modeでは262.5 line cycle掛けてOTIRの実行回数が測定されるが、これを262.0 line cycle間に測定されたものとしてOTIRの実行時間を算出するために、実行時間が減少したように見える。ちなみに、計算式はOTIRの実行回数をNとすれば <OTIR>=((262.0*63.69*10-6)*(3.579545*106))/N なわけで。
ところが、Interlace modeではNは本来のNの262.5/262.0倍になっているので、OTIRの見かけ上の実行時間は本来の262.0/262.5倍になる(Nが逆数で効いてくる為)。これを実際の実験結果に適用してみると
24.02*(262.0/262.5)=23.97
となり、この推測とよく一致する。結局、何が言いたいかとゆ〜と、割り込み周期はInterlace modeによって微妙に異なるとゆ〜事で。
あんまり蟲取り日記らしくないけど、サンプル作ってMSX資料室に持ってくのも大変なんで。
ON device GOSUB による割込は以下のワークエリアによって定義される
名前 | Address | 内容 | 対応するトラップ |
---|---|---|---|
TRPTBL | FC4CH | Function key [1] | ON KEY GOSUB |
: | : | ||
FC67H | Function key [10] | ON KEY GOSUB | |
FC6AH | [CTRL] + [STOP] | ON STOP GOSUB | |
FC6DH | Sprite衝突 | ON SPRITE GOSUB | |
FC70H | Space key | ON STRIG GOSUB | |
FC73H | Trig.A [1] | ON STRIG GOSUB | |
FC76H | Trig.A [2] | ON STRIG GOSUB | |
FC79H | Trig.B [1] | ON STRIG GOSUB | |
FC7CH | Trig.B [2] | ON STRIG GOSUB | |
FC7FH | 1/60sec | ON INTERVAL GOSUB | |
FC82H | 予約 [1] | ユーザー定義 | |
: | : | ||
FC91H | 予約 [6] | ユーザー定義 | |
FC94H | システム予約 [1] | ユーザーの使用は禁止 | |
FC97H | システム予約 [2] | ユーザーの使用は禁止 |
ユーザーによる割込の拡張は、予約[1]〜予約[6] に値を書き込むことで行われる
書き込む内容は以下の通りである
TRPTBL+3n+0 |
|
||||||||
---|---|---|---|---|---|---|---|---|---|
+1 | トラップ先の行アドレス(下位) | ||||||||
+2 | トラップ先の行アドレス(上位) |
以上の事を踏まえて、拡張BASIC がサポートすべきコマンドとその内容は次のようになると考えられる
TRPTBLの予約エリア(TRPTBL+n*3+1)に割込行アドレスを設定する
この時、拡張BIOS のブロードキャストコマンド・機能番号1を使ってTRPTBLの空き領域の先頭を調べる
例)
EXTBIO EQU 0FFCAH TRPTBL EQU 0FC4CH LD DE,0001H ;返値 CALL EXTBIO ;A ← 使用されているトラップ数 CP 24 JP NC,ERROR ;全てのトラップが使用されている CP 18 ;最初の拡張BIOSが18を返さないかも知れない JR NC,BRANCH ;Disk BASICやDOS環境ならこの部分は要らない ADD A,18 BRANCH: LD C,A ;A ← A*3 ADD A,A ADD A,C LD E,A LD D,0 LD HL,TRPTBL ADD HL,DE ;HL ← 使用するトラップテーブル
TRPTBLの予約エリア(TRPTBL+n*3+0)の bit1〜bit0 を 01 にする
TRPTBLの予約エリア(TRPTBL+n*3+0)の bit1〜bit0 を 00 にする
TRPTBLの予約エリア(TRPTBL+n*3+0)の bit1 を 1 にする
更に、割込デバイスの初期化(或いは拡張BASICのインストール)の際に 次のフックを設定しなければならない
ブロードキャストの機能番号1に対して、自分自身が使用するトラップ数だけ A reg. を増やして返す。
割込デバイスのチェックをし、デバイスが目的の値を返しているなら HL にTRPTBL(FC7FH〜FC97Hのいずれか)を入れて SETFLG(後述) を呼ぶ
例)
GTSTCK EQU 00D5H XOR A CALL GTSTCK ;カーソルの状態を読む LD HL,0FC82H ;予約 [1] OR A CALL NZ,SETFLG ;カーソルが押されていれば割込要求を発生
ONGSBF EQU 0FBD8H ;広域イベントフラグ SETFLG: LD A,(HL) ;device OFF なら何もしない RRCA RET NC LD A,(HL) ;既に割込要求中なら何もしない OR 100B CP (HL) RET Z LD (HL),A XOR 101B ;device STOP かつ割込要求中なら何もしない RET NZ LD A,(ONGSBF) ;システム処理すべきイベントを一つ増やす INC A LD (ONGSBF),A RET
割込要求を受けて、システムの割込ルーチンはイベント処理ルーチンを起動する
これにより TRPTBL に設定された行へ実行が移る
STATE WARS ていぶるの逆襲。
HL=0 | → | HL=256 |
HL=1〜255 | → | HL=1〜255 |
という変換をいかに簡単にやるかというのがあって、その時の結論はテーブルを使うよりもレジスタ演算のみでコーディングする方が良いというものだった。
しかし、今見るとテーブルの引き方が悪い。どうしょうもなく悪い。
ADD HL,HL LD A,(HL) INC HL LD H,(HL) LD L,A
てな感じで、要は0000H〜01FFHに変換テーブルを置くというものだったのだが、愚かにも垂直型でテーブルを作っている。これを水平型にするだけで、
LD A,(HL) INC H LD H,(HL) LD L,A
といった具合に1byte・14clockの節約になる。
#垂直型・水平型というのは私の勝手な呼び方。
#MSX2とPC-9801のVRAMをそれぞれ垂直型・水平型と呼ぶのを真似してるだけとゆ〜。
だがしかしである。よくよく考えればこの変換は L reg.を変化させないから、2bytesのテーブルを引く必要性はない。H reg.のみをテーブル引きすればよいのである。すなわち、
LD H,(HL)
これで前回の結論
DEC L INC HL
より速く短くなった。更にテーブルの大きさも半分になって、正に良い事ずくめ。力技は全てに勝るんである。Z80みたいに遅いCPUでは。
#メモリアクセスが相対的に重くなるR800だとやっぱり前回のが速い。
もっとも、システムのジャンプテーブルが密集している0000H〜00FFHを使うのは、かなり抵抗を感じるところではあるが、そんな物は保存しておいて後で書き戻せばいいんでないのという声が聞こえる今日このごろ。
なお、一個目の512bytes垂直型テーブルは0000Hに配置された
DW 0100H ;0000H [L][H] DW 0001H ;0002H DW 0002H ;0004H : DW 00FFH ;01FEH
という連続した2bytesにデータを置くテーブル。
二個目の512bytes水平型テーブルは
DB 000H ;0000H [L] DB 001H ;0001H DB 002H ;0002H : DB 0FFH ;00FFH DB 001H ;0100H [H] DB 000H ;0101H DB 000H ;0102H : DB 000H ;01FFH
という100H離れた位置に一組のデータを置く方式。
三個目は、水平型の後半256bytesだけを0000H〜00FFHに置くという物。
おせっかいとは思うけど、念のため。
PHC-70FDの謎。
とゆ〜か、PHC-35/70FD/70FD2(要するにSANYO製MSX2+)といったべーしっ君
内蔵マシンの話です。
一部で有名な話に、これらのマシンではいくつかのソフトが暴走するってのがあります。んで、これは長いこと原因不明とされてきたわけですが、大阪での解析データや拡張BASICの再拡張(MSX資料室参照)の経験から暴走の要因が特定出来たような気がします。
まずはCALL文拡張の仕組みから。
CALL文が実行されると、BASICはCALL文の拡張されているスロットに対して次々に制御を渡して行きます。このとき使われるのはインタースロットコールで、例えばPage.1のRAMに拡張BASICがあれば一時的にPage.1がROMからRAMに切り替わるわけです。
ちなみに拡張BASIC側では、渡されたコマンド名が自分のものであるか判定し、自分のものならコマンドに応じたプログラムを実行し、そうでなければ何もしなかった事を示すフラグを立ててBASICに制御を戻します。
で、PHC-70FDの話。
PHC-70FDではべーしっ君
がSlot#3-3のPage.2に置かれています。そして、CALL BCというコマンドを検出するとべーしっ君
本体をPage.1のRAMに転送し、同時にCALL TURBO ON/OFFといったコマンドを拡張します。
さてここで問題。
スタックがPage.2にあるとき、CALL BCとするとどうなるでしょう?
答え:暴走する。
つまりPage.2のスロットがべーしっ君
切り替わった瞬間、スタックが消失しリターンアドレスが失われます。レジスタをスタックに保存するといった事も出来ません。当然、制御がBASICインタプリタへ戻る時か、或いはその前に暴走します。
スタックの位置はCLEAR文の第二パラメータで指定します。だからCLEAR 200,&HC000:CALL BCとかやれば即暴走です。
しかし、普通はCALL BCは使いません。そんな物が存在することを前提に組まれたソフトなんて、そうは無いでしょう。では何故暴走するのか?答えは存在しないコマンドにあります。
CLEAR 200,&HC000:CALL ZZZとした時の事を考えてみましょう。CALL ZZZという拡張コマンドを持ったスロットが無ければ、普通はSyntax errorが出て終了するはずです(どのスロットも該当コマンドを持たなかったという事)。しかしPHC-70FDの場合はエラーが出る前、べーしっ君
のスロットに制御を渡した瞬間に暴走してくれるわけです。
とゆ〜事で結論。
拡張BASICが既にインストールされているかどうか調べるのに、拡張BASIC自身のコマンドを試し実行するのは止めましょう。インストールされていればいいのですが、インストールされていなかった場合は存在しないコマンドと見なされてPHC-35/70FD/70FD2/で暴走する危険があります。
防止策は、他の方法でインストールチェックをするか、スタックをPage.3に置くかする事です。ちゅ〜いしませう
おぉ、今回は有用かも新米。
役にたたない常駐ネタ。
ここに書く事というのは、あまり知られていない技術的な小ネタで、かつ物の役にはたたない事柄である。そういう意味でリフィルくん
フォントの解析なんていうのは条件にぴったりだったのだが、残念ながらMSX資料室の方に上げてしまった。そういう理由で、今回は常駐の話...。
常駐物を作る時に一番頭を悩ませるのが、プログラムを何処に置くか、という問題である。通常はTPAを減らして常駐し、そこからインターセグメントコールを行ったりするわけだが、中にはTPAを減らさずに常駐する方法もある。
一つは使われていないシステムワークに居座る方法で、プログラムを絶対アドレスで書ける為、気楽に出来る。ただ、他のプログラムと衝突する可能性も考えないと、恐ろしい事になるが。
二つ目は外部マッパ専用になるが、Page0に常駐するなら外部マッパのSeg.3に、Page1ならSeg.2に、Page2ならSeg.1に常駐ルーチン全体を書き込む方法である。これは通常のMain RAMの割当がPage0からSeg.3,2,1,0となっている事、マッパ選択レジスタが一本しかないために外部マッパも通常は同じセグメント配置になっている事を利用しており外部マッパをインタースロットコールで呼び出す事が出来る。したがって、フックから呼ぶ事が可能になるという技である。ただし該当ページのセグメントを切替えるプログラムがあるといきなり飛ぶ可能性があるので、普通は恐くて使えない。
三つ目は、turboRのDRAMモード専用で、ROM化しているDRAMの空き部分にプライマリマッパへのインターセグメントコールを書き込み、フック等からはインタースロットコールでMain ROMの該当部分(インターセグメントコールの先頭)を呼び出すという物である。インターセグメントコールには7bytes必要なのでMain ROMにそれ以上の空きを見つける必要があるが、半角フォントテーブルの7FHでも使えば8bytes確保出来るので問題無いだろう。
....長いけど、やっぱり役にたたない。
VDPアクセスに掛かるウェイトの話。
Z80ではOTIR一回当たりの実行ステート数は21ステートである。しかしMSXの場合、M1サイクルのウェイトが加わるため、23ステートで実行される。しかし、VDPアクセスの場合更なるウェイトがI/Oサイクルで掛かってくる機種がある。
例えば、FS-A1WXではI/Oサイクルで1ウェイト掛かるため、VDPに対するOTIRの実行には1byte当たり24ステートを要する。現在、FS-A1WX/FX/WSX、PHC-70FD/FD2のMSX2+、そしてFS-A1ST/GTのZ80モードで1ウェイト掛かることが確認されている。
ソフトウェアでタイミングを取っているプログラムは、注意すべきかも知れない。
カラーコード0に関する豆情報。
カラーコード0(以下P#0)というのは、通常透明色として扱われる。非透明色にするにはTP(R#8/bit5)を1にすれば良いのだが、SCREEN 0/10/11(TEXT1・TEXT2・RGB/YJK混在)ではTPに関係なく非透明色として扱われる(らしい)。
で、これだけなら何という事もないのだが、問題はTEXT2でブリンク機能を使った時に起こる。何故かP#0をブリンク色(R#12の上位4bit)に使うと、ブリンク色が下位4bitの非ブリンク色と同じになってしまうのである(つまり一種の透明色扱いされている)。
という訳で、COLOR 15,0,0としてZero Typeを使った場合、正常な動作は不可能である。/CB000といったオプションを付ける以外、今のところ方法が無い。困ったものである。