Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Timer

In this part

Four chapters cover the SM83-side subsystems: the timer (this chapter), interrupt dispatch, HALT, EI, and the IME pipeline, and the IF register.

The timer's behavioural surface — DIV, TIMA/TMA/TAC semantics, the reload quirks, the write-during-reload edge cases — is documented in Pan Docs, and this book does not restate it. (gb-ctr defines the four timer registers in its memory-map tables but does not yet cover their behaviour.) What this chapter adds is the gate inventory underneath: the actual counter cells, the exact register-to-counter bit mapping, and the picosecond-resolution edge sequence of the reload cycle.

At a glance

  • DIV and TIMA share one free-running 16-bit ripple counter that increments per M-cycle; FF04 reads reg_div16[13:6].
  • TIMA increments on the falling edge of the TAC-selected bit through a NOR gate — which is why TAC disables and DIV writes can themselves increment it.
  • IF[2] and the TMA load both fire at the start of the reload M-cycle — the "one-M-cycle delay" is wrap-late-in-one-cell to IF-early-in-the-next.
  • MEXU holds the TIMA cells transparent for the whole reload M-cycle — the window where the documented TIMA/TMA write interactions live.
GateRoleTypeClock / TriggerNotes
UKUP … UPOFreg_div16 bits 0–15dffM-cycle boundary, then ~Q rippleThe free-running divider
reset_div_nDivider reset netnorUCOB (DIV write), reset, TAPEResets all 16 DFFs
REGA … NUGATIMA bits 0–7tffnl (toggle FF with load)LSB by SOGU; ripple by lower bit's qmexu↑ switches all 8 to level-sensitive load
UKAP / TEKO / TECYTAC mux chainmuxiSelect the reg_div16 bit driving TIMA
SOGUTIMA clock gatenor2TECY, sabo_nTAC.2 low ties the TIMA clock high
NYDUNUGA pre-wrap capturedffrCLK9↑Holds NUGA's previous M-cycle value
MERYWrap detectnor2NUGA, nydu_nRises when the MSB just fell
MOBAWrap capturedffrCLK9↑Q = int_timer
MEKE / MEXUReload enable chainnot / nand3MEXU drives the tffnl load pins
NYBOIF[2] capturedffsrclocked by int_timerIF register

The 16-bit counter

DIV and TIMA share one free-running 16-bit ripple counter (reg_div16): bit 0 toggles at the M-cycle boundary; each higher bit clocks from the bit below. It runs unconditionally outside STOP. After a reset deassert it starts cleanly from 0 — the first M-cycle boundary tick reads 0x0001, with no reset artefacts on any bit (dmg-sim measurement).

The advance edge, measured by ps-bisect: a 976,000 ps period exactly. Bit 0 toggles ~1.2 ns after the master-clock edge separating consecutive M-cycles, and each carry stage adds ~1.3 ns of ripple — a boundary carrying through k bits settles ~(1.2 + 1.3k) ns in.

DIV is bits [13:6], not the top byte. The FF04 register reads reg_div16[13:6] combinationally (verified directly in the netlist model). Because the counter increments per M-cycle rather than per T-cycle, bit 6 toggles every 64 M-cycles = 256 T-cycles — yielding exactly the documented 16,384 Hz DIV rate. The bit numbering differs from Pan Docs's T-cycle-scale convention; the observable register matches. A DIV write clears the whole counter through reset_div_n.

The TIMA path

TIMA is eight toggle cells (REGA → POVY → PERU → RATE → RUBY → RAGE → PEDA → NUGA), the LSB clocked by SOGU and each higher bit by the lower bit's q — so bit N+1 toggles on bit N's falling edge. The TAC selection, in reg_div16 terms:

TAC selectFrequencyreg_div16 bit feeding TIMA
004,096 Hzbit 7
01262,144 Hzbit 1
1065,536 Hzbit 3
1116,384 Hzbit 5

TIMA increments on the falling edge of the selected bit, gated by TAC.2 through SOGU — which is why a TAC disable (or a DIV write that drops the selected bit) can itself produce an increment: the gate output falls. (The behavioural consequences are documented in Pan Docs; the gate explains them all.)

The reload cycle, edge by edge

flowchart LR
    NUGA["NUGA↓<br>TIMA MSB falls"] --> MERY["MERY<br>wrap detect"]
    NYDU["NYDU<br>pre-wrap capture"] -- "q_n" --> MERY
    MERY -- "CLK9↑ (next M-cycle)" --> MOBA["MOBA<br>int_timer"]
    MOBA --> NYBO["NYBO<br>IF[2] set"]
    MOBA --> MEKE["MEKE"]
    MEKE --> MEXU["MEXU<br>TMA load enable"]
    MEXU -- "tffnl cells transparent" --> TIMA["TIMA ← TMA"]

NUGA's falling edge is the only TIMA transition that distinguishes the 0xFF → 0x00 wrap. The wrap-detect and reload sequence spans exactly two M-cycles — the wrap M-cycle (TIMA ripples to 0x00 late in it) and the reload M-cycle (IF[2] and the TMA load fire at its start). Measured (dmg-sim measurement; offsets from each M-cycle's opening CLK9↑):

EventΔNotes
Wrap M-cycleTIMA = 0xFF entering
SOGU↓ (increment edge)+2,998 psselected bit falls
TIMA ripples 0xFF → 0x00+4,600 → +15,500 ps~1,550 ps per stage, NUGA last
MERY↑ (wrap detect)+15,700 ps173 ps after NUGA
Reload M-cycleopens at the next CLK9↑
MOBA.q↑ (= int_timer)+0 psDFF capture on the boundary edge
NYBO.q↑ (IF[2] set)+931 ps
MEKE↓+951 ps
MERY↓+1,032 psNYDU has caught up
MEXU↑ (TMA load begins)+2,005 psall 8 tffnl cells go transparent
TIMA visibly = TMA≈ +2,000 pslevel-sensitive load
MEXU↓+978,463 ps2,463 ps into the following M-cycle

The key phase fact: both the interrupt flag and the TMA load fire at the start of the reload M-cycle — the CLK9 edge that closes the wrap M-cycle — not at its end. The documented "one-M-cycle delay" between overflow and interrupt is the gap from the wrap late in one M-cycle (~+15.7 ns in) to the IF rise early in the next (+931 ps in). MEXU stays high through the whole reload M-cycle — the window where the documented TIMA/TMA write interactions play out — and releases 2,463 ps into the next.

The dispatch-side continuation of int_timer — through the IF latch and into the CPU's interrupt chain — is in the IF register and interrupt dispatch.