Running our first test ROM

The NES dev community has created large suites of tests that can be used to check our emulator.

They cover pretty much every aspect of the console, including quirks and notable bugs that were embedded in the platform.

We will start with the most basic test covering main CPU features: instruction set, memory access, and CPU cycles.

The iNES file of the test is located here: nestest.nes An execution log accompanies the test, showing what the execution should look like: nestest.log

The next goal is to generate a similar execution log for the CPU while running a program.

For now, we can ignore the last column and focus on the first five.

The fourth column @ 80 = 0200 = 00 is somewhat interesting.

  • The first number is the actual mem reference that we get if we apply an offset to the requesting address. 0xE1 is using the "Indirect X" addressing mode, and the offset is defined by register X
  • The second number is a 2-byte target address fetched from [0x80 .. 0x81]. In this case it's [0x00, 0x02]
  • The third number is content of address cell 0x0200

We already have a place to intercept CPU execution:


#![allow(unused_variables)]
fn main() {
impl CPU  {

// ..
    pub fn run_with_callback<F>(&mut self, mut callback: F)
   where
       F: FnMut(&mut CPU),
   {
       let ref opcodes: HashMap<u8, &'static opcodes::OpCode> = *opcodes::OPCODES_MAP;

       loop {
           callback(self);
 // ...
      }
   }
}
}

All we need to do is to create a callback function that will trace CPU state:

fn main() {
//...
    cpu.run_with_callback(move |cpu| {
       println!("{}", trace(cpu));
   }
}

It's vital to get a execution log format precisely like the one used in the provided log.

Following tests can help you to get it right:


#![allow(unused_variables)]

fn main() {
#[cfg(test)]
mod test {
   use super::*;
   use crate::bus::Bus;
   use crate::cartridge::test::test_rom;

   #[test]
   fn test_format_trace() {
       let mut bus = Bus::new(test_rom());
       bus.mem_write(100, 0xa2);
       bus.mem_write(101, 0x01);
       bus.mem_write(102, 0xca);
       bus.mem_write(103, 0x88);
       bus.mem_write(104, 0x00);

       let mut cpu = CPU::new(bus);
       cpu.program_counter = 0x64;
       cpu.register_a = 1;
       cpu.register_x = 2;
       cpu.register_y = 3;
       let mut result: Vec<String> = vec![];
       cpu.run_with_callback(|cpu| {
           result.push(trace(cpu));
       });
       assert_eq!(
           "0064  A2 01     LDX #$01                        A:01 X:02 Y:03 P:24 SP:FD",
           result[0]
       );
       assert_eq!(
           "0066  CA        DEX                             A:01 X:01 Y:03 P:24 SP:FD",
           result[1]
       );
       assert_eq!(
           "0067  88        DEY                             A:01 X:00 Y:03 P:26 SP:FD",
           result[2]
       );
   }

   #[test]
   fn test_format_mem_access() {
       let mut bus = Bus::new(test_rom());
       // ORA ($33), Y
       bus.mem_write(100, 0x11);
       bus.mem_write(101, 0x33);


       //data
       bus.mem_write(0x33, 00);
       bus.mem_write(0x34, 04);

       //target cell
       bus.mem_write(0x400, 0xAA);

       let mut cpu = CPU::new(bus);
       cpu.program_counter = 0x64;
       cpu.register_y = 0;
       let mut result: Vec<String> = vec![];
       cpu.run_with_callback(|cpu| {
           result.push(trace(cpu));
       });
       assert_eq!(
           "0064  11 33     ORA ($33),Y = 0400 @ 0400 = AA  A:00 X:00 Y:00 P:24 SP:FD",
           result[0]
       );
   }
}

}

Now it's time to compare our execution log to the golden standard.

cargo run > mynes.log
diff -y mynes.log nestest.log

You can use any diff tool you'd like. But because our NES doesn't support CPU clock cycles yet, it makes sense to remove last columns in the provided log:

cat nestest.log | awk '{print substr($0,0, 73)}' > nestest_no_cycle.log
diff -y mynes.log nestest_no_cycle.log

If everything is OK, the first mismatch should look like this:

C6B3  A9 AA     LDA #$AA                        A:FF X:97 Y:4   C6B3  A9 AA     LDA #$AA                        A:FF X:97 Y:4
C6B5  D0 05     BNE $C6BC                       A:AA X:97 Y:4   C6B5  D0 05     BNE $C6BC                       A:AA X:97 Y:4
C6BC  28        PLP                             A:AA X:97 Y:4   C6BC  28        PLP                             A:AA X:97 Y:4
                                                              > C6BD  04 A9    *NOP $A9 = 00                    A:AA X:97 Y:4

I.e., everything that our emulator has produced should exactly match the golden standard, up to line 0xC6BC. If anything is off before the line, we have a mistake in our CPU implementation and it needs to be fixed.

But that doesn't explain why our program got terminated. Why didn't we get the perfect match after the line 0xC6BC?

The program has failed at

C6BD  04 A9    *NOP $A9 = 00

It looks like our CPU doesn't know how to interpret the opcode 0x04.

Here is the bad news: there are about 110 unofficial CPU instructions. Most of the real NES games use them a lot. For us to move on, we will need to implement all of them.

The specs can be found here:

Remember how to draw an owl?

The testing ROM should drive your progress. In the end, the CPU should support 256 instructions. Considering that 1 byte is for the operation code, we've exhausted all possible values.

Finally, the first mismatch should happen on this line:

C68B  8D 15 40  STA $4015 = FF                  A:02 X:FF Y:15 P:25 SP:FB

almost at the very end of the NES test log file.

That's a good sign. 4015 is a memory map for the APU register, which we haven't implemented yet.



The full source code for this chapter: GitHub