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, and that idiom has three parts. A small ripple up-counter is loaded with ~pace through a level-sensitive load net. A fire latch catches the counter's saturation on a frame-sequencer strobe. A pace-0 detector 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, so the first fire comes 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) is captured into BEXA on the next AJER edge. BEXA reloads the counter, then 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, because the l pin overrides the toggle path. The effective first-fire time is (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 CH1's period source, the sweep shadow accumulator acc_d — the shadow — shift M-cycles late (below). When the check resolves with direction = add and adder carry-out, ch1_sum_ovfl_n falls and BONE rises. The channel-stop OR4 then clears the running latch, so the channel is disabled and NR52 bit 0 reads 0. Shift = 0 skips the commit (item 2) and resolves in one operand-load cycle. That single calc — 2 × shadow — disables the channel ≈ 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. That recheck is 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 after the fire. The extra half M-cycle is there because 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, and a retrigger one step earlier in the grid lands at + 7.75.

Measured: the sweep timeline end to end

The timeline has five measured parts (dmg-sim measurement). The trigger-reload cascade runs restart → CYMU → all three cells loaded in ~1.7 ns, with the window ≈ 975 ns = one ch1_1mhz cycle. The ripple free-runs, +1 per 128 Hz tick, ripple-binary. The NR10 write itself fires the sweep before the trigger: releasing BEXA's reset with COZE already saturated fires immediately, which is harmless when the channel is stopped. The first natural fire lands at trigger→first-tick + (pace−1) periods. Steady-state fires follow at exactly pace × 7.995 ms (sim time) thereafter. 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, so their power-on state is undefined on silicon. The boot ROM never writes NR10 (pace 0 throughout), so BEXA stays reset. With no reset path and no load, 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 / or2—vol 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 holds JOPA in reset. JUPU also 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 has two outcomes. If the write commits before the horu_512hz↑ sample, JOPA is held in reset and the tick is suppressed. If it commits after, JOPA has fired and the queued toggle completes, so the tick is 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. That can produce a spurious carry edge into the next bit; measured, $50→$18 on a volume-5 counter lands at 10. This is 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. During each write strobe the whole data byte transiently reads $FF (~16–19 ns) before settling to the written value. That is 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. The toggle therefore commits while the muxes are still up/carry, and the whole carry ripple completes there too. 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. The counter wraps 15→0 freely, because 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, because the transient dip still fires. A pace 1→0 return write completes no HOFO pulse and does not step on its own. In decrease mode, though, the transient's direction edges can switch a ripple mux combinationally — the same spurious carry a deliberate flip produces. $F0-over-$F1 therefore lands +2 where $18-over-$19 holds.

Saturation stop: HEPO captures the saturated-low/high decodes (volume 0 + down / 15 + up) on each fire. Those decodes 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. A 64 Hz tick inside the window is skipped, so the first fire comes 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): the counter saturates 14.1 ms after the trigger. The ROM's pace→0 write commits 0.18 ms before the next 512 Hz sample, so JOPA never fires. Volume stays 0 and the channel is silent, matching hardware.
  2. The frame-sequencer-phase race (_1, env1). That ROM 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. The write falls inside the (JOPA↑, HOFO↓) window, though, freezing the in-flight toggle: volume never steps and the channel is 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. This 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 across that window, the counter provably does not advance. The first fire is one full period late, and the channel is silent.

For implementors

The unified rule has two halves. One load-window test fixes the first-fire time: hold the counter at ~pace while the load net is open, and ignore any tick edge inside it. 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, with volume decayed to 0, the stop latch set, and the counter holding ~3 from the chime's pace. CH2's envelope is all-zero with the stop latch clear. See post-boot state.