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.
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–$4019routing 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.
Memory & Mirrored Nametables
To prevent the CPU and rendering engines from bottlenecking each other, we instantiated three separate Block RAM (BRAM) units:
- Nametable RAM: Stores the tile IDs for backgrounds. We supported both horizontal and vertical nametable mirroring, switchable via an onboard FPGA toggle.
- Object Attribute Memory (OAM): Holds sprite positioning, tile indices, and rendering attributes for up to 64 on-screen sprites.
- Palette RAM: Maps 6-bit palette selections to a 12-bit RGB lookup table covering the standard 64-color NES master palette.
Loopy Scrolling & Sprite Evaluation
Background rendering implements the classic hardware “Loopy” scrolling algorithm. We maintained internal registers for the active VRAM address (), the temporary scroll latch (), and fine horizontal offset (). 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.
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.