/* hp3000_gic_dc.c: HP 3000 31262A GIC CS/80 disc and tape drive simulator

   Copyright (c) 2022-2023, J. David Bryan

   Permission is hereby granted, free of charge, to any person obtaining a
   copy of this software and associated documentation files (the "Software"),
   to deal in the Software without restriction, including without limitation
   the rights to use, copy, modify, merge, publish, distribute, sublicense,
   and/or sell copies of the Software, and to permit persons to whom the
   Software is furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included in
   all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
   THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
   IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

   Except as contained in this notice, the name of the author shall not be
   used in advertising or otherwise to promote the sale, use or other dealings
   in this Software without prior written authorization from the author.

   DC           31262A GIC CS/80 disc and tape drive simulator

   04-Jul-23    JDB     First release
   08-May-23    JDB     Added RESET -C to perform a Device Clear
   15-Feb-23    JDB     CS80DIAG passes
   14-Feb-23    JDB     Added Set Burst and burst-mode transfers
   09-Feb-23    JDB     Fixed error when all required parameters are missing
                        Invalid read execution message now gets dummy byte + EOI
                        Added Initiate Diagnostic command as NOP
   08-Feb-23    JDB     Set Return Address Mode is now allowed for unit 15
   12-Dec-22    JDB     Modified for multicard support
   14-Nov-22    JDB     Added Read Spare Table utility
   04-Nov-22    JDB     Added Set Release/Release/Release Denied support
   02-Nov-22    JDB     Added HP 7935 model
   12-Oct-22    JDB     Created DC device from HP2100 simulator module

   References:
     - CS/80 Instruction Set Programming Manual
         (5955-3442, July 1982)
     - Subset 80 for Fixed and Flexible Disc Drives (HP-IB Implementation)
         (5958-4129, November 1985)
     - LINUS External Reference Specification
         (October 1984)
     - IEEE Standard Digital Interface for Programmable Instrumentation
         (IEEE-488A-1980, September 1979)


   The Command Set 80 (CS/80) protocol was developed by HP in the early 1980s to
   simplify the support of new disc and tape models across the HP computer line.
   Prior to CS/80, operating system drivers had to be modified as each new model
   was introduced to specify the permitted addressing (valid cylinder, head, and
   sector ranges).  CS/80 introduced the Describe command to allow the drive to
   inform the driver of its addressing requirements, so the driver could
   automatically configure itself to whatever type of drives were connected.

   In addition, addressing was simplified by specifying linear block numbers, so
   that the driver did not need to concern itself with the physical division
   into cylinders, heads, and sectors.  Also, formatting, sparing, and testing
   functions were moved from the host to the drives, again relieving the driver
   of internal knowledge of drive configurations.  Read and write operations
   were simplified by integrating seeks into the commands, allowing a single
   command, such as Locate and Read, to replace several separate operations in
   prior disc systems.  Finally, cartridge tape drives were developed with CS/80
   command sets, so that the I/O drivers required minimal modification to
   support discs, tapes, and disc/tape combination drives.

   While the CS/80 protocol could be delivered over any interconnection system,
   in practice it was always paired with the Hewlett-Packard Interface Bus
   (HP-IB), HP's implementation of IEEE-488.  Because all CS/80 drives used the
   same electrical connection, drives were interchangeable across systems
   without requiring separate interface cards for each host CPU.

   CS/80 contains a large and complex set of commands.  In 1985, HP introduced
   Subset 80 (SS/80), a restricted version of CS/80, for use in HP's smaller
   hard and floppy drives.  This subset permitted the use of a simpler host
   controller.  SS/80 interconnections were extended to HP-IL and direct
   register interfaces, as well as being implemented over HP-IB.

   A large range of CS/80 and SS/80 drives were sold by HP, starting with the HP
   7908A disc drive in 1982, and continuing with the HP 7911A, 7912A, and 7933A
   in 1983, the HP 7914A and 7935A in 1984, the HP 7941A, 7942A, 7945A, and
   7946A in 1985, the HP 7907A fixed/removable disc drive (replacing the HP
   7906) in 1986, and the HP 7957B, 7958B, and 7959B in 1989.

   CS/80 philosophy moved diagnostic responsibility from the host CPU running
   stand-alone diagnostic programs to the device running internal self-tests.
   Extensive error-rate testing and reporting is included in CS/80.
   Consequently, external CS/80 exerciser programs are limited to querying the
   drive's internal error tables and initiating device diagnostics.


   This simulator implements the CS/80 protocol over HP-IB.  It calls and is
   called by the 31262A General I/O Channel simulator to send and receive bytes
   across the HP-IB to and from the CPU.  Because disc operations are greatly
   simplified with linear block addressing, disc "read block" and "write block"
   routines are implemented here, rather than requiring a separate disc library.
   Cartridge tape drives also use the same block read and write routines but
   take advantage of the SIMH tape library to present CTD images in standard
   SIMH tape format.

   To support disc, tape, and disc/tape combination units, the eight SIMH units
   provided can be assigned to eight disc units, eight tape units, four
   disc/tape combination units, or any required mix of these.  Each SIMH unit
   corresponds to a CS/80 unit, with CS/80 devices containing one or two units,
   designated unit 0 and unit 1, depending on the model.  Each CS/80 device is
   assigned a unique bus address from 0 to 7.  In addition, each device
   contains an addressable controller, designated as unit 15.

   A single CS/80 transaction consists of a Command Message, an optional
   Execution Message, and a Reporting Message.  Each message is indicated by a
   specific HP-IB secondary address following the appropriate Listen or Talk
   primary address.  Execution Messages are used to transfer data between the
   CPU and a device, e.g., for a disc read or write.  Commands that do not
   transfer data, such as Spare Block or Write File Mark, omit the Execution
   Message.  The Reporting Message returns a one-byte "quick status" (QSTAT)
   report of the success or failure of the command.  Only when the QSTAT
   indicates an error is a full Request Status command required.  This reduces
   command overhead by transmitting full status only when needed.

   Commands are divided into two categories: device commands and transparent
   commands.  Device commands are directed to a unit of a device and always
   follow the transaction sequence.  Transparent commands are (logically)
   intercepted and executed by the channel and may not follow the transaction
   model.

   The CS/80 protocol provides the following commands.  In the following table,
   the bus addressing state is indicated by U [untalk], L [listen], and T
   [talk], the secondary and opcode numeric values are in hex, and the command
   type is R [real time], G [general purpose], D [diagnostic], or C
   [complementary].

   Some of the commands are not allowed if they are directed to unit 15, and
   some may only be directed to unit 15.  The specific permissions regarding
   unit 15 are shown the "U15" column as N [unit 15 not allowed], Y [unit 15
   allowed], or O [only unit 15 allowed].

   The commands that are implemented are:

     Bus  Sec  Opc  Typ  U15  Operation
     ---  ---  ---  ---  ---  ----------------------------------------
      U   MSA   -    -    -   Identify
      U    -    14   -    -   Universal Device Clear (primary command)
      L    -    04   -    -   Selected Device Clear (primary command)

      L    05   -    -    -   Command Message
      L    05   00   R    N   Locate and Read
      L    05   02   R    N   Locate and Write
      L    05   04   R    N   Locate and Verify
      L    05   08   G    O   Copy Data
      L    05   0A   R    N   Cold Load Read
      L    05   0D   D    Y   Request Status
      L    05   0E   G    Y   Release
      L    05   0F   G    Y   Release Denied
      L    05   10   C    N   Set Address (single-vector)
      L    05   11   C    N   Set Address (three-vector)
      L    05   12   C    Y   Set Block Displacement
      L    05   18   C    Y   Set Length
      L    05   2n   C    Y   Set Unit (n = 0-15)
      L    05   30   D    Y   Initiate Utility, no execution message
      L    05   32   D    Y   Initiate Utility, send execution message
      L    05   34   C    Y   No Operation
      L    05   35   G    Y   Describe
      L    05   37   G    N   Initialize Media
      L    05   3C   C    Y   Set Burst (EOI tags last burst only)
      L    05   3D   C    Y   Set Burst (EOI tags all bursts)
      L    05   3E   C    Y   Set Status Mask
      L    05   4n   C    N   Set Volume (n = 0-7)
      L    05   48   C    Y   Set Return Addressing Mode
      L    05   49   R    N   Write File Mark
      L    05   4A   G    N   Unload

     L/T   0E   -    -    -   Execution Message

      L    10   -    -    -   Amigo Clear
      T    10   -    -    -   Reporting Message (QSTAT)

     L/T   12   -    -    -   Transparent Message
      L    12   01   -    -   HP-IB Parity Checking
      T    12   02   -    -   Read Loopback
      L    12   03   -    -   Write Loopback
      L    12   08   -    Y   Channel Independent Clear
      L    12   09   -    Y   Cancel

   The following CS/80 commands are accepted, but they have no effect on
   operations, i.e., they report success but are otherwise ignored:

     Bus  Sec  Opc  Typ  U15  Operation
     ---  ---  ---  ---  ---  -------------------
      L    05   33   D    O   Initiate Diagnostic
      L    05   38   C    Y   Set Options
      L    05   39   C    Y   Set RPS
      L    05   3A   C    Y   Set Retry Time
      L    05   3B   C    Y   Set Release

   The following CS/80 commands are not implemented by this module and return an
   Illegal Opcode error if received:

     Bus  Sec  Opc  Typ  U15  Operation
     ---  ---  ---  ---  ---  -------------------------------------------
      L    05   06   G    N   Spare Block
      L    05   31   D    Y   Initiate Utility, receive execution message


   The HP-IB bus transaction sequence for each of the implemented commands is
   listed below:

     Identify
     --------

       ATN  UNT     Untalk
       ATN  MSA     My secondary address
            DAB     ID data byte #1
       EOI  DAB     ID data byte #2
       ATN  OTA     Talk 30


     Universal Device Clear
     ----------------------

       ATN  PCG     Primary command 14H
            ppe     Parallel poll enabled when clear completes

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Selected Device Clear
     ---------------------

       ATN  MLA     My listen address
       ATN  PCG     Primary command 04H
            ppe     Parallel poll enabled when clear completes

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Locate and Read, Cold Load Read
     -------------------------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     0 to n complementary commands
       EOI  DAB     Opcode 00H, 0AH
       ATN  UNL     Unlisten
            ...     (seeking)
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 0EH (Execution Message)
            ppd     Parallel poll disabled
            DAB     Read data byte #1
            ...     (more data bytes)
       EOI  DAB     Read data byte #n
            ppe     Parallel poll enabled
       ATN  UNT     Untalk

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Locate and Write
     ----------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     0 to n complementary commands
       EOI  DAB     Opcode 02H
       ATN  UNL     Unlisten
            ...     (seeking)
            ppe     Parallel poll enabled

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 0EH (Execution Message)
            DAB     Write data byte #1
            ...     (more data bytes)
       EOI  DAB     Write data byte #n
            ppe     Parallel poll enabled
       ATN  UNL     Unlisten

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Locate and Verify
     -----------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     0 to n complementary commands
       EOI  DAB     Opcode 04H
       ATN  UNL     Unlisten
            ...     (verifying)
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Copy Data
     ---------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     0 to n complementary commands
            DAB     Opcode 08H
            DAB     Source Volume and Unit
            DAB     Opcode 10H, 11H (Set Address)
            DAB     Parameter byte #1
            DAB     Parameter byte #2
            DAB     Parameter byte #3
            DAB     Parameter byte #4
            DAB     Parameter byte #5
            DAB     Parameter byte #6
            DAB     Target Volume and Unit
            DAB     Opcode 10H, 11H (Set Address)
            DAB     Parameter byte #1
            DAB     Parameter byte #2
            DAB     Parameter byte #3
            DAB     Parameter byte #4
            DAB     Parameter byte #5
       EOI  DAB     Parameter byte #6
       ATN  UNL     Unlisten
            ...     (copying)
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Request Status
     --------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     0 to n complementary commands
       EOI  DAB     Opcode 0DH
       ATN  UNL     Unlisten
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 0EH (Execution Message)
            ppd     Parallel poll disabled
            DAB     Status byte #1
            ...     (more status bytes)
       EOI  DAB     Status byte #n
            ppe     Parallel poll enabled
       ATN  UNT     Untalk

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Set Address (single-vector), Set Address (three-vector), Set Block Displacement
     -------------------------------------------------------------------------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     Opcode 10H, 11H
            DAB     Parameter byte #1
            DAB     Parameter byte #2
            DAB     Parameter byte #3
            DAB     Parameter byte #4
            DAB     Parameter byte #5
       EOI  DAB     Parameter byte #6
       ATN  UNL     Unlisten
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Set Length
     ----------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     Opcode 18H
            DAB     Parameter byte #1
            DAB     Parameter byte #2
            DAB     Parameter byte #3
       EOI  DAB     Parameter byte #4
       ATN  UNL     Unlisten
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Set Unit, Set Volume
     --------------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
       EOI  DAB     Opcode 20H-2FH for units 0-15, 40H-47H for volumes 0-7
       ATN  UNL     Unlisten
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Initiate Utility, send execution message
     ----------------------------------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     0 to n complementary commands
            DAB     Opcode 32H
            DAB     Parameter byte #1
            DAB     Optional Parameter byte #2
            DAB     Optional Parameter byte #3
            DAB     Optional Parameter byte #4
            DAB     Optional Parameter byte #5
            DAB     Optional Parameter byte #6
            DAB     Optional Parameter byte #7
            DAB     Optional Parameter byte #8
       EOI  DAB     Optional Parameter byte #9
       ATN  UNL     Unlisten
            ...     (utility setup)
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 0EH (Execution Message)
            ppd     Parallel poll disabled
            DAB     Returned data byte #1
            ...     (more data bytes)
       EOI  DAB     Returned data byte #n
            ppe     Parallel poll enabled
       ATN  UNT     Untalk

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     No Operation
     ------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
       EOI  DAB     Opcode 34H
       ATN  UNL     Unlisten
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Describe
     --------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     0 to n complementary commands
       EOI  DAB     Opcode 35H
       ATN  UNL     Unlisten
            ...     (formatting data)
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 0EH (Execution Message)
            ppd     Parallel poll disabled
            DAB     Information byte #1
            ...     (more information bytes)
       EOI  DAB     Information byte #n
            ppe     Parallel poll enabled
       ATN  UNT     Untalk

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Initialize Media
     ----------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     0 to n complementary commands
            DAB     Opcode 37H
            DAB     Initialization options
       EOI  DAB     Block interleave
       ATN  UNL     Unlisten
            ...     (initializing)
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Set Status Mask
     ---------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     Opcode 3EH
            DAB     Parameter byte #1
            DAB     Parameter byte #2
            DAB     Parameter byte #3
            DAB     Parameter byte #4
            DAB     Parameter byte #5
            DAB     Parameter byte #6
            DAB     Parameter byte #7
       EOI  DAB     Parameter byte #8
       ATN  UNL     Unlisten
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Set Return Addressing Mode, Set Burst
     -------------------------------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     Opcode 48H, 3CH, 3DH
       EOI  DAB     Parameter byte #1
       ATN  UNL     Unlisten
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Write File Mark, Unload
     -----------------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 05H (Command Message)
            ppd     Parallel poll disabled
            DAB     0 to n complementary commands
       EOI  DAB     Opcode 49H, 4AH
       ATN  UNL     Unlisten
            ...     (seeking)
            ppe     Parallel poll enabled

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Amigo Clear
     -----------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 10H (Amigo Clear)
            ppd     Parallel poll disabled
       EOI  DAB     Parity check byte
       ATN  SDC     Selected device clear
            ppe     Parallel poll enabled when clear completes
       ATN  UNL     Unlisten


     HP-IB Parity Checking
     ---------------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 12H (Transparent Command)
            ppd     Parallel poll disabled
            DAB     Opcode 01H
       EOI  DAB     Checking option byte
       ATN  UNL     Unlisten


     Read Loopback, Write Loopback
     -----------------------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 12H (Transparent Command)
            ppd     Parallel poll disabled
            DAB     Opcode 02H, 03H
            DAB     Length byte #1
            DAB     Length byte #2
            DAB     Length byte #3
       EOI  DAB     Length byte #4
       ATN  UNL     Unlisten

       ATN  MxA     My talk/listen address
       ATN  SCG     Secondary command 12H (Transparent Execution)
            DAB     Loopback data byte #1
            ...     (more loopback bytes)
       EOI  DAB     Loopback data byte #n
       ATN  UNx     Untalk/Unlisten

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Optional Reporting Message)
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


     Channel Independent Clear, Cancel
     ---------------------------------

       ATN  MLA     My listen address
       ATN  SCG     Secondary command 12H (Transparent Command)
            ppd     Parallel poll disabled
            DAB     Optional 20H-2FH for Set Unit
       EOI  DAB     Opcode 08H, 09H
       ATN  UNL     Unlisten
            ppe     Parallel poll enabled when operation completes

       ATN  MTA     My talk address
       ATN  SCG     Secondary command 10H (Reporting Message)
            ppd     Parallel poll disabled
       EOI  DAB     QSTAT
       ATN  UNT     Untalk


   Disc image files are implemented as a bare stream of data bytes, with each
   16-bit word written as two bytes in big-endian format to match HP's
   organization.  Disc sector address headers and CRC/ECC trailers are not
   implemented, although the time taken to traverse them is included when
   operating in realistic-time mode.

   Cartridge tape image files are in SIMH tape format.  To permit random access,
   each tape record occupies exactly 1040 bytes in one of three layouts:

     Data Record                      File Mark and Erased Block
     -----------------------------    ---------------------------
     4-byte erase gap marker          4-byte tape or erasure mark
     4-byte leading record length     1036-byte erase gap
     2-1024 byte data block
     4-byte trailing record length
     erase gap marker(s) padding

   Data records contain from 1-1024 bytes; the data area of an odd-length record
   is padded with a zero byte.  Erase gap markers are added at the end of data
   records to pad the block lengths to 1040 bytes.  A half-gap marker will begin
   the gap if the data record is not a multiple of four bytes.  Erased blocks
   appear in a new tape image that has not been certified.

   Full access to a tape image is granted if the file contains the full
   complement of data blocks specified by the cartridge length.  The block count
   ends with the physical end of the image file or with an EOM marker following
   the last complete data block.  "Truncated" cartridges that are not full
   length are accepted but will be marked as read-only to prevent random writes
   beyond the physical EOF.  Reads beyond the physical length return No Data
   Found status.

   Both disc and tape image file formats are interchangeable with HPDrive, a
   disc/tape emulation program written by Ansgar Kueckes.


   Implementation notes:

    1. The CS/80 manual says, "Transparent commands are intercepted at the
       channel module and modify the normal command-execution-reporting
       transaction sequence," and "Operations occurring at the channel module
       level are transparent to the functional operation of the device."  This
       implies that a transparent command is allowed between phases of a device
       command.  The several clears and the Cancel commands fall into this
       category, although they effect the current device operation.  Presumably,
       the Loopback and HP-IB Parity Checking commands are also allowed,
       although because Loopback transfers data across the channel, this seems
       problematic.

       However, the one command that is issued between device phases and must
       not otherwise disturb the executing command is Identify.  For instance,
       the RTE driver sends an Identify whenever an operation times out.  If the
       device responds to the Identify, the driver resumes waiting for the
       eventual completion of the in-progress device command.  Otherwise, it
       aborts the transaction with a Cancel.  Operations such as Initialize
       Media that take a long time may time out one or more times, depending on
       how the user has configured the system.  These timeouts are expected and
       will not disturb the eventual command completion, provided that the
       Identify succeeds. Therefore, we must allow an Identify to be processed
       between any device command phases without disturbing the command.

    2. The Initiate Utility command invokes a device-specific routine.  We
       implement only the Read Error Log utility of the HP 9144 and 9145
       cartridge tape drives and the general Read Revision Numbers and Read
       Spare Table utilities.  DVM33 invokes the first utility to determine
       whether or not the mounted tape cartridge is certified.  EXER invokes the
       second utility to determine whether firmware-version-specific handling is
       needed and the third utility in response to the TABLES command.

    3. While CS/80 supports devices with up to 16 units (15 drive units plus a
       controller unit) with up to 8 volumes per unit, in practice no supported
       device contains more than two drive units, each with a single volume.
       The only exception is the HP 7907A fixed/removable disc drive, which
       supports two volumes on one drive unit.  However, we do not support that
       device here, so we can simplify the device state by restricting it to
       four units (including the controller as unit 15) with one volume each.

    4. A note on terminology: both CS/80 and SIMH use the terms device and unit.
       In hardware, a CS/80 device corresponds to a physical disc or tape drive
       identified by its bus address, and a CS/80 unit corresponds to the device
       controller or a separate storage implementation within a device
       identified by its unit number.  For example, the HP 7946A drive contains
       a hard disc drive at unit 0, a cartridge tape drive at unit 1, and the
       combined controller at unit 15.

       A SIMH device refers to the interface card and all connected drives, and
       a SIMH unit refers to one storage implementation connected to a media
       image file on the host platform.  For this device, CS/80 units and SIMH
       units correspond one-for-one, although the assignment of CS/80 units to
       SIMH units is arbitrary and determined by the unique combination of bus
       address and unit number.

    5. This module is unusual, in that a given physical device may require two
       SIMH units (e.g., the HP 7946 disc/tape combination).  To permit free
       association in assigning SIMH units to CS/80 units, assignment validity
       checks must be deferred until execution time, although some simple
       checks, such as ensuring that a specified unit number is present in the
       device model currently assigned, are made by the SET command processor.
       Consequently, our table of device and unit state information cannot be
       established until after consistency has been confirmed.

       For example, a SET DC1 ENABLED,9144,BUS=0,UNIT=0 command is accepted,
       even though DC0 has conflicting defaults (7945,BUS=0,UNIT=0).  The user
       must resolve the conflict before execution is permitted, but until then,
       we cannot set up device 0 unit 0's state table entry, because we don't
       know whether it is intended as a disc unit or a tape unit.  So while the
       simulation is stopped, the unit state information is unreliable.

       An exception must be made for the ATTACH command, because the type of
       image file attached must reflect the characteristics of the drive unit
       (disc or tape, volume size, etc.).  The properties that affect this --
       device model number and CS/80 unit number -- in effect when the ATTACH
       command is entered are used to initialize the corresponding unit state
       entry.  Assuming the attach is successful, those properties become
       "frozen," and changes that would affect them, e.g., SET DCn <model> or
       SET DCn UNIT=n commands, are rejected with "Unit already attached"
       errors.

       In general, though, the state table must be considered unreliable until
       verification completes successfully and execution begins.

    6. Multi-cartridge continuation is not implemented for the Copy Data
       command.  Attempting to do a full-volume copy that does not fit on the
       attached cartridge will terminate with an End of Volume error on the tape
       unit.

    7. Cartridge tape images are in SIMH Extended format.  This allows the use
       of a private marker to indicate an erased block that has not been
       written.  All other extended classes are ignored, in particular any tape
       description records that might be present.  However, because cartridge
       tapes allow random access, any such records must appear after the end of
       the full set of data blocks or after an EOM marker for a truncated
       cartridge.

    8. HP CTD documentation does not explicitly specify the condition of the
       tape after each stage of formatting, initialization, and certification.
       Documentation and source code usage of the Uninitialized Media and No
       Data Found errors is contradictory.  A best guess is that formatting
       divides the tape into blocks but does not write the data frames.
       Initialization by itself writes data frames for the "hidden" blocks
       containing the run-time and error logs, spare block table, etc. but not
       the user data blocks.  Certification writes data frames for the user
       blocks.  In simulation, all tape images are considered to be formatted.
       Accessing a new tape image that has not been initialized will return
       Uninitialized Media status.  Reading blocks after initializing but not
       certifying the image will return No Data Found status.  After certifying,
       blocks contain 1024-byte records consisting of hex FF bytes.

    9. This module supports CS/80 disc, tape, and combination drives.  For the
       first release, targeting MPE V/R, the tape and combination drives are
       commented out in the modifier table, making their selections unavailable
       to the user interface.  When the simulator has been extended to the HP-IB
       systems and targets MPE V/E, the additional models should be reenabled.
*/



#include "hp3000_defs.h"
#include "hp3000_io.h"
#include "hp3000_imb.h"                         /* must be included after hp3000_io.h */
#include "hp3000_gic.h"

#include "sim_tape.h"



/* Program constants */

#define CHANNEL_COUNT       (CHAN_MAX + 1)      /* count of IMB channels */

#define MAX_DC_COUNT        (CHANNEL_COUNT - 2) /* count of potential DC assignments */

#define SIM_UNITS           8                   /* number of SIMH units */

#define DEVICE_COUNT        8                   /* count of addressable CS/80 devices on the bus */
#define UNIT_COUNT          4                   /* count of CS/80 units per device (0-2 and 15 allowed) */
#define UNIT_15             3                   /* unit state index for unit 15 */

#define DRIVE_UNITS         BITS (14, 0)        /* bitmap of the drive units 0-14 */

#define ERASED_BLOCK        0x77777777          /* private tape marker for erased blocks */

#define C_SWITCH            SWMASK ('C')        /* perform a Device Clear during reset */
#define F_SWITCH            SWMASK ('F')        /* perform a forced detach */
#define N_SWITCH            SWMASK ('N')        /* create a new image during attach */
#define P_SWITCH            SWMASK ('P')        /* perform a power-on reset */
#define R_SWITCH            SWMASK ('R')        /* perform a read-only attach */
#define S_SWITCH            SWMASK ('S')        /* specify a short tape cartridge during attach */
#define X_SWITCH            SWMASK ('X')        /* extend a short tape cartridge to the long length */

#define NR_SWITCHES         (N_SWITCH | R_SWITCH)
#define XR_SWITCHES         (X_SWITCH | R_SWITCH)
#define NS_SWITCHES         (N_SWITCH | S_SWITCH)
#define XS_SWITCHES         (X_SWITCH | S_SWITCH)


/* Byte-swap macros.

   CS/80 exchanges integer values across the bus as a series of bytes, so,
   for example, a 16-bit value is passed as two sequential eight-bit bytes.
   Integers are sent most-significant byte first, i.e., in big-endian format.
   Because integers on the host platform may be stored in memory in
   little-endian format, we cannot simply copy values to the byte-addressed
   buffer for exchange.  Instead, we must move the bytes of the integer to the
   buffer individually.  These macros assist with this process.

   Two sets are provided: one set that converts bytes to variably sized
   integers, and a second set that converts variably sized integers to a set of
   bytes.  The first set of macros are invoked as functions taking two
   parameters: a byte array name or pointer to a byte array element, and a
   starting offset into that array.  They return an assembled integer value of
   the specified size.

   The second set of macros are invoked as statements.  These also take two
   parameters: a pointer to the starting byte array element, and an integer of
   the specified size.  These translate into a series of byte assignment
   statements.


   Implementation notes:

    1. Although the macro expansions look quite messy, compilers are typically
       able to optimize these to a relatively small set of machine instructions.
       As such, it doesn't pay to try to out-guess the compiler's optimization
       algorithms.

    2. Assigning a buffer address to a pointer and then indexing from the
       pointer in four-byte TO_DWORD conversions compiles to a single BSWAP
       instruction on x86 platforms, whereas indexing from the buffer directly
       compiles to a lengthy sequence of MOV/SAL/OR instructions.

    3. Converting three bytes to a 24-bit number is accomplished more
       efficiently by byte-swapping four bytes and then shifting right to
       eliminate the lowest byte.  This is much faster than byte-swapping the
       three bytes directly.

    4. The TO_4_BYTES macro must assign the value to be converted to a temporary
       for the sequence to be optimized to a single BSWAP instruction using gcc
       on x86 platforms.  As a consequence, the block resulting from the macro
       expansion will have a null statement at the end if the usual semicolon is
       appended to the macro call.  Caution, then, is needed if the macro is
       used in the "then" part of a conditional statement -- either the macro
       statement must be enclosed in a block itself, or the semicolon must be
       omitted if it is the only statement in the "if" part and an "else"
       follows.

    5. The TO_16_BITS macro would be used if the Set Retry Time command is ever
       implemented.  Currently, it is unused.
*/

#define TO_32_BITS(b,x)     (TO_DWORD (TO_WORD (b [x], b [x + 1]), \
                                       TO_WORD (b [x + 2], b [x + 3])))

#define TO_24_BITS(b,x)     (TO_DWORD (TO_WORD (b [x], b [x + 1]), \
                                       TO_WORD (b [x + 2], b [x + 3])) >> 8 \
                                       & 0xFFFFFF)

#define TO_16_BITS(b,x)     (TO_WORD (b [x], b [x + 1]))


#define TO_4_BYTES(b,v)     { const uint32 i = v; \
                              *b++ = UPPER_BYTE (UPPER_WORD (i)); \
                              *b++ = LOWER_BYTE (UPPER_WORD (i)); \
                              *b++ = UPPER_BYTE (i); \
                              *b++ = LOWER_BYTE (i); \
                              }

#define TO_3_BYTES(b,v)     *b++ = LOWER_BYTE (UPPER_WORD (v)); \
                            *b++ = UPPER_BYTE (v); \
                            *b++ = LOWER_BYTE (v)

#define TO_2_BYTES(b,v)     *b++ = UPPER_BYTE (v); \
                            *b++ = LOWER_BYTE (v)

#define TO_1_BYTE(b,v)      *b++ = LOWER_BYTE (v)


/* Access types */

typedef enum {                                  /* type of access requested for validation checks */
    Read_Access,
    Write_Access
    } ACCESS_TYPE;


/* Supported device model numbers */

typedef enum {                                  /* device model identifiers */
    HP_7908A,                                   /*   HP 7908A option 140 */
    HP_7908T,                                   /*   HP 7908A */
    HP_7911A,                                   /*   HP 7911A option 140 */
    HP_7911T,                                   /*   HP 7911A */
    HP_7912A,                                   /*   HP 7912A option 140 */
    HP_7912T,                                   /*   HP 7912A */
    HP_7914A,                                   /*   HP 7914A option 140 */
    HP_7914T,                                   /*   HP 7914A */
    HP_7933A,                                   /*   HP 7933A */
    HP_7935A,                                   /*   HP 7935A */
/*  HP_7936A,                                    *   HP 7936A */
/*  HP_7937A,                                    *   HP 7937A */
    HP_7941A,                                   /*   HP 7941A */
    HP_7942A,                                   /*   HP 7942A */
    HP_7945A,                                   /*   HP 7945A */
    HP_7946A,                                   /*   HP 7946A */
/*  HP_7957A,                                    *   HP 7957A */
    HP_7957B,                                   /*   HP 7957B */
/*  HP_7958A,                                    *   HP 7958A */
    HP_7958B,                                   /*   HP 7958B */
    HP_7959B,                                   /*   HP 7959B */
/*  HP_7961B,                                    *   HP 7961B */
/*  HP_7962B,                                    *   HP 7962B */
/*  HP_7963B,                                    *   HP 7963B */
    HP_9144A,                                   /*   HP 9144A */
    HP_9145A                                    /*   HP 9145A */
/*  HP_35401                                     *   HP 35401A */
    } MODEL_TYPE;


/* Device flags and accessors.

   DEV_V_UF + 15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
             +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
             | (reserved for disc interface) | - | - | - | R |    channel    |
             +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

   Where:

     R = realtime mode is enabled
*/

#define DEV_CHANNEL         DF_BITS (3, 0)      /* GIC channel address */
#define DEV_REALTIME        DF_BIT  (4)         /* timing mode is realistic */

#define CHANNEL(w)          DF_BITS_TO (3, 0, w)
#define TO_CHANNEL(w)       TO_DF_BITS (3, 0, w)


/* Unit flag declarations.

   UNIT_V_UF + 15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
   MTUF_V_UF + 10   9   8   7   6   5   4   3   2   1   0
              +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
              | - |       model       | unit  |  address  |    (reserved)     |
              +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

   CS/80 unit numbers are restricted by the SET UNIT validator to 0-1.  Unit
   number 15 (the controller) is mapped to unit index 3, so that with the
   restriction in place, a simple mask of the current CS/80 unit number to the
   lower two bits yields the index into the unit state array.  However, no SIMH
   unit is allocated for the controller; it is always scheduled on unit 0.

   The lower flag bits are reserved for use by the SIMH magnetic tape library.
*/

#define UNIT_MODEL          MT_BITS (9, 5)      /* Device model type */
#define UNIT_UNIT           MT_BITS (4, 3)      /* Device unit 0-2 */
#define UNIT_ADDRESS        MT_BITS (2, 0)      /* Bus address 0-7 */

#define MAPUS(u)            BITS_TO (1, 0, u)   /* map the unit number to the state array index */

#define MODEL(w)            MT_BITS_TO (9, 5, w)
#define UNIT(w)             MT_BITS_TO (4, 3, w)
#define ADDRESS(w)          MT_BITS_TO (2, 0, w)

#define TO_MODEL(w)         TO_MT_BITS (9, 5, w)
#define TO_UNIT(w)          TO_MT_BITS (4, 3, w)
#define TO_ADDRESS(w)       TO_MT_BITS (2, 0, w)

#define UNIT_7908A          TO_MODEL (HP_7908A)
#define UNIT_7908T          TO_MODEL (HP_7908T)
#define UNIT_7911A          TO_MODEL (HP_7911A)
#define UNIT_7911T          TO_MODEL (HP_7911T)
#define UNIT_7912A          TO_MODEL (HP_7912A)
#define UNIT_7912T          TO_MODEL (HP_7912T)
#define UNIT_7914A          TO_MODEL (HP_7914A)
#define UNIT_7914T          TO_MODEL (HP_7914T)
#define UNIT_7933A          TO_MODEL (HP_7933A)
#define UNIT_7935A          TO_MODEL (HP_7935A)
#define UNIT_7941A          TO_MODEL (HP_7941A)
#define UNIT_7942A          TO_MODEL (HP_7942A)
#define UNIT_7945A          TO_MODEL (HP_7945A)
#define UNIT_7946A          TO_MODEL (HP_7946A)
#define UNIT_7957B          TO_MODEL (HP_7957B)
#define UNIT_7958B          TO_MODEL (HP_7958B)
#define UNIT_7959B          TO_MODEL (HP_7959B)
#define UNIT_9144A          TO_MODEL (HP_9144A)
#define UNIT_9145A          TO_MODEL (HP_9145A)

#define Instance            u3                  /* instance ID of the controlling device */
#define IDC_Time            u4                  /* time remaining for an interrupted device command */
#define IDC_Opcode_State    u5                  /* opcode and state of an interrupted device command */
#define User_Flags          u6                  /* additional user flags */

typedef UNIT *UPTR;                             /* a pointer to a SIMH unit structure */


/* Interrupted device command declarations.

    31  30  29  [...]   18  17  16  15  14  13   [...]   2   1   0
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   |        command opcode         |         command state         |
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

   CS/80 devices will respond to an Identify sequence in the middle of a
   transaction without disturbing that transaction.  For example, RTE driver
   DVM33 places a timeout on CS/80 command execution, so that a program will
   not hang if the device dies.  However, very long transactions, such as an
   Initialize Media command, may exceed the timeout period.  In this case, DVM33
   does an Identify to see if the device is still responsive.  If it responds,
   DVM33 continues to wait for the command to complete.

   To handle this, we need to schedule the Identify on the same unit as is
   executing the long command.  To avoid disturbing the command-in-progress, we
   save the the unit state (remaining time, current opcode, and current state)
   in the UNIT structure, cancel the service before setting up the Identify, and
   then restore the prior service when it completes.

   As the opcode and state do not need a full 32-bit word to save their values,
   we use two 16-bit fields in a single UNIT word.
*/

#define IDC_OPCODE(u)       (CN_OPCODE) UPPER_WORD (u)  /* extract the command opcode field value */
#define IDC_STATE(u)        (CN_STATE)  LOWER_WORD (u)  /* extract the command state field value */

#define TO_IDC_OPCODE(o)    TO_DWORD ((o), 0)           /* align the opcode field value */
#define TO_IDC_STATE(s)     TO_DWORD (0, (s))           /* align the state field value */


/* User flag declarations.

    15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
   | - | - | - | - | - | - | - | - | - | - | - | - | - | S | state |
   +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

   Where:

     S = short (150-foot) tape cartridge

   State:

     00 = tape is formatted
     01 = tape is initialized
     10 = tape is certified

   These flags are kept in the User_Flags (i.e., uptr->u6) field.  There is
   insufficient room in the uptr->flags field to accommodate them, so we add a
   second flags field in the UNIT structure.  They are stored here, rather than
   in the volume structure, because they must be restored during a RESTORE
   command.
*/

typedef enum {                                  /* media states */
    Formatted,                                  /*   formatted, uninitialized */
    Initialized,                                /*   initialized, uncertified */
    Certified,                                  /*   certified */
    Decertified,                                /*   uncertified */
    Recertified,                                /*   recertified (force certified) */
    Reinitialized                               /*   reinitialized (force initialized) */
    } MEDIA_STATE;

#define USER_MEDIA_MASK     BITS (2, 0)         /* tape media mask */
#define USER_SHORT_CART     BIT  (2)            /* tape cartridge is short */
#define USER_STATE          BITS (1, 0)         /* tape cartridge state */

#define STATE(f)            BITS_TO (1, 0, f)   /* extract a state field value */

#define TO_STATE(e)         TO_BITS (1, 0, e)   /* align a state field value */


/* Describe structure declarations.

   The CS/80 protocol defines a Describe command that allows a host to query a
   device for its operational parameters.  The host may direct this query to a
   specific unit and volume in a device or may direct the query to the
   controller (unit 15), in which case the parameters for all units and volumes
   in the device are returned.

   Information returned by the Describe command is divided into three parts:
   controller, unit, and volume.  The return data contains the controller
   information, unit information (once for each unit present in the device), and
   volume information (once for each volume present in the unit).

   In simulation, the Describe information, along with a device's Identify
   command response, are encoded in a table that is indexed by the model
   identifier.  This table is used not only for the Describe command but also to
   provide the operational limits for read and write commands.


   Implementation notes:

    1. Multi-byte integer values are always returned in big-endian format.
       However, the information in the table below may be little-endian or
       big-endian, depending on the underlying memory arrangement of the host.
       As the data will need to be reformatted, rather than simply copied as a
       block, each datum is stored in a uint32 variable for optimal access,
       regardless of the underlying numeric range.
*/

typedef struct {                                /* volume descriptor */
    uint32  max_cylinder;                       /*   maximum cylinder address vector value */
    uint32  max_head;                           /*   maximum head address vector value */
    uint32  max_sector;                         /*   maximum sector address vector value */
    uint32  max_block;                          /*   maximum single-vector address value */
    uint32  interleave;                         /*   current interleave value */
    } VOL_DESC;

typedef struct {                                /* unit descriptor */
    uint32    type;                             /*   device type */
    uint32    model;                            /*   product model number and option */
    uint32    bytes_per_block;                  /*   count of bytes per data block */
    uint32    buffered;                         /*   count of block buffers */
    uint32    burst;                            /*   burst size recommended */
    uint32    block_time;                       /*   time between blocks in microseconds */
    uint32    avg_xfer;                         /*   continuous transfer rate in KB/second */
    uint32    retry_time;                       /*   optimal retry time in centiseconds */
    uint32    access_time;                      /*   maximum access time in centiseconds */
    uint32    max_interleave;                   /*   maximum interleave value */
    uint32    fixed_volumes;                    /*   bitmap of installed fixed volumes */
    uint32    removable_volumes;                /*   bitmap of installed removable volumes */
    VOL_DESC  volume;                           /*   descriptor for volume 0 */
    } UNIT_DESC;

typedef const UNIT_DESC *UDPTR;

typedef struct {                                /* device descriptor */
    uint32     identity;                        /*   Amigo ID value */
    uint32     installed_units;                 /*   bitmap of installed units */
    uint32     max_rate;                        /*   instantaneous transfer rate in KB/second */
    uint32     type;                            /*   controller type */
    UNIT_DESC  unit [2];                        /*   descriptors for units 0-1 */
    } DEV_DESC;

typedef const DEV_DESC *DDPTR;


/* Describe information declarations.

   Information describing the supported drive models is presented in the
   following format:

     [Device] Identity, Installed Units, Max Transfer Rate, Type
       [Unit] Type, Product, Bytes/Block, Buffered Blocks, Recommended Burst,
              Block Time, Avg Transfer Rate, Retry Time, Max Access,
              Max Interleave, Fixed Volumes, Removable Volumes
         [Volume] Max Cylinder, Max Head, Max Sector, Max Block Address,
                  Current Interleave

   For the 7908A, 7911A, 7912A, and 7914A, two Describe entries are present: one
   for option 140, which deletes the integrated cartridge tape drive, and a
   second for the standard configuration that includes the drive.

   Implementation notes:

    1. No supported device has more than two units or more than one volume, so
       the Describe information is similarly restricted.

    2. Although two unit descriptors are allocated for each device descriptor,
       the Describe information only populates the defined number of units.  An
       uninitialized unit descriptor will contain all zeros, but as it is never
       accessed, this is immaterial.

    3. Returned Describe data for tape units do not report maximum cylinder,
       head, or sector values, and we honor this in the Describe command
       handler.  However, we use the head value to give the maximum track
       number and the sector value to give the maximum (relative) block number
       for each track, as these are used in the real-time tape seek
       calculations.
*/

static const DEV_DESC describe [] = {           /* Describe parameters, indexed by MODEL_TYPE */

    { 0x0200, 0x8001, 1250, 1,                                              /* HP 7908A option 140 */
      { { 0, 0x079080,  256, 16, 0,   470, 538,  10,    30, 31, 0x01, 0x00, /*   Unit 0 */
          {  369,  4,   34,   64749, 1 } },                                 /*     Volume 0 */
      } },

    { 0x0200, 0x8003, 1250, 1,                                              /* HP 7908A */
      { { 0, 0x079080,  256, 16, 0,   470, 538,  10,    30, 31, 0x01, 0x00, /*   Unit 0 */
          {  369,  4,   34,   64749, 1 } },                                 /*     Volume 0 */
        { 2, 0x079080, 1024,  4, 0, 30000,  35, 100, 24000,  0, 0x00, 0x01, /*   Unit 1 */
          {    0, 15, 4087,   65407, 0 } }                                  /*     Volume 0 */
      } },

    { 0x0204, 0x8001, 1000, 1,                                              /* HP 7911A option 140 */
      { { 0, 0x079110,  256, 16, 0,   470, 538,  10,    30, 31, 0x01, 0x00, /*   Unit 0 */
          {  571,  2,   63,  109823, 1 } },                                 /*     Volume 0 */
      } },

    { 0x0204, 0x8003, 1000, 1,                                              /* HP 7911A */
      { { 0, 0x079110,  256, 16, 0,   470, 538,  10,    30, 31, 0x01, 0x00, /*   Unit 0 */
          {  571,  2,   63,  109823, 1 } },                                 /*     Volume 0 */
        { 2, 0x079080, 1024,  4, 0, 30000,  35, 100, 24000,  0, 0x00, 0x01, /*   Unit 1 */
          {    0, 15, 4087,   65407, 0 } }                                  /*     Volume 0 */
      } },

    { 0x0208, 0x8001, 1000, 1,                                              /* HP 7912A option 140 */
      { { 0, 0x079120,  256, 16, 0,   470, 538,  10,    30, 31, 0x01, 0x00, /*   Unit 0 */
          {  571,  6,   63,  256255, 1 } },                                 /*     Volume 0 */
      } },

    { 0x0208, 0x8003, 1000, 1,                                              /* HP 7912A */
      { { 0, 0x079120,  256, 16, 0,   470, 538,  10,    30, 31, 0x01, 0x00, /*   Unit 0 */
          {  571,  6,   63,  256255, 1 } },                                 /*     Volume 0 */
        { 2, 0x079080, 1024,  4, 0, 30000,  35, 100, 24000,  0, 0x00, 0x01, /*   Unit 1 */
          {    0, 15, 4087,   65407, 0 } }                                  /*     Volume 0 */
      } },

    { 0x020A, 0x8001, 1250, 1,                                              /* HP 7914A option 140 */
      { { 0, 0x079140,  256, 16, 0,   300, 936,  80,   300, 31, 0x01, 0x00, /*   Unit 0 */
          { 1151,  6,   63,  516095, 1 } },                                 /*     Volume 0 */
      } },

    { 0x020A, 0x8003, 1250, 1,                                              /* HP 7914A */
      { { 0, 0x079140,  256, 16, 0,   300, 936,  80,   300, 31, 0x01, 0x00, /*   Unit 0 */
          { 1151,  6,   63,  516095, 1 } },                                 /*     Volume 0 */
        { 2, 0x079080, 1024,  4, 0, 30000,  35, 100, 24000,  0, 0x00, 0x01, /*   Unit 1 */
          {    0, 15, 4087,   65407, 0 } }                                  /*     Volume 0 */
      } },

    { 0x0212, 0x8001, 1250, 0,                                              /* HP 7933A */
      { { 0, 0x079330,  256, 16, 0,   240, 930,  80,    84, 31, 0x01, 0x00, /*   Unit 0 */
          { 1320, 12,   91, 1579915, 1 } }                                  /*     Volume 0 */
      } },

    { 0x0212, 0x8001, 1250, 0,                                              /* HP 7935A */
      { { 1, 0x079350,  256, 16, 0,   240, 930,  80,    84, 31, 0x00, 0x01, /*   Unit 0 */
          { 1320, 12,   91, 1579915, 1 } }                                  /*     Volume 0 */
      } },

    { 0x0220, 0x8001, 1000, 1,                                              /* HP 7941A */
      { { 0, 0x079410,  256, 48, 0,   531, 414, 100,  5000,  1, 0x01, 0x00, /*   Unit 0 */
          {  967,  2,   31,   92927, 1 } }                                  /*     Volume 0 */
      } },

    { 0x0220, 0x8003, 1000, 1,                                              /* HP 7942A */
      { { 0, 0x079420,  256, 48, 0,   531, 414, 100,  5000,  1, 0x01, 0x00, /*   Unit 0 */
          {  967,  2,   31,   92927, 1 } },                                 /*     Volume 0 */
        { 2, 0x091440, 1024, 10, 0, 30000,  35, 100, 24000,  0, 0x00, 0x01, /*   Unit 1 */
          {    0, 15, 4087,   65407, 0 } }                                  /*     Volume 0 */
      } },

    { 0x0220, 0x8001, 1000, 1,                                              /* HP 7945A */
      { { 0, 0x079450,  256, 48, 0,   531, 414, 100,  5000,  1, 0x01, 0x00, /*   Unit 0 */
          {  967,  6,   31,  216831, 1 } }                                  /*     Volume 0 */
      } },

    { 0x0220, 0x8003, 1000, 1,                                              /* HP 7946A */
      { { 0, 0x079460,  256, 48, 0,   531, 414, 100,  5000,  1, 0x01, 0x00, /*   Unit 0 */
          {  967,  6,   31,  216831, 1 } },                                 /*     Volume 0 */
        { 2, 0x091440, 1024, 10, 0, 30000,  35, 100, 24000,  0, 0x00, 0x01, /*   Unit 1 */
          {    0, 15, 4087,   65407, 0 } }                                  /*     Volume 0 */
      } },

    { 0x022C, 0x8001, 1000, 0,                                              /* HP 7957B */
      { { 0, 0x079571,  256, 64, 0,   270, 768,  80,   500,  1, 0x01, 0x00, /*   Unit 0 */
          { 1268,  3,   62,  319787, 1 } }                                  /*     Volume 0 */
      } },

    { 0x022D, 0x8001, 1000, 0,                                              /* HP 7958B */
      { { 0, 0x079581,  256, 64, 0,   265, 768,  80,   500,  1, 0x01, 0x00, /*   Unit 0 */
          { 1571,  5,   62,  594215, 1 } }                                  /*     Volume 0 */
      } },

    { 0x022E, 0x8001, 1000, 0,                                              /* HP 7959B */
      { { 0, 0x079591,  256, 64, 0,   265, 768,  80,   500,  1, 0x01, 0x00, /*   Unit 0 */
          { 1571, 11,   62, 1188431, 1 } }                                  /*     Volume 0 */
      } },

    { 0x0260, 0x8001, 1000, 1,                                              /* HP 9144A */
      { { 2, 0x091440, 1024, 12, 0, 30000,  35, 100, 24000,  0, 0x00, 0x01, /*   Unit 0 */
          {    0, 15, 4087,   65407, 0 } }                                  /*     Volume 0 */
      } },

    { 0x0268, 0x8001, 1000, 0,                                              /* HP 9145A */
      { { 2, 0x091450, 1024, 64, 0, 15000,  66,   0, 48000,  0, 0x00, 0x01, /*   Unit 0 */
          {    0, 31, 4079,  130559, 0 } }                                  /*     Volume 0 */
      } }

    };


/* Simulator tape support library status names */

static const char *tape_errors [] = {           /* error names, indexed by MTSE value */
    NULL,                                       /*   MTSE_OK       */
    "tape mark seen",                           /*   MTSE_TMK      */
    "unit not attached",                        /*   MTSE_UNATT    */
    "I/O error",                                /*   MTSE_IOERR    */
    "invalid record length",                    /*   MTSE_INVRL    */
    "invalid tape format",                      /*   MTSE_FMT      */
    "beginning of tape seen",                   /*   MTSE_BOT      */
    "end of medium seen",                       /*   MTSE_EOM      */
    "uncorrectable data error",                 /*   MTSE_RECE     */
    "write protected",                          /*   MTSE_WRP      */
    "logical end of tape",                      /*   MTSE_LEOT     */
    "tape runaway",                             /*   MTSE_RUNAWAY  */
    "reserved marker seen"                      /*   MTSE_RESERVED */
    };


/* Device controller state declarations */

typedef enum {                                  /* controller state */
    Reporting_Wait = 0,                         /*   waiting for a required report (default for reset) */
    Optional_Wait,                              /*   waiting for an optional report */
    Command_Ready,                              /*   waiting for a command message secondary */
    Channel_Wait,                               /*   waiting for a channel command opcode */
    Command_Wait,                               /*   waiting for a device command opcode */
    Parameter_Wait,                             /*   waiting for a parameter reception */
    Execution_Wait,                             /*   waiting for an execution message secondary */
    Execution_Send,                             /*   sending read data or status */
    Execution_Receive,                          /*   receiving write data */
    Error_Wait,                                 /*   waiting for error recovery */
    Error_Source,                               /*   sending bytes for error recovery */
    Error_Sink                                  /*   receiving bytes for error recovery */
    } CN_STATE;

/* Device controller state names */

static const char * const state_names [] = {    /* state names, indexed by CN_STATE */
    "Reporting Wait",
    "Optional Reporting Wait",
    "Command Ready",
    "Channel Wait",
    "Command Wait",
    "Parameter Wait",
    "Execution Wait",
    "Execution Send",
    "Execution Receive",
    "Error Wait",
    "Error Source",
    "Error Sink"
    };


/* CS/80 command opcode declarations */

typedef enum {
    Invalid = 0,                                /* invalid command (default for reset) */
    Locate_and_Read,                            /* device command opcodes */
    Locate_and_Write,
    Locate_and_Verify,
    Spare_Block,
    Copy_Data,
    Cold_Load_Read,
    Request_Status,
    Release,
    Release_Denied,
    Set_Block_Address,
    Set_CHS_Address,
    Set_Block_Displacement,
    Set_Length,
    Set_Unit,
    Initiate_Utility,
    Initiate_Utility_Write,
    Initiate_Utility_Read,
    Initiate_Diagnostic,
    No_Operation,
    Describe,
    Initialize_Media,
    Set_Options,
    Set_RPS,
    Set_Retry_Time,
    Set_Release,
    Set_Burst_Last,
    Set_Burst_All,
    Set_Status_Mask,
    Set_Volume,
    Set_Return_Addressing_Mode,
    Write_File_Mark,
    Unload,

    Amigo_Clear,                                /* transparent (channel) command opcodes */
    HPIB_Parity_Checking,
    Read_Loopback,
    Write_Loopback,
    Channel_Independent_Clear,
    Cancel,
    Quick_Status,
    Identify
    } CN_OPCODE;


/* CS/80 command opcode name declarations */

static const char * const opcode_names [] = {   /* opcode names, indexed by CN_OPCODE */
    "invalid",
    "Locate and Read",
    "Locate and Write",
    "Locate and Verify",
    "Spare Block",
    "Copy Data",
    "Cold Load Read",
    "Request Status",
    "Release",
    "Release Denied",
    "Set Block Address",
    "Set Vector Address",
    "Set Block Displacement",
    "Set Length",
    "Set Unit",
    "Initiate Utility",
    "Initiate Utility Write",
    "Initiate Utility Read",
    "Initiate Diagnostic",
    "No Operation",
    "Describe",
    "Initialize Media",
    "Set Options",
    "Set RPS",
    "Set Retry Time",
    "Set Release",
    "Set Burst Last",
    "Set Burst All",
    "Set Status Mask",
    "Set Volume",
    "Set Return Addressing Mode",
    "Write File Mark",
    "Unload",

    "Amigo Clear",
    "HP-IB Parity Checking",
    "Read Loopback",
    "Write Loopback",
    "Channel Independent Clear",
    "Cancel",
    "Quick Status",
    "Identify"
    };


/* Command message values to CS/80 command opcode maps */

static const CN_OPCODE command_map [] = {       /* device command map, indexed by message value */
    Locate_and_Read,                            /*   00 */
    Invalid,                                    /*   01 */
    Locate_and_Write,                           /*   02 */
    Invalid,                                    /*   03 */
    Locate_and_Verify,                          /*   04 */
    Invalid,                                    /*   05 */
    Spare_Block,                                /*   06 */
    Invalid,                                    /*   07 */
    Copy_Data,                                  /*   08 */
    Invalid,                                    /*   09 */
    Cold_Load_Read,                             /*   0A */
    Invalid,                                    /*   0B */
    Invalid,                                    /*   0C */
    Request_Status,                             /*   0D */
    Release,                                    /*   0E */
    Release_Denied,                             /*   0F */
    Set_Block_Address,                          /*   10 */
    Set_CHS_Address,                            /*   11 */
    Set_Block_Displacement,                     /*   12 */
    Invalid,                                    /*   13 */
    Invalid,                                    /*   14 */
    Invalid,                                    /*   15 */
    Invalid,                                    /*   16 */
    Invalid,                                    /*   17 */
    Set_Length,                                 /*   18 */
    Invalid,                                    /*   19 */
    Invalid,                                    /*   1A */
    Invalid,                                    /*   1B */
    Invalid,                                    /*   1C */
    Invalid,                                    /*   1D */
    Invalid,                                    /*   1E */
    Invalid,                                    /*   1F */
    Set_Unit,                                   /*   20 */
    Set_Unit,                                   /*   21 */
    Set_Unit,                                   /*   22 */
    Set_Unit,                                   /*   23 */
    Set_Unit,                                   /*   24 */
    Set_Unit,                                   /*   25 */
    Set_Unit,                                   /*   26 */
    Set_Unit,                                   /*   27 */
    Set_Unit,                                   /*   28 */
    Set_Unit,                                   /*   29 */
    Set_Unit,                                   /*   2A */
    Set_Unit,                                   /*   2B */
    Set_Unit,                                   /*   2C */
    Set_Unit,                                   /*   2D */
    Set_Unit,                                   /*   2E */
    Set_Unit,                                   /*   2F */
    Initiate_Utility,                           /*   30 */
    Initiate_Utility_Write,                     /*   31 */
    Initiate_Utility_Read,                      /*   32 */
    Initiate_Diagnostic,                        /*   33 */
    No_Operation,                               /*   34 */
    Describe,                                   /*   35 */
    Invalid,                                    /*   36 */
    Initialize_Media,                           /*   37 */
    Set_Options,                                /*   38 */
    Set_RPS,                                    /*   39 */
    Set_Retry_Time,                             /*   3A */
    Set_Release,                                /*   3B */
    Set_Burst_Last,                             /*   3C */
    Set_Burst_All,                              /*   3D */
    Set_Status_Mask,                            /*   3E */
    Invalid,                                    /*   3F */
    Set_Volume,                                 /*   40 */
    Set_Volume,                                 /*   41 */
    Set_Volume,                                 /*   42 */
    Set_Volume,                                 /*   43 */
    Set_Volume,                                 /*   44 */
    Set_Volume,                                 /*   45 */
    Set_Volume,                                 /*   46 */
    Set_Volume,                                 /*   47 */
    Set_Return_Addressing_Mode,                 /*   48 */
    Write_File_Mark,                            /*   49 */
    Unload                                      /*   4A */
    };

#define CMD_COUNT           (sizeof command_map / sizeof command_map [0])

static const CN_OPCODE channel_map [] = {       /* channel command map, indexed by message value */
    Invalid,                                    /*   00 */
    HPIB_Parity_Checking,                       /*   01 */
    Read_Loopback,                              /*   02 */
    Write_Loopback,                             /*   03 */
    Invalid,                                    /*   04 */
    Invalid,                                    /*   05 */
    Invalid,                                    /*   06 */
    Invalid,                                    /*   07 */
    Channel_Independent_Clear,                  /*   08 */
    Cancel                                      /*   09 */
    };

#define CHAN_COUNT          (sizeof channel_map / sizeof channel_map [0])


/* Command properties declarations */

typedef enum {                                  /* allowed controller unit selection */
    No,                                         /*   controller cannot be selected */
    Yes,                                        /*   controller can be selected */
    Only                                        /*   controller only can be selected */
    } CN_SELECT;

typedef struct {                                /* command properties */
    int32      parameter_count;                 /*   count of parameters for command */
    CN_STATE   next_state;                      /*   next command state */
    t_bool     complementary;                   /*   TRUE if command is complementary */
    t_bool     EOI_required;                    /*   TRUE if command must end with EOI */
    CN_SELECT  unit_15;                         /*   command application to unit 15 (the controller) */
    } CMD_PROPERTIES;


static const CMD_PROPERTIES cmd_props [] = {    /* command properties, indexed by CN_OPCODE */
/*    Param                              EOI     Unit */
/*    Count  Next State          Compl   Reqd     15  */
/*    -----  ------------------  ------  ------  ---- */
    {  -1,   Error_Wait,         FALSE,  TRUE,   Yes  },    /* invalid */
    {   0,   Execution_Wait,     FALSE,  TRUE,   No   },    /* Locate and Read */
    {   0,   Execution_Wait,     FALSE,  TRUE,   No   },    /* Locate and Write */
    {   0,   Reporting_Wait,     FALSE,  TRUE,   No   },    /* Locate and Verify */
    {   1,   Reporting_Wait,     FALSE,  TRUE,   No   },    /* Spare Block */
    {  16,   Reporting_Wait,     FALSE,  TRUE,   Only },    /* Copy Data */
    {   0,   Execution_Wait,     FALSE,  TRUE,   No   },    /* Cold Load Read */
    {   0,   Execution_Wait,     FALSE,  TRUE,   Yes  },    /* Request Status */
    {   0,   Reporting_Wait,     FALSE,  TRUE,   Yes  },    /* Release */
    {   0,   Reporting_Wait,     FALSE,  TRUE,   Yes  },    /* Release Denied */
    {   6,   Reporting_Wait,     TRUE,   FALSE,  No   },    /* Set Block Address */
    {   6,   Reporting_Wait,     TRUE,   FALSE,  No   },    /* Set Vector Address */
    {   6,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Block Displacement */
    {   4,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Length */
    {   0,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Unit */
    {   9,   Reporting_Wait,     FALSE,  TRUE,   Yes  },    /* Initiate Utility */
    {   9,   Execution_Wait,     FALSE,  TRUE,   Yes  },    /* Initiate Utility Write */
    {   9,   Execution_Wait,     FALSE,  TRUE,   Yes  },    /* Initiate Utility Read */
    {   3,   Reporting_Wait,     FALSE,  TRUE,   Only },    /* Initiate Diagnostic */
    {   0,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* No Operation */
    {   0,   Execution_Wait,     FALSE,  TRUE,   Yes  },    /* Describe */
    {   2,   Reporting_Wait,     FALSE,  TRUE,   No   },    /* Initialize Media */
    {   1,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Options */
    {   2,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set RPS */
    {   2,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Retry Time */
    {   1,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Release */
    {   1,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Burst Last */
    {   1,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Burst All */
    {   8,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Status Mask */
    {   0,   Reporting_Wait,     TRUE,   FALSE,  No   },    /* Set Volume */
    {   1,   Reporting_Wait,     TRUE,   FALSE,  Yes  },    /* Set Return Addressing Mode */
    {   0,   Reporting_Wait,     FALSE,  TRUE,   No   },    /* Write File Mark */
    {   0,   Reporting_Wait,     FALSE,  TRUE,   Yes  },    /* Unload */

    {   0,   Optional_Wait,      FALSE,  TRUE,   Yes  },    /* Amigo Clear */
    {   1,   Command_Ready,      FALSE,  TRUE,   Yes  },    /* HP-IB Parity Checking */
    {   4,   Execution_Wait,     FALSE,  TRUE,   Yes  },    /* Read Loopback */
    {   4,   Execution_Wait,     FALSE,  TRUE,   Yes  },    /* Write Loopback */
    {   0,   Optional_Wait,      FALSE,  TRUE,   Yes  },    /* Channel Independent Clear */
    {   0,   Reporting_Wait,     FALSE,  TRUE,   Yes  },    /* Cancel */
    {   0,   Execution_Send,     FALSE,  FALSE,  Yes  },    /* Quick Status */
    {   0,   Execution_Send,     FALSE,  FALSE,  Yes  }     /* Identify */
    };


/* CS/80 Initiate Utility subopcodes */

typedef enum {
    Read_Revision_Numbers = 0xC3,
    Read_Drive_Table      = 0xC4,
    Read_Error_Log        = 0xC5,
    Read_ERT_Log          = 0xC6,
    Pattern_ERT           = 0xC8
    } UTILITY_SUBOPCODE;


/* Set Unit and Set Volume command accessors.

     7   6   5   4   3   2   1   0
   +---+---+---+---+---+---+---+---+
   | 0   0   1   0 |     unit      |  Set Unit command
   +---+---+---+---+---+---+---+---+
   | 0   1   0   0 | 0 |  volume   |  Set Volume command
   +---+---+---+---+---+---+---+---+
*/

#define SET_UNIT(b)         BITS_TO (3, 0, b)   /* unit number in lower four bits of Set Unit command */
#define SET_VOLUME(b)       BITS_TO (2, 0, b)   /* volume number in lower three bits of Set Volume command */


/* Copy Data command accessors.

     7   6   5   4   3   2   1   0
   +---+---+---+---+---+---+---+---+
   | 0 |  volume   | 0 |   unit    |  Copy Data specifier
   +---+---+---+---+---+---+---+---+
*/

#define COPY_VOLUME(b)      BITS_TO (6, 4, b)   /* extract the volume number */
#define COPY_UNIT(b)        BITS_TO (2, 0, b)   /* extract the unit number */

#define COPY_BLOCK          0x10                /* subopcode for single-vector addressing */
#define COPY_CHS            0x11                /* subopcode for three-vector addressing */


/* Request Status command accessors.

     7   6   5   4   3   2   1   0
   +---+---+---+---+---+---+---+---+
   |    volume     |     unit      |  Request Status identification field
   +---+---+---+---+---+---+---+---+
*/

#define TO_RS_VOLUME(v)     TO_BITS (7, 4, v)   /* position the volume number */
#define TO_RS_UNIT(u)       TO_BITS (3, 0, u)   /* position the unit number */


/* CS/80 command formatter declarations */

static const BITSET_NAME release_names [] = {   /* Set Release names */
    "\1Enable timeout\0Disable timeout",        /*   bit 7 */
    "Release automatically"                     /*   bit 6 */
    };

static const BITSET_FORMAT release_format =     /* names, offset, direction, alternates, bar */
    { FMT_INIT (release_names, 6, msb_first, has_alt, no_bar) };


static const BITSET_NAME option_names [] = {    /* Set Options names */
    "\1Media Unload\0Cartridge Unload",         /*   bit 7 */
    NULL,                                       /*   bit 6 */
    NULL,                                       /*   bit 5 */
    NULL,                                       /*   bit 4 */
    "Immediate Reporting",                      /*   bit 3 */
    "Auto Sparing",                             /*   bit 2 */
    "\1Skip Sparing\0Jump Sparing",             /*   bit 1 */
    "Character Count"                           /*   bit 0 */
    };

static const BITSET_FORMAT option_format =      /* names, offset, direction, alternates, bar */
    { FMT_INIT (option_names, 0, msb_first, has_alt, no_bar) };


static const BITSET_NAME parity_names [] = {    /* HP-IB Parity Checking names */
    "\1Enabled\0Disabled",                      /*   bit 0 */
    "Enable SRQ with poll"                      /*   bit 1 */
    };

static const BITSET_FORMAT parity_format =      /* names, offset, direction, alternates, bar */
    { FMT_INIT (parity_names, 0, lsb_first, has_alt, no_bar) };


/* Unit error status bit declarations.

   The "Error Reporting Fields" of the Request Status command assign one bit to
   each possible error.  The eight bytes of status information is divided
   logically into four 16-bit error classes: Reject, Fault, Access, and
   Information errors.  Multiple errors can exist concurrently, so each error is
   allocated one bit in the 64 bits of error status, which is implemented as an
   eight element array of bytes.

   The error status codes effectively number the 64 bits from left to right,
   i.e., bit 0 as the MSB and bit 63 as the LSB.  Three macros are provided to
   translate codes into their correct array index and bit position.  The SET_ERR
   and CLEAR_ERR statement macros set and clear the array bit corresponding to
   the supplied error code.  The TEST_ERR function macro produces a Boolean
   value that reflects whether the error bit is set in the status array.
*/

typedef enum {
    Channel_Parity              =  2,
    Illegal_Opcode              =  5,
    Module_Addressing           =  6,
    Address_Bounds              =  7,
    Parameter_Bounds            =  8,
    Illegal_Parameter           =  9,
    Message_Sequence            = 10,
    Message_Length              = 12,
    Cross_Unit                  = 17,
    Unit_Fault                  = 22,
    Power_Fail                  = 30,
    Uninitialized_Media         = 33,
    Not_Ready                   = 35,
    Write_Protect               = 36,
    No_Data_Found               = 37,
    Unrecoverable_Data_Overflow = 40,
    Unrecoverable_Data          = 41,
    End_of_File                 = 43,
    End_of_Volume               = 44,
    Operator_Request            = 48
    } ERROR_STATUS;

#define SET_ERR(s,c)        s [(c) / 8] |=  (1u << 7 - (c) % 8)         /* set bit assignment statement */
#define CLEAR_ERR(s,c)      s [(c) / 8] &= ~(1u << 7 - (c) % 8)         /* clear bit assignment statement */
#define TEST_ERR(s,c)       ((s [(c) / 8] & (1u << 7 - (c) % 8)) != 0)  /* test bit function */


/* Delay properties table declarations.

   To support the realistic timing mode, the delay properties table contains
   timing specifications for the supported drives.  The times represent the
   delays for mechanical and electronic operations.  Delay values are in event
   tick counts; macros are used to convert from times to ticks.

   Disc delays involve moving the head positioner to the target cylinder (seek
   delay), waiting for the target sector to arrive under the head (rotational
   delay), and transferring the data contained within the larger sector field,
   plus the controller command processing delay.  To model this, we provide
   delays for seeking one cylinder (the acceleration and deceleration time), for
   crossing additional cylinders once the positioner is in motion (the
   per-cylinder traversal time), and the average rotational delay.  Sectors
   contain headers and trailers surrounding the data bytes, so we provide the
   per-byte data time and the delay imposed by the header and trailer bytes (the
   sector residue time).  For convenience during verifies that do not transfer
   data, we also provide the full block delay, although those could be
   calculated from the count of bytes per block times the data time plus the
   residue time.

   Tape delays are similar, except that the head positioner start/stop time and
   the incremental movement time are the same.  Tapes have no rotational delay,
   so the positioning time represents the per-block tape movement time for tape
   units, which seek at a higher rate than the read/write speed.

   We also provide a table of optimized ("fast") delays that represent the
   minimum times acceptable to the operating system driver.  These allow the
   simulator to operate much faster than real CS/80 drives would.  During device
   reset, these times are copied to a variable array to allow the user to alter
   them via the register interface if desired.


   Implementation notes:

    1. DVM33 has a race condition when requesting full status.  The Request
       Status command sends ten words (20 bytes), but the STATR subroutine reads
       only nine words from the FIFO before untalking the device.  The driver
       executes 15 machine instructions after retrieving the last word before
       asserting NRFD and Unlistening the card.  If the last two bytes are not
       sent to the FIFO before card stops listening, the Request Status command
       fails with a Message Length error.  Unfortunately, the driver responds to
       a QSTAT = 1 return by sending a Request Status command, which fails,
       producing an infinite loop.  Therefore, the controller status byte return
       time must be less than seven instructions (~4 usec) to avoid the race.
*/

typedef enum {                                  /* activation delays classification */
    Controller_Time = 0,                        /*   controller command response time */
    Status_Time,                                /*   controller status per-byte transfer time */
    Seek_Time,                                  /*   one cylinder seek time */
    Traverse_Time,                              /*   additional cylinder traverse time */
    Position_Time,                              /*   positioning to target time */
    Block_Time,                                 /*   full block transfer time */
    Data_Time,                                  /*   per-byte data transfer time */
    Residue_Time,                               /*   block residue time */
    DELAY_COUNT                                 /*   count of DELAY enumeration values */
    } DELAY;

static const char *delay_names [] = {           /* delay names, indexed by DELAY */
    "controller",                               /*   controller command response time */
    "status",                                   /*   controller status per-byte transfer time */
    "seek",                                     /*   one cylinder seek time */
    "traverse",                                 /*   additional cylinder traverse time */
    "position",                                 /*   positioning to target time */
    "block",                                    /*   full block transfer time */
    "data",                                     /*   per-byte data transfer time */
    "residue"                                   /*   block residue time */
    };

typedef int32 DELAY_PROPS [DELAY_COUNT];
typedef const DELAY_PROPS *DPPTR;

static const DELAY_PROPS real_time [] = {
   /* Controller  Status     Seek     Traverse    Position     Block       Data       Residue   */
   /*    Time      Time      Time       Time        Time       Time        Time         Time    */
   /* ----------  -------  ---------  ---------  ----------  ---------  -----------  ---------- */
    { mS ( 9.4),  uS (4),  mS (  5),  uS (176),  mS ( 8.3),  uS (470),  uS ( 1.60),  uS (60.40) },  /* HP 7908A */
    { mS ( 9.4),  uS (4),  mS (  5),  uS (176),  mS ( 8.3),  uS (470),  uS ( 1.60),  uS (60.40) },  /* HP 7908T */
    { mS ( 9.4),  uS (4),  mS (  5),  uS ( 77),  mS ( 8.3),  uS (300),  uS ( 0.90),  uS (77.28) },  /* HP 7911A */
    { mS ( 9.4),  uS (4),  mS (  5),  uS ( 77),  mS ( 8.3),  uS (300),  uS ( 0.90),  uS (77.28) },  /* HP 7911T */
    { mS ( 9.4),  uS (4),  mS (  5),  uS ( 77),  mS ( 8.3),  uS (300),  uS ( 0.90),  uS (77.28) },  /* HP 7912A */
    { mS ( 9.4),  uS (4),  mS (  5),  uS ( 77),  mS ( 8.3),  uS (300),  uS ( 0.90),  uS (77.28) },  /* HP 7912T */
    { mS ( 9.4),  uS (4),  mS (  5),  uS ( 44),  mS ( 8.3),  uS (300),  uS ( 0.90),  uS (77.28) },  /* HP 7914A */
    { mS ( 9.4),  uS (4),  mS (  5),  uS ( 44),  mS ( 8.3),  uS (300),  uS ( 0.90),  uS (77.28) },  /* HP 7914T */
    { mS ( 3.5),  uS (4),  mS (  5),  uS ( 33),  mS (11.1),  uS (240),  uS ( 0.82),  uS (30.08) },  /* HP 7933A */
    { mS ( 3.5),  uS (4),  mS (  5),  uS ( 33),  mS (11.1),  uS (240),  uS ( 0.82),  uS (30.08) },  /* HP 7935A */
/*  { mS ( 0.8),  uS (4),  mS (  5),  uS ( 26),  mS ( 8.3),  uS (179),  uS ( 0.47),  uS (58.68) },   * HP 7937A */
    { mS (10.1),  uS (4),  mS (  5),  uS ( 57),  mS ( 8.3),  uS (531),  uS ( 1.77),  uS (77.88) },  /* HP 7941A */
    { mS (10.1),  uS (4),  mS (  5),  uS ( 57),  mS ( 8.3),  uS (531),  uS ( 1.77),  uS (77.88) },  /* HP 7942A */
    { mS (10.1),  uS (4),  mS (  5),  uS ( 57),  mS ( 8.3),  uS (531),  uS ( 1.77),  uS (77.88) },  /* HP 7945A */
    { mS (10.1),  uS (4),  mS (  5),  uS ( 57),  mS ( 8.3),  uS (531),  uS ( 1.77),  uS (77.88) },  /* HP 7946A */
    { mS ( 0.8),  uS (4),  mS (  3),  uS ( 23),  mS (8.96),  uS (265),  uS ( 0.93),  uS (26.92) },  /* HP 7957B */
    { mS ( 0.8),  uS (4),  mS (  3),  uS ( 18),  mS (8.96),  uS (265),  uS ( 0.93),  uS (26.92) },  /* HP 7958B */
    { mS ( 0.8),  uS (4),  mS (  3),  uS ( 18),  mS (8.96),  uS (265),  uS ( 0.93),  uS (26.92) },  /* HP 7959B */
    { mS ( 9.4),  uS (4),  mS (195),  mS (195),  mS (29.3),  mS ( 30),  uS (19.10),  mS (10.44) },  /* HP 9144A */
    { mS ( 9.4),  uS (4),  mS (195),  mS (195),  mS (14.7),  mS ( 15),  uS ( 9.55),  mS ( 5.17) }   /* HP 9145A */
    };

static const DELAY_PROPS init_time = {          /* initial optimized times, indexed by DELAY */
   /* Controller  Status     Seek     Traverse    Position     Block       Data       Residue   */
   /*    Time      Time      Time       Time        Time       Time        Time         Time    */
   /* ----------  -------  ---------  ---------  ----------  ---------  -----------  ---------- */
      uS (  10),  uS (1),  uS ( 10),  uS (  1),  uS (   1),  uS (  1),  uS (  0.7),  uS (   10)
    };


/* Operating state declarations.

   Each CS/80 unit maintains two sets of operating state values: a persistent
   set, and a current set.  The two sets are initially identical when
   initialized to this power-on state:

     Attribute               Value
     ----------------------  ----------------
     Unit                    0
     Volume                  0
     Address                 0
     Length                  -1 (full volume)
     Burst                   disabled
     Rot. Position Sensing   disabled
     Retry Time              device-specific
     Status Mask             disabled
     Release                 T = 0, Z = 0
     Options                 device-specific
     Return Addressing Mode  single-vector

   Complementary commands that precede a real-time, general-purpose, or
   diagnostic command alter the current set but not the persistent set [*].  The
   current values apply to the current command but then revert to the persistent
   value set at command completion.  If only complementary commands are given,
   i.e., if the command tagged with EOI is a complementary command, then then
   current set replaces the persistent set.

   [*] Actually, this is not quite true.  A Set Unit command always sets both
   the current and persistent values, and both Address values are updated during
   commands that read or write data blocks.


   Implementation notes:

    1. The current implementation does not include the RPS or retry time
       attributes.
*/

typedef enum {                                  /* device-specific options */
    No_Options      = 0000,                     /*   no options enabled */
    Character_Count = 0001,                     /*   bit 0 (C) = character count mode enabled */
    Skip_Sparing    = 0002,                     /*   bit 1 (S) = skip-sparing mode enabled */
    Auto_Sparing    = 0004,                     /*   bit 2 (A) = auto-sparing mode enabled */
    Immed_Report    = 0010,                     /*   bit 3 (I) = immediate reporting mode enabled */
    Media_Unload    = 0200                      /*   bit 7 (M) = media unload mode enabled */
    } OPTION_SET;

typedef struct {                                /* unit operating parameters */
    uint32      address;                        /*   single-vector address (lower four bytes) */
    uint32      length;                         /*   transfer length */
    uint32      burst;                          /*   burst size in bytes + EOI tag bit in MSB */
/*  uint32      rps;                            **   rotational position sensing time and window size */
/*  uint32      retry_time;                     **   retry time in tens of milliseconds */
    uint8       status_mask [8];                /*   status mask */
    uint32      release;                        /*   release mode bits */
    OPTION_SET  options;                        /*   device-specific option bits */
    uint32      return_mode;                    /*   return addressing mode */
    } OPERATING_STATE;

#define FULL_VOLUME         ~0u                 /* transfer length specifying the full volume */
#define BURST_TAG           D32_SIGN            /* EOI tag on all bursts */

static const uint8 zero [8] = { 0, 0, 0, 0, 0, 0, 0, 0 };   /* zero-value status array */

static const OPERATING_STATE power_on_state = { /* initial operating state */
    0,                                          /*   address */
    FULL_VOLUME,                                /*   length */
    0,                                          /*   burst size in bytes */
    { 0, 0, 0, 0, 0, 0, 0, 0 },                 /*   status */
    0,                                          /*   release mode */
    No_Options,                                 /*   options */
    0                                           /*   return addressing mode */
    };


/* Unit state declarations.

   State variables for each CS/80 unit consist of the information needed to
   perform a single transaction (command, execution, and reporting phases), as
   well as that information that must persist between transactions, such as the
   transaction status.  In addition, some frequently used information that could
   be obtained elsewhere, such as the unit's block size or the volume size, is
   included here to avoid repeated table lookups during operational access.

   The state fields are divided into three sections, corresponding to unique
   operational information (the current and persistent operating states, the
   quick status byte, the full status bytes, etc.), copies of derived
   information (the CS/80 unit number, a pointer to the SIMH unit associated
   with the CS/80 unit, and a back-pointer to the enclosing device state
   structure), and copies of Describe information that is used so often during
   read and write operations (the type of drive, the valid volumes within the
   unit, the unit block size, etc.).

   The operational and derived unit state is cleared and recreated during a
   power-on reset.  The Describe information is set when the unit is
   initialized prior to beginning instruction execution.


   Implementation notes:

    1. Because a command can be initiated while a unit is reporting errors,
       i.e., has QSTAT = 1, we must have a separate flag to indicate that an
       error has occurred in the current transaction in order to report an
       Unrecoverable Data Overflow.  We cannot use the presence of an Error_Wait
       state because data errors do not set the error state.

    2. We keep the unit number and a back-pointer to the containing device state
       because pointers to isolated unit structures are often passed to
       routines that need to determine the unit number and device structure.
       These can be obtained via the SIMH unit pointer to the flags field that
       contains the bus address and unit number, but uptrs are only assigned
       when the corresponding unit is enabled, so most unit state structures
       have NULL uptrs.  Also, the device controller is scheduled using that
       device's unit 0 pointer, so using that to derive the unit number will be
       incorrect.  Therefore these fields aren't entirely redundant.
*/

typedef struct device_tag DEVICE_STATE;         /* an incomplete declaration of the device state */

typedef DEVICE_STATE *DSPTR;                    /* a pointer to the device state structure */

typedef struct {                                /* unit state */
    OPERATING_STATE  current;                   /*   current operating state */
    OPERATING_STATE  persistent;                /*   persistent operating state */
    uint32           qstat;                     /*   unit quick status */
    uint8            status [8];                /*   unit status */
    t_bool           interlocked;               /*   TRUE if QSTAT = 2 has not been reported to the host */
    t_bool           data_error;                /*   TRUE if a data error occurs in the current transaction */
    t_bool           release_pending;           /*   TRUE if a release is pending */
    uint32           bad_address;               /*   the address of a block with a data error */
    uint32           delay_index;               /*   index into the realistic time delay table */

    uint32           unit;                      /*   the unit number */
    UPTR             uptr;                      /*   a pointer to the associated SIMH unit */
    DSPTR            dsptr;                     /*   a back-pointer to the enclosing device state */

    t_bool           tape_drive;                /*   TRUE if the unit is a tape drive */
    uint32           valid_volumes;             /*   bitmap of installed volumes */
    uint32           block_size;                /*   per-block data size in bytes */
    uint32           block_stride;              /*   per-block record size in bytes */
    uint32           track_size;                /*   number of blocks available per track */
    uint32           step_size;                 /*   number of blocks available per actuator step */
    uint32           volume_size;               /*   number of blocks in the volume */
    } UNIT_STATE;

typedef UNIT_STATE *USPTR;                      /* a pointer to the unit state record */


/* Device state declarations.

   State variable for each CS/80 device includes controller information, such as
   the block data buffer, size, current index, remaining buffer count, the
   currently addressed CS/80 unit and volume, and a bitmap of units with pending
   status.  The latter is needed to report units needing attention to the host
   in a Request Status command.  The state also includes unit state structures
   for the device's units.

   Implementation notes:

    1. To simplify allocation and accessing, each device has a full set of unit
       state structures, even though only one or two drive units are defined by
       the current model number.

    2. We must depart from our standard of defining structures via typedefs to
       permit the unit state structure to have a back-pointer to the enclosing
       device state.
*/

struct device_tag {                             /* bus device state */
    CN_STATE    state;                          /*   controller state */
    CN_OPCODE   opcode;                         /*   controller opcode */
    uint8       buffer [1024];                  /*   the parameter / data block buffer */
    uint32      size;                           /*   current logical size of the buffer */
    uint32      index;                          /*   current index into the buffer */
    uint32      count;                          /*   remaining byte count */
    uint32      burst_count;                    /*   remaining burst count */
    uint32      step_count;                     /*   remaining blocks before positioner motion required */
    uint32      unit;                           /*   current unit number */
    uint32      volume;                         /*   current volume number */
    uint32      pending;                        /*   bitmap of units with status pending */
    uint32      valid_units;                    /*   bitmap of valid units */
    UNIT_STATE  units [UNIT_COUNT];             /*   unit state for units 0-2 and 15 */
    };


/* Channel state declarations.

   Channel state variables include bitmaps of those devices currently installed
   on the bus, currently checking bus parity, and currently addressed as
   listeners, the currently addressed talker, the current set of parallel poll
   responses from the installed devices, and the last primary channel command.


   Implementation notes:

    1. In hardware, each device maintains its own channel state.  To simulate
       this exactly, each installed device would have to receive data sourced to
       the bus and would have to be queried during a parallel poll.  It would be
       up to the device to determine if it was addressed to listen or talk and
       whether its poll response was enabled.  This is inefficient.  By
       maintaining these indications in a simulated channel state, only the
       active listener and talker devices are called to examine a sourced byte.

    2. The Identify command is an Untalk immediately followed by a secondary
       address.  This sequence requires knowing that an Untalk was the last
       primary command received.  We must save that information here, rather
       than locally in the bus acceptor routine, to ensure that it persists
       across a SAVE/RESTORE command sequence.
*/

typedef struct {                                /* channel state */
    BUS_CABLE     bus;                          /*   the current HP-IB state */
    uint32        present;                      /*   bus address bitmap of devices present on the bus */
    uint32        parity;                       /*   bus address bitmap of parity checking devices */
    uint32        listeners;                    /*   bus address bitmap of the listeners */
    uint32        talker;                       /*   bus address of the talker */
    t_bool        active;                       /*   TRUE if a channel transaction is active */
    t_bool        poll_active;                  /*   TRUE if a parallel poll is in progress */
    BUS_DATA      response_set;                 /*   set of parallel poll responses */
    BUS_DATA      command;                      /*   last channel command */
    DEVICE_STATE  devices [DEVICE_COUNT];       /*   device state, indexed by bus address */
    DELAY_PROPS   fast_times;                   /*   optimized times, indexed by DELAY */
    } DC_STATE;


/* Multicard structure declarations */

typedef struct {                                /* DC card instance structure */
    DEVICE    *dptr;                            /*   a pointer to the device instance */
    DC_STATE  *sptr;                            /*   a pointer to the state instance */
    } CARD_ENTRY;


/* CS/80 local state declarations.

   The local state consists of the DC state structure.  The original device's
   state structure is used as a template to create a new state instance for each
   new device instance.
 */

static DC_STATE dc_state;


/* CS/80 local SCP support routine declarations */

static t_stat dc_service    (UNIT   *uptr);
static t_stat dc_reset      (DEVICE *dptr);
static t_stat dc_boot       (int32  unit_number, DEVICE *dptr);
static t_bool dc_initialize (DEVICE *dptr, DIB *dibptr);
static uint32 dc_adjust     (DEVICE **dvptr, int32 delta);
static t_stat dc_attach     (UNIT   *uptr, char *cptr);
static t_stat dc_release    (UNIT   *uptr);
static t_stat dc_detach     (UNIT   *uptr);
static t_stat dc_set_model  (UNIT   *uptr, int32 value, char *cptr, void *desc);
static t_stat dc_set_unit   (UNIT   *uptr, int32 value, char *cptr, void *desc);


/* CS/80 local bus routine declarations */

static ACCEPTOR  dc_hpib_accept;
static REPORTER  dc_report;


/* CS/80 local utility routine declarations */

static void   process_secondary (uint32 dc_id, uint32 address, BUS_CABLE bus);
static void   process_data      (uint32 dc_id, uint32 address, BUS_CABLE bus);
static void   execute_command   (uint32 dc_id, uint32 address, BUS_CABLE bus);

static t_bool validate_access   (uint32 dc_id, USPTR usptr, ACCESS_TYPE access);
static int32  seek_block        (uint32 dc_id, USPTR usptr);
static t_bool read_block        (uint32 dc_id, USPTR usptr);
static t_bool write_block       (uint32 dc_id, USPTR usptr);

static t_stat activate_unit     (uint32 dc_id, USPTR usptr, DELAY delay);
static void   initialize_unit   (USPTR  usptr);
static t_stat initialize_media  (uint32 dc_id, USPTR usptr, uint32 starting_block, MEDIA_STATE new_state);
static t_bool set_block         (USPTR  usptr);
static t_bool set_chs           (USPTR  usptr);
static void   set_error         (uint32 dc_id, USPTR usptr, ERROR_STATUS error);
static void   set_data_error    (uint32 dc_id, USPTR usptr, t_stat cause);
static t_stat report_io_error   (UPTR   uptr, char *source);

static void   channel_abort     (uint32 dc_id, BUS_DATA bus_group);
static void   set_response      (uint32 dc_id, uint32 address, FLIP_FLOP response);
static void   clear_device      (uint32 dc_id, uint32 address);


/* HP-IB registration declaration */

static const BUS_REGISTER dc_registration = {   /* bus registration */
    DC_Index,                                   /*   the dispatcher table index */
    { &dc_report,                               /*   a pointer to the protocol reporting routine */
      &dc_hpib_accept }                         /*   a pointer to the bus acceptor routine */
    };


/* Interface SCP data structure declarations */


/* Device information block */

static DIB dc_dib = {
    NULL,                                       /* the device's IOP interface function pointer */
    &dc_initialize,                             /* the execution initialization function pointer */
    DEVNO_UNUSED,                               /* the device number */
    SRNO_UNUSED,                                /* the service request number */
    INTPRI_UNUSED,                              /* the interrupt priority */
    INTMASK_UNUSED,                             /* the interrupt mask */
    0,                                          /* the card index */
    CLEAR,                                      /* the interrupt request flip-flop */
    CLEAR,                                      /* the interrupt active flip-flop */
    FALSE,                                      /* TRUE if service has been requested */
    NULL,                                       /* the device's IMB interface function pointer */
    CHAN_UNUSED,                                /* the IMB channel number */
    0                                           /* the IMB slot number */
    };


/* Unit list.

   SIMH units are enabled and assigned to CS/80 units as needed.  For
   efficiency, only one unit is initially enabled.  Before instruction execution
   is allowed to begin, all defined units for an enabled device must be
   assigned.  This ensures that if a device is present on the bus, it is fully
   supported with all defined units accessible.


   Implementation notes:

    1. The unit capacity is set dynamically, depending on the model number
       assigned.  The initial capacity is set in the device power-on reset
       routine.

    2. We use the extended SIMH tape format to accommodate the "erased data
       block" private marker.
*/

#define UNIT_FLAGS  (TO_UNIT (0) | UNIT_FIX | UNIT_ATTABLE | UNIT_ROABLE | UNIT_DISABLE | MT_F_EXT)

static UNIT dc_unit [SIM_UNITS] = {             /* unit array, indexed by SIMH unit number */
/*           Event Routine  Unit Flags                                           Capacity  */
/*           -------------  ---------------------------------------------------  --------- */
    { UDATA (&dc_service,   UNIT_7933A | TO_ADDRESS (0) | UNIT_FLAGS,               0)     },
    { UDATA (&dc_service,   UNIT_7933A | TO_ADDRESS (1) | UNIT_FLAGS | UNIT_DIS,    0)     },
    { UDATA (&dc_service,   UNIT_7933A | TO_ADDRESS (2) | UNIT_FLAGS | UNIT_DIS,    0)     },
    { UDATA (&dc_service,   UNIT_7933A | TO_ADDRESS (3) | UNIT_FLAGS | UNIT_DIS,    0)     },
    { UDATA (&dc_service,   UNIT_7933A | TO_ADDRESS (4) | UNIT_FLAGS | UNIT_DIS,    0)     },
    { UDATA (&dc_service,   UNIT_7933A | TO_ADDRESS (5) | UNIT_FLAGS | UNIT_DIS,    0)     },
    { UDATA (&dc_service,   UNIT_7933A | TO_ADDRESS (6) | UNIT_FLAGS | UNIT_DIS,    0)     },
    { UDATA (&dc_service,   UNIT_7933A | TO_ADDRESS (7) | UNIT_FLAGS | UNIT_DIS,    0)     },
    };


/* Register list.

   We provide register access to the optimized timing table to allow the user to
   alter these if desired.  CS/80 hardware devices do not have externally
   accessible registers, but we provide a few "pseudo-registers" to provide a
   view into the device controller state.


   Implementation notes:

    1. We do not need a register to save the "uptr->pos" values for a RESTORE
       because attached files are repositioned during bus initialization.  Their
       associated "pos" fields are reset at that time before execution resumes.
*/

static REG dc_reg [] = {
/*    Macro   Name     Location           Field   Radix  Width  Offset  Depth          Flags              */
/*    ------  -------  -----------------  ------  -----  -----  ------  -------------  ------------------ */
    { SRDATA (OPCODE,  dc_state.devices,  opcode,  10,     6,      0,   DEVICE_COUNT), PV_LEFT | REG_RO  },
    { SRDATA (UNIT,    dc_state.devices,  unit,    10,     4,      0,   DEVICE_COUNT), PV_LEFT | REG_RO  },
    { SRDATA (CSTATE,  dc_state.devices,  state,   10,     6,      0,   DEVICE_COUNT), PV_LEFT | REG_RO  },

    { DRDATA (CTIME,   dc_state.fast_times [Controller_Time], 24),                     PV_LEFT | REG_NZ  },
    { DRDATA (STIME,   dc_state.fast_times [Status_Time],     24),                     PV_LEFT | REG_NZ  },
    { DRDATA (KTIME,   dc_state.fast_times [Seek_Time],       24),                     PV_LEFT | REG_NZ  },
    { DRDATA (TTIME,   dc_state.fast_times [Traverse_Time],   24),                     PV_LEFT | REG_NZ  },
    { DRDATA (PTIME,   dc_state.fast_times [Position_Time],   24),                     PV_LEFT | REG_NZ  },
    { DRDATA (BTIME,   dc_state.fast_times [Block_Time],      24),                     PV_LEFT | REG_NZ  },
    { DRDATA (DTIME,   dc_state.fast_times [Data_Time],       24),                     PV_LEFT | REG_NZ  },
    { DRDATA (RTIME,   dc_state.fast_times [Residue_Time],    24),                     PV_LEFT | REG_NZ  },

    { SVDATA (STATE,   dc_state)                                                                         },

    { NULL }
    };


/* Modifier list */

#define ETAB_DEVICE         (MTAB_EDV)
#define ETAB_CHANNEL        (MTAB_EDV | MTAB_ERX (10) | MTAB_EMN (2))
#define ETAB_UNIT           (MTAB_EUN | MTAB_ERX (10))

static MTAB dc_mod [] = {
/*    Mask Value   Match Value  Print String      Match String  Validation      Display  Descriptor */
/*    -----------  -----------  ----------------  ------------  --------------  -------  ---------- */
/*  { UNIT_MODEL,  UNIT_7908A,  "7908A opt.140",  "7908A",      &dc_set_model,  NULL,    NULL       }, */
/*  { UNIT_MODEL,  UNIT_7908T,  "7908A",          "7908T",      &dc_set_model,  NULL,    NULL       }, */
    { UNIT_MODEL,  UNIT_7911A,  "7911A opt.140",  "7911A",      &dc_set_model,  NULL,    NULL       },
/*  { UNIT_MODEL,  UNIT_7911T,  "7911A",          "7911T",      &dc_set_model,  NULL,    NULL       }, */
    { UNIT_MODEL,  UNIT_7912A,  "7912A opt.140",  "7912A",      &dc_set_model,  NULL,    NULL       },
/*  { UNIT_MODEL,  UNIT_7912T,  "7912A",          "7912T",      &dc_set_model,  NULL,    NULL       }, */
    { UNIT_MODEL,  UNIT_7914A,  "7914A opt.140",  "7914A",      &dc_set_model,  NULL,    NULL       },
/*  { UNIT_MODEL,  UNIT_7914T,  "7914A",          "7914T",      &dc_set_model,  NULL,    NULL       }, */
    { UNIT_MODEL,  UNIT_7933A,  "7933A",          "7933A",      &dc_set_model,  NULL,    NULL       },
    { UNIT_MODEL,  UNIT_7935A,  "7935A",          "7935A",      &dc_set_model,  NULL,    NULL       },
    { UNIT_MODEL,  UNIT_7941A,  "7941A",          "7941A",      &dc_set_model,  NULL,    NULL       },
/*  { UNIT_MODEL,  UNIT_7942A,  "7942A",          "7942A",      &dc_set_model,  NULL,    NULL       }, */
    { UNIT_MODEL,  UNIT_7945A,  "7945A",          "7945A",      &dc_set_model,  NULL,    NULL       },
/*  { UNIT_MODEL,  UNIT_7946A,  "7946A",          "7946A",      &dc_set_model,  NULL,    NULL       }, */
    { UNIT_MODEL,  UNIT_7957B,  "7957B",          "7957B",      &dc_set_model,  NULL,    NULL       },
    { UNIT_MODEL,  UNIT_7958B,  "7958B",          "7958B",      &dc_set_model,  NULL,    NULL       },
    { UNIT_MODEL,  UNIT_7959B,  "7959B",          "7959B",      &dc_set_model,  NULL,    NULL       },
/*  { UNIT_MODEL,  UNIT_9144A,  "9144A",          "9144A",      &dc_set_model,  NULL,    NULL       }, */
/*  { UNIT_MODEL,  UNIT_9145A,  "9145A",          "9145A",      &dc_set_model,  NULL,    NULL       }, */

/*    Mask Value     Match/Max    Print String        Match String  Valid  Display  Descr  4.x   Expansion Flags */
/*    ------------  ------------  ------------------  ------------  -----  -------  -----  ----  --------------- */
    { DEV_REALTIME, DEV_REALTIME, "realistic timing", "REALTIME",   NULL,  NULL,    NULL,  NULL, ETAB_DEVICE     },
    { DEV_REALTIME, 0,            "fast timing",      "FASTTIME",   NULL,  NULL,    NULL,  NULL, ETAB_DEVICE     },
    { DEV_CHANNEL,  15,           "channel",          "CHANNEL",    NULL,  NULL,    NULL,  NULL, ETAB_CHANNEL    },

    { UNIT_ADDRESS, 7,            "bus",              "BUS",        NULL,  NULL,    NULL,  NULL, ETAB_UNIT       },
    { UNIT_UNIT,    1,            "unit",             NULL,         &dc_set_unit, \
                                                                           NULL,    NULL,  NULL, ETAB_UNIT       },

    { 0 }
    };


/* Debugging trace list */

static DEBTAB dc_deb [] = {                     /* debugging trace list */
    { "CMD",   TRACE_CMD   },                   /*   trace interface and controller commands */
    { "INCO",  TRACE_INCO  },                   /*   trace interface and controller command initiations and completions */
    { "STATE", TRACE_STATE },                   /*   trace state changes */
    { "SERV",  TRACE_SERV  },                   /*   trace unit service scheduling calls and entries */
    { NULL,    0           }
    };

/* Device descriptor.

   Defining the device as having a 16-bit data width with an address increment
   of 2 means that attached file addresses are byte addresses but that files may
   be examined and deposited as either bytes or words.  Calling the HP word
   examine and deposit routines ensures that words are processed in big-endian
   (MSB first) format, regardless of the host platform convention.
*/

DEVICE dc_dev = {
    "DC",                                       /* device name */
    dc_unit,                                    /* unit array */
    dc_reg,                                     /* register array */
    dc_mod,                                     /* modifier array */
    SIM_UNITS,                                  /* number of units */
    10,                                         /* address radix */
    32,                                         /* address width = 4 GB */
    2,                                          /* address increment */
    8,                                          /* data radix */
    16,                                         /* data width */
    &hp_word_examine,                           /* examine routine */
    &hp_word_deposit,                           /* deposit routine */
    &dc_reset,                                  /* reset routine */
    &dc_boot,                                   /* boot routine */
    &dc_attach,                                 /* attach routine */
    &dc_release,                                /* detach routine */
    &dc_dib,                                    /* device information block pointer */
    DEV_DISABLE | DEV_DEBUG | TO_CHANNEL (11),  /* device flags */
    0,                                          /* debug control flags */
    dc_deb,                                     /* debug flag name table */
    NULL,                                       /* memory size change routine */
    NULL,                                       /* logical device name */
    NULL,                                       /* (4.0 dummy) help routine */
    NULL,                                       /* (4.0 dummy) help attach routine */
    NULL,                                       /* (4.0 dummy) help context */
    NULL,                                       /* (4.0 dummy) description */
    dc_adjust                                   /* the device adjustment function pointer */
    };


/* CS/80 local allocation state declarations */

static uint32 dc_count = 1;                     /* the current count of DC device instances */

static CARD_ENTRY dcs [MAX_DC_COUNT] = {        /* DC bus entries, indexed by instance ID */
    { &dc_dev, &dc_state }                      /*   instance 0 (original) */
    };



/************************************
 *                                  *
 * CS/80 local SCP support routines *
 *                                  *
 ************************************/


/* Service a CS/80 drive I/O event.

   This service routine is called to provide command timing and to control the
   transfer of data to and from the HP-IB bus.  The actions to be taken depend
   on the current state of the device's CS/80 controller, as follows:

     Execution_Wait, Optional_Wait, Reporting_Wait, Error_Wait
     ---------------------------------------------------------
     Entry in this state indicates that the controller is ready to receive an
     execution message or a reporting message.  For execution and optional
     waits, parallel poll enabling is skipped if a Read Loopback or Write
     Loopback command is executing.  Otherwise, parallel poll is enabled to
     notify the host to send the next message.

     Execution_Send
     --------------
     Commands that send data to the CPU enter the service routine to source a
     byte to the bus.  Bytes are transferred only when ATN and NRFD are denied;
     if they are not, we simply exit, as we will be reentered from our bus
     responder when the lines are dropped.  Otherwise, we get a byte from the
     data buffer and source it to the bus.  If the buffer is now empty, it is
     refilled if necessary to continue.  If the transfer is complete, the
     controller moves to the reporting phase of the command.  Otherwise, the
     next data byte transfer is scheduled.

     If the listeners have stopped listening, a Message Length error occurs, and
     the transfer is aborted.

     Execution_Receive
     -----------------
     Commands that receive data from the CPU enter the service routine to
     determine whether or not to continue the transfer.  Our bus accept routine
     has already stored the received byte in the data buffer and has asserted
     NRFD to hold off the talker.

     For a Locate and Write command, if there are more bytes to transfer, then
     if the buffer is full, then it is written to the currently addressed block.
     NRFD is denied to allow the talker to deliver the next byte from the CPU;
     event service will be reentered when the next byte arrives.  If the
     transfer is complete, then the final block is written, and the reporting
     phase is entered.

     For a Write Loopback command, if the buffer is full, it is compared to the
     pattern preloaded into the second block of the buffer.  A comparison
     failure results in a Channel Parity error, and the remaining bytes
     transferred from the host are sunk; when a byte tagged with EOI arrives,
     the controller moves to the (required) reporting phase.  If the comparison
     succeeds, the buffer is reset to accept another block of loopback data.  If
     there are more bytes to transfer, NRFD is denied to allow the talker to
     deliver the next byte from the CPU; event service will be reentered when
     the next byte arrives.  Otherwise, the controller moves to the optional
     reporting phase.

     Error_Source, Error_Sink
     ------------------------
     If an error occurs during the execution phase of a read or status command,
     a dummy byte tagged with EOI is sourced to the bus, and parallel poll
     response is enabled.  This allows the OS driver for the controller to
     terminate the command and request the device's status.

     If an error occurs during the execution phase of a write command, dummy
     bytes are sunk from the bus until the transfer length is reached or EOI is
     seen.  Parallel poll response is then enabled to notify the OS driver to
     complete the command as expected and then determine the cause of the
     failure by requesting the device's status.

     Command_Ready, Channel_Wait, Command_Wait, Parameter_Wait
     ---------------------------------------------------------
     Entry in any of these state is an error.  These states occur while
     receiving command bytes from the controller or while waiting for the
     controller to begin a command message.  None of the states involve
     scheduled events.  Entry in an invalid state will return an Internal Error
     to cause a simulator stop.

   Exit from the routine will be either in one of the above states, or in the
   Command_Ready state if the operation has no reporting phase.


   Implementation notes:

    1. At most one unit for a given device will be scheduled at a time.
       Although the CS/80 manual says that units "are capable of parallel
       operation," commands cannot overlap.  Consider a disc/tape combination
       device.  We want to issue Locate and Read commands and have the seeks
       overlap, so we issue command phases sequentially to both units.  The
       controller will enable the parallel poll response when the first seek
       completes.  However, there is only one poll response for the device, so
       when the poll response is seen, the host doesn't know which unit's
       execution message is to be sent.  Consequently, it is safe to assume that
       the unit field of the device state whose address is in the uptr->flags
       field indicates the unit being serviced.  We can't depend on the unit
       number in the flags field because both unit 0 and unit 15 are scheduled
       on the uptr corresponding to unit 0 (for example, the execution phase of
       the Request Status or Describe commands when they are directed to unit
       15).

    2. When an Identify command completes, we must untalk the current device.
       In hardware, devices respond to their secondary address following an
       Untalk.  In simulation, the addressed device must be marked as the
       current talker so that the bus responder can reschedule the proper SIMH
       unit when the controller denied ATN to permit return of the identity
       bytes.  Untalking this device after Identify completes returns the bus to
       its proper state as in hardware.

       In addition, if the Identify interrupted an executing command, we restore
       the remaining time, current opcode, and current state from the UNIT
       structure, and reschedule the service before returning.

    3. The reporting phase could be completed in the bus accept routine.  It is
       serviced here instead to avoid presenting a zero command execution time
       to the CPU.

    4. Entry in the execution wait or reporting wait states cannot perform any
       action, such as end-of-command processing, other than enabling parallel
       poll.  This is because the host is not obligated to wait for PPR before
       issuing the execution or reporting secondaries.  If the secondary beats
       the service entry, activation is canceled, and this routine is never
       entered in those states.

    5. Execution messages transfer bytes until the defined length is reached.
       If an execution message is terminated early, either by an Untalk, an
       Unlisten, or by receiving a byte tagged with EOI before the count is
       exhausted, a Message Length error is logged.

    6. After processing a byte in the Execution Receive state, NRFD is denied to
       allow the talker to resume the transfer.  If the talker has another byte
       ready to send, "dc_bus_accept" will be called to accept the byte, which
       calls "process_data" to add the byte to the buffer.  Normally, an event
       service is then scheduled, and reentry here completes the cycle.
       However, if the message ends abnormally -- either the remaining length is
       > 0 but the byte is tagged with EOI, or vice versa -- "process_data" will
       log a Message Sequence error, which changes the state to Error_Wait or
       Error_Sink, respectively. If tracing is enabled, the routine will report
       the state change before exiting.  Then, when the call chain unwinds and
       returns here, we also report the state change before exiting.  As a
       result, the change is reported twice.

    7. A read execution message will also be terminated early if the acceptor
       quits listening.  In hardware, transmission stops because the listener
       denies NDAC and NRFD (the HP-IB handshake requires NDAC and NRFD to be
       asserted to start the handshake sequence; TACS * SDYS * ~NDAC * ~NRFD is
       an error condition).  In simulation, we handle this by terminating a read
       transfer if the listener stops accepting, i.e., if NDAC is asserted in
       the returned bus state.  If we did not, then the drive would continue to
       source bytes to the bus, overflowing the listener's FIFO.
*/

static t_stat dc_service (UNIT *uptr)
{
const uint32    dc_id       = uptr->Instance;               /* the instance identifier */
DC_STATE *const dc          = dcs [dc_id].sptr;             /* the pointer to the channel state */
DEVICE   *const dptr        = dcs [dc_id].dptr;             /* the pointer to the controlling device */
const uint32    channel     = CHANNEL (dptr->flags);        /* the channel address of the bus controller */
const uint32    address     = ADDRESS (uptr->flags);        /* the bus address of the device containing the unit */
const DSPTR     dsptr       = &dc->devices [address];       /* the pointer to the device state */
const uint32    unit        = dsptr->unit;                  /* the corresponding CS/80 unit number */
const USPTR     usptr       = &dsptr->units [MAPUS (unit)]; /* the pointer to the current unit state */
const CN_STATE  entry_state = dsptr->state;                 /* the controller state at entry */
const BUS_CABLE error_byte  = Bus_DAV | Bus_EOI | 1;        /* the value returned for a transaction error */
uint32          bus;
t_bool          completed = FALSE;
t_stat          result = SCPE_OK;

tpprintf (dptr, TRACE_SERV, "Device %u unit %u state %s service entered\n",
          address, unit, state_names [entry_state]);

switch (entry_state) {                                  /* dispatch the controller state */

    case Execution_Wait:                                /* waiting for an execution message */
    case Optional_Wait:                                 /* waiting for an optional reporting message */
        if (dsptr->opcode == Read_Loopback              /* if a loopback command is executing */
          || dsptr->opcode == Write_Loopback)           /*   the skip the poll response enable */
          break;

    /* fall through into Reporting_Wait case */

    case Reporting_Wait:                                /* waiting for a reporting message */
    case Error_Wait:                                    /* waiting for error recovery */
        set_response (dc_id, address, SET);             /* enable PPR to prompt the host */
        break;

    case Execution_Send:                                    /* waiting to send data to the interface */
        if (not (dc->bus & (Bus_ATN | Bus_NRFD))) {         /* if the bus is ready for data */
            bus = Bus_DAV | dsptr->buffer [dsptr->index++]; /*   then put the next byte on the bus */
            dsptr->count--;                                 /*     and count it */

            switch (dsptr->opcode) {                    /* dispatch the interface command */

                case Locate_and_Read:                   /* Locate and Read */
                case Cold_Load_Read:                    /* Cold Load Read */
                    usptr->current.length--;            /* count the byte against the transfer length */

                    if (usptr->current.burst > 0) {     /* if burst mode is active */
                        dsptr->burst_count--;           /*   then count the byte */

                        if (dsptr->burst_count == 0) {      /* if this is the end of the burst */
                            dsptr->state = Execution_Wait;  /*   then wait for the next execution message */
                            dc->active = FALSE;             /*     and end this transaction */

                            if (usptr->current.burst & BURST_TAG)   /* if each burst is tagged */
                                bus |= Bus_EOI;                     /*   then tag the byte with EOI */

                            tpprintf (dptr, TRACE_INCO, "Device %u unit %u end of burst\n",
                                      address, unit);
                            }
                        }

                    if (usptr->current.length == 0)     /* if the transfer is complete */
                        bus |= Bus_EOI;                 /*   then tag the byte with EOI */

                    dc->bus = hpib_source (channel, address, bus);  /* send the byte to the bus */

                    if (dc->bus & Bus_NDAC) {                       /* if the PHI is not listening */
                        set_error (dc_id, usptr, Message_Length);   /*   then log the error */
                        completed = TRUE;                           /*     and terminate the read */
                        }

                    else if (usptr->current.length == 0)    /* otherwise if the transfer is now complete */
                        completed = TRUE;                   /*   then terminate the read */

                    else if (dsptr->count == 0) {       /* otherwise if the block is complete */
                        uptr->wait = 0;                 /*   then initialize the seek delay */

                        if (read_block (dc_id, usptr))                      /* read the next block; if it succeeds */
                            activate_unit (dc_id, usptr, Position_Time);    /*   then schedule the next transfer */

                        else {                                          /* otherwise the read failed */
                            dsptr->state = Error_Source;                /*   so source a termination byte */
                            activate_unit (dc_id, usptr, Data_Time);    /*     and schedule its transmission */
                            }
                        }

                    else                                            /* otherwise the execution phase continues */
                        activate_unit (dc_id, usptr, Data_Time);    /*   so schedule the next transfer */
                    break;


                case Initiate_Utility_Read:             /* Initiate Utility (send execution) */
                case Request_Status:                    /* Request Status */
                case Describe:                          /* Describe */
                case Quick_Status:                      /* Quick Status */
                case Identify:                          /* Amigo Identify */
                    if (dsptr->count == 0)              /* if the transfer is complete */
                        bus |= Bus_EOI;                 /*   then tag the last byte with EOI */

                    dc->bus = hpib_source (channel, address, bus);  /* send the byte to the bus */

                    if (dc->bus & Bus_NDAC) {                           /* if the PHI is not listening */
                        if (dsptr->opcode == Identify)                  /*   then if this is an Identify command */
                            dsptr->state = Command_Ready;               /*     then not listening is accepted */
                        else                                            /* otherwise */
                            set_error (dc_id, usptr, Message_Length);   /*   log the error */

                        completed = TRUE;               /* terminate the command */
                        }

                    else if (dsptr->count == 0) {       /* if all bytes have been sent */
                        completed = TRUE;               /*   then terminate the command */

                        if (dsptr->opcode == Quick_Status) {    /* if the QSTAT was sent */
                            dsptr->state = Command_Ready;       /*   then end the transaction */
                            usptr->interlocked = FALSE;         /*     and clear the unseen interlock */
                            }

                        else if (dsptr->opcode == Request_Status) {             /* otherwise if the status was sent */
                            memset (usptr->status, 0, sizeof usptr->status);    /*   then clear the status fields */
                            usptr->qstat = 0;                                   /*     and the quick status value */
                            }

                        else if (dsptr->opcode == Identify) {   /* otherwise if the ID was sent */
                            dsptr->state = Command_Ready;       /*   then end the transaction */
                            dc->talker = BUS_UNADDRESS;         /*     and unaddress the device */
                            }
                        }

                    else                                            /* otherwise the execution phase continues */
                        activate_unit (dc_id, usptr, Status_Time);  /*   so reschedule the next transfer */
                    break;


                case Read_Loopback:                     /* Read Loopback */
                    usptr->current.length--;            /* count the byte against the transfer length */

                    if (usptr->current.length == 0)     /* if the transfer is complete */
                        bus |= Bus_EOI;                 /*   then tag the byte with EOI */

                    else if (dsptr->count == 0) {       /* otherwise if the set is complete */
                        dsptr->index = 0;               /*   then point back at the first byte */
                        dsptr->count = 256;             /*     and send another set of bytes */
                        }

                    dc->bus = hpib_source (channel, address, bus);  /* send the byte to the bus */

                    if (dc->bus & Bus_NDAC) {                       /* if the PHI is not listening */
                        set_error (dc_id, usptr, Message_Length);   /*   then log the error */
                        completed = TRUE;                           /*     and terminate the read */
                        }

                    else if (usptr->current.length == 0) {  /* otherwise if the transfer is now complete */
                        completed = TRUE;                   /*   then terminate the command */
                        dsptr->state = Optional_Wait;       /*     and wait for an optional reporting message */
                        }

                    else                                            /* otherwise the execution phase continues */
                        activate_unit (dc_id, usptr, Data_Time);    /*   so schedule the next transfer */
                    break;


                default:                                        /* no other commands send data */
                    set_error (dc_id, usptr, Message_Sequence); /*   so log a sequence error */

                    dc->bus = hpib_source (channel, address, error_byte);  /* source a dummy byte to the bus */
                    completed = TRUE;                                       /*   and terminate the command */
                    break;
                }                                           /* end of execution send dispatch */
            }
        break;


    case Execution_Receive:                             /* waiting to receive data from the interface */
        switch (dsptr->opcode) {                        /* dispatch the interface command */

            case Locate_and_Write:                      /* Locate and Write */
                if (usptr->current.length > 0) {        /* if there are more bytes to write */
                    if (dsptr->count == 0) {            /*   then if the block is full */
                        uptr->wait = 0;                 /*     then clear the accumulated wait time */

                        if (write_block (dc_id, usptr))                     /* write the block; if it succeeds */
                            activate_unit (dc_id, usptr, Position_Time);    /*   then schedule the next transfer */

                        else {                                          /* otherwise the write failed */
                            dsptr->state = Error_Sink;                  /*   so sink the remaining bytes */
                            activate_unit (dc_id, usptr, Data_Time);    /*     and schedule the reception */
                            break;
                            }
                        }

                    if (usptr->current.burst > 0) {     /* if burst mode is active */
                        dsptr->burst_count--;           /*   then count the byte */

                        if (dsptr->burst_count == 0) {      /* if this is the end of the burst */
                            dsptr->state = Execution_Wait;  /*   then wait for the next execution message */
                            dc->active = FALSE;             /*     and end this transaction */

                            tpprintf (dptr, TRACE_INCO, "Device %u unit %u end of burst\n",
                                      address, unit);
                            }
                        }
                    }

                else {                                  /* otherwise the transfer is complete */
                    completed = TRUE;                   /*   so terminate the command */

                    uptr->wait = 0;                     /* clear the accumulated wait time */
                    write_block (dc_id, usptr);         /*   and write the final block */

                    dsptr->state = Reporting_Wait;                  /* wait for the reporting message */
                    activate_unit (dc_id, usptr, Position_Time);    /*   and schedule the poll response */
                    }
                break;


            case Write_Loopback:                                    /* Write Loopback */
                if (dsptr->count == 0                               /* if the second block is full */
                  || usptr->current.length == 0)                    /*   or the transfer is complete */
                    if (memcmp (&dsptr->buffer [0],                 /*     then if the two blocks */
                                &dsptr->buffer [256],               /*       do not compare exactly */
                                256 - dsptr->count) != 0) {
                        set_error (dc_id, usptr, Channel_Parity);   /*         then report the error */

                        usptr->interlocked = TRUE;      /* set the interlock to require reporting */
                        dsptr->state = Error_Sink;      /*   and sink the remaining bytes */
                        }

                    else {                              /* otherwise */
                        dsptr->index = 256;             /*   point back at the second block */
                        dsptr->count = 256;             /*     and receive another set of bytes */
                        }

                if (usptr->current.length == 0) {       /* if the transfer is complete */
                    completed = TRUE;                   /*   then terminate the command */

                    if (dsptr->state == Execution_Receive)  /* if the loopback ended successfully */
                        dsptr->state = Optional_Wait;       /*   then reporting is optional */
                    }
                break;


            default:                                        /* no other commands receive data */
                set_error (dc_id, usptr, Message_Sequence); /*   so log a sequence error */
                completed = TRUE;                           /*     and terminate the command */
                break;
            }                                           /* end of execution receive dispatch */

        dc->bus = hpib_source (channel, address, dc->bus & ~Bus_NRFD);  /* deny NRFD to remove the handshake hold */
        break;


    case Error_Source:                                              /* send data after an error */
        if (not (dc->bus & (Bus_ATN | Bus_NRFD))) {                 /* if the bus is ready for data */
            dc->bus = hpib_source (channel, address, error_byte);   /*   then source a dummy byte to the bus */
            completed = TRUE;                                       /*     and terminate the command */
            }
        break;


    case Error_Sink:                                    /* absorb data after an error */
        if (usptr->current.length == 0)                 /* if the transfer is complete */
            completed = TRUE;                           /*   then terminate the command */

        dc->bus = hpib_source (channel, address, dc->bus & ~Bus_NRFD);  /* deny NRFD to remove the handshake hold */
        break;


    default:                                            /* no other states schedule service */
        tpprintf (dptr, TRACE_STATE, "Device %u unit %u state %s not handled\n",
                  address, unit, state_names [entry_state]);

        result = SCPE_IERR;                             /* signal an internal error */
        break;
    }                                                   /* end of interface state dispatch */


if (completed) {                                        /* if the command completed */
    dc->active = FALSE;                                 /*   then end the transaction */

    if (dsptr->state != Command_Ready                   /* if reporting is needed */
      && dsptr->state != Reporting_Wait                 /*   and is not */
      && dsptr->state != Optional_Wait) {               /*   already scheduled */
        if (dsptr->state != Error_Wait)                 /*     then if completion was successful */
            dsptr->state = Reporting_Wait;              /*       then wait for the reporting phase */

        activate_unit (dc_id, usptr, Status_Time);      /* schedule the poll response */
        }

    tpprintf (dptr, TRACE_INCO, "Device %u %s command completed with qstat %u\n",
              address, opcode_names [dsptr->opcode], usptr->qstat);

    if (dsptr->opcode == Identify && uptr->IDC_Time > 0) {      /* if an Identify interrupted a device command */
        dsptr->opcode = IDC_OPCODE (uptr->IDC_Opcode_State);    /*   then restore the command opcode */
        dsptr->state  = IDC_STATE  (uptr->IDC_Opcode_State);    /*     and the interrupted state */

        sim_activate (uptr, uptr->IDC_Time - 1);        /* reschedule the device command */
        }
    }

if (dsptr->state != entry_state)
    tpprintf (dptr, TRACE_STATE, "Device %u transitioned from %s state to %s state\n",
              address, state_names [entry_state], state_names [dsptr->state]);

return result;                                          /* return the result of the service */
}


/* DC reset.

   This routine is called for a RESET or RESET DC command.  With the addition of
   the -P switch, it is the simulation equivalent of applying or cycling power
   on the set of bus-connected devices.  With the -C switch, it performs a
   Device Clear (DCL) operation.  Without either switch, all bus devices are
   idled; this action has no direct hardware analog.

   In hardware, a self-test is performed by each device controller at power-on.
   When the self-test completes, the operating parameters are reset to their
   initial power-on states, the controller logs a Power Fail error, sets QSTAT =
   2, and enables its parallel poll response.

   In simulation, a power-on reset also clears and reinitializes the channel,
   device, and unit state structures.  The following unit state values are set
   explicitly:

     Structure Field  Description
     ---------------  ------------------------------------------------------
     unit             the CS/80 unit number
     dsptr            a back-pointer to the enclosing device state
     current          the current operating state
     persistent       the persistent operating state
     qstat            the quick status value
     status           the full status array
     interlocked      TRUE if QSTAT = 2 has not been reported to the host

   The "tape_drive" field is set indirectly when the initial set of device model
   numbers is established.  The remaining unit state fields are implicitly
   initialized to their default states.

   A DCL reset is equivalent to all devices on the bus receiving a Device Clear
   primary command.  This clears all units of all devices.  In particular, it
   clears the power-on QSTAT and the power-fail interlock.

   A normal reset idles each device by setting the controller state to
   Command_Ready.  All devices are untalked and unlistened, all event service
   activations are canceled, and the original optimized (i.e., FASTTIME) timing
   values are restored.


   Implementation notes:

    1. Power restoration enables parallel poll.  However, we cannot do that here
       because at this point we don't know what devices will be enabled.
       Response enabling is done in the bus initializer if it detects the power
       fail interlock, and the device is in the reporting state.

    2. We set back-pointers to the enclosing device state in each unit state
       structure.  These will cause a problem if a SAVEd state file is RESTOREd
       using an executable compiled with a different compiler, compiler version,
       or target platform, as the addresses of the structures may have changed,
       rendering the saved pointers invalid.  However, the same issue occurs
       with structure field layout, for which there is no solution except for
       naming every field in every structure as a hidden register.  Therefore,
       we state in the user's manual that SAVE/RESTORE is only valid when using
       the same executable, which seems to be a reasonable restriction.
*/

static t_stat dc_reset (DEVICE *dptr)
{
DIB *const   dibptr = (DIB *) dptr->ctxt;
const uint32 dc_id  = dibptr->card_index;
DC_STATE     *dc;
DSPTR        dsptr;
USPTR        usptr;
UPTR         uptr;
uint32       device, unit;

dc = dcs [dc_id].sptr;                                  /* get the state pointer */

if (sim_switches & P_SWITCH) {                          /* if this is a power-on reset */
    hpib_register (dc_registration);                    /*   then register our protocol handler */

    memset (dc, 0, sizeof (DC_STATE));                  /* clear the controller */

    for (device = 0; device < DEVICE_COUNT; device++) { /* loop through the CS/80 devices */
        dsptr = &dc->devices [device];                  /*   and get the device pointer */

        for (unit = 0; unit < UNIT_COUNT; unit++) {     /* loop through the units */
            usptr = &dsptr->units [unit];               /*   and get the unit pointer */

            usptr->dsptr = dsptr;                       /* set the back-pointer to the device state */
            usptr->unit = unit;                         /*   and the unit number */

            if (usptr->unit == UNIT_15)                 /* if the unit number references the controller */
                usptr->unit = 15;                       /*   then correct to the proper unit number */

            set_error (dc_id, usptr, Power_Fail);       /* set power-failed status and enable the interlock */

            usptr->current    = power_on_state;         /* reset the operating parameters */
            usptr->persistent = power_on_state;         /*   to their power-on values */
            }
        }

    for (unit = 0; unit < SIM_UNITS; unit++) {          /* loop through the SIMH units */
        uptr = &dptr->units [unit];                     /*   and get the unit pointer */

        uptr->Instance = dc_id;                         /* set the ID of the controlling instance */
        dc_set_model (uptr, uptr->flags, NULL, NULL);   /*   and set up the default model parameters */
        }
    }

else if (sim_switches & C_SWITCH)                       /* otherwise if this is a device clear reset */
    for (device = 0; device < DEVICE_COUNT; device++) { /*   so loop through the devices */
        dc->devices [device].unit = 15;                 /* set the controller unit */
        clear_device (dc_id, device);                   /*   to clear it and all units */
        }

else                                                    /* otherwise it's a normal reset */
    for (device = 0; device < DEVICE_COUNT; device++)   /*   so loop through the devices */
        dc->devices [device].state = Command_Ready;     /*     and idle the controllers */

memcpy (dc->fast_times, init_time, sizeof dc->fast_times);  /* restore the original optimized timing values */

dc->listeners = 0;                                      /* unaddress all current listeners */
dc->talker    = BUS_UNADDRESS;                          /*   and the current talker */

for (unit = 0; unit < SIM_UNITS; unit++) {              /* loop through the SIMH units */
    uptr = &dcs [dc_id].dptr->units [unit];             /*   and get the unit pointer */

    if (not (uptr->flags & UNIT_DIS))                   /* if the unit is not disabled */
        sim_cancel (uptr);                              /*   then cancel any current activation */
    }

return SCPE_OK;
}


/* Device boot routine.

   This routine is called for the BOOT DC command to initiate the system cold
   load procedure for the disc.  It is the simulation equivalent to presetting
   the System Switch Register to the appropriate device number bytes and then
   pressing the ENABLE+LOAD front panel switches.

   For the Starfish, the switch register is set to the device number of the IMB
   adapter in the lower byte and the device number (channel/device) of the boot
   device in the upper byte.

   The cold load procedure always uses unit 0.
*/

static t_stat dc_boot (int32 unit_number, DEVICE *dptr)
{
const uint32 channel = CHANNEL (dptr->flags);                       /* the channel address of device */
const uint32 device  = ADDRESS (dptr->units [unit_number].flags);   /* the bus address of the device */
const uint32 unit    = UNIT    (dptr->units [unit_number].flags);   /* the CS/80 unit number of the device */

if (unit != 0)                                          /* if a unit other than 0 is specified */
    return SCPE_NOFNC;                                  /*   then fail with a Command not allowed error */

else {                                                      /* otherwise */
    cpu_front_panel (TO_WORD (TO_DEVNO (channel, device),   /*   set up the cold load */
                              IMBA_DEVNO),                  /*     for the Starfish */
                     Cold_Load);

    return SCPE_OK;                                     /* return to run the bootstrap */
    }
}


/* Run-time initialization.

   This bus routine is called during I/O initialization each time machine
   instruction execution is to begin.  The routine returns TRUE to permit
   execution to commence or FALSE to inhibit execution.  Initialization is
   optional for modules but is required in our case to check the instance table
   for channel conflicts and the SIMH unit assignments for model number, bus
   address, or unit conflicts.

   On entry, the "dptr" parameter points at the device instance, and "dibptr"
   points at the device information block.  Channel conflicts are checked first
   by setting up a usage table, indexed by channel number, and then searching
   through the instance table and tallying all of the channel references.  If a
   conflict was seen, then a pass through the usage table is made to identify
   those channels with multiple assignment.  For each one found, a search is
   made of the instance table to identify and print the names of the devices
   with that channel assignment.  Once all channel conflicts are identified, the
   routine returns FALSE, as there's no need to proceed with additional checks.

   If the channel check passes, then addressing conflicts are detected by
   setting up a table of bus addresses and CS/80 unit numbers and counting the
   number of SIMH units for that apply to each.  This table is then used to
   verify that:

     - each SIMH unit has a unique bus address/unit number combination,

     - all SIMH units for a given bus address have the same device model number.

     - all installed CS/80 units for a given device are assigned to SIMH units.

   Each table entry is a conflict counter that records the number of SIMH units
   assigned to the specific bus address and CS/80 unit combination.

   First, the conflict table is initialized to all ones values (zero is a legal
   assignment value, so we have to differentiate between that and unassigned
   entries), and the bitmap of present devices is cleared.  Then each enabled
   SIMH unit is examined (disabled units are "not present"), and the assigned
   device model number, bus address, and CS/80 unit number are determined.

   For the first unit in each device, the map of installed units is obtained
   from the model's CS/80 Describe information, and the conflict counter for
   each installed unit is cleared.  SIMH units are not assigned to the device
   controller units (i.e., unit 15), so the conflict counter for the controller
   (remapped to unit 3) is set to the model number.  If this is not the first
   unit for the device, then if the model numbers disagree, then the conflict
   entry is set to an error value.

   Then the conflict table entry for the specified address/unit combination is
   incremented, and the bus address of the device is entered into the device
   presence bitmap.  If this SIMH unit is assigned to CS/80 unit 0, the unit
   pointer is copied to the device's controller unit pointer (as the controller
   unit has no dedicated SIMH unit, it schedules events on the unit
   corresponding to unit 0).

   If the unit is interlocked and is waiting to report, the parallel poll
   response for the device is enabled.  If the unit is attached, the file
   position is recalculated, and the file is repositioned to return it to its
   correct location.  Doing this here saves us from seeking for each block read
   or written; instead, a seek can be done to start the transfer, and thereafter
   the file can be accessed sequentially.

   Once all of the SIMH units have been entered into the conflict table, the
   table is then scanned in bus address order for problems:

     - If a device's model error flag is set, "device has multiple models" is
       reported.

     - If a unit's reference counter is zero, "device has no assignment for
       unit" is reported.

     - If a unit's reference counter is greater than one, "device has multiple
       assignments for unit" is reported.

   Finally, if any conflicts exist, the routine returns FALSE to inhibit machine
   instruction execution.


   Implementation notes:

    1. In addition to being called as a result of a BOOT, RUN, GO, or CONTINUE
       command, we are also called after any concurrent-mode command, as that
       suspends and then resumes execution.

    2. Unit number assignments are always valid, as they are checked whenever
       the model or unit number is changed.  So we cannot have a case where a
       unit number is assigned to a model that doesn't have such a unit.

    3. Channel presence is recalculated to remove disabled devices.  Disabled
       devices are also removed from the set of devices that are checking
       channel parity.

    4. Every device has a unit 0, so we can test the conflict entry for this
       unit to see if the installed units have been determined.

    5. Repositioning attached files is necessary each time the simulation
       resumes because the user may have indirectly changed the file position,
       e.g., with an EXAMINE command.

    6. Although the routine is called for each instance, only one check of the
       channel assignments needs to be done, as all checks would be identical.
       So we perform the channel check only when we are called for the original
       instance.
*/

static t_bool dc_initialize (DEVICE *dptr, DIB *dibptr)
{
const uint32    dc_id   = dibptr->card_index;   /* the instance ID of the protocol */
DC_STATE *const dc      = dcs [dc_id].sptr;     /* the pointer to the channel state */
const int       not_set = D8_UMAX;              /* initial table value */
const uint8     error   = D8_SMAX;              /* model error value */
DSPTR           dsptr;
USPTR           usptr;
DEVICE          *ckptr;
UPTR            uptr;
MODEL_TYPE      model;
uint32          count, address, unit, index, map, simunit;
uint32          channels [CHANNEL_COUNT];
uint8           conflicts [DEVICE_COUNT] [UNIT_COUNT];
t_bool          coherent = TRUE;

if (dptr == &dc_dev) {                                  /* if initializing the original device */
    memset (channels, 0, sizeof channels);              /*   then clear the channel conflict table */

    for (index = 0; index < dc_count; index++) {        /* search through the instance table */
        address = CHANNEL (dcs [index].dptr->flags);    /*   to get the bus address of the entry */
        channels [address]++;                           /*     and increment the usage count */

        if (channels [address] > 1)                     /* if more than one instance uses the channel */
            coherent = FALSE;                           /*   then the configuration is not coherent */
        }

    if (coherent == FALSE) {                            /* if the conflict check failed */
        sim_ttcmd ();                                   /*   then restore the console and log I/O modes */

        for (address = 0; address < CHANNEL_COUNT; address++)   /* search through the channel usage table */
            if (channels [address] > 1) {                       /*   to find a multiply-assigned channel */
                count = channels [address];                     /*     and get the usage count */

                cprintf ("Channel %u conflict (", address);     /* report the conflicted channel */

                for (index = 0; index < dc_count; index++) {    /* search through the instance table */
                    ckptr = dcs [index].dptr;                   /*   to get the next device pointer */

                    if (CHANNEL (ckptr->flags) == address) {    /* if this device has the same channel */
                        if (count < channels [address])         /*   then if this is not the first match */
                            cputs (" and ");                    /*     then add a spacer */

                        cputs (sim_dname (ckptr));      /* report the conflicting device name */
                        count = count - 1;              /*   and drop the count of remaining conflicts */

                        if (count == 0)                 /* if all devices have been reported */
                            break;                      /*   then there's no need to look further */
                        }
                    }                                   /* loop until all conflicting devices are reported */

                cputs (")\n");                          /* tie off the line */
                }                                       /*   and continue to look for other channel conflicts */

        return FALSE;                                   /* report that initialization has failed */
        }
    }


memset (conflicts, not_set, sizeof conflicts);          /* initialize the addressing conflict table */

dc->present = 0;                                        /* reset the map of installed devices */

for (simunit = 0; simunit < SIM_UNITS; simunit++) {     /* loop through the SIMH units */
    uptr = &dptr->units [simunit];                      /*   and get the unit pointer */

    if (not (uptr->flags & UNIT_DIS)) {                 /* if the unit is not disabled */
        model   = MODEL   (uptr->flags);                /*   then get the model */
        address = ADDRESS (uptr->flags);                /*     and the address */
        unit    = UNIT    (uptr->flags);                /*       and the CS/80 unit number */

        dsptr = &dc->devices [address];                 /* get a pointer to the device state */
        usptr = &dsptr->units [unit];                   /*   and a pointer to the unit state */

        if (conflicts [address] [0] == not_set) {                   /* if this is the first unit for this device */
            dsptr->valid_units = describe [model].installed_units;  /*   then get the map of installed units */

            map = dsptr->valid_units & DRIVE_UNITS;     /* restrict the search to the drive units */

            while (map != 0) {                          /* loop through the units */
                index = BIT_NUMBER (map);               /*   and get the next unit index */

                conflicts [address] [index] = 0;        /* clear the unit's conflict counter */

                map ^= BIT (index);                     /* clear the current bit */
                }

            conflicts [address] [UNIT_15] = model;      /* set the device model number in the controller entry */
            }

        else if (model != conflicts [address] [UNIT_15])    /* otherwise if the models conflict */
            conflicts [address] [UNIT_15] = error;          /*   then flag the error */

        if (conflicts [address] [unit] != not_set)      /* if the unit is valid for the model */
            conflicts [address] [unit]++;               /*   then bump the assignment count for this unit */

        dc->present |= BIT (address);                   /* set the "device is present" bit */
        usptr->uptr = uptr;                             /*   and associate the SIMH unit with the CS/80 unit */

        if (unit == 0)                                  /* if this is unit 0 of the device */
            dsptr->units [UNIT_15].uptr = uptr;         /*   then use its SIMH unit for unit 15 scheduling */

        initialize_unit (usptr);                        /* initialize the unit state */

        if (usptr->interlocked                          /* if the unit is interlocked */
          && (dsptr->state == Reporting_Wait            /*   and is waiting to report */
          || dsptr->state == Optional_Wait))            /*   or is ready to report */
            set_response (dc_id, address, SET);         /*     then enable parallel poll to notify the host */

        if (uptr->flags & UNIT_ATT) {                                   /* if the unit is attached */
            uptr->pos = usptr->current.address * usptr->block_stride;   /*   then reset the unit byte position */
            sim_fseek (uptr->fileref, uptr->pos, SEEK_SET);             /*     and reposition the file stream */

            if (ferror (uptr->fileref)) {               /* if the seek failed */
                report_io_error (uptr, "reposition");   /*   then report the error to the console */
                coherent = FALSE;                       /*     and inhibit execution until the user resolves */
                }
            }
        }
    }

dc->parity &= dc->present;                              /* ensure only present devices are checking parity */
dc->response_set &= dc->present;                        /*   and reporting poll responses */

for (address = 0; address < DEVICE_COUNT; address++) {  /* loop through each device */
    if (conflicts [address] [UNIT_15] == error) {       /* if the models conflict between units */
        coherent = FALSE;                               /*   then the assignments are not coherent */

        cprintf ("%s bus device %u has multiple models\n",
                 sim_dname (dptr), address);
        }

    for (unit = 0; unit < UNIT_COUNT - 1; unit++)       /* loop through the units for each device */
        if (conflicts [address] [unit] == 0) {          /* if the CS/80 unit is present but has no SIMH unit */
            coherent = FALSE;                           /*   then the assignments are not coherent */

            cprintf ("%s bus device %u has no assignment for unit %u\n",
                     sim_dname (dptr), address, unit);
            }

        else if (conflicts [address] [unit] > 1         /* otherwise if the CS/80 unit has multiple SIMH units */
          && conflicts [address] [unit] != not_set) {   /*   and the unit is present */
            coherent = FALSE;                           /*     then the assignments are not coherent */

            cprintf ("%s bus device %u has multiple assignments for unit %u\n",
                     sim_dname (dptr), address, unit);
            }
    }

return coherent;                                        /* return the bus coherency state */
}


/* Adjust the device instances.

   This routine is called by SCP to adjust device instances that are newly
   allocated or pending deletion.  On entry, "dvptr" points at the "sim_devices"
   element that contains a pointer to the first of potentially several device
   instances to be adjusted, and "delta" contains the number of instances just
   created (if positive) or to be deleted (if negative).

   When the "dvptr" parameter is NULL, SCP expects the return value to be
   formatted as follows:

      31  30  ... 25  24 |23  22  ..  17  16 |15  14 13  ...  2   1   0
     +---+---+---+---+---+---+---+---+---+---+---+--+---+---+---+---+---+
     | clone identifier  |  instance limit   |     sim_devices size     |
     +---+---+---+---+---+---+---+---+---+---+---+--+---+---+---+---+---+

   The fields have the following meanings:

     sim_devices size      The total number of elements defined in the
                           "sim_devices" array, including the NULL element at
                           the end.  The difference between this number and the
                           number of initialized elements gives the number of
                           new device instances that can be added by the user.

     instance limit        The maximum number of instances allowed.  The count
                           includes the original instance.

     clone identifier      A character used to produce unique names for new
                           device instances.  The ID will be appended to the
                           original device name and incremented by the device
                           instance number.

   When the adjust function is called with a non-NULL pointer parameter, the
   routine performs the actions are necessary to prepare the indicated device
   instances for use or destruction.  The return value is either SCPE_OK if all
   adjustments succeeded, or an SCPE error code indicating the nature of the
   failure.  In the latter case, any allocations made by the routine prior to
   the failure must be freed before returning.


   Implementation notes:

    1. The deallocation part of the routine is entered either for a deletion
       request or for an addition request when a required allocation fails.
*/

static uint32 dc_adjust (DEVICE **dvptr, int32 delta)
{
DC_STATE *nsptr;
DEVICE   *dptr;
DIB      *dibptr;
REG      *rptr;
char     *nptr;
int32    count;
t_stat   status = SCPE_OK;

if (dvptr == NULL)                                      /* if this is a limits request */
    return TO_DWORD (TO_WORD ('A', MAX_DC_COUNT),       /*   then return the clone ID and instance limit */
                     SIM_DEVICE_SIZE);                  /*     and device array size */

else if (delta > 0) {                                   /* otherwise if device clones are being added */
    for (count = 0; count < delta; count++) {           /*   then for each new device clone */
        dptr = dvptr [count];                           /*     get a pointer to the new device */

        status = SCPE_INCOMP;                           /* preset for name duplication error recovery */

        nptr = dptr->name;                              /* save the newly assigned name */
        dptr->name = "";                                /*   and temporarily remove it */

        if (find_dev (nptr) != NULL) {                      /* if the device name is duplicated */
            cprintf ("Duplicate device name %s\n", nptr);   /*   then report it to the user */

            dptr->name = nptr;                          /* restore the assigned name */
            break;                                      /*   and fail at this point */
            }

        else                                            /* otherwise the name is unique */
            dptr->name = nptr;                          /*   so restore it to the device */

        status = SCPE_MEM;                              /* preset for memory error recovery */

        dptr = dvptr [count];                           /* get a pointer to the new device */

        dibptr = malloc (sizeof (DIB));                 /* allocate a new DIB structure */

        if (dibptr == NULL)                             /* if the allocation failed */
            break;                                      /*   then clean up before exiting */

        dptr->ctxt = dibptr;                            /* save the new DIB pointer */

        memcpy (dibptr, &dc_dib, sizeof (DIB));         /* copy the original DIB structure */
        dibptr->card_index = dc_count + count;          /*   and set the new card index */

        nsptr = calloc (1, sizeof (DC_STATE));          /* allocate a new DC state structure */

        if (nsptr == NULL)                              /* if the allocation failed */
            break;                                      /*   then clean up before exiting */

        dcs [dc_count + count].sptr = nsptr;            /* save the new state pointer */
        dcs [dc_count + count].dptr = dptr;             /*   and device pointer in the table */

        for (rptr = dptr->registers; rptr->name != NULL; rptr++)    /* loop through the register entries */
            rptr->loc = (char *) rptr->loc - (char *) &dc_state     /*   to get the location offsets */
                          + (char *) nsptr;                         /*     and relocate them to the new state */

        status = SCPE_OK;                               /* this pass succeeded */
        }                                               /* end of the for loop */

    dc_count = dc_count + delta;                        /* bump the count of DC instances */
    }


if (delta < 0 || status != SCPE_OK) {                   /* if device clones are being deallocated */
    if (delta < 0)                                      /*   then if existing entries are to be deleted */
        delta = - delta;                                /*     then make the deletion count positive */

    dc_count = dc_count - delta;                        /* drop the count of DC instances */

    for (count = 0; count < delta; count++) {           /* for each device clone to be deleted */
        dptr = dvptr [count];                           /*   get the device pointer */

        if (dptr != NULL)                               /* if it is defined */
            free (dptr->ctxt);                          /*   then free the device context */

        if (dcs [dc_count + count].sptr != NULL)        /* if the DC state was allocated */
            free (dcs [dc_count + count].sptr);         /*   then free it */

        dcs [dc_count + count].dptr = NULL;             /* clear the */
        dcs [dc_count + count].sptr = NULL;             /*   table entries */
        }
    }

return (uint32) status;                                 /* return the adjustment status */
}


/* Attach a media image file to a unit.

   The file named by the "cptr" parameter is attached to the unit specified by
   the "uptr" parameter.  This is the simulation equivalent of loading a
   cartridge tape into a tape drive or the internal disc reaching its
   operational speed in a disc drive (disc media is not removable).  While a
   unit is unattached, it reports Not Ready error status.  Attaching the unit
   reports Power Fail status, as required by the CS/80 specification.  Whether a
   disc or a tape image is attached depends on the drive type associated with
   the unit.

   The following switch may be set on the ATTACH command line for disc units:

     Switch  Meaning
     ------  ----------------------------------------
       -N    Create a new disc file and initialize it

   Initialization writes zeros on every disc block, resulting in a full-size
   image file.  If the switch is not specified, an existing disc image file is
   mounted, or a new, zero-length image is created.  "Truncated" image files,
   i.e., those smaller than the defined media size, are permitted.  Reading from
   blocks beyond the physical EOF return all-zeros data, and writing beyond the
   EOF extends the image file.

   The following switches may be set on the ATTACH command line for tape units:

     Switch  Meaning
     ------  ----------------------------------------------------------------
       -N    Create a new long tape file and initialize it
       -NS   Create a new short tape file and initialize it
       -R    Open an existing tape file and write protect it
       -RS   Open an existing short tape file and write protect it
       -S    Open an existing short tape file or create a new short tape file
       -X    Open an existing tape file and extend it to long size if needed
       -XS   Open an existing tape file and extend it to short size if needed

   A new image file may be requested by supplying the "-N" switch.  If an
   existing file is specified with "-N", it will be cleared.  In either case,
   the file is initialized and certified.  If the "-S" switch is also specified,
   a short-length (150-foot) tape cartridge image is created.  Otherwise, a
   long-length (600-foot) image is created.

   If the file does not exist, and no switches are specified, then a zero-length
   file is created.  This represents a long-length tape cartridge that has not
   been initialized.  Specifying the -S switch indicates that the media is a
   short-length uninitialized tape cartridge.  Tape cartridges must be
   initialized and certified before use.

   If the image file exists, it is scanned to determine if the media is
   initialized and certified.  If the scan determines that the media is
   uninitialized, it must be initialized before use, and the user is notified.
   The user is also notified if the media is not certified.  However, the media
   can be used, although reading a record not written will return No Data Found
   status.

   The length of the cartridge is determined from the number of blocks in the
   file, which ends either with an EOM marker, the physical end of the file, or
   the number of blocks defined for a long cartridge.  If the block count can be
   contained in a short-length cartridge, it is marked as such; otherwise, a
   long-length cartridge is designated.  Including the -S switch will ensure that
   the image fits in a short-length cartridge.  If it does not, "Invalid switch"
   is printed on the console, and the attach fails.

   Tape images may also be attached for read-only access with the -R switch.
   This is equivalent to setting the cartridge's Write Protect switch to the
   Safe position.

   If the image is determined to be initialized and certified, then the actual
   block count is compared to the expected block count.  If the file contains
   fewer blocks than defined, and the user specified the -X switch, then the
   missing blocks are written to the file, so that the image then contains a
   full set of blocks for a short-length cartridge (-S switch included with -X)
   or a long-length cartridge (-S switch omitted).  This allows unrestricted
   random access.  If -X was not specified, then the file is write-protected,
   and the user is warned.  This allows read-only access to those blocks
   physically present in the tape image; attempting to access blocks that are
   not present results in No Data Found errors.

   The -X switch may also be used to extend a full-size short-length cartridge
   to a full-size long-length image.  As above, the added blocks are certified.

   A disc image is always initialized; if the file size is too large, the extra
   bytes will not be accessed.  If the size is too small, reads beyond the end
   of the file will return zeros.


   On entry, the switch combinations are checked for validity.  Attempting to
   change the tape format with the -F switch, requesting a new read-only file by
   specifying -N and -R together, requesting that a short read-only file be
   extended by specifying -X and -R together, or specifying -R, -S, or -X for a
   disc unit produces an "Invalid switch" error.  If the switches are valid, the
   file is attached.

   If the attach is successful, then if the attach is not being done as part of
   a RESTORE command, then Power Fail error status is set to notify the OS
   driver of the media change.  The pointer to the SIMH unit is saved in the
   unit state structure, and the unit flags that describe the media are cleared.

   If a new short-length tape image is requested by specifying the -N and -S
   switches, the short-length cartridge flag is set.  Then the unit state
   parameters are initialized (setting the flag will cause the volume size to be
   set to one-quarter of the long-length cartridge).

   If a new file is requested, the media is initialized and certified.  For disc
   images, blocks of zeros are written throughout the file.  For tape images,
   formatted tape records containing hex FF bytes are written; the number of
   blocks written depends on the length of the tape cartridge, as indicated by
   the volume size set during unit state initialization.

   For existing image files, if a disc image was attached, the volume
   initialized flag is set (disc images are always initialized).  Otherwise, a
   tape image was attached and must be checked for initialization and
   certification.  This is required to support random access.

   The check begins by assuming that the image is certified.  A scan of each
   block in the file is then performed to determine if this assumption is valid.

   The first two tape marker words are read from each block.  If a host I/O
   error occurred while reading, the scan is stopped, and the volume is marked
   as uninitialized (formatted).  Otherwise, if the physical EOF or an EOM
   marker was read after the first block, the block number is reduced by one to
   reflect the count of valid blocks.  If the condition occurred on the first
   block, then no blocks are valid, so the volume is considered to be empty.  In
   either case, the scan is stopped.

   If the marker pair indicates either a data record or a tape mark record, the
   scan continues.  These will be either a gap marker and a record length
   marker, or a tape mark and a gap marker, respectively.  If the pair indicates
   an erased block, certification is denied, and the scan continues.  If the
   block does not start with a correct marker pair, the volume is marked as
   uninitialized, the scan is stopped, and the cartridge length is determined by
   the number of full blocks contained in the file.

   After the scan completes, the media state is set.  A tape image containing
   only data and tape mark records is initialized and certified.  If an
   otherwise certifiable file contains any erased blocks, the tape is
   initialized and uncertified.  If any block contains an unrecognized marker
   pair, the tape is uninitialized.

   After the scan to determine if the volume is initialized, the cartridge
   length is determined.  First, the block count of a short-length cartridge is
   calculated.  If the image is smaller than the short-length size, will not be
   extended to the long-length size, and is not empty or the user has requested
   a short file with -S switch, then the short-length flag is set, and the
   volume size parameters are adjusted to match the shorter length.

   If any host I/O error occurred during the scan, it is reported, and the
   command fails with "I/O Error."  Otherwise, if the existing file does not fit
   in a short-length cartridge, but the user specified that a short-length tape
   is being attached, an "Invalid switch" error will be printed.  Otherwise, if
   the volume is not initialized or certified, that condition is reported to the
   console.  Otherwise, if the image size is smaller than the defined size, the
   image is either extended by writing the missing blocks or write-protected to
   allow read-only access to the blocks that are physically present.

   Finally, if a fatal error occurred after attaching, the file is detached.
   Then the status code representing either success or failure is returned.


   Implementation notes:

    1. The "tape_drive" field is set indirectly when the initial set of device
       model numbers is established and thereafter whenever the model numbers
       are changed.  So it is valid before the rest of the unit state structure
       is initialized.

    2. The SCP "attach_unit" routine sets "uptr->pos" to 0 to match the newly
       opened file position.  During a RESTORE or after validating a tape image,
       the stream position does not agree with "pos".  However, during the next
       bus initialization before execution begins, "pos" is recalculated from
       the unit's current block address, and the stream position is reset.
       Therefore, it is not necessary to resynchronize the values here.

    3. The -F ("format") switch that is generally accepted when attaching tape
       units is disallowed here because we do not call the "sim_tape_attach"
       routine that processes the format setting.  Were it not rejected, then
       specifying a format would create a file whose name is the concatenation
       of the format name and the specified file name (e.g., "E11 FC.CART").
       Moreover, we only support the SIMH tape format, as cartridge tape images
       rely on erase gaps to pad data records to the block size.

    4. The "initialize_media" routine handles its own host I/O errors, so we do
       not need check for errors explicitly after it returns.

    5. The SS/80 manual says that a media change is not reported until a command
       that accesses the media, such as Locate and Read, is attempted.  In
       simulation, we set Power Fail status here, rather than noting the change
       and deferring the status change until an access command is received.

    6. RESTOREd units do not set Power Fail status, as this would disturb the
       restored state.

    7. The HPDrive utility permits tape images to contain fewer records than the
       defined volume sizes, which for the HP 9144 and 9145 drives respectively
       are 65,408 or 130,560 blocks for a 600-foot cartridge and 16,352 or
       32,640 blocks for a 150-foot cartridge.  We require image files to be the
       defined size for unrestricted random read and write access and
       accommodate these truncated images, at the user's option, either by
       writing the missing blocks here or marking the file as write-protected
       and returning No Data Found errors if the missing blocks are read.

    8. The HPDrive cartridge tape specification allows a redundant, optional
       end-of-medium (EOM) marker at the end of tape image files.  We do not
       write the marker, but we accept an EOM marker as equivalent to the
       physical EOF.  The EOM is not redundant if private or metadata are
       appended to the tape image.

    9. The cartridge sizing logic declares a cartridge to be long if a truncated
       shorter-length image is accompanied by the -X switch but not the -S
       switch.  This allows the user to attach a short-length image but extend
       it to the defined long-length size.  If -X and -S are both present, the
       user wants to extend it to the defined short-length size.

   10. Except in the case of requesting a new, short-length tape image,
       initializing the unit state sets the volume size to the long-length size.
       Because a full-size short-length tape image may be followed by an EOM
       marker and additional private records, we cannot simply use the image
       file size to determine whether it represents a short- or long-length
       tape.  Instead, we must wait until the certification scan is complete
       before we know the number of blocks present in the image.

   11. A real cartridge tape may be unformatted, formatted, initialized, or
       certified.  A degaussed tape is unformatted.  Formatting records keys
       on an otherwise blank tape and can only be done at the factory.
       Initializing writes "hidden" data structures to the tape but leaves
       erased data records.  Certifying writes all blocks with 1024-byte data
       records containing 0xFF bytes.  In simulation, these states correspond to
       an empty image file, a file containing unrecognized data blocks, a file
       containing erased data blocks, and a file containing defined data or tape
       mark blocks.
*/

static t_stat dc_attach (UNIT *uptr, char *cptr)
{
const uint32    dc_id   = uptr->Instance;       /* the instance identifier */
DC_STATE *const dc      = dcs [dc_id].sptr;     /* the pointer to the channel state */
const uint32    address = ADDRESS (uptr->flags);
const uint32    unit    = UNIT    (uptr->flags);
const USPTR     usptr   = &dc->devices [address].units [unit];
MEDIA_STATE     state;
int32           block, volume_size, short_size;
t_mtrlnt        marker [2];
t_stat          result;

if (sim_switches & F_SWITCH                             /* if a tape format was specified */
  || (sim_switches & NR_SWITCHES) == NR_SWITCHES        /*   or if a new read-only file is requested */
  || (sim_switches & XR_SWITCHES) == XR_SWITCHES        /*   or a read-only file is to be extended */
  || sim_switches & (R_SWITCH | XS_SWITCHES)            /*   or a tape switch is specified */
  && not usptr->tape_drive)                             /*     and the unit is not a tape drive */
    return SCPE_INVSW;                                  /*       then report an Invalid switch error */

if (uptr->flags & UNIT_ATT                              /* if the unit is currently attached */
  && not (sim_switches & SIM_SW_REST))                  /*   and this is not a RESTORE call */
    result = SCPE_ALATT;                                /*     then the unit cannot be automatically detached */

else if (usptr->tape_drive)                             /* if the unit is a tape drive */
    result = sim_tape_attach (uptr, cptr);              /*   then call the tape library attach routine */

else                                                    /* otherwise */
    result = attach_unit (uptr, cptr);                  /*   attach the image to the disc drive */

if (result == SCPE_OK) {                                /* if the attach was successful */
    if (not (sim_switches & SIM_SW_REST))               /*   then if RESTORE is not reattaching */
        set_error (dc_id, usptr, Power_Fail);           /*     then log a power fail error for the media change */

    usptr->uptr = uptr;                                 /* associate the SIMH unit with the CS/80 unit */
    uptr->User_Flags &= ~USER_MEDIA_MASK;               /*   and clear the media description flags */

    if ((sim_switches & NS_SWITCHES) == NS_SWITCHES)    /* if a new short-length tape is requested */
        uptr->User_Flags |= USER_SHORT_CART;            /*   then set the short-length flag */

    initialize_unit (usptr);                            /* set up the unit parameters */

    if (sim_switches & N_SWITCH)                                /* if a new image was requested */
        result = initialize_media (dc_id, usptr, 0, Certified); /*   then initialize and certify it */

    else if (not usptr->tape_drive)                     /* otherwise if it's a disc unit */
        uptr->User_Flags |= TO_STATE (Initialized);     /*   then all disc images are initialized */

    else {                                              /* otherwise it's an existing tape image */
        state = Certified;                              /*   so assume it's certified until proven otherwise */

        volume_size = (int32) usptr->volume_size;       /* get the defined volume size */

        for (block = 0; block < volume_size; block++) { /* for each block in the file */
            sim_fseek (uptr->fileref,                   /*   seek to the start of the block */
                       block * usptr->block_stride,
                       SEEK_SET);

            sim_fread (marker, sizeof (t_mtrlnt),       /* read a potential marker pair */
                       2, uptr->fileref);

            if (ferror (uptr->fileref)) {               /* if a host file error occurred */
                state = Formatted;                      /*   then mark the tape as uninitialized */
                break;                                  /*     and abort the scan */
                }

            else if (feof (uptr->fileref)               /* otherwise if we hit the end of the file */
              || marker [0] == MTR_EOM) {               /*   or if it's an end-of-medium mark */
                block = block - 1;                      /*     then drop the block count */

                if (block < 0)                          /* if no data blocks are present */
                    state = Formatted;                  /*   then the tape will need to be initialized */

                break;                                  /* stop the scan */
                }

            else if (marker [0] == MTR_GAP && marker [1] <= usptr->block_size   /* otherwise if it's a data record */
              || marker [0] == MTR_TMK && marker [1] == MTR_GAP)                /*   or a tape mark */
                continue;                                                       /*     then continue the scan */

            else if (marker [0] == ERASED_BLOCK && marker [1] == MTR_GAP)   /* otherwise if it's an unwritten block */
                state = Initialized;                                        /*   then it's not certified */

            else {                                          /* otherwise it's not a valid tape block */
                block = (uint32) sim_fsize (uptr->fileref)  /*   so get the last block number */
                          / usptr->block_stride - 1;        /*     based on the file size */

                state = Formatted;                          /* mark the tape as uninitialized */
                break;                                      /*   and abort the scan */
                }
            }

        short_size = volume_size / 4;                   /* get the block count for a short-length tape */

        if (block < short_size                              /* if the cartridge size is short */
          && (sim_switches & XS_SWITCHES) != X_SWITCH       /*   and not extending to a long cartridge */
          && (block != -1 || sim_switches & S_SWITCH)) {    /*   and contains data or is specified as short */
            uptr->User_Flags |= USER_SHORT_CART;            /*     then set the short-length flag */
            volume_size = short_size;                       /*       and drop the volume size */

            usptr->volume_size = short_size;            /* cut the volume size */
            usptr->step_size /= 4;                      /*   and step size and capacity */
            uptr->capac /= 4;                           /*     to one-fourth of the long length */
            }

        if (ferror (uptr->fileref))                     /* if a host I/O error occurred */
            result = report_io_error (uptr, "volume");  /*   then report it to the console */

        else if (sim_switches & S_SWITCH                /* otherwise if the user specified a short tape */
          && block >= short_size)                       /*   but the tape length is long */
            result = SCPE_INVSW;                        /*     then the switch is not valid */

        else if (state < Initialized)                   /* otherwise if the volume is not initialized */
            cputs ("Volume is not initialized\n");      /*   then report it to the console */

        else if (state != Certified)                    /* otherwise if the volume is not certified */
            cputs ("Volume is not certified\n");        /*   then report it to the console */

        else if (++block < volume_size)                         /* otherwise if the tape is smaller than defined */
            if (sim_switches & X_SWITCH)                        /*   then if the user requested an extension */
                result = initialize_media (dc_id, usptr,        /*     then initialize and certify */
                                           block, Certified);   /*       the missing blocks */

            else {                                          /*   otherwise */
                uptr->flags |= UNIT_RO;                     /*     write-protect the image */
                cputs ("Volume is write protected\n");      /*       to allow read access without extending */
                }

        if (result == SCPE_OK)                          /* if the attach and scan were successful */
            uptr->User_Flags |= TO_STATE (state);       /*   then set the media state */
        else                                            /* otherwise */
            detach_unit (uptr);                         /*   detach the file before failing */
        }
    }

return result;                                          /* return the result of the attach */
}


/* Release a unit prior to detaching.

   This routine is called in response to a DETACH DCn command.  The CS/80
   specification requires that devices with removable media request a release
   from the operating system before allowing the media to be removed.  The OS
   may allow the release, or it may decline the release; in the latter case, the
   request must be repeated until the host grants the request.

   In simulation, detaching a unit enables the parallel poll response to gain
   the attention of the host.  When the host responds, it will see QSTAT = 1 and
   an Operator Request error status.  If the host then sends a Release command,
   the "dc_detach" routine is called to detach the unit.  If Release Denied is
   sent instead, the unit remains attached.  In either case, a note is printed
   on the simulation console to inform the user of the host's decision.

   The user may force a release by including the -F switch with the DETACH
   command.  This performs an immediate detach without informing the host, who
   will then receive a Not Ready error at the next access.  The force option is
   therefore recommended only when the OS is not running.


   Implementation notes:

    1. We need only enable PPR here if the device controller is idle.  If the
       controller is executing a command for the unit requesting the release,
       the host will see the release request automatically when it sends a
       Request Status command in response to the concluding QSTAT = 1 report.

       If the controller is executing a command for another unit, "dc_service"
       will check all device units for Release Request flags and, seeing one
       set, will call "set_response (SET)" to enable parallel poll from the
       Reporting_Wait state.  The controller will then remain in that state.  By
       checking request bits instead of the "release_pending" flag, we ensure
       that an unsolicited message is not sent after the status has been cleared
       by a Request Status command.
*/

static t_stat dc_release (UNIT *uptr)
{
const uint32    dc_id   = uptr->Instance;           /* the instance identifier */
DC_STATE *const dc      = dcs [dc_id].sptr;         /* the pointer to the channel state */
DEVICE   *const dptr    = dcs [dc_id].dptr;         /* the pointer to the controlling device */
const uint32    address = ADDRESS (uptr->flags);    /* the bus address of the device containing the unit */
const uint32    unit    = UNIT    (uptr->flags);    /* the unit number of the device */
const DSPTR     dsptr   = &dc->devices [address];   /* the pointer to the device state */
const USPTR     usptr   = &dsptr->units [unit];     /* the pointer to the current unit state */
t_stat          result  = SCPE_OK;

if (uptr->flags & UNIT_ATT)                             /* if the unit is attached */
    if (sim_switches & (F_SWITCH | SIM_SW_SHUT)) {      /*   then if this is a forced detach or shut down request */
        usptr->release_pending = FALSE;                 /*     then clear any pending release */

        result = dc_detach (uptr);                      /* detach the unit */
        }

    else {                                              /* otherwise */
        usptr->release_pending = TRUE;                  /*   set a pending release */
        set_error (dc_id, usptr, Operator_Request);     /*     and report a release request */

        if (dsptr->state == Command_Ready               /* if we are ready for a command */
          || dsptr->state == Optional_Wait) {           /*   or waiting for an optional report */
            set_response (dc_id, address, SET);         /*     then enable parallel poll */

            tpprintf (dptr, TRACE_STATE, "Device %u transitioned from %s state to %s state\n",
                      address, state_names [dsptr->state], state_names [Reporting_Wait]);

            dsptr->state = Reporting_Wait;              /* move to the reporting state */
            }

        cprintf ("Bus %u unit %u release requested\n",  /* inform the operator */
                 address, unit);
        }

return result;
}


/* Detach a media image file from a unit.

   The file currently attached to the unit specified by the "uptr" parameter is
   detached.  This is the simulation equivalent of unloading a cartridge tape
   from a tape drive.  Fixed disc units cannot be unloaded, so detaching behaves
   as though the internal disc has not reached its operational speed.  An
   unattached unit reports Not Ready error status when it is accessed.

   If the unit is a tape unit, the original capacity is restored, so that it
   displays the full capacity of a long cartridge tape.

   If the detach is successful, the unit status is cleared, as it pertains to
   the image file just detached.


   Implementation notes:

    1. Detaching does not disable the poll response, as it may still be set from
       attaching another unit within the device.
*/

static t_stat dc_detach (UNIT *uptr)
{
const uint32     dc_id   = uptr->Instance;                      /* the instance identifier */
DC_STATE *const  dc      = dcs [dc_id].sptr;                    /* the pointer to the channel state */
const MODEL_TYPE model   = MODEL   (uptr->flags);               /* the model number of the device */
const uint32     address = ADDRESS (uptr->flags);               /* the bus address of the device  */
const uint32     unit    = UNIT    (uptr->flags);               /* the unit number of the device */
const USPTR      usptr   = &dc->devices [address].units [unit]; /* the pointer to the current unit state */
UDPTR            udptr;
t_stat           result;

if (usptr->tape_drive) {                                /* if the unit is a tape drive */
    result = sim_tape_detach (uptr);                    /*   then call the tape library detach routine */

    udptr = &describe [model].unit [unit];              /* get a pointer to the Describe info */

    usptr->volume_size = udptr->volume.max_block + 1;       /* restore the original volume size */
    usptr->step_size = (udptr->volume.max_head + 1)         /*   and step size */
                         * (udptr->volume.max_sector + 1);  /*     and capacity in case */
    uptr->capac = usptr->volume_size * usptr->block_size;   /*       a short tape had been attached */
    }

else                                                    /* otherwise */
    result = detach_unit (uptr);                        /*   detach the image from the disc drive */

if (result == SCPE_OK) {                                /* if the detach succeeded */
    memset (usptr->status, 0, sizeof usptr->status);    /*   then clear the status fields */
    usptr->qstat = 0;                                   /*     and the quick status value */
    }

return result;                                          /* return the result of the detach */
}


/* Set a device model number.

   This regular validation routine is called to change the model number of a
   simulated unit.  The list of available models is preset by the MODEL_TYPE
   enumeration.

   On entry, "uptr" points at the simulator unit to configure, "value" indicates
   the intended model number, and "cptr" and "desc" are not used.

   Because a model could be changed from a disc unit to a tape unit, or vice
   versa, the unit must not be attached to a media image file, or an error
   occurs.  Also, if the current CS/80 unit number corresponding to the SIMH
   unit does not exist in the new model, the unit number is reset to zero (all
   devices have a unit 0).  The capacity field of the unit structure is reset to
   the capacity of the new model, and the "tape_drive" flag is set, depending on
   whether the corresponding CS/80 unit of the new model is a tape drive or disc
   drive.

   The routine always succeeds, so it returns SCPE_OK.


   Implementation notes:

    1. The unit state information cannot be set here, because a later command
       might change the address, which would shift where the unit data should
       go.  So it has to be set in the bus initialization routine.
*/

static t_stat dc_set_model (UNIT *uptr, int32 value, char *cptr, void *desc)
{
const uint32     dc_id   = uptr->Instance;              /* the instance identifier */
DC_STATE *const  dc      = dcs [dc_id].sptr;            /* the pointer to the channel state */
const MODEL_TYPE model   = MODEL   (value);
const uint32     address = ADDRESS (uptr->flags);
const DSPTR      dsptr   = &dc->devices [address];
UDPTR            udptr;
uint32           unit = UNIT (uptr->flags);

if (uptr->flags & UNIT_ATT)                             /* if the unit is currently attached */
    return SCPE_ALATT;                                  /*   then the model cannot be changed */

if (not (describe [model].installed_units & BIT (unit))) {  /* if the unit number is not valid for the new model */
    uptr->flags &= ~UNIT_UNIT;                              /*   then reset it in the unit flags */
    unit = 0;                                               /*     and redesignate as unit 0 */
    }

udptr = &describe [model].unit [unit];                  /* get the describe pointer */

uptr->capac = (udptr->volume.max_block + 1)             /* update the capacity */
                * udptr->bytes_per_block;               /*   to reflect the new model */

dsptr->units [unit].tape_drive = (udptr->type == 2);    /* TRUE if unit is a tape drive */

return SCPE_OK;
}


/* Set a device unit number.

   This expanded validation routine is called to set the unit number of a
   simulated unit, which range from 0-1.  Each disc or tape device attached to
   the bus may have one or two internal units, depending on the device model.
   Each device unit is assigned to one simulator unit.  Addressing conflicts
   between devices are permitted during assignment but are checked before
   execution begins, and invalid combinations must be corrected.  However, unit
   assignments are validated here against the currently assigned model, and
   specifying an invalid unit number for a model is rejected.

   On entry, "uptr" points at the simulator unit to configure, "value" contains
   the new unit number, and "cptr" and "desc" are not used.  Because the unit
   could be changed from a disc unit to a tape unit, or vice versa, the unit
   must not be attached to a media image file, or an error will occur.

   As this is a validation routine for an expanded modifier entry, parsing and a
   check against the maximum unit number (1) have already been performed.  If
   then routine returns SCPE_OK, the value will be stored in the target field.
   so we do not need to store it here.


   Implementation notes:

    1. The alternate of validating all changes at assignment time and rejecting
       any assignment that conflicts would force the user to make intermediate
       assignments to swap a pair of bus addresses.  It would also require
       trapping unit enable commands to prevent enabling a unit whose
       assignments conflict with an existing one.  These same reasons are why
       setting conflicting IMB channel assignments are allowed, and a
       consistency check is made prior to commencing execution.
*/

static t_stat dc_set_unit (UNIT *uptr, int32 value, char *cptr, void *desc)
{
const uint32     dc_id   = uptr->Instance;              /* the instance identifier */
DC_STATE *const  dc      = dcs [dc_id].sptr;            /* the pointer to the channel state */
const MODEL_TYPE model   = MODEL   (uptr->flags);       /* the device model type */
const uint32     address = ADDRESS (uptr->flags);       /* the device bus address */
const DSPTR      dsptr   = &dc->devices [address];      /* the pointer to the device state */
UDPTR            udptr;

if (uptr->flags & UNIT_ATT)                             /* if the unit is currently attached */
    return SCPE_ALATT;                                  /*   then the unit cannot be changed */

else if (describe [model].installed_units & BIT (value)) {  /* otherwise if the unit number is valid */
    udptr = &describe [model].unit [value];                 /*   then get the describe pointer */

    uptr->capac = (udptr->volume.max_block + 1)         /* update the capacity */
                    * udptr->bytes_per_block;           /*   to reflect the new unit */

    dsptr->units [value].tape_drive = (udptr->type == 2);   /* TRUE if unit is a tape drive */

    return SCPE_OK;                                     /* the new unit number is valid */
    }

else                                                    /* otherwise the unit is out of range */
    return SCPE_ARG;                                    /*   so report an invalid argument */
}



/****************************
 *                          *
 * CS/80 local bus routines *
 *                          *
 ****************************/


/* Accept a command or data byte from the bus.

   This routine is called whenever the controller or talker has sourced a data
   byte to the bus, or whenever a controller, talker, or listener has changed
   the control state of the bus.  It is responsible for decoding commands and
   for calling routines to process secondaries, incoming data, and control
   state changes.

   When accepting a byte with DAV asserted, it corresponds in hardware to
   performing the IEEE-488 AH (Acceptor Handshake), T (Talker), L (Listener),
   RL (Remote Local), and DC (Device Clear) interface functions.  When changing
   the control signals without sourcing a byte, i.e., with DAV denied, it
   corresponds to performing the IEEE-488 SR (Service Request) function or as a
   participant in the SH (Source Handshake) function when asserting and denying
   NRFD (Not Ready for Data).

   If data is present, it decodes primary, secondary, talk, and listen commands,
   and receives data bytes if any of the attached devices are current listeners.
   For control changes, it responds to parallel polls and the IFC (Interface
   Clear), DCL (Device Clear), and SDC (Selected Device Clear) commands.

   On entry, the "dc_id" parameter is the instance identifier of the DC device
   set connected to the bus, and the "bus" parameter is the set of eight control
   and eight data lines representing the HP-IB.

   If the DAV signal is not asserted, then a control signal change occurred.
   There are three conditions to which we must respond:

    1. A parallel poll is being conducted.  We record the poll state, so that if
       a poll response is subsequently enabled while a poll is still in
       progress, we can notify the controller.  Then we return the set of poll
       responses from currently enabled devices.

    2. An Interface Clear is initiated.  IFC unaddresses all units, so any
       transactions in progress must be terminated as though an Untalk and
       Unlisten had been accepted from the bus.

    3. Attention or Not Ready for Data are denied.  A device addressed to talk
       must wait for ATN to deny before data may be sent.  Also, a listener that
       has asserted NRFD must deny it before a talker may send data.  If a
       talker is sending data and both ATN and NRFD are denied, then we
       reenter the service routine to send the next byte.

   If DAV is asserted, then a command or data byte is present on the bus.  If
   the ATN signal is asserted, parity is checked to be sure it is correct.  If
   it is not, then a parity error is logged to all present devices.  Otherwise,
   the command byte is decoded to determine the proper action.

   Primary commands placed on the bus may be universal (applying to all present
   devices) or addressed (applying only to those devices that are addressed to
   listen).  Address commands select the device that will talk and the device(s)
   that will listen.  If the addressed device is present, the byte is accepted;
   a talk or listen command directed to a device not present is ignored.

   Secondary commands and data bytes are processed only if one or more listeners
   are present.

   This routine is called once for each byte sourced and is responsible for
   notifying all connected devices on the specified bus.  For universal and
   addressed commands and data, the routine iterates through the list of present
   or currently listening devices, passing the bus state to each in turn.  The
   type of byte passed is determined by the state of the ATN signal and, if ATN
   is asserted, by the high-order bits of the value.

   If a Talk, Unlisten, or Untalk command is received while a transaction is
   currently in progress, a channel abort occurs.  This terminates the current
   command with a Message Length error and then begins a new transaction.  A new
   Listen command does not cause a channel abort, because the current listener
   remains addressed.

   For secondaries, the CS/80 Identify command sequence is decoded here, as it
   does not follow the standard HP-IB protocol.  Otherwise, secondaries are
   processed by each device that is addressed to listen or talk.

   If ATN is not asserted, a data byte has been sourced.  It is processed by
   each device that is addressed to listen.

   After processing is complete, the routine returns the set of bits
   corresponding to the addresses of the current listeners.


   Implementation notes:

    1. The HP-IB Parity Checking command says that if checking is enabled and a
       channel command does not have odd parity, "...the device will NOT accept
       the command, i.e., Not Data Accepted (NDAC) will remain in the low state.
       This condition will remain until the host removes Data Valid (DAV) and
       corrects the channel command parity."  Because all devices on the bus
       drive the common NDAC line, any device holding it low will prevent the
       data handshake from completing.  Therefore, we need only test if the map
       of parity checkers is non-zero before declining acceptance of a byte with
       bad parity.  Before returning, however, we must log Channel Parity errors
       in all devices that are checking, because in hardware each would
       determine for itself that bus parity was incorrect.

    2. Identify is handled as a special case because of the odd bus protocol
       employed (it is an Untalk followed by a secondary containing a device
       address, and the device puts data on the bus without having been
       addressed as the talker).  To detect this sequence, the current bus
       command is saved before returning.  If a secondary is immediately
       preceded by an Untalk, and the address contained in the secondary is
       present, then an Identify command has been received.  When the command is
       processed, the device is temporarily assigned as the talker for the
       duration of the command, even though it is preceded by an Untalk.  This
       allows us to know which SIMH unit to reschedule when the controller
       denies ATN to permit return of the identity bytes.

    3. Identify can be issued between phases of a transaction, and it must not
       disturb the command in progress.  So if an event service is scheduled, we
       save the remaining time, current opcode, and current state in the UNIT
       structure, and cancel the service before setting up the Identify.  When
       it completes, the prior service will be restored.

    4. While adding a new listener in the middle of a transaction does not
       disturb that transaction, the new listener will undoubtedly log a Message
       Sequence error.  No special action is needed for this case.

    5. The "process_data" routine may assert NRFD to hold off the talker, so we
       remove the DAV and EOI signals from the bus state before calling.  This
       prevents the assertion call from being misinterpreted as a data transfer.

    6. This protocol module maintains the current state of the channel, so that
       the routines know which devices are present and addressed.  This is more
       efficient than querying each bus address at each call to determine
       whether a device is present and, if so, whether it is presently addressed
       to talk or listen.  Consequently, it should be called only if one or more
       devices are present (for commands) or listening (for data).  If the
       routine is called inadvertently, it simply returns with no action taken.
*/

static uint32 dc_hpib_accept (uint32 dc_id, BUS_CABLE bus)
{
DC_STATE *const dc          = dcs [dc_id].sptr;         /* the pointer to the channel state */
DEVICE   *const dptr        = dcs [dc_id].dptr;         /* the pointer to the controlling device */
const uint32    address     = BUS_ADDRESS (bus);        /* the address indicated for addressing commands */
const uint32    address_bit = BIT (address);            /* the bit corresponding to the address */
DSPTR           dsptr;
UPTR            uptr;
CN_STATE        entry_state;
uint32          bus_address, dev_map;

dc->bus = bus;                                          /* synchronize the bus */

dc->poll_active = (bus & BUS_PPOLL) == BUS_PPOLL;       /* record the parallel poll state */

if (dc->poll_active)                                    /* if a poll is currently active */
    return dc->response_set;                            /*   then return the poll response set */

else if (not (bus & Bus_DAV)) {                         /* otherwise if this is a control change */
    if (bus & Bus_IFC) {                                /*   then if interface clear is asserted */
        if (dc->active) {                               /*     then if the channel is active */
            if (dc->talker != BUS_UNADDRESS)            /*       then if a talker is addressed */
                channel_abort (dc_id, BUS_TAG);         /*         then abort the transaction */

            if (dc->listeners)                          /* if there are any listeners */
                channel_abort (dc_id, BUS_LAG);         /*   then abort them too */
            }

        dc->listeners = 0;                              /* unaddress all listeners */
        dc->talker = BUS_UNADDRESS;                     /*   and unaddress the current talker */
        }

    else if (not (bus & (Bus_ATN | Bus_NRFD))           /* otherwise if the bus is ready for data */
      && dc->talker != BUS_UNADDRESS) {                 /*   and a talker is addressed */
        dsptr = &dc->devices [dc->talker];              /*     then point at the talking device */
        uptr = dsptr->units [MAPUS (dsptr->unit)].uptr; /*       and the associated unit */

        if ((dsptr->state == Execution_Send             /* if the device is waiting to send data */
          || dsptr->state == Error_Source)              /*   or source an error byte */
          && uptr != NULL && not sim_is_active (uptr))  /*   and the associated SIMH unit is not scheduled */
            dc_service (uptr);                          /*     then start or resume the transfer */
        }

    return dc->listeners;                               /* return the current set of listeners */
    }

else if (not dc->present)                               /* otherwise if there are no devices present */
    return 0;                                           /*   then the listener bitmap is empty */

else if (bus & Bus_ATN) {                               /* otherwise if this is a command */
    if (dc->parity && odd_parity [BUS_DATA (bus)]) {    /*   then if parity is enabled but wrong */
        dev_map = dc->parity;                           /*     then notify all checking devices */

        while (dev_map != 0) {                          /* loop through the devices */
            bus_address = BIT_NUMBER (dev_map);         /*   and get the next bus address */

            dsptr = &dc->devices [bus_address];                     /* log a parity error */
            set_error (dc_id, &dsptr->units [MAPUS (dsptr->unit)],  /*   to its current unit */
                       Channel_Parity);

            dev_map ^= BIT (bus_address);               /* clear the current bit */
            }
        }

    else switch (BUS_GROUP (bus)) {                     /* otherwise dispatch the bus group */

        case BUS_PCG:                                   /* Primary Command Group */
            dev_map = dc->listeners;                    /* get the current set of devices listening */

            switch (address) {                          /* dispatch the primary command */

                case 0x14:                              /* Universal Device Clear */
                    dev_map = dc->present;              /* clear all devices present on the bus */

                /* fall through into the SDC case */

                case 0x04:                                  /* Selected Device Clear or Amigo Clear */
                    while (dev_map != 0) {                  /* loop through the devices */
                        bus_address = BIT_NUMBER (dev_map); /*   and get the next bus address */

                        dc->devices [bus_address].unit = 15;    /* set the controller unit */
                        clear_device (dc_id, bus_address);      /*   to clear it and all units */

                        dev_map ^= BIT (bus_address);       /* clear the current bit */
                        }
                    break;


                default:                                /* unsupported universal commands */
                    break;                              /*   are ignored */
                }                                       /* end of primary command dispatching */

            dc->active = FALSE;                         /* end the transaction for an Amigo Clear */
            break;


        case BUS_LAG:                                   /* Listen Address Group */
            if (address == BUS_UNADDRESS) {             /* if this is an Unlisten */
                if (dc->active                          /*   then if unlistening while the channel is active */
                  && dc->listeners)                     /*     and listeners are addressed */
                    channel_abort (dc_id, BUS_LAG);     /*       then abort the transaction */

                dc->listeners = 0;                      /* unaddress all of the listeners */
                }

            else if (dc->present & address_bit) {       /* otherwise if the addressed device is present */
                dc->listeners |= address_bit;           /*   then it becomes a listener */

                if (dc->talker == address)              /* if it is also the current talker */
                    dc->talker = BUS_UNADDRESS;         /*   then untalk it */
                }
            break;


        case BUS_TAG:                                   /* Talk Address Group */
            if (dc->active                              /* if readdressing while the channel is active */
              && dc->talker != BUS_UNADDRESS            /*   and a talker is currently addressed */
              && address != dc->talker)                 /*     and not specifying the same address */
                channel_abort (dc_id, BUS_TAG);         /*       then abort the transaction */

            if (address == BUS_UNADDRESS)               /* if this is an Untalk */
                dc->talker = BUS_UNADDRESS;             /*   then clear the talker */

            else if (dc->present & address_bit) {       /* otherwise if the addressed device is present */
                dc->talker = address;                   /*   then it becomes the current talker */

                if (dc->listeners & address_bit)        /* if the new talker had been listening */
                    dc->listeners &= ~address_bit;      /*   then clear its listener bit */
                }
            break;


        case BUS_SCG:                                   /* Secondary Command Group */
            if (dc->command == BUS_UNTALK               /* if preceded by an Untalk */
              && dc->present & address_bit) {           /*   and the addressed device is present */
                dsptr = &dc->devices [address];         /*     then this is an Identify sequence */

                uptr = dsptr->units [MAPUS (dsptr->unit)].uptr; /* get the current SIMH unit pointer */
                uptr->IDC_Time = sim_is_active (uptr);          /*   and unit activation time */

                entry_state = dsptr->state;             /* save the entry state */

                if (uptr->IDC_Time > 0) {               /* if a command is in progress */
                    uptr->IDC_Opcode_State =            /*   then save the command opcode */
                      TO_IDC_OPCODE (dsptr->opcode)     /*     and the current state */
                      | TO_IDC_STATE (dsptr->state);

                    sim_cancel (uptr);                  /* cancel the current operation */
                    }

                dsptr->opcode = Identify;               /* set up the Identify command */

                dc->talker = address;                   /* temporarily address the device to talk */
                execute_command (dc_id, address, bus);  /*   and start the command */

                tpprintf (dptr, TRACE_STATE, "Device %u transitioned from %s state to %s state\n",
                          address, state_names [entry_state], state_names [dsptr->state]);
                }

            else {                                      /* otherwise it's a normal secondary */
                dev_map = dc->listeners;                /*   so get the current set of listeners */

                if (dc->talker != BUS_UNADDRESS)        /* if a talker is addressed */
                    dev_map |= BIT (dc->talker);        /*   then include it in the set as well */

                while (dev_map != 0) {                  /* loop through the devices */
                    bus_address = BIT_NUMBER (dev_map); /*   and get the next bus address */

                    process_secondary (dc_id, bus_address, bus);    /* process the secondary */

                    dev_map ^= BIT (bus_address);       /* clear the current bit */
                    }
                }
            break;                                      /* end of secondary processing */
        }                                               /* end of command dispatching */

    dc->command = BUS_COMMAND (bus);                    /* save the command for a potential Identify */
    }

else {                                                  /* otherwise it is bus data (ATN is denied) */
    dc->bus &= ~(Bus_DAV | Bus_EOI);                    /*   so remove the handshake from the bus state */

    dev_map = dc->listeners;                            /* get the current set of devices listening */

    while (dev_map != 0) {                              /* loop through the devices */
        bus_address = BIT_NUMBER (dev_map);             /*   and get the next bus address */

        process_data (dc_id, bus_address, bus);         /* process the data byte */

        dev_map ^= BIT (bus_address);                   /* clear the current bit */
        }
    }

return dc->listeners;                                   /* return the current set of listeners */
}


/* Report the protocol configuration.

   This routine is called during run-time initialization to report the
   configuration of the GIC protocol handler.  On entry, "channel" contains the
   channel number for which the report is requested.  The routine returns a
   structure containing a pointer to the DC device, the instance ID of the
   protocol handler, and a bitmap of the devices present on the bus and
   supported by the protocol.
*/

static BUS_REPORT dc_report (uint32 channel)
{
DEVICE     *dptr;
UPTR       uptr;
uint32     unit, dc_id;
BUS_REPORT report = { NULL, 0, 0 };

for (dc_id = 0; dc_id < dc_count; dc_id++) {            /* search through the instance table */
    dptr = dcs [dc_id].dptr;                            /*   and get the next device pointer */

    if (CHANNEL (dptr->flags) == channel) {             /* if the instance is configured for the channel */
        report.dptr = dptr;                             /*   then report the device pointer */
        report.instance = dc_id;                        /*     and the DC instance */

        if (not (dptr->flags & DEV_DIS))                /* if the device is not disabled */
            for (unit = 0; unit < SIM_UNITS; unit++) {  /*   then loop through the SIMH units */
                uptr = &dptr->units [unit];             /*     and get the next unit pointer */

                uptr->Instance = dc_id;                 /* set the ID of the controlling instance */

                if (not (uptr->flags & UNIT_DIS))                   /* if the unit is not disabled */
                    report.present |= BIT (ADDRESS (uptr->flags));  /*   then report the address as assigned */
                }
        }
    }

return report;                                          /* return the configuration report */
}



/********************************
 *                              *
 * CS/80 local utility routines *
 *                              *
 ********************************/


/* Process a secondary command.

   This routine is called to process a secondary command received from the bus
   for an addressed device.  Each device present on the bus that has been
   addressed either as a listener or talker receives the secondary.  Secondaries
   are used by the CS/80 protocol to identify various types of messages,
   specifically:

     T/L  Value  Identifies
     ---  -----  ----------------------------
      L    05H   Device command message
      L    0EH   Execution message (receive)
      L    10H   Amigo Clear
      L    12H   Transparent message

      T    0EH   Execution message (send)
      T    10H   Reporting message
      T    12H   Transparent message

   On entry, the "dc_id" parameter is the instance identifier of the DC device
   set connected to the bus, the "address" parameter contains the bus address of
   the target device, which will be either a listener or talker, and the "bus"
   parameter contains the set of eight control and eight data lines representing
   the HP-IB state.

   The routine decodes the valid secondaries, checks that the device controller
   is in the appropriate state for the supplied secondary, and establishes the
   new controller state as a result of receiving the secondary.  The states
   required for valid entry are:

     T/L  Message               Required Entry State(s)
     ---  -------------------  -----------------------------------------------------------
      L   Device command       Command_Ready | Optional_Wait
      L   Execution (receive)  Execution_Wait | Error_Wait
      L   Amigo Clear          (any)
      L   Transparent          (any)

      T   Execution (send)     Execution_Wait | Error_Wait
      T   Reporting            Reporting_Wait | Optional_Wait | Error_Wait | Command_Ready
      T   Transparent          Execution_Wait | Error_Wait

   A transparent message received while listening can be either a transparent
   command or a transparent execution.  A transparent message received while
   talking must be a transparent execution.  Only the Read Loopback and Write
   Loopback transparent commands have execution phases.

   If the controller is not in the required entry state, or if an invalid
   secondary is received, a Message Sequence error occurs.  Error recovery is as
   follows:

    - An unsupported talk secondary sends a single data byte tagged with EOI.

    - An unsupported listen secondary accepts and discards any accompanying data
      bytes until EOI is asserted or an Unlisten is received.

    - An Execution Send secondary that is not permitted sends a single data byte
      tagged with EOI.

    - An Execution Receive secondary that is not permitted accepts and discards
      data bytes until EOI is asserted or an Unlisten is received.

   An Execution Send secondary while writing or an Execution Receive secondary
   while reading will be picked up in the service routine when the opcode
   isn't valid for the current state.

   When the device command secondary is received, the current operational values
   are reset to the persistent values.  This ensures that the prior unit's
   values are correctly set before the next command directed to that unit is
   executed.


   Implementation notes:

    1. On entry, any scheduled event service is canceled.  Events are scheduled
       between messages only to issue the parallel poll that indicates that the
       device is ready to receive an execution or reporting message (events are
       scheduled within execution messages to send or receive each data byte).
       Canceling these is legitimate when the messages are issued by the host
       without waiting for the poll response.

       Receiving any other message while an event is scheduled represents a
       Message Sequence error, and recovery involves terminating the current
       command and then accepting and discarding the new message.  Service is
       canceled as part of terminating the current command.

       Legal reception of a device or transparent command will mean issuing a
       cancel for an event even though there are none scheduled.  However, the
       alternative of testing for an active event before issuing the cancel
       causes SCP to make two passes through the event list instead of one.  So
       it is actually more efficient to issue the cancel in all cases.

    2. Current operational values are nominally in force for the duration of the
       command.  Logically, they are reset to the persistent values at the end
       of the command unless the command sequence ends with a complementary, in
       which case the current set becomes persistent.  However, command
       completion can occur in three different places: in the command executor
       if the command does not have an execution phase, in the device service
       routine if it does have an execution phase, or in the channel abort
       routine if execution is aborted by Untalking or Unlistening the device in
       the middle of a command.

       The problem is that the host may not follow the transaction sequence, for
       example skipping a required reporting phase.  This produces a Message
       Sequence error but is otherwise recoverable.  So resetting the current
       set during the reporting phase is not reliable.  Nor is resetting after
       an execution phase completes, because again the host can abort the phase
       with an Untalk or Unlisten.  If the current set is to be reverted
       reliably at the end of each command, it is necessary to accommodate all
       of the possible ways that a command can end.

       It is simpler to reset them here prior to starting the next command
       rather than at the end of the prior command.  Assuming that each unit
       starts out with its current and persistent sets equal, then every unit
       that had processed a command has an incorrect current set.  However, that
       set will not be referenced until another command is issued to the same
       unit.  Resetting the current set at the start of such a command would
       have the same effect as resetting it at the end of the prior command.

       On entry, the device's "unit" field identifies the last unit accessed
       and is used  to reset the current set.  If the new command includes a Set
       Unit complementary to change the target unit, then that unit's current
       set will now be changed.  But the next command directed to that new unit
       will reset its current set.  Each command, therefore, starts with a
       correct current set, and resets occur reliably because each command must
       start with a device command secondary.

       One exception to this is the Copy Data command.  This command is directed
       to unit 15 but changes the current sets of two specified units.  The next
       command entry will reset unit 15's current set but units 0 and 1 (e.g.)
       will retain the wrong current values.  This is handled in the Copy Data
       execution routine itself.

       A pathological command sequence containing a Set Unit after the current
       unit's values have been changed (e.g., Set Length, Set Unit, Set Length)
       could not be accommodated by resetting the last unit's current set at the
       next command entry.  However, this cannot occur, because a Set Unit
       command appearing anywhere other than at the start of a sequence results
       in an Illegal Opcode error.  So at most one unit's set must be reset per
       command phase.

    3. A device enables its poll response to notify the host to send execution
       or reporting secondaries after initiation or completion of a command
       message.  However, the host is not obligated to wait for a poll and may
       initiate the execution or reporting messages before the poll response is
       received.  In simulation, poll response is scheduled as a service event
       that represents the controller overhead.  This event may still be pending
       when the secondaries are received, so the service event is
       unconditionally canceled before proceeding with the next message.

    4. The CS/80 manual says, "Except for the Clear and Cancel commands, a
       command message is valid only if it occurs during the command phase of a
       transaction."  That implies that the Loopback and HP-IB Parity Checking
       transparent commands are invalid if issued in another state.  Enforcing
       this would require delaying the proper-state check until after the opcode
       was received.  We avoid this complication simply by allowing all
       transparent commands regardless of the current state.

    5. When a device or transparent command is initiated, we set the current
       opcode to Invalid, so that the command processor can differentiate
       between the first and subsequent opcodes received.
*/

static void process_secondary (uint32 dc_id, uint32 address, BUS_CABLE bus)
{
DC_STATE *const dc          = dcs [dc_id].sptr;                    /* the pointer to the channel state */
DEVICE   *const dptr        = dcs [dc_id].dptr;                    /* the pointer to the controlling device */
const DSPTR     dsptr       = &dc->devices [address];              /* the pointer to the device state */
const USPTR     usptr       = &dsptr->units [MAPUS (dsptr->unit)]; /* the pointer to the current unit state */
const uint32    secondary   = BUS_ADDRESS (bus);                   /* the secondary address without parity */
const CN_STATE  entry_state = dsptr->state;                        /* the controller state at entry */

sim_cancel (usptr->uptr);                               /* cancel any scheduled event service */

if (dc->listeners & BIT (address))                      /* if this device is listening */
    switch (secondary) {                                /*   then dispatch the listen secondary */

        case 0x05:                                      /* Device Command message */
            if (dsptr->state == Command_Ready           /* if we are ready for a command */
              || dsptr->state == Optional_Wait) {       /*   or an optional report was not taken */
                dc->active = TRUE;                      /*     then start the transaction */

                usptr->persistent.address = usptr->current.address; /* update the address */
                usptr->current = usptr->persistent;                 /*   and reset the current settings */

                dsptr->opcode = Invalid;                /* invalidate the opcode */
                dsptr->state = Command_Wait;            /*   and wait for the device command that must follow */
                }

            else                                            /* otherwise */
                set_error (dc_id, usptr, Message_Sequence); /*   the controller is in the wrong state */
            break;


        case 0x0E:                                      /* Receive Execution message */
            if (dsptr->state == Execution_Wait) {       /* if we are expecting the message */
                dc->active = TRUE;                      /*   then start the transaction */
                dsptr->state = Execution_Receive;       /*     and set up the data reception */

                dsptr->burst_count = LOWER_WORD (usptr->current.burst); /* reset the burst count */
                }

            else {                                              /* otherwise */
                if (dsptr->state != Error_Wait)                 /*   if we are not recovering from an error */
                    set_error (dc_id, usptr, Message_Sequence); /*     then the controller is in the wrong state */

                dsptr->state = Error_Sink;                  /* sink any data sourced to the bus until EOI */
                }
            break;


        case 0x10:                                      /* Amigo Clear */
            dc->active = TRUE;                          /* start the transaction */

            dsptr->opcode = Amigo_Clear;                /* set the command and wait */
            dsptr->state = Parameter_Wait;              /*   for the single control parameter */
            dsptr->index = 0;                           /*     that must follow */
            dsptr->count = 1;                           /*       the secondary */
            break;


        case 0x12:                                      /* Transparent message */
            dc->active = TRUE;                          /* start the transaction */

            if (dsptr->state == Execution_Wait)         /* if we are expecting an execution message */
                dsptr->state = Execution_Receive;       /*   then set up the data reception */

            else {                                      /* otherwise assume a command message */
                dsptr->opcode = Invalid;                /*   so invalidate the opcode */
                dsptr->state = Channel_Wait;            /*     and wait for the channel command */
                }
            break;


        default:                                        /* an unsupported listen secondary was received */
            set_error (dc_id, usptr, Message_Sequence); /*   so log a sequence error */
            dsptr->state = Error_Sink;                  /*     and sink any data sourced to the bus until EOI */
            break;
        }

else                                                    /* otherwise the device is talking */
    switch (secondary) {                                /*   so dispatch the talk secondary */

        case 0x0E:                                      /* Send Execution message */
            if (dsptr->state == Execution_Wait) {       /* if we are expecting the message */
                dc->active = TRUE;                      /*   then start the transaction */
                dsptr->state = Execution_Send;          /*     and set up the data reception */

                dsptr->burst_count = LOWER_WORD (usptr->current.burst); /* reset the burst count */
                }

            else {                                              /* otherwise */
                if (dsptr->state != Error_Wait)                 /*   if we are not recovering from an error */
                    set_error (dc_id, usptr, Message_Sequence); /*     then the controller is in the wrong state */

                dsptr->state = Error_Source;            /* source an error byte to the bus */
                }

            activate_unit (dc_id, usptr, Data_Time);    /* schedule the data transfer */
            break;


        case 0x10:                                      /* Reporting message */
            if (dsptr->state == Reporting_Wait          /* if we are ready to report */
              || dsptr->state == Optional_Wait          /*   or offer a report */
              || dsptr->state == Error_Wait             /*     or recovering from an error */
              || dsptr->state == Command_Ready) {       /*       or in between commands */
                dc->active = TRUE;                      /*         then start the transaction */

                dsptr->opcode = Quick_Status;           /* set up the reporting command */
                dsptr->state = Reporting_Wait;          /*   and the reporting state */

                execute_command (dc_id, address, bus);  /* start the command */
                }

            else                                            /* otherwise */
                set_error (dc_id, usptr, Message_Sequence); /*   the controller is in the wrong state */
            break;


        case 0x12:                                      /* Transparent execution send message */
            if (dsptr->state == Execution_Wait) {       /* if we are expecting the message */
                dc->active = TRUE;                      /*   then start the transaction */
                dsptr->state = Execution_Send;          /*     and set up the data transmission */
                }

            else {                                              /* otherwise */
                if (dsptr->state != Error_Wait)                 /*   if we are not recovering from an error */
                    set_error (dc_id, usptr, Message_Sequence); /*     then the controller is in the wrong state */

                dsptr->state = Error_Source;            /* source an error byte to the bus */
                }
            break;


        default:                                        /* an unsupported talk secondary was received */
            set_error (dc_id, usptr, Message_Sequence); /* abort and source an error byte */
            dsptr->state = Error_Source;                /*   tagged with EOI */

            activate_unit (dc_id, usptr, Data_Time);    /*   and reschedule the data transfer */
            break;
        }

set_response (dc_id, address, CLEAR);                   /* disable PPR for the addressed device */

if (dsptr->state != entry_state)
    tpprintf (dptr, TRACE_STATE, "Device %u transitioned from %s state to %s state\n",
              address, state_names [entry_state], state_names [dsptr->state]);

return;
}


/* Process a data byte.

   This routine is called to process a data byte received from the bus for an
   addressed device.  Each device present on the bus that has been addressed to
   listen receives the byte.  Depending on the controller state, the byte
   represents either a command opcode, a command parameter, or a data value.

   On entry, the "dc_id" parameter is the instance identifier of the DC device
   set connected to the bus, the "address" parameter contains the bus address of
   the target device, which will be a listener, and the "bus" parameter contains
   the set of eight control and eight data lines representing the HP-IB state.

   The routine interprets the data byte in the context of the current device
   controller state, as follows:

     Channel_Wait
     ------------
     The value is an opcode that is used as an index into the channel command
     map to obtain the command.  The only device command allowed within a
     channel command is Set Unit, which is allowed only in the Cancel or Channel
     Independent Clear commands.  If the command is valid, it is executed (no
     channel commands have parameters); otherwise, a Message Sequence error is
     reported.

     Command_Wait
     ------------
     The value is an opcode that is used as an index into the device command map
     to obtain the command.  If the current unit is interlocked, i.e., has an
     unseen QSTAT = 2 pending, and the command is not Set Unit, it is ignored by
     setting the state to Error_Wait.  In this state, any remaining command
     bytes will be sunk, any associated execution message will be sourced or
     sunk, and the reporting phase will report the error.  Otherwise, if the
     opcode is invalid, or the Set Unit complementary is issued in any location
     other than the first (or only) complementary in a command, an Illegal
     Opcode error is reported.  If the command is valid and has associated
     parameters, they are obtained by changing to the Parameter_Wait state, so
     that the following bytes will be interpreted correctly.  If the command has
     no parameters, it is executed.

     Parameter_Wait
     --------------
     The value is a parameter to the current command.  It is placed in the
     device buffer, and the count of parameters remaining to be received is
     decremented.  If the byte is tagged with EOI, and the parameter count is
     non-zero, an Illegal Parameter error is reported unless the command allows
     a variable number of parameters (only the Initiate Utility and Initiate
     Utility Read commands do so).  Otherwise, all expected parameters have been
     received, so the pending command is executed.

     Execution_Receive
     -----------------
     The value is a data byte received during the execution phase.  It is placed
     in the device buffer and the byte counts remaining in the buffer and the
     transaction are decremented.  If a byte tagged with EOI arrives before the
     transfer length is exhausted, or if it does not arrive when the length
     reaches zero, a Message Length error is reported; for the latter case, we
     also change the state to sink the extraneous bytes offered by the host.
     The routine then proceeds as below.

     Error_Sink
     ----------
     The value is a data byte that is discarded due to a previous error.  If the
     byte is tagged with EOI, the remaining transaction count is set to zero to
     end the transaction.  The NRFD signal is asserted to suspend further
     transfers, and a service event is scheduled to continue the reception after
     the data transfer time elapses.

     Error_Wait
     ----------
     The value is a command opcode or parameter that is discarded due to a
     previous error.  As soon as an error is detected in the command stream, all
     additional bytes are discarded.  When EOI is seen, a service event is
     scheduled to enable the poll response to notify the host of the error.

   Entry in any other state results in a Message Sequence error.


   Implementation notes:

    1. Illegal channel command opcodes are reported as sequence errors instead
       of the more logical opcode errors, per the CS/80 reference manual.

    2. Channel commands are not inhibited by the unseen QSTAT interlock.

    3. A length mismatch in the Execution_Receive state for the Read/Write
       Loopback commands reports a Channel Parity error according to the CS/80
       manual but a Message Length error according to the SS/80 manual.  The
       latter seems more reasonable, so that is what we implement.

    4. NRFD assertion uses the "dc->bus" value rather than the "bus" value
       because the former has the DAV and EOI signals denied, which is necessary
       for the control change.
*/

static void process_data (uint32 dc_id, uint32 address, BUS_CABLE bus)
{
DC_STATE *const dc          = dcs [dc_id].sptr;                     /* the pointer to the channel state */
DEVICE   *const dptr        = dcs [dc_id].dptr;                     /* the pointer to the controlling device */
const uint32    channel     = CHANNEL (dptr->flags);                /* the channel address of the bus controller */
const DSPTR     dsptr       = &dc->devices [address];               /* the pointer to the device state */
const USPTR     usptr       = &dsptr->units [MAPUS (dsptr->unit)];  /* the pointer to the current unit state */
const uint8     data        = BUS_DATA (bus);                       /* the data value from the bus */
const CN_STATE  entry_state = dsptr->state;                         /* the controller state at entry */
CN_OPCODE       last_opcode;

switch (dsptr->state) {                                 /* dispatch the controller state */

    case Channel_Wait:                                  /* waiting for a channel command opcode */
        last_opcode = dsptr->opcode;                    /* save the prior opcode, if any */

        if (data < CHAN_COUNT)                          /* if the opcode is in range */
            dsptr->opcode = channel_map [data];         /*   then map the opcode to a channel command */

        else if (data < CMD_COUNT                       /* otherwise if it's a device command */
          && command_map [data] == Set_Unit             /*   specifically Set Unit */
          && not (bus & Bus_EOI))                       /*     and EOI is not present */
            dsptr->opcode = Set_Unit;                   /*       then assign the the opcode */

        else                                            /* otherwise the opcode is out of range */
            dsptr->opcode = Invalid;                    /*   and so the command is invalid */

        if (dsptr->opcode == Invalid                        /* if the transparent command is invalid */
          || last_opcode == Set_Unit                        /*   or if Set Unit precedes a command */
          && dsptr->opcode != Cancel                        /*     other than Cancel */
          && dsptr->opcode != Channel_Independent_Clear)    /*       or Channel Independent Clear */
            set_error (dc_id, usptr, Message_Sequence);     /*         then report a sequence error */

        else {                                          /* otherwise */
            dsptr->count =                              /*   get the count of associated parameters */
              cmd_props [dsptr->opcode].parameter_count;

            if (dsptr->count > 0) {                     /* if parameters are expected */
                dsptr->index = 0;                       /*   then clear the buffer index */
                dsptr->state = Parameter_Wait;          /*     and wait for the first parameter */
                }

            else                                        /* otherwise */
                execute_command (dc_id, address, bus);  /*   start the command */
            }
        break;


    case Command_Wait:                                  /* waiting for a device command opcode */
        last_opcode = dsptr->opcode;                    /* save the prior opcode, if any */

        if (data < CMD_COUNT)                           /* if the opcode is in range */
            dsptr->opcode = command_map [data];         /*   then map the opcode to a device command */
        else                                            /* otherwise the opcode is out of range */
            dsptr->opcode = Invalid;                    /*   and so the command is invalid */

        if (usptr->interlocked                          /* if the current unit is interlocked */
          && dsptr->opcode != Set_Unit) {               /*   and the command is not Set Unit */
            dsptr->state = Error_Wait;                  /*     then ignore the command */

            tpprintf (dptr, TRACE_CMD, "Device %u %s command inhibited by unseen qstat\n",
                      address, opcode_names [dsptr->opcode]);

            if (bus & Bus_EOI) {                                /* if this is the last byte from the host */
                activate_unit (dc_id, usptr, Controller_Time);  /*   then schedule the poll enable */
                dc->active = FALSE;                             /*     and end the transaction */
                }
            }

        else if (dsptr->opcode == Invalid               /* otherwise if the command is invalid */
          || dsptr->opcode == Set_Unit                  /*   or the command is Set Unit */
          && last_opcode != Invalid)                    /*     but it's not the first command */
            set_error (dc_id, usptr, Illegal_Opcode);   /*       then report an illegal opcode */

        else {                                          /* otherwise */
            dsptr->count =                              /*   get the count of associated parameters */
              cmd_props [dsptr->opcode].parameter_count;

            if (dsptr->count == 0)                      /* if the command has no parameters */
                execute_command (dc_id, address, bus);  /*   then start it */

            else if (bus & Bus_EOI)                             /* otherwise if needed parameters are missing */
                set_error (dc_id, usptr, Illegal_Parameter);    /*   then a parameter error occurs */

            else {                                      /* otherwise */
                dsptr->index = 0;                       /*   then clear the buffer index */
                dsptr->state = Parameter_Wait;          /*     and wait for the first parameter */
                }
            }
        break;


    case Parameter_Wait:                                /* waiting for a parameter */
        dsptr->buffer [dsptr->index++] = data;          /* add the byte to the buffer */
        dsptr->count--;                                 /*   and count it */

        if (bus & Bus_EOI)                                  /* if this is the last parameter */
            if (dsptr->opcode >= Initiate_Utility           /*   then if the command has */
              && dsptr->opcode <= Initiate_Utility_Read)    /*     a variable number of parameters */
                dsptr->count = 0;                           /*       then proceed to execute it */

            else if (dsptr->count > 0)                          /*   otherwise if more parameters are needed */
                set_error (dc_id, usptr, Illegal_Parameter);    /*     then a parameter error occurred */

        if (dsptr->count == 0)                          /* if all of the expected parameters are in */
            execute_command (dc_id, address, bus);      /*   then execute the command */
        break;


    case Execution_Receive:                             /* receiving data from the host */
        if (usptr->current.length > 0) {                /* if there is more to receive */
            dsptr->buffer [dsptr->index++] = data;      /*   then add the byte to the buffer */
            dsptr->count--;                             /*     and drop the buffer */
            usptr->current.length--;                    /*       and transmission counts */
            }

        if (bus & Bus_EOI) {                                /* if this is the last byte from the host */
            if (usptr->current.length > 0                   /*   then if more bytes are expected */
              && not (dsptr->burst_count == 1               /*     and this is not the last byte of a burst */
              && usptr->current.burst & BURST_TAG))         /*     that terminates with EOI  */
                set_error (dc_id, usptr, Message_Length);   /*       then report a Message Length error */
            }

        else if (usptr->current.length == 0) {          /* otherwise if more are coming but are not wanted */
            set_error (dc_id, usptr, Message_Length);   /*   then report a Message Length error */

            usptr->current.length = D32_UMAX;           /* sink as many bytes as the host wants to send */
            dsptr->state = Error_Sink;                  /*   until one is tagged with EOI */
            }

        dc->bus = hpib_source (channel, address, dc->bus | Bus_NRFD);   /* assert NRFD to hold off the talker */

        activate_unit (dc_id, usptr, Data_Time);        /* schedule the reception */
        break;


    case Error_Sink:                                    /* sinking data bytes after an error */
        if (bus & Bus_EOI)                              /* if this is the last byte from the host */
            usptr->current.length = 0;                  /*   then terminate the transfer with this byte */

        dc->bus = hpib_source (channel, address, dc->bus | Bus_NRFD);   /* assert NRFD to hold off the talker */

        activate_unit (dc_id, usptr, Data_Time);        /* schedule the reception */
        break;


    case Error_Wait:                                        /* sinking command bytes after an error */
        if (bus & Bus_EOI) {                                /* if this is the last byte from the host */
            activate_unit (dc_id, usptr, Controller_Time);  /*     and schedule the poll enable */
            dc->active = FALSE;                             /*       and end the transaction */
            }
        break;


    default:                                            /* data was received in the wrong state */
        set_error (dc_id, usptr, Message_Sequence);     /*   so log the error and sink any additional data */
        break;
    }                                                   /* end of state dispatch */

if (dsptr->state != entry_state)
    tpprintf (dptr, TRACE_STATE, "Device %u transitioned from %s state to %s state\n",
              address, state_names [entry_state], state_names [dsptr->state]);

return;
}


/* Execute a CS/80 command.

   This routine is called to execute device and channel commands received from
   the bus for an addressed device.  Each device present on the bus that has
   been addressed to listen receives the command, although in practice only one
   device is addressed to listen at a time.

   On entry, the "dc_id" parameter is the instance identifier of the DC device
   set connected to the bus, the "address" parameter contains the bus address of
   the target device, which will be a listener, and the "bus" parameter contains
   the set of eight control and eight data lines representing the HP-IB state.
   The command to execute is present in the "opcode" field of the device state
   structure, and any parameters required by the command have already been
   received and are present in the "buffer" state field.  The "index" field
   reflects the number of received parameters for those commands that take
   variable numbers of parameters.

   This routine implements the following CS/80 commands:

     Type  Opcode  Operation
     ----  ------  -----------------------------------------
     Dev    00H    Locate and Read
     Dev    02H    Locate and Write
     Dev    04H    Locate and Verify
     Dev    08H    Copy Data
     Dev    0AH    Cold Load Read
     Dev    0DH    Request Status
     Dev    0EH    Release
     Dev    0FH    Release Denied
     Dev    10H    Set Address (single-vector)
     Dev    11H    Set Address (three-vector)
     Dev    12H    Set Block Displacement
     Dev    18H    Set Length
     Dev    2nH    Set Unit (n = 0-15)
     Dev    30H    Initiate Utility, no execution message
     Dev    32H    Initiate Utility, send execution message
     Dev    34H    No Operation
     Dev    35H    Describe
     Dev    37H    Initialize Media
     Dev    3EH    Set Status Mask
     Dev    4nH    Set Volume (n = 0-7)
     Dev    48H    Set Return Addressing Mode
     Dev    49H    Write File Mark
     Dev    4AH    Unload

     Chan   01H    HP-IB Parity Checking
     Chan   02H    Read Loopback
     Chan   03H    Write Loopback
     Chan   08H    Channel Independent Clear
     Chan   09H    Cancel
     Chan   ---    Amigo Clear
     Chan   ---    Quick Status (QSTAT)
     Chan   ---    Identify

   The following CS/80 commands are accepted, but they have no effect on
   operations, i.e., they report success but are otherwise ignored:

     Type  Opcode  Operation
     ----  ------  -----------------------------------------
     Dev    33H    Initiate Diagnostic
     Dev    38H    Set Options
     Dev    39H    Set RPS
     Dev    3AH    Set Retry Time
     Dev    3BH    Set Release

   The following CS/80 commands are not currently implemented and report an
   Illegal Opcode or Parameter Bounds error:

     Type  Opcode  Operation
     ----  ------  -------------------------------------------
     Dev    06H    Spare Block
     Dev    31H    Initiate Utility, receive execution message

   Commands either execute to completion here (e.g., Set Unit, Locate and
   Verify) or complete after transferring data in the service routine (e.g.,
   Locate and Write, Request Status).  Only commands without execution phases,
   including those commands with omitted phases, such as a zero-length Locate
   and Read, complete here.  If the command completes here, a completion trace
   is reported; otherwise, it will be reported at the end of the execution
   phase.

   A device command sequence contains one primary command, tagged with EOI, that
   ends the sequence.  An optional set of complementary commands may precede
   some primary commands.  No complementary command has an execution phase, so
   they are all executed here as they arrive.  After reception and execution of
   the primary command, the service routine is activated to enable parallel poll
   to prompt the host to issue the required execution phase or reporting phase
   secondary.  The activation delay represents the controller execution time
   and, in the case of commands that access media, the media access time.

   Errors are handled here, unless the command has an execution phase, in which
   case the data to be transferred is either sourced or sunk in the service
   routine.  When an error occurs, the controller transitions to the Error_Wait
   state.  If the error occurs while processing an intermediate complementary
   command, the Error_Wait state causes all following commands, including the
   terminating primary command, to be sunk and ignored by our caller, so we are
   not called for them.  If the error occurs on the primary command, the host
   will not discover this until any required execution phase is performed.  In
   this case, the service routine is entered in the Error_Wait state, which
   enables the poll response.  When the host sends the execution secondary, the
   controller transitions from the Error_Wait to the Error_Source (for a talk
   secondary) or Error_Sink (for a listen secondary) state.  In these states,
   the service routine either sources one byte tagged with EOI or sinks bytes
   from the host until EOI is received.  These error states then transition to
   the reporting phase to deliver the QSTAT error code.

   A complication occurs with the Illegal Opcode error.  Because the opcode is
   unrecognized, the controller does not know if the host plans to issue an
   execution message.  The Error_Wait state takes care of this, as entry into
   either the execution phase or reporting phase are allowed in this state, and
   reception of the corresponding secondary will indicate the host's
   expectation.

   Before dispatching the command, the routine verifies that the unit selected
   is valid for the command.  Some commands cannot be addressed to unit 15 (the
   controller unit), while others must be addressed to unit 15.  An error of
   this type is reported as an Illegal Opcode.

   Also, all primary commands must be tagged with EOI, and a complementary
   command may be tagged with EOI to end the command sequence.  A command that
   requires EOI but is not so tagged causes a Message Length error.

   After the command is initiated or completed, the presence of EOI is checked.
   If EOI is not present, the controller remains in the Command_Wait or
   Channel_Wait state, and the routine returns to process the next command in
   the sequence.  If EOI us present, the sequence ends.  If an error occurred,
   the routine exits with the controller in the Error_Wait state; recovery will
   occur when the host sends either an execution message or a reporting message.
   If no error occurred, the controller is moved to the next state dictated by
   the command.  If a complementary command ended the sequence, the persistent
   operational state is set to the current state.  In either case, a service
   event is scheduled to complete the command sequence.


   Implementation notes:

    1. The Copy Data command executes entirely within this routine, as it has no
       execution phase.  The read and write block routines handle the difference
       in block sizes between the disc and tape units.

    2. Specifying a full-volume transfer for the Copy Data command uses the
       source-unit size to determine the transfer size.  This is accomplished in
       the "validate_access" command, which detects Length = -1 and resets the
       length to that remaining between the starting address and the end of the
       volume, so the last read terminates normally.  If that transfer size
       exceeds the space remaining on the target unit, an End of Volume error
       will occur.  In particular, multi-tape copies are not supported.

    3. Normally, a unit's current operational values are reset to the persistent
       set at the start of the next command phase.  The Copy Data command is an
       exception to this action, as it is directed to unit 15 but changes the
       current sets of two units specified in the command.  The next command
       entry will reset unit 15's current set but units 0 and 1 (e.g.) will
       retain the wrong current values.  Consequently, we must reset the values
       here (Copy Data has no execution phase, so the command is complete when
       we return).  Also, Copy Data must check unit interlocks explicitly.  If
       unit 15 is not interlocked, the command will be initiated, but it must
       not execute if either specified unit is still interlocked.

    4. Of the device utilities (send execution message), we implement only C3H
       Read Revision Numbers, C5H Read [Runtime] Error Log, and C6H (Read ERT
       Log) as these are required by the RTE EXER, DVM33, and TAPE programs,
       respectively.  In theory, these should return device-specific
       information, but in practice the values are used in very limited ways
       (e.g., Read Error Log is used by DVM33 only to determine if an inserted
       tape cartridge is certified).

       Of the device utilities (no execution message), we implement only C8H
       Pattern Error-Rate Test, as this is required by the TAPE program CERT
       command.  We implement only Type 2, Certification.  All other types and
       utilities report a Parameter Bounds error.

    5. Certain error-specific information is returned in the parameter field
       bytes of the Request Status command.  The errors that return information
       are Cross-Unit, Unrecoverable Data, Diagnostic Result, Request Release,
       Operator Request, Diagnostic Request, Internal Maintenance, Marginal
       Data, Recoverable Data, and Maintenance Track Overflow.  The Spare Block
       command also sets the parameter field.  Of these, however, only the
       Cross-Unit and Unrecoverable Data errors are implemented.  If no errors
       are present, the parameter field contains the current unit address.

       The last four bytes of the parameter field are used to report run-time
       device errors ("DERRORS").  We include error D1, "Not Certified," if an
       Uninitialized Media error is being reported.

    6. The Set Block Displacement command uses a six-byte twos-complement
       displacement that is added to the current address.  We maintain only a
       four-byte address, as the largest supported disc drive uses only 22 bits
       of the address, and eight-byte integers are slow on 32-bit platforms.  To
       ensure that overflow from adding the displacement is detected, we
       restrict the displacement value to three bytes, which would accommodate
       discs up to 4 GB in size.

    7. All device commands enable poll response after the end of the command
       message and again after the end of the execution message, if present.
       Transparent messages enable poll response only after the clear commands
       (Universal Device Clear, Selected Device Clear, Channel Independent
       Clear, Amigo Clear) and Cancel.  All other transparent commands or
       executions leave the poll response disabled.

    8. Only device commands can have parameters in the middle of a command
       message; parameters for channel commands are always at the end of the
       message, i.e., end with EOI.  So it is safe to move the controller state
       from Parameter_Wait to Command_Wait if EOI is not present.

    9. The media_action table below is derived from a table on page 11 of the
       "LINUS External Reference Specification" that describes the results of
       the Initialize Media command for each of the eight possible CWZ option
       combinations and three initial tape conditions.  The "Decertified" action
       arises from changing the spare blocks table and is confirmed in the INIT
       MEDIA command description of the CS/80 External Exerciser TAPE module.

   10. The "%.0u" print specification in the Set Burst trace call absorbs the
       zero block count parameter without printing when the command disables
       burst mode.
*/

static void execute_command (uint32 dc_id, uint32 address, BUS_CABLE bus)
{
static const MEDIA_STATE media_action [8] [3] = {       /* Initialize Media actions, indexed by option byte and state */
/*    Formatted    Initialized  Certified   */          /*   C W Z */
/*    -----------  -----------  ----------- */          /*   - - - */
    { Certified,   Certified,   Certified   },          /*   0 0 0 */
    { Certified,   Certified,   Recertified },          /*   0 0 1 */
    { Initialized, Initialized, Decertified },          /*   0 1 0 */
    { Initialized, Initialized, Decertified },          /*   0 1 1 */
    { Initialized, Initialized, Certified   },          /*   1 0 0 */
    { Initialized, Initialized, Decertified },          /*   1 0 1 */
    { Initialized, Initialized, Decertified },          /*   1 1 0 */
    { Initialized, Initialized, Decertified }           /*   1 1 1 */
    };
static const char *execution [] = {                     /* execution state names, indexed by Boolean completion */
    "executing",                                        /*   FALSE = command is executing */
    "executed"                                          /*   TRUE  = command is complete */
    };
DC_STATE *const dc       = dcs [dc_id].sptr;            /* the pointer to the channel state */
DEVICE   *const dptr     = dcs [dc_id].dptr;            /* the pointer to the controlling device */
const DSPTR     dsptr    = &dc->devices [address];      /* the pointer to the current device state */
const uint8     cwz_mask = 007;                         /* the Initialize Media option bits mask */
USPTR           susptr, tusptr;
DDPTR           ddptr;
UDPTR           udptr;
uint8           *bptr, unit, index;
int32           displacement;
uint32          unit_map, block, cylinder, head, sector, significand, count;
uint32          source_unit, source_volume, source_address;
uint32          target_unit, target_volume, target_address;
MEDIA_STATE     current_state, new_state;
UPTR            suptr, tuptr;
t_stat          status;
USPTR           usptr = &dsptr->units [MAPUS (dsptr->unit)];    /* the pointer to the current unit state */
UPTR            uptr = usptr->uptr;                             /* the pointer to the current unit */
t_bool          completed = TRUE;                               /* TRUE if the command has completed */
t_bool          reported = FALSE;                               /* TRUE if the command trace has been reported */
t_bool          unsupported = FALSE;                            /* TRUE if the command or utility is unsupported */
t_bool          no_execution = FALSE;                           /* TRUE if the command has no execution state */
DELAY           activation_delay = Controller_Time;             /* the type of service event delay */

tpprintf (dptr, TRACE_INCO, "Device %u %s command started\n",
          address, opcode_names [dsptr->opcode]);

if (dsptr->unit == 15                                   /* if unit 15 is selected */
  && cmd_props [dsptr->opcode].unit_15 == No            /*   but the command does not permit it */
  || dsptr->unit != 15                                  /* or if another unit is selected */
  && cmd_props [dsptr->opcode].unit_15 == Only)         /*   but the command requires unit 15 */
    set_error (dc_id, usptr, Illegal_Opcode);           /*     then report an illegal opcode error */

else if (not (bus & Bus_EOI)                            /* otherwise if the command isn't tagged with EOI */
  && cmd_props [dsptr->opcode].EOI_required)            /*   but must be */
    set_error (dc_id, usptr, Message_Length);           /*     then report a message length error */

else switch (dsptr->opcode) {                           /* otherwise dispatch the command opcode */

    case Locate_and_Read:                                   /* Locate and Read */
    case Cold_Load_Read:                                    /* Cold Load Read */
        if (validate_access (dc_id, usptr, Read_Access)) {  /* validate the access; if it succeeds */
            if (usptr->current.length == 0)                 /*   then if a zero-length read is specified */
                no_execution = TRUE;                        /*     then there is no execution phase */
            else                                            /*   otherwise */
                read_block (dc_id, usptr);                  /*     read the first block into the device buffer */

            activation_delay = Position_Time;           /* delay for the positioning time */
            }
        break;


    case Locate_and_Write:                                  /* Locate and Write */
        if (validate_access (dc_id, usptr, Write_Access)) { /* validate the access; if it succeeds */
            if (usptr->current.length == 0)                 /*   then if a zero-length write is specified */
                no_execution = TRUE;                        /*     then there is no execution phase */

            activation_delay = Position_Time;           /* delay for the positioning time */
            }
        break;


    case Locate_and_Verify:                                     /* Locate and Verify */
        if (validate_access (dc_id, usptr, Read_Access)) {      /* validate the access; if it succeeds */
            while (usptr->current.length > 0)                   /*   then while there are bytes to verify */
                if (read_block (dc_id, usptr))                  /*     read the next block; if it succeeds */
                    if (usptr->current.length >= dsptr->count)  /*       then if verification is not complete */
                        usptr->current.length -= dsptr->count;  /*         then drop the remaining length */
                    else                                        /*       otherwise */
                        usptr->current.length = 0;              /*         we are finished */

                else                                    /* otherwise the read failed */
                    break;                              /*   so quit at the failing block */

            activation_delay = Position_Time;           /* schedule the cumulative verification delay */
            }
        break;


    case Copy_Data:
        source_unit   = COPY_UNIT (dsptr->buffer [0]);      /* get the source unit */
        source_volume = COPY_VOLUME (dsptr->buffer [0]);    /*   and volume numbers */

        if (BIT (source_unit) & dsptr->valid_units) {   /* if the source unit is valid */
            susptr = &dsptr->units [source_unit];       /*   then point at the unit state */

            if (BIT (source_volume) & susptr->valid_volumes) {  /* if the source volume is valid */
                suptr = susptr->uptr;                           /*   then point at the SIMH unit */

                if (susptr->interlocked)                    /* if the source unit is interlocked */
                    set_error (dc_id, usptr, Cross_Unit);   /*   then abort with a cross-unit error */
                }

            else {                                              /* otherwise */
                suptr = NULL;                                   /*   there is no valid volume */
                set_error (dc_id, usptr, Module_Addressing);    /*     so report an addressing error */
                }
            }

        else {                                              /* otherwise */
            susptr = NULL;                                  /*   neither the unit */
            suptr = NULL;                                   /*     nor the volume is valid */
            set_error (dc_id, usptr, Module_Addressing);    /*       so report an addressing error */
            }

        source_address = 0;                             /* clear the source address */

        if (dsptr->state != Error_Wait) {               /* if no error is pending */
            dsptr->index = 2;                           /*   then set the index of the address parameters */

            switch (dsptr->buffer [1]) {                /* dispatch the source addressing opcode */

                case COPY_BLOCK:                                    /* if a single-vector address is specified */
                    if (set_block (susptr))                         /*   then set it; if it is within range */
                        source_address = susptr->current.address;   /*     then save the address */

                    else {                                          /* otherwise */
                        susptr->current.address = 0;                /*   clear the address */
                        set_error (dc_id, susptr, Address_Bounds);  /*     and report an addressing error */
                        }
                    break;

                case COPY_CHS:                                      /* if a three-vector address is specified */
                    if (set_chs (susptr))                           /*   then set it; if it is within range */
                        source_address = susptr->current.address;   /*     then save the address */

                    else {                                          /* otherwise */
                        susptr->current.address = 0;                /*   clear the address */
                        set_error (dc_id, susptr, Address_Bounds);  /*     and report an addressing error */
                        }
                    break;

                default:                                        /* otherwise */
                    set_error (dc_id, usptr, Illegal_Opcode);   /*   the addressing opcode is illegal */
                    break;
                }
            }

        target_unit   = COPY_UNIT   (dsptr->buffer [8]);    /* get the target unit */
        target_volume = COPY_VOLUME (dsptr->buffer [8]);    /*   and volume numbers */

        if (BIT (target_unit) & dsptr->valid_units) {   /* if the target unit is valid */
            tusptr = &dsptr->units [target_unit];       /*   then point at the unit state */

            if (BIT (target_volume) & tusptr->valid_volumes) {  /* if the target volume is valid */
                tuptr = tusptr->uptr;                           /*   then point at the SIMH unit */

                if (tusptr->interlocked)                    /* if the target unit is interlocked */
                    set_error (dc_id, usptr, Cross_Unit);   /*   then abort with a cross-unit error */
                }

            else {                                              /* otherwise */
                tuptr = NULL;                                   /*   there is no valid volume */
                set_error (dc_id, usptr, Module_Addressing);    /*     so report an addressing error */
                }
            }

        else {                                              /* otherwise */
            tusptr = NULL;                                  /*   neither the unit */
            tuptr = NULL;                                   /*     nor the volume is valid */
            set_error (dc_id, usptr, Module_Addressing);    /*       so report an addressing error */
            }

        target_address = 0;                             /* clear the target address */

        if (dsptr->state != Error_Wait) {               /* if no error is pending */
            dsptr->index = 10;                          /*   then set the index of the address parameters */

            switch (dsptr->buffer [9]) {                /* dispatch the target addressing opcode */

                case COPY_BLOCK:                                    /* if a single-vector address is specified */
                    if (set_block (tusptr))                         /*   then set it; if it is within range */
                        target_address = tusptr->current.address;   /*     then save the address */

                    else {                                          /* otherwise */
                        tusptr->current.address = 0;                /*   clear the address */
                        set_error (dc_id, tusptr, Address_Bounds);  /*     and report an addressing error */
                        }
                    break;

                case COPY_CHS:                                      /* if a three-vector address is specified */
                    if (set_chs (tusptr))                           /*   then set it; if it is within range */
                        target_address = tusptr->current.address;   /*     then save the address */

                    else {                                          /* otherwise */
                        tusptr->current.address = 0;                /*   clear the address */
                        set_error (dc_id, tusptr, Address_Bounds);  /*     and report an addressing error */
                        }
                    break;

                default:                                        /* otherwise */
                    set_error (dc_id, usptr, Illegal_Opcode);   /*   the addressing opcode is illegal */
                    break;
                }
            }

        tpprintf (dptr, TRACE_CMD, "Device %u Copy Data %u bytes from unit %u volume %u address %u"
                                   " to unit %u volume %u address %u executing\n",
                  address, usptr->current.length,
                  source_unit, source_volume, source_address,
                  target_unit, target_volume, target_address);

        if (dsptr->state != Error_Wait) {                           /* if no error is pending */
            if (validate_access (dc_id, susptr, Read_Access)        /*   then if source */
              && validate_access (dc_id, tusptr, Write_Access)) {   /*     and target validations succeed */
                if (susptr->block_size > tusptr->block_size)        /*       then set the transfer size */
                    dsptr->size = susptr->block_size;               /*         to that of the device */
                else                                                /*           with the */
                    dsptr->size = tusptr->block_size;               /*             larger block size */

                while (usptr->current.length > 0) {             /* while bytes remain to be copied */
                    if (dsptr->size > usptr->current.length)    /* if the remaining length doesn't fill the buffer */
                        dsptr->size = usptr->current.length;    /*   then reduce the buffer length to match */

                    if (read_block (dc_id, susptr)) {   /* read the next block; if it succeeds */
                        dsptr->index = dsptr->count;    /*   show that the buffer is occupied */

                        if (write_block (dc_id, tusptr))            /* write the next block; if it succeeds */
                            usptr->current.length -= dsptr->size;   /*   then decrement the remaining length */
                        else                                        /* otherwise the write failed */
                            break;                                  /*   so abort the copy at this point */
                        }

                    else                                /* otherwise the read failed */
                        break;                          /*   so abort the copy at this point */
                    }                                   /* continue until the copy is complete */

                susptr->persistent.address = susptr->current.address;   /* update the source */
                tusptr->persistent.address = tusptr->current.address;   /*   and target addresses */
                }

            uptr->wait = suptr->wait + tuptr->wait;     /* combine the source and target delays */
            activation_delay = Position_Time;           /*   and schedule the copy delay */
            }

        if (susptr != NULL)                             /* if the source unit was valid */
            susptr->current = susptr->persistent;       /*   then reset the current state */

        if (tusptr != NULL)                             /* if the target unit was valid */
            tusptr->current = tusptr->persistent;       /*   then reset the current state */

        reported = TRUE;
        break;


    case Request_Status:                                    /* Request Status */
        dsptr->buffer [0] = TO_RS_VOLUME (dsptr->volume)    /* set the identification byte */
                              | TO_RS_UNIT (dsptr->unit);   /*   to reflect the current unit and volume */

        dsptr->buffer [1] = 0xFF;                       /* assume no units have status pending */

        unit_map = dsptr->valid_units & ~BIT (dsptr->unit); /* get the map of valid units without the current unit */

        while (unit_map != 0) {                         /* loop through the units */
            unit = (uint8) BIT_NUMBER (unit_map);       /*   and get the next unit number */

            if (dsptr->units [MAPUS (unit)].qstat > 0) {    /* if the unit has has pending status */
                dsptr->buffer [1] = unit;                   /*   then save */
                break;                                      /*     the lowest unit number */
                }

            unit_map ^= BIT (unit);                     /* clear the current bit */
            }

        memcpy (&dsptr->buffer [2], usptr->status,      /* copy the unit status fields to the buffer */
                sizeof usptr->status);

        bptr = &dsptr->buffer [10];                     /* point at the first parameter byte */
        memset (bptr, 0, 10);                           /*   and clear the ten parameter bytes */

        if (TEST_ERR (usptr->status, Cross_Unit)) {     /* if a cross-unit error is present */
            unit_map = dsptr->valid_units;              /*   then get the map of valid units */

            while (unit_map != 0) {                     /* loop through the units */
                unit = (uint8) BIT_NUMBER (unit_map);   /*   and get the next unit number */

                if (dsptr->units [MAPUS (unit)].qstat > 0)  /* if the unit has has pending status */
                    *bptr++ = unit;                         /*   then store the unit number */

                unit_map ^= BIT (unit);                 /* clear the current bit */
                }

            *bptr = 0xFF;                               /* add an end-of-list marker */
            }

        else if (TEST_ERR (usptr->status, Operator_Request)) {  /* otherwise if a release request is present */
            unit_map = dsptr->valid_units;                      /*   then get the map of valid units */

            while (unit_map != 0) {                     /* loop through the units */
                unit = (uint8) BIT_NUMBER (unit_map);   /*   and get the next unit number */

                if (dsptr->units [MAPUS (unit)].release_pending)    /* if the unit has a pending release */
                    *bptr++ = unit;                                 /*   then store the unit number */

                unit_map ^= BIT (unit);                 /* clear the current bit */
                }

            *bptr = 0xFF;                               /* add an end-of-list marker */
            }

        else {                                                  /* otherwise report a block address */
            if (TEST_ERR (usptr->status, Unrecoverable_Data))   /* if an Unrecoverable Data error is present */
                block = usptr->bad_address;                     /*   then report the bad address */
            else                                                /* otherwise */
                block = usptr->current.address;                 /*   report the current address */

            if (usptr->current.return_mode == 0) {      /* if in single-vector mode */
                bptr += 2;                              /*   skip the first two zero bytes */
                TO_4_BYTES (bptr, block);               /*     and return a block address */
                }

            else {                                      /* otherwise */
                cylinder = block / usptr->step_size;    /*   split out the cylinder */
                head     = (block % usptr->step_size)   /*     head */
                             / usptr->track_size;       /*       and sector */
                sector   = block % usptr->track_size;   /*         from the block address */

                TO_3_BYTES (bptr, cylinder);            /* return the */
                TO_1_BYTE  (bptr, head);                /*   address in */
                TO_2_BYTES (bptr, sector);              /*     three-vector form */
                }
            }

        if (TEST_ERR (usptr->status, Uninitialized_Media)   /* if an Unrecoverable Data error is present */
          && usptr->tape_drive                              /*   for a tape unit */
          && STATE (uptr->User_Flags) != Certified)         /*     that is not certified */
            dsptr->buffer [16] = 0xD1;                      /*       then include a Not Certified drive error */

        dsptr->index = 0;                               /* reset the index to the first return byte */
        dsptr->count = 20;                              /*   and set the count of bytes to return */

        tpprintf (dptr, TRACE_CMD, "Device %u Request Status unit %u volume %u "
                                   "%02X-%02X %02X-%02X %02X-%02X %02X-%02X"
                                   "%s%.*u executing\n",
                  address, LOWER_HALF (dsptr->buffer [0]), UPPER_HALF (dsptr->buffer [0]),
                  dsptr->buffer [2], dsptr->buffer [3], dsptr->buffer [4], dsptr->buffer [5],
                  dsptr->buffer [6], dsptr->buffer [7], dsptr->buffer [8], dsptr->buffer [9],
                  (dsptr->buffer [1] == 255 ? "" : " pending status unit "),
                  (dsptr->buffer [1] == 255 ? 0 : 1),
                  (dsptr->buffer [1] == 255 ? 0 : dsptr->buffer [1]));

        reported = TRUE;
        break;


    case Release:                                       /* Release */
    case Release_Denied:                                /* Release Denied */
        if (dsptr->unit == 15)                              /* if the controller is addressed */
            unit_map = dsptr->valid_units & DRIVE_UNITS;    /*   then get all units except the controller */
        else                                                /* otherwise */
            unit_map = BIT (dsptr->unit);                   /*   get just the selected unit */

        while (unit_map != 0) {                         /* process the unit release(s) */
            unit = (uint8) BIT_NUMBER (unit_map);       /* get the unit number */
            usptr = &dsptr->units [MAPUS (unit)];       /*   and the unit state pointer */

            if (usptr->release_pending) {               /* if a release request is pending */
                usptr->release_pending = FALSE;         /*   then clear it */

                if (dsptr->opcode == Release) {         /* if the host granted the request */
                    status = dc_detach (usptr->uptr);   /*   then detach the media image */

                    if (status == SCPE_OK)              /* if the detach succeeded, report success */
                        cprintf ("Bus %u unit %u released and detached\n",
                                 address, unit);
                    }

                else                                    /* otherwise report that the host denied the request */
                    cprintf ("Bus %u unit %u release denied\n",
                             address, unit);
                }

            unit_map ^= BIT (unit);                     /* clear the current bit */
            }                                           /*   and continue until all units are processed */
        break;


    case Set_Block_Address:                             /* Set Address (single-vector) */
        dsptr->index = 0;                               /* set the index of the address parameters */

        if (not set_block (usptr)) {                    /* set the address; if it is out of range */
            usptr->current.address = 0;                 /*   then clear the address */
            set_error (dc_id, usptr, Address_Bounds);   /*     and report an addressing error */
            }

        tpprintf (dptr, TRACE_CMD, "Device %u Set Address %u executed\n",
                  address, usptr->current.address);

        reported = TRUE;
        break;


    case Set_CHS_Address:                               /* Set Address (three-vector */
        if (usptr->tape_drive)                          /* if the target unit is a tape drive */
            set_error (dc_id, usptr, Illegal_Opcode);   /*   then it does not support this command */

        else {                                          /* otherwise */
            dsptr->index = 0;                           /*   set the index of the address parameters */

            if (not set_chs (usptr)) {                      /* set the address; if it is out of range */
                usptr->current.address = 0;                 /*   then clear the address */
                set_error (dc_id, usptr, Address_Bounds);   /*     and report an addressing error */
                }
            }

        tpprintf (dptr, TRACE_CMD, "Device %u Set Address %u (%u/%u/%u) executed\n",
                  address, usptr->current.address,
                  usptr->current.address / usptr->step_size,
                  (usptr->current.address % usptr->step_size) / usptr->track_size,
                  usptr->current.address % usptr->track_size);

        reported = TRUE;
        break;


    case Set_Block_Displacement:                        /* Set Block Displacement */
        bptr = dsptr->buffer;                           /* point at the first of the parameter bytes */

        displacement = (int32) TO_32_BITS (bptr, 2);    /* get the displacement from the lower four bytes */
        significand = TO_24_BITS (bptr, 0);             /*   and the significand from the upper three bytes */

        if ((significand == 0 || significand == 0xFFFFFF)       /* if the resulting address */
          && (int32) usptr->current.address + displacement >= 0 /*   is within range */
          && (int32) usptr->current.address + displacement < (int32) usptr->volume_size)
            usptr->current.address += displacement;             /*     then set the new address */

        else {                                          /* otherwise */
            usptr->current.address = 0;                 /*   then clear the address */
            set_error (dc_id, usptr, Address_Bounds);   /*     and report an addressing error */
            }

        tpprintf (dptr, TRACE_CMD, "Device %u Set Block Displacement %d executed\n",
                  address, displacement);

        reported = TRUE;
        break;


    case Set_Length:                                    /* Set Length */
        bptr = dsptr->buffer;                           /* point at the first of the parameter bytes */
        usptr->current.length = TO_32_BITS (bptr, 0);   /*   and get the length */

        tpprintf (dptr, TRACE_CMD, "Device %u Set Length %u executed\n",
                  address, usptr->current.length);

        reported = TRUE;
        break;


    case Set_Unit:                                          /* Set Unit */
        if (BIT (SET_UNIT (bus)) & dsptr->valid_units) {    /* if the unit is present */
            dsptr->unit = SET_UNIT (bus);                   /*   then set the new unit */
            usptr = &dsptr->units [MAPUS (dsptr->unit)];    /*     and the pointer to the new unit state */
            }

        else                                                /* otherwise */
            set_error (dc_id, usptr, Module_Addressing);    /*   report an addressing error */

        tpprintf (dptr, TRACE_CMD, "Device %u Set Unit %u executed\n",
                  address, SET_UNIT (bus));

        reported = TRUE;
        break;


    case Initiate_Utility_Write:                        /* Initiate Utility (receive execution) */
        set_error (dc_id, usptr, Parameter_Bounds);     /* report a parameter error */
        unsupported = TRUE;                             /*   as these are currently unsupported */
        break;


    case Initiate_Utility:                              /* Initiate Utility (no execution) */
        if (not usptr->tape_drive)                      /* if the unit is not a tape drive */
            dsptr->buffer [0] = 0;                      /*   then force an unrecognized utility */

        switch (dsptr->buffer [0]) {                    /* dispatch the subopcode */

            case Pattern_ERT:                           /* subcode C8H */
                if (dsptr->buffer [2] == 2) {           /* if the ERT type is "certification" */
                    usptr->current.address = 0;         /*   then start at the first block of the media */

                    if (validate_access (dc_id, usptr, Write_Access)) {     /* validate the access; if it succeeds */
                        initialize_media (dc_id, usptr, 0, Recertified);    /*   then perform a media certification */

                        if (uptr->wait < 0)             /* if the positioning delay has overflowed */
                            uptr->wait = D32_SMAX;      /*   then reset to the largest delay possible */

                        activation_delay = Position_Time;   /* delay for the positioning time */
                        }
                    break;
                    }

            /* otherwise fall through unto the error case */

            default:                                        /* all other utilities are not implemented */
                set_error (dc_id, usptr, Parameter_Bounds); /*   so report a parameter error */
                unsupported = TRUE;                         /*     as the subopcode is unsupported */
                break;
            }

        dsptr->index = 0;                               /* reset the buffer index */
        break;


    case Initiate_Utility_Read:                         /* Initiate Utility (send execution) */
        if (usptr->tape_drive)                          /* if the unit is a tape drive */
            switch (dsptr->buffer [0]) {                /*   then dispatch the tape subcode */

                case Read_Revision_Numbers:             /* subcode C3H */
                    dsptr->count = 4;                   /* set the count of bytes returned */

                    dsptr->buffer [0] = 3;              /* count of bytes following */
                    dsptr->buffer [1] = 0x01;           /* ROM revision number */
                    dsptr->buffer [2] = 0x01;           /* ROM revision number */
                    dsptr->buffer [3] = 0x01;           /* ROM revision number */
                    break;

                case Read_Error_Log:                    /* subcode C5H */
                    dsptr->count = 4;                   /* count of bytes returned */

                    dsptr->buffer [0] = 0;              /* number of error records returned */
                    dsptr->buffer [1] = 0;              /* number of uncorrectable errors */
                    dsptr->buffer [2] = 0;              /* number of key errors */

                    if (STATE (uptr->User_Flags) == Certified)  /* if the volume is certified */
                        dsptr->buffer [3] = 2;                  /*   then type is "HP factory certified" */
                    else                                        /* otherwise */
                        dsptr->buffer [3] = 0;                  /*   the type is "Not certified" */
                    break;

                case Read_ERT_Log:                      /* subcode C6H */
                    memset (dsptr->buffer, 0, 11);      /* clear the log header */
                    dsptr->count = 11;                  /*   and set the count of bytes returned */

                    bptr = &dsptr->buffer [1];              /* point at the number of blocks accessed */
                    TO_4_BYTES (bptr, usptr->volume_size);  /*   and set the count */
                    break;

                default:                                /* all other utilities are not implemented */
                    unsupported = TRUE;                 /*   as the subopcodes are unsupported */
                    break;
                }

        else                                            /* otherwise */
            switch (dsptr->buffer [0]) {                /*   dispatch the disc subcode */

                case Read_Revision_Numbers:             /* subcode C3H */
                    dsptr->count = 2;                   /* set the count of bytes returned */

                    dsptr->buffer [0] = 1;              /* count of bytes following */
                    dsptr->buffer [1] = 0x01;           /* ROM revision number */
                    break;

                case Read_Drive_Table:                              /* subcode C4H */
                    if (dsptr->buffer [1] == 1                      /* if the spare track table is requested */
                      && dsptr->unit != 15) {                       /*   and not from the controller */
                        ddptr = &describe [MODEL (uptr->flags)];    /*     then point at the model description */
                        udptr = &ddptr->unit [dsptr->unit];         /*       and at the unit description */

                        count = udptr->volume.max_head + 1; /* get the count of drive heads */
                        dsptr->count = count * 5;           /*   and set the count of bytes returned */

                        bptr = dsptr->buffer;           /* point at the start of the return buffer */
                        memset (bptr, 0, dsptr->count); /*   and clear the buffer */

                        for (head = 0; head < count; head++) {  /* for each defined head */
                            *bptr = (uint8) head;               /*   report the head number */
                            bptr = bptr + 5;                    /*     and advance the buffer pointer */
                            }
                        }

                    else                                /* otherwise */
                        unsupported = TRUE;             /*   the table request is unsupported */
                    break;

                default:                                /* all other utilities are not implemented */
                    unsupported = TRUE;                 /*   as the subopcodes are unsupported */
                    break;
                }

        if (unsupported)                                /* if the utility is unsupported */
            set_error (dc_id, usptr, Parameter_Bounds); /*   then report a parameter error */

        dsptr->index = 0;                               /* reset the buffer index */
        break;


    case Initiate_Diagnostic:                           /* Initiate Diagnostic */
        tpprintf (dptr, TRACE_CMD, "Device %u Initiate Diagnostic section %u loop count %u executed\n",
                  address, dsptr->buffer [2], TO_WORD (dsptr->buffer [0], dsptr->buffer [1]));

        reported = TRUE;
        break;


    case No_Operation:                                  /* No Operation */
        break;


    case Describe:                                      /* Describe */
        ddptr = &describe [MODEL (uptr->flags)];        /* point at the description for this model */
        bptr = dsptr->buffer;                           /*   and the start of the return buffer */

        /* describe the controller */

        TO_2_BYTES (bptr, ddptr->installed_units);      /* store the installed units word */
        TO_2_BYTES (bptr, ddptr->max_rate);             /*   and the maximum transfer rate */
        TO_1_BYTE  (bptr, ddptr->type);                 /*     and the controller type */

        if (dsptr->unit == 15)                              /* if the controller is addressed */
            unit_map = dsptr->valid_units & DRIVE_UNITS;    /*   then describe all units except the controller */
        else                                                /* otherwise */
            unit_map = BIT (dsptr->unit);                   /*   describe just the selected unit */

        udptr = ddptr->unit;                            /* point at the first unit's description */

        while (unit_map != 0) {                         /* loop through the units */
            unit = (uint8) BIT_NUMBER (unit_map);       /*   and get the next unit number */

            /* describe the unit */

            TO_1_BYTE  (bptr, udptr->type);             /* report the drive type */
            TO_3_BYTES (bptr, udptr->model);            /*   and the model number */

            TO_2_BYTES (bptr, udptr->bytes_per_block);  /* report the number of bytes per block */
            TO_1_BYTE  (bptr, udptr->buffered);         /*   and number of blocks buffered */
            TO_1_BYTE  (bptr, udptr->burst);            /*     and recommended burst size */

            TO_2_BYTES (bptr, udptr->block_time);       /* report the block time in microseconds */
            TO_2_BYTES (bptr, udptr->avg_xfer);         /*   and average transfer rate in KB/second */
            TO_2_BYTES (bptr, udptr->retry_time);       /*     and optimal retry time in centiseconds */
            TO_2_BYTES (bptr, udptr->access_time);      /*       and maximum access time in centiseconds */
            TO_1_BYTE  (bptr, udptr->max_interleave);   /*         and the maximum interleave factor */

            TO_1_BYTE  (bptr, udptr->fixed_volumes);        /* report the bitmap of fixed volumes */
            TO_1_BYTE  (bptr, udptr->removable_volumes);    /*   and of removable volumes */

            /* describe the volume */

            if (udptr->type == 2) {                     /* if the device is a tape drive */
                TO_4_BYTES (bptr, 0);                   /*   then clear */
                TO_2_BYTES (bptr, 0);                   /*     the CHS values */
                }

            else {                                              /* otherwise */
                TO_3_BYTES (bptr, udptr->volume.max_cylinder);  /*   report the maximum cylinder */
                TO_1_BYTE  (bptr, udptr->volume.max_head);      /*     head */
                TO_2_BYTES (bptr, udptr->volume.max_sector);    /*       and sector values */
                }

            TO_2_BYTES (bptr, 0);                       /* clear the leading bytes of the block value */

            if (udptr->type == 2                        /* if the device is a tape drive */
              && not (uptr->flags & UNIT_ATT)) {        /*   but no tape is loaded */
                TO_4_BYTES (bptr, 0);                   /*     then report zero for the maximum block */
                }

            else {                                                      /* otherwise */
                TO_4_BYTES (bptr, dsptr->units [unit].volume_size - 1); /*   report the maximum allowed */
                }

            TO_1_BYTE (bptr, udptr->volume.interleave); /* report the current interleave value */

            udptr++;                                    /* point at the next unit's description */
            unit_map ^= BIT (unit);                     /*   and clear the current bit */
            }

        dsptr->index = 0;                               /* set the starting buffer index */
        dsptr->count = (uint32) (bptr - dsptr->buffer); /*   and the count of bytes reported */
        break;


    case Initialize_Media:
        usptr->current.address = 0;                     /* start at the first block of the media */

        if (validate_access (dc_id, usptr, Write_Access)) { /* validate the access; if it succeeds */
            current_state = STATE (uptr->User_Flags);       /*   then get the current tape status */

            if (usptr->tape_drive)                                      /* if unit is a tape drive */
                new_state = media_action [dsptr->buffer [0] & cwz_mask] /*   then determine the media action */
                                         [current_state];               /*     from the options and current state */
            else                                                        /*   otherwise */
                new_state = Reinitialized;                              /*     disc media is always reinitialized */

            initialize_media (dc_id, usptr, 0, new_state);  /* initialize the media */

            if (uptr->wait < 0)                         /* if the positioning delay has overflowed */
                uptr->wait = D32_SMAX;                  /*   then reset to the largest delay possible */

            activation_delay = Position_Time;           /* delay for the positioning time */
            }
        break;


    case Set_Options:                                               /* Set Options */
        usptr->current.options = (OPTION_SET) dsptr->buffer [0];    /* save the option set */

        tpprintf (dptr, TRACE_CMD, "Device %u Set Options %s executed\n",
                  address, fmt_bitset (usptr->current.options, option_format));

        reported = TRUE;
        break;


    case Set_RPS:                                       /* Set Rotational Position Sensing */
    case Set_Retry_Time:                                /* Set Retry Time */
        break;                                          /* accept the command but otherwise ignore it */


    case Set_Release:                                   /* Set Release */
        usptr->current.release = dsptr->buffer [0];     /* save the release mode */

        tpprintf (dptr, TRACE_CMD, "Device %u Set Release %s executed\n",
                  address, fmt_bitset (usptr->current.release, release_format));

        reported = TRUE;
        break;


    case Set_Burst_Last:
    case Set_Burst_All:
        usptr->current.burst = dsptr->buffer [0] * 256; /* save the burst count in bytes */

        if (usptr->current.burst > 0                    /* if the mode is enabled */
          && dsptr->opcode == Set_Burst_All)            /*   and all bursts are tagged with EOI */
            usptr->current.burst |= BURST_TAG;          /*     then set the tag bit */

        tpprintf (dptr, TRACE_CMD, "Device %u Set Burst (%s) %s%.0u executed\n",
                  address, (dsptr->opcode == Set_Burst_All ? "EOI all" : "EOI last"),
                  (dsptr->buffer [0] ? "byte count " : "disable"),
                  dsptr->buffer [0] * 256);

        reported = TRUE;
        break;


    case Set_Status_Mask:
        if (dsptr->buffer [2] != 0 || dsptr->buffer [3] != 0)       /* if any fault errors are masked */
            set_error (dc_id, usptr, Parameter_Bounds);             /*   then log a parameter error */
        else                                                        /* otherwise */
            memcpy (usptr->current.status_mask, dsptr->buffer, 8);  /*   set the status mask as directed */
        break;


    case Set_Volume:                                        /* Set Volume */
        if (BIT (SET_VOLUME (bus)) & usptr->valid_volumes)  /* if the volume is present */
            dsptr->volume = SET_VOLUME (bus);               /*   then set the new volume */
        else                                                /* otherwise */
            set_error (dc_id, usptr, Module_Addressing);    /*   report an addressing error */

        tpprintf (dptr, TRACE_CMD, "Device %u Set Volume %u executed\n",
                  address, SET_VOLUME (bus));

        reported = TRUE;
        break;


    case Set_Return_Addressing_Mode:                        /* Set Return Addressing Mode */
        if (dsptr->buffer [0] > 1)                          /* if the value is not 0 or 1 */
            set_error (dc_id, usptr, Parameter_Bounds);     /*   then report a bad parameter */

        else if (not usptr->tape_drive)                     /* otherwise if the unit is not a tape drive */
            usptr->current.return_mode = dsptr->buffer [0]; /*   then set the new addressing mode */

        tpprintf (dptr, TRACE_CMD, "Device %u Set Return Addressing Mode to %s executed\n",
                  address, dsptr->buffer [0] ? "three-vector" : "single-vector");

        reported = TRUE;
        break;


    case Write_File_Mark:                               /* Write File Mark */
        if (not usptr->tape_drive)                      /* if the unit is not a tape drive */
            set_error (dc_id, usptr, Illegal_Opcode);   /*   then the command is not allowed */

        else if (validate_access (dc_id, usptr, Write_Access)) {    /* otherwise validate the access; if it succeeds */
            dsptr->index = dsptr->size;                             /*   then fake a full buffer to suppress filling */
            write_block (dc_id, usptr);                             /*     and write the file mark */

            activation_delay = Position_Time;           /* delay for the positioning time */
            }
        break;


    case Unload:                                        /* Unload */
        dc_detach (uptr);                               /* detach the tape image from the drive */
        break;


    /* Transparent Commands */


    case HPIB_Parity_Checking:                          /* HP-IB Parity Checking */
        if (dsptr->buffer [0] & 1)                      /* if parity checking is enabled */
            dc->parity |= BIT (address);                /*   then this device will check parity */
        else                                            /* otherwise */
            dc->parity &= ~BIT (address);               /*   it will not check parity */

        tpprintf (dptr, TRACE_CMD, "Device %u HP-IB Parity Checking %s executed\n",
                  address, fmt_bitset (dsptr->buffer [0], parity_format));

        reported = TRUE;
        break;


    case Read_Loopback:                                 /* Read Loopback */
    case Write_Loopback:                                /* Write Loopback */
        bptr = dsptr->buffer;                           /* point at the first of the parameter bytes */
        usptr->current.length = TO_32_BITS (bptr, 0);   /*   and get the length */

        if (usptr->current.length == 0)                 /* if the length is zero */
            set_error (dc_id, usptr, Parameter_Bounds); /*   then report a bounds error */

        dsptr->buffer [0] = 255;                        /* seed the first loopback byte */

        for (index = 0; index < 255; index++)           /* fill elements 1-255 */
            dsptr->buffer [index + 1] = index;          /*   with sequential values */

        if (dsptr->opcode == Read_Loopback)             /* if this is a read loopback */
            dsptr->index = 0;                           /*   then send the first block of the buffer */
        else                                            /* otherwise it's a write loopback */
            dsptr->index = 256;                         /*   so receive the second block of the buffer */

        dsptr->count = 256;                             /* transfer up to a full set of bytes */

        tpprintf (dptr, TRACE_CMD, "Device %u %s Loopback length %u executing\n",
                  address, (dsptr->opcode == Read_Loopback ? "Read" : "Write"),
                  usptr->current.length);

        reported = TRUE;
        break;


    case Channel_Independent_Clear:                     /* Channel Independent Clear */
        clear_device (dc_id, address);                  /* clear only the current unit unless unit = 15 */
        break;


    case Cancel:                                        /* Cancel */
        CLEAR_ERR (usptr->status, Message_Length);      /* clear the Message Length */
        CLEAR_ERR (usptr->status, Message_Sequence);    /*   and Message Sequence errors */

        if (usptr->qstat != 2)                          /* if power-fail status is not currently set */
            usptr->qstat =                              /*   then set QSTAT if any other errors remain */
              (memcmp (usptr->status, zero, sizeof zero) != 0);
        break;


    case Amigo_Clear:                                   /* Amigo Clear */
        if (dsptr->buffer [0] & 1)                      /* if parity checking is enabled */
            dc->parity |= BIT (address);                /*   then this device will check parity */
        else                                            /* otherwise */
            dc->parity &= ~BIT (address);               /*   it will not check parity */

        tpprintf (dptr, TRACE_CMD, "Device %u Amigo Clear parity checking %s executed\n",
                  address, fmt_bitset (dsptr->buffer [0], parity_format));

        reported = TRUE;                                /* clear all units */
        return;                                         /*   when the Selected Device Clear follows */


    case Quick_Status:                                  /* Quick Status */
        dsptr->buffer [0] = (uint8) usptr->qstat;       /* return the status summary value */

        dsptr->index = 0;                               /* set the starting buffer index */
        dsptr->count = 1;                               /*   and the count of bytes reported */

        bus |= Bus_EOI;                                 /* set EOI explicitly for proper completion */

        tpprintf (dptr, TRACE_CMD, "Device %u unit %u Quick Status %u executing\n",
                  address, usptr->unit, usptr->qstat);

        reported = TRUE;
        break;


    case Identify:                                      /* Identify */
        ddptr = &describe [MODEL (uptr->flags)];        /* point at the description for this model */
        bptr = dsptr->buffer;                           /*   and the start of the return buffer */

        TO_2_BYTES (bptr, ddptr->identity);             /* report the device identity */

        dsptr->index = 0;                               /* set the starting buffer index */
        dsptr->count = 2;                               /*   and the count of bytes reported */

        bus |= Bus_EOI;                                 /* set EOI explicitly for proper completion */

        tpprintf (dptr, TRACE_CMD, "Device %u Identify %04XH executing\n",
                  address, ddptr->identity);

        reported = TRUE;
        break;


    default:                                            /* commands not implemented */
        set_error (dc_id, usptr, Illegal_Opcode);       /* report an illegal */
        unsupported = TRUE;                             /*   or unsupported opcode */
        break;
    }


if (bus & Bus_EOI) {                                                /* if the command sequence is finished */
    if ((cmd_props [dsptr->opcode].next_state == Execution_Wait     /*   then if the command includes */
      || cmd_props [dsptr->opcode].next_state == Execution_Send)    /*     an execution phase */
      && no_execution == FALSE)                                     /*       and it is not omitted */
        completed = FALSE;                                          /*         then completion occurs after transfer */

    if (dsptr->state != Error_Wait) {                               /* if the command completed successfully */
        if (no_execution)                                           /*   then if the execution state is omitted */
            dsptr->state = Reporting_Wait;                          /*     then move to the reporting state */
        else                                                        /*   otherwise */
            dsptr->state = cmd_props [dsptr->opcode].next_state;    /*     move to the next indicated state */

        if (cmd_props [dsptr->opcode].complementary)    /* if a complementary command ends the sequence */
            usptr->persistent = usptr->current;         /*   then the current values become persistent */
        }

    activate_unit (dc_id, usptr, activation_delay);     /* schedule the event service now */
    dc->active = FALSE;                                 /*   and end the transaction */
    }

else if (dsptr->state == Parameter_Wait)                /* otherwise if a parameter list was processed */
    dsptr->state = Command_Wait;                        /*   then wait for another command opcode */

if (unsupported)
    tpprintf (dptr, TRACE_CMD, "Device %u %s command not implemented\n",
              address, opcode_names [dsptr->opcode]);

else if (not reported)
    tpprintf (dptr, TRACE_CMD, "Device %u %s %s\n",
              address, opcode_names [dsptr->opcode], execution [completed]);

if (completed)
    tpprintf (dptr, TRACE_INCO, "Device %u %s command completed with qstat %u\n",
              address, opcode_names [dsptr->opcode], usptr->qstat);

return;                                                 /* return with the command initiated or completed */
}


/* Validate read or write access.

   This routine is called to validate access to a device unit.  On entry, the
   "uptr" parameter points to the SIMH unit associated with the CS/80 unit being
   accessed, and the "access" parameter indicates the type of access (read or
   write).

   Validation consists of checking that media is present and initialized and,
   for writes, that the media is not write-protected.  If these checks pass,
   then if full-volume access is requested (i.e., the current length is -1), the
   length is reset to the remaining block count from the current position to the
   end of the volume.  The step count, i.e., the number of blocks that remain to
   be accessed before the unit positioner must be moved, is also set.  The data
   buffer is initialized by resetting the index to the first byte and the count
   of bytes to the unit's block size.

   A seek is then done to the current address, and the time required to move the
   positioner is saved as the unit's wait time.  The controller overhead to
   process the command is added to the seek time.

   If any error occurs, the current address is reset because no access was done,
   and the routine returns FALSE to indicate that validation failed.  Otherwise,
   the routine returns TRUE to indicate that validation and positioning
   succeeded.


   Implementation notes:

    1. If an error occurs, we must undo the action of any Set Address command,
       because no access occurred.  The persistent address is always updated
       from the current address when the next command message arrives, so we
       reset the current address here, so that the eventual update does not
       change anything.

    2. The Uninitialized Media error occurs only with tape units, but as disc
       media is always initialized, we do not need a separate tape unit check.

    3. Use of uncertified tape media reports Uninitialized Media and QSTAT = 1,
       even for the completion of an Initialize Media command.  This is not
       documented but appears from testing to be the behavior of the hardware.
*/

static t_bool validate_access (uint32 dc_id, USPTR usptr, ACCESS_TYPE access)
{
DC_STATE *const dc    = dcs [dc_id].sptr;       /* the pointer to the channel state */
const DSPTR     dsptr = usptr->dsptr;           /* a pointer to the device state */
const UPTR      uptr  = usptr->uptr;            /* a pointer to the associated SIMH unit */

if (not (uptr->flags & UNIT_ATT))                       /* if the unit is not attached to a media image file */
    set_error (dc_id, usptr, Not_Ready);                /*   then report the unit as not ready */

else if (STATE (uptr->User_Flags) < Initialized         /* otherwise if the volume is not initialized */
  && dsptr->opcode != Initialize_Media) {               /*   and this is not an Initialize Media command */
    set_error (dc_id, usptr, Uninitialized_Media);      /*     then report the condition */
    dsptr->state = Error_Wait;                          /*       and begin error recovery */
    }

else if (access == Write_Access                         /* otherwise if this a write validation */
  && uptr->flags & UNIT_RO)                             /*   and the image is read-only */
    set_error (dc_id, usptr, Write_Protect);            /*     then report a protection error */

else {                                                      /* otherwise the unit is accessible */
    if (usptr->current.length == FULL_VOLUME)               /*   so if full volume access is specified */
        usptr->current.length = usptr->block_size           /*     then get the count from the current */
          * (usptr->volume_size - usptr->current.address);  /*       position to the end of the volume */

    dsptr->step_count = usptr->step_size                /* set the remaining step count */
                          - usptr->current.address % usptr->step_size;

    dsptr->index = 0;                                   /* reset the index and the count */
    dsptr->count = dsptr->size = usptr->block_size;     /*   and set the buffer size to the block size */

    dsptr->burst_count = LOWER_WORD (usptr->current.burst); /* reset the burst count */

    usptr->data_error = FALSE;                          /* clear the data error indicator */

    uptr->wait = seek_block (dc_id, usptr);             /* position the image file */

    if (dcs [dc_id].dptr->flags & DEV_REALTIME)         /* add in */
        uptr->wait += real_time [usptr->delay_index]    /*   the appropriate */
                                [Controller_Time];      /*     controller command */
    else                                                /*       processing overhead */
        uptr->wait += dc->fast_times [Controller_Time]; /*         delay */
    }

if (dsptr->state == Error_Wait) {                       /* if an error occurred */
    usptr->current.address = usptr->persistent.address; /*   then undo any Set Address command */
    return FALSE;                                       /*     because no data was accessed */
    }

else {
    if (usptr->tape_drive                               /* if this is a tape drive */
      && STATE (uptr->User_Flags) != Certified)         /*   and the volume is not certified */
        set_error (dc_id, usptr, Uninitialized_Media);  /*     then report the condition */

    return TRUE;                                        /* validation succeeds */
    }
}


/* Seek to a data block.

   This routine positions the attached image file to the current address and
   returns the time required to perform the seek.  On entry, the "uptr"
   parameter points to the SIMH unit associated with the CS/80 unit being
   positioned, and the "usptr" parameter points to the unit state of the
   associated unit.

   The routine updates the unit position to the byte address corresponding to
   the current block address and moves the stream associated with the image file
   accordingly.  Then the routine calculates the time occupied by the movement.

   If optimized timing mode is selected, the fast seek time is returned.
   Otherwise, the realistic seek time is determined by calculating the
   positioner movement needed to move from the current position to the new
   position.  If the head positioner is moved, the time taken is the sum of the
   time needed to initiate and complete movement (single-step time), plus the
   time needed to traverse the intervening positions.  The time to access the
   target block once positioner movement ceases is then added.

   In disc terms, the times are:

     - seek = (cylinder_delta - 1) * per_cylinder_traverse_time + start_stop_time

     - access = full_rotation_time / 2

   For tapes, the times are:

     - seek = track_delta * per_track_step_time

     - access = block_delta * per_block_block_search_time

   The times are present in the "real_time" delay properties table, indexed by
   the drive model used.


   Implementation notes:

    1. On entry, persistent.address is the current address (before the seek),
       while current.address is the target address (after the seek).  So the
       difference represents the traversal distance in blocks, which is then
       used to determine the number of head positioner steps needed.

    2. We do not attempt to model the disc rotational delay accurately, instead
       simply using the average rotational delay time.  Consequently, rotational
       position sensing (Set RPS command) and block interleave (Initialize Media
       command) have no effect on transaction timing in realistic timing mode.
*/

static int32 seek_block (uint32 dc_id, USPTR usptr)
{
const      UPTR uptr = usptr->uptr;             /* a pointer to the associated SIMH unit */
MODEL_TYPE model;
int32      time;
uint32     current_track, new_track, delta_track;
uint32     current_block, new_block, delta_block;

tpprintf (dcs [dc_id].dptr, TRACE_CMD, "Device %u unit %u reposition from block %u to block %u\n",
          ADDRESS (uptr->flags), UNIT (uptr->flags),
          usptr->persistent.address, usptr->current.address);

uptr->pos = usptr->current.address * usptr->block_stride;   /* set the unit byte position */
sim_fseek (uptr->fileref, uptr->pos, SEEK_SET);             /*   and reposition the file stream */

if (ferror (uptr->fileref)) {                           /* if the seek failed */
    report_io_error (uptr, "seek");                     /*   then report it to the console */
    set_error (dc_id, usptr, Unit_Fault);               /*     and log a unit fault */

    time = 0;                                           /* a failure takes no time */
    }

else if (dcs [dc_id].dptr->flags & DEV_REALTIME) {      /* otherwise if real time mode is enabled */
    model = usptr->delay_index;                         /*   then point at the model's delay properties */

    current_track = usptr->persistent.address / usptr->step_size;       /* calculate the current track */
    new_track     = usptr->current.address / usptr->step_size;          /*   and the new track */
    delta_track   = abs ((int32) new_track - (int32) current_track);    /*     and the track change */

    if (delta_track > 0)                                            /* if the positioner moved */
        time = real_time [model] [Seek_Time]                        /*   then sum the start/stop time */
          + real_time [model] [Traverse_Time] * (delta_track - 1)   /*     and the track traversal time */
          + real_time [model] [Position_Time];                      /*       and the access time */
    else                                                            /* otherwise */
        time = real_time [model] [Position_Time];                   /*   use only the access time */

    if (usptr->tape_drive) {                                                /* if this is a tape drive */
        current_block = usptr->persistent.address % usptr->step_size;       /*   then calculate */
        new_block     = usptr->current.address % usptr->step_size;          /*     the linear access time */
        delta_block   = abs ((int32) new_block - (int32) current_block);    /*       between the positions */

        time += real_time [model] [Position_Time] * (delta_block - 1);  /* add in the linear access time */
        }
    }

else                                                    /* otherwise optimized timing is active */
    time = dcs [dc_id].sptr->fast_times [Seek_Time];    /*   so just use the fast seek time */

return time;                                            /* return the cumulative time for the seek */
}


/* Read one or more data blocks.

   This routine reads one or more blocks of data from the image file into the
   device buffer.  The read begins at the current address and continues for the
   number of bytes specified by the unit's current buffer size.  Before
   returning, the buffer is set up for data access, and the time taken by the
   read is added to the current unit wait time.  The routine returns TRUE if the
   buffer contains data and FALSE if it does not.

   On entry, the "usptr" parameter points to the unit state of the accessed
   unit; the associated device state pointer "dsptr" and the SIMH unit pointer
   "uptr" are obtained from the unit state.  "dsptr->size" gives the size of the
   buffer to read, which may be less than the block size if the remaining
   transaction length covers only part of a block.  On exit, "dsptr->count"
   gives the number of bytes read into the buffer, and "dsptr->index" is reset
   to 0 to prepare for buffer access.

   First, a pointer to the model delays is set up, depending on whether
   realistic or optimized timing is enabled.  Then the number of blocks to read
   is determined by dividing the current buffer size by the unit's block size.
   The two sizes are usually the same but will be different during a Copy Data
   command if the two units do not have a common block size.

   If the read requires a positioner step, the single-step time is added.  If
   the routine is called from the execution phase of a read command, then the
   time to traverse the residue of the prior block (i.e., the trailing CRC and
   ECC bytes and interblock time) is added.  Otherwise, if the executing command
   does not transfer data to the host, i.e., is a Copy Data or Locate and Verify
   command, then the time to traverse a full block is added.  On return, the
   unit "wait" field will have the accumulated transfer delay.

   Before reading the block or blocks, the routine confirms that the unit is
   still attached.  If the user has detached the file between the validation
   check at the start of the command and routine entry, a Not Ready error is
   logged, and the routine returns FALSE to tell the caller that recovery is
   needed.  If the user has reattached a different image file, the unseen QSTAT
   interlock will be active.  In this case, the state is changed to Error_Wait,
   and the routine returns FALSE.  The caller will recover by sourcing an error
   byte for a Locate and Read or Cold Load Read, or aborting a Copy Data
   command.

   For each tape block to be read, the routine calls the SIMH tape library "read
   record forward" function.  If the call returns tape mark status or an "erased
   block" marker, and the read is for a Locate and Verify command, the status is
   ignored, and reading continues as though valid data were read.  Otherwise, an
   End of File or No Data Found error is reported, respectively, and the read is
   terminated.

   If EOM (end of medium) status occurs, a No Data Found error is reported; this
   only happens if an imported HPDrive image file contains fewer than the
   defined number of blocks.  Other tape library errors result in unrecoverable
   data errors.  These are fatal for Locate and Verify but not for other
   commands.

   Disc blocks are read using a SIMH file library call.  Host I/O errors result
   in Unrecoverable Data errors.  These are fatal for Locate and Verify but not
   for other commands.

   For both types of blocks, if the read results in less than a full block of
   data, either due to a short tape record with the character count option
   disabled, or reading beyond the end of a short disc file, the remaining bytes
   in the buffer are set to zero.  The current address is then incremented, the
   unit byte position is updated, and the buffer pointer is advanced to
   accommodate the next read.  The loop continues until all requested blocks are
   read, or the address reaches the end of the volume.

   Once the reads are complete, the buffer index is reset, and the count is set
   to the number of bytes residing in the buffer.  If the address has been
   incremented beyond the end of the volume, the address is reset to 0.  If the
   remaining transfer length is larger than the count of bytes in the buffer, it
   is reduced to the amount of data available in the buffer, and an End of
   Volume error is logged.  The routine returns TRUE if the buffer contains data
   or FALSE if no data was obtained.

   Error handling is complex.  CS/80 requires devices to return the full count
   of bytes requested, even if header or data errors are encountered during the
   read.  Recovery is complicated by the fact that the routine may be asked to
   read more than one disc block at a time.  When executing a Copy Data command
   from disc to tape, four disc blocks will be requested at a time, and errors
   can occur on any of the blocks.

   Another complication is that the controller may or may not be in the middle
   of an execution message when the error occurs.  Reads are performed wholly
   within the Command Wait state for Locate and Verify and Copy Data, as well as
   for the initial read for Locate and Read and Cold Load Read.  For the latter
   two, additional reads occur in the Execution Send state.

   Still another complication is that the error response is different depending
   on whether or not a Locate and Verify command is executing.  Unrecoverable
   Data errors normally are logged but do not stop the read process; these
   errors are fatal for Locate and Verify.  End of File and No Data Found errors
   normally are fatal during a read; they are ignored for Locate and Verify.

   Recovery depends on the specific error and can take one of three forms: an
   abort that stops the sequence immediately, an error that is logged but allows
   reading to continue, and a read without errors that will fail immediately at
   the next entry.  Not Ready, interlock, End of File, and No Data Found cause
   aborts, Unrecoverable Data logs the error and continues, and End of Volume
   logs the error but also ends the read transaction early to prevent another
   entry.  When the address is incremented beyond the last addressable block, it
   must be wrapped around to block 0, regardless of whether or not an End of
   Volume error is reported.

   Note that End of File and No Data Found only occur when reading tape units,
   and the routine is never asked to read more than one tape block per call.
   Therefore, if either of these errors occur, no data was read.

   Errors that prevent reading will return FALSE to indicate that no data was
   read into the buffer.  If this occurs in an execution phase, the caller must
   react to the FALSE return by changing the state to Error Source, so that the
   next event service will source an error byte of value 1 tagged with EOI.  A
   TRUE return does not mean that an error did not occur; both data errors and
   End of Volume errors occur while returning data.

   The read recovery rules may be summarized as follows:

     - A Not_Ready or unit interlocked error returns FALSE without updating the
       address.

     - An End_of_File or No Data Found error is ignored for Locate_and_Verify.
       For all other commands, it increments the address, changes to Error_Wait,
       and returns FALSE.

     - An End_of_Volume error resets the address to 0, retains the current
       state, shortens the transaction length to the buffer length, and returns
       TRUE.

     - An Unrecoverable_Data error sets the data_error flag, increments the
       address, retains the current state, and returns FALSE for
       Locate_and_Verify and TRUE otherwise.


   Implementation notes:

    1. The size of the data buffer dictates how many unit blocks are read for
       each call.  Normally, the buffer size equals the block size, so one block
       is read.  However, for Copy Data, the buffer size is the larger of the
       two unit block sizes, so multiple blocks will be read from the unit with
       the smaller size.  For example, when copying from disc to tape, four disc
       blocks of 256 bytes will be read to fill the 1024-word buffer that then
       will be used to write one block of 1024 bytes to the tape.

    2. Reading a tape block containing a partial record with the character count
       option enabled returns only the data present in the record.  This works
       properly for one-block Locate and Read commands but presents a problem
       with the Copy Data command when the tape unit is the source.  If the
       option is disabled, one tape block of 1024 bytes read and written to four
       disc blocks of 256 bytes.  With the option enabled, multiple tape blocks
       would need to be read -- conceivably up to 1024 blocks -- to accumulate
       the 1024 bytes needed before returning.  Moreover, the final data record
       might have to be split between the current buffer and the next buffer,
       which would greatly complicate this routine.

       Given that the action of Copy Data with the character count option
       enabled is unspecified, and given that such a copy could not be restored
       back to tape (as all of the record divisions would be lost), we read the
       same number of records -- one -- regardless of the option setting.

    3. When a shorter-than-block-length tape record is read with character count
       option disabled, we pad the data to the block size with zero bytes.  Page
       1-10 of the CS/80 manual says, "When a partial block is written, the
       controller will pad the unused portion of the block with zeros."  When
       writing, a short-length record is followed by an erase gap to pad the
       block, not zeros, so we simulate that when reading by padding the buffer
       data here.

    4. Normally, the file position would not need to be updated explicitly after
       tape calls, as the Read Record Forward library routine updates the
       position after all successful calls, returning with the position pointing
       at the erase gap that should follow the data record or tape mark.  If the
       gap is present, this would be OK, as a sequential read would skip over
       the gap and read the following record.  However, for performance reasons,
       file format validation performed during attachment does not actually
       verify that a properly formatted erase gap pads each block.  Therefore,
       we cannot rely on reading through the gap, so we explicitly set the
       position to the start of the next block after each read (tape library
       routines always seek to the specified position before accessing the image
       file).

    5. Tape format validation verifies that all blocks contain data records,
       tape marks, or erased-block markers, so any tape library errors other
       than reading a tape mark or attempting to read beyond the end of the
       medium are unexpected.  Disc images are not validated, so reading beyond
       the EOF is possible.  We detect this case and provide zero bytes for the
       missing data.  No error occurs.

    6. A full-length read, i.e., with a Set Length specifying a length of -1,
       stops at the end of the volume without error but also resets the current
       address to block 0.  The "validate_access" routine detects the
       full-volume length setting and resets the length to that remaining
       between the starting address and the end of the volume, so the last read
       terminates normally.

    7. A read that extends beyond the end of the volume must assert EOI on the
       last valid byte.  We do this by dropping the remaining transfer length to
       the count of bytes in the buffer, so that EOI will be asserted normally
       when the last buffered byte is sent.

    8. On each entry, the file is expected to be attached and positioned
       correctly; otherwise, the validation check made when the read command is
       initiated would have failed.  However, the user could have detached
       and/or reattached the image file while the transaction is in progress.
       In this case, the unseen QSTAT interlock that is set during attachment
       will still be set, and we can abort reading to ensure that the host is
       made aware of the media change before any additional data is returned.

    9. End_of_Volume can never be present on routine entry, because the address
       can never point beyond the end of the volume.  If Set Address attempts
       it, the command is rejected with Address Bounds, and if a read increments
       it, it wraps to zero.  End_of_Volume is always set after the reads
       complete within a call, so some data will always be read.
*/

static t_bool read_block (uint32 dc_id, USPTR usptr)
{
const    DSPTR dsptr = usptr->dsptr;            /* a pointer to the device state */
const    UPTR  uptr  = usptr->uptr;             /* a pointer to the associated SIMH unit */
DPPTR    delay;
uint8    *bufptr;
uint32   block_count;
size_t   count;
t_mtrlnt length;
t_stat   status;

if (dcs [dc_id].dptr->flags & DEV_REALTIME)                 /* if real-time mode is active */
    delay = &real_time [usptr->delay_index];                /*   then point at the selected model's timing */
else                                                        /* otherwise */
    delay = (const DPPTR) &dcs [dc_id].sptr->fast_times;    /*   point at the optimized timing */

block_count = (dsptr->size + usptr->block_size - 1)     /* round up the count */
                / usptr->block_size;                    /*   of blocks to read */

if (dsptr->step_count >= block_count)                   /* if the read will not require positioner movement */
    dsptr->step_count -= block_count;                   /*   then drop the count remaining before end-of-track */

else {                                                      /* otherwise */
    dsptr->step_count += usptr->step_size - block_count;    /*   reset the count for the new track */
    uptr->wait += (*delay) [Seek_Time];                     /*     and add in the time for a positioner step */
    }

if (dsptr->state == Execution_Send)                     /* if we are reading within a data transfer */
    uptr->wait += (*delay) [Residue_Time];              /*   then add the residue of the previous block */

else if (dsptr->opcode == Locate_and_Verify             /* otherwise if we are reading for a command */
  || dsptr->opcode == Copy_Data)                        /*   that has no data transfer */
    uptr->wait += (*delay) [Block_Time] * block_count;  /*     then add the full block time */


bufptr = dsptr->buffer;                                 /* point at the buffer location to begin reading */

if (not (uptr->flags & UNIT_ATT))                       /* if the unit is no longer attached */
    set_error (dc_id, usptr, Not_Ready);                /*   then report that the unit is not ready */

else if (usptr->interlocked)                            /* otherwise if the media has been replaced */
    dsptr->state = Error_Wait;                          /*   then wait for error recovery */

else do {                                               /* otherwise read "block_count" blocks */
    tpprintf (dcs [dc_id].dptr, TRACE_INCO, "Device %u unit %u read block %u\n",
              ADDRESS (uptr->flags), UNIT (uptr->flags), usptr->current.address);

    if (usptr->tape_drive) {                            /* if this is a tape drive */
        length = MTB_STANDARD | MTB_PMARK;              /*   then accept standard and private markers */

        status = sim_tape_rdrecf (uptr, bufptr, &length,    /* read the current tape record */
                                  usptr->block_size);       /*   into the data buffer */

        if (status == MTSE_TMK                          /* if the record contains a tape mark */
          || length == ERASED_BLOCK) {                  /*   or the block has not been written */
            count = 0;                                  /*     then no data was returned */

            if (dsptr->opcode != Locate_and_Verify) {           /* if this is not a verify command */
                if (status == MTSE_TMK)                         /*   then tape mark status */
                    set_error (dc_id, usptr, End_of_File);      /*     records an end of file error */
                else                                            /*   while no data status */
                    set_error (dc_id, usptr, No_Data_Found);    /*     records a No Data Found error */

                usptr->current.address++;               /* point the address at the next block */
                break;                                  /*   and terminate the read */
                }
            }                                           /* otherwise ignore the tape mark or unwritten block */

        else if (status == MTSE_EOM) {                  /* otherwise if the end-of-medium was seen */
            count = 0;                                  /*   then no data was returned */

            set_error (dc_id, usptr, No_Data_Found);    /* record a No Data Found error */
            break;                                      /*   and terminate the read */
            }

        else if (status == MTSE_OK)                     /* otherwise if the read was successful */
            count = (size_t) MTR_RL (length);           /*   then save the count of bytes read */

        else {                                          /* otherwise some other error occurred */
            if (status == MTSE_RECE)                    /* if a bad record was encountered */
                count = (size_t) MTR_RL (length);       /*   then save the count of bytes read */
            else                                        /* otherwise */
                count = 0;                              /*   no data was returned */

            set_data_error (dc_id, usptr, status);      /* report an unrecoverable data error */

            if (dsptr->opcode == Locate_and_Verify) {   /* if this is a verify command */
                usptr->current.address++;               /*   then point the address at the next block */
                break;                                  /*     and terminate the read */
                }
            }
        }

    else {                                                  /* otherwise this is a disc drive */
        count = sim_fread (bufptr, 1, usptr->block_size,    /*   so read the current disc block */
                           uptr->fileref);

        if ((uint32) count < usptr->block_size          /* if less than a full block was read */
          && ferror (uptr->fileref)) {                  /*   and a host file system error occurred */
            set_data_error (dc_id, usptr, MTSE_OK);     /*     then report an unrecoverable data error */

            if (dsptr->opcode == Locate_and_Verify) {   /* if this is a verify command */
                usptr->current.address++;               /*   then point the address at the next block */
                break;                                  /*     and terminate the read */
                }
            }
        }

    if ((uint32) count < usptr->block_size              /* if less than a full block was read */
      && not (usptr->tape_drive                         /*   from other than a tape drive unit */
      && usptr->current.options & Character_Count)) {   /*     with the character count option enabled */
        memset (bufptr + count, 0,                      /*       then zero out the buffer portion */
                usptr->block_size - count);             /*         that was not read */

        count = usptr->block_size;                      /* reset the count to the full block size */
        }

    usptr->current.address++;                                   /* update the file address */
    uptr->pos = usptr->current.address * usptr->block_stride;   /*   and the unit byte position */

    bufptr += count;                                    /* advance the buffer pointer */
    }

while (--block_count > 0                                /* loop until all blocks are read */
  && usptr->current.address < usptr->volume_size);      /*   or the address exceeds the volume size */

dsptr->index = 0;                                       /* set the starting buffer index */
dsptr->count = (uint32) (bufptr - dsptr->buffer);       /*   and the count of bytes actually read */

if (usptr->current.address >= usptr->volume_size) {     /* if the address now exceeds the volume size */
    usptr->current.address = 0;                         /*   then wrap the address */

    if (usptr->current.length > dsptr->count) {         /* if the transfer would continue beyond the end */
        usptr->current.length = dsptr->count;           /*   then shorten the transfer to the data available */
        set_error (dc_id, usptr, End_of_Volume);        /*     and report an end of volume error */
        }
    }

return (dsptr->count > 0);                              /* return TRUE if the read completed with data */
}


/* Write one or more data blocks.

   This routine writes one or more blocks of data from the device buffer into
   the image file.  The write begins at the current file position and continues
   for the number of bytes specified by the unit's current buffer size.  Before
   returning, the buffer is reset for filling, and the time taken by the write
   is added to the current unit wait time.  The routine returns TRUE if the
   write(s) succeeded and FALSE if not.

   On entry, the "usptr" parameter points to the unit state of the accessed
   unit; the associated device state pointer "dsptr" and the SIMH unit pointer
   "uptr" are obtained from the unit state.  "dsptr->size" gives the size of the
   buffer to write, which may be less than the block size if the remaining
   transaction length covers only part of a block, and "dsptr->index" gives the
   first unused buffer element after the valid data.  On exit, "dsptr->index" is
   reset to 0, and "dsptr->count" is reset to the number of bytes available in
   the buffer.

   First, a pointer to the model delays is set up, depending on whether
   realistic or optimized timing is enabled.  Then the number of blocks to write
   is determined by dividing the current buffer size by the unit's block size.
   The two sizes are usually the same but will be different during a Copy Data
   command if the two units do not have a common block size.

   If the write requires a positioner step, the single-step time is added.  If
   the routine is called from the execution phase of a write command, then the
   time to traverse the residue of the prior block (i.e., the trailing CRC and
   ECC bytes and interblock time) is added.  Otherwise, if the executing command
   does not transfer data from the host, i.e., is a Copy Data or Write File Mark
   command, then the time to traverse a full block is added.  On return, the
   unit "wait" field will have the accumulated transfer delay.

   Normal entry is with dsptr->index = dsptr->size, indicating that the buffer
   is full.  If a disc unit is being written, and the buffer is not full, the
   remaining bytes are padded with copies of the last valid byte in the buffer.

   Before writing the block or blocks, the routine confirms that the unit is
   still attached.  If the user has detached the file between the validation
   check at the start of the command and routine entry, a Not Ready error is
   logged, and the routine returns FALSE to tell the caller that recovery is
   needed.  If the user has reattached a different image file, the unseen QSTAT
   interlock will be active.  In this case, the state is changed to Error_Wait,
   and the routine returns FALSE.  The caller will recover by sinking the
   remaining bytes of a Locate and Write or aborting a Copy Data command.

   For each tape block to be written, the routine calls the SIMH tape library
   "write tape mark" or "write record forward" function to write a tape mark or
   data record, respectively.  Additional calls to the "tape erase" function are
   interspersed to provide the surrounding erase gaps necessary to implement the
   cartridge tape drive format, which requires that tape blocks start on
   "usptr->block_stride" boundaries.  If the calls return errors, an
   Unrecoverable Data error is logged, the state is changed to "Error_Wait," and
   the address is incremented to point beyond the bad block.  These are fatal
   and terminate the write sequence.

   Disc blocks are written using a SIMH file library call.  After the call, the
   unit position is updated; this is not necessary for tape calls, as the tape
   library updates the position automatically.  Host I/O errors result in
   fatal Unrecoverable Data errors that are handled as above.

   After each block is successfully written, the current address is
   incremented, and the buffer pointer is advanced to accommodate the next
   write.  The loop continues until all requested blocks are written, or the
   address reaches the end of the volume.

   Once the writes are complete, the buffer index is reset, and the count is set
   to the number of bytes available in the empty buffer.  If the address has
   been incremented beyond the end of the volume, the address is reset to 0.  If
   the transfer is not complete, i.e., another write will be needed, an End of
   Volume error is logged, and the state is changed to Error_Wait.  The routine
   returns TRUE if all writes were successful or FALSE if an error occurred.

   Error handling is straightforward because writing ceases when any error
   occurs.  The current address will point at the block after the block in which
   the write error occurred.  CS/80 requires devices to accept the full count of
   bytes requested, even if header or data errors are encountered during the
   write.  Recovery is accomplished by sinking any remaining bytes sent by the
   host until the end of the transfer.  When the host requests a reporting
   message, the error will be seen by the host.  Recovery is the responsibility
   of the caller; we simply return FALSE to indicate that an error has occurred
   and depend on the caller to take the appropriate action.


   Implementation notes:

    1. The size of the data buffer dictates how many unit blocks are  written
       for each call.  Normally, the buffer size equals the block size, so one
       block is written.  However, for Copy Data, the buffer size is the larger
       of the two unit block sizes, so multiple blocks will be written  to the
       unit with the smaller size.  For example, when copying from tape to disc,
       one block of 1024 bytes will be read from the tape to fill the buffer
       that will then be used to write four blocks of 256 bytes to the disc.

    2. The position does not need to be updated explicitly after tape calls.
       The Write Tape Mark, Write Record Forward, and Erase Tape library
       routines update the position after all successful calls.  After writing a
       data record or tape mark, the position will point at the next record.

    3. A full-length write, i.e., with a Set Length specifying a length of -1,
       stops at the end of the volume without error but also resets the current
       address to block 0.  The "validate_access" routine detects length = -1
       and resets the length to that remaining between the starting address and
       the end of the volume, so the last write terminates normally.

    4. An Unrecoverable Data error in the last block of the volume aborts the
       write sequence at that point.  If the transaction length specifies that
       another block would have been written, an End of Volume error also
       occurs, just as it would have if the data error were not present.  This
       ensures that the host is aware of the incorrect volume access, even
       though the data error is what caused the abort.

    5. On each entry, the file is expected to be attached and positioned
       correctly; otherwise, the validation check made when the write command is
       initiated would have failed.  However, the user could have detached
       and/or reattached the image file while the transaction is in progress.
       In this case, the unseen QSTAT interlock that is set during attachment
       will still be set, and we can abort writing to ensure that the host is
       made aware of the media change before any additional data is written.

    6. End_of_Volume can never be present on routine entry, because the address
       can never point beyond the end of the volume.  If Set Address attempts
       it, the command is rejected with Address Bounds, and if a write
       increments it, it wraps to zero.  End_of_Volume will be set only after
       the writes complete within a call.

    7. When a shorter tape data record is written over a longer one, the area
       from the end of the new record to the end of the block must be erased to
       maintain the HPDrive tape format.  However, the SIMH tape library "Write
       Gap" and "Erase Record Forward" routines scan the content of the area to
       be erased to ensure that the resulting erasure leaves a valid tape image
       (e.g., that half of a data record is not erased).  In this case, the area
       includes part of the data from the prior record.  If the data bytes there
       are misinterpreted as the start of another data record, the erase
       routines will attempt to shorten that record for the part erased by
       rewriting the leading and trailing length words.  This can corrupt data
       outside of the current block.  To prevent this, we call the "Tape Erase"
       routine that erases the area without examining it.
*/

static t_bool write_block (uint32 dc_id, USPTR usptr)
{
const    DSPTR dsptr = usptr->dsptr;            /* a pointer to the device state */
const    UPTR  uptr  = usptr->uptr;             /* a pointer to the associated SIMH unit */
DPPTR    delay;
uint8    *bufptr;
uint32   block_count;
size_t   count;
t_mtrlnt gap;
t_stat   status;

if (dcs [dc_id].dptr->flags & DEV_REALTIME)                 /* if real-time mode is active */
    delay = &real_time [usptr->delay_index];                /*   then point at the selected model's timing */
else                                                        /* otherwise */
    delay = (const DPPTR) &dcs [dc_id].sptr->fast_times;    /*   point at the optimized timing */

block_count = (dsptr->size + usptr->block_size - 1)     /* round up the count */
                / usptr->block_size;                    /*   of blocks to write */

if (dsptr->step_count >= block_count)                   /* if the write will not require positioner movement */
    dsptr->step_count -= block_count;                   /*   then drop the count remaining before end-of-track */

else {                                                      /* otherwise */
    dsptr->step_count += usptr->step_size - block_count;    /*   reset the count for the new track */
    uptr->wait += (*delay) [Seek_Time];                     /*     and add in the time for a positioner step */
    }

if (dsptr->state == Execution_Receive)                  /* if we are writing within a data transfer */
    uptr->wait += (*delay) [Residue_Time];              /*   then add the residue of the previous block */

else if (dsptr->opcode == Write_File_Mark               /* otherwise if we are writing for a command */
  || dsptr->opcode == Copy_Data)                        /*   that has no data transfer */
    uptr->wait += (*delay) [Block_Time] * block_count;  /*     then add the full block time */


if (dsptr->index < dsptr->size && not usptr->tape_drive)    /* if a disc data buffer is only partially filled */
    memset (&dsptr->buffer [dsptr->index],                  /*   then fill the remaining bytes */
            dsptr->buffer [dsptr->index - 1],               /*     with copies of the last byte */
            dsptr->size - dsptr->index);                    /*       to complete the buffer */

bufptr = dsptr->buffer;                                 /* point at the buffer location to begin writing */

if (not (uptr->flags & UNIT_ATT))                       /* if the unit is no longer attached */
    set_error (dc_id, usptr, Not_Ready);                /*   then report that the unit is not ready */

else if (usptr->interlocked)                            /* otherwise if the media has been replaced */
    dsptr->state = Error_Wait;                          /*   then wait for error recovery */

else do {                                               /* otherwise write "block_count" blocks */
    tpprintf (dcs [dc_id].dptr, TRACE_INCO, "Device %u unit %u wrote block %u\n",
              ADDRESS (uptr->flags), UNIT (uptr->flags), usptr->current.address);

    if (usptr->tape_drive) {                            /* if this is a tape drive */
        if (dsptr->opcode == Write_File_Mark) {         /*   then if a file mark is being written */
            gap = usptr->block_stride                   /*     then get the size */
                    - sizeof (t_mtrlnt);                /*       of the remaining area in the block */

            status = sim_tape_wrtmk (uptr);             /* write a tape mark */

            if (status == MTSE_OK)                      /* if the write succeeded */
                status = sim_tape_erase (uptr, gap);    /*   then erase the remainder of the block */
            }

        else {                                          /* otherwise it's a data write */
            gap = usptr->block_stride                   /*   so get the size */
                    - 3 * sizeof (t_mtrlnt)             /*     of the area in the block */
                    - (dsptr->index + 1 & ~1);          /*       remaining after the data record */

            status = sim_tape_erase (uptr, sizeof (t_mtrlnt));  /* erase a leading gap */

            if (status == MTSE_OK)                      /* if the erase succeeded */
                status = sim_tape_wrrecf (uptr, bufptr, /*   then write the data record */
                                          dsptr->index);

            if (status == MTSE_OK)                      /* if that succeeded */
                status = sim_tape_erase (uptr, gap);    /*   then erase the remainder of the block */
            }

        if (status != MTSE_OK) {                        /* if a tape error occurred */
            set_data_error (dc_id, usptr, status);      /*   then report an unrecoverable data error */
            dsptr->state = Error_Wait;                  /*     and wait for error recovery */

            usptr->current.address++;                   /* point the address at the next block */
            break;                                      /*   and terminate the write */
            }
        }

    else {                                                  /* otherwise this is a disc drive */
        count = sim_fwrite (bufptr, 1, usptr->block_size,   /*   so write the current disc block */
                            uptr->fileref);

        uptr->pos += usptr->block_stride;               /* update the file position */

        if ((uint32) count < usptr->block_size          /* if less than a full block was written */
          && ferror (uptr->fileref)) {                  /*   and a host file system error occurred */
            set_data_error (dc_id, usptr, MTSE_OK);     /*     then report an unrecoverable data error */
            dsptr->state = Error_Wait;                  /*       and wait for error recovery */

            usptr->current.address++;                   /* point the address at the next block */
            break;                                      /*   and terminate the write */
            }
        }

    usptr->current.address++;                           /* update the file address */
    bufptr += usptr->block_size;                        /*   and advance the buffer pointer */
    }

while (--block_count > 0                                /* loop until all blocks are written */
  && usptr->current.address < usptr->volume_size);      /*   or the address exceeds the volume size */


dsptr->index = 0;                                       /* reset the index and the count */
dsptr->count = dsptr->size;                             /*   to indicate the buffer is now empty */

if (usptr->current.address >= usptr->volume_size) {     /* if the address now exceeds the volume size */
    usptr->current.address = 0;                         /*   then wrap the address */

    if (usptr->current.length > 0) {                    /* if the transfer would continue beyond the end */
        set_error (dc_id, usptr, End_of_Volume);        /*   then report an end of volume error */
        dsptr->state = Error_Wait;                      /*     and wait for error recovery */
        }
    }

return (dsptr->state != Error_Wait);                    /* return TRUE if the write succeeded */
}


/* Activate a SIMH unit.

   This routine activates the SIMH unit associated with the CS/80 unit state
   pointer supplied by the "usptr" parameter using the activation time indicated
   by the "delay" parameter.  If the delay is "Position_Time", then the time is
   specified by the "wait" field of the SIMH unit structure, which must be set
   before calling this routine.  Otherwise, the delay is used as an index into
   either the real time or fast time delay tables, depending on which mode is
   currently active.  If tracing is enabled, the activation is logged to the
   debug file.

   The status of the activation is returned to the caller.
*/

static t_stat activate_unit (uint32 dc_id, USPTR usptr, DELAY delay)
{
int32 wait;

if (delay == Position_Time)                             /* if this is a calculated position activation */
    wait = usptr->uptr->wait;                           /*   then use the specified wait time */
else if (dcs [dc_id].dptr->flags & DEV_REALTIME)        /* otherwise if in realistic timing mode */
    wait = real_time [usptr->delay_index] [delay];      /*   then use the appropriate realistic wait */
else                                                    /* otherwise */
    wait = dcs [dc_id].sptr->fast_times [delay];        /*   use the optimized timing delay */

tpprintf (dcs [dc_id].dptr, TRACE_SERV, "Device %u unit %u state %s %s delay %d service scheduled\n",
          ADDRESS (usptr->uptr->flags), usptr->unit,
          state_names [dcs [dc_id].sptr->devices [ADDRESS (usptr->uptr->flags)].state],
          delay_names [delay], wait);

return sim_activate (usptr->uptr, wait);                /* activate the unit and return the result */
}


/* Initialize the state of a CS/80 unit.

   This routine sets up the model-specific values in the unit state structure
   specified by the "usptr" parameter.  To allow maximum user flexibility in
   assigning CS/80 units to SIMH units, setting the model-specific information
   in each CS/80 unit's state structure is delayed as long as possible --
   specifically, until a media image file is attached to a unit, or execution is
   initiated.  This routine is called for the specific unit in an ATTACH command
   or for all units during assignment validation prior to execution.

   This routine sets the following unit state fields:

     Structure Field  Description
     ---------------  ------------------------------------------------
     valid_volumes    the bitmap of the installed volumes
     delay_index      the index into the realistic time delay table
     block_size       the per-block data size in bytes
     block_stride     the per-block record size in bytes
     volume_size      the number of blocks in the volume
     step_size        the number of blocks available per actuator step
     track_size       the number of blocks available per track

   ...and depends on these state fields being properly set prior to entry:

     Structure Field  Description
     ---------------  -------------------------------------
     unit             the CS/80 unit number
     uptr             a pointer to the associated SIMH unit
     tape_drive       TRUE if the unit is a tape drive

   The "unit" field is initialized during power-on reset and is invariant.  The
   "uptr" field is set just before calling this routine by the ATTACH command
   processor and the bus assignment validator.  The "tape_drive" field is set
   appropriately during power-on reset and again whenever the the model number
   or unit number is changed by the user.  The remaining unit state fields are
   initialized elsewhere.

   Model-specific values are obtained from the Describe information.  For
   cartridge tape drives, the information describes the "long" length (600-foot)
   cartridge.  If a short-length cartridge is attached to the unit, as indicated
   by the short cartridge flag being set in the unit's user flags, the volume
   size, step size, and capacity are reduced by one-fourth to agree with the
   150-foot length.


   Implementation notes:

    1. The tape format used adds four 4-byte tape markers (leading and trailing
       erase gaps and leading and trailing record-length values) to each
       1024-byte data block, so we must increase the block stride for tape units
       by the amount of this overhead to get proper random access.  Disc units
       have no overhead, so the block stride is the same as the block size.
*/

static void initialize_unit (USPTR usptr)
{
const UPTR       uptr  = usptr->uptr;                   /* a pointer to the associated SIMH unit */
const MODEL_TYPE model = MODEL (uptr->flags);           /* the associated device model number */
const UDPTR      udptr = &describe [model].unit [usptr->unit];
const uint32     tape_record_overhead = 4 * sizeof (t_mtrlnt);

usptr->valid_volumes = udptr->fixed_volumes             /* set the valid volume map */
                         | udptr->removable_volumes;    /*   to the set of fixed and removable volumes */

if (usptr->tape_drive && model != HP_9145A)             /* all tape drive except the HP 9145 */
    usptr->delay_index = HP_9144A;                      /*   have the HP 9144's operation timing */
else                                                    /* otherwise it's a disc drive */
    usptr->delay_index = model;                         /*   so use the model's characteristic timing */

usptr->block_size   = udptr->bytes_per_block;           /* set the unit's block */
usptr->block_stride = udptr->bytes_per_block;           /*   and stride size */

if (usptr->tape_drive)                                  /* if the unit is a tape drive */
    usptr->block_stride += tape_record_overhead;        /*   then increase the stride by the overhead */

usptr->volume_size = udptr->volume.max_block + 1;       /* set the volume size */

usptr->step_size = (udptr->volume.max_head + 1)         /* determine the count of blocks accessible */
                     * (udptr->volume.max_sector + 1);  /*   without moving the positioner */

usptr->track_size = udptr->volume.max_sector + 1;       /* set the track size */

uptr->capac = usptr->volume_size * usptr->block_size;   /* reset the capacity to the default size */

if (uptr->flags & UNIT_ATT                              /* if the unit is attached */
  && uptr->User_Flags & USER_SHORT_CART) {              /*   and a short-length tape is mounted */
    usptr->volume_size /= 4;                            /*     then cut the volume size */
    usptr->step_size /= 4;                              /*       and step size */
    uptr->capac /= 4;                                   /*         and capacity to one-fourth */
    }

return;
}


/* Initialize the attached media.

   This routine initializes an attached disc or tape image file, with optional
   certification of the latter.  It implements the CS/80 Initialize Media
   command and may also be invoked when a new file is ATTACHed to a unit or an
   existing file is extended to the defined length during attachment.
   Initializing clears the data present in the file.

   On entry, the "usptr" parameter points at the unit state structure of the
   CS/80 unit to be initialized, "starting_block" specifies the address of the
   first block to initialize, and "new_state" indicates the desired state of the
   media after initialization.  The SIMH unit is assumed to be attached.  The
   routine returns the result of the initialization, which will be SCPE_OK if it
   succeeded and SCPE_IOERR if a host I/O error occurred.  For successful
   returns, the "wait" field of the SIMH unit structure is set to the
   initialization time, either real or fast as indicated by the current timing
   mode.

   Initializing disc media always overwrites the existing data.  For tape units,
   initializing or certifying may or may not overwrite, depending on the current
   and final media states.  If the media state is not changing, i.e., the
   current and new states are the same, or the media is being decertified, then
   initialization only changes the tape tables and logs.  These system blocks
   are "outside" of the data area and are not present in the tape image file.
   Consequently, we update the unit "wait" field for the time taken but
   otherwise take no action, except for updating the media state if
   decertifying.

   If writing new data blocks is indicated, one of three initialization formats
   is used, depending on whether the unit is a disc drive or tape drive.  For
   disc media, 256-byte blocks are initialized to all zeros.  For tape media
   certification, each block consists of a 1024-byte tape data record containing
   hex FF values, plus 4-byte erase gaps preceding and following the record.
   For tape media initialization, each block consists of an "erased-block"
   marker and a 1036-byte erase gap to pad the block.

   Before returning, the media state is updated, and the stream is repositioned
   to correspond to the unit's "pos" field.


   Implementation notes:

    1. This routine is called from "dc_attach" for a newly created file or a
       truncated file that is to be extended to the defined length, from the
       Initialize Media command, and from the Initiate Utility command for the
       Pattern Error Rate Test.  The latter two have called "validate_access"
       first.  Except when extending an existing file, the starting block will
       be 0.

    2. Because this routine make take some time for large files, and because it
       can be called from the SCP command line while the user waits, we
       construct the gaps + tape record block and use direct stream write calls
       rather than going through the SIMH tape library.  This imposes a smaller
       wait time on the user.

    3. Nominally, tape certification should leave behind a pattern corresponding
       to one of the internal Pattern ERT patterns.  No documentation exists as
       to which of the eight internal patterns is used.  However, several sample
       tape images have 0xFF bytes filling the blocks, so we use that value.
*/

static t_stat initialize_media (uint32 dc_id, USPTR usptr, uint32 starting_block, MEDIA_STATE new_state)
{
const UPTR        uptr = usptr->uptr;                       /* a pointer to the associated SIMH unit */
const MEDIA_STATE current_state = STATE (uptr->User_Flags); /* the current media state */
uint32            starting_byte, step_count, block_count;
uint32            buffer [260], start, end, fill, stride, block;
t_stat            result = SCPE_OK;

if (new_state == current_state                          /* if the media state is not changing */
  || new_state == Decertified) {                        /*   or the media is being decertified */
    block_count = 16;                                   /*     then set up the time that would be taken */
    step_count  = 16;                                   /*       if the system blocks were updated */

    if (new_state == Decertified)                       /* if the media is decertified */
        new_state = Initialized;                        /*   then revert to the initialized state */
    }

else {
    if (usptr->tape_drive) {                            /* if a tape initialization is being done */
        if (new_state == Recertified)                   /* otherwise if a forced certification is requested */
            new_state = Certified;                      /*   then the new state will be certified */

        if (new_state == Certified) {                   /* if certifying */
            buffer [0]   = MTR_GAP;                     /*   then write data blocks */
            buffer [1]   = usptr->block_size;           /*     of the full record size */
            buffer [258] = usptr->block_size;
            buffer [259] = MTR_GAP;

            start = 2;                                  /* fill the data block */
            end   = 257;                                /*   with all-ones bytes */
            fill  = D32_UMAX;                           /*     to represent the certification pattern */
            }

        else {                                          /* otherwise we are initializing */
            buffer [0] = ERASED_BLOCK;                  /*   so leave the data area erased */

            start = 1;                                  /* uninitialized blocks consist */
            end   = 259;                                /*   of "data not written" markers */
            fill  = MTR_GAP;                            /*     with erase gap padding */
            }
        }

    else {                                              /* otherwise disc initialization */
        new_state = Initialized;                        /*   is always performed */

        start = 0;                                      /* fill the buffer */
        end   = 63;                                     /*   with one block */
        fill  = 0;                                      /*     of zero values */
        }

    do                                                  /* fill the buffer */
        buffer [start++] = fill;                        /*   from the start index to the end index */
    while (start <= end);                               /*     with the fill value */

    starting_byte = starting_block * usptr->block_stride;   /* set the starting byte position */
    sim_fseek (uptr->fileref, starting_byte, SEEK_SET);     /*   and reposition the file stream */

    stride = usptr->block_stride / sizeof (buffer [0]);     /* convert the block stride to a buffer stride */

    for (block = starting_block; block < usptr->volume_size; block++)       /* for each block in the volume */
        sim_fwrite (buffer, sizeof (buffer [0]), stride, uptr->fileref);    /*   write the tape or disc block */

    fflush (uptr->fileref);                             /* flush the writes */
    sim_fseek (uptr->fileref, uptr->pos, SEEK_SET);     /*   and reposition to the original location */

    block_count = block - starting_block;                   /* get the number of blocks written */
    step_count  = usptr->volume_size / usptr->step_size;    /*   and the number of tracks crossed */

    if (ferror (uptr->fileref)) {                       /* if a stream error occurred */
        new_state = Formatted;                          /*   then mark the media as uninitialized */

        set_error (dc_id, usptr, Unit_Fault);                   /* log a unit fault */
        result = report_io_error (uptr, "initialize media");    /*   and report it to the console with failure */
        }
    }

if (dcs [dc_id].dptr->flags & DEV_REALTIME)             /* calculate the time to write the blocks */
    uptr->wait += step_count * real_time [usptr->delay_index] [Seek_Time]
                    + block_count * real_time [usptr->delay_index] [Block_Time];
else
    uptr->wait += step_count * dcs [dc_id].sptr->fast_times [Seek_Time]
                    + block_count * dcs [dc_id].sptr->fast_times [Block_Time];

uptr->User_Flags = uptr->User_Flags & ~USER_STATE       /* set the tape status */
                     | TO_STATE (new_state);            /*   to the new state */

return result;                                          /* return the result of the initialization */
}


/* Set the block address.

   This routine sets the current address from the six-byte single-vector value
   present in the device buffer and returns TRUE if the address is in range or
   FALSE if not.

   On entry, the "usptr" parameter points to the unit state structure of the
   CS/80 unit to be set.  The first byte of the six-byte vector is indicated by
   the buffer index.


   Implementation notes:

    1. Taking the start of the vector from the buffer index rather than from
       element zero allows the routine to be used to obtain the addresses for
       the Copy Data command as well as for Set Address.

    2. Setting a local pointer to the first byte and then assembling the integer
       value by shifting and masking the four bytes allows gcc to compile the
       entire operation to a single BSWAP instruction.

    3. The address is set from the lower four bytes of the vector, as no
       supported drive is larger than 2 ** 32 blocks (1.1 TB).  Success
       therefore depends on the upper two bytes being zero, as well as the
       converted address being within the volume size.
*/

static t_bool set_block (USPTR usptr)
{
const uint8 *p = &(usptr->dsptr->buffer [usptr->dsptr->index]); /* point at the first of the parameter bytes */

usptr->current.address =                                /* set the address */
  TO_DWORD (TO_WORD (p [2], p [3]),                     /*   to the lower four bytes */
            TO_WORD (p [4], p [5]));                    /*     of the block number */

return p [0] == 0 && p [1] == 0                         /* return TRUE if the upper bytes are zero */
  && usptr->current.address < usptr->volume_size;       /*   and the address is within range */
}


/* Set the cylinder/head/sector address.

   This routine sets the current address from the six-byte three-vector value
   present in the device buffer and returns TRUE if the address is in range or
   FALSE if not.

   On entry, the "usptr" parameter points to the unit state structure of the
   CS/80 unit to be set.  The first byte of the six-byte vector is indicated by
   the buffer index.  The values for the cylinder, head, and sector are
   contained successively in three, one, and two bytes of the buffer.  These
   values are converted to single-vector format before setting the block
   address.


   Implementation notes:

    1. Taking the start of the vector from the buffer index rather than from
       element zero allows the routine to be used to obtain the addresses for
       the Copy Data command as well as for Set Address.
*/

static t_bool set_chs (USPTR usptr)
{
const uint8 *p = &(usptr->dsptr->buffer [usptr->dsptr->index]); /* point at the first of the parameter bytes */

usptr->current.address =                                        /* set the address to the block */
  TO_DWORD (p [0], TO_WORD (p [1], p [2])) * usptr->step_size   /*   specified by the cylinder */
    + p [3] * usptr->track_size                                 /*     and head */
    + TO_WORD (p [4], p [5]);                                   /*       and sector */

return usptr->current.address < usptr->volume_size;     /* return TRUE if the address is within range */
}


/* Set the error status.

   This routine is called to set one of the error bits in the unit status array.
   On entry, the "usptr" parameter points to the unit state structure of the
   CS/80 unit to be set, and the "error" parameter indicates the status bit to
   set.  On exit, the QSTAT value has been set, and, if appropriate, the
   controller state has been changed to Reporting_Wait or Error_Wait.

   The error status value is used to determine the index into the error status
   array and the bit to set within the indexed element.  If the error is not
   masked by a prior Set Status Mask command, the selected bit is set.  If QSTAT
   is not already 2, it is set to 1 to indicate a hard error; this prevents a
   subsequent error from hiding the power fail status.  If the bit has been
   masked, then neither the status byte nor the QSTAT value are changed.  The
   remainder of the error processing is performed in either case.

   If the error occurred during execution of a Copy Data command, and the unit
   recording the error is not the controller, an additional Cross-Unit error is
   set in the controller.

   If a Power Fail error occurred, then QSTAT is set to 2, the QSTAT interlock
   is set, and the state is changed to the reporting state.

   If the error is Unrecoverable Data, and the unit has experienced an earlier
   Unrecoverable Data error within the same transaction, an Unrecoverable Data
   Overflow error is set.

   Finally, if the error is not Unrecoverable Data, Unrecoverable_Data_Overflow,
   or End of Volume, the controller state is changed to Error_Wait to enable
   recovery from the error (these three errors are not immediately fatal but
   instead allow the current command to continue).


   Implementation notes:

    1. Fault errors, including Power Fail, cannot be masked.  These are
       rejected by Set Status Mask command, so we do not need to test for it
       here.

    2. The CS/80 specification says that a Message Sequence error is suppressed
       if a reject or fault error has occurred earlier, so that moving directly
       to the Reporting phase does not trigger additional errors when, e.g., an
       execution message is received.  However, we move to the Error_Wait state
       rather than the Reporting_Wait state, as the former allows an execution
       message without producing a sequence error.  Therefore, we do not need to
       suppress this error explicitly.

       An exception is made for a Power Fail entry.  In this case, commands must
       be accepted but ignored until the "unseen QSTAT" interlock is cleared.
       We use the optional reporting state to allow commands to be accepted
       (receiving a command in the Reporting_Wait or Error_Wait state would
       generate a Message Sequence error).  Command execution is still inhibited
       by the interlock.
*/

static void set_error (uint32 dc_id, USPTR usptr, ERROR_STATUS error)
{
static const char *operation [] = {             /* bit operation names, indexed by Boolean masking */
    "set",                                      /*   FALSE = bit is not masked */
    "masked"                                    /*   TRUE  = bit is masked */
    };
DC_STATE *const dc    = dcs [dc_id].sptr;       /* the pointer to the channel state */
DEVICE   *const dptr  = dcs [dc_id].dptr;       /* the pointer to the controlling device */
const DSPTR     dsptr = usptr->dsptr;           /* the pointer to the associated device state */
t_bool          masked;

masked = TEST_ERR (usptr->current.status_mask, error);  /* TRUE if the error is masked */

if (not masked) {                                       /* if the error is not masked */
    SET_ERR (usptr->status, error);                     /*   then set the error status bit */

    if (usptr->qstat != 2)                              /* if a power fail report is not pending */
        usptr->qstat = 1;                               /*   then set the standard QSTAT value */
    }

tpprintf (dptr, TRACE_CMD, "Device %u unit %u error %u %s\n",
                           dsptr - dc->devices, usptr->unit,
                           error, operation [masked]);

if (dsptr->opcode == Copy_Data && usptr->unit != 15)        /* if this is a Copy Data unit error */
    set_error (dc_id, &dsptr->units [UNIT_15], Cross_Unit); /*   then record a cross-unit error too */

if (error == Power_Fail) {                              /* if a power failure or media change occurred */
    usptr->qstat = 2;                                   /*   then set the special QSTAT value */
    usptr->interlocked = TRUE;                          /*     and enable the unseen QSTAT interlock */

    dsptr->state = Optional_Wait;                       /* wait for the reporting request */
    }

else if (error == Unrecoverable_Data)                           /* otherwise if a data error occurred */
    if (usptr->data_error)                                      /*   then if this is not the first error */
        set_error (dc_id, usptr, Unrecoverable_Data_Overflow);  /*     then record an error overflow */

    else {                                              /* otherwise */
        usptr->bad_address = usptr->current.address;    /*   save the bad address */
        usptr->data_error = TRUE;                       /*     and mark the error */
        }

else if (error != End_of_Volume                         /* otherwise if the error */
  && error != Uninitialized_Media                       /*   is a */
  && error != Unrecoverable_Data_Overflow               /*     fatal */
  && error != Operator_Request)                         /*       one */
    dsptr->state = Error_Wait;                          /*         then begin error recovery */

return;
}


/* Report a tape or disc error.

   This routine is called to report errors from the SIMH magnetic tape library
   or host disc I/O errors to the simulation console, and to reflect these as
   Unrecoverable Data errors in the invoking unit.

   On entry, the "usptr" parameter points to the unit state structure of the
   CS/80 unit to report, and the "cause" parameter indicates the type of error.
   The latter will be MTSE_OK if a disc error occurred, or one of the MTSE error
   codes if a tape error occurred.  For the former, the actual stream error is
   retrieved and printed, and the error indicator in the file stream is cleared.
   For the latter, the MTSE error code is used as an index into a table of error
   descriptions to determine the error message; the stream has already been
   cleared by the tape library.

   In either case, an Unrecoverable Data error is logged to the supplied unit's
   status.
*/

static void set_data_error (uint32 dc_id, USPTR usptr, t_stat cause)
{
set_error (dc_id, usptr, Unrecoverable_Data);           /* report an unrecoverable data error */

if (cause == MTSE_OK)                                   /* if it is not a tape error */
    report_io_error (usptr->uptr, "disc");              /*   then report a stream error to the console */

else                                                    /* otherwise it is a tape error */
    cprintf ("%s simulator tape I/O error: %s\n",       /*   so report the cause to the console */
             sim_name, tape_errors [cause]);

return;
}


/* Report a stream I/O error.

   This routine reports errors indicated by the host file system to the console.
   On entry, "uptr" points to the SIMH unit whose file stream encountered the
   error, and "source" points at a character string that gives the source of
   the error.  The error is cleared, and the routine returns the error code for
   "I/O error", which can be returned to SCP to stop the simulator.
*/

static t_stat report_io_error (UPTR uptr, char *source)
{
cprintf ("%s simulator %s I/O error: %s\n",             /* report the error to the console */
         sim_name, source, strerror (errno));

clearerr (uptr->fileref);                               /* clear the error */

return SCPE_IOERR;                                      /* return an I/O error to stop the simulator */
}


/* Abort an active channel transaction.

   This routine is called to abort a channel transaction in progress.  An abort
   occurs when a device is unaddressed in the middle of exchanging information
   with the bus controller, rather than at the logical completion of a
   transaction phase.  An abort usually precedes a device clear to return the
   device to a known state.

   On entry, the "bus_group" parameter is either BUS_LAG or BUS_TAG to designate
   whether the device was unaddressed while listening or talking.  Normally,
   this is done by an Unlisten or Untalk primary issued by the bus controller.
   However, per IEEE-488, a listening device may be unaddressed by IFC, by an
   Unlisten, or by addressing the device to talk, and a talking device may be
   unaddressed by IFC, by addressing another device to talk (or no device via
   Untalk), or by addressing the device to listen.

   In practice, aborts are not issued by changing a device from listener to
   talker or vice versa, or by addressing another device to talk, without first
   Unlistening or Untalking the device.  So the only cases we need to handle are
   Unlistens, Untalks, or Interface Clears.  For the latter, all currently
   addressed devices are unaddressed by calling this routine twice: once to
   Unlisten and then to Untalk.

   If the parameter is BUS_TAG, the Untalk affects the current talker.  If it is
   BUS_LAG, the Unlisten affects all current listeners.  Then for each affected
   device, a Message Length error is logged for the currently active unit, and
   if the corresponding SIMH unit is canceled to stop the transaction.  If the
   controller is Unlistened in the middle of a write command execution phase, a
   partially filled buffer must be posted to the media image file before
   returning.

   Finally, the device state is changed to Reporting_Wait, and the poll response
   is enabled to complete the abort.


   Implementation notes:

    1. Bus initialization ensures prior to execution that every device in the
       "dc->present" map has all of its CS/80 units mapped to SIMH units.
       As channel presence is used to qualify listeners and the talker, the unit
       state "uptr" is guaranteed to be valid.  Therefore, no NULL check is
       needed before passing it to "sim_cancel".

    2. As only one talker is addressed at a time, a BUS_TAG abort could be
       quicker if the loop had "bus_address" initialized to the channel talker
       and the device map initialized to 1 (so only that address would be
       aborted).  However, the extra complexity of the setup isn't worth the
       effort, given that channel aborts very rarely occur.
*/

static void channel_abort (uint32 dc_id, BUS_DATA bus_group)
{
DC_STATE *const dc   = dcs [dc_id].sptr;        /* the pointer to the channel state */
DEVICE   *const dptr = dcs [dc_id].dptr;        /* the pointer to the controlling device */
DSPTR  dsptr;
USPTR  usptr;
uint32 dev_map, bus_address;

tpprintf (dptr, TRACE_CMD, "Channel abort\n");

if (bus_group == BUS_TAG)                               /* if this is an Untalk abort */
    dev_map = BIT (dc->talker);                         /*   then clear the talking device */
else                                                    /* otherwise it's an Unlisten abort */
    dev_map = dc->listeners;                            /*   so clear all listening devices */

while (dev_map != 0) {                                  /* loop through the devices */
    bus_address = BIT_NUMBER (dev_map);                 /*   and get the next bus address */

    dsptr = &dc->devices [bus_address];                 /*   then get pointers to the device state */
    usptr = &dsptr->units [MAPUS (dsptr->unit)];        /*     and the currently active unit state */

    set_error (dc_id, usptr, Message_Length);           /* log a message length error for the active unit */

    sim_cancel (usptr->uptr);                           /* cancel any pending event service */

    if (dsptr->opcode == Locate_and_Write               /* if a write is aborting */
      && dsptr->state == Execution_Receive              /*   and data is being transferred */
      && dsptr->index > 0)                              /*     and the next block is partially filled */
        write_block (dc_id, usptr);                     /*       then write it with last-byte fill */

    tpprintf (dptr, TRACE_INCO, "Device %u %s command completed with qstat %u\n",
              bus_address, opcode_names [dsptr->opcode], usptr->qstat);

    dsptr->state = Reporting_Wait;                      /* move to the reporting state */
    set_response (dc_id, bus_address, SET);             /*   and enable the device's poll response */

    dev_map ^= BIT (bus_address);                       /* clear the current bit */
    }

dc->active = FALSE;                                     /* end the transaction */

return;
}


/* Enable or disable a device's parallel poll response.

   This routine changes the poll response for a specified bus device.  It
   corresponds in hardware to a device's IEEE-488 PPE (parallel poll enable) or
   PPD (parallel poll disable) state.  When the controller conducts a parallel
   poll, the responses of all eight bus devices are placed on the data bus for
   retrieval by the controller.  An enabled poll response indicates to the host
   that a device is requesting service.

   On entry, the "address" parameter is the bus address of the device whose
   poll response is changing, and the "response" parameter indicates whether the
   response should be set (enabled) or cleared (disabled).  The poll response
   bit corresponding to the bus address is included or excluded from the set of
   poll responses.

   If a parallel poll is in progress when a poll response is set, the poll is
   conducted again to reflect the new response.


   Implementation notes:

    1. The set of poll responses is maintained by the channel to improve
       performance, so that we do not need to query each device during a poll to
       ascertain its response.
*/

static void set_response (uint32 dc_id, uint32 address, FLIP_FLOP response)
{
static const char *response_name [] = {         /* poll response names, indexed by Boolean response */
    "disabled",                                 /*   FALSE = poll response is disabled */
    "enabled"                                   /*   TRUE  = poll response is enabled */
    };
DC_STATE *const dc           = dcs [dc_id].sptr;        /* the pointer to the channel state */
const BUS_DATA old_responses = dc->response_set;        /* the current set of poll responses */
DEVICE   *const dptr         = dcs [dc_id].dptr;        /* the pointer to the controlling device */
const uint32    channel      = CHANNEL (dptr->flags);   /* the channel address of the bus controller */

if (response == SET)                                    /* if the poll response is set */
    dc->response_set |= HPIB_BIT (address);             /*   then include the poll bit in the response */
else                                                    /* otherwise we are clearing the poll response */
    dc->response_set &= ~HPIB_BIT (address);            /*   so remove the poll bit */

if (dc->response_set != old_responses)
    tpprintf (dcs [dc_id].dptr, TRACE_STATE, "Device %d parallel poll response %s\n",
              address, response_name [response]);

if (dc->poll_active && response == SET)                             /* if a parallel poll is in progress */
    dc->bus = hpib_source (channel, address, dc->bus);              /*   then conduct it again with the new response */

return;
}


/* Clear a device or unit.

   This routine is called to clear one unit or all units within a device.
   Clearing a unit returns it to a known state.

   On entry, the "address" parameter is the bus address of the device to clear.
   Only the currently selected unit is cleared, unless the selected unit is the
   controller (unit 15), in which case all units within the device are cleared.

   Per the CS/80 specification, a clear aborts the current transaction as soon
   as possible without losing any data and then resets the internal state to a
   known set of values.  After the clear, the device goes to the reporting
   state, although retrieving the report is optional.

   Both the Clear and Cancel commands can be used to terminate a lengthy
   command, such as Initialize Media.  The difference is that Clear returns the
   unit(s) to the power-on state, while Cancel simply terminates the interrupted
   transaction.  If a write is being done, and the buffer is partly filled, it
   will be written as part of the Clear or Cancel.

   Clears are often used at system startup to place all bus devices in a known
   state.


   Implementation notes:

    1. A clear resets the complementary parameters to their power-on values.
       For tape units, resetting the address to 0 may take a long time,
       depending on the original tape position.  We do not model that time here.

    2. The spec says that a Clear must not lose any data.  If a write command is
       cleared, and a partial buffer is filled, the buffer should be written.
       To issue the Clear, the device must first be Unlistened and then
       readdressed to supply the command secondary and opcode.  The spec doesn't
       say if the Unlisten or the Clear should post the buffer.  For safety, we
       elect to post the buffer in response to the Unlisten (in the
       "channel_abort" routine).  So we need not take any action here.

    3. While tracing reports clearing the defined units only, we actually clear
       all requested units, so that a subsequent unit assignment will begin in
       the cleared state.
*/

static void clear_device (uint32 dc_id, uint32 address)
{
const DSPTR dsptr = &dcs [dc_id].sptr->devices [address];
USPTR       usptr;
uint32      unit, first_unit, last_unit;

if (dsptr->unit == 15) {                                /* if all units are to be cleared */
    first_unit = 0;                                     /*   then set */
    last_unit  = UNIT_COUNT - 1;                        /*     the full unit range */
    }

else                                                    /* otherwise */
    first_unit = last_unit = dsptr->unit;               /*   clear just the current unit */

dsptr->unit = 0;                                        /* clear the device's current unit */
dsptr->volume = 0;                                      /*   and current volume */

dsptr->state = Optional_Wait;                           /* reporting is optional after a clear */

for (unit = first_unit; unit <= last_unit; unit++) {    /* for each selected unit */
    usptr = &dsptr->units [unit];                       /*   get the corresponding unit state pointer */

    usptr->persistent = power_on_state;                 /* reset the persistent and current parameters */
    usptr->current    = power_on_state;                 /*   to their power-on values */

    usptr->qstat = 0;                                   /* clear the quick status */
    memset (usptr->status, 0, 8);                       /*   and all of the error status bits */

    usptr->interlocked = FALSE;                         /* clear the QSTAT interlock */

    if (usptr->uptr != NULL) {                          /* if the unit is defined */
        sim_cancel (usptr->uptr);                       /*   then cancel any in-progress command */

        tpprintf (dcs [dc_id].dptr, TRACE_INCO, "Device %u unit %u cleared\n",
                  address, (unit == 3 ? 15 : unit));
        }
    }

set_response (dc_id, address, SET);                     /* enable the device's poll response */

return;
}
