Class: Amaterasu::GameBoy::Timer
- Inherits:
-
Object
- Object
- Amaterasu::GameBoy::Timer
- 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
-
#tac ⇒ Object
Returns the 8-bit value stored in the TAC (Timer Control) register.
-
#tima ⇒ Object
Returns the 8-bit value stored in the TIMA (Timer Counter) register.
-
#tma ⇒ Object
Returns the 8-bit value stored in the TMA (Timer Modulo) register.
Instance Method Summary collapse
-
#div ⇒ Integer
Reading DIV only exposes the upper byte of the system counter.
-
#div=(_value) ⇒ Object
Writing to DIV register always resets the system counter.
-
#initialize(interrupts, skip_boot_rom: true, trace_timer: false) ⇒ Timer
constructor
Creates an instance of the timer.
-
#tick ⇒ Object
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 ⇒ Object
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 ⇒ Object
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 ⇒ Object
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
#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) ⇒ 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.
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 |
#tick ⇒ Object
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 |