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

Sweep and Envelope

The sweep unit (CH1 only) and the volume envelopes (CH1/CH2) share one silicon idiom: a small ripple up-counter loaded with ~pace through a level-sensitive load net, a fire latch that catches the counter's saturation on a frame-sequencer strobe, and a pace-0 detector that holds the fire latch in reset. Every documented quirk — "+1 on trigger near a tick", pace-0 pausing, zombie volume, the saturation stop — is a consequence of those gates.

At a glance

  • Both timers are ~pace-loaded up-counters that fire on saturation — sweep at 128 Hz, envelope at 64 Hz.
  • Pace = 0 pauses, not "period 8": the pace-0 decode holds the fire latch in reset (the envelope's is doubly paused).
  • The "+1 quirk" is the load window: a frame-sequencer tick landing inside the ~1 µs reload window is lost — first fire one period late.
  • The volume step is a two-edge event (fire rise arms, fire fall commits) — every mid-channel NRx2 write subtlety follows from it.
  • Saturation is a stop latch, not a clamp — an unstopped 4-bit counter would simply wrap.

The CH1 sweep timer

A 3-bit counter clocked by the frame sequencer's 128 Hz tap and loaded with NOT(NR10[6:4]) by either reload path through one shared net:

GateRoleTypeClock / TriggerNotes
cate_128hzSweep toggle clocknot chain= byfe_128hz, the frame-sequencer 128 Hz tap (APU clocks)Held low while apu_reset=1
CUPO / CYPU / CAXYSweep counter bits 0–2tffnlcate_128hz, then bit-to-bit rippleLoad: q ← NOT(NR10[4..6])no reset path at all
COZESweep counter-at-max detectorand3CUPO, CYPU, CAXYHigh at counter = 7; drives BEXA's data
BAVESweep pace = 0 detectorand3NR10[6:4] complementsHolds BEXA in async reset via BURY
BEXASweep-fire latchdffrAJER (the 2 MHz CH1 prescaler)Captures COZE; while high, reloads the counter; falls on the next AJER↑. Held at 0 when pace = 0
DAFA → CYMULoad enablenor2 + notBEXA, ch1_restartCYMU = OR(BEXA, ch1_restart) drives all three l pins

The two reload paths:

  • Trigger reload — the same ch1_restart synchroniser as the period divider; no second synchroniser, no pending-reload latch.
  • Self-reload on fire — COZE (counter = 7) captured into BEXA on the next AJER edge; BEXA reloads the counter and drops one cycle later.

Fire interval = pace × 1/128 s (Pan Docs' rule, with the table exact for every pace). Pace = 0 pauses via BAVE holding BEXA in async reset — the counter still ripples and saturates invisibly.

Rule: the +1 quirk is the load window

cate_128hz free-runs; a trigger landing so that the 128 Hz tick falls inside the ~1 µs load window loses that tick (the l pin overrides the toggle path) — effective first-fire time (pace + 1) × 1/128 s. Just-after, just-before, and inside-the-window are the three landing cases; only the third loses a tick.

On each fire, three independent things happen:

  1. The counter reloads (above).
  2. The sweep-adder commit strobes (ch1_freq_upd1/2) fire — gated by AND3(BEXA, no-overflow, shift ≠ 0). Shift = 0 means the computed period is not written back (Pan Docs' rule, as a literal AND term).
  3. The overflow check runs regardless of shift, but it does not resolve at the fire — it samples a registered snapshot of the shadow (below), shift M-cycles late. When it resolves, with direction = add and adder carry-out, ch1_sum_ovfl_n falls, BONE rises, and the channel-stop OR4 clears the running latch — channel disabled, NR52 bit 0 reads 0. Shift = 0 skips the commit (item 2) and resolves in one operand-load cycle: the single calc — 2 × shadow — disables ≈ 1 M-cycle after the fire, so any shadow ≥ 0x400 cuts the channel.

The snapshot is loaded shift M-cycles late because a second counter must count out the shift first:

GateRoleTypeClock / TriggerNotes
BYRA / CAJA / COPASweep shift-step counter bits 2–0tffnlch1_shift_clk, then bit-to-bit rippleLoad ~shift; saturate shift steps after a fresh shadow
ch1_ld_sumSweep-adder operand-load strobenot_x4Pulses on BYRA·CAJA·COPA saturationClocks the adder's operand registers — the adder never reads the live shadow

Rule: a non-overflowing fire rechecks ~shift M-cycles later

Because the check lands shift M-cycles after the shadow is presented, a fire whose first calc passes does not cut the channel: item 2 commits the result to the shadow, and the next ch1_ld_sum strobe rechecks the committed value — the double-check. A sweep that overflows only on the recheck stays audible for shift M-cycles past the fire. A trigger's own shift ≠ 0 check resolves the same way, shift M-cycles after the write.

Measured: the overflow-check latency is shift-dependent (dmg-sim)

From the fire to the channel-disable:

shiftfire → disable (M-cycles)event
01.0single calc (no double-check)
11.5recheck
77.5recheck

For the double-check (shift ≥ 1) the recheck lands shift M-cycles + 0.5 — BEXA fires on the 2 MHz AJER grid, half an M-cycle off the step counter's grid. Shift 0 has no double-check; its single calc disables ≈ 1 M-cycle after the fire (one operand-load cycle). A shift ≠ 0 trigger runs its check shift M-cycles after the write, but a CPU write's sub-cycle phase is free: shift 7 lands at trigger + 8.75 (a re-trigger one step earlier in the grid, + 7.75).

Measured: the sweep timeline end to end

The trigger-reload cascade (restart → CYMU → all three cells loaded in ~1.7 ns; window ≈ 975 ns = one ch1_1mhz cycle), the free-running ripple (+1 per 128 Hz tick, ripple-binary), a pre-trigger fire on the NR10 write itself (releasing BEXA's reset with COZE already saturated fires immediately — harmless when the channel is stopped), the first natural fire at trigger→first-tick + (pace−1) periods, and steady-state fires at exactly pace × 7.995 ms (sim time) thereafter (dmg-sim measurement). A hardware-verified NR52 probe between trigger and first fire reads the channel still enabled.

Post-boot: the sweep counter cells have no reset at all — their power-on state is undefined on silicon, and the boot ROM never writes NR10 (pace 0 throughout), so BEXA stays reset and the counter state at handoff is power-on residue. Tests needing a known state must write NR10 and trigger.

The volume envelope (CH2 cells; CH1 mirrors cell-for-cell)

Three pieces: the envelope counter, the fire latch, and the volume up/down counter.

GateRoleTypeClock / TriggerNotes
kene64 Hz envelope toggle clocknot chain from JYNAJYNA = /2 of byfe_128hzThe frame-sequencer ripple's last stage (APU clocks)
JORE / JONA / JEVYEnvelope counter bits 0–2tffnlkene, then bit-to-bit rippleLoad: q ← NOT(NRx2[0..2]) via JAKE
KYVOEnvelope counter-at-max detectorand3JORE, JONA, JEVYHigh at counter = 7; drives JOPA's data
JUPUEnvelope pace = 0 detectornor3NRx2[2:0]Feeds both JOPA's reset (via HAFE) and HOFO
JOPAEnvelope-fire latchdffrhoru_512hzCaptures KYVO; while high, reloads the counter; falls on the following 512 Hz tick. Held at 0 when pace = 0
HYLY → JAKELoad enablenor2 + notJOPA, ch2_restartJAKE = OR(JOPA, ch2_restart) drives all three l pins
HOFOVolume bit-0 toggle clockor3JOPA, JUPU, ch2_eg_stopHigh while firing, paced-off, or stopped — the toggle commits on its fall
FENO / FETE / FOMY / FENAVolume counter bits 0–3tffnlHOFO, then direction-muxed rippleLoad: q ← NRx2[7:4] on trigger
(per-bit AO22 muxes)Direction ripple selectsao22NRx2[3]Up: ripple on carry; down: ripple on borrow
EMYR / FYRE → GUFYSaturation detectorsnor5 / nand5+not / or2vol 0 + down / vol 15 + up
HEPOSaturation capturedffrJOPA↑Reset on trigger or apu_reset
JEME / ch2_eg_stopEnvelope-stop latchnor_latchSet: HEPO; Reset: trigger / apu_resetPins HOFO high — the saturation lockout

Fire interval = pace × 1/64 s. Pace = 0 is doubly paused: JUPU both holds JOPA in reset and pins HOFO high so the volume bit-0 clock can never complete a pulse. (Pan Docs' "period 0 treated as 8" does not apply to the DMG envelope — the gates pause it outright.)

Rule: the volume step is a two-edge event

HOFO rises at the fire (JOPA↑) and the tffnl slave commits the toggle only on HOFO's fall (the natural JOPA↓ one 512 Hz cycle later). Everything subtle about mid-channel NRx2 writes follows from it.

The mid-channel write cases:

  • A pace→0 write between fires just freezes the volume (HOFO rises once and never falls — no toggle).
  • A pace→0 write racing the fire itself: if the write commits before the horu_512hz↑ sample, JOPA is held in reset — tick suppressed; if after, JOPA has fired and the queued toggle completes — tick applied.
  • A pace→0 write landing inside the (JOPA↑, HOFO↓) window freezes the in-flight toggle — the fire happened but the volume never steps.
  • A direction flip mid-channel switches each ripple mux combinationally and can produce a spurious carry edge into the next bit — measured: $50$18 on a volume-5 counter lands at 10. One of two silicon paths behind "zombie mode" volume changes; the second — the per-write +1 step — is measured below.

Measured: zombie volume stepping — every pace-0 write ticks the counter +1

The NRx2 cells are transparent latches, and during each write strobe the whole data byte transiently reads $FF (~16–19 ns) before settling to the written value — measured directly: bits written 0 read 1 across the transient. The pace bits dip JUPU, so HOFO completes one fall→rise pulse and the volume counter gets exactly one clock; the direction bit (NRx2[3]) reads 1 through the same transient, so the toggle commits — and the whole carry ripple completes — while the muxes are still up/carry. The step is therefore +1 regardless of the direction the value selects: a decrease-mode write increments. Measured on a running CH2, pace = 0, zero variance (dmg-sim, the purpose-built zombie_x8_ch2 and decrease-mode zombie_x0_dec_ch2 ROMs): every $18-over-$18 and every $F0-over-$F0 write steps +1, wrapping 15→0 freely — HEPO and JEME never fire under pace = 0, so no stop exists (the "repeat to decrement" idiom). A pace 0→1 write still steps +1 (the transient dip still fires); a pace 1→0 return write completes no HOFO pulse and does not step on its own — though in decrease mode the transient's direction edges can switch a ripple mux combinationally (the same spurious carry a deliberate flip produces), so $F0-over-$F1 lands +2 where $18-over-$19 holds.

Saturation stop: the saturated-low/high decodes (volume 0 + down / 15 + up) are captured by HEPO on each fire and latch the envelope-stop (JEME), which pins HOFO high — there is no arithmetic clamp; an unstopped 4-bit counter would simply wrap. The next trigger clears both latches.

The trigger window and the "+1 quirk"

Identical shape to the sweep: the reload window is one ch2_1mhz cycle (measured ≈ 975 ns, opening ~950 ps after ch2_restart↑), kene free-runs, and a 64 Hz tick inside the window is skipped — first fire one full period late. Pan Docs documents this as the envelope's trigger-near-a-tick reload quirk; the gate content is just the l pin override.

The three measured regimes

Three measured anchors pin the complete model (dmg-sim measurements, gambatte ch2_init[_reset]_env_counter_timing ROMs):

  1. The suppression race (_3): counter saturates 14.1 ms after the trigger; the ROM's pace→0 write commits 0.18 ms before the next 512 Hz sample; JOPA never fires; volume stays 0 — silent, matching hardware.
  2. The frame-sequencer-phase race (_1, env1 — a ROM that never touches NR52 or DIV, so its kene phase is the boot ROM's leftover ripple seed, Δ = 3 (APU clocks)). At the real boot phase, the fire lands ~1 ms before the pace→0 write — but the write falls inside the (JOPA↑, HOFO↓) window, freezing the in-flight toggle: volume never steps, channel silent (dmg-sim measurement with the real boot ROM). Under the quickboot harness's wrong seed (Δ = 7) the same ROM comes out audible — a harness artifact, not hardware. The anchor is what pins the post-boot Δ = 3 requirement for skip-boot emulators.
  3. The +1-quirk case (_11, a _reset_ ROM that re-locks the phase deterministically): the 64 Hz tick lands ~1.5 ns inside the ~975 ns load window — measured with the counter provably not advancing across it — first fire one full period late, channel silent.

For implementors

The unified rule: one load-window test fixes the first-fire time (hold the counter at ~pace while the load net is open; ignore any tick edge inside it), and the HOFO toggle-vs-pace-0-write race fixes whether a fire steps the volume. The two compose; no further mechanism is needed across the test family.

Post-boot: CH1's envelope is saturated-stopped (volume decayed to 0, stop latch set, counter holding ~3 from the chime's pace); CH2's is all-zero with the stop latch clear. See post-boot state.