Class: Amaterasu::GameBoy::Timer
- Inherits:
-
Object
- Object
- Amaterasu::GameBoy::Timer
- 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
-
#tac ⇒ Integer
Returns the 8-bit value stored in the TAC (Timer Control) register.
-
#tima ⇒ Integer
Returns the 8-bit value stored in the TIMA (Timer Counter) register.
-
#tma ⇒ Integer
Returns the 8-bit value stored in the TMA (Timer Modulo) register.
Instance Method Summary collapse
- #clock_bit_position ⇒ Integer
-
#div ⇒ Integer
Reading DIV only exposes the upper byte of the system counter.
-
#div=(_value) ⇒ void
Writing to DIV register always resets the system counter.
-
#falling_edge?(old_signal, new_signal) ⇒ Boolean
Checks for a falling edge (1 -> 0) in the joint signal.
- #increment_tima? ⇒ Boolean
-
#initialize(interrupts, skip_boot_rom: true, trace_timer: false) ⇒ Timer
constructor
Creates an instance of the timer.
- #tac_clock_select ⇒ Integer
- #tac_enable ⇒ Integer
-
#tick ⇒ void
Advances the system counter, increments TIMA if needed and handles TIMA overflow logic.
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
#tac ⇒ Integer
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 |
#tima ⇒ Integer
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 |
#tma ⇒ Integer
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_position ⇒ Integer
26 |
# File 'sig/akane/game_boy/timer.rbs', line 26
def clock_bit_position: () -> Integer
|
#div ⇒ Integer
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_select ⇒ Integer
25 |
# File 'sig/akane/game_boy/timer.rbs', line 25
def tac_clock_select: () -> Integer
|
#tac_enable ⇒ Integer
24 |
# File 'sig/akane/game_boy/timer.rbs', line 24
def tac_enable: () -> Integer
|
#tick ⇒ void
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 |