45
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ほぼ最強の8bitゲーム機「ゲームギア」プログラミング入門

Last updated at Posted at 2022-02-25

はじめに

ゲームギアは、8bitゲーム機としてはほぼ最後発のゲーム機です。

個人的に思い入れのある機種で、若い頃はドラゴンクリスタルを無限に遊んでいました。ですが、ファミコンやゲームボーイといった 8bitゲーム機 と比べるとマイノリティな方なので、ホームブリューでゲーム開発をする好事家が少なく、技術情報も少ない気がします。

しかし、よくよく見てみると技術的には優れていて、あのビルゲイツも当時興味を持っていたようです。

という訳で、ゲームギアの技術情報をザックリと解説して、HELLO, WORLD! を作ってみます。

1. ゲームギア概要

スペック

  • CPU: Z80A互換
  • VDP: セガカスタムLSI 315-5246 (TMS9918A + Mode4)
  • Sound: Digital Complex Sound Generator SN76489A
  • RAM: 8KB
  • VRAM: 16KB
  • CRAM: 64 bytes
  • ROM: 32KB ~ 1MB
  • LCD: 160×144px (STN方式)
  • 同時発色数: 4096色中32色
    • パレットはBG専用(パレット0)とスプライト・BG兼用(パレット1)の2種類x各16色
    • TVモードの場合は4096色同時発色

SEGA公式サイトにもハードウェア情報が公開されているのでリンクを貼っておきます。

なお、SEGA公式情報の「画素数」の仕様に関する記述(480×146ドット)ですが、恐らく仮想スキャンラインの非表示エリア(同期領域など)を含むピクセル数だと思われます。

ただし、スキャンラインを表示領域+同期用で+2本積んで「146ドット」というのは何となく理解できますが、水平方向の内部ピクセル数(1本のスキャンラインの画素数)は、ハードウェアマニュアルを読む限り「342px」らしいので、「480ドット」の数値根拠がイマイチよく分かりません。(水平同期用に+138pxとか?)

メモリサイズをビット表記にしたりなど、とにかく数字を大きく見せることで消費者に「性能が良い」と思わせる手法は当時としては普通のことだったので、恐らく当時のカタログスペックをそのまま載せていると思いますが、流石に全く根拠が無い情報を載せることは無いので、480pxにも何かしらの根拠はある筈ですが謎です。

2023.12.25 追記
480ドットの根拠ですが Twitter にてのっち様より以下のような情報を頂きました。 「なるほど、確かに!」 と思ったので引用させていただきます:bow:

1ピクセル表示するのに3画素(RGB)使うから
160x3=480
昔の液晶テレビってよくこういう書き方してたと思う。

また、ゲームギアのこと(主にLCD関連)については、佐藤秀樹さん(元セガ社長)のインタビュー記事に興味深いことが書かれていて面白かったので、参考までにリンクを貼っておきます。

ほぼ最強の根拠

本書表題で、ゲームギアを 「ほぼ最強の8bitゲーム機」 としている根拠を解説します。

1983年〜1990年までの間の主要なゲーム機の発売年と CPU/VDP(PPU) を下表に示します。

Year Name CPU VDP (PPU)
1983 ファミコン RP2A03 (6502系) RP2C02
1983 SG-1000 Z80A互換 TMS9918A
1985 セガマークIII Z80A互換 315-5124
(Mode4)
1987 PCエンジン HuC6280 (6502系) HuC6260 + HuC6270
1988 メガドライブ
(※16bit)
MC68000 + Z80A互換 315-5313
(Mode5)
1989 ゲームボーイ LR35902 (Z80系) LR35902
(CPUとワンチップ)
1990 ゲームギア Z80A互換 315-5246
(Mode4 ± α)
1990 スーパーファミコン
(※16bit)
RP5A22 (65C816系) S-PPU1 + S-PPU2

「スーパーカセットビジョンが無いぞ、けしからん!」などのご意見もあるかもしれませんが、ある程度売れた機種に絞ってます :bow:

そういった意味ではSG-1000も微妙なラインかもしれませんが...

ゲームギアのシステム構成はセガマークIII(1985)とほぼ同等です。

セガマークIIIのVDP (315-5124) は ヤマハ製 で、テキサス・インスツルメンツ社 の TMS9918A をベースに「Mode4」と呼ばれる新たな画面モードが追加されていて、ゲームギアのVDP(315-5246)はパレット機能の強化やLCD向けには不要な機能の削除などのカスタムがされています。

アーケードゲーム基盤 SEGA System II の特徴を追加したといった事が、佐藤秀樹さんのインタビュー記事に書かれています。

主要なテクノロジーとしてはファミコン(1983)とPCエンジン(1987)のちょうど中間的な位置づけになるものと考えられます。

1987年発売のPCエンジンが8bitゲーム機として最後発で、1988年以降はメガドライブやスーパーファミコンなどの 16bitゲーム機 が主流の時代になりました。つまり、最強の8bitゲーム機は「PCエンジン」だと考えられます。

PCエンジン は CMOS チップ上で 16bit 演算ができる 8bit CPU を採用しているので、厳密には8bit/16bitの中間的な位置づけかもしれませんが。

この当時のハンドヘルドは、16bitゲーム機の時代(1988年〜)に発売されていますが、技術的な事情で(主に省電力化やコストカットのため)、8bit時代の古いテクノロジーで作られていたものと類推できます。

ゲームボーイと同年(1989年)に発売された Atari Lynx は、CMOSチップ上で16bit演算ができるCPU(65C02)を搭載する攻めたテクノロジー構成になってますが、ほぼ普及しなかったので割愛します。Atariは最初のVCSがテクノロジー起因でコケたことがトラウマになっているのか(は定かではないですが)、このパターンが多い気がしないでもないです...Atari Jaguarとか。

古いゲーム機のゲーム開発(ホームブリュー)を楽しむ好事家界隈では、ファミコンやゲームボーイが圧倒的に人気があります。

ファミコン用ゲームの開発方法については、GitHubで記事を公開しているので、興味がある方はそちらをご参照ください。

ゲームボーイについては私は未経験ですが、海外でかなり人気があり、GitHubにも多くの情報があります。

https://github.com/gbdev/awesome-gbdev

ただし、私は 8bit ゲーム機の中では、ゲームギアのプログラミングが最もしやすいと考えています。

そのように考える根拠は、 画面解像度の仕様と 8bit プログラミングとの相性の良さ です。

ゲームギアは、LCD(液晶ディスプレイ)の都合で画面解像度が 160×144px という 8bit の据え置きゲーム機(※)よりも低解像度な制約があります。

※参考: 8bit の据え置きゲーム機の解像度

機種 解像度
Atari 2600 160x192
ファミコン 256x240
SG-1000 256x192
スーパーカセットビジョン 309x246
セガマークIII 256x240 or 256x224
PCエンジン 256×240, 336×240 or 512×240

しかし、ゲームギアのスキャンラインの内部的な本数はNTSCと同じ262本を1/60秒毎なのでVBLANK期間が長くなります。つまり、クロックレートが低い 8bit CPU でも余裕を持って 60fps で処理を実行できる メリットがあります。

ゲームボーイの解像度もゲームギアと同じ 160x144px ですが、スキャンラインの本数は 154本 なので、この点のメリットはほぼ無いようです... :cry:

また、ウィンドウ(可視)領域が狭いのに Name Table(BG面の仮想スクリーン)のサイズは据え置き機(セガマークIII)と同じなので、 仮想スクリーンの非可視領域が広い 特徴があります。これにより、スクロール処理などの実装が据え置き機と比べてシンプルになるメリットもあります。

下図の緑枠の部分がウィンドウ領域で、青色の矩形全体が仮想スクリーン領域です:
image.png

更に、ゲームギアは ピュアZ80でプログラミングが出来る ので、MSX、PC-88、X1などのZ80系PCでプログラミング経験があれば、少ない学習コストでゲーム開発ができます。

ゲームボーイのCPUも一応Z80系ではあるのですが、コストカットや省電力化のための魔改造 (I/O命令を撤廃してメモリマップドI/Oを採用、一部命令やレジスタを削除、メモリアクセスする命令で連続アクセス時にステージ短縮されることで1Hz命令サイクル短縮がされる特徴の削除...etc) がされています。

素人の浅知恵ですが、ここまで魔改造するなら、回路構造がZ80系よりもシンプルな6502系をベースに省電力化して搭載した方が手っ取り早かったのではないかと思っていたりしますが、それでもZ80系をベースにするメリットがあったのだろうか :thinking:

実はソフトウェア的な意味での技術的合理性はあまり無くて、リソース的な事情ではないかと推測してます。(任天堂もセガもチップセット類は自社製造ではないから、製造委託先の都合優先になる筈で、当時FC/SFCはリコー、GBはシャープ、セガのVDPはYAMAHAで、各々の会社に知見があるチップセットが採用されたのであろう...と想像)

「GB CPUとは何なのか?」という考察については、以下の記事が詳しいです。
https://www.wizforest.com/diary/120123.html

上記記事で省略されている Z80 と LR35902 の詳しい違いについては、私が開発した Z80 エミュレータのソースで isLR35902 で検索して頂けると分かりやすいかもしれません。 (Z80 のエミュレーションはほぼ完璧ですが、LR35902実装についてはドキュメントを読んで適当に作ってみただけなのでイマイチ自信がありませんが...)
https://github.com/suzukiplan/z80/blob/master/z80.hpp

以上のことから総合的に判断して、ゲームギアを「(プログラマにとって)ほぼ最強の8bitゲーム機」と定義しました。(少し苦しいかな ^^;)

本題から少し逸れますが、ファミコンやゲームギアの「いいとこ取り」をした FAIRY COMPUTER SYSTEM 80 という新しい 8bit ゲーム機を考案して、エミュレータをMITライセンスのOSSとして公開しています。

参考文献

本書は基本的に以下のハードウェアリファレンスマニュアルを参考にしてます。

如何せん文量が多くついでに全部英語なので、読むにはかなりのガッツを要しましたが、何とか一通り読んで咀嚼した内容を本書で解説しつつ、最後にサンプルプログラム(HELLO, WORLD!)の実装を紹介します。

2. 開発環境の準備(z88dk)

2-1. アセンブラ

プログラムはZ80アセンブリ言語で書くので、以前記事で書いた MSX の開発環境と同じ「z88dk」付属のアセンブラ(z80asm)を使います。

なお、以下の記事でC言語を用いたプログラミング方法が解説されているので、C言語を使いたい方は以下の記事を参考にして頂ければ良さそうです。

2-2. エミュレータ

デバッグ機能が豊富な Gearsystem がオススメです。

私は macOS 版を使っていますが、Windows と Linux にも対応しているようです。

ファミコンでいうところの mesen 相当の機能が揃っています。

mesen は Mac では使えないので、マカー向けにも環境が整っているのが有り難い :thumbsup:

3. メモリマップ

アドレス サイズ 用途
0x0000 ~ 0x3FFF 16KB Page0 (カートリッジROM)
0x4000 ~ 0x7FFF 16KB Page1 (カートリッジROM)
0x8000 ~ 0xBFFF 16KB Page2 (カートリッジROM / SRAM)
0xC000 ~ 0xDFFF 8KB Main RAM
0xE000 ~ 0xFFFF 8KB Main RAM (ミラー)

16KB 区切り (4ページ) になっていて、先頭3ページでバンク切り替えができます。

また、バッテリーバックアップ対応のカートリッジの場合、3番目のページ(Page2)をSRAMに切り替えることもできます。

バンク切り替えは、メモリアドレス 0xFFFC ~ 0xFFFF にマップされているコントロールレジスタに以下の値を書き込むことで実行できます。

アドレス ビット配列 用途
0xFFFC ----S-RR バンクコントロールレジスタ
S = 0: ROM, 1: SRAM
R = ROM オフセット
0xFFFD BBBBBBBB Page0 の バンク番号 (0 ~ 255)
※ハードウェアマニュアルではPage0はBank0で固定すべきという注記あり
0xFFFE BBBBBBBB Page1 の バンク番号 (0 ~ 255)
0xFFFF BBBBBBBB Page2 の バンク番号 (0 ~ 255)

ゲームギア ROM カセットの容量は、SEGAの公式情報によると 256K~8M とのことですが、市販されていたゲームギアのソフトで最もサイズが大きかったのは Sonic Blast の 1MB だと思われるので、多分ビット表記(バイト表記なら 32KB ~ 1MB)だと考えられます。

16KBバンクが256個なら最大4MBなので、理論MAXは4MBかもしれませんが、一旦メーカー公式仕様に倣って最大バンク数は64ということで上位2bitは 0 ということにしておきます。(「実機で4MBのROMが動いたよ!」とか「2MB や 4MB のゲームも市販されていたよ!」といった情報があれば、事実確認後に記事修正しますのでコメントで教えてください)

ちなみに、ハードウェアマニュアル上は上位3bitを 0 にすることと書かれており、それが事実の場合は最大 512KB (4Mbit) ということになるので、この点はハードウェアマニュアルの誤植ではないかと考えられます。

追記: 4MB ROM が実機で動いたとの情報を頂いたので、パンク番号は全ビット(0~255)使うことができるようです。(ギア太郎様、ご連絡いただきありがとうございます :bow:

コントロールレジスタを汚すことを回避するため、SP(スタックポインタ)は 0xDFF0 初期化すべきとのことです。

4. I/O マップ

ポート番号 I/O 用途
0x00 I bits: SRN-----
S: STARTボタンの入力 (0: ON, 1: OFF)
R: 地域 (0: 日本, 1: 海外)
N: 画面モード (0: NTSC, 1:PAL)
0x01 I/O 外部コネクタ入出力
0x02 I/O NMI割り込みコントローラ
0x03 I/O シリアル通信(送信データ)
0x04 I シリアル通信(受信データ)
0x05 I/O シリアル通信モード
0x06 O NL T3L T2L T1L NR T3R T2R T1R
サウンド(ノイズ、トーン1~3)の左右出力の有効(1)、無効(0)
0x30 I 開発ボード用
0x31 I 開発ボード用
0xDC I ジョイスティック入力(1)
bits: DWE UPE TR1 TL1 RI1 LE1 DW1 UP1
DWE: 外部入力の下カーソル
UPE: 外部入力の上カーソル
TR1: 右トリガー
TL1: 左トリガー
RI1: 右カーソル
LE1: 左カーソル
DW1: 下カーソル
UP1: 上カーソル
※0: ON, 1: OFF
0xDD I ジョイスティック入力(2)
外部入力の状態(省略)
0x7E I Vカウンタ(走査線の縦方向の描画位置)
O サウンドモジュール(SN76489)データポート
0x7F I Hカウンタ(走査線の横方向の描画位置)
0xBE I/O VDP(315-5245)データポート
0xBF I VDP(315-5245)ステータスフラグ
O VDP(315-5245)制御ポート

※ミラーポートは省略してます

特徴的なのは Vカウンタ の存在です。

8bit ~ 16bit 時代のゲームでは、走査線(スキャンライン)の位置を意識してプログラムを組む必要があることが割とよくあり、割り込みでプログラム的に同期するのが一般的です。ゲームギアでも同様に割り込みで同期することも出来ますが、Vカウンタを参照すれば割り込みを用いずにスキャンラインの垂直同期を実装できそうです。

Hカウンタの使い所は正直よく分からないです...多分、1命令で数〜数十ピクセルぐらい動くと思うので、画面上の水平同期用途では使え無さそうです。(Hブランク検出には使えるかもしれません)

5. VDP (315-5246)

5-1. 制御ポート

TMS9918Aと同様、2バイト連続で制御ポート (0xBF) に OUT をすることで VDP へのアクセス方法などを指定することができます。

 MSB                         LSB
 A07 A06 A05 A04 A03 A02 A01 A00    1回目の書き込み
 CD1 CD0 A13 A12 A11 A10 A09 A08    2回目の書き込み
  • CD* : Code Value
    • 0b00: VRAM読み込みアドレスを指定(データポート入力でVRAM読み込み)
    • 0b01: VRAM書き込みアドレスを指定(データポート出力でVRAM書き込み)
    • 0b10: VDPレジスタ更新 (A11~A08: レジスタ番号, A07~A00: 値)
    • 0b11: CRAM書き込み(データポート出力でCRAM書き込み)
  • A** : Address Value

5-2. VDPレジスタ

(1) 0x80 - モード制御#1

  • A07: 1 = BGの24~31行目の縦スクロールを無効化
  • A06 : 未使用(0をセット)
  • A05 : 未使用(0をセット)
  • A04: 1 = ライン割り込み有効化
  • A03: 1 = 全てのスプライトを左に8ピクセルシフトして表示
  • A02: 必ず 1 を設定する (Mode4有効化フラグ)
  • A00 : 未使用(1をセット)
  • A00 : 未使用(0をセット)

SEGAのハードウェアマニュアル(P.21)によると、必ずMode4で初期化するように記述されているので、市販されているゲームギアのソフトは恐らく全て Mode4 で作られているものと思われます。なので、以降の解説は全て Mode4 で初期化されていることを前提にします。

(2) 0x81 - モード制御#2

  • A07 : 未使用(1をセット)
  • A06 : 1 = 画面表示, 0 = 画面非表示.
  • A05 : 1 = フレーム割り込み有効化
  • A04 : 未使用(0をセット)
  • A03 : 未使用(0をセット)
  • A02 : 未使用(0をセット)
  • A01 : スプライトサイズ ... 1 = 8x16, 0 = 8x8
  • A00 : 未使用(0をセット)

(3) 0x82 - Name Table

このレジスタで、Name Table(BGのタイル配置情報テーブル)のVRAM上の先頭アドレスを指定します。

  • A07 : 未使用(1をセット)
  • A06 : 未使用(1をセット)
  • A05 : 未使用(1をセット)
  • A04 : 未使用(1をセット)
  • A03 : Name Table の A13 = 14ビット目 (213 = 8192)
  • A02 : Name Table の A12 = 13ビット目 (212 = 4096)
  • A01 : Name Table の A11 = 12ビット目 (212 = 2048)
  • A00 : 未使用(1をセット)

Mode4 の Name Table の仕様は次の通りです:

  • BGタイル: 8x8px x 512パターン
  • 1タイルのセルサイズ: 2バイト
  • セル数: 32(横) x 28(縦)
  • テーブルサイズ: 2 x 32 x 28 = 1792 bytes

セルのビット配列仕様は次の通りです:

    7654 3210
+0: NNNN NNNN ... 0バイト目(パターン番号)
+1: ---P CVHN ... 1バイト目(属性+パターン番号の9bit目)

- N: タイル番号 (0 ~ 511)
- H: 1 で 左右反転
- V: 1 で 上下反転
- C: 使用パレット (0: BG専用, 1: スプライト共有)
- P: 優先順位 (0: スプライトの背面, 1: スプライトの前面)

ファミコンだと属性情報が2bit(1バイトで4セル分)という面倒臭い構成になっているのに対して、属性情報をキレイに(1バイトで)指定できるのが有り難いですね。

(4) 0x83 - Color Table

  • ゲームギアでは使わない(TMS9918A 互換)
  • プログラムで必ず 0xFF をセットする必要がある

(5) 0x84 - BG Pattern Generator

  • ゲームギアでは使わない(TMS9918A 互換)
  • プログラムで必ず 0xFF をセットする必要がある

Mode4 の場合、パターンゼネレータは VRAM 全体(0x0000 ~ 0x3FFF) で、Name Table や OAM の領域に該当するキャラクタ番号を避ける形で使用する仕様のようです。つまり、パターンゼネレータの開始アドレスは必ず $0000 になります。

(6) 0x85 - Sprite (OAM)

このレジスタで、スプライトのオブジェクト属性情報(OAM)テーブルのVRAM上の先頭アドレスを指定します。

  • A07 : 未使用(1をセット)
  • A06 : OAM の A13 = 14ビット目 (213 = 8192)
  • A05 : OAM の A12 = 13ビット目 (212 = 4096)
  • A04 : OAM の A11 = 12ビット目 (211 = 2048)
  • A03 : OAM の A10 = 11ビット目 (210 = 1024)
  • A02 : OAM の A09 = 10ビット目 (29 = 512)
  • A01 : OAM の A08 = 9ビット目 (28 = 256)
  • A00 : 未使用(1をセット)

Mode4 の OAM のテーブルサイズは 256 (0x100) バイトです。

全64個のスプライト属性を次のように指定します。

オフセット 用途
0x00 ~ 0x3F スプライト0 〜 スプライト63 の Y 座標
0x40 ~ 0x7F 未使用領域
0x80 ~ 0xFF X座標(0), キャラクタ番号(0), X座標(1), キャラクタ番号(1)...
  • Mode4のスプライトは、パターン番号が小さいものが前面に描画されます
  • スキャンライン毎の水平方向表示上限は 8個 です
  • Y座標に 0xE0 を設定すると、そのスプライトの表示が無効化されます
  • Y座標に 0xD0 を設定すると、そのスプライト以降の表示処理が無効化されます
  • 未使用領域はキャラクタパターンとして使うことはできません

(7) 0x86 - Sprite Pattern Generator

このレジスタで、スプライト用キャラクタパターンテーブル(BGとも並用可能)のVRAM上の先頭アドレスを指定します。

  • A07 : 未使用(1をセット)
  • A06 : 未使用(1をセット)
  • A05 : 未使用(1をセット)
  • A04 : 未使用(1をセット)
  • A03 : 未使用(1をセット)
  • A02 : スプライト用キャラクタパターン の A13 = 14ビット目 (213 = 8192)
  • A01 : 未使用(1をセット)
  • A00 : 未使用(1をセット)

VRAM は 16KB (16384 bytes) なので、上半分 or 下半分の何れか一方をスプライト用キャラクタパターンテーブルとすることができます。(どちらかといえば、下半分を使う方がメモリ効率が良いかも?)

(8) 0x87 - 背景色

  • A07 : 未使用(0をセット)
  • A06 : 未使用(0をセット)
  • A05 : 未使用(0をセット)
  • A04 : 未使用(0をセット)
  • A03 : C03
  • A02 : C02
  • A01 : C01
  • A00 : C00

背景色に使用するスプライトパレット(パレット1)の色番号を 0 〜 15 の範囲で指定します。

このレジスタは、パワーオンのタイミングで 0x00 に初期化されます。

スプライトパレットの色番号0は透明色扱いになるので、基本的にこのレジスタは設定せず、スプライトパレットの色番号0で背景色を設定するのが良さそうです。

(9) 0x88 - BG スクロール X

  • A07 : HS7
  • A06 : HS6
  • A05 : HS5
  • A04 : HS4
  • A03 : HS3
  • A02 : HS2
  • A01 : HS1
  • A00 : HS0

BGの水平方向(256ピクセル)の基点座標を 0 ~ 255 の範囲で指定します。

このレジスタは、パワーオンのタイミングで 0x00 に初期化されます。

(10) 0x89 - BG スクロール Y

  • A07 : VS7
  • A06 : VS6
  • A05 : VS5
  • A04 : VS4
  • A03 : VS3
  • A02 : VS2
  • A01 : VS1
  • A00 : VS0

BGの垂直方向(224ピクセル)の基点座標を 0 ~ 255 の範囲で指定します。

このレジスタは、パワーオンのタイミングで 0x00 に初期化されます。

(11) 0x8A - ラインカウンタ

このレジスタを設定することで、任意のスキャンライン位置で割り込みを発生させることが出来ます。

  • A07 : VC7
  • A06 : VC6
  • A05 : VC5
  • A04 : VC4
  • A03 : VC3
  • A02 : VC2
  • A01 : VC1
  • A00 : VC0

このレジスタは、パワーオンのタイミングで 0xFF に初期化されます。

ハードウェアマニュアルでは「0x01に初期化される」と書かれていますが、実機では 0xFF になっているらしいので、実機動作の方を優先しました。

このレジスタを設定すると、ダウンカウンタと呼ばれる内部レジスタに値がロードされます。ダウンカウンタは、有効なスキャンラインのHブランクのタイミング(0xF4)で0だった場合に割り込みが発生して再びこのレジスタの値が再ロードされ、0でない場合は割り込みが発生せずにデクリメントされます。

つまり、0を設定すると毎スキャンラインで割り込みが発生し、1が設定されているデフォルト状態では2スキャンライン毎に割り込みが発生することになります。

5-3. CRAM (Color RAM)

ゲームギアのVDPには、VRAMとは別にパレット情報を保持する 64バイト の CRAM と呼ばれるメモリ領域が存在します。

オフセット ビットレイアウト 意味
+0x00 RRRRGGGG パレット0 色番号0 の赤と緑の色素(各0~15)
+0x01 ----BBBB パレット0 色番号0 の青の色素(0~15)
+0x02 RRRRGGGG パレット0 色番号1 の赤と緑の色素(各0~15)
+0x03 ----BBBB パレット0 色番号1 の青の色素(0~15)
: : :
+0x1E RRRRGGGG パレット0 色番号15 の赤と緑の色素(各0~15)
+0x1F ----BBBB パレット0 色番号15 の青の色素(0~15)
+0x20 RRRRGGGG パレット1 色番号0 の赤と緑の色素(各0~15)
+0x21 ----BBBB パレット1 色番号0 の青の色素(0~15)
: : :
+0x3E RRRRGGGG パレット1 色番号15 の赤と緑の色素(各0~15)
+0x3F ----BBBB パレット1 色番号15 の青の色素(0~15)

5-4. キャラクタパターン・テーブル

Mode4 では VRAM の全領域がキャラクタパターンテーブルになっています。

  • 1キャラクタの画素: 8x8ピクセル
  • 1キャラクタのレコードサイズ: 32 bytes
  • キャラクタ数: 512 (32 x 512 = 16KB)

各キャラクタのビットレイアウトは次のようになっていて、8x8px の各ピクセル (64px) のパレット番号(4ビット=0~15)で指定します。

オフセット ビットレイアウト
+0x00 1行目の 1 ビット目
+0x01 1行目の 2 ビット目
+0x02 1行目の 3 ビット目
+0x03 1行目の 4 ビット目
+0x04 2行目の 1 ビット目
+0x05 2行目の 2 ビット目
+0x06 2行目の 3 ビット目
+0x07 2行目の 4 ビット目
: :
+0x1C 8行目の 1 ビット目
+0x1D 8行目の 2 ビット目
+0x1E 8行目の 3 ビット目
+0x1F 8行目の 4 ビット目

6. HELLO, WORLD!

本章で解説するプログラム・リソース類は GitHub で公開中です。

6-1. キャラクタパターン & パレットデータの準備

256x512 ピクセル & 256色 の bmp ファイルから Mode4 のパターンデータとパレットデータのファイルを生成するプログラム (bmp2chr.c) を使います。

今回は、テキスト(HELLO, WORLD!)だけ表示できれば良いので、キャラクタ番号とASCIIコードを一致させたパターン画像を準備しました。
image.png

bmp2chr は、256色 bmp ファイルのパレットの先頭32色を CRAM 形式 (12bit color) にコンバートしつつ、各ピクセルの色番号を mod 16 した状態でキャラクタパターン形式 (4bit color) に変換する仕様です。

6-2. プログラム作成

org $0000

.init
    IM 1
    LD SP, $DFF0
    CALL init_vdp
    CALL load_pattern
    CALL load_palette

    DI
    ; 書き込みアドレス $22D4 (ここからHELLO,WORLD!を書けばだいたい画面真ん中)
    LD A, $D4
    OUT ($BF), A
    LD A, $62
    OUT ($BF), A
    EI

    LD C, $BE
    LD HL, text
    LD B, 24
    OTIR

    JMP end

text:
    DB "H", $00, "E", $00, "L", $00, "L", $00, "O", $00, ",", $00
    DB "W", $00, "O", $00, "R", $00, "L", $00, "D", $00, "!", $00

.init_vdp
    ; 温まるのを待つ
    IN      A,(07EH)
    CP      0B0H
    JNZ     init_vdp

    DI
    ; Register: モード制御 #1
    LD A, $86
    OUT ($BF), A
    LD A, $80
    OUT ($BF), A

    ; Register: モード制御 #2
    LD A, $C0
    OUT ($BF), A
    LD A, $81
    OUT ($BF), A

    ; Register: NameTable = $2000
    LD A, $F9
    OUT ($BF), A
    LD A, $82
    OUT ($BF), A

    ; Register: unused
    LD A, $FF
    OUT ($BF), A
    LD A, $83
    OUT ($BF), A

    ; Register: unused
    LD A, $FF
    OUT ($BF), A
    LD A, $84
    OUT ($BF), A

    ; Register: OAM = $2800
    LD A, $D1
    OUT ($BF), A
    LD A, $85
    OUT ($BF), A

    ; Register: SpritePattern = $0000
    LD A, $FB
    OUT ($BF), A
    LD A, $86
    OUT ($BF), A
    EI
    RET

.load_pattern
    DI
    LD A, $00
    OUT ($BF), A
    LD A, $40
    OUT ($BF), A
    EI
    LD D, 64
    LD C, $BE
    LD HL, $4000
    LD B, $00
load_pattern_loop:
    OTIR
    DEC D
    jnz load_pattern_loop
    RET

.load_palette
    DI
    LD A, $00
    OUT ($BF), A
    LD A, $C0
    OUT ($BF), A
    EI
    LD C, $BE
    LD HL, $8000
    LD B, 64
    OTIR
    RET

.end
    HALT
    JMP end

6-3. ビルド(アセンブル & ROM ファイル生成)

かなり冗長な構成ですが、次のような構成の ROM ファイルを出力します。

  • Bank0: hello.bin
  • Bank1: キャラクターデータ
  • Bank2: パレットデータ

以下、MacOS or Linux用の Makefile を示します。

all:
	clang bmp2chr.c -o bmp2chr
	./bmp2chr chrdata.bmp chrdata.vram chrdata.cram 
	z80asm -b hello.asm
	dd bs=16k conv=sync if=hello.bin of=hello_00.rom
	dd bs=16k conv=sync if=chrdata.vram of=hello_01.rom
	dd bs=16k conv=sync if=chrdata.cram of=hello_02.rom
	cat hello_00.rom hello_01.rom hello_02.rom > hello.gg

6-4. 実行結果

無事動きました :tada:

image.png

45
38
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?