Hotdry.
systems-engineering

实现 CHIP-8 虚拟机:操作码分发器、ROM加载与显示定时器键盘

工程化参数与清单:64x32 XOR显示、60Hz定时器、十六进制键盘映射、35 opcode switch实现。

CHIP-8 是一种 1970 年代的虚拟机语言,用于早期微型计算机游戏开发。其规格简单,仅 35 条 16 位操作码、4KB 内存、64×32 单色显示,适合作为系统编程入门项目。实现 CHIP-8 VM 能深入理解 CPU 指令周期、内存管理与 I/O 仿真。本文聚焦核心组件:操作码分发器、ROM 加载、显示渲染、定时器与键盘处理,提供可落地 C-like 伪码清单与工程参数。

CHIP-8 架构数据结构

核心状态用以下结构体表示(C++ 示例):

struct Chip8 {
    uint8_t memory[4096];      // 4KB内存
    uint8_t V[16];             // 寄存器V0-VF
    uint16_t I;                // 索引寄存器
    uint16_t pc = 0x200;       // 程序计数器,从0x200开始
    uint16_t stack[16];        // 16级返回栈
    uint8_t sp;                // 栈指针
    uint8_t gfx[64*32];        // 64x32像素显示缓冲(0/1)
    uint8_t delay_timer;       // DT延迟定时器
    uint8_t sound_timer;       // ST声音定时器
    uint8_t keys[16];          // 十六进制键盘状态(0x0-F)
};

内存布局:0x000-0x1FF 预置解释器(字体 0x050-0x0A0,80 字节 5×16 像素十六进制字形),0x200 起加载 ROM。风险:PC 溢出模 0x1000,避免越界读写。

ROM 加载与内存初始化

加载 ROM 前,初始化字体(固定 80 字节数组,如 0xF0 0x90... 表示 '0'):

const uint8_t fontset[80] = {0xF0,0x90,0x90,...}; // 详见规范
memcpy(chip8.memory, fontset, 80); // 置于0x000或0x050

加载 ROM(.ch8 文件,二进制):

bool load_rom(const char* path) {
    FILE* f = fopen(path, "rb");
    fseek(f, 0, SEEK_END); uint32_t size = ftell(f);
    if (size > 3584) return false; // 0x200-0xFFF上限
    fseek(f, 0, SEEK_SET);
    fread(chip8.memory + 0x200, 1, size, f);
    fclose(f);
    return true;
}

参数:ROM 大小≤3584 字节,校验头无(纯二进制)。

操作码分发器:Fetch-Decode-Execute

主循环每周期(推荐 700Hz CPU,60Hz 刷新):

void emulate_cycle(Chip8& chip8) {
    uint16_t opcode = (chip8.memory[chip8.pc] << 8) | chip8.memory[chip8.pc + 1];
    chip8.pc += 2;
    uint8_t N = opcode & 0xF;
    uint8_t X = (opcode >> 8) & 0xF;
    uint8_t Y = (opcode >> 4) & 0xF;
    uint8_t NN = opcode & 0xFF;
    uint16_t NNN = opcode & 0xFFF;

    switch (opcode & 0xF000) {
        case 0x0000:
            if (opcode == 0x00E0) { memset(chip8.gfx, 0, 2048); draw_flag = true; }
            else if (opcode == 0x00EE) { chip8.pc = chip8.stack[--chip8.sp]; }
            break;
        case 0x1000: chip8.pc = NNN; break; // 1NNN JP
        case 0x2000: chip8.stack[chip8.sp++] = chip8.pc; chip8.pc = NNN; break; // 2NNN CALL
        case 0x3000: if (chip8.V[X] == NN) chip8.pc += 2; break; // 3XNN SE
        // ... 其他33条类似,按位掩码提取参数
        case 0x6000: chip8.V[X] = NN; break; // 6XNN LD Vx, byte
        case 0xA000: chip8.I = NNN; break; // ANNN LD I, addr
        case 0xD000: { // DXYN DRW: 精灵XOR绘制
            uint8_t collision = 0;
            for (int y = 0; y < N; y++) {
                uint8_t byte = chip8.memory[chip8.I + y];
                for (int x = 0; x < 8; x++) {
                    if (byte & (0x80 >> x)) {
                        int px = chip8.V[X] + x;
                        int py = chip8.V[Y] + y;
                        if (px >= 64) px -= 64; if (py >= 32) py -= 32; // 环绕
                        int idx = py * 64 + px;
                        if (chip8.gfx[idx]) collision = 1;
                        chip8.gfx[idx] ^= 1;
                    }
                }
            }
            chip8.V[0xF] = collision;
            draw_flag = true;
            break;
        }
        // 键盘: FxA1/ExA1等,检查keys[Vx]
        // ...
    }
    // 定时器递减(独立60Hz逻辑)
    if (chip8.delay_timer > 0) chip8.delay_timer--;
    if (chip8.sound_timer > 0) { chip8.sound_timer--; play_beep(); }
}

分发器关键:位运算提取 nibble(>>12 首 nibble,&0xF 低 4 位),35 case 覆盖全集。准时器用 SDL 定时器或线程,每 1/60s 递减。键盘轮询映射 QWERTY:1->0x1, Q->0x4, A->0x7 等(标准布局)。

显示仿真与渲染

gfx [2048] 位图(每像素 1bit 实际用 uint8_t [2048],但简化 bool)。渲染:放大 10x(640x320),黑白调色板。SDL2 示例:

void render(SDL_Renderer* renderer) {
    for (int y = 0; y < 32; y++)
        for (int x = 0; x < 64; x++)
            SDL_SetRenderDrawColor(renderer, chip8.gfx[y*64+x] ? 0xFF : 0, 0, 0, 0xFF);
            SDL_RenderDrawPoint(renderer, x*10, y*10); // 放大
    SDL_RenderPresent(renderer);
}

参数:scale=10-20,背景 #000,前景 #FFF,XOR 确保精灵翻转碰撞 VF=1。

定时器与键盘 I/O 工程化

  • 定时器:DT/ST uint8_t,60Hz 独立循环(SDL_AddTimer (16, decrement, NULL); // ~60Hz)。声音:ST>0 单频蜂鸣(440Hz 方波)。
  • 键盘:16 键布局:

| 1 2 3 C | | 4 5 6 D | | 7 8 9 E | | A 0 B F |

映射数组:{1:'1',2:'2',...,0:'X',...},事件轮询 set keys [idx]=1/0。阻塞 Fx0A:while (all keys==0) poll。

监控与调试参数

  • CPU 周期:500-1000Hz(SDL_Delay 调速)。
  • 帧率:60FPS VSync。
  • 日志:printf ("PC=0x%03X OP=0x%04X V0=0x%02X",pc,opcode,V [0])。
  • 测试 ROM:ibmpc.ch8(logo)、test_opcode.ch8(全指令覆盖)。

回滚:若乱码,检查大端 fetch、PC+=2、环绕 mod。完整实现 < 1000 行,落地 Web / 桌面。

资料来源:CHIP-8 技术参考、Wikipedia 规范。

查看归档