/** * @fileoverview Implements the PCjs Bus component. * @author Jeff Parsons * @version 1.0 * @suppress {missingProperties} * Created 2012-Sep-04 * * Copyright © 2012-2014 Jeff Parsons * * This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines) * at and . * * PCjs is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCjs. If not, * see . * * You are required to include the above copyright notice in every source code file of every * copy or modified version of this work, and to display that copyright notice on every screen * that loads or runs any version of this software (see Computer.sCopyright). * * Some PCjs files also attempt to load external resource files, such as character-image files, * ROM files, and disk image files. Those external resource files are not considered part of the * PCjs program for purposes of the GNU General Public License, and the author does not claim * any copyright as to their contents. */ "use strict"; if (typeof module !== 'undefined') { var str = require("../../shared/lib/strlib"); var Component = require("../../shared/lib/component"); var Memory = require("./mem"); var State = require("./state"); } /** * Bus(cpu, dbg) * * The Bus component manages "physical" memory and I/O address spaces. * * The Bus component has no UI elements, so it does not require an init() handler, * but it still inherits from the Component class and must be allocated like any * other device component. It's currently allocated by the Computer's init() handler, * which then calls the initBus() method of all the other components. * * When initMemory() initializes the entire address space, it also passes aMemBlocks * to the CPU object, so that the CPU can perform all its own address-to-block and memory * block accesses directly. * * For memory beyond the simple needs of the ROM and RAM components (ie, memory-mapped * devices), the address space must still be allocated through the Bus component via * addMemory(). If the component needs something more than simple read/write storage, * it must provide a controller with getMemoryBuffer() and getMemoryAccess() methods. * * By contrast, all port access operations are defined by external handlers; they * register with us, and we manage those registrations, and we'll probably provide I/O * breakpoints at some point, but unlike memory accesses, we're not involved with I/O * accesses at all. * * @constructor * @extends Component * @param {Object} parmsBus * @param {X86CPU|Component} cpu * @param {Debugger|Component} dbg */ function Bus(parmsBus, cpu, dbg) { Component.call(this, "Bus", parmsBus, Bus); this.cpu = cpu; this.dbg = dbg; this.nBusWidth = parmsBus['buswidth'] || 20; /* * Compute all the Bus memory block addressing values that we rely on, based on the width of the bus. * * Regarding this.blockTotal, we want to avoid address-overflow-detection expressions like: * * iBlock < this.blockTotal? iBlock : 0 * * and as long as we know that this.blockTotal is a power-of-two (eg, 256 or 0x100, in the case * of nBusWidth == 20), we can define this.blockMask as (this.blockTotal - 1) and rewrite the previous * expression as: * * iBlock & this.blockMask * * While we *could* say that we mask addresses with this.addrLimit to simulate "A20 wrap", the simple * fact is it relieves us from bounds-checking every aMemBlocks index. Address wrapping at the 1Mb * boundary (ie, the A20 address line) is something we'll have to deal with more carefully on the 80286. * * New property Old property Old hard-coded values (when nBusWidth was always 20) * ------------ ------------ ---------------------------------------------------- * this.addrLimit Bus.ADDR.LIMIT 0xfffff * this.addrMask N/A N/A * this.blockSize Bus.BLOCK.SIZE 4096 * this.blockLen Bus.BLOCK.LEN (this.blockSize >> 2) * this.blockShift Bus.BLOCK.SHIFT 12 * this.blockLimit Bus.BLOCK.LIMIT 0xfff * this.blockTotal Bus.BLOCK.TOTAL ((this.addrLimit + this.blockSize) / this.blockSize) | 0 * this.blockMask Bus.BLOCK.MASK (this.blockTotal - 1) (ie, 0xff) * * Note that the blockShift calculation below chooses a 4Kb physical memory block size for a 20-bit bus * (1Mb address space) and a 16Kb physical memory block for a 24-bit bus (16Mb address space). This yields * a 256-block array for the smaller bus and a 1024-block array for the larger bus. If we left the block * size at 4Kb in all cases, we'd end up with a 4096-block array for an 80286, which seems a bit excessive. * * I can't think of any reason why a coarser block granularity (of 16Kb) should hurt anything, other than * wasting a little memory for ROMs smaller than the block size. Realize that this is strictly a physical * memory implementation detail, which should have no bearing on segment or page granularity of any future * virtual memory implementation. */ this.addrLimit = this.addrMask = (1 << this.nBusWidth) - 1; this.blockShift = (this.nBusWidth <= 20? 12 : 14); this.blockSize = 1 << this.blockShift; this.blockLen = this.blockSize >> 2; this.blockLimit = this.blockSize - 1; this.blockTotal = ((this.addrLimit + this.blockSize) / this.blockSize) | 0; this.blockMask = this.blockTotal - 1; /* * Lists of I/O notification functions: aPortInputNotify and aPortOutputNotify are arrays, indexed by * port, of sub-arrays which contain: * * [0]: registered component * [1]: registered function to call for every I/O access * * The registered function is called with the port address, and if the access was triggered by the CPU, * the physical address (EIP) that the access occurred from. * * WARNING: Unlike the (old) read and write memory notification functions, these support only one * pair of input/output functions per port. A more sophisticated architecture could support a list * of chained functions across multiple components, but I doubt that will be necessary here. * * UPDATE: The Debugger now piggy-backs on these arrays to indicate ports for which it wants notification * of I/O. In those cases, the registered component/function elements may or may not be set, but the following * additional element will be set: * * [2]: true to break on I/O, false to ignore I/O * * The false case is important if fPortInputBreakAll and/or fPortOutputBreakAll is set, because it allows the * Debugger to selectively ignore specific ports. */ this.aPortInputNotify = []; this.aPortOutputNotify = []; this.fPortInputBreakAll = this.fPortOutputBreakAll = false; /* * Allocate empty Memory blocks to span the entire physical address space. */ this.initMemory(); this.setReady(); } Component.subclass(Component, Bus); /** * initMemory() * * Allocate enough (empty) Memory blocks to span the entire physical address space. * * @this {Bus} */ Bus.prototype.initMemory = function() { this.aMemBlocks = new Array(this.blockTotal); for (var iBlock = 0; iBlock < this.blockTotal; iBlock++) { var addr = iBlock * this.blockSize; var block = this.aMemBlocks[iBlock] = new Memory(addr); if (DEBUGGER) block.setDebugInfo(this.cpu, this.dbg, addr, this.blockSize); } this.cpu.initMemory(this.aMemBlocks, this.addrLimit, this.blockShift, this.blockLimit, this.blockMask); this.cpu.setAddressMask(this.addrMask); }; /** * reset() * * @this {Bus} */ Bus.prototype.reset = function() { this.setA20(true); }; /** * addMemory(addr, size, fReadOnly, controller) * * Adds new Memory blocks to the specified address range. Any Memory blocks previously * added to that range must first be removed via removeMemory(); otherwise, you'll get * an allocation conflict error. Moreover, the address range must start at a block-granular * address and span exactly one or more blocks; otherwise, you'll get a memory range error. * * These restrictions help prevent address calculation errors, redundant allocations, etc. * * @this {Bus} * @param {number} addr is the starting physical address of the memory address range * @param {number} size of the length in bytes of the range; must be a multiple of BLOCK_SIZE * @param {boolean} [fReadOnly] is true if the memory must be read-only; default is read-write * @param {Object} [controller] is an optional memory controller component * @return {boolean} true if successful, false if not */ Bus.prototype.addMemory = function(addr, size, fReadOnly, controller) { if (!(addr & this.blockLimit) && size && !(size & this.blockLimit)) { var iBlock = addr >> this.blockShift; while (size > 0 && iBlock < this.aMemBlocks.length) { var block = this.aMemBlocks[iBlock]; if (block !== undefined && block.size) { return this.reportError(1, addr, size); } addr = iBlock * this.blockSize; block = this.aMemBlocks[iBlock++] = new Memory(addr, this.blockSize, fReadOnly, controller); if (DEBUGGER) block.setDebugInfo(this.cpu, this.dbg, addr, this.blockSize); size -= this.blockSize; } return true; } return this.reportError(2, addr, size); }; /** * cleanMemory(addr, size) * * @this {Bus} * @param {number} addr * @param {number} size * @return {boolean} true if all blocks were clean, false if dirty; all blocks are cleaned in the process */ Bus.prototype.cleanMemory = function(addr, size) { var fClean = true; var iBlock = addr >> this.blockShift; while (size > 0 && iBlock < this.aMemBlocks.length) { if (this.aMemBlocks[iBlock].fDirty) { this.aMemBlocks[iBlock].fDirty = fClean = false; this.aMemBlocks[iBlock].fDirtyEver = true; } size -= this.blockSize; iBlock++; } return fClean; }; /** * getA20() * * @this {Bus} * @return {boolean} true if enabled, false if disabled */ Bus.prototype.getA20 = function() { return this.addrLimit == this.addrMask; }; /** * setA20(fEnable) * * @this {Bus} * @param {boolean} fEnable is true to enable A20 (default), false to disable */ Bus.prototype.setA20 = function(fEnable) { Component.assert(fEnable !== undefined); if (fEnable !== undefined) { if (this.nBusWidth > 20) { var addrMask = (this.addrMask & ~0x100000) | (fEnable? 0x100000 : 0); if (addrMask != this.addrMask) { this.addrMask = addrMask; /* * This callback is required only because the CPU "insists" on using its own memory access functions. */ if (this.cpu) this.cpu.setAddressMask(addrMask); } } } }; /** * setMemoryAccess(addr, size) * * Updates the access functions in every block of the specified address range. Since the only components * that should be dynamically modifying the memory access functions are those that use addMemory() with a custom * memory controller, we require that the block(s) being updated do in fact have a controller. * * @this {Bus} * @param {number} addr * @param {number} size * @param {Array.} [afn] * @return {boolean} true if successful, false if not */ Bus.prototype.setMemoryAccess = function(addr, size, afn) { if (!(addr & this.blockLimit) && size && !(size & this.blockLimit)) { var iBlock = addr >> this.blockShift; while (size > 0) { var block = this.aMemBlocks[iBlock]; if (!block.controller) { return this.reportError(5, addr, size); } block.setAccess(afn); size -= this.blockSize; iBlock++; } return true; } return this.reportError(3, addr, size); }; /** * removeMemory(addr, size) * * Replaces every block in the specified address range with empty Memory blocks that will ignore all reads/writes. * * @this {Bus} * @param {number} addr * @param {number} size * @return {boolean} true if successful, false if not */ Bus.prototype.removeMemory = function(addr, size) { if (!(addr & this.blockLimit) && size && !(size & this.blockLimit)) { var iBlock = addr >> this.blockShift; while (size > 0) { addr = iBlock * this.blockSize; var block = this.aMemBlocks[iBlock++] = new Memory(addr); if (DEBUGGER) block.setDebugInfo(this.cpu, this.dbg, addr, this.blockSize); size -= this.blockSize; } return true; } return this.reportError(4, addr, size); }; /** * getByteDirect(addr) * * @this {Bus} * @param {number} addr is a physical (non-segmented) address * @return {number} byte (8-bit) value at that address */ Bus.prototype.getByteDirect = function(addr) { return this.aMemBlocks[(addr & this.addrMask) >> this.blockShift].readByteDirect(addr & this.blockLimit); }; /** * getWordDirect(addr) * * @this {Bus} * @param {number} addr is a physical (non-segmented) address * @return {number} word (16-bit) value at that address */ Bus.prototype.getWordDirect = function(addr) { var off = addr & this.blockLimit; var iBlock = (addr & this.addrMask) >> this.blockShift; if (off != this.blockLimit) { return this.aMemBlocks[iBlock].readWordDirect(off); } return this.aMemBlocks[iBlock++].readByteDirect(off) | (this.aMemBlocks[iBlock & this.blockMask].readByteDirect(0) << 8); }; /** * setByteDirect(addr, b) * * @this {Bus} * @param {number} addr is a physical (non-segmented) address * @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe) */ Bus.prototype.setByteDirect = function(addr, b) { this.aMemBlocks[(addr & this.addrMask) >> this.blockShift].writeByteDirect(addr & this.blockLimit, b & 0xff); }; /** * setWordDirect(addr, w) * * @this {Bus} * @param {number} addr is a physical (non-segmented) address * @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe) */ Bus.prototype.setWordDirect = function(addr, w) { var off = addr & this.blockLimit; var iBlock = (addr & this.addrMask) >> this.blockShift; if (off != this.blockLimit) { this.aMemBlocks[iBlock].writeWordDirect(off, w & 0xffff); return; } this.aMemBlocks[iBlock++].writeByteDirect(off, w & 0xff); this.aMemBlocks[iBlock & this.blockMask].writeByteDirect(0, (w >> 8) & 0xff); }; /** * saveMemory() * * The only memory blocks we save are those marked as dirty; most likely all of RAM will have been marked dirty, * and even if our dirty-memory flags were as smart as our dirty-sector flags (ie, were set only when a write changed * what was already there), it's unlikely that would reduce the number of RAM blocks we must save/restore. At least * all the ROM blocks should be clean (except in the unlikely event that the Debugger was used to modify them). * * All dirty blocks will be stored in a single array, as pairs of block numbers and data arrays, like so: * * [iBlock0, [dw0, dw1, ...], iBlock1, [dw0, dw1, ...], ...] * * In a normal 4Kb block, there will be 1K DWORD values in the data array. Remember that each DWORD is a signed 32-bit * integer (because they are formed using bit-wise operator rather than floating-point math operators), so don't be * surprised to see negative numbers in the data. * * The above example assumes "uncompressed" data arrays. If we choose to use "compressed" data arrays, the data arrays * will look like: * * [count0, dw0, count1, dw1, ...] * * where each count indicates how many times the following DWORD value occurs. A data array length less than 1K indicates * that it's compressed, since we'll only store them in compressed form if they actually shrank, and we'll use State * helper methods compress() and decompress() to create and expand the compressed data arrays. * * @this {Bus} * @return {Array} a */ Bus.prototype.saveMemory = function() { var i = 0; var a = []; for (var iBlock = 0; iBlock < this.blockTotal; iBlock++) { var block = this.aMemBlocks[iBlock]; /* * We have to check both fDirty and fDirtyEver, because we may have called cleanMemory() on some of * the memory blocks (eg, video memory), and while cleanMemory() will clear a dirty block's fDirty flag, * it also sets the dirty block's fDirtyEver flag, which is left set for the lifetime of the machine. */ if (block.fDirty || block.fDirtyEver) { a[i++] = iBlock; a[i++] = State.compress(block.save()); } } a[i] = this.getA20(); return a; }; /** * restoreMemory(a) * * This restores the contents of all Memory blocks; called by X86CPU.restore(). * * In theory, we ONLY have to save/restore block contents. Other block attributes, * like fReadOnly, the memory controller (if any), and the active memory access functions, * should already be restored, since every component (re)allocates all the memory blocks * it was using when it's restored. And since the CPU is guaranteed to be the last * component to be restored, all those blocks (and their attributes) should be in place now. * * See saveMemory() for a description of how the memory block contents are saved. * * @this {Bus} * @param {Array} a * @return {boolean} true if successful, false if not */ Bus.prototype.restoreMemory = function(a) { var i; for (i = 0; i < a.length - 1; i += 2) { var iBlock = a[i]; var adw = a[i+1]; if (adw && adw.length < this.blockLen) { adw = State.decompress(adw, this.blockLen); } var block = this.aMemBlocks[iBlock]; if (!block || !block.restore(adw)) { /* * Either the block to restore hasn't been allocated, indicating a change in the machine * configuration since it was last saved (the most likely explanation) or there's some internal * inconsistency (eg, the block size is wrong). */ this.error("Unable to restore memory block " + iBlock); return false; } } if (a[i] !== undefined) this.setA20(a[i]); return true; }; /** * addMemoryBreakpoint(addr, fWrite) * * @this {Bus} * @param {number} addr * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint */ Bus.prototype.addMemoryBreakpoint = function(addr, fWrite) { if (DEBUGGER) { var iBlock = addr >> this.blockShift; this.aMemBlocks[iBlock].addBreakpoint(addr & this.blockLimit, fWrite); } }; /** * removeMemoryBreakpoint(addr, fWrite) * * @this {Bus} * @param {number} addr * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint */ Bus.prototype.removeMemoryBreakpoint = function(addr, fWrite) { if (DEBUGGER) { var iBlock = addr >> this.blockShift; this.aMemBlocks[iBlock].removeBreakpoint(addr & this.blockLimit, fWrite); } }; /** * addPortInputBreak(port) * * @this {Bus} * @param {number} [port] * @return {boolean} true if break on port input enabled, false if disabled */ Bus.prototype.addPortInputBreak = function(port) { if (port === undefined) { this.fPortInputBreakAll = !this.fPortInputBreakAll; return this.fPortInputBreakAll; } if (this.aPortInputNotify[port] === undefined) { this.aPortInputNotify[port] = [null, null, false]; } this.aPortInputNotify[port][2] = !this.aPortInputNotify[port][2]; return this.aPortInputNotify[port][2]; }; /** * addPortInputNotify(start, end, component, fn) * * Add a port input-notification handler to the list of such handlers. * * @this {Bus} * @param {number} start port address * @param {number} end port address * @param {Component} component * @param {function(number,number)} fn is called with the port and EIP values at the time of the input */ Bus.prototype.addPortInputNotify = function(start, end, component, fn) { if (fn !== undefined) { for (var port = start; port <= end; port++) { if (this.aPortInputNotify[port] !== undefined) { this.warning("Input port " + str.toHexWord(port) + " registered by " + this.aPortInputNotify[port][0].id + ", ignoring " + component.id); continue; } this.aPortInputNotify[port] = [component, fn, false, false]; if (MAXDEBUG) this.log("addPortInputNotify(" + str.toHexWord(port) + "," + component.id + ")"); } } }; /** * addPortInputTable(component, table, offset) * * Add port input-notification handlers from the specified table (a batch version of addPortInputNotify) * * @this {Bus} * @param {Component} component * @param {Object} table * @param {number} [offset] is an optional port offset */ Bus.prototype.addPortInputTable = function(component, table, offset) { if (offset === undefined) offset = 0; for (var port in table) { /* * JavaScript coerces property keys to strings, so we use parseInt() to coerce them back to numbers. */ port = parseInt(port, 10); this.addPortInputNotify(port + offset, port + offset, component, table[port]); } }; /** * checkPortInputNotify(port, addrFrom) * * @this {Bus} * @param {number} port * @param {number} [addrFrom] is the EIP value at the time of the input * @return {number} simulated port value (0xff if none) * * NOTE: It seems that at least parts of the ROM BIOS (like the RS-232 probes around F000:E5D7 in the 5150 BIOS) * assume that ports for non-existent hardware return 0xff rather than 0x00, hence my new default (0xff) below. */ Bus.prototype.checkPortInputNotify = function(port, addrFrom) { var bIn = 0xff; var aNotify = this.aPortInputNotify[port]; if (aNotify !== undefined) { if (aNotify[1]) { bIn = aNotify[1].call(aNotify[0], port, addrFrom); } if (DEBUGGER && this.dbg && this.fPortInputBreakAll != aNotify[2]) { this.dbg.checkPortInput(port, bIn); } } else { if (DEBUGGER && this.dbg) { this.dbg.messagePort(this, port, null, addrFrom); if (this.fPortInputBreakAll) this.dbg.checkPortInput(port, bIn); } } return bIn; }; /** * removePortInputNotify(start, end, component, fn) * * Remove a port input-notification handler from the list of such handlers (to be ENABLED later if needed) * * @this {Bus} * @param {number} start address * @param {number} end address * @param {Component} component * @param {function(number,number)} fn of previously added handler * Bus.prototype.removePortInputNotify = function(start, end, component, fn) { for (var port = start; port < end; port++) { if (this.aPortInputNotify[port] && this.aPortInputNotify[port][0] == component && this.aPortInputNotify[port][1] == fn) { this.aPortInputNotify[port] = undefined; } } }; */ /** * addPortOutputBreak(port) * * @this {Bus} * @param {number} [port] * @return {boolean} true if break on port output enabled, false if disabled */ Bus.prototype.addPortOutputBreak = function(port) { if (port === undefined) { this.fPortOutputBreakAll = !this.fPortOutputBreakAll; return this.fPortOutputBreakAll; } if (this.aPortOutputNotify[port] === undefined) { this.aPortOutputNotify[port] = [null, null, false]; } this.aPortOutputNotify[port][2] = !this.aPortOutputNotify[port][2]; return this.aPortOutputNotify[port][2]; }; /** * addPortOutputNotify(start, end, component, fn) * * Add a port output-notification handler to the list of such handlers. * * @this {Bus} * @param {number} start port address * @param {number} end port address * @param {Component} component * @param {function(number,number)} fn is called with the port and EIP values at the time of the output */ Bus.prototype.addPortOutputNotify = function(start, end, component, fn) { if (fn !== undefined) { for (var port = start; port <= end; port++) { if (this.aPortOutputNotify[port] !== undefined) { this.warning("Output port " + str.toHexWord(port) + " registered by " + this.aPortOutputNotify[port][0].id + ", ignoring " + component.id); continue; } this.aPortOutputNotify[port] = [component, fn, false, false]; if (MAXDEBUG) this.log("addPortOutputNotify(" + str.toHexWord(port) + "," + component.id + ")"); } } }; /** * addPortOutputTable(component, table, offset) * * Add port output-notification handlers from the specified table (a batch version of addPortOutputNotify) * * @this {Bus} * @param {Component} component * @param {Object} table * @param {number} [offset] is an optional port offset */ Bus.prototype.addPortOutputTable = function(component, table, offset) { if (offset === undefined) offset = 0; for (var port in table) { /* * JavaScript converts property keys to strings (brilliant), so we use parseInt() to convert them back to numbers. */ port = parseInt(port, 10); this.addPortOutputNotify(port + offset, port + offset, component, table[port]); } }; /** * checkPortOutputNotify(port, bOut, addrFrom) * * @this {Bus} * @param {number} port * @param {number} bOut * @param {number} [addrFrom] is the EIP value at the time of the output */ Bus.prototype.checkPortOutputNotify = function(port, bOut, addrFrom) { var aNotify = this.aPortOutputNotify[port]; if (aNotify !== undefined) { if (aNotify[1]) { aNotify[1].call(aNotify[0], port, bOut, addrFrom); } if (DEBUGGER && this.dbg && this.fPortOutputBreakAll != aNotify[2]) { this.dbg.checkPortOutput(port, bOut); } } else { if (DEBUGGER && this.dbg) { this.dbg.messagePort(this, port, bOut, addrFrom); if (this.fPortOutputBreakAll) this.dbg.checkPortOutput(port, bOut); } } }; /** * removePortOutputNotify(start, end, component, fn) * * Remove a port output-notification handler from the list of such handlers (to be ENABLED later if needed) * * @this {Bus} * @param {number} start address * @param {number} end address * @param {Component} component * @param {function(number,number)} fn of previously added handler * Bus.prototype.removePortOutputNotify = function(start, end, component, fn) { for (var port = start; port < end; port++) { if (this.aPortOutputNotify[port] && this.aPortOutputNotify[port][0] == component && this.aPortOutputNotify[port][1] == fn) { this.aPortOutputNotify[port] = undefined; } } }; */ /** * reportError(op, addr, size) * * @this {Bus} * @param {number} op * @param {number} addr * @param {number} size * @return {boolean} false */ Bus.prototype.reportError = function(op, addr, size) { this.error("Memory block error (" + op + "," + str.toHex(addr) + "," + str.toHex(size) + ")"); return false; }; if (typeof APP_PCJS !== 'undefined') APP_PCJS.Bus = Bus; if (typeof module !== 'undefined') module.exports = Bus;