Switching to new interrupt triggers

This commit is contained in:
Jeff Parsons 2016-10-12 16:40:47 -07:00 committed by Jeff Parsons
commit 58d358b51e
7 changed files with 534 additions and 454 deletions

View file

@ -199,46 +199,34 @@ BusPDP11.IOHANDLER = {
/*
* 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 appropriate read/write handlers.
* (above) to locate the registered read/write handlers. If no handler is found, then unknownAccess()
* is called, triggering a trap -- unless traps are disabled because direct access was requested
* (eg, by the Debugger).
*
* Note that we try to have reasonable fallbacks for byte reads when only word read handlers exist,
* and word reads if only byte handlers exist. Ditto for writes. These fallbacks may not always be
* appropriate; for example, when a byte write falls back to a word write, the address must be read
* first, and depending on the underlying I/O device, that may or may not have side-effects. It's really
* up to the device to know whether that matters, and provide all the necessary handlers if it does.
* 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 zero address so that the handler can distinguish a normal read from one of these
* preliminary reads, and return an appropriate value for the update (ie, 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. What is unclear, however,
* is what is exactly supposed to happen when the CPU reads or writes a BYTE from an ODD IOPAGE address;
* it seems clear that our built-in fallbacks are NOT correct in all cases.
* 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 case-by-case basis.
*
* For example, let's imagine that only read/write WORD handlers have been registered for ODD address
* 177701 (ie, general register R1, set 0). If a readByte(177701) request is made, we would fallback to
* reading the word at 177700 and returning the high byte, but that would fetch the contents of general
* register R0 instead of R1, which is clearly wrong.
*
* One solution is for the caller to register both BYTE and WORD handlers for ODD addresses, making
* the caller responsible for the defining the correct behavior in all cases. However, that's more work
* for the caller, and we're just punting the problem instead of solving it.
*
* Another solution is for addIOHandlers() to detect the ODD address case, and install custom fallback
* handlers for read and write BYTE accesses. In the above example, the custom read BYTE handler could
* invoke the read WORD handler and mask the result with 0xff, and the custom write BYTE handler could
* first call the read WORD handler, insert the new data into the low byte of the result, and then
* call the write WORD handler.
*
* However, that would produce inconsistent results between EVEN and ODD addresses in the register
* address range (0o177700 through 0o177717), so I'm assuming that the correct solution is to install
* alternate fallback BYTE handlers for ALL addresses in that range. The alternate read BYTE handler will
* mask its result with 0xff, and the alternate write BYTE handler will store the (zero-extended) byte to
* the entire corresponding register.
*
* One of things that gives me pause about this solution is that it differs from the behavior of a MOVB
* instruction when the destination is a general register, because MOVB always sign-extends the source byte
* to a word; it does NOT zero-extend. So it seems to strange to have a different MOVB behavior when the
* destination register is specified using an IOPAGE address. But, maybe that was by design.
*
* TODO: Another small potential improvement would be for addIOHandlers() to predefine fall-backs for all
* 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
@ -336,6 +324,10 @@ BusPDP11.IOController = {
* 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, then call writeWord() with the new
* data pre-inserted in (the high byte of) the original data.
*
* WARNING: Whenever we call readWord() under these circumstances, we zero the address parameter,
* so that the handler can distinguish this case. Thus, if we're dealing with a special register
* where a byte write operation modifies the entire register, the handler can simply return zero.
*/
afn = bus.aIOHandlers[off & ~0x1];
if (afn) {
@ -484,7 +476,7 @@ BusPDP11.prototype.setIOPageRange = function(nRange)
/**
* getControllerBuffer(addr)
*
* Our Bus component also acts as custom memory controller, so it must also provide this function.
* Our Bus component also acts as custom memory controller for the IOPAGE, so it must also provide this function.
*
* @this {BusPDP11}
* @param {number} addr
@ -493,7 +485,7 @@ BusPDP11.prototype.setIOPageRange = function(nRange)
BusPDP11.prototype.getControllerBuffer = function(addr)
{
/*
* No buffer is required; all accesses go to a registered I/O handler or the bit-bucket.
* No buffer is required for the IOPAGE; all accesses go to registered I/O handlers or to unknownAccess().
*/
return [null, 0];
};
@ -501,7 +493,7 @@ BusPDP11.prototype.getControllerBuffer = function(addr)
/**
* getControllerAccess()
*
* Our Bus component also acts as custom memory controller, so it must also provide this function.
* Our Bus component also acts as custom memory controller for the IOPAGE, so it must also provide this function.
*
* @this {BusPDP11}
* @return {Array.<function()>}
@ -511,6 +503,17 @@ BusPDP11.prototype.getControllerAccess = function()
return this.afnIOPage;
};
/**
* getWidth()
*
* @this {BusPDP11}
* @return {number}
*/
BusPDP11.prototype.getWidth = function()
{
return this.nBusWidth;
};
/**
* reset()
*
@ -530,9 +533,7 @@ BusPDP11.prototype.reset = function()
* unknownAccess(addr, data, byteFlag)
*
* This is our default I/O handler, called whenever there's an IOPAGE access without a corresponding entry
* in aIOHandlers; in the interim, our Device component will override this default handler with its own function
* by calling addIODefaultHandlers(), until the Device component has been completely rewritten to install I/O
* handlers for all supported I/O addresses.
* in aIOHandlers.
*
* @this {BusPDP11}
* @param {number} addr (ie, an IOPAGE address)
@ -757,17 +758,6 @@ BusPDP11.prototype.scanMemory = function(info, addr, size)
return info;
};
/**
* getWidth()
*
* @this {BusPDP11}
* @return {number}
*/
BusPDP11.prototype.getWidth = function()
{
return this.nBusWidth;
};
/**
* removeMemory(addr, size)
*

View file

@ -83,15 +83,37 @@ function CPUStatePDP11(parmsCPU)
Component.subclass(CPUStatePDP11, CPUPDP11);
/*
* 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 process before issuing 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 simpler. First, the CPU offers timer services to any device that wants a callback
* after a specified delay, which are much more efficient than requiring the CPU to dive into an interrupt queue and
* look for (and decrement) delay counts on every instruction.
*
* Second, when a device decides it's time to interrupt (either at the end of some operation or when its delay timer
* has fired), it will simply request the interrupt, and CPU priority permitting, the interrupt will be dispatched.
* If CPU priority is NOT permitting, then the interrupt will remain pending until the priority drops. This means
* that the CPU need not check for any pending interrupts until/unless the CPU priority changes.
*
* All this will be managed with "triggers". Every device that wants to interrupt must register via addTrigger().
* This will add a Trigger object to an array, and each object will contain the (self) assigned vector, the priority,
* the interrupt request status, and a next pointer.
*/
/**
* @typedef {{
* delay: number,
* priority: number,
* vector: number,
* callback: (function()|null|undefined)
* }} InterruptEvent
* priority: number,
* request: number,
* next: (Trigger|null)
* }} Trigger
*/
var InterruptEvent;
var Trigger;
/**
* initProcessor()
@ -101,7 +123,15 @@ var InterruptEvent;
CPUStatePDP11.prototype.initProcessor = function()
{
this.decode = PDP11.op1170.bind(this);
/** @type {Array.<Trigger>} */
this.aTriggers = [];
/** @type {Trigger|null} */
this.triggerNext = null;
this.initRegs();
this.flags.complete = this.flags.debugCheck = false;
};
@ -169,7 +199,7 @@ CPUStatePDP11.prototype.initRegs = function()
this.regMB = 0;
/*
* opFlags contains various triggers that stepCPU() needs to be aware of
* opFlags contains various conditions that stepCPU() needs to be aware of
*/
this.opFlags = 0;
@ -208,8 +238,6 @@ CPUStatePDP11.prototype.resetRegs = function()
this.mmuMask = 0x3ffff;
this.mmuMemorySize = BusPDP11.IOPAGE_18BIT;
/** @type {Array.<InterruptEvent>} */
this.interruptQueue = [];
this.opFlags |= PDP11.OPFLAG.INTQ;
if (this.bus) this.setMemoryAccess();
@ -562,77 +590,131 @@ CPUStatePDP11.prototype.setSP = function(addr)
};
/**
* interrupt(delay, priority, vector, callback)
*
* Interrupts are stored in a queue in delay order with the delay expressed as
* a difference. For example if the delays were 0, 1, 0 then the first entry
* is active and both the second and third are waiting for one more instruction
* execution to become active.
* addTrigger(vector, priority)
*
* @this {CPUStatePDP11}
* @param {number} delay
* @param {number} priority
* @param {number} vector
* @param {function()} [callback]
* @param {number} priority
* @return {Trigger}
*/
CPUStatePDP11.prototype.interrupt = function(delay, priority, vector, callback)
CPUStatePDP11.prototype.addTrigger = function(vector, priority)
{
var i = this.interruptQueue.length;
while (i-- > 0) {
if (this.interruptQueue[i].vector === vector) {
if (i > 0) {
this.interruptQueue[i - 1].delay += this.interruptQueue[i].delay;
}
this.interruptQueue.splice(i, 1);
break;
}
}
if (delay >= 0) {
i = this.interruptQueue.length; // queue in delay 'difference' order
while (i-- > 0) {
if (this.interruptQueue[i].delay > delay) {
this.interruptQueue[i].delay -= delay;
break;
}
delay -= this.interruptQueue[i].delay;
}
/*
* NOTE regarding a Google Closure Compiler "bug": if an InterruptEvent is inserted into the
* interruptQueue with the named properties below, they will never be seen by the rest of the
* code, because the compiler renames all other property references EXCEPT these.
*
* this.interruptQueue.splice(i + 1, 0, {
* "delay": delay,
* "priority": (priority << 5) & 0xe0,
* "vector": vector,
* "callback": callback
* });
*
* Perhaps if the inlined object had been explicitly @typed, that wouldn't have happened, but
* I'm playing it safe now: I've taken the object out-of-line, removed the quoted property names,
* and explicitly typed it. The compiler re-inlines the object with correctly renamed properties.
*/
/** @type {InterruptEvent} */
var interruptEvent = {
delay: delay,
priority: (priority << 5) & 0xe0,
vector: vector,
callback: callback
};
this.interruptQueue.splice(i + 1, 0, interruptEvent);
}
this.opFlags |= PDP11.OPFLAG.INTQ;
var trigger = {vector: vector, priority: priority, request: -1, next: null};
this.aTriggers.push(trigger);
return trigger;
};
/**
* checkInterruptQueue()
* checkTriggers()
*
* @this {CPUStatePDP11}
*/
CPUStatePDP11.prototype.checkInterruptQueue = function()
CPUStatePDP11.prototype.checkTriggers = function()
{
if (this.triggerNext && this.setTrigger(this.triggerNext)) {
this.removeTrigger(this.triggerNext);
}
};
/**
* insertTrigger(trigger)
*
* @this {CPUStatePDP11}
* @param {Trigger} trigger
*/
CPUStatePDP11.prototype.insertTrigger = function(trigger)
{
if (trigger != this.triggerNext) {
var triggerPrev = this.triggerNext;
if (!triggerPrev || triggerPrev.priority <= trigger.priority) {
trigger.next = triggerPrev;
this.triggerNext = trigger;
} else {
do {
var triggerNext = triggerPrev.next;
if (!triggerNext || triggerNext.priority <= trigger.priority) {
trigger.next = triggerNext;
triggerPrev.next = trigger;
break;
}
triggerPrev = triggerNext;
} while (true);
}
}
};
/**
* removeTrigger(trigger)
*
* @this {CPUStatePDP11}
* @param {Trigger} trigger
*/
CPUStatePDP11.prototype.removeTrigger = function(trigger)
{
var triggerPrev = this.triggerNext;
if (triggerPrev == trigger) {
this.triggerNext = trigger.next;
} else {
do {
var triggerNext = triggerPrev.next;
if (triggerNext == trigger) {
triggerPrev.next = triggerNext.next;
break;
}
triggerPrev = triggerNext;
} while (true);
}
};
/**
* setTrigger(trigger)
*
* @this {CPUStatePDP11}
* @param {Trigger|null} trigger
* @return {boolean} (true if interrupt dispatched, false if not)
*/
CPUStatePDP11.prototype.setTrigger = function(trigger)
{
if (!this.dispatchInterrupt(trigger.vector, trigger.priority)) {
this.insertTrigger(trigger);
return false;
}
return true;
};
/**
* dispatchInterrupt(vector, priority)
*
* @this {CPUStatePDP11}
* @param {number} vector
* @param {number} priority
* @return {boolean} (true if dispatched, false if not)
*/
CPUStatePDP11.prototype.dispatchInterrupt = function(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, PDP11.REASON.INTERRUPT);
return true;
}
return false;
};
/**
* checkInterrupts()
*
* @this {CPUStatePDP11}
*/
CPUStatePDP11.prototype.checkInterrupts = function()
{
if (this.opFlags & PDP11.OPFLAG.INTQ) {
this.opFlags &= ~PDP11.OPFLAG.INTQ;
this.checkTriggers();
/*
var interruptEvent = null;
var savePSW = this.regPIR & 0xe0;
for (var i = this.interruptQueue.length; --i >= 0;) {
@ -666,6 +748,7 @@ CPUStatePDP11.prototype.checkInterruptQueue = function()
this.trap(interruptEvent.vector, PDP11.REASON.INTERRUPT);
}
}
*/
}
else if (this.opFlags & PDP11.OPFLAG.INTQ_SPL) {
/*
@ -735,7 +818,7 @@ CPUStatePDP11.prototype.setPSW = function(newPSW)
this.regsGen[6] = this.regsAltStack[this.mmuMode];
}
/*
* Trigger a call to checkInterruptQueue()
* Trigger a call to checkInterrupts()
*/
this.opFlags |= PDP11.OPFLAG.INTQ;
this.regPSW = newPSW;
@ -1017,7 +1100,7 @@ CPUStatePDP11.prototype.trap = function(vector, reason)
}
}
throw vector;
if (reason != PDP11.REASON.INTERRUPT) throw vector;
};
/**
@ -1891,7 +1974,7 @@ CPUStatePDP11.prototype.stepCPU = function(nMinCycles)
* interrupting the natural flow of instructions whenever the Debugger is stepping through code.
*/
if ((this.opFlags & (PDP11.OPFLAG.INTQ_SPL | PDP11.OPFLAG.INTQ | PDP11.OPFLAG.WAIT)) /*&& nMinCycles*/) {
this.checkInterruptQueue();
this.checkInterrupts();
}
}

View file

@ -506,7 +506,6 @@ var PDP11 = {
PRI: 4,
RVEC: 0o60,
XVEC: 0o64,
DELAY: 40,
RCSR: { // 177560
RE: 0x0001, // Reader Enable (W/O) TODO: Determine if we really need to exclude this write-only bit from RMASK
DTR: 0x0002, // Data Terminal Ready (R/W)
@ -529,7 +528,8 @@ var PDP11 = {
PARITY: 0x1000, // Received Data Parity (R/O)
FE: 0x2000, // Framing Error (R/O)
OE: 0x4000, // Overrun Error (R/O)
ERROR: 0x8000 // Error (R/O)
ERROR: 0x8000, // Error (R/O)
DELAY: 1
},
XCSR: { // 177564
BREAK: 0x0001, // BREAK (R/W)
@ -538,11 +538,11 @@ var PDP11 = {
READY: 0x0080, // Transmitter Ready (R/O)
RMASK: 0x00C5,
WMASK: 0x0045,
DELAY: 8
DELAY: 1
},
XBUF: { // 177566
DATA: 0x00FF, // Transmitted Data (W/O) TODO: Determine why pdp11.js effectively defined this as 0x7F
DELAY: 100
DELAY: 1
}
},
KW11: { // KW11-L Line Time Clock

View file

@ -125,10 +125,12 @@ DevicePDP11.prototype.initBus = function(cmp, bus, cpu, dbg)
this.dbg = dbg;
var device = this;
this.kw11.timer = this.cpu.addTimer(function() {
this.kw11.timer = cpu.addTimer(function() {
device.kw11_interrupt();
});
this.kw11.trigger = cpu.addTrigger(PDP11.KW11.VEC, PDP11.KW11.PRI);
bus.addIOTable(this, DevicePDP11.UNIBUS_IOTABLE);
bus.addResetHandler(this.reset.bind(this));
@ -1008,7 +1010,7 @@ DevicePDP11.prototype.kw11_interrupt = function()
{
this.kw11.lks |= PDP11.KW11.LKS.MON;
if (this.kw11.lks & PDP11.KW11.LKS.IE) {
this.cpu.interrupt(PDP11.KW11.DELAY, PDP11.KW11.PRI, PDP11.KW11.VEC);
this.cpu.setTrigger(this.kw11.trigger);
this.cpu.setTimer(this.kw11.timer, 1000/60);
}
};

View file

@ -260,22 +260,28 @@ SerialPortPDP11.prototype.initBus = function(cmp, bus, cpu, dbg)
var serial = this;
this.timerReceiveData = this.cpu.addTimer(function() {
this.triggerReceiveInterrupt = this.cpu.addTrigger(PDP11.DL11.RVEC, PDP11.DL11.PRI);
this.timerReceiveInterrupt = this.cpu.addTimer(function() {
if (!(serial.rcsr & PDP11.DL11.RCSR.RD)) {
if (serial.abReceive.length) {
serial.rbuf = serial.abReceive.shift();
serial.rcsr |= PDP11.DL11.RCSR.RD;
if (serial.rcsr & PDP11.DL11.RCSR.RIE) {
serial.cpu.interrupt(PDP11.DL11.DELAY, PDP11.DL11.PRI, PDP11.DL11.RVEC);
serial.cpu.setTrigger(serial.triggerReceiveInterrupt);
}
}
}
});
this.doneTransmitInterrupt = function() {
this.triggerTransmitInterrupt = this.cpu.addTrigger(PDP11.DL11.XVEC, PDP11.DL11.PRI);
this.timerTransmitInterrupt = this.cpu.addTimer(function() {
serial.xcsr |= PDP11.DL11.XCSR.READY;
return !!(serial.xcsr & PDP11.DL11.XCSR.TIE);
};
if (serial.xcsr & PDP11.DL11.XCSR.TIE) {
serial.cpu.setTrigger(serial.triggerTransmitInterrupt);
}
});
bus.addIOTable(this, SerialPortPDP11.UNIBUS_IOTABLE);
this.setReady();
@ -465,7 +471,7 @@ SerialPortPDP11.prototype.receiveData = function(data)
else {
this.abReceive = this.abReceive.concat(data);
}
this.cpu.setTimer(this.timerReceiveData, 50);
this.cpu.setTimer(this.timerReceiveInterrupt, PDP11.DL11.RBUF.DELAY);
return true; // for now, return true regardless, since we're buffering everything anyway
};
@ -568,7 +574,7 @@ SerialPortPDP11.prototype.readRBUF = function(addr)
{
this.rcsr &= ~PDP11.DL11.RCSR.RD;
if (this.abReceive.length > 0) {
this.cpu.setTimer(this.timerReceiveData, 50);
this.cpu.setTimer(this.timerReceiveInterrupt, PDP11.DL11.RBUF.DELAY);
}
return this.rbuf;
};
@ -609,7 +615,7 @@ SerialPortPDP11.prototype.writeXCSR = function(data, addr)
* If the device is READY, and IE is transitioning on, then request an interrupt.
*/
if ((this.xcsr & (PDP11.DL11.XCSR.READY | PDP11.DL11.XCSR.TIE)) == PDP11.DL11.XCSR.READY && (data & PDP11.DL11.XCSR.TIE)) {
this.cpu.interrupt(PDP11.DL11.XCSR.DELAY, PDP11.DL11.PRI, PDP11.DL11.XVEC, this.doneTransmitInterrupt);
this.cpu.setTimer(this.timerTransmitInterrupt, PDP11.DL11.XCSR.DELAY);
}
this.xcsr = (this.xcsr & ~PDP11.DL11.XCSR.WMASK) | (data & PDP11.DL11.XCSR.WMASK);
};
@ -639,7 +645,7 @@ SerialPortPDP11.prototype.writeXBUF = function(data, addr)
if (data) {
this.transmitByte(data);
this.xcsr &= ~PDP11.DL11.XCSR.READY;
this.cpu.interrupt(PDP11.DL11.XBUF.DELAY, PDP11.DL11.PRI, PDP11.DL11.XVEC, this.doneTransmitInterrupt);
this.cpu.setTimer(this.timerTransmitInterrupt, PDP11.DL11.XBUF.DELAY);
}
};