BUS, Interrupts そして NES

前回はメモリ周りとカートリッジを実装したので、次はNESの各パーツを繋ぐBUS周りを実装していきます。

Bus 周りの実装

このエミュレータではNESの各パーツを繋ぐのにBUSを実装しています。CPU や PPU が直接他のオブジェクトへのアクセスを握っているのではなくBUSを介してやり取りをすることで、カートリッジ毎のマッパーの違いなどをそこで吸収します。

まず、CPU_BUS は CPU のメモリアクセスがどこへアクセスするのかを決定します。
アクセス先は RAM, Cartridge への読み書き、そして PPU への I/O(と言っても普通にByteの読み書き)になります。

CPU から見たメモリマップはこんな感じです。

0x0000 - 0x07FF   WRAM
0x0800 - 0x1FFF   WRAM のミラー
0x2000 - 0x2007   I/O(PPU レギスタへのアクセス)
0x2008 - 0x3FFF   I/O のミラー
0x4000 - 0x401F   I/O(APU, KeyPad)
0x4020 - 0x5FFF   拡張RAM
0x6000 - 0x7FFF   バッテリーバックアップRAM
0x8000 - 0xBFFF   プログラムROM LOW
0xC000 - 0xFFFF   プログラムROM HIGH

内蔵 RAM(WRAM)への読み書きは0x0000 – 0x0800 で、0x0800 – 0x1FFF がミラーになります。
PPU へのアクセスは 0x2000 – 0x2007 のアドレスになり、0x2008 – 0x3FFF がミラーとなります。(この部分の詳細は PPU を実装する時に説明します。)
Cartridge への読み書きはマッパーによって変わりますが、今回は program ROM のサイズが 16K か 32K かの違いしかないマッパー 0 にしか対応しないので、特にマッパーは用意せずに CPU_BUS でアクセス先を分けています。

コードの詳細

CPU_BUS.swift
まだ PPU, DMA, KeyPad などの実装が済んでいないので接続するのは Program ROM と Work RAM だけです。
Program ROM はカートリッジで読み込まれたもので、Work RAM は NES に搭載されている 2K の RAM です。

CPU_BUS から直接読み書きできるメモリは NES に搭載されている 2K の Work RAM と、NES に挿入されたカートリッジのメモリです。
コード内では Work RAM へのアクセスは 0x0800 以下ならそのままのアドレスで RAM へアクセスし、0x2000 よりも小さい場合は Work RAM へのミラーのアクセスになるのでアドレスの剰余(address % 0x0800)へアクセスします。
JavaScript のオリジナルでは address – 0x0800 となっていますが、これだと 0x1FFF などにアクセスするとメモリのサイズを超えてしまいエラーが出るので、他のエミュレータを参考に address % 0x0800 としました。

0x8000 より大きなアドレスはカートリッジへのアクセスになります。
カートリッジ内の ROM のサイズが 16K か 32K かで始まりのアドレスが変わるので、それに対応したアドレスに変換してアクセスしています。(この変換の部分がマッパーの仕事になります)

        if address < 0x0800 {           // Read Work RAM
            return ram.read(address)
        }
        else if address < 0x2000 {      // Read Work RAM Mirror
            return ram.read(address & 0x0800)
        }
        else if address >= 0xC000 {
            if programROM.size() <= 0x4000 {            // Cartridge size 16K
                return programROM.read(address - 0xC000)
            }
            return programROM.read(address - 0x8000)    // Cartridge size 32K
        }
        else if address >= 0x8000 {                     // Cartridge size 32K
            return programROM.read(address - 0x8000)
        }
        else {
            return 0
        }

PPU_BUS.swift
PPU_BUS は PPU と PPU につながっている characterRAM を接続しています。
PPU が character RAM にアクセスする際はここを通してアクセスします。が、特に何かしているわけではなく、そのまま character RAM を読み書きするだけです。

Interrupts.swift
NMI と IRQ、2種類の割り込みの状態を保存しています。
割り込みを起こすデバイスと受けるデバイスから読み書きできるようにして、割り込みをエミュレートしています。

nes.swift
Nes では NES のハードウェアを実装していきます。
今回までに RAM, Cartridge, CPU_BUS, PPU_BUS, Interrupts を実装しているので、これらを Nes に実装します。
アプリケーションの動作としては、カートリッジを読み込んだ時点でそれに合わせて各デバイスを準備する形になっています。

まず、Nes オブジェクトが作成された時点では何のデバイスも準備されていません。そしてカートリッジが読み込まれたタイミングで、カートリッジの programROM と characterROM を使って各デバイスが準備されて接続されます。
今回はカートリッジから characterMem へ characterROM をコピーしてそれを PPU_BUS へ接続。Interrupts を準備。
Working RAM として 2K の RAM を用意してそれと programROM で CPU_BUS を作成するところまで行っています。

まとめ

今回は Bus 周りのデバイスの実装でした。
NES の中のデバイスを繋ぐ部分ができたので今までに実装したデバイスを繋げてみましたが、現状では実行しても結果は前回と変わりません。ただ読み込んだカートリッジの情報が表示されるだけです。次回以降 CPU が実装されれば、ステップ処理をしたり何か面白い結果を表示できるようになる予定です。

というわけで、次回は CPU の実装その1になります。
6502はシンプルな CPU なのでそんなに大変ではなさそうなのですが、それでもたぶんそこそこの量になると思うので2回に分ける予定です。

GitHub
StNesEmu