diff --git a/emulator/B220Processor.js b/emulator/B220Processor.js index 865e66c..f19d47c 100644 --- a/emulator/B220Processor.js +++ b/emulator/B220Processor.js @@ -229,22 +229,22 @@ function B220Processor(config, devices) { this.rightPanelOpen = false; // Context-bound routines - this.boundUpdateLampGlow = B220Processor.bindMethod(this, B220Processor.prototype.updateLampGlow); + this.boundConsoleOutputSign = B220Processor.prototype.consoleOutputSign.bind(this); + this.boundConsoleOutputChar = B220Processor.prototype.consoleOutputChar.bind(this); + this.boundConsoleOutputFinished = B220Processor.prototype.consoleOutputFinished.bind(this); + this.boundConsoleInputReceiveChar = B220Processor.prototype.consoleInputReceiveChar.bind(this); + this.boundConsoleInputInitiateNormal = B220Processor.prototype.consoleInputInitiateNormal.bind(this); + this.boundConsoleInputInitiateInverse = B220Processor.prototype.consoleInputInitiateInverse.bind(this); - this.boundConsoleOutputSign = B220Processor.bindMethod(this, B220Processor.prototype.consoleOutputSign); - this.boundConsoleOutputChar = B220Processor.bindMethod(this, B220Processor.prototype.consoleOutputChar); - this.boundConsoleOutputFinished = B220Processor.bindMethod(this, B220Processor.prototype.consoleOutputFinished); - this.boundConsoleInputReceiveChar = B220Processor.bindMethod(this, B220Processor.prototype.consoleInputReceiveChar); - this.boundConsoleInputInitiateNormal = B220Processor.bindMethod(this, B220Processor.prototype.consoleInputInitiateNormal); - this.boundConsoleInputInitiateInverse = B220Processor.bindMethod(this, B220Processor.prototype.consoleInputInitiateInverse); + this.boundCardatronOutputWord= B220Processor.prototype.cardatronOutputWord.bind(this); + this.boundCardatronOutputFinished = B220Processor.prototype.cardatronOutputFinished.bind(this); + this.boundCardatronReceiveWord = B220Processor.prototype.cardatronReceiveWord.bind(this); - this.boundCardatronOutputWord= B220Processor.bindMethod(this, B220Processor.prototype.cardatronOutputWord); - this.boundCardatronOutputFinished = B220Processor.bindMethod(this, B220Processor.prototype.cardatronOutputFinished); - this.boundCardatronReceiveWord = B220Processor.bindMethod(this, B220Processor.prototype.cardatronReceiveWord); + this.boundMagTapeComplete = B220Processor.prototype.magTapeComplete.bind(this); + this.boundMagTapeReceiveWord = B220Processor.prototype.magTapeReceiveWord.bind(this); + this.boundMagTapeSendWord = B220Processor.prototype.magTapeSendWord.bind(this); - this.boundMagTapeComplete = B220Processor.bindMethod(this, B220Processor.prototype.magTapeComplete); - this.boundMagTapeReceiveWord = B220Processor.bindMethod(this, B220Processor.prototype.magTapeReceiveWord); - this.boundMagTapeSendWord = B220Processor.bindMethod(this, B220Processor.prototype.magTapeSendWord); + this.boundIoComplete = B220Processor.prototype.ioComplete.bind(this); this.clear(); // Create and initialize the processor state @@ -256,13 +256,13 @@ function B220Processor(config, devices) { * Global Constants * ***********************************************************************/ -B220Processor.version = "0.03b"; +B220Processor.version = "0.03c"; B220Processor.tick = 1000/200000; // milliseconds per clock cycle (200KHz) B220Processor.cyclesPerMilli = 1/B220Processor.tick; // clock cycles per millisecond (200 => 200KHz) B220Processor.timeSlice = 10; // maximum processor time slice, ms -B220Processor.delayAlpha = 0.001; // decay factor for exponential weighted average delay +B220Processor.delayAlpha = 0.0001; // decay factor for exponential weighted average delay B220Processor.delayAlpha1 = 1-B220Processor.delayAlpha; B220Processor.slackAlpha = 0.0001; // decay factor for exponential weighted average slack B220Processor.slackAlpha1 = 1-B220Processor.slackAlpha; @@ -322,15 +322,6 @@ B220Processor.emptyFunction = function emptyFunction() { return; }; -/**************************************/ -B220Processor.bindMethod = function bindMethod(context, f) { - /* Returns a new function that binds the function "f" to the object "context". - Note that this is a static constructor property function, NOT an instance - method of the CC object */ - - return function bindMethodAnon() {return f.apply(context, arguments)}; -}; - /**************************************/ B220Processor.bcdBinary = function bcdBinary(v) { /* Converts the BCD value "v" to a binary number and returns it. If the @@ -2834,21 +2825,19 @@ B220Processor.prototype.cardatronReceiveWord = function cardatronReceiveWord(wor ***********************************************************************/ /**************************************/ -B220Processor.prototype.magTapeComplete = function magTapeComplete(alarm, control, word) { +B220Processor.prototype.magTapeComplete = function magTapeComplete(control, word) { /* Call-back routine to signal completion of a magnetic tape operation. If - "alarm" is true, the Magnetic Tape Alarm will be set. If "control" is true, - the contents of "word" are processed as a tape control word and an appropriate - branch is set up. Unconditionally terminates the tape I/O instruction */ + "control" is true, the contents of "word" are processed as a tape control + word and an appropriate branch is set up. Unconditionally terminates the + tape I/O instruction */ var aaaa; var bbbb; - if (alarm) { - this.setMagneticTapeCheck(true); - } else if (control) { + if (control) { this.D.set(word); bbbb = word%0x10000; aaaa = ((word - bbbb)/0x10000)%0x10000; - if ((word - word%0x10000000000)%2) { // if sign bit is 1, + if (word%0x20000000000 >= 0x10000000000) { // if sign bit is 1, bbbb = this.bcdAdd(bbbb, this.B.value, 4); // B-adjust the low-order 4 digits } @@ -2862,13 +2851,13 @@ B220Processor.prototype.magTapeComplete = function magTapeComplete(alarm, contro } } - this.ioComplete(true); + Promise.resolve(true).then(this.boundIoComplete); }; /**************************************/ -B220Processor.prototype.magTapeSendWord = function magTapeSendWord(initial) { +B220Processor.prototype.magTapeSendWord = function magTapeSendWord(initialFetch) { /* Sends the next of data from memory to the tape control unit, starting at - the current operand address in the C register. "initial" is true if this + the current operand address in the C register. "initialFetch" is true if this call is the first to fetch words for a block. This causes the routine to save the current operand address in the control digits of C. Returns binary -1 if the processor has been cleared or a memory address error @@ -2878,7 +2867,7 @@ B220Processor.prototype.magTapeSendWord = function magTapeSendWord(initial) { if (!this.AST.value) { result = -1; // we've probably been cleared } else { - if (initial) { + if (initialFetch) { this.clockIn(); this.CCONTROL = this.CADDR; // copy C address into control digits } @@ -2900,9 +2889,9 @@ B220Processor.prototype.magTapeSendWord = function magTapeSendWord(initial) { }; /**************************************/ -B220Processor.prototype.magTapeReceiveWord = function magTapeReceiveWord(initial, word) { +B220Processor.prototype.magTapeReceiveWord = function magTapeReceiveWord(initialStore, word) { /* Stores the next of data from the tape control unit to memory, starting at - the current operand address in the C register. "initial" is true if this + the current operand address in the C register. "initialStore" is true if this call is the first to store words for a block. This causes the routine to save the current operand address in the control digits of C. Returns binary -1 if the processor has been cleared or a memory address error @@ -2913,7 +2902,7 @@ B220Processor.prototype.magTapeReceiveWord = function magTapeReceiveWord(initial if (!this.AST.value) { result = -1; // we've probably been cleared } else { - if (initial) { + if (initialStore) { this.clockIn(); this.CCONTROL = this.CADDR; // copy C address into control digits } @@ -2923,7 +2912,7 @@ B220Processor.prototype.magTapeReceiveWord = function magTapeReceiveWord(initial this.C.set((this.CCONTROL*0x100 + this.COP)*0x10000 + this.CADDR); this.D.set(word); if (this.vDigit & 0x08) { // B-adjustment of words is enabled - sign = (word - word%0x10000000000); + sign = (word - word%0x10000000000)/0x10000000000; if (sign & 0x08) { // this word is to be B-adjusted word = (sign&0x07)*0x10000000000 + word%0x10000000000 - word%0x100000 + this.bcdAdd(word, this.B.value, 4); @@ -3601,22 +3590,33 @@ B220Processor.prototype.execute = function execute() { this.vDigit = this.CCONTROL%0x10; this.ioInitiate(); if (this.vDigit & 0x08) { // MRW/MDA: rewind, with or without lockout - this.magTape.rewind(this.D.value, this.boundMagTapeComplete, this.boundMagTapeSendWord); + this.magTape.rewind(this.D.value); } else if (this.vDigit & 0x04) { // MLS: lane select - this.magTape.laneSelect(this.D.value, this.boundMagTapeComplete, this.boundMagTapeSendWord); + this.magTape.laneSelect(this.D.value); } else { // MTS/MFS: search or field search - if (this.D.value%0x80000000000 < 0x40000000000) { // full-word search - this.magTape.search(this.D.value, this.boundMagTapeComplete, 0, this.boundMagTapeSendWord); - } else { // partial-word search based on sL in B - this.magTape.search(this.D.value, this.boundMagTapeComplete, this.B.value, this.boundMagTapeSendWord); + if (this.D.value%0x80000000000 < 0x40000000000) { // sign 4-bit = 0: full-word search + this.magTape.search(this.D.value, 0); + } else { // partial-word search based on sL00 in B + this.magTape.search(this.D.value, this.B.value); } } } break; case 0x51: //--------------------- MTC/MFC Magnetic tape scan/field scan - this.setProgramCheck(1); - this.operationComplete(); + this.opTime = 0.160; + if (!this.magTape) { + this.setMagneticTapeCheck(true); // no tape control + this.operationComplete(); + } else { + this.selectedUnit = (this.CCONTROL >>> 12)%0x10; + this.ioInitiate(); + if (this.D.value%0x80000000000 < 0x40000000000) { // sign 4-bit = 0: full-word search + this.magTape.scan(this.D.value, 0); + } else { // partial-word search based on sL00 in B + this.magTape.scan(this.D.value, this.B.value); + } + } break; case 0x52: //--------------------- MRD Magnetic tape read @@ -3626,9 +3626,9 @@ B220Processor.prototype.execute = function execute() { this.operationComplete(); } else { this.selectedUnit = (this.CCONTROL >>> 12)%0x10; - this.vDigit = this.CCONTROL%0x10; + this.vDigit = this.CCONTROL%0x10; // controlword and B-mod bits this.ioInitiate(); - this.magTape.read(this.D.value, this.boundMagTapeComplete, false, this.boundMagTapeReceiveWord); + this.magTape.read(this.D.value, false); } break; @@ -3639,9 +3639,9 @@ B220Processor.prototype.execute = function execute() { this.operationComplete(); } else { this.selectedUnit = (this.CCONTROL >>> 12)%0x10; - this.vDigit = this.CCONTROL%0x10; + this.vDigit = this.CCONTROL%0x10; // controlword and B-mod bits this.ioInitiate(); - this.magTape.read(this.D.value, this.boundMagTapeComplete, true, this.boundMagTapeReceiveWord); + this.magTape.read(this.D.value, true); } break; @@ -3653,7 +3653,7 @@ B220Processor.prototype.execute = function execute() { } else { this.selectedUnit = (this.CCONTROL >>> 12)%0x10; this.ioInitiate(); - this.magTape.initialWrite(this.D.value, this.boundMagTapeComplete, false, this.boundMagTapeSendWord); + this.magTape.initialWrite(this.D.value, false); } break; @@ -3665,7 +3665,7 @@ B220Processor.prototype.execute = function execute() { } else { this.selectedUnit = (this.CCONTROL >>> 12)%0x10; this.ioInitiate(); - this.magTape.initialWrite(this.D.value, this.boundMagTapeComplete, true, this.boundMagTapeSendWord); + this.magTape.initialWrite(this.D.value, true); } break; @@ -3677,7 +3677,7 @@ B220Processor.prototype.execute = function execute() { } else { this.selectedUnit = (this.CCONTROL >>> 12)%0x10; this.ioInitiate(); - this.magTape.overwrite(this.D.value, this.boundMagTapeComplete, false, this.boundMagTapeSendWord); + this.magTape.overwrite(this.D.value, false); } break; @@ -3689,7 +3689,7 @@ B220Processor.prototype.execute = function execute() { } else { this.selectedUnit = (this.CCONTROL >>> 12)%0x10; this.ioInitiate(); - this.magTape.overwrite(this.D.value, this.boundMagTapeComplete, true, this.boundMagTapeSendWord); + this.magTape.overwrite(this.D.value, true); } break; @@ -3703,13 +3703,13 @@ B220Processor.prototype.execute = function execute() { this.ioInitiate(); switch (this.CCONTROL%0x10) { case 1: // MPB: position tape backward - this.magTape.positionBackward(this.D.value, this.boundMagTapeComplete); + this.magTape.positionBackward(this.D.value); break; case 2: // MPE: position tape at end - this.magTape.positionAtEnd(this.D.value, this.boundMagTapeComplete); + this.magTape.positionAtEnd(this.D.value); break; default: // MPF: position tape forward - this.magTape.positionForward(this.D.value, this.boundMagTapeComplete); + this.magTape.positionForward(this.D.value); break; } // switch on operation variant } @@ -3723,7 +3723,7 @@ B220Processor.prototype.execute = function execute() { } else { opTime = 0.14; if (this.CCONTROL%0x10 == 1) { // MIE - if (this.magTape.testUnitAtMagneticEOT(this.D.value)) { + if (this.magTape.testUnitAtEOT(this.D.value)) { this.P.set(this.CADDR); this.opTime += 0.020; } @@ -4257,19 +4257,20 @@ B220Processor.prototype.loadDefaultProgram = function loadDefaultProgram() { this.MM[ 2] = 0x1000540000; // MIW 0,1,10,100 this.MM[ 3] = 0x1750540100; // MIW 100,1,7,50 this.MM[ 4] = 0x1500550079; // MIR 79,1,5,00 - this.MM[ 5] = 0x1101542000; // MIW 2000,1,1,1 // write control block + this.MM[ 5] = 0x1101542000; // MIW 2000,1,1,1 // write an EOT block this.MM[ 6] = 0x1008500000; // MRW 1 this.MM[ 7] = 0x1000560000; // MOW 0,1,10,100 this.MM[ 8] = 0x1750560100; // MOW 100,1,7,50 this.MM[ 9] = 0x1500570079; // MOR 79,1,5,00 - this.MM[ 10] = 0x1101012000; // MOW 2000,1,1,1 // TEMP: changed to a NOP + //this.MM[ 10] = 0x1101562000; // MOW 2000,1,1,1 + this.MM[ 10] = 0x1110562000; // MOW 2000,1,1,10 // TEMP: block-length=10, should fire EOT control word this.MM[ 11] = 0x1008500000; // MRW 1 this.MM[ 12] = 0x1000523000; // MRD 3000,1,10,0 this.MM[ 13] = 0x1700524000; // MRD 4000,1,7,0 this.MM[ 14] = 0x1500534350; // MRR 4350,1,5,0 - this.MM[ 15] = 0x1100534800; // MRR 4800,1,1,0 // should be a control block + this.MM[ 15] = 0x1100534800; // MRR 4800,1,1,0 // should be an EOT block this.MM[ 16] = 0x1009500000; // MDA 1 this.MM[ 17] = 0x7777009999; // HLT 9999,7777 @@ -4283,6 +4284,7 @@ B220Processor.prototype.loadDefaultProgram = function loadDefaultProgram() { this.MM[2000] = 0x9920012002; // end-of-tape control word this.MM[2001] = 0x9999999999; // storage for end-of-tape block state this.MM[2002] = 0x9999008421; // HLT: target for end-of-tape control branch + this.MM[2003] = 0x0000300011; // branch to read test sequence // Simple counter speed test this.MM[ 80] = 0x0000120082; // ADD 82 diff --git a/webUI/B220ConsolePrinter.js b/webUI/B220ConsolePrinter.js index 6b8aea2..22f4d23 100644 --- a/webUI/B220ConsolePrinter.js +++ b/webUI/B220ConsolePrinter.js @@ -66,16 +66,22 @@ B220ConsolePrinter.maxScrollLines = 15000; // Maximum amount of paper scrollback B220ConsolePrinter.codeXlate = [ // translate internal B220 code to ANSI - " ", "?", " ", ".", "\u00A4", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", // 00-0F - "&", "?", "?", "$", "*", "\f", "\n", "?", "?", "?", "?", "?", "?", "?", "?", "?", // 10-1F - "-", "/", "?", ",", "%", "?", "\t", "?", "?", "?", "?", "?", "?", "?", "?", "?", // 20-2F - "?", "?", "?", "#", "@", "!", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", // 30-3F - "?", "A", "B", "C", "D", "E", "F", "G", "H", "I", "?", "?", "?", "?", "?", "?", // 40-4F - "?", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "?", "?", "?", "?", "?", "?", // 50-5F - "?", "?", "S", "T", "U", "V", "W", "X", "Y", "Z", "?", "?", "?", "?", "?", "?", // 60-6F - "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", // 70-7F - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "?", "?", "?", "?", "?", "?", // 80-8F - "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?"]; // 90-9F + " ", "?", " ", ".", "\u00A4", "?", "?", "?", "?", "?", "!", "!", "!", "!", "!", "!", // 00-0F + "&", "?", "?", "$", "*", "\f", "\n", "?", "?", "?", "!", "!", "!", "!", "!", "!", // 10-1F + "-", "/", "?", ",", "%", "?", "\t", "?", "?", "?", "!", "!", "!", "!", "!", "!", // 20-2F + "?", "?", "?", "#", "@", "\\", "?", "?", "?", "?", "!", "!", "!", "!", "!", "!", // 30-3F + "?", "A", "B", "C", "D", "E", "F", "G", "H", "I", "!", "!", "!", "!", "!", "!", // 40-4F + "?", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "!", "!", "!", "!", "!", "!", // 50-5F + "?", "?", "S", "T", "U", "V", "W", "X", "Y", "Z", "!", "!", "!", "!", "!", "!", // 60-6F + "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "!", "!", "!", "!", "!", "!", // 70-7F + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "!", "!", "!", "!", "!", "!", // 80-8F + "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "!", "!", "!", "!", "!", "!", // 90-9F + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // A0-AF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // B0-BF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // C0-CF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // D0-DF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // E0-EF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!"]; // F0-FF /**************************************/ diff --git a/webUI/B220ControlConsole.css b/webUI/B220ControlConsole.css index 8b278e2..6c2e87b 100644 --- a/webUI/B220ControlConsole.css +++ b/webUI/B220ControlConsole.css @@ -34,8 +34,8 @@ font-weight: bold} #IntervalTimerResetCaption { - bottom: 21px; left: calc(50% - 206px); + bottom: 20px; width: 120px; text-align: right} #IntervalTimerResetBtn { @@ -69,7 +69,7 @@ bottom: 12px} #PowerOffCaption { left: calc(50% + 92px); - bottom: 21px} + bottom: 20px} #VersionDiv { right: 150px; @@ -85,6 +85,25 @@ right: 12px; bottom: 12px} +#ProcDelta { + position: absolute; + width: 64px; + text-align: right; + left: 120px; + bottom: 12px} +#ProcSlack { + position: absolute; + width: 64px; + text-align: right; + left: 190px; + bottom: 12px} +#ProcRun { + position: absolute; + width: 64px; + text-align: right; + left: 260px; + bottom: 12px} + #PanelSurface { height: 100%; width: 100%} diff --git a/webUI/B220ControlConsole.html b/webUI/B220ControlConsole.html index 3ad2b07..16503cd 100644 --- a/webUI/B220ControlConsole.html +++ b/webUI/B220ControlConsole.html @@ -148,17 +148,16 @@ +
+
+
+
retro-220 ?.??
-
- Proc Delta: -    - Latency: - diff --git a/webUI/B220ControlConsole.js b/webUI/B220ControlConsole.js index 641278b..e2d4c10 100644 --- a/webUI/B220ControlConsole.js +++ b/webUI/B220ControlConsole.js @@ -17,7 +17,7 @@ function B220ControlConsole(p, systemShutdown) { /* Constructor for the ControlConsole object */ var h = 600; var w = 1064; - var mnemonic = "ControlConsole"; + var mnemonic = "Console"; var inputConfig = p.config.getNode("ConsoleInput"); var outputConfig = p.config.getNode("ConsoleOutput"); var u; @@ -30,12 +30,12 @@ function B220ControlConsole(p, systemShutdown) { this.keyboard = new B220ConsoleKeyboard(p); - this.boundLamp_Click = B220Util.bindMethod(this, B220ControlConsole.prototype.lamp_Click); - this.boundPowerBtn_Click = B220Util.bindMethod(this, B220ControlConsole.prototype.powerBtn_Click); - this.boundSwitch_Click = B220Util.bindMethod(this, B220ControlConsole.prototype.switch_Click); - this.boundStartBtn_Click = B220Util.bindMethod(this, B220ControlConsole.prototype.startBtn_Click); - this.boundResetTimer = B220Util.bindMethod(this, B220ControlConsole.prototype.resetTimer); - this.boundUpdatePanel = B220Util.bindMethod(this, B220ControlConsole.prototype.updatePanel); + this.boundMeatballMemdump = B220ControlConsole.prototype.meatballMemdump.bind(this); + this.boundLamp_Click = B220ControlConsole.prototype.lamp_Click.bind(this); + this.boundPowerBtn_Click = B220ControlConsole.prototype.powerBtn_Click.bind(this); + this.boundSwitch_Click = B220ControlConsole.prototype.switch_Click.bind(this); + this.boundResetTimer = B220ControlConsole.prototype.resetTimer.bind(this); + this.boundUpdatePanel = B220ControlConsole.prototype.updatePanel.bind(this); // Configure the console input unit objects. These are paper-tape readers. this.inputUnit = [ @@ -109,6 +109,24 @@ B220ControlConsole.onSwitchImage = "./resources/ToggleUp.png"; B220ControlConsole.offOrganSwitchImage = "./resources/Organ-Switch-Up.png" B220ControlConsole.onOrganSwitchImage = "./resources/Organ-Switch-Down.png" +B220ControlConsole.codeXlate = [ // translate internal B220 code to ANSI + " ", "_", " ", ".", "\u00A4", "_", "_", "_", "_", "_", "!", "!", "!", "!", "!", "!", // 00-0F + "&", "_", "_", "$", "*", "^", "~", "_", "_", "_", "!", "!", "!", "!", "!", "!", // 10-1F + "-", "/", "_", ",", "%", "_", "|", "_", "_", "_", "!", "!", "!", "!", "!", "!", // 20-2F + "_", "_", "_", "#", "@", "\\", "_", "_", "_", "_", "!", "!", "!", "!", "!", "!", // 30-3F + "_", "A", "B", "C", "D", "E", "F", "G", "H", "I", "!", "!", "!", "!", "!", "!", // 40-4F + "_", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "!", "!", "!", "!", "!", "!", // 50-5F + "_", "_", "S", "T", "U", "V", "W", "X", "Y", "Z", "!", "!", "!", "!", "!", "!", // 60-6F + "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "!", "!", "!", "!", "!", "!", // 70-7F + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "!", "!", "!", "!", "!", "!", // 80-8F + "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "!", "!", "!", "!", "!", "!", // 90-9F + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // A0-AF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // B0-BF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // C0-CF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // D0-DF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", // E0-EF + "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!", "!"]; // F0-FF + /**************************************/ B220ControlConsole.prototype.$$ = function $$(e) { return this.doc.getElementById(e); @@ -150,6 +168,234 @@ B220ControlConsole.prototype.beforeUnload = function beforeUnload(ev) { return msg; }; +/**************************************/ +B220ControlConsole.prototype.meatballMemdump = function meatballMemdump() { + /* Opens a temporary window and formats the current processor and memory + state to it */ + var doc = null; // loader window.document + var p = this.p; // local copy of Processor object + var paper = null; //
 element to receive dump lines
+    var trimRightRex = /[\s\uFEFF\xA0]+$/;
+    var win = this.window.open("./B220FramePaper.html", this.mnemonic + "-MEMDUMP",
+            "location=no,scrollbars=yes,resizable,width=800,height=600");
+    var xlate = B220ControlConsole.codeXlate; // local copy
+
+    function formatWord(w) {
+        /* Formats a 220 numeric word as "S DDDDDDDDDD" and returns it */
+        var s = padBCD(w, 11);
+
+        return s.substring(0, 1) + " " + s.substring(1);
+    }
+
+    function padBCD(value, digits) {
+        /* Formats "value" as a BCD number of "digits" length, left-padding with
+        zeroes as necessary */
+        var text = value.toString(16);
+
+        if (value < 0) {
+            return text;
+        } else {
+            return padLeft(text, digits, "0");
+        }
+    }
+
+    function padLeft(text, minLength, c) {
+        /* Pads "text" on the left to a total length of "minLength" with "c" */
+        var s = text.toString();
+        var len = s.length;
+        var pad = c || " ";
+
+        while (len++ < minLength) {
+            s = pad + s;
+        }
+        return s;
+    }
+
+    function trimRight(text) {
+        /* Returns the string with all terminating whitespace removed from "text" */
+
+        return text.replace(trimRightRex, '');
+    }
+
+    function wordToANSI(value) {
+        /* Converts the "value" as a 220 word to a five-character string and returns it */
+        var c;                              // current character
+        var s = "";                         // working string value
+        var w = value;                      // working word value
+        var x;                              // character counter
+
+        for (x=0; x<5; ++x) {
+            c = w % 256;
+            w = (w-c)/256;
+            s = xlate[c] + s;
+        }
+
+        return s;
+    }
+
+    function writer(text) {
+        /* Outputs one line of text to the dump window */
+
+        paper.appendChild(doc.createTextNode(trimRight(text) + "\n"));
+    }
+
+    function dumpProcessorState() {
+        /* Dumps the register state for the Processor */
+        var s = "";
+        var x = 0;
+
+        writer("");
+        writer("Processor:");
+        writer("");
+        writer("A: " + formatWord(p.A.value) + "     R: " + formatWord(p.R.value) +
+               "      D: " + formatWord(p.D.value));
+        writer("");
+        s = padBCD(p.C.value, 10);
+        s = s.substring(0, 4) + " " + s.substring(4, 6) + " " + s.substring(6);
+        writer("B: " + padBCD(p.B.value, 4) + "   P: " + padBCD(p.P.value, 4) + "   C: " + s +
+               "      E: " + padBCD(p.E.value, 4) + "   S: " + padBCD(p.S.value, 4));
+
+        writer("");
+        s = "Program Switches:";
+        for (x=1; x<=10; ++x) {
+            if (p["PC" + x%10 + "SW"]) {
+                s += " " + x%10;
+            } else {
+                s += " .";
+            }
+        }
+
+        if (p.SONSW)    {s += " S-ON"}
+        if (p.SUNITSSW) {s += " S-UNITS"}
+        if (p.STOCSW)   {s += " S-TO-C"}
+        if (p.STOPSW)   {s += " S-TO-P"}
+        writer(s);
+
+        s = "Flip-Flops:";
+        if (p.digitCheckAlarm.value)    {s += " DCK"}
+        if (p.ALT.value)                {s += " ALT"}
+        if (p.AST.value)                {s += " AST"}
+        if (p.CCT.value)                {s += " CCT"}
+        if (p.CRT.value)                {s += " CRT"}
+        if (p.DPT.value)                {s += " DPT"}
+        if (p.EWT.value)                {s += " EWT"}
+        if (p.EXT.value)                {s += " EXT"}
+        if (p.HAT.value)                {s += " HAT"}
+        if (p.HCT.value)                {s += " HCT"}
+        if (p.HIT.value)                {s += " HIT"}
+        if (p.MAT.value)                {s += " MAT"}
+        if (p.MET.value)                {s += " MET"}
+        if (p.MNT.value)                {s += " MNT"}
+        if (p.OFT.value)                {s += " OFT"}
+        if (p.PAT.value)                {s += " PAT"}
+        if (p.PRT.value)                {s += " PRT"}
+        if (p.PZT.value)                {s += " PZT"}
+        if (p.RPT.value)                {s += " RPT"}
+        if (p.RUT.value)                {s += " RUT"}
+        if (p.SST.value)                {s += " SST"}
+        if (p.TAT.value)                {s += " TAT"}
+        if (p.UET.value)                {s += " UET"}
+        writer(s);
+        s = "           ";
+        if (p.systemNotReady.value)     {s += " SNR"}
+        if (p.computerNotReady.value)   {s += " CNR"}
+        if (p.compareLowLamp.value)     {s += " LOW"}
+        if (p.compareEqualLamp.value)   {s += " EQUAL"}
+        if (p.compareLowLamp.value)     {s += " HIGH"}
+        if (p.C10.value)                {s += " C10"}
+        if (p.DST.value)                {s += " DST"}
+        if (p.LT1.value)                {s += " LT1"}
+        if (p.LT2.value)                {s += " LT2"}
+        if (p.LT3.value)                {s += " LT3"}
+        if (p.SCI.value)                {s += " SCI"}
+        if (p.SGT.value)                {s += " SGT"}
+        if (p.SUT.value)                {s += " SUT"}
+        if (p.TBT.value)                {s += " TBT"}
+        if (p.TCT.value)                {s += " TCT"}
+        if (p.TPT.value)                {s += " TPT"}
+        if (p.TWT.value)                {s += " TWT"}
+        writer(s);
+    }
+
+    function memdumpDriver() {
+        /* Driver for formatting the memory and Processor state dump */
+        var addr = 0;
+        var dupCount = 0;
+        var lastLine = "";
+        var line = "";
+        var top = p.memorySize-1;       // max memory address
+        var x = 0;                      // image data index
+
+        function dumpDupes() {
+            /* Outputs the duplicate-line message, if any */
+
+            if (dupCount > 0) {
+                writer("....  ..... DUP FOR " + dupCount + " LINE" + (dupCount>1 ? "S" : "") +
+                       " THRU " + padLeft(addr-1, 4, "0") + " .....");
+                dupCount = 0;
+            }
+        }
+
+        while (paper.firstChild) {               // delete any existing 
 content
+            paper.removeChild(paper.firstChild);
+        }
+
+        writer("retro-220 Processor State and Memory Dump : " + new Date().toString());
+
+        dumpProcessorState();
+
+        // Dump all of memory
+        writer("");
+        writer("Memory: ");
+        writer("");
+        addr = 0;
+        while (addr <= top) {
+            // Format the next five words
+            line = "";
+            for (x=0; x<5; ++x) {
+                line += "  " + formatWord(p.MM[addr+x]);
+            } // for x
+
+            // Check for duplicate lines; write a non-duplicate
+            if (line == lastLine) {
+                ++dupCount;
+            } else {
+                dumpDupes();
+                lastLine = line;
+                line = padLeft(addr, 4, "0") + line + "  ";
+                for (x=0; x<5; ++x) {
+                    line += wordToANSI(p.MM[addr+x]);
+                } // for x
+
+                writer(line);
+            }
+
+            addr += 5;
+        } // for addr
+
+        dumpDupes();
+        writer("");
+        writer("End dump, memory size: " + (top+1).toString() + " words");
+    }
+
+    function memdumpSetup() {
+        /* Loads a status message into the "paper" rendering area, then calls
+        dumpDriver after a short wait to allow the message to appear */
+
+        win.removeEventListener("load", memdumpSetup, false);
+        doc = win.document;
+        doc.title = "retro-220 Console: Meatball Memdump";
+        paper = doc.getElementById("Paper");
+        writer("Rendering the dump... please wait...");
+        setTimeout(memdumpDriver, 50);
+    }
+
+    // Outer block of meatBallMemdump
+    win.moveTo((screen.availWidth-win.outerWidth)/2, (screen.availHeight-win.outerHeight)/2);
+    win.focus();
+    win.addEventListener("load", memdumpSetup, false);
+};
+
 /**************************************/
 B220ControlConsole.prototype.displayCallbackState = function displayCallbackState() {
     /* Builds a table of outstanding callback state */
@@ -259,9 +505,10 @@ B220ControlConsole.prototype.updatePanel = function updatePanel() {
     this.equalLamp.set(p.compareEqualLamp.glow);
     this.highLamp.set(p.compareHighLamp.glow);
 
+    this.$$("ProcDelta").textContent = p.delayDeltaAvg.toFixed(2) + " D";
+    this.$$("ProcSlack").textContent = p.procSlackAvg.toFixed(2)  + " S";
+    this.$$("ProcRun").textContent = p.procRunAvg.toFixed(2)  +     " R";
     /********** DEBUG **********
-    this.$$("ProcDelta").value = p.procSlackAvg.toFixed(2);
-    this.$$("LastLatency").value = p.delayDeltaAvg.toFixed(2);
     this.displayCallbackState();
     ***************************/
 };
@@ -737,6 +984,7 @@ B220ControlConsole.prototype.consoleOnLoad = function consoleOnLoad() {
     this.resetTransferSwitch.addEventListener("click", this.boundSwitch_Click);
     this.tcuClearSwitch.addEventListener("click", this.boundSwitch_Click);
 
+    this.$$("BurroughsMeatball").addEventListener("click", this.boundMeatballMemdump);
     this.$$("IntervalTimerResetBtn").addEventListener("click", this.boundResetTimer);
     this.$$("PowerOffBtn").addEventListener("click", this.boundPowerBtn_Click);
 
diff --git a/webUI/B220MagTapeControl.js b/webUI/B220MagTapeControl.js
index ccc67ec..324e8d6 100644
--- a/webUI/B220MagTapeControl.js
+++ b/webUI/B220MagTapeControl.js
@@ -23,6 +23,7 @@ function B220MagTapeControl(p) {
     this.config = p.config;             // System configuration object
     this.mnemonic = "MCU";
     this.p = p;                         // B220Processor object
+    this.releaseProcessor = p.boundMagTapeComplete;
 
     // Do not call this.clear() here -- call this.clearUnit() from onLoad instead
 
@@ -33,29 +34,58 @@ function B220MagTapeControl(p) {
     this.window.addEventListener("load",
         B220Util.bindMethod(this, B220MagTapeControl.prototype.magTapeOnLoad));
 
-    this.boundControlFinished = B220Util.bindMethod(this, B220MagTapeControl.prototype.controlFinished);
-    this.boundTapeUnitFinished = B220Util.bindMethod(this, B220MagTapeControl.prototype.tapeUnitFinished);
-    this.boundSwitch_Click = B220Util.bindMethod(this, B220MagTapeControl.prototype.switch_Click);
+    this.boundReleaseControl = B220MagTapeControl.prototype.releaseControl.bind(this);
+    this.boundCancelIO = B220MagTapeControl.prototype.cancelIO.bind(this);
+    this.boundTapeUnitFinished = B220MagTapeControl.prototype.tapeUnitFinished.bind(this);
+    this.boundFetchWord = B220MagTapeControl.prototype.fetchWord.bind(this);
+    this.boundStoreWord = B220MagTapeControl.prototype.storeWord.bind(this);
+    this.boundSwitch_Click = B220MagTapeControl.prototype.switch_Click.bind(this);
 
     this.currentUnit = null;            // stashed tape unit object
 
+    /* driveState is a status object passed to mag tape units that allows them
+    to report their status back to the control unit */
+
+    this.driveState = {
+        state: 0,                       // state/error code, see below
+        preface: 0,                     // preface/block length word
+        keyword: 0,                     // block keyword (first data word)
+        controlWord: 0,                 // block controlword (last data word)
+        startTime: 0,                   // start time for the operation (ms)
+        completionDelay: 0,             // extra delay before drive is released (ms)
+        // State constants
+        driveNoError: 0,                // operation successful
+        driveNotReady: 1,               // drive not ready or rewind-lockout
+        driveBusy: 2,                   // drive busy
+        driveAtBOT: 3,                  // tape at physical BOT
+        driveAtEOT: 4,                  // tape at physical EOT
+        driveAtEOI: 5,                  // tape at end-of-information
+        driveHasControlWord: 6,         // drive returned an EOT- or control-block control word
+        drivePrefaceCheck: 10,          // invalid preface word
+        drivePrefaceMismatch: 12,       // preface/instruction block-length mismatch
+        driveReadCheck: 13,             // preface/tape block-length mismatch
+        driveInvalidBlockLength: 14,    // invalid block length from instruction
+        driveMemoryError: 15,           // memory address or parity error
+        driveNotEditedTape: 16,         // attempt to initial-write over non-edited tape
+        driveUndefined: 99};            // undefined error
+
     /* Set up the tape units from the system configuration. These can be any
     combination of Tape Storage Units (DataReaders) and DataFiles. The indexes
     into this array are physical unit numbers used internally -- unit designate
     is set on the tape unit itself */
 
     this.tapeUnit = [
-            null,                       // 0=not used
-            null,                       // tape unit A
-            null,                       // tape unit B
-            null,                       // tape unit C
-            null,                       // tape unit D
-            null,                       // tape unit E
-            null,                       // tape unit F
-            null,                       // tape unit G
-            null,                       // tape unit H
-            null,                       // tape unit I
-            null];                      // tape unit J
+        null,                           // 0=not used
+        null,                           // tape unit A
+        null,                           // tape unit B
+        null,                           // tape unit C
+        null,                           // tape unit D
+        null,                           // tape unit E
+        null,                           // tape unit F
+        null,                           // tape unit G
+        null,                           // tape unit H
+        null,                           // tape unit I
+        null];                          // tape unit J
 
     for (x=1; x proceed with I/O
+    var t = dReg%0x10000000000;         // temp to partition fields of Processor's D register
     var ux;                             // internal unit index
-    var result = false;                 // return value
 
     //console.log(this.mnemonic + " loadCommand: " + dReg.toString(16));
     if (this.controlBusy) {
         this.queuePendingOperation(callee, args);
     } else {
         this.T = t;
+        this.regT.update(this.T);
         this.unitNr = (t - t%0x1000000000)/0x1000000000;
         t = (t - t%0x10000)/0x10000;
         c = t%0x10;                     // low-order digit of op code
         t = (t - t%0x100)/0x100;        // control digits from instruction
-        this.blockWords = t%0x100;
-        if (this.blockWords > 0) {
-            this.blockWords = B220Processor.bcdBinary(this.blockWords);
-        } else {
-            this.blockWords = 100;
-        }
-
         this.C = this.unitNr*0x100000 + t*0x10 + c;
-        this.clearMisc();
         this.regC.update(this.C);
-        this.regT.update(this.T);
+        this.clearMisc();
         this.unitIndex = ux = this.findDesignate(this.unitNr);
         if (ux < 0) {
-            this.reportStatus(2);       // drive not ready, not present
-            setCallback(this.mnemonic, this, 0, releaseProcessor, true);
+            this.reportStatus(this.driveState.driveNotReady);   // drive not ready, not present
+            this.releaseProcessor(false, 0);
         } else {
             this.currentUnit = this.tapeUnit[ux];
             if (this.currentUnit.busy || this.currentUnit.rewindLock) {
                 this.queuePendingOperation(callee, args);
             } else {
-                result = true;
+                proceed = true;
+                this.driveState.startTime = performance.now();
+                this.driveState.completionDelay = 0;
+                this.driveState.state = this.driveState.driveNoError;
             }
         }
     }
 
-    return result;
+    return proceed;
 };
 
 /**************************************/
-B220MagTapeControl.prototype.controlFinished = function controlFinished(alarm) {
-    /* Releases the busy status of the control. Typically used as a timed call-
-    back to simulate the amount of time the control unit is busy with an I/O.
-    If alarm is true, sets the Processor's Magnetic Tape Check alarm.
-    If another operation is pending, initiates that operation */
+B220MagTapeControl.prototype.releaseControl = function releaseControl(param) {
+    /* Releases the busy status of the control. If an error is present, sets the
+    bits in the MISC register and the Processor's Magnetic Tape Check alarm, as
+    appropriate. If another operation is pending, initiates that operation.
+    Returns but does not use its parameter so that it can be used with
+    Promise.then() */
 
-    //console.log(this.mnemonic + " controlFinished: " + alarm + ", busy=" + this.controlBusy);
-    if (alarm) {
-        this.p.setMagneticTapeCheck(true);
+    this.TFLamp.set(0);
+    this.TBLamp.set(0);
+    this.controlBusy = false;
+    if (this.driveState.state != this.driveState.driveNoError) {
+        this.currentUnit.releaseUnit(this.driveState);
+        this.reportStatus(this.driveState.state);
     }
 
-    this.controlBusy = false;
     if (this.pendingCallee !== null) {
         this.dequeuePendingOperation();
     }
+
+    return param;
 };
 
 /**************************************/
-B220MagTapeControl.prototype.tapeUnitFinished = function tapeUnitFinished() {
+B220MagTapeControl.prototype.cancelIO = function cancelIO(param) {
+    /* Terminates the current I/O operation by releasing the Processor, tape
+    unit, and tape control unit. Returns but does not use its parameter so it
+    can be used with Promise.then() */
+
+    this.releaseProcessor(false, 0);
+    this.currentUnit.releaseUnit();
+    this.releaseControl();
+    return param;
+};
+
+/**************************************/
+B220MagTapeControl.prototype.tapeUnitFinished = function tapeUnitFinished(param) {
     /* Call-back function passed to tape unit methods to signal when the unit has
-    completed its asynchronous operation */
+    completed its asynchronous operation. Returns but does not use "param", so
+    that it can be used with Promise.then() */
 
     if (!this.controlBusy) {            // if the control unit is currently idle...
         if (this.pendingCallee !== null) {
             this.dequeuePendingOperation();
         }
     }
+
+    return param;
 };
 
 /**************************************/
@@ -250,6 +320,121 @@ B220MagTapeControl.prototype.decrementBlockCount = function decrementBlockCount(
     return result;
 };
 
+/**************************************/
+B220MagTapeControl.prototype.determineBlockLength = function determineBlockLength(record) {
+    /* Determines the length of the next block to be read or written. If
+    "record" is true, the length is fetched from the next word in the Processor's
+    memory at the address in the Processor's C register. The length is converted
+    to binary and checked for valid values. Returns a negative value on error.
+    Note that the TCU always checks the kk digits from the instruction, even if
+    this is a record-mode operation and those digits do not determine block length */
+    var words = (this.C%0x1000 - this.C%0x10)/0x10; // kk digits from TCU C register
+
+    if (words > 0) {
+        words = (words >>> 4)*10 + words%0x10;
+    } else {
+        words = 100;                    // kk == 0 => 100
+    }
+
+    if (words < this.currentUnit.minBlockWords && words > 1) {
+        words = -1;                     // invalid kk digits in instruction
+        this.driveState.state = this.driveState.driveInvalidBlockLength;
+    } else if (record) {
+        words = this.fetchWord(true);
+        if (words < 0) {                // memory fetch failed
+            this.driveState.state = this.driveState.driveMemoryError;
+        } else {                        // convert preface word to binary
+            words = ((words - words%0x100000000)/0x100000000)%0x100;
+            if (words > 0) {
+                words = (words >>> 4)*10 + words%0x10;
+            } else {
+                words = 100;            // preface == 0 => 100
+            }
+
+            if (words < this.currentUnit.minBlockWords && words > 1) {
+                words = -1;             // invalid preface read from memory
+                this.driveState.state = this.driveState.driveInvalidBlockLength;
+            }
+        }
+    }
+
+    return words;
+};
+
+/**************************************/
+B220MagTapeControl.prototype.determineFieldCompare = function determineFieldCompare(bReg) {
+    /* Determines the field to be compared during search and scan operations.
+    Decodes the sL value in bReg to this.sField and this.LField, checks that
+    s 0) {
+        kd = kw%0x10;
+        td = tw%0x10;
+        if (s < 10) {
+            ++s;                        // just shift until s=10
+        } else {
+            --L;
+            adder = 9 - kd + td + carry;
+            if (adder < 10) {
+                carry = 0;
+            } else {
+                carry = 1;
+                adder -= 10
+            }
+
+            if (adder) {
+                equal = false;
+            }
+        }
+
+        kw = (kw-kd)/0x10;              // shift both words right one digit
+        tw = (tw-td)/0x10;
+    } // while L
+
+    if (equal) {
+        return 0;                       // keyword equal T
+    } else if (carry) {
+        return -1;                      // keyword less than T
+    } else {
+        return 1;                       // keyword greater than T
+    }
+};
+
 /**************************************/
 B220MagTapeControl.prototype.clearMisc = function clearMisc() {
     /* Resets this.regMisc and the individual lamps for that register */
@@ -263,22 +448,44 @@ B220MagTapeControl.prototype.clearMisc = function clearMisc() {
 };
 
 /**************************************/
-B220MagTapeControl.prototype.reportStatus = function reportStatus(code) {
+B220MagTapeControl.prototype.reportStatus = function reportStatus(state) {
     /* Sets bits in the MISC register to indicate various drive and control unit
     status and error conditions */
 
-    switch (code) {
-    case 1: // report tape unit ready
-        this.TX2Lamp.set(0);
-        this.TX10Lamp.set(0);
+    switch (state) {
+    case this.driveState.driveNoError:
+        this.clearMisc();
         break;
-    case 2: // report tape unit not ready
+    case this.driveState.driveNotReady:
         this.TX2Lamp.set(1);
         this.TX10Lamp.set(1);
         break;
-    case 4: // read check
+    case this.driveState.drivePrefaceCheck:
+        this.p.setMagneticTapeCheck(1);
+        this.TPCLamp.set(1);
+        break;
+    case this.driveState.drivePrefaceMismatch:
+        this.p.setMagneticTapeCheck(1);
+        this.TCFLamp.set(1);
+        this.C = (this.C & 0x00FFFF) | 0xFF0000;
+        this.regC.update(this.C);
+        break;
+    case this.driveState.driveReadCheck:
+        this.p.setMagneticTapeCheck(1);
         this.TYC1Lamp.set(1);
         this.TYC2Lamp.set(1);
+        this.C = (this.C & 0xFFF00F) | 0x000F90;
+        this.regC.update(this.C);
+        break;
+    case this.driveState.driveInvalidBlockLength:
+        this.p.setMagneticTapeCheck(1);
+        this.TX2Lamp.set(1);
+        this.TX4Lamp.set(1);
+        this.C = (this.C & 0x000F0F) | 0xB010F0;
+        this.regC.update(this.C);
+        break;
+    case this.driveState.driveNotEditedTape:
+        this.p.setMagneticTapeCheck(1);
         break;
     } // switch code
 };
@@ -330,17 +537,21 @@ B220MagTapeControl.prototype.magTapeOnLoad = function magTapeOnLoad() {
     this.regMisc = new PanelRegister(this.$$("MiscRegPanel"), 4*4, 4, "Misc_", " ");
     this.regMisc.lamps[15].setCaption("MCL", true);
     this.regMisc.lamps[14].setCaption("MC6", true);
-    this.TYC1Lamp = this.regMisc.lamps[13];
+    this.TYC1Lamp = this.regMisc.lamps[13];     // Yozzle toggle 1
     this.TYC1Lamp.setCaption("TYC", true);
-    this.TYC2Lamp = this.regMisc.lamps[12];
+    this.TYC2Lamp = this.regMisc.lamps[12];     // Yozzle toggle 2
     this.TYC2Lamp.setCaption("TYC", true);
-    this.TCFLamp = this.regMisc.lamps[10];      // not in this physical position on a 220
+    this.TCFLamp = this.regMisc.lamps[10];      // Preface compare failure: not in this register on a 220
     this.TCFLamp.setCaption("TCF", true);
-    this.TPCLamp = this.regMisc.lamps[7];
+    this.TFLamp = this.regMisc.lamps[9];        // Tape forward: not in this register on a 220
+    this.TFLamp.setCaption("TF", true);
+    this.TBLamp = this.regMisc.lamps[8];        // Tape bacward: not in this register on a 220
+    this.TBLamp.setCaption("TB", true);
+    this.TPCLamp = this.regMisc.lamps[7];       // Preface check
     this.TPCLamp.setCaption("TPC", true);
     this.regMisc.lamps[6].setCaption("TSX", true);
     this.regMisc.lamps[5].setCaption("1R6", true);
-    this.TX1Lamp = this.regMisc.lamps[4];
+    this.TX1Lamp = this.regMisc.lamps[4];       // TX register
     this.TX1Lamp.setCaption("TX1", true);
     this.TX10Lamp = this.regMisc.lamps[3];
     this.TX10Lamp.setCaption("TX10", true);
@@ -367,321 +578,534 @@ B220MagTapeControl.prototype.magTapeOnLoad = function magTapeOnLoad() {
 };
 
 /**************************************/
-B220MagTapeControl.prototype.search = function search(dReg, releaseProcessor, bReg, fetchWord) {
-    /* Searches a tape unit for a block with a keyWord matching the word at the
-    operand address in memory. "bReg is the contents of the B register for a
-    search, or 0 for a full-word search. This routine is used by MTS and MFS */
-    var alarm = false;                  // error result
-    var blocksLeft = true;              // true => more blocks to process
-    var searchWord;                     // target word to search for
-    var that = this;                    // local self-reference
+B220MagTapeControl.prototype.scan = function scan(dReg, bReg) {
+    /* Scans a tape unit for a block with a word matching the word at the
+    operand address in memory, which is stored in the TCU's T register. "bReg"
+    is the contents of the B register for a partial-word match, or 0 for a
+    full-word match. The index of the category word in the block to be compared
+    to the scan key is determined from dReg:41. This routine is used by MTC and
+    MFC */
+    var laneNr = 0;                     // lane number from TCU C register
+    var searchWord = 0;                 // target word to scan for
+    var wordIndex = 0;                  // index of category word
 
-    function signalControl(controlWord) {
-        /* Call-back function to send the EOT or control word to the Processor
-        and release it for the next operation */
+    var scanForward = () => {
+        /* Handles a block after it has been scanned in a forward direction.
+        If the category word is not equal, continues scanning in a forward
+        direction; otherwise, repositions to allow the matching block to be read
+        next. If an EOT or control block is encountered, the control word from
+        that block is ignored, and the tape is then repositioned, ready to read
+        the EOT or control block. The drive forward scan stops in the erase gap
+        of the block containing the category or control word, so a reposition
+        backs up to allow reading the next block. A second reposition is needed
+        is needed to back up into the prior block, allowing the block just
+        scanned to be read next */
+        var compare = 0;                // category word field comparison result
 
-        releaseProcessor(false, true, controlWord);
-    }
-
-    function blockReady(alarm, control, controlWord, readBlock, completed) {
-        /* Call-back function when the drive is ready to send the next block
-        of data, or when it has encountered an error such as EOT. "alarm"
-        indicates that an error has occurred and the operation is to be aborted.
-        "control" inidicates that an EOT or control block was encountered, and
-        "controlWord" is to be passed to the Processor for handling. Otherwise,
-        if there are more blocks to write, fetches the next block from the
-        Processor and calls the drive's "readBlock" function, Finally calls
-        "completed" to finish the operation */
-
-        if (alarm) {
-            setCallback(that.mnemonic, that, 0, releaseProcessor, true);
-            completed(true);            // drive detected an error
-        } else if (control) {
-            setCallback(that.mnemonic, that, 0, signalControl, controlWord);
-            completed(false);
-        } else if (blocksLeft) {
-            blocksLeft = that.decrementBlockCount();    // set to false on last block
-            readBlock(storeWord, record, controlEnabled);// read the next block
+        if (this.driveState.state == this.driveState.driveHasControlWord) {
+            // EOT or control block encountered: terminate the I/O
+            this.driveState.state = this.driveState.driveNoError;
+            this.TFLamp.set(0);
+            this.TBLamp.set(1);
+            this.currentUnit.reposition(this.driveState)        // reposition into the EOT block
+            .then(this.currentUnit.boundReposition)             // reposition again into prior block
+            .then(this.currentUnit.boundReleaseDelay)           //   allowing the EOT block to be read next
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
         } else {
-            setCallback(that.mnemonic, that, 0, releaseProcessor, false);
-            completed(false);           // normal termination
+            // Compare category word from tape with T register
+            compare = this.compareKeywordField(this.driveState.keyword);
+            if (compare != 0) {          // if category word unequal, keep scanning
+                this.currentUnit.scanBlock(this.driveState, wordIndex)
+                .then(scanForward)
+                .catch(this.boundReleaseControl);
+            } else {                    // Keyword was equal: stop and reposition
+                this.TFLamp.set(0);
+                this.TBLamp.set(1);
+                this.currentUnit.reposition(this.driveState)    // reposition into the matching block
+                .then(this.currentUnit.boundReposition)         // reposition again into prior block
+                .then(this.currentUnit.boundReleaseDelay)       //   allowing the matching block to be read next
+                .then(this.currentUnit.boundReleaseUnit)
+                .then(this.boundReleaseControl)
+                .catch(this.boundReleaseControl);
+            }
         }
-    }
+    };
 
-    if (this.loadCommand(dReg, releaseProcessor, search, arguments)) {
+    var scanFirstBlock = (driveState) => {
+        /* Closure to call u.scanBlock with additional parameters from Promise.then() */
+
+        this.currentUnit.scanBlock(this.driveState, wordIndex);
+    };
+
+    if (this.loadCommand(dReg, scan, arguments)) {
         this.controlBusy = true;
-        searchWord = fetchWord(true);
-        if (searchWord < 0) {
-            alarm = true;
-        } else {
-            alarm = this.currentUnit.searchBlock(blockReady, this.boundControlFinished);
-        }
+        this.driveState.completionDelay = 16;
 
-        if (alarm) {
-            setCallback(this.mnemonic, this,  0, releaseProcessor, alarm);
-            this.controlFinished(true);
+        // Check for field compare validity
+        if (this.determineFieldCompare(bReg)) {
+            this.releaseProcessor(false, 0);
+            this.releaseControl();
+        } else {
+            // Fetch the scan target word from memory
+            searchWord = this.fetchWord(false);
+            if (searchWord < 0) {
+                this.driveState.state = this.driveState.driveMemoryError;
+                this.releaseProcessor(false, 0);
+                this.releaseControl();
+            } else {
+                // Start the scan after changing lane, as appropriate
+                this.T = searchWord;
+                this.regT.update(this.T);
+                this.releaseProcessor(false, 0);
+                this.TFLamp.set(1);
+                laneNr = ((this.C - this.C%0x100)/0x100)%2;
+                wordIndex = ((this.C - this.C%0x10)/0x10)%2;
+                if (wordIndex == 0) {
+                    wordIndex = 10;
+                }
+
+                this.currentUnit.setLane(laneNr, this.driveState)
+                .then(this.currentUnit.boundStartUpForward)
+                .then(scanFirstBlock)
+                .then(scanForward)
+                .catch(this.boundReleaseControl);
+            }
         }
     }
 };
 
 /**************************************/
-B220MagTapeControl.prototype.read = function read(dReg, releaseProcessor, record, storeWord) {
+B220MagTapeControl.prototype.search = function search(dReg, bReg) {
+    /* Searches a tape unit for a block with a keyword matching the word at the
+    operand address in memory, which is stored in the TCU's T register. "bReg"
+    is the contents of the B register for a partial-word match, or 0 for a
+    full-word match. This routine is used by MTS and MFS */
+    var laneNr = 0;                     // lane number from TCU C register
+    var searchWord = 0;                 // target word to search for
+
+    var searchForward = () => {
+        /* Handles a block after it has been searched in a forward direction.
+        If the keyword is low, continues searching in a forward direction;
+        otherwise, reverses tape direction and initiates a backward search.
+        If an EOT block is encountered, the control word from that block is
+        ignored, and the tape is then repositioned, ready to read the EOT block.
+        The drive forward search stops in the block after the keyword, so a
+        reposition backs up to allow reading the block just searched */
+        var compare = 0;                // keyword field comparison result
+
+        if (this.driveState.state == this.driveState.driveHasControlWord) {
+            // EOT block encountered: terminate the I/O
+            this.driveState.state = this.driveState.driveNoError;
+            this.TFLamp.set(0);
+            this.TBLamp.set(1);
+            this.currentUnit.reposition(this.driveState)
+            .then(this.currentUnit.boundReleaseDelay)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
+        } else {
+            // Compare keyword from tape with T register
+            compare = this.compareKeywordField(this.driveState.keyword);
+            if (compare < 0) {          // if keyword low, keep searching
+                this.currentUnit.searchForwardBlock(this.driveState)
+                .then(searchForward)
+                .catch(this.boundReleaseControl);
+            } else {                    // Keyword was high or equal: reverse direction
+                this.TFLamp.set(0);
+                this.TBLamp.set(1);
+                this.currentUnit.reverseDirection(this.driveState)
+                .then(this.currentUnit.boundSearchBackwardBlock)
+                .then(searchBackward)
+                .catch(this.boundReleaseControl);
+            }
+        }
+    };
+
+    var searchBackward = () => {
+        /* Handles a block after it has been searched in a backward direction.
+        If the keyword is high, continues searching in a backward direction.
+        If the keyword is low, reverses tape direction again and searches one
+        block in the forward direction. In this case, the result of the search
+        is ignored, leaving the tape positioned to read the next block, which
+        will be greater-than or equal-to the search target.
+        If the keyword is equal, the tape is already positioned in the prior
+        block, so we just quit, leaving the tape in position to read the block
+        with the equal key */
+        var compare = this.compareKeywordField(this.driveState.keyword);
+
+        if (compare > 0) {              // keyword is high, continue searching...
+            this.currentUnit.searchBackwardBlock(this.driveState)
+            .then(searchBackward)
+            .catch(this.boundReleaseControl);
+        } else if (compare < 0) {       // keyword is low, reverse direction, search one block, and quit
+            this.TBLamp.set(0);
+            this.TFLamp.set(1);
+            this.currentUnit.reverseDirection(this.driveState)
+            .then(this.currentUnit.boundSearchForwardBlock)
+            .then(this.currentUnit.boundReleaseDelay)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
+        } else {                        // keyword equal, just quit with tape positioned in prior block
+            this.currentUnit.releaseDelay(this.driveState)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
+        }
+    };
+
+    if (this.loadCommand(dReg, search, arguments)) {
+        this.controlBusy = true;
+        this.driveState.completionDelay = 16;
+
+        // Check for field compare validity
+        if (this.determineFieldCompare(bReg)) {
+            this.releaseProcessor(false, 0);
+            this.releaseControl();
+        } else {
+            // Fetch the search target word from memory
+            searchWord = this.fetchWord(false);
+            if (searchWord < 0) {
+                this.driveState.state = this.driveState.driveMemoryError;
+                this.releaseProcessor(false, 0);
+                this.releaseControl();
+            } else {
+                // Start the search after changing lane, as appropriate
+                this.T = searchWord;
+                this.regT.update(this.T);
+                this.releaseProcessor(false, 0);
+                this.TFLamp.set(1);
+                laneNr = ((this.C - this.C%0x100)/0x100)%2;
+
+                this.currentUnit.setLane(laneNr, this.driveState)
+                .then(this.currentUnit.boundStartUpForward)
+                .then(this.currentUnit.boundSearchForwardBlock)
+                .then(searchForward)
+                .catch(this.boundReleaseControl);
+            }
+        }
+    }
+};
+
+/**************************************/
+B220MagTapeControl.prototype.read = function read(dReg, record) {
     /* Reads the number of blocks indicated in dReg. If "record" is true (MRR),
     block lengths (preface words) are stored into the word in memory preceding
-    the data read from tape. "storeWord" is a function to store a word to the
-    Processor's memory. This routine is used by MRD and MRR */
-    var alarm = false;                  // error result
-    var blocksLeft = true;              // true => more blocks to process
+    the data read from tape. This routine is used by MRD and MRR */
     var controlEnabled = false;         // true => control blocks will be recognized
-    var that = this;                    // local self-reference
 
-    function signalControl(controlWord) {
-        /* Call-back function to send the EOT or control word to the Processor
-        and release it for the next operation */
+    var readBlock = () => {
+        /* Reads blocks on the designated unit until the block count is
+        exhausted or some error occurs. If an EOT block or control block
+        is encountered, the drive returns the control word from that block and
+        the I/O is terminated normally after passing the control word to the
+        Processor for action. The tape is repositioned, ready to read the next
+        block */
 
-        releaseProcessor(false, true, controlWord);
-    }
-
-    function blockReady(alarm, control, controlWord, readBlock, completed) {
-        /* Call-back function when the drive is ready to send the next block
-        of data, or when it has encountered an error such as EOT. "alarm"
-        indicates that an error has occurred and the operation is to be aborted.
-        "control" inidicates that an EOT or control block was encountered, and
-        "controlWord" is to be passed to the Processor for handling. Otherwise,
-        if there are more blocks to write, fetches the next block from the
-        Processor and calls the drive's "readBlock" function, Finally calls
-        "completed" to finish the operation */
-
-        if (alarm) {
-            setCallback(that.mnemonic, that, 0, releaseProcessor, true);
-            completed(true);            // drive detected an error
-        } else if (control) {
-            setCallback(that.mnemonic, that, 0, signalControl, controlWord);
-            completed(false);
-        } else if (blocksLeft) {
-            blocksLeft = that.decrementBlockCount();    // set to false on last block
-            readBlock(storeWord, record, controlEnabled);// read the next block
-        } else {
-            setCallback(that.mnemonic, that, 0, releaseProcessor, false);
-            completed(false);           // normal termination
+        if (this.driveState.state == this.driveState.driveHasControlWord) {
+            this.driveState.state = this.driveState.driveNoError;
+            this.releaseProcessor(true, this.driveState.controlWord);
+            this.TFLamp.set(0);
+            this.TBLamp.set(1);
+            this.currentUnit.reposition(this.driveState)
+            .then(this.currentUnit.boundReleaseDelay)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
+        } else if (this.decrementBlockCount()) {
+            this.currentUnit.readNextBlock(this.driveState, record, controlEnabled, this.boundStoreWord)
+            .then(readBlock)
+            .catch(this.boundCancelIO);
+        } else {                        // block count exhausted
+            this.releaseProcessor(false, 0);
+            this.TFLamp.set(0);
+            this.TBLamp.set(1);
+            this.currentUnit.reposition(this.driveState)
+            .then(this.currentUnit.boundReleaseDelay)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
         }
-    }
+    };
 
-    if (this.loadCommand(dReg, releaseProcessor, read, arguments)) {
+    var readFirstBlock = (driveState) => {
+        /* Closure to call u.readNextBlock with additional parameters from Promise.then() */
+
+        this.currentUnit.readNextBlock(this.driveState, record, controlEnabled, this.boundStoreWord);
+    };
+
+    if (this.loadCommand(dReg, read, arguments)) {
         this.controlBusy = true;
-        controlEnabled = (this.blockWords%2 == 0);      // low-order bit of v-digit
-        alarm = this.currentUnit.readBlock(blockReady, this.boundControlFinished);
-        if (alarm) {
-            setCallback(this.mnemonic, this,  0, releaseProcessor, alarm);
-            this.controlFinished(true);
-        }
+        this.driveState.completionDelay = 18;
+        controlEnabled = (this.C%0x20 < 0x10);  // low-order bit of instruction v-digit
+        this.TFLamp.set(1);
+
+        this.currentUnit.startUpForward(this.driveState)
+        .then(readFirstBlock)
+        .then(readBlock)
+        .catch(this.boundCancelIO);
     }
 };
 
 /**************************************/
-B220MagTapeControl.prototype.overwrite = function overwrite(dReg, releaseProcessor, record, fetchWord) {
+B220MagTapeControl.prototype.overwrite = function overwrite(dReg, record) {
     /* Overwrites the number of blocks and of the size indicated in dReg. If
     "record" is true (MOR), block lengths (preface words) are taken from the
     word in memory preceding the data to be written. Otherwise, block lengths
-    are taken from the instruction control digits. "fetchWord" is a function to
-    read a word from the Processor's memory. This routine is used by MOW and MOR */
-    var alarm = false;                  // error result
+    are taken from the instruction control digits. This routine is used by
+    MOW and MOR */
     var blocksLeft = true;              // true => more blocks to process
-    var that = this;                    // local self-reference
-    var words;
 
-    function signalControl(controlWord) {
-        /* Call-back function to send the EOT control word to the Processor
-        and release it for the next operation */
+    var writeBlock = () => {
+        /* Overwrites blocks on the designated unit until the block count is
+        exhausted or some error occurs. If an EOT block with a preface mismatch
+        is encountered, the drive returns the control word from that block and
+        the I/O is terminated normally after passing the control word to the
+        Processor for action. The tape is repositioned, ready to read the next
+        block */
+        var words = 0;
 
-        releaseProcessor(false, true, controlWord);
-    }
-
-    function blockReady(alarm, control, controlWord, writeBlock, completed) {
-        /* Call-back function when the drive is ready to receive the next block
-        of data, or when it has encountered an error such as EOT. "alarm"
-        indicates that an error has occurred and the operation is to be aborted.
-        "control" inidicates that an EOT block with a preface mismatch occurred,
-        and "controlWord" is to be passed to the Processor for handling.
-        Otherwise, if there are more blocks to write, fetches the next block
-        from the Processor and calls the drive's "writeBlock" function, Finally
-        calls "completed" to finish the operation */
-
-        if (alarm) {
-            setCallback(that.mnemonic, that, 0, releaseProcessor, true);
-            completed(true);            // drive detected an error
-        } else if (control) {
-            setCallback(that.mnemonic, that, 0, signalControl, controlWord);
-            completed(false);
+        if (this.driveState.state == this.driveState.driveHasControlWord) {
+            this.driveState.state = this.driveState.driveNoError;
+            this.releaseProcessor(true, this.driveState.controlWord);
+            this.TFLamp.set(0);
+            this.TBLamp.set(1);
+            this.currentUnit.reposition(this.driveState)
+            .then(this.currentUnit.boundReleaseDelay)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
         } else if (blocksLeft) {
-            blocksLeft = that.decrementBlockCount();    // set to false on last block
-            writeBlock(fetchWord, words);               // write the next block
-        } else {
-            setCallback(that.mnemonic, that, 0, releaseProcessor, false);
-            completed(false);           // normal termination
+            words = this.determineBlockLength(record);
+            if (words < 0) {
+                this.cancelIO();
+            } else {
+                blocksLeft = this.decrementBlockCount();
+                this.currentUnit.overwriteBlock(this.driveState, record, words, this.boundFetchWord)
+                .then(writeBlock)
+                .catch(this.boundCancelIO);
+            }
+        } else {                        // block count exhausted
+            this.releaseProcessor(false, 0);
+            this.TFLamp.set(0);
+            this.TBLamp.set(1);
+            this.currentUnit.reposition(this.driveState)
+            .then(this.currentUnit.boundReleaseDelay)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
         }
-    }
+    };
 
-    if (this.loadCommand(dReg, releaseProcessor, overwrite, arguments)) {
+    if (this.loadCommand(dReg, overwrite, arguments)) {
         this.controlBusy = true;
-        if (this.blockWords < this.currentUnit.minBlockWords && this.blockWords > 1) {
-            alarm = true;               // invalid block length
-        } else {
-            words = (record ? 0 : this.blockWords);
-            alarm = this.currentUnit.overwriteBlock(blockReady, this.boundControlFinished);
-        }
+        this.driveState.completionDelay = 18;
+        this.TFLamp.set(1);
 
-        if (alarm) {
-            setCallback(this.mnemonic, this,  0, releaseProcessor, alarm);
-            this.controlFinished(true);
-        }
+        this.currentUnit.startUpForward(this.driveState)
+        .then(writeBlock)
+        .catch(this.boundCancelIO);
     }
 };
 
 /**************************************/
-B220MagTapeControl.prototype.initialWrite = function initialWrite(dReg, releaseProcessor, record, fetchWord) {
+B220MagTapeControl.prototype.initialWrite = function initialWrite(dReg, record) {
     /* Initial-writes the number of blocks and of the size indicated in dReg.
     If "record" is true (MIR), block lengths (preface words) are taken from the
     word in memory preceding the data to be written. Otherwise, block lengths
-    are taken from the instruction control digits. fetchWord" is a function to
-    read a word from the Processor's memory. This routine is used by MIW and MIR */
-    var alarm = false;                  // error result
+    are taken from the instruction control digits. This routine is used by
+    MIW and MIR */
     var blocksLeft = true;              // true => more blocks to process
-    var that = this;                    // local self-reference
-    var words;
 
-    function blockReady(alarm, writeBlock, completed) {
-        /* Call-back function when the drive is ready to receive the next block
-        of data, or when it has encountered an error such as EOT. "alarm"
-        indicates that an error has occurred and the operation is to be aborted.
-        Otherwise, if there are more blocks to write, fetches the next block
-        from the Processor and calls the drive's "writeBlock" function. Finally
-        calls "completed" to finish the operation */
+    var writeBlock = () => {
+        /* Initial-writes blocks on the designated unit until the block count is
+        exhausted or some error occurs */
+        var words = 0;
 
-        if (alarm) {
-            setCallback(that.mnemonic, that, 0, releaseProcessor, true);
-            completed(true);            // drive detected an error
-        } else if (blocksLeft) {
-            blocksLeft = that.decrementBlockCount();    // set to false on last block
-            writeBlock(fetchWord, words);               // write the next block
-        } else {
-            setCallback(that.mnemonic, that, 0, releaseProcessor, false);
-            completed(false);           // normal termination
+        if (blocksLeft) {
+            words = this.determineBlockLength(record);
+            if (words < 0) {
+                this.cancelIO();
+            } else {
+                blocksLeft = this.decrementBlockCount();
+                this.currentUnit.initialWriteBlock(this.driveState, record, words, this.boundFetchWord)
+                .then(writeBlock)
+                .catch(this.boundCancelIO);
+            }
+        } else {                        // block count exhausted
+            this.releaseProcessor(false, 0);
+            this.TFLamp.set(0);         // direction actually changes after WriteFinalize
+            this.TBLamp.set(1);         // but that's messy to do here...
+            this.currentUnit.initialWriteFinalize(this.driveState)
+            .then(this.currentUnit.boundReposition)
+            .then(this.currentUnit.boundReleaseDelay)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
         }
-    }
+    };
 
-    if (this.loadCommand(dReg, releaseProcessor, initialWrite, arguments)) {
+    if (this.loadCommand(dReg, initialWrite, arguments)) {
         this.controlBusy = true;
-        if (this.blockWords < this.currentUnit.minBlockWords && this.blockWords > 1) {
-            alarm = true;               // invalid block length
-        } else {
-            words = (record ? 0 : this.blockWords);
-            alarm = this.currentUnit.initialWriteBlock(blockReady, this.boundControlFinished);
-        }
+        this.driveState.completionDelay = 20;
+        this.TFLamp.set(1);
 
-        if (alarm) {
-            setCallback(this.mnemonic, this,  0, releaseProcessor, alarm);
-            this.controlFinished(true);
-        }
+        this.currentUnit.startUpForward(this.driveState)
+        .then(writeBlock)
+        .catch(this.boundCancelIO);
     }
 };
 
 /**************************************/
-B220MagTapeControl.prototype.positionForward = function positionForward(dReg, releaseProcessor) {
+B220MagTapeControl.prototype.positionForward = function positionForward(dReg) {
     /* Positions the tape forward the number of blocks indicated in dReg */
-    var alarm = false;                  // error result
-    var that = this;                    // local self-reference
 
-    function blockFinished(nextBlock, completed) {
-        /* Call-back function when the drive has finished spacing one block
-        forward. If there are more blocks to space, calls "nextBlock", otherwise
-        calls "completed" to finish the operation */
+    var spaceBlock = () => {
+        /* Spaces forward over blocks on the designated unit until the block
+        count is exhausted or some error (like end-of-tape) occurs */
 
-        if (that.decrementBlockCount()) {
-            nextBlock(blockFinished);
-        } else {
-            completed(false);
+        if (this.decrementBlockCount()) {
+            this.currentUnit.spaceForwardBlock(this.driveState)
+            .then(spaceBlock)
+            .catch(this.boundReleaseControl);
+        } else {                        // block count exhausted
+            this.TFLamp.set(0);
+            this.TBLamp.set(1);
+            this.currentUnit.reposition(this.driveState)
+            .then(this.currentUnit.boundReleaseDelay)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
         }
-    }
+    };
 
-    if (this.loadCommand(dReg, releaseProcessor, positionForward, arguments)) {
+    if (this.loadCommand(dReg, positionForward, arguments)) {
         this.controlBusy = true;
-        alarm = this.currentUnit.positionForward(blockFinished, this.boundControlFinished);
-        setCallback(this.mnemonic, this,  0, releaseProcessor, alarm);
+        this.driveState.completionDelay = 16;
+        this.releaseProcessor(false, 0);
+        this.TFLamp.set(1);
+
+        this.currentUnit.startUpForward(this.driveState)
+        .then(this.currentUnit.boundSpaceForwardBlock)
+        .then(spaceBlock)
+        .catch(this.boundReleaseControl);
     }
 };
 
 /**************************************/
-B220MagTapeControl.prototype.positionBackward = function positionBackward(dReg, releaseProcessor) {
+B220MagTapeControl.prototype.positionBackward = function positionBackward(dReg) {
     /* Positions the tape backward the number of blocks indicated in dReg */
-    var alarm = false;                  // error result
-    var that = this;                    // local self-reference
 
-    function blockFinished(nextBlock, completed) {
-        /* Call-back function when the drive has finished spacing one block
-        backward. If there are more blocks to space, calls "nextBlock", otherwise
-        calls "completed" to finish the operation */
+    var spaceBlock = () => {
+        /* Spaces backward over blocks on the designated unit until the block
+        count is exhausted or some error (like beginning-of-tape) occurs */
 
-        if (that.decrementBlockCount()) {
-            nextBlock(blockFinished);
-        } else {
-            completed(false);
+        if (this.decrementBlockCount()) {
+            this.currentUnit.spaceBackwardBlock(this.driveState)
+            .then(spaceBlock)
+            .catch(this.boundReleaseControl);
+        } else {                        // block count exhausted
+            this.currentUnit.boundReleaseDelay(this.driveState)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
         }
-    }
+    };
 
-    if (this.loadCommand(dReg, releaseProcessor, positionBackward, arguments)) {
+    if (this.loadCommand(dReg, positionBackward, arguments)) {
         this.controlBusy = true;
-        alarm = this.currentUnit.positionBackward(blockFinished, this.boundControlFinished);
-        setCallback(this.mnemonic, this,  0, releaseProcessor, alarm);
+        this.driveState.completionDelay = 6;
+        this.releaseProcessor(false, 0);
+        this.TBLamp.set(1);
+
+        this.currentUnit.startUpBackward(this.driveState)
+        .then(this.currentUnit.boundSpaceBackwardBlock)
+        .then(spaceBlock)
+        .catch(this.boundReleaseControl);
     }
 };
 
 /**************************************/
-B220MagTapeControl.prototype.positionAtEnd = function positionAtEnd(dReg, releaseProcessor) {
+B220MagTapeControl.prototype.positionAtEnd = function positionAtEnd(dReg) {
     /* Positions the tape to the end of recorded information (i.e., when a gap
     longer than inter-block gap is detected. Leaves the tape at the end of the
     prior recorded block */
-    var alarm = false;                  // error result
 
-    if (this.loadCommand(dReg, releaseProcessor, positionAtEnd, arguments)) {
+    var spaceBlock = () => {
+        /* Spaces over blocks on the designated unit until end-of-information is
+        detected or some error (like end-of-tape) occurs */
+
+        if (this.driveState.state != this.driveState.driveAtEOI) {
+            this.currentUnit.spaceEOIBlock(this.driveState)
+            .then(spaceBlock)
+            .catch(this.boundReleaseControl);
+        } else {                        // found end-of-information
+            this.driveState.state = this.driveState.driveNoError;
+            this.TFLamp.set(0);
+            this.TBLamp.set(1);
+            this.currentUnit.reposition(this.driveState)
+            .then(this.currentUnit.boundReleaseDelay)
+            .then(this.currentUnit.boundReleaseUnit)
+            .then(this.boundReleaseControl)
+            .catch(this.boundReleaseControl);
+        }
+    };
+
+    if (this.loadCommand(dReg, positionAtEnd, arguments)) {
         this.controlBusy = true;
-        alarm = this.currentUnit.positionAtEnd(this.boundControlFinished);
-        setCallback(this.mnemonic, this,  0, releaseProcessor, alarm);
+        this.driveState.completionDelay = 23;
+        this.releaseProcessor(false, 0);
+        this.TFLamp.set(1);
+
+        this.currentUnit.startUpForward(this.driveState)
+        .then(this.currentUnit.boundSpaceEOIBlock)
+        .then(spaceBlock)
+        .catch(this.boundReleaseControl);
     }
 };
 
 /**************************************/
-B220MagTapeControl.prototype.laneSelect = function laneSelect(dReg, releaseProcessor, fetchWord) {
+B220MagTapeControl.prototype.laneSelect = function laneSelect(dReg) {
     /* Selects the tape lane of the designated unit. Returns an alarm if the
     unit does not exist or is not ready */
-    var alarm = false;                  // error result
     var laneNr;                         // lane to select (0, 1)
 
-    if (this.loadCommand(dReg, releaseProcessor,laneSelect, arguments)) {
+    if (this.loadCommand(dReg, laneSelect, arguments)) {
         this.controlBusy = true;
         laneNr = ((this.C - this.C%0x100)/0x100)%2;
-        fetchWord(true);                // memory access for MTS/MFS not used by MLS
-        alarm = this.currentUnit.laneSelect(laneNr, this.boundControlFinished);
-        setCallback(this.mnemonic, this,  0, releaseProcessor, alarm);
+        this.driveState.completionDelay = 3;
+        this.fetchWord(true);           // memory access for MTS/MFS not used by MLS
+        this.releaseProcessor(false, 0);
+
+        this.currentUnit.laneSelect(this.driveState, laneNr)
+        .then(this.currentUnit.boundReleaseDelay)
+        .then(this.currentUnit.boundReleaseUnit)
+        .then(this.boundReleaseControl)
+        .catch(this.boundReleaseControl);
     }
 };
 
 /**************************************/
-B220MagTapeControl.prototype.rewind = function rewind(dReg, releaseProcessor, fetchWord) {
+B220MagTapeControl.prototype.rewind = function rewind(dReg) {
     /* Initiates rewind of the designated unit. Returns an alarm if the unit
     does not exist or is not ready */
-    var alarm = false;                  // error result
     var laneNr;                         // lane to select (0, 1)
     var lockout;                        // lockout after rewind  (0, 1)
 
-    if (this.loadCommand(dReg, releaseProcessor, rewind, arguments)) {
+    if (this.loadCommand(dReg, rewind, arguments)) {
         this.controlBusy = true;
         laneNr = ((this.C - this.C%0x100)/0x100)%2;
         lockout = ((this.C - this.C%0x10)/0x10)%2;
-        fetchWord(true);                // memory access for MTS/MFS not used by MRW/MDA
-        alarm = this.currentUnit.rewind(laneNr, lockout);
-        setCallback(this.mnemonic, this, 50, this.controlFinished, false);
-        setCallback(this.mnemonic, this,  0, releaseProcessor, alarm);
+        this.fetchWord(true);           // memory access for MTS/MFS not used by MRW/MDA
+        this.releaseProcessor(false, 0);
+        this.TBLamp.set(1);
+        setCallback(this.mnemonic, this, 50, this.releaseControl, this.driveState);
+
+        this.currentUnit.rewind(this.driveState, laneNr, lockout)
+        .then(this.currentUnit.boundReleaseUnit)
+        .then(this.boundTapeUnitFinished);
     }
 };
 
@@ -705,9 +1129,9 @@ B220MagTapeControl.prototype.testUnitReady = function testUnitReady(dReg) {
 };
 
 /**************************************/
-B220MagTapeControl.prototype.testUnitAtMagneticEOT = function testUnitAtMagneticEOT(dReg) {
+B220MagTapeControl.prototype.testUnitAtEOT = function testUnitAtEOT(dReg) {
     /* Interrogates status of the designated unit. Returns true if ready and at
-    Magnetic-End-of-Tape */
+    end-of-tape */
     var result = false;                 // return value
     var ux;                             // internal unit index
 
diff --git a/webUI/B220MagTapeDrive.css b/webUI/B220MagTapeDrive.css
index a85b146..708f4b6 100644
--- a/webUI/B220MagTapeDrive.css
+++ b/webUI/B220MagTapeDrive.css
@@ -197,6 +197,13 @@ SELECT {
     right:              16px}
 
 #MTUnloadedLight {
+    top:                36px;
+    right:              8px;
+    width:              64px}
+
+#MTLaneNrLight {
+    color:              black;
+    font-weight:        bold;
     top:                60px;
     right:              8px;
     width:              64px}
diff --git a/webUI/B220MagTapeDrive.html b/webUI/B220MagTapeDrive.html
index 20c6197..4d5b49f 100644
--- a/webUI/B220MagTapeDrive.html
+++ b/webUI/B220MagTapeDrive.html
@@ -87,6 +87,7 @@
     
 
     
UNLOADED
+
LANE 0
AT BOT
AT EOT
REWINDING
diff --git a/webUI/B220MagTapeDrive.js b/webUI/B220MagTapeDrive.js index c068c3d..05fb1cc 100644 --- a/webUI/B220MagTapeDrive.js +++ b/webUI/B220MagTapeDrive.js @@ -105,11 +105,28 @@ function B220MagTapeDrive(mnemonic, unitIndex, tcu, config) { this.reelBar = null; // handle for tape-full meter this.reelIcon = null; // handle for the reel spinner + this.boundInitialWriteBlock = B220MagTapeDrive.prototype.initialWriteBlock.bind(this); + this.boundOverwriteBlock = B220MagTapeDrive.prototype.overwriteBlock.bind(this); + this.boundReadNextBlock = B220MagTapeDrive.prototype.readNextBlock.bind(this); + this.boundReleaseDelay = B220MagTapeDrive.prototype.releaseDelay.bind(this); + this.boundReleaseUnit = B220MagTapeDrive.prototype.releaseUnit.bind(this); + this.boundReposition = B220MagTapeDrive.prototype.reposition.bind(this); + this.boundReverseDirection = B220MagTapeDrive.prototype.reverseDirection.bind(this); + this.boundSearchBackwardBlock = B220MagTapeDrive.prototype.searchBackwardBlock.bind(this); + this.boundSearchForwardBlock = B220MagTapeDrive.prototype.searchForwardBlock.bind(this); + this.boundSetBOT = B220MagTapeDrive.prototype.setBOT.bind(this); + this.boundSetEOT = B220MagTapeDrive.prototype.setEOT.bind(this); + this.boundSpaceBackwardBlock = B220MagTapeDrive.prototype.spaceBackwardBlock.bind(this); + this.boundSpaceEOIBlock = B220MagTapeDrive.prototype.spaceEOIBlock.bind(this); + this.boundSpaceForwardBlock = B220MagTapeDrive.prototype.spaceForwardBlock.bind(this); + this.boundStartUpBackward = B220MagTapeDrive.prototype.startUpBackward.bind(this); + this.boundStartUpForward = B220MagTapeDrive.prototype.startUpForward.bind(this); + this.doc = null; this.window = window.open("../webUI/B220MagTapeDrive.html", mnemonic, "location=no,scrollbars=no,resizable,width=384,height=184,left=0,top=" + y); this.window.addEventListener("load", - B220Util.bindMethod(this, B220MagTapeDrive.prototype.tapeDriveOnload), false); + B220MagTapeDrive.prototype.tapeDriveOnload.bind(this), false); } // this.tapeState enumerations @@ -146,10 +163,12 @@ B220MagTapeDrive.prototype.startOfBlockWords = 4; // inter-block tape gap + preface [words] B220MagTapeDrive.prototype.endOfBlockWords = 2; // end-of-block + erase gap [words] -B220MagTapeDrive.prototype.repositionWords = 2; +B220MagTapeDrive.prototype.repositionWords = 5; // number of words to reposition back into the block after a turnaround B220MagTapeDrive.prototype.startTime = 3; // tape start time [ms] +B220MagTapeDrive.prototype.startWords = 6; + // number of words traversed during tape start time B220MagTapeDrive.prototype.stopTime = 3; // tape stop time [ms] B220MagTapeDrive.prototype.turnaroundTime = 5; @@ -198,15 +217,23 @@ B220MagTapeDrive.prototype.nil = function nil() { }; /**************************************/ -B220MagTapeDrive.prototype.releaseUnit = function releaseUnit(releaseControl, alarm) { - /* Releases the busy status of the unit. Typically used as a timed call- - back to simulate the amount of time the unit is busy with an I/O */ +B220MagTapeDrive.prototype.releaseUnit = function releaseUnit(param) { + /* Releases the busy status of the unit. Returns but does not use its + parameter so it can be used with Promise.then() */ this.busy = false; this.designatedLamp.set(0); - if (releaseControl) { - releaseControl(alarm); - } + return param; +}; + +/**************************************/ +B220MagTapeDrive.prototype.releaseDelay = function releaseDelay(driveState) { + /* Delays the specified number of milliseconds in driveState.completionDelay. + Returns a Promise for completion of the delay */ + + return new Promise((resolve, reject) => { + setCallback(this.mnemonic, this, driveState.completionDelay, resolve, driveState); + }); }; /**************************************/ @@ -254,13 +281,14 @@ B220MagTapeDrive.prototype.spinReel = function spinReel(inches) { }; /**************************************/ -B220MagTapeDrive.prototype.moveTape = function moveTape(inches, delay, successor) { +B220MagTapeDrive.prototype.moveTape = function moveTape(inches, delay, successor, param) { /* Delays the I/O during tape motion, during which it animates the reel image icon. At the completion of the "delay" time in milliseconds, "successor" is - called with no parameters. */ + called with "param" as a parameter */ var delayLeft = Math.abs(delay); // milliseconds left to delay var direction = (inches < 0 ? -1 : 1); var inchesLeft = inches; // inches left to move tape + var initiallyReady = this.ready; // remember initial ready state to detect change var lastStamp = performance.now(); // last timestamp for spinDelay function spinFinish() { @@ -268,7 +296,7 @@ B220MagTapeDrive.prototype.moveTape = function moveTape(inches, delay, successor if (inchesLeft != 0) { this.spinReel(inchesLeft); } - successor.call(this); + successor.call(this, param); } function spinDelay() { @@ -283,28 +311,49 @@ B220MagTapeDrive.prototype.moveTape = function moveTape(inches, delay, successor } } - if ((delayLeft -= interval) > this.spinUpdateInterval) { - lastStamp = stamp; - this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, spinDelay); - } else { - this.timer = setCallback(this.mnemonic, this, delayLeft, spinFinish); - } - motion = inches*interval/delay; - if (inchesLeft*direction <= 0) { // inchesLeft crossed zero - motion = inchesLeft = 0; - } else if (motion*direction <= inchesLeft*direction) { - inchesLeft -= motion; - } else { - motion = inchesLeft; + if (initiallyReady && !this.ready) { // drive went not ready inchesLeft = 0; - } + this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, spinFinish); + } else { + delayLeft -= interval; + if (delayLeft > this.spinUpdateInterval) { + lastStamp = stamp; + this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, spinDelay); + } else { + this.timer = setCallback(this.mnemonic, this, delayLeft, spinFinish); + } - this.spinReel(motion); + motion = inchesLeft*interval/delayLeft; + if (inchesLeft*direction <= 0) { // inchesLeft crossed zero + motion = inchesLeft = 0; + } else if (motion*direction <= inchesLeft*direction) { + inchesLeft -= motion; + } else { + motion = inchesLeft; + inchesLeft = 0; + } + + this.spinReel(motion); + } } spinDelay.call(this); }; +/**************************************/ +B220MagTapeDrive.prototype.moveTapeTo = function moveTapeTo(index, result) { + /* Advances the tape to the specified image index and returns a Promise + that will resolve when tape motion completes */ + + return new Promise((resolve, reject) => { + var len = index - this.imgIndex; // number of words passed + var delay = len*this.millisPerWord; // amount of tape spin time + + this.imgIndex = index; + this.moveTape(len*this.inchesPerWord, delay, resolve, result); + }); +}; + /**************************************/ B220MagTapeDrive.prototype.setAtBOT = function setAtBOT(atBOT) { /* Controls the at-Beginning-of-Tape state of the tape drive */ @@ -324,6 +373,16 @@ B220MagTapeDrive.prototype.setAtBOT = function setAtBOT(atBOT) { } }; +/**************************************/ +B220MagTapeDrive.prototype.setBOT = function setBOT(driveState) { + /* Sets BOT status on the drive and releases the drive after encountering + physical BOT. Return value is designed for use with Promise.then() */ + + this.setAtBOT(true); + this.releaseUnit(driveState); // release the unit, but leave the control hung: + return driveState; // do not reject the Promise +}; + /**************************************/ B220MagTapeDrive.prototype.setAtEOT = function setAtEOT(atEOT) { /* Controls the at-End-of-Tape state of the tape drive */ @@ -339,6 +398,36 @@ B220MagTapeDrive.prototype.setAtEOT = function setAtEOT(atEOT) { } }; +/**************************************/ +B220MagTapeDrive.prototype.setEOT = function setEOT(driveState) { + /* Sets EOT status on the drive and releases the drive after encountering + physical EOT. Return value is designed for use with Promise.then() */ + + this.setAtEOT(true); + this.releaseUnit(driveState); // release the unit, but leave the control hung: + return driveState; // do not reject the Promise +}; + +/**************************************/ +B220MagTapeDrive.prototype.setLane = function(laneNr, param) { + /* Sets the lane of the tape drive and updates its annunciator on the drive + control panel. If the lane is changing, introduces a 70ms delay. Returns a + Promise that resolves once the lane is changed and any delay is completed. + "param" is not used by this routine, but is passed as a resolve result for + use with Promise.then() */ + var lane = laneNr%2; // make sure it's 0 or 1 + + this.$$("MTLaneNrLight").textContent = "LANE " + lane; + return new Promise((resolve, reject) => { + if (this.laneNr == lane) { + resolve(param); + } else { + this.laneNr = lane; + setCallback(this.mnemonic, this, 70, resolve, param); + } + }); +}; + /**************************************/ B220MagTapeDrive.prototype.setTapeReady = function setTapeReady(makeReady) { /* Controls the ready-state of the tape drive */ @@ -380,6 +469,7 @@ B220MagTapeDrive.prototype.setTapeUnloaded = function setTapeUnloaded() { this.reelBar.value = 0; this.reelIcon.style.visibility = "hidden"; this.$$("MTFileName").value = ""; + this.$$("MTLaneNrLight").style.visibility = "hidden"; B220Util.addClass(this.$$("MTUnloadedLight"), "annunciatorLit"); if (this.timer) { clearCallback(this.timer); @@ -388,100 +478,6 @@ B220MagTapeDrive.prototype.setTapeUnloaded = function setTapeUnloaded() { } }; -/**************************************/ -B220MagTapeDrive.prototype.findBlockStart = function findBlockStart() { - /* Searches forward in the tape image on the currently-selected lane for the - start of a block or magnetic end-of-tape markers, or physical end-of-tape, - whichever occurs first. If an inter-block (blank) gap word is found, skips - over all of them and reads the preface word. Returns the preface word, or - the magnetic EOT marker (-3) word, or if physical EOT is encountered, an - inter-block gap (-1) word */ - var imgLength = this.imgLength; // physical end of tape index - var lane = this.image[this.laneNr]; // image data for current lane - var result = this.markerGap; // function result - var state = 1; // FSA state variable - var w; // current image word - var x = this.imgIndex; // lane image word index - - while (x < imgLength) { - w = lane[x++]; - switch (state) { - case 1: // search for inter-block gap word - if (w == this.markerGap) { - state = 2; - } else if (w == this.markerMagEOT) { - result = w; // return the EOT word - this.imgIndex = x-1; // point to EOT word - x = imgLength; // kill the loop - } - break; - - case 2: // search for preface word - if (w >= 0) { - result = w; // found preface, return it - this.imgIndex = x; // point to first data word in block - x = imgLength; // kill the loop - } else if (w != this.markerGap) { - result = w; // return whatever marker word was found - this.imgIndex = x-1; // point to the word found - x = imgLength; // kill the loop - } - break; - - default: - x = imgLength; // kill the loop - throw new Error("Invalid state: B220MagTapeDrive.findBlockStart, " + state); - break; - } // switch state - } // while x - - return result; -}; - -/**************************************/ -B220MagTapeDrive.prototype.writeBlockStart = function writeBlockStart(length) { - /* Writes the start-of-block words to the tape image buffer in the current - lane number and at the current offset of this.imgIndex: 3 gap words + the - preface word with the binary block length */ - var x; - - for (x=0; x element to receive tape image data var mt = this; // tape drive object var win = this.window.open("./B220FramePaper.html", this.mnemonic + "-Unload", "location=no,scrollbars=yes,resizable,width=800,height=600"); + function findBlockStart() { + /* Searches forward in the tape image on the currently-selected lane for the + start of a block or magnetic end-of-tape markers, or physical end-of-tape, + whichever occurs first. If an inter-block (blank) gap word is found, skips + over all of them and reads the preface word. Returns the preface word, or + the magnetic EOT marker (-3) word, or if physical EOT is encountered, an + inter-block gap (-1) word */ + var imgLength = mt.imgLength; // physical end of tape index + var lane = mt.image[mt.laneNr]; // image data for current lane + var result = mt.markerGap; // function result + var state = 1; // FSA state variable + var w; // current image word + var x = mt.imgIndex; // lane image word index + + while (x < imgLength) { + w = lane[x++]; + switch (state) { + case 1: // search for inter-block gap word + if (w == mt.markerGap) { + state = 2; + } else if (w == mt.markerMagEOT) { + result = w; // return the EOT word + mt.imgIndex = x-1; // point to EOT word + x = imgLength; // kill the loop + } + break; + + case 2: // search for preface word + if (w >= 0) { + result = w; // found preface, return it + mt.imgIndex = x; // point to first data word in block + x = imgLength; // kill the loop + } else if (w != mt.markerGap) { + result = w; // return whatever marker word was found + mt.imgIndex = x-1; // point to the word found + x = imgLength; // kill the loop + } + break; + + default: + x = imgLength; // kill the loop + throw new Error("Invalid state: B220MagTapeDrive.unloadTape.findBlockStart, " + state); + break; + } // switch state + } // while x + + return result; + } + function unloadDriver() { /* Converts the tape image to ASCII once the window has displayed the waiting message */ @@ -801,16 +895,12 @@ B220MagTapeDrive.prototype.unloadTape = function unloadTape() { var lx; // lane index var nzw; // number of consecutive zero words var state; // lane processing state variable - var tape; //
 element to receive tape data
         var w;                          // current image word
         var wx;                         // word index within block
         var x = 0;                      // image data index
 
-        doc = win.document;
-        doc.title = "retro-220 " + mt.mnemonic + " Unload Tape";
-        tape = doc.getElementById("Paper");
-        while (tape.firstChild) {               // delete any existing 
 content
-            tape.removeChild(tape.firstChild);
+        while (image.firstChild) {      // delete any existing 
 content
+            image.removeChild(image.firstChild);
         }
 
         for (lx=0; lx<2; ++lx) {
@@ -823,7 +913,7 @@ B220MagTapeDrive.prototype.unloadTape = function unloadTape() {
                 case 1: // Search for start of block
                     nzw = 0;
                     mt.imgIndex = x;
-                    w = mt.findBlockStart();
+                    w = findBlockStart();
                     if (w < 0) {        // done with this lane
                         x = imgLength;  // kill the loop
                     } else {            // format the preface word
@@ -843,7 +933,7 @@ B220MagTapeDrive.prototype.unloadTape = function unloadTape() {
                         }
                         buf += "," + w.toString(16);
                     } else {
-                        tape.appendChild(doc.createTextNode(buf + "\n"));
+                        image.appendChild(doc.createTextNode(buf + "\n"));
                         state = 1;      // reset for next block
                     }
                     break;
@@ -862,8 +952,12 @@ B220MagTapeDrive.prototype.unloadTape = function unloadTape() {
         /* Loads a status message into the "paper" rendering area, then calls
         unloadDriver after a short wait to allow the message to appear */
 
-        win.document.getElementById("Paper").appendChild(
-                win.document.createTextNode("Rendering tape image... please wait..."));
+        win.removeEventListener("load", unloadSetup, false);
+        doc = win.document;
+        doc.title = "retro-220 " + mt.mnemonic + " Unload Tape";
+        image = doc.getElementById("Paper");
+        image.appendChild(win.document.createTextNode(
+                "Rendering tape image... please wait..."));
         setTimeout(unloadDriver, 50);
     }
 
@@ -877,105 +971,60 @@ B220MagTapeDrive.prototype.unloadTape = function unloadTape() {
 B220MagTapeDrive.prototype.tapeRewind = function tapeRewind(laneNr, lockout) {
     /* Rewinds the tape. Makes the drive not-ready and delays for an appropriate
     amount of time depending on how far up-tape we are. Readies the unit again
-    when the rewind is complete unless lockout is truthy */
-    var lastStamp;
+    when the rewind is complete unless lockout is truthy. Returns a Promise that
+    resolves when the rewind completes */
 
-    function rewindFinish() {
-        this.timer = 0;
-        this.tapeState = this.tapeLocal;
-        B220Util.removeClass(this.$$("MTRewindingLight"), "annunciatorLit");
-        this.laneNr = laneNr%2;
-        this.rewindLock = (lockout ? true : false);
-        this.rwlLamp.set(this.rewindLock ? 1 : 0);
-        this.setTapeReady(!this.rewindLock);
-        this.releaseUnit(null, false);
-        if (this.ready) {
-            this.tcu.tapeUnitFinished();
+    return new Promise((resolve, reject) => {
+        var lastStamp;
+
+        function rewindFinish() {
+            this.timer = 0;
+            this.tapeState = this.tapeLocal;
+            B220Util.removeClass(this.$$("MTRewindingLight"), "annunciatorLit");
+            this.rewindLock = (lockout ? true : false);
+            this.rwlLamp.set(this.rewindLock ? 1 : 0);
+            this.setTapeReady(!this.rewindLock);
+            resolve(this.setLane(laneNr, null));
         }
-    }
 
-    function rewindDelay() {
-        var inches;
-        var stamp = performance.now();
-        var interval = stamp - lastStamp;
+        function rewindDelay() {
+            var inches;
+            var stamp = performance.now();
+            var interval = stamp - lastStamp;
 
-        if (interval <= 0) {
-            interval = this.spinUpdateInterval/2;
+            if (interval <= 0) {
+                interval = this.spinUpdateInterval/2;
+            }
+            if (this.tapeInches <= 0) {
+                this.setAtBOT(true);
+                this.timer = setCallback(this.mnemonic, this, 1000, rewindFinish);
+            } else {
+                inches = interval*this.rewindSpeed;
+                lastStamp = stamp;
+                this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, rewindDelay);
+                this.spinReel(-inches);
+            }
         }
-        if (this.tapeInches <= 0) {
-            this.setAtBOT(true);
-            this.timer = setCallback(this.mnemonic, this, 1000, rewindFinish);
-        } else {
-            inches = interval*this.rewindSpeed;
-            lastStamp = stamp;
+
+        function rewindStart() {
+            this.designatedLamp.set(0);
+            lastStamp = performance.now();
             this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, rewindDelay);
-            this.spinReel(-inches);
         }
-    }
 
-    function rewindStart() {
-        this.designatedLamp.set(0);
-        lastStamp = performance.now();
-        this.timer = setCallback(this.mnemonic, this, this.spinUpdateInterval, rewindDelay);
-    }
-
-    if (this.timer) {
-        clearCallback(this.timer);
-        this.timer = 0;
-    }
-    if (this.tapeState != this.tapeUnloaded && this.tapeState != this.tapeRewinding) {
-        this.busy = true;
-        this.tapeState = this.tapeRewinding;
-        this.setAtEOT(false);
-        B220Util.addClass(this.$$("MTRewindingLight"), "annunciatorLit");
-        this.timer = setCallback(this.mnemonic, this, 1000, rewindStart);
-    }
-};
-
-/**************************************/
-B220MagTapeDrive.prototype.tapeReposition = function tapeReposition(successor) {
-    /* Reverses tape direction after a forward tape operation and repositions
-    the head two words from the end of the block just passed, giving room for
-    startup of the next forward operation. At completion, calls the successor
-    function, passing false.
-
-    A real 220 drive repositioned tape about 60 digits (five words) from the end
-    of the data portion of the block, but that was to allow for tape acceleration
-    of about 3ms, at which point it took about 2ms (50 digits, or just over four
-    words) to reach the end of the erase gap and start of the inter-block gap
-    for the next block. this.repositionWords is sized to approximate that 2ms
-    delay */
-    var done = false;                   // completion flag
-    var lane = this.image[this.laneNr]; // image data for current lane
-    var state = 1;                      // FSA state variable
-    var x = this.imgIndex-1;            // lane image word index
-
-    do {
-        if (x <= 0) {
-            done = true;
-        } else {
-            switch (state) {
-            case 1: // initial state: skip backwards until erase-gap
-                if (lane[x] == this.markerEOB) {
-                    state = 2;
-                } else {
-                    --x;
-                }
-                break;
-            case 2: // skip over erase-gap words
-                if (lane[x] == this.markerEOB) {
-                    --x;
-                } else {
-                    done = true;
-                }
-                break;
-            } // switch state
+        if (this.timer) {
+            clearCallback(this.timer);
+            this.timer = 0;
         }
-    } while (!done);
 
-    x = this.imgIndex - x + this.repositionWords;       // words to reposition
-    this.imgIndex -= x;
-    this.moveTape(-x*this.inchesPerWord, this.turnaroundTime, successor);
+        if (this.tapeState != this.tapeUnloaded && this.tapeState != this.tapeRewinding) {
+            this.busy = true;
+            this.tapeState = this.tapeRewinding;
+            this.setAtEOT(false);
+            B220Util.addClass(this.$$("MTRewindingLight"), "annunciatorLit");
+            this.timer = setCallback(this.mnemonic, this, 1000, rewindStart);
+        }
+    });
 };
 
 /**************************************/
@@ -1006,7 +1055,9 @@ B220MagTapeDrive.prototype.RewindBtn_onclick = function RewindBtn_onclick(ev) {
     /* Handle the click event for the REWIND button */
 
     if (!this.busy && !this.powerOn && this.tapeState != this.tapeUnloaded) {
-        this.tapeRewind(this.laneNr, this.rewindLock, null);
+        this.tapeRewind(this.laneNr, this.rewindLock)
+        .then(this.boundReleaseUnit)
+        .then(this.tcu.boundTapeUnitFinished);
     }
 };
 
@@ -1092,1118 +1143,420 @@ B220MagTapeDrive.prototype.tapeDriveOnload = function tapeDriveOnload() {
     this.window.addEventListener("beforeunload",
             B220MagTapeDrive.prototype.beforeUnload, false);
     this.$$("LoadBtn").addEventListener("click",
-            B220Util.bindMethod(this, B220MagTapeDrive.prototype.LoadBtn_onclick), false);
+            B220MagTapeDrive.prototype.LoadBtn_onclick.bind(this), false);
     this.$$("UnloadBtn").addEventListener("click",
-            B220Util.bindMethod(this, B220MagTapeDrive.prototype.UnloadBtn_onclick), false);
+            B220MagTapeDrive.prototype.UnloadBtn_onclick.bind(this), false);
     this.$$("RewindBtn").addEventListener("click",
-            B220Util.bindMethod(this, B220MagTapeDrive.prototype.RewindBtn_onclick), false);
+            B220MagTapeDrive.prototype.RewindBtn_onclick.bind(this), false);
     this.unitDesignateList.addEventListener("change",
-            B220Util.bindMethod(this, B220MagTapeDrive.prototype.UnitDesignate_onchange), false);
+            B220MagTapeDrive.prototype.UnitDesignate_onchange.bind(this), false);
     this.$$("RWLRBtn").addEventListener("click",
-            B220Util.bindMethod(this, B220MagTapeDrive.prototype.RWLRBtn_onclick), false);
+            B220MagTapeDrive.prototype.RWLRBtn_onclick.bind(this), false);
     this.$$("WriteBtn").addEventListener("click",
-            B220Util.bindMethod(this, B220MagTapeDrive.prototype.WriteBtn_onclick), false);
+            B220MagTapeDrive.prototype.WriteBtn_onclick.bind(this), false);
     this.$$("NotWriteBtn").addEventListener("click",
-            B220Util.bindMethod(this, B220MagTapeDrive.prototype.WriteBtn_onclick), false);
+            B220MagTapeDrive.prototype.WriteBtn_onclick.bind(this), false);
     this.$$("TransportOnBtn").addEventListener("click",
-            B220Util.bindMethod(this, B220MagTapeDrive.prototype.TransportOnBtn_onclick), false);
+            B220MagTapeDrive.prototype.TransportOnBtn_onclick.bind(this), false);
     this.$$("TransportStandbyBtn").addEventListener("click",
-            B220Util.bindMethod(this, B220MagTapeDrive.prototype.TransportOnBtn_onclick), false);
+            B220MagTapeDrive.prototype.TransportOnBtn_onclick.bind(this), false);
 };
 
 /**************************************/
-B220MagTapeDrive.prototype.searchBlock = function searchBlock(blockReady, releaseControl, laneNr) {
-    /* Searches blocks on tape. "blockReady" is a function to be called after a
-    block is searched. "releaseControl" is a function to call after the control
-    signals completion (see release). "laneNr" is the lane on the tape to search.
-    This routine is used for MTS and MFS */
-    var imgLength = this.imgLength;     // physical end of tape index
-    var keyWord = 0;                    // keyWord read from block
-    var lane;                           // image data for current lane
-    var result = false;
-    var startDelay = this.startTime;    // delay for drive startup and lane change
-    var that = this;
+B220MagTapeDrive.prototype.startUpForward = function startUpForward(driveState) {
+    /* Initializes the I/O in a forward direction and provides the start-up
+    delay for drive acceleration. Returns a Promise that resolves at completion
+    of the start-up acceleration */
 
-    function release() {
-        /* Releases the unit and control */
-
-        that.releaseUnit(releaseControl, false);
-    }
-
-    function finish() {
-        /* Wraps up the positioning and delays before releasing the unit and control */
-        var delay = 16 - that.startTime;
-
-        setCallback(that.mnemonic, that, delay, release);
-    }
-
-    function turnaround() {
-        /* Repositions the tape after the last block is written */
-
-        that.tapeReposition(finish);
-    }
-
-    function signalControl() {
-        /* Returns to the tape control after encountering an end-of-tape block,
-        after the tape has been repositioned to read the EOT block */
-
-        blockReady(false, true, keyWord, null, null, turnaround);
-    }
-
-    function repositionAndSignal() {
-        /* Repositions the tape image so that an end-of-tape block can be read
-        by the next operation. Then terminates the I/O */
-
-        that.tapeReposition(signalControl);
-    }
-
-    function finalizeBlockForward() {
-        /* Returns the keyWord to the tape control after searching one block in
-        a forward direction */
-
-        blockReady(false, false, keyWord, searchBlockForward, searchBlockBackward, turnaround);
-    }
-
-    function abort() {
-        /* Aborts the I/O due to some error */
-
-        blockReady(true, false, 0, null, null, release);
-    }
-
-    function setEOT() {
-        /* Sets EOT status after positioning to end-of-tape. Does not release
-        the control unit */
-
-        that.setAtEOT(true);
-        that.releaseUnit(null, false);
-    }
-
-    function advance (index, successor) {
-        /* Advances the tape after a block is passed, then calls the successor */
-        var len = index - that.imgIndex;    // number of words passed
-        var delay = len*that.millisPerWord; // amount of tape spin time
-
-        that.imgIndex = index;
-        that.moveTape(len*that.inchesPerWord, delay, successor);
-    }
-
-    function searchBlockForward() {
-        /* Reads the start of the next block in the tape image in a forward
-        direction to obtain its keyword */
-        var count = 0;                  // word counter
-        var done = false;               // completion flag
-        var state = 1;                  // FSA state variable
-        var words = 0;                  // number of words from preface
-        var w;                          // current image word
-        var x = that.imgIndex;          // lane image word index
-
-        if (!that.ready) {
-            done = true;                // drive went non-ready
-            advance.call(that, x, abort);
-        } else {
-            do {
-                if (x >= imgLength) {
-                    done = true;        // at EOT: just exit and leave everybody hanging...
-                    that.busy = false;
-                    advance.call(that, x, setEOT);
-                } else {
-                    w = lane[x];
-
-                    switch (state) {
-                    case 1: // initial state: skip over flaw and intra-block words
-                        if (w == that.markerGap) {
-                            state = 2;
-                        } else {
-                            ++x;
-                        }
-                        break;
-
-                    case 2: // skip over inter-block gap and magnetic EOT words
-                        if (w == that.markerGap) {
-                            ++x;
-                        } else if (w == that.markerEOT) {
-                            ++x;
-                        } else if (w >= 0) {
-                            state = 3;          // found the preface
-                        } else {
-                            state = 1;          // ignore this block
-                        }
-                        break;
-
-                    case 3: // read the preface
-                        ++x;
-                        words = w;
-                        if (words < that.minBlockWords && words > 1) {
-                            done = true;        // preface check: invalid block size
-                            advance.call(that, x, abort);
-                        } else {
-                            state = 4;          // advance to the keyWord
-                        }
-                        break;
-
-                    case 4: // read the keyword
-                        ++x;
-                        if (w < 0) {            // block was shorter than preface indicated,
-                            state = 1;          // so ignore it and look for next block
-                        } else {
-                            done = true;
-                            keyWord = w;
-                            if (words == 1) {   // signal end-of-tape block
-                                advance.call(that, x, repositionAndSignal);
-                            } else {            // return keyWord to control
-                                advance.call(that, x, finalizeBlockForward);
-                            }
-                        }
-                        break;
-                    } // switch state
-                }
-            } while (!done);
-        }
-    }
-
-    function firstBlock() {
-        /* Called after the startTime delay to signal the control unit we are
-        ready for the first block of data */
-
-        blockReady(false, false, 0, searchBlockForward, searchBlockBackward, release);
-    }
-
-    if (this.busy || this.rewindLock) {
-        result = true;                  // unit busy or in RWL
-    } else if (!this.ready) {
-        result = true;                  // unit not ready
+    if (this.busy) {
+        driveState.state = driveState.driveBusy;
+        return Promise.reject(driveState);
+    } else if (!this.ready || this.rewindLock) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
     } else if (this.atEOT) {
-        result = true;                  // tape at EOT
+        driveState.state = driveState.driveAtEOT;
+        return Promise.reject(driveState);
     } else {
-        this.busy = true;
-        this.designatedLamp.set(1);
-        this.setAtBOT(false);
-        if (laneNr != this.laneNr) {
-            this.laneNr = laneNr;
-            startDelay += 70;           // additional time for lane change
-        }
-
-        // Begin with a delay for start-up time
-        lane = this.image[this.laneNr];
-        setCallback(this.mnemonic, this, startDelay, firstBlock);
+        return new Promise((resolve, reject) => {
+            this.busy = true;
+            this.designatedLamp.set(1);
+            this.setAtBOT(false);
+            this.imgIndex += this.startWords;
+            driveState.completionDelay -= this.startTime;
+            setCallback(this.mnemonic, this, this.startTime, resolve, driveState);
+            this.moveTape(this.startWords*this.inchesPerWord, this.startTime, resolve, driveState);
+        });
     }
-
-    //console.log(this.mnemonic + " searchBlock       result=" + result.toString());
-    return result;
 };
 
 /**************************************/
-B220MagTapeDrive.prototype.readBlock = function readBlock(blockReady, releaseControl) {
-    /* Reads blocks on tape. "blockReady" is a function to be called after a
-    block is read. "releaseControl" is a function to call after the control
-    signals completion (see release). This routine is used for both MRD and MRR,
-    as block lengths are determined by the tape control unit */
-    var controlWord = 0;                // stashed control word for end-of-tape or control block
-    var imgLength = this.imgLength;     // physical end of tape index
-    var lane = this.image[this.laneNr]; // image data for current lane
-    var result = false;
-    var that = this;
+B220MagTapeDrive.prototype.startUpBackward = function startUpBackward(driveState) {
+    /* Initializes the I/O in a backward direction and provides the start-up
+    delay for drive acceleration */
 
-    function release() {
-        /* Releases the unit and control */
-
-        that.releaseUnit(releaseControl, false);
-    }
-
-    function finish() {
-        /* Wraps up the positioning and delays before releasing the unit and control */
-        var delay = 18 - that.startTime - that.repositionTime;
-
-        setCallback(that.mnemonic, that, delay, release);
-    }
-
-    function turnaround() {
-        /* Repositions the tape after the last block is written */
-
-        that.tapeReposition(finish);
-    }
-
-    function signalControl() {
-        /* Returns to the tape control after encountering an end-of-tape block,
-        using the control word stashed by readBlockNext. Note that tape must be
-        positioned to read the block after the end-of-tape block, but we are
-        already past the first word in the block, so we don't reposition backwards
-        in this case -- just leave the tape where it is and normal pre-block
-        scanning will find the next block */
-
-        blockReady(false, true, controlWord, null, finish);
-    }
-
-    function repositionAndSignal() {
-        /* Repositions the tape image so that the block following a control block
-        can be read by the next operation. Then terminates the I/O */
-
-        that.tapeReposition(signalControl);
-    }
-
-    function finalizeBlock() {
-        /* Returns to the tape control after completion of one block */
-
-        blockReady(false, false, 0, readBlockNext, turnaround);
-    }
-
-    function abort() {
-        /* Aborts the I/O due to some error */
-
-        blockReady(true, false, 0, null, release);
-    }
-
-    function repositionAndAbort() {
-        /* Repositions the tape image so that the block causing the abort can be
-        read by the next operation. Then aborts the I/O */
-
-        that.tapeReposition(abort);
-    }
-
-    function setEOT() {
-        /* Sets EOT status after positioning to end-of-tape. Does not release
-        the control unit */
-
-        that.setAtEOT(true);
-        that.releaseUnit(null, false);
-    }
-
-    function advance (index, successor) {
-        /* Advances the tape after a block is passed, then calls the successor */
-        var len = index - that.imgIndex;    // number of words passed
-        var delay = len*that.millisPerWord; // amount of tape spin time
-
-        that.imgIndex = index;
-        that.moveTape(len*that.inchesPerWord, delay, successor);
-    }
-
-    function readBlockNext(storeWord, record, controlEnabled) {
-        /* Reads the next block in the tape image. "storeWord" is a call-back
-        to store the next word to the Processor's memory. If "record" is true,
-        the preface is stored in memory before the data. If "controlEnabled" is
-        true, control blocks are recognized */
-        var controlBlock = false;       // true if control block detected
-        var count = 0;                  // word counter
-        var done = false;               // completion flag
-        var firstWord = true;           // flag for initial memory fetch
-        var sign = 0;                   // sign digit of keyword
-        var state = 1;                  // FSA state variable
-        var words = 0;                  // number of words from preface
-        var w;                          // current image word
-        var x = that.imgIndex;          // lane image word index
-
-        if (!that.ready) {
-            done = true;                // drive went non-ready
-            advance.call(that, x, abort);
-        } else {
-            do {
-                if (x >= imgLength) {
-                    done = true;        // at EOT: just exit and leave everybody hanging...
-                    that.busy = false;
-                    advance.call(that, x, setEOT);
-                } else {
-                    w = lane[x];
-
-                    switch (state) {
-                    case 1: // initial state: skip over flaw and intra-block words
-                        if (w == that.markerGap) {
-                            state = 2;
-                        } else {
-                            ++x;
-                        }
-                        break;
-
-                    case 2: // skip over inter-block gap and magnetic EOT words
-                        if (w == that.markerGap) {
-                            ++x;
-                        } else if (w == that.markerEOT) {
-                            ++x;
-                        } else if (w >= 0) {
-                            state = 3;          // found the preface
-                        } else {
-                            done = true;        // not a formatted tape
-                            advance.call(that, x, repositionAndAbort);
-                        }
-                        break;
-
-                    case 3: // read the preface and check for EOT block
-                        ++x;
-                        words = w;
-                        if (words == 1) {
-                            state = 6;          // detected end-of-tape block
-                        } else if (words < that.minBlockWords && words > 1) {
-                            done = true;        // preface check: invalid block size
-                            advance.call(that, x, abort);
-                        } else {
-                            state = 4;          // normal or control block
-                        }
-                        break;
-
-                    case 4: // read the keyword, detect control block, and store the preface if necessary
-                        ++x;
-                        if (w < 0) {
-                            done = true;        // block was shorter than preface indicated
-                            advance.call(that, x-1, repositionAndAbort);
-                        } else {
-                            if (controlEnabled) {
-                                sign = (w - w%0x10000000000)%0x10;
-                                if (sign == 7) {
-                                    controlBlock = true;
-                                    // strip sign digit from keyword (not entirely sure this should be done)
-                                    w %= 0x10000000000;
-                                }
-                            }
-
-                            if (record) {       // store the preface word (with keyword sign if a control block)
-                                sign = ((sign*0x10 + (words%100 - words%10)/10)*0x10 + words%10)*0x100000000;
-                                done = (storeWord(firstWord, sign) < 0);        // true if memory error
-                                firstWord = false;
-                            }
-
-                            if (controlBlock) {
-                                --words;        // prevent storing the control word
-                            }
-
-                            if (done) {         // error storing preface word
-                                advance.call(that, x, abort);
-                            } else {
-                                w = storeWord(firstWord, w);
-                                if (w < 0) {
-                                    done = true;        // keyword memory store failed
-                                    advance.call(that, x, abort);
-                                } else {
-                                    firstWord = false;
-                                    ++count;
-                                    state = 5;
-                                }
-                            }
-                        }
-                        break;
-
-                    case 5: // read and store the remaining block words
-                        if (count < words) {
-                            if (w < 0) {
-                                done = true;    // block was shorter than preface indicated
-                                advance.call(that, x-1, repositionAndAbort);
-                            } else {
-                                w = storeWord(firstWord, w);
-                                if (w < 0) {
-                                    done = true;        // memory store failed
-                                    advance.call(that, x, abort);
-                                } else {
-                                    firstWord = false;
-                                    ++x;
-                                    ++count;
-                                }
-                            }
-                        } else if (controlBlock) {
-                            if (w < 0) {
-                                done = true;    // block was shorter than preface indicated
-                                advance.call(that, x-1, repositionAndAbort);
-                            } else {
-                                controlWord = w;
-                                ++x;
-                                state = 7;      // deal with the control word after EOB
-                            }
-                        } else {
-                            state = 7;          // check for proper EOB
-                        }
-                        break;
-
-                    case 6: // fetch the control word for an end-of-tape block and terminate
-                        controlWord = w;
-                        ++x;
-                        done = true;            // signal end-of-tape block
-                        advance.call(that, x, signalControl);
-                        break;
-
-                    case 7: // check for proper end-of-block and terminate
-                        done = true;
-                        if (w != that.markerEOB) {
-                            advance.call(that, x, repositionAndAbort);
-                        } else {                // block was longer than preface indicated
-                            if (controlBlock) {
-                                advance.call(that, x+that.endOfBlockWords, repositionAndSignal);
-                            } else {
-                                advance.call(that, x+that.endOfBlockWords, finalizeBlock);
-                            }
-                        }
-                        break;
-                    } // switch state
-                }
-            } while (!done);
-        }
-    }
-
-    function firstBlock() {
-        /* Called after the startTime delay to signal the control unit we are
-        ready for the first block of data */
-
-        blockReady(false, false, 0, readBlockNext, release);
-    }
-
-    if (this.busy || this.rewindLock) {
-        result = true;                  // unit busy or in RWL
-    } else if (!this.ready) {
-        result = true;                  // unit not ready
-    } else if (this.atEOT) {
-        result = true;                  // tape at EOT
+    if (this.busy) {
+        driveState.state = driveState.driveBusy;
+        return Promise.reject(driveState);
+    } else if (!this.ready || this.rewindLock) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
+    } else if (this.atBOT) {
+        driveState.state = driveState.driveAtBOT;
+        return Promise.reject(driveState);
     } else {
-        this.busy = true;
-        this.designatedLamp.set(1);
-        this.setAtBOT(false);
-
-        // Begin with a delay for start-up time
-        setCallback(this.mnemonic, this, this.startTime, firstBlock);
+        return new Promise((resolve, reject) => {
+            this.busy = true;
+            this.designatedLamp.set(1);
+            this.setAtEOT(false);
+            this.imgIndex -= this.startWords;
+            driveState.completionDelay -= this.startTime;
+            setCallback(this.mnemonic, this, this.startTime, resolve, driveState);
+            this.moveTape(-this.startWords*this.inchesPerWord, this.startTime, resolve, driveState);
+        });
     }
-
-    //console.log(this.mnemonic + " readBlock         result=" + result.toString());
-    return result;
 };
 
 /**************************************/
-B220MagTapeDrive.prototype.overwriteBlock = function overwriteBlock(blockReady, releaseControl) {
-    /* Overwrites blocks on tape. "blockReady" is a function to be called after
-    a block is overwritten. "releaseControl" is a function to call after the
-    control signals completion (see release). This routine is used for both MOW
-    and MOR, as block lengths are determined by the tape control unit */
-    var controlWord = 0;                // stashed control word for end-of-tape block
-    var imgLength = this.imgLength;     // physical end of tape index
-    var lane = this.image[this.laneNr]; // image data for current lane
-    var result = false;
-    var that = this;
-
-    function release() {
-        /* Releases the unit and control */
-
-        that.releaseUnit(releaseControl, false);
-    }
-
-    function finish() {
-        /* Wraps up the positioning and delays before releasing the unit and control */
-        var delay = 18 - that.startTime - that.repositionTime;
-
-        setCallback(that.mnemonic, that, delay, release);
-    }
-
-    function turnaround() {
-        /* Repositions the tape after the last block is written */
-
-        that.tapeReposition(finish);
-    }
-
-    function signalControl() {
-        /* Returns to the tape control after encountering an end-of-tape block,
-        using the control word stashed by writeBlock. Note that tape must be
-        positioned to read the block after the end-of-tape block, but we are
-        already past the first word in the block, so we don't reposition backwards
-        in this case -- just leave the tape where it is and normal pre-block
-        scanning will find the next block */
-
-        blockReady(false, true, controlWord, null, finish);
-    }
-
-    function finalizeBlock() {
-        /* Returns to the tape control after completion of one block */
-
-        blockReady(false, false, 0, writeBlock, turnaround);
-    }
-
-    function abort() {
-        /* Aborts the I/O due to some error */
-
-        blockReady(true, false, 0, null, release);
-    }
-
-    function repositionAndAbort() {
-        /* Repositions the tape image so that the block causing the abort can be
-        read by the next operation. Then aborts the I/O */
-
-        that.tapeReposition(abort);
-    }
-
-    function setEOT() {
-        /* Sets EOT status after positioning to end-of-tape. Does not release
-        the control unit */
-
-        that.setAtEOT(true);
-        that.releaseUnit(null, false);
-    }
-
-    function advance (index, successor) {
-        /* Advances the tape after a block is passed, then calls the successor */
-        var len = index - that.imgIndex;    // number of words passed
-        var delay = len*that.millisPerWord; // amount of tape spin time
-
-        that.imgIndex = index;
-        if (index > that.imgTopWordNr) {
-            that.imgTopWordNr = index;
-        }
-        that.moveTape(len*that.inchesPerWord, delay, successor);
-    }
-
-    function writeBlock(fetchWord, words) {
-        /* Overwrites the next block in the tape image. "fetchWord" is a call-back
-        to retrieve the next word from the Processor's memory. "words" is the
-        number of words in the block, 0 => "record" mode */
-        var count = 0;                  // word counter
-        var done = false;               // completion flag
-        var firstWord = true;           // flag for initial memory fetch
-        var state = 1;                  // FSA state variable
-        var w;                          // current image word
-        var x = that.imgIndex;          // lane image word index
-
-        if (!that.ready) {
-            done = true;                // drive went non-ready
-            advance.call(that, x, abort);
-        } else {
-            do {
-                if (x >= imgLength) {
-                    done = true;        // at EOT: just exit and leave everybody hanging...
-                    that.busy = false;
-                    advance.call(that, x, setEOT);
-                } else {
-                    w = lane[x];
-
-                    switch (state) {
-                    case 1: // initial state: skip over flaw and intra-block words
-                        if (w == that.markerGap) {
-                            state = 2;
-                        } else {
-                            ++x;
-                        }
-                        break;
-
-                    case 2: // skip over inter-block gap and magnetic EOT words
-                        if (w == that.markerGap) {
-                            ++x;
-                        } else if (w == that.markerEOT) {
-                            ++x;
-                        } else if (w >= 0) {
-                            state = 3;          // found the preface
-                        } else {
-                            done = true;        // not a formatted tape
-                            advance.call(that, x, abort);
-                        }
-                        break;
-
-                    case 3: // check the preface
-                        ++x;
-                        if (words <= 0) {       // overwrite, record: fetch preface
-                            words = fetchWord(firstWord);
-                            if (words < 0) {    // memory fetch failed
-                                done = true;
-                            } else {            // convert preface word to binary
-                                firstWord = false;
-                                words = ((words - words%0x100000000)/0x100000000)%0x100;
-                                if (words > 0) {
-                                    words = (words >>> 4)*10 + words%0x10;
-                                } else {
-                                    words = 100;        // preface == 0 => 100
-                                }
-                            }
-                        }
-
-                        if (done) {             // preface fetch failed
-                            advance.call(that, x, abort);
-                        } else if (w < that.minBlockWords && w > 1) {
-                            done = true;        // preface check: invalid block size
-                            advance.call(that, x, abort);
-                        } else if (w == words) {
-                            state = 4;          // preface match: overwrite the block
-                        } else if (w == 1) {
-                            state = 5;          // preface mismatch on end-of-tape block
-                        } else {
-                            done = true;        // other preface mismatch
-                            advance.call(that, x, repositionAndAbort);
-                        }
-                        break;
-
-                    case 4: // overwrite the block words
-                        if (count < words) {
-                            if (w < 0) {
-                                done = true;    // block was shorter than preface indicated
-                                advance.call(that, x-1, repositionAndAbort);
-                            } else {
-                                w = fetchWord(firstWord);
-                                if (w < 0) {
-                                    done = true;        // memory fetch failed
-                                    advance.call(that, x, abort);
-                                } else {
-                                    firstWord = false;
-                                    lane[x] = w;
-                                    ++x;
-                                    ++count;
-                                }
-                            }
-                        } else if (count < that.minBlockWords) {
-                            if (w < 0) {        // block was shorter than preface indicated
-                                done = true;
-                                advance.call(that, x-1, repositionAndAbort);
-                            } else {
-                                lane[x] = 0;
-                                ++x;
-                                ++count;
-                            }
-                        } else if (w == that.markerEOB) {
-                            done = true;        // normal block termination
-                            advance.call(that, x+that.endOfBlockWords, finalizeBlock);
-                        } else {
-                            done = true;        // block was longer than preface indicated
-                            advance.call(that, x, repositionAndAbort);
-                        }
-                        break;
-
-                    case 5: // fetch the control word for an end-of-tape preface mismatch and terminate
-                        controlWord = w;
-                        ++x;
-                        done = true;            // signal end-of-tape block
-                        advance.call(that, x, signalControl);
-                        break;
-                    } // switch state
-                }
-            } while (!done);
-        }
-    }
-
-    function firstBlock() {
-        /* Called after the startTime delay to signal the control unit we are
-        ready for the first block of data */
-
-        blockReady(false, false, 0, writeBlock, release);
-    }
-
-    if (this.busy || this.rewindLock) {
-        result = true;                  // unit busy or in RWL
-    } else if (!this.ready) {
-        result = true;                  // unit not ready
-    } else if (this.notWrite) {
-        result = true;                  // tape in not-write status
-    } else if (this.atEOT) {
-        result = true;                  // tape at EOT
-    } else {
-        this.busy = true;
-        this.imgWritten = true;
-        this.designatedLamp.set(1);
-        this.setAtBOT(false);
-
-        // Begin with a delay for start-up time
-        setCallback(this.mnemonic, this, this.startTime, firstBlock);
-    }
-
-    //console.log(this.mnemonic + " overwriteBlock    result=" + result.toString());
-    return result;
-};
-
-/**************************************/
-B220MagTapeDrive.prototype.initialWriteBlock = function initialWriteBlock(blockReady, releaseControl) {
-    /* Writes blocks on edited (blank) tape. "blockReady" is a function to be
-    called after the block is written. "releaseControl" is a function to call
-    after the control signals completion (see release). This routine is used for
-    both MIW and MIR, as block lengths are determined by the tape control unit */
-    var imgLength = this.imgLength;     // physical end of tape index
-    var lane = this.image[this.laneNr]; // image data for current lane
-    var result = false;
-    var that = this;
-
-    function release() {
-        /* Releases the unit and control */
-
-        that.releaseUnit(releaseControl, false);
-    }
-
-    function finish() {
-        /* Wraps up the positioning and delays before releasing the unit and control */
-        var delay = 20 - that.startTime - that.repositionTime;
-
-        setCallback(that.mnemonic, that, delay, release);
-    }
-
-    function turnaround() {
-        /* Repositions the tape after the last block is written */
-
-        that.tapeReposition(finish);
-    }
-
-    function finalizeWrite() {
-        /* Write a longer inter-block gap so that MPE will work at magnetic
-        end-of-tape, check for end-of-tape, and initiate repositioning prior
-        to terminating the I/O */
-        var count = that.startOfBlockWords*2; // number of gap words
-        var done = false;               // loop control
-        var x = that.imgIndex;          // lane image word index
-
-        do {
-            if (x >= imgLength) {
-                done = true;            // at end-of-tape
-                that.setAtEOT(true);
-                advance.call(that, x, finish);
-            } else if (count > 0) {
-                --count;                // write extra inter-block gap word
-                lane[x] = that.markerGap;
-                ++x;
-            } else if (lane[x] == that.markerMagEOT) {
-                ++x;                    // space over magnetic EOT words
-            } else {
-                done = true;            // finish write and initiate repositioning
-                advance.call(that, x, turnaround);
-            }
-        } while(!done);
-    }
-
-    function finalizeBlock() {
-        /* Returns to the tape control after completion of one block */
-
-        blockReady(false, writeBlock, finalizeWrite);
-    }
-
-    function abort() {
-        /* Aborts the I/O due to some error */
-
-        blockReady(true, null, release);
-    }
-
-    function setEOT() {
-        /* Sets EOT status after positioning to end-of-tape. Does not release
-        the control unit */
-
-        that.setAtEOT(true);
-        that.releaseUnit(null, false);
-    }
-
-    function advance (index, successor) {
-        /* Advances the tape after a block is passed, then calls the successor */
-        var len = index - that.imgIndex;    // number of words passed
-        var delay = len*that.millisPerWord; // amount of tape spin time
-
-        that.imgIndex = index;
-        if (index > that.imgTopWordNr) {
-            that.imgTopWordNr = index;
-        }
-        that.moveTape(len*that.inchesPerWord, delay, successor);
-    }
-
-    function writeBlock(fetchWord, words) {
-        /* Initial-writes the next block in the tape image. "fetchWord" is a
-        call-back to retrieve the next word from the Processor's memory.
-        "words" is the number of words in the block, 0 => "record" mode */
-        var count = 0;                  // word counter
-        var done = false;               // completion flag
-        var firstWord = true;           // flag for initial memory fetch
-        var gapCount = 0;               // count of consecutive inter-block gap words
-        var state = 1;                  // FSA state variable
-        var w;                          // memory word
-        var x = that.imgIndex;          // lane image word index
-
-        if (!that.ready) {
-            done = true;                // drive went non-ready
-            advance.call(that, x, abort);
-        } else {
-            do {
-                if (x >= imgLength) {
-                    done = true;        // at EOT: just exit and leave everybody hanging...
-                    that.busy = false;
-                    advance.call(that, x, setEOT);
-                } else {
-
-                    switch (state) {
-                    case 1: // initial state: skip over flaw and intra-block words
-                        if (lane[x] == that.markerGap) {
-                            state = 2;
-                        } else {
-                            ++x;
-                        }
-                        break;
-
-                    case 2: // count 3 consecutive inter-block gap words
-                        if (lane[x] == that.markerGap) {
-                            ++gapCount;
-                            if (gapCount < that.startOfBlockWords) {
-                                ++x;
-                            } else {
-                                state = 3;
-                            }
-                        } else if (lane[x] == that.markerMagEOT) {
-                            ++x;
-                        } else {
-                            done = true;        // not an edited tape
-                            advance.call(that, x, abort);
-                        }
-                        break;
-
-                    case 3: // write the preface
-                        if (words <= 0) {       // initial-write, record: fetch preface
-                            words = fetchWord(firstWord);
-                            if (words < 0) {    // memory fetch failed
-                                done = true;
-                            } else {            // convert preface word to binary
-                                firstWord = false;
-                                words = ((words - words%0x100000000)/0x100000000)%0x100;
-                                if (words > 0) {
-                                    words = (words >>> 4)*10 + words%0x10;
-                                } else {
-                                    words = 100;        // preface == 0 => 100
-                                }
-                            }
-                        }
-
-                        if (done) {             // preface fetch failed
-                            advance.call(that, x, abort);
-                        } else if (words < that.minBlockWords && words > 1) {
-                            done = true;        // invalid block size
-                            advance.call(that, x, abort);
-                        } else {
-                            lane[x] = words;
-                            ++x;
-                            state = 4;
-                        }
-                        break;
-
-                    case 4: // write the block words
-                        if (count < words) {
-                            w = fetchWord(firstWord);
-                            if (w < 0) {
-                                done = true;        // memory fetch failed
-                                advance.call(that, x, abort);
-                            } else {
-                                firstWord = false;
-                                lane[x] = w;
-                                ++x;
-                                ++count;
-                            }
-                        } else if (count < that.minBlockWords) {
-                            lane[x] = 0;
-                            ++x;
-                            ++count;
-                        } else {
-                            count = 0;
-                            state = 5;
-                        }
-                        break;
-
-                    case 5: // write the erase gap
-                        if (count < that.endOfBlockWords) {
-                            lane[x] = that.markerEOB;
-                            ++x;
-                            ++count;
-                        } else {
-                            done = true;
-                            advance.call(that, x, finalizeBlock);
-                        }
-                        break;
-                    } // switch state
-                }
-            } while (!done);
-        }
-    }
-
-    function firstBlock() {
-        /* Called after the startTime delay to signal the control unit we are
-        ready for the first block of data */
-
-        blockReady(false, writeBlock, release);
-    }
-
-    if (this.busy || this.rewindLock) {
-        result = true;                  // unit busy or in RWL
-    } else if (!this.ready) {
-        result = true;                  // unit not ready
-    } else if (this.notWrite) {
-        result = true;                  // tape in not-write status
-    } else if (this.atEOT) {
-        result = true;                  // tape at EOT
-    } else {
-        this.busy = true;
-        this.imgWritten = true;
-        this.designatedLamp.set(1);
-        this.setAtBOT(false);
-
-        // Begin with a delay for start-up time
-        setCallback(this.mnemonic, this, this.startTime, firstBlock);
-    }
-
-    //console.log(this.mnemonic + " initialWriteBlock result=" + result.toString());
-    return result;
-};
-
-/**************************************/
-B220MagTapeDrive.prototype.positionForward = function positionForward(blockFinished, releaseControl) {
-    /* Positions tape in a forward direction. "blockFinished" is a function  to
-    be called after a block is spaced. "releaseControl" is a function to call
-    after the last block is spaced (see spaceForward and turnaround) */
-    var lane = this.image[this.laneNr]; // image data for current lane
-    var result = false;
-    var that = this;
-
-    function release() {
-        /* Releases the unit and control */
-
-        that.releaseUnit(releaseControl, false);
-    }
-
-    function finish() {
-        /* Wraps up the positioning and delays before releasing the unit and control */
-        var delay = 16 - that.startTime - that.repositionTime;
-
-        setCallback(that.mnemonic, that, delay, release);
-    }
-
-    function turnaround() {
-        /* Repositions the tape after the last block is spaced over */
-
-        that.tapeReposition(finish);
-    }
-
-    function setEOT() {
-        /* Sets EOT status after positioning to end-of-tape. Does not release
-        the control unit */
-
-        that.setAtEOT(true);
-        that.relaseUnit(null, false);
-    }
-
-    function advance (index, successor) {
-        /* Advances the tape after a block is passed, then calls the successor */
-        var len = index - that.imgIndex;    // number of words passed
-        var delay = len*that.millisPerWord; // amount of tape spin time
-
-        that.imgIndex = index;
-        that.moveTape(len*that.inchesPerWord, delay, successor);
-    }
-
-    function spaceForward(blockFinished) {
-        /* Spaces over the next block */
-        var done = false;                   // completion flag
-        var imgLength = that.imgLength;     // physical end of tape index
+B220MagTapeDrive.prototype.reposition = function reposition(driveState) {
+    /* Reverses tape direction after a forward tape operation and repositions
+    the head five words from the end of the prior block, giving room for
+    startup acceleration of the next forward operation. The "prior block" is
+    located by the first EOB (erase gap) or flaw marker word encountered when
+    moving in a backward direction. Returns a Promise that resolves when tape
+    motion is complete.
+
+    A real 220 drive repositioned tape about 60 digits (five words) from the end
+    of the data portion of the block, to allow for tape acceleration of about
+    3ms, at which point it took about 2ms (50 digits, or just over four words)
+    to reach the end of the erase gap and start of the inter-block gap for the
+    next block. this.repositionWords is sized to approximate the 3ms
+    acceleration delay */
+
+    return new Promise((resolve, reject) => {
+        var lane = this.image[this.laneNr]; // image data for current lane
         var state = 1;                      // FSA state variable
-        var w;                              // current image word
-        var x = that.imgIndex;              // lane image word index
+        var x = this.imgIndex-1;            // lane image word index (start with prior word)
 
         do {
-            if (x >= imgLength) {
-                done = true;                // just exit and leave everybody hanging...
-                that.busy = false;
-                advance.call(that, x, setEOT);
-            } else if (!that.ready) {
-                done = true;
-                that.busy = false;
-                that.releaseUnit(releaseControl, true);
+            if (x <= 0) {
+                state = 0;                  // at BOT
             } else {
-                w = lane[x];
-
                 switch (state) {
-                case 1: // initial state: skip over flaw and intra-block words
-                    if (w == that.markerGap) {
+                case 1: // initial state: skip backwards until erase-gap or BOT flaw-marker words
+                    if (lane[x] == this.markerEOB) {
                         state = 2;
+                    } else if (lane[x] == this.markerFlaw) {
+                        state = 0;
                     } else {
-                        ++x;
+                        --x;
                     }
                     break;
-
-                case 2: // skip inter-block gap words
-                    if (w != that.markerGap) {
-                        state = 3;
+                case 2: // skip backwards over erase-gap words
+                    if (lane[x] == this.markerEOB) {
+                        --x;
                     } else {
-                        ++x;
-                    }
-                    break;
-
-                case 3: // search for end of block (next inter-block gap word)
-                    if (w != that.markerGap) {
-                        ++x;
-                    } else {
-                        done = true;
-                        advance.call(that, x, function cb() {blockFinished(spaceForward, turnaround)});
+                        state = 0;
                     }
                     break;
                 } // switch state
             }
-        } while (!done);
+        } while (state);
 
-    }
+        x = this.imgIndex - x + this.repositionWords;       // words to reposition
+        if (x < this.imgIndex) {
+            this.imgIndex -= x;
+        } else {
+            x = this.imgIndex;
+            this.imgIndex = 0;
+            driveState.state = driveState.driveAtBOT;
+            this.setAtBOT(true);
+        }
 
-    if (this.busy || this.rewindLock) {
-        result = true;                  // unit busy or in RWL
-    } else if (!this.ready) {
-        result = true;                  // unit not ready
-    } else if (this.atEOT) {
-        result = true;                  // tape at EOT, leave control hung
-    } else {
-        this.busy = true;
-        this.designatedLamp.set(1);
-        this.setAtBOT(false);
-
-        // Begin with a delay for start-up time
-        setCallback(this.mnemonic, this, this.startTime, spaceForward, blockFinished);
-    }
-
-    //console.log(this.mnemonic + " positionForward   result=" + result.toString());
-    return result;
+        driveState.completionDelay -= this.turnaroundTime;
+        this.moveTape(-x*this.inchesPerWord, this.turnaroundTime, resolve, driveState);
+    });
 };
 
 /**************************************/
-B220MagTapeDrive.prototype.positionBackward = function positionBackward(blockFinished, releaseControl) {
-    /* Positions tape in a backward direction. "blockFinished" is a function to
-    be called after a block is spaced. "releaseControl" is a function to call
-    after the last block is spaced (see spaceBackward and finish) */
-    var lane = this.image[this.laneNr]; // image data for current lane
-    var result = false;
-    var that = this;
+B220MagTapeDrive.prototype.reverseDirection = function reverseDirection(driveState) {
+    /* Generates a delay to allow the drive to stop and reverse direction.
+    Returns a Promise that resolves when the delay is complete */
 
-    function release() {
-        /* Releases the unit and control */
+    return new Promise((resolve, reject) => {
+        setCallback(this.mnemonic, this, this.turnaroundTime, resolve, driveState);
+    });
+};
 
-        that.releaseUnit(releaseControl, false);
+/**************************************/
+B220MagTapeDrive.prototype.scanBlock = function scanBlock(driveState, wordIndex) {
+    /* Scans one block in a forward direction. Terminates with either the control
+    word from an EOT or control block, or the category word from any other block
+    stored in the "driveState" structure. "wordIndex" is the 1-relative index
+    of the category word to match. Returns a Promise that resolves at the end
+    of the block scan. This routine is used for MTC and MFC */
+
+    var scanForward = (resolve, reject) => {
+        /* Reads the start of the next block in the tape image in a forward
+        direction to obtain its category word */
+        var count = 0;                  // word counter within block
+        var imgLength = this.imgLength; // physical end of tape index
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var preface = 0;                // block length read from tape image
+        var sign = 0;                   // sign digit of keyword
+        var state = 1;                  // FSA state variable
+        var w = 0;                      // current image word
+        var x = this.imgIndex;          // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                state = 0;              // at EOT: just exit and leave the control hanging...
+                driveState.state = driveState.driveAtEOT;
+                this.moveTapeTo(x, driveState).then(this.boundSetEOT);
+            } else {
+                w = lane[x];
+                switch (state) {
+                case 1: // initial state: skip over flaw and intra-block words
+                    if (w == this.markerGap) {
+                        state = 2;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 2: // skip over inter-block gap and magnetic EOT words
+                    if (w == this.markerGap) {
+                        ++x;
+                    } else if (w == this.markerMagEOT) {
+                        ++x;
+                    } else if (w >= 0) {
+                        state = 3;      // found the preface
+                    } else {
+                        state = 0;      // not a formatted tape
+                        driveState.state = driveState.drivePrefaceCheck;
+                        reject(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+
+                case 3: // read the preface and check for EOT block
+                    ++x;
+                    preface = w;
+                    if (preface == 1) {
+                        state = 6;      // detected end-of-tape block
+                    } else if (preface < this.minBlockWords && preface > 1) {
+                        state = 0;      // invalid preface on tape
+                        driveState.state = driveState.drivePrefaceCheck;
+                        reject(this.moveTapeTo(x, driveState));
+                    } else {
+                        state = 4;      // normal or control block
+                    }
+                    break;
+
+                case 4: // read keyword, detect control block, do not advance beyond keyword (yet)
+                    if (w < 0) {
+                        state = 0;      // preface/block-length mismatch
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    } else {
+                        sign = (w - w%0x10000000000)/0x10000000000;
+                        if (sign == 7) {
+                            state = 6;  // detected control block
+                        } else {
+                            state = 5;  // normal block
+                        }
+                    }
+                    break;
+
+                case 5: // read the block words and capture the category word
+                    if (w < 0) {
+                        state = 0;  // preface/block-length mismatch
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    } else {
+                        ++count;
+                        ++x;
+                        if (count >= wordIndex) {
+                            driveState.keyword = w;     // return category word to the TCU
+                            state = 7;                  // finish the block
+                        }
+                    }
+                    break;
+
+                case 6: // handle an end-of-tape or control block
+                    if (w < 0) {
+                        state = 0;      // block was shorter than preface indicated
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    } else {
+                        driveState.state = driveState.driveHasControlWord;
+                        driveState.controlWord = w;     // not used by the TCU
+                        ++x;
+                        state = 7;
+                    }
+                    break;
+
+                case 7: // step through remaining words in the block until normal EOB
+                    if (w == this.markerEOB) {
+                        state = 8;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 8: // step through erase-gap words and finish normally
+                    if (w == this.markerEOB) {
+                        ++x;
+                    } else {
+                        state = 0;
+                        resolve(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+                } // switch state
+            }
+        } while (state);
     }
 
-    function finish() {
-        /* Wraps up the positioning and delays before releasing the unit and control */
-        var delay = 6 - that.startTime;
+    if (!this.ready || this.atEOT) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
+    } else {
+        return new Promise(scanForward);
+    }
+};
 
-        setCallback(that.mnemonic, that, delay, release);
+/**************************************/
+B220MagTapeDrive.prototype.searchForwardBlock = function searchForwardBlock(driveState) {
+    /* Searches one block in a forward direction. Terminates with either the
+    control word from an EOT block or the keyword from any other block stored
+    in the "driveState" structure. Returns a Promise that resolves at the end
+    of the block search. This routine is used for MTS and MFS */
+
+    var searchForward = (resolve, reject) => {
+        /* Reads the start of the next block in the tape image in a forward
+        direction to obtain its keyword, leaving the tape positioned after
+        the keyword */
+        var imgLength = this.imgLength; // physical end of tape index
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var preface = 0;                // block length read from tape image
+        var state = 1;                  // FSA state variable
+        var w = 0;                      // current image word
+        var x = this.imgIndex;          // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                state = 0;              // at EOT: just exit and leave the control hanging...
+                driveState.state = driveState.driveAtEOT;
+                this.moveTapeTo(x, driveState).then(this.boundSetEOT);
+            } else {
+                w = lane[x];
+                switch (state) {
+                case 1: // initial state: skip over flaw and intra-block words
+                    if (w == this.markerGap) {
+                        state = 2;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 2: // skip over inter-block gap and magnetic EOT words
+                    if (w == this.markerGap) {
+                        ++x;
+                    } else if (w == this.markerMagEOT) {
+                        ++x;
+                    } else if (w >= 0) {
+                        state = 3;      // found the preface
+                    } else {
+                        state = 0;      // not a formatted tape
+                        driveState.state = driveState.drivePrefaceCheck;
+                        reject(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+
+                case 3: // read the preface and check for EOT block
+                    ++x;
+                    preface = w;
+                    if (preface == 1) {
+                        state = 5;      // detected end-of-tape block
+                    } else if (preface < this.minBlockWords && preface > 1) {
+                        state = 0;      // invalid preface on tape
+                        driveState.state = driveState.drivePrefaceCheck;
+                        reject(this.moveTapeTo(x, driveState));
+                    } else {
+                        state = 4;      // normal or control block
+                    }
+                    break;
+
+                case 4: // read the keyword and finish
+                    if (w < 0) {
+                        state = 0;      // preface/block-length mismatch
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    } else {
+                        state = 0;      // finish the block
+                        driveState.keyword = w;
+                        resolve(this.moveTapeTo(x+this.repositionWords, driveState));
+                    }
+                    break;
+
+                case 5: // handle an end-of-tape block and finish
+                    if (w < 0) {
+                        state = 0;      // block was shorter than preface indicated
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    } else {
+                        state = 0;      // finish the block
+                        driveState.state = driveState.driveHasControlWord;
+                        driveState.controlWord = w;     // not used by the TCU
+                        resolve(this.moveTapeTo(x+this.repositionWords, driveState));
+                    }
+                    break;
+                } // switch
+            }
+        } while (state);
     }
 
-    function setBOT() {
-        /* Sets BOT status after positioning to beginning-of-tape. Does not
-        release the control unit */
-
-        that.setAtBOT(true);
-        that.releaseUnit(null, false);
+    if (!this.ready || this.atEOT) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
+    } else {
+        return new Promise(searchForward);
     }
+};
 
-    function advance (index, successor) {
-        /* Advances the tape after a block is passed, then calls the successor */
-        var len = index - that.imgIndex;    // number of words passed
-        var delay = len*that.millisPerWord; // amount of tape spin time
+/**************************************/
+B220MagTapeDrive.prototype.searchBackwardBlock = function searchBackwardBlock(driveState) {
+    /* Searches one block in a backward direction. Terminates with the keyword
+    from block stored in the "driveState" structure. Returns a Promise that
+    resolves at the end of the block search. This routine is used for MTS and
+    MFS */
 
-        that.imgIndex = index;
-        that.moveTape(len*that.inchesPerWord, delay, successor);
-    }
-
-    function spaceBackward(blockFinished) {
-        /* Spaces over the current or prior block until the inter-block gap */
-        var done = false;                   // completion flag
-        var state = 1;                      // FSA state variable
-        var w;                              // current image word
-        var x = that.imgIndex;              // lane image word index
+    var searchBackward = (resolve, reject) => {
+        /* Reads the start of the next block in the tape image in a backward
+        direction to obtain its keyword, leaving the tape positioned in the
+        prior block */
+        var count = 0;                  // contiguous block data word counter
+        var imgLength = this.imgLength; // physical end of tape index
+        var keyword = 0;                // keyword from block
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var preface = 0;                // block length read from tape image
+        var state = 1;                  // FSA state variable
+        var w = 0;                      // current image word
+        var x = this.imgIndex-1;        // lane image word index (start with prior word)
 
         do {
             if (x <= 0) {
-                done = true;                // just exit and leave everybody hanging...
-                that.busy = false;
-                advance.call(that, x, setBOT);
-            } else if (!that.ready) {
-                done = true;
-                that.busy = false;
-                that.releaseUnit(releaseControl, true);
+                state = 0;                      // at BOT: just exit and leave the control hanging...
+                driveState.state = driveState.driveAtBOT;
+                this.moveTapeTo(x, driveState).then(this.boundSetBOT);
             } else {
                 w = lane[x];
-
                 switch (state) {
-                case 1: // initial state: skip over flaw words
-                    if (w == that.markerGap) {
+                case 1: // initial state: skip over flaw and magnetic EOT words
+                    if (w == this.markerGap) {
                         state = 2;
-                    } else if (w == that.markerFlaw) {
+                    } else if (w == this.markerFlaw) {
+                        --x;
+                    } else if (w == this.markerMagEOT) {
                         --x;
                     } else {
                         state = 3;
@@ -2211,125 +1564,763 @@ B220MagTapeDrive.prototype.positionBackward = function positionBackward(blockFin
                     break;
 
                 case 2: // skip initial inter-block gap words
-                    if (w != that.markerGap) {
-                        state = 3;
-                    } else {
+                    if (w == this.markerGap) {
                         --x;
+                    } else {
+                        state = 3;
                     }
                     break;
 
                 case 3: // search for start of block (first prior inter-block gap word)
-                    if (w != that.markerGap) {
+                    if (w == this.markerGap) {
+                        state = 4;
+                    } else if (w < 0) {
+                        count = 0;
                         --x;
                     } else {
-                        state = 4;
+                        keyword = preface;      // remember the last two words we've seen
+                        preface = w;
+                        --x;
+                        ++count;
                     }
                     break;
 
                 case 4: // skip this block's inter-block gap words
-                    if (w != that.markerGap) {
-                        done = true;
-                        x -= that.repositionWords;      // position into end of prior block, as usual
-                        advance.call(that, x, function cb() {blockFinished(spaceBackward, finish)});
-                    } else {
+                    if (w == this.markerGap) {
                         --x;
+                    } else {
+                        state = 5;
+                    }
+                    break;
+
+                case 5: // skip the prior block's erase-gap words, store the keyword, and then quit
+                    if (w == this.markerEOB) {
+                        --x;
+                    } else if (count < 2) {
+                        state = 1;      // saw less than 2 block data words, start over
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-this.repositionWords, driveState));
+                    } else {
+                        state = 0;      // position into end of prior block, as usual
+                        driveState.keyword = keyword;
+                        resolve(this.moveTapeTo(x-this.repositionWords, driveState));
                     }
                     break;
                 } // switch state
             }
-        } while (!done);
+        } while (state);
     }
 
-    if (this.busy || this.rewindLock) {
-        result = true;                  // unit busy or in RWL
-    } else if (!this.ready) {
-        result = true;                  // unit not ready
-    } else if (this.atBOT) {
-        result = true;                  // tape at BOT, leave control hung
+    if (!this.ready || this.atBOT) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
     } else {
-        this.busy = true;
-        this.designatedLamp.set(1);
-        this.setAtEOT(false);
-
-        // Begin with a delay for start-up time
-        setCallback(this.mnemonic, this, this.startTime, spaceBackward, blockFinished);
+        return new Promise(searchBackward);
     }
-
-    //console.log(this.mnemonic + " positionBackward  result=" + result.toString());
-    return result;
 };
 
 /**************************************/
-B220MagTapeDrive.prototype.positionAtEnd = function positionAtEnd(releaseControl) {
-    /* Positions the tape to the end of recorded information (i.e., gap longer
-    and inter-block gap */
-    var lane = this.image[this.laneNr]; // image data for current lane
-    var result = false;
-    var that = this;
+B220MagTapeDrive.prototype.readNextBlock = function readNextBlock(driveState, record, controlEnabled, storeWord) {
+    /* Reads one block on tape. "record" is true for MRR. "controlEnabled" is true
+    if control blocks are to be recognized. "storeWord" is a call-back to store
+    the next word into the Processor's memory. This routine is used for both MRD
+    and MRR, as block lengths are determined by the tape control unit. Returns
+    a Promise that resolves when the read completes */
 
-    function release() {
-        /* Releases the unit and control */
-
-        that.releaseUnit(releaseControl, false);
-    }
-
-    function finish() {
-        /* Wraps up the positioning and delays before releasing the unit and control */
-        var delay = 23 - that.startTime - that.repositionTime;
-
-        setCallback(that.mnemonic, that, delay, release);
-    }
-
-    function turnaround() {
-        /* Repositions the tape after finding end-of-info */
-
-        that.tapeReposition(finish);
-    }
-
-    function setEOT() {
-        /* Sets EOT status after positioning to end-of-tape. Does not release
-        the control unit */
-
-        that.setAtEOT(true);
-        that.relaseUnit(null, false);
-    }
-
-    function advance (index, successor) {
-        /* Advances the tape after a block is passed, then calls the successor */
-        var len = index - that.imgIndex;    // number of words passed
-        var delay = len*that.millisPerWord; // amount of tape spin time
-
-        that.imgIndex = index;
-        that.moveTape(len*that.inchesPerWord, delay, successor);
-    }
-
-    function spaceForward() {
-        /* Spaces over the next block, unless a long gap or physical EOT is
-        encountered */
-        var done = false;                   // completion flag
-        var gapCount = 0;                   // number of consecutive erase-gap words
-        var imgLength = that.imgLength;     // physical end of tape index
-        var state = 1;                      // FSA state variable
-        var w;                              // current image word
-        var x = that.imgIndex;              // lane image word index
+    var readBlock = (resolve, reject) => {
+        /* Reads the next block in the tape image */
+        var controlBlock = false;       // true if control block detected
+        var count = 0;                  // word counter within block
+        var firstWord = false;          // flag for initial memory fetch
+        var imgLength = this.imgLength; // physical end of tape index
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var preface = 0;                // block length read from tape image
+        var sign = 0;                   // sign digit of keyword
+        var state = 1;                  // FSA state variable
+        var w = 0;                      // current image word
+        var x = this.imgIndex;          // lane image word index
 
         do {
             if (x >= imgLength) {
-                done = true;                // just exit and leave everybody hanging...
-                that.busy = false;
-                advance.call(that, x, setEOT);
-            } else if (!that.ready) {
-                done = true;
-                that.busy = false;
-                that.releaseUnit(releaseControl, true);
+                state = 0;              // at EOT: just exit and leave the control hanging...
+                driveState.state = driveState.driveAtEOT;
+                this.moveTapeTo(x, driveState).then(this.boundSetEOT);
             } else {
                 w = lane[x];
+                switch (state) {
+                case 1: // initial state: skip over flaw and intra-block words
+                    if (w == this.markerGap) {
+                        state = 2;
+                    } else {
+                        ++x;
+                    }
+                    break;
 
+                case 2: // skip over inter-block gap and magnetic EOT words
+                    if (w == this.markerGap) {
+                        ++x;
+                    } else if (w == this.markerMagEOT) {
+                        ++x;
+                    } else if (w >= 0) {
+                        state = 3;      // found the preface
+                    } else {
+                        state = 0;      // not a formatted tape
+                        driveState.state = driveState.drivePrefaceCheck;
+                        reject(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+
+                case 3: // read the preface and check for EOT block
+                    ++x;
+                    preface = w;
+                    if (preface == 1) {
+                        state = 6;      // detected end-of-tape block
+                    } else if (preface < this.minBlockWords && preface > 1) {
+                        state = 0;      // invalid preface on tape
+                        driveState.state = driveState.drivePrefaceCheck;
+                        reject(this.moveTapeTo(x, driveState));
+                    } else {
+                        state = 4;      // normal or control block
+                    }
+                    break;
+
+                case 4: // read the keyword, detect control block, store the preface if necessary, store keyword
+                    if (w < 0) {
+                        state = 0;      // preface/block-length mismatch
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    } else {
+                        ++x;
+                        if (controlEnabled) {
+                            sign = (w - w%0x10000000000)/0x10000000000;
+                            if (sign == 7) {
+                                controlBlock = true;
+                                // strip sign digit from keyword (not entirely sure this should be done)
+                                w %= 0x10000000000;
+                            }
+                        }
+
+                        if (record) {   // store the preface word (with keyword sign if a control block)
+                            sign = ((sign*0x10 + (preface%100 - preface%10)/10)*0x10 + preface%10)*0x100000000;
+                            if (storeWord(firstWord, sign) < 0) {
+                                state = 10; // memory error storing preface word
+                                driveState.state = driveState.driveMemoryError;
+                            } else {
+                                firstWord = false;
+                            }
+                        }
+
+                        if (state == 4) {  // no error detected yet
+                            if (controlBlock) {
+                                --preface; // decrement block length to prevent storing the control word
+                            }
+
+                            if (storeWord(firstWord, w) < 0) {
+                                state = 10; // memory error storing keyword from block
+                                driveState.state = driveState.driveMemoryError;
+                            } else {
+                                firstWord = false;
+                                ++count;
+                                state = 5;
+                            }
+                        }
+                    }
+                    break;
+
+                case 5: // read and store the remaining block words
+                    if (count < preface) {
+                        if (w < 0) {
+                            state = 0;  // preface/block-length mismatch
+                            driveState.state = driveState.driveReadCheck;
+                            reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                        } else {
+                            if (storeWord(firstWord, w) < 0) {
+                                state = 10; // memory error storing data word from block
+                                driveState.state = driveState.driveMemoryError;
+                            } else {
+                                firstWord = false;
+                                ++x;
+                                ++count;
+                            }
+                        }
+                    } else if (controlBlock) {
+                        if (w < 0) {
+                            state = 0;  // preface/block-length mismatch
+                            driveState.state = driveState.driveReadCheck;
+                            reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                        } else {
+                            driveState.state = driveState.driveHasControlWord;
+                            driveState.controlWord = w;
+                            ++x;
+                            state = 7;      // deal with the control word after EOB
+                        }
+                    } else {
+                        state = 7;          // check for proper EOB
+                    }
+                    break;
+
+                case 6: // capture the control word for an end-of-tape block
+                    if (w < 0) {
+                        state = 0;      // block was shorter than preface indicated
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    } else {
+                        driveState.state = driveState.driveHasControlWord;
+                        driveState.controlWord = w;
+                        ++x;
+                        state = 8;
+                    }
+                    break;
+
+                case 7: // check for proper end-of-block
+                    if (w == this.markerEOB) {
+                        state = 9;
+                    } else {
+                        state = 0;      // block was longer than preface indicated
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    }
+                    break;
+
+                case 8: // step through remaining words in the block until normal EOB
+                    if (w == this.markerEOB) {
+                        state = 9;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 9: // step through erase-gap words and finish normally
+                    if (w == this.markerEOB) {
+                        ++x;
+                    } else {
+                        state = 0;
+                        resolve(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+
+                case 10: // step through remaining words in the block until EOB for error
+                    if (w == this.markerEOB) {
+                        state = 11;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 11: // step through erase-gap words and finish with error
+                    if (w == this.markerEOB) {
+                        ++x;
+                    } else {
+                        state = 0;
+                        reject(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+                } // switch state
+            }
+        } while (state);
+    }
+
+    if (!this.ready || this.atEOT) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
+    } else {
+        return new Promise(readBlock);
+    }
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.overwriteBlock = function overwriteBlock(driveState, record, words, fetchWord) {
+    /* Overwrites one block on tape. "record" is true for MOR. "words" is the
+    number of words to write in the block. "fetchWord" is a call-back to
+    retrieve the next word from the Processor's memory. This routine is used
+    for both MOW and MOR, as block lengths are determined by the tape
+    control unit. Returns a Promise that resolves when the write completes */
+
+    var writeBlock = (resolve, reject) => {
+        /* Overwrites the next block in the tape image */
+        var count = 0;                  // word counter within block
+        var firstWord = !record;        // flag for initial memory fetch
+        var imgLength = this.imgLength; // physical end of tape index
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var state = 1;                  // FSA state variable
+        var w = 0;                      // current image word
+        var x = this.imgIndex;          // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                state = 0;              // at EOT: just exit and leave the control hanging...
+                driveState.state = driveState.driveAtEOT;
+                this.moveTapeTo(x, driveState).then(this.boundSetEOT);
+            } else {
+                w = lane[x];
+                switch (state) {
+                case 1: // initial state: skip over flaw and intra-block words
+                    if (w == this.markerGap) {
+                        state = 2;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 2: // skip over inter-block gap and magnetic EOT words
+                    if (w == this.markerGap) {
+                        ++x;
+                    } else if (w == this.markerMagEOT) {
+                        ++x;
+                    } else if (w >= 0) {
+                        state = 3;      // found the preface
+                    } else {
+                        state = 0;      // not a formatted tape
+                        driveState.state = driveState.drivePrefaceCheck;
+                        reject(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+
+                case 3: // check the preface
+                    ++x;
+                    if (w < this.minBlockWords && w > 1) {
+                        state = 0;      // invalid preface on tape
+                        driveState.state = driveState.drivePrefaceCheck;
+                        reject(this.moveTapeTo(x, driveState));
+                    } else if (w == words) {
+                        state = 4;      // preface match: overwrite the block
+                    } else if (w == 1) {
+                        state = 5;
+                    } else {
+                        state = 0;      // other preface mismatch
+                        driveState.state = driveState.drivePrefaceMismatch;
+                        reject(this.moveTapeTo(x, driveState).then(this.boundReposition));
+                    }
+                    break;
+
+                case 4: // overwrite the block words
+                    if (count < words) {
+                        if (w < 0) {
+                            state = 0;  // block was shorter than preface indicated
+                            driveState.state = driveState.driveReadCheck;
+                            reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                        } else {
+                            w = fetchWord(firstWord);
+                            if (w < 0) {
+                                state = 8; // memory error: gobble rest of block
+                                driveState.state = driveState.driveMemoryError;
+                            } else {
+                                firstWord = false;
+                                lane[x] = w;
+                                ++x;
+                                ++count;
+                            }
+                        }
+                    } else if (count < this.minBlockWords) {
+                        if (w < 0) {
+                            state = 0;  // block was shorter than preface indicated
+                            driveState.state = driveState.driveReadCheck;
+                            reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                        } else {
+                            lane[x] = 0; // pad out an EOT block
+                            ++x;
+                            ++count;
+                        }
+                    } else if (w == this.markerEOB) {
+                        state = 7;      // normal block termination
+                    } else {
+                        state = 0;      // block was longer than preface indicated
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    }
+                    break;
+
+                case 5: // capture control word for EOT block
+                    if (w < 0) {
+                        state = 0;      // block was shorter than preface indicated
+                        driveState.state = driveState.driveReadCheck;
+                        reject(this.moveTapeTo(x-1, driveState).then(this.boundReposition));
+                    } else {
+                        driveState.state = driveState.driveHasControlWord;
+                        driveState.controlWord = w;
+                        ++x;
+                        state = 6;      // gobble the rest of the block and finish normally
+                    }
+                    break;
+
+                case 6: // step through remaining words in the block until normal EOB
+                    if (w == this.markerEOB) {
+                        state = 7;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 7: // step through erase-gap words and finish normally
+                    if (w == this.markerEOB) {
+                        ++x;
+                    } else {
+                        state = 0;
+                        resolve(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+
+                case 8: // step through remaining words in the block until EOB for error
+                    if (w == this.markerEOB) {
+                        state = 9;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 9: // step through erase-gap words and finish with error
+                    if (w == this.markerEOB) {
+                        ++x;
+                    } else {
+                        state = 0;
+                        reject(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+                } // switch state
+            }
+        } while (state);
+    }
+
+    if (!this.ready || this.atEOT || this.notWrite) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
+    } else {
+        this.imgWritten = true;         // mark the tape image as modified
+        return new Promise(writeBlock);
+    }
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.initialWriteFinalize = function initialWriteFinalize(driveState) {
+    /* Write a longer inter-block gap so that MPE will work at magnetic
+    end-of-tape and check for end-of-tape prior to terminating the I/O */
+
+    return new Promise((resolve, reject) => {
+        var count = this.startOfBlockWords*2; // number of gap words
+        var done = false;               // loop control
+        var imgLength = this.imgLength; // physical end of tape index
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var x = this.imgIndex;          // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                done = true;            // at end-of-tape
+                resolve(this.moveTapeTo(x, driveState).then(this.boundSetEOT));
+            } else if (count > 0) {
+                --count;                // write extra inter-block gap word
+                lane[x] = this.markerGap;
+                ++x;
+            } else if (lane[x] == this.markerMagEOT) {
+                ++x;                    // space over magnetic EOT words
+            } else {
+                done = true;            // finish write
+                resolve(this.moveTapeTo(x, driveState));
+            }
+        } while(!done);
+    });
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.initialWriteBlock = function initialWriteBlock(driveState, record, words, fetchWord) {
+    /* Initial-writes one block on edited (blank) tape. "record" is true for MIR.
+    "words" is the number of words to write in the block. "fetchWord" is a call-
+    back to retrieve the next word from the Processor's memory. This routine is
+    used for both MIW and MIR, as block lengths are determined by the tape
+    control unit. Returns a Promise that resolves when the write completes */
+
+    var writeBlock = (resolve, reject) => {
+        /* Initial-writes the next block in the tape image */
+        var count = 0;                  // word counter within block
+        var firstWord = !record;        // flag for initial memory fetch
+        var gapCount = 0;               // count of consecutive inter-block gap words
+        var imgLength = this.imgLength; // physical end of tape index
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var state = 1;                  // FSA state variable
+        var w = 0;                      // memory word value
+        var x = this.imgIndex;          // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                state = 0;              // at EOT: just exit and leave the control hanging...
+                driveState.state = driveState.driveAtEOT;
+                this.moveTapeTo(x, driveState).then(this.boundSetEOT);
+            } else {
+                switch (state) {
+                case 1: // initial state: skip over flaw and intra-block words
+                    if (lane[x] == this.markerGap) {
+                        state = 2;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 2: // count 3 consecutive inter-block gap words
+                    if (lane[x] == this.markerGap) {
+                        ++gapCount;
+                        if (gapCount < this.startOfBlockWords) {
+                            ++x;
+                        } else {
+                            state = 3;
+                        }
+                    } else if (lane[x] == this.markerMagEOT) {
+                        ++x;
+                    } else {
+                        state = 0;      // not an edited tape
+                        driveState.state = driveState.driveNotEditedTape;
+                        reject(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+
+                case 3: // write the preface
+                    lane[x] = words;
+                    ++x;
+                    state = 4;
+                    break;
+
+                case 4: // write the block words
+                    if (count < words) {
+                        w = fetchWord(firstWord);
+                        if (w < 0) {
+                            state = 0;  // memory fetch failed
+                            driveState.state = driveState.driveMemoryError;
+                            state = 5;
+                        } else {
+                            firstWord = false;
+                            lane[x] = w;
+                            ++x;
+                            ++count;
+                        }
+                    } else {
+                        state = 6;
+                    }
+                    break;
+
+                case 5: // pad out the block after a memory error
+                    if (count < words) {
+                        lane[x] = 0;
+                        ++x;
+                        ++count;
+                    } else {
+                        state = 6;
+                    }
+                    break;
+
+                case 6: // pad out to the minimum block length (e.g., for an EOT block)
+                    if (count < this.minBlockWords) {
+                        lane[x] = 0;
+                        ++x;
+                        ++count;
+                    } else {
+                        count = 0;
+                        state = 7;
+                    }
+                    break;
+
+                case 7: // write the erase gap
+                    if (count < this.endOfBlockWords) {
+                        lane[x] = this.markerEOB;
+                        ++x;
+                        ++count;
+                    } else {
+                        state = 0;      // finished with the block
+                        this.imgTopWordNr = x;
+                        resolve(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+                } // switch state
+            }
+        } while (state);
+    };
+
+    if (!this.ready || this.atEOT || this.notWrite) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
+    } else {
+        this.imgWritten = true;         // mark the tape image as modified
+        return new Promise(writeBlock);
+    }
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.spaceForwardBlock = function spaceForwardBlock(driveState) {
+    /* Positions tape one block in a forward direction. Leaves the block
+    positioned at the preface word, ready to space the next block or reposition
+    into the prior block at the end of the operation. Returns a Promise that
+    resolves after the block is spaced */
+
+    var spaceBlock = (resolve, reject) => {
+        /* Spaces forward over the next block. Blocks are counted as their
+        preface words are passed. This routine does this by detecting the
+        transition between the preface and its immediately preceding gap word */
+        var imgLength = this.imgLength;     // physical end of tape index
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var state = 1;                      // FSA state variable
+        var w = 0;                          // current image word
+        var x = this.imgIndex;              // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                state = 0;                      // at EOT: just exit and leave the control hanging...
+                driveState.state = driveState.driveAtEOT;
+                this.moveTapeTo(x, driveState).then(this.boundSetEOT);
+            } else {
+                w = lane[x];
+                switch (state) {
+                case 1: // initial state: skip over flaw and intra-block words
+                    if (w == this.markerGap) {
+                        state = 2;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 2: // skip inter-block gap words
+                    if (w == this.markerGap) {
+                        ++x;
+                    } else {
+                        state = 3;
+                    }
+                    break;
+
+                case 3: // found preface: search for end of block (next erase-gap word)
+                    if (w == this.markerEOB) {
+                        state = 4;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 4: // step through erase-gap words and finish
+                    if (w == this.markerEOB) {
+                        ++x;
+                    } else {
+                        state = 0;
+                        resolve(this.moveTapeTo(x, driveState));
+                    }
+                    break;
+                } // switch state
+            }
+        } while (state);
+
+    }
+
+    if (!this.ready || this.atEOT) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
+    } else {
+        return new Promise(spaceBlock);
+    }
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.spaceBackwardBlock = function spaceBackwardBlock(driveState) {
+    /* Positions tape one block in a backward direction. Leaves the block
+    positioned five words into the end of the prior block, as for a normal
+    reposition after a forward operation. Returns a Promise that resolves
+    after the block is spaced */
+
+    var spaceBlock = (resolve, reject) => {
+        /* Spaces backward over the current or prior block. Blocks are counted
+        as their preface words are passed. This routine does that by detecting
+        the transition between the preface and its immediately preceding gap word */
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var state = 1;                      // FSA state variable
+        var w = 0;                          // current image word
+        var x = this.imgIndex-1;            // lane image word index (start with prior word)
+
+        do {
+            if (x <= 0) {
+                state = 0;                      // at BOT: just exit and leave the control hanging...
+                driveState.state = driveState.driveAtBOT;
+                this.moveTapeTo(x, driveState).then(this.boundSetBOT);
+            } else {
+                w = lane[x];
+                switch (state) {
+                case 1: // initial state: skip over flaw and magnetic EOT words
+                    if (w == this.markerGap) {
+                        state = 2;
+                    } else if (w == this.markerFlaw) {
+                        --x;
+                    } else if (w == this.markerMagEOT) {
+                        --x;
+                    } else {
+                        state = 3;
+                    }
+                    break;
+
+                case 2: // skip initial inter-block gap words
+                    if (w == this.markerGap) {
+                        --x;
+                    } else {
+                        state = 3;
+                    }
+                    break;
+
+                case 3: // search for start of block (first prior inter-block gap word)
+                    if (w == this.markerGap) {
+                        state = 4;
+                    } else {
+                        --x;
+                    }
+                    break;
+
+                case 4: // skip this block's inter-block gap words
+                    if (w == this.markerGap) {
+                        --x;
+                    } else {
+                        state = 5;
+                    }
+                    break;
+
+                case 5: // skip the prior block's erase-gap words
+                    if (w == this.markerEOB) {
+                        --x;
+                    } else {            // position into end of prior block, as usual
+                        state = 0;
+                        resolve(this.moveTapeTo(x-this.repositionWords, driveState));
+                    }
+                    break;
+                } // switch state
+            }
+        } while (state);
+    }
+
+    if (!this.ready || this.atBOT) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
+    } else {
+        return new Promise(spaceBlock);
+    }
+};
+
+/**************************************/
+B220MagTapeDrive.prototype.spaceEOIBlock = function spaceEOIBlock(driveState) {
+    /* Spaces forward one block on tape, detecting end-of-information if encountered
+    (i.e., gap longer and inter-block gap. Returns a Promise that resolves
+    after the block is spaced or EOI is encountered */
+
+    var spaceBlock = (resolve, reject) => {
+        /* Spaces over the next block, unless a long gap or physical EOT is
+        encountered */
+        var gapCount = 0;                   // number of consecutive erase-gap words
+        var imgLength = this.imgLength;     // physical end of tape index
+        var lane = this.image[this.laneNr]; // image data for current lane
+        var state = 1;                      // FSA state variable
+        var w = 0;                          // current image word
+        var x = this.imgIndex;              // lane image word index
+
+        do {
+            if (x >= imgLength) {
+                state = 0;                  // at EOT: just exit and leave the control hanging...
+                driveState.state = driveState.driveAtEOT;
+                this.moveTapeTo(x, driveState).then(this.boundSetEOT);
+            } else {
+                w = lane[x];
                 switch (state) {
                 case 1: // initial state: skip over flaw words
-                    if (w == that.markerGap) {
+                    if (w == this.markerGap) {
                         state = 2;
-                    } else if (w == that.markerFlaw) {
+                    } else if (w == this.markerFlaw) {
                         ++x;
                     } else {
                         state = 3;
@@ -2337,98 +2328,82 @@ B220MagTapeDrive.prototype.positionAtEnd = function positionAtEnd(releaseControl
                     break;
 
                 case 2: // count inter-block gap words
-                    if (w != that.markerGap) {
+                    if (w != this.markerGap) {
                         state = 3;
+                    } else if (gapCount > this.startOfBlockWords) {
+                        state = 0;
+                        driveState.state = driveState.driveAtEOI;
+                        resolve(this.moveTapeTo(x, driveState));
                     } else {
-                        if (gapCount > that.startOfBlockWords) {
-                            done = true;
-                            advance.call(that, x, turnaround);
-                        } else {
-                            ++x;
-                            ++gapCount;
-                        }
+                        ++x;
+                        ++gapCount;
                     }
                     break;
 
-                case 3: // search for end of block (next inter-block gap word)
-                    if (w != that.markerGap) {
+                case 3: // search for end of block (next erase-gap word)
+                    if (w == this.markerEOB) {
+                        state = 4;
+                    } else {
+                        ++x;
+                    }
+                    break;
+
+                case 4: // step through erase-gap words and finish
+                    if (w == this.markerEOB) {
                         ++x;
                     } else {
-                        done = true;
-                        advance.call(that, x, spaceForward);
+                        state = 0;
+                        resolve(this.moveTapeTo(x, driveState));
                     }
                     break;
                 } // switch state
             }
-        } while (!done);
+        } while (state);
+    };
 
-    }
-
-    if (this.busy || this.rewindLock) {
-        result = true;                  // unit busy or in RWL
-    } else if (!this.ready) {
-        result = true;                  // unit not ready
-    } else if (this.atEOT) {
-        result = true;                  // tape at EOT, leave control hung
+    if (!this.ready || this.atEOT) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
     } else {
-        this.busy = true;
-        this.designatedLamp.set(1);
-        this.setAtBOT(false);
-
-        // Begin with a delay for start-up time
-        setCallback(this.mnemonic, this, this.startTime, spaceForward);
+        return new Promise(spaceBlock);
     }
-
-    //console.log(this.mnemonic + " positionAtEnd:    result=" + result.toString());
-    return result;
 };
 
 /**************************************/
-B220MagTapeDrive.prototype.laneSelect = function laneSelect(laneNr, releaseControl) {
+B220MagTapeDrive.prototype.laneSelect = function laneSelect(driveState, laneNr) {
     /* Selects the tape lane on the unit. If the drive is busy or not ready,
     returns true */
-    delay = 3;                          // ms for a no-lane change
-    var lane = laneNr%2;
-    var result = false;
 
-    if (this.busy || this.rewindLock) {
-        result = true;                  // unit busy or in RWL
-    } else if (!this.ready) {
-        result = true;                  // unit not ready
+    if (this.busy) {
+        driveState.state = driveState.driveBusy;
+        return Promise.reject(driveState);
+    } else if (!this.ready || this.rewindLock || this.atEOT) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
     } else {
         this.busy = true;
         this.designatedLamp.set(1);
-        if (this.laneNr != lane) {
-            delay += 70;                // additional time (ms) if the lane changes
-            this.laneNr = laneNr%2;
-        }
-
-        setCallback(this.mnemonic, this, delay, this.releaseUnit, releaseControl);
+        return this.setLane(laneNr, driveState);
     }
-
-    //console.log(this.mnemonic + " lane-select:      lane=" + laneNr + ", result=" + result.toString());
-    return result;
 };
 
 /**************************************/
-B220MagTapeDrive.prototype.rewind = function rewind(laneNr, lockout) {
-    /* Initiates a rewind operation on the unit. If the drive is busy or not ready,
-    returns true.  Otherwise, makes the drive not-ready, delays for an
-    appropriate amount of time depending on how far up-tape we are, then readies the
-    unit again */
-    var result = false;
+B220MagTapeDrive.prototype.rewind = function rewind(driveState, laneNr, lockout) {
+    /* Initiates a rewind operation on the unit. Makes the drive not-ready,
+    delays for an appropriate amount of time depending on how far up-tape we
+    are, then readies the unit again. Returns a Promise that resolves once the
+    rewind completes */
 
-    if (this.busy || this.rewindLock) {
-        result = true;                  // unit busy or in RWL
-    } else if (!this.ready) {
-        result = true;                  // unit not ready
+    if (this.busy) {
+        driveState.state = driveState.driveBusy;
+        return Promise.reject(driveState);
+    } else if (!this.ready || this.rewindLock) {
+        driveState.state = driveState.driveNotReady;
+        return Promise.reject(driveState);
     } else {
         this.designatedLamp.set(1);
-        this.tapeRewind(laneNr, lockout);
+        return this.tapeRewind(laneNr, lockout);
     }
-
-    //console.log(this.mnemonic + " rewind:           lane=" + laneNr + ", lockout=" + lockout + ", result=" + result.toString());
-    return result;
 };
 
 /**************************************/
diff --git a/webUI/B220MagTapeLoadPanel.html b/webUI/B220MagTapeLoadPanel.html
index 7fba338..9bfbe2a 100644
--- a/webUI/B220MagTapeLoadPanel.html
+++ b/webUI/B220MagTapeLoadPanel.html
@@ -49,7 +49,7 @@