第3章
キー入力を試す

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

3.1 キー入力の取得方法

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

[*1] IOレジスタに対するレジスタ名は資料によりまちまちです。本書では本書末尾「参考にさせてもらった情報」に記載の「Everything You Always Wanted To Know About GAMEBOY」を参考にしています。

表3.1: キー入力のレジスタ(JOYP)について

ビット役割
7使用しない
6使用しない
5ボタン入力を選択(0で選択)
4方向入力を選択(0で選択)
3下/スタート状態(0=押下)(読み取り専用)
2上/セレクト状態(0=押下)(読み取り専用)
1左/B状態(0=押下)(読み取り専用)
0右/A状態(0=押下)(読み取り専用)

ビット5と4で、ボタン(A/B/スタート/セレクト)の入力を取得するのか、方向(上下左右)の入力を取得するのかを選択すると、下位4ビットで選択したキーの押下状態を取得できます。

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ブランク割り込みを有効化する
  • 割り込み機能自体を有効化

割り込みベクタに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のみにこの命令を配置し、その他の領域は全て0で埋めるようにしています。

Vブランク割り込みを有効化する

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

IEレジスタの各ビットの役割は表3.2の通りです。

表3.2: IEレジスタについて

ビット役割
7 - 5使用しない
4ボタン入力割り込み(1=有効)
3シリアル割り込み(1=有効)
2タイマー割り込み(1=有効)
1LCDステータス割り込み(1=有効)
0Vブランク割り込み(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
        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を抜けるようになりました。

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
                lr35902_halt

                # 手動画面スクロール
                manual_scroll
        ) >main.4.o
        cat main.4.o
        local 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

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

JOYPレジスタでは、押下状態のキーに対するビットが0になるので、今後の処理で扱いやすいようにlr35902_complement_regAでビット反転しています。

そして、レジスタAは別の処理でも使うのでレジスタBへ値をコピーして、十字キー入力取得処理は完了です。

スクロール処理

取得した十字キー状態に応じてスクロールレジスタを更新する処理を追加します。

ここでも実装で説明するため、さっそくコードを紹介します(リスト3.5)。

リスト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
        (
                # 左キーが押下中の場合

                # 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をインクリメントしています。

実行結果

実行結果の見た目は前章の最後と変わらないため画像は省略します。

十字キーでの上下左右の入力に応じて画面がスクロールすることが確認できます。