范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

用JavaScript编写8位机仿真器

  What is Chip-8? 什么是8位机
  在开始这个项目之前,我从未听说过8位机,所以我想大多数人也没有听说过,除非他们已经进入了模拟器。Chip-8是一种非常简单的解释型编程语言,于20世纪70年代为业余计算机开发。人们编写了基本的Chip-8程序,模仿当时流行的游戏,如Pong,俄罗斯方块,太空侵略者,可能还有其他独特的游戏,失去了时间的歼灭。
  玩这些游戏的虚拟机实际上是Chip-8解释器,而不是技术上的模拟器,因为模拟器是模拟特定机器硬件的软件,而Chip-8程序不与任何特定硬件相关联。通常,Chip-8解释器用于图形计算器。
  尽管如此,它已经足够接近于成为模拟器,对于任何想要学习如何构建模拟器的人来说,它通常是开始的项目,因为它比创建NES模拟器或除此之外的任何东西都要简单得多。对于许多CPU概念来说,这也是一个很好的起点,比如内存、堆栈和I/O,这些都是我在JavaScript运行时无限复杂的世界中每天要处理的事情。What Goes Into a Chip-8 Interpreter? 8位机解释器
  我必须做很多预习才能开始理解我正在做什么,因为我以前从未学习过计算机科学的基础知识。所以我写了理解位,字节,基,并在JavaScript中编写十六进制转储,其中大部分内容。
  总而言之,该文章有两个主要要点:位和字节 - 位是二进制数字 - 0 或 1,真或假,开或关。八位是一个字节,它是计算机使用的基本信息单位。 数字基数 - 十进制是我们最常处理的基数系统,但计算机通常使用二进制(基数2)或十六进制(基数16)。二进制的 1111、十进制的 15 和十六进制的 f 都是相同的数字。
  CPU是执行程序指令的计算机的主处理器。在这种情况下,它由下面描述的各种状态位以及包含获取,解码和执行步骤的指令周期组成。Memory 内存Program counter 计算计算器Registers 寄存器Index register 寄存器索引Stack 栈Stack pointer 栈指针Key input 输入Graphical output 图像输出Timers 时钟Memory 内存
  8位计算机,可以访问高达4千字节的内存(RAM)。(这是软盘上存储空间的 0.002%。CPU中的绝大多数数据都存储在内存中。
  4kb是4096字节,JavaScript有一些有用的类型化数组,比如Uint8Array,它是某个元素的固定大小的数组 - 在这种情况下是8位。let memory = new Uint8Array(4096)
  您可以像访问和使用此数组一样访问和使用此数组,从内存[0]到内存[4095],并将每个元素设置为最大255的值。任何高于此值的内容都将回退到该值(例如,内存[0] = 300 将导致内存[0] === 255)。
  Program counter 程序计数器
  程序计数器将当前指令的地址存储为 16 位整数。Chip-8中的每条指令都将在完成后更新程序计数器(PC),以进入下一条指令,方法是访问以PC为索引的存储器。
  在8位机内存布局中,内存中0x1FF 0x000是保留的,因此它从0x200开始。let PC = 0x200 // memory[PC] will access the address of  the current instruvtion
  *您会注意到内存阵列是8位的,而PC是16位整数,因此两个程序代码将被组合成一个大的字节序操作码。Registers 寄存器
  存储器通常用于长期存储和程序数据,因此寄存器作为一种"短期存储器"存在,用于即时数据和计算。8位机有 16 个 8 位寄存器。它们被称为 V0 到 VF。let registers = new Uint8Array(16)Index register 索引寄存器
  有一个特殊的16位寄存器,用于访问内存中的特定点,称为I。I寄存器通常用于读取和写入内存,因为可寻址内存也是16位的。let I = 0Stack 栈
  8位机能够进入子例程,以及一个用于跟踪返回位置的堆栈。堆栈是 16 个 16 位值,这意味着程序在经历"堆栈溢出"之前可以进入 16 个嵌套的子例程。let stack = new Uint16Array(16)Stack pointer 栈指针
  堆栈指针 (SP) 是一个 8 位整数,指向堆栈中的某个位置。即使堆栈是 16 位,它也只需要是 8 位,因为它只引用堆栈的索引,因此只需要 0 彻底 15。let SP = -1  // stack[SP] will access the current return address in the stackTimers 定时器
  8位机能够发出光荣的一声蜂鸣声。说实话,我没有费心为"音乐"实现实际输出,尽管CPU本身都设置为与它正确接口。有两个计时器,都是8位寄存器 - 一个声音计时器(ST)用于决定何时发出蜂鸣声,一个延迟计时器(DT)用于在整个游戏中对某些事件进行计时。它们在 60 Hz 下倒计时。let DT = 0 let ST = 0Key input 输入
  8位机的设置是为了与惊人的十六进制键盘接口。它看起来像这样:┌───┬───┬───┬───┐ │ 1 │ 2 │ 3 │ C │ │ 4 │ 5 │ 6 │ D │ │ 7 │ 8 │ 9 │ E │ │ A │ 0 │ B │ F │ └───┴───┴───┴───┘
  在实践中,似乎只使用了几个键,你可以将它们映射到你想要的任何4x4网格,但它们在游戏之间非常不一致。Graphical output 图形输出
  8位机 使用单色 64x32 分辨率显示器。每个像素要么打开,要么关闭。
  可以保存在内存中的精灵为 8x15 - 8 个像素宽 x 15 个像素高。Chip-8还附带了字体集,但它只包含十六进制键盘中的字符,因此总体上不是最有用的字体集。CPU
  将它们放在一起,您将获得CPU状态。
  CPUclass CPU {   constructor() {     this.memory = new Uint8Array(4096)     this.registers = new Uint8Array(16)     this.stack = new Uint16Array(16)     this.ST = 0     this.DT = 0     this.I = 0     this.SP = -1     this.PC = 0x200   } }Decoding Chip-8 Instructions 指令解码
  8位机有36条指令。此处列出了所有说明。所有指令的长度均为 2 个字节(16 位)。每条指令都由操作码(操作码)和操作数(操作码)编码,操作数据作。
  如两个操作x = 1 y = 2  ADD x, y
  其中 ADD 是操作码,x、y 是操作数。这种类型的语言称为汇编语言。此指令将映射到:x = x + y
  使用此指令集,我必须将此数据存储在16位中,因此每个指令最终都是从0x0000到0xffff的数字。这些集合中的每个数字位置都是一个半字节(4 位)。
  那么我怎么能从nnnn到像ADD x,y这样的东西,这更容易理解呢?好吧,我将首先查看Chip-8中的一条指令,它与上面的例子基本相同:
  Instruction
  Description
  8xy4
  ADD Vx, Vy
  那么,我们在这里处理的是什么呢?有一个关键字 ADD 和两个参数 Vx 和 Vy,我们在上面建立的它们是寄存器。
  如操作指令:ADD (add)SUB (subtract)JP (jump)SKP (skip)RET (return)LD (load)
  数据类型:地址(I)寄存器(Vx, Vy)常数(N or NN for nibble or byte)
  下一步是找到一种方法将16位操作码解释为这些更易于理解的指令。Bit Masking 位操作const opcode = 0x8124 const mask = 0xf00f const pattern = 0x8004  const isMatch = (opcode & mask) === pattern // trueconst x = (0x8124 & 0x0f00) >> 8 // 1  // (0x8124 & 0x0f00) is 100000000 in binary // right shifting by 8 (>> 8) will remove 8 zeroes from the right // This leaves us with 1  const y = (0x8124 & 0x00f0) >> 4 // 2 // (0x8124 & 0x00f0) is 100000 in binary // right shifting by 4 (>> 4) will remove 4 zeroes from the right // This leaves us with 10, the binary equivalent of 2const instruction = {   id: "ADD_VX_VY",   name: "ADD",   mask: 0xf00f,   pattern: 0x8004,   arguments: [     { mask: 0x0f00, shift: 8, type: "R" },     { mask: 0x00f0, shift: 4, type: "R" },   ], }
  Disassemblerfunction disassemble(opcode) {   // Find the instruction from the opcode   const instruction = INSTRUCTION_SET.find(     (instruction) => (opcode & instruction.mask) === instruction.pattern   )   // Find the argument(s)   const args = instruction.arguments.map((arg) => (opcode & arg.mask) >> arg.shift)    // Return an object containing the instruction data and arguments   return { instruction, args } }Reading the ROM
  由于我们将此项目视为仿真器,因此每个8位程序文件都可以被视为一个ROM。ROM只是二进制数据,我们正在编写程序来解释它。我们可以把Chip8 CPU想象成一个虚拟游戏机,而一个8位ROM是一个虚拟游戏卡带。
  RomBuffer.jsclass RomBuffer {   /**    * @param {binary} fileContents ROM binary    */   constructor(fileContents) {     this.data = []      // Read the raw data buffer from the file     const buffer = fileContents      // Create 16-bit big endian opcodes from the buffer     for (let i = 0; i < buffer.length; i += 2) {       this.data.push((buffer[i] << 8) | (buffer[i + 1] << 0))     }   } }
  指令周期 - 获取、解码、执行
  现在,我已经准备好解释指令集和游戏数据。CPU只需要对它做点什么。指令周期包括三个步骤 - 获取、解码和执行。Fetch - 获取数据Decode - 译码Execute - 执行
  Fetch// Get address value from memory function fetch() {   return memory[PC] }
  Decode// Decode instruction function decode(opcode) {   return disassemble(opcode) }
  Execute// Execute instruction function execute(instruction) {   const { id, args } = instruction    switch (id) {     case "ADD_VX_VY":       // Perform the instruction operation       registers[args[0]] += registers[args[1]]        // Update program counter to next instruction       PC = PC + 2       break     case "SUB_VX_VY":     // etc...   } }
  CPU.jsclass CPU {   constructor() {     this.memory = new Uint8Array(4096)     this.registers = new Uint8Array(16)     this.stack = new Uint16Array(16)     this.ST = 0     this.DT = 0     this.I = 0     this.SP = -1     this.PC = 0x200   }    // Load buffer into memory   load(romBuffer) {     this.reset()      romBuffer.forEach((opcode, i) => {       this.memory[i] = opcode     })   }    // Step through each instruction   step() {     const opcode = this._fetch()     const instruction = this._decode(opcode)      this._execute(instruction)   }    _fetch() {     return this.memory[this.PC]   }    _decode(opcode) {     return disassemble(opcode)   }    _execute(instruction) {     const { id, args } = instruction      switch (id) {       case "ADD_VX_VY":         this.registers[args[0]] += this.registers[args[1]]         this.PC = this.PC + 2         break     }   } }Creating a CPU Interface for I/O 输入输出
  所以现在我有了这个CPU,它正在解释和执行指令并更新它自己的所有状态,但我现在还不能用它做任何事情。为了玩游戏,你必须看到它并能够与之互动。
  这就是输入/输出或 I/O 的用武之地。I/O 是 CPU 与外部世界之间的通信。输入是 CPU 接收的数据输出是从 CPU 发送的数据
  CpuInterface.js// Abstract CPU interface class class CpuInterface {   constructor() {     if (new.target === CpuInterface) {       throw new TypeError("Cannot instantiate abstract class")     }   }    clearDisplay() {     throw new TypeError("Must be implemented on the inherited class.")   }    waitKey() {     throw new TypeError("Must be implemented on the inherited class.")   }    getKeys() {     throw new TypeError("Must be implemented on the inherited class.")   }    drawPixel() {     throw new TypeError("Must be implemented on the inherited class.")   }    enableSound() {     throw new TypeError("Must be implemented on the inherited class.")   }    disableSound() {     throw new TypeError("Must be implemented on the inherited class.")   } }class CPU {   // Initialize the interface   constructor(cpuInterface) {     this.interface = cpuInterface   }    _execute(instruction) {     const { id, args } = instruction      switch (id) {       case "CLS":         // Use the interface while executing an instruction         this.interface.clearDisplay()   } }Screen 显示
  屏幕的分辨率为 64 像素宽 x 32 像素高。因此,就CPU和接口而言,它是一个64x32的位网格,这些位要么打开要么关闭。要设置一个空屏幕,我可以制作一个零的3D数组来表示所有关闭的像素。帧缓冲区是内存的一部分,其中包含将呈现到显示器上的位图图像。
  MockCpuInterface.js// Interface for testing class MockCpuInterface extends CpuInterface {   constructor() {     super()      // Store the screen data in the frame buffer     this.frameBuffer = this.createFrameBuffer()   }    // Create 3D array of zeroes   createFrameBuffer() {     let frameBuffer = []      for (let i = 0; i < 32; i++) {       frameBuffer.push([])       for (let j = 0; j < 64; j++) {         frameBuffer[i].push(0)       }     }      return frameBuffer   }    // Update a single pixel with a value (0 or 1)   drawPixel(x, y, value) {     this.frameBuffer[y][x] ^= value   } }
  在 DRW 函数中,CPU 将循环遍历它从内存中提取的子画面,并更新子画面中的每个像素(为简洁起见,省略了一些细节)。case "DRW_VX_VY_N":   // The interpreter reads n bytes from memory, starting at the address stored in I   for (let i = 0; i < args[2]; i++) {     let line = this.memory[this.I + i]       // Each byte is a line of eight pixels       for (let position = 0; position < 8; position++) {         // ...Get value, x, and y...         this.interface.drawPixel(x, y, value)       }     }
  clearDisplay() 函数是将用于与屏幕交互的唯一其他方法。这是与屏幕交互所需的所有CPU接口。Keys 按键┌───┬───┬───┬───┐ │ 1 │ 2 │ 3 │ 4 │ │ Q │ W │ E │ R │ │ A │ S │ D │ F │ │ Z │ X │ C │ V │ └───┴───┴───┴───┘// prettier-ignore const keyMap = [   "1", "2", "3", "4",   "q", "w", "e", "r",    "a", "s", "d", "f",    "z", "x", "c", "v" ]
  按键按下状态.this.keys = 00b1000000000000000 // V is pressed (keyMap[15], or index 15) 0b0000000000000011 // 1 and 2 are pressed (index 0, 1) 0b0000000000110000 // Q and W are pressed (index 4, 5)case "SKP_VX":   // Skip next instruction if key with the value of Vx is pressed   if (this.interface.getKeys() & (1 << this.registers[args[0]])) {    // Skip instruction   } else {     // Go to next instruction   }Screen 显示器
  对于所有实现,包含屏幕数据位图的帧缓冲区都是相同的,但屏幕与每个环境的接口方式将不同。
  带着祝福,我只是定义了一个屏幕对象:this.screen = blessed.screen({ smartCSR: true })
  并在像素上使用fillRegion或clearRegion,并使用完整的 unicode 块进行填充,使用帧缓冲区作为数据源。drawPixel(x, y, value) {   this.frameBuffer[y][x] ^= value    if (this.frameBuffer[y][x]) {     this.screen.fillRegion(this.color, "█", x, x + 1, y, y + 1)   } else {     this.screen.clearRegion(x, x + 1, y, y + 1)   }    this.screen.render() }Keys 按键
  按键处理程序与我对DOM的期望没有太大区别。如果按下某个键,处理程序将传递该键,然后我可以使用该键查找索引并使用已按下的任何其他新键更新 keys 对象。this.screen.on("keypress", (_, key) => {   const keyIndex = keyMap.indexOf(key.full)    if (keyIndex) {     this._setKeys(keyIndex)   } })setInterval(() => {   // Emulate a keyup event to clear all pressed keys   this._resetKeys() }, 100)Entrypoint 入口点
  terminal.jsterminal.js const fs = require("fs") const { CPU } = require("../classes/CPU") const { RomBuffer } = require("../classes/RomBuffer") const { TerminalCpuInterface } = require("../classes/interfaces/TerminalCpuInterface")  // Retrieve the ROM file const fileContents = fs.readFileSync(process.argv.slice(2)[0])  // Initialize the terminal interface const cpuInterface = new TerminalCpuInterface()  // Initialize the CPU with the interface const cpu = new CPU(cpuInterface)  // Convert the binary code into opcodes const romBuffer = new RomBuffer(fileContents)  // Load the game cpu.load(romBuffer)  function cycle() {   cpu.step()    setTimeout(cycle, 3) }  cycle()

和政,享誉西北的陇上绿色明珠地名,是鲜活且广泛的文化符号,蕴含着丰富的历史文化信息,承载着人们的情感传承。一个长期形成的地名,是一个地方独具特色的符号。甘肃的地名,既有彰显刀光剑影中的武功军威,也回荡着大漠孤秦始皇每天都吃什么?你吃的或许要比他好秦始皇作为我国的始皇帝,一生有功有过,但这并不影响他被称为千古一帝,那你知道作为千古一帝的秦始皇每天都吃什么吗?或许,你吃的东西远远要比他吃得好!首先在秦朝不存在一日三餐这个说法,这是一组让人愤怒的老照片1900年八国联军在北京犯下了滔天罪行1900年,八国联军以镇压义和团为由,组织兵力近5万人,声势浩荡地入侵了大清帝国。同年8月14日北京城彻底沦陷,八国联军在北京城内恶贯满盈犯下了不可饶恕的滔天罪行。下面一组照片是由一个断臂,一个失明,河北农民搭伙种树15年,将荒滩变成绿洲毛乌素沙漠位于我国陕西省榆林市一线之北,毛乌素在蒙语中的意思是坏水,指的是沙漠之中缺乏水源,到处都是半沙丘,而榆林市也因为紧靠沙漠被称之为驼城。其实在5世纪的时候,毛乌素沙漠附近的他是古代十大美男子之一,最后却无辜被杀提到古代美男子,大家都能想到谁呢,潘安宋玉兰陵王等这些都是比较有代表性的美男子,韩子高也是古代十大美男子之一。他出生在晋朝,历史上记载的韩子高容貌惊艳皮肤白皙,宛如一个女子一般,这朝鲜战争金化攻势,联军向中国军队发起的最猛烈的一次进攻追授称号1953年4月8日追授他一级英雄的称号,朝鲜民主主义人民共和国最高人民会议常任委员会追授他朝鲜民主主义人民共和国英雄称号和勋章。用身体连接电话线在朝鲜战争的摊牌作战计划实施1950年,解放军接管被遗忘4年的边境哨所,竟发现8名国民党士兵在阅读此文之前,麻烦您点击一下关注,既方便您进行讨论和分享,又能给您带来不一样的参与感,感谢您的支持我国历史上版图面积发生过多次变化,每换一次朝代就意味着一次政权的交替,不同朝代的研究发现格陵兰岛冰川消融速度比预想快数倍或加速海平面上升据阿根廷布宜诺斯艾利斯经济新闻网29日报道,近20年来,由于冰面进一步融化和排入海洋的冰块增加,格陵兰岛冰盖的消融正在加速。现在,一项新研究已经证实,冰盖的消融速度比以前想象的快得滑雪雪地摩托狗拉雪橇湖南首家高山户外滑雪公园下月投用景区效果图。华声在线11月29日讯户外滑雪娱雪看冰雕展今日,记者从张家界七星山旅游度假区获悉,湖南首家高山户外滑雪戏雪公园七星山冰雪世界将于12月建成并投入使用。届时,游客和市民在人在旅途走进洪洞图文弘扬十月,一趟南下的列车,把我从迁安带到了洪洞。很早以前就知道有洪洞这么一个地方,小时候经常会听老人们讲,我们这一带的人大多是从山西洪洞大槐树下迁移过来的。洪洞是一个什么样的地哈密终于迎来了冬天的第一场雪11月27日上午哈密迎来了冬天的第一场雪,气温也骤降了10左右,多加衣物避免感冒。给大家介绍一下哈密吧哈密,古称伊州,是新疆维吾尔自治区辖地级市,地处新疆东部,是新疆通向内地的要道
每天两个核桃,能降低慢性炎症心血管患病风险?研究告诉你答案核桃在中国已经存在了上千年,其含有多种微量元素以及矿物质。自古以来,我们中国就流传着一种说法吃什么补什么的说法,核桃的果仁外表非常像我们的大脑,所以大家普遍认为吃核桃能补脑。63岁特写为了每一位危重症患者中央纪委国家监委网站张驰2022年12月28日早上7时30分,首都医科大学附属北京地坛医院纪检办公室主任李春霞如往常般准时到达办公室,翻阅起医院数据专班提供的新冠患者救治数据。数据高血压,多因瘀堵!中医5种方法,化开瘀堵疏通经脉降下血压全世界十几亿高血压病人,现代医学说,这个病只能吃药控制,终身吃药。而中医说高血压是可以调理好的,今天我们来聊聊这个话题。首先,我们要明白一个最基本的道理所有的控制终将失效。血压高只好时黑巧克力被曝铅镉含量超标,在美国被起诉索赔500万美元据路透社29日报道,美国一名消费者对知名巧克力品牌好时公司提起诉讼,称该公司销售的黑巧克力铅和镉的含量超标。克里斯托弗拉扎扎罗(ChristopherLazazzaro)是纽约拿骚卫健委发布恶性肿瘤患者膳食指导本标准参照GBT1。12009给出的规则起草。本标准起草单位中国医学科学院肿瘤医院苏州大学附属第一医院中国人民解放军总医院浙江大学医学院附属第二医院中国疾病预防控制中心营养与健康所男人过了50岁眉毛变长,意味着什么?越长,说明长寿可能越大吗?男人过了50岁后,身体各项机能会衰退,很多事情做起来都会有点力不从心,但有一个部位却没有衰退反而越来越长,就是眉毛。中国人喜欢将长眉和长寿联系在一起,也有老话讲两只眉毛长,胜过万担小柴胡汤丸不仅疏肝理气,搭配好还可燥湿发散风寒肝脾同调说起小柴胡汤丸,估计很多人都知道他,我们都知道小柴胡丸可以疏肝理气,对于因肝气郁结引起的,胸肋胀痛,头晕目眩,心烦喜呕,咽喉有异物感,有很好的改善效果。但是你知道吗,小柴胡汤丸,如耳聋耳鸣?三味药泡水,行气活血通利耳窍!得了耳鸣怎么办?今天和医生教你用三味药,解除耳鸣,我们之前总是认为,耳聋耳鸣是上了年纪的人才会有,但是现在很多年轻人,才三十来岁,未老耳先鸣,很多人得神经性耳鸣有好几年了,怎么样都古人云黎明即起,日暮则息早睡早起有助于胆汁代谢肝脏排毒肺气充盛促进排便等。中医将每天的十二时辰对应人体的十二经络称为子午流注,主张23点之前睡觉,早上7点之前起床,养成早睡早起的良好作息习惯,有利于经脉运早晨起来后喝一杯温水等于喝细菌?医生很多人都误解了很多人在早上起床之后,都会选择先洗漱再去进食或者喝水。因为他们大多觉得自己的嘴巴经过了一夜的时间,里面会充满不干净的东西。他们觉得在这种情况下不洗漱,自己是吃不下东西的。有的人甚至阳康后又复阳,到底怎么回事?这2件事不能做,必须知道随着疫情的放开,越来越多的人都阳了,反正我身边很多的朋友都已经阳了,既然已经到达了这个阶段,大家只能保持良好心态,积极应对,也有不少朋友已经恢复了,但是也有的人转阴之后又复阳,这到