Class: Amaterasu::GameBoy::Timer

Inherits:
Object
  • Object
show all
Defined in:
lib/amaterasu/game_boy/timer.rb

Overview

Models the built-in clock timer inside the Game Boy.

As of now it is implemented using M-cycle accuracy, I might change it afterwards to T-cycle accuracy, but as of now all acceptance tests are passing, so no need.

Constant Summary collapse

T_CYCLES =

Each tick advances 4 T-cycles / 1 M-cycle

4
MASTER_CLOCK_FREQUENCY =

Master clock defined by the hardware specs (in T-cycles).

4_194_304
TIMA_INCREMENT_FREQUENCIES =

Frequency in which TIMA increments once for each TAC clock select.

[
  4_096,
  262_144,
  65_536,
  16_384
].freeze
TIMA_INCREMENT_CYCLES =

How many T-cycles are needed to increment TIMA once for each clock select.

[
  MASTER_CLOCK_FREQUENCY / TIMA_INCREMENT_FREQUENCIES[0b00],
  MASTER_CLOCK_FREQUENCY / TIMA_INCREMENT_FREQUENCIES[0b01],
  MASTER_CLOCK_FREQUENCY / TIMA_INCREMENT_FREQUENCIES[0b10],
  MASTER_CLOCK_FREQUENCY / TIMA_INCREMENT_FREQUENCIES[0b11]
].freeze
COUNTER_FALLING_EDGE_CYCLES =

TIMA only increments if there is a falling edge. The value needs to be divided by 2 to achieve the correct value. The given bit needs to flip twice to reach a falling edge (0 -> 1 and 1 -> 0).

[
  TIMA_INCREMENT_CYCLES[0b00] / 2,
  TIMA_INCREMENT_CYCLES[0b01] / 2,
  TIMA_INCREMENT_CYCLES[0b10] / 2,
  TIMA_INCREMENT_CYCLES[0b11] / 2
].freeze
COUNTER_BITS_TO_WATCH =

In binary a given Bit N always flips its value after 2^N ticks. Based on the number of cycles derived above you can find the correct bit to watch.

Example for clock select 0b00:

  • 1024 T-cycles to increment TIMA.

  • So we need to find a Bit N that flips (1024 / 2) times to achieve a falling edge.

  • 2^N = 512 => N = log2(512) => N = 9 (Watch Bit 9 from the system counter).

[
  Math.log2(COUNTER_FALLING_EDGE_CYCLES[0b00]).round,
  Math.log2(COUNTER_FALLING_EDGE_CYCLES[0b01]).round,
  Math.log2(COUNTER_FALLING_EDGE_CYCLES[0b10]).round,
  Math.log2(COUNTER_FALLING_EDGE_CYCLES[0b11]).round
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(interrupts, skip_boot_rom: true, trace_timer: false) ⇒ Timer

Creates an instance of the timer.

  • Needs the interrupts instance to request a timer interrupt.

  • Has an internal-only 16-bit counter that increments each T-cycle.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/amaterasu/game_boy/timer.rb', line 70

def initialize(interrupts, skip_boot_rom: true, trace_timer: false)
  @interrupts = interrupts
  @trace_timer = trace_timer

  @counter = skip_boot_rom ? 0xABCC : 0x0000
  @tima    = 0x00
  @tma     = 0x00
  @tac     = skip_boot_rom ? 0xF8 : 0x00

  @tac_enable_bit = @tac[2]
  @tac_clock_select_bits = @tac & 0b11
  @counter_watched_bit_pos = COUNTER_BITS_TO_WATCH[@tac_clock_select_bits]

  @state = :running
  @tima_overflow = false
end

Instance Attribute Details

#tacObject

Returns the 8-bit value stored in the TAC (Timer Control) register.



64
65
66
# File 'lib/amaterasu/game_boy/timer.rb', line 64

def tac
  @tac
end

#timaObject

Returns the 8-bit value stored in the TIMA (Timer Counter) register.



58
59
60
# File 'lib/amaterasu/game_boy/timer.rb', line 58

def tima
  @tima
end

#tmaObject

Returns the 8-bit value stored in the TMA (Timer Modulo) register.



61
62
63
# File 'lib/amaterasu/game_boy/timer.rb', line 61

def tma
  @tma
end

Instance Method Details

#divInteger

Reading DIV only exposes the upper byte of the system counter.

Returns:

  • (Integer)


90
91
92
# File 'lib/amaterasu/game_boy/timer.rb', line 90

def div
  (@counter >> 8) & 0xFF
end

#div=(_value) ⇒ Object

Writing to DIV register always resets the system counter.

When the value is reset to 0x0000, if the current bit being “watched” goes from 1 -> 0 it can trigger a TIMA increment due to a falling edge in the joint signal.

Parameters:

  • value (Integer)

    Value is ignored and resets the whole @counter.



101
102
103
104
105
106
107
# File 'lib/amaterasu/game_boy/timer.rb', line 101

def div=(_value)
  old_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]
  @counter = 0x0000
  new_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]

  increment_tima if falling_edge?(old_signal, new_signal)
end

#tickObject

Advances the system counter, increments TIMA if needed and handles TIMA overflow logic.

  • Counter value should wrap around 0xFFFF (16-bit).

  • The Counter is always counting independent from all the other logic.

  • The tick is being implemented in M-cycle precision, so each tick is 4 T-cycles.

  • After TIMA overflows, there is a 1 M-cycle delay before setting TMA into TIMA and requesting the Timer interrupt.



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/amaterasu/game_boy/timer.rb', line 168

def tick
  old_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]
  @counter = (@counter + T_CYCLES) & 0xFFFF
  new_signal = @tac_enable_bit & @counter[@counter_watched_bit_pos]

  case @state
  when :running
    increment_tima if falling_edge?(old_signal, new_signal)
  when :tima_reload_pending
    @tima = @tma
    @interrupts.request(:timer)
    @state = :tima_reloaded
  when :tima_reloaded
    @state = :running
  end

  log_state(old_signal, new_signal) if @trace_timer
end