/***********************************************************************
* retro-b5500/emulator B5500IOUnit.js
************************************************************************
* Copyright (c) 2012, Nigel Williams and Paul Kimpel.
* Licensed under the MIT License, see
*       http://www.opensource.org/licenses/mit-license.php
************************************************************************
* B5500 Input/Output Unit module.
*
* Instance variables in all caps generally refer to register or flip-flop (FF)
* entities in the processor hardware. See the Burroughs B5500 Reference Manual
* (1021326, May 1967) and B5283 Input/Output Training Manual (1036993, January 1969)
* for details:
* http://bitsavers.org/pdf/burroughs/B5000_5500_5700/1021326_B5500_RefMan_May67.pdf
* http://bitsavers.org/pdf/burroughs/B5000_5500_5700/1036993_B5283_TrainingMan_Jan69.pdf
*
* B5500 word format: 48 bits plus (hidden) parity.
*   Bit 0 is high-order, bit 47 is low-order, big-endian character ordering.
*   I/O and Result Descriptors have the following general format:
*       [0:1]   Flag bit (1=descriptor)
*       [1:1]   (0=descriptor)
*       [2:1]   Presence bit (1=present and available in memory, 0=absent or unavailable)
*       [3:5]   Unit designate
*       [8:10]  Word count (optional, see [23:1])
*       [18:1]  Memory inhibit bit (1=no data transfer)
*       [19:2]  (not used by I/O Unit)
*       [21:1]  Mode bit (0=alpha, 1=binary)
*       [22:1]  Direction bit (0=forward, 1=reverse for mag tape, 120/132 col for printers)
*       [23:1]  Word count bit (0=ignore, 1=use word count in [8:10])
*       [24:1]  I/O bit (0=write, 1=read)
*       [25:1]  (not used by I/O Unit)
*       [26:7]  Control and error-reporting bits (depend on unit)
*       [33:15] Memory address
*
************************************************************************
* 2012-12-08  P.Kimpel
*   Original version, from thin air.
***********************************************************************/
"use strict";

/**************************************/
function B5500IOUnit(ioUnitID, cc) {
    /* Constructor for the I/O Unit object */

    this.ioUnitID = ioUnitID;           // I/O Unit ID ("1", "2", "3", or "4")
    this.cc = cc;                       // Reference back to Central Control module

    this.forkHandle = null;             // Reference to current setTimeout id
    this.accessor = {                   // Memory access control block
        requestorID: ioUnitID,             // Memory requestor ID
        addr: 0,                           // Memory address
        word: 0,                           // 48-bit data word
        MAIL: 0,                           // Truthy if attempt to access @000-@777 in normal state
        MPED: 0,                           // Truthy if memory parity error
        MAED: 0                            // Truthy if memory address/inhibit error
    };

    // Establish a buffer for the peripheral modules to use during their I/O.
    // The size is sufficient for 63 disk sectors, rounded up to the next 8KB.
    this.bufferArea = new ArrayBuffer(16384);
    this.buffer = new Uint8Array(this.bufferArea);

    // Establish contexts for asynchronously-called methods
    this.boundForkIO = B5500CentralControl.bindMethod(this.forkIO, this);
    this.boundFinishGeneric = this.makeFinish(this.finishGeneric);
    this.boundFinishGenericRead = this.makeFinish(this.finishGenericRead);
    this.boundFinishBusy = this.makeFinish(this.finishBusy);
    this.boundFinishDiskRead = this.makeFinish(this.finishDiskRead);
    this.boundFinishSPORead = this.makeFinish(this.finishSPORead);

    this.clear();                       // Create and initialize the processor state
}

/**************************************/

B5500IOUnit.BICtoANSI = [               // Index by 6-bit BIC to get 8-bit ANSI code
        0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,        // 00-07, @00-07
        0x38,0x39,0x23,0x40,0x3F,0x3A,0x3E,0x7D,        // 08-1F, @10-17
        0x2B,0x41,0x42,0x43,0x44,0x45,0x46,0x47,        // 10-17, @20-27
        0x48,0x49,0x2E,0x5B,0x26,0x28,0x3C,0x7E,        // 18-1F, @30-37
        0x7C,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0x50,        // 20-27, @40-47
        0x51,0x52,0x24,0x2A,0x2D,0x29,0x3B,0x7B,        // 28-2F, @50-57
        0x20,0x2F,0x53,0x54,0x55,0x56,0x57,0x58,        // 30-37, @60-67
        0x59,0x5A,0x2C,0x25,0x21,0x3D,0x5D,0x22];       // 38-3F, @70-77

B5500IOUnit.BICtoBCLANSI = [            // Index by 6-bit BIC to get 8-bit BCL-as-ANSI code
        0x23,0x31,0x32,0x33,0x34,0x35,0x36,0x37,        // 00-07, @00-07
        0x38,0x39,0x40,0x3F,0x30,0x3A,0x3E,0x7D,        // 08-1F, @10-17
        0x2C,0x2F,0x53,0x54,0x55,0x56,0x57,0x58,        // 10-17, @20-27
        0x59,0x5A,0x25,0x21,0x20,0x3D,0x5D,0x22,        // 18-1F, @30-37
        0x24,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0x50,        // 20-27, @40-47
        0x51,0x52,0x2A,0x2D,0x7C,0x29,0x3B,0x7B,        // 28-2F, @50-57
        0x2B,0x41,0x42,0x43,0x44,0x45,0x46,0x47,        // 30-37, @60-67
        0x48,0x49,0x5B,0x26,0x2E,0x28,0x3C,0x7E];       // 38-3F, @70-77

B5500IOUnit.ANSItoBIC = [               // Index by 8-bit ANSI to get 6-bit BIC (upcased, invalid=>"?")
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // 00-0F
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // 10-1F
        0x30,0x3C,0x3F,0x0A,0x2A,0x3B,0x1C,0x0C,0x1D,0x2D,0x2B,0x10,0x3A,0x2C,0x1A,0x31,  // 20-2F
        0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0D,0x2E,0x1E,0x3D,0x0E,0x0C,  // 30-3F
        0x0B,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x21,0x22,0x23,0x24,0x25,0x26,  // 40-4F
        0x27,0x28,0x29,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x1B,0x0C,0x3E,0x0C,0x0C,  // 50-5F
        0x0C,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x21,0x22,0x23,0x24,0x25,0x26,  // 60-6F
        0x27,0x28,0x29,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x2F,0x20,0x0F,0x1F,0x0C,  // 70-7F
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // 80-8F
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // 90-9F
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // A0-AF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // B0-BF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // C0-CF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // D0-DF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // E0-EF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C]; // F0-FF

B5500IOUnit.BCLANSItoBIC = [            // Index by 8-bit BCL-as-ANSI to get 6-bit BIC (upcased, invalid=>"?")
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // 00-0F
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // 10-1F
        0x1C,0x1B,0x1F,0x00,0x20,0x1A,0x3B,0x0C,0x3D,0x2D,0x2A,0x30,0x10,0x2B,0x3C,0x11,  // 20-2F
        0x0C,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0D,0x2E,0x3E,0x1D,0x0E,0x0B,  // 30-3F
        0x0A,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x21,0x22,0x23,0x24,0x25,0x26,  // 40-4F
        0x27,0x28,0x29,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x3A,0x0C,0x1E,0x0C,0x0C,  // 50-5F
        0x0C,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x21,0x22,0x23,0x24,0x25,0x26,  // 60-6F
        0x27,0x28,0x29,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x2F,0x2C,0x0F,0x3F,0x0C,  // 70-7F
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // 80-8F
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // 90-9F
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // A0-AF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // B0-BF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // C0-CF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // D0-DF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,  // E0-EF
        0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C]; // F0-FF

/**************************************/
B5500IOUnit.prototype.clear = function clear() {
    /* Initializes (and if necessary, creates) the I/O Unit state */

    this.W = 0;                         // Memory buffer register
    this.D = 0;                         // I/O descriptor (control) register
    this.clearD();                      // clear the D-register exploded fields

    this.CC = 0;                        // Character counter (3 bits)
    this.IB = 0;                        // Input buffer register (6 bits+parity)
    this.IR = 0;                        // Tape read register (6 bits+parity)
    this.OB = 0;                        // Output buffer register (6 bits+parity)
    this.WB = 0;                        // Tape write register (6 bits+parity)
    this.LP = 0;                        // Longitudinal parity register (6 bits+parity)
    this.SC = 0;                        // Sequence counter (4 bits)
    this.PC = 0;                        // Pulse counter register (6 bits, 1MHz rate)

    this.AOFF = 0;                      // Address overflow FF
    this.EXNF = 0;                      // W-contents FF (0=has address, 1=has descriptor)
    this.HOLF = 0;                      // Hold-over FF (for alpha vs. binary card read)
    this.LCHF = 0;                      // Last-character FF (triggers RD construction)
    this.LPWF = 0;                      // (unknown)
    this.MANF = 0;                      // Memory access needed FF
    this.MAOF = 0;                      // Memory access obtained FF
    this.OBCF = 0;                      // (unknown)
    this.STRF = 0;                      // Strobe FF

    this.BKWF = 0;                      // Tape control FF
    this.FWDF = 0;                      // Tape control FF
    this.IM1F = 0;                      // Tape control FF
    this.IMFF = 0;                      // Tape control FF
    this.PUCF = 0;                      // Tape control FF
    this.RCNF = 0;                      // Tape control FF
    this.SHOF = 0;                      // Tape control FF
    this.SKFF = 0;                      // Tape control FF
    this.VRCF = 0;                      // Tape control FF

    this.IMCF = 0;                      // (unknown)
    this.RECF = 0;                      // (unknown)
    this.REMF = 1;                      // Remote FF (0=local, 1=remote and available)

    this.busy = 0;                      // I/O Unit is busy
    this.busyUnit = 0;                  // Peripheral unit index currently assigned to the I/O Unit

    this.cycleCount = 0;                // Current cycle count for this.run()
    this.cycleLimit = 0;                // Cycle limit for this.run()
    this.totalCycles = 0;               // Total cycles executed on this I/O Unit
    this.ioUnitTime = 0;                // Total I/O Unit running time, based on cycles executed
    this.ioUnitSlack = 0;               // Total I/O Unit throttling delay, milliseconds

    if (this.forkHandle) {
        clearTimeout(this.forkHandle);
    }
};

/**************************************/
B5500IOUnit.prototype.clearD = function clearD() {
    /* Clears the D-register and the exploded field variables used internally */

    this.D = 0;
    this.Dunit = 0;                     // Unit designate field (5 bits)
    this.DwordCount = 0;                // Word count field (10 bits)
    this.D18F = 0;                      // Memory inhibit bit (0=transfer, 1=no transfer)
    this.D21F = 0;                      // Mode bit (0=alpha, 1=binary)
    this.D22F = 0;                      // Direction bit (0=forward), etc.
    this.D23F = 0;                      // Word counter bit (0=ignore, 1=use)
    this.D24F = 0;                      // I/O bit (0=write, 1=read)
    this.D26F = 0;                      // Memory address error bit
    this.D27F = 0;                      // Device error bit 1
    this.D28F = 0;                      // Device error bit 2
    this.D29F = 0;                      // Device error bit 3
    this.D30F = 0;                      // Unit not-ready bit
    this.D31F = 0;                      // Memory parity error on descriptor fetch bit
    this.D32F = 0;                      // Unit busy bit
    this.Daddress = 0;                  // Memory transfer address (15 bits)
};

/**************************************/
B5500IOUnit.prototype.fetch = function fetch(addr) {
    /* Fetch a word from memory at address "addr" and leave it in the W register.
    Returns 1 if a memory access error occurred, 0 if no error */
    var acc = this.accessor;            // get a local reference to the accessor object

    acc.addr = addr;
    this.cc.fetch(acc);
    this.W = acc.word;

    this.cycleCount += B5500CentralControl.memCycles;
    if (acc.MAED) {
        this.D26F = 1;                  // set memory address error
        return 1;
    } else if (acc.MPED) {
        this.D29F = 1;                  // set memory parity error on data transfer
        return 1;
    } else {
        return 0;                       // no error
    }
};

/**************************************/
B5500IOUnit.prototype.store = function store(addr) {
    /* Store a word in memory at address "addr" from the W register.
    Returns 1 if a memory access error occurred, 0 if no error */
    var acc = this.accessor;            // get a local reference to the accessor object

    acc.addr = addr;
    acc.word = this.W;
    this.cc.store(acc);

    this.cycleCount += B5500CentralControl.memCycles;
    if (acc.MAED) {
        this.D26F = 1;                  // set memory address error
        return 1;
    } else {
        return 0;                       // no error
    }
};

/**************************************/
B5500IOUnit.prototype.fetchBuffer = function fetchBuffer(mode, words) {
    /* Fetches words from memory starting at this.Daddress and coverts the
    BIC characters to ANSI or BCLANSI in this.buffer. "mode": 0=BCLANSI, 1=ANSI;
    "words": maximum number of words to transfer. At exit, updates this.Daddress
    with the final transfer address+1. If this.D23F, updates this.wordCount
    with any remaining count.
    Returns the number of characters fetched into the buffer */
    var addr = this.Daddress;           // local copy of memory address
    var buf = this.buffer;              // local pointer to buffer
    var c;                              // current character code
    var count = 0;                      // number of characters fetched
    var done = false;                   // loop control
    var overflow = false;               // memory address overflowed max
    var s;                              // character shift counter
    var table = (mode ? B5500IOUnit.BICtoANSI : B5500IOUnit.BICtoBCLANSI);
    var w;                              // local copy of this.W

    do {                                // loop through the words
        if (words <= 0) {
            done = true;
        } else {
            words--;
            if (overflow) {
                this.AOFF = 1;          // for display only
                this.D26F = 1;          // address overflow: set invalid address error
                done = true;
            } else if (!this.fetch(addr)) { // fetch the next word from memory
                w = this.W;             // fill the buffer with this word's characters
                for (s=0; s<8; s++) {
                    c = (w - (w %= 0x40000000000))/0x40000000000;
                    buf[count++] = table[c];
                    w *= 64;            // shift word left 6 bits
                } // for s
            }
            if (addr < 0x7FFF) {
                addr++;
            } else {
                overflow = true;
            }
        }
    } while (!done);

    this.Daddress = addr;
    if (this.D23F) {
        this.DwordCount = words & 0x1FF;
    }
    return count;
};

/**************************************/
B5500IOUnit.prototype.fetchBufferWithGM = function fetchBufferWithGM(mode, words) {
    /* Fetches words from memory starting at this.Daddress and coverts the
    BIC characters to ANSI or BCLANSI in this.buffer. "mode": 0=BCLANSI, 1=ANSI;
    "words": maximum number of words to transfer. The transfer can be terminated
    by a group-mark code in memory. At exit, updates this.Daddress with the
    final transfer address+1. If this.D23F, updates this.wordCount
    with any remaining count.
    Returns the number of characters fetched into the buffer */
    var addr = this.Daddress;           // local copy of memory address
    var buf = this.buffer;              // local pointer to buffer
    var c;                              // current character code
    var count = 0;                      // number of characters fetched
    var done = false;                   // loop control
    var overflow = false;               // memory address overflowed max
    var s;                              // character shift counter
    var table = (mode ? B5500IOUnit.BICtoANSI : B5500IOUnit.BICtoBCLANSI);
    var w;                              // local copy of this.W

    do {                                // loop through the words
        if (words <= 0) {
            done = true;
        } else {
            words--;
            if (overflow) {
                this.AOFF = 1;          // for display only
                this.D26F = 1;          // address overflow: set invalid address error
                done = true;
            } else if (!this.fetch(addr)) { // fetch the next word from memory
                w = this.W;             // fill the buffer with this word's characters
                for (s=0; s<8; s++) {
                    c = (w - (w %= 0x40000000000))/0x40000000000;
                    if (c == 0x1F) {
                        done = true;    // group-mark detected
                        break;
                    } else {
                        buf[count++] = table[c];
                        w *= 64;        // shift word left 6 bits
                    }
                } // for s
            }
            if (addr < 0x7FFF) {
                addr++;
            } else {
                overflow = true;
            }
        }
    } while (!done);

    this.Daddress = addr;
    if (this.D23F) {
        this.DwordCount = words & 0x1FF;
    }
    return count;
};

/**************************************/
B5500IOUnit.prototype.storeBuffer = function storeBuffer(chars, offset, mode, words) {
    /* Converts characters in this.buffer from ANSI or BCLANSI to BIC, assembles
    them into words, and stores the words into memory starting at this.Daddress.
    "chars": the number of characters to store, starting at "offset" in the buffer;
    "mode": 0=BCLANSI, 1=ANSI; "words": maximum number of words to transfer.
    At exit, updates this.Daddress with the final transfer address+1.
    If this.D23F, updates this.wordCount with any remaining count.
    Returns the number of characters stored into memory from the buffer */
    var addr = this.Daddress;           // local copy of memory address
    var buf = this.buffer;              // local pointer to buffer
    var c;                              // current character code
    var count = 0;                      // number of characters fetched
    var done = (words == 0);            // loop control
    var overflow = false;               // memory address overflowed max
    var power = 0x40000000000;          // factor for character shifting into a word
    var s = 0;                          // character shift counter
    var table = (mode ? B5500IOUnit.ANSItoBIC : B5500IOUnit.BCLANSItoBIC);
    var w = 0;                          // local copy of this.W

    while (!done) {                     // loop through the words
        if (count >= chars) {
            done = true;
        } else {
            c = table[buf[offset+(count++)]];
            w += c*power;
            power /= 64;
            if (++s > 7) {
                this.W = w;
                if (overflow) {
                    this.AOFF = 1;      // for display only
                    this.D26F = 1;      // address overflow: set invalid address error
                    done = true;
                } else {
                    this.store(addr);   // store the word in memory
                }
                if (addr < 0x7FFF) {
                    addr++;
                } else {
                    overflow = true;
                }
                w = s = 0;
                power = 0x40000000000;
                if (--words <= 0) {
                    done = true;
                }
            }
        }
    } // while !done

    if (s > 0 && words > 0) {           // partial word left to be stored
        this.W = w;
        if (overflow) {
            this.AOFF = 1;              // for display only
            this.D26F = 1;              // address overflow: set invalid address error
            done = true;
        } else {
            this.store(addr);           // store the word in memory
        }
        words--;
        if (addr < 0x7FFF) {
            addr++;
        }
    }

    this.Daddress = addr;
    if (this.D23F) {
        this.DwordCount = words & 0x1FF;
    }
    return count;
};

/**************************************/
B5500IOUnit.prototype.storeBufferWithGM = function storeBufferWithGM(chars, offset, mode, words) {
    /* Converts characters in this.buffer from ANSI to BIC, assembles them into
    words, and stores the words into memory starting at this.Daddress.
    "chars": the number of characters to store, starting at "offset" in the buffer;
    "mode": 0=BCLANSI, 1=ANSI; "words": maximum number of words to transfer.
    The final character stored from the buffer is followed in memory by a group-mark,
    assuming the word count is not exhausted. At exit, updates this.Daddress with the
    final transfer address+1.
    If this.D23F, updates this.wordCount with any remaining count.
    Returns the number of characters stored into memory from the buffer, plus one
    for the group-mark */
    var addr = this.Daddress;           // local copy of memory address
    var buf = this.buffer;              // local pointer to buffer
    var c;                              // current character code
    var count = 0;                      // number of characters fetched
    var done = (words == 0);            // loop control
    var overflow = false;               // memory address overflowed max
    var power = 0x40000000000;          // factor for character shifting into a word
    var s = 0;                          // character shift counter
    var table = (mode ? B5500IOUnit.ANSItoBIC : B5500IOUnit.BCLANSItoBIC);
    var w = 0;                          // local copy of this.W

    while (!done) {                     // loop through the words
        if (count >= chars) {
            done = true;
        } else {
            c = table[buf[offset+(count++)]];
            w += c*power;
            power /= 64;
            if (++s > 7) {
                this.W = w;
                if (overflow) {
                    this.AOFF = 1;      // for display only
                    this.D26F = 1;      // address overflow: set invalid address error
                    done = true;
                } else {
                    this.store(addr);   // store the word in memory
                }
                if (addr < 0x7FFF) {
                    addr++;
                } else {
                    overflow = true;
                }
                w = s = 0;
                power = 0x40000000000;
                if (--words <= 0) {
                    done = true;
                }
            }
        }
    } // while !done

    w += 0x1F*power;                // set group mark in register
    s++;
    count++;

    if (s > 0 && words > 0) {           // partial word left to be stored
        this.W = w;
        if (overflow) {
            this.AOFF = 1;              // for display only
            this.D26F = 1;              // address overflow: set invalid address error
            done = true;
        } else {
            this.store(addr);           // store the word in memory
        }
        words--;
        if (addr < 0x7FFF) {
            addr++;
        }
    }

    this.Daddress = addr;
    if (this.D23F) {
        this.DwordCount = words & 0x1FF;
    }
    return count;
};

/**************************************/
B5500IOUnit.prototype.finish = function finish() {
    /* Called to finish an I/O operation on this I/O Unit. Constructs and stores
    the result descriptor, sets the appropriate I/O Finished interrupt in CC */

    this.W = this.D =
        this.Dunit *   0x10000000000 +
        this.DwordCount * 0x40000000 +
        this.D18F *       0x20000000 +
        this.D21F *        0x4000000 +
        this.D22F *        0x2000000 +
        this.D23F *        0x1000000 +
        this.D24F *         0x800000 +
        this.D26F *         0x200000 +
        this.D27F *         0x100000 +
        this.D28F *          0x80000 +
        this.D29F *          0x40000 +
        this.D30F *          0x20000 +
        this.D31F *          0x10000 +
        this.D32F *           0x8000 +
        this.Daddress;

    switch(this.ioUnitID) {
    case "1":
        this.store(0x0C);
        this.cc.CCI08F = 1;             // set I/O Finished #1
        break;
    case "2":
        this.store(0x0D);
        this.cc.CCI09F = 1;             // set I/O Finished #2
        break;
    case "3":
        this.store(0x0E);
        this.cc.CCI10F = 1;             // set I/O Finished #3
        break;
    case "4":
        this.store(0x0F);
        this.cc.CCI11F = 1;             // set I/O Finished #4
        break;
    }

    this.busy = 0;                      // zero so CC won't think I/O unit is busy
    this.busyUnit = 0;
    this.cc.signalInterrupt();
};

/**************************************/
B5500IOUnit.prototype.makeFinish = function makeFinish(f) {
    /* Utility function to create a closure for I/O finish handlers */
    var that = this;

    return function(mask, length) {return f.call(that, mask, length)};
};

/**************************************/
B5500IOUnit.prototype.decodeErrorMask = function decodeErrorMask(errorMask) {
    /* Decodes the errorMask returned by the device drivers and ORs it into
    the D-register error bits */

    if (errorMask & 0x01) {this.D32F = 1}
    if (errorMask & 0x02) {this.D31F = 1}
    if (errorMask & 0x04) {this.D30F = 1}
    if (errorMask & 0x08) {this.D29F = 1}
    if (errorMask & 0x10) {this.D28F = 1}
    if (errorMask & 0x20) {this.D27F = 1}
    if (errorMask & 0x40) {this.D26F = 1}
};

/**************************************/
B5500IOUnit.prototype.finishBusy = function finishBusy(errorMask, length) {
    /* Handles a generic I/O finish when no word-count update or input data
    transfer is needed, but leaves the unit marked as busy in CC. This is needed
    by device operations that have a separate completion signal, such as line
    printer finish and disk read-check. The completion signal is responsible for
    resetting unit busy status */

    this.decodeErrorMask(errorMask);
    this.finish();
};

/**************************************/
B5500IOUnit.prototype.finishGeneric = function finishGeneric(errorMask, length) {
    /* Handles a generic I/O finish when no word-count update or input data
    transfer is needed. Can also be used to apply common error mask posting
    at the end of specialized finish handlers. Note that this turns off the
    busyUnit mask bit in CC */

    this.decodeErrorMask(errorMask);
    this.cc.setUnitBusy(this.busyUnit, 0);
    this.finish();
};

/**************************************/
B5500IOUnit.prototype.finishGenericRead = function finishGenericRead(errorMask, length) {
    /* Handles a generic I/O finish when input data transfer, and optionally,
    word-count update, is needed. Note that this turns off the busyUnit mask bit in CC */

    this.storeBuffer(length, 0, 1, (this.D23F ? this.DwordCount : 0x7FFF));
    this.finishGeneric(errorMask, length);
};

/**************************************/
B5500IOUnit.prototype.finishSPORead = function finishSPORead(errorMask, length) {
    /* Handles I/O finish for a SPO keyboard input operation */

    this.storeBufferWithGM(length, 0, 1, 0x7FFF);
    this.finishGeneric(errorMask, length);
};

/**************************************/
B5500IOUnit.prototype.finishDiskRead = function finishDiskRead(errorMask, length) {
    /* Handles I/O finish for a DFCU data read operation */
    var segWords = Math.floor((length+7)/8);
    var memWords = (this.D23F ? this.DwordCount : segWords);

    if (segWords < memWords) {
        memWords = segWords;
    }
    this.storeBuffer(length, 0, this.D21F, memWords);
    this.finishGeneric(errorMask, length);
};

/**************************************/
B5500IOUnit.prototype.initiateDiskIO = function initiateDiskIO(u) {
    /* Initiates an I/O to the Disk File Control Unit. The disk address is fetched from
    the first word of the memory area and converted to binary for the DFCU module. Read
    check and interrogate operations are determined from their respective IOD bits. If
    it's a read data operation, we request the specified number of segments from the disk
    and will sort out word count issues in finishDiskRead(). If it's a write data operation,
    we truncate or pad the data from memory as appropriate and request a write of the
    specified number of segments */
    var c;                              // address char
    var memWords;                       // number of memory words to transfer
    var p = 1;                          // address digit power
    var w;                              // current memory word value
    var x;                              // temp variable

    var segAddr = 0;                    // disk segment address
    var segs = this.LP;                 // I/O size in segments
    var segWords = segs*30;             // I/O size in words
    var segChars = segWords*8;          // I/O size in characters

    if (this.fetch(this.Daddress)) {    // fetch the segment address from first buffer word
        this.finish();
    } else {
        this.Daddress++;                // bump memory address past the seg address word
        w = this.W;                     // convert address word to binary
        for (x=0; x<7; x++) {           // 7 decimal digits: 1 for EU, 6 for EU-relative address
            c = w % 0x40;               // get low-order six bits of word
            segAddr += (c % 0x10)*p;    // isolate the numeric portion and accumulate
            w = (w-c)/0x40;             // shift word right six bits
            p *= 10;                    // bump power for next digit
        }

        if (this.D18F) {                // mem inhibit => read check operation
            u.readCheck(this.boundFinishBusy, segChars, segAddr);
        } else if (this.D23F && this.DwordCount == 0) {
            if (this.D24F) {            // read interrogate operation
                u.readInterrogate(this.boundFinishGeneric, segAddr);
            } else {                    // write interrogate operation
                u.writeInterrogate(this.boundFinishGeneric, segAddr);
            }
        } else if (this.D24F) {         // it's a read data operation
            u.read(this.boundFinishDiskRead, this.buffer, segChars, this.D21F, segAddr);
        } else {                        // it's a write data operation
            memWords = (this.D23F ? this.DwordCount : segWords);
            if (segWords <= memWords) { // transfer size is limited by number of segs
                this.fetchBuffer(this.D21F, segWords);
            } else {                    // transfer size is limited by word count
                x = this.fetchBuffer(this.D21F, memWords);
                c = (this.D21F ? 0x30 : 0x2B);  // pad "0" if binary, "#" if alpha (for BCL " ")
                while (x < segChars) {  // pad remainder of buffer up to seg count
                    this.buffer[x++] = c;
                }
            }
            u.write(this.boundFinishGeneric, this.buffer, segChars, this.D21F, segAddr);
        }
    }
};

/**************************************/
B5500IOUnit.prototype.initiatePrinterIO = function initiatePrinterIO(u) {
    /* Initiates an I/O to a Line Printer unit */
    var addr = this.Daddress;           // initial data transfer address
    var cc;                             // carriage control value to driver
    var chars;                          // characters to print
    var memWords;                       // words to fetch from memory

    if (this.D24F) {
        this.D30F = 1;                  // can't read from the printer
        this.finish();
    } else {
        if (this.D18F) {
            chars = memWords = 0;
        } else {
            memWords = (this.D23F ? this.DwordCount : 0);
            if (memWords > 0) {
                chars = memWords*8;
            } else {
                memWords = 17;          // assume a 132-character printer
                chars = 132;
            }
            this.fetchBuffer(1, memWords);
            this.Daddress = (addr-1) & 0x7FFF;      // printer accesses memory backwards, so final address is initial-1
        }
        cc = this.LP;
        if (cc & 0x0F) {
            cc = -(cc & 0x0F);          // skip to channel after print
        } else if (cc & 0x10) {
            cc = 2;                     // double space after print
        } else if (cc & 0x20) {
            cc = 1;                     // single space after print
        } else {
            cc = 0;                     // zero space after print
        }
        this.DwordCount = memWords*32;  // actual word count in D.[8:5], D.[13:5]=0
        u.write(this.boundFinishGeneric, this.buffer, chars, 0, cc);
    }
};

/**************************************/
B5500IOUnit.prototype.forkIO = function forkIO() {
    /* Asynchronously initiates an I/O operation on this I/O Unit for a peripheral device */
    var chars;                          // I/O memory transfer length
    var index;                          // unit index
    var u;                              // peripheral unit object
    var x;                              // temp number variable

    this.forkHandle = null;             // clear the setTimeout() handle

    x = this.D;                         // explode the D-register into its fields
    this.Dunit = (x%0x200000000000 - x%0x10000000000)/0x10000000000;    // [3:5]
    this.DwordCount = (x%0x10000000000 - x%0x40000000)/0x40000000;      // [8:10]
    x = x % 0x40000000;                 // isolate low-order 30 bits
    this.D18F = (x >>> 29) & 1;         // memory inhibit
    this.D21F = (x >>> 26) & 1;         // mode
    this.D22F = (x >>> 25) & 1;         // direction (for tapes)
    this.D23F = (x >>> 24) & 1;         // use word counter
    this.D24F = (x >>> 23) & 1;         // write/read
    this.LP = (x >>> 15) & 0x3F;        // save control bits for disk, drum, and printer
    this.Daddress = x % 0x8000;

    this.busyUnit = index = B5500CentralControl.unitIndex[this.D24F & 1][this.Dunit & 0x1F];
    if (this.cc.testUnitBusy(index)) {
        this.D32F = 1;                  // set unit busy error
        this.finish();
    } else if (!this.cc.testUnitReady(index)) {
        this.D30F = 1;                  // set unit not-ready error
        this.finish();
    } else {
        this.cc.setUnitBusy(index, 1);
        u = this.cc.unit[index];

        switch(this.Dunit) {
        // disk designates
        case 6:
        case 12:
            this.initiateDiskIO(u);
            break;

        // printer designates
        case 22:
        case 26:
            this.initiatePrinterIO(u);
            break;

        // datacom designate
        case 16:
            this.D30F = 1; this.finish(); // >>> temp until implemented <<<
            break;

        // card #1 reader/punch
        case 10:
            if (this.D24F) {            // CRA
                u.read(this.boundFinishGenericRead, this.buffer, (this.D21F ? 160 : 80), this.D21F, 0);
            } else {                    // CPA
                this.fetchBuffer(1, 10);
                this.D21F = 0;                  // force alpha mode
                this.DwordCount = 10;           // word count to 10
                x = (this.D % 0x10000) >>> 15;  // D32F = select alternate stacker
                u.write(this.boundFinishGeneric, this.buffer, 80, 0, x);
            }
            break;

        // card #2 reader
        case 14:
            if (this.D24F) {
                u.read(this.boundFinishGenericRead, this.buffer, (this.D21F ? 160 : 80), this.D21F, 0);
            } else {
                this.D30F = 1;          // can't write to CRB, report as not ready
                this.finish();
            }
            break;

        // SPO designate
        case 30:
            if (this.D24F) {
                u.read(this.boundFinishSPORead, this.buffer, 0x7FFF, 0, 0);
            } else {
                chars = this.fetchBufferWithGM(1, 0x7FFF);
                u.write(this.boundFinishGeneric, this.buffer, chars, 0, 0);
            }
            break;

        // magnetic tape designates
        case  1: case  3: case  5: case  7: case  9: case 11: case 13: case 15:
        case 17: case 19: case 21: case 23: case 25: case 27: case 29: case 31:
            this.D30F = 1; this.finish(); // >>> temp until implemented <<<
            break;

        // drum designates
        case 4:
        case 8:
            this.D30F = 1; this.finish(); // >>> temp until implemented <<<
            break;

        // paper tape designates
        case 18:
        case 20:
            this.D30F = 1; this.finish(); // >>> temp until implemented <<<
            break;

        // illegal designates
        default:
            this.D30F = 1;      // report invalid unit as not ready
            this.finish();
            break;
        } // switch this.Dunit
    }
};

/**************************************/
B5500IOUnit.prototype.initiate = function initiate() {
    /* Initiates an I/O operation on this I/O Unit. When P1 executes an IIO instruction,
    it calls the CentralControl.initiateIO() function, which selects an idle I/O Unit and
    calls this function for that unit. Thus, at entry we are still running on top of the
    processor. This routine merely fetches the IOD from memory and then schedules forkIO()
    to run asynchronously. Then we exit back through CC and into P1, thus allowing the
    actual I/O operation to run asynchronously from the processor. Of course, in a browser
    environment, all of the Javascript action occurs on one thread, so this allows us to
    multiplex what are supposed to be asynchronous operations on that thread */

    this.clearD();
    this.AOFF = 0;
    this.EXNF = 0;
    this.D31F = 1;                      // preset IOD fetch error condition (cleared if successful)
    if (this.fetch(0x08)) {             // fetch the IOD address from @10
        this.finish();
    } else {
        this.EXNF = 1;
        this.Daddress = this.W % 0x8000;
        if (this.fetch(this.Daddress)) {// fetch the IOD from that address
            this.finish();
        } else {
            this.D31F = 0;              // reset the IOD-fetch error condition
            this.D = this.W;
            this.forkHandle = setTimeout(this.boundForkIO, 0);
        }
    }
};

/**************************************/
B5500IOUnit.prototype.initiateLoad = function initiateLoad(cardLoadSelect, loadComplete) {
    /* Initiates a hardware load operation on this I/O unit. "cardLoadSelect" is true
    if the load is to come from the card reader, otherwise it will come from sector 1
    of DKA EU0. "loadComplete" is called on completion of the I/O.
    CentralControl calls this function in response to the Load button being
    pressed and released. Since there is no IOD in memory, we must generate one in the
    D register and initiate the I/O intrinsically. Note that if the I/O has an error,
    the RD is not stored and the I/O finish interrupt is not set in CC */
    var chars;
    var index;
    var u;

    function finishLoad(errorMask, length) {
        var memWords = Math.floor((length+7)/8);

        this.storeBuffer(length, 0, this.D21F, memWords);
        this.decodeErrorMask(errorMask);
        this.cc.setUnitBusy(this.busyUnit, 0);
        if (errorMask) {
            this.busy = 0;
            this.busyUnit = 0;
        } else {
            this.finish();
        }
        loadComplete();
    }

    this.clearD();
    if (cardLoadSelect) {
        this.D = 0x0A0004800010;        // unit 10, read, binary mode, addr @00020
        this.Dunit = 10;                // 10=CRA
        this.D21F = 1;                  // binary mode
        chars = 20*8;
    } else {
        this.D = 0x0600009F8010;        // unit 6, read, alpha mode, 63 segs, addr @00020
        this.Dunit = 6;                 // 6=DKA
        this.D21F = 0;                  // alpha mode
        this.LP = 63;                   // 63 sectors
        chars = 63*240;
    }

    this.D24F = 1;                      // read
    this.Daddress = 0x10;               // memory address @20
    this.busyUnit = index = B5500CentralControl.unitIndex[this.D24F & 1][this.Dunit & 0x1F];
    if (this.cc.testUnitBusy(index)) {
        this.D32F = 1;                  // set unit busy error
    } else if (!this.cc.testUnitReady(index)) {
        this.D30F = 1;                  // set unit not-ready error
    } else {
        this.cc.setUnitBusy(index, 1);
        u = this.cc.unit[index];
        u.read(this.makeFinish(finishLoad), this.buffer, chars, this.D21F, 1);
    }
};