Added support for PDP-10 RAM images
This commit is contained in:
parent
6cf141baf9
commit
aecd943c52
23 changed files with 583 additions and 632 deletions
|
|
@ -46,7 +46,7 @@ var DumpAPI = require("../../shared/lib/dumpapi");
|
|||
* what the loadFile() function already does.
|
||||
*
|
||||
* @constructor
|
||||
* @param {string|undefined} sFormat should be one of "json"|"data"|"hex"|"bytes"|"rom" (see the FORMAT constants)
|
||||
* @param {string|undefined} sFormat should be one of "json"|"longs"|"hex"|"bytes"|"rom" (see the FORMAT constants)
|
||||
* @param {boolean|string|undefined} fComments enables comments and other readability enhancements in the JSON output
|
||||
* @param {boolean|string|undefined} fDecimal forces decimal output if not undefined
|
||||
* @param {number|string|undefined} offDump
|
||||
|
|
@ -82,7 +82,7 @@ function FileDump(sFormat, fComments, fDecimal, offDump, nWidthDump, sServerRoot
|
|||
FileDump.sAPIURL = "http://www.pcjs.org" + DumpAPI.ENDPOINT;
|
||||
FileDump.sCopyright = COPYRIGHT;
|
||||
FileDump.sNotice = FileDump.sAPIURL + " " + FileDump.sCopyright;
|
||||
FileDump.sUsage = "Usage: " + FileDump.sAPIURL + "?" + DumpAPI.QUERY.FILE + "=({path}|{URL})&" + DumpAPI.QUERY.FORMAT + "=(json|data|hex|octal|bytes|words|rom)";
|
||||
FileDump.sUsage = "Usage: " + FileDump.sAPIURL + "?" + DumpAPI.QUERY.FILE + "=({path}|{URL})&" + DumpAPI.QUERY.FORMAT + "=(json|longs|hex|octal|bytes|words|rom)";
|
||||
|
||||
FileDump.asBadExts = [
|
||||
"js", "log"
|
||||
|
|
@ -99,7 +99,7 @@ FileDump.asBadExts = [
|
|||
*
|
||||
* Usage
|
||||
* ---
|
||||
* filedump --file=({path}|{URL}) [--merge=({path}|{url})] [--format=(json|data|hex|octal|bytes|words|rom)]
|
||||
* filedump --file=({path}|{URL}) [--merge=({path}|{url})] [--format=(json|longs|hex|octal|bytes|words|rom)]
|
||||
* [--comments] [--decimal] [--offset={number}] [--width={number}] [--load={number}] [--exec={number}]
|
||||
* [--output={path}] [--overwrite]
|
||||
*
|
||||
|
|
@ -146,7 +146,7 @@ FileDump.CLI = function()
|
|||
var args = proc.getArgs();
|
||||
|
||||
if (!args.argc) {
|
||||
console.log("usage: filedump --file=({path}|{URL}) [--merge=({path}|{url})] [--format=(json|data|hex|octal|bytes|words|rom)] [--comments] [--decimal] [--offset={number}] [--width={number}] [--output={path}] [--overwrite]");
|
||||
console.log("usage: filedump --file=({path}|{URL}) [--merge=({path}|{url})] [--format=(json|longs|hex|octal|bytes|words|rom)] [--comments] [--decimal] [--offset={number}] [--width={number}] [--output={path}] [--overwrite]");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -752,7 +752,7 @@ FileDump.prototype.buildJSON = function()
|
|||
else if (this.sFormat == DumpAPI.FORMAT.OCTAL || this.sFormat == DumpAPI.FORMAT.WORDS) {
|
||||
this.json += this.dumpBuffer(this.sFormat, this.buf, this.buf.length, 2);
|
||||
} else {
|
||||
this.json += this.dumpBuffer("data", this.buf, this.buf.length, 4);
|
||||
this.json += this.dumpBuffer(DumpAPI.FORMAT.LONGS, this.buf, this.buf.length, 4);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -344,10 +344,10 @@ class CPUStatePDP10 extends CPUPDP10 {
|
|||
}
|
||||
|
||||
/*
|
||||
* Bits 0-22 (I,X,Y) contain what we call a "reference address" (R), which is used to calculate the
|
||||
* "effective address" (E). To determine E from R, we must extract I, X, and Y from R, set E to Y,
|
||||
* then add [X] to E if X is non-zero. If I is zero, then we're done; otherwise, we must set R to [E]
|
||||
* and repeat the process.
|
||||
* Bits 0-22 (I,X,Y) contain what we call a "reference address" (R), which is used to calculate an
|
||||
* 18-bit "effective address" (E). To determine E from R, we must extract I, X, and Y from R, set E
|
||||
* to Y, then add [X] to E if X is non-zero. If I is zero, then we're done; otherwise, we must set R
|
||||
* to [E] and repeat the process.
|
||||
*
|
||||
* However, we don't actually repeat the process immediately; we need to treat each indirection as a
|
||||
* separate decoding step, to ensure that the emulator can "breathe" periodically. So instead, we
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class RAMPDP10 extends Component {
|
|||
{
|
||||
super("RAM", parmsRAM);
|
||||
|
||||
this.abInit = null;
|
||||
this.aData = null;
|
||||
this.aSymbols = null;
|
||||
|
||||
this.addrRAM = +parmsRAM['addr'];
|
||||
|
|
@ -71,7 +71,7 @@ class RAMPDP10 extends Component {
|
|||
if (this.addrExec != null) this.addrExec = +this.addrExec;
|
||||
|
||||
this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified
|
||||
this.fAllocated = false;
|
||||
this.fAllocated = this.fReset = false;
|
||||
|
||||
this.sFilePath = parmsRAM['file'];
|
||||
this.sFileName = Str.getBaseName(this.sFilePath);
|
||||
|
|
@ -180,7 +180,7 @@ class RAMPDP10 extends Component {
|
|||
Component.addMachineResource(this.idMachine, sURL, sData);
|
||||
var resource = Web.parseMemoryResource(sURL, sData);
|
||||
if (resource) {
|
||||
this.abInit = resource.aBytes;
|
||||
this.aData = resource.aData;
|
||||
this.aSymbols = resource.aSymbols;
|
||||
if (this.addrLoad == null) this.addrLoad = resource.addrLoad;
|
||||
if (this.addrExec == null) this.addrExec = resource.addrExec;
|
||||
|
|
@ -219,9 +219,9 @@ class RAMPDP10 extends Component {
|
|||
/*
|
||||
* Too early...
|
||||
*/
|
||||
if (!this.abInit || !this.bus) return;
|
||||
if (!this.aData) return;
|
||||
|
||||
if (this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM)) {
|
||||
if (this.loadImage(this.aData, this.addrLoad, this.addrExec, this.addrRAM)) {
|
||||
this.status('Loaded image "' + this.sFileName + '"');
|
||||
} else {
|
||||
this.notice('Error loading image "' + this.sFileName + '"');
|
||||
|
|
@ -230,9 +230,10 @@ class RAMPDP10 extends Component {
|
|||
/*
|
||||
* NOTE: We now retain this data, so that reset() can return the RAM to its predefined state.
|
||||
*
|
||||
* delete this.abInit;
|
||||
* delete this.aData;
|
||||
*/
|
||||
}
|
||||
this.fReset = true;
|
||||
this.setReady();
|
||||
}
|
||||
}
|
||||
|
|
@ -244,126 +245,46 @@ class RAMPDP10 extends Component {
|
|||
*/
|
||||
reset()
|
||||
{
|
||||
if (this.fAllocated) {
|
||||
if (this.fAllocated && !this.fReset) {
|
||||
/*
|
||||
* TODO: Add a configuration parameter for selecting the byte pattern on reset?
|
||||
* Note that when memory blocks are originally created, they are currently always
|
||||
* zero-initialized, so this would only affect resets.
|
||||
*/
|
||||
this.bus.zeroMemory(this.addrRAM, this.sizeRAM, 0);
|
||||
if (this.abInit) {
|
||||
this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM, !this.dbg);
|
||||
if (this.aData) {
|
||||
this.loadImage(this.aData, this.addrLoad, this.addrExec, this.addrRAM, !this.dbg);
|
||||
}
|
||||
}
|
||||
this.fReset = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* loadImage(aBytes, addrLoad, addrExec, addrInit, fStart)
|
||||
* loadImage(aData, addrLoad, addrExec, addrInit, fStart)
|
||||
*
|
||||
* If the array contains a PAPER tape image in the "Absolute Format," load it as specified
|
||||
* by the format; otherwise, load it as-is using the address(es) supplied.
|
||||
*
|
||||
* @this {RAMPDP10}
|
||||
* @param {Array|Uint8Array} aBytes
|
||||
* @param {Array} aData
|
||||
* @param {number|null} [addrLoad]
|
||||
* @param {number|null} [addrExec] (this CAN override any starting address INSIDE the image)
|
||||
* @param {number|null} [addrInit]
|
||||
* @param {boolean} [fStart]
|
||||
* @return {boolean} (true if loaded, false if not)
|
||||
*/
|
||||
loadImage(aBytes, addrLoad, addrExec, addrInit, fStart)
|
||||
loadImage(aData, addrLoad, addrExec, addrInit, fStart)
|
||||
{
|
||||
var fStop = false;
|
||||
var fLoaded = false;
|
||||
/*
|
||||
* Data on tapes in the "Absolute Format" is organized into blocks; each block begins with
|
||||
* a 6-byte header:
|
||||
*
|
||||
* 2-byte signature (0x0001)
|
||||
* 2-byte block length (N + 6, because it includes the 6-byte header)
|
||||
* 2-byte load address
|
||||
*
|
||||
* followed by N data bytes. If N is zero, then the 2-byte load address is the exec address,
|
||||
* unless the address is odd (usually 1). DEC's Absolute Loader jumps to the exec address
|
||||
* in former case, halts in the latter.
|
||||
*
|
||||
* All values are stored "little endian" (low byte followed by high byte), just like the
|
||||
* PDP-11's memory architecture.
|
||||
*
|
||||
* After the data bytes, there is a single checksum byte. The 8-bit sum of all the bytes in
|
||||
* the block (including the header bytes and checksum byte) should be zero.
|
||||
*
|
||||
* ANOMALIES: Tape files don't always begin with a signature word, so I allow any number of
|
||||
* leading zeros before the first signature. Tape files don't always end cleanly either, so as
|
||||
* soon as I see an invalid signature, I break out of the loop without signalling an error, as
|
||||
* long as at least ONE block was successfully processed. In fact, it's possible that as
|
||||
* soon as a block with ZERO data bytes is encountered, processing is supposed to stop, but
|
||||
* I haven't examined enough tapes (or the Absolute Loader code) to know for sure.
|
||||
*/
|
||||
|
||||
if (addrLoad == null) {
|
||||
var off = 0, fError = false;
|
||||
while (off < aBytes.length - 1) {
|
||||
var w = (aBytes[off] & 0xff) | ((aBytes[off+1] & 0xff) << 8);
|
||||
if (!w) { // ignore pairs of leading zeros
|
||||
off += 2;
|
||||
continue;
|
||||
}
|
||||
if (!(w & 0xff)) { // as well as single bytes of zero
|
||||
off++;
|
||||
continue;
|
||||
}
|
||||
var offBlock = off;
|
||||
if (w != 0x0001) {
|
||||
this.printMessage("invalid signature (" + Str.toHexWord(w) + ") at offset " + Str.toHexWord(offBlock), MessagesPDP10.PAPER);
|
||||
break;
|
||||
}
|
||||
if (off + 6 >= aBytes.length) {
|
||||
this.printMessage("invalid block at offset " + Str.toHexWord(offBlock), MessagesPDP10.PAPER);
|
||||
break;
|
||||
}
|
||||
off += 2;
|
||||
var checksum = w;
|
||||
var len = (aBytes[off++] & 0xff) | ((aBytes[off++] & 0xff) << 8);
|
||||
var addr = (aBytes[off++] & 0xff) | ((aBytes[off++] & 0xff) << 8);
|
||||
checksum += (len & 0xff) + (len >> 8) + (addr & 0xff) + (addr >> 8);
|
||||
var offData = off, cbData = len -= 6;
|
||||
while (len > 0 && off < aBytes.length) {
|
||||
checksum += aBytes[off++] & 0xff;
|
||||
len--;
|
||||
}
|
||||
if (len != 0 || off >= aBytes.length) {
|
||||
this.printMessage("insufficient data for block at offset " + Str.toHexWord(offBlock), MessagesPDP10.PAPER);
|
||||
break;
|
||||
}
|
||||
checksum += aBytes[off++] & 0xff;
|
||||
if (checksum & 0xff) {
|
||||
this.printMessage("invalid checksum (" + Str.toHexByte(checksum) + ") for block at offset " + Str.toHexWord(offBlock), MessagesPDP10.PAPER);
|
||||
break;
|
||||
}
|
||||
if (!cbData) {
|
||||
if (addr & 0x1) {
|
||||
fStop = true;
|
||||
} else {
|
||||
if (addrExec == null) addrExec = addr;
|
||||
}
|
||||
if (addrExec != null) this.printMessage("starting address: " + Str.toHexWord(addrExec), MessagesPDP10.PAPER);
|
||||
} else {
|
||||
this.printMessage("loading " + Str.toHexWord(cbData) + " bytes at " + Str.toHexWord(addr) + "-" + Str.toHexWord(addr + cbData), MessagesPDP10.PAPER);
|
||||
while (cbData--) {
|
||||
this.bus.setWordDirect(addr++, aBytes[offData++]);
|
||||
}
|
||||
}
|
||||
fLoaded = true;
|
||||
}
|
||||
addrLoad = addrInit;
|
||||
}
|
||||
if (!fLoaded) {
|
||||
if (addrLoad == null) addrLoad = addrInit;
|
||||
if (addrLoad != null) {
|
||||
for (var i = 0; i < aBytes.length; i++) {
|
||||
this.bus.setWordDirect(addrLoad + i, aBytes[i]);
|
||||
}
|
||||
fLoaded = true;
|
||||
if (addrLoad != null) {
|
||||
for (var i = 0; i < aData.length; i++) {
|
||||
this.bus.setWordDirect(addrLoad + i, aData[i]);
|
||||
}
|
||||
fLoaded = true;
|
||||
}
|
||||
if (fLoaded) {
|
||||
/*
|
||||
|
|
@ -374,7 +295,7 @@ class RAMPDP10 extends Component {
|
|||
* image, but we know that the directions for that diagnostic say to "Start and Restart at 200",
|
||||
* so we have manually inserted an "exec":128 in the JSON containing the image.
|
||||
*/
|
||||
if (addrExec == null || fStop) {
|
||||
if (addrExec == null) {
|
||||
this.cpu.stopCPU();
|
||||
fStart = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class RAMPDP11 extends Component {
|
|||
if (this.addrExec != null) this.addrExec = +this.addrExec;
|
||||
|
||||
this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified
|
||||
this.fAllocated = false;
|
||||
this.fAllocated = this.fReset = false;
|
||||
|
||||
this.sFilePath = parmsRAM['file'];
|
||||
this.sFileName = Str.getBaseName(this.sFilePath);
|
||||
|
|
@ -224,7 +224,7 @@ class RAMPDP11 extends Component {
|
|||
/*
|
||||
* Too early...
|
||||
*/
|
||||
if (!this.abInit || !this.bus) return;
|
||||
if (!this.abInit) return;
|
||||
|
||||
if (this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM)) {
|
||||
this.status('Loaded image "' + this.sFileName + '"');
|
||||
|
|
@ -238,6 +238,7 @@ class RAMPDP11 extends Component {
|
|||
* delete this.abInit;
|
||||
*/
|
||||
}
|
||||
this.fReset = true;
|
||||
this.setReady();
|
||||
}
|
||||
}
|
||||
|
|
@ -249,7 +250,7 @@ class RAMPDP11 extends Component {
|
|||
*/
|
||||
reset()
|
||||
{
|
||||
if (this.fAllocated) {
|
||||
if (this.fAllocated && !this.fReset) {
|
||||
/*
|
||||
* TODO: Add a configuration parameter for selecting the byte pattern on reset?
|
||||
* Note that when memory blocks are originally created, they are currently always
|
||||
|
|
@ -260,6 +261,7 @@ class RAMPDP11 extends Component {
|
|||
this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM, !this.dbg);
|
||||
}
|
||||
}
|
||||
this.fReset = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ var DumpAPI = {
|
|||
OCTAL: "octal", // displays data as octal words
|
||||
BYTES: "bytes", // displays data as hex bytes; normally used only when comments are enabled
|
||||
WORDS: "words", // displays data as hex words; normally used only when comments are enabled
|
||||
LONGS: "longs", // displays data as dwords
|
||||
IMG: "img", // returns the raw disk data (ie, using a Buffer object) (DiskDump only)
|
||||
ROM: "rom" // returns the raw file data (ie, using a Buffer object) (FileDump only)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,6 +268,23 @@ class Web {
|
|||
/**
|
||||
* parseMemoryResource(sURL, sData)
|
||||
*
|
||||
* This converts a variety of JSON-style data streams into an Object with the following properties:
|
||||
*
|
||||
* aBytes
|
||||
* aSymbols
|
||||
* addrLoad
|
||||
* addrExec
|
||||
*
|
||||
* If the source data contains a 'bytes' array, it's passed through to 'aBytes'; alternatively, if
|
||||
* it contains a 'words' array, the values are converted from 16-bit to 8-bit and stored in 'aBytes',
|
||||
* and if it contains a 'longs' array, the values are converted from 32-bit longs into bytes and
|
||||
* stored in 'aBytes'.
|
||||
*
|
||||
* Alternatively, if the source data contains a 'data' array, we simply pass that through to the output
|
||||
* object as:
|
||||
*
|
||||
* aData
|
||||
*
|
||||
* @param {string} sURL
|
||||
* @param {string} sData
|
||||
* @return {Object|null} (resource)
|
||||
|
|
@ -315,8 +332,8 @@ class Web {
|
|||
*
|
||||
* ["unrecognized disk path: test.img"]
|
||||
*/
|
||||
if (sData.indexOf("0x") < 0 && sData.substr(0, 2) != "[\"") {
|
||||
data = JSON.parse(sData.replace(/([a-z]+):/gm, "\"$1\":").replace(/\/\/[^\n]*/gm, ""));
|
||||
if (sData.indexOf("0x") < 0 && sData.indexOf("0o") < 0 && sData.substr(0, 2) != '["') {
|
||||
data = JSON.parse(sData.replace(/([a-z]+):/gm, '"$1":').replace(/\/\/[^\n]*/gm, ""));
|
||||
} else {
|
||||
data = eval("(" + sData + ")");
|
||||
}
|
||||
|
|
@ -338,7 +355,7 @@ class Web {
|
|||
Component.assert(!(a[i] & ~0xffff));
|
||||
}
|
||||
}
|
||||
else if (a = data['data']) {
|
||||
else if (a = data['longs']) {
|
||||
/*
|
||||
* Convert all dwords (longs) into bytes
|
||||
*/
|
||||
|
|
@ -350,20 +367,25 @@ class Web {
|
|||
resource.aBytes[ib++] = (a[i] >> 24) & 0xff;
|
||||
}
|
||||
}
|
||||
else if (a = data['data']) {
|
||||
resource.aData = a;
|
||||
}
|
||||
else {
|
||||
resource.aBytes = data;
|
||||
}
|
||||
|
||||
if (resource.aBytes) {
|
||||
if (!resource.aBytes.length) {
|
||||
Component.error("Empty resource: " + sURL);
|
||||
resource = null;
|
||||
}
|
||||
else if (resource.aBytes.length == 1) {
|
||||
Component.error(resource.aBytes[0]);
|
||||
resource = null;
|
||||
}
|
||||
}
|
||||
resource.aSymbols = data['symbols'];
|
||||
|
||||
if (!resource.aBytes.length) {
|
||||
Component.error("Empty resource: " + sURL);
|
||||
resource = null;
|
||||
}
|
||||
else if (resource.aBytes.length == 1) {
|
||||
Component.error(resource.aBytes[0]);
|
||||
resource = null;
|
||||
}
|
||||
} catch (e) {
|
||||
Component.error("Resource data error (" + sURL + "): " + e.message);
|
||||
resource = null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue