Updated PC8080 and PDPjs to support RAM images, eliminating the need for the old "writable ROM" kludge; RAM images can include their own load and exec addresses as part of the JSON file, but the RAM component can provide explicit overrides (if no load address is specified either way, the default load address is the starting RAM address)

This commit is contained in:
Jeff Parsons 2016-10-17 11:01:32 -07:00 committed by Jeff Parsons
commit 980c2ac5ab
64 changed files with 1479 additions and 1154 deletions

View file

@ -31,11 +31,12 @@
if (NODE) {
var str = require("../../shared/lib/strlib");
var web = require("../../shared/lib/weblib");
var DumpAPI = require("../../shared/lib/dumpapi");
var Component = require("../../shared/lib/component");
var State = require("../../shared/lib/state");
var PC8080 = require("./defines");
var CPUDef8080 = require("./cpudef");
var Memory8080 = require("./memory");
var ROM8080 = require("./rom");
}
/**
@ -45,6 +46,9 @@ if (NODE) {
*
* 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().
@ -57,14 +61,66 @@ function RAM8080(parmsRAM)
{
Component.call(this, "RAM", parmsRAM, RAM8080);
this.abInit = null;
this.aSymbols = null;
this.addrRAM = parmsRAM['addr'];
this.sizeRAM = parmsRAM['size'];
this.nFileLoad = parmsRAM['load'];
this.nFileExec = parmsRAM['exec'];
this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified
this.fAllocated = 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(sURL, sResponse, nErrorCode) {
ram.doneLoad(sURL, sResponse, nErrorCode);
});
}
}
Component.subclass(RAM8080);
RAM8080.CPM = {
BIOS: {
VECTOR: 0x0000
},
BDOS: {
VECTOR: 0x0005,
FUNC: { // function number (specified in regC)
RESET: 0x00,
CON_READ: 0x01, // output: A = L = ASCII character
CON_WRITE: 0x02, // input: E = ASCII character
AUX_READ: 0x03, // output: A = L = ASCII character
AUX_WRITE: 0x04, // input: E = ASCII character
PRN_WRITE: 0x05, // input: E = ASCII character
MEM_SIZE: 0x06, // output: base address of CCP (Console Command Processor), but which register? (perhaps moot if this was CP/M 1.3 only...)
CON_IO: 0x06, // input: E = ASCII character (or 0xFF to return ASCII character in A)
GET_IOBYTE: 0x07,
SET_IOBYTE: 0x08,
STR_WRITE: 0x09 // input: DE = address of string
}
},
INIT: 0x100
};
RAM8080.CPM.VECTORS = [RAM8080.CPM.BIOS.VECTOR, RAM8080.CPM.BDOS.VECTOR];
/**
* initBus(cmp, bus, cpu, dbg)
*
@ -79,7 +135,7 @@ RAM8080.prototype.initBus = function(cmp, bus, cpu, dbg)
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.setReady();
this.initRAM();
};
/**
@ -92,15 +148,12 @@ RAM8080.prototype.initBus = function(cmp, bus, cpu, dbg)
*/
RAM8080.prototype.powerUp = function(data, fRepower)
{
if (!fRepower) {
/*
* The Computer powers up the CPU last, at which point CPUState state is restored,
* which includes the Bus state, and since we use the Bus to allocate all our memory,
* memory contents are already restored for us, so we don't need the usual restore
* logic. We just need to call reset(), to allocate memory for the RAM.
*/
this.reset();
}
/*
* The Computer powers up the CPU last, at which point CPUState state is restored,
* which includes the Bus state, and since we use the Bus to allocate all our memory,
* memory contents are already restored for us, so we don't need the usual restore
* logic.
*/
return true;
};
@ -120,7 +173,98 @@ RAM8080.prototype.powerDown = function(fSave, fShutdown)
* our memory, memory contents are already saved for us, so we don't need the usual
* save logic.
*/
return (fSave)? this.save() : true;
return true;
};
/**
* doneLoad(sURL, sData, nErrorCode)
*
* @this {RAM8080}
* @param {string} sURL
* @param {string} sData
* @param {number} nErrorCode (response from server if anything other than 200)
*/
RAM8080.prototype.doneLoad = function(sURL, sData, nErrorCode)
{
if (nErrorCode) {
this.notice("Unable to load RAM resource (error " + nErrorCode + ": " + sURL + ")");
return;
}
Component.addMachineResource(this.idMachine, sURL, sData);
var resource = web.parseMemoryResource(sURL, sData);
if (resource) {
this.abInit = resource.aBytes;
this.aSymbols = resource.aSymbols;
if (this.nFileLoad == null && resource.nLoad != null) this.nFileLoad = resource.nLoad;
if (this.nFileExec == null && resource.nExec != null) this.nFileExec = resource.nExec;
} else {
this.sFilePath = null;
}
this.initRAM();
};
/**
* initRAM()
*
* This function is called by both initBus() and doneLoad(), but it cannot copy the initial data into place
* until after initBus() has received the Bus component AND doneLoad() has received the data. When both those
* criteria are satisfied, the component becomes "ready".
*
* @this {RAM8080}
*/
RAM8080.prototype.initRAM = function()
{
if (!this.fAllocated && this.sizeRAM) {
if (this.bus.addMemory(this.addrRAM, this.sizeRAM, Memory8080.TYPE.RAM)) {
this.fAllocated = true;
}
}
if (!this.isReady()) {
if (!this.fAllocated) {
Component.error("No RAM allocated");
}
else if (this.sFilePath) {
/*
* Too early...
*/
if (!this.abInit || !this.bus) return;
var addr = this.addrRAM;
if (this.nFileLoad !== null) addr = this.nFileLoad;
for (var i = 0; i < this.abInit.length; i++) {
this.bus.setByteDirect(addr + i, this.abInit[i]);
}
if (this.nFileExec !== null) {
/*
* Here's where we enable our "Fake CP/M" support, triggered by the user loading a "writable" ROM image
* at offset 0x100. Fake CP/M support works by installing HLT opcodes at well-known CP/M addresses
* (namely, 0x0000, which is the CP/M reset vector, and 0x0005, which is the CP/M system call vector) and
* then telling the CPU to call us whenever a HLT occurs, so we can check PC for one of these addresses.
*/
if (this.nFileExec == RAM8080.CPM.INIT) {
for (i = 0; i < RAM8080.CPM.VECTORS.length; i++) {
this.bus.setByteDirect(RAM8080.CPM.VECTORS[i], CPUDef8080.OPCODE.HLT);
}
this.cpu.addHaltCheck(function(rom) {
return function(addr) {
return rom.checkCPMVector(addr)
};
}(this));
}
this.cpu.setReset(this.nFileExec);
}
/*
* TODO: Consider an option to retain this data and give the user a way of restoring the initial contents.
*/
delete this.abInit;
}
this.setReady();
}
};
/**
@ -130,41 +274,99 @@ RAM8080.prototype.powerDown = function(fSave, fShutdown)
*/
RAM8080.prototype.reset = function()
{
if (!this.fAllocated && this.sizeRAM) {
if (this.bus.addMemory(this.addrRAM, this.sizeRAM, Memory8080.TYPE.RAM)) {
this.fAllocated = true;
/*
* If you want to zero RAM on reset, then this would be a good place to do it.
*/
};
/**
* checkCPMVector(addr)
*
* @this {RAM8080}
* @param {number} addr (of the HLT opcode)
* @return {boolean} true if special processing performed, false if not
*/
RAM8080.prototype.checkCPMVector = function(addr)
{
var i = RAM8080.CPM.VECTORS.indexOf(addr);
if (i >= 0) {
var fCPM = false;
var cpu = this.cpu;
var dbg = this.dbg;
if (addr == RAM8080.CPM.BDOS.VECTOR) {
fCPM = true;
switch(cpu.regC) {
case RAM8080.CPM.BDOS.FUNC.CON_WRITE:
this.writeCPMString(this.getCPMChar(cpu.regE));
break;
case RAM8080.CPM.BDOS.FUNC.STR_WRITE:
this.writeCPMString(this.getCPMString(cpu.getDE(), '$'));
break;
default:
fCPM = false;
break;
}
}
if (fCPM) {
CPUDef8080.opRET.call(cpu); // for recognized calls, automatically return
}
else if (dbg) {
this.println("\nCP/M vector " + str.toHexWord(addr));
cpu.setPC(addr); // this is purely for the Debugger's benefit, to show the HLT
dbg.stopCPU();
}
return true;
}
if (!this.fAllocated) {
Component.error("No RAM allocated");
}
return false;
};
/**
* getCPMChar(ch)
*
* @this {RAM8080}
* @param {number} ch
* @return {string}
*/
RAM8080.prototype.getCPMChar = function(ch)
{
return String.fromCharCode(ch);
};
/**
* save()
*
* This implements save support for the RAM8080 component.
* getCPMString(addr, chEnd)
*
* @this {RAM8080}
* @return {Object}
* @param {number} addr (of a string)
* @param {string|number} [chEnd] (terminating character, default is 0)
* @return {string}
*/
RAM8080.prototype.save = function()
RAM8080.prototype.getCPMString = function(addr, chEnd)
{
return null;
var s = "";
var cchMax = 255;
var bEnd = chEnd && chEnd.length && chEnd.charCodeAt(0) || chEnd || 0;
while (cchMax--) {
var b = this.cpu.getByte(addr++);
if (b == bEnd) break;
s += String.fromCharCode(b);
}
return s;
};
/**
* restore(data)
*
* This implements restore support for the RAM8080 component.
* writeCPMString(s)
*
* @this {RAM8080}
* @param {Object} data
* @return {boolean} true if successful, false if failure
* @param {string} s
*/
RAM8080.prototype.restore = function(data)
RAM8080.prototype.writeCPMString = function(s)
{
return true;
s = s.replace(/\r/g, '');
if (this.controlPrint) {
this.controlPrint.value += s;
this.controlPrint.scrollTop = this.controlPrint.scrollHeight;
}
};
/**

View file

@ -34,7 +34,6 @@ if (NODE) {
var DumpAPI = require("../../shared/lib/dumpapi");
var Component = require("../../shared/lib/component");
var PC8080 = require("./defines");
var CPUDef8080 = require("./cpudef");
var Memory8080 = require("./memory");
}
@ -47,7 +46,6 @@ if (NODE) {
* size: amount of ROM, in bytes
* alias: physical alias address (null if none)
* file: name of ROM data file
* writable: true to make ROM writable (default is false)
*
* 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 doneLoad()).
@ -55,13 +53,6 @@ if (NODE) {
* Also, while the size parameter may seem redundant, I consider it useful to confirm that the ROM you received
* is the ROM you expected.
*
* Finally, while making ROM "writable" may seem a contradiction in terms, I want to be able to load selected
* binary files into memory purely for testing purposes, and the RAM component has no "file" option, so the
* simplest solution was to add the option to load binary files into memory as "writable" ROMs.
*
* Moreover, if a "writable" ROM is installed at addr 0x100, that triggers our "Fake CP/M" support, providing
* a quick-and-dirty means of loading simple CP/M test binaries. See addROM() for details.
*
* @constructor
* @extends Component
* @param {Object} parmsROM
@ -73,7 +64,7 @@ function ROM8080(parmsROM)
this.abROM = null;
this.addrROM = parmsROM['addr'];
this.sizeROM = parmsROM['size'];
this.fWritable = parmsROM['writable'];
/*
* The new 'alias' property can now be EITHER a single physical address (like 'addr') OR an array of
@ -112,30 +103,6 @@ function ROM8080(parmsROM)
Component.subclass(ROM8080);
ROM8080.CPM = {
BIOS: {
VECTOR: 0x0000
},
BDOS: {
VECTOR: 0x0005,
FUNC: { // function number (specified in regC)
RESET: 0x00,
CON_READ: 0x01, // output: A = L = ASCII character
CON_WRITE: 0x02, // input: E = ASCII character
AUX_READ: 0x03, // output: A = L = ASCII character
AUX_WRITE: 0x04, // input: E = ASCII character
PRN_WRITE: 0x05, // input: E = ASCII character
MEM_SIZE: 0x06, // output: base address of CCP (Console Command Processor), but which register? (perhaps moot if this was CP/M 1.3 only...)
CON_IO: 0x06, // input: E = ASCII character (or 0xFF to return ASCII character in A)
GET_IOBYTE: 0x07,
SET_IOBYTE: 0x08,
STR_WRITE: 0x09 // input: DE = address of string
}
}
};
ROM8080.CPM.VECTORS = [ROM8080.CPM.BIOS.VECTOR, ROM8080.CPM.BDOS.VECTOR];
/*
* 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.
@ -350,31 +317,12 @@ ROM8080.prototype.copyROM = function()
*/
ROM8080.prototype.addROM = function(addr)
{
if (this.bus.addMemory(addr, this.sizeROM, this.fWritable? Memory8080.TYPE.RAM : Memory8080.TYPE.ROM)) {
if (this.bus.addMemory(addr, this.sizeROM, Memory8080.TYPE.ROM)) {
if (DEBUG) this.log("addROM(): copying ROM to " + str.toHexLong(addr) + " (" + str.toHexLong(this.abROM.length) + " bytes)");
var i;
for (i = 0; i < this.abROM.length; i++) {
this.bus.setByteDirect(addr + i, this.abROM[i]);
}
if (this.fWritable && addr == 0x100) {
/*
* Here's where we enable our "Fake CP/M" support, triggered by the user loading a "writable" ROM image
* at offset 0x100. Fake CP/M support works by installing HLT opcodes at well-known CP/M addresses
* (namely, 0x0000, which is the CP/M reset vector, and 0x0005, which is the CP/M system call vector) and
* then telling the CPU to call us whenever a HLT occurs, so we can check PC for one of these addresses.
*/
for (i = 0; i < ROM8080.CPM.VECTORS.length; i++) {
this.bus.setByteDirect(ROM8080.CPM.VECTORS[i], CPUDef8080.OPCODE.HLT);
}
this.cpu.addHaltCheck(function(rom) {
return function(addr) {
return rom.checkCPMVector(addr)
};
}(this));
this.cpu.setReset(addr);
}
return true;
}
/*
@ -383,96 +331,6 @@ ROM8080.prototype.addROM = function(addr)
return false;
};
/**
* checkCPMVector(addr)
*
* @this {ROM8080}
* @param {number} addr (of the HLT opcode)
* @return {boolean} true if special processing performed, false if not
*/
ROM8080.prototype.checkCPMVector = function(addr)
{
var i = ROM8080.CPM.VECTORS.indexOf(addr);
if (i >= 0) {
var fCPM = false;
var cpu = this.cpu;
var dbg = this.dbg;
if (addr == ROM8080.CPM.BDOS.VECTOR) {
fCPM = true;
switch(cpu.regC) {
case ROM8080.CPM.BDOS.FUNC.CON_WRITE:
this.writeCPMString(this.getCPMChar(cpu.regE));
break;
case ROM8080.CPM.BDOS.FUNC.STR_WRITE:
this.writeCPMString(this.getCPMString(cpu.getDE(), '$'));
break;
default:
fCPM = false;
break;
}
}
if (fCPM) {
CPUDef8080.opRET.call(cpu); // for recognized calls, automatically return
}
else if (dbg) {
this.println("\nCP/M vector " + str.toHexWord(addr));
cpu.setPC(addr); // this is purely for the Debugger's benefit, to show the HLT
dbg.stopCPU();
}
return true;
}
return false;
};
/**
* getCPMChar(ch)
*
* @this {ROM8080}
* @param {number} ch
* @return {string}
*/
ROM8080.prototype.getCPMChar = function(ch)
{
return String.fromCharCode(ch);
};
/**
* getCPMString(addr, chEnd)
*
* @this {ROM8080}
* @param {number} addr (of a string)
* @param {string|number} [chEnd] (terminating character, default is 0)
* @return {string}
*/
ROM8080.prototype.getCPMString = function(addr, chEnd)
{
var s = "";
var cchMax = 255;
var bEnd = chEnd && chEnd.length && chEnd.charCodeAt(0) || chEnd || 0;
while (cchMax--) {
var b = this.cpu.getByte(addr++);
if (b == bEnd) break;
s += String.fromCharCode(b);
}
return s;
};
/**
* writeCPMString(s)
*
* @this {ROM8080}
* @param {string} s
*/
ROM8080.prototype.writeCPMString = function(s)
{
s = s.replace(/\r/g, '');
if (this.controlPrint) {
this.controlPrint.value += s;
this.controlPrint.scrollTop = this.controlPrint.scrollHeight;
}
};
/**
* cloneROM(addr)
*