Thank you for the clarification! You’re proposing that the FPGA itself acts as the CHR-RAM, directly presenting VIC-2-rendered graphics data to the NES PPU bus, rather than using a separate CHR-RAM chip on the cartridge. This approach changes the design significantly, as the FPGA would dynamically generate and serve tile/sprite data in real-time to the PPU, emulating CHR-RAM behavior internally. Let’s analyze this and provide an updated Markdown design addressing this approach, confirming feasibility, and including relevant VHDL and example game code.
Using the FPGA to emulate CHR-RAM (instead of a physical SRAM chip) is feasible but introduces specific considerations:
- FPGA as CHR-RAM: The FPGA can present tile/sprite data on the PPU bus (CHR address/data lines) as if it were CHR-RAM. It would internally generate VIC-2 graphics and format them into NES-compatible 8x8 tiles, serving them in real-time as the PPU requests data.
- Advantages:
- Eliminates the need for a separate CHR-RAM chip, reducing cartridge complexity and cost.
- Allows dynamic, on-the-fly rendering of VIC-2 graphics, potentially enabling more complex effects.
- Challenges:
- Timing Precision: The FPGA must respond to PPU CHR address requests (~5.37 MHz, NTSC) with precise timing to mimic CHR-RAM’s read behavior.
- FPGA Resources: Emulating VIC-2 and serving CHR data requires significant logic (LUTs) and internal memory (BRAM) to buffer rendered tiles.
- PPU Bus Interface: The FPGA must handle the PPU’s 13-bit address bus (A0-A12) and 8-bit data bus (D0-D7) correctly, acting as a memory device.
- VBlank Updates: Unlike physical CHR-RAM (updated during VBlank), the FPGA can update its internal tile buffer continuously, but synchronization with the PPU’s scanline rendering is critical.
- Conclusion: This approach is viable with a sufficiently capable FPGA (e.g., Lattice iCE40HX8K or Xilinx Spartan-6) and careful timing design. It simplifies the cartridge hardware but increases FPGA complexity.
Below is the updated Markdown design, reflecting the FPGA acting as CHR-RAM.
This cartridge uses an FPGA to emulate the Commodore 64’s VIC-2 graphics chip and act as CHR-RAM, dynamically serving NES-compatible tile/sprite data to the PPU bus. A custom mapper expands memory, enabling C64 games to run on the NES 6502 CPU with minimal modification. The solution is cartridge-only, using no expansion slot or physical CHR-RAM chip.
- FPGA: Emulates VIC-2, generates CHR-RAM data, and implements a custom mapper.
- PRG-ROM: Stores game code for the 6502 CPU.
- Mapper: FPGA-based, handles VIC-2 register access, memory banking, and PPU bus interfacing.
- NES Interface: 72-pin cartridge connector for CPU and PPU bus access.
- No Physical CHR-RAM: The FPGA internally buffers and serves tile/sprite data to the PPU bus, emulating CHR-RAM.
- Role: The FPGA acts as CHR-RAM, responding to PPU read requests on the CHR address/data bus (A0-A12, D0-D7).
- Operation:
- The FPGA renders VIC-2 graphics (e.g., 320x200 bitmap, 40x25 text, 8 sprites) into NES-compatible 8x8 tiles and sprites.
- It stores rendered tiles in internal BRAM (e.g., 8KB equivalent for 128 tiles).
- When the PPU requests CHR data, the FPGA provides the appropriate tile/sprite data in real-time, mimicking CHR-RAM behavior.
- Timing: The FPGA synchronizes with the PPU’s ~5.37 MHz (NTSC) clock, responding within ~186 ns per read cycle.
- Capacity: Emulates 8KB CHR-RAM (128 tiles); larger sizes (e.g., 32KB) possible with more BRAM.
- Registers: VIC-2 registers ($D000-$D03F) are mapped to FPGA at $6000-$603F (CPU address space).
- Rendering:
- Converts VIC-2 graphics (16 colors, sprites, scrolling) to NES tiles/sprites.
- Updates internal BRAM tile buffer, potentially continuously or during VBlank.
- Maps C64’s 16 colors to NES’s 52-color palette.
- Sprites: Maps VIC-2’s 8 sprites to NES sprites, with FPGA multiplexing for >8 sprites per scanline.
- Memory Expansion:
- Option 1: Provides 32KB SRAM (on-cart) at $6000-$7FFF, banked like MMC5.
- Option 2: Implements a flat 64KB+ address space, bypassing NES’s 2KB WRAM (requires game code to use custom mapper).
- Bank Switching: Supports 16KB/32KB PRG-ROM banks.
- VIC-2 Memory: Maps C64 memory ($0000-$FFFF) to FPGA-managed RAM for game data.
- Minimal Changes: C64 games access VIC-2 registers via FPGA, with graphics served to the PPU bus.
- Input: NES controllers (2 buttons + D-pad) may require patches for C64 joystick compatibility.
- Timing: Adjust for 6502 (1.79 MHz) vs. 6510 (1 MHz) differences.
- Power: 5V from NES, ~500mA budget for FPGA.
- Size: FPGA (e.g., Lattice iCE40HX8K) fits in a standard NES cartridge shell.
- No Expansion Slot: All functionality via 72-pin connector.
- No CHR-RAM Chip: FPGA’s internal BRAM replaces physical CHR-RAM.
This module emulates CHR-RAM, serving tile data to the PPU bus.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity chr_ram_emulator is
Port (
clk : in STD_LOGIC; -- PPU clock (~5.37 MHz)
reset : in STD_LOGIC;
ppu_addr : in STD_LOGIC_VECTOR(12 downto 0); -- PPU CHR address (8KB)
ppu_data : out STD_LOGIC_VECTOR(7 downto 0); -- PPU CHR data
vic2_tile_data : in STD_LOGIC_VECTOR(7 downto 0); -- VIC-2 rendered tile
vblank : in STD_LOGIC -- VBlank signal for tile updates
);
end chr_ram_emulator;
architecture Behavioral of chr_ram_emulator is
type chr_mem_t is array (0 to 8191) of STD_LOGIC_VECTOR(7 downto 0); -- 8KB
signal chr_mem : chr_mem_t; -- Internal BRAM for tiles
signal tile_counter : unsigned(12 downto 0) := (others => '0');
begin
process(clk, reset)
begin
if reset = '1' then
chr_mem <= (others => (others => '0'));
tile_counter <= (others => '0');
elsif rising_edge(clk) then
-- Update tile buffer (e.g., during VBlank)
if vblank = '1' then
chr_mem(to_integer(tile_counter)) <= vic2_tile_data;
tile_counter <= tile_counter + 1;
end if;
-- Serve CHR data to PPU
ppu_data <= chr_mem(to_integer(unsigned(ppu_addr)));
end if;
end process;
end Behavioral;This code assumes the FPGA handles VIC-2 rendering and CHR-RAM emulation.
; Set VIC-2 background color (emulated in FPGA)
LDA #$01 ; C64 black
STA $6000 ; FPGA VIC-2 register $D021
LDA #$07 ; C64 white
STA $6001 ; FPGA VIC-2 register
; Main loop
main_loop:
JSR read_input ; NES controller
JSR update_game; Game logic
JSR wait_vblank; Wait for VBlank (FPGA updates tile buffer)
JMP main_loop
wait_vblank:
LDA $2002 ; PPU status
BPL wait_vblank; Wait for VBlank
RTS- FPGA Choice: Lattice iCE40HX8K (8K LUTs, ~128KB BRAM) or Xilinx Spartan-6 for VIC-2 emulation and CHR-RAM emulation.
- Timing: FPGA must respond to PPU reads within ~186 ns, synchronized to the PPU clock.
- BRAM Usage: 8KB BRAM for CHR-RAM emulation; additional BRAM for VIC-2 buffers.
- Testing: Simulate with Mesen emulator, configuring a custom mapper to emulate FPGA CHR-RAM behavior.
- PPU Timing: Precise response to PPU CHR reads is critical to avoid graphical glitches.
- FPGA Resources: VIC-2 emulation + CHR-RAM emulation may push smaller FPGAs to their limits.
- Sprite Multiplexing: FPGA must handle VIC-2 sprite-to-NES sprite conversion, respecting NES’s 8-sprite-per-scanline limit.
Copy the Markdown above into a .md file for documentation. Save the VHDL as chr_ram_emulator.vhd for FPGA synthesis (e.g., with iCEcube2). The 6502 assembly can be saved as game.asm for use with assemblers like ca65.
This design leverages the FPGA’s flexibility to emulate CHR-RAM, simplifying the cartridge hardware. If you need a deeper dive into the VHDL (e.g., VIC-2 rendering logic), a specific C64 game example, or a diagram of the FPGA-PPU interface, let me know!
Yes and also vblank should =1 not 0 when swapping:
You're right to question this—let’s clarify the logic behind the
frame_complete = '1' and vblank = '0'condition in the VHDL and why you suspectvblank = '1'might be intended. In the context of your Artix-7 FPGA-based NES cartridge emulating a C64, the FPGA buffers CPU writes to VIC-2 registers ($D000-$D03F) and CHR-RAM ($6000-$7FFF) anytime, as C64 game code (running on the NES CPU) doesn’t wait for NES VBlank. The FPGA tracks VIC-2 frame completion (~20 ms, 50 Hz PAL) and must update the PPU-accessible CHR-RAM ($0000-$1FFF) at the right moment to ensure complete graphics data, mapping C64’s $0800-$9FFF to $6000-$EFFF and handling $D000-$DFFF. Your concern aboutvblank = '1'suggests the CHR-RAM update should occur during VBlank (when the PPU isn’t rendering), not when VBlank is de-asserted (during rendering,vblank = '0'). Let’s analyze and fix this.Analyzing the Condition
Current Logic:
frame_complete = '1': The VIC-2 frame (~20 ms, 312 scanlines, 50 Hz PAL) is complete, with updated graphics (e.g., NES tiles from VIC-2 data) ready inchr_buffer.vblank = '0': NES VBlank is de-asserted (scanlines 0–239, ~15.4 ms, 60 Hz NTSC), meaning the PPU is actively rendering and reading CHR-RAM.chr_buffertochr_ramduring rendering (vblank = '0') risks the PPU reading partially updated CHR-RAM, causing graphical glitches (e.g., torn tiles), as the swap isn’t instantaneous across all 8KB.Your Suggestion:
vblank = '1'(VBlank asserted, scanlines 241–260, ~1.27 ms, $2002 bit 7 = 1) makes sense because:chr_buffertochr_ramis safe.chr_ramin the next rendering phase (VBlank de-asserted), ensuring complete graphics.Why
vblank = '1'Is Correctchr_bufferover a VIC-2 frame (~20 ms).chr_ramduring this period risks glitches.chr_buffertochr_ramduring VBlank (vblank = '1'), when the PPU isn’t reading, ensuring the next frame uses complete data.frame_complete = '1') and NES VBlank is active (vblank = '1'), roughly every 5 NES frames ≈ 4 VIC-2 frames.Updated VHDL: Corrected Buffer Swap
Below is the corrected VHDL, focusing on the addressing and mapper logic with buffering, updating the buffer swap to
vblank = '1'.Explanation of Changes
vblank = '0'tovblank = '1'in the buffer swap condition (if frame_complete = '1' and vblank = '1' then).chr_bufferis copied tochr_ramduring NES VBlank (scanlines 241–260, $2002 bit 7 = 1), when the PPU isn’t reading CHR-RAM, ensuring no glitches.frame_complete = '1', ~20 ms), aligning with the next available NES VBlank (~16.67 ms cycles).vic2_regsandchr_buffer, processing VIC-2 data into NES tiles.vblank = '1') to swap buffers, ensuringchr_ramis updated before PPU rendering (VBlank de-asserted).chr_ramonly during rendering (vblank = '0'), after the swap, so it sees complete data.Addressing Your Concerns
vblank = '1') ensureschr_ramis fully updated before PPU rendering starts, preventing glitches.frame_counter, ensuring only complete graphics are swapped.LDA #$05 : STA $6000initializes C64 mode. No changes to $0800-$9FFF or $D000-$DFFF; only input/timing patches needed (e.g., C64 1 MHz to NES 1.79 MHz, joystick).Example Game Code (6502 Assembly)
Unchanged C64 code, running on NES CPU.
Conclusion
The corrected VHDL swaps
chr_buffertochr_ramwhenframe_complete = '1' and vblank = '1', ensuring the PPU reads complete VIC-2 frame data during NES rendering, without requiring C64 code to wait for VBlank. The FPGA maps $0800-$9FFF to $6000-$EFFF, buffers VIC-2/CHR-RAM writes anytime, and prevents PPU read conflicts via double-buffering, needing no semaphore. Only initialization (STA $6000) and minor patches are required.Copy the VHDL (
c64_nes_mapper.vhd) and assembly (game.asm). If you need a timing diagram, Artix-7 constraints, or a specific C64 game example, let me know!