⭅ Previous (6502 Extras) | Next (Cartridge Basics) ⭆ |
Also available on Youtube.
Before we wrap up the CPU portion of the NES, I thought I’d provide some tips I’ve learned while developing my emulator.
Though the 6502 CPU was originally released in 1975, it is still a fairly complex device. There are many opportunities for errors while developing an emulator.
One way of testing is to write conventional unit tests, ensuring each instruction works as expected. Though for the number of instructions in this CPU, that can prove to be a significant amount of effort.
Another approach would be to write small programs, and confirm that the results match that of another trusted system. This can either be a physical system with a real 6502, or an emulated system which you trust. Differences in behavior can then be reconciled using a 6502 reference. This helps avoid duplicating bugs found in other emulated implementations.
One especially useful test program is nestest.nes, developed by Kevin Horton. The program helps test both standard functionality of most 6502 instructions, but also some tricky edge cases.
The program is a collection of test cases, and can run through each sequentially. Errors are reported into a location in memory, which can be checked automatically by your testing framework to ensure no errors are encountered.
Additionally, there is nestest.log, which reports the expected internal state before each instruction. This is extremely useful for catching discrepancies as soon as they occur, giving you the best chance at understanding what error occurred.
One challenge in running nestest with an early CPU emulator is that it is packaged as a .NES file, which contains other information about the cartridge. We’ll look at cartridges in the next article in detail. You could wait for that article if you like.
I have also repackaged nestest as a .bin file, so that no parsing of a .NES file is required. Simply load the .bin file into your emulators memory at 0xC000, then set your emulator’s PC at 0xC000 and start running. More details at nestest.bin info
The 6502 features many clever design tricks that made it simpler and cost effective to manufacture. Though some of these result in surprising behavior that may trip up emulator developers. I encountered several during the development of my 6502 emulator, and document these below.
Some instructions take shortcuts in order to allow faster completion. One example of this is the lack of page crossing seen in some instructions.
A page is a range of memory, starting with 0xZZ00 and ending at 0xZZFF. ZZ can be any numbers. A page crossing is when moving from an address at a lower page, say ZZ, to ZZ+1.
Most addressing modes on single bytes from memory. However, some of the addressing modes use indirection, where an address in memory is expected to hold the first of a full 2 byte address. The input is then retrieved from that other address. The indirect indexed address mode does the following:
lda ($AD),Y
; 1. read the start address from 0x00AD (call this A)
; 2. interpret memory at A, A+1 as a 16 byte address
; 3. add y to that value, then treat that address as the input.
This can be useful for efficiently implementing arrays, for example. One quirk however is that the two bytes in step 2 cant cross a page. So if A was 0x00FF, step 2 would construct an address from 0x00FF and 0x0000. When adding 1 to that lower byte, this avoided the need to wait for the carry from the lower byte.
It helps speed up this addressing mode, but can definitely surprise new 6502 programmers (like myself).
Early in testing, I would load demo programs at address 0x00. I had a few programs, particularly those using the stack and the zero page, which strangely would have incorrect behavior.
Zeropage addressing reads and writes from 0x0000 to 0x00FF. Stack operations read and write from 0x0100 to 0x01FF.
If your program is loaded in this region, then using the zero page or manipulating the stack may overwrite the bytes from your program.
An argument for an addressing mode can actually be treated in two different ways, depending on whether it is an input or an output location.
a as output / destination:
a = 1;
lda $00 ; load 00 into acc
a as input / value:
x = a;
sta $00 ; store acc into 00
Due to the non-page crossing quirks, these addressing modes cannot be implemented simply as a pointer. Instead, you’ll need a way to read from and write to a value indicated by an addressing mode.
That covers the especially tricky issues I encountered in the 6502. If you’re following along, you can build a functional 6502 emulator at this point, and it should be capable of running simple programs that need only memory.
In the next article we’ll take a look at some simple cartridge hardware, which will allow applications to also specify graphical data.
⭅ Previous (6502 Extras) | Next (Cartridge Basics) ⭆ |