All Projects
COMPLETE

NES Hardware Emulator on FPGA

Rather than emulating in software, we synthesized the console's digital logic — CPU buses, PPU, APU and controller shift registers — directly onto an FPGA, running original Mapper 0 titles at native speed.

Building an 8-Bit Console in Silicon

For our ECE 385 final project at UIUC, Constantine Vasilakis and I recreated the Nintendo Entertainment System on an FPGA — not as a software emulator, but as the console’s actual digital logic, written in SystemVerilog. The CPU memory buses, Picture Processing Unit (PPU), Audio Processing Unit (APU), and controller shift registers all run directly on the Urbana development board, playing original Mapper 0 titles like Super Mario Bros. and Donkey Kong at native speed.

The project placed in the ECE 385 Best Projects Showcase, and I later joined the course staff.

TOP-LEVEL SoC // nes_top.sv URBANA FPGA (XC7S50) Soft core handles USB host enumeration and HID report parsing in software; button state reaches the fabric over two AXI GPIO registers. MicroBlaze SoC USB Gamepad HID MAX3421E USB-SPI Classic NESdev shift-register protocol at $4016 — strobe held high continuously reloads the register; each read afterward shifts one button out through bit 0. NES Controller IP $4016 Latch / Shift nes_controller.sv OpenCores T65, clocked at ~1.79 MHz (21.47 MHz bus clock ÷ 12). Decimal mode is hardwired to ground — the real Ricoh 2A03 never had it either. MOS 6502 CPU OpenCores T65 Core Clock: ~1.79 MHz Decimal Mode Disabled 16-bit Addr / 8-bit Data 4-state FSM (IDLE→DELAY→READ→WRITE) triggered by a write to $4014. Halts the CPU for all 256 read/write cycle pairs of the OAM copy. DMA Controller ($4014) 256-Byte Burst OAM Copy FSM: IDLE/DELAY/RD/WR Runs on its own 25 MHz VGA clock domain, independent of the 21.47 MHz NES bus clock. See the frame/scanline timing diagram further down for the scanline-level detail. Picture Unit (PPU) Loopy Scroll VRAM 2KB Nametable BRAM OAM 64-Sprite Eval 12-bit RGB Palette LUT Five channels register-mapped at $4000–$4015, same addresses real NES software expects. DMC is tracked in the status register but not implemented as full sample playback. Audio Unit (APU) Pulse 1 & 2 (Square) Triangle Waveform 15-bit LFSR Noise Digital Output Mixer PWM D/A Audio Out HDMI / VGA PWM 3.5mm 25MHz Pixel Data
NES Top-Level Hardware Block Diagram


System Architecture & The 6502 Core

The console is orchestrated by a central address and data bus connecting our submodules:

  • CPU Implementation: We integrated OpenCores’ T65 VHDL implementation of the MOS 6502. The NTSC NES runs its CPU at approximately 1.79 MHz, which we derived from a 21.47 MHz master clock using a cycle counter that enables the CPU every 12 clock ticks.
  • Bus Multiplexing: Handling memory-mapped I/O cleanly was critical. Address ranges route directly to their corresponding hardware blocks (such as $4000–$4019 routing to the APU).
  • The Decimal Flag Bug: Our CPU test ROM (nestest.nes) kept failing specific branches until we realized the original Ricoh 2A03 physically lacked decimal mode. Hardwiring the core’s decimal input to ground fixed every remaining failure.

Picture Processing Unit (PPU)

The PPU was the most involved submodule in the project. It runs across two clock domains: the 21.47 MHz NES bus clock and a 25 MHz VGA pixel clock.

PPU FRAME & SCANLINE TIMING // ppu.sv, 25 MHz VGA-clocked 262 scanlines/frame, derived from DrawY/DrawX at 512×480 — NMI fires the instant VBlank sets FRAME TIMELINE (SCANLINE 0–261) 512×480 VGA canvas. NES scanline = DrawY>>1, valid while DrawY[9:1] < 240. vblank_pulse sets at DrawY==480, DrawX 1–9. Cleared at the pre-render line (DrawY==522) or immediately on a CPU read of PPUSTATUS ($2002). VBLANK SET → NMI 0 239 241–260 261 VISIBLE (rendered scanlines) VBLANK 240 · POST-RENDER • 261 · PRE-RENDER (1 scanline each) ONE 8-DOT BACKGROUND TILE FETCH (ZOOMED — repeats ×32/visible scanline) vram_addr = $2000 | (v & $0FFF) — fetched_tile latches from ppu_nametable_o at DrawX[3:1]==1. NAMETABLE vram_addr = $23C0 | ... — selects the 2-bit palette group for this tile's quadrant. ATTRIBUTE Low bitplane byte of the 8×8 CHR tile — the first half of the 2-bit-per-pixel color index. PATTERN LOW High bitplane byte — combined with the low byte to give each pixel its final 2-bit color index. Both shift into pattern_shift_l at the DrawX[3:0]==15 tile boundary. PATTERN HIGH Latched on odd DrawX, shifted into the tile registers at the DrawX[3:0]==15 boundary Sprite eval (SCAN_Y → WAIT → COPY) buffers up to 8 sprites/line; FETCH runs during HBlank (dot ≥ 512) Sprite-0 hit sets when sprite 0 and background pixels are both opaque, dots 16–511
Animated PPU frame and scanline timing, plus the background tile fetch pattern

Memory & Mirrored Nametables

To prevent the CPU and rendering engines from bottlenecking each other, we instantiated three separate Block RAM (BRAM) units:

  1. Nametable RAM: Stores the tile IDs for backgrounds. We supported both horizontal and vertical nametable mirroring, switchable via an onboard FPGA toggle.
  2. Object Attribute Memory (OAM): Holds sprite positioning, tile indices, and rendering attributes for up to 64 on-screen sprites.
  3. Palette RAM: Maps 6-bit palette selections to a 12-bit RGB lookup table covering the standard 64-color NES master palette.

VRAM NAMETABLE MEMORY BANK MIRRORING Hardware address line multiplexing across 2KB physical BRAM HORIZONTAL MIRRORING (Vertical Games) $2000 [Bank A] A $2400 [Mirrors A] A $2800 [Bank B] B $2C00 [Mirrors B] B VERTICAL MIRRORING (Horizontal Scrolling) $2000 [Bank A] A $2400 [Bank B] B $2800 [Mirrors A] A $2C00 [Mirrors B] B
NES VRAM Nametable Mirroring

Loopy Scrolling & Sprite Evaluation

Background rendering implements the classic hardware “Loopy” scrolling algorithm. We maintained internal registers for the active VRAM address (vv), the temporary scroll latch (tt), and fine horizontal offset (xx). As the rasterizer moves across the screen, coarse counters auto-increment and wrap across nametables to produce smooth side-scrolling.

In parallel, a secondary OAM state machine evaluates up to 8 sprites per scanline during horizontal blanking intervals, passing pixel priorities to the multiplexer to handle foreground and background occlusion correctly.

The DMA controller is a small four-state machine (IDLE → DELAY → READ → WRITE) that watches the bus for a write to $4014, latches the requested page, then walks all 256 bytes of that page into OAM one read/write pair at a time — asserting cpu_suspend for the whole transfer so the 6502 core can’t interleave stray bus cycles with the copy.

OAM DMA BUS TIMING // dma.sv ($4014) 256-byte burst copy into sprite OAM, CPU halted for the duration $4014 WR ADDR A RD $xx00 RD $2004 WR $xx01 RD $2004 WR ⋮ ×254 $xxFF RD $2004 WR RESUME RUN DMA STATE CPU BUS RUN HALTED — CPU BUS GRANTED TO DMA RUN IDLE DELAY READ WRITE READ/WRITE ×254 The 6502 core is bus-granted away for the full 256-byte copy — one read/write cycle pair per OAM byte, IDLE → DELAY → READ → WRITE per dma.sv.
OAM DMA bus timing diagram — CPU halted while the DMA controller walks 256 bytes into OAM


Audio: The APU & PWM DAC

The APU mirrors the real 2A03’s five-channel layout — two pulse channels, a triangle channel, a noise channel, and a DMC channel — exposed at the same register addresses ($4000–$4015) real NES software expects. Each pulse channel has its own duty-cycle sequencer, volume envelope, and length counter, all driven off a shared frame sequencer that ticks the envelopes every 3728 APU cycles and silences expired channels every other tick.

The noise channel uses a 15-bit linear-feedback shift register whose feedback tap switches between bit 1 and bit 6 depending on a mode bit, matching the two noise modes the original silicon exposes. All four implemented channels sum into an 8-bit mix:

assign pulse_mix = pulse1_out + pulse2_out + triangle_out + noise_out;
assign audio_o = {pulse_mix, 8'b0};

Rather than requiring an external DAC, audio_o is synchronized into the board’s 125 MHz HDMI clock domain and fed into a small PWM module that compares a free-running 9-bit counter against the sample’s upper bits, producing a 1-bit pulse-density stream whose duty cycle tracks the sample value — a software-free way to get analog-sounding audio out of a single FPGA pin.


Player Input via USB HID

Instead of wiring up an original controller’s shift-register connector, we terminate a standard USB HID gamepad on the board. A MicroBlaze soft processor handles USB enumeration and HID report parsing in software, exposing raw keycode bytes to the fabric over AXI GPIO; a small decode layer maps those scan codes onto the eight NES button lines.

From there, nes_controller.sv behaves exactly like real controller hardware: while $4016’s strobe bit is high it continuously reloads from live button state, and once strobe drops, each read of $4016 shifts one more button out through bit 0 — the same polling protocol every NES game already expects.


System Architecture & Clock Domains

In short: the board runs on three separate clocks at once — one for the CPU, one for video, one for the display connector — and none of them are allowed to talk to each other directly without a safety net.

One clk_wiz_0 instance splits a single reference clock into three: 21 MHz for the CPU/APU/DMA core, 25 MHz for the PPU’s pixel-accurate rendering, and 125 MHz for HDMI output. Video timing is still generated the classic VGA way (VGA_controller.sv), just handed to Xilinx’s hdmi_tx_0 IP instead of a physical VGA port — same timing model, different connector.

Two spots needed real care crossing between those clocks: the PPU’s vblank interrupt is passed to the CPU through a 2-flop synchronizer (the standard fix for a signal crossing clock domains at a random moment), and each finished audio sample is double-buffered on its way into the 125 MHz domain — so video and audio each run at their own pace without stalling the other.