4

我正在编写一个 Chip 8 仿真器作为仿真的介绍,我有点迷路了。基本上,我已经读取了 Chip 8 ROM 并将其存储在内存中的 char 数组中。然后,按照指南,我使用以下代码在当前程序计数器(pc)处检索操作码:

// Fetch opcode
opcode = memory[pc] << 8 | memory[pc + 1];

芯片 8 个操作码每个是 2 个字节。这是来自指南的代码,我模糊地理解为向 memory[pc] 添加 8 个额外的位空间(使用 << 8),然后将 memory[pc + 1] 与其合并(使用 |)并将结果存储在操作码变量中.

然而,现在我已经隔离了操作码,我真的不知道如何处理它。我正在使用这个操作码表,我基本上迷失了将我读取的十六进制操作码与该表中的操作码标识符匹配。此外,我意识到我正在阅读的许多操作码也包含操作数(我假设后一个字节?),这可能使我的情况更加复杂。

帮助?!

4

4 回答 4

10

基本上,一旦您获得了需要对其进行解码的指令。例如,从您的操作码表中:

if ((inst&0xF000)==0x1000)
{
  write_register(pc,(inst&0x0FFF)<<1);
}

并且猜测由于您每条指令访问 rom 两个字节,因此地址可能是(16 位)字地址而不是字节地址,所以我将其向左移动了一个(您需要研究这些指令是如何编码的,您提供的操作码表是不够的,不用做假设)。

还有很多事情要做,我不知道我是否在我的 github 示例中写过任何关于它的内容。我建议您创建一个用于在地址处获取指令的 fetch 函数、一个读存储器函数、一个写存储器函数、一个读寄存器函数、一个写寄存器函数。我建议您的解码和执行功能一次只解码和执行一条指令。正常执行只是在循环中调用它,它提供了执行中断和类似操作的能力,而无需大量额外工作。它还模块化您的解决方案。通过创建 fetch() read_mem_byte() read_mem_word() 等函数。您将代码模块化(以轻微的性能成本为代价),使调试变得更加容易,因为您有一个可以观察寄存器或内存访问并确定正在发生或未发生的事情的地方。

根据您的问题以及您在此过程中所处的位置,我认为在编写模拟器之前您需要做的第一件事就是编写反汇编程序。作为一个固定指令长度的指令集(16 位),这使得它更容易。您可以从 rom 中的某个有趣点开始,或者如果您愿意,也可以从头开始,然后解码您看到的所有内容。例如:

if ((inst&0xF000)==0x1000)
{
  printf("jmp 0x%04X\n",(inst&0x0FFF)<<1);
}

只有 35 条指令不应该花一个下午,也许是整个星期六,这是您第一次解码指令(我假设这是基于您的问题)。反汇编程序成为您的模拟器的核心解码器。用仿真替换 printf()s,最好保留 printfs,只添加代码来模拟指令执行,这样您就可以跟踪执行。(同样的交易有一个反汇编一个指令函数,为每条指令调用它,这成为你的模拟器的基础)。

您需要对获取代码行的作用非常模糊,为了完成这项任务,您必须对位操作有深刻的理解。

另外,我会称您提供的那行代码有错误或至少有风险。如果 memory[] 是一个字节数组,编译器可能会很好地使用字节大小的数学来执行左移,产生一个零,然后与第二个字节进行零或排列只会产生第二个字节。

基本上,编译器有权改变它:

opcode = memory[pc] << 8) | memory[pc + 1];

进入这个:

opcode = memory[pc + 1];

这根本不适合你,一个非常快速的解决方法:

opcode = memory[pc + 0];
opcode <<= 8;
opcode |= memory[pc + 1];

将为您省去一些头痛。最小优化将使编译器免于为每个操作将中间结果存储到 ram 中,从而产生相同的(所需的)输出/性能。

我在上面编写和提到的指令集模拟器不是为了性能,而是为了可读性、可见性,并希望具有教育意义。我会从类似的东西开始,然后如果对性能感兴趣,你将不得不重新编写它。这个chip8仿真器,一旦体验过,从头开始是一个下午的任务,所以一旦你第一次完成这个,你可以在一个周末重新编写它可能三四次,而不是一个巨大的任务(必须重新编写)。(拇指调节器花了我一个周末,大部分时间。msp430 可能更像是一个晚上或两个晚上的工作。一劳永逸地正确设置溢出标志是最大的任务,后来才出现)。无论如何,重点是,看看诸如mame来源之类的东西,大多数(如果不是全部)这些指令集模拟器都是为提高执行速度而设计的,如果没有大量研究,许多指令集几乎无法阅读。通常是大量的表驱动,有时还有很多 C 编程技巧等。从可管理的东西开始,让它正常运行,然后担心改进它的速度、大小或可移植性等。这个chip8的东西看起来是基于图形的,所以你还必须在位图/屏幕/任何地方处理大量的线条绘制和其他位操作。或者你可以只调用 api 或操作系统函数。基本上这个chip8的东西不是你传统的带有寄存器和寻址模式和alu操作的清单的指令集。有时有很多 C 编程技巧等。从可管理的东西开始,让它正常运行,然后担心改进它的速度或大小或可移植性或其他什么。这个chip8的东西看起来是基于图形的,所以你还必须在位图/屏幕/任何地方处理大量的线条绘制和其他位操作。或者你可以只调用 api 或操作系统函数。基本上这个chip8的东西不是你传统的带有寄存器和寻址模式和alu操作的清单的指令集。有时有很多 C 编程技巧等。从可管理的东西开始,让它正常运行,然后担心改进它的速度或大小或可移植性或其他什么。这个chip8的东西看起来是基于图形的,所以你还必须在位图/屏幕/任何地方处理大量的线条绘制和其他位操作。或者你可以只调用 api 或操作系统函数。基本上这个chip8的东西不是你传统的带有寄存器和寻址模式和alu操作的清单的指令集。这个chip8的东西看起来是基于图形的,所以你还必须在位图/屏幕/任何地方处理大量的线条绘制和其他位操作。或者你可以只调用 api 或操作系统函数。基本上这个chip8的东西不是你传统的带有寄存器和寻址模式和alu操作的清单的指令集。这个chip8的东西看起来是基于图形的,所以你还必须在位图/屏幕/任何地方处理大量的线条绘制和其他位操作。或者你可以只调用 api 或操作系统函数。基本上这个chip8的东西不是你传统的带有寄存器和寻址模式和alu操作的清单的指令集。

于 2011-07-08T05:56:48.483 回答
4

基本上——屏蔽掉操作码的可变部分,然后寻找匹配项。然后使用可变部分。

例如 1NNN 是跳跃。所以:

int a = opcode & 0xF000;
int b = opcode & 0x0FFF;
if(a == 0x1000)
   doJump(b);

然后,如果您愿意,游戏就是使该代码快速或小,或优雅。好干净的乐趣!

于 2011-07-08T04:17:14.900 回答
3

不同的 CPU 在内存中存储值的方式不同。Big endian 机器以 FF,CC 的顺序在内存中存储像 $FFCC 这样的数字。Little-endian 机器以相反的顺序 CC、FF 存储字节(即,“小端”在前)。

CHIP-8 架构是大端,因此您将运行的代码具有以大端编写的指令和数据。

在您的语句“opcode = memory[pc] << 8 | memory[pc + 1];”中,主机 CPU(您的计算机的 CPU)是小端还是大端都没关系。它将始终以正确的顺序将 16 位大端值放入整数中。

有一些资源可能会有所帮助:http ://www.emulator101.com提供了 CHIP-8 仿真器教程以及一些通用仿真器技术。这个也不错: http: //www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/

于 2012-01-13T05:50:47.673 回答
2

您将不得不设置一堆不同的位掩码以从 16 位字中获取实际操作码,并结合有限状态机来解释这些操作码,因为操作码的方式似乎有些复杂被编码(即,某些操作码具有寄存器标识符等,而其他操作码则相当简单,只有一个标识符)。

您的有限状态机基本上可以执行以下操作:

  1. 使用像`0xF000这样的掩码获取操作码的第一个半字节。这将允许您“分类”操作码
  2. 根据步骤 1 中的函数类别,应用更多掩码以从操作码中获取寄存器值,或者使用操作码编码的任何其他变量,这将缩小需要调用的实际函数,以及论据。
  3. 获得操作码和变量信息后,请查找具有适当处理程序的函数的固定长度表,以与操作码功能和与操作码一起出现的变量一致。虽然您可以在您的状态机中硬编码一旦您隔离了适当的功能后将与每个操作码一起使用的函数的名称,但您使用每个操作码的函数指针初始化的表是一种更灵活的方法将使您更轻松地修改代码功能(即,您可以轻松地在调试处理程序和“正常”处理程序之间进行交换等)。
于 2011-07-08T04:32:57.350 回答