diff --git a/assembler.js b/assembler.js index 4d6fe53..05d7d0d 100644 --- a/assembler.js +++ b/assembler.js @@ -13,7 +13,7 @@ exports.assemble = (str, debug = false) => { return decodeInstructions(str); } -const mnemonicsWithOptionalArgs = ['end', 'cfc', 'chp', 'nop']; +const mnemonicsWithOptionalArgs = ['end', 'nop']; const mnemonics2opcodes = { end: { direct: 0, indirect: 0 }, sto: { direct: 1, indirect: 2 }, @@ -22,8 +22,8 @@ const mnemonics2opcodes = { sub: { direct: 7, indirect: 8 }, hop: { direct: 9, indirect: 10 }, jmp: { direct: 11, indirect: 12 }, - cfc: { direct: 13, indirect: 13 }, - chp: { direct: 14, indirect: 14 }, + ftg: { direct: 13, indirect: 13 }, + fhp: { direct: 14, indirect: 14 }, nop: { direct: 15, indirect: 15 }, }; diff --git a/logging.js b/logging.js index 29c33f9..9922e61 100644 --- a/logging.js +++ b/logging.js @@ -50,17 +50,34 @@ const num2hex = (num) => num.toString(16).toUpperCase().padStart(2, "0"); const hex2num = (hex) => parseInt(hex, 16); /** - * Convert a number to binary + * Convert a number to binary, padded to 8 bits * See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript * @param {number} num * @returns {string} binary representation of the input **/ const num2bin = (num) => (num >>> 0).toString(2).padStart(8, "0"); +/** + * Convert a number to binary, padded to 4 bits + * See here for an explanation: https://stackoverflow.com/questions/9939760/how-do-i-convert-an-integer-to-binary-in-javascript + * @param {number} num + * @returns {string} binary representation of the input + **/ +const num2bin_4bit = (num) => (num >>> 0).toString(2).padStart(4, "0"); + +/** + * @param {string} bin + * @returns {number} + */ +const bin2num = (bin) => parseInt(bin, 2) + + module.exports = { "logMemory": logMemory, "logRunningHeader": logRunningHeader, "num2hex": num2hex, "hex2num": hex2num, "num2bin": num2bin, + "num2bin_4bit": num2bin_4bit, + "bin2num": bin2num, } \ No newline at end of file diff --git a/notes/todo.md b/notes/todo.md index c421eb9..729f756 100644 --- a/notes/todo.md +++ b/notes/todo.md @@ -11,8 +11,9 @@ - Steve Losh: https://stevelosh.com/blog/2016/12/chip8-input/ - https://tonisagrista.com/blog/2021/chip8-spec/ -## Misc +## Documentation +- [ ] Improve docs for flags register - [ ] Play with JSDoc ## Design diff --git a/readme.md b/readme.md index ebc31e0..20c9b12 100644 --- a/readme.md +++ b/readme.md @@ -36,12 +36,12 @@ With verbose debugging output: 06 ADD addr ; add ... A <- A + mem[addr] ... and un/set carry flag 07 SUB lit# ; sub ... A <- A - lit# ... and un/set carry flag 08 SUB addr ; sub ... A <- A - mem[addr] ... and un/set carry flag - 09 HOP lit# ; hop ... skip next instruction if A == lit# ... when true: IP <- PC + 2 - 0A HOP addr ; hop ... skip next instruction if A == addr ... when true: IP <- PC + 2 + 09 HOP lit# ; hop ... skip next instruction if A == lit# ... when true: IP <- PC + 4 + 0A HOP addr ; hop ... skip next instruction if A == addr ... when true: IP <- PC + 4 0B JMP lit# ; jump ... IP <- lit# 0C JMP addr ; jump ... IP <- addr - 0D CCF ———— ; clear Carry Flag ... CF = 0 - 0E CHP ———— ; carry hop ... skip next instruction if Carry Flag is set ... when true: IP <- PC + 2 + 0D FTG lit# ; toggle flag by number (see details below) + 0E FHP lit# ; flag hop ... skip next instruction if flag is set ... when true: IP <- PC + 4 0F NOP ———— ; no operation - Instructions are two bytes long: @@ -52,7 +52,13 @@ With verbose debugging output: - `A` - accumulator - `IP` - instruction pointer (aka program counter) -- `CF` - carry flag +- `FLAGS` - flags: **N**egative, **Z**ero, **O**verflow, **C**arry + - in machine language, each flag is given a number: + - N = 3 + Z = 2 + O = 1 + C = 0 + - (bitwise, `0000 = NZOC`) ## Memory map / Peripherals @@ -71,8 +77,8 @@ With verbose debugging output: ADD $FF ; this is direct addressing ADD ($CC) ; this is indirect addressing - END ; END, CFC, and CHP don't require operands - ; (a default value of 0 will be used as their operand) + END ; END and NOP don't require operands + ; (the assembler will fill in a default value of 0) @subroutine ; create a label ADD $01 ; (it must be on the line before the code it names) diff --git a/simulator.js b/simulator.js index 17945c2..1cde7c5 100644 --- a/simulator.js +++ b/simulator.js @@ -1,26 +1,41 @@ const { INITIAL_IP_ADDRESS, CYCLE_LIMIT } = require('./machine.config'); -const { num2hex } = require('./logging.js'); +const { num2hex, num2bin_4bit } = require('./logging.js'); const display = require('./display.js'); +// TODO: +// implement setting and clearing of: +// - [ ] overflow flag +// - [ ] zero flag +// - [ ] negative flag + // STATE const CPU = { running: false, IP: INITIAL_IP_ADDRESS, - CF: 0, + FLAGS: 0, // A bit field! 0000 = NZOC Acc: 0, memory: null, + currentInstruction: { + opcode: null, + operand: null, + mnemonic: null, + }, + /** @param {Uint8Array} data */ loadMemory: (data) => { if (data.length > 256) { throw new Error("Out of memory error (program too long)"); } CPU.memory = data; }, - currentInstruction: { - opcode: null, - operand: null, - mnemonic: null, - } + setFlagNegative: () => { CPU.FLAGS |= 8 }, + setFlagZero: () => { CPU.FLAGS |= 4 }, + setFlagOverflow: () => { CPU.FLAGS |= 2 }, + setFlagCarry: () => { CPU.FLAGS |= 1 }, + unsetFlagNegative: () => { CPU.FLAGS &= ~8 }, + unsetFlagZero: () => { CPU.FLAGS &= ~4 }, + unsetFlagOverflow: () => { CPU.FLAGS &= ~2 }, + unsetFlagCarry: () => { CPU.FLAGS &= ~1 }, } @@ -51,7 +66,7 @@ const Instructions = { }, load_addr: (addr) => { - CPU.currentInstruction.mnemonic = `LDA addr \n mem at addr: ${num2hex(CPU.memory[addr])}`; + CPU.currentInstruction.mnemonic = `LDA addr; @ addr: ${num2hex(CPU.memory[addr])}`; CPU.Acc = CPU.memory[addr]; CPU.IP = CPU.IP += 2; }, @@ -59,11 +74,12 @@ const Instructions = { add_lit: (lit) => { CPU.currentInstruction.mnemonic = 'ADD lit'; let sum = CPU.Acc + lit; + // TODO: implement NZO flags if (sum > 255) { - CPU.CF = 1; + CPU.setFlagCarry(); CPU.Acc = (sum % 255) - 1; } else { - CPU.CF = 0; + CPU.unsetFlagCarry(); CPU.Acc = sum; } CPU.IP = CPU.IP += 2; @@ -72,11 +88,12 @@ const Instructions = { add_addr: (addr) => { CPU.currentInstruction.mnemonic = 'ADD addr'; let sum = CPU.Acc + CPU.memory[addr]; + // TODO: implement NZO flags if (sum > 255) { - CPU.CF = 1; + CPU.setFlagCarry(); CPU.Acc = (sum % 255) - 1; } else { - CPU.CF = 0; + CPU.unsetFlagCarry(); CPU.Acc = sum; } CPU.IP = CPU.IP += 2; @@ -85,11 +102,12 @@ const Instructions = { sub_lit: (lit) => { CPU.currentInstruction.mnemonic = 'SUB lit'; let sum = CPU.Acc - lit; + // TODO: implement NZO flags if (sum < 0) { - CPU.CF = 1; + CPU.setFlagCarry(); CPU.Acc = 255 + sum + 1; } else { - CPU.CF = 0; + CPU.unsetFlagCarry(); CPU.Acc = sum; } CPU.IP = CPU.IP += 2; @@ -98,18 +116,19 @@ const Instructions = { sub_addr: (addr) => { CPU.currentInstruction.mnemonic = 'SUB addr'; let sum = CPU.Acc - CPU.memory[addr]; + // TODO: implement NZO flags if (sum < 0) { - CPU.CF = 1; + CPU.setFlagCarry(); CPU.Acc = 255 + sum + 1; } else { - CPU.CF = 0; + CPU.unsetFlagCarry(); CPU.Acc = sum; } CPU.IP = CPU.IP += 2; }, hop_lit: (lit) => { - CPU.currentInstruction.mnemonic = `HOP lit \n ↳ Memory at IP+2 and +3: ${CPU.memory[CPU.IP+2]}, ${CPU.memory[CPU.IP+3]}`; + CPU.currentInstruction.mnemonic = `HOP lit; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`; if (CPU.Acc === lit) { CPU.IP += 4; } else { @@ -136,15 +155,30 @@ const Instructions = { CPU.IP = CPU.memory[addr]; }, - carry_clear: () => { - CPU.currentInstruction.mnemonic = 'CFC'; - CPU.CF = 0; + flag_toggle: (flagNum) => { + CPU.currentInstruction.mnemonic = 'FTG'; + let mask = null; + if (flagNum === 0) { mask = 1; } + if (flagNum === 1) { mask = 2; } + if (flagNum === 2) { mask = 4; } + if (flagNum === 3) { mask = 8; } + if (mask === null) { throw new Error('Invalid flag number'); } + console.log('flag mask:', mask); + CPU.FLAGS = CPU.FLAGS ^= mask; CPU.IP += 2; }, - carry_hop: () => { - CPU.currentInstruction.mnemonic = `CHP \n ↳ Memory at IP+2 and +3: ${CPU.memory[CPU.IP+2]}, ${CPU.memory[CPU.IP+3]}`; - if (CPU.CF != 0) { + flag_hop: (flagNum) => { + CPU.currentInstruction.mnemonic = `FHP; IP+2: ${CPU.memory[CPU.IP+2]}, IP+3: ${CPU.memory[CPU.IP+3]}`; + console.log('flag number:', flagNum); + let mask = null; + if (flagNum === 0) { mask = 1; } + if (flagNum === 1) { mask = 2; } + if (flagNum === 2) { mask = 4; } + if (flagNum === 3) { mask = 8; } + if (mask === null) { throw new Error('Invalid flag number'); } + console.log('flag mask:', mask); + if (CPU.FLAGS & mask) { CPU.IP += 4; } else { CPU.IP += 2; @@ -171,8 +205,8 @@ const opcodes2mnemonics = { 10: (operand) => Instructions.hop_addr(operand), 11: (operand) => Instructions.jump_lit(operand), 12: (operand) => Instructions.jump_addr(operand), - 13: (operand) => Instructions.carry_clear(), - 14: (operand) => Instructions.carry_hop(), + 13: (operand) => Instructions.flag_toggle(operand), + 14: (operand) => Instructions.flag_hop(operand), 15: (operand) => Instructions.no_op(), }; @@ -230,7 +264,7 @@ async function logCPUState(debug = false) { console.log('Mnemonic:', CPU.currentInstruction.mnemonic); console.log(`Machine: $${num2hex(CPU.currentInstruction.opcode)} $${num2hex(CPU.currentInstruction.operand)}`); console.log(); - console.log(`IP: $${num2hex(CPU.IP)} Acc: $${num2hex(CPU.Acc)} CF: ${CPU.CF}  ${CPU.running ? "running" : "halted" }`); + console.log(`IP: $${num2hex(CPU.IP)} Acc: $${num2hex(CPU.Acc)} FLAGS: ${num2bin_4bit(CPU.FLAGS)}  ${CPU.running ? "running" : "halted" }`); console.log(); // Pause to show animated display: if (!debug) await new Promise(resolve => setTimeout(resolve, 75)); diff --git a/test-programs/chp.asm b/test-programs/chp.asm deleted file mode 100644 index e61d00c..0000000 --- a/test-programs/chp.asm +++ /dev/null @@ -1,11 +0,0 @@ -;; test CHP instruction - -ADD 8 ; Acc = 8 -CHP 0 ; shouldn't hop (CF = 0) -ADD 1 ; Acc = 9 -ADD 8 ; Acc = 1 and CF = 1 -CHP 0 ; hop! (CF = 1) -END -SUB 1 ; Acc = 0 -CFC 0 ; CF = 0 -END \ No newline at end of file diff --git a/test-programs/fill-display.asm b/test-programs/fill-display.asm index 3b1d935..aa09e53 100644 --- a/test-programs/fill-display.asm +++ b/test-programs/fill-display.asm @@ -17,7 +17,7 @@ ADD $01 STO $21 ; if CF is set, then the display is full and we're done -CHP +FHP 0 JMP @copy-to-display END \ No newline at end of file diff --git a/test-programs/flag-carry--fhp-ftg.asm b/test-programs/flag-carry--fhp-ftg.asm new file mode 100644 index 0000000..cee29d2 --- /dev/null +++ b/test-programs/flag-carry--fhp-ftg.asm @@ -0,0 +1,10 @@ +;; test FHP and FTG for carry +ADD $FF ; Acc = $FF +FHP 0 ; shouldn't hop (CF = 0) + ADD 2 ; Acc = $01 and CF = 1 +FHP 0 ; hop! (CF = 1) + END +SUB 1 ; Acc = $00, CF = 0 +SUB 1 ; Acc = $FF, CF = 1 +FTG 0 ; CF = 0 +END \ No newline at end of file