用js实现8bit虚拟机(chip-8)
用js实现8bit虚拟机(chip-8)
cpu实现
export class Chip8 {
/**
* @type {import("./render.js").Renderer }
*/
renderer;
/**
* @type {import("./keyboard.js").Keyboard }
*/
keyboard;
/**
* @type {import("./speaker.js").Speaker }
*/
speaker;
/**
* @type { Uint8Array }
*/
memory;
i = 0;
/**
* @type { Uint8Array }
*/
v = 0
pc = 0;
sp = 0;
delayTimer = 0;
soundTimer = 0;
constructor(render, keyboard, speaker) {
this.renderer = render;
this.keyboard = keyboard;
this.speaker = speaker;
this.init();
}
init() {
this.renderer.clear();
this.speaker.stop();
this.memory = new Uint8Array(4096); // 4KB内存
this.v = new Uint8Array(16); // 16个8bit通用寄存器
this.i = 0; // 内存地址寄存器I
this.pc = 0x200; //程序计数器,用于存储当前执行的地址
this.sp = 0; //堆栈指针 (SP) 可以是 8 位,用于指向堆栈的最顶层。
this.delayTimer = 0;
this.soundTimer = 0;
this.stack = new Array();
this.paused = false;
this.speed = 5;
}
cycle() {
// exec code
for (let i = 0; i < this.speed; i++) {
if (this.paused) continue;
let code_h8 = this.memory[this.pc]
let code_l8 = this.memory[this.pc + 1]
let command = code_h8 << 8 | code_l8;
this.execute(command);
}
// timer update
if (!this.paused) {
this.delayTimer = Math.max(0, this.delayTimer - 1)
this.soundTimer = Math.max(0, this.soundTimer - 1)
}
// render graphics
if (!this.paused)
this.renderer.render();
// play sound
if (!this.paused && this.soundTimer > 0)
this.speaker.play(440);
else this.speaker.stop();
}
execute(opcode) {
this.pc += 2;
let nnn = opcode & 0x0fff;
let kk = opcode & 0x00ff;
let x = (opcode & 0x0f00) >> 8;
let y = (opcode & 0x00f0) >> 4;
let n = (opcode & 0x000f);
switch (opcode & 0xf000) {
// ################################################################################
case 0x0000:
switch (opcode) {
// `00E0 - CLS` 清除显示。
case 0x00e0:
this.renderer.clear();
break;
// `00EE - RET` 从子例程返回
case 0x00ee:
this.pc = this.stack.pop();
break;
}
// `0nnn - SYS addr` 跳转到 nnn 处的机器代码例程。
// This opcode can be ignored.
// console.log(nnn);
break;
// `1nnn - JP addr` 跳转到位置 nnn。
case 0x1000:
this.pc = nnn; // 跳转到该地址
break;
// `2nnn - CALL addr` 调用 nnn 处的子例程。
case 0x2000:
this.stack.push(this.pc); // 入栈当前地址
this.pc = nnn; // 跳转到该地址
break;
// ################################################################################
// `3xkk - SE Vx,byte` 如果 Vx = kk,则跳过下一条指令。
case 0x3000:
if (this.v[x] == kk) this.pc += 2;
break;
// `4xkk - SNE Vx,byte` 如果 Vx != kk,则跳过下一条指令。
case 0x4000:
if (this.v[x] != kk) this.pc += 2;
break;
// `5xy0 - SE Vx,Vy` 如果 Vx = Vy,则跳过下一条指令。
case 0x5000:
if (this.v[x] == this.v[y]) this.pc += 2;
break;
// ################################################################################
// `6xkk - LD Vx,byte` 设置 Vx = kk。
case 0x6000:
this.v[x] = kk;
break;
// `7xkk - ADD Vx,byte` 设置 Vx = Vx + kk。
case 0x7000:
this.v[x] += kk;
break;
// ################################################################################
case 0x8000:
switch (opcode & 0x0f) {
// `8xy0 - LD Vx, Vy` 设置 Vx = Vy。
case 0x00:
this.v[x] = this.v[y];
break;
// `8xy1 - OR Vx, Vy` 设置 Vx = Vx OR Vy。
case 0x01:
this.v[x] |= this.v[y];
break;
// `8xy2 - AND Vx, Vy` 设置 Vx = Vx AND Vy。
case 0x02:
this.v[x] &= this.v[y];
break;
// `8xy3 - XOR Vx, Vy` 设置 Vx = Vx XOR Vy。
case 0x03:
this.v[x] ^= this.v[y];
break;
// `8xy4 - ADD Vx, Vy` 设置 Vx = Vx ADD Vy。 set VF = carry.
case 0x04:
this.v[x] += this.v[y];
this.v[0xf] = this.v[x] > 0xff ? 1 : 0; // 溢出标志
this.v[x] &= 0xff // 截断
break;
// `8xy5 - SUB Vx, Vy` 设置 Vx = Vx - Vy,set VF = NOT borrow。
case 0x05:
// 如果 Vx > Vy,则将 VF 设置为 1,否则设置为 0。然后从 Vx 中减去 Vy,并将结果存储在 Vx 中。
this.v[0xf] = this.v[x] > this.v[y] ? 1 : 0;
this.v[x] -= this.v[y];
this.v[x] &= 0xff // 截断
break;
// `8xy6 - SHR Vx {, Vy}` 设置 Vx = Vx SHR 1。
// SHR: 右移位
case 0x06:
// 如果 Vx 的最低有效位为 1,则将 VF 设置为 1,否则设置为 0。然后将 Vx 除以 2。
this.v[0xf] = this.v[x] & 0x1;
this.v[x] >>= 1; // 右移1位
break;
// `8xy7 - SUBN Vx, Vy` 设置 Vx = Vy - Vx,设置 VF = NOT 借位。
case 0x07:
// 如果 Vy > Vx,则将 VF 设置为 1,否则设置为 0。然后从 Vy 中减去 Vx,并将结果存储在 Vx 中。
this.v[0xf] = this.v[y] > this.v[x] ? 1 : 0;
this.v[x] = this.v[y] - this.v[x];
this.v[x] &= 0xff // 截断
break;
// `8xyE - SHL Vx {, Vy}` 设置 Vx = Vx SHL 1。
// 如果 Vx 的最高有效位为 1,则将 VF 设置为 1,否则设置为 0。然后将 Vx 乘以 2。
case 0x0e:
this.v[0xf] = this.v[x] & 0x80;
this.v[x] <<= 1; // 左移1位
this.v[x] &= 0xff;// 截断
break;
}
break;
// ################################################################################
// `9xy0 - SNE Vx, Vy` 如果 Vx != Vy,则跳过下一条指令。
case 0x9000:
if (this.v[x] != this.v[y]) this.pc += 2;
break;
// `Annn - LD I, addr` 设置 I = nnn。
case 0xa000:
// 寄存器 I 的值设置为 nnn。
this.i = nnn;
break;
// `Bnnn - JP V0, addr` 跳转到位置 nnn + V0。
case 0xb000:
// 程序计数器设置为 nnn 加上 V0 的值。
this.pc = this.v[0] + nnn; // 相对跳转指令
break;
// `Cxkk - RND Vx, byte` 设置 Vx = 随机字节 AND kk。
case 0xc000:
// 解释器生成一个从 0 到 255 的随机数,然后将其与值 kk 进行 AND 运算。结果存储在 Vx 中。有关 AND 的更多信息,请参阅指令 8xy2。
this.v[x] = Math.floor(Math.random() * 0xff) & kk & 0xff;
break;
// `Dxyn - DRW Vx、Vy、nibble`
case 0xd000:
// 从(Vx, Vy)开始显示内存中从I位置开始的n的 n 个字节精灵,设置 VF = 碰撞。
for (let row = 0; row < n; row++) {
let byte = this.memory[this.i + row];
for (let col = 0; col < 8; col++) {
if (
(byte & 0x80) > 0 &&
this.renderer.setPixel(
this.v[x] + col,
this.v[y] + row
)) {
this.v[0xf] = 1;
}
byte <<= 1;
}
}
break;
case 0xe000:
switch (opcode & 0x00ff) {
// `Ex9E - SKP Vx` 如果按下值为 Vx 的键,则跳过下一条指令。
case 0x9E:
if (this.keyboard.isPressed(this.v[x])) this.pc += 2;
break;
// `ExA1 - SKNP Vx` 如果未按下值为 Vx 的键,则跳过下一条指令。
case 0xA1:
if (!this.keyboard.isPressed(this.v[x])) this.pc += 2;
break;
}
break;
case 0xf000:
switch (opcode & 0x00ff) {
// `Fx07 - LD Vx, DT` 设置 Vx = 延迟定时器值。
case 0x07:
this.v[x] = this.delayTimer;
break;
// `Fx0A - LD Vx, K` 等待按键,将按键的值存储在 Vx 中。
case 0x0a:
this.paused = true;
// 阻塞式等待按键按下
this.keyboard.waitforKey = (key) => {
this.v[x] = key
this.paused = false
}
break;
// ################################################################################
// 高级指令
// Fx15 - LD DT, Vx
case 0x15:
this.delayTimer = this.v[x];
break;
case 0x18:
this.soundTimer = this.v[x];
break;
case 0x1E:
this.i += this.v[x];
break;
case 0x29:
this.i = this.v[x] * 5;
break;
case 0x33:
this.memory[this.i + 0] = parseInt(this.v[x] / 100);
this.memory[this.i + 1] = parseInt((this.v[x] % 100) / 10);
this.memory[this.i + 2] = parseInt(this.v[x] % 10);
break;
case 0x55:
for (let registerIndex = 0; registerIndex <= x; registerIndex++) {
this.memory[this.i + registerIndex] = this.v[registerIndex];
}
break;
case 0x65:
for (let registerIndex = 0; registerIndex <= x; registerIndex++) {
this.v[registerIndex] = this.memory[this.i + registerIndex];
}
break;
}
break;
}
}
loadSprites() {
const sprites = [
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
];
for (let i = 0; i < sprites.length; i++) {
this.memory[i] = sprites[i];
}
}
loadProgram(program) {
const offset = 0x200;
for (let idx = 0; idx < program.length; idx++) {
this.memory[offset + idx] = program[idx];
}
this.pc = offset
}
async loadROM(path) {
let response = await fetch(path);
let buffer = await response.arrayBuffer();
let program = new Uint8Array(buffer);
this.loadProgram(program);
}
}
render实现
export class Renderer {
cols = 0
rows = 0
scale = 0
/**
* @type {HTMLCanvasElement}
*/
canvas = null
/**
* @type {CanvasRenderingContext2D}
*/
ctx = null
/**
* @type {Array<number>}
*/
display = null
constructor(canvas, scale) {
this.cols = 64;
this.rows = 32;
this.canvas = canvas;
this.scale = scale;
this.ctx = this.canvas.getContext('2d');
this.canvas.width = this.cols * this.scale;
this.canvas.height = this.rows * this.scale;
this.display = new Array(this.cols * this.rows);
}
setPixel(x, y) {
x = Math.max(0, Math.min(this.cols - 1, x));
y = Math.max(0, Math.min(this.rows - 1, y));
let offset = y * this.cols + x;
// sprites are XORed onto the display:
this.display[offset] ^= 1;
return !this.display[offset]; // return pixel was erased or not.
}
clear() {
this.display = new Array(this.cols * this.rows);
}
render() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (let y = 0; y < this.rows; y++) {
for (let x = 0; x < this.cols; x++) {
let offset = y * this.cols + x;
this.ctx.fillStyle = this.display[offset] ? '#000' : '#fff';
this.ctx.fillRect(x * this.scale, y * this.scale, this.scale, this.scale);
}
}
}
test() {
this.setPixel(0, 0);
this.setPixel(10, 0);
this.setPixel(this.cols - 1, this.rows - 1);
console.log(this.display)
this.render();
}
}
speaker实现
export class Speaker {
constructor() {
this.ctx = new AudioContext();
this.gain = this.ctx.createGain(); // for to control the volume
this.gain.connect(this.ctx.destination);
}
mute() {
this.gain.gain.value = 0; // 0%
}
unmute() {
this.gain.gain.value = 1; // 100%
}
play(freq) {
if (this.osc) return;
this.osc = this.ctx.createOscillator();
this.osc.frequency.value = freq || 400;
this.osc.type = 'square'; // 三角波
this.osc.connect(this.gain);
this.osc.start();
}
stop() {
if (!this.osc) return;
this.osc.stop();
this.osc.disconnect();
this.osc = null;
}
}
keyboard实现
export class Keyboard {
KEYMAP = {
'1': 0x1, '2': 0x2, '3': 0x3, '4': 0xc, // 1 2 3 4
'q': 0x4, 'w': 0x5, 'e': 0x6, 'r': 0xD, // q w e r
'a': 0x7, 's': 0x8, 'd': 0x9, 'f': 0xE, // a s d f
'z': 0xA, 'x': 0x0, 'c': 0xB, 'v': 0xF // z x c v
}
/**
* @type { Array<boolean> }
*/
buttons = [];
constructor() {
window.addEventListener('keydown', this.onKeyDown.bind(this))
window.addEventListener('keyup', this.onKeyUp.bind(this))
}
isPressed(key) {
return this.buttons[key]
}
/**
*
* @param {KeyboardEvent} e
*/
onKeyDown(e) {
if (this.KEYMAP[e.key]) {
this.buttons[this.KEYMAP[e.key]] = true;
if (this.waitforKey) {
this.waitforKey(this.KEYMAP[e.key])
this.waitforKey = null
}
}
}
/**
*
* @param {KeyboardEvent} e
*/
onKeyUp(e) {
if (this.KEYMAP[e.key]) {
this.buttons[this.KEYMAP[e.key]] = false;
if (this.waitforKey) {
this.waitforKey(this.KEYMAP[e.key])
this.waitforKey = null
}
}
}
}
main.js
import { Renderer } from "./chip-8/render.js";
import { Keyboard } from "./chip-8/keyboard.js"
import { Chip8 } from "./chip-8/cpu.js";
import { Speaker } from "./chip-8/speaker.js";
document.addEventListener("DOMContentLoaded", async () => {
let main = document.querySelector("#main");
let canvas = document.createElement("canvas");
let renderer = new Renderer(canvas, 10);
let keyboard = new Keyboard();
let speaker = new Speaker();
let chip8 = new Chip8(renderer, keyboard, speaker);
chip8.loadSprites()
await chip8.loadROM("/src/rom/PONG");
console.log(chip8.pc, chip8.memory);
// renderer.test()
requestAnimationFrame(function loop() {
chip8.cycle();
requestAnimationFrame(loop);
});
let selecter = document.createElement("select");
selecter.innerHTML = ['BLINKY', 'CONNECT4', 'INVADERS', 'LANDING', 'MAZE', 'PONG', 'SPACE', 'TANK', 'TETRIS', 'TICTACTOE', 'WALL'].map(file => `<option value="${file}">${file}</option>`).join("");
selecter.addEventListener("change", async (e) => {
chip8.init()
chip8.loadROM(`/src/rom/${e.target.value}`);
});
main.appendChild(canvas);
main.appendChild(selecter);
});
mindex.html
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="main"></div>
<script type="module" src="./src/main.js"></script>
<style>
* {
padding: 0;
margin: 0;
}
:root,
body,
#main {
width: 100%;
height: 100%;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
max-width: 100%;
max-height: 100%;
border: 1px solid #000;
}
</style>
</body>
</html>
参考文档
关于 chip-8
“Chip-8 是一种简单的解释型编程语言,最早在 20 世纪 70 年代末和 80 年代初用于一些 DIY 计算机系统。COSMAC VIP、DREAM 6800 和 ETI 660 计算机就是其中几个例子。这些计算机通常设计为使用电视作为显示器,具有 1 到 4K 的 RAM,并使用 16 键十六进制键盘进行输入。该解释器仅占用 512 字节内存,而以十六进制输入计算机的程序甚至更小。
20 世纪 90 年代初,一个名叫 Andreas Gustafsson 的人使 Chip-8 语言再次流行。他为 HP48 图形计算器创建了一个 Chip-8 解释器,称为 Chip-48。当时,HP48 缺乏一种轻松制作快速游戏的方法,而 Chip-8 就是答案。Chip-48 后来催生了 Super Chip-48,这是 Chip-48 的改进版,允许更高分辨率的图形以及其他图形增强功能。
Chip-48 启发了一系列适用于各种平台的全新 Chip-8 解释器,包括 MS-DOS、Windows 3.1、Amiga、HP48、MSX、Adam 和 ColecoVision。”
内存 Memory
- Chip-8 语言能够访问最多 4KB(4,096 字节)的 RAM,从位置 0x000(0)到 0xFFF(4095)。
- 前 512 个字节(从 0x000 到 0x1FF)是原始解释器所在的位置,不应由程序使用。
- 大多数 Chip-8 程序从位置 0x200(512)开始,但有些从 0x600(1536)开始。
- 从 0x600 开始的程序适用于 ETI 660 计算机。
+---------------+= 0xFFF (4095) 内存结束地址
| |
| |
| |
| |
| |
| 0x200 to 0xFFF|
| Chip-8 |
| Program / Data|
| Space |
| |
| |
| |
+- - - - - - - -+= 0x600 (1536) ETI 660 Chip-8 程序的开始位置
| |
| |
| |
+---------------+= 0x200 (512) 大多数 Chip-8 程序的开始位置
| 0x000 to 0x1FF|
| Reserved for |
| interpreter |
+---------------+= 0x000 (0) 内存开始地址
寄存器 Registers
Vx 通用寄存器
- 16 个通用 8 位寄存器,通常称为 Vx,其中 x 是十六进制数字(0 到 F)。
I 存储内存地址寄存器
- 一个 16 位寄存器,称为 I。
- 此寄存器通常用于存储内存地址,因此通常只使用最低(最右边)的 12 位。
VF 寄存器
- VF 寄存器不应由任何程序使用,因为它被某些指令用作标志。
2 个专用 8 位寄存器
- Chip-8 还有两个专用 8 位寄存器,用于延迟和声音计时器。
- 当这些寄存器非零时,它们会自动以 60Hz 的速率递减。
程序计数器 (PC)
- 程序计数器 (PC) 应该是 16 位,用于存储当前执行的地址。
堆栈指针 (SP)
- 堆栈指针 (SP) 可以是 8 位,用于指向堆栈的最顶层。
- 堆栈是一个由 16 个 16 位值组成的数组,用于存储解释器在完成子程序后应返回的地址。
- Chip-8 最多允许 16 层嵌套子程序。
伪寄存器
- 还有一些“伪寄存器”无法从 Chip-8 程序访问。
键盘 Keyboard
最初使用 Chip-8 语言的计算机有一个 16 键十六进制键盘,布局如下:
1 2 3 C
4 5 6 D
7 8 9 E
A 0 B F
显示器 Display
Chip-8 语言的原始实现使用了 64x32 像素的单色显示器,其格式如下:
(00,00) +-------------------+ (63,00)
| |
| |
(00,31) +-------------------+ (63,31)
其他一些解释器(最著名的是 ETI 660 上的解释器)也具有 64x48 和 64x64 模式。
HP48 计算器的解释器 Super Chip-48 添加了 128x64 像素模式。其他平台上的大多数解释器都支持此模式。
Chip-8 通过使用精灵(sprite)在屏幕上绘制图形。精灵是一组字节,是所需图片的二进制表示。
Chip-8 精灵可能最多有 15 个字节,精灵大小可能为 8x15。
程序还可以引用一组表示十六进制数字 0 到 F 的精灵。
这些精灵长 5 个字节,或 8x5 像素。
数据应存储在 Chip-8 内存的解释器区域(0x000 到 0x1FF)。
以下是每个字符的字节的二进制和十六进制列表:
"0" Binary Hex
**** 11110000 0xF0
* * 10010000 0x90
* * 10010000 0x90
* * 10010000 0x90
**** 11110000 0xF0
"1"
* 00100000 0x20
** 01100000 0x60
* 00100000 0x20
* 00100000 0x20
*** 01110000 0x70
"2"
**** 11110000 0xF0
* 00010000 0x10
**** 11110000 0xF0
* 10000000 0x80
**** 11110000 0xF0
"3"
**** 11110000 0xF0
* 00010000 0x10
**** 11110000 0xF0
* 00010000 0x10
**** 11110000 0xF0
"4"
* * 10010000 0x90
* * 10010000 0x90
**** 11110000 0xF0
* 00010000 0x10
* 00010000 0x10
"5"
**** 11110000 0xF0
* 10000000 0x80
**** 11110000 0xF0
* 00010000 0x10
**** 11110000 0xF0
"6"
**** 11110000 0xF0
* 10000000 0x80
**** 11110000 0xF0
* * 10010000 0x90
**** 11110000 0xF0
"7"
**** 11110000 0xF0
* 00010000 0x10
* 00100000 0x20
* 01000000 0x40
* 01000000 0x40
"8"
**** 11110000 0xF0
* * 10010000 0x90
**** 11110000 0xF0
* * 10010000 0x90
**** 11110000 0xF0
"9"
**** 11110000 0xF0
* * 10010000 0x90
**** 11110000 0xF0
* 00010000 0x10
**** 11110000 0xF0
"A"
**** 11110000 0xF0
* * 10010000 0x90
**** 11110000 0xF0
* * 10010000 0x90
* * 10010000 0x90
"B"
*** 11100000 0xE0
* * 10010000 0x90
*** 11100000 0xE0
* * 10010000 0x90
*** 11100000 0xE0
"C"
**** 11110000 0xF0
* 10000000 0x80
* 10000000 0x80
* 10000000 0x80
**** 11110000 0xF0
"D"
*** 11100000 0xE0
* * 10010000 0x90
* * 10010000 0x90
* * 10010000 0x90
*** 11100000 0xE0
"E"
**** 11110000 0xF0
* 10000000 0x80
**** 11110000 0xF0
* 10000000 0x80
**** 11110000 0xF0
"F"
**** 11110000 0xF0
* 10000000 0x80
**** 11110000 0xF0
* 10000000 0x80
* 10000000 0x80
定时器和声音 Timers & Sound
Chip-8 提供 2 个定时器,一个延迟定时器和一个声音定时器。
只要延迟定时器寄存器 (DT) 非零,延迟定时器就会激活。此定时器所做的只是以 60Hz 的速率从 DT 的值中减 1。当 DT 达到 0 时,它将停用。
只要声音定时器寄存器 (ST) 非零,声音定时器就会激活。此定时器也以 60Hz 的速率递减,但是,只要 ST 的值大于零,Chip-8 蜂鸣器就会发声。当 ST 达到零时,声音定时器将停用。
Chip-8 解释器产生的声音只有一个音调。此音调的频率由解释器的作者决定。
指令 Instructions
Chip-8 语言的原始实现包括 36 条不同的指令,包括数学、图形和流控制功能。
Super Chip-48 又增加了 10 条指令,总共 46 条。
所有指令都是 2 个字节长,并以最高有效字节优先的方式存储。在内存中,每条指令的第一个字节应位于偶数地址。如果程序包含精灵数据,则应对其进行填充,以便其后的任何指令都能正确放置在 RAM 中。
在这些列表中,使用了以下变量:
nnn
或addr
- 12 位值,指令的最低 12 位n
或nibble
- 4 位值,指令的最低 4 位x
- 4 位值,指令高字节的低 4 位y
- 4 位值,指令低字节的高 4 位kk
或byte
- 8 位值,指令的最低 8 位
标准指令 Standard Chip-8 Instructions
0nnn - SYS addr
- 跳转到 nnn 处的机器代码例程。
- 此指令仅用于最初实现 Chip-8 的旧计算机。现代解释器会忽略它。
00E0 - CLS
- 清除显示。
00EE - RET
- 从子例程返回。
- 解释器将程序计数器设置为堆栈顶部的地址,然后从堆栈指针中减 1。
1nnn - JP addr
- 跳转到位置 nnn。
- 解释器将程序计数器设置为 nnn。
2nnn - CALL addr
- 调用 nnn 处的子例程。
- 解释器增加堆栈指针,然后将当前 PC 放在堆栈顶部。然后将 PC 设置为 nnn。
3xkk - SE Vx,byte
- 如果 Vx = kk,则跳过下一条指令。
- 解释器将寄存器 Vx 与 kk 进行比较,如果它们相等,则将程序计数器加 2。
4xkk - SNE Vx,byte
- 如果 Vx != kk,则跳过下一条指令。
- 解释器将寄存器 Vx 与 kk 进行比较,如果它们不相等,则将程序计数器加 2。
5xy0 - SE Vx,Vy
- 如果 Vx = Vy,则跳过下一条指令。
- 解释器将寄存器 Vx 与寄存器 Vy 进行比较,如果它们相等,则将程序计数器加 2。
6xkk - LD Vx,byte
- 设置 Vx = kk。
- 解释器将值 kk 放入寄存器 Vx。
7xkk - ADD Vx,byte
- 设置 Vx = Vx + kk。
- 将值 kk 添加到寄存器 Vx 的值,然后将结果存储在 Vx 中。
8xy0 - LD Vx, Vy
- 设置 Vx = Vy。
- 将寄存器 Vy 的值存储在寄存器 Vx 中。
8xy1 - OR Vx, Vy
- 设置 Vx = Vx OR Vy。
- 对 Vx 和 Vy 的值执行按位或,然后将结果存储在 Vx 中。按位或比较两个值的对应位,如果任一位为 1,则结果中的同一位也为 1。否则为 0。
8xy2 - AND Vx, Vy
- 设置 Vx = Vx AND Vy。
- 对 Vx 和 Vy 的值执行按位与,然后将结果存储在 Vx 中。按位与比较两个值的对应位,如果两个位都是 1,则结果中的相同位也为 1。否则,结果为 0。
8xy3 - XOR Vx, Vy
- 设置 Vx = Vx XOR Vy。
- 对 Vx 和 Vy 的值执行按位异或,然后将结果存储在 Vx 中。异或比较两个值的对应位,如果两个位不相同,则结果中的对应位设置为 1。否则,结果为 0。
8xy4 - ADD Vx, Vy
- 设置 Vx = Vx + Vy,设置 VF = 进位。
- 将 Vx 和 Vy 的值相加。如果结果大于 8 位(即 > 255),则 VF 设置为 1,否则为 0。只保留结果的最低 8 位,并存储在 Vx 中。
8xy5 - SUB Vx, Vy
- 设置 Vx = Vx - Vy,设置 VF = NOT 借位。
- 如果 Vx > Vy,则将 VF 设置为 1,否则设置为 0。然后从 Vx 中减去 Vy,并将结果存储在 Vx 中。
8xy6 - SHR Vx {, Vy}
- 设置 Vx = Vx SHR 1。
- 如果 Vx 的最低有效位为 1,则将 VF 设置为 1,否则设置为 0。然后将 Vx 除以 2。
8xy7 - SUBN Vx, Vy
- 设置 Vx = Vy - Vx,设置 VF = NOT 借位。
- 如果 Vy > Vx,则将 VF 设置为 1,否则设置为 0。然后从 Vy 中减去 Vx,并将结果存储在 Vx 中。
8xyE - SHL Vx {, Vy}
- 设置 Vx = Vx SHL 1。
- 如果 Vx 的最高有效位为 1,则将 VF 设置为 1,否则设置为 0。然后将 Vx 乘以 2。
9xy0 - SNE Vx, Vy
- 如果 Vx != Vy,则跳过下一条指令。
- 比较 Vx 和 Vy 的值,如果它们不相等,则程序计数器增加 2。
Annn - LD I, addr
- 设置 I = nnn。
- 寄存器 I 的值设置为 nnn。
Bnnn - JP V0, addr
- 跳转到位置 nnn + V0。
- 程序计数器设置为 nnn 加上 V0 的值。
Cxkk - RND Vx, byte
- 设置 Vx = 随机字节 AND kk。
- 解释器生成一个从 0 到 255 的随机数,然后将其与值 kk 进行 AND 运算。结果存储在 Vx 中。有关 AND 的更多信息,请参阅指令 8xy2。
Dxyn - DRW Vx、Vy、nibble
- 显示从内存位置 I 的 (Vx, Vy) 开始的 n 字节精灵,设置 VF = 碰撞。
- 解释器从内存中读取 n 个字节,从存储在 I 中的地址开始。然后,这些字节在屏幕上的坐标 (Vx, Vy) 处显示为精灵。精灵在现有屏幕上进行异或运算。如果这导致任何像素被擦除,则 VF 设置为 1,否则设置为 0。如果精灵的位置使其部分超出显示器的坐标,则它会绕到屏幕的另一侧。有关 XOR 的更多信息,请参阅指令 8xy3;
Ex9E - SKP Vx
- 如果按下值为 Vx 的键,则跳过下一条指令。
- 检查键盘,如果与 Vx 值对应的键当前处于向下位置,则 PC 增加 2。
ExA1 - SKNP Vx
- 如果未按下值为 Vx 的键,则跳过下一条指令。
- 检查键盘,如果与 Vx 值对应的键当前处于向上位置,则 PC 增加 2。
Fx07 - LD Vx, DT
- 设置 Vx = 延迟定时器值。
- DT 的值放入 Vx。
Fx0A - LD Vx, K
- 等待按键,将按键的值存储在 Vx 中。
- 所有执行停止,直到按下按键,然后
扩展指令 Super Chip-48 Instructions
00Cn - SCD nibble
00FB - SCR
00FC - SCL
00FD - EXIT
00FE - LOW
00FF - HIGH
Dxy0 - DRW Vx, Vy, 0
Fx30 - LD HF, Vx
Fx75 - LD R, Vx
Fx85 - LD Vx, R
实现
Title | Version | Author | Platform(s) |
---|---|---|---|
Chip-48 | 2.20 | Anrdreas Gustafsson | HP48 |
Chip8 | 1.1 | Paul Robson DOS | |
Chip-8 | Emulator 2.0.0 | David Winter | DOS |
CowChip | 0.1 | Thomas P. Greene | Windows 3.1 |
DREAM MON | 1.1 | Paul Hayter | Amiga |
Super Chip-48 | 1.1 | Based on Chip-48, modified by Erik Bryntse | HP48 |
Vision-8 | 1.0 | Marcel de Kogel | DOS, Adam, MSX, ColecoVision |