Code status: assembled and emulator-executed. NTSC NES with NROM and rendering off, in Emu198x. The recorded checks identify the tested sources and observations. They establish the stated cases on that configuration; original-hardware checks are separate.
Overview
The NES’s APU has five channels. Two pulse (square) channels, a triangle and a noise channel generate their waveforms in hardware from a few register settings. The fifth, the DMC, plays samples: 1-bit delta-encoded data fetched from cartridge memory, or 7-bit levels the program writes to $4011 itself.
This pattern uses only pulse 1. A pulse channel needs no waveform data. Switch it on, choose its duty (the shape of the square), its volume and an 11-bit timer that sets the pitch, and it keeps oscillating until something changes or mutes it. Unlike Paula’s sample DMA, nothing streams from memory.
The example targets an NTSC NES with an NROM cartridge (mapper 0) and rendering switched off.
The routine’s contract
pulse1_tone takes the timer’s low byte in A and its top three bits in X. The full 11-bit timer must be 8 to $7FF; below 8 the channel is muted. It changes A and preserves X and Y.
The caller must have set bit 0 of $4015 first. Writing $4003 loads the channel’s length counter only while the channel is enabled. Write $4003 first and enable afterwards, and the counter stays at zero: the channel is silent.
; Hold a tone on pulse 1: 50% duty, constant volume 15, no sweep.
; A = timer low byte, X = timer high bits (0-7). The 11-bit timer must be
; 8-$7FF; below 8 the channel is muted. NTSC pitch is 1789773/(16*(timer+1)) Hz.
; The caller owns $4015 and must already have set its bit 0: the $4003 write
; loads the length counter only while the channel is enabled.
; Clobbers A. Preserves X and Y.
PULSE1_CTRL = $4000 ; DDLC VVVV: duty, length halt, constant volume, volume
PULSE1_SWEEP = $4001 ; EPPP NSSS: enable, period, negate, shift
PULSE1_LO = $4002 ; timer bits 0-7
PULSE1_HI = $4003 ; LLLL LHHH: length index, timer bits 8-10
pulse1_tone:
pha
lda #%10111111 ; duty 2 (50%), halt the length counter, constant volume 15
sta PULSE1_CTRL
lda #%00001000 ; sweep disabled, negate set: the sweep cannot mute it
sta PULSE1_SWEEP
pla
sta PULSE1_LO
stx PULSE1_HI ; length index 0, timer high bits; restarts the duty step
rtsThe routine leaves $4015 to the caller because that one register holds the enable bits for all five channels. Writing %00000001 turns pulse 1 on and every other channel off, and switching a channel off clears its length counter. In a game, every sound routine has to share one copy of that byte.
$4000 packs four settings into one byte:
| Bits | Value here | Meaning |
|---|---|---|
| 7–6 | 10 |
Duty 2: 50%, a true square |
| 5 | 1 |
Halt the length counter (also the envelope’s loop flag) |
| 4 | 1 |
Constant volume: bits 3–0 are the volume |
| 3–0 | 1111 |
Volume 15 |
The $4003 write also restarts the duty sequence and the envelope. A program that changes pitch while a note plays usually writes only $4002, so the wave’s phase doesn’t jump and click.
Run the complete example
The demonstration is a complete NROM image. It enables pulse 1, calls pulse1_tone with timer 253, then loops forever, copying $4015 into status so a debugger can see that the length counter is still running. The tone needs none of that loop’s time.
; =============================================================================
; A SQUARE WAVE - NES (NROM)
; Hold A-4 on pulse 1 with no further CPU work. Rendering stays off.
; =============================================================================
APU_STATUS = $4015 ; write: channel enables; read: length counters > 0
APU_FRAME = $4017 ; frame counter mode and IRQ inhibit
PPUCTRL = $2000
PPUMASK = $2001
.segment "HEADER"
.byte "NES", $1A
.byte 2 ; 2 x 16 KB PRG
.byte 1 ; 1 x 8 KB CHR
.byte $00, $00 ; mapper 0, horizontal mirroring
.byte 0,0,0,0,0,0,0,0
.segment "ZEROPAGE"
status: .res 1 ; last $4015 read; bit 0 = pulse 1 length counter > 0
.segment "CODE"
reset:
sei
cld
ldx #$40
stx APU_FRAME ; 4-step sequence, frame IRQ inhibited
ldx #$FF
txs
inx
stx PPUCTRL ; no NMI
stx PPUMASK ; rendering off
stx $4010 ; no DMC IRQ
lda #%00000001 ; enable pulse 1 only, before the length load
sta APU_STATUS
lda #$FD ; timer 253 = $0FD
ldx #$00
jsr pulse1_tone ; NTSC: 1789773/(16*254) = 440.4 Hz
hold:
lda APU_STATUS ; the tone needs no CPU; this only records state
sta status
jmp hold
nmi:
irq:
rti
.include "square-wave.inc"
.segment "VECTORS"
.word nmi, reset, irq
.segment "CHARS"
.res 8192, $00Save demo.asm and square-wave.inc together and build with:
asm198x --dialect ca65 demo.asm -o demo.nesLoad demo.nes in an NES emulator set to NTSC. The screen stays black. You should hear a steady A-4 at about 440 Hz that keeps going, without fading, until you reset. On a PAL machine the same timer gives about 409 Hz.
Timer into pitch
A pulse channel’s 11-bit timer counts down once every two CPU cycles. Its 8-step duty sequence moves on one step each time the timer reloads, so one full wave takes 16 × (timer + 1) CPU cycles:
frequency (Hz) = CPU clock / (16 × (timer + 1))The CPU clock is 1,789,773 Hz on NTSC and 1,662,607 Hz on PAL. A bigger timer means a lower note:
| Timer | NTSC | PAL |
|---|---|---|
126 ($07E) |
880.8 Hz | 818.2 Hz |
253 ($0FD) |
440.4 Hz | 409.1 Hz |
1024 ($400) |
109.1 Hz | 101.4 Hz |
Halving timer + 1 doubles the frequency, so 126 is exactly an octave above 253. Music routines keep one timer value per semitone in a table. The triangle channel’s sequence has 32 steps rather than 8, so the same timer sounds an octave lower there.
Why a pulse channel goes silent
Each of these is a classic first-tone bug.
- The channel was never enabled, or was enabled too late. With bit 0 of
$4015clear, the length counter is forced to zero and a$4003write cannot load it. Enable first, then write$4003. - The length counter ran out. The top five bits of
$4003choose a starting count from a 32-entry table; index 0 is 10. If bit 5 of$4000is clear, the frame counter decrements it about 120 times a second (NTSC, 4-step mode). At zero the channel stops abruptly. Index 0 lasts about 75–83 ms, depending on when the first decrement arrives. This is a cut-off, not a fade. Setting bit 5 halts the count, aspulse1_tonedoes. - The envelope decayed. With bit 4 of
$4000clear, bits 3–0 become the envelope’s period instead of the volume. The volume starts at 15 and drops by one everyperiod + 1quarter-frames (about 240 a second on NTSC) until it reaches 0. Because bit 5 doubles as the envelope’s loop flag, setting it with bit 4 clear makes the decay restart at 15 forever rather than hold. - The sweep unit muted it. The sweep keeps calculating a target period even when it is disabled. With the negate flag clear and a shift of 0, that target is twice the current timer, and any target above
$7FFmutes the channel. So$00in$4001silences every timer from$400up, the bottom octave.$08(disabled, negate set) keeps the target at or below the current timer. A timer below 8 is always muted.
Set the sweep register to $00 and the timer to $400. Does $4015 bit 0 still read 1?
Show the explanation
Yes. The length counter is loaded and halted, so $4015 reports the channel as active. The sweep unit’s muting happens after the length counter and isn’t visible in $4015. A status read can’t tell you whether you’ll hear anything.
Trade-offs
| Aspect | Cost |
|---|---|
| CPU | None once the five writes are made; the channel oscillates in hardware |
| Memory | About 30 bytes of code |
| Limitation | Four fixed duty shapes; 4-bit volume through a non-linear mixer; one note per channel |
When to use: Leads, bleeps and harmony, and the first step of any music routine.
When to avoid: Speech or recorded drums, which need the DMC. Bass often goes on the triangle instead.
What has been checked
The sample’s source and verification recipe runs the demonstration and six variants in Emu198x. It reads $4015 from the running program and measures the captured audio both in emulated time and at the WAV header’s playback rate. The capture duration must also match emulated time, and deliberately mislabelling a WAV must fail both pitch and duration checks. It confirms the three pitches above, the octave ratio, muting by sweep $00 at timer $400 and by a timer below 8, silence when $4003 is written before enabling, and a full-level cut-off after about 82 ms when the length counter runs. These are emulator observations. Listening and original-hardware checks are separate.
Related
Patterns: NMI Game Loop
Vault: APU | Nintendo Entertainment System
Sources
NESdev Wiki: APU (channels and $4015), APU Pulse (timer, duty, $4003 side effects), APU Sweep (“Muting”), APU Length Counter, APU Envelope, APU Frame Counter, APU Triangle and APU DMC. These pages are community hardware research. The pitch table is calculated from the formula above.