Gameboy Library


img
ゲームボーイでテキストリーダーを作ってみました。

複数文書を格納でき、セーブやページジャンプ機能もあります。
PDA 並とはいきませんが、ゲームボーイでもフリーの電子テキストなどが利用できます。

漢字表示の実装、GDMA による全画面の一括転送などの習作として作ったものですが、
割とまともなアプリケーションになったのではないかと思います。
できればバックライト付きの GBASP 以上での試用をお勧めします。




■ GB文庫とテキストコンバーター

・GBLibMaker と GBLibrary gblibmaker.zip (2008.01.04版)
 .NET Framework 2.0 以上が必要です。Windows Update で入手できます。
 GBLibrary はGameboy Color 専用(白黒不可)です。GDMA等の拡張機能を使用するためです。

img

GBLibMaker はテキスト文書を GBテキストリーダーに組み込んで ROM 形式で出力します。
使い方は以下の通り。

テキストファイル(複数可)をドラッグ&ドロップで放り込むと、リストに登録されます。
登録したテキストを選択して、タイトルとコメント(半角30文字、全角は15文字まで)を入力します。
登録を取り消したい場合は Delete キーを押してください。
テキストの順番はカーソルキーの上下で入れ替わります。

全体で 1.75MB(1,835,008byte) までのテキストファイルを組み込むことが出来ますが、
ページのインデックスを当コンバータで追加するので、実際はもっと少なくなります。
「ナガ10」「Mplus」の2種類のフォントを選ぶことができます。

設定が終わったら Output ボタンを押すと gblib.gbc という ROM ファイルが出力されます。
同時に出来る gblib.bin ファイルはソースからコンパイルする際に使うものです。
必要なければ消去してかまいません。

img img img

スクリーンショットのテキストは青空文庫を利用させて頂いたものです。
左二つの文字フォントは永尾 制一氏の作られたナガ10を使用させていただきました。
一番右の文字フォントは COZ氏の作られた M+ BITMAP FONTSを使用させていただきました。
このような大変有用なリソースをフリーで公開されていることに感謝です。

一番左端はタイトル画面で、下二行にテキストのタイトルとコメントが表示されます。
パッドの上下でテキストの選択、スタートボタンでテキストリーダー画面に切り替わります。
テキストリーダー画面でスタートを押すとタイトルに戻ります。
パッドの左右またはABボタンでページの送り/戻しが出来ます。片手操作で楽々です。

img
セレクトボタンを押している間はページメニューが開きます。
その状態で上下左右キーでページジャンプができます。

セレクト+スタートを押すことで、セーブができます。
次回同じテキストを開いたときに、そこから再開できます。

最初から読みたいときは、タイトル画面で文書を開くとき、
セレクト+スタートを押してください。



■ プログラムについて

・GDMAによる全画面転送

GB は元々8x8ドットのタイルを20x18個敷き詰めて1画面を構成するタイルマップなのですが
GBLibrary では、これを1ドット単位のビットマップのように扱います。

白黒GBでは、全画面をタイルで埋めようにも256個のタイルしか使えないので20x18=320個を
カバーすることが出来なかったのですが(ただし特殊なやり方で可能)、GBC になって、
さらに256個増えたので、20x18の全てに1つ1つタイルを敷き詰め、各タイル8x8ドットを
1ドット単位でアクセスすれば、全画面160x144ドットをビットマップとして扱えるようになりました。

さて、そうなると全画面を書き換えた場合にどのくらいの転送量が発生するかですが、
パレット固定4色に制限すると、1ドット=2ビットなので、160x144x2=46080ビット=5760バイト
の転送をしなければなりません。

      LD   BC,5760 / 2
      DEC  BC
      INC  B
      INC  C
      LD   SP,BUFFER+5760 
      LD   HL,VRAM+5760
LOOP:
      DEC  HL
      POP  DE
      LD   [HL],E
      DEC  HL
      LD   [HL],D
      DEC  C
      JR   NZ,LOOP
      DEC  B
      JR   NZ,LOOP
普通に転送するとこんな感じでしょうか。実際はタイルエリアのバンク切り替えを挟まないと
いけないので、このままではダメですが。DI前提SP待避とか細々した制約もあります。
ループ部分だけ数えると 41,852clock、 GB は CPU クロックが 4MHz なので 4clock=1usとなり
10,463us=約10ミリ秒かかることになります。
・・・VRAM の構造とパレット配色を考えてループ展開するともう少し速くなるかも。

VRAM を安全に操作するには CRTC が VRAM にアクセスしていない間、V-Blank期間を使います。
この V-blank期間は 1.08ミリ秒しかありません。上記の方法では全然間に合わないので
CRTCの方を止めて、転送が終わってからCRTCを再度ONにするしかありません。
このため、液晶画面が一旦ブラックアウトして画面が出るまで少し見苦しいことになります。

      LD   A,$C0
      LDH  [rHDMA1],A 
      LD   A,$00
      LDH  [rHDMA2],A 
      LD   A,$08
      LDH  [rHDMA3],A 
      LD   A,$00
      LDH  [rHDMA4],A 
      LD   A,$7F
      LDH  [rHDMA5],A 
WAIT:
      LDH  A,[rHDMA5] 
      BIT  7,A
      JR   Z,WAIT

こちらは GBC から拡張された汎用 DMA 転送を使った転送です。
上の例では $C000 を転送元、$8800 を転送先として、2048バイト 転送します。
一度に転送できる最大が2048バイトで、転送単位が16バイト、さらに転送先が
VRAM 限定という条件はありますが、16バイトの転送に 8us しかかかりません。
5760バイトの転送が 360us で済むことになります。

うまくやれば動画なども出来そうです。

GBLibraryではバッファの方に文字フォントを書き込んでおいて、V-Blank期間に
GDMAで一気にVRAMに転送を行います。バッファへの書き込みの方が圧倒的に遅く
そのままでは書き込んでいる様子がVRAMに出てしまうので、書き込みが終わってから
転送を許可(=V-Blank割り込み許可)するようにしています。

と、ここまではエミュレータを使ってうまく動いていたのですが、いざ実機で
動かしてみると、画面が崩れまくって話になりません。
どうやら、360us で転送完了という数字が間違っている様子。
V-Blank 割り込みの直後に駆動しても、V-Blank 期間中に終わらず、CRTC の
VRAM アクセスとかぶってしまうために画面が崩れてしまうのだと思われます。

GBC の内蔵 RAM からの転送なので、メモリのアクセス速度に問題があるとは思えません。
あるいはフラッシュカートのメモリコントローラーに問題があるのかもしれません。

仕方ないので別の方法を考えます。
GDMA と同じレジスタを使った DMA 転送に HDMA というものがあります。
これは H-Blank期間にのみ転送を行うモードで、転送を開始すると、自動的に
H-Blankを監視して、CRTC のアクセスとかぶらないように転送してくれます。

これなら原理的に画面が崩れることはありません。
ただし、H-Blank 毎にしか転送しないために安全と引き替えに速度が犠牲になります。
H-Blank毎に16バイト転送なので、5760バイトを転送するためにスキャンライン 360 本分、
GB の1画面がスキャンライン144本なので、2.5画面分で転送完了します。
分かりやすく言うと60/2.5=24FPSということです。
書き換えの様子がギリギリ見えるかどうか、というタイミングですね。

・文字フォントの表示

文字フォントが10x10ドットの場合、1タイル8x8ドットのタイルを最大何枚使うか。
答えは横3枚*縦3枚の最大9枚です。上下左右に1ドットずつはみ出した時が最大になります。

img

160x144のX,Y座標から書き始めの左上のタイル番号を算出したら、文字フォントの
データを横1ライン分読み出し、横3枚分のタイルの位置に合わせてシフト演算を
行います。これを縦10ライン分行って、ようやく一文字表示できます。
タイルに書き込む際は OR演算で書き込まないと、タイルが被っている文字が
部分的に消えてしまうので注意が必要です。

フォントサイズが決まっている以上、文字を書く位置は毎回同じなので、XY座標から
VRAMアドレスと文字フォントのシフト値を事前計算し、テーブル化しておくと楽なのですが、
それだとつまらないので(?)力業で計算しています。

文字フォントは ナガ10 が 10x10、M+ が 10x11 なので、それぞれ 2*10バイト、2*11バイトを
1文字に要します。第一・第二水準漢字6877文字(多分)を GB の ROM に収めるには
ナガ10で 137,540バイト、M+ で151,294バイト必要ということになります。
GBのROMバンクメモリ1つが16,384バイト(16KB)なので9〜10バンクを漢字ROMとして使用する
ことになります。加えて半角フォントに1バンクを使います。

さらに、どの漢字がどの ROM に収まっているかを索引するテーブルを3バンク使用して、
格納バンク、アドレス下位、アドレス上位を収めておくことにしました。
このテーブルは Shift-JIS コードから簡単な計算でアドレスを求めることが出来ます。


文字コードが1バイトコード(半角)か2バイトコード(Shift-JIS全角)かを調べるには
以下の方法で1バイト目をテストします。

if ((Value1 ^ 0x20) - 0xa1 < 0x3c) IS_Shift_JIS 

1バイト目と0x20で xor を取り、0xa1 を引いたものが 0x3c 以下ならShift-JISコードです。
割とポピュラーな方法のようですね。筆者は知りませんでした。

・テキスト格納

テキストはそのままROMに埋め込んでいます。(なのでシフトJISエンコードでないとダメ)
コンバーターが何をしているかというと、タイトルとコメントを文書毎につけるだけでなく、
文書の開始バンクとアドレスを記録して埋め込みます。

さらに、30x12 の1画面でテキストを表示するシミュレートを行い、毎ページの開始バンクと
アドレスをページインデックスとして記録します。
ページを送るだけなら GB 単体で出来るのですが、戻ったりジャンプしたりしたときに
どのようにレイアウトするかが大変面倒なので、インデックスが必要となるのです。
本当は、インデックス作成処理もGB側で出来れば良いのですが・・・。

というわけで、テキスト格納は以下のような配置になっています。

4000 1byte 全文書の数(0-127)
4001 1byte 空き

     (文書情報 72バイト)
4002  2byte 1st文書のインデックス開始バンク
4004  2byte 1st文書のインデックス開始アドレス
4006  2byte 1st文書の終了アドレス + 1
4008  2byte 1st文書の総ページ数 - 1
400A 32byte 1stタイトル
402A 32byte 1stコメント
   (文書の数だけ文書情報が続く)

   (文書開始アドレス=(文書の数)*72+$4002)
404A 文書本体
   (文書は複数を連結して順に格納)

   (インデックス)
404B  1byte 1st文書の1ページ目格納バンク
404C  2byte 1st文書の1ページ目格納アドレス
   (以下、2ページ目…最終ページ、続けて2nd文書の1ページ目インデックスと続く) 

該当テキストの該当ページの検索は

1)該当テキストの文書番号から文書情報開始アドレスを計算
2)文書情報からインデックス開始バンクとアドレスを取り出す
3)インデックス開始アドレス+ページ*3を計算
4)そのアドレスから該当ページのバンクとアドレスを取り出す
5)バンクを切り替え、アドレスから文書本体を読み出す

という手順が必要になります。けっこう面倒です。

・セーブ

意外と SRAM セーブの方法を記した情報が無いような気がするので、ここに書いておきます。

SRAM は $A000-$BFFF の領域を使用します。ここはカートリッジに割り当てられている領域で
GB のヘッダの ROM 種別に BATTERY と付くとバッテリーバックアップ SRAM の意味になります。
普通に RAM と書いたらバッテリーバックアップをしない普通のカートリッジ側 RAM です。
逆に言うとヘッダの情報だけの差であり、SRAM と RAM はバックアップされるか否かだけで、
扱い方は変わりません。

サイズも0KB(無し)から128KB まで設定できます。$A000-$BFFF が 8KB なので
最大 16バンクのバンク切り替えということになります。

唯一注意しないといけないのが、デフォルトではアクセス禁止になっているということです。
SRAM も RAM も[$0000]番地に $0a を書き込むと、以後のアクセスが許可され、
[$0000]番地に $00 を書き込むと以後のアクセスが禁止になります。
これは、電源 ON/OFF などで発生するノイズで内容が破壊されるのを防ぐためと思われます。
よって、普通の RAM の場合は、破壊を気にしなくて良いので、初期化の際に
$0a を一度書きこんでおけばOKです。

といってもエミュレータで使う分には破壊されたりしないのですが・・・。


■ 反省

数年前に書いたプログラムを見直してみて、あまりに意味不明なコードだったので
つい書き直してしまいました。増改築を繰り返してきたプログラムに手を加えると
さらに訳が分からなくなりそうですが、前よりはマシかな、と。
しかし機能的には全く前進していません。
速度も遅いまま。
アプリケーションとしては基本中の基本なんでしょうけれど。

でも手元に初代白黒GBしかなくても、同じようなものを作っているような気がします。


▲ TOP