Skip to content

A Square Wave

Enable pulse 1 in $4015, then set its duty, volume, sweep and 11-bit timer. It holds a tone with no further CPU work, as long as nothing mutes it.

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.

square-wave.inc
; 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
    rts

The 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.

demo.asm
; =============================================================================
; 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, $00

Save demo.asm and square-wave.inc together and build with:

Shell
asm198x --dialect ca65 demo.asm -o demo.nes

Load 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:

Code
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 $4015 clear, the length counter is forced to zero and a $4003 write cannot load it. Enable first, then write $4003.
  • The length counter ran out. The top five bits of $4003 choose a starting count from a 32-entry table; index 0 is 10. If bit 5 of $4000 is 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, as pulse1_tone does.
  • The envelope decayed. With bit 4 of $4000 clear, bits 3–0 become the envelope’s period instead of the volume. The volume starts at 15 and drops by one every period + 1 quarter-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 $7FF mutes the channel. So $00 in $4001 silences every timer from $400 up, 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.

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.