/* hp_disclib.c: HP MAC/ICD disc controller simulator library

   Copyright (c) 2011-2021, J. David Bryan
   Copyright (c) 2004-2011, Robert M. Supnik

   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
   AUTHORS 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 names of the authors 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 authors.

   02-Apr-21    JDB     Changed u3-u6 synonyms to title case to avoid conflicts
   09-Dec-19    JDB     Replaced debugging macros with tracing macros
   27-Dec-18    JDB     Revised fall through comments to comply with gcc 7
   22-Apr-17    JDB     A failed sim_fseek call now causes a drive fault
   10-Oct-16    JDB     Moved "hp3000_defs.h" inclusion from "hp_disclib.h"
   03-Aug-16    JDB     "fmt_bitset" now allows multiple concurrent calls
   09-Jun-16    JDB     Added casts for ptrdiff_t to int32 values
   08-Jun-16    JDB     Corrected %d format to %u for unsigned values
   16-May-16    JDB     DRIVE_PROPS.name is now a pointer-to-constant
   13-May-16    JDB     Modified for revised SCP API function parameter types
   03-May-16    JDB     Added a trace to identify the unit requesting attention
   24-Mar-16    JDB     Changed the buffer element type from uint16 to DL_BUFFER
   21-Mar-16    JDB     Changed uint16 types to HP_WORD
   27-Jul-15    JDB     First revised release version
   21-Feb-15    JDB     Revised for new controller interface model
   24-Dec-14    JDB     Added casts for explicit downward conversions
   27-Oct-14    JDB     Corrected the relative movement calculation in start_seek
   20-Dec-12    JDB     sim_is_active() now returns t_bool
   24-Oct-12    JDB     Changed CNTLR_OPCODE to title case to avoid name clash
   07-May-12    JDB     Corrected end-of-track delay time logic
   02-May-12    JDB     First release
   09-Nov-11    JDB     Created disc controller common library from DS simulator

   References:
     - 13037 Disc Controller Technical Information Package
         (13037-90902, August 1980)
     - HP 13365 Integrated Controller Programming Guide
         (13365-90901, February 1980)
     - HP 1000 ICD/MAC Disc Diagnostic Reference Manual
         (5955-4355, June 1984)
     - RTE-IVB System Manager's Manual
         (92068-90006, January 1983)
     - DVR32 RTE Moving Head Driver Source
         (92084-18711, Revision 5000)


   The 13037 multiple-access disc controller (MAC) connects from one to eight HP
   7905 (15 MB), 7906 (20 MB), 7920 (50 MB), or 7925 (120 MB) disc drives to
   interfaces installed in from one to eight HP 1000, 2000, or 3000 CPUs.  The
   drives use a common command set and present data to the controller
   synchronously at a data rate of 468.75 kilowords per second (2.133
   microseconds per word).

   The controller hardware consists of three PCAs: a 16-bit microprogrammed
   processor constructed from 74S181 bit slices operating at 5 MHz, a device
   controller that provides the interconnections to the drives and CPU
   interfaces, and an error correction controller that enables the correction of
   up to 32-bit error bursts.  1024 words of 24-bit firmware are stored in ROM
   on the error correction PCA, and the execution time is 200 nanoseconds per
   instruction.

   The Integrated Controller Drive (ICD) models include the HP 7906H, 7920H, and
   7925H.  These drives are identical to the corresponding MAC drives, except
   that they integrate a single-CPU version of the MAC controller on two PCAs
   housed within the drive: an 8-bit microprocessor constructed from two 4-bit
   slices operating at 3.75 MHz, and an 8-bit DMA that handles the data path
   between the drive and CPU.  Connection to the CPU is via the Hewlett-Packard
   Interface Bus (HP-IB) -- HP's implementation of the IEEE-488 standard.

   The ICD command set essentially is the MAC command set modified for
   single-unit operation.  The unit number and CPU hold bit fields in the opcode
   words are unused in the ICD implementation.  The Load TIO Register, Wakeup,
   and Request Syndrome commands are removed, as Load TIO is used with the HP
   3000, Wakeup is used in a multi-CPU environment, and the simpler ICD
   controller does not support ECC.  Controller status values 02B (Unit
   Available) and 27B (Unit Unavailable) are dropped as the controller supports
   only single units, 12B (I/O Program Error) is reused to indicate HP-IB
   protocol errors, 13B (Sync Not Received) is added, and 17B (Possibly
   Correctable Data Error) is removed as error correction is not supported.

   Some minor redefinitions also occur.  For example, status 14B (End of
   Cylinder) is expanded to include an auto-seek beyond the drive limits, and
   37B (Drive Attention) is restricted to just head unloads (from head loads and
   unloads).

   The MAC controller offers an HP-IB option: the HP 12745A Disc Controller to
   HP-IB Adapter Kit.  This card plugs into the 13037's chassis containing the
   other three controller cards and connects to the CPU interface port of the
   device controller PCA in place of the multi-CPU-interface cable.  It allows
   HP-IB 3000s and the HP 64000 Logic Development Station to connect to MAC disc
   drives; the ICD drives are not supported on these systems.

   This library provides the common functions required by HP disc controllers.
   It implements the 13037 MAC and 13365 ICD controller command sets used with
   the 7905/06/20/25 and 7906H/20H/25H disc drives.

   The library is an adaptation of the code originally written by Bob Supnik
   for the HP2100 DS simulator.  DS simulates a 13037 controller connected via a
   13175 disc interface to an HP 1000 computer.  To create the library, the
   functions of the controller were separated from the functions of the
   interface.  This allows the library to work with other CPU interfaces, such
   as the 12821A HP-IB disc interface, that use substantially different
   communication protocols.  The library functions implement the controller
   command set for the drive units.  The interface functions handle the transfer
   of commands and data to and from the CPU.

   The original release of this library did not handle the data transfer between
   the controller and the interface directly.  Instead, data was moved between
   the interface and a sector buffer by the interface simulator, and then the
   buffer was passed to the disc library for reading or writing.  This buffer
   was also used to pass disc commands and parameters to the controller, and to
   receive status information from the controller.

   While this approach served to allow the library to be shared between
   dissimilar interfaces, each interface had to have intimate knowledge of the
   internal controller state in order to schedule parameter and data transfers.
   In particular, the unit service routine had to base its actions on specific
   controller phase and opcode pairs and manipulate the internal state variables
   of the controller.  As such, the library could not be viewed opaquely.

   In addition, the HP 3000 interface required channel program support that was
   not provided by the simulation library although it was present in hardware.
   Adapting the existing library model would have placed an even larger and more
   intimate burden on the interface simulation.

   As a result, the library API was rewritten to model the hardware more
   closely and provide a more strict separation of controller and interface
   functions.  Instead of providing separate routines to prepare, start, and end
   commands, service units, and poll drives for Attention status, the new model
   provides a single routine that represents the hardware data, flag, and
   function buses between the interface and controller.  The interface calls
   this routine whenever the state of the flag bus changes, and the controller
   responds by potentially changing the data and function buses.  The interface
   merely responds to those changes without requiring any other knowledge of the
   internal state of the controller.


   A device interface simulator interacts with the disc controller simulator via
   the dl_controller routine, which simulates the command, status, and data
   interconnection between the interface and controller.  Utility routines are
   also provided to attach and detach disc image files from drive units, load or
   unload the drive's heads, set drive model and protection status, select the
   interface timing mode (real or fast), and enable overriding of disc command
   status returns for diagnostics.

   In hardware, the interface and controller are interconnected via a 16-bit
   bidirectional data bus (IBUS), a 6-bit flag bus and one signal (CLEAR) to
   the controller, and a 4-bit function bus (IFNBUS) and four signals (ENID,
   ENIR, IFVLD, and IFCLK) to the interface.  The interface initiates controller
   action by changing the state of the flag bus, and the controller responds by
   asserting a function on the function bus.  For example, the interface starts
   a disc command by asserting CMRDY on the flag bus.  The controller responds
   by placing the IFGTC (Interface Get Command) function on the function bus and
   asserting the ENID (Enable Interface Drivers) and IFVLD (Interface Function
   Valid) signals.  The interface then replies by placing the command on the
   data bus, where it is read by the controller.  The controller then decodes
   the command and initiates processing.

   The controller microprogram runs continuously.  However, command execution
   pauses in various wait loops whenever the controller must suspend until an
   external event occurs.  For example, an Address Record command waits first
   for the CPU to send the cylinder address and then waits again for the CPU to
   send the head/sector address.  The controller then saves these values in
   registers before completing the command and then waiting for the CPU to send
   a new command.

   In simulation, the dl_controller routine is called with a set of flags and
   the content of the data bus whenever the flag state changes or a service
   event occurs.  The routine returns a set of functions and the new content of
   the data bus.  To use the above example, dl_controller would be called with
   CMRDY and the command word and would return IFGTC; the data bus return value
   would not be used in this case.  In hardware, the controller might send a
   series of individual functions to the interface in response to a single
   invocation.  In simulation, this series would be collected into a single
   function set for return.

   Hardware wait loops are simulated by the dl_controller routine returning to
   the caller until the expected external event occurs.  In the Address Record
   example, dl_controller would be called first when the command is issued by
   the CPU.  The routine would initiate command processing and then return to
   wait for the cylinder address.  When the CPU provided the address, the
   interface simulator would call dl_controller again with the cylinder value.
   The routine would then return to wait for the head/sector address.  When it
   was available, dl_controller would be called with the value, and the routine
   would complete the command and return to the caller to wait for a new
   command.  So, in simulation, the controller only "runs" when it has work to
   do.

   A controller instance is represented by a CNTLR_VARS structure, which
   maintains the controller's internal state.  A MAC interface will have a
   single controller instance that controls up to eight drive units, whereas an
   ICD interface will have one controller instance per drive unit.  The minor
   differences in controller action between the two are handled internally.

   The interface simulator must declare one unit for each disc drive to be
   controlled by the library.  For a MAC controller, eight units are required,
   plus one additional unit for the controller itself.  For an ICD controller,
   only one unit is required.

   The controller maintains five values in each drive's unit structure:

     u3 (Cyl)    -- the current drive cylinder
     u4 (Status) -- the drive status (Status-2)
     u5 (Opcode) -- the drive current operation in process
     u6 (Phase)  -- the drive current operation phase
     pos         -- the current byte offset into the disc image file

   Drives maintain their cylinder (head positioner) locations separate from the
   cylinder location stored in the controller.  This allows the controller to
   implement sparing by positioning to one location while storing a different
   location in the sector headers.  It also allows seek retries by issuing a
   Recalibrate (which moves the positioner to cylinder 0) followed by the
   original read or write (which repositioned to the cylinder stored in the
   controller).

   The drive status field contains only a subset of the status maintained by
   drives in hardware.  Specifically, the Attention, Read-Only, First Status,
   Fault, and Seek Check bits are stored in the status field.  The other bits
   (Format Enabled, Not Ready, and Drive Busy) are set dynamically whenever
   status is requested.

   Per-drive opcode and phase values allow seeks to be overlapped.  For example,
   a Seek issued to unit 0 may be followed by a Read issued to unit 1.  When the
   seek completes on unit 0, its opcode and phase values will let the controller
   set the appropriate seek completion status without disturbing the values
   currently in-use by unit 1.

   The simulation defines these command phases:

     Idle        -- the unit is not currently executing a command
     Parameter   -- the unit is obtaining or returning parameter values
     Seek        -- the unit is seeking to a new head position
     Rotate      -- the unit is rotating into position to access a sector
     Data        -- the unit is obtaining or returning sector data values
     Intersector -- the unit is rotating between sectors
     End         -- the unit is completing a command

   A value represents the current state of the unit.  If a unit is active, the
   phase will end when the unit is serviced.

   In addition to the controller structure(s), an interface declares a data
   buffer to be used for sector transfers.  The buffer is an array containing
   DL_BUFSIZE 16-bit elements; the address of the buffer is stored in the
   controller state structure.  The controller maintains the current index into
   the buffer, as well as the length of valid data stored there.  Only one
   buffer is needed per interface, regardless of the number of controllers or
   units handled, as a single interface cannot perform data transfers on one
   drive concurrently with a command directed to another drive.

   An interface is also responsible for declaring a structure of type
   DELAY_PROPS that contains the timing values for the controller when in
   FASTTIME mode.  The values are event counts for these actions:

     - track-to-track seek time
     - full-stroke seek time
     - full sector rotation time
     - per-word data transfer time
     - intersector gap time
     - controller execution overhead time

   These values are typically exposed via the interface's register set and so
   may be altered by the user.  A macro, DELAY_INIT, is provided to initialize
   the structure.

   An interface may optionally declare an array of DIAG_ENTRY structures if it
   wishes to use the diagnostic override capability.  Diagnostic overrides are
   used to return controller status values that otherwise are not simulated to a
   diagnostic program.  An example would be a Correctable Data Error or a
   Head-Sector Miscompare.  If this facility is to be used, a pointer to the
   array is placed in the CNTLR_VARS structure when it is initialized, and the
   array itself is initialized with the DL_OVEND value that indicates that no
   overrides are currently defined.

   If the pointer is set, then when each command is started, the cylinder, head,
   sector, and opcode values from the current override entry are checked against
   the corresponding current controller values.  If a match occurs, then the
   controller status and Spare/Protected/Defective values are set from the entry
   rather than being cleared, and the pointer is moved to the next entry.  These
   values will then be returned as the completion status of the current command.

   If the command performs address verification, then any SPD value(s) will be
   used as the result of the verification.  In particular, setting the P bit for
   a Write will cause a Protected Track error if the FORMAT switch is not on,
   and any status value other than Normal Completion, Correctable Data Error, or
   Uncorrectable Data Error will cause a verification abort.

   In hardware, the errors that may occur during verification are Cylinder
   Miscompare, Head-Sector Miscompare, Sync Timeout, Illegal Spare Access, and
   Defective Track; any of these errors may be simulated.  In addition, an
   Uncorrectable Data Error may occur in hardware if the controller is unable to
   verify any of the 16 sectors starting at the sector preceding the target
   sector, but this error cannot be simulated.

   Specifying either a Correctable Data Error or an Uncorrectable Data Error
   will cause an abort at the end of the first sector of a read, write, or
   verify command.

   Correctable Data Error and Uncorrectable Data Error may also be specified for
   the Request Syndrome command.  A table entry with the former status value is
   always followed by an additional entry that contains the values to be
   returned for the three syndrome words and the displacement.

   The last defined table entry always contains a special end-of-table value.

   The controller library provides a macro, DL_MODS, that initializes MTAB
   entries, and two utility routines, dl_set_diag and dl_show_diag, that provide
   a user interface for setting up a table of diagnostic overrides.  See the
   comments for these routines below for the command syntax.

   A macro, CNTLR_INIT, is provided to initialize the controller structure from
   the following parameters:

     - the type of the controller (MAC or ICD)
     - the simulation DEVICE structure on which the controller operates
     - the data buffer array
     - the diagnostic override array or NULL if not used
     - the structure containing the FASTTIME values

   A macro, DL_REGS, is also provided that initializes a set of REG structures
   to provide a user interface to the controller structure.

   In hardware, disc drives respond to commands issued by the controller.  The
   only unsolicited status from drives occurs when the heads are loaded or
   unloaded by the operator.  In simulation, SET <dev> UNLOAD and SET <dev> LOAD
   commands represent these actions.  The controller must be notified by calling
   the dl_load_unload routine in response to changes in a disc drive's RUN/STOP
   switch.

   Finally, the controller library provides extensive tracing of its internal
   operations via debug logging.  Six debug flags are declared for use by the
   interface simulator.  When enabled, these report controller actions at
   various levels of detail.


   Implementation notes:

    1. The library does not simulate sector headers and trailers.  Initialize
       and Write Full Sector commands ignore the SPD bits and the supplied
       header and trailer words.  Read Full Sector fills in the header with the
       current CHS address and sets the SPD bits to zero.  The CRC and ECC words
       in the trailer are returned as zeros.  Programs that depend on drives
       retaining the set values will fail.

    2. The library does not simulate drive hold bits or support multiple CPU
       interfaces connected to the same controller.  CPU access to a valid drive
       always succeeds.

    3. The sector buffer is an array of 16-bit elements.  Byte-oriented
       interface simulators, such as the 12821A HP-IB Disc Interface, must do
       their own byte packing and unpacking.

    4. In hardware, a command pending on an interface (CMRDY asserted) while a
       previous command is executing will be started as soon as the current
       command completes.  In simulation, dl_controller will exit when the
       current command completes and must be called again if CMRDY is asserted.
       This is necessary to allow the completion status of the prior command to
       be returned to the interface before the new command is started.
*/



#include <math.h>

#include "hp3000_defs.h"                        /* this must reflect the machine used */
#include "hp_disclib.h"



/* Program constants */

#define CNTLR_UNIT          (DL_MAXDRIVE + 1)   /* controller unit number */
#define MAX_UNIT            10                  /* last legal unit number */

#define WORDS_PER_SECTOR    128                 /* data words per sector */

#define UNTALK_DELAY        160                 /* ICD untalk delay (constant instruction count) */
#define CNTLR_TIMEOUT       S (1.74)            /* command and parameter wait timeout (1.74 seconds) */

#define NO_EVENT            -1                  /* do not schedule an event */

#define NO_ACTION           (CNTLR_IFN_IBUS) (NO_FUNCTIONS | NO_DATA)


/* Controller unit pointer */

#define CNTLR_UPTR          (cvptr->device->units + cvptr->device->numunits - 1)


/* Unit flags accessor */

#define GET_MODEL(f)        (DRIVE_TYPE) ((f) >> UNIT_MODEL_SHIFT & UNIT_MODEL_MASK)


/* Controller clear types */

typedef enum {
    Hard_Clear,                                 /* power-on/preset hard clear */
    Timeout_Clear,                              /* command or parameter timeout clear */
    Soft_Clear                                  /* programmed soft clear */
    } CNTLR_CLEAR;


/* Command accessors.

   Disc commands are passed across the data bus to the controller with the CMRDY
   flag asserted.  The commands have several forms, depending on the particular
   opcode:

       15| 14  13  12| 11  10  9 | 8   7   6 | 5   4   3 | 2   1   0    HP 1000 numbering
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     | -   -   - |  command opcode   | -   -   -   -   -   -   -   - |  form 1
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     | -   -   - |  command opcode   | -   -   -   - |  unit number  |  form 2
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     | -   -   - |  command opcode   | H | -   -   - |  unit number  |  form 3
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     | S | P | D |  command opcode   | H | -   -   - |  unit number  |  form 4
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     | -   -   - |  command opcode   |    retries    | D | S | C | A |  form 5
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     | -   -   - |  command opcode   | head  |        sector         |  form 6
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
       0 | 1   2   3 | 4   5   6 | 7   8   9 |10  11  12 |13  14  15    HP 3000 numbering

   Form 1 is used by the Address Record, Clear, End, Load TIO Register, Request
   Disc Address, and Request Syndrome commands.

   Form 2 is used by the Request Disc Sector and Request Status commands.

   Form 3 is used by the Read, Read Full Sector, Read With Offset, Read Without
   Verify, Recalibrate, Seek, Verify, Wakeup, Write, and Write Full Sector
   commands, where:

     H = hold the drive

   Form 4 is used by the Initialize Command, where:

     S = initialize the track to spare status
     P = initialize the track to protected status
     D = initialize the track to defective status
     H = hold the drive

   Form 5 is used by the Set File Mask command, where:

     D = decremental seek
     S = sparing enabled
     C = cylinder mode
     A = auto-seek enabled

   Form 6 is used by the Cold Load Read command.
*/

#define CM_OPCODE_MASK      0017400u            /* operation code mask */
#define CM_UNIT_MASK        0000017u            /* unit number mask */

#define CM_SPARE            0100000u            /* spare track */
#define CM_PROTECTED        0040000u            /* protected track */
#define CM_DEFECTIVE        0020000u            /* defective track */
#define CM_SPD_MASK         (CM_SPARE | CM_PROTECTED | CM_DEFECTIVE)

#define CM_RETRY_MASK       0000360u            /* retry count mask */
#define CM_FILE_MASK_MASK   0000017u            /* file mask mask */

#define CM_DECR_SEEK        0000010u            /* 0/1 = incremental/decremental seek */
#define CM_SPARE_EN         0000004u            /* sparing enabled */
#define CM_CYL_MODE         0000002u            /* 0/1 = surface/cylinder mode */
#define CM_AUTO_SEEK_EN     0000001u            /* auto-seek enabled */

#define CM_HEAD_MASK        0000300u            /* cold load read head mask */
#define CM_SECTOR_MASK      0000077u            /* cold load read sector mask */


#define CM_OPCODE_SHIFT     8
#define CM_UNIT_SHIFT       0

#define CM_RETRY_SHIFT      4
#define CM_FILE_MASK_SHIFT  0

#define CM_HEAD_SHIFT       6
#define CM_SECTOR_SHIFT     0


#define CM_SPD(c)           ((c) & CM_SPD_MASK)

#define CM_OPCODE(c)        (CNTLR_OPCODE) (((c) & CM_OPCODE_MASK) >> CM_OPCODE_SHIFT)

#define CM_UNIT(c)          (((c) & CM_UNIT_MASK)      >> CM_UNIT_SHIFT)

#define CM_RETRY(c)         (((c) & CM_RETRY_MASK)     >> CM_RETRY_SHIFT)
#define CM_FILE_MASK(c)     (((c) & CM_FILE_MASK_MASK) >> CM_FILE_MASK_SHIFT)

#define CM_HEAD(c)          (((c) & CM_HEAD_MASK)      >> CM_HEAD_SHIFT)
#define CM_SECTOR(c)        (((c) & CM_SECTOR_MASK)    >> CM_SECTOR_SHIFT)


static const BITSET_NAME file_mask_names [] = {     /* File mask word */
    "\1decremental seek\0incremental seek",         /* bit  3/12 */
    "sparing",                                      /* bit  2/13 */
    "\1cylinder mode\0surface mode",                /* bit  1/14 */
    "autoseek"                                      /* bit  0/15 */
    };

static const BITSET_FORMAT file_mask_format =       /* names, offset, direction, alternates, bar */
    { FMT_INIT (file_mask_names, 0, msb_first, has_alt, append_bar) };


/* Parameter accessors.

   Parameters are passed across the data bus to the controller with the DTRDY
   flag asserted.  The parameters have several forms, depending on the commands
   requesting them:

       15| 14  13  12| 11  10  9 | 8   7   6 | 5   4   3 | 2   1   0    HP 1000 numbering
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     |                           cylinder                            |  form 1  (in/out)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     |     0     |       head        |            sector             |  form 2  (in/out)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     | S | P | D |    status code    | -   -   -   - |  unit number  |  form 3  (out)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     | E | -   - |   drive type  | - | A | R | F | L | S | K | N | B |  form 4  (out)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     |               0               |            sector             |  form 5  (out)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     |                         sector count                          |  form 6  (in)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     |                         displacement                          |  form 7  (out)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     |                            pattern                            |  form 8  (out)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     |                           data word                           |  form 9  (in)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     |                       | A | D | S | - | cyl offset magnitude  |  form 10 (in)
     +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
       0 | 1   2   3 | 4   5   6 | 7   8   9 |10  11  12 |13  14  15    HP 3000 numbering

   Forms 1 and 2 are used by the Address Record, Request Disc Address, Request
   Syndrome, and Seek commands.

   Form 3 is used by the Request Status and Request Syndrome commands, where:

     S = last track was a spare
     P = last track was protected
     D = last track was defective

   Form 4 is used by the Request Status command, where:

     E = error present
     A = attention
     R = read-only
     F = format enabled
     L = drive fault
     S = first status
     K = seek check
     N = not ready
     B = drive busy

   Form 5 is used by the Request Sector Address command.

   Form 6 is used by the Verify command.

   Forms 7 and 8 are used by the Request Syndrome command.

   Form 9 is used by the Load TIO Register command.

   Form 10 is used by the Read With Offset command, where:

     A = advance the clock (valid for 13037A only)
     D = delay the clock (valid for 13037A only)
     S = sign of cylinder offset
*/

#define S1_SPARE            0100000u            /* spare track */
#define S1_PROTECTED        0040000u            /* protected track */
#define S1_DEFECTIVE        0020000u            /* defective track */
#define S1_STATUS_MASK      0017400u            /* encoded termination status mask */
#define S1_UNIT_MASK        0000017u            /* unit number mask */

#define S1_STATUS_SHIFT     8
#define S1_UNIT_SHIFT       0

#define S1_STATUS(n)        ((n) << S1_STATUS_SHIFT & S1_STATUS_MASK)
#define S1_UNIT(n)          ((n) << S1_UNIT_SHIFT   & S1_UNIT_MASK)


#define S2_ERROR            0100000u            /* any error */
#define S2_DRIVE_TYPE_MASK  0017000u            /* drive type mask */
#define S2_ATTENTION        0000200u            /* attention */
#define S2_READ_ONLY        0000100u            /* read-only */
#define S2_FORMAT_EN        0000040u            /* format enabled */
#define S2_FAULT            0000020u            /* drive fault */
#define S2_FIRST_STATUS     0000010u            /* first status */
#define S2_SEEK_CHECK       0000004u            /* seek check */
#define S2_NOT_READY        0000002u            /* not ready */
#define S2_BUSY             0000001u            /* drive busy */

#define S2_STOPS            (S2_FAULT \
                               | S2_SEEK_CHECK \
                               | S2_NOT_READY)  /* bits that stop drive access */

#define S2_ERRORS           (S2_FAULT \
                               | S2_SEEK_CHECK \
                               | S2_NOT_READY \
                               | S2_BUSY)       /* bits that set S2_ERROR */

#define S2_CPS              (S2_ATTENTION \
                               | S2_FAULT \
                               | S2_FIRST_STATUS \
                               | S2_SEEK_CHECK) /* bits that are cleared by Controller Preset */

#define S2_DRIVE_TYPE_SHIFT 9

#define S2_DRIVE_TYPE(n)    ((n) << S2_DRIVE_TYPE_SHIFT & S2_DRIVE_TYPE_MASK)
#define S2_TO_DRIVE_TYPE(n) (((n) & S2_DRIVE_TYPE_MASK) >> S2_DRIVE_TYPE_SHIFT)


#define PIO_HEAD_MASK       0017400u            /* head mask */
#define PIO_SECTOR_MASK     0000377u            /* sector mask */

#define PI_ADV_CLOCK        0001000u            /* advance clock */
#define PI_DEL_CLOCK        0000400u            /* delay clock */
#define PI_NEG_OFFSET       0000200u            /* 0/1 = positive/negative cylinder offset sign */
#define PI_OFFSET_MASK      0000077u            /* cylinder offset mask */


#define PIO_HEAD_SHIFT      8
#define PIO_SECTOR_SHIFT    0

#define PI_OFFSET_SHIFT     0


#define PI_HEAD(p)          (((p) & PIO_HEAD_MASK)   >> PIO_HEAD_SHIFT)
#define PI_SECTOR(p)        (((p) & PIO_SECTOR_MASK) >> PIO_SECTOR_SHIFT)
#define PI_OFFSET(p)        (((p) & PI_OFFSET_MASK)  >> PI_OFFSET_SHIFT)

#define PO_HEAD(n)          (((n) << PIO_HEAD_SHIFT   & PIO_HEAD_MASK))
#define PO_SECTOR(n)        (((n) << PIO_SECTOR_SHIFT & PIO_SECTOR_MASK))


static const BITSET_NAME status_1_names [] = {  /* Status-1 word */
    "spare",                                    /* bit 15/0 */
    "protected",                                /* bit 14/1 */
    "defective"                                 /* bit 13/2 */
    };

static const BITSET_FORMAT status_1_format =    /* names, offset, direction, alternates, bar */
    { FMT_INIT (status_1_names, 13, msb_first, no_alt, append_bar) };

static const BITSET_FORMAT initialize_format =  /* names, offset, direction, alternates, bar */
    { FMT_INIT (status_1_names, 13, msb_first, no_alt, no_bar) };


static const BITSET_NAME status_2_names [] = {  /* Status-2 word */
    "attention",                                /* bit  7/ 8 */
    "read only",                                /* bit  6/ 9 */
    "format enabled",                           /* bit  5/10 */
    "fault",                                    /* bit  4/11 */
    "first status",                             /* bit  3/12 */
    "seek check",                               /* bit  2/13 */
    "not ready",                                /* bit  1/14 */
    "busy"                                      /* bit  0/15 */
    };

static const BITSET_FORMAT status_2_format =    /* names, offset, direction, alternates, bar */
    { FMT_INIT (status_2_names, 0, msb_first, no_alt, no_bar) };


static const BITSET_NAME offset_names [] = {    /* Read With Offset parameter */
    "advanced clock",                           /* bit  9/ 6 */
    "delayed clock"                             /* bit  8/ 7 */
    };

static const BITSET_FORMAT offset_format =      /* names, offset, direction, alternates, bar */
    { FMT_INIT (offset_names, 8, msb_first, no_alt, append_bar) };


/* Drive properties table.

   In hardware, drives report their drive type numbers to the controller upon
   receipt of a Request Status tag bus command.  The drive type is used to
   determine the legal range of head and sector addresses (the drive itself will
   validate the cylinder address during a Seek command and the head/sector
   address during an Address Record drive command).

   In simulation, the model ID number from the unit flags is used as an index
   into the drive properties table.  The table is used to validate seek
   parameters and to provide the mapping between CHS addresses and the linear
   byte addresses required by the host file access routines.

   The 7905/06(H) drives consist of removable and fixed platters, whereas the
   7920(H)/25(H) drives have only removable multi-platter packs.  As a result,
   7905/06 drives are almost always accessed in platter mode, i.e., a given
   logical disc area is fully contained on either the removable or fixed
   platter, whereas the 7920/25 drives are almost always accessed in cylinder
   mode with logical disc areas spanning some or all of the platters.

   Disc image files are arranged as a linear set of tracks.  To improve
   locality of access, tracks in the 7905/06 images are grouped per-platter,
   whereas tracks on the 7920 and 7925 are sequential by cylinder and head
   number.

   The simulator maps the tracks on the 7905/06 removable platter (heads 0 and
   1) to the first half of the disc image, and the tracks on the fixed platter
   (heads 2 and, for the 7906 only, 3) to the second half of the image.  For the
   7906(H), the cylinder-head order of the tracks is 0-0, 0-1, 1-0, 1-1, ...,
   410-0, 410-1, 0-2, 0-3, 1-2, 1-3, ..., 410-2, 410-3.  The 7905 order is the
   same, except that head 3 tracks are omitted.

   For the 7920(H)/25(H), all tracks appear in cylinder-head order, e.g., 0-0,
   0-1, 0-2, 0-3, 0-4, 1-0, 1-1, ..., 822-2, 822-3, 822-4 for the 7920(H).

   This variable-access geometry is accomplished by defining separate "heads per
   cylinder" values for the fixed and removable sections of each drive that
   indicates the number of heads that should be grouped for locality.  The
   removable values are set to 2 on the 7905 and 7906, indicating that those
   drives typically use cylinders consisting of two heads.  They are set to the
   number of heads per drive for the 7920 and 7925, as those typically use
   cylinders encompassing the entire pack.

   The Drive Type is reported by the controller in the second status word
   (Status-2) returned by the Request Status command.
*/

typedef struct {
    const char  *name;                          /* drive name */
    uint32      sectors;                        /* sectors per head */
    uint32      heads;                          /* heads per cylinder*/
    uint32      cylinders;                      /* cylinders per drive */
    uint32      words;                          /* words per drive */
    uint32      remov_heads;                    /* number of removable-platter heads */
    uint32      fixed_heads;                    /* number of fixed-platter heads */
    } DRIVE_PROPS;

static const DRIVE_PROPS drive_props [] = {     /* indexed by DRIVE_TYPE */
   /*  drive   sectors    heads   cylinders      words     remov  fixed */
   /*  name    per trk   per cyl  per drive    per drive   heads  heads */
   /* -------  -------   -------  ----------  -----------  -----  ----- */
    { "7906",    48,       4,        411,     WORDS_7906,    2,     2   },     /* drive type 0 */
    { "7920",    48,       5,        823,     WORDS_7920,    5,     0   },     /* drive type 1 */
    { "7905",    48,       3,        411,     WORDS_7905,    2,     1   },     /* drive type 2 */
    { "7925",    64,       9,        823,     WORDS_7925,    9,     0   }      /* drive type 3 */
    };


/* Delay properties table.

   To support the realistic timing mode, the delay properties table contains
   timing specifications for the supported disc 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.

   The drive type field differentiates between drive models available on a given
   controller.  The field is not significant for MAC and ICD controllers because
   all of the drives supported have the same specifications.

   The controller overhead values are estimates; they do not appear to be
   documented for MAC and ICD controllers, although they are published for
   various CS/80 drives.
*/

static const DELAY_PROPS real_times [] = {
   /* cntlr   drive     seek     seek       sector        data     intersector   cntlr   */
   /* type     type    trk-trk   full      rotation     per word       gap      overhead */
   /* -----  --------  -------  --------  -----------  ----------  -----------  -------- */
    { MAC,   HP_All,   mS (5),  mS (45),  uS (347.2),  uS (2.13),  uS (27.2),   uS (200) },
    { ICD,   HP_All,   mS (5),  mS (45),  uS (347.2),  uS (2.13),  uS (27.2),   mS (1.5) }
    };

#define DELAY_COUNT         (sizeof real_times / sizeof real_times [0])


/* Estimate the current sector.

   The sector currently passing under the disc heads is estimated from the
   current "simulation time," which is the number of event ticks since the
   simulation run was started, and the simulated disc rotation time.  The
   computation logic is:

     current_sector := (simulator_time / per_sector_time) MOD sectors_per_track;
*/

#define CURRENT_SECTOR(cvptr,uptr) \
            (uint32) fmod (sim_gtime () / cvptr->dlyptr->sector_full, \
                           drive_props [GET_MODEL (uptr->flags)].sectors)


/* Command properties table.

   The validity of each command for a given controller type is checked against
   the command properties table when it is prepared for execution.  The table
   also includes the count of inbound or outbound parameters, the class of the
   command, and flags that indicate certain common actions that are be taken.


   Implementation notes:

    1. The verify field of the Read_Without_Verify property record is set to
       TRUE so that address verification will be done if a track boundary is
       crossed.  Initial verification is suppressed by setting the controller's
       verify field to FALSE during initial Read_Without_Verify processing.
*/

typedef struct {
    uint32       param_count;                   /* count of input or output parameters */
    CNTLR_CLASS  classification;                /* command classification */
    t_bool       valid [CNTLR_COUNT];           /* command validity, indexed by CNTLR_TYPE */
    t_bool       clear_status;                  /* command clears the controller status */
    t_bool       unit_field;                    /* command has a unit field */
    t_bool       unit_check;                    /* command checks the unit number validity */
    t_bool       unit_access;                   /* command accesses the drive unit */
    t_bool       seek_wait;                     /* command waits for seek completion */
    t_bool       verify_address;                /* command does address verification */
    t_bool       idle_at_end;                   /* command idles the controller at completion */
    uint32       preamble_size;                 /* size of preamble in words */
    uint32       transfer_size;                 /* size of data transfer in words */
    uint32       postamble_size;                /* size of postamble in words */
    } COMMAND_PROPERTIES;

typedef const COMMAND_PROPERTIES *PRPTR;

#define T   TRUE
#define F   FALSE

static const COMMAND_PROPERTIES cmd_props [] = {
/*   parm      opcode        valid for    clr  unit unit unit seek addr end  pre  xfer post   */
/*   I/O   classification   MAC ICD CS80  stat fld  chk  acc  wait verf idle size size size   */
/*   ----  --------------   --- --- ----  ---- ---- ---- ---- ---- ---- ---- ---- ---- ----   */
    {  0,  Class_Read,     { T,  T,  F },  T,   F,   T,   T,   F,   T,   F,   15, 128,  7  }, /* 00 = Cold_Load_Read */
    {  0,  Class_Control,  { T,  T,  F },  T,   T,   T,   T,   T,   F,   T,    0,  0,   0  }, /* 01 = Recalibrate */
    {  2,  Class_Control,  { T,  T,  F },  T,   T,   T,   T,   F,   F,   T,    0,  0,   0  }, /* 02 = Seek */
    {  2,  Class_Status,   { T,  T,  F },  F,   T,   F,   F,   F,   F,   F,    0,  0,   0  }, /* 03 = Request_Status */
    {  1,  Class_Status,   { T,  T,  F },  T,   T,   T,   T,   F,   F,   F,    0,  0,   0  }, /* 04 = Request_Sector_Address */
    {  0,  Class_Read,     { T,  T,  F },  T,   T,   T,   T,   T,   T,   F,   15, 128,  7  }, /* 05 = Read */
    {  0,  Class_Read,     { T,  T,  F },  T,   T,   T,   T,   T,   F,   F,   12, 138,  0  }, /* 06 = Read_Full_Sector */
    {  1,  Class_Read,     { T,  T,  F },  T,   T,   T,   T,   T,   T,   F,    0,  0,   0  }, /* 07 = Verify */
    {  0,  Class_Write,    { T,  T,  F },  T,   T,   T,   T,   T,   T,   F,   15, 128,  7  }, /* 10 = Write */
    {  0,  Class_Write,    { T,  T,  F },  T,   T,   T,   T,   T,   F,   F,   12, 138,  0  }, /* 11 = Write_Full_Sector */
    {  0,  Class_Control,  { T,  T,  F },  T,   F,   F,   F,   F,   F,   F,    0,  0,   0  }, /* 12 = Clear */
    {  0,  Class_Write,    { T,  T,  F },  T,   T,   T,   T,   T,   F,   F,   15, 128,  7  }, /* 13 = Initialize */
    {  2,  Class_Control,  { T,  T,  F },  T,   F,   F,   F,   F,   F,   F,    0,  0,   0  }, /* 14 = Address_Record */
    {  7,  Class_Status,   { T,  F,  F },  T,   F,   F,   F,   F,   F,   F,    0,  0,   0  }, /* 15 = Request_Syndrome */
    {  1,  Class_Read,     { T,  T,  F },  T,   T,   T,   T,   T,   T,   F,   15, 128,  7  }, /* 16 = Read_With_Offset */
    {  0,  Class_Control,  { T,  T,  F },  T,   F,   F,   F,   F,   F,   F,    0,  0,   0  }, /* 17 = Set_File_Mask */
    {  0,  Class_Invalid,  { F,  F,  F },  T,   F,   F,   F,   F,   F,   F,    0,  0,   0  }, /* 20 = Invalid_Opcode */
    {  0,  Class_Invalid,  { F,  F,  F },  T,   F,   F,   F,   F,   F,   F,    0,  0,   0  }, /* 21 = Invalid_Opcode */
    {  0,  Class_Read,     { T,  T,  F },  T,   T,   T,   T,   T,   T,   F,   15, 128,  7  }, /* 22 = Read_Without_Verify */
    {  1,  Class_Status,   { T,  F,  F },  T,   F,   F,   F,   F,   F,   F,    0,  0,   0  }, /* 23 = Load_TIO_Register */
    {  2,  Class_Status,   { T,  T,  F },  F,   F,   F,   F,   F,   F,   F,    0,  0,   0  }, /* 24 = Request_Disc_Address */
    {  0,  Class_Control,  { T,  T,  F },  T,   F,   F,   F,   F,   F,   T,    0,  0,   0  }, /* 25 = End */
    {  0,  Class_Control,  { T,  F,  F },  T,   T,   T,   F,   F,   F,   F,    0,  0,   0  }  /* 26 = Wakeup */
    };


/* Command functions table.

   At each phase of command execution, the controller may return zero or more
   functions for the interface to perform.  The functions control the transfer
   of parameters and data to and from the CPU.  Note that commands do not
   necessarily use all available phases.


   Implementation notes:

    1. Commands usually return BUSY and IFGTC functions to begin.  However, the
       Clear and Set File Mask commands delay the IFGTC function, which requests
       channel service, to the End Phase to ensure that the WRTIO function
       completes before the channel program continues.  This ensures that an End
       I/O order will pick up the correct status value from the interface.

    2. Invalid commands have WRTIO in their Idle_Phase entries because the
       diagnostic expects to see Illegal_Opcode status immediately after the SIO
       program ends.
*/

typedef CNTLR_IFN IFN_ARRAY [7];

static const IFN_ARRAY cmd_functions [] = {     /* indexed by CNTLR_OPCODE */
                                                /* 00 = Cold_Load_Read */
    { BUSY | SRTRY | IFGTC,                     /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      IFIN,                                     /*   Data Phase */
      0,                                        /*   Intersector Phase */
      STDFL | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 01 = Recalibrate */
    { BUSY | IFGTC,                             /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      WRTIO | FREE },                           /*   End Phase */

                                                /* 02 = Seek */
    { BUSY  | IFGTC | STDFL,                    /*   Idle Phase */
      IFOUT | STDFL,                            /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      IFOUT | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 03 = Request_Status */
    { BUSY | IFGTC,                             /*   Idle Phase */
      IFIN | STDFL,                             /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      WRTIO | FREE | RQSRV },                   /*   End Phase */

                                                /* 04 = Request_Sector_Address */
    { BUSY | IFGTC,                             /*   Idle Phase */
      IFIN | STDFL,                             /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      WRTIO | RQSRV | FREE },                   /*   End Phase */

                                                /* 05 = Read */
    { BUSY | IFGTC,                             /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      IFIN,                                     /*   Data Phase */
      0,                                        /*   Intersector Phase */
      STDFL | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 06 = Read_Full_Sector */
    { BUSY | IFGTC,                             /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      IFIN,                                     /*   Data Phase */
      0,                                        /*   Intersector Phase */
      STDFL | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 07 = Verify */
    { BUSY | IFGTC | STDFL,                     /*   Idle Phase */
      IFOUT,                                    /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      STDFL | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 10 = Write */
    { BUSY | IFGTC,                             /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      IFOUT,                                    /*   Data Phase */
      0,                                        /*   Intersector Phase */
      STDFL | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 11 = Write_Full_Sector */
    { BUSY | IFGTC,                             /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      IFOUT,                                    /*   Data Phase */
      0,                                        /*   Intersector Phase */
      STDFL | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 12 = Clear */
    { BUSY,                                     /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      IFGTC | WRTIO | STDFL | FREE },           /*   End Phase */

                                                /* 13 = Initialize */
    { BUSY | IFGTC,                             /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      IFOUT,                                    /*   Data Phase */
      0,                                        /*   Intersector Phase */
      STDFL | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 14 = Address_Record */
    { BUSY  | IFGTC | STDFL,                    /*   Idle Phase */
      IFOUT | STDFL,                            /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      IFOUT | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 15 = Request_Syndrome */
    { BUSY | IFGTC,                             /*   Idle Phase */
      IFIN | STDFL,                             /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      WRTIO | RQSRV | FREE },                   /*   End Phase */

                                                /* 16 = Read_With_Offset */
    { BUSY  | IFGTC | STDFL,                    /*   Idle Phase */
      IFOUT,                                    /*   Parameter Phase */
      RQSRV | STDFL,                            /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      IFIN,                                     /*   Data Phase */
      0,                                        /*   Intersector Phase */
      STDFL | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 17 = Set_File_Mask */
    { BUSY | SRTRY,                             /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      IFGTC | WRTIO | STDFL | FREE },           /*   End Phase */

                                                /* 20 = Invalid_Opcode */
    { BUSY | IFGTC | WRTIO,                     /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      FREE },                                   /*   End Phase */

                                                /* 21 = Invalid_Opcode */
    { BUSY | IFGTC | WRTIO,                     /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      FREE },                                   /*   End Phase */

                                                /* 22 = Read_Without_Verify */
    { BUSY | IFGTC,                             /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      IFIN,                                     /*   Data Phase */
      0,                                        /*   Intersector Phase */
      STDFL | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 23 = Load_TIO_Register */
    { BUSY  | IFGTC | STDFL,                    /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      IFOUT | WRTIO | RQSRV | FREE },           /*   End Phase */

                                                /* 24 = Request_Disc_Address */
    { BUSY | IFGTC,                             /*   Idle Phase */
      IFIN | STDFL,                             /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      WRTIO | RQSRV | FREE },                   /*   End Phase */

                                                /* 25 = End */
    { IFGTC,                                    /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      0 },                                      /*   End Phase */

                                                /* 26 = Wakeup */
    { BUSY | IFGTC,                             /*   Idle Phase */
      0,                                        /*   Parameter Phase */
      0,                                        /*   Seek Phase */
      0,                                        /*   Rotate Phase */
      0,                                        /*   Data Phase */
      0,                                        /*   Intersector Phase */
      WRTIO | STDFL | FREE }                    /*   End Phase */
    };


/* Status functions table.

   The End Phase functions above are proper for commands that complete normally.
   Commands that return an error status return additional functions that depend
   on the particular status code.  An entry in this table is ORed with the End
   Phase function to produce the final function set that is returned to the
   interface.
*/

static const CNTLR_IFN status_functions [] = {  /* indexed by CNTLR_STATUS */
    0,                                          /*   000 = Normal Completion */
    STINT | WRTIO | FREE,                       /*   001 = Illegal Opcode */
    STDFL | WRTIO | FREE,                       /*   002 = Unit Available */
    STINT | WRTIO | FREE,                       /*   003 = Illegal Drive Type */
    0,                                          /*   004 = (undefined) */
    0,                                          /*   005 = (undefined) */
    0,                                          /*   006 = (undefined) */
    STINT | WRTIO | FREE,                       /*   007 = Cylinder Miscompare */
    DVEND | RQSRV | WRTIO | FREE,               /*   010 = Uncorrectable Data Error */
    STINT | WRTIO | FREE,                       /*   011 = Head-Sector Miscompare */
    STINT | WRTIO | FREE,                       /*   012 = I/O Program Error */
    DVEND | RQSRV | WRTIO | FREE,               /*   013 = Sync Timeout */
    STINT | WRTIO | FREE,                       /*   014 = End of Cylinder */
    0,                                          /*   015 = (undefined) */
    DVEND | RQSRV | WRTIO | FREE,               /*   016 = Data Overrun */
    DVEND | RQSRV | WRTIO | FREE,               /*   017 = Correctable Data Error */
    STINT | WRTIO | FREE,                       /*   020 = Illegal Spare Access */
    STINT | WRTIO | FREE,                       /*   021 = Defective Track */
    STINT | WRTIO | FREE,                       /*   022 = Access Not Ready */
    STINT | WRTIO | FREE,                       /*   023 = Status-2 Error */
    0,                                          /*   024 = (undefined) */
    0,                                          /*   025 = (undefined) */
    STINT | WRTIO | FREE,                       /*   026 = Protected Track */
    STINT | WRTIO | FREE,                       /*   027 = Unit Unavailable */
    0,                                          /*   030 = (undefined) */
    0,                                          /*   031 = (undefined) */
    0,                                          /*   032 = (undefined) */
    0,                                          /*   033 = (undefined) */
    0,                                          /*   034 = (undefined) */
    0,                                          /*   035 = (undefined) */
    0,                                          /*   036 = (undefined) */
    STINT | WRTIO | FREE                        /*   037 = Drive Attention */
    };


/* Controller operation names */

static const BITSET_NAME flag_names [] = {      /* controller flag names, in CNTLR_FLAG order */
    "CLEAR",                                    /* 000001 */
    "CMRDY",                                    /* 000002 */
    "DTRDY",                                    /* 000004 */
    "EOD",                                      /* 000010 */
    "INTOK",                                    /* 000020 */
    "OVRUN",                                    /* 000040 */
    "XFRNG"                                     /* 000100 */
    };

static const BITSET_FORMAT flag_format =        /* names, offset, direction, alternates, bar */
    { FMT_INIT (flag_names, 0, lsb_first, no_alt, no_bar) };


static const BITSET_NAME function_names [] = {  /* interface function names, in CNTLR_IFN order */
    "BUSY",                                     /* 000000200000 */
    "DSCIF",                                    /* 000000400000 */
    "SELIF",                                    /* 000001000000 */
    "IFIN",                                     /* 000002000000 */
    "IFOUT",                                    /* 000004000000 */
    "IFGTC",                                    /* 000010000000 */
    "IFPRF",                                    /* 000020000000 */
    "RQSRV",                                    /* 000040000000 */
    "DVEND",                                    /* 000100000000 */
    "SRTRY",                                    /* 000200000000 */
    "STDFL",                                    /* 000400000000 */
    "STINT",                                    /* 001000000000 */
    "WRTIO",                                    /* 002000000000 */
    "FREE"                                      /* 004000000000 */
    };

static const BITSET_FORMAT function_format =    /* names, offset, direction, alternates, bar */
    { FMT_INIT (function_names, 16, lsb_first, no_alt, no_bar) };


static const char invalid_name [] = "Invalid";

static const char *opcode_name [] = {           /* command opcode names, in CNTLR_OPCODE order */
    "Cold Load Read",                           /*   00 */
    "Recalibrate",                              /*   01 */
    "Seek",                                     /*   02 */
    "Request Status",                           /*   03 */
    "Request Sector Address",                   /*   04 */
    "Read",                                     /*   05 */
    "Read Full Sector",                         /*   06 */
    "Verify",                                   /*   07 */
    "Write",                                    /*   10 */
    "Write Full Sector",                        /*   11 */
    "Clear",                                    /*   12 */
    "Initialize",                               /*   13 */
    "Address Record",                           /*   14 */
    "Request Syndrome",                         /*   15 */
    "Read With Offset",                         /*   16 */
    "Set File Mask",                            /*   17 */
    invalid_name,                               /*   20 = invalid */
    invalid_name,                               /*   21 = invalid */
    "Read Without Verify",                      /*   22 */
    "Load TIO Register",                        /*   23 */
    "Request Disc Address",                     /*   24 */
    "End",                                      /*   25 */
    "Wakeup"                                    /*   26 */
    };

#define OPCODE_LENGTH       22                  /* length of the longest opcode name */


static const char *const status_name [] = {     /* command status names, in CNTLR_STATUS order */
    "Normal Completion",                        /*   000 */
    "Illegal Opcode",                           /*   001 */
    "Unit Available",                           /*   002 */
    "Illegal Drive Type",                       /*   003 */
    NULL,                                       /*   004 */
    NULL,                                       /*   005 */
    NULL,                                       /*   006 */
    "Cylinder Miscompare",                      /*   007 */
    "Uncorrectable Data Error",                 /*   010 */
    "Head-Sector Miscompare",                   /*   011 */
    "I/O Program Error",                        /*   012 */
    "Sync Timeout",                             /*   013 */
    "End of Cylinder",                          /*   014 */
    NULL,                                       /*   015 */
    "Data Overrun",                             /*   016 */
    "Correctable Data Error",                   /*   017 */
    "Illegal Spare Access",                     /*   020 */
    "Defective Track",                          /*   021 */
    "Access Not Ready",                         /*   022 */
    "Status-2 Error",                           /*   023 */
    NULL,                                       /*   024 */
    NULL,                                       /*   025 */
    "Protected Track",                          /*   026 */
    "Unit Unavailable",                         /*   027 */
    NULL,                                       /*   030 */
    NULL,                                       /*   031 */
    NULL,                                       /*   032 */
    NULL,                                       /*   033 */
    NULL,                                       /*   034 */
    NULL,                                       /*   035 */
    NULL,                                       /*   036 */
    "Drive Attention"                           /*   037 */
    };

#define STATUS_LENGTH       24                  /* length of the longest status name */


static const char *state_name [] = {            /* controller state names, in CNTLR_STATE order */
    "idle",
    "wait",
    "busy"
    };


static const char *phase_name [] = {            /* unit state names, in CNTLR_PHASE order */
    "idle",
    "parameter",
    "seek",
    "rotate",
    "data",
    "intersector",
    "end"
    };


/* Disc library local controller routines */

static CNTLR_IFN_IBUS start_command    (CVPTR cvptr, CNTLR_FLAG_SET flags, CNTLR_IBUS data);
static CNTLR_IFN_IBUS continue_command (CVPTR cvptr, UNIT *uptr, CNTLR_FLAG_SET flags, CNTLR_IBUS data);
static CNTLR_IFN_IBUS poll_drives      (CVPTR cvptr);

static void   end_command      (CVPTR cvptr, UNIT *uptr, CNTLR_STATUS status);
static t_bool start_seek       (CVPTR cvptr, UNIT *uptr);
static t_bool start_read       (CVPTR cvptr, UNIT *uptr, CNTLR_FLAG_SET flags);
static void   end_read         (CVPTR cvptr, UNIT *uptr, CNTLR_FLAG_SET flags);
static t_bool start_write      (CVPTR cvptr, UNIT *uptr);
static void   end_write        (CVPTR cvptr, UNIT *uptr, CNTLR_FLAG_SET flags);
static t_bool position_sector  (CVPTR cvptr, UNIT *uptr);
static void   next_sector      (CVPTR cvptr, UNIT *uptr);
static void   io_error         (CVPTR cvptr, UNIT *uptr, CNTLR_STATUS status);
static void   set_completion   (CVPTR cvptr, UNIT *uptr, CNTLR_STATUS status);
static void   clear_controller (CVPTR cvptr, CNTLR_CLEAR clear_type);
static void   idle_controller  (CVPTR cvptr);

/* Disc library local utility routines */

static void    set_address   (CVPTR cvptr, uint32    index);
static void    wait_timer    (CVPTR cvptr, FLIP_FLOP action);
static HP_WORD drive_status  (UNIT  *uptr);
static t_stat  activate_unit (CVPTR cvptr, UNIT *uptr);
static void    set_rotation  (CVPTR cvptr, UNIT *uptr);
static void    set_file_pos  (CVPTR cvptr, UNIT *uptr, uint32 model);



/* Disc library global controller routines */


/* Disc controller interface.

   This routine simulates the hardware interconnection between the disc
   controller and the CPU interface.  This routine is called whenever the flag
   state changes.  This would be when a new command is to be started, when
   command parameters are supplied or status words are retrieved, and when
   sector data is read or written.  It must also be called when the unit service
   routine is entered.  The caller passes in the set of interface flags and the
   contents of the data buffer.  The routine returns a set of functions and, if
   IFIN is included in set, the new content of the data buffer.

   In hardware, the controller microcode executes in one of three states:

    1. In the Poll Loop, which looks for commands and drive attention requests.

       In each pass of the loop, the next CPU interface in turn is selected and
       checked for a command; if present, it is executed.  If not, the
       interface is disconnected, and then all drives are checked in turn until
       one is found with Attention status; if none are found, the loop
       continues.  If a drive is requesting attention, the associated CPU
       interface is selected to check for a command; if present, it is executed.
       If not, and the interface allows interrupts, an interrupt request is made
       and the Command Wait Loop is entered.  If interrupts are not allowed, the
       interface is disconnected, and the Poll Loop continues.

    2. In the Command Wait Loop, which looks for commands.

       In each pass of the loop, the currently selected CPU interface is checked
       for a command; if present, it is executed.  If not, the Command Wait Loop
       continues.  While in the loop, a 1.74 second timer is running.  If it
       expires before a command is received, the file mask is reset, the current
       interface is disconnected, and the Poll Loop is entered.

    3. In command execution, which processes the current command.

       While a command is executing, any waits for input parameters, seek
       completion, data transfers, or output status words are handled
       internally.  Each wait is governed by the 1.74 second timer; if it
       expires, the command is aborted, the file mask is reset, the current
       interface is disconnected, and the Poll Loop is reentered.

   In simulation, these states are represented by the CNTLR_STATE values
   Idle_State, Wait_State, and Busy_State, respectively.

   A MAC controller operates from one to eight drives, represented by an array
   of one to eight UNITs.  The unit number present in the command is used to
   index to the target unit via the "units" pointer in the DEVICE structure.
   One additional unit that represents the controller is required, separate from
   the individual drive units.  Commands that do not access the drive, such as
   Address Record, are scheduled on the controller unit to allow controller
   commands to execute while drive units are seeking.  The command wait timer is
   also scheduled on the controller unit to limit the amount of time the
   controller will wait for the interface to supply a command or parameter.

   An ICD simulation manages a single unit corresponding to the drive in which
   the controller is integrated.  A device interface declares a UNIT array
   corresponding to the number of drives supported and passes the unit number to
   use during controller initialization.  A controller unit is not used, as all
   commands are scheduled on the drive unit associated with a given controller.

   On entry, the flags and controller state are examined to determine if a new
   controller command should be initiated or the current command should be
   continued.  If this routine is being called as a result of an event service,
   "uptr" will point at the unit being serviced.  Otherwise, "uptr" will be NULL
   (for example, when starting a new controller command).

   If the CLEARF flag is asserted, then perform a hard clear on the controller.
   Otherwise, if a 3000 channel error has occurred, then terminate any command
   in progress and return I/O Program Error status.  Otherwise, if the
   controller is currently busy with a command, or if this is an event service
   entry, then process the next step of the command.  Otherwise, if the CMRDY
   flag is asserted, then start a new command.  If none of these cases pertain,
   or if the controller is now idle, poll the drives for attention.

   In all cases, return a combined function set and outbound data word to the
   caller.


   Implementation notes:

    1. This routine will be entered when the drive unit event service occurs
       for seek completion, and Seek_Phase processing in continue_command will
       set the drive's Attention bit.  The drives must then be polled and the
       return functions and TIO value set to generate a CPU interrupt.

       A seek command started on one drive while a second drive already has its
       Attention bit set would seem to overwrite the first drive's Seek
       completion status with the second drive's Attention status.  However,
       this won't occur because INTOK will not be set until the first drive's
       channel program completes, and so the drive poll is inhibited until then.
*/

CNTLR_IFN_IBUS dl_controller (CVPTR cvptr, UNIT *uptr, CNTLR_FLAG_SET flags, CNTLR_IBUS data)
{
CNTLR_IFN_IBUS outbound;

tpprintf (cvptr->device, DL_DEB_IOB, "Controller (%s) received data %06o with flags %s\n",
          state_name [cvptr->state], data, fmt_bitset (flags, flag_format));

if (flags & CLEARF) {                                   /* if the CLEAR flag is asserted */
    clear_controller (cvptr, Hard_Clear);               /*   then perform a hard clear on the controller */
    outbound = NO_ACTION;                               /*     and take no other action on return */

    tpprintf (cvptr->device, DL_DEB_CMD, "Hard clear\n");
    }

else if (flags & XFRNG) {                               /* otherwise if a channel error has occurred */
    end_command (cvptr, uptr, IO_Program_Error);        /*   then terminate the command with error status */
    cvptr->spd_unit = 0;                                /*     and clear the SPD and unit parts of the status */

    outbound = status_functions [IO_Program_Error]      /* set the Status-1 value for WRTIO */
                 | S1_STATUS (IO_Program_Error);
    }

else if (uptr || cvptr->state == Busy_State)                /* otherwise if a command is in process */
    outbound = continue_command (cvptr, uptr, flags, data); /*   then continue with command processing */

else if (flags & CMRDY)                                 /* otherwise if a new command is ready */
    outbound = start_command (cvptr, flags, data);      /*   then begin command execution */

else                                                    /* otherwise there's nothing to do */
    outbound = NO_ACTION;                               /*   except possibly poll for attention */

if (cvptr->state == Idle_State                          /* if the controller is idle */
  && cvptr->type == MAC                                 /*   and it's a MAC controller */
  && flags & INTOK)                                     /*     and interrupts are allowed */
    outbound = poll_drives (cvptr);                     /*       then poll the drives for attention */

tpprintf (cvptr->device, DL_DEB_IOB, "Controller (%s) returned data %06o with functions %s\n",
          state_name [cvptr->state], DLIBUS (outbound),
          fmt_bitset (DLIFN (outbound), function_format));

return outbound;
}


/* Start a new command.

   This routine simulates the controller microcode entry into the command
   executor corresponding to the command presented by the CPU interface.  It's
   called when the controller is waiting for a command and the interface asserts
   CMRDY to indicate that a new command is available.  It returns a set of
   action functions and a data word to the caller.  For a good command, it also
   sets up the next phase of operation on the controller and/or drive unit and
   schedules the unit(s) as appropriate.

   On entry, the command word is supplied in the "inbound_data" parameter; this
   simulates the microcode issuing an IFPRF (Interface Prefetch) to obtain the
   command.  The opcode is isolated from the command word and checked for
   validity.  If it's OK, it's used as an index into the command properties
   table.  If the command contains a unit number field, it is extracted, checked
   for validity, and used to derive a pointer to the corresponding UNIT
   structure.  If the command does not access the drive, or if the unit number
   is invalid, the unit pointer is set to NULL.  A pointer to the controller
   unit is also set up; for ICD controllers, the controller and drive unit are
   the same.

   The controller library supports up to eight drives per MAC controller and one
   drive per ICD controller.  Unit numbers 0-7 represent valid drive addresses
   for a MAC controller.  The unit number field is ignored for an ICD
   controller, and unit 0 is always implied.  In simulation, MAC unit numbers
   correspond one-for-one with device units, whereas one ICD controller is
   associated with each of the several device units that are independently
   addressed as unit 0.

   The MAC controller firmware allows access to unit numbers 8-10 without
   causing a Unit Unavailable error.  Instead, the controller reports these
   legal-but-invalid units as permanently offline.

   If a diagnostic override is defined, and the cylinder, head, sector, and
   opcode values from the current override entry match their corresponding
   controller values, then the status and SPD values are set from the entry
   rather than being cleared, and the pointer is moved to the next entry.  The
   controller is then set to the busy state, and the validity of the opcode and
   unit are checked.  If any errors are detected, the appropriate status is set,
   and the controller unit is scheduled for the End_Phase to simulate the
   controller processing overhead.

   If the command and unit are valid, then if the command accesses a drive unit,
   the unit's Opcode field is set, and any pending Attention status is cleared.
   If the command takes or returns parameters, then the Parameter_Phase is set
   up on the controller unit and the wait timer is started.  Commands that
   return parameters temporarily store their parameter values in the sector
   buffer at this time for return as the CPU interface requests them.  The Cold
   Load Read and Recalibrate commands start their respective seeks at this time,
   and commands that complete immediately, e.g., Set File Mask, Wakeup, etc.,
   schedule the End_Phase on the controller unit and set up the status value for
   return if they end with the WRTIO function.

   Finally, the controller and/or drive units are activated if they were
   scheduled.  If a seek is in progress on a drive when a command that
   waits for seek completion is started, the unit is not rescheduled.  Instead,
   the unit is left in the Seek_Phase, but the unit's Opcode field is changed to
   reflect the new command so that the command will start automatically when the
   seek completes.


   Implementation notes:

    1. A command is started only if the controller is not busy.  Therefore, the
       target unit can be only in either the Seek_Phase or the Idle_Phase, as
       all other phases will have the controller in the Busy_State.

    2. Commands that access units and take parameters (e.g., Verify) set up the
       parameter access on the controller unit but perform the rest of the
       operation on the drive unit.  The controller must be used so that
       parameters may be read for the next command for a unit that is currently
       seeking (stacked commands wait for seek completion after parameters have
       been read).

    3. Drive Attention may be still set on a drive that has completed a seek but
       has not been able to interrupt the CPU before a new command is started.
       Therefore, Attention is always cleared when a command starts.

    4. In hardware, the Recalibrate command waits for a seek-in-progress to
       complete before the RCL tag is sent to the drive.  In simulation,
       repositioning the heads to cylinder 0 is started immediately, but any
       remaining time from a seek-in-progress is added to the time required for
       repositioning.  The effect is that recalibration completes in the time it
       would have taken for the seek-in-progress to complete followed by
       repositioning from that location.

    5. The Cold Load Read command does not check for Access Not Ready before
       issuing the seek to cylinder 0, so if a seek or recalibrate is in
       progress, the drive will reject it with a Seek Check error.  However, the
       command continues, and when the seek in progress completes and the read
       begins, the Seek Check error will abort the command at this point with a
       Status-2 Error.

    6. ECC is not simulated, so the Request Syndrome command always returns zero
       values for the displacement and patterns and Uncorrectable Data Error for
       the status.  Correctable Data Error status cannot occur unless a
       diagnostic override is in effect.

    7. The Wakeup command references a drive unit but is scheduled on the
       controller unit because it may be issued while the drive is seeking.
*/

static CNTLR_IFN_IBUS start_command (CVPTR cvptr, CNTLR_FLAG_SET inbound_flags, CNTLR_IBUS inbound_data)
{
UNIT *cuptr, *duptr, *rptr;
uint32 unit;
int32 seek_wait_time;
PRPTR props;
CNTLR_IFN_IBUS outbound;
DIAG_ENTRY *dop = NULL;

wait_timer (cvptr, CLEAR);                              /* stop the command wait timer */

cvptr->opcode = CM_OPCODE (inbound_data);               /* get the opcode from the command */

if (cvptr->opcode > LAST_OPCODE                                 /* if the opcode is undefined */
  || cvptr->type > LAST_CNTLR                                   /*   or the controller type is undefined */
  || cmd_props [cvptr->opcode].valid [cvptr->type] == FALSE)    /*     or the opcode is not valid for this controller */
    cvptr->opcode = Invalid_Opcode;                             /*       then replace it with the invalid opcode */

props = &cmd_props [cvptr->opcode];                     /* get the properties associated with the opcode */

if (cvptr->type == MAC) {                               /* if this a MAC controller */
    if (props->unit_field)                              /*   then if the unit field is defined */
        unit = CM_UNIT (inbound_data);                  /*     then get it from the command */
    else                                                /*   otherwise the unit is not specified in the command */
        unit = 0;                                       /*     so the unit is always unit 0 */

    cuptr = CNTLR_UPTR;                                 /* set the controller unit pointer */

    if (unit > DL_MAXDRIVE                              /* if the unit number is invalid */
      || props->unit_access == FALSE)                   /*   or the command accesses the controller only */
        duptr = NULL;                                   /*     then the drive pointer does not correspond to a unit */
    else                                                /* otherwise the command accesses a valid drive unit */
        duptr = cvptr->device->units + unit;            /*   so set the drive pointer to the unit */
    }

else {                                                  /* otherwise this is an ICD or CS/80 controller */
    unit = 0;                                           /*   so the unit value isn't used */
    cuptr = duptr =                                     /*     and the unit number was predefined */
      cvptr->device->units + cvptr->poll_unit;          /*       when the controller structure was initialized */
    }

tpprintf (cvptr->device, DL_DEB_INCO, "Unit %u %s command started\n",
          unit, opcode_name [cvptr->opcode]);

if (cvptr->dop_index >= 0)                              /* if the diagnostic override table is defined */
    dop = cvptr->dop_base + cvptr->dop_index;           /*   then point at the current entry */

if (dop                                                 /* if the table entry exists */
  && dop->cylinder == cvptr->cylinder                   /*   and the cylinder, */
  && dop->head     == cvptr->head                       /*     head, */
  && dop->sector   == cvptr->sector                     /*       sector, */
  && dop->opcode   == cvptr->opcode)  {                 /*         and opcode values match the current values */
    cvptr->spd_unit = dop->spd | unit;                  /*           then override the Spare/Protected/Defective */
    cvptr->status   = dop->status;                      /*             and status values from the override entry */

    cvptr->dop_index++;                                 /* point at the  */
    dop++;                                              /*   next table entry */

    tpprintf (cvptr->device, DL_DEB_INCO, "Unit %u cylinder %u head %u sector %u diagnostic override\n",
              unit, cvptr->cylinder, cvptr->head, cvptr->sector);
    }

else if (props->clear_status) {                         /* otherwise if this command clears prior status */
    cvptr->status = Normal_Completion;                  /*   then do it */
    cvptr->spd_unit = unit;                             /*     and save the unit number for status requests */
    }


cvptr->state = Busy_State;                              /* the controller is now busy */
cvptr->index = 0;                                       /* reset the buffer index */
cvptr->count = 0;                                       /*   and the sector/word count */
cvptr->verify = props->verify_address;                  /* set the address verification flag */

cuptr->Opcode = cvptr->opcode;                          /* set the controller unit opcode */
cuptr->wait = NO_EVENT;                                 /*   and assume no controller scheduling */

outbound = cmd_functions [cvptr->opcode] [Idle_Phase];  /* set up the initial function set and zero data */


if (cvptr->opcode == Invalid_Opcode)                        /* if the opcode is invalid */
    set_completion (cvptr, cuptr, Illegal_Opcode);          /*   then finish with an illegal opcode error */

else if (props->unit_check && unit > MAX_UNIT)              /* otherwise if the unit number is checked and is illegal */
    set_completion (cvptr, cuptr, Unit_Unavailable);        /*   then finish with a unit unavailable error */

else if (props->unit_check && unit > DL_MAXDRIVE            /* otherwise if the unit number is checked and is invalid */
  || props->seek_wait && (drive_status (duptr) & S2_STOPS)) /*   or if we're waiting for an offline drive */
    set_completion (cvptr, cuptr, Status_2_Error);          /*     then finish with a Status-2 error */

else {                                                  /* otherwise the command and unit are valid */
    if (duptr) {                                        /* if the drive unit is accessed  */
        duptr->Opcode = cvptr->opcode;                  /*   then set the drive opcode for later reference */
        duptr->wait = NO_EVENT;                         /* assume no drive scheduling */
        duptr->Status &= ~S2_ATTENTION;                 /* clear any pending Attention status */
        }

    if (props->param_count != 0) {                      /* if the command takes or returns parameters */
        cvptr->length = props->param_count;             /*   then set the parameter count */
        cuptr->Phase = Parameter_Phase;                 /* set up the parameter transfer on the controller */
        wait_timer (cvptr, SET);                        /*   and start the timer to wait for the first parameter */
        }

    switch (cvptr->opcode) {                            /* dispatch the command for initiation */

        case Cold_Load_Read:
            cvptr->cylinder = 0;                                /* set the cylinder address to 0 */
            cvptr->head = CM_HEAD (inbound_data);               /* set the head and */
            cvptr->sector = CM_SECTOR (inbound_data);           /*   sector addresses from the command */

            if (start_seek (cvptr, duptr) == FALSE)             /* start the seek; if it failed */
                set_completion (cvptr, cuptr, Status_2_Error);  /*   then set up the completion status */

            tpprintf (cvptr->device, DL_DEB_CMD, "Unit %u %s from cylinder %u head %u sector %u\n",
                      unit, opcode_name [Cold_Load_Read], cvptr->cylinder, cvptr->head, cvptr->sector);
            break;                                              /* wait for seek completion */


        case Recalibrate:
            tpprintf (cvptr->device, DL_DEB_CMD, "Unit %u %s to cylinder 0\n",
                      unit, opcode_name [Recalibrate]);

            if (duptr->Phase == Seek_Phase) {                   /* if the unit is currently seeking */
                seek_wait_time = sim_activate_time (duptr);     /*   then get the remaining event time */

                sim_cancel (duptr);                             /* cancel the event to allow rescheduling */
                duptr->Phase = Idle_Phase;                      /*   and idle the drive so that the seek succeeds */

                tpprintf (cvptr->device, DL_DEB_INCO, "Unit %u %s command waiting for seek completion\n",
                          unit, opcode_name [Recalibrate]);
                }

            else                                                /* otherwise the drive is idle */
                seek_wait_time = 0;                             /*   so there's no seek wait time */

            if (start_seek (cvptr, duptr) == FALSE)                 /* start the seek; if it failed */
                set_completion (cvptr, cuptr, Status_2_Error);      /*   then set up the completion status */

            else if (cvptr->type == MAC)                            /* otherwise if this a MAC controller */
                set_completion (cvptr, cuptr, Normal_Completion);   /*   then schedule seek completion */

            duptr->wait = duptr->wait + seek_wait_time;             /* increase the delay by any remaining seek time */
            break;                                                  /*   and wait for the recalibrate to complete */


        case Request_Status:
            cvptr->buffer [0] = (DL_BUFFER) (cvptr->spd_unit    /* set the Status-1 value */
                                  | S1_STATUS (cvptr->status)); /*   into the buffer */

            if (cvptr->type == MAC)                     /* if this a MAC controller */
                if (unit > DL_MAXDRIVE)                 /*   then if the unit number is invalid */
                    rptr = NULL;                        /*     then it does not correspond to a unit */
                else                                    /*   otherwise the unit is valid */
                    rptr = cvptr->device->units + unit; /*     so get the address of the referenced unit */
            else                                        /* otherwise it is not a MAC controller */
                rptr = duptr;                           /*   so the referenced unit is the current unit */

            cvptr->buffer [1] = (DL_BUFFER) drive_status (rptr);    /* set the Status-2 value into the buffer */

            tpprintf (cvptr->device, DL_DEB_CMD, "Unit %u %s returns %sunit %u | %s and %s%s | %s\n",
                      unit, opcode_name [Request_Status],
                      fmt_bitset (cvptr->spd_unit, status_1_format),
                      CM_UNIT (cvptr->spd_unit), dl_status_name (cvptr->status),
                      (cvptr->buffer [1] & S2_ERROR ? "error | " : ""),
                      drive_props [S2_TO_DRIVE_TYPE (cvptr->buffer [1])].name,
                      fmt_bitset (cvptr->buffer [1], status_2_format));

            if (rptr)                                   /* if the referenced unit is valid */
                rptr->Status &= ~S2_FIRST_STATUS;       /*   then clear the First Status bit */

            cvptr->spd_unit = S1_UNIT (unit);           /* save the unit number referenced in the command */

            if (unit > MAX_UNIT)                        /* if the unit number is illegal */
                cvptr->status = Unit_Unavailable;       /*   then the next status will be Unit Unavailable */
            else                                        /* otherwise a legal unit */
                cvptr->status = Normal_Completion;      /*   clears the controller status */
            break;


        case Request_Sector_Address:
            if (drive_status (duptr) & S2_NOT_READY)            /* if the drive is not ready */
                set_completion (cvptr, cuptr, Status_2_Error);  /*   then finish with Not Ready status */

            else                                                /* otherwise the drive is ready */
                cvptr->buffer [0] =                             /*   so calculate the current sector address */
                  (DL_BUFFER) CURRENT_SECTOR (cvptr, duptr);

            tpprintf (cvptr->device, DL_DEB_CMD, "Unit %u %s returns sector %u\n",
                      unit, opcode_name [Request_Sector_Address], cvptr->buffer [0]);
            break;


        case Clear:
            clear_controller (cvptr, Soft_Clear);               /* clear the controller */
            set_completion (cvptr, cuptr, Normal_Completion);   /*   and schedule the command completion */

            tpprintf (cvptr->device, DL_DEB_CMD, "%s\n", opcode_name [Clear]);
            break;


        case Request_Syndrome:
            if (cvptr->status == Correctable_Data_Error) {      /* if this is a correction override */
                cvptr->buffer [3] = (DL_BUFFER) dop->spd;       /*   then load the displacement */
                cvptr->buffer [4] = (DL_BUFFER) dop->cylinder;  /*     and three */
                cvptr->buffer [5] = (DL_BUFFER) dop->head;      /*       syndrome words */
                cvptr->buffer [6] = (DL_BUFFER) dop->sector;    /*         from the override entry */

                cvptr->dop_index++;                             /* point at the  */
                dop++;                                          /*   next table entry */
                }

            else {                                          /* otherwise no correction data was supplied */
                cvptr->buffer [3] = 0;                      /*   so the displacement is always zero */
                cvptr->buffer [4] = 0;                      /*     as are */
                cvptr->buffer [5] = 0;                      /*       the three */
                cvptr->buffer [6] = 0;                      /*         syndrome words */

                if (cvptr->status == Normal_Completion)         /* if we've been called without an override */
                    cvptr->status = Uncorrectable_Data_Error;   /*   then presume that an uncorrectable error occurred */
                }

            cvptr->buffer [0] = (DL_BUFFER) (cvptr->spd_unit    /* save the Status-1 value */
                                  | S1_STATUS (cvptr->status)); /*   in the buffer */

            set_address (cvptr, 1);                         /* save the CHS values in the buffer */

            tpprintf (cvptr->device, DL_DEB_CMD, "%s returns %sunit %u | %s | cylinder %u head %u sector %u | "
                                                 "syndrome %06o %06o %06o %06o\n",
                      opcode_name [Request_Syndrome], fmt_bitset (cvptr->spd_unit, status_1_format),
                      CM_UNIT (cvptr->spd_unit), dl_status_name (cvptr->status),
                      cvptr->cylinder, cvptr->head, cvptr->sector,
                      cvptr->buffer [3], cvptr->buffer [4], cvptr->buffer [5], cvptr->buffer [6]);

            next_sector (cvptr, cvptr->device->units        /* address the next sector of the last unit used */
                                  + S1_UNIT (cvptr->spd_unit));
            break;


        case Set_File_Mask:
            cvptr->file_mask = CM_FILE_MASK (inbound_data);     /* save the supplied file mask */

            outbound |= CM_RETRY (inbound_data);                /* return the retry count */

            set_completion (cvptr, cuptr, Normal_Completion);   /* schedule the command completion */

            tpprintf (cvptr->device, DL_DEB_CMD, "%s to %sretries %u\n",
                      opcode_name [Set_File_Mask], fmt_bitset (cvptr->file_mask, file_mask_format),
                      CM_RETRY (inbound_data));
            break;


        case Request_Disc_Address:
            set_address (cvptr, 0);                     /* set the controller's CHS values into the buffer */

            tpprintf (cvptr->device, DL_DEB_CMD, "Unit %u %s returns cylinder %u head %u sector %u\n",
                      unit, opcode_name [Request_Disc_Address], cvptr->cylinder, cvptr->head, cvptr->sector);
            break;


        case End:
            tpprintf (cvptr->device, DL_DEB_CMD, "%s\n", opcode_name [End]);

            end_command (cvptr, NULL, Normal_Completion);   /* end the command and idle the controller */
            break;


        case Wakeup:
            set_completion (cvptr, cuptr, Unit_Available);  /* schedule the command completion */

            tpprintf (cvptr->device, DL_DEB_CMD, "Unit %u %s\n",
                      unit, opcode_name [Wakeup]);
            break;


        /* these commands wait for seek completion before starting */

        case Read_Without_Verify:
            cvptr->verify = FALSE;                      /* do not verify until a track is crossed */
            inbound_data &= ~CM_SPD_MASK;               /* clear the SPD bits to avoid changing the state */

        /* fall through into the Initialize case */

        case Initialize:
            cvptr->spd_unit |= CM_SPD (inbound_data);   /* merge the SPD flags with the unit */

        /* fall through into the read/write cases */

        case Read:
        case Read_Full_Sector:
        case Write:
        case Write_Full_Sector:
            if (duptr->Phase == Seek_Phase)                 /* if the unit is currently seeking */
                tpprintf (cvptr->device, DL_DEB_INCO, "Unit %u %s command waiting for seek completion\n",
                          unit, opcode_name [cvptr->opcode]);

            else                                            /* otherwise the unit is idle */
                set_rotation (cvptr, duptr);                /*   so set up the rotation phase and latency */
            break;


        /* these commands take parameters but otherwise require no preliminary work */

        case Seek:
        case Verify:
        case Address_Record:
        case Read_With_Offset:
        case Load_TIO_Register:
            break;


        case Invalid_Opcode:                            /* for completeness; invalid commands are not dispatched */
            break;
        }
    }


if (cvptr->state == Busy_State) {                       /* if the command has not completed immediately */
    if (cuptr->wait != NO_EVENT && cvptr->type == MAC)  /*   then if the controller unit is scheduled */
        activate_unit (cvptr, cuptr);                   /*      then activate it */

    if (duptr && duptr->wait != NO_EVENT)               /*   and if the drive unit is valid and scheduled */
        activate_unit (cvptr, duptr);                   /*      then activate it as well */
    }

if (outbound & WRTIO)                                           /* if status is expected immediately */
    outbound |= S1_STATUS (cvptr->status) | cvptr->spd_unit;    /*   then return the Status-1 value */

return outbound;                                        /* return the data word and function set */
}


/* Continue the current command.

   This routine simulates continuing execution of the controller microcode for
   the current command.  It's called whenever the controller has had to wait for
   action from the CPU interface or the drive unit, and that action has now
   occurred.  Typically, this would be whenever the interface flag status
   changes, or a unit's event service has been entered.  It returns a set of
   action functions and a data word to the caller.  It also sets up the next
   phase of operation on the controller and/or drive unit and schedules the
   unit(s) as appropriate.

   On entry, the "uptr" parameter is set to NULL if the controller was called
   for a CPU interface action, or it points to the unit whose event service was
   just called; this may be either the controller unit or a drive unit.  The
   "inbound_flags" and "inbound_data" parameters contain the CPU interface flags
   and data buffer values.

   If the entry is for a unit service, the unit number of the drive requesting
   service is determined.  If the entry is for the CPU interface, the controller
   is checked; if it's idle, the routine returns because no command is in
   progress.

   While a unit is activated, the current phase indicates the reason for the
   activation, i.e., what the simulated drive is "doing" at the moment, as
   follows:

     Idle_Phase        -- waiting for the next command to be issued (note that
                          this phase, which indicates that the unit is not
                          scheduled, is distinct from the controller Idle_State,
                          which indicates that the controller itself is idle)

     Parameter_Phase   -- waiting for a parameter to be transferred to or from
                          the interface

     Seek_Phase        -- waiting for a seek to complete (explicit or automatic)

     Rotate_Phase      -- waiting for the target sector to arrive under the head

     Data_Phase        -- waiting for the interface to accept or return the next
                          data word to be transferred to or from the drive

     Intersector_Phase -- waiting for the controller to finish executing the
                          end-of-sector microcode

     End_Phase         -- waiting for the controller to finish executing the
                          microcode corresponding to the current command

   Depending on the current command opcode and phase, a number of actions may be
   taken:

     Idle_Phase -- If the controller unit is being serviced, then the 1.74
     second command wait timer has expired while waiting for a new command.
     Reset the file mask and idle the controller.

     Parameter_Phase -- If the controller unit is being serviced, then the 1.74
     second command wait timer has expired while waiting for a parameter from
     the CPU.  Reset the file mask and idle the controller.  Otherwise, for
     outbound parameters, return the next word from the sector buffer and
     restart the timer; if the last word has been sent, end the command.  For
     inbound parameters, store the next word in the appropriate controller state
     variable; if the last word has been received, end the command or set up the
     next operation phase (seek, rotate, etc.).

     Seek_Phase -- If a Seek or Recalibrate has completed, set Drive Attention
     status.  All other commands have been waiting for seek completion before
     starting, so set up the rotate phase to begin the command.

     Rotate_Phase -- Set up the read or write of the current sector.  For all
     commands except Verify, proceed to the data phase to begin the data
     transfer.  For Verify, skip the data phase and proceed directly to the
     end-of-sector processing, but schedule that event as though the full sector
     rotation time had elapsed.

     Data_Phase -- For read transfers, return the next word from the sector
     buffer, or for write transfers, store the next word into the sector buffer,
     and schedule the next data phase transfer if the CPU has not indicated an
     end-of-data condition.  If it has, or if the last word of the sector has
     been transmitted, schedule the intersector phase.

     Intersector_Phase -- Complete the read or write of the current sector.  If
     the CPU has indicated an end-of-data condition, end the command.
     Otherwise, address the next sector and schedule the rotate phase.

     End_Phase -- End the command.  The end phase is used to provide a
     controller delay when an operation has no other command phases.

   At the completion of the current phase, the next phase is scheduled, if
   required, before returning the appropriate function set and data word to the
   caller.

   The commands employ the various phases as follows:
                                                               Inter
       Command                  Param   Seek   Rotate   Data   sector   End
     +------------------------+-------+------+--------+------+--------+-----+
     | Cold Load Read         |   -   |  D   |   D    |  D   |   D    |  -  |
     | Recalibrate            |   -   |  D   |   -    |  -   |   -    |  -  |
     | Seek                   |   c   |  D   |   -    |  -   |   -    |  -  |
     | Request Status         |   c   |  -   |   -    |  -   |   -    |  -  |
     | Request Sector Address |   c   |  -   |   -    |  -   |   -    |  -  |
     | Read                   |   -   |  -   |   D    |  D   |   D    |  -  |
     | Read Full Sector       |   -   |  -   |   D    |  D   |   D    |  -  |
     | Verify                 |   c   |  -   |   D    |  -   |   D    |  -  |
     | Write                  |   -   |  -   |   D    |  D   |   D    |  -  |
     | Write Full Sector      |   -   |  -   |   D    |  D   |   D    |  -  |
     | Clear                  |   -   |  -   |   -    |  -   |   -    |  C  |
     | Initialize             |   -   |  -   |   D    |  D   |   D    |  -  |
     | Address Record         |   c   |  -   |   -    |  -   |   -    |  -  |
     | Request Syndrome       |   c   |  -   |   -    |  -   |   -    |  -  |
     | Read With Offset       |   c   |  -   |   D    |  D   |   D    |  -  |
     | Set File Mask          |   -   |  -   |   -    |  -   |   -    |  C  |
     | Invalid Opcode         |   -   |  -   |   -    |  -   |   -    |  C  |
     | Read Without Verify    |   -   |  -   |   D    |  D   |   D    |  -  |
     | Load TIO Register      |   c   |  -   |   -    |  -   |   -    |  -  |
     | Request Disc Address   |   c   |  -   |   -    |  -   |   -    |  -  |
     | End                    |   -   |  -   |   -    |  -   |   -    |  -  |
     | Wakeup                 |   -   |  -   |   -    |  -   |   -    |  C  |
     +------------------------+-------+------+--------+------+--------+-----+

     Key:
       C = controller unit is scheduled
       c = controller is called directly by the CPU interface
       D = drive unit is scheduled
       - = the phase is not used


   Implementation notes:

    1. The "%.0u" print specification in the trace call absorbs the zero "unit"
       value parameter without printing when the controller unit is specified.

    2. The Seek command does not check for Access Not Ready before issuing the
       seek, so if a prior seek is in progress, the drive will reject it with a
       Seek Check error.  However, the parameters (e.g., controller CHS
       addresses) are still set as though the command had succeeded.

       A Seek command will return to the Poll Loop with Seek Check status set.
       When the seek in progress completes, the controller will interrupt with
       Drive Attention status.  The controller address will differ from the
       drive address, so it's incumbent upon the caller to issue a Request
       Status command after the seek, which will return Status-2 Error status.

    3. The set of interface functions to assert on command completion is
       normally specified by the End_Phase entry in the cmd_functions table,
       regardless of whether or not a command has an end phase.  However, the
       Request Syndrome command returns either Correctable Data Error or
       Uncorrectable Data Error for normal command completion.  These status
       returns would normally include DVEND to indicate that the command should
       be retried, but that's wrong for Request Syndrome, so we explicitly
       override the function set for this command.

    4. Command completion must be detected by the controller state changing to
       "not busy" rather than simply being "not busy" at the end of the routine.
       Otherwise, a seek that completes while the controller is waiting for a
       command would re-issue the end phase functions.

    5. The disc is a synchronous device, so overrun or underrun can occur if the
       interface is not ready when the controller must transfer data.  There are
       four conditions that lead to an overrun or underrun:

         a. The controller is ready with a disc read word (IFCLK * IFIN), but
            the interface buffer is full (DTRDY).

         b. The controller needs a disc write word (IFCLK * IFOUT), but the
            interface buffer is empty (~DTRDY).

         c. The CPU attempts to read a word, but the interface buffer is empty
            (~DTRDY).

         d. The CPU attempts to write a word, but the interface buffer is full
            (DTRDY).

       The 13037 controller ORs the interface-supplied OVRUN signal with an
       internal overrun latch that sets on condition 2 above (write underrun).
       However, both the 13175A HP 1000 interface and the 30229B HP 3000
       interface assert OVRUN for all four conditions, so the latch is not
       simulated.

    6. Not all changes of CPU interface flag status are significant.  If the
       routine is called when it isn't needed, the routine simply returns with
       no action.
*/

static CNTLR_IFN_IBUS continue_command (CVPTR cvptr, UNIT *uptr, CNTLR_FLAG_SET inbound_flags, CNTLR_IBUS inbound_data)
{
const t_bool service_entry = (uptr != NULL);            /* set TRUE if entered via unit service */
CNTLR_OPCODE opcode;
CNTLR_PHASE phase;
CNTLR_IFN_IBUS outbound;
t_bool controller_service, controller_was_busy;
int32 unit;
uint32 sector_count;

if (service_entry) {                                    /* if this is an event service entry */
    unit = (int32) (uptr - cvptr->device->units);       /*   then get the unit number */

    controller_service = (uptr == CNTLR_UPTR            /* set TRUE if the controller is being serviced */
                            && cvptr->type == MAC);
    }

else if (CNTLR_UPTR->Phase == Idle_Phase)               /* otherwise if this interface entry isn't needed */
    return NO_ACTION;                                   /*   then quit as there's nothing to do */

else {                                                  /* otherwise the controller is expecting the entry */
    uptr = CNTLR_UPTR;                                  /* set up to use the controller unit */
    unit = CNTLR_UNIT;                                  /*   and unit number */
    controller_service = FALSE;                         /*     but note that this isn't a service entry */
    }


opcode = (CNTLR_OPCODE) uptr->Opcode;                   /* get the current opcode */
phase  = (CNTLR_PHASE)  uptr->Phase;                    /*   and command phase */

if (controller_service == FALSE || phase == End_Phase)
    tpprintf (cvptr->device, DL_DEB_STATE, (unit == CNTLR_UNIT
                                              ? "Controller unit%.0d %s %s phase entered from %s\n"
                                              : "Unit %d %s %s phase entered from %s\n"),
              (unit == CNTLR_UNIT ? 0 : unit), opcode_name [opcode], phase_name [phase],
              (service_entry ? "service" : "interface"));

controller_was_busy = (cvptr->state == Busy_State);     /* set TRUE if the controller was busy on entry */

outbound = cmd_functions [opcode] [phase];              /* set up the initial function return set */


switch (phase) {                                        /* dispatch the phase */

    case Idle_Phase:                                    /* the command wait timer has expired */
        clear_controller (cvptr, Timeout_Clear);        /*   so idle the controller and clear the file mask */

        outbound = NO_FUNCTIONS;                        /* clear the function set for an idle return */

        tpprintf (cvptr->device, DL_DEB_INCO, "Controller command wait timed out\n");
        break;


    case Parameter_Phase:
        if (controller_service) {                       /* if the parameter wait timer has expired */
            clear_controller (cvptr, Timeout_Clear);    /*   then idle the controller and clear the file mask */

            outbound = NO_FUNCTIONS;                    /* clear the function set for an idle return */

            tpprintf (cvptr->device, DL_DEB_INCO, "Unit %u %s command aborted with parameter wait timeout\n",
                      CM_UNIT (cvptr->spd_unit), opcode_name [opcode]);
            }

        else switch (opcode) {                          /* otherwise dispatch the command */

            case Request_Status:                        /* these commands */
            case Request_Disc_Address:                  /*   return parameters */
            case Request_Sector_Address:                /*     to the interface */
            case Request_Syndrome:
                if (cvptr->length == 0)                         /* if the last parameter has been sent */
                    end_command (cvptr, uptr, cvptr->status);   /*   then terminate the command with the preset status */

                else {                                          /* otherwise there are more to send */
                    outbound |= cvptr->buffer [cvptr->index++]; /*   so return the next value from the buffer */
                    cvptr->length = cvptr->length - 1;          /*     and drop the parameter count */

                    wait_timer (cvptr, SET);                    /* restart the parameter timer */
                    }
                break;


            case Seek:                                          /* these commands receive parameters */
            case Address_Record:                                /*   from the interface */
                cvptr->buffer [cvptr->index++] =                /* save the current one in the buffer */
                   (DL_BUFFER) inbound_data;
                cvptr->length = cvptr->length - 1;              /*   and drop the parameter count */

                if (cvptr->length > 0)                          /* if another parameter is expected */
                    wait_timer (cvptr, SET);                    /*   then restart the parameter timer */

                else {                                              /* otherwise all parameters are in */
                    cvptr->cylinder = cvptr->buffer [0];            /*   so fill in the supplied cylinder */
                    cvptr->head = PI_HEAD (cvptr->buffer [1]);      /*     and head */
                    cvptr->sector = PI_SECTOR (cvptr->buffer [1]);  /*       and sector addresses */

                    if (opcode == Address_Record) {             /* if this is an Address Record command */
                        cvptr->eoc = CLEAR;                     /*   then clear the end-of-cylinder flag */

                        tpprintf (cvptr->device, DL_DEB_CMD, "%s to cylinder %u head %u sector %u\n",
                                  opcode_name [Address_Record],
                                  cvptr->cylinder, cvptr->head, cvptr->sector);

                        end_command (cvptr, uptr,               /* the command is now complete */
                                     Normal_Completion);
                        }

                    else {                                      /* otherwise it's a Seek command */
                        tpprintf (cvptr->device, DL_DEB_CMD, "Unit %u %s to cylinder %u head %u sector %u\n",
                                  CM_UNIT (cvptr->spd_unit), opcode_name [Seek],
                                  cvptr->cylinder, cvptr->head, cvptr->sector);

                        uptr = cvptr->device->units             /* get the target unit */
                                 + CM_UNIT (cvptr->spd_unit);

                        if (start_seek (cvptr, uptr) == FALSE)  /* start the seek; if it failed, */
                            end_command (cvptr, uptr,           /*   then report the error */
                                         Status_2_Error);

                        else if (cvptr->type == MAC)            /* otherwise if this a MAC controller */
                            end_command (cvptr, uptr,           /*   then complete the command and idle the controller */
                                         Normal_Completion);
                        }                                       /* otherwise an ICD command ends when the seek completes */
                    }
                break;


            case Verify:
                if (inbound_data == 0)                  /* if the sector count is zero */
                    sector_count = 65536;               /*   then use the rollover count */
                else                                    /* otherwise */
                    sector_count = inbound_data;        /*   use the count as is */

                cvptr->count = sector_count * WORDS_PER_SECTOR; /* convert to the number of words to verify */

                tpprintf (cvptr->device, DL_DEB_CMD, "Unit %u %s %u sector%s\n",
                          CM_UNIT (cvptr->spd_unit), opcode_name [Verify],
                          sector_count, (sector_count == 1 ? "" : "s"));

                wait_timer (cvptr, CLEAR);              /* stop the parameter timer */

                uptr = cvptr->device->units             /* get the target unit */
                         + CM_UNIT (cvptr->spd_unit);

                if (uptr->Phase == Seek_Phase) {        /* if a seek is in progress, */
                    uptr->wait = NO_EVENT;              /*   then wait for it to complete */

                    tpprintf (cvptr->device, DL_DEB_INCO, "Unit %u %s command waiting for seek completion\n",
                              CM_UNIT (cvptr->spd_unit), opcode_name [Verify]);
                    }

                else                                    /* otherwise the unit is idle */
                    set_rotation (cvptr, uptr);         /*   so set up the rotation phase and latency */
                break;


            case Read_With_Offset:
                tpprintf (cvptr->device, DL_DEB_CMD, "Unit %u %s using %soffset %+d\n",
                          CM_UNIT (cvptr->spd_unit), opcode_name [Read_With_Offset],
                          fmt_bitset (inbound_data, offset_format),
                          (inbound_data & PI_NEG_OFFSET ? - (int) PI_OFFSET (inbound_data)
                                                        :   (int) PI_OFFSET (inbound_data)));

                wait_timer (cvptr, CLEAR);                  /* stop the parameter timer */

                uptr = cvptr->device->units                 /* get the target unit */
                         + CM_UNIT (cvptr->spd_unit);

                if (uptr->Phase == Seek_Phase) {            /* if a seek is in progress, */
                    uptr->wait = NO_EVENT;                  /*   then wait for it to complete */

                    tpprintf (cvptr->device, DL_DEB_INCO, "Unit %u %s command waiting for seek completion\n",
                              CM_UNIT (cvptr->spd_unit), opcode_name [Read_With_Offset]);
                    }

                else {                                      /* otherwise the unit is idle */
                    uptr->Phase = Seek_Phase;               /*   so schedule the seek phase */
                    uptr->wait = cvptr->dlyptr->seek_one;   /*     with the offset positioning delay */
                    }
                break;


            case Load_TIO_Register:
                wait_timer (cvptr, CLEAR);                      /* stop the parameter timer */

                tpprintf (cvptr->device, DL_DEB_CMD, "%s with %06o\n",
                          opcode_name [Load_TIO_Register], inbound_data);

                end_command (cvptr, uptr, Normal_Completion);   /* complete the command */

                return inbound_data                             /* return the supplied TIO value */
                         | cmd_functions [Load_TIO_Register] [End_Phase];
                break;


            default:                                    /* the remaining commands */
                break;                                  /*   do not have a parameter phase */
            }

        break;


    case Seek_Phase:
        switch (opcode) {                               /* dispatch the command */

            case Recalibrate:
            case Seek:
                if (cvptr->type == MAC) {               /* if this a MAC controller */
                    uptr->Status |= S2_ATTENTION;       /*   set Attention in the unit status */
                    uptr->Phase = Idle_Phase;           /*     and idle the drive */
                    }

                else                                            /* otherwise this is an ICD or CS/80 drive */
                    end_command (cvptr, uptr, Drive_Attention); /*   so seeks end with Drive Attention status */
                break;


            case Cold_Load_Read:
                cvptr->file_mask = CM_SPARE_EN;             /* enable sparing in surface mode without auto-seek */

            /* fall through into the default case */

            default:                                        /* a command was waiting on seek completion */
                set_rotation (cvptr, uptr);                 /*   so set up the rotation phase and latency */
                break;
            }

        break;


    case Rotate_Phase:
        switch (opcode) {                               /* dispatch the command */

            case Write:
            case Write_Full_Sector:
            case Initialize:
                start_write (cvptr, uptr);              /* start the sector write */
                break;


            case Read:
            case Read_Full_Sector:
            case Read_With_Offset:
            case Read_Without_Verify:
            case Cold_Load_Read:
                start_read (cvptr, uptr, inbound_flags);    /* start the sector read */
                break;


            case Verify:
                inbound_flags &= ~EOD;                          /* EOD is not relevant for Verify */

                if (start_read (cvptr, uptr, inbound_flags)) {  /* if the sector read was set up successfully */
                    uptr->Phase = Intersector_Phase;            /*   then skip the data phase */
                    uptr->wait = cvptr->dlyptr->sector_full;    /*     and reschedule for the sector read time */
                    }
                break;


            default:                                    /* the remaining commands */
                break;                                  /*   do not have a rotate phase */
            }

        break;


    case Data_Phase:
        if (inbound_flags & EOD)                        /* if the transfer has ended */
            outbound = NO_FUNCTIONS;                    /*   then don't assert IFIN/IFOUT on return */

        switch (opcode) {                               /* dispatch the command */

            case Read:
            case Read_With_Offset:
            case Read_Without_Verify:
            case Read_Full_Sector:
            case Cold_Load_Read:
                if ((inbound_flags & EOD) == NO_FLAGS) {            /* if the transfer continues */
                    outbound |= cvptr->buffer [cvptr->index++];     /*   then get the next word from the buffer */

                    cvptr->count  = cvptr->count  + 1;              /* count the */
                    cvptr->length = cvptr->length - 1;              /*   transfer */

                    tpprintf (cvptr->device, DL_DEB_XFER, "Unit %d %s word %u is %06o\n",
                              unit, opcode_name [opcode],
                              cvptr->count, DLIBUS (outbound));
                    }

                uptr->wait = cvptr->dlyptr->data_xfer;              /* set the transfer delay */

                if (cvptr->length == 0 || inbound_flags & EOD) {    /* if the buffer is empty or the transfer is done */
                      uptr->Phase = Intersector_Phase;              /*   then set up the intersector phase */

                      if (cvptr->device->flags & DEV_REALTIME)      /* if we're in realistic timing mode */
                          uptr->wait = uptr->wait                   /*   then account for the actual delay */
                                         * (cvptr->length + cmd_props [opcode].postamble_size);
                      }
                break;


            case Write:
            case Write_Full_Sector:
            case Initialize:
                if ((inbound_flags & EOD) == NO_FLAGS) {    /* if the transfer continues */
                    cvptr->buffer [cvptr->index++] =        /*   then store the next word in the buffer */
                       (DL_BUFFER) inbound_data;

                    cvptr->count  = cvptr->count  + 1;      /* count the */
                    cvptr->length = cvptr->length - 1;      /*   transfer */

                    tpprintf (cvptr->device, DL_DEB_XFER, "Unit %d %s word %u is %06o\n",
                              unit, opcode_name [opcode],
                              cvptr->count, inbound_data);
                    }

                uptr->wait = cvptr->dlyptr->data_xfer;              /* set the transfer delay */

                if (cvptr->length == 0 || inbound_flags & EOD) {    /* if the buffer is empty or the transfer is done */
                      uptr->Phase = Intersector_Phase;              /*   then set up the intersector phase */

                      if (cvptr->device->flags & DEV_REALTIME)      /* if we're in realistic timing mode */
                          uptr->wait = uptr->wait                   /*   then account for the actual delay */
                                         * (cvptr->length + cmd_props [opcode].postamble_size);
                      }
                break;


            default:                                    /* the remaining commands */
                break;                                  /*   do not have a data phase */
            }

        break;


    case Intersector_Phase:
        switch (opcode) {                               /* dispatch the command */

            case Read:
            case Read_With_Offset:
            case Read_Without_Verify:
            case Read_Full_Sector:
            case Cold_Load_Read:
                end_read (cvptr, uptr, inbound_flags);  /* end the sector read */
                break;


            case Write:
            case Write_Full_Sector:
            case Initialize:
                end_write (cvptr, uptr, inbound_flags); /* end the sector write */
                break;


            case Verify:
                cvptr->count = cvptr->count - WORDS_PER_SECTOR; /* decrement the word count */

                if (cvptr->count > 0)                   /* if there more sectors to verify */
                    inbound_flags &= ~EOD;              /*   then this is not the end of data */
                else                                    /* otherwise the command is complete */
                    inbound_flags |= EOD;               /*   and this is the end of data */

                end_read (cvptr, uptr, inbound_flags);  /* end the sector read */
                break;


            default:                                    /* the remaining commands */
                break;                                  /*   do not have an intersector phase */
            }

        break;


    case End_Phase:
        end_command (cvptr, uptr, cvptr->status);       /* complete the command with the preset status */
        break;
    }


if (uptr->wait != NO_EVENT)                             /* if the unit has been scheduled */
    activate_unit (cvptr, uptr);                        /*   then activate it */

if (controller_was_busy && cvptr->state != Busy_State) {    /* if the command has just completed */
    if (cvptr->status == Normal_Completion                  /*   then if the command completed normally */
      || opcode == Request_Syndrome)                        /*   or it was a Request Syndrome command */
        outbound = cmd_functions [opcode] [End_Phase];      /*     then use the normal exit function set */
    else                                                    /*   otherwise */
        outbound = status_functions [cvptr->status];        /*     the function set depends on the status */

    if (outbound & WRTIO)                                           /* if the TIO register will be written */
        outbound |= S1_STATUS (cvptr->status) | cvptr->spd_unit;    /*   then include the Status-1 value */
    }

return outbound;                                        /* return the data word and function set */
}


/* Poll the drives for Attention status.

   MAC controllers complete their Seek and Recalibrate commands when the seeks
   are initiated, so that other drives may be serviced during the waits.  A
   drive will set its Attention status when its seek completes, and the
   controller must poll the drives for attention requests when it is idle and
   interrupts are allowed by the CPU interface.

   Starting with the last unit that had previously requested attention, each
   drive is checked in sequence.  If a drive has its Attention status set, the
   controller saves its unit number, sets the result status to Drive Attention,
   and enters the command wait state.  The routine returns a function set that
   indicates that an interrupt should be generated.  The next time the routine
   is called, the poll begins with the last unit that requested attention, so
   that each unit is given an equal chance to respond.

   If no unit is requesting attention, the routine returns an empty function set
   to indicate that no interrupt should be generated.

   ICD controllers do not call this routine, because each controller waits for
   seek completion on its dedicated drive before completing the associated Seek
   or Recalibrate command.
*/

static CNTLR_IFN_IBUS poll_drives (CVPTR cvptr)
{
uint32  unit;
UNIT   *units = cvptr->device->units;

tpprintf (cvptr->device, DL_DEB_INCO, "Controller polled drives for attention\n");

for (unit = 0; unit <= DL_MAXDRIVE; unit++) {           /* check each unit in turn */
    cvptr->poll_unit =                                  /* start with the last unit checked */
      (cvptr->poll_unit + 1) % (DL_MAXDRIVE + 1);       /*   and cycle back to unit 0 */

    if (units [cvptr->poll_unit].Status & S2_ATTENTION) {   /* if the unit is requesting attention, */
        units [cvptr->poll_unit].Status &= ~S2_ATTENTION;   /*   clear the Attention status */

        tpprintf (cvptr->device, DL_DEB_INCO, "Unit %u requested attention\n",
                  cvptr->poll_unit);

        cvptr->spd_unit = cvptr->poll_unit;             /* set the controller's unit number */
        cvptr->status = Drive_Attention;                /*   and status */

        cvptr->state = Wait_State;                      /* set the controller state to waiting */
        wait_timer (cvptr, SET);                        /* start the command wait timer */

        return status_functions [Drive_Attention]               /* tell the caller to interrupt */
                 | S1_STATUS (cvptr->status) | cvptr->spd_unit; /*   and include the Status-1 value */
        }
    }

return NO_ACTION;                                       /* no drives have attention set */
}


/* Clear the controller.

   A "hard", "timeout", or "soft" clear is performed on the indicated controller
   as specified by the "clear_type" parameter.

   In hardware, four conditions clear the 13037 controller:

     - an initial application of power
     - an assertion of the CLEAR signal by the CPU interface
     - a timeout of the command wait timer
     - a programmed Clear command

   The first two conditions, called "hard clears," are equivalent and cause a
   firmware restart with the PWRON flag set.  The 13175 interface for the HP
   1000 asserts the CLEAR signal in response to the backplane CRS signal if the
   PRESET jumper is installed in the enabled position (which is the usual case).
   The 30229B interface for the HP 3000 asserts CLEAR in response to an IORESET
   signal or a programmed Master Reset if the PRESET DISABLE jumper is installed
   in the enabled position.

   The third condition, a "timeout clear", also causes a firmware restart but
   with the PWRON flag clear.  The last condition, a "soft clear" is executed in
   the command handler and therefore returns to the Command Wait Loop instead of
   the Poll Loop.

   For a hard clear, the 13037 controller will:

     - disconnect the CPU interface
     - zero the controller RAM (no drives held, last polled unit number reset)
     - clear the clock offset
     - clear the file mask
     - issue a Controller Preset to clear all connected drives
     - enter the Poll Loop (which clears the controller status)

   For a timeout clear, the 13037 controller will:

     - disconnect the CPU interface
     - clear the hold bits of any drives held by the interface that timed out
     - clear the clock offset
     - clear the file mask
     - enter the Poll Loop (which clears the controller status)

   For a programmed "soft" clear, the 13037 controller will:

     - clear the controller status
     - issue a Controller Preset to clear all connected drives
     - enter the Command Wait Loop

   Controller Preset is a tag bus command that is sent to all drives connected
   to the controller.  Each drive will:

     - disconnect from the controller
     - clear its internal drive faults
     - clear its head and sector registers
     - clear its illegal head and sector flip-flops
     - reset its seek check, first status, drive fault, and attention status

   On a 7905 or 7906 drive, clearing the head register will change Read-Only
   status to reflect the position of the PROTECT LOWER DISC switch.

   In simulation, a hard clear occurs when an SCP RESET command is entered, or a
   programmed CLC 0 instruction or Master Clear is executed.  A timeout clear
   occurs when the command or parameter wait timer expires.  A soft clear occurs
   when a programmed Clear command is issued.

   Because the controller execution state is implemented by scheduling command
   phases for the target or controller unit, a simulated firmware restart must
   abort any in-process activation.  However, a firmware restart does not affect
   seeks in progress, so these must be allowed to continue to completion so that
   their Attention requests will be honored.


   Implementation notes:

    1. The specific 13365 controller actions on hard or soft clears are not
       documented.  Therefore, an ICD controller clear is handled as a MAC
       controller clear, except that only the current drive is preset (as an ICD
       controller manages only a single drive).

    2. Neither hard nor soft clears affect the controller flags (e.g., EOC) or
       registers (e.g., cylinder address).

    3. In simulation, an internal seek, such as an auto-seek during a Read
       command or the initial seek during a Cold Load Read command, will be
       aborted for a hard clear, whereas in hardware it would complete normally.
       This is OK, however, because an internal seek always clears the drive's
       Attention status on completion, so aborting the simulated seek is
       equivalent to an immediate seek completion.

    4. If a drive unit is disabled, it is still cleared.  A unit cannot be
       disabled while it is active, so it must be in the idle state while
       disabled.  Clearing the status keeps it consistent in case it is
       reenabled later.

    5. A soft clear does not abort commands in progress on a drive unit.
       However, a soft clear is a result of a programmed command that can only
       be issued when the prior command has completed (except for Seek and
       Recalibrate commands, which are never aborted).

    6. In simulation, a Controller Preset only resets the specified status bits,
       as the remainder of the hardware actions are not implemented.
*/

static void clear_controller (CVPTR cvptr, CNTLR_CLEAR clear_type)
{
uint32  unit_count;
UNIT   *uptr;

if (clear_type == Timeout_Clear) {                      /* if this is a timeout clear */
    cvptr->file_mask = 0;                               /*   then clear the file mask */
    idle_controller (cvptr);                            /*     and idle the controller */
    }

else {                                                  /* otherwise */
    if (clear_type == Hard_Clear) {                     /*   if this a hard clear */
        cvptr->file_mask = 0;                           /*     then clear the file mask */

        if (cvptr->type == MAC)                         /* if this is a MAC controller */
            cvptr->poll_unit = 0;                       /*   then clear the last unit polled too */

        idle_controller (cvptr);                        /* idle the controller */
        }

    else                                                /* otherwise it's a soft clear */
        cvptr->status = Normal_Completion;              /*   so clear the status explicitly */

    if (cvptr->type == MAC) {                           /* if this a MAC controller */
        uptr = cvptr->device->units;                    /*   then preset all units */
        unit_count = cvptr->device->numunits - 1;       /*     except the controller unit */
        }

    else {                                              /* otherwise preset */
        uptr = cvptr->device->units + cvptr->poll_unit; /*   only the single unit */
        unit_count = 1;                                 /*     dedicated to the controller */
        }

    while (unit_count > 0) {                            /* preset each drive in turn */
        if (uptr->Phase != Idle_Phase                   /* if the unit is active */
          && uptr->Opcode != Seek                       /*   but not seeking */
          && uptr->Opcode != Recalibrate) {             /*     or recalibrating */
            sim_cancel (uptr);                          /*       then cancel the unit event */
            uptr->Phase = Idle_Phase;                   /*         and idle it */
            }

        uptr->Status &= ~(S2_CPS | S2_READ_ONLY);       /* do a "Controller Preset" on the unit */

        if (uptr->flags & UNIT_PROT_U)                  /* if the (upper) heads are protected */
            uptr->Status |= S2_READ_ONLY;               /*   then set read-only status */

        uptr++;                                         /* point at the next unit */
        unit_count = unit_count - 1;                    /*   and count the unit just cleared */
        }
    }

return;
}



/* Disc library global utility routines */


/* Return the name of an opcode.

   A string representing the supplied controller opcode is returned to the
   caller.  If the opcode is illegal or undefined for the indicated controller,
   the string "Invalid" is returned.
*/

const char *dl_opcode_name (CNTLR_TYPE controller, CNTLR_OPCODE opcode)
{
if (controller <= LAST_CNTLR                            /* if the controller type is legal */
  && opcode <= LAST_OPCODE                              /*   and the opcode is legal */
  && cmd_props [opcode].valid [controller])             /*     and is defined for this controller, */
    return opcode_name [opcode];                        /*       then return the opcode name */
else                                                    /* otherwise the type or opcode is illegal */
    return invalid_name;                                /*   so return an error indication */
}


/* Return the name of a command result status.

   A string representing the supplied command result status is returned to the
   caller.  If the status is illegal or undefined, the string "Invalid" is
   returned.
*/

const char *dl_status_name (CNTLR_STATUS status)
{
if (status <= Drive_Attention && status_name [status])  /* if the status is legal */
    return status_name [status];                        /*   then return the status name */
else                                                    /* otherwise the status is illegal */
    return invalid_name;                                /*   so return an error indication */
}



/* Disc library global SCP support routines */


/* Attach a disc image file to a unit.

   The file specified by the supplied filename is attached to the indicated
   unit.  If the attach was successful, the heads are loaded on the drive.


   Implementation notes:

    1. The pointer to the appropriate event delay times is set in case we are
       being called during a RESTORE command (the assignment is redundant
       otherwise).
*/

t_stat dl_attach (CVPTR cvptr, UNIT *uptr, char *cptr)
{
t_stat result;

result = attach_unit (uptr, cptr);                          /* attach the unit */

if (result == SCPE_OK)                                      /* if the attach succeeded */
  result = dl_load_unload (cvptr, uptr, TRUE);              /*   then load the heads */

dl_set_timing (cvptr->device->units,                        /* reestablish */
               (cvptr->device->flags & DEV_REALTIME),       /*   the delay times */
               NULL, (void *) cvptr);                       /*     pointer(s) */

return result;                                              /* return the command result status */
}


/* Detach a disc image file from a unit.

   The heads are unloaded on the drive, and the attached file, if any, is
   detached.
*/

t_stat dl_detach (CVPTR cvptr, UNIT *uptr)
{
t_stat unload, detach;

unload = dl_load_unload (cvptr, uptr, FALSE);           /* unload the heads */

if (unload == SCPE_OK || unload == SCPE_INCOMP) {       /* if the unload succeeded */
    detach = detach_unit (uptr);                        /*   then detach the unit */

    if (detach == SCPE_OK)                              /* if the detach succeeded as well */
        return unload;                                  /*   then return the unload status */
    else                                                /* otherwise */
        return detach;                                  /*   return the detach failure status */
    }

else                                                    /* otherwise the unload failed */
    return unload;                                      /*   so return the failure status */
}


/* Load or unload the drive heads.

   In hardware, a drive's heads are loaded when a disc pack is installed and the
   RUN/STOP switch is set to RUN.  The drive reports First Status when the heads
   load to indicate that the pack has potentially changed.  Setting the switch
   to STOP unloads the heads.  When the heads are unloaded, the drive reports
   Not Ready and Drive Busy status.  In both cases, the drive reports Attention
   status to the controller.  A MAC controller will clear a drive's Attention
   status and will interrupt the CPU when the drives are polled in the Idle
   Loop.  An ICD controller also clears the drive's Attention status but will
   assert a Parallel Poll Response only when the heads unload.

   In simulation, the unit must be attached, corresponding to having a disc pack
   installed in the drive, before the heads may be unloaded or loaded.  If it
   isn't, the routine returns SCPE_UNATT.  Otherwise, the UNIT_UNLOAD flag and
   the drive status are set accordingly.

   If the (MAC) controller is idle, the routine returns SCPE_INCOMP to indicate
   that the caller must then call the controller to poll for drive attention to
   complete the command.  Otherwise, it returns SCPE_OK, and the drives will be
   polled automatically when the current command or command wait completes and
   the controller is idled.


   Implementation notes:

    1. Loading or unloading the heads clears Fault and Seek Check status.

    2. If we are called during a RESTORE command, the unit's flags are not
       changed to avoid upsetting the state that was SAVEd.

    3. Unloading an active unit does not cancel the event.  This ensures that
       the controller will not hang during a transfer but instead will see the
       unloaded unit and fail the transfer with Access_Not_Ready status.
*/

t_stat dl_load_unload (CVPTR cvptr, UNIT *uptr, t_bool load)
{
if ((uptr->flags & UNIT_ATT) == 0)                      /* the unit must be attached to [un]load */
    return SCPE_UNATT;                                  /*   so return "Unit not attached" if it is not */

else if ((sim_switches & SIM_SW_REST) == 0) {           /* if we are not being called during a RESTORE command */
    uptr->Status &= ~S2_CPS;                            /*   then do a "Controller Preset" on the unit */

    if (load) {                                         /* if we are loading the heads */
        uptr->flags &= ~UNIT_UNLOAD;                    /*   then clear the unload flag */
        uptr->Status |= S2_FIRST_STATUS;                /*     and set First Status */

        if (cvptr->type != ICD)                         /* if this is not an ICD controller */
            uptr->Status |= S2_ATTENTION;               /*   then set Attention status also */
        }

    else {                                              /* otherwise we are unloading the heads */
        uptr->flags |= UNIT_UNLOAD;                     /*   so set the unload flag */
        uptr->Status |= S2_ATTENTION;                   /*     and Attention status */
        }

    tpprintf (cvptr->device, DL_DEB_CMD, "RUN/STOP switch set to %s\n",
              (load ? "RUN" : "STOP"));

    if (cvptr->type == MAC && cvptr->state == Idle_State)   /* if this is a MAC controller, and it's idle */
        return SCPE_INCOMP;                                 /*   then the controller must be called to poll the drives */
    }                                                       /* otherwise indicate that the command is complete */

return SCPE_OK;                                         /* return normal completion status to the caller */
}


/* Set the drive model.

   This validation routine is called to set the model of the disc drive
   associated with the specified unit.  The "value" parameter indicates the
   model ID, and the unit capacity is set to the size indicated.


   Implementation notes:

    1. If the drive is changed from a 7905 or 7906, which has separate head
       protect switches, to a 7920 or 7925, which has a single protect switch,
       ensure that both protect bits are set so that all heads are protected.
*/

t_stat dl_set_model (UNIT *uptr, int32 value, char *cptr, void *desc)
{
if (uptr->flags & UNIT_ATT)                             /* if the unit is currently attached */
    return SCPE_ALATT;                                  /*   then the disc model cannot be changed */

uptr->capac = drive_props [GET_MODEL (value)].words;    /* set the capacity to the new value */

if (uptr->flags & UNIT_PROT                             /* if either protect bit is set */
  && (value == UNIT_7920 || value == UNIT_7925))        /*   and the new drive is a 7920 or 7925 */
    uptr->flags |= UNIT_PROT;                           /*     then ensure that both bits are set */

return SCPE_OK;
}


/* Set or clear the write protection status.

   This validation routine is called to set the write protection status of the
   disc drive associated with the specified unit.  The "value" parameter
   indicates whether the drive is to be protected (1) or unprotected (0).

   In hardware, the 7920 and 7925 drives have a READ ONLY switch that write-
   protects all heads in the drive, and Read-Only status directly reflects the
   position of the switch.  The switch is simulated by the SET <unit> PROTECT
   and SET <unit> UNPROTECT commands.

   The 7905 and 7906 drives have separate PROTECT UPPER DISC and PROTECT LOWER
   DISC switches that protect heads 0-1 and 2 (7905) or 2-3 (7906),
   respectively, and Read-Only status reflects the position of the switch
   associated with the head currently addressed by the drive's Head Register.
   The Head Register is loaded by the controller as part of a Seek or Cold Load
   Read command, or during an auto-seek or spare track seek, if permitted by the
   file mask.  A Controller Preset command sent to a drive clears the Head
   Register, so Read-Only status reflects the PROTECT UPPER DISC switch setting.

   The SET <unit> PROTECT=(UPPER | LOWER) and SET <unit> UNPROTECT=(UPPER |
   LOWER) simulation commands are provided to protect or unprotect the upper or
   lower heads individually.  If the option values are omitted, e.g., SET <unit>
   PROTECT, then both upper and lower heads are (un)protected.
*/

t_stat dl_set_protect (UNIT *uptr, int32 value, char *cptr, void *desc)
{
const uint32 model = uptr->flags & UNIT_MODEL;
char gbuf [CBUFSIZE];

if (cptr == NULL)                                       /* if there are no arguments */
    if (value)                                          /*   then if setting the protection status */
        uptr->flags |= UNIT_PROT;                       /*     then protect all heads */
    else                                                /*   otherwise */
        uptr->flags &= ~UNIT_PROT;                      /*     unprotect all heads */

else if (*cptr == '\0')                                 /* otherwise if the argument is empty */
    return SCPE_MISVAL;                                 /*   then reject the command */

else if (model == UNIT_7920 || model == UNIT_7925)      /* otherwise if this is a 7920 or 7925 */
    return SCPE_ARG;                                    /*   then the heads cannot be protected separately */

else {                                                  /* otherwise a 7905/06 argument is present */
    cptr = get_glyph (cptr, gbuf, ';');                 /* get the argument */

    if (strcmp ("LOWER", gbuf) == 0)                    /* if the LOWER option was specified */
        if (value)                                      /*   then if setting the protection status */
            uptr->flags |= UNIT_PROT_L;                 /*     then set the protect lower disc flag */
        else                                            /*   otherwise */
            uptr->flags &= ~UNIT_PROT_L;                 /*    clear the protect lower disc flag */

    else if (strcmp ("UPPER", gbuf) == 0)               /* otherwise if the UPPER option was specified */
        if (value)                                      /*   then if setting the protection status */
            uptr->flags |= UNIT_PROT_U;                 /*     then set the protect upper disc flag */
        else                                            /*   otherwise */
            uptr->flags &= ~UNIT_PROT_U;                /*    clear the protect upper disc flag */

    else                                                /* otherwise some other argument was given */
        return SCPE_ARG;                                /*   so report the error */
    }

return SCPE_OK;
}


/* Show the write protection status.

   This display routine is called to show the write protection status of the
   disc drive associated with the specified unit.  The "value" and "desc"
   parameters are unused.

   The unit flags contain two bits that indicate write protection for heads 0-1
   and heads 2-n.  If both bits are clear, the drive is unprotected.  A 7905 or
   7906 drive may have one or the other or both bits set, indicating that the
   upper, lower, or both platters are protected.  A 7920 or 7925 will have both
   bits set, indicating that the entire drive is protected.
*/

t_stat dl_show_protect (FILE *st, UNIT *uptr, int32 value, void *desc)
{
const uint32 model = uptr->flags & UNIT_MODEL;

if ((uptr->flags & UNIT_PROT) == 0)                     /* if the protection flags are clear */
    fputs ("unprotected", st);                          /*   then report the disc as unprotected */

else if (model == UNIT_7905 || model == UNIT_7906)      /* otherwise if this is a 7905/06 */
    if ((uptr->flags & UNIT_PROT) == UNIT_PROT_L)       /*   then if only the lower disc protect flag is set */
        fputs ("lower protected", st);                  /*     then report it */
    else if ((uptr->flags & UNIT_PROT) == UNIT_PROT_U)  /*   otherwise if only the upper disc protect flag is set */
        fputs ("upper protected", st);                  /*     then report it */
    else                                                /*   otherwise both flags are set */
        fputs ("lower/upper protected", st);            /*     so report them */

else                                                    /* otherwise it's a 7920/25 */
    fputs ("protected", st);                            /*   so either flag set indicates a protected disc */

return SCPE_OK;
}


/* Set or clear the diagnostic override table.

   This validation routine is called to set or clear the diagnostic override
   table associated with the specified unit.  The "value" parameter is either
   the positive maximum table entry count if an entry is to be added, or zero if
   the table is to be cleared, and "desc" is a pointer to the controller.

   If the CPU interface declares a diagnostic override table, it will specify
   the table when initializing the controller variables structure with the
   CNTLR_INIT macro.  This sets the "dop_base" pointer in the structure to point
   at the table.  If the interface does not declare a table, the pointer will be
   NULL.

   If the table is present, new entries may be added with this command:

     SET <dev> DIAG=<cylinder>;<head>;<sector>;<opcode>;<spd>;<status>

   The cylinder, head, and sector values are entered as decimal numbers, the
   opcode and status values are entered as octal numbers, and the SPD value is
   specified as any combination of the letters "S", "P", or "D".

   If a command specifies the opcode value as Request Syndrome (15) and the
   status value as Correctable Data Error (17), then four additional values must
   be specified as part of the command line above:

      ;<displacement>;<syndrome 1>;<syndrome 2>;<syndrome 3>

   The displacement value is entered as a decimal number, and the three syndrome
   values are entered as octal numbers.

   Entering SET <dev> DIAG by itself resets the current entry pointer to the
   first table entry.  Entering SET <dev> NODIAG clears the table.

   If the override table was not declared by the CPU interface, the routine
   returns "Command not allowed."  If SET NODIAG was entered, the "value"
   parameter will be -1, and the table will be cleared by storing the special
   value DL_OVEND into the first table entry.  Otherwise, if SET DIAG is
   entered, the table pointer is reset.  However, if the table is already empty,
   the routine returns "Missing value" to indicate that the table must be
   populated first.

   If a new entry is to be added, the "value" parameter will indicate the size
   of the table in entries.  If no free entry exists, the routine returns
   "Memory exhausted".  Otherwise, the supplied parameters are parsed and
   entered into the table.  An array of maximum allowed values and radixes are
   used during parsing to validate each parameter.  The non-numeric SPD
   parameter is parsed separately.  If, after parsing the first six parameters,
   the opcode and status are Request Syndrome and Correctable Data Error,
   respectively, then four more parameters are sought.  These are stored into
   the following table entry.


   Implementation notes:

    1. Each maximum array element value limits the absolute value of its
       corresponding numeric parameter.  Negative values may be specified for
       unsigned values; no error is returned, and these simply will never match
       their intended controller values.

    2. Each radix array element value is used to parse its corresponding
       parameter.  A radix of 0 is used to indicate the S/P/D parameter, which
       is parsed alphabetically rather than numerically.

    3. One table entry is always reserved for the end-of-table value, so the
       number of configurable entries is one less than the defined table size.
*/

t_stat dl_set_diag (UNIT *uptr, int32 value, char *cptr, void *desc)
{
typedef struct {
    t_value  max;                               /* maximum allowed value */
    uint32   radix;                             /* numeric parsing radix */
    } PARSER_PROP;

static const PARSER_PROP param [] = {
/*    maximum value  radix */
/*    -------------  ----- */
    {         822,     10  },                   /* cylinder */
    {           8,     10  },                   /* head */
    {          63,     10  },                   /* sector */
    { LAST_OPCODE,      8  },                   /* opcode */
    {           0,      0  },                   /* SPD */
    { LAST_STATUS,      8  },                   /* status */
    {         135,     10  },                   /* displacement */
    {    D16_UMAX,      8  },                   /* syndrome 1 */
    {    D16_UMAX,      8  },                   /* syndrome 2 */
    {    D16_UMAX,      8  }                    /* syndrome 3 */
    };

const CVPTR cvptr = (CVPTR) desc;
DIAG_ENTRY *entry;
uint32 pidx, params [10];
t_stat status;
char gbuf [CBUFSIZE];

if (cvptr->dop_base == NULL)                            /* if the override array is not present */
    return SCPE_NOFNC;                                  /*   then the command is not allowed */

else if (value == 0)                                    /* otherwise if this is a NODIAG call */
    if (cptr != NULL)                                   /*   then if something follows the keyword */
        return SCPE_2MARG;                              /*     then report an error */

    else {                                              /* otherwise the command is valid */
        cvptr->dop_index = -1;                          /*   so clear the current entry pointer */
        cvptr->dop_base->cylinder = DL_OVEND;           /*     and mark the first entry as the end */
        }

else if (cptr == NULL)                                  /* otherwise if DIAG is by itself */
    if (cvptr->dop_base->cylinder == DL_OVEND)          /*   then if there are no entries in the table */
        return SCPE_MISVAL;                             /*     then one must be entered first */
    else                                                /*   otherwise */
        cvptr->dop_index = 0;                           /*     reset the current pointer to the first entry */

else if (*cptr == '\0')                                 /* otherwise if there are no parameters */
    return SCPE_MISVAL;                                 /*   then report a missing value */

else {                                                  /* otherwise at least one parameter is present */
    for (entry = cvptr->dop_base;                       /* find the */
         entry->cylinder != DL_OVEND && value > 0;      /*   last entry */
         entry++, value--);                             /*     in the current table */

    if (value <= 1)                                     /* if there's not enough room to add a new entry */
        return SCPE_MEM;                                /*   then report the error to the user */

    else {                                              /* otherwise there's at least one free entry */
        for (pidx = 0; pidx < 10; pidx++) {             /*   so assume a full set of arguments are present */
            if (*cptr == '\0')                          /* if the next argument is not there */
                return SCPE_2FARG;                      /*   then report it missing */

            if (param [pidx].radix == 0) {              /* if this is the SPD argument */
                params [pidx] = 0;                      /*   then parse it specially */

                while (*cptr != ';' && *cptr != '\0') { /* look for multiple arguments */
                    if (*cptr == 'S')                   /* if it's an S */
                        params [pidx] |= CM_SPARE;      /*   then set the spare bit */
                    else if (*cptr == 'P')              /* or if it's a P */
                        params [pidx] |= CM_PROTECTED;  /*   then set the protected bit */
                    else if (*cptr == 'D')              /* or if it's a D */
                        params [pidx] |= CM_DEFECTIVE;  /*   then set the defective bit */
                    else                                /* any other character */
                        return SCPE_ARG;                /*   results in an invalid argument error */

                    cptr++;                             /* point at the next character and continue */
                    }                                   /*   until a separator or terminator is seen */

                if (*cptr == ';')                       /* if a separator was seen */
                    cptr++;                             /*   then move past it */

                status = SCPE_OK;                       /* reassure the compiler that status is not uninitialized */
                }

            else {                                      /* otherwise parse a numeric argument */
                cptr = get_glyph (cptr, gbuf, ';');     /* get the argument */

                if (gbuf [0] == '-') {                  /* if the argument is negative */
                    gbuf [0] = ' ';                     /*   then clear the sign */
                    params [pidx] =                     /*     and negate the resulting value */
                      (uint32) NEG16 (get_uint (gbuf, param [pidx].radix,
                                                param [pidx].max, &status));
                    }

                else                                    /* otherwise the argument is unsigned */
                    params [pidx] =                     /*   so use the value as is */
                      (uint32) get_uint (gbuf, param [pidx].radix,
                                         param [pidx].max, &status);
                }

            if (status != SCPE_OK)                      /* if an error occurred */
                return status;                          /*   then return the parsing status */

            if (pidx == 5                               /* if we have parsed the status */
              && (params [3] != Request_Syndrome        /*   and this is not a Request Syndrome entry */
              || params [5] != Correctable_Data_Error)) /*     with Correctable Data Error status */
                break;                                  /*       then no more parameters are expected */
            }

        if (*cptr != '\0')                              /* if more characters are present */
            return SCPE_2MARG;                          /*   then report the excess */

        else if (pidx == 10 && value <= 2)              /* otherwise if we have syndrome values but no space */
            return SCPE_MEM;                            /*   the report that we can't store them */

        entry->cylinder = params [0];                   /* store the */
        entry->head     = params [1];                   /*   first set */
        entry->sector   = params [2];                   /*     of parameter values */
        entry->opcode   = (CNTLR_OPCODE) params [3];    /*       in the */
        entry->spd      = params [4];                   /*         first available */
        entry->status   = (CNTLR_STATUS) params [5];    /*            empty entry */

        if (pidx == 10) {                               /* if syndrome values were present */
            entry++;                                    /*   then store them in the next available entry */

            entry->spd      = params [6];               /* save the displacement */
            entry->cylinder = params [7];               /*   and the */
            entry->head     = params [8];               /*     three syndrome */
            entry->sector   = params [9];               /*       values */
            entry->opcode   = Request_Syndrome;         /* identify the entry */
            entry->status   = Correctable_Data_Error;   /*   by opcode and status */
            }

        entry++;                                        /* point at the next available entry */
        entry->cylinder = DL_OVEND;                     /*   and mark it as the end of the list */

        cvptr->dop_index = 0;                           /* reset the current pointer to the start of the list */
        }
    }

return SCPE_OK;
}


/* Show the diagnostic override table.

   This display routine is called to show the contents of the diagnostic
   override table.  The "value" parameter is either the positive maximum table
   entry count if the routine was invoked by a SHOW <dev> DIAG command, or -1 if
   the routine was invoked by a SHOW <dev> command.  The "desc" parameter is a
   pointer to the controller.

   If the override table was not declared by the CPU interface, the routine
   returns "Command not allowed."  Otherwise, if the table is empty, the routine
   prints "override disabled".  If the table is populated, then the routine
   prints "override enabled" if it was invoked as part of a general SHOW for the
   device, or it prints the individual table entries if it was invoked as SHOW
   <dev> DIAG.

   Entries are printed in tabular form with the columns corresponding to the SET
   DIAG parameters, except that the opcode and status fields are decoded.  A
   Request Syndrome command entry with Correctable Data Error status is followed
   by an indented second line containing the displacement and syndrome values.
   Numeric values are printed in the same radix as is used to enter them.


   Implementation notes:

    1. The Request Syndrome displacement parameter is a 16-bit signed value
       contained in a 32-bit unsigned array element.  To print it properly, we
       convert the latter to a 16-bit signed value and then sign-extend to "int"
       size for fprintf.

    2. The explicit use of "const CNTLR_VARS *" is necessary to declare a
       pointer to a constant structure.  Using "const CVPTR" declares a constant
       pointer instead.
*/

t_stat dl_show_diag (FILE *st, UNIT *uptr, int32 value, void *desc)
{
const CNTLR_VARS *cvptr = (const CNTLR_VARS *) desc;    /* the controller pointer is supplied */
DIAG_ENTRY *entry;

if (cvptr->dop_base == NULL)                            /* if the table isn't defined */
    return SCPE_NOFNC;                                  /*   then the command is illegal */

else if (cvptr->dop_index < 0) {                        /* otherwise if overrides are currently disabled */
    fputs ("override disabled", st);                    /*   then report it */

    if (value > 0)                                      /* if we were invoked by a SHOW DIAG command */
        fputc ('\n', st);                               /*   then we must add the line terminator */
    }

else if (value < 0)                                     /* otherwise if we were invoked by a SHOW <dev> command */
    fputs ("override enabled", st);                     /*   then print the table status instead of the details */

else for (entry = cvptr->dop_base;                      /* otherwise print each table entry */
          entry->cylinder != DL_OVEND && value > 0;     /*   until the end-of-table marker or count exhaustion */
          entry++, value--) {
    fprintf (st, "%3d  %1d  %2d  %*s  %c%c%c  %*s\n",   /* print the entry */
             entry->cylinder, entry->head, entry->sector,
             - OPCODE_LENGTH, dl_opcode_name (cvptr->type, entry->opcode),
             (entry->spd & CM_SPARE     ? 'S' : ' '),
             (entry->spd & CM_PROTECTED ? 'P' : ' '),
             (entry->spd & CM_DEFECTIVE ? 'D' : ' '),
             - STATUS_LENGTH, dl_status_name (entry->status));

    if (entry->opcode == Request_Syndrome               /* if the current entry is a syndrome request */
      && entry->status == Correctable_Data_Error) {     /*   for a correctable data error */
        entry++;                                        /*     then the next entry contains the values */
        value = value - 1;                              /* drop the entry count to account for it */

        fprintf (st, "            %3d  %06o  %06o  %06o\n", /* print the displacement and syndrome values */
                 (int) INT16 (entry->spd),
                 entry->cylinder, entry->head, entry->sector);
        }
    }

return SCPE_OK;
}


/* Set the controller timing mode.

   This validation routine is called to set the timing mode for the disc
   subsystem.  As this is an extended MTAB call, the "uptr" parameter points to
   the unit array of the device.  The "value" parameter is set non-zero to use
   realistic timing and 0 to use fast timing.  For a MAC controller, the "desc"
   parameter is a pointer to the controller.  For ICD controllers, the "desc"
   parameter is a pointer to the first element of the controller array.  There
   must be one controller for each unit defined by the device associated with
   the controllers.  The "cptr" parameter is not used.

   If fast timing is selected, the controller's timing pointer is set to the
   fast timing pointer supplied by the interface when the controller was
   initialized.  If real timing is selected, the table of real times is searched
   for an entry whose controller type matches the supplied controller.  MAC and
   ICD controllers support several drive models with identical timing
   characteristics.  However, CS/80 controllers allow mixing drives with
   different timing on a single CPU interface.  For these, the drive type must
   match as well.  If a match is found, the controller's timing pointer is set
   to the associated real-time entry.  Otherwise, the routine returns SCPE_IERR.

   Non-MAC controllers are dedicated per-drive, so "desc" points not at a single
   controller instance but at an array of controller instances.  The timing mode
   is common to every controller in the array.
*/

t_stat dl_set_timing (UNIT *uptr, int32 value, char *cptr, void *desc)
{
CVPTR  cvptr = (CVPTR) desc;                            /* the controller pointer is supplied */
const  DELAY_PROPS *dpptr;
DRIVE_TYPE model;
uint32 delay, cntlr_count;

if (cvptr->type == MAC)                                 /* if this is a MAC controller */
    cntlr_count = 1;                                    /*   then there is one controller for all units */
else                                                    /* otherwise */
    cntlr_count = cvptr->device->numunits;              /*   there is one controller per unit */

while (cntlr_count--) {                                 /* set each controller's timing mode */
    if (value) {                                        /* if realistic timing is requested */
        model = GET_MODEL (uptr->flags);                /*   then get the drive model from the current unit */
        dpptr = real_times;                             /*     and reset the real-time delay table pointer */

        for (delay = 0; delay < DELAY_COUNT; delay++)   /* search for the correct set of times */
            if (dpptr->type == cvptr->type              /* if the controller types match */
              && (dpptr->drive == HP_All                /*   and all drive times are the same */
              || dpptr->drive == model)) {              /*     or the drive types match as well */
                cvptr->dlyptr = dpptr;                  /*       then use this set of times */
                break;
                }
            else                                        /* otherwise */
                dpptr++;                                /*   point at the next array element */

        if (delay == DELAY_COUNT)                       /* if the model was not found in the table */
            return SCPE_IERR;                           /*   then report an internal (impossible) error */
        else                                            /* otherwise */
            cvptr->device->flags |= DEV_REALTIME;       /*   set the real time flag */
        }

    else {                                              /* otherwise fast timing is requested */
        cvptr->device->flags &= ~DEV_REALTIME;          /*   so clear the real time flag */
        cvptr->dlyptr = cvptr->fastptr;                 /*     and set the delays to the FASTTIME settings */
        }

    cvptr++;                                            /* point at the next controller */
    uptr++;                                             /*   and corresponding unit for non-MAC controllers */
    }

return SCPE_OK;
}


/* Show the controller timing mode

   This display routine is called to show the timing mode for the disc
   subsystem.  The "value" parameter is unused; the "desc" parameter is a
   pointer to the controller.


   Implementation notes:

    1. The explicit use of "const CNTLR_VARS *" is necessary to declare a
       pointer to a constant structure.  Using "const CVPTR" declares a constant
       pointer instead.
*/

t_stat dl_show_timing (FILE *st, UNIT *uptr, int32 value, void *desc)
{
const CNTLR_VARS *cvptr = (const CNTLR_VARS *) desc;    /* the controller pointer is supplied */

if (cvptr->device->flags & DEV_REALTIME)                /* if the real time flag is set */
    fputs ("realistic timing", st);                     /*   then we're using realistic timing */
else                                                    /* otherwise */
    fputs ("fast timing", st);                          /*   we're using optimized timing */

return SCPE_OK;
}



/* Disc library local controller routines */


/* Start or stop the command/parameter wait timer.

   A MAC controller uses a 1.74 second timer to ensure that it does not wait
   forever for a non-responding disc drive or CPU interface.  In simulation, MAC
   interfaces supply an additional controller unit that is activated when the
   command or parameter wait timer is started and cancelled when the timer is
   stopped.

   ICD interfaces do not use the wait timer or supply an additional unit.


   Implementation notes:

    1. Absolute activation is used because the timer is restarted between
       parameter word transfers.
*/

static void wait_timer (CVPTR cvptr, FLIP_FLOP action)
{
if (cvptr->type == MAC)                                 /* if this a MAC controller */
    if (action == SET)                                  /*  then if the timer is to be set */
        sim_activate_abs (CNTLR_UPTR, CNTLR_TIMEOUT);   /*    then activate the controller unit */

    else {                                              /*  otherwise the timer is to be stopped */
        sim_cancel (CNTLR_UPTR);                        /*    so cancel the unit */
        CNTLR_UPTR->Phase = Idle_Phase;                 /*      and idle the controller unit */
        }
return;
}


/* Idle the controller.

   The command wait timer is turned off, the status is reset, and the controller
   is returned to the idle state (Poll Loop).
*/

static void idle_controller (CVPTR cvptr)
{
wait_timer (cvptr, CLEAR);                              /* stop the command wait timer */

cvptr->status = Normal_Completion;                      /* the Poll Loop clears the status */
cvptr->state  = Idle_State;                             /* idle the controller */
return;
}


/* End the current command.

   The currently executing command is completed with the supplied status.  If
   the command completed normally, and it returns to the Poll Loop, the
   controller is idled, and the wait timer is cancelled.  Otherwise, the
   controller enters the Wait Loop, and the wait timer is started.  If the
   command had accessed a drive unit, the unit is idled.  Also, for a MAC
   controller, the controller unit is idled as well.
*/

static void end_command (CVPTR cvptr, UNIT *uptr, CNTLR_STATUS status)
{
cvptr->status = status;                                 /* set the command result status */

if (status == Normal_Completion                         /* if the command completed normally */
  && cmd_props [cvptr->opcode].idle_at_end) {           /*   and this command idles the controller */
    cvptr->state = Idle_State;                          /*     then set idle status */
    wait_timer (cvptr, CLEAR);                          /*       and stop the command wait timer */
    }

else {                                                  /* otherwise */
    cvptr->state = Wait_State;                          /*   the controller waits for a new command */
    wait_timer (cvptr, SET);                            /*     so start the command wait timer */

    if (uptr)                                           /* if the command accessed a drive */
        uptr->Phase = Idle_Phase;                       /*   then idle it */

    if (cvptr->type == MAC)                             /* if this is a MAC controller */
        CNTLR_UPTR->Phase = Idle_Phase;                 /*   then idle the controller unit as well */
    }

if (cmd_props [cvptr->opcode].transfer_size > 0)
    tpprintf (cvptr->device, DL_DEB_CMD, (cvptr->opcode == Initialize
                                            ? "Unit %u Initialize %s for %u words (%u sector%s)\n"
                                            : "Unit %u %s for %u words (%u sector%s)\n"),
              CM_UNIT (cvptr->spd_unit),
              (cvptr->opcode == Initialize
                 ? fmt_bitset (cvptr->spd_unit, initialize_format)
                 : opcode_name [cvptr->opcode]),
              cvptr->count,
              cvptr->count / cmd_props [cvptr->opcode].transfer_size + (cvptr->length > 0),
              (cvptr->count <= cmd_props [cvptr->opcode].transfer_size ? "" : "s"));

tpprintf (cvptr->device, DL_DEB_INCO, "Unit %u %s command completed with %s status\n",
          CM_UNIT (cvptr->spd_unit), opcode_name [cvptr->opcode],
          dl_status_name (cvptr->status));

return;
}


/* Start a read operation on the current sector.

   This routine is called at the end of the rotate phase to begin a read
   operation.  The current sector given by the controller address is read from
   the disc image file into the sector buffer in preparation for data transfer
   to the CPU.  If the end of the track had been reached, and the file mask
   permits, an auto-seek is scheduled instead to allow the read to continue.
   The routine returns TRUE if the data is ready to be transferred and FALSE if
   it is not (due to command completion, an error, or an auto-seek that must
   complete first).

   On entry, the end-of-data flag is checked.  If it is set, the current read
   command is completed.  Otherwise, the buffer data offset and verify options
   are set up.  For a Read Full Sector, the sync word is set from the controller
   type, and dummy cylinder and head-sector words are generated from the current
   location (as would be the case in the absence of track sparing).

   The image file is positioned to the correct sector in preparation for
   reading.  If the positioning requires a permitted seek, it is scheduled, and
   the routine returns with the Seek_Phase set to wait for seek completion
   before resuming the read (when the seek completes, the service routine will
   be entered, and we will be called again; this time, the end-of-cylinder flag
   will be clear and positioning will succeed).  If positioning resulted in an
   error, the current read is terminated with the error status set.

   If positioning succeeded within the same track, the sector image is read into
   the buffer at an offset determined by the operation (Read Full Sector leaves
   room at the start of the buffer for the sector header).  If the image read
   failed with a host file system error, it is reported to the simulation
   console and the read ends with an Uncorrectable Data Error.  If it succeeded
   but did not return a full sector, the remainder of the buffer is padded with
   zeros.

   If the image was read correctly, the operation phase is set for the data
   transfer, the index of the first word to transfer is set, and the routine
   returns TRUE to begin the data transfer.


   Implementation notes:

    1. This routine changes the unit phase state as follows:

         Rotate_Phase => Idle_Phase if EOD or error (returns FALSE)
         Rotate_Phase => Seek_Phase if auto-seek (returns FALSE)
         Rotate_Phase => Data_Phase otherwise (returns TRUE)

    2. The position_sector routine sets up the data phase if it succeeds or the
       seek phase if a seek is required.
*/

static t_bool start_read (CVPTR cvptr, UNIT *uptr, CNTLR_FLAG_SET flags)
{
uint32 count, offset;
const CNTLR_OPCODE opcode = (CNTLR_OPCODE) uptr->Opcode;

if (flags & EOD) {                                      /* if the end of data is indicated */
    end_command (cvptr, uptr, Normal_Completion);       /*   then complete the command */
    return FALSE;                                       /*     and end the current operation */
    }

if (opcode == Read_Full_Sector) {                       /* if we are starting a Read Full Sector command */
    if (cvptr->type == MAC)                             /*   then if this is a MAC controller */
        cvptr->buffer [0] = 0100376;                    /*     indicate that ECC support is valid */
    else                                                /*   otherwise */
        cvptr->buffer [0] = 0100377;                    /*     indicate that ECC support is not available */

    set_address (cvptr, 1);                             /* set the current address into buffer words 1-2 */
    offset = 3;                                         /*   and start the data after the header */
    }

else                                                    /* otherwise it's a normal read command */
    offset = 0;                                         /*   so data starts at the beginning of the buffer */

if (position_sector (cvptr, uptr) == FALSE)             /* position the sector; if it was not */
    return FALSE;                                       /*   then a seek is in progress or an error occurred */

tpprintf (cvptr->device, DL_DEB_INCO, "Unit %d %s from cylinder %u head %u sector %u\n",
          (int32) (uptr - cvptr->device->units), opcode_name [opcode],
          uptr->Cyl, cvptr->head, cvptr->sector);

count = sim_fread (cvptr->buffer + offset,              /* read the sector from the image */
                   sizeof (DL_BUFFER),                  /*   into the sector buffer */
                   WORDS_PER_SECTOR, uptr->fileref);

if (ferror (uptr->fileref)) {                           /* if a host file system error occurred */
    io_error (cvptr, uptr, Uncorrectable_Data_Error);   /*   then report it to the simulation console */
    return FALSE;                                       /*     and terminate with Uncorrectable Data Error status */
    }

cvptr->length = cmd_props [opcode].transfer_size;       /* set the appropriate transfer length */
cvptr->index = 0;                                       /*   and reset the data index */

for (count = count + offset; count < cvptr->length; count++)    /* pad the sector as needed */
    cvptr->buffer [count] = 0;                                  /*   e.g., if reading from a new file */

return TRUE;                                            /* the read was successfully started */
}


/* Finish a read operation on the current sector.

   This routine is called at the end of the intersector phase to finish a read
   operation.  Command termination conditions are checked, and the next sector
   is addressed in preparation for the read to continue.

   On entry, the diagnostic override status is checked.  If it is set, then the
   read is terminated with the indicated status.  Otherwise, the data overrun
   flag is checked.  If it is set, the read is terminated with an error.
   Otherwise, the next sector is addressed.

   If the end-of-data flag is set, the current read is completed.  Otherwise,the
   rotate phase is set up in preparation for the next sector read.


   Implementation notes:

    1. This routine changes the unit phase state as follows:

         Intersector_Phase => Idle_Phase if EOD or error
         Intersector_Phase => Rotate_Phase otherwise

    2. The HP 1000 CPU indicates the end of a read data transfer to an ICD
       controller by untalking the drive.  The untalk is done by the driver as
       soon as the DCPC completion interrupt is processed.  However, the time
       from the final DCPC transfer through driver entry to the point where the
       untalk is asserted on the bus varies from 80 instructions (RTE-6/VM with
       OS microcode and the buffer in the system map) to 152 instructions
       (RTE-IVB with the buffer in the user map).  The untalk must occur before
       the start of the next sector, or the drive will begin the data transfer.

       Normally, this is not a problem, as the driver clears the FIFO of any
       received data after DCPC completion.  However, if the read terminates
       after the last sector of a track, and accessing the next sector would
       require an intervening seek, and the file mask disables auto-seeking or
       an enabled seek would move the positioner beyond the drive limits, then
       the controller will indicate an End of Cylinder error if the untalk does
       not arrive before the seek is initiated.

       The RTE driver (DVA32) and various utilities that manage the disc
       directly (e.g., SWTCH) do not appear to account for these bogus errors,
       so the ICD controller hardware must avoid them in some unknown manner.
       We work around the issue by extending the intersector delay to allow time
       for a potential untalk whenever the next access would otherwise fail.

       Note that this issue does not occur with writes because DCPC completion
       asserts EOI concurrently with the final data byte to terminate the
       command explicitly.

       Note also that the delay is a fixed number of instructions, regardless of
       timing mode, to ensure that the CPU makes it through the driver code to
       output the untalk command.
*/

static void end_read (CVPTR cvptr, UNIT *uptr, CNTLR_FLAG_SET flags)
{
uint32 bound;

if (cvptr->status != Normal_Completion)                 /* if a diagnostic override is present */
    end_command (cvptr, uptr, cvptr->status);           /*   then report the indicated status */

else if (flags & OVRUN)                                 /* otherwise if a read overrun occurred */
    end_command (cvptr, uptr, Data_Overrun);            /*   then terminate the command with an error */

else {                                                  /* otherwise the read succeeded */
    next_sector (cvptr, uptr);                          /*   so address the next sector */

    if (flags & EOD)                                    /* if the end of data is indicated */
        end_command (cvptr, uptr, Normal_Completion);   /*   then complete the command */

    else {                                              /* otherwise reading continues */
        uptr->Phase = Rotate_Phase;                     /*   so set up the unit for the rotate phase */
        uptr->wait = cvptr->dlyptr->intersector_gap;    /*     with a delay for the intersector time */

        if (cvptr->eoc == SET && cvptr->type == ICD) {      /* if a seek will be required on an ICD controller */
            if ((cvptr->file_mask & CM_AUTO_SEEK_EN) == 0)  /*   then if auto-seek is disabled */
                bound = cvptr->cylinder;                    /*     then the bound is the current cylinder */
            else if (cvptr->file_mask & CM_DECR_SEEK)       /*   otherwise if a decremental seek is enabled */
                bound = 0;                                  /*     then the bound is cylinder 0 */
            else                                            /*   otherwise the enabled bound is the last cylinder */
                bound = drive_props [GET_MODEL (uptr->flags)].cylinders - 1;

            if (cvptr->cylinder == bound)               /* if the positioner is already at the bound */
                uptr->wait = UNTALK_DELAY;              /*   then the seek will fail; delay to allow CPU to untalk */
            }
        }
    }

return;
}


/* Start a write operation on the current sector.

   This routine is called at the end of the rotate phase to begin a write
   operation.  The current sector indicated by the controller address is
   positioned for writing from the sector buffer to the disc image file after
   data transfer from the CPU.  If the end of the track had been reached, and
   the file mask permits, an auto-seek is scheduled instead to allow the write
   to continue.   The routine returns TRUE if the data is ready to be
   transferred and FALSE if it is not (due to an error or an auto-seek that must
   complete first).

   On entry, if writing is not permitted, or formatting is required but not
   enabled, the command is terminated with an error.  Otherwise, the disc image
   file is positioned to the correct sector in preparation for writing.

   If the positioning requires a permitted seek, it is scheduled, and the
   routine returns with the Seek_Phase set to wait for seek completion before
   resuming the write (when the seek completes, the service routine will be
   entered, and we will be called again; this time, the end-of-cylinder flag
   will be clear and positioning will succeed).  If positioning resulted in an
   error, the current write is terminated with the error status set.

   If positioning succeeded within the same track, the operation phase is set
   for the data transfer, the index of the first word to transfer is set, and
   the routine returns TRUE to begin the data transfer.


   Implementation notes:

    1. This routine changes the unit phase state as follows:

         Rotate_Phase => Idle_Phase if write protected or error (returns FALSE)
         Rotate_Phase => Seek_Phase if auto-seek (returns FALSE)
         Rotate_Phase => Data_Phase otherwise (returns TRUE)

    2. The position_sector routine sets up the data phase if it succeeds or the
       seek phase if a seek is required.
*/

static t_bool start_write (CVPTR cvptr, UNIT *uptr)
{
const CNTLR_OPCODE opcode = (CNTLR_OPCODE) uptr->Opcode;

if (opcode == Write                                     /* if this is a Write command */
  && cvptr->spd_unit & CM_PROTECTED                     /*   and the track is protected */
  && (uptr->flags & UNIT_FMT) == 0)                     /*     but the FORMAT switch is not set */
    end_command (cvptr, uptr, Protected_Track);         /*       then fail with a protection error */

else if (uptr->Status & S2_READ_ONLY                    /* otherwise if the unit is write protected */
  || opcode != Write && (uptr->flags & UNIT_FMT) == 0)  /*   or the FORMAT switch must be set but is not */
    end_command (cvptr, uptr, Status_2_Error);          /*     then fail with a status error */

else if (position_sector (cvptr, uptr) == TRUE) {       /* otherwise if positioning the sector succeeded */
    cvptr->length = cmd_props [opcode].transfer_size;   /*   then set the appropriate transfer length */
    cvptr->index = 0;                                   /*     and reset the data index */

    tpprintf (cvptr->device, DL_DEB_INCO, "Unit %d %s to cylinder %u head %u sector %u\n",
              (int32) (uptr - cvptr->device->units), opcode_name [opcode],
              uptr->Cyl, cvptr->head, cvptr->sector);

    return TRUE;                                        /* the write was successfully started */
    }

return FALSE;                                           /* otherwise an error occurred or a seek is required */
}


/* Finish a write operation on the current sector.

   This routine is called at the end of the intersector phase to finish a write
   operation.  The current sector is written from the sector buffer to the disc
   image file at the current file position.  The next sector address is then
   updated to allow writing to continue.

   On entry, the drive is checked to ensure that it is ready for the write.
   Then the sector buffer is padded appropriately if a full sector of data was
   not transferred.  The buffer is written to the disc image file at the
   position corresponding to the controller address as set when the sector was
   started.  The write begins at a buffer offset determined by the command (a
   Write Full Sector has three header words at the start of the buffer that are
   not written to the disc image).

   If the image write failed with a host file system error, it is reported to
   the simulation console and the write ends with an Uncorrectable Data Error.
   If it succeeded, the diagnostic override status is checked.  If it is set,
   then the write is terminated with the indicated status.  Otherwise, the data
   overrun flag is checked.  If it is set, the write is terminated with an
   error.  Otherwise, the next sector is addressed.  If the end-of-data flag is
   set, the current write is completed.  Otherwise,the rotate phase is set up in
   preparation for the next sector write.


   Implementation notes:

    1. This routine changes the unit phase state as follows:

         Intersector_Phase => Idle_Phase if EOD or error
         Intersector_Phase => Rotate_Phase otherwise

    2. A partial sector is filled either with octal 177777 words (ICD) or copies
       of the last word (MAC), per page 7-10 of the ICD/MAC Disc Diagnostic
       manual.
*/

static void end_write (CVPTR cvptr, UNIT *uptr, CNTLR_FLAG_SET flags)
{
uint32 count;
DL_BUFFER pad;
const CNTLR_OPCODE opcode = (CNTLR_OPCODE) uptr->Opcode;
const uint32 offset = (opcode == Write_Full_Sector ? 3 : 0);

if (uptr->flags & UNIT_UNLOAD) {                        /* if the drive is not ready */
    end_command (cvptr, uptr, Access_Not_Ready);        /*   then terminate the command */
    return;                                             /*     with an access error */
    }

if (cvptr->index < WORDS_PER_SECTOR + offset) {         /* if a partial sector was transferred */
    if (cvptr->type == ICD)                             /*   then an ICD controller */
        pad = D16_UMAX;                                 /*     pads the sector with -1 words */
    else                                                /*   whereas a MAC controller */
        pad = cvptr->buffer [cvptr->index - 1];         /*     pads with the last word written */

    for (count = cvptr->index; count < WORDS_PER_SECTOR + offset; count++)
        cvptr->buffer [count] = pad;                    /* pad the sector buffer as needed */
    }

sim_fwrite (cvptr->buffer + offset, sizeof (DL_BUFFER), /* write the sector to the file */
            WORDS_PER_SECTOR, uptr->fileref);

if (ferror (uptr->fileref))                             /* if a host file system error occurred, then report it */
    io_error (cvptr, uptr, Uncorrectable_Data_Error);   /*    and terminate with Uncorrectable Data Error status */

else if (cvptr->status != Normal_Completion)            /* otherwise if a diagnostic override is present */
    end_command (cvptr, uptr, cvptr->status);           /*   then report the indicated status */

else if (flags & OVRUN)                                 /* otherwise if a write overrun occurred */
    end_command (cvptr, uptr, Data_Overrun);            /*   then terminate the command with an error */

else {                                                  /* otherwise the write succeeded */
    next_sector (cvptr, uptr);                          /*   so address the next sector */

    if (flags & EOD)                                    /* if the end of data is indicated */
        end_command (cvptr, uptr, Normal_Completion);   /*   then complete the command */

    else {                                              /* otherwise writing continues */
        uptr->Phase = Rotate_Phase;                     /*   so set up the unit for the rotate phase */
        uptr->wait = cvptr->dlyptr->intersector_gap;    /*     with a delay for the intersector time */
        }
    }

return;
}


/* Position the disc image file at the current sector.

   The image file is positioned at the byte address corresponding to the drive's
   current cylinder and the controller's current head and sector addresses.
   Positioning may involve an auto-seek if a prior read or write addressed the
   final sector of a cylinder.  If a seek is initiated or an error is detected,
   the routine returns FALSE to indicate that the positioning was not performed.
   If the file was positioned, the routine returns TRUE.

   On entry, the diagnostic override status is checked.  If it is set to
   anything other than a data error, then positioning is terminated with the
   indicated status to simulate an address verification failure.  Otherwise, if
   the controller's end-of-cylinder flag is set, a prior read or write addressed
   the final sector in the current cylinder.  If the file mask does not permit
   auto-seeking, the command is terminated with an End of Cylinder error.
   Otherwise, the cylinder is incremented or decremented as directed by the file
   mask, and a seek to the new cylinder is started.

   If the increment or decrement resulted in an out-of-bounds value, the seek
   will return Seek Check status, and the command is terminated with an error.
   Otherwise, the seek is legal, and the routine returns with the seek phase set
   to wait for seek completion before resuming the current read or write.  When
   the seek completes, the service routine will be entered, and we will be
   called again; this time, the end-of-cylinder flag will be clear and the read
   or write will continue on the new cylinder.

   If the EOC flag was not set, the drive's position is checked against the
   controller's position if address verification is requested.  If they are
   different, as may occur with an Address Record command that specified a
   different location than the last Seek command, a seek is started to the
   correct cylinder, and the routine returns with the unit set to the seek phase
   to wait for seek completion as above.

   If the drive and controller positions agree or address verification is not
   requested, the CHS addresses are validated against the drive limits.  If they
   are invalid, Seek Check status is set, and the command is terminated with an
   error.

   If the addresses are valid, the drive is checked to ensure that it is ready
   for positioning.  If it is, the file is positioned to a byte offset in the
   image file that is calculated from the CHS address.  If positioning succeeds,
   the data phase is set up to begin the data transfer, and the routine returns
   TRUE to indicate that the file position is set.  If positioning fails with a
   host file system error, it is reported to the simulation console, and the
   routine returns FALSE to indicate that an AGC (drive positioner) fault
   occurred.


   Implementation notes:

    1. The ICD controller returns an End of Cylinder error if an auto-seek
       results in a position beyond the drive limits.  The MAC controller
       returns a Status-2 error.  Both controllers set the Seek Check bit in the
       drive status word.
*/

static t_bool position_sector (CVPTR cvptr, UNIT *uptr)
{
const DRIVE_TYPE model = GET_MODEL (uptr->flags);           /* get the drive model */

if (cvptr->status != Normal_Completion                      /* if a diagnostic override is present */
  && cvptr->status != Uncorrectable_Data_Error              /*   and it's not */
  && cvptr->status != Correctable_Data_Error)               /*     a data error */
    end_command (cvptr, uptr, cvptr->status);               /*       then report it */

else if (cvptr->eoc == SET)                                     /* otherwise if we are at the end of a cylinder */
    if (cvptr->file_mask & CM_AUTO_SEEK_EN) {                   /*   then if an auto-seek is allowed */
        if (cvptr->file_mask & CM_DECR_SEEK)                    /*     then if a decremental seek is requested */
            cvptr->cylinder = cvptr->cylinder - 1 & D16_MASK;   /*       then decrease the address with wraparound */
        else                                                    /*     otherwise an incremental seek is requested */
            cvptr->cylinder = cvptr->cylinder + 1 & D16_MASK;   /*       so increase the address with wraparound */

        start_seek (cvptr, uptr);                               /* start the auto-seek */

        tpprintf (cvptr->device, DL_DEB_INCO, "Unit %d %s%s autoseek to cylinder %u head %u sector %u\n",
                  (int32) (uptr - cvptr->device->units), opcode_name [uptr->Opcode],
                  (uptr->Status & S2_SEEK_CHECK ? " seek check on" : ""),
                  cvptr->cylinder, cvptr->head, cvptr->sector);

        if (uptr->Status & S2_SEEK_CHECK)                   /* if a seek check occurred */
            if (cvptr->type == ICD)                         /*   then if this is an ICD controller */
                end_command (cvptr, uptr, End_of_Cylinder); /*     then report it as an End of Cylinder error */
            else                                            /*   otherwise */
                end_command (cvptr, uptr, Status_2_Error);  /*     report it as a Status-2 error */
        }

    else                                                    /* otherwise the file mask does not permit an auto-seek */
        end_command (cvptr, uptr, End_of_Cylinder);         /*   so terminate with an EOC error */

else if (cvptr->verify                                      /* if address verification is enabled */
  && (uint32) uptr->Cyl != cvptr->cylinder) {               /*   and the positioner is on the wrong cylinder */
    start_seek (cvptr, uptr);                               /*     then start a seek to the correct cylinder */

    tpprintf (cvptr->device, DL_DEB_INCO, "Unit %d %s%s reseek to cylinder %u head %u sector %u\n",
              (int32) (uptr - cvptr->device->units), opcode_name [uptr->Opcode],
              (uptr->Status & S2_SEEK_CHECK ? " seek check on" : ""),
              cvptr->cylinder, cvptr->head, cvptr->sector);

    if (uptr->Status & S2_SEEK_CHECK)                       /* if a seek check occurred */
        end_command (cvptr, uptr, Status_2_Error);          /*   then report a Status-2 error */
    }

else if (((uint32) uptr->Cyl >= drive_props [model].cylinders)  /* otherwise the heads are positioned correctly */
  || (cvptr->head >= drive_props [model].heads)                 /*   but if the cylinder */
  || (cvptr->sector >= drive_props [model].sectors)) {          /*     or head or sector is out of bounds */
    uptr->Status |= S2_SEEK_CHECK;                              /*       then set Seek Check status */
    end_command (cvptr, uptr, Status_2_Error);                  /*         and terminate with an error */
    }

else if (uptr->flags & UNIT_UNLOAD)                     /* otherwise if the drive is not ready for positioning */
    end_command (cvptr, uptr, Access_Not_Ready);        /*   then terminate with an access error */

else {                                                  /* otherwise we are ready to move the heads */
    set_file_pos (cvptr, uptr, model);                  /*   so calculate the new position */

    if (sim_fseek (uptr->fileref, uptr->pos, SEEK_SET)) {   /* set the image file position; if it failed */
        io_error (cvptr, uptr, Status_2_Error);             /*   then report it to the simulation console */

        dl_load_unload (cvptr, uptr, FALSE);                /* unload the heads */
        uptr->Status |= S2_FAULT;                           /*   and set Fault status */
        }

    else {                                              /* otherwise the seek succeeded */
        uptr->Phase = Data_Phase;                       /*   so set up the data transfer phase */

        if (cvptr->device->flags & DEV_REALTIME)        /* if the real time mode is enabled */
            uptr->wait = cvptr->dlyptr->data_xfer       /*   then base the delay on the sector preamble size */
                           * cmd_props [uptr->Opcode].preamble_size;
        else                                            /* otherwise */
            uptr->wait = cvptr->dlyptr->data_xfer;      /*   start the transfer with a nominal delay */

        return TRUE;                                    /* report that positioning was accomplished */
        }
    }

return FALSE;                                           /* positioning failed or was deferred */
}


/* Address the next sector.

   This routine is called after a sector has been successfully read or written
   in preparation for continuing the transfer.  It is also called after the
   Request Syndrome command returns the correction status for a sector in error.

   The controller's CHS address is incremented to point at the next sector.  If
   the next sector number is valid, the routine returns.  Otherwise, the sector
   number is reset to sector 0 and the address verification state is reset to
   enable it for a Read_Without_Verify command.  If the file mask is set for
   cylinder mode, the head is incremented, and if the new head number is valid,
   the routine returns.  If the head number is invalid, it is reset to head 0,
   and the end-of-cylinder flag is set.  The EOC flag is also set if the file
   mask is set for surface mode.

   The new cylinder address is not set here, because cylinder validation must
   only occur when the next sector is actually accessed.  Otherwise, reading or
   writing the last sector on a track or cylinder with auto-seek disabled would
   cause an End of Cylinder error, even if the transfer ended with that sector.
   Instead, we set the EOC flag to indicate that a cylinder update is pending.

   As a result of this deferred update method, the state of the EOC flag must be
   considered when returning the disc address to the CPU.
*/

static void next_sector (CVPTR cvptr, UNIT *uptr)
{
const DRIVE_TYPE model = GET_MODEL (uptr->flags);       /* get the disc model */

cvptr->sector = cvptr->sector + 1;                      /* increment the sector number */

if (cvptr->sector < drive_props [model].sectors)        /* if we at not the end of the track */
    return;                                             /*   then the next sector value is OK */

cvptr->sector = 0;                                          /* otherwise wrap the sector number */
cvptr->verify = cmd_props [uptr->Opcode].verify_address;    /*   and set the address verification flag */

if (cvptr->file_mask & CM_CYL_MODE) {                   /* if the controller is in cylinder mode */
    cvptr->head = cvptr->head + 1;                      /*   then increment the head */

    if (cvptr->head < drive_props [model].heads)        /* if we are not at the end of the cylinder */
        return;                                         /*   then the next head value is OK */

    cvptr->head = 0;                                    /* otherwise wrap the head number */
    }

cvptr->eoc = SET;                                       /* set the end-of-cylinder flag */
return;                                                 /*   to indicate that an update is required */
}


/* Start a seek.

   A seek is initiated on the indicated unit if the drive is ready and the
   cylinder, head, and sector values in the controller are valid for the current
   drive model.  The routine returns TRUE if the unit is seeking and FALSE if
   the seek failed to start.

   If the drive is not ready, the seek fails immediately with a Status-2 error.
   If the drive is already seeking, Seek Check status will occur, and the
   routine will return TRUE to allow the current seek to complete normally.

   Otherwise, a seek is initiated to cylinder 0 if the current command is
   Recalibrate or to the cylinder value stored in the controller if it is not.
   EOC is reset for a seek but not for recalibrate, so that a reseek will return
   to the same location as was current when the recalibration was done.

   If the controller cylinder is beyond the drive's limit, Seek Check status is
   set in the unit, and the heads are not moved.  Otherwise, the relative
   cylinder position change is calculated, and the heads are moved to the new
   position.

   If the controller head or sector is beyond the drive's limit, Seek Check
   status is set in the unit.  Otherwise, Seek Check status is cleared.

   In hardware, the controller issues tag bus SEEK and ADR (address record)
   commands to the drive to load the drive's cylinder, head, and sector
   registers.  On the 7905 and 7906 drives, loading the head register
   establishes the drive's Read-Only status in conjunction with the PROTECT
   UPPER/LOWER DISC switch settings.

   A seek check for either the cylinder, head, or sector terminates the current
   command for an ICD controller.  For a MAC controller, the seek check is noted
   in the drive status, but processing will continue until the drive sets
   Attention status.

   Finally, the unit is set to the seek phase, and the scheduling delay is
   calculated by the distance the heads traversed (in real time mode), or
   it is set to a fixed delay (in fast time mode).


   Implementation notes:

    1. For ICD drives, a seek check will terminate the command immediately with
       a Status-2 error.  A seek-in-progress seek check cannot occur on an ICD
       drive, however, because the second seek command will not be started until
       the first seek completes.

    2. In hardware, a seek to the current location will set Drive Busy status
       for 1.3 milliseconds (the head settling time).  In simulation, disc
       service is scheduled as though a one-cylinder seek was requested.

    3. The head register contents does not affect Read-Only status on the 7920
       or 7925, which is established solely by the switch setting.  However, we
       set the drive status here anyway as a convenience.
*/

static t_bool start_seek (CVPTR cvptr, UNIT *uptr)
{
int32 delta;
uint32 target_cylinder;
const DRIVE_TYPE model = GET_MODEL (uptr->flags);       /* get the drive model */

if (uptr->flags & UNIT_UNLOAD)                          /* if the heads are unloaded */
    return FALSE;                                       /*   then the seek fails as the drive was not ready */

else if (uptr->Phase == Seek_Phase) {                   /* otherwise if a seek is in progress */
    uptr->Status |= S2_SEEK_CHECK;                      /*   then set Seek Check status */
    return TRUE;                                        /*     and return to let the seek complete */
    }

else if (uptr->Opcode == Recalibrate)                   /* otherwise if the unit is recalibrating */
    target_cylinder = 0;                                /*   then seek to cylinder 0 and don't reset the EOC flag */

else {                                                  /* otherwise it's a Seek command or an auto-seek request */
    target_cylinder = cvptr->cylinder;                  /*   so seek to the controller cylinder */
    cvptr->eoc = CLEAR;                                 /*     and clear the end-of-cylinder flag */
    }

if (target_cylinder >= drive_props [model].cylinders) { /* if the cylinder is out of bounds */
    delta = 0;                                          /*   then don't change the positioner */
    uptr->Status |= S2_SEEK_CHECK;                      /*     and set Seek Check status */
    }

else {                                                  /* otherwise the cylinder value is OK */
    delta = abs (uptr->Cyl - (int32) target_cylinder);  /* calculate the relative movement */
    uptr->Cyl = target_cylinder;                        /*   and move the positioner */

    if (cvptr->head >= drive_props [model].heads        /* if the head */
      || cvptr->sector >= drive_props [model].sectors)  /*   or the sector is out of bounds */
        uptr->Status |= S2_SEEK_CHECK;                  /*     then set Seek Check status */

    else {                                              /* otherwise the head and sector are OK */
        uptr->Status &= ~S2_SEEK_CHECK;                 /*   so clear Seek Check status */

        if (uptr->flags &                               /* if the selected head is protected */
          (cvptr->head > 1 ? UNIT_PROT_L : UNIT_PROT_U))
            uptr->Status |= S2_READ_ONLY;               /*   then set read-only status */
        else                                            /* otherwise */
            uptr->Status &= ~S2_READ_ONLY;              /*   clear it */
        }
    }

if (uptr->Status & S2_SEEK_CHECK && cvptr->type == ICD) /* if a seek check occurred on an ICD controller */
    return FALSE;                                       /*   then the command fails immediately */

else {                                                  /* otherwise the seek was OK or this is a MAC controller */
    uptr->Phase = Seek_Phase;                           /*   so set the unit to the seek phase */

    uptr->wait = cvptr->dlyptr->seek_one                /* set the seek delay, based on the relative movement */
                   + delta * (cvptr->dlyptr->seek_full - cvptr->dlyptr->seek_one)
                   / drive_props [model].cylinders;
    }

return TRUE;                                            /* the seek is underway */
}


/* Report a stream I/O error.

   Errors indicated by the host file system are printed on the simulation
   console, and the current command is terminated with the supplied status
   indication.  The target OS will respond to the status return appropriately.
*/

static void io_error (CVPTR cvptr, UNIT *uptr, CNTLR_STATUS status)
{
cprintf ("%s simulator disc library I/O error: %s\n",   /* report the error to the console */
         sim_name, strerror (errno));

tpprintf (cvptr->device, cvptr->device->dctrl, "Host system stream I/O call failed: %s\n",
          strerror (errno));

clearerr (uptr->fileref);                               /* clear the error */

end_command (cvptr, uptr, status);                      /* terminate the command with the supplied status */
return;
}


/* Set up the controller completion.

   This routine performs a scheduled "end_command" to complete a command after a
   short delay.  It is called for commands that execute to completion with no
   drive or CPU interface interaction.  An otherwise unused "end phase" is
   scheduled just so that the command does not appear to complete
   instantaneously.
*/

static void set_completion (CVPTR cvptr, UNIT *uptr, CNTLR_STATUS status)
{
cvptr->status = status;                                 /* save the supplied status */
uptr->Phase   = End_Phase;                              /* schedule the end phase */
uptr->wait    = cvptr->dlyptr->overhead / 2;            /*   with a short delay */
return;
}



/* Disc library local utility routines */


/* Set the current controller address into the buffer.

   The controller's current cylinder, head, and sector are packed into two words
   and stored in the sector buffer, starting at the index specified.  If the
   end-of-cylinder flag is set, the cylinder is incremented to reflect the
   auto-seek that will be attempted when the next sequential access is made.


   Implementation notes:

    1. The 13037 firmware always increments the cylinder number if the EOC flag
       is set, rather than checking cylinder increment/decrement bit in the file
       mask.
*/

static void set_address (CVPTR cvptr, uint32 index)
{
cvptr->buffer [index] =                                 /* update the cylinder if EOC is set */
  (DL_BUFFER) cvptr->cylinder + (cvptr->eoc == SET ? 1 : 0);

cvptr->buffer [index + 1] =                             /* merge the head and sector */
  (DL_BUFFER) (PO_HEAD (cvptr->head) | PO_SECTOR (cvptr->sector));

return;
}


/* Return the drive status (Status-2).

   This routine returns the formatted unit status for the indicated drive unit.

   In hardware, the controller outputs the Address Unit command on the drive tag
   bus and the unit number on the drive control bus.  The addressed drive then
   responds by setting its internal "selected" flag.  The controller then
   outputs the Request Status command on the tag bug, and the selected drive
   returns its status on the control bus.  If a drive is selected but the heads
   are unloaded, the drive returns Not Ready and Busy status.  If no drive is
   selected, the control bus floats inactive.  This is interpreted by the
   controller as Not Ready status (because the drive returns an inactive Ready
   status).

   In simulation, an enabled but detached unit corresponds to "selected but
   heads unloaded," and a disabled unit corresponds to a non-existent unit.


   Implementation notes:

    1. The Attention, Read-Only, First Status, Fault, and Seek Check bits are
       stored in the unit status field.  The other status bits are determined
       dynamically.

    2. The Drive Busy bit is set if the unit is in the seek phase.  In hardware,
       this bit indicates that the heads are not positioned over a track, i.e.,
       that a seek is in progress.  A Request Status command is accepted only
       when the controller is waiting for seek completion or for a new command.
       Therefore, the unit will be either in the seek phase or the idle phase,
       respectively, when status is returned.
*/

static HP_WORD drive_status (UNIT *uptr)
{
HP_WORD status;

if (uptr == NULL)                                       /* if the unit is invalid */
    return S2_ERROR | S2_NOT_READY;                     /*   then it does not respond */

status =                                                /* start with the drive type and unit status */
  S2_DRIVE_TYPE (GET_MODEL (uptr->flags)) | uptr->Status;

if (uptr->flags & UNIT_FMT)                             /* if the format switch is enabled */
    status |= S2_FORMAT_EN;                             /*   then set the Format status bit */

if (uptr->flags & UNIT_DIS)                             /* if the unit does not exist */
    status |= S2_NOT_READY;                             /*   then set the Not Ready bit */

else if (uptr->flags & UNIT_UNLOAD)                     /* if the heads are unloaded */
    status |= S2_NOT_READY | S2_BUSY;                   /*   then set the Not Ready and Drive Busy bits */

if (uptr->Phase == Seek_Phase)                          /* if a seek is in progress */
    status |= S2_BUSY;                                  /*   then set the Drive Busy bit */

if (status & S2_ERRORS)                                 /* if there any Status-2 errors */
    status |= S2_ERROR;                                 /*   then set the Error summary bit */

return status;                                          /* return the unit status */
}


/* Activate the unit.

   The specified unit is activated using the unit's "wait" time.  If tracing
   is enabled, the activation is logged to the debug file.


   Implementation notes:

    1. The "%.0u" print specification in the trace call absorbs the zero "unit"
       value parameter without printing when the controller unit is specified.
*/

static t_stat activate_unit (CVPTR cvptr, UNIT *uptr)
{
t_stat result;
const int32 unit = (int32) (uptr - cvptr->device->units);   /* the unit number */

tpprintf (cvptr->device, DL_DEB_SERV, (unit == CNTLR_UNIT
                                         ? "Controller unit%.0d %s %s phase delay %d service scheduled\n"
                                         : "Unit %d %s %s phase delay %d service scheduled\n"),
          (unit == CNTLR_UNIT ? 0 : unit), opcode_name [uptr->Opcode],
          phase_name [uptr->Phase], uptr->wait);

result = sim_activate (uptr, uptr->wait);               /* activate the unit */
uptr->wait = NO_EVENT;                                  /*   and reset the activation time */

return result;                                          /* return the activation status */
}


/* Set up the rotation phase.

   The supplied unit is set to the rotate phase at the start of a read or write
   command.  In real time mode, the rotational latency is determined by the
   distance between the "current" sector location and the target sector
   location.  The former is estimated from the current "simulation time," which
   is the number of event ticks since the simulation run was started, and the
   simulated disc rotation time, as follows:

     (simulation_time / per_sector_time) MOD sectors_per_track

   The distance is then:

     (sectors_per_track + target_sector - current_sector) MOD sectors_per_track

   ...and the latency is then:

     distance * per_sector_time

   In fast time mode, the latency is fixed at the specified per-sector time.
*/

static void set_rotation (CVPTR cvptr, UNIT *uptr)
{
uint32 sectors_per_track;
double distance;

uptr->Phase = Rotate_Phase;                             /* set the phase */

if (cvptr->device->flags & DEV_REALTIME) {              /* if the mode is real time */
    sectors_per_track =                                 /*   then calculate the latency as above */
      drive_props [GET_MODEL (uptr->flags)].sectors;

    distance =
      fmod (sectors_per_track + cvptr->sector - CURRENT_SECTOR (cvptr, uptr),
            sectors_per_track);

    uptr->wait = (int32) (cvptr->dlyptr->sector_full * distance);
    }

else                                                    /* otherwise the mode is fast time */
    uptr->wait = cvptr->dlyptr->sector_full;            /*   so use the specified time directly */
}


/* Set the image file position.

   A cylinder/head/sector address is converted into a byte offset to pass to the
   host file I/O routines.  The cylinder is supplied by the drive unit, and the
   head and sector addresses are supplied by the controller.  The disc image
   file is laid out in one or two pieces, depending on whether a fixed platter
   is present in the drive.  If it is, then the area corresponding to the
   removable platter precedes the area corresponding to the fixed platter.  If
   not, then the file contains a single area encompassing all of the (removable)
   heads.

   In either case, the target track within the area is:

     cylinder * heads_per_cylinder + head

   ...and the target byte position in the file is:

     (target_track * sectors_per_track + sector) * bytes_per_sector
*/

static void set_file_pos (CVPTR cvptr, UNIT *uptr, uint32 model)
{
uint32 track;

if (cvptr->head < drive_props [model].remov_heads)              /* if the head is on a removable platter */
    track = uptr->Cyl * drive_props [model].remov_heads         /*   then the tracks in the file are contiguous */
              + cvptr->head;

else                                                            /* otherwise the head is on a fixed platter */
    track = drive_props [model].cylinders                       /*   so the target track is located */
              * drive_props [model].remov_heads                 /*     in the second area */
              + uptr->Cyl * drive_props [model].fixed_heads     /*       that is offset from the first */
              + cvptr->head - drive_props [model].remov_heads;  /*         by the size of the removable platter */

uptr->pos = (track * drive_props [model].sectors + cvptr->sector)   /* set the byte offset in the file */
              * WORDS_PER_SECTOR * sizeof (DL_BUFFER);              /*   of the CHS target sector */

return;
}
