Skip to content

Commit

Permalink
Improve documentation of timer bugs (#543)
Browse files Browse the repository at this point in the history
* Update TIMA diagram with findings from @Gekkio's schematics

Thanks for confirming our hypotheses!

* Clarify and correct "Timer overflow behavior"

Research assisted by @LIJI32, @Gekkio, and @SonoSooS.
Thank you all!

* Clarify DIV/TAC-related timer glitches

* Update src/Timer_Obscure_Behaviour.md

Co-authored-by: Eldred Habert <eldredhabert0@gmail.com>

---------

Co-authored-by: Antonio Vivace <avivace4@gmail.com>
  • Loading branch information
ISSOtm and avivace committed Feb 16, 2024
1 parent 7a8bc4a commit acd54eb
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 1,101 deletions.
156 changes: 60 additions & 96 deletions src/Timer_Obscure_Behaviour.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,99 +32,63 @@ On **CGB**:
{{#include imgs/src/timer_tac_bug_gbc.svg:2:}}
</figure>

Notice how the values that are connected to the inputs of the
multiplexer are the values of those bits, not the carry of those bits.
This is the reason of a few things:

- When writing to DIV, the system counter is reset to zero, so the timer is
also affected.

- When writing to DIV, if the current output is 1 and timer is
enabled, as the new value after reseting the system counter will be 0, the falling
edge detector will detect a falling edge and TIMA will increase.

- When writing to TAC, if the previously selected multiplexer input was
1 and the new input is 0, TIMA will increase too. This doesn't
happen when the timer is disabled, but it also happens when disabling
the timer (the same effect as writing to DIV). The following code explains the behavior in DMG and MGB.

```
clocks_array[4] = {256, 4, 16, 64}
old_clocks = clocks_array[old_TAC&3]
new_clocks = clocks_array[new_TAC&3]
old_enable = old_TAC & BIT(2)
new_enable = new_TAC & BIT(2)
sys_clocks = system counter
IF old_enable == 0 THEN
glitch = 0 (*)
ELSE
IF new_enable == 0 THEN
glitch = (sys_clocks & (old_clocks/2)) != 0
ELSE
glitch = ((sys_clocks & (old_clocks/2)) != 0) && ((sys_clocks & (new_clocks/2)) == 0)
END IF
END IF
```

The sentence marked with a (\*) has a different behaviour in GBC (AGB
and AGS seem to have strange behaviour even in the other statements).
When enabling the timer and maintaining the same frequency it doesn't
glitch. When disabling the timer it doesn't glitch either. When another
change of value happens (so timer is enabled after the write), the
behaviour depends on a race condition, so it cannot be predicted for
every device.

## Timer Overflow Behaviour

When TIMA overflows, the value from TMA is loaded and IF timer flag is
set to 1, but this doesn't happen immediately. Timer interrupt is
delayed 1 M-cycle from the TIMA overflow. The TMA reload to
TIMA is also delayed. For 1 M-cycle, after overflowing TIMA, the value
in TIMA is $00, not TMA. This happens only when an overflow happens, not
when the upper bit goes from 1 to 0, it can't be done manually writing
to TIMA, the timer has to increment itself.

For example (SYS here is the lower 8 bits of the system counter):

Timer overflows:

[A] [B]
SYS FD FE FF |00| 01 02 03
TIMA FF FF FF |00| 23 23 23
TMA 23 23 23 |23| 23 23 23
IF E0 E0 E0 |E0| E4 E4 E4

Timer doesn't overflow:

[C]
SYS FD FE FF 00 01 02 03
TIMA 45 45 45 46 46 46 46
TMA 23 23 23 23 23 23 23
IF E0 E0 E0 E0 E0 E0 E0

- During the strange cycle \[A\] you can prevent the IF flag from being
set and prevent the TIMA from being reloaded from TMA by writing a value
to TIMA. That new value will be the one that stays in the TIMA register
after the instruction. Writing to DIV, TAC or other registers won't
prevent the IF flag from being set or TIMA from being reloaded.

- If you write to TIMA during the M-cycle that TMA is being loaded to it
\[B\], the write will be ignored and TMA value will be written to TIMA
instead.

- If TMA is written the same M-cycle it is loaded to TIMA \[B\], TIMA is
also loaded with that value.

- This is a guessed schematic to explain the priorities with registers
TIMA and TMA:

![](imgs/timer_tima_tma_detailed.svg "imgs/timer_tima_tma_detailed.svg")

TMA is a latch. As soon as it is written, the output shows that value.
That explains that when TMA is written and TIMA is being incremented,
the value written to TMA is also written to TIMA. It doesn't affect the
IF flag though.
Notice how the bits themselves are connected to the multiplexer and then to the falling-edge detector; this causes a few odd behaviors:

- Resetting the entire system counter (by writing to `DIV`) can reset the bit currently selected by the multiplexer, thus sending a "Timer tick" and/or "[DIV-APU event](<#DIV-APU>)" pulse early.
- Changing which bit of the system counter is selected (by changing the "Clock select" bits of [`TAC`]) from a bit currently set to another that is currently unset, will send a "Timer tick" pulse.
(For example: if the system counter is equal to \$3FF0 and `TAC` to \$FC, writing \$05 or \$06 to `TAC` will instantly send a "Timer tick", but \$04 or \$07 won't.)
- On monochrome consoles, disabling the timer if the currently selected bit is set, will send a "Timer tick" once.
This does not happen on Color models.
- On Color models, a write to `TAC` that fulfills the previous bullet's conditions *and* turns the timer on (it was disabled before) may or may not send a "Timer tick".
The exact behaviour varies between individual consoles.

## Timer overflow behavior

When `TIMA` overflows, the value from `TMA` is copied, and the timer flag is set in [`IF`], but **one M-cycle later**.
This means that `TIMA` is equal to \$00 for the M-cycle after it overflows.

This only happens when `TIMA` overflows from incrementing, it cannot be made to happen by manually writing to `TIMA`.

Here is an example; `SYS` represents the lower 8 bits of the system counter, and `TAC` is \$FD (timer enabled, bit 1 of `SYS` selected as source):

<figure><figcaption>

`TIMA` overflows on cycle <var>A</var>, but the interrupt is only requested on cycle <var>B</var>:

</figcaption>

M-cycle | | ||<var>A</var>|<var>B</var>||&#8203;
--------|----|----|----|--------|----|----|---
`SYS` | 2B | 2C | 2D | 2E | 2F | 30 | 31
`TIMA` | FE | FF | FF | **00** | 23 | 24 | 24
`TMA` | 23 | 23 | 23 | 23 | 23 | 23 | 23
`IF` | E0 | E0 | E0 | **E0** | E4 | E4 | E4

</figure>

Here are some unexpected behaviors:

1. Writing to `TIMA` during cycle <var>A</var> acts as if the overflow **didn't happen**!
`TMA` will not be copied to `TIMA` (the value written will therefore stay), and bit 2 of `IF` will not be set.
Writing to `DIV`, `TAC`, or other registers won't prevent the `IF` flag from being set or `TIMA` from being reloaded.
2. Writing to `TIMA` during cycle <var>B</var> will be ignored; `TIMA` will be equal to `TMA` at the end of the cycle anyway.
3. Writing to `TMA` during cycle <var>B</var> will have the same value copied to `TIMA` as well, on the same cycle.

Here is how `TIMA` and `TMA` interact:

{{#include imgs/src/timer_tima_tma_detailed.svg:2:}}

<details><summary>Explanation of the above behaviors:</summary>

1. Writing to `TIMA` blocks the falling edge from the increment from being detected (see the `AND` gate)[^write_edge].
2. The "Load" signal stays enabled for the entirety of cycle <var>B</var>, and since `TIMA` is made of <abbr title="T-flip-flop with Asynchronous Load">TAL</abbr> cells, it's constantly copying its input.
However, the "Write to TIMA" signal gets reset in the middle of the cycle, thus the multiplexer emits `TMA`'s value again; in essence, the CPU's write to `TIMA` *does* go through, but it's overwritten right after.
3. As mentioned in the previous bullet point, `TIMA` constantly copies its input, so it updates together with `TMA`.
This and the previous bullet point can be emulated as if `TMA` was copied to `TIMA` at the very end of the cycle, though this is not quite what's happening in hardware.

[^write_edge]: This is necessary, because otherwise writing a number with bit 7 reset (either from the CPU or from `TMA`) when `TIMA`'s bit 7 is set, would trigger the bit 7 falling edge detector and thus schedule a spurious interrupt.

</details>

[`TAC`]: <#FF07 — TAC: Timer control>
[`IF`]: <#FF0F — IF: Interrupt flag>
4 changes: 2 additions & 2 deletions src/imgs/src/timer_simplified.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions src/imgs/src/timer_tac_bug_dmg.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit acd54eb

Please sign in to comment.