Files
Gameboy_MiSTer/rtl/timer.v
Mark Johnson 1b131a0de1 Timer module rewrite, APU clocked from timer (#280)
* Rework timer modules to resolve #101

Sound clock is passed to the APU but is not currently being used. Work needed to match this up.

* Rename APU frame clock.

* VHDL-2008 fix.

* Fix APU clock to 1x CPU clock.

* Clarify machine cycles in timer.

* Whitespace

* Add APU channel enables for debugging.

* Fix timer generated sound clk

R14 - Audio issues remain for 2X CPU games

* Fix issue with 2x gameboy APU sound clock

R22

* Re-implement sound clocking in timer module.

R25, Remove old code N.B. Likely a bug still here.

* Fix timer generated APU clock to 4 MiHz cycle.

R27

* Tidy up.

Remove attribution in timer module since doing a full rewrite.

* Disabling TAC can generate a clock-edge

Thanks to @paulb-nl for pointing out

* Prevent name clash

* Rework timer modules to resolve #101

Sound clock is passed to the APU but is not currently being used. Work needed to match this up.

* Rename APU frame clock.

* VHDL-2008 fix.

* Fix APU clock to 1x CPU clock.

* Clarify machine cycles in timer.

* Whitespace

* Add APU channel enables for debugging.

* Fix timer generated sound clk

R14 - Audio issues remain for 2X CPU games

* Fix issue with 2x gameboy APU sound clock

R22

* Re-implement sound clocking in timer module.

R25, Remove old code N.B. Likely a bug still here.

* Fix timer generated APU clock to 4 MiHz cycle.

R27

* Tidy up.

Remove attribution in timer module since doing a full rewrite.

* Disabling TAC can generate a clock-edge

Thanks to @paulb-nl for pointing out

* Prevent name clash

* Remove unused variable

* Set en_len_r on same cycle as en_len

* Buffer en_len to en_len_r properly

* Remove debug tools
2026-03-22 21:32:35 +08:00

152 lines
4.8 KiB
Verilog

// Implementation follows the gbdev pandocs
// https://gbdev.io/pandocs/Timer_Obscure_Behaviour.html
module timer (
input reset,
input clk_sys,
input ce, // 4 MiHz / 8 MiHz cpu clock
input ce_4MHz,
input cpu_speed,
output irq,
// CPU register interface
input cpu_sel,
input [1:0] cpu_addr,
input cpu_wr,
input [7:0] cpu_di,
output [7:0] cpu_do,
output apu_framecount_en,
// Save states
input [63:0] SaveStateBus_Din,
input [9:0] SaveStateBus_Adr,
input SaveStateBus_wren,
input SaveStateBus_rst,
output [63:0] SaveStateBus_Dout
);
assign cpu_do =
(cpu_addr == 2'b00) ? div :
(cpu_addr == 2'b01) ? tima :
(cpu_addr == 2'b10) ? tma :
{5'b11111, tac};
// https://gbdev.io/pandocs/Timer_Obscure_Behaviour.html#timer-global-circuit
// Falling-edge of selected counter
assign apu_framecount_en = clk_sound_r && !clk_sound;
reg clk_sound_r;
wire clk_sound = cpu_speed ? div[5] : div[4];
// Use 4 MiHz clock to generate APU trigger to enforce alignment.
always @(posedge clk_sys) begin : CLK_SOUND_BLK
if (reset)
clk_sound_r <= 1'b0;
else if (ce_4MHz)
clk_sound_r <= clk_sound;
end
// Save states
wire [46:0] SS_Timer;
wire [46:0] SS_Timer_BACK;
eReg_SavestateV #(0, 6, 46, 0, 64'h0000000000000008) iREG_SAVESTATE_Timer (clk_sys, SaveStateBus_Din, SaveStateBus_Adr, SaveStateBus_wren, SaveStateBus_rst, SaveStateBus_Dout, SS_Timer_BACK, SS_Timer);
// Unused legacy bits: 8-9, 29, 39-41
assign {SS_Timer_BACK[37:30], SS_Timer_BACK[ 7: 0]} = clk_div;
assign SS_Timer_BACK[17:10] = tima;
assign SS_Timer_BACK[25:18] = tma;
assign SS_Timer_BACK[28:26] = tac;
assign SS_Timer_BACK[38] = clk_tac_r;
assign SS_Timer_BACK[46:42] = tima_overflow_buffer;
reg [15:0] clk_div;
wire [7:0] div = clk_div[15:8];
always @(posedge clk_sys) begin : CLK_DIV_BLK
if (reset)
clk_div <= {SS_Timer[37:30], SS_Timer[7:0]}; // 16'd8;
else if(cpu_sel && cpu_wr && (cpu_addr == 2'b00)) // Writing any value to DIV register clears counter.
clk_div <= 16'd2; // For some reason this needs to be set to 2, rather than zero. This differs from sameboy.
else if (ce)
clk_div <= clk_div + 16'd1;
end
reg [7:0] tma;
always @(posedge clk_sys) begin : TMA_BLK
if (reset)
tma <= SS_Timer[25:18]; // 0
else if (ce) begin
if (cpu_sel && cpu_wr && (cpu_addr == 2'b10))
tma <= cpu_di;
end
end
reg [2:0] tac;
// Disabling TAC can create a clock event to TIMA
wire clk_tac = tac[2] && (
(tac[1:0] == 2'b00) ? clk_div[9]:
(tac[1:0] == 2'b01) ? clk_div[3]:
(tac[1:0] == 2'b10) ? clk_div[5]:
clk_div[7]
);
reg clk_tac_r;
always @(posedge clk_sys) begin : TAC_BLK
if (reset) begin
tac <= SS_Timer[28:26]; // 0
clk_tac_r <= SS_Timer[38]; // 0
end else if (ce) begin
clk_tac_r <= clk_tac;
if (cpu_sel && cpu_wr && (cpu_addr == 2'b11))
tac <= cpu_di[2:0];
end
end
/* Overflow timing explanation (https://gbdev.io/pandocs/Timer_Obscure_Behaviour.html#timer-overflow-behaviour)
Here, the timer clock operates at 1/4 of the system clock frequency. That is, the timer aligns with a machine cycle.
Basic sequence of events with values at each cycle:
Clock tick -1: TIMA overflow occurs, i.e. {OVERFLOW_FLAG, TIMA} == (8'hff + 1)
Overflow cycle:
Clock tick 0: OVERFLOW_BUFFER[0] = 1, TIMA = 0, IRQ = 0,
Clock tick 1: OVERFLOW_BUFFER[1] = 1, TIMA = 0, IRQ = 0,
Clock tick 2: OVERFLOW_BUFFER[2] = 1, TIMA = 0, IRQ = 0,
Clock tick 3: OVERFLOW_BUFFER[3] = 1, TIMA = 0, IRQ = 0,
Interrupt Cycle:
Clock tick 4: OVERFLOW_BUFFER[4] = 1, TIMA = 1?, IRQ = 1,
Clock tick 5: OVERFLOW_BUFFER = X, TIMA = TMA, IRQ = 0,
If TIMA is written to during the overflow cycle (ticks 0 to 3) the IRQ is prevented and the timer continues as normal.
If TMA is written at the same time TMA is loaded into TIMA (tick 5), the new value is also loaded into TIMA.
*/
reg [7:0] tima;
reg [4:0] tima_overflow_buffer;
assign irq = tima_overflow_buffer[4];
always @(posedge clk_sys) begin : TIMA_BLK
if(reset) begin
tima <= SS_Timer[17:10]; // 0
tima_overflow_buffer <= SS_Timer[46:42]; // 0
end else if (ce) begin
tima_overflow_buffer <= {tima_overflow_buffer[3:0], 1'b0};
if(clk_tac_r && !clk_tac)
{tima_overflow_buffer[0], tima} <= tima + 1'b1;
// IRQ asserted with clock tick 4 (beginning of interrupt cycle), TIMA write takes place 1 clock TICK later
if (irq) begin
tima <= tma;
if(cpu_sel && cpu_wr && cpu_addr == 2'b10) // Writing TMA when loading TIMA has instant effect
tima <= cpu_di;
end
if(cpu_sel && cpu_wr && cpu_addr == 2'b01) begin
tima_overflow_buffer[4:1] <= 4'b0; // Writing to TIMA during overflow cycle prevents interrupt
if (!irq) // Writes to TIMA during interrupt cycle are ignored.
tima <= cpu_di;
end
end
end
endmodule