プログラミング
CHIP-8の芸術
The Art of Chip-8 (beyondloom.com)
要約
CHIP-8は、1977年のCOSMAC VIPキットコンピュータ用に開発されたプログラミング言語であり、その命令セットとエミュレータの原理について解説しています。多くのCHIP-8実装が存在するものの、互換性の問題が指摘されており、本記事ではOctoアセンブラの記法を用いながら、移植性の高いプログラミングアプローチに焦点を当てて解説します。
全文翻訳
CHIP-8の芸術
CHIP-8は、1977年のCOSMAC VIPキットコンピュータ用に元々開発されたプログラミング言語です。
CHIP-8プログラムは、単純な仮想命令セットアーキテクチャのマシンコードに似た一連の2バイト命令で構成されているため、CHIP-8インタプリタは「エミュレータ」とも呼ばれます。
実際、CHIP-8インタプリタを書くことは、古いコンピュータやゲームコンソールのエミュレータの基本原則を学ぶのに最適であり、その結果、ほぼすべての考えられるプラットフォームで数千ものCHIP-8ランタイムが利用可能になっています。
歴史的なプラットフォームが生き残るためには、それらのために新しいソフトウェアを書く必要があります。
CHIP-8実装の増加は、半世紀にわたる伝言ゲームがインタプリタ間で広範な動作のばらつきを生み出したため、多くの混乱を引き起こしています。
私の高水準CHIP-8アセンブラであるOctoを開発する過程で、私は現存するCHIP-8の様々なフレーバー間の一般的なばらつきを捉える「クワークスフラグ」を普及させ標準化するのを助け、影響力のあるインタプリタの多くの暗く、仕様外の領域を調査しました。
現在、CHIP-8インタプリタとそのバリアントのための成熟したテストスイートが利用可能であるため、現代のインタプリタが詳細を間違える言い訳はありません。
それでも、実際のCHIP-8の現実は断片的です。多くのプラットフォームのインタプリタは、より広範なホビイストコミュニティを認識していない初心者が書き、PONG.CH8を(正しく)実行できるようになったらすぐに放棄されます。
この記事では、命令セットとしてのCHIP-8と、新しいプログラムを書く上での実際的な意味について考察し、私が過去に書いた多くの散在するチュートリアル、例、FAQを蒸留します。
特に、バグが最も少なく、最も不完全な既存のCHIP-8インタプリタを除くすべてで移植可能なアプローチを指摘します。
例コードはOctoの記法を使用します。このドキュメントはOctoアセンブリ言語の完全なリファレンスマニュアルを意図したものではありませんが、新しいアイデアに出会ったときに説明するよう努めます。
インデックス
システム概要
算術命令
メモリ
サブルーチン
制御フロー
入力
乱数
タイミング
出力
次へ進む場所
さらなる読書
システム概要
CHIP-8は12ビットのアドレス空間で動作します。
元のCHIP-8インタプリタは、この空間の最初の512バイトに常駐し、プログラムはアドレス0x200から開始します。
また、スタック、フレームバッファ、およびいくつかのスクラッチパッドのために、アドレス空間の上部領域の一部を予約していました。
その結果、コードとデータのために最大3232バイトが残されています。
現代のCHIP-8インタプリタはしばしばより寛大で、ユーザープログラムのために最大3584バイトを残し、しばしば低位512バイトをメモリに格納するために使用します。これは、それらの16進フォント、または何も格納しないため、プログラムが操作できる状態にします。
コードとデータは、最大限の移植性のために3232バイト以内に収まる必要があります。
v0-vfという名前の16個の汎用8ビットレジスタのファイルがあり、プラットフォームに心地よいRISC風の感触を与えています。
12ビットの「インデックスレジスタ」iは、メモリを参照または操作するすべての操作に使用されます。
サブルーチンのリターンアドレスをスレッド化するための内部スタックがありますが、それは不透明です。命令セットは、プログラムが一時値を自由にプッシュまたはポップしたり、スタックの内容を検査したりすることを許可しません。
プログラムは、16進キーパッドから入力を受け取ります。
出力のために、64x32ピクセルの1ビットビットマップディスプレイと、ノイズを出すためのシンプルなピエゾブザーがあります。
また、割り込みのない遅延タイマーと乱数ジェネレータも利用できます。
CHIP-8には34個の基本的な命令があります。
以下の説明では、vxとvyは任意のvレジスタであり、NNN、NN、Nはそれぞれ即値の12ビット、8ビット、または4ビット値です。
マシンコード
Octo構文
ノート
00E0
clear
ディスプレイをクリアします。
00EE
;
または return
サブルーチンを終了します。
1NNN
jump NNN
2NNN
NNN または :call NNN
サブルーチンを呼び出します。
3XNN
if vx != NN then
条件付きスキップ。
4XNN
if vx == NN then
条件付きスキップ。
5XY0
if vx != vy then
条件付きスキップ。
6XNN
vx := NN
7XNN
vx += NN
8XY0
vx := vy
8XY1
vx |= vy
ビットワイズ OR。
8XY2
vx &= vy
ビットワイズ AND。
8XY3
vx ^= vy
ビットワイズ XOR。
8XY4
vx += vy
vf はキャリー時に1、それ以外の場合は0。
8XY5
vx -= vy
vf はボロー時に0、それ以外の場合は1。
8XY6
vx >>= vy
vf は古い最下位ビットを取得します。
8XY7
vx =- vy
vf はボロー時に0、それ以外の場合は1。
8XYE
vx <<= vy
vf は古い最上位ビットを取得します。
9XY0
if vx == vy then
条件付きスキップ。
ANNN
i := NNN
BNNN
jump0 NNN
v0 + NNN のアドレスにジャンプします。
CXNN
vx := random NN
NN とビットワイズ AND されたランダムバイト。
DXYN
sprite vx vy N
ディスプレイに描画します。vf は衝突時に1、それ以外の場合は0。
EX9E
if vx -key then
キーが押されていないか?
EXA1
if vx key then
キーが押されているか?
FX07
vx := delay
FX0A
vx := key
キープレスを待ちます。
FX15
delay := vx
FX18
buzzer := vx
FX1E
i += vx
FX29
i := hex vx
vx の16進文字スプライトに i を設定します。
FX33
bcd vx
vx をBCD (Binary-Coded Decimal) にデコードします。
FX55
save vx
v0-vx をアドレス i から i+x のメモリに保存します。
FX65
load vx
v0-vx をアドレス i から i+x のメモリからロードします。
すべての命令は2バイト幅であり、コンポーネントフィールドはニブル(4ビット)単位で整列されています。これらの特性は、ペンと紙で手作業でアセンブルするプログラムを容易にし、一部の形式の自己変更コードの作成を簡素化するのに役立ちます。
次のセクションでは、これらの命令を機能的な目的別にグループ化して、より詳細に説明します。
算術命令
2つの算術命令は即値引数を取ります。
マシンコード
Octo構文
6XNN
vx := NN
7XNN
vx += NN
残りは2つのレジスタを操作し、vxが結果を格納します。
マシンコード
Octo構文
8XY0
vx := vy
8XY1
vx |= vy
8XY2
vx &= vy
8XY3
vx ^= vy
8XY4
vx += vy
8XY5
vx -= vy
8XY6
vx >>= vy
8XY7
vx =- vy
8XYE
vx <<= vy
Octoは、これらの操作のためにCファミリー言語に似た構文を使用し、対称性のためにPascalスタイルの := 代入演算子を使用します。
^=、=-、<<=、>>= 命令は元のCHIP-8インタプリタに存在しましたが、文書化されていませんでした。それらはRCA-1802命令エンコーディングの自然な結果として生まれました。
=- 命令は -= と似ていますが、vyからvxを引くのではなく、vxからvyを引きます。常に結果はvxに格納されます。
シフト命令は、vxをvyを1ビット左または右にシフトした値に設定し、シフトアウトされたビットをvfに格納することを意図しています。
多くの現代のインタプリタは、vyを無視してvxをインプレースでシフトするように、これらの命令を誤って実装しています。
vx <<= vx および vx >>= vx の形式のシフト命令のみを使用してください。
両方の引数に同じレジスタを使用すると、「シフトクワークス」の有無にかかわらず一貫した動作が得られます。
一部のインタプリタでは、ビットワイズ操作 |=、&=、および ^= は副作用としてvfを変更します。
この問題は「シフトクワークス」よりもあまり知られておらず、エミュレートされたCOSMAC VIPでプログラムを実行しようとすると、かなりの驚きとなる可能性があります。
ビットワイズ命令 vx |= vy 、 vx &= vy、および vx ^= vy は vf の内容を破壊すると仮定してください。
ビットワイズ NOT を行う必要がある場合は、レジスタ内の適切な定数とのXORを使用できます。
vf レジスタは、定数が一度だけ必要な場合に理想的な選択肢です。
vf := 0xFF
v0 ^= vf # v0 のビットを反転します
多くのインタプリタは、算術命令のメイン結果の前または後にキャリーフラグの結果を書き込むかどうかに一貫性がなく、宛先レジスタ(vx)がvfである場合に曖昧さを生じさせます。
vf := NN、vf += NN、または vf := vx 以外の場合、算術命令の宛先として vf レジスタを使用しないでください。
「ノープ(no-op)」命令が必要な場合は、vx := vx の形式の命令、例えば v0 := v0 が最良の選択肢です。
vx += 0 の形式の命令も機能します。これは重要な潜在的な「落とし穴」につながります。
16ビットカウンタをインクリメントしようとしているとします。
v0 += 1 # 低位バイトをインクリメントします
v1 += vf # 高位バイトへのキャリー
このスニペットの動作は未定義です。なぜなら、vレジスタに即値を加算してもキャリーフラグ vf は変更されないからです。
元のバージョンが、vfにすでにあったものに依存して、時々機能するように見えるという事実は、さらに腹立たしいだけです。
あなたはこれを行うつもりでした。