for(;;)
{
主回圈也可以写成这样:
CPUIsRunning 是个布林值,这样写有个好处,你可以在任何时候,设 CPUIsRunning=0,来终止主回圈。然而在每个回圈检查这个变数,会花不少的 CPU,而我们应该尽量减少花费 CPU。同时,不要写成下面这样子:
因为这样写,编译器产生代码,去检查 1 为 "真" 或 "假",你不会希望在主回圈的每个回圈,都去执行这多余的动作。
现在我们在主回圈内,第一件事,就是去读下一个执行码,然后修改程式位址记数器。
注意,这是最简易的方式,来模拟读取记忆体,但并非永远可行。更通用的方式,来存取记忆体,稍后会提到。
在提取操作码后,会从 CPU 周期计数器,扣掉这个指令所需的周期数。
Counter-=Cycles[OpCode];
Cycles[] 表内放的是每个操作码,所需要的周期数。要特别注意,有些指令(例如条件式跳跃,或是呼叫副程式),需要的周期数,是跟操作后面紧接的参数而变动。这个可以在执行指令码时调整。
现在该是解译操作码,然后跟着执行的时候了:
有一个错误的观念,认为 switch 叙述事没有效率的,因为会被编译成 if () ...... else if () ........ 叙述。这只有在 case 数量很少的 switch 叙述,才会被这样编译。当有 100 到 200 个 case 的时候,switch 叙述通常会被翻译成 jump 表格,jump 表格,其实蛮有效率的。
有其他两种替代方案,可以用来解译操作码。第一种方法,是建一个函式表,然后呼叫对应的函式。这种方式,比用 switch() 没效率,因为呼叫函式,有额外的开销。第二种方式,是建一个位址的表格,然后使用 goto 叙述。这种方式,稍比用 switch() 有效率一点,但这种方式,只适合用在编译器支援未预定位址表格。其他的编译器,不会允许你这样定义表格。
在成功解译并执行一个操作码后,这时候该去检查有没有任何系统中断发生。这时候,你也可以执行任何需要跟系统时钟同步的工作。
if(Counter<=0)
{
/* Check for interrupts and do other hardware emulation here */
...
Counter+=InterruptPeriod;
if(ExitRequired) break;
}
有关周期性的工作,后面会提到。
注意,我们并非直接指定 Counter=InterruptPeriod,而是执行 Counter+=InterruptPeriod,这样会让周期的计算更精确,因为有时候,Counter 会变成负数。
同时,注意这
这个叙述如果在每个回圈都执行,成本太高,所以只有在中断发生时才检查。这样就可以在 ExitRequired=1 时,停止模拟,但又不会花太大的成本。
-------------------------------------------------------------------------
如何存取被模拟的记忆体?
模拟记忆体存取最简单的方式,就是把它当成一个摊平的位元组或字元组阵列。如此,存取记忆体,就是一件微不足道的事情:
Data=Memory[Address1]; /* Read from Address1 */
Memory[Address2]=Data; /* Write to Address2 */
这种简易的作法,并非永远可行,原因如下:
- 分页式的记忆体
记忆体空间,可能被切成小块,变成可以切换的页,就是所谓的 banks。例如常见的,小记忆体位址空间( 64 KB),所使用的扩充记忆体。
- 映射的记忆体
这块记忆体空间,可以用数个不同的位址来存取。例如你写资料到位址 $4000,然后你在位址$6000,及位址 $8000,你也可以读到。
- ROM 的读取保护
有些存到卡夹的软体(例如 MSX 的游戏),就算你写到 ROM,回传成功,事实上 ROM 上的资料也不会改变。这么做,是为了做软体保护。为了让这样的软体,可以在你的模拟器运行,你需要把 ROM 设成唯读。
- 记忆体映射到 I/O
系统可能有 I/O 装置,映射到记忆体位址。存取这样的记忆体位址,会产生特殊效果,所以必须被追踪。
要成功处理上述问题,我们引进几个函式:
Data=ReadMemory(Address1); /* Read from Address1 */
WriteMemory(Address2,Data); /* Write to Address2 */