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

STAT Interrupts

The STAT interrupt (IF bit 1) is set by LALU, a DFF whose clock is the combined STAT condition: SUKO, an AO2222 that OR-ANDs the four sources with their enable bits. There is no periodic clock anywhere in this chain — LALU fires when SUKO's output rises. Everything subtle about DMG STAT interrupts ("blocking", the write glitch, the boundary races) falls out of that one fact plus per-leg gate depths.

At a glance

  • LALU (IF bit 1) is clocked by the condition itself: it fires only on a genuine SUKO through-zero — "STAT blocking" is emergent, not a rule.
  • The Mode 0 leg reaches the interrupt in 0.011 dots — 0.425 dots before the CPU-visible mode change.
  • Leg swaps are decided by gate depth: ROPO (LYC) is the fastest leg, PARU (Mode 1) next, TARU/TAPA (Mode 0/2) slowest — VBlank entry and exit glitch (Cases 1/4); same-line swaps stay covered (Cases 2/3).
  • The "DMG STAT-write glitch" is ordinary transparent-latch bus settling — whether it fires depends on SUKO's through-zero, nothing else.
  • ROPO survives LCD-off (its reset is system reset, not VID_RST).
GateRoleTypeClock / TriggerNotes
LALUSTAT interrupt latch (IF bit 1)dffsrClock: VOTY; D tied highEdge-triggered by the SUKO pulse
SUKOCondition combining gateao2222(ROXE·TARU) + (RUFO·PARU) + (REFE·TAPA) + (RUGU·ROPO)
TUVA / VOTYInverter pairnotSUKO → LALU clock
TARUMode 0 conditionand2WODU, TOLUTOLU = NOT(mode1) — non-VBlank gate
PARUMode 1 conditionnot_x1popu_n= POPU.Q; also feeds TOLU
TAPAMode 2 conditionand2TOLU, SELASELA ≡ RUTU buffered — the line-end pulse
ROPOLY==LYC synced matchdff17TALU risingCaptures PALY
ROXE / RUFO / REFE / RUGUSTAT.3/.4/.5/.6 enablesdrlatch_eeCUPA strobe
WODUMode 0 conditionand2XENA, XANOFull breakdown below; mode-control is the primary chapter
PALYLY==LYC comparatornot_x1combinationalPer-bit XNORs → SUBO/SOVU → RAPE → PALY
RUPOSTAT bit 2 visible latchnor_latchs = ROPO.Q; r = PAGOTransparent in normal operation (below)

The enable bits and the FF41-write transient

The four enables are level-sensitive latches sharing the CUPA strobe — and that transparency is the netlist origin of the "DMG STAT-write glitch". During a CPU FF41 write, the data bus settles bit by bit across the write window, and the transparent latches follow it. Measured (dmg-sim measurement, gambatte enable_after_lyc_during ROM, write $40 → $08): the bits being cleared walk through a transient where all four enables read 1 simultaneously for ≈ 15.8 ns before settling to the written value.

Whether that transient fires an interrupt depends entirely on SUKO's through-zero behaviour. A transiently-set enable matters only if its paired condition is high — and if some other leg already held SUKO high, no edge occurs.

In the measured scenario (WODU high mid-H-Blank, LYC leg high then dropping), SUKO transitions exactly zero times: the Mode 0 leg takes over as the LYC leg drops, the output never dips, LALU never clocks. The glitch is ordinary transparent-latch bus-settling: SUKO = OR(EN·COND) settles per-leg, and LALU clocks only on a genuine through-zero.

WODU: the Mode 0 condition

WODU = AND2(XENA, XANO)
├── XENA = NOT(FEPO) — no sprite fetch active
└── XANO = NOT(XUGU) — pixel counter at terminal count (PX = 167)

One signal, two consumers with very different latencies: the mode transition (WODU → VOGA → XYMU, 0.436 dots — mode transitions) and the STAT chain (WODU → TARU → SUKO → TUVA → VOTY → LALU, 0.011 dots). The interrupt fires 0.425 dots before the CPU-visible mode changes.

The terminal-count WODU pulse

WODU's inputs settle at different depths. On the advance onto PX=167, XANO (a shallow NAND5 decode) rises while the deep sprite-match aggregate FEPO is still low for the new count. With a sprite stacked at X=167, this ordering produces a brief WODU pulse before the terminal sprite's match suppresses it (dmg-sim measurement):

SignalΔ from XANO↑ (ps)Event
XANO↑0PX=167 decode asserts
WODU↑ (early pulse)+483XENA still high
FEPO↑+3,141terminal sprite match
XENA↓+3,205
WODU↓+3,494pulse ends — width ≈ 3 ns

The two consumers treat the pulse asymmetrically. The combinational STAT chain catches the pulse and sets IF bit 1 at the early edge. That early edge leads the visible Mode 3 end by the whole terminal-sprite fetch extension. VOGA samples on the ALET edge after FEPO has settled and misses the pulse entirely. The mode bits therefore flip only at the sustained WODU rise after the last terminal sprite. A sprite at X=166 or below produces no pulse (it is fetched before the terminal count).

Propagation: measured leg latencies

Mode 0 path (dmg-sim measurement, 1,289 assertions, ps-identical):

SignalΔ from WODU↑ (ps)
TARU↑+801
SUKO↑+1,671
TUVA↓+2,064
VOTY↑ = LALU clock+2,637
LALU.q↑ (IF bit 1)+2,637

Mode 2 path (dmg-sim measurement, 1,289 assertions):

SignalΔ from RUTU↑ (ps)
SELA↑+2,860
TAPA↑+3,011
SUKO↑+3,881
VOTY↑ = LALU clock+4,847

The 2,210 ps differential vs Mode 0 is entirely SELA's double-inverter; the shared SUKO→VOTY tail (966 ps) and the AO2222 input delay (870 ps) match the Mode 0 leg to ps resolution. The static analysis gives LALU a tight-but-safe per-line race (diff 5.8 ge, deepest input VOTY at 64.8 ge).

The Mode 0 condition (TARU) drops in two structurally different situations.

On VBlank entry WODU stays high and TARU drops via TOLU as mode1 rises (Case 3), a cascade riding the boundary TALU↑ — TARU↓ = TALU↑ + 4,038 ps (dmg-sim measurement).

At a line start the trigger is WODU itself, de-asserting as the pixel counter resets — ≈ 1 dot after RUTU↑ (WODU↓ at +1.008 dots, TARU↓ at +1.010), quantifying Case 2's "further dot to drop". With only the Mode 0 leg enabled that SUKO drop is uncovered, exposing the whole falling leg (dmg-sim measurement, three consecutive boundaries ps-identical):

SignalΔ from WODU↓ (ps)
TARU↓+449
SUKO↓+1,465
TUVA↑+2,246
VOTY↓ = LALU clock+2,542

The AO2222 input delay (1,016 ps) and the SUKO→VOTY tail (1,077 ps) match the Mode 1 / Mode 2 legs' falling cascade to the picosecond — the tail is leg-independent, the falling mirror of the rising 870 / 966 ps. LALU clocks on VOTY's rise, so this VOTY↓ sets no interrupt; it only readies the clock for the next line's Mode 0 rise.

Two Mode 0 falling arrivals

The VBlank-entry fall rides the boundary TALU↑; the line-start fall lands a full dot later, a single AND2 after WODU's de-assert. They are different events on different reference edges — one Mode 0 falling arrival cannot time both.

Edge-triggered behaviour and the leg-swap races

Because LALU clocks on SUKO's rise, overlapping conditions produce one interrupt, not two — the "STAT IRQ blocking" behaviour emerges naturally. The interesting cases are leg swaps: one source leg falls and another rises on the same boundary. Whether SUKO dips (→ fresh interrupt) or stays covered (→ nothing) is decided by gate-prop depth, and all four configurations have been measured (dmg-sim measurements, gambatte interrupt-precedence ROM family):

Case 1 — VBlank entry, LYC + Mode 1 enabled: glitch fires. On the TALU edge ending scanline 143, the LYC arm drops via ROPO (one TALU-clocked stage — fast) while the Mode 1 arm rises via NYPE → POPU → PARU (buffered, ~1.9 ns slower). All four arms sit at 0 for 1,802 ps; SUKO dips; LALU fires ~4.4 ns after POPU's capture. IF bit 1 sets even though "the LYC interrupt ended and the VBlank interrupt began".

Case 2 — line boundary with all four enables: covered. The Mode 2 arm rises ~1 ns after RUTU while the Mode 0 arm takes a further dot to drop (WODU's deep pipeline-end conditions). At every instant some arm is high — measured across 143 consecutive boundaries: zero SUKO transitions, zero spurious interrupts. The structural ordering (Mode 2 rises fast, Mode 0 drops slowly) is intrinsic to the netlist.

Case 3 — VBlank entry, Mode 0 + Mode 1 enabled: covered. Both arms cascade off the same POPU chain: the Mode 1 arm rises at PARU (2 stages from POPU.q), the Mode 0 arm drops at TARU (4 stages, via TOLU). The rising leg arrives 1,216 ps before the falling one — no dip.

Case 4 — VBlank exit (LY 153→0), Mode 1 + Mode 2 enabled: glitch fires. The symmetric counterpart: the Mode 1 arm drops at PARU (2 stages) before the Mode 2 arm can rise at TAPA (4 stages). SUKO dips for 1,524 ps; LALU fires ~5.5 ns after POPU's fall. Measured identically on two independent ROMs.

The general structure, from the shared propagation tree:

POPU.q → popu_n → PARU (mode1)            — 2 stages: the FAST leg
                    └→ TOLU → TARU (mode0) — 4 stages
                          └→ TAPA (mode2)  — 4 stages
ROPO (LYC) — 1 TALU-clocked stage: the only leg FASTER than PARU

Rule: the through-zero test

SUKO glitches low only when, between the falling leg's transition and the rising leg's, no other (EN·COND) pair is true. On VBlank entry with Mode 1 enabled, PARU's early rise covers everything except the still-faster LYC drop (Case 1 fires, Case 3 doesn't). On VBlank exit, PARU's early fall uncovers everything (Case 4 fires). Emulators must apply each leg's transition on its own hardware edge and fire only on a genuine through-zero.

Open question: Case 4 vs hardware annotation

One ROM (gambatte m2enable/late_m1disable_ly0_3) expects IF[1]=0 at a read that dmg-sim places after a Case 4 glitch with no intervening clear — the simulation says 1. An equivalent-cascade ROM with the clear on the other side of the boundary is netlist-consistent. The disagreement is margin-sensitive. Case 4's dip is 1,524 ps wide, while the narrowest dip that hardware-verified expectations require to fire is Case 1's 1,802 ps. That 278 ps gap sits inside the scale where gate delays are model predictions rather than hardware facts (methodology). A real unit whose LALU clock threshold sits between the two would match the annotation and the simulated Case 1 — the annotation may simply be correct for the tested unit. Resolving it needs a hardware measurement. See Appendix C.

The LYC-match pipeline

LY bits + LYC bits → per-bit XNORs → SUBO/SOVU → RAPE → PALY (combinational)
  → ROPO (dff17, clk = TALU rising)         — the only registered stage
      ├→ SUKO's LYC arm (with RUGU)
      └→ RUPO (nor_latch) → tri-state → STAT bit 2

RUPO is transparent in normal operation. Its reset-side driver PAGO is static-1 outside hardware reset, so the NOR latch continuously re-evaluates and STAT bit 2 tracks ROPO.Q with gate delay only. A true latch-hold requires hardware reset and a concurrent FF41 write — irrelevant to normal operation.

LCD on/off does not reset the pipeline. ROPO's reset traces to the system-level hardware reset, not VID_RST. Across an LCD-off span, ROPO holds whatever it captured at the last TALU edge before the LCD went off; after LCD-on, the first TALU edge (+1.483 dots after VID_RST deasserts — register writes) recaptures (LY=0 == LYC). STAT bit 2 in the brief window between LCD-on and that first TALU edge reads the pre-LCD-off match value.

The LYC=153 boundary race

At the 152→153 boundary, MYTA captures FRAME_END one TALU period after POPU (line counters) and async-resets LY to 0. The reset propagates to PALY through six gate stages; ROPO's capture on the same TALU edge completes through four shorter internal stages — ROPO wins the race and captures pre-reset PALY, the LY=153 comparison.

Across three consecutive TALU rises at the boundary:

TALU riseEventROPO captures
1POPU's edge; LY already 153(153 == LYC)
2MYTA fires; LY resets after the capture(153 == LYC) — pre-reset, via the race
3LY stably 0(0 == LYC)

For LYC=153 the match window is therefore two TALU periods; for LYC=0 the match onsets at rise 3 — one TALU period after MYTA's edge. The sub-phase window between PALY's combinational transition and ROPO's next capture — where the comparator and the visible bit disagree — interacts with CPU read sampling and is worked through in CPU-visible timing at mode boundaries.