/** * Copyright 2020 wixette@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @fileoverview Altair 8800 front panel simulator. */ /** * The simulator. */ class Sim8800 { /** * @param {number} memSize The memory size, in bytes. * @param {number} clockRate The clock rate. * @param {function(Array)?} setAddressLedsCallback The * callback to set address LEDs. * @param {function(Array)?} setDataLedsCallback The * callback to set data LEDs. * @param {function(boolean)?} setWaitLedCallback The callback to * set the WAIT LED. * @param {function(boolean)?} setStatusLedsCallback The callback to * set the STATUS LEDs. * @param {function():number?} getInputAddressCallback The * callback to get the input word from address/data switches. * @param {function(string)?} dumpCpuCallback The callback to receive * CPU status dump, in HTML string. * @param {function(string)?} dumpMemCallback The callback to receive * memory contents dump, in HTML string. */ constructor(memSize, clockRate, setAddressLedsCallback, setDataLedsCallback, setWaitLedCallback, setStatusLedsCallback, getInputAddressCallback, dumpCpuCallback, dumpMemCallback) { this.clockRate = clockRate; this.mem = new Array(memSize); this.setAddressLedsCallback = setAddressLedsCallback; this.setDataLedsCallback = setDataLedsCallback; this.setWaitLedCallback = setWaitLedCallback; this.setStatusLedsCallback = setStatusLedsCallback; this.getInputAddressCallback = getInputAddressCallback; this.dumpCpuCallback = dumpCpuCallback; this.dumpMemCallback = dumpMemCallback; this.isPoweredOn = false; this.isRunning = false; this.lastAddress = 0; this.initMem(); CPU8080.init(this.getWriteByteCallback(), this.getReadByteCallback(), null, /* not used. */ this.getWritePortCallback(), this.getReadPortCallback()); } /** * Formats a number to fixed length hex string. * @param {number} n The number to be formatted. * @param {number} len The output length, with leading zeros. */ static toHex(n, len) { var leadingZeros = (new Array(len)).fill('0').join(''); return (leadingZeros + n.toString(16)).substr(-len); } /** * Parses a number into an array of binary bits. * @param {number} data The data to be parsed. * @param {number} numBits Number of bits to be parsed. * @return {Array} Sequence of 0 or 1, from the lowest bit to * the highest bit. */ static parseBits(data, numBits) { var bits = []; for (let i = 0; i < numBits; i++) { bits.push(data & 1 != 0 ? 1 : 0); data >>>= 1; } return bits; } /** * Fills the memory with dummy bytes. */ initMem(random = false) { if (random) { for (let i = 0; i < this.mem.length; i++) { this.mem[i] = Math.floor(Math.random() * 256); } } else { this.mem.fill(0); } } /** * Loads data into memory. * @param {number} address The start address to load the data/program. * @param {Array} data The array of data. */ loadData(address, data) { if (!this.isPoweredOn) return; for (let i = 0; i < data.length && address < this.mem.length; i++) { this.mem[address++] = data[i]; } this.dumpMem(); } /** * Loads data into memory. * @param {number} address The start address to load the data/program. * @param {string} hexString Data encoded in hex string, like 'c3 00 00'. */ loadDataAsHexString(address, hexString) { if (!this.isPoweredOn || !hexString) return; var data = hexString.split(' '); for (let i = 0; i < data.length && address < this.mem.length; i++) { var byte = parseInt('0x' + data[i]); if (!isNaN(byte)) { this.mem[address++] = byte; } } this.dumpMem(); } /** * Dumps the memory to HTML, for debugging or monitoring. */ dumpMem() { if (this.dumpMemCallback) { var sb = ['
\n'];
            for (let i = 0; i < this.mem.length; i += 16) {
                sb.push(Sim8800.toHex(i, 4));
                sb.push('  ');
                for (let j = i;
                     j < Math.min(this.mem.length, i + 16);
                     j++) {
                    sb.push(Sim8800.toHex(this.mem[j], 2));
                    sb.push((j + 1) % 8 == 0 ? '  ' : ' ');
                }
                sb.push('\n');
            }
            sb.push('
\n'); this.dumpMemCallback(sb.join('')); } } /** * Decodes the FLAGs register. * @param {number} flags The value of the FLAGs register. * @return {Object} The decoded flags. */ decodeFlags(flags) { var ret = {}; ret.sign = flags & 0x80 != 0; ret.zero = flags & 0x40 != 0; ret.auxiliaryCarry = flags & 0x10 != 0; ret.parity = flags & 0x04 != 0; ret.carry = flags & 0x01 != 0; return ret; } /** * Dumps the internal CPU status to HTML, for debugging or mornitoring. */ dumpCpu() { if (this.dumpCpuCallback) { var cpu = CPU8080.status(); var sb = ['
\n'];
            sb.push('PC = ' + Sim8800.toHex(cpu.pc, 4) + '  ');
            sb.push('SP = ' + Sim8800.toHex(cpu.sp, 4) + '\n');
            sb.push('A = ' + Sim8800.toHex(cpu.a, 2) + '  ');
            sb.push('B = ' + Sim8800.toHex(cpu.b, 2) + '  ');
            sb.push('C = ' + Sim8800.toHex(cpu.c, 2) + '  ');
            sb.push('D = ' + Sim8800.toHex(cpu.d, 2) + '\n');
            sb.push('E = ' + Sim8800.toHex(cpu.e, 2) + '  ');
            sb.push('F = ' + Sim8800.toHex(cpu.f, 2) + '  ');
            sb.push('H = ' + Sim8800.toHex(cpu.h, 2) + '  ');
            sb.push('L = ' + Sim8800.toHex(cpu.l, 2) + '\n');
            var flags = this.decodeFlags(cpu.f);
            sb.push('FLAGS: ');
            if (flags.sign) sb.push('SIGN ');
            if (flags.zero) sb.push('ZERO ');
            if (flags.auxiliaryCarry) sb.push('AC ');
            if (flags.parity) sb.push('PARITY ');
            if (flags.carry) sb.push('CARRY ');
            sb.push('
\n'); this.dumpCpuCallback(sb.join('')); } } /** * Returns the byteTo (write memory) callback. * @return {function(number, number)} */ getWriteByteCallback() { var self = this; return function(address, value) { address = address % self.mem.length; self.mem[address] = value; }; } /** * Returns the byteAt (read memory) callback. * @return {function(number): number} */ getReadByteCallback() { var self = this; return function(address) { address = address % self.mem.length; var value = self.mem[address]; return value; }; } /** * Returns the porto (write port) callback. * @return {function(number, number)} */ getWritePortCallback() { var self = this; return function(address, value) { if (address == 0xff && self.setDataLedsCallback) { var bits = Sim8800.parseBits(value, 8); self.setDataLedsCallback(bits); } }; } /** * Returns the byteAt (read memory) callback. * @return {function(number): number} */ getReadPortCallback() { var self = this; return function(address) { var value = 0; // We only care about the port 0xff. if (address == 0xff && self.getInputAddressCallback) { var word = self.getInputAddressCallback(); return word >> 8; } return value; }; } /** * Gets the clock ticker callback. * @return {function()} */ getClockTickerCallback() { var self = this; return function(timestamp) { if (self.isRunning) { var cycles = self.clockRate / 1000; self.step(cycles); window.setTimeout(self.getClockTickerCallback(), 1); } }; } /** * Powers on the machine. */ powerOn() { this.isPoweredOn = true; this.initMem(); this.reset(); if (this.setStatusLedsCallback) { this.setStatusLedsCallback(true); } if (this.setWaitLedCallback) { this.setWaitLedCallback(false); } } /** * Powers off the machine. */ powerOff() { if (this.setStatusLedsCallback) { this.setStatusLedsCallback(false); } if (this.setWaitLedCallback) { this.setWaitLedCallback(true); } if (this.setAddressLedsCallback) { this.setAddressLedsCallback(new Array(16).fill(0)); } if (this.setDataLedsCallback) { this.setDataLedsCallback(new Array(8).fill(0)); } if (this.dumpCpuCallback) { this.dumpCpuCallback(''); } if (this.dumpMemCallback) { this.dumpMemCallback(''); } this.isPoweredOn = false; } /** * Resets the machine. */ reset() { if (!this.isPoweredOn) return; CPU8080.reset(); this.stop(); this.lastAddress = 0; if (this.setAddressLedsCallback) { this.setAddressLedsCallback(new Array(16).fill(1)); } if (this.setDataLedsCallback) { this.setDataLedsCallback(new Array(8).fill(1)); } this.dumpCpu(); this.dumpMem(); var self = this; window.setTimeout(function() { if (self.setAddressLedsCallback) { self.setAddressLedsCallback(new Array(16).fill(0)); } if (self.setDataLedsCallback) { self.setDataLedsCallback(new Array(8).fill(0)); } }, 400); } /** * Stops the CPU. */ stop() { if (!this.isPoweredOn) return; this.isRunning = false; if (this.setWaitLedCallback) { this.setWaitLedCallback(this.isRunning); } } /** * Starts the CPU. */ start() { if (!this.isPoweredOn) return; this.isRunning = true; if (this.setWaitLedCallback) { this.setWaitLedCallback(this.isRunning); } window.setTimeout(this.getClockTickerCallback(), 1); } /** * Runs a given number of CPU cycles. * @param {number} cycles The number of CPU cycles to step on. */ step(cycles) { if (!this.isPoweredOn) return; CPU8080.steps(cycles); this.dumpCpu(); this.dumpMem(); if (this.setAddressLedsCallback) { let cpu = CPU8080.status(); let bits = Sim8800.parseBits(cpu.pc, 16); this.setAddressLedsCallback(bits); } } /** * Shows the address and the byte at the address via LEDs. */ showAddressAndData() { if (this.setAddressLedsCallback) { let bits = Sim8800.parseBits(this.lastAddress, 16); this.setAddressLedsCallback(bits); } if (this.setDataLedsCallback) { let bits = Sim8800.parseBits(this.mem[this.lastAddress], 8); this.setDataLedsCallback(bits); } } /** * Reads a byte from the given address. */ examine() { if (!this.isPoweredOn) return; if (this.getInputAddressCallback) { var address = this.getInputAddressCallback(); this.lastAddress = address; this.showAddressAndData(); } } /** * Reads a byte from the next address. */ examineNext() { if (!this.isPoweredOn) return; this.lastAddress++; this.showAddressAndData(); } /** * Writes a byte to the given address. */ deposit() { if (!this.isPoweredOn) return; if (this.getInputAddressCallback) { // Only 8 bits of input is considered. var value = this.getInputAddressCallback() & 0xff; this.getWriteByteCallback()(this.lastAddress, value); this.showAddressAndData(); this.dumpMem(); } } /** * Writes a byte to the next address. */ depositNext() { if (!this.isPoweredOn) return; this.lastAddress++; this.deposit(); } };