ぺんぎんさんのおうち

日本語勉強中のドイツ産ペンギンがいろんなことを書く

Golangでファミコンエミュを作る 3日目

3日目です. 前回はこれ Golangでファミコンエミュを作る 2日目


少し雑談

前の更新から2週間ほど経ってしまいました. あまり時間が取れなかったのが原因です. 空いた時間は少しコード書いたりしてました(GitHubはたまに更新してた). 

ひとまず落ち着いたので(卒研と論文がありますが), また再開していきたいと思います.


ここから本編

前回はNESファイル全体を読み込んでヘッダー, プログラムROM, キャラクターROMを切り出し, NESファイルフォーマットの話をしました.

ここからどうやってエミュレータを完成させていくか(どういう順に実装して行くか)の目処がまだ立っていないので今回はCPUまわりの解説をしたいと思います.



CPU

詳細はこちらが参考になります. NES on FPGA

レジスタ

まずはCPUに搭載されているレジスタです.

15 - 8 7 - 0 レジスタ
A アキュムレーターレジスタ
X インデックスレジスタ
Y インデックスレジスタ
PC PC プログラムカウンタ
0x01 SP スタックポインタ
P プロセッサステータスレジスタ

スタックポインタは2バイトですが, 上位は 01 固定です.
プログラムカウンタ(インストラクションポインタ)は2バイトで, 現在実行している命令のアドレスを指します.

ステータスレジスタ

上の表のPレジスタです. これは割り込み発生時や演算結果によって変化します.

bit フラグ 詳細
7 N (Negative) 演算結果のビット7をストア(負の時にセット)
6 V (oVerflow) 演算によってオーバーフローが起きた時にセット
5 R (Reserved) 常に1
4 B (Break) BRK命令による割り込みが発生した時にセット
3 D (Decimal) セットされていれば10進演算が行われる
2 I (Interrupt) 割り込みが発生するとセット
1 Z (Zero) 演算結果がゼロだった時にセット, そうでない時にクリア
0 C (Carry) 桁上げが起きた時にセット

セットは該当フラグを1に, クリアは0にします.


メモリマップ

CPUのメモリで, どの番地にどの情報が配置されるかを示しています.

アドレス サイズ マッピングされるもの
0x0000 ~ 0x07FF 0x0800 WRAM
0x0800 ~ 0x1FFF WRAM ミラー
0x2000 ~ 0x2007 0x0008 PPUレジスタ
0x2008~0x3FFF PPUレジスタ ミラー
0x4000 ~ 0x401F 0x0020 APU, PAD
0x4020 ~ 0x5FFF 0x1FE0 ExRAM
0x6000 ~ 0x7FFF 0x2000 ExROM
0x8000 ~ 0xFFFF 0x8000 ROM

0x8000 ~ 0xFFFF がROMになっていますね. ここにNESファイルから切り出したPRG-ROMが配置されます.


ここまでを踏まえて, レジスタとCPUの構造体を表すコードを示します.

type Registers struct {
    A uint8     // Accumulator
    X uint8     // Index Register
    Y uint8     // Index Register
    P uint8     // Status Register
    SP uint16   // Stack Register
    PC uint16   // Program Counter
}

type CPU struct {
    Registers
    Memory [0x10000]uint8
}

CPU構造体の中にレジスタとメモリを定義しています.

NESから切り出したPRG-ROMはメモリに配置されます.
ということで配置のための関数が必要になります.

func (cpu *CPU) MemoryMapping(val []uint8, start int) {
    for i := 0; i < len(val); i++ {
        cpu.Memory[start + i] = val[i]
    }
}

メモリに配置させたいデータ(配列)と開始アドレスを渡すと, そのアドレスを起点にデータが配置されます. start + len(val) > 0x10000 の場合は配置ができないことに注意しましょう.


スタック

レジスタにSP(スタックポインタ)がありました, 上位バイトは0x01固定で, 下位バイトが0x00~0xFFまで変化します.

つまり, SPの取り得る範囲は 0x0100 ~ 0x01FF となります.
SPの値はあくまでアドレスで, そのアドレスはどこを指すのかというとCPUのメモリになるわけです. 上のメモリマップを確認するとWRAMの領域になっていますね.

スタック操作は次の2つがあります.

  • PUSH
    SPが指すアドレスにデータをストアし, SPをデクリメント
  • POP
    SPをインクリメントし, SPが指すアドレスにあるデータをフェッチ

スタックは上位アドレスから下位アドレスに成長していきます.
PUSHするたびにSPは小さくなっていき, POPするとSPは大きくなります. (この表現はなんか気になりますが)


pushとpopのコードは以下のようになります.

func (cpu *CPU) StackPush(val uint8) {
    cpu.Memory[cpu.SP] = val
    cpu.SP -= 1
}
func (cpu *CPU) StackPop() uint8 {
    cpu.SP += 1
    return cpu.Memory[cpu.SP]
}


割り込み

割り込みにはRESET, NMI, IQR, BRKがあります.
今回はRESETのみ解説をします. 他の割り込みについてはやはり NES on FPGA こちらを参照するのがよいです.

RESETは電源を入れた時, リセット時に発生します.
PレジスタのIフラグをセットし, PCの下位バイトをメモリの0xFFFCから, 上位バイトを0xFFFDからフェッチします.

0xFFFC 0xFFFD
0x00 0x80

メモリに上の表のようにデータが配置されていた場合, PCには0x8000(ROMの開始アドレス)が入るので順にROMが読まれていくわけです.

PC // 2 bytes  
CPU.Memory[0xFFFC] // 1 byte  
CPU.Memory[0xFFFD] // 1 byte  

PC = CPU.Memory[0xFFFC] | (CPU.Memory[0xFFFD] << 0x08)

フェッチ部分の擬似コードはこんな感じです.


プログラムカウンタ

プログラムカウンタ(PC)は現在実行中の命令のアドレスを指すレジスタです.
PRG-ROMはメモリの0x8000番地から配置されているので, PCを0x8000から処理を進めることでプログラムが動きます.

PCが指すアドレスにあるデータをフェッチし, そのデータに該当する処理を... 難しいのでこの部分はひとまずおいておくことにします.


フェッチとPCをインクリメントするコードは以下のようになります.

func (cpu *CPU) Fetch() uint8 {
    defer cpu.IncrementPC() // cannot use statement cpu.Memory[cpu.PC++]
    return cpu.Memory[cpu.PC]
}

func (cpu *CPU) IncrementPC() {
    if cpu.PC < 0xFFFF {
        cpu.PC += 1
    } else {
        // Raise panic for now
        panic("End of Memory")
    }
}

フェッチした後でPCをインクリメントするようにしています.

とりあえずROMを全部読み込んでみる

実行はしません(できません)が, ひとまず読み込みだけ.

func (cpu *CPU) Run() {
    for ; cpu.PC <= 0xFFFF; {
        opecode := cpu.Fetch()
        // opecodeから実行する処理を決定し
        // 必要であればフェッチを行う
        fmt.Printf("%02X ", opecode)
    }
}

Run()を実行するとPCが0xFFFFになるまでループが続きます.

func main() {

    ... 略

    cpu.MemoryMapping(programROM, 0x8000)
    fmt.Printf("%X\n", cpu.Memory[0x8000 : 0x8080])

    cpu.Run()
}

CPU.Memoryに配置されたPRG-ROMが順にフェッチされ表示されます.



まとめ

今回はCPUのレジスタやメモリマッピング等の解説をしました.
コードはここにあります https://github.com/ykm11/fc-emulator/tree/master/03_day

実装が増えてきたのでそろそろテストコードを書きたいと思います.