Fixed lost 8080 interrupts, and added new CPU-driven setTimer() functionality

This commit is contained in:
Jeff Parsons 2016-08-17 11:58:01 -07:00
commit 99023617fd
6 changed files with 562 additions and 447 deletions

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,20 @@ SerialPort.prototype.initBus = function(cmp, bus, cpu, dbg)
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
var serial = this;
this.timerReceiveData = this.cpu.addTimer(function() {
serial.printMessage("timerReceiveData()");
serial.receiveData()
});
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 +556,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;
}
@ -575,21 +578,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.timerReceiveData, this.getBaudTimeout(SerialPort.UART8251.BAUDRATES.RECV_RATE));
}
}
};