Game boy模拟器(10):计时器

Posted 妇男主人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Game boy模拟器(10):计时器相关的知识,希望对你有一定的参考价值。

自从第一台计算机组装在一起以来,它们的一个基本功能就是计时:根据计时器协调动作。即使是最简单的游戏也有时间元素:例如,Pong 需要以特定的速度在屏幕上移动球。为了处理这些时间问题,每个游戏机都有某种形式的计时器,以允许事情在给定的时刻或以特定的速率发生。

GameBoy 也不例外,它包含一组根据可编程计划自动递增的寄存器。在本系列的这一部分中,我将研究计时器的结构和操作,以及如何使用它来播种伪随机数生成器,例如俄罗斯方块及其各种克隆中包含的生成器。

定时器结构

GameBoy 的 CPU,如本系列第一部分所述,在 4,194,304Hz 时钟上运行,有两个内部测量执行每条指令所用的时间:T 时钟,随时钟步长增加,M-时钟,以速度的四分之一 (1,048,576Hz) 递增。这些时钟用作定时器的源,定时器依次以 M 时钟速率的四分之一进行计数:262,144Hz。在本文中,我将这个最终值称为计时器的“基本速度”。

GameBoy 的计时器硬件提供两个独立的计时器寄存器:系统通过以预定速率递增每个寄存器中的值来工作。“分频器”计时器永久设置为以 16384Hz 的频率递增,即基本速度的十六分之一;由于它只是一个 8 位寄存器,它的值在达到 255 后将归零。“计数器”定时器更具可编程性:它可以设置为四种速度之一(基数除以 1、4、16 或64),并且可以设置为在溢出超过 255 时返回一个不为零的值。此外,定时器硬件将向 CPU 发送中断,如第 8 部分所述,每当“计数器”计时器确实溢出。

定时器使用的寄存器有四个;它们作为 I/O 页面的一部分供系统使用,就像图形和中断寄存器一样:

定时器寄存器

AddressRegisterDetails
0xFF04DividerCounts up at a fixed 16384Hz;
reset to 0 whenever written to
0xFF05CounterCounts up at the specified rate
Triggers INT 0x50 when going 255->0
0xFF06ModuloWhen Counter overflows to 0,
it's reset to start at Modulo
0xFF07Control
BitsFunctionDetails
0-1Speed00: 4096Hz
01: 262144Hz
10: 65536Hz
11: 16384Hz
2Running1 to run timer, 0 to stop
3-7Unused

由于“计数器”计时器在溢出时会触发中断,因此如果游戏需要定期发生某些事情,它会特别有用。然而,Gameboy 游戏通常可以使用垂直消隐来达到大致相同的效果,因为它以几乎 60Hz 的正常速度发生;垂直消隐处理程序不仅可以用于刷新屏幕内容,还可以用于检查键盘和更新游戏状态。因此,在传统的 Gameboy 游戏中很少需要使用计时器,尽管它可以在图形演示中发挥更大的作用。

实现定时器仿真

本系列文章中开发的仿真使用 CPU 的时钟作为基本时间单位。因此,为与 CPU 时钟同步运行并由调度函数更新的计时器维护时钟是最简单的。在这个阶段,将 DIV 寄存器作为一个独立于可控计时器的实体保持很方便,再次以最快计时器步长的 1/16 的速率递增:

源码

timer.js

TIMER = {
  _div: 0,
  _tma: 0,
  _tima: 0,
  _tac: 0,

  _clock: {main:0, sub:0, div:0},

  reset: function() {
    TIMER._div = 0;
    TIMER._sdiv = 0;
    TIMER._tma = 0;
    TIMER._tima = 0;
    TIMER._tac = 0;
    TIMER._clock.main = 0;
    TIMER._clock.sub = 0;
    TIMER._clock.div = 0;
    LOG.out('TIMER', 'Reset.');
  },

  step: function() {
    TIMER._tima++;
    TIMER._clock.main = 0;
    if(TIMER._tima > 255)
    {
      TIMER._tima = TIMER._tma;
      MMU._if |= 4;
    }
  },

  inc: function() {
    var oldclk = TIMER._clock.main;

    TIMER._clock.sub += Z80._r.m;
    if(TIMER._clock.sub > 3)
    {
      TIMER._clock.main++;
      TIMER._clock.sub -= 4;

      TIMER._clock.div++;
      if(TIMER._clock.div==16)
      {
        TIMER._clock.div = 0;
	TIMER._div++;
	TIMER._div &= 255;
      }
    }

    if(TIMER._tac & 4)
    {
      switch(TIMER._tac & 3)
      {
        case 0:
	  if(TIMER._clock.main >= 64) TIMER.step();
	  break;
	case 1:
	  if(TIMER._clock.main >=  1) TIMER.step();
	  break;
	case 2:
	  if(TIMER._clock.main >=  4) TIMER.step();
	  break;
	case 3:
	  if(TIMER._clock.main >= 16) TIMER.step();
	  break;
      }
    }
  },

  rb: function(addr) {
    switch(addr)
    {
      case 0xFF04: return TIMER._div;
      case 0xFF05: return TIMER._tima;
      case 0xFF06: return TIMER._tma;
      case 0xFF07: return TIMER._tac;
    }
  },

  wb: function(addr, val) {
    switch(addr)
    {
      case 0xFF04: TIMER._div = 0; break;
      case 0xFF05: TIMER._tima = val; break;
      case 0xFF06: TIMER._tma = val; break;
      case 0xFF07: TIMER._tac = val&7; break;
    }
  }
};

Timer.js:时钟增量

TIMER = {
    _clock: {
        main: 0,
	sub:  0,
	div:  0
    },

    _reg: {
        div:  0,
	tima: 0,
	tma:  0,
	tac:  0
    },

    inc: function()
    {
        // Increment by the last opcode's time
        TIMER._clock.sub += Z80._r.m;

	// No opcode takes longer than 4 M-times,
	// so we need only check for overflow once
	if(TIMER._clock.sub >= 4)
	{
	    TIMER._clock.main++;
	    TIMER._clock.sub -= 4;

	    // The DIV register increments at 1/16th
	    // the rate, so keep a count of this
	    TIMER._clock.div++;
	    if(TIMER._clock.div == 16)
	    {
	        TIMER._reg.div = (TIMER._reg.div+1) & 255;
		TIMER._clock.div = 0;
	    }
	}

	// Check whether a step needs to be made in the timer
	TIMER.check();
    }
};

Z80.js:调度器

while(true)
{
    // Run execute for this instruction
    var op = MMU.rc(Z80._r.pc++);
    Z80._map[op]();
    Z80._r.pc &= 65535;
    Z80._clock.m += Z80._r.m;
    Z80._clock.t += Z80._r.t;

    // Update the timer
    TIMER.inc();

    Z80._r.m = 0;
    Z80._r.t = 0;

    // If IME is on, and some interrupts are enabled in IE, and
    // an interrupt flag is set, handle the interrupt
    if(Z80._r.ime && MMU._ie && MMU._if)
    {
        // Mask off ints that aren't enabled
        var ifired = MMU._ie & MMU._if;

	if(ifired & 0x01)
	{
	    MMU._if &= (255 - 0x01);
	    Z80._ops.RST40();
	}
    }

    Z80._clock.m += Z80._r.m;
    Z80._clock.t += Z80._r.t;

    // Update timer again, in case a RST occurred
    TIMER.inc();
}

从这里开始,可控定时器由不同的基速划分组成,使得检查定时器值是否需要递增以及提供寄存器作为内存 I/O 页面的一部分变得相对简单。以下代码段与 MMU I/O 页面处理程序之间的接口留给读者作为练习。

Timer.js:注册检查和更新

    check: function()
    {
        if(TIMER._reg.tac & 4)
	{
	    switch(TIMER._reg.tac & 3)
	    {
	        case 0: threshold = 64; break;		// 4K
		case 1: threshold =  1; break;		// 256K
		case 2: threshold =  4; break;		// 64K
		case 3: threshold = 16; break;		// 16K
	    }

	    if(TIMER._clock.main >= threshold) TIMER.step();
	}
    },

    step: function()
    {
        // Step the timer up by one
	TIMER._clock.main = 0;
    	TIMER._reg.tima++;

	if(TIMER._reg.tima > 255)
	{
	    // At overflow, refill with the Modulo
	    TIMER._reg.tima = TIMER._reg.tma;

	    // Flag a timer interrupt to the dispatcher
	    MMU._if |= 4;
	}
    },

    rb: function(addr)
    {
	switch(addr)
	{
	    case 0xFF04: return TIMER._reg.div;
	    case 0xFF05: return TIMER._reg.tima;
	    case 0xFF06: return TIMER._reg.tma;
	    case 0xFF07: return TIMER._reg.tac;
	}
    },

    wb: function(addr, val)
    {
	switch(addr)
	{
	    case 0xFF04: TIMER._reg.div = 0; break;
	    case 0xFF05: TIMER._reg.tima = val; break;
	    case 0xFF06: TIMER._reg.tma = val; break;
	    case 0xFF07: TIMER._reg.tac = val & 7; break;
	}
    }

播种伪随机数生成器

许多游戏的一个主要组成部分是不可预测性:例如,俄罗斯方块会将未知模式的棋子扔到井下,游戏包括使用这些棋子建造行。理想情况下,计算机通过生成随机数来提供不可预测性,但这与计算机的有条不紊性质背道而驰;计算机不可能提供真正随机的数字模式。存在各种算法来生成表面上看起来像是随机的数字序列,这些被称为伪随机数生成 (PRNG) 算法。

PRNG 通常作为一个公式实现,给定一个特定的输入数字,将产生另一个与输入几乎没有关系的数字。对于俄罗斯方块,不需要这么复杂的东西;相反,以下代码用于生成看似随机的块。

Tetris.asm:选择新块

BLK_NEXT = 0xC203
BLK_CURR = 0xC213
REG_DIV  = 0x04

NBLOCK: ld hl, BLK_CURR		; Bring the next block
	ld a, (BLK_NEXT)	; forward to current
	ld (hl),a
	and 0xFC		; Clear out any rotations
	ld c,a			; and hold onto previous

	ld h,3			; Try the following 3 times

.seed:	ldh a, (REG_DIV)	; Get a "random" seed
	ld b,a
.loop:	xor a			; Step down in sevens
.seven:	dec b			; until zero is reached
	jr z, .next		; This loop is equivalent
	inc a			; to (a%7)*4
	inc a
	inc a
	inc a
	cp 28
	jr z, .loop
	jr .seven

.next:	ld e,a			; Copy the new value
	dec h			; If this is the
	jr z, .end		; last try, just use this
	or c			; Otherwise check
	and 0xFC		; against the previous block
	cp c			; If it's the same again,
	jr z, .seed		; try another random number
.end:	ld a,e			; Get the copy back
	ld (BLK_NEXT), a	; This is our next block

Tetris 块选择器的基础是 DIV 寄存器:由于选择程序每几秒只运行一次,因此寄存器在任何给定的运行中都会有一个未知值,因此它可以公平地近似随机数源。模拟定时器系统后,可以模拟俄罗斯方块及其克隆体的全部功能。

迄今为止一直被忽视的游戏仿真的一个方面是声音的产生,以及声音与仿真速度的同步。除了模拟器产生声音的方面之外,是将声音输出到浏览器的方法;本系列的下一部分将研究围绕声音输出机制的问题,以及是否可以将连贯的策略放在一起以在 javascript 中产生声音。

以上是关于Game boy模拟器(10):计时器的主要内容,如果未能解决你的问题,请参考以下文章

Game boy模拟器:运行内存

Game boy模拟器:图形

Game boy模拟器:内存池

Game boy模拟器:完整的 Z80 内核CPU源码

Game boy模拟器:CPU

Game boy模拟器:输入