32159 lines
1.1 MiB
32159 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);
|
|
}
|
|
|
|
/**
|
|
* 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 = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
|
|
/*
|
|
* 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.TYPE.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.TYPE.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.TYPE.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.TYPE.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.TYPE.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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* These are the standard TYPE 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.TYPE = {
|
|
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;
|