retro-silicon

Components

audio/apu-2a03

The NES's 2A03 APU (NTSC), cycle-exact, with a save-state port

Portable Sim: passing (15 passed). Used by Nintendo Entertainment System. Source.

BoardStateRun
Sipeed Tang Nano 20Kverified2026-10-29, at 25e2339
Sipeed Tang Console 138Kstale2026-10-25, at f2536e6
TUL PYNQ-Z2verified2026-10-29, at 25e2339

Verified: the latest run on that board of a system using it passed every test, and the component hasn't changed since. Stale: it has changed since that run.

README

The NES’s 2A03 audio processing unit (NTSC), cycle-exact: two pulse channels with sweeps, the triangle, noise, the delta modulation channel (DMC) with its DMA requests, and the frame counter with its IRQ. Its timing follows the 2A03 as MesenCE models it. It outputs each channel’s raw level; mixing them is the system’s job.

Module apu_2a03 (the folder name can’t start a module name), built from apu_frame, apu_pulse, apu_triangle, apu_noise, apu_dmc, apu_length and apu_envelope.

Module apu_mixer mixes the levels as the 2A03’s output does, nonlinearly, with the NESdev wiki’s formulas: 95.52 / (8128 / (p1 + p2) + 100) for the pulses and 163.67 / (24329 / (3t + 2n + d) + 100) for the rest. It’s two tables that tools/gen_mixer.py generates, as block RAM ROMs whose addresses are taken on the falling edge (on the GW2AR-18 a block RAM’s address needs more hold than a short path from a register gives). The 16-bit unsigned sum, 0 for silence and 65,518 at most, follows the levels by one to two clocks. A test checks the RTL is the script’s, and another checks it against the formulas, computed separately, to within 1.

Timing

Ports

PortDirWidthMeaning
clk, rstin1Clock; synchronous reset to the power-up state
cein1One CPU cycle
oddin1This cycle’s count is odd
wr, addr, wdatain1, 5, 8A write to $4000 + addr this cycle ($4014 and $4016 are ignored)
rd_statusin1A read of $4015 this cycle
statusout8$4015: DMC IRQ, frame IRQ, 0 (open bus, the system’s), DMC active, then the length counters of noise, triangle, pulse 2, pulse 1
irq_nout1The frame counter’s or DMC’s IRQ after this cycle’s access, active low
dma_req, dma_cancel, dma_addrout1, 1, 16The DMC’s DMA request
dma_done, dma_datain1, 8The DMA’s byte
pulse1, pulse2, triangle, noiseout4Channel levels, 0-15
dmcout7The DMC’s level, 0-127
st_addr, st_rdata, st_we, st_wdata5, 32, 1, 32Save-state port (below)

Parameters

ParameterDefaultMeaning
SAVE_STATE10 leaves out the save-state port’s logic: writes do nothing and reads give 0. For a board short of logic: the port costs about 800 LUTs on a GW2A

Save state

The port follows docs/conventions.md: 17 words, every register.

WordsHolds
0Frame counter
1-3Pulse 1: timer, sequencer and sweep; envelope; length counter
4-6Pulse 2, likewise
7-9Triangle: timer and sequencer; linear counter; length counter
10-12Noise: shift register and period; timer and envelope; length counter
13-16DMC

The fields within each word are packed as listed by each module’s st_rdata.

Status

Unit tests check:

sim/apu_model.hpp is a C++ model written in MesenCE’s style, unit by unit. The RTL matches it cycle for cycle, on every output and on irq_n at each cycle’s end, through 2.4 million cycles of random register traffic.

In systems/nes, these ROMs pass in the sim, with MesenCE’s frames:

instr_misc-04 and instr_timing-1 need the CPU’s unstable unofficial opcodes. Not modelled: PAL timing, the DMC sample-duplication glitch of later 2A03 revisions (MesenCE has it off by default), and the $4015 reads and writes of test mode.

References