首页 > 解决方案 > 指令 0x3000 和跳转指令在 CHIP-8 上的几乎所有游戏中重复

问题描述

我是仿真新手,想开始编写 CHIP-8 解释器。但是,我面临一个问题。运行游戏时,例如 Brix,它绘制游戏没有问题(桨等),但在完成后,它只是卡在 0x3000 的循环中,然后,跳转指令跳转回0x3000。很明显 0x3000 是错误的,这就是它循环的原因,但我不明白为什么这对我来说是终身的。

游戏和 Chrome devtools 控制台的屏幕截图(游戏是 Brix,取自此处):https ://i.stack.imgur.com/a0wNM.png

在该屏幕截图中,在控制台中,您可以看到 0x3000 失败并进行跳转,然后跳转回到 0x3000,并且循环重复。大多数(如果不是所有)游戏都会发生这种情况。我怀疑这与延迟计时器有关,因为 0x3000 正在检查 v0 === 0,但它失败了,然后转到跳转指令。

这是我的主要 CHIP-8 课程:

import { createMemory } from './memory.js';
import Display from './display.js';
import { CHIP8Error } from './error.js';
import { wait, toHex } from './utility.js';

export default class CHIP8 { constructor() {} }

CHIP8.prototype.init = function(displayX=64, displayY=32) {
    this.display = new Display();
    this.memory = createMemory(0xFFF, 'buffer', false); // Fill does not work with buffer
    this.v = createMemory(0xF, 'uint8', 0);
    this.I = 0;
    this.stack = createMemory(0xF, 'uint16', 0);
    this.halted = 1;

    // Thanks to https://codereview.stackexchange.com/questions/190905/chip-8-emulator-in-javascript for the keymap
    this.keyMap = {
        49:0x1,
        50:0x2,
        51:0x3,
        52:0xc,
        81:0x4,
        87:0x5,
        69:0x6,
        82:0xd,
        65:0x7,
        83:0x8,
        68:0x9,
        70:0xe,
        90:0xa,
        88:0x0,
        67:0xb,
        86:0xf
    };

    this.pressedKeys = {};
    
    this.sp = 0;
    this.pc = 0;
    this.dt = 0;
    this.st = 0;

    this.display.init(displayX, displayY);

    const fonts = [
        0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
        0x20, 0x60, 0x20, 0x20, 0x70, // 1
        0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
        0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
        0x90, 0x90, 0xF0, 0x10, 0x10, // 4
        0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
        0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
        0xF0, 0x10, 0x20, 0x40, 0x40, // 7
        0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
        0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
        0xF0, 0x90, 0xF0, 0x90, 0x90, // A
        0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
        0xF0, 0x80, 0x80, 0x80, 0xF0, // C
        0xE0, 0x90, 0x90, 0x90, 0xE0, // D
        0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
        0xF0, 0x80, 0xF0, 0x80, 0x80  // F
    ];
    fonts.forEach((v, i) => {
        this.memory[i] = v;
    });
};

CHIP8.prototype.load = function(program, programStart=0x200) {
    for (let i = 0; i < program.length; i++) {
        this.memory[programStart + i] = program[i];
    }
    this.pc = programStart;

    this.programStart = programStart;
    this.programEnd = programStart + program.length;
    this.halted = 0;
};

CHIP8.prototype.updateTimers = function() {
    // TODO: This function may not be complete
    if (this.dt > 0) {
        this.dt -= 100;
    }
    if (this.st > 0) {
        // TODO: Add the sound
        this.st--;
    }
};

CHIP8.prototype.decodeOpcode = function(addr) {
    let opcode = this.memory[addr];
    opcode <<= 8;
    opcode |= this.memory[addr+1];
    return opcode;
};

CHIP8.prototype.step = function() {
    if (this.halted) return;
    if (this.haltUntilKeypress) return;

    let opcode = this.decodeOpcode(this.pc);
    this.executeOpcode(opcode, this.pc);
    this.pc += 2;
};

CHIP8.prototype.tick = function() {
    this.step();
    this.updateTimers();
    this.renderer.draw(this.display);
};

CHIP8.prototype.setSingleSteppingEnabled = function(enable=true) {
    if (enable) {
        this.noLoop = true;
    } else {
        this.noLoop = false;
    }
};

CHIP8.prototype.run = async function() {
    if (!this.renderer) {
        CHIP8Error('Renderer not defined. Use setRenderer on the CHIP8 object in order to do so', true, undefined);
        return;
    }
    while (this.pc <= this.programEnd) {
        if (this.noLoop) {
            await wait(1);
            continue;
        }
        this.tick();
        await wait(1000/60);
    }

    console.log('[CPU] Execution finished');
};

CHIP8.prototype.setRenderer = function(renderer) {
    this.renderer = renderer;
    // TODO: Move the init call somewhere else
    this.renderer.init();
};

// Keyboard events
// NOTE: Need to be bound by user, with a .bind() to the chip8 instance!
// NOTE: The function getKeyboardEvent will do the .bind() for you, but will not actually bind the event
CHIP8.prototype.keydown = function(e) {
    // Only for browsers
    let keycode = e.keyCode;

    let key = this.keyMap[keycode];
    if (key) {
        this.pressedKeys[key] = 1;
        if (this.haltUntilKeypress) {
            this.v[this.haltUntilKeypress] = key;
            this.haltUntilKeypress = undefined;
        }
        console.log(`[CPU] [KEYBOARD EVENT] Keydown: ${key}`);
    }
};
CHIP8.prototype.keyup = function(e) {
    // Only for browsers
    let keycode = e.keyCode;

    let key = this.keyMap[keycode];
    if (key) {
        this.pressedKeys[key] = 0;
        console.log(`[CPU] [KEYBOARD EVENT] Keyup: ${key}`);
    }
};
CHIP8.prototype.getKeyboardEvent = function(event) {
    switch (event) {
        case 'keydown': {
            return this.keydown.bind(this);
        }
        case 'keyup': {
            return this.keyup.bind(this);
        }
    }
    return;
};

CHIP8.prototype.dumpToConsole = function() {
    console.warn('[DUMP] BEGIN DUMP');
    console.log('[DUMP] Vx registers', this.v);
    console.log('[DUMP] I register', toHex(this.I), 'Stack pointer', toHex(this.sp), 'Program counter', toHex(this.pc), 'ST', toHex(this.st), 'DT', toHex(this.dt));
    console.log('[DUMP] Memory', this.memory);
    console.log('[DUMP] Video memory', this.display.displayMemory);
    console.log('[DUMP] Pressed keys', this.pressedKeys);
    console.warn('[DUMP] END DUMP');
};

CHIP8.prototype.executeOpcode = function(opcode, addr) {
    let firstNibble = opcode & 0xF000;

    const nnn = opcode & 0x0FFF;
    const n = opcode & 0x000F;
    const x = (opcode & 0x0F00) >> 8;
    const y = (opcode & 0x00F0) >> 4;
    const kk = (opcode & 0x00FF);

    console.log(`[CPU] [OPCODE] [EXECUTE] Opcode ${toHex(opcode)} at ${toHex(addr)}: firstNibble: ${toHex(firstNibble)}, nnn: ${toHex(nnn)}, n: ${toHex(n)}, x: ${toHex(x)}, y: ${toHex(y)}, kk: ${toHex(kk)}`);

    switch (firstNibble) {
        case 0x0000: {
            switch (nnn) {
                case 0x0E0: {
                    let displayX = this.display.xs;
                    let displayY = this.display.ys;
                    this.display.init(displayX, displayY);

                    this.renderer.clear();
                    break;
                }
                case 0x0EE: {
                    this.pc = this.stack[this.sp];
                    this.sp--;
                    break;
                }
            }
            break;
        }
        case 0x1000: {
            this.pc = nnn;
            break;
        }
        case 0x2000: {
            this.sp++;
            this.stack[this.sp] = this.pc;
            this.pc = nnn;
            break;
        }
        case 0x3000: {
            if (this.v[x] == kk) {
                this.pc += 2;
            }
            break;
        }
        case 0x4000: {
            if (this.v[x] !== kk) {
                this.pc += 2;
            }
            break;
        }
        case 0x5000: {
            if (this.v[x] === this.v[y]) {
                this.pc += 2;
            }
            break;
        }
        case 0x6000: {
            this.v[x] = kk;
            break;
        }
        case 0x7000: {
            this.v[x] += kk;

            if (this.v[x] > 255) {
                this.v[x] -= 256;
            }
            break;
        }
        case 0x8000: {
            switch (n) {
                case 0x0: {
                    this.v[x] = this.v[y];
                    break;
                }
                case 0x1: {
                    this.v[x] |= this.v[y];
                    break;
                }
                case 0x2: {
                    this.v[x] &= this.v[y];
                    break;
                }
                case 0x3: {
                    this.v[x] ^= this.v[y];
                    break;
                }
                case 0x4: {
                    this.v[x] += this.v[y];
                    if (this.v[x] > 255) {
                        this.v[x] -= 256;
                        this.v[0xF] = 1;
                    } else {
                        this.v[0xF] = 0;
                    }
                    break;
                }
                case 0x5: {
                    if (this.v[x] > this.v[y]) {
                        this.v[0xF] = 1;
                    } else {
                        this.v[0xF] = 0;
                    }
                    this.v[x] -= this.v[y];
                    if (this.v[x] < 0) {
                        this.v[x] += 256;
                    }
                    break;
                }
                case 0x6: {
                    this.v[0xF] = this.v[x] & 0x1;
                    this.v[x] >>= 1;
                    break;
                }
                case 0x7: {
                    if (this.v[x] > this.v[y]) {
                        this.v[0xF] = 1;
                    } else {
                        this.v[0xF] = 0;
                    }
                    this.v[x] = this.v[y] - this.v[x];
                    if (this.v[x] < 0) {
                        this.v[x] += 256;
                    }
                    break;
                }
                case 0xE: {
                    if (this.v[x] & 0x80) {
                        this.v[0xF] = 1;
                    } else {
                        this.v[0xF] = 0;
                    }
                    this.v[x] <<= 1;
                    if (this.v[x] > 255) {
                        this.v[x] -= 256;
                    }
                    break;
                }
            }
            break;
        }
        case 0x9000: {
            if (this.v[x] !== this.v[y]) {
                this.pc += 2;
            }
            break;
        }
        case 0xA000: {
            this.I = nnn;
            break;
        }
        case 0xB000: {
            this.pc = nnn + this.v[0x0];
            break;
        }
        case 0xC000: {
            this.v[x] = Math.floor(Math.random() * 256);
            this.v[x] &= kk;
            break;
        }
        case 0xD000: {
            let xVal = this.v[x];
            let yVal = this.v[y];
            let height = n;

            for (let i = 0; i < height; i++) {
                let sprite = this.memory[this.I + i];
                for (let j = 0; j < 8; j++) {
                    if ((sprite & 0x80) > 0) {
                        if (this.display.setPixel(xVal + j, yVal + i)) {
                            this.v[0xF] = 1;
                        }
                    }
                    sprite <<= 1;
                }
            }
            break;
        }
        case 0xE000: {
            switch (kk) {
                case 0x9E: {
                    if (this.pressedKeys[this.v[x]] === 1) {
                        this.pc += 2;
                    }
                    break;
                }
                case 0xA1: {
                    if (this.pressedKeys[this.v[x]] !== 1) {
                        this.pc += 2;
                    }
                    break;
                }
            }
            break;
        }
        case 0xF000: {
            switch (kk) {
                case 0x07: {
                    this.v[x] = this.dt;
                    break;
                }
                case 0x0A: {
                    this.haltUntilKeypress = x;
                    break;
                }
                case 0x15: {
                    this.dt = this.v[x];
                    break;
                }
                case 0x18: {
                    this.st = this.v[x];
                    break;
                }
                case 0x1E: {
                    this.I += this.v[x];
                    break;
                }
                case 0x29: {
                    this.I = this.v[x] * 5;
                    break;
                }
                case 0x33: {
                    // Thanks to github.com/reu/chip8.js
                    this.memory[this.I] = parseInt(this.v[x] / 100);
                    this.memory[this.I + 1] = parseInt(this.v[x] % 100 / 10);
                    this.memory[this.I + 2] = this.v[x] % 10;
                    break;
                }
                case 0x55: {
                    for (let i = 0; i <= x; i++) {
                        this.memory[this.i + i] = this.v[i];
                    }
                    break;
                }
                case 0x65: {
                    for (let i = 0; i <= x; i++) {
                        this.v[i] = this.memory[this.I + i];
                    }
                    break;
                }
            }
            break;
        }

        default: {
            CHIP8Error(`Invalid opcode ${toHex(opcode)} at address ${toHex(addr)}`, true, undefined);
            break;
        }
    }

    if (this.pc !== addr) {
        console.log(`Jump to ${toHex(this.pc)}`);
    }
};

标签: javascriptchip-8

解决方案


您的问题似乎是您在 JMP 指令 (0x1nnn) 中分配 PC 后再次递增 PC(您可以在调试输出中看到差异)。所以在当前executeOpcode循环之后,执行返回this.step并命中这一行:

this.pc += 2

您应该在将 PC 增加 2 之前添加一个条件检查。应该这样做。

在操作码处理程序中:

case 0x1000: {
    this.pc = nnn;
    this.advancePC = false;
    break;
}

this.step

CHIP8.prototype.step = function() {
    if (this.halted) return;
    if (this.haltUntilKeypress) return;

    let opcode = this.decodeOpcode(this.pc);
    this.executeOpcode(opcode, this.pc);
    if (this.advancePC) {
        this.pc += 2;
    }
};

推荐阅读