Class: Amaterasu::GameBoy::Timer

Inherits:
Object
  • Object
show all
Defined in:
lib/amaterasu/game_boy/timer.rb,
sig/akane/game_boy/timer.rbs

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

#tacInteger

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

#timaInteger

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

#tmaInteger

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

#clock_bit_positionInteger



26
# File 'sig/akane/game_boy/timer.rbs', line 26

def clock_bit_position: () -> Integer

#divInteger

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



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

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

#div=(_value) ⇒ void

This method returns an undefined value.

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.



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

#falling_edge?(old_signal, new_signal) ⇒ Boolean

Checks for a falling edge (1 -> 0) in the joint signal. Joint signal is a bitwise AND between:

  • TAC enable bit (Bit 2)
  • Currently selected bit in the system counter


204
205
206
# File 'lib/amaterasu/game_boy/timer.rb', line 204

def falling_edge?(old_signal, new_signal)
  old_signal == 1 && new_signal == 0
end

#increment_tima?Boolean



23
# File 'sig/akane/game_boy/timer.rbs', line 23

def increment_tima?: (Integer previous_counter) -> bool

#tac_clock_selectInteger



25
# File 'sig/akane/game_boy/timer.rbs', line 25

def tac_clock_select: () -> Integer

#tac_enableInteger



24
# File 'sig/akane/game_boy/timer.rbs', line 24

def tac_enable: () -> Integer

#tickvoid

This method returns an undefined value.

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