シェルスクリプトでゲームボーイプログラミング⼊⾨

⼤神祐真 著

エアコミケ（2020 年春）新刊

2020 年 5 ⽉ 5 ⽇

ver 1.0

■免責
本書は情報の提供のみを⽬的としています。
本書の内容を実⾏・適⽤・運⽤したことで何が起きようとも、それは実⾏・適⽤・運⽤した⼈⾃⾝の責任であり、著者や関係者はいかなる責任も負いません。
■商標
本書に登場するシステム名や製品名は、関係各社の商標または登録商標です。
また本書では、™、®、© などのマークは省略しています。

はじめに本書をお⼿にとっていただきありがとうございます！
本書では、アセンブラやコンパイラなどを使⽤せず、echo コマンドのバイナリ出⼒やdd コマンドなどを駆使して、シェルスクリプトでゲームボーイの ROM ファイルを⽣成します。
そのためには、ゲームボーイの CPU の各機械語命令について、そのバイナリ列を
echo 等で⽣成する処理をシェル関数で⼀つ⼀つ⽤意して、というような、正にアセンブ
ラを作る作業をすることになります。
ただ、本書では、筆者が個⼈の開発で使っている⾃作のライブラリ群を使います。このライブラリ群⾃⾝も単なるシェルスクリプトで、source コマンドで読み込むことで、ゲームボーイの CPU の機械語命令やカートリッジヘッダ等のバイナリ列を⽣成するい
くつものシェル関数が使えるようになります。
本書では、このように、予め⽤意したシェル関数を活⽤してシェルからバイナリを直接吐くことで ROM ファイルを⽣成します。
具体的なやり⽅はこのあと本⽂で紹介しますが、「シェルスクリプトのみでやる」というのは⼿軽さもある反⾯、とても強い縛りでもあるので、少々トリッキーなやり⽅が登場するのはご了承ください。。

本書の構成
本書は以下の 3 章構成です。

• 第 1 章 開発環境構築と最初のプログラム◦ 開発に使⽤するスクリプトやエミュレータを紹介しながら、最初のプログラムとして「無限ループするだけ」の ROM ファイルを⽣成します• 第 2 章 背景にタイルを配置◦ 画⾯描画の⼊⾨として、ゲームボーイの画⾯描画⽅式を紹介し、背景にタイルを配置してみます• 第 3 章 キー⼊⼒を試す◦ ⼊⼒取得の⽅法と、割り込みを使⽤する⽅法を紹介し、キー⼊⼒取得を試し
ます

–i–

はじめに

PDF/HTML 版や本書の更新情報について
本書の PDF/HTML 版は筆者のウェブページで公開しています。

• http://yuma.ohgami.jp
本書の内容について訂正や更新があった場合もこちらのページに記載しますので、何かおかしな点などあった場合はまずはこちらのページをご覧ください。

サンプルについて本⽂でも紹介しますが、本書のサンプルは以下のリポジトリで公開しています。

• https://github.com/cupnes/gb_programming_with_shell-script_
samples

本⽂中、コードリストを掲載する際は、このリポジトリ内のファイルパスをキャプションに記載していますので、適宜参考にしてみてください。

– ii –

⽬次
はじめに
本書の構成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

PDF/HTML 版や本書の更新情報について . . . . . . . . . . . . . . . . . . . .
サンプルについて . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
第1章

開発環境構築と最初のプログラム

i
i
ii
ii

1.1

事前準備 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1
1

1.2

本書のサンプルプログラムをダウンロード . . . . . . . . . . . . . . .

1

1.3

簡単なプログラムを作ってみる . . . . . . . . . . . . . . . . . . . . .
本書における GB プログラミングの⽅針 . . . . . . . . . . . . . . . . . .

2

割り込みベクタを作る

1.4
1.5

第2章

2.1

2.2
2.3

. . . . . . . . . . . . . . . . . . . . . . . . . . .
カートリッジヘッダを作る . . . . . . . . . . . . . . . . . . . . . . . . .
無限ループのプログラムを作成する . . . . . . . . . . . . . . . . . . . . .
余⽩を埋める . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
結合して ROM ファイル完成 . . . . . . . . . . . . . . . . . . . . . . . .
エミュレータで実⾏してみる . . . . . . . . . . . . . . . . . . . . . .
次章からのための準備 . . . . . . . . . . . . . . . . . . . . . . . . . .
シェルスクリプト化 . . . . . . . . . . . . . . . . . . . . . . . . . . . .
halt を使う . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
割り込みは⼀旦全て⽌める . . . . . . . . . . . . . . . . . . . . . . . . .
背景にタイルを配置

GB の画⾯描画⽅式 . . . . . . . . . . . . . . . . . . . . . . . . . . .
タイルについて . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
パレットについて . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
タイルを作ってみる . . . . . . . . . . . . . . . . . . . . . . . . . . .
タイルをロードしてみる . . . . . . . . . . . . . . . . . . . . . . . . .
VRAM へアクセスするには . . . . . . . . . . . . . . . . . . . . . . . .
LCD を⽌めるためには . . . . . . . . . . . . . . . . . . . . . . . . . . .
LCDC のレジスタについて . . . . . . . . . . . . . . . . . . . . . . . . .
タイルを ROM へ追加 . . . . . . . . . . . . . . . . . . . . . . . . . . .
GB の CPU が持つ汎⽤レジスタについて . . . . . . . . . . . . . . . . . .
タイルデータを VRAM へロードする . . . . . . . . . . . . . . . . . . . .
パレットを設定する . . . . . . . . . . . . . . . . . . . . . . . . . . . .
試しに動作確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

– iii –

2
3
4
5
7
7

8
11
11
12
14

15
15
15
15

16
17
18
18
19
21
23
23
26
27

⽬次

2.4

画⾯全体を⾃作タイルで敷き詰めてみる . . . . . . . . . . . . . . . . .

28

タイルマップ領域の使い⽅

28
29
31

. . . . . . . . . . . . . . . . . . . . . . . . .
実装 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
結果確認 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
第3章

3.1
3.2
3.3

3.4

キー⼊⼒を試す

33
33
画⾯スクロールの⽅法 . . . . . . . . . . . . . . . . . . . . . . . . . . 34
V ブランク割り込みを使⽤する . . . . . . . . . . . . . . . . . . . . . 34
割り込みベクタに V ブランク割り込み時の処理を記載 . . . . . . . . . . . . 35
V ブランク割り込みを有効化する . . . . . . . . . . . . . . . . . . . . . . 36
割り込み機能⾃体を有効化 . . . . . . . . . . . . . . . . . . . . . . . . . 37
⼗字キーに応じてスクロールする処理を実装する . . . . . . . . . . . . 38
⼗字キー⼊⼒取得処理 . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
スクロール処理 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
実⾏結果 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
キー⼊⼒の取得⽅法 . . . . . . . . . . . . . . . . . . . . . . . . . . .

おわりに

43

参考にさせてもらった情報

45
45
45

GB のアーキテクチャや CPU について . . . . . . . . . . . . . . . . . . . . .
bgb について . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

– iv –

第1章

開発環境構築と最初のプログラム

この章では、使⽤するスクリプト群とエミュレータを紹介しながら、本書でのゲー
ムボーイ (以下、GB) プログラミングの流れを紹介します。

1.1

事前準備

Bash が動作する環境を⽤意しておいてください。Linux や macOS などの場合は問題ないかと思いますが、Windows の場合は WSL などで Bash が動作する環境を⽤意しておいてください。

1.2

本書のサンプルプログラムをダウンロード

シェルスクリプトでの GB プログラミングを補助するスクリプト群は、本書のサンプルに含まれています。
本書のサンプルは以下の GitHub リポジトリで公開しておりますので、こちらを
clone あるいはダウンロードしてください。

• https://github.com/cupnes/gb_programming_with_shell-script_
samples

zip でダウンロードした場合はお好きな場所へ展開してください。

–1–

第1章

開発環境構築と最初のプログラム

1.3

簡単なプログラムを作ってみる

プログラミングのための準備はこれで完了です*1 。さっそく、簡単なプログラムを作ってみましょう。
ここでは、最も簡単なプログラムとして「起動後、無限ループするだけ」というもの
を作ってみることにします。

♣ 本書における GB プログラミングの⽅針
まず、本書での GB プログラミングの⽅針を説明します。
GB プログラミングのアウトプットは「GB の ROM ファイル」です。本書ではこのROM ファイルをシェルスクリプトで⽣成します。
ROM ファイルは GB のメモリ空間の ROM の領域そのものです。GB のアドレス幅
は 16 ビットで 0x0000 から 0xFFFF までのアドレス空間があります。その内、ROMの領域は 0x0000 から 0x7FFF までの 32KB で、この部分のメモリマップは図 1.1 の通りです。

*1 テキストエディタは適宜お好きなものをご⽤意ください。

–2–

1.3 簡単なプログラムを作ってみる▲ 図 1.1: GB の ROM 領域のメモリマップ
本書ではこの 32KB をシェルスクリプトで⽣成します。

♣ 割り込みベクタを作る
前節で紹介した ROM 領域のメモリマップを先頭から埋めていきましょう。
まずは、割り込みベクタです。ここは割り込みやリセットが発⽣した際にジャンプしてくる 256 バイトの領域です。アドレスごとに「どの割り込み/リセットのときにそこへジャンプしてくるか」が決まっています。
ここでは割り込みは使わないので、この領域は全て nop という命令で埋めることにします。nop は「何もしない (No OPeration)」という CPU 命令です。
後述するカートリッジヘッダの先頭にはプログラム本体へのジャンプ命令を配置する
ため、割り込みベクタを nop で埋めておけば、どのような割り込み/リセットが発⽣したとしても、何もせずにカートリッジヘッダの先頭まで CPU の命令実⾏が進み、プログラム本体へジャンプさせることができます。
では、割り込みベクタを作ってみましょう。
まず、作業場所として、サンプルリポジトリを clone あるいはダウンロードして展開したディレクトリへ移動します。

–3–

第1章

開発環境構築と最初のプログラム$ cd gb_programming_with_shell-script_samples

nop は機械語で 0x00 です。ここでは dd コマンドを使って全て 0 の 256 バイトのファイルを⽣成します。
$ dd if=/dev/zero of=vec.dat bs=1 count=256
256+0 レコード⼊⼒
256+0 レコード出⼒
256 bytes copied, 0.00247894 s, 103 kB/s$

これで割り込みベクタを作成できました。

♣ カートリッジヘッダを作る
次にカートリッジヘッダを作ります。
カートリッジヘッダは、先述の通り「GB の起動時にどこへジャンプするか」というプログラムのエントリポイントや、ゲームタイトル⽂字列、カートリッジタイプ (ゲームボーイかゲームボーイカラーか・バッテリ搭載の有無など) などと末尾にチェックサ
ムが書かれている領域です。
本書では、ゲームボーイ⽤でバッテリ無しの ROM(ROM only) でゲームタイトル⽂
字列も空で作成することにします。チェックサムはエントリポイントアドレス以降の領域で計算するため、タイトル⽂字列やカートリッジタイプなどを固定にしておくとチェックサム含めて固定のバイト列で使いまわすことができます。
そのようにしてカートリッジヘッダの⽣成をシェル関数化したものを、サンプルの include/gb.sh に gb_cart_header_no_title という名前の関数で⽤意しています。
その他にも GB の LR35902*2 という CPU の CPU 命令や、いくつかの処理をシェル関数化しており、 gb.sh を source コマンドで読み込むことで使えるようにしています。
gb_cart_header_no_title は エ ン ト リ ポ イ ン ト ア ド レ ス を 引 数 に と り ま す 。

ROM で⾃由にプログラムを配置できる領域は 0x0150 以降なので、エントリポイ
ントは 0x0150 にします。
それでは、このシェル関数を使ってカートリッジヘッダを⽣成してみましょう。
$ . include/gb.sh$ gb_cart_header_no_title 0150 >head.dat*2 8 ビット CPU で、Z80 のカスタム CPU(シャープ製)–4–

1.3 簡単なプログラムを作ってみる$

これで head.dat というファイル名でカートリッジヘッダのバイナリを⽣成できました。

....................................................
■注意：include ディレクトリと同じ階層で作業すること
なお、 include ディレクトリ内のスクリプトを source コマンド (あるいは簡略表現の".") で読み込む際は、include が存在するディレクトリと同じディレクト
リで⾏ってください。

include 内のスクリプトはその他のスクリプトに定義された処理を使⽤する際はそのスクリプトを . include/スクリプト名 のように読み込んでいます。実⾏
時のカレントディレクトリが include ディレクトリが存在するディレクトリであることを想定して作られているので、サブディレクトリを作ってその中で作業する
際は、その中に include ディレクトリのシンボリックリンクを作成するか include
ディレクトリをコピーして配置してください。

....................................................

♣ 無限ループのプログラムを作成する
続いて、0x0150 以降の領域を作成します。ここからがプログラム本体です。好きな
ようにプログラムやデータを配置することができます。
カートリッジヘッダに、起動後のエントリアドレスとしてこの領域先頭である 0x0150
を指定しましたので、ゲームボーイの電源を⼊れて Nintendo ロゴが表⽰された後、0x0150 へジャンプしてきます。
今回は「無限ループするだけのプログラム」ということで、ここに無限ループのプログラムを配置することにします。
プログラムは CPU が直接解釈する機械語で配置します。ただ、⼿で機械語の 16 進数を書いていくのは少しつらいので、CPU の機械語命令のバイナリを⽣成するシェル関数を⽤意しています*3 。
ここでは、CPU の相対ジャンプ命令を使って無限ループを実現します。各 CPU 命
令は lr35902_ から始まる名前のシェル関数で実装しています。相対ジャンプ命令は lr35902_rel_jump という名前のシェル関数です。引数にジャンプする相対的なバイト数を 2 桁の 16 進数 (1 バイト) で指定します。
試しに、引数に 0x00 を指定して実⾏してみます。標準出⼒へ機械語バイナリを出⼒するので、適当なファイルへリダイレクトします。
*3 まだ全部ではないと思いますが、GB 上で動く OS もどきを作るのに困らないくらいはあります。

–5–

第1章

開発環境構築と最初のプログラム$ lr35902_rel_jump 00 >rel.dat$
lr35902 から始まる CPU 命令をシェル関数化したものを実⾏すると、カレント

ディレクトリに asm.lst というファイルが⽣成されます。ここには⽣成した機械語
命令のアセンブラのリストが追記されていきます。特に本書で使ったりはしませんが、デバッグ時などに参考にしてみてください。(コロン区切りの右側の数字はその命令にかかるサイクル数です。)$ cat asm.lst
jr $00 ;12$

リダイレクトしたバイナリを hexdump で⾒てみると 0x18 00 という 2 バイトが⽣成されていることがわかります。
$ hexdump -C rel.dat
00000000 18 00
00000002$|..|

これが相対ジャンプ命令の機械語バイナリです。 0x18 が「相対ジャンプ命令」であることを⽰し、続く 0x00 がジャンプする相対的なバイト数を⽰しています (引数で与えた 0x00 がそのまま⼊っています)。
相対ジャンプ命令で指定する「相対的なバイト数」は、「⾃⾝の次の命令」が基準です。そのため、今回のように 0 を指定した場合、「⾃⾝の次の命令」へジャンプすることになります*4 。
無限ループにするためには⾃⾝の命令のバイト数マイナスの位置へジャンプする必要があります。マイナスの表現には 2 の補数を使います。相対ジャンプ命令のバイト数は2 バイトなので、-2 を表す 0xfe を指定すると無限ループになります。
$ lr35902_rel_jump fe >inf.dat$

これで、無限ループのプログラムを作ることができました。
なお、2 の補数も都度計算するのは⾯倒なので、シェル関数を⽤意しています。
two_comp というシェル関数は、引数で与えた 16 進数 2 桁の数値を 2 の補数によるマイナス表現へ変換します。例えば、-2 の 2 の補数表現を得る際は以下のように使⽤*4 単に次の命令へ進むだけで、nop と同じ挙動–6–

1.3 簡単なプログラムを作ってみるします。
$ two_comp 02
FE$

これは、そのまま相対ジャンプ命令の引数に与えることができます。
$ lr35902_rel_jump $(two_comp 02) | hexdump -C
00000000 18 fe
00000002$|..|

lr35902_rel_jump の 出 ⼒ を そ の ま ま hexdump に 与 え て 確 認 す る と 、
0x18 fe という 2 バイトが⽣成できています。 0x18 という相対ジャンプ命令のオペランドとして 0xfe (-2 の 2 の補数表現) が与えられており、意図通りです。
ま た 、10 進 数 で 指 定 し た 数 を 2 の 補 数 に よ る マ イ ナ ス 表 現 へ 変 換 す る 関
数 two_comp_d も⽤意しています。(適宜使い分けます)♣ 余⽩を埋める
ここまでで、割り込みベクタ (256 バイト)・カートリッジヘッダ (80 バイト)・機械語
プログラム (2 バイト) を作成しました。ROM ファイルを 32KB とするために、残るROM の領域を適当なデータで埋めます。
残る ROM の領域は、 (32*1024)-256-80-2=32430 より、32430 バイトです。このサイズ分を全て 0 で埋めたデータを dd コマンドで作成します。
$ dd if=/dev/zero of=pad.dat bs=1 count=32430
32430+0 レコード⼊⼒
32430+0 レコード出⼒32430 bytes (32 kB, 32 KiB) copied, 0.0553519 s, 586 kB/s$

♣ 結合して ROM ファイル完成
ここまでで作成した全てを結合すると ROM ファイルになります。
$ cat cat vec.dat head.dat inf.dat pad.dat >inf.gb$

無限ループするだけの ROM ということで、 inf.gb という ROM ファイル名で作成してみました。

–7–

第1章

1.4

開発環境構築と最初のプログラム

エミュレータで実⾏してみる

エミュレータは何でも構いませんが、本書では「BGB」というエミュレータで説明します。

BGB は以下のウェブサイトからダウンロードできます。
• http://bgb.bircd.org/「downloads」というリンクをクリックするとジャンプするページ下部の以下の場所
で実⾏バイナリをダウンロードできます。

• http://bgb.bircd.org/#downloads
筆者は 64 ビット版のアーカイブ「bgbw64.zip」をダウンロードして使⽤しています。
公開されている実⾏バイナリは Windows 向けのみですが、Linux 上でも Wine*5 を使⽤することで問題なく動作します*6 。

Linux の場合について、Wine のセットアップ⽅法を詳しく説明はしませんが、Debian/Ubuntu などの APT が使える環境の場合、以下のあたりをインストールすれば問題ないかと思います。

• wine64• winetricks
winetricks は、Wine ⽤のツールやデータをダウンロードしたり設定したりする

パッケージマネージャのようなものです。Wine で BGB を起動した際にアルファベッ
トが⽂字化けしたため、その対処を⾏うためにインストールしました。このような問題に陥った場合の対処⽅法は、筆者が参考にさせていただいたブログ記事を本書末尾の「参考にさせてもらった情報」に記載していますので、参照してみてください。

Wine を使⽤する場合、以下のように BGB を起動できます。なお、BGB の zip アー
カイブ (bgbw64.zip) はホームディレクトリ直下に展開されてるとします。
$ wine64 ~/bgbw64/bgb64.exe -nowarn ROMファイル名

なお、 -nowarn のオプションは、カートリッジヘッダの⽣成時に設定を省いている
⼀部のチェックサムのために付けています。実機では参照されないチェックサムなのですが*7 、BGB ではそこも確認し、起動時にワーニングのメッセージを表⽰するため、それをスキップするようにしています。
*5 Linux 上で Windows アプリケーションを動作させるツール。
*6 筆者⾃⾝、Linux(Debian) 上で Wine を使⽤して BGB を動作させています。BGB のために Wine

をインストールするくらい、BGB は⾊々なデバッグができる強⼒なエミュレータです。
*7 カートリッジヘッダの「グローバルチェックサム」というものです。詳しくは、本書末尾の「参考に–8–

1.4 エミュレータで実⾏してみる

以降、BGB で ROM ファイルを動作させることを⽰す際のコマンドは以下のように記載します*8 。
$ bgb ROMファイル名

なお、Windows の場合は、上記の表記が出てきた場合も、よしなに BGB の実⾏ファ
イルをダブルクリックして ROM ファイルを開くなりしてもらえれば構いません。
それでは、作成した ROM ファイル inf.gb を起動してみましょう。
$ bgb inf.gb

起動すると図 1.2 のような画⾯が表⽰されます。

▲ 図 1.2: inf.gb の実⾏画⾯させてもらった情報」の「Everything You Always Wanted To Know About GAMEBOY」の「The Cartridge Header」の章を参照してください。
*8 適宜読み替えても良いですし、シェルへのエイリアス設定追加や、パスの通った場所に"bgb"というファイル名で起動⽤のスクリプトを配置しても構いません。

–9–

第1章

開発環境構築と最初のプログラム「Nintendo」というロゴが表⽰されている状態がゲームボーイの起動直後のデフォル
ト画⾯です。
無限ループしているだけなので、画⾯上は何も変化がありません。
ちゃんと意図通りに動いているのか確認するために、デバッグ画⾯を開いてみましょう。
右クリックで表⽰されるメニューから [Other] の中の [Debugger] をクリックしてください。
すると、図 1.3 の画⾯が表⽰されます。

▲ 図 1.3: inf.gb のデバッグ画⾯
⾊々と表⽰されていますが、ここでは 4 分割された中の左上の領域を⾒てください。
ここには、「今 ROM のどこを実⾏中であるか」が、機械語を逆アセンブルした結果
とともに表⽰されています。
右向き⽮印で⽰された⾏が今実⾏中の箇所で、この⾏のみ抜き出すとリスト 1.1 の通りです。
▼ リスト 1.1: 今実⾏中の⾏
ROM0: 0150 18 FE

jr 0150

この⾏は「ROM のバンク 0 内でアドレスが 0x0150」、「0x18 fe というバイナリが書かれていて逆アセンブルすると jr 0150」ということを表しています。

– 10 –

1.5 次章からのための準備

jr は相対ジャンプ命令のアセンブラ表現で、オペランドが 0150 になっているのはBGB が逆アセンブル時に絶対アドレスへ変換しているためです。(機械語を⾒ると 18
fe と、意図通りに書かれています。)「0x0150 へジャンプし続ける無限ループ」が実現できており、意図通りに動いていることを確認できました。
本書ではこのようにして GB の ROM ファイルを作成していきます。

1.5

次章からのための準備

基本的には以上のやり⽅で GB の ROM ファイルを作っていくことになります。
ただ、都度コマンドを⼿打ちするのは⾯倒なので、今後はシェルスクリプト化したものをベースに説明します。

♣ シェルスクリプト化
この章の内容をリスト 1.2 のシェルスクリプトにします。
▼ リスト 1.2: 01_inf/01_inf.sh#!/bin/bash
set -uex. include/gb.sh
PROG_SIZE=2# 全てnopの割り込みベクタ⽣成
gb_all_nop_vector_table# カートリッジヘッダ⽣成
gb_cart_header_no_title ${GB_ROM_FREE_BASE}# 無限ループ
lr35902_rel_jump $(two_comp 02)# 32KBに満たない分を0で埋める
dd if=/dev/zero bs=1 \count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - PROG_SIZE))

シェルスクリプト化にあたり、以下の変更を加えています。

• 実⾏時のデバッグ出⼒や未定義変数チェックなどのために、冒頭に set -uex を追加しています– 11 –

第1章

開発環境構築と最初のプログラム• 割り込みベクタ⽣成処理は gb_all_nop_vector_table というシェル関数化
しているため、それを使っています• ROM の カ ー ト リ ッ ジ ヘ ッ ダ 直 後 の ⾃ 由 に 使 え る 領 域 の 先 頭 ア ド レ ス
は GB_ROM_FREE_BASE という名前で変数を定義済みのため、それを使っています• 32KB に満たない分を埋める処理のサイズ計算にも、定義済みの変数を使っています
シェルスクリプトでは、標準出⼒へ ROM バイナリを⽣成するようにしています。

ROM ファイルはシェルスクリプト実⾏時にファイルへリダイレクトすることで⽣成できます。
$ 01_inf/01_inf.sh >inf.gb$♣ halt を使う
次章以降を始める前の準備として、無限ループの箇所を少し変更します。
このような OS レスのプログラミングにおいては、実⾏終了後の戻り先が無いこともあり、プログラムの最後では無限ループで停⽌させることがよくあります。
本書今後作っていくいくつかのサンプルもその⽅針なのですが、CPU の実⾏を進めないようにする際に、単なるビジーループは CPU に余計な負荷をかけてしまい、優し
くないです。
halt という CPU 命令を使うと、割り込みなどのイベントがあるまで CPU を休ませることができるので、何もしないビジーループでも、halt を挟むと良いです。
先程シェルスクリプト化したものを halt を使⽤するように変更するとリスト 1.3 の通りです。
▼ リスト 1.3: 01_inf/02_halt.sh#!/bin/bash
set -uex. include/gb.sh
PROG_SIZE=4# 変更# 全てnopの割り込みベクタ⽣成
gb_all_nop_vector_table– 12 –

1.5 次章からのための準備# カートリッジヘッダ⽣成
gb_cart_header_no_title ${GB_ROM_FREE_BASE}# ・・・変更(ここから)・・・# 無限halt
lr35902_halt
lr35902_rel_jump $(two_comp 04)# ・・・変更(ここまで)・・・# 32KBに満たない分を0で埋める
dd if=/dev/zero bs=1 \count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - PROG_SIZE))

halt 命令は lr35902_halt というシェル関数で⽤意しています。
2 バイトの命令にしている*9 ので、相対ジャンプのジャンプの戻り先も 2 バイト分増
やして $(two_comp 04) としています。
$ lr35902_halt | hexdump -C
00000000 76 00
00000002|v.|

halt を使⽤して無限に待ち続ける、という処理は今後も良く書くと思われるので、
infinite_halt というシェル関数化してあります。

これを使うように変更するとリスト 1.4 の通りです。
▼ リスト 1.4: 01_inf/03_halt_2.sh#!/bin/bash
set -uex. include/gb.sh
PROG_SIZE=4# 全てnopの割り込みベクタ⽣成
gb_all_nop_vector_table# カートリッジヘッダ⽣成
gb_cart_header_no_title ${GB_ROM_FREE_BASE}*9 halt 命令⾃体は 0x76 のみの 1 バイトの命令なのですが、⾊々調べてみると halt 命令は実⾏時、次の命令を⼀つ実⾏してしまう挙動があるとの情報もあったので、halt 命令には明⽰的に nop(0x00) を⼀つ追加するようにしています。(halt を呼ぶような時は急いでいることも無いので)– 13 –

第1章

開発環境構築と最初のプログラム# ・・・変更(ここから)・・・# 無限halt
infinite_halt# ・・・変更(ここまで)・・・# 32KBに満たない分を0で埋める
dd if=/dev/zero bs=1 \count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - PROG_SIZE))♣ 割り込みは⼀旦全て⽌める
割り込みは、使うまで⼀旦全て⽌めます。
割り込み機能⾃体を有効化/無効化する命令があり、ここで予め割り込みを無効化す
る命令を実⾏するようにしておきます。
処理を追加するとリスト 1.5 の通りです。
▼ リスト 1.5: 01_inf/04_di.sh#!/bin/bash
set -uex. include/gb.sh
PROG_SIZE=5# 変更# 全てnopの割り込みベクタ⽣成
gb_all_nop_vector_table# カートリッジヘッダ⽣成
gb_cart_header_no_title ${GB_ROM_FREE_BASE}# ・・・追加(ここから)・・・# 割り込み無効化
lr35902_disable_interrupts# ・・・追加(ここまで)・・・# 無限halt
infinite_halt# 32KBに満たない分を0で埋める
dd if=/dev/zero bs=1 \count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - PROG_SIZE))

次章からはこのコードをベースに説明します。

– 14 –

第2章

背景にタイルを配置

GB では画像は全て「タイル」という単位で扱います。ここではタイルの定義⽅法を紹介し、タイルの定義と背景への配置を⾏ってみます。

2.1

GB の画⾯描画⽅式

GB の画⾯を制御する LCDC(LCD コントローラ) は、「タイル」という単位で画⾯を描画します。
「タイル」は 8x8 のビットマップで、専⽤のメモリ領域に定義します。タイルには連番で番号が対応付いており、例えば背景に何かを描画する際は、「N 番のタイルを配置する」というようにタイル番号を指定します。

♣ タイルについてタイルを定義するメモリ空間は、設定により 2 つあるのですが、本書では 0x8000 から 0x8FFF の領域を使⽤します*1 。
ゲームボーイのタイルの各ピクセルは 2 ビットで、2 * 8 * 8 = 128 ビット (16 バイト) で 1 つのタイルを表します。2 ビットで⽩黒なので、表現できる⾊は、黒・濃いグ
レー・薄いグレー・⽩の 4 ⾊です。

♣ パレットについて
タイルの各 2 ビットを黒・濃いグレー・薄いグレー・⽩のどれと対応付けるかを決めるのが「パレット」という仕組みです。
パレットは 8 ビットのレジスタで、背景⽤に 1 つ (BackGround Palette:BGP) と、*1 もう⽚⽅は 0x8800 から 0x97FF です。タイルを配置する領域としてどちらを使うかで使い⽅が変わるのですが、本書では 0x8000 から 0x8FFF を使う場合についてのみ紹介します。

– 15 –

第2章

背景にタイルを配置

本書では扱いませんがオブジェクト⽤に 2 つ (Object Palette 0,1:OBP0,OBP1) の計

3 つあります。
パレットの各ビットの役割は表 2.1 の通りです。
▼ 表 2.1: パレットレジスタの各ビットの役割
ビット

役割

7-6
5-4
3-2
1-0

⾊番号 3 の⾊指定
⾊番号 2 の⾊指定
⾊番号 1 の⾊指定
⾊番号 0 の⾊指定

⾊番号 0 から 3 の全 4 つの⾊番号に何⾊を対応付けるかを、各 2 ビットずつの⾊指定で指定します。
そして、⾊指定の 2 ビットと⾊との対応が表 2.2 の通りです。
▼ 表 2.2: ⾊指定の 2 ビットと⾊の対応
11
10
01
00

黒
濃いグレー
薄いグレー
⽩

そのため、BGP へ 0xE4 を設定すれば、タイルを背景として描画する際、タイルの各ピクセル (2 ビット) を、「11 は黒」
・「10 は濃いグレー」
・「01 は薄いグレー」
・「00 は⽩」と表⽰できます。

2.2

タイルを作ってみる

では、実際にタイルを定義してみましょう。
と⾔っても、1 タイル 16 バイトの中に 8x8 の各ピクセル (2 ビット) がどのように並ぶのかというと、それも少し特殊です*2 。

*2 詳しくは本書末尾の「参考にさせてもらった情報」に記載しているリンク先などを参照してみてください。

– 16 –

2.3 タイルをロードしてみる

16 バイトのタイルデータも、⾃動⽣成するスクリプトを⽤意しているので、本書ではそれを使います。
使⽤するスクリプトは、サンプルの tools ディレクトリ内にある txt22bpp です。
「2bpp」は「2 Bits Per Pixel」の略で、GB のピクセルフォーマットをこのように呼んだりします。
txt22bpp は、8x8 のビットマップをテキストで表現したテキストファイルを与えると、2bpp 形式のタイルバイナリデータへ変換します。
例えばリスト 2.1 のようなファイルを引数に与えることができます。
▼ リスト 2.1: txt22bpp へ与えるテキストの例####****####****####****####****
....
....
....
....

txt22bpp は、’#’ を⾊番号 3 へ、’*’ を⾊番号 2 へ、’.’ を⾊番号 1 へ、’ ’ を⾊番号0 へ変換します 02_about_txt22bpp_case 。
リスト 2.1 を tile.txt というファイル名で保存したとすると、以下のようにタイ
ルデータを⽣成できます。
$ tools/txt22bpp tile.txt tile.2bpp$ ls tile.2bpp
tile.2bpp$⽣成された tile.2bpp が 2bpp 形式のタイルデータバイナリです。

2.3

タイルをロードしてみる

タイルはメモリ上 0x8000 以降のタイルデータ領域へロードすることで使えるようになります。この領域は先頭から 16 バイト毎にタイル番号が割り振られていきます。

(0x8000 以降の 16 バイトの領域にロードしたタイルデータはタイル番号 0 番、0x8010
以降の 16 バイトの領域にロードしたタイルデータはタイル番号 1 番、という具合です。)
ここでは、0x8000 以降の 16 バイトの領域に先程⽣成したタイルデータをロードして– 17 –

第2章

背景にタイルを配置

タイル番号 0 番として使えるようにしてみます。

♣ VRAM へアクセスするには
タイルデータ領域や、背景⽤のタイルマップ領域、オブジェクトを定義する領域などの VRAM の領域は、画⾯描画を⾏う LCDC もアクセスします。LCD がアクセスしている間、CPU は VRAM へアクセスしてはいけないことになっています。
そのため、VRAM を読み書きする際は以下のいずれかの対応を⾏う必要があります。

• LCD を⽌める
◦ ⽌めている間、VRAM へアクセスし放題◦ ただし、画⾯が消える (真っ⽩になる)◦ ⽌める操作は V ブランク期間に⾏う必要がある• LCD がアクセスしていない期間にアクセスするようにする◦ 次に LCD がアクセスし始めるまでは VRAM アクセス可能◦ LCD のステータスを監視する、割り込みを使う等でタイミングを計る必要がある• DMA を使う
◦ オブジェクトデータの転送にのみ使える
DMA は⽤途が決まっているのでここでは該当しないとして、「LCD を⽌める」⽅法と「LCD がアクセスしていない期間にアクセスするようにする」⽅法は、ゲーム起動直後に前者の⽅法で VRAM を初期化し、その後に VRAM へアクセスする必要がある際は後者の⽅法でアクセスする、と使い分けていることが多いようです。
今の実装は正に起動直後なので、「LCD を⽌める」⽅法で実装してみることにします。

♣ LCD を⽌めるためには
LCD が画⾯描画を⾏う中ではいくつかの期間があり、その中で VRAM へアクセス
していない期間はいくつかあるのですが、LCD の停⽌はその中でも「V ブランク」期
間限定でハードウェア的に許されている操作です。
た だ 、「LCD が V ブ ラ ン ク 期 間 に な る の を 待 つ 」処 理は gb_wait_for_vblank_to_start と い う 関 数 で 実 装 済 み で す 。今 回 は こ のシェル関数を使えば良いので、LCD の振る舞いについて詳しくは説明しません*3 。

*3 ただ、ディスプレイの画⾯更新処理はこのようなレトロなハードでプログラミングする際の基礎知識だとも思うので、興味があれば「LCD H ブランク V ブランク」などのキーワードで調べてみると⾯⽩いです。

– 18 –

2.3 タイルをロードしてみる♣ LCDC のレジスタについて
そして、V ブランクを待った上で、LCD を⽌めるにはどうするかというと、LCDCのレジスタを操作します。LCD の有効/無効⾃体はこのレジスタの最上位ビットなので、このビットだけ操作すれば LCD を⽌めることができます。
ただ、LCD についてはいくつか設定しなければならない部分もあるので、LCDC のレジスタの各ビットについてここで説明します。

LCDC の 8 ビットレジスタの各ビットをまとめると表 2.3 の通りです。
▼ 表 2.3: LCDC のレジスタ
ビット

役割

7

LCD の有効 (=1)/無効 (=0)
ウィンドウタイルマップアドレス設定

6
5(0=0x9800-9BFF, 1=0x9C00-9FFF)
ウィンドウの有効 (=1)/無効 (=0)
背景とウィンドウ⽤のタイルデータ配置アドレス設定

4(0=0x8800-97FF, 1=0x8000-8FFF)
背景タイルマップアドレス設定

3
2
1
0

(0=0x9800-9BFF, 1=0x9C00-9FFF)
オブジェクトサイズ (0=8x8, 1=8x16)
オブジェクトの有効 (=1)/無効 (=0)
背景の有効 (=1)/無効 (=0)

「ウィンドウ」や「オブジェクト」は、本書では扱いませんが、簡単に説明すると、

GB の画⾯は 3 層のレイヤー構造のようになっていて、⼀番⼿前にあるのが「オブジェクト」、次が「ウィンドウ」、最後が「背景」です。そして、それぞれは単なるレイヤー
ではなく機能が異なります。
「オブジェクト」は「スプライト」などとも呼ばれるもので、8x8(タイル 1 枚) あるいは 8x16(タイル 2 枚) の画像を座標やその他いくつかの属性情報とともに管理する機能
で、オブジェクトに紐付いた座標値のみ更新するだけで、LCD はそのオブジェクトを指定された位置に描画してくれます。
「ウィンドウ」は機能的にはほとんど「背景」と同じで、VRAM 上のタイルマップ領
域に「この座標にはこのタイル」と指定することで画⾯上にタイルを描画します。透過
⾊や、ウィンドウ幅・⾼さの変更などはできないので、有効にすると、画⾯サイズ分のスクリーンがそのまま 1 枚、背景を覆い尽くす形で描画されることになります。ただ、ウィンドウの開始座標 (左上座標) は変更できるので、画⾯下部にメッセージウィンド
ウを描画する、などに使うことができます。

– 19 –

第2章

背景にタイルを配置

以上を踏まえ、本書では LCDC には表 2.4 の設定を⾏うことにします。
▼ 表 2.4: 本書の LCDC 設定
ビット

設定値

7
6
5
4
3
2
1
0

有効 (=1)/無効 (=0) は状況に応じて設定
背景に使わない側を設定 (1=0x9C00-9FFF)
ウィンドウは無効 (=0)
タイルデータ配置アドレスは 0x8000-8FFF(=1) にする
背景タイルマップアドレスは 0x9800-9BFF(=0) にする
オブジェクトは使わないのでどちらでも良い (=0)
オブジェクトは無効 (=0)
背景は有効 (=1)

表 2.4 を踏まえて、LCD の無効 (ビット 7 を 0) も⾏うと、LCDC のレジスタ設定値
は 2 進数で 0b0101 0001 で 16 進数で 0x51 です。
それでは、この設定を LCDC へ⾏う処理を追加してみます (リスト 2.2)。
▼ リスト 2.2: 02_bg/01_stop_lcd.sh#!/bin/bash
set -uex. include/gb.sh# ・・・追加(ここから)・・・
main() {# 割り込み無効化
lr35902_disable_interrupts# Vブランク期間を待つ
gb_wait_for_vblank_to_start# LCD設定&LCD⽌める
lr35902_set_reg regA 51
lr35902_copy_to_ioport_from_regA $GB_IO_LCDC# 無限halt
infinite_halt}# ・・・追加(ここまで)・・・# 全てnopの割り込みベクタ⽣成– 20 –

2.3 タイルをロードしてみる

gb_all_nop_vector_table# カートリッジヘッダ⽣成
gb_cart_header_no_title ${GB_ROM_FREE_BASE}# ・・・変更(ここから)・・・# main処理
main >main.ocat main.o# 32KBに満たない分を0で埋める
main_size=$(stat -c '%s' main.o)dd if=/dev/zero bs=1 \count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - main_size))# ・・・変更(ここまで)・・・

処理を追加するにあたり、「32KB に満たない分を 0 で埋める」の箇所の計算を都度⾏うのは⾯倒なので、プログラム本体部分は main というシェル関数に記述し、その部分のみ main.o というファイルへ出⼒して、そのファイルサイズを使うようにしました。
「LCD 設 定&LCD ⽌ め る 」処 理 は 2 つ の 命 令 で ⾏ っ て い ま す 。ま ず 1 つ ⽬の lr35902_set_reg regA 51 で、レジスタ A へ LCDC へ設定する値 0x51 を格納しています。 lr35902_set_reg は「汎⽤レジスタ」へ値を設定する命令をシェル関数化したものです。GB の CPU である LR35902 の汎⽤レジスタについて詳しくは後述しますが、ここでは A という 8 ビットのレジスタに 0x51 という値を格納してい
ます。
そ

し

て

、 lr35902_copy_to_ioport_from_regA $GB_IO_LCDC で

レ ジ ス タ A の 内 容 を LCDC の レ ジ ス タ へ 設 定 し て い ま す 。
lr35902_copy_to_ioport_from_regA は 、レ ジ ス タ A の 内 容 を 指 定 さ れ た

IO レジスタへ設定する命令をシェル関数化したものです。

♣ タイルを ROM へ追加
VRAM のタイルデータ領域へロードするには、そもそもロード対象であるタイル
データが ROM 内に存在する必要があります。
タイルデータは 2bpp 形式で作成しましたので ( tile.2bpp )、それを ROM の中に組み込むようにします (リスト 2.3)。

– 21 –

第2章

背景にタイルを配置▼ リスト 2.3: 02_bg/02_add_tile_to_rom.sh#!/bin/bash
set -uex. include/gb.sh# ・・・追加(ここから)・・・
TILE_SIZE=10# 16バイト# エントリアドレスは⾃由に使えるROM領域の先頭(0x0150)から# タイルを配置した16バイト先のアドレスになる
ENTRY_ADDR=$(echo "${GB_ROM_FREE_BASE}+${TILE_SIZE}" | bc)# ・・・追加(ここまで)・・・
main() {# ・・・追加(ここから)・・・# ⾃由に使える領域の先頭(0x0150)にタイルデータを追加する
cat tile.2bpp# ・・・追加(ここまで)・・・# 割り込み無効化
lr35902_disable_interrupts# Vブランク期間を待つ
gb_wait_for_vblank_to_start# LCD設定&LCD無効化
lr35902_set_reg regA 51
lr35902_copy_to_ioport_from_regA $GB_IO_LCDC# 無限halt
infinite_halt}# 全てnopの割り込みベクタ⽣成
gb_all_nop_vector_table# カートリッジヘッダ⽣成
gb_cart_header_no_title $ENTRY_ADDR# main処理
main >main.ocat main.o# 32KBに満たない分を0で埋める
main_size=$(stat -c '%s' main.o)– 22 –

2.3 タイルをロードしてみる

dd if=/dev/zero bs=1 \count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - main_size))

タイルデータをロードする際にタイルデータ⾃⾝の位置がずれると⾯倒なので、タイルデータは⾃由に使える ROM 領域の先頭へ配置しました。
それに伴って、実⾏時のエントリアドレスがタイルサイズ分 (16 バイト) 後ろにずれるので、その調整を⾏っています。

♣ GB の CPU が持つ汎⽤レジスタについて
次の項でロード処理を実装するにあたり、ここで、GB の CPU(LR35902) が持つ汎⽤レジスタについて説明します。

LR35902 が持つ汎⽤レジスタ (⼀部そうでないものも混ざっていますが) をまとめると表 2.5 の通りです。
▼ 表 2.5: LR35902 の汎⽤レジスタ
16 ビット
AF
BC
DE
HL

8 ビット
A
B
D
H

8 ビット
C
E
L

備考

F はフラグレジスタ

8 ビットレジスタが A・B・C・D・E・H・L の計 7 個があります。
そして、それぞれは合体させることで 16 ビットレジスタとして使うことも可能で、AF・BC・DE・HL の 4 つの 16 ビットレジスタとして機能します。合体させる際の上位側・下位側も名前の並びの通りです。(例えば、レジスタ BC は、レジスタ B が上位
8 ビットでレジスタ C が下位 8 ビット)
ただし、A と合体させた F は「フラグレジスタ」という演算結果に応じて変化する専⽤のレジスタなので、AF については「16 ビットの値を格納」などには使えません。

♣ タイルデータを VRAM へロードする
それでは、ROM に配置したタイルデータを VRAM のタイルデータ領域へロードしてみましょう。
実装の⽅針は以下の通りです。

• レジスタ BC へ ROM 上のタイルデータのアドレス (0x0150) を設定• レジスタ HL へ VRAM 上のタイルデータロード先アドレス (0x8000) を設定– 23 –

第2章

背景にタイルを配置• レジスタ C へタイルサイズ (16) を設定• 以下をレジスタ C が 0 になるまで繰り返す◦ レジスタ BC が指す先 1 バイトをレジスタ HL の指す先へコピー◦ レジスタ BC と HL をそれぞれインクリメント◦ レジスタ C をデクリメント
これを実装するとリスト 2.4 の通りです。
▼ リスト 2.4: 02_bg/03_load_tile_to_vram.sh# ・・・省略・・・
VRAM_TILE_DATA_BASE=8000# 追加

main() {# ⾃由に使える領域の先頭(0x0150)にタイルデータを追加する
cat tile.2bpp# 割り込み無効化
lr35902_disable_interrupts# Vブランク期間を待つ
gb_wait_for_vblank_to_start# LCD設定&LCD無効化
lr35902_set_reg regA 51
lr35902_copy_to_ioport_from_regA $GB_IO_LCDC# ・・・追加(ここから)・・・# タイルデータをVRAMのタイルデータ領域(0x8000)へロードする
lr35902_set_reg regDE $GB_ROM_FREE_BASE
lr35902_set_reg regHL $VRAM_TILE_DATA_BASE
lr35902_set_reg regC $TILE_SIZE(
lr35902_copy_to_from regA ptrDE
lr35902_copyinc_to_ptrHL_from_regA
lr35902_inc regDE
lr35902_dec regC
) >main.1.ocat main.1.olocal sz_1=$(stat -c '%s' main.1.o)
lr35902_rel_jump_with_cond NZ $(two_comp_d $((sz_1+2)))# ・・・追加(ここまで)・・・# 無限halt
infinite_halt}– 24 –

2.3 タイルをロードしてみる# ・・・省略・・・

レジスタに数値を設定する命令も lr35902_set_reg という名前でシェル関数化しています。 lr35902_set_reg regDE $GB_ROM_FREE_BASE というようにレジスタ

DE にタイルデータのアドレスを設定していて、HL や C も同様の⽅法で値を設定しています。
() でサブシェル化している箇所がループで繰り返し実⾏される部分です。ここで ROM から VRAM へのタイルデータのコピーを 1 バイトずつ⾏っています。各⾏を説明すると、まず lr35902_copy_to_from regA ptrDE で、レジスタ DE が指す先の 1 バイトをレジスタ A へコピーします。 lr35902_copy_to_from は第1 引数のレジスタへ第 2 引数のレジスタをコピーする機械語を⽣成するシェル関数
で、 ptrDE のようにレジスタ指定するとそのレジスタをポインタとして使います*4 。
次に lr35902_copyinc_to_ptrHL_from_regA でレジスタ HL が指す先へレジス
タ A をコピーします。なお、このシェル関数ではレジスタ HL のインクリメントも同時に⾏います*5 。これで 1 バイト分の ROM から VRAM へのコピーが完了です。
lr35902_inc regDE でレジスタ DE をインクリメントし、 lr35902_dec regC でレジスタ C をデクリメントします。
そして、このサブシェル部分は出⼒を main.1.o というファイルへリダイレクト
しています。これにより、このサブシェル部分は直ちに標準出⼒へ出⼒するのではなく、⼀旦ファイルへ保存するようにしています。ただこの場合は直ちに cat でその内容を標準出⼒へ出⼒しているわけですが、何故このようなことをしているのかというと、この後、この処理をループさせるために相対ジャンプ命令で「何バイト戻れば良いのか」を算出するためです。 local sz_1=$(stat -c ’%s’ main.1.o) で
サ ブ シ ェ ル 化 し た 部 分 の バ イ ト 数 を sz_1 と い う 変 数 へ 格 納 し 、
lr35902_rel_jump_with_cond NZ $(two_comp_d $((sz_1+2))) で 相 対 ジ ャ
ンプする際の戻るバイト数の計算に使っています。2 を⾜しているのは、相対ジャ
ンプ命令⾃体のバイト数 (2 バイト) です。
最後に、この相対ジャンプ命令のシェル関数には _with_cond が付いています。これは「条件付き相対ジャンプ命令」という「直前の演算結果に応じてジャンプするか否かを変える」命令をシェル関数化したものです。今回の場合、第 1 引数に NZ を指定しているので「直前の演算結果がゼロでは無い場合 (Not Zero)」にジャンプすることに*4 C ⾔語のポインタと同じ意味です。そのレジスタの中⾝ではなく、そのレジスタに格納されている値をアドレスとして使い、そのアドレスが指す先に作⽤するようになります。
*5 レジスタ HL に対しては同時にインクリメントも⾏う命令 (ldi) が⽤意されているため、それに対応するシェル関数を⽤意しています。

– 25 –

第2章

背景にタイルを配置なります。直前の演算はレジスタ C のデクリメントなので、レジスタ C が 0 ではない
間、サブシェル化した部分の先頭へジャンプすることになります。

♣ パレットを設定する
この節の最後に背景⽤パレット (BGP) を設定しておきます。
パレットについては先述の通りで、0xE4 を設定します。実装はリスト 2.5 の通りです。
▼ リスト 2.5: 02_bg/04_set_bgp.sh# ・・・省略・・・
main() {# ・・・省略・・・# タイルデータをVRAMのタイルデータ領域(0x8000)へロードする
lr35902_set_reg regDE $GB_ROM_FREE_BASE
lr35902_set_reg regHL $VRAM_TILE_DATA_BASE
lr35902_set_reg regC $TILE_SIZE(
lr35902_copy_to_from regA ptrDE
lr35902_copyinc_to_ptrHL_from_regA
lr35902_inc regDE
lr35902_dec regC
) >main.1.ocat main.1.olocal sz_1=$(stat -c '%s' main.1.o)
lr35902_rel_jump_with_cond NZ $(two_comp_d $((sz_1+2)))# ・・・追加(ここから)・・・# LCDを再開させる
lr35902_copy_to_regA_from_ioport $GB_IO_LCDC
lr35902_set_bitN_of_reg 7 regA
lr35902_copy_to_ioport_from_regA $GB_IO_LCDC# BGP設定
lr35902_set_reg regA $BGP_VAL
lr35902_copy_to_ioport_from_regA $GB_IO_BGP# ・・・追加(ここまで)・・・# 無限halt
infinite_halt}# ・・・省略・・・– 26 –

2.3 タイルをロードしてみる

パレット設定は VRAM とは別の単なるレジスタ設定なので、LCD 再開後でも構いません。そこで、LCD 再開の処理も追加して、その後で BGP 設定を⾏うようにしました。
シェル関数名や変数名からやっていることはわかるかと思いますが、「LCD を再開させ る 」の 箇 所 で は 、 lr35902_copy_to_regA_from_ioport $GB_IO_LCDC で

LCDC

レ ジ ス タ の 内 容 を レ ジ ス タA

へ ロ ー ド し た 後 、

lr35902_set_bitN_of_reg 7 regA で ビ ッ ト 7 の み セ ッ ト (LCD 有 効 化)

し、 lr35902_copy_to_ioport_from_regA $GB_IO_LCDC で LCDC レジスタへ書き戻しています。
「BGP 設定」の⽅は設定⽅法⾃体は LCDC のときと同様なので、コードの説明は省略します。

♣ 試しに動作確認
この時点で⼀度、動作確認してみます。
シ ェ ル ス ク リ プ ト を フ ァ イ ル へ リ ダ イ レ ク ト し て 、⽣ 成 し た ROM フ
ァ イ ル を エ ミ ュ レ ー タ で 実 ⾏ し て み て く だ さ い 。(前 項 時 点 の シ ェ ル ス ク
リ プ ト が 02_bg/04_set_bgp.sh に 保 存 さ れ て い る と し ま す 。ま た 、作 成 した tile.2bpp がカレントディレクトリに存在するようにしてください。)$ 02_bg/04_set_bgp.sh >02_bg/04_set_bgp.gb...
$ bgb 02_bg/04_set_bgp.gb...

実⾏すると図 2.1 の画⾯が表⽰されます。

– 27 –

第2章

背景にタイルを配置▲ 図 2.1: BGP 設定までの実⾏結果

Nintendo ロゴのみが表⽰されているデフォルト状態では、タイル番号 0 のタイル
データは真っ⽩のタイルだったのですが、今回そこへ⾃作のタイルのタイルデータを上書きしたため、背景のタイルマップでタイル番号 0 が指定されていた部分には⾃作のタイルが表⽰されるようになりました。

2.4

画⾯全体を⾃作タイルで敷き詰めてみる

では、この章の最後に画⾯全体を⾃作のタイル (タイル番号 0) で敷き詰めるように、背景のタイルマップ領域を初期化してみます。

♣ タイルマップ領域の使い⽅
LCDC レジスタ設定の際に、背景⽤のタイルマップ領域は 0x9800-9BFF(1024 バイト) のアドレスとしました。この領域をどのように使うかというと、画⾯左上から順に
タイル番号 (1 バイト) を並べるだけです。

GB の画⾯は、表⽰領域は 160x144 ピクセルで、8x8 のタイルで換算すると 20x18 タ– 28 –

2.4 画⾯全体を⾃作タイルで敷き詰めてみる

イルなのですが、実は⾮表⽰の領域もあり、スクリーン全体としては 256x256 ピクセ
ル (32x32 タイル) の⼤きさがあります。次章で紹介しますが LCD には画⾯スクロー
ル機能があり、その機能のレジスタを設定すると⾮表⽰の領域まで画⾯をスクロールできます。

0x9800-9BFF(1024 バイト) のタイルマップ領域は、各 1 バイトがスクリーン全体
32x32 タイル (全 1024 タイル) の各タイルと対応づいていて、この各バイトにタイル番号を設定すると、画⾯の対応する位置のタイルがそのタイル番号のタイルになる、というわけです。
ここでは、⾮表⽰の部分も含めて全てタイル番号 0 で初期化したいので、0x9800-

9BFF(1024 バイト) の領域を全て 0 クリアします。

♣ 実装
さっそくですが、実装してみるとリスト 2.6 の通りです。
▼ リスト 2.6: 02_bg/05_init_bg.sh# ・・・省略・・・
VRAM_TILE_DATA_BASE=8000
VRAM_BG_TILE_MAP_BASE=9800
VRAM_BG_TILE_MAP_END=9bff

# 追加
# 追加

BGP_VAL=e4
main() {# ・・・省略・・・# タイルデータをVRAMのタイルデータ領域(0x8000)へロードする
lr35902_set_reg regDE $GB_ROM_FREE_BASE
lr35902_set_reg regHL $VRAM_TILE_DATA_BASE
lr35902_set_reg regC $TILE_SIZE(
lr35902_copy_to_from regA ptrDE
lr35902_copyinc_to_ptrHL_from_regA
lr35902_inc regDE
lr35902_dec regC
) >main.1.ocat main.1.olocal sz_1=$(stat -c '%s' main.1.o)
lr35902_rel_jump_with_cond NZ $(two_comp_d $((sz_1+2)))# ・・・追加(ここから)・・・# 背景⽤タイルマップ(0x9800-9BFF)をタイル番号0で初期化する## 最終アドレス上位8ビット(0x9b)– 29 –

第2章

背景にタイルを配置

local end_th=$(echo $VRAM_BG_TILE_MAP_END | cut -c1-2)## 最終アドレス下位8ビット(0xff)
local end_bh=$(echo $VRAM_BG_TILE_MAP_END | cut -c3-4)
lr35902_set_reg regHL $VRAM_BG_TILE_MAP_BASE((# 0x00をレジスタHLの指す先へ設定し、HLをインクリメント
lr35902_clear_reg regA
lr35902_copyinc_to_ptrHL_from_regA# レジスタHと最終アドレス上位8ビットを⽐較
lr35902_copy_to_from regA regH
lr35902_compare_regA_and $end_th) >main.2.ocat main.2.olocal sz_2=$(stat -c '%s' main.2.o)# レジスタH != 0x9b なら# main.2.oのサイズ(+相対ジャンプ命令のサイズ)分戻る
lr35902_rel_jump_with_cond NZ $(two_comp_d $((sz_2+2)))# レジスタLと最終アドレス下位8ビットを⽐較
lr35902_copy_to_from regA regL
lr35902_compare_regA_and $end_bh) >main.3.ocat main.3.olocal sz_3=$(stat -c '%s' main.3.o)# レジスタL != 0xff なら# main.3.oのサイズ(+相対ジャンプ命令のサイズ)分戻る
lr35902_rel_jump_with_cond NZ $(two_comp_d $((sz_3+2)))# ・・・追加(ここまで)・・・# LCDを再開させる
lr35902_copy_to_regA_from_ioport $GB_IO_LCDC
lr35902_set_bitN_of_reg 7 regA
lr35902_copy_to_ioport_from_regA $GB_IO_LCDC# ・・・省略・・・}# ・・・省略・・・

追加の⾏数が少し多めですが、戦略としては「レジスタ HL へタイルマップアドレスを設定し、最終アドレス (0x9bﬀ) への設定完了まで、インクリメントしながら繰り返す」というものです。
ここで新たに登場したシェル関数は 2 つで、まず 1 つ⽬は lr35902_clear_reg です。これは、指定したレジスタをゼロクリアするシェル関数で、A レジスタ以外を指定– 30 –

2.4 画⾯全体を⾃作タイルで敷き詰めてみる

した場合、指定されたレジスタへ 0 を設定する命令を出⼒します。A レジスタについては xor でゼロクリアする命令を出⼒します*6 。
そして、2 つ⽬は lr35902_compare_regA_and で、「レジスタ A と⽐較する」命
令を出⼒するシェル関数です。LR35902 が持つ⽐較命令はレジスタ A との⽐較のみなので、このようなシェル関数名になっています。⽐較をするとそれに応じてフラグレジ
スタが変化するため、直後の条件付きジャンプ命令と組み合わせて使⽤します。
なお、⽐較が 8 ビットずつしか⾏えないため、「レジスタ H が最終アドレス上位 8
ビット (0x9b) と等しく無い間繰り返す (main.2.o のサブシェル部分)」と、それを囲む「レジスタ L が最終アドレス下位 8 ビット (0xﬀ) と等しくない間繰り返す (main.3.o のサブシェル部分)」の 2 段のループを作り、レジスタ H も L も最終アドレスと等しい場
合にループを脱出するようにしています。
ここではあくまでも、背景⽤タイルマップ領域を全て 0 で初期化できれば、その実装
⽅法は何でも良いです。(おそらくもっと良い実装もあるかも*7 )♣ 結果確認
エミュレータで実⾏すると、今度は図 2.2 のように画⾯全体が⾃作のタイルで初期化されました。

*6 xor 命令の⽅が 0 を設定する命令より命令バイト数が少ないためそのようにしています。LR35902 が持つ xor 命令は「レジスタ A と何かを xor した結果をレジスタ A へ格納する」というものであるた
めレジスタ A に関してのみ、xor でクリアするようにしています。
*7 ただし、16 ビット変数に 1024 を⼊れてデクリメントしていく⽅針の場合、注意が必要です。例えばレジスタ BC をカウンタ⽤として、そこに 1024 を⼊れて、デクリメントしながらループを回す実装が考えられますが、デクリメント命令は 16 ビット演算の際はフラグを設定しないので、相対ジャンプ
命令の条件にできません。結局、カウンタ⽤のレジスタ B と C を別々に 0 と等しいか⽐較する必要があります。

– 31 –

第2章

背景にタイルを配置▲ 図 2.2: 背景⽤タイルマップ初期化後の実⾏結果– 32 –

第3章

キー⼊⼒を試す

この章ではキー⼊⼒を使ってみます。例として⼗字キーに応じて画⾯スクロールするプログラムを作ってみます。

3.1

キー⼊⼒の取得⽅法

キー⼊⼒も LCDC やパレットと同様に専⽤のレジスタ (ジョイパッド,JOYP*1 ) から取得できます。8 ビットのレジスタで、各ビットについては表 3.1 の通りです。
▼ 表 3.1: キー⼊⼒のレジスタ (JOYP) について
ビット

役割

7
6
5
4
3
2
1
0

使⽤しない
使⽤しない
ボタン⼊⼒を選択 (0 で選択)
⽅向⼊⼒を選択 (0 で選択)
下/スタート状態 (0=押下)(読み取り専⽤)
上/セレクト状態 (0=押下)(読み取り専⽤)
左/B 状態 (0=押下)(読み取り専⽤)
右/A 状態 (0=押下)(読み取り専⽤)

ビット 5 と 4 で、ボタン (A/B/スタート/セレクト) の⼊⼒を取得するのか、⽅向 (上下左右) の⼊⼒を取得するのかを選択すると、下位 4 ビットで選択したキーの押下状態を取得できます。
*1 IO レジスタに対するレジスタ名は資料によりまちまちです。本書では本書末尾「参考にさせてもらった情報」に記載の「Everything You Always Wanted To Know About GAMEBOY」を参考にしています。

– 33 –

第3章

3.2

キー⼊⼒を試す

画⾯スクロールの⽅法

画⾯スクロールもレジスタ操作で⾏います。以下の 2 つのレジスタを使⽤します。

• SCY - 表⽰領域原点 (左上) の Y 座標を指定• SCX - 表⽰領域原点 (左上) の X 座標を指定
SCX と SCY で指定した座標を原点 (左上) として、そこから 160x144 ピクセルの領域を表⽰します。
また、それぞれのレジスタはオーバーフロー/アンダーフローした場合、境界をもう⽚⽅の端と⾃動的に合わせてくれるため、「右端より右へスクロールしたら左端が⾒える」という処理がハードウェア的に⾃動で⾏われます。

3.3

V ブランク割り込みを使⽤するキーの⼊⼒状態の取得とそれに応じてスクロールレジスタを更新すること⾃体は後は実装するだけなのですが、問題は定期的に処理されるようにするにはどうするかです。
その際に使⽤するのが割り込みですが、キー⼊⼒の割り込みはボタンを押し込んだ時と指を話した時しか割り込みを発⽣しないので、押しっぱなしの間に継続的に何かの処理を⾏うことが難しいです。
ここでは、V ブランク割り込み時にキー状態の確認とスクロールレジスタ更新を⾏う
ことにします。V ブランクは、ざっくり説明すると、ディスプレイが画⾯を更新する⼀連の流れの中で、画⾯を描画し終わってから次に画⾯を描画し始める間の期間です。V
ブランク割り込みは LCD がこの期間に⼊った際に発⽣する割り込みです。V ブランク
期間中、LCD は VRAM へアクセスしないので、動作中にタイルデータやタイルマッ
プ領域を更新したい場合にはこの期間で更新したりします。今回の場合、あるレジスタ
を読んで別のレジスタを更新するだけで、VRAM へアクセスしたりはしないのですが、画⾯描画に関するレジスタの更新なので、V ブランクと同期して実施すると都合が良いです。

V ブランク割り込みを使⽤するまでの流れは以下の通りです• 割り込みベクタに V ブランク割り込み時の処理を記載• 割り込みイネーブル (IE) レジスタで V ブランク割り込みを有効化する• 割り込み機能⾃体を有効化– 34 –

3.3 V ブランク割り込みを使⽤する♣ 割り込みベクタに V ブランク割り込み時の処理を記載
まず、これまで全て nop で埋めていた割り込みベクタに V ブランク割り込み時の処理を記載します。

V ブランクの割り込みベクタアドレスは 0x0040 です。そのため、V ブランクが発⽣した時、割り込みが有効であれば 0x0040 へジャンプしてきます。
ただ、今回はここには割り込みからリターンする命令だけを書きます。そして、

main() の halt 無限ループの箇所で、halt と相対ジャンプの間に「キー⼊⼒確認&スク
ロール更新」の処理を追加します。
なぜかというと、割り込みが発⽣後、割り込みからリターンするまでの間、その他の全ての割り込みが無効化されるため、⼀般的に割り込みからはなるべく早くリターンすべきで、今回の場合、必ずしも割り込み期間中に実施しなければならない処理では無いからです。
そこで利⽤するのが halt 命令の振る舞いです。halt 命令は、CPU の実⾏がこの命令に到達すると CPU を休ませますが、割り込みが発⽣すると halt 命令の次の命令へ

CPU の実⾏を進めます。そのため、halt 命令の後ろに処理を実装しておけば、割り込
み契機で実⾏されるが、その他の割り込みは⽌めていない状態で実⾏できます。
そんな訳で、ここでは V ブランク割り込みのベクタアドレスの位置には割り込みからリターンする命令だけを書きます。実装するとリスト 3.1 の通りです。
▼ リスト 3.1: 03_joypad/01_vblank_vec.sh# ・・・省略・・・
main() {# ・・・省略・・・}# ・・・変更(ここから)・・・# 割り込みベクタ⽣成## 0x0000 - 0x003F(64バイト)は0x00(nop)で埋める
dd if=/dev/zero bs=1 count=64## 0x0040(Vブランク)に割り込みからリターンする命令(reti)(1バイト)を配置
lr35902_ei_and_ret## 0x0041 - 0x00FF(191バイト)は0x00(nop)で埋める
dd if=/dev/zero bs=1 count=191# ・・・変更(ここまで)・・・# ・・・省略・・・

lr35902_ei_and_ret が、割り込みからリターンする命令 (1 バイト) を出⼒する

シェル関数です。0x0000 から 0x00FF の割り込みベクタ領域の中で 0x0040 のみにこ– 35 –

第3章

キー⼊⼒を試すの命令を配置し、その他の領域は全て 0 で埋めるようにしています。

♣ V ブランク割り込みを有効化する
個別に割り込みを有効化する割り込みイネーブル (IE) レジスタで V ブランク割り込
みを有効化します。

IE レジスタの各ビットの役割は表 3.2 の通りです。
▼ 表 3.2: IE レジスタについて
ビット

役割

7-5
4
3
2
1
0

使⽤しない
ボタン⼊⼒割り込み (1=有効)
シリアル割り込み (1=有効)タイマー割り込み (1=有効)

LCD ステータス割り込み (1=有効)
V ブランク割り込み (1=有効)

シリアル割り込みは、通信ケーブルでのシリアル通信に関する割り込みです。LCD
ステータス割り込みは、予め設定した LCD 状態になったら割り込みを発⽣させてくれるものです。

V ブランク割り込みはビット 0 です。1 をセットすると有効になります。
実装するとリスト 3.2 の通りです。
▼ リスト 3.2: 03_joypad/02_vblank_en.sh# ・・・省略・・・
main() {# ・・・省略・・・# BGP設定
lr35902_set_reg regA $BGP_VAL
lr35902_copy_to_ioport_from_regA $GB_IO_BGP# ・・・追加(ここから)・・・# Vブランク割り込み有効化
lr35902_copy_to_regA_from_ioport $GB_IO_IE
lr35902_set_bitN_of_reg 0 regA
lr35902_copy_to_ioport_from_regA $GB_IO_IE# ・・・追加(ここまで)・・・# 無限halt– 36 –

3.3 V ブランク割り込みを使⽤する

infinite_halt}# ・・・省略・・・

lr35902_copy_to_regA_from_ioport $GB_IO_IE で IE レジスタの値をレジス

タ A に取得し、 lr35902_set_bitN_of_reg 0 regA でレジスタ A のビット 0 に 1をセットした後、逆に書き戻しています。

♣ 割り込み機能⾃体を有効化
今は割り込み機能⾃体を無効化しているので、割り込みを有効化します。
割り込み機能の有効化の命令は lr35902_enable_interrupts という名前でシェル関数化しています。

halt に⼊る直前に有効化するようにします (リスト 3.3)。
▼ リスト 3.3: 03_joypad/03_ei.sh# ・・・省略・・・
main() {# ・・・省略・・・# Vブランク割り込み有効化
lr35902_copy_to_regA_from_ioport $GB_IO_IE
lr35902_set_bitN_of_reg 0 regA
lr35902_copy_to_ioport_from_regA $GB_IO_IE# ・・・追加(ここから)・・・# 割り込み有効化
lr35902_enable_interrupts# ・・・追加(ここまで)・・・# 無限halt
infinite_halt}# ・・・省略・・・

これで、V ブランクに⼊ると割り込みにより halt を抜けるようになりました。

– 37 –

第3章

キー⼊⼒を試す

3.4

⼗字キーに応じてスクロールする処理を実装する

それでは、本題の処理を実装していきます。

♣ ⼗字キー⼊⼒取得処理
まずは⼗字キーの⼊⼒取得です。キーの状態は先述の通り、ジョイパッドのレジスタから取得できます。
さっそく実装を紹介するとリスト 3.4 の通りです。
▼ リスト 3.4: 03_joypad/04_joyp.sh# ・・・省略・・・# ・・・追加(ここから)・・・
manual_scroll() {# ジョイパッド⼊⼒取得(⼗字キー)## ⼗字キーの⼊⼒を取得するように設定
lr35902_copy_to_regA_from_ioport $GB_IO_JOYP
lr35902_set_bitN_of_reg 5 regA
lr35902_res_bitN_of_reg 4 regA
lr35902_copy_to_ioport_from_regA $GB_IO_JOYP## ⼊⼒取得(ノイズ除去のため2回読む)
lr35902_copy_to_regA_from_ioport $GB_IO_JOYP
lr35902_copy_to_regA_from_ioport $GB_IO_JOYP## ビット反転(押下中のキーのビットが1になる)
lr35902_complement_regA## レジスタBへ格納
lr35902_copy_to_from regB regA}
# ・・・追加(ここまで)・・・
main() {# ・・・省略・・・# Vブランク割り込み有効化
lr35902_copy_to_regA_from_ioport $GB_IO_IE
lr35902_set_bitN_of_reg 0 regA
lr35902_copy_to_ioport_from_regA $GB_IO_IE# 割り込み有効化
lr35902_enable_interrupts# ・・・変更(ここから)・・・(# 割り込みがあるまでhalt– 38 –

3.4 ⼗字キーに応じてスクロールする処理を実装する

lr35902_halt# ⼿動画⾯スクロール
manual_scroll) >main.4.ocat main.4.olocal sz_4=$(stat -c '%s' main.4.o)
lr35902_rel_jump $(two_comp_d $((sz_4+2)))# ・・・変更(ここまで)・・・}# ・・・省略・・・

⼗字キーの⼊⼒を取得してそれに応じてスクロールレジスタを更新する処理は、
manual_scroll というシェル関数へ分けました。
main 関数では、「無限 halt」していた箇所を、halt 命令と相対ジャンプ命令へ分け、その間に manual_scroll を⼊れました。halt と manual_scroll をサブシェルに⼊れて⼀旦ファイル出⼒し、そのサイズを相対ジャンプ命令に使っています。(manual_scroll
後の相対ジャンプで halt 命令の位置まで戻るようにしています。)

manual_scroll には、ここではまず、⼗字キー⼊⼒の取得処理を実装しました。JOYP
レジスタのレジスタ値をレジスタ A へ取得し、ビット 5 に 1 を設定 (ボタン側を⾮選択)、ビット 4 に 0 を設定 (⼗字キー側を選択) し、レジスタ A を JOYP レジスタへ書
き戻すことで設定を反映させています。
その後、JOYP レジスタを⼊⼒状態確認のために読むのですが、設定が反映されるまでの時間なのか直後だと正しく読めないため、2 回読むようにしています*2 。

JOYP レジスタでは、押下状態のキーに対するビットが 0 になるので、今後の処理で扱いやすいように lr35902_complement_regA でビット反転しています。
そして、レジスタ A は別の処理でも使うのでレジスタ B へ値をコピーして、⼗字キー⼊⼒取得処理は完了です。

♣ スクロール処理
取得した⼗字キー状態に応じてスクロールレジスタを更新する処理を追加します。
ここでも実装で説明するため、さっそくコードを紹介します (リスト 3.5)。

*2 本書末尾「参考にさせてもらった情報」の「GameBoy CPU Manual」の「1. FF00 (P1)」(P.35)を参考にさせてもらいました。

– 39 –

第3章

キー⼊⼒を試す▼ リスト 3.5: 03_joypad/05_joyp_scrl.sh# ・・・省略・・・
manual_scroll() {# ・・・省略・・・## レジスタBへ格納
lr35902_copy_to_from regB regA
# ・・・追加(ここから)・・・# ⼗字キー押下状態に応じてスクロールレジスタ更新## 下キーチェック
lr35902_test_bitN_of_reg 3 regB(
# 下キーが押下中の場合# SCYをインクリメント
lr35902_copy_to_regA_from_ioport $GB_IO_SCY
lr35902_inc regA
lr35902_copy_to_ioport_from_regA $GB_IO_SCY) >manual_scroll.1.o
local sz_1=$(stat -c '%s' manual_scroll.1.o)## 下キーが押下中でない場合、押下中の処理を⾶ばす
lr35902_rel_jump_with_cond Z $(two_digits_d $sz_1)## 押下中の処理
cat manual_scroll.1.o## 上キーチェック
lr35902_test_bitN_of_reg 2 regB(
# 上キーが押下中の場合# SCYをデクリメント
lr35902_copy_to_regA_from_ioport $GB_IO_SCY
lr35902_dec regA
lr35902_copy_to_ioport_from_regA $GB_IO_SCY) >manual_scroll.2.o
local sz_2=$(stat -c '%s' manual_scroll.2.o)## 上キーが押下中でない場合、押下中の処理を⾶ばす
lr35902_rel_jump_with_cond Z $(two_digits_d $sz_2)## 押下中の処理
cat manual_scroll.2.o## 左キーチェック
lr35902_test_bitN_of_reg 1 regB(
# 左キーが押下中の場合– 40 –

3.4 ⼗字キーに応じてスクロールする処理を実装する# SCXをデクリメント
lr35902_copy_to_regA_from_ioport $GB_IO_SCX
lr35902_dec regA
lr35902_copy_to_ioport_from_regA $GB_IO_SCX) >manual_scroll.3.o
local sz_3=$(stat -c '%s' manual_scroll.3.o)## 左キーが押下中でない場合、押下中の処理を⾶ばす
lr35902_rel_jump_with_cond Z $(two_digits_d $sz_3)## 押下中の処理
cat manual_scroll.3.o## 右キーチェック
lr35902_test_bitN_of_reg 0 regB(
# 右キーが押下中の場合# SCXをインクリメント
lr35902_copy_to_regA_from_ioport $GB_IO_SCX
lr35902_inc regA
lr35902_copy_to_ioport_from_regA $GB_IO_SCX) >manual_scroll.4.o
local sz_4=$(stat -c '%s' manual_scroll.4.o)## 右キーが押下中でない場合、押下中の処理を⾶ばす
lr35902_rel_jump_with_cond Z $(two_digits_d $sz_4)## 押下中の処理
cat manual_scroll.4.o# ・・・追加(ここまで)・・・}# ・・・省略・・・

追加した⾏数は多いですが、同じような処理を 4 つ (上下左右) 実施しているだけです。
例えば下キーについて、まず、キー状態を格納したレジスタ B で、ビット 3(下キー)がセットされているか否かをチェックします。
押下中である場合に実施する処理をサブシェル内に⼊れて⼀旦ファイルに保存し、そのファイルサイズを相対ジャンプ命令に使っています。(キーが押下中でない場合はそのサイズ分だけ処理をスキップするようにしています。)
押下中の処理としては、下キーに対しては SCY をインクリメント、上キーに対しては SCY をデクリメント、左キーに対しては SCX をデクリメント、右キーに対しては

SCX をインクリメントしています。

– 41 –

第3章

キー⼊⼒を試す♣ 実⾏結果実⾏結果の⾒た⽬は前章の最後と変わらないため画像は省略します。
⼗字キーでの上下左右の⼊⼒に応じて画⾯がスクロールすることが確認できます。

– 42 –

おわりにここまで本書をお読みいただきありがとうございました！

C98 が中⽌となり電⼦/紙問わず本を書くことは中断していたのですが、エアコミケという形で実施されることを知り、急遽、今やっていることを本にしました。そのため、お⾒苦しいところや誤記など、残っているかもしれません (すみません)。
ただ、本書を通して低レイヤーの⾯⽩さ、エミュレータではあるけどハードウェアを直接操作する感動を少しでも感じてもらえれば幸いです。なお、本書で⽣成した ROM
イメージは、もちろん実機でも動きます。本書では紹介しきれませんでしたが興味があ
ればぜひ調べて試してみてください。
本書では、アセンブリ⾔語さえ使わずに ROM ファイルを⽣成してきました。結局のところ、欲しいのはバイナリで、バイナリさえ得られればその⼿段は「ソースコードから変換する」だけでなくても良いんだ、という低レイヤーの⾃由さ (?) みたいなところも伝わったら嬉しいです。
そして、8 ビットというレトロなコンピュータは、現代の 64 ビットに⽐べて「マシンを直接動かす機械語バイナリを⼿で書く」事が楽です。8 ビットというコンパクトさでありながら、画⾯描画やユーザ⼊⼒、サウンド、そして通信 (通信ケーブル) まである
ゲームボーイは、⾊々いじり回すのには⾯⽩いコンピュータなんじゃないかなと、最近は思っています。

– 43 –

参考にさせてもらった情報
GB のアーキテクチャや CPU について
• Everything You Always Wanted To Know About GAMEBOY◦ http://bgb.bircd.org/pandocs.htm◦ BGB が公開しているゲームボーイの仕様まとめ◦ 網羅的にまとめてありとても参考になります• GameBoy CPU Manual◦ http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf◦ ゲームボーイの CPU(LR35902) の仕様について PDF にまとめられています◦ 各 CPU 命令の動作などはこちらが分かりやすいです• Gameboy CPU (LR35902) instruction set◦ https://www.pastraiser.com/cpu/gameboy/gameboy_opcodes.
html◦ ゲームボーイの CPU である LR35902 の全命令の機械語コードが表の形で⾒やすくまとめられています• GameBoy Memory Map◦ http://gameboy.mongenel.com/dmg/asmmemmap.html◦ ゲームボーイのメモリマップはこちらが⾒やすいです• Gameboy 2BPP Graphics Format◦ https://www.huderlem.com/demos/gameboy2bpp.html◦ GB のピクセルフォーマットに関して説明しているページです◦ 16 バイト分の 16 進数を⼊⼒すると、どのような表⽰になるかを⾒せてくれるフォームもあります

bgb について• winetricks による Wine の⽂字化け (アルファベットが⾖腐) を解消 - Symfoware◦ https://symfoware.blog.fc2.com/blog-entry-2228.html◦ Wine で BGB を起動した際にアルファベットが⾖腐になってしまった問題
をこちらの記事を参考に解決しました– 45 –

シェルスクリプトでゲームボーイプログラミング⼊⾨
2020 年 5 ⽉ 5 ⽇
著

ver 1.0

者

⼤神祐真

印刷所

⽇光企画

© 2020 へにゃぺんて(powered by Re:VIEW Starter)