Rustで創る MOS 6502 CPU その1
2024.07.19
どもです。
すごい、久々です。
更新が途絶えたと思わせつつの復活です。
いやぁ。最近、本当に色々と慌ただしい日々を過ごしておりました。
振り返れば、去年前半はFlutterのゲームエンジンFlameを、後半はずっぽりRustにハマってしまい、Rust教と化していましたが、今年に入って再びTypeScriptで実務作業。
社内で使用するライブラリをVue jsのReactiveのコア部分の仕組みを真似て、Reactiveなモーダルウィンドウの作成などしておりましたが、気がついたらUnityのuGUI実装ですよ。
今年後半はガッツリC#でGUI作成ですかね。
UnityでGUI作成している感想ですが、フロント実装でアーキテクチャやフレームワークが定まっていない(まだ登場していない)時代に、MVCだのMVPだのでバニラJSに近い状態で一生懸命実装していた頃の様な感覚です。
実際、UniRxなど用いて、MVPの実装が主流ということで、なんとかPresenterの層を機械的(ルールではなく)、仕組み的になくせないか、隠蔽できないかと言うことで、楽な形でMVVMフレームワークを適応、または自作できないかと思い色々模索しておりました。何個かAssets Storeに配信されているパッケージも試してみましたが、uGUIとの相性の悪さもあり、楽な形にはなかなかたどり着かず、より複雑になってしまいそうだったので断念しました。
ということで、uGUIでゴリゴリUI実装していくのはなかなか苦行に近いのですが、Unity公式も方も新たなUIツールとして、UI Tool Kitを推奨しているとのことでしたのでこちらを試したところ、フロント実装そのまんまの印象でした。
こちらがスタンダードの形になれば、MVVMフレームワークも自然と生まれてきそうな気がしまして、その辺の調査検証もプレイベートも含め、進めて行こうかと思う次第ではあります。
ということで、ローレベルの作業がしたいとRustで実務を行っていたのですが、気がついたら高レイヤーの作業に戻ってしまいC#という高級言語と共にゲームエンジンも引き連れて、今年後半を過ごして行きそうです。
もちろん、そちらの記事も書けて行ければと思いますが、流石にUnityの記事はかなり世に出ている様子ではありますので、今更自分が書いてもということあり、多分UI Tool Kitメインの記事になるのかなといった印象です。
そんなこんなあって、業務では高レイヤーですが、プライベートではやっぱり低レイヤーを触っていたいという気持ちなので、何番煎じか分かりませんが「ファミコンエミュレータ」でも作成しよう。と思い立ったわけです。
以前、Rustで「ゲームボーイエミュレータ」を作成したこともあって、ファミコンもいけるだろうと思ったのですが、歳のせいかなかなか覚えが悪いw
まずは、CC65を用いて、C言語でファミコンプログラムをやるか。と思ったのですが、特に作りたいもゲームはなく(試したいのはあるが)、ガッツリ制作に意欲が沸かなかったのもあり、シンプルにCPUから制覇するか。という気持ちになりました。
ファミコンエミュレータだと、CPU以外にも、PPUもAPUと考えないといけないのもあるので、まずはCPUを知ると良いだろう。という判断。
というか、「Writing NES Emulator in Rust」を行っていた途中で、 interactive tutorial on 6502 instructionsに立ち寄ってしまい、道が逸れてしまった感はあります。
こちらの interactive tutorial on 6502 instructionsは、ブラウザ上で6502 ニーモニック(アセンブリ言語)を記述しビルドし実行しながら6502を学べるという神的サイト。
しかしながら、全部英文ということもあってすんなり入りにくいところはあります。
そういえば。。と思い出したのが、以前購入していたこちらの本
前に読んでいたのですが、途中で別の本を読み始め忘れていたのですが、再度読み直しているのですが、6502を知りたければこちらの本を読むべきである。
理解しづらかったところがすらすらと頭に入ってくる。
正に、MOS 6502の説明書、いやバイブルである。
そんな事を含め、会社のメンバーにMOS 6502の魅力を熱く語っていたのですが、何を言ってんだこのアセンブラおじさんは?的な反応しか返ってこなかった。
おぉ。世の中すべてのエンジニアは最終的にアセンブラに到着するかと思われがちですが、どうやら違うみたいです。皆さんそれが分かって良かったですね。
と、話は戻しまして、
やはり、MOS 6502の魅力を次の世代にも残していくことを宿命を感じながら、まずはマスターしようということで、このシリーズはだらだらと続けて行ければと思っております。
網羅できるドキュメントのスタイルは別途検討するとして、こちらは学んだことを忘れないようにとメモ程度となりますが、良ければ観ていって貰えればと思います。
ということで、今回は上記の書籍を参考に、アドレッシングモードについてのまとめをと。
アドレッシングモードのまとめ
- アドレッシングモードとは → プログラムが参照するメモリのアドレスを指定する方法
- 同じ名前の命令の中に、アドレッシングモードによって色々なバリエーションが含まれる
- 命令によって使えるアドレッシングモードの種類が異なる
- とにかく、強力なアドレッシングモードを持つ6502
● 一番単純なアドレス指定方法
メモリの物理的な番地を直接数値で指定
例:
4桁の16進数を使って、0800番地のメモリの値をアキュムレーターに読み込む
「アブソリュート(absolute)」アドレッシングモード
各モードは以下の通り
アキュムレーターモード
- 特別かつ単純なモード
- 6502ではAレジスターだけで完結。メモリアドレス指定なし
簡略式
A ← A’
アキュムレーター(Aレジスター)の値をシフトや回転によって変更したものを再び同じアキュムレーターに戻す。
ASL (Aritmetric Shift Left) LSR (Logical Shift Right) ROL (Rotate Left) ROR (Rotate Right)
の4つ
イミーディエイト
- immediateとは直接
- 命令のオペランドとして直接指定した値を処理の対象
- 例:レジスターの値を直接指定、何かの値とレジスターの値を比較、レジスターの値と直接指定する値の間で演算を実行
使える命令は大別すると3種類。
- 指定した値を直接レジスターにロードするもの
- レジスター内の値を直接指定した値と比較するもの
- レジスター内の値と直接指定した値との間で様々な演算実行し、結果をまたレジスターに入れる演算命令
2つのインデックスレジスター、XとYについては、ロード命令と比較命令のみ使える。
LDA (Load Accumulator) LDX (Load X Register) LDY (Load Y Register)
イミーディエイト値は先頭に「#」付与。
A,X,Yの各レジスターをRと、16進数の値を「$xx」と表すと、イミーディエイトのロードは
R ← #$xx
レジスター内の値と、直接指定した値を比較する命令もA,X,Yの各レジスターに1つずつある。
CMP (Compare) CPX (Compare X Register) CPY (Compare Y Register)
比較命令
引き算実行、その結果に応じてステータスレジスターの値を変化。結果自体は捨てる。
N, Z, C ← R ← #$xx
変化可能性ビット
- ネガティブ(N)
- ゼロ(Z)
- キャリー(C)
残りは、Aレジスターのみに有効な演算命令
足し算、引き算、AND、OR、XORの3種類のビットごとの論理演算
ADC (Add with Carry) AND (Logical AND) EOR (Exclusive OR) ORA (Logical Inclusive OR) SBC (Subtract with Carry)
何らかの演算を「・」で表現
A ← A ・ #$xx
アブソリュート
命令のオペランドで16ビットの絶対アドレスを直接指定するモード
6502には16ビットのレジスターはないので指定したアドレスのメモリ内の8ビットの値をレジスターにロードしたり、レジスターとの間の演算に使ったりする。
サブルーチン呼び出す命令はこのモードしか使えない
JSR (Jump to Subroutine)
6502のジャンプ命令は、アブソリュートモード以外にもう一つ、16ビットで指定したアドレスのメモリ内容が示すアドレスにジャンプするインダイレクト(間接)モードが使える。
JMP (Jump)
アブソリュートモードは他に使える命令として、ロード・ストア命令がある。A,X,Yの3つの8ビットレジスター用に用意されている。
LDA (Load Accumulator) LDX (Load X Register) LDY (Load Y Register) STA (Store Accumulator) STX (Store X Register) STY (Store Y Register)
これらのロード命令は、16ビットの値で指定したメモリアドレスから、1バイトの値を読み出して、それぞれのレジスターに上書き。ストア命令はその逆。
簡略式
R ← ($xxxx) ($xxxx) ← R
3つのレジスターに対する比較命令
CMP (Compare) CPX (Compare X Register) CPY (Compare Y Register)
各レジスターに入っている値と、16ビットのアドレスで指定したメモリの内容の8ビットを比較。
N, Z, C ← R - ($xxxx)
Aレジスターに対する加算/減算命令
ADC - Add with Carry SBC - Subtract with Carry
Aレジスターとの間の3種類の論理演算命令
AND (Logical AND) EOR (Exclusive OR) ORA (Logical Inclusive OR)
5つの演算命令をまとめて陥落式にすると
A ← A ・ ($xxxx)
シフト/回転命令は、どのレジスターも介さず、直接メモリの値をシフトしたり回転したりする
($xxxx) ← ($xxxx)’
イミーディエイトモード ☓
アブソリュートモード ◯
指定したアドレスのメモリ内容をビット単位で調べる。
BIT (Bit Test)
- 調べたいビットだけ1にセットした値をAレジスターに
- それとメモリの内容のANDを取る
簡略式
N, V, Z ← A ^ ($xxxx)
この命令はオーバーフローフラグ(V)が変化
そのほかに1つずつ増減の命令
DEC - Decrement Memory INC - Increment Memory ($xxxx) ← ($xxxx) + 1 ($xxxx) ← ($xxxx) - 1
ゼロページ
ページとはメモリを256バイト単位に区切った1つの範囲。
ゼロページとは$0000から$00FFまでの256バイト。
1ページは$0100から$01FFまでの256バイト。
この領域は固定のスタック領域で、全部で256ページある。
6502では、256個の8ビットレジスターと同じ様に使える極めて重要なメモリ領域。
ゼロページのメモリ領域ではアブソリュートモードよりも少ないクロック数で命令を実行できる。
ゼロモードで使える命令の種類はJMPとJSRを除いたアブソリュートで使える命令が使える。
直接メモリのアドレス指定は8ビット。$xxxxを$00xxに入れ替え。
使える命令
3つのレジスターに対するロード/ストア命令、LDA、LDX、LDY,STA、STX、STY
3つのレジスターに対する比較命令、CMP、CPX,CPY
Aレジスターに対する、加算/減算命令、ADC、SRC、論理演算命令、AND、EOR、ORA
ゼロページ内のメモリ内容のビットを直接シフトしたり回転したりのASL、LSR、ROL、ROR
BIT、DEC、INC
インデックスト・アブソリュート
アドレッシングモードに「インデックスト(Indexed)」が付くと、インデックスレジスターによって「修飾」するという意味。
何らかの方法で指定したアドレスに、インデックスレジスターの値を足したものを目的のアドレスとする。
- アブソリュート
- ゼロページ
- インダイレクト
がある。
インデックスレジスターが2つあるので、Xで修飾する場合と、Yで修飾する場合の2通りある。
Xレジスターで修飾できるものが多い。
X, Yレジスターで修飾するにしても、このインデックス・アブソリュートは、
16ビットの値で直接指定するメモリアドレスに、XまたはYレジスターの値を足したアドレスを実効アドレスとするもの。
簡略式
($xxxx + X) または、($xxxx + Y)
命令のオペランドで指定した16ビット値にXまたはYレジスターの値を加えたアドレスのメモリ内容を読み取ったり、そこに書き込んだり、演算に使用したり、直接操作したりする。
● アブソリュートモードで使用でき、インデックス・アブソリュートで使用できない命令
BIT、CPX、CPY、JMP、JSR、STX、STY
● Xレジスターのみ修飾できる命令
DEC、INC、ASL、LSR、ROL、ROR
LDX、LDYは互いに異なるインデックスレジスターのみ修飾できる。
それ以外のAレジスターのロード、ストア命令、LDA、STA、加算、減算命令、ADC、SBCや3種類の論理演算命令、AND、EOR、ORAなどについては、Xレジスター、Yレジスターいずれでも、インデックス・アブソリュートで使用可能。
インデックスト・ゼロページ
8ビットの値で直接指定するゼロページのメモリアドレスに、XまたはYレジスターの値を足したアドレスを、実効アドレスとするもの。
このモードによってアクセスできるのは、あくまでもゼロページの範囲内。
8ビットの値で直接指定するゼロページのアドレスは、$00から$FFの範囲。
X、Yレジスターで指定するインデックスの値も同じ。
元のアドレスにインデックスを加えた値の範囲は$00から$1FF
約半分、ゼロページのアドレスの範囲をはみ出す。
その場合、合計値の下位8ビットが表すゼロページアドレスが、実効アドレスとなる。
常に合計の下位8ビットが実効アドレスとなる。
簡略式
(($00xx + X) ^ $00FF)
Yレジスターを使ったインデックスト・ゼロページモードで使える命令は2つ。
Xレジスターのロード、ストア命令、LDXとSTXだけ。
簡略式
X ← ($00xx + Y) ($00xx + Y) ← X
● ゼロページ使えるが、Xレジスターを使ったインデックスト・ゼロページでは使えない命令
BIT、CPX、CPY、LDX、STX
インダイレクト
「直接ではない」「間接」的なアドレッシングモードを指す。
命令コードに続くオペランドで示すアドレスのメモリに入っている値をアドレスとみなし、そのアドレスを命令の対象とする。
このモードで使える命令は1つ、JMP(Jump)命令。
使用例:
プログラムの飛び先を何らかの方法によって計算で求め、その値をどこかのメモリアドレスに一旦ストアし、その値が示すアドレスにジャンプする。
注意点として、16ビット(2バイト)のアドレス値を、ページの境界をまたいで読み込もうとすると、おかしな挙動になる。
同じページの下位アドレスが$00から読み込んでくる。
例として、
アドレスとして$8FFを指定してインダイレクトモードを使うと、実効アドレスの下位バイトは指定通りの$8FFから読み込むが、上位バイトは$900ではなく$800から読み込む
回避策として、境界をまたぐようなアドレスを指定しなければ良い。
インデックスト・インダイレクト
インデックスレジスターでアドレスを修飾するモード。
使えるのはXレジスターのみ。
命令コードで指定できるのは、16ビットのアドレスではなく、8ビットのゼロページアドレス。
インデックスト・ゼロページと異なる点として、インデックスしたゼロページから読み込むのは、8ビットアドレス(ゼロページ内のロケーション)ではなく、16ビットのフルアドレスとなる。
命令コードで指定した値にXレジスターの値を足して、8ビットのゼロページのアドレスを得て、そのアドレスのメモリの内容を下位8ビット、そのアドレス+1のアドレスの内容を上位8ビットとする16ビットアドレスが最終的な実効アドレスとなる。
簡略式
(($00xx + X) + ($00xx + X + 1) × $100)
使える命令は8種類
Aレジスターのロード、ストア命令、LDA、STA、加算、減算命令のADC、SBC、3つの論理演算命令、AND、EOR、ORA、Aレジスターとメモリの値を比較するCMP
インダイレクト・インデックスト
先ほどの逆で、先にインダイレクトしてその後でインデックスを行うモード。
命令コードで指定された1バイトのゼロページアドレスから16ビットアドレスの下位8ビット、そのゼロページアドレス+1から上位8ビットを読み込み16ビットアドレスとする。
ここまでがインダイレクトのパート。
そのアドレスにインデックスレジスターの値を加えたものが最終的な実効アドレスとする。
使えるインデックスレジスターはYレジスターだけ。
簡略式
(($00xx) + ($00xx + 1) × $100) + Y)
使える命令は8種類。内訳はインデックスト・インダイレクトと同じ。
Aレジスターのロード、ストア命令、LDA、STA、加算、減算命令のADC、SBC、3つの論理演算命令、AND、EOR、ORA、Aレジスターとメモリの値を比較するCMP
リラティブ
現在のPC(プログラムカウンター)を基準に相対ジャンプを行うアドレッシングモード。
この命令の次の命令が置かれているアドレスに命令コードで指定した8ビットのオフセット値を加えたアドレスにジャンプする。
符号あり。$00から$7Fは正、$80から$FFは負。
使える命令は8種類。すべて相対ジャンプ命令
BCC (Branch if Carry Clear) BCS (Branch if Carry Set) BEQ (Branch if Equal) BMI (Branch if Minus) BNE (Branch if Not Equal) BPL (Branch if Positive) BVC (Branch if Overflow Clear) BVS (Branch if Overflow Set)む
無条件の相対ジャンプ命令は存在しない。条件成立したときだけ分岐。
無条件ジャンプを行いたい場合は、絶対ジャンプ(JMP)を使うか、その時点で必ず成立しているはずの条件を指定して条件相対ジャンプを使うかになる。
インプライド
「インプライド(Implied)」とは、「暗黙の」という意味。
アドレッシングモードによって命令の対象となる何を指定するのではなく、その命令に対しては一意に操作の対象が決まっている。
インプライドモードで使える命令は、インプライドモードでしか使えない。
インプライドモードで使える命令は全部で25種類。
各種レジスターの操作や、CPU自体の動作モードの設定に関わるものが多め。
BRK (Force Interrupt) CLC (Clear Carry Flag) CLD (Clear Decimal Mode) CLI (Clear Interrupt Disable) CLV (Clear Overflow Flag) DEX (Decrement X Register) DEY (Decrement Y Register) INX (Increment X Register) INY (Increment Y Register) NOP (No Operation) PHA (Push Accumulator) PHP (Push Processor Status) PLA (Pull Accumulator) PLP (Pull Processor Status) RTI (Return from Interrupt) RTS (Return from Subroutine) SEC (Set Carry Flag) SED (Set Decimal Flag) SEI (Set Interrupt Disable) TAX (Transfer Accumulator to X) TAY (Transfer Accumulator to Y) TSX (Transfer Stack Ponter to X)
といった感じで、わかりにくかったアドレッシングモードに関して詳しく解説されており参考になりました。