SA‐1 RAM protection test - absindx/SNES-TestRoms GitHub Wiki
このテストROMではテスト実行の都合上、I-RAM, BW-RAMはプロテクションにより書き込めなかったり、メモリクリアされてしまいます。
そのため、SA-1側ではメモリを1バイトも使用することができません。
また、SNES CPUとの連携時のパラメータとしてI-RAM, BW-RAMへリクエスト情報を書き込むこともできません。
いくつかのテクニックを用いて制約を緩和しつつ処理を構築しています。
I-RAMが使用できないケースは普通では発生しないため、このテストROM特有の事情です。
他の用途に転用できる機会はあまりないかもしれません。
このテストROMではSA-1がテストの主導権を握り、SNES CPUへテスト情報をIRQ経由で送信しています。
標準のI/Oレジスタでは $2209 SCNT
のメッセージポート4bitしかありません。
これではデータ送信がままなりません。(試験的にこの4bitだけでのやりとりを設計したところ、1バイトの送信に4回のIRQが必要になりました。)
そこで、SNES CPUのIRQベクタルーチンの前に256バイトのNOPスライドを用意しておき、このNOP領域に着地させることでIRQベクタアドレス下位バイトを任意の値にできるようにしました。
このアドレス下位バイトを追加で送信できるメッセージ領域として利用します。
SNES CPUのIRQベクタアドレスはSA-1から CIV
レジスタを通して制御可能です。
$00FFEE NativeIRQL
を読むことで追加メッセージ(IRQ着地点)がわかります。
少しのサイクル数と引き換えに4bitしかなかったメッセージポートを12bitに拡張できました。
本テストROMでの用途
$2209 SCNT
IS-Nmmmm
|| |++++- `CMSG` 送信メッセージの種別 (!Message_XXX)
|| +----- `NVSW` 未使用 (常に0)
|+------- `IVSW` 使用 (常に1)
+-------- `IRQ` IRQしか使用しない (常に1)
$2207-$2208 CIV
HHHHHHHH LLLLLLLL
|||||||| ++++++++- メッセージバイト ($00-$FF の任意値)
++++++++---------- IRQベクタ(NativeIRQ) 上位バイト
%SendSA1Message(messageType, messageValue)
マクロでこのレジスタ書き込みをラッピングしています。
BW-RAM protectionのテスト関係で8bit+1bitを送信するためにメッセージ2つを割り当てているのは、後々考えるともったいなかったですね。
NMIや CMSG
最下位ビットで9bitまとめられたかもしれません。
NOPスライドには通常の NOP ($EA)
命令を使用してもよいですが、最適化として CMP long ($CF)
命令を使用しています。
CIVL=$00
で実行した時のサイクル数(最悪実行時間)
-
NOP ($EA)
: 512 CPU cycle, 3584 Master cycle -
CMP long ($CF)
: 320 CPU cycle, 2560 Master cycle
分岐命令も混ぜればもっと短縮できそうではありますが、256パターンすべての最短パスを構築するのが面倒で妥協しました。
SA-1ではRAMへの書き込みができないため、サブルーチンの戻りアドレスを保存できません。
SNES CPUではDMAレジスタが汎用RAMとして使用できたのですが、SA-1にはそういった都合の良いI/Oレジスタはありません。
幸い、65816ではスタックポインタが16bitであるため、「戻りアドレスが記載されたROMのアドレス」を指すことができます。
呼び出し側で調整をすることで、サブルーチン側は通常通り RTS
で戻ってくることができます。
スタックポインタの指せる領域であることが必須条件のためバンク0固定になってしまいますが、1段だけであればサブルーチン呼び出しが実現できます。
%SoftwareJSR(addr)
マクロでサブルーチン呼び出しをラッピングしています。
なお、この実装はサブルーチン呼び出しのたびにスタックポインタは再設定されるため、呼び出し元ではスタックポインタが破壊可能です。
サブルーチン呼び出しをしない一部テストでは汎用レジスタとして使っています。
ラベル名を引数と結合して生成しています。
macro TestDefineMain(id, name)
!TestDefined_<id> = 1
pushpc
org TestResults+<id>
if <id> >= 100
TestResults_<id>_<name>: skip 1
elseif <id> >= 10
TestResults_0<id>_<name>: skip 1
else
TestResults_00<id>_<name>: skip 1
endif
pullpc
endmacro
macro TestDefine(id)
%TestDefineMain(!<id>, <id>)
endmacro
; 使う側
!TestID_SNES_IRamProtection_Boot = 1
%TestDefine(TestID_SNES_IRamProtection_Boot)
; 以下が定義される
; !TestDefined_1 = 1
; TestResults_001_TestID_SNES_IRamProtection_Boot:
多分アップデートで壊れる。
0埋め分岐もダサい。
そういう機能を用意しないのが悪い。