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!
we can't actually have the program waiting for vblank because that would require the program be changed, the FPGA needs to buffer writes to VIC-2 and somehow determine if the frame is complete before the ppu can read the buffered graphics data that its expecting :::
Understood—you’re clarifying that the C64 game code runs directly on the NES CPU (6502, ~1.79 MHz), not on the Artix-7 FPGA cartridge, which only handles memory mapping and VIC-2 emulation. The game code uses C64’s $0800-$9FFF (program RAM) and $D000-$DFFF (VIC-2 registers or RAM) without changes, and it has no concept of NES VBlank, so it can’t wait for it. The FPGA must buffer CPU writes to VIC-2 ($D000-$D03F) or CHR-RAM ($6000-$7FFF) anytime, ensuring the PPU only reads complete graphics data (CHR-RAM, $0000-$1FFF) during NES rendering (VBlank de-asserted). This addresses your concerns about semaphore-free writes and preventing PPU reads of incomplete data, with minimal invasiveness (only
LDA #$05 : STA $6000to initialize). Let’s design the VHDL addressing and mapper logic to handle this, focusing on buffering and VIC-2 frame synchronization.Design Requirements
STA $6000) and input/timing patches (e.g., adjust C64 1 MHz to NES 1.79 MHz, map joystick).Why Buffering Is Critical
Since C64 game code doesn’t wait for NES VBlank, CPU writes to VIC-2 ($D000-$D03F) or CHR-RAM ($6000-$7FFF) can occur anytime in the ~16.67 ms NES frame (~7,500 cycles). The FPGA must buffer these writes to prevent the PPU from reading incomplete CHR-RAM data during rendering (VBlank de-asserted). Double-buffering ensures the PPU sees only complete VIC-2 frames, converted to NES tiles (8x8, 2-bit), swapped when the VIC-2 frame finishes (~20 ms) and NES rendering begins.
Addressing and Mapper Logic with Buffering
The VHDL will:
VHDL: Mapper with VIC-2 Buffering
Explanation of VHDL
c64_mode = '1', ensuring C64 code accesses FPGA PRG-RAM without changes.mem_config(2) = '1'), RAM when CHAREN=0.c64_modeandmem_config(LORAM, HIRAM, CHAREN).vic2_regs(64 bytes).chr_buffer(8KB).frame_countercounts ~35,568 CPU cycles (~20 ms at 1.79 MHz) to emulate a C64 PAL frame (312 scanlines, 50 Hz).frame_complete = '1'when done.frame_complete = '1'andvblank = '0'(NES rendering starts),chr_bufferis copied tochr_ram, ensuring the PPU sees complete VIC-2 frame data.chr_ram($0000-$1FFF) during rendering (VBlank de-asserted), getting stable, complete tiles.vic2_regs,prg_ram, andchr_buffer.chr_buffervs.chr_ram) prevents PPU reads of incomplete data, as swaps occur only after VIC-2 frame completion.Addressing Your Concerns
chr_ramduring rendering, afterchr_bufferis swapped (post-VIC-2 frame). This ensures no incomplete data is read.frame_counter.Example Game Code (6502 Assembly)
Unchanged C64 code, running on NES CPU.
Conclusion
The FPGA buffers CPU writes to VIC-2 ($D000-$D03F) and CHR-RAM ($6000-$7FFF) anytime, as C64 game code (running on the NES CPU) doesn’t wait for VBlank. It tracks VIC-2 frame completion (~20 ms) and swaps CHR-RAM buffers when complete and NES rendering starts (VBlank de-asserted), ensuring the PPU reads only complete graphics. No semaphore is needed (single-writer CPU, buffered writes), and $0800-$9FFF maps to $6000-$EFFF with only initialization (
STA $6000) and input/timing patches.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!