Merge branch 'next-release'

This commit is contained in:
Jeff Parsons 2016-08-17 14:09:47 -07:00
commit 634b31468f
12 changed files with 715 additions and 474 deletions

View file

@ -13,4 +13,4 @@ The following 8080-based machines are currently available:
* [Space Invaders (1978)](invaders/) (with [Debugger](invaders/debugger/))
* [DEC VT100 Terminal](vt100/) (with [Debugger](vt100/debugger/))
And check out the [Dual VT100 Terminals](vt100/dual/) for a demo of two PCjs machines communicating over a *virtual* serial connection.
Check out the [Dual VT100 Terminals](vt100/dual/) for a demo of two PCjs machines communicating over a *virtual* serial connection.

View file

@ -14,7 +14,8 @@ This is a PCjs work-in-progress emulation of another 8080-based machine: the VT1
Unlike other VT100 emulators, it is not simply an emulation of VT100 protocols. It is a simulation of the original VT100
machine, running the [VT100 Firmware](/devices/pc8080/rom/vt100/) inside the [PC8080](/modules/pc8080/) CPU emulator.
A [Debugger Configuration](/devices/pc8080/machine/vt100/debugger/) is also available.
A [Debugger Configuration](debugger/) is also available, along with a [Dual VT100 Terminals](dual/) demo.
Admittedly, terminals aren't that useful in isolation, since they're designed to be connected to other (host) machines.
But once this PCjs VT100 Terminal simulation is fully operational, you can expect to see it used in conjunction with a variety

View file

@ -20,6 +20,8 @@ This is a test of two [VT100 Terminals](../) whose serial ports are *virtually*
terminal should appear on the screen of the second, and vice versa. Click or tap on the screen of the desired terminal before
you begin typing.
A [Dual Debugger Configuration](debugger/) is also available.
If either terminal becomes LOCKED, press the SET-UP key (mapped to your F9 function key) twice to unlock it.
{% include machine.html id="vt100a" %}

View file

@ -0,0 +1,29 @@
---
layout: page
title: Dual VT100 Terminals with Debuggers
permalink: /devices/pc8080/machine/vt100/dual/debugger/
machines:
- id: vt100a
type: pc8080
debugger: true
autoStart: true
messages: serial|mem|port
config: /devices/pc8080/machine/vt100/debugger/machine.xml
connection: serialPort=vt100b.serialPort
- id: vt100b
type: pc8080
debugger: true
autoStart: true
messages: serial|mem|port
config: /devices/pc8080/machine/vt100/debugger/machine.xml
connection: serialPort=vt100a.serialPort
---
Dual VT100 Terminals with Debuggers
-----------------------------------
[Dual VT100 Terminals](../) with Debuggers attached.
{% include machine.html id="vt100a" %}
{% include machine.html id="vt100b" %}

View file

@ -122,6 +122,11 @@ function CPU(parmsCPU, nCyclesDefault)
this.aCounts.nCyclesChecksumInterval = parmsCPU["csInterval"];
this.aCounts.nCyclesChecksumStop = parmsCPU["csStop"];
/*
* Array of countdown timers managed by addTimer() and setTimer().
*/
this.aTimers = [];
this.onRunTimeout = this.runCPU.bind(this); // function onRunTimeout() { cpu.runCPU(); };
this.setReady();
@ -536,6 +541,9 @@ CPU.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
* in anticipation of the timer requiring an update sooner than the normal nCyclesPerYield
* period in runCPU() would normally provide.
*
* NOTE: In this context, "timer" refers to a timer chip (eg, an Intel 8253) being emulated by
* by the ChipSet component, not the timers managed by the CPU (eg, addTimer(), setTimer(), etc).
*
* @this {CPU}
* @param {number} nCycles is the target number of cycles to drop the current burst to
* @return {boolean}
@ -754,14 +762,14 @@ CPU.prototype.getSpeedTarget = function()
*
* 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 {CPU}
* @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
*
* @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).
*/
CPU.prototype.setSpeed = function(nMultiplier, fUpdateFocus)
{
@ -939,6 +947,104 @@ CPU.prototype.calcRemainingTime = function()
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.
*
* @this {CPU}
* @param {function()} callBack
* @return {number} timer index
*/
CPU.prototype.addTimer = function(callBack)
{
var iTimer = this.aTimers.length;
this.aTimers.push([-1, callBack]);
return iTimer;
};
/**
* setTimer(iTimer, ms)
*
* 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
* receiver interrupts at 9600 baud).
*
* Ideally, the only function that would use setTimeout() is runCPU(), while the rest of the components would
* use setTimer(); however, due to legacy code (ie, code that predates these functions) and/or laziness,
* that's currently not the case. TODO: Fix.
*
* @this {CPU}
* @param {number} iTimer
* @param {number} ms (converted into a cycle countdown internally)
* @return {number} (number of cycles used to arm timer, or -1 if error)
*/
CPU.prototype.setTimer = function(iTimer, ms)
{
var nCycles = -1;
if (iTimer >= 0 && iTimer < this.aTimers.length) {
nCycles = (this.aCounts.nCyclesPerSecond * this.aCounts.nCyclesMultiplier) / 1000 * ms;
this.aTimers[iTimer][0] = nCycles;
}
return nCycles;
};
/**
* getTimerBurst(nCycles)
*
* Used by runCPU() to either accept or shorten the current burst if any timers need to fire soon.
*
* @this {CPU}
* @param {number} nCycles (number of cycles about to execute)
* @return {number} (either nCycles or less if a timer needs to fire)
*/
CPU.prototype.getTimerBurst = function(nCycles)
{
for (var i = 0; i < this.aTimers.length; i++) {
var timer = this.aTimers[i];
if (timer[0] < 0) continue;
if (nCycles > timer[0]) {
nCycles = timer[0];
}
}
return nCycles;
};
/**
* 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 {CPU}
* @param {number} nCycles (number of cycles actually executed)
*/
CPU.prototype.updateTimers = function(nCycles)
{
for (var i = 0; i < this.aTimers.length; 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
}
}
};
/**
* runCPU(fUpdateFocus)
*
@ -962,22 +1068,37 @@ CPU.prototype.runCPU = function(fUpdateFocus)
this.calcStartTime();
try {
do {
var nCyclesPerBurst = (this.flags.fChecksum? 1 : this.aCounts.nCyclesPerBurst);
/*
* nCyclesPerBurst is how many cycles we WANT to run on each iteration of stepCPU(), but it may run
* significantly less (or slightly more, since we can't execute partial instructions).
*/
var nCyclesPerBurst = (this.flags.fChecksum? 1 : this.aCounts.nCyclesPerBurst);
/*
* Adjust nCyclesPerBurst if there are any CPU timers that need to fire within the current burst.
*/
nCyclesPerBurst = this.getTimerBurst(nCyclesPerBurst);
/*
* Execute the burst.
*/
this.stepCPU(nCyclesPerBurst);
/*
* nBurstCycles, less any remaining nStepCycles, is how many cycles stepCPU() ACTUALLY ran (nCycles).
* We add that to nCyclesThisRun, as well as nRunCycles, which is the cycle count since the CPU first
* started running.
* nCycles is how many cycles stepCPU() actually ran (nBurstCycles less any remaining nStepCycles).
*/
var nCycles = this.nBurstCycles - this.nStepCycles;
this.nRunCycles += nCycles;
/*
* Update any/all timers, firing those whose cycle countdowns have reached (or dropped below) zero.
*/
this.updateTimers(nCycles);
/*
* Add nCycles to nCyclesThisRun, as well as nRunCycles (the cycle count since the CPU first started).
*/
this.aCounts.nCyclesThisRun += nCycles;
this.nRunCycles += nCycles;
this.addCycles(0, true);
this.updateChecksum(nCycles);

View file

@ -60,17 +60,17 @@ var CPUDef = {
* Processor Status flag definitions (stored in regPS)
*/
PS: {
CF: 0x0001, // bit 0: Carry flag
CF: 0x0001, // bit 0: Carry Flag
BIT1: 0x0002, // bit 1: reserved, always set
PF: 0x0004, // bit 2: Parity flag
PF: 0x0004, // bit 2: Parity Flag
BIT3: 0x0008, // bit 3: reserved, always clear
AF: 0x0010, // bit 4: Auxiliary Carry flag
AF: 0x0010, // bit 4: Auxiliary Carry Flag
BIT5: 0x0020, // bit 5: reserved, always clear
ZF: 0x0040, // bit 6: Zero flag
SF: 0x0080, // bit 7: Sign flag
ZF: 0x0040, // bit 6: Zero Flag
SF: 0x0080, // bit 7: Sign Flag
ALL: 0x00D5, // all "arithmetic" flags (CF, PF, AF, ZF, SF)
MASK: 0x00FF, //
IF: 0x0200 // bit 9: Interrupt flag (set if interrupts enabled; for internal use only)
IF: 0x0200 // bit 9: Interrupt Flag (set if interrupts enabled; Intel calls this the INTE bit)
},
PARITY: [ // 256-byte array with a 1 wherever the number of set bits of the array index is EVEN
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1,
@ -94,10 +94,9 @@ var CPUDef = {
* Interrupt-related flags (stored in intFlags)
*/
INTFLAG: {
NONE: 0x00,
INTL: 0x07, // last interrupt level requested
INTR: 0x08, // set if interrupt has been requested
HALT: 0x10 // halt requested; see opHLT()
NONE: 0x0000,
INTR: 0x00ff, // mask for 8 bits, representing interrupt levels 0-7
HALT: 0x0100 // halt requested; see opHLT()
},
/*
* Opcode definitions

View file

@ -938,47 +938,50 @@ CPUState.prototype.pushWord = function(w)
CPUState.prototype.checkINTR = function()
{
if ((this.intFlags & CPUDef.INTFLAG.INTR) && this.getIF()) {
var bRST = CPUDef.OPCODE.RST0 | ((this.intFlags & CPUDef.INTFLAG.INTL) << 3);
this.intFlags &= ~CPUDef.INTFLAG.HALT;
this.clearINTR();
for (var nLevel = 0; nLevel < 8; nLevel++) {
if (this.intFlags & (1 << nLevel)) break;
}
this.clearINTR(nLevel);
this.clearIF();
this.aOps[bRST].call(this);
this.intFlags &= ~CPUDef.INTFLAG.HALT;
this.aOps[CPUDef.OPCODE.RST0 | (nLevel << 3)].call(this);
return true;
}
return false;
};
/**
* clearINTR()
* clearINTR(nLevel)
*
* Clear the corresponding interrupt level.
*
* nLevel can either be a valid interrupt level (0-7), or -1 to clear all pending interrupts
* (eg, in the event of a system-wide reset).
*
* @this {CPUState}
* @param {number} nLevel (0-7, or -1 for all)
*/
CPUState.prototype.clearINTR = function()
CPUState.prototype.clearINTR = function(nLevel)
{
this.intFlags &= ~(CPUDef.INTFLAG.INTL | CPUDef.INTFLAG.INTR);
var bitsClear = nLevel < 0? 0xff : (1 << nLevel);
this.intFlags &= ~bitsClear;
};
/**
* requestINTR(nLevel)
*
* This is called by any component that wants to request a h/w interrupt.
* Request the corresponding interrupt level.
*
* NOTE: We allow INTR to be set regardless of the current state of interrupt flag (IF), on the theory
* that if/when the CPU briefly turns interrupts off, it shouldn't lose the last h/w interrupt requested.
* So instead of ignoring INTR here, checkINTR() ignores INTR as long as the interrupt flag (IF) is clear.
*
* The downside is that, as long as the CPU has interrupts disabled, an active INTR state will slow stepCPU()
* down slightly. We could avoid that by introducing a two-stage interrupt tracking system, where a separate
* variable keeps track of the last interrupt requested whenever the interrupt flag (IF) is clear, and when
* setIF() finally occurs, that interrupt is propagated to intFlags. But for now, we're going to assume that
* scenario is rare.
* Each interrupt level (0-7) has its own intFlags bit (0-7). If one or more of those bits are set,
* and the Interrupt Flag (IF) is also set, indicating that interrupts are enabled, then checkINTR()
* chooses one of those bits, clears it, clears IF, and executes the corresponding RST opcode.
*
* @this {CPUState}
* @param {number} nLevel (0-7)
*/
CPUState.prototype.requestINTR = function(nLevel)
{
this.intFlags = (this.intFlags & ~CPUDef.INTFLAG.INTL) | nLevel | CPUDef.INTFLAG.INTR;
this.intFlags |= (1 << nLevel);
};
/**

View file

@ -115,15 +115,6 @@ function SerialPort(parmsSerial) {
Component.bindExternalControl(this, sBinding, SerialPort.sIOBuffer);
}
/*
* Define a setTimeout() function that receiveData() can use when there's more data to receive.
*/
this.fnCheckDataReceived = function(serial) {
return function() {
serial.receiveData();
}
}(this);
/*
* No connection until initBus() invokes initConnection().
*/
@ -368,10 +359,24 @@ SerialPort.prototype.initBus = function(cmp, bus, cpu, dbg)
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
var serial = this;
this.timerReceiveNext = this.cpu.addTimer(function() {
serial.printMessage("timerReceiveNext()");
serial.receiveData();
});
this.timerTransmitNext = this.cpu.addTimer(function() {
serial.printMessage("timerTransmitNext()");
serial.transmitData();
});
this.chipset = /** @type {ChipSet} */ (cmp.getMachineComponent("ChipSet"));
bus.addPortInputTable(this, SerialPort.aPortInput, this.portBase);
bus.addPortOutputTable(this, SerialPort.aPortOutput, this.portBase);
this.initConnection();
this.setReady();
};
@ -555,9 +560,11 @@ SerialPort.prototype.getBaudTimeout = function(maskRate)
*/
SerialPort.prototype.receiveByte = function(b)
{
this.printMessage("receiveByte(" + str.toHexByte(b) + "): " + str.toHexByte(this.bStatus));
if (!(this.bStatus & SerialPort.UART8251.STATUS.RECV_FULL)) {
this.bDataIn = b;
this.bStatus |= SerialPort.UART8251.STATUS.RECV_FULL;
this.printMessage("receiveByte(" + str.toHexByte(b) + "): " + str.toHexByte(this.bStatus) + " (requesting interrupt)");
this.cpu.requestINTR(this.nIRQ);
return true;
}
@ -567,6 +574,11 @@ SerialPort.prototype.receiveByte = function(b)
/**
* receiveData()
*
* Helper for clocking received data at the expected RECV_RATE.
*
* Currently, this is only use for test data that we "cram" down the terminal's throat, ensuring that we don't
* cram it too rapidly.
*
* @this {SerialPort}
*/
SerialPort.prototype.receiveData = function()
@ -575,21 +587,8 @@ SerialPort.prototype.receiveData = function()
if (this.receiveByte(this.sDataReceived.charCodeAt(0))) {
this.sDataReceived = this.sDataReceived.substr(1);
}
/*
* TODO: If data has become undeliverable for some reason (eg, the Debugger has paused execution),
* we should stop setting timeouts, and add one or more notification mechanisms to kickstart it again.
*/
if (this.sDataReceived) {
/*
* TODO: setTimeout() is a less-than-ideal solution, because it's too slow; timeouts won't fire until
* the end of a CPU burst. So instead of calculating a number of milliseconds, we should calculate a
* number of CPU cycles, and create a CPU notification mechanism that calls us back after that many cycles
* have elapsed (and which will automatically shorten the current CPU burst as needed).
*
* This will also solve the other issue noted above, because if the CPU has been halted, it won't be
* generating any notifications either.
*/
setTimeout(this.fnCheckDataReceived, this.getBaudTimeout(SerialPort.UART8251.BAUDRATES.RECV_RATE));
if (this.sDataReceived && this.cpu) {
this.cpu.setTimer(this.timerReceiveNext, this.getBaudTimeout(SerialPort.UART8251.BAUDRATES.RECV_RATE));
}
}
};
@ -612,10 +611,7 @@ SerialPort.prototype.transmitByte = function(b)
}
if (this.controlIOBuffer) {
if (b == 0x0D) {
this.iLogicalCol = 0;
}
else if (b == 0x08) {
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.
@ -623,13 +619,17 @@ SerialPort.prototype.transmitByte = function(b)
if (this.iLogicalCol > 0) this.iLogicalCol--;
}
else {
var s = String.fromCharCode(b);
var nChars = (b >= 0x20? 1 : 0);
var s = str.toASCIICode(b);
var nChars = s.length;
if (b == 0x09) {
var tabSize = this.tabSize || 8;
nChars = tabSize - (this.iLogicalCol % tabSize);
if (this.tabSize) s = str.pad("", nChars);
}
else if (b == 0x0D) {
this.iLogicalCol = nChars = 0;
s = "\n";
}
if (this.charBOL && !this.iLogicalCol && nChars) s = String.fromCharCode(this.charBOL) + s;
this.controlIOBuffer.value += s;
this.controlIOBuffer.scrollTop = this.controlIOBuffer.scrollHeight;
@ -651,6 +651,21 @@ SerialPort.prototype.transmitByte = function(b)
return fTransmitted;
};
/**
* transmitData()
*
* Helper for clocking transmitted data at the expected XMIT_RATE.
*
* When timerTransmitNext fires, we have honored the programmed XMIT_RATE period, so we can
* set XMIT_READY (and XMIT_EMPTY), which signals the firmware that another byte can be transmitted.
*
* @this {SerialPort}
*/
SerialPort.prototype.transmitData = function()
{
this.bStatus |= (SerialPort.UART8251.STATUS.XMIT_READY | SerialPort.UART8251.STATUS.XMIT_EMPTY);
};
/**
* isTransmitterReady()
*
@ -708,8 +723,20 @@ SerialPort.prototype.outData = function(port, bOut, addrFrom)
this.printMessageIO(port, bOut, addrFrom, "DATA");
this.bDataOut = bOut;
this.bStatus &= ~(SerialPort.UART8251.STATUS.XMIT_READY | SerialPort.UART8251.STATUS.XMIT_EMPTY);
if (this.transmitByte(bOut)) {
this.bStatus |= (SerialPort.UART8251.STATUS.XMIT_READY | SerialPort.UART8251.STATUS.XMIT_EMPTY);
/*
* If we're transmitting to a virtual device that has no measurable delay, this code may clear XMIT_READY
* too quickly.
*
* if (this.transmitByte(bOut)) {
* this.bStatus |= (SerialPort.UART8251.STATUS.XMIT_READY | SerialPort.UART8251.STATUS.XMIT_EMPTY);
* }
*
* A better solution is to arm a timer based on the XMIT_RATE baud rate, and clear the above bits when that
* timer fires. Consequently, we no longer care what transmitByte() reports.
*/
this.transmitByte(bOut);
if (this.cpu) {
this.cpu.setTimer(this.timerTransmitNext, this.getBaudTimeout(SerialPort.UART8251.BAUDRATES.XMIT_RATE));
}
};

View file

@ -1115,7 +1115,7 @@ Video.prototype.updateVT100 = function(fForced)
/*
* Cell cache logic is complicated by the fact that a line may be single-width one frame and double-width
* the next. So we store the visible line length at the start of each row in the cache, which must match if
* the cache is considered valid for the current line.
* the cache can be considered valid for the current line.
*/
var fLineCacheValid = this.fCellCacheValid && (this.aCellCache[iCell] == nColsVisible);
this.aCellCache[iCell++] = nColsVisible;
@ -1153,6 +1153,9 @@ Video.prototype.updateVT100 = function(fForced)
* cache entry, to guarantee that it's redrawn on the next update.
*/
this.assert(iCellUpdated >= 0);
if (DEBUG && (this.aCellCache[iCellUpdated] & 0x7f) == 0x48) {
console.log("spurious character?");
}
this.aCellCache[iCellUpdated] = -1;
cUpdated = 0;
}

View file

@ -381,7 +381,7 @@ str.replaceArray = function(a, s)
* @param {string} s is a string
* @param {number} cch is desired length
* @param {boolean} [fPadLeft] (default is padding on the right)
* @returns {string} the original string (s) with spaces padding it to the specified length
* @return {string} the original string (s) with spaces padding it to the specified length
*/
str.pad = function(s, cch, fPadLeft)
{
@ -393,7 +393,7 @@ str.pad = function(s, cch, fPadLeft)
* trim(s)
*
* @param {string} s
* @returns {string}
* @return {string}
*/
str.trim = function(s)
{
@ -403,4 +403,56 @@ str.trim = function(s)
return s.replace(/^\s+|\s+$/g, "");
};
str.aASCIICodes = {
0x00: "NUL",
0x01: "SOH", // Start of Heading
0x02: "STX", // Start of Text
0x03: "ETX", // End of Text
0x04: "EOT", // End of Transmission
0x05: "ENQ", // Enquiry
0x06: "ACK", // Acknowledge
0x07: "BEL", // Bell
0x08: "BS", // Backspace
0x09: "TAB", // Horizontal Tab
0x0A: "LF", // Line Feed (New Line)
0x0B: "VT", // Vertical Tab
0x0C: "FF", // Form Feed (New Page)
0x0D: "CR", // Carriage Return
0x0E: "SO", // Shift Out
0x0F: "SI", // Shift In
0x10: "DLE", // Data Link Escape
0x11: "DC1", // Device Control 1
0x12: "DC2", // Device Control 2
0x13: "DC3", // Device Control 3
0x14: "DC4", // Device Control 4
0x15: "NAK", // Negative Acknowledge
0x16: "SYN", // Synchronous Idle
0x17: "ETB", // End of Transmission Block
0x18: "CAN", // Cancel
0x19: "EM", // End of Medium
0x1A: "SUB", // Substitute
0x1B: "ESC", // Escape
0x1C: "FS", // File Separator
0x1D: "GS", // Group Separator
0x1E: "RS", // Record Separator
0x1F: "US" // Unit Separator
};
/**
* toASCIICode(b)
*
* @param {number} b
* @return {string}
*/
str.toASCIICode = function(b)
{
var s = str.aASCIICodes[b];
if (s) {
s = '<' + s + '>';
} else {
s = String.fromCharCode(b);
}
return s;
};
if (NODE) module.exports = str;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long