この記事について
当初、タイトル記載の通り MSXシリーズでは、ユーザプログラムで SP(スタックポインタ)を初期化してはならない と思ったのですが、色々と MSX の闇に触れてしまった結果、最終的に 「SP は 0xF380 に初期化しなければならない」 という結論にたどり着きました。
本書では、その検証・考察を記します。
1. SP 初期化でよく発生する不具合
MSX では動作するけど、MSX2 以降だと動かない という機種依存の問題が、MSX 初期の頃に販売されていた市販ゲームソフトにいくつか存在するようです。
機種依存問題の原因は色々とありますが、以下の記事に書かれているように ユーザプログラムでスタックポインタを初期化してしまっている (その結果、拡張スロット選択レジスタ破壊により動作しない) というパターンが多い印象です。
上記記事より引用:
初期バージョンのMSX版ギャラクシアンは、スタックが拡張スロット選択レジスタを破壊してしまうため、MSX2以降の機種や、内蔵ソフトなどで拡張スロットを使用しているMSX1では動きません。
記事のパッチコード_(方法3 ROM版を作ってしまえ)_では、プログラムの
ld sp, $0000
となっている部分を、
ld sp, $FFFD
という形に修正しています。
実際、これで拡張スロットの書き換えによる想定外の動作は起こらなくなるので、問題なく動くようになるだろうと思われます。
私は MSX の ROM リーダ を持っているので、ソフトさえあれば検証できるのですが、残念ながらソフトを持っていないので未検証です(最近レトロゲームの値段が無駄に高いですし、初期ロットを引けるかは完全にガチャですし...)
ただし、SPを $FFFD に初期化 というのが、実用上は問題ないだろうと思いつつ「MSX のコードとして正しいのか?」という疑問があったので、実際に検証プログラムを組んで調べてみることにしました。
2. スタックの基礎(Z80編)
検証に先立って、Z80 のスタックについておさらいしておきます。
なお、「スタックとは何ぞや」というスタック自体の基礎的な説明については省略します。分からない方は Wikipedia の記事あたりが分かりやすいのでオススメです。
2-1. プリ・デクリメント/ポスト・インクリメント
Z80 のスタックポインタ(SP)は、PUSH / POP を行った時にプリ・デクリメント/ポスト・インクリメントの形で更新されます。
つまり、
- PUSH(スタック積み上げ)
-
SP
をデクリメント (プリ・デクリメント) - メモリの
SP
番地にレジスタ値を書き込む
-
- POP(スタック取り出し)
- メモリの
SP
番地からレジスタに値を読み込む -
SP
をインクリメント (ポスト・インクリメント)
- メモリの
という形で動作します。
拙作の Z80 エミュレータの この辺の実装 を参照
つまり、SP を 0x0000
に初期化すると、PUSH する都度 0xFFFF
→ 0xFFFE
→ 0xFFFD
...という順序で RAM(スタック領域)にデータが積み上げられます。
2-2. メモリレイアウト
ユーザメモリ領域は、
- 変数は、RAM の先頭から順番に割り当てる
- 上から下へ
- スタックは、RAM の末尾から順番に積み上げる
- 下から上へ
というレイアウトで使っていくことで、メモリの利用効率を最適化できます。
例えば、使用できる RAM のアドレスが 0xC000 ~ 0xFFFF
の範囲であれば、
- 変数:
0xC000
,0xC001
,0xC002
... の順番で割り当て - スタック:
0xFFFF
,0xFFFE
,0xFFFD
... の順番で積み上げ
となります。(理想的には)
変数とスタックの領域が重なった状態(スタックオーバーフロー)になると、プログラムが強かにバギーな状態に陥ってしまうので、変数割り当ての際は「スタックが最大どこまで積み上げられる可能性があるか」という点に気をつける必要があります。
2-3. Z80 の SP 初期値
Z80 の データシート には、リセット時の SP の扱いに関しては定義されていないので、CPU起動(リセット)時の SP の値は不定 です。
データシート - PIN Functions - RESET. から引用:
RESET. Reset (input, active Low). RESET initializes the CPU as follows: it resets the interrupt enable flip-flop, clears the Program Counter and registers I and R, and sets the interrupt status to Mode 0. During reset time, the address and data bus enter a high-imped- ance state, and all control output signals enter an inactive state. RESET must be active for a minimum of three full clock cycles before a reset operation is complete.
つまり、原則的にはスタックを使う命令(CALL/RET、割り込み/復帰、PUSH/POP)が実行される前に、プログラムで明示的に SP の初期値を設定 しなければなりません。
ただし、ユーザプログラムでの SP 初期化責務の有無は、マシンの種類(BIOSの有無)によって異なります。
- BIOS が有る Z80 搭載マシン
- PC 系全般(MSX, MSX2, MSX2+, X1, PC-88 ... etc)
- コレコビジョン
- ゲームボーイ
- BIOS が無い Z80 搭載マシン
- SG-1000
- マスターシステム
- ゲームギア
BIOS が有るマシンでプログラミングする場合、プログラマは「SP の初期化責務が BIOS 側にあるのか、ユーザプログラム側にあるのか」をハードウェアマニュアルを見るなどして確認する必要があります。
3. MSX の SP 初期値を検証してみる
MSX のハードウェアマニュアルを持っていないので、BIOS のブート処理が完了して ROM のプログラムが実行された直後の SP の値を表示して検証してみます。
org $4000
.Header
; MSX の ROM ヘッダ (16 bytes)
defb 'A', 'B', $10, $40, $00, $00, $00, $00
defb $00, $00, $00, $00, $00, $00, $00, $00
.Start
; スタート直後の SP を $C000 に退避しておく
ld ($C000), sp
; 画面をクリア
call ClearScreen
; 画面左上に SP の値を表示
ld hl, $1842
call SetVramAddressFromHL
ld hl, TextSpIs
ld b, 4
ld a, ($0007)
ld c, a
otir
ld a, ($C001)
call PrintHexA
ld a, ($C000)
call PrintHexA
.End
jmp End
; レジスタAの値を16進数2桁で画面に表示
.PrintHexA
ld b, a
ld a, ($0007)
ld c, a
ld a, b
srl a
srl a
srl a
srl a
cp $0A
jc TopLess10
add 'A' - $0A
jmp PrintTop
TopLess10:
add '0'
PrintTop:
out (c), a
ld a, b
and $0F
cp $0A
jc BottomLess10
add 'A' - $0A
jmp PrintBottom
BottomLess10:
add '0'
PrintBottom:
out (c), a
ret
; VRAM のアドレスを HL 設定値に書き込みモードで設定
.SetVramAddressFromHL
di
ld a, ($0007)
inc a
ld c, a
ld a, l
out (c), a
ld a, h
or $40
out (c), a
ei
ret
.ClearScreen
ld hl, $1800
call SetVramAddressFromHL
ld a, ($0007)
ld c, a
ld a, $00
ld b, $00
ld d, $03
ClearLoop:
out (c), a
djnz ClearLoop
dec d
jnz ClearLoop
ret
.Data
TextSpIs:
defb "SP:$"
上記プログラムのアセンブル〜実行手順については、こちらの記事を参照してください。
このプログラムを MSX、MSX2、MSX2+、MSX turboR のそれぞれで実行して確認したところ、全ての機種で SP の初期値は 0xF08E になっていました。(下図は MSX での実行結果)
WebMSX のソースコードを確認してみたところ、電源 ON をした時に SP を 0xFFFF に初期化されていたので、0xF08E は BIOS が設定している値 だと断定できます。
F08E は MSX プログラマの間では常識的なことなのかな?と思って「MSX F08E」とかでググってみたのですが、それらしい情報は出てこなかったので、断定しつつも少し自信がなかったりします ^^;
4. 初期化をしない場合の実害
ここまでの調査で、MSX の場合、SP 初期化責務は BIOS 側にあるため、ユーザープログラムでは SP を初期化するのは NG だと判断しましたが、逆に「SP を初期化しないために一部機種で発生する不具合」というものがあるようです。
4-1. ディスクインタフェース ROM による暴走
発生する事象としては、コチラの記事に書かれていますが、ディスク内蔵機で、ディスクインタフェースがスロット0の拡張スロットに置かれている場合、ディスクインタフェースの初期化処理が走ることでフリーエリアが減るため、ディスクドライブの機能を使用しないプログラムは SP を 0xF380 に初期化しなければならないとのことです。
※再現環境を持っていないので当方では内容は未検証です
記事から引用:
2.1.1 ROMカートリッジの暴走
ディスク内蔵機などで、ROMカートリッジが暴走する現象が知られています。これは主に、内蔵のディスクインターフェイスがスロット0の拡張スロットに置かれているYIS-805や、HB-F500で起こります。シフトキーを押しながら立ち上げれば、ちゃんと立ち上がるのですが、はっきりいって不便です。これは、ROMカートリッジよりディスクインターフェイスが先に初期化を済ませてしまい、フリーエリアが減っているにもかかわらず、そのままプログラムを実行してしまうために起こります。これを回避するためには、ROMカートリッジが最初に
LD SP,F380H
としてスタックを初期化すればよいわけです。ただし、これはカートリッジヘッダのINITから直接実行を開始する場合です。また、ディスクドライブなどの機能を全く使わないプログラムの場合だけ、この方法が使えます。このほかの場合にはスタックの場所を変更するなどの方法が必要になります。
同じような注意点が MSX Datapack の方にも記載されています。
MSX Datapackの「3.2.3 スタックポインタの初期化」から引用:
ディスク内蔵のMSXの場合、スロットの位置によってはカートリッジよりもディスク・インターフェイスROMの方が先に初期化動作を行い、 ワークエリア確保のためスタックポインタを下位アドレス方向に下げる ことがあります。このとき、ディスクを使わないソフトウェアはカートリッジに制御が渡ってきてからもう一度スタックポインタを設定し直さないと、スタック領域がなくなり暴走などのトラブルを生じる可能性があります。プログラムの初めにはスタックポインタのイニシャライズを忘れずに行って下さい。
ただし、通常、初期化処理(INIT)ではSP非破壊の制約がある旨が同じドキュメントの 3.1 に明記されているので、上記のディスクインタフェース ROM の挙動(ワークエリア確保のためスタックポインタを下位アドレス方向に下げる)は、そもそも仕様として矛盾している気がします。
MSX Datapackの「3.1 カートリッジヘッダ」の「INIT」から引用:
この2バイトには、カートリッジがワークエリアやI/Oなどの初期化を行うのであれば、その初期化ルーチンのアドレスを書き込み、行わなければ0000Hとしておきます。初期化ルーチンの中でワークエリアの確保など必要な処理を行ったら、「RET」で終了させて下さい。この時 SP以外のレジスタは内容を破壊してもかまいません 。なお、ゲームのようにカートリッジ内でループしていればよいマシン語プログラムの場合は、ここからそのまま目的のプログラムを実行することが可能です。
ROM からのディスクアクセスはそもそも NG なのだろうか...謎が深まります。
4-2. SP初期化できる値
MSX Datapack によると、BIOS の BASIC インタープリタ機能を利用しない場合、0xF380 未満のアドレスはワークエリアとして利用しても良いという記載があります。
3.2.1 カートリッジで使用するワークエリアの確保
他のカートリッジに入っているプログラムといっしょに実行する必要がないプログラム(ゲームカートリッジのようなスタンドアロンのソフトウェア)では、BIOSの使用するワークエリア(F380H)よりアドレスの小さい部分は自由に使用することができます。しかし、BASICインタープリタの機能を利用して実行されるプログラムでは、同じ領域をワークエリアとして使用するわけにはいきません。
4-3. SP 初期化の条件
MSX の ROM プログラムでは、次の条件を全て満たす場合、SP を 0xF380 に初期化することが許容されるものと判断できます。
- 内蔵ディスクを使用しない
- BASIC インタープリタの機能を使用しない
例えば、「BASICインタープリタの機能は利用するが、内蔵ディスクは利用しない」というケースでは、SPを初期化することはできないものの、ワークエリアとして使用可能なメモリ残量が不明なので、まともにプログラムを作ることができない形になる仕様(なんだそれw)と解釈できるので、実質的に MSX の ROM プログラムでは内蔵ディスクと BASIC インタープリタの機能は使用できない ということになります。
32 KB 以上の RAM を搭載した MSX(または標準で 64KB の RAM を搭載している MSX2)であれば、スロット3(0xC000 ~ 0xFFFF)の RAM を全て「システムリザーブ」として触らないようにして、スロット2(0x8000 ~ 0xBFFF)に RAM を割り当てて SP を 0xC000 にして、ユーザプログラムはスロット1(0x4000 ~ 0x7FFF)のみで動作する形にすれば、内蔵ディスク(※ディスクインタフェース ROM 初期化のためユーザプログラムでも INIT から一度 RET する必要あり) or BASIC インタープリタの機能を使えるかもしれない...と思ったのですが未検証です。
5. まとめ
MSX2 以降(※一部の MSX を含む)では、拡張スロットのコントローラとしてメモリ番地 0xFFFF
を使っているため、スタックを 0xFFFF
から使用すると CALL
、PUSH
、割り込みなどが実行された時にスロット構成が切り替わってしまい、ソフトが正常に動作しなくなる不具合が発生します。
MSXの場合、スタックポインタ(SP)の初期化責務は BIOS側 にあり、BIOS は SP の初期値を 0xF08E
に設定しています。
RAM の 0xF08E
(SP の初期値)〜 0xFFFF
までの 3,954 バイト(約4KB)は、システムリザーブを含めた MSX のシステム領域だろうと考えられます。コチラに書かれている MSX シリーズのシステム変数割り当てアドレスの情報 によると、RAM 領域の 0xF30F ~ 0xFFFF
がシステム領域として使われているようです。
ただし、ASCII が発行している MSX Datapack に次のような記述があるので、
3.2.1 カートリッジで使用するワークエリアの確保
他のカートリッジに入っているプログラムといっしょに実行する必要がないプログラム(ゲームカートリッジのようなスタンドアロンのソフトウェア)では、BIOSの使用するワークエリア(F380H)よりアドレスの小さい部分は自由に使用することができます。しかし、BASICインタープリタの機能を利用して実行されるプログラムでは、同じ領域をワークエリアとして使用するわけにはいきません。
次の条件を満たす場合であれば、
- 他のカートリッジに入っているプログラムといっしょに実行する必要がないプログラム(ゲームカートリッジのようなスタンドアロンのソフトウェア)
- BASICインタープリタの機能を利用しない
SP を以下のように初期化することも許容されているようです。
ld sp, $F380
これにより、754 バイト使えるメモリが増えます。
また、内蔵ディスクを搭載している一部の機種では、この他にもシステム(内蔵ディスクインタフェースROM)によりワークエリアが余分に確保される可能性があり、それが何バイト確保されるかという点が仕様として規定されていません。(仕様規定されていない点は MSX の規格自体の不備だと思われる)
仕様規定がされてない以上、ROMプログラム向けに確保されるメモリ残量が予め計算できないため、内蔵ディスクを使用する機能はROMプログラムでは通常は実装できないものと考えられます。
6. 考察
MSX の SP 初期化に関するベストプラクティスは、次のようになるものと考えられます。
-
ROM プログラムでは BASIC インタープリタの機能を使ってはならない
- BASIC インタープリタの機能を使用するには SP 初期化しないようにしなければならない
- SP初期化しないようにすると内蔵ディスクのワークにより不規定サイズの領域が確保される
-
ROM プログラムでは 内蔵ディスクの機能を使ってはならない
- 内蔵ディスクのワークエリアのサイズ保証が無いので、この機能を使うと動作保証できない(特定の機器限定の対応ならできる)
-
ROM プログラムでは SP を 0xF380 に初期化しなければならない
- 消去法でこうなる...
この検証をする前、「多分 1KB ~ 2KB ぐらいシステムリザーブしているんじゃないか?」と思っていた(ので、パッチコードで SP を 0xFFFD に初期化していることに違和感を覚えた)のですが、まさか約 4KB もリザーブされていたとは...少し驚きました。
MSX でプログラミングする時は、RAM の約 4KB がシステムに持っていかれるので、16KB の RAM を搭載していてもユーザプログラムが実際に使える RAM は約 12KB (8KB RAM なら約 4KB!!) しか無いという点に注意する必要がありそうです。
SG-1000 なら 1KB しか RAM が搭載されていないので、4KB あれば「まぁ、余裕かな」と思えるサイズ感だと思います。ただ、C言語とかで組もうとするとやや心許無いかも。C言語で組むなら搭載 RAM サイズは 16KB 以上にスペック引き上げてしまった方が良いかもしれない...などと 16KB の 196,608倍(3GB)の RAM を搭載したスマホを当たり前のように弄りながら考えている様は中々シュール。