Added timer support to the PCx86 SerialPort component, so that we can honor the programmed baud rate now

This commit is contained in:
Jeff Parsons 2017-08-23 10:50:31 -07:00 • committed by Jeff Parsons
commit 6a3498e666
10 changed files with 800 additions and 714 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7131,29 +7131,55 @@ class CPU8080 extends Component {
}
/**
* addTimer(callBack)
* addTimer(id, callBack, ms)
*
* 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
* Components that want to have timers that fire after some number of milliseconds call addTimer() to create
* the timer, and then setTimer() when they want to arm it. Alternatively, they can specify an automatic timeout
* value (in milliseconds) to have the timer fire automatically at regular intervals. 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.
* Internally, each timer entry is a preallocated Array with the following entries:
*
* [0]: timer ID
* [1]: countdown value, in cycles
* [2]: automatic setTimer value, if any, in milliseconds
* [3]: callback function
*
* 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 {CPU8080}
* @param {string} id
* @param {function()} callBack
* @param {number} [ms] (if set, enables automatic setTimer calls)
* @return {number} timer index
*/
addTimer(callBack)
addTimer(id, callBack, ms = -1)
{
var iTimer = this.aTimers.length;
this.aTimers.push([-1, callBack]);
this.aTimers.push([id, -1, ms, callBack]);
if (ms >= 0) this.setTimer(iTimer, ms);
return iTimer;
}
/**
* findTimer(id)
*
* @this {CPU8080}
* @param {string} id
* @return {Array|null}
*/
findTimer(id)
{
for (var iTimer = 0; iTimer < this.aTimers.length; iTimer++) {
var timer = this.aTimers[iTimer];
if (timer[0] == id) return timer;
}
return null;
}
/**
* setTimer(iTimer, ms, fReset)
*
@ -7179,18 +7205,19 @@ class CPU8080 extends Component {
{
var nCycles = -1;
if (iTimer >= 0 && iTimer < this.aTimers.length) {
if (fReset || this.aTimers[iTimer][0] < 0) {
var timer = this.aTimers[iTimer];
if (fReset || timer[1] < 0) {
nCycles = this.getMSCycles(ms);
/*
* We must now confront the following problem: if the CPU is currently executing a burst of cycles,
* the number of cycles it has executed in that burst so far must NOT be charged against the cycle
* timeout we're about to set. The simplest way to resolve that is to immediately call endBurst()
* and bias the above cycle timeout by the number of cycles that the burst executed.
* If the CPU is currently executing a burst of cycles, the number of cycles it has executed in
* that burst so far must NOT be charged against the cycle timeout we're about to set. The simplest
* way to resolve that is to immediately call endBurst() and bias the cycle timeout by the number
* of cycles that the burst executed.
*/
if (this.flags.running) {
nCycles += this.endBurst();
}
this.aTimers[iTimer][0] = nCycles;
timer[1] = nCycles;
}
}
return nCycles;
@ -7219,12 +7246,12 @@ class CPU8080 extends Component {
*/
getBurstCycles(nCycles)
{
for (var i = this.aTimers.length - 1; i >= 0; i--) {
var timer = this.aTimers[i];
for (var iTimer = this.aTimers.length - 1; iTimer >= 0; iTimer--) {
var timer = this.aTimers[iTimer];
if (timer[0] < 0) continue;
if (nCycles > timer[0]) {
nCycles = timer[0];
if (timer[1] < 0) continue;
if (nCycles > timer[1]) {
nCycles = timer[1];
}
}
return nCycles;
@ -7242,14 +7269,23 @@ class CPU8080 extends Component {
*/
updateTimers(nCycles)
{
for (var i = this.aTimers.length - 1; i >= 0; i--) {
var timer = this.aTimers[i];
for (var iTimer = this.aTimers.length - 1; iTimer >= 0; iTimer--) {
var timer = this.aTimers[iTimer];
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
if (timer[1] < 0) continue;
timer[1] -= nCycles;
if (timer[1] <= 0) {
if (DEBUG && this.messageEnabled(Messages8080.CPU)) {
this.printMessage("updateTimer(" + nCycles + "): firing " + timer[0] + " with only " + (timer[1] + nCycles) + " cycles left");
}
timer[1] = -1; // zero is technically an "active" value, so ensure the timer is dormant now
timer[3](); // safe to invoke the callback function now
if (timer[2] >= 0) {
this.setTimer(iTimer, timer[2]);
if (DEBUG && this.messageEnabled(Messages8080.CPU)) {
this.printMessage("updateTimer(" + nCycles + "): rearming " + timer[0] + " for " + timer[2] + "ms (" + timer[1] + " cycles)");
}
}
}
}
}
@ -13588,7 +13624,7 @@ class Keyboard8080 extends Component {
this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work
var kbd = this;
this.timerReleaseKeys = this.cpu.addTimer(function() {
this.timerReleaseKeys = this.cpu.addTimer(this.id, function() {
kbd.checkSoftKeysToRelease();
});
@ -15167,7 +15203,7 @@ class Video8080 extends Component {
}
var video = this;
this.timerUpdateNext = this.cpu.addTimer(function() {
this.timerUpdateNext = this.cpu.addTimer(this.id, function() {
video.updateScreen();
});
this.cpu.setTimer(this.timerUpdateNext, this.getRefreshTime());
@ -16593,10 +16629,10 @@ class SerialPort8080 extends Component {
this.dbg = dbg;
var serial = this;
this.timerReceiveNext = this.cpu.addTimer(function() {
this.timerReceiveNext = this.cpu.addTimer(this.id + ".receive", function() {
serial.receiveData();
});
this.timerTransmitNext = this.cpu.addTimer(function() {
this.timerTransmitNext = this.cpu.addTimer(this.id + ".transmit", function() {
serial.transmitData();
});
@ -16803,8 +16839,8 @@ class SerialPort8080 extends Component {
var nBits = ((this.bMode & SerialPort8080.UART8251.MODE.DATA_BITS) >> 2) + 6; // includes an extra +1 for start bit
if (this.bMode & SerialPort8080.UART8251.MODE.PARITY_ENABLE) nBits++;
nBits += ((((this.bMode & SerialPort8080.UART8251.MODE.STOP_BITS) >> 6) + 1) >> 1);
var nBytesPerSecond = Math.round(nBaud / nBits);
return 1000 / nBytesPerSecond;
var nBytesPerSecond = nBaud / nBits;
return (1000 / nBytesPerSecond)|0;
}
/**
@ -16900,11 +16936,9 @@ class SerialPort8080 extends Component {
}
}
if (this.sendData) {
if (this.sendData.call(this.connection, b)) {
if (this.sendData && this.sendData.call(this.connection, b)) {
fTransmitted = true;
}
}
if (this.echoByte(b)) {
fTransmitted = true;
@ -16921,6 +16955,10 @@ class SerialPort8080 extends Component {
* 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.
*
* The sData parameter is not used when we're called via the timer; it's an optional parameter used by
* the Keyboard component to deliver data pasted via the clipboard, and is currently only useful when
* the SerialPort is connected to another machine. TODO: Define a separate interface for that feature.
*
* @this {SerialPort8080}
* @param {string} [sData]
* @return {boolean} true if successful, false if not

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7668,8 +7668,8 @@ class Panel extends Component {
* startTimer()
*
* This timer replaces the CPU's old dedicated VIDEO_UPDATES_PER_SECOND logic, which periodically called
* the Computer's updateVideo() function, which in turn called us; periodic updateAnimation() calls are now
* our own responsibility.
* the Computer's updateVideo() function, which in turn called our updateAnimation() function; periodic
* animation updates are now our own responsibility.
*
* @this {Panel}
*/
@ -12640,11 +12640,12 @@ class CPU extends Component {
* value (in milliseconds) to have the timer fire automatically at regular intervals. 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 three entries:
* Internally, each timer entry is a preallocated Array with the following entries:
*
* [0]: countdown value, in cycles
* [1]: automatic setTimer value, if any, in milliseconds
* [2]: callback function
* [0]: timer ID
* [1]: countdown value, in cycles
* [2]: automatic setTimer value, if any, in milliseconds
* [3]: callback function
*
* 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.
@ -55094,8 +55095,11 @@ class SerialPort extends Component {
*
* binding: name of a control (based on its "binding" attribute) to bind to this port's I/O
*
* tabSize: set to a non-zero number to convert tabs to spaces (applies only to output to
* the above binding); default is 0 (no conversion)
* tabSize: a non-zero number specifies the tab-stop multiple to use for automatic tab-to-space
* conversion; it applies only to the above binding, and the default is 0 (no tab conversion)
*
* charBOL: a non-zero number specifies the ASCII code of a character to display at the beginning
* of every line; it applies only to the above binding, and the default is 0 (no BOL character)
*
* In the future, we may support 'port' and 'irq' properties that allow the machine to define a non-standard
* serial port configuration, instead of only our pre-defined 'adapter' configurations.
@ -55158,11 +55162,12 @@ class SerialPort extends Component {
* being echoed via transmitByte(), maintain a logical column position, and convert any tabs into the appropriate
* number of spaces.
*
* charBOL, if nonzero, is a character to automatically output at the beginning of every line. This probably
* isn't generally useful; I use it internally to preformat serial output.
* Another controlIOBuffer feature is charBOL, which, if nonzero, specifies a character to automatically output
* at the beginning of every line. This probably isn't generally useful; I use it internally to preformat serial
* output.
*/
this.tabSize = parmsSerial['tabSize'];
this.charBOL = parmsSerial['charBOL'];
this.tabSize = parmsSerial['tabSize'] || 0;
this.charBOL = parmsSerial['charBOL'] || 0;
this.charPrev = 0;
this.iLogicalCol = 0;
@ -55310,6 +55315,14 @@ class SerialPort extends Component {
this.cpu = cpu;
this.dbg = dbg;
var serial = this;
this.timerReceiveNext = this.cpu.addTimer(this.id + ".receive", function() {
serial.receiveData();
});
this.timerTransmitNext = this.cpu.addTimer(this.id + ".transmit", function() {
serial.transmitData();
});
this.chipset = cmp.getMachineComponent("ChipSet");
bus.addPortInputTable(this, SerialPort.aPortInput, this.portBase);
@ -55331,7 +55344,7 @@ class SerialPort extends Component {
*
* For now, we're not going to worry about communication in the other direction, because when the target component
* performs its own initConnection(), it will find our receiveData() and receiveStatus() functions, at which point
* communication in both directions should be established, and the circle of life complete.
* communication in both directions should be established, and the circle of life is complete.
*
* For added robustness, if the target machine initializes much more slowly than we do, and our connection attempt
* fails, that's OK, because when it finally initializes, its initConnection() will call our initConnection();
@ -55520,19 +55533,38 @@ class SerialPort extends Component {
return data;
}
/**
* getBaudTimeout()
*
* The 16-bit Divisor Latch is stored in wDL. If we take the frequency value 1843200 and divide it by wDL*128,
* we get the maximum number of bytes per second that the SerialPort interface should generate. For example,
* if a baud rate of 1200 is being used, the divisor will be 0x60 (96), so we calculate 1843200/(96*128) = 150,
* which means there should be a 1000ms/150 or 6.667ms delay between bytes delivered.
*
* @this {SerialPort}
* @return {number} (number of milliseconds per byte)
*/
getBaudTimeout()
{
var nBytesPerSecond = 1843200 / ((this.wDL || 1) << 7);
return (1000 / nBytesPerSecond)|0;
}
/**
* receiveData(data)
*
* This replaces the old sendRBR() function, which expected an Array of bytes. We still support that,
* but in order to support connections with other SerialPort components (ie, the PC8080 SerialPort), we
* have added support for numbers and strings as well.
* have added support for numbers and strings as well. If no data is specified at all, then all we do is
* "clock" any remaining data into the receiver.
*
* @this {SerialPort}
* @param {number|string|Array} data
* @param {number|string|Array} [data]
* @return {boolean} true if received, false if not
*/
receiveData(data)
{
if (data != null) {
if (typeof data == "number") {
this.abReceive.push(data);
}
@ -55544,6 +55576,7 @@ class SerialPort extends Component {
else {
this.abReceive = this.abReceive.concat(data);
}
}
this.advanceRBR();
return true; // for now, return true regardless, since we're buffering everything anyway
}
@ -55551,9 +55584,9 @@ class SerialPort extends Component {
/**
* receiveStatus(pins)
*
* NOTE: Prior to the addition of this interface, the CTS and DSR bits were initialized set and remained set for the life
* of the machine. It is entirely appropriate that this is the only way those bits can be changed, because they represent
* external control signals.
* NOTE: Prior to the addition of this interface, the CTS and DSR bits were initialized set and remained set
* for the life of the machine. It is entirely appropriate that this is the only way those bits can be changed,
* because they represent external control signals.
*
* @this {SerialPort}
* @param {number} pins
@ -55581,6 +55614,9 @@ class SerialPort extends Component {
if (this.abReceive.length > 0 && !(this.bLSR & SerialPort.LSR.DR)) {
this.bRBR = this.abReceive.shift();
this.bLSR |= SerialPort.LSR.DR;
if (this.abReceive.length && this.cpu) {
this.cpu.setTimer(this.timerReceiveNext, this.getBaudTimeout());
}
}
this.updateIRR();
}
@ -55719,7 +55755,16 @@ class SerialPort extends Component {
this.bTHR = bOut;
this.bLSR &= ~(SerialPort.LSR.THRE | SerialPort.LSR.TSRE);
if (this.transmitByte(bOut)) {
this.bLSR |= (SerialPort.LSR.THRE | SerialPort.LSR.TSRE);
/*
* If we're transmitting to a virtual device that has no measurable delay, this code may set the
* transmitter empty bits too quickly:
*
* this.bLSR |= (SerialPort.LSR.THRE | SerialPort.LSR.TSRE);
*
* A better solution is to arm a timer based on the baud rate, and clear the above bits when that
* timer fires.
*/
if (this.cpu) this.cpu.setTimer(this.timerTransmitNext, this.getBaudTimeout());
this.updateIRR();
/*
* QUESTION: Does this mean we should also flush/zero bTHR?
@ -55815,8 +55860,24 @@ class SerialPort extends Component {
this.bIIR &= ~(SerialPort.IIR.NO_INT | SerialPort.IIR.INT_BITS);
this.bIIR |= bIIR;
/*
* TODO: Remove this arbitrary 100-instruction delay once we've added support for baud rate throttling
* (see notes above regarding baud rate).
* I still throttle SerialPort interrupts by passing a hard-coded delay of 100 instructions to setIRR(),
* even though we are now (theoretically) honoring the programmed baud rate. The setIRR() delay does not
* ensure any particular baud rate, it simply gives the underlying Interrupt Service Routine (ISR) some
* breathing room.
*
* The Microsoft Windows 1.01 serial mouse driver ISR issues an EOI before it has safely exited, presumably
* relying on the fact that a 1200 baud serial device would not normally interrupt frequently enough to
* blow the stack. However, in PCx86, all you have to do is remove the delay below and enable Debugger
* messages on every serial interrupt and mouse event, eg:
*
* m serial on;m pic on;m mouse on
*
* to slow the machine down to the point where serial mouse interrupts overwhelm the ISR. The Debugger
* messages display the current stack pointer, which you can watch drop to zero and then wrap around, no
* doubt trampling lots of code and data along the way.
*
* This problem could also occur without being forced by the Debugger; eg, if your physical machine's mouse
* was configured for a high interrupt rate, and your browser generated mouse events at a comparable rate.
*/
if (this.chipset && this.nIRQ) this.chipset.setIRR(this.nIRQ, 100);
} else {
@ -55893,6 +55954,18 @@ class SerialPort extends Component {
return fTransmitted;
}
/**
* transmitData()
*
* Helper for clocking transmitted data at the expected baud rate.
*
* @this {SerialPort}
*/
transmitData()
{
this.bLSR |= (SerialPort.LSR.THRE | SerialPort.LSR.TSRE);
}
/**
* SerialPort.init()
*
@ -55958,34 +56031,6 @@ SerialPort.DLL = {REG: 0}; // Divisor Latch LSB (only when SerialPo
SerialPort.THR = {REG: 0}; // Transmitter Holding Register (write)
SerialPort.DL_DEFAULT = 0x180; // we select an arbitrary default Divisor Latch equivalent to 300 baud
/*
* The divisor is stored in wDL. If we take the frequency value 1843200 and divide it by wDL*128, we get the
* maximum number of bytes per second that the SerialPort interface should generate. For example, if a baud
* rate of 1200 is being used, the divisor will be 0x60 (96), so we calculate 1843200/(96*128) = 150, which means
* there should be a 1000ms/150 or 6.667ms delay between bytes delivered.
*
* TODO: Enforce that delay. However, the delay should be converted from real-world milliseconds to the
* appropriate number of CPU cycles we can pass to setBurstCycles(). This will also require the CPU to call
* us at the start of each burst, to see if advanceRBR() has more data to deliver. For now, I'm throttling
* SerialPort interrupts by passing a hard-coded delay to setIRR(). The setIRR() delay does not ensure any
* particular baud rate, it simply gives the underlying Interrupt Service Routine (ISR) some breathing room.
*
* The Microsoft Windows 1.01 serial mouse driver ISR issues an EOI before it has safely exited, presumably
* relying on the fact that a 1200 baud serial device would not normally interrupt frequently enough to blow
* the stack. However, in PCx86, all you have to do is enable Debugger messages on every serial interrupt
* and mouse event, eg:
*
* m serial on;m pic on;m mouse on
*
* to slow the machine down to the point where serial mouse interrupts overwhelm the ISR. The Debugger messages
* display the current stack pointer, which you can watch drop to zero and then wrap around, no doubt trampling
* lots of code and data along the way.
*
* This problem could also occur without being forced by the Debugger; eg, if your physical machine's mouse was
* configured for a high interrupt rate, and your browser generated mouse events at a comparable rate, then you
* could blow the simulation's stack.
*/
/*
* Receiver Buffer Register (RBR.REG, offset 0; eg, 0x3F8 or 0x2F8) on read, Transmitter Holding Register on write
*/
@ -56050,8 +56095,9 @@ SerialPort.MCR.UNUSED = 0xE0; // always zero
/*
* Line Status Register (LSR.REG, offset 5; eg, 0x3FD or 0x2FD)
*
* NOTE: I've seen different specs for the LSR_TSRE. I'm following the IBM Tech Ref's lead here, but the data sheet I have calls it TEMT
* instead of TSRE, and claims that it is set whenever BOTH the THR and TSR are empty, and clear whenever EITHER the THR or TSR contain data.
* NOTE: I've seen different specs for the LSR_TSRE. I'm following the IBM Tech Ref's lead here, but the data sheet
* I have calls it TEMT instead of TSRE, and claims that it is set whenever BOTH the THR and TSR are empty, and clear
* whenever EITHER the THR or TSR contain data.
*/
SerialPort.LSR = {};
SerialPort.LSR.REG = 5; // Line Status Register

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long