1

我目前正在尝试用 C++ 编写一个 NES 模拟器作为暑期编程项目,为下一学年的秋季学期做好准备(我有一段时间没有编码了)。我已经编写了一个 Chip8 模拟器,所以我想下一步是尝试编写一个 NES 模拟器。

无论如何,我被卡住了。我正在将此网站用于我的操作码表,但遇到了障碍。在 Chip8 上,所有操作码都是两个字节长,因此很容易获取。但是,NES 似乎有 2 个或 3 个字节的操作码,具体取决于 CPU 所处的寻址模式。我想不出任何简单的方法来确定每个操作码需要读取多少字节(我唯一的想法是创建非常长的 if 语句来检查操作码的第一个字节以查看要读取的字节数)。

我也很难弄清楚如何计算周期。如何在编程语言中创建时钟以使一切同步?

在不相关的旁注中,由于 NES 是 little-endian,我是否需要读取 programCounter + 1 然后读取 programCounter 以获得正确的操作码?

4

2 回答 2

2

However, the NES seems to have either 2 or 3 byte opcodes depending on what addressing mode the CPU is in. I can't think of any easy way to figure out how many bytes I need to read for each opcode.

The opcode is still only one byte. The extra bytes specify the operands for those instructions that have explicit operands. To do the decoding, you can create a switch-block with 256 cases (actually it won't be 256 cases, because some opcodes are illegal). It could look something like this:

opcode = ReadByte(PC++);
switch (opcode) {
...
case 0x4C:              // JMP abs
    address = ReadByte(PC++);
    address |= (uint16_t)ReadByte(PC) << 8;
    PC = address;
    cycles += 3;
    break;
...
}

The compiler will typically create a jump table for the cases, so you'll end up with fairly efficient (albeit slightly bloated) code.

Another alternative is to create an array with one entry per opcode. This could simply be an array of function pointers, with one function per opcode - or the table could contain a pointer to one function for fetching the operands, one for performing the actual operation, plus information about the number of cycles that the instruction requires. This way you can share a lot of code. An example:

const Instruction INSTRUCTIONS[] =
{
    ...
    // 0x4C: JMP abs
    {&jmp, &abs_operand, 3},
    ...
};

I'm also having trouble with figuring how to count cycles. How do I create a clock within a programming language so that everything is in sync?

Counting CPU cycles is just a matter of incrementing a counter, like I showed in my code examples above.

To sync video with the CPU, the easiest way would be to run the CPU for the amount of cycles corresponding to the active display period of a single scanline, then draw one scanline, then run the CPU for the amount of cycles correspond to the horizontal blanking period, and start over again.

When you start involving audio, how you sync things can depend a bit on the audio API you're using. For example, some APIs might send you a callback to which you respond by filling a buffer with samples and returning the number of samples generated. In this case you could calculate the number of CPU cycles that have been emulated since the previous callback and determine how many samples to generate based on that.


On an unrelated side note, since the NES is little-endian, do I need to read programCounter + 1 and then read programCounter to get the correct opcode?

Since the opcode is a single byte and instructions on the 6502 aren't packed into a word like on some other CPU architectures, endianness doesn't really matter. It does become relevant for 16-bit operands, but on the other hand PCs and most mobile phones are also based on little-endian CPUs.

于 2013-10-22T11:55:08.647 回答
0

大约 25 多年前,我为 6502 编写了一个模拟器。

这是一个非常简单的处理器,所以要么是一个函数指针表,要么是一个开关,有 256 个字节条目[开关可以更短一些,因为在所有 256 个条目中都没有有效的操作码,只有大约 200 个操作码实际使用]。

现在,如果你想写一个能精确模拟指令时间的模拟器,那么你会玩得更开心。您基本上必须模拟更多每个组件的工作方式,并通过时钟“涟漪”单元。这是相当多的工作,所以如果可能的话,我可能会忽略时间,让系统的速度取决于模拟器的速度。

于 2013-06-29T08:38:44.783 回答