Infrastructure in place for 8080 hardware interrupts (more specifically, vertical retrace interrupts)

This commit is contained in:
Jeff Parsons 2016-04-25 18:39:43 -07:00
commit 07259c93ef
17 changed files with 1072 additions and 434 deletions

View file

@ -0,0 +1,486 @@
/**
* @fileoverview Implements the PC8080 ChipSet component.
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
* @version 1.0
* Created 2016-Apr-25
*
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
*
* This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines)
* at <http://jsmachines.net/> and <http://pcjs.org/>.
*
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCjs. If not,
* see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every source code file of every
* copy or modified version of this work, and to display that copyright notice on every screen
* that loads or runs any version of this software (see Computer.COPYRIGHT).
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of the
* PCjs program for purposes of the GNU General Public License, and the author does not claim
* any copyright as to their contents.
*/
"use strict";
if (NODE) {
var str = require("../../shared/lib/strlib");
var usr = require("../../shared/lib/usrlib");
var web = require("../../shared/lib/weblib");
var Component = require("../../shared/lib/component");
var Messages = require("./messages");
var State = require("./state");
var CPUDef = require("./cpudef");
}
/**
* ChipSet(parmsChipSet)
*
* The ChipSet component has the following component-specific (parmsChipSet) properties:
*
* model: eg, "SI1978" (should be a member of ChipSet.MODELS)
* swDIP: eg, "00000000", where swDIP[0] is DIP0, swDIP[1] is DIP1, etc.
*
* @constructor
* @extends Component
* @param {Object} parmsChipSet
*/
function ChipSet(parmsChipSet)
{
Component.call(this, "ChipSet", parmsChipSet, ChipSet, Messages.CHIPSET);
var model = parmsChipSet['model'];
/*
* this.model is a numeric version of the 'model' string; when comparing this.model to standard IBM
* model numbers, you should generally compare (this.model|0) to the target value, which truncates it.
*/
if (model && !ChipSet.MODELS[model]) {
Component.notice("Unrecognized ChipSet model: " + model);
}
this.model = ChipSet.MODELS[model];
this.bSwitches = this.parseDIPSwitches(parmsChipSet['swDIP']);
/*
* Here, I'm finally getting around to trying the Web Audio API. Fortunately, based on what little I know about
* sound generation, using the API to make the same noises as the IBM PC speaker seems straightforward.
*
* To start, we create an audio context, unless the 'sound' parameter has been explicitly set to false.
*
* From:
*
* http://developer.apple.com/library/safari/#documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/PlayingandSynthesizingSounds/PlayingandSynthesizingSounds.html
*
* "Similar to how HTML5 canvas requires a context on which lines and curves are drawn, Web Audio requires an audio context
* on which sounds are played and manipulated. This context will be the parent object of further audio objects to come....
* Your audio context is typically created when your page initializes and should be long-lived. You can play multiple sounds
* coming from multiple sources within the same context, so it is unnecessary to create more than one audio context per page."
*/
this.fSpeaker = false;
if (parmsChipSet['sound']) {
this.classAudio = this.contextAudio = null;
if (window) {
this.classAudio = window['AudioContext'] || window['webkitAudioContext'];
}
if (this.classAudio) {
this.contextAudio = new this.classAudio();
} else {
if (DEBUG) this.log("AudioContext not available");
}
}
this.setReady();
}
Component.subclass(ChipSet);
ChipSet.SI_1978 = {
MODEL: 1978.1,
STATUS0: {
PORT: 0,
DIP4: 0x01, // self-test request at power up?
ALWAYS_SET: 0x0E, // always set
FIRE: 0x10, // fire
LEFT: 0x20, // move left
RIGHT: 0x40, // move right
PORT7: 0x80 // some connection to port 7?
},
STATUS1: {
PORT: 1,
CREDIT: 0x01, // credit
P1: 0x02, // 1P start
P2: 0x04, // 2P start
ALWAYS_SET: 0x08, // always set
P1_FIRE: 0x10, // P1 fire (cocktail machines only?)
P1_LEFT: 0x20, // P1 left (cocktail machines only?)
P1_RIGHT: 0x40 // P1 right (cocktail machines only?)
},
STATUS2: {
PORT: 2,
DIP3_5: 0x03, // 00 = 3 ships, 01 = 4 ships, 10 = 5 ships, 11 = 6 ships
TILT: 0x04,
DIP6: 0x08, // 0 = extra ship at 1500, 1 = extra ship at 1000
P2_FIRE: 0x10, // P2 fire (cocktail machines only?)
P2_LEFT: 0x20, // P2 left (cocktail machines only?)
P2_RIGHT: 0x40, // P2 right (cocktail machines only?)
DIP7: 0x80 // 0 = display coin info on demo ("attract") screen
},
SHIFT_RESULT: { // bits 0-7 of barrel shifter result
PORT: 3
},
SHIFT_COUNT: {
PORT: 2,
MASK: 0x07
},
SOUND1: {
PORT: 3,
UFO: 0x01,
SHOT: 0x02,
PDEATH: 0x04,
IDEATH: 0x08,
EXPLAY: 0x10,
AMP_ENABLE: 0x20
},
SHIFT_DATA: {
PORT: 4
},
SOUND2: {
PORT: 5,
FLEET1: 0x01,
FLEET2: 0x02,
FLEET3: 0x04,
FLEET4: 0x08,
UFO_HIT: 0x10
}
};
/*
* Supported model strings
*/
ChipSet.MODELS = {
"SI1978": ChipSet.SI_1978.MODEL
};
/**
* parseDIPSwitches(sBits, bDefault)
*
* @this {ChipSet}
* @param {string} sBits describing switch settings
* @param {number} [bDefault]
* @return {number|undefined}
*/
ChipSet.prototype.parseDIPSwitches = function(sBits, bDefault)
{
var b = bDefault;
if (sBits) {
/*
* NOTE: We can't use parseInt() with a base of 2, because both bit order and bit sense are reversed.
*/
b = 0;
var bit = 0x1;
for (var i = 0; i < sBits.length; i++) {
if (sBits.charAt(i) == "0") b |= bit;
bit <<= 1;
}
}
return b;
};
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {ChipSet}
* @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, "sw1")
* @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
*/
ChipSet.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
{
return false;
};
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {ChipSet}
* @param {Computer} cmp
* @param {Bus} bus
* @param {CPUSim} cpu
* @param {Debugger} dbg
*/
ChipSet.prototype.initBus = function(cmp, bus, cpu, dbg)
{
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
this.cmp = cmp;
if (this.model == ChipSet.SI_1978.MODEL) {
bus.addPortInputTable(this, ChipSet.aPortInput);
bus.addPortOutputTable(this, ChipSet.aPortOutput);
}
};
/**
* powerUp(data, fRepower)
*
* @this {ChipSet}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
ChipSet.prototype.powerUp = function(data, fRepower)
{
if (!fRepower) {
if (!data) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
};
/**
* powerDown(fSave, fShutdown)
*
* @this {ChipSet}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
ChipSet.prototype.powerDown = function(fSave, fShutdown)
{
return fSave? this.save() : true;
};
/**
* reset()
*
* @this {ChipSet}
*/
ChipSet.prototype.reset = function()
{
this.bStatus0 = 0;
this.bStatus1 = 0;
this.bStatus2 = 0;
this.bShiftData = 0;
this.bShiftCount = 0;
this.bSound1 = this.bSound2 = 0;
};
/**
* save()
*
* This implements save support for the ChipSet component.
*
* @this {ChipSet}
* @return {Object}
*/
ChipSet.prototype.save = function()
{
var state = new State(this);
if (this.model == ChipSet.SI_1978.MODEL) {
state.set(0, [this.bStatus0, this.bStatus1, this.bStatus2, this.bShiftData, this.bShiftCount, this.bSound1, this.bSound2]);
}
return state.data();
};
/**
* restore(data)
*
* This implements restore support for the ChipSet component.
*
* @this {ChipSet}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
ChipSet.prototype.restore = function(data)
{
var a, i;
a = data[0];
if (this.model == ChipSet.SI_1978.MODEL) {
this.bStatus0 = a[0];
this.bStatus1 = a[1];
this.bStatus2 = a[2];
this.bShiftData = a[3];
this.bShiftCount = a[4];
this.bSound1 = a[5];
this.bSound2 = a[6];
}
return true;
};
/**
* inSIStatus0(port, addrFrom)
*
* @this {ChipSet}
* @param {number} port (0x00)
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
* @return {number} simulated port value
*/
ChipSet.prototype.inSIStatus0 = function(port, addrFrom)
{
var b = this.bStatus0;
this.printMessageIO(port, null, addrFrom, "STATUS0", b, true);
return b;
};
/**
* inSIStatus1(port, addrFrom)
*
* @this {ChipSet}
* @param {number} port (0x01)
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
* @return {number} simulated port value
*/
ChipSet.prototype.inSIStatus1 = function(port, addrFrom)
{
var b = this.bStatus1;
this.printMessageIO(port, null, addrFrom, "STATUS1", b, true);
return b;
};
/**
* inSIStatus2(port, addrFrom)
*
* @this {ChipSet}
* @param {number} port (0x02)
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
* @return {number} simulated port value
*/
ChipSet.prototype.inSIStatus2 = function(port, addrFrom)
{
var b = this.bStatus2;
this.printMessageIO(port, null, addrFrom, "STATUS2", b, true);
return b;
};
/**
* inSIShiftResult(port, addrFrom)
*
* @this {ChipSet}
* @param {number} port (0x03)
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
* @return {number} simulated port value
*/
ChipSet.prototype.inSIShiftResult = function(port, addrFrom)
{
var b = ((this.bShiftData << this.bShiftCount) >> 8) & 0xff;
this.printMessageIO(port, null, addrFrom, "SHIFT.RESULT", b, true);
return b;
};
/**
* outSIShiftCount(port, b, addrFrom)
*
* @this {ChipSet}
* @param {number} port (0x02)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
ChipSet.prototype.outSIShiftCount = function(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "SHIFT.COUNT", null, true);
this.bShiftCount = b;
};
/**
* outSISound1(port, b, addrFrom)
*
* @this {ChipSet}
* @param {number} port (0x03)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
ChipSet.prototype.outSISound1 = function(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "SOUND1", null, true);
this.bSound1 = b;
};
/**
* outSIShiftData(port, b, addrFrom)
*
* @this {ChipSet}
* @param {number} port (0x04)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
ChipSet.prototype.outSIShiftData = function(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "SHIFT.DATA", null, true);
this.bShiftData = b;
};
/**
* outSISound2(port, b, addrFrom)
*
* @this {ChipSet}
* @param {number} port (0x05)
* @param {number} b
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
*/
ChipSet.prototype.outSISound2 = function(port, b, addrFrom)
{
this.printMessageIO(port, b, addrFrom, "SOUND2", null, true);
this.bSound2 = b;
};
/*
* Port input notification tables
*/
ChipSet.aPortInput = {
0x00: ChipSet.prototype.inSIStatus0,
0x01: ChipSet.prototype.inSIStatus1,
0x02: ChipSet.prototype.inSIStatus2,
0x03: ChipSet.prototype.inSIShiftResult
};
/*
* Port output notification tables
*/
ChipSet.aPortOutput = {
0x02: ChipSet.prototype.outSIShiftCount,
0x03: ChipSet.prototype.outSISound1,
0x04: ChipSet.prototype.outSIShiftData,
0x05: ChipSet.prototype.outSISound2
};
/**
* ChipSet.init()
*
* This function operates on every HTML element of class "chipset", extracting the
* JSON-encoded parameters for the ChipSet constructor from the element's "data-value"
* attribute, invoking the constructor to create a ChipSet component, and then binding
* any associated HTML controls to the new component.
*/
ChipSet.init = function()
{
var aeChipSet = Component.getElementsByClass(document, PCJSCLASS, "chipset");
for (var iChip = 0; iChip < aeChipSet.length; iChip++) {
var eChipSet = aeChipSet[iChip];
var parmsChipSet = Component.getComponentParms(eChipSet);
var chipset = new ChipSet(parmsChipSet);
Component.bindComponentControls(chipset, eChipSet, PCJSCLASS);
}
};
/*
* Initialize every ChipSet module on the page.
*/
web.onInit(ChipSet.init);
if (NODE) module.exports = ChipSet;

View file

@ -1479,22 +1479,18 @@ Computer.prototype.updateStatus = function(fForce)
};
/**
* updateVideo(fForce)
* updateVideo(n)
*
* Any high-frequency updates should be performed here. Avoid DOM updates, since updateVideo() can be called up to
* 60 times per second (see VIDEO_UPDATES_PER_SECOND).
* Any high-frequency updates should be performed here (avoid updating DOM elements).
*
* @this {Computer}
* @param {boolean} [fForce] (true to force a video update)
* @param {number} n (where 0 <= n < VIDEO_UPDATES_PER_SECOND for a normal update, or -1 for a forced update)
*/
Computer.prototype.updateVideo = function(fForce)
Computer.prototype.updateVideo = function(n)
{
/*
for (var i = 0; i < this.aVideo.length; i++) {
this.aVideo[i].updateScreen(fForce);
this.aVideo[i].updateScreen(n);
}
if (this.panel) this.panel.updateAnimation();
*/
};
/**

View file

@ -82,6 +82,7 @@ function CPU(parmsCPU, nCyclesDefault)
this.aCounts = {};
this.aCounts.nCyclesPerSecond = nCycles;
this.aCounts.nVideoUpdates = 0;
/*
* nCyclesMultiplier replaces the old "speed" variable (0, 1, 2) and eliminates the need for
@ -148,7 +149,7 @@ Component.subclass(CPU);
* this.aCounts.nCyclesNextStatusUpdate <= this.aCounts.nCyclesPerStatusUpdate
*/
CPU.YIELDS_PER_SECOND = 30;
CPU.VIDEO_UPDATES_PER_SECOND = 60; // WARNING: if you change this, beware of side-effects in the Video component
CPU.VIDEO_UPDATES_PER_SECOND = 60;
CPU.STATUS_UPDATES_PER_SECOND = 2;
CPU.BUTTONS = ["power", "reset"];
@ -173,13 +174,14 @@ CPU.prototype.initBus = function(cmp, bus, cpu, dbg)
if (control) this.cmp.setBinding(null, CPU.BUTTONS[i], control);
}
this.fpu = cmp.getMachineComponent("FPU");
/*
* We need to know the refresh rate (and corresponding interrupt rate, if any) of the Video component.
*/
var video = cmp.getMachineComponent("Video");
this.refreshRate = video && video.getRefreshRate() || CPU.VIDEO_UPDATES_PER_SECOND;
/*
* Attach the ChipSet component to the CPU so that it can obtain the IDT vector number
* of pending hardware interrupts in response to the ChipSet's updateINTR() notifications.
*
* We must also call chipset.updateAllTimers() periodically; stepCPU() takes care of that.
* Attach the ChipSet component to the CPU so that it can be notified whenever the CPU stops and starts.
*/
this.chipset = cmp.getMachineComponent("ChipSet");
@ -197,12 +199,11 @@ CPU.prototype.initBus = function(cmp, bus, cpu, dbg)
/**
* reset()
*
* This is a placeholder for reset (overridden by the CPUSim component).
*
* @this {CPU}
*/
CPU.prototype.reset = function()
{
this.aCounts.nVideoUpdates = 0;
};
/**
@ -550,7 +551,7 @@ CPU.prototype.setBurstCycles = function(nCycles)
* costs are being properly assessed, then we need to update nSnapCycles as well.
*
* TODO: If the delta is negative, we could simply ignore the request, but we must first carefully
* consider the impact on the ChipSet timers.
* consider the impact on the ChipSet timers, if any.
*/
// if (DEBUG) this.nSnapCycles -= nDelta;
this.nStepCycles -= nDelta;
@ -606,7 +607,7 @@ CPU.prototype.calcCycles = function(fRecalc)
* Calculate the most cycles we're allowed to execute in a single "burst"
*/
var nMostUpdatesPerSecond = CPU.YIELDS_PER_SECOND;
if (nMostUpdatesPerSecond < CPU.VIDEO_UPDATES_PER_SECOND) nMostUpdatesPerSecond = CPU.VIDEO_UPDATES_PER_SECOND;
if (nMostUpdatesPerSecond < this.refreshRate) nMostUpdatesPerSecond = this.refreshRate;
if (nMostUpdatesPerSecond < CPU.STATUS_UPDATES_PER_SECOND) nMostUpdatesPerSecond = CPU.STATUS_UPDATES_PER_SECOND;
/*
@ -622,7 +623,7 @@ CPU.prototype.calcCycles = function(fRecalc)
this.aCounts.msPerYield = Math.round(1000 / CPU.YIELDS_PER_SECOND);
this.aCounts.nCyclesPerBurst = Math.floor(this.aCounts.nCyclesPerSecond / nMostUpdatesPerSecond * vMultiplier);
this.aCounts.nCyclesPerYield = Math.floor(this.aCounts.nCyclesPerSecond / CPU.YIELDS_PER_SECOND * vMultiplier);
this.aCounts.nCyclesPerVideoUpdate = Math.floor(this.aCounts.nCyclesPerSecond / CPU.VIDEO_UPDATES_PER_SECOND * vMultiplier);
this.aCounts.nCyclesPerVideoUpdate = Math.floor(this.aCounts.nCyclesPerSecond / this.refreshRate * vMultiplier);
this.aCounts.nCyclesPerStatusUpdate = Math.floor(this.aCounts.nCyclesPerSecond / CPU.STATUS_UPDATES_PER_SECOND * vMultiplier);
/*
@ -807,7 +808,6 @@ CPU.prototype.calcSpeed = function(nCycles, msElapsed)
this.aCounts.mhz = Math.round(nCycles / (msElapsed * 10)) / 100;
if (msElapsed >= 86400000) {
this.nTotalCycles = 0;
if (this.chipset) this.chipset.updateAllTimers(true);
this.setSpeed(); // reset all counters once per day so that we never have to worry about overflow
}
}
@ -965,12 +965,6 @@ CPU.prototype.runCPU = function(fUpdateFocus)
do {
var nCyclesPerBurst = (this.flags.fChecksum? 1 : this.aCounts.nCyclesPerBurst);
if (this.chipset) {
this.chipset.updateAllTimers();
nCyclesPerBurst = this.chipset.getTimerCycleLimit(0, nCyclesPerBurst);
nCyclesPerBurst = this.chipset.getRTCCycleLimit(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).
@ -1005,7 +999,8 @@ CPU.prototype.runCPU = function(fUpdateFocus)
this.aCounts.nCyclesNextVideoUpdate -= nCycles;
if (this.aCounts.nCyclesNextVideoUpdate <= 0) {
this.aCounts.nCyclesNextVideoUpdate += this.aCounts.nCyclesPerVideoUpdate;
if (this.cmp) this.cmp.updateVideo();
if (this.cmp) this.cmp.updateVideo(this.aCounts.nVideoUpdates++);
if (this.aCounts.nVideoUpdates > this.refreshRate) this.aCounts.nVideoUpdates = 0;
}
this.aCounts.nCyclesNextStatusUpdate -= nCycles;
@ -1117,7 +1112,7 @@ CPU.prototype.stopCPU = function(fComplete)
CPU.prototype.updateCPU = function(fForce)
{
if (this.cmp) {
this.cmp.updateVideo(fForce);
this.cmp.updateVideo(-1);
this.cmp.updateStatus(fForce);
}
};

View file

@ -91,28 +91,22 @@ var CPUDef = {
0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1
],
/*
* Bit values for opFlags, which are all reset to zero prior to each instruction
*/
OPFLAG: {
NOREAD: 0x0001, // disable memory reads for the remainder of the current instruction
NOWRITE: 0x0002, // disable memory writes for the remainder of the current instruction
NOINTR: 0x0004 // a segreg has been set, or a prefix, or an STI (delay INTR acknowledgement)
},
/*
* Bit values for intFlags
*/
INTFLAG: {
NONE: 0x00,
INTR: 0x01, // h/w interrupt requested
HALT: 0x04 // halt (HLT) requested
INTL: 0x07, // last interrupt level requested
INTR: 0x08, // set if interrupt has been requested
HALT: 0x10 // halt requested; see the HLT opcode
},
/*
* Opcode definitions
*/
OPCODE: {
ACI: 0xCE, // PS.ALL
CALL: 0xCD
CALL: 0xCD,
RST0: 0xC7
// to be continued....
}
};

View file

@ -2747,7 +2747,7 @@ CPUDef.opCP = function()
*/
CPUDef.opPUPSW = function()
{
this.pushWord(this.getPS() | (this.regA << 8));
this.pushWord((this.getPS() & 0xff) | (this.regA << 8));
this.nStepCycles -= 11;
};

View file

@ -226,6 +226,7 @@ CPUSim.prototype.reset = function()
this.resetRegs();
this.resetCycles();
this.clearError(); // clear any fatal error/exception that setError() may have flagged
this.parent.reset.call(this);
};
/**
@ -255,8 +256,6 @@ CPUSim.prototype.resetRegs = function()
* intFlags contains some internal states we use to indicate whether a hardware interrupt (INTFLAG.INTR) or
* Trap software interrupt (INTR.TRAP) has been requested, as well as when we're in a "HLT" state (INTFLAG.HALT)
* that requires us to wait for a hardware interrupt (INTFLAG.INTR) before continuing execution.
*
* intFlags must be cleared only by checkINTR(), whereas opFlags must be cleared prior to every CPU operation.
*/
this.intFlags = CPUDef.INTFLAG.NONE;
};
@ -286,7 +285,7 @@ CPUSim.prototype.save = function()
{
var state = new State(this);
state.set(0, [this.regA, this.regB, this.regC, this.regD, this.regE, this.regH, this.regL, this.getSP(), this.getPC(), this.getPS()]);
state.set(1, [this.opFlags, this.intFlags, this.nTotalCycles, this.getSpeed()]);
state.set(1, [this.intFlags, this.nTotalCycles, this.getSpeed()]);
state.set(2, this.bus.saveMemory());
return state.data();
};
@ -314,9 +313,8 @@ CPUSim.prototype.restore = function(data)
this.setPC(a[8]);
this.setPS(a[9]);
a = data[1];
this.opFlags = a[0];
this.intFlags = a[1];
this.nTotalCycles = a[2];
this.intFlags = a[0];
this.nTotalCycles = a[1];
this.setSpeed(a[3]);
return this.bus.restoreMemory(data[2]);
};
@ -965,39 +963,52 @@ CPUSim.prototype.pushWord = function(w)
/**
* checkINTR()
*
* This must only be called when intFlags (containing the simulated INTFLAG.INTR signal) is known to be set.
* Note that it's perfectly possible that between the time updateINTR(true) was called and we request the
* interrupt vector number below, the interrupt could have been cleared or masked, in which case getIRRVector()
* will return -1 and we'll simply clear INTFLAG.INTR.
*
* @this {CPUSim}
* @return {boolean} true if h/w interrupt (or trap) has just been acknowledged, false if not
* @return {boolean} true if h/w interrupt has just been acknowledged, false if not
*/
CPUSim.prototype.checkINTR = function()
{
if ((this.intFlags & CPUDef.INTFLAG.INTR) && this.getIF()) {
var bRST = CPUDef.OPCODE.RST0 | ((this.intFlags & CPUDef.INTFLAG.INTL) << 3);
this.clearINTR();
this.clearIF();
this.aOps[bRST].call(this);
return true;
}
return false;
};
/**
* updateINTR(fRaise)
*
* This is called by the ChipSet component whenever a h/w interrupt needs to be simulated.
* This is how the PIC component simulates raising the INTFLAG.INTR signal. We will honor the request
* only if we have a reference back to the ChipSet component. The CPU will then "respond" by calling
* checkINTR() and request the corresponding interrupt vector from the ChipSet.
* clearINTR()
*
* @this {CPUSim}
* @param {boolean} fRaise is true to raise INTFLAG.INTR, false to lower
*/
CPUSim.prototype.updateINTR = function(fRaise)
CPUSim.prototype.clearINTR = function()
{
if (this.chipset) {
if (fRaise) {
this.intFlags |= CPUDef.INTFLAG.INTR;
} else {
this.intFlags &= ~CPUDef.INTFLAG.INTR;
}
}
this.intFlags &= ~(CPUDef.INTFLAG.INTL | CPUDef.INTFLAG.INTR);
};
/**
* requestINTR(nLevel)
*
* This is called by any component that wants to request a h/w interrupt.
*
* 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.
*
* @this {CPUSim}
* @param {number} nLevel (0-7)
*/
CPUSim.prototype.requestINTR = function(nLevel)
{
this.intFlags = (this.intFlags & ~CPUDef.INTFLAG.INTL) | nLevel | CPUDef.INTFLAG.INTR;
};
/**
@ -1110,27 +1121,6 @@ CPUSim.prototype.stepCPU = function(nMinCycles)
*/
this.nBurstCycles = this.nStepCycles = nMinCycles;
/*
* NOTE: Even though runCPU() calls updateAllTimers(), we need an additional call here if we're being
* called from the Debugger, so that any single-stepping will update the timers as well.
*/
if (this.chipset && !nMinCycles) this.chipset.updateAllTimers();
/*
* Let's also suppress h/w interrupts whenever the Debugger is single-stepping an instruction; I'm loathe
* to allow Debugger interactions to affect the behavior of the virtual machine in ANY way, but I'm making
* this small concession to avoid the occasional and sometimes unexpected Debugger command that ends up
* stepping into a hardware interrupt service routine (ISR).
*
* Note that this is similar to the problem discussed in checkINTR() regarding the priority of external h/w
* interrupts vs. Trap interrupts, but they require different solutions, because our Debugger operates
* independently of the CPU.
*
* One exception I make here is when you've asked the Debugger to display PIC messages, the idea being that
* if you're watching the PIC that closely, then you want to hardware interrupts to occur regardless.
*/
if (!nMinCycles) this.opFlags |= CPUDef.OPFLAG.NOINTR;
do {
if (this.intFlags) {
if (this.checkINTR()) {
@ -1138,12 +1128,11 @@ CPUSim.prototype.stepCPU = function(nMinCycles)
this.assert(DEBUGGER); // nMinCycles of zero should be generated ONLY by the Debugger
if (DEBUGGER) {
this.println("interrupt dispatched");
this.opFlags = 0;
break;
}
}
}
if (this.intFlags & CPUDef.INTFLAG.HALT) {
else if (this.intFlags & CPUDef.INTFLAG.HALT) {
/*
* As discussed in opHLT(), the CPU is never REALLY halted by a HLT instruction; instead,
* opHLT() sets CPUDef.INTFLAG.HALT, signalling to us that we're free to end the current burst
@ -1160,7 +1149,6 @@ CPUSim.prototype.stepCPU = function(nMinCycles)
* number of cycles during which we did not actually execute any instructions).
*/
this.nStepCycles = 0;
this.opFlags = 0;
break;
}
}
@ -1173,8 +1161,6 @@ CPUSim.prototype.stepCPU = function(nMinCycles)
nDebugState = 1;
}
this.opFlags = 0;
this.aOps[this.getPCByte()].call(this);
} while (this.nStepCycles > 0);

View file

@ -359,7 +359,7 @@ if (DEBUGGER) {
Debugger.TYPE_E = (Debugger.REG_E << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
Debugger.TYPE_H = (Debugger.REG_H << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
Debugger.TYPE_L = (Debugger.REG_L << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
Debugger.TYPE_M = (Debugger.REG_M << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
Debugger.TYPE_M = (Debugger.REG_M << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE | Debugger.TYPE_MEM);
Debugger.TYPE_BC = (Debugger.REG_BC << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD);
Debugger.TYPE_DE = (Debugger.REG_DE << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD);
Debugger.TYPE_HL = (Debugger.REG_HL << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD);
@ -404,7 +404,7 @@ if (DEBUGGER) {
/* 0x06 */ [Debugger.INS.MVI, Debugger.TYPE_B, Debugger.TYPE_IMM],
/* 0x07 */ [Debugger.INS.RLC],
/* 0x08 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
/* 0x09 */ [Debugger.INS.DAD, Debugger.TYPE_BC | Debugger.TYPE_IN], // REG_HL is the implied (unlisted) destination
/* 0x09 */ [Debugger.INS.DAD, Debugger.TYPE_HL, Debugger.TYPE_BC | Debugger.TYPE_IN], // let's be more explicit about the operands
/* 0x0A */ [Debugger.INS.LDAX, Debugger.TYPE_BC | Debugger.TYPE_MEM],
/* 0x0B */ [Debugger.INS.DCX, Debugger.TYPE_BC],
/* 0x0C */ [Debugger.INS.INR, Debugger.TYPE_C],
@ -420,7 +420,7 @@ if (DEBUGGER) {
/* 0x16 */ [Debugger.INS.MVI, Debugger.TYPE_D, Debugger.TYPE_IMM],
/* 0x17 */ [Debugger.INS.RAL],
/* 0x18 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
/* 0x19 */ [Debugger.INS.DAD, Debugger.TYPE_DE | Debugger.TYPE_IN], // REG_HL is the implied (unlisted) destination
/* 0x19 */ [Debugger.INS.DAD, Debugger.TYPE_HL, Debugger.TYPE_DE | Debugger.TYPE_IN], // let's be more explicit about the operands
/* 0x1A */ [Debugger.INS.LDAX, Debugger.TYPE_DE | Debugger.TYPE_MEM],
/* 0x1B */ [Debugger.INS.DCX, Debugger.TYPE_DE],
/* 0x1C */ [Debugger.INS.INR, Debugger.TYPE_E],
@ -436,7 +436,7 @@ if (DEBUGGER) {
/* 0x26 */ [Debugger.INS.MVI, Debugger.TYPE_H, Debugger.TYPE_IMM],
/* 0x27 */ [Debugger.INS.DAA],
/* 0x28 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
/* 0x29 */ [Debugger.INS.DAD, Debugger.TYPE_HL | Debugger.TYPE_IN], // REG_HL is the implied (unlisted) destination
/* 0x29 */ [Debugger.INS.DAD, Debugger.TYPE_HL, Debugger.TYPE_HL | Debugger.TYPE_IN], // let's be more explicit about the operands
/* 0x2A */ [Debugger.INS.LHLD, Debugger.TYPE_IMM | Debugger.TYPE_WORD | Debugger.TYPE_MEM],
/* 0x2B */ [Debugger.INS.DCX, Debugger.TYPE_HL],
/* 0x2C */ [Debugger.INS.INR, Debugger.TYPE_L],
@ -445,15 +445,15 @@ if (DEBUGGER) {
/* 0x2F */ [Debugger.INS.CMA],
/* 0x30 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
/* 0x31 */ [Debugger.INS.LXI, Debugger.TYPE_SP, Debugger.TYPE_IMM],
/* 0x32 */ [Debugger.INS.STA, Debugger.TYPE_IMM | Debugger.TYPE_BYTE | Debugger.TYPE_MEM],
/* 0x32 */ [Debugger.INS.STA, Debugger.TYPE_IMM | Debugger.TYPE_WORD | Debugger.TYPE_MEM],
/* 0x33 */ [Debugger.INS.INX, Debugger.TYPE_SP],
/* 0x34 */ [Debugger.INS.INR, Debugger.TYPE_M],
/* 0x35 */ [Debugger.INS.DCR, Debugger.TYPE_M],
/* 0x36 */ [Debugger.INS.MVI, Debugger.TYPE_M, Debugger.TYPE_IMM],
/* 0x37 */ [Debugger.INS.STC],
/* 0x38 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
/* 0x39 */ [Debugger.INS.DAD, Debugger.TYPE_SP | Debugger.TYPE_IN], // REG_HL is the implied (unlisted) destination
/* 0x3A */ [Debugger.INS.LDA, Debugger.TYPE_IMM | Debugger.TYPE_BYTE | Debugger.TYPE_MEM],
/* 0x39 */ [Debugger.INS.DAD, Debugger.TYPE_HL, Debugger.TYPE_SP | Debugger.TYPE_IN], // let's be more explicit about the operands
/* 0x3A */ [Debugger.INS.LDA, Debugger.TYPE_IMM | Debugger.TYPE_WORD | Debugger.TYPE_MEM],
/* 0x3B */ [Debugger.INS.DCX, Debugger.TYPE_SP],
/* 0x3C */ [Debugger.INS.INR, Debugger.TYPE_A],
/* 0x3D */ [Debugger.INS.DCR, Debugger.TYPE_A],
@ -622,15 +622,15 @@ if (DEBUGGER) {
/* 0xE0 */ [Debugger.INS.RPO],
/* 0xE1 */ [Debugger.INS.POP, Debugger.TYPE_HL],
/* 0xE2 */ [Debugger.INS.JPO, Debugger.TYPE_IMM | Debugger.TYPE_WORD],
/* 0xE3 */ [Debugger.INS.XTHL],
/* 0xE3 */ [Debugger.INS.XTHL, Debugger.TYPE_SP | Debugger.TYPE_MEM, Debugger.TYPE_HL], // let's be more explicit about the operands
/* 0xE4 */ [Debugger.INS.CPO, Debugger.TYPE_IMM | Debugger.TYPE_WORD],
/* 0xE5 */ [Debugger.INS.PUSH, Debugger.TYPE_HL],
/* 0xE6 */ [Debugger.INS.ANI, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
/* 0xE7 */ [Debugger.INS.RST],
/* 0xE8 */ [Debugger.INS.RPE],
/* 0xE9 */ [Debugger.INS.PCHL],
/* 0xE9 */ [Debugger.INS.PCHL, Debugger.TYPE_PC, Debugger.TYPE_HL], // let's be more explicit about the operands
/* 0xEA */ [Debugger.INS.JPE, Debugger.TYPE_IMM | Debugger.TYPE_WORD],
/* 0xEB */ [Debugger.INS.XCHG],
/* 0xEB */ [Debugger.INS.XCHG, Debugger.TYPE_HL, Debugger.TYPE_DE], // let's be more explicit about the operands
/* 0xEC */ [Debugger.INS.CPE, Debugger.TYPE_IMM | Debugger.TYPE_WORD],
/* 0xED */ [Debugger.INS.CALL, Debugger.TYPE_IMM | Debugger.TYPE_WORD | Debugger.TYPE_UNDOC],
/* 0xEE */ [Debugger.INS.XRI, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
@ -644,7 +644,7 @@ if (DEBUGGER) {
/* 0xF6 */ [Debugger.INS.ORI, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
/* 0xF7 */ [Debugger.INS.RST],
/* 0xF8 */ [Debugger.INS.RM],
/* 0xF9 */ [Debugger.INS.SPHL],
/* 0xF9 */ [Debugger.INS.SPHL, Debugger.TYPE_SP, Debugger.TYPE_HL], // let's be more explicit about the operands
/* 0xFA */ [Debugger.INS.JM, Debugger.TYPE_IMM | Debugger.TYPE_WORD],
/* 0xFB */ [Debugger.INS.EI],
/* 0xFC */ [Debugger.INS.CM, Debugger.TYPE_IMM | Debugger.TYPE_WORD],
@ -672,7 +672,7 @@ if (DEBUGGER) {
"bus": Messages.BUS,
"mem": Messages.MEM,
"port": Messages.PORT,
"timer": Messages.TIMER,
"chipset": Messages.CHIPSET,
"keyboard": Messages.KEYBOARD, // "kbd" is also allowed as shorthand for "keyboard"; see doMessages()
"key": Messages.KEYS, // using "key" instead of "keys", since the latter is a method on JavasScript objects
"video": Messages.VIDEO,
@ -1384,6 +1384,7 @@ if (DEBUGGER) {
case Debugger.REG_E:
case Debugger.REG_H:
case Debugger.REG_L:
case Debugger.REG_M:
case Debugger.REG_F:
cch = 2;
break;
@ -1417,7 +1418,7 @@ if (DEBUGGER) {
n = cpu.regA;
break;
case Debugger.REG_F:
n = cpu.getPS() & 0xff;
n = cpu.getPS();
break;
case Debugger.REG_B:
n = cpu.regB;
@ -1426,7 +1427,7 @@ if (DEBUGGER) {
n = cpu.regC;
break;
case Debugger.REG_BC:
n = (cpu.regB << 8) | cpu.regC;
n = cpu.getBC();
break;
case Debugger.REG_D:
n = cpu.regD;
@ -1435,7 +1436,7 @@ if (DEBUGGER) {
n = cpu.regE;
break;
case Debugger.REG_DE:
n = (cpu.regD << 8) | cpu.regE;
n = cpu.getDE();
break;
case Debugger.REG_H:
n = cpu.regH;
@ -1444,7 +1445,10 @@ if (DEBUGGER) {
n = cpu.regL;
break;
case Debugger.REG_HL:
n = (cpu.regH << 8) | cpu.regL;
n = cpu.getHL();
break;
case Debugger.REG_M:
n = cpu.getByte(cpu.getHL());
break;
case Debugger.REG_PS:
n = (cpu.regA << 8) | (cpu.getPS() & 0xff);
@ -2430,31 +2434,38 @@ if (DEBUGGER) {
var iIns = aOpDesc[0];
var sOperands = "";
var typeSizeDefault = null;
var sOpcode = asOpcodes[iIns];
var cOperands = aOpDesc.length - 1;
var typeSizeDefault = Debugger.TYPE_NONE, type;
for (var iOperand = 1; iOperand <= cOperands; iOperand++) {
var disp, off, cch;
var sOperand = "";
var type = aOpDesc[iOperand];
type = aOpDesc[iOperand];
if (type === undefined) continue;
var typeSize = type & Debugger.TYPE_SIZE;
if (typeSize == Debugger.TYPE_NONE) {
if (typeSizeDefault == null) continue;
typeSize = typeSizeDefault;
if (!typeSize) {
type |= typeSizeDefault;
} else {
typeSizeDefault = typeSize;
}
var typeOther = type & Debugger.TYPE_OTHER;
if (!typeOther) {
type |= (iOperand == 1? Debugger.TYPE_OUT : Debugger.TYPE_IN);
}
typeSizeDefault = typeSize;
var typeMode = type & Debugger.TYPE_MODE;
if (typeMode == Debugger.TYPE_IMM) {
if (typeMode & Debugger.TYPE_IMM) {
sOperand = this.getImmOperand(type, dbgAddr);
}
else if (typeMode == Debugger.TYPE_REG) {
else if (typeMode & Debugger.TYPE_REG) {
sOperand = this.getRegOperand((type & Debugger.TYPE_IREG) >> 8, type, dbgAddr);
}
if (!sOperand || !sOperand.length) {
sOperands = "INVALID";
break;
@ -2472,7 +2483,8 @@ if (DEBUGGER) {
} while (dbgAddrIns.addr != dbgAddr.addr);
}
sLine += str.pad(sBytes, 11);
sLine += str.pad(sBytes, 10);
sLine += (type & Debugger.TYPE_UNDOC)? '*' : ' ';
sLine += str.pad(sOpcode, 7);
if (sOperands) sLine += ' ' + sOperands;
@ -2503,14 +2515,7 @@ if (DEBUGGER) {
switch (typeSize) {
case Debugger.TYPE_BYTE:
/*
* There's the occasional immediate byte we don't need to display (eg, the 0x0A
* following an AAM or AAD instruction), so we suppress the byte if it lacks a TYPE_IN
* or TYPE_OUT designation (and TYPE_BOTH, as the name implies, includes both).
*/
if (type & Debugger.TYPE_BOTH) {
sOperand = str.toHex(this.getByte(dbgAddr, 1), 2);
}
sOperand = str.toHex(this.getByte(dbgAddr, 1), 2);
break;
case Debugger.TYPE_SBYTE:
sOperand = str.toHex((this.getByte(dbgAddr, 1) << 24) >> 24, 4);
@ -2522,6 +2527,9 @@ if (DEBUGGER) {
sOperand = "imm(" + str.toHexWord(type) + ')';
break;
}
if (type & Debugger.TYPE_MEM) {
sOperand = '[' + sOperand + ']';
}
return sOperand;
};
@ -2536,9 +2544,12 @@ if (DEBUGGER) {
*/
Debugger.prototype.getRegOperand = function(iReg, type, dbgAddr)
{
var typeMode = type & Debugger.TYPE_MODE;
var typeSize = type & Debugger.TYPE_SIZE;
return Debugger.REGS[iReg];
/*
* Although this breaks with assembler conventions, I'm going to experiment with some different
* mnemonics; specifically, "[HL]" instead of "M". This is also more in keeping with how getImmOperand()
* displays memory references (ie, by enclosing them in brackets).
*/
return iReg == Debugger.REG_M? "[HL]" : Debugger.REGS[iReg];
};
/**

View file

@ -37,7 +37,7 @@ var Messages = {
BUS: 0x00000040,
MEM: 0x00000080,
PORT: 0x00000100,
TIMER: 0x00000800,
CHIPSET: 0x00008000,
KEYBOARD: 0x00010000,
KEYS: 0x00020000,
VIDEO: 0x00040000,

View file

@ -49,10 +49,12 @@ if (NODE) {
*
* screenWidth: width of the screen canvas, in pixels
* screenHeight: height of the screen canvas, in pixels
* aspect
* rotate
* addr
* screenColor: background color of the screen canvas (default is black)
* aspectRatio
* frameBuffer
* interruptRate (eg, 120)
* refreshRate (eg, 60)
* rotation (eg, 90)
*
* @constructor
* @extends Component
@ -65,11 +67,72 @@ if (NODE) {
function Video(parmsVideo, canvas, context, textarea, container)
{
Component.call(this, "Video", parmsVideo, Video, Messages.VIDEO);
this.interruptRate = parmsVideo['interruptRate'];
this.refreshRate = parmsVideo['refreshRate'] || 60;
this.setReady();
}
Component.subclass(Video);
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {Video}
* @param {Computer} cmp
* @param {Bus} bus
* @param {CPUSim} cpu
* @param {Debugger} dbg
*/
Video.prototype.initBus = function(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
};
/**
* getRefreshRate()
*
* @this {Video}
* @return {number}
*/
Video.prototype.getRefreshRate = function()
{
return Math.max(this.refreshRate, this.interruptRate);
};
/**
* updateScreen(n)
*
* Propagates the video buffer to the cell cache and updates the screen with any changes. Forced updates
* are generally internal updates triggered by an I/O operation or other state change, while non-forced updates
* are the periodic updates coming from the CPU.
*
* For every cell in the video buffer, compare it to the cell stored in the cell cache, render if it differs,
* and then update the cell cache to match. Since initCache() sets every cell in the cell cache to an
* invalid value, we're assured that the next call to updateScreen() will redraw the entire (visible) video buffer.
*
* @this {Video}
* @param {number} n (where 0 <= n < getRefreshRate() for a normal update, or -1 for a forced update)
*/
Video.prototype.updateScreen = function(n)
{
if (!(n & 1)) {
/*
* On even updates, call cpu.requestINTR(1), and also update our copy of the screen.
*/
this.cpu.requestINTR(1);
} else {
/*
* On odd updates, call cpu.requestINTR(2), but do NOT update our copy of the screen, because
* the machine has presumably only updated the top half of the frame buffer at this point; it will
* update the bottom half of the frame buffer after acknowledging this interrupt.
*/
this.cpu.requestINTR(2);
}
};
/**
* Video.init()
*

View file

@ -1038,10 +1038,41 @@
<xsl:otherwise>false</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="aspectRatio">
<xsl:choose>
<xsl:when test="@aspect"><xsl:value-of select="@aspect"/></xsl:when>
<xsl:when test="@aspectRatio"><xsl:value-of select="@aspectRatio"/></xsl:when>
<xsl:otherwise>0</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="frameBuffer">
<xsl:choose>
<xsl:when test="@frameBuffer"><xsl:value-of select="@frameBuffer"/></xsl:when>
<xsl:otherwise>0</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="interruptRate">
<xsl:choose>
<xsl:when test="@interruptRate"><xsl:value-of select="@interruptRate"/></xsl:when>
<xsl:otherwise>0</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="refreshRate">
<xsl:choose>
<xsl:when test="@refreshRate"><xsl:value-of select="@refreshRate"/></xsl:when>
<xsl:otherwise>60</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="rotation">
<xsl:choose>
<xsl:when test="@rotation"><xsl:value-of select="@rotation"/></xsl:when>
<xsl:otherwise>0</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:call-template name="component">
<xsl:with-param name="machine" select="$machine"/>
<xsl:with-param name="class">video</xsl:with-param>
<xsl:with-param name="parms">,model:'<xsl:value-of select="$model"/>',mode:<xsl:value-of select="$mode"/>,screenWidth:<xsl:value-of select="$screenWidth"/>,screenHeight:<xsl:value-of select="$screenHeight"/>,memory:<xsl:value-of select="$memory"/>,switches:'<xsl:value-of select="$switches"/>',scale:<xsl:value-of select="$scale"/>,charCols:<xsl:value-of select="$charCols"/>,charRows:<xsl:value-of select="$charRows"/>,fontROM:'<xsl:value-of select="$fontROM"/>',screenColor:'<xsl:value-of select="$screenColor"/>',touchScreen:'<xsl:value-of select="$touchScreen"/>',autoLock:<xsl:value-of select="$autoLock"/></xsl:with-param>
<xsl:with-param name="parms">,model:'<xsl:value-of select="$model"/>',mode:<xsl:value-of select="$mode"/>,screenWidth:<xsl:value-of select="$screenWidth"/>,screenHeight:<xsl:value-of select="$screenHeight"/>,memory:<xsl:value-of select="$memory"/>,switches:'<xsl:value-of select="$switches"/>',scale:<xsl:value-of select="$scale"/>,charCols:<xsl:value-of select="$charCols"/>,charRows:<xsl:value-of select="$charRows"/>,fontROM:'<xsl:value-of select="$fontROM"/>',screenColor:'<xsl:value-of select="$screenColor"/>',touchScreen:'<xsl:value-of select="$touchScreen"/>',autoLock:<xsl:value-of select="$autoLock"/>,aspectRatio:<xsl:value-of select="$aspectRatio"/>,frameBuffer:<xsl:value-of select="$frameBuffer"/>,interruptRate:<xsl:value-of select="$interruptRate"/>,refreshRate:<xsl:value-of select="$refreshRate"/>,rotation:<xsl:value-of select="$rotation"/></xsl:with-param>
</xsl:call-template>
</xsl:template>