6502 and little-endian conversion

2019-07-24 03:23发布

问题:

For fun I'm implementing an NES emulator. I'm currently reading through documentation for the 6502 CPU and I'm a little confused.

I've seen documentation stating because the 6502 is little-endian so when using absolute addressing mode you need to swap the bytes. I'm writing this on an x86 machine which is also little-endian, so I don't understand why I couldn't simply cast to a uint16_t*, dereference that, and let the compiler work out the details.

I've written some simple tests in google test and they seem to agree with me.

// implementation of READ16
#define READ16(addr) (*(uint16_t*)addr)

TEST(MemMacro, READ16) {
  uint8_t arr[] = {0xFF,0xCC};
  uint8_t *mem = (&arr[0]);

  EXPECT_EQ(0xCCFF, READ16(mem));
}

This passes, so it appears my supposition is correct, but I thought I'd ask someone with more experience than I.

Is this correct for pulling out the operand in 6502 absolute addressing mode? Am I possibly missing something?

回答1:

It will work for simple cases on little-endian systems, but tying your implementation to those feels unnecessary when the corresponding portable implementation is simple. Sticking to the macro, you could do this instead:

#define READ16(addr) (addr[0] + (addr[1] << 8))

(Just to be pedantic, you should also make sure that addr[1] can't be out-of-bounds, and would need to add some more parentheses if addr could be a complex expression.)

However, as you keep developing your emulator, you will find that it's most natural to use a pair of general-purpose read_mem() and write_mem() functions that operate on single bytes. Remember that the address space is split up into multiple regions (RAM, ROM, and memory-mapped registers from the PPU and APU), so having e.g. a single array that you index into won't work well. The fact that memory regions can be remapped by mappers also complicates things. (You won't have to worry about that for simple games though -- I recommend starting with Donkey Kong.)

What you need to do is to figure out what region or memory-mapped register the address belongs to inside your read_mem() and write_mem() functions (this is called address decoding), and do the right thing for the address.

Returning to the original question, the fact that you'll end up using read_mem() to read the individual bytes of the address anyway means that the uint16_t casting trickery is even less likely to be useful. This is the simplest and most robust approach w.r.t. handling corner cases, and what every emulator I've seen does in practice (Nestopia, Nintendulator, and FCEUX).

In case you've missed it, the #nesdev channel on EFNet is very active and a good resource by the way. I assume you're already familiar with the NESDev wiki. :)

I've also been working on an emulator which can be found here.