pcjs/versions/pdpjs/1.37.1/pdp11-uncompiled.js
2017-09-25 11:21:37 -07:00

32232 lines
1.1 MiB

"use strict";
/**
* @copyright http://pcjs.org/modules/shared/lib/defines.js (C) Jeff Parsons 2012-2017
*/
/**
* @define {string}
*/
var APPVERSION = "1.x.x"; // this @define is overridden by the Closure Compiler with the version in package.json
var XMLVERSION = null; // this is set in non-COMPILED builds by embedMachine() if a version number was found in the machine XML
var COPYRIGHT = "Copyright © 2012-2017 Jeff Parsons <Jeff@pcjs.org>";
var LICENSE = "License: GPL version 3 or later <http://gnu.org/licenses/gpl.html>";
var CSSCLASS = "pcjs";
/**
* @define {string}
*/
var SITEHOST = "localhost:8088";// this @define is overridden by the Closure Compiler with "www.pcjs.org"
/**
* @define {boolean}
*/
var COMPILED = false; // this @define is overridden by the Closure Compiler (to true)
/**
* @define {boolean}
*/
var DEBUG = true; // this @define is overridden by the Closure Compiler (to false) to remove DEBUG-only code
/**
* @define {boolean}
*/
var MAXDEBUG = false; // this @define is overridden by the Closure Compiler (to false) to remove MAXDEBUG-only code
/**
* @define {boolean}
*/
var PRIVATE = false; // this @define is overridden by the Closure Compiler (to false) to enable PRIVATE code
/*
* RS-232 DB-25 Pin Definitions, mapped to bits 1-25 in a 32-bit status value.
*
* SerialPorts in PCjs machines are considered DTE (Data Terminal Equipment), which means they should be "virtually"
* connected to each other via a null-modem cable, which assumes the following cross-wiring:
*
* G 1 <-> 1 G (Ground)
* TD 2 <-> 3 RD (Received Data)
* RD 3 <-> 2 TD (Transmitted Data)
* RTS 4 <-> 5 CTS (Clear To Send)
* CTS 5 <-> 4 RTS (Request To Send)
* DSR 6+8 <-> 20 DTR (Data Terminal Ready)
* SG 7 <-> 7 SG (Signal Ground)
* DTR 20 <-> 6+8 DSR (Data Set Ready + Carrier Detect)
* RI 22 <-> 22 RI (Ring Indicator)
*
* TODO: Move these definitions to a more appropriate shared file at some point.
*/
var RS232 = {
RTS: {
PIN: 4,
MASK: 0x00000010
},
CTS: {
PIN: 5,
MASK: 0x00000020
},
DSR: {
PIN: 6,
MASK: 0x00000040
},
CD: {
PIN: 8,
MASK: 0x00000100
},
DTR: {
PIN: 20,
MASK: 0x00100000
},
RI: {
PIN: 22,
MASK: 0x00400000
}
};
/*
* NODE should be true if we're running under NodeJS (eg, command-line), false if not (eg, web browser)
*/
var NODE = false;
/**
* @copyright http://pcjs.org/modules/shared/lib/diskapi.js (C) Jeff Parsons 2012-2017
*/
/*
* Our "DiskIO API" looks like:
*
* http://www.pcjs.org/api/v1/disk?action=open&volume=*10mb.img&mode=demandrw&chs=c:h:s&machine=xxx&user=yyy
*/
var DiskAPI = {
ENDPOINT: "/api/v1/disk",
QUERY: {
ACTION: "action", // value is one of DiskAPI.ACTION.*
VOLUME: "volume", // value is path of a disk image
MODE: "mode", // value is one of DiskAPI.MODE.*
CHS: "chs", // value is cylinders:heads:sectors:bytes
ADDR: "addr", // value is cylinder:head:sector:count
MACHINE: "machine", // value is machine token
USER: "user", // value is user ID
DATA: "data" // value is data to be written
},
ACTION: {
OPEN: "open",
READ: "read",
WRITE: "write",
CLOSE: "close"
},
MODE: {
LOCAL: "local", // this mode implies no API (at best, localStorage backing only)
PRELOAD: "preload", // this mode implies use of the DumpAPI
DEMANDRW: "demandrw",
DEMANDRO: "demandro"
},
FAIL: {
BADACTION: "invalid action",
BADUSER: "invalid user",
BADVOL: "invalid volume",
OPENVOL: "unable to open volume",
CREATEVOL: "unable to create volume",
WRITEVOL: "unable to write volume",
REVOKED: "access revoked"
}
};
/*
* TODO: Eventually, our tools will need to support looking up disk formats by "model" rather than by raw disk size,
* because obviously multiple disk geometries can yield the same raw disk size. For each conflict that arises, I'll
* probably create a fake (approximate) disk size entry above, and then create a mapping to that approximate size below.
*/
DiskAPI.MODELS = {
"RL01": 5242880,
"RL02": 10485760
};
DiskAPI.MBR = {
PARTITIONS: {
OFFSET: 0x1BE,
ENTRY: {
STATUS: 0x00, // 0x80 if active
CHS_FIRST: 0x01, // 3-byte CHS specifier
TYPE: 0x04, // see TYPE.*
CHS_LAST: 0x05, // 3-byte CHS specifier
LBA_FIRST: 0x08,
LBA_TOTAL: 0x0C,
LENGTH: 0x10
},
STATUS: {
ACTIVE: 0x80
},
TYPE: {
EMPTY: 0x00,
FAT12_PRIMARY: 0x01, // DOS 2.0 and up (12-bit FAT)
FAT16_PRIMARY: 0x04 // DOS 3.0 and up (16-bit FAT)
}
},
SIG_OFFSET: 0x1FE,
SIGNATURE: 0xAA55 // to be clear, the low byte (at offset 0x1FE) is 0x55 and the high byte (at offset 0x1FF) is 0xAA
};
/*
* Boot sector offsets (and assorted constants) in DOS-compatible boot sectors (DOS 2.0 and up)
*
* WARNING: I've heard apocryphal stories about SIGNATURE being improperly reversed on some systems
* (ie, 0x55AA instead 0xAA55) -- perhaps by a dyslexic programmer -- so be careful out there.
*/
DiskAPI.BOOT = {
JMP_OPCODE: 0x000, // 1 byte for a JMP opcode, followed by a 1 or 2-byte offset
OEM_STRING: 0x003, // 8 bytes
SIG_OFFSET: 0x1FE,
SIGNATURE: 0xAA55 // to be clear, the low byte (at offset 0x1FE) is 0x55 and the high byte (at offset 0x1FF) is 0xAA
};
/*
* BIOS Parameter Block (BPB) offsets in DOS-compatible boot sectors (DOS 2.x and up)
*
* NOTE: DOS 2.x OEM documentation says that the words starting at offset 0x018 (TRACK_SECS, TOTAL_HEADS, and HIDDEN_SECS)
* are optional, but even the DOS 2.0 FORMAT utility initializes all three of those words. There may be some OEM media out
* there with BPBs that are only valid up to offset 0x018, but I've not run across any media like that.
*
* DOS 3.20 added LARGE_SECS, but unfortunately, it was added as a 2-byte value at offset 0x01E. DOS 3.31 decided
* to make both HIDDEN_SECS and LARGE_SECS 4-byte values, which meant that LARGE_SECS had to move from 0x01E to 0x020.
*/
DiskAPI.BPB = {
SECTOR_BYTES: 0x00B, // 2 bytes: bytes per sector (eg, 0x200 or 512)
CLUSTER_SECS: 0x00D, // 1 byte: sectors per cluster (eg, 1)
RESERVED_SECS: 0x00E, // 2 bytes: reserved sectors; ie, # sectors preceding the first FAT--usually just the boot sector (eg, 1)
TOTAL_FATS: 0x010, // 1 byte: FAT copies (eg, 2)
ROOT_DIRENTS: 0x011, // 2 bytes: root directory entries (eg, 0x40 or 64) 0x40 * 0x20 = 0x800 (1 sector is 0x200 bytes, total of 4 sectors)
TOTAL_SECS: 0x013, // 2 bytes: number of sectors (eg, 0x140 or 320); if zero, refer to LARGE_SECS
MEDIA_ID: 0x015, // 1 byte: media ID (see DiskAPI.FAT.MEDIA_*); should also match the first byte of the FAT (aka FAT ID)
FAT_SECS: 0x016, // 2 bytes: sectors per FAT (eg, 1)
TRACK_SECS: 0x018, // 2 bytes: sectors per track (eg, 8)
TOTAL_HEADS: 0x01A, // 2 bytes: number of heads (eg, 1)
HIDDEN_SECS: 0x01C, // 2 bytes (DOS 2.x) or 4 bytes (DOS 3.31 and up): number of hidden sectors (always 0 for non-partitioned media)
LARGE_SECS: 0x020 // 4 bytes (DOS 3.31 and up): number of sectors if TOTAL_SECS is zero
};
/*
* Common (supported) diskette geometries.
*
* Each entry in GEOMETRIES is an array of values in "CHS" order:
*
* [# cylinders, # heads, # sectors/track, # bytes/sector, media ID]
*
* If the 4th value is omitted, the sector size is assumed to be 512. The order of these "geometric" values mirrors
* the structure of our JSON-encoded disk images, which consist of an array of cylinders, each of which is an array of
* heads, each of which is an array of sector objects.
*/
DiskAPI.GEOMETRIES = {
163840: [40,1,8,,0xFE], // media ID 0xFE: 40 cylinders, 1 head (single-sided), 8 sectors/track, ( 320 total sectors x 512 bytes/sector == 163840)
184320: [40,1,9,,0xFC], // media ID 0xFC: 40 cylinders, 1 head (single-sided), 9 sectors/track, ( 360 total sectors x 512 bytes/sector == 184320)
327680: [40,2,8,,0xFF], // media ID 0xFF: 40 cylinders, 2 heads (double-sided), 8 sectors/track, ( 640 total sectors x 512 bytes/sector == 327680)
368640: [40,2,9,,0xFD], // media ID 0xFD: 40 cylinders, 2 heads (double-sided), 9 sectors/track, ( 720 total sectors x 512 bytes/sector == 368640)
737280: [80,2,9,,0xF9], // media ID 0xF9: 80 cylinders, 2 heads (double-sided), 9 sectors/track, (1440 total sectors x 512 bytes/sector == 737280)
1228800: [80,2,15,,0xF9], // media ID 0xF9: 80 cylinders, 2 heads (double-sided), 15 sectors/track, (2400 total sectors x 512 bytes/sector == 1228800)
1474560: [80,2,18,,0xF0], // media ID 0xF0: 80 cylinders, 2 heads (double-sided), 18 sectors/track, (2880 total sectors x 512 bytes/sector == 1474560)
2949120: [80,2,36,,0xF0], // media ID 0xF0: 80 cylinders, 2 heads (double-sided), 36 sectors/track, (5760 total sectors x 512 bytes/sector == 2949120)
/*
* The following are some common disk sizes and their CHS values, since missing or bogus MBR and/or BPB values
* might mislead us when attempting to determine the exact disk geometry.
*/
10653696:[306,4,17], // PC XT 10Mb hard drive (type 3)
21411840:[615,4,17], // PC AT 20Mb hard drive (type 2)
/*
* Assorted DEC disk formats.
*/
256256: [77, 1,26,128], // RX01 single-platter diskette: 77 tracks, 1 head, 26 sectors/track, 128 bytes/sector, for a total of 256256 bytes
2494464: [203,2,12,512], // RK03 single-platter disk cartridge: 203 tracks, 2 heads, 12 sectors/track, 512 bytes/sector, for a total of 2494464 bytes
5242880: [256,2,40,256], // RL01K single-platter disk cartridge: 256 tracks, 2 heads, 40 sectors/track, 256 bytes/sector, for a total of 5242880 bytes
10485760:[512,2,40,256] // RL02K single-platter disk cartridge: 512 tracks, 2 heads, 40 sectors/track, 256 bytes/sector, for a total of 10485760 bytes
};
/*
* Media ID (descriptor) bytes for DOS-compatible FAT-formatted disks (stored in the first byte of the FAT)
*/
DiskAPI.FAT = {
MEDIA_160KB: 0xFE, // 5.25-inch, 1-sided, 8-sector, 40-track
MEDIA_180KB: 0xFC, // 5.25-inch, 1-sided, 9-sector, 40-track
MEDIA_320KB: 0xFF, // 5.25-inch, 2-sided, 8-sector, 40-track
MEDIA_360KB: 0xFD, // 5.25-inch, 2-sided, 9-sector, 40-track
MEDIA_720KB: 0xF9, // 3.5-inch, 2-sided, 9-sector, 80-track
MEDIA_1200KB: 0xF9, // 3.5-inch, 2-sided, 15-sector, 80-track
MEDIA_FIXED: 0xF8, // fixed disk (aka hard drive)
MEDIA_1440KB: 0xF0, // 3.5-inch, 2-sided, 18-sector, 80-track
MEDIA_2880KB: 0xF0 // 3.5-inch, 2-sided, 36-sector, 80-track
};
/*
* Cluster constants for 12-bit FATs (CLUSNUM_FREE, CLUSNUM_RES and CLUSNUM_MIN are the same for all FATs)
*/
DiskAPI.FAT12 = {
MAX_CLUSTERS: 4084,
CLUSNUM_FREE: 0, // this should NEVER appear in cluster chain (except at the start of an empty chain)
CLUSNUM_RES: 1, // reserved; this should NEVER appear in cluster chain
CLUSNUM_MIN: 2, // smallest valid cluster number
CLUSNUM_MAX: 0xFF6, // largest valid cluster number
CLUSNUM_BAD: 0xFF7, // bad cluster; this should NEVER appear in cluster chain
CLUSNUM_EOC: 0xFF8 // end of chain (actually, anything from 0xFF8-0xFFF indicates EOC)
};
/*
* Cluster constants for 16-bit FATs (CLUSNUM_FREE, CLUSNUM_RES and CLUSNUM_MIN are the same for all FATs)
*/
DiskAPI.FAT16 = {
MAX_CLUSTERS: 65524,
CLUSNUM_FREE: 0, // this should NEVER appear in cluster chain (except at the start of an empty chain)
CLUSNUM_RES: 1, // reserved; this should NEVER appear in cluster chain
CLUSNUM_MIN: 2, // smallest valid cluster number
CLUSNUM_MAX: 0xFFF6, // largest valid cluster number
CLUSNUM_BAD: 0xFFF7, // bad cluster; this should NEVER appear in cluster chain
CLUSNUM_EOC: 0xFFF8 // end of chain (actually, anything from 0xFFF8-0xFFFF indicates EOC)
};
/*
* Directory Entry offsets (and assorted constants) in FAT disk images
*
* NOTE: Versions of DOS prior to 2.0 use INVALID exclusively to mark available directory entries; any entry marked
* UNUSED will actually be considered USED. In DOS 2.0 and up, UNUSED was added to indicate that all remaining entries
* are unused, relieving it from having to initialize the rest of the sectors in the directory cluster(s). And in fact,
* you WILL encounter garbage in subsequent directory sectors if you attempt to read past an UNUSED entry.
*/
DiskAPI.DIRENT = {
NAME: 0x000, // 8 bytes
EXT: 0x008, // 3 bytes
ATTR: 0x00B, // 1 byte
MODTIME: 0x016, // 2 bytes
MODDATE: 0x018, // 2 bytes
CLUSTER: 0x01A, // 2 bytes
SIZE: 0x01C, // 4 bytes (typically zero for subdirectories)
LENGTH: 0x20, // 32 bytes total
UNUSED: 0x00, // indicates this and all subsequent directory entries are unused
INVALID: 0xE5 // indicates this directory entry is unused
};
/*
* Possible values for DIRENT.ATTR
*/
DiskAPI.ATTR = {
READONLY: 0x01, // PC-DOS 2.0 and up
HIDDEN: 0x02,
SYSTEM: 0x04,
LABEL: 0x08, // PC-DOS 2.0 and up
SUBDIR: 0x10, // PC-DOS 2.0 and up
ARCHIVE: 0x20 // PC-DOS 2.0 and up
};
/**
* @copyright http://pcjs.org/modules/shared/lib/dumpapi.js (C) Jeff Parsons 2012-2017
*/
/*
* Our "DiskDump API", such as it was, used to look like:
*
* http://jsmachines.net/bin/convdisk.php?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img
*
* To make it (a bit) more "REST-like", the above request now looks like:
*
* http://www.pcjs.org/api/v1/dump?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img
*
* Similarly, our "FileDump API" used to look like:
*
* http://jsmachines.net/bin/convrom.php?rom=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json
*
* and that request now looks like:
*
* http://www.pcjs.org/api/v1/dump?file=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json
*
* I don't think it makes sense to avoid "query" parameters, because blending the path of a disk image with the
* the rest of the URL would be (a) confusing, and (b) more work to parse.
*/
var DumpAPI = {
ENDPOINT: "/api/v1/dump",
QUERY: {
DIR: "dir", // value is path of a directory (DiskDump only)
DISK: "disk", // value is path of a disk image (DiskDump only)
FILE: "file", // value is path of a ROM image file (FileDump only)
IMG: "img", // alias for DISK
PATH: "path", // value is path of a one or more files (DiskDump only)
FORMAT: "format", // value is one of FORMAT values below
COMMENTS: "comments", // value is either "true" or "false"
DECIMAL: "decimal", // value is either "true" to force all numbers to decimal, "false" or undefined otherwise
MBHD: "mbhd", // value is hard drive size in Mb (formerly "mbsize") (DiskDump only) (DEPRECATED)
SIZE: "size" // value is target disk size in Kb (supersedes "mbhd") (DiskDump only)
},
FORMAT: {
JSON: "json", // default
JSON_GZ: "gz", // gzip is currently used ONLY for compressed JSON
DATA: "data", // same as "json", but built without JSON.stringify() (DiskDump only)
HEX: "hex", // deprecated
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)
}
};
/*
* Because we use an overloaded API endpoint (ie, one that's shared with the FileDump module), we must
* also provide a list of commands which, when combined with the endpoint, define a unique request.
*/
DumpAPI.asDiskCommands = [DumpAPI.QUERY.DIR, DumpAPI.QUERY.DISK, DumpAPI.QUERY.PATH];
DumpAPI.asFileCommands = [DumpAPI.QUERY.FILE];
/**
* @copyright http://pcjs.org/modules/shared/lib/reportapi.js (C) Jeff Parsons 2012-2017
*/
var ReportAPI = {
ENDPOINT: "/api/v1/report",
QUERY: {
APP: "app",
VER: "ver",
URL: "url",
USER: "user",
TYPE: "type",
DATA: "data"
},
TYPE: {
BUG: "bug"
},
RES: {
OK: "Thank you"
}
};
/**
* @copyright http://pcjs.org/modules/shared/lib/userapi.js (C) Jeff Parsons 2012-2017
*/
/*
* Examples of User API requests:
*
* web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUser;
*/
var UserAPI = {
ENDPOINT: "/api/v1/user",
QUERY: {
REQ: "req", // specifies a request
USER: "user", // specifies a user ID
STATE: "state", // specifies a state ID
DATA: "data" // specifies state data
},
REQ: {
CREATE: "create", // creates a user ID
VERIFY: "verify", // requests verification of a user ID
STORE: "store", // stores a machine state on the server
LOAD: "load" // loads a machine state from the server
},
RES: {
CODE: "code",
DATA: "data"
},
CODE: {
OK: "ok",
FAIL: "error"
},
FAIL: {
DUPLICATE: "user already exists",
VERIFY: "unable to verify user",
BADSTATE: "invalid state parameter",
NOSTATE: "no machine state",
BADLOAD: "unable to load machine state",
BADSTORE: "unable to save machine state"
}
};
/**
* @copyright http://pcjs.org/modules/shared/lib/keys.js (C) Jeff Parsons 2012-2017
*/
var Keys = {
/*
* Keys and/or key combinations that generate common ASCII codes.
*
* NOTE: If you're looking for a general-purpose ASCII code table, see Str.ASCII in strlib.js;
* if something's missing, that's probably the more appropriate table to add it to.
*
* TODO: The Closure Compiler doesn't inline all references to these values, at least those with
* quoted property names, which is why I've 'unquoted' as many of them as possible. One solution
* would be to add mnemonics for all of them, not just the non-printable ones (eg, SPACE instead
* of ' ', AMP instead of '&', etc.)
*/
ASCII: {
BREAK: 0, CTRL_A: 1, CTRL_B: 2, CTRL_C: 3, CTRL_D: 4, CTRL_E: 5, CTRL_F: 6, CTRL_G: 7,
CTRL_H: 8, CTRL_I: 9, CTRL_J: 10, CTRL_K: 11, CTRL_L: 12, CTRL_M: 13, CTRL_N: 14, CTRL_O: 15,
CTRL_P: 16, CTRL_Q: 17, CTRL_R: 18, CTRL_S: 19, CTRL_T: 20, CTRL_U: 21, CTRL_V: 22, CTRL_W: 23,
CTRL_X: 24, CTRL_Y: 25, CTRL_Z: 26,
' ': 32, '!': 33, '"': 34, '#': 35, '$': 36, '%': 37, '&': 38, "'": 39,
'(': 40, ')': 41, '*': 42, '+': 43, ',': 44, '-': 45, '.': 46, '/': 47,
'0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55,
'8': 56, '9': 57, ':': 58, ';': 59, '<': 60, '=': 61, '>': 62, '?': 63,
'@': 64, A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71,
H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79,
P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87,
X: 88, Y: 89, Z: 90, '[': 91, '\\':92, ']': 93, '^': 94, '_': 95,
'`': 96, a: 97, b: 98, c: 99, d: 100, e: 101, f: 102, g: 103,
h: 104, i: 105, j: 106, k: 107, l: 108, m: 109, n: 110, o: 111,
p: 112, q: 113, r: 114, s: 115, t: 116, u: 117, v: 118, w: 119,
x: 120, y: 121, z: 122, '{':123, '|':124, '}':125, '~':126, DEL: 127
},
/*
* Browser keyCodes we must pay particular attention to. For the most part, these are non-alphanumeric
* or function keys, some which may require special treatment (eg, preventDefault() if returning false on
* the initial keyDown event is insufficient).
*
* keyCodes for most common ASCII keys can simply use the appropriate ASCII code above.
*
* Most of these represent non-ASCII keys (eg, the LEFT arrow key), yet for some reason, browsers defined
* them using ASCII codes (eg, the LEFT arrow key uses the ASCII code for '%' or 37).
*/
KEYCODE: {
/* 0x08 */ BS: 8, // BACKSPACE (ASCII.CTRL_H)
/* 0x09 */ TAB: 9, // TAB (ASCII.CTRL_I)
/* 0x0A */ LF: 10, // LINE FEED (ASCII.CTRL_J) (TODO: Determine if any key actually generates this)
/* 0x0D */ CR: 13, // CARRIAGE RETURN (ASCII.CTRL_M)
/* 0x10 */ SHIFT: 16,
/* 0x11 */ CTRL: 17,
/* 0x12 */ ALT: 18,
/* 0x13 */ PAUSE: 19, // PAUSE/BREAK
/* 0x14 */ CAPS_LOCK: 20,
/* 0x1B */ ESC: 27,
/* 0x20 */ SPACE: 32,
/* 0x21 */ PGUP: 33,
/* 0x22 */ PGDN: 34,
/* 0x23 */ END: 35,
/* 0x24 */ HOME: 36,
/* 0x25 */ LEFT: 37,
/* 0x26 */ UP: 38,
/* 0x27 */ RIGHT: 39,
/* 0x27 */ FF_QUOTE: 39,
/* 0x28 */ DOWN: 40,
/* 0x2C */ FF_COMMA: 44,
/* 0x2C */ PRTSC: 44,
/* 0x2D */ INS: 45,
/* 0x2E */ DEL: 46,
/* 0x2E */ FF_PERIOD: 46,
/* 0x2F */ FF_SLASH: 47,
/* 0x30 */ ZERO: 48,
/* 0x31 */ ONE: 49,
/* 0x32 */ TWO: 50,
/* 0x33 */ THREE: 51,
/* 0x34 */ FOUR: 52,
/* 0x35 */ FIVE: 53,
/* 0x36 */ SIX: 54,
/* 0x37 */ SEVEN: 55,
/* 0x38 */ EIGHT: 56,
/* 0x39 */ NINE: 57,
/* 0x3B */ FF_SEMI: 59,
/* 0x3D */ FF_EQUALS: 61,
/* 0x5B */ CMD: 91, // aka WIN
/* 0x5B */ FF_LBRACK: 91,
/* 0x5C */ FF_BSLASH: 92,
/* 0x5D */ RCMD: 93, // aka MENU
/* 0x5D */ FF_RBRACK: 93,
/* 0x60 */ NUM_0: 96,
/* 0x60 */ NUM_INS: 96,
/* 0x60 */ FF_BQUOTE: 96,
/* 0x61 */ NUM_1: 97,
/* 0x61 */ NUM_END: 97,
/* 0x62 */ NUM_2: 98,
/* 0x62 */ NUM_DOWN: 98,
/* 0x63 */ NUM_3: 99,
/* 0x63 */ NUM_PGDN: 99,
/* 0x64 */ NUM_4: 100,
/* 0x64 */ NUM_LEFT: 100,
/* 0x65 */ NUM_5: 101,
/* 0x65 */ NUM_CENTER: 101,
/* 0x66 */ NUM_6: 102,
/* 0x66 */ NUM_RIGHT: 102,
/* 0x67 */ NUM_7: 103,
/* 0x67 */ NUM_HOME: 103,
/* 0x68 */ NUM_8: 104,
/* 0x68 */ NUM_UP: 104,
/* 0x69 */ NUM_9: 105,
/* 0x69 */ NUM_PGUP: 105,
/* 0x6A */ NUM_MUL: 106,
/* 0x6B */ NUM_ADD: 107,
/* 0x6D */ NUM_SUB: 109,
/* 0x6E */ NUM_DEL: 110, // aka PERIOD
/* 0x6F */ NUM_DIV: 111,
/* 0x70 */ F1: 112,
/* 0x71 */ F2: 113,
/* 0x72 */ F3: 114,
/* 0x73 */ F4: 115,
/* 0x74 */ F5: 116,
/* 0x75 */ F6: 117,
/* 0x76 */ F7: 118,
/* 0x77 */ F8: 119,
/* 0x78 */ F9: 120,
/* 0x79 */ F10: 121,
/* 0x7A */ F11: 122,
/* 0x7B */ F12: 123,
/* 0x90 */ NUM_LOCK: 144,
/* 0x91 */ SCROLL_LOCK: 145,
/* 0xAD */ FF_DASH: 173,
/* 0xBA */ SEMI: 186, // Firefox: 59 (FF_SEMI)
/* 0xBB */ EQUALS: 187, // Firefox: 61 (FF_EQUALS)
/* 0xBC */ COMMA: 188,
/* 0xBD */ DASH: 189, // Firefox: 173 (FF_DASH)
/* 0xBE */ PERIOD: 190,
/* 0xBF */ SLASH: 191,
/* 0xC0 */ BQUOTE: 192,
/* 0xDB */ LBRACK: 219,
/* 0xDC */ BSLASH: 220,
/* 0xDD */ RBRACK: 221,
/* 0xDE */ QUOTE: 222,
/* 0xE0 */ FF_CMD: 224, // Firefox only (used for both CMD and RCMD)
//
// The following biases use what I'll call Decimal Coded Binary or DCB (the opposite of BCD),
// where the thousands digit is used to store the sum of "binary" digits 1 and/or 2 and/or 4.
//
// Technically, that makes it DCO (Decimal Coded Octal), but then again, BCD should have really
// been called HCD (Hexadecimal Coded Decimal), so if "they" can take liberties, so can I.
//
// ONDOWN is a bias we add to browser keyCodes that we want to handle on "down" rather than on "press".
//
ONDOWN: 1000,
//
// ONRIGHT is a bias we add to browser keyCodes that need to check for a "right" location (default is "left")
//
ONRIGHT: 2000,
//
// FAKE is a bias we add to signal these are fake keyCodes corresponding to internal keystroke combinations.
// The actual values are for internal use only and merely need to be unique and used consistently.
//
FAKE: 4000
},
/*
* The set of values that a browser may store in the 'location' property of a keyboard event object
* which we also support.
*/
LOCATION: {
LEFT: 1,
RIGHT: 2,
NUMPAD: 3
}
};
/*
* Check the event object's 'location' property for a non-zero value for the following ONRIGHT keys.
*/
Keys.KEYCODE.NUM_CR = Keys.KEYCODE.CR + Keys.KEYCODE.ONRIGHT;
/*
* Maps Firefox keyCodes to their more common keyCode counterparts; a number of entries in this table
* are no longer valid (if indeed they ever were), so they've been commented out. It's likely that I
* simply extended this table to resolve additional differences in other browsers (ie, Opera), but without
* browser-specific checks, it's not safe to perform all the mappings shown below.
*/
Keys.FF_KEYCODES = {};
Keys.FF_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.KEYCODE.SEMI; // 59 -> 186
Keys.FF_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.KEYCODE.EQUALS; // 61 -> 187
Keys.FF_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.KEYCODE.DASH; // 173 -> 189
Keys.FF_KEYCODES[Keys.KEYCODE.FF_CMD] = Keys.KEYCODE.CMD; // 224 -> 91
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_COMMA] = Keys.KEYCODE.COMMA; // 44 -> 188
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_PERIOD] = Keys.KEYCODE.PERIOD; // 46 -> 190
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_SLASH] = Keys.KEYCODE.SLASH; // 47 -> 191
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_BQUOTE] = Keys.KEYCODE.BQUOTE; // 96 -> 192
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_LBRACK = Keys.KEYCODE.LBRACK; // 91 -> 219
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_BSLASH] = Keys.KEYCODE.BSLASH; // 92 -> 220
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_RBRACK] = Keys.KEYCODE.RBRACK; // 93 -> 221
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_QUOTE] = Keys.KEYCODE.QUOTE; // 39 -> 222
/*
* Maps non-ASCII keyCodes to their ASCII counterparts
*/
Keys.NONASCII_KEYCODES = {};
Keys.NONASCII_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['-']; // 173 -> 45
Keys.NONASCII_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[';']; // 186 -> 59
Keys.NONASCII_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['=']; // 187 -> 61
Keys.NONASCII_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['-']; // 189 -> 45
Keys.NONASCII_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII[',']; // 188 -> 44
Keys.NONASCII_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['.']; // 190 -> 46
Keys.NONASCII_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['/']; // 191 -> 47
Keys.NONASCII_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['`']; // 192 -> 96
Keys.NONASCII_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['[']; // 219 -> 91
Keys.NONASCII_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['\\']; // 220 -> 92
Keys.NONASCII_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII[']']; // 221 -> 93
Keys.NONASCII_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII["'"]; // 222 -> 39
/*
* Maps unshifted keyCodes to their shifted counterparts; to be used when a shift-key is down.
* Alphabetic characters are handled in code, since they must also take CAPS_LOCK into consideration.
*/
Keys.SHIFTED_KEYCODES = {};
Keys.SHIFTED_KEYCODES[Keys.ASCII['1']] = Keys.ASCII['!'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['2']] = Keys.ASCII['@'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['3']] = Keys.ASCII['#'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['4']] = Keys.ASCII['$'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['5']] = Keys.ASCII['%'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['6']] = Keys.ASCII['^'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['7']] = Keys.ASCII['&'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['8']] = Keys.ASCII['*'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['9']] = Keys.ASCII['('];
Keys.SHIFTED_KEYCODES[Keys.ASCII['0']] = Keys.ASCII[')'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[':'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['+'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII['<'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['_'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['>'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['?'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['~'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['{'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['|'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII['}'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII['"'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['_'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.ASCII['+'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.ASCII[':'];
/**
* @copyright http://pcjs.org/modules/shared/lib/strlib.js (C) Jeff Parsons 2012-2017
*/
class Str {
/**
* isValidInt(s, base)
*
* The built-in parseInt() function has the annoying feature of returning a partial value (ie,
* up to the point where it encounters an invalid character); eg, parseInt("foo", 16) returns 0xf.
*
* So it's best to use our own Str.parseInt() function, which will in turn use this function to
* validate the entire string.
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix to use (default is 10); only 2, 8, 10 and 16 are supported
* @return {boolean} true if valid, false if invalid (or the specified base isn't supported)
*/
static isValidInt(s, base)
{
if (!base || base == 10) return s.match(/^-?[0-9]+$/) !== null;
if (base == 16) return s.match(/^-?[0-9a-f]+$/i) !== null;
if (base == 8) return s.match(/^-?[0-7]+$/) !== null;
if (base == 2) return s.match(/^-?[01]+$/) !== null;
return false;
}
/**
* parseInt(s, base)
*
* This is a wrapper around the built-in parseInt() function. Our wrapper recognizes certain prefixes
* ('$' or "0x" for hex, '#' or "0o" for octal) and suffixes ('.' for decimal, 'h' for hex, 'y' for
* binary), and then calls isValidInt() to ensure we don't convert strings that contain partial values;
* see isValidInt() for details.
*
* The use of multiple prefix/suffix combinations is undefined (although for the record, we process
* prefixes first). We do NOT support the "0b" prefix to indicate binary UNLESS one or more commas are
* also present (because "0b" is also a valid hex sequence), and we do NOT support a single leading zero
* to indicate octal (because such a number could also be decimal or hex). Any number of commas are
* allowed; we remove them all before calling the built-in parseInt().
*
* More recently, we've added support for "^D", "^O", and "^B" prefixes to accommodate the base overrides
* that the PDP-10's MACRO-10 assembly language supports (decimal, octal, and binary, respectively).
* If this support turns out to adversely affect other debuggers, then it will have to be "conditionalized".
* Similarly, we've added support for "K", "M", and "G" MACRO-10-style suffixes that add 3, 6, or 9 zeros
* to the value to be parsed, respectively.
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix to use (default is 10); can be overridden by prefixes/suffixes
* @return {number|undefined} corresponding value, or undefined if invalid
*/
static parseInt(s, base)
{
var value;
if (s) {
if (!base) base = 10;
var ch, chPrefix, chSuffix;
var fCommas = (s.indexOf(',') > 0);
if (fCommas) s = s.replace(/,/g, '');
ch = chPrefix = s.charAt(0);
if (chPrefix == '#') {
base = 8;
chPrefix = '';
}
else if (chPrefix == '$') {
base = 16;
chPrefix = '';
}
if (ch != chPrefix) {
s = s.substr(1);
}
else {
ch = chPrefix = s.substr(0, 2);
if (chPrefix == '0b' && fCommas || chPrefix == '^B') {
base = 2;
chPrefix = '';
}
else if (chPrefix == '0o' || chPrefix == '^O') {
base = 8;
chPrefix = '';
}
else if (chPrefix == '^D') {
base = 10;
chPrefix = '';
}
else if (chPrefix == '0x') {
base = 16;
chPrefix = '';
}
if (ch != chPrefix) s = s.substr(2);
}
ch = chSuffix = s.slice(-1);
if (chSuffix == 'Y' || chSuffix == 'y') {
base = 2;
chSuffix = '';
}
else if (chSuffix == '.') {
base = 10;
chSuffix = '';
}
else if (chSuffix == 'H' || chSuffix == 'h') {
base = 16;
chSuffix = '';
}
else if (chSuffix == 'K') {
chSuffix = '000';
}
else if (chSuffix == 'M') {
chSuffix = '000000';
}
else if (chSuffix == 'G') {
chSuffix = '000000000';
}
if (ch != chSuffix) s = s.slice(0, -1) + chSuffix;
/*
* This adds support for the MACRO-10 binary shifting (Bn) suffix, which must be stripped from the
* number before parsing, and then applied to the value after parsing. If n is omitted, 35 is assumed,
* which is a net shift of zero. If n < 35, then a left shift of (35 - n) is required; if n > 35, then
* a right shift of -(35 - n) is required.
*/
var v, shift = 0;
if (base <= 10) {
var match = s.match(/(-?[0-9]+)B([0-9]*)/);
if (match) {
s = match[1];
shift = 35 - ((match[2] || 35) & 0xff);
}
}
if (Str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) {
/*
* With the need to support larger (eg, 36-bit) integers, truncating to 32 bits is no longer helpful.
*
* value = v|0;
*/
if (shift) {
/*
* Since binary shifting is a logical operation, and since shifting by division only works properly
* with positive numbers, we must convert a negative value to a positive value, by computing the two's
* complement.
*/
if (v < 0) v += Math.pow(2, 36);
if (shift > 0) {
v *= Math.pow(2, shift);
} else {
v = Math.trunc(v / Math.pow(2, -shift));
}
}
value = v;
}
}
return value;
}
/**
* toBase(n, radix, cch, sPrefix, nGrouping)
*
* Displays the given number as an unsigned integer using the specified radix and number of digits.
*
* @param {number|null|undefined} n
* @param {number} radix (ie, the base)
* @param {number} cch (the desired number of digits)
* @param {string} [sPrefix] (default is none)
* @param {number} [nGrouping]
* @return {string}
*/
static toBase(n, radix, cch, sPrefix = "", nGrouping = 0)
{
/*
* An initial "falsey" check for null takes care of both null and undefined;
* we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough.
*
* Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN,
* since JavaScript coerces such operands to zero, but I think there's "value" in seeing those
* values displayed differently.
*/
var s = "";
if (isNaN(n)) {
n = null;
} else if (n != null) {
/*
* Callers that produced an input by dividing by a power of two rather than shifting (in order
* to access more than 32 bits) may produce a fractional result, which ordinarily we would simply
* ignore, but if the integer portion is zero and the sign is negative, we should probably treat
* this value as a sign-extension.
*/
if (n < 0 && n > -1) n = -1;
/*
* Negative values should be two's complemented according to the number of digits; for example,
* 12 octal digits implies an upper limit 8^12.
*/
if (n < 0) {
n += Math.pow(radix, cch);
}
if (n >= Math.pow(radix, cch)) {
cch = Math.ceil(Math.log(n) / Math.log(radix));
}
}
var g = nGrouping || -1;
while (cch-- > 0) {
if (!g) {
s = ',' + s;
g = nGrouping;
}
if (n == null) {
s = '?' + s;
} else {
var d = n % radix;
d += (d >= 0 && d <= 9? 0x30 : 0x41 - 10);
s = String.fromCharCode(d) + s;
n = Math.trunc(n / radix);
}
g--;
}
return sPrefix + s;
}
/**
* toBin(n, cch, nGrouping)
*
* Converts an integer to binary, with the specified number of digits (up to a maximum of 36).
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of binary digits (0 or undefined for default of either 8, 18, or 36)
* @param {number} [nGrouping]
* @return {string} the binary representation of n
*/
static toBin(n, cch, nGrouping)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN2) || 1;
var v = Math.abs(n);
if (v <= 0b11111111) {
cch = 8;
} else if (v <= 0b111111111111111111) {
cch = 18;
} else {
cch = 36;
}
} else if (cch > 36) cch = 36;
return Str.toBase(n, 2, cch, "", nGrouping);
}
/**
* toBinBytes(n, cb, fPrefix)
*
* Converts an integer to binary, with the specified number of bytes (up to the default of 4).
*
* @param {number|null|undefined} n (interpreted as a 32-bit value)
* @param {number} [cb] is the desired number of binary bytes (4 is both the default and the maximum)
* @param {boolean} [fPrefix]
* @return {string} the binary representation of n
*/
static toBinBytes(n, cb, fPrefix)
{
var s = "";
if (!cb || cb > 4) cb = 4;
for (var i = 0; i < cb; i++) {
if (s) s = ',' + s;
s = Str.toBin(n & 0xff, 8) + s;
n >>= 8;
}
return (fPrefix? "0b" : "") + s;
}
/**
* toOct(n, cch, fPrefix)
*
* Converts an integer to octal, with the specified number of digits (default of 6; max of 12)
*
* You might be tempted to use the built-in n.toString(8) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw
* an exception, whereas this function will return '?' characters.
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of octal digits (0 or undefined for default of either 6, 8, or 12)
* @param {boolean} [fPrefix]
* @return {string} the octal representation of n
*/
static toOct(n, cch, fPrefix)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(8)) || 1;
var v = Math.abs(n);
if (v <= 0o777777) {
cch = 6;
} else if (v <= 0o77777777) {
cch = 8;
} else {
cch = 12;
}
} else if (cch > 12) cch = 12;
return Str.toBase(n, 8, cch, fPrefix? "0o" : "");
}
/**
* toDec(n, cch)
*
* Converts an integer to decimal, with the specified number of digits (default of 5; max of 11)
*
* You might be tempted to use the built-in n.toString(10) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw
* an exception, whereas this function will return '?' characters.
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of decimal digits (0 or undefined for default of either 5 or 11)
* @return {string} the decimal representation of n
*/
static toDec(n, cch)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN10) || 1;
var v = Math.abs(n);
if (v <= 99999) {
cch = 5;
} else {
cch = 11;
}
} else if (cch > 11) cch = 11;
return Str.toBase(n, 10, cch);
}
/**
* toHex(n, cch, fPrefix)
*
* Converts an integer to hex, with the specified number of digits (default of 4 or 8, max of 9).
*
* You might be tempted to use the built-in n.toString(16) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values; for example, if n is -2147483647, then n.toString(16)
* will return "-7fffffff" instead of "80000001". Moreover, if n is undefined, n.toString() will
* throw an exception, whereas this function will return '?' characters.
*
* NOTE: The following work-around (adapted from code found on StackOverflow) would be another solution,
* taking care of negative values, zero-padding, and upper-casing, but not null/undefined/NaN values:
*
* s = (n < 0? n + 0x100000000 : n).toString(16);
* s = "00000000".substr(0, 8 - s.length) + s;
* s = s.substr(0, cch).toUpperCase();
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of hex digits (0 or undefined for default of either 4, 8, or 9)
* @param {boolean} [fPrefix]
* @return {string} the hex representation of n
*/
static toHex(n, cch, fPrefix)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(16)) || 1;
var v = Math.abs(n);
if (v <= 0xffff) {
cch = 4;
} else if (v <= 0xffffffff) {
cch = 8;
} else {
cch = 9;
}
} else if (cch > 9) cch = 9;
return Str.toBase(n, 16, cch, fPrefix? "0x" : "");
}
/**
* toHexByte(b)
*
* Alias for Str.toHex(b, 2, true)
*
* @param {number|null|undefined} b is a byte value
* @return {string} the hex representation of b
*/
static toHexByte(b)
{
return Str.toHex(b, 2, true);
}
/**
* toHexWord(w)
*
* Alias for Str.toHex(w, 4, true)
*
* @param {number|null|undefined} w is a word (16-bit) value
* @return {string} the hex representation of w
*/
static toHexWord(w)
{
return Str.toHex(w, 4, true);
}
/**
* toHexLong(l)
*
* Alias for Str.toHex(l, 8, true)
*
* @param {number|null|undefined} l is a dword (32-bit) value
* @return {string} the hex representation of w
*/
static toHexLong(l)
{
return Str.toHex(l, 8, true);
}
/**
* getBaseName(sFileName, fStripExt)
*
* This is a poor-man's version of Node's path.basename(), which Node-only components should use instead.
*
* Note that if fStripExt is true, this strips ANY extension, whereas path.basename() strips the extension only
* if it matches the second parameter (eg, path.basename("/foo/bar/baz/asdf/quux.html", ".html") returns "quux").
*
* @param {string} sFileName
* @param {boolean} [fStripExt]
* @return {string}
*/
static getBaseName(sFileName, fStripExt)
{
var sBaseName = sFileName;
var i = sFileName.lastIndexOf('/');
if (i >= 0) sBaseName = sFileName.substr(i + 1);
/*
* This next bit is a kludge to clean up names that are part of a URL that includes unsightly query parameters.
*/
i = sBaseName.indexOf('&');
if (i > 0) sBaseName = sBaseName.substr(0, i);
if (fStripExt) {
i = sBaseName.lastIndexOf(".");
if (i > 0) {
sBaseName = sBaseName.substring(0, i);
}
}
return sBaseName;
}
/**
* getExtension(sFileName)
*
* This is a poor-man's version of Node's path.extname(), which Node-only components should use instead.
*
* Note that we EXCLUDE the period from the returned extension, whereas path.extname() includes it.
*
* @param {string} sFileName
* @return {string} the filename's extension (in lower-case and EXCLUDING the "."), or an empty string
*/
static getExtension(sFileName)
{
var sExtension = "";
var i = sFileName.lastIndexOf(".");
if (i >= 0) {
sExtension = sFileName.substr(i + 1).toLowerCase();
}
return sExtension;
}
/**
* endsWith(s, sSuffix)
*
* @param {string} s
* @param {string} sSuffix
* @return {boolean} true if s ends with sSuffix, false if not
*/
static endsWith(s, sSuffix)
{
return s.indexOf(sSuffix, s.length - sSuffix.length) !== -1;
}
/**
* escapeHTML(sHTML)
*
* @param {string} sHTML
* @return {string} with HTML entities "escaped", similar to PHP's htmlspecialchars()
*/
static escapeHTML(sHTML)
{
return sHTML.replace(/[&<>"']/g, function(m)
{
return Str.aHTMLEscapeMap[m];
});
}
/**
* replace(sSearch, sReplace, s)
*
* The JavaScript replace() function ALWAYS interprets "$" specially in replacement strings, even when
* the search string is NOT a RegExp; specifically:
*
* $$ Inserts a "$"
* $& Inserts the matched substring
* $` Inserts the portion of the string that precedes the matched substring
* $' Inserts the portion of the string that follows the matched substring
* $n Where n is a positive integer less than 100, inserts the nth parenthesized sub-match string,
* provided the first argument was a RegExp object
*
* So, if a replacement string containing dollar signs passes through a series of replace() calls, untold
* problems could result. Hence, this function, which simply uses the replacement string as-is.
*
* Similar to the JavaScript replace() method (when sSearch is a string), this replaces only ONE occurrence
* (ie, the FIRST occurrence); it might be nice to add options to replace the LAST occurrence and/or ALL
* occurrences, but we'll revisit that later.
*
* @param {string} sSearch
* @param {string} sReplace
* @param {string} s
* @return {string}
*/
static replace(sSearch, sReplace, s)
{
var i = s.indexOf(sSearch);
if (i >= 0) {
s = s.substr(0, i) + sReplace + s.substr(i + sSearch.length);
}
return s;
}
/**
* replaceAll(sSearch, sReplace, s)
*
* @param {string} sSearch
* @param {string} sReplace
* @param {string} s
* @return {string}
*/
static replaceAll(sSearch, sReplace, s)
{
var a = {};
a[sSearch] = sReplace;
return Str.replaceArray(a, s);
}
/**
* replaceArray(a, s)
*
* @param {Object} a
* @param {string} s
* @return {string}
*/
static replaceArray(a, s)
{
var sMatch = "";
for (var k in a) {
/*
* As noted in:
*
* http://www.regexguru.com/2008/04/escape-characters-only-when-necessary/
*
* inside character classes, only backslash, caret, hyphen and the closing bracket need to be
* escaped. And in fact, if you ensure that the closing bracket is first, the caret is not first,
* and the hyphen is last, you can avoid escaping those as well.
*/
k = k.replace(/([\\[\]*{}().+?])/g, "\\$1");
sMatch += (sMatch? '|' : '') + k;
}
return s.replace(new RegExp('(' + sMatch + ')', "g"), function(m)
{
return a[m];
});
}
/**
* pad(s, cch, fPadLeft)
*
* NOTE: the maximum amount of padding currently supported is 40 spaces.
*
* @param {string} s is a string
* @param {number} cch is desired length
* @param {boolean} [fPadLeft] (default is padding on the right)
* @return {string} the original string (s) with spaces padding it to the specified length
*/
static pad(s, cch, fPadLeft)
{
var sPadding = " ";
return fPadLeft? (sPadding + s).slice(-cch) : (s + sPadding).slice(0, cch);
}
/**
* sprintf(format, ...)
*
* Copied from the CCjs project (/ccjs/lib/stdio.js) and extended. Far from complete let alone sprintf-compatible,
* but it's a start.
*
* @param {string} format
* @param {...} args
* @return {string}
*/
static sprintf(format, ...args)
{
var parts = format.split(/%([-+ 0#]?)([0-9]*)(\.?)([0-9]*)([hlL]?)([A-Za-z%])/);
var buffer = "";
var partIndex = 0;
for (var i = 0; i < args.length; i++) {
var arg = args[i], d, s;
buffer += parts[partIndex++];
var flags = parts[partIndex];
var minimum = +parts[partIndex+1] || 0;
var precision = +parts[partIndex+3] || 0;
var conversion = parts[partIndex+5];
switch(conversion) {
case 'd':
case 'f':
d = Math.trunc(arg);
s = d + "";
if (precision) {
minimum -= (precision + 1);
}
if (s.length < minimum) {
if (flags == '0') {
if (d < 0) minimum--;
s = ("0000000000" + Math.abs(d)).slice(-minimum);
if (d < 0) s = '-' + s;
} else {
s = (" " + s).slice(-minimum);
}
}
if (precision) {
d = Math.trunc((arg - Math.trunc(arg)) * Math.pow(10, precision));
s += '.' + ("0000000000" + Math.abs(d)).slice(-precision);
}
buffer += s;
break;
case 's':
buffer += arg;
break;
default:
/*
* The supported ANSI C set of conversions: "dioxXucsfeEgGpn%"
*/
buffer += "(unrecognized printf conversion %" + conversion + ")";
break;
}
partIndex += 6;
}
buffer += parts[partIndex];
return buffer;
}
/**
* stripLeadingZeros(s, fPad)
*
* @param {string} s
* @param {boolean} [fPad]
* @return {string}
*/
static stripLeadingZeros(s, fPad)
{
var cch = s.length;
s = s.replace(/^0+([0-9A-F]+)$/i, "$1");
if (fPad) s = Str.pad(s, cch, true);
return s;
}
/**
* trim(s)
*
* @param {string} s
* @return {string}
*/
static trim(s)
{
if (String.prototype.trim) {
return s.trim();
}
return s.replace(/^\s+|\s+$/g, "");
}
/**
* toASCIICode(b)
*
* @param {number} b
* @return {string}
*/
static toASCIICode(b)
{
var s;
if (b != Str.ASCII.CR && b != Str.ASCII.LF) {
s = Str.aASCIICodes[b];
}
if (s) {
s = '<' + s + '>';
} else {
s = String.fromCharCode(b);
}
return s;
}
}
Str.aHTMLEscapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
/*
* Future home of a general-purpose ASCII table. TODO: Flesh it out.
*/
Str.ASCII = {
LF: 0x0A,
CR: 0x0D
};
/*
* Table for converting "unprintable" ASCII codes into mnemonics, to more clearly see what's being printed.
*/
Str.aASCIICodes = {
0x00: "NUL",
0x01: "SOH", // (CTRL_A) Start of Heading
0x02: "STX", // (CTRL_B) Start of Text
0x03: "ETX", // (CTRL_C) End of Text
0x04: "EOT", // (CTRL_D) End of Transmission
0x05: "ENQ", // (CTRL_E) Enquiry
0x06: "ACK", // (CTRL_F) Acknowledge
0x07: "BEL", // (CTRL_G) Bell
0x08: "BS", // (CTRL_H) Backspace
0x09: "TAB", // (CTRL_I) Horizontal Tab
0x0A: "LF", // (CTRL_J) Line Feed (New Line)
0x0B: "VT", // (CTRL_K) Vertical Tab
0x0C: "FF", // (CTRL_L) Form Feed (New Page)
0x0D: "CR", // (CTRL_M) Carriage Return
0x0E: "SO", // (CTRL_N) Shift Out
0x0F: "SI", // (CTRL_O) Shift In
0x10: "DLE", // (CTRL_P) Data Link Escape
0x11: "XON", // (CTRL_Q) Device Control 1 (aka DC1)
0x12: "DC2", // (CTRL_R) Device Control 2
0x13: "XOFF", // (CTRL_S) Device Control 3 (aka DC3)
0x14: "DC4", // (CTRL_T) Device Control 4
0x15: "NAK", // (CTRL_U) Negative Acknowledge
0x16: "SYN", // (CTRL_V) Synchronous Idle
0x17: "ETB", // (CTRL_W) End of Transmission Block
0x18: "CAN", // (CTRL_X) Cancel
0x19: "EM", // (CTRL_Y) End of Medium
0x1A: "SUB", // (CTRL_Z) Substitute
0x1B: "ESC", // Escape
0x1C: "FS", // File Separator
0x1D: "GS", // Group Separator
0x1E: "RS", // Record Separator
0x1F: "US" // Unit Separator
};
Str.TYPES = {
NULL: 0,
BYTE: 1,
WORD: 2,
DWORD: 3,
NUMBER: 4,
STRING: 5,
BOOLEAN: 6,
OBJECT: 7,
ARRAY: 8
};
/**
* @copyright http://pcjs.org/modules/shared/lib/usrlib.js (C) Jeff Parsons 2012-2017
*/
/**
* @typedef {{
* mask: number,
* shift: number
* }}
*/
var BitField;
/**
* @typedef {Object.<BitField>}
*/
var BitFields;
class Usr {
/**
* binarySearch(a, v, fnCompare)
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v
* @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare]
* @return {number} the index of matching entry if non-negative, otherwise the index of the insertion point
*/
static binarySearch(a, v, fnCompare)
{
var left = 0;
var right = a.length;
var found = 0;
if (fnCompare === undefined) {
fnCompare = function(a, b)
{
return a > b ? 1 : a < b ? -1 : 0;
};
}
while (left < right) {
var middle = (left + right) >> 1;
var compareResult;
compareResult = fnCompare(v, a[middle]);
if (compareResult > 0) {
left = middle + 1;
} else {
right = middle;
found = !compareResult;
}
}
return found ? left : ~left;
}
/**
* binaryInsert(a, v, fnCompare)
*
* If element v already exists in array a, the array is unchanged (we don't allow duplicates); otherwise, the
* element is inserted into the array at the appropriate index.
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v is the value to insert
* @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare]
*/
static binaryInsert(a, v, fnCompare)
{
var index = Usr.binarySearch(a, v, fnCompare);
if (index < 0) {
a.splice(-(index + 1), 0, v);
}
}
/**
* getTimestamp()
*
* @return {string} timestamp containing the current date and time ("yyyy-mm-dd hh:mm:ss")
*/
static getTimestamp()
{
return Usr.formatDate("Y-m-d H:i:s");
}
/**
* getMonthDays(nMonth, nYear)
*
* Note that if we're being called on behalf of the RTC, its year is always truncated to two digits (mod 100),
* so we have no idea what century the year 0 might refer to. When using the normal leap-year formula, 0 fails
* the mod 100 test but passes the mod 400 test, so as far as the RTC is concerned, every century year is a leap
* year. Since we're most likely dealing with the year 2000, that's fine, since 2000 was also a leap year.
*
* TODO: There IS a separate CMOS byte that's supposed to be set to CMOS_ADDR.CENTURY_DATE; it's always BCD,
* so theoretically it will contain values like 0x19 or 0x20 (for the 20th and 21st centuries, respectively), and
* we could add that as another parameter to this function, to improve the accuracy, but that would go beyond what
* a real RTC actually does.
*
* @param {number} nMonth (1-12)
* @param {number} nYear (normally a 4-digit year, but it may also be mod 100)
* @return {number} the maximum (1-based) day allowed for the specified month and year
*/
static getMonthDays(nMonth, nYear)
{
var nDays = Usr.aMonthDays[nMonth - 1];
if (nDays == 28) {
if ((nYear % 4) === 0 && ((nYear % 100) || (nYear % 400) === 0)) {
nDays++;
}
}
return nDays;
}
/**
* formatDate(sFormat, date)
*
* @param {string} sFormat (eg, "F j, Y", "Y-m-d H:i:s")
* @param {Date} [date] (default is the current time)
* @return {string}
*
* Supported identifiers in sFormat include:
*
* a: lowercase ante meridiem and post meridiem (am or pm)
* d: day of the month, 2 digits with leading zeros (01,02,...,31)
* D: 3-letter day of the week ("Sun","Mon",...,"Sat")
* F: month ("January","February",...,"December")
* g: hour in 12-hour format, without leading zeros (1,2,...,12)
* h: hour in 24-hour format, without leading zeros (0,1,...,23)
* H: hour in 24-hour format, with leading zeros (00,01,...,23)
* i: minutes, with leading zeros (00,01,...,59)
* j: day of the month, without leading zeros (1,2,...,31)
* l: day of the week ("Sunday","Monday",...,"Saturday")
* m: month, with leading zeros (01,02,...,12)
* M: 3-letter month ("Jan","Feb",...,"Dec")
* n: month, without leading zeros (1,2,...,12)
* s: seconds, with leading zeros (00,01,...,59)
* y: 2-digit year (eg, 14)
* Y: 4-digit year (eg, 2014)
*
* For more inspiration, see: http://php.net/manual/en/function.date.php (of which we support ONLY a subset).
*/
static formatDate(sFormat, date)
{
var sDate = "";
if (!date) date = new Date();
var iHour = date.getHours();
var iDay = date.getDate();
var iMonth = date.getMonth() + 1;
for (var i = 0; i < sFormat.length; i++) {
var ch;
switch ((ch = sFormat.charAt(i))) {
case 'a':
sDate += (iHour < 12 ? "am" : "pm");
break;
case 'd':
sDate += ('0' + iDay).slice(-2);
break;
case 'D':
sDate += Usr.asDays[date.getDay()].substr(0, 3);
break;
case 'F':
sDate += Usr.asMonths[iMonth - 1];
break;
case 'g':
sDate += (!iHour ? 12 : (iHour > 12 ? iHour - 12 : iHour));
break;
case 'h':
sDate += iHour;
break;
case 'H':
sDate += ('0' + iHour).slice(-2);
break;
case 'i':
sDate += ('0' + date.getMinutes()).slice(-2);
break;
case 'j':
sDate += iDay;
break;
case 'l':
sDate += Usr.asDays[date.getDay()];
break;
case 'm':
sDate += ('0' + iMonth).slice(-2);
break;
case 'M':
sDate += Usr.asMonths[iMonth - 1].substr(0, 3);
break;
case 'n':
sDate += iMonth;
break;
case 's':
sDate += ('0' + date.getSeconds()).slice(-2);
break;
case 'y':
sDate += ("" + date.getFullYear()).slice(-2);
break;
case 'Y':
sDate += date.getFullYear();
break;
default:
sDate += ch;
break;
}
}
return sDate;
}
/**
* defineBitFields(bfs)
*
* Prepares a bit field definition for use with getBitField() and setBitField(); eg:
*
* var bfs = Usr.defineBitFields({num:20, count:8, btmod:1, type:3});
*
* The above defines a set of bit fields containing four fields: num (bits 0-19), count (bits 20-27), btmod (bit 28), and type (bits 29-31).
*
* Usr.setBitField(bfs.num, n, 1);
*
* The above set bit field "bfs.num" in numeric variable "n" to the value 1.
*
* @param {Object} bfs
* @return {BitFields}
*/
static defineBitFields(bfs)
{
var bit = 0;
for (var f in bfs) {
var width = bfs[f];
var mask = ((1 << width) - 1) << bit;
bfs[f] = {mask: mask, shift: bit};
bit += width;
}
return bfs;
}
/**
* initBitFields(bfs, ...)
*
* @param {BitFields} bfs
* @param {...number} var_args
* @return {number} a value containing all supplied bit fields
*/
static initBitFields(bfs, var_args)
{
var v = 0, i = 1;
for (var f in bfs) {
if (i >= arguments.length) break;
v = Usr.setBitField(bfs[f], v, arguments[i++]);
}
return v;
}
/**
* getBitField(bf, v)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @return {number} the value of the bit field in v defined by bf
*/
static getBitField(bf, v)
{
return (v & bf.mask) >> bf.shift;
}
/**
* setBitField(bf, v, n)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @param {number} n is a value to store in v in the bit field defined by bf
* @return {number} updated v
*/
static setBitField(bf, v, n)
{
return (v & ~bf.mask) | ((n << bf.shift) & bf.mask);
}
/**
* indexOf(a, t, i)
*
* Use this instead of Array.prototype.indexOf() if you can't be sure the browser supports it.
*
* @param {Array} a
* @param {*} t
* @param {number} [i]
* @returns {number}
*/
static indexOf(a, t, i)
{
if (Array.prototype.indexOf) {
return a.indexOf(t, i);
}
i = i || 0;
if (i < 0) i += a.length;
if (i < 0) i = 0;
for (var n = a.length; i < n; i++) {
if (i in a && a[i] === t) return i;
}
return -1;
}
}
Usr.asDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
Usr.asMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
Usr.aMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
/**
* getTime()
*
* @return {number} the current time, in milliseconds
*/
Usr.getTime = Date.now || function() { return +new Date(); };
/**
* @copyright http://pcjs.org/modules/shared/lib/weblib.js (C) Jeff Parsons 2012-2017
*/
/*
* According to http://www.w3schools.com/jsref/jsref_obj_global.asp, these are the *global* properties
* and functions of JavaScript-in-the-Browser:
*
* Property Description
* ---
* Infinity A numeric value that represents positive/negative infinity
* NaN "Not-a-Number" value
* undefined Indicates that a variable has not been assigned a value
*
* Function Description
* ---
* decodeURI() Decodes a URI
* decodeURIComponent() Decodes a URI component
* encodeURI() Encodes a URI
* encodeURIComponent() Encodes a URI component
* escape() Deprecated in version 1.5. Use encodeURI() or encodeURIComponent() instead
* eval() Evaluates a string and executes it as if it was script code
* isFinite() Determines whether a value is a finite, legal number
* isNaN() Determines whether a value is an illegal number
* Number() Converts an object's value to a number
* parseFloat() Parses a string and returns a floating point number
* parseInt() Parses a string and returns an integer
* String() Converts an object's value to a string
* unescape() Deprecated in version 1.5. Use decodeURI() or decodeURIComponent() instead
*
* And according to http://www.w3schools.com/jsref/obj_window.asp, these are the properties and functions
* of the *window* object.
*
* Property Description
* ---
* closed Returns a Boolean value indicating whether a window has been closed or not
* defaultStatus Sets or returns the default text in the statusbar of a window
* document Returns the Document object for the window (See Document object)
* frames Returns an array of all the frames (including iframes) in the current window
* history Returns the History object for the window (See History object)
* innerHeight Returns the inner height of a window's content area
* innerWidth Returns the inner width of a window's content area
* length Returns the number of frames (including iframes) in a window
* location Returns the Location object for the window (See Location object)
* name Sets or returns the name of a window
* navigator Returns the Navigator object for the window (See Navigator object)
* opener Returns a reference to the window that created the window
* outerHeight Returns the outer height of a window, including toolbars/scrollbars
* outerWidth Returns the outer width of a window, including toolbars/scrollbars
* pageXOffset Returns the pixels the current document has been scrolled (horizontally) from the upper left corner of the window
* pageYOffset Returns the pixels the current document has been scrolled (vertically) from the upper left corner of the window
* parent Returns the parent window of the current window
* screen Returns the Screen object for the window (See Screen object)
* screenLeft Returns the x coordinate of the window relative to the screen
* screenTop Returns the y coordinate of the window relative to the screen
* screenX Returns the x coordinate of the window relative to the screen
* screenY Returns the y coordinate of the window relative to the screen
* self Returns the current window
* status Sets or returns the text in the statusbar of a window
* top Returns the topmost browser window
*
* Method Description
* ---
* alert() Displays an alert box with a message and an OK button
* atob() Decodes a base-64 encoded string
* blur() Removes focus from the current window
* btoa() Encodes a string in base-64
* clearInterval() Clears a timer set with setInterval()
* clearTimeout() Clears a timer set with setTimeout()
* close() Closes the current window
* confirm() Displays a dialog box with a message and an OK and a Cancel button
* createPopup() Creates a pop-up window
* focus() Sets focus to the current window
* moveBy() Moves a window relative to its current position
* moveTo() Moves a window to the specified position
* open() Opens a new browser window
* print() Prints the content of the current window
* prompt() Displays a dialog box that prompts the visitor for input
* resizeBy() Resizes the window by the specified pixels
* resizeTo() Resizes the window to the specified width and height
* scroll() This method has been replaced by the scrollTo() method.
* scrollBy() Scrolls the content by the specified number of pixels
* scrollTo() Scrolls the content to the specified coordinates
* setInterval() Calls a function or evaluates an expression at specified intervals (in milliseconds)
* setTimeout() Calls a function or evaluates an expression after a specified number of milliseconds
* stop() Stops the window from loading
*/
class Web {
/**
* log(s, type)
*
* For diagnostic output only. DEBUG must be true (or "--debug" specified via the command-line)
* for Component.log() to display anything.
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
static log(s, type)
{
Component.log(s, type);
}
/**
* notice(s, fPrintOnly, id)
*
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
*/
static notice(s, fPrintOnly, id)
{
Component.notice(s, fPrintOnly, id);
}
/**
* getResource(sURL, dataPost, fAsync, done, progress)
*
* Request the specified resource (sURL), and once the request is complete, notify done().
*
* If fAsync is true, a done() callback should ALWAYS be supplied; otherwise, you'll have no
* idea when the request is complete or what the response was. done() is passed three parameters:
*
* done(sURL, sResource, nErrorCode)
*
* If nErrorCode is zero, sResource should contain the requested data; otherwise, an error occurred.
*
* If dataPost is set to a string, that string can be used to control the response format;
* by default, the response format is plain text, but you can specify "bytes" to request arbitrary
* binary data, which should come back as a string of bytes.
*
* TODO: The "bytes" option works by calling overrideMimeType(), which was never a best practice.
* Instead, we should implement supported response types ("text" and "arraybuffer", at a minimum)
* by setting xmlHTTP.responseType to one of those values before calling xmlHTTP.send().
*
* @param {string} sURL
* @param {string|Object|null} [dataPost] for a POST request (default is a GET request)
* @param {boolean} [fAsync] is true for an asynchronous request
* @param {function(string,string,number)} [done]
* @param {function(number)} [progress]
* @return {Array|null} Array containing [sResource, nErrorCode], or null if no response yet
*/
static getResource(sURL, dataPost, fAsync = false, done, progress)
{
var nErrorCode = 0, sResource = null, response = null;
if (typeof resources == 'object' && (sResource = resources[sURL])) {
if (done) done(sURL, sResource, nErrorCode);
return [sResource, nErrorCode];
}
else if (fAsync && typeof resources == 'function') {
resources(sURL, function(sResource, nErrorCode)
{
if (done) done(sURL, sResource, nErrorCode);
});
return response;
}
if (DEBUG) {
/*
* The larger resources we put on archive.pcjs.org should also be available locally.
*
* NOTE: "http://archive.pcjs.org" is now "https://s3-us-west-2.amazonaws.com/archive.pcjs.org"
*/
sURL = sURL.replace(/^(http:\/\/archive\.pcjs\.org|https:\/\/s3-us-west-2\.amazonaws\.com\/archive\.pcjs\.org)(\/.*)\/([^\/]*)$/, "$2/archive/$3");
}
var xmlHTTP = (window.XMLHttpRequest? new window.XMLHttpRequest() : new window.ActiveXObject("Microsoft.XMLHTTP"));
if (fAsync) {
xmlHTTP.onreadystatechange = function()
{
if (xmlHTTP.readyState !== 4) {
if (progress) progress(1);
return;
}
/*
* The following line was recommended for WebKit, as a work-around to prevent the handler firing multiple
* times when debugging. Unfortunately, that's not the only XMLHttpRequest problem that occurs when
* debugging, so I think the WebKit problem is deeper than that. When we have multiple XMLHttpRequests
* pending, any debugging activity means most of them simply get dropped on floor, so what may actually be
* happening are mis-notifications rather than redundant notifications.
*
* xmlHTTP.onreadystatechange = undefined;
*/
sResource = xmlHTTP.responseText;
/*
* The normal "success" case is an HTTP status code of 200, but when testing with files loaded
* from the local file system (ie, when using the "file:" protocol), we have to be a bit more "flexible".
*/
if (xmlHTTP.status == 200 || !xmlHTTP.status && sResource.length && Web.getHostProtocol() == "file:") {
if (MAXDEBUG) Web.log("xmlHTTP.onreadystatechange(" + sURL + "): returned " + sResource.length + " bytes");
}
else {
nErrorCode = xmlHTTP.status || -1;
Web.log("xmlHTTP.onreadystatechange(" + sURL + "): error code " + nErrorCode);
}
if (progress) progress(2);
if (done) done(sURL, sResource, nErrorCode);
};
}
if (progress) progress(0);
if (dataPost && typeof dataPost == "object") {
var sDataPost = "";
for (var p in dataPost) {
if (!dataPost.hasOwnProperty(p)) continue;
if (sDataPost) sDataPost += "&";
sDataPost += p + '=' + encodeURIComponent(dataPost[p]);
}
sDataPost = sDataPost.replace(/%20/g, '+');
if (MAXDEBUG) Web.log("Web.getResource(POST " + sURL + "): " + sDataPost.length + " bytes");
xmlHTTP.open("POST", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it)
xmlHTTP.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlHTTP.send(sDataPost);
} else {
if (MAXDEBUG) Web.log("Web.getResource(GET " + sURL + ")");
xmlHTTP.open("GET", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it)
if (dataPost == "bytes") {
xmlHTTP.overrideMimeType("text/plain; charset=x-user-defined");
}
xmlHTTP.send();
}
if (!fAsync) {
sResource = xmlHTTP.responseText;
if (xmlHTTP.status == 200) {
if (MAXDEBUG) Web.log("Web.getResource(" + sURL + "): returned " + sResource.length + " bytes");
} else {
nErrorCode = xmlHTTP.status || -1;
Web.log("Web.getResource(" + sURL + "): error code " + nErrorCode);
}
if (done) done(sURL, sResource, nErrorCode);
response = [sResource, nErrorCode];
}
return response;
}
/**
* 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)
*/
static parseMemoryResource(sURL, sData)
{
var i;
var resource = {
aBytes: null,
aSymbols: null,
addrLoad: null,
addrExec: null
};
if (sData.charAt(0) == "[" || sData.charAt(0) == "{") {
try {
var a, ib, data;
if (sData.substr(0, 1) == "<") { // if the "data" begins with a "<"...
/*
* Early server configs reported an error (via the nErrorCode parameter) if a tape URL was invalid,
* but more recent server configs now display a somewhat friendlier HTML error page. The downside,
* however, is that the original error has been buried, and we've received "data" that isn't actually
* tape data. So if the data we've received appears to be "HTML-like", we treat it as an error message.
*/
throw new Error(sData);
}
/*
* TODO: IE9 is rather unfriendly and restrictive with regard to how much data it's willing to
* eval(). In particular, the 10Mb disk image we use for the Windows 1.01 demo config fails in
* IE9 with an "Out of memory" exception. One work-around would be to chop the data into chunks
* (perhaps one track per chunk, using regular expressions) and then manually re-assemble it.
*
* However, it turns out that using JSON.parse(sDiskData) instead of eval("(" + sDiskData + ")")
* is a much easier fix. The only drawback is that we must first quote any unquoted property names
* and remove any comments, because while eval() was cool with them, JSON.parse() is more particular;
* the following RegExp replacements take care of those requirements.
*
* The use of hex values is something else that eval() was OK with, but JSON.parse() is not, and
* while I've stopped using hex values in DumpAPI responses (at least when "format=json" is specified),
* I can't guarantee they won't show up in "legacy" images, and there's no simple RegExp replacement
* for transforming hex values into decimal values, so I cop out and fall back to eval() if I detect
* any hex prefixes ("0x") in the sequence. Ditto for error messages, which appear like so:
*
* ["unrecognized disk path: test.img"]
*/
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 + ")");
}
resource.addrLoad = data['load'];
resource.addrExec = data['exec'];
if (a = data['bytes']) {
resource.aBytes = a;
}
else if (a = data['words']) {
/*
* Convert all words into bytes
*/
resource.aBytes = new Array(a.length * 2);
for (i = 0, ib = 0; i < a.length; i++) {
resource.aBytes[ib++] = a[i] & 0xff;
resource.aBytes[ib++] = (a[i] >> 8) & 0xff;
}
}
else if (a = data['longs']) {
/*
* Convert all dwords (longs) into bytes
*/
resource.aBytes = new Array(a.length * 4);
for (i = 0, ib = 0; i < a.length; i++) {
resource.aBytes[ib++] = a[i] & 0xff;
resource.aBytes[ib++] = (a[i] >> 8) & 0xff;
resource.aBytes[ib++] = (a[i] >> 16) & 0xff;
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'];
} catch (e) {
Component.error("Resource data error (" + sURL + "): " + e.message);
resource = null;
}
}
else {
/*
* Parse the data manually; we assume it's a series of hex byte-values separated by whitespace.
*/
var ab = [];
var sHexData = sData.replace(/\n/gm, " ").replace(/ +$/, "");
var asHexData = sHexData.split(" ");
for (i = 0; i < asHexData.length; i++) {
var n = parseInt(asHexData[i], 16);
if (isNaN(n)) {
Component.error("Resource data error (" + sURL + "): invalid hex byte (" + asHexData[i] + ")");
break;
}
ab.push(n & 0xff);
}
if (i == asHexData.length) resource.aBytes = ab;
}
return resource;
}
/**
* sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)
*
* Send a report (eg, bug report) to the server.
*
* @param {string} sApp (eg, "PCjs")
* @param {string} sVer (eg, "1.02")
* @param {string} sURL (eg, "/devices/pc/machine/5150/mda/64kb/machine.xml")
* @param {string} sUser (ie, the user key, if any)
* @param {string} sType (eg, "bug"); one of ReportAPI.TYPE.*
* @param {string} sReport (eg, unparsed state data)
* @param {string} [sHostName] (default is http://SITEHOST)
*/
static sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)
{
var dataPost = {};
dataPost[ReportAPI.QUERY.APP] = sApp;
dataPost[ReportAPI.QUERY.VER] = sVer;
dataPost[ReportAPI.QUERY.URL] = sURL;
dataPost[ReportAPI.QUERY.USER] = sUser;
dataPost[ReportAPI.QUERY.TYPE] = sType;
dataPost[ReportAPI.QUERY.DATA] = sReport;
var sReportURL = (sHostName? sHostName : "http://" + SITEHOST) + ReportAPI.ENDPOINT;
Web.getResource(sReportURL, dataPost, true);
}
/**
* getHost()
*
* @return {string}
*/
static getHost()
{
return ("http://" + (window? window.location.host : SITEHOST));
}
/**
* getHostURL()
*
* @return {string|null}
*/
static getHostURL()
{
return (window? window.location.href : null);
}
/**
* getHostProtocol()
*
* @return {string}
*/
static getHostProtocol()
{
return (window? window.location.protocol : "file:");
}
/**
* getUserAgent()
*
* @return {string}
*/
static getUserAgent()
{
return (window? window.navigator.userAgent : "");
}
/**
* hasLocalStorage
*
* true if localStorage support exists, is enabled, and works; false otherwise
*
* @return {boolean}
*/
static hasLocalStorage()
{
if (Web.fLocalStorage == null) {
var f = false;
if (window) {
try {
window.localStorage.setItem(Web.sLocalStorageTest, Web.sLocalStorageTest);
f = (window.localStorage.getItem(Web.sLocalStorageTest) == Web.sLocalStorageTest);
window.localStorage.removeItem(Web.sLocalStorageTest);
} catch (e) {
Web.logLocalStorageError(e);
f = false;
}
}
Web.fLocalStorage = f;
}
return Web.fLocalStorage;
}
/**
* logLocalStorageError(e)
*
* @param {Error} e is an exception
*/
static logLocalStorageError(e)
{
Web.log(e.message, "localStorage error");
}
/**
* getLocalStorageItem(sKey)
*
* Returns the requested key value, or null if the key does not exist, or undefined if localStorage is not available
*
* @param {string} sKey
* @return {string|null|undefined} sValue
*/
static getLocalStorageItem(sKey)
{
var sValue;
if (window) {
try {
sValue = window.localStorage.getItem(sKey);
} catch (e) {
Web.logLocalStorageError(e);
}
}
return sValue;
}
/**
* setLocalStorageItem(sKey, sValue)
*
* @param {string} sKey
* @param {string} sValue
* @return {boolean} true if localStorage is available, false if not
*/
static setLocalStorageItem(sKey, sValue)
{
try {
window.localStorage.setItem(sKey, sValue);
return true;
} catch (e) {
Web.logLocalStorageError(e);
}
return false;
}
/**
* removeLocalStorageItem(sKey)
*
* @param {string} sKey
*/
static removeLocalStorageItem(sKey)
{
try {
window.localStorage.removeItem(sKey);
} catch (e) {
Web.logLocalStorageError(e);
}
}
/**
* getLocalStorageKeys()
*
* @return {Array}
*/
static getLocalStorageKeys()
{
var a = [];
try {
for (var i = 0, c = window.localStorage.length; i < c; i++) {
a.push(window.localStorage.key(i));
}
} catch (e) {
Web.logLocalStorageError(e);
}
return a;
}
/**
* reloadPage()
*/
static reloadPage()
{
if (window) window.location.reload();
}
/**
* isUserAgent(s)
*
* Check the browser's user-agent string for the given substring; "iOS" and "MSIE" are special values you can
* use that will match any iOS or MSIE browser, respectively (even IE11, in the case of "MSIE").
*
* 2013-11-06: In a questionable move, MSFT changed the user-agent reported by IE11 on Windows 8.1, eliminating
* the "MSIE" string (which MSDN calls a "version token"; see http://msdn.microsoft.com/library/ms537503.aspx);
* they say "public websites should rely on feature detection, rather than browser detection, in order to design
* their sites for browsers that don't support the features used by the website." So, in IE11, we get a user-agent
* that tries to fool apps into thinking the browser is more like WebKit or Gecko:
*
* Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko
*
* That's a nice idea, but in the meantime, they hosed the XSL transform code in embed.js, which contained
* some very critical browser-specific code; turning on IE's "Compatibility Mode" didn't help either, because
* that's a sledgehammer solution which restores the old user-agent string but also disables other features like
* HTML5 canvas support. As an interim solution, I'm treating any "MSIE" check as a check for either "MSIE" or
* "Trident".
*
* UPDATE: I've since found ways to make the code in embed.js more browser-agnostic, so for now, there's isn't
* any code that cares about "MSIE", but I've left the change in place, because I wouldn't be surprised if I'll
* need more IE-specific code in the future, perhaps for things like copy/paste functionality, or mouse capture.
*
* @param {string} s is a substring to search for in the user-agent; as noted above, "iOS" and "MSIE" are special values
* @return {boolean} is true if the string was found, false if not
*/
static isUserAgent(s)
{
if (window) {
var userAgent = Web.getUserAgent();
/*
* Here's one case where we have to be careful with Component, because when isUserAgent() is called by
* the init code below, component.js hasn't been loaded yet. The simple solution for now is to remove the call.
*
* Web.log("agent: " + userAgent);
*
* And yes, it would be pointless to use the conditional (?) operator below, if not for the Google Closure
* Compiler (v20130823) failing to detect the entire expression as a boolean.
*/
return s == "iOS" && !!userAgent.match(/(iPod|iPhone|iPad)/) && !!userAgent.match(/AppleWebKit/) || s == "MSIE" && !!userAgent.match(/(MSIE|Trident)/) || (userAgent.indexOf(s) >= 0);
}
return false;
}
/**
* isMobile()
*
* Check the browser's user-agent string for the substring "Mobi", as per Mozilla recommendation:
*
* https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent
*
* @return {boolean} is true if the browser appears to be a mobile (ie, non-desktop) web browser, false if not
*/
static isMobile()
{
return Web.isUserAgent("Mobi");
}
/**
* findProperty(obj, sProp, sSuffix)
*
* If both sProp and sSuffix are set, then any browser-specific prefixes are inserted between sProp and sSuffix,
* and if a match is found, it is returned without sProp.
*
* For example, if findProperty(document, 'on', 'fullscreenchange') discovers that 'onwebkitfullscreenchange' exists,
* it will return 'webkitfullscreenchange', in preparation for an addEventListener() call.
*
* More commonly, sSuffix is not used, so whatever property is found is returned as-is.
*
* @param {Object|null|undefined} obj
* @param {string} sProp
* @param {string} [sSuffix]
* @return {string|null}
*/
static findProperty(obj, sProp, sSuffix)
{
if (obj) {
for (var i = 0; i < Web.asBrowserPrefixes.length; i++) {
var sName = Web.asBrowserPrefixes[i];
if (sSuffix) {
sName += sSuffix;
var sEvent = sProp + sName;
if (sEvent in obj) return sName;
} else {
if (!sName) {
sName = sProp[0];
} else {
sName += sProp[0].toUpperCase();
}
sName += sProp.substr(1);
if (sName in obj) return sName;
}
}
}
return null;
}
/**
* getURLParm(sParm)
*
* First looks for sParm exactly as specified, then looks for the lower-case version.
*
* @param {string} sParm
* @return {string|undefined}
*/
static getURLParm(sParm)
{
if (!Web.parmsURL) {
Web.parmsURL = Web.parseURLParms();
}
return Web.parmsURL[sParm] || Web.parmsURL[sParm.toLowerCase()];
}
/**
* parseURLParms(sParms)
*
* @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?')
* @return {Object} containing properties for each parameter found
*/
static parseURLParms(sParms)
{
var aParms = {};
if (window) { // an alternative to "if (typeof module === 'undefined')" if require("defines") was used
if (!sParms) {
/*
* Note that window.location.href returns the entire URL, whereas window.location.search
* returns only the parameters, if any (starting with the '?', which we skip over with a substr() call).
*/
sParms = window.location.search.substr(1);
}
var match;
var pl = /\+/g; // RegExp for replacing addition symbol with a space
var search = /([^&=]+)=?([^&]*)/g;
var decode = function(s)
{
return decodeURIComponent(s.replace(pl, " "));
};
while ((match = search.exec(sParms))) {
aParms[decode(match[1])] = decode(match[2]);
}
}
return aParms;
}
/**
* downloadFile(sData, sType, fBase64, sFileName)
*
* @param {string} sData
* @param {string} sType
* @param {boolean} [fBase64]
* @param {string} [sFileName]
*/
static downloadFile(sData, sType, fBase64, sFileName)
{
var link = null, sAlert;
var sURI = "data:application/" + sType + (fBase64? ";base64" : "") + ",";
if (!Web.isUserAgent("Firefox")) {
sURI += (fBase64? sData : encodeURI(sData));
} else {
sURI += (fBase64? sData : encodeURIComponent(sData));
}
if (sFileName) {
link = document.createElement('a');
if (typeof link.download != 'string') link = null;
}
if (link) {
link.href = sURI;
link.download = sFileName;
document.body.appendChild(link); // Firefox allegedly requires the link to be in the body
link.click();
document.body.removeChild(link);
sAlert = 'Check your Downloads folder for ' + sFileName + '.';
} else {
window.open(sURI);
sAlert = 'Check your browser for a new window/tab containing the requested data' + (sFileName? (' (' + sFileName + ')') : '') + '.';
}
return sAlert;
}
/**
* onCountRepeat(n, fnRepeat, fnComplete, msDelay)
*
* Call fnRepeat() n times with an msDelay millisecond delay between calls,
* then call fnComplete() when n has been exhausted OR fnRepeat() returns false.
*
* @param {number} n
* @param {function()} fnRepeat
* @param {function()} fnComplete
* @param {number} [msDelay]
*/
static onCountRepeat(n, fnRepeat, fnComplete, msDelay)
{
var fnTimeout = function doCountRepeat()
{
n -= 1;
if (n >= 0) {
if (!fnRepeat()) n = 0;
}
if (n > 0) {
setTimeout(fnTimeout, msDelay || 0);
return;
}
fnComplete();
};
fnTimeout();
}
/**
* onClickRepeat(e, msDelay, msRepeat, fn)
*
* Repeatedly call fn() with an initial msDelay, and an msRepeat delay thereafter,
* as long as HTML control Object e has an active "down" event and fn() returns true.
*
* @param {Object} e
* @param {number} msDelay
* @param {number} msRepeat
* @param {function(boolean)} fn is passed false on the first call, true on all repeated calls
*/
static onClickRepeat(e, msDelay, msRepeat, fn)
{
var ms = 0, timer = null, fIgnoreMouseEvents = false;
var fnRepeat = function doClickRepeat()
{
if (fn(ms === msRepeat)) {
timer = setTimeout(fnRepeat, ms);
ms = msRepeat;
}
};
e.onmousedown = function()
{
// Web.log("onMouseDown()");
if (!fIgnoreMouseEvents) {
if (!timer) {
ms = msDelay;
fnRepeat();
}
}
};
e.ontouchstart = function()
{
// Web.log("onTouchStart()");
if (!timer) {
ms = msDelay;
fnRepeat();
}
};
e.onmouseup = e.onmouseout = function()
{
// Web.log("onMouseUp()/onMouseOut()");
if (timer) {
clearTimeout(timer);
timer = null;
}
};
e.ontouchend = e.ontouchcancel = function()
{
// Web.log("onTouchEnd()/onTouchCancel()");
if (timer) {
clearTimeout(timer);
timer = null;
}
/*
* Devices that generate ontouch* events ALSO generate onmouse* events,
* and generally do so immediately after all the touch events are complete,
* so unless we want double the action, we need to ignore mouse events.
*/
fIgnoreMouseEvents = true;
};
}
/**
* onPageEvent(sName, fn)
*
* For 'onload', 'onunload', and 'onpageshow' events, most callers should NOT use this function, but
* instead use Web.onInit(), Web.onShow(), and Web.onExit(), respectively.
*
* The only components that should still use onPageEvent() are THIS component (see the bottom of this file)
* and components that need to capture other events (eg, the 'onresize' event in the Video component).
*
* This function creates a chain of callbacks, allowing multiple JavaScript modules to define handlers
* for the same event, which wouldn't be possible if everyone modified window['onload'], window['onunload'],
* etc, themselves. However, that's less of a concern now, because assuming everyone else is now using
* onInit(), onExit(), etc, then there really IS only one component setting the window callback: this one.
*
* NOTE: It's risky to refer to obscure event handlers with "dot" names, because the Closure Compiler may
* erroneously replace them (eg, window.onpageshow is a good example).
*
* @param {string} sFunc
* @param {function()} fn
*/
static onPageEvent(sFunc, fn)
{
if (window) {
var fnPrev = window[sFunc];
if (typeof fnPrev !== 'function') {
window[sFunc] = fn;
} else {
/*
* TODO: Determine whether there's any value in receiving/sending the Event object that the
* browser provides when it generates the original event.
*/
window[sFunc] = function onWindowEvent()
{
if (fnPrev) fnPrev();
fn();
};
}
}
};
/**
* onInit(fn)
*
* Use this instead of setting window.onload. Allows multiple JavaScript modules to define their own 'onload' event handler.
*
* @param {function()} fn
*/
static onInit(fn)
{
Web.aPageEventHandlers['init'].push(fn);
};
/**
* onShow(fn)
*
* @param {function()} fn
*
* Use this instead of setting window.onpageshow. Allows multiple JavaScript modules to define their own 'onpageshow' event handler.
*/
static onShow(fn)
{
Web.aPageEventHandlers['show'].push(fn);
};
/**
* onExit(fn)
*
* @param {function()} fn
*
* Use this instead of setting window.onunload. Allows multiple JavaScript modules to define their own 'onunload' event handler.
*/
static onExit(fn)
{
Web.aPageEventHandlers['exit'].push(fn);
};
/**
* doPageEvent(afn)
*
* @param {Array.<function()>} afn
*/
static doPageEvent(afn)
{
if (Web.fPageEventsEnabled) {
try {
for (var i = 0; i < afn.length; i++) {
afn[i]();
}
} catch (e) {
Web.notice("An unexpected exception occurred:\n\n" + e.message + "\n\nPlease send this information to support@pcjs.org. Thanks.");
}
}
};
/**
* enablePageEvents(fEnable)
*
* @param {boolean} fEnable is true to enable page events, false to disable (they're enabled by default)
*/
static enablePageEvents(fEnable)
{
if (!Web.fPageEventsEnabled && fEnable) {
Web.fPageEventsEnabled = true;
if (Web.fPageLoaded) Web.sendPageEvent('init');
if (Web.fPageShowed) Web.sendPageEvent('show');
return;
}
Web.fPageEventsEnabled = fEnable;
}
/**
* sendPageEvent(sEvent)
*
* This allows us to manually trigger page events.
*
* @param {string} sEvent (one of 'init', 'show' or 'exit')
*/
static sendPageEvent(sEvent)
{
if (Web.aPageEventHandlers[sEvent]) {
Web.doPageEvent(Web.aPageEventHandlers[sEvent]);
}
}
}
Web.parmsURL = null; // initialized on first call to parseURLParms()
Web.aPageEventHandlers = {
'init': [], // list of window 'onload' handlers
'show': [], // list of window 'onpageshow' handlers
'exit': [] // list of window 'onunload' handlers (although we prefer to use 'onbeforeunload' if possible)
};
Web.asBrowserPrefixes = ['', 'moz', 'ms', 'webkit'];
Web.fPageLoaded = false; // set once the page's first 'onload' event has occurred
Web.fPageShowed = false; // set once the page's first 'onpageshow' event has occurred
Web.fPageEventsEnabled = true; // default is true, set to false (or true) by enablePageEvents()
/**
* fLocalStorage
*
* true if localStorage support exists, is enabled, and works; "falsey" otherwise
*
* @type {boolean|null}
*/
Web.fLocalStorage = null;
/**
* TODO: Is there any way to get the Closure Compiler to stop inlining this string? This isn't cutting it.
*
* @const {string}
*/
Web.sLocalStorageTest = "PCjs.localStorage";
Web.onPageEvent('onload', function onPageLoad() {
Web.fPageLoaded = true;
Web.doPageEvent(Web.aPageEventHandlers['init']);
});
Web.onPageEvent('onpageshow', function onPageShow() {
Web.fPageShowed = true;
Web.doPageEvent(Web.aPageEventHandlers['show']);
});
Web.onPageEvent(Web.isUserAgent("iOS")? 'onpagehide' : (Web.isUserAgent("Opera")? 'onunload' : 'onbeforeunload'), function onPageUnload() {
Web.doPageEvent(Web.aPageEventHandlers['exit']);
});
/**
* @copyright http://pcjs.org/modules/shared/lib/component.js (C) Jeff Parsons 2012-2017
*/
/*
* All PCjs components now use JSDoc types, primarily so that Google's Closure Compiler will compile
* everything with zero warnings when ADVANCED_OPTIMIZATIONS are enabled. For more information about
* the JSDoc types supported by the Closure Compiler:
*
* https://developers.google.com/closure/compiler/docs/js-for-compiler#types
*
* I also attempted to validate this code with JSLint, but it complained too much; eg, it didn't like
* "while (true)", a tried and "true" programming convention for decades, and it wanted me to replace
* all "++" and "--" operators with "+= 1" and "-= 1", use "(s || '')" instead of "(s? s : '')", etc.
*
* I prefer sticking with traditional C-style idioms, in part because they are more portable. That
* does NOT mean I'm trying to write "portable JavaScript," but some of this code was ported from C code
* I'd written long ago, so portability is good, and I'm not going to throw that away if there's no need.
*
* UPDATE: I've since switched from JSLint to JSHint, which seems to have more reasonable defaults.
* And for new code, I have adopted some popular JavaScript idioms, like "(s || '')", although the need
* for those kinds of expressions will be reduced as I also start adopting some ES6 features, like
* default parameters.
*/
/**
* Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default,
* it deters us from defining named properties on our components; eg:
*
* this['exports'] = {...}
*
* results in an error:
*
* Cannot do '[]' access on a struct
*
* So, in order to define 'exports', we must override the @struct assumption by annotating
* the class as @unrestricted (or @dict). Note that this must be done both here and in the
* subclass (eg, SerialPort), because otherwise the Compiler won't allow us to *reference*
* the named property either.
*
* TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to
* define every single property the class uses in its constructor, which results in a fair
* bit of redundant initialization, since many properties aren't (and don't need to be) fully
* initialized until the appropriate init(), reset(), restore(), etc. function is called.
*
* The upside, however, may be that since the structure of the class is completely defined by
* the constructor, JavaScript engines may be able to optimize and run more efficiently.
*
* @unrestricted
*/
class Component {
/**
* Component(type, parms, bitsMessage)
*
* A Component object requires:
*
* type: a user-defined type name (eg, "CPU")
*
* and accepts any or all of the following (parms) properties:
*
* id: component ID (default is "")
* name: component name (default is ""; if blank, toString() will use the type name only)
* comment: component comment string (default is undefined)
*
* Component subclasses will usually have additional (parms) properties.
*
* @param {string} type
* @param {Object} [parms]
* @param {number} [bitsMessage] selects message(s) that the component wants to enable (default is 0)
*/
constructor(type, parms, bitsMessage)
{
this.type = type;
if (!parms) parms = {'id': "", 'name': ""};
this.id = parms['id'] || "";
this.name = parms['name'];
this.comment = parms['comment'];
this.parms = parms;
/*
* The following Component properties need to be accessible by other machines and/or command scripts;
* well, OK, or we could have exported some new functions to walk the contents of these properties, as we
* did with findMachineComponent(), but this works just as well.
*
* Also, while the double-assignment looks silly (ie, using both dot and bracket property notation), it
* resolves a complaint from the Closure Compiler, because if we use ONLY bracket notation here, then the
* Compiler wants us to change all the other references to bracket notation as well.
*/
this.exports = this['exports'] = {};
this.bindings = this['bindings'] = {};
var i = this.id.indexOf('.');
if (i < 0) {
this.idComponent = this.id;
} else {
this.idMachine = this.id.substr(0, i);
this.idComponent = this.id.substr(i + 1);
}
/*
* Gather all the various component flags (booleans) into a single "flags" object, and encourage
* subclasses to do the same, to reduce the property clutter we have to wade through while debugging.
*/
this.flags = {
ready: false,
busy: false,
busyCancel: false,
initDone: false,
powered: false,
unloading: false,
error: false
};
this.fnReady = null;
this.clearError();
this.bitsMessage = bitsMessage || 0;
this.cmp = null;
this.bus = null;
this.cpu = null;
this.dbg = null;
/*
* TODO: Consider adding another parameter to the Component() constructor that allows components to tell
* us if they support single or multiple instances per machine. For example, there can be multiple SerialPort
* components per machine, but only one CPU component (some machines also support an FPU, but that component
* is considered separate from the CPU).
*
* It's not critical, but it would help catch machine configuration errors; for example, a machine that mistakenly
* includes two CPU components may, aside from wasting memory, end up with odd side-effects, like unresponsive
* CPU controls.
*/
Component.add(this);
}
/**
* Component.add(component)
*
* @param {Component} component
*/
static add(component)
{
/*
* This just generates a lot of useless noise, handy in the early days, not so much these days....
*
* if (DEBUG) Component.log("Component.add(" + component.type + "," + component.id + ")");
*/
Component.components.push(component);
}
/**
* Component.addMachine(idMachine)
*
* @param {string} idMachine
*/
static addMachine(idMachine)
{
Component.machines[idMachine] = {};
}
/**
* Component.addMachineResource(idMachine, sName, data)
*
* @param {string} idMachine
* @param {string|null} sName (name of the resource)
* @param {*} data
*/
static addMachineResource(idMachine, sName, data)
{
/*
* I used to assert(Component.machines[idMachine]), but when we're running as a Node app, embed.js is not used,
* so addMachine() is never called, so resources do not need to be recorded.
*/
if (Component.machines[idMachine] && sName) {
Component.machines[idMachine][sName] = data;
}
}
/**
* Component.getMachineResources(idMachine)
*
* @param {string} idMachine
* @return {Object|undefined}
*/
static getMachineResources(idMachine)
{
return Component.machines[idMachine];
}
/**
* Component.getTime()
*
* @return {number} the current time, in milliseconds
*/
static getTime()
{
return Date.now() || +new Date();
}
/**
* Component.log(s, type)
*
* For diagnostic output only.
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
static log(s, type)
{
if (!COMPILED) {
if (s) {
var sElapsed = "", sMsg = (type? (type + ": ") : "") + s;
if (typeof Usr != "undefined") {
if (Component.msStart === undefined) {
Component.msStart = Component.getTime();
}
sElapsed = (Component.getTime() - Component.msStart) + "ms: ";
}
sMsg = sMsg.replace(/\r/g, '\\r').replace(/\n/g, ' ');
if (window && window.console) console.log(sElapsed + sMsg);
}
}
}
/**
* Component.assert(f, s)
*
* Verifies conditions that must be true (for DEBUG builds only).
*
* The Closure Compiler should automatically remove all references to Component.assert() in non-DEBUG builds.
* TODO: Add a task to the build process that "asserts" there are no instances of "assertion failure" in RELEASE builds.
*
* @param {boolean} f is the expression we are asserting to be true
* @param {string} [s] is description of the assertion on failure
*/
static assert(f, s)
{
if (DEBUG) {
if (!f) {
if (!s) s = "assertion failure";
Component.log(s);
throw new Error(s);
}
}
}
/**
* Component.print(s)
*
* Components that inherit from this class should use this.print(), rather than Component.print(), because
* if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the
* class method would improperly affect any other machines loaded on the same page).
*
* @this {Component}
* @param {string} s
*/
static print(s)
{
if (!COMPILED) {
var i = s.lastIndexOf('\n');
if (i >= 0) {
Component.println(s.substr(0, i));
s = s.substr(i + 1);
}
Component.printBuffer += s;
}
}
/**
* Component.println(s, type, id)
*
* Components that inherit from this class should use this.println(), rather than Component.println(), because
* if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the
* class method would improperly affect any other machines loaded on the same page).
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
* @param {string} [id] is the caller's ID, if any
*/
static println(s, type, id)
{
if (!COMPILED) {
s = Component.printBuffer + (s || "");
Component.log((id? (id + ": ") : "") + (s? ("\"" + s + "\"") : ""), type);
Component.printBuffer = "";
}
}
/**
* Component.notice(s, fPrintOnly, id)
*
* notice() is like println() but implies a need for user notification, so we alert() as well.
*
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
* @return {boolean}
*/
static notice(s, fPrintOnly, id)
{
if (!COMPILED) {
Component.println(s, Component.PRINT.NOTICE, id);
}
if (!fPrintOnly) Component.alertUser((id? (id + ": ") : "") + s);
return true;
}
/**
* Component.warning(s)
*
* @param {string} s describes the warning
*/
static warning(s)
{
if (!COMPILED) {
Component.println(s, Component.PRINT.WARNING);
}
Component.alertUser(s);
}
/**
* Component.error(s)
*
* @param {string} s describes the error; an alert() is displayed as well
*/
static error(s)
{
if (!COMPILED) {
Component.println(s, Component.PRINT.ERROR);
}
Component.alertUser(s);
}
/**
* Component.alertUser(sMessage)
*
* @param {string} sMessage
*/
static alertUser(sMessage)
{
if (window) {
window.alert(sMessage);
} else {
Component.log(sMessage);
}
};
/**
* Component.confirmUser(sPrompt)
*
* @param {string} sPrompt
* @returns {boolean} true if the user clicked OK, false if Cancel/Close
*/
static confirmUser(sPrompt)
{
var fResponse = false;
if (window) {
fResponse = window.confirm(sPrompt);
}
return fResponse;
}
/**
* Component.promptUser()
*
* @param {string} sPrompt
* @param {string} [sDefault]
* @returns {string|null}
*/
static promptUser(sPrompt, sDefault)
{
var sResponse = null;
if (window) {
sResponse = window.prompt(sPrompt, sDefault === undefined? "" : sDefault);
}
return sResponse;
}
/**
* Component.appendControl(control, sText)
*
* @param {Object} control
* @param {string} sText
*/
static appendControl(control, sText)
{
control.value += sText;
/*
* Prevent the <textarea> from getting too large; otherwise, printing becomes slower and slower.
*/
if (COMPILED) {
sText = control.value;
if (sText.length > 8192) control.value = sText.substr(sText.length - 4096);
}
control.scrollTop = control.scrollHeight;
}
/**
* Component.replaceControl(control, sSearch, sReplace)
*
* @param {Object} control
* @param {string} sSearch
* @param {string} sReplace
*/
static replaceControl(control, sSearch, sReplace)
{
var sText = control.value;
var i = sText.lastIndexOf(sSearch);
if (i < 0) {
sText += sSearch + '\n';
} else {
sText = sText.substr(0, i) + sReplace + sText.substr(i + sSearch.length);
}
/*
* Prevent the <textarea> from getting too large; otherwise, printing becomes slower and slower.
*/
if (COMPILED && sText.length > 8192) sText = sText.substr(sText.length - 4096);
control.value = sText;
control.scrollTop = control.scrollHeight;
}
/**
* Component.bindExternalControl(component, sControl, sBinding, sType)
*
* @param {Component} component
* @param {string} sControl
* @param {string} sBinding
* @param {string} [sType] is the external component type
*/
static bindExternalControl(component, sControl, sBinding, sType)
{
if (sControl) {
if (sType === undefined) sType = "Panel";
var target = Component.getComponentByType(sType, component.id);
if (target) {
var eBinding = target.bindings[sControl];
if (eBinding) {
component.setBinding(null, sBinding, eBinding);
}
}
}
}
/**
* Component.bindComponentControls(component, element, sAppClass)
*
* @param {Component} component
* @param {HTMLElement} element from the DOM
* @param {string} sAppClass
*/
static bindComponentControls(component, element, sAppClass)
{
var aeControls = Component.getElementsByClass(element.parentNode, sAppClass + "-control");
for (var iControl = 0; iControl < aeControls.length; iControl++) {
var aeChildNodes = aeControls[iControl].childNodes;
for (var iNode = 0; iNode < aeChildNodes.length; iNode++) {
var control = aeChildNodes[iNode];
if (control.nodeType !== 1 /* document.ELEMENT_NODE */) {
continue;
}
var sClass = control.getAttribute("class");
if (!sClass) continue;
var aClasses = sClass.split(" ");
for (var iClass = 0; iClass < aClasses.length; iClass++) {
var parms;
sClass = aClasses[iClass];
switch (sClass) {
case sAppClass + "-binding":
parms = Component.getComponentParms(control);
if (parms && parms['binding']) {
component.setBinding(parms['type'], parms['binding'], control, parms['value']);
} else if (!parms || parms['type'] != "description") {
Component.log("Component '" + component.toString() + "' missing binding" + (parms? " for " + parms['type'] : ""), "warning");
}
iClass = aClasses.length;
break;
default:
// if (DEBUG) Component.log("Component.bindComponentControls(" + component.toString() + "): unrecognized control class \"" + sClass + "\"", "warning");
break;
}
}
}
}
}
/**
* Component.getComponents(idRelated)
*
* We could store components as properties, using the component's ID, and change
* this linear lookup into a property lookup, but some components may have no ID.
*
* @param {string} [idRelated] of related component
* @return {Array} of components
*/
static getComponents(idRelated)
{
var i;
var aComponents = [];
/*
* getComponentByID(id, idRelated)
*
* If idRelated is provided, we check it for a machine prefix, and use any
* existing prefix to constrain matches to IDs with the same prefix, in order to
* avoid matching components belonging to other machines.
*/
if (idRelated) {
if ((i = idRelated.indexOf('.')) > 0)
idRelated = idRelated.substr(0, i + 1);
else
idRelated = "";
}
for (i = 0; i < Component.components.length; i++) {
var component = Component.components[i];
if (!idRelated || !component.id.indexOf(idRelated)) {
aComponents.push(component);
}
}
return aComponents;
}
/**
* Component.getComponentByID(id, idRelated)
*
* We could store components as properties, using the component's ID, and change
* this linear lookup into a property lookup, but some components may have no ID.
*
* @param {string} id of the desired component
* @param {string} [idRelated] of related component
* @return {Component|null}
*/
static getComponentByID(id, idRelated)
{
if (id !== undefined) {
var i;
/*
* If idRelated is provided, we check it for a machine prefix, and use any
* existing prefix to constrain matches to IDs with the same prefix, in order to
* avoid matching components belonging to other machines.
*/
if (idRelated && (i = idRelated.indexOf('.')) > 0) {
id = idRelated.substr(0, i + 1) + id;
}
for (i = 0; i < Component.components.length; i++) {
if (Component.components[i].id === id) {
return Component.components[i];
}
}
if (Component.components.length) {
Component.log("Component ID '" + id + "' not found", "warning");
}
}
return null;
}
/**
* Component.getComponentByType(sType, idRelated, componentPrev)
*
* @param {string} sType of the desired component
* @param {string} [idRelated] of related component
* @param {Component|null} [componentPrev] of previously returned component, if any
* @return {Component|null}
*/
static getComponentByType(sType, idRelated, componentPrev)
{
if (sType !== undefined) {
var i;
/*
* If idRelated is provided, we check it for a machine prefix, and use any
* existing prefix to constrain matches to IDs with the same prefix, in order to
* avoid matching components belonging to other machines.
*/
if (idRelated) {
if ((i = idRelated.indexOf('.')) > 0) {
idRelated = idRelated.substr(0, i + 1);
} else {
idRelated = "";
}
}
for (i = 0; i < Component.components.length; i++) {
if (componentPrev) {
if (componentPrev == Component.components[i]) componentPrev = null;
continue;
}
if (sType == Component.components[i].type && (!idRelated || !Component.components[i].id.indexOf(idRelated))) {
return Component.components[i];
}
}
Component.log("Component type '" + sType + "' not found", "warning");
}
return null;
}
/**
* Component.getComponentParms(element)
*
* @param {HTMLElement} element from the DOM
*/
static getComponentParms(element)
{
var parms = null;
var sParms = element.getAttribute("data-value");
if (sParms) {
try {
parms = eval('(' + sParms + ')'); // jshint ignore:line
/*
* We can no longer invoke removeAttribute() because some components (eg, Panel) need
* to run their initXXX() code more than once, to avoid initialization-order dependencies.
*
* if (!DEBUG) {
* element.removeAttribute("data-value");
* }
*/
} catch(e) {
Component.error(e.message + " (" + sParms + ")");
}
}
return parms;
}
/**
* Component.getElementsByClass(element, sClass, sObjClass)
*
* This is a cross-browser helper function, since not all browser's support getElementsByClassName()
*
* TODO: This should probably be moved into weblib.js at some point, along with the control binding functions above,
* to keep all the browser-related code together.
*
* @param {HTMLDocument|HTMLElement|Node} element from the DOM
* @param {string} sClass
* @param {string} [sObjClass]
* @return {Array|NodeList}
*/
static getElementsByClass(element, sClass, sObjClass)
{
if (sObjClass) sClass += '-' + sObjClass + "-object";
/*
* Use the browser's built-in getElementsByClassName() if it appears to be available
* (for example, it's not available in IE8, but it should be available in IE9 and up)
*/
if (element.getElementsByClassName) {
return element.getElementsByClassName(sClass);
}
var i, j, ae = [];
var aeAll = element.getElementsByTagName("*");
var re = new RegExp('(^| )' + sClass + '( |$)');
for (i = 0, j = aeAll.length; i < j; i++) {
if (re.test(aeAll[i].className)) {
ae.push(aeAll[i]);
}
}
if (!ae.length) {
Component.log('No elements of class "' + sClass + '" found');
}
return ae;
}
/**
* Component.getScriptCommands(sScript)
*
* This is a simple parser that breaks sScript into an array of commands, where each command
* is an array of tokens, where tokens are sequences of characters separated by any of: tab, space,
* carriage-return (CR), line-feed (LF), semicolon, single-quote, or double-quote; if a quote is
* used, all characters up to the next matching quote become part of the token, allowing any of the
* other separators to be part of the token. CR, LF and semicolon also serve to terminate a command,
* with semicolon being preferred, because it's 1) more visible, and 2) essential when the entire
* script is a multi-line string where all CR/LF were replaced by spaces (which is what Jekyll does,
* and since we can't change Jekyll, it's what our own MarkDown Front Matter parser does as well;
* see convertMD() in markout.js, where the aCommandDefs array is built).
*
* Backslash sequences like \n, \r, and \\ have already been converted to LF, CR and backslash
* characters, since the entire script string is injected into a JavaScript function call, so any
* backslash sequence that JavaScript supports is automatically converted:
*
* \0 \' \" \\ \n \r \v \t \b \f \uXXXX \xXX
* ^J ^M ^K ^I ^H ^L
*
* To support any other non-printable 8-bit character, such as ESC, you should use \xXX, where XX
* is the ASCII code in hex. For ESC, that would be \x1B.
*
* @param {string} sScript
* @return {Array}
*/
static getScriptCommands(sScript)
{
var cch = sScript.length;
var aCommands = [], aTokens = [], sToken = "", chQuote = null;
for (var i = 0; i < cch; i++) {
var ch = sScript[i];
if (ch == '"' || ch == "'") {
if (chQuote && ch != chQuote) {
sToken += ch;
continue;
}
if (!chQuote) {
chQuote = ch;
} else {
chQuote = null;
}
if (sToken) {
aTokens.push(sToken);
sToken = "";
}
continue;
}
if (!chQuote) {
if (ch == '\r' || ch == '\n') {
ch = ';';
}
if (ch == ' ' || ch == '\t' || ch == ';') {
if (sToken) {
aTokens.push(sToken);
sToken = "";
}
if (ch == ';' && aTokens.length) {
aCommands.push(aTokens);
aTokens = [];
}
continue;
}
}
sToken += ch;
}
if (sToken) {
aTokens.push(sToken);
}
if (aTokens.length) {
aCommands.push(aTokens);
}
return aCommands;
}
/**
* Component.processScript(idMachine, sScript)
*
* @param {string} idMachine
* @param {string} [sScript]
* @return {boolean}
*/
static processScript(idMachine, sScript)
{
var fSuccess = false;
idMachine += ".machine";
if (!sScript) {
delete Component.commands[idMachine];
fSuccess = true;
}
else if (typeof sScript == "string" && !Component.commands[idMachine]) {
fSuccess = true;
Component.commands[idMachine] = Component.getScriptCommands(sScript);
if (!Component.processCommands(idMachine)) {
fSuccess = false;
}
}
return fSuccess;
}
/**
* Component.processCommands(idMachine)
*
* @param {string} idMachine
* @return {boolean}
*/
static processCommands(idMachine)
{
var fSuccess = true;
var aCommands = Component.commands[idMachine];
// var dbg = Component.getComponentByType("Debugger", idMachine);
while (aCommands && aCommands.length) {
var aTokens = aCommands.splice(0, 1)[0];
var sCommand = aTokens[0];
/*
* It's possible to route this output to the Debugger window with dbg.println()
* instead, but it's a bit too confusing mingling script output in a window that
* already mingles Debugger and machine output.
*/
Component.println(aTokens.join(' '), Component.PRINT.SCRIPT);
var fnCallReady = null;
if (Component.asyncCommands.indexOf(sCommand) >= 0) {
fnCallReady = function processNextCommand() {
return function() {
Component.processCommands(idMachine);
}
}();
}
var fnCommand = Component.globalCommands[sCommand];
if (fnCommand) {
if (!fnCallReady) {
fSuccess = fnCommand(aTokens[1], aTokens[2], aTokens[3]);
} else {
if (!fnCommand(fnCallReady, aTokens[1], aTokens[2], aTokens[3])) break;
}
}
else {
fSuccess = false;
var component = Component.getComponentByType(aTokens[1], idMachine);
if (component) {
fnCommand = Component.componentCommands[sCommand];
if (fnCommand) {
fSuccess = fnCommand(component, aTokens[2], aTokens[3]);
}
else {
var exports = component['exports'];
if (exports) {
fnCommand = exports[sCommand];
if (fnCommand) {
fSuccess = true;
if (!fnCallReady) {
fSuccess = fnCommand.call(component, aTokens[2], aTokens[3]);
} else {
if (!fnCommand.call(component, fnCallReady, aTokens[2], aTokens[3])) break;
}
}
}
}
}
}
if (!fSuccess) {
Component.alertUser("Script error: '" + sCommand + (fnCommand? " failed" : " unrecognized"));
break;
}
}
if (aCommands && !aCommands.length) {
delete Component.commands[idMachine];
}
return fSuccess;
}
/**
* Component.scriptAlert(sMessage)
*
* @param {string} sMessage
* @return {boolean}
*/
static scriptAlert(sMessage)
{
Component.alertUser(sMessage);
return true;
}
/**
* Component.scriptSelect(component, sBinding, sValue)
*
* @param {Component} component
* @param {string} sBinding
* @param {string} sValue
* @return {boolean}
*/
static scriptSelect(component, sBinding, sValue)
{
var fSuccess = false;
var aBindings = component['bindings'];
var control = aBindings[sBinding];
if (control) {
for (var i = 0; i < control.options.length; i++) {
if (control.options[i].textContent == sValue) {
if (control.selectedIndex != i) {
control.selectedIndex = i;
}
fSuccess = true;
break;
}
}
}
return fSuccess;
}
/**
* Component.scriptSleep(fnCallback, sDelay)
*
* @param {function()} fnCallback
* @param {string} sDelay (in milliseconds)
* @return {boolean}
*/
static scriptSleep(fnCallback, sDelay)
{
setTimeout(fnCallback, +sDelay);
return false;
}
/**
* toString()
*
* @this {Component}
* @return {string}
*/
toString()
{
return (this.name? this.name : (this.id || this.type));
}
/**
* getMachineNum()
*
* @this {Component}
* @return {number} unique machine number
*/
getMachineNum()
{
var nMachine = 1;
if (this.idMachine) {
var aDigits = this.idMachine.match(/\d+/);
if (aDigits !== null)
nMachine = parseInt(aDigits[0], 10);
}
return nMachine;
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* Component's setBinding() method is intended to be overridden by subclasses.
*
* @this {Component}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, 'print')
* @param {HTMLElement} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
switch (sBinding) {
case 'clear':
if (!this.bindings[sBinding]) {
this.bindings[sBinding] = control;
control.onclick = (function(component) {
return function clearControl() {
if (component.bindings['print']) {
component.bindings['print'].value = "";
}
};
}(this));
}
return true;
case 'print':
if (!this.bindings[sBinding]) {
var controlTextArea = /** @type {HTMLTextAreaElement} */ (control);
this.bindings[sBinding] = controlTextArea;
/**
* Override this.notice() with a replacement function that eliminates the Component.alertUser() call.
*
* @this {Component}
* @param {string} s
* @param {boolean} [fPrintOnly]
* @param {string} [id]
* @return {boolean}
*/
this.notice = function noticeControl(s, fPrintOnly, id) {
this.println(s, this.type);
return true;
};
/*
* This was added for Firefox (Safari will clear the <textarea> on a page reload, but Firefox does not).
*/
controlTextArea.value = "";
this.print = function(control) {
return function printControl(s) {
Component.appendControl(control, s);
};
}(controlTextArea);
this.println = function(component, control) {
return function printlnControl(s, type, id) {
if (!s) s = "";
if (type != Component.PRINT.PROGRESS || s.slice(-3) != "...") {
if (type) s = type + ": " + s;
Component.appendControl(control, s + '\n');
} else {
Component.replaceControl(control, s, s + '.');
}
if (!COMPILED && window && window.console) Component.println(s, type, id);
};
}(this, controlTextArea);
}
return true;
default:
return false;
}
}
/**
* log(s, type)
*
* For diagnostic output only.
*
* WARNING: Even though this function's body is completely wrapped in DEBUG, that won't prevent the Closure Compiler
* from including it, so all calls must still be prefixed with "if (DEBUG) ....". For this reason, the class method,
* Component.log(), is preferred, because the compiler IS smart enough to remove those calls.
*
* @this {Component}
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
log(s, type)
{
if (!COMPILED) {
Component.log(s, type || this.id || this.type);
}
}
/**
* assert(f, s)
*
* Verifies conditions that must be true (for DEBUG builds only).
*
* WARNING: Make sure you preface all calls to this.assert() with "if (DEBUG)", because unlike Component.assert(),
* the Closure Compiler can't be sure that this instance method hasn't been overridden, so it refuses to treat it as
* dead code in non-DEBUG builds.
*
* TODO: Add a task to the build process that "asserts" there are no instances of "assertion failure" in RELEASE builds.
*
* @this {Component}
* @param {boolean|number} f is the expression asserted to be true
* @param {string} [s] is a description of the assertion to be displayed or logged on failure
*/
assert(f, s)
{
if (DEBUG) {
if (!f) {
s = "assertion failure in " + (this.id || this.type) + (s? ": " + s : "");
if (DEBUGGER && this.dbg) {
this.dbg.stopCPU();
/*
* Why do we throw an Error only to immediately catch and ignore it? Simply to give
* any IDE the opportunity to inspect the application's state. Even when the IDE has
* control, you should still be able to invoke Debugger commands from the IDE's REPL,
* using the global function that the Debugger constructor defines; eg:
*
* pcx86('r')
* pcx86('dw 0:0')
* pcx86('h')
* ...
*
* If you have no desire to stop on assertions, consider this a no-op. However, another
* potential benefit of creating an Error object is that, for browsers like Chrome, we get
* a stack trace, too.
*/
try {
throw new Error(s);
} catch(e) {
this.println(e.stack || e.message);
}
return;
}
this.log(s);
throw new Error(s);
}
}
}
/**
* print(s)
*
* Components using this.print() should wait until after their constructor has run to display any messages, because
* if a Control Panel has been loaded, its override will not take effect until its own constructor has run.
*
* @this {Component}
* @param {string} s
*/
print(s)
{
Component.print(s);
}
/**
* println(s, type, id)
*
* Components using this.println() should wait until after their constructor has run to display any messages, because
* if a Control Panel has been loaded, its override will not take effect until its own constructor has run.
*
* @this {Component}
* @param {string} [s] is the message text
* @param {string} [type] is the message type
* @param {string} [id] is the caller's ID, if any
*/
println(s, type, id)
{
Component.println(s, type, id || this.id);
}
/**
* status(s)
*
* status() is like println() but it also includes information about the component (ie, the component type),
* which is why there is no corresponding Component.status() function.
*
* @param {string} s is the message text
*/
status(s)
{
this.println(this.type + ": " + s);
}
/**
* notice(s, fPrintOnly, id)
*
* notice() is like println() but implies a need for user notification, so we alert() as well; however, if this.println()
* is overridden, this.notice will be replaced with a similar override, on the assumption that the override is taking care
* of alerting the user.
*
* @this {Component}
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
* @return {boolean}
*/
notice(s, fPrintOnly, id)
{
if (!fPrintOnly) {
/*
* See if the associated computer, if any, is "unloading"....
*/
var computer = Component.getComponentByType("Computer", this.id);
if (computer && computer.flags.unloading) {
console.log("ignoring notice during unload: " + s);
return false;
}
}
Component.notice(s, fPrintOnly, id || this.type);
return true;
}
/**
* setError(s)
*
* Set a fatal error condition
*
* @this {Component}
* @param {string} s describes a fatal error condition
*/
setError(s)
{
this.flags.error = true;
this.notice(s); // TODO: Any cases where we should still prefix this string with "Fatal error: "?
}
/**
* clearError()
*
* Clear any fatal error condition
*
* @this {Component}
*/
clearError() {
this.flags.error = false;
}
/**
* isError()
*
* Report any fatal error condition
*
* @this {Component}
* @return {boolean} true if a fatal error condition exists, false if not
*/
isError()
{
if (this.flags.error) {
this.println(this.toString() + " error");
return true;
}
return false;
}
/**
* isReady(fnReady)
*
* Return the "ready" state of the component; if the component is not ready, it will queue the optional
* notification function, otherwise it will immediately call the notification function, if any, without queuing it.
*
* NOTE: Since only the Computer component actually cares about the "readiness" of other components, the so-called
* "queue" of notification functions supports exactly one function. This keeps things nice and simple.
*
* @this {Component}
* @param {function()} [fnReady]
* @return {boolean} true if the component is in a "ready" state, false if not
*/
isReady(fnReady)
{
if (fnReady) {
if (this.flags.ready) {
fnReady();
} else {
if (MAXDEBUG) this.log("NOT ready");
this.fnReady = fnReady;
}
}
return this.flags.ready;
}
/**
* setReady(fReady)
*
* Set the "ready" state of the component to true, and call any queued notification functions.
*
* @this {Component}
* @param {boolean} [fReady] is assumed to indicate "ready" unless EXPLICITLY set to false
*/
setReady(fReady)
{
if (!this.flags.error) {
this.flags.ready = (fReady !== false);
if (this.flags.ready) {
if (MAXDEBUG /* || this.name */) this.log("ready");
var fnReady = this.fnReady;
this.fnReady = null;
if (fnReady) fnReady();
}
}
}
/**
* isBusy(fCancel)
*
* Return the "busy" state of the component
*
* @this {Component}
* @param {boolean} [fCancel] is set to true to cancel a "busy" state
* @return {boolean} true if "busy", false if not
*/
isBusy(fCancel)
{
if (this.flags.busy) {
if (fCancel) {
this.flags.busyCancel = true;
} else if (fCancel === undefined) {
this.println(this.toString() + " busy");
}
}
return this.flags.busy;
}
/**
* setBusy(fBusy)
*
* Update the current busy state; if a busyCancel request is pending, it will be honored now.
*
* @this {Component}
* @param {boolean} fBusy
* @return {boolean}
*/
setBusy(fBusy)
{
if (this.flags.busyCancel) {
this.flags.busy = false;
this.flags.busyCancel = false;
return false;
}
if (this.flags.error) {
this.println(this.toString() + " error");
return false;
}
this.flags.busy = fBusy;
return this.flags.busy;
}
/**
* powerUp(fSave)
*
* @this {Component}
* @param {Object|null} data
* @param {boolean} [fRepower] is true if this is "repower" notification
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
this.flags.powered = true;
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {Component}
* @param {boolean} fSave
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
if (fShutdown) this.flags.powered = false;
return true;
}
/**
* messageEnabled(bitsMessage)
*
* If bitsMessage is not specified, the component's MESSAGE category is used.
*
* @this {Component}
* @param {number} [bitsMessage] is zero or more MESSAGE_* category flag(s)
* @return {boolean} true if all specified message enabled, false if not
*/
messageEnabled(bitsMessage)
{
if (DEBUGGER && this.dbg) {
if (this === this.dbg) {
bitsMessage |= 0;
} else {
bitsMessage = bitsMessage || this.bitsMessage;
}
var bitsEnabled = this.dbg.bitsMessage & bitsMessage;
return (!!bitsMessage && bitsEnabled === bitsMessage || !!(bitsEnabled & this.dbg.bitsWarning));
}
return false;
}
/**
* printMessage(sMessage, bitsMessage, fAddress)
*
* If bitsMessage is not specified, the component's MESSAGE category is used.
* If bitsMessage is true, the message is displayed regardless.
*
* @this {Component}
* @param {string} sMessage is any caller-defined message string
* @param {number|boolean} [bitsMessage] is zero or more MESSAGE_* category flag(s)
* @param {boolean} [fAddress] is true to display the current address
*/
printMessage(sMessage, bitsMessage, fAddress)
{
if (DEBUGGER && this.dbg) {
if (bitsMessage === true || this.messageEnabled(bitsMessage | 0)) {
this.dbg.message(sMessage, fAddress);
}
}
}
/**
* printMessageIO(port, bOut, addrFrom, name, bIn, bitsMessage)
*
* If bitsMessage is not specified, the component's MESSAGE category is used.
* If bitsMessage is true, the message is displayed as long as MESSAGE.PORT is enabled.
*
* @this {Component}
* @param {number} port
* @param {number|null} bOut if an output operation
* @param {number|null} [addrFrom]
* @param {string|null} [name] of the port, if any
* @param {number|null} [bIn] is the input value, if known, on an input operation
* @param {number|boolean} [bitsMessage] is zero or more MESSAGE_* category flag(s)
*/
printMessageIO(port, bOut, addrFrom, name, bIn, bitsMessage)
{
if (DEBUGGER && this.dbg) {
if (bitsMessage === true) {
bitsMessage = 0;
} else if (bitsMessage == null) {
bitsMessage = this.bitsMessage;
}
this.dbg.messageIO(this, port, bOut, addrFrom, name, bIn, bitsMessage);
}
}
}
/*
* Types recognized and supported by selected functions (eg, Computer.getMachineParm())
*/
Component.TYPE = {
NUMBER: "number",
OBJECT: "object",
STRING: "string"
};
/*
* These are the standard PRINT values you can pass as an optional argument to println(); in reality,
* you can pass anything you want, because they are simply prepended to the message, although PROGRESS
* messages may also be merged with earlier similar messages to keep the output buffer under control.
*/
Component.PRINT = {
ERROR: "error",
NOTICE: "notice",
PROGRESS: "progress",
SCRIPT: "script",
WARNING: "warning"
};
/*
* Every component created on the current page is recorded in this array (see Component.add()),
* enabling any component to locate another component by ID (see Component.getComponentByID())
* or by type (see Component.getComponentByType()).
*
* Every machine on the page are now recorded as well, by their machine ID. We then record the
* various resources used by that machine.
*
* Includes a fallback for non-browser-based environments (ie, Node). TODO: This will need to be
* tailored to Node, probably using the global object instead of the window object, if we ever want
* to support multi-machine configs in that environment.
*/
if (window) {
if (!window['PCjs']) window['PCjs'] = {};
if (!window['PCjs']['Machines']) window['PCjs']['Machines'] = {};
if (!window['PCjs']['Components']) window['PCjs']['Components'] = [];
if (!window['PCjs']['Commands']) window['PCjs']['Commands'] = {};
}
Component.machines = window? window['PCjs']['Machines'] : {};
Component.components = window? window['PCjs']['Components'] : [];
Component.commands = window? window['PCjs']['Commands'] : {};
Component.asyncCommands = [
'hold', 'sleep', 'wait'
];
Component.globalCommands = {
'alert': Component.scriptAlert,
'sleep': Component.scriptSleep
};
Component.componentCommands = {
'select': Component.scriptSelect
};
Component.printBuffer = "";
/*
* The following polyfills provide ES5 functionality that's missing in older browsers (eg, IE8),
* allowing PCjs apps to run without slamming into exceptions; however, due to the lack of HTML5 canvas
* support in those browsers, all you're likely to see are "soft" errors (eg, "Missing <canvas> support").
*
* Perhaps we can implement a text-only faux video display for a fun retro-browser experience someday.
*
* TODO: Come up with a better place to put these polyfills. We will likely have more if we decide to
* make the leap from ES5 to ES6 features.
*/
/*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
*/
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(obj, start) {
for (var i = (start || 0), j = this.length; i < j; i++) {
if (this[i] === obj) { return i; }
}
return -1;
}
}
/*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
*/
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
/*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
*/
if (!Function.prototype.bind) {
Function.prototype.bind = function(obj) {
if (typeof this != "function") {
// Closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind: non-callable object");
}
var args = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fnNOP = /** @constructor */ (function() {});
var fnBound = function() {
return fToBind.apply(this instanceof fnNOP && obj? this : obj, args.concat(Array.prototype.slice.call(arguments)));
};
fnNOP.prototype = this.prototype;
fnBound.prototype = new fnNOP();
return fnBound;
};
}
/**
* @copyright http://pcjs.org/modules/pdp11/lib/defines.js (C) Jeff Parsons 2012-2017
*/
/**
* @define {string}
*/
var APPCLASS = "pdp11"; // this @define is the default application class (eg, "pcx86", "c1pjs")
/**
* APPNAME is used more for display purposes than anything else now. APPCLASS is what matters in terms
* of folder and file names, CSS styles, etc.
*
* @define {string}
*/
var APPNAME = "PDPjs"; // this @define is the default application name (eg, "PCx86", "C1Pjs")
/**
* WARNING: DEBUGGER needs to accurately reflect whether or not the Debugger component is (or will be) loaded.
* In the compiled case, we rely on the Closure Compiler to override DEBUGGER as appropriate. When it's *false*,
* nearly all of debugger.js will be conditionally removed by the compiler, reducing it to little more than a
* "type skeleton", which also solves some type-related warnings we would otherwise have if we tried to remove
* debugger.js from the compilation process altogether.
*
* However, when we're in "development mode" and running uncompiled code in debugger-less configurations,
* I would like to skip loading debugger.js altogether. When doing that, we must ALSO arrange for an additional file
* (nodebugger.js) to be loaded immediately after this file, which *explicitly* overrides DEBUGGER with *false*.
*
* @define {boolean}
*/
var DEBUGGER = true; // this @define is overridden by the Closure Compiler to remove Debugger-related support
/**
* BYTEARRAYS is a Closure Compiler compile-time option that allocates an Array of numbers for every Memory block,
* where each a number represents ONE byte; very wasteful, but potentially slightly faster.
*
* See the Memory component for details.
*
* @define {boolean}
*/
var BYTEARRAYS = false;
/**
* TYPEDARRAYS enables use of typed arrays for Memory blocks. This used to be a compile-time-only option, but I've
* added Memory access functions for typed arrays (see MemoryPDP11.afnTypedArray), so support can be enabled dynamically now.
*
* See the Memory component for details.
*/
var TYPEDARRAYS = (typeof ArrayBuffer !== 'undefined');
/**
* MEMFAULT forces the Memory interfaces to signal a CPU fault when a word is accessed using an odd (unaligned) address.
*
* Since PDPjs inherited its Bus component from PCx86, it included support for both aligned and unaligned word accesses
* by default. However, the PDP-11 adds a wrinkle: when an odd address is used to access a memory word, a BUS trap
* must be generated. Note that odd IOPAGE word accesses are fine; this only affects the Memory component.
*
* When the MMU is enabled, these checks may also be performed at a higher level, eliminating the need for them at the
* physical memory level.
*/
var MEMFAULT = true;
/**
* WORDBUS turns off support for unaligned memory words. Whereas MEMFAULT necessarily slows down memory word accesses
* slightly, WORDBUS is able to speed them up slightly, by assuming that all word accesses (which didn't fault) must be
* aligned. This affects all word accesses, even IOPAGE accesses, because it also eliminates cross-block boundary checks.
*
* Don't worry that the source code looks MORE complicated rather than LESS with the additional MEMFAULT and WORDBUS checks,
* because the Closure Compiler eliminates those checks and throws away the (unreachable) code blocks that deal with unaligned
* accesses.
*/
var WORDBUS = true;
/*
* Combine all the shared globals and machine-specific globals into one machine-specific global object,
* which all machine components should start using; eg: "if (PDP11.DEBUG) ..." instead of "if (DEBUG) ...".
*/
var PDP11 = {
APPCLASS: APPCLASS,
APPNAME: APPNAME,
APPVERSION: APPVERSION, // shared
BYTEARRAYS: BYTEARRAYS,
COMPILED: COMPILED, // shared
CSSCLASS: CSSCLASS, // shared
DEBUG: DEBUG, // shared
DEBUGGER: DEBUGGER,
MAXDEBUG: MAXDEBUG, // shared
PRIVATE: PRIVATE, // shared
TYPEDARRAYS:TYPEDARRAYS,
MEMFAULT: MEMFAULT,
WORDBUS: WORDBUS,
SITEHOST: SITEHOST, // shared
XMLVERSION: XMLVERSION, // shared
/*
* CPU model numbers (supported)
*
* The 11/20 includes the 11/10, which is not identified separately because there was
* nothing functionally different about it.
*
* The 11/40 added the MODE bits to the PSW (but only KERNEL=00 and USER=11) and 18-bit
* addressing via an MMU; there was still only one register set.
*
* The 11/45 added REGSET bit to the PSW (to support a second register set), SUPER=01
* mode to the existing KERNEL=00 and USER=11 modes, separate I/D spaces, and other MMU
* extensions (eg, MMR1 and MMR3).
*
* The 11/70 added 22-bit addressing and corresponding extensions to the MMU.
*/
MODEL_1120: 1120,
MODEL_1140: 1140,
MODEL_1145: 1145,
MODEL_1170: 1170,
/*
* This constant is used to mark points in the code where the physical address being returned
* is invalid and should not be used.
*
* In a 32-bit CPU, -1 (ie, 0xffffffff) could actually be a valid address, so consider changing
* ADDR_INVALID to NaN or null (which is also why all ADDR_INVALID tests should use strict equality
* operators).
*
* The main reason I'm NOT using NaN or null now is my concern that, by mixing non-numbers
* (specifically, values outside the range of signed 32-bit integers), performance may suffer.
*
* WARNING: Like many of the properties defined here, ADDR_INVALID is a common constant, which the
* Closure Compiler will happily inline (with or without @const annotations; in fact, I've yet to
* see a @const annotation EVER improve automatic inlining). However, if you don't make ABSOLUTELY
* certain that this file is included BEFORE the first reference to any of these properties, that
* automatic inlining will no longer occur.
*/
ADDR_INVALID: -1,
/*
* Processor modes
*/
MODE: {
KERNEL: 0x0, // 11/40 and higher
SUPER: 0x1, // 11/45 and higher
UNUSED: 0x2,
USER: 0x3, // 11/40 and higher
MASK: 0x3
},
/*
* Processor Status Word (stored in regPSW) at 177776
*/
PSW: {
CF: 0x0001, // bit 0 (000001) Carry Flag
VF: 0x0002, // bit 1 (000002) Overflow Flag (aka OF on Intel processors)
ZF: 0x0004, // bit 2 (000004) Zero Flag
NF: 0x0008, // bit 3 (000010) Negative Flag (aka SF -- Sign Flag -- on Intel processors)
TF: 0x0010, // bit 4 (000020) Trap Flag
PRI: 0x00E0, // bits 5-7 (000340) Priority
UNUSED: 0x0700, // bits 8-10 (003400) UNUSED
/*
* The REGSET bit (and the alternate register set stored in regsAlt) came into existence
* with the 11/45; (ie, they were not present on the 11/10, 11/20, or 11/40).
*/
REGSET: 0x0800, // bit 11 (004000) Register Set
/*
* The MODE bits came into existence with the 11/40 (eg, not present on the 11/10 or 11/20).
*/
PMODE: 0x3000, // bits 12-13 (030000) Prev Mode (see PDP11.MODE)
CMODE: 0xC000, // bits 14-15 (140000) Curr Mode (see PDP11.MODE)
SHIFT: {
CF: 0,
VF: 1,
ZF: 2,
NF: 3,
TF: 4,
PRI: 5,
PMODE: 12,
CMODE: 14
}
},
/*
* Program Interrupt Register (stored in regPIR) at 177772
*
* The PIA bits at 5-7 are designed to align with PRI bits 5-7 in the PSW.
*/
PIR: {
BITS: 0xFE00, // bits 9-15 correspond to interrupt requests 1-7
PIA: 0x00EE, // the PIA bits contain two copies of the corresponding interrupt request priority
PIA_INC: 0x0022, // both sets of PIA bits can be incremented with this constant
SHIFT: {
BITS: 9
}
},
/*
* PDP-11 trap vectors
*/
TRAP: {
UNDEFINED: 0x00, // 000 (reserved)
BUS: 0x04, // 004 unaligned address, non-existent memory, illegal instruction, etc
RESERVED: 0x08, // 010 reserved instructions
BPT: 0x0C, // 014 BPT: breakpoint trap (trace)
IOT: 0x10, // 020 IOT: input/output trap
PF: 0x14, // 024 power fail
EMT: 0x18, // 030 EMT: emulator trap
TRAP: 0x1C, // 034 TRAP instruction
PIRQ: 0xA0, // 240 PIRQ: program interrupt request
MMU: 0xA8 // 250 MMU: aborts and traps
},
/*
* PDP-11 trap reasons; the reason may also be a non-negative address indicating a BUS memory error
* (unaligned address or non-existent memory). Any reason >= RED (which includes BUS memory errors) generate
* immediate (thrown) traps, as they are considered ABORTs; the rest generate synchronous traps.
*/
REASON: {
PANIC: -1, // immediate halt (internal error)
ABORT: -2, // immediate MMU fault
ILLEGAL: -3, // immediate invalid opcode (BUS)
RED: -4, // immediate stack overflow fault (BUS)
YELLOW: -5, // deferred stack overflow fault (BUS)
FAULT: -6, // deferred MMU fault
TRACE: -7, // deferred TF fault (BPT)
HALT: -8, // illegal HALT (BUS)
OPCODE: -9, // opcode-generated trap (eg, BPT, EMT, IOT, TRAP, or RESERVED opcode)
INTERRUPT: -10, // device-generated trap (vector is device-specific)
},
REASONS: [
"UNKNOWN",
"PANIC",
"ABORT",
"ILLEGAL",
"RED",
"YELLOW",
"FAULT",
"TRACE",
"HALT",
"OPCODE",
"INTERRUPT"
],
/*
* Assorted common opcodes
*/
OPCODE: {
HALT: 0x0000,
WAIT: 0x0001,
BPT: 0x0003,
IOT: 0x0004,
JSR_OP: 0x0800,
JSR_MASK: 0xFE00,
SOB_OP: 0x7E00,
SOB_MASK: 0xFE00,
EMT_OP: 0x8800,
EMT_MASK: 0xFF00,
TRAP_OP: 0x8900,
TRAP_MASK: 0xFF00,
INVALID: 0xFFFF // far from the only invalid opcode, just a KNOWN invalid opcode
},
/*
* Internal operation state flags
*/
OPFLAG: {
IRQ_DELAY: 0x0001, // incremented until it becomes IRQ (set by SPL and traps)
IRQ: 0x0002, // time to call checkInterrupts()
IRQ_MASK: 0x0003,
DEBUGGER: 0x0004, // set if the Debugger wants to perform checks
WAIT: 0x0008, // WAIT operation in progress
PRESERVE: 0x000F, // OPFLAG bits to preserve prior to the next instruction
TRAP_TF: 0x0010, // aka PDP11.PSW.TF (WARNING: do not change this bit, or you will likely break opRTI())
TRAP_SP: 0x0020, // set for a deferred BUS trap (due to a "yellow" stack overflow condition)
TRAP_MMU: 0x0040,
TRAP_MASK: 0x0070,
TRAP_LAST: 0x0080, // set if last operation was a trap (see trapLast for the vector, and trapReason for the reason)
TRAP_RED: 0x0100, // set whenever a RED trap occurs, used to catch double RED traps (time to PANIC)
},
/*
* Opcode reg (opcode bits 2-0)
*/
OPREG: {
MASK: 0x07
},
/*
* Opcode modes (opcode bits 5-3)
*/
OPMODE: {
REG: 0x00, // REGISTER (register is operand)
REGD: 0x08, // REGISTER DEFERRED (register is address of operand)
POSTINC: 0x10, // AUTO-INCREMENT (register is address of operand, register incremented)
POSTINCD: 0x18, // AUTO-INCREMENT DEFERRED (register is address of address of operand, register incremented)
PREDEC: 0x20, // AUTO-DECREMENT (register decremented, register is address of operand)
PREDECD: 0x28, // AUTO-DECREMENT DEFERRED (register decremented, register is address of address of operand)
INDEX: 0x30, // INDEX (register + next word is address of operand)
INDEXD: 0x38, // INDEX DEFERRED (register + next word is address of address of operand)
MASK: 0x38,
SHIFT: 3
},
DSTMODE: {
REG: 0x0007,
MODE: 0x0038,
MASK: 0x003F,
SHIFT: 0
},
SRCMODE: {
REG: 0x01C0,
MODE: 0x0E00,
MASK: 0x0FC0,
SHIFT: 6
},
REG: {
SP: 6,
PC: 7,
},
/*
* Internal memory access flags
*/
ACCESS: {
WORD: 0x00,
BYTE: 0x01,
READ: 0x02,
WRITE: 0x04,
UPDATE: 0x06,
VIRT: 0x08, // getVirtualByMode() leaves bit 17 clear if this is set (otherwise the caller would have to clear it again)
ISPACE: 0x00000,
DSPACE: 0x10000 // getVirtualByMode() sets bit 17 in any 16-bit virtual address that refers to D space (as opposed to I space)
},
/*
* Internal flags passed to writeDstByte()
*
* The BYTE and SBYTE values have been chosen so that they can be used directly as masks.
*/
WRITE: {
BYTE: 0xff, // write byte normally
SBYTE: 0xffff // sign-extend byte to word
},
CPUERR: { // 177766
RED: 0x0004, // 000004 red zone stack limit
YELLOW: 0x0008, // 000010 yellow zone stack limit
TIMEOUT: 0x0010, // 000020 UNIBUS timeout error
NOMEMORY: 0x0020, // 000040 non-existent memory error
ODDADDR: 0x0040, // 000100 odd word address error (as in non-even, not strange)
BADHALT: 0x0080 // 000200 HALT attempted in USER or SUPER modes
},
MMR0: { // 177572
ENABLED: 0x0001, // 000001 address relocation enabled
PAGE_NUM: 0x000E, // 000016 page number of last fault
PAGE_D: 0x0010, // 000020 last fault occurred in D space (11/45 and 11/70)
PAGE: 0x001E, // 000176 (all of the PAGE bits)
MODE: 0x0060, // 000140 processor mode as of last fault
COMPLETED: 0x0080, // 000200 last instruction completed (R/O) (11/70)
MAINT: 0x0100, // 000400 only destination mode references will be relocated
MMU_TRAPS: 0x0200, // 001000 enable MMU traps (11/70)
UNUSED: 0x0C00, // 006000
TRAP_MMU: 0x1000, // 010000 trap: MMU (11/70)
ABORT_RO: 0x2000, // 020000 abort: read-only
ABORT_PL: 0x4000, // 040000 abort: page length
ABORT_NR: 0x8000, // 100000 abort: non-resident
ABORT: 0xE000, // 160000 (all of the ABORT bits)
UPDATE: 0xF0FE, // Includes all of: ABORT, TRAP, COMPLETED, MODE, and PAGE bits
SHIFT: {
PAGE: 1,
MODE: 5
}
},
MMR1: { // 177574: general purpose auto-inc/auto-dec register (11/45 and 11/70)
REG1_NUM: 0x0007, //
REG1_DELTA: 0x00F8, //
REG2_NUM: 0x0700, //
REG2_DELTA: 0xF800 //
},
MMR2: { // 177576: virtual program counter register
},
MMR3: { // 172516: mapping register (11/45 and 11/70)
USER_D: 0x0001, // (000001)
SUPER_D: 0x0002, // (000002)
KERNEL_D: 0x0004, // (000004)
MMU_22BIT: 0x0010, // (000020)
UNIBUS_MAP: 0x0020 // (000040) UNIBUS map relocation enabled
},
PDR: {
ACF: {
NR: 0x0, // non-resident, abort all accesses
RO1: 0x1, // read-only, abort on write attempt, memory management trap on read (11/70)
RO: 0x2, // read-only, abort on write attempt
U1: 0x3, // unused, abort all accesses--reserved for future use
RW1: 0x4, // read/write, memory management trap upon completion of a read or write
RW2: 0x5, // read/write, memory management trap upon completion of a write (11/70)
RW: 0x6, // read/write, no system trap/abort action
U2: 0x7, // unused, abort all accesses--reserved for future use
MASK: 0x7
},
ED: 0x0008, // expansion direction (if set, the page expands downward from block number 127)
UNUSED: 0x0030,
MODIFIED: 0x0040, // page has been written (bit cleared when either PDR or PAR is written)
ACCESSED: 0x0080, // page has been accessed (bit cleared when either PDR or PAR is written) (11/70)
PLF: 0x7F00, // page length field
BC: 0x8000 // bypass cache (11/44 only)
},
/*
* Assorted special (UNIBUS) addresses
*
* Within the PDP-11/45's 18-bit address space, of the 0x40000 possible addresses (256Kb), the top 0x2000
* (8Kb) is called the IOPAGE and is reserved for CPU and I/O registers. The IOPAGE spans 0x3E000-0x3FFFF.
*
* Within the PDP-11/70's 22-bit address space, of the 0x400000 possible addresses (4Mb), the top 0x20000
* (256Kb) is mapped to the UNIBUS (not physical memory), and as before, the top 0x2000 (8Kb) of that is
* mapped to the IOPAGE.
*
* To map 18-bit UNIBUS addresses to 22-bit physical addresses, the 11/70 uses a UNIBUS relocation map.
* It consists of 31 double-word registers that each hold a 22-bit base address. When UNIBUS relocation
* is enabled, the top 5 bits of an address select one of the 31 mapping registers, and the bottom 13 bits
* are then added to the contents of the selected mapping register.
*
* ES6 ALERT: By using octal constants, I'm finally dipping my toe into ES6 (aka ECMAScript 2015) waters.
* You'll even see a few binary constants below, too. If you're loading this raw source code into your browser,
* then by now (2016) you're almost certainly using an ES6-aware browser. Production sites should be using code
* compiled by Google's Closure Compiler, which we configure to produce code that's backward-compatible with ES5
* (for example, all binary, octal, and hex constants are converted to decimal values).
*
* For more details: https://github.com/google/closure-compiler/wiki/ECMAScript6
*/
UNIBUS: { //16-bit 18-bit 22-bit Hex Description
UNIMAP: 0o170200, // UNIBUS Mapping Registers (0-31) 64 words (ends at 0o170372)
SIPDR0: 0o172200, // Supervisor I Page Descriptor Register 0
SIPDR1: 0o172202, // Supervisor I Page Descriptor Register 1
SIPDR2: 0o172204, // Supervisor I Page Descriptor Register 2
SIPDR3: 0o172206, // Supervisor I Page Descriptor Register 3
SIPDR4: 0o172210, // Supervisor I Page Descriptor Register 4
SIPDR5: 0o172212, // Supervisor I Page Descriptor Register 5
SIPDR6: 0o172214, // Supervisor I Page Descriptor Register 6
SIPDR7: 0o172216, // Supervisor I Page Descriptor Register 7
SDPDR0: 0o172220, // Supervisor D Page Descriptor Register 0
SDPDR1: 0o172222, // Supervisor D Page Descriptor Register 1
SDPDR2: 0o172224, // Supervisor D Page Descriptor Register 2
SDPDR3: 0o172226, // Supervisor D Page Descriptor Register 3
SDPDR4: 0o172230, // Supervisor D Page Descriptor Register 4
SDPDR5: 0o172232, // Supervisor D Page Descriptor Register 5
SDPDR6: 0o172234, // Supervisor D Page Descriptor Register 6
SDPDR7: 0o172236, // Supervisor D Page Descriptor Register 7
SIPAR0: 0o172240, // Supervisor I Page Address Register 0
SIPAR1: 0o172242, // Supervisor I Page Address Register 1
SIPAR2: 0o172244, // Supervisor I Page Address Register 2
SIPAR3: 0o172246, // Supervisor I Page Address Register 3
SIPAR4: 0o172250, // Supervisor I Page Address Register 4
SIPAR5: 0o172252, // Supervisor I Page Address Register 5
SIPAR6: 0o172254, // Supervisor I Page Address Register 6
SIPAR7: 0o172256, // Supervisor I Page Address Register 7
SDPAR0: 0o172260, // Supervisor D Page Address Register 0
SDPAR1: 0o172262, // Supervisor D Page Address Register 1
SDPAR2: 0o172264, // Supervisor D Page Address Register 2
SDPAR3: 0o172266, // Supervisor D Page Address Register 3
SDPAR4: 0o172270, // Supervisor D Page Address Register 4
SDPAR5: 0o172272, // Supervisor D Page Address Register 5
SDPAR6: 0o172274, // Supervisor D Page Address Register 6
SDPAR7: 0o172276, // Supervisor D Page Address Register 7
KIPDR0: 0o172300, // Kernel I Page Descriptor Register 0
KIPDR1: 0o172302, // Kernel I Page Descriptor Register 1
KIPDR2: 0o172304, // Kernel I Page Descriptor Register 2
KIPDR3: 0o172306, // Kernel I Page Descriptor Register 3
KIPDR4: 0o172310, // Kernel I Page Descriptor Register 4
KIPDR5: 0o172312, // Kernel I Page Descriptor Register 5
KIPDR6: 0o172314, // Kernel I Page Descriptor Register 6
KIPDR7: 0o172316, // Kernel I Page Descriptor Register 7
KDPDR0: 0o172320, // Kernel D Page Descriptor Register 0
KDPDR1: 0o172322, // Kernel D Page Descriptor Register 1
KDPDR2: 0o172324, // Kernel D Page Descriptor Register 2
KDPDR3: 0o172326, // Kernel D Page Descriptor Register 3
KDPDR4: 0o172330, // Kernel D Page Descriptor Register 4
KDPDR5: 0o172332, // Kernel D Page Descriptor Register 5
KDPDR6: 0o172334, // Kernel D Page Descriptor Register 6
KDPDR7: 0o172336, // Kernel D Page Descriptor Register 7
KIPAR0: 0o172340, // Kernel I Page Address Register 0
KIPAR1: 0o172342, // Kernel I Page Address Register 1
KIPAR2: 0o172344, // Kernel I Page Address Register 2
KIPAR3: 0o172346, // Kernel I Page Address Register 3
KIPAR4: 0o172350, // Kernel I Page Address Register 4
KIPAR5: 0o172352, // Kernel I Page Address Register 5
KIPAR6: 0o172354, // Kernel I Page Address Register 6
KIPAR7: 0o172356, // Kernel I Page Address Register 7
KDPAR0: 0o172360, // Kernel D Page Address Register 0
KDPAR1: 0o172362, // Kernel D Page Address Register 1
KDPAR2: 0o172364, // Kernel D Page Address Register 2
KDPAR3: 0o172366, // Kernel D Page Address Register 3
KDPAR4: 0o172370, // Kernel D Page Address Register 4
KDPAR5: 0o172372, // Kernel D Page Address Register 5
KDPAR6: 0o172374, // Kernel D Page Address Register 6
KDPAR7: 0o172376, // Kernel D Page Address Register 7
MMR3: 0o172516, // 772516 17772516
RLCS: 0o174400, // RL11 Control Status Register
RLBA: 0o174402, // RL11 Bus Address Register
RLDA: 0o174404, // RL11 Disk Address Register
RLMP: 0o174406, // RL11 Multi-Purpose Register
RLBE: 0o174410, // RL11 Bus (Address) Extension Register (RLV12 controller only)
DL11: 0o176500, // DL11 Additional Register Range (ends at 0o176676)
RXCS: 0o177170, // RX11 Command and Status Register
RXDB: 0o177172, // RX11 Data Buffer Register
RKDS: 0o177400, // RK11 Drive Status Register
RKER: 0o177402, // RK11 Error Register
RKCS: 0o177404, // RK11 Control Status Register
RKWC: 0o177406, // RK11 Word Count Register
RKBA: 0o177410, // RK11 Bus Address Register
RKDA: 0o177412, // RK11 Disk Address Register
RKUN: 0o177414, // RK11 UNUSED (just to make it clear we didn't forget something)
RKDB: 0o177416, // RK11 Data Buffer Register
LKS: 0o177546, // KW11-L Clock Status
PRS: 0o177550, // PC11 (and PR11) Reader Status Register
PRB: 0o177552, // PC11 (and PR11) Reader Buffer Register
PPS: 0o177554, // PC11 Punch Status Register
PPB: 0o177556, // PC11 Punch Buffer Register
RCSR: 0o177560, // DL11 Receiver Status Register
RBUF: 0o177562, // DL11 Receiver Data Buffer Register
XCSR: 0o177564, // DL11 Transmitter Status Register
XBUF: 0o177566, // DL11 Transmitter Data Buffer Register
CNSW: 0o177570, // Console (Front Panel) Switch/Display Register
MMR0: 0o177572, // 777572 17777572
MMR1: 0o177574, // 777574 17777574
MMR2: 0o177576, // 777576 17777576
UIPDR0: 0o177600, // User I Page Descriptor Register 0
UIPDR1: 0o177602, // User I Page Descriptor Register 1
UIPDR2: 0o177604, // User I Page Descriptor Register 2
UIPDR3: 0o177606, // User I Page Descriptor Register 3
UIPDR4: 0o177610, // User I Page Descriptor Register 4
UIPDR5: 0o177612, // User I Page Descriptor Register 5
UIPDR6: 0o177614, // User I Page Descriptor Register 6
UIPDR7: 0o177616, // User I Page Descriptor Register 7
UDPDR0: 0o177620, // User D Page Descriptor Register 0
UDPDR1: 0o177622, // User D Page Descriptor Register 1
UDPDR2: 0o177624, // User D Page Descriptor Register 2
UDPDR3: 0o177626, // User D Page Descriptor Register 3
UDPDR4: 0o177630, // User D Page Descriptor Register 4
UDPDR5: 0o177632, // User D Page Descriptor Register 5
UDPDR6: 0o177634, // User D Page Descriptor Register 6
UDPDR7: 0o177636, // User D Page Descriptor Register 7
UIPAR0: 0o177640, // User I Page Address Register 0
UIPAR1: 0o177642, // User I Page Address Register 1
UIPAR2: 0o177644, // User I Page Address Register 2
UIPAR3: 0o177646, // User I Page Address Register 3
UIPAR4: 0o177650, // User I Page Address Register 4
UIPAR5: 0o177652, // User I Page Address Register 5
UIPAR6: 0o177654, // User I Page Address Register 6
UIPAR7: 0o177656, // User I Page Address Register 7
UDPAR0: 0o177660, // User D Page Address Register 0
UDPAR1: 0o177662, // User D Page Address Register 1
UDPAR2: 0o177664, // User D Page Address Register 2
UDPAR3: 0o177666, // User D Page Address Register 3
UDPAR4: 0o177670, // User D Page Address Register 4
UDPAR5: 0o177672, // User D Page Address Register 5
UDPAR6: 0o177674, // User D Page Address Register 6
UDPAR7: 0o177676, // User D Page Address Register 7
R0SET0: 0o177700, //
R1SET0: 0o177701, //
R2SET0: 0o177702, //
R3SET0: 0o177703, //
R4SET0: 0o177704, //
R5SET0: 0o177705, //
R6KERNEL: 0o177706, //
R7KERNEL: 0o177707, //
R0SET1: 0o177710, //
R1SET1: 0o177711, //
R2SET1: 0o177712, //
R3SET1: 0o177713, //
R4SET1: 0o177714, //
R5SET1: 0o177715, //
R6SUPER: 0o177716, //
R6USER: 0o177717, //
/*
* This next group of registers is largely ignored; all accesses are routed to regsControl[],
* and therefore are managed as a block of 8 "CTRL" registers.
*/
CTRL: 0o177740,
LAERR: 0o177740, // Low Address Error (11/70 only)
HAERR: 0o177742, // High Address Error (11/70 only)
MEMERR: 0o177744, // Memory System Error (11/70 only)
CACHEC: 0o177746, // Cache Control (11/70 only)
MAINT: 0o177750, // Maintenance (11/70 only)
HITMISS: 0o177752, // Hit/Miss (11/70 only)
UNDEF1: 0o177754, //
UNDEF2: 0o177756, //
LSIZE: 0o177760, // Lower Size Register (last 64-byte block #) (11/70 only)
HSIZE: 0o177762, // Upper Size Register (always zero) (11/70 only)
SYSID: 0o177764, // System ID Register (11/70 only)
CPUERR: 0o177766, // CPU error (11/70 only)
MB: 0o177770, // Microprogram break (11/70 only)
PIR: 0o177772, // Program Interrupt Request
SL: 0o177774, // Stack Limit Register
PSW: 0o177776 // 777776 17777776 0x3FFFFE Processor Status Word
},
DL11: { // Serial Line Interface (program compatible with the KL11 for control of console teleprinters)
PRI: 4,
RVEC: 0o060,
XVEC: 0o064,
RCSR: { // 177560: DL11 Receiver Status Register
RE: 0x0001, // Reader Enable (W/O)
DTR: 0x0002, // Data Terminal Ready (R/W)
RTS: 0x0004, // Request To Send (R/W)
STD: 0x0008, // Secondary Transmitted Data (R/W)
DIE: 0x0020, // Dataset Interrupt Enable (R/W)
RIE: 0x0040, // Receiver Interrupt Enable (R/W)
RD: 0x0080, // Receiver Done (R/O)
SRD: 0x0400, // Secondary Received Data (R/O)
RA: 0x0800, // Receiver Active (R/O)
CD: 0x1000, // Carrier Detect (R/O)
CTS: 0x2000, // Clear To Send (R/O)
RI: 0x4000, // Ring Indicator (R/O)
DSC: 0x8000, // Dataset Status Change (R/O)
RMASK: 0xFFFE, // bits readable (TODO: All I know for sure is that bit 0 is NOT readable; see readRCSR())
WMASK: 0x006F, // bits writable
RS232: 0x0006, // bits affecting RS-232 status updates
BAUD: 9600
},
RBUF: { // 177562: DL11 Receiver Data Buffer Register
DATA: 0x00ff, // Received Data (R/O)
PARITY: 0x1000, // Received Data Parity (R/O)
FE: 0x2000, // Framing Error (R/O)
OE: 0x4000, // Overrun Error (R/O)
ERROR: 0x8000 // Error (R/O)
},
XCSR: { // 177564: DL11 Transmitter Status Register
BREAK: 0x0001, // BREAK (R/W)
MAINT: 0x0004, // Maintenance (R/W)
TIE: 0x0040, // Transmitter Interrupt Enable (R/W)
READY: 0x0080, // Transmitter Ready (R/O)
RMASK: 0x00C5,
WMASK: 0x0045,
BAUD: 9600
},
XBUF: { // 177566: DL11 Transmitter Data Buffer Register
DATA: 0x00FF // Transmitted Data (W/O) (TODO: Determine why pdp11.js effectively defined this as 0x7F)
}
},
KW11: { // KW11-L Line Time Clock (60Hz; well, OK, or 50Hz, if you're in the UK, I suppose...)
PRI: 6,
VEC: 0o100,
DELAY: 0,
LKS: { // 177546: KW11-L Clock Status
IE: 0x0040, // Interrupt Enable
MON: 0x0080, // Monitor
MASK: 0x00C0 // these are the only bits that can read or written
}
},
PC11: { // High Speed Reader & Punch (PR11 is a Reader-only unit)
PRI: 4, // NOTE: reader has precedence over punch
RVEC: 0o070, // reader vector
PVEC: 0o074, // punch vector
PRS: { // 177550: PC11 (and PR11) Reader Status Register
RE: 0x0001, // (000001) Reader Enable (W/O)
IE: 0x0040, // (000100) Reader Interrupt Enable (allows the DONE and ERROR bits to trigger an interrupt)
DONE: 0x0080, // (000200) Done (R/O)
BUSY: 0x0800, // (004000) Busy (R/O)
ERROR: 0x8000, // (100000) Error (R/O)
CLEAR: 0x08C0, // (004300) bits cleared on INIT
RMASK: 0xFFFE, // (177776) bits readable (TODO: All I know for sure is that bit 0 is NOT readable; see readPRS())
WMASK: 0x0041, // (000101) bits writable
BAUD: 3600
},
PRB: { // 177552: PC11 (and PR11) Reader Buffer Register
MASK: 0x00FF // Data
},
PPS: { // 177554: PC11 Punch Status Register
IE: 0x0040, // Interrupt Enable
RDY: 0x0080, // Ready
ERROR: 0x8000, // Error (eg, no tape in punch, or punch has no power)
WMASK: 0x0040, // bits writable
BAUD: 600
},
PPB: { // 177556: PC11 Punch Buffer Register
MASK: 0x00FF // Data
}
},
RK11: { // RK11 Disk Controller
PRI: 5,
VEC: 0o220,
DRIVES: 8, // maximum of 8 drives
RKDS: { // 177400: Drive Status Register
SC: 0x000F, // (000017) Sector Counter
SCESA: 0x0010, // (000020) Sector Counter Equals Sector Address
WPS: 0x0020, // (000040) Write Protected Status (set if write-protected)
RRDY: 0x0040, // (000100) Read/Write/Seek Ready
DRDY: 0x0080, // (000200) Drive Ready
SOK: 0x0100, // (000400) Sector Counter OK
SIN: 0x0200, // (001000) Seek Incomplete
DRU: 0x0400, // (002000) Drive Unsafe
RK05: 0x0800, // (004000) RK05 is the selected disk drive (always set)
DPL: 0x1000, // (010000) Drive Power Low
ID: 0xE000, // (160000) Drive ID (logical drive number of an interrupting drive)
SHIFT: {
ID: 13
}
},
RKER: { // 177402: Error Register
WCE: 0x0001, // Write Check Error
CSE: 0x0002, // Checksum Error
SE: 0x0003, // Soft Error bits (cleared at the start of a new function)
UNUSED: 0x001C, // unused (returns zero)
NXS: 0x0020, // Non-Existent Sector
NXC: 0x0040, // Non-Existent Cylinder
NXD: 0x0080, // Non-Existent Disk
TE: 0x0100, // Timing Error
DLT: 0x0200, // Date Late
NXM: 0x0400, // Non-Existent Memory
PGE: 0x0800, // Programming Error
SKE: 0x1000, // Seek Error
WLO: 0x2000, // Write Lock-Out Violation
OVR: 0x4000, // Overrun
DRE: 0x8000, // Drive Error
HE: 0x7FE0 // Hard Error bits (cleared only by Bus RESET or RK11 CRESET function)
},
RKCS: { // 177404: Control Status Register
GO: 0x0001, // (000001) Go (W/O)
FUNC: 0x000E, // (000016) Function Code (F2,F1,F0) (R/W)
MEX: 0x0030, // (000060) Memory Extension (R/W)
IE: 0x0040, // (000100) Interrupt Enable (R/W)
CRDY: 0x0080, // (000200) Controller Ready (R/O)
SSE: 0x0100, // (000400) Stop on Soft Error (R/W)
EXB: 0x0200, // (001000) Extra Bit (R/W)
FMT: 0x0400, // (002000) Format (R/W)
IBA: 0x0800, // (004000) Inhibit RKBA Increment (R/W)
SCP: 0x2000, // (020000) Search Complete (R/O)
HE: 0x4000, // (040000) Hard Error (R/O)
ERR: 0x8000, // (100000) Composite Error (R/O) (set when any RKER bit is set)
UNUSED: 0x1200, // (011000) unused
RMASK: 0xEFFE, // (167776) bits readable
WMASK: 0x0F7F, // (007577) bits writable
SHIFT: {
FUNC: 1,
MEX: 4
}
},
RKDA: { // 177412: Disk Address Register
SA: 0x000F, // (000017) Sector Address
HS: 0x0010, // (000020) Head Select (aka SUR: clear for upper disk head, set for lower)
CA: 0x1FE0, // (017740) Cylinder Address (aka CYL ADDR)
DS: 0xE000, // (160000) Drive Select (aka DR SEL)
SHIFT: {
HS: 4,
CA: 5,
DS: 13
}
},
FUNC: {
CRESET: 0b0000, // (00) Controller Reset
WRITE: 0b0010, // (02) Write
READ: 0b0100, // (04) Read
WCHK: 0b0110, // (06) Write Check
SEEK: 0b1000, // (10) Seek
RCHK: 0b1010, // (12) Read Check
DRESET: 0b1100, // (14) Drive Reset
WLOCK: 0b1110 // (16) Write Lock
}
},
RL11: { // RL11 Disk Controller
PRI: 5,
VEC: 0o160,
DRIVES: 4, // maximum of 4 drives
PREFIX: "DY",
RLCS: { // 174400: Control Status Register
DRDY: 0x0001, // (000001) Drive Ready (R/O)
FUNC: 0x000E, // (000016) Function Code (F2,F1,F0) (R/W)
BAE: 0x0030, // (000060) Bus Address Extension bits (BA17,BA16) (R/W)
IE: 0x0040, // (000100) Interrupt Enable (R/W)
CRDY: 0x0080, // (000200) Controller Ready (R/W)
DS: 0x0300, // (001400) Drive Select (DS1,DS0) (R/W)
ERRC: 0x3C00, // (036000) Error Code (R/O)
DE: 0x4000, // (040000) Drive Error (R/O)
ERR: 0x8000, // (100000) Composite Error (R/O)
CLEAR: 0x3F7E, // (037576) bits cleared on INIT (bits 1-6 and 8-13 are cleared)
SET: 0x0080, // (000200) bits set on INIT (bit 7 is set)
RMASK: 0xFFFF, // (177777) no write-only bits
WMASK: 0x03FE, // (001776) bits writable
SHIFT: {
FUNC: 1,
BAE: 4,
DS: 8
}
},
RLBA: { // 174402: Bus Address Register
WMASK: 0xFFFE // bit 0 is effectively not writable (always zero)
},
/*
* This register has 3 formats: one for Seek, another for Read/Write, and a third for Get Status
*/
RLDA: { // 174404: Disk Address Register
SEEK_CMD: 0x0001, // Seek: bit 0 must be set, bits 1 and 3 must be clear
SEEK_DIR: 0x0004, // Direction (clear to move heads away from spindle (lower cylinder), set to move to higher cylinder)
SEEK_HS: 0x0010, // Head Select (clear to select upper head, set to select lower head)
SEEK_CAD: 0xFF80, // Cylinder Address Difference
RW_SA: 0x003F, // Sector Address
RW_HS: 0x0040, // Head Select
RW_CA: 0xFF80, // Cylinder Address (RL01 has 256 cylinders, RL02 has 512)
GS_CMD: 0x0003, // Get Status: bit 0 must be set, bit 1 set, and bits 2 and 4-7 clear (bits 8-15 unused)
GS_RST: 0x0008, // Reset (when set, clears error register before sending status word to controller)
SHIFT: {
RW_HS: 6,
RW_CA: 7
}
},
/*
* This register has 3 formats: one for Read Header, another for Read/Write, and a third for Get Status
*/
RLMP: { // 177406: Multi-Purpose Register
GS_ST: { // Major State Code (of the drive)
LOADC: 0x0, // Load Cartridge
SPINUP: 0x1, // Spin-Up
BRUSHC: 0x2, // Brush Cycle
LOADH: 0x3, // Load Heads
SEEK: 0x4, // Seek
LOCKON: 0x5, // Lock On
UNLOADH:0x6, // Unload Heads
SPINDN: 0x7 // Spin-Down
},
GS_BH: 0x0008, // Brushes Home
GS_HO: 0x0010, // Heads Out
GS_CO: 0x0020, // Cover Open (or dust cover is not in place)
GS_HS: 0x0040, // Head Selected (0 for upper head, 1 for lower head)
GS_DT: 0x0080, // Drive Type (0 for RL01, 1 for RL02)
GS_DSE: 0x0100, // Drive Select Error
GS_VC: 0x0200, // Volume Check (Set during transition from a head load state to a head-on-track state; cleared by execution of a Get Status command with Bit 3 asserted)
GS_WGE: 0x0400, // Write Gate Error
GS_SPE: 0x0800, // Spin Error
GS_SKTO: 0x1000, // Seek Time-Out
GS_WL: 0x2000, // Write Lock
GS_CHE: 0x4000, // Current Head Error
GS_WDE: 0x8000 // Write Data Error
},
RLBE: { // 174410: Bus (Address) Extension Register
MASK: 0x003F // bits 5-0 correspond to bus address bits 21-16
},
ERRC: { // NOTE: These error codes are pre-shifted to read/write directly from/to RLCS.ERRC
OPI: 0x0400, // Operation Incomplete
DCRC: 0x0800, // Read Data CRC
WCE: 0x0800, // Write Check Error
HCRC: 0x0C00, // Header CRC
DLT: 0x1000, // Data Late
HNF: 0x1400, // Header Not Found
NXM: 0x2000, // Non-Existent Memory
MPE: 0x2400 // Memory Parity Error (RLV12 only)
},
FUNC: { // NOTE: These function codes are pre-shifted to read/write directly from/to RLCS.FUNC
NOP: 0b0000, // (00) No-Op
WCHK: 0b0010, // (02) Write Check
STATUS: 0b0100, // (04) Get Status
SEEK: 0b0110, // (06) Seek
RHDR: 0b1000, // (10) Read Header
WDATA: 0b1010, // (12) Write Data
RDATA: 0b1100, // (14) Read Data
RDNC: 0b1110 // (16) Read Data without Header Check
}
},
RX11: { // RX11 Disk Controller
PRI: 5,
VEC: 0o264,
DRIVES: 2, // maximum of 2 drives
PREFIX: "DX",
RXCS: { // 177170: Command and Status Register
GO: 0x0001, // (000001) Go (W/O)
FUNC: 0x000E, // (000016) Function Code (F2,F1,F0) (W/O)
UNIT: 0x0010, // (000020) Unit Select (W/O)
DONE: 0x0020, // (000040) Done (R/O)
IE: 0x0040, // (000100) Interrupt Enable (R/W, cleared on INIT)
TR: 0x0080, // (000200) Transfer Request (R/O)
INIT: 0x4000, // (040000) RX11 Initialize (W/O)
ERR: 0x8000, // (100000) Error (R/O, cleared on INIT or command)
UNUSED: 0x3F00, // (037400) unused
RMASK: 0x80E0, // (100340) bits readable
WMASK: 0x405F // (040137) bits writable
},
RXDB: { // 177172: Data Buffer Register
},
RXTA: {
MASK: 0x007F
},
RXSA: {
MASK: 0x001F
},
RXES: {
/*
* The DRDY bit is only valid when retrieved via a Read Status function or at completion of Initialize when it indicates
* status of drive O. It is asserted if the unit currently selected exists, is properly supplied with power, has a diskette
* installed correctly, has its door closed, and has a diskette up to speed.
*
* If the Error bit was set in the RXCS but Error bits are not set in the RXES, then specific error conditions can be accessed via
* a Read Error Register function.
*/
CRC: 0x0001, // CRC error (RXES is moved to the RXDB, and Error and Done are asserted)
PARITY: 0x0002, // parity error (RXES is moved to the RXDB, and Error and Done are asserted)
ID: 0x0004, // Initialize Done (following a programmable or UNIBUS initialization, or a power failure)
DEL: 0x0040, // Deleted Data Detected
DRDY: 0x0080 // Drive Ready
},
FUNC: { // NOTE: These function codes are pre-shifted to read/write directly from/to RXCS.FUNC
FILL: 0b0000, // Fill Buffer
EMPTY: 0b0010, // Empty Buffer
WRITE: 0b0100, // Write Sector
READ: 0b0110, // Read Sector
UNUSED: 0b1000, // UNUSED
RDSTAT: 0b1010, // Read Status
WRDEL: 0b1100, // Write Deleted Data Sector
RDERR: 0b1110 // Read Error Register
},
ERROR: {
HOME0: 0o0010, // Drive 0 failed to see home on Initialize
HOME1: 0o0020, // Drive 1 failed to see home on Initialize
BAD_HOME: 0o0030, // Found home when stepping out 10 tracks for INIT
NO_TRACK: 0o0040, // Tried to access a track greater than 77
FOUND_HOME: 0o0050, // Home was found before desired track was reached
SELF_DIAG: 0o0060, // Self-diagnostic error
NO_SECTOR: 0o0070, // Desired sector could not be found after looking at 52 headers (2 revolutions)
NO_SEP: 0o0110, // More than 40us and no SEP clock seen
NO_PREAM: 0o0120, // A preamble could not be found
NO_IOMARK: 0o0130, // Preamble found but no I/O mark found within allowable time span
CRC_HEADER: 0o0140, // CRC error on what we thought was a header
BAD_TRACK: 0o0150, // The header track address of a good header does not compare with the desired track
NO_ID: 0o0160, // Too many tries for an IDAM (identifies header)
NO_DATA: 0o0170, // Data AM not found in allotted time
CRC_DATA: 0o0200, // CRC error on reading the sector from the disk (No code appears in the ERREG).
BAD_PARITY: 0o0210 // All parity errors
}
},
VECTORS: {
0o060: "DL11R",
0o064: "DL11X",
0o070: "PC11R",
0o074: "PC11X",
0o100: "KW11",
0o160: "RL11",
0o220: "RK11",
0o264: "RX11"
}
};
PDP11.RX11.RX01 = [
"DX",
77, 1, 26, 128, // disk geometry (CHSN: cylinders, heads, sectors/track, and bytes/sector)
1, 0, 0, 128, // boot code location (cylinder, head, sector index (NOT sector number), and number of bytes)
0 // default drive status
];
PDP11.RK11.RK05 = [
"RK",
203, 2, 12, 512, // disk geometry (CHSN: cylinders, heads, sectors/track, and bytes/sector)
0, 0, 0, 512, // boot code location (cylinder, head, sector index (NOT sector number), and number of bytes)
PDP11.RK11.RKDS.RK05 | PDP11.RK11.RKDS.SOK | PDP11.RK11.RKDS.RRDY
];
PDP11.RL11.RL02K = [
"RL",
512, 2, 40, 256, // disk geometry (CHSN: cylinders, heads, sectors/track, and bytes/sector)
0, 0, 0, 256, // boot code location (cylinder, head, sector index (NOT sector number), and number of bytes)
PDP11.RL11.RLMP.GS_ST.LOCKON | PDP11.RL11.RLMP.GS_BH | PDP11.RL11.RLMP.GS_HO
];
PDP11.ACCESS.READ_WORD = PDP11.ACCESS.WORD | PDP11.ACCESS.READ; // formerly READ_MODE (2)
PDP11.ACCESS.READ_BYTE = PDP11.ACCESS.BYTE | PDP11.ACCESS.READ; // formerly READ_MODE (2) | BYTE_MODE (1)
PDP11.ACCESS.WRITE_WORD = PDP11.ACCESS.WORD | PDP11.ACCESS.WRITE; // formerly WRITE_MODE (4)
PDP11.ACCESS.WRITE_BYTE = PDP11.ACCESS.BYTE | PDP11.ACCESS.WRITE; // formerly WRITE_MODE (4) | BYTE_MODE (1)
PDP11.ACCESS.UPDATE_WORD = PDP11.ACCESS.WORD | PDP11.ACCESS.UPDATE; // formerly MODIFY_WORD (2 | 4)
PDP11.ACCESS.UPDATE_BYTE = PDP11.ACCESS.BYTE | PDP11.ACCESS.UPDATE; // formerly MODIFY_BYTE (1 | 2 | 4)
/*
* PSW arithmetic flags are NOT stored directly into the PSW register; they are maintained across separate flag registers.
*/
PDP11.PSW.FLAGS = (PDP11.PSW.NF | PDP11.PSW.ZF | PDP11.PSW.VF | PDP11.PSW.CF);
/*
* Combine all the shared globals and machine-specific globals into one machine-specific global object,
* which all machine components should start using; eg: "if (PDP11.DEBUGGER)" instead of "if (DEBUGGER)".
*/
PDP11.APPCLASS = APPCLASS;
PDP11.APPNAME = APPNAME;
PDP11.DEBUGGER = DEBUGGER;
PDP11.BYTEARRAYS = BYTEARRAYS;
PDP11.TYPEDARRAYS = TYPEDARRAYS;
PDP11.MEMFAULT = MEMFAULT;
PDP11.WORDBUS = WORDBUS;
/**
* @copyright http://pcjs.org/modules/pdp11/lib/messages.js (C) Jeff Parsons 2012-2017
*/
var MessagesPDP11 = {
CPU: 0x00000001,
TRAP: 0x00000002,
FAULT: 0x00000004,
INT: 0x00000008,
BUS: 0x00000010,
MEMORY: 0x00000020,
MMU: 0x00000040,
ROM: 0x00000080,
DEVICE: 0x00000100,
PANEL: 0x00000200,
KEYBOARD: 0x00000400,
KEYS: 0x00000800,
PC11: 0x00001000,
PAPER: 0x00001000,
DISK: 0x00002000,
READ: 0x00004000,
WRITE: 0x00008000,
RK11: 0x00010000,
RL11: 0x00020000,
RX11: 0x00040000,
DL11: 0x00100000,
SERIAL: 0x00100000,
KW11: 0x00200000,
TIMER: 0x00200000,
SPEAKER: 0x01000000,
COMPUTER: 0x02000000,
LOG: 0x20000000,
WARN: 0x40000000,
HALT: 0x80000000|0
};
/*
* Message categories supported by the messageEnabled() function and other assorted message
* functions. Each category has a corresponding bit value that can be combined (ie, OR'ed) as
* needed. The Debugger's message command ("m") is used to turn message categories on and off,
* like so:
*
* m port on
* m port off
* ...
*
* NOTE: The order of these categories can be rearranged, alphabetized, etc, as desired; just be
* aware that changing the bit values could break saved Debugger states (not a huge concern, just
* something to be aware of).
*/
MessagesPDP11.CATEGORIES = {
"cpu": MessagesPDP11.CPU,
"trap": MessagesPDP11.TRAP,
"fault": MessagesPDP11.FAULT,
"int": MessagesPDP11.INT,
"bus": MessagesPDP11.BUS,
"memory": MessagesPDP11.MEMORY,
"mmu": MessagesPDP11.MMU,
"rom": MessagesPDP11.ROM,
"device": MessagesPDP11.DEVICE,
"panel": MessagesPDP11.PANEL,
"keyboard": MessagesPDP11.KEYBOARD, // "kbd" is also allowed as shorthand for "keyboard"; see doMessages()
"key": MessagesPDP11.KEYS, // using "key" instead of "keys", since the latter is a method on JavasScript objects
"pc11": MessagesPDP11.PC11,
"paper": MessagesPDP11.PAPER,
"disk": MessagesPDP11.DISK,
"read": MessagesPDP11.READ,
"write": MessagesPDP11.WRITE,
"rk11": MessagesPDP11.RK11,
"rl11": MessagesPDP11.RL11,
"rx11": MessagesPDP11.RX11,
"dl11": MessagesPDP11.DL11,
"serial": MessagesPDP11.SERIAL,
"kw11": MessagesPDP11.KW11,
"timer": MessagesPDP11.TIMER,
"speaker": MessagesPDP11.SPEAKER,
"computer": MessagesPDP11.COMPUTER,
/*
* Now we turn to message actions rather than message types; for example, setting "halt"
* on or off doesn't enable "halt" messages, but rather halts the CPU on any message above.
*
* Similarly, "m log on" turns on message logging, deferring the display of all messages
* until "m log off" is issued.
*/
"log": MessagesPDP11.LOG,
"warn": MessagesPDP11.WARN,
"halt": MessagesPDP11.HALT
};
/**
* @copyright http://pcjs.org/modules/pdp11/lib/panel.js (C) Jeff Parsons 2012-2017
*/
/**
* Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default,
* it deters us from defining named properties on our components; eg:
*
* this['exports'] = {...}
*
* results in an error:
*
* Cannot do '[]' access on a struct
*
* So, in order to define 'exports', we must override the @struct assumption by annotating
* the class as @unrestricted (or @dict). Note that this must be done both here and in the
* Component class, because otherwise the Compiler won't allow us to *reference* the named
* property either.
*
* TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to
* define every single property the class uses in its constructor, which results in a fair
* bit of redundant initialization, since many properties aren't (and don't need to be) fully
* initialized until the appropriate init(), reset(), restore(), etc. function is called.
*
* The upside, however, may be that since the structure of the class is completely defined by
* the constructor, JavaScript engines may be able to optimize and run more efficiently.
*
* @unrestricted
*/
class PanelPDP11 extends Component {
/**
* PanelPDP11(parmsPanel)
*
* The PanelPDP11 component has no required (parmsPanel) properties.
*
* @param {Object} parmsPanel
* @param {boolean} fBindings (true if panel may have bindings, otherwise not)
*/
constructor(parmsPanel, fBindings)
{
super("Panel", parmsPanel, MessagesPDP11.PANEL);
/*
* If there are any live registers, LEDs, etc, to display, this will provide a count.
* TODO: Add some UI for fDisplayLiveRegs (either an XML property, or a UI checkbox, or both).
*/
this.cLiveRegs = 0;
this.nDisplayCount = 0;
this.nDisplayLimit = 60;
this.fDisplayLiveRegs = true;
this.fBindings = fBindings;
/*
* regSwitches contains the Front Panel (aka Console) SWITCH register, which is also available
* as a read-only register at 177570 (but only the low 16 bits). regDisplay contains the DISPLAY
* register, a write-only register at the same address.
*
* regAddr is an internal register containing the contents of the Front Panel's ADDRESS display,
* and regData corresponds to the DATA display. They are updated by updateAddr() and updateData(),
* which in turn take care of calling updateLEDArray().
*
* The state of ALL switches is maintained in this.switches, and likewise all LED states are
* maintained in this.leds, but for convenience, we also mirror some of those states in dedicated
* variables (eg, regSwitches for the SWITCH register, fLEDTest for the 'TEST' switch, etc).
*/
this.regDisplay = 0;
this.regSwitches = 0;
this.regAddr = this.regData = 0;
this.ledAddr = this.ledData = -1;
/*
* The panel hardware has the following additional (supported) state; note that there are several
* settings on a real Front Panel that we don't support (eg, stepping one cycle vs. one instruction).
*
* While my initial intent is to eventually support all the ADDRSEL switch settings, I probably
* won't bother with any DATASEL switch settings; instead, I will automatically display the DISPLAY
* register (regDisplay) [the equivalent of selecting 'DISPLAY REGISTER'] except when data is being
* examined or deposited [the equivalent of selecting 'DATA PATHS'].
*/
this.fLEDTest = false; // LED (lamp) test in progress
this.fExamine = false; // true if the previously pressed switch was the 'EXAM' switch
this.fDeposit = false; // true if the previously pressed switch was the 'DEP' switch
this.nAddrSel = PanelPDP11.ADDRSEL.CONS_PHY;
/*
* Every LED has a simple numeric value, assigned when setBinding() is called:
*
* zero if "off", non-zero if "on"
*
* initBus() will call displayLEDs() to ensure that every LED is set to its initial value.
*/
this.leds = {};
/*
* Every switch has an array associated with it:
*
* [0]: initial value of switch (0 if "down", 1 if "up")
* [1]: current value of switch
* [2]: true if the switch is momentary, false if not
* [3]: true if the switch is currently pressed, false if released
* [4]: optional handler to call whenever the switch is pressed or released
* [5]: optional switch index (used with CNSW switches 'S0' through 'S21')
*
* initBus() will call displaySwitches() to ensure that every switch is the position represented below.
*
* NOTE: Not all switches have the same "process" criteria. For example, 'TEST' will perform a LED test
* when it is momentarily pressed "up", whereas 'LOAD [ADRS]' will load the ADDRESS register from the
* SWITCH register when it is momentarily pressed "down".
*
* This means that processLEDTest(value) must act when value == 1 ("up"), whereas processLoadAddr(value)
* must act when value == 0 ("down"). You can infer all this from the table below, because the initial value
* of any momentary switch is its "inactive" value, so the opposite is its "active" value.
*/
this.switches = {
'START': [1, 1, true, false, this.processStart],
'STEP': [1, 1, false, false, this.processStep],
'ENABLE': [1, 1, false, false, this.processEnable],
'CONT': [1, 1, true, false, this.processContinue],
'DEP': [0, 0, true, false, this.processDeposit],
'EXAM': [1, 1, true, false, this.processExamine],
'LOAD': [1, 1, true, false, this.processLoadAddr],
'TEST': [0, 0, true, false, this.processLEDTest]
};
for (var i = 0; i < 22; i++) {
this.switches['S'+i] = [0, 0, false, false, this.processSRSwitch, i];
}
/** @type {ComputerPDP11} */
this.cmp = null;
/** @type {BusPDP11} */
this.bus = null;
/** @type {CPUStatePDP11} */
this.cpu = null;
/** @type {DebuggerPDP11} */
this.dbg = null;
/*
* The 'hold' and 'toggle' exports, which map to holdSwitch() and toggleSwitch(), both press and release
* the specified switch, but processCommands() considers a 'hold' function to be asynchronous, which means
* that holdSwitch() will be passed a callback function that can be used to implement a delay between the
* press and the release, whereas toggleSwitch() will not.
*
* holdSwitch() only makes sense for momentary switches (eg, 'TEST'), where a visual delay might be nice.
* If the switch isn't momentary, or no delay is desired, then use toggleSwitch(); it will be more efficient.
*
* Finally, for switches that are toggles (eg, 'ENABLE'), you can use setSwitch() to set it to a specific
* state: zero for "off" and non-zero for "on". setSwitch() also supports meta-switches like "SR", using
* the entire value to set a series of switches at once; the value is assumed to be octal unless overridden
* by a prefix (eg, "0x") or suffix (eg, ".").
*/
this['exports'] = {
'hold': this.holdSwitch,
'toggle': this.toggleSwitch,
'reset': this.resetSwitches,
'set': this.setSwitch
};
this.setReady();
}
/**
* getAR()
*
* @this {PanelPDP11}
* @return {number} (current ADDRESS register)
*/
getAR()
{
return this.regAddr;
}
/**
* setAR(value)
*
* @this {PanelPDP11}
* @param {number} value (new ADDRESS register)
*/
setAR(value)
{
this.updateAddr(this.regAddr = value);
}
/**
* getDR()
*
* @this {PanelPDP11}
* @return {number} (current DISPLAY register)
*/
getDR()
{
return this.regDisplay;
}
/**
* setDR(value)
*
* @this {PanelPDP11}
* @param {number} value (new DISPLAY register)
* @return {number}
*/
setDR(value)
{
return this.updateData(this.regDisplay = value);
}
/**
* getSR()
*
* @this {PanelPDP11}
* @return {number} (current SWITCH register)
*/
getSR()
{
return this.regSwitches;
}
/**
* setSR(value)
*
* @this {PanelPDP11}
* @param {number} value (new SWITCH register)
*/
setSR(value)
{
this.setSRSwitches(value);
}
/**
* getSwitch(name)
*
* @this {PanelPDP11}
* @param {string} name
* @return {number|undefined} 0 if switch is off ("down"), 1 if on ("up"), or undefined if unrecognized
*/
getSwitch(name)
{
return this.switches[name] && this.switches[name][1];
}
/**
* reset(fPowerUp)
*
* NOTE: Since we've registered our handler with the Bus component, we will be called twice whenever
* the entire machine is reset: once when the Computer's reset() handler calls the Bus's reset() handler,
* and again when the Computer's reset() handler calls us directly. Multiple resets should be harmless.
*
* @this {PanelPDP11}
* @param {boolean} [fPowerUp]
*/
reset(fPowerUp)
{
/*
* Simulate a call to our stop() handler, to update the panel's ADDRESS register with the current PC.
*/
this.stop();
if (fPowerUp) this.setDR(0);
}
/**
* setBinding(sType, sBinding, control, sValue)
*
* Some panel layouts don't have bindings of their own, and even when they do, there may still be some
* components (eg, the CPU) that prefer to update their own bindings, so we pass along all binding requests
* to the Computer, CPU, Keyboard and Debugger components first. The order shouldn't matter, since any
* component that doesn't recognize the specified binding should simply ignore it.
*
* @this {PanelPDP11}
* @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc)
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset")
* @param {HTMLElement} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sType, sBinding, control, sValue)
{
if (this.cmp && this.cmp.setBinding(sType, sBinding, control, sValue)) {
return true;
}
if (this.cpu && this.cpu.setBinding(sType, sBinding, control, sValue)) {
return true;
}
if (DEBUGGER && this.dbg && this.dbg.setBinding(sType, sBinding, control, sValue)) {
return true;
}
switch (sBinding) {
case 'R0':
case 'R1':
case 'R2':
case 'R3':
case 'R4':
case 'R5':
case 'R6':
case 'R7':
case 'NF':
case 'ZF':
case 'VF':
case 'CF':
case 'PS':
this.bindings[sBinding] = control;
this.cLiveRegs++;
return true;
default:
/*
* Square ("led") or round ("rled") LEDs are defined in machine XML files like so:
*
* <control type="rled" binding="A3" value="1" width="100%" container="center"/>
*
* Only *type* and *binding* attributes are required; if *value* is omitted, the default value is 0 ("off").
*/
if (sType == "led" || sType == "rled") {
this.bindings[sBinding] = control;
this.leds[sBinding] = sValue? 1 : 0;
this.cLiveRegs++;
return true;
}
/*
* Switches are defined in machine XML files like so:
*
* <control type="switch" binding="S3" value="1" width="100%" container="center"/>
*
* Only *type* and *binding* attributes are required; if *value* is omitted, the default value is 0 ("down").
*
* Currently, there is no XML attribute to indicate whether a switch is "momentary"; only recognized switches
* in our internal table can have that attribute.
*/
if (sType == "switch") {
/*
* Like LEDs, we allow unrecognized switches to be defined as well, but they won't do anything useful,
* since only recognized switches will have handlers that perform the appropriate operations.
*/
if (this.switches[sBinding] === undefined) {
this.switches[sBinding] = [sValue? 1 : 0, sValue? 1 : 0];
}
this.bindings[sBinding] = control;
var parent = control.parentElement || control;
parent = parent.parentElement || parent;
parent.onmousedown = function(panel, sBinding) {
return function onPressSwitch() {
panel.pressSwitch(sBinding);
};
}(this, sBinding);
parent.onmouseup = parent.onmouseout = function(panel, sBinding) {
return function onReleaseSwitch() {
panel.releaseSwitch(sBinding);
};
}(this, sBinding);
parent.ontouchstart = function(panel, sBinding) {
return function onPressSwitch(event) {
panel.pressSwitch(sBinding);
event.preventDefault();
};
}(this, sBinding);
parent.ontouchend = function(panel, sBinding) {
return function onReleaseSwitch() {
panel.releaseSwitch(sBinding);
};
}(this, sBinding);
return true;
}
return super.setBinding(sType, sBinding, control, sValue);
}
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {PanelPDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
bus.addIOTable(this, PanelPDP11.UNIBUS_IOTABLE);
bus.addResetHandler(this.reset.bind(this));
this.displayLEDs();
this.displaySwitches();
}
/**
* powerUp(data, fRepower)
*
* @this {PanelPDP11}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
/*
* As noted in init(), our powerUp() method gives us a second opportunity to notify any
* components that that might care (eg, CPU, Keyboard, and Debugger) that we have some controls
* (ie, bindings) they might want to use.
*/
if (this.fBindings) PanelPDP11.init();
if (!data) {
this.reset(true);
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {PanelPDP11}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* save()
*
* This implements save support for the PanelPDP11 component.
*
* @this {PanelPDP11}
* @return {Object}
*/
save()
{
var state = new State(this);
state.set(0, [
this.getAR(),
this.getDR(),
this.getSR()
]);
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the PanelPDP11 component.
*
* @this {PanelPDP11}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
var a = data[0];
if (a) {
this.setAR(a[0]);
this.setDR(a[1]);
this.setSR(a[2]);
}
return true;
}
/**
* resetSwitches()
*
* @this {PanelPDP11}
* @return {boolean}
*/
resetSwitches()
{
for (var sBinding in this.switches) {
var sw = this.switches[sBinding];
sw[1] = sw[0];
}
this.displaySwitches();
return true;
}
/**
* displayLED(sBinding, value)
*
* @this {PanelPDP11}
* @param {string} sBinding
* @param {boolean|number} value (true or non-zero if the LED should be on, false or zero if off)
*/
displayLED(sBinding, value)
{
var control = this.bindings[sBinding];
if (control) {
/*
* TODO: Add support for user-definable LED colors?
*/
control.style.backgroundColor = (value? "#ff0000" : "#000000");
}
}
/**
* displayLEDs(override)
*
* @this {PanelPDP11}
* @param {boolean|number|null} [override] (true turn on all LEDs, false to turn off all LEDs, null or undefined for normal LED activity)
*/
displayLEDs(override)
{
for (var sBinding in this.leds) {
this.displayLED(sBinding, override != null? override : this.leds[sBinding]);
}
}
/**
* displaySwitch(sBinding, value)
*
* @this {PanelPDP11}
* @param {string} sBinding
* @param {boolean|number} value (true if the switch should be "up" (on), false if "down" (off))
*/
displaySwitch(sBinding, value)
{
var control = this.bindings[sBinding];
if (control) {
control.style.marginTop = (value? "0px" : "20px");
control.style.backgroundColor = (value? "#00ff00" : "#228B22");
}
}
/**
* displaySwitches()
*
* @this {PanelPDP11}
*/
displaySwitches()
{
for (var sBinding in this.switches) {
this.displaySwitch(sBinding, this.switches[sBinding][1]);
}
}
/**
* displayValue(sLabel, nValue, cch)
*
* This is principally for displaying register values, but in reality, it can be used to display any
* numeric value bound to the given label.
*
* @this {PanelPDP11}
* @param {string} sLabel
* @param {number} nValue
* @param {number} [cch]
*/
displayValue(sLabel, nValue, cch)
{
if (this.bindings[sLabel]) {
var sVal;
var nBase = this.dbg && this.dbg.nBase || 8;
nValue = nValue || 0;
if (!this.cpu.isRunning() || this.fDisplayLiveRegs) {
sVal = nBase == 8? Str.toOct(nValue, cch) : Str.toHex(nValue, cch);
} else {
sVal = "--------".substr(0, cch || 4);
}
/*
* TODO: Determine if this test actually avoids any redrawing when a register hasn't changed, and/or if
* we should maintain our own (numeric) cache of displayed register values (to avoid creating these temporary
* string values that will have to garbage-collected), and/or if this is actually slower, and/or if I'm being
* too obsessive.
*/
if (this.bindings[sLabel].textContent != sVal) this.bindings[sLabel].textContent = sVal;
}
}
/**
* holdSwitch(fnCallback, sBinding, sDelay)
*
* @this {PanelPDP11}
* @param {function()|null} fnCallback
* @param {string} sBinding
* @param {string} [sDelay]
* @return {boolean} false if wait required, true otherwise
*/
holdSwitch(fnCallback, sBinding, sDelay)
{
if (this.pressSwitch(sBinding)) {
if (sDelay) {
var panel = this;
setTimeout(function() {
panel.releaseSwitch(sBinding);
if (fnCallback) fnCallback();
}, +sDelay);
return false;
} else {
this.releaseSwitch(sBinding);
}
}
return true;
}
/**
* setSwitch(sBinding, sValue)
*
* @this {PanelPDP11}
* @param {string} sBinding
* @param {string} sValue
* @return {boolean}
*/
setSwitch(sBinding, sValue)
{
if (sBinding == "SR") {
return this.setSRSwitches(Str.parseInt(sValue, 8))
}
var sw = this.switches[sBinding];
if (sw) {
sw[1] = +sValue? 1 : 0;
this.displaySwitch(sBinding, sw[1]);
return true;
}
return false;
}
/**
* toggleSwitch(sBinding)
*
* @this {PanelPDP11}
* @param {string} sBinding
* @return {boolean}
*/
toggleSwitch(sBinding)
{
if (this.pressSwitch(sBinding)) {
this.releaseSwitch(sBinding);
return true;
}
return false;
}
/**
* pressSwitch(sBinding)
*
* @this {PanelPDP11}
* @param {string} sBinding
* @return {boolean}
*/
pressSwitch(sBinding)
{
var sw = this.switches[sBinding];
if (sw) {
/*
* Set the new switch value in sw[1] and then immediately display it
*/
this.displaySwitch(sBinding, (sw[1] = 1 - sw[1]));
/*
* Mark the switch as "pressed"
*/
sw[3] = true;
/*
* Call the appropriate process handler with the current switch value (sw[1])
*/
if (sw[4]) sw[4].call(this, sw[1], sw[5]);
/*
* This helps the next 'DEP' or 'EXAM' press determine if the previous press was the same,
* while also ignoring any intervening 'STEP' presses (see processStep() for why we do that).
*/
if (sBinding != PanelPDP11.SWITCH.STEP) {
this.fDeposit = (sBinding == PanelPDP11.SWITCH.DEP);
this.fExamine = (sBinding == PanelPDP11.SWITCH.EXAM);
}
return true;
}
return false;
}
/**
* releaseSwitch(sBinding)
*
* @this {PanelPDP11}
* @param {string} sBinding
* @return {boolean}
*/
releaseSwitch(sBinding)
{
/*
* pressSwitch() is simple: flip the switch's current value in sw[1] and marked it "pressed" in sw[3].
*
* releaseSwitch() is more complicated, because we must handle both mouseUp and mouseOut events. The first time
* we receive EITHER of those events AND the switch is marked momentary (sw[2]) AND the switch is pressed (sw[3]),
* then we must flip the switch back to its original value.
*
* Otherwise, the only thing we have to do is mark the switch as "released" (ie, set sw[3] to false).
*/
var sw = this.switches[sBinding];
if (sw) {
if (sw[2] && sw[3]) {
/*
* Set the new switch value in sw[1] and then immediately display it
*/
this.displaySwitch(sBinding, (sw[1] = sw[0]));
/*
* Call the appropriate process handler with the current switch value (sw[1])
*/
if (sw[4]) sw[4].call(this, sw[1], sw[5]);
}
/*
* Mark the switch as "released"
*/
sw[3] = false;
return true;
}
return false;
}
/**
* processStart(value, index)
*
* @this {PanelPDP11}
* @param {number} value
* @param {number} [index]
*/
processStart(value, index)
{
if (!value && !this.cpu.isRunning()) {
this.cpu.setPC(this.regAddr);
/*
* TODO: Verify what the PDP-11/70 Handbook means when it says that when the 'START' switch
* is depressed, "the computer system will be cleared." I take it to mean that it performs
* the equivalent of a RESET instruction.
*/
this.cpu.resetCPU();
/*
* The PDP-11/70 Handbook goes on to say: "If the system needs to be initialized but execution
* is not wanted, the START switch should be depressed while the HALT/ENABLE switch is in the HALT
* position."
*/
if (this.getSwitch(PanelPDP11.SWITCH.ENABLE)) {
this.cpu.startCPU();
}
}
}
/**
* processStep(value, index)
*
* If value == 1 (our initial value), then the 'STEP' switch is set to "S INST" (step one instruction);
* otherwise, it's set to "S BUS CYCLE" (step one bus cycle).
*
* However, since we can't currently support cycle-stepping, I've decided to innovate a little and
* change the meaning of this switch: the normal ("up") position means that successive 'EXAM' and 'DEP'
* operations will first add 2 to the ADDRESS register, while the opposite ("down") position means
* they will first subtract 2.
*
* See processLEDTest() for more of these exciting "innovations". ;-)
*
* @this {PanelPDP11}
* @param {number} value
* @param {number} [index]
*/
processStep(value, index)
{
/*
* There's really nothing for us to do here, because the normal press and release handlers
* already record the state of this switch, so it can be queried as needed, using getSwitch().
*/
}
/**
* processEnable(value, index)
*
* If value == 1 (our initial value), then the 'ENABLE'/'HALT' switch is set to 'ENABLE', otherwise 'HALT'.
*
* @this {PanelPDP11}
* @param {number} value
* @param {number} [index]
*/
processEnable(value, index)
{
/*
* The "down" (0) position is 'HALT', which stops the CPU; however, the "up" (1) position ('ENABLE')
* does NOT start the CPU. You must press 'CONT' to continue execution, which will either continue for
* one instruction if this switch to set to 'HALT' or indefinitely if it is set to 'ENABLE'.
*/
if (!value) {
this.cpu.stopCPU();
}
}
/**
* processContinue(value, index)
*
* @this {PanelPDP11}
* @param {number} value
* @param {number} [index]
*/
processContinue(value, index)
{
if (!value && !this.cpu.isRunning()) {
/*
* TODO: Technically, we're also supposed to check the 'STEP' switch to determine if we should
* step one instruction or just one cycle, but we don't currently have the ability to do the latter.
*/
if (!this.getSwitch(PanelPDP11.SWITCH.ENABLE)) {
/*
* Using the Debugger's stepCPU() function is more convenient, and has the pleasant side-effect
* of updating the debugger's display; however, not all machines with a Front Panel will necessarily
* also have the Debugger loaded.
*/
var dbg = this.dbg;
if (dbg && !dbg.isBusy(true)) {
dbg.setBusy(true);
dbg.stepCPU(0, null);
dbg.setBusy(false);
}
else {
/*
* For this tiny single-instruction burst, mimic what runCPU() does.
*/
try {
var nCyclesStep = this.cpu.stepCPU(1);
if (nCyclesStep > 0) {
this.cpu.updateTimers(nCyclesStep);
this.cpu.addCycles(nCyclesStep, true);
this.cpu.updateChecksum(nCyclesStep);
}
}
catch(exception) {
/*
* We assume that any numeric exception was explicitly thrown by the CPU to interrupt the
* current instruction. For all other exceptions, we attempt a stack dump.
*/
if (typeof exception != "number") {
var e = exception;
this.cpu.setError(e.stack || e.message);
}
}
}
/*
* Simulate a call to our stop() handler, to update the panel's ADDRESS register with the new PC.
*/
this.stop();
/*
* Going through the normal channels (ie, the Computer's updateDisplays() interface) ensures that
* ALL updateDisplay() handlers will be called, including ours.
*
* NOTE: If we used the Debugger's stepCPU() function, then that includes a call to updateDisplay();
* unfortunately, it will have happened BEFORE we called stop() to update the ADDRESS register, so
* we still need to call it again.
*/
if (this.cmp) this.cmp.updateDisplays();
}
else {
this.cpu.startCPU();
}
}
}
/**
* processDeposit(value, index)
*
* @this {PanelPDP11}
* @param {number} value
* @param {number} [index]
*/
processDeposit(value, index)
{
if (value && !this.cpu.isRunning()) {
if (this.fDeposit) this.advanceAddr();
/*
* This used to be updateData(), but that only updates regData, whereas setDR() updates both regData and regDisplay,
* and for these kinds of explicit Front Panel operations, I'm assuming the values should be synced.
*/
var w = this.setDR(this.regSwitches);
if (this.nAddrSel == PanelPDP11.ADDRSEL.CONS_PHY) {
/*
* TODO: Determine if this needs to take the UNIBUS map into consideration.
*/
this.bus.setWordDirect(this.regAddr, w);
} else {
/*
* TODO: This code is obviously incomplete, since it doesn't take into account the precise ADDRSEL mode.
*/
this.cpu.setWordSafe(this.regAddr, w);
}
}
}
/**
* processExamine(value, index)
*
* @this {PanelPDP11}
* @param {number} value
* @param {number} [index]
*/
processExamine(value, index)
{
if (!value && !this.cpu.isRunning()) {
var w;
if (this.fExamine) this.advanceAddr();
if (this.nAddrSel == PanelPDP11.ADDRSEL.CONS_PHY) {
/*
* TODO: Determine if this needs to take the UNIBUS map into consideration.
*/
w = this.bus.getWordDirect(this.regAddr);
} else {
/*
* TODO: This code is obviously incomplete, since it doesn't take into account the precise ADDRSEL mode.
*/
w = this.cpu.getWordSafe(this.regAddr);
}
/*
* This used to be updateData(), but that only updates regData, whereas setDR() updates both regData and regDisplay,
* and for these kinds of explicit Front Panel operations, I'm assuming the values should be synced.
*/
this.setDR(w);
}
}
/**
* processLoadAddr(value, index)
*
* @this {PanelPDP11}
* @param {number} value
* @param {number} [index]
*/
processLoadAddr(value, index)
{
if (!value && !this.cpu.isRunning()) {
this.updateAddr(this.regSwitches);
}
}
/**
* processLEDTest(value, index)
*
* @this {PanelPDP11}
* @param {number} value
* @param {number} [index]
*/
processLEDTest(value, index)
{
if (value) {
this.fLEDTest = true;
this.displayLEDs(true);
} else {
this.fLEDTest = false;
this.displayLEDs();
/*
* This is another one of my "innovations": when you're done testing the LEDs, all the switches reset as well.
*/
this.setSRSwitches(0);
}
}
/**
* processSRSwitch(value, index)
*
* @this {PanelPDP11}
* @param {number} value (normally 0 or 1, but we only depend on it being zero or non-zero)
* @param {number} index
*/
processSRSwitch(value, index)
{
if (value) {
this.regSwitches |= 1 << index;
} else {
this.regSwitches &= ~(1 << index);
}
}
/**
* advanceAddr()
*
* This should also take care of the following Front Panel behaviors when the accessing the general-purpose
* registers:
*
* 1) ADDRESS display incremented by 1 (instead of 2)
* 2) The STEP after the last register is 177700, such that the addresses are looped
*
* A third behavior is NOT emulated: preventing the ADDRESS from stepping to the first General Register (177700)
* from 177676.
*
* @this {PanelPDP11}
* @return {number}
*/
advanceAddr()
{
var nRegs = this.cpu.model <= PDP11.MODEL_1140? 8 : 16;
var fGenRegs = (this.regAddr >= PDP11.UNIBUS.R0SET0 /*177700*/ && this.regAddr < PDP11.UNIBUS.R0SET0 + nRegs);
var inc = fGenRegs? 1 : 2;
var mask = fGenRegs? 0xf : this.bus.nBusMask;
if (!this.getSwitch(PanelPDP11.SWITCH.STEP)) inc = -inc;
return this.updateAddr((this.regAddr & ~mask) | ((this.regAddr + inc) & mask));
}
/**
* updateAddr(value)
*
* @this {PanelPDP11}
* @param {number} value
* @return {number}
*/
updateAddr(value)
{
this.regAddr = value & this.bus.nBusMask;
if (this.ledAddr !== this.regAddr) {
this.ledAddr = this.regAddr;
this.updateLEDArray("A", this.ledAddr, 22);
}
return this.regAddr;
}
/**
* updateData(value)
*
* @this {PanelPDP11}
* @param {number} value
* @return {number}
*/
updateData(value)
{
this.regData = value & 0xffff;
if (this.ledData !== this.regData) {
this.ledData = this.regData;
this.updateLEDArray("D", this.ledData, 16);
}
return this.regData;
}
/**
* updateLED(sBinding, value)
*
* @this {PanelPDP11}
* @param {string} sBinding
* @param {number} value
* @return {number}
*/
updateLED(sBinding, value)
{
this.leds[sBinding] = value;
if (!this.fLEDTest) this.displayLED(sBinding, value);
return value;
}
/**
* updateLEDArray(sPrefix, value, nLEDs)
*
* @this {PanelPDP11}
* @param {string} sPrefix
* @param {number} value
* @param {number} nLEDs
*/
updateLEDArray(sPrefix, value, nLEDs)
{
for (var i = 0; i < nLEDs; i++) {
var sBinding = sPrefix + i;
this.updateLED(sBinding, value & (1 << i));
}
}
/**
* setSRSwitches(value)
*
* @this {PanelPDP11}
* @param {number|undefined} value
* @return {boolean}
*/
setSRSwitches(value)
{
this.regSwitches = value | 0;
for (var i = 0; i < 22; i++) {
this.switches['S'+i][1] = (this.regSwitches & (1 << i))? 1 : 0;
}
/*
* This (re)displays ALL switches, not merely the SR switches, but that's OK.
*/
this.displaySwitches();
return true;
}
/**
* stop(ms, nCycles)
*
* This is a notification handler, called by the Computer, to inform us the CPU has now stopped.
*
* @this {PanelPDP11}
* @param {number} [ms]
* @param {number} [nCycles]
*/
stop(ms, nCycles)
{
this.updateAddr(this.cpu.regsGen[7]);
}
/**
* setAddr(value, fActive)
*
* This interface is for passing new addresses to the Front Panel. However, whether or not this will become the
* ADDRESS actually displayed will depend on other settings (see updateStatus() for details).
*
* @this {PanelPDP11}
* @param {number} value
* @param {boolean} [fActive] (true if this should become the "active" ADDRESS regardless of other settings)
*/
setAddr(value, fActive)
{
this.regAddr = value;
}
/**
* setData(value, fActive)
*
* This interface is for passing new data to the Front Panel. However, whether or not this will become the
* DATA actually displayed will depend on the Front Panel's DATASEL switch setting, as well as the fActive flag.
*
* @this {PanelPDP11}
* @param {number} value
* @param {boolean} [fActive] (true if this should become the "active" DATA regardless of the DATASEL switch setting)
*/
setData(value, fActive)
{
if (!fActive) {
this.regData = value;
} else {
this.regDisplay = value;
}
}
/**
* updateDisplay(nUpdate)
*
* Called by the Computer component at intervals to update registers, LEDs, etc.
*
* @this {PanelPDP11}
* @param {number} [nUpdate] (-2 for power on, -1 for forced, > 0 for periodic, 0 or undefined otherwise)
*/
updateDisplay(nUpdate)
{
if (this.cLiveRegs) {
var fRunning = this.cpu.isRunning();
var fWaiting = this.cpu.isWaiting();
if (nUpdate < 0 || !fRunning || this.fDisplayLiveRegs) {
/*
* We arbitrarily separate the display elements into two categories: cheap and expensive.
*
* LEDs are considered cheap, register displays are not. So we'll skip the latter if this
* is a periodic update AND our periodic update counter hasn't reached the periodic update limit.
*/
if (nUpdate <= 0 || (this.nDisplayCount += nUpdate) >= this.nDisplayLimit) {
for (var i = 0; i < this.cpu.regsGen.length; i++) {
this.displayValue('R'+i, this.cpu.regsGen[i]);
}
var regPSW = this.cpu.getPSW();
this.displayValue("PS", regPSW);
this.displayValue("NF", (regPSW & PDP11.PSW.NF)? 1 : 0, 1);
this.displayValue("ZF", (regPSW & PDP11.PSW.ZF)? 1 : 0, 1);
this.displayValue("VF", (regPSW & PDP11.PSW.VF)? 1 : 0, 1);
this.displayValue("CF", (regPSW & PDP11.PSW.CF)? 1 : 0, 1);
this.nDisplayCount = 0;
}
/*
* Update the ADDRESS and DATA LEDs by selecting the appropriate values.
*
* TODO: There is currently no mechanism for selecting regData over regDisplay;
* we are acting as if the DATASEL switch setting is locked to "DISPLAY REGISTER".
*/
if (nUpdate < -1) {
this.regAddr = this.cpu.regsGen[7];
} else if (nUpdate > 0 && fRunning && !fWaiting) {
this.regAddr = this.cpu.getLastAddr();
}
this.updateAddr(this.regAddr);
this.updateData(this.regDisplay);
var bits = this.cpu.getMMUState();
/*
* Bit 0 set if 22-bit, bit 1 set if 18-bit, bit 2 set if 16-bit
*/
this.updateLED(PanelPDP11.LED.B22, bits & 1);
this.updateLED(PanelPDP11.LED.B18, bits & 2);
this.updateLED(PanelPDP11.LED.B16, bits & 4);
}
}
}
/**
* readCNSW(addr, fPreWrite)
*
* If fPreWrite, this is a read-before-write, so we must return the DISPLAY register (ie, regDisplay);
* otherwise, this a normal read, so we should return the SWITCH register (ie, regSwitches).
*
*
* @this {PanelPDP11}
* @param {number} addr (eg, PDP11.UNIBUS.CNSW or 177570)
* @param {boolean} [fPreWrite]
* @return {number}
*/
readCNSW(addr, fPreWrite)
{
return (fPreWrite? this.regDisplay : this.regSwitches) & 0xffff;
}
/**
* writeCNSW(value, addr)
*
* Handles writes to the DISPLAY register (ie, regDisplay).
*
* @this {PanelPDP11}
* @param {number} value
* @param {number} addr (eg, PDP11.UNIBUS.CNSW or 177570)
*/
writeCNSW(value, addr)
{
this.regDisplay = value;
}
/**
* PanelPDP11.init()
*
* This function operates on every HTML element of class "panel", extracting the
* JSON-encoded parameters for the PanelPDP11 constructor from the element's "data-value"
* attribute, invoking the constructor to create a PanelPDP11 component, and then binding
* any associated HTML controls to the new component.
*
* NOTE: Unlike most other component init() functions, this one is designed to be
* called multiple times: once at load time, so that we can bind our print()
* function to the panel's output control ASAP, and again when the Computer component
* is verifying that all components are ready and invoking their powerUp() functions.
*
* Our powerUp() method gives us a second opportunity to notify any components that
* that might care (eg, CPU, Keyboard, and Debugger) that we have some controls they
* might want to use.
*/
static init()
{
var aePanels = Component.getElementsByClass(document, PDP11.APPCLASS, "panel");
for (var iPanel=0; iPanel < aePanels.length; iPanel++) {
var ePanel = aePanels[iPanel];
var parmsPanel = Component.getComponentParms(ePanel);
var panel = Component.getComponentByID(parmsPanel['id']);
if (!panel) panel = new PanelPDP11(parmsPanel, true);
Component.bindComponentControls(panel, ePanel, PDP11.APPCLASS);
}
}
}
PanelPDP11.ADDRSEL = {
KERNEL_I: 0, // use a 16-bit virtual address where bits 16 to 21 are always OFF
KERNEL_D: 1, // use a 16-bit virtual address where bits 16 to 21 are always OFF
SUPER_I: 2, // use a 16-bit virtual address where bits 16 to 21 are always OFF
SUPER_D: 3, // use a 16-bit virtual address where bits 16 to 21 are always OFF
USER_I: 4, // use a 16-bit virtual address where bits 16 to 21 are always OFF
USER_D: 5, // use a 16-bit virtual address where bits 16 to 21 are always OFF
PROG_PHY: 6, // display the 22-bit physical address of the current bus cycle generated by the MMU
CONS_PHY: 7 // use a 22-bit physical address to perform console operations (e.g., LOAD ADRS, EXAM, & DEP)
};
/*
* To get the current state of a switch; eg::
*
* this.getSwitch(PanelPDP11.SWITCH.ENABLE)
*
* I haven't filled out this table, primarily it only needs to list switches we actually query
* (eg, non-momentary ones like 'ENABLE' and 'STEP', and 'EXAM' and 'DEP' since they have special
* "step" behavior when pressed more than once in a row). Ditto for the LED table.
*/
PanelPDP11.SWITCH = {
DEP: 'DEP',
ENABLE: 'ENABLE',
EXAM: 'EXAM',
STEP: 'STEP'
};
PanelPDP11.LED = {
B16: 'B16',
B18: 'B18',
B22: 'B22'
};
PanelPDP11.UNIBUS_IOTABLE = {
[PDP11.UNIBUS.CNSW]: /* 177570 */ [null, null, PanelPDP11.prototype.readCNSW, PanelPDP11.prototype.writeCNSW, "CNSW"]
};
/*
* Initialize every Panel module on the page.
*/
Web.onInit(PanelPDP11.init);
/**
* @copyright http://pcjs.org/modules/pdp11/lib/bus.js (C) Jeff Parsons 2012-2017
*/
/*
* Data types used by scanMemory()
*/
/**
* This defines the BlockInfo bit fields used by scanMemory() when it creates the aBlocks array.
*
* @typedef {{
* num: BitField,
* count: BitField,
* btmod: BitField,
* type: BitField
* }}
*/
var BlockInfoPDP11 = Usr.defineBitFields({num:20, count:8, btmod:1, type:3});
/**
* BusInfoPDP11 object definition (returned by scanMemory())
*
* cbTotal: total bytes allocated
* cBlocks: total Memory blocks allocated
* aBlocks: array of allocated Memory block numbers
*
* @typedef {{
* cbTotal: number,
* cBlocks: number,
* aBlocks: Array.<BlockInfoPDP11>
* }}
*/
var BusInfoPDP11;
class BusPDP11 extends Component {
/**
* BusPDP11(parmsBus, cpu, dbg)
*
* The BusPDP11 component manages physical memory and I/O address spaces.
*
* The BusPDP11 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.
*
* 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 BusPDP11 component via
* addMemory(). If the component needs something more than simple read/write storage,
* it must provide a custom controller.
*
* @param {Object} parmsBus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
constructor(parmsBus, cpu, dbg)
{
super("Bus", parmsBus, MessagesPDP11.BUS);
this.cpu = cpu;
this.dbg = dbg;
/*
* Supported values for nBusWidth are 16 (default), 18, and 22. This represents the maximum size
* of the bus for the life of the machine, regardless what memory management mode the CPU has enabled.
*/
this.nBusWidth = +parmsBus['busWidth'] || 16;
/*
* Compute all BusPDP11 memory block parameters now, based on the width of the bus.
*
* Note that all PCjs machines divide their address space into blocks, using a block size appropriate for
* the machine's bus width. This allows us to efficiently allocate the entire address space, by reusing blocks
* as appropriate, and to define to different address behaviors on a block-granular level.
*
* For PDPjs machines, the ideal block size is 8Kb (IOPAGE_LENGTH), the size of the IOPAGE on all PDP-11 machines;
* as a result, our IOController functions assume that all incoming offsets are within a single 8Kb block.
*/
this.addrTotal = 1 << this.nBusWidth;
this.nBusMask = (this.addrTotal - 1);
this.nBlockSize = BusPDP11.IOPAGE_LENGTH;
this.nBlockShift = Math.log2(this.nBlockSize); // ES6 ALERT (alternatively: Math.log(this.nBlockSize) / Math.LN2)
this.nBlockLen = this.nBlockSize >> 2;
this.nBlockLimit = this.nBlockSize - 1;
this.nBlockTotal = (this.addrTotal / this.nBlockSize) | 0;
this.nBlockMask = this.nBlockTotal - 1;
/*
* aIOHandlers is an array (ie, a hash) of I/O notification handlers, indexed by address, where each
* entry contains an array:
*
* [0]: readByte(addr)
* [1]: writeByte(b, addr)
* [2]: readWord(addr)
* [3]: writeWord(w, addr)
*
* Each of these 4-element arrays are similar to the memory access arrays assigned to entire Memory
* blocks, but these handlers generally target a specific address (or handful of addresses), while
* Memory access handlers must service the entire block; see the setAccess() function in the Memory
* component for details.
*
* Finally, for debugging purposes, if an I/O address has a symbolic name and message category,
* they will be saved here:
*
* [4]: symbolic name of I/O address
* [5]: message category
*
* UPDATE: The Debugger wants to piggy-back on these arrays to indicate addresses for which it wants
* notification. In those cases, the following additional element will be set:
*
* [6]: true to break on I/O, false to ignore I/O
*
* The false case is important if fIOBreakAll is set, because it allows the Debugger to selectively
* ignore specific addresses.
*/
this.aIOHandlers = [];
this.fIOBreakAll = false;
this.nDisableFaults = 0;
this.fFault = false;
/*
* Array of RESET notification handlers registered by Device components.
*/
this.afnReset = [];
/*
* Before we can add any memory blocks that declare our component as a custom memory controller,
* we must initialize the array that the getControllerAccess() method supplies to the Memory component.
*/
this.afnIOPage = [
BusPDP11.IOController.readByte,
BusPDP11.IOController.writeByte,
BusPDP11.IOController.readWord,
BusPDP11.IOController.writeWord
];
/*
* Define all the properties to be initialized by initMemory()
*/
this.aBusBlocks = this.aMemBlocks = [];
this.iBlockIOPageBus = this.iBlockIOPageMem = 0;
this.addrIOPage = this.nIOPageRange = this.nMemMask = 0;
/*
* We're ready to allocate empty Memory blocks to span the entire physical address space, including the
* initial location of the IOPAGE.
*/
this.initMemory();
this.setReady();
}
/**
* initMemory()
*
* Allocate enough (empty) Memory blocks to span the entire physical address space.
*
* Note that we now maintain two parallel arrays of these Memory blocks: aBusBlocks is for use by
* devices (or any component using the "direct" interfaces), while aMemBlocks is for use by the CPU.
*
* Whereas the Bus memory map is fixed at init time, the CPU's memory map will vary depending on MMU
* settings. The CPU will call setIOPageRange() as needed to update the range of addressible memory,
* which in turn will determine where the IOPAGE can be accessed.
*
* @this {BusPDP11}
*/
initMemory()
{
var block = new MemoryPDP11(this);
block.copyBreakpoints(this.dbg);
this.aBusBlocks = new Array(this.nBlockTotal);
this.aMemBlocks = new Array(this.nBlockTotal);
for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) {
this.aBusBlocks[iBlock] = this.aMemBlocks[iBlock] = block;
}
/*
* NOTE: Don't confuse the Bus addrIOPage with the CPU's addrIOPage; ours is fixed,
* based on the machine's Bus width, whereas the CPU's varies according to the MMU setting.
*/
this.addrIOPage = this.addrTotal - BusPDP11.IOPAGE_LENGTH;
this.addMemory(this.addrIOPage, BusPDP11.IOPAGE_LENGTH, MemoryPDP11.TYPE.CONTROLLER, this);
this.iBlockIOPageBus = (this.addrIOPage & this.nBusMask) >>> this.nBlockShift;
this.iBlockIOPageMem = this.iBlockIOPageBus;
this.nIOPageRange = 0;
this.nMemMask = this.nBusMask;
}
/**
* setIOPageRange(nRange)
*
* This function is responsible for syncing the CPU memory map (aMemBlocks) with the Bus memory map (aBusBlocks)
* and then updating the location of the IOPAGE within the CPU's memory map. The location of the IOPAGE is always
* fixed at the top of the Bus address space, but it moves (logically) within the CPU's address space according
* to the CPU's current MMU settings, which nRange is a reflection of.
*
* @this {BusPDP11}
* @param {number} nRange (16, 18 or 22; 0 removes the IOPAGE altogether)
*/
setIOPageRange(nRange)
{
if (nRange != this.nIOPageRange) {
for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) {
this.aMemBlocks[iBlock] = this.aBusBlocks[iBlock];
}
this.nIOPageRange = 0;
this.nMemMask = this.nBusMask;
if (nRange) {
this.nIOPageRange = nRange;
var addr = (1 << nRange);
this.nMemMask = (addr - 1);
addr -= BusPDP11.IOPAGE_LENGTH;
this.iBlockIOPageMem = (addr & this.nMemMask) >>> this.nBlockShift;
this.aMemBlocks[this.iBlockIOPageMem] = this.aBusBlocks[this.iBlockIOPageBus];
}
}
}
/**
* getControllerBuffer(addr)
*
* Our Bus component also acts as custom memory controller for the IOPAGE, so it must also provide this function.
*
* @this {BusPDP11}
* @param {number} addr
* @return {Array} containing the buffer (and the offset within that buffer that corresponds to the requested block)
*/
getControllerBuffer(addr)
{
/*
* No buffer is required for the IOPAGE; all accesses go to registered I/O handlers or to fault().
*/
return [null, 0];
}
/**
* getControllerAccess()
*
* Our Bus component also acts as custom memory controller for the IOPAGE, so it must also provide this function.
*
* @this {BusPDP11}
* @return {Array.<function()>}
*/
getControllerAccess()
{
return this.afnIOPage;
}
/**
* getWidth()
*
* @this {BusPDP11}
* @return {number}
*/
getWidth()
{
return this.nBusWidth;
}
/**
* reset()
*
* Call all registered reset() handlers.
*
* @this {BusPDP11}
*/
reset()
{
for (var i = 0; i < this.afnReset.length; i++) {
this.afnReset[i]();
}
this.setIOPageRange(16);
}
/**
* powerUp(data, fRepower)
*
* @this {BusPDP11}
* @param {Object|null} data (always null because we supply no powerDown() handler)
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (!data) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {BusPDP11}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* save()
*
* @this {BusPDP11}
* @return {Object|null}
*/
save()
{
var state = new State(this);
state.set(0, this.saveMemory());
return state.data();
}
/**
* restore(data)
*
* @this {BusPDP11}
* @param {Object} data
* @return {boolean} true if restore successful, false if not
*/
restore(data)
{
return this.restoreMemory(data[0]);
}
/**
* addMemory(addr, size, type, 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. This helps prevent address calculation errors, redundant
* allocations, etc.
*
* We've relaxed some of the original requirements (ie, that addresses must start at a
* block-granular address, or that sizes must be equal to exactly one or more blocks),
* because machines with large block sizes can make it impossible to load certain ROMs at
* their required addresses. Every allocation still allocates a whole number of blocks.
*
* Even so, BusPDP11 memory management does NOT provide a general-purpose heap. Most memory
* allocations occur during machine initialization and never change. In particular, there
* is NO support for removing partial-block allocations.
*
* Each Memory block keeps track of a start address (addr) and length (used), indicating
* the used space within the block; any free space that precedes or follows that used space
* can be allocated later, by simply extending the beginning or ending of the previously used
* space. However, any holes that might have existed between the original allocation and an
* extension are subsumed by the extension.
*
* @this {BusPDP11}
* @param {number} addr is the starting physical address of the request
* @param {number} size of the request, in bytes
* @param {number} type is one of the MemoryPDP11.TYPE constants
* @param {Object} [controller] is an optional memory controller component
* @return {boolean} true if successful, false if not
*/
addMemory(addr, size, type, controller)
{
var addrNext = addr;
var sizeLeft = size;
var iBlock = addrNext >>> this.nBlockShift;
while (sizeLeft > 0 && iBlock < this.aBusBlocks.length) {
var block = this.aBusBlocks[iBlock];
var addrBlock = iBlock * this.nBlockSize;
var sizeBlock = this.nBlockSize - (addrNext - addrBlock);
if (sizeBlock > sizeLeft) sizeBlock = sizeLeft;
/*
* addMemory() will now happily replace an existing block when a memory controller is specified;
* this is a work-around to make life easier for setIOPageRange(), which otherwise would have to call
* removeMemory() first, which would just waste time and memory allocating more (empty) blocks.
*/
if (!controller && block && block.size) {
if (block.type == type /* && block.controller == controller */) {
/*
* Where there is already a similar block with a non-zero size, we allow the allocation only if:
*
* 1) addrNext + sizeLeft <= block.addr (the request precedes the used portion of the current block), or
* 2) addrNext >= block.addr + block.used (the request follows the used portion of the current block)
*/
if (addrNext + sizeLeft <= block.addr) {
block.used += (block.addr - addrNext);
block.addr = addrNext;
return true;
}
if (addrNext >= block.addr + block.used) {
var sizeAvail = block.size - (addrNext - addrBlock);
if (sizeAvail > sizeLeft) sizeAvail = sizeLeft;
block.used = addrNext - block.addr + sizeAvail;
addrNext = addrBlock + this.nBlockSize;
sizeLeft -= sizeAvail;
iBlock++;
continue;
}
}
return this.reportError(BusPDP11.ERROR.RANGE_INUSE, addrNext, sizeLeft);
}
var blockNew = new MemoryPDP11(this, addrNext, sizeBlock, this.nBlockSize, type, controller);
blockNew.copyBreakpoints(this.dbg, block);
this.aBusBlocks[iBlock++] = blockNew;
addrNext = addrBlock + this.nBlockSize;
sizeLeft -= sizeBlock;
}
if (sizeLeft <= 0) {
this.status("Added " + (size >> 10) + "Kb " + MemoryPDP11.TYPE_NAMES[type] + " at " + Str.toOct(addr));
return true;
}
return this.reportError(BusPDP11.ERROR.RANGE_INVALID, addr, size);
}
/**
* cleanMemory(addr, size)
*
* @this {BusPDP11}
* @param {number} addr
* @param {number} size
* @return {boolean} true if all blocks were clean, false if dirty; all blocks are cleaned in the process
*/
cleanMemory(addr, size)
{
var fClean = true;
var iBlock = addr >>> this.nBlockShift;
var sizeBlock = this.nBlockSize - (addr & this.nBlockLimit);
while (size > 0 && iBlock < this.aBusBlocks.length) {
if (this.aBusBlocks[iBlock].fDirty) {
this.aBusBlocks[iBlock].fDirty = fClean = false;
this.aBusBlocks[iBlock].fDirtyEver = true;
}
size -= sizeBlock;
sizeBlock = this.nBlockSize;
iBlock++;
}
return fClean;
}
/**
* zeroMemory(addr, size, pattern)
*
* @this {BusPDP11}
* @param {number} addr
* @param {number} size
* @param {number} [pattern]
*/
zeroMemory(addr, size, pattern)
{
var off = addr & this.nBlockLimit;
var iBlock = addr >>> this.nBlockShift;
while (size > 0 && iBlock < this.aBusBlocks.length) {
this.aBusBlocks[iBlock].zero(off, size, pattern);
size -= this.nBlockSize;
iBlock++;
off = 0;
}
}
/**
* scanMemory(info, addr, size)
*
* Returns a BusInfoPDP11 object for the specified address range.
*
* @this {BusPDP11}
* @param {BusInfoPDP11} [info] previous BusInfoPDP11, if any
* @param {number} [addr] starting address of range (0 if none provided)
* @param {number} [size] size of range, in bytes (up to end of address space if none provided)
* @return {BusInfoPDP11} updated info (or new info if no previous info provided)
*/
scanMemory(info, addr, size)
{
if (addr == null) addr = 0;
if (size == null) size = (this.addrTotal - addr) | 0;
if (info == null) info = {cbTotal: 0, cBlocks: 0, aBlocks: []};
var iBlock = addr >>> this.nBlockShift;
var iBlockMax = ((addr + size - 1) >>> this.nBlockShift);
info.cbTotal = 0;
info.cBlocks = 0;
while (iBlock <= iBlockMax) {
var block = this.aBusBlocks[iBlock];
info.cbTotal += block.size;
if (block.size) {
info.aBlocks.push(/** @type {BlockInfoPDP11} */ (Usr.initBitFields(BlockInfoPDP11, iBlock, 0, 0, block.type)));
info.cBlocks++
}
iBlock++;
}
return info;
}
/**
* removeMemory(addr, size)
*
* Replaces every block in the specified address range with empty Memory blocks that ignore all reads/writes.
*
* TODO: Update the removeMemory() interface to reflect the relaxed requirements of the addMemory() interface.
*
* @this {BusPDP11}
* @param {number} addr
* @param {number} size
* @return {boolean} true if successful, false if not
*/
removeMemory(addr, size)
{
if (!(addr & this.nBlockLimit) && size && !(size & this.nBlockLimit)) {
var iBlock = addr >>> this.nBlockShift;
while (size > 0) {
var blockOld = this.aBusBlocks[iBlock];
var blockNew = new MemoryPDP11(this, addr);
blockNew.copyBreakpoints(this.dbg, blockOld);
this.aBusBlocks[iBlock++] = blockNew;
addr = iBlock * this.nBlockSize;
size -= this.nBlockSize;
}
return true;
}
return this.reportError(BusPDP11.ERROR.RANGE_INVALID, addr, size);
}
/**
* getMemoryBlocks(addr, size)
*
* @this {BusPDP11}
* @param {number} addr is the starting physical address
* @param {number} size of the request, in bytes
* @return {Array} of Memory blocks
*/
getMemoryBlocks(addr, size)
{
var aBlocks = [];
var iBlock = addr >>> this.nBlockShift;
while (size > 0 && iBlock < this.aBusBlocks.length) {
aBlocks.push(this.aBusBlocks[iBlock++]);
size -= this.nBlockSize;
}
return aBlocks;
}
/**
* setMemoryAccess(addr, size, afn, fQuiet)
*
* 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 {BusPDP11}
* @param {number} addr
* @param {number} size
* @param {Array.<function()>} [afn]
* @param {boolean} [fQuiet] (true if any error should be quietly logged)
* @return {boolean} true if successful, false if not
*/
setMemoryAccess(addr, size, afn, fQuiet)
{
if (!(addr & this.nBlockLimit) && size && !(size & this.nBlockLimit)) {
var iBlock = addr >>> this.nBlockShift;
while (size > 0) {
var block = this.aBusBlocks[iBlock];
if (!block.controller) {
return this.reportError(BusPDP11.ERROR.NO_CONTROLLER, addr, size, fQuiet);
}
block.setAccess(afn, true);
size -= this.nBlockSize;
iBlock++;
}
return true;
}
return this.reportError(BusPDP11.ERROR.RANGE_INVALID, addr, size);
}
/**
* setMemoryBlocks(addr, size, aBlocks, type)
*
* If no type is specified, then specified address range uses all the provided blocks as-is;
* this form of setMemoryBlocks() is used for complete physical aliases.
*
* Otherwise, new blocks are allocated with the specified type; the underlying memory from the
* provided blocks is still used, but the new blocks may have different access to that memory.
*
* @this {BusPDP11}
* @param {number} addr is the starting physical address
* @param {number} size of the request, in bytes
* @param {Array} aBlocks as returned by getMemoryBlocks()
* @param {number} [type] is one of the MemoryPDP11.TYPE constants
*/
setMemoryBlocks(addr, size, aBlocks, type)
{
var i = 0;
var iBlock = addr >>> this.nBlockShift;
while (size > 0 && iBlock < this.aBusBlocks.length) {
var block = aBlocks[i++];
if (!block) break;
if (type !== undefined) {
var blockNew = new MemoryPDP11(this, addr);
blockNew.clone(block, type, this.dbg);
block = blockNew;
}
this.aBusBlocks[iBlock++] = block;
size -= this.nBlockSize;
}
}
/**
* getByte(addr)
*
* @this {BusPDP11}
* @param {number} addr is a physical address
* @return {number} byte (8-bit) value at that address
*/
getByte(addr)
{
return this.aMemBlocks[(addr & this.nMemMask) >>> this.nBlockShift].readByte(addr & this.nBlockLimit, addr);
}
/**
* getWord(addr)
*
* @this {BusPDP11}
* @param {number} addr is a physical address
* @return {number} word (16-bit) value at that address
*/
getWord(addr)
{
var off = addr & this.nBlockLimit;
var iBlock = (addr & this.nMemMask) >>> this.nBlockShift;
if (!PDP11.WORDBUS && off == this.nBlockLimit) {
return this.aMemBlocks[iBlock++].readByte(off, addr) | (this.aMemBlocks[iBlock & this.nBlockMask].readByte(0, addr + 1) << 8);
}
return this.aMemBlocks[iBlock].readWord(off, addr);
}
/**
* setByte(addr, b)
*
* @this {BusPDP11}
* @param {number} addr is a physical address
* @param {number} b is the byte (8-bit) value to write
*/
setByte(addr, b)
{
this.aMemBlocks[(addr & this.nMemMask) >>> this.nBlockShift].writeByte(addr & this.nBlockLimit, b, addr);
}
/**
* setWord(addr, w)
*
* @this {BusPDP11}
* @param {number} addr is a physical address
* @param {number} w is the word (16-bit) value to write
*/
setWord(addr, w)
{
var off = addr & this.nBlockLimit;
var iBlock = (addr & this.nMemMask) >>> this.nBlockShift;
if (!PDP11.WORDBUS && off == this.nBlockLimit) {
this.aMemBlocks[iBlock++].writeByte(off, w & 0xff, addr);
this.aMemBlocks[iBlock & this.nBlockMask].writeByte(0, (w >> 8) & 0xff, addr + 1);
return;
}
this.aMemBlocks[iBlock].writeWord(off, w, addr);
}
/**
* getBlockDirect(addr)
*
* @this {BusPDP11}
* @param {number} addr is a physical address
* @return {MemoryPDP11}
*/
getBlockDirect(addr)
{
return this.aBusBlocks[(addr & this.nBusMask) >>> this.nBlockShift];
}
/**
* getByteDirect(addr)
*
* This is used for device I/O and Debugger physical memory requests, not the CPU.
*
* @this {BusPDP11}
* @param {number} addr is a physical address
* @return {number} byte (8-bit) value at that address
*/
getByteDirect(addr)
{
this.fFault = false;
this.nDisableFaults++;
var b = this.getBlockDirect(addr).readByteDirect(addr & this.nBlockLimit, addr);
this.nDisableFaults--;
return b;
}
/**
* getWordDirect(addr)
*
* This is used for device I/O and Debugger physical memory requests, not the CPU.
*
* @this {BusPDP11}
* @param {number} addr is a physical address
* @return {number} word (16-bit) value at that address
*/
getWordDirect(addr)
{
var w;
this.fFault = false;
this.nDisableFaults++;
var off = addr & this.nBlockLimit;
var block = this.getBlockDirect(addr);
if (!PDP11.WORDBUS && off == this.nBlockLimit) {
w = block.readByteDirect(off, addr) | (this.getBlockDirect(addr + 1).readByteDirect(0, addr + 1) << 8);
} else {
w = block.readWordDirect(off, addr);
}
this.nDisableFaults--;
return w;
}
/**
* setByteDirect(addr, b)
*
* This is used for device I/O and Debugger physical memory requests, not the CPU.
*
* @this {BusPDP11}
* @param {number} addr is a physical address
* @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe)
*/
setByteDirect(addr, b)
{
this.fFault = false;
this.nDisableFaults++;
this.getBlockDirect(addr).writeByteDirect(addr & this.nBlockLimit, b & 0xff, addr);
this.nDisableFaults--;
}
/**
* setWordDirect(addr, w)
*
* This is used for device I/O and Debugger physical memory requests, not the CPU.
*
* @this {BusPDP11}
* @param {number} addr is a physical address
* @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe)
*/
setWordDirect(addr, w)
{
this.fFault = false;
this.nDisableFaults++;
var off = addr & this.nBlockLimit;
var block = this.getBlockDirect(addr);
if (!PDP11.WORDBUS && off == this.nBlockLimit) {
block.writeByteDirect(off, w & 0xff, addr);
this.getBlockDirect(addr + 1).writeByteDirect(0, (w >> 8) & 0xff, addr + 1);
} else {
block.writeWordDirect(off, w & 0xffff, addr);
}
this.nDisableFaults--;
}
/**
* addMemBreak(addr, fWrite)
*
* @this {BusPDP11}
* @param {number} addr
* @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint
*/
addMemBreak(addr, fWrite)
{
if (DEBUGGER) {
var iBlock = addr >>> this.nBlockShift;
this.aBusBlocks[iBlock].addBreakpoint(addr & this.nBlockLimit, fWrite);
}
}
/**
* removeMemBreak(addr, fWrite)
*
* @this {BusPDP11}
* @param {number} addr
* @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint
*/
removeMemBreak(addr, fWrite)
{
if (DEBUGGER) {
var iBlock = addr >>> this.nBlockShift;
this.aBusBlocks[iBlock].removeBreakpoint(addr & this.nBlockLimit, fWrite);
}
}
/**
* saveMemory(fAll)
*
* The only memory blocks we save are those marked as dirty, but 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 bitwise 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 {BusPDP11}
* @param {boolean} [fAll] (true to save all non-ROM memory blocks, regardless of their dirty flags)
* @return {Array} a
*/
saveMemory(fAll)
{
var i = 0;
var a = [];
for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) {
var block = this.aBusBlocks[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 (fAll && block.type != MemoryPDP11.TYPE.ROM || block.fDirty || block.fDirtyEver) {
a[i++] = iBlock;
a[i++] = State.compress(block.save());
}
}
return a;
}
/**
* restoreMemory(a)
*
* This restores the contents of all Memory blocks; called by CPUState.restore().
*
* In theory, we ONLY have to save/restore block contents. Other block attributes,
* like the type, 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 more information on how the memory block contents are saved.
*
* @this {BusPDP11}
* @param {Array} a
* @return {boolean} true if successful, false if not
*/
restoreMemory(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.nBlockLen) {
adw = State.decompress(adw, this.nBlockLen);
}
var block = this.aBusBlocks[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).
*/
Component.error("Unable to restore memory block " + iBlock);
return false;
}
}
return true;
}
/**
* getMemoryLimit(type)
*
* @this {BusPDP11}
* @param {number} type is one of the MemoryPDP11.TYPE constants
* @return {number} (the limiting address of the specified memory type, zero if none)
*/
getMemoryLimit(type)
{
var addr = 0;
for (var iBlock = 0; iBlock < this.aBusBlocks.length; iBlock++) {
var block = this.aBusBlocks[iBlock];
if (block.type == type) {
addr = block.addr + block.used;
}
}
return addr;
}
/**
* addIOHandlers(start, end, fnReadByte, fnWriteByte, fnReadWord, fnWriteWord, message, sName)
*
* Add I/O notification handlers to the master list (aIOHandlers). The start and end addresses are typically
* relative to the starting IOPAGE address, but they can also be absolute; we simply mask all addresses with
* IOPAGE_MASK.
*
* CAVEATS: If a conflict is reported, a partial set of handlers may still have been added. There is no mechanism
* for removing handlers, since this is considered an initialization function. And finally, when a range of addresses
* is used, each successive address is advanced by 2, so if you really want to add a handler for a "+1" (usually odd)
* address, then you must add it individually. Failure to do is not necessarily fatal, because the IOController's
* fallback behavior for an odd address is to call the byte handler for the preceding even address, but the byte
* handler must be prepared for that (the handlers installed by ROM component's addROM() function are a good example).
*
* @this {BusPDP11}
* @param {number} start address
* @param {number} end address
* @param {function(number)|null|undefined} fnReadByte
* @param {function(number,number)|null|undefined} fnWriteByte
* @param {function(number)|null|undefined} fnReadWord
* @param {function(number,number)|null|undefined} fnWriteWord
* @param {number} [message]
* @param {string} [sName]
* @return {boolean} (true if entire range successfully registered, false if any conflicts)
*/
addIOHandlers(start, end, fnReadByte, fnWriteByte, fnReadWord, fnWriteWord, message, sName)
{
var index = (start == end? -1 : 0);
for (var addr = start; addr <= end; addr += 2) {
var off = addr & BusPDP11.IOPAGE_MASK;
if (this.aIOHandlers[off] !== undefined) {
Component.warning("I/O address already registered: " + Str.toHexLong(addr));
return false;
}
var s = sName || "unknown";
if (s && index >= 0) s += index++;
this.aIOHandlers[off] = [fnReadByte, fnWriteByte, fnReadWord, fnWriteWord, s, message || MessagesPDP11.BUS, false];
if (MAXDEBUG) this.log("addIOHandlers(" + Str.toHexLong(addr) + ")");
}
return true;
}
/**
* addIOTable(component, table, offReg)
*
* Add I/O notification handlers from the specified table (a batch version of addIOHandlers).
*
* @this {BusPDP11}
* @param {Component} component
* @param {Object} table
* @param {number} [offReg] (optional offset to add to all register addresses)
* @return {boolean} (true if entire range successfully registered, false if any conflicts)
*/
addIOTable(component, table, offReg)
{
for (var reg in table) {
var addr = +reg + (offReg || 0);
var afn = table[reg];
/*
* Don't install (ie, ignore) handlers for I/O addresses that are defined with a model number
* that is "greater than" than the current model.
*/
if (afn[6] && afn[6] > this.cpu.model) continue;
var fnReadByte = afn[0]? afn[0].bind(component) : null;
var fnWriteByte = afn[1]? afn[1].bind(component) : null;
var fnReadWord = afn[2]? afn[2].bind(component) : null;
var fnWriteWord = afn[3]? afn[3].bind(component) : null;
/*
* As discussed in the IOController comments below, when handlers are being registered for these
* BYTE-granular UNIBUS addresses, we must install custom fallback handlers for all BYTE accesses.
*/
if (addr >= PDP11.UNIBUS.R0SET0 && addr <= PDP11.UNIBUS.R6USER) {
if (!fnReadByte && fnReadWord) {
fnReadByte = function readByteIORegister(readWord) {
return function(addr) {
return readWord(addr) & 0xff;
}.bind(component);
}(fnReadWord);
}
if (!fnWriteByte && fnWriteWord) {
fnWriteByte = function writeByteIORegister(writeWord) {
return function(data, addr) {
return writeWord(data, addr);
}.bind(component);
}(fnWriteWord);
}
}
var sReg = afn[4];
var nRegs = afn[5] || 1;
for (var iReg = 0; iReg < nRegs; iReg++, addr += 2) {
if (sReg && nRegs > 1) sReg = afn[4] + iReg;
if (!this.addIOHandlers(addr, addr, fnReadByte, fnWriteByte, fnReadWord, fnWriteWord, afn[7] || component.bitsMessage, sReg || component.idComponent)) {
return false;
}
}
}
return true;
}
/**
* getAddrInfo(addr)
*
* Determine if the physical address is a known IOPAGE address, and return information about it (ie, the name).
*
* @this {BusPDP11}
* @param {number} addr (physical)
* @return {string|null}
*/
getAddrInfo(addr)
{
var sName = null;
if (addr >= this.addrIOPage) {
var off = addr & BusPDP11.IOPAGE_MASK;
var afn = this.aIOHandlers[off];
if (afn) sName = afn[BusPDP11.IOHANDLER.REG_NAME];
}
return sName;
}
/**
* getAddrByName(sName)
*
* Determine if the specified name has a corresponding physical address.
*
* @this {BusPDP11}
* @param {string} sName
* @return {number|null}
*/
getAddrByName(sName)
{
sName = sName.toUpperCase();
for (var i in this.aIOHandlers) {
var off = +i;
var afn = this.aIOHandlers[off];
if (afn[BusPDP11.IOHANDLER.REG_NAME] == sName) {
return this.addrIOPage + off;
}
}
return null;
}
/**
* addResetHandler(fnReset)
*
* @this {BusPDP11}
* @param {function()} fnReset
*/
addResetHandler(fnReset)
{
this.afnReset.push(fnReset);
}
/**
* fault(addr, err, access)
*
* Bus interface for signaling alignment errors, invalid memory, etc.
*
* @this {BusPDP11}
* @param {number} addr
* @param {number} [err]
* @param {number} [access] (for diagnostic purposes only)
*/
fault(addr, err, access)
{
this.fFault = true;
if (!this.nDisableFaults) {
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.FAULT)) {
this.dbg.printMessage("memory fault (" + access + ") on " + this.dbg.toStrBase(addr), true, true);
}
if (err) this.cpu.regErr |= err;
this.cpu.trap(PDP11.TRAP.BUS, 0, addr);
}
}
/**
* checkFault()
*
* This also serves as a clearFault() function.
*
* @this {BusPDP11}
* @return {boolean}
*/
checkFault()
{
var f = this.fFault;
this.fFault = false;
return f;
}
/**
* reportError(errNum, addr, size, fQuiet)
*
* @this {BusPDP11}
* @param {number} errNum
* @param {number} addr
* @param {number} size
* @param {boolean} [fQuiet] (true if any error should be quietly logged)
* @return {boolean} false
*/
reportError(errNum, addr, size, fQuiet)
{
var sError = "Memory block error (" + errNum + ": " + Str.toHex(addr) + "," + Str.toHex(size) + ")";
if (fQuiet) {
if (this.dbg) {
this.dbg.message(sError);
} else {
this.log(sError);
}
} else {
Component.error(sError);
}
return false;
}
}
BusPDP11.IOPAGE_16BIT = 0x00E000; /*000160000*/ // eg, PDP-11/20
BusPDP11.IOPAGE_18BIT = 0x03E000; /*000760000*/ // eg, PDP-11/45
BusPDP11.IOPAGE_22BIT = 0x3FE000; /*017760000*/ // eg, PDP-11/70
BusPDP11.IOPAGE_LENGTH = 0x002000; // ie, 8Kb
BusPDP11.IOPAGE_MASK = BusPDP11.IOPAGE_LENGTH - 1;
BusPDP11.MASK_18BIT = 0x03FFFF; /*000777777*/
BusPDP11.UNIBUS_22BIT = 0x3C0000; /*017000000*/
BusPDP11.MASK_22BIT = 0x3FFFFF; /*017777777*/
BusPDP11.ERROR = {
RANGE_INUSE: 1,
RANGE_INVALID: 2,
NO_CONTROLLER: 3
};
/*
* Every entry in the aIOHandlers table is an array with the following indexes:
*/
BusPDP11.IOHANDLER = {
READ_BYTE: 0,
WRITE_BYTE: 1,
READ_WORD: 2,
WRITE_WORD: 3,
REG_NAME: 4,
MSG_CATEGORY: 5,
DBG_BREAK: 6
};
/*
* These are our custom IOController functions for all IOPAGE accesses. They look up the IOPAGE
* offset in the aIOHandlers table, and if an entry exists, they use the appropriate IOHANDLER indexes
* (above) to locate the registered read/write handlers. If no handler is found, then fault() will
* be called, triggering a trap -- unless traps are disabled because direct access was requested
* (eg, by the Debugger).
*
* Handlers receive the original IOPAGE address that was used, although in most cases, it's ignored,
* because most handlers usually handle only one address. Only handlers used for a range of addresses
* must pay attention to it.
*
* Note that these functions include fallbacks for byte reads when only word read handlers exist (by
* masking or shifting the result) and for word reads if only byte handlers exist (by combining bytes).
* Fallbacks for writes exist, too, but they are slightly more complicated, because a byte write using
* a word write handler requires reading the word first, and then updating the appropriate byte within
* that word.
*
* Those fallbacks may not always be appropriate; for example, byte writes to some device registers
* must be zero-extended to update the entire word. For those cases, the fallback's "preliminary" read
* is issued with a fPreWrite flag so that the handler can distinguish a normal read from one of these
* preliminary reads (aka read-before-write), and return an appropriate value for the update (eg, zero).
*
* If none of these fallback behaviors are appropriate, the device has a simple recourse: register
* handlers for all possible addresses and sizes.
*
* Unlike regular Memory blocks, IOPAGE accesses permit word accesses on ODD addresses; that works
* just fine by registering WORD handlers for the appropriate ODD addresses. For BYTE accesses, it
* depends. For CPU register addresses, addIOHandlers() installs special byte handlers that perform
* either a simple word read or write. Other addresses must be handled on a case-by-case basis.
*
* TODO: Another small potential improvement would be for addIOHandlers() to install fallbacks for ALL
* missing handlers, in both the ODD and EVEN cases, so there's never a need to check each function index
* before calling it. However, since there's no avoiding checking aIOHandlers[off] (unless we FULLY populate
* the aIOHandlers array), and since these I/O accesses should be pretty infrequent relative to all other
* memory accesses, the benefit seems pretty minimal. Plus, all our fallback assumptions still need to be
* verified, so let's wait until that's done before we start optimizing this code.
*/
BusPDP11.IOController = {
/**
* readByte(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readByte: function(off, addr)
{
var b = -1;
var bus = this.controller;
var afn = bus.aIOHandlers[off];
/*
* Since addr is primarily used to advise an I/O handler of the target IOPAGE address, and since we don't want
* our handlers to worry about the current IOPAGE location, we truncate addr to 16 bits (the IOPAGE's lowest location).
*/
var addrMasked = addr & 0xffff;
if (afn) {
if (afn[BusPDP11.IOHANDLER.READ_BYTE]) {
b = afn[BusPDP11.IOHANDLER.READ_BYTE](addrMasked);
} else if (afn[BusPDP11.IOHANDLER.READ_WORD]) {
if (!(addrMasked & 0x1)) {
b = afn[BusPDP11.IOHANDLER.READ_WORD](addrMasked) & 0xff;
} else {
b = afn[BusPDP11.IOHANDLER.READ_WORD](addrMasked & ~0x1) >> 8;
}
}
} else if (addrMasked & 0x1) {
afn = bus.aIOHandlers[off & ~0x1];
if (afn) {
if (afn[BusPDP11.IOHANDLER.READ_WORD]) {
b = afn[BusPDP11.IOHANDLER.READ_WORD](addrMasked & ~0x1) >> 8;
} else if (afn[BusPDP11.IOHANDLER.READ_BYTE]) {
/*
* WARNING: This is an unusual fall-back, because we're trying to read an ODD byte
* access using a BYTE handler registered for EVEN bytes. But if that's all we've got,
* then presumably the handler is prepared for it (certainly, readROMByte() is).
*/
b = afn[BusPDP11.IOHANDLER.READ_BYTE](addrMasked)
}
}
}
if (b >= 0) {
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.BUS | afn[BusPDP11.IOHANDLER.MSG_CATEGORY])) {
this.dbg.printMessage(afn[BusPDP11.IOHANDLER.REG_NAME] + ".readByte(" + this.dbg.toStrBase(addr) + "): " + this.dbg.toStrBase(b), true, !bus.nDisableFaults);
}
return b;
}
bus.fault(addr, PDP11.CPUERR.TIMEOUT, PDP11.ACCESS.READ_BYTE);
b = 0xff;
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.BUS)) {
this.dbg.printMessage("warning: unconverted read access to byte @" + this.dbg.toStrBase(addr) + ": " + this.dbg.toStrBase(b), true, !bus.nDisableFaults);
}
return b;
},
/**
* writeByte(off, b, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits)
* @param {number} addr
*/
writeByte: function(off, b, addr)
{
var w;
var fWrite = false;
var bus = this.controller;
var afn = bus.aIOHandlers[off];
/*
* Since addr is primarily used to advise an I/O handler of the target IOPAGE address, and since we don't want
* our handlers to worry about the current IOPAGE location, we truncate addr to 16 bits (the IOPAGE's lowest location).
*/
var addrMasked = addr & 0xffff;
if (afn) {
/*
* If a writeByte() handler exists, call it; we're done.
*/
if (afn[BusPDP11.IOHANDLER.WRITE_BYTE]) {
afn[BusPDP11.IOHANDLER.WRITE_BYTE](b, addrMasked);
fWrite = true;
}
/*
* If a writeWord() handler exists, call the readWord() handler first to get the original data
* (with fPreWrite set to true) and call writeWord() with the new data inserted into the original data.
*/
else if (afn[BusPDP11.IOHANDLER.WRITE_WORD]) {
w = afn[BusPDP11.IOHANDLER.READ_WORD]? afn[BusPDP11.IOHANDLER.READ_WORD](addrMasked, true) : 0;
if (!(addrMasked & 0x1)) {
afn[BusPDP11.IOHANDLER.WRITE_WORD]((w & ~0xff) | b, addrMasked);
fWrite = true;
} else {
afn[BusPDP11.IOHANDLER.WRITE_WORD]((w & 0xff) | (b << 8), addrMasked & ~0x1);
fWrite = true;
}
}
} else if (addrMasked & 0x1) {
/*
* If no handler existed, and this address was odd, then perhaps a handler exists for the even address;
* if so, call the readWord() handler first to get the original data (with fPreWrite set to true) and call
* writeWord() with the new data inserted into (the high byte of) the original data.
*/
afn = bus.aIOHandlers[off & ~0x1];
if (afn) {
if (afn[BusPDP11.IOHANDLER.WRITE_WORD]) {
addrMasked &= ~0x1;
w = afn[BusPDP11.IOHANDLER.READ_WORD]? afn[BusPDP11.IOHANDLER.READ_WORD](addrMasked, true) : 0;
afn[BusPDP11.IOHANDLER.WRITE_WORD]((w & 0xff) | (b << 8), addrMasked);
fWrite = true;
} else if (afn[BusPDP11.IOHANDLER.WRITE_BYTE]) {
/*
* WARNING: This is an unusual fall-back, because we're trying to write an ODD byte
* access using a BYTE handler registered for EVEN bytes. But if that's all we've got,
* then presumably the handler is prepared for it (certainly, writeROMByte() is).
*/
afn[BusPDP11.IOHANDLER.WRITE_BYTE](b, addrMasked);
fWrite = true;
}
}
}
if (fWrite) {
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.BUS | afn[BusPDP11.IOHANDLER.MSG_CATEGORY])) {
this.dbg.printMessage(afn[BusPDP11.IOHANDLER.REG_NAME] + ".writeByte(" + this.dbg.toStrBase(addr) + "," + this.dbg.toStrBase(b) + ")", true, !bus.nDisableFaults);
}
return;
}
bus.fault(addr, PDP11.CPUERR.TIMEOUT, PDP11.ACCESS.WRITE_BYTE);
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.BUS)) {
this.dbg.printMessage("warning: unconverted write access to byte @" + this.dbg.toStrBase(addr) + ": " + this.dbg.toStrBase(b), true, !bus.nDisableFaults);
}
},
/**
* readWord(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readWord: function(off, addr)
{
var w = -1;
var bus = this.controller;
var afn = bus.aIOHandlers[off];
/*
* Since addr is primarily used to advise an I/O handler of the target IOPAGE address, and since we don't want
* our handlers to worry about the current IOPAGE location, we truncate addr to 16 bits (the IOPAGE's lowest location).
*/
var addrMasked = addr & 0xffff;
if (afn) {
if (afn[BusPDP11.IOHANDLER.READ_WORD]) {
w = afn[BusPDP11.IOHANDLER.READ_WORD](addrMasked);
} else if (afn[BusPDP11.IOHANDLER.READ_BYTE]) {
w = afn[BusPDP11.IOHANDLER.READ_BYTE](addrMasked) | (afn[BusPDP11.IOHANDLER.READ_BYTE](addrMasked + 1) << 8);
}
}
if (w >= 0) {
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.BUS | afn[BusPDP11.IOHANDLER.MSG_CATEGORY])) {
this.dbg.printMessage(afn[BusPDP11.IOHANDLER.REG_NAME] + ".readWord(" + this.dbg.toStrBase(addr) + "): " + this.dbg.toStrBase(w), true, !bus.nDisableFaults);
}
return w;
}
bus.fault(addr, PDP11.CPUERR.TIMEOUT, PDP11.ACCESS.READ_WORD);
w = 0xffff;
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.BUS)) {
this.dbg.printMessage("warning: unconverted read access to word @" + this.dbg.toStrBase(addr) + ": " + this.dbg.toStrBase(w), true, !bus.nDisableFaults);
}
return w;
},
/**
* writeWord(off, w, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} w (which should already be pre-masked to 16 bits)
* @param {number} addr
*/
writeWord: function(off, w, addr)
{
var fWrite = false;
var bus = this.controller;
var afn = bus.aIOHandlers[off];
/*
* Since addr is primarily used to advise an I/O handler of the target IOPAGE address, and since we don't want
* our handlers to worry about the current IOPAGE location, we truncate addr to 16 bits (the IOPAGE's lowest location).
*/
var addrMasked = addr & 0xffff;
if (afn) {
if (afn[BusPDP11.IOHANDLER.WRITE_WORD]) {
afn[BusPDP11.IOHANDLER.WRITE_WORD](w, addrMasked);
fWrite = true;
} else if (afn[BusPDP11.IOHANDLER.WRITE_BYTE]) {
afn[BusPDP11.IOHANDLER.WRITE_BYTE](w & 0xff, addrMasked);
afn[BusPDP11.IOHANDLER.WRITE_BYTE](w >> 8, addrMasked + 1);
fWrite = true;
}
}
if (fWrite) {
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.BUS | afn[BusPDP11.IOHANDLER.MSG_CATEGORY])) {
this.dbg.printMessage(afn[BusPDP11.IOHANDLER.REG_NAME] + ".writeWord(" + this.dbg.toStrBase(addr) + "," + this.dbg.toStrBase(w) + ")", true, !bus.nDisableFaults);
}
return;
}
bus.fault(addr, PDP11.CPUERR.TIMEOUT, PDP11.ACCESS.WRITE_WORD);
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.BUS)) {
this.dbg.printMessage("warning: unconverted write access to word @" + this.dbg.toStrBase(addr) + ": " + this.dbg.toStrBase(w), true, !bus.nDisableFaults);
}
}
};
/**
* @copyright http://pcjs.org/modules/pdp11/lib/device.js (C) Jeff Parsons 2012-2017
*/
class DevicePDP11 extends Component {
/**
* DevicePDP11(parmsDevice)
*
* The Device component implements the following "default" devices:
*
* KW11 (KW11-L Line Time Clock)
*
* as well providing access to all the MMU and CPU registers, PSW, etc.
*
* @param {Object} parmsDevice
*/
constructor(parmsDevice)
{
super("Device", parmsDevice, MessagesPDP11.DEVICE);
this.kw11 = { // KW11 registers
lks: PDP11.KW11.LKS.MON,
timer: -1 // initBus() will initialize this timer ID
};
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {DevicePDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cmp = cmp;
this.cpu = cpu;
this.dbg = dbg;
var device = this;
this.kw11.timer = cpu.addTimer(function() {
device.interruptKW11();
});
this.kw11.irq = cpu.addIRQ(PDP11.KW11.VEC, PDP11.KW11.PRI, MessagesPDP11.KW11);
bus.addIOTable(this, DevicePDP11.UNIBUS_IOTABLE);
bus.addResetHandler(this.reset.bind(this));
if (DEBUGGER && dbg) {
dbg.messageDump(MessagesPDP11.MMU, function onDumpMMU(asArgs) {
device.dumpMMU(asArgs);
});
}
this.setReady();
}
/**
* dumpMMU(asArgs)
*
* @this {DevicePDP11}
* @param {Array.<string>} asArgs
*/
dumpMMU(asArgs)
{
if (DEBUGGER) {
var cpu = this.cpu;
this.dumpRegs("KIPDR", cpu.regsPDR[0], 0, asArgs[0]);
this.dumpRegs("KDPDR", cpu.regsPDR[0], 8, asArgs[0]);
this.dumpRegs("KIPAR", cpu.regsPAR[0], 0, asArgs[0]);
this.dumpRegs("KDPAR", cpu.regsPAR[0], 8, asArgs[0], true);
this.dumpRegs("SIPDR", cpu.regsPDR[1], 0, asArgs[0]);
this.dumpRegs("SDPDR", cpu.regsPDR[1], 8, asArgs[0]);
this.dumpRegs("SIPAR", cpu.regsPAR[1], 0, asArgs[0]);
this.dumpRegs("SDPAR", cpu.regsPAR[1], 8, asArgs[0], true);
this.dumpRegs("UIPDR", cpu.regsPDR[3], 0, asArgs[0]);
this.dumpRegs("UDPDR", cpu.regsPDR[3], 8, asArgs[0]);
this.dumpRegs("UIPAR", cpu.regsPAR[3], 0, asArgs[0]);
this.dumpRegs("UDPAR", cpu.regsPAR[3], 8, asArgs[0], true);
if (cpu.regMMR3 & PDP11.MMR3.UNIBUS_MAP) {
this.dumpRegs("UNIMAP", cpu.regsUniMap, -1, asArgs[0]);
}
}
}
/**
* dumpRegs(sName, aRegs, offset, sFilter, fBreak)
*
* @this {DevicePDP11}
* @param {string} sName
* @param {Array.<number>} aRegs
* @param {number} offset
* @param {string} sFilter
* @param {boolean} [fBreak]
*/
dumpRegs(sName, aRegs, offset, sFilter, fBreak)
{
if (DEBUGGER) {
var dbg = this.dbg;
if (sFilter && sName.indexOf(sFilter.toUpperCase()) < 0) return;
var nBits = 0;
var nRegs = 8;
var sDump = "";
var fIndex = false;
var nWidth = 8;
if (offset < 0) {
nBits = 22;
nRegs = aRegs.length;
offset = 0;
fIndex = true;
nWidth = 4;
}
for (var i = 0; i < nRegs; i++) {
if (i % nWidth == 0) {
if (sDump) sDump += '\n';
sDump += sName + (fIndex? ('[' + Str.toDec(i, 2) + ']') : '') + ':';
}
sDump += ' ' + dbg.toStrBase(aRegs[offset + i], nBits);
}
dbg.println(sDump + (fBreak? '\n' : ''));
}
}
/**
* powerUp(data, fRepower)
*
* @this {DevicePDP11}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (!data) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {DevicePDP11}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* reset()
*
* @this {DevicePDP11}
*/
reset()
{
this.kw11.lks = PDP11.KW11.LKS.MON;
this.cpu.setTimer(this.kw11.timer, 1000/60, true);
}
/**
* save()
*
* This implements save support for the DevicePDP11 component.
*
* @this {DevicePDP11}
* @return {Object}
*/
save()
{
var state = new State(this);
state.set(0, [
this.kw11.lks
]);
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the DevicePDP11 component.
*
* @this {DevicePDP11}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
/*
* ES6 ALERT: A handy destructuring assignment, which makes it easy to perform the inverse
* of what save() does when it collects a bunch of object properties into an array.
*/
[
this.kw11.lks
] = data[0];
return true;
}
/**
* interruptKW11()
*
* We used to call this function only when the the KW11's "Interrupt Enable" bit was set,
* but now we call it at 60Hz regardless. In part, this was so we could piggy-back on it
* to drive display updates, but more importantly, the KW11's "Monitor" bit is supposed to
* be set at the "line frequency" independent of whether KW11 interrupts are enabled or not.
*
* @this {DevicePDP11}
*/
interruptKW11()
{
this.kw11.lks |= PDP11.KW11.LKS.MON;
if (this.kw11.lks & PDP11.KW11.LKS.IE) {
this.cpu.setIRQ(this.kw11.irq);
}
if (this.cmp) this.cmp.updateDisplays(1);
this.cpu.setTimer(this.kw11.timer, 1000/60);
}
/**
* readLKS(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.LKS or 177546)
* @return {number}
*/
readLKS(addr)
{
/*
* NOTE: The original code always cleared LKS.MON (bit 7) after snapping the value for the read,
* but based on DEC's "Non-Interrupt Mode" programming examples, it's clear that's not how LKS.MON
* operates; if the caller wants to clear it, they must explicitly clear it with a write.
*/
return this.kw11.lks;
}
/**
* writeLKS(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.LKS or 177546)
*/
writeLKS(data, addr)
{
/*
* NOTE: The original code always cleared LKS.MON (bit 7) as part of any write, but based on DEC's
* "Non-Interrupt Mode" programming examples, which explicitly CLRB after TSTB reveals LKS.MON is set,
* I think that was wrong, and that all a write should do is mask off all the other (non-writable) bits.
*/
this.kw11.lks = data & PDP11.KW11.LKS.MASK;
if (!(this.kw11.lks & PDP11.KW11.LKS.IE)) this.cpu.clearIRQ(this.kw11.irq);
}
/**
* readMMR0(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.MMR0 or 177572)
* @return {number}
*/
readMMR0(addr)
{
return this.cpu.getMMR0();
}
/**
* writeMMR0(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.MMR0 or 177572)
*/
writeMMR0(data, addr)
{
this.cpu.setMMR0((data & ~PDP11.MMR0.COMPLETED) | (this.cpu.regMMR0 & PDP11.MMR0.COMPLETED));
}
/**
* readMMR1(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.MMR1 or 177574)
* @return {number}
*/
readMMR1(addr)
{
return this.cpu.getMMR1();
}
/**
* readMMR2(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.MMR2 or 177576)
* @return {number}
*/
readMMR2(addr)
{
return this.cpu.getMMR2();
}
/**
* readMMR3(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.MMR3 or 172516)
* @return {number}
*/
readMMR3(addr)
{
return this.cpu.getMMR3();
}
/**
* writeMMR3(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.MMR3 or 172516)
*/
writeMMR3(data, addr)
{
this.cpu.setMMR3(data);
}
/**
* readUNIMAP(addr)
*
* NOTE: The UNIBUS map ("UNIMAP") is 32 registers spread across 64 words, so we first calculate the word index.
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.UNIMAP)
* @return {number}
*/
readUNIMAP(addr)
{
var word = (addr >> 1) & 0x3f, reg = word >> 1;
var data = this.cpu.regsUniMap[reg];
return (word & 1)? (data >> 16) : (data & 0xffff);
}
/**
* writeUNIMAP(data, addr)
*
* NOTE: The UNIBUS map ("UNIMAP") is 32 registers spread across 64 words, so we first calculate the word index.
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.UNIMAP)
*/
writeUNIMAP(data, addr)
{
var word = (addr >> 1) & 0x3f, reg = word >> 1;
if (word & 1) {
this.cpu.regsUniMap[reg] = (this.cpu.regsUniMap[reg] & 0xffff) | ((data & 0x003f) << 16);
} else {
this.cpu.regsUniMap[reg] = (this.cpu.regsUniMap[reg] & ~0xffff) | (data & 0xfffe);
}
}
/**
* readSIPDR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.SIPDR0--SIPDR7 or 172200--172216)
* @return {number}
*/
readSIPDR(addr)
{
var reg = (addr >> 1) & 7;
return this.cpu.regsPDR[1][reg];
}
/**
* writeSIPDR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.SIPDR0--SIPDR7 or 172200--172216)
*/
writeSIPDR(data, addr)
{
var reg = (addr >> 1) & 7;
this.cpu.regsPDR[1][reg] = data & 0xff0f;
}
/**
* readSDPDR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.SDPDR0--SDPDR7 or 172220--172236)
* @return {number}
*/
readSDPDR(addr)
{
var reg = ((addr >> 1) & 7) + 8;
return this.cpu.regsPDR[1][reg];
}
/**
* writeSDPDR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.SDPDR0--SDPDR7 or 172220--172236)
*/
writeSDPDR(data, addr)
{
var reg = ((addr >> 1) & 7) + 8;
this.cpu.regsPDR[1][reg] = data & 0xff0f;
}
/**
* readSIPAR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.SIPAR0--SIPAR7 or 172240--172256)
* @return {number}
*/
readSIPAR(addr)
{
var reg = (addr >> 1) & 7;
return this.cpu.regsPAR[1][reg];
}
/**
* writeSIPAR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.SIPAR0--SIPAR7 or 172240--172256)
*/
writeSIPAR(data, addr)
{
var reg = (addr >> 1) & 7;
this.cpu.regsPAR[1][reg] = data;
this.cpu.regsPDR[1][reg] &= 0xff0f;
}
/**
* readSDPAR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.SDPAR0--SDPAR7 or 172260--172276)
* @return {number}
*/
readSDPAR(addr)
{
var reg = ((addr >> 1) & 7) + 8;
return this.cpu.regsPAR[1][reg];
}
/**
* writeSDPAR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.SDPAR0--SDPAR7 or 172260--172276)
*/
writeSDPAR(data, addr)
{
var reg = ((addr >> 1) & 7) + 8;
this.cpu.regsPAR[1][reg] = data;
this.cpu.regsPDR[1][reg] &= 0xff0f;
}
/**
* readKIPDR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.KIPDR0--KIPDR7 or 172300--172316)
* @return {number}
*/
readKIPDR(addr)
{
var reg = (addr >> 1) & 7;
return this.cpu.regsPDR[0][reg];
}
/**
* writeKIPDR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.KIPDR0--KIPDR7 or 172300--172316)
*/
writeKIPDR(data, addr)
{
var reg = (addr >> 1) & 7;
this.cpu.regsPDR[0][reg] = data & 0xff0f;
}
/**
* readKDPDR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.KDPDR0--KDPDR7 or 172320--172336)
* @return {number}
*/
readKDPDR(addr)
{
var reg = ((addr >> 1) & 7) + 8;
return this.cpu.regsPDR[0][reg];
}
/**
* writeKDPDR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.KDPDR0--KDPDR7 or 172320--172336)
*/
writeKDPDR(data, addr)
{
var reg = ((addr >> 1) & 7) + 8;
this.cpu.regsPDR[0][reg] = data & 0xff0f;
}
/**
* readKIPAR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.KIPAR0--KIPAR7 or 172340--172356)
* @return {number}
*/
readKIPAR(addr)
{
var reg = (addr >> 1) & 7;
return this.cpu.regsPAR[0][reg];
}
/**
* writeKIPAR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.KIPAR0--KIPAR7 or 172340--172356)
*/
writeKIPAR(data, addr)
{
var reg = (addr >> 1) & 7;
this.cpu.regsPAR[0][reg] = data;
this.cpu.regsPDR[0][reg] &= 0xff0f;
}
/**
* readKDPAR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.KDPAR0--KDPAR7 or 172360--172376)
* @return {number}
*/
readKDPAR(addr)
{
var reg = ((addr >> 1) & 7) + 8;
return this.cpu.regsPAR[0][reg];
}
/**
* writeKDPAR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.KDPAR0--KDPAR7 or 172360--172376)
*/
writeKDPAR(data, addr)
{
var reg = ((addr >> 1) & 7) + 8;
this.cpu.regsPAR[0][reg] = data;
this.cpu.regsPDR[0][reg] &= 0xff0f;
}
/**
* readUIPDR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.UIPDR0--UIPDR7 or 177600--177616)
* @return {number}
*/
readUIPDR(addr)
{
var reg = (addr >> 1) & 7;
return this.cpu.regsPDR[3][reg];
}
/**
* writeUIPDR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.UIPDR0--UIPDR7 or 177600--177616)
*/
writeUIPDR(data, addr)
{
var reg = (addr >> 1) & 7;
this.cpu.regsPDR[3][reg] = data & 0xff0f;
}
/**
* readUDPDR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.UDPDR0--UDPDR7 or 177620--177636)
* @return {number}
*/
readUDPDR(addr)
{
var reg = ((addr >> 1) & 7) + 8;
return this.cpu.regsPDR[3][reg];
}
/**
* writeUDPDR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.UDPDR0--UDPDR7 or 177620--177636)
*/
writeUDPDR(data, addr)
{
var reg = ((addr >> 1) & 7) + 8;
this.cpu.regsPDR[3][reg] = data & 0xff0f;
}
/**
* readUIPAR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.UIPAR0--UIPAR7 or 177640--177656)
* @return {number}
*/
readUIPAR(addr)
{
var reg = (addr >> 1) & 7;
return this.cpu.regsPAR[3][reg];
}
/**
* writeUIPAR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.UIPAR0--UIPAR7 or 177640--177656)
*/
writeUIPAR(data, addr)
{
var reg = (addr >> 1) & 7;
this.cpu.regsPAR[3][reg] = data;
this.cpu.regsPDR[3][reg] &= 0xff0f;
}
/**
* readUDPAR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.UDPAR0--UDPAR7 or 177660--177676)
* @return {number}
*/
readUDPAR(addr)
{
var reg = ((addr >> 1) & 7) + 8;
return this.cpu.regsPAR[3][reg];
}
/**
* writeUDPAR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.UDPAR0--UDPAR7 or 177660--177676)
*/
writeUDPAR(data, addr)
{
var reg = ((addr >> 1) & 7) + 8;
this.cpu.regsPAR[3][reg] = data;
this.cpu.regsPDR[3][reg] &= 0xff0f;
}
/**
* readRSET0(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.R0SET0--R5SET0 or 177700--177705)
* @return {number}
*/
readRSET0(addr)
{
var data;
var reg = addr & 7;
if (this.cpu.regPSW & PDP11.PSW.REGSET) {
data = this.cpu.regsAlt[reg];
} else {
data = this.cpu.regsGen[reg];
}
return data;
}
/**
* writeRSET0(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.R0SET0--R5SET0 or 177700--177705)
*/
writeRSET0(data, addr)
{
var reg = addr & 7;
if (this.cpu.regPSW & PDP11.PSW.REGSET) {
this.cpu.regsAlt[reg] = data;
} else {
this.cpu.regsGen[reg] = data;
}
}
/**
* readR6KERNEL(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.R6KERNEL or 177706)
* @return {number}
*/
readR6KERNEL(addr)
{
var data;
if (!(this.cpu.regPSW & PDP11.PSW.CMODE)) { // Kernel Mode
data = this.cpu.regsGen[6];
} else {
data = this.cpu.regsAltStack[0];
}
return data;
}
/**
* writeR6KERNEL(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.R6KERNEL or 177706)
*/
writeR6KERNEL(data, addr)
{
if (!(this.cpu.regPSW & PDP11.PSW.CMODE)) { // Kernel Mode
this.cpu.regsGen[6] = data;
} else {
this.cpu.regsAltStack[0] = data;
}
}
/**
* readR7KERNEL(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.R7KERNEL or 177707)
* @return {number}
*/
readR7KERNEL(addr)
{
return this.cpu.regsGen[7];
}
/**
* writeR7KERNEL(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.R7KERNEL or 177707)
*/
writeR7KERNEL(data, addr)
{
this.cpu.regsGen[7] = data;
}
/**
* readRSET1(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.R0SET1--R5SET1 or 177710--177715)
* @return {number}
*/
readRSET1(addr)
{
var data;
var reg = addr & 7;
if (this.cpu.regPSW & PDP11.PSW.REGSET) {
data = this.cpu.regsGen[reg];
} else {
data = this.cpu.regsAlt[reg];
}
return data;
}
/**
* writeRSET1(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.R0SET1--R5SET1 or 177710--177715)
*/
writeRSET1(data, addr)
{
var reg = addr & 7;
if (this.cpu.regPSW & PDP11.PSW.REGSET) {
this.cpu.regsGen[reg] = data;
} else {
this.cpu.regsAlt[reg] = data;
}
}
/**
* readR6SUPER(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.R6SUPER or 177716)
* @return {number}
*/
readR6SUPER(addr)
{
var data;
if (((this.cpu.regPSW & PDP11.PSW.CMODE) >> PDP11.PSW.SHIFT.CMODE) == PDP11.MODE.SUPER) {
data = this.cpu.regsGen[6];
} else {
data = this.cpu.regsAltStack[1];
}
return data;
}
/**
* writeR6SUPER(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.R6SUPER or 177716)
*/
writeR6SUPER(data, addr)
{
if (((this.cpu.regPSW & PDP11.PSW.CMODE) >> PDP11.PSW.SHIFT.CMODE) == PDP11.MODE.SUPER) {
this.cpu.regsGen[6] = data;
} else {
this.cpu.regsAltStack[1] = data;
}
}
/**
* readR6USER(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.R6USER or 177717)
* @return {number}
*/
readR6USER(addr)
{
var data;
if (((this.cpu.regPSW & PDP11.PSW.CMODE) >> PDP11.PSW.SHIFT.CMODE) == PDP11.MODE.USER) {
data = this.cpu.regsGen[6];
} else {
data = this.cpu.regsAltStack[3];
}
return data;
}
/**
* writeR6USER(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.R6USER or 177717)
*/
writeR6USER(data, addr)
{
if (((this.cpu.regPSW & PDP11.PSW.CMODE) >> PDP11.PSW.SHIFT.CMODE) == PDP11.MODE.USER) {
this.cpu.regsGen[6] = data;
} else {
this.cpu.regsAltStack[3] = data;
}
}
/**
* readCTRL(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.LAERR--UNDEF2 or 177740--177756)
* @return {number}
*/
readCTRL(addr)
{
var reg = (addr - PDP11.UNIBUS.CTRL) >> 1;
return this.cpu.regsControl[reg];
}
/**
* writeCTRL(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.LAERR--UNDEF2 or 177740--177756)
*/
writeCTRL(data, addr)
{
var reg = (addr - PDP11.UNIBUS.CTRL) >> 1;
this.cpu.regsControl[reg] = data;
}
/**
* readSIZE(addr)
*
* We're adhering to DEC's documentation, which says:
*
* This read-only register specifies the memory size of the system. It is defined to indicate the
* last addressable block of 32 words in memory (bit 0 is equivalent to bit 6 of the Physical Address).
*
* Looking at the Memory Clear "toggle-in" code in /devices/pdp11/machine/1170/panel/debugger/README.md, the
* memory loop gives up when the block number stored in KIPAR0 is >= LSIZE, suggesting that LSIZE is actually
* the total number of 64-byte blocks, rather than the block number of the last block. But that code is
* not conclusive, since it writes 8192 bytes at a time rather than 64, so it doesn't really matter if LSIZE
* is off by one.
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.LSIZE--HSIZE or 177760--177762)
* @return {number}
*/
readSIZE(addr)
{
return addr == PDP11.UNIBUS.LSIZE? ((this.bus.getMemoryLimit(MemoryPDP11.TYPE.RAM) >> 6) - 1) : 0;
}
/**
* writeSIZE(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.LSIZE--HSIZE or 177760--177762)
*/
writeSIZE(data, addr)
{
}
/**
* readSYSID(addr)
*
* TODO: For SYSID, we currently ignore writes and return 1 on reads
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.SYSID or 177764)
* @return {number}
*/
readSYSID(addr)
{
return 1;
}
/**
* writeSYSID(data, addr)
*
* TODO: For SYSID, we currently ignore writes and return 1 on reads
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.SYSID or 177764)
*/
writeSYSID(data, addr)
{
}
/**
* readCPUERR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.CPUERR or 177766)
* @return {number}
*/
readCPUERR(addr)
{
return this.cpu.regErr;
}
/**
* writeCPUERR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.CPUERR or 177766)
*/
writeCPUERR(data, addr)
{
this.cpu.regErr = 0; // TODO: Confirm that writes always zero the register
}
/**
* readMBR(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.MB or 177770)
* @return {number}
*/
readMBR(addr)
{
return this.cpu.regMBR;
}
/**
* writeMBR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.MB or 177770)
*/
writeMBR(data, addr)
{
if (!(addr & 0x1)) {
data &= 0xff; // required for KB11-CM without MFPT instruction
}
this.cpu.regMBR = data;
}
/**
* readPIR(addr, fPreWrite)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.PIR or 177772)
* @param {boolean} [fPreWrite]
* @return {number}
*/
readPIR(addr, fPreWrite)
{
if (fPreWrite) return 0;
return this.cpu.getPIR();
}
/**
* writePIR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.PIR or 177772)
*/
writePIR(data, addr)
{
this.cpu.setPIR(data);
}
/**
* readSLR(addr, fPreWrite)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.SL or 177774)
* @param {boolean} [fPreWrite]
* @return {number}
*/
readSLR(addr, fPreWrite)
{
if (fPreWrite) return 0;
return this.cpu.getSLR();
}
/**
* writeSLR(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.SL or 177774)
*/
writeSLR(data, addr)
{
this.cpu.setSLR(data);
}
/**
* readPSW(addr)
*
* @this {DevicePDP11}
* @param {number} addr (eg, PDP11.UNIBUS.PSW or 177776)
* @return {number}
*/
readPSW(addr)
{
return this.cpu.getPSW();
}
/**
* writePSW(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.PSW or 177776)
*/
writePSW(data, addr)
{
/*
* pdp11.js disallowed PSW.TF in addition to PSW.UNUSED, but DEC's "TRAP TEST" expects the
* following instruction to trap:
*
* 004174: 052767 000020 173574 BIS #20,177776
*
* Since that test was written for the PDP-11/20, it's possible that newer machines have a different
* behavior, but for now, we assume that all machines allow setting PSW.TF.
*
* Moreover, we have changed setPSW() to disallow the setting of any bits not supported by the current
* CPU model, so it seems rather pointless to do any masking of bits here.
*/
this.cpu.setPSW(data);
}
/**
* writeIgnored(data, addr)
*
* @this {DevicePDP11}
* @param {number} data
* @param {number} addr
*/
writeIgnored(data, addr)
{
if (this.messageEnabled()) {
this.printMessage("writeIgnored(" + Str.toOct(addr) + "): " + Str.toOct(data), true, true);
}
}
/**
* DevicePDP11.init()
*
* This function operates on every HTML element of class "device", extracting the
* JSON-encoded parameters for the DevicePDP11 constructor from the element's "data-value"
* attribute, invoking the constructor to create a DevicePDP11 component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeDevice = Component.getElementsByClass(document, PDP11.APPCLASS, "device");
for (var iDevice = 0; iDevice < aeDevice.length; iDevice++) {
var device;
var eDevice = aeDevice[iDevice];
var parmsDevice = Component.getComponentParms(eDevice);
switch(parmsDevice['type']) {
case 'default':
device = new DevicePDP11(parmsDevice);
Component.bindComponentControls(device, eDevice, PDP11.APPCLASS);
break;
case 'pc11':
device = new PC11(parmsDevice);
Component.bindComponentControls(device, eDevice, PDP11.APPCLASS);
break;
case 'rl11':
device = new RL11(parmsDevice);
Component.bindComponentControls(device, eDevice, PDP11.APPCLASS);
break;
case 'rk11':
device = new RK11(parmsDevice);
Component.bindComponentControls(device, eDevice, PDP11.APPCLASS);
break;
case 'rx11':
device = new RX11(parmsDevice);
Component.bindComponentControls(device, eDevice, PDP11.APPCLASS);
break;
}
}
}
}
/*
* ES6 ALERT: As you can see below, I've finally started using computed property names.
*/
DevicePDP11.UNIBUS_IOTABLE = {
[PDP11.UNIBUS.UNIMAP]: /* 170200 */ [null, null, DevicePDP11.prototype.readUNIMAP, DevicePDP11.prototype.writeUNIMAP, "UNIMAP", 64, PDP11.MODEL_1170],
[PDP11.UNIBUS.SIPDR0]: /* 172200 */ [null, null, DevicePDP11.prototype.readSIPDR, DevicePDP11.prototype.writeSIPDR, "SIPDR", 8, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.SDPDR0]: /* 172220 */ [null, null, DevicePDP11.prototype.readSDPDR, DevicePDP11.prototype.writeSDPDR, "SDPDR", 8, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.SIPAR0]: /* 172240 */ [null, null, DevicePDP11.prototype.readSIPAR, DevicePDP11.prototype.writeSIPAR, "SIPAR", 8, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.SDPAR0]: /* 172260 */ [null, null, DevicePDP11.prototype.readSDPAR, DevicePDP11.prototype.writeSDPAR, "SDPAR", 8, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.KIPDR0]: /* 172300 */ [null, null, DevicePDP11.prototype.readKIPDR, DevicePDP11.prototype.writeKIPDR, "KIPDR", 8, PDP11.MODEL_1140, MessagesPDP11.MMU],
[PDP11.UNIBUS.KDPDR0]: /* 172320 */ [null, null, DevicePDP11.prototype.readKDPDR, DevicePDP11.prototype.writeKDPDR, "KDPDR", 8, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.KIPAR0]: /* 172340 */ [null, null, DevicePDP11.prototype.readKIPAR, DevicePDP11.prototype.writeKIPAR, "KIPAR", 8, PDP11.MODEL_1140, MessagesPDP11.MMU],
[PDP11.UNIBUS.KDPAR0]: /* 172360 */ [null, null, DevicePDP11.prototype.readKDPAR, DevicePDP11.prototype.writeKDPAR, "KDPAR", 8, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.MMR3]: /* 172516 */ [null, null, DevicePDP11.prototype.readMMR3, DevicePDP11.prototype.writeMMR3, "MMR3", 1, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.LKS]: /* 177546 */ [null, null, DevicePDP11.prototype.readLKS, DevicePDP11.prototype.writeLKS, "LKS"],
[PDP11.UNIBUS.MMR0]: /* 177572 */ [null, null, DevicePDP11.prototype.readMMR0, DevicePDP11.prototype.writeMMR0, "MMR0", 1, PDP11.MODEL_1140, MessagesPDP11.MMU],
[PDP11.UNIBUS.MMR1]: /* 177574 */ [null, null, DevicePDP11.prototype.readMMR1, DevicePDP11.prototype.writeIgnored, "MMR1", 1, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.MMR2]: /* 177576 */ [null, null, DevicePDP11.prototype.readMMR2, DevicePDP11.prototype.writeIgnored, "MMR2", 1, PDP11.MODEL_1140, MessagesPDP11.MMU],
[PDP11.UNIBUS.UIPDR0]: /* 177600 */ [null, null, DevicePDP11.prototype.readUIPDR, DevicePDP11.prototype.writeUIPDR, "UIPDR", 8, PDP11.MODEL_1140, MessagesPDP11.MMU],
[PDP11.UNIBUS.UDPDR0]: /* 177620 */ [null, null, DevicePDP11.prototype.readUDPDR, DevicePDP11.prototype.writeUDPDR, "UDPDR", 8, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.UIPAR0]: /* 177640 */ [null, null, DevicePDP11.prototype.readUIPAR, DevicePDP11.prototype.writeUIPAR, "UIPAR", 8, PDP11.MODEL_1140, MessagesPDP11.MMU],
[PDP11.UNIBUS.UDPAR0]: /* 177660 */ [null, null, DevicePDP11.prototype.readUDPAR, DevicePDP11.prototype.writeUDPAR, "UDPAR", 8, PDP11.MODEL_1145, MessagesPDP11.MMU],
[PDP11.UNIBUS.R0SET0]: /* 177700 */ [null, null, DevicePDP11.prototype.readRSET0, DevicePDP11.prototype.writeRSET0, "R0SET0"],
[PDP11.UNIBUS.R1SET0]: /* 177701 */ [null, null, DevicePDP11.prototype.readRSET0, DevicePDP11.prototype.writeRSET0, "R1SET0"],
[PDP11.UNIBUS.R2SET0]: /* 177702 */ [null, null, DevicePDP11.prototype.readRSET0, DevicePDP11.prototype.writeRSET0, "R2SET0"],
[PDP11.UNIBUS.R3SET0]: /* 177703 */ [null, null, DevicePDP11.prototype.readRSET0, DevicePDP11.prototype.writeRSET0, "R3SET0"],
[PDP11.UNIBUS.R4SET0]: /* 177704 */ [null, null, DevicePDP11.prototype.readRSET0, DevicePDP11.prototype.writeRSET0, "R4SET0"],
[PDP11.UNIBUS.R5SET0]: /* 177705 */ [null, null, DevicePDP11.prototype.readRSET0, DevicePDP11.prototype.writeRSET0, "R5SET0"],
[PDP11.UNIBUS.R6KERNEL]:/* 177706 */ [null, null, DevicePDP11.prototype.readR6KERNEL,DevicePDP11.prototype.writeR6KERNEL,"R6KERNEL"],
[PDP11.UNIBUS.R7KERNEL]:/* 177707 */ [null, null, DevicePDP11.prototype.readR7KERNEL,DevicePDP11.prototype.writeR7KERNEL,"R7KERNEL"],
[PDP11.UNIBUS.R0SET1]: /* 177710 */ [null, null, DevicePDP11.prototype.readRSET1, DevicePDP11.prototype.writeRSET1, "R0SET1", 1, PDP11.MODEL_1145],
[PDP11.UNIBUS.R1SET1]: /* 177711 */ [null, null, DevicePDP11.prototype.readRSET1, DevicePDP11.prototype.writeRSET1, "R1SET1", 1, PDP11.MODEL_1145],
[PDP11.UNIBUS.R2SET1]: /* 177712 */ [null, null, DevicePDP11.prototype.readRSET1, DevicePDP11.prototype.writeRSET1, "R2SET1", 1, PDP11.MODEL_1145],
[PDP11.UNIBUS.R3SET1]: /* 177713 */ [null, null, DevicePDP11.prototype.readRSET1, DevicePDP11.prototype.writeRSET1, "R3SET1", 1, PDP11.MODEL_1145],
[PDP11.UNIBUS.R4SET1]: /* 177714 */ [null, null, DevicePDP11.prototype.readRSET1, DevicePDP11.prototype.writeRSET1, "R4SET1", 1, PDP11.MODEL_1145],
[PDP11.UNIBUS.R5SET1]: /* 177715 */ [null, null, DevicePDP11.prototype.readRSET1, DevicePDP11.prototype.writeRSET1, "R5SET1", 1, PDP11.MODEL_1145],
[PDP11.UNIBUS.R6SUPER]: /* 177716 */ [null, null, DevicePDP11.prototype.readR6SUPER, DevicePDP11.prototype.writeR6SUPER, "R6SUPER", 1, PDP11.MODEL_1145],
[PDP11.UNIBUS.R6USER]: /* 177717 */ [null, null, DevicePDP11.prototype.readR6USER, DevicePDP11.prototype.writeR6USER, "R6USER", 1, PDP11.MODEL_1145],
[PDP11.UNIBUS.CTRL]: /* 177740 */ [null, null, DevicePDP11.prototype.readCTRL, DevicePDP11.prototype.writeCTRL, "CTRL", 8, PDP11.MODEL_1170],
[PDP11.UNIBUS.LSIZE]: /* 177760 */ [null, null, DevicePDP11.prototype.readSIZE, DevicePDP11.prototype.writeSIZE, "LSIZE", 1, PDP11.MODEL_1170],
[PDP11.UNIBUS.HSIZE]: /* 177762 */ [null, null, DevicePDP11.prototype.readSIZE, DevicePDP11.prototype.writeSIZE, "HSIZE", 1, PDP11.MODEL_1170],
[PDP11.UNIBUS.SYSID]: /* 177764 */ [null, null, DevicePDP11.prototype.readSYSID, DevicePDP11.prototype.writeSYSID, "SYSID", 1, PDP11.MODEL_1170],
[PDP11.UNIBUS.CPUERR]: /* 177766 */ [null, null, DevicePDP11.prototype.readCPUERR, DevicePDP11.prototype.writeCPUERR, "ERR", 1, PDP11.MODEL_1170],
[PDP11.UNIBUS.MB]: /* 177770 */ [null, null, DevicePDP11.prototype.readMBR, DevicePDP11.prototype.writeMBR, "MBR", 1, PDP11.MODEL_1170],
[PDP11.UNIBUS.PIR]: /* 177772 */ [null, null, DevicePDP11.prototype.readPIR, DevicePDP11.prototype.writePIR, "PIR"],
[PDP11.UNIBUS.SL]: /* 177774 */ [null, null, DevicePDP11.prototype.readSLR, DevicePDP11.prototype.writeSLR, "SLR"],
[PDP11.UNIBUS.PSW]: /* 177776 */ [null, null, DevicePDP11.prototype.readPSW, DevicePDP11.prototype.writePSW, "PSW"]
};
/*
* Initialize all the DevicePDP11 modules on the page.
*/
Web.onInit(DevicePDP11.init);
/**
* @copyright http://pcjs.org/modules/pdp11/lib/memory.js (C) Jeff Parsons 2012-2017
*/
/**
* @class DataView
* @property {function(number,boolean):number} getUint8
* @property {function(number,number,boolean)} setUint8
* @property {function(number,boolean):number} getUint16
* @property {function(number,number,boolean)} setUint16
* @property {function(number,boolean):number} getInt32
* @property {function(number,number,boolean)} setInt32
*/
class MemoryPDP11 {
/**
* MemoryPDP11(bus, addr, used, size, type, controller)
*
* The Bus component allocates Memory objects so that each has a memory buffer with a
* block-granular starting address and an address range equal to bus.nBlockSize; however,
* the size of any given Memory object's underlying buffer can be either zero or bus.nBlockSize;
* memory read/write functions for empty (buffer-less) blocks are mapped to readNone/writeNone.
*
* The Bus allocates empty blocks for the entire address space during initialization, so that
* any reads/writes to undefined addresses will have no effect. Later, the ROM and RAM
* components will ask the Bus to allocate memory for specific ranges, and the Bus will allocate
* as many new blockSize Memory objects as the ranges require. Partial Memory blocks could
* also be supported in theory, but in practice, they're not.
*
* Because Memory blocks now allow us to have a "sparse" address space, we could choose to
* take the memory hit of allocating 4K arrays per block, where each element stores only one byte,
* instead of the more frugal but slightly slower approach of allocating arrays of 32-bit dwords
* (LONGARRAYS) and shifting/masking bytes/words to/from dwords; in theory, byte accesses would
* be faster and word accesses somewhat less faster.
*
* However, preliminary testing of that feature (BYTEARRAYS) did not yield significantly faster
* performance, so it is OFF by default to minimize our memory consumption. Using TYPEDARRAYS
* would seem best, but as discussed in defines.js, it's off by default, because it doesn't perform
* as well as LONGARRAYS; the other advantage of TYPEDARRAYS is that it should theoretically use
* about 1/2 the memory of LONGARRAYS (32-bit elements vs 64-bit numbers), but I value speed over
* size at this point. Also, not all JavaScript implementations support TYPEDARRAYS (IE9 is probably
* the only real outlier: it lacks typed arrays but otherwise has all the necessary HTML5 support).
*
* WARNING: Since Memory blocks are low-level objects that have no UI requirements, they
* do not inherit from the Component class, so if you want to use any Component class methods,
* such as Component.assert(), use the corresponding Debugger methods instead (assuming a debugger
* is available).
*
* @param {BusPDP11} bus
* @param {number|null} [addr] of lowest used address in block
* @param {number} [used] portion of block in bytes (0 for none); must be a multiple of 4
* @param {number} [size] of block's buffer in bytes (0 for none); must be a multiple of 4
* @param {number} [type] is one of the MemoryPDP11.TYPE constants (default is MemoryPDP11.TYPE.NONE)
* @param {Object} [controller] is an optional memory controller component
*/
constructor(bus, addr, used, size, type, controller)
{
var a, i;
this.bus = bus;
this.id = (MemoryPDP11.idBlock += 2);
this.adw = null;
this.offset = 0;
this.addr = addr;
this.used = used;
this.size = size || 0;
this.type = type || MemoryPDP11.TYPE.NONE;
this.fReadOnly = (type == MemoryPDP11.TYPE.ROM);
this.controller = null;
this.dbg = null;
this.readByte = this.readByteDirect = this.readNone;
this.readWord = this.readWordDirect = this.readWordDefault;
this.writeByte = this.writeByteDirect = this.writeNone;
this.writeWord = this.writeWordDirect = this.writeWordDefault;
this.cReadBreakpoints = this.cWriteBreakpoints = 0;
this.copyBreakpoints(); // initialize the block's Debugger info; the caller will reinitialize
/*
* TODO: Study the impact of dirty block tracking. The original purposes were to allow saveMemory()
* to save only dirty blocks, and to enable the Video component to quickly detect changes to the video buffer.
* But the benefit to saveMemory() is minimal, and the Video component has other options; for example, it can
* now use a custom memory controller that performs its own dirty block tracking.
*
* However, a quick test with dirty block tracking disabled didn't yield a noticeable improvement in performance,
* so I think the overhead of our block-based architecture is swamping the impact of these micro-updates.
*/
this.fDirty = this.fDirtyEver = false;
/*
* For empty memory blocks, all we need to do is ensure all access functions are mapped to "none" handlers.
*/
if (!this.size) {
this.setAccess();
return;
}
/*
* When a controller is specified, the controller must provide a buffer, via getControllerBuffer(),
* and memory access functions, via getControllerAccess().
*/
if (controller) {
this.controller = controller;
a = controller.getControllerBuffer(addr);
this.adw = a[0];
this.offset = a[1];
this.setAccess(controller.getControllerAccess());
return;
}
/*
* This is the normal case: allocate a buffer that provides 8 bits of data per address;
* no controller is required because our default memory access functions (see afnMemory)
* know how to deal with this simple 1-1 mapping of addresses to bytes and words.
*
* TODO: Consider initializing the memory array to random (or pseudo-random) values in DEBUG
* mode; pseudo-random might be best, to help make any bugs reproducible.
*/
if (TYPEDARRAYS) {
this.buffer = new ArrayBuffer(this.size);
this.dv = new DataView(this.buffer, 0, this.size);
/*
* If littleEndian is true, we can use ab[], aw[] and adw[] directly; well, we can use them
* whenever the offset is a multiple of 1, 2 or 4, respectively. Otherwise, we must fallback to
* dv.getUint8()/dv.setUint8(), dv.getUint16()/dv.setUint16() and dv.getInt32()/dv.setInt32().
*/
this.ab = new Uint8Array(this.buffer, 0, this.size);
this.aw = new Uint16Array(this.buffer, 0, this.size >> 1);
this.adw = new Int32Array(this.buffer, 0, this.size >> 2);
this.setAccess(littleEndian? MemoryPDP11.afnArrayLE : MemoryPDP11.afnArrayBE);
} else {
/*
* NOTE: An ArrayBuffer is defined as being zero-initialized, but the elements of a new
* Array are not, so this code path takes care of zero-initialization ourselves.
*/
if (BYTEARRAYS) {
a = this.ab = new Array(this.size);
} else {
/*
* NOTE: This used to be the default mode of operation (!TYPEDARRAYS && !BYTEARRAYS), because
* it seemed to provide the best performance; however, that was then, and this is now. TYPEDARRAYS
* is more efficient.
*/
a = this.adw = new Array(this.size >> 2);
}
for (i = 0; i < a.length; i++) a[i] = 0;
this.setAccess(MemoryPDP11.afnMemory);
}
}
/**
* init(addr)
*
* Quick reinitializer when reusing a Memory block.
*
* @this {MemoryPDP11}
* @param {number} addr
*/
init(addr)
{
this.addr = addr;
}
/**
* clone(mem, type, dbg)
*
* Converts the current Memory block (this) into a clone of the given Memory block (mem),
* and optionally overrides the current block's type with the specified type.
*
* @this {MemoryPDP11}
* @param {MemoryPDP11} mem
* @param {number} [type]
* @param {DebuggerPDP11} [dbg]
*/
clone(mem, type, dbg)
{
/*
* Original memory block IDs are even; cloned memory block IDs are odd;
* the original ID of the current block is lost, but that's OK, since it was presumably
* produced merely to become a clone.
*/
this.id = mem.id | 0x1;
this.used = mem.used;
this.size = mem.size;
if (type) {
this.type = type;
this.fReadOnly = (type == MemoryPDP11.TYPE.ROM);
}
if (TYPEDARRAYS) {
this.buffer = mem.buffer;
this.dv = mem.dv;
this.ab = mem.ab;
this.aw = mem.aw;
this.adw = mem.adw;
this.setAccess(littleEndian? MemoryPDP11.afnArrayLE : MemoryPDP11.afnArrayBE);
} else {
if (BYTEARRAYS) {
this.ab = mem.ab;
} else {
this.adw = mem.adw;
}
this.setAccess(MemoryPDP11.afnMemory);
}
this.copyBreakpoints(dbg, mem);
}
/**
* save()
*
* This gets the contents of a Memory block as an array of 32-bit values; used by Bus.saveMemory(),
* which in turn is called by CPUState.save().
*
* Memory blocks with custom memory controllers do NOT save their contents; that's the responsibility
* of the controller component.
*
* @this {MemoryPDP11}
* @return {Array|Int32Array|null}
*/
save()
{
var adw, i;
if (this.controller) {
adw = null;
}
else if (BYTEARRAYS) {
adw = new Array(this.size >> 2);
var off = 0;
for (i = 0; i < adw.length; i++) {
adw[i] = this.ab[off] | (this.ab[off + 1] << 8) | (this.ab[off + 2] << 16) | (this.ab[off + 3] << 24);
off += 4;
}
}
else if (TYPEDARRAYS) {
/*
* It might be tempting to just return a copy of Int32Array(this.buffer, 0, this.size >> 2),
* but we can't be sure of the "endianness" of an Int32Array -- which would be OK if the array
* was always saved/restored on the same machine, but there's no guarantee of that, either.
* So we use getInt32() and require little-endian values.
*
* Moreover, an Int32Array isn't treated by JSON.stringify() and JSON.parse() exactly like
* a normal array; it's serialized as an Object rather than an Array, so it lacks a "length"
* property and causes problems for State.store() and State.parse().
*/
adw = new Array(this.size >> 2);
for (i = 0; i < adw.length; i++) {
adw[i] = this.dv.getInt32(i << 2, true);
}
}
else {
adw = this.adw;
}
return adw;
}
/**
* restore(adw)
*
* This restores the contents of a Memory block from an array of 32-bit values;
* used by Bus.restoreMemory(), which is called by CPUState.restore(), after all other
* components have been restored and thus all Memory blocks have been allocated
* by their respective components.
*
* @this {MemoryPDP11}
* @param {Array|null} adw
* @return {boolean} true if successful, false if block size mismatch
*/
restore(adw)
{
if (this.controller) {
return (adw == null);
}
/*
* At this point, it's a consistency error for adw to be null; it's happened once already,
* when there was a restore bug in the Video component that added the frame buffer at the video
* card's "spec'ed" address instead of the programmed address, so there were no controller-owned
* memory blocks installed at the programmed address, and so we arrived here at a block with
* no controller AND no data.
*/
if (adw && this.size == adw.length << 2) {
var i;
if (BYTEARRAYS) {
var off = 0;
for (i = 0; i < adw.length; i++) {
this.ab[off] = adw[i] & 0xff;
this.ab[off + 1] = (adw[i] >> 8) & 0xff;
this.ab[off + 2] = (adw[i] >> 16) & 0xff;
this.ab[off + 3] = (adw[i] >> 24) & 0xff;
off += 4;
}
} else if (TYPEDARRAYS) {
for (i = 0; i < adw.length; i++) {
this.dv.setInt32(i << 2, adw[i], true);
}
} else {
this.adw = adw;
}
this.fDirty = true;
return true;
}
return false;
}
/**
* zero(off, len, pattern)
*
* Zeros the block. Supporting off and len parameters is probably overkill, and makes more
* work in the non-TYPEDARRAY, non-BYTEARRAY case, but that's not the typical case. The other
* exception is controller-based blocks, which may not have any array backing at all.
*
* @this {MemoryPDP11}
* @param {number} [off] (optional starting byte offset within block)
* @param {number} [len] (optional maximum number of bytes; default is the entire block)
* @param {number} [pattern]
*/
zero(off, len, pattern)
{
var i;
off = off || 0;
pattern = (pattern || 0) & 0xff; // pattern & 0xff wasn't good enough for the Closure Compiler
/*
* NOTE: If len happens to be larger than the block, that's OK, because we also bounds-check the index.
*/
if (len === undefined) len = this.size;
if ((TYPEDARRAYS || BYTEARRAYS) && this.ab) {
for (i = off; len-- && i < this.ab.length; i++) this.ab[i] = pattern;
} else {
for (i = off; len-- && i < this.size; i++) this.writeByteDirect(off, pattern, this.addr + off);
}
}
/**
* setAccess(afn, fDirect)
*
* The afn parameter should be a 4-entry function table containing two byte handlers and
* two word handlers. See the static afnMemory table for an example.
*
* If no function table is specified, a default is selected based on the Memory type;
* similarly, any undefined entries in the table are filled with default handlers that fall
* back to the byte handlers, and if one or both byte handlers are undefined, they default
* to handlers that simply ignore the access.
*
* fDirect indicates that both the default AND the direct handlers should be updated. Direct
* handlers normally match the default handlers, except when "checked" handlers are installed;
* this allows "checked" handlers to know where to dispatch the call after performing checks.
* Examples of checks are read/write breakpoints, but it's really up to the Debugger to decide
* what the check consists of.
*
* @this {MemoryPDP11}
* @param {Array.<function()>} [afn] function table
* @param {boolean} [fDirect] (true to update direct access functions as well; default is true)
*/
setAccess(afn, fDirect)
{
if (!afn) {
afn = MemoryPDP11.afnNone;
}
this.setReadAccess(afn, fDirect);
this.setWriteAccess(afn, fDirect);
}
/**
* setReadAccess(afn, fDirect)
*
* @this {MemoryPDP11}
* @param {Array.<function()>} afn
* @param {boolean} [fDirect]
*/
setReadAccess(afn, fDirect)
{
if (!fDirect || !this.cReadBreakpoints) {
this.readByte = afn[0] || this.readNone;
this.readWord = afn[2] || this.readWordDefault;
}
if (fDirect || fDirect === undefined) {
this.readByteDirect = afn[0] || this.readNone;
this.readWordDirect = afn[2] || this.readWordDefault;
}
}
/**
* setWriteAccess(afn, fDirect)
*
* @this {MemoryPDP11}
* @param {Array.<function()>} afn
* @param {boolean} [fDirect]
*/
setWriteAccess(afn, fDirect)
{
if (!fDirect || !this.cWriteBreakpoints) {
this.writeByte = !this.fReadOnly && afn[1] || this.writeNone;
this.writeWord = !this.fReadOnly && afn[3] || this.writeWordDefault;
}
if (fDirect || fDirect === undefined) {
this.writeByteDirect = afn[1] || this.writeNone;
this.writeWordDirect = afn[3] || this.writeWordDefault;
}
}
/**
* resetReadAccess()
*
* @this {MemoryPDP11}
*/
resetReadAccess()
{
this.readByte = this.readByteDirect;
this.readWord = this.readWordDirect;
}
/**
* resetWriteAccess()
*
* @this {MemoryPDP11}
*/
resetWriteAccess()
{
this.writeByte = this.fReadOnly? this.writeNone : this.writeByteDirect;
this.writeWord = this.fReadOnly? this.writeWordDefault : this.writeWordDirect;
}
/**
* printAddr(sMessage)
*
* @this {MemoryPDP11}
* @param {string} sMessage
*/
printAddr(sMessage)
{
if (DEBUG && this.dbg && this.dbg.messageEnabled(MessagesPDP11.MEMORY)) {
this.dbg.printMessage(sMessage + ' ' + (this.addr != null? ('@' + this.dbg.toStrBase(this.addr)) : '#' + this.id), true);
}
}
/**
* addBreakpoint(off, fWrite)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {boolean} fWrite
*/
addBreakpoint(off, fWrite)
{
if (!fWrite) {
if (this.cReadBreakpoints++ === 0) {
this.setReadAccess(MemoryPDP11.afnChecked, false);
}
if (DEBUG) this.printAddr("read breakpoint added to memory block");
}
else {
if (this.cWriteBreakpoints++ === 0) {
this.setWriteAccess(MemoryPDP11.afnChecked, false);
}
if (DEBUG) this.printAddr("write breakpoint added to memory block");
}
}
/**
* removeBreakpoint(off, fWrite)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {boolean} fWrite
*/
removeBreakpoint(off, fWrite)
{
if (!fWrite) {
if (--this.cReadBreakpoints === 0) {
this.resetReadAccess();
if (DEBUG) this.printAddr("all read breakpoints removed from memory block");
}
}
else {
if (--this.cWriteBreakpoints === 0) {
this.resetWriteAccess();
if (DEBUG) this.printAddr("all write breakpoints removed from memory block");
}
}
}
/**
* copyBreakpoints(dbg, mem)
*
* @this {MemoryPDP11}
* @param {DebuggerPDP11} [dbg]
* @param {MemoryPDP11} [mem] (outgoing MemoryPDP11 block to copy breakpoints from, if any)
*/
copyBreakpoints(dbg, mem)
{
this.dbg = dbg;
this.cReadBreakpoints = this.cWriteBreakpoints = 0;
if (mem) {
if ((this.cReadBreakpoints = mem.cReadBreakpoints)) {
this.setReadAccess(MemoryPDP11.afnChecked, false);
}
if ((this.cWriteBreakpoints = mem.cWriteBreakpoints)) {
this.setWriteAccess(MemoryPDP11.afnChecked, false);
}
}
}
/**
* readNone(off, addr)
*
* Previously, this always returned 0x00, but the initial memory probe by the COMPAQ DeskPro 386 ROM BIOS
* writes 0x0000 to the first word of every 64Kb block in the nearly 16Mb address space it supports, and
* if it reads back 0x0000, it will initially think that LOTS of RAM exists, only to be disappointed later
* when it performs a more exhaustive memory test, generating unwanted error messages in the process.
*
* TODO: Determine if we should have separate readByteNone(), readWordNone() and readLongNone() functions
* to return 0xff, 0xffff and 0xffffffff|0, respectively. This seems sufficient for now, as it seems unlikely
* that a system would require nonexistent memory locations to return ALL bits set. However, another factor
* is whether or not ODDADDR faults take precedence over NOMEMORY faults; if they do, then we need separate
* interfaces.
*
* Also, I'm reluctant to address that potential issue by simply returning -1, because to date, the above
* Memory interfaces have always returned values that are properly masked to 8, 16 or 32 bits, respectively.
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readNone(off, addr)
{
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.MEMORY) /* && !off */) {
this.dbg.printMessage("attempt to read invalid address " + this.dbg.toStrBase(addr), true);
}
this.bus.fault(addr, PDP11.CPUERR.NOMEMORY, PDP11.ACCESS.READ);
return 0xff;
}
/**
* writeNone(off, v, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} v (could be either a byte or word value, since we use the same handler for both kinds of accesses)
* @param {number} addr
*/
writeNone(off, v, addr)
{
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.MEMORY) /* && !off */) {
this.dbg.printMessage("attempt to write " + this.dbg.toStrBase(v) + " to invalid addresses " + this.dbg.toStrBase(addr), true);
}
this.bus.fault(addr, PDP11.CPUERR.NOMEMORY, PDP11.ACCESS.WRITE);
}
/**
* readWordDefault(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readWordDefault(off, addr)
{
return this.readByte(off++, addr++) | (this.readByte(off, addr) << 8);
}
/**
* writeWordDefault(off, w, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} w
* @param {number} addr
*/
writeWordDefault(off, w, addr)
{
this.writeByte(off++, w & 0xff, addr++);
this.writeByte(off, w >> 8, addr);
}
/**
* readByteMemory(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readByteMemory(off, addr)
{
if (BYTEARRAYS) {
return this.ab[off];
}
return ((this.adw[off >> 2] >>> ((off & 0x3) << 3)) & 0xff);
}
/**
* readWordMemory(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readWordMemory(off, addr)
{
if (PDP11.MEMFAULT && (off & 0x1)) {
this.bus.fault(addr, PDP11.CPUERR.ODDADDR, PDP11.ACCESS.READ_WORD);
}
if (BYTEARRAYS) {
return this.ab[off] | (this.ab[off + 1] << 8);
}
var w;
var idw = off >> 2;
var nShift = (off & 0x3) << 3;
var dw = (this.adw[idw] >> nShift);
if (nShift < 24) {
w = dw & 0xffff;
} else {
w = (dw & 0xff) | ((this.adw[idw + 1] & 0xff) << 8);
}
return w;
}
/**
* writeByteMemory(off, b, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} b
* @param {number} addr
*/
writeByteMemory(off, b, addr)
{
if (BYTEARRAYS) {
this.ab[off] = b;
} else {
var idw = off >> 2;
var nShift = (off & 0x3) << 3;
this.adw[idw] = (this.adw[idw] & ~(0xff << nShift)) | (b << nShift);
}
this.fDirty = true;
}
/**
* writeWordMemory(off, w, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} w
* @param {number} addr
*/
writeWordMemory(off, w, addr)
{
if (PDP11.MEMFAULT && (off & 0x1)) {
this.bus.fault(addr, PDP11.CPUERR.ODDADDR, PDP11.ACCESS.WRITE_WORD);
}
if (BYTEARRAYS) {
this.ab[off] = (w & 0xff);
this.ab[off + 1] = (w >> 8);
} else {
var idw = off >> 2;
var nShift = (off & 0x3) << 3;
if (nShift < 24) {
this.adw[idw] = (this.adw[idw] & ~(0xffff << nShift)) | (w << nShift);
} else {
this.adw[idw] = (this.adw[idw] & 0x00ffffff) | (w << 24);
idw++;
this.adw[idw] = (this.adw[idw] & (0xffffff00|0)) | (w >> 8);
}
}
this.fDirty = true;
}
/**
* readByteChecked(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readByteChecked(off, addr)
{
if (DEBUGGER && this.dbg && this.addr != null) {
this.dbg.checkMemoryRead(this.addr + off);
}
return this.readByteDirect(off, addr);
}
/**
* readWordChecked(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readWordChecked(off, addr)
{
if (DEBUGGER && this.dbg && this.addr != null) {
this.dbg.checkMemoryRead(this.addr + off, 2);
}
return this.readWordDirect(off, addr);
}
/**
* writeByteChecked(off, b, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} b
* @param {number} addr
*/
writeByteChecked(off, b, addr)
{
if (DEBUGGER && this.dbg && this.addr != null) {
this.dbg.checkMemoryWrite(this.addr + off);
}
if (this.fReadOnly) this.writeNone(off, b, addr); else this.writeByteDirect(off, b, addr);
}
/**
* writeWordChecked(off, w, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} w
* @param {number} addr
*/
writeWordChecked(off, w, addr)
{
if (DEBUGGER && this.dbg && this.addr != null) {
this.dbg.checkMemoryWrite(this.addr + off, 2)
}
if (this.fReadOnly) this.writeNone(off, w, addr); else this.writeWordDirect(off, w, addr);
}
/**
* readByteBE(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readByteBE(off, addr)
{
return this.ab[off];
}
/**
* readByteLE(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readByteLE(off, addr)
{
var b = this.ab[off];
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.MEMORY)) {
this.dbg.printMessage("Memory.readByte(" + this.dbg.toStrBase(addr) + "): " + this.dbg.toStrBase(b), true);
}
return b;
}
/**
* readWordBE(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readWordBE(off, addr)
{
if (PDP11.MEMFAULT && (off & 0x1)) {
this.bus.fault(addr, PDP11.CPUERR.ODDADDR, PDP11.ACCESS.READ_WORD);
}
return this.dv.getUint16(off, true);
}
/**
* readWordLE(off, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readWordLE(off, addr)
{
var w;
if (PDP11.MEMFAULT && (off & 0x1)) {
this.bus.fault(addr, PDP11.CPUERR.ODDADDR, PDP11.ACCESS.READ_WORD);
}
/*
* TODO: For non-WORDBUS machines, it remains to be seen if there's any advantage to checking the offset
* for an aligned read vs. always reading the bytes separately.
*/
if (PDP11.WORDBUS || !(off & 0x1)) {
w = this.aw[off >> 1];
} else {
w = this.ab[off] | (this.ab[off+1] << 8);
}
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.MEMORY)) {
this.dbg.printMessage("Memory.readWord(" + this.dbg.toStrBase(addr) + "): " + this.dbg.toStrBase(w), true);
}
return w;
}
/**
* writeByteBE(off, b, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} b
* @param {number} addr
*/
writeByteBE(off, b, addr)
{
this.ab[off] = b;
this.fDirty = true;
}
/**
* writeByteLE(off, b, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} b
* @param {number} addr
*/
writeByteLE(off, b, addr)
{
this.ab[off] = b;
this.fDirty = true;
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.MEMORY)) {
this.dbg.printMessage("Memory.writeByte(" + this.dbg.toStrBase(addr) + "," + this.dbg.toStrBase(b) + ")", true);
}
}
/**
* writeWordBE(off, w, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} w
* @param {number} addr
*/
writeWordBE(off, w, addr)
{
if (PDP11.MEMFAULT && (off & 0x1)) {
this.bus.fault(addr, PDP11.CPUERR.ODDADDR, PDP11.ACCESS.WRITE_WORD);
}
this.dv.setUint16(off, w, true);
this.fDirty = true;
}
/**
* writeWordLE(off, w, addr)
*
* @this {MemoryPDP11}
* @param {number} off
* @param {number} w
* @param {number} addr
*/
writeWordLE(off, w, addr)
{
if (PDP11.MEMFAULT && (off & 0x1)) {
this.bus.fault(addr, PDP11.CPUERR.ODDADDR, PDP11.ACCESS.WRITE_WORD);
}
/*
* TODO: For non-WORDBUS machines, it remains to be seen if there's any advantage to checking the offset
* for an aligned write vs. always writing the bytes separately.
*/
if (PDP11.WORDBUS || !(off & 0x1)) {
this.aw[off >> 1] = w;
} else {
this.ab[off] = w;
this.ab[off+1] = w >> 8;
}
this.fDirty = true;
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP11.MEMORY)) {
this.dbg.printMessage("Memory.writeWord(" + this.dbg.toStrBase(addr) + "," + this.dbg.toStrBase(w) + ")", true);
}
}
}
/*
* Basic memory types
*
* RAM is the most conventional memory type, providing full read/write capability to x86-compatible (ie,
* 'little endian") storage. ROM is equally conventional, except that the fReadOnly property is set,
* disabling writes. VIDEO is treated exactly like RAM, unless a controller is provided. Both RAM and
* VIDEO memory are always considered writable, and even ROM can be written using the Bus setByteDirect()
* interface (which in turn uses the Memory writeByteDirect() interface), allowing the ROM component to
* initialize its own memory. The CONTROLLER type is used to identify memory-mapped devices that do not
* need any default storage and always provide their own controller.
*
* Unallocated regions of the address space contain a special memory block of type NONE that contains
* no storage. Mapping every addressible location to a memory block allows all accesses to be routed in
* exactly the same manner, without resorting to any range or processor checks.
*
* These types are not mutually exclusive. For example, VIDEO memory could be allocated as RAM, with or
* without a custom controller (the original Monochrome and CGA video cards used read/write storage that
* was indistinguishable from RAM), and CONTROLLER memory could be allocated as an empty block of any type,
* with a custom controller. A few types are required for certain features (eg, ROM is required if you want
* read-only memory), but the larger purpose of these types is to help document the caller's intent and to
* provide the Control Panel with the ability to highlight memory regions accordingly.
*/
MemoryPDP11.TYPE = {
NONE: 0,
RAM: 1,
ROM: 2,
VIDEO: 3,
CONTROLLER: 4
};
MemoryPDP11.TYPE_COLORS = ["black", "blue", "green", "cyan"];
MemoryPDP11.TYPE_NAMES = ["NONE", "RAM", "ROM", "VID", "H/W"];
/*
* Last used block ID (used for debugging only)
*/
MemoryPDP11.idBlock = 0;
/*
* This is the effective definition of afnNone, but we need not fully define it, because setAccess()
* uses these defaults when any of the 4 handlers (ie, 2 byte handlers and 2 word handlers) are undefined.
*
MemoryPDP11.afnNone = [
MemoryPDP11.prototype.readNone,
MemoryPDP11.prototype.writeNone,
MemoryPDP11.prototype.readWordDefault,
MemoryPDP11.prototype.writeWordDefault
];
*/
MemoryPDP11.afnNone = [];
MemoryPDP11.afnMemory = [
MemoryPDP11.prototype.readByteMemory,
MemoryPDP11.prototype.writeByteMemory,
MemoryPDP11.prototype.readWordMemory,
MemoryPDP11.prototype.writeWordMemory
];
MemoryPDP11.afnChecked = [
MemoryPDP11.prototype.readByteChecked,
MemoryPDP11.prototype.writeByteChecked,
MemoryPDP11.prototype.readWordChecked,
MemoryPDP11.prototype.writeWordChecked
];
if (TYPEDARRAYS) {
MemoryPDP11.afnArrayBE = [
MemoryPDP11.prototype.readByteBE,
MemoryPDP11.prototype.writeByteBE,
MemoryPDP11.prototype.readWordBE,
MemoryPDP11.prototype.writeWordBE
];
MemoryPDP11.afnArrayLE = [
MemoryPDP11.prototype.readByteLE,
MemoryPDP11.prototype.writeByteLE,
MemoryPDP11.prototype.readWordLE,
MemoryPDP11.prototype.writeWordLE
];
}
var littleEndian = (TYPEDARRAYS? (function() {
var buffer = new ArrayBuffer(2);
new DataView(buffer).setUint16(0, 256, true);
return new Uint16Array(buffer)[0] === 256;
})() : false);
/**
* @copyright http://pcjs.org/modules/pdp11/lib/cpu.js (C) Jeff Parsons 2012-2017
*/
class CPUPDP11 extends Component {
/**
* CPUPDP11(parmsCPU, nCyclesDefault)
*
* The CPUPDP11 class supports the following (parmsCPU) properties:
*
* cycles: the machine's base cycles per second; the CPUStatePDP11 constructor
* will provide us with a default (based on the CPU model) to use as a fallback.
*
* multiplier: base cycle multiplier; default is 1.
*
* autoStart: true to automatically start, false to not, or null if "it depends";
* null is the default, which means do not autostart UNLESS there is no Debugger
* and no "Run" button (ie, no way to manually start the machine).
*
* csStart: the number of cycles that runCPU() must wait before generating
* checksum records; -1 if disabled. checksum records are a diagnostic aid
* used to help compare one CPU run to another.
*
* csInterval: the number of cycles that runCPU() must execute before generating
* a checksum record; -1 if disabled.
*
* csStop: the number of cycles to stop generating checksum records.
*
* This component is primarily responsible for interfacing the CPU with the outside
* world (eg, Panel and Debugger components), and managing overall CPU operation.
*
* It is extended by the CPUStatePDP11 component, where the simulation control logic resides.
*
* @param {Object} parmsCPU
* @param {number} nCyclesDefault
*/
constructor(parmsCPU, nCyclesDefault)
{
super("CPU", parmsCPU, MessagesPDP11.CPU);
var nCycles = +parmsCPU['cycles'] || nCyclesDefault;
var nMultiplier = +parmsCPU['multiplier'] || 1;
this.nDisplayCount = 0;
this.nDisplayLimit = 30;
this.nCyclesPerSecond = nCycles;
/*
* nCyclesMultiplier replaces the old "speed" variable (0, 1, 2) and eliminates the need for
* the constants (SPEED_SLOW, SPEED_FAST and SPEED_MAX). The UI simply doubles the multiplier
* until we've exceeded the host's speed limit and then starts the multiplier over at 1.
*/
this.nCyclesMultiplier = nMultiplier;
this.mhzDefault = Math.round(this.nCyclesPerSecond / 10000) / 100;
this.mhzTarget = this.mhzDefault * this.nCyclesMultiplier;
this.msPerYield = this.nCyclesPerYield = this.nCyclesNextYield = this.nCyclesRecalc = 0;
/*
* We add a number of flags to the set initialized by Component
*/
this.flags.running = this.flags.starting = false;
this.flags.autoStart = parmsCPU['autoStart'];
if (typeof this.flags.autoStart == "string") this.flags.autoStart = (this.flags.autoStart == "true");
/*
* Get checksum parameters, if any. runCPU() behavior is not affected until fChecksum
* is true, which won't happen until resetChecksum() is called with nCyclesChecksumInterval
* ("csInterval") set to a positive value.
*
* As above, any of these parameters can also be set with the Debugger's execution options
* command ("x"); for example, "x cs int 5000" will set nCyclesChecksumInterval to 5000
* and call resetChecksum().
*/
this.flags.checksum = false;
this.nChecksum = this.nCyclesChecksumNext = 0;
this.nCyclesChecksumStart = +parmsCPU["csStart"];
this.nCyclesChecksumInterval = +parmsCPU["csInterval"];
this.nCyclesChecksumStop = +parmsCPU["csStop"];
/*
* Array of countdown timers managed by addTimer() and setTimer().
*/
this.aTimers = [];
this.onRunTimeout = this.runCPU.bind(this); // function onRunTimeout() { cpu.runCPU(); };
/*
* Define the rest of the properties used by the class
*/
this.mhz = 0;
this.nYieldsSinceStatusUpdate = 0;
this.msStartRun = this.msStartThisRun = this.msEndThisRun = this.nCyclesThisRun = 0;
this.nTotalCycles = this.nRunCycles = this.nBurstCycles = this.nStepCycles = this.nSnapCycles = 0;
this.panel = null;
this.setReady();
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {CPUPDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUPDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.dbg = dbg;
this.panel = cmp.panel;
for (var i = 0; i < CPUPDP11.BUTTONS.length; i++) {
var control = this.bindings[CPUPDP11.BUTTONS[i]];
if (control) this.cmp.setBinding(null, CPUPDP11.BUTTONS[i], control);
}
this.setReady();
}
/**
* reset()
*
* Stub for reset notification (overridden by the CPUStatePDP11 component).
*
* @this {CPUPDP11}
*/
reset()
{
}
/**
* save()
*
* Stub for save support (overridden by the CPUStatePDP11 component).
*
* @this {CPUPDP11}
* @return {Object|null}
*/
save()
{
return null;
}
/**
* restore(data)
*
* Stub for restore support (overridden by the CPUStatePDP11 component).
*
* @this {CPUPDP11}
* @param {Object} data
* @return {boolean} true if restore successful, false if not
*/
restore(data)
{
return false;
}
/**
* powerUp(data, fRepower)
*
* @this {CPUPDP11}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
/*
* We've already saved the parmsCPU 'autoStart' setting, but there may be a machine (or URL) override.
*/
var sAutoStart = this.cmp.getMachineParm('autoStart');
if (sAutoStart != null) {
this.flags.autoStart = (sAutoStart == "true"? true : (sAutoStart == "false"? false : !!sAutoStart));
}
else if (this.flags.autoStart == null) {
/*
* If there's no explicit parmsCPU setting either, then we will autoStart if there's no Debugger and
* no "Run" button.
*/
this.flags.autoStart = ((!DEBUGGER || !this.dbg) && this.bindings["run"] === undefined);
}
if (!fRepower) {
if (!data) {
this.reset();
} else {
this.resetCycles();
if (!this.restore(data)) return false;
this.resetChecksum();
}
/*
* Give the Debugger a chance to do/print something once we've powered up.
*/
if (DEBUGGER && this.dbg) {
this.dbg.init(this.flags.autoStart);
} else {
this.status("No debugger detected");
}
if (!this.flags.autoStart) {
this.println("CPU will not be auto-started " + (this.panel? "(click Run to start)" : "(type 'go' to start)"));
}
}
/*
* The Computer component (which is responsible for all powerDown and powerUp notifications)
* is now responsible for managing a component's fPowered flag, not us.
*
* this.flags.powered = true;
*/
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {CPUPDP11}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* autoStart()
*
* @this {CPUPDP11}
* @return {boolean} true if started, false if not
*/
autoStart()
{
if (this.flags.running) {
return true;
}
if (this.flags.autoStart) {
/*
* We used to also set fUpdateFocus when calling startCPU(), on the assumption that in the "auto-starting"
* context, a machine without focus is like a day without sunshine, but in reality, focus should only be
* forced when the user takes some other machine-related action.
*/
return this.startCPU();
}
return false;
}
/**
* isPowered()
*
* @this {CPUPDP11}
* @return {boolean}
*/
isPowered()
{
if (!this.flags.powered) {
this.println(this.toString() + " not powered");
return false;
}
return true;
}
/**
* isRunning()
*
* @this {CPUPDP11}
* @return {boolean}
*/
isRunning()
{
return this.flags.running;
}
/**
* getChecksum()
*
* This will be implemented by the CPUStatePDP11 component.
*
* @this {CPUPDP11}
* @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code)
*/
getChecksum()
{
return 0;
}
/**
* resetChecksum()
*
* If checksum generation is enabled (fChecksum is true), this resets the running 32-bit checksum and the
* cycle counter that will trigger the next displayChecksum(); called by resetCycles(), which is called whenever
* the CPU is reset or restored.
*
* @this {CPUPDP11}
* @return {boolean} true if checksum generation enabled, false if not
*/
resetChecksum()
{
if (this.nCyclesChecksumStart === undefined) this.nCyclesChecksumStart = 0;
if (this.nCyclesChecksumInterval === undefined) this.nCyclesChecksumInterval = -1;
if (this.nCyclesChecksumStop === undefined) this.nCyclesChecksumStop = -1;
this.flags.checksum = (this.nCyclesChecksumStart >= 0 && this.nCyclesChecksumInterval > 0);
if (this.flags.checksum) {
this.nChecksum = 0;
this.nCyclesChecksumNext = this.nCyclesChecksumStart - this.nTotalCycles;
/*
* this.nCyclesChecksumNext = this.nCyclesChecksumStart + this.nCyclesChecksumInterval -
* (this.nTotalCycles % this.nCyclesChecksumInterval);
*/
return true;
}
return false;
}
/**
* updateChecksum(nCycles)
*
* When checksum generation is enabled (fChecksum is true), runCPU() asks stepCPU() to execute a minimum
* number of cycles (1), effectively limiting execution to a single instruction, and then we're called with
* the exact number cycles that were actually executed. This should give us instruction-granular checksums
* at precise intervals that are 100% repeatable.
*
* @this {CPUPDP11}
* @param {number} nCycles
*/
updateChecksum(nCycles)
{
if (this.flags.checksum) {
/*
* Get a 32-bit summation of the current CPU state and add it to our running 32-bit checksum
*/
var fDisplay = false;
this.nChecksum = (this.nChecksum + this.getChecksum())|0;
this.nCyclesChecksumNext -= nCycles;
if (this.nCyclesChecksumNext <= 0) {
this.nCyclesChecksumNext += this.nCyclesChecksumInterval;
fDisplay = true;
}
if (this.nCyclesChecksumStop >= 0) {
if (this.nCyclesChecksumStop <= this.getCycles()) {
this.nCyclesChecksumInterval = this.nCyclesChecksumStop = -1;
this.resetChecksum();
this.stopCPU();
fDisplay = true;
}
}
if (fDisplay) this.displayChecksum();
}
}
/**
* displayChecksum()
*
* When checksum generation is enabled (fChecksum is true), this is called to provide a crude log of all
* checksums generated at the specified cycle intervals, as specified by the "csStart" and "csInterval" parmsCPU
* properties).
*
* @this {CPUPDP11}
*/
displayChecksum()
{
this.println(this.getCycles() + " cycles: " + "checksum=" + Str.toHex(this.nChecksum));
}
/**
* setBinding(sType, sBinding, control, sValue)
*
* @this {CPUPDP11}
* @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc)
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "run")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sType, sBinding, control, sValue)
{
var cpu = this;
switch (sBinding) {
case "power":
case "reset":
/*
* The "power" and "reset" buttons are functions of the entire computer, not just the CPU,
* but it's not always convenient to stick a power button in the Computer component definition,
* so we record those bindings here and pass them on to the Computer component in initBus().
*/
this.bindings[sBinding] = control;
return true;
case "run":
this.bindings[sBinding] = control;
control.onclick = function onClickRun() {
if (!cpu.cmp || !cpu.cmp.checkPower()) return;
/*
* We no longer pass true to these startCPU()/stopCPU() calls, on the theory that if the "run"
* control is visible, then the computer is probably sufficiently visible as well; the problem
* with setting fUpdateFocus to true is that it can jerk the web page around in annoying ways.
*/
if (!cpu.flags.running)
cpu.startCPU();
else
cpu.stopCPU();
};
return true;
case "speed":
this.bindings[sBinding] = control;
return true;
case "setSpeed":
this.bindings[sBinding] = control;
control.onclick = function onClickSetSpeed() {
cpu.setSpeed(cpu.nCyclesMultiplier << 1, true);
};
control.textContent = this.getSpeedTarget();
return true;
default:
break;
}
return false;
}
/**
* updateDisplays(nUpdate)
*
* Simpler wrapper around the Computer's updateDisplays() method.
*
* @this {CPUPDP11}
* @param {number} [nUpdate] (1 for periodic, -1 for forced, 0 or undefined otherwise)
*/
updateDisplays(nUpdate)
{
if (this.cmp) this.cmp.updateDisplays(nUpdate);
}
/**
* updateDisplay(nUpdate)
*
* Some of the CPU bindings provide feedback and therefore need to be updated periodically.
* However, this should be called via the Computer's updateDisplays() interface, not directly.
*
* @this {CPUPDP11}
* @param {number} [nUpdate] (1 for periodic, -1 for forced, 0 otherwise)
*/
updateDisplay(nUpdate)
{
var controlSpeed = this.bindings["speed"];
if (controlSpeed) {
if (nUpdate <= 0 || (this.nDisplayCount += nUpdate) >= this.nDisplayLimit) {
controlSpeed.textContent = this.getSpeedCurrent();
this.nDisplayCount = 0;
}
}
}
/**
* addCycles(nCycles, fEndStep)
*
* @this {CPUPDP11}
* @param {number} nCycles
* @param {boolean} [fEndStep]
*/
addCycles(nCycles, fEndStep)
{
this.nTotalCycles += nCycles;
if (fEndStep) {
this.nBurstCycles = this.nStepCycles = this.nSnapCycles = 0;
}
}
/**
* calcCycles(fRecalc)
*
* Calculate the number of cycles to process for each "burst" of CPU activity. The size of a burst
* is driven by YIELDS_PER_SECOND (eg, 30).
*
* At the end of each burst, we subtract burst cycles from the yield cycle "threshold" counter.
* Whenever the "next yield" cycle counter goes to (or below) zero, we compare elapsed time to the time
* we expected the virtual hardware to take (eg, 1000ms/50 or 20ms), and if we still have time remaining,
* we sleep the remaining time (or 0ms if there's no remaining time), and then restart runCPU().
*
* @this {CPUPDP11}
* @param {boolean} [fRecalc] is true if the caller wants to recalculate thresholds based on the most recent
* speed calculation (see calcSpeed).
*/
calcCycles(fRecalc)
{
/*
* Calculate "per" yield values.
*/
var vMultiplier = 1;
if (fRecalc) {
if (this.nCyclesMultiplier > 1 && this.mhz) {
vMultiplier = (this.mhz / this.mhzDefault);
}
}
this.msPerYield = Math.round(1000 / CPUPDP11.YIELDS_PER_SECOND);
this.nCyclesPerYield = Math.floor(this.nCyclesPerSecond / CPUPDP11.YIELDS_PER_SECOND * vMultiplier);
/*
* And initialize "next" yield values to the "per" values.
*/
if (!fRecalc) this.nCyclesNextYield = this.nCyclesPerYield;
this.nCyclesRecalc = 0;
}
/**
* getCycles(fScaled)
*
* getCycles() returns the number of cycles executed so far. Note that we can be called after
* runCPU() OR during runCPU(), perhaps from a handler triggered during the current run's stepCPU(),
* so nRunCycles must always be adjusted by number of cycles stepCPU() was asked to run (nBurstCycles),
* less the number of cycles it has yet to run (nStepCycles).
*
* nRunCycles is zeroed whenever the CPU is halted or the CPU speed is changed, which is why we also
* have nTotalCycles, which accumulates all nRunCycles before we zero it. However, nRunCycles and
* nTotalCycles eventually get reset by calcSpeed(), to avoid overflow, so components that rely on
* getCycles() returning steadily increasing values should also be prepared for a reset at any time.
*
* @this {CPUPDP11}
* @param {boolean} [fScaled] is true if the caller wants a cycle count relative to a multiplier of 1
* @return {number}
*/
getCycles(fScaled)
{
var nCycles = this.nTotalCycles + this.nRunCycles + this.nBurstCycles - this.nStepCycles;
if (fScaled && this.nCyclesMultiplier > 1 && this.mhz > this.mhzDefault) {
/*
* We could scale the current cycle count by the current effective speed (this.mhz); eg:
*
* nCycles = Math.round(nCycles / (this.mhz / this.mhzDefault));
*
* but that speed will fluctuate somewhat: large fluctuations at first, but increasingly smaller
* fluctuations after each burst of instructions that runCPU() executes.
*
* Alternatively, we can scale the cycle count by the multiplier, which is good in that the
* multiplier doesn't vary once the user changes it, but a potential downside is that the
* multiplier might be set too high, resulting in a target speed that's higher than the effective
* speed is able to reach.
*
* Also, if multipliers were always limited to a power-of-two, then this could be calculated
* with a simple shift. However, only the "setSpeed" UI binding limits it that way; the Debugger
* interface allows any value, as does the CPU "multiplier" parmsCPU property (from the machine's
* XML file).
*/
nCycles = Math.round(nCycles / this.nCyclesMultiplier);
}
return nCycles;
}
/**
* getCyclesPerSecond()
*
* This returns the CPU's "base" speed (ie, the original cycles per second defined for the machine)
*
* @this {CPUPDP11}
* @return {number}
*/
getCyclesPerSecond()
{
return this.nCyclesPerSecond;
}
/**
* resetCycles()
*
* Resets speed and cycle information as part of any reset() or restore(); this typically occurs during powerUp().
* It's important that this be called BEFORE the actual restore() call, because restore() may want to call setSpeed(),
* which in turn assumes that all the cycle counts have been initialized to sensible values.
*
* @this {CPUPDP11}
*/
resetCycles()
{
this.mhz = 0;
this.nYieldsSinceStatusUpdate = 0;
this.nTotalCycles = this.nRunCycles = this.nBurstCycles = this.nStepCycles = this.nSnapCycles = 0;
this.resetChecksum();
this.setSpeed(1);
}
/**
* getSpeed()
*
* @this {CPUPDP11}
* @return {number} the current speed multiplier
*/
getSpeed()
{
return this.nCyclesMultiplier;
}
/**
* getSpeedCurrent()
*
* @this {CPUPDP11}
* @return {string} the current speed, in mhz, as a string formatted to two decimal places
*/
getSpeedCurrent()
{
/*
* TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now?
*/
return ((this.flags.running)? (this.mhz.toFixed(2) + "Mhz") : "Stopped");
}
/**
* getSpeedTarget()
*
* @this {CPUPDP11}
* @return {string} the target speed, in mhz, as a string formatted to two decimal places
*/
getSpeedTarget()
{
/*
* TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now?
*/
return this.mhzTarget.toFixed(2) + "Mhz";
}
/**
* setSpeed(nMultiplier, fUpdateFocus)
*
* NOTE: This used to return the target speed, in mhz, but no callers appear to care at this point.
*
* @desc Whenever the speed is changed, the running cycle count and corresponding start time must be reset,
* so that the next effective speed calculation obtains sensible results. In fact, when runCPU() initially calls
* setSpeed() with no parameters, that's all this function does (it doesn't change the current speed setting).
*
* @this {CPUPDP11}
* @param {number} [nMultiplier] is the new proposed multiplier (reverts to 1 if the target was too high)
* @param {boolean} [fUpdateFocus] is true to update Computer focus
* @return {boolean} true if successful, false if not
*/
setSpeed(nMultiplier, fUpdateFocus)
{
var fSuccess = false;
if (nMultiplier !== undefined) {
/*
* If we haven't reached 80% (0.8) of the current target speed, revert to a multiplier of one (1).
*/
if (this.mhz / this.mhzTarget < 0.8) {
nMultiplier = 1;
} else {
fSuccess = true;
}
this.nCyclesMultiplier = nMultiplier;
var mhz = this.mhzDefault * this.nCyclesMultiplier;
if (this.mhzTarget != mhz) {
this.mhzTarget = mhz;
var sSpeed = this.getSpeedTarget();
var controlSpeed = this.bindings["setSpeed"];
if (controlSpeed) controlSpeed.textContent = sSpeed;
this.println("target speed: " + sSpeed);
}
if (fUpdateFocus && this.cmp) this.cmp.setFocus();
}
this.addCycles(this.nRunCycles);
this.nRunCycles = 0;
this.msStartRun = Component.getTime();
this.msEndThisRun = 0;
this.calcCycles();
return fSuccess;
}
/**
* calcSpeed(nCycles, msElapsed)
*
* @this {CPUPDP11}
* @param {number} nCycles
* @param {number} msElapsed
*/
calcSpeed(nCycles, msElapsed)
{
if (msElapsed) {
this.mhz = Math.round(nCycles / (msElapsed * 10)) / 100;
if (msElapsed >= 86400000) {
this.nTotalCycles = 0;
this.setSpeed(); // reset all counters once per day so that we never have to worry about overflow
}
}
}
/**
* calcStartTime()
*
* @this {CPUPDP11}
*/
calcStartTime()
{
if (this.nCyclesRecalc >= this.nCyclesPerSecond) {
this.calcCycles(true);
}
this.nCyclesThisRun = 0;
this.msStartThisRun = Component.getTime();
/*
* Try to detect situations where the browser may have throttled us, such as when the user switches
* to a different tab; in those situations, Chrome and Safari may restrict setTimeout() callbacks
* to roughly one per second.
*
* Another scenario: the user resizes the browser window. setTimeout() callbacks are not throttled,
* but there can still be enough of a lag between the callbacks that CPU speed will be noticeably
* erratic if we don't compensate for it here.
*
* We can detect throttling/lagging by verifying that msEndThisRun (which was set at the end of the
* previous run and includes any requested sleep time) is comparable to the current msStartThisRun;
* if the delta is significant, we compensate by bumping msStartRun forward by that delta.
*
* This shouldn't be triggered when the Debugger halts the CPU, because setSpeed() -- which is called
* whenever the CPU starts running again -- zeroes msEndThisRun.
*
* This also won't do anything about other internal delays; for example, Debugger message() calls.
* By the time the message() function has called yieldCPU(), the cost of the message has already been
* incurred, so it will be end up being charged against the instruction(s) that triggered it.
*
* TODO: Consider calling yieldCPU() sooner from message(), so that it can arrange for the msEndThisRun
* "snapshot" to occur sooner; it's unclear, however, whether that will really improve the CPU's ability
* to hit its target speed, since you would expect any instruction that displays a message to be an
* EXTREMELY slow instruction.
*/
if (this.msEndThisRun) {
var msDelta = this.msStartThisRun - this.msEndThisRun;
if (msDelta > this.msPerYield) {
if (MAXDEBUG) this.println("large time delay: " + msDelta + "ms");
this.msStartRun += msDelta;
/*
* Bumping msStartRun forward should NEVER cause it to exceed msStartThisRun; however, just
* in case, I make absolutely sure it cannot happen, since doing so could result in negative
* speed calculations.
*/
if (this.msStartRun > this.msStartThisRun) {
this.msStartRun = this.msStartThisRun;
}
}
}
}
/**
* calcRemainingTime()
*
* @this {CPUPDP11}
* @return {number}
*/
calcRemainingTime()
{
this.msEndThisRun = Component.getTime();
var msYield = this.msPerYield;
if (this.nCyclesThisRun) {
/*
* Normally, we would assume we executed a full quota of work over msPerYield, but since the CPU
* now has the option of calling yieldCPU(), that might not be true. If nCyclesThisRun is correct, then
* the ratio of nCyclesThisRun/nCyclesPerYield should represent the percentage of work we performed,
* and so applying that percentage to msPerYield should give us a better estimate of work vs. time.
*/
msYield = Math.round(msYield * this.nCyclesThisRun / this.nCyclesPerYield);
}
var msElapsedThisRun = this.msEndThisRun - this.msStartThisRun;
var msRemainsThisRun = msYield - msElapsedThisRun;
/*
* We could pass only "this run" results to calcSpeed():
*
* nCycles = this.nCyclesThisRun;
* msElapsed = msElapsedThisRun;
*
* but it seems preferable to use longer time periods and hopefully get a more accurate speed.
*
* Also, if msRemainsThisRun >= 0 && this.nCyclesMultiplier == 1, we could pass these results instead:
*
* nCycles = this.nCyclesThisRun;
* msElapsed = this.msPerYield;
*
* to insure that we display a smooth, constant N Mhz. But for now, I prefer seeing any fluctuations.
*/
var nCycles = this.nRunCycles;
var msElapsed = this.msEndThisRun - this.msStartRun;
if (MAXDEBUG && msRemainsThisRun < 0 && this.nCyclesMultiplier > 1) {
this.println("warning: updates @" + msElapsedThisRun + "ms (prefer " + Math.round(msYield) + "ms)");
}
this.calcSpeed(nCycles, msElapsed);
if (msRemainsThisRun < 0 || this.mhz < this.mhzTarget) {
/*
* Try "throwing out" the effects of large anomalies, by moving the overall run start time up;
* ordinarily, this should only happen when the someone is using an external Debugger or some other
* tool or feature that is interfering with our overall execution.
*/
if (msRemainsThisRun < -1000) {
this.msStartRun -= msRemainsThisRun;
}
/*
* If the last burst took MORE time than we allotted (ie, it's taking more than 1 second to simulate
* nCyclesPerSecond), all we can do is yield for as little time as possible (ie, 0ms) and hope that the
* simulation is at least usable.
*/
msRemainsThisRun = 0;
}
/*
* Last but not least, update nCyclesRecalc, so that when runCPU() starts up again and calls calcStartTime(),
* it'll be ready to decide if calcCycles() should be called again.
*/
this.nCyclesRecalc += this.nCyclesThisRun;
if (DEBUG && this.messageEnabled(MessagesPDP11.LOG) && msRemainsThisRun) {
this.log("calcRemainingTime: " + msRemainsThisRun + "ms to sleep after " + this.msEndThisRun + "ms");
}
this.msEndThisRun += msRemainsThisRun;
return msRemainsThisRun;
}
/**
* addTimer(callBack)
*
* Components that want to have timers that periodically fire after some number of milliseconds call
* addTimer() to create the timer, and then setTimer() every time they want to arm it. There is currently
* no removeTimer() because these are generally used for the entire lifetime of a component.
*
* Internally, each timer entry is a preallocated Array with two entries: a cycle countdown in element [0]
* and a callback function in element [1]. A timer is initially dormant; dormant timers have a countdown
* value of -1 (although any negative number will suffice) and active timers have a non-negative value.
*
* Why not use JavaScript's setTimeout() instead? Good question. For a good answer, see setTimer() below.
*
* TODO: Consider making the addTimer() and setTimer() interfaces more like the addIRQ() and setIRQ()
* interfaces (which return the underlying object instead of an array index) and maintaining a separate list
* of active timers, in order of highest to lowest cycle countdown values, as this could speed up
* getBurstCycles() and updateTimers() functions ever so slightly.
*
* @this {CPUPDP11}
* @param {function()} callBack
* @return {number} timer index
*/
addTimer(callBack)
{
var iTimer = this.aTimers.length;
this.aTimers.push([-1, callBack]);
return iTimer;
}
/**
* setTimer(iTimer, ms, fReset)
*
* Using the timer index from a previous addTimer() call, this sets that timer to fire after the
* specified number of milliseconds.
*
* This is preferred over JavaScript's setTimeout(), because all our timers are effectively paused when
* the CPU is paused (eg, when the Debugger halts execution). Moreover, setTimeout() handlers only run after
* runCPU() yields, which is far too granular for some components (eg, when the SerialPort tries to simulate
* interrupts at 9600 baud).
*
* Ideally, the only function that would use setTimeout() is runCPU(), while the rest of the components
* use setTimer(); however, due to legacy code (ie, code that predates these functions) and/or laziness,
* that may not be the case.
*
* @this {CPUPDP11}
* @param {number} iTimer
* @param {number} ms (converted into a cycle countdown internally)
* @param {boolean} [fReset] (true if the timer should be reset even if already armed)
* @return {number} (number of cycles used to arm timer, or -1 if error)
*/
setTimer(iTimer, ms, fReset)
{
var nCycles = -1;
if (iTimer >= 0 && iTimer < this.aTimers.length) {
if (fReset || this.aTimers[iTimer][0] < 0) {
nCycles = this.getMSCycles(ms);
/*
* We must now confront the following problem: if the CPU is currently executing a burst of cycles,
* the number of cycles it has executed in that burst so far must NOT be charged against the cycle
* timeout we're about to set. The simplest way to resolve that is to immediately call endBurst()
* and bias the cycle timeout by the number of cycles that the burst executed.
*/
if (this.flags.running) {
nCycles += this.endBurst();
}
this.aTimers[iTimer][0] = nCycles;
}
}
return nCycles;
}
/**
* getMSCycles(ms)
*
* @this {CPUPDP11}
* @param {number} ms
* @return {number} number of corresponding cycles
*/
getMSCycles(ms)
{
return ((this.nCyclesPerSecond * this.nCyclesMultiplier) / 1000 * ms)|0;
}
/**
* getBurstCycles(nCycles)
*
* Used by runCPU() to get min(nCycles,[timer cycle counts])
*
* @this {CPUPDP11}
* @param {number} nCycles (number of cycles about to execute)
* @return {number} (either nCycles or less if a timer needs to fire)
*/
getBurstCycles(nCycles)
{
for (var i = this.aTimers.length - 1; i >= 0; i--) {
var timer = this.aTimers[i];
if (timer[0] < 0) continue;
if (nCycles > timer[0]) {
nCycles = timer[0];
}
}
return nCycles;
}
/**
* saveTimers()
*
* @this {CPUPDP11}
* @return {Array.<number>}
*/
saveTimers()
{
var aTimerCycles = [];
for (var i = 0; i < this.aTimers.length; i++) {
var timer = this.aTimers[i];
aTimerCycles.push(timer[0]);
}
return aTimerCycles;
}
/**
* restoreTimers(aTimerCycles)
*
* @this {CPUPDP11}
* @param {Array.<number>} aTimerCycles
*/
restoreTimers(aTimerCycles)
{
for (var i = 0; i < this.aTimers.length && i < aTimerCycles.length; i++) {
var timer = this.aTimers[i];
timer[0] = aTimerCycles[i];
}
}
/**
* updateTimers(nCycles)
*
* Used by runCPU() to reduce all active timer countdown values by the number of cycles just executed;
* this is the function that actually "fires" any timer(s) whose countdown has reached (or dropped below)
* zero, invoking their callback function.
*
* @this {CPUPDP11}
* @param {number} nCycles (number of cycles actually executed)
*/
updateTimers(nCycles)
{
for (var i = this.aTimers.length - 1; i >= 0; i--) {
var timer = this.aTimers[i];
if (timer[0] < 0) continue;
timer[0] -= nCycles;
if (timer[0] <= 0) {
timer[0] = -1; // zero is technically an "active" value, so ensure the timer is dormant now
timer[1](); // safe to invoke the callback function now
}
}
}
/**
* endBurst(fReset)
*
* @this {CPUPDP11}
* @param {boolean} [fReset]
* @return {number} (number of cycles executed in the most recent burst)
*/
endBurst(fReset)
{
var nCycles = this.nBurstCycles -= this.nStepCycles;
/*
* In addition to zeroing nStepCycles, it's important that we also zero nSnapCycles, because if a CPU
* burst is being ended after nStepCycles has been "snapped" (because a certain opcode has an unusual timing
* calculation that must be based on a "snapped" cycle count rather the opcode's starting cycle count), we
* could inadvertently undo the endBurst() if the original "snapped" value was used to update nStepCycles.
*/
this.nStepCycles = this.nSnapCycles = 0;
if (fReset) this.nBurstCycles = 0;
return nCycles;
}
/**
* runCPU()
*
* @this {CPUPDP11}
*/
runCPU()
{
if (!this.flags.running) return;
/*
* calcStartTime() initializes the cycle counter and timestamp for this runCPU() invocation, and optionally
* recalculates the the maximum number of cycles for each burst if the nCyclesRecalc threshold has been reached.
*/
this.calcStartTime();
try {
do {
/*
* nCycles is how many cycles we WANT to run on each iteration of stepCPU(), and may be as
* HIGH as nCyclesPerYield, but it may be significantly less. getBurstCycles() will adjust
* nCycles downward if any CPU timers need to fire during the next burst.
*/
var nCycles = this.getBurstCycles(this.flags.checksum? 1 : this.nCyclesPerYield);
/*
* Execute the burst.
*/
try {
this.stepCPU(nCycles);
}
catch(exception) {
/*
* We assume that any numeric exception was explicitly thrown by the CPU to interrupt the
* current instruction (and by extension, the current burst, but not the current run). All
* other exceptions are re-thrown to the catch below, which will attempt a stack dump.
*/
if (typeof exception != "number") throw exception;
}
/*
* Terminate the burst, returning the number of cycles that stepCPU() actually ran.
*/
nCycles = this.endBurst(true);
/*
* Add nCycles to nCyclesThisRun, as well as nRunCycles (the cycle count since the CPU started).
*/
this.nCyclesThisRun += nCycles;
this.nRunCycles += nCycles;
this.updateChecksum(nCycles);
/*
* Update any/all timers, firing those whose cycle countdowns have reached (or dropped below) zero.
*/
this.updateTimers(nCycles);
this.nCyclesNextYield -= nCycles;
if (this.nCyclesNextYield <= 0) {
this.nCyclesNextYield += this.nCyclesPerYield;
if (++this.nYieldsSinceStatusUpdate >= CPUPDP11.YIELDS_PER_STATUS) {
this.updateDisplays();
this.nYieldsSinceStatusUpdate = 0;
}
break;
}
} while (this.flags.running);
}
catch (e) {
this.stopCPU();
if (this.cmp) this.cmp.stop(Component.getTime(), this.getCycles());
this.setError(e.stack || e.message);
return;
}
if (this.flags.running) setTimeout(this.onRunTimeout, this.calcRemainingTime());
}
/**
* startCPU(fUpdateFocus)
*
* For use by any component that wants to start the CPU.
*
* @param {boolean} [fUpdateFocus]
* @return {boolean}
*/
startCPU(fUpdateFocus)
{
if (this.isError()) {
return false;
}
if (this.flags.running) {
this.println(this.toString() + " busy");
return false;
}
/*
* setSpeed() without a speed parameter leaves the selected speed in place, but also resets the
* cycle counter and timestamp for the current series of runCPU() calls, calculates the maximum number
* of cycles for each burst based on the last known effective CPU speed, and resets the nCyclesRecalc
* threshold counter.
*/
this.setSpeed();
this.flags.running = true;
this.flags.starting = true;
var controlRun = this.bindings["run"];
if (controlRun) controlRun.textContent = "Halt";
if (this.cmp) {
if (fUpdateFocus) this.cmp.setFocus(true);
this.cmp.start(this.msStartRun, this.getCycles());
}
if (!this.dbg) this.status("Started");
setTimeout(this.onRunTimeout, 0);
return true;
}
/**
* stepCPU(nMinCycles)
*
* This will be implemented by the CPUStatePDP11 component.
*
* @this {CPUPDP11}
* @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored)
* @return {number} of cycles executed; 0 indicates that the last instruction was not executed
*/
stepCPU(nMinCycles)
{
return 0;
}
/**
* stopCPU(fComplete)
*
* For use by any component that wants to stop the CPU.
*
* This similar to yieldCPU(), but it doesn't need to zero nCyclesNextYield to break out of runCPU();
* it simply needs to clear fRunning (well, "simply" may be oversimplifying a bit....)
*
* @this {CPUPDP11}
* @param {boolean} [fComplete]
* @return {boolean} true if the CPU was stopped, false if it was already stopped
*/
stopCPU(fComplete)
{
var fStopped = false;
if (this.flags.running) {
this.endBurst();
this.addCycles(this.nRunCycles);
this.nRunCycles = 0;
this.flags.running = false;
var controlRun = this.bindings["run"];
if (controlRun) controlRun.textContent = "Run";
if (this.cmp) {
this.cmp.stop(Component.getTime(), this.getCycles());
}
fStopped = true;
if (!this.dbg) this.status("Stopped");
}
this.flags.complete = fComplete;
return fStopped;
}
/**
* yieldCPU()
*
* Similar to stopCPU() with regard to how it resets various cycle countdown values, but the CPU
* remains in a "running" state.
*
* @this {CPUPDP11}
*/
yieldCPU()
{
this.endBurst(); // this will break us out of stepCPU()
this.nCyclesNextYield = 0; // this will break us out of runCPU(), once we break out of stepCPU()
/*
* The Debugger calls yieldCPU() after every message() to ensure browser responsiveness, but it looks
* odd for those messages to show CPU state changes if the Control Panel, Video display, etc, does not,
* so I've added this call to try to keep things looking synchronized.
*/
this.updateDisplays();
}
}
/*
* Constants that control the frequency at which various updates should occur.
*
* These values do NOT control the simulation directly. Instead, they are used by
* calcCycles(), which uses the nCyclesPerSecond passed to the constructor as a starting
* point and computes the following variables:
*
* this.nCyclesPerYield: (this.nCyclesPerSecond / CPUPDP11.YIELDS_PER_SECOND)
*
* The above variables are also multiplied by any cycle multiplier in effect, via setSpeed(),
* and then they're used to initialize another set of variables for each runCPU() iteration:
*
* this.nCyclesNextYield: this.nCyclesPerYield
*/
CPUPDP11.YIELDS_PER_SECOND = 30; // just a gut feeling for the MINIMUM number of yields per second
CPUPDP11.YIELDS_PER_STATUS = 15; // every 15 yields (ie, twice per second), perform CPU status updates
CPUPDP11.BUTTONS = ["power", "reset"];
/**
* @copyright http://pcjs.org/modules/pdp11/lib/cpustate.js (C) Jeff Parsons 2012-2017
*/
/*
* Overview of Device Interrupt Support
*
* Originally, the CPU maintained a queue of requested interrupts. Entries in this queue recorded a device's
* priority, vector, and delay (ie, a number of instructions to execute before dispatching the interrupt). This
* queue would constantly grow and shrink as requests were issued and dispatched, and as long as there was something
* in the queue, the CPU was constantly examining it.
*
* Now we are trying something more efficient. First, for devices that require delays (like the SerialPort's receiver
* and transmitter buffer registers, which are supposed to "clock" the data in and out at a specific baud rate), the
* CPU offers timer services that will "fire" a callback after a specified delay, which are much more efficient than
* requiring the CPU to dive into an interrupt queue and decrement delay counts on every instruction.
*
* Second, devices that generate interrupts will allocate an IRQ object during initialization; we will no longer
* be creating and destroying interrupt event objects and inserting/deleting them in a constantly changing queue.
* Each IRQ contains properties that never change (eg, the vector and priority), along with a "next" pointer that's
* only used when the IRQ is active.
*
* When a device decides it's time to interrupt (either at the end of some I/O operation or when a timer has fired),
* it will simply set the IRQ, which basically means that the IRQ will be linked onto a list of active IRQs, in
* priority order, so that when the CPU is ready to acknowledge interrupts, it need only check the top of the active
* IRQ list.
*/
/**
* @typedef {{
* vector: number,
* priority: number,
* message: number,
* next: (IRQ|null)
* }}
*/
var IRQ;
class CPUStatePDP11 extends CPUPDP11 {
/**
* CPUStatePDP11(parmsCPU)
*
* The CPUStatePDP11 class uses the following (parmsCPU) properties:
*
* model: a number (eg, 1170) that should match one of the PDP11.MODEL_* values
* addrReset: reset address (default is 0)
*
* This extends the CPU class and passes any remaining parmsCPU properties to the CPU class
* constructor, along with a default speed (cycles per second) based on the specified
* (or default) CPU model number.
*
* After looking over the timings of PDP-11/70 instructions, nearly all of them appear
* to be multiples of 150ns. So that's what we'll consider a cycle. How many 150ns are
* in one second? Approximately 6666667. So by way of comparison to other PCjs machines,
* that makes the PDP-11 (or at least the PDP-11/70) look like a 6.67Mhz machine.
*
* I've started with the PDP-11/70, since that's what Paul Nankervis started with. When
* I go back and add support for earlier PDP-11 models (primarily by neutering functions
* that didn't exist), I will no doubt have to tweak some instruction cycle counts, too.
*
* Examples of operations that take 1 extra cycle (150ns): single and double operand byte
* instructions with an odd address (except MOV/MTPI/MTPD/JMP/JRS), ADD/SUB/BIC/BIS/MOVB/CMP/BIT
* instructions with src of R1-R7 and dst of R6-R7, RORB/ASRB with an odd address, and each
* shift of ASH/ASHC. As you can see, the rules are not simple.
*
* We're not simulating cache hardware, but our timings should be optimistic and assume 100%
* cache hits; for cache hits, each read cycle is 300ns. As for write cycles, they are always
* 750ns. My initial take on DEC's timings is that they are including the write time as part
* of the total EF (execute/fetch) time. So, for instructions that write to memory, it looks
* like we'll normally need to add 5 cycles (750/150) to the instruction's base time, but
* we'll need to keep an eye out for exceptions.
*
* @param {Object} parmsCPU
*/
constructor(parmsCPU)
{
var nCyclesDefault = 0;
var model = +parmsCPU['model'] || PDP11.MODEL_1170;
switch(model) {
case PDP11.MODEL_1170:
default:
nCyclesDefault = 6666667;
break;
}
/*
* ES6 ALERT: Classes cannot access "this" until all superclasses have been initialized as well.
*/
super(parmsCPU, nCyclesDefault);
this.model = model;
this.addrReset = +parmsCPU['addrReset'] || 0;
/*
* These properties will be initialized by initCPU()
*/
this.flagC = this.flagV = this.flagZ = this.flagN = 0;
this.regPSW = this.pswMode = 0;
this.pswTrap = 0;
this.regsGen = this.regsAlt = this.regsAltStack = [];
this.regsPAR = this.regsPDR = this.regsUniMap = this.regsControl = [];
this.opFlags = 0;
/*
* These properties will be initialized by initMMU()
*/
this.regMMR0 = this.regMMR1 = this.regMMR2 = this.regMMR3 = 0;
this.regErr = this.regMBR = this.regPIR = this.regSLR = 0;
this.mmuEnable = this.mmuLastMode = this.mmuLastPage = this.mmuMask = 0;
this.addrLast = this.opLast = this.addrInvalid = 0;
this.mapMMR3 = [4,2,0,1]; // map from mode to MMR3 I/D bit
/*
* Initialize processor operation to match the requested model.
*
* offRegSrc is a bias added to the register index calculated in readSrcWord() and readSrcByte(),
* and by default has no effect on the register index, UNLESS this is a PDP-11/20, in which case the
* bias is changed to 8 and we return one of the negative values you see above. Those negative values
* act as signals to writeDstWord() and writeDstByte(), effectively delaying evaluation of the register
* until then.
*/
this.offRegSrc = 0;
this.maskRegSrcByte = 0xff;
if (this.model <= PDP11.MODEL_1120) {
this.opDecode = PDP11.op1120.bind(this);
this.checkStackLimit = this.checkStackLimit1120;
this.offRegSrc = 8;
this.maskRegSrcByte = -1;
this.pswUsed = ~(PDP11.PSW.UNUSED | PDP11.PSW.REGSET | PDP11.PSW.PMODE | PDP11.PSW.CMODE) & 0xffff;
this.pswRegSet = 0;
} else {
this.opDecode = PDP11.op1140.bind(this);
this.checkStackLimit = this.checkStackLimit1140;
/*
* The alternate register set (REGSET) doesn't exist on the 11/20 or 11/40; it's available on the 11/45 and 11/70.
* Ditto for separate I/D spaces, SUPER mode, and the instructions MFPD, MTPD, and SPL.
*/
this.pswUsed = ~(PDP11.PSW.UNUSED | (this.model <= PDP11.MODEL_1140? PDP11.PSW.REGSET : 0)) & 0xffff;
this.pswRegSet = (this.model > PDP11.MODEL_1140? PDP11.PSW.REGSET : 0);
}
this.nDisableTraps = 0;
this.trapVector = this.trapReason = 0;
/** @type {IRQ|null} */
this.irqNext = null; // the head of the active IRQ list, in priority order
/** @type {Array.<IRQ>} */
this.aIRQs = []; // list of all IRQs, active or not (to be used for auto-configuration)
this.getByte = this.getByteDirect = this.getByteChecked;
this.getWord = this.getWordDirect = this.getWordChecked;
this.setByte = this.setByteDirect = this.setByteChecked;
this.setWord = this.setWordDirect = this.setWordChecked;
this.nReadBreaks = this.nWriteBreaks = 0;
this.addrDSpace = this.addrIOPage = 0;
this.getAddr = this.getVirtualAddrByMode;
this.readWord = this.readWordFromVirtual;
this.writeWord = this.writeWordToVirtual;
this.srcMode = this.srcReg = 0;
this.dstMode = this.dstReg = this.dstAddr = 0;
this.flags.complete = false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* Called once the Bus has been initialized.
*
* @this {CPUStatePDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUPDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
super.initBus(cmp, bus, cpu, dbg);
this.getByteDirect = bus.getByte.bind(bus);
this.getWordDirect = bus.getWord.bind(bus);
this.setByteDirect = bus.setByte.bind(bus);
this.setWordDirect = bus.setWord.bind(bus);
}
/**
* powerUp(data, fRepower)
*
* We hook the powerUp() notification only because it's our best opportunity to take care of any
* floating vector assignments.
*
* @this {CPUStatePDP11}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
var vectorFloating = 0o300;
for (var i = 0; i < this.aIRQs.length; i++) {
var irq = this.aIRQs[i];
if (irq.vector < 0) {
irq.vector = vectorFloating;
vectorFloating += 4;
}
}
return super.powerUp(data, fRepower);
}
/**
* reset()
*
* @this {CPUStatePDP11}
*/
reset()
{
this.status("Model " + this.model);
if (this.flags.running) this.stopCPU();
this.initCPU();
this.resetCycles();
this.clearError(); // clear any fatal error/exception that setError() may have flagged
super.reset();
}
/**
* initCPU()
*
* WARNING: It's tempting to call this function as early as the constructor() or initBus() calls, but
* but we actually need to wait until our reset() or restore() function is called by the powerUp() handler,
* ensuring that all device memory allocations have finished. Only then is it safe to make the first call
* to initCPU() -> initMMU() -> setMemoryAccess() -> Bus.setIOPageRange() and sync the Bus memory map with
* the CPU memory map.
*
* @this {CPUStatePDP11}
*/
initCPU()
{
/*
* TODO: Verify the initial state of all PDP-11 flags and registers (are they well-documented?)
*/
var f = 0xffff;
this.flagC = 0x10000; // PSW C bit
this.flagV = 0x8000; // PSW V bit
this.flagZ = f; // PSW Z bit (TODO: Why do we clear instead of set Z, like other flags?)
this.flagN = 0x8000; // PSW N bit
this.regPSW = 0x000f; // PSW other bits (TODO: What's the point of setting the flag bits here, too?)
this.regsGen = [ // General R0-R7
0, 0, 0, 0, 0, 0, 0, this.addrReset, -1, -2, -3, -4, -5, -6, -7, -8
];
this.regsAlt = [ // Alternate R0-R5
0, 0, 0, 0, 0, 0
];
this.regsAltStack = [ // Alternate R6 stack pointers (KERNEL, SUPER, UNUSED, USER)
0, 0, 0, 0
];
this.regsPAR = [ // memory management PAR registers by mode
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // KERNEL (8 KIPAR regs followed by 8 KDPAR regs)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // SUPER (8 SIPDR regs followed by 8 SDPDR regs)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // mode 2 (not used)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // USER (8 UIPDR regs followed by 8 UDPDR regs)
];
this.regsPDR = [ // memory management PDR registers by mode
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // KERNEL (8 KIPDR regs followed by 8 KDPDR regs)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // SUPER (8 SIPDR regs followed by 8 SDPDR regs)
[f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f], // mode 2 (not used)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // USER (8 UIPDR regs followed by 8 UDPDR regs)
];
this.regsUniMap = [ // 32 UNIBUS map registers
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
this.regsControl = [ // various control registers (177740-177756) we don't really care about
0, 0, 0, 0, 0, 0, 0, 0
];
this.pswMode = 0; // current memory management mode (see PDP11.MODE.KERNEL | SUPER | UNUSED | USER)
this.pswTrap = -1;
this.regMBR = 0;
/*
* opFlags contains various conditions that stepCPU() needs to be aware of.
*/
this.opFlags = 0;
/*
* srcMode and srcReg are set by SRCMODE decodes, and dstMode and dstReg are set for DSTMODE decodes,
* indicating to the opcode handlers the mode(s) and register(s) used as part of the current opcode, so
* that they can calculate the correct number of cycles. dstAddr is set for byte operations that also
* need to know the effective address for their cycle calculation.
*/
this.srcMode = this.srcReg = 0;
this.dstMode = this.dstReg = this.dstAddr = 0;
this.initMMU();
}
/**
* initMMU()
*
* Reset all registers required as part of a RESET instruction.
*
* TODO: Do we ever need to automatically clear regErr, or is it cleared manually?
*
* @this {CPUStatePDP11}
*/
initMMU()
{
this.regMMR0 = 0; // 177572
this.regMMR1 = 0; // 177574
this.regMMR2 = 0; // 177576
this.regMMR3 = 0; // 172516
this.regErr = 0; // 177766
this.regPIR = 0; // 177772
this.regSLR = 0xff; // 177774
this.mmuEnable = 0; // MMU enabled for PDP11.ACCESS.READ or PDP11.ACCESS.WRITE
this.mmuLastMode = 0;
this.mmuLastPage = 0;
this.mmuMask = 0x3ffff;
/*
* This is queried and displayed by the Panel when it's not displaying its own ADDRESS register
* (which takes precedence when, for example, you've manually halted the CPU and are independently
* examining the contents of other addresses).
*
* We initialize it to whatever the current PC is, because according to @paulnank's pdp11.js: "Reset
* displays next instruction address" and initMMU() is called on a RESET.
*/
this.addrLast = this.regsGen[7];
/*
* This stores the PC in the lower 16 bits, and any auto-incs or auto-decs from the last opcode in the
* upper 16 bits; the lower 16 bits are used to update MMR2, and the upper 16 bits are used to update MMR1.
* The upper bits are automatically zeroed at the start of every operation when the PC is copied to opLast.
*/
this.opLast = 0;
this.resetIRQs();
/*
* As initCPU() explains, we shouldn't be calling this function until well after initBus() has been
* called, but we still make absolutely sure we have Bus access.
*/
if (this.bus) {
this.setMemoryAccess();
this.addrInvalid = this.bus.getMemoryLimit(MemoryPDP11.TYPE.RAM);
}
}
/**
* getMMUState()
*
* Returns bit 0 set if 22-bit, bit 1 set if 18-bit, or bit 2 set if 16-bit; used by the Panel component.
*
* @this {CPUStatePDP11}
* @return {number}
*/
getMMUState()
{
return this.mmuEnable? ((this.regMMR3 & PDP11.MMR3.MMU_22BIT)? 1 : 2) : 4;
}
/**
* resetCPU()
*
* @this {CPUStatePDP11}
*/
resetCPU()
{
this.bus.reset();
this.initMMU();
}
/**
* setMemoryAccess()
*
* Define handlers and DSPACE setting appropriate for the current MMU mode, in order to eliminate unnecessary calls
* to mapVirtualToPhysical().
*
* @this {CPUStatePDP11}
*/
setMemoryAccess()
{
this.getByte = this.getByteDirect;
this.getWord = this.getWordDirect;
this.setByte = this.setByteDirect;
this.setWord = this.setWordDirect;
if (this.nReadBreaks) {
this.getByte = this.getByteChecked;
this.getWord = this.getWordChecked;
}
if (this.nWriteBreaks) {
this.setByte = this.setByteChecked;
this.setWord = this.setWordChecked;
}
if (this.mmuEnable) {
this.addrDSpace = PDP11.ACCESS.DSPACE;
this.addrIOPage = (this.regMMR3 & PDP11.MMR3.MMU_22BIT)? BusPDP11.IOPAGE_22BIT : BusPDP11.IOPAGE_18BIT;
this.getAddr = this.getVirtualAddrByMode;
this.readWord = this.nReadBreaks? this.readWordFromVirtualChecked : this.readWordFromVirtual;
this.writeWord = this.nWriteBreaks? this.writeWordToVirtualChecked : this.writeWordToVirtual;
this.bus.setIOPageRange((this.regMMR3 & PDP11.MMR3.MMU_22BIT)? 22 : 18);
} else {
this.addrDSpace = 0;
this.addrIOPage = BusPDP11.IOPAGE_16BIT;
this.getAddr = this.getPhysicalAddrByMode;
this.readWord = this.nReadBreaks? this.readWordFromPhysicalChecked : this.readWordFromPhysical;
this.writeWord = this.nWriteBreaks? this.writeWordToPhysicalChecked : this.writeWordToPhysical;
this.bus.setIOPageRange(16);
}
}
/**
* getMMR0()
*
* NOTE: It's OK to bypass this function if you're only interested in bits that always stored directly in MMR0.
*
* 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 MMR0
* nonr leng read trap unus unus ena mnt cmp -mode- i/d --page-- enable
*
* @this {CPUStatePDP11}
* @return {number}
*/
getMMR0()
{
var data = this.regMMR0;
if (!(data & PDP11.MMR0.ABORT)) {
data = (data & ~(PDP11.MMR0.UNUSED | PDP11.MMR0.PAGE | PDP11.MMR0.MODE)) | (this.mmuLastMode << 5) | (this.mmuLastPage << 1);
}
return data;
}
/**
* setMMR0()
*
* @this {CPUStatePDP11}
* @param {number} newMMR0
*/
setMMR0(newMMR0)
{
newMMR0 &= ~PDP11.MMR0.UNUSED;
if (this.regMMR0 != newMMR0) {
if (newMMR0 & PDP11.MMR0.ABORT) {
/*
* If updates to MMR0[1-7], MMR1, and MMR2 are being shut off (ie, MMR0.ABORT bits are transitioning
* from clear to set), then do one final sync with their real-time counterparts in opLast.
*/
if (!(this.regMMR0 & PDP11.MMR0.ABORT)) {
this.regMMR1 = (this.opLast >> 16) & 0xffff;
this.regMMR2 = this.opLast & 0xffff;
}
}
/*
* NOTE: We are not protecting the read-only state of the COMPLETED bit here; that's handled by writeMMR0().
*/
this.regMMR0 = newMMR0;
this.mmuLastMode = (newMMR0 & PDP11.MMR0.MODE) >> PDP11.MMR0.SHIFT.MODE;
this.mmuLastPage = (newMMR0 & PDP11.MMR0.PAGE) >> PDP11.MMR0.SHIFT.PAGE;
var mmuEnable = 0;
if (newMMR0 & (PDP11.MMR0.ENABLED | PDP11.MMR0.MAINT)) {
mmuEnable = PDP11.ACCESS.WRITE;
if (newMMR0 & PDP11.MMR0.ENABLED) mmuEnable |= PDP11.ACCESS.READ;
}
if (this.mmuEnable != mmuEnable) {
this.mmuEnable = mmuEnable;
this.setMemoryAccess();
}
}
}
/**
* getMMR1()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getMMR1()
{
/*
* If updates to MMR1 have not been shut off (ie, MMR0.ABORT bits are clear), then we are allowed
* to sync MMR1 with its real-time counterpart in opLast.
*
* UPDATE: Apparently, I was mistaken that this register would only be updated when the MMR0 ENABLED
* bit was set.
*
* if ((this.regMMR0 & (PDP11.MMR0.ABORT | PDP11.MMR0.ENABLED)) == PDP11.MMR0.ENABLED)
*/
if (!(this.regMMR0 & PDP11.MMR0.ABORT)) {
this.regMMR1 = (this.opLast >> 16) & 0xffff;
}
var result = this.regMMR1;
if (result & 0xff00) {
result = ((result << 8) | (result >> 8)) & 0xffff;
}
return result;
}
/**
* getMMR2()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getMMR2()
{
/*
* If updates to MMR2 have not been shut off (ie, MMR0.ABORT bits are clear), then we are allowed
* to sync MMR2 with its real-time counterpart in opLast.
*
* UPDATE: Apparently, I was mistaken that this register would only be updated when the MMR0 ENABLED
* bit was set.
*
* if ((this.regMMR0 & (PDP11.MMR0.ABORT | PDP11.MMR0.ENABLED)) == PDP11.MMR0.ENABLED)
*/
if (!(this.regMMR0 & PDP11.MMR0.ABORT)) {
this.regMMR2 = this.opLast & 0xffff;
}
return this.regMMR2;
}
/**
* getMMR3()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getMMR3()
{
return this.regMMR3;
}
/**
* setMMR3()
*
* @this {CPUStatePDP11}
* @param {number} newMMR3
*/
setMMR3(newMMR3)
{
/*
* Don't allow non-11/70 models to use 22-bit addressing or the UNIBUS map.
*/
if (this.model < PDP11.MODEL_1170) {
newMMR3 &= ~(PDP11.MMR3.MMU_22BIT | PDP11.MMR3.UNIBUS_MAP);
}
if (this.regMMR3 != newMMR3) {
this.regMMR3 = newMMR3;
this.mmuMask = (newMMR3 & PDP11.MMR3.MMU_22BIT)? BusPDP11.MASK_22BIT : BusPDP11.MASK_18BIT;
this.setMemoryAccess();
}
}
/**
* setReset(addr, fStart, bUnit, addrStack)
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {boolean} [fStart] (true if a "startable" image was just loaded, false if not)
* @param {number} [bUnit] (boot unit #)
* @param {number} [addrStack]
*/
setReset(addr, fStart, bUnit, addrStack)
{
this.addrReset = addr;
this.setPC(addr);
this.setPSW(0);
this.resetCPU();
if (fStart) {
this.regsGen[0] = bUnit || 0;
for (var i = 1; i <= 5; i++) this.regsGen[i] = 0;
this.regsGen[6] = addrStack || 0o2000;
if (!this.flags.powered) {
this.flags.autoStart = true;
}
else if (!this.flags.running) {
this.startCPU();
}
}
else {
if (this.dbg && this.flags.powered) {
/*
* TODO: Review the decision to always stop the CPU if the Debugger is loaded. Note that
* when stopCPU() stops a running CPU, the Debugger gets notified, so no need to notify it again.
*
* TODO: There are more serious problems to deal with if another component is slamming a new PC down
* the CPU's throat (presumably while also dropping some new code into RAM) while the CPU is running;
* we should probably force a complete reset, but for now, it's up to the user to hit the reset button
* themselves.
*/
if (!this.stopCPU() && !this.cmp.flags.reset) {
this.dbg.updateStatus();
this.cmp.updateDisplays(-1);
}
}
else if (fStart === false) {
this.stopCPU();
}
}
if (!this.isRunning() && this.panel) this.panel.stop();
}
/**
* getChecksum()
*
* TODO: Implement
*
* @this {CPUStatePDP11}
* @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code)
*/
getChecksum()
{
return 0;
}
/**
* save()
*
* @this {CPUStatePDP11}
* @return {Object|null}
*/
save()
{
var state = new State(this);
state.set(0, [
this.regsGen,
this.regsAlt,
this.regsAltStack,
this.regsPAR,
this.regsPDR,
this.regsUniMap,
this.regsControl,
this.regErr,
this.regMBR,
this.regPIR,
this.regSLR,
this.mmuLastMode,
this.mmuLastPage,
this.addrLast,
this.opFlags,
this.opLast,
this.pswTrap,
this.trapReason,
this.trapVector,
this.addrReset
]);
state.set(1, [this.getPSW(),this.getMMR0(),this.getMMR1(),this.getMMR2(),this.getMMR3()]);
state.set(2, [this.nTotalCycles, this.getSpeed(), this.flags.autoStart]);
state.set(3, this.saveIRQs());
state.set(4, this.saveTimers());
return state.data();
}
/**
* restore(data)
*
* @this {CPUStatePDP11}
* @param {Object} data
* @return {boolean} true if restore successful, false if not
*/
restore(data)
{
/*
* ES6 ALERT: A handy destructuring assignment, which makes it easy to perform the inverse
* of what save() does when it collects a bunch of object properties into an array.
*/
[
this.regsGen,
this.regsAlt,
this.regsAltStack,
this.regsPAR,
this.regsPDR,
this.regsUniMap,
this.regsControl,
this.regErr,
this.regMBR,
this.regPIR,
this.regSLR,
this.mmuLastMode,
this.mmuLastPage,
this.addrLast,
this.opFlags,
this.opLast,
this.pswTrap,
this.trapReason,
this.trapVector,
this.addrReset
] = data[0];
var a = data[1];
this.setPSW(a[0]);
this.setMMR0(a[1]);
this.regMMR1 = a[2];
this.regMMR2 = a[3];
this.setMMR3(a[4]);
a = data[2];
this.nTotalCycles = a[0];
this.setSpeed(a[1]);
this.flags.autoStart = a[2];
this.restoreIRQs(data[3]);
this.restoreTimers(data[4]);
return true;
}
/**
* clearCF()
*
* @this {CPUStatePDP11}
*/
clearCF()
{
this.flagC = 0;
}
/**
* getCF()
*
* @this {CPUStatePDP11}
* @return {number} 0 or PDP11.PSW.CF
*/
getCF()
{
return (this.flagC & 0x10000)? PDP11.PSW.CF: 0;
}
/**
* setCF()
*
* @this {CPUStatePDP11}
*/
setCF()
{
this.flagC = 0x10000;
}
/**
* clearVF()
*
* @this {CPUStatePDP11}
*/
clearVF()
{
this.flagV = 0;
}
/**
* getVF()
*
* @this {CPUStatePDP11}
* @return {number} 0 or PDP11.PSW.VF
*/
getVF()
{
return (this.flagV & 0x8000)? PDP11.PSW.VF: 0;
}
/**
* setVF()
*
* @this {CPUStatePDP11}
*/
setVF()
{
this.flagV = 0x8000;
}
/**
* clearZF()
*
* @this {CPUStatePDP11}
*/
clearZF()
{
this.flagZ = 1;
}
/**
* getZF()
*
* @this {CPUStatePDP11}
* @return {number} 0 or PDP11.PSW.ZF
*/
getZF()
{
return (this.flagZ & 0xffff)? 0 : PDP11.PSW.ZF;
}
/**
* setZF()
*
* @this {CPUStatePDP11}
*/
setZF()
{
this.flagZ = 0;
}
/**
* clearNF()
*
* @this {CPUStatePDP11}
*/
clearNF()
{
this.flagN = 0;
}
/**
* getNF()
*
* @this {CPUStatePDP11}
* @return {number} 0 or PDP11.PSW.NF
*/
getNF()
{
return (this.flagN & 0x8000)? PDP11.PSW.NF : 0;
}
/**
* setNF()
*
* @this {CPUStatePDP11}
*/
setNF()
{
this.flagN = 0x8000;
}
/**
* getOpcode()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getOpcode()
{
var pc = this.opLast = this.regsGen[PDP11.REG.PC];
/*
* If PC is unaligned, a BUS trap will be generated, and because it will generate an
* exception, the next line (the equivalent of advancePC(2)) will not be executed, ensuring that
* original unaligned PC will be pushed onto the stack by trap().
*/
var opCode = this.readWord(pc);
this.regsGen[PDP11.REG.PC] = (pc + 2) & 0xffff;
return opCode;
}
/**
* advancePC(off)
*
* NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime.
*
* @this {CPUStatePDP11}
* @param {number} off
* @return {number} (original PC)
*/
advancePC(off)
{
var pc = this.regsGen[PDP11.REG.PC];
this.regsGen[PDP11.REG.PC] = (pc + off) & 0xffff;
return pc;
}
/**
* branch(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @param {boolean|number} condition
*/
branch(opCode, condition)
{
if (condition) {
var off = ((opCode << 24) >> 23);
if (DEBUG && DEBUGGER && this.dbg && off == -2) {
this.dbg.stopInstruction("branch to self");
}
this.setPC(this.getPC() + off);
this.nStepCycles -= 2;
}
this.nStepCycles -= (2 + 1);
}
/**
* getPC()
*
* NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime.
*
* @this {CPUStatePDP11}
* @return {number}
*/
getPC()
{
return this.regsGen[PDP11.REG.PC];
}
/**
* getLastAddr()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getLastAddr()
{
return this.addrLast;
}
/**
* getLastPC()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getLastPC()
{
return this.opLast & 0xffff;
}
/**
* setPC()
*
* NOTE: Unlike other PCjs emulators, such as PCx86, where all PC updates MUST go through the setPC()
* function, this function is nothing more than a convenience, because in the PDP-11, the PC can be loaded
* like any other general register. We fully expect this function to be inlined at runtime.
*
* @this {CPUStatePDP11}
* @param {number} addr
*/
setPC(addr)
{
this.regsGen[PDP11.REG.PC] = addr & 0xffff;
}
/**
* getSP()
*
* NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime.
*
* @this {CPUStatePDP11}
* @return {number}
*/
getSP()
{
return this.regsGen[PDP11.REG.SP];
}
/**
* setSP()
*
* NOTE: Unlike other PCjs emulators, such as PCx86, where all SP updates MUST go through the setSP()
* function, this function is nothing more than a convenience, because in the PDP-11, the PC can be loaded
* like any other general register. We fully expect this function to be inlined at runtime.
*
* @this {CPUStatePDP11}
* @param {number} addr
*/
setSP(addr)
{
this.regsGen[PDP11.REG.SP] = addr & 0xffff;
}
/**
* addIRQ(vector, priority, message)
*
* @this {CPUStatePDP11}
* @param {number} vector (-1 for floating vector)
* @param {number} priority
* @param {number} [message]
* @return {IRQ}
*/
addIRQ(vector, priority, message)
{
var irq = {vector: vector, priority: priority, message: message || 0, name: PDP11.VECTORS[vector], next: null};
this.aIRQs.push(irq);
return irq;
}
/**
* insertIRQ(irq)
*
* @this {CPUStatePDP11}
* @param {IRQ} irq
*/
insertIRQ(irq)
{
if (irq != this.irqNext) {
var irqPrev = this.irqNext;
if (!irqPrev || irqPrev.priority <= irq.priority) {
irq.next = irqPrev;
this.irqNext = irq;
} else {
do {
var irqNext = irqPrev.next;
if (!irqNext || irqNext.priority <= irq.priority) {
irq.next = irqNext;
irqPrev.next = irq;
break;
}
irqPrev = irqNext;
} while (irqPrev);
}
}
/*
* See the writeXCSR() function for an explanation of why signalling an IRQ hardware interrupt
* should be done using IRQ_DELAY rather than setting IRQ directly.
*/
this.opFlags |= PDP11.OPFLAG.IRQ_DELAY;
}
/**
* removeIRQ(irq)
*
* @this {CPUStatePDP11}
* @param {IRQ} irq
*/
removeIRQ(irq)
{
var irqPrev = this.irqNext;
if (irqPrev == irq) {
this.irqNext = irq.next;
} else {
while (irqPrev) {
var irqNext = irqPrev.next;
if (irqNext == irq) {
irqPrev.next = irqNext.next;
break;
}
irqPrev = irqNext;
}
}
/*
* We could also set irq.next to null now, but strictly speaking, that shouldn't be necessary.
*
* Last but not least, if there's still an IRQ on the active IRQ list, we need to make sure IRQ_DELAY
* is still set.
*/
if (this.irqNext) {
this.opFlags |= PDP11.OPFLAG.IRQ_DELAY;
}
}
/**
* setIRQ(irq)
*
* @this {CPUStatePDP11}
* @param {IRQ|null} irq
*/
setIRQ(irq)
{
if (irq) {
this.insertIRQ(irq);
if (irq.message && this.messageEnabled(irq.message | MessagesPDP11.INT)) {
this.printMessage("setIRQ(vector=" + Str.toOct(irq.vector) + ",priority=" + irq.priority + ")", true, true);
}
}
}
/**
* clearIRQ(irq)
*
* @this {CPUStatePDP11}
* @param {IRQ|null} irq
*/
clearIRQ(irq)
{
if (irq) {
this.removeIRQ(irq);
if (irq.message && this.messageEnabled(irq.message | MessagesPDP11.INT)) {
this.printMessage("clearIRQ(vector=" + Str.toOct(irq.vector) + ",priority=" + irq.priority + ")", true, true);
}
}
}
/**
* findIRQ(vector)
*
* @this {CPUStatePDP11}
* @param {number} vector
* @return {IRQ|null}
*/
findIRQ(vector)
{
for (var i = 0; i < this.aIRQs.length; i++) {
var irq = this.aIRQs[i];
if (irq.vector === vector) return irq;
}
return null;
}
/**
* checkIRQs(priority)
*
* @this {CPUStatePDP11}
* @param {number} priority
* @return {IRQ|null}
*/
checkIRQs(priority)
{
return (this.irqNext && this.irqNext.priority > priority)? this.irqNext : null;
}
/**
* resetIRQs(priority)
*
* @this {CPUStatePDP11}
*/
resetIRQs()
{
this.irqNext = null;
}
/**
* saveIRQs()
*
* @this {CPUStatePDP11}
* @return {Array.<number>}
*/
saveIRQs()
{
var aIRQVectors = [];
var irq = this.irqNext;
while (irq) {
aIRQVectors.push(irq.vector);
irq = irq.next;
}
return aIRQVectors;
}
/**
* restoreIRQs(aIRQVectors)
*
* @this {CPUStatePDP11}
* @param {Array.<number>} aIRQVectors
*/
restoreIRQs(aIRQVectors)
{
for (var i = aIRQVectors.length - 1; i >= 0; i--) {
var irq = this.findIRQ(aIRQVectors[i]);
if (irq) {
irq.next = this.irqNext;
this.irqNext = irq;
}
}
}
/**
* checkInterrupts()
*
* @this {CPUStatePDP11}
* @return {boolean} true if an interrupt was dispatched, false if not
*/
checkInterrupts()
{
var fInterrupt = false;
if (this.opFlags & PDP11.OPFLAG.IRQ) {
var vector = PDP11.TRAP.PIRQ;
var priority = (this.regPIR & PDP11.PSW.PRI) >> PDP11.PSW.SHIFT.PRI;
var irq = this.checkIRQs(priority);
if (irq) {
vector = irq.vector;
priority = irq.priority;
}
if (this.dispatchInterrupt(vector, priority)) {
if (irq) this.removeIRQ(irq);
fInterrupt = true;
}
if (!this.irqNext && !this.regPIR) {
this.opFlags &= ~PDP11.OPFLAG.IRQ;
}
}
else if (this.opFlags & PDP11.OPFLAG.IRQ_DELAY) {
/*
* We know that IRQ (bit 2) is clear, so since IRQ_DELAY (bit 0) is set, incrementing opFlags
* will eventually transform IRQ_DELAY into IRQ, without affecting any other (higher) bits.
*/
this.opFlags++;
}
return fInterrupt;
}
/**
* dispatchInterrupt(vector, priority)
*
* TODO: The process of dispatching an interrupt MUST cost some cycles; either trap() needs to assess
* that cost, or we do.
*
* @this {CPUStatePDP11}
* @param {number} vector
* @param {number} priority
* @return {boolean} (true if dispatched, false if not)
*/
dispatchInterrupt(vector, priority)
{
var priorityCPU = (this.regPSW & PDP11.PSW.PRI) >> PDP11.PSW.SHIFT.PRI;
if (priority > priorityCPU) {
if (this.opFlags & PDP11.OPFLAG.WAIT) {
this.advancePC(2);
this.opFlags &= ~PDP11.OPFLAG.WAIT;
}
this.trap(vector, 0, PDP11.REASON.INTERRUPT);
return true;
}
return false;
}
/**
* checkTraps()
*
* NOTE: The following code processes these "deferred" traps in priority order. Unfortunately, that
* order seems to have changed since the 11/20. For reference, here's the priority list for the 11/20:
*
* 1. Bus Errors
* 2. Instruction Traps
* 3. Trace Trap
* 4. Stack Overflow Trap
* 5. Power Failure Trap
*
* and for the 11/70:
*
* 1. HALT (Instruction, Switch, or Command)
* 2. MMU Faults
* 3. Parity Errors
* 4. Bus Errors (including stack overflow traps?)
* 5. Floating Point Traps
* 6. TRAP Instruction
* 7. TRACE Trap
* 8. OVFL Trap
* 9. Power Fail Trap
* 10. Console Bus Request (Front Panel Operation)
* 11. PIR 7, BR 7, PIR 6, BR 6, PIR 5, BR 5, PIR 4, BR 4, PIR 3, BR 3, PIR 2, PIR 1
* 12. WAIT Loop
*
* TODO: Determine 1) if the 11/20 Handbook was wrong, or 2) if the 11/70 really has different priorities.
*
* Also, as the PDP-11/20 Handbook (1971), p.100, notes:
*
* If a bus error is caused by the trap process handling instruction traps, trace traps, stack overflow
* traps, or a previous bus error, the processor is halted.
*
* If a stack overflow is caused by the trap process in handling bus errors, instruction traps, or trace traps,
* the process is completed and then the stack overflow trap is sprung.
*
* TODO: Based on the above notes, we should probably be halting the CPU when a bus error occurs during a trap.
*
* @this {CPUStatePDP11}
* @return {boolean} (true if dispatched, false if not)
*/
checkTraps()
{
if (this.opFlags & PDP11.OPFLAG.TRAP_MMU) {
this.trap(PDP11.TRAP.MMU, PDP11.OPFLAG.TRAP_MMU, PDP11.REASON.FAULT);
return true;
}
if (this.opFlags & PDP11.OPFLAG.TRAP_SP) {
this.trap(PDP11.TRAP.BUS, PDP11.OPFLAG.TRAP_SP, PDP11.REASON.YELLOW);
return true;
}
if (this.opFlags & PDP11.OPFLAG.TRAP_TF) {
this.trap(PDP11.TRAP.BPT, PDP11.OPFLAG.TRAP_TF, PDP11.REASON.TRACE);
return true;
}
return false;
}
/**
* isWaiting()
*
* @this {CPUStatePDP11}
* @return {boolean} (true if OPFLAG.WAIT is set, false otherwise)
*/
isWaiting()
{
return !!(this.opFlags & PDP11.OPFLAG.WAIT);
}
/**
* getPSW()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getPSW()
{
var mask = PDP11.PSW.CMODE | PDP11.PSW.PMODE | PDP11.PSW.REGSET | PDP11.PSW.PRI | PDP11.PSW.TF;
return this.regPSW = (this.regPSW & mask) | this.getNF() | this.getZF() | this.getVF() | this.getCF();
}
/**
* setPSW(newPSW)
*
* This updates the CPU Processor Status Word. The PSW should generally be written through
* this routine so that changes can be tracked properly, for example the correct register set,
* the current memory management mode, etc. An exception is SPL which writes the priority directly.
* Note that that N, Z, V, and C flags are actually stored separately for performance reasons.
*
* PSW 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
* CMODE PMODE RS -------- PRIORITY T N Z V C
*
* @this {CPUStatePDP11}
* @param {number} newPSW
*/
setPSW(newPSW)
{
newPSW &= this.pswUsed;
this.flagN = newPSW << 12;
this.flagZ = (~newPSW) & 4;
this.flagV = newPSW << 14;
this.flagC = newPSW << 16;
if ((newPSW ^ this.regPSW) & this.pswRegSet) {
/*
* Swap register sets
*/
for (var i = this.regsAlt.length; --i >= 0;) {
var tmp = this.regsGen[i];
this.regsGen[i] = this.regsAlt[i];
this.regsAlt[i] = tmp;
}
}
this.pswMode = (newPSW >> PDP11.PSW.SHIFT.CMODE) & PDP11.MODE.MASK;
var oldMode = (this.regPSW >> PDP11.PSW.SHIFT.CMODE) & PDP11.MODE.MASK;
if (this.pswMode != oldMode) {
/*
* Swap stack pointers
*/
this.regsAltStack[oldMode] = this.regsGen[6];
this.regsGen[6] = this.regsAltStack[this.pswMode];
}
this.regPSW = newPSW;
/*
* Trigger a call to checkInterrupts(), just in case. If there's an active IRQ, then setting
* OPFLAG.IRQ is a no-brainer, but even if not, we set IRQ_DELAY in case the priority was lowered
* enough to permit a programmed interrupt (via regPIR).
*/
this.opFlags &= ~PDP11.OPFLAG.IRQ;
this.opFlags |= (this.irqNext? PDP11.OPFLAG.IRQ : PDP11.OPFLAG.IRQ_DELAY);
}
/**
* getSLR()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getSLR()
{
return this.regSLR & 0xff00;
}
/**
* setSLR(newSL)
*
* @this {CPUStatePDP11}
* @param {number} newSLR
*/
setSLR(newSLR)
{
this.regSLR = newSLR | 0xff;
}
/**
* getPIR()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getPIR()
{
return this.regPIR;
}
/**
* setPIR(newPIR)
*
* @this {CPUStatePDP11}
* @param {number} newPIR
*/
setPIR(newPIR)
{
newPIR &= PDP11.PIR.BITS;
if (newPIR) {
var bits = newPIR >> PDP11.PIR.SHIFT.BITS;
do {
newPIR += PDP11.PIR.PIA_INC;
} while (bits >>= 1);
this.opFlags |= PDP11.OPFLAG.IRQ_DELAY;
}
this.regPIR = newPIR;
}
/**
* updateNZVFlags(result)
*
* NOTE: Only N and Z are updated based on the result; V is zeroed, C is unchanged.
*
* @this {CPUStatePDP11}
* @param {number} result
*/
updateNZVFlags(result)
{
this.flagN = this.flagZ = result;
this.flagV = 0;
}
/**
* updateNZVCFlags(result)
*
* NOTE: Only N and Z are updated based on the result; both V and C are simply zeroed.
*
* @this {CPUStatePDP11}
* @param {number} result
*/
updateNZVCFlags(result)
{
this.flagN = this.flagZ = result;
this.flagV = this.flagC = 0;
}
/**
* updateAllFlags(result, overflow)
*
* NOTE: The V flag is simply zeroed, unless a specific value is provided (eg, by NEG).
*
* @this {CPUStatePDP11}
* @param {number} result
* @param {number} [overflow]
*/
updateAllFlags(result, overflow)
{
this.flagN = this.flagZ = this.flagC = result;
this.flagV = overflow || 0;
}
/**
* updateAddFlags(result, src, dst)
*
* @this {CPUStatePDP11}
* @param {number} result (dst + src)
* @param {number} src
* @param {number} dst
*/
updateAddFlags(result, src, dst)
{
this.flagN = this.flagZ = this.flagC = result;
this.flagV = (src ^ result) & (dst ^ result);
}
/**
* updateDecFlags(result, dst)
*
* NOTE: We could have used updateSubFlags() if not for the fact that the C flag must be preserved.
*
* @this {CPUStatePDP11}
* @param {number} result (dst - src, where src is an implied 1)
* @param {number} dst
*/
updateDecFlags(result, dst)
{
this.flagN = this.flagZ = result;
/*
* Because src is always 1 (with a zero sign bit), it can be optimized out of this calculation.
*/
this.flagV = (/* src ^ */ dst) & (dst ^ result);
}
/**
* updateIncFlags(result, dst)
*
* NOTE: We could have used updateAddFlags() if not for the fact that the C flag must be preserved.
*
* @this {CPUStatePDP11}
* @param {number} result (dst + src, where src is an implied 1)
* @param {number} dst
*/
updateIncFlags(result, dst)
{
this.flagN = this.flagZ = result;
/*
* Because src is always 1 (with a zero sign bit), it can be optimized out of this calculation.
*/
this.flagV = (/* src ^ */ result) & (dst ^ result);
}
/**
* updateMulFlags(result)
*
* @this {CPUStatePDP11}
* @param {number} result
*/
updateMulFlags(result)
{
this.flagN = result >> 16;
this.flagZ = this.flagN | result;
this.flagV = 0;
this.flagC = (result < -32768 || result > 32767)? 0x10000 : 0;
}
/**
* updateShiftFlags(result)
*
* @this {CPUStatePDP11}
* @param {number} result
*/
updateShiftFlags(result)
{
this.flagN = this.flagZ = this.flagC = result;
this.flagV = this.flagN ^ (this.flagC >> 1);
}
/**
* updateSubFlags(result, src, dst)
*
* NOTE: CMP operations calculate (src - dst) rather than (dst - src), so when they call updateSubFlags(),
* they must reverse the order of the src and dst parameters.
*
* @this {CPUStatePDP11}
* @param {number} result (dst - src)
* @param {number} src
* @param {number} dst
*/
updateSubFlags(result, src, dst)
{
this.flagN = this.flagZ = this.flagC = result;
this.flagV = (src ^ dst) & (dst ^ result);
}
/**
* trap(vector, flag, reason)
*
* trap() handles all the trap/abort functions. It reads the trap vector from kernel
* D space, changes mode to reflect the new PSW and PC, and then pushes the old PSW and
* PC onto the new mode stack.
*
* @this {CPUStatePDP11}
* @param {number} vector
* @param {number} flag
* @param {number} [reason] (for diagnostic purposes only)
*/
trap(vector, flag, reason)
{
if (DEBUG && this.dbg) {
if (this.messageEnabled(MessagesPDP11.TRAP)) {
var sReason = reason < 0? PDP11.REASONS[-reason] : this.dbg.toStrBase(reason);
this.printMessage("trap to vector " + this.dbg.toStrBase(vector, 8) + " (" + sReason + ")", MessagesPDP11.TRAP, true);
}
}
if (this.nDisableTraps) return;
if (this.pswTrap < 0) {
this.pswTrap = this.getPSW();
} else if (!this.pswMode) {
reason = PDP11.REASON.RED; // double-fault (nested trap) forces a RED condition
}
if (reason == PDP11.REASON.RED) {
if (this.opFlags & PDP11.OPFLAG.TRAP_RED) {
reason = PDP11.REASON.PANIC;
}
this.opFlags |= PDP11.OPFLAG.TRAP_RED;
/*
* The next two lines used to be deferred until after the setPSW() below, but
* I'm not seeing any dependencies on these registers, so I'm consolidating the code.
*/
this.regErr |= PDP11.CPUERR.RED;
this.regsGen[6] = vector = 4;
}
if (reason != PDP11.REASON.PANIC) {
/*
* NOTE: Pre-setting the auto-dec values for MMR1 to 0xF6F6 is a work-around for an "EKBEE1"
* diagnostic (PC 056710), which tests what happens when a misaligned read triggers a BUS trap,
* and that trap then triggers an MMU trap during the first pushWord() below.
*
* One would think it would be fine to zero those bits by setting opLast to vector alone,
* and then letting each of the pushWord() calls below shift their own 0xF6 auto-dec value into
* opLast. When the first pushWord() triggers an MMU trap, we obviously won't get to the second
* pushWord(), yet the diagnostic expects TWO auto-decs to be recorded. I'm puzzled why the
* hardware apparently indicates TWO auto-decs, if SP wasn't actually decremented twice, but who
* am I to judge.
*/
this.opLast = vector | 0xf6f60000;
/*
* Read from kernel D space
*/
this.pswMode = 0;
var newPC = this.readWord(vector | this.addrDSpace);
var newPSW = this.readWord(((vector + 2) & 0xffff) | this.addrDSpace);
/*
* Set new PSW with previous mode
*/
this.setPSW((newPSW & ~PDP11.PSW.PMODE) | ((this.pswTrap >> 2) & PDP11.PSW.PMODE));
this.pushWord(this.pswTrap);
this.pushWord(this.regsGen[7]);
this.setPC(newPC);
}
/*
* TODO: Determine the appropriate number of cycles for traps; all I've done for now is move the
* cycle charge from opTrap() to here, and reduced the amount the other opcode handlers that call
* trap() charge by a corresponding amount (5).
*/
this.nStepCycles -= (4 + 1);
/*
* DEC's "TRAP TEST" (MAINDEC-11-D0NA-PB) triggers a RESERVED trap with an invalid opcode and the
* stack deliberately set too low, and expects the stack overflow trap to be "sprung" immediately
* afterward, so we only want to "lose interest" in the TRAP flag(s) that were set on entry, not ALL
* of them.
*
* this.opFlags &= ~PDP11.OPFLAG.TRAP_MASK; // lose interest in traps after an abort
*
* Well, OK, we're also supposed to "lose interest" in the TF flag, too; otherwise, DEC tests fail.
*
* Finally, setPSW() likes to always set IRQ, to force a check of hardware interrupts prior to
* the next instruction, just in case the PSW priority was lowered. However, there are "TRAP TEST"
* tests like this one:
*
* 005640: 012706 007700 MOV #7700,SP
* 005644: 012767 000340 172124 MOV #340,177776
* 005652: 012767 000100 171704 MOV #100,177564
* 005660: 012767 005712 172146 MOV #5712,000034 ; set TRAP vector (its PSW is already zero)
* 005666: 012767 005714 172170 MOV #5714,000064 ; set hardware interrupt vector (its PSW is already zero)
* 005674: 012767 005716 172116 MOV #5716,000020 ; set IOT vector
* 005702: 012767 000340 172112 MOV #340,000022 ; set IOT PSW
* 005710: 104400 TRAP 000
* 005712: 000004 IOT
* 005714: 000000 HALT
*
* where, after "TRAP 000" has executed, a hardware interrupt will be acknowledged, and instead of
* executing the IOT, we'll execute the HALT and fail the test. We avoid that by relying on the same
* trick that the SPL instruction uses: setting IRQ_DELAY instead of IRQ, which effectively delays
* IRQ detection for one instruction, which is just long enough to allow the diagnostic to pass.
*/
this.opFlags &= ~(flag | PDP11.OPFLAG.TRAP_TF | PDP11.OPFLAG.IRQ_MASK);
this.opFlags |= PDP11.OPFLAG.IRQ_DELAY | PDP11.OPFLAG.TRAP_LAST;
this.pswTrap = -1; // reset flag that we have a trap within a trap
/*
* These next properties (in conjunction with setting PDP11.OPFLAG.TRAP_LAST) are purely an aid for the Debugger;
* see getTrapStatus().
*/
this.trapReason = reason;
this.trapVector = vector;
if (reason == PDP11.REASON.PANIC) {
this.stopCPU();
}
if (reason >= PDP11.REASON.RED) throw vector;
}
/**
* trapReturn()
*
* @this {CPUStatePDP11}
*/
trapReturn()
{
/*
* This code used to defer updating regsGen[6] (SP) until after BOTH words had been popped, which seems
* safer, but if we're going to do pushes in trap(), then I see no reason to avoid doing pops in trapReturn().
*/
var addr = this.popWord();
var newPSW = this.popWord();
if (this.regPSW & PDP11.PSW.CMODE) {
/*
* Keep SPL and allow lower only for modes and register set.
*
* TODO: Review, because it seems a bit odd to only CLEAR the PRI bits in the new PSW, and then to OR in
* CMODE, PMODE, and REGSET bits from the current PSW.
*/
newPSW = (newPSW & ~PDP11.PSW.PRI) | (this.regPSW & (PDP11.PSW.PRI | PDP11.PSW.REGSET | PDP11.PSW.PMODE | PDP11.PSW.CMODE));
}
this.setPC(addr);
this.setPSW(newPSW);
this.opFlags &= ~PDP11.OPFLAG.TRAP_TF;
}
/**
* getTrapStatus()
*
* @this {CPUStatePDP11}
* @return {number}
*/
getTrapStatus()
{
return (this.opFlags & PDP11.OPFLAG.TRAP_LAST)? (this.trapVector | this.trapReason << 8) : 0;
}
/**
* mapUnibus(addr)
*
* Used to convert 18-bit addresses to 22-bit addresses. Since mapUnibus() only looks at the low 18 bits of addr,
* there's no need to mask addr first. Note that if bits 13-17 are all set, then the 18-bit address points to the
* top 8Kb of its 256Kb range, and mapUnibus() will return addr unchanged, since it should already be pointing to
* the top 8Kb of the 4Mb 22-bit range.
*
* Also, when bits 18-21 of addr are ALL set (which callers check using addr >= BusPDP11.UNIBUS_22BIT aka 0x3C0000),
* then we have a 22-bit address pointing to the top 256Kb range, so if the UNIBUS relocation map is enabled, we again
* pass the lower 18 bits of that address through the map.
*
* From the PDP-11/70 Handbook:
*
* On the 11/44 and 11/70, there are a total of 31 mapping registers for address relocation. Each register is
* composed of a double 16-bit PDP-11 word (in consecutive locations) that holds the 22-bit base address. These
* registers have UNIBUS addresses in the range 770200 to 770372.
*
* If the UNIBUS map relocation is not enabled, an incoming 18-bit UNIBUS address has 4 leading zeroes added for
* referencing a 22-bit physical address. The lower 18 bits are the same. No relocation is performed.
*
* If UNIBUS map relocation is enabled, the five high order bits of the UNIBUS address are used to select one of the
* 31 mapping registers. The low-order 13 bits of the incoming address are used as an offset from the base address
* contained in the 22-bit mapping register. To form the physical address, the 13 low-order bits of the UNIBUS
* address are added to 22 bits of the selected mapping register to produce the 22-bit physical address. The lowest
* order bit of all mapping registers is always a zero, since relocation is always on word boundaries.
*
* Sadly, because these mappings occur at a word-granular level, we can't implement the mappings by simply shuffling
* the underlying block around in the Bus component; it would be much more efficient if we could. That's how we move
* the IOPAGE in response to addressing changes.
*
* @this {CPUStatePDP11}
* @param {number} addr
* @return {number}
*/
mapUnibus(addr)
{
var idx = (addr >> 13) & 0x1F;
if (idx < 31) {
if (this.regMMR3 & PDP11.MMR3.UNIBUS_MAP) {
/*
* The UNIBUS map relocation is enabled
*/
addr = (this.regsUniMap[idx] + (addr & 0x1FFF)) & BusPDP11.MASK_22BIT;
/*
* TODO: Review this assertion.
*
*
*/
} else {
/*
* Since UNIBUS map relocation is NOT enabled, then as explained above:
*
* If the UNIBUS map relocation is not enabled, an incoming 18-bit UNIBUS address has 4 leading zeroes added for
* referencing a 22-bit physical address. The lower 18 bits are the same. No relocation is performed.
*/
addr &= ~BusPDP11.UNIBUS_22BIT;
}
}
return addr;
}
/**
* getAddrInfo(addr, fPhysical)
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {boolean} [fPhysical]
* @return {Array}
*/
getAddrInfo(addr, fPhysical)
{
var a = [];
var addrPhysical;
if (fPhysical) {
addrPhysical = this.mapUnibus(addr);
var idx = (addr >> 13) & 0x1F;
a.push(addrPhysical);
a.push(idx);
if (this.regMMR3 & PDP11.MMR3.UNIBUS_MAP) {
a.push(this.regsUniMap[idx]);
a.push(addr & 0x1FFF);
}
}
else if (!this.mmuEnable) {
addrPhysical = addr & 0xffff;
if (addrPhysical >= BusPDP11.IOPAGE_16BIT) addrPhysical |= this.addrIOPage;
a.push(addrPhysical);
}
else {
var mode = this.pswMode << 1;
var page = addr >> 13;
if (page > 7) mode |= 1;
if (!(this.regMMR3 & this.mapMMR3[this.pswMode])) page &= 7;
var pdr = this.regsPDR[this.pswMode][page];
var off = addr & 0x1fff;
var paf = (this.regsPAR[this.pswMode][page] << 6);
addrPhysical = (paf + off) & this.mmuMask;
if (addrPhysical >= BusPDP11.UNIBUS_22BIT) addrPhysical = this.mapUnibus(addrPhysical);
a.push(addrPhysical); // a[0]
a.push(off); // a[1]
a.push(mode); // a[2] (0=KI, 1=KD, 2=SI, 3=SD, 4=??, 5=??, 6=UI, 7=UD)
a.push(page & 7); // a[3]
a.push(paf); // a[4]
a.push(this.mmuMask); // a[5]
}
return a;
}
/**
* mapVirtualToPhysical(addrVirtual, access)
*
* mapVirtualToPhysical() does memory management. It converts a 17-bit I/D virtual address to a
* 22-bit physical address. A real PDP 11/70 memory management unit can be enabled separately for
* read and write for diagnostic purposes. This is handled here by having an enable mask (mmuEnable)
* which is tested against the operation access mask (access). If there is no match, then the virtual
* address is simply mapped as a 16 bit physical address with the upper page going to the IO address
* space. Significant access mask values used are PDP11.ACCESS.READ and PDP11.ACCESS.WRITE.
*
* When doing mapping, pswMode is used to decide what address space is to be used (0 = kernel,
* 1 = supervisor, 2 = illegal, 3 = user). Normally, pswMode is set by the setPSW() function, but
* there are exceptions for instructions which move data between address spaces (MFPD, MFPI, MTPD,
* and MTPI) and trap(). These will modify pswMode outside of setPSW() and then restore it again if
* all worked. If however something happens to cause a trap then no restore is done as setPSW()
* will have been invoked as part of the trap, which will resynchronize pswMode.
*
* A PDP-11/70 is different from other PDP-11s in that the highest 18 bit space (017000000 & above)
* maps directly to UNIBUS space - including low memory. This doesn't appear to be particularly useful
* as it restricts maximum system memory - although it does appear to allow software testing of the
* UNIBUS map. This feature also appears to confuse some OSes which test consecutive memory locations
* to find maximum memory -- and on a full memory system find themselves accessing low memory again at
* high addresses.
*
* Construction of a Physical Address
* ----------------------------------
*
* Virtual Addr (VA) 12 11 10 9 8 7 6 5 4 3 2 1 0
* + Page Addr Field (PAF) 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
* -----------------------------------------------------------------
* = Physical Addr (PA) 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
*
* The Page Address Field (PAF) comes from a Page Address Register (PAR) that is selected by Virtual
* Address (VA) bits 15-13. You can see from the above alignments that the VA contributes to the low
* 13 bits, providing an 8Kb range.
*
* VA bits 0-5 pass directly through to the PA; those are also called the DIB (Displacement in Block) bits.
* VA bits 6-12 are added to the low 7 bits of the PAF and are also called the BN (Block Number) bits.
*
* You can also think of the entire PAF as a block number, where each block is 64 bytes. This is consistent
* with the LSIZE register at 177760, which is supposed to contain the block number of the last 64-byte block
* of memory installed.
*
* Note that if a PAR is initialized to zero, successively adding 0200 (0x80) to the PAR will advance the
* base physical address to the next 8Kb page.
*
* @this {CPUStatePDP11}
* @param {number} addrVirtual
* @param {number} access
* @return {number}
*/
mapVirtualToPhysical(addrVirtual, access)
{
var page, pdr, addr;
/*
* This can happen when the MAINT bit of MMR0 is set but not the ENABLED bit.
*/
if (!(access & this.mmuEnable)) {
addr = addrVirtual & 0xffff;
if (addr >= BusPDP11.IOPAGE_16BIT) addr |= this.addrIOPage;
return addr;
}
page = addrVirtual >> 13;
if (!(this.regMMR3 & this.mapMMR3[this.pswMode])) page &= 7;
pdr = this.regsPDR[this.pswMode][page];
addr = ((this.regsPAR[this.pswMode][page] << 6) + (addrVirtual & 0x1fff)) & this.mmuMask;
if (addr >= BusPDP11.UNIBUS_22BIT) addr = this.mapUnibus(addr);
if (this.nDisableTraps) return addr;
/*
* TEST #122 ("KT BEND") in the "EKBEE1" diagnostic (PC 076060) triggers a NOMEMORY error using
* this instruction:
*
* 076170: 005037 140100 CLR @#140100
*
* It also triggers an ODDADDR error using this instruction:
*
* 076356: 005037 140001 CLR @#140001
*
* @paulnank: So it turns out that the memory management unit that does odd address and non-existent
* memory trapping: who knew? :-) I thought these would have been handled at access time.
*
* @jeffpar: We're assuming, at least, that the MMU does its "NEXM" (NOMEMORY) non-existent memory test
* very simplistically, by range-checking the address against something like the memory SIZE registers,
* because otherwise the MMU would have to wait for a bus time-out: something so prohibitively expensive
* that the MMU could not afford to do it. I rely on addrInvalid, which is derived from the same Bus
* getMemoryLimit() service that the SIZE registers (177760--177762) use to derive their value.
*/
if (addr >= this.addrInvalid && addr < this.addrIOPage) {
this.regErr |= PDP11.CPUERR.NOMEMORY;
this.trap(PDP11.TRAP.BUS, 0, addr);
}
else if ((addr & 0x1) && !(access & PDP11.ACCESS.BYTE)) {
this.regErr |= PDP11.CPUERR.ODDADDR;
this.trap(PDP11.TRAP.BUS, 0, addr);
}
var newMMR0 = 0;
switch (pdr & PDP11.PDR.ACF.MASK) {
case PDP11.PDR.ACF.RO1: // 0x1: read-only, abort on write attempt, memory management trap on read (11/70 only)
newMMR0 = PDP11.MMR0.TRAP_MMU;
/* falls through */
case PDP11.PDR.ACF.RO: // 0x2: read-only, abort on write attempt
pdr |= PDP11.PDR.ACCESSED;
if (access & PDP11.ACCESS.WRITE) {
newMMR0 = PDP11.MMR0.ABORT_RO;
}
break;
case PDP11.PDR.ACF.RW1: // 0x4: read/write, memory management trap upon completion of a read or write
newMMR0 = PDP11.MMR0.TRAP_MMU;
/* falls through */
case PDP11.PDR.ACF.RW2: // 0x5: read/write, memory management trap upon completion of a write (11/70 only)
if (access & PDP11.ACCESS.WRITE) {
newMMR0 = PDP11.MMR0.TRAP_MMU;
}
/* falls through */
case PDP11.PDR.ACF.RW: // 0x6: read/write, no system trap/abort action
pdr |= ((access & PDP11.ACCESS.WRITE) ? (PDP11.PDR.ACCESSED | PDP11.PDR.MODIFIED) : PDP11.PDR.ACCESSED);
break;
default: // 0x0 (non-resident, abort all accesses) or 0x3 or 0x7 (unused, abort all accesses)
newMMR0 = PDP11.MMR0.ABORT_NR;
break;
}
if ((pdr & (PDP11.PDR.PLF | PDP11.PDR.ED)) != PDP11.PDR.PLF) { // skip checking most common case (hopefully)
/*
* The Page Descriptor Register (PDR) Page Length Field (PLF) is a 7-bit block number, where a block
* is 64 bytes. Since the bit 0 of the block number is located at bit 8 of the PDR, we shift the PDR
* right 2 bits and then clear the bottom 6 bits by masking it with 0x1FC0.
*/
if (pdr & PDP11.PDR.ED) {
if (pdr & PDP11.PDR.PLF) {
if ((addrVirtual & 0x1FC0) < ((pdr >> 2) & 0x1FC0)) {
newMMR0 |= PDP11.MMR0.ABORT_PL;
}
}
} else {
if ((addrVirtual & 0x1FC0) > ((pdr >> 2) & 0x1FC0)) {
newMMR0 |= PDP11.MMR0.ABORT_PL;
}
}
}
/*
* Aborts and traps: log FIRST trap and MOST RECENT abort
*/
this.regsPDR[this.pswMode][page] = pdr;
if (addr != ((BusPDP11.IOPAGE_22BIT | PDP11.UNIBUS.MMR0) & this.mmuMask) || this.pswMode) {
this.mmuLastMode = this.pswMode;
this.mmuLastPage = page;
}
if (newMMR0) {
if (newMMR0 & PDP11.MMR0.ABORT) {
if (this.pswTrap >= 0) {
newMMR0 |= PDP11.MMR0.COMPLETED;
}
if (!(this.regMMR0 & PDP11.MMR0.ABORT)) {
newMMR0 |= (this.regMMR0 & PDP11.MMR0.TRAP_MMU) | (this.mmuLastMode << 5) | (this.mmuLastPage << 1);
this.setMMR0((this.regMMR0 & ~PDP11.MMR0.UPDATE) | (newMMR0 & PDP11.MMR0.UPDATE));
}
/*
* NOTE: In unusual circumstances, if regMMR0 already indicated an ABORT condition above,
* we run the risk of infinitely looping; eg, we call trap(), which calls mapVirtualToPhysical()
* on the trap vector, which faults again, etc.
*
* TODO: Determine what a real PDP-11 does in that situation; in our case, trap() deals with it
* by checking an internal OPFLAG (TRAP_RED) and turning the next trap into a PANIC, triggering an
* immediate HALT.
*/
this.trap(PDP11.TRAP.MMU, PDP11.OPFLAG.TRAP_MMU, PDP11.REASON.ABORT);
}
if (!(this.regMMR0 & (PDP11.MMR0.ABORT | PDP11.MMR0.TRAP_MMU))) {
/*
* TODO: Review the code below, because the address range seems over-inclusive.
*/
if (addr < ((BusPDP11.IOPAGE_22BIT | PDP11.UNIBUS.SIPDR0) & this.mmuMask) ||
addr > ((BusPDP11.IOPAGE_22BIT | PDP11.UNIBUS.UDPAR7 | 0x1) & this.mmuMask)) {
this.regMMR0 |= PDP11.MMR0.TRAP_MMU;
if (this.regMMR0 & PDP11.MMR0.MMU_TRAPS) this.opFlags |= PDP11.OPFLAG.TRAP_MMU;
}
}
}
return addr;
}
/**
* popWord()
*
* @this {CPUStatePDP11}
* @return {number}
*/
popWord()
{
var result = this.readWord(this.regsGen[6] | this.addrDSpace);
this.regsGen[6] = (this.regsGen[6] + 2) & 0xffff;
return result;
}
/**
* pushWord(data)
*
* @this {CPUStatePDP11}
* @param {number} data
*/
pushWord(data)
{
var addrVirtual = (this.regsGen[6] - 2) & 0xffff;
this.regsGen[6] = addrVirtual; // BSD needs SP updated before any fault :-(
this.opLast = (this.opLast & 0xffff) | ((this.opLast & ~0xffff) << 8) | (0x00f6 << 16);
if (!(this.opFlags & PDP11.OPFLAG.TRAP_RED)) this.checkStackLimit(PDP11.ACCESS.WRITE_WORD, -2, addrVirtual);
this.writeWord(addrVirtual, data);
}
/**
* getAddrByMode(mode, reg, access)
*
* getAddrByMode() maps a six bit operand to a 17 bit I/D virtual address space.
*
* Instruction operands are six bits in length - three bits for the mode and three
* for the register. The 17th I/D bit in the resulting virtual address represents
* whether the reference is to Instruction space or Data space - which depends on
* combination of the mode and whether the register is the Program Counter (R7).
*
* The eight modes are:-
* 0 R no valid virtual address
* 1 (R) operand from I/D depending if R = 7
* 2 (R)+ operand from I/D depending if R = 7
* 3 @(R)+ address from I/D depending if R = 7 and operand from D space
* 4 -(R) operand from I/D depending if R = 7
* 5 @-(R) address from I/D depending if R = 7 and operand from D space
* 6 x(R) x from I space but operand from D space
* 7 @x(R) x from I space but address and operand from D space
*
* Also need to keep MMR1 updated as this stores which registers have been
* incremented and decremented so that the OS can reset and restart an instruction
* if a page fault occurs.
*
* Stack Overflow Traps
* --------------------
* On the PDP-11/20, stack overflow traps occur when an address below 400 is referenced
* by SP in either mode 4 (auto-decrement) or 5 (auto-decrement deferred). The instruction
* is allowed to complete before the trap is issued. NOTE: This information comes
* directly from the PDP-11/20 Handbook (1971), but the 11/20 diagnostics apparently only
* test mode 4, not mode 5, because when I later removed stack limit checks for mode 5 on
* the 11/70, none of the 11/20 tests complained.
*
* TODO: Find some independent confirmation as to whether ANY PDP-11 models check for
* stack overflow on mode 5 (auto-decrement deferred); if they do, then further tweaks to
* checkStackLimit functions may be required.
*
* On the PDP-11/70, the stack limit register (177774) allows a variable boundary for the
* kernel stack.
*
* @this {CPUStatePDP11}
* @param {number} mode
* @param {number} reg
* @param {number} access
* @return {number}
*/
getAddrByMode(mode, reg, access)
{
var addrVirtual, step;
var addrDSpace = (access & PDP11.ACCESS.VIRT)? 0 : this.addrDSpace;
/*
* Modes that need to auto-increment or auto-decrement will break, in order to perform
* the update; others will return an address immediately.
*/
switch (mode) {
/*
* Mode 0: Registers don't have a virtual address, so trap.
*
* NOTE: Most instruction code paths never call getAddrByMode() when the mode is zero;
* JMP and JSR instructions are exceptions, but that's OK, because those are documented as
* ILLEGAL instructions which produce a BUS trap (as opposed to UNDEFINED instructions
* that cause a RESERVED trap).
*/
case 0:
this.trap(PDP11.TRAP.BUS, 0, PDP11.REASON.ILLEGAL);
return 0;
/*
* Mode 1: (R)
*/
case 1:
if (reg == 6) this.checkStackLimit(access, 0, this.regsGen[6]);
this.nStepCycles -= (2 + 1);
return (reg == 7? this.regsGen[reg] : (this.regsGen[reg] | addrDSpace));
/*
* Mode 2: (R)+
*/
case 2:
step = 2;
addrVirtual = this.regsGen[reg];
if (reg == 6) this.checkStackLimit(access, step, addrVirtual);
if (reg != 7) {
addrVirtual |= addrDSpace;
if (reg < 6 && (access & PDP11.ACCESS.BYTE)) step = 1;
}
this.nStepCycles -= (2 + 1);
break;
/*
* Mode 3: @(R)+
*/
case 3:
step = 2;
addrVirtual = this.regsGen[reg];
if (reg != 7) addrVirtual |= addrDSpace;
addrVirtual = this.readWord(addrVirtual);
addrVirtual |= addrDSpace;
this.nStepCycles -= (5 + 2);
break;
/*
* Mode 4: -(R)
*/
case 4:
step = -2;
if (reg < 6 && (access & PDP11.ACCESS.BYTE)) step = -1;
addrVirtual = (this.regsGen[reg] + step) & 0xffff;
if (reg == 6) this.checkStackLimit(access, step, addrVirtual);
if (reg != 7) addrVirtual |= addrDSpace;
this.nStepCycles -= (3 + 1);
break;
/*
* Mode 5: @-(R)
*/
case 5:
step = -2;
addrVirtual = (this.regsGen[reg] - 2) & 0xffff;
if (reg != 7) addrVirtual |= addrDSpace;
addrVirtual = this.readWord(addrVirtual) | addrDSpace;
this.nStepCycles -= (6 + 2);
break;
/*
* Mode 6: d(R)
*/
case 6:
addrVirtual = this.readWord(this.advancePC(2));
addrVirtual = (addrVirtual + this.regsGen[reg]) & 0xffff;
if (reg == 6) this.checkStackLimit(access, 0, addrVirtual);
this.nStepCycles -= (4 + 2);
return addrVirtual | addrDSpace;
/*
* Mode 7: @d(R)
*/
case 7:
addrVirtual = this.readWord(this.advancePC(2));
addrVirtual = (addrVirtual + this.regsGen[reg]) & 0xffff;
addrVirtual = this.readWord(addrVirtual | this.addrDSpace);
this.nStepCycles -= (7 + 3);
return addrVirtual | addrDSpace;
}
this.regsGen[reg] = (this.regsGen[reg] + step) & 0xffff;
this.opLast = (this.opLast & 0xffff) | ((this.opLast & ~0xffff) << 8) | ((((step << 3) & 0xf8) | reg) << 16);
return addrVirtual;
}
/**
* checkStackLimit1120(access, step, addr)
*
* @this {CPUStatePDP11}
* @param {number} access
* @param {number} step
* @param {number} addr
*/
checkStackLimit1120(access, step, addr)
{
/*
* NOTE: DEC's "TRAP TEST" (MAINDEC-11-D0NA-PB) expects "TST -(SP)" to trap when SP is 150,
* so we ignore the access parameter. Also, strangely, it does NOT expect this instruction
* to trap:
*
* R0=006302 R1=000000 R2=000000 R3=000000 R4=000000 R5=000776
* SP=000000 PC=006346 PS=000344 IR=000000 SL=000377 T0 N0 Z1 V0 C0
* 006346: 112667 171426 MOVB (SP)+,000000
*
* so if the step parameter is positive, we let it go.
*/
if (!this.pswMode && step <= 0 && addr <= this.regSLR) {
/*
* On older machines (eg, the PDP-11/20), there is no "YELLOW" and "RED" distinction, and the
* instruction is always allowed to complete, so the trap must always be issued in this fashion.
*/
this.opFlags |= PDP11.OPFLAG.TRAP_SP;
}
}
/**
* checkStackLimit1140(access, step, addr)
*
* @this {CPUStatePDP11}
* @param {number} access
* @param {number} step
* @param {number} addr
*/
checkStackLimit1140(access, step, addr)
{
if (!this.pswMode) {
/*
* NOTE: The 11/70 CPU Instruction Exerciser does NOT expect reads to trigger a stack overflow,
* so we check the access parameter.
*
* Moreover, TEST 40 of diagnostic EKBBF0 executes this instruction:
*
* R0=177777 R1=032435 R2=152110 R3=000024 R4=153352 R5=001164
* SP=177776 PC=020632 PS=000350 IR=000000 SL=000377 T0 N1 Z0 V0 C0
* 020632: 005016 CLR @SP ;cycles=7
*
* expecting a RED stack overflow trap. Yes, using *any* addresses in the IOPAGE for the stack isn't
* a good idea, but who said it was illegal? For now, we're going to restrict overflows to the highest
* address tested by the diagnostic (0xFFFE, aka the PSW), by making that address negative.
*/
if (addr >= 0xFFFE) addr |= ~0xFFFF;
if ((access & PDP11.ACCESS.WRITE) && addr <= this.regSLR) {
/*
* regSLR can never fall below 0xFF, so this subtraction can never go negative, so this comparison
* is always safe.
*/
if (addr <= this.regSLR - 32) {
this.trap(PDP11.TRAP.BUS, 0, PDP11.REASON.RED);
} else {
this.regErr |= PDP11.CPUERR.YELLOW;
this.opFlags |= PDP11.OPFLAG.TRAP_SP;
}
}
}
}
/**
* getByteChecked(addr)
*
* This is the getByte() handler whenever the Debugger has one or more virtual memory READ breakpoints set;
* otherwise, getByte() is bound to Bus.getByte().
*
* @this {CPUStatePDP11}
* @param {number} addr
* @return {number}
*/
getByteChecked(addr)
{
if (DEBUGGER && this.dbg) {
this.dbg.checkMemoryRead(addr, 1);
}
return this.getByteDirect(addr);
}
/**
* getWordChecked(addr)
*
* This is the getWord() handler whenever the Debugger has one or more virtual memory READ breakpoints set;
* otherwise, getWord() is bound to Bus.getWord().
*
* @this {CPUStatePDP11}
* @param {number} addr
* @return {number}
*/
getWordChecked(addr)
{
if (DEBUGGER && this.dbg) {
this.dbg.checkMemoryRead(addr, 2);
}
return this.getWordDirect(addr);
}
/**
* setByteChecked(addr, data)
*
* This is the setByte() handler whenever the Debugger has one or more virtual memory WRITE breakpoints set;
* otherwise, setByte() is bound to Bus.setByte().
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {number} data
*/
setByteChecked(addr, data)
{
if (DEBUGGER && this.dbg) {
this.dbg.checkMemoryWrite(addr, 1);
}
this.setByteDirect(addr, data);
}
/**
* setWordChecked(addr, data)
*
* This is the setWord() handler whenever the Debugger has one or more virtual memory WRITE breakpoints set;
* otherwise, setWord() is bound to Bus.setWord().
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {number} data
*/
setWordChecked(addr, data)
{
if (DEBUGGER && this.dbg) {
this.dbg.checkMemoryWrite(addr, 2);
}
this.setWordDirect(addr, data);
}
/**
* getByteSafe(addr)
*
* This interface is expressly for the Debugger, to access virtual memory without faulting.
*
* @this {CPUStatePDP11}
* @param {number} addr
* @return {number}
*/
getByteSafe(addr)
{
this.nDisableTraps++;
var b = this.bus.getByte(this.mapVirtualToPhysical(addr, PDP11.ACCESS.READ_BYTE));
this.nDisableTraps--;
return b;
}
/**
* getWordSafe(addr)
*
* This interface is expressly for the Debugger, to access virtual memory without faulting.
*
* @this {CPUStatePDP11}
* @param {number} addr
* @return {number}
*/
getWordSafe(addr)
{
this.nDisableTraps++;
var w = this.bus.getWord(this.mapVirtualToPhysical(addr, PDP11.ACCESS.READ_WORD));
this.nDisableTraps--;
return w;
}
/**
* setByteSafe(addr, data)
*
* This interface is expressly for the Debugger, to access virtual memory without faulting.
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {number} data
*/
setByteSafe(addr, data)
{
this.nDisableTraps++;
this.bus.setByte(this.mapVirtualToPhysical(addr, PDP11.ACCESS.WRITE_BYTE), data);
this.nDisableTraps--;
}
/**
* setWordSafe(addr, data)
*
* This interface is expressly for the Debugger, to access virtual memory without faulting.
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {number} data
*/
setWordSafe(addr, data)
{
this.nDisableTraps++;
this.bus.setWord(this.mapVirtualToPhysical(addr, PDP11.ACCESS.WRITE_WORD), data);
this.nDisableTraps--;
}
/**
* addMemBreak(addr, fWrite)
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint
*/
addMemBreak(addr, fWrite)
{
if (DEBUGGER) {
var nBreaks = fWrite? this.nWriteBreaks++ : this.nReadBreaks++;
if (!nBreaks) this.setMemoryAccess();
}
}
/**
* removeMemBreak(addr, fWrite)
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint
*/
removeMemBreak(addr, fWrite)
{
if (DEBUGGER) {
var nBreaks = fWrite? --this.nWriteBreaks : --this.nReadBreaks;
if (!nBreaks) this.setMemoryAccess();
}
}
/**
* getPhysicalAddrByMode(mode, reg, access)
*
* This is a handler set up by setMemoryAccess(). All calls should go through getAddr().
*
* @this {CPUStatePDP11}
* @param {number} mode
* @param {number} reg
* @param {number} access
* @return {number}
*/
getPhysicalAddrByMode(mode, reg, access)
{
return this.getAddrByMode(mode, reg, access);
}
/**
* getVirtualAddrByMode(mode, reg, access)
*
* This is a handler set up by setMemoryAccess(). All calls should go through getAddr().
*
* @this {CPUStatePDP11}
* @param {number} mode
* @param {number} reg
* @param {number} access
* @return {number}
*/
getVirtualAddrByMode(mode, reg, access)
{
return this.mapVirtualToPhysical(this.getAddrByMode(mode, reg, access), access);
}
/**
* readWordFromPhysical(addr)
*
* This is a handler set up by setMemoryAccess(). All calls should go through readWord().
*
* @this {CPUStatePDP11}
* @param {number} addr
* @return {number}
*/
readWordFromPhysical(addr)
{
return this.bus.getWord(this.addrLast = addr);
}
/**
* readWordFromPhysicalChecked(addr)
*
* This is a handler set up by setMemoryAccess(). All calls should go through readWord().
*
* @this {CPUStatePDP11}
* @param {number} addr
* @return {number}
*/
readWordFromPhysicalChecked(addr)
{
if (DEBUGGER && this.dbg) {
this.dbg.checkMemoryRead(addr, 2);
}
return this.readWordFromPhysical(addr);
}
/**
* readWordFromVirtual(addrVirtual)
*
* This is a handler set up by setMemoryAccess(). All calls should go through readWord().
*
* @this {CPUStatePDP11}
* @param {number} addrVirtual (input address is 17 bit (I&D))
* @return {number}
*/
readWordFromVirtual(addrVirtual)
{
return this.bus.getWord(this.addrLast = this.mapVirtualToPhysical(addrVirtual, PDP11.ACCESS.READ_WORD));
}
/**
* readWordFromVirtualChecked(addrVirtual)
*
* This is a handler set up by setMemoryAccess(). All calls should go through readWord().
*
* @this {CPUStatePDP11}
* @param {number} addrVirtual (input address is 17 bit (I&D))
* @return {number}
*/
readWordFromVirtualChecked(addrVirtual)
{
if (DEBUGGER && this.dbg) {
this.dbg.checkMemoryRead(addrVirtual, 2);
}
return this.readWordFromVirtual(addrVirtual);
}
/**
* writeWordToPhysical(addr, data)
*
* This is a handler set up by setMemoryAccess(). All calls should go through writeWord().
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {number} data
*/
writeWordToPhysical(addr, data)
{
this.bus.setWord(this.addrLast = addr, data);
}
/**
* writeWordToPhysicalChecked(addr, data)
*
* This is a handler set up by setMemoryAccess(). All calls should go through writeWord().
*
* @this {CPUStatePDP11}
* @param {number} addr
* @param {number} data
*/
writeWordToPhysicalChecked(addr, data)
{
if (DEBUGGER && this.dbg) {
this.dbg.checkMemoryWrite(addr, 2);
}
this.writeWordToPhysical(addr, data);
}
/**
* writeWordToVirtual(addrVirtual, data)
*
* This is a handler set up by setMemoryAccess(). All calls should go through writeWord().
*
* @this {CPUStatePDP11}
* @param {number} addrVirtual (input address is 17 bit (I&D))
* @param {number} data
*/
writeWordToVirtual(addrVirtual, data)
{
this.bus.setWord(this.addrLast = this.mapVirtualToPhysical(addrVirtual, PDP11.ACCESS.WRITE_WORD), data);
}
/**
* writeWordToVirtualChecked(addrVirtual, data)
*
* This is a handler set up by setMemoryAccess(). All calls should go through writeWord().
*
* @this {CPUStatePDP11}
* @param {number} addrVirtual (input address is 17 bit (I&D))
* @param {number} data
*/
writeWordToVirtualChecked(addrVirtual, data)
{
if (DEBUGGER && this.dbg) {
this.dbg.checkMemoryWrite(addrVirtual, 2);
}
this.writeWordToVirtual(addrVirtual, data);
}
/**
* readWordFromPrevSpace(opCode, access)
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @param {number} access (really just PDP11.ACCESS.DSPACE or PDP11.ACCESS.ISPACE)
* @return {number}
*/
readWordFromPrevSpace(opCode, access)
{
var data;
var reg = this.dstReg = opCode & PDP11.OPREG.MASK;
var mode = this.dstMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
if (reg != 6 || ((this.regPSW >> 2) & PDP11.PSW.PMODE) === (this.regPSW & PDP11.PSW.PMODE)) {
data = this.regsGen[reg];
} else {
data = this.regsAltStack[(this.regPSW >> 12) & 3];
}
} else {
var addr = this.getAddrByMode(mode, reg, PDP11.ACCESS.READ_WORD);
if (!(access & PDP11.ACCESS.DSPACE)) {
if ((this.regPSW & 0xf000) !== 0xf000) addr &= 0xffff;
}
this.pswMode = (this.regPSW >> 12) & 3;
data = this.readWord(addr | (access & this.addrDSpace));
this.pswMode = (this.regPSW >> 14) & 3;
}
return data;
}
/**
* writeWordToPrevSpace(opCode, access, data)
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @param {number} access (really just PDP11.ACCESS.DSPACE or PDP11.ACCESS.ISPACE)
* @param {number} data
*/
writeWordToPrevSpace(opCode, access, data)
{
this.opLast = (this.opLast & 0xffff) | (0x0016 << 16);
var reg = this.dstReg = opCode & PDP11.OPREG.MASK;
var mode = this.dstMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
if (reg != 6 || ((this.regPSW >> 2) & PDP11.PSW.PMODE) === (this.regPSW & PDP11.PSW.PMODE)) {
this.regsGen[reg] = data;
} else {
this.regsAltStack[(this.regPSW >> 12) & 3] = data;
}
} else {
var addr = this.getAddrByMode(mode, reg, PDP11.ACCESS.WRITE_WORD);
if (!(access & PDP11.ACCESS.DSPACE)) addr &= 0xffff;
/*
* TODO: Consider replacing the following code with writeWord(), by adding optional pswMode
* parameters for each of the discrete mapVirtualToPhysical() and setWord() operations, because
* as it stands, this is the only remaining call to mapVirtualToPhysical() outside of our
* setMemoryAccess() handlers.
*/
this.pswMode = (this.regPSW >> 12) & 3;
addr = this.mapVirtualToPhysical(addr | (access & PDP11.ACCESS.DSPACE), PDP11.ACCESS.WRITE);
this.pswMode = (this.regPSW >> 14) & 3;
this.setWord(addr, data);
}
}
/**
* readSrcByte(opCode)
*
* WARNING: If the SRC operand is a register, offRegSrc ensures we return a negative register number
* rather than the register value, because on the PDP-11/20, the final value of the register must be
* resolved AFTER the DST operand has been decoded and any pre-decrement or post-increment operations
* affecting the SRC register have been completed. See readSrcWord() for more details.
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @return {number}
*/
readSrcByte(opCode)
{
var result;
opCode >>= PDP11.SRCMODE.SHIFT;
var reg = this.srcReg = opCode & PDP11.OPREG.MASK;
var mode = this.srcMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
result = this.regsGen[reg + this.offRegSrc] & this.maskRegSrcByte;
} else {
result = this.getByte(this.getAddr(mode, reg, PDP11.ACCESS.READ_BYTE));
}
return result;
}
/**
* readSrcWord(opCode)
*
* WARNING: If the SRC operand is a register, offRegSrc ensures we return a negative register number
* rather than the register value, because on the PDP-11/20, the final value of the register must be
* resolved AFTER the DST operand has been decoded and any pre-decrement or post-increment operations
* affecting the SRC register have been completed.
*
* Here's an example from DEC's "TRAP TEST" (MAINDEC-11-D0NA-PB):
*
* 007200: 012700 006340 MOV #6340,R0
* 007204: 010020 MOV R0,(R0)+
* 007206: 026727 177126 006342 CMP 006340,#6342
* 007214: 001401 BEQ 007220
* 007216: 000000 HALT
*
* If this function returned the value of R0 for the SRC operand of "MOV R0,(R0)+", then the operation
* would write 6340 to the destination, rather than 6342.
*
* Most callers don't need to worry about this, because if they pass the result from readSrcWord() directly
* to writeDstWord() or updateDstWord(), those functions will take care of converting any negative register
* number back into the current register value. The exceptions are opcodes that don't modify the DST operand
* (BIT, BITB, CMP, and CMPB); those opcode handlers must deal with negative register numbers themselves.
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @return {number}
*/
readSrcWord(opCode)
{
var result;
opCode >>= PDP11.SRCMODE.SHIFT;
var reg = this.srcReg = opCode & PDP11.OPREG.MASK;
var mode = this.srcMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
result = this.regsGen[reg + this.offRegSrc];
} else {
result = this.getWord(this.getAddr(mode, reg, PDP11.ACCESS.READ_WORD));
}
return result;
}
/**
* readDstAddr(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @return {number}
*/
readDstAddr(opCode)
{
var reg = this.dstReg = opCode & PDP11.OPREG.MASK;
var mode = this.dstMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
return this.getAddrByMode(mode, reg, PDP11.ACCESS.VIRT);
}
/**
* readDstByte(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @return {number}
*/
readDstByte(opCode)
{
var result;
var reg = this.dstReg = opCode & PDP11.OPREG.MASK;
var mode = this.dstMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
result = this.regsGen[reg] & 0xff;
} else {
result = this.getByte(this.getAddr(mode, reg, PDP11.ACCESS.READ_BYTE));
}
return result;
}
/**
* readDstWord(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @return {number}
*/
readDstWord(opCode)
{
var result;
var reg = this.dstReg = opCode & PDP11.OPREG.MASK;
var mode = this.dstMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
result = this.regsGen[reg];
} else {
result = this.getWord(this.getAddr(mode, reg, PDP11.ACCESS.READ_WORD));
}
return result;
}
/**
* updateDstByte(opCode, data, fnOp)
*
* Used whenever the DST operand (as described by opCode) needs to be read before writing.
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @param {number} data
* @param {function(number,number)} fnOp
*/
updateDstByte(opCode, data, fnOp)
{
var reg = this.dstReg = opCode & PDP11.OPREG.MASK;
var mode = this.dstMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
var dst = this.regsGen[reg];
data = (data < 0? (this.regsGen[-data-1] & 0xff) : data);
this.regsGen[reg] = (dst & 0xff00) | fnOp.call(this, data, dst & 0xff);
} else {
var addr = this.dstAddr = this.getAddr(mode, reg, PDP11.ACCESS.UPDATE_BYTE);
data = (data < 0? (this.regsGen[-data-1] & 0xff) : data);
this.setByte(addr, fnOp.call(this, data, this.getByte(addr)));
if (addr & 1) this.nStepCycles--;
}
}
/**
* updateDstWord(opCode, data, fnOp)
*
* Used whenever the DST operand (as described by opCode) needs to be read before writing.
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @param {number} data
* @param {function(number,number)} fnOp
*/
updateDstWord(opCode, data, fnOp)
{
var reg = this.dstReg = opCode & PDP11.OPREG.MASK;
var mode = this.dstMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
this.regsGen[reg] = fnOp.call(this, data < 0? this.regsGen[-data-1] : data, this.regsGen[reg]);
} else {
var addr = this.getAddr(mode, reg, PDP11.ACCESS.UPDATE_WORD);
this.setWord(addr, fnOp.call(this, data < 0? this.regsGen[-data-1] : data, this.getWord(addr)));
}
}
/**
* writeDstByte(opCode, data, writeFlags, fnFlags)
*
* Used whenever the DST operand (as described by opCode) does NOT need to be read before writing.
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @param {number} data
* @param {number} writeFlags (WRITE.BYTE aka 0xff, or WRITE.SBYTE aka 0xffff)
* @param {function(number)} fnFlags
*/
writeDstByte(opCode, data, writeFlags, fnFlags)
{
var reg = this.dstReg = opCode & PDP11.OPREG.MASK;
var mode = this.dstMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
if (!data) {
/*
* Potentially worthless optimization (but it looks good on "paper").
*/
this.regsGen[reg] &= ~writeFlags;
} else {
/*
* Potentially worthwhile optimization: skipping the sign-extending data shifts
* if writeFlags is WRITE.BYTE (but that requires an extra test and separate code paths).
*/
data = (data < 0? (this.regsGen[-data-1] & 0xff): data);
this.regsGen[reg] = (this.regsGen[reg] & ~writeFlags) | (((data << 24) >> 24) & writeFlags);
}
fnFlags.call(this, data << 8);
} else {
var addr = this.getAddr(mode, reg, PDP11.ACCESS.WRITE_BYTE);
fnFlags.call(this, (data = data < 0? (this.regsGen[-data-1] & 0xff) : data) << 8);
this.setByte(addr, data);
if (addr & 1) this.nStepCycles--;
}
}
/**
* writeDstWord(opCode, data, fnFlags)
*
* Used whenever the DST operand (as described by opCode) does NOT need to be read before writing.
*
* @this {CPUStatePDP11}
* @param {number} opCode
* @param {number} data
* @param {function(number)} fnFlags
*/
writeDstWord(opCode, data, fnFlags)
{
var reg = this.dstReg = opCode & PDP11.OPREG.MASK;
var mode = this.dstMode = (opCode & PDP11.OPMODE.MASK) >> PDP11.OPMODE.SHIFT;
if (!mode) {
this.regsGen[reg] = (data = data < 0? this.regsGen[-data-1] : data);
fnFlags.call(this, data);
} else {
var addr = this.getAddr(mode, reg, PDP11.ACCESS.WRITE_WORD);
fnFlags.call(this, (data = data < 0? this.regsGen[-data-1] : data));
this.setWord(addr, data);
}
}
/**
* stepCPU(nMinCycles)
*
* NOTE: Single-stepping should not be confused with the Trap flag; single-stepping is a Debugger
* operation that's completely independent of Trap status. The CPU can go in and out of Trap mode,
* in and out of h/w interrupt service routines (ISRs), etc, but from the Debugger's perspective,
* they're all one continuous stream of instructions that can be stepped or run at will. Moreover,
* stepping vs. running should never change the behavior of the simulation.
*
* @this {CPUStatePDP11}
* @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored)
* @return {number} of cycles executed; 0 indicates a pre-execution condition (ie, an execution breakpoint
* was hit), -1 indicates a post-execution condition (eg, a read or write breakpoint was hit), and a positive
* number indicates successful completion of that many cycles (which should always be >= nMinCycles).
*/
stepCPU(nMinCycles)
{
/*
* The Debugger uses complete to determine if the instruction completed (true) or was interrupted
* by a breakpoint or some other exceptional condition (false). NOTE: this does NOT include JavaScript
* exceptions, which stepCPU() expects the caller to catch using its own exception handler.
*
* The CPU relies on the use of stopCPU() rather than complete, because the CPU never single-steps
* (ie, nMinCycles is always some large number), whereas the Debugger does. And conversely, when the
* Debugger is single-stepping (even when performing multiple single-steps), fRunning is never set,
* so stopCPU() would have no effect as far as the Debugger is concerned.
*/
this.flags.complete = true;
/*
* nDebugCheck is 1 if we want the Debugger's checkInstruction() to check every instruction,
* -1 if we want it to check just the first instruction, and 0 if there's no need for any checks.
*/
var nDebugCheck = (DEBUGGER && this.dbg)? (this.dbg.checksEnabled()? 1 : (this.flags.starting? -1 : 0)) : 0;
/*
* nDebugState is needed only when nDebugCheck is non-zero; it is -1 if this is a single-step, 0 if
* this is the start of a new run, and 1 if this is a continuation of a previous run. It is used by
* checkInstruction() to determine if it should skip breakpoint checks and/or HALT instructions (ie,
* if nDebugState is <= zero).
*/
var nDebugState = (!nMinCycles)? -1 : (this.flags.starting? 0 : 1);
this.flags.starting = false; // we've moved beyond "starting" and have officially "started" now
/*
* We move the minimum cycle count to nStepCycles (the number of cycles left to step), so that other
* functions have the ability to force that number to zero (eg, stopCPU()), and thus we don't have to check
* any other criteria to determine whether we should continue stepping or not.
*/
this.nBurstCycles = this.nStepCycles = nMinCycles;
/*
* And finally, move the nDebugCheck state to an OPFLAG bit, so that the loop need check only one variable.
*/
this.opFlags = (this.opFlags & ~PDP11.OPFLAG.DEBUGGER) | (nDebugCheck? PDP11.OPFLAG.DEBUGGER : 0);
do {
if (this.opFlags) {
/*
* NOTE: We still check DEBUGGER to ensure that this code will be compiled out of existence in
* non-DEBUGGER builds.
*/
if (DEBUGGER && (this.opFlags & PDP11.OPFLAG.DEBUGGER)) {
if (this.dbg.checkInstruction(this.getPC(), nDebugState)) {
this.stopCPU();
break;
}
if (!++nDebugCheck) this.opFlags &= ~PDP11.OPFLAG.DEBUGGER;
if (!nDebugState) nDebugState++;
}
/*
* If we're in the IRQ or WAIT state, check for any pending interrupts.
*
* NOTE: It's no coincidence that we're checking this BEFORE any pending traps, because in rare
* cases (including some presented by those pesky "TRAP TEST" diagnostics), the process of dispatching
* an interrupt can trigger a TRAP_SP stack overflow condition, which must be dealt with BEFORE we
* execute the first instruction of the interrupt handler.
*/
if ((this.opFlags & (PDP11.OPFLAG.IRQ_MASK | PDP11.OPFLAG.WAIT)) /* && nDebugState >= 0 */) {
if (this.checkInterrupts()) {
if ((this.opFlags & PDP11.OPFLAG.DEBUGGER) && this.dbg.checkInstruction(this.getPC(), nDebugState)) {
this.stopCPU();
break;
}
/*
* Since an interrupt was just dispatched, altering the normal flow of time and changing
* the future as we knew it, let's break out immediately if we're single-stepping, so that
* the Debugger gets to see the first instruction of the interrupt handler. NOTE: This
* assumes that we've still commented out the nDebugState check above that used to bypass
* checkInterrupts() when single-stepping.
*/
if (nDebugState < 0) break;
}
}
/*
* Next, check for any pending traps (which, as noted above, must be done after checkInterrupts()).
*
* I've moved this TRAP_MASK check BEFORE we decode the next instruction instead of immediately AFTER,
* just in case the last instruction threw an exception that kicked us out before we reached the bottom
* of the stepCPU() loop.
*/
if (this.opFlags & PDP11.OPFLAG.TRAP_MASK) {
if (this.checkTraps()) {
if ((this.opFlags & PDP11.OPFLAG.DEBUGGER) && this.dbg.checkInstruction(this.getPC(), nDebugState)) {
this.stopCPU();
break;
}
if (nDebugState < 0) break;
}
}
}
/*
* Snapshot the TF bit in opFlags, while clearing all other opFlags (except those in PRESERVE);
* we'll check the TRAP_TF bit in opFlags when we come back around for another opcode.
*/
this.opFlags = (this.opFlags & PDP11.OPFLAG.PRESERVE) | (this.regPSW & PDP11.PSW.TF);
var opCode = this.getOpcode();
this.opDecode(opCode);
} while (this.nStepCycles > 0);
return (this.flags.complete? this.nBurstCycles - this.nStepCycles : (this.flags.complete === false? -1 : 0));
}
/**
* CPUStatePDP11.init()
*
* This function operates on every HTML element of class "cpu", extracting the
* JSON-encoded parameters for the CPUStatePDP11 constructor from the element's "data-value"
* attribute, invoking the constructor (which in turn invokes the CPU constructor)
* to create a CPUStatePDP11 component, and then binding any associated HTML controls to the
* new component.
*/
static init()
{
var aeCPUs = Component.getElementsByClass(document, PDP11.APPCLASS, "cpu");
for (var iCPU = 0; iCPU < aeCPUs.length; iCPU++) {
var eCPU = aeCPUs[iCPU];
var parmsCPU = Component.getComponentParms(eCPU);
var cpu = new CPUStatePDP11(parmsCPU);
Component.bindComponentControls(cpu, eCPU, PDP11.APPCLASS);
}
}
}
/*
* Initialize every CPU module on the page
*/
Web.onInit(CPUStatePDP11.init);
/**
* @copyright http://pcjs.org/modules/pdp11/lib/cpuops.js (C) Jeff Parsons 2012-2017
*/
/*
* Decoding starts near the bottom of this file, in op1120() and op1140(). Obviously, there are
* MANY more PDP-11 models than the 11/20 and 11/40, but for the broad model categories that PDPjs
* supports (ie, MODEL_1120, MODEL_1140, MODEL_1145, and MODEL_1170), the biggest differences are
* between MODEL_1120 and MODEL_1140, so decoding is divided into those two categories, and all
* other differences are handled inside the opcode handlers.
*
* The basic decoding approach is to dispatch on the top 4 bits of the opcode, and if further
* decoding is required, the dispatched function will dispatch on the next 4 bits, and so on
* (although some of the intermediate levels dispatch only on 2 bits, which could also be handled
* with a switch statement).
*
* Eventually, every opcode should end up either in an opXXX() function or opUndefined(). For
* opcodes that perform a simple read or write operation, the entire operation is handled by
* the opXXX() function. For opcodes that perform a more extensive read/modify/write operation
* (also known as an update operation), those opXXX() functions usually rely on a corresponding
* fnXXX() helper function.
*
* For example, opADD() passes the helper function fnADD() to the appropriate update method. This
* allows the update method to perform the entire read/modify/write operation, because the modify
* step is performed internally, via the fnXXX() helper function.
*
* For the handful of instructions in the 1140 tables that actually exist only on the 11/45 and
* 11/70 (ie, MFPD, MTPD, and SPL), those opcode handlers perform their own model checks. That's
* simpler than creating additional tables, and seems fine for instructions that are not commonly
* executed.
*/
/**
* fnADD(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src
* @param {number} dst
* @return {number} (dst + src)
*/
PDP11.fnADD = function(src, dst)
{
var result = dst + src;
this.updateAddFlags(result, src, dst);
return result & 0xffff;
};
/**
* fnADDB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src
* @param {number} dst
* @return {number} (dst + src)
*/
PDP11.fnADDB = function(src, dst)
{
var result = dst + src;
this.updateAddFlags(result << 8, src << 8, dst << 8);
return result & 0xff;
};
/**
* fnASL(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (dst << 1)
*/
PDP11.fnASL = function(src, dst)
{
var result = dst << 1;
this.updateShiftFlags(result);
return result & 0xffff;
};
/**
* fnASLB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (dst << 1)
*/
PDP11.fnASLB = function(src, dst)
{
var result = dst << 1;
this.updateShiftFlags(result << 8);
return result & 0xff;
};
/**
* fnASR(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (dst >> 1)
*/
PDP11.fnASR = function(src, dst)
{
var result = (dst & 0x8000) | (dst >> 1) | (dst << 16);
this.updateShiftFlags(result);
return result & 0xffff;
};
/**
* fnASRB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (dst >> 1)
*/
PDP11.fnASRB = function(src, dst)
{
var result = (dst & 0x80) | (dst >> 1) | (dst << 8);
this.updateShiftFlags(result << 8);
return result & 0xff;
};
/**
* fnBIC(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src
* @param {number} dst
* @return {number} (~src & dst)
*/
PDP11.fnBIC = function(src, dst)
{
var result = dst & ~src;
this.updateNZVFlags(result);
return result;
};
/**
* fnBICB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src
* @param {number} dst
* @return {number} (~src & dst)
*/
PDP11.fnBICB = function(src, dst)
{
var result = dst & ~src;
this.updateNZVFlags(result << 8);
return result;
};
/**
* fnBIS(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src
* @param {number} dst
* @return {number} (dst | src)
*/
PDP11.fnBIS = function(src, dst)
{
var result = dst | src;
this.updateNZVFlags(result);
return result;
};
/**
* fnBISB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src
* @param {number} dst
* @return {number} (dst | src)
*/
PDP11.fnBISB = function(src, dst)
{
var result = dst | src;
this.updateNZVFlags(result << 8);
return result;
};
/**
* fnCOM(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (~dst)
*/
PDP11.fnCOM = function(src, dst)
{
var result = ~dst | 0x10000;
this.updateAllFlags(result);
return result & 0xffff;
};
/**
* fnCOMB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (~dst)
*/
PDP11.fnCOMB = function(src, dst)
{
var result = ~dst | 0x100;
this.updateAllFlags(result << 8);
return result & 0xff;
};
/**
* fnDEC(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ie, 1)
* @param {number} dst
* @return {number} (dst - src)
*/
PDP11.fnDEC = function(src, dst)
{
var result = dst - src;
this.updateDecFlags(result, dst);
return result & 0xffff;
};
/**
* fnDECB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ie, 1)
* @param {number} dst
* @return {number} (dst - src)
*/
PDP11.fnDECB = function(src, dst)
{
var result = dst - src;
this.updateDecFlags(result << 8, dst << 8);
return result & 0xff;
};
/**
* fnINC(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ie, 1)
* @param {number} dst
* @return {number} (dst + src)
*/
PDP11.fnINC = function(src, dst)
{
var result = dst + src;
this.updateIncFlags(result, dst);
return result & 0xffff;
};
/**
* fnINCB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ie, 1)
* @param {number} dst
* @return {number} (dst + src)
*/
PDP11.fnINCB = function(src, dst)
{
var result = dst + src;
this.updateIncFlags(result << 8, dst << 8);
return result & 0xff;
};
/**
* fnNEG(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (-dst)
*/
PDP11.fnNEG = function(src, dst)
{
var result = -dst;
/*
* If the sign bit of both dst and result are set, the original value must have been 0x8000, triggering overflow.
*/
this.updateAllFlags(result, result & dst & 0x8000);
return result & 0xffff;
};
/**
* fnNEGB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (-dst)
*/
PDP11.fnNEGB = function(src, dst)
{
var result = -dst;
/*
* If the sign bit of both dst and result are set, the original value must have been 0x80, which triggers overflow.
*/
this.updateAllFlags(result << 8, (result & dst & 0x80) << 8);
return result & 0xff;
};
/**
* fnROL(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (dst >> 1)
*/
PDP11.fnROL = function(src, dst)
{
var result = (dst << 1) | ((this.flagC >> 16) & 1);
this.updateShiftFlags(result);
return result & 0xffff;
};
/**
* fnROLB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (dst >> 1)
*/
PDP11.fnROLB = function(src, dst)
{
var result = (dst << 1) | ((this.flagC >> 16) & 1);
this.updateShiftFlags(result << 8);
return result & 0xff;
};
/**
* fnROR(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (dst >> 1)
*/
PDP11.fnROR = function(src, dst)
{
var result = (((this.flagC & 0x10000) | dst) >> 1) | (dst << 16);
this.updateShiftFlags(result);
return result & 0xffff;
};
/**
* fnRORB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (dst >> 1)
*/
PDP11.fnRORB = function(src, dst)
{
var result = ((((this.flagC & 0x10000) >> 8) | dst) >> 1) | (dst << 8);
this.updateShiftFlags(result << 8);
return result & 0xff;
};
/**
* fnSUB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src
* @param {number} dst
* @return {number} (dst - src)
*/
PDP11.fnSUB = function(src, dst)
{
var result = dst - src;
this.updateSubFlags(result, src, dst);
return result & 0xffff;
};
/**
* fnSUBB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src
* @param {number} dst
* @return {number} (dst - src)
*/
PDP11.fnSUBB = function(src, dst)
{
var result = dst - src;
this.updateSubFlags(result << 8, src << 8, dst << 8);
return result & 0xff;
};
/**
* fnSWAB(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src (ignored)
* @param {number} dst
* @return {number} (dst with bytes swapped)
*/
PDP11.fnSWAB = function(src, dst)
{
var result = (dst << 8) | (dst >> 8);
/*
* N and Z are based on the low byte of the result, which is the same as the high byte of dst.
*/
this.updateNZVCFlags(dst & 0xff00);
return result & 0xffff;
};
/**
* fnXOR(src, dst)
*
* @this {CPUStatePDP11}
* @param {number} src
* @param {number} dst
* @return {number} (dst ^ src)
*/
PDP11.fnXOR = function(src, dst)
{
var result = dst ^ src;
this.updateNZVFlags(result);
return result & 0xffff;
};
/**
* opADC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opADC = function(opCode)
{
this.updateDstWord(opCode, this.getCF()? 1 : 0, PDP11.fnADD);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opADCB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opADCB = function(opCode)
{
this.updateDstByte(opCode, this.getCF()? 1 : 0, PDP11.fnADDB);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opADD(opCode)
*
* From the PDP-11/20 Processor HandBook (1971), p. 61:
*
* Add src,dst (06SSDD)
*
* Operation:
* (dst) = (src) + (dst)
*
* Condition Codes:
* N: set if result < 0; cleared otherwise
* Z: set if result = 0; cleared otherwise
* V: set if there was arithmetic overflow as a result of the operation, that is both operands
* were of the same sign and the result was of the opposite sign; cleared otherwise
* C: set if there was a carry from the most significant bit of the result; cleared otherwise
*
* Description:
* Adds the source operand to the destination operand and stores the result at the destination address.
* The original contents of the destination are lost. The contents of the source are not affected.
* Two's complement addition is performed.
*
* Examples:
* Add to register: ADD 20,R0
* Add to memory: ADD R1,XXX
* Add register to register: ADD R1,R2
* Add memory to memory: ADD @#17750,XXX
*
* XXX is a programmer-defined mnemonic for a memory location.
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opADD = function(opCode)
{
this.updateDstWord(opCode, this.readSrcWord(opCode), PDP11.fnADD);
this.nStepCycles -= (this.dstMode? (8 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 2) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opASH(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opASH = function(opCode)
{
var src = this.readDstWord(opCode);
var reg = (opCode >> 6) & 7;
var result = this.regsGen[reg];
if (result & 0x8000) result |= 0xffff0000;
this.flagC = this.flagV = 0;
src &= 0x3F;
if (src & 0x20) {
src = 64 - src; // shift right
if (src > 16) src = 16;
this.flagC = result << (17 - src);
result = result >> src;
} else if (src) {
if (src > 16) { // shift left
this.flagV = result;
result = 0;
} else {
result = result << src;
this.flagC = result;
var dst = (result >> 15) & 0xffff; // check successive sign bits
if (dst && dst !== 0xffff) this.flagV = 0x8000;
}
}
this.regsGen[reg] = result & 0xffff;
this.flagN = this.flagZ = result;
this.nStepCycles -= (this.dstMode? (5 + 1) : (6 + 1)) + src;
};
/**
* opASHC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opASHC = function(opCode)
{
var src = this.readDstWord(opCode);
var reg = (opCode >> 6) & 7;
var dst = (this.regsGen[reg] << 16) | this.regsGen[reg | 1];
this.flagC = this.flagV = 0;
src &= 0x3F;
if (src & 0x20) {
src = 64 - src; // shift right
if (src > 32) src = 32;
var result = dst >> (src - 1);
this.flagC = result << 16;
result >>= 1;
if (dst & 0x80000000) result |= 0xffffffff << (32 - src);
} else {
if (src) { // shift left
result = dst << (src - 1);
this.flagC = result >> 15;
result <<= 1;
if (src > 32) src = 32;
dst = dst >> (32 - src);
if (dst) {
dst |= (0xffffffff << src) & 0xffffffff;
if (dst !== 0xffffffff) this.flagV = 0x8000;
}
} else {
result = dst;
}
}
this.regsGen[reg] = (result >> 16) & 0xffff;
this.regsGen[reg | 1] = result & 0xffff;
this.flagN = result >> 16;
this.flagZ = result >> 16 | result;
this.nStepCycles -= (this.dstMode? (5 + 1) : (6 + 1)) + src;
};
/**
* opASL(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opASL = function(opCode)
{
this.updateDstWord(opCode, 0, PDP11.fnASL);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opASLB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opASLB = function(opCode)
{
this.updateDstByte(opCode, 0, PDP11.fnASLB);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opASR(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opASR = function(opCode)
{
this.updateDstWord(opCode, 0, PDP11.fnASR);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opASRB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opASRB = function(opCode)
{
this.updateDstByte(opCode, 0, PDP11.fnASRB);
this.nStepCycles -= (this.dstMode? (8 + 1) + (this.dstAddr & 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opBCC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBCC = function(opCode)
{
this.branch(opCode, !this.getCF());
};
/**
* opBCS(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBCS = function(opCode)
{
this.branch(opCode, this.getCF());
};
/**
* opBIC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBIC = function(opCode)
{
this.updateDstWord(opCode, this.readSrcWord(opCode), PDP11.fnBIC);
this.nStepCycles -= (this.dstMode? (8 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 2) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opBICB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBICB = function(opCode)
{
this.updateDstByte(opCode, this.readSrcByte(opCode), PDP11.fnBICB);
this.nStepCycles -= (this.dstMode? (8 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 2) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opBIS(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBIS = function(opCode)
{
this.updateDstWord(opCode, this.readSrcWord(opCode), PDP11.fnBIS);
this.nStepCycles -= (this.dstMode? (8 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 2) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opBISB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBISB = function(opCode)
{
this.updateDstByte(opCode, this.readSrcByte(opCode), PDP11.fnBISB);
this.nStepCycles -= (this.dstMode? (8 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 2) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opBIT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBIT = function(opCode)
{
var src = this.readSrcWord(opCode);
var dst = this.readDstWord(opCode);
this.updateNZVFlags((src < 0? this.regsGen[-src-1] : src) & dst);
this.nStepCycles -= (this.dstMode? (3 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 1) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opBITB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBITB = function(opCode)
{
var src = this.readSrcByte(opCode);
var dst = this.readDstByte(opCode);
this.updateNZVFlags(((src < 0? (this.regsGen[-src-1] & 0xff) : src) & dst) << 8);
this.nStepCycles -= (this.dstMode? (3 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 1) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opBEQ(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBEQ = function(opCode)
{
this.branch(opCode, this.getZF());
};
/**
* opBGE(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBGE = function(opCode)
{
this.branch(opCode, !this.getNF() == !this.getVF());
};
/**
* opBGT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBGT = function(opCode)
{
this.branch(opCode, !this.getZF() && (!this.getNF() == !this.getVF()));
};
/**
* opBHI(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBHI = function(opCode)
{
this.branch(opCode, !this.getCF() && !this.getZF());
};
/**
* opBLE(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBLE = function(opCode)
{
this.branch(opCode, this.getZF() || (!this.getNF() != !this.getVF()));
};
/**
* opBLOS(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBLOS = function(opCode)
{
this.branch(opCode, this.getCF() || this.getZF());
};
/**
* opBLT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBLT = function(opCode)
{
this.branch(opCode, !this.getNF() != !this.getVF());
};
/**
* opBMI(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBMI = function(opCode)
{
this.branch(opCode, this.getNF());
};
/**
* opBNE(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBNE = function(opCode)
{
this.branch(opCode, !this.getZF());
};
/**
* opBPL(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBPL = function(opCode)
{
this.branch(opCode, !this.getNF());
};
/**
* opBPT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBPT = function(opCode)
{
this.trap(PDP11.TRAP.BPT, 0, PDP11.REASON.OPCODE);
};
/**
* opBR(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBR = function(opCode)
{
this.branch(opCode, true);
};
/**
* opBVC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBVC = function(opCode)
{
this.branch(opCode, !this.getVF());
};
/**
* opBVS(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opBVS = function(opCode)
{
this.branch(opCode, this.getVF());
};
/**
* opCLR(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCLR = function(opCode)
{
this.writeDstWord(opCode, 0, this.updateAllFlags);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opCLRB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCLRB = function(opCode)
{
this.writeDstByte(opCode, 0, PDP11.WRITE.BYTE, this.updateAllFlags);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opCLC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCLC = function(opCode)
{
this.clearCF();
this.nStepCycles -= (4 + 1);
};
/**
* opCLN(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCLN = function(opCode)
{
this.clearNF();
this.nStepCycles -= (4 + 1);
};
/**
* opCLV(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCLV = function(opCode)
{
this.clearVF();
this.nStepCycles -= (4 + 1);
};
/**
* opCLZ(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCLZ = function(opCode)
{
this.clearZF();
this.nStepCycles -= (4 + 1);
};
/**
* opCLx(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCLx = function(opCode)
{
if (opCode & 0x1) this.clearCF();
if (opCode & 0x2) this.clearVF();
if (opCode & 0x4) this.clearZF();
if (opCode & 0x8) this.clearNF();
/*
* TODO: Review whether this class of undocumented instructions really has a constant cycle time.
*/
this.nStepCycles -= (4 + 1);
};
/**
* opCMP(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCMP = function(opCode)
{
var src = this.readSrcWord(opCode);
var dst = this.readDstWord(opCode);
var result = (src = (src < 0? this.regsGen[-src-1] : src)) - dst;
/*
* NOTE: CMP calculates (src - dst) rather than (dst - src), so src and dst updateSubFlags() parms must be reversed.
*/
this.updateSubFlags(result, dst, src);
this.nStepCycles -= (this.dstMode? (3 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 1) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opCMPB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCMPB = function(opCode)
{
var src = this.readSrcByte(opCode);
var dst = this.readDstByte(opCode);
var result = (src = (src < 0? (this.regsGen[-src-1] & 0xff): src) << 8) - (dst <<= 8);
/*
* NOTE: CMP calculates (src - dst) rather than (dst - src), so src and dst updateSubFlags() parms must be reversed.
*/
this.updateSubFlags(result, dst, src);
this.nStepCycles -= (this.dstMode? (3 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 1) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opCOM(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCOM = function(opCode)
{
this.updateDstWord(opCode, 0, PDP11.fnCOM);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opCOMB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opCOMB = function(opCode)
{
this.updateDstByte(opCode, 0, PDP11.fnCOMB);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opDEC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opDEC = function(opCode)
{
this.updateDstWord(opCode, 1, PDP11.fnDEC);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opDECB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opDECB = function(opCode)
{
this.updateDstByte(opCode, 1, PDP11.fnDECB);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opDIV(opCode)
*
* The instruction "DIV SRC,Rn" determines SRC using the DSTMODE portion of the opcode and Rn using
* the SRCMODE portion; Rn can only be a register (and it should be an EVEN-numbered register, lest you
* get unexpected results). The dividend (DST) is then calculated as:
*
* DST = (regs[Rn] << 16) | (regs[Rn|1])
*
* DST is divided by SRC, and the quotient is stored in regs[Rn] and the remainder in regs[Rn|1].
*
* For example:
*
* DIV R4,R0
*
* where R4 = 006400 and R0,R1 = 000000,015000 will result in R0,R1 = 000002,000000.
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opDIV = function(opCode)
{
/*
* TODO: Review and determine if flag updates can be encapsulated in an updateDivFlags() function.
*/
var src = this.readDstWord(opCode);
if (!src) {
this.flagN = 0; // NZVC
this.flagZ = 0;
this.flagV = 0x8000;
this.flagC = 0x10000; // divide by zero
this.nStepCycles -= (6 + 1);
} else {
var reg = (opCode >> 6) & 7;
var dst = (this.regsGen[reg] << 16) | this.regsGen[reg | 1];
this.flagC = this.flagV = 0;
if (src & 0x8000) src |= ~0xffff;
var result = ~~(dst / src);
if (result >= -32768 && result <= 32767) {
this.regsGen[reg] = result & 0xffff;
this.regsGen[reg | 1] = (dst - (result * src)) & 0xffff;
this.flagZ = (result >> 16) | result;
this.flagN = result >> 16;
} else {
this.flagV = 0x8000; // overflow - following are indeterminate
this.flagZ = (result >> 15) | result; // dodgy
this.flagN = dst >> 16; // just as dodgy
if (src === -1 && this.regsGen[reg] === 0xfffe) {
this.regsGen[reg] = this.regsGen[reg | 1] = 1; // etc
}
}
this.nStepCycles -= (52 + 1); // 52 is the average of the shortest and longest times
}
};
/**
* opEMT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opEMT = function(opCode)
{
this.trap(PDP11.TRAP.EMT, 0, PDP11.REASON.OPCODE);
this.nStepCycles -= (22 + 3 - 5);
};
/**
* opHALT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opHALT = function(opCode)
{
if (this.regPSW & PDP11.PSW.CMODE) {
this.regErr |= PDP11.CPUERR.BADHALT;
this.trap(PDP11.TRAP.BUS, 0, PDP11.REASON.HALT);
} else {
if (this.panel) {
/*
* The PDP-11/20 Handbook (1971) says that HALT does the following:
*
* Causes the processor operation to cease. The console is given control of the bus.
* The console data lights display the contents of RO; the console address lights display
* the address after the halt instruction. Transfers on the UNIBUS are terminated immediately.
* The PC points to the next instruction to be executed. Pressing the continue key on the
* console causes processor operation to resume. No INIT signal is given.
*
* However, the PDP-11/70 Handbook (1979) suggests some slight differences:
*
* Causes the processor operation to cease. The console is given control of the processor.
* The data lights display the contents of the PC (which is the address of the HALT instruction
* plus 2). Transfers on the UNIBUS are terminated immediately. Pressing the continue key on
* the console causes processor operation to resume.
*
* Given that the 11/70 doesn't saying anything about displaying R0 on a HALT, and also given that
* the 11/70 CPU EXERCISER diagnostic writes a value to the Console Switch/Display Register immediately
* before HALT'ing, I'm going to assume that updating the data display with R0 is unique to the 11/20.
*
* Also, I'm a little suspicious of the 11/70 comment that the "data lights display the contents of
* the PC," since previous models display the PC on the ADDRESS lights, not the DATA lights. And as
* I already explained, doing anything to the data lights at this point would undo what the 11/70
* diagnostics do.
*/
if (this.model == PDP11.MODEL_1120) {
this.panel.setData(this.regsGen[0], true);
}
}
if (!this.dbg) {
/*
* This will leave the PC exactly where it's supposed to be: at the address of the HALT + 2.
*/
this.stopCPU();
} else {
/*
* When the Debugger is present, this call will rewind PC by 2 so that the HALT instruction is
* displayed, making it clear why the processor stopped; the user could also use the "dh" command
* to dump the Debugger's instruction history buffer to see why it stopped, assuming the history
* buffer is enabled, but that's more work.
*
* Because rewinding is not normal CPU behavior, attempting to Run again (or use the Debugger's
* "g" command) would cause an immediate HALT again -- except that checkInstruction() checks for that
* precise condition, so if the CPU starts on a HALT, checkInstruction() will skip over it.
*/
this.dbg.stopInstruction();
}
}
this.nStepCycles -= 7;
};
/**
* opINC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opINC = function(opCode)
{
this.updateDstWord(opCode, 1, PDP11.fnINC);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opINCB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opINCB = function(opCode)
{
this.updateDstByte(opCode, 1, PDP11.fnINCB);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opIOT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opIOT = function(opCode)
{
this.trap(PDP11.TRAP.IOT, 0, PDP11.REASON.OPCODE);
this.nStepCycles -= (22 + 3 - 5);
};
PDP11.JMP_CYCLES = [
0, 6 + 1, 6 + 1, 8 + 2, 6 + 1, 9 + 2, 7 + 2, 10 + 3
];
/**
* opJMP(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opJMP = function(opCode)
{
/*
* Since JMP and JSR opcodes have their own unique timings for the various dst modes, we must snapshot
* nStepCycles before decoding the mode, and then use that to update nStepCycles.
*/
this.nSnapCycles = this.nStepCycles;
this.setPC(this.readDstAddr(opCode));
this.nStepCycles = this.nSnapCycles - PDP11.JMP_CYCLES[this.dstMode];
};
PDP11.JSR_CYCLES = [
0, 13 + 1, 13 + 1, 15 + 2, 13 + 1, 16 + 2, 14 + 2, 17 + 3
];
/**
* opJSR(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opJSR = function(opCode)
{
/*
* Since JMP and JSR opcodes have their own unique timings for the various dst modes, we must
* snapshot nStepCycles before decoding the mode, and then use that to update nStepCycles.
*/
this.nSnapCycles = this.nStepCycles;
var addr = this.readDstAddr(opCode);
/*
* As per the WARNING in readSrcWord(), reading the SRC register AFTER decoding the DST operand
* is entirely appropriate.
*/
var reg = (opCode >> PDP11.SRCMODE.SHIFT) & PDP11.OPREG.MASK;
this.pushWord(this.regsGen[reg]);
this.regsGen[reg] = this.getPC();
this.setPC(addr);
this.nStepCycles = this.nSnapCycles - PDP11.JSR_CYCLES[this.dstMode];
};
/**
* opMARK(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMARK = function(opCode)
{
var addr = (this.getPC() + ((opCode & 0x3F) << 1)) & 0xffff;
var src = this.readWord(addr | this.addrDSpace);
this.setPC(this.regsGen[5]);
this.setSP(addr + 2);
this.regsGen[5] = src;
this.nStepCycles -= (6 + 2);
};
/**
* opMFPD(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMFPD = function(opCode)
{
var data = this.readWordFromPrevSpace(opCode, PDP11.ACCESS.DSPACE);
this.updateNZVFlags(data);
this.pushWord(data);
this.nStepCycles -= (10 + 1);
};
/**
* opMFPI(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMFPI = function(opCode)
{
var data = this.readWordFromPrevSpace(opCode, PDP11.ACCESS.ISPACE);
this.updateNZVFlags(data);
this.pushWord(data);
this.nStepCycles -= (10 + 1);
};
/**
* opMFPS(opCode)
*
* 1067XX MFPS - Move Byte From PSW
*
* The 8-bit contents of the PS are moved to the effective destination. If destination is mode 0,
* PS bit 7 is sign extended through the upper byte of the register. The destination operand is treated
* as a byte address. 11/34A only.
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMFPS = function(opCode)
{
PDP11.opUndefined.call(this, opCode);
};
/**
* opMFPT(opCode)
*
* 000007 MFPT - Move From Processor Type
*
* Loads R0 with a value indicating the processor type.
*
* R0 Hardware
* 1 PDP-11/44
* 3 PDP-11/24 (should be 2)
* 3 PDP-11/23
* 4 SBC-11/21
* 5 All J11 chips including 11/73, 11/83, 11/93
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMFPT = function(opCode)
{
PDP11.opUndefined.call(this, opCode);
};
PDP11.MOV_CYCLES = [
2 + 1, 8 + 1, 8 + 1, 11 + 2, 9 + 1, 12 + 2, 10 + 2, 13 + 3,
3 + 1, 8 + 1, 8 + 1, 11 + 2, 9 + 1, 12 + 2, 11 + 2, 14 + 3
];
/**
* opMOV(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMOV = function(opCode)
{
/*
* Since MOV opcodes have their own unique timings for the various dst modes, we must snapshot
* nStepCycles after decoding the src mode, and then use that to update nStepCycles.
*/
var data = this.readSrcWord(opCode);
this.nSnapCycles = this.nStepCycles;
this.writeDstWord(opCode, data, this.updateNZVFlags);
this.nStepCycles = this.nSnapCycles - PDP11.MOV_CYCLES[(this.srcMode? 8 : 0) + this.dstMode] + (this.dstReg == 7 && !this.dstMode? 2 : 0);
};
/**
* opMOVB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMOVB = function(opCode)
{
var data = this.readSrcByte(opCode);
this.writeDstByte(opCode, data, PDP11.WRITE.SBYTE, this.updateNZVFlags);
this.nStepCycles -= (this.dstMode? (8 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 2) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
PDP11.MTP_CYCLES = [
6 + 1, 11 + 2, 11 + 2, 14 + 3, 12 + 2, 15 + 3, 14 + 3, 17 + 4
];
/**
* opMTPD(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMTPD = function(opCode)
{
/*
* Since MTPD and MTPI opcodes have their own unique timings for the various dst modes, we must snapshot
* nStepCycles before decoding the mode, and then use that to update nStepCycles.
*/
var data = this.popWord();
this.nSnapCycles = this.nStepCycles;
this.updateNZVFlags(data);
this.writeWordToPrevSpace(opCode, PDP11.ACCESS.DSPACE, data);
this.nStepCycles = this.nSnapCycles - PDP11.MTP_CYCLES[this.dstMode];
};
/**
* opMTPI(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMTPI = function(opCode)
{
/*
* Since MTPD and MTPI opcodes have their own unique timings for the various dst modes, we must snapshot
* nStepCycles before decoding the mode, and then use that to update nStepCycles.
*/
var data = this.popWord();
this.nSnapCycles = this.nStepCycles;
this.updateNZVFlags(data);
this.writeWordToPrevSpace(opCode, PDP11.ACCESS.ISPACE, data);
this.nStepCycles = this.nSnapCycles - PDP11.MTP_CYCLES[this.dstMode];
};
/**
* opMTPS(opCode)
*
* 1064XX MTPS - Move Byte To PSW
*
* The 8 bits of the effective operand replace the current contents of the PS <0:7>. The source operand
* address is treated as a byte address. Note that PS bit 4 cannot be set with this instruction. The
* src operand remains unchanged. 11/34A only.
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMTPS = function(opCode)
{
PDP11.opUndefined.call(this, opCode);
};
/**
* opMUL(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opMUL = function(opCode)
{
var src = this.readDstWord(opCode);
var reg = (opCode >> 6) & 7;
var dst = this.regsGen[reg];
var result = ((src << 16) >> 16) * ((dst << 16) >> 16);
this.regsGen[reg] = (result >> 16) & 0xffff;
this.regsGen[reg | 1] = result & 0xffff;
this.updateMulFlags(result|0);
this.nStepCycles -= (22 + 1);
};
/**
* opNEG(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opNEG = function(opCode)
{
this.updateDstWord(opCode, 0, PDP11.fnNEG);
this.nStepCycles -= (this.dstMode? (10 + 1) : (5 + 1));
};
/**
* opNEGB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opNEGB = function(opCode)
{
this.updateDstByte(opCode, 0, PDP11.fnNEGB);
this.nStepCycles -= (this.dstMode? (10 + 1) : (5 + 1));
};
/**
* opNOP(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opNOP = function(opCode)
{
this.nStepCycles -= (4 + 1); // TODO: Review (this is just a guess based on CLC)
};
/**
* opRESET(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opRESET = function(opCode)
{
if (!(this.regPSW & PDP11.PSW.CMODE)) {
this.resetCPU();
if (this.panel) {
/*
* The PDP-11/70 XXDP test "EKBBF0" reports the following, with PANEL messages on ("m panel on"):
*
* CNSW.writeWord(177570,000101) @033502
* CNSW.readWord(177570): 000000 @032114
* LOOK AT THE CONSOLE LIGHTS
* THE DATA LIGHTS SHOULD READ 166667
* THE ADDRESS LIGHTS SHOULD READ CNSW.readWord(177570): 000000 @032150
* 032236
* CHANGE SWITCH 7 TO CONTINUE
* CNSW.readWord(177570): 000000 @032236
* stopped (31518011 instructions, 358048873 cycles, 58644 ms, 6105465 hz)
* R0=166667 R1=002362 R2=000000 R3=000000 R4=000000 R5=026642
* SP=001074 PC=032236 PS=000344 SR=00000000 T0 N0 Z1 V0 C0
* 032236: 032737 000200 177570 BIT #200,@#177570
* >> tr
* CNSW.readWord(177570): 000000 @032236 (cpu halted)
* R0=166667 R1=002362 R2=000000 R3=000000 R4=000000 R5=026642
* SP=001074 PC=032244 PS=000344 SR=00000000 T0 N0 Z1 V0 C0
* 032244: 001773 BEQ 032234 ;cycles=0
* >> tr
* R0=166667 R1=002362 R2=000000 R3=000000 R4=000000 R5=026642
* SP=001074 PC=032234 PS=000344 SR=00000000 T0 N0 Z1 V0 C0
* 032234: 000005 RESET ;cycles=5
*
* It's a little hard to see why the DATA lights should read 166667, since the PANEL messages indicate
* that the last CNSW.writeWord(177570) was for 000101, not 166667. So I'm guessing that the RESET
* instruction is supposed to propagate R0 to the console's DISPLAY register.
*
* This is similar to what we do for the HALT instruction (but only if this.model == PDP11.MODEL_1120).
* These Console features do not seem to be very well documented, assuming they exist.
*
* UPDATE: This behavior appears to be confirmed by remarks in the PDP-11/20 Processor Handbook (1971),
* p. 141:
*
* HALT - displays processor register R0 when bus control is transferred to console during a HALT
* instruction.
*
* RESET - displays register R0 for during [duration?] of RESET (70 msec).
*
* I haven't found similar remarks in the PDP-11/70 Processor Handbooks, so I'm not sure if that's an
* oversight or if 11/70 panels are slightly different in this regard. It's also not clear what they meant
* by "for duration of RESET". Is something supposed to happen to the DATA lights after the RESET is done?
*/
this.panel.setData(this.regsGen[0], true);
}
}
this.nStepCycles -= 667; // TODO: Review (but it's definitely a big number)
};
/**
* opROL(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opROL = function(opCode)
{
this.updateDstWord(opCode, 0, PDP11.fnROL);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opROLB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opROLB = function(opCode)
{
this.updateDstByte(opCode, 0, PDP11.fnROLB);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opROR(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opROR = function(opCode)
{
this.updateDstWord(opCode, 0, PDP11.fnROR);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opRORB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opRORB = function(opCode)
{
this.updateDstByte(opCode, 0, PDP11.fnRORB);
this.nStepCycles -= (this.dstMode? (8 + 1) + (this.dstAddr & 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opRTI(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opRTI = function(opCode)
{
this.trapReturn();
/*
* Unlike RTT, RTI permits an immediate trace, which we resolve by propagating PSW.TF to OPFLAG.TRAP_TF
* (which, as written below, requires that both flags have the same bit value; see defines.js).
*
* NOTE: This RTI trace behavior is NEW for machines that have both RTI and RTT. Early models didn't have RTT,
* so the old RTI behaved exactly like the new RTT. Which is why the 11/20 jump table below calls opRTT() instead
* of opRTI() for RTI.
*/
this.opFlags |= (this.regPSW & PDP11.PSW.TF);
this.nStepCycles -= (10 + 3);
};
/**
* opRTS(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opRTS = function(opCode)
{
if (opCode & 0x08) {
PDP11.opUndefined.call(this, opCode);
return;
}
var src = this.popWord();
var reg = opCode & PDP11.OPREG.MASK;
/*
* When the popular "RTS PC" form is used, we might as well eliminate the useless setting of PC...
*/
if (reg == PDP11.REG.PC) {
this.setPC(src);
} else {
this.setPC(this.regsGen[reg]);
this.regsGen[reg] = src;
}
this.nStepCycles -= (7 + 2);
};
/**
* opRTT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opRTT = function(opCode)
{
this.trapReturn();
this.nStepCycles -= (10 + 3);
};
/**
* opSBC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSBC = function(opCode)
{
this.updateDstWord(opCode, this.getCF()? 1 : 0, PDP11.fnSUB);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opSBCB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSBCB = function(opCode)
{
this.updateDstByte(opCode, this.getCF()? 1 : 0, PDP11.fnSUBB);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opSEC(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSEC = function(opCode)
{
this.setCF();
this.nStepCycles -= (4 + 1);
};
/**
* opSEN(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSEN = function(opCode)
{
this.setNF();
this.nStepCycles -= (4 + 1);
};
/**
* opSEV(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSEV = function(opCode)
{
this.setVF();
this.nStepCycles -= (4 + 1);
};
/**
* opSEZ(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSEZ = function(opCode)
{
this.setZF();
this.nStepCycles -= (4 + 1);
};
/**
* opSEx(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSEx = function(opCode)
{
if (opCode & 0x1) this.setCF();
if (opCode & 0x2) this.setVF();
if (opCode & 0x4) this.setZF();
if (opCode & 0x8) this.setNF();
/*
* TODO: Review whether this class of undocumented instructions really has a constant cycle time.
*/
this.nStepCycles -= (4 + 1);
};
/**
* opSOB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode (077Rnn)
*/
PDP11.opSOB = function(opCode)
{
var reg = (opCode & PDP11.SRCMODE.REG) >> PDP11.SRCMODE.SHIFT;
if ((this.regsGen[reg] = ((this.regsGen[reg] - 1) & 0xffff))) {
this.setPC(this.getPC() - ((opCode & PDP11.DSTMODE.MASK) << 1));
this.nStepCycles += 1; // unlike normal branches, taking this branch is actually 1 cycle faster
}
this.nStepCycles -= (5 + 1);
};
/**
* opSPL(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSPL = function(opCode)
{
if (!(opCode & 0x08) || this.model < PDP11.MODEL_1145) {
PDP11.opUndefined.call(this, opCode);
return;
}
if (!(this.regPSW & PDP11.PSW.CMODE)) {
this.regPSW = (this.regPSW & ~PDP11.PSW.PRI) | ((opCode & 0x7) << PDP11.PSW.SHIFT.PRI);
this.opFlags |= PDP11.OPFLAG.IRQ_DELAY;
this.opFlags &= ~PDP11.OPFLAG.IRQ;
}
this.nStepCycles -= (4 + 1);
};
/**
* opSUB(opCode)
*
* From the PDP-11/20 Processor HandBook (1971), p. 62:
*
* Subtract src,dst (16SSDD)
*
* Operation:
* (dst) = (dst) - (src) [in detail, (dst) + ~(src) + 1 (dst)]
*
* Condition Codes:
* N: set if result < 0; cleared otherwise
* Z: set if result = 0; cleared otherwise
* V: set if there was arithmetic overflow as a result of the operation, that is if operands were of
* opposite signs and the sign of the source was the same as the sign of the result; cleared otherwise
* C: cleared if there was a carry from the most significant bit of the result; set otherwise
*
* Description:
* Subtracts the source operand from the destination operand and leaves the result at the destination address.
* The orignial [sic] contents of the destination are lost. The contents of the source are not affected.
* In double-precision arithmetic the C-bit, when set, indicates a "borrow".
*
* Example:
* SUB R1,R2
*
* BEFORE AFTER
* (R1) = 011111 (R2) = 012345
* (R1) = 011111 (R2) = 001234
*
* NZVC NZVC
* 1111 0001
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSUB = function(opCode)
{
this.updateDstWord(opCode, this.readSrcWord(opCode), PDP11.fnSUB);
this.nStepCycles -= (this.dstMode? (8 + 1) + (this.srcReg && this.dstReg >= 6? 1 : 0) : (this.srcMode? (3 + 2) : (2 + 1)) + (this.dstReg == 7? 2 : 0));
};
/**
* opSWAB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSWAB = function(opCode)
{
this.updateDstWord(opCode, 0, PDP11.fnSWAB);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opSXT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opSXT = function(opCode)
{
this.writeDstWord(opCode, this.getNF()? 0xffff : 0, this.updateNZVFlags);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opTRAP(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opTRAP = function(opCode)
{
this.trap(PDP11.TRAP.TRAP, 0, PDP11.REASON.OPCODE);
};
/**
* opTST(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opTST = function(opCode)
{
var result = this.readDstWord(opCode);
this.updateAllFlags(result);
this.nStepCycles -= (this.dstMode? (3 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opTSTB(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opTSTB = function(opCode)
{
var result = this.readDstByte(opCode);
this.updateAllFlags(result << 8);
this.nStepCycles -= (this.dstMode? (3 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opWAIT(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opWAIT = function(opCode)
{
/*
* The original PDP-11 emulation code would actually stop emulating instructions now, relying on assorted
* setTimeout() callbacks, setInterval() callbacks, device XHR (XMLHttpRequest) callbacks, etc, to eventually
* call interrupt(), which would then transition the CPU out of its "wait" state and kickstart emulate() again.
*
* That approach isn't compatible with PCjs emulators, which prefer to rely on the simulated CPU clock to
* drive all simulated device updates. This means components should call the CPU's setTimer() function, which
* invokes the provided callback when the number of CPU cycles that correspond to the requested number of
* milliseconds have elapsed. This also gives us the ability to scale device response times as needed if the
* user decides to crank up CPU speed, and to freeze them along with the CPU whenever the user halts the machine.
*
* However, the PCjs approach requires the CPU to continue running. One simple solution to this dilemma:
*
* 1) opWAIT() sets a new opFlags bit (OPFLAG.WAIT)
* 2) Rewind the PC back to the WAIT instruction
* 3) Whenever stepCPU() detects OPFLAG.WAIT, call checkInterrupts()
* 4) If checkInterrupts() detects an interrupt, advance PC past the WAIT and then dispatch the interrupt
*
* Technically, the PC is already exactly where it's supposed to be, so why are we wasting time with steps
* 2 and 4? It's largely for the Debugger's sake, so that as long as execution is "blocked" by a WAIT, that's
* what you'll see in the Debugger. I could make those steps conditioned on the presence of the Debugger,
* but I feel it's better to keep all code paths the same.
*
* NOTE: It's almost always a bad idea to add more checks to the inner stepCPU() loop, because every additional
* check can have a measurable (negative) impact on performance. Which is why it's important to use opFlags bits
* whenever possible, since we can test for multiple (up to 32) exceptional conditions with a single check.
*
* We also used to update the machine's display(s) whenever transitioning to the WAIT state. However, that
* caused this instruction to generate enormous overhead, and it's no longer necessary, since we now rely on
* a timer (the PDP-11's own KW11 60Hz Line Clock timer, to be precise) to generate periodic display updates.
*
* if (!(this.opFlags & PDP11.OPFLAG.WAIT) && this.cmp) this.cmp.updateDisplays();
*
* Finally, it's been noted several places online that the WAIT instruction puts the contents of R0 into the
* Front Panel's "DATA PATH" (and possibly even directly into the "DISPLAY REGISTER", making the DATASEL switch
* setting irrelevant). I can't find any supporting DEC documentation regarding this, but for now, we'll go
* with popular lore and propagate R0 to the panel's "active" data register.
*/
if (this.panel) {
this.panel.setAddr(this.regsGen[7], true);
this.panel.setData(this.regsGen[0], true);
}
this.opFlags |= PDP11.OPFLAG.WAIT;
this.advancePC(-2);
this.nStepCycles -= 3;
};
/**
* opXOR(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opXOR = function(opCode)
{
var reg = (opCode >> PDP11.SRCMODE.SHIFT) & PDP11.OPREG.MASK;
this.updateDstWord(opCode, this.regsGen[reg + this.offRegSrc], PDP11.fnXOR);
this.nStepCycles -= (this.dstMode? (8 + 1) : (2 + 1) + (this.dstReg == 7? 2 : 0));
};
/**
* opUndefined(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.opUndefined = function(opCode)
{
if (DEBUGGER && this.dbg) {
if (this.dbg.undefinedInstruction(opCode)) return;
}
this.trap(PDP11.TRAP.RESERVED, 0, PDP11.REASON.OPCODE);
};
/**
* op1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op1120 = function(opCode)
{
PDP11.aOpXnnn_1120[opCode >> 12].call(this, opCode);
};
/**
* op0Xnn_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op0Xnn_1120 = function(opCode)
{
PDP11.aOp0Xnn_1120[(opCode >> 8) & 0xf].call(this, opCode);
};
/**
* op0AXn_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op0AXn_1120 = function(opCode)
{
PDP11.aOp0AXn_1120[(opCode >> 6) & 0x3].call(this, opCode);
};
/**
* op0BXn_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op0BXn_1120 = function(opCode)
{
PDP11.aOp0BXn_1120[(opCode >> 6) & 0x3].call(this, opCode);
};
/**
* op0CXn_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op0CXn_1120 = function(opCode)
{
PDP11.aOp0CXn_1120[(opCode >> 6) & 0x3].call(this, opCode);
};
/**
* op00Xn_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op00Xn_1120 = function(opCode)
{
PDP11.aOp00Xn_1120[(opCode >> 4) & 0xf].call(this, opCode);
};
/**
* op00AX_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op00AX_1120 = function(opCode)
{
PDP11.aOp00AX_1120[opCode & 0xf].call(this, opCode);
};
/**
* op00BX_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op00BX_1120 = function(opCode)
{
PDP11.aOp00BX_1120[opCode & 0xf].call(this, opCode);
};
/**
* op000X_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op000X_1120 = function(opCode)
{
PDP11.aOp000X_1120[opCode & 0xf].call(this, opCode);
};
/**
* op8Xnn_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op8Xnn_1120 = function(opCode)
{
PDP11.aOp8Xnn_1120[(opCode >> 8) & 0xf].call(this, opCode);
};
/**
* op8AXn_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op8AXn_1120 = function(opCode)
{
PDP11.aOp8AXn_1120[(opCode >> 6) & 0x3].call(this, opCode);
};
/**
* op8BXn_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op8BXn_1120 = function(opCode)
{
PDP11.aOp8BXn_1120[(opCode >> 6) & 0x3].call(this, opCode);
};
/**
* op8CXn_1120(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op8CXn_1120 = function(opCode)
{
PDP11.aOp8CXn_1120[(opCode >> 6) & 0x3].call(this, opCode);
};
PDP11.aOpXnnn_1120 = [
PDP11.op0Xnn_1120, // 0x0nnn
PDP11.opMOV, // 0x1nnn 01SSDD 11/20+ 2.3
PDP11.opCMP, // 0x2nnn 02SSDD 11/20+ 2.3*
PDP11.opBIT, // 0x3nnn 03SSDD 11/20+ 2.9*
PDP11.opBIC, // 0x4nnn 04SSDD 11/20+ 2.9
PDP11.opBIS, // 0x5nnn 05SSDD 11/20+ 2.3
PDP11.opADD, // 0x6nnn 06SSDD 11/20+ 2.3
PDP11.opUndefined, // 0x7nnn
PDP11.op8Xnn_1120, // 0x8nnn
PDP11.opMOVB, // 0x9nnn 11SSDD 11/20+ 2.3
PDP11.opCMPB, // 0xAnnn 12SSDD 11/20+ 2.3
PDP11.opBITB, // 0xBnnn 13SSDD 11/20+ 2.9
PDP11.opBICB, // 0xCnnn 14SSDD 11/20+ 2.9
PDP11.opBISB, // 0xDnnn 15SSDD 11/20+ 2.3
PDP11.opSUB, // 0xEnnn 16SSDD 11/20+ 2.3
PDP11.opUndefined // 0xFnnn
];
PDP11.aOp0Xnn_1120 = [
PDP11.op00Xn_1120, // 0x00nn
PDP11.opBR, // 0x01nn 0004XX 11/20+ 2.6
PDP11.opBNE, // 0x02nn 0010XX 11/20+ 2.6**
PDP11.opBEQ, // 0x03nn 0014XX 11/20+ 2.6**
PDP11.opBGE, // 0x04nn 0020XX 11/20+ 2.6**
PDP11.opBLT, // 0x05nn 0024XX 11/20+ 2.6**
PDP11.opBGT, // 0x06nn 0030XX 11/20+ 2.6**
PDP11.opBLE, // 0x07nn 0034XX 11/20+ 2.6**
PDP11.opJSR, // 0x08nn 004RDD 11/20+ 4.4
PDP11.opJSR, // 0x09nn 004RDD 11/20+ 4.4
PDP11.op0AXn_1120, // 0x0Ann
PDP11.op0BXn_1120, // 0x0Bnn
PDP11.op0CXn_1120, // 0x0Cnn
PDP11.opUndefined, // 0x0Dnn
PDP11.opUndefined, // 0x0Enn
PDP11.opUndefined // 0x0Fnn
];
PDP11.aOp0AXn_1120 = [
PDP11.opCLR, // 0x0A0n 0050DD 11/20+ 2.3
PDP11.opCOM, // 0x0A4n 0051DD 11/20+ 2.3
PDP11.opINC, // 0x0A8n 0052DD 11/20+ 2.3
PDP11.opDEC // 0x0ACn 0053DD 11/20+ 2.3
];
PDP11.aOp0BXn_1120 = [
PDP11.opNEG, // 0x0B0n 0054DD 11/20+ 2.3
PDP11.opADC, // 0x0B4n 0055DD 11/20+ 2.3
PDP11.opSBC, // 0x0B8n 0056DD 11/20+ 2.3
PDP11.opTST // 0x0BCn 0057DD 11/20+ 2.3*
];
PDP11.aOp0CXn_1120 = [
PDP11.opROR, // 0x0C0n 0060DD 11/20+ 2.3*
PDP11.opROL, // 0x0C4n 0061DD 11/20+ 2.3*
PDP11.opASR, // 0x0C8n 0062DD 11/20+ 2.3*
PDP11.opASL // 0x0CCn 0063DD 11/20+ 2.3*
];
PDP11.aOp00Xn_1120 = [
PDP11.op000X_1120, // 0x000n 000000-000017
PDP11.opUndefined, // 0x001n 000020-000037
PDP11.opUndefined, // 0x002n 000040-000057
PDP11.opUndefined, // 0x003n 000060-000077
PDP11.opJMP, // 0x004n 0001DD 11/20+ 1.2
PDP11.opJMP, // 0x005n 0001DD 11/20+ 1.2
PDP11.opJMP, // 0x006n 0001DD 11/20+ 1.2
PDP11.opJMP, // 0x007n 0001DD 11/20+ 1.2
PDP11.opRTS, // 0x008n 00020R 11/20+ 3.5 (opRTS() will also confirm that bit 3 is clear)
PDP11.opUndefined, // 0x009n 00023N
PDP11.op00AX_1120, // 0x00An 000240-000257
PDP11.op00BX_1120, // 0x00Bn 000260-000277
PDP11.opSWAB, // 0x00Cn 0003DD 11/20+ 2.3
PDP11.opSWAB, // 0x00Dn 0003DD 11/20+ 2.3
PDP11.opSWAB, // 0x00En 0003DD 11/20+ 2.3
PDP11.opSWAB // 0x00Fn 0003DD 11/20+ 2.3
];
PDP11.aOp000X_1120 = [
PDP11.opHALT, // 0x0000 000000 11/20+ 1.8
PDP11.opWAIT, // 0x0001 000001 11/20+ 1.8
PDP11.opRTT, // 0x0002 000002 11/20+ 4.8 (this is really RTI, but on the 11/20, it behaves like RTT)
PDP11.opBPT, // 0x0003
PDP11.opIOT, // 0x0004 000004 11/20+ 9.3
PDP11.opRESET, // 0x0005 000005 11/20+ 20ms
PDP11.opUndefined, // 0x0006
PDP11.opUndefined, // 0x0007
PDP11.opUndefined, // 0x0008
PDP11.opUndefined, // 0x0009
PDP11.opUndefined, // 0x000A
PDP11.opUndefined, // 0x000B
PDP11.opUndefined, // 0x000C
PDP11.opUndefined, // 0x000D
PDP11.opUndefined, // 0x000E
PDP11.opUndefined // 0x000F
];
PDP11.aOp00AX_1120 = [
PDP11.opNOP, // 0x00A0 000240 11/20+ 1.5
PDP11.opCLC, // 0x00A1 000241 11/20+ 1.5
PDP11.opCLV, // 0x00A2 000242 11/20+ 1.5
PDP11.opCLx, // 0x00A3 000243 11/20+ 1.5
PDP11.opCLZ, // 0x00A4 000244 11/20+ 1.5
PDP11.opCLx, // 0x00A5 000245 11/20+ 1.5
PDP11.opCLx, // 0x00A6 000246 11/20+ 1.5
PDP11.opCLx, // 0x00A7 000247 11/20+ 1.5
PDP11.opCLN, // 0x00A8 000250 11/20+ 1.5
PDP11.opCLx, // 0x00A9 000251 11/20+ 1.5
PDP11.opCLx, // 0x00AA 000252 11/20+ 1.5
PDP11.opCLx, // 0x00AB 000253 11/20+ 1.5
PDP11.opCLx, // 0x00AC 000254 11/20+ 1.5
PDP11.opCLx, // 0x00AD 000255 11/20+ 1.5
PDP11.opCLx, // 0x00AE 000256 11/20+ 1.5
PDP11.opCLx // 0x00AF 000257 11/20+ 1.5
];
PDP11.aOp00BX_1120 = [
PDP11.opNOP, // 0x00B0 000260 11/20+ 1.5
PDP11.opSEC, // 0x00B1 000261 11/20+ 1.5
PDP11.opSEV, // 0x00B2 000262 11/20+ 1.5
PDP11.opSEx, // 0x00B3 000263 11/20+ 1.5
PDP11.opSEZ, // 0x00B4 000264 11/20+ 1.5
PDP11.opSEx, // 0x00B5 000265 11/20+ 1.5
PDP11.opSEx, // 0x00B6 000266 11/20+ 1.5
PDP11.opSEx, // 0x00B7 000267 11/20+ 1.5
PDP11.opSEN, // 0x00B8 000270 11/20+ 1.5
PDP11.opSEx, // 0x00B9 000271 11/20+ 1.5
PDP11.opSEx, // 0x00BA 000272 11/20+ 1.5
PDP11.opSEx, // 0x00BB 000273 11/20+ 1.5
PDP11.opSEx, // 0x00BC 000274 11/20+ 1.5
PDP11.opSEx, // 0x00BD 000275 11/20+ 1.5
PDP11.opSEx, // 0x00BE 000276 11/20+ 1.5
PDP11.opSEx // 0x00BF 000277 11/20+ 1.5
];
PDP11.aOp8Xnn_1120 = [
PDP11.opBPL, // 0x80nn 1000XX 11/20+ 2.6**
PDP11.opBMI, // 0x81nn 1004XX 11/20+ 2.6**
PDP11.opBHI, // 0x82nn 1010XX 11/20+ 2.6**
PDP11.opBLOS, // 0x83nn 1014XX 11/20+ 2.6**
PDP11.opBVC, // 0x84nn 1020XX 11/20+ 2.6**
PDP11.opBVS, // 0x85nn 1024XX 11/20+ 2.6**
PDP11.opBCC, // 0x86nn 1030XX 11/20+ 2.6**
PDP11.opBCS, // 0x87nn 1034XX 11/20+ 2.6**
PDP11.opEMT, // 0x88nn 104000-104377 11/20+ 9.3
PDP11.opTRAP, // 0x89nn 104400-104777 11/20+ 9.3
PDP11.op8AXn_1120, // 0x8Ann
PDP11.op8BXn_1120, // 0x8Bnn
PDP11.op8CXn_1120, // 0x8Cnn
PDP11.opUndefined, // 0x8Dnn
PDP11.opUndefined, // 0x8Enn
PDP11.opUndefined // 0x8Fnn
];
PDP11.aOp8AXn_1120 = [
PDP11.opCLRB, // 0x8A0n 1050DD 11/20+ 2.3
PDP11.opCOMB, // 0x8A4n 1051DD 11/20+ 2.3
PDP11.opINCB, // 0x8A8n 1052DD 11/20+ 2.3
PDP11.opDECB // 0x8ACn 1053DD 11/20+ 2.3
];
PDP11.aOp8BXn_1120 = [
PDP11.opNEGB, // 0x8B0n 1054DD 11/20+ 2.3
PDP11.opADCB, // 0x8B4n 1055DD 11/20+ 2.3
PDP11.opSBCB, // 0x8B8n 1056DD 11/20+ 2.3
PDP11.opTSTB // 0x8BCn 1057DD 11/20+ 2.3*
];
PDP11.aOp8CXn_1120 = [
PDP11.opRORB, // 0x8C0n 1060DD 11/20+ 2.3*
PDP11.opROLB, // 0x8C4n 1061DD 11/20+ 2.3*
PDP11.opASRB, // 0x8C8n 1062DD 11/20+ 2.3*
PDP11.opASLB // 0x8CCn 1063DD 11/20+ 2.3*
];
/**
* op1140(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op1140 = function(opCode)
{
PDP11.aOpXnnn_1140[opCode >> 12].call(this, opCode);
};
/**
* op0Xnn_1140(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op0Xnn_1140 = function(opCode)
{
PDP11.aOp0Xnn_1140[(opCode >> 8) & 0xf].call(this, opCode);
};
/**
* op0DXn_1140(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op0DXn_1140 = function(opCode)
{
PDP11.aOp0DXn_1140[(opCode >> 6) & 0x3].call(this, opCode);
};
/**
* op00Xn_1140(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op00Xn_1140 = function(opCode)
{
PDP11.aOp00Xn_1140[(opCode >> 4) & 0xf].call(this, opCode);
};
/**
* op000X_1140(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op000X_1140 = function(opCode)
{
PDP11.aOp000X_1140[opCode & 0xf].call(this, opCode);
};
/**
* op7Xnn_1140(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op7Xnn_1140 = function(opCode)
{
PDP11.aOp7Xnn_1140[(opCode >> 8) & 0xf].call(this, opCode);
};
/**
* op8Xnn_1140(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op8Xnn_1140 = function(opCode)
{
PDP11.aOp8Xnn_1140[(opCode >> 8) & 0xf].call(this, opCode);
};
/**
* op8DXn_1140(opCode)
*
* @this {CPUStatePDP11}
* @param {number} opCode
*/
PDP11.op8DXn_1140 = function(opCode)
{
if (this.model < PDP11.MODEL_1145) {
PDP11.opUndefined.call(this, opCode);
return;
}
PDP11.aOp8DXn_1140[(opCode >> 6) & 0x3].call(this, opCode);
};
PDP11.aOpXnnn_1140 = [
PDP11.op0Xnn_1140, // 0x0nnn
PDP11.opMOV, // 0x1nnn 01SSDD 11/20+ 2.3
PDP11.opCMP, // 0x2nnn 02SSDD 11/20+ 2.3*
PDP11.opBIT, // 0x3nnn 03SSDD 11/20+ 2.9*
PDP11.opBIC, // 0x4nnn 04SSDD 11/20+ 2.9
PDP11.opBIS, // 0x5nnn 05SSDD 11/20+ 2.3
PDP11.opADD, // 0x6nnn 06SSDD 11/20+ 2.3
PDP11.op7Xnn_1140, // 0x7nnn
PDP11.op8Xnn_1140, // 0x8nnn
PDP11.opMOVB, // 0x9nnn 11SSDD 11/20+ 2.3
PDP11.opCMPB, // 0xAnnn 12SSDD 11/20+ 2.3
PDP11.opBITB, // 0xBnnn 13SSDD 11/20+ 2.9
PDP11.opBICB, // 0xCnnn 14SSDD 11/20+ 2.9
PDP11.opBISB, // 0xDnnn 15SSDD 11/20+ 2.3
PDP11.opSUB, // 0xEnnn 16SSDD 11/20+ 2.3
PDP11.opUndefined // 0xFnnn
];
PDP11.aOp0Xnn_1140 = [
PDP11.op00Xn_1140, // 0x00nn
PDP11.opBR, // 0x01nn 0004XX 11/20+ 2.6
PDP11.opBNE, // 0x02nn 0010XX 11/20+ 2.6**
PDP11.opBEQ, // 0x03nn 0014XX 11/20+ 2.6**
PDP11.opBGE, // 0x04nn 0020XX 11/20+ 2.6**
PDP11.opBLT, // 0x05nn 0024XX 11/20+ 2.6**
PDP11.opBGT, // 0x06nn 0030XX 11/20+ 2.6**
PDP11.opBLE, // 0x07nn 0034XX 11/20+ 2.6**
PDP11.opJSR, // 0x08nn 004RDD 11/20+ 4.4
PDP11.opJSR, // 0x09nn 004RDD 11/20+ 4.4
PDP11.op0AXn_1120, // 0x0Ann
PDP11.op0BXn_1120, // 0x0Bnn
PDP11.op0CXn_1120, // 0x0Cnn
PDP11.op0DXn_1140, // 0x0Dnn
PDP11.opUndefined, // 0x0Enn
PDP11.opUndefined // 0x0Fnn
];
PDP11.aOp0DXn_1140 = [
PDP11.opMARK, // 0x0D0n 11/40+ LEIS
PDP11.opMFPI, // 0x0D4n 11/40+
PDP11.opMTPI, // 0x0D8n 11/40+
PDP11.opSXT // 0x0DCn 11/40+ LEIS
];
PDP11.aOp00Xn_1140 = [
PDP11.op000X_1140, // 0x000n 000000-000017
PDP11.opUndefined, // 0x001n 000020-000037
PDP11.opUndefined, // 0x002n 000040-000057
PDP11.opUndefined, // 0x003n 000060-000077
PDP11.opJMP, // 0x004n 0001DD 11/20+ 1.2
PDP11.opJMP, // 0x005n 0001DD 11/20+ 1.2
PDP11.opJMP, // 0x006n 0001DD 11/20+ 1.2
PDP11.opJMP, // 0x007n 0001DD 11/20+ 1.2
PDP11.opRTS, // 0x008n 00020R 11/20+ 3.5 (opRTS() will also confirm that bit 3 is clear)
PDP11.opSPL, // 0x009n 00023N 11/45+ (opSPL() will also confirm that bit 3 is set)
PDP11.op00AX_1120, // 0x00An 000240-000257
PDP11.op00BX_1120, // 0x00Bn 000260-000277
PDP11.opSWAB, // 0x00Cn 0003DD 11/20+ 2.3
PDP11.opSWAB, // 0x00Dn 0003DD 11/20+ 2.3
PDP11.opSWAB, // 0x00En 0003DD 11/20+ 2.3
PDP11.opSWAB // 0x00Fn 0003DD 11/20+ 2.3
];
PDP11.aOp000X_1140 = [
PDP11.opHALT, // 0x0000 000000 11/20+ 1.8
PDP11.opWAIT, // 0x0001 000001 11/20+ 1.8
PDP11.opRTI, // 0x0002 000002 11/20+ 4.8
PDP11.opBPT, // 0x0003 000003
PDP11.opIOT, // 0x0004 000004 11/20+ 9.3
PDP11.opRESET, // 0x0005 000005 11/20+ 20ms
PDP11.opRTT, // 0x0006 000006 11/40+ LEIS
PDP11.opMFPT, // 0x0007 000007 11/44+
PDP11.opUndefined, // 0x0008
PDP11.opUndefined, // 0x0009
PDP11.opUndefined, // 0x000A
PDP11.opUndefined, // 0x000B
PDP11.opUndefined, // 0x000C
PDP11.opUndefined, // 0x000D
PDP11.opUndefined, // 0x000E
PDP11.opUndefined // 0x000F
];
PDP11.aOp7Xnn_1140 = [
PDP11.opMUL, // 0x70nn 11/40+ EIS
PDP11.opMUL, // 0x71nn 11/40+ EIS
PDP11.opDIV, // 0x72nn 11/40+ EIS
PDP11.opDIV, // 0x73nn 11/40+ EIS
PDP11.opASH, // 0x74nn 11/40+ EIS
PDP11.opASH, // 0x75nn 11/40+ EIS
PDP11.opASHC, // 0x76nn 11/40+ EIS
PDP11.opASHC, // 0x77nn 11/40+ EIS
PDP11.opXOR, // 0x78nn 11/40+ LEIS
PDP11.opXOR, // 0x79nn 11/40+ LEIS
PDP11.opUndefined, // 0x7Ann
PDP11.opUndefined, // 0x7Bnn
PDP11.opUndefined, // 0x7Cnn
PDP11.opUndefined, // 0x7Dnn
PDP11.opSOB, // 0x7Enn 11/40+ LEIS
PDP11.opSOB // 0x7Fnn 11/40+ LEIS
];
PDP11.aOp8Xnn_1140 = [
PDP11.opBPL, // 0x80nn 1000XX 11/20+ 2.6**
PDP11.opBMI, // 0x81nn 1004XX 11/20+ 2.6**
PDP11.opBHI, // 0x82nn 1010XX 11/20+ 2.6**
PDP11.opBLOS, // 0x83nn 1014XX 11/20+ 2.6**
PDP11.opBVC, // 0x84nn 1020XX 11/20+ 2.6**
PDP11.opBVS, // 0x85nn 1024XX 11/20+ 2.6**
PDP11.opBCC, // 0x86nn 1030XX 11/20+ 2.6**
PDP11.opBCS, // 0x87nn 1034XX 11/20+ 2.6**
PDP11.opEMT, // 0x88nn 104000-104377 11/20+ 9.3
PDP11.opTRAP, // 0x89nn 104400-104777 11/20+ 9.3
PDP11.op8AXn_1120, // 0x8Ann 1050XX
PDP11.op8BXn_1120, // 0x8Bnn 1054XX
PDP11.op8CXn_1120, // 0x8Cnn 1060XX
PDP11.op8DXn_1140, // 0x8Dnn 106400-106777
PDP11.opUndefined, // 0x8Enn 1070XX
PDP11.opUndefined // 0x8Fnn 1074XX
];
PDP11.aOp8DXn_1140 = [
PDP11.opMTPS, // 0x8D0n 1064XX 11/34A only
PDP11.opMFPD, // 0x8D4n 1065XX 11/45+
PDP11.opMTPD, // 0x8D8n 1066XX 11/45+
PDP11.opMFPS // 0x8DCn 1067XX 11/34A only
];
/**
* @copyright http://pcjs.org/modules/pdp11/lib/rom.js (C) Jeff Parsons 2012-2017
*/
class ROMPDP11 extends Component {
/**
* ROMPDP11(parmsROM)
*
* The ROMPDP11 component expects the following (parmsROM) properties:
*
* addr: physical address of ROM
* size: amount of ROM, in bytes
* alias: physical alias address (null if none)
* file: name of ROM data file
*
* NOTE: The ROM data will not be copied into place until the Bus is ready (see initBus()) AND
* the ROM data file has finished loading (see finishLoad()).
*
* Also, while the size parameter may seem redundant, I consider it useful to confirm that the ROM
* you received is the ROM you expected.
*
* @param {Object} parmsROM
*/
constructor(parmsROM)
{
super("ROM", parmsROM, MessagesPDP11.ROM);
this.abInit = null;
this.aSymbols = null;
this.addrROM = +parmsROM['addr'];
this.sizeROM = +parmsROM['size'];
this.fRetainROM = false;
/*
* The new 'alias' property can now be EITHER a single physical address (like 'addr') OR an array of
* physical addresses; eg:
*
* [0xf0000,0xffff0000,0xffff8000]
*
* We could have overloaded 'addr' to accomplish the same thing, but I think it's better to have any
* aliased locations listed under a separate property.
*
* Most ROMs are not aliased, in which case the 'alias' property should have the default value of null.
*/
this.addrAlias = parmsROM['alias'];
if (typeof this.addrAlias == "string") {
this.addrAlias = eval(this.addrAlias);
}
this.sFilePath = parmsROM['file'];
this.sFileName = Str.getBaseName(this.sFilePath);
if (this.sFilePath) {
var sFileURL = this.sFilePath;
if (DEBUG) this.log('load("' + sFileURL + '")');
/*
* If the selected ROM file has a ".json" extension, then we assume it's pre-converted
* JSON-encoded ROM data, so we load it as-is; ditto for ROM files with a ".hex" extension.
* Otherwise, we ask our server-side ROM converter to return the file in a JSON-compatible format.
*/
var sFileExt = Str.getExtension(this.sFileName);
if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) {
sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true';
}
var rom = this;
Web.getResource(sFileURL, null, true, function doneLoad(sURL, sResponse, nErrorCode) {
rom.finishLoad(sURL, sResponse, nErrorCode);
});
}
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {ROMPDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.initROM();
}
/**
* powerUp(data, fRepower)
*
* @this {ROMPDP11}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (this.aSymbols) {
if (this.dbg) {
this.dbg.addSymbols(this.id, this.addrROM, this.sizeROM, this.aSymbols);
}
/*
* Our only role in the handling of symbols is to hand them off to the Debugger at our
* first opportunity. Now that we've done that, our copy of the symbols, if any, are toast.
*/
delete this.aSymbols;
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* Since we have nothing to do on powerDown(), and no state to return, we could simply omit
* this function. But it doesn't hurt anything, and maybe we'll use our state to save something
* useful down the road, like user-defined symbols (ie, symbols that the Debugger may have
* created, above and beyond those symbols we automatically loaded, if any, along with the ROM).
*
* @this {ROMPDP11}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return true;
}
/**
* finishLoad(sURL, sData, nErrorCode)
*
* @this {ROMPDP11}
* @param {string} sURL
* @param {string} sData
* @param {number} nErrorCode (response from server if anything other than 200)
*/
finishLoad(sURL, sData, nErrorCode)
{
if (nErrorCode) {
this.notice("Unable to load ROM resource (error " + nErrorCode + ": " + sURL + ")");
this.sFilePath = null;
}
else {
Component.addMachineResource(this.idMachine, sURL, sData);
var resource = Web.parseMemoryResource(sURL, sData);
if (resource) {
this.abInit = resource.aBytes;
this.aSymbols = resource.aSymbols;
} else {
this.sFilePath = null;
}
}
this.initROM();
}
/**
* initROM()
*
* This function is called by both initBus() and finishLoad(), but it cannot copy the initial data into place
* until after initBus() has received the Bus component AND finishLoad() has received the data. When both those
* criteria are satisfied, the component becomes "ready".
*
* @this {ROMPDP11}
*/
initROM()
{
if (!this.isReady()) {
if (this.sFilePath) {
/*
* Too early...
*/
if (!this.abInit || !this.bus) return;
/*
* If no explicit size was specified, then use whatever the actual size is.
*/
if (!this.sizeROM) {
this.sizeROM = this.abInit.length;
}
if (this.abInit.length != this.sizeROM) {
/*
* Note that setError() sets the component's fError flag, which in turn prevents setReady() from
* marking the component ready. TODO: Revisit this decision. On the one hand, it sounds like a
* good idea to stop the machine in its tracks whenever a setError() occurs, but there may also be
* times when we'd like to forge ahead anyway.
*/
this.setError("ROM size (" + Str.toHexLong(this.abInit.length) + ") does not match specified size (" + Str.toHexLong(this.sizeROM) + ")");
}
else if (this.addROM(this.addrROM)) {
var aliases = [];
if (typeof this.addrAlias == "number") {
aliases.push(this.addrAlias);
} else if (this.addrAlias != null && this.addrAlias.length) {
aliases = this.addrAlias;
}
for (var i = 0; i < aliases.length; i++) {
this.cloneROM(aliases[i]);
}
/*
* We used to hang onto the initial ROM data so that we could restore any bytes the CPU overwrote,
* using memory write-notification handlers, but with the introduction of read-only memory blocks, that's
* no longer necessary.
*
* TODO: Consider an option to retain the ROM data, and give the user some way of restoring ROMs.
* That may be useful for "resumable" machines that save/restore all dirty block of memory, regardless
* whether they're ROM or RAM. However, the only way to modify a machine's ROM is with the Debugger,
* and Debugger users should know better.
*/
if (!this.fRetainROM) {
delete this.abInit;
}
}
}
this.setReady();
}
}
/**
* addROM(addr)
*
* @this {ROMPDP11}
* @param {number} addr
* @return {boolean}
*/
addROM(addr)
{
if (addr >= BusPDP11.IOPAGE_16BIT && addr < BusPDP11.IOPAGE_16BIT + BusPDP11.IOPAGE_LENGTH) {
/*
* This code has been added as a work-around to effectively allow us to install small ROMs into portions
* of the IOPAGE address space, by installing I/O handlers for the entire range that return the corresponding
* bytes of the current ROM image on reads, and ignore any writes (which I'm only assuming is how a typical
* ROM "device" deals with writes; we could remove the write handler, but then writes would fault).
*
* TODO: It would be more efficient if we parsed ROM data as words rather than bytes, and then installed
* only word handlers instead of only byte handlers. It was done this way purely for historical reasons (ie,
* because that's how other PCjs machines parse their ROMs). For now, all this means is that executing code
* out of ROM will be slower than out of RAM -- although that's often true in the real world as well.
*/
var IOTable = {
[addr]: [ROMPDP11.prototype.readROMByte, ROMPDP11.prototype.writeROMByte, null, null, null, this.sizeROM >> 1]
};
if (this.bus.addIOTable(this, IOTable)) {
this.status("Added " + this.sizeROM + "-byte ROM at " + Str.toOct(addr));
this.fRetainROM = true;
return true;
}
}
else if (this.bus.addMemory(addr, this.sizeROM, MemoryPDP11.TYPE.ROM)) {
if (DEBUG) this.log("addROM(): copying ROM to " + Str.toHexLong(addr) + " (" + Str.toHexLong(this.abInit.length) + " bytes)");
var i;
for (i = 0; i < this.abInit.length; i++) {
this.bus.setByteDirect(addr + i, this.abInit[i]);
}
return true;
}
/*
* We don't need to report an error here, because addMemory() already takes care of that.
*/
return false;
}
/**
* cloneROM(addr)
*
* For ROMs with one or more alias addresses, we used to call addROM() for each address. However,
* that obviously wasted memory, since each alias was an independent copy, and if you used the
* Debugger to edit the ROM in one location, the changes would not appear in the other location(s).
*
* Now that the Bus component provides low-level getMemoryBlocks() and setMemoryBlocks() methods
* to manually get and set the blocks of any memory range, it is now possible to create true aliases.
*
* @this {ROMPDP11}
* @param {number} addr
*/
cloneROM(addr)
{
var aBlocks = this.bus.getMemoryBlocks(this.addrROM, this.sizeROM);
this.bus.setMemoryBlocks(addr, this.sizeROM, aBlocks);
}
/**
* readROMByte(addr)
*
* @this {ROMPDP11}
* @param {number} addr
* @return {number}
*/
readROMByte(addr)
{
var i = (addr - this.addrROM);
return this.abInit[i];
}
/**
* writeROMByte(data, addr)
*
* This handler exists simply to ignore any writes, so that they don't cause faults.
*
* TODO: Another possible use for this would be to allow the Debugger to alter ROM contents,
* if the Debugger were to provide an interface indicating whether or not it was responsible
* for this write.
*
* @this {ROMPDP11}
* @param {number} data
* @param {number} addr
*/
writeROMByte(data, addr)
{
}
/**
* ROMPDP11.init()
*
* This function operates on every HTML element of class "rom", extracting the
* JSON-encoded parameters for the ROMPDP11 constructor from the element's "data-value"
* attribute, invoking the constructor to create a ROMPDP11 component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeROM = Component.getElementsByClass(document, PDP11.APPCLASS, "rom");
for (var iROM = 0; iROM < aeROM.length; iROM++) {
var eROM = aeROM[iROM];
var parmsROM = Component.getComponentParms(eROM);
var rom = new ROMPDP11(parmsROM);
Component.bindComponentControls(rom, eROM, PDP11.APPCLASS);
}
}
}
/*
* NOTE: There's currently no need for this component to have a reset() function, since
* once the ROM data is loaded, it can't be changed, so there's nothing to reinitialize.
*
* OK, well, I take that back, because the Debugger, if installed, has the ability to modify
* ROM contents, so in that case, having a reset() function that restores the original ROM data
* might be useful; then again, it might not, depending on what you're trying to debug.
*
* If we do add reset(), then we'll want to change initROM() to hang onto the original
* ROM data; currently, we release it after copying it into the read-only memory allocated
* via bus.addMemory().
*/
/*
* Initialize all the ROMPDP11 modules on the page.
*/
Web.onInit(ROMPDP11.init);
/**
* @copyright http://pcjs.org/modules/pdp11/lib/ram.js (C) Jeff Parsons 2012-2017
*/
class RAMPDP11 extends Component {
/**
* RAMPDP11(parmsRAM)
*
* The RAMPDP11 component expects the following (parmsRAM) properties:
*
* addr: starting physical address of RAM (default is 0)
* size: amount of RAM, in bytes (default is 0, which means defer to motherboard switch settings)
* file: name of optional data file to load into RAM (default is "")
* load: optional file load address (overrides any load address specified in the data file; default is null)
* exec: optional file exec address (overrides any exec address specified in the data file; default is null)
*
* NOTE: We make a note of the specified size, but no memory is initially allocated for the RAM until the
* Computer component calls powerUp().
*
* TODO: I seem to recall a PDP-11 diagnostic that failed if total RAM wasn't a multiple of 16Kb; our Bus
* component defaults to a block size that matches BusPDP11.IOPAGE_LENGTH (ie, 8Kb), and we even allow partial
* block allocations, so internally, we don't have that requirement, but for better compatibility, perhaps we
* should display a non-fatal warning if addr or size don't fall on 16Kb boundaries.
*
* @param {Object} parmsRAM
*/
constructor(parmsRAM)
{
super("RAM", parmsRAM);
this.abInit = null;
this.aSymbols = null;
this.addrRAM = +parmsRAM['addr'];
this.sizeRAM = +parmsRAM['size'];
this.addrLoad = parmsRAM['load'];
this.addrExec = parmsRAM['exec'];
if (this.addrLoad != null) this.addrLoad = +this.addrLoad;
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 = this.fReset = false;
this.sFilePath = parmsRAM['file'];
this.sFileName = Str.getBaseName(this.sFilePath);
if (this.sFilePath) {
var sFileURL = this.sFilePath;
if (DEBUG) this.log('load("' + sFileURL + '")');
/*
* If the selected data file has a ".json" extension, then we assume it's pre-converted
* JSON-encoded data, so we load it as-is; ditto for ROM files with a ".hex" extension.
* Otherwise, we ask our server-side converter to return the file in a JSON-compatible format.
*/
var sFileExt = Str.getExtension(this.sFileName);
if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) {
sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true';
}
var ram = this;
Web.getResource(sFileURL, null, true, function doneLoad(sURL, sResponse, nErrorCode) {
ram.finishLoad(sURL, sResponse, nErrorCode);
});
}
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {RAMPDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.initRAM();
}
/**
* powerUp(data, fRepower)
*
* @this {RAMPDP11}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (this.aSymbols) {
if (this.dbg) {
this.dbg.addSymbols(this.id, this.addrRAM, this.sizeRAM, this.aSymbols);
}
/*
* Our only role in the handling of symbols is to hand them off to the Debugger at our
* first opportunity. Now that we've done that, our copy of the symbols, if any, are toast.
*/
delete this.aSymbols;
}
if (!fRepower) {
/*
* Since we use the Bus to allocate all our memory, memory contents are already restored for us,
* so we don't save any state, and therefore no state should be restored. Just do a reset().
*/
this.reset();
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {RAMPDP11}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
/*
* The Computer powers down the CPU first, at which point CPUState state is saved,
* which includes the Bus state, and since we use the Bus component to allocate all
* our memory, memory contents are already saved for us, so we don't need the usual
* save logic.
*/
return true;
}
/**
* finishLoad(sURL, sData, nErrorCode)
*
* @this {RAMPDP11}
* @param {string} sURL
* @param {string} sData
* @param {number} nErrorCode (response from server if anything other than 200)
*/
finishLoad(sURL, sData, nErrorCode)
{
if (nErrorCode) {
this.notice("Unable to load RAM resource (error " + nErrorCode + ": " + sURL + ")");
this.sFilePath = null;
}
else {
Component.addMachineResource(this.idMachine, sURL, sData);
var resource = Web.parseMemoryResource(sURL, sData);
if (resource) {
this.abInit = resource.aBytes;
this.aSymbols = resource.aSymbols;
if (this.addrLoad == null) this.addrLoad = resource.addrLoad;
if (this.addrExec == null) this.addrExec = resource.addrExec;
} else {
this.sFilePath = null;
}
}
this.initRAM();
}
/**
* initRAM()
*
* This function is called by both initBus() and finishLoad(), but it cannot copy the initial data into place
* until after initBus() has received the Bus component AND finishLoad() has received the data. When both those
* criteria are satisfied, the component becomes "ready".
*
* @this {RAMPDP11}
*/
initRAM()
{
if (!this.bus) return;
if (!this.fAllocated && this.sizeRAM) {
if (this.bus.addMemory(this.addrRAM, this.sizeRAM, MemoryPDP11.TYPE.RAM)) {
this.fAllocated = true;
} else {
this.sizeRAM = 0; // don't bother trying again (it just results in redundant error messages)
}
}
if (!this.isReady()) {
if (!this.fAllocated) {
Component.error("No RAM allocated");
}
else if (this.sFilePath) {
/*
* Too early...
*/
if (!this.abInit) return;
if (this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM)) {
this.status('Loaded image "' + this.sFileName + '"');
} else {
this.notice('Error loading image "' + this.sFileName + '"');
}
/*
* NOTE: We now retain this data, so that reset() can return the RAM to its predefined state.
*
* delete this.abInit;
*/
}
this.fReset = true;
this.setReady();
}
}
/**
* reset()
*
* @this {RAMPDP11}
*/
reset()
{
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);
}
}
this.fReset = false;
}
/**
* loadImage(aBytes, 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 {RAMPDP11}
* @param {Array|Uint8Array} aBytes
* @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)
{
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), MessagesPDP11.PAPER);
break;
}
if (off + 6 >= aBytes.length) {
this.printMessage("invalid block at offset " + Str.toHexWord(offBlock), MessagesPDP11.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), MessagesPDP11.PAPER);
break;
}
checksum += aBytes[off++] & 0xff;
if (checksum & 0xff) {
this.printMessage("invalid checksum (" + Str.toHexByte(checksum) + ") for block at offset " + Str.toHexWord(offBlock), MessagesPDP11.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), MessagesPDP11.PAPER);
} else {
this.printMessage("loading " + Str.toHexWord(cbData) + " bytes at " + Str.toHexWord(addr) + "-" + Str.toHexWord(addr + cbData), MessagesPDP11.PAPER);
while (cbData--) {
this.bus.setByteDirect(addr++, aBytes[offData++] & 0xff);
}
}
fLoaded = true;
}
}
if (!fLoaded) {
if (addrLoad == null) addrLoad = addrInit;
if (addrLoad != null) {
for (var i = 0; i < aBytes.length; i++) {
this.bus.setByteDirect(addrLoad + i, aBytes[i]);
}
fLoaded = true;
}
}
if (fLoaded) {
/*
* Set the start address to whatever the caller provided, or failing that, whatever start
* address was specified inside the image.
*
* For example, the diagnostic "MAINDEC-11-D0AA-PB" doesn't include a start address inside the
* 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) {
this.cpu.stopCPU();
fStart = false;
}
if (addrExec != null) {
this.cpu.setReset(addrExec, fStart);
}
}
return fLoaded;
}
/**
* RAMPDP11.init()
*
* This function operates on every HTML element of class "ram", extracting the
* JSON-encoded parameters for the RAMPDP11 constructor from the element's "data-value"
* attribute, invoking the constructor to create a RAMPDP11 component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeRAM = Component.getElementsByClass(document, PDP11.APPCLASS, "ram");
for (var iRAM = 0; iRAM < aeRAM.length; iRAM++) {
var eRAM = aeRAM[iRAM];
var parmsRAM = Component.getComponentParms(eRAM);
var ram = new RAMPDP11(parmsRAM);
Component.bindComponentControls(ram, eRAM, PDP11.APPCLASS);
}
}
}
/*
* Initialize all the RAMPDP11 modules on the page.
*/
Web.onInit(RAMPDP11.init);
/**
* @copyright http://pcjs.org/modules/pdp11/lib/keyboard.js (C) Jeff Parsons 2012-2017
*/
class KeyboardPDP11 extends Component {
/**
* KeyboardPDP11(parmsKbd)
*
* @param {Object} parmsKbd
*/
constructor(parmsKbd)
{
super("Keyboard", parmsKbd, MessagesPDP11.KEYBOARD);
this.setReady();
}
/**
* setBinding(sType, sBinding, control, sValue)
*
* @this {KeyboardPDP11}
* @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc)
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sType, sBinding, control, sValue)
{
return false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {KeyboardPDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.cpu = cpu;
this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work
}
/**
* KeyboardPDP11.init()
*
* This function operates on every HTML element of class "keyboard", extracting the
* JSON-encoded parameters for the Keyboard constructor from the element's "data-value"
* attribute, invoking the constructor to create a Keyboard component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeKbd = Component.getElementsByClass(document, PDP11.APPCLASS, "keyboard");
for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) {
var eKbd = aeKbd[iKbd];
var parmsKbd = Component.getComponentParms(eKbd);
var kbd = new KeyboardPDP11(parmsKbd);
Component.bindComponentControls(kbd, eKbd, PDP11.APPCLASS);
}
}
}
KeyboardPDP11.MINPRESSTIME = 100; // 100ms
/*
* Initialize every Keyboard module on the page.
*/
Web.onInit(KeyboardPDP11.init);
/**
* @copyright http://pcjs.org/modules/pdp11/lib/serial.js (C) Jeff Parsons 2012-2017
*/
/**
* Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default,
* it deters us from defining named properties on our components; eg:
*
* this['exports'] = {...}
*
* results in an error:
*
* Cannot do '[]' access on a struct
*
* So, in order to define 'exports', we must override the @struct assumption by annotating
* the class as @unrestricted (or @dict). Note that this must be done both here and in the
* Component class, because otherwise the Compiler won't allow us to *reference* the named
* property either.
*
* TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to
* define every single property the class uses in its constructor, which results in a fair
* bit of redundant initialization, since many properties aren't (and don't need to be) fully
* initialized until the appropriate init(), reset(), restore(), etc. function is called.
*
* The upside, however, may be that since the structure of the class is completely defined by
* the constructor, JavaScript engines may be able to optimize and run more efficiently.
*
* @unrestricted
*/
class SerialPortPDP11 extends Component {
/**
* SerialPortPDP11(parmsSerial)
*
* The SerialPort component has the following component-specific (parmsSerial) properties:
*
* adapter: adapter number; 0 if not defined (the PCx86 SerialPort component uses this
* value to set the device's internal COM number, which in turn determines other properties,
* such as I/O ports and IRQ; for the PDP-11, this currently has no defined use)
*
* baudReceive: the default number of bits/second that the device should receive data at;
* 0 means use the device default (PDP11.DL11.RCSR.BAUD)
*
* baudTransmit: the default number of bits/second that the device should transmit data at;
* 0 means use the device default (PDP11.DL11.XCSR.BAUD)
*
* binding: name of a control (based on its "binding" attribute) to bind to this port's I/O
*
* tabSize: set to a non-zero number to convert tabs to spaces (applies only to output to
* the above binding); default is 0 (no conversion)
*
* upperCase: if true, all received input is upper-cased; it is normally the responsibility
* of the sending device to ensure this, but sometimes it's more convenient to enforce
* on the receiving end.
*
* NOTE: Since the XSL file defines the 'adapter' and 'baud' properties as numbers, not strings,
* there's no need to use parseInt(), and as an added benefit, we don't need to worry about whether
* a hex or decimal format was used.
*
* @param {Object} parmsSerial
*/
constructor(parmsSerial)
{
super("SerialPort", parmsSerial, MessagesPDP11.SERIAL);
this.iAdapter = +parmsSerial['adapter'];
this.nBaudReceive = +parmsSerial['baudReceive'] || PDP11.DL11.RCSR.BAUD;
this.nBaudTransmit = +parmsSerial['baudTransmit'] || PDP11.DL11.XCSR.BAUD;
this.fUpperCase = parmsSerial['upperCase'];
if (typeof this.fUpperCase == "string") this.fUpperCase = (this.fUpperCase == "true");
/**
* consoleOutput becomes a string that records serial port output if the 'binding' property is set to the
* reserved name "console". Nothing is written to the console, however, until a linefeed (0x0A) is output
* or the string length reaches a threshold (currently, 1024 characters).
*
* @type {string|null}
*/
this.consoleOutput = null;
/**
* controlIOBuffer is a DOM element bound to the port (currently used for output only; see transmitByte()).
*
* Example: CTTY COM2
*
* The CTTY DOS command redirects all CON I/O to the specified serial port (eg, COM2), which it assumes is
* connected to a serial terminal, and therefore anything it *transmits* via COM2 will be displayed by the
* terminal. It further assumes that anything typed on such a terminal is NOT displayed, so as DOS *receives*
* serial input, DOS *transmits* the appropriate characters back to the terminal via COM2.
*
* As a result, controlIOBuffer only needs to be updated by the transmitByte() function.
*
* @type {Object}
*/
this.controlIOBuffer = null;
/*
* If controlIOBuffer is being used AND 'tabSize' is set, then we make an attempt to monitor the characters
* being echoed via transmitByte(), maintain a logical column position, and convert any tabs into the appropriate
* number of spaces.
*
* charBOL, if nonzero, is a character to automatically output at the beginning of every line. This probably
* isn't generally useful; I use it internally to preformat serial output.
*/
this.tabSize = +parmsSerial['tabSize'];
this.charBOL = +parmsSerial['charBOL'];
this.iLogicalCol = 0;
this.fNullModem = true;
this.irqReceiver = this.irqTransmitter = null;
this.timerReceiveInterrupt = this.timerTransmitInterrupt = -1;
this.regRBUF = this.regRCSR = this.regXCSR = 0;
this.abReceive = [];
var sBinding = parmsSerial['binding'];
if (sBinding == "console") {
this.consoleOutput = "";
} else {
/*
* NOTE: If sBinding is not the name of a valid Control Panel DOM element, this call does nothing.
*/
Component.bindExternalControl(this, sBinding, SerialPortPDP11.sIOBuffer);
}
/*
* No connection until initConnection() is called.
*/
this.sDataReceived = "";
this.connection = this.sendData = this.updateStatus = null;
/*
* Export all functions required by initConnection().
*/
this['exports'] = {
'connect': this.initConnection,
'receiveData': this.receiveData,
'receiveStatus': this.receiveStatus,
'setConnection': this.setConnection
};
}
/**
* setBinding(sType, sBinding, control, sValue)
*
* @this {SerialPortPDP11}
* @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc)
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "buffer")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sType, sBinding, control, sValue)
{
var serial = this;
switch (sBinding) {
case SerialPortPDP11.sIOBuffer:
this.bindings[sBinding] = this.controlIOBuffer = control;
/*
* An onkeydown handler is required for certain keys that browsers tend to consume themselves;
* for example, BACKSPACE is often defined as going back to the previous web page, and certain
* CTRL keys are often used for browser shortcuts (usually on Windows-based browsers).
*
* NOTE: We don't bother with a keyUp handler, because for the most part, we're only intercepting
* keys that require special treatment; in general, we're content with keyPress events.
*/
control.onkeydown = function onKeyDown(event) {
event = event || window.event;
var bASCII = 0;
var keyCode = event.keyCode;
/*
* Perform the same remapping of BACKSPACE and DELETE that our VT100 emulation performs,
* for PCjs-wide consistency; see the KEYMAP table in /modules/pc8080/lib/keyboard.js for
* the rationale. Ditto for ALT-DELETE; see onKeyDown() in /modules/pc8080/lib/keyboard.js
* for details.
*
* NOTE: keyDown (and keyUp) events supply us with KEYCODE values, which are NOT the same as
* ASCII values, which is why we are comparing with KEYCODE values but assigning ASCII values,
* because receiveData() requires ASCII values.
*/
if (keyCode == Keys.KEYCODE.BS) {
bASCII = event.altKey? Keys.ASCII.CTRL_H : Keys.ASCII.DEL;
}
else if (keyCode == Keys.KEYCODE.DEL) {
bASCII = Keys.ASCII.CTRL_H;
}
else if (event.ctrlKey && keyCode >= Keys.ASCII.A && keyCode <= Keys.ASCII.Z) {
bASCII = keyCode - (Keys.ASCII.A - Keys.ASCII.CTRL_A);
}
if (bASCII) {
if (event.preventDefault) event.preventDefault();
serial.receiveData(bASCII);
}
return true;
};
control.onkeypress = function onKeyPress(event) {
/*
* NOTE: Unlike keyDown events, keyPress events generally supply us with ASCII values,
* despite the fact that, as above, they come to us via the keyCode property. Yes, it's
* brilliant (or rather, the opposite of brilliant), but that's life.
*/
event = event || window.event;
/*
* Not sure why COMMAND-key combinations are coming through here (on Safari at least),
* but in any case, let's make sure we don't act on them.
*/
if (!event.metaKey) {
var bASCII = event.which || event.keyCode;
/*
* Perform the same remapping of ALT-ENTER (to LINE-FEED) that our VT100 emulation performs,
* for PCjs-wide consistency; see onKeyDown() in /modules/pc8080/lib/keyboard.js for details.
*/
if (event.altKey) {
if (bASCII == Keys.ASCII.CTRL_M) {
bASCII = Keys.ASCII.CTRL_J;
}
}
serial.receiveData(bASCII);
/*
* Since we're going to remove the "readonly" attribute from the <textarea> control
* (so that the soft keyboard activates on iOS), instead of calling preventDefault() for
* selected keys (eg, the SPACE key, whose default behavior is to scroll the page), we must
* now call it for *all* keys, so that the keyCode isn't added to the control immediately,
* on top of whatever the machine is echoing back, resulting in double characters.
*/
if (event.preventDefault) event.preventDefault();
}
return true;
};
control.onpaste = function onKeyPress(event) {
if (event.stopPropagation) event.stopPropagation();
if (event.preventDefault) event.preventDefault();
var clipboardData = event.clipboardData || window.clipboardData;
if (clipboardData) {
/*
* NOTE: Multiple lines of pasted text will (at least on macOS) contain LFs instead of CRs;
* this is dealt with in receiveData() whenever it receives a string of characters.
*/
serial.receiveData(clipboardData.getData('Text'));
}
};
/*
* Now that we've added an onkeypress handler that calls preventDefault() for ALL keys, the control
* itself no longer needs the "readonly" attribute; we primarily need to remove it for iOS browsers,
* so that the soft keyboard will activate, but it shouldn't hurt to remove the attribute for all browsers.
*/
control.removeAttribute("readonly");
return true;
default:
break;
}
return false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {SerialPortPDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
var serial = this;
this.irqReceiver = this.cpu.addIRQ(this.iAdapter? -1 : PDP11.DL11.RVEC, PDP11.DL11.PRI, MessagesPDP11.DL11);
this.timerReceiveInterrupt = this.cpu.addTimer(function readyReceiver() {
var b = serial.receiveByte();
if (b >= 0) {
serial.regRBUF = b;
if (!(serial.regRCSR & PDP11.DL11.RCSR.RD)) {
serial.regRCSR |= PDP11.DL11.RCSR.RD;
} else {
serial.regRBUF |= PDP11.DL11.RBUF.OE | PDP11.DL11.RBUF.ERROR;
}
if (serial.regRCSR & PDP11.DL11.RCSR.RIE) {
cpu.setIRQ(serial.irqReceiver);
}
}
});
this.irqTransmitter = this.cpu.addIRQ(this.iAdapter? -1 : PDP11.DL11.XVEC, PDP11.DL11.PRI, MessagesPDP11.DL11);
this.timerTransmitInterrupt = this.cpu.addTimer(function readyTransmitter() {
serial.regXCSR |= PDP11.DL11.XCSR.READY;
if (serial.regXCSR & PDP11.DL11.XCSR.TIE) {
cpu.setIRQ(serial.irqTransmitter);
}
});
bus.addIOTable(this, SerialPortPDP11.UNIBUS_IOTABLE, this.iAdapter? ((PDP11.UNIBUS.DL11 + (this.iAdapter - 1) * 8) - PDP11.UNIBUS.RCSR) : 0);
bus.addResetHandler(this.reset.bind(this));
this.setReady();
}
/**
* initConnection(fNullModem)
*
* If a machine 'connection' parameter exists of the form "{sourcePort}->{targetMachine}.{targetPort}",
* and "{sourcePort}" matches our idComponent, then look for a component with id "{targetMachine}.{targetPort}".
*
* If the target component is found, then verify that it has exported functions with the following names:
*
* receiveData(data): called when we have data to transmit; aliased internally to sendData(data)
* receiveStatus(pins): called when our control signals have changed; aliased internally to updateStatus(pins)
*
* For now, we're not going to worry about communication in the other direction, because when the target component
* performs its own initConnection(), it will find our receiveData() and receiveStatus() functions, at which point
* communication in both directions should be established, and the circle of life complete.
*
* For added robustness, if the target machine initializes much more slowly than we do, and our connection attempt
* fails, that's OK, because when it finally initializes, its initConnection() will call our initConnection();
* if we've already initialized, no harm done.
*
* @this {SerialPortPDP11}
* @param {boolean} [fNullModem] (caller's null-modem setting, to ensure our settings are in agreement)
*/
initConnection(fNullModem)
{
if (!this.connection) {
var sConnection = this.cmp.getMachineParm("connection");
if (sConnection) {
var asParts = sConnection.split('->');
if (asParts.length == 2) {
var sSourceID = Str.trim(asParts[0]);
if (sSourceID != this.idComponent) return; // this connection string is intended for another instance
var sTargetID = Str.trim(asParts[1]);
this.connection = Component.getComponentByID(sTargetID);
if (this.connection) {
var exports = this.connection['exports'];
if (exports) {
var fnConnect = exports['connect'];
if (fnConnect) fnConnect.call(this.connection, this.fNullModem);
this.sendData = exports['receiveData'];
if (this.sendData) {
this.fNullModem = fNullModem;
this.updateStatus = exports['receiveStatus'];
this.status("Connected " + this.idMachine + '.' + sSourceID + " to " + sTargetID);
return;
}
}
}
}
/*
* Changed from notice() to status() because sometimes a connection fails simply because one of us is a laggard.
*/
this.status("Unable to establish connection: " + sConnection);
}
}
}
/**
* powerUp(data, fRepower)
*
* @this {SerialPortPDP11}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
/*
* This is as late as we can currently wait to make our first inter-machine connection attempt;
* even so, the target machine's initialization process may still be ongoing, so any connection
* may be not fully resolved until the target machine performs its own initConnection(), which will
* in turn invoke our initConnection() again.
*/
this.initConnection(this.fNullModem);
if (!data) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {SerialPortPDP11}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* reset()
*
* @this {SerialPortPDP11}
*/
reset()
{
this.initState();
}
/**
* save()
*
* This implements save support for the SerialPort component.
*
* @this {SerialPortPDP11}
* @return {Object}
*/
save()
{
var state = new State(this);
state.set(0, this.saveRegisters());
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the SerialPort component.
*
* @this {SerialPortPDP11}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
return this.initState(data[0]);
}
/**
* initState(a)
*
* @this {SerialPortPDP11}
* @param {Array} [a]
* @return {boolean} true if successful, false if failure
*/
initState(a)
{
if (!a) {
a = [0, PDP11.DL11.RCSR.CTS, PDP11.DL11.XCSR.READY, this.abReceive];
}
/*
* ES6 ALERT: A handy destructuring assignment, which makes it easy to perform the inverse
* of what saveRegisters() does when it collects a bunch of object properties into an array.
*/
[
this.regRBUF,
this.regRCSR,
this.regXCSR,
this.abReceive
] = a;
return true;
}
/**
* saveRegisters()
*
* Basically, the inverse of initState().
*
* @this {SerialPortPDP11}
* @return {Array}
*/
saveRegisters()
{
return [
this.regRBUF,
this.regRCSR,
this.regXCSR,
this.abReceive
];
}
/**
* getBaudTimeout(nBaud)
*
* Based on the selected baud rate (nBaud), convert that rate into a millisecond delay.
*
* @this {SerialPortPDP11}
* @param {number} nBaud
* @return {number} (number of milliseconds per byte)
*/
getBaudTimeout(nBaud)
{
/*
* TODO: Do a better job computing this, based on actual numbers of start, stop and parity bits,
* instead of hard-coding the total number of bits per byte to 10.
*/
var nBytesPerSecond = Math.round(nBaud / 10);
return 1000 / nBytesPerSecond;
}
/**
* receiveData(data)
*
* This replaces the old sendRBR() function, which expected an Array of bytes. We still support that,
* but in order to support connections with other SerialPort components (ie, the PC8080 SerialPort), we
* have added support for numbers and strings as well.
*
* @this {SerialPortPDP11}
* @param {number|string|Array} data
* @return {boolean} true if received, false if not
*/
receiveData(data)
{
if (typeof data == "number") {
this.abReceive.push(data);
}
else if (typeof data == "string") {
var bASCII = 0, bASCIIPrev;
for (var i = 0; i < data.length; i++) {
bASCIIPrev = bASCII;
bASCII = data.charCodeAt(i);
/*
* NOTE: Multiple lines of pasted text will (at least on macOS) contain LFs instead of CRs;
* we convert them to CRs below. Windows may do something different, but in the worst case,
* even if we receive CR/LF pairs, this code should keep the CRs and lose the LFs.
*/
if (bASCII == Str.ASCII.LF) {
if (bASCIIPrev == Str.ASCII.CR) continue;
bASCII = Str.ASCII.CR;
}
this.abReceive.push(bASCII);
}
}
else {
this.abReceive = this.abReceive.concat(data);
}
this.cpu.setTimer(this.timerReceiveInterrupt, this.getBaudTimeout(this.nBaudReceive));
return true; // for now, return true regardless, since we're buffering everything anyway
}
/**
* receiveByte()
*
* @this {SerialPortPDP11}
* @return {number} (0x00-0xff if byte available, -1 if not)
*/
receiveByte()
{
var b = -1;
if (this.abReceive.length) {
/*
* Here, as elsewhere (eg, the PC11 component), even if I trusted all incoming data
* to be byte values (which I don't), there's also the risk that it could be signed data
* (eg, -128 to 127, instead of 0 to 255). Both risks are good reasons to always mask
* the data assigned to RBUF with 0xff.
*/
b = this.abReceive.shift() & 0xff;
this.printMessage("receiveByte(" + Str.toHexByte(b) + ")");
if (this.fUpperCase) {
/*
* Automatically transform lower-case ASCII codes to upper-case; fUpperCase should
* only be set when a terminal or some sort of pseudo-display is being used and we don't
* trust it to have its CAPS-LOCK setting correct.
*/
if (b >= 0x61 && b < 0x7A) b -= 0x20;
}
this.cpu.setTimer(this.timerReceiveInterrupt, this.getBaudTimeout(this.nBaudReceive));
}
return b;
}
/**
* receiveStatus(pins)
*
* @this {SerialPortPDP11}
* @param {number} pins
*/
receiveStatus(pins)
{
var oldRCSR = this.regRCSR;
this.regRCSR &= ~(PDP11.DL11.RCSR.CTS | PDP11.DL11.RCSR.CD);
if (pins & RS232.CTS.MASK) {
this.regRCSR |= PDP11.DL11.RCSR.CTS;
}
if (pins & RS232.CD.MASK) {
this.regRCSR |= PDP11.DL11.RCSR.CD;
}
if (oldRCSR != this.regRCSR) {
this.regRCSR |= PDP11.DL11.RCSR.DSC;
if (this.regRCSR & PDP11.DL11.RCSR.DIE) {
this.cpu.setIRQ(this.irqReceiver);
}
}
}
/**
* setConnection(component, fn)
*
* @this {SerialPortPDP11}
* @param {Object|null} component
* @param {function(number)} fn
* @return {boolean}
*/
setConnection(component, fn)
{
if (!this.connection) {
this.connection = component;
this.sendData = fn;
return true;
}
return false;
}
/**
* transmitByte(b)
*
* @this {SerialPortPDP11}
* @param {number} b
* @return {boolean} true if transmitted, false if not
*/
transmitByte(b)
{
var fTransmitted = false;
if (MAXDEBUG) this.printMessage("transmitByte(" + Str.toHexByte(b) + ")");
if (this.sendData) {
if (this.sendData.call(this.connection, b)) {
fTransmitted = true;
}
}
/*
* TODO: Why do DEC diagnostics like to output bytes with bit 7 set?
*/
b &= 0x7F;
if (this.controlIOBuffer) {
if (b == 0x0D) {
this.iLogicalCol = 0;
}
else if (b == 0x08) {
this.controlIOBuffer.value = this.controlIOBuffer.value.slice(0, -1);
/*
* TODO: Back up the correct number of columns if the character erased was a tab.
*/
if (this.iLogicalCol > 0) this.iLogicalCol--;
}
else if (b) {
/*
* RT-11 outputs lots of NULL characters, at least after a "D 56=5015" (0x0A0D) command has
* been issued, hence the "if (b)" check above.
*
* TODO: Also consider a check for Keys.ASCII.CTRL_C, because by default, RT-11 outputs "raw"
* CTRL_C characters, which we capture below and render as <ETX>. RT-11 does this for other keys
* as well, such as CTRL_K (<VT>) and CTRL_L (<FF>).
*/
var s = Str.toASCIICode(b); // formerly: String.fromCharCode(b);
var nChars = s.length; // formerly: (b >= 0x20? 1 : 0);
if (b < 0x20 && nChars == 1) nChars = 0;
if (b == 0x09) {
var tabSize = this.tabSize || 8;
nChars = tabSize - (this.iLogicalCol % tabSize);
if (this.tabSize) s = Str.pad("", nChars);
}
if (this.charBOL && !this.iLogicalCol && nChars) s = String.fromCharCode(this.charBOL) + s;
this.controlIOBuffer.value += s;
this.controlIOBuffer.scrollTop = this.controlIOBuffer.scrollHeight;
this.iLogicalCol += nChars;
}
fTransmitted = true;
}
else if (this.consoleOutput != null) {
if (b == 0x0A || this.consoleOutput.length >= 1024) {
this.println(this.consoleOutput);
this.consoleOutput = "";
}
if (b != 0x0A) {
this.consoleOutput += String.fromCharCode(b);
}
fTransmitted = true;
}
/*
* NOTE: When debugging issues involving the SerialPort, such as debugging code between a pair of
* transmitted bytes, you can pass 0 instead of getBaudTimeout() to setTimer() to minimize the amount
* of time spent waiting for XCSR.READY to be set again.
*/
this.cpu.setTimer(this.timerTransmitInterrupt, this.getBaudTimeout(this.nBaudTransmit));
return fTransmitted;
}
/**
* readRCSR(addr)
*
* @this {SerialPortPDP11}
* @param {number} addr (eg, PDP11.UNIBUS.RCSR or 177560)
* @return {number}
*/
readRCSR(addr)
{
var data = this.regRCSR & PDP11.DL11.RCSR.RMASK;
this.regRCSR &= ~PDP11.DL11.RCSR.DSC;
return data;
}
/**
* writeRCSR(data, addr)
*
* @this {SerialPortPDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RCSR or 177560)
*/
writeRCSR(data, addr)
{
var delta = (data ^ this.regRCSR);
this.regRCSR = (this.regRCSR & ~PDP11.DL11.RCSR.WMASK) | (data & PDP11.DL11.RCSR.WMASK);
/*
* Whenever DTR or RTS changes, we also want to notify any connected machine, via updateStatus().
*/
if (this.updateStatus) {
if (delta & PDP11.DL11.RCSR.RS232) {
var pins = 0;
if (this.fNullModem) {
pins |= (data & PDP11.DL11.RCSR.RTS)? RS232.CTS.MASK : 0;
pins |= (data & PDP11.DL11.RCSR.DTR)? (RS232.DSR.MASK | RS232.CD.MASK): 0;
} else {
pins |= (data & PDP11.DL11.RCSR.RTS)? RS232.RTS.MASK : 0;
pins |= (data & PDP11.DL11.RCSR.DTR)? RS232.DTR.MASK : 0;
}
this.updateStatus.call(this.connection, pins);
}
}
}
/**
* readRBUF(addr)
*
* @this {SerialPortPDP11}
* @param {number} addr (eg, PDP11.UNIBUS.RBUF or 177562)
* @return {number}
*/
readRBUF(addr)
{
this.regRCSR &= ~PDP11.DL11.RCSR.RD;
return this.regRBUF;
}
/**
* writeRBUF(data, addr)
*
* @this {SerialPortPDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RBUF or 177562)
*/
writeRBUF(data, addr)
{
}
/**
* readXCSR(addr)
*
* @this {SerialPortPDP11}
* @param {number} addr (eg, PDP11.UNIBUS.XCSR or 177564)
* @return {number}
*/
readXCSR(addr)
{
return this.regXCSR;
}
/**
* writeXCSR(data, addr)
*
* @this {SerialPortPDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.XCSR or 177564)
*/
writeXCSR(data, addr)
{
/*
* If the device is READY, and TIE is being set, then request a hardware interrupt.
*
* Conversely, if TIE is being cleared, remove the request; this resolves a problem within
* MAINDEC TEST 15, where the Transmitter Interrupt Enable (TIE) bit is cleared, set, and cleared
* in rapid succession, with the expectation that NO interrupt will be generated. Note that
* this fix also requires a complementary change in setIRQ(), to request hardware interrupts with
* IRQ_DELAY rather than IRQ.
*/
if (this.regXCSR & PDP11.DL11.XCSR.READY) {
if (data & PDP11.DL11.XCSR.TIE) {
this.cpu.setIRQ(this.irqTransmitter);
} else {
this.cpu.clearIRQ(this.irqTransmitter);
}
}
this.regXCSR = (this.regXCSR & ~PDP11.DL11.XCSR.WMASK) | (data & PDP11.DL11.XCSR.WMASK);
}
/**
* readXBUF(addr)
*
* @this {SerialPortPDP11}
* @param {number} addr (eg, PDP11.UNIBUS.XBUF or 177566)
* @return {number}
*/
readXBUF(addr)
{
return 0;
}
/**
* writeXBUF(data, addr)
*
* @this {SerialPortPDP11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.XBUF or 177566)
*/
writeXBUF(data, addr)
{
this.transmitByte(data & PDP11.DL11.XBUF.DATA);
this.regXCSR &= ~PDP11.DL11.XCSR.READY;
}
/**
* SerialPortPDP11.init()
*
* This function operates on every HTML element of class "serial", extracting the
* JSON-encoded parameters for the SerialPort constructor from the element's "data-value"
* attribute, invoking the constructor to create a SerialPort component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeSerial = Component.getElementsByClass(document, PDP11.APPCLASS, "serial");
for (var iSerial = 0; iSerial < aeSerial.length; iSerial++) {
var eSerial = aeSerial[iSerial];
var parmsSerial = Component.getComponentParms(eSerial);
var serial = new SerialPortPDP11(parmsSerial);
Component.bindComponentControls(serial, eSerial, PDP11.APPCLASS);
}
}
}
/*
* Internal name used for the I/O buffer control, if any, that we bind to the SerialPort.
*
* Alternatively, if SerialPort wants to use another component's control (eg, the Panel's
* "print" control), it can specify the name of that control with the 'binding' property.
*
* For that binding to succeed, we also need to know the target component; for now, that's
* been hard-coded to "Panel", in part because that's one of the few components we can rely
* upon initializing before we do, but it would be a simple matter to include a component type
* or ID as part of the 'binding' property as well, if we need more flexibility later.
*/
SerialPortPDP11.sIOBuffer = "buffer";
/*
* ES6 ALERT: As you can see below, I've finally started using computed property names.
*/
SerialPortPDP11.UNIBUS_IOTABLE = {
[PDP11.UNIBUS.RCSR]: /* 177560 */ [null, null, SerialPortPDP11.prototype.readRCSR, SerialPortPDP11.prototype.writeRCSR, "RCSR"],
[PDP11.UNIBUS.RBUF]: /* 177562 */ [null, null, SerialPortPDP11.prototype.readRBUF, SerialPortPDP11.prototype.writeRBUF, "RBUF"],
[PDP11.UNIBUS.XCSR]: /* 177564 */ [null, null, SerialPortPDP11.prototype.readXCSR, SerialPortPDP11.prototype.writeXCSR, "XCSR"],
[PDP11.UNIBUS.XBUF]: /* 177566 */ [null, null, SerialPortPDP11.prototype.readXBUF, SerialPortPDP11.prototype.writeXBUF, "XBUF"]
};
/*
* Initialize every SerialPort module on the page.
*/
Web.onInit(SerialPortPDP11.init);
/**
* @copyright http://pcjs.org/modules/pdp11/lib/pc11.js (C) Jeff Parsons 2012-2017
*/
class PC11 extends Component {
/**
* PC11(parms)
*
* The PC11 component has the following component-specific (parms) properties:
*
* autoMount: a JSON-encoded object containing 'name' and 'path' properties, describing a
* tape resource to automatically load at startup (only the "load" operation is supported
* for autoMount; if you want to "read" a tape image directly into RAM at startup, you must
* ask the RAM component to do that).
*
* baudReceive: the default number of bits/second that the device should receive data at;
* 0 means use the device default (PDP11.PC11.PRS.BAUD)
*
* baudTransmit: the default number of bits/second that the device should transmit data at;
* 0 means use the device default (PDP11.PC11.PPS.BAUD); currently ignored, since punch
* support isn't implemented yet.
*
* NOTE: Since the XSL file defines the 'baud' properties as numbers, not strings, there's no need to
* use parseInt(), and as an added benefit, we don't need to worry about whether a hex or decimal format
* was used.
*
* @param {Object} parms
*/
constructor(parms)
{
super("PC11", parms, MessagesPDP11.PC11);
this.sDevice = "PTR"; // TODO: Make the device name configurable
/*
* We preliminarily parse and record any 'autoMount' object now, but we no longer process it
* until initBus(), because the Computer's getMachineParm() service may have an override for us.
*/
this.configMount = this.parseConfig(parms['autoMount']);
this.cAutoMount = 0;
this.nBaudReceive = +parms['baudReceive'] || PDP11.PC11.PRS.BAUD;
this.regPRS = 0; // PRS register
this.regPRB = 0; // PRB register
this.regPPS = PDP11.PC11.PPS.ERROR; // PPS register (TODO: Stop signaling error once punch is implemented)
this.regPPB = 0; // PPB register
this.iTapeData = 0; // buffer index
this.aTapeData = []; // buffer for the PRB register
this.sTapeSource = PC11.SOURCE.NONE;
this.nTapeTarget = PC11.TARGET.NONE;
this.sTapeName = this.sTapePath = "";
/*
* These next few variables simply keep track of the previous parameters to parseTape(),
* so that we can easily reparse the previous tape as needed.
*/
this.aBytes = this.addrLoad = this.addrExec = null;
this.nLastPercent = -1; // ensure the first displayProgress() displays something
/*
* Support for local tape images is currently limited to desktop browsers with FileReader support;
* when this flag is set, setBinding() allows local tape bindings and informs initBus() to update the
* "listTapes" binding accordingly.
*/
this.fLocalTapes = (!Web.isMobile() && window && 'FileReader' in window);
this.irqReader = null;
this.timerReader = -1;
this.ram = null;
}
/**
* parseConfig(config)
*
* @this {PC11}
* @param {*} config
* @return {*}
*/
parseConfig(config)
{
if (config && typeof config == "string") {
try {
/*
* The most likely source of any exception will be right here, where we're parsing
* this JSON-encoded data.
*/
config = eval("(" + config + ")");
} catch (e) {
Component.error(this.type + " auto-mount error: " + e.message + " (" + config + ")");
config = null;
}
}
return config || {};
}
/**
* setBinding(sType, sBinding, control, sValue)
*
* @this {PC11}
* @param {string|null} sType is the type of the HTML control (eg, "button", "list", "text", etc)
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "listTapes")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sType, sBinding, control, sValue)
{
var pc11 = this;
var nTapeTarget = PC11.TARGET.NONE;
switch (sBinding) {
case "listTapes":
this.bindings[sBinding] = control;
control.onchange = function onChangeListTapes(event) {
var controlDesc = pc11.bindings["descTape"];
var controlOption = control.options[control.selectedIndex];
if (controlDesc && controlOption) {
var dataValue = {};
var sValue = controlOption.getAttribute("data-value");
if (sValue) {
try {
dataValue = eval("(" + sValue + ")");
} catch (e) {
Component.error("PC11 option error: " + e.message);
}
}
var sHTML = dataValue['desc'];
if (sHTML === undefined) sHTML = "";
var sHRef = dataValue['href'];
if (sHRef !== undefined) sHTML = "<a href=\"" + sHRef + "\" target=\"_blank\">" + sHTML + "</a>";
controlDesc.innerHTML = sHTML;
}
};
return true;
case "descTape":
this.bindings[sBinding] = control;
return true;
/*
* "readTape" operation must do pretty much everything that the "loadTape" does, but whereas the load
* operation records the bytes in aTapeData, the read operation stuffs them directly into the machine's memory;
* the former sets nTapeTarget to TARGET.READER, while the latter sets it to TARGET.MEMORY.
*/
case "readTape":
nTapeTarget = PC11.TARGET.MEMORY;
/* falls through */
case "loadTape":
if (!nTapeTarget) nTapeTarget = PC11.TARGET.READER;
this.bindings[sBinding] = control;
control.onclick = function onClickReadTape(event) {
var controlTapes = pc11.bindings["listTapes"];
if (controlTapes) {
var sTapeName = controlTapes.options[controlTapes.selectedIndex].text;
var sTapePath = controlTapes.value;
pc11.loadSelectedTape(sTapeName, sTapePath, nTapeTarget);
}
};
return true;
case "mountTape":
if (!this.fLocalTapes) {
if (DEBUG) this.log("Local tape support not available");
/*
* We could also simply hide the control; eg:
*
* control.style.display = "none";
*
* but removing the control altogether seems better.
*/
control.parentNode.removeChild(/** @type {Node} */ (control));
return false;
}
this.bindings[sBinding] = control;
/*
* Enable "Mount" button only if a file is actually selected
*/
control.addEventListener('change', function() {
var fieldset = control.children[0];
var files = fieldset.children[0].files;
var submit = fieldset.children[1];
submit.disabled = !files.length;
});
control.onsubmit = function(event) {
var file = event.currentTarget[1].files[0];
if (file) {
var sTapePath = file.name;
var sTapeName = Str.getBaseName(sTapePath, true);
/*
* TODO: Provide a way to mount tapes into MEMORY as well as READER.
*/
pc11.loadSelectedTape(sTapeName, sTapePath, PC11.TARGET.READER, file);
}
/*
* Prevent reloading of web page after form submission
*/
return false;
};
return true;
case PC11.BINDING.READ_PROGRESS:
this.bindings[sBinding] = control;
return true;
default:
break;
}
return false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {PC11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.ram = /** @type {RAMPDP11} */ (cmp.getMachineComponent("RAM"));
var pc11 = this;
var configMount = this.parseConfig(this.cmp.getMachineParm('autoMount'));
/*
* Add only devices from the machine-wide autoMount configuration that match devices managed by this component.
*/
if (configMount) {
for (var sDevice in configMount) {
if (sDevice != this.sDevice) continue;
this.configMount[sDevice] = configMount[sDevice];
}
}
this.irqReader = this.cpu.addIRQ(PDP11.PC11.RVEC, PDP11.PC11.PRI, MessagesPDP11.PC11);
this.timerReader = this.cpu.addTimer(function readyReader() {
pc11.advanceReader();
});
bus.addIOTable(this, PC11.UNIBUS_IOTABLE);
bus.addResetHandler(this.reset.bind(this));
this.addTape("None", PC11.SOURCE.NONE, true);
if (this.fLocalTapes) this.addTape("Local Tape", PC11.SOURCE.LOCAL);
this.addTape("Remote Tape", PC11.SOURCE.REMOTE);
if (!this.autoMount()) this.setReady();
}
/**
* powerUp(data, fRepower)
*
* @this {PC11}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (!data) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {PC11}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* reset()
*
* TODO: Consider making our reset() handler ALSO restore the original loaded tape, in much the same
* way the RAM component now restores the original predefined memory or tape image after resetting the RAM.
*
* @this {PC11}
*/
reset()
{
this.regPRS &= ~PDP11.PC11.PRS.CLEAR;
this.regPRB = 0;
}
/**
* autoMount(fRemount)
*
* @this {PC11}
* @param {boolean} [fRemount] is true if we're remounting all auto-mounted tapes
* @return {boolean} true if one or more tape images are being auto-mounted, false if none
*/
autoMount(fRemount)
{
if (!fRemount) this.cAutoMount = 0;
var configMount = this.configMount[this.sDevice];
if (configMount) {
var sTapePath = configMount['path'] || "";
var sTapeName = configMount['name'] || this.findTape(sTapePath);
if (sTapePath && sTapeName) {
/*
* TODO: Provide a way to autoMount tapes into MEMORY as well as READER.
*/
if (!this.loadTape(sTapeName, sTapePath, PC11.TARGET.READER, true) && fRemount) {
this.setReady(false);
}
} else {
/*
* This likely happened because there was no autoMount setting (or it was overridden with an empty value),
* so just make sure the current selection is set to "None".
*/
this.displayTape();
}
}
return !!this.cAutoMount;
}
/**
* loadSelectedTape(sTapeName, sTapePath, nTapeTarget, file)
*
* @this {PC11}
* @param {string} sTapeName
* @param {string} sTapePath
* @param {number} nTapeTarget
* @param {File} [file] is set if there's an associated File object
*/
loadSelectedTape(sTapeName, sTapePath, nTapeTarget, file)
{
if (!sTapePath) {
this.unloadTape(false);
return;
}
if (sTapePath == PC11.SOURCE.LOCAL) {
this.notice('Use "Choose File" and "Mount" to select and load a local tape.');
return;
}
/*
* If the special PC11.SOURCE.REMOTE path is selected, then we want to prompt the user for a URL.
* Oh, and make sure we pass an empty string as the 2nd parameter to prompt(), so that IE won't display
* "undefined" -- because after all, undefined and "undefined" are EXACTLY the same thing, right?
*
* TODO: This is literally all I've done to support remote tape images. There's probably more
* I should do, like dynamically updating "listTapes" to include new entries, and adding new entries
* to the save/restore data.
*/
if (sTapePath == PC11.SOURCE.REMOTE) {
sTapePath = window.prompt("Enter the URL of a remote tape image.", "") || "";
if (!sTapePath) return;
sTapeName = Str.getBaseName(sTapePath);
this.status("Attempting to load " + sTapePath + " as \"" + sTapeName + "\"");
this.sTapeSource = PC11.SOURCE.REMOTE;
}
else {
this.sTapeSource = sTapePath;
}
this.loadTape(sTapeName, sTapePath, nTapeTarget, false, file);
}
/**
* loadTape(sTapeName, sTapePath, nTapeTarget, fAutoMount, file)
*
* NOTE: If sTapePath is already loaded, nothing needs to be done.
*
* @this {PC11}
* @param {string} sTapeName
* @param {string} sTapePath
* @param {number} nTapeTarget
* @param {boolean} [fAutoMount]
* @param {File} [file] is set if there's an associated File object
* @return {number} 1 if tape loaded, 0 if queued up (or busy), -1 if already loaded
*/
loadTape(sTapeName, sTapePath, nTapeTarget, fAutoMount, file)
{
var nResult = -1;
if (this.sTapePath.toLowerCase() != sTapePath.toLowerCase() || this.nTapeTarget != nTapeTarget) {
nResult++;
this.unloadTape(true);
if (this.flags.busy) {
this.notice("PC11 busy");
}
else {
// this.status("tape queued: " + sTapeName);
if (fAutoMount) {
this.cAutoMount++;
if (this.messageEnabled()) this.printMessage("auto-loading tape: " + sTapeName);
}
if (this.load(sTapeName, sTapePath, nTapeTarget, file)) {
nResult++;
} else {
this.flags.busy = true;
}
}
}
if (nResult) {
/*
* Now that we're calling parseTape() again (so that the current tape can either be restarted on
* the reader or reloaded into RAM), we can also rely on it to display an appropriate status message, too.
*
* this.status(this.nTapeTarget == PC11.TARGET.READER? "tape loaded" : "tape read");
*/
this.parseTape(this.sTapeName, this.sTapePath, this.nTapeTarget, this.aBytes, this.addrLoad, this.addrExec);
}
return nResult;
}
/**
* load(sTapeName, sTapePath, nTapeTarget, file)
*
* @this {PC11}
* @param {string} sTapeName
* @param {string} sTapePath
* @param {number} nTapeTarget
* @param {File} [file] is set if there's an associated File object
* @return {boolean} true if load completed (successfully or not), false if queued
*/
load(sTapeName, sTapePath, nTapeTarget, file)
{
var pc11 = this;
var sTapeURL = sTapePath;
if (DEBUG) {
var sMessage = 'load("' + sTapeName + '","' + sTapePath + '")';
this.printMessage(sMessage);
}
if (file) {
var reader = new FileReader();
reader.onload = function doneRead() {
pc11.finishRead(sTapeName, sTapePath, nTapeTarget, reader.result);
};
reader.readAsArrayBuffer(file);
return false;
}
/*
* If there's an occurrence of API_ENDPOINT anywhere in the path, we assume we can use it as-is;
* ie, that the user has already formed a URL of the type we use ourselves for unconverted tape images.
*/
if (sTapePath.indexOf(DumpAPI.ENDPOINT) < 0) {
/*
* If the selected tape image has a "json" extension, then we assume it's a pre-converted
* JSON-encoded tape image, so we load it as-is; otherwise, we ask our server-side tape image
* converter to return the corresponding JSON-encoded data.
*/
var sTapeExt = Str.getExtension(sTapePath);
if (sTapeExt == DumpAPI.FORMAT.JSON || sTapeExt == DumpAPI.FORMAT.JSON_GZ) {
sTapeURL = encodeURI(sTapePath);
} else {
var sTapeParm = DumpAPI.QUERY.PATH;
sTapeURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + sTapeParm + '=' + encodeURIComponent(sTapePath) + "&" + DumpAPI.QUERY.FORMAT + "=" + DumpAPI.FORMAT.JSON;
}
}
return !!Web.getResource(sTapeURL, null, true, function doneLoad(sURL, sResponse, nErrorCode) {
pc11.finishLoad(sTapeName, sTapePath, nTapeTarget, sResponse, sURL, nErrorCode);
});
}
/**
* finishLoad(sTapeName, sTapePath, sTapeData, nTapeTarget, sURL, nErrorCode)
*
* @this {PC11}
* @param {string} sTapeName
* @param {string} sTapePath
* @param {string} sTapeData
* @param {number} nTapeTarget
* @param {string} sURL
* @param {number} nErrorCode (response from server if anything other than 200)
*/
finishLoad(sTapeName, sTapePath, nTapeTarget, sTapeData, sURL, nErrorCode)
{
var fPrintOnly = (nErrorCode < 0 && !!this.cmp && !this.cmp.flags.powered);
if (nErrorCode) {
/*
* This can happen for innocuous reasons, such as the user switching away too quickly, forcing
* the request to be cancelled. And unfortunately, the browser cancels XMLHttpRequest requests
* BEFORE it notifies any page event handlers, so if the Computer's being powered down, we won't know
* that yet. For now, we rely on the lack of a specific error (nErrorCode < 0), and suppress the
* notify() alert if there's no specific error AND the computer is not powered up yet.
*/
this.notice("Unable to load tape \"" + sTapeName + "\" (error " + nErrorCode + ": " + sURL + ")", fPrintOnly);
}
else {
if (DEBUG && this.messageEnabled()) {
this.printMessage('finishLoad("' + sTapePath + '")');
}
Component.addMachineResource(this.idMachine, sURL, sTapeData);
var resource = Web.parseMemoryResource(sURL, sTapeData);
if (resource) {
this.parseTape(sTapeName, sTapePath, nTapeTarget, resource.aBytes, resource.addrLoad, resource.addrExec);
}
}
this.flags.busy = false;
if (this.cAutoMount) {
this.cAutoMount--;
if (!this.cAutoMount) this.setReady();
}
this.displayTape();
}
/**
* finishRead(sTapeName, sTapePath, nTapeTarget, buffer)
*
* @this {PC11}
* @param {string} sTapeName
* @param {string} sTapePath
* @param {number} nTapeTarget
* @param {?} buffer (we KNOW this is an ArrayBuffer, but we can't seem to convince the Closure Compiler)
*/
finishRead(sTapeName, sTapePath, nTapeTarget, buffer)
{
if (buffer) {
var aBytes = new Uint8Array(buffer, 0, buffer.byteLength);
this.parseTape(sTapeName, sTapePath, nTapeTarget, aBytes);
this.sTapeSource = PC11.SOURCE.LOCAL;
}
this.flags.busy = false;
this.displayTape();
}
/**
* addTape(sName, sPath, fTop)
*
* @this {PC11}
* @param {string} sName
* @param {string} sPath
* @param {boolean} [fTop] (default is bottom)
*/
addTape(sName, sPath, fTop)
{
var controlTapes = this.bindings["listTapes"];
if (controlTapes && controlTapes.options) {
for (var i = 0; i < controlTapes.options.length; i++) {
if (controlTapes.options[i].value == sPath) return;
}
var controlOption = document.createElement("option");
controlOption.text = sName;
controlOption.value = sPath;
if (fTop && controlTapes.childNodes[0]) {
controlTapes.insertBefore(controlOption, controlTapes.childNodes[0]);
} else {
controlTapes.appendChild(controlOption);
}
}
}
/**
* findTape(sPath)
*
* This is used to deal with mount requests (eg, autoMount) that supply a path without a name;
* if we can find the path in the "listTapes" control, then we return the associated tape name.
*
* @this {PC11}
* @param {string} sPath
* @return {string|null}
*/
findTape(sPath)
{
var controlTapes = this.bindings["listTapes"];
if (controlTapes && controlTapes.options) {
for (var i = 0; i < controlTapes.options.length; i++) {
var control = controlTapes.options[i];
if (control.value == sPath) return control.text;
}
}
return Str.getBaseName(sPath, true);
}
/**
* displayTape()
*
* @this {PC11}
*/
displayTape()
{
var controlTapes = this.bindings["listTapes"];
if (controlTapes && controlTapes.options) {
var sTargetPath = this.sTapeSource || this.sTapePath;
for (var i = 0; i < controlTapes.options.length; i++) {
if (controlTapes.options[i].value == sTargetPath) {
if (controlTapes.selectedIndex != i) {
controlTapes.selectedIndex = i;
}
break;
}
}
if (i == controlTapes.options.length) controlTapes.selectedIndex = 0;
}
}
/**
* displayProgress(nPercent)
*
* @this {PC11}
* @param {number} nPercent
*/
displayProgress(nPercent)
{
nPercent |= 0;
if (nPercent !== this.nLastPercent) {
var control = this.bindings[PC11.BINDING.READ_PROGRESS];
if (control) {
var aeControls = Component.getElementsByClass(control, PC11.CSSCLASS.PROGRESS_BAR);
var controlBar = aeControls && aeControls[0];
if (controlBar && controlBar.style) {
controlBar.style.width = nPercent + "%";
}
}
this.nLastPercent = nPercent;
}
}
/**
* parseTape(sTapeName, sTapePath, nTapeTarget, aBytes, addrLoad, addrExec)
*
* @this {PC11}
* @param {string} sTapeName
* @param {string} sTapePath
* @param {number} nTapeTarget
* @param {Array|Uint8Array} aBytes
* @param {number|null} [addrLoad]
* @param {number|null} [addrExec]
*/
parseTape(sTapeName, sTapePath, nTapeTarget, aBytes, addrLoad, addrExec)
{
this.sTapeName = sTapeName;
this.sTapePath = sTapePath;
this.nTapeTarget = nTapeTarget;
this.aBytes = aBytes;
this.addrLoad = addrLoad;
this.addrExec = addrExec;
if (nTapeTarget == PC11.TARGET.MEMORY) {
/*
* Use the RAM component's loadImage() service to do our dirty work. If the load succeeds, then
* depending on whether there was also exec address, either the CPU will be stopped or the PC wil be
* reset.
*
* NOTE: Some tapes are not in the Absolute Loader format, so if the JSON-encoded tape resource file
* we downloaded didn't ALSO include a load address, the load will fail.
*
* For example, the "Absolute Loader" tape is NOT itself in the Absolute Loader format. You just have
* to know that in order to load that tape, you must first load the appropriate "Bootstrap Loader" (which
* DOES include its own hard-coded load address), load the "Absolute Loader" tape, and then run the
* "Bootstrap Loader".
*/
if (!this.ram || !this.ram.loadImage(aBytes, addrLoad, addrExec, null, false)) {
/*
* This doesn't seem to serve any purpose, other than to be annoying, because perhaps you accidentally
* clicked "Read" instead of "Load"....
*
* this.sTapeName = "";
* this.sTapePath = "";
* this.sTapeSource = PC11.SOURCE.NONE;
* this.nTapeTarget = PC11.TARGET.NONE;
*/
this.notice('No valid memory address for tape "' + sTapeName + '"');
return;
}
this.status('Read tape "' + sTapeName + '"');
return;
}
this.iTapeData = 0;
this.aTapeData = aBytes;
this.regPRS &= ~PDP11.PC11.PRS.ERROR;
this.status('Loaded tape "' + sTapeName + '" (' + aBytes.length + " bytes)");
this.displayProgress(0);
}
/**
* unloadTape(fLoading)
*
* @this {PC11}
* @param {boolean} [fLoading]
*/
unloadTape(fLoading)
{
if (this.sTapePath || fLoading === false) {
this.sTapeName = "";
this.sTapePath = "";
/*
* Avoid any unnecessary hysteresis regarding the display if this unload is merely a prelude to another load.
*/
if (!fLoading) {
if (this.nTapeTarget) this.status(this.nTapeTarget == PC11.TARGET.READER? "tape detached" : "tape unloaded");
this.sTapeSource = PC11.SOURCE.NONE;
this.nTapeTarget = PC11.TARGET.NONE;
this.displayTape();
}
}
}
/**
* save()
*
* This implements save support for the PC11 component.
*
* @this {PC11}
* @return {Object}
*/
save()
{
var state = new State(this);
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the PC11 component.
*
* @this {PC11}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
return true;
}
/**
* getBaudTimeout(nBaud)
*
* Based on the selected baud rate (nBaud), convert that rate into a millisecond delay.
*
* @this {PC11}
* @param {number} nBaud
* @return {number} (number of milliseconds per byte)
*/
getBaudTimeout(nBaud)
{
/*
* TODO: Do a better job computing this, based on actual numbers of start, stop and parity bits,
* instead of hard-coding the total number of bits per byte to 10.
*/
var nBytesPerSecond = Math.round(nBaud / 10);
return 1000 / nBytesPerSecond;
}
/**
* advanceReader()
*
* If the reader is enabled (RE is set) and there is no exceptional condition (ie, ERROR is set),
* and if the buffer register is empty (DONE is clear), then if we have more data in our internal buffer,
* store it in the buffer register, and optionally trigger an interrupt if device interrupts are enabled.
*
* @this {PC11}
*/
advanceReader()
{
if ((this.regPRS & (PDP11.PC11.PRS.RE | PDP11.PC11.PRS.ERROR)) == PDP11.PC11.PRS.RE) {
if (!(this.regPRS & PDP11.PC11.PRS.DONE)) {
if (this.iTapeData < this.aTapeData.length) {
/*
* Here, as elsewhere (eg, the DL11 component), even if I trusted all incoming data
* to be byte values (which I don't), there's also the risk that it could be signed data
* (eg, -128 to 127, instead of 0 to 255). Both risks are good reasons to always mask
* the data assigned to PRB with 0xff.
*/
this.regPRB = this.aTapeData[this.iTapeData] & 0xff;
if (this.messageEnabled()) this.printMessage(this.type + ".advanceReader(" + this.iTapeData + "): " + Str.toHexByte(this.regPRB), true);
this.iTapeData++;
this.displayProgress(this.iTapeData / this.aTapeData.length * 100);
}
else {
this.regPRS |= PDP11.PC11.PRS.ERROR;
}
this.regPRS |= PDP11.PC11.PRS.DONE;
this.regPRS &= ~PDP11.PC11.PRS.BUSY;
if (this.regPRS & PDP11.PC11.PRS.IE) {
this.cpu.setIRQ(this.irqReader);
}
}
}
}
/**
* readPRS(addr)
*
* NOTE: We use the PRS RMASK to honor the "write-only" behavior of bit 0, the reader enable bit (RE), because
* DEC's tiny Bootstrap Loader (/apps/pdp11/boot/bootstrap/BOOTSTRAP-16KB.lst) repeatedly enables the reader using
* the INC instruction, which causes the PRS to be read, incremented, and written, so if bit 0 isn't always read
* as zero, the INC instruction would clear RE instead of setting it.
*
* @this {PC11}
* @param {number} addr (eg, PDP11.UNIBUS.PRS or 177550)
* @return {number}
*/
readPRS(addr)
{
return this.regPRS & PDP11.PC11.PRS.RMASK; // RMASK honors the "write-only" nature of the RE bit by returning zero on reads
}
/**
* writePRS(data, addr)
*
* @this {PC11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.PRS or 177550)
*/
writePRS(data, addr)
{
if (data & PDP11.PC11.PRS.RE) {
/*
* From the 1976 Peripherals Handbook, p. 4-378:
*
* Set [RE] to allow the Reader to fetch one character. The setting of this bit clears Done,
* sets Busy, and clears the Reader Buffer (PRB). Operation of this bit is disabled if Error = 1;
* attempting to set it when Error = 1 will cause an immediate interrupt if Interrupt Enable = 1.
*/
if (this.regPRS & PDP11.PC11.PRS.ERROR) {
data &= ~PDP11.PC11.PRS.RE;
if (this.regPRS & PDP11.PC11.PRS.IE) {
this.cpu.setIRQ(this.irqReader);
}
} else {
this.regPRS &= ~PDP11.PC11.PRS.DONE;
this.regPRS |= PDP11.PC11.PRS.BUSY;
this.regPRB = 0;
/*
* The PC11, by virtue of its "high speed", is supposed to deliver characters at 300 CPS, so
* that's the rate we'll choose as well (ie, 1000ms / 300). As an aside, the original "low speed"
* version of the reader ran at 10 CPS.
*/
this.cpu.setTimer(this.timerReader, this.getBaudTimeout(this.nBaudReceive));
}
}
this.regPRS = (this.regPRS & ~PDP11.PC11.PRS.WMASK) | (data & PDP11.PC11.PRS.WMASK);
}
/**
* readPRB(addr)
*
* @this {PC11}
* @param {number} addr (eg, PDP11.UNIBUS.PRB or 177552)
* @return {number}
*/
readPRB(addr)
{
/*
* I'm guessing that the DONE and BUSY bits always remain more-or-less inverses of each other. They definitely
* start out that way when writePRS() sets the reader enable (RE) bit, and so that's how we treat them elsewhere, too.
*/
this.regPRS &= ~PDP11.PC11.PRS.DONE;
this.regPRS |= PDP11.PC11.PRS.BUSY;
return this.regPRB;
}
/**
* writePRB(data, addr)
*
* @this {PC11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.PRB or 177552)
*/
writePRB(data, addr)
{
}
/**
* readPPS(addr)
*
* @this {PC11}
* @param {number} addr (eg, PDP11.UNIBUS.PPS or 177554)
* @return {number}
*/
readPPS(addr)
{
return this.regPPS;
}
/**
* writePPS(data, addr)
*
* NOTE: This was originally added ONLY because when RT-11 v4.0 copies from device "PC:" (the paper tape reader),
* it executes the following code:
*
* 016010: 005037 177550 CLR @#177550 ;history=2 PRS
* 016014: 005037 177554 CLR @#177554 ;history=1
*
* and as you can see, without this PPS handler, a TRAP to 4 would normally occur. I guess since we claim to be
* a PC11, that makes sense. But what about PDP-11 machines with only a PR11 (ie, a reader-only unit)?
*
* @this {PC11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.PPS or 177554)
*/
writePPS(data, addr)
{
this.regPPS = (this.regPPS & ~PDP11.PC11.PPS.WMASK) | (data & PDP11.PC11.PPS.WMASK);
}
/**
* readPPB(addr)
*
* @this {PC11}
* @param {number} addr (eg, PDP11.UNIBUS.PPB or 177556)
* @return {number}
*/
readPPB(addr)
{
return this.regPPB;
}
/**
* writePPB(data, addr)
*
* @this {PC11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.PPB or 177556)
*/
writePPB(data, addr)
{
this.regPPB = (data & PDP11.PC11.PPB.MASK);
}
}
/*
* There's nothing super special about these values, except that NONE should be falsey and the others should not.
*/
PC11.SOURCE = {
NONE: "",
LOCAL: "?",
REMOTE: "??"
};
PC11.TARGET = {
NONE: 0,
READER: 1,
MEMORY: 2
};
PC11.BINDING = {
READ_PROGRESS: "readProgress"
};
PC11.CSSCLASS = {
PROGRESS_BAR: PDP11.CSSCLASS + "-progress-bar"
};
/*
* ES6 ALERT: As you can see below, I've finally started using computed property names.
*/
PC11.UNIBUS_IOTABLE = {
[PDP11.UNIBUS.PRS]: /* 177550 */ [null, null, PC11.prototype.readPRS, PC11.prototype.writePRS, "PRS"],
[PDP11.UNIBUS.PRB]: /* 177552 */ [null, null, PC11.prototype.readPRB, PC11.prototype.writePRB, "PRB"],
[PDP11.UNIBUS.PPS]: /* 177554 */ [null, null, PC11.prototype.readPPS, PC11.prototype.writePPS, "PPS"],
[PDP11.UNIBUS.PPB]: /* 177556 */ [null, null, PC11.prototype.readPPB, PC11.prototype.writePPB, "PPB"]
};
/**
* @copyright http://pcjs.org/modules/pdp11/lib/disk.js (C) Jeff Parsons 2012-2017
*/
/*
* The DiskPDP11 component provides methods for:
*
* 1) creating an empty disk: create()
* 2) loading a disk image: load()
* 3) getting disk information: info()
* 4) seeking a disk sector: seek()
* 5) reading data from a sector: read()
* 6) writing data to a sector: write()
* 7) save disk deltas: save()
* 8) restore disk deltas: restore()
* 9) converting disk contents: convertToJSON()
*
* More functionality may be factored out of the disk controller components later and moved here,
* to further reduce some of the duplication between them, but the above functionality is a good start.
*/
/**
* Every Sector object (once loaded, parsed, and "normalized") should have ALL of the following named properties:
*
* 'sector': sector number
* 'length': size of the sector, in bytes
* 'data': array of dwords
* 'pattern': dword pattern to use for empty or partial sectors (or null if sector still needs to be loaded)
*
* initSector() also sets the following properties, to help us quickly identify its location within aDiskData:
*
* iCylinder
* iHead
*
* In addition, we will maintain the following information on a per-sector basis, as sectors are modified:
*
* iModify: index of first modified dword in sector
* cModify: number of modified dwords in sector
* fDirty: true if sector is dirty, false if clean (or cleaning in progress)
*
* @typedef {{
* sector: number,
* length: number,
* data: Array.<number>,
* pattern: (number|null),
* iCylinder: number,
* iHead: number,
* iModify: number,
* cModify: number
* }}
*/
var SectorInfo;
class DiskPDP11 extends Component {
/**
* DiskPDP11(controller, drive, mode)
*
* Disk contents are stored as an array (aDiskData) of cylinders, each of which is an array of
* heads, each of which is an array of sector objects; the latter contain sector numbers and
* sector data, where sector data is an array of dwords. The format does not impose any
* limitations on number of cylinders, number of heads, sectors per track, or bytes per sector.
*
* WARNING: All accesses to disk sector properties must be via their string names, not their
* "dot" names, otherwise code will break after it's been processed by the Closure Compiler,
* and any dumped disks may be unmountable. This is a side-effect of how we mount and dump
* disk images (ie, as JSON-encoded streams).
*
* This means, for example, that all references to "track[iSector].data" must actually appear as
* "track[iSector]['data']".
*
* @param {DriveController|RK11|RL11} controller
* @param {Object} drive
* @param {string} mode
*/
constructor(controller, drive, mode)
{
super("Disk", {'id': controller.idMachine + ".disk" + Str.toHex(++DiskPDP11.nDisks, 4)}, MessagesPDP11.DISK);
/*
* Route all non-Debugger messages (eg, notice() and println() calls) through
* this.controller (eg, controller.notice() and controller.println()), because
* the Computer component is unaware of any Disk objects and therefore will not
* set up the usual overrides when a Control Panel is installed.
*/
this.controller = controller;
this.cmp = controller.cmp;
this.dbg = controller.dbg;
this.drive = drive;
/*
* We pull out a number of drive properties that we may or may not need as defaults.
*/
this.sDiskName = drive.name;
this.sDiskPath = this.sDiskFile = "";
this.fRemovable = drive.fRemovable;
/*
* Initialize the disk contents
*/
this.mode = 0;
this.nCylinders = this.nHeads = this.nSectors = this.cbSector = 0;
this.aDiskData = [];
this.dwChecksum = null;
this.fWriteProtected = false;
this.create(mode, drive.nCylinders, drive.nHeads, drive.nSectors, drive.cbSector);
this.fnNotify = this.controllerNotify = null;
this.setReady();
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* We have no real interest in this notification, other than to obtain a reference to the Debugger
* for every disk loaded BEFORE the initBus() phase; any disk loaded AFTER that point will get its Debugger
* reference, if any, from the disk controller passed to the DiskPDP11() constructor.
*
* @this {DiskPDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.dbg = dbg;
}
/**
* create()
*
* @this {DiskPDP11}
* @param {string} mode
* @param {number} nCylinders
* @param {number} nHeads
* @param {number} nSectors (per track)
* @param {number} cbSector
*
* Initializes the disk contents according to the current drive mode and parameters.
*/
create(mode, nCylinders, nHeads, nSectors, cbSector)
{
this.mode = mode;
this.nCylinders = nCylinders;
this.nHeads = nHeads;
this.nSectors = nSectors;
this.cbSector = cbSector;
this.aDiskData = [];
/*
* If the drive is using PRELOAD mode, then it will use the load()/mount() process to initialize the disk contents;
* it wouldn't hurt to let create() do its thing, too, but it's a waste of time.
*/
if (this.mode != DiskAPI.MODE.PRELOAD) {
if (DEBUG && this.messageEnabled()) {
this.printMessage("blank disk for \"" + this.sDiskName + "\": " + this.nCylinders + " cylinders, " + this.nHeads + " head(s)");
}
var aCylinders = new Array(this.nCylinders);
for (var iCylinder = 0; iCylinder < aCylinders.length; iCylinder++) {
var aHeads = new Array(this.nHeads);
for (var iHead = 0; iHead < aHeads.length; iHead++) {
var aSectors = new Array(this.nSectors);
for (var iSector = 1; iSector <= aSectors.length; iSector++) {
/*
* Now that our read() and write() functions can deal with unallocated data
* arrays, and can read/write the specified pattern on-the-fly, we no longer need
* to pre-allocate and pre-initialize the 'data' array.
*/
aSectors[iSector - 1] = this.initSector(null, iCylinder, iHead, iSector, this.cbSector, 0);
}
aHeads[iHead] = aSectors;
}
aCylinders[iCylinder] = aHeads;
}
this.aDiskData = aCylinders;
}
this.dwChecksum = null;
}
/**
* load(sDiskName, sDiskPath, file, fnNotify)
*
* TODO: Figure out how we can strongly type fnNotify, because the Closure Compiler has issues with:
*
* param {function(Component,Object,Disk,string,string)} fnNotify
*
* for:
*
* this.fnNotify.call(this.controller, this.drive, disk, this.sDiskName, this.sDiskPath);
*
* Also, while we're at it, learn if there are ways to:
*
* 1) declare a function taking NO parameters (ie, generate a warning if any parameters are specified)
* 2) declare a type for a function's return value
*
* @this {DiskPDP11}
* @param {string} sDiskName
* @param {string} sDiskPath
* @param {File} [file] is set if there's an associated File object
* @param {function(...)} [fnNotify]
* @param {Component} [controller]
* @return {boolean} true if load completed (successfully or not), false if queued
*/
load(sDiskName, sDiskPath, file, fnNotify, controller)
{
var sDiskURL = sDiskPath;
/*
* We could use this.log() as well, but it wouldn't display which component initiated the load.
*/
if (DEBUG) {
var sMessage = 'load("' + sDiskName + '","' + sDiskPath + '")';
this.controller.log(sMessage);
this.printMessage(sMessage);
}
if (this.fnNotify) {
if (DEBUG) this.controller.log('too many load requests for "' + sDiskName + '" (' + sDiskPath + ')');
return true;
}
this.sDiskName = sDiskName;
this.sDiskPath = sDiskPath;
this.sDiskFile = Str.getBaseName(sDiskPath);
var disk = this;
this.fnNotify = fnNotify;
this.controllerNotify = controller || this.controller;
if (file) {
var reader = new FileReader();
reader.onload = function() {
disk.build(reader.result, true);
};
reader.readAsArrayBuffer(file);
return true;
}
/*
* If there's an occurrence of API_ENDPOINT anywhere in the path, we assume we can use it as-is;
* ie, that the user has already formed a URL of the type we use ourselves for unconverted disk images.
*/
if (sDiskPath.indexOf(DumpAPI.ENDPOINT) < 0) {
/*
* If the selected disk image has a "json" extension, then we assume it's a pre-converted
* JSON-encoded disk image, so we load it as-is; otherwise, we ask our server-side disk image
* converter to return the corresponding JSON-encoded data.
*/
var sDiskExt = Str.getExtension(sDiskPath);
if (sDiskExt == DumpAPI.FORMAT.JSON || sDiskExt == DumpAPI.FORMAT.JSON_GZ) {
sDiskURL = encodeURI(sDiskPath);
} else {
var sDiskParm = DumpAPI.QUERY.PATH;
var sSizeParm = '&' + DumpAPI.QUERY.MBHD + "=10";
/*
* 'mbhd' is a new parm added for hard drive support. In the case of 'file' or 'dir' requests,
* 'mbhd' informs DumpAPI.ENDPOINT that it should create a hard disk image, and one not larger than
* the specified size (eg, 10mb). In fact, until DumpAPI.ENDPOINT is changed to create custom hard
* disk BPBs, you'll always get a standard PC XT 10mb disk image, so if the 'file' or 'dir' contains
* more than 10mb of data, the request will fail. Ultimately, I want to honor the controller's
* driveConfig 'size' parm, or to match the capacity required by the driveConfig 'type' parameter.
*
* If a 'disk' is specified, we pass mbhd=0, because the actual size will depend on the image.
* However, I don't currently have any "dsk" or "img" files containing hard disk images; those formats
* were really intended for floppy disk images. If I never create any hard disk image files, then
* we can simply eliminate sSizeParm in the 'disk' case.
*
* Added more extensions to the list of paths-treated-as-disk-images, so that URLs to files located here:
*
* ftp://ftp.oldskool.org/pub/TOPBENCH/dskimage/
*
* can be used as-is. TODO: There's a TODO in netlib.getFile() regarding remote support that needs
* to be resolved first; DiskDump relies on that function for its remote requests, and it currently
* supports only HTTP.
*/
if (!sDiskPath.indexOf("http:") || !sDiskPath.indexOf("ftp:") || ["dsk", "ima", "img", "360", "720", "12", "144"].indexOf(sDiskExt) >= 0) {
sDiskParm = DumpAPI.QUERY.DISK;
sSizeParm = '&' + DumpAPI.QUERY.MBHD + "=0";
} else if (Str.endsWith(sDiskPath, '/')) {
sDiskParm = DumpAPI.QUERY.DIR;
}
sDiskURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + sDiskParm + '=' + encodeURIComponent(sDiskPath) + (this.fRemovable ? "" : sSizeParm) + "&" + DumpAPI.QUERY.FORMAT + "=" + DumpAPI.FORMAT.JSON;
}
}
return !!Web.getResource(sDiskURL, null, true, function(sURL, sResponse, nErrorCode) {
disk.doneLoad(sURL, sResponse, nErrorCode);
});
}
/**
* build(buffer, fModified)
*
* Builds a disk image from an ArrayBuffer (eg, from a FileReader object), rather than from JSON-encoded data.
*
* @this {DiskPDP11}
* @param {?} buffer (we KNOW this is an ArrayBuffer, but we can't seem to convince the Closure Compiler)
* @param {boolean} [fModified] is true if we should mark the entire disk modified (to ensure that we save/restore it)
*/
build(buffer, fModified)
{
var disk;
var cbDiskData = buffer? buffer.byteLength : 0;
var diskFormat = DiskAPI.GEOMETRIES[cbDiskData];
if (diskFormat) {
this.nCylinders = diskFormat[0];
this.nHeads = diskFormat[1];
this.nSectors = diskFormat[2];
this.cbSector = (diskFormat[3] || 512);
var cdw = this.cbSector >> 2, dwPattern = 0, dwChecksum = 0;
var ib = 0;
var dv = new DataView(buffer, 0, cbDiskData);
this.aDiskData = new Array(this.nCylinders);
for (var iCylinder = 0; iCylinder < this.aDiskData.length; iCylinder++) {
var cylinder = this.aDiskData[iCylinder] = new Array(this.nHeads);
for (var iHead = 0; iHead < cylinder.length; iHead++) {
var head = cylinder[iHead] = new Array(this.nSectors);
for (var iSector = 0; iSector < head.length; iSector++) {
var sector = this.initSector(null, iCylinder, iHead, iSector + 1, this.cbSector, dwPattern);
var adw = sector['data'];
for (var idw = 0; idw < cdw; idw++, ib += 4) {
var dw = adw[idw] = dv.getInt32(ib, true);
dwChecksum = (dwChecksum + dw) & (0xffffffff|0);
}
if (fModified) sector.cModify = cdw;
head[iSector] = sector;
}
}
}
this.dwChecksum = dwChecksum;
disk = this;
} else {
this.notice("Unrecognized disk format (" + cbDiskData + " bytes)");
}
if (this.fnNotify) {
this.fnNotify.call(this.controller, this.drive, disk, this.sDiskName, this.sDiskPath);
this.fnNotify = null;
}
}
/**
* doneLoad(sURL, sDiskData, nErrorCode)
*
* This function was originally called mount(). If the mount is successful, we pass the Disk object to the
* caller's fnNotify handler; otherwise, we pass null.
*
* @this {DiskPDP11}
* @param {string} sURL
* @param {string|null} sDiskData
* @param {number} nErrorCode (response from server if anything other than 200)
*/
doneLoad(sURL, sDiskData, nErrorCode)
{
var disk = null;
var fPrintOnly = (nErrorCode < 0 && !!this.cmp && !this.cmp.flags.powered);
this.fWriteProtected = false;
if (nErrorCode) {
/*
* This can happen for innocuous reasons, such as the user switching away too quickly, forcing
* the request to be cancelled. And unfortunately, the browser cancels XMLHttpRequest requests
* BEFORE it notifies any page event handlers, so if the Computer's being powered down, we won't know
* that yet. For now, we rely on the lack of a specific error (nErrorCode < 0), and suppress the
* notify() alert if there's no specific error AND the computer is not powered up yet.
*/
this.controller.notice("Unable to load disk \"" + this.sDiskName + "\" (error " + nErrorCode + ": " + sURL + ")", fPrintOnly);
} else {
if (DEBUG && this.messageEnabled()) {
this.printMessage('doneLoad("' + this.sDiskPath + '")');
}
Component.addMachineResource(this.controller.idMachine, sURL, sDiskData);
try {
/*
* The following code was a hack to turn on write-protection for a disk image if there was
* an initial comment line containing the string "write-protected". However, since comments
* are technically not allowed in JSON, I needed an alternative solution. So, if the basename
* contains the suffix "-readonly", then I'll turn on write-protection for that disk as well.
*
* TODO: Provide some UI for turning write-protection on/off for disks at will, and provide
* an XML-based solution (ie, a per-disk XML configuration option) for controlling it as well.
*/
var sBaseName = Str.getBaseName(this.sDiskFile, true).toLowerCase();
if (sBaseName.indexOf("-readonly") > 0) {
this.fWriteProtected = true;
} else {
var iEOL = sDiskData.indexOf("\n");
if (iEOL > 0 && iEOL < 1024) {
var sConfig = sDiskData.substring(0, iEOL);
if (sConfig.indexOf("write-protected") > 0) {
this.fWriteProtected = true;
}
}
}
/*
* The most likely source of any exception will be here, where we're parsing the disk data.
*/
var aDiskData;
if (sDiskData.substr(0, 1) == "<") { // if the "data" begins with a "<"...
/*
* Early server configs reported an error (via the nErrorCode parameter) if a disk URL was invalid,
* but more recent server configs now display a somewhat friendlier HTML error page. The downside,
* however, is that the original error has been buried, and we've received "data" that isn't actually
* disk data.
*
* So, if the data we've received appears to be "HTML-like", all we can really do is assume that the
* disk image is missing. And so we pretend we received an error message to that effect.
*/
aDiskData = ["Missing disk image: " + this.sDiskName];
} else {
/*
* TODO: IE9 is rather unfriendly and restrictive with regard to how much data it's willing to
* eval(). In particular, the 10Mb disk image we use for the Windows 1.01 demo config fails in
* IE9 with an "Out of memory" exception. One work-around would be to chop the data into chunks
* (perhaps one track per chunk, using regular expressions) and then manually re-assemble it.
*
* However, it turns out that using JSON.parse(sDiskData) instead of eval("(" + sDiskData + ")")
* is a much easier fix. The only drawback is that we must first quote any unquoted property names
* and remove any comments, because while eval() was cool with them, JSON.parse() is more particular;
* the following RegExp replacements take care of those requirements.
*
* The use of hex values is something else that eval() was OK with, but JSON.parse() is not, and
* while I've stopped using hex values in DumpAPI responses (at least when "format=json" is specified),
* I can't guarantee they won't show up in "legacy" images, and there's no simple RegExp replacement
* for transforming hex values into decimal values, so I cop out and fall back to eval() if I detect
* any hex prefixes ("0x") in the sequence. Ditto for error messages, which appear like so:
*
* ["unrecognized disk path: test.img"]
*/
if (sDiskData.indexOf("0x") < 0 && sDiskData.substr(0, 2) != "[\"") {
aDiskData = JSON.parse(sDiskData.replace(/([a-z]+):/gm, "\"$1\":").replace(/\/\/[^\n]*/gm, ""));
} else {
aDiskData = eval("(" + sDiskData + ")");
}
}
if (!aDiskData.length) {
Component.error("Empty disk image: " + this.sDiskName);
}
else if (aDiskData.length == 1) {
Component.error(aDiskData[0]);
}
/*
* aDiskData is an array of cylinders, each of which is an array of heads, each of which
* is an array of sector objects. The format does not impose any limitations on number of
* cylinders, number of heads, or number of bytes in any of the sector object byte-arrays.
*
* WARNING: All accesses to sector object properties must be via their string names, not their
* "dot" names, otherwise code will break after it's been processed by the Closure Compiler.
*
* Sector object properties include:
*
* 'sector' the sector number (1-based, not required to be sequential)
* 'length' the byte-length (ie, formatted length) of the sector
* 'data' the dword-array containing the sector data
* 'pattern' if the dword-array length is less than 'length'/4, this value must be used
* to pad out the sector; if no 'pattern' is specified, it's assumed to be zero
*
* We still support the older JSON encoding, where sector data was encoded as an array of 'bytes'
* rather than a dword 'data' array. However, our support is strictly limited to an on-the-fly
* conversion to a forward-compatible 'data' array.
*/
else {
if (DEBUG && this.messageEnabled(MessagesPDP11.DISK | MessagesPDP11.LOG)) {
var sCylinders = aDiskData.length + " track" + (aDiskData.length > 1 ? "s" : "");
var nHeads = aDiskData[0].length;
var sHeads = nHeads + " head" + (nHeads > 1 ? "s" : "");
var nSectorsPerTrack = aDiskData[0][0].length;
var sSectorsPerTrack = nSectorsPerTrack + " sector" + (nSectorsPerTrack > 1 ? "s" : "") + "/track";
this.printMessage(sCylinders + ", " + sHeads + ", " + sSectorsPerTrack);
}
/*
* Before the image is usable, we must "normalize" all the sectors. In the past, this meant
* "inflating" them all. However, that's no longer strictly necessary. Mainly, it just means
* setting 'length', 'data', and 'pattern' properties, so that all the sectors are well-defined.
* This includes detecting sector data in older formats (eg, the old array of 'bytes' instead
* of the new 'data' array of dwords) and converting them on-the-fly to the current format.
*/
this.nCylinders = aDiskData.length;
this.nHeads = aDiskData[0].length;
this.nSectors = aDiskData[0][0].length;
var sector = aDiskData[0][0][0];
this.cbSector = (sector && sector['length']) || 512;
var dwChecksum = 0;
for (var iCylinder = 0; iCylinder < this.nCylinders; iCylinder++) {
for (var iHead = 0; iHead < this.nHeads; iHead++) {
for (var iSector = 0; iSector < this.nSectors; iSector++) {
sector = aDiskData[iCylinder][iHead][iSector];
if (!sector) continue; // non-standard (eg, XDF) disk images may have "unused" (null) sectors
var length = sector['length'];
if (length === undefined) { // provide backward-compatibility with older JSON...
length = sector['length'] = 512;
}
length >>= 2; // convert length from a byte-length to a dword-length
var dwPattern = sector['pattern'];
if (dwPattern === undefined) {
dwPattern = sector['pattern'] = 0;
}
var adw = sector['data'];
if (adw === undefined) {
var ab = sector['bytes'];
if (ab === undefined || !ab.length) {
/*
* It would be odd if there was neither a 'bytes' nor 'data' array; I'm just
* being paranoid. It's more likely that the 'bytes' array is simply empty,
* in which case we need only create an empty 'data' array and turn the byte
* pattern, if any, into a dword pattern.
*/
adw = [];
dwPattern = sector['pattern'] = (dwPattern | (dwPattern << 8) | (dwPattern << 16) | (dwPattern << 24));
sector['data'] = adw;
} else {
/*
* To keep the conversion code simple, we'll do any necessary pattern-filling first,
* to fully "inflate" the sector, eliminating the possibility of partial dwords and
* saving any code downstream from dealing with byte-size patterns.
*/
var cb = length << 2;
for (var ib = ab.length; ib < cb; ib++) {
ab[ib] = dwPattern; // the pattern for byte-arrays was only a byte
}
this.fill(sector, ab, 0);
}
delete sector['bytes'];
}
this.initSector(sector, iCylinder, iHead);
/*
* For the disk as a whole, we maintain a checksum of the original unmodified data:
*
* dwChecksum: summation of all dwords in all non-empty sectors
*
* Pattern-filling of sectors is deferred until absolutely necessary (eg, when a sector is
* being written). So all we need to do at this point is checksum all the initial sector data.
*/
for (var idw = 0; idw < adw.length; idw++) {
dwChecksum = (dwChecksum + adw[idw]) & (0xffffffff|0);
}
}
}
}
this.aDiskData = aDiskData;
this.dwChecksum = dwChecksum;
disk = this;
}
} catch (e) {
Component.error("Disk image error (" + sURL + "): " + e.message);
}
}
if (this.fnNotify) {
this.fnNotify.call(this.controllerNotify, this.drive, disk, this.sDiskName, this.sDiskPath);
this.fnNotify = null;
}
}
/**
* getSectorString(sector, off, len)
*
* WARNING: This function is restricted to reading a string contained ENTIRELY within the specified sector.
*
* @this {DiskPDP11}
* @param {Object} sector
* @param {number} off (byte offset)
* @param {number} len (use -1 to read a null-terminated string)
* @return {string}
*/
getSectorString(sector, off, len)
{
var s = "";
while (len--) {
var b = this.read(sector, off++);
if (b <= 0) break;
s += String.fromCharCode(b);
}
return s;
}
/**
* initSector(sector, iCylinder, iHead, iSector, cbSector, dwPattern)
*
* Ensures every sector has ALL the properties of a proper Sector object; ie:
*
* 'sector': sector number
* 'length': size of the sector, in bytes
* 'data': array of dwords
* 'pattern': dword pattern to use for empty or partial sectors
*
* In addition, we will maintain the following information on a per-sector basis,
* as sectors are modified:
*
* iModify: index of first modified dword in sector
* cModify: number of modified dwords in sector
* fDirty: true if sector is dirty, false if clean (or cleaning in progress)
*
* @this {DiskPDP11}
* @param {Object} sector
* @param {number} iCylinder
* @param {number} iHead
* @param {number} [iSector]
* @param {number} [cbSector]
* @param {number|null} [dwPattern]
* @return {Object}
*/
initSector(sector, iCylinder, iHead, iSector, cbSector, dwPattern)
{
if (!sector) {
sector = {'sector': iSector, 'length': cbSector, 'data': [], 'pattern': dwPattern};
}
sector.iCylinder = iCylinder;
sector.iHead = iHead;
sector.iModify = sector.cModify = 0;
sector.fDirty = false;
return sector;
}
/**
* info()
*
* TODO: Decide whether deprecate this in favor of accessing the nCylinders, nHeads, nSectors, and cbSector
* properties of the Disk object directly.
*
* @this {DiskPDP11}
* @return {Array} containing: [nCylinders, nHeads, nSectorsPerTrack, nBytesPerSector]
*/
info()
{
if (!this.aDiskData.length) {
return [0, 0, 0, 0];
}
return [this.aDiskData.length, this.aDiskData[0].length, this.aDiskData[0][0].length, this.aDiskData[0][0][0]['length']];
}
/**
* seek(iCylinder, iHead, iSector, fWrite, done)
*
* TODO: There's some dodgy code in seek() that allows floppy images to be dynamically
* reconfigured with more heads and/or sectors/track, and it does so by peeking at more drive
* properties. That code used to be in the FDC component, where it was perfectly reasonable
* to access those properties. We need a cleaner interface back to the drive, similar to the
* info() interface we provide to the controller.
*
* Whether or not the "dynamic reconfiguration" feature itself is perfectly reasonable is,
* of course, a separate question.
*
* @this {DiskPDP11}
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {boolean} [fWrite]
* @param {function(Object,boolean)} [done]
* @return {Object|null} is the requested sector, or null if not found (or not available yet)
*/
seek(iCylinder, iHead, iSector, fWrite, done)
{
var sector = null;
var drive = this.drive;
var cylinder = this.aDiskData[iCylinder];
if (cylinder) {
var i;
var track = cylinder[iHead];
/*
* The following code allows a single-sided diskette image to be reformatted (ie, "expanded")
* as a double-sided image, provided the drive has more than one head (see drive.nHeads).
*/
if (!track && drive.bFormatting && iHead < drive.nHeads) {
track = cylinder[iHead] = new Array(drive.bSectorEnd);
for (i = 0; i < track.length; i++) {
track[i] = this.initSector(null, iCylinder, iHead, i + 1, drive.nBytes, 0);
}
}
if (track) {
for (i = 0; i < track.length; i++) {
if (track[i] && track[i]['sector'] == iSector) {
/*
* If the sector's pattern is null, then this sector's true contents have not yet
* been fetched from the server.
*/
sector = track[i];
break;
}
}
/*
* The following code allows an 8-sector track to be reformatted (ie, "expanded") as a 9-sector track.
*/
if (!sector && drive.bFormatting && drive.bSector == 9) {
sector = track[i] = this.initSector(null, iCylinder, iHead, drive.bSector, drive.nBytes, 0);
}
}
}
if (done) done(sector, false);
return sector;
}
/**
* fill(sector, ab, off)
*
* @this {DiskPDP11}
* @param {Object} sector
* @param {*} ab (technically, this should be typed as Array.<number> but I'm having trouble coercing JSON.parse() to that)
* @param {number} off
*/
fill(sector, ab, off)
{
var cdw = sector['length'] >> 2;
var adw = new Array(cdw);
for (var idw = 0; idw < cdw; idw++) {
adw[idw] = ab[off] | (ab[off + 1] << 8) | (ab[off + 2] << 16) | (ab[off + 3] << 24);
off += 4;
}
sector['data'] = adw;
/*
* TODO: Consider taking this opportunity to shrink 'data' down by the number of dwords at the end of the buffer that
* contain the same pattern, and setting 'pattern' accordingly.
*/
}
/**
* toBytes(sector)
*
* @this {DiskPDP11}
* @param {Object} sector
* @return {Array.<number>} is an array of bytes
*/
toBytes(sector)
{
var cb = sector['length'];
var ab = new Array(cb);
var ib = 0;
var cdw = cb >> 2;
var adw = sector['data'];
var dwPattern = sector['pattern'];
for (var idw = 0; idw < cdw; idw++) {
var dw = (idw < adw.length? adw[idw] : dwPattern);
ab[ib++] = dw & 0xff;
ab[ib++] = (dw >> 8) & 0xff;
ab[ib++] = (dw >> 16) & 0xff;
ab[ib++] = (dw >> 24) & 0xff;
}
return ab;
}
/**
* read(sector, ibSector, fCompare)
*
* @this {DiskPDP11}
* @param {Object} sector (returned from a previous seek)
* @param {number} ibSector a byte index within the given sector
* @param {boolean} [fCompare] is true if this write-compare read
* @return {number} the specified (unsigned) byte, or -1 if no more data in the sector
*/
read(sector, ibSector, fCompare)
{
var b = -1;
if (sector) {
if (ibSector < sector['length']) {
var adw = sector['data'];
var idw = ibSector >> 2;
var dw = (idw < adw.length ? adw[idw] : sector['pattern']);
b = ((dw >> ((ibSector & 0x3) << 3)) & 0xff);
}
if (DEBUG && !fCompare && this.messageEnabled()) {
this.printMessage('read("' + this.sDiskFile + '",CHS=' + sector.iCylinder + ':' + sector.iHead + ':' + sector['sector'] + ',index=' + ibSector + ',value=' + Str.toHexByte(b) + ')');
}
}
return b;
}
/**
* write(sector, ibSector, b)
*
* @this {DiskPDP11}
* @param {Object} sector (returned from a previous seek)
* @param {number} ibSector a byte index within the given sector
* @param {number} b the byte value to write
* @return {boolean|null} true if write successful, false if write-protected, null if out of bounds
*/
write(sector, ibSector, b)
{
if (this.fWriteProtected)
return false;
if (DEBUG && this.messageEnabled()) {
this.printMessage('write("' + this.sDiskFile + '",CHS=' + sector.iCylinder + ':' + sector.iHead + ':' + sector['sector'] + ',index=' + ibSector + ',value=' + Str.toHexByte(b) + ')');
}
if (ibSector < sector['length']) {
if (b != this.read(sector, ibSector, true)) {
var adw = sector['data'];
var dwPattern = sector['pattern'];
var idw = ibSector >> 2;
var nShift = (ibSector & 0x3) << 3;
/*
* Ensure every byte up to the specified byte is properly initialized.
*/
for (var i = adw.length; i <= idw; i++) adw[i] = dwPattern;
if (!sector.cModify) {
sector.iModify = idw;
sector.cModify = 1;
} else if (idw < sector.iModify) {
sector.cModify += sector.iModify - idw;
sector.iModify = idw;
} else if (idw >= sector.iModify + sector.cModify) {
sector.cModify += idw - (sector.iModify + sector.cModify) + 1;
}
adw[idw] = (adw[idw] & ~(0xff << nShift)) | (b << nShift);
}
return true;
}
return null;
}
/**
* getSector(pba)
*
* @this {DiskPDP11}
* @param {number} pba (physical block address)
* @return {Object|null} sector
*/
getSector(pba)
{
var nSectorsPerCylinder = this.nHeads * this.nSectors;
var iCylinder = (pba / nSectorsPerCylinder) | 0;
if (iCylinder < this.nCylinders) {
var nSectorsRemaining = (pba % nSectorsPerCylinder);
var iHead = (nSectorsRemaining / this.nSectors) | 0;
/*
* PBA numbers are 0-based, but the sector numbers in CHS addressing are 1-based, so add one to iSector
*/
var iSector = (nSectorsRemaining % this.nSectors) + 1;
return this.seek(iCylinder, iHead, iSector);
}
return null;
}
/**
* getSectorData(sector, off, len)
*
* WARNING: This function is restricted to reading data contained ENTIRELY within the specified sector.
*
* NOTE: Yes, this function is not the most efficient way to read a byte/word/dword value from within a sector,
* but given the different states a sector may be in, it's certainly the simplest and safest, and since this is
* only used by buildFileTable() and its progeny, it's not clear that we need to be superfast anyway.
*
* @this {DiskPDP11}
* @param {Object} sector
* @param {number} off (byte offset)
* @param {number} len (1 to 4 bytes)
* @return {number}
*/
getSectorData(sector, off, len)
{
var dw = 0;
var nShift = 0;
while (len--) {
var b = this.read(sector, off++);
if (b < 0) break;
dw |= (b << nShift);
nShift += 8;
}
return dw;
}
/**
* encodeAsBase64()
*
* @this {DiskPDP11}
* @return {string}
*/
encodeAsBase64()
{
/*
* Gross, but simple; more importantly, it works -- at least for disks of typical floppy magnitude.
*/
var s = "", pba = 0, sector;
while ((sector = this.getSector(pba++))) {
for (var off = 0, len = sector['length']; off < len; off++) {
s += String.fromCharCode(this.getSectorData(sector, off, 1));
}
}
return btoa(s);
}
/**
* save()
*
* The first array entry contains some disk information:
*
* [sDiskPath, dwChecksum, nCylinders, nHeads, nSectors, cbSector]
*
* Each subsequent entry in the returned array contains the following:
*
* [iCylinder, iHead, iSector, iModify, [...]]
*
* where [...] is an array of modified dword(s) in the corresponding sector.
*
* @this {DiskPDP11}
* @return {Array} of modified sectors
*/
save()
{
var i = 0;
var deltas = [];
deltas[i++] = [this.sDiskPath, this.dwChecksum, this.nCylinders, this.nHeads, this.nSectors, this.cbSector];
if (!this.fWriteProtected) {
var aDiskData = this.aDiskData;
for (var iCylinder = 0; iCylinder < aDiskData.length; iCylinder++) {
for (var iHead = 0; iHead < aDiskData[iCylinder].length; iHead++) {
for (var iSector = 0; iSector < aDiskData[iCylinder][iHead].length; iSector++) {
var sector = aDiskData[iCylinder][iHead][iSector];
if (sector && sector.cModify) {
var mods = [], n = 0;
var iModify = sector.iModify, iModifyLimit = sector.iModify + sector.cModify;
while (iModify < iModifyLimit) {
mods[n++] = sector['data'][iModify++];
}
deltas[i++] = [iCylinder, iHead, iSector, sector.iModify, mods];
}
}
}
}
}
if (DEBUG && this.messageEnabled()) {
this.printMessage('save("' + this.sDiskName + '"): saved ' + (deltas.length - 1) + ' change(s)');
}
return deltas;
}
/**
* restore(deltas)
*
* The first array entry contains some disk information:
*
* [sDiskPath, dwChecksum, nCylinders, nHeads, nSectors, cbSector]
*
* Each subsequent entry in the supplied array contains the following:
*
* [iCylinder, iHead, iSector, iModify, [...]]
*
* where [...] is an array of modified dword(s) in the corresponding sector.
*
* @this {DiskPDP11}
* @param {Array} deltas
* @return {number} 0 if no changes applied, -1 if an error occurred, otherwise the number of sectors modified
*/
restore(deltas)
{
/*
* If deltas is undefined, that's not necessarily an error; the controller may simply be (re)initializing
* itself (although neither controller should be calling restore() under those conditions anymore).
*/
var nChanges = 0;
var sReason = "unsupported restore format";
/*
* I originally added a check for aDiskData here on the assumption that if there was an error loading
* a disk image, we will have already notified the user, so any additional errors about differing checksums,
* failure to restore the disk state, etc, would just be annoying. HOWEVER, HDC will create an empty disk
* image if its initialization code discovers that no disk was loaded earlier (see verifyDrive). So while
* checking aDiskData is still a good idea, be aware that it won't necessarily avoid redundant error messages
* (at least in the case of HDC).
*/
if (deltas && deltas.length > 0) {
var i = 0;
var aDiskInfo = deltas[i++];
if (aDiskInfo && aDiskInfo.length >= 2) {
/*
* Before getting to the checksum, we have to deal with a new situation: restoring an uninitialized
* disk image from a complete set of deltas. And that is only possible if the disk was saved with the
* original disk geometry.
*/
if (!this.aDiskData.length && aDiskInfo.length >= 6) {
this.create(DiskAPI.MODE.LOCAL, aDiskInfo[2], aDiskInfo[3], aDiskInfo[4], aDiskInfo[5]);
/*
* TODO: Consider setting a flag here that we can check at the end of the restore() function
* that indicates we should recalculate dwChecksum, because we currently have an inconsistency
* between local disks that are mounted via build() and the same disks that are "remounted"
* later by this code; the former has the correct checksum, while the latter has a null checksum.
*
* As you can see below, we currently deal with this by simply ignoring null checksums....
*/
}
/*
* v1.01 failed to indicate an error if either one of these failure conditions occurred. Although maybe that's
* just as well, since v1.01 also failed to properly deal with situations where the user mounted different diskette(s)
* prior to exiting (hopefully fixed in v1.02).
*/
else if (aDiskInfo[1] != null && this.dwChecksum != null && aDiskInfo[1] != this.dwChecksum) {
sReason = "original checksum (" + aDiskInfo[1] + ") differs from current checksum (" + this.dwChecksum + ")";
nChanges = -2;
}
/*
* Checksum is more important than disk path, and for now, I want the flexibility to move disk images.
*
else if (aDiskInfo[0] != this.sDiskPath) {
sReason = "original path '" + aDiskInfo[0] + "' differs from current path '" + this.sDiskPath + "'";
nChanges = -1;
}
*/
}
if (!this.aDiskData.length) nChanges = -1;
while (i < deltas.length && nChanges >= 0) {
var m = 0;
var mod = deltas[i++];
var iCylinder = mod[m++];
var iHead = mod[m++];
var iSector = mod[m++];
/*
* Note the buried test for write-protection. Yes, an invariant condition should be tested
* outside the loop, not inside, but (a) it's a trivial test, (b) the test should never fail
* because save() should never generate any mods for a write-protected disk, and (c) it
* centralizes all the failure conditions we're currently checking (which, admittedly, ain't much).
*/
if (iCylinder >= this.aDiskData.length || iHead >= this.aDiskData[iCylinder].length || iSector >= this.aDiskData[iCylinder][iHead].length) {
sReason = "sector (CHS=" + iCylinder + ':' + iHead + ':' + iSector + ") out of range (" + nChanges + " changes applied)";
nChanges = -1;
break;
}
if (this.fWriteProtected) {
sReason = "unable to modify write-protected disk";
nChanges = -1;
break;
}
var iModify = mod[m++];
var mods = mod[m++];
var iModifyLimit = iModify + mods.length;
var sector = this.aDiskData[iCylinder][iHead][iSector];
if (!sector) continue;
/*
* Since write() now deals with empty/partial sectors, we no longer need to completely "inflate"
* the sector prior to applying modifications. So let's just make sure that the sector is "inflated"
* up to iModify.
*/
var idw = sector['data'].length;
while (idw < iModify) {
sector['data'][idw++] = sector['pattern'];
}
var n = 0;
sector.iModify = iModify;
sector.cModify = mods.length;
while (iModify < iModifyLimit) {
sector['data'][iModify++] = mods[n++];
}
nChanges++;
}
}
if (nChanges < 0) {
/*
* We're suppressing checksum messages for the general public for now....
*/
if (DEBUG || nChanges != -2) {
this.controller.notice("Unable to restore disk '" + this.sDiskName + ": " + sReason);
}
} else {
if (DEBUG && this.messageEnabled()) {
this.printMessage('restore("' + this.sDiskName + '"): restored ' + nChanges + ' change(s)');
}
}
return nChanges;
}
/**
* convertToJSON(fFormatted)
*
* We perform some RegExp massaging on the JSON data to eliminate unnecessary properties
* (eg, 'length' values of 512, 'pattern' values of 0, since those are defaults).
*
* In addition, we first check every sector to see if it can be "deflated". Sectors that were
* initially "deflated" should remain that way unless/until they were modified, so technically,
* we could call deflateSector() just for modified sectors, but this isn't a common operation,
* so it doesn't hurt to check every sector.
*
* @this {DiskPDP11}
* @param {boolean} [fFormatted]
* @return {string} containing the entire disk image as JSON-encoded data
*/
convertToJSON(fFormatted)
{
var s, pba = 0, sector, sectorLast;
while ((sector = this.getSector(pba++))) {
this.deflateSector(sector);
}
s = JSON.stringify(this.aDiskData, function(key, value) {
/*
* If BACKTRACK support is enabled, we have to filter out any 'file' properties that may
* be attached to the sector objects, lest we risk blowing the stack due to circular references.
*/
if (key == 'file') {
return undefined;
}
return value;
});
/*
* Eliminate unnecessary default properties (eg, 'length' values of 512, 'pattern' values of 0).
*/
s = s.replace(/,"length":512/g, "").replace(/,"pattern":0/g, "");
/*
* I don't really want to strip quotes from disk image property names, since I would have to put them
* back again during mount() -- or whenever JSON.parse() is used instead of eval(). But I still remove
* them temporarily, so that any remaining property names (eg, "iModify", "cModify", "fDirty") can
* easily be stripped out, by virtue of their being the only quoted properties left. We then "requote"
* all the property names that remain.
*/
s = s.replace(/"(sector|length|data|pattern)":/g, "$1:");
/*
* The next line will remove any other numeric or boolean properties that were added at runtime, although
* they may have completely different ("minified") names if the code has been compiled.
*/
s = s.replace(/,"[^"]*":([0-9]+|true|false)/g, "");
s = s.replace(/(sector|length|data|pattern):/g, "\"$1\":");
/*
* Last but not least, insert line breaks after every object definition, to improve human readability
* (but only if the caller asks for it).
*/
if (fFormatted) s = s.replace(/([\]}]),/g, "$1,\n");
return s;
}
/**
* deflateSector(sector)
*
* This is just the first revision: it currently looks only at fully inflated sectors.
*
* @this {DiskPDP11}
* @param {Object} sector
*/
deflateSector(sector)
{
var adw = sector['data'];
var cdw = adw.length;
if ((cdw << 2) == sector['length']) {
var idw = cdw - 1;
var dwPattern = adw[idw], cDupes = 0;
while (idw--) {
if (adw[idw] !== dwPattern) break;
cDupes++;
}
if (cDupes++) {
adw.length = cdw - cDupes;
sector['pattern'] = dwPattern;
}
}
}
/**
* dumpSector(sector, pba, sDesc)
*
* @this {DiskPDP11}
* @param {Object} sector (returned from a previous seek)
* @param {number} [pba]
* @param {string} [sDesc]
* @return {string}
*/
dumpSector(sector, pba, sDesc)
{
var sDump = "";
if (DEBUG && sector) {
if (pba != null) sDump += "sector " + pba + (sDesc? (" for " + sDesc) : "") + ':';
var sBytes = "", sChars = "";
var cbSector = sector['length'];
var cdwData = sector['data'].length;
var dw = 0;
for (var i = 0; i < cbSector; i++) {
if ((i % 16) === 0) {
if (sDump) sDump += sBytes + ' ' + sChars + '\n';
sDump += Str.toHex(i, 4) + ": ";
sBytes = sChars = "";
}
if ((i % 4) === 0) {
var idw = i >> 2;
dw = (idw < cdwData? sector['data'][idw] : sector['pattern']);
}
var b = dw & 0xff;
dw >>>= 8;
sBytes += Str.toHex(b, 2) + (i % 16 == 7? "-" : " ");
sChars += (b >= 32 && b < 128? String.fromCharCode(b) : ".");
}
if (sBytes) sDump += sBytes + ' ' + sChars;
}
return sDump;
}
}
/*
* A global disk count, used to form unique Disk component IDs (totally optional; for debugging purposes only)
*/
DiskPDP11.nDisks = 0;
/**
* @copyright http://pcjs.org/modules/pdp11/lib/drive.js (C) Jeff Parsons 2012-2017
*/
/**
* @typedef {{
* PRI: number,
* VEC: number,
* DRIVES: number
* }}
*/
var Config;
/**
* Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default,
* it deters us from defining named properties on our components; eg:
*
* this['exports'] = {...}
*
* results in an error:
*
* Cannot do '[]' access on a struct
*
* So, in order to define 'exports', we must override the @struct assumption by annotating
* the class as @unrestricted (or @dict). Note that this must be done both here and in the
* Component class, because otherwise the Compiler won't allow us to *reference* the named
* property either.
*
* TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to
* define every single property the class uses in its constructor, which results in a fair
* bit of redundant initialization, since many properties aren't (and don't need to be) fully
* initialized until the appropriate init(), reset(), restore(), etc. function is called.
*
* The upside, however, may be that since the structure of the class is completely defined by
* the constructor, JavaScript engines may be able to optimize and run more efficiently.
*
* @unrestricted
*/
class DriveController extends Component {
/**
* DriveController(type, parms, bitsMessage, configDC, configDrive, configIO)
*
* The DriveController component has the following component-specific (parms) properties:
*
* autoMount: one or more JSON-encoded objects, each containing 'name' and 'path' properties
*
* @this {DriveController}
* @param {string} type
* @param {Object} parms
* @param {number} bitsMessage
* @param {Config} configDC
* @param {Array} configDrive
* @param {Object} configIO
*/
constructor(type, parms, bitsMessage, configDC, configDrive, configIO)
{
super(type, parms, bitsMessage);
/*
* We preliminarily parse and record any 'autoMount' object now, but we no longer process it
* until initBus(), because the Computer's getMachineParm() service may have an override for us.
*/
this.configMount = this.parseConfig(parms['autoMount']);
this.cAutoMount = 0;
this.configDC = configDC;
this.configDrive = configDrive;
this.configIO = configIO;
this.nDrives = configDC.DRIVES;
this.aDrives = new Array(this.nDrives);
this.fLocalDisks = (!Web.isMobile() && window && 'FileReader' in window);
this.sDiskSource = DriveController.SOURCE.NONE;
/*
* The following array keeps track of every disk image we've ever mounted. Each entry in the
* array is another array whose elements are:
*
* [0]: name of disk
* [1]: path of disk
* [2]: array of deltas, uninitialized until the disk is unmounted and/or all state is saved
*
* See functions addDiskHistory() and updateDiskHistory().
*/
this.aDiskHistory = [];
this.irq = null;
this['exports'] = {
'bootDisk': this.bootSelectedDisk,
'loadDisk': this.loadSelectedDisk,
'selectDrive': this.selectDrive,
'wait': this.waitDrives
};
}
/**
* parseConfig(config)
*
* @this {DriveController}
* @param {*} config
* @return {*}
*/
parseConfig(config)
{
if (config && typeof config == "string") {
try {
/*
* The most likely source of any exception will be right here, where we're parsing
* this JSON-encoded data.
*/
config = eval("(" + config + ")");
} catch (e) {
Component.error(this.type + " auto-mount error: " + e.message + " (" + config + ")");
config = null;
}
}
return config || {};
}
/**
* setBinding(sType, sBinding, control, sValue)
*
* @this {DriveController}
* @param {string|null} sType is the type of the HTML control (eg, "button", "list", "text", etc)
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "listDisks")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sType, sBinding, control, sValue)
{
var dc = this;
switch (sBinding) {
case "listDisks":
this.bindings[sBinding] = control;
control.onchange = function onChangeListDisks(event) {
dc.updateSelectedDisk();
};
return true;
case "descDisk":
case "listDrives":
this.bindings[sBinding] = control;
/*
* I tried going with onclick instead of onchange, so that if you wanted to confirm what's
* loaded in a particular drive, you could click the drive control without having to change it.
* However, that doesn't seem to work for all browsers, so I've reverted to onchange.
*/
control.onchange = function onChangeListDrives(event) {
var iDrive = Str.parseInt(control.value, 10);
if (iDrive != null) dc.displayDisk(iDrive);
};
return true;
case "loadDisk":
this.bindings[sBinding] = control;
control.onclick = function onClickLoadDrive(event) {
dc.loadSelectedDisk();
};
return true;
case "bootDisk":
this.bindings[sBinding] = control;
control.onclick = function onClickBootDisk(event) {
dc.bootSelectedDisk();
};
return true;
case "saveDisk":
/*
* Yes, technically, this feature does not require "Local disk support" (which is really a reference
* to FileReader support), but since fLocalDisks is also false for all mobile devices, and since there
* is an "orthogonality" to disabling both features in tandem, let's just let it slide, OK?
*/
if (!this.fLocalDisks) {
if (DEBUG) this.log("Local disk support not available");
/*
* We could also simply hide the control; eg:
*
* control.style.display = "none";
*
* but removing the control altogether seems better.
*/
control.parentNode.removeChild(/** @type {Node} */ (control));
return false;
}
this.bindings[sBinding] = control;
control.onclick = function onClickSaveDrive(event) {
var controlDrives = dc.bindings["listDrives"];
if (controlDrives && controlDrives.options && dc.aDrives) {
var iDriveSelected = Str.parseInt(controlDrives.value, 10) || 0;
var drive = dc.aDrives[iDriveSelected];
if (drive) {
/*
* Note the similarity (and hence factoring opportunity) between this code and the HDC's "saveHD*" binding.
*/
var disk = drive.disk;
if (disk) {
if (DEBUG) dc.println("saving disk " + disk.sDiskPath + "...");
var sAlert = Web.downloadFile(disk.encodeAsBase64(), "octet-stream", true, disk.sDiskFile.replace(".json", ".img"));
Component.alertUser(sAlert);
} else {
dc.notice("No disk loaded in drive.");
}
} else {
dc.notice("No disk drive selected.");
}
}
};
return true;
case "mountDisk":
if (!this.fLocalDisks) {
if (DEBUG) this.log("Local disk support not available");
/*
* We could also simply hide the control; eg:
*
* control.style.display = "none";
*
* but removing the control altogether seems better.
*/
control.parentNode.removeChild(/** @type {Node} */ (control));
return false;
}
this.bindings[sBinding] = control;
/*
* Enable "Mount" button only if a file is actually selected
*/
control.addEventListener('change', function() {
var fieldset = control.children[0];
var files = fieldset.children[0].files;
var submit = fieldset.children[1];
submit.disabled = !files.length;
});
control.onsubmit = function(event) {
var file = event.currentTarget[1].files[0];
if (file) {
var sDiskPath = file.name;
var sDiskName = Str.getBaseName(sDiskPath, true);
dc.loadSelectedDisk(sDiskName, sDiskPath, file);
}
/*
* Prevent reloading of web page after form submission
*/
return false;
};
return true;
default:
break;
}
return false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {DriveController}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
var configMount = this.parseConfig(this.cmp.getMachineParm('autoMount'));
/*
* Add only drives from the machine-wide autoMount configuration that match drives managed by this component.
*/
if (configMount) {
for (var sDrive in configMount) {
if (sDrive.substr(0, 2) != this.type.substr(0, 2)) continue;
this.configMount[sDrive] = configMount[sDrive];
}
}
/*
* If we didn't need auto-mount support, we could defer controller and drive initialization until we received
* a powerUp() notification, at which point reset() would call initController(), or restore() would restore the
* controller.
*/
this.reset();
this.irq = this.cpu.addIRQ(this.configDC.VEC, this.configDC.PRI, this.bitsMessage);
bus.addIOTable(this, this.configIO);
bus.addResetHandler(this.reset.bind(this));
this.addDisk("None", DriveController.SOURCE.NONE, true);
if (this.fLocalDisks) this.addDisk("Local Disk", DriveController.SOURCE.LOCAL);
this.addDisk("Remote Disk", DriveController.SOURCE.REMOTE);
if (!this.autoMount()) this.setReady();
}
/**
* getDriveName(iDrive)
*
* Form a drive name using the two-letter controller type prefix and the drive number.
*
* @this {DriveController}
* @param {number} iDrive
* @return {string}
*/
getDriveName(iDrive)
{
var drive = this.aDrives[iDrive];
return drive.sName || "---";
}
/**
* getDriveNumber(sDrive)
*
* @this {DriveController}
* @param {string} sDrive
* @return {number} (0-3, or -1 if error)
*/
getDriveNumber(sDrive)
{
var iDrive = -1;
if (sDrive) {
iDrive = sDrive.charCodeAt(sDrive.length - 1) - 0x30;
if (iDrive < 0 || iDrive > 9) iDrive = -1;
}
return iDrive;
}
/**
* powerUp(data, fRepower)
*
* @this {DriveController}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (!data) {
this.reset();
if (this.cmp.fReload) {
/*
* If the computer's fReload flag is set, we're required to toss all currently
* loaded disks and remount all disks specified in the auto-mount configuration.
*/
this.unloadAllDrives(true);
this.autoMount(true);
}
} else {
if (!this.restore(data)) return false;
}
/*
* Populate the HTML controls to match the actual (well, um, specified) number of floppy drives.
*/
var controlDrives;
if ((controlDrives = this.bindings['listDrives'])) {
while (controlDrives.firstChild) {
controlDrives.removeChild(controlDrives.firstChild);
}
controlDrives.value = "";
for (var iDrive = 0; iDrive < this.nDrives; iDrive++) {
var controlOption = document.createElement("option");
controlOption.value = iDrive;
controlOption.text = this.getDriveName(iDrive);
controlDrives.appendChild(controlOption);
}
if (this.nDrives > 0) {
controlDrives.value = "0";
this.displayDisk(0);
}
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {DriveController}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* reset()
*
* @this {DriveController}
*/
reset()
{
this.initController();
this.initDrives();
}
/**
* save()
*
* This implements save support for the DriveController component.
*
* @this {DriveController}
* @return {Object}
*/
save()
{
var state = new State(this);
state.set(0, this.saveController());
state.set(1, this.saveHistory());
state.set(2, this.saveDrives());
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the DriveController component.
*
* @this {DriveController}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
var fSuccess = true;
if (!this.initController(data[0])) fSuccess = false;
if (!this.initHistory(data[1])) fSuccess = false;
if (!this.initDrives(data[2])) fSuccess = false;
return fSuccess;
}
/**
* initController(aRegs)
*
* Placeholder for subclasses.
*
* @this {DriveController}
* @param {Array} [aRegs]
* @return {boolean} true if successful, false if failure
*/
initController(aRegs)
{
return true;
}
/**
* saveController()
*
* Placeholder for subclasses.
*
* @this {DriveController}
* @return {Array}
*/
saveController()
{
return [];
}
/**
* initDrive(drive, iDrive, configDrive, configDisk)
*
* @this {DriveController}
* @param {Object} drive
* @param {number} iDrive
* @param {Array} configDrive
* @param {Array} [configDisk]
* @return {boolean} true if successful, false if failure
*/
initDrive(drive, iDrive, configDrive, configDisk)
{
var i = 0;
var fSuccess = true;
drive.iDrive = iDrive;
drive.name = this.idComponent;
drive.fBusy = drive.fLocal = false;
drive.fnCallReady = null;
drive.fRemovable = true;
/*
* NOTE: We initialize the following drive properties to their MAXIMUMs; disks may have
* these or SMALLER values (subject to the limits of what the controller supports, of course).
*/
drive.sName = configDrive[i++] + iDrive;
drive.nCylinders = configDrive[i++];
drive.nHeads = configDrive[i++];
drive.nSectors = configDrive[i++];
drive.cbSector = configDrive[i++];
drive.iCylinderBoot = configDrive[i++];
drive.iHeadBoot = configDrive[i++];
drive.iSectorBoot = configDrive[i++];
drive.cbSectorBoot = configDrive[i++];
drive.status = configDrive[i];
/*
* The next group of properties are set by various controller command sequences.
*/
drive.bHead = 0;
drive.bCylinder = 0;
drive.bSector = 1;
drive.bSectorEnd = drive.nSectors; // aka EOT
drive.nBytes = drive.cbSector;
/*
* The next group of properties are managed by worker functions (eg, doRead()) to maintain state across DMA requests.
*/
drive.ibSector = 0;
drive.sector = null;
if (!drive.disk) {
drive.sDiskPath = ""; // ensure this is initialized to a default that displayDisk() can deal with
}
if (configDisk) {
var fLocal = configDisk[0];
var sDiskName = configDisk[1];
var sDiskPath = configDisk[2];
/*
* If we're restoring a local disk image, then the entire disk contents should be captured in aDiskHistory,
* so all we have to do is mount a blank disk and let disk.restore() do the rest; ie, there's nothing to
* "load" (it's a purely synchronous operation).
*
* Otherwise, we must call loadDrive(); in the common case, loadDrive() will have already "auto-mounted"
* the disk, so it will return true, and then we restore any deltas to the current image.
*
* However, if loadDrive() returns false, then it has initiated the load for a *different* disk image,
* so we must mark ourselves as "not ready" again, and add another "wait for ready" test in Computer before
* finally powering the CPU.
*/
if (fLocal) {
this.mountDrive(iDrive, sDiskName, sDiskPath);
}
else if (this.loadDrive(iDrive, sDiskName, sDiskPath, true)) {
if (drive.disk) {
if (sDiskPath) {
this.addDiskHistory(sDiskName, sDiskPath, drive.disk);
} else {
if (MAXDEBUG) Component.warning("Disk '" + (drive.disk.sDiskName || sDiskName) + "' not recorded properly in drive " + iDrive);
}
}
} else {
this.setReady(false);
}
}
return fSuccess;
}
/**
* initDrives(aConfigDisks)
*
* @this {DriveController}
* @param {Array} [aConfigDisks]
* @return {boolean} true if successful, false if failure
*/
initDrives(aConfigDisks)
{
var fSuccess = true;
for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
var drive = this.aDrives[iDrive];
if (drive === undefined) {
drive = this.aDrives[iDrive] = {};
}
var configDisk = aConfigDisks && aConfigDisks[iDrive];
if (!this.initDrive(drive, iDrive, this.configDrive, configDisk)) {
fSuccess = false;
}
}
return fSuccess;
}
/**
* saveDrive(drive)
*
* @this {DriveController}
* @param {Object} drive
* @return {Array}
*/
saveDrive(drive)
{
return [
drive.fLocal,
drive.sDiskName,
drive.sDiskPath
]
}
/**
* saveDrives()
*
* @this {DriveController}
* @return {Array}
*/
saveDrives()
{
var data = [];
for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
data.push(this.saveDrive(this.aDrives[iDrive]));
}
return data;
}
/**
* initHistory(aHistory)
*
* @this {DriveController}
* @param {Array} [aHistory]
* @return {boolean} true if successful, false if failure
*/
initHistory(aHistory)
{
/*
* Initialize the disk history (if available) before initializing the drives, so that any disk deltas can be
* applied to disk images that are already loaded.
*/
if (aHistory) this.aDiskHistory = aHistory;
return true;
}
/**
* saveHistory()
*
* This returns an array of entries, one for each disk image we've ever mounted, including any deltas; ie:
*
* [name, path, deltas]
*
* aDiskHistory contains exactly that, except that deltas may not be up-to-date for any currently mounted
* disk image(s), so we call updateHistory() for all those disks, and then aDiskHistory is ready to be saved.
*
* @this {DriveController}
* @return {Array}
*/
saveHistory()
{
for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
var drive = this.aDrives[iDrive];
if (drive.disk) {
this.updateDiskHistory(drive.sDiskName, drive.sDiskPath, drive.disk);
}
}
return this.aDiskHistory;
}
/**
* autoMount(fRemount)
*
* @this {DriveController}
* @param {boolean} [fRemount] is true if we're remounting all auto-mounted disks
* @return {boolean} true if one or more disk images are being auto-mounted, false if none
*/
autoMount(fRemount)
{
if (!fRemount) this.cAutoMount = 0;
for (var sDrive in this.configMount) {
var configDisk = this.configMount[sDrive];
var sDiskPath = configDisk['path'] || "";
var sDiskName = configDisk['name'] || this.findDisk(sDiskPath);
if (sDiskPath && sDiskName) {
var iDrive = this.getDriveNumber(sDrive);
if (iDrive >= 0 && iDrive < this.aDrives.length) {
if (!this.loadDrive(iDrive, sDiskName, sDiskPath, true) && fRemount) {
this.setReady(false);
}
continue;
}
}
this.notice("Incorrect auto-mount settings for drive " + sDrive + " (" + JSON.stringify(configDisk) + ")");
}
return !!this.cAutoMount;
}
/**
* loadSelectedDisk(sDiskName, sDiskPath, file)
*
* @this {DriveController}
* @param {string} [sDiskName]
* @param {string} [sDiskPath]
* @param {File} [file] is set if there's an associated File object
* @return {boolean}
*/
loadSelectedDisk(sDiskName, sDiskPath, file)
{
if (!sDiskName && !sDiskPath) {
var controlDisks = this.bindings["listDisks"];
if (controlDisks && controlDisks.options) {
sDiskName = controlDisks.options[controlDisks.selectedIndex].text;
sDiskPath = controlDisks.value;
}
}
var controlDrives = this.bindings["listDrives"];
var iDrive = controlDrives && Str.parseInt(controlDrives.value, 10);
if (iDrive === undefined || iDrive < 0 || iDrive >= this.aDrives.length) {
this.notice("Unable to load the selected drive");
return false;
}
if (!sDiskPath) {
this.unloadDrive(iDrive);
return true;
}
if (sDiskPath == DriveController.SOURCE.LOCAL) {
this.notice('Use "Choose File" and "Mount" to select and load a local disk.');
return false;
}
/*
* If the special DriveController.SOURCE.REMOTE path is selected, then we want to prompt the user for a URL.
* Oh, and make sure we pass an empty string as the 2nd parameter to prompt(), so that IE won't display
* "undefined" -- because after all, undefined and "undefined" are EXACTLY the same thing, right?
*
* TODO: This is literally all I've done to support remote disk images. There's probably more
* I should do, like dynamically updating "listDisks" to include new entries, and adding new entries
* to the save/restore data.
*/
if (sDiskPath == DriveController.SOURCE.REMOTE) {
sDiskPath = window.prompt("Enter the URL of a remote disk image.", "") || "";
if (!sDiskPath) return false;
sDiskName = Str.getBaseName(sDiskPath);
this.status("Attempting to load " + sDiskPath + " as \"" + sDiskName + "\"");
this.sDiskSource = DriveController.SOURCE.REMOTE;
}
else {
this.sDiskSource = sDiskPath;
}
this.loadDrive(iDrive, sDiskName, sDiskPath, false, file);
return true;
}
/**
* bootSelectedDisk()
*
* @this {DriveController}
* @return {boolean}
*/
bootSelectedDisk()
{
var drive;
var controlDrives = this.bindings["listDrives"];
var iDrive = controlDrives && Str.parseInt(controlDrives.value, 10);
if (iDrive == null || iDrive < 0 || iDrive >= this.aDrives.length || !(drive = this.aDrives[iDrive])) {
this.notice("Unable to boot the selected drive");
return false;
}
if (!drive.disk) {
this.notice("Load a disk into the drive first");
return false;
}
/*
* NOTE: We're calling setReset() BEFORE reading the boot code in order to eliminate any side-effects
* of the previous state of either the controller OR the CPU; for example, we don't want any previous MMU
* or UNIBUS Map registers affecting the simulated readData() call. Also, some boot code (eg, RSTS/E)
* expects the controller to be in a READY state; since setReset() triggers a call to our reset() handler,
* a READY state is assured, and the readData() call shouldn't do anything to change that.
*/
this.cpu.setReset(0, true, iDrive);
var err = this.readData(drive, drive.iCylinderBoot, drive.iHeadBoot, drive.iSectorBoot, drive.cbSectorBoot, 0x0000, 2);
if (err) {
this.notice("Unable to read the boot sector (" + err + ")");
return false;
}
return true;
}
/**
* mountDrive(iDrive, sDiskName, sDiskPath)
*
* @this {DriveController}
* @param {number} iDrive
* @param {string} sDiskName
* @param {string} sDiskPath
*/
mountDrive(iDrive, sDiskName, sDiskPath)
{
var drive = this.aDrives[iDrive];
this.unloadDrive(iDrive, true);
drive.fLocal = true;
var disk = new DiskPDP11(this, drive, DiskAPI.MODE.PRELOAD);
this.doneLoadDrive(drive, disk, sDiskName, sDiskPath, true);
}
/**
* loadDrive(iDrive, sDiskName, sDiskPath, fAutoMount, file)
*
* NOTE: If sDiskPath is already loaded, nothing needs to be done.
*
* @this {DriveController}
* @param {number} iDrive
* @param {string} sDiskName
* @param {string} sDiskPath
* @param {boolean} [fAutoMount]
* @param {File} [file] is set if there's an associated File object
* @return {number} 1 if disk loaded, 0 if queued up (or busy), -1 if already loaded
*/
loadDrive(iDrive, sDiskName, sDiskPath, fAutoMount, file)
{
var nResult = -1;
var drive = this.aDrives[iDrive];
if (drive.sDiskPath.toLowerCase() != sDiskPath.toLowerCase()) {
nResult++;
this.unloadDrive(iDrive, true);
if (drive.fBusy) {
this.notice(this.type + " busy");
}
else {
// this.status("disk queued: " + sDiskName);
drive.fBusy = true;
if (fAutoMount) {
drive.fAutoMount = true;
this.cAutoMount++;
if (this.messageEnabled()) this.printMessage("auto-loading disk: " + sDiskName);
}
drive.fLocal = !!file;
var disk = new DiskPDP11(this, drive, DiskAPI.MODE.PRELOAD);
if (disk.load(sDiskName, sDiskPath, file, this.doneLoadDrive)) {
nResult++;
}
}
}
return nResult;
}
/**
* doneLoadDrive(drive, disk, sDiskName, sDiskPath, fAutoMount)
*
* The disk parameter is set if the disk was successfully loaded, null if not.
*
* @this {DriveController}
* @param {Object} drive
* @param {DiskPDP11} disk
* @param {string} sDiskName
* @param {string} sDiskPath
* @param {boolean} [fAutoMount]
*/
doneLoadDrive(drive, disk, sDiskName, sDiskPath, fAutoMount)
{
drive.fBusy = false;
if (disk) {
/*
* TODO: While this is a perfectly reasonable thing to do, one wonders if the Disk object shouldn't
* have done this itself, since we passed our Drive object to it (it already knows the drive's limits).
*/
if (disk.nCylinders > drive.nCylinders || disk.nHeads > drive.nHeads /* || disk.nSectors > drive.nSectors */) {
this.notice("Disk \"" + sDiskName + "\" too large for drive " + this.getDriveName(drive.iDrive));
disk = null;
}
}
if (disk) {
drive.disk = disk;
drive.sDiskName = sDiskName;
drive.sDiskPath = sDiskPath;
/*
* Inform the controller implementation (eg, RX11) of the disk change.
*/
this.notifyLoad(drive.iDrive);
/*
* Adding local disk image names to the disk list seems like a nice idea, but it's too confusing,
* because then it looks like the "Mount" button should be able to (re)load them, and that can NEVER
* happen, for security reasons; local disk images can ONLY be loaded via the "Mount" button after
* the user has selected them via the "Choose File" button.
*
* this.addDisk(sDiskName, sDiskPath);
*
* So we're going to take a different approach: when displayDisk() is asked to display the name
* of a local disk image, it will map all such disks to "Local Disk", and any attempt to "Mount" such
* a disk, will essentially result in a "Disk not found" error.
*/
this.addDiskHistory(sDiskName, sDiskPath, disk);
/*
* With the addition of notify(), users are now "alerted" whenever a disk has finished loading;
* notify() is selective about its output, using print() if a print window is open, alert() otherwise.
*/
this.notice("Loaded disk \"" + sDiskName + "\" in drive " + this.getDriveName(drive.iDrive), drive.fAutoMount || fAutoMount);
/*
* Since you usually want the Computer to have focus again after loading a new disk, let's try automatically
* updating the focus after a successful load.
*/
if (this.cmp) this.cmp.setFocus();
}
else {
drive.fLocal = false;
}
if (drive.fAutoMount) {
drive.fAutoMount = false;
if (!--this.cAutoMount) this.setReady();
}
this.displayDisk(drive.iDrive);
if (drive.fnCallReady) {
drive.fnCallReady();
drive.fnCallReady = null;
}
}
/**
* addDisk(sName, sPath, fTop)
*
* @this {DriveController}
* @param {string} sName
* @param {string} sPath
* @param {boolean} [fTop] (default is bottom)
*/
addDisk(sName, sPath, fTop)
{
var controlDisks = this.bindings["listDisks"];
if (controlDisks && controlDisks.options) {
for (var i = 0; i < controlDisks.options.length; i++) {
if (controlDisks.options[i].value == sPath) return;
}
var controlOption = document.createElement("option");
controlOption.text = sName;
controlOption.value = sPath;
if (fTop && controlDisks.childNodes[0]) {
controlDisks.insertBefore(controlOption, controlDisks.childNodes[0]);
} else {
controlDisks.appendChild(controlOption);
}
}
}
/**
* findDisk(sPath)
*
* This is used to deal with mount requests (eg, autoMount) that supply a path without a name;
* if we can find the path in the "listDisks" control, then we return the associated disk name.
*
* @this {DriveController}
* @param {string} sPath
* @return {string|null}
*/
findDisk(sPath)
{
var controlDisks = this.bindings["listDisks"];
if (controlDisks && controlDisks.options) {
for (var i = 0; i < controlDisks.options.length; i++) {
var control = controlDisks.options[i];
if (control.value == sPath) return control.text;
}
}
return Str.getBaseName(sPath, true);
}
/**
* displayDisk(iDrive, fUpdateDrive)
*
* This ensures that the selected disk matches the drive's sDiskPath property, and if fUpdateDrive is set,
* it also ensures that the selected drive matches the specified drive number.
*
* @this {DriveController}
* @param {number} iDrive (unvalidated)
* @param {boolean} [fUpdateDrive] is true to update the drive list to match the specified drive (eg, the auto-mount case)
* @return {boolean} true if successful, false if not
*/
displayDisk(iDrive, fUpdateDrive)
{
/*
* First things first: validate iDrive.
*/
var fSuccess = false;
if (iDrive >= 0 && iDrive < this.aDrives.length) {
var drive = this.aDrives[iDrive];
var controlDisks = this.bindings["listDisks"];
var controlDrives = this.bindings["listDrives"];
/*
* Next, make sure controls for both drives and disks exist.
*/
if (controlDisks && controlDrives && controlDisks.options && controlDrives.options) {
/*
* Next, update the drive if the caller has requested it.
*/
var i;
if (fUpdateDrive) {
for (i = 0; i < controlDrives.options.length; i++) {
if (Str.parseInt(controlDrives.options[i].value, 10) == drive.iDrive) {
if (controlDrives.selectedIndex != i) {
controlDrives.selectedIndex = i;
}
fSuccess = true;
break;
}
}
}
/*
* Next, make sure the drive whose disk we're updating is the currently selected drive.
*/
var iDriveSelected = Str.parseInt(controlDrives.value, 10);
var sTargetPath = (drive.fLocal? DriveController.SOURCE.LOCAL : drive.sDiskPath);
if (!isNaN(iDriveSelected) && iDriveSelected == iDrive) {
for (i = 0; i < controlDisks.options.length; i++) {
if (controlDisks.options[i].value == sTargetPath) {
if (controlDisks.selectedIndex != i) {
controlDisks.selectedIndex = i;
}
fSuccess = true;
break;
}
}
if (i == controlDisks.options.length) controlDisks.selectedIndex = 0;
}
}
}
return fSuccess;
}
/**
* selectDrive(sDrive)
*
* Used to select a drive by name.
*
* @this {DriveController}
* @param {number} sDrive
* @return {boolean} true if successful, false if not
*/
selectDrive(sDrive)
{
var controlDrives = this.bindings["listDrives"];
if (controlDrives && controlDrives.options) {
var nDrives = controlDrives.options.length;
for (var i = 0; i < nDrives; i++) {
if (controlDrives.options[i].textContent == sDrive) {
var iDrive = Str.parseInt(controlDrives.options[i].value, 10);
if (iDrive >= 0) {
return this.displayDisk(iDrive, true);
}
}
}
}
return false;
}
/**
* updateSelectedDisk()
*
* @this {DriveController}
*/
updateSelectedDisk()
{
var control = this.bindings["listDisks"];
var controlDesc = this.bindings["descDisk"];
var controlOption = control.options && control.options[control.selectedIndex];
if (controlDesc && controlOption) {
var dataValue = {};
var sValue = controlOption.getAttribute("data-value");
if (sValue) {
try {
dataValue = eval("(" + sValue + ")");
} catch (e) {
Component.error(this.type + " option error: " + e.message);
}
}
var sHTML = dataValue['desc'];
if (sHTML === undefined) sHTML = "";
var sHRef = dataValue['href'];
if (sHRef !== undefined) sHTML = "<a href=\"" + sHRef + "\" target=\"_blank\">" + sHTML + "</a>";
controlDesc.innerHTML = sHTML;
}
}
/**
* waitDrives(fnCallReady)
*
* @this {DriveController}
* @param {function()|null} fnCallReady
* @return {boolean} false if wait required, true otherwise
*/
waitDrives(fnCallReady)
{
for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
var drive = this.aDrives[iDrive];
if (drive && drive.fBusy) {
if (!drive.fnCallReady) drive.fnCallReady = fnCallReady;
return false;
}
}
return true;
}
/**
* unloadDrive(iDrive, fLoading)
*
* @this {DriveController}
* @param {number} iDrive
* @param {boolean} [fLoading]
*/
unloadDrive(iDrive, fLoading)
{
var drive = this.aDrives[iDrive];
if (drive.disk || fLoading === false) {
/*
* Before we toss the disk's information, capture any deltas that may have occurred.
*/
this.updateDiskHistory(drive.sDiskName, drive.sDiskPath, drive.disk);
drive.sDiskName = "";
drive.sDiskPath = "";
drive.disk = null;
drive.fLocal = false;
if (!fLoading) {
this.notice("Drive " + this.getDriveName(iDrive) + " unloaded", fLoading);
this.sDiskSource = DriveController.SOURCE.NONE;
this.displayDisk(iDrive);
}
/*
* Inform the controller implementation (eg, RX11) of the disk removal.
*/
this.notifyUnload(iDrive);
}
}
/**
* unloadAllDrives(fDiscard)
*
* @this {DriveController}
* @param {boolean} fDiscard to discard all disk history before unloading
*/
unloadAllDrives(fDiscard)
{
if (fDiscard) this.aDiskHistory = [];
for (var iDrive = 0; iDrive < this.aDrives.length; iDrive++) {
this.unloadDrive(iDrive, true);
}
}
/**
* addDiskHistory(sDiskName, sDiskPath, disk)
*
* @this {DriveController}
* @param {string} sDiskName
* @param {string} sDiskPath
* @param {DiskPDP11} disk containing corresponding disk image
*/
addDiskHistory(sDiskName, sDiskPath, disk)
{
var i;
for (i = 0; i < this.aDiskHistory.length; i++) {
if (this.aDiskHistory[i][1] == sDiskPath) {
var nChanges = disk.restore(this.aDiskHistory[i][2]);
if (DEBUG && this.messageEnabled()) {
this.printMessage("disk '" + sDiskName + "' restored from history (" + nChanges + " changes)");
}
return;
}
}
if (DEBUG && this.messageEnabled()) {
this.printMessage("disk '" + sDiskName + "' added to history (nothing to restore)");
}
this.aDiskHistory[i] = [sDiskName, sDiskPath, []];
}
/**
* removeDiskHistory(sDiskName, sDiskPath)
*
* @this {DriveController}
* @param {string} sDiskName
* @param {string} sDiskPath
*/
removeDiskHistory(sDiskName, sDiskPath)
{
var i;
for (i = 0; i < this.aDiskHistory.length; i++) {
if (this.aDiskHistory[i][1] == sDiskPath) {
this.aDiskHistory.splice(i, 1);
if (DEBUG && this.messageEnabled()) {
this.printMessage("disk '" + sDiskName + "' removed from history");
}
return;
}
}
if (DEBUG && this.messageEnabled()) {
this.printMessage("unable to remove disk '" + sDiskName + "' from history (" + sDiskPath + ")");
}
}
/**
* updateDiskHistory(sDiskName, sDiskPath, disk)
*
* @this {DriveController}
* @param {string} sDiskName
* @param {string} sDiskPath
* @param {DiskPDP11} disk containing corresponding disk image, with possible deltas
*/
updateDiskHistory(sDiskName, sDiskPath, disk)
{
var i;
for (i = 0; i < this.aDiskHistory.length; i++) {
if (this.aDiskHistory[i][1] == sDiskPath) {
this.aDiskHistory[i][2] = disk.save();
if (DEBUG && this.messageEnabled()) {
this.printMessage("disk '" + sDiskName + "' updated in history");
}
return;
}
}
/*
* I used to report this as an error (at least in the DEBUG release), but it's no longer really
* an error, because if we're trying to re-mount a clean copy of a disk, we toss its history, then
* unload, and then reload/remount. And since unloadDrive's normal behavior is to call updateDiskHistory()
* before unloading, the fact that the disk is no longer listed here can't be treated as an error.
*/
if (DEBUG && this.messageEnabled()) {
this.printMessage("unable to update disk '" + sDiskName + "' in history (" + sDiskPath + ")");
}
}
/**
* notifyLoad(iDrive)
*
* Placeholder for subclasses. Called whenever DriveController has loaded a new disk into the specified drive.
*
* @this {RX11}
* @param {number} iDrive
*/
notifyLoad(iDrive)
{
}
/**
* notifyUnload(iDrive)
*
* Placeholder for subclasses. Called whenever DriveController has unloaded a disk from the specified drive.
*
* @this {RX11}
* @param {number} iDrive
*/
notifyUnload(iDrive)
{
}
/**
* readData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
*
* Placeholder for subclasses. Implementation is optional, but the automatic BOOT feature will be unavailable.
*
* @this {DriveController}
* @param {Object} drive
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {number} nWords
* @param {number} addr
* @param {number} inc (normally 2, unless inhibited, in which case it's 0)
* @param {boolean} [fCheck]
* @param {function(...)} [done]
* @return {boolean|number} true if complete, false if queued (or if no done() is supplied, the error code, if any)
*/
readData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
{
return -1;
}
/**
* writeData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
*
* Placeholder for subclasses. Implementation is optional.
*
* @this {DriveController}
* @param {Object} drive
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {number} nWords
* @param {number} addr
* @param {number} inc (normally 2, unless inhibited, in which case it's 0)
* @param {boolean} [fCheck]
* @param {function(...)} [done]
* @return {boolean|number} true if complete, false if queued (or if no done() is supplied, the error code, if any)
*/
writeData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
{
return -1;
}
}
/*
* There's nothing super special about these values, except that NONE should be falsey and the others should not.
*/
DriveController.SOURCE = {
NONE: "",
LOCAL: "?",
REMOTE: "??"
};
/**
* @copyright http://pcjs.org/modules/pdp11/lib/rk11.js (C) Jeff Parsons 2012-2017
*/
class RK11 extends DriveController {
/**
* RK11(parms)
*
* The RK11 component has the following component-specific (parms) properties:
*
* autoMount: one or more JSON-encoded objects, each containing 'name' and 'path' properties
*
* The RK11 Disk Controller controls up to eight RK05 disk drives, which in turn read/write RK03-KA
* disk cartridges. See [RK11 Disk Controller Configuration Files](/devices/pdp11/rk11/).
*
* RK03 (or more precisely, RK03-KA) disks are single-platter cartridges with 203 tracks per side,
* 12 sectors per track, and a sector size of 256 words (512 bytes), for a total capacity of 2.38Mb
* (2,494,464 bytes). See [RK03-KA Disk Images](/disks/dec/rk03/).
*
* @param {Object} parms
*/
constructor(parms)
{
super("RK11", parms, MessagesPDP11.RK11, PDP11.RK11, PDP11.RK11.RK05, RK11.UNIBUS_IOTABLE);
/*
* Define all the registers required for this controller.
*
* TODO: Determine what we should really be doing with the RKDB register.
*/
this.regRKDS = this.regRKER = this.regRKCS = this.regRKWC = this.regRKBA = this.regRKDA = this.regRKDB = 0;
}
/**
* initController(aRegs)
*
* @this {RK11}
* @param {Array} [aRegs]
* @return {boolean} true if successful, false if failure
*/
initController(aRegs)
{
if (!aRegs) {
aRegs = [(RK11.RKDS.RK05 | RK11.RKDS.SOK | RK11.RKDS.RRDY), 0, (RK11.RKCS.CRDY), 0, 0, 0, 0];
}
/*
* ES6 ALERT: A handy destructuring assignment, which makes it easy to perform the inverse
* of what saveController() does when it collects a bunch of object properties into an array.
*/
[
this.regRKDS,
this.regRKER,
this.regRKCS,
this.regRKWC,
this.regRKBA,
this.regRKDA,
this.regRKDB
] = aRegs;
return true;
}
/**
* saveController()
*
* Basically, the inverse of initController().
*
* @this {RK11}
* @return {Array}
*/
saveController()
{
return [
this.regRKDS,
this.regRKER,
this.regRKCS,
this.regRKWC,
this.regRKBA,
this.regRKDA,
this.regRKDB
];
}
/**
* processCommand()
*
* @this {RK11}
*/
processCommand()
{
var fInterrupt = true;
var fnReadWrite, func, sFunc = "";
var iDrive = (this.regRKDA & RK11.RKDA.DS) >> RK11.RKDA.SHIFT.DS;
var drive = this.aDrives[iDrive];
var iCylinder, iHead, iSector, nWords, addr, inc;
this.regRKCS &= ~(RK11.RKCS.CRDY | RK11.RKCS.SCP);
this.regRKER &= ~(RK11.RKER.SE);
switch(func = this.regRKCS & RK11.RKCS.FUNC) {
case RK11.FUNC.CRESET:
if (this.messageEnabled()) this.printMessage(this.type + ": CRESET(" + iDrive + ")", true);
this.regRKER = this.regRKDA = 0;
this.regRKCS = RK11.RKCS.CRDY;
break;
case RK11.FUNC.RCHK:
sFunc = "RCHK";
/* falls through */
case RK11.FUNC.READ:
if (!sFunc) sFunc = "READ";
fnReadWrite = this.readData;
/* falls through */
case RK11.FUNC.WCHK:
if (!sFunc) sFunc = "WCHK";
/* falls through */
case RK11.FUNC.WRITE:
if (!sFunc) sFunc = "WRITE";
if (!fnReadWrite) fnReadWrite = this.writeData;
iCylinder = (this.regRKDA & RK11.RKDA.CA) >> RK11.RKDA.SHIFT.CA;
iHead = (this.regRKDA & RK11.RKDA.HS) >> RK11.RKDA.SHIFT.HS;
iSector = this.regRKDA & RK11.RKDA.SA;
nWords = (0x10000 - this.regRKWC) & 0xffff;
addr = (((this.regRKCS & RK11.RKCS.MEX)) << (16 - RK11.RKCS.SHIFT.MEX)) | this.regRKBA;
inc = (this.regRKCS & RK11.RKCS.IBA)? 0 : 2;
if (this.messageEnabled()) this.printMessage(this.type + ": " + sFunc + "(" + iCylinder + ":" + iHead + ":" + iSector + ") " + Str.toOct(addr) + "--" + Str.toOct(addr + (nWords << 1)), true, true);
if (iCylinder >= drive.nCylinders) {
this.regRKER |= RK11.RKER.NXC;
break;
}
if (iSector >= drive.nSectors) {
this.regRKER |= RK11.RKER.NXS;
break;
}
fInterrupt = fnReadWrite.call(this, drive, iCylinder, iHead, iSector, nWords, addr, inc, (func >= RK11.FUNC.WCHK), this.doneReadWrite.bind(this));
break;
case RK11.FUNC.SEEK:
iCylinder = (this.regRKDA & RK11.RKDA.CA) >> RK11.RKDA.SHIFT.CA;
if (this.messageEnabled()) this.printMessage(this.type + ": SEEK(" + iCylinder + ")", true);
if (iCylinder < drive.nCylinders) {
this.regRKCS |= RK11.RKCS.SCP;
} else {
this.regRKER |= RK11.RKER.NXC;
}
break;
case RK11.FUNC.DRESET:
if (this.messageEnabled()) this.printMessage(this.type + ": DRESET(" + iDrive + ")");
this.regRKER = this.regRKDA = 0;
this.regRKCS = RK11.RKCS.CRDY | RK11.RKCS.SCP;
break;
default:
if (this.messageEnabled()) this.printMessage(this.type + ": UNSUPPORTED(" + func + ")");
break;
}
this.regRKDS = drive.status | (drive.disk? RK11.RKDS.DRDY : 0) | (iDrive << RK11.RKDS.SHIFT.ID) | (this.regRKDA & RK11.RKDS.SC);
this.updateErrors();
if (fInterrupt) {
this.regRKCS &= ~RK11.RKCS.GO;
this.regRKCS |= RK11.RKCS.CRDY;
if (this.regRKCS & RK11.RKCS.IE) this.cpu.setIRQ(this.irq);
}
}
/**
* readData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
*
* @this {RK11}
* @param {Object} drive
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {number} nWords
* @param {number} addr
* @param {number} inc (normally 2, unless inhibited, in which case it's 0)
* @param {boolean} [fCheck]
* @param {function(...)} [done]
* @return {boolean|number} true if complete, false if queued (or if no done() is supplied, the error code, if any)
*/
readData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
{
var nError = 0;
var disk = drive.disk;
var sector = null, ibSector;
if (!disk) {
nError = RK11.RKER.NXD;
nWords = 0;
}
var sWords = "";
while (nWords) {
if (!sector) {
if (iCylinder >= disk.nCylinders) {
nError = RK11.RKER.NXC;
break;
}
sector = disk.seek(iCylinder, iHead, iSector + 1);
if (!sector) {
nError = RK11.RKER.SKE;
break;
}
ibSector = 0;
if (++iSector >= disk.nSectors) {
iSector = 0;
if (++iHead >= disk.nHeads) {
iHead = 0;
++iCylinder;
}
}
}
var b0, b1;
if ((b0 = disk.read(sector, ibSector++)) < 0 || (b1 = disk.read(sector, ibSector++)) < 0) {
nError = RK11.RKER.NXS;
break;
}
if (!fCheck) {
var data = b0 | (b1 << 8);
this.bus.setWordDirect(this.cpu.mapUnibus(addr), data);
if (DEBUG && this.messageEnabled(MessagesPDP11.READ)) {
if (!sWords) sWords = Str.toOct(addr) + ": ";
sWords += Str.toOct(data) + ' ';
if (sWords.length >= 64) {
console.log(sWords);
sWords = "";
}
}
if (this.bus.checkFault()) {
nError = RK11.RKER.NXM;
break;
}
}
if (ibSector >= disk.cbSector) sector = null;
addr += inc;
nWords--;
}
return done? done(nError, iCylinder, iHead, iSector, nWords, addr) : nError;
}
/**
* writeData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
*
* @this {RK11}
* @param {Object} drive
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {number} nWords
* @param {number} addr
* @param {number} inc (normally 2, unless inhibited, in which case it's 0)
* @param {boolean} [fCheck]
* @param {function(...)} [done]
* @return {boolean|number} true if complete, false if queued (or if no done() is supplied, the error code, if any)
*/
writeData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
{
var nError = 0;
var disk = drive.disk;
var sector = null, ibSector;
if (!disk) {
nError = RK11.RKER.NXD;
nWords = 0;
}
while (nWords) {
var data = this.bus.getWordDirect(this.cpu.mapUnibus(addr));
if (this.bus.checkFault()) {
nError = RK11.RKER.NXM;
break;
}
if (!sector) {
if (iCylinder >= disk.nCylinders) {
nError = RK11.RKER.NXC;
break;
}
sector = disk.seek(iCylinder, iHead, iSector + 1, true);
if (!sector) {
nError = RK11.RKER.SKE;
break;
}
ibSector = 0;
if (++iSector >= disk.nSectors) {
iSector = 0;
if (++iHead >= disk.nHeads) {
iHead = 0;
++iCylinder;
}
}
}
if (fCheck) {
var b0, b1;
if ((b0 = disk.read(sector, ibSector++)) < 0 || (b1 = disk.read(sector, ibSector++)) < 0) {
nError = RK11.RKER.NXS;
break;
}
/*
* NOTE: During the 11/70 CPU EXERCISER diagnostic, a number of WCHK requests will fail
* when the test starts reading/writing with physical addresses > 177777. I'm pretty sure all
* the UNIBUS address calculations are fine, and therefore those failures are expected.
*
* Originally, those failures were causing me some grief because I was treating a WCE error like
* any other error; ie, as a HARD error. That was wrong. Two errors (WCE and CSE) are soft
* errors, so while they should still trigger the general-purpose RKCS ERR bit, they should NOT
* trigger the RKCS HE (Hard Error) bit. This is all taken care of in updateErrors() now.
*/
if (data != (b0 | (b1 << 8))) {
nError = RK11.RKER.WCE;
break;
}
} else {
if (!disk.write(sector, ibSector++, data & 0xff) || !disk.write(sector, ibSector++, data >> 8)) {
nError = RK11.RKER.NXS;
break;
}
}
if (ibSector >= disk.cbSector) sector = null;
addr += inc;
nWords--;
}
return done? done(nError, iCylinder, iHead, iSector, nWords, addr) : nError;
}
/**
* doneReadWrite(nError, iCylinder, iHead, iSector, nWords, addr)
*
* @this {RK11}
* @param {number} nError
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {number} nWords
* @param {number} addr
* @return {boolean}
*/
doneReadWrite(nError, iCylinder, iHead, iSector, nWords, addr)
{
this.regRKBA = addr & 0xffff;
this.regRKCS = (this.regRKCS & ~RK11.RKCS.MEX) | ((addr >> (16 - RK11.RKCS.SHIFT.MEX)) & RK11.RKCS.MEX);
this.regRKWC = (0x10000 - nWords) & 0xffff;
this.regRKDA = (this.regRKDA & ~RK11.RKDA.SA) | (iSector & RK11.RKDA.SA);
this.regRKER |= nError;
this.updateErrors();
return true;
}
/**
* updateErrors()
*
* @this {RK11}
*/
updateErrors()
{
/*
* Reflect RKER bits to RKCS bits as appropriate.
*
* TODO: I'm not entirely sure about the handling of the DRE bit here. DEC's RK11 documentation says:
*
* Sets if one of the drives in the system senses a loss of either AC or DC power and a function is
* either initiated or in process while the selected drive is not ready or in some error condition.
*
* I'm not sure how to parse all the "ands" and "ors" in that sentence. For now, we're treating the DRE bit
* much like the high error bit found in other hardware registers: we always set it if any lower error bits
* are also set.
*/
this.regRKCS &= ~RK11.RKCS.ERR;
if (this.regRKER) {
this.regRKER |= RK11.RKER.DRE;
this.regRKCS |= RK11.RKCS.ERR;
if (this.regRKER & RK11.RKER.HE) this.regRKCS |= RK11.RKCS.HE;
if (this.messageEnabled()) this.printMessage(this.type + ": ERROR: " + Str.toOct(this.regRKER));
}
}
/**
* readRKDS(addr)
*
* @this {RK11}
* @param {number} addr (eg, PDP11.UNIBUS.RKDS or 177400)
* @return {number}
*/
readRKDS(addr)
{
return this.regRKDS;
}
/**
* writeRKDS(data, addr)
*
* @this {RK11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RKDS or 177400)
*/
writeRKDS(data, addr)
{
/*
* This is a read-only register
*/
}
/**
* readRKER(addr)
*
* @this {RK11}
* @param {number} addr (eg, PDP11.UNIBUS.RKER or 177402)
* @return {number}
*/
readRKER(addr)
{
return this.regRKER;
}
/**
* writeRKER(data, addr)
*
* @this {RK11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RKER or 177402)
*/
writeRKER(data, addr)
{
/*
* This is a read-only register
*/
}
/**
* readRKCS(addr)
*
* @this {RK11}
* @param {number} addr (eg, PDP11.UNIBUS.RKCS or 177404)
* @return {number}
*/
readRKCS(addr)
{
return this.regRKCS & RK11.RKCS.RMASK;
}
/**
* writeRKCS(data, addr)
*
* @this {RK11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RKCS or 177404)
*/
writeRKCS(data, addr)
{
this.regRKCS = (this.regRKCS & ~RK11.RKCS.WMASK) | (data & RK11.RKCS.WMASK);
if (this.regRKCS & RK11.RKCS.GO) this.processCommand();
}
/**
* readRKWC(addr)
*
* @this {RK11}
* @param {number} addr (eg, PDP11.UNIBUS.RKWC or 177406)
* @return {number}
*/
readRKWC(addr)
{
return this.regRKWC;
}
/**
* writeRKWC(data, addr)
*
* @this {RK11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RKWC or 177406)
*/
writeRKWC(data, addr)
{
this.regRKWC = data;
}
/**
* readRKBA(addr)
*
* @this {RK11}
* @param {number} addr (eg, PDP11.UNIBUS.RKBA or 177410)
* @return {number}
*/
readRKBA(addr)
{
return this.regRKBA;
}
/**
* writeRKBA(data, addr)
*
* @this {RK11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RKBA or 177410)
*/
writeRKBA(data, addr)
{
this.regRKBA = data;
}
/**
* readRKDA(addr)
*
* @this {RK11}
* @param {number} addr (eg, PDP11.UNIBUS.RKDA or 177412)
* @return {number}
*/
readRKDA(addr)
{
return this.regRKDA;
}
/**
* writeRKDA(data, addr)
*
* @this {RK11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RKDA or 177412)
*/
writeRKDA(data, addr)
{
this.regRKDA = data;
}
/**
* readRKDB(addr)
*
* @this {RK11}
* @param {number} addr (eg, PDP11.UNIBUS.RKDB or 177416)
* @return {number}
*/
readRKDB(addr)
{
return this.regRKDB;
}
/**
* writeRKDB(data, addr)
*
* @this {RK11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RKDB or 177416)
*/
writeRKDB(data, addr)
{
this.regRKDB = data;
}
}
/*
* Alias RK11 definitions as class constants
*/
RK11.RKDS = PDP11.RK11.RKDS; // 177400: Drive Status Register
RK11.RKER = PDP11.RK11.RKER; // 177402: Error Register
RK11.RKCS = PDP11.RK11.RKCS; // 177404: Control Status Register
RK11.RKDA = PDP11.RK11.RKDA; // 177412: Disk Address Register
RK11.FUNC = PDP11.RK11.FUNC;
/*
* ES6 ALERT: As you can see below, I've finally started using computed property names.
*/
RK11.UNIBUS_IOTABLE = {
[PDP11.UNIBUS.RKDS]: /* 177400 */ [null, null, RK11.prototype.readRKDS, RK11.prototype.writeRKDS, "RKDS"],
[PDP11.UNIBUS.RKER]: /* 177402 */ [null, null, RK11.prototype.readRKER, RK11.prototype.writeRKER, "RKER"],
[PDP11.UNIBUS.RKCS]: /* 177404 */ [null, null, RK11.prototype.readRKCS, RK11.prototype.writeRKCS, "RKCS"],
[PDP11.UNIBUS.RKWC]: /* 177406 */ [null, null, RK11.prototype.readRKWC, RK11.prototype.writeRKWC, "RKWC"],
[PDP11.UNIBUS.RKBA]: /* 177410 */ [null, null, RK11.prototype.readRKBA, RK11.prototype.writeRKBA, "RKBA"],
[PDP11.UNIBUS.RKDA]: /* 177412 */ [null, null, RK11.prototype.readRKDA, RK11.prototype.writeRKDA, "RKDA"],
[PDP11.UNIBUS.RKDB]: /* 177416 */ [null, null, RK11.prototype.readRKDB, RK11.prototype.writeRKDB, "RKDB"]
};
/**
* @copyright http://pcjs.org/modules/pdp11/lib/rl11.js (C) Jeff Parsons 2012-2017
*/
class RL11 extends DriveController {
/**
* RL11(parms)
*
* The RL11 component has the following component-specific (parms) properties:
*
* autoMount: one or more JSON-encoded objects, each containing 'name' and 'path' properties
*
* The RL11 Disk Controller controls up to four RL01 or RL02 disk drives, which in turn read/write RL01K or
* RL02K disk cartridges. See [RL11 Disk Controller Configuration Files](/devices/pdp11/rl11/).
*
* RL01K disks are single-platter cartridges with 256 tracks per side, 40 sectors per track, and a sector size
* of 256 bytes, for a total capacity of 5Mb (5,242,880 bytes). See [RL01K Disk Images](/disks/dec/rl01k/).
*
* RL02K disks are single-platter cartridges with 512 tracks per side, 40 sectors per track, and a sector size
* of 256 bytes, for a total capacity of 10Mb (10,485,760 bytes). See [RL02K Disk Images](/disks/dec/rl02k/).
*
* @param {Object} parms
*/
constructor(parms)
{
super("RL11", parms, MessagesPDP11.RL11, PDP11.RL11, PDP11.RL11.RL02K, RL11.UNIBUS_IOTABLE);
/*
* Define all the registers required for this controller.
*/
this.regRLCS = this.regRLBA = this.regRLDA = this.tmpRLDA = this.regRLMP = this.regRLBE = 0;
}
/**
* initController(aRegs)
*
* @this {RL11}
* @param {Array} [aRegs]
* @return {boolean} true if successful, false if failure
*/
initController(aRegs)
{
if (!aRegs) {
aRegs = [(RL11.RLCS.DRDY | RL11.RLCS.CRDY), 0, 0, 0, 0, 0];
}
/*
* ES6 ALERT: A handy destructuring assignment, which makes it easy to perform the inverse
* of what saveController() does when it collects a bunch of object properties into an array.
*/
[
this.regRLCS,
this.regRLBA,
this.regRLDA,
this.tmpRLDA,
this.regRLMP,
this.regRLBE
] = aRegs;
return true;
}
/**
* saveController()
*
* Basically, the inverse of initController().
*
* @this {RL11}
* @return {Array}
*/
saveController()
{
return [
this.regRLCS,
this.regRLBA,
this.regRLDA,
this.tmpRLDA,
this.regRLMP,
this.regRLBE
];
}
/**
* processCommand()
*
* @this {RL11}
*/
processCommand()
{
var fInterrupt = true;
var fnReadWrite, sFunc = "";
var iDrive = (this.regRLCS & RL11.RLCS.DS) >> RL11.RLCS.SHIFT.DS;
var drive = this.aDrives[iDrive];
var disk = drive.disk;
var iCylinder, iHead, iSector, nWords, addr;
/*
* The typical pattern of DRDY and CRDY:
*
* 1) Normally both set
* 2) CRDY is cleared to process a command
* 3) DRDY is cleared to indicate a command in process
*/
this.regRLCS &= ~RL11.RLCS.DRDY;
switch(this.regRLCS & RL11.RLCS.FUNC) {
case RL11.FUNC.NOP:
case RL11.FUNC.WCHK:
case RL11.FUNC.RDNC:
break;
case RL11.FUNC.STATUS:
if (this.regRLMP & RL11.RLMP.GS_BH) {
this.regRLCS &= (RL11.RLCS.DRDY | RL11.RLCS.FUNC | RL11.RLCS.BAE); // TODO: Review
}
/*
* The bit indicating whether or not the disk contains 256 or 512 cylinders is critical;
* for example, the first RSTS/E disk image we tried was an RL01K, which has only 256 cylinders,
* and the operating system would crash mysteriously if we didn't report the correct geometry.
*/
this.regRLMP = drive.status | (this.tmpRLDA & RL11.RLDA.RW_HS) | (disk && disk.nCylinders == 512? RL11.RLMP.GS_DT : 0);
break;
case RL11.FUNC.SEEK:
if ((this.regRLDA & RL11.RLDA.GS_CMD) == RL11.RLDA.SEEK_CMD) {
var darCA = (this.regRLDA & RL11.RLDA.RW_CA);
var darHS = (this.regRLDA & RL11.RLDA.SEEK_HS) << 2;
if (this.regRLDA & RL11.RLDA.SEEK_DIR) {
this.tmpRLDA += darCA;
} else {
this.tmpRLDA -= darCA;
}
this.regRLDA = this.tmpRLDA = (this.tmpRLDA & RL11.RLDA.RW_CA) | darHS;
}
break;
case RL11.FUNC.RHDR:
this.regRLMP = this.tmpRLDA;
break;
case RL11.FUNC.RDATA:
sFunc = "READ";
fnReadWrite = this.readData;
/* falls through */
case RL11.FUNC.WDATA:
if (!sFunc) sFunc = "WRITE";
if (!fnReadWrite) fnReadWrite = this.writeData;
iCylinder = this.regRLDA >> RL11.RLDA.SHIFT.RW_CA;
iHead = (this.regRLDA & RL11.RLDA.RW_HS)? 1 : 0;
iSector = this.regRLDA & RL11.RLDA.RW_SA;
if (!disk || iCylinder >= disk.nCylinders || iSector >= disk.nSectors) {
this.regRLCS |= RL11.ERRC.HNF | RL11.RLCS.ERR;
break;
}
nWords = (0x10000 - this.regRLMP) & 0xffff;
addr = (((this.regRLBE & RL11.RLBE.MASK)) << 16) | this.regRLBA; // 22 bit mode
if (this.messageEnabled()) this.printMessage(this.type + ": " + sFunc + "(" + iCylinder + ":" + iHead + ":" + iSector + ") " + Str.toOct(addr) + "--" + Str.toOct(addr + (nWords << 1)), true, true);
fInterrupt = fnReadWrite.call(this, drive, iCylinder, iHead, iSector, nWords, addr, 2, false, this.doneReadWrite.bind(this));
break;
default:
break;
}
if (fInterrupt) {
this.regRLCS |= RL11.RLCS.DRDY | RL11.RLCS.CRDY;
if (this.regRLCS & RL11.RLCS.IE) this.cpu.setIRQ(this.irq);
}
}
/**
* readData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
*
* @this {RL11}
* @param {Object} drive
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {number} nWords
* @param {number} addr
* @param {number} inc (normally 2, unless inhibited, in which case it's 0)
* @param {boolean} [fCheck]
* @param {function(...)} [done]
* @return {boolean|number} true if complete, false if queued (or if no done() is supplied, the error code, if any)
*/
readData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
{
var nError = 0;
var checksum = 0;
var disk = drive.disk;
var sector = null, ibSector;
if (!disk) {
nError = RL11.ERRC.HNF; // TODO: Review
nWords = 0;
}
var sWords = "";
while (nWords) {
if (!sector) {
sector = disk.seek(iCylinder, iHead, iSector + 1);
if (!sector) {
nError = RL11.ERRC.HNF;
break;
}
ibSector = 0;
}
var b0, b1, data;
if ((b0 = disk.read(sector, ibSector++)) < 0 || (b1 = disk.read(sector, ibSector++)) < 0) {
nError = RL11.ERRC.HNF;
break;
}
/*
* Apparently, this controller honors the UNIBUS Map registers, which means we must call mapUnibus()
* on the address REGARDLESS whether it is actually >= BusPDP11.UNIBUS_22BIT. TODO: This is inherited
* code, so let's review the documentation on this.
*/
this.bus.setWordDirect(this.cpu.mapUnibus(addr), data = b0 | (b1 << 8));
if (DEBUG && this.messageEnabled(MessagesPDP11.READ)) {
if (!sWords) sWords = Str.toOct(addr) + ": ";
sWords += Str.toOct(data) + ' ';
if (sWords.length >= 64) {
console.log(sWords);
sWords = "";
}
}
if (this.bus.checkFault()) {
nError = RL11.ERRC.NXM;
break;
}
addr += 2;
nWords--;
checksum += data;
if (ibSector >= disk.cbSector) {
sector = null;
if (++iSector >= disk.nSectors) {
iSector = 0;
if (++iHead >= disk.nHeads) {
iHead = 0;
if (++iCylinder >= disk.nCylinders) {
nError = RL11.ERRC.HNF;
break;
}
}
}
}
}
if (DEBUG && this.messageEnabled(MessagesPDP11.READ)) {
console.log("checksum: " + (checksum|0));
}
return done? done(nError, iCylinder, iHead, iSector, nWords, addr) : nError;
}
/**
* writeData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
*
* @this {RL11}
* @param {Object} drive
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {number} nWords
* @param {number} addr
* @param {number} inc (normally 2, unless inhibited, in which case it's 0)
* @param {boolean} [fCheck]
* @param {function(...)} [done]
* @return {boolean|number} true if complete, false if queued (or if no done() is supplied, the error code, if any)
*/
writeData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
{
var nError = 0;
var checksum = 0;
var disk = drive.disk;
var sector = null, ibSector;
if (!disk) {
nError = RL11.ERRC.HNF; // TODO: Review
nWords = 0;
}
var sWords = "";
while (nWords) {
/*
* Apparently, this controller honors the UNIBUS Map registers, which means we must call mapUnibus()
* on the address REGARDLESS whether it is actually >= BusPDP11.UNIBUS_22BIT. TODO: This is inherited
* code, so let's review the documentation on this.
*/
var data = this.bus.getWordDirect(this.cpu.mapUnibus(addr));
if (this.bus.checkFault()) {
nError = RL11.ERRC.NXM;
break;
}
if (DEBUG && this.messageEnabled(MessagesPDP11.WRITE)) {
if (!sWords) sWords = Str.toOct(addr) + ": ";
sWords += Str.toOct(data) + ' ';
if (sWords.length >= 64) {
console.log(sWords);
sWords = "";
}
}
addr += 2;
nWords--;
checksum += data;
if (!sector) {
sector = disk.seek(iCylinder, iHead, iSector + 1, true);
if (!sector) {
nError = RL11.ERRC.HNF;
break;
}
ibSector = 0;
}
if (!disk.write(sector, ibSector++, data & 0xff) || !disk.write(sector, ibSector++, data >> 8)) {
nError = RL11.ERRC.HNF;
break;
}
if (ibSector >= disk.cbSector) {
sector = null;
if (++iSector >= disk.nSectors) {
iSector = 0;
if (++iHead >= disk.nHeads) {
iHead = 0;
if (++iCylinder >= disk.nCylinders) {
nError = RL11.ERRC.HNF;
break;
}
}
}
}
}
if (DEBUG && this.messageEnabled(MessagesPDP11.WRITE)) {
console.log("checksum: " + (checksum|0));
}
return done? done(nError, iCylinder, iHead, iSector, nWords, addr) : nError;
}
/**
* doneReadWrite(nError, iCylinder, iHead, iSector, nWords, addr)
*
* @this {RL11}
* @param {number} nError
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {number} nWords
* @param {number} addr
* @return {boolean}
*/
doneReadWrite(nError, iCylinder, iHead, iSector, nWords, addr)
{
this.regRLBA = addr & 0xffff;
this.regRLCS = (this.regRLCS & ~RL11.RLCS.BAE) | ((addr >> (16 - RL11.RLCS.SHIFT.BAE)) & RL11.RLCS.BAE);
this.regRLBE = (addr >> 16) & RL11.RLBE.MASK; // 22 bit mode
this.regRLDA = (iCylinder << RL11.RLDA.SHIFT.RW_CA) | (iHead? RL11.RLDA.RW_HS : 0) | (iSector & RL11.RLDA.RW_SA);
this.tmpRLDA = this.regRLDA;
this.regRLMP = (0x10000 - nWords) & 0xffff;
if (nError) {
this.regRLCS |= nError | RL11.RLCS.ERR;
}
return true;
}
/**
* readRLCS(addr)
*
* @this {RL11}
* @param {number} addr (eg, PDP11.UNIBUS.RLCS or 174400)
* @return {number}
*/
readRLCS(addr)
{
return this.regRLCS & RL11.RLCS.RMASK;
}
/**
* writeRLCS(data, addr)
*
* @this {RL11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RLCS or 174400)
*/
writeRLCS(data, addr)
{
this.regRLCS = (this.regRLCS & ~RL11.RLCS.WMASK) | (data & RL11.RLCS.WMASK);
this.regRLBE = (this.regRLBE & 0x3C) | ((data & RL11.RLCS.BAE) >> RL11.RLCS.SHIFT.BAE);
if (!(this.regRLCS & RL11.RLCS.CRDY)) this.processCommand();
}
/**
* readRLBA(addr)
*
* @this {RL11}
* @param {number} addr (eg, PDP11.UNIBUS.RLBA or 174402)
* @return {number}
*/
readRLBA(addr)
{
return this.regRLBA;
}
/**
* writeRLBA(data, addr)
*
* @this {RL11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RLBA or 174402)
*/
writeRLBA(data, addr)
{
this.regRLBA = data & RL11.RLBA.WMASK;
}
/**
* readRLDA(addr)
*
* @this {RL11}
* @param {number} addr (eg, PDP11.UNIBUS.RLDA or 174404)
* @return {number}
*/
readRLDA(addr)
{
return this.regRLDA;
}
/**
* writeRLDA(data, addr)
*
* @this {RL11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RLDA or 174404)
*/
writeRLDA(data, addr)
{
this.regRLDA = data;
}
/**
* readRLMP(addr)
*
* @this {RL11}
* @param {number} addr (eg, PDP11.UNIBUS.RLMP or 174406)
* @return {number}
*/
readRLMP(addr)
{
return this.regRLMP;
}
/**
* writeRLMP(data, addr)
*
* @this {RL11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RLMP or 174406)
*/
writeRLMP(data, addr)
{
this.regRLMP = data;
}
/**
* readRLBE(addr)
*
* @this {RL11}
* @param {number} addr (eg, PDP11.UNIBUS.RLBE or 174410)
* @return {number}
*/
readRLBE(addr)
{
return this.regRLBE;
}
/**
* writeRLBE(data, addr)
*
* Curiously, we see RSTS/E v7.0 writing RLBE bits that aren't documented:
*
* R0=000000 R1=000000 R2=174410 R3=000000 R4=102076 R5=045166
* SP=052662 PC=067624 PS=034344 IR=000000 SL=000377 T0 N0 Z1 V0 C0
* 067624: 012712 000300 MOV #300,@R2
*
* @this {RL11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RLBE or 174410)
*/
writeRLBE(data, addr)
{
this.regRLBE = data & RL11.RLBE.MASK;
this.regRLCS = (this.regRLCS & ~RL11.RLCS.BAE) | ((this.regRLBE & 0x3) << RL11.RLCS.SHIFT.BAE);
}
}
/*
* Alias RL11 definitions as class constants
*/
RL11.RLCS = PDP11.RL11.RLCS; // 174400: Control Status Register
RL11.RLBA = PDP11.RL11.RLBA; // 174402: Bus Address Register
RL11.RLDA = PDP11.RL11.RLDA; // 174404: Disk Address Register
RL11.RLMP = PDP11.RL11.RLMP; // 177406: Multi-Purpose Register
RL11.RLBE = PDP11.RL11.RLBE; // 174410: Bus (Address) Extension Register
RL11.ERRC = PDP11.RL11.ERRC; // NOTE: These error codes are pre-shifted to read/write directly from/to RLCS.ERRC
RL11.FUNC = PDP11.RL11.FUNC; // NOTE: These function codes are pre-shifted to read/write directly from/to RLCS.FUNC
/*
* ES6 ALERT: As you can see below, I've finally started using computed property names.
*/
RL11.UNIBUS_IOTABLE = {
[PDP11.UNIBUS.RLCS]: /* 174400 */ [null, null, RL11.prototype.readRLCS, RL11.prototype.writeRLCS, "RLCS"],
[PDP11.UNIBUS.RLBA]: /* 174402 */ [null, null, RL11.prototype.readRLBA, RL11.prototype.writeRLBA, "RLBA"],
[PDP11.UNIBUS.RLDA]: /* 174404 */ [null, null, RL11.prototype.readRLDA, RL11.prototype.writeRLDA, "RLDA"],
[PDP11.UNIBUS.RLMP]: /* 174406 */ [null, null, RL11.prototype.readRLMP, RL11.prototype.writeRLMP, "RLMP"],
[PDP11.UNIBUS.RLBE]: /* 174410 */ [null, null, RL11.prototype.readRLBE, RL11.prototype.writeRLBE, "RLBE"]
};
/**
* @copyright http://pcjs.org/modules/pdp11/lib/rx11.js (C) Jeff Parsons 2012-2017
*/
class RX11 extends DriveController {
/**
* RX11(parms)
*
* The RX11 component has the following component-specific (parms) properties:
*
* autoMount: one or more JSON-encoded objects, each containing 'name' and 'path' properties
*
* The RX11 Disk Controller controls up to two RX01 disk drives, which in turn read/write
* disk cartridges. See [RX11 Disk Controller Configuration Files](/devices/pdp11/rx11/).
*
* RX01 diskettes are single-sided, with 77 tracks per side, 26 sectors per track, and a sector size
* of 128 bytes, for a total capacity of 250Kb (256,256 bytes). See [RX01 Disk Images](/disks/dec/rx01/).
*
* @param {Object} parms
*/
constructor(parms)
{
super("RX11", parms, MessagesPDP11.RX11, PDP11.RX11, PDP11.RX11.RX01, RX11.UNIBUS_IOTABLE);
/*
* Define all the registers required for this controller.
*/
this.regRXCS = this.regRXDB = 0;
this.regRXTA = this.regRXSA = this.regRXES = this.regError = 0;
/*
* Whenever a command is issued, we record the function code internally here, and when the command
* is completed, we set the internal function code back to UNUSED.
*/
this.funCode = RX11.FUNC.UNUSED; // no function in progress (device is idle)
this.iBuffer = 0;
/*
* We use the new ES6 fill() method to ensure that the buffer returns something reasonable if, for some
* strange reason, the first command we receive is an Empty Buffer command.
*/
this.abBuffer = new Array(128).fill(0);
}
/**
* initController(aRegs)
*
* @this {RX11}
* @param {Array} [aRegs]
* @return {boolean} true if successful, false if failure
*/
initController(aRegs)
{
if (!aRegs) {
this.regRXCS = 0;
this.regRXDB = 0;
this.regRXTA = 1;
this.regRXSA = 1;
this.regRXES = 0;
this.regError = 0;
this.funCode = RX11.FUNC.READ;
this.iBuffer = 0;
this.cpu.clearIRQ(this.irq);
this.readSector();
}
else {
/*
* ES6 ALERT: A handy destructuring assignment, which makes it easy to perform the inverse
* of what saveController() does when it collects a bunch of object properties into an array.
*/
[
this.regRXCS,
this.regRXDB,
this.regRXTA,
this.regRXSA,
this.regRXES,
this.regError,
this.funCode,
this.iBuffer,
this.abBuffer
] = aRegs;
}
return true;
}
/**
* saveController()
*
* Basically, the inverse of initController().
*
* @this {RX11}
* @return {Array}
*/
saveController()
{
return [
this.regRXCS,
this.regRXDB,
this.regRXTA,
this.regRXSA,
this.regRXES,
this.regError,
this.funCode,
this.iBuffer,
this.abBuffer
];
}
/**
* notifyLoad(iDrive)
*
* Called whenever DriveController has loaded a new disk into the specified drive.
*
* We're interested in this so that whenever a disk change occurs for drive 0, we can automatically
* refill the sector buffer with the data from sector 1 from track 1.
*
* @this {RX11}
* @param {number} iDrive
*/
notifyLoad(iDrive)
{
if (iDrive == 0) this.initController();
}
/**
* notifyUnload(iDrive)
*
* Called whenever DriveController has unloaded a disk from the specified drive.
*
* @this {RX11}
* @param {number} iDrive
*/
notifyUnload(iDrive)
{
}
/**
* processCommand()
*
* @this {RX11}
*/
processCommand()
{
this.funCode = this.regRXCS & RX11.RXCS.FUNC;
this.regRXCS &= ~(RX11.RXCS.GO | RX11.RXCS.TR | RX11.RXCS.DONE | RX11.RXCS.ERR);
this.cpu.clearIRQ(this.irq);
if (this.messageEnabled()) this.printMessage(this.type + ".processCommand(" + RX11.FUNCS[this.funCode >> 1]+ ")", true, true);
switch(this.funCode) {
case RX11.FUNC.FILL:
case RX11.FUNC.EMPTY:
case RX11.FUNC.READ:
case RX11.FUNC.WRITE:
case RX11.FUNC.WRDEL:
this.initCommand();
break;
case RX11.FUNC.RDSTAT:
this.readStatus();
break;
case RX11.FUNC.RDERR:
this.readError();
break;
default:
break;
}
}
/**
* initCommand()
*
* @this {RX11}
*/
initCommand()
{
this.iBuffer = 0;
}
/**
* doneCommand(nError)
*
* @this {RX11}
* @param {number} [nError]
*/
doneCommand(nError)
{
if (nError) {
this.regError = nError;
this.regRXDB = this.regRXES;
this.regRXCS |= RX11.RXCS.ERR;
}
this.funCode = RX11.FUNC.UNUSED;
this.regRXCS |= RX11.RXCS.DONE;
if (this.regRXCS & RX11.RXCS.IE) this.cpu.setIRQ(this.irq);
}
/**
* readData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
*
* This function is required ONLY if we want to support DriveController's bootSelectedDisk() function (and we do).
*
* @this {RX11}
* @param {Object} drive
* @param {number} iCylinder
* @param {number} iHead
* @param {number} iSector
* @param {number} nWords
* @param {number} addr
* @param {number} inc (normally 2, unless inhibited, in which case it's 0)
* @param {boolean} [fCheck]
* @param {function(...)} [done]
* @return {boolean|number} true if complete, false if queued (or if no done() is supplied, the error code, if any)
*/
readData(drive, iCylinder, iHead, iSector, nWords, addr, inc, fCheck, done)
{
var nError = 0;
var disk = drive.disk;
var sector = null, ibSector;
if (this.messageEnabled()) this.printMessage(this.type + ".readData(" + iCylinder + ":" + iHead + ":" + iSector + ") " + Str.toOct(addr) + "--" + Str.toOct(addr + (nWords << 1)), true, true);
if (!disk) {
nError = drive.iDrive? RX11.ERROR.HOME1 : RX11.ERROR.HOME0;
nWords = 0;
}
var sWords = "";
while (nWords) {
if (!sector) {
if (iCylinder >= disk.nCylinders) {
nError = RX11.ERROR.NO_TRACK;
break;
}
sector = disk.seek(iCylinder, iHead, iSector + 1);
if (!sector) {
nError = RX11.ERROR.NO_SECTOR;
break;
}
ibSector = 0;
if (++iSector >= disk.nSectors) {
iSector = 0;
if (++iHead >= disk.nHeads) {
iHead = 0;
++iCylinder;
}
}
}
var b0, b1;
if ((b0 = disk.read(sector, ibSector++)) < 0 || (b1 = disk.read(sector, ibSector++)) < 0) {
nError = RX11.ERROR.NO_DATA;
break;
}
var data = b0 | (b1 << 8);
this.bus.setWordDirect(this.cpu.mapUnibus(addr), data);
if (DEBUG && this.messageEnabled(MessagesPDP11.READ)) {
if (!sWords) sWords = Str.toOct(addr) + ": ";
sWords += Str.toOct(data) + ' ';
if (sWords.length >= 64) {
console.log(sWords);
sWords = "";
}
}
if (ibSector >= disk.cbSector) sector = null;
addr += inc;
nWords--;
}
return done? done(nError, iCylinder, iHead, iSector, nWords, addr) : nError;
}
/**
* readSector()
*
* @this {RX11}
*/
readSector()
{
var nError = 0;
var iDrive = (this.regRXCS & RX11.RXCS.UNIT)? 1 : 0;
var drive = this.aDrives[iDrive];
var disk = drive && drive.disk;
var iCylinder = this.regRXTA & RX11.RXTA.MASK, iHead = 0, nSector = this.regRXSA & RX11.RXSA.MASK;
this.regRXES &= ~(RX11.RXES.CRC | RX11.RXES.PARITY | RX11.RXES.DEL | RX11.RXES.DRDY);
if (disk) {
this.regRXES |= RX11.RXES.DRDY;
if (this.messageEnabled()) this.printMessage(this.type + ".readSector(" + iCylinder + ":" + iHead + ":" + nSector + ")", true, true);
var sector = disk.seek(iCylinder, iHead, nSector, true);
if (sector) {
var i = 0, nBytes = this.abBuffer.length;
while (i < nBytes) {
var b = disk.read(sector, i);
if (b < 0) {
nError = RX11.ERROR.NO_DATA;
break;
}
this.abBuffer[i++] = b;
}
if (sector.deleted) this.regRXES |= RX11.RXES.DEL;
} else {
nError = RX11.ERROR.NO_SECTOR;
}
} else {
nError = iDrive? RX11.ERROR.HOME1 : RX11.ERROR.HOME0;
}
this.doneCommand(nError);
}
/**
* writeSector(fDeleted)
*
* @this {RX11}
* @param {boolean} fDeleted
*/
writeSector(fDeleted)
{
var nError = 0;
var iDrive = (this.regRXCS & RX11.RXCS.UNIT)? 1 : 0;
var drive = this.aDrives[iDrive];
var disk = drive && drive.disk;
var iCylinder = this.regRXTA & RX11.RXTA.MASK, iHead = 0, nSector = this.regRXSA & RX11.RXSA.MASK;
this.regRXES &= ~(RX11.RXES.CRC | RX11.RXES.PARITY | RX11.RXES.DEL | RX11.RXES.DRDY);
if (disk) {
this.regRXES |= RX11.RXES.DRDY;
if (this.messageEnabled()) this.printMessage(this.type + ".writeSector(" + iCylinder + ":" + iHead + ":" + nSector + ")", true, true);
var sector = disk.seek(iCylinder, iHead, nSector, true);
if (sector) {
if (fDeleted) sector.deleted = true;
var i = 0, nBytes = this.abBuffer.length;
while (i < nBytes) {
var data = this.abBuffer[i];
if (!disk.write(sector, i, data & 0xff)) {
nError = RX11.ERROR.NO_DATA;
break;
}
i++;
}
} else {
nError = RX11.ERROR.NO_SECTOR;
}
} else {
nError = iDrive? RX11.ERROR.HOME1 : RX11.ERROR.HOME0;
}
this.doneCommand(nError);
}
/**
* readStatus()
*
* @this {RX11}
*/
readStatus()
{
var iDrive = (this.regRXCS & RX11.RXCS.UNIT)? 1 : 0;
var drive = this.aDrives[iDrive];
this.regRXES &= ~RX11.RXES.DRDY;
if (drive && drive.disk) this.regRXES |= RX11.RXES.DRDY;
this.regRXDB = this.regRXES;
this.doneCommand();
}
/**
* readError()
*
* @this {RX11}
*/
readError()
{
this.regRXDB = this.regError;
this.doneCommand();
}
/**
* readRXCS(addr)
*
* @this {RX11}
* @param {number} addr (eg, PDP11.UNIBUS.RXCS or 177170)
* @param {boolean} [fPreWrite]
* @return {number}
*/
readRXCS(addr, fPreWrite)
{
var w = this.regRXCS;
if (!fPreWrite) {
w &= RX11.RXCS.RMASK;
switch (this.funCode) {
case RX11.FUNC.FILL:
case RX11.FUNC.EMPTY:
if (this.iBuffer < this.abBuffer.length) {
this.regRXCS |= RX11.RXCS.TR;
}
break;
case RX11.FUNC.READ:
case RX11.FUNC.WRITE:
case RX11.FUNC.WRDEL:
if (this.iBuffer < 2) {
this.regRXCS |= RX11.RXCS.TR;
}
break;
}
}
return w;
}
/**
* writeRXCS(data, addr)
*
* @this {RX11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RXCS or 177170)
*/
writeRXCS(data, addr)
{
this.regRXCS = (this.regRXCS & ~RX11.RXCS.WMASK) | (data & RX11.RXCS.WMASK);
if (this.regRXCS & RX11.RXCS.INIT) {
this.initController();
return;
}
if ((this.regRXCS & RX11.RXCS.GO) && this.funCode == RX11.FUNC.UNUSED) {
this.processCommand();
return;
}
if (!(this.regRXCS & RX11.RXCS.IE)) {
this.cpu.clearIRQ(this.irq);
}
else if (this.regRXCS & RX11.RXCS.DONE) {
this.cpu.setIRQ(this.irq);
}
}
/**
* readRXDB(addr)
*
* @this {RX11}
* @param {number} addr (eg, PDP11.UNIBUS.RXDB or 177172)
* @param {boolean} [fPreWrite]
* @return {number}
*/
readRXDB(addr, fPreWrite)
{
if (!fPreWrite) {
switch (this.funCode) {
case RX11.FUNC.EMPTY:
if (this.regRXCS & RX11.RXCS.TR) {
this.regRXCS &= ~RX11.RXCS.TR;
this.regRXDB = this.abBuffer[this.iBuffer] & 0xff;
if (this.messageEnabled()) this.printMessage(this.type + ".readByte(" + this.iBuffer + "): " + Str.toHexByte(this.regRXDB), true, true);
if (++this.iBuffer >= this.abBuffer.length) {
this.doneCommand();
}
}
break;
}
}
return this.regRXDB;
}
/**
* writeRXDB(data, addr)
*
* @this {RX11}
* @param {number} data
* @param {number} addr (eg, PDP11.UNIBUS.RXDB or 177172)
*/
writeRXDB(data, addr)
{
switch(this.funCode) {
case RX11.FUNC.FILL:
if (this.regRXCS & RX11.RXCS.TR) {
this.regRXCS &= ~RX11.RXCS.TR;
this.abBuffer[this.iBuffer] = data & 0xff;
if (this.messageEnabled()) this.printMessage(this.type + ".writeByte(" + this.iBuffer + "," + Str.toHexByte(data) + ")", true, true);
if (++this.iBuffer >= this.abBuffer.length) {
this.doneCommand();
}
}
break;
case RX11.FUNC.READ:
case RX11.FUNC.WRITE:
case RX11.FUNC.WRDEL:
if (this.regRXCS & RX11.RXCS.TR) {
this.regRXCS &= ~RX11.RXCS.TR;
switch(this.iBuffer++) {
case 0:
this.regRXSA = data;
break;
case 1:
this.regRXTA = data;
if (this.funCode == RX11.FUNC.READ) {
this.readSector();
} else {
this.writeSector(this.funCode == RX11.FUNC.WRDEL);
}
break;
default:
break;
}
}
break;
}
this.regRXDB = data;
}
}
/*
* Alias RX11 definitions as class constants
*/
RX11.RXCS = PDP11.RX11.RXCS; // 177170: Command and Status Register
RX11.RXDB = PDP11.RX11.RXDB; // 177172: Data Buffer Register
RX11.RXTA = PDP11.RX11.RXTA;
RX11.RXSA = PDP11.RX11.RXSA;
RX11.RXES = PDP11.RX11.RXES;
RX11.FUNC = PDP11.RX11.FUNC;
RX11.ERROR = PDP11.RX11.ERROR;
RX11.FUNCS = [
"FILL", "EMPTY", "WRITE", "READ", "UNUSED", "RDSTAT", "WRDEL", "RDERR"
];
/*
* ES6 ALERT: As you can see below, I've finally started using computed property names.
*/
RX11.UNIBUS_IOTABLE = {
[PDP11.UNIBUS.RXCS]: /* 177170 */ [null, null, RX11.prototype.readRXCS, RX11.prototype.writeRXCS, "RXCS"],
[PDP11.UNIBUS.RXDB]: /* 177172 */ [null, null, RX11.prototype.readRXDB, RX11.prototype.writeRXDB, "RXDB"]
};
/**
* @copyright http://pcjs.org/modules/shared/lib/debugger.js (C) Jeff Parsons 2012-2017
*/
/**
* Debugger Address Object
*
* This is the basic structure; other debuggers may extend it.
*
* addr address
* fTemporary true if this is a temporary breakpoint address
* sCmd set for breakpoint addresses if there's an associated command string
* aCmds preprocessed commands (from sCmd)
*
* @typedef {{
* addr:(number|undefined),
* fTemporary:(boolean|undefined),
* sCmd:(string|undefined),
* aCmds:(Array.<string>|undefined)
* }}
*/
var DbgAddr;
/**
* Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default,
* it deters us from defining named properties on our components; eg:
*
* this['exports'] = {...}
*
* results in an error:
*
* Cannot do '[]' access on a struct
*
* So, in order to define 'exports', we must override the @struct assumption by annotating
* the class as @unrestricted (or @dict). Note that this must be done both here and in the
* subclass (eg, SerialPort), because otherwise the Compiler won't allow us to *reference*
* the named property either.
*
* TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to
* define every single property the class uses in its constructor, which results in a fair
* bit of redundant initialization, since many properties aren't (and don't need to be) fully
* initialized until the appropriate init(), reset(), restore(), etc. function is called.
*
* The upside, however, may be that since the structure of the class is completely defined by
* the constructor, JavaScript engines may be able to optimize and run more efficiently.
*
* @unrestricted
*/
class Debugger extends Component
{
/**
* Debugger(parmsDbg)
*
* The Debugger component supports the following optional (parmsDbg) properties:
*
* base: the base to use for most numeric input/output (default is 16)
*
* The Debugger component is a shared component containing a subset of functionality used by
* the other CPU-specific Debuggers (eg, DebuggerX86). Over time, the goal is to factor out as
* much common debugging support as possible from those components into this one.
*
* @param {Object} parmsDbg
*/
constructor(parmsDbg)
{
if (DEBUGGER) {
super("Debugger", parmsDbg);
/*
* Default base used to display all values; modified with the "s base" command.
*/
this.nBase = +parmsDbg['base'] || 16;
/*
* Default number of bits of integer precision; it can be overridden by the Debugger
* but there is no command to adjust it.
*/
this.nBits = 32;
this.achGroup = ['{','}'];
this.achAddress = ['[',']'];
/*
* These keep track of instruction activity, but only when tracing or when Debugger checks
* have been enabled (eg, one or more breakpoints have been set).
*
* They are zeroed by the reset() notification handler. cInstructions is advanced by
* stepCPU() and checkInstruction() calls. nCycles is updated by every stepCPU() or stop()
* call and simply represents the number of cycles performed by the last run of instructions.
*/
this.nCycles = 0;
this.cOpcodes = this.cOpcodesStart = 0;
/*
* fAssemble is true when "assemble mode" is active, false when not.
*/
this.fAssemble = false;
/*
* This maintains command history. New commands are inserted at index 0 of the array.
* When Enter is pressed on an empty input buffer, we default to the command at aPrevCmds[0].
*/
this.iPrevCmd = -1;
this.aPrevCmds = [];
/*
* aVariables is an object with properties that grow as setVariable() assigns more variables;
* each property corresponds to one variable, where the property name is the variable name (ie,
* a string beginning with a non-digit, followed by zero or more symbol characters and/or digits)
* and the property value is the variable's numeric value. See doVar() and setVariable() for
* details.
*
* Note that parseValue() parses variables before numbers, so any variable that looks like a
* unprefixed hex value (eg, "a5" as opposed to "0xa5") will trump the numeric value. Unprefixed
* hex values are a convenience of parseValue(), which always calls Str.parseInt() with a default
* base of 16; however, that default be overridden with a variety of explicit prefixes or suffixes
* (eg, a leading "0o" to indicate octal, a trailing period to indicate decimal, etc.)
*
* See Str.parseInt() for more details about supported numbers.
*/
this.aVariables = {};
} // endif DEBUGGER
}
/**
* getRegIndex(sReg, off)
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {string} sReg
* @param {number} [off] optional offset into sReg
* @return {number} register index, or -1 if not found
*/
getRegIndex(sReg, off)
{
return -1;
}
/**
* getRegValue(iReg)
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {number} iReg
* @return {number|undefined}
*/
getRegValue(iReg)
{
return undefined;
}
/**
* parseAddrReference(s, sAddr)
*
* Returns the given string with the given address reference replaced with the contents of that address.
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {string} s
* @param {string} sAddr
* @return {string}
*/
parseAddrReference(s, sAddr)
{
return s.replace('[' + sAddr + ']', "unimplemented");
}
/**
* getNextCommand()
*
* @this {Debugger}
* @return {string}
*/
getNextCommand()
{
var sCmd;
if (this.iPrevCmd > 0) {
sCmd = this.aPrevCmds[--this.iPrevCmd];
} else {
sCmd = "";
this.iPrevCmd = -1;
}
return sCmd;
}
/**
* getPrevCommand()
*
* @this {Debugger}
* @return {string|null}
*/
getPrevCommand()
{
var sCmd = null;
if (this.iPrevCmd < this.aPrevCmds.length - 1) {
sCmd = this.aPrevCmds[++this.iPrevCmd];
}
return sCmd;
}
/**
* parseCommand(sCmd, fSave, chSep)
*
* @this {Debugger}
* @param {string|undefined} sCmd
* @param {boolean} [fSave] is true to save the command, false if not
* @param {string} [chSep] is the command separator character (default is ';')
* @return {Array.<string>}
*/
parseCommand(sCmd, fSave, chSep)
{
if (fSave) {
if (!sCmd) {
if (this.fAssemble) {
sCmd = "end";
} else {
sCmd = this.aPrevCmds[this.iPrevCmd+1];
}
} else {
if (this.iPrevCmd < 0 && this.aPrevCmds.length) {
this.iPrevCmd = 0;
}
if (this.iPrevCmd < 0 || sCmd != this.aPrevCmds[this.iPrevCmd]) {
this.aPrevCmds.splice(0, 0, sCmd);
this.iPrevCmd = 0;
}
this.iPrevCmd--;
}
}
var a = [];
if (sCmd) {
/*
* With the introduction of breakpoint commands (ie, quoted command sequences
* associated with a breakpoint), we can no longer perform simplistic splitting.
*
* a = sCmd.split(chSep || ';');
* for (var i = 0; i < a.length; i++) a[i] = Str.trim(a[i]);
*
* We may now split on semi-colons ONLY if they are outside a quoted sequence.
*
* Also, to allow quoted strings *inside* breakpoint commands, we first replace all
* DOUBLE double-quotes with single quotes.
*/
sCmd = sCmd.replace(/""/g, "'");
var iPrev = 0;
var chQuote = null;
chSep = chSep || ';';
/*
* NOTE: Processing charAt() up to and INCLUDING length is not a typo; we're taking
* advantage of the fact that charAt() with an invalid index returns an empty string,
* allowing us to use the same substring() call to capture the final portion of sCmd.
*
* In a sense, it allows us to pretend that the string ends with a zero terminator.
*/
for (var i = 0; i <= sCmd.length; i++) {
var ch = sCmd.charAt(i);
if (ch == '"' || ch == "'") {
if (!chQuote) {
chQuote = ch;
} else if (ch == chQuote) {
chQuote = null;
}
}
else if (ch == chSep && !chQuote || !ch) {
/*
* Recall that substring() accepts starting (inclusive) and ending (exclusive)
* indexes, whereas substr() accepts a starting index and a length. We need the former.
*/
a.push(Str.trim(sCmd.substring(iPrev, i)));
iPrev = i + 1;
}
}
}
return a;
}
/**
* evalAND(dst, src)
*
* Adapted from /modules/pdp10/lib/cpuops.js:PDP10.AND().
*
* Performs the bitwise "and" (AND) of two operands > 32 bits.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst & src)
*/
evalAND(dst, src)
{
/*
* We AND the low 32 bits separately from the higher bits, and then combine them with addition.
* Since all bits above 32 will be zero, and since 0 AND 0 is 0, no special masking for the higher
* bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
if (this.nBits <= 32) {
return dst & src;
}
/*
* Negative values don't yield correct results when dividing, so pass them through an unsigned truncate().
*/
dst = this.truncate(dst, 0, true);
src = this.truncate(src, 0, true);
return ((((dst / Debugger.TWO_POW32)|0) & ((src / Debugger.TWO_POW32)|0)) * Debugger.TWO_POW32) + ((dst & src) >>> 0);
}
/**
* evalIOR(dst, src)
*
* Adapted from /modules/pdp10/lib/cpuops.js:PDP10.IOR().
*
* Performs the logical "inclusive-or" (OR) of two operands > 32 bits.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst | src)
*/
evalIOR(dst, src)
{
/*
* We OR the low 32 bits separately from the higher bits, and then combine them with addition.
* Since all bits above 32 will be zero, and since 0 OR 0 is 0, no special masking for the higher
* bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
if (this.nBits <= 32) {
return dst | src;
}
/*
* Negative values don't yield correct results when dividing, so pass them through an unsigned truncate().
*/
dst = this.truncate(dst, 0, true);
src = this.truncate(src, 0, true);
return ((((dst / Debugger.TWO_POW32)|0) | ((src / Debugger.TWO_POW32)|0)) * Debugger.TWO_POW32) + ((dst | src) >>> 0);
}
/**
* evalXOR(dst, src)
*
* Adapted from /modules/pdp10/lib/cpuops.js:PDP10.XOR().
*
* Performs the logical "exclusive-or" (XOR) of two operands > 32 bits.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst ^ src)
*/
evalXOR(dst, src)
{
/*
* We XOR the low 32 bits separately from the higher bits, and then combine them with addition.
* Since all bits above 32 will be zero, and since 0 XOR 0 is 0, no special masking for the higher
* bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
if (this.nBits <= 32) {
return dst | src;
}
/*
* Negative values don't yield correct results when dividing, so pass them through an unsigned truncate().
*/
dst = this.truncate(dst, 0, true);
src = this.truncate(src, 0, true);
return ((((dst / Debugger.TWO_POW32)|0) ^ ((src / Debugger.TWO_POW32)|0)) * Debugger.TWO_POW32) + ((dst ^ src) >>> 0);
}
/**
* evalMUL(dst, src)
*
* I could have adapted the code from /modules/pdp10/lib/cpuops.js:PDP10.doMUL(), but it was simpler to
* write this base method and let the PDP-10 Debugger override it with a call to the *actual* doMUL() method.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst * src)
*/
evalMUL(dst, src)
{
return dst * src;
}
/**
* truncate(v, nBits, fUnsigned)
*
* @this {Debugger}
* @param {number} v
* @param {number} [nBits]
* @param {boolean} [fUnsigned]
* @return {number}
*/
truncate(v, nBits, fUnsigned)
{
var limit, vNew = v;
nBits = nBits || this.nBits;
if (fUnsigned) {
if (nBits == 32) {
vNew = v >>> 0;
}
else if (nBits < 32) {
vNew = v & ((1 << nBits) - 1);
}
else {
limit = Math.pow(2, nBits);
if (v < 0 || v >= limit) {
vNew = v % limit;
if (vNew < 0) vNew += limit;
}
}
}
else {
if (nBits <= 32) {
vNew = (v << (32 - nBits)) >> (32 - nBits);
}
else {
limit = Math.pow(2, nBits - 1);
if (v >= limit) {
vNew = (v % limit);
if (((v / limit)|0) & 1) vNew -= limit;
} else if (v < -limit) {
vNew = (v % limit);
if ((((-v - 1) / limit) | 0) & 1) {
if (vNew) vNew += limit;
}
else {
if (!vNew) vNew -= limit;
}
}
}
}
if (v != vNew) {
if (MAXDEBUG) this.println("warning: value " + v + " truncated to " + vNew);
v = vNew;
}
return v;
}
/**
* evalOps(aVals, aOps, cOps)
*
* Some of our clients want a specific number of bits of integer precision. If that precision is
* greater than 32, some of the operations below will fail; for example, JavaScript bitwise operators
* always truncate the result to 32 bits, so beware when using shift operations. Similarly, it would
* be wrong to always "|0" the final result, which is why we rely on truncate() now.
*
* Note that JavaScript integer precision is limited to 52 bits. For example, in Node, if you set a
* variable to 0x80000001:
*
* foo=0x80000001|0
*
* then calculate foo*foo and display the result in binary using "(foo*foo).toString(2)":
*
* '11111111111111111111111111111100000000000000000000000000000000'
*
* which is slightly incorrect because it has overflowed JavaScript's floating-point precision.
*
* 0x80000001 in decimal is -2147483647, so the product is 4611686014132420609, which is 0x3FFFFFFF00000001.
*
* @this {Debugger}
* @param {Array.<number>} aVals
* @param {Array.<string>} aOps
* @param {number} [cOps] (default is -1 for all)
* @return {boolean} true if successful, false if error
*/
evalOps(aVals, aOps, cOps = -1)
{
while (cOps-- && aOps.length) {
var chOp = aOps.pop();
if (aVals.length < 2) return false;
var valNew;
var val2 = aVals.pop();
var val1 = aVals.pop();
switch(chOp) {
case '*':
valNew = this.evalMUL(val1, val2);
break;
case '/':
if (!val2) return false;
valNew = Math.trunc(val1 / val2);
break;
case '^/':
if (!val2) return false;
valNew = val1 % val2;
break;
case '+':
valNew = val1 + val2;
break;
case '-':
valNew = val1 - val2;
break;
case '<<':
valNew = val1 << val2;
break;
case '>>':
valNew = val1 >> val2;
break;
case '>>>':
valNew = val1 >>> val2;
break;
case '<':
valNew = (val1 < val2? 1 : 0);
break;
case '<=':
valNew = (val1 <= val2? 1 : 0);
break;
case '>':
valNew = (val1 > val2? 1 : 0);
break;
case '>=':
valNew = (val1 >= val2? 1 : 0);
break;
case '==':
valNew = (val1 == val2? 1 : 0);
break;
case '!=':
valNew = (val1 != val2? 1 : 0);
break;
case '&':
valNew = this.evalAND(val1, val2);
break;
case '!': // alias for MACRO-10 to perform a bitwise inclusive-or (OR)
case '|':
valNew = this.evalIOR(val1, val2);
break;
case '^!': // since MACRO-10 uses '^' for base overrides, '^!' is used for bitwise exclusive-or (XOR)
valNew = this.evalXOR(val1, val2);
break;
case '&&':
valNew = (val1 && val2? 1 : 0);
break;
case '||':
valNew = (val1 || val2? 1 : 0);
break;
case ',,':
valNew = this.truncate(val1, 18, true) * Math.pow(2, 18) + this.truncate(val2, 18, true);
break;
case '_':
case '^_':
valNew = val1;
/*
* While we always try to avoid assuming any particular number of bits of precision, the 'B' shift
* operator (which we've converted to '^_') is unique to the MACRO-10 environment, which imposes the
* following restrictions on the shift count.
*/
if (chOp == '^_') val2 = 35 - (val2 & 0xff);
if (val2) {
/*
* Since binary shifting is a logical (not arithmetic) operation, and since shifting by division only
* works properly with positive numbers, we call truncate() to produce an unsigned value.
*/
valNew = this.truncate(valNew, 0, true);
if (val2 > 0) {
valNew *= Math.pow(2, val2);
} else {
valNew = Math.trunc(valNew / Math.pow(2, -val2));
}
}
break;
default:
return false;
}
aVals.push(this.truncate(valNew));
}
return true;
}
/**
* parseArray(asValues, iValue, iLimit, nBase, aUndefined)
*
* parseExpression() takes a complete expression and divides it into array elements, where even elements
* are values (which may be empty if two or more operators appear consecutively) and odd elements are operators.
*
* For example, if the original expression was "2*{3+{4/2}}", parseExpression() would call parseArray() with:
*
* 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
* - - - - - - - - - - -- -- -- -- --
* 2 * { 3 + { 4 / 2 } }
*
* This function takes care of recursively processing grouped expressions, by processing subsets of the array,
* as well as handling certain base overrides (eg, temporarily switching to base-10 for binary shift suffixes).
*
* @param {Array.<string>} asValues
* @param {number} iValue
* @param {number} iLimit
* @param {number} nBase
* @param {Array|undefined} [aUndefined]
* @return {number|undefined}
*/
parseArray(asValues, iValue, iLimit, nBase, aUndefined)
{
var value;
var sValue, sOp;
var fError = false;
var nUnary = 0;
var aVals = [], aOps = [];
var nBasePrev = this.nBase;
this.nBase = nBase;
while (iValue < iLimit) {
var v;
sValue = asValues[iValue++].trim();
sOp = (iValue < iLimit? asValues[iValue++] : "");
if (sValue) {
v = this.parseValue(sValue, null, aUndefined, nUnary);
} else {
if (sOp == '{') {
var cOpen = 1;
var iStart = iValue;
while (iValue < iLimit) {
sValue = asValues[iValue++].trim();
sOp = (iValue < asValues.length? asValues[iValue++] : "");
if (sOp == '{') {
cOpen++;
} else if (sOp == '}') {
if (!--cOpen) break;
}
}
v = this.parseArray(asValues, iStart, iValue-1, this.nBase, aUndefined);
if (v != null && nUnary) {
v = this.parseUnary(v, nUnary);
}
sValue = (iValue < iLimit? asValues[iValue++].trim() : "");
sOp = (iValue < iLimit? asValues[iValue++] : "");
}
else {
/*
* When parseExpression() calls us, it has collapsed all runs of whitespace into single spaces,
* and although it allows single spaces to divide the elements of the expression, a space is neither
* a unary nor binary operator. It's essentially a no-op. If we encounter it here, then it followed
* another operator and is easily ignored (although perhaps it should still trigger a reset of nBase
* and nUnary -- TBD).
*/
if (sOp == ' ') {
continue;
}
if (sOp == '^B') {
this.nBase = 2;
continue;
}
if (sOp == '^O') {
this.nBase = 8;
continue;
}
if (sOp == '^D') {
this.nBase = 10;
continue;
}
if (!(nUnary & (0xC0000000|0))) {
if (sOp == '+') {
continue;
}
if (sOp == '-') {
nUnary = (nUnary << 2) | 1;
continue;
}
if (sOp == '~' || sOp == '^-') {
nUnary = (nUnary << 2) | 2;
continue;
}
if (sOp == '^L') {
nUnary = (nUnary << 2) | 3;
continue;
}
}
fError = true;
break;
}
}
if (v === undefined) {
if (aUndefined) {
aUndefined.push(sValue);
v = 0;
} else {
fError = true;
aUndefined = [];
break;
}
}
aVals.push(this.truncate(v));
/*
* When parseExpression() calls us, it has collapsed all runs of whitespace into single spaces,
* and although it allows single spaces to divide the elements of the expression, a space is neither
* a unary nor binary operator. It's essentially a no-op. If we encounter it here, then it followed
* a value, and since we don't want to misinterpret the next operator as a unary operator, we look
* ahead and grab the next operator if it's not preceded by a value.
*/
if (sOp == ' ') {
if (iValue < asValues.length - 1 && !asValues[iValue]) {
iValue++;
sOp = asValues[iValue++]
} else {
fError = true;
break;
}
}
if (!sOp) break;
var aBinOp = (this.achGroup[0] == '<'? Debugger.aDECOpPrecedence : Debugger.aBinOpPrecedence);
if (!aBinOp[sOp]) {
fError = true;
break;
}
if (aOps.length && aBinOp[sOp] <= aBinOp[aOps[aOps.length - 1]]) {
this.evalOps(aVals, aOps, 1);
}
aOps.push(sOp);
/*
* The MACRO-10 binary shifting operator assumes a base-10 shift count, regardless of the current
* base, so we must override the current base to ensure the count is parsed correctly.
*/
this.nBase = (sOp == '^_')? 10 : nBase;
nUnary = 0;
}
if (fError || !this.evalOps(aVals, aOps) || aVals.length != 1) {
fError = true;
}
if (!fError) {
value = aVals.pop();
} else if (!aUndefined) {
this.println("parse error (" + (sValue || sOp) + ")");
}
this.nBase = nBasePrev;
return value;
}
/**
* parseASCII(sExp, chDelim, nBits, cchMax)
*
* @this {Debugger}
* @param {string} sExp
* @param {string} chDelim
* @param {number} nBits
* @param {number} cchMax
* @return {string|undefined}
*/
parseASCII(sExp, chDelim, nBits, cchMax)
{
var i;
while ((i = sExp.indexOf(chDelim)) >= 0) {
var v = 0;
var j = i + 1;
var cch = cchMax;
while (j < sExp.length) {
var ch = sExp[j++];
if (ch == chDelim) {
cch = -1;
break;
}
if (!cch) break;
cch--;
var c = ch.charCodeAt(0);
if (nBits == 7) {
c &= 0x7F;
} else {
c = (c - 0x20) & 0x3F;
}
v = this.truncate(v * Math.pow(2, nBits) + c, nBits * cchMax, true);
}
if (cch >= 0) {
this.println("parse error (" + chDelim + sExp + chDelim + ")");
return undefined;
} else {
sExp = sExp.substr(0, i) + this.toStrBase(v, -1) + sExp.substr(j);
}
}
return sExp;
}
/**
* parseExpression(sExp, fQuiet)
*
* A quick-and-dirty expression parser. It takes an expression like:
*
* EDX+EDX*4+12345678
*
* and builds a value stack in aVals and a "binop" (binary operator) stack in aOps:
*
* aVals aOps
* ----- ----
* EDX +
* EDX *
* 4 +
* ...
*
* We pop 1 "binop" from aOps and 2 values from aVals whenever a "binop" of lower priority than its
* predecessor is encountered, evaluate, and push the result back onto aVals. Only selected unary
* operators are supported (eg, negate and complement); no ternary operators like '?:' are supported.
*
* fQuiet can be used to pass an array that collects any undefined variables that parseExpression()
* encounters; the value of an undefined variable is zero. This mode was added for components that need
* to support expressions containing "fixups" (ie, values that must be determined later).
*
* @this {Debugger}
* @param {string|undefined} sExp
* @param {Array|undefined|boolean} [fQuiet]
* @return {number|undefined} numeric value, or undefined if sExp contains any undefined or invalid values
*/
parseExpression(sExp, fQuiet)
{
var value = undefined;
var fPrint = (fQuiet === false);
var aUndefined = Array.isArray(fQuiet)? fQuiet : undefined;
if (sExp) {
/*
* The default delimiting characters for grouped expressions are braces; they can be changed by altering
* achGroup, but when that happens, instead of changing our regular expressions and operator tables,
* we simply replace all achGroup characters with braces in the given expression.
*
* Why not use parentheses for grouped expressions? Because some debuggers use parseReference() to perform
* parenthetical value replacements in message strings, and they don't want parentheses taking on a different
* meaning. And for some machines, like the PDP-10, the convention is to use parentheses for other things,
* like indexed addressing, and to use angle brackets for grouped expressions.
*/
if (this.achGroup[0] != '{') {
sExp = sExp.split(this.achGroup[0]).join('{').split(this.achGroup[1]).join('}');
}
/*
* Quoted ASCII characters can have a numeric value, too, which must be converted now, to avoid any
* conflicts with the operators below.
*/
sExp = this.parseASCII(sExp, '"', 7, 5); // MACRO-10 packs up to 5 7-bit ASCII codes into a value
if (!sExp) return value;
sExp = this.parseASCII(sExp, "'", 6, 6); // MACRO-10 packs up to 6 6-bit ASCII (SIXBIT) codes into a value
if (!sExp) return value;
/*
* All browsers (including, I believe, IE9 and up) support the following idiosyncrasy of a RegExp split():
* when the RegExp uses a capturing pattern, the resulting array will include entries for all the pattern
* matches along with the non-matches. This effectively means that, in the set of expressions that we
* support, all even entries in asValues will contain "values" and all odd entries will contain "operators".
*
* Although I started listing the operators in the RegExp in "precedential" order, that's not important;
* what IS important is listing operators than contain shorter operators first. For example, bitwise
* shift operators must be listed BEFORE the logical less-than or greater-than operators. The aBinOp tables
* (aBinOpPrecedence and aDECOpPrecedence) are what determine precedence, not the RegExp.
*
* Also, to better accommodate MACRO-10 syntax, I've replaced the single '^' for XOR with '^!', and I've
* added '!' as an alias for '|' (bitwise inclusive-or), '^-' as an alias for '~' (one's complement operator),
* and '_' as a shift operator (+/- values specify a left/right shift, and the count is not limited to 32).
*
* And to avoid conflicts with MACRO-10 syntax, I've replaced the original mod operator ('%') with '^/'.
*
* The MACRO-10 binary shifting suffix ('B') is a bit more problematic, since a capital B can also appear
* inside symbols, or inside hex values. So if the default base is NOT 16, then I pre-scan for that suffix
* and replace all non-symbolic occurrences with an internal shift operator ('^_').
*
* Note that Str.parseInt(), which parseValue() relies on, supports both the MACRO-10 base prefix overrides
* and the binary shifting suffix ('B'), but since that suffix can also be a bracketed expression, we have to
* support it here as well.
*
* MACRO-10 supports only a subset of all the PCjs operators; for example, MACRO-10 doesn't support any of
* the boolean logical/compare operators. But unless we run into conflicts, I prefer sticking with this
* common set of operators.
*
* All whitespace in the expression is collapsed to single spaces, and space has been added to the list
* of "operators", but its sole function is as a separator, not as an operator. parseArray() will ignore
* single spaces as long as they are preceded and/or followed by a "real" operator. It would be dangerous
* to remove spaces entirely, because if an operator-less expression like "A B" was passed in, we would want
* that to generate an error; if we converted it to "AB", evaluation might inadvertently succeed.
*/
var regExp = /({|}|\|\||&&|\||\^!|\^B|\^O|\^D|\^L|\^-|~|\^_|_|&|!=|!|==|>=|>>>|>>|>|<=|<<|<|-|\+|\^\/|\/|\*|,,| )/;
if (this.nBase != 16) {
sExp = sExp.replace(/(^|[^A-Z0-9$%.])([0-9]+)B/, "$1$2^_").replace(/\s+/g, ' ');
}
var asValues = sExp.split(regExp);
value = this.parseArray(asValues, 0, asValues.length, this.nBase, aUndefined);
if (value !== undefined && fPrint) {
this.printValue(null, value);
}
}
return value;
}
/**
* parseReference(s)
*
* Returns the given string with any "{expression}" sequences replaced with the value of the expression,
* and any "[address]" references replaced with the contents of the address. Expressions are parsed BEFORE
* addresses.
*
* @this {Debugger}
* @param {string} s
* @return {string|undefined}
*/
parseReference(s)
{
var a;
var chOpen = this.achGroup[0];
var chClose = this.achGroup[1];
var chEscape = (chOpen == '(' || chOpen == '{' || chOpen == '[')? '\\' : '';
var chInnerEscape = (chOpen == '['? '\\' : '');
var reSubExp = new RegExp(chEscape + chOpen + "([^" + chInnerEscape + chOpen + chInnerEscape + chClose + "]+)" + chEscape + chClose);
while (a = s.match(reSubExp)) {
var value = this.parseExpression(a[1]);
if (value === undefined) return undefined;
var sSearch = chOpen + a[1] + chClose;
var sReplace = value != null? this.toStrBase(value) : "undefined";
/*
* Note that by default, the String replace() method only replaces the FIRST occurrence,
* and there MIGHT be more than one occurrence of the expression we just parsed, so we could
* do this instead:
*
* s = s.split(sSearch).join(sReplace);
*
* However, that's knd of an expensive (slow) solution, and it's not strictly necessary, since
* any additional identical expressions will be picked up on a subsequent iteration through this loop.
*/
s = s.replace(sSearch, sReplace);
}
if (this.achAddress.length) {
chOpen = this.achAddress[0];
chClose = this.achAddress[1];
chEscape = (chOpen == '(' || chOpen == '{' || chOpen == '[')? '\\' : '';
chInnerEscape = (chOpen == '['? '\\' : '');
reSubExp = new RegExp(chEscape + chOpen + "([^" + chInnerEscape + chOpen + chInnerEscape + chClose + "]+)" + chEscape + chClose);
while (a = s.match(reSubExp)) {
s = this.parseAddrReference(s, a[1]);
}
}
return this.parseSysVars(s);
}
/**
* parseSysVars(s)
*
* Returns the given string with any recognized "$var" replaced with its value; eg:
*
* $ops: the number of opcodes executed since the last time it was displayed (or reset)
*
* @this {Debugger}
* @param {string} s
* @return {string}
*/
parseSysVars(s)
{
var a;
while (a = s.match(/\$([a-z]+)/i)) {
var v = null;
switch(a[1].toLowerCase()) {
case "ops":
v = this.cOpcodes - this.cOpcodesStart;
break;
}
if (v == null) break;
s = s.replace(a[0], v.toString());
}
return s;
}
/**
* parseUnary(value, nUnary)
*
* nUnary is actually a small "stack" of unary operations encoded in successive pairs of bits.
* As parseExpression() encounters each unary operator, nUnary is shifted left 2 bits, and the
* new unary operator is encoded in bits 0 and 1 (0b00 is none, 0b01 is negate, 0b10 is complement,
* and 0b11 is reserved). Here, we process the bits in reverse order (hence the stack-like nature),
* ensuring that we process the unary operators associated with this value right-to-left.
*
* Since bitwise operators see only 32 bits, more than 16 unary operators cannot be supported
* using this method. We'll let parseExpression() worry about that; if it ever happens in practice,
* then we'll have to switch to a more "expensive" approach (eg, an actual array of unary operators).
*
* @this {Debugger}
* @param {number} value
* @param {number} nUnary
* @return {number}
*/
parseUnary(value, nUnary)
{
while (nUnary) {
switch(nUnary & 0o3) {
case 1:
value = -this.truncate(value);
break;
case 2:
value = this.evalXOR(value, -1); // this is easier than adding an evalNOT()...
break;
case 3:
var bit = 35; // simple left-to-right zero-bit-counting loop...
while (bit >= 0 && !this.evalAND(value, Math.pow(2, bit))) bit--;
value = 35 - bit;
break;
}
nUnary >>>= 2;
}
return value;
}
/**
* parseValue(sValue, sName, fQuiet, nUnary)
*
* @this {Debugger}
* @param {string|undefined} sValue
* @param {string|null} [sName] is the name of the value, if any
* @param {Array|undefined|boolean} [fQuiet]
* @param {number} [nUnary] (0 for none, 1 for negate, 2 for complement, 3 for leading zeros)
* @return {number|undefined} numeric value, or undefined if sValue is either undefined or invalid
*/
parseValue(sValue, sName, fQuiet, nUnary = 0)
{
var value;
var aUndefined = Array.isArray(fQuiet)? fQuiet : undefined;
if (sValue != null) {
var iReg = this.getRegIndex(sValue);
if (iReg >= 0) {
value = this.getRegValue(iReg);
} else {
value = this.getVariable(sValue);
if (value != null) {
var sUndefined = this.getVariableFixup(sValue);
if (sUndefined) {
if (aUndefined) {
aUndefined.push(sUndefined);
} else {
var valueUndefined = this.parseExpression(sUndefined, fQuiet);
if (valueUndefined !== undefined) {
value += valueUndefined;
} else {
if (!fQuiet) {
this.println("undefined " + (sName || "value") + ": " + sValue + " (" + sUndefined + ")");
}
value = undefined;
}
}
}
} else {
/*
* A feature of MACRO-10 is that any single-digit number is automatically interpreted as base-10.
*/
value = Str.parseInt(sValue, sValue.length > 1 || this.nBase > 10? this.nBase : 10);
}
}
if (value != null) {
value = this.truncate(this.parseUnary(value, nUnary));
} else {
if (!fQuiet) {
this.println("invalid " + (sName || "value") + ": " + sValue);
}
}
} else {
if (!fQuiet) {
this.println("missing " + (sName || "value"));
}
}
return value;
}
/**
* printValue(sVar, value)
*
* @this {Debugger}
* @param {string|null} sVar
* @param {number|undefined} value
* @return {boolean} true if value defined, false if not
*/
printValue(sVar, value)
{
var sValue;
var fDefined = false;
if (value !== undefined) {
fDefined = true;
if (this.nBase == 8) {
sValue = this.toStrBase(value, this.nBits, 8, 1) + " " + value + '.';
} else {
sValue = this.toStrBase(value, this.nBits, 16, 1) + " " + this.toStrBase(value, this.nBits, 8, 1) + " " + this.toStrBase(value, this.nBits, 2, this.nBits <= 32? 8 : 6) + " " + value + '.';
}
if (value >= 0x20 && value < 0x7F) {
sValue += " '" + String.fromCharCode(value) + "'";
}
}
sVar = (sVar != null? (sVar + ": ") : "");
this.println(sVar + sValue);
return fDefined;
}
/**
* resetVariables()
*
* @this {Debugger}
* @return {Object}
*/
resetVariables()
{
var a = this.aVariables;
this.aVariables = {};
return a;
}
/**
* restoreVariables(a)
*
* @this {Debugger}
* @param {Object} a (from previous resetVariables() call)
*/
restoreVariables(a)
{
this.aVariables = a;
}
/**
* printVariable(sVar)
*
* @this {Debugger}
* @param {string} [sVar]
* @return {boolean} true if all value(s) defined, false if not
*/
printVariable(sVar)
{
var cVariables = 0;
if (this.aVariables) {
if (sVar) {
return this.printValue(sVar, this.aVariables[sVar] && this.aVariables[sVar].value);
}
var aVars = Object.keys(this.aVariables);
aVars.sort();
for (var i = 0; i < aVars.length; i++) {
this.printValue(aVars[i], this.aVariables[aVars[i]].value);
cVariables++;
}
}
return cVariables > 0;
}
/**
* delVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
*/
delVariable(sVar)
{
delete this.aVariables[sVar];
}
/**
* getVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {number|undefined}
*/
getVariable(sVar)
{
if (this.aVariables[sVar]) {
return this.aVariables[sVar].value;
}
sVar = sVar.substr(0, 6);
return this.aVariables[sVar] && this.aVariables[sVar].value;
}
/**
* getVariableFixup(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {string|undefined}
*/
getVariableFixup(sVar)
{
return this.aVariables[sVar] && this.aVariables[sVar].sUndefined;
}
/**
* isVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {boolean}
*/
isVariable(sVar)
{
return this.aVariables[sVar] !== undefined;
}
/**
* setVariable(sVar, value, sUndefined)
*
* @this {Debugger}
* @param {string} sVar
* @param {number} value
* @param {string|undefined} [sUndefined]
*/
setVariable(sVar, value, sUndefined)
{
this.aVariables[sVar] = {value, sUndefined};
}
/**
* toStrBase(n, nBits, nBase, nGrouping)
*
* Use this instead of Str's toOct()/toDec()/toHex() to convert numbers to the Debugger's default base.
*
* @this {Debugger}
* @param {number|null|undefined} n
* @param {number} [nBits] (-1 to strip leading zeros, 0 to allow a variable number of digits)
* @param {number} [nBase]
* @param {number} [nGrouping] (if nBase is 2, this is a grouping; otherwise, it's a prefix condition)
* @return {string}
*/
toStrBase(n, nBits = 0, nBase = 0, nGrouping = 0)
{
var s;
switch(nBase || this.nBase) {
case 2:
s = Str.toBin(n, nBits > 0? nBits : 0, nGrouping);
break;
case 8:
s = Str.toOct(n, nBits > 0? ((nBits + 2)/3)|0 : 0, !!nGrouping);
break;
case 10:
/*
* The multiplier is actually Math.log(2)/Math.log(10), but an approximation is more than adequate.
*/
s = Str.toDec(n, nBits > 0? Math.ceil(nBits * 0.3) : 0);
break;
case 16:
default:
s = Str.toHex(n, nBits > 0? ((nBits + 3) >> 2) : 0, !!nGrouping);
break;
}
return (nBits < 0? Str.stripLeadingZeros(s) : s);
}
}
if (DEBUGGER) {
/*
* These are our operator precedence tables. Operators toward the bottom (with higher values) have
* higher precedence. aBinOpPrecedence was our original table; we had to add aDECOpPrecedence because
* the precedence of operators in DEC's MACRO-10 expressions differ. Having separate tables also allows
* us to remove operators that shouldn't be supported, but unless some operator creates a problem,
* I prefer to keep as much commonality between the tables as possible.
*
* Missing from these tables are the (limited) set of unary operators we support (negate and complement),
* since this is only a BINARY operator precedence, not a general-purpose precedence table. Assume that
* all unary operators take precedence over all binary operators.
*/
Debugger.aBinOpPrecedence = {
'||': 5, // logical OR
'&&': 6, // logical AND
'!': 7, // bitwise OR (conflicts with logical NOT, but we never supported that)
'|': 7, // bitwise OR
'^!': 8, // bitwise XOR (added by MACRO-10 sometime between the 1972 and 1978 versions)
'&': 9, // bitwise AND
'!=': 10, // inequality
'==': 10, // equality
'>=': 11, // greater than or equal to
'>': 11, // greater than
'<=': 11, // less than or equal to
'<': 11, // less than
'>>>': 12, // unsigned bitwise right shift
'>>': 12, // bitwise right shift
'<<': 12, // bitwise left shift
'-': 13, // subtraction
'+': 13, // addition
'^/': 14, // remainder
'/': 14, // division
'*': 14, // multiplication
'_': 19, // MACRO-10 shift operator
'^_': 19, // MACRO-10 internal shift operator (converted from 'B' suffix form that MACRO-10 uses)
'{': 20, // open grouped expression (converted from achGroup[0])
'}': 20 // close grouped expression (converted from achGroup[1])
};
Debugger.aDECOpPrecedence = {
',,': 1, // high-word,,low-word
'||': 5, // logical OR
'&&': 6, // logical AND
'!=': 10, // inequality
'==': 10, // equality
'>=': 11, // greater than or equal to
'>': 11, // greater than
'<=': 11, // less than or equal to
'<': 11, // less than
'>>>': 12, // unsigned bitwise right shift
'>>': 12, // bitwise right shift
'<<': 12, // bitwise left shift
'-': 13, // subtraction
'+': 13, // addition
'^/': 14, // remainder
'/': 14, // division
'*': 14, // multiplication
'!': 15, // bitwise OR (conflicts with logical NOT, but we never supported that)
'|': 15, // bitwise OR
'^!': 15, // bitwise XOR (added by MACRO-10 sometime between the 1972 and 1978 versions)
'&': 15, // bitwise AND
'_': 19, // MACRO-10 shift operator
'^_': 19, // MACRO-10 internal shift operator (converted from 'B' suffix form that MACRO-10 uses)
'{': 20, // open grouped expression (converted from achGroup[0])
'}': 20 // close grouped expression (converted from achGroup[1])
};
/*
* Assorted constants
*/
Debugger.TWO_POW32 = Math.pow(2, 32);
} // endif DEBUGGER
/**
* @copyright http://pcjs.org/modules/pdp11/lib/debugger.js (C) Jeff Parsons 2012-2017
*/
/**
* DebuggerPDP11 Address Object
*
* addr address
* fPhysical true if this is a physical address
* fTemporary true if this is a temporary breakpoint address
* nBase set if the address contained an explicit base (eg, 16, 10, 8, etc)
* sCmd set for breakpoint addresses if there's an associated command string
* aCmds preprocessed commands (from sCmd)
*
* @typedef {{
* addr:(number|null),
* fPhysical:(boolean),
* fTemporary:(boolean),
* nBase:(number|undefined),
* sCmd:(string|undefined),
* aCmds:(Array.<string>|undefined)
* }}
*/
var DbgAddrPDP11;
class DebuggerPDP11 extends Debugger {
/**
* DebuggerPDP11(parmsDbg)
*
* The DebuggerPDP11 component supports the following optional (parmsDbg) properties:
*
* commands: string containing zero or more commands, separated by ';'
*
* messages: string containing zero or more message categories to enable;
* multiple categories must be separated by '|' or ';'. Parsed by messageInit().
*
* The DebuggerPDP11 component is an optional component that implements a variety of user
* commands for controlling the CPU, dumping and editing memory, etc.
*
* @param {Object} parmsDbg
*/
constructor(parmsDbg)
{
if (DEBUGGER) {
super(parmsDbg);
/*
* Since this Debugger doesn't use replaceRegs(), we can use parentheses instead of braces.
*/
this.fInit = false;
this.achGroup = ['(',')'];
this.achAddress = [];
/*
* Most commands that require an address call parseAddr(), which defaults to dbgAddrNextCode
* or dbgAddrNextData when no address has been given. doDump() and doUnassemble(), in turn,
* update dbgAddrNextData and dbgAddrNextCode, respectively, when they're done.
*
* For TEMPORARY breakpoint addresses, we set fTemporary to true, so that they can be automatically
* cleared when they're hit.
*/
this.dbgAddrNextCode = this.newAddr();
this.dbgAddrNextData = this.newAddr();
this.dbgAddrAssemble = this.newAddr();
/*
* aSymbolTable is an array of SymbolTable objects, one per ROM or other chunk of address space,
* where each object contains the following properties:
*
* sModule
* addr (physical address, if any; eg, symbols for a ROM)
* len
* aSymbols
* aOffsets
*
* See addSymbols() for more details, since that's how callers add sets of symbols to the table.
*/
this.aSymbolTable = [];
/*
* clearBreakpoints() initializes the breakpoints lists: aBreakExec is a list of addresses
* to halt on whenever attempting to execute an instruction at the corresponding address,
* and aBreakRead and aBreakWrite are lists of addresses to halt on whenever a read or write,
* respectively, occurs at the corresponding address.
*
* NOTE: Curiously, after upgrading the Google Closure Compiler from v20141215 to v20150609,
* the resulting compiled code would crash in clearBreakpoints(), because the (renamed) aBreakRead
* property was already defined. To eliminate whatever was confusing the Closure Compiler, I've
* explicitly initialized all the properties that clearBreakpoints() (re)initializes.
*/
this.aBreakExec = this.aBreakRead = this.aBreakWrite = [];
this.clearBreakpoints();
/*
* The new "bn" command allows you to specify a number of instructions to execute and then stop;
* "bn 0" disables any outstanding count.
*/
this.nBreakInstructions = 0;
/*
* Execution history is allocated by historyInit() whenever checksEnabled() conditions change.
* Execution history is updated whenever the CPU calls checkInstruction(), which will happen
* only when checksEnabled() returns true (eg, whenever one or more breakpoints have been set).
* This ensures that, by default, the CPU runs as fast as possible.
*/
this.iInstructionHistory = 0;
this.aInstructionHistory = [];
this.nextHistory = undefined;
this.historyInit();
/*
* Initialize DebuggerPDP11 message support.
*/
this.dbg = this;
this.afnDumpers = {};
this.bitsMessage = this.bitsWarning = 0;
this.sMessagePrev = null;
this.aMessageLog = [];
this.messageInit(parmsDbg['messages']);
this.sInitCommands = parmsDbg['commands'];
/*
* Define remaining miscellaneous DebuggerPDP11 properties.
*/
this.opTable = DebuggerPDP11.OPTABLE;
this.aOpReserved = [];
this.nStep = 0;
this.sCmdTracePrev = null;
this.sCmdDumpPrev = null;
this.fIgnoreNextCheckFault = false; // TODO: Does this serve any purpose on a PDP-11?
this.nSuppressBreaks = 0;
this.cInstructions = this.cInstructionsStart = 0;
this.nCycles = this.nCyclesStart = this.msStart = 0;
this.controlDebug = null;
this.panel = null;
/*
* Make it easier to access DebuggerPDP11 commands from an external REPL (eg, the WebStorm
* "live" console window); eg:
*
* pdp11('r')
* pdp11('dw 0:0')
* pdp11('h')
* ...
*/
var dbg = this;
if (window) {
if (window[PDP11.APPCLASS] === undefined) {
window[PDP11.APPCLASS] = function(s) { return dbg.doCommands(s); };
}
} else {
if (global[PDP11.APPCLASS] === undefined) {
global[PDP11.APPCLASS] = function(s) { return dbg.doCommands(s); };
}
}
} // endif DEBUGGER
}
/**
* getAddr(dbgAddr, fWrite, nb)
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11|null} [dbgAddr]
* @param {boolean} [fWrite]
* @param {number} [nb] number of bytes to check (1 or 2); default is 1
* @return {number} is the corresponding linear address, or PDP11.ADDR_INVALID
*/
getAddr(dbgAddr, fWrite, nb)
{
var addr = dbgAddr && dbgAddr.addr;
if (addr == null) addr = PDP11.ADDR_INVALID;
return addr;
}
/**
* newAddr(addr, fPhysical, nBase)
*
* Returns a NEW DbgAddrPDP11 object, initialized with specified values and/or defaults.
*
* @this {DebuggerPDP11}
* @param {number|null} [addr]
* @param {boolean} [fPhysical]
* @param {number} [nBase]
* @return {DbgAddrPDP11}
*/
newAddr(addr = null, fPhysical = false, nBase)
{
return {addr: addr, fPhysical: fPhysical, fTemporary: false, nBase: nBase};
}
/**
* setAddr(dbgAddr, addr)
*
* Updates an EXISTING DbgAddrPDP11 object, initialized with specified values and/or defaults.
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {number} addr
* @return {DbgAddrPDP11}
*/
setAddr(dbgAddr, addr)
{
dbgAddr.addr = addr;
dbgAddr.fTemporary = false;
dbgAddr.nBase = undefined;
return dbgAddr;
}
/**
* packAddr(dbgAddr)
*
* Packs a DbgAddrPDP11 object into an Array suitable for saving in a machine state object.
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @return {Array}
*/
packAddr(dbgAddr)
{
return [dbgAddr.addr, dbgAddr.fPhysical, dbgAddr.nBase, dbgAddr.fTemporary, dbgAddr.sCmd];
}
/**
* unpackAddr(aAddr)
*
* Unpacks a DbgAddrPDP11 object from an Array created by packAddr() and restored from a saved machine state.
*
* @this {DebuggerPDP11}
* @param {Array} aAddr
* @return {DbgAddrPDP11}
*/
unpackAddr(aAddr)
{
var dbgAddr = this.newAddr(aAddr[0], aAddr[1], aAddr[2]);
dbgAddr.fTemporary = aAddr[3];
if (aAddr[4]) {
dbgAddr.aCmds = this.parseCommand(dbgAddr.sCmd = aAddr[4]);
}
return dbgAddr;
}
/**
* initBus(bus, cpu, dbg)
*
* @this {DebuggerPDP11}
* @param {ComputerPDP11} cmp
* @param {BusPDP11} bus
* @param {CPUStatePDP11} cpu
* @param {DebuggerPDP11} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cmp = cmp;
this.cpu = cpu;
this.panel = cmp.panel;
/*
* Re-initialize Debugger message support if necessary
*/
var sMessages = /** @type {string|undefined} */ (cmp.getMachineParm('messages'));
if (sMessages) this.messageInit(sMessages);
if (this.cpu.model < PDP11.MODEL_1140) {
this.aOpReserved = this.aOpReserved.concat(DebuggerPDP11.OP1140);
}
if (this.cpu.model < PDP11.MODEL_1145) {
this.aOpReserved = this.aOpReserved.concat(DebuggerPDP11.OP1145);
}
this.messageDump(MessagesPDP11.BUS, function onDumpBus(asArgs) { dbg.dumpBus(asArgs); });
this.setReady();
}
/**
* setBinding(sType, sBinding, control, sValue)
*
* @this {DebuggerPDP11}
* @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc)
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "debugInput")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sType, sBinding, control, sValue)
{
var dbg = this;
switch (sBinding) {
case "debugInput":
this.bindings[sBinding] = control;
this.controlDebug = control;
/*
* For halted machines, this is fine, but for auto-start machines, it can be annoying.
*
* control.focus();
*/
control.onkeydown = function onKeyDownDebugInput(event) {
var sCmd;
if (event.keyCode == Keys.KEYCODE.CR) {
sCmd = control.value;
control.value = "";
dbg.doCommands(sCmd, true);
}
else if (event.keyCode == Keys.KEYCODE.ESC) {
control.value = sCmd = "";
}
else {
if (event.keyCode == Keys.KEYCODE.UP) {
sCmd = dbg.getPrevCommand();
}
else if (event.keyCode == Keys.KEYCODE.DOWN) {
sCmd = dbg.getNextCommand();
}
if (sCmd != null) {
var cch = sCmd.length;
control.value = sCmd;
control.setSelectionRange(cch, cch);
}
}
if (sCmd != null && event.preventDefault) event.preventDefault();
};
return true;
case "debugEnter":
this.bindings[sBinding] = control;
Web.onClickRepeat(
control,
500, 100,
function onClickDebugEnter(fRepeat) {
if (dbg.controlDebug) {
var sCmd = dbg.controlDebug.value;
dbg.controlDebug.value = "";
dbg.doCommands(sCmd, true);
return true;
}
if (DEBUG) dbg.log("no debugger input buffer");
return false;
}
);
return true;
case "step":
this.bindings[sBinding] = control;
Web.onClickRepeat(
control,
500, 100,
function onClickStep(fRepeat) {
var fCompleted = false;
if (!dbg.isBusy(true)) {
dbg.setBusy(true);
fCompleted = dbg.stepCPU(fRepeat? 1 : 0, null);
dbg.setBusy(false);
}
return fCompleted;
}
);
return true;
default:
break;
}
return false;
}
/**
* setFocus(fScroll)
*
* @this {DebuggerPDP11}
* @param {boolean} [fScroll] (true if you really want the control scrolled into view)
*/
setFocus(fScroll)
{
if (this.controlDebug) {
/*
* This is the recommended work-around to prevent the browser from scrolling the focused element
* into view. The CPU is not a visual component, so when the CPU wants to set focus, the primary intent
* is to ensure that keyboard input is fielded properly.
*/
var x = 0, y = 0;
if (!fScroll && window) {
x = window.scrollX;
y = window.scrollY;
}
this.controlDebug.focus();
if (!fScroll && window) {
window.scrollTo(x, y);
}
}
}
/**
* mapUnibus(addr)
*
* @this {DebuggerPDP11}
* @param {number} addr
* @return {number}
*/
mapUnibus(addr)
{
return this.cpu.mapUnibus(addr);
}
/**
* getByte(dbgAddr, inc)
*
* We must route all our memory requests through the CPU now, in case paging is enabled.
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {number} [inc]
* @return {number}
*/
getByte(dbgAddr, inc)
{
var b = 0xff;
var addr = this.getAddr(dbgAddr, false, 1);
if (addr !== PDP11.ADDR_INVALID) {
b = (dbgAddr.fPhysical || addr > 0xffff)? this.bus.getByteDirect(this.mapUnibus(addr)) : this.cpu.getByteSafe(addr);
if (inc) this.incAddr(dbgAddr, inc);
}
return b;
}
/**
* getWord(dbgAddr, inc)
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {number} [inc]
* @return {number}
*/
getWord(dbgAddr, inc)
{
var w = 0xffff;
var addr = this.getAddr(dbgAddr, false, 2);
if (addr !== PDP11.ADDR_INVALID) {
w = (dbgAddr.fPhysical || addr > 0xffff)? this.bus.getWordDirect(this.mapUnibus(addr)) : this.cpu.getWordSafe(addr);
if (inc) this.incAddr(dbgAddr, inc);
}
return w;
}
/**
* setByte(dbgAddr, b, inc)
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {number} b
* @param {number} [inc]
*/
setByte(dbgAddr, b, inc)
{
var addr = this.getAddr(dbgAddr, true, 1);
if (addr !== PDP11.ADDR_INVALID) {
if (dbgAddr.fPhysical || addr > 0xffff) {
this.bus.setByteDirect(this.mapUnibus(addr), b);
} else {
this.cpu.setByteSafe(addr, b);
}
if (inc) this.incAddr(dbgAddr, inc);
this.cmp.updateDisplays(-1);
}
}
/**
* setWord(dbgAddr, w, inc)
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {number} w
* @param {number} [inc]
*/
setWord(dbgAddr, w, inc)
{
var addr = this.getAddr(dbgAddr, true, 2);
if (addr !== PDP11.ADDR_INVALID) {
if (dbgAddr.fPhysical || addr > 0xffff) {
this.bus.setWordDirect(this.mapUnibus(addr), w);
} else {
this.cpu.setWordSafe(addr, w);
}
if (inc) this.incAddr(dbgAddr, inc);
this.cmp.updateDisplays(-1);
}
}
/**
* parseAddr(sAddr, fCode, fNoChecks)
*
* Address evaluation and validation (eg, range checks) are no longer performed at this stage. That's
* done later, by getAddr(), which returns PDP11.ADDR_INVALID for invalid segments, out-of-range offsets,
* etc. The Debugger's low-level get/set memory functions verify all getAddr() results, but even if an
* invalid address is passed through to the Bus memory interfaces, the address will simply be masked with
* bus.nBusMask; in the case of PDP11.ADDR_INVALID, that will generally refer to the top of the physical
* address space.
*
* @this {DebuggerPDP11}
* @param {string|undefined} sAddr
* @param {boolean} [fCode] (true if target is code, false if target is data)
* @param {boolean} [fNoChecks] (true when setting breakpoints that may not be valid now, but will be later)
* @return {DbgAddrPDP11|null|undefined}
*/
parseAddr(sAddr, fCode, fNoChecks)
{
var dbgAddr;
var dbgAddrNext = (fCode? this.dbgAddrNextCode : this.dbgAddrNextData);
var addr = dbgAddrNext.addr;
var fPhysical, nBase;
if (sAddr !== undefined) {
sAddr = this.parseReference(sAddr);
var ch = sAddr.charAt(0);
if (ch == '%') {
fPhysical = true;
sAddr = sAddr.substr(1);
}
dbgAddr = this.findSymbolAddr(sAddr);
if (dbgAddr) return dbgAddr;
if (sAddr.indexOf("0x") >= 0) {
nBase = 16
} else if (sAddr.indexOf("0o") >= 0) {
nBase = 8;
} else if (sAddr.indexOf('.') >= 0) {
nBase = 10;
}
addr = this.parseExpression(sAddr);
}
if (addr != null) {
dbgAddr = this.newAddr(addr, fPhysical, nBase);
}
return dbgAddr;
}
/**
* parseAddrOptions(dbdAddr, sOptions)
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {string} [sOptions]
*/
parseAddrOptions(dbgAddr, sOptions)
{
if (sOptions) {
var a = sOptions.match(/(['"])(.*?)\1/);
if (a) {
dbgAddr.aCmds = this.parseCommand(dbgAddr.sCmd = a[2]);
}
}
}
/**
* incAddr(dbgAddr, inc)
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {number} [inc] contains value to increment dbgAddr by (default is 1)
*/
incAddr(dbgAddr, inc)
{
if (dbgAddr.addr != null) {
dbgAddr.addr += (inc || 1);
}
}
/**
* toStrOffset(off)
*
* @this {DebuggerPDP11}
* @param {number|null|undefined} [off]
* @return {string} the hex representation of off
*/
toStrOffset(off)
{
return this.toStrBase(off);
}
/**
* toStrAddr(dbgAddr)
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @return {string} the hex representation of the address
*/
toStrAddr(dbgAddr)
{
return (dbgAddr.fPhysical? '%' : '') + this.toStrOffset(dbgAddr.addr);
}
/**
* getSZ(dbgAddr, cchMax)
*
* Gets zero-terminated (aka "ASCIIZ") string from dbgAddr. It also stops at the first '$', in case this is
* a '$'-terminated string -- mainly because I'm lazy and didn't feel like writing a separate get() function.
* Yes, a zero-terminated string containing a '$' will be prematurely terminated, and no, I don't care.
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {number} [cchMax] (default is 256)
* @return {string} (and dbgAddr advanced past the terminating zero)
*/
getSZ(dbgAddr, cchMax)
{
var s = "";
cchMax = cchMax || 256;
while (s.length < cchMax) {
var b = this.getByte(dbgAddr, 1);
if (!b || b == 0x24 || b >= 127) break;
s += (b >= 32? String.fromCharCode(b) : '.');
}
return s;
}
/**
* dumpBlocks(aBlocks, sAddr)
*
* @this {DebuggerPDP11}
* @param {Array} aBlocks
* @param {string} [sAddr] (optional block address)
*/
dumpBlocks(aBlocks, sAddr)
{
var addr = 0, i = 0, n = aBlocks.length;
if (sAddr) {
addr = this.getAddr(this.parseAddr(sAddr));
if (addr === PDP11.ADDR_INVALID) {
this.println("invalid address: " + sAddr);
return;
}
i = addr >>> this.bus.nBlockShift;
n = 1;
}
this.println("blockid physical blockaddr used size type");
this.println("-------- --------- --------- ------ ------ ----");
var typePrev = -1, cPrev = 0;
while (n--) {
var block = aBlocks[i];
if (block.type == typePrev) {
if (!cPrev++) this.println("...");
} else {
typePrev = block.type;
var sType = MemoryPDP11.TYPE_NAMES[typePrev];
if (block) {
this.println(Str.toHex(block.id, 8) + " %" + Str.toHex(i << this.bus.nBlockShift, 8) + " %" + Str.toHex(block.addr, 8) + " " + Str.toHexWord(block.used) + " " + Str.toHexWord(block.size) + " " + sType);
}
if (typePrev != MemoryPDP11.TYPE.NONE) typePrev = -1;
cPrev = 0;
}
addr += this.bus.nBlockSize;
i++;
}
}
/**
* dumpBus(asArgs)
*
* Dumps Bus allocations.
*
* @this {DebuggerPDP11}
* @param {Array.<string>} asArgs (asArgs[0] is an optional block address)
*/
dumpBus(asArgs)
{
this.dumpBlocks(this.bus.aBusBlocks, asArgs[0]);
}
/**
* dumpHistory(sPrev, sLines)
*
* If sLines is not a number, it can be a instruction filter. However, for the moment, the only
* supported filter is "call", which filters the history buffer for all CALL and RET instructions
* from the specified previous point forward.
*
* @this {DebuggerPDP11}
* @param {string} [sPrev] is a (decimal) number of instructions to rewind to (default is 10)
* @param {string} [sLines] is a (decimal) number of instructions to print (default is, again, 10)
*/
dumpHistory(sPrev, sLines)
{
var sMore = "";
var cHistory = 0;
var iHistory = this.iInstructionHistory;
var aHistory = this.aInstructionHistory;
if (aHistory.length) {
var nPrev = +sPrev || this.nextHistory;
var nLines = +sLines || 10;
if (isNaN(nPrev)) {
nPrev = nLines;
} else {
sMore = "more ";
}
if (nPrev > aHistory.length) {
this.println("note: only " + aHistory.length + " available");
nPrev = aHistory.length;
}
iHistory -= nPrev;
if (iHistory < 0) {
/*
* If the dbgAddr of the last aHistory element contains a valid selector, wrap around.
*/
if (aHistory[aHistory.length - 1].addr == null) {
nPrev = iHistory + nPrev;
iHistory = 0;
} else {
iHistory += aHistory.length;
}
}
var aFilters = [];
if (sLines == "call") {
nLines = 100000;
aFilters = ["CALL"];
}
if (sPrev !== undefined) {
this.println(nPrev + " instructions earlier:");
}
/*
* TODO: The following is necessary to prevent dumpHistory() from causing additional (or worse, recursive)
* faults due to segmented addresses that are no longer valid, but the only alternative is to dramatically
* increase the amount of memory used to store instruction history (eg, storing copies of all the instruction
* bytes alongside the execution addresses).
*
* For now, we're living dangerously, so that our history dumps actually work.
*
* this.nSuppressBreaks++;
*
* If you re-enable this protection, be sure to re-enable the decrement below, too.
*/
while (nLines > 0 && iHistory != this.iInstructionHistory) {
var dbgAddr = aHistory[iHistory++];
if (dbgAddr.addr == null) break;
/*
* We must create a new dbgAddr from the address in aHistory, because dbgAddr was
* a reference, not a copy, and we don't want getInstruction() modifying the original.
*/
var dbgAddrNew = this.newAddr(dbgAddr.addr);
var sComment = "history";
var nSequence = nPrev--;
/*
* TODO: Need to some UI to control whether cycle counts are displayed as part of the history.
* It's currently disabled in checkInstruction(), so it's disable here, too.
*
if (DEBUG && dbgAddr.cycleCount != null) {
sComment = "cycles";
nSequence = dbgAddr.cycleCount;
}
*/
var sInstruction = this.getInstruction(dbgAddrNew, sComment, nSequence);
if (!aFilters.length || sInstruction.indexOf(aFilters[0]) >= 0) {
this.println(sInstruction);
}
/*
* If there were OPERAND or ADDRESS overrides on the previous instruction, getInstruction()
* will have automatically disassembled additional bytes, so skip additional history entries.
*/
if (dbgAddrNew.cOverrides) {
iHistory += dbgAddrNew.cOverrides; nLines -= dbgAddrNew.cOverrides; nPrev -= dbgAddrNew.cOverrides;
}
if (iHistory >= aHistory.length) iHistory = 0;
this.nextHistory = nPrev;
cHistory++;
nLines--;
}
/*
* See comments above.
*
* this.nSuppressBreaks--;
*/
}
if (!cHistory) {
this.println("no " + sMore + "history available");
this.nextHistory = undefined;
}
}
/**
* messageInit(sEnable)
*
* @this {DebuggerPDP11}
* @param {string|undefined} sEnable contains zero or more message categories to enable, separated by '|'
*/
messageInit(sEnable)
{
this.dbg = this;
this.bitsMessage = this.bitsWarning = MessagesPDP11.WARN;
this.sMessagePrev = null;
this.aMessageLog = [];
/*
* Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects,
* but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard".
*/
var aEnable = this.parseCommand(sEnable.replace("keys","key").replace("kbd","keyboard"), false, '|');
if (aEnable.length) {
for (var m in MessagesPDP11.CATEGORIES) {
if (Usr.indexOf(aEnable, m) >= 0) {
this.bitsMessage |= MessagesPDP11.CATEGORIES[m];
this.println(m + " messages enabled");
}
}
}
}
/**
* messageDump(bitMessage, fnDumper)
*
* @this {DebuggerPDP11}
* @param {number} bitMessage is one Messages category flag
* @param {function(Array.<string>)} fnDumper is a function the Debugger can use to dump data for that category
* @return {boolean} true if successfully registered, false if not
*/
messageDump(bitMessage, fnDumper)
{
for (var m in MessagesPDP11.CATEGORIES) {
if (bitMessage == MessagesPDP11.CATEGORIES[m]) {
this.afnDumpers[m] = fnDumper;
return true;
}
}
return false;
}
/**
* getRegIndex(sReg, off)
*
* @this {DebuggerPDP11}
* @param {string} sReg
* @param {number} [off] optional offset into sReg
* @return {number} register index, or -1 if not found
*/
getRegIndex(sReg, off)
{
sReg = sReg.toUpperCase();
var iReg = DebuggerPDP11.REGS[sReg];
if (iReg == null) {
iReg = -1;
if (sReg.charAt(0) == "R") {
iReg = +sReg.charAt(1);
if (iReg < 0 || iReg > 7) iReg = -1;
}
}
return iReg;
}
/**
* getRegName(iReg)
*
* @this {DebuggerPDP11}
* @param {number} iReg (0-7; not used for other registers)
* @return {string}
*/
getRegName(iReg)
{
var sReg;
if (iReg < DebuggerPDP11.REG_AR || this.panel) sReg = DebuggerPDP11.REGNAMES[iReg];
return sReg || "";
}
/**
* getRegValue(iReg)
*
* Register numbers 0-7 are reserved for cpu.regsGen, 8-15 are reserved for cpu.regsAlt,
* 16-19 for cpu.regsAltStack, 20 for regPSW, etc.
*
* @this {DebuggerPDP11}
* @param {number} iReg
* @return {number|undefined}
*/
getRegValue(iReg)
{
var value;
if (iReg >= 0) {
if (iReg < 8) {
value = this.cpu.regsGen[iReg];
}
else if (iReg < 16) {
value = this.cpu.regsAlt[iReg-8];
}
else if (iReg < 20) {
value = this.cpu.regsAltStack[iReg-16];
}
else {
var cpu = this.cpu;
var panel = this.panel;
switch(iReg) {
case DebuggerPDP11.REG_PS:
value = this.cpu.getPSW();
break;
case DebuggerPDP11.REG_PI:
value = cpu.getPIR();
break;
case DebuggerPDP11.REG_ER:
value = cpu.regErr;
break;
case DebuggerPDP11.REG_SL:
value = cpu.getSLR();
break;
case DebuggerPDP11.REG_M0:
value = cpu.getMMR0();
break;
case DebuggerPDP11.REG_M1:
value = cpu.getMMR1();
break;
case DebuggerPDP11.REG_M2:
value = cpu.getMMR2();
break;
case DebuggerPDP11.REG_M3:
value = cpu.getMMR3();
break;
case DebuggerPDP11.REG_AR:
if (panel) value = panel.getAR();
break;
case DebuggerPDP11.REG_DR:
if (panel) value = panel.getDR();
break;
case DebuggerPDP11.REG_SR:
if (panel) value = panel.getSR();
break;
}
}
}
return value;
}
/**
* replaceRegs(s)
*
* TODO: Implement or eliminate.
*
* @this {DebuggerPDP11}
* @param {string} s
* @return {string}
*/
replaceRegs(s)
{
return s;
}
/**
* message(sMessage, fAddress)
*
* @this {DebuggerPDP11}
* @param {string} sMessage is any caller-defined message string
* @param {boolean} [fAddress] is true to display the current address
*/
message(sMessage, fAddress)
{
if (fAddress) {
sMessage += " @" + this.toStrAddr(this.newAddr(this.cpu.getLastPC()));
}
if (this.sMessagePrev && sMessage == this.sMessagePrev) return;
this.sMessagePrev = sMessage;
if (this.bitsMessage & MessagesPDP11.LOG) {
this.aMessageLog.push(sMessage);
return;
}
var fRunning;
if ((this.bitsMessage & MessagesPDP11.HALT) && this.cpu && (fRunning = this.cpu.isRunning()) || this.isBusy(true)) {
this.stopCPU();
if (fRunning) sMessage += " (cpu halted)";
}
this.println(sMessage); // + " (" + this.cpu.getCycles() + " cycles)"
/*
* We have no idea what the frequency of println() calls might be; all we know is that they easily
* screw up the CPU's careful assumptions about cycles per burst. So we call yieldCPU() after every
* message, to effectively end the current burst and start fresh.
*
* TODO: See CPUPDP11.calcStartTime() for a discussion of why we might want to call yieldCPU() *before*
* we display the message.
*/
if (this.cpu) this.cpu.yieldCPU();
}
/**
* init()
*
* @this {DebuggerPDP11}
* @param {boolean} [fAutoStart]
*/
init(fAutoStart)
{
this.fInit = true;
this.println("Type ? for help with PDPjs Debugger commands");
this.updateStatus();
if (!fAutoStart) this.setFocus();
if (this.sInitCommands) {
var sCmds = this.sInitCommands;
this.sInitCommands = null;
this.doCommands(sCmds);
}
}
/**
* historyInit(fQuiet)
*
* This function is intended to be called by the constructor, reset(), addBreakpoint(), findBreakpoint()
* and any other function that changes the checksEnabled() criteria used to decide whether checkInstruction()
* should be called.
*
* That is, if the history arrays need to be allocated and haven't already been allocated, then allocate them,
* and if the arrays are no longer needed, then deallocate them.
*
* @this {DebuggerPDP11}
* @param {boolean} [fQuiet]
*/
historyInit(fQuiet)
{
var i;
if (!this.checksEnabled()) {
if (this.aInstructionHistory && this.aInstructionHistory.length && !fQuiet) {
this.println("instruction history buffer freed");
}
this.iInstructionHistory = 0;
this.aInstructionHistory = [];
return;
}
if (!this.aInstructionHistory || !this.aInstructionHistory.length) {
this.aInstructionHistory = new Array(DebuggerPDP11.HISTORY_LIMIT);
for (i = 0; i < this.aInstructionHistory.length; i++) {
/*
* Preallocate dummy Addr (Array) objects in every history slot, so that
* checkInstruction() doesn't need to call newAddr() on every slot update.
*/
this.aInstructionHistory[i] = this.newAddr();
}
this.iInstructionHistory = 0;
if (!fQuiet) {
this.println("instruction history buffer allocated");
}
}
}
/**
* startCPU(fUpdateFocus, fQuiet)
*
* @this {DebuggerPDP11}
* @param {boolean} [fUpdateFocus] is true to update focus
* @param {boolean} [fQuiet]
* @return {boolean} true if run request successful, false if not
*/
startCPU(fUpdateFocus, fQuiet)
{
if (!this.checkCPU(fQuiet)) return false;
this.cpu.startCPU(fUpdateFocus);
return true;
}
/**
* stepCPU(nCycles, fRegs, fUpdateDisplays)
*
* @this {DebuggerPDP11}
* @param {number} nCycles (0 for one instruction without checking breakpoints)
* @param {boolean|null} [fRegs] is true to display registers after step (default is false; use null for previous setting)
* @param {boolean} [fUpdateDisplays] is false to disable Computer display updates (default is true)
* @return {boolean}
*/
stepCPU(nCycles, fRegs, fUpdateDisplays)
{
if (!this.checkCPU()) return false;
var sCmd = "";
if (fRegs === null) {
fRegs = (!this.sCmdTracePrev || this.sCmdTracePrev == "tr");
sCmd = fRegs? "tr" : "t";
}
this.nCycles = 0;
if (!nCycles) {
/*
* When single-stepping, the CPU won't call checkInstruction(), which is good for
* avoiding breakpoints, but bad for instruction data collection if checks are enabled.
* So we call checkInstruction() ourselves.
*/
if (this.checksEnabled()) this.checkInstruction(this.cpu.getPC(), 0);
}
/*
* For our typically tiny bursts (usually single instructions), mimic what runCPU() does.
*/
try {
nCycles = this.cpu.getBurstCycles(nCycles);
var nCyclesStep = this.cpu.stepCPU(nCycles);
if (nCyclesStep > 0) {
this.cpu.updateTimers(nCyclesStep);
this.nCycles += nCyclesStep;
this.cpu.addCycles(nCyclesStep, true);
this.cpu.updateChecksum(nCyclesStep);
this.cInstructions++;
}
}
catch(exception) {
/*
* We assume that any numeric exception was explicitly thrown by the CPU to interrupt the
* current instruction. For all other exceptions, we attempt a stack dump.
*/
if (typeof exception != "number") {
var e = exception;
this.nCycles = 0;
this.cpu.setError(e.stack || e.message);
}
}
/*
* Because we called cpu.stepCPU() and not cpu.startCPU(), we must nudge the Computer's update code,
* and then update our own state. Normally, the only time fUpdateDisplays will be false is when doTrace()
* is calling us in a loop, in which case it will perform its own updateDisplays() when it's done.
*/
if (fUpdateDisplays !== false) {
if (this.panel) this.panel.stop();
this.cmp.updateDisplays(-1);
}
this.updateStatus(fRegs || false, sCmd);
return (this.nCycles > 0);
}
/**
* stopCPU()
*
* @this {DebuggerPDP11}
* @param {boolean} [fComplete]
*/
stopCPU(fComplete)
{
if (this.cpu) this.cpu.stopCPU(fComplete);
}
/**
* updateStatus(fRegs, sCmd)
*
* @this {DebuggerPDP11}
* @param {boolean} [fRegs] (default is true)
* @param {string} [sCmd]
*/
updateStatus(fRegs, sCmd)
{
if (!this.fInit) return;
if (fRegs === undefined) fRegs = true;
if (sCmd) {
this.println(DebuggerPDP11.PROMPT + sCmd);
}
var trapStatus = this.cpu.getTrapStatus();
if (trapStatus) {
var reason = trapStatus >> 8;
var sReason = reason < 0? PDP11.REASONS[-reason] : this.toStrBase(reason);
this.println("trapped to " + this.toStrBase(trapStatus & 0xff, 8) + " (" + sReason + ")");
}
this.dbgAddrNextCode = this.newAddr(this.cpu.getPC());
/*
* this.nStep used to be a simple boolean, but now it's 0 (or undefined)
* if inactive, 1 if stepping over an instruction without a register dump, or 2
* if stepping over an instruction with a register dump.
*/
if (!fRegs || this.nStep == 1) {
this.doUnassemble();
} else {
this.doRegisters();
}
}
/**
* checkCPU(fQuiet)
*
* Make sure the CPU is ready (finished initializing), powered, not already running, and not in an error state.
*
* @this {DebuggerPDP11}
* @param {boolean} [fQuiet]
* @return {boolean}
*/
checkCPU(fQuiet)
{
if (!this.cpu || !this.cpu.isReady() || !this.cpu.isPowered() || this.cpu.isRunning()) {
if (!fQuiet) this.println("cpu busy or unavailable, command ignored");
return false;
}
return !this.cpu.isError();
}
/**
* powerUp(data, fRepower)
*
* @this {DebuggerPDP11}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
/*
* Because Debugger save/restore support is somewhat limited (and didn't always exist),
* we deviate from the typical save/restore design pattern: instead of reset OR restore,
* we always reset and then perform a (potentially limited) restore.
*/
this.reset(true);
// this.println(data? "resuming" : "powering up");
if (data) {
return this.restore(data);
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {DebuggerPDP11}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean}
*/
powerDown(fSave, fShutdown)
{
if (fShutdown) this.println(fSave? "suspending" : "shutting down");
return fSave? this.save() : true;
}
/**
* reset(fQuiet)
*
* This is a notification handler, called by the Computer, to inform us of a reset.
*
* @this {DebuggerPDP11}
* @param {boolean} fQuiet (true only when called from our own powerUp handler)
*/
reset(fQuiet)
{
this.historyInit();
this.cInstructions = this.cInstructionsStart = 0;
this.sMessagePrev = null;
this.nCycles = 0;
this.dbgAddrNextCode = this.newAddr(this.cpu.getPC());
/*
* fRunning is set by start() and cleared by stop(). In addition, we clear
* it here, so that if the CPU is reset while running, we can prevent stop()
* from unnecessarily dumping the CPU state.
*/
this.flags.running = false;
this.clearTempBreakpoint();
if (!fQuiet) this.updateStatus();
}
/**
* save()
*
* This implements (very rudimentary) save support for the Debugger component.
*
* @this {DebuggerPDP11}
* @return {Object}
*/
save()
{
var state = new State(this);
state.set(0, this.packAddr(this.dbgAddrNextCode));
state.set(1, this.packAddr(this.dbgAddrAssemble));
state.set(2, [this.aPrevCmds, this.fAssemble, this.bitsMessage]);
state.set(3, this.aSymbolTable);
return state.data();
}
/**
* restore(data)
*
* This implements (very rudimentary) restore support for the Debugger component.
*
* @this {DebuggerPDP11}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
var i = 0;
if (data[2] !== undefined) {
this.dbgAddrNextCode = this.unpackAddr(data[i++]);
this.dbgAddrAssemble = this.unpackAddr(data[i++]);
this.aPrevCmds = data[i][0];
if (typeof this.aPrevCmds == "string") this.aPrevCmds = [this.aPrevCmds];
this.fAssemble = data[i][1];
this.bitsMessage |= data[i][2]; // keep our current message bits set, and simply "add" any extra bits defined by the saved state
}
if (data[3]) this.aSymbolTable = data[3];
return true;
}
/**
* start(ms, nCycles)
*
* This is a notification handler, called by the Computer, to inform us the CPU has started.
*
* @this {DebuggerPDP11}
* @param {number} ms
* @param {number} nCycles
*/
start(ms, nCycles)
{
if (!this.nStep) this.println("running");
this.flags.running = true;
this.msStart = ms;
this.nCyclesStart = nCycles;
}
/**
* stop(ms, nCycles)
*
* This is a notification handler, called by the Computer, to inform us the CPU has now stopped.
*
* @this {DebuggerPDP11}
* @param {number} ms
* @param {number} nCycles
*/
stop(ms, nCycles)
{
if (this.flags.running) {
this.flags.running = false;
this.nCycles = nCycles - this.nCyclesStart;
if (!this.nStep) {
var sStopped = "stopped";
if (this.nCycles) {
var msTotal = ms - this.msStart;
var nCyclesPerSecond = (msTotal > 0? Math.round(this.nCycles * 1000 / msTotal) : 0);
sStopped += " (";
if (this.checksEnabled()) {
sStopped += this.cInstructions + " instructions, ";
/*
* $ops displays progress by calculating cInstructions - cInstructionsStart, so before
* zeroing cInstructions, we should subtract cInstructions from cInstructionsStart (since
* we're effectively subtracting cInstructions from cInstructions as well).
*/
this.cInstructionsStart -= this.cInstructions;
this.cInstructions = 0;
}
sStopped += this.nCycles + " cycles, " + msTotal + " ms, " + nCyclesPerSecond + " hz)";
} else {
if (this.messageEnabled(MessagesPDP11.HALT)) {
/*
* It's possible the user is trying to 'g' past a fault that was blocked by helpCheckFault()
* for the Debugger's benefit; if so, it will continue to be blocked, so try displaying a helpful
* message (another helpful tip would be to simply turn off the "halt" message category).
*/
sStopped += " (use the 't' command to execute blocked faults)";
}
}
this.println(sStopped);
}
this.updateStatus(true);
this.setFocus();
this.clearTempBreakpoint(this.cpu.getPC());
this.sMessagePrev = null;
}
}
/**
* checksEnabled(fRelease)
*
* This "check" function is called by the CPU; we indicate whether or not every instruction needs to be checked.
*
* Originally, this returned true even when there were only read and/or write breakpoints, but those breakpoints
* no longer require the intervention of checkInstruction(); the Bus component automatically swaps in/out appropriate
* "checked" Memory access functions to deal with those breakpoints in the corresponding Memory blocks. So I've
* simplified the test below.
*
* @this {DebuggerPDP11}
* @param {boolean} [fRelease] is true for release criteria only; default is false (any criteria)
* @return {boolean} true if every instruction needs to pass through checkInstruction(), false if not
*/
checksEnabled(fRelease)
{
return ((DEBUG && !fRelease)? true : (this.aBreakExec.length > 1 || !!this.nBreakInstructions));
}
/**
* checkInstruction(addr, nState)
*
* This "check" function is called by the CPU to inform us about the next instruction to be executed,
* giving us an opportunity to look for "exec" breakpoints and update opcode instruction history.
*
* @this {DebuggerPDP11}
* @param {number} addr
* @param {number} nState is < 0 if stepping, 0 if starting, or > 0 if running
* @return {boolean} true if breakpoint hit, false if not
*/
checkInstruction(addr, nState)
{
var opCode = -1;
var cpu = this.cpu;
/*
* If opHalt() calls our stopInstruction() function, it will effectively rewind the PC back to the HALT,
* purely for our debugging benefit, so we must compensate for that here by advancing the PC past the HALT
* when the machine starts up again.
*/
if (!nState) {
opCode = this.cpu.getWordSafe(addr);
/*
* We have to be careful about this HALT-skipping code, because as fate would have it, I inadvertently
* stopped the following diagnostic with a breakpoint *on* a HALT instruction:
*
* .R EKBEE1
* EKBEE1.BIC
*
* CEKBEE0 11/70 MEM MGMT
*
* CPU UNDER TEST FOUND TO BE A KB11-CM
* bp 033330 hit
* stopped (28339757 instructions, 123994176 cycles, 19177 ms, 6465775 hz)
* R0=140000 R1=033330 R2=100143 R3=133260 R4=000000 R5=177700
* SP=000600 PC=033330 PS=140000 IR=000000 SL=000377 T0 N0 Z0 V0 C0
* 033330: 000000 HALT
*
* Since we haven't executed the HALT yet, it would be wrong (and would cause a diagnostic failure) to
* skip over it. In this particular case, the PDR for the address of the HALT instruction was invalid,
* so the HALT gets fetched but not executed.
*
* My first thought was that maybe we need to probe the address more thoroughly (getWordSafe() does
* not), but it should be sufficient to simply confirm that the PC of the last opcode executed matches
* the addr of this HALT.
*
* Yes, I could save myself this grief by eliminating these PC hacks, both here and in stopInstruction(),
* but I still think it's a useful debugging aid.
*/
if (opCode == PDP11.OPCODE.HALT && this.cpu.getLastPC() == addr) {
addr = this.cpu.advancePC(2);
}
}
/*
* If the CPU stopped on a breakpoint, we're not interested in stopping again if the machine is starting.
*/
if (nState > 0) {
if (this.nBreakInstructions) {
if (!--this.nBreakInstructions) return true;
}
if (this.checkBreakpoint(addr, 1, this.aBreakExec)) {
return true;
}
}
/*
* The rest of the instruction tracking logic can only be performed if historyInit() has allocated the
* necessary data structures. Note that there is no explicit UI for enabling/disabling history, other than
* adding/removing breakpoints, simply because it's breakpoints that trigger the call to checkInstruction();
* well, OK, and a few other things now, like enabling MessagesPDP11.INT messages.
*/
if (nState >= 0 && this.aInstructionHistory.length) {
this.cInstructions++;
if (opCode < 0) {
opCode = this.cpu.getWordSafe(addr);
}
if ((opCode & 0xffff) != PDP11.OPCODE.INVALID) {
var dbgAddr = this.aInstructionHistory[this.iInstructionHistory];
this.setAddr(dbgAddr, addr);
// if (DEBUG) dbgAddr.cycleCount = cpu.getCycles();
if (++this.iInstructionHistory == this.aInstructionHistory.length) this.iInstructionHistory = 0;
}
}
return false;
}
/**
* stopInstruction(sMessage)
*
* TODO: Currently, the only way to prevent this call from stopping the CPU is when you're single-stepping.
*
* @this {DebuggerPDP11}
* @param {string} [sMessage]
* @return {boolean} true if stopping is enabled, false if not
*/
stopInstruction(sMessage)
{
var cpu = this.cpu;
if (cpu.isRunning()) {
cpu.setPC(this.cpu.getLastPC());
if (sMessage) this.println(sMessage);
this.stopCPU();
/*
* TODO: Review the appropriate-ness of throwing a bogus vector number in order to immediately stop
* the instruction. It's handy, but it also means that we no longer actually return true, so callers
* of either stopInstruction() or undefinedInstruction() may have unreachable code paths.
*/
throw -1;
}
return false;
}
/**
* undefinedInstruction(opCode)
*
* @this {DebuggerPDP11}
* @param {number} opCode
* @return {boolean} true if stopping is enabled, false if not
*/
undefinedInstruction(opCode)
{
if (this.messageEnabled(MessagesPDP11.CPU)) {
this.printMessage("undefined opcode " + this.toStrBase(opCode), true, true);
return this.stopInstruction(); // allow the caller to step over it if they really want a trap generated
}
return false;
}
/**
* checkMemoryRead(addr, nb)
*
* This "check" function is called by a Memory block to inform us that a memory read occurred, giving us an
* opportunity to track the read if we want, and look for a matching "read" breakpoint, if any.
*
* In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now
* Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" read.
*
* If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing.
*
* @this {DebuggerPDP11}
* @param {number} addr
* @param {number} [nb] (# of bytes; default is 1)
* @return {boolean} true if breakpoint hit, false if not
*/
checkMemoryRead(addr, nb)
{
if (this.checkBreakpoint(addr, nb || 1, this.aBreakRead)) {
this.stopCPU(false);
return true;
}
return false;
}
/**
* checkMemoryWrite(addr, nb)
*
* This "check" function is called by a Memory block to inform us that a memory write occurred, giving us an
* opportunity to track the write if we want, and look for a matching "write" breakpoint, if any.
*
* In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now
* Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" write.
*
* If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing.
*
* @this {DebuggerPDP11}
* @param {number} addr
* @param {number} [nb] (# of bytes; default is 1)
* @return {boolean} true if breakpoint hit, false if not
*/
checkMemoryWrite(addr, nb)
{
if (this.checkBreakpoint(addr, nb || 1, this.aBreakWrite)) {
this.stopCPU(false);
return true;
}
return false;
}
/**
* clearBreakpoints()
*
* @this {DebuggerPDP11}
*/
clearBreakpoints()
{
var i, dbgAddr, addr;
this.aBreakExec = ["bp"];
if (this.aBreakRead !== undefined) {
for (i = 1; i < this.aBreakRead.length; i++) {
dbgAddr = this.aBreakRead[i];
addr = this.getAddr(dbgAddr);
if (!dbgAddr.fPhysical) {
this.cpu.removeMemBreak(addr, false);
} else {
this.bus.removeMemBreak(addr, false);
}
}
}
this.aBreakRead = ["br"];
if (this.aBreakWrite !== undefined) {
for (i = 1; i < this.aBreakWrite.length; i++) {
dbgAddr = this.aBreakWrite[i];
addr = this.getAddr(dbgAddr);
if (!dbgAddr.fPhysical) {
this.cpu.removeMemBreak(addr, true);
} else {
this.bus.removeMemBreak(addr, true);
}
}
}
this.aBreakWrite = ["bw"];
/*
* nSuppressBreaks ensures we can't get into an infinite loop where a breakpoint lookup
* requires reading memory that triggers more memory reads, which triggers more breakpoint checks.
*/
this.nSuppressBreaks = 0;
this.nBreakInstructions = 0;
}
/**
* addBreakpoint(aBreak, dbgAddr, fTemporary)
*
* In case you haven't already figured this out, all our breakpoint commands use the address
* to identify a breakpoint, not an incrementally assigned breakpoint index like other debuggers;
* see doBreak() for details.
*
* This has a few implications, one being that you CANNOT set more than one kind of breakpoint
* on a single address. In practice, that's rarely a problem, because you can almost always set
* a different breakpoint on a neighboring address.
*
* Also, there is one exception to the "one address, one breakpoint" rule, and that involves
* temporary breakpoints (ie, one-time execution breakpoints that either a "p" or "g" command
* may create to step over a chunk of code). Those breakpoints automatically clear themselves,
* so there usually isn't any need to refer to them using breakpoint commands.
*
* TODO: Consider supporting the more "traditional" breakpoint index syntax; the current
* address-based syntax was implemented solely for expediency and consistency. At the same time,
* also consider a more WDEB386-like syntax, where "br" is used to set a variety of access-specific
* breakpoints, using modifiers like "r1", "r2", "w1", "w2, etc.
*
* @this {DebuggerPDP11}
* @param {Array} aBreak
* @param {DbgAddrPDP11} dbgAddr
* @param {boolean} [fTemporary]
* @return {boolean} true if breakpoint added, false if already exists
*/
addBreakpoint(aBreak, dbgAddr, fTemporary)
{
var fSuccess = true;
// this.nSuppressBreaks++;
/*
* Instead of complaining that a breakpoint already exists (as we used to do), we now
* allow breakpoints to be re-set; this makes it easier to update any commands that may
* be associated with the breakpoint.
*
* The only exception: we DO allow a temporary breakpoint at an address where there may
* already be a breakpoint, so that you can easily step ("p" or "g") over such addresses.
*/
if (!fTemporary) {
this.findBreakpoint(aBreak, dbgAddr, true, false, true);
}
if (aBreak != this.aBreakExec) {
var addr = this.getAddr(dbgAddr);
if (addr === PDP11.ADDR_INVALID) {
this.println("invalid address: " + this.toStrAddr(dbgAddr));
fSuccess = false;
} else {
var fWrite = (aBreak == this.aBreakWrite);
/*
* We automatically promote any read/write breakpoint address to fPhysical if it's
* outside the 16-bit virtual address range.
*/
if (addr > 0xffff) dbgAddr.fPhysical = true;
if (!dbgAddr.fPhysical) {
this.cpu.addMemBreak(addr, fWrite);
} else {
this.bus.addMemBreak(addr, fWrite);
}
}
}
if (fSuccess) {
aBreak.push(dbgAddr);
if (fTemporary) {
dbgAddr.fTemporary = true;
}
else {
this.printBreakpoint(aBreak, aBreak.length-1, "set");
this.historyInit();
}
}
// this.nSuppressBreaks--;
return fSuccess;
}
/**
* findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet)
*
* @this {DebuggerPDP11}
* @param {Array} aBreak
* @param {DbgAddrPDP11} dbgAddr
* @param {boolean} [fRemove]
* @param {boolean} [fTemporary]
* @param {boolean} [fQuiet]
* @return {boolean} true if found, false if not
*/
findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet)
{
var fFound = false;
var addr = this.getAddr(dbgAddr);
for (var i = 1; i < aBreak.length; i++) {
var dbgAddrBreak = aBreak[i];
if (addr == this.getAddr(dbgAddrBreak)) {
if (!fTemporary || dbgAddrBreak.fTemporary) {
fFound = true;
if (fRemove) {
if (!dbgAddrBreak.fTemporary && !fQuiet) {
this.printBreakpoint(aBreak, i, "cleared");
}
aBreak.splice(i, 1);
if (aBreak != this.aBreakExec) {
var fWrite = (aBreak == this.aBreakWrite);
if (!dbgAddrBreak.fPhysical) {
this.cpu.removeMemBreak(addr, fWrite);
} else {
this.bus.removeMemBreak(addr, fWrite);
}
}
/*
* We'll mirror the logic in addBreakpoint() and leave the history buffer alone if this
* was a temporary breakpoint.
*/
if (!dbgAddrBreak.fTemporary) {
this.historyInit();
}
break;
}
if (!fQuiet) this.printBreakpoint(aBreak, i, "exists");
break;
}
}
}
return fFound;
}
/**
* listBreakpoints(aBreak)
*
* @this {DebuggerPDP11}
* @param {Array} aBreak
* @return {number} of breakpoints listed, 0 if none
*/
listBreakpoints(aBreak)
{
for (var i = 1; i < aBreak.length; i++) {
this.printBreakpoint(aBreak, i);
}
return aBreak.length - 1;
}
/**
* printBreakpoint(aBreak, i, sAction)
*
* @this {DebuggerPDP11}
* @param {Array} aBreak
* @param {number} i
* @param {string} [sAction]
*/
printBreakpoint(aBreak, i, sAction)
{
var dbgAddr = aBreak[i];
this.println(aBreak[0] + ' ' + this.toStrAddr(dbgAddr) + (sAction? (' ' + sAction) : (dbgAddr.sCmd? (' "' + dbgAddr.sCmd + '"') : '')));
}
/**
* setTempBreakpoint(dbgAddr)
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr of new temp breakpoint
*/
setTempBreakpoint(dbgAddr)
{
this.addBreakpoint(this.aBreakExec, dbgAddr, true);
}
/**
* clearTempBreakpoint(addr)
*
* @this {DebuggerPDP11}
* @param {number|undefined} [addr] clear all temp breakpoints if no address specified
*/
clearTempBreakpoint(addr)
{
if (addr !== undefined) {
this.checkBreakpoint(addr, 1, this.aBreakExec, true);
this.nStep = 0;
} else {
for (var i = 1; i < this.aBreakExec.length; i++) {
var dbgAddrBreak = this.aBreakExec[i];
if (dbgAddrBreak.fTemporary) {
if (!this.findBreakpoint(this.aBreakExec, dbgAddrBreak, true, true)) break;
i = 0;
}
}
}
}
/**
* checkBreakpoint(addr, nb, aBreak, fTemporary)
*
* @this {DebuggerPDP11}
* @param {number} addr
* @param {number} nb (# of bytes)
* @param {Array} aBreak
* @param {boolean} [fTemporary]
* @return {boolean} true if breakpoint has been hit, false if not
*/
checkBreakpoint(addr, nb, aBreak, fTemporary)
{
/*
* Time to check for breakpoints; note that this should be done BEFORE updating history data
* (see checkInstruction), since we might not actually execute the current instruction.
*/
var fBreak = false;
if (!this.nSuppressBreaks++) {
for (var i = 1; !fBreak && i < aBreak.length; i++) {
var dbgAddrBreak = aBreak[i];
if (fTemporary && !dbgAddrBreak.fTemporary) continue;
/*
* If we're checking an execution address, which is always virtual, and virtual
* addresses are always restricted to 16 bits, let's mask the breakpoint address to match
* (the user should know better, but we'll be nice).
*/
var addrBreak = this.getAddr(dbgAddrBreak) & (aBreak == this.aBreakExec? 0xffff : -1);
for (var n = 0; n < nb; n++) {
if ((addr + n) != addrBreak) continue;
var a;
fBreak = true;
if (dbgAddrBreak.fTemporary) {
this.findBreakpoint(aBreak, dbgAddrBreak, true, true);
fTemporary = true;
}
if (a = dbgAddrBreak.aCmds) {
/*
* When one or more commands are attached to a breakpoint, we don't halt by default.
* Instead, we set fBreak to true only if, at the completion of all the commands, the
* CPU is halted; in other words, you should include "h" as one of the breakpoint commands
* if you want the breakpoint to stop execution.
*
* Another useful command is "if", which will return false if the expression is false,
* at which point we'll jump ahead to the next "else" command, and if there isn't an "else",
* we abort.
*/
fBreak = false;
for (var j = 0; j < a.length; j++) {
if (!this.doCommand(a[j], true)) {
if (a[j].indexOf("if")) {
fBreak = true; // the failed command wasn't "if", so abort
break;
}
var k = j + 1;
for (; k < a.length; k++) {
if (!a[k].indexOf("else")) break;
j++;
}
if (k == a.length) { // couldn't find an "else" after the "if", so abort
fBreak = true;
break;
}
/*
* If we're still here, we'll execute the "else" command (which is just a no-op),
* followed by any remaining commands.
*/
}
}
if (!this.cpu.isRunning()) fBreak = true;
}
if (fBreak) {
if (!fTemporary) this.printBreakpoint(aBreak, i, "hit");
break;
}
}
}
}
this.nSuppressBreaks--;
return fBreak;
}
/**
* getInstruction(dbgAddr, sComment, nSequence)
*
* Get the next instruction, by decoding the opcode and any operands.
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {string} [sComment] is an associated comment
* @param {number|null} [nSequence] is an associated sequence number, undefined if none
* @return {string} (and dbgAddr is updated to the next instruction)
*/
getInstruction(dbgAddr, sComment, nSequence)
{
var opNames = DebuggerPDP11.OPNAMES;
var dbgAddrOp = this.newAddr(dbgAddr.addr);
var opCode = this.getWord(dbgAddr, 2);
var opDesc;
for (var mask in this.opTable) {
var opMasks = this.opTable[mask];
opDesc = opMasks[opCode & mask];
if (opDesc) break;
}
if (!opDesc) {
opDesc = DebuggerPDP11.OPNONE;
}
var opNum = opDesc[0];
if (this.aOpReserved.indexOf(opNum) >= 0) {
opDesc = DebuggerPDP11.OPNONE;
opNum = opDesc[0];
}
var sOperands = "", sTarget = "";
var sOpName = opNames[opNum];
var cOperands = opDesc.length - 1;
if (!opNum && !cOperands) {
sOperands = this.toStrBase(opCode);
}
for (var iOperand = 1; iOperand <= cOperands; iOperand++) {
var opType = opDesc[iOperand];
if (opType === undefined) continue;
var sOperand = this.getOperand(opCode, opType, dbgAddr);
if (!sOperand || !sOperand.length) {
sOperands = "INVALID";
break;
}
/*
* If getOperand() returns an Array rather than a string, then the first element is the original
* operand, and the second element contains additional information (eg, the target) of the operand.
*/
if (typeof sOperand != "string") {
sTarget = sOperand[1];
sOperand = sOperand[0];
}
if (sOperands.length > 0) sOperands += ',';
sOperands += (sOperand || "???");
}
var sOpCodes = "";
var sLine = this.toStrAddr(dbgAddrOp) + ":";
if (dbgAddrOp.addr !== PDP11.ADDR_INVALID && dbgAddr.addr !== PDP11.ADDR_INVALID) {
do {
sOpCodes += ' ' + this.toStrBase(this.getWord(dbgAddrOp, 2));
if (dbgAddrOp.addr == null) break;
} while (dbgAddrOp.addr != dbgAddr.addr);
}
sLine += Str.pad(sOpCodes, 24);
sLine += Str.pad(sOpName, 5);
if (sOperands) sLine += ' ' + sOperands;
if (sComment || sTarget) {
sLine = Str.pad(sLine, 60) + ';' + (sComment || "");
if (!this.cpu.flags.checksum) {
sLine += (nSequence != null? '=' + nSequence.toString() : "");
} else {
var nCycles = this.cpu.getCycles();
sLine += "cycles=" + nCycles.toString() + " cs=" + Str.toHex(this.cpu.nChecksum);
}
if (sTarget) {
if (sLine.slice(-1) != ';') sLine += ' ';
sLine += sTarget;
}
}
return sLine;
}
/**
* getOperand(opCode, opType, dbgAddr)
*
* If getOperand() returns an Array rather than a string, then the first element is the original
* operand, and the second element is a comment containing additional information (eg, the target)
* of the operand.
*
* @this {DebuggerPDP11}
* @param {number} opCode
* @param {number} opType
* @param {DbgAddrPDP11} dbgAddr
* @return {string|Array.<string>}
*/
getOperand(opCode, opType, dbgAddr)
{
var sOperand = "", disp, addr;
/*
* Take care of OP_OTHER opcodes first; then all we'll have to worry about
* next are OP_SRC or OP_DST opcodes.
*/
var opTypeOther = opType & DebuggerPDP11.OP_OTHER;
if (opTypeOther == DebuggerPDP11.OP_BRANCH) {
disp = ((opCode & 0xff) << 24) >> 23;
addr = (dbgAddr.addr + disp) & 0xffff;
sOperand = this.toStrBase(addr);
}
else if (opTypeOther == DebuggerPDP11.OP_DSTOFF) {
disp = (opCode & 0x3f) << 1;
addr = (dbgAddr.addr - disp) & 0xffff;
sOperand = this.toStrBase(addr);
}
else if (opTypeOther == DebuggerPDP11.OP_DSTNUM3) {
disp = (opCode & 0x07);
sOperand = this.toStrBase(disp, 3);
}
else if (opTypeOther == DebuggerPDP11.OP_DSTNUM6) {
disp = (opCode & 0x3f);
sOperand = this.toStrBase(disp, 6);
}
else if (opTypeOther == DebuggerPDP11.OP_DSTNUM8) {
disp = (opCode & 0xff);
sOperand = this.toStrBase(disp, 8);
}
else {
/*
* Isolate all OP_SRC or OP_DST bits from opcode in the opMode variable.
*/
var opMode = opCode & opType;
/*
* Convert OP_SRC bits into OP_DST bits, since they use the same format.
*/
if (opType & DebuggerPDP11.OP_SRC) {
opMode >>= 6;
opType >>= 6;
}
if (opType & DebuggerPDP11.OP_DST) {
var wIndex;
var sTarget = null;
var reg = opMode & DebuggerPDP11.OP_DSTREG;
/*
* Note that opcodes that specify only REG bits in the opType mask (ie, no MOD bits)
* will automatically default to OPMODE_REG below.
*/
switch((opMode & DebuggerPDP11.OP_DSTMODE)) {
case PDP11.OPMODE.REG: // 0x0: REGISTER
sOperand = this.getRegName(reg);
break;
case PDP11.OPMODE.REGD: // 0x1: REGISTER DEFERRED
sOperand = '@' + this.getRegName(reg);
sTarget = this.getTarget(this.cpu.regsGen[reg]);
break;
case PDP11.OPMODE.POSTINC: // 0x2: POST-INCREMENT
if (reg < 7) {
sOperand = '(' + this.getRegName(reg) + ")+";
} else {
/*
* When using R7 (aka PC), POST-INCREMENT is known as IMMEDIATE
*/
wIndex = this.getWord(dbgAddr, 2);
sOperand = '#' + this.toStrBase(wIndex, -1);
}
break;
case PDP11.OPMODE.POSTINCD: // 0x3: POST-INCREMENT DEFERRED
if (reg < 7) {
sOperand = "@(" + this.getRegName(reg) + ")+";
} else {
/*
* When using R7 (aka PC), POST-INCREMENT DEFERRED is known as ABSOLUTE
*/
wIndex = this.getWord(dbgAddr, 2);
sOperand = "@#" + this.toStrBase(wIndex, -1);
sTarget = this.getTarget(wIndex);
}
break;
case PDP11.OPMODE.PREDEC: // 0x4: PRE-DECREMENT
sOperand = "-(" + this.getRegName(reg) + ")";
break;
case PDP11.OPMODE.PREDECD: // 0x5: PRE-DECREMENT DEFERRED
sOperand = "@-(" + this.getRegName(reg) + ")";
break;
case PDP11.OPMODE.INDEX: // 0x6: INDEX
wIndex = this.getWord(dbgAddr, 2);
sOperand = this.toStrBase(wIndex, -1) + '(' + this.getRegName(reg) + ')';
if (reg == 7) {
/*
* When using R7 (aka PC), INDEX is known as RELATIVE. However, instead of displaying
* such an instruction like this:
*
* 016156: 010167 001300 MOV R1,1300(PC) ; @017462
*
* with the effective address display to the far right, let's display it like this instead:
*
* 016156: 010167 001300 MOV R1,017462
*
* because you can still clearly see PC-relative offset (eg, 001300) as part of the disassembly.
*
* sOperand = [sOperand, this.toStrBase((wIndex + dbgAddr.addr) & 0xffff)];
*/
sOperand = this.toStrBase(wIndex = (wIndex + dbgAddr.addr) & 0xffff);
sTarget = this.getTarget(wIndex);
}
break;
case PDP11.OPMODE.INDEXD: // 0x7: INDEX DEFERRED
wIndex = this.getWord(dbgAddr, 2);
sOperand = '@' + this.toStrBase(wIndex) + '(' + this.getRegName(reg) + ')';
if (reg == 7) {
/*
* When using R7 (aka PC), INDEX DEFERRED is known as RELATIVE DEFERRED. And for the same
* reasons articulated above, we now display the effective address inline.
*
* sOperand = [sOperand, this.toStrBase((wIndex + dbgAddr.addr) & 0xffff)];
*/
sOperand = '@' + this.toStrBase(wIndex = (wIndex + dbgAddr.addr) & 0xffff);
sTarget = this.getTarget(this.cpu.getWordSafe(wIndex));
}
break;
default:
break;
}
if (sTarget) sOperand = [sOperand, sTarget];
}
else {
}
}
return sOperand;
}
/**
* getTarget(addr)
*
* @this {DebuggerPDP11}
* @param {number} addr
* @return {string|null}
*/
getTarget(addr)
{
var sTarget = null;
var a = this.cpu.getAddrInfo(addr);
var addrPhysical = a[0];
if (addrPhysical >= this.cpu.addrIOPage && addrPhysical < this.bus.addrIOPage) {
addrPhysical = (addrPhysical - this.cpu.addrIOPage) + this.bus.addrIOPage;
}
return this.bus.getAddrInfo(addrPhysical);
}
/**
* parseInstruction(sOp, sOperand, addr)
*
* TODO: Unimplemented. See parseInstruction() in modules/c1pjs/lib/debugger.js for a sample implementation.
*
* @this {DebuggerPDP11}
* @param {string} sOp
* @param {string|undefined} sOperand
* @param {DbgAddrPDP11} dbgAddr of memory where this instruction is being assembled
* @return {Array.<number>} of opcode bytes; if the instruction can't be parsed, the array will be empty
*/
parseInstruction(sOp, sOperand, dbgAddr)
{
var aOpBytes = [];
this.println("not supported yet");
return aOpBytes;
}
/**
* getFlagOutput(sFlag)
*
* @this {DebuggerPDP11}
* @param {string} sFlag
* @return {string} value of flag
*/
getFlagOutput(sFlag)
{
var b;
switch (sFlag) {
case 'N':
b = this.cpu.getNF();
break;
case 'Z':
b = this.cpu.getZF();
break;
case 'V':
b = this.cpu.getVF();
break;
case 'C':
b = this.cpu.getCF();
break;
default:
b = 0;
break;
}
return sFlag.charAt(0) + (b? '1' : '0') + ' ';
}
/**
* getRegOutput(iReg)
*
* @this {DebuggerPDP11}
* @param {number} iReg
* @return {string}
*/
getRegOutput(iReg)
{
var sReg = this.getRegName(iReg);
if (sReg) {
sReg += '=' + this.toStrBase(this.getRegValue(iReg)) + ' ';
}
return sReg;
}
/**
* getMiscDump()
*
* Sample register dump:
*
* M0=xxxxxx M1=xxxxxx M2=xxxxxx M3=xxxxxx ER=xxxxxx
*
* @this {DebuggerPDP11}
* @return {string}
*/
getMiscDump()
{
var sDump = "";
sDump += this.getRegOutput(DebuggerPDP11.REG_M0) + this.getRegOutput(DebuggerPDP11.REG_M1);
sDump += this.getRegOutput(DebuggerPDP11.REG_M2) + this.getRegOutput(DebuggerPDP11.REG_M3) + this.getRegOutput(DebuggerPDP11.REG_ER);
sDump += '\n';
sDump += this.getRegOutput(DebuggerPDP11.REG_SR) + this.getRegOutput(DebuggerPDP11.REG_AR) + this.getRegOutput(DebuggerPDP11.REG_DR);
return sDump;
}
/**
* getRegDump(fMisc)
*
* Sample register dump:
*
* R0=xxxxxx R1=xxxxxx R2=xxxxxx R3=xxxxxx R4=xxxxxx R5=xxxxxx
* SP=xxxxxx PC=xxxxxx PS=xxxxxx PI=xxxxxx SL=xxxxxx T0 N0 Z0 V0 C0
*
* @this {DebuggerPDP11}
* @param {boolean} [fMisc] (true to include misc registers)
* @return {string}
*/
getRegDump(fMisc)
{
var i;
var sDump = "";
for (i = 0; i < PDP11.REG.SP; i++) {
sDump += this.getRegOutput(i);
}
sDump += '\n';
sDump += this.getRegOutput(PDP11.REG.SP) + this.getRegOutput(PDP11.REG.PC);
sDump += this.getRegOutput(DebuggerPDP11.REG_PS) + this.getRegOutput(DebuggerPDP11.REG_PI) + this.getRegOutput(DebuggerPDP11.REG_SL);
sDump += this.getFlagOutput('T') + this.getFlagOutput('N') + this.getFlagOutput('Z') + this.getFlagOutput('V') + this.getFlagOutput('C');
if (fMisc) sDump += '\n' + this.getMiscDump();
return sDump;
}
/**
* comparePairs(p1, p2)
*
* @this {DebuggerPDP11}
* @param {number|string|Array|Object} p1
* @param {number|string|Array|Object} p2
* @return {number}
*/
comparePairs(p1, p2)
{
return p1[0] > p2[0]? 1 : p1[0] < p2[0]? -1 : 0;
}
/**
* addSymbols(sModule, addr, len, aSymbols)
*
* As filedump.js (formerly convrom.php) explains, aSymbols is a JSON-encoded object whose properties consist
* of all the symbols (in upper-case), and the values of those properties are objects containing any or all of
* the following properties:
*
* 'v': the value of an absolute (unsized) value
* 'b': either 1, 2, 4 or undefined if an unsized value
* 's': either a hard-coded segment or undefined
* 'o': the offset of the symbol within the associated address space
* 'l': the original-case version of the symbol, present only if it wasn't originally upper-case
* 'a': annotation for the specified offset; eg, the original assembly language, with optional comment
*
* To that list of properties, we also add:
*
* 'p': the physical address (calculated whenever both 's' and 'o' properties are defined)
*
* Note that values for any 'v', 'b', 's' and 'o' properties are unquoted decimal values, and the values
* for any 'l' or 'a' properties are quoted strings. Also, if double-quotes were used in any of the original
* annotation ('a') values, they will have been converted to two single-quotes, so we're responsible for
* converting them back to individual double-quotes.
*
* For example:
* {
* 'HF_PORT': {
* 'v':800
* },
* 'HDISK_INT': {
* 'b':4, 's':0, 'o':52
* },
* 'ORG_VECTOR': {
* 'b':4, 's':0, 'o':76
* },
* 'CMD_BLOCK': {
* 'b':1, 's':64, 'o':66
* },
* 'DISK_SETUP': {
* 'o':3
* },
* '.40': {
* 'o':40, 'a':"MOV AX,WORD PTR ORG_VECTOR ;GET DISKETTE VECTOR"
* }
* }
*
* If a symbol only has an offset, then that offset value can be assigned to the symbol property directly:
*
* 'DISK_SETUP': 3
*
* The last property is an example of an "anonymous" entry, for offsets where there is no associated symbol.
* Such entries are identified by a period followed by a unique number (usually the offset of the entry), and
* they usually only contain offset ('o') and annotation ('a') properties. I could eliminate the leading
* period, but it offers a very convenient way of quickly discriminating among genuine vs. anonymous symbols.
*
* We add all these entries to our internal symbol table, which is an array of 4-element arrays, each of which
* look like:
*
* [addr, len, aSymbols, aOffsets]
*
* There are two basic symbol operations: findSymbol(), which takes an address and finds the symbol, if any,
* at that address, and findSymbolAddr(), which takes a string and attempts to match it to a non-anonymous
* symbol with a matching offset ('o') property.
*
* To implement findSymbol() efficiently, addSymbols() creates an array of [offset, sSymbol] pairs
* (aOffsets), one pair for each symbol that corresponds to an offset within the specified address space.
*
* We guarantee the elements of aOffsets are in offset order, because we build it using binaryInsert();
* it's quite likely that the MAP file already ordered all its symbols in offset order, but since they're
* hand-edited files, we can't assume that, and we need to ensure that findSymbol()'s binarySearch() operates
* properly.
*
* @this {DebuggerPDP11}
* @param {string|null} sModule
* @param {number|null} addr (physical address where the symbols are located, if the memory is physical; eg, ROM)
* @param {number} len (the size of the region, in bytes)
* @param {Object} aSymbols (collection of symbols in this group; the format of this collection is described below)
*/
addSymbols(sModule, addr, len, aSymbols)
{
var dbgAddr = {};
var aOffsets = [];
for (var sSymbol in aSymbols) {
var symbol = aSymbols[sSymbol];
if (typeof symbol == "number") {
aSymbols[sSymbol] = symbol = {'o': symbol};
}
var offSymbol = symbol['o'];
var sAnnotation = symbol['a'];
if (offSymbol !== undefined) {
Usr.binaryInsert(aOffsets, [offSymbol >>> 0, sSymbol], this.comparePairs);
}
if (sAnnotation) symbol['a'] = sAnnotation.replace(/''/g, "\"");
}
var symbolTable = {
sModule: sModule,
addr: addr,
len: len,
aSymbols: aSymbols,
aOffsets: aOffsets
};
this.aSymbolTable.push(symbolTable);
}
/**
* dumpSymbols()
*
* TODO: Add "numerical" and "alphabetical" dump options. This is simply dumping them in whatever
* order they appeared in the original MAP file.
*
* @this {DebuggerPDP11}
*/
dumpSymbols()
{
for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) {
var symbolTable = this.aSymbolTable[iTable];
for (var sSymbol in symbolTable.aSymbols) {
if (sSymbol.charAt(0) == '.') continue;
var symbol = symbolTable.aSymbols[sSymbol];
var offSymbol = symbol['o'];
if (offSymbol === undefined) continue;
var sSymbolOrig = symbolTable.aSymbols[sSymbol]['l'];
if (sSymbolOrig) sSymbol = sSymbolOrig;
this.println(this.toStrOffset(offSymbol) + ' ' + sSymbol);
}
}
}
/**
* findSymbol(dbgAddr, fNearest)
*
* Search aSymbolTable for dbgAddr, and return an Array for the corresponding symbol (empty if not found).
*
* If fNearest is true, and no exact match was found, then the Array returned will contain TWO sets of
* entries: [0]-[3] will refer to closest preceding symbol, and [4]-[7] will refer to the closest subsequent symbol.
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @param {boolean} [fNearest]
* @return {Array} where [0] == symbol name, [1] == symbol value, [2] == any annotation, and [3] == any associated comment
*/
findSymbol(dbgAddr, fNearest)
{
var aSymbol = [];
var addrSymbol = this.getAddr(dbgAddr) >>> 0;
for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) {
var symbolTable = this.aSymbolTable[iTable];
var addr = symbolTable.addr >>> 0;
var len = symbolTable.len;
if (addrSymbol >= addr && addrSymbol < addr + len) {
var offSymbol = addrSymbol - addr;
var result = Usr.binarySearch(symbolTable.aOffsets, [offSymbol], this.comparePairs);
if (result >= 0) {
this.returnSymbol(iTable, result, aSymbol);
}
else if (fNearest) {
result = ~result;
this.returnSymbol(iTable, result-1, aSymbol);
this.returnSymbol(iTable, result, aSymbol);
}
break;
}
}
return aSymbol;
}
/**
* findSymbolAddr(sSymbol)
*
* Search our symbol tables for sSymbol, and if found, return a dbgAddr (same as parseAddr()).
*
* @this {DebuggerPDP11}
* @param {string} sSymbol
* @return {DbgAddrPDP11|undefined}
*/
findSymbolAddr(sSymbol)
{
var dbgAddr;
var offSymbol = this.bus.getAddrByName(sSymbol);
if (offSymbol == null && sSymbol.match(/^[a-z_][a-z0-9_]*$/i)) {
var sUpperCase = sSymbol.toUpperCase();
for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) {
var symbolTable = this.aSymbolTable[iTable];
var symbol = symbolTable.aSymbols[sUpperCase];
if (symbol != null) {
offSymbol = symbol['o'];
/*
* If the symbol matched but there's no 'o' offset (ie, it wasn't for an address), there's
* no point looking any farther, since each symbol appears only once.
*
* NOTE: We assume that every ROM is ORG'ed at 0x0000, and therefore unless the symbol has an
* explicitly-defined segment, we return the segment associated with the entire group; for a ROM,
* that segment is normally "addrROM >>> 4". Down the road, we may want/need to support a special
* symbol entry (eg, ".ORG") that defines an alternate origin.
*/
break;
}
}
}
if (offSymbol != null) {
dbgAddr = this.newAddr(offSymbol);
}
return dbgAddr;
}
/**
* returnSymbol(iTable, iOffset, aSymbol)
*
* Helper function for findSymbol().
*
* @param {number} iTable
* @param {number} iOffset
* @param {Array} aSymbol is updated with the specified symbol, if it exists
*/
returnSymbol(iTable, iOffset, aSymbol)
{
var symbol = {};
var aOffsets = this.aSymbolTable[iTable].aOffsets;
var offset = 0, sSymbol = null;
if (iOffset >= 0 && iOffset < aOffsets.length) {
offset = aOffsets[iOffset][0];
sSymbol = aOffsets[iOffset][1];
}
if (sSymbol) {
symbol = this.aSymbolTable[iTable].aSymbols[sSymbol];
sSymbol = (sSymbol.charAt(0) == '.'? null : (symbol['l'] || sSymbol));
}
aSymbol.push(sSymbol);
aSymbol.push(offset);
aSymbol.push(symbol['a']);
aSymbol.push(symbol['c']);
}
/**
* doHelp()
*
* @this {DebuggerPDP11}
*/
doHelp()
{
var s = "commands:";
for (var sCommand in DebuggerPDP11.COMMANDS) {
s += '\n' + Str.pad(sCommand, 9) + DebuggerPDP11.COMMANDS[sCommand];
}
if (!this.checksEnabled()) s += "\nnote: history disabled if no exec breakpoints";
this.println(s);
}
/**
* doAssemble(asArgs)
*
* This always receives the complete argument array, where the order of the arguments is:
*
* [0]: the assemble command (assumed to be "a")
* [1]: the target address (eg, "200")
* [2]: the operation code, aka instruction name (eg, "adc")
* [3]: the operation mode operand, if any (eg, "14", "[1234]", etc)
*
* The Debugger enters "assemble mode" whenever only the first (or first and second) arguments are present.
* As long as "assemble mode is active, the user can omit the first two arguments on all later assemble commands
* until "assemble mode" is cancelled with an empty command line; the command processor automatically prepends "a"
* and the next available target address to the argument array.
*
* Entering "assemble mode" is optional; one could enter a series of fully-qualified assemble commands; eg:
*
* a ff00 cld
* a ff01 ldx 28
* ...
*
* without ever entering "assemble mode", but of course, that requires more typing and doesn't take advantage
* of automatic target address advancement (see dbgAddrAssemble).
*
* NOTE: As the previous example implies, you can even assemble new instructions into ROM address space;
* as our setByte() function explains, the ROM write-notification handlers only refuse writes from the CPU.
*
* @this {DebuggerPDP11}
* @param {Array.<string>} asArgs is the complete argument array, beginning with the "a" command in asArgs[0]
*/
doAssemble(asArgs)
{
var dbgAddr = this.parseAddr(asArgs[1], true);
if (!dbgAddr) return;
this.dbgAddrAssemble = dbgAddr;
if (asArgs[2] === undefined) {
this.println("begin assemble at " + this.toStrAddr(dbgAddr));
this.fAssemble = true;
this.cmp.updateDisplays();
return;
}
var aOpBytes = this.parseInstruction(asArgs[2], asArgs[3], dbgAddr);
if (aOpBytes.length) {
for (var i = 0; i < aOpBytes.length; i++) {
this.setByte(dbgAddr, aOpBytes[i], 1);
}
/*
* Since getInstruction() also updates the specified address, dbgAddrAssemble is automatically advanced.
*/
this.println(this.getInstruction(this.dbgAddrAssemble));
}
}
/**
* doBreak(sCmd, sAddr, sOptions)
*
* As the "help" output below indicates, the following breakpoint commands are supported:
*
* bp # set exec breakpoint
* br # set read breakpoint
* bw # set write breakpoint
* bc # clear breakpoint (* to clear all)
* bl list all breakpoints
* bn [#] break after # instruction(s)
*
* The "bn" command, like the "dh" command and all other commands that use an instruction count,
* assumes a decimal value, regardless of the current base. Use "bn" without an argument to display
* the break count, and use "bn 0" to clear the break count.
*
* @this {DebuggerPDP11}
* @param {string} sCmd
* @param {string|undefined} [sAddr]
* @param {string} [sOptions] (the rest of the breakpoint command-line)
*/
doBreak(sCmd, sAddr, sOptions)
{
if (sAddr == '?') {
this.println("breakpoint commands:");
this.println("\tbp #\tset exec breakpoint");
this.println("\tbr #\tset read breakpoint");
this.println("\tbw #\tset write breakpoint");
this.println("\tbc #\tclear breakpoint (* to clear all)");
this.println("\tbl\tlist all breakpoints");
this.println("\tbn [#]\tbreak after # instruction(s)");
return;
}
var sParm = sCmd.charAt(1);
if (sParm == 'l') {
var cBreaks = 0;
cBreaks += this.listBreakpoints(this.aBreakExec);
cBreaks += this.listBreakpoints(this.aBreakRead);
cBreaks += this.listBreakpoints(this.aBreakWrite);
if (!cBreaks) this.println("no breakpoints");
return;
}
if (sParm == 'n') {
var n = +sAddr || 0;
if (sAddr) this.nBreakInstructions = n;
this.println("break after " + n + " instruction(s)");
return;
}
if (sAddr === undefined) {
this.println("missing breakpoint address");
return;
}
var dbgAddr = this.newAddr();
if (sAddr != '*') {
dbgAddr = this.parseAddr(sAddr, true, true);
if (!dbgAddr) return;
}
if (sParm == 'c') {
if (dbgAddr.addr == null) {
this.clearBreakpoints();
this.println("all breakpoints cleared");
return;
}
if (this.findBreakpoint(this.aBreakExec, dbgAddr, true))
return;
if (this.findBreakpoint(this.aBreakRead, dbgAddr, true))
return;
if (this.findBreakpoint(this.aBreakWrite, dbgAddr, true))
return;
this.println("breakpoint missing: " + this.toStrAddr(dbgAddr));
return;
}
if (dbgAddr.addr == null) return;
this.parseAddrOptions(dbgAddr, sOptions);
if (sParm == 'p') {
this.addBreakpoint(this.aBreakExec, dbgAddr);
return;
}
if (sParm == 'r') {
this.addBreakpoint(this.aBreakRead, dbgAddr);
return;
}
if (sParm == 'w') {
this.addBreakpoint(this.aBreakWrite, dbgAddr);
return;
}
this.println("unknown breakpoint command: " + sParm);
}
/**
* doClear(sCmd)
*
* @this {DebuggerPDP11}
* @param {string} [sCmd] (eg, "cls" or "clear")
*/
doClear(sCmd)
{
this.cmp.clearPanel();
}
/**
* doDump(asArgs)
*
* The length parameter is interpreted as a number of bytes (or words, or dwords) to dump,
* and it is interpreted using the current base.
*
* @this {DebuggerPDP11}
* @param {Array.<string>} asArgs (formerly sCmd, [sAddr], [sLen] and [sBytes])
*/
doDump(asArgs)
{
var m;
var sCmd = asArgs[0];
var sAddr = asArgs[1];
var sLen = asArgs[2];
var sBytes = asArgs[3];
if (sAddr == '?') {
var sDumpers = "";
for (m in MessagesPDP11.CATEGORIES) {
if (this.afnDumpers[m]) {
if (sDumpers) sDumpers += ',';
sDumpers = sDumpers + m;
}
}
sDumpers += ",state,symbols";
this.println("dump memory commands:");
this.println("\tda [a] dump info for address a");
this.println("\tdb [a] [n] dump n bytes at address a");
this.println("\tdw [a] [n] dump n words at address a");
this.println("\tdd [a] [n] dump n dwords at address a");
this.println("\tds [a] [n] dump n words at address a as JSON");
this.println("\tdh [p] [n] dump n instructions from history position p");
if (sDumpers.length) this.println("dump extension commands:\n\t" + sDumpers);
return;
}
if (sAddr == "state") {
var sState = this.cmp.powerOff(true);
if (sLen == "console") {
/*
* Console buffers are notoriously small, and even the following code, which breaks the
* data into parts (eg, "d state console 1", "d state console 2", etc) just isn't that helpful.
*
* var nPart = +sBytes;
* if (nPart) sState = sState.substr(1000000 * (nPart-1), 1000000);
*
* So, the best way to capture a large machine state is to use the new "Save Machine" link
* that downloads a machine's entire state. Alternatively, run your own local server and use
* server-side storage. Take a look at the "Save" binding in computer.js, which binds an HTML
* control to the computer.powerOff() and computer.saveServerState() functions.
*/
console.log(sState);
} else {
this.doClear();
if (sState) this.println(sState);
}
return;
}
if (sAddr == "symbols") {
this.dumpSymbols();
return;
}
if (sCmd == "d") {
for (m in MessagesPDP11.CATEGORIES) {
if (asArgs[1] == m) {
var fnDumper = this.afnDumpers[m];
if (fnDumper) {
asArgs.shift();
asArgs.shift();
fnDumper(asArgs);
} else {
this.println("no dump registered for " + sAddr);
}
return;
}
}
if (!sAddr) sCmd = this.sCmdDumpPrev || "dw";
} else {
this.sCmdDumpPrev = sCmd;
}
if (sCmd == "dh") {
this.dumpHistory(sAddr, sLen);
return;
}
var dbgAddr = this.parseAddr(sAddr);
if (!dbgAddr) return;
if (sCmd == "da") {
/*
* Sample output for a virtual address ("da 23042"):
*
* 00,010,011,000,100,010 00023042
* OFFSET: 0,011,000,100,010 00003042
* + KIPAR1: 0,000,001,101,111,010,000,000 00157200
* & MMUMASK: 1,111,111,111,111,111,111,111 17777777
* = PHYSICAL: 0,000,001,110,010,010,100,010 00162242
*
* and sample output for a physical address (eg, "da %37772"; note the % prefix):
*
* 0,000,000,011,111,111,111,010 00037772
* OFFSET: 1,111,111,111,010 00017772
* UNIMAP[01]: 1,111,100,001,110,111,000,000 17416700
* PHYSICAL: 1,111,100,011,110,110,111,010 17436672
*
* TODO: Tweak this output to accommodate 18-bit machines as well as 22-bit machines.
*/
var fPhysical = (dbgAddr.fPhysical || dbgAddr.addr > 0xffff);
var a = this.cpu.getAddrInfo(dbgAddr.addr || 0, fPhysical);
this.println(Str.pad("", fPhysical? 12: 19) + Str.toBin(dbgAddr.addr, fPhysical? 22 : 17, 3) + " " + Str.toOct(dbgAddr.addr, 8));
if (a.length < 6) {
if (a.length > 2) {
this.println(" OFFSET: " + Str.toBin(a[3], 13, 3) + " " + Str.toOct(a[3], 8));
this.println("UNIMAP[" + Str.toDec(a[1], 2) + "]: " + Str.toBin(a[2], 22, 3) + " " + Str.toOct(a[2], 8));
}
this.println(" PHYSICAL: " + Str.toBin(a[0], 22, 3) + " " + Str.toOct(a[0], 8))
} else {
this.println(" OFFSET: " + Str.toBin(a[1], 13, 3) + " " + Str.toOct(a[1], 8));
this.println("+ " + DebuggerPDP11.MODES[a[2]] + "PAR" + a[3] + ": " + Str.toBin(a[4], 22, 3) + " " + Str.toOct(a[4], 8));
this.println("& MMUMASK: " + Str.toBin(a[5], 22, 3) + " " + Str.toOct(a[5], 8));
this.println("= PHYSICAL: " + Str.toBin(a[0], 22, 3) + " " + Str.toOct(a[0], 8))
}
return;
}
var len = 0;
var fJSON = (sCmd == "ds");
if (sLen) {
if (sLen.charAt(0) == 'l') {
sLen = sLen.substr(1) || sBytes;
len = this.parseValue(sLen);
}
else {
var dbgAddrEnd = this.parseAddr(sLen);
if (dbgAddrEnd) len = dbgAddrEnd.addr - dbgAddr.addr;
}
if (len < 0) len = 0;
if (len > 0x10000) len = 0x10000;
}
var nBase = this.nBase;
if (dbgAddr.nBase) this.nBase = dbgAddr.nBase;
/*
* I've changed the code below to effectively make "dw" the default if only "d" is specified,
* since this is primarily a word-oriented machine.
*/
var size = (sCmd == "dd"? 4 : (sCmd == "db"? 1 : 2));
var nBytes = (size * len) || 128;
var nBytesPerLine = fJSON? 16 : this.nBase;
var nLines = (((nBytes + nBytesPerLine - 1) / nBytesPerLine)|0) || 1;
var sDump = "";
while (nLines-- && nBytes > 0) {
var sData = "", sChars = "";
sAddr = this.toStrAddr(dbgAddr);
/*
* Dump 8 bytes per line when using base 8, and dump 16 bytes when using base 16.
*
* And while we used to always call getByte() and assemble them into words or dwords as appropriate, I've
* changed the logic below to honor "dw" by calling getWord(), since the Bus interfaces have been updated
* to prevent generating traps due to to Debugger access of unaligned memory and/or undefined IOPAGE addresses.
*
* Besides, it's nice for "db" and "dw" to generate the same Bus activity that typical byte and word reads do.
*/
var i = nBytesPerLine;
var data = 0, shift = 0;
while (i > 0 && nBytes > 0) {
var n = 1;
var v = size == 1? this.getByte(dbgAddr, n) : this.getWord(dbgAddr, (n = 2));
data |= (v << (shift << 3));
shift += n;
if (shift == size) {
if (fJSON) {
if (sData) sData += ",";
sData += "0x"+ Str.toHex(data, size << 1);
} else {
sData += this.toStrBase(data, size << 3);
sData += (size == 1? (i == 9? '-' : ' ') : " ");
}
data = shift = 0;
}
i -= n; nBytes -= n;
while (size == 1 && n--) {
var c = v & 0xff;
sChars += (c >= 32 && c < 128? String.fromCharCode(c) : '.');
v >>= 8;
}
}
if (sDump) sDump += "\n";
if (fJSON) {
sDump += sData + ",";
} else {
sDump += sAddr + ": " + sData + ((i == 0)? (' ' + sChars) : "");
}
}
if (sDump) this.println(sDump);
this.dbgAddrNextData = dbgAddr;
this.nBase = nBase;
}
/**
* doEdit(asArgs)
*
* @this {DebuggerPDP11}
* @param {Array.<string>} asArgs
*/
doEdit(asArgs)
{
var size, mask;
var fnGet, fnSet;
var sCmd = asArgs[0];
var sAddr = asArgs[1];
if (sCmd == "eb") {
size = 1;
mask = 0xff;
fnGet = this.getByte;
fnSet = this.setByte;
}
else if (sCmd == "e" || sCmd == "ew") {
size = 2;
mask = 0xffff;
fnGet = this.getWord;
fnSet = this.setWord;
} else {
sAddr = null;
}
if (sAddr == null) {
this.println("edit memory commands:");
this.println("\teb [a] [...] edit bytes at address a");
this.println("\tew [a] [...] edit words at address a");
return;
}
var dbgAddr = this.parseAddr(sAddr);
if (!dbgAddr) return;
for (var i = 2; i < asArgs.length; i++) {
var vNew = this.parseExpression(asArgs[i]);
if (vNew === undefined) {
this.println("unrecognized value: " + asArgs[i]);
break;
}
if (vNew & ~mask) {
this.println("warning: " + Str.toHex(vNew) + " exceeds " + size + "-byte value");
}
this.println("changing " + this.toStrAddr(dbgAddr) + (this.messageEnabled(MessagesPDP11.BUS)? "" : (" from " + this.toStrBase(fnGet.call(this, dbgAddr), size << 3))) + " to " + this.toStrBase(vNew, size << 3));
fnSet.call(this, dbgAddr, vNew, size);
}
}
/**
* doHalt(fQuiet)
*
* @this {DebuggerPDP11}
* @param {boolean} [fQuiet]
*/
doHalt(fQuiet)
{
var sMsg;
if (this.flags.running) {
if (!fQuiet) this.println("halting");
this.stopCPU();
} else {
if (this.isBusy(true)) return;
if (!fQuiet) this.println("already halted");
}
}
/**
* doIf(sCmd, fQuiet)
*
* NOTE: Don't forget that the default base for all numeric constants is 16 (hex), so when you evaluate
* an expression like "a==10", it will compare the value of the variable "a" to 0x10; use a trailing period
* (eg, "10.") if you really intend decimal.
*
* Also, if no variable named "a" exists, "a" will evaluate to 0x0A, so the expression "a==10" becomes
* "0x0A==0x10" (false), whereas the expression "a==10." becomes "0x0A==0x0A" (true).
*
* @this {DebuggerPDP11}
* @param {string} sCmd
* @param {boolean} [fQuiet]
* @return {boolean} true if expression is non-zero, false if zero (or undefined due to a parse error)
*/
doIf(sCmd, fQuiet)
{
sCmd = Str.trim(sCmd);
if (!this.parseExpression(sCmd)) {
if (!fQuiet) this.println("false: " + sCmd);
return false;
}
if (!fQuiet) this.println("true: " + sCmd);
return true;
}
/**
* doInfo(asArgs)
*
* @this {DebuggerPDP11}
* @param {Array.<string>} asArgs
* @return {boolean} true only if the instruction info command ("n") is supported
*/
doInfo(asArgs)
{
if (DEBUG) {
this.println("msPerYield: " + this.cpu.msPerYield);
this.println("nCyclesPerYield: " + this.cpu.nCyclesPerYield);
return true;
}
return false;
}
/**
* doVar(sCmd)
*
* The command must be of the form "{variable} = [{expression}]", where expression may contain constants,
* operators, registers, symbols, other variables, or nothing at all; in the latter case, the variable, if
* any, is deleted.
*
* Other supported shorthand: "var" with no parameters prints the values of all variables, and "var {variable}"
* prints the value of the specified variable.
*
* @this {DebuggerPDP11}
* @param {string} sCmd
* @return {boolean} true if valid "var" assignment, false if not
*/
doVar(sCmd)
{
var a = sCmd.match(/^\s*([A-Z_]?[A-Z0-9_]*)\s*(=?)\s*(.*)$/i);
if (a) {
if (!a[1]) {
if (!this.printVariable()) this.println("no variables");
return true; // it's not considered an error to print an empty list of variables
}
if (!a[2]) {
return this.printVariable(a[1]);
}
if (!a[3]) {
this.delVariable(a[1]);
return true; // it's not considered an error to delete a variable that didn't exist
}
var v = this.parseExpression(a[3]);
if (v !== undefined) {
this.setVariable(a[1], v);
return true;
}
return false;
}
this.println("invalid assignment:" + sCmd);
return false;
}
/**
* doList(sAddr, fPrint)
*
* @this {DebuggerPDP11}
* @param {string} sAddr
* @param {boolean} [fPrint]
* @return {string|null}
*/
doList(sAddr, fPrint)
{
var sSymbol = null;
var dbgAddr = this.parseAddr(sAddr, true);
if (dbgAddr) {
var addr = this.getAddr(dbgAddr);
var aSymbol = this.findSymbol(dbgAddr, true);
if (aSymbol.length) {
var nDelta, sDelta, s;
if (aSymbol[0]) {
sDelta = "";
nDelta = dbgAddr.addr - aSymbol[1];
if (nDelta) sDelta = " + " + Str.toHexWord(nDelta);
s = aSymbol[0] + " (" + this.toStrOffset(aSymbol[1]) + ')' + sDelta;
if (fPrint) this.println(s);
sSymbol = s;
}
if (aSymbol.length > 4 && aSymbol[4]) {
sDelta = "";
nDelta = aSymbol[5] - dbgAddr.addr;
if (nDelta) sDelta = " - " + Str.toHexWord(nDelta);
s = aSymbol[4] + " (" + this.toStrOffset(aSymbol[5]) + ')' + sDelta;
if (fPrint) this.println(s);
if (!sSymbol) sSymbol = s;
}
} else {
if (fPrint) this.println("no symbols");
}
}
return sSymbol;
}
/**
* doMessages(asArgs)
*
* @this {DebuggerPDP11}
* @param {Array.<string>} asArgs
*/
doMessages(asArgs)
{
var m;
var fCriteria = null;
var sCategory = asArgs[1];
if (sCategory == '?') sCategory = undefined;
if (sCategory !== undefined) {
var bitsMessage = 0;
if (sCategory == "all") {
bitsMessage = (0xffffffff|0) & ~(MessagesPDP11.HALT | MessagesPDP11.KEYS | MessagesPDP11.LOG);
sCategory = null;
} else if (sCategory == "on") {
fCriteria = true;
sCategory = null;
} else if (sCategory == "off") {
fCriteria = false;
sCategory = null;
} else {
/*
* Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects,
* but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard".
*/
if (sCategory == "keys") sCategory = "key";
if (sCategory == "kbd") sCategory = "keyboard";
for (m in MessagesPDP11.CATEGORIES) {
if (sCategory == m) {
bitsMessage = MessagesPDP11.CATEGORIES[m];
fCriteria = !!(this.bitsMessage & bitsMessage);
break;
}
}
if (!bitsMessage) {
this.println("unknown message category: " + sCategory);
return;
}
}
if (bitsMessage) {
if (asArgs[2] == "on") {
this.bitsMessage |= bitsMessage;
fCriteria = true;
}
else if (asArgs[2] == "off") {
this.bitsMessage &= ~bitsMessage;
fCriteria = false;
if (bitsMessage == MessagesPDP11.LOG) {
var i = this.aMessageLog.length >= 1000? this.aMessageLog.length - 1000 : 0;
while (i < this.aMessageLog.length) {
this.println(this.aMessageLog[i++]);
}
this.aMessageLog = [];
}
}
}
}
/*
* Display those message categories that match the current criteria (on or off)
*/
var n = 0;
var sCategories = "";
for (m in MessagesPDP11.CATEGORIES) {
if (!sCategory || sCategory == m) {
var bitMessage = MessagesPDP11.CATEGORIES[m];
var fEnabled = !!(this.bitsMessage & bitMessage);
if (fCriteria !== null && fCriteria != fEnabled) continue;
if (sCategories) sCategories += ',';
if (!(++n % 10)) sCategories += "\n\t"; // jshint ignore:line
/*
* Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects,
* but externally, we allow the user to specify "keys".
*/
if (m == "key") m = "keys";
sCategories += m;
}
}
if (sCategory === undefined) {
this.println("message commands:\n\tm [category] [on|off]\tturn categories on/off");
}
this.println((fCriteria !== null? (fCriteria? "messages on: " : "messages off: ") : "message categories:\n\t") + (sCategories || "none"));
this.historyInit(); // call this just in case MessagesPDP11.INT was turned on
}
/**
* doOptions(asArgs)
*
* @this {DebuggerPDP11}
* @param {Array.<string>} asArgs
*/
doOptions(asArgs)
{
switch (asArgs[1]) {
case "base":
if (asArgs[2]) {
var nBase = +asArgs[2];
if (nBase == 8 || nBase == 10 || nBase == 16) {
this.nBase = nBase;
} else {
this.println("invalid base: " + nBase);
break;
}
}
this.println("default base: " + this.nBase);
break;
case "cs":
var nCycles;
if (asArgs[3] !== undefined) nCycles = +asArgs[3]; // warning: decimal instead of hex conversion
switch (asArgs[2]) {
case "int":
this.cpu.nCyclesChecksumInterval = nCycles;
break;
case "start":
this.cpu.nCyclesChecksumStart = nCycles;
break;
case "stop":
this.cpu.nCyclesChecksumStop = nCycles;
break;
default:
this.println("unknown cs option");
return;
}
if (nCycles !== undefined) {
this.cpu.resetChecksum();
}
this.println("checksums " + (this.cpu.flags.checksum? "enabled" : "disabled"));
return;
case "sp":
if (asArgs[2] !== undefined) {
if (!this.cpu.setSpeed(+asArgs[2])) {
this.println("warning: using 1x multiplier, previous target not reached");
}
}
this.println("target speed: " + this.cpu.getSpeedTarget() + " (" + this.cpu.getSpeed() + "x)");
return;
default:
if (asArgs[1]) {
this.println("unknown option: " + asArgs[1]);
return;
}
/* falls through */
case "?":
this.println("debugger options:");
this.println("\tbase #\t\tset default base to #");
this.println("\tcs int #\tset checksum cycle interval to #");
this.println("\tcs start #\tset checksum cycle start count to #");
this.println("\tcs stop #\tset checksum cycle stop count to #");
this.println("\tsp #\t\tset speed multiplier to #");
break;
}
}
/**
* doRegisters(asArgs, fInstruction)
*
* @this {DebuggerPDP11}
* @param {Array.<string>} [asArgs]
* @param {boolean} [fInstruction] (true to include the current instruction; default is true)
*/
doRegisters(asArgs, fInstruction)
{
if (asArgs && asArgs[1] == '?') {
this.println("register commands:");
this.println("\tr\tdump registers");
this.println("\trm\tdump misc registers");
this.println("\trx [#]\tset flag or register x to [#]");
return;
}
var fMisc = false;
var cpu = this.cpu;
if (fInstruction == null) fInstruction = true;
if (asArgs != null && asArgs.length > 1) {
var sReg = asArgs[1];
if (sReg == 'm') {
fMisc = true;
}
else {
var sValue = null;
var i = sReg.indexOf('=');
if (i > 0) {
sValue = sReg.substr(i + 1);
sReg = sReg.substr(0, i);
}
else if (asArgs.length > 2) {
sValue = asArgs[2];
}
else {
this.println("missing value for " + asArgs[1]);
return;
}
var w = this.parseExpression(sValue);
if (w === undefined) return;
var sRegMatch = sReg.toUpperCase();
switch (sRegMatch) {
case "SP":
case "R6":
cpu.setSP(w);
break;
case "PC":
case "R7":
cpu.setPC(w);
this.dbgAddrNextCode = this.newAddr(cpu.getPC());
break;
case "N":
if (w) cpu.setNF(); else cpu.clearNF();
break;
case "Z":
if (w) cpu.setZF(); else cpu.clearZF();
break;
case "V":
if (w) cpu.setVF(); else cpu.clearVF();
break;
case "C":
if (w) cpu.setCF(); else cpu.clearCF();
break;
case "PS":
cpu.setPSW(w);
break;
case "PI":
cpu.setPIR(w);
break;
case "ER":
cpu.regErr = w;
fMisc = true;
break;
case "SL":
cpu.setSLR(w);
break;
case "M0":
cpu.setMMR0(w);
fMisc = true;
break;
case "M3":
cpu.setMMR3(w);
fMisc = true;
break;
case "AR":
if (this.panel) this.panel.setAR(w);
fMisc = true;
break;
case "DR":
if (this.panel) this.panel.setDR(w);
fMisc = true;
break;
case "SR":
if (this.panel) this.panel.setSR(w);
fMisc = true;
break;
default:
if (sRegMatch.charAt(0) == 'R') {
var iReg = +sRegMatch.charAt(1);
if (iReg >= 0 && iReg < 6) {
cpu.regsGen[iReg] = w & 0xffff;
break;
}
}
this.println("unknown register: " + sReg);
return;
}
this.cmp.updateDisplays();
this.println("updated registers:");
}
}
this.println(this.getRegDump(fMisc));
if (fInstruction) {
this.dbgAddrNextCode = this.newAddr(cpu.getPC());
this.doUnassemble(this.toStrAddr(this.dbgAddrNextCode));
}
}
/**
* doRun(sCmd, sAddr, sOptions, fQuiet)
*
* @this {DebuggerPDP11}
* @param {string} sCmd
* @param {string|undefined} [sAddr]
* @param {string} [sOptions] (the rest of the breakpoint command-line)
* @param {boolean} [fQuiet]
*/
doRun(sCmd, sAddr, sOptions, fQuiet)
{
if (sCmd == "gt") {
this.fIgnoreNextCheckFault = true;
}
if (sAddr !== undefined) {
var dbgAddr = this.parseAddr(sAddr, true);
if (!dbgAddr) return;
this.parseAddrOptions(dbgAddr, sOptions);
this.setTempBreakpoint(dbgAddr);
}
this.startCPU(true, fQuiet);
}
/**
* doPrint(sCmd)
*
* NOTE: If the string to print is a quoted string, then we run it through replaceRegs(), so that
* you can take advantage of all the special replacement options used for software interrupt logging.
*
* @this {DebuggerPDP11}
* @param {string} sCmd
*/
doPrint(sCmd)
{
sCmd = Str.trim(sCmd);
var a = sCmd.match(/^(['"])(.*?)\1$/);
if (!a) {
this.parseExpression(sCmd, false);
} else {
if (a[2].length > 1) {
this.println(this.replaceRegs(a[2]));
} else {
this.printValue(null, a[2].charCodeAt(0));
}
}
}
/**
* doStep(sCmd, sOption)
*
* @this {DebuggerPDP11}
* @param {string} [sCmd] "p" or "pr"
* @param {string} [sOption]
*/
doStep(sCmd, sOption)
{
if (sOption == '?') {
this.println("step commands:");
this.println("\tp\tstep over instruction");
this.println("\tpr\tstep over instruction with register update");
return;
}
var fCallStep = true;
var nRegs = (sCmd == "pr"? 1 : 0);
/*
* Set up the value for this.nStep (ie, 1 or 2) depending on whether the user wants
* a subsequent register dump ("pr") or not ("p").
*/
var nStep = 1 + nRegs;
if (!this.nStep) {
var dbgAddr = this.newAddr(this.cpu.getPC());
var opCode = this.getWord(dbgAddr);
if (opCode == PDP11.OPCODE.BPT || opCode == PDP11.OPCODE.IOT ||
(opCode & PDP11.OPCODE.EMT_MASK) == PDP11.OPCODE.EMT_OP ||
(opCode & PDP11.OPCODE.SOB_MASK) == PDP11.OPCODE.SOB_OP ||
(opCode & PDP11.OPCODE.TRAP_MASK) == PDP11.OPCODE.TRAP_OP) {
if (fCallStep) {
this.nStep = nStep;
this.incAddr(dbgAddr, 2);
}
} else if ((opCode & PDP11.OPCODE.JSR_MASK) == PDP11.OPCODE.JSR_OP) {
var s = this.getInstruction(dbgAddr);
if (fCallStep) {
this.nStep = nStep;
}
}
if (this.nStep) {
this.setTempBreakpoint(dbgAddr);
if (!this.startCPU()) {
if (this.cmp) this.cmp.setFocus();
this.nStep = 0;
}
/*
* A successful run will ultimately call stop(), which will in turn call clearTempBreakpoint(),
* which will clear nStep, so there's your assurance that nStep will be reset. Now we may have
* stopped for reasons unrelated to the temporary breakpoint, but that's OK.
*/
} else {
this.doTrace(nRegs? "tr" : "t");
}
} else {
this.println("step in progress");
}
}
/**
* getCall(dbgAddr)
*
* Given a possible return address (typically from the stack), look for a matching CALL (or INT) that
* immediately precedes that address.
*
* @this {DebuggerPDP11}
* @param {DbgAddrPDP11} dbgAddr
* @return {string|null} CALL instruction at or near dbgAddr, or null if none
*/
getCall(dbgAddr)
{
var sCall = null;
var addr = dbgAddr.addr;
var addrOrig = addr;
for (var n = 1; n <= 6 && !!addr; n++) {
if (n > 2) {
dbgAddr.addr = addr;
var s = this.getInstruction(dbgAddr);
if (s.indexOf("JSR") >= 0) {
/*
* Verify that the length of this call, when added to the address of the call, matches
* the original return address. We do this by getting the string index of the opcode bytes,
* subtracting that from the string index of the next space, and dividing that difference
* by two, to yield the length of the CALL (or INT) instruction, in bytes.
*/
var i = s.indexOf(' ');
var j = s.indexOf(' ', i+1);
if (addr + (j - i - 1)/2 == addrOrig) {
sCall = s;
break;
}
}
}
addr -= 2;
}
dbgAddr.addr = addrOrig;
return sCall;
}
/**
* doStackTrace(sCmd, sAddr)
*
* Use "k" for a normal stack trace and "ks" for a stack trace with symbolic info.
*
* @this {DebuggerPDP11}
* @param {string} [sCmd]
* @param {string} [sAddr] (not used yet)
*/
doStackTrace(sCmd, sAddr)
{
if (sAddr == '?') {
this.println("stack trace commands:");
this.println("\tk\tshow frame addresses");
this.println("\tks\tshow symbol information");
return;
}
var nFrames = 10, cFrames = 0;
var dbgAddrCall = this.newAddr();
var dbgAddrStack = this.newAddr(this.cpu.getSP());
this.println("stack trace for " + this.toStrAddr(dbgAddrStack));
while (cFrames < nFrames) {
var sCall = null, sCallPrev = null, cTests = 256;
while ((dbgAddrStack.addr >>> 0) < 0x10000) {
dbgAddrCall.addr = this.getWord(dbgAddrStack, 2);
/*
* Because we're using the auto-increment feature of getWord(), and because that will automatically
* wrap the offset around the end of the segment, we must also check the addr property to detect the wrap.
*/
if (dbgAddrStack.addr == null || !cTests--) break;
if (dbgAddrCall.addr & 0x1) continue; // an odd address on the PDP-11 is not a valid instruction boundary
sCall = this.getCall(dbgAddrCall);
if (sCall) break;
}
/*
* The sCallPrev check eliminates duplicate sequential calls, which are usually (but not always)
* indicative of a false positive, in which case the previous call is probably bogus as well, but
* at least we won't duplicate that mistake. Of course, there are always exceptions, recursion
* being one of them, but it's rare that we're debugging recursive code.
*/
if (!sCall || sCall == sCallPrev) break;
var sSymbol = null;
if (sCmd == "ks") {
var a = sCall.match(/[0-9A-F]+$/);
if (a) sSymbol = this.doList(a[0]);
}
sCall = Str.pad(sCall, 50) + " ;" + (sSymbol || "stack=" + this.toStrAddr(dbgAddrStack)); // + " return=" + this.toStrAddr(dbgAddrCall));
this.println(sCall);
sCallPrev = sCall;
cFrames++;
}
if (!cFrames) this.println("no return addresses found");
}
/**
* doTrace(sCmd, sCount)
*
* The "t" and "tr" commands interpret the count as a number of instructions, and since
* we call the Debugger's stepCPU() for each iteration, a single instruction includes
* any/all prefixes; the CPU's stepCPU() treats prefixes as discrete operations. The only
* difference between "t" and "tr": the former displays only the next instruction, while
* the latter also displays the (updated) registers.
*
* The "tc" command interprets the count as a number of cycles rather than instructions,
* allowing you to quickly execute large chunks of instructions with a single command; it
* doesn't display anything until the the chunk has finished. "tc 1" is also a useful
* command in that it doesn't inhibit interrupts like "t" or "tr" does.
*
* However, generally a more useful command is "bn", which allows you to break after some
* number of instructions have been executed (as opposed to some number of cycles).
*
* @this {DebuggerPDP11}
* @param {string} [sCmd] ("t", "tc", or "tr")
* @param {string} [sCount] # of instructions to step
*/
doTrace(sCmd, sCount)
{
if (sCount == '?') {
this.println("trace commands:");
this.println("\tt [#]\ttrace # instructions");
this.println("\ttr [#]\ttrace # instructions with register updates");
this.println("\ttc [#]\ttrace # cycles");
this.println("note: bn [#] breaks after # instructions without updates");
return;
}
var dbg = this;
var fRegs = (sCmd != "t");
var nCount = this.parseValue(sCount, null, true) || 1;
/*
* We used to set nCycles to 1 when a count > 1 was specified, because nCycles set
* to 0 used to mean "execute the next instruction without checking for interrupts".
* Well, this machine's stepCPU() doesn't do that; it ALWAYS checks for interrupts,
* so we should leave nCycles set to 0, so that if an interrupt is dispatched, we will
* get to see the first instruction of the interrupt handler.
*/
var nCycles = 0; // (nCount == 1? 0 : 1);
if (sCmd == "tc") {
nCycles = nCount;
nCount = 1;
}
this.sCmdTracePrev = sCmd;
Web.onCountRepeat(
nCount,
function onCountStep() {
return dbg.setBusy(true) && dbg.stepCPU(nCycles, fRegs, false);
},
function onCountStepComplete() {
/*
* We explicitly called stepCPU() with fUpdateDisplays set to false, because repeatedly
* calling updateDisplays() can be very slow, especially if a Control Panel is present with
* displayLiveRegs enabled, so once the repeat count has been exhausted, we must perform
* a final updateDisplays().
*/
if (dbg.panel) dbg.panel.stop();
dbg.cmp.updateDisplays(-1);
dbg.setBusy(false);
}
);
}
/**
* doUnassemble(sAddr, sAddrEnd, nLines)
*
* @this {DebuggerPDP11}
* @param {string} [sAddr]
* @param {string} [sAddrEnd]
* @param {number} [nLines]
*/
doUnassemble(sAddr, sAddrEnd, nLines)
{
var dbgAddr = this.parseAddr(sAddr, true);
if (!dbgAddr) return;
if (nLines === undefined) nLines = 1;
var nBytes = 0x100;
if (sAddrEnd !== undefined) {
if (sAddrEnd.charAt(0) == 'l') {
var n = this.parseValue(sAddrEnd.substr(1));
if (n != null) nLines = n;
}
else {
var dbgAddrEnd = this.parseAddr(sAddrEnd, true);
if (!dbgAddrEnd || dbgAddrEnd.addr < dbgAddr.addr) return;
nBytes = dbgAddrEnd.addr - dbgAddr.addr;
if (!DEBUG && nBytes > 0x100) {
/*
* Limiting the amount of disassembled code to 256 bytes in non-DEBUG builds is partly to
* prevent the user from wedging the browser by dumping too many lines, but also a recognition
* that, in non-DEBUG builds, this.println() keeps print output buffer truncated to 8Kb anyway.
*/
this.println("range too large");
return;
}
nLines = -1;
}
}
var nPrinted = 0;
var sInstruction;
while (nBytes > 0 && nLines--) {
var nSequence = (this.isBusy(false) || this.nStep)? this.nCycles : null;
var sComment = (nSequence != null? "cycles" : null);
var aSymbol = this.findSymbol(dbgAddr);
var addr = dbgAddr.addr; // we snap dbgAddr.addr *after* calling findSymbol(), which re-evaluates it
if (aSymbol[0] && nLines) {
if (!nPrinted && nLines || aSymbol[0].indexOf('+') < 0) {
var sLabel = aSymbol[0] + ':';
if (aSymbol[2]) sLabel += ' ' + aSymbol[2];
this.println(sLabel);
}
}
if (aSymbol[3]) {
sComment = aSymbol[3];
nSequence = null;
}
sInstruction = this.getInstruction(dbgAddr, sComment, nSequence);
this.println(sInstruction);
this.dbgAddrNextCode = dbgAddr;
nBytes -= dbgAddr.addr - addr;
nPrinted++;
}
}
/**
* splitArgs(sCmd)
*
* @this {DebuggerPDP11}
* @param {string} sCmd
* @return {Array.<string>}
*/
splitArgs(sCmd)
{
var asArgs = sCmd.replace(/ +/g, ' ').split(' ');
asArgs[0] = asArgs[0].toLowerCase();
if (asArgs && asArgs.length) {
var s0 = asArgs[0];
var ch0 = s0.charAt(0);
for (var i = 1; i < s0.length; i++) {
var ch = s0.charAt(i);
if (ch0 == '?' || ch0 == 'r' || ch < 'a' || ch > 'z') {
asArgs[0] = s0.substr(i);
asArgs.unshift(s0.substr(0, i));
break;
}
}
}
return asArgs;
}
/**
* doCommand(sCmd, fQuiet)
*
* @this {DebuggerPDP11}
* @param {string} sCmd
* @param {boolean} [fQuiet]
* @return {boolean} true if command processed, false if unrecognized
*/
doCommand(sCmd, fQuiet)
{
var result = true;
try {
if (!sCmd.length || sCmd == "end") {
if (this.fAssemble) {
this.println("ended assemble at " + this.toStrAddr(this.dbgAddrAssemble));
this.dbgAddrNextCode = this.dbgAddrAssemble;
this.fAssemble = false;
}
sCmd = "";
}
else if (!fQuiet) {
this.println(DebuggerPDP11.PROMPT + sCmd);
}
var ch = sCmd.charAt(0);
if (ch == '"' || ch == "'") return true;
/*
* Zap the previous message buffer to ensure the new command's output is not tossed out as a repeat.
*/
this.sMessagePrev = null;
/*
* I've relaxed the !isBusy() requirement, to maximize our ability to issue Debugger commands externally.
*/
if (this.isReady() /* && !this.isBusy(true) */ && sCmd.length > 0) {
if (this.fAssemble) {
sCmd = "a " + this.toStrAddr(this.dbgAddrAssemble) + ' ' + sCmd;
}
var fError = false;
var asArgs = this.splitArgs(sCmd);
switch (asArgs[0].charAt(0)) {
case 'a':
this.doAssemble(asArgs);
break;
case 'b':
this.doBreak(asArgs[0], asArgs[1], sCmd);
break;
case 'c':
this.doClear(asArgs[0]);
break;
case 'd':
if (!COMPILED && sCmd == "debug") {
window.DEBUG = true;
this.println("DEBUG checks on");
break;
}
this.doDump(asArgs);
break;
case 'e':
if (asArgs[0] == "else") break;
this.doEdit(asArgs);
break;
case 'g':
this.doRun(asArgs[0], asArgs[1], sCmd, fQuiet);
break;
case 'h':
this.doHalt(fQuiet);
break;
case 'i':
if (asArgs[0] == "if") {
if (!this.doIf(sCmd.substr(2), fQuiet)) {
result = false;
}
break;
}
fError = true;
break;
case 'k':
this.doStackTrace(asArgs[0], asArgs[1]);
break;
case 'l':
if (asArgs[0] == "ln") {
this.doList(asArgs[1], true);
break;
}
fError = true;
break;
case 'm':
this.doMessages(asArgs);
break;
case 'p':
if (asArgs[0] == "print") {
this.doPrint(sCmd.substr(5));
break;
}
this.doStep(asArgs[0], asArgs[1]);
break;
case 'r':
if (sCmd == "reset") {
if (this.cmp) this.cmp.reset();
break;
}
this.doRegisters(asArgs);
break;
case 's':
this.doOptions(asArgs);
break;
case 't':
this.doTrace(asArgs[0], asArgs[1]);
break;
case 'u':
this.doUnassemble(asArgs[1], asArgs[2], 8);
break;
case 'v':
if (asArgs[0] == "var") {
if (!this.doVar(sCmd.substr(3))) {
result = false;
}
break;
}
if (asArgs[0] == "ver") {
this.println((PDP11.APPNAME || "PDP11") + " version " + (XMLVERSION || PDP11.APPVERSION) + " (" + this.cpu.model + (PDP11.COMPILED? ",RELEASE" : (PDP11.DEBUG? ",DEBUG" : ",NODEBUG")) + (PDP11.TYPEDARRAYS? ",TYPEDARRAYS" : (PDP11.BYTEARRAYS? ",BYTEARRAYS" : ",LONGARRAYS")) + ')');
this.println(Web.getUserAgent());
break;
}
fError = true;
break;
case '?':
if (asArgs[1]) {
this.doPrint(sCmd.substr(1));
break;
}
this.doHelp();
break;
case 'n':
if (!COMPILED && sCmd == "nodebug") {
window.DEBUG = false;
this.println("DEBUG checks off");
break;
}
if (this.doInfo(asArgs)) break;
/* falls through */
default:
fError = true;
break;
}
if (fError) {
this.println("unknown command: " + sCmd);
result = false;
}
}
} catch(e) {
this.println("debugger error: " + (e.stack || e.message));
result = false;
}
return result;
}
/**
* doCommands(sCmds, fSave)
*
* @this {DebuggerPDP11}
* @param {string} sCmds
* @param {boolean} [fSave]
* @return {boolean} true if all commands processed, false if not
*/
doCommands(sCmds, fSave)
{
var a = this.parseCommand(sCmds, fSave);
for (var s in a) {
if (!this.doCommand(a[+s])) return false;
}
return true;
}
/**
* DebuggerPDP11.init()
*
* This function operates on every HTML element of class "debugger", extracting the
* JSON-encoded parameters for the Debugger constructor from the element's "data-value"
* attribute, invoking the constructor to create a Debugger component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeDbg = Component.getElementsByClass(document, PDP11.APPCLASS, "debugger");
for (var iDbg = 0; iDbg < aeDbg.length; iDbg++) {
var eDbg = aeDbg[iDbg];
var parmsDbg = Component.getComponentParms(eDbg);
var dbg = new DebuggerPDP11(parmsDbg);
Component.bindComponentControls(dbg, eDbg, PDP11.APPCLASS);
}
}
}
if (DEBUGGER) {
/*
* NOTE: Every DebuggerPDP11 property from here to the first prototype function definition (initBus()) is
* considered a "class constant"; most of them use our "all-caps" convention (and all of them SHOULD, but
* that wouldn't help us catch any bugs).
*
* Technically, all of them should ALSO be preceded by a "@const" annotation, but that's a lot of work and it
* really clutters the code. I wish the Closure Compiler had a way to annotate every definition with a given
* section with a single annotation....
*/
DebuggerPDP11.COMMANDS = {
'?': "help/print",
'a [#]': "assemble", // TODO: Implement this command someday
'b [#]': "breakpoint", // multiple variations (use b? to list them)
'c': "clear output",
'd [#]': "dump memory", // additional syntax: d [#] [l#], where l# is a number of bytes to dump
'e [#]': "edit memory",
'g [#]': "go [to #]",
'h': "halt",
'if': "eval expression",
'int [#]': "request interrupt",
'k': "stack trace",
"ln": "list nearest symbol(s)",
'm': "messages",
'p': "step over", // other variations: pr (step and dump registers)
'print': "print expression",
'r': "dump/set registers",
'reset': "reset machine",
's': "set options",
't [#]': "trace", // other variations: tr (trace and dump registers)
'u [#]': "unassemble",
'var': "assign variable",
'ver': "print version"
};
/*
* CPU opcode IDs
*
* Not listed: BLO (same as BCS) and BHIS (same as BCC).
*/
DebuggerPDP11.OPS = {
NONE: 0, ADC: 1, ADCB: 2, ADD: 3, ASL: 4, ASLB: 5, ASR: 6, ASRB: 7,
BCC: 8, BCS: 9, BEQ: 10, BGE: 11, BGT: 12, BHI: 13, BIC: 14, BICB: 15,
BIS: 16, BISB: 17, BIT: 18, BITB: 19, BLE: 20, BLOS: 21, BLT: 22, BMI: 23,
BNE: 24, BPL: 25, BPT: 26, BR: 27, BVC: 28, BVS: 29, CCC: 30, CLC: 31,
CLCN: 32, CLCV: 33, CLCVN: 34, CLCVZ: 35, CLCZ: 36, CLCZN: 37, CLN: 38, CLR: 39,
CLRB: 40, CLV: 41, CLVN: 42, CLVZ: 43, CLVZN: 44, CLZ: 45, CLZN: 46, CMP: 47,
CMPB: 48, COM: 49, COMB: 50, DEC: 51, DECB: 52, INC: 53, INCB: 54, HALT: 55,
JMP: 56, JSR: 57, MARK: 58, MFPD: 59, MFPI: 60, MFPS: 61, MOV: 62, MOVB: 63,
MTPD: 64, MTPI: 65, MTPS: 66, NEG: 67, NEGB: 68, NOP: 69, RESET: 70, ROL: 71,
ROLB: 72, ROR: 73, RORB: 74, RTI: 75, RTS: 76, SBC: 77, SBCB: 78, SCC: 79,
SEC: 80, SECN: 81, SECV: 82, SECVN: 83, SECVZ: 84, SECZ: 85, SECZN: 86, SEN: 87,
SEV: 88, SEVN: 89, SEVZ: 90, SEVZN: 91, SEZ: 92, SEZN: 93, SUB: 94, SWAB: 95,
SXT: 96, TST: 97, TSTB: 98, WAIT: 99, MUL: 100, DIV: 101, ASH: 102, ASHC: 103,
XOR: 104, SOB: 105, EMT: 106, TRAP: 107, SPL: 108, IOT: 109, RTT: 110, MFPT: 111
};
/*
* CPU opcode names, indexed by CPU opcode ordinal (above)
*/
DebuggerPDP11.OPNAMES = [
".WORD", "ADC", "ADCB", "ADD", "ASL", "ASLB", "ASR", "ASRB",
"BCC", "BCS", "BEQ", "BGE", "BGT", "BHI", "BIC", "BICB",
"BIS", "BISB", "BIT", "BITB", "BLE", "BLOS", "BLT", "BMI",
"BNE", "BPL", "BPT", "BR", "BVC", "BVS", "CCC", "CLC",
"CLCN", "CLCV", "CLCVN", "CLCVZ", "CLCZ", "CLCZN", "CLN", "CLR",
"CLRB", "CLV", "CLVN", "CLVZ", "CLVZN", "CLZ", "CLZN", "CMP",
"CMPB", "COM", "COMB", "DEC", "DECB", "INC", "INCB", "HALT",
"JMP", "JSR", "MARK", "MFPD", "MFPI", "MFPS", "MOV", "MOVB",
"MTPD", "MTPI", "MTPS", "NEG", "NEGB", "NOP", "RESET", "ROL",
"ROLB", "ROR", "RORB", "RTI", "RTS", "SBC", "SBCB", "SCC",
"SEC", "SECN", "SECV", "SECVN", "SECVZ", "SECZ", "SECZN", "SEN",
"SEV", "SEVN", "SEVZ", "SEVZN", "SEZ", "SEZN", "SUB", "SWAB",
"SXT", "TST", "TSTB", "WAIT", "MUL", "DIV", "ASH", "ASHC",
"XOR", "SOB", "EMT", "TRAP", "SPL", "IOT", "RTT", "MFPT"
];
/*
* Register numbers 0-7 are reserved for cpu.regsGen, 8-15 are reserved for cpu.regsAlt, and 16-19 for cpu.regsStack.
*/
DebuggerPDP11.REG_PS = 20;
DebuggerPDP11.REG_PI = 21;
DebuggerPDP11.REG_ER = 22;
DebuggerPDP11.REG_SL = 23;
DebuggerPDP11.REG_M0 = 24;
DebuggerPDP11.REG_M1 = 25;
DebuggerPDP11.REG_M2 = 26;
DebuggerPDP11.REG_M3 = 27;
DebuggerPDP11.REG_AR = 28; // ADDRESS register; see Panel's getAR() and setAR()
DebuggerPDP11.REG_DR = 29; // DISPLAY/DATA register; see Panel's getDR() and setDR()
DebuggerPDP11.REG_SR = 30; // SWITCH register; see Panel's getSR() and setSR()
DebuggerPDP11.REGS = {
"SP": 6,
"PC": 7,
"PS": DebuggerPDP11.REG_PS,
"PI": DebuggerPDP11.REG_PI,
"ER": DebuggerPDP11.REG_ER,
"SL": DebuggerPDP11.REG_SL,
"M0": DebuggerPDP11.REG_M0,
"M1": DebuggerPDP11.REG_M1,
"M2": DebuggerPDP11.REG_M2,
"M3": DebuggerPDP11.REG_M3,
"AR": DebuggerPDP11.REG_AR,
"DR": DebuggerPDP11.REG_DR,
"SR": DebuggerPDP11.REG_SR
};
DebuggerPDP11.REGNAMES = [
"R0", "R1", "R2", "R3", "R4", "R5", "SP", "PC",
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7",
"S0", "S1", "S2", "S3",
"PS", "PI", "ER", "SL", "M0", "M1", "M2", "M3",
"AR", "DR", "SR"
];
DebuggerPDP11.MODES = ["KI","KD","SI","SD","??","??","UI","UD"];
/*
* Operand type masks; anything that's not covered by OP_SRC or OP_DST must be a OP_OTHER value.
*/
DebuggerPDP11.OP_DSTREG = PDP11.OPREG.MASK;
DebuggerPDP11.OP_DSTMODE = PDP11.OPMODE.MASK;
DebuggerPDP11.OP_DST = (DebuggerPDP11.OP_DSTMODE | DebuggerPDP11.OP_DSTREG);
DebuggerPDP11.OP_SRCREG = PDP11.OPREG.MASK << 6;
DebuggerPDP11.OP_SRCMODE = PDP11.OPMODE.MASK << 6;
DebuggerPDP11.OP_SRC = (DebuggerPDP11.OP_SRCMODE | DebuggerPDP11.OP_SRCREG);
DebuggerPDP11.OP_BRANCH = 0x1000;
DebuggerPDP11.OP_DSTOFF = 0x2000;
DebuggerPDP11.OP_DSTNUM3 = 0x3000; // DST 3-bit number (ie, just the DSTREG field)
DebuggerPDP11.OP_DSTNUM6 = 0x6000; // DST 6-bit number (ie, both the DSTREG and DSTMODE fields)
DebuggerPDP11.OP_DSTNUM8 = 0x8000; // DST 8-bit number
DebuggerPDP11.OP_OTHER = 0xF000;
/*
* The OPTABLE contains opcode masks, and each mask refers to table of possible values, and each
* value refers to an array that contains:
*
* [0]: {number} of the opcode name (see OP.*)
* [1]: {number} containing the first operand type bit(s), if any
* [2]: {number} containing the second operand type bit(s), if any
*
* Note that, by convention, opcodes that require two operands list the SRC operand first and DST operand
* second (ie, the OPPOSITE of the Intel convention).
*
* Also note that, for some of the newer PDP-11 opcodes (eg, MUL, DIV, ASH, ASHC), the location of the
* opcode's SRC and DST bits are reversed. This is why, for example, you'll see the MUL instruction defined
* below as having OP_DST for the first operand and OP_SRCREG for the second operand. This does NOT mean
* that the opcode's destination operand is being listed first, but rather that the bits describing the source
* operand are in the opcode's OP_DST field.
*/
DebuggerPDP11.OPTABLE = {
0xF000: {
0x1000: [DebuggerPDP11.OPS.MOV, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 01SSDD
0x2000: [DebuggerPDP11.OPS.CMP, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 02SSDD
0x3000: [DebuggerPDP11.OPS.BIT, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 03SSDD
0x4000: [DebuggerPDP11.OPS.BIC, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 04SSDD
0x5000: [DebuggerPDP11.OPS.BIS, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 05SSDD
0x6000: [DebuggerPDP11.OPS.ADD, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 06SSDD
0x9000: [DebuggerPDP11.OPS.MOVB, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 11SSDD
0xA000: [DebuggerPDP11.OPS.CMPB, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 12SSDD
0xB000: [DebuggerPDP11.OPS.BITB, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 13SSDD
0xC000: [DebuggerPDP11.OPS.BICB, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 14SSDD
0xD000: [DebuggerPDP11.OPS.BISB, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST], // 15SSDD
0xE000: [DebuggerPDP11.OPS.SUB, DebuggerPDP11.OP_SRC, DebuggerPDP11.OP_DST] // 16SSDD
},
0xFE00: {
0x0800: [DebuggerPDP11.OPS.JSR, DebuggerPDP11.OP_SRCREG, DebuggerPDP11.OP_DST], // 004RDD
0x7000: [DebuggerPDP11.OPS.MUL, DebuggerPDP11.OP_DST, DebuggerPDP11.OP_SRCREG], // 070RSS
0x7200: [DebuggerPDP11.OPS.DIV, DebuggerPDP11.OP_DST, DebuggerPDP11.OP_SRCREG], // 071RSS
0x7400: [DebuggerPDP11.OPS.ASH, DebuggerPDP11.OP_DST, DebuggerPDP11.OP_SRCREG], // 072RSS
0x7600: [DebuggerPDP11.OPS.ASHC, DebuggerPDP11.OP_DST, DebuggerPDP11.OP_SRCREG], // 073RSS
0x7800: [DebuggerPDP11.OPS.XOR, DebuggerPDP11.OP_SRCREG, DebuggerPDP11.OP_DST], // 074RDD
0x7E00: [DebuggerPDP11.OPS.SOB, DebuggerPDP11.OP_SRCREG, DebuggerPDP11.OP_DSTOFF] // 077Rnn
},
0xFF00: {
0x0100: [DebuggerPDP11.OPS.BR, DebuggerPDP11.OP_BRANCH],
0x0200: [DebuggerPDP11.OPS.BNE, DebuggerPDP11.OP_BRANCH],
0x0300: [DebuggerPDP11.OPS.BEQ, DebuggerPDP11.OP_BRANCH],
0x0400: [DebuggerPDP11.OPS.BGE, DebuggerPDP11.OP_BRANCH],
0x0500: [DebuggerPDP11.OPS.BLT, DebuggerPDP11.OP_BRANCH],
0x0600: [DebuggerPDP11.OPS.BGT, DebuggerPDP11.OP_BRANCH],
0x0700: [DebuggerPDP11.OPS.BLE, DebuggerPDP11.OP_BRANCH],
0x8000: [DebuggerPDP11.OPS.BPL, DebuggerPDP11.OP_BRANCH],
0x8100: [DebuggerPDP11.OPS.BMI, DebuggerPDP11.OP_BRANCH],
0x8200: [DebuggerPDP11.OPS.BHI, DebuggerPDP11.OP_BRANCH],
0x8300: [DebuggerPDP11.OPS.BLOS, DebuggerPDP11.OP_BRANCH],
0x8400: [DebuggerPDP11.OPS.BVC, DebuggerPDP11.OP_BRANCH],
0x8500: [DebuggerPDP11.OPS.BVS, DebuggerPDP11.OP_BRANCH],
0x8600: [DebuggerPDP11.OPS.BCC, DebuggerPDP11.OP_BRANCH],
0x8700: [DebuggerPDP11.OPS.BCS, DebuggerPDP11.OP_BRANCH],
0x8800: [DebuggerPDP11.OPS.EMT, DebuggerPDP11.OP_DSTNUM8], // 104000..104377
0x8900: [DebuggerPDP11.OPS.TRAP, DebuggerPDP11.OP_DSTNUM8] // 104400..104777
},
0xFFC0: {
0x0040: [DebuggerPDP11.OPS.JMP, DebuggerPDP11.OP_DST], // 0001DD
0x00C0: [DebuggerPDP11.OPS.SWAB, DebuggerPDP11.OP_DST], // 0003DD
0x0A00: [DebuggerPDP11.OPS.CLR, DebuggerPDP11.OP_DST], // 0050DD
0x0A40: [DebuggerPDP11.OPS.COM, DebuggerPDP11.OP_DST], // 0051DD
0x0A80: [DebuggerPDP11.OPS.INC, DebuggerPDP11.OP_DST], // 0052DD
0x0AC0: [DebuggerPDP11.OPS.DEC, DebuggerPDP11.OP_DST], // 0053DD
0x0B00: [DebuggerPDP11.OPS.NEG, DebuggerPDP11.OP_DST], // 0054DD
0x0B40: [DebuggerPDP11.OPS.ADC, DebuggerPDP11.OP_DST], // 0055DD
0x0B80: [DebuggerPDP11.OPS.SBC, DebuggerPDP11.OP_DST], // 0056DD
0x0BC0: [DebuggerPDP11.OPS.TST, DebuggerPDP11.OP_DST], // 0057DD
0x0C00: [DebuggerPDP11.OPS.ROR, DebuggerPDP11.OP_DST], // 0060DD
0x0C40: [DebuggerPDP11.OPS.ROL, DebuggerPDP11.OP_DST], // 0061DD
0x0C80: [DebuggerPDP11.OPS.ASR, DebuggerPDP11.OP_DST], // 0062DD
0x0CC0: [DebuggerPDP11.OPS.ASL, DebuggerPDP11.OP_DST], // 0063DD
0x0D00: [DebuggerPDP11.OPS.MARK, DebuggerPDP11.OP_DSTNUM6], // 0064nn
0x0D40: [DebuggerPDP11.OPS.MFPI, DebuggerPDP11.OP_DST], // 0065SS
0x0D80: [DebuggerPDP11.OPS.MTPI, DebuggerPDP11.OP_DST], // 0066DD
0x0DC0: [DebuggerPDP11.OPS.SXT, DebuggerPDP11.OP_DST], // 0067DD
0x8A00: [DebuggerPDP11.OPS.CLRB, DebuggerPDP11.OP_DST], // 1050DD
0x8A40: [DebuggerPDP11.OPS.COMB, DebuggerPDP11.OP_DST], // 1051DD
0x8A80: [DebuggerPDP11.OPS.INCB, DebuggerPDP11.OP_DST], // 1052DD
0x8AC0: [DebuggerPDP11.OPS.DECB, DebuggerPDP11.OP_DST], // 1053DD
0x8B00: [DebuggerPDP11.OPS.NEGB, DebuggerPDP11.OP_DST], // 1054DD
0x8B40: [DebuggerPDP11.OPS.ADCB, DebuggerPDP11.OP_DST], // 1055DD
0x8B80: [DebuggerPDP11.OPS.SBCB, DebuggerPDP11.OP_DST], // 1056DD
0x8BC0: [DebuggerPDP11.OPS.TSTB, DebuggerPDP11.OP_DST], // 1057DD
0x8C00: [DebuggerPDP11.OPS.RORB, DebuggerPDP11.OP_DST], // 1060DD
0x8C40: [DebuggerPDP11.OPS.ROLB, DebuggerPDP11.OP_DST], // 1061DD
0x8C80: [DebuggerPDP11.OPS.ASRB, DebuggerPDP11.OP_DST], // 1062DD
0x8CC0: [DebuggerPDP11.OPS.ASLB, DebuggerPDP11.OP_DST], // 1063DD
0x8D00: [DebuggerPDP11.OPS.MTPS, DebuggerPDP11.OP_DST], // 1064SS (only on LSI-11)
0x8D40: [DebuggerPDP11.OPS.MFPD, DebuggerPDP11.OP_DST], // 1065DD (same as MFPI if no separate instruction/data spaces)
0x8D80: [DebuggerPDP11.OPS.MTPD, DebuggerPDP11.OP_DST], // 1066DD (same as MTPI if no separate instruction/data spaces)
0x8DC0: [DebuggerPDP11.OPS.MFPS, DebuggerPDP11.OP_DST] // 1067SS (only on LSI-11)
},
0xFFF8: {
0x0080: [DebuggerPDP11.OPS.RTS, DebuggerPDP11.OP_DSTREG], // 00020R
0x0098: [DebuggerPDP11.OPS.SPL, DebuggerPDP11.OP_DSTNUM3] // 00023N
},
0xFFFF: {
0x0000: [DebuggerPDP11.OPS.HALT], // 000000
0x0001: [DebuggerPDP11.OPS.WAIT], // 000001
0x0002: [DebuggerPDP11.OPS.RTI], // 000002
0x0003: [DebuggerPDP11.OPS.BPT], // 000003
0x0004: [DebuggerPDP11.OPS.IOT], // 000004
0x0005: [DebuggerPDP11.OPS.RESET], // 000005
0x0006: [DebuggerPDP11.OPS.RTT], // 000006
0x0007: [DebuggerPDP11.OPS.MFPT], // 000007 (only on PDP-11/44 & KB11-EM?)
0x00A0: [DebuggerPDP11.OPS.NOP],
0x00A1: [DebuggerPDP11.OPS.CLC],
0x00A2: [DebuggerPDP11.OPS.CLV],
0x00A3: [DebuggerPDP11.OPS.CLCV],
0x00A4: [DebuggerPDP11.OPS.CLZ],
0x00A5: [DebuggerPDP11.OPS.CLCZ],
0x00A6: [DebuggerPDP11.OPS.CLVZ],
0x00A7: [DebuggerPDP11.OPS.CLCVZ],
0x00A8: [DebuggerPDP11.OPS.CLN],
0x00A9: [DebuggerPDP11.OPS.CLCN],
0x00AA: [DebuggerPDP11.OPS.CLVN],
0x00AB: [DebuggerPDP11.OPS.CLCVN],
0x00AC: [DebuggerPDP11.OPS.CLZN],
0x00AD: [DebuggerPDP11.OPS.CLCZN],
0x00AE: [DebuggerPDP11.OPS.CLVZN],
0x00AF: [DebuggerPDP11.OPS.CCC], // aka CLCVZN
0x00B0: [DebuggerPDP11.OPS.NOP],
0x00B1: [DebuggerPDP11.OPS.SEC],
0x00B2: [DebuggerPDP11.OPS.SEV],
0x00B3: [DebuggerPDP11.OPS.SECV],
0x00B4: [DebuggerPDP11.OPS.SEZ],
0x00B5: [DebuggerPDP11.OPS.SECZ],
0x00B6: [DebuggerPDP11.OPS.SEVZ],
0x00B7: [DebuggerPDP11.OPS.SECVZ],
0x00B8: [DebuggerPDP11.OPS.SEN],
0x00B9: [DebuggerPDP11.OPS.SECN],
0x00BA: [DebuggerPDP11.OPS.SEVN],
0x00BB: [DebuggerPDP11.OPS.SECVN],
0x00BC: [DebuggerPDP11.OPS.SEZN],
0x00BD: [DebuggerPDP11.OPS.SECZN],
0x00BE: [DebuggerPDP11.OPS.SEVZN],
0x00BF: [DebuggerPDP11.OPS.SCC] // aka SECVZN
}
};
DebuggerPDP11.OPNONE = [DebuggerPDP11.OPS.NONE];
/*
* Table of opcodes added to the 11/40 and newer
*/
DebuggerPDP11.OP1140 = [
DebuggerPDP11.OPS.MARK,
DebuggerPDP11.OPS.MFPI,
DebuggerPDP11.OPS.MTPI,
DebuggerPDP11.OPS.SXT,
DebuggerPDP11.OPS.RTT,
DebuggerPDP11.OPS.MUL,
DebuggerPDP11.OPS.DIV,
DebuggerPDP11.OPS.ASH,
DebuggerPDP11.OPS.ASHC,
DebuggerPDP11.OPS.XOR,
DebuggerPDP11.OPS.SOB
];
/*
* Table of opcodes added to the 11/45 and newer
*/
DebuggerPDP11.OP1145 = [
DebuggerPDP11.OPS.SPL,
DebuggerPDP11.OPS.MFPD,
DebuggerPDP11.OPS.MTPD
];
DebuggerPDP11.HISTORY_LIMIT = DEBUG? 100000 : 1000;
DebuggerPDP11.PROMPT = ">> ";
/*
* Initialize every Debugger module on the page (as IF there's ever going to be more than one ;-))
*/
Web.onInit(DebuggerPDP11.init);
} // endif DEBUGGER
/**
* @copyright http://pcjs.org/modules/pdp11/lib/computer.js (C) Jeff Parsons 2012-2017
*/
class ComputerPDP11 extends Component {
/**
* ComputerPDP11(parmsComputer, parmsMachine, fSuspended)
*
* The ComputerPDP11 component has no required (parmsComputer) properties, but it does
* support the following:
*
* autoPower: true to automatically power the computer (default), false to wait;
* false is honored only if a "power" button binding exists.
*
* busWidth: number of memory address lines (address bits) on the computer's "bus";
* 20 is the minimum (and the default), which implies 8086/8088 real-mode addressing,
* while 24 is required for 80286 protected-mode addressing. This value is passed
* directly through to the Bus component; see that component for more details.
*
* resume: one of the ComputerPDP11.RESUME constants, which are as follows:
* '0' if resume disabled (default)
* '1' if enabled without prompting
* '2' if enabled with prompting
* '3' if enabled with prompting and auto-delete
* or a string containing the path of a predefined JSON-encoded state
*
* state: the path to JSON-encoded state file (see details regarding 'state' below)
*
* The parmsMachine object, if provided, may contain any of:
*
* autoMount: if set, this should override any 'autoMount' property in the FDC's
* parmsFDC object.
*
* autoPower: if set, this should override any 'autoPower' property in the ComputerPDP11's
* parmsComputer object.
*
* messages: if set, this should override any 'messages' property in the Debugger's
* parmsDbg object.
*
* state: if set, this should override any 'state' property in the ComputerPDP11's
* parmsComputer object.
*
* url: the location of the machine XML file
*
* If a predefined state is supplied AND it's successfully loaded, then resume behavior
* defaults to '1' (ie, resume enabled without prompting).
*
* This component insures that all components are ready before "powering" them.
*
* Different components become ready at different times, and initialization order (ie,
* the order the scripts are combined on the page) only partially determines readiness.
* This is because components like ROM and Video must finish loading their resource files
* before they are ready. Other components become ready after we call their initBus()
* function, because they have a Bus or CPU dependency, such as access to memory management
* functions. And other components, like CPU and Panel, are ready as soon as their
* constructor finishes.
*
* Once a component has indicated it's ready, we call its powerUp() notification
* function (if it has one--it's optional). We call the CPU's powerUp() function last,
* so that the CPU is assured that all other components are ready and "powered".
*
* @this {ComputerPDP11}
* @param {Object} parmsComputer
* @param {Object} [parmsMachine]
* @param {boolean} [fSuspended]
*/
constructor(parmsComputer, parmsMachine, fSuspended)
{
super("Computer", parmsComputer, MessagesPDP11.COMPUTER);
this.flags.powered = false;
this.parmsMachine = null;
this.setMachineParms(parmsMachine);
this.fAutoPower = this.getMachineParm('autoPower', parmsComputer, Str.TYPES.BOOLEAN);
/*
* nPowerChange is 0 while the power state is stable, 1 while power is transitioning
* to "on", and -1 while power is transitioning to "off".
*/
this.nPowerChange = 0;
/*
* TODO: Deprecate 'buswidth' (it should have always used camelCase)
*/
this.nBusWidth = +parmsComputer['busWidth'] || +parmsComputer['buswidth'];
this.sResumePath = this.sStatePath = null;
this.sStateData = null;
this.fStateData = false; // remembers if sStateData was loaded
this.fServerState = false;
this.stateComputer = this.stateFailSafe = null;
this.fInitialized = this.fReload = this.fRestoreError = false;
this.url = /** @type {string} */ (this.getMachineParm('url') || "");
/*
* Generate a random number x (where 0 <= x < 1), add 0.1 so that it's guaranteed to be
* non-zero, convert to base 36, and chop off the leading digit and "decimal" point.
*/
this.sMachineID = (Math.random() + 0.1).toString(36).substr(2,12);
this.sUserID = this.queryUserID();
/*
* Find the appropriate CPU (and Debugger and Control Panel, if any).
*
* CLOSURE COMPILER TIP: To override the type of a right-hand expression (as we need to do here,
* where we know getComponentByType() will only return an CPUState object or null), wrap the expression
* in parentheses. I never knew this until I stumbled across it in "Closure: The Definitive Guide".
*/
this.cpu = /** @type {CPUStatePDP11} */ (Component.getComponentByType("CPU", this.id));
if (!this.cpu) {
Component.error("Unable to find CPU component");
return;
}
this.dbg = /** @type {DebuggerPDP11} */ (Component.getComponentByType("Debugger", this.id));
/*
* Initialize the Bus component
*/
this.bus = new BusPDP11({'id': this.idMachine + '.bus', 'busWidth': this.nBusWidth}, this.cpu, this.dbg);
/*
* Iterate through all the components and connect them to the Control Panel, if any
*/
var iComponent, component;
var aComponents = Component.getComponents(this.id);
this.panel = /** @type {PanelPDP11} */ (Component.getComponentByType("Panel", this.id));
this.controlPrint = this.panel && this.panel.bindings['print'];
if (this.controlPrint) {
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
component = aComponents[iComponent];
/*
* I can think of many "cleaner" ways for the Control Panel component to pass its
* notice(), println(), etc, overrides on to all the other components, but it's just
* too darn convenient to slam those overrides into the components directly.
*/
component.notice = this.panel.notice;
component.print = this.panel.print;
component.println = this.panel.println;
}
}
this.println(PDP11.APPNAME + " v" + PDP11.APPVERSION + "\n" + COPYRIGHT + "\n" + LICENSE);
this.println("Portions adapted from the PDP-11/70 Emulator by Paul Nankervis <http://skn.noip.me/pdp11/pdp11.html>");
if (DEBUG && this.messageEnabled()) this.printMessage("TYPEDARRAYS: " + TYPEDARRAYS);
/*
* Iterate through all the components again and call their initBus() handler, if any
*/
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
component = aComponents[iComponent];
if (component.initBus) component.initBus(this, this.bus, this.cpu, this.dbg);
}
var sStatePath = null;
var sResume = /** @type {string} */ (this.getMachineParm('resume', parmsComputer));
if (sResume !== undefined) {
/*
* Decide whether the 'resume' property is a number or the path of a state file to resume.
*/
if (sResume.length > 1) {
sStatePath = this.sResumePath = sResume;
} else {
this.resume = parseInt(sResume, 10);
}
}
/*
* The Computer 'state' property allows a state file to be specified independent of the 'resume' feature;
* previously, you could only use 'resume' to load a state file -- which we still support, but loading a state
* file that way prevents the machine's state from being saved, since we always resume from the 'resume' file.
*
* The other wrinkle is on the restore side: we need to IGNORE the 'state' property if a saved state now exists.
* So we have to peek at localStorage, and unfortunately, the only way to "peek" is to actually load the data,
* but we're not ready to use it yet, so powerUp() has been changed to use any existing stateComputer that we've
* already loaded.
*
* However, there's now a wrinkle to the wrinkle: if a 'state' parameter has been passed via the URL, then that
* OVERRIDES everything; it overrides any 'state' Computer parameter AND it disables resume of any saved state in
* localStorage (in other words, it prevents fAllowResume from being true, and forcing resume off).
*/
var fAllowResume;
var sState = this.getMachineParm('state') || (fAllowResume = true) && parmsComputer['state'];
if (sState) {
this.sStatePath = sStatePath = sState;
if (!fAllowResume) {
this.fServerState = true;
this.resume = ComputerPDP11.RESUME_NONE;
}
if (this.resume) {
this.stateComputer = new State(this, PDP11.APPVERSION);
if (this.stateComputer.load()) {
sStatePath = null;
} else {
delete this.stateComputer;
}
}
}
/*
* If sStatePath is set, we must use it. But if there's no sStatePath AND resume is set,
* then we have the option of resuming from a server-side state, assuming a valid USERID.
*/
if (!sStatePath && this.resume) {
sStatePath = this.getServerStatePath();
if (sStatePath) this.fServerState = true;
}
if (!sStatePath) {
this.setReady();
} else {
var cmp = this;
Web.getResource(sStatePath, null, true, function doneStateLoad(sURL, sResource, nErrorCode) {
cmp.finishStateLoad(sURL, sResource, nErrorCode);
});
}
if (!this.bindings["power"]) this.fAutoPower = true;
/*
* Power on the computer, giving every component the opportunity to reset or restore itself.
*/
if (!fSuspended && this.fAutoPower) this.wait(this.powerOn);
}
/**
* clearPanel()
*
* @this {ComputerPDP11}
*/
clearPanel()
{
if (this.controlPrint) {
this.controlPrint.value = "";
}
}
/**
* getMachineID()
*
* @this {ComputerPDP11}
* @return {string}
*/
getMachineID()
{
return this.sMachineID;
}
/**
* setMachineParms(parmsMachine)
*
* If no explicit machine parms were provided, then we check for 'parms' in the bundled resources (if any).
*
* @this {ComputerPDP11}
* @param {Object} [parmsMachine]
*/
setMachineParms(parmsMachine)
{
if (!parmsMachine) {
var sParms;
if (typeof resources == 'object' && (sParms = resources['parms'])) {
try {
parmsMachine = /** @type {Object} */ (eval("(" + sParms + ")"));
} catch(e) {
Component.error(e.message + " (" + sParms + ")");
}
}
}
this.parmsMachine = parmsMachine;
}
/**
* getMachineParm(sParm, parmsComponent, type, defaultValue)
*
* If the machine parameter doesn't exist, we check for a matching component parameter
* (if parmsComponent is provided), and failing that, we check the bundled resources (if any).
*
* At the moment, the only bundled resource request we expect to encounter is 'state'; if it exists,
* then we return 'state' back to the caller (ie, the name of the resource), so that the caller will
* then attempt to load the 'state' resource to obtain the actual state.
*
* TODO: It would be nice if we could tell the Closure Compiler that when a specific type parameter
* (eg, Str.TYPES.NUMBER) is used, the return value will be that type; unfortunately, every caller
* must coerce their own return value.
*
* @this {ComputerPDP11}
* @param {string} sParm
* @param {Object|null} [parmsComponent]
* @param {number} [type] (from Str.TYPES)
* @param {*} [defaultValue]
* @return {*}
*/
getMachineParm(sParm, parmsComponent, type, defaultValue)
{
/*
* When checking parmsURL, the check is allowed be a bit looser, because URL parameters are
* user-supplied, whereas most other parameters are developer-supplied. Granted, a developer
* may also be sloppy and neglect to use correct case (eg, 'automount' instead of 'autoMount'),
* but there are limits to my paranoia.
*/
var sParmLC = sParm.toLowerCase();
var value = Web.getURLParm(sParm) || Web.getURLParm(sParmLC);
if (value === undefined && this.parmsMachine) value = this.parmsMachine[sParm];
if (value === undefined && parmsComponent) value = parmsComponent[sParm];
if (value === undefined && typeof resources == 'object' && resources[sParm]) value = sParm;
if (value === undefined) value = defaultValue;
if (typeof value == "string" && type) {
switch(type) {
case Str.TYPES.NUMBER:
value = +value;
if (isNaN(/** @type {number} */(value))) value = defaultValue || 0;
break;
case Str.TYPES.BOOLEAN:
value = (value == "true");
break;
}
}
return value;
}
/**
* saveMachineParms()
*
* @this {ComputerPDP11}
* @return {string|null}
*/
saveMachineParms()
{
return this.parmsMachine? JSON.stringify(this.parmsMachine) : null;
}
/**
* getUserID()
*
* @this {ComputerPDP11}
* @return {string}
*/
getUserID()
{
return this.sUserID || "";
}
/**
* finishStateLoad(sURL, sStateData, nErrorCode)
*
* @this {ComputerPDP11}
* @param {string} sURL
* @param {string} sStateData
* @param {number} nErrorCode
*/
finishStateLoad(sURL, sStateData, nErrorCode)
{
if (!nErrorCode) {
this.sStateData = sStateData;
this.fStateData = true;
if (DEBUG && this.messageEnabled()) {
this.printMessage("loaded state file " + sURL.replace(this.sUserID || "xxx", "xxx"));
}
} else {
this.sResumePath = null;
this.fServerState = false;
this.notice('Unable to load machine state from server (error ' + nErrorCode + (sStateData? ': ' + Str.trim(sStateData) : '') + ')');
}
this.setReady();
}
/**
* wait(fn, parms)
*
* wait() waits until every component is ready (including ourselves, the last component we check), then calls the
* specified Computer method.
*
* TODO: The Closure Compiler makes it difficult for us to define a function type for "fn" that works in all cases;
* sometimes we want to pass a function that takes only a "number", and other times we want to pass a function that
* takes only an "Array" (the type will mirror that of the "parms" parameter). However, the Closure Compiler insists
* that both functions must be declared as accepting both types of parameters. So once again, we must use an untyped
* function declaration, instead of something stricter like:
*
* param {function(this:Computer, (number|Array|undefined)): undefined} fn
*
* @this {ComputerPDP11}
* @param {function(...)} fn
* @param {number|Array} [parms] optional parameters
*/
wait(fn, parms)
{
var computer = this;
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent <= aComponents.length; iComponent++) {
var component = (iComponent < aComponents.length ? aComponents[iComponent] : this);
if (!component.isReady()) {
component.isReady(function onComponentReady() {
computer.wait(fn, parms);
});
return;
}
}
if (DEBUG && this.messageEnabled()) this.printMessage("ComputerPDP11.wait(ready)");
fn.call(this, parms);
}
/**
* validateState(stateComputer)
*
* NOTE: We clear() stateValidate only when there's no stateComputer.
*
* @this {ComputerPDP11}
* @param {State|null} [stateComputer]
* @return {boolean} true if state passes validation, false if not
*/
validateState(stateComputer)
{
var fValid = true;
var stateValidate = new State(this, PDP11.APPVERSION, ComputerPDP11.STATE_VALIDATE);
if (stateValidate.load() && stateValidate.parse()) {
var sTimestampValidate = stateValidate.get(ComputerPDP11.STATE_TIMESTAMP);
var sTimestampComputer = stateComputer? stateComputer.get(ComputerPDP11.STATE_TIMESTAMP) : "unknown";
if (sTimestampValidate != sTimestampComputer) {
this.notice("Machine state may be out-of-date\n(" + sTimestampValidate + " vs. " + sTimestampComputer + ")\nCheck your browser's local storage limits");
fValid = false;
if (!stateComputer) stateValidate.clear();
} else {
if (DEBUG && this.messageEnabled()) {
this.printMessage("Last state: " + sTimestampComputer + " (validate: " + sTimestampValidate + ")");
}
}
}
return fValid;
}
/**
* powerOn(resume)
*
* Power every component "up", applying any previously available state information.
*
* @this {ComputerPDP11}
* @param {number} [resume] is a valid RESUME value; default is this.resume
*/
powerOn(resume)
{
if (resume === undefined) {
resume = this.resume || (this.sStateData? ComputerPDP11.RESUME_AUTO : ComputerPDP11.RESUME_NONE);
}
if (DEBUG && this.messageEnabled()) {
this.printMessage("ComputerPDP11.powerOn(" + (resume == ComputerPDP11.RESUME_REPOWER ? "repower" : (resume ? "resume" : "")) + ")");
}
if (this.nPowerChange) {
return;
}
this.nPowerChange++;
var fRepower = false;
var fRestore = false;
this.fRestoreError = false;
var stateComputer = this.stateComputer || new State(this, PDP11.APPVERSION);
if (resume == ComputerPDP11.RESUME_REPOWER) {
fRepower = true;
}
else if (resume > ComputerPDP11.RESUME_NONE) {
if (stateComputer.load(this.sStateData)) {
/*
* Since we're resuming something (either a predefined state or a state from localStorage), let's
* create a "failsafe" checkpoint in localStorage, and destroy it at the end of a successful powerOn().
* Which means, of course, that if a previous "failsafe" checkpoint already exists, something bad
* may have happened the last time around.
*/
this.stateFailSafe = new State(this, PDP11.APPVERSION, ComputerPDP11.STATE_FAILSAFE);
if (this.stateFailSafe.load()) {
this.powerReport(stateComputer);
/*
* We already know resume is something other than RESUME_NONE, so we'll go ahead and bump it
* all the way to RESUME_PROMPT, so that the user will be prompted, and if the user declines to
* restore, the state will be removed.
*/
resume = ComputerPDP11.RESUME_PROMPT;
/*
* To ensure that the set() below succeeds, we need to call unload(), otherwise it may fail
* with a "read only" error (eg, "TypeError: Cannot assign to read only property 'timestamp'").
*/
this.stateFailSafe.unload();
}
this.stateFailSafe.set(ComputerPDP11.STATE_TIMESTAMP, Usr.getTimestamp());
this.stateFailSafe.store();
var fValidate = this.resume && !this.fServerState;
if (resume == ComputerPDP11.RESUME_AUTO || Component.confirmUser("Click OK to restore the previous " + PDP11.APPNAME + " machine state, or CANCEL to reset the machine.")) {
fRestore = stateComputer.parse();
if (fRestore) {
var sCode = /** @type {string} */ (stateComputer.get(UserAPI.RES.CODE));
var sData = /** @type {string} */ (stateComputer.get(UserAPI.RES.DATA));
if (sCode) {
if (sCode == UserAPI.CODE.OK) {
stateComputer.load(sData);
} else {
/*
* A missing (or not yet created) state file is no cause for alarm, but other errors might be
*/
if (sCode == UserAPI.CODE.FAIL && sData != UserAPI.FAIL.NOSTATE) {
this.notice("Error: " + sData);
if (sData == UserAPI.FAIL.VERIFY) this.resetUserID();
} else {
this.println(sCode + ": " + sData);
}
/*
* Try falling back to the state that we should have saved in localStorage, as a backup to the
* server-side state.
*/
stateComputer.unload(); // discard the invalid server-side state first
if (stateComputer.load()) {
fRestore = stateComputer.parse();
fValidate = true;
} else {
fRestore = false; // hmmm, there was nothing in localStorage either
}
}
}
}
/*
* If the load/parse was successful, and it was from localStorage (not sStateData),
* then we should to try verify that localStorage snapshot is current. One reason it may
* NOT be current is if localStorage was full and we got a quota error during the last
* powerOff().
*/
if (fValidate) this.validateState(fRestore? stateComputer : null);
} else {
/*
* RESUME_PROMPT indicates we should delete the state if they clicked Cancel to confirm() above.
*/
if (resume == ComputerPDP11.RESUME_PROMPT) stateComputer.clear();
}
} else {
/*
* If there's no state, then there should also be no validation timestamp; if there is, then once again,
* we're probably dealing with a quota error.
*/
this.validateState();
}
delete this.sStateData;
delete this.stateComputer;
}
/*
* Start powering all components, including any data they may need to restore their state;
* we restore power to the CPU last.
*/
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component !== this && component != this.cpu) {
fRestore = this.powerRestore(component, stateComputer, fRepower, fRestore);
}
}
/*
* Assuming this is not a repower, we must perform another wait, because some components may
* have marked themselves as "not ready" again (eg, the FDC component, if the restore forced it
* to mount one or more additional disk images).
*/
var aParms = [stateComputer, resume, fRestore];
if (resume != ComputerPDP11.RESUME_REPOWER) {
this.wait(this.donePowerOn, aParms);
return;
}
this.donePowerOn(aParms);
}
/**
* powerRestore(component, stateComputer, fRepower, fRestore)
*
* @this {ComputerPDP11}
* @param {Component} component
* @param {State} stateComputer
* @param {boolean} fRepower
* @param {boolean} fRestore
* @return {boolean} true if restore should continue, false if not
*/
powerRestore(component, stateComputer, fRepower, fRestore)
{
if (!component.flags.powered) {
/*
* TODO: If all components called super.powerUp(), the powered flag would be set automatically.
*/
component.flags.powered = true;
var data = null;
try {
if (fRestore) {
data = stateComputer.get(component.id);
if (!data) {
/*
* This is a hack that makes it possible for a machine whose ID has been
* supplemented with a hyphenated numeric suffix to find object IDs in states
* created from a machine without such a suffix.
*
* For example, if a state file was created from a machine with ID "ibm5160"
* but the current machine is "ibm5160-1", this attempts a second lookup with
* "ibm5160", enabling us to find objects that match the original machine ID
* (eg, "ibm5160.romEGA").
*
* See /devices/pcx86/machine/5160/ega/640kb/array/ for examples of this.
*/
data = stateComputer.get(component.id.replace(/-[0-9]+\./i, '.'));
}
}
/*
* State.get() will return whatever was originally passed to State.set() (eg, an
* Object or a string), but components are supposed to store only Objects, so if a
* string comes back, something went wrong. By explicitly eliminating "string" data,
* the Closure Compiler stops complaining that we might be passing strings to our
* powerUp() functions (even though we know we're not).
*
* TODO: Determine if there's some way to coerce the Closure Compiler into treating
* data as Object or null, without having to include this runtime check. An assert
* would be a good idea, but this is overkill.
*/
if (typeof data === "string") data = null;
/*
* If computer is null, this is simply a repower notification, which most components
* don't do anything with. Exceptions include: CPU (since it may be halted) and Video
* (since its screen may be "turned off").
*/
if (!component.powerUp(data, fRepower) && data) {
Component.error("Unable to restore state for " + component.type);
/*
* If this is a resume error for a machine that also has a predefined state
* AND we're not restoring from that state, then throw away the current state,
* prevent any new state from being created, and then force a reload, which will
* hopefully restore us to the functioning predefined state.
*
* TODO: Considering doing this in ALL cases, not just in situations where a
* 'state' exists but we're not actually resuming from it.
*/
if (this.sStatePath && !this.fStateData) {
stateComputer.clear();
this.resume = ComputerPDP11.RESUME_NONE;
Web.reloadPage();
} else {
/*
* In all other cases, we set fRestoreError, which should trigger a call to
* powerReport() and then delete the offending state.
*/
this.fRestoreError = true;
}
/*
* Any failure triggers an automatic to call powerUp() again, without any state,
* in the hopes that the component can recover by performing a reset.
*/
component.powerUp(null);
/*
* We also disable the rest of the restore operation, because it's not clear
* the remaining state information can be trusted; the machine is already in an
* inconsistent state, so we're not likely to make things worse, and the only
* alternative (starting over and performing a state-less reset) isn't likely to make
* the user any happier. But, we'll see... we need some experience with the code.
*/
fRestore = false;
}
if (!fRepower && component.comment) {
var asComments = component.comment.split("|");
for (var i = 0; i < asComments.length; i++) {
component.status(asComments[i]);
}
}
}
catch (err) {
Component.error("Error restoring state for " + component.type + " (" + err.message + ")");
}
}
return fRestore;
}
/**
* donePowerOn(aParms)
*
* This is nothing more than a continuation of powerOn(), giving us the option of calling wait() one more time.
*
* @this {ComputerPDP11}
* @param {Array} aParms containing [stateComputer, resume, fRestore]
*/
donePowerOn(aParms)
{
var stateComputer = aParms[0];
var fRepower = (aParms[1] < 0);
var fRestore = aParms[2];
if (DEBUG && this.flags.powered && this.messageEnabled()) {
this.printMessage("ComputerPDP11.donePowerOn(): redundant");
}
this.fInitialized = true;
this.flags.powered = true;
var controlPower = this.bindings["power"];
if (controlPower) controlPower.textContent = "Shutdown";
/*
* Once we get to this point, we're guaranteed that all components are ready, so it's safe to power the CPU;
* the CPU should begin executing immediately, unless a debugger is attached.
*/
if (this.cpu) {
/*
* TODO: Do we not care about the return value here? (ie, is checking fRestoreError sufficient)?
*/
this.powerRestore(this.cpu, stateComputer, fRepower, fRestore);
this.updateDisplays(-2);
this.cpu.autoStart();
}
/*
* If the state was bad, offer to report it and then delete it. Deleting may be moot, since invariably a new
* state will be created on powerOff() before the next powerOn(), but it seems like good paranoia all the same.
*/
if (this.fRestoreError) {
this.powerReport(stateComputer);
stateComputer.clear();
}
if (!fRepower && this.stateFailSafe) {
this.stateFailSafe.clear();
delete this.stateFailSafe;
}
this.nPowerChange = 0;
}
/**
* checkPower()
*
* @this {ComputerPDP11}
* @return {boolean} true if the computer is fully powered, false otherwise
*/
checkPower()
{
if (this.flags.powered) return true;
var component = null, iComponent;
var aComponents = Component.getComponents(this.id);
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
component = aComponents[iComponent];
if (component !== this && !component.flags.ready) break;
}
if (iComponent == aComponents.length) {
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
component = aComponents[iComponent];
if (component !== this && !component.flags.powered) break;
}
}
if (iComponent == aComponents.length) component = this;
var s = "The " + component.type + " component (" + component.id + ") is not " + (!component.flags.ready? "ready yet" + (component.fnReady? " (waiting for notification)" : "") : "powered yet") + ".";
Component.alertUser(s);
return false;
}
/**
* powerReport(stateComputer)
*
* @this {ComputerPDP11}
* @param {State} stateComputer
*/
powerReport(stateComputer)
{
if (Component.confirmUser("There may be a problem with your " + PDP11.APPNAME + " machine.\n\nTo help us diagnose it, click OK to send this " + PDP11.APPNAME + " machine state to http://" + SITEHOST + ".")) {
Web.sendReport(PDP11.APPNAME, PDP11.APPVERSION, this.url, this.getUserID(), ReportAPI.TYPE.BUG, stateComputer.toString());
}
}
/**
* powerOff(fSave, fShutdown)
*
* Power every component "down" and optionally save the machine state.
*
* There's one scenario that powerOff() isn't currently able to deal with very effectively: what to do when
* the user switches away while it's still being restored, causing Disk getResource() calls to fail. The
* Disk component calls notify() when that happens -- see Disk.mount() -- but the FDC and HDC controllers don't
* notify *us* of those problems, so Computer assumes that the restore was completely successful, when in fact
* it was only partially successful.
*
* Then we immediately arrive here to perform a save, following that incomplete restore. It would be wrong to
* deal with that incomplete restore by setting fRestoreError, because we don't want to trigger a powerReport()
* and the deletion of the previous state, because the state itself was presumably OK. Unfortunately, the new
* state we now save will no longer include manually mounted disk images whose remounts were interrupted, so future
* restores won't remount them either.
*
* We could perhaps solve this by having the Disk component notify us in those situations, set a new flag
* (fRestoreIncomplete?), and set fSave to false if that's ever set. Be careful though: when fSave is false,
* that means MORE than not saving; it also means deleting any previous state, which is NOT what you'd want to
* do in a "fRestoreIncomplete" situation. Also, we have to worry about Disk operations that fail for other reasons,
* making sure those failures don't interfere with the save process in the same way.
*
* As it stands, the worst that happens is any manually mounted disk images might have to be manually remounted,
* which doesn't seem like a huge problem.
*
* @this {ComputerPDP11}
* @param {boolean} [fSave] is true to request a saved state
* @param {boolean} [fShutdown] is true if the machine is being shut down
* @return {string|null} string representing the saved state (or null if error)
*/
powerOff(fSave, fShutdown)
{
var data;
var sState = "none";
if (DEBUG && this.messageEnabled()) {
this.printMessage("ComputerPDP11.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")");
}
if (this.nPowerChange) {
return null;
}
this.nPowerChange--;
var stateComputer = new State(this, PDP11.APPVERSION);
var stateValidate = new State(this, PDP11.APPVERSION, ComputerPDP11.STATE_VALIDATE);
var sTimestamp = Usr.getTimestamp();
stateValidate.set(ComputerPDP11.STATE_TIMESTAMP, sTimestamp);
stateComputer.set(ComputerPDP11.STATE_TIMESTAMP, sTimestamp);
stateComputer.set(ComputerPDP11.STATE_VERSION, APPVERSION);
stateComputer.set(ComputerPDP11.STATE_HOSTURL, Web.getHostURL());
stateComputer.set(ComputerPDP11.STATE_BROWSER, Web.getUserAgent());
/*
* Always power the CPU "down" first, just to help insure it doesn't ask other components to do anything
* after they're no longer ready.
*/
if (this.cpu && this.cpu.powerDown) {
if (fShutdown) {
if (fSave) this.cpu.flags.autoStart = this.cpu.flags.running;
this.cpu.stopCPU();
}
data = this.cpu.powerDown(fSave, fShutdown);
if (typeof data === "object") stateComputer.set(this.cpu.id, data);
if (fShutdown) {
this.cpu.flags.powered = false;
if (data === false) sState = null;
}
}
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component.flags.powered) {
if (component.powerDown) {
data = component.powerDown(fSave, fShutdown);
if (typeof data === "object") stateComputer.set(component.id, data);
}
if (fShutdown) {
component.flags.powered = false;
if (data === false) sState = null;
}
}
}
if (sState) {
if (fShutdown) {
var fClear = false;
var fClearAll = false;
if (fSave) {
if (this.sUserID) {
this.saveServerState(this.sUserID, stateComputer.toString());
}
if (!stateValidate.store() || !stateComputer.store()) {
sState = null;
/*
* New behavior as of v1.13.2: if it appears that localStorage is full, we blow it ALL away.
* Dedicated server-side storage is the only way we'll ever be able to reliably preserve a
* particular machine's state. Historically, attempting to limp along with whatever localStorage
* is left just generates the same useless and annoying warnings over and over.
*/
fClear = fClearAll = true;
}
}
else {
/*
* I used to ALWAYS clear (ie, delete) any associated computer state, but now I do this only if the
* current machine is "resumable", because there are situations where I have two configurations
* for the same machine -- one resumable and one not -- and I don't want the latter throwing away the
* state of the former.
*
* So this code is here now strictly for callers to delete the state of a "resumable" machine, not as
* some paranoid clean-up operation.
*
* An undocumented feature of this operation is that if your configuration uses the special 'resume="3"'
* value, and you click the "Reset" button, and then you click OK to reset the everything, this will
* actually reset EVERYTHING (ie, all localStorage for ALL configs will be reclaimed).
*/
if (this.resume) {
fClear = true;
fClearAll = (this.resume == ComputerPDP11.RESUME_DELETE);
}
}
if (fClear) {
stateComputer.clear(fClearAll);
}
} else {
sState = stateComputer.toString();
}
}
if (fShutdown) {
this.flags.powered = false;
var controlPower = this.bindings["power"];
if (controlPower) controlPower.textContent = "Power";
}
this.nPowerChange = 0;
return sState;
}
/**
* reset()
*
* Notify all (other) components with a reset() method that the Computer is being reset.
*
* NOTE: We'd like to reset the Bus first (due to the importance of the A20 line), but since we
* allocated the Bus object ourselves, after all the other components were allocated, it ends
* up near the end of Component's list of components. Hence the special case for this.bus below.
*
* Ditto for the CPU, in part because if the Front Panel resets before the CPU, it will end up
* snapping/displaying the PC as of the last instruction executed, before the CPU resets the PC,
* causing the Front Panel to display a stale address when we call updateDisplays() at the end.
*
* @this {ComputerPDP11}
*/
reset()
{
this.flags.reset = true;
if (this.bus && this.bus.reset) {
this.printMessage("Resetting " + this.bus.type);
this.bus.reset();
}
if (this.cpu && this.cpu.reset) {
this.printMessage("Resetting " + this.cpu.type);
this.cpu.reset();
}
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component !== this && component !== this.bus && component !== this.cpu && component.reset) {
this.printMessage("Resetting " + component.type);
component.reset();
}
}
this.flags.reset = false;
this.updateDisplays(-1);
}
/**
* start(ms, nCycles)
*
* Notify all (other) components with a start() method that the CPU has started.
*
* Note that we're called by startCPU(), which is why we exclude the CPU component,
* as well as ourselves.
*
* @this {ComputerPDP11}
* @param {number} ms
* @param {number} nCycles
*/
start(ms, nCycles)
{
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component.type == "CPU" || component === this) continue;
if (component.start) {
component.start(ms, nCycles);
}
}
this.updateDisplays(-1);
}
/**
* stop(ms, nCycles)
*
* Notify all (other) components with a stop() method that the CPU has stopped.
*
* Note that we're called by stopCPU(), which is why we exclude the CPU component,
* as well as ourselves.
*
* @this {ComputerPDP11}
* @param {number} ms
* @param {number} nCycles
*/
stop(ms, nCycles)
{
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (component.type == "CPU" || component === this) continue;
if (component.stop) {
component.stop(ms, nCycles);
}
}
this.updateDisplays(-1);
}
/**
* updateDisplays(nUpdate)
*
* TODO: Notify all components with an updateDisplay() method that the computer's state has changed (not
* just the hard-coded ones below).
*
* If any DOM controls were bound to the CPU, then we need to call its updateDisplay() handler; if there are no
* such bindings, then cpu.updateDisplay() does nothing.
*
* Similarly, if there's a Panel, then we need to call its updateDisplay() handler, in case it created its own canvas
* and implemented its own register display (eg, dumpRegisters()); if not, then panel.updateDisplay() also does nothing.
*
* In practice, there will *either* be a Panel with a custom canvas *or* a set of DOM controls bound to the CPU *or*
* neither. In theory, there could be BOTH, but that would be unusual.
*
* TODO: Consider alternate approaches to these largely register-oriented display updates. Ordinarily, we like to
* separate logic from presentation, and currently the CPUState contains both, since it's the component that intimately
* knows the names, number, sizes, etc, of all the active registers. The Panel component is the logical candidate,
* but Panel is an optional component; it's often the case that only machines that include the Debugger also include
* Panel.
*
* @this {ComputerPDP11}
* @param {number} [nUpdate] (1 for periodic, -1 for forced, 0 or undefined otherwise)
*/
updateDisplays(nUpdate)
{
/*
* nUpdate is generally set to -1 whenever the CPU is transitioning to/from a running state, in which case
* cpu.updateDisplay() will definitely want to hide/show register contents; however, at other times, when the
* CPU is running, constantly updating the DOM controls too frequently can adversely impact overall performance.
*
* nUpdate will also be -1 whenever the Debugger has modified the state of the machine, implying that we're
* not sure what, if anything, actually changed.
*/
if (this.cpu) this.cpu.updateDisplay(nUpdate || 0);
if (this.panel) this.panel.updateDisplay(nUpdate || 0);
}
/**
* setBinding(sType, sBinding, control, sValue)
*
* @this {ComputerPDP11}
* @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc)
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset")
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sType, sBinding, control, sValue)
{
var computer = this;
switch (sBinding) {
case "power":
this.bindings[sBinding] = control;
control.onclick = function onClickPower() {
computer.onPower();
};
return true;
case "reset":
this.bindings[sBinding] = control;
control.onclick = function onClickReset() {
computer.onReset();
};
return true;
/*
* Technically, this binding should now be called "saveState", to clearly distinguish it from
* the "Save Machine" control that's normally bound to the savePC() function in save.js. Saving
* an entire machine includes everything needed to start/restore the machine; eg, the machine
* XML configuration file(s) *and* the JSON-encoded machine state.
*/
case "save":
/*
* Since this feature depends on the server supporting the PCjs User API (see userapi.js),
* and since pcjs.org is no longer running a Node web server, we disable the feature for that
* particular host.
*/
if (Str.endsWith(Web.getHost(), "pcjs.org")) {
if (DEBUG) this.log("Remote user API not available");
/*
* We could also simply hide the control; eg:
*
* control.style.display = "none";
*
* but removing the control altogether seems better.
*/
control.parentNode.removeChild(/** @type {Node} */ (control));
return false;
}
this.bindings[sBinding] = control;
control.onclick = function onClickSave() {
var sUserID = computer.queryUserID(true);
if (sUserID) {
/*
* I modified the test to include a check for sStatePath so that I could save new states
* for machines with existing states; otherwise, I'd have no (easy) way of capturing and
* updating their state. Making the machine (even temporarily) resumable would have been
* one work-around, but it's not appropriate for some machines, as their state is simply
* too large (for localStorage anyway, which is the default storage solution).
*/
var fSave = !!(computer.resume && !computer.sResumePath || computer.sStatePath);
var sState = computer.powerOff(fSave);
if (fSave) {
computer.saveServerState(sUserID, sState);
} else {
computer.notice("Resume disabled, machine state not saved");
}
}
/*
* This seemed like a handy alternative, but it turned out to be a no-go, at least for large states:
*
* var sState = computer.powerOff(true);
* if (sState) {
* sState = "data:text/json;charset=utf-8," + encodeURIComponent(sState);
* window.open(sState);
* }
*
* Perhaps if I embedded the data in a link on the current page instead; eg:
*
* $('<a href="' + sState + '" download="state.json">Download</a>').appendTo('#container');
*/
};
return true;
default:
break;
}
return false;
}
/**
* resetUserID()
*
* @this {ComputerPDP11}
*/
resetUserID()
{
Web.setLocalStorageItem(ComputerPDP11.STATE_USERID, "");
this.sUserID = null;
}
/**
* queryUserID(fPrompt)
*
* @this {ComputerPDP11}
* @param {boolean} [fPrompt]
* @returns {string|null|undefined}
*/
queryUserID(fPrompt)
{
var sUserID = this.sUserID;
if (!sUserID) {
sUserID = Web.getLocalStorageItem(ComputerPDP11.STATE_USERID);
if (sUserID !== undefined) {
if (!sUserID && fPrompt) {
/*
* NOTE: Warning the user here that "Save" operations are not currently supported by pcjs.org is
* merely a precaution, because ordinarily, setBinding() should have already determined if we are
* running from pcjs.org and disabled any "Save" button.
*/
sUserID = Component.promptUser("Saving machine states on the pcjs.org server is currently unsupported.\n\nIf you're running your own server, enter your user ID below.");
if (sUserID) {
sUserID = this.verifyUserID(sUserID);
if (!sUserID) this.notice("The user ID is invalid.");
}
}
} else if (fPrompt) {
this.notice("Browser local storage is not available");
}
}
return sUserID;
}
/**
* verifyUserID(sUserID)
*
* @this {ComputerPDP11}
* @param {string} sUserID
* @return {string} validated user ID, or null if error
*/
verifyUserID(sUserID)
{
this.sUserID = null;
var fMessages = DEBUG && this.messageEnabled();
if (fMessages) this.printMessage("verifyUserID(" + sUserID + ")");
var sRequest = Web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUserID;
var response = Web.getResource(sRequest);
var nErrorCode = response[0];
var sResponse = response[1];
if (!nErrorCode && sResponse) {
try {
response = eval("(" + sResponse + ")");
if (response.code && response.code == UserAPI.CODE.OK) {
Web.setLocalStorageItem(ComputerPDP11.STATE_USERID, response.data);
if (fMessages) this.printMessage(ComputerPDP11.STATE_USERID + " updated: " + response.data);
this.sUserID = response.data;
} else {
if (fMessages) this.printMessage(response.code + ": " + response.data);
}
} catch (e) {
Component.error(e.message + " (" + sResponse + ")");
}
} else {
if (fMessages) this.printMessage("invalid response (error " + nErrorCode + ")");
}
return this.sUserID;
}
/**
* getServerStatePath()
*
* @this {ComputerPDP11}
* @return {string|null} sStatePath (null if no localStorage or no USERID stored in localStorage)
*/
getServerStatePath()
{
var sStatePath = null;
if (this.sUserID) {
if (DEBUG && this.messageEnabled()) {
this.printMessage(ComputerPDP11.STATE_USERID + " for load: " + this.sUserID);
}
sStatePath = Web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.LOAD + '&' + UserAPI.QUERY.USER + '=' + this.sUserID + '&' + UserAPI.QUERY.STATE + '=' + State.key(this, PDP11.APPVERSION);
} else {
if (DEBUG && this.messageEnabled()) {
this.printMessage(ComputerPDP11.STATE_USERID + " unavailable");
}
}
return sStatePath;
}
/**
* saveServerState(sUserID, sState)
*
* @this {ComputerPDP11}
* @param {string} sUserID
* @param {string|null} sState
*/
saveServerState(sUserID, sState)
{
/*
* We must pass fSync == true, because (as I understand it) browsers will blow off any async
* requests when a page is being closed. Since our request is synchronous, storeServerState()
* should also return a result, but there's not much we can do with it, since browsers ALSO
* tend to blow off alerts() and the like when closing down.
*/
if (sState) {
if (DEBUG && this.messageEnabled()) {
this.printMessage("size of server state: " + sState.length + " bytes");
}
var response = this.storeServerState(sUserID, sState, true);
if (response && response[UserAPI.RES.CODE] == UserAPI.CODE.OK) {
this.notice("Machine state saved to server");
} else if (sState) {
var sError = (response && response[UserAPI.RES.DATA]) || UserAPI.FAIL.BADSTORE;
if (response[UserAPI.RES.CODE] == UserAPI.CODE.FAIL) {
sError = "Error: " + sError;
} else {
sError = "Error " + response[UserAPI.RES.CODE] + ": " + sError;
}
this.notice(sError);
this.resetUserID();
}
} else {
if (DEBUG && this.messageEnabled()) {
this.printMessage("no state to store");
}
}
}
/**
* storeServerState(sUserID, sState, fSync)
*
* @this {ComputerPDP11}
* @param {string} sUserID
* @param {string} sState
* @param {boolean} [fSync] is true if we're powering down and should perform a synchronous request (default is async)
* @return {*} server response if fSync is true and a response was received; otherwise null
*/
storeServerState(sUserID, sState, fSync)
{
if (DEBUG && this.messageEnabled()) {
this.printMessage(ComputerPDP11.STATE_USERID + " for store: " + sUserID);
}
/*
* TODO: Determine whether or not any browsers cancel our request if we're called during a browser "shutdown" event,
* and whether or not it matters if we do an async request (currently, we're not, to try to ensure the request goes through).
*/
var dataPost = {};
dataPost[UserAPI.QUERY.REQ] = UserAPI.REQ.STORE;
dataPost[UserAPI.QUERY.USER] = sUserID;
dataPost[UserAPI.QUERY.STATE] = State.key(this, PDP11.APPVERSION);
dataPost[UserAPI.QUERY.DATA] = sState;
var sRequest = Web.getHost() + UserAPI.ENDPOINT;
if (!fSync) {
Web.getResource(sRequest, dataPost, true);
} else {
var response = Web.getResource(sRequest, dataPost);
var sResponse = response[0];
if (response[1]) {
if (sResponse) {
var i = sResponse.indexOf('\n');
if (i > 0) sResponse = sResponse.substr(0, i);
if (!sResponse.indexOf("Error: ")) sResponse = sResponse.substr(7);
}
sResponse = '{"' + UserAPI.RES.CODE + '":' + response[1] + ',"' + UserAPI.RES.DATA + '":"' + sResponse + '"}';
}
if (DEBUG && this.messageEnabled()) this.printMessage(sResponse);
return JSON.parse(sResponse);
}
return null;
}
/**
* onPower()
*
* This handles UI requests to toggle the computer's power (eg, see the "power" button binding).
*
* @this {ComputerPDP11}
*/
onPower()
{
if (!this.nPowerChange) {
if (!this.flags.powered) {
this.wait(this.powerOn);
} else {
this.powerOff(false, true);
}
}
}
/**
* onReset()
*
* This handles UI requests to reset the computer's state (eg, see the "reset" button binding).
*
* @this {ComputerPDP11}
*/
onReset()
{
/*
* I'm going to start with the presumption that it makes little sense for an "unpowered" computer to be "reset";
* ditto if the power state is currently being changed.
*/
if (!this.flags.powered || this.nPowerChange) return;
/*
* If this is a "resumable" machine (and it's not using a predefined state), then we overload the reset
* operation to offer an explicit "save or discard" option first. This is currently the only UI we offer to
* discard a machine's state, including any disk changes. The traditional "reset" operation is still available
* for non-resumable machines.
*
* TODO: Break this behavior out into a separate "discard" operation, in case the designer of the machine really
* wants to clutter the UI with confusing options. ;-)
*/
if (this.resume && !this.sResumePath) {
/*
* I used to bypass the prompt if this.resume == ComputerPDP11.RESUME_AUTO, setting fSave to true automatically,
* but that gives the user no means of resetting a resumable machine that contains errors in its resume state.
*/
var fSave = (/* this.resume == ComputerPDP11.RESUME_AUTO || */ Component.confirmUser("Click OK to save changes to this " + PDP11.APPNAME + " machine.\n\nWARNING: If you CANCEL, all disk changes will be discarded."));
this.powerOff(fSave, true);
/*
* Forcing the page to reload is an expedient option, but ugly. It's preferable to call powerOn()
* and rely on all the components to reset themselves to their default state. The components with
* the greatest burden here are FDC and HDC, which must rely on the fReload flag to determine whether
* or not to unload/reload all their original auto-mounted disk images.
*
* However, if we started with a predefined state (ie, sStatePath is set), we take this shortcut, because
* we don't (yet) have code in place to gracefully reload the initial state (requires calling getResource()
* again); alternatively, we could avoid throwing that state away, but it seems better to save the memory.
*
* TODO: Make this more graceful, so that we can stop using the reloadPage() sledgehammer.
*/
if (!fSave && this.sStatePath) {
Web.reloadPage();
return;
}
if (!fSave) this.fReload = true;
this.powerOn(ComputerPDP11.RESUME_NONE);
this.fReload = false;
} else {
this.reset();
if (this.cpu && !this.dbg) this.cpu.autoStart();
}
}
/**
* getMachineComponent(sType, componentPrev)
*
* @this {ComputerPDP11}
* @param {string} sType
* @param {Component|null} [componentPrev] of previously returned component, if any
* @return {Component|null}
*/
getMachineComponent(sType, componentPrev)
{
var componentLast = componentPrev;
var aComponents = Component.getComponents(this.id);
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
var component = aComponents[iComponent];
if (componentPrev) {
if (componentPrev == component) componentPrev = null;
continue;
}
if (component.type == sType) return component;
}
if (!componentLast) Component.log("Machine component type '" + sType + "' not found", "warning");
return null;
}
/**
* setFocus(fScroll)
*
* NOTE: When soft keyboard buttons call us to return focus to the machine (and away from the button),
* the browser's default behavior is to scroll the element into view, which can be annoying, especially on iOS,
* where the display is more constrained, so we no longer do it by default (fScroll must be true).
*
* @this {ComputerPDP11}
* @param {boolean} [fScroll] (true if you really want the control scrolled into view)
*/
setFocus(fScroll)
{
if (this.controlPrint) {
/*
* This seems to be recommended work-around to prevent the browser from scrolling the focused element
* into view. The CPU is not a visual component, so when the CPU wants to set focus, the primary intent
* is to ensure that keyboard input is fielded properly.
*/
var x = 0, y = 0;
if (!fScroll && window) {
x = window.scrollX;
y = window.scrollY;
}
this.controlPrint.focus();
if (!fScroll && window) {
window.scrollTo(x, y);
}
}
}
/**
* ComputerPDP11.init()
*
* For every machine represented by an HTML element of class "pdp11-machine", this function
* locates the HTML element of class "computer", extracting the JSON-encoded parameters for the
* Computer constructor from the element's "data-value" attribute, invoking the constructor to
* create a Computer component, and then binding any associated HTML controls to the new component.
*/
static init()
{
/*
* In non-COMPILED builds, embedMachine() may have set XMLVERSION.
*/
if (!COMPILED && XMLVERSION) PDP11.APPVERSION = XMLVERSION;
var aeMachines = Component.getElementsByClass(document, PDP11.APPCLASS + "-machine");
for (var iMachine = 0; iMachine < aeMachines.length; iMachine++) {
var eMachine = aeMachines[iMachine];
var parmsMachine = Component.getComponentParms(eMachine);
var aeComputers = Component.getElementsByClass(eMachine, PDP11.APPCLASS, "computer");
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
var eComputer = aeComputers[iComputer];
var parmsComputer = Component.getComponentParms(eComputer);
/*
* We set fSuspended in the Computer constructor because we want to "power up" the
* computer ourselves, after any/all bindings are in place.
*/
var computer = new ComputerPDP11(parmsComputer, parmsMachine, true);
if (DEBUG && computer.messageEnabled()) {
computer.printMessage("onInit(" + computer.flags.powered + ")");
}
/*
* Bind any "power", "reset" and "save" buttons. An "erase" button was also considered,
* but "reset" now provides a way to force the machine to start from scratch again, so "erase"
* may be redundant now.
*/
Component.bindComponentControls(computer, eComputer, PDP11.APPCLASS);
/*
* Power on the computer, giving every component the opportunity to reset or restore itself.
*/
if (computer.fAutoPower) computer.wait(computer.powerOn);
}
}
}
/**
* ComputerPDP11.show()
*
* When exit() is using an "onbeforeunload" handler, this "onpageshow" handler allows us to repower everything,
* without either resetting or restoring. We call powerOn() with a special resume value (RESUME_REPOWER) if the
* computer is already marked as "ready", meaning the browser didn't change anything. This "repower" process
* should be very quick, essentially just marking all components as powered again (so that, for example, the Video
* component will start drawing again) and firing the CPU up again.
*/
static show()
{
var aeComputers = Component.getElementsByClass(document, PDP11.APPCLASS, "computer");
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
var eComputer = aeComputers[iComputer];
var parmsComputer = Component.getComponentParms(eComputer);
var computer = /** @type {ComputerPDP11} */ (Component.getComponentByType("Computer", parmsComputer['id']));
if (computer) {
computer.flags.unloading = false;
if (DEBUG && computer.messageEnabled()) {
computer.printMessage("onShow(" + computer.fInitialized + "," + computer.flags.powered + ")");
}
/*
* Note that the FIRST 'onpageshow' event, and therefore the first show() callback, occurs
* AFTER the the initial 'onload' event, and at that point in time, fInitialized will not be set yet.
* So, practically speaking, the first show() callback isn't all that useful.
*/
if (computer.fInitialized && !computer.flags.powered) {
/**
* Repower the computer, notifying every component to continue running as-is.
*/
computer.powerOn(ComputerPDP11.RESUME_REPOWER);
}
}
}
}
/**
* ComputerPDP11.exit()
*
* The Computer is currently the only component that uses an "exit" handler, which Web.onExit() defines as
* either an "unload" or "onbeforeunload" handler. This gives us the opportunity to save the machine state,
* using our powerOff() function, before the page goes away.
*
* It's worth noting that "onbeforeunload" offers one nice feature when used instead of "onload": the entire
* page (and therefore this entire application) is retained in its current state by the browser (well, some
* browsers), so that if you go to a new URL, either by entering a new URL in the same window/tab, or by pressing
* the FORWARD button, and then you press the BACK button, the page is immediately restored to its previous state.
*
* In fact, that's how some browsers operate whether you have an "onbeforeunload" handler or not; in other words,
* an "onbeforeunload" handler doesn't change the page retention behavior of the browser. By contrast, the mere
* presence of an "onunload" handler generally causes a browser to throw the page away once the handler returns.
*
* However, in order to safely use "onbeforeunload", we must add yet another handler ("onpageshow") to repower
* everything, without either resetting or restoring. Hence, the ComputerPDP11.show() function, which calls powerOn()
* with a special resume value (RESUME_REPOWER) if the computer is already marked as "ready", meaning the browser
* didn't change anything. This "repower" process should be very quick, essentially just marking all components as
* powered again (so that, for example, the Video component will start drawing again) and firing the CPU up again.
*
* Reportedly, some browsers (eg, Opera) don't support "onbeforeunload", in which case Component will have to use
* "unload" instead. But even when the page must be rebuilt from scratch, the combination of browser cache and
* localStorage means the simulation should be restored and become operational almost immediately.
*/
static exit()
{
var aeComputers = Component.getElementsByClass(document, PDP11.APPCLASS, "computer");
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
var eComputer = aeComputers[iComputer];
var parmsComputer = Component.getComponentParms(eComputer);
var computer = /** @type {ComputerPDP11} */ (Component.getComponentByType("Computer", parmsComputer['id']));
if (computer) {
/*
* Added a new flag that Component functions (eg, notice()) should check before alerting the user.
*/
computer.flags.unloading = true;
if (DEBUG && computer.messageEnabled()) {
computer.printMessage("onExit(" + computer.flags.powered + ")");
}
if (computer.flags.powered) {
/**
* Power off the computer, giving every component an opportunity to save its state,
* but only if 'resume' has been set AND there is no valid resume path (because if a valid resume
* path exists, we'll always load our state from there, and not from whatever we save here).
*/
computer.powerOff(!!(computer.resume && !computer.sResumePath), true);
}
}
}
}
}
ComputerPDP11.STATE_FAILSAFE = "failsafe";
ComputerPDP11.STATE_VALIDATE = "validate";
ComputerPDP11.STATE_TIMESTAMP = "timestamp";
ComputerPDP11.STATE_VERSION = "version";
ComputerPDP11.STATE_HOSTURL = "url";
ComputerPDP11.STATE_BROWSER = "browser";
ComputerPDP11.STATE_USERID = "user";
/*
* The following constants define all the resume options. Negative values (eg, RESUME_REPOWER) are for
* internal use only, and RESUME_DELETE is not documented (it provides a way of deleting ALL saved states
* whenever a resume is declined). As a result, the only "end-user" values are 0, 1 and 2.
*/
ComputerPDP11.RESUME_REPOWER = -1; // resume without changing any state (for internal use only)
ComputerPDP11.RESUME_NONE = 0; // default (no resume)
ComputerPDP11.RESUME_AUTO = 1; // automatically save/restore state
ComputerPDP11.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded)
ComputerPDP11.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented)
/*
* Initialize every Computer on the page.
*/
Web.onInit(ComputerPDP11.init);
Web.onShow(ComputerPDP11.show);
Web.onExit(ComputerPDP11.exit);
/**
* @copyright http://pcjs.org/modules/shared/lib/state.js (C) Jeff Parsons 2012-2017
*/
class State {
/**
* State(component, sVersion, sSuffix)
*
* State objects are used by components to save/restore their state.
*
* During a save operation, components add data to a State object via set(), and then return
* the resulting data using data().
*
* During a restore operation, the Computer component passes the results of each data() call
* back to the originating component.
*
* WARNING: Since State objects are low-level objects that have no UI requirements, they do not
* inherit from the Component class, so you should only use class methods of Component, such as
* Component.assert() (or Debugger methods if the Debugger is available).
*
* NOTE: 1.01 is the first version to provide limited save/restore support using localStorage.
* From that point on, care must be taken to insure that any new version that's incompatible with
* previous localStorage data be released with a version number that is at least 1 greater,
* since we're tagging the localStorage data with the integer portion of the version string.
*
* @param {Component} component
* @param {string} [sVersion] is used to append a major version number to the key
* @param {string} [sSuffix] is used to append any additional suffixes to the key
*/
constructor(component, sVersion, sSuffix)
{
this.id = component.id;
this.dbg = component.dbg;
this.json = "";
this.state = {};
this.fLoaded = this.fParsed = false;
this.key = State.key(component, sVersion, sSuffix);
this.unload(component.parms);
}
/**
* set(id, data)
*
* @this {State}
* @param {number|string} id
* @param {Object|string} data
*/
set(id, data)
{
try {
this.state[id] = data;
} catch(e) {
Component.log(e.message);
}
}
/**
* get(id)
*
* @this {State}
* @param {number|string} id
* @return {Object|string|null}
*/
get(id)
{
return this.state[id] || null;
}
/**
* data()
*
* @this {State}
* @return {Object}
*/
data()
{
return this.state;
}
/**
* load(json)
*
* WARNING: Make sure you follow this call with either a call to parse() or unload(),
* because any stringified data that we've loaded isn't usable until it's been parsed.
*
* @this {State}
* @param {string|null} [json]
* @return {boolean} true if state exists in localStorage, false if not
*/
load(json)
{
if (json) {
this.json = json;
this.fLoaded = true;
this.fParsed = false;
return true;
}
if (this.fLoaded) {
/*
* This is assumed to be a redundant load().
*/
return true;
}
if (Web.hasLocalStorage()) {
var s = Web.getLocalStorageItem(this.key);
if (s) {
this.json = s;
this.fLoaded = true;
if (DEBUG) Component.log("localStorage(" + this.key + "): " + s.length + " bytes loaded");
return true;
}
}
return false;
}
/**
* parse()
*
* This completes the load() operation, by parsing what was loaded, on the assumption there
* might be some benefit to deferring parsing until we've given the user a chance to confirm.
* Otherwise, load() could have just as easily done this, too.
*
* @this {State}
* @return {boolean} true if successful, false if error
*/
parse()
{
var fSuccess = true;
if (!this.fParsed) {
try {
this.state = JSON.parse(this.json);
this.fParsed = true;
} catch (e) {
Component.error(e.message || e);
fSuccess = false;
}
}
return fSuccess;
}
/**
* store()
*
* @this {State}
* @return {boolean} true if successful, false if error
*/
store()
{
var fSuccess = true;
if (Web.hasLocalStorage()) {
var s = JSON.stringify(this.state);
if (Web.setLocalStorageItem(this.key, s)) {
if (DEBUG) Component.log("localStorage(" + this.key + "): " + s.length + " bytes stored");
} else {
/*
* WARNING: Because browsers tend to disable all alerts() during an "unload" operation,
* it's unlikely anyone will ever see the "quota" errors that occur at this point. Need to
* think of some way to notify the user that there's a problem, and offer a way of cleaning
* up old states.
*/
Component.error("Unable to store " + s.length + " bytes in browser local storage");
fSuccess = false;
}
}
return fSuccess;
}
/**
* toString()
*
* @this {State}
* @return {string} JSON-encoded state
*/
toString()
{
return this.state? JSON.stringify(this.state) : this.json;
}
/**
* unload(parms)
*
* This discards any data saved via set() or loaded via load(), creating an empty State object.
* Note that you have to follow this call with an explicit call to store() if you want to remove
* the state from localStorage as well.
*
* @this {State}
* @param {Object} [parms]
*/
unload(parms)
{
this.json = "";
this.state = {};
this.fLoaded = this.fParsed = false;
if (parms) this.set("parms", parms);
}
/**
* clear(fAll)
*
* This unloads the current state, and then clears ALL localStorage for the current machine,
* independent of version, to reduce the chance of orphaned states wasting part of our limited allocation.
*
* @this {State}
* @param {boolean} [fAll] true to unconditionally clear ALL localStorage for the current domain
*/
clear(fAll)
{
this.unload();
var aKeys = Web.getLocalStorageKeys();
for (var i = 0; i < aKeys.length; i++) {
var sKey = aKeys[i];
if (sKey && (fAll || sKey.substr(0, this.key.length) == this.key)) {
Web.removeLocalStorageItem(sKey);
if (DEBUG) Component.log("localStorage(" + sKey + ") removed");
aKeys.splice(i, 1);
i = 0;
}
}
}
/**
* State.key(component, sVersion, sSuffix)
*
* This encapsulates the key generation code.
*
* @param {Component} component
* @param {string} [sVersion] is used to append a major version number to the key
* @param {string} [sSuffix] is used to append any additional suffixes to the key
* @return {string} key
*/
static key(component, sVersion, sSuffix)
{
var key = component.id;
if (sVersion) {
var i = sVersion.indexOf('.');
if (i > 0) key += ".v" + sVersion.substr(0, i);
}
if (sSuffix) {
key += "." + sSuffix;
}
return key;
}
/**
* State.compress(aSrc)
*
* @param {Array.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
static compress(aSrc)
{
if (aSrc) {
var iSrc = 0;
var iComp = 0;
var aComp = [];
while (iSrc < aSrc.length) {
var n = aSrc[iSrc];
var iCompare = iSrc + 1;
while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare++;
aComp[iComp++] = iCompare - iSrc;
aComp[iComp++] = n;
iSrc = iCompare;
}
if (aComp.length < aSrc.length) return aComp;
}
return aSrc;
}
/**
* State.decompress(aComp)
*
* @param {Array.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
static decompress(aComp, nLength)
{
var iDst = 0;
var aDst = new Array(nLength);
var iComp = 0;
while (iComp < aComp.length - 1) {
var c = aComp[iComp++];
var n = aComp[iComp++];
while (c--) {
aDst[iDst++] = n;
}
}
return aDst;
}
/**
* State.compressEvenOdd(aSrc)
*
* This is a very simple variation on compress() that compresses all the EVEN elements of aSrc first,
* followed by all the ODD elements. This tends to work better on EGA video memory, because when odd/even
* addressing is enabled (eg, for text modes), the DWORD values tend to alternate, which is the worst case
* for compress(), but the best case for compressEvenOdd().
*
* One wrinkle we support: if the first element is uninitialized, then we assume the entire array is undefined,
* and return an empty compressed array. Conversely, decompressEvenOdd() will take an empty compressed array
* and return an uninitialized array.
*
* @param {Array.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
static compressEvenOdd(aSrc)
{
if (aSrc) {
var iComp = 0, aComp = [];
if (aSrc[0] !== undefined) {
for (var off = 0; off < 2; off++) {
var iSrc = off;
while (iSrc < aSrc.length) {
var n = aSrc[iSrc];
var iCompare = iSrc + 2;
while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare += 2;
aComp[iComp++] = (iCompare - iSrc) >> 1;
aComp[iComp++] = n;
iSrc = iCompare;
}
}
}
if (aComp.length < aSrc.length) return aComp;
}
return aSrc;
}
/**
* State.decompressEvenOdd(aComp, nLength)
*
* This is the counterpart to compressEvenOdd(). Note that because there's nothing in the compressed sequence
* that differentiates a compress() sequence from a compressEvenOdd() sequence, you simply have to be consistent:
* if you used even/odd compression, then you must use even/odd decompression.
*
* @param {Array.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
static decompressEvenOdd(aComp, nLength)
{
var iDst = 0;
var aDst = new Array(nLength);
var iComp = 0;
while (iComp < aComp.length - 1) {
var c = aComp[iComp++];
var n = aComp[iComp++];
while (c--) {
aDst[iDst] = n;
iDst += 2;
}
/*
* The output of a "count,value" pair will never exceed the end of the output array, so as soon as we reach it
* the first time, we know it's time to switch to ODD elements, and as soon as we reach it again, we should be
* done.
*/
if (iDst == nLength) iDst = 1;
}
return aDst;
}
}
/**
* @copyright http://pcjs.org/modules/shared/lib/embed.js (C) Jeff Parsons 2012-2017
*/
/*
* We now support asynchronous XML and XSL file loads; simply set fAsync (below) to true.
*
* NOTE: For that support to work, we have to keep track of the number of machines on the page
* (ie, how many embedMachine() calls were issued), reduce the count as each machine XML file
* is fully transformed into HTML, and when the count finally returns to zero, notify all the
* machine component init() handlers.
*
* Also, to prevent those init() handlers from running prematurely, we must disable all page
* notification events at the start of the embedding process (Web.enablePageEvents(false)) and
* re-enable them at the end (Web.enablePageEvents(true)).
*/
var fAsync = true;
var cAsyncMachines = 0;
/**
* loadXML(sFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
*
* This is the preferred way to load all XML and XSL files. It uses getResource()
* to load them as strings, which parseXML() can massage before parsing/transforming them.
*
* For example, since I've been unable to get the XSLT document() function to work inside any
* XSL document loaded by JavaScript's XSLT processor, that has prevented me from dynamically
* loading any XML machine file that uses the "ref" attribute to refer to and incorporate
* another XML document.
*
* To solve that, I've added an fResolve parameter that tells parseXML() to fetch any
* referenced documents ITSELF and insert them into the XML string prior to parsing, instead
* of relying on the XSLT template to pull them in. That fetching is handled by resolveXML(),
* which iterates over the XML until all "refs" have been resolved (including any nested
* references).
*
* Also, XSL files with a <!DOCTYPE [...]> cause MSIE's Microsoft.XMLDOM.loadXML() function
* to choke, so I strip that out prior to parsing as well.
*
* TODO: Figure out why the XSLT document() function works great when the web browser loads an
* XML file (and the associated XSL file) itself, but does not work when loading documents via
* JavaScript XSLT support. Is it broken, is it a security issue, or am I just calling it wrong?
*
* @param {string} sXMLFile
* @param {string|null|undefined} idMachine
* @param {string|null|undefined} sAppName
* @param {string|null|undefined} sAppClass
* @param {string|null|undefined} sParms
* @param {boolean} fResolve is true to resolve any "ref" attributes
* @param {function(string)} display
* @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object)
*/
function loadXML(sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
{
var doneLoadXML = function(sURLName, sXML, nErrorCode) {
if (nErrorCode) {
if (!sXML) sXML = "unable to load " + sXMLFile + " (" + nErrorCode + ")";
done(sXML, null);
return;
}
parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done);
};
display("Loading " + sXMLFile + "...");
Web.getResource(sXMLFile, null, fAsync, doneLoadXML);
}
/**
* parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
*
* Generates an XML document from an XML string. This function also provides a work-around for XSLT's
* lack of support for the document() function (at least on some browsers), by replacing every reference
* tag (ie, a tag with a "ref" attribute) with the contents of the referenced file.
*
* @param {string} sXML
* @param {string|null} sXMLFile
* @param {string|null|undefined} idMachine
* @param {string|null|undefined} sAppName
* @param {string|null|undefined} sAppClass
* @param {string|null|undefined} sParms
* @param {boolean} fResolve is true to resolve any "ref" attributes; default is false
* @param {function(string)} display
* @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object)
*/
function parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
{
var buildXML = function(sXML, sError) {
if (sError) {
done(sError, null);
return;
}
if (idMachine) {
/*
* A more sensible place to record the machine XML would be embedMachine(), like we do for the
* XSL file, but since we're about to modify the original machine XML, it's best to record it now.
*/
Component.addMachineResource(idMachine, sXMLFile, sXML);
var sURL = sXMLFile;
if (sURL && sURL.indexOf('/') < 0 && window.location.pathname.slice(-1) == '/') {
sURL = window.location.pathname + sURL;
}
/*
* We embed the URL of the XML file both as a separate "xml" attribute for easy access from the
* XSL file, and as part of the "parms" attribute for easy access from machines (see getMachineParm()).
*/
if (!sParms) {
sParms = '{';
} else if (sParms.slice(-1) == '}') {
sParms = sParms.slice(0, -1);
if (sParms.length > 1) sParms += ',';
} else { // sParms must just be a "state" file, so encode it as a "state" property
sParms = '{state:"' + sParms + '",';
}
sParms += 'url:"' + sURL + '"}';
/*
* Note that while we no longer generate a machine XML file with a "state" attribute (because it's
* encoded inside the "parms" attribute), the XSL file must still cope with "state" attributes inside
* other XML files; for example, manifest XML files like /apps/pc/1981/visicalc/manifest.xml contain
* machine elements with "state" attributes that must still be passed down to the computer element
* "the old fashioned way".
*
* Until/unless that changes, components.xsl cannot be simplified as much as I might have hoped.
*/
if (typeof resources == 'object') sURL = null; // turn off URL inclusion if we have embedded resources
sParms = sParms.replace(/\$/g, "$$$$");
sXML = sXML.replace(/(<machine[^>]*\sid=)(['"]).*?\2/, "$1$2" + idMachine + "$2" + (sParms? " parms='" + sParms + "'" : "") + (sURL? ' url="' + sURL + '"' : ''));
}
if (!fResolve) {
/*
* I'm trying to switch to a shared components.xsl (at least for all PC-class machines),
* but in the interim, that means hacking the XSL file on the fly to reflect the actual class.
*/
sXML = sXML.replace(/(<xsl:variable name="APPNAME">).*?(<\/xsl:variable>)/, "$1" + sAppName + "$2");
sXML = sXML.replace(/(<xsl:variable name="APPCLASS">).*?(<\/xsl:variable>)/, "$1" + sAppClass + "$2");
/*
* Non-COMPILED kludge to replace the version number template in the XSL file (which we assume we're reading,
* since fResolve is false) with whatever XMLVERSION we extracted from the XML file (see corresponding kludge below).
*
* ES6 ALERT: Template strings.
*/
if (!COMPILED && XMLVERSION) {
sXML = sXML.replace(/<xsl:variable name="APPVERSION">1.x.x<\/xsl:variable>/, `<xsl:variable name="APPVERSION">${XMLVERSION}</xsl:variable>`);
}
}
/*
* If the resource we requested is not really an XML file (or the file didn't exist and the server simply returned
* a message like "Cannot GET /devices/pc/machine/5150/cga/64kb/donkey/machine.xml"), we'd like to display a more
* meaningful message, because the XML DOM parsers will blithely return a document that contains nothing useful; eg:
*
* This page contains the following errors:error on line 1 at column 1:
* Document is empty Below is a rendering of the page up to the first error.
*
* Supposedly, the IE XML DOM parser will throw an exception, but I haven't tested that, and unless all other
* browsers do that, that's not helpful.
*
* The best I can do at this stage (assuming Web.getResource() didn't drop any error information on the floor)
* is verify that the requested resource "looks like" valid XML (in other words, it begins with a '<').
*/
var xmlDoc = null;
if (sXML.charAt(0) == '<') {
try {
/*
* Another hack for MSIE, which fails to load XSL documents containing a <!DOCTYPE [...]> tag.
*
* This is also why the XSLTProcessor 'transformToFragment' method in Microsoft Edge silently failed,
* so I had pull this hack out of the "ActiveXObject" code. And rather than add yet-another Microsoft
* browser check, I'm going to try doing this across the board, and hope that none of the other XSLT
* processors fail *without* the DOCTYPE tag.
*/
if (!fResolve) {
sXML = sXML.replace(/<!DOCTYPE(.|[\r\n])*]>\s*/g, "");
}
/*
* Beginning with Microsoft Edge and the corresponding release of Windows 10, all the
* 'ActiveXObject' crud has gone away; but of course, this code must remain in place if
* we want to continue supporting older Internet Explorer browsers (ie, back to IE9).
*/
/** @namespace window.ActiveXObject */
if (window.ActiveXObject || 'ActiveXObject' in window) { // second test is required for IE11 on Windows 8.1
xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async = false;
xmlDoc['loadXML'](sXML);
} else {
/** @namespace window.DOMParser */
xmlDoc = (new window.DOMParser()).parseFromString(sXML, "text/xml");
}
} catch(e) {
xmlDoc = null;
sXML = e.message;
}
} else {
sXML = "unrecognized XML: " + (sXML.length > 255? sXML.substr(0, 255) + "..." : sXML);
}
done(sXML, xmlDoc);
};
if (sXML) {
if (PRIVATE) sXML = sXML.replace(/\/library.xml/, "/private/library.xml");
if (fResolve) {
resolveXML(sXML, display, buildXML);
return;
}
buildXML(sXML, null);
return;
}
done("no data" + (sXMLFile? " for file: " + sXMLFile : ""), null);
}
/**
* resolveXML(sXML, display, done)
*
* Replaces every tag with a "ref" attribute with the contents of the corresponding file.
*
* TODO: Fix some of the limitations of this code, such as: 1) requiring the "ref" attribute
* to appear as the tag's first attribute, 2) requiring the "ref" attribute to be double-quoted,
* and 3) requiring the "ref" tag to be self-closing.
*
* @param {string} sXML
* @param {function(string)} display
* @param {function(string,(string|null))} done (the first string contains the resolved XML data, the second is for any error message)
*/
function resolveXML(sXML, display, done)
{
var matchRef;
var reRef = /<([a-z]+)\s+ref="(.*?)"(.*?)\/>/g;
if ((matchRef = reRef.exec(sXML))) {
var sRefFile = matchRef[2];
var doneReadXML = function(sURLName, sXMLRef, nErrorCode) {
if (nErrorCode || !sXMLRef) {
done(sXML, "unable to resolve XML reference: " + matchRef[0] + " (" + nErrorCode + ")");
return;
}
/*
* If there are additional attributes in the "referring" XML tag, we want to insert them
* into the "referred" XML tag; attributes that don't exist in the referred tag should be
* appended, and attributes that DO exist should be overwritten.
*/
var sRefAttrs = matchRef[3];
if (sRefAttrs) {
var aXMLRefTag = sXMLRef.match(new RegExp("<" + matchRef[1] + "[^>]*>"));
if (aXMLRefTag) {
var sXMLNewTag = aXMLRefTag[0];
/*
* Iterate over all the attributes in the "referring" XML tag (sRefAttrs)
*/
var matchAttr;
var reAttr = /( [a-z]+=)(['"])(.*?)\2/gi;
while ((matchAttr = reAttr.exec(sRefAttrs))) {
if (sXMLNewTag.toLowerCase().indexOf(matchAttr[1].toLowerCase()) < 0) {
/*
* This is the append case....
*/
sXMLNewTag = sXMLNewTag.replace(">", matchAttr[0] + ">");
} else {
/*
* This is the overwrite case....
*/
sXMLNewTag = sXMLNewTag.replace(new RegExp(matchAttr[1] + "(['\"])(.*?)\\1"), matchAttr[0]);
}
}
if (aXMLRefTag[0] != sXMLNewTag) {
sXMLRef = sXMLRef.replace(aXMLRefTag[0], sXMLNewTag);
}
} else {
done(sXML, "missing <" + matchRef[1] + "> in " + sRefFile);
return;
}
}
/*
* Apparently when a Windows Azure server delivers one of my XML files, it may modify the first line:
*
* <?xml version="1.0" encoding="UTF-8"?>\n
*
* I didn't determine exactly what it was doing at this point (probably just changing the \n to \r\n),
* but in any case, relaxing the following replace() solved it.
*/
sXMLRef = sXMLRef.replace(/<\?xml[^>]*>[\r\n]*/, "");
sXML = sXML.replace(matchRef[0], sXMLRef);
resolveXML(sXML, display, done);
};
display("Loading " + sRefFile + "...");
Web.getResource(sRefFile, null, fAsync, doneReadXML);
return;
}
done(sXML, null);
}
/**
* embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFile, sParms)
*
* This allows to you embed a machine on a web page, by transforming the machine XML into HTML.
*
* @param {string} sAppName is the app name (eg, "PCx86")
* @param {string} sAppClass is the app class (eg, "pcx86"); also known as the machine class
* @param {string} sVersion is the app version (eg, "1.15.7")
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFile, sParms)
{
var eMachine, eWarning, fSuccess = true;
cAsyncMachines++;
Component.addMachine(idMachine);
var doneMachine = function() {
if (!--cAsyncMachines) {
if (fAsync) Web.enablePageEvents(true);
}
};
var displayError = function(sError) {
Component.log(sError);
displayMessage("Error: " + sError);
if (fSuccess) doneMachine();
fSuccess = false;
};
var displayMessage = function(sMessage) {
if (eWarning === undefined) {
/*
* Our MarkOut module (in convertMDMachineLinks()) creates machine containers that look like:
*
* <div id="' + sMachineID + '" class="machine-placeholder"><p>Embedded PC</p><p class="machine-warning">...</p></div>
*
* with the "machine-warning" paragraph pre-populated with a warning message that the user will
* see if nothing at all happens. But hopefully, in the normal case (and especially the error case),
* *something* will have happened.
*
* Note that it is the HTMLOut module (in processMachines()) that ultimately decides which scripts to
* include and then generates the embedXXX() call.
*/
var aeWarning = (eMachine && Component.getElementsByClass(eMachine, "machine-warning"));
eWarning = (aeWarning && aeWarning[0]) || eMachine;
}
if (eWarning) eWarning.innerHTML = Str.escapeHTML(sMessage);
};
try {
eMachine = document.getElementById(idMachine);
if (eMachine) {
/*
* If we have a 'css' resource, add it to the page first.
*/
var css;
if (typeof resources == "object" && (css = resources['css'])) {
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
head.appendChild(style);
}
if (!sXSLFile) {
/*
* Now that PCjs is an open-source project, we can make the following test more flexible,
* and revert to the internal template if DEBUG *or* internal version (instead of *and*).
*
* Third-party sites that don't use the PCjs server will ALWAYS want to specify a fully-qualified
* path to the XSL file, unless they choose to mirror our folder structure.
*/
var sAppFolder = sAppClass;
if (DEBUG || sVersion == "1.x.x") {
if (sAppClass != "c1pjs") sAppFolder = "shared";
sXSLFile = "/modules/" + sAppFolder + "/templates/components.xsl";
} else {
if (sAppClass.substr(0, 3) == "pdp") sAppFolder = "pdpjs";
sXSLFile = "/versions/" + sAppFolder + "/" + sVersion + "/components.xsl";
}
}
var processXML = function(sXML, xml) {
if (!xml) {
displayError(sXML);
return;
}
/*
* Non-COMPILED kludge to extract the version number from the stylesheet path in the machine XML file;
* we don't need this code in COMPILED (non-DEBUG) releases, because APPVERSION is hard-coded into them.
*/
if (!COMPILED) {
var aMatch = sXML.match(/<\?xml-stylesheet[^>]* href=(['"])[^'"]*?\/([0-9.]*)\/([^'"]*)\1/);
if (aMatch) XMLVERSION = aMatch[2];
}
var transformXML = function(sXSL, xsl) {
if (!xsl) {
displayError(sXSL);
return;
}
/*
* Record the XSL file, in case someone wants to save the entire machine later.
*/
Component.addMachineResource(idMachine, sXSLFile, sXSL);
/*
* The <machine> template in components.xsl now generates a "machine div" that makes
* the div we required the caller of embedMachine() to provide redundant, so instead
* of appending this fragment to the caller's node, we REPLACE the caller's node.
* This works only because because we ALSO inject the caller's "machine div" ID into
* the fragment's ID during parseXML().
*
* eMachine.innerHTML = sFragment;
*
* Also, if the transform function fails, make sure you're using the appropriate
* "components.xsl" and not a "machine.xsl", because the latter will not produce valid
* embeddable HTML (and is the most common cause of failure at this final stage).
*/
displayMessage("Processing " + sXMLFile + "...");
/*
* Beginning with Microsoft Edge and the corresponding release of Windows 10, all the
* 'ActiveXObject' crud has gone away; but of course, this code must remain in place if
* we want to continue supporting older Internet Explorer browsers (ie, back to IE9).
*/
if (window.ActiveXObject || 'ActiveXObject' in window) { // second test is required for IE11 on Windows 8.1
var sFragment = xml['transformNode'](xsl);
if (sFragment) {
eMachine.outerHTML = sFragment;
doneMachine();
} else {
displayError("transformNodeToObject failed");
}
}
else if (document.implementation && document.implementation.createDocument) {
var xsltProcessor = new XSLTProcessor();
xsltProcessor['importStylesheet'](xsl);
var eFragment = xsltProcessor['transformToFragment'](xml, document);
if (eFragment) {
/*
* This fails in Microsoft Edge...
*
var machine = eFragment.getElementById(idMachine);
if (!machine) {
displayError("machine generation failed: " + idMachine);
} else
*/
if (eMachine.parentNode) {
eMachine.parentNode.replaceChild(eFragment, eMachine);
doneMachine();
} else {
/*
* NOTE: This error can occur if our Node web server, when processing a folder with
* both a manifest.xml with a machine.xml reference AND a README.md containing a
* machine link, generates duplicate embedXXX() calls for the same machine; if the
* first embedXXX() call finds its target, subsequent calls for the same target will
* fail.
*
* Technically, such a folder is in a misconfigured state, but it happens, in part
* because when we switched to the Jekyll web server, we had to add machine links to
* all README.md files where we had previously relied on manifest.xml or machine.xml
* processing. This is because the Jekyll web server currently doesn't process XML
* files, nor is support for that likely to be added any time soon; it was a nice
* feature of the Node web server, but it's not clear that it's worth doing for Jekyll.
*/
displayError("invalid machine element: " + idMachine);
}
} else {
displayError("transformToFragment failed");
}
} else {
/*
* Perhaps I should have performed this test at the outset; on the other hand, I'm
* not aware of any browsers don't support one or both of the above XSLT transformation
* methods, so treat this as a bug.
*/
displayError("unable to transform XML: unsupported browser");
}
};
loadXML(sXSLFile, null, sAppName, sAppClass, null, false, displayMessage, transformXML);
};
if (sXMLFile.charAt(0) != '<') {
loadXML(sXMLFile, idMachine, sAppName, sAppClass, sParms, true, displayMessage, processXML);
} else {
parseXML(sXMLFile, null, idMachine, sAppName, sAppClass, sParms, false, displayMessage, processXML);
}
} else {
displayError("missing machine element: " + idMachine);
}
} catch(e) {
displayError(e.message);
}
return fSuccess;
}
/**
* embedC1P(idMachine, sXMLFile, sXSLFile)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @return {boolean} true if successful, false if error
*/
function embedC1P(idMachine, sXMLFile, sXSLFile)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("C1Pjs", "c1pjs", APPVERSION, idMachine, sXMLFile, sXSLFile);
}
/**
* embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PCx86", "pcx86", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PC8080", "pc8080", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPDP10(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPDP10(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PDPjs", "pdp10", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPDP11(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPDP11(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PDPjs", "pdp11", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* findMachineComponent(idMachine, sType)
*
* @param {string} idMachine
* @param {string} sType
* @return {Component|null}
*/
function findMachineComponent(idMachine, sType)
{
return Component.getComponentByType(sType, idMachine + ".machine");
}
/**
* commandMachine(control, fSingle, idMachine, sComponent, sCommand, sValue)
*
* Use Component methods to find the requested component for a specific machine, and if the component is found,
* then check its 'exports' table for an entry matching the specified command string, and if an entry is found, then
* the corresponding function is called with the specified data.
*
* @param {Object} control
* @param {boolean} fSingle
* @param {string} idMachine
* @param {string} sComponent
* @param {string} sCommand
* @param {string} [sValue]
* @return {boolean}
*/
function commandMachine(control, fSingle, idMachine, sComponent, sCommand, sValue)
{
if (sCommand == "script") {
if (Component.processScript(idMachine, sValue)) {
if (fSingle) control.disabled = true;
return true;
}
return false;
}
if (sComponent) {
var component = Component.getComponentByType(sComponent, idMachine + ".machine");
if (component) {
var exports = component['exports'];
if (exports) {
var fnCommand = exports[sCommand];
if (fnCommand) {
if (fnCommand.call(component, sValue)) {
if (fSingle) control.disabled = true;
return true;
}
return false;
}
}
}
}
console.log("unimplemented: commandMachine('" + idMachine + "','" + sComponent + "','" + sCommand + "','" + sValue + "')");
return false;
}
/**
* Prevent the Closure Compiler from renaming functions we want to export, by adding them as global properties.
*
* TODO: Consider making all these functions properties on a single global object (eg, 'PCjs'), to minimize global
* pollution and risk of name collision.
*/
if (APPNAME == "C1Pjs") {
window['embedC1P'] = embedC1P;
}
if (APPNAME == "PCx86") {
window['embedPC'] = embedPCx86; // WARNING: embedPC() deprecated as of v1.23.0
window['embedPCx86'] = embedPCx86;
}
if (APPNAME == "PC8080") {
window['embedPC8080'] = embedPC8080;
}
if (APPNAME == "PDPjs") {
window['embedPDP10'] = embedPDP10;
window['embedPDP11'] = embedPDP11;
}
window['commandMachine'] = commandMachine;
window['enableEvents'] = Web.enablePageEvents;
window['sendEvent'] = Web.sendPageEvent;