用c语言写一个nes游戏模拟器
-
PU(部分指令)
-
模拟 PPU(简单渲染,只支持背景)
-
使用 SDL2 显示画面
⚠️ 注意
这是一个 极简版本,只支持 NROM mapper(mapper 0),没有声音、没有控制器、没有中断、没有精灵渲染,仅用于教学目的。
✅ 环境要求
-
GCC 编译器
-
SDL2 开发库(
sudo apt install libsdl2-dev
)
📁 项目结构
nesemu/
├── main.c
├── cpu.c
├── cpu.h
├── ppu.c
├── ppu.h
├── rom.c
├── rom.h
├── sdl_video.c
├── sdl_video.h
└── Makefile
📦 1. rom.h
– ROM 加载头文件
#ifndef ROM_H
#define ROM_H
#include <stdint.h>
typedef struct {
uint8_t* prg_rom;
uint8_t* chr_rom;
uint32_t prg_size;
uint32_t chr_size;
} NESRom;
NESRom* load_nes_file(const char* path);
void free_nes_rom(NESRom* rom);
#endif
📦 2. rom.c
– ROM 加载实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "rom.h"
typedef struct {
char signature[4];
uint8_t prg_rom_size;
uint8_t chr_rom_size;
uint8_t flags6;
uint8_t flags7;
uint8_t padding[8];
} NESHeader;
NESRom* load_nes_file(const char* path) {
FILE* f = fopen(path, "rb");
if (!f) {
perror("Failed to open ROM");
return NULL;
}
NESHeader header;
fread(&header, sizeof(header), 1, f);
if (memcmp(header.signature, "NES\x1A", 4) != 0) {
fprintf(stderr, "Invalid NES file\n");
fclose(f);
return NULL;
}
NESRom* rom = malloc(sizeof(NESRom));
rom->prg_size = header.prg_rom_size * 16384;
rom->chr_size = header.chr_rom_size * 8192;
rom->prg_rom = malloc(rom->prg_size);
rom->chr_rom = malloc(rom->chr_size);
fread(rom->prg_rom, rom->prg_size, 1, f);
fread(rom->chr_rom, rom->chr_size, 1, f);
fclose(f);
return rom;
}
void free_nes_rom(NESRom* rom) {
free(rom->prg_rom);
free(rom->chr_rom);
free(rom);
}
📦 3. cpu.h
– 6502 CPU 头文件
#ifndef CPU_H
#define CPU_H
#include <stdint.h>
#include "rom.h"
typedef struct {
uint8_t A, X, Y, SP, P;
uint16_t PC;
uint8_t memory[0x10000];
} CPU;
void cpu_init(CPU* cpu, NESRom* rom);
void cpu_reset(CPU* cpu);
void cpu_step(CPU* cpu);
#endif
📦 4. cpu.c
– 极简 6502 模拟(只支持 LDA、STA、JMP)
#include "cpu.h"
#include <string.h>
void cpu_init(CPU* cpu, NESRom* rom) {
memset(cpu, 0, sizeof(CPU));
memcpy(cpu->memory + 0x8000, rom->prg_rom, rom->prg_size);
if (rom->prg_size == 16384) {
memcpy(cpu->memory + 0xC000, rom->prg_rom, 16384);
}
}
void cpu_reset(CPU* cpu) {
cpu->PC = cpu->memory[0xFFFD] << 8 | cpu->memory[0xFFFC];
}
void cpu_step(CPU* cpu) {
uint8_t opcode = cpu->memory[cpu->PC++];
switch (opcode) {
case 0xA9: // LDA immediate
cpu->A = cpu->memory[cpu->PC++];
break;
case 0x85: // STA zero page
cpu->memory[cpu->memory[cpu->PC++]] = cpu->A;
break;
case 0x4C: // JMP absolute
cpu->PC = cpu->memory[cpu->PC] | (cpu->memory[cpu->PC + 1] << 8);
break;
default:
break;
}
}
📦 5. ppu.h
– PPU 头文件
#ifndef PPU_H
#define PPU_H
#include <stdint.h>
#include "rom.h"
#define SCREEN_W 256
#define SCREEN_H 240
typedef struct {
uint8_t vram[0x0800];
uint8_t palette[0x20];
uint8_t screen[SCREEN_W * SCREEN_H];
NESRom* rom;
} PPU;
void ppu_init(PPU* ppu, NESRom* rom);
void ppu_render(PPU* ppu);
#endif
📦 6. ppu.c
– 极简 PPU(只渲染背景)
#include "ppu.h"
#include <string.h>
void ppu_init(PPU* ppu, NESRom* rom) {
memset(ppu, 0, sizeof(PPU));
ppu->rom = rom;
}
void ppu_render(PPU* ppu) {
for (int y = 0; y < SCREEN_H; y++) {
for (int x = 0; x < SCREEN_W; x++) {
ppu->screen[y * SCREEN_W + x] = (x + y) % 256;
}
}
}
📦 7. sdl_video.h
#ifndef SDL_VIDEO_H
#define SDL_VIDEO_H
#include <SDL2/SDL.h>
#include "ppu.h"
int init_sdl();
void render_screen(PPU* ppu);
void cleanup_sdl();
#endif
📦 8. sdl_video.c
#include "sdl_video.h"
SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;
SDL_Texture* texture = NULL;
int init_sdl() {
SDL_Init(SDL_INIT_VIDEO);
window = SDL_CreateWindow("NES Emulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 512, 480, 0);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB888, SDL_TEXTUREACCESS_STREAMING, 256, 240);
return 0;
}
void render_screen(PPU* ppu) {
uint32_t pixels[256 * 240];
for (int i = 0; i < 256 * 240; i++) {
uint8_t color = ppu->screen[i];
pixels[i] = (color << 16) | (color << 8) | color;
}
SDL_UpdateTexture(texture, NULL, pixels, 256 * sizeof(uint32_t));
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
void cleanup_sdl() {
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
📦 9. main.c
#include "cpu.h"
#include "ppu.h"
#include "rom.h"
#include "sdl_video.h"
#include <SDL2/SDL.h>
int main(int argc, char** argv) {
if (argc < 2) {
fprintf(stderr, "Usage: %s rom.nes\n", argv[0]);
return 1;
}
NESRom* rom = load_nes_file(argv[1]);
if (!rom) return 1;
CPU cpu;
PPU ppu;
cpu_init(&cpu, rom);
ppu_init(&ppu, rom);
cpu_reset(&cpu);
init_sdl();
int running = 1;
SDL_Event e;
while (running) {
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) running = 0;
}
cpu_step(&cpu);
ppu_render(&ppu);
render_screen(&ppu);
SDL_Delay(16);
}
cleanup_sdl();
free_nes_rom(rom);
return 0;
}
📦 10. Makefile
CC = gcc
CFLAGS = -Wall -O2 -std=c99 `sdl2-config --cflags`
LDFLAGS = `sdl2-config --libs`
SRCS = main.c cpu.c ppu.c rom.c sdl_video.c
OBJS = $(SRCS:.c=.o)
TARGET = nesemu
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET) $(LDFLAGS)
clean:
rm -f $(OBJS) $(TARGET)
编译运行
make
./nesemu your_rom.nes
下一步建议
-
实现完整 6502 指令集
-
支持更多 mapper(如 MMC1、MMC3)
-
实现 NMI 和 PPU 寄存器
-
添加 APU 声音支持
-
添加控制器输入
📚 推荐资源
-
https://www.nesdev.org/
-
https://bugzmanov.github.io/nes_ebook/