1
0
mirror of https://github.com/pkimpel/retro-220.git synced 2026-01-11 23:52:46 +00:00

Commit retro-220 emulator version 0.06:

1. Correct scaling and normalization limiter errors in floating Add/Subtract uncovered by BALGOL testing; set Single-Step instead of Stop if normalization limiter is exceeded.
2. Correct improper comparison of sign digits by CFA/CFR uncovered by BALGOL testing.
3. Change method of lamp glow update in Processor and ControlConsole to provide smoother, more realistic display.
4. Implement diagnostic trace facility in Processor; toggle on and off by double-clicking "220" logo on ControlConsole.
5. Implement temporary, experimental floatingAdd() routine in Processor that does rounding for use in BALGOL validity checking (the 220 did not round, and it is disabled in this commit).
6. Correct printer carriage control handling in CardatronOutput.
7. Improve timing for ConsolePrinter TTY "Whippet mode".
8. Implement compression of consecutive duplicate blocks in tape image files for MagTapeDrive -- see comments in source.
9. Remove inappropriate SPO selection from unit designate on PaperTapeReader panel.
This commit is contained in:
Paul Kimpel 2018-02-05 09:46:26 -08:00
parent 0fe12839a7
commit b80b0f8c5f
9 changed files with 512 additions and 183 deletions

View File

@ -74,6 +74,7 @@ function B220Processor(config, devices) {
this.magTape = null; // reference to Magnetic Tape Control Unit
this.poweredOn = 0; // system is powered on and initialized
this.successor = null; // current delayed-action successor function
this.tracing = false; // emulator diagnostic tracing flag
// Memory
this.memorySize = config.getNode("memorySize"); // memory size, words
@ -256,7 +257,7 @@ function B220Processor(config, devices) {
* Global Constants *
***********************************************************************/
B220Processor.version = "0.05";
B220Processor.version = "0.06";
B220Processor.tick = 1000/200000; // milliseconds per clock cycle (200KHz)
B220Processor.cyclesPerMilli = 1/B220Processor.tick;
@ -359,6 +360,35 @@ B220Processor.binaryBCD = function binaryBCD(v) {
return result;
};
/**************************************/
B220Processor.padLeft = function padLeft(v, digits, pad) {
/* Converts "v" to a string if necessary and formats to a total length of
"digits," padding with the "pad" character on the left. Used only for debug */
var padChar = (pad || "0").toString();
var s = v.toString();
var len = s.length;
if (len > digits) {
s = s.substring(len-digits);
} else {
while (len < digits) {
s = padChar + s;
++len;
}
}
return s;
};
/**************************************/
B220Processor.formatWord = function formatWord(w) {
/* Formats the BCD value of 220 word "w" as the customary "9 9999 99 9999" */
var s = B220Processor.padLeft(w.toString(16), 11);
return s.substring(0, 1) + " " + s.substring(1, 5) + " " +
s.substring(5, 7) + " " + s.substring(7);
};
/***********************************************************************
* Bit and Field Manipulation Functions *
@ -1504,7 +1534,9 @@ B220Processor.prototype.floatingAdd = function floatingAdd(absolute) {
augend (A), placing the result in A and clearing D. The R register is not
affected. All values are BCD with the sign in the 11th digit position.
The floating exponent is in the first two digit positions, biased by 50.
Sets Overflow and the Digit Check alarm as necessary */
Sets Overflow and the Digit Check alarm as necessary. For more on the use
of the limiter digit in C/11 and the mechanization of floating add/subtract
on the 220, see United States Patent 3,022,006, 1962-02-20 */
var ax; // augend exponent (binary)
var am = this.A.value % 0x10000000000; // augend mantissa (BCD)
var aSign = ((this.A.value - am)/0x10000000000)%2;
@ -1538,102 +1570,101 @@ B220Processor.prototype.floatingAdd = function floatingAdd(absolute) {
// If the exponents are unequal, scale the smaller
// until they are in alignment, or one mantissa becomes zero.
if (ax > dx) {
// Scale D until its exponent matches or the mantissa goes to zero.
while (ax > dx) {
if (++shifts < 8) {
timing += 0.010;
dx = this.bcdAdd(1, dx, 2, 0, 0); // ++dx
d = dm % 0x10;
dm = (dm - d)/0x10; // shift right
} else {
sign = aSign; // result is value in A
dm = dSign = limiter = 0;
dx = 0x10;
break;
}
}
} else if (ax < dx) {
// Scale A until its exponent matches or the mantissa goes to zero.
while (ax < dx) {
if (++shifts < 8) {
timing += 0.010;
ax = this.bcdAdd(1, ax, 3, 0, 0); // ++ax
d = am % 0x10;
am = (am - d)/0x10; // shift right
} else {
am = dm; // result is value in D with adjusted sign
ax = dx;
dm = dSign = limiter = 0;
dx = 0x10;
break;
}
// Scale D until its exponent matches or the mantissa goes to zero.
while (ax > dx) {
if (++shifts < 8) {
timing += 0.010;
dx = this.bcdAdd(1, dx, 2, 0, 0); // ++dx
d = dm % 0x10;
dm = (dm - d)/0x10; // shift right
} else {
sign = aSign; // result is value in A
limiter = 0;
break;
}
}
if (am && dm) { // both mantissas are non-zero
// Scale A until its exponent matches or the mantissa goes to zero.
while (ax < dx) {
if (++shifts < 8) {
timing += 0.010;
ax = this.bcdAdd(1, ax, 3, 0, 0); // ++ax
d = am % 0x10;
am = (am - d)/0x10; // shift right
} else {
am = dm; // result is value in D with adjusted sign
ax = dx;
limiter = 0;
break;
}
}
// Add the mantissas
if (shifts < 8) {
compl = (aSign^sign);
am = this.bcdAdd(am, dm, 11, compl, compl);
dm = dSign = 0;
dx = 0x10;
// Now examine the resulting sign (still in the adder) to see if we
// need to recomplement the result.
// Now examine the resulting sign (still in the adder) to see if there
// is a carry and we need to recomplement the result and sign.
if (this.Z.value) {
// Reverse the sign toggle and recomplement the result.
sign = 1-sign;
am = this.bcdAdd(am, 0, 11, 1, 1);
timing += 0.060;
}
}
// Normalize or scale the result as necessary
if (am >= 0x100000000) {
// Mantissa overflow: add/subtract can produce at most one digit of
// overflow, so shift right and increment the exponent, checking for
// overflow in the exponent.
limiter = 0;
if (ax < 0x99) {
timing += 0.005;
ax = this.bcdAdd(1, ax, 3, 0, 0); // ++ax
d = am % 0x10;
am = (am - d)/0x10; // shift right
dm = dSign = 0; // Set D to its strange result value
dx = 0x10;
// Normalize or scale the result as necessary
if (am >= 0x100000000) {
// Mantissa overflow: add/subtract can produce at most one digit of
// overflow, so scale by shifting right and incrementing the exponent,
// checking for overflow in the exponent.
limiter = 0;
if (ax < 0x99) {
timing += 0.005;
ax = this.bcdAdd(1, ax, 3, 0, 0); // ++ax
d = am % 0x10;
am = (am - d)/0x10; // shift right
} else {
// A scaling shift would overflow the exponent, so set the overflow
// toggle and leave the mantissa as it was from the add, without the
// exponent inserted back into it. Since the A register gets reassembled
// below, we need to set up the mantissa and exponent so the reconstruct
// will effectively do nothing.
this.OFT.set(1);
sign = ax = dx = limiter = 0;
}
} else if (am == 0) { // mantissa is zero
ax = sign = limiter = 0;
timing += 0.065;
} else { // normalize the result as necessary
shifts = 0;
while (am < 0x10000000) {
if (ax > 0) {
++shifts;
timing += 0.010;
ax = this.bcdAdd(1, ax, 3, 1, 1); // --ax
am *= 0x10; // shift left
} else {
// A scaling shift would overflow the exponent, so set the overflow
// toggle and leave the mantissa as it was from the add, without the
// exponent inserted back into it. Since the A register gets reassembled
// below, we need to set up the mantissa and exponent so the reconstruct
// will effectively do nothing.
this.OFT.set(1);
sign = ax = dx = limiter = 0;
}
} else if (am == 0) { // mantissa is zero
ax = sign = limiter = 0;
timing += 0.065;
} else { // normalize the result as necessary
shifts = 0;
while (am < 0x10000000) {
if (ax > 0) {
++shifts;
timing += 0.010;
ax = this.bcdAdd(1, ax, 3, 1, 1); // --ax
am *= 0x10; // shift left
} else {
// Exponent underflow: set the reconstructed A to zero.
am = ax = sign = 0;
break;
}
// Exponent underflow: set the reconstructed A to zero.
am = ax = sign = 0;
break;
}
}
// Determine whether normalizing shifts exceed the limiter value
if (limiter > 0) {
if (limiter >= 8) {
limiter = 0;
} else if (shifts > limiter) {
limiter = 10 - (shifts-limiter);
this.setStop();
} else {
limiter = 0;
}
// Determine whether normalizing shifts exceed the limiter value
if (limiter > 0) {
if (limiter >= 8) {
limiter = 0;
} else if (shifts > limiter) {
limiter = 10 - (shifts-limiter);
this.SST.set(1); // limiter exceeded: set Single-Step
} else {
limiter = 0;
}
}
}
@ -1652,6 +1683,177 @@ B220Processor.prototype.floatingAdd = function floatingAdd(absolute) {
this.opTime = timing;
};
/**************************************/
B220Processor.prototype.floatingAdd__WITH_ROUND = function floatingAdd(absolute) {
/* Algebraically add the floating-point addend (IB) to the floating-point
augend (A), placing the result in A and clearing D. The R register is not
affected. All values are BCD with the sign in the 11th digit position.
The floating exponent is in the first two digit positions, biased by 50.
Sets Overflow and the Digit Check alarm as necessary. For more on the use
of the limiter digit in C/11 and the mechanization of floating add/subtract
on the 220, see United States Patent 3,022,006, 1962-02-20 */
/* THIS IS AN EXPERIMENTAL VERSION THAT ROUNDS RESULTS */
var ax; // augend exponent (binary)
var am = this.A.value % 0x10000000000; // augend mantissa (BCD)
var aSign = ((this.A.value - am)/0x10000000000)%2;
var compl; // complement addition required
var d; // scratch digit;
var dx; // addend exponent (binary)
var dm; // addend mantissa (BCD)
var dSign; // addend sign
var limiter = (this.CCONTROL - this.CCONTROL%0x1000)/0x1000; // normalizing limiter
var shifts = 0; // number of scaling/normalization shifts done
var sign; // local copy of sign toggle
var timing = 0.125; // minimum instruction timing
this.E.set(this.CADDR);
this.readMemory();
if (this.MET.value) { // invalid address
return; // exit to Operation Complete
}
dm = this.IB.value % 0x10000000000;
dSign = ((this.IB.value - dm)/0x10000000000)%2;
sign = (absolute ? 0 : dSign);
if (this.SUT.value) {
sign = 1-sign; // complement sign for subtraction
}
ax = (am - am%0x100000000)/0x100000000;
am %= 0x100000000;
dx = (dm - dm%0x100000000)/0x100000000;
dm %= 0x100000000;
am *= 0x100; // insert two low-order rounding digits
dm *= 0x100;
// If the exponents are unequal, scale the smaller
// until they are in alignment, or one mantissa becomes zero.
// Scale D until its exponent matches or the mantissa goes to zero.
while (ax > dx) {
if (++shifts < 8) {
timing += 0.010;
dx = this.bcdAdd(1, dx, 2, 0, 0); // ++dx
d = dm % 0x10;
dm = (dm - d)/0x10; // shift right
} else {
sign = aSign; // result is value in A
limiter = 0;
break;
}
}
// Scale A until its exponent matches or the mantissa goes to zero.
while (ax < dx) {
if (++shifts < 8) {
timing += 0.010;
ax = this.bcdAdd(1, ax, 3, 0, 0); // ++ax
d = am % 0x10;
am = (am - d)/0x10; // shift right
} else {
am = dm; // result is value in D with adjusted sign
ax = dx;
limiter = 0;
break;
}
}
// Add the mantissas
if (shifts < 8) {
compl = (aSign^sign);
am = this.bcdAdd(am, dm, 13, compl, compl);
// Now examine the resulting sign (still in the adder) to see if there
// is a carry and we need to recomplement the result and sign.
if (this.Z.value) {
// Reverse the sign toggle and recomplement the result.
sign = 1-sign;
am = this.bcdAdd(am, 0, 13, 1, 1);
timing += 0.060;
}
}
dm = dSign = 0; // Set D to its strange result value
dx = 0x10;
// Normalize or scale the result as necessary
if (am >= 0x10000000000) {
// Mantissa overflow: add/subtract can produce at most one digit of
// overflow, so scale by shifting right and incrementing the exponent,
// checking for overflow in the exponent.
limiter = 0;
if (ax < 0x99) {
timing += 0.005;
ax = this.bcdAdd(1, ax, 3, 0, 0); // ++ax
d = am % 0x10;
am = (am - d)/0x10; // shift right
} else {
// A scaling shift would overflow the exponent, so set the overflow
// toggle and leave the mantissa as it was from the add, without the
// exponent inserted back into it. Since the A register gets reassembled
// below, we need to set up the mantissa and exponent so the reconstruct
// will effectively do nothing.
this.OFT.set(1);
sign = ax = dx = limiter = 0;
}
} else if (am < 0x50) { // mantissa is zero
ax = sign = limiter = 0;
timing += 0.065;
} else { // normalize the result as necessary
shifts = 0;
while (am < 0x1000000000) { // NOTE: THIS INCLUDES THE ROUNDING DIGITS
if (ax > 0) {
++shifts;
timing += 0.010;
ax = this.bcdAdd(1, ax, 3, 1, 1); // --ax
am *= 0x10; // shift left
} else {
// Exponent underflow: set the reconstructed A to zero.
am = ax = sign = 0;
break;
}
}
// Determine whether normalizing shifts exceed the limiter value
if (limiter > 0) {
if (limiter >= 8) {
limiter = 0;
} else if (shifts > limiter) {
limiter = 10 - (shifts-limiter);
this.SST.set(1); // limiter exceeded: set Single-Step
} else {
limiter = 0;
}
}
}
// Rebuild the C register with the final normalization limiter
this.CCONTROL = this.CCONTROL%0x1000 + limiter*0x1000;
this.C.set((this.CCONTROL*0x100 + this.COP)*0x10000 + this.CADDR);
// Set toggles for display purposes and set the result.
d = am%0x100; // get the rounding digits
am = (am - am%0x100)/0x100; // scale back to 8 digits
if (d > 0x50) { // round required
am = this.bcdAdd(1, am, 11, 0, 0);
if (am >= 0x100000000) {
am = (am - am%0x10)/0x10;
ax = this.bcdAdd(1, ax, 3, 0, 0); // ignore exponent overflow, for now
}
}
this.AX.set(ax);
this.DX.set(dx);
this.DST.set(dSign);
this.SGT.set(sign);
this.A.set((sign*0x100 + ax)*0x100000000 + am);
this.D.set((dSign*0x100 + dx)*0x100000000 + dm);
this.opTime = timing;
};
/**************************************/
B220Processor.prototype.floatingMultiply = function floatingMultiply() {
/* Algebraically multiply the floating-point multiplicand in the IB register
@ -1739,7 +1941,6 @@ B220Processor.prototype.floatingMultiply = function floatingMultiply() {
am = (am-ad)/0x100;
rd = rm % 0x100;
rm = (rm-rd)/0x100 + ad*0x100000000;
ax = this.bcdAdd(0x02, ax, 3, 1, 1); // decrement exponent
} else if (ax > 0) {
// Shift product one place right
timing += 0.010;
@ -1955,33 +2156,44 @@ B220Processor.prototype.compareField = function compareField() {
}
// If the sign digit is included in the comparison, set up for algebraic
// comparison and the strange sign ordering.
// comparison and the strange sign ordering. This is tricky. Basically,
// the non-signed digits need to be compared in a signed, algebraic manner,
// but the transformed sign digits need to be compared unsigned. Since the
// compare is based on a signed subtraction, then if the original sign of
// either of the operands is negative, we use the 9s-complement of the
// transformed sign digit for that operand, converting the unsigned
// compare of the transformed sign digits into a signed one.
if (L > s) { // sign digit is included
rSign = (rw - rw%0x10000000000)/0x10000000000;
dSign = (dw - dw%0x10000000000)/0x10000000000;
sign = 1-dSign%2;
compl = (rSign^sign)%2;
compl = ((rSign%2)^sign);
carry = compl;
if (rSign < 8) {
rSign ^= 3; // complement two low bits of sign
if (rSign%2) { // complement the transformed sign
rSign = 9 - (rSign<8 ? rSign^3 : rSign);
} else if (rSign < 8) { // just transform the sign
rSign ^= 3;
}
rw = rw%0x10000000000 + rSign*0x10000000000;
if (dSign < 8) {
dSign ^= 3; // complement two low bits of sign
rw = rw%0x10000000000 + rSign*0x10000000000;
if (dSign%2) { // complement the transformed sign
dSign = 9 - (dSign<8 ? dSign^3 : dSign);
} else if (dSign < 8) {
dSign ^= 3; // just transform the sign
}
dw = dw%0x10000000000 + dSign*0x10000000000;
}
// Now go through a modified add cycle, subtracting the digit pairs using
// 9s-complement addition, and updating the comparison toggles for each digit.
this.DC.set(0x09); // set up to rotate 11 digits
while (this.DC.value < 0x20) {
// 10s-complement addition, and marking the result unequal if any digits differ.
this.DC.set(0x09); // set up to rotate through 11 digits
do {
rd = rw%0x10;
dd = dw%0x10;
if (s < 10) { // positition to the "s" digit
++s;
} else if (L > 0) {
} else if (L > 0) { // compare digits in the sL field
--L;
this.X.set(rd); // for display only
this.Y.set(dd);
@ -1997,25 +2209,24 @@ B220Processor.prototype.compareField = function compareField() {
if (adder) { // if the adder is not zero,
unequal = 1; // result will be unequal, determined by sign
}
} else {
// Ignore any digits after L is exhausted
} else { // Ignore any digits after L is exhausted
this.DC.set(0x19); // (the 220 didn't quit early like this, though)
}
// Shift both words right (no need to rotate them)
rw = (rw-rd)/0x10;
dw = (dw-dd)/0x10;
this.DC.inc();
} // while DC < 20
} while (this.DC.value < 0x20)
// If we are complementing and there is no final carry, we would normally
// decomplement the result and reverse the sign, but decomp is not needed.
// If we are not complementing and there is a final carry, we have overflow.
if (compl ^ carry) {
if (carry) {
unequal = 1; // overflow, so force unequality
} else {
sign = 1-sign; // reverse sign (pseudo decomplement)
// If there is a final carry, we keep the original sign; if we are not complementing,
// force an unequal result. If there is no final carry, we complement the result sign.
if (carry) {
if (!compl) {
unequal = 1;
}
} else {
sign = 1-sign;
}
// Set the console lamps and toggles to the result.
@ -2030,7 +2241,7 @@ B220Processor.prototype.compareField = function compareField() {
this.compareHighLamp.set(0);
}
this.DST.set(dSign);
this.DST.set(dSign%2);
this.SGT.set(sign);
this.HIT.set(high);
this.UET.set(unequal);
@ -4015,10 +4226,28 @@ B220Processor.prototype.ioInitiate = function ioInitiate() {
/* Initiates asynchronous mode of the processor for I/O */
this.AST.set(1);
this.updateLampGlow(1); // update the console lamps
this.updateLampGlow(0); // update the console lamps
this.execLimit = 0; // kill the run() loop
};
/**************************************/
B220Processor.prototype.traceState = function traceState() {
/* Logs a subset of the Processor state to the Javascript console for
debugging purposes */
console.log("P=" + B220Processor.padLeft(this.P.value.toString(16), 4) +
" | B=" + B220Processor.padLeft(this.B.value.toString(16), 4) +
" | C=" + B220Processor.formatWord(this.C.value).substring(2) +
" | A=" + B220Processor.formatWord(this.A.value) +
" | R=" + B220Processor.formatWord(this.R.value) +
" | D=" + B220Processor.formatWord(this.D.value) +
" | E=" + B220Processor.padLeft(this.E.value.toString(16), 4) +
" | UET=" + this.UET.value +
" | HIT=" + this.HIT.value +
" | OFT=" + this.OFT.value +
" | RPT=" + this.RPT.value);
};
/**************************************/
B220Processor.prototype.run = function run() {
/* Main execution control loop for the processor. Called from this.schedule()
@ -4040,6 +4269,10 @@ B220Processor.prototype.run = function run() {
if (this.EXT.value) { // enter EXECUTE cycle
this.execute();
} else { // enter FETCH cycle
if (this.tracing) {
this.traceState(); // DEBUG ONLY
}
if (this.SONSW) { // check for post-fetch S-to-P stop
if (this.STOPSW) { // must check before P is incremented in fetch()
if (this.SUNITSSW) {
@ -4147,7 +4380,7 @@ B220Processor.prototype.start = function start() {
this.runTimer -= stamp;
}
this.updateLampGlow(1);
this.updateLampGlow(1); // freeze state in the lamps
this.schedule();
}
};
@ -4219,7 +4452,7 @@ B220Processor.prototype.setCycle = function setCycle(cycle) {
};
/**************************************/
B220Processor.prototype.toggleCompare = function toggleCompare(condition) {
B220Processor.prototype.toggleCompareLamps = function toggleCompareLamps(condition) {
/* Toggles the comparison lamps and sets the processor UET and HIT toggles
according to the condition: <0=LOW, 0=EQUAL, >0=HIGH */

View File

@ -433,7 +433,7 @@ B220CardatronOutput.prototype.initiateWrite = function initiateWrite() {
case 1: // Relay 1 (eject page after printing)
case 9: // same as 1
this.printLine(line, this.pendingSpaceBefore);
this.pendingSpaceBefore = 0;
this.pendingSpaceBefore = -99;
break;
case 2: // Relay 2 (single space before and after printing)
this.printLine(line, this.pendingSpaceBefore+1);

View File

@ -33,6 +33,7 @@ function B220ConsolePrinter(mnemonic, unitIndex, config) {
this.tabStop = []; // 0-relative tab stop positions
this.zeroSuppress = 0; // zero-suppression switch setting
this.charPeriod = 0; // printer speed, ms/char
this.newLinePeriod = 0; // delay for carriage-returns
this.boundButton_Click = B220ConsolePrinter.prototype.button_Click.bind(this);
this.boundText_OnChange = B220ConsolePrinter.prototype.text_OnChange.bind(this);
@ -60,8 +61,11 @@ B220ConsolePrinter.offSwitchImage = "./resources/ToggleDown.png";
B220ConsolePrinter.onSwitchImage = "./resources/ToggleUp.png";
B220ConsolePrinter.ttySpeed = 10; // TTY printer speed, char/sec
B220ConsolePrinter.whippetSpeed = 200; // Whippet printer speed, char/sec
// Inter-character period, ms
B220ConsolePrinter.ttyNewLine = 200; // TTY carriage-return delay, ms
B220ConsolePrinter.whippetSpeed = 5000; // Whippet printer speed, char/sec
B220ConsolePrinter.whippetNewLine = 200;// Whippet carriage-return delay, ms
B220ConsolePrinter.formFeedPeriod = 500;// form-feed average delay, ms
B220ConsolePrinter.pageSize = 66; // lines/page for form-feed
B220ConsolePrinter.maxScrollLines = 15000;
// Maximum amount of paper scrollback
@ -282,8 +286,10 @@ B220ConsolePrinter.prototype.flipSwitch = function flipSwitch(ev) {
prefs.printerSpeed = this.speedSwitch.state;
if (this.speedSwitch.state) {
this.charPeriod = 1000/B220ConsolePrinter.whippetSpeed;
this.newLinePeriod = B220ConsolePrinter.whippetNewLine;
} else {
this.charPeriod = 1000/B220ConsolePrinter.ttySpeed;
this.newLinePeriod = B220ConsolePrinter.ttyNewLine;
}
break;
default:
@ -409,8 +415,10 @@ B220ConsolePrinter.prototype.printerOnLoad = function printerOnLoad() {
this.speedSwitch.set(prefs.printerSpeed);
if (this.speedSwitch.state) {
this.charPeriod = 1000/B220ConsolePrinter.whippetSpeed;
this.newLinePeriod = B220ConsolePrinter.whippetNewLine;
} else {
this.charPeriod = 1000/B220ConsolePrinter.ttySpeed;
this.newLinePeriod = B220ConsolePrinter.ttyNewLine;
}
mask = 0x001;
@ -538,13 +546,13 @@ B220ConsolePrinter.prototype.receiveChar = function receiveChar(char, successor)
break;
case 0x15: // form-feed
delay *= 4;
delay = B220ConsolePrinter.formFeedPeriod;
this.suppressLZ = 0;
this.printFormFeed();
break;
case 0x16: // carriage-return
delay *= 2;
delay = this.newLinePeriod;
this.suppressLZ = 0;
this.emptyLine();
break;
@ -565,7 +573,7 @@ B220ConsolePrinter.prototype.receiveChar = function receiveChar(char, successor)
this.printTab();
break;
case 2: // EOW = carriage-return
delay *= 2;
delay = this.newLinePeriod;
this.emptyLine();
break;
}

View File

@ -33,6 +33,9 @@
font-size: 36px;
font-weight: bold}
#B220Logo.tracing {
color: red}
#IntervalTimerResetCaption {
left: calc(50% - 206px);
bottom: 20px;

View File

@ -29,13 +29,15 @@
<div id=PanelSurface class=panelSurface>
<img id=BurroughsMeatball src="resources/Burroughs-Meatball.png">
<img id=BurroughsLogo src="resources/Burroughs-Logo.png">
<div id=B220Logo>2 2 0</div>
<div id=B220Logo
title="Double-click to toggle emulator tracking">2 2 0</div>
<div id=IntervalTimerResetCaption class=caption>ZERO TIMER</div>
<div id=IntervalTimerResetBtn class=blackButton3>&nbsp;</div>
<div id=IntervalTimer>0000.0</div>
<div id=PowerOffBtn class=redButton3>&nbsp;</div>
<div id=PowerOffBtn class=redButton3
title="Double-click to shut down emulator">&nbsp;</div>
<div id=PowerOffCaption class=caption>POWER OFF</div>
<div id=ARegPanel class=panelRegister></div>

View File

@ -469,7 +469,7 @@ B220ControlConsole.prototype.updatePanel = function updatePanel() {
text = (timer/1000 + 10000).toFixed(1);
this.intervalTimer.textContent = text.substring(text.length-6);
p.updateLampGlow(0);
p.updateLampGlow(p.AST.value ? 0.25 : 0);
eLevel = (p.RUT.value ? p.EXT.glow : p.EXT.value);
// Primary Registers
@ -762,13 +762,23 @@ B220ControlConsole.prototype.switch_Click = function switch_Click(ev) {
p.RPT.flip();
break;
case "LowLamp":
p.toggleCompare(-1);
p.toggleCompareLamps(-1);
break;
case "EqualLamp":
p.toggleCompare(0);
p.toggleCompareLamps(0);
break;
case "HighLamp":
p.toggleCompare(+1);
p.toggleCompareLamps(+1);
break;
case "B220Logo":
p.tracing = !p.tracing;
this.$$("LeftPanelBtn").focus(); // release any selection by the click
if (p.tracing) {
B220Util.addClass(ev.target, "tracing");
} else {
B220Util.removeClass(ev.target, "tracing");
}
break;
} // switch ev.target.id
}
@ -985,6 +995,7 @@ B220ControlConsole.prototype.consoleOnLoad = function consoleOnLoad() {
this.tcuClearSwitch.addEventListener("click", this.boundSwitch_Click);
this.$$("BurroughsMeatball").addEventListener("click", this.boundMeatballMemdump, false);
this.$$("B220Logo").addEventListener("dblclick", this.boundSwitch_Click);
this.$$("IntervalTimerResetBtn").addEventListener("click", this.boundResetTimer, false);
this.$$("PowerOffBtn").addEventListener("dblclick", this.boundPowerBtn_Click, false);

View File

@ -51,29 +51,47 @@
* represents the magnetic end-of-tape area written to newly-edited tape.
*
* External tape images are ordinary text files in comma-separated variable (CSV)
* format. Each line in the file represents one block for one lane. Each field
* on the line represents one word for the tape image. The first field on the
* line represents the lane number -- only its low-order bit is significant (0/1).
* The second field is the preface word indicating the length of the data. A
* block length of 100 is represented as either 0 or 100. Values greater than 100
* or less than zero will be treated as 100.
* format. Each line in the file represents one block for one lane. Lines may be
* delimited by ASCII line-feed (hex 0A), carriage-return (hex 0D) or a carriage-
* return/line-feed pair.
*
* Note that with this representation, it is possible that the count in the
* preface may not match the actual number of words on the rest of the line.
* The first field on the line contains one or two integers, formatted as "L" or
* "R*L". L represents the lane number. Only its low-order bit (0/1) is used.
* R is a repeat factor used to compress the size of tape image files. It
* indicates the number of copies of this block that exist consecutively at this
* point in the tape image. If R and its delimiting "*" are not present, R is
* assumed to be one. If R is not present, the "*" must also be not present.
*
* The second field on a line is the preface word indicating the length of the
* data. A block length of 100 may be represented as either 0 or 100. Values
* greater than 100 or less than zero will be treated as 100.
*
* The remaining fields on a line represent the data words of the block. Since
* words are stored internally in 4-bit BCD, these fields are interpreted as
* hexadecimal values, although normally they should be composed only using the
* decimal digits. Leading zero digits may be omitted. The digits of a word may
* be preceded by a hyphen ("-"), which will cause a 1 to be OR-ed into the sign
* digit of the word. Spaces may precede or follow the digits of a field, but may
* not appear within the digits or between any leading "-" and the first digit.
*
* Note that with this representation, it is possible that the word count in
* the preface may not match the actual number of words on the rest of the line.
* While this might be useful at some point to allow construction of invalid
* tape blocks that generate Tape Preface Failure (TPF) halts, at present the
* block is stored in the internal tape image with exactly the number of words
* specified by the preface. The remaining words on the line will be truncated
* specified by the preface. The words specified on the line will be truncated
* or padded with zero words as necessary in the internal image to achieve this
* block length.
*
* Also note that the arrangement of blocks with respect to their lane is
* arbitrary. Blocks for a lane can be arranged on the external image separately
* or intermingled with the blocks for the other lane. When exporting a tape
* image that has been modified, the drive will always dump all of lane 0 and
* then all of lane 1. To save space in the external image, trailing words of
* zeroes will be trimmed from each block. The lane number and preface word will
* written, however.
* or intermingled with the blocks for the other lane. The only requirement is
* that blocks for a lane be in sequence. When exporting a tape image that has
* been modified, the drive will always dump all of lane 0 and then all of
* lane 1. To save space in the external image, trailing words of zeroes will
* be trimmed from each block, and consecutive blocks containing the same data
* will be compressed using the R*L notation discussed above. The lane number
* and preface fields will always be written, however.
*
************************************************************************
* 2017-07-09 P.Kimpel
@ -610,9 +628,9 @@ B220MagTapeDrive.prototype.loadTape = function loadTape() {
}
function editedLoader() {
/* Loads an edited (blank) tape image into both lanes of the drive. If
a block length was chosen on the tape-load panel, initializes the image
with blocks of that size, followed by an end-of-tape block with
/* Loads an edited (blank) tape image into the drive. If a block length
was chosen on the tape-load panel, initializes the image with blocks of
that size, followed by an end-of-tape block in both lanes having the
controlWord (aaaa=0000, bbbb=0001) */
var blockLen;
var fmt = $$$("MTLoadFormatSelect").selectedIndex;
@ -656,25 +674,49 @@ B220MagTapeDrive.prototype.loadTape = function loadTape() {
/* Event handler for tape image file onLoad. Loads a text image as
comma-delimited decimal word values. No end-of-tape block is written
unless it is present in the text image */
var blockLen; // length of current tape block
var blockWords; // words in current tape block
var chunk; // ANSI text of current chunk
var chunkLength; // length of current ASCII chunk
var buf = ev.target.result; // ANSI tape image buffer
var bufLength = buf.length; // length of ANSI tape image buffer
var dups; // repeat factor for consecutive blocks
var eolRex = /([^\n\r\f]*)((:?\r[\n\f]?)|\n|\f)?/g;
var index = 0; // char index into tape image buffer for next chunk
var lane; // current tape lane image
var lx = [0,0]; // word indexes for each lane
var match; // result of eolRex.exec()
var preface; // preface word: block length in words
var repeatRex = /\s*(\d+)\s*\*\s*/; // regex to detect and parse repeat factor
var tx; // char index into ANSI chunk text
var wx; // word index within current block
function parseWord() {
/* Parses the next word from the chunk text and returns its value as BCD */
function parseRepeatFactor() {
/* Parses the repeat factor, if any, from the first field on the
line and returns its value. If there is no repeat factor, returns 1.
Leaves "tx" (the index into the line) pointing to the lane number */
var match; // result of regex match
var v; // parsed numeric value
match = repeatRex.exec(chunk);
if (!match) {
v = 1; // default if no repeat present
} else {
tx += match[0].length;
v = parseInt(match[1], 10);
if (isNaN(v)) {
v = 1; // default if repeat is non-numeric
}
}
return v;
}
function parseWord(radix) {
/* Parses the next word from the chunk text and returns its value as
determined by "radix" */
var cx; // offset to next comma
var text; // text of parsed word
var v; // parsed decimal value
var v; // parsed numeric value
var w = 0; // result BCD word
if (tx < chunkLength) {
@ -684,7 +726,7 @@ B220MagTapeDrive.prototype.loadTape = function loadTape() {
}
text = chunk.substring(tx, cx).trim();
if (text.length > 0) {
v = parseInt(text, 16); // parse as hex (BCD)
v = parseInt(text, radix);
if (!isNaN(v)) {
if (v > 0) {
w = v % 0x100000000000;
@ -716,36 +758,43 @@ B220MagTapeDrive.prototype.loadTape = function loadTape() {
chunk = match[1].trim();
chunkLength = chunk.length;
if (chunkLength > 0) { // ignore empty lines
tx = wx = 0;
mt.laneNr = parseWord()%2; // get the lane number
preface = parseWord(); // get the preface word as hex BCD
mt.imgIndex = lx[mt.laneNr]; // restore current offset for this lane
if (preface > 0x100) {
blockLen = 100; // limit blocks to 100 words
} else if (preface > 0) {
blockLen = B220Processor.bcdBinary(preface);
} else { // if the block length is 0, make it 100
blockLen = 100;
tx = 0;
dups = parseRepeatFactor(); // get the repeat factor, if any
mt.laneNr = parseWord(10)%2; // get the lane number
preface = parseWord(10); // get the preface word as decimal
if (preface > 100) { // limit blocks to 100 words
preface = 100;
} else if (preface < 1) { // if block length <= 0, make it 100
preface = 100;
}
writeBlockStart(blockLen);
blockWords = (preface == 1 ? 10 : preface); // pad out end-of-tape blocks to 10 words
lane = mt.image[mt.laneNr];
while (tx < chunkLength && wx < blockLen) {
lane[mt.imgIndex+wx] = parseWord();
mt.imgIndex = lx[mt.laneNr]; // restore internal offset for this lane
writeBlockStart(preface);
wx = 0; // load data words from tape image
while (tx < chunkLength && wx < preface) {
lane[mt.imgIndex+wx] = parseWord(16);
++wx;
} // while
} // while tx:wx
if (blockLen == 1) {
blockLen = 10; // pad out end-of-tape blocks to 10 words
}
while (wx < blockLen) {
while (wx < blockWords) { // pad block with zero words, if needed
lane[mt.imgIndex+wx] = 0;
++wx;
} // while
} // while wx
mt.imgIndex += blockLen;
mt.imgIndex += blockWords; // update the internal image offset
writeBlockEnd();
wx = lx[mt.laneNr]; // starting offset of block
blockWords = mt.imgIndex - wx; // total words in block, including overhead
while (dups > 1) { // repeat the block as necessary
--dups;
lane.copyWithin(mt.imgIndex, wx, wx+blockWords);
mt.imgIndex += blockWords;
} // while dups
lx[mt.laneNr] = mt.imgIndex; // save current offset for this lane
}
}
@ -757,9 +806,10 @@ B220MagTapeDrive.prototype.loadTape = function loadTape() {
mt.imgIndex = lx[mt.laneNr];
for (wx=0; wx<mt.startOfBlockWords*2; ++wx) {
lane[mt.imgIndex+wx] = mt.markerGap;
}
} // for wx
lx[mt.laneNr] = mt.imgIndex + mt.startOfBlockWords*2;
} // for tx (lane number)
} // for mt.laneNr
mt.imgTopWordNr = Math.max(lx[0], lx[1]);
mt.imgWritten = false;
@ -939,9 +989,11 @@ B220MagTapeDrive.prototype.unloadTape = function unloadTape() {
/* Converts the tape image to ASCII once the window has displayed the
waiting message */
var buf; // ANSI tape image buffer
var dups = 0; // block repeat count
var imgLength = mt.imgLength; // active words in tape image
var imgTop = mt.imgTopWordNr; // tape image last block number
var lane; // lane image buffer
var lastBuf = ""; // last block image output
var lx; // lane index
var nzw; // number of consecutive zero words
var state; // lane processing state variable
@ -958,7 +1010,7 @@ B220MagTapeDrive.prototype.unloadTape = function unloadTape() {
lane = mt.image[lx];
state = 1;
x = 0;
while (x < imgLength) {
do {
switch (state) {
case 1: // Search for start of block
nzw = 0;
@ -966,25 +1018,36 @@ B220MagTapeDrive.prototype.unloadTape = function unloadTape() {
w = findBlockStart();
if (w < 0) { // done with this lane
x = imgLength; // kill the loop
} else { // format the preface word
} else { // format the lane number and preface word
buf = lx.toString(10) + "," + w.toString(10);
x = mt.imgIndex;
state = 2;
state = 2; // switch state to blocking data words
}
break;
case 2: // Record the block data words
w = lane[x++];
if (w == 0) {
++nzw; // suppress trailing zero words
} else if (w >= 0) {
if (w == 0) { // suppress trailing zero words
++nzw;
} else if (w >= 0) {// buffer the current word
while (nzw > 0) {
--nzw; // restore non-trailing zero words
--nzw; // restore non-trailing zero words
buf += ",0";
}
buf += "," + w.toString(16);
} else {
image.appendChild(doc.createTextNode(buf + "\n"));
state = 1; // reset for next block
} else { // output the last block image(s)
state = 1; // reset state for next block
if (buf == lastBuf) { // compress consecutive duplicate blocks
++dups;
} else {
if (dups > 1) {
image.appendChild(doc.createTextNode(dups.toString(10) + "*" + lastBuf + "\n"));
} else if (dups > 0) {
image.appendChild(doc.createTextNode(lastBuf + "\n"));
}
lastBuf = buf;
dups = 1;
}
}
break;
default:
@ -992,7 +1055,17 @@ B220MagTapeDrive.prototype.unloadTape = function unloadTape() {
throw new Error("Invalid state: B220MagTapeDrive.unloadTape, " + state);
break;
} // switch state
} // while not at end of lane
} while (x < imgLength);
// Output the final block(s) for the lane.
if (dups > 1) {
image.appendChild(doc.createTextNode(dups.toString(10) + "*" + lastBuf + "\n"));
} else if (dups > 0) {
image.appendChild(doc.createTextNode(lastBuf + "\n"));
}
dups = 0;
lastBuf = "";
} // for lx
mt.setTapeUnloaded();

View File

@ -38,7 +38,6 @@
<div id=UnitDesignateKnobCaption class=caption>UNIT DESIGNATE</div>
<select id=UnitDesignateKnob>
<option value= 0>SPO
<option value= 1>1
<option value= 2>2
<option value= 3>3

View File

@ -197,7 +197,7 @@ B220PaperTapeReader.prototype.flipSwitch = function flipSwitch(ev) {
/* Handler for switch clicks */
var id = ev.target.id;
var prefs = this.config.getNode("ConsoleInput.units", this.unitIndex);
var x;
var x = 0;
switch (id) {
case "RemoteSwitch":
@ -214,12 +214,12 @@ B220PaperTapeReader.prototype.flipSwitch = function flipSwitch(ev) {
case "UnitDesignateKnob":
x = this.unitDesignateKnob.selectedIndex;
if (x < 0) {
x = this.unitDesignateKnob.length-1;
this.unitMask = 0;
} else {
this.unitMask = B220Processor.pow2[x];
prefs.unitMask = this.unitMask
this.unitMask = B220Processor.pow2[x+1];
}
prefs.unitMask = this.unitMask;
break;
}
@ -258,11 +258,11 @@ B220PaperTapeReader.prototype.readerOnload = function readerOnload() {
this.charPeriod = (this.speedSwitch.state ? B220PaperTapeReader.highSpeedPeriod : B220PaperTapeReader.lowSpeedPeriod);
this.unitDesignateKnob = this.$$("UnitDesignateKnob");
mask = 0x001;
this.unitMask = prefs.unitMask;
if (this.unitMask == 0) {
this.unitDesignateKnob.selectedIndex = this.unitDesignateKnob.length-1; // OFF
} else {
mask = 0x002; // ignore the 1-bit (used for SPO on output devices)
for (x=0; x<this.unitDesignateKnob.length; ++x) {
if (this.unitMask & mask) {
this.unitDesignateKnob.selectedIndex = x;